# CSS Without Tears

> CSS looks like a pile of unrelated rules until you learn the handful of ideas underneath it - selectors, the box model, units, and positioning. This guide builds all four on one running example.


---

# CSS Without Tears

You just finished `html-from-zero` and you have a plain "About Me" page: black text, blue links,
default font, top to bottom, no personality. That page is correct HTML and it looks like it's from
1998. CSS is what turns it into something you'd actually want someone to see.

The reason CSS feels chaotic at first is that it looks like a flat list of rules with no logic
connecting them - until you learn the small set of ideas everything else builds on. Which selector
wins when two rules disagree. What "the box model" means for every single element on the page. Why
`rem` behaves differently from `px`. What `position` actually repositions relative to. Once those four
ideas click, the rest of CSS is vocabulary you can look up as needed.

This guide styles that one "About Me" page from nothing to a finished layout, one phase at a time.

## How to read this
- **Want it to finally make sense?** Read in order - each phase styles more of the same page and
  depends on the last.
- **Already comfortable with basics, need positioning?** Jump straight to
  [Phase 5: Positioning](05-positioning.md).

## The phases

1. **[Selectors and the Cascade](01-selectors-and-the-cascade.md)** - how CSS picks which rule wins,
   what inherits from parent to child, and when `!important` is a legitimate escape hatch.
2. **[The Box Model](02-the-box-model.md)** - what padding, border, and margin actually do to an
   element's size, and why `box-sizing: border-box` is in almost every real stylesheet.
3. **[Margin Collapse](03-margin-collapse.md)** - why two stacked margins become the larger one
   instead of their sum, and why a child's margin sometimes escapes its parent entirely.
4. **[Colors, Units, and Typography](04-colors-units-and-typography.md)** - hex vs. rgb vs. hsl, why
   `rem` beats `px` for font sizes, and the line-height mistake almost everyone makes.
5. **[Positioning](05-positioning.md)** - `static`, `relative`, `absolute`, `fixed`, and `sticky`,
   worked through a sticky header and a centered modal.

Real layout systems - lining elements up in rows, columns, and grids - are their own guide:
[Flexbox and Grid](/guides/flexbox-and-grid). This guide is what you need before that one makes sense.


---

# Selectors and the Cascade

Here's your About Me page right now:

```html
<body>
  <h1>Nikola Petrova</h1>
  <p class="tagline">Backend developer, coffee enthusiast, occasional hiker.</p>
  <p id="intro">I build APIs for a living and break them for fun on weekends.</p>
  <a href="mailto:nikola@example.com">Email me</a>
</body>
```

Default browser styling: black serif text, blue underlined link, no spacing worth mentioning. Every
change from here starts with a CSS rule - a selector telling the browser *which* elements to touch, and
a declaration telling it *what* to change.

```css
h1 {
  color: #1a1a2e;
}
```

This says: find every `h1`, make its text that color. `h1` is the selector, `color: #1a1a2e` is the
declaration. Simple until two rules disagree about the same element - which is where most CSS confusion
actually comes from.

## Selector types

**Element selectors** target a tag name: `p { }` hits every paragraph. Good for broad defaults.

**Class selectors** target a `class` attribute, written with a dot: `.tagline { }` hits only elements
with `class="tagline"`. Classes are reusable - put the same class on ten elements, style them all at
once.

**ID selectors** target an `id` attribute, written with a hash: `#intro { }` hits the one element with
`id="intro"`. IDs must be unique per page, so this only ever matches one thing.

**Attribute selectors** target an attribute directly, no class or ID needed:
`a[href^="mailto:"] { }` matches any link whose `href` starts with `mailto:`. Useful for styling
external links, specific input types, or anything else identifiable by its HTML attributes without
adding a class.

```css
p { line-height: 1.6; }
.tagline { font-style: italic; }
#intro { font-weight: bold; }
a[href^="mailto:"] { color: #d64550; }
```

*What just happened:* every `<p>` got more breathing room between lines. The tagline paragraph also got
italics, because it has both a `p` rule and a `.tagline` rule applying to it - CSS doesn't pick one, it
merges every rule that matches. The intro paragraph got bold on top of its line-height. The email link
turned red because its `href` matches the attribute selector.

## The cascade: what wins when rules conflict

Merging rules is fine until two rules set the *same* property on the *same* element with different
values. Say your stylesheet has both:

```css
p { color: black; }
.tagline { color: gray; }
```

The tagline paragraph matches both. Gray wins. Not because it's written second - because a class is more
specific than an element selector, and **more specific wins**. That's the entire rule. Specificity is a
pecking order, roughly:

1. Inline `style="..."` attributes (most specific)
2. IDs (`#intro`)
3. Classes, attribute selectors, pseudo-classes (`.tagline`, `[href]`, `:hover`)
4. Element selectors (`p`, `h1`)

An ID beats any number of classes. A class beats any number of element selectors. If two rules have
equal specificity, the one later in the stylesheet wins - order only matters as a tiebreaker, not a
first resort.

💡 **Key point.** Don't count specificity points like a scoring game. Remember the pecking order:
inline beats ID beats class beats element. When a style refuses to apply, check whether something more
specific is overriding it - that's the fix in nearly every case.

⚠️ **The gotcha.** `!important` jumps the entire queue - it beats specificity, not just ties with it.
```css
p { color: black !important; }
```
This wins over the ID and the inline style both. That's exactly the problem: once you reach for
`!important`, the only way to override it later is another `!important`, and now you're in an arms race.
Legitimate uses are narrow - overriding a third-party library's inline styles you can't edit, or a
utility class that must always win regardless of context. Reaching for it because you can't figure out
why your selector lost is a code smell: fix the specificity instead.

## Inheritance: some properties pass down, some don't

Some CSS properties inherit from parent to child automatically. Set `color` on `<body>` and every
paragraph, heading, and span inside it inherits that color unless something overrides it. Text-related
properties inherit: `color`, `font-family`, `font-size`, `line-height`, `text-align`.

Box- and layout-related properties don't inherit: `border`, `margin`, `padding`, `background`, `width`.
That's deliberate - if `border` inherited, setting a border on `<body>` would put a border around every
element on the page.

```css
body {
  color: #333;
  font-family: Georgia, serif;
  border: 1px solid red;
}
```

*What just happened:* every piece of text on the page turned dark gray and switched to Georgia -
`color` and `font-family` inherited straight down. Only `<body>` itself got the red border, because
`border` doesn't inherit.

📝 **Terminology.** If you ever need to force inheritance on a property that doesn't do it by default,
the value `inherit` does that explicitly: `border: inherit;` copies the parent's border. Rare, but it
exists for exactly this case.

## Recap

1. Element, class, ID, and attribute selectors each target elements differently - classes are the
   workhorse for reusable styling.
2. When rules conflict, the more specific selector wins: inline > ID > class > element. Order only
   breaks ties between equal specificity.
3. `!important` overrides specificity entirely - save it for overriding styles you can't otherwise
   touch, not for winning an argument with your own stylesheet.
4. Text properties (`color`, `font-family`, `line-height`) inherit down the tree. Box properties
   (`border`, `margin`, `padding`) don't.

Test what you just learned:

```quiz
[
  {
    "q": "A paragraph has both `p { color: black; }` and `.tagline { color: gray; }` applied. What color is it?",
    "choices": ["black, because element selectors are checked first", "gray, because a class is more specific than an element selector", "It depends on which rule appears first in the file"],
    "answer": 1,
    "explain": "Classes outrank element selectors regardless of order. Order only breaks ties between rules of equal specificity."
  },
  {
    "q": "Which of these inherits from a parent to its children by default?",
    "choices": ["border", "margin", "color", "padding"],
    "answer": 2,
    "explain": "Text-related properties like color, font-family, and line-height inherit. Box-model properties like border, margin, and padding do not."
  },
  {
    "q": "Why is `!important` considered a code smell when overused?",
    "choices": ["It's slower for the browser to process", "It breaks the normal specificity order, so future overrides need another !important", "It only works on class selectors"],
    "answer": 1,
    "explain": "Once a rule uses !important, only another !important can override it - that escalation is what makes stylesheets hard to maintain."
  }
]
```


---

# The Box Model

Every element on your About Me page - the `h1`, the paragraphs, the link - is a rectangle, whether it
looks like one or not. CSS calls that rectangle "the box," and it's made of four layers stacked outward
from the text: content, padding, border, margin. Understanding this box is the difference between
guessing at spacing and controlling it.

```css
.tagline {
  padding: 12px;
  border: 2px solid #1a1a2e;
  margin: 20px 0;
}
```

*What just happened:* the tagline text now sits 12px away from its own border on every side (padding),
wrapped in a visible 2px line (border), with 20px of empty space above and below separating it from
neighboring elements (margin). Three different layers, three different jobs.

## Content, padding, border, margin

**Content** is the text or child elements themselves - what you'd see with every other layer stripped
away.

**Padding** is space *inside* the border, between the content and the box's edge. It's part of the
element - if the element has a background color, padding shows that color.

**Border** is the visible edge of the box. It sits between padding and margin.

**Margin** is space *outside* the border, pushing other elements away. Margins are transparent - you
never see a background color there, only whatever's behind the element.

```mermaid
flowchart TD
  M["margin (outside, transparent)"] --> B["border (the visible edge)"]
  B --> P["padding (inside, same background as content)"]
  P --> C["content (text or children)"]
```

Think of a framed photo on a wall: the photo is content, the mat around it is padding, the frame is
border, and the gap of wall between it and the next photo is margin.

## The default that surprises everyone: box-sizing

Here's the part that trips up nearly every beginner. By default, `width` only sets the content box -
padding and border get *added on top*, making the element bigger than the width you asked for.

```css
.card {
  width: 300px;
  padding: 20px;
  border: 2px solid black;
}
```

You'd expect a 300px-wide box. You get 344px: 300 (content) + 20 + 20 (padding, both sides) + 2 + 2
(border, both sides). This default is called `content-box`, and it's the box-sizing value every browser
starts with.

⚠️ **The gotcha.** Add padding to fix spacing, and your carefully-measured layout shifts and wraps
because every box quietly grew. This is the single most common "why is my CSS broken" moment for
beginners, and it isn't a bug - it's the spec working as documented, in a way that surprises almost
everyone the first time.

The fix, and one nearly every real stylesheet includes, is switching to `border-box`: `width` then
means the *total* width, padding and border included, and content shrinks to make room.

```css
* {
  box-sizing: border-box;
}
```

*What just happened:* every element on the page now measures padding and border as part of its stated
width instead of adding to it. That `.card` above is now genuinely 300px wide, full stop. This one rule,
applied globally at the top of a stylesheet, is close to a universal default in production CSS - there's
rarely a reason to keep fighting `content-box`.

## display: block vs. inline vs. inline-block

Every element also has a `display` value that decides how it sits next to its neighbors.

**`block`** elements (`div`, `p`, `h1`) take the full width available and stack vertically - each one
starts on a new line. Width, height, padding, and margin all apply exactly as you'd expect.

**`inline`** elements (`span`, `a`, `strong`) flow with the text, sitting side by side like words in a
sentence. `width` and `height` are ignored entirely, and top/bottom margin and padding don't push
neighboring content away - only left/right spacing works.

**`inline-block`** is the middle ground: it flows inline like text, but respects `width`, `height`, and
margin/padding on every side, like a block does.

```css
a {
  display: inline-block;
  padding: 8px 16px;
  background: #1a1a2e;
  color: white;
}
```

*What just happened:* the "Email me" link now has real clickable padding around it and a background
color that actually shows on all sides - none of that works reliably on a plain `inline` element, because
top/bottom padding on `inline` doesn't affect surrounding layout. Switching to `inline-block` keeps the
link sitting next to text while giving it a proper button-like box.

## Recap

1. Every element is a box: content, then padding, then border, then margin, from the inside out.
2. Padding shares the element's background; margin is always transparent space between elements.
3. Default `box-sizing: content-box` adds padding and border on top of `width`. `box-sizing: border-box`
   makes `width` the total size - set it globally with `* { box-sizing: border-box; }`.
4. `display: block` stacks full-width, `inline` flows with text but ignores width/height, `inline-block`
   flows with text while respecting both.

Test what you just learned:

```quiz
[
  {
    "q": "An element has `width: 200px; padding: 10px; box-sizing: content-box;`. What's its total rendered width, ignoring border?",
    "choices": ["200px", "210px", "220px"],
    "answer": 2,
    "explain": "content-box adds padding on top of width on both sides: 200 + 10 + 10 = 220px."
  },
  {
    "q": "What does `box-sizing: border-box` change?",
    "choices": ["Margin becomes part of the width", "Padding and border are included inside the stated width instead of adding to it", "Borders become invisible"],
    "answer": 1,
    "explain": "With border-box, width is the total size - the content area shrinks to make room for padding and border."
  },
  {
    "q": "Why doesn't `height: 40px` do anything on a `span` by default?",
    "choices": ["span elements can't have height in any browser", "span is inline by default, and inline elements ignore width/height", "height only works on the body element"],
    "answer": 1,
    "explain": "Inline elements flow with text and ignore width/height. Switching to inline-block or block makes them apply."
  }
]
```


---

# Margin Collapse

You gave your tagline `margin: 20px 0` in the last phase, and the bio paragraphs
below it each have a top and bottom margin too. So the gap between two paragraphs
should be the bottom margin of one plus the top margin of the next, right?

It isn't. Measure it and you get the *larger* of the two, not the sum. And
sometimes it's stranger than that: you put a margin on the top of a heading
inside a colored box, and instead of pushing the heading down *inside* the box,
the margin shoves the *whole box* down and leaves the heading jammed against the
top edge. Nothing errored. The number you typed is just landing somewhere you
didn't expect.

This is **margin collapse**, and it is not a bug. It is a deliberate rule from
the earliest days of CSS, built for a good reason, that surprises absolutely
everyone the first time it bites. Once you can see it, it stops being spooky.

## What margin collapse actually is

When two vertical margins touch, they don't add together. They **collapse** into
a single margin, and that single margin is the larger of the two. Only vertical
margins do this - `margin-top` and `margin-bottom`. Left and right margins never
collapse.

That's the whole rule. The confusion comes entirely from the three different
situations where two vertical margins end up touching.

## Case 1: two stacked elements

The everyday one. Two paragraphs, one after the other:

```css
.bio p {
  margin: 20px 0; /* 20px top and bottom on every bio paragraph */
}
```

```console
first  paragraph  ──────────┐
                            │  margin-bottom: 20px  ┐
                            │                        ├─ these overlap; you get 20px,
second paragraph ───────────┘  margin-top: 20px     ┘   not 40px
```

*What just happened:* the bottom margin of the first paragraph and the top margin
of the second occupy the *same* gap instead of stacking. Both are 20px, so the
gap is 20px. Make one of them 30px and the gap becomes 30px - the larger wins,
the smaller is absorbed into it.

**Why it works this way.** CSS was born for documents - articles, not app
dashboards. Give every paragraph `margin: 1em 0` and, without collapsing, the
space between paragraphs would be `2em` while the space above the first would be
`1em`, so the gaps wouldn't match. Collapsing makes every gap a consistent `1em`.
It was the right call for text, and it's still the behavior you're standing on
every time a stack of paragraphs looks evenly spaced.

## Case 2: the margin that escapes its parent

This is the one that eats an afternoon. Put a heading with a top margin inside a
box that has no padding and no border:

```css
.note        { background: #fff3cd; }       /* no padding, no border */
.note h2     { margin-top: 24px; }
```

You expect 24px of yellow above the heading, inside the box. Instead:

```console
    ↑ 24px of margin, now OUTSIDE the box
┌───────────────────────┐  ← the box starts here, shoved down 24px
│ Before you deploy      │  ← heading jammed against the top, no gap above it
│ Run the migration...   │
└───────────────────────┘
```

*What just happened:* the heading's `margin-top` had nothing between it and the
top edge of the box - no padding, no border - so it collapsed straight *through*
the box's edge and came out the other side. The margin now sits above the box and
pushes the whole thing down. The yellow background doesn't grow to include it,
because margins are always transparent and always outside the element (last
phase). Measure the box and it's 24px shorter than you'd expect, sitting 24px
lower than you'd expect.

⚠️ **The gotcha.** This is why "there's a mysterious gap above my card" and "my
card's background won't fill the space above its title" are the same bug. The
margin you put on the child leaked out of the parent. You'll hunt through the
parent's styles for a padding or a position bug and find nothing, because the
call is coming from the child.

## The fix: give the margin something to touch

The margin escapes only because nothing sits between it and the parent's edge. Put
*anything* there and it can't collapse through. Any one of these fixes it:

```css
.note { padding-top: 1px; }        /* padding between edge and child */
.note { border-top: 1px solid; }   /* a border does it too */
.note { overflow: auto; }          /* establishes a block formatting context */
.note { display: flow-root; }      /* the modern, side-effect-free way */
```

*What just happened:* each of these stops the collapse, and the 24px margin now
sits *inside* the box where you wanted it - the box grows by 24px and its
background fills the gap. `padding-top` and `border-top` work by physically
sitting between the edge and the child. The other two work by a deeper mechanism:

📝 **Block formatting context (BFC).** A BFC is a box that keeps its own layout to
itself - and one of its rules is that its margins do *not* collapse with its
children's. `overflow` (anything but `visible`), `display: flow-root`, and being
a flex or grid item all turn an element into a BFC. That's the single reason all
those different-looking fixes work: they each make the parent a BFC. When you
want to stop parent-child collapse on purpose, `display: flow-root` is the clean
choice - it was added to CSS for exactly this, with none of the side effects that
`overflow` or a fake `1px` border drag along.

## When margins do NOT collapse

Collapsing is a normal-flow behavior. Step outside normal flow and it stops:

- **Flex and grid items don't collapse.** The moment a container is `display: flex`
  or `display: grid`, its children's margins are left alone. Stack two items with
  `margin: 20px 0` in a flex column and you get the full 40px gap.
- **Horizontal margins never collapse** - only `margin-top`/`margin-bottom`.
- **Floated and absolutely-positioned elements don't collapse** their margins with
  anything.

💡 **Key point.** If margins are behaving *predictably* - adding up the way you'd
expect - you're almost always inside a flex or grid container, which is most modern
layout. Margin collapse is a normal-document-flow rule, and the layout systems in
the next guide quietly switch it off.

## Recap

1. Touching vertical margins collapse into one - the larger of the two, never the
   sum. Only `margin-top`/`margin-bottom`, never left/right.
2. Between two stacked siblings, that's why the gap is the bigger margin, not both
   added together. It exists so stacked paragraphs get even spacing.
3. A child's top or bottom margin can collapse straight *through* a parent that has
   no padding or border between them, pushing the whole parent instead of adding
   space inside it.
4. Stop the parent-child version with padding, a border, `overflow`, or the
   purpose-built `display: flow-root` - all of which make the parent a block
   formatting context.
5. Flex and grid items don't collapse margins at all.

Test what you just learned:

```quiz
[
  {
    "q": "Two stacked block elements: the first has `margin-bottom: 30px`, the second has `margin-top: 10px`. What's the gap between them?",
    "choices": ["40px", "30px", "10px"],
    "answer": 1,
    "explain": "Touching vertical margins collapse to the larger of the two. 30 wins; the 10px is absorbed into it."
  },
  {
    "q": "A `div` with a background color but no padding or border wraps an `h2` that has `margin-top: 32px`. Where does the 32px go?",
    "choices": ["Inside the div, as a gap above the h2", "Outside the div, pushing the whole div down 32px", "It's ignored because the div has no height set"],
    "answer": 1,
    "explain": "With nothing between the div's edge and the h2, the margin collapses through the edge and ends up outside, shoving the div down. The background doesn't grow to cover it."
  },
  {
    "q": "Which of these does NOT stop a child's margin from escaping its parent?",
    "choices": ["Adding padding-top to the parent", "Setting the parent to display: flow-root", "Setting margin-top on the parent to 0"],
    "answer": 2,
    "explain": "The escape is about the child's margin touching the parent's edge. Padding, a border, or a block formatting context (flow-root, overflow) all stop it; changing the parent's own margin does nothing."
  }
]
```


---

# Colors, Units, and Typography

Your About Me page has structure and boxes now, but every measurement so far has been an arbitrary
pixel number, and every color has been a name or a hex code you didn't think about. This phase is about
choosing those numbers on purpose - what unit to reach for, and why the "obvious" choice for font size
is usually the wrong one.

## Three ways to write a color

```css
h1 { color: #1a1a2e; }
h1 { color: rgb(26, 26, 46); }
h1 { color: hsl(240, 28%, 14%); }
```

All three produce the exact same dark navy. **Hex** (`#1a1a2e`) packs red, green, and blue into
pairs of hexadecimal digits (00-ff each) - compact, and what design tools usually hand you. **rgb()**
spells out the same three channels in plain decimal (0-255) - easier to read and to tweak one channel
at a time. **hsl()** uses hue (0-360, a position on the color wheel), saturation (0-100%), and lightness
(0-100%) - the one that matches how humans actually think about color: "same hue, but darker" is a
one-number change in hsl, not a hex-code guessing game.

Both `rgb()` and `hsl()` accept a fourth value for transparency: `rgb(26, 26, 46, 0.5)` is the same
navy at 50% opacity.

📝 **Terminology.** `background-color` paints the padding and content area (not margin - margin is
always transparent, per Phase 2). `color` sets text color. The two names look alike but control
different things.

## px, %, em, and rem

```css
.tagline {
  font-size: 18px;
  padding: 1em;
  width: 90%;
}
```

**`px`** is an absolute pixel. Predictable, but fixed - it ignores any font-size preference the reader
set in their own browser.

**`%`** is relative to the parent element's corresponding size - `width: 90%` means 90% of the parent's
width.

**`em`** is relative to the *current element's own* `font-size`. This makes `em` compound: if a parent
has `font-size: 20px` and a child has `padding: 1em`, that's 20px of padding. But if the child also sets
its own `font-size: 1.5em`, later `em` values on that same child are relative to the *new* size, and
nested `em` values multiply through the tree - which is exactly why deeply nested `em` sizing gets
confusing fast.

**`rem`** ("root em") is always relative to the root `<html>` element's font-size, ignoring how deeply
nested you are. No compounding, no surprises.

⚠️ **The gotcha.** `em` for font-size specifically is the one that bites people: three levels of nested
`em` font-sizes multiply together, and a "small" 0.9em on each level quietly shrinks to illegibly tiny
text by the fourth generation. `rem` doesn't have this problem because it never looks at its parent.

💡 **Key point.** Use `rem` as your default for font sizes. Browsers let users set a base font size
(commonly for low vision or personal preference) and `rem` values scale with that setting automatically -
`px` font sizes stay locked at whatever you hard-coded, ignoring the reader's accessibility settings
entirely. `rem` is also the right choice for most spacing (`margin`, `padding`) so your layout scales
proportionally if the base size ever changes. `%` still earns its place for widths relative to a
container, and `px` is fine for things that should never scale, like a 1px border.

```css
html { font-size: 100%; }        /* respects the browser/OS default, usually 16px */
h1   { font-size: 2rem; }         /* 32px, unless the reader changed their base size */
p    { font-size: 1rem; }         /* 16px, same logic */
```

## Font stacks: always have a fallback

```css
body {
  font-family: "Helvetica Neue", Arial, sans-serif;
}
```

*What just happened:* the browser tries "Helvetica Neue" first. If that font isn't installed - common on
Windows and Linux, where it usually isn't - it falls back to Arial, then to the browser's generic
sans-serif if neither exists. A font-family without a fallback chain means some fraction of your readers
silently get the browser's default serif font instead of your design.

The last entry should always be a generic family: `serif`, `sans-serif`, `monospace`. That's the
guaranteed floor - every browser has one.

## line-height: the cramped-text mistake

```css
p {
  font-size: 1rem;
  line-height: 1.6;
}
```

`line-height` sets the vertical space a line of text occupies - effectively the gap between baselines.
Leave it unset and browsers use a default around 1.1-1.2, which is fine for a single short line and
cramped for a paragraph. Multi-line body text with tight line-height is measurably harder to read: the
eye has trouble tracking back to the start of the next line.

⚠️ **The gotcha.** Beginners either leave `line-height` at the browser default (too tight for paragraphs)
or set it in `px` (which stops scaling if `font-size` changes, defeating the point). Use a unitless
number like `1.5` or `1.6` - it's a *multiplier* of the element's own font-size, so it scales
automatically if the font-size ever changes, including via a reader's `rem`-driven base font setting.

## Recap

1. Hex, rgb(), and hsl() all describe the same colors - hsl() is easiest to reason about when adjusting
   one property like lightness.
2. `px` is fixed, `%` is relative to the parent, `em` is relative to the current element's own font-size
   (and compounds when nested), `rem` is relative to the root and never compounds.
3. Default to `rem` for font sizes - it respects the reader's browser font-size setting, which `px`
   ignores.
4. Always give `font-family` a fallback chain ending in a generic family.
5. Set `line-height` explicitly, as a unitless multiplier like `1.5`, or paragraphs read as cramped.

Test what you just learned:

```quiz
[
  {
    "q": "Why is rem usually the better default than px for font-size?",
    "choices": ["rem renders faster in the browser", "rem scales with the reader's browser font-size setting; px ignores it", "rem is shorter to type"],
    "answer": 1,
    "explain": "px locks text at a fixed size regardless of accessibility settings. rem respects the root font-size, which the reader can change."
  },
  {
    "q": "A child element has `font-size: 1.5em` and its parent also has `font-size: 1.5em` relative to a 16px root. What is the child's actual font-size?",
    "choices": ["24px", "36px", "16px"],
    "answer": 1,
    "explain": "em compounds: parent is 16 * 1.5 = 24px, child is 24 * 1.5 = 36px. This is the nested-em trap rem avoids."
  },
  {
    "q": "Paragraph text looks visually cramped with lines almost touching. What's the likely fix?",
    "choices": ["Increase font-size only", "Set line-height to a unitless value like 1.5 or 1.6", "Switch font-family to a monospace font"],
    "answer": 1,
    "explain": "The browser's default line-height (around 1.1-1.2) is too tight for multi-line body text. A unitless line-height like 1.5 fixes it and scales with font-size."
  }
]
```


---

# Positioning

Everything so far has followed normal document flow - each box stacks below the last, in source order.
`position` is how you pull an element out of that flow, or anchor it to something other than "wherever
it landed." Five values, each answering "positioned relative to what?"

## The five values

**`static`** is the default. No repositioning, `top`/`left`/`right`/`bottom` do nothing. Every element
is `static` until you say otherwise.

**`relative`** keeps the element in normal flow - it still takes up its original space - but lets you
nudge it with `top`/`left`/`right`/`bottom`, offset from where it *would* have been.

```css
.badge {
  position: relative;
  top: -4px;
}
```

*What just happened:* `.badge` shifted up 4px visually, but the space it originally occupied in the
layout is still reserved - other elements don't move to fill the gap.

**`absolute`** removes the element from normal flow entirely and positions it relative to its nearest
ancestor that has a `position` other than `static`. If no ancestor qualifies, it falls back to the
`<html>` element - which is almost never what you want, and the most common reason `position: absolute`
"doesn't work."

**`fixed`** positions relative to the browser viewport itself, ignoring scrolling entirely - it stays
glued to the same spot on screen as the page scrolls underneath it.

**`sticky`** behaves like `relative` until the page scrolls past a threshold you set (`top: 0`, for
instance), then it locks in place like `fixed` - but only within its parent's boundaries. Scroll the
parent out of view and the sticky element scrolls away with it.

## Worked example 1: a sticky header

Add a header to your About Me page that stays visible while you scroll the bio text below it.

```css
header {
  position: sticky;
  top: 0;
  background: white;
  padding: 12px 20px;
  border-bottom: 1px solid #ddd;
}
```

*What just happened:* while the page is scrolled to the top, the header sits in normal flow, exactly
where its HTML position puts it. Scroll down, and the moment the header would scroll past `top: 0`, it
sticks there instead - staying visible above the content scrolling underneath. No JavaScript, no manual
scroll-tracking.

⚠️ **The gotcha.** `position: sticky` silently does nothing if any ancestor has `overflow: hidden`,
`overflow: auto`, or a fixed `height` that clips it - the sticky element can't escape a container that
doesn't let its content overflow. If your sticky header refuses to stick, check every parent up the
tree for an `overflow` rule.

## Worked example 2: a centered modal overlay

A modal needs two things: a dark backdrop covering the whole screen, and a centered box on top of it.
This is the pairing that makes `absolute` click - a positioned parent, and an absolutely positioned
child anchored to it.

```html
<div class="modal-backdrop">
  <div class="modal">
    <p>Thanks for visiting my page!</p>
  </div>
</div>
```

```css
.modal-backdrop {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(0, 0, 0, 0.5);
}

.modal {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  background: white;
  padding: 24px;
  border-radius: 8px;
}
```

*What just happened:* `.modal-backdrop` uses `fixed` to cover the entire viewport regardless of scroll
position, dimmed by a semi-transparent black background. `.modal` uses `absolute`, and because its
parent `.modal-backdrop` has `position: fixed` (not `static`), the modal positions relative to *that*
backdrop instead of falling back to `<html>`. `top: 50%; left: 50%` puts its top-left corner at the
backdrop's center - then `transform: translate(-50%, -50%)` shifts it back by half its own width and
height, so the modal's actual center lands on the backdrop's center, not its corner.

💡 **Key point.** `position: absolute` only does something useful once you've deliberately given some
ancestor a non-static position to anchor to. That pairing - "positioned parent, absolutely positioned
child" - is the pattern behind dropdowns, tooltips, badges on avatars, and modals alike.

## Recap

1. `static` is the default - no offsets apply.
2. `relative` nudges an element while keeping its original space reserved.
3. `absolute` removes the element from flow and anchors it to the nearest non-static ancestor (or
   `<html>` if none exists).
4. `fixed` anchors to the viewport and ignores scrolling.
5. `sticky` is `relative` until a scroll threshold, then behaves like `fixed` within its parent - breaks
   silently under `overflow: hidden` on an ancestor.
6. Centering with `absolute` needs three things together: a positioned parent, `top/left: 50%`, and
   `transform: translate(-50%, -50%)`.

Test what you just learned:

```quiz
[
  {
    "q": "An element has `position: absolute` and none of its ancestors have a position set. What does it position relative to?",
    "choices": ["Its immediate parent, always", "The <html> element", "It stays in normal flow like static"],
    "answer": 1,
    "explain": "absolute anchors to the nearest ancestor with a non-static position. With no qualifying ancestor, it falls back to the html element - the most common cause of absolute positioning behaving unexpectedly."
  },
  {
    "q": "A sticky header isn't sticking - it scrolls away like a normal element. What's the most likely cause?",
    "choices": ["sticky doesn't exist as a real value", "An ancestor has overflow: hidden or auto, which breaks sticky", "top: 0 was set instead of top: 100%"],
    "answer": 1,
    "explain": "position: sticky is silently disabled by an ancestor with overflow set to anything other than visible."
  },
  {
    "q": "Why does the centered modal need `transform: translate(-50%, -50%)` in addition to `top: 50%; left: 50%`?",
    "choices": ["top/left alone centers the element's top-left corner, not its actual center", "transform is required for position: absolute to work at all", "It's purely a decorative animation"],
    "answer": 0,
    "explain": "top: 50%; left: 50% places the corner at the midpoint. Shifting back by half the element's own width and height centers the element itself."
  }
]
```

Positioning moves individual elements around, but it isn't how you build real page layouts - rows,
columns, and grids of content are their own tool. That's
[Flexbox and Grid](/guides/flexbox-and-grid), the natural next guide from here.
