---
title: 'Visual Presentation with CSS'
source: 'https://academia.sh/en/courses/css-fundamentals'
language: en
updated: '2026-08-17T04:40:51+00:00'
license: 'CC BY-SA 4.0'
lessons: 26
outcomes:
    - 'Uses selectors and specificity rules correctly'
    - 'Explains the box model and flow layout'
    - 'Applies color, typography, and spacing scales consistently'
---

# Visual Presentation with CSS

How the rules that style a document are selected, how conflicts are resolved, how boxes are measured, and how color, typography, and spacing scales are built consistently.

## How CSS Works

The syntax of a declaration, how selectors map to the document, how conflicting rules are resolved by the cascade and specificity, and how values are inherited down the tree.

### Rule, Declaration, and Value

Source: https://academia.sh/en/courses/css-fundamentals/rule-declaration-and-value

The smallest units of a style language; a rule made of a selector and a declaration block, the structure of a property-value pair, and the rule that an invalid declaration is dropped.

The Web Fundamentals and HTML course turned the document into a node tree and loaded
meaning onto that tree's nodes: which one is a heading, which one is a section, which one
is a table cell. In that course's first lesson, one question in the three-way distinction
it established went unanswered — the question of "how will it look?" This course answers it.

The answer comes in a separate language. That language is called CSS, and like HTML it is
declarative: it states not what to do, but what should be so. This lesson names that
language's smallest units: rule, declaration, and value. If the distinctions are not
established here, the conflict resolution in later lessons will not make sense, because a
conflict is resolved not between rules, but between **declarations**.

#### A Rule Has Two Parts

A CSS rule has two parts: the **selector**, which says which elements are affected, and
the **declaration block**, which says what will happen to them.

```css
h1 {
  color: #143a52;
  margin-block-end: 0.5rem;
}
```

Here, `h1` is the selector, and the part between the curly braces is the declaration
block. The selector looks at the document's tree and returns a set of elements; the
declaration block lists the changes to apply to every member of that set. It matters that
the two parts are independent of each other: the same declaration block can be written
with a different selector, and the same selector with a different block.

A selector can consist of several selectors separated by commas. This is called writing a
**selector list**, and it is equivalent to writing the same block separately for each one:

```css
h1, h2 { color: #143a52; }
```

This notation gives the same result as writing the rules `h1 { color: #143a52; }` and
`h2 { color: #143a52; }`. The equivalence is not complete: if one member of a selector list
is invalid, the behavior depends on the selector's kind — a detail this course returns to
in the pseudo-class lesson.

#### Declaration: Property and Value

Every line inside a declaration block is a **declaration**, and it has two parts: the
**property** to the left of the colon, and the **value** to the right. Declarations are
separated by semicolons.

```css
padding: 8px 12px;
```

A property name comes from a fixed dictionary; it cannot be invented. What forms a value
accepts is defined separately for every property: `color` expects a color, `padding`
expects between one and four lengths, `text-align` expects one of a numbered set of
keywords.

The following program splits a style text into its rules and declarations. It checks
property names against a small dictionary and flags any that are not in it.

```js
// style-parse.mjs — splits a style text into its rules and declarations
const css = `
/* station page - first rules */
body { font-family: system-ui; line-height: 1.5 }
h1, h2 { color: #143a52; margin-block-end: 0.5rem; }
.measurement-table td { padding: 8px 12px; text-align: right; colour: blue }
`;

// drop comments, then split on the { } pair into rules
const clean = css.replace(/\/\*[\s\S]*?\*\//g, "");
const rules = [];
for (const chunk of clean.split("}")) {
  const [selector, body] = chunk.split("{");
  if (body === undefined) continue;
  const declarations = body
    .split(";")
    .map((d) => d.trim())
    .filter((d) => d.length > 0)
    .map((d) => {
      const colon = d.indexOf(":");
      return { property: d.slice(0, colon).trim(), value: d.slice(colon + 1).trim() };
    });
  rules.push({ selector: selector.trim().replace(/\s+/g, " "), declarations });
}

const known = new Set([
  "font-family", "line-height", "color", "margin-block-end", "padding", "text-align",
]);

for (const r of rules) {
  console.log(`selector    : ${r.selector}`);
  console.log(`declaration : ${r.declarations.length}`);
  for (const d of r.declarations) {
    const status = known.has(d.property) ? "valid" : "DROPPED";
    console.log(`  ${d.property} = ${d.value}   [${status}]`);
  }
}
console.log(`total rules       : ${rules.length}`);
console.log(`total declarations: ${rules.reduce((t, r) => t + r.declarations.length, 0)}`);
```

```
selector    : body
declaration : 2
  font-family = system-ui   [valid]
  line-height = 1.5   [valid]
selector    : h1, h2
declaration : 2
  color = #143a52   [valid]
  margin-block-end = 0.5rem   [valid]
selector    : .measurement-table td
declaration : 3
  padding = 8px 12px   [valid]
  text-align = right   [valid]
  colour = blue   [DROPPED]
total rules       : 3
total declarations: 7
```

Three things become visible. First, the semicolon after the **last** declaration in a
block is not required; it was missing for `body` and for the last rule, and the
declarations still split correctly. Second, comments are written between `/* … */` and are
discarded before parsing. Third, there is no property named `colour`; that declaration is
dropped, and **only** that declaration — the other two in the block are applied.

#### Invalidity Is Resolved at the Declaration Level

This last point speaks to the design of the language. The HTML Fundamentals course showed that the HTML parser does not
reject a malformed document — it produces a result through defined recovery rules. CSS
follows the same principle and defines recovery at three levels:

- If the **property name** is not recognized, that declaration is dropped; the block
  survives.
- If the property is recognized but the **value** does not match that property's grammar,
  that declaration is dropped as well. If `color: navy-ish` is written, the `color`
  property stays at its previous value.
- If the selector is not recognized, the **entire rule** is dropped; the declaration block
  applies to no element.

The common result of these three rules is that a style file never "fails to compile." The
part that is not understood silently falls away, and the rest is processed. The syntax
error concept from the Programming Fundamentals course does not apply here: an error is not
a reason to stop, it is a reason to skip.

This tolerance has a cost: a misspelled property name disappears without producing any
warning. A declaration can fail to have an effect for two separate reasons — it was
dropped, or it was overridden by another rule — and from the outside the two look
identical. This distinction is made measurable in the cascade lesson.

#### Shorthand Properties and Their Longhands

Some properties do not carry a single value; they set several properties at once. These are
called **shorthand properties**, and the properties they set are called that shorthand's
**longhands**.

The declaration `padding: 8px 12px` does not write to a single property, but to four. The
distribution rule based on the number of values is:

| Value count | Top | Right | Bottom | Left |
|---|---|---|---|---|
| 1 → `8px` | 8px | 8px | 8px | 8px |
| 2 → `8px 12px` | 8px | 12px | 8px | 12px |
| 3 → `8px 12px 4px` | 8px | 12px | 4px | 12px |
| 4 → `8px 12px 4px 2px` | 8px | 12px | 4px | 2px |

The order is clockwise: top, right, bottom, left. Missing values are copied from the
opposite side.

A shorthand has a behavior that matters as much as its notation: **it resets any longhand
it does not write to its initial value.** When `font: 16px serif` is written, it is not only
size and family that get set; every longhand that shorthand covers — `font-weight`,
`font-style`, `line-height`, and so on — is reset to its initial value. This behavior makes
it dangerous to use a shorthand to change a single longhand that was set earlier. The rule
is: a shorthand is written to **establish** a group, a longhand to **change** a single
value from that group, and in that case the shorthand comes first.

#### The Station Page's First Rules

This course styles the measurement station page built in the previous course. The page
carried North Slope Station's identification block, its measurement table, a location
image, and a correction-notice form. So far, no style has been written; the page's
appearance has come from the browser's own default style file — headings being bold and
large, lists being indented, links being underlined are that file's decisions.

The style file at the center of this course begins here:

```css
/* station.css — step 1: base declarations */
body {
  font-family: system-ui, sans-serif;
  line-height: 1.5;
  color: #1c2733;
}

h1 {
  font-size: 2rem;
  margin-block-end: 0.5rem;
}

.measurement-table td {
  padding: 8px 12px;
  text-align: right;
}
```

Three rules, seven declarations. Each lesson will add a layer to this file and justify why
the added layer is written the way it is. By the end of the course, the file will be a
complete style file carrying a color scale, a type scale, a spacing scale, and status
styles.

#### Summary

- A CSS rule consists of a selector and a declaration block; the selector returns a set of
  elements from the document, and the block lists the declarations to apply to that set.
- A declaration is a property-value pair separated by a colon; the property name comes
  from a fixed dictionary, and each property defines its value's grammar separately.
- Invalidity is resolved at the declaration level: an unrecognized property or a value
  that does not match only drops that declaration, and the block keeps working. An
  unrecognized selector drops the entire rule.
- A shorthand property sets a group of longhands at once and resets any longhand it
  does not write to its initial value; that is why a shorthand is for establishing a group,
  a longhand for fixing one.
- The page's unstyled appearance comes from the browser's default style file; every rule
  written is added on top of that file.

#### Next Step

This lesson established the structure of a rule but did not say how rules reach the
document. A style file can be attached to a document in three different ways, and these
three ways differ not just in how easy they are to write, but in loading order and
conflict resolution. The next lesson compares these three ways and revisits, from the
style file's side, the render-blocking behavior established in the Web Fundamentals and
HTML course's resource-loading-order lesson.

### Linking the Stylesheet

Source: https://academia.sh/en/courses/css-fundamentals/linking-the-stylesheet

Attaching a set of styles to a document in three ways — inline, internal, and external — and how these three ways differ in request count, reuse, and conflict resolution.

The previous lesson established that a rule consists of a selector and a declaration
block, and wrote the station page's first three rules. Rules have to live somewhere, and
where they live determines both maintenance cost and when the page's first render begins.

This lesson defines the three ways of attaching a stylesheet, shows the differences
between them with a measurable example, and revisits, from the stylesheet's side, the
render-blocking behavior established in the Web Fundamentals and HTML course's
resource-loading-order lesson.

#### Three Ways

**Inline style** is written into an element's `style` attribute. There is no selector: the
declarations attach directly to that one element.

```html
<h1 style="color: #8a1c1c; font-size: 2rem">North Slope Measurement Station</h1>
```

**Internal style sheet** is written inside a `style` element in the document's head. The
rules are fully formed; the selectors only look at that one document.

```html
<head>
  <meta charset="utf-8">
  <title>North Slope Measurement Station</title>
  <style>
    h1 { font-size: 2rem; }
  </style>
</head>
```

**External style sheet** is a separate `.css` file attached to the document with a `link`
element. `rel="stylesheet"` states the kind of the link; `href` gives the resource's
address.

```html
<link rel="stylesheet" href="/station.css">
```

There is a fourth way: writing `@import url("…")` inside a style file to pull in another
file. This notation works, but it introduces a sequential dependency: the address of the
imported file cannot be known until the file that calls it has been downloaded and parsed.
This is why an `@import` chain finishes later than the same number of `link` elements
would.

#### The External Style Is a Separate Resource

That an external style file is a resource separate from the document is measurable. The
following server serves two addresses separately and logs every request it receives.

```js
// style-server.mjs — serves two resources separately and logs every request received
import { createServer } from "node:http";

const document = `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>North Slope Measurement Station</title>
<link rel="stylesheet" href="/station.css">
<style>h1 { font-size: 2rem }</style>
</head>
<body>
<h1 style="color: #8a1c1c">North Slope Measurement Station</h1>
</body>
</html>
`;

const style = `body { font-family: system-ui; line-height: 1.5 }
h1 { color: #143a52 }
`;

const server = createServer((request, response) => {
  console.log(`request: ${request.method} ${request.url}`);
  if (request.url === "/station.css") {
    response.writeHead(200, { "content-type": "text/css; charset=utf-8" });
    response.end(style);
  } else {
    response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
    response.end(document);
  }
});

server.listen(8123, "127.0.0.1", () => {
  console.log("listening: 127.0.0.1:8123");
});
```

The server is started in the background and the two addresses are requested separately:

```sh
node style-server.mjs > server.log 2>&1 &
sleep 1
curl -s -o /dev/null -w "%{http_code} %{content_type}\n" http://127.0.0.1:8123/
curl -s -o /dev/null -w "%{http_code} %{content_type}\n" http://127.0.0.1:8123/station.css
cat server.log
```

```
200 text/html; charset=utf-8
200 text/css; charset=utf-8
listening: 127.0.0.1:8123
request: GET /
request: GET /station.css
```

The two lines in the log show that the external style file does not arrive with the
document: a second request is made. This has three consequences.

First, the file is **reusable**. If ten pages attach to the same `station.css` file, the
file is downloaded once; later pages read it from cache. The conditional-request and
time-to-live concepts defined in the How the Internet Works course apply here.

Second, the file **can change independently of the document**. The document's bytes can
stay unchanged while the style is updated, and the reverse is also true.

Third, the second request is a **cost**. The `link` element found while the document is
parsed may require a new connection, and that delays the start of the first render.

#### Rendering Does Not Start Until the Style Resolves

The Web Fundamentals and HTML course's resource-loading-order lesson described how
scripts block parsing. A style file does not block parsing — the document tree keeps being
built — but it blocks **rendering**.

The reason becomes visible when looking at what not blocking would cause. If rendering
happened before the style resolved, the page would first render with the default style,
then render again once the file arrived. The user would see two different pages. This is
why the defined behavior is to wait until the style resolves.

This rule produces three practical consequences:

- The style file is attached in the document's **head**. If it is placed at the end of the
  body, rendering still waits, but the wait starts later; the total time grows longer.
- The file is **kept small**; rules unused on a given page delay that page's rendering.
- If a link is only needed under a specific condition, it is conditioned with the `media`
  attribute. A link whose condition is not met still has its file downloaded, but it does
  not block rendering:

```html
<link rel="stylesheet" href="/station.css">
<link rel="stylesheet" href="/print.css" media="print">
```

The second file carries rules written for print output; it does not wait for the start of
screen rendering.

#### Comparing the Three Ways

| Criterion | Inline | Internal | External |
|---|---|---|---|
| Selector can be written | No | Yes | Yes |
| Usable across multiple documents | No | No | Yes |
| Separate request | No | No | Yes |
| Cacheable | With the document | With the document | Independently |
| Priority in a conflict | Highest | By source order | By source order |
| Pseudo-class can be written | No | Yes | Yes |

Two rows in the table call for further explanation.

**There is no selector in inline style**, so a pseudo-class cannot be written either. A
declaration that should change when an element is hovered over or focused cannot be
written into the `style` attribute; that requires a rule.

**Inline style has the highest priority.** The specificity calculation covered in a later
section does not cover inline declarations; they sit above the calculation, outside of it.
This makes it effectively impossible to override an inline declaration with a rule through
ordinary means. The result is that inline style is the most expensive way to maintain, and
it is justified only in cases where the value is computed while the document is being
produced.

#### Relative Addresses Resolve Against the File Itself

A commonly overlooked consequence of an external style file being a separate resource is
that the relative addresses inside that file resolve not against the document, but against
**the style file's own address**.

Suppose the document is at `/products/station.html` and the style file is at
`/styles/station.css`. If `background-image: url("pattern.png")` is written in the style
file, the address requested is `/styles/pattern.png` — not `/products/pattern.png`. The
same notation written in the document's `style` attribute would resolve against the
document, requesting `/products/pattern.png`.

This distinction is exactly what makes a style file reusable: the file refers to assets
next to it through a fixed path and is unaffected by which document called it. Keeping
image files next to the style file is, for this reason, more than a matter of
organizational preference.

#### Attaching the Station Page

The course center uses a single external file. The document's head becomes:

```html
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>North Slope Measurement Station</title>
  <link rel="stylesheet" href="/station.css">
</head>
```

The `station.css` file starts with the previous lesson's three rules and grows through the
course. Attaching to the document from a single point means that changing a rule in later
lessons can be done without touching the document — this is the file-level counterpart of
the structure-presentation distinction.

#### Summary

- A set of styles attaches to a document in three ways: an element's `style` attribute
  (inline), a `style` element in the head (internal), and a separate file called with a
  `link` element (external).
- An external style file is a separate HTTP request; it is independently cacheable and
  reusable across multiple documents, but it comes with a request cost.
- A style file does not block parsing, it blocks rendering; this is a behavior defined to
  keep the page from appearing twice, in two different states.
- A link conditioned with the `media` attribute still has its file downloaded, but it does
  not block rendering if its condition is not met.
- Inline style has no selector, so a status style cannot be written with it; its priority
  also sits above the specificity calculation, which makes it hard to override.

#### Next Step

Rules now reach the document, but which elements they reach is still limited to a single
example. Writing `h1` selects every top-level heading; what if only the missing values in
the measurement table need to be selected? The next lesson defines the four basic kinds of
selector — type, class, id, and universal — and measures, with a runnable matcher, which
set each one returns from the document tree.

### Selectors

Source: https://academia.sh/en/courses/css-fundamentals/selectors

Which set the type, class, id, and universal selectors return from the document tree; the compound selector concept, and naming class names by meaning.

The previous lesson attached the style file to the document. Every rule in the file starts
with a selector, and the selectors written so far have only been an element name: `body`,
`h1`, `td`. This means treating every cell in the measurement table the same way. But the
table has cells marking missing measurements, and those need to look different.

This lesson defines four basic selectors: **type**, **class**, **id**, and **universal**.
Which set each one returns from the document tree is measured with a runnable matcher.

#### A Selector Returns a Set

A selector has a single job: it looks at the nodes in the document tree and returns a
subset. The size of the returned set can be zero; an empty set is not an error, it means
only that the rule applies to no element.

The following program builds a shrunk-down tree of the previous course's station page and
matches the four selector kinds against that tree. The tree carries each node's name, its
class list, and its id if it has one — that is the information a selector match needs.

```js
// selector-match.mjs — matches basic selectors on a small document tree
const tree = {
  name: "body", classNames: [], id: null, children: [
    { name: "header", classNames: ["masthead"], id: null, children: [
      { name: "h1", classNames: [], id: null, children: [] },
      { name: "p", classNames: ["summary"], id: null, children: [] },
    ]},
    { name: "main", classNames: [], id: "content", children: [
      { name: "table", classNames: ["measurement-table"], id: null, children: [
        { name: "tr", classNames: [], id: null, children: [
          { name: "td", classNames: ["value"], id: null, children: [] },
          { name: "td", classNames: ["value", "missing"], id: null, children: [] },
        ]},
      ]},
      { name: "p", classNames: ["summary", "note"], id: null, children: [] },
    ]},
  ],
};

function* allNodes(n, path = []) {
  const ownPath = [...path, label(n)];
  yield { node: n, path: ownPath.join(" > ") };
  for (const c of n.children) yield* allNodes(c, ownPath);
}

function label(n) {
  return n.name + (n.id ? `#${n.id}` : "") + n.classNames.map((s) => `.${s}`).join("");
}

// simple selector only: type | .class | #id | *
function matches(selector, n) {
  if (selector === "*") return true;
  if (selector.startsWith(".")) return n.classNames.includes(selector.slice(1));
  if (selector.startsWith("#")) return n.id === selector.slice(1);
  return n.name === selector;
}

const selectors = ["*", "p", ".summary", "#content", ".value", ".missing", "td", "#value"];
for (const s of selectors) {
  const found = [...allNodes(tree)].filter(({ node }) => matches(s, node));
  console.log(`${s.padEnd(8)} -> ${found.length} element${found.length === 1 ? "" : "s"}`);
  for (const f of found) console.log(`           ${f.path}`);
}
```

```
*        -> 10 elements
           body
           body > header.masthead
           body > header.masthead > h1
           body > header.masthead > p.summary
           body > main#content
           body > main#content > table.measurement-table
           body > main#content > table.measurement-table > tr
           body > main#content > table.measurement-table > tr > td.value
           body > main#content > table.measurement-table > tr > td.value.missing
           body > main#content > p.summary.note
p        -> 2 elements
           body > header.masthead > p.summary
           body > main#content > p.summary.note
.summary -> 2 elements
           body > header.masthead > p.summary
           body > main#content > p.summary.note
#content -> 1 element
           body > main#content
.value   -> 2 elements
           body > main#content > table.measurement-table > tr > td.value
           body > main#content > table.measurement-table > tr > td.value.missing
.missing -> 1 element
           body > main#content > table.measurement-table > tr > td.value.missing
td       -> 2 elements
           body > main#content > table.measurement-table > tr > td.value
           body > main#content > table.measurement-table > tr > td.value.missing
#value   -> 0 elements
```

#### Four Kinds

**Type selector** writes an element name and returns every element that carries that name.
The `p` selector returned both paragraphs: one in the masthead block, one in the main
content. A type selector does not look at its position in the document.

**Class selector** starts with a dot and returns elements that carry that name in their
`class` attribute. An element can carry more than one class; the cell written as
`class="value missing"` matched both the `.value` and `.missing` selectors. Class names are
a space-separated list, and their order carries no meaning.

**Id selector** starts with a hash sign, and the `id` attribute matching returns that
element. Ids must be unique within a document, so an id selector returns at most one
element. `#content` returned one element.

**Universal selector** is an asterisk and matches every element. The example tree had ten
nodes, and it returned all ten.

The last line shows a distinction: `#value` returned zero elements, because `value` is a
**class** name, not an id. Confusing a dot with a hash sign is a rule that returns an empty
set and produces no warning at all. It is one of the most common reasons a rule fails to
have any effect.

#### Compound Selector

When simple selectors are written next to each other with no space between them, they form
a **compound selector**, and it returns elements that satisfy **all of them at once**. In
set terms: intersection.

```css
td.missing    { color: #8a1c1c; }
p.summary.note { font-style: italic; }
```

The first selector returns cells that are both a `td` and carry the `missing` class. In the
example tree, this is a single cell. The second selector asks for all three conditions and
also reaches a single element.

If a compound selector includes a type selector, it is written **first**: `td.missing` is
valid, `.missingtd` is something else. When no type selector is written, the universal
selector is assumed; `.missing` and `*.missing` return the same set.

#### Class or Id

Both are names the author places into the document. The technical difference between them
is single: an id is unique, a class is not. Two separate consequences follow from that
difference.

An id places an anchor pointing to **one single place** in the document. A link target
(`href="#content"`), a label-field relationship (the `for` attribute), and accessibility
references are all built with an id. These have nothing to do with styling; that is an
id's actual job.

A class, on the other hand, declares **membership in a group**. That is exactly what style
needs: "this cell carries a missing measurement," "this paragraph is a summary." No matter
how many times the same statement appears in the document, it should get the same style.

The problem with using an id selector in style shows up not when it fails to work, but
when it **does** work: an id selector's specificity is higher than a class selector's, and
once written, overriding it requires another id selector. This is worked out with
numbers in the cascade-and-specificity lesson. For now, the actionable rule is: **style is
written with a class, an id is for reference.**

#### A Class Name Names Meaning, Not Appearance

A class name can be chosen in two ways. `.red` names an appearance; `.missing` names a
state. Either can be attached to the same cell and give the same result at first.

The difference shows up when the decision changes. If it is decided that missing
measurements should be shown in gray instead of red, the name `.red` starts lying: a class
named red with a gray color remains. Fixing it requires touching the document itself,
changing the class name everywhere it occurs. With `.missing`, the only thing that changes
is one declaration's value; the document stays fixed.

This is the class-name counterpart of the structure-presentation distinction established
in the Web Fundamentals and HTML course: the document states **what something is**, the
style file states **how it will look**.

The station page's class names have been chosen according to this principle:

```css
/* station.css — step 2: status classes */
.measurement-table td.value { text-align: right; }
.measurement-table td.missing { color: #8a1c1c; }
.masthead .summary           { font-size: 1.125rem; }
```

The last rule has a space between two selectors, and that space is an operator — not a
compound selector, but a relationship statement. The next lesson defines that operator and
its relatives.

#### Summary

- A selector returns a subset from the document tree; returning an empty set is not an
  error, it only means the rule applies to no element.
- A type selector is written with an element name, a class selector with a dot, an id
  selector with a hash sign; a universal selector is an asterisk and matches every
  element.
- When simple selectors are written next to each other with no space, they form a
  compound selector and return the intersection of their conditions; a type selector is
  written first.
- An id is unique within a document, and its actual job is being a reference target; style
  is written with a class, because a style decision applies to more than one element.
- A class name names meaning, not appearance; a name that names appearance forces the
  document to change too when a style decision changes.

#### Next Step

The selectors in this lesson looked at the element **itself**: what its name is, which
classes it carries, whether it has an id. Its position in the document tree was never
used — yet a request like "only the cells inside the measurement table" requires asking
where an element stands. The next lesson defines the combinators that bring tree
relationships into a selector, and shows how a selector is evaluated from right to left.

### Combinator Selectors

Source: https://academia.sh/en/courses/css-fundamentals/combinator-selectors

The descendant, child, adjacent-sibling, and general-sibling combinators; a selector's right-to-left evaluation and the key-selector concept.

The previous lesson's last rule wrote `.masthead .summary`, and it was said in passing that
the space between the two selectors is an operator. That space is called a
**combinator**, and it adds a question of position in the tree to the selector: not the
element itself, but where it stands is asked.

This lesson defines the four combinators and shows how a selector is evaluated. The
evaluation order matters for understanding both the result and a selector's cost.

#### Four Combinators

| Notation | Name | Question asked |
|---|---|---|
| `A B` | descendant combinator | Is one of B's ancestors A? |
| `A > B` | child combinator | Is B's direct parent A? |
| `A + B` | adjacent sibling | Is B's immediately preceding sibling A? |
| `A ~ B` | general sibling | Is one of B's preceding siblings A? |

All four are read the same way: **the right side is selected, the left side is the
condition.** The rule `.measurement-table td` selects `td` elements;
`.measurement-table` only narrows down which `td` elements get selected. The left side is
never styled.

Sibling combinators only look **backward**. The notation `A + B` means "B that follows A";
there is no combinator for "A that follows B."

#### Evaluation Happens from Right to Left

A selector is read left to right but evaluated right to left. The reason is a difference
in cost between the two directions.

If evaluation went left to right, for `.measurement-table td` every element carrying that
class would first be found across the whole document, then **its entire subtree** would be
scanned for each one. Going right to left, for every `td` element only the ancestor chain
is followed upward; the length of that chain is the depth of the tree. This second
direction is also the natural direction of the question asked when an element's style is
computed: "which rules match this element?"

The compound selector on the right is called the **key selector**. It is what determines
which elements a rule is tested against.

The following program evaluates the four combinators from right to left. The tree carries
the measurement table's real structure: rows are grouped inside `thead` and `tbody`.

```js
// combinator.mjs — evaluates four combinators from right to left
const tree = { name: "body", classNames: [], children: [
  { name: "main", classNames: [], children: [
    { name: "table", classNames: ["measurement-table"], children: [
      { name: "thead", classNames: [], children: [
        { name: "tr", classNames: [], children: [{ name: "th", classNames: [], children: [] }] },
      ]},
      { name: "tbody", classNames: [], children: [
        { name: "tr", classNames: [], children: [
          { name: "td", classNames: ["name"], children: [] },
          { name: "td", classNames: ["value"], children: [] },
          { name: "td", classNames: ["value", "missing"], children: [] },
        ]},
      ]},
    ]},
    { name: "p", classNames: ["note"], children: [] },
    { name: "table", classNames: [], children: [
      { name: "tbody", classNames: [], children: [
        { name: "tr", classNames: [], children: [{ name: "td", classNames: ["value"], children: [] }] },
      ]},
    ]},
  ]},
]};

// add parent and sibling info to every node
function attach(n, parent = null) {
  n.parent = parent;
  n.children.forEach((c, i) => { c.previousSibling = i > 0 ? n.children[i - 1] : null; attach(c, n); });
  return n;
}
attach(tree);

const path = (n) => (n.parent ? path(n.parent) + " " : "") + n.name + n.classNames.map((s) => "." + s).join("");
function* all(n) { yield n; for (const c of n.children) yield* all(c); }

const simpleMatches = (s, n) =>
  s === "*" ? true : s.startsWith(".") ? n.classNames.includes(s.slice(1)) : n.name === s;

// compound selector: simple selectors with no space between them
function compoundMatches(compound, n) {
  const parts = compound.match(/(^[a-z]+|\.[a-z-]+|\*)/g) ?? [];
  return parts.every((p) => simpleMatches(p, n));
}

// selector: compound (combinator compound)*  — resolved from right to left
function selectorMatches(selector, node) {
  const tokens = selector.trim().split(/\s+/);
  let n = node;
  let i = tokens.length - 1;
  if (!compoundMatches(tokens[i], n)) return false;
  i--;
  while (i >= 0) {
    const combinator = ">+~".includes(tokens[i]) ? tokens[i--] : " ";
    const target = tokens[i--];
    if (combinator === ">") { n = n.parent; if (!n || !compoundMatches(target, n)) return false; }
    else if (combinator === "+") { n = n.previousSibling; if (!n || !compoundMatches(target, n)) return false; }
    else if (combinator === "~") {
      let k = n.previousSibling;
      while (k && !compoundMatches(target, k)) k = k.previousSibling;
      if (!k) return false; n = k;
    } else {
      let a = n.parent;
      while (a && !compoundMatches(target, a)) a = a.parent;
      if (!a) return false; n = a;
    }
  }
  return true;
}

const trials = [
  "table td",
  ".measurement-table td",
  ".measurement-table > td",
  "tbody > tr > td",
  "td.name + td",
  "td.name ~ td",
  ".measurement-table + p",
];
for (const s of trials) {
  const found = [...all(tree)].filter((n) => selectorMatches(s, n));
  console.log(`${s.padEnd(26)} -> ${found.length}`);
  for (const f of found) console.log(`  ${path(f)}`);
}
```

```
table td                   -> 4
  body main table.measurement-table tbody tr td.name
  body main table.measurement-table tbody tr td.value
  body main table.measurement-table tbody tr td.value.missing
  body main table tbody tr td.value
.measurement-table td      -> 3
  body main table.measurement-table tbody tr td.name
  body main table.measurement-table tbody tr td.value
  body main table.measurement-table tbody tr td.value.missing
.measurement-table > td    -> 0
tbody > tr > td            -> 4
  body main table.measurement-table tbody tr td.name
  body main table.measurement-table tbody tr td.value
  body main table.measurement-table tbody tr td.value.missing
  body main table tbody tr td.value
td.name + td               -> 1
  body main table.measurement-table tbody tr td.value
td.name ~ td               -> 2
  body main table.measurement-table tbody tr td.value
  body main table.measurement-table tbody tr td.value.missing
.measurement-table + p     -> 1
  body main p.note
```

This implementation does not backtrack on the descendant and general-sibling steps: it
picks the first matching ancestor or the first matching sibling and stays there. A real
matcher backtracks and tries another ancestor if the next step fails. In the seven examples
above, both behaviors give the same result.

#### A Selector That Returns Zero Elements

The third line calls for attention: `.measurement-table > td` returned no element, while
`.measurement-table td` returned three. The only difference between them is one character.

The reason is in the tree. Cells are not the table's direct children; the chain between
them is `table > tbody > tr > td`. The child combinator demands exactly one step, and that
step falls on the `tbody` element.

The Web Fundamentals and HTML course described how the parser builds elements not written
in the source; `tbody` is the most commonly encountered case of this. Even if the `tbody`
tag was not written in the source text, the element exists in the document, and a selector
sees that element. This is the style side's first concrete consequence of the "tag" versus
"element" distinction: **a selector does not look at the source text, it looks at the tree
after parsing.**

#### When the Child Combinator Is Needed

The descendant combinator does not care about depth, and that is the desired behavior most
of the time. But it gives an unwanted result in structures nested inside the same kind of
structure.

If the station page's navigation section carries nested lists, the selector `.nav li`
returns both the top-level items and the nested ones. To target only the top level,
`.nav > ul > li` is written. The rule is: if depth carries meaning, the child combinator is
used; if it does not, the descendant combinator is used.

#### Sibling Combinators Depend on Order

`td.name + td` returned one cell: the value cell immediately following the name cell.
`td.name ~ td` returned both, because the general-sibling combinator does not care how
many siblings come between them.

The typical use of sibling combinators is styling the relationship **between** two
elements. To put space between two consecutive paragraphs:

```css
p + p { margin-block-start: 0.75em; }
```

This notation does not add space before the **first** paragraph of a section — because it
has no preceding paragraph sibling. The same result could also be reached by giving every
paragraph space and then removing it from the first one; the form written with a sibling
combinator keeps the rule in a single place, with no exception.

#### The Station Page's Rules

The style file starts using tree positions with this lesson:

```css
/* station.css — step 3: relationship-based rules */
.measurement-table tbody td   { padding: 8px 12px; }
.measurement-table tbody > tr { border-block-end: 1px solid #d5dbe0; }
.nav > ul > li                { display: inline-block; }
.measurement-table + p        { font-size: 0.875rem; }
```

The last rule shrinks the caption paragraph that comes right after the table. Giving the
paragraph a separate class would give the same result; the difference is whether the rule
requires adding a class to the document. A style problem that can be solved without
touching the document is solved in the style file.

#### Summary

- A combinator adds a question of tree position to a selector; all four are read the same
  way — the right side is selected, the left side is the condition.
- The descendant combinator does not care about depth; the child combinator demands
  exactly one step; the adjacent sibling looks at the immediately preceding sibling, the
  general sibling at any of the preceding siblings.
- A selector is evaluated right to left; the rightmost compound selector is the key
  selector, and it determines which elements a rule is tested against.
- A selector looks at the tree after parsing, not the source text; elements not written in
  the source, such as `tbody`, are part of the chain and affect the child combinator.
- Sibling combinators only look backward; there is no combinator for "the one before the
  next sibling."

#### Next Step

Selectors have so far looked at an element's name, class, id, and position in the tree.
There is one more piece of information in the document, and it has not been used yet:
attribute values. A form field's type, a link's target address, the measurement units
attached with data attributes — all of it sits in attributes. The next lesson defines
attribute selectors and their value-matching forms.

### Attribute Selectors

Source: https://academia.sh/en/courses/css-fundamentals/attribute-selectors

Matching by an attribute's presence and value; exact match, list membership, hyphen-prefixed match, and substring matches, along with the case-sensitivity flag.

Selectors have so far looked at an element's name, class, id, and position in the tree.
There is one more layer of information in the document: **attributes**. A link's target
address, a form field's type, a measurement unit attached to a cell with a data attribute —
all of it sits there, and none of it is written in the class list.

This lesson defines attribute selectors. There are seven matching forms, and the
differences between them are string operations, so all of them can be demonstrated by
running code.

#### Seven Matching Forms

An attribute selector is written inside square brackets. Its simplest form asks about the
attribute's **presence**; it does not look at its value.

| Notation | Condition |
|---|---|
| `[name]` | does the attribute exist |
| `[name="x"]` | is the value exactly `x` |
| `[name~="x"]` | is `x` in the space-separated value list |
| `[name\|="x"]` | is the value `x`, or does it start with `x-` |
| `[name^="x"]` | does the value start with `x` |
| `[name$="x"]` | does the value end with `x` |
| `[name*="x"]` | does the value contain `x` |

The last three are substring matches and treat the value as an unstructured piece of text.
The middle two assume the value has a structure: `~=` a space-separated list, `|=` a
hyphen-segmented code.

The following program implements all seven forms and tests them against elements taken
from the station page.

```js
// attr-match.mjs — implements the seven attribute matching forms
function match(form, name, value, attrs, insensitive = false) {
  if (!(name in attrs)) return false;
  let v = attrs[name];
  let d = value;
  if (insensitive) { v = v.toLowerCase(); d = d.toLowerCase(); }
  switch (form) {
    case "has": return true;                          // [name]
    case "=":   return v === d;                       // [name="x"]
    case "~=":  return v.split(/\s+/).includes(d);    // [name~="x"]
    case "|=":  return v === d || v.startsWith(d + "-");
    case "^=":  return d !== "" && v.startsWith(d);
    case "$=":  return d !== "" && v.endsWith(d);
    case "*=":  return d !== "" && v.includes(d);
    default: throw new Error("unknown form: " + form);
  }
}

const elements = [
  { name: "a",     attrs: { href: "https://data.example.test/station.pdf", rel: "noopener help" } },
  { name: "a",     attrs: { href: "/measurements", rel: "help" } },
  { name: "a",     attrs: { href: "mailto:observer@example.test" } },
  { name: "input", attrs: { type: "number", name: "temperature", required: "" } },
  { name: "input", attrs: { type: "text", name: "observer" } },
  { name: "td",    attrs: { "data-unit": "degrees-C" } },
  { name: "td",    attrs: { "data-unit": "degrees" } },
  { name: "html",  attrs: { lang: "en-US" } },
];

const trials = [
  ["[required]",              "has", "required", ""],
  ['[type="number"]',         "=",   "type",     "number"],
  ['[rel~="help"]',           "~=",  "rel",      "help"],
  ['[lang|="en"]',            "|=",  "lang",     "en"],
  ['[href^="https://"]',      "^=",  "href",     "https://"],
  ['[href$=".pdf"]',          "$=",  "href",     ".pdf"],
  ['[href*="measure"]',       "*=",  "href",     "measure"],
  ['[data-unit|="degrees"]',  "|=",  "data-unit", "degrees"],
];

for (const [notation, form, name, value] of trials) {
  const found = elements.filter((e) => match(form, name, value, e.attrs));
  console.log(`${notation.padEnd(26)} -> ${found.length}`);
  for (const f of found) {
    const attrs = Object.entries(f.attrs).map(([k, v]) => `${k}="${v}"`).join(" ");
    console.log(`   <${f.name} ${attrs}>`);
  }
}

// case sensitivity
console.log("--- case sensitivity ---");
const element = { type: "NUMBER" };
console.log('[type="number"]   ->', match("=", "type", "number", element));
console.log('[type="number" i] ->', match("=", "type", "number", element, true));
```

```
[required]                 -> 1
   <input type="number" name="temperature" required="">
[type="number"]            -> 1
   <input type="number" name="temperature" required="">
[rel~="help"]              -> 2
   <a href="https://data.example.test/station.pdf" rel="noopener help">
   <a href="/measurements" rel="help">
[lang|="en"]               -> 1
   <html lang="en-US">
[href^="https://"]         -> 1
   <a href="https://data.example.test/station.pdf" rel="noopener help">
[href$=".pdf"]             -> 1
   <a href="https://data.example.test/station.pdf" rel="noopener help">
[href*="measure"]          -> 1
   <a href="/measurements" rel="help">
[data-unit|="degrees"]     -> 2
   <td data-unit="degrees-C">
   <td data-unit="degrees">
--- case sensitivity ---
[type="number"]   -> false
[type="number" i] -> true
```

#### The Difference Between List Membership and Substring

`[rel~="help"]` returned both links. One has a `rel` value of `"noopener help"`, the
other's is `"help"`. The tilde sign splits the value on whitespace and searches the list;
it is therefore independent of order and is not tripped up by a partial word.

`[rel*="help"]` would give the same result here, but that notation is open to false
matches: the value `rel="helpless"` would also match, even though there is no list
membership there. If an attribute carries a space-separated list of keywords,
**`~=` is written**, not `*=`.

The same distinction holds for `|=`. The notation `[lang|="en"]` matches the values
`lang="en"` and `lang="en-US"`, and does not match `lang="enough"`. This is a match
designed around the hyphen-segmented structure of language tags. The output's last line
shows the same form working on a data attribute too: `data-unit="degrees"` and
`data-unit="degrees-C"` matched together.

#### A Class Selector Is an Attribute Selector

The `~=` behavior in the output should look familiar. The `class` attribute is also a
space-separated list, and a class selector asks for list membership exactly. The two
notations are equivalent:

```css
.missing        { color: #8a1c1c; }
[class~="missing"] { color: #8a1c1c; }
```

The second notation is not used in practice, but it puts a concept in its place: a class
is not a privileged concept of the language, it is a shorthand layered on top of an
attribute. In the same way, the `#content` selector returns the same set as
`[id="content"]` — the weight the two carry in the specificity calculation differs, but
their matching behavior is the same.

#### Case Sensitivity and Empty Values

The output's last two lines show a trap. The `[type="number"]` selector did not match an
element whose value is `NUMBER`. Attribute values are case-sensitive as a general rule;
some HTML attributes have a defined case insensitivity, but rather than relying on that, it
is safer to state it explicitly with a flag:

```css
input[type="number" i] { text-align: right; }
```

The `i` flag written before the closing bracket makes the match case-insensitive. The `s`
flag requests the opposite, an explicitly case-sensitive match.

The second trap is the empty string. The `d !== ""` check present in the program's three
substring forms reflects a rule of the language: the notation `[href*=""]` matches
**no** element at all. Intuition says the opposite — every string contains the empty
string — but the definition excludes this case. If a production places a value into a
selector dynamically, a rule should be expected to silently die when the value is empty.

#### When an Attribute Selector Is the Right Tool

Adding a class is always possible, so why would an attribute selector be needed at all?

An attribute selector is the right tool in cases where the distinction is **already in the
document**. A form field's type is written in the `type` attribute; adding
`class="number-field"` on top of that writes the same piece of information to two places
and takes on the obligation of keeping the two in sync. When the two sources drift apart,
which one is correct becomes unclear.

The same reasoning holds for a required state:

```css
/* station.css — step 4: form fields and outbound links */
input[type="number"]  { text-align: right; }
input[required]       { border-inline-start: 3px solid #8a5b00; }
a[href^="http"]       { padding-inline-end: 1em; }
td[data-unit|="degrees"] { font-variant-numeric: tabular-nums; }
```

The second rule reads required fields from the `required` attribute already present in the
document. When a field stops being required, the attribute is removed and the style drops
by itself; no class update is needed. Validation behavior and visual indicator are fed by
the single same source.

The third rule's reasoning is different: it makes room for outbound links. An address
starting with `http` is not an author decision, it is the address's own shape; marking it
with a class as well would be needless repetition.

The limit is worth stating too. An attribute selector cannot be invented if the distinction
is not in the document. If the information "this paragraph is a warning" is not written in
any attribute, a class name is the place to carry it.

#### Summary

- An attribute selector is written with square brackets; its simplest form asks about the
  attribute's presence, the remaining six forms test its value.
- `~=` splits the value on whitespace and looks for list membership, `|=` matches the
  prefix of a hyphen-segmented code; `^= $= *=` treat the value as unstructured text.
- A class selector returns the same set as `[class~="…"]`, an id selector the same as
  `[id="…"]`; both are shorthands for attribute matching.
- Attribute values are matched case-sensitively; insensitivity is requested explicitly
  with the `i` flag. Substring matches written with an empty string match no element.
- If the distinction already sits in an attribute in the document, an attribute selector
  is written; writing the same information into a class as well takes on the obligation
  of keeping two sources in sync.

#### Next Step

Every selector so far has looked at something written in the document: name, class, id,
attribute, position. There are also things not written in the document — whether a link is
being hovered over, whether a field has focus, which position a row sits at. The next
lesson defines the pseudo-classes that select these states and the pseudo-elements that
generate content not present in the tree at all.

### Pseudo-Classes and Pseudo-Elements

Source: https://academia.sh/en/courses/css-fundamentals/pseudo-classes-and-pseudo-elements

Pseudo-classes that select states not written in the document, nth-child arithmetic, logical selectors, and pseudo-elements that generate content absent from the tree.

Every selector so far has looked at something written in the document: element name,
class, id, attribute, position in the tree. There is also information that is not written
in the document. Whether a link is being hovered over, whether a form field has focus,
which position a row sits at — none of this appears in the markup, because none of it is a
property of the document.

This lesson defines two mechanisms: pseudo-classes, which select **states** not written in
the document, and pseudo-elements, which target **parts** not present in the document tree
at all. The two do different jobs, and the only difference in notation is the number of
colons.

#### A Pseudo-Class Selects a State

A **pseudo-class** is written with a single colon and adds a condition to the selector
about the element's current state. It splits into two families.

The first family asks about **interaction state**: `:hover` matches while the pointer is
over the element, `:focus` while the element holds keyboard focus, `:active` while the
element is being activated, `:disabled` while a control is disabled, `:checked` while an
option is checked, `:invalid` while a field's value does not satisfy its constraints. None
of these states is written in the document; they come from user interaction or from
validation results.

The second family asks about **position in the tree**: `:first-child`, `:last-child`,
`:only-child`, `:empty`, `:root`, and `:nth-child()`, the head of a numbered family.

#### nth-child Is an Arithmetic Expression

`:nth-child()` does not take a number, it takes a **formula**: `an+b`. Here `n` is an
integer starting at zero and increasing; every value the formula produces, as long as it
falls between 1 and the sibling count, matches a position.

The following program solves this formula and lists which positions match in a nine-row
measurement table.

```js
// nth-calc.mjs — solves the :nth-child(an+b) formula and lists which positions match
function parse(expr) {
  const s = expr.replace(/\s+/g, "").toLowerCase();
  if (s === "odd")  return { a: 2, b: 1 };
  if (s === "even") return { a: 2, b: 0 };
  const m = s.match(/^([+-]?\d*)n([+-]\d+)?$/);
  if (!m) return { a: 0, b: Number(s) };
  const aText = m[1];
  const a = aText === "" || aText === "+" ? 1 : aText === "-" ? -1 : Number(aText);
  return { a, b: m[2] ? Number(m[2]) : 0 };
}

// n = 0,1,2,... gives an+b; positions where 1 <= position <= total match
function matched(expr, total) {
  const { a, b } = parse(expr);
  const c = [];
  for (let position = 1; position <= total; position++) {
    if (a === 0) { if (position === b) c.push(position); continue; }
    const n = (position - b) / a;
    if (Number.isInteger(n) && n >= 0) c.push(position);
  }
  return { a, b, matched: c };
}

const TOTAL = 9; // nine rows in the measurement table
for (const expr of ["odd", "even", "3", "2n+1", "3n", "3n+1", "-n+3", "n+4", "5n-2"]) {
  const { a, b, matched: m } = matched(expr, TOTAL);
  const formula = `${a}n${b >= 0 ? "+" : ""}${b}`;
  console.log(`:nth-child(${expr.padEnd(5)}) = ${formula.padEnd(7)} -> ${m.join(" ") || "(empty)"}`);
}
```

```
:nth-child(odd  ) = 2n+1    -> 1 3 5 7 9
:nth-child(even ) = 2n+0    -> 2 4 6 8
:nth-child(3    ) = 0n+3    -> 3
:nth-child(2n+1 ) = 2n+1    -> 1 3 5 7 9
:nth-child(3n   ) = 3n+0    -> 3 6 9
:nth-child(3n+1 ) = 3n+1    -> 1 4 7
:nth-child(-n+3 ) = -1n+3   -> 1 2 3
:nth-child(n+4  ) = 1n+4    -> 4 5 6 7 8 9
:nth-child(5n-2 ) = 5n-2    -> 3 8
```

Three lines call for a further reading. `odd` and `even` are keywords that name the
formulas `2n+1` and `2n`; they are not a separate mechanism. `-n+3`, with its negative
coefficient, means "the first three": as `n` grows, the value shrinks, and the match ends
once it drops below 1. Its counterpart `n+4` means "from the fourth to the end." These two
are the way to select a range.

A warning: `:nth-child()` counts **all siblings**, regardless of type. If a section has a
heading followed by paragraphs, the first paragraph does not match `p:nth-child(1)` —
that position belongs to the heading. When counting by type is needed, `:nth-of-type()` is
written; it counts only siblings that carry the same element name.

#### Logical Selectors

Four pseudo-classes take a selector list and perform a logical operation.

`:is()` returns elements that match **any one** of the selectors in the list; it shortens
long selectors. `:where()` performs the same match but contributes **zero** to
specificity — why this distinction matters is shown with numbers in the next lesson.

```css
:is(h1, h2, h3) + p { margin-block-start: 0.5em; }
```

This rule selects the same elements as writing three separate rules would.

`:not()` returns elements that match none of the items in the list:

```css
.measurement-table td:not(.name) { text-align: right; }
```

`:has()`, in turn, reverses the direction: it selects an element based on **its content**.
The selector `tr:has(td.missing)` returns rows that contain a missing cell. This is
something combinators cannot do; a combinator could only select the element on the right.

Whether a selector is recognized by implementations can be tested. Earlier lessons showed
that an unrecognized selector drops the entire rule; that also gives a way to test for it:

```css
@supports selector(:has(*)) {
  tr:has(td.missing) { background-color: #fbf4f4; }
}
```

The `@supports` block ignores its entire contents if the condition inside it is not met.
Writing a feature check is different from assuming a feature is supported: when the
condition is not met, what state the page is left in is decided by the author.

#### A Pseudo-Element Targets a Part

A **pseudo-element** is written with two colons and targets a part that is not present in
the document tree.

`::first-line` selects a paragraph's first line, `::first-letter` its first letter. Neither
of these exists as an element in the document; the line break is a result of layout, and
the first line's extent changes when the box's width changes.

`::marker` targets a list item's bullet or number, `::placeholder` a form field's
placeholder text, `::selection` the text range the user has selected.

`::before` and `::after` are different: they do not select a part, they **generate** one.
Neither exists without the `content` property being written.

```css
td.missing::after { content: " —"; }
```

This rule appends a dash to the end of a missing-measurement cell's content. The generated
content is inside the element, not outside it: `::before` behaves like the element's first
child, `::after` like its last.

Generated content has two limits, and both are by design.

First, **it is not in the document**. Being selectable as text and copied, being indexed
by a search engine, or being conveyed to assistive technology is not guaranteed. For this
reason, no text carrying meaning is ever written here. The information "missing" should be
stated in the document with a class and, where needed, visible text; a pseudo-element
should only add a visual indicator.

Second, **it cannot be selected**. No other selector reaches inside a pseudo-element; the
generated content has no substructure of its own.

A note on notation: the double-colon form was defined to separate a pseudo-element from a
pseudo-class. The single-colon notation `:before` is also accepted for compatibility with
older implementations; this course uses the double colon to keep the distinction clear.

#### The Station Page's Status Styles

```css
/* station.css — step 5: states and positions */
.measurement-table tbody tr:nth-child(odd) { background-color: #f5f7f8; }
.measurement-table tbody tr:last-child     { border-block-end: none; }
.measurement-table td:not(.name)           { text-align: right; }
.measurement-table td.missing::after       { content: " —"; color: #8a5b00; }

a:hover                { text-decoration-thickness: 2px; }
a:focus-visible        { outline: 3px solid #143a52; outline-offset: 2px; }
input:invalid          { border-color: #8a1c1c; }
input:disabled         { color: #6b767f; }
```

The first two rules of the second group carry a distinction. `:hover` is a state that only
occurs on pointer devices; a user navigating by keyboard never reaches it. This is why
every hint given with `:hover` should have a focus counterpart.

`:focus-visible` is used instead of `:focus` because the two return different sets:
`:focus` also matches when a field is clicked on with a pointer, `:focus-visible` matches
in cases where a focus indicator is needed. Removing the focus outline entirely (writing
`outline: none`) makes keyboard navigation impossible; if the outline is going to be
removed, a visible replacement should be put in its place.

#### Summary

- A pseudo-class is written with a single colon and selects a state not written in the
  document; it splits into two families — interaction state and position in the tree.
- `:nth-child()` takes an `an+b` formula; `n` starts at zero, and the valid positions the
  formula produces match. `odd` and `even` are named forms of this formula.
- `:nth-child()` counts all siblings, `:nth-of-type()` only those of the same type; the
  difference shows up around interposed elements like a heading.
- `:is()`, `:where()`, `:not()`, and `:has()` take a selector list; `:has()` selects an
  element by its content and provides the reverse direction of a combinator.
- A pseudo-element is written with two colons; `::before` and `::after` generate content
  not present in the document. This content is not reliably reachable as text, so it
  should carry only a visual indicator.

#### Next Step

More than one rule can now match the same element: a `td` by one rule, `.missing` by
another, `.measurement-table td:not(.name)` by a third. If all three write to the
`text-align` property, which one wins? The next lesson answers this question not with
intuition but with a countable ordering: it computes the specificity triple and applies
the cascade's steps in order.

### Cascade and Specificity

Source: https://academia.sh/en/courses/css-fundamentals/cascade-and-specificity

Ordering declarations that write to the same property by source, importance, specificity, and order criteria; computing the specificity triple and why it cannot be read in base ten.

By the end of the previous lessons, more than one rule matches the same element. A cell
matches the `td` selector, the `.measurement-table td` selector, the `td.missing`
selector, and the `#content td` selector, all at once. If all four write to the
`text-align` property, what is the cell's alignment?

The answer to this question is not intuition, it is a defined ordering. The name of that
ordering is **cascade**, and it is where the first letter in the language's name comes
from. This lesson applies the cascade's steps in order and makes each step computable.

#### The Cascade Orders Declarations

An important distinction to start with: the cascade orders not rules, but
**declarations**. The ordering is done for a single element and a single property. Two
declarations of the same rule compete separately; one winning does not mean the other
wins too.

After the declarations valid for an element-property pair are collected, four criteria are
applied in order. If a winner emerges at one criterion, the next criterion is not
consulted.

1. **Source and importance.** Where the declaration comes from, and whether it carries
   `!important`.
2. **Being inline.** Declarations coming from the element's `style` attribute go first.
3. **Specificity.** How narrow a set the selector targets.
4. **Order.** Among declarations of the same specificity, the one written later wins.

There are three sources: the browser's own default style, styles the user has defined,
and styles the document's author has written. Their normal ordering, weakest to strongest,
is: browser, user, author.

#### Specificity Is a Triple

Specificity is not a single number, it is an ordered triple of three numbers: $(a, b, c)$.

- $a$: the number of id selectors in the selector.
- $b$: the total number of class selectors, attribute selectors, and pseudo-classes.
- $c$: the total number of type selectors and pseudo-elements.

The universal selector `*` and combinators contribute to no column.

The following program converts a selector string into this triple.

```js
// specificity.mjs — converts a selector string into the (a, b, c) triple
function specificity(selector) {
  let s = selector;
  let a = 0, b = 0, c = 0;

  // :where(...) contributes nothing to specificity -> strip first
  s = s.replace(/:where\([^()]*\)/g, "");

  // :is( ) / :not( ) / :has( ) -> inherit the highest specificity inside
  const wrapping = /:(?:is|not|has)\(([^()]*)\)/g;
  let m;
  while ((m = wrapping.exec(s)) !== null) {
    const highest = m[1]
      .split(",")
      .map((p) => specificity(p.trim()))
      .reduce((top, o) => (compare(o, top) > 0 ? o : top), [0, 0, 0]);
    a += highest[0]; b += highest[1]; c += highest[2];
  }
  s = s.replace(wrapping, "");

  // pseudo-elements (::x) -> c; pseudo-classes (:x) -> b
  const pseudoElement = s.match(/::[a-z-]+/g) ?? [];
  c += pseudoElement.length;
  s = s.replace(/::[a-z-]+/g, "");

  a += (s.match(/#[\w-]+/g) ?? []).length;
  s = s.replace(/#[\w-]+/g, "");

  b += (s.match(/\.[\w-]+/g) ?? []).length;      // class
  s = s.replace(/\.[\w-]+/g, "");
  b += (s.match(/\[[^\]]*\]/g) ?? []).length;    // attribute
  s = s.replace(/\[[^\]]*\]/g, "");
  b += (s.match(/:[a-z-]+(\([^()]*\))?/g) ?? []).length; // pseudo-class
  s = s.replace(/:[a-z-]+(\([^()]*\))?/g, "");

  c += (s.match(/[a-z][\w-]*/gi) ?? []).length;  // type selector (* not counted)
  return [a, b, c];
}

const compare = (x, y) => x[0] - y[0] || x[1] - y[1] || x[2] - y[2];

const selectors = [
  "*",
  "td",
  ".measurement-table td",
  "td.missing",
  "tbody > tr > td",
  ".measurement-table tbody td:not(.name)",
  "input[type=\"number\"]",
  "#content td",
  ".measurement-table td.value.missing",
  "td.missing::after",
  ":where(.measurement-table) td",
  ":is(#content, .side) p",
  "tr:has(td.missing)",
];

for (const s of selectors) {
  const [a, b, c] = specificity(s);
  console.log(`${a},${b},${c}   ${s}`);
}
```

```
0,0,0   *
0,0,1   td
0,1,1   .measurement-table td
0,1,1   td.missing
0,0,3   tbody > tr > td
0,2,2   .measurement-table tbody td:not(.name)
0,1,1   input[type="number"]
1,0,1   #content td
0,3,1   .measurement-table td.value.missing
0,1,2   td.missing::after
0,0,1   :where(.measurement-table) td
1,0,1   :is(#content, .side) p
0,1,2   tr:has(td.missing)
```

Five lines call for a further reading.

`.measurement-table td` and `td.missing` gave the **same** triple: each has one class and
one type. A selector's length or how "specific it looks" does not enter the calculation;
only the counts do.

`:not()`, `:is()`, and `:has()` are not counted themselves; they inherit the **highest
specificity** among the members of their list. `:is(#content, .side) p` therefore gave
$1,0,1$ — because the list contains one id selector.

`:where()`, on the other hand, contributes zero. `:where(.measurement-table) td`'s triple
is $0,0,1$, exactly as if only `td` had been written. This is a way to narrow a selector
without raising specificity.

A pseudo-element counts toward the third column: `td.missing::after`'s triple has $c = 2$.

#### The Triple Is Not a Base-Ten Number

A common mistake is reading the triple as a hundreds-tens-ones digit. This reading breaks
down at values of ten and above, because there is no carrying between columns. The
comparison is lexicographic order: first $a$, if tied $b$, if tied $c$.

```js
// not-decimal.mjs — the specificity triple is not a base-ten number
const compare = (x, y) => x[0] - y[0] || x[1] - y[1] || x[2] - y[2];
const pairs = [
  [[0, 11, 0], [1, 0, 0]],
  [[0, 0, 12], [0, 1, 0]],
  [[1, 0, 0], [0, 99, 99]],
];
for (const [x, y] of pairs) {
  const s = compare(x, y);
  const winner = s > 0 ? x : s < 0 ? y : "tie";
  console.log(`${x.join(",")} vs ${y.join(",")} -> winner ${Array.isArray(winner) ? winner.join(",") : winner}`);
  console.log(`   read as base ten: ${x[0]*100+x[1]*10+x[2]} vs ${y[0]*100+y[1]*10+y[2]}`);
}
```

```
0,11,0 vs 1,0,0 -> winner 1,0,0
   read as base ten: 110 vs 100
0,0,12 vs 0,1,0 -> winner 0,1,0
   read as base ten: 12 vs 10
1,0,0 vs 0,99,99 -> winner 1,0,0
   read as base ten: 100 vs 1089
```

The first line is critical: a selector carrying eleven classes cannot beat a single id
selector. Reading it as base ten would say the opposite. The result is that the only way to
override a declaration written with an id selector is to write an id selector as well.
This is the reason for the "style is written with a class" rule from the selectors lesson.

#### How the Cascade Is Applied

The following program puts seven declarations into full order. Source layers are numbered
weakest to strongest; the ordering criteria are applied in sequence.

```js
// cascade.mjs — orders declarations that write to the same property and finds the winner
// (specificity calculation is from the previous program; given by hand here)

// layer order: smaller number = weaker
const LAYER = {
  "browser-normal": 0,
  "user-normal": 1,
  "author-normal": 2,
  "author-inline": 3,
  "author-important": 4,
  "user-important": 5,
  "browser-important": 6,
};

const declarations = [
  { source: "browser-normal",  selector: "td",                        specificity: [0,0,1], order: 1, value: "left"    },
  { source: "author-normal",   selector: "td",                        specificity: [0,0,1], order: 2, value: "left"    },
  { source: "author-normal",   selector: ".measurement-table td",     specificity: [0,1,1], order: 3, value: "right"   },
  { source: "author-normal",   selector: "td.name",                   specificity: [0,1,1], order: 4, value: "start"   },
  { source: "author-normal",   selector: "#content td",               specificity: [1,0,1], order: 5, value: "center"  },
  { source: "author-important",selector: ".measurement-table td.missing", specificity: [0,2,1], order: 6, value: "end" },
  { source: "user-important",  selector: "td",                        specificity: [0,0,1], order: 7, value: "justify" },
];

const compare = (x, y) =>
  LAYER[x.source] - LAYER[y.source] ||
  x.specificity[0] - y.specificity[0] ||
  x.specificity[1] - y.specificity[1] ||
  x.specificity[2] - y.specificity[2] ||
  x.order - y.order;

function resolve(candidates) {
  const sorted = [...candidates].sort(compare);
  console.log("weakest to strongest:");
  for (const d of sorted) {
    const s = d.specificity.join(",");
    console.log(`  ${d.source.padEnd(17)} ${s}  order=${d.order}  ${d.selector.padEnd(28)} -> ${d.value}`);
  }
  console.log(`winner: text-align: ${sorted.at(-1).value}`);
  console.log("");
}

console.log("=== all declarations ===");
resolve(declarations);

console.log("=== author-normal only ===");
resolve(declarations.filter((d) => d.source === "author-normal"));

console.log("=== specificity tied, order decides ===");
resolve(declarations.filter((d) => d.source === "author-normal" && d.specificity.join() === "0,1,1"));
```

```
=== all declarations ===
weakest to strongest:
  browser-normal    0,0,1  order=1  td                           -> left
  author-normal     0,0,1  order=2  td                           -> left
  author-normal     0,1,1  order=3  .measurement-table td        -> right
  author-normal     0,1,1  order=4  td.name                      -> start
  author-normal     1,0,1  order=5  #content td                  -> center
  author-important  0,2,1  order=6  .measurement-table td.missing -> end
  user-important    0,0,1  order=7  td                           -> justify
winner: text-align: justify

=== author-normal only ===
weakest to strongest:
  author-normal     0,0,1  order=2  td                           -> left
  author-normal     0,1,1  order=3  .measurement-table td        -> right
  author-normal     0,1,1  order=4  td.name                      -> start
  author-normal     1,0,1  order=5  #content td                  -> center
winner: text-align: center

=== specificity tied, order decides ===
weakest to strongest:
  author-normal     0,1,1  order=3  .measurement-table td        -> right
  author-normal     0,1,1  order=4  td.name                      -> start
winner: text-align: start

```

The second run shows the criteria being applied **in order**: staying within a single
source, the winner was determined by specificity. In the third run, specificity was also
tied, and the decision came down only to write order; the one written later won.

#### The Importance Flag Reverses the Ordering

When `!important` is written at the end of a declaration, that declaration moves to a
separate layer. The order in the layer list must be read carefully: for important
declarations, source ordering **reverses**. In normal declarations, author beats user; in
important declarations, user beats author.

This reversal is not an accident, it is a design decision. A style a user writes to meet
their own needs — enlarging text, increasing contrast, stopping motion — should be able to
override the page author's decisions. The output's first block, with the
`user-important` layer sitting at the very top, shows exactly this.

On the author's side, the cost of writing `!important` is opening an exception layer in
one's own style file. Once written, the only way to override it is to write
`!important` again, and at that point the specificity calculation starts over. The
actionable rule is: `!important` is written not because a conflict cannot be resolved, but
because resolving it was not attempted; lowering specificity is tried first.

There is another mechanism that sits between source and importance: **cascade layers**
defined with `@layer`. Author style opens named layers, and layer order is evaluated
**before** specificity; this way, an entire base layer, whatever its specificity, can be
overridden by a layer above it.

#### Keeping Specificity Low

This lesson's practical result is a writing discipline.

- Selectors are kept as **flat** as possible. Writing `.measurement-table .value` instead
  of `.measurement-table tbody tr td.value` reaches the same element with a lower triple.
- An id selector is not used in style; a rule that opens the first column once forces
  every later fix to write an id as well.
- If a selector needs to be narrowed by context, `:where()` is written; the narrowing
  happens, specificity does not rise.

```css
/* station.css — step 6: notation that keeps specificity flat */
:where(.measurement-table) .value { text-align: right; }   /* 0,1,0 */
:where(.measurement-table) .name  { text-align: start; }   /* 0,1,0 */
.missing                          { color: #8a1c1c; }      /* 0,1,0 */
```

All three rules carry the same triple. This means that conflicts between them are
resolved only by write order — the state that is easiest to read and predict.

#### Summary

- The cascade orders not rules but declarations; the ordering is done for a single
  element and a single property.
- The criteria are applied in order: source and importance, being inline, specificity,
  write order. If an earlier criterion produces a winner, the next one is not consulted.
- Specificity is the $(a, b, c)$ triple: id count, class–attribute–pseudo-class count,
  type–pseudo-element count. The comparison is lexicographic; there is no carrying
  between columns.
- `:is()`, `:not()`, and `:has()` inherit the highest specificity inside them;
  `:where()` contributes zero and allows narrowing without raising specificity.
- `!important` moves a declaration to a separate layer and reverses source ordering; this
  is defined so that user style can override author decisions.

#### Next Step

The cascade picks the winner among declarations that **match** an element. What if no
declaration matches at all? If no rule writes `color` to a paragraph, what is that
paragraph's color? The next lesson shows that this gap is filled by two separate
mechanisms — inheritance and initial value — and defines the keywords that call each of
them explicitly.

### Inheritance and Default Values

Source: https://academia.sh/en/courses/css-fundamentals/inheritance-and-default-values

How the value of a property no declaration reaches is determined; inherited and non-inherited properties, initial value, and the inherit, initial, unset, and revert keywords.

The cascade picks the winner among declarations that match an element. One problem
remains: what happens if no declaration matches at all? If no rule writes `color` to a
cell in the measurement table, what is that cell's text color?

The gap is filled by two separate mechanisms. Some properties **inherit** their value from
the parent element; the rest fall back to a defined **initial value**. This lesson
separates the two mechanisms, gives the reason behind which property is subject to which
one, and defines the keywords that call each of them explicitly.

#### Inheritance Works Down the Tree

**Inheritance** is a property's value passing from parent element to child element down
the document tree. Not every property inherits; which property inherits is part of the
property's definition, and it is not arbitrary.

Roughly: properties that **determine the appearance of text** inherit — `color`,
`font-family`, `font-size`, `font-weight`, `line-height`, `text-align`,
`letter-spacing`, `visibility`. Properties that **determine the geometry of a box** do not
inherit — `margin`, `padding`, `border`, `width`, `height`, `background`, `display`,
`position`.

The reason for the distinction is usefulness. Setting a section's typeface once and
having all the text inside it take it on is an ordinary thing to want; giving a section a
border and wanting every paragraph inside it to also get a border is not. Inheritance is
open in decisions where repetition is ordinary.

The inherited value is the parent element's **computed** value, not the text written in
the source. If a parent element has `font-size: 150%` written on it, what passes to the
child is not the text `150%`, but that percentage's resolved result. This distinction is
handled with numbers in the units lesson.

#### Initial Value Is Not the Browser Default

If no declaration reaches a non-inherited property, the **initial value** is used. This
value is written into the property's definition and is independent of implementation.

There is a distinction here that is commonly confused. A `div` element producing a block
box does not come from the `display` property's initial value — the `display` property's
initial value is `inline`. A `div` element being block is because the browser's own
default style file has `div { display: block }` written in it.

The distinction can be summarized as: **the initial value comes from the language's
definition, the browser default comes from a style file.** The first takes effect not when
no rule is written, but when no rule **wins**; the second is a declaration capable of
winning and, as the previous lesson showed, is the weakest source in the cascade.

#### Running Value Resolution

The following program takes declarations that won the cascade as input and walks down the
tree, resolving each element's value for four properties. Each line ends with where the
value came **from**.

```js
// inheritance.mjs — runs value resolution down the tree
const INHERITED = new Set(["color", "font-family", "line-height", "text-align", "visibility"]);
const INITIAL = {
  "color": "canvastext", "font-family": "serif", "line-height": "normal",
  "text-align": "start", "visibility": "visible", "border-color": "currentcolor",
  "background-color": "transparent",
};

// winning declarations (cascade result) — node path -> {property: value}
const winners = {
  "body":                         { "color": "#1c2733", "font-family": "system-ui" },
  "body/main":                    {},
  "body/main/table":              { "text-align": "right", "border-color": "#d5dbe0" },
  "body/main/table/tr":           {},
  "body/main/table/tr/td.name":   { "text-align": "start" },
  "body/main/table/tr/td.missing":{ "color": "inherit", "text-align": "unset",
                                     "border-color": "unset", "font-family": "initial" },
};

const tree = { name: "body", children: [ { name: "main", children: [ { name: "table", children: [
  { name: "tr", children: [ { name: "td.name", children: [] }, { name: "td.missing", children: [] } ] },
] } ] } ] };

const PROPERTIES = ["color", "font-family", "text-align", "border-color"];

function resolve(node, path, parentValues) {
  const fullPath = path ? `${path}/${node.name}` : node.name;
  const written = winners[fullPath] ?? {};
  const values = {};
  for (const prop of PROPERTIES) {
    let v = written[prop];
    let source;
    if (v === undefined) {
      // no declaration: take from parent if inherited, else initial value
      if (INHERITED.has(prop) && parentValues) { v = parentValues[prop]; source = "inherited"; }
      else { v = INITIAL[prop]; source = "initial"; }
    } else if (v === "inherit") {
      v = parentValues ? parentValues[prop] : INITIAL[prop]; source = "inherit";
    } else if (v === "initial") {
      v = INITIAL[prop]; source = "initial-keyword";
    } else if (v === "unset") {
      if (INHERITED.has(prop) && parentValues) { v = parentValues[prop]; source = "unset->inherited"; }
      else { v = INITIAL[prop]; source = "unset->initial"; }
    } else {
      source = "declared";
    }
    values[prop] = v;
    console.log(`  ${prop.padEnd(12)} = ${String(v).padEnd(12)} (${source})`);
  }
  for (const c of node.children) {
    console.log(`${fullPath}/${c.name}`);
    resolve(c, fullPath, values);
  }
}

console.log("body");
resolve(tree, "", null);
```

```
body
  color        = #1c2733      (declared)
  font-family  = system-ui    (declared)
  text-align   = start        (initial)
  border-color = currentcolor (initial)
body/main
  color        = #1c2733      (inherited)
  font-family  = system-ui    (inherited)
  text-align   = start        (inherited)
  border-color = currentcolor (initial)
body/main/table
  color        = #1c2733      (inherited)
  font-family  = system-ui    (inherited)
  text-align   = right        (declared)
  border-color = #d5dbe0      (declared)
body/main/table/tr
  color        = #1c2733      (inherited)
  font-family  = system-ui    (inherited)
  text-align   = right        (inherited)
  border-color = currentcolor (initial)
body/main/table/tr/td.name
  color        = #1c2733      (inherited)
  font-family  = system-ui    (inherited)
  text-align   = start        (declared)
  border-color = currentcolor (initial)
body/main/table/tr/td.missing
  color        = #1c2733      (inherit)
  font-family  = serif        (initial-keyword)
  text-align   = right        (unset->inherited)
  border-color = currentcolor (unset->initial)
```

Three observations follow from the output.

`color` and `font-family` were only declared on the `body` element, and carried down five
generations to the cells. A declaration written at the root of the tree affects the entire
subtree.

`border-color` was `#d5dbe0` at the table but dropped back to `currentcolor` at the row.
Border color is not an inherited property; a border color given to the table does not
pass to the cells. This is why setting up a border on a table requires writing a rule to
the cells as well.

`text-align` became `right` at the table and inherited down to the row and cells; a
declaration on the `td.name` element overrode it to `start`. Inheritance does not hold
once there is a declaration on the child element — inheritance is the weakest source and
is always overridden by a declaration.

#### Four Keywords

There are four keywords that can be written to any property's value. The output's last
block shows three of the four.

`inherit` — forces even a non-inherited property to be taken from the parent element.
`color: inherit` was written on `td.missing`, and the value came from above. This is
meaningful on a non-inherited property; writing `border-color: inherit` is a way to pull
the border color from above.

`initial` — returns to the property's initial value. In the output, the cell with
`font-family: initial` dropped to the value `serif`; the inheritance of `system-ui` from
above was cut. This result is often not the one desired: `initial` goes back to the
language's definition, not to the page's design.

`unset` — behaves conditionally: if the property is an inherited one, it acts like
`inherit`; if not, like `initial`. In the output, `text-align: unset` dropped to
inheritance, `border-color: unset` to the initial value. This is the answer to the
question "what would this be if this declaration had never been written at all."

`revert` — is the fourth, and answers a different question: it returns the value to what a **weaker
source** would have won. Writing `revert` in author style means going back, on that property, to the
previous origin: to the user style if it has a rule for that property, otherwise to the browser default.
The difference from `initial` shows up here: writing `color: initial` on a link takes the color to the
language's initial value, while `color: revert` takes it to the color the browser has defined for links.

All four can be written to every property at once with the `all` shorthand. `all: revert`
isolates an element from author style; `all: unset` keeps inherited properties and resets
the rest.

#### Value Stages

The term "computed value," which came up in this section, needs to be defined. A
declaration's value goes through several stages from what is written to what ends up on
screen:

| Stage | What happens |
|---|---|
| declared value | The text written in the source |
| cascaded value | The declaration that won the cascade |
| specified value | If there is no winner, filled in with inheritance or initial value |
| computed value | Relative units resolved, the form passed on by inheritance |
| used value | The final form once layout has been performed |

The stage carried by inheritance is the **computed** value. If a parent element has
`font-size: 1.5em` written on it, the child does not inherit the text `1.5em`, it inherits
that expression's resolved result — otherwise it would be multiplied again at every
generation, and the font size would grow exponentially.

The used-value stage, on the other hand, waits for layout. In the declaration
`width: 50%`, the computed value is still a percentage; the exact number only emerges once
the containing box's width is known. This stage will be covered in detail in the box model
topic.

#### Using Inheritance in the Station Page

```css
/* station.css — step 7: using inheritance as a base */
body {
  color: #1c2733;
  font-family: system-ui, sans-serif;
  line-height: 1.5;
}

/* form controls do not inherit typography by default */
input, select, textarea, button {
  font: inherit;
  color: inherit;
}

.measurement-table { border-collapse: collapse; }
.measurement-table th,
.measurement-table td { border-block-end: 1px solid #d5dbe0; }
```

The second rule closes an exception. Form controls do not take their typography from the
document; they carry their own default typefaces. Writing `font: inherit` removes this
exception and ties the controls to the document's typography.

The third group shows that, because border color is not inherited, the border has to be
written to the cells separately — the direct consequence of the behavior seen in the
output's fourth block.

#### Summary

- A property either inherits or, if there is no declaration, falls back to its initial
  value; which applies is written in the property's definition, and text properties
  inherit while box-geometry properties do not.
- The initial value comes from the language's definition; the browser's default
  appearance comes from a style file and is the weakest source in the cascade.
  `display: block` is not an initial value, it is a declaration written in that file.
- Inheritance is the weakest source: if there is a declaration on the child element for
  that property, inheritance does not apply.
- `inherit` forces the value from above, `initial` returns to the language's initial
  value, `unset` picks between the two based on the property's kind, `revert` returns to
  the value of a weaker source.
- What inheritance carries is the computed value; relative units are resolved before
  being carried, otherwise they would be multiplied again at every generation.

#### Next Step

Throughout this topic, which elements rules reach and which value wins have been
resolved. The value is settled, but what that value means has not been established yet.
Where on a cell does `padding: 8px` add eight pixels? What is the total width of a box
given a width? The next topic defines the model every element turns into a box, and works
through that box's sections with arithmetic.

## The Box Model and Flow

Every element turning into a box, the arithmetic of the box's regions, the rules of normal flow, and the positioning and stacking order that take a box out of flow.

### The Box Model

Source: https://academia.sh/en/courses/css-fundamentals/box-model

Every element turning into a box made of four nested rectangles; the boundaries and arithmetic of the content, padding, border, and margin regions.

The previous topic resolved which declarations an element receives. The value is now settled:
a cell's `padding` value is `8px 12px`. But what that value means was never said. Where on
the cell do the 8 pixels get added? Does the added space grow the cell's total width, or does
it shrink the area of the text inside it?

The answer to these questions is the **box model**. The model is summed up in one sentence:
every element in the document tree turns into one or more rectangular boxes during layout, and
each box is made of four nested regions. This lesson defines those four regions and the
arithmetic between them.

#### Four Regions

From outside in:

**The margin box** is the outermost rectangle. It carries the distance between the box and its
neighbors. It is transparent; no background is painted here.

**The border box** sits inside the margin. The border itself is drawn on this region's
boundary.

**The padding box** sits inside the border. It carries the distance between the content and
the border. Unlike the margin, **the background extends this far**.

**The content box** is the innermost rectangle. Text, an image, or child boxes sit here.

Four boundaries separate the regions, and each has its own name: the content edge, the
padding edge, the border edge, the margin edge. These names appear in the values of
properties like `background-clip`, `background-origin`, and `box-sizing`.

#### Arithmetic

Each region's size is found by adding two edges to the region inside it. On the horizontal
axis:

$$\text{padding width} = \text{content width} + p_{\text{left}} + p_{\text{right}}$$
$$\text{border width} = \text{padding width} + b_{\text{left}} + b_{\text{right}}$$
$$\text{margin width} = \text{border width} + m_{\text{left}} + m_{\text{right}}$$

On the vertical axis the same sums are made with the top and bottom edges. The program below
runs these sums for two boxes: a cell in the measurement table and the page's masthead.

```js
// box-measure.mjs — computes the bounds of a box's four regions
function measureBox({ contentW, contentH, padding, border, margin }) {
  const [pt, pr, pb, pl] = padding;   // top, right, bottom, left
  const [bt, br, bb, bl] = border;
  const [mt, mr, mb, ml] = margin;
  const paddingBoxW = contentW + pl + pr;
  const paddingBoxH = contentH + pt + pb;
  const borderBoxW = paddingBoxW + bl + br;
  const borderBoxH = paddingBoxH + bt + bb;
  const marginBoxW = borderBoxW + ml + mr;
  const marginBoxH = borderBoxH + mt + mb;
  return { paddingBoxW, paddingBoxH, borderBoxW, borderBoxH, marginBoxW, marginBoxH };
}

const cell = {
  contentW: 120, contentH: 24,
  padding: [8, 12, 8, 12],
  border:  [0, 0, 1, 0],
  margin:  [0, 0, 0, 0],
};
const masthead = {
  contentW: 640, contentH: 96,
  padding: [24, 32, 24, 32],
  border:  [0, 0, 4, 0],
  margin:  [0, 16, 32, 16],
};

for (const [name, box] of [["cell", cell], ["masthead", masthead]]) {
  const r = measureBox(box);
  console.log(`--- ${name} ---`);
  console.log(`content     : ${box.contentW} x ${box.contentH}`);
  console.log(`padding box : ${r.paddingBoxW} x ${r.paddingBoxH}`);
  console.log(`border box  : ${r.borderBoxW} x ${r.borderBoxH}`);
  console.log(`margin box  : ${r.marginBoxW} x ${r.marginBoxH}`);
  console.log(`painted area (border box)  : ${r.borderBoxW * r.borderBoxH} px2`);
  console.log(`space occupied (margin box): ${r.marginBoxW * r.marginBoxH} px2`);
}
```

```
--- cell ---
content     : 120 x 24
padding box : 144 x 40
border box  : 144 x 41
margin box  : 144 x 41
painted area (border box)  : 5904 px2
space occupied (margin box): 5904 px2
--- masthead ---
content     : 640 x 96
padding box : 704 x 144
border box  : 704 x 148
margin box  : 736 x 180
painted area (border box)  : 104192 px2
space occupied (margin box): 132480 px2
```

The cell shows two observations. The `padding: 8px 12px` declaration, once expanded into four
values, added $12 + 12 = 24$ pixels horizontally and $8 + 8 = 16$ pixels vertically: content
width went from 120 to 144, height from 24 to 40. **Padding does not shrink the text's area;
it grows the box.** This is the answer to the question the previous topic asked.

Since only a bottom border was given, the width did not change and the height grew by 1
pixel. Because the margin is zero, the border box and the margin box are the same.

The masthead shows the effect of the margin: the border box is 704 units wide while the
margin box is 736. The 32 units in between are not painted, but they hold neighboring boxes at
a distance. The difference between the two area calculations — 132480 and 104192 — gives this
as a number.

#### Which Property Applies to Which Region

The box model's practical value is knowing how far each visual property extends.

| Property | Region it covers |
|---|---|
| `background-color` | Content + padding + border (up to the border box) |
| `background-image` | Placed in the padding box by default, painted up to the border box |
| `border` | On the boundary of the border box |
| `box-shadow` | Outside the border box (or inside it with `inset`) |
| `outline` | Outside the border box, without taking up space |

Two rows deserve extra emphasis.

**The background covers the padding.** When a button gets a background color and padding, the
colored area is larger than the text — the padding has been painted. If the same space had
been given as margin instead, the colored area would be only as large as the text. This is why
the two declarations have different visual results.

**An outline takes up no space.** `outline` is drawn outside the border box but does not
affect layout; neighboring boxes sit as if nothing were there. This is why the focus indicator
is given with `outline`: the page does not shift when focus changes. If the same job were done
with `border`, the box would grow on every focused element and everything around it would
shift.

#### Logical Edges

There are two ways to name edges. Physical names are based on the screen: `margin-top`,
`padding-left`, `border-right`. Logical names are based on writing direction:
`margin-block-start`, `padding-inline-start`, `border-inline-end`.

Logical names define two axes. The **inline axis** is the direction text flows in; in a
document written left to right, that is left to right. The **block axis** is the direction
lines stack in; in the same document, top to bottom.

In a document written right to left, `padding-inline-start` lands on the right edge, while
`padding-left` still stays on the left edge. Logical names are preferred so that the style
does not need to be rewritten when the document's language changes. Logical names are used
throughout this course; the information the `lang` attribute carries, from the Web
Fundamentals and HTML course, finds its counterpart here.

Shorthands for all four edges also exist in logical form: `margin-block` sets both block
edges at once, `margin-inline` sets both inline edges. The `margin-inline: auto` notation is
the standard way to center a block horizontally.

#### The Station Page's Dimensions

```css
/* station.css — step 8: box dimensions */
.masthead {
  padding-block: 24px;
  padding-inline: 32px;
  margin-inline: 16px;
  margin-block-end: 32px;
  border-block-end: 4px solid #143a52;
  background-color: #f5f7f8;
}

.measurement-table th,
.measurement-table td {
  padding-block: 8px;
  padding-inline: 12px;
  border-block-end: 1px solid #d5dbe0;
}
```

The masthead's background color covers the padding; a gap of 24 and 32 units remains between
the heading and the edge of the colored area. The margin, on the other hand, sits outside the
colored area and keeps the masthead away from the edges.

In the cells, padding is 12 units horizontally, 8 vertically. What makes the table readable is
not the text itself but this gap between cells: once the padding is removed, columns stick
together and the bottom border alone is not enough to tell columns apart.

#### Summary

- Every element turns into a box during layout; the box is made, from outside in, of the
  margin, border, padding, and content regions.
- Each region's size is found by adding two edges to the one inside it; padding does not
  shrink the content, it grows the box.
- The background extends up to the border box and covers the padding; the margin is
  transparent and only keeps neighbors at a distance.
- `outline` is drawn outside the border box but takes up no space; the focus indicator is
  given with it for this reason — with `border` instead, layout would shift as focus changed.
- Logical edge names resolve according to writing direction; the block axis is the direction
  lines stack in, the inline axis is the direction text flows in.

#### Next Step

This lesson never gave the box a `width`; the content width was given, and the outer
dimensions were computed from it. But writing styles is usually done the other way around: a
width is declared, and the content size is derived from it. There are two separate models for
this derivation, and the difference between them changes a box's total width. The next lesson
compares the two models using the same numbers.

### Box Sizing

Source: https://academia.sh/en/courses/css-fundamentals/box-sizing

Which region a declared width measures; comparing the content-box and border-box models with the same numbers, and the effect of minimum and maximum bounds.

The previous lesson computed a box's regions starting from the content size and working
outward. Writing styles is usually done the other way around: a `width` is declared, and the
remaining dimensions are derived from it. This raises a question — which region does the
declared width measure?

There are two answers to this question, and both are defined. Which one applies is chosen with
the `box-sizing` property. This lesson compares the two models with the same numbers.

#### Two Models

`box-sizing: content-box` — the declared width measures the **content box**. Padding and
border are added outside this value; the space the box occupies is larger than the declared
value. This is the property's initial value.

`box-sizing: border-box` — the declared width measures the **border box**. Padding and border
are taken out of this value; the content box is whatever remains.

The difference between the two is not a matter of preference, it is different arithmetic:

$$\text{content-box:}\quad W_{\text{border}} = w + p_{\text{left}} + p_{\text{right}} + b_{\text{left}} + b_{\text{right}}$$
$$\text{border-box:}\quad W_{\text{content}} = w - p_{\text{left}} - p_{\text{right}} - b_{\text{left}} - b_{\text{right}}$$

```js
// box-sizing.mjs — gives the content-box vs border-box difference as a number
function resolve({ width, padding, border, boxSizing }) {
  const [pl, pr] = padding, [bl, br] = border;
  const extra = pl + pr + bl + br;
  if (boxSizing === "content-box") {
    return { content: width, borderBox: width + extra };
  }
  // border-box: the declared value is the border box; content is what remains
  return { content: Math.max(0, width - extra), borderBox: width };
}

const cases = [
  { name: "card",       width: 300, padding: [16, 16], border: [1, 1] },
  { name: "narrow box", width: 40,  padding: [16, 16], border: [1, 1] },
];

for (const c of cases) {
  for (const model of ["content-box", "border-box"]) {
    const r = resolve({ ...c, boxSizing: model });
    console.log(`${c.name.padEnd(11)} ${model.padEnd(11)} width=${c.width}  -> content=${r.content}  borderBox=${r.borderBox}`);
  }
}

console.log("--- three columns, each 33.333%, padding 16, border 1 ---");
const container = 960;
const percent = 100 / 3;
for (const model of ["content-box", "border-box"]) {
  const w = container * percent / 100;
  const r = resolve({ width: w, padding: [16, 16], border: [1, 1], boxSizing: model });
  const total = r.borderBox * 3;
  console.log(`${model.padEnd(11)} column border box=${r.borderBox.toFixed(2)}  sum of three=${total.toFixed(2)}  overflow=${(total - container).toFixed(2)}`);
}
```

```
card        content-box width=300  -> content=300  borderBox=334
card        border-box  width=300  -> content=266  borderBox=300
narrow box  content-box width=40  -> content=40  borderBox=74
narrow box  border-box  width=40  -> content=6  borderBox=40
--- three columns, each 33.333%, padding 16, border 1 ---
content-box column border box=354.00  sum of three=1062.00  overflow=102.00
border-box  column border box=320.00  sum of three=960.00  overflow=0.00
```

#### What the Numbers Say

In the first two lines, the same declaration produced two different boxes: a card declaring
`width: 300px` occupied 334 units in the `content-box` model, and exactly 300 units in the
`border-box` model. The 34 units in between are the sum of padding and border ($16 + 16 + 1 +
1$).

The narrow box line shows an edge case. In the `border-box` model, for a box given
`width: 40px`, only 6 units are left for the content once the 34-unit addition is subtracted.
If the sum of the addition had exceeded the declared width, the content size would be clamped
to zero; it never goes negative. This means the box does not become invisible — the border box
is still 40 units, but nothing fits inside it.

The third block shows the actual practical problem. To place three columns in a 960-unit
container, when each is given `width: 33.333%`, the `content-box` model comes to a total of
1062 units — a 102-unit overflow. The cause is that the percentage measures the **content
box**, and padding and border are added on top of that. The same declaration fits exactly in
the `border-box` model.

This explains why `border-box` is a common baseline choice: it makes the space a box occupies
independent of that box's own padding. When padding is raised from 16 to 24, the layout does
not break; only the space left for the content shrinks.

The model is set uniformly across the whole document:

```css
*, *::before, *::after { box-sizing: border-box; }
```

The universal selector does not cover pseudo-elements, so all three are written together.
Since its specificity is $0,0,0$, any later rule can override it.

#### Height Behaves Differently

`width` and `height` look symmetric, but their behaviors diverge.

A block box's width fills the containing box when it is not declared. Its height, when not
declared, is determined **by its content**. This asymmetry is a consequence of normal flow and
will be taken up in later lessons.

One result of this is that a `height` percentage often does not work. A `height: 50%`
declaration cannot be resolved unless the containing box's height is known; if the containing
box's height also depends on its own content, a circular definition forms. In this case, the
percentage is treated like `auto`.

Giving a fixed height is also fragile: when text is longer than expected, it overflows the
box. Height is not declared unless it is considered together with overflow management.

#### Minimum and Maximum Bounds

The `min-width`, `max-width`, `min-height`, and `max-height` properties bound the computed
size. The order of application is defined, and it matters:

1. `width` (or `auto`) is resolved first.
2. If the result exceeds `max-width`, it is brought down to `max-width`.
3. If this result is below `min-width`, it is raised to `min-width`.

The order means `min-width` beats `max-width`. If `min-width: 400px` and `max-width: 300px`
are written together, the result is 400.

The most common use of this trio is building a readable column of text:

```css
main {
  max-width: 70ch;
  margin-inline: auto;
}
```

`max-width` sets an upper bound; the box does not overflow just because the container is
narrow, and when the container is wide, the text does not spread into lines too long to read.
`margin-inline: auto` distributes the remaining space evenly to both sides. Together, these
two give a column that is both bounded and adaptive, without declaring a fixed width.

The `ch` unit will be defined in the next topic; for now it can be read as roughly a
character's width.

#### Sizing the Station Page

```css
/* station.css — step 9: sizing model and bounds */
*, *::before, *::after { box-sizing: border-box; }

main {
  max-width: 70ch;
  margin-inline: auto;
  padding-inline: 16px;
}

.measurement-table { width: 100%; }

.measurement-table .name { width: 40%; }

.location-image img {
  max-width: 100%;
  height: auto;
}
```

The last rule is a frequently needed pair. Giving an image `max-width: 100%` keeps an image
wider than its container from overflowing. `height: auto` preserves the aspect ratio: if only
the width is bounded and the height stays declared in the document, the image gets squashed.

Giving the `.name` column a percentage width acts as a suggestion in table layout; the table
algorithm weighs column widths together with content. A table's sizing behavior is subject to
its own rules and does not follow every detail of the model here.

#### Summary

- Which region a declared `width` measures is chosen with `box-sizing`: `content-box`
  measures the content box, `border-box` measures the border box.
- In the `content-box` model, padding and border are added outside the declared value; with
  percentage widths, this is the direct cause of overflow.
- In the `border-box` model, the content size is whatever remains and never goes negative; if
  the sum of the addition exceeds the declared width, it is clamped to zero.
- Width fills the container when not declared, height is determined by content when not
  declared; this asymmetry is why percentage heights often fail to resolve.
- Bounds apply in order: `max` first, then `min`. This order makes `min` stronger than `max`.

#### Next Step

These two lessons computed a box's dimensions, but what **kind** of box a box is was never
asked. Why does a paragraph occupy its own line while an emphasis element stays inside the
text? Why, when both are given a `width`, is only one affected? The next lesson defines
display types and shows where this difference comes from.

### Display Types

Source: https://academia.sh/en/courses/css-fundamentals/display-types

The behavioral differences between block, inline, and inline-block boxes; the distinction between outer and inner display type, and the three separate ways of hiding an element.

The previous two lessons computed a box's dimensions, but what **kind** of box a box is was
never asked. Why does a paragraph occupy its own line while an emphasis element inside it
stays in the flow of the text? When both are given `width: 200px`, why does only one change?

The difference comes from the `display` property. This lesson compares the three basic types —
block, inline, and inline-block — across four criteria, and shows that hiding is a separate
matter.

#### Three Types, Four Criteria

```js
// display-types.mjs — tabulates which properties the display value affects
const TYPES = {
  "block":        { ownLine: true,  widthApplies: true,  verticalMargin: true,  whitespaceSensitive: false },
  "inline":       { ownLine: false, widthApplies: false, verticalMargin: false, whitespaceSensitive: true  },
  "inline-block": { ownLine: false, widthApplies: true,  verticalMargin: true,  whitespaceSensitive: true  },
  "none":         { ownLine: false, widthApplies: false, verticalMargin: false, whitespaceSensitive: false },
};

const headers = ["display", "own line", "width/height", "vertical margin", "whitespace-sensitive"];
console.log(headers.map((h, i) => h.padEnd(i === 0 ? 14 : 16)).join(""));
for (const [name, props] of Object.entries(TYPES)) {
  const yn = (v) => (v ? "yes" : "no").padEnd(16);
  console.log(name.padEnd(14) + yn(props.ownLine) + yn(props.widthApplies) + yn(props.verticalMargin) + yn(props.whitespaceSensitive));
}

// inline boxes laid side by side: the whitespace character in between is also a box
console.log("\n--- inline layout (whitespace character included) ---");
const pieces = [
  { kind: "inline-block", width: 80 }, { kind: "space", width: 4 },
  { kind: "inline-block", width: 80 }, { kind: "space", width: 4 },
  { kind: "inline-block", width: 80 },
];
let x = 0;
for (const p of pieces) { console.log(`${String(x).padStart(4)} .. ${x + p.width}  ${p.kind}`); x += p.width; }
console.log(`total line width: ${x}  (boxes: ${80 * 3}, spaces: ${x - 240})`);
```

```
display       own line        width/height    vertical margin whitespace-sensitive
block         yes             yes             yes             no              
inline        no              no              no              yes             
inline-block  no              yes             yes             yes             
none          no              no              no              no              

--- inline layout (whitespace character included) ---
   0 .. 80  inline-block
  80 .. 84  space
  84 .. 164  inline-block
 164 .. 168  space
 168 .. 248  inline-block
total line width: 248  (boxes: 240, spaces: 8)
```

The table gives the answer to the previous lesson's questions.

**A block box** fills the containing box's inline axis from end to end and pushes whatever
follows it onto a new line. `width` and `height` apply; margin on all four sides applies too.
A paragraph, a heading, a section, and a list are of this type.

**An inline box** stays in the flow of the text and occupies only as much space as its
content. `width` and `height` declarations have **no effect at all**; the box's size comes
from its content. Vertical margin is also ignored. Emphasis, a link, an abbreviation, and a
general-purpose `span` are of this type.

One warning is needed: on an inline box, horizontal padding and border **do apply**, but the
painted area does not grow the line height. When vertical padding is given to a link, the
colored area grows, the distance between lines does not change, and areas can overlap.

**An inline-block box** sits between the two: from outside it behaves like inline — it does
not occupy its own line, it sits side by side with its neighbors — but from inside it is a
block box; `width`, `height`, and margin on all four sides apply.

#### The Whitespace Character Counts in Inline Layout

The second part of the output shows a commonly encountered problem. When three 80-unit
inline-block boxes are placed side by side, the total line width came out not 240 but 248. The
8 units in between come from the **whitespace characters between the boxes** in the source
text.

The reason is that inline layout is text layout. Line breaks and spaces in the source text are
collapsed into a single space character, and that character also takes up space. Its width
depends on the font; the 4 units here are an example value.

The result is that when three boxes are given `width: 33.333%`, they do not fit on one line —
once the spaces are added, the total exceeds a hundred percent. This is the fragile side of
building grid-like layouts with inline-block, and it disappears in the layout systems in the
next course; in those models, text nodes between boxes take no part in layout.

#### Outer and Inner Display Type

The `display` property does not say one thing, it says two.

**The outer display type** determines how the box behaves *in its container*: `block` or
`inline`.

**The inner display type** determines how the box lays out *its children*: `flow`,
`flow-root`, `flex`, `grid`, `table`.

In two-value notation, the two are given separately:

```css
.nav { display: block flex; }
```

The old single-value notations are shorthand for this pair: the word `block` means
`block flow`, the word `inline-block` means `inline flow-root`, the word `flex` means
`block flex`. This distinction explains why the `inline-block` type is described as
"block from the inside, inline from the outside" — the word really is made of two parts.

The `flow-root` value also deserves mention: it makes the box an independent root that
completes the flow of its children within itself. What this is for is shown in the floating
elements lesson.

#### Changing the Type Does Not Change the Meaning

An element's display type comes from the browser's default stylesheet and can be changed with
a declaration. Writing `li { display: inline; }` lines up list items side by side.

Only the layout changes. The document's meaning — that this is a list item — stays in place,
and programs reading the document tree continue to see it as one. This is a direct consequence
of the structure-presentation distinction built in the Web Fundamentals and HTML course: making
a heading inline does not stop it from being a heading.

The reverse is also true, and it carries a warning: making an element visually resemble
something else does not make it that thing. Giving a `div` element the appearance of a button
does not produce a button that can be focused and activated by keyboard. Appearance is style's
job, behavior and meaning are markup's.

#### Three Ways to Hide

`display: none` was the fourth row in the table, and it gave "no" on every criterion. The
reason is that this value **produces no box**. The element stays in the document tree but
takes no part in layout at all.

There are three separate ways to hide something, and their results differ:

| Notation | Produces a box | Occupies space | Visible to assistive technology |
|---|---|---|---|
| `display: none` | No | No | No |
| `visibility: hidden` | Yes | Yes | No |
| `opacity: 0` | Yes | Yes | Yes |

The third row calls for attention: an element whose opacity is set to zero is invisible but is
still there — it can be focused, clicked, and read by a screen reader. Writing `opacity: 0` to
hide something from the user does not remove it from the accessibility tree.

There is a fourth case as well: hiding text **visually** while leaving it for assistive
technology. This cannot be done with `display: none`; it is done with a rule set that clips
the element to a one-pixel area, and is usually named with a utility class like
`.visually-hidden`.

#### Types on the Station Page

```css
/* station.css — step 10: display types */
.nav li { display: inline-block; margin-inline-end: 12px; }

.measurement-table caption { display: table-caption; text-align: start; }

.status-badge {
  display: inline-block;
  padding-block: 2px;
  padding-inline: 8px;
  border-radius: 4px;
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
```

The `.status-badge` class is defined as inline-block because it wants two things at once:
staying in the flow of the text and being able to take vertical padding. If it were inline,
the padding would be painted but would not grow the line height; if it were block, it would
drop to its own line.

The `.visually-hidden` class carries out the fourth case above: a box is produced, clipped to a
one-pixel area, and it stays in the accessibility tree. It is used in the measurement table to
write a unit visually once in the column header and repeat it for assistive technology in
every cell.

#### Summary

- A block box fills its container's inline axis and occupies its own line; `width`, `height`,
  and margin on all four sides apply.
- An inline box stays in the flow of the text; `width`, `height`, and vertical margin do not
  apply, horizontal padding and border are painted but do not grow the line height.
- An inline-block box is inline from the outside, block from the inside; it lines up side by
  side but takes size and spacing declarations.
- In inline layout, whitespace characters in the source text also take up space; this is why
  the total of boxes placed side by side comes out wider than expected.
- The three ways of hiding give different results: `display: none` produces no box,
  `visibility: hidden` occupies space, `opacity: 0` stays in the accessibility tree and open to
  interaction.

#### Next Step

This lesson defined the type of boxes but did not go into detail on how they are laid out. Why
do block boxes stack top to bottom, by what rule are inline boxes broken into lines, why is the
margin between two block boxes not the sum of the two? The next lesson defines the rules of
normal flow and computes margin collapsing with numbers.

### Flow Layout

Source: https://academia.sh/en/courses/css-fundamentals/flow-layout

Normal flow's block and inline placement rules; the three cases of margin collapsing and its numeric result.

The previous lesson defined the type of boxes: a block box occupies its own line, an inline
box stays in the flow of the text. The word "flow" was used there without being defined. This
lesson defines it.

**Normal flow** is the way boxes are placed when no positioning declaration is written. This
is the default for every layout model, and taking a box out of flow is something that must be
declared separately. Knowing the rules of flow lets most layout problems be solved with fewer
declarations than expected.

#### Block Placement

Block boxes inside a block box stack one after another along the block axis. The rules are
three items:

1. Each block box's size on the inline axis, when not declared, fills the containing box's
   content box.
2. Its size on the block axis, when not declared, is determined by its content.
3. The next box starts after the previous one's margin edge.

The third rule is written incompletely. Two boxes' facing margins are not added together;
they **collapse**.

#### Margin Collapsing

**Margin collapsing** is the reduction of two margins on the block axis into a single margin.
The rule is this:

- If both values are positive, the result is the **larger of the two**.
- If both values are negative, the result is the **smaller of the two** (the one with the
  larger absolute value).
- If one is positive and one is negative, the result is the **sum of the two**.

The program below applies this rule and computes the placement of three paragraphs in flow.

```js
// collapse.mjs — applies the margin collapsing rules
// rule: the result of margins that collapse together
//   if all are positive, the largest
//   if all are negative, the smallest (largest in absolute value)
//   if mixed, largest positive + smallest negative
function collapse(...margins) {
  const pos = margins.filter((m) => m > 0);
  const neg = margins.filter((m) => m < 0);
  const maxPos = pos.length ? Math.max(...pos) : 0;
  const minNeg = neg.length ? Math.min(...neg) : 0;
  return maxPos + minNeg;
}

console.log("--- between two siblings ---");
const cases = [[32, 16], [16, 32], [24, 24], [32, -8], [-8, -16], [0, 24]];
for (const [bottom, top] of cases) {
  console.log(`bottom=${String(bottom).padStart(3)} top=${String(top).padStart(3)} -> gap=${String(collapse(bottom, top)).padStart(3)}   (if summed ${bottom + top})`);
}

console.log("\n--- parent and first child (escapes outward if no border/padding) ---");
function parentChild({ parentMargin, childMargin, parentPadding, parentBorder }) {
  if (parentPadding > 0 || parentBorder > 0) {
    return { outside: parentMargin, inside: childMargin, collapses: false };
  }
  return { outside: collapse(parentMargin, childMargin), inside: 0, collapses: true };
}
for (const c of [
  { parentMargin: 16, childMargin: 32, parentPadding: 0, parentBorder: 0 },
  { parentMargin: 16, childMargin: 32, parentPadding: 1, parentBorder: 0 },
  { parentMargin: 16, childMargin: 32, parentPadding: 0, parentBorder: 1 },
]) {
  const r = parentChild(c);
  console.log(`parentPadding=${c.parentPadding} parentBorder=${c.parentBorder} -> outside=${r.outside} inside=${r.inside} collapses=${r.collapses}`);
}

console.log("\n--- an empty box collapses within itself ---");
console.log(`top=20 bottom=12, no content -> box's total vertical space = ${collapse(20, 12)}`);

console.log("\n--- three paragraphs in flow ---");
const paragraphs = [
  { name: "p1", marginTop: 0,  height: 24, marginBottom: 16 },
  { name: "p2", marginTop: 16, height: 48, marginBottom: 16 },
  { name: "p3", marginTop: 32, height: 24, marginBottom: 0  },
];
let y = 0;
for (let i = 0; i < paragraphs.length; i++) {
  const gap = i === 0 ? paragraphs[i].marginTop : collapse(paragraphs[i - 1].marginBottom, paragraphs[i].marginTop);
  y += gap;
  console.log(`${paragraphs[i].name}: gap=${String(gap).padStart(2)}  top=${String(y).padStart(3)}  bottom=${y + paragraphs[i].height}`);
  y += paragraphs[i].height;
}
console.log(`total height: ${y}`);
```

```
--- between two siblings ---
bottom= 32 top= 16 -> gap= 32   (if summed 48)
bottom= 16 top= 32 -> gap= 32   (if summed 48)
bottom= 24 top= 24 -> gap= 24   (if summed 48)
bottom= 32 top= -8 -> gap= 24   (if summed 24)
bottom= -8 top=-16 -> gap=-16   (if summed -24)
bottom=  0 top= 24 -> gap= 24   (if summed 24)

--- parent and first child (escapes outward if no border/padding) ---
parentPadding=0 parentBorder=0 -> outside=32 inside=0 collapses=true
parentPadding=1 parentBorder=0 -> outside=16 inside=32 collapses=false
parentPadding=0 parentBorder=1 -> outside=16 inside=32 collapses=false

--- an empty box collapses within itself ---
top=20 bottom=12, no content -> box's total vertical space = 20

--- three paragraphs in flow ---
p1: gap= 0  top=  0  bottom=24
p2: gap=16  top= 40  bottom=88
p3: gap=32  top=120  bottom=144
total height: 144
```

The first block shows the third rule most clearly: when two neighbors each give 24 units of
margin, the distance between them comes out not 48 but 24. If they were summed, giving every
paragraph the same margin would double the gap between paragraphs.

This is the reason for collapsing: in a text document, every block says "leave this much space
before and after me," and neighboring requests should not double each other. The collapsing
rule lets each block declare its own request independently while the result stays the larger
of the two.

#### Three Cases of Collapsing

Collapsing does not happen only between siblings.

**Between siblings.** A box's bottom margin and the next one's top margin collapse. The
output's first block shows this.

**Between a parent and its first/last child.** A box's top margin and its first child's top
margin collapse and **escape outward**. The output's second block gives this: the 16- and
32-unit margins merged and came out as 32 outside the box, leaving zero inside. The same thing
happens at the bottom with the last child.

This behavior is often surprising: when a section is given a background, the child's top
margin appears not inside the colored area but outside it. The same block's next two lines
show the fix too: once a single unit of padding or border is added to the parent, collapsing
stops and the margin stays inside.

**An empty box collapses within itself.** A box with no content, height, padding, or border
has its own top and bottom margins collapse with each other. In the output's third block, the
20- and 12-unit margins came down to a single 20 units.

#### When Collapsing Does Not Happen

Collapsing is not universal. It does not happen in these cases:

- **Never on the inline axis.** Two boxes sitting side by side always have their horizontal
  margins summed. Collapsing is defined only on the block axis.
- When there is **border or padding** between the boxes.
- When a box is **taken out of flow**: absolutely positioned or floated boxes take no part in
  collapsing.
- When a box is an **independent formatting context**: `display: flow-root`, boxes whose
  `overflow` value is other than `visible`, and children inside a flexible box or grid layout
  do not collapse.

The last item is the one used most in practice. Gaps given with `gap` in a layout model do not
collapse; the declared value applies as-is. This makes gaps predictable, and explains why the
next course's layout systems eliminate the collapsing problem.

#### Inline Placement

Inline boxes do not stack on the block axis; they are placed into **line boxes**. A line box
fills the containing block's inline axis and takes as many inline boxes as fit inside it.
Whatever does not fit moves to the next line box.

A line box's height is computed from the heights of the boxes inside it and their
`line-height` values. The detail of this computation is taken up in the typography lesson;
what matters here are its two consequences.

First, giving an inline box vertical margin does not grow the line box — this is the reason
for the behavior seen in the previous lesson's table.

Second, what determines the distance between line boxes is not margin but `line-height`.
Line spacing within a paragraph therefore cannot be adjusted with margin.

#### The Practical Result of Flow

The rules of normal flow ensure that a document is readable **with no layout declaration at
all**. Blocks stack, text breaks into lines, boxes fill their containers. The station document
written in the Web Fundamentals and HTML course was readable from start to finish even without
a single line of CSS.

The writing discipline that follows from this is: a layout problem is first attempted with the
rules of flow. Declarations that take boxes out of flow — floating and positioning — are
written when flow cannot give something needed, not because flow is unknown.

```css
/* station.css — step 11: flow-based spacing */
main > * + * { margin-block-start: 1rem; }

main > section { margin-block-start: 2.5rem; }

.masthead > :first-child { margin-block-start: 0; }
```

The first rule puts a gap between every pair of siblings in the main content and does not
touch the first element — the natural result of the sibling combinator. The second rule widens
the gap between sections and, thanks to collapsing, is **not added** to the first: 1rem and
2.5rem collapse, and the result is 2.5rem. The third rule zeroes out the masthead's first
child's margin instead of letting it escape outward.

#### Summary

- In normal flow, block boxes stack on the block axis, inline boxes are placed into line
  boxes; whatever does not fit moves to the next line box.
- Two neighboring margins on the block axis are not summed, they collapse: if both are
  positive, the larger applies; if both are negative, the smaller; if mixed, the sum applies.
- Collapsing happens in three cases: between siblings, between a parent and its first/last
  child, and within an empty box. In the second case, the margin escapes outward past the
  parent box.
- Collapsing never happens on the inline axis; it stops when border or padding sits in
  between, when the box is taken out of flow, or when it is an independent formatting context.
- The distance between line boxes is set by `line-height`, not margin; vertical margin given
  to an inline box does not change the line height.

#### Next Step

Flow lays boxes out in one direction: blocks stacked, inline boxes side by side. Wanting an
image to sit next to text and have the text flow around it fits neither pattern. The next
lesson takes up floating, which was defined for exactly this need, and the height problem it
brings with it.

### Floating Elements

Source: https://academia.sh/en/courses/css-fundamentals/floating-elements

Floating shortening line boxes so text flows around a box; the containing box's height problem, and the clearing and formatting-root solutions.

Normal flow laid boxes out in two patterns: blocks stacked, inline boxes side by side. In the
station page's location section there is a request that fits neither pattern — the location
image should sit to the left of the text, and the caption text should **flow around** the
image.

The mechanism defined for this behavior is called **float**. Floating pushes a box to one edge
of the inline axis and has the line boxes in flow pass around it. This lesson takes up what
floating does, what it does not do, and the height problem it brings with it.

#### Floating Shortens Line Boxes

A floated box is taken out of normal flow but **keeps affecting** flow. Its effect is in one
place only: line boxes that share its vertical range get shortened.

```js
// float.mjs — computes how a floated box shortens line boxes
function lines({ containerWidth, floatWidth, floatHeight, lineHeight, lineCount }) {
  const result = [];
  for (let i = 0; i < lineCount; i++) {
    const top = i * lineHeight;
    const insideFloat = top < floatHeight;
    const available = insideFloat ? containerWidth - floatWidth : containerWidth;
    result.push({ line: i + 1, top, available, start: insideFloat ? floatWidth : 0 });
  }
  return result;
}

for (const s of lines({ containerWidth: 640, floatWidth: 200, floatHeight: 120, lineHeight: 24, lineCount: 7 })) {
  console.log(`line ${s.line}: top=${String(s.top).padStart(3)}  x=${String(s.start).padStart(3)}..640  available width=${s.available}`);
}

console.log("\n--- the container's height ---");
function containerHeight({ flowContentHeight, floatHeight, isFormattingContext }) {
  return isFormattingContext ? Math.max(flowContentHeight, floatHeight) : flowContentHeight;
}
for (const isFormattingContext of [false, true]) {
  const h = containerHeight({ flowContentHeight: 48, floatHeight: 120, isFormattingContext });
  console.log(`formatting context=${String(isFormattingContext).padEnd(5)} -> container height=${h}`);
}
```

```
line 1: top=  0  x=200..640  available width=440
line 2: top= 24  x=200..640  available width=440
line 3: top= 48  x=200..640  available width=440
line 4: top= 72  x=200..640  available width=440
line 5: top= 96  x=200..640  available width=440
line 6: top=120  x=  0..640  available width=640
line 7: top=144  x=  0..640  available width=640

--- the container's height ---
formatting context=false -> container height=48
formatting context=true  -> container height=120
```

The first five line boxes are 440 units wide instead of 640, and start from 200: that is where
the floated image sits over that range. The sixth line, since it passes the image's bottom
edge, returns to full width. This is what it means for text to flow around the image — not
some mysterious wrap behavior, but line boxes being shortened.

Two conclusions follow from this.

**Floating affects only line boxes, not block boxes.** A block box next to a floated box does
not narrow; it keeps filling its container and passes underneath the floated box. Only the
text lines inside that block box get shortened. This is why using float to build column
layout is fragile.

**Floating has a direction.** `float: inline-start` pushes the box to the start of the inline
axis, `float: inline-end` to the end. Their physical counterparts are `left` and `right`;
logical values adapt to writing direction.

#### The Container's Height Collapses

The output's second block gives the actual problem with floating. Since a floated box is
taken out of flow, it **takes no part** in its container's height. If the container has 48
units of flow content and a 120-unit floated image, the container's height comes out 48; the
image spills outside the box.

The visible result is that a background or border given to the container does not cover the
image. This is not a bug, it is a direct consequence of the definition: a box out of flow
contributes nothing to flow's height.

There are two solutions.

#### Solution 1: Clearing

The `clear` property tells a box to drop **below** any floated boxes that precede it.

```css
.clear { clear: both; }
```

`clear: inline-start` drops below only start-floated boxes, `inline-end` below only
end-floated ones, `both` below either.

Clearing is used to fix the container's height indirectly: if a clearing box is placed as the
container's last child, that box drops below the float and the container stretches to contain
it. This can be done without cluttering the document with a pseudo-element:

```css
.clearfix::after {
  content: "";
  display: block;
  clear: both;
}
```

The solution works but is indirect: a content element is generated to solve the height
problem.

#### Solution 2: Independent Formatting Root

The direct solution is to make the container a **block formatting context**. Such a box
includes the floated boxes inside it in its own height; the output's last line, where the
height is 120 in the `formatting context=true` case, shows this.

The explicit way to make a box a formatting root:

```css
.location-section { display: flow-root; }
```

There are also indirect ways that give the same result: an `overflow` value other than
`visible`, the box sitting inside a flexible box or grid layout, being absolutely positioned.
These come with side effects — writing `overflow: hidden` also clips overflowing content.
`flow-root`, on the other hand, does only the job wanted.

A formatting root has three effects, and all three have already come up in earlier lessons:

- Floated boxes inside it are included in its height.
- Floated boxes outside it do not spill into it.
- Margin collapsing stops at its boundary.

All three come from the same definition: the box completes block placement within itself.

#### The Floated Box's Own Size

A floated box produces a block box but does not fill its container. When its width is not
declared, it **shrinks to fit its content**: the box pulls in to the narrowest size it can fit
into. This is the opposite of a block box's behavior in flow.

The result is that declaring a width for a floated box is often necessary. A box carrying text
that shrinks grows the line count and stretches unpredictably; a box carrying an image settles
at the image's natural size.

A second behavior is that floated boxes **push against each other**. When more than one box is
floated to the same edge, they line up side by side; whatever does not fit on the line drops to
a row below. Where it drops is determined by the bottom edges of the preceding floated boxes,
and if the boxes are different heights, the arrangement becomes irregular. This is another
source of the fragility of building grid-like layout with float.

#### The Shape of the Float Boundary

The boundary that shortens line boxes is, by default, the floated box's margin rectangle — a
rectangle no matter its shape. This boundary can be changed with the `shape-outside` property:

```css
.location-image {
  float: inline-start;
  shape-outside: circle(50%);
  shape-margin: 12px;
}
```

`shape-outside` only changes the boundary of flowing text; it does not clip the box itself. If
the image is also wanted to look circular, `clip-path` is written separately. The two
properties do different jobs: one decides where neighbors pass, the other decides what part of
the box is painted.

#### What Floating Is For

Float has long been used as a tool for building column-based page layouts. This use does not
fit float's definition, and the output's first block shows why: floating does not place a box
into a column, it only shortens line boxes. Equalizing column heights, aligning boxes, and
spacing them remain problems that must be solved separately.

The one use that matches float's own definition is **flowing text around a box**. The layout
systems taken up in the next course take over the job of building columns, and do it with
fewer declarations and more predictably than float. Float still has a place in those courses
too, because no layout model flows text around a box.

```css
/* station.css — step 12: location image and text flowing around it */
.location-section { display: flow-root; }

.location-image {
  float: inline-start;
  inline-size: 200px;
  margin-inline-end: 16px;
  margin-block-end: 8px;
}

.location-section .source-note { clear: both; }
```

The `display: flow-root` declaration makes the section contain the image. The margin given
toward the end of the image keeps text from pressing against it — a floated box's margin edge
is the boundary that shortens line boxes, not its border edge.

The last rule drops the source note below the image; the note should be read not to the right
of the text but at the end of the section.

#### Summary

- A floated box leaves normal flow but shortens line boxes that share its vertical range; this
  is what it means for text to flow around it.
- Floating affects line boxes, not block boxes; this is why it is fragile as a tool for
  building column layout.
- A floated box takes no part in its container's height; the container collapses and its
  background or border does not cover the floated content.
- `clear` drops a box below floated ones; used as the container's last child, it fixes the
  height indirectly.
- `display: flow-root` makes a box an independent formatting root: it includes floats in its
  height, keeps outside floats from spilling in, and stops margin collapsing at its boundary.

#### Next Step

Floating took a box out of flow but did not let its destination be chosen; the box was only
pushed to one edge. Placing an element at a wanted point in the document — a badge in a card's
corner, a heading held fixed while scrolling — needs a different mechanism. The next lesson
defines positioning's five values and what box each is measured against.

### Positioning

Source: https://academia.sh/en/courses/css-fundamentals/positioning

Where static, relative, absolute, fixed, and sticky positioning put a box; the containing block concept and what the offset properties are measured against.

Floating took a box out of flow but did not let its destination be chosen; the box was only
pushed to one edge. The station page has places where this is not enough: a status badge
sitting in a card's corner, a measurement table's column headers staying visible while
scrolling.

This lesson defines the `position` property's five values. Each value has two questions: does
the box stay in flow, and what box are its offset values measured **against**?

#### Five Values, Two Questions

```js
// position.mjs — computes where each of the five positioning values puts a box
const CONTAINER = { name: "main",     x: 40, y: 100, width: 640,  height: 800 };
const CARD       = { name: "card",     x: 80, y: 200, width: 400,  height: 240 }; // its place in flow
const VIEWPORT    = { name: "viewport", x: 0,  y: 0,   width: 1024, height: 768 };

function place({ position, flowPosition, containingBlock, offset, scrollY = 0 }) {
  const { top = null, left = null, width = 120, height = 40 } = offset;
  switch (position) {
    case "static":
      return { x: flowPosition.x, y: flowPosition.y, width: flowPosition.width, height: flowPosition.height, inFlow: true };
    case "relative":
      return { x: flowPosition.x + (left ?? 0), y: flowPosition.y + (top ?? 0),
               width: flowPosition.width, height: flowPosition.height, inFlow: true };
    case "absolute":
      return { x: containingBlock.x + (left ?? 0), y: containingBlock.y + (top ?? 0), width, height, inFlow: false };
    case "fixed":
      return { x: VIEWPORT.x + (left ?? 0), y: VIEWPORT.y + (top ?? 0), width, height, inFlow: false };
    case "sticky": {
      const threshold = top ?? 0;
      const flowedY = flowPosition.y - scrollY;
      return { x: flowPosition.x, y: Math.max(threshold, flowedY),
               width: flowPosition.width, height: flowPosition.height, inFlow: true, stuck: flowedY < threshold };
    }
  }
}

const badge = { x: 0, y: 0, width: 120, height: 40 };
console.log("containing block: main  x=40 y=100 640x800");
console.log("flow position    : badge x=0  y=0   120x40\n");

for (const position of ["static", "relative", "absolute", "fixed"]) {
  const r = place({ position, flowPosition: badge, containingBlock: CARD, offset: { top: 16, left: 12 } });
  console.log(`${position.padEnd(9)} -> x=${String(r.x).padStart(4)} y=${String(r.y).padStart(4)}  ${r.width}x${r.height}  inFlow=${r.inFlow}`);
}
console.log("  (for absolute, containing block = card: x=80 y=200)");

console.log("\n--- sticky: heading, threshold top=0, as scroll advances ---");
const heading = { x: 40, y: 300, width: 640, height: 48 };
for (const scrollY of [0, 200, 300, 400, 520]) {
  const r = place({ position: "sticky", flowPosition: heading, containingBlock: CONTAINER, offset: { top: 0 }, scrollY });
  console.log(`scroll=${String(scrollY).padStart(3)} -> y on screen=${String(r.y).padStart(3)}  stuck=${r.stuck}`);
}
```

```
containing block: main  x=40 y=100 640x800
flow position    : badge x=0  y=0   120x40

static    -> x=   0 y=   0  120x40  inFlow=true
relative  -> x=  12 y=  16  120x40  inFlow=true
absolute  -> x=  92 y= 216  120x40  inFlow=false
fixed     -> x=  12 y=  16  120x40  inFlow=false
  (for absolute, containing block = card: x=80 y=200)

--- sticky: heading, threshold top=0, as scroll advances ---
scroll=  0 -> y on screen=300  stuck=false
scroll=200 -> y on screen=100  stuck=false
scroll=300 -> y on screen=  0  stuck=false
scroll=400 -> y on screen=  0  stuck=true
scroll=520 -> y on screen=  0  stuck=true
```

#### Reading the Values

**`static`** — the initial value. The box stays in its normal-flow place, and the offset
properties (`top`, `right`, `bottom`, `left`; their logical counterparts `inset-block-start`
and its relatives) are **ignored**. In the output, the position did not change even though an
offset was given.

**`relative`** — the box is shifted from its flow position by the offset but **keeps its flow
place**. In the output, the box shifted 12 and 16 units; its neighbors are unaffected by this
shift, and the box's old place stays empty. This is both the power and the limit of relative
positioning: it makes a visual correction, it does not rearrange layout.

**`absolute`** — the box is **taken out** of flow, and offsets are measured against the
containing block. In the output, the card was at $x=80$, $y=200$; the badge landed at point
$92, 216$. Since the box left flow, its width no longer fills its container either — it
shrinks to fit its content.

**`fixed`** — the box is taken out of flow, and offsets are measured against the **viewport**; if any
ancestor carries `transform`, `filter`, or `perspective` with a value other than `none`, the containing
block is that ancestor rather than the viewport. In the output, wherever the card was, the box landed at
point $12, 16$. When the page is scrolled, the box stays fixed on screen.

**`sticky`** — it sits between the two, and is shown in the output's second block.

#### How the Containing Block Is Determined

The most commonly misunderstood part of absolute positioning is what the "containing block"
is. The rule is this: the box is placed relative to its **nearest positioned ancestor**.
Positioned means an ancestor whose `position` value is something **other than** `static`.

If no ancestor is positioned, the containing block is the document's first box; the box is
placed relative to the entire page — this is usually the unwanted result.

This gives rise to a standard pairing:

```css
.card         { position: relative; }
.card .badge  { position: absolute; inset-block-start: 8px; inset-inline-end: 8px; }
```

The card is given `position: relative` but no offset is written. The goal is not to shift the
card, it is to turn it into a **containing block**. The badge now sits at the card's corner.

The shorthand for offsets is `inset`: `inset: 0` zeroes all four offsets and stretches an
absolutely positioned box to exactly fill its containing block.

When two opposite offsets are given together — `inset-inline-start: 0` and
`inset-inline-end: 0` — the box's width is forced to the distance between them. This is a way
of giving a width without writing `width`.

#### Sticky Positioning Is a Threshold Behavior

`position: sticky` leaves the box in flow. The box stays in its normal place — until, during
scrolling, the declared threshold is reached. From that point on, the box **sticks** at the
threshold.

The output's second block shows this step by step. The heading was at $y = 300$ in flow, and
the threshold was given as `inset-block-start: 0`. Once scrolling advanced 200 units, the
heading moved to point 100 on screen — still moving together with flow. Once scrolling reached
300, the heading touched the threshold; at 400 and 520 it stayed at point 0 on screen, meaning
it had stuck.

Two conditions are required, and both are often overlooked.

**A threshold must be declared.** `position: sticky` alone does nothing; at least one offset
property must be written.

**Sticking is confined to the containing box's bounds.** The box does not leave its container;
once the container is scrolled past, the box goes with it. This is what lets a section heading
stick only while its own section is on screen — that is usually the wanted behavior.

There is also an obstacle: a sticky box sticks relative to a scrollable ancestor, if it has
one. Having `overflow: hidden` on one of the ancestors in between silently stops sticking. This
is the first place to check on a box that fails to stick.

#### The Cost of Leaving Flow

Absolute and fixed positioning take the box out of flow. The cost is that the box **takes no
part in its container's height** — the same problem seen with floating.

The result is that absolutely positioned content can overlap the content beneath it. This is
why absolute positioning is used not for something whose size changes together with content,
but for something whose size is fixed or whose overlapping is not a problem: a badge, a close
button, a decorative marker.

Building a page's main layout with positioning means rewriting by hand every automatic
behavior flow provides. The general rule is: **layout is built with flow; positioning is for
individual exceptions flow cannot give.**

```css
/* station.css — step 13: positioning */
.card { position: relative; }

.card .status-badge {
  position: absolute;
  inset-block-start: 8px;
  inset-inline-end: 8px;
}

.measurement-table thead th {
  position: sticky;
  inset-block-start: 0;
  background-color: #f5f7f8;
}

.skip-link {
  position: absolute;
  inset-block-start: -100px;
}

.skip-link:focus {
  position: fixed;
  inset-block-start: 8px;
  inset-inline-start: 8px;
}
```

The background color on the table header is required: the sticky header is drawn over the
rows passing beneath it, and if it stays transparent, two lines of text get read on top of each
other.

The last two rules are an accessibility pattern. The skip link to content sits outside the
viewport; when keyboard focus reaches it, it switches to a fixed position and becomes visible.
The rule could not be written with `display: none` — an element producing no box cannot be
focused.

#### Summary

- `static` leaves the box in its flow place and ignores offsets; `relative` shifts the box by
  the offset but keeps its flow place.
- `absolute` and `fixed` take the box out of flow; the first measures against the nearest
  positioned ancestor, the second against the viewport — though for `fixed` too, an ancestor
carrying `transform`, `filter`, or `perspective` becomes the containing block.
- For absolute positioning, the containing block is the nearest ancestor whose `position`
  value is other than `static`; writing `position: relative` with no offset is exactly this
  job.
- `sticky` leaves the box in flow and holds it in place once the declared threshold is
  reached; at least one offset must be declared, and sticking is bounded by the containing
  box.
- A box taken out of flow contributes nothing to its container's height; this is why
  positioning is used not for the main layout but for exceptions flow cannot give.

#### Next Step

Positioning can make boxes overlap: a badge over a card, a stuck header over rows. So when two
boxes overlap at the same point, which one is drawn on top? Writing `z-index` does not always
work — sometimes a higher value ends up underneath. The next lesson defines paint order and
the stacking context that is the reason for this behavior.

### Stacking Context

Source: https://academia.sh/en/courses/css-fundamentals/stacking-context

The paint order of boxes; z-index values being compared only among siblings, and the declarations that establish a stacking context.

Positioning made boxes overlap: a badge sitting over a card, a stuck header over rows. One
question was left open — if two boxes overlap at the same point, which one is drawn on top?

The answer does not end with writing `z-index`. This property has a commonly encountered
behavior: sometimes a higher value ends up underneath. The reason is not a bug, it is a
structure called the **stacking context**. This lesson defines that structure.

#### Default Paint Order

There is an order even when `z-index` is never written. Boxes in the same stacking context
are painted roughly in this order — what is painted first stays at the bottom:

1. The background and border of the box that establishes the context.
2. Positioned boxes with a negative `z-index` value.
3. The backgrounds of block boxes in flow.
4. Floated boxes.
5. Inline content and text.
6. Positioned boxes whose `z-index` is `auto` or `0`.
7. Positioned boxes with a positive `z-index` value.

Two observations follow from this list. First, **a positioned box is painted on top of boxes
in flow**; there is no need to write `z-index`. `position: absolute` is enough for a badge to
appear over a card.

Second, among boxes at the same level, **document order** is what decides: whatever comes
later is painted on top.

#### z-index Is Compared Only Among Siblings

`z-index` does not give a box a global elevation. The value is compared only against the
box's siblings **within its own stacking context**.

When a box establishes a stacking context, every box inside it is enclosed within that
context. What gets compared with boxes outside is no longer the children's values, it is the
value of **the box that establishes the context**.

```js
// stacking.mjs — resolves stacking contexts as a tree and produces paint order
const tree = {
  name: "root", zIndex: "auto", formsContext: true, children: [
    { name: "A", zIndex: 1, formsContext: true, children: [
      { name: "A1", zIndex: 999, formsContext: true, children: [] },
      { name: "A2", zIndex: 5,   formsContext: true, children: [] },
    ]},
    { name: "B", zIndex: 2, formsContext: true, children: [
      { name: "B1", zIndex: 1, formsContext: true, children: [] },
    ]},
    { name: "C", zIndex: "auto", formsContext: false, children: [] },
  ],
};

// children inside a context are ordered by z-index, ties broken by document order
function paint(node, depth = 0, prefix = "") {
  const path = prefix ? `${prefix} > ${node.name}` : node.name;
  const z = node.zIndex === "auto" ? 0 : node.zIndex;
  console.log(`${"  ".repeat(depth)}${node.name.padEnd(4)} z=${String(node.zIndex).padEnd(4)} path=${path}`);
  const sorted = [...node.children]
    .map((c, i) => ({ c, i }))
    .sort((x, y) => {
      const zx = x.c.zIndex === "auto" ? 0 : x.c.zIndex;
      const zy = y.c.zIndex === "auto" ? 0 : y.c.zIndex;
      return zx - zy || x.i - y.i;
    });
  for (const { c } of sorted) paint(c, depth + 1, path);
  return z;
}

console.log("paint order (bottom to top, nested):");
paint(tree);

console.log("\n--- comparing A1 and B ---");
console.log("A1.z = 999, B.z = 2");
console.log("A1 is not in the root context; it is inside A's (z=1) context.");
console.log("Values compared in the root context: A=1, B=2  -> B on top.");
console.log("Result: A1 (999) stays BELOW all of B's subtree.");
```

```
paint order (bottom to top, nested):
root z=auto path=root
  C    z=auto path=root > C
  A    z=1    path=root > A
    A2   z=5    path=root > A > A2
    A1   z=999  path=root > A > A1
  B    z=2    path=root > B
    B1   z=1    path=root > B > B1

--- comparing A1 and B ---
A1.z = 999, B.z = 2
A1 is not in the root context; it is inside A's (z=1) context.
Values compared in the root context: A=1, B=2  -> B on top.
Result: A1 (999) stays BELOW all of B's subtree.
```

The last block is the exact statement of the problem this lesson solves. Box `A1`'s `z-index`
value is 999 — the highest value on the page. Despite this, it stays **below** box `B`, whose
value is 2.

The reason is that box `A1` is not in the root context. Two values are compared in the root
context: 1 for `A` and 2 for `B`. `B` wins and is painted, along with its whole subtree, on
top of `A`. Box `A1`'s value of 999 is only ever compared against its sibling `A2`.

The actionable conclusion is this: if a box is not rising as high as expected, the fix is not
to raise the value. Raising the value to a million does not help either. The fix is to
**find which box in the ancestor chain establishes a context** and make the comparison at that
level.

#### What Establishes a Stacking Context

A box establishes a stacking context when one of these conditions holds:

- The document's root element (always establishes a context).
- Boxes whose `position` value is other than `static` and whose `z-index` value is other than
  `auto`.
- Boxes with `position: fixed` or `position: sticky` (even without `z-index` written).
- Boxes whose `opacity` value is less than 1.
- Boxes on which a property like `transform`, `filter`, `perspective`, `clip-path`, or `mask`
  takes a value other than `none`.
- Boxes declaring `isolation: isolate`.
- Children of a flexible box or grid layout whose `z-index` value is other than `auto`.

The middle of this list stands out. Writing `opacity: 0.99` — a change that will not be
visually noticeable — establishes a stacking context and isolates every `z-index` calculation
inside that box from the outside. The same holds for a `transform` declaration written to give
a box motion.

That a declaration with no apparent relation to stacking changes stacking looks surprising, but
there is a reason: all of these properties require the box and its content to be processed
**as a whole**. Transparency is computed by blending everything inside the box together rather
than separately, and this forces them to be treated as a single layer.

#### isolation: isolate

The `isolation: isolate` declaration, the item before the last on the list, is the **side-effect-free**
way to establish a stacking context. It is written when a box's internal `z-index` values
should not leak outward, and it changes nothing else.

This is useful on pages where multiple components coexist: if each component establishes its
own context, internal `z-index` values can stay small and local. One component's `10` does not
compete with another component's `10`.

#### A z-index Scale

This lesson's practical result is a writing discipline.

- `z-index` values are kept **small and named**. A page has a handful of layers — base, stuck
  header, dropdown layer, top layer — and each is given sparse values like 1, 10, 100. The
  gaps in between leave room for adding a layer later.
- Before reaching for `z-index`, it is checked whether `position` alone is enough for a box to
  stay on top; a positioned box is already above boxes in flow.
- Components are isolated with `isolation: isolate`, so their internal values do not leak
  outward.

```css
/* station.css — step 14: layer scale */
.card { position: relative; isolation: isolate; }

.card .status-badge {
  position: absolute;
  z-index: 1;
  inset-block-start: 8px;
  inset-inline-end: 8px;
}

.measurement-table thead th {
  position: sticky;
  z-index: 10;
  inset-block-start: 0;
  background-color: #f5f7f8;
}

.skip-link:focus {
  position: fixed;
  z-index: 100;
  inset-block-start: 8px;
  inset-inline-start: 8px;
}
```

The `isolation: isolate` written on the card ensures the badge's `z-index: 1` value never
competes with anything outside the card. The stuck header stays at layer 10, the focused skip
link at layer 100; the order among the three is defined for the whole page and is readable.

#### Summary

- Paint order is defined even without writing `z-index`; positioned boxes are painted above
  boxes in flow, and boxes at the same level are painted in document order.
- `z-index` does not give a global elevation; it is compared only against the box's siblings
  within its own stacking context.
- When a box establishes a context, every value inside it is enclosed within that context; what
  gets compared with the outside is the value of the box that establishes the context.
- Declarations that look unrelated to stacking, like `opacity`, `transform`, and `filter`, also
  establish a context, because they require the box's content to be processed as a whole.
- `isolation: isolate` is the side-effect-free way to establish a context; it is used to keep a
  component's internal layer values local.

#### Next Step

Throughout this topic, boxes were sized, arranged, and overlapped. One situation has not been
addressed yet: what happens when content does not fit its box? A long station name does not
fit a narrow cell, a wide measurement table does not fit a narrow screen. The next lesson takes
up what overflow is and what each of the clipping, scrolling, and hiding options costs.

### Overflow Management

Source: https://academia.sh/en/courses/css-fundamentals/overflow-management

Handling the case where content does not fit its box; comparing overflow values, the separate mechanisms for text overflow, and clipping's accessibility cost.

Throughout this topic, boxes were sized, arranged, and overlapped. One situation has not been
addressed: what happens when content does not **fit** its box?

The situation is unavoidable. A long station name does not fit a narrow cell, a nine-column
measurement table does not fit a narrow screen, a long caption does not fit a card given a
fixed height. This lesson takes up what overflow is, what each of the options costs, and why
text overflow is a separate matter.

#### Overflow's Default Behavior Is Visibility

When a box's content does not fit the box, the default behavior is for the content to
**spill out and stay visible**. It is not clipped, not hidden, and does not grow the box.

This choice is deliberate: content that disappears is worse than content that breaks the
layout. Overflowing text stays visible and readable; the problem is seen and can be fixed.

```js
// overflow.mjs — computes whether content fits the box and the result of each overflow value
function overflowState({ boxWidth, boxHeight, contentWidth, contentHeight, overflow }) {
  const horizontal = Math.max(0, contentWidth - boxWidth);
  const vertical = Math.max(0, contentHeight - boxHeight);
  const result = {
    "visible": { visible: true,  scrollable: false, clips: false },
    "hidden":  { visible: false, scrollable: false, clips: true  },
    "clip":    { visible: false, scrollable: false, clips: true  },
    "scroll":  { visible: false, scrollable: true,  clips: true  },
    "auto":    { visible: false, scrollable: horizontal > 0 || vertical > 0, clips: true },
  }[overflow];
  return { horizontal, vertical, ...result };
}

const box = { boxWidth: 320, boxHeight: 120 };
const content = { contentWidth: 480, contentHeight: 200 };
console.log(`box ${box.boxWidth}x${box.boxHeight}, content ${content.contentWidth}x${content.contentHeight}`);
for (const value of ["visible", "hidden", "clip", "scroll", "auto"]) {
  const r = overflowState({ ...box, ...content, overflow: value });
  console.log(`${value.padEnd(8)} overflow=${r.horizontal}x${r.vertical}  visible from outside=${String(r.visible).padEnd(5)} scrollable=${String(r.scrollable).padEnd(5)} clips=${r.clips}`);
}

console.log("\n--- content that fits ---");
for (const value of ["scroll", "auto"]) {
  const r = overflowState({ ...box, contentWidth: 300, contentHeight: 100, overflow: value });
  console.log(`${value.padEnd(8)} overflow=${r.horizontal}x${r.vertical}  scrollable=${r.scrollable}`);
}

console.log("\n--- a long word fitting a line ---");
const word = "North-Slope-Automatic-Measurement-Station";
const CHAR_WIDTH = 8;   // an example font's character width
const boxWidth = 160;
const fits = Math.floor(boxWidth / CHAR_WIDTH);
console.log(`word ${word.length} characters x ${CHAR_WIDTH}px = ${word.length * CHAR_WIDTH}px`);
console.log(`box ${boxWidth}px -> ${fits} characters fit a line, overflow ${word.length * CHAR_WIDTH - boxWidth}px`);
const pieces = word.match(new RegExp(`.{1,${fits}}`, "g"));
console.log("split with overflow-wrap: anywhere:");
for (const p of pieces) console.log(`  |${p.padEnd(fits)}|`);
```

```
box 320x120, content 480x200
visible  overflow=160x80  visible from outside=true  scrollable=false clips=false
hidden   overflow=160x80  visible from outside=false scrollable=false clips=true
clip     overflow=160x80  visible from outside=false scrollable=false clips=true
scroll   overflow=160x80  visible from outside=false scrollable=true  clips=true
auto     overflow=160x80  visible from outside=false scrollable=true  clips=true

--- content that fits ---
scroll   overflow=0x0  scrollable=true
auto     overflow=0x0  scrollable=false

--- a long word fitting a line ---
word 41 characters x 8px = 328px
box 160px -> 20 characters fit a line, overflow 168px
split with overflow-wrap: anywhere:
  |North-Slope-Automati|
  |c-Measurement-Statio|
  |n                   |
```

#### Comparing the Five Values

`visible` — the initial value. Content overflows and stays visible. The box itself does not
grow; its neighbors have no knowledge of the overflow and can be painted over it.

`hidden` — the overflowing part is clipped and becomes **unreachable**. No scrollbar is
shown, but the box is still a box that can be scrolled programmatically; a focused element can
be brought into the visible area.

`clip` — clips like `hidden` but does not make the box scrollable. The clipping is final.

`scroll` — the overflowing part is clipped and a scrollbar is **always** shown.

`auto` — clips and shows a scrollbar **only when needed**.

The output's second block gives the difference between these last two values: when content
fits the box, `scroll` still produces a scrollable box, `auto` does not. In some environments a
scrollbar takes up space; writing `scroll` in this case reserves a strip and narrows the box
even when content fits. If scrolling should appear only when needed, `auto` is written.

#### The Cost of Clipping

Writing `overflow: hidden` is a frequently reached-for fix, and it is used for two different
purposes. The two need to be told apart.

The first use is **deliberate clipping**: fitting an image into a box, keeping a decorative
shape from spilling out. What is clipped here carries no information.

The second use is **making the overflow problem invisible**: content does not fit, and hiding
it pretends the problem has gone away. This does not solve the problem; it hides it from the
user. Clipped text can still be read by a screen reader but is visually unreachable; a user
navigating by keyboard can focus it but cannot see it.

The distinction is made with this question: **is it acceptable for the clipped thing to be
lost?** If the answer is no, `hidden` is not written — `auto` is written instead, or the box's
size is made flexible.

There is one more side effect, and it has come up twice in earlier lessons: an `overflow`
value other than `visible` and `clip` makes the box an **independent formatting root**. Floated children
are included in the height, margin collapsing stops at the box's boundary. This is why writing
`overflow: hidden` sometimes produces a layout change that looks unrelated.

#### Two Axes Separately

`overflow` is a shorthand; it sets the `overflow-x` and `overflow-y` longhands. Their logical
counterparts are `overflow-inline` and `overflow-block`.

The two axes can be given different values, but there is a constraint: if one axis is `visible` while
the other takes `hidden`, `scroll`, or `auto`, the `visible` axis is treated like `auto`; `clip` is
exempt from this rule. The two axes are not independent; a box that clips in one direction cannot leave
overflowing content visible in the other.

Horizontal scrolling needs special handling. The way to show a wide measurement table on a
narrow screen is to place the table inside a horizontally scrollable box. Such a box must also
be reachable by keyboard; giving a scrollable region `tabindex="0"` and naming it with an
accessible name is the counterpart of this.

#### Text Overflow Is a Separate Mechanism

The output's third block points to a different problem. A forty-one-character word does not
fit a 160-unit box and overflows by 168 units. This is not a problem to be solved with
`overflow` — the word needs to be **broken**.

Line-breaking rules are managed by separate properties:

`overflow-wrap: anywhere` — breaks a word that does not fit at any point. The output's last
lines showed the break made under this rule. The `break-word` value does similar work but is
not included in the box's minimum content size calculation.

`word-break: break-all` — breaks words whether or not they fit; it pays no attention to word
integrity.

`hyphens: auto` — hyphenates according to the document's language and puts a hyphen at the
break point. It requires the `lang` attribute to be declared; this is one more counterpart of
the `lang="en"` declaration written on the root element in the Web Fundamentals and HTML
course.

`white-space: nowrap` — does the opposite: prevents line breaking altogether. This does not
prevent overflow, it guarantees it.

`text-overflow: ellipsis` — puts an ellipsis at the end of overflowing text. It does not work
alone; the box must be clipping and must be a single line:

```css
.station-name {
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}
```

The three declarations are written together. It must not be forgotten that the clipped text is
lost: the full name should stay reachable in a `title` attribute or somewhere else.

```css
/* station.css — step 15: overflow */
.table-wrapper {
  overflow-x: auto;
  overscroll-behavior-x: contain;
}

.measurement-table { min-width: 40rem; }

.card .description {
  overflow-wrap: anywhere;
  hyphens: auto;
}

.location-image img { overflow: clip; }
```

`.table-wrapper` makes the table horizontally scrollable on a narrow screen; the `min-width`
declaration keeps the table from being squeezed into unreadability. Together, the two choose
to scroll the table rather than shrink it.

`overscroll-behavior-x: contain` keeps the page from shifting once the box's own scrolling
ends — it keeps the scroll chain stopping at that box.

#### Summary

- Overflow's default behavior is visibility: content spills outside the box and stays
  readable. This rests on the assumption that lost content is worse than a broken layout.
- `hidden` and `clip` clip; `scroll` always shows a scrollbar, `auto` shows one only when
  needed; when content fits, `scroll` still produces a scrollable box.
- An `overflow` value other than `visible` and `clip` makes the box an independent formatting root; it
  contains floated children and stops margin collapsing at its boundary.
- The two axes are not independent: while one axis takes `hidden`, `scroll`, or `auto`, the
  other cannot stay `visible` and is treated like `auto`; `clip` is exempt from this rule.
- A word that does not fit is solved not with `overflow` but with line-breaking properties;
  `text-overflow: ellipsis` does not work alone, it is written together with clipping and
  single-line declarations.

#### Next Step

This topic built the size, arrangement, and boundaries of boxes. The page that results is
structured but colorless: every decision so far concerned geometry. The next topic takes up
visual properties, and the first question is color itself — how a color is written, in which
space it is defined, and how the contrast between two colors turns into a number.

## Visual Properties

Color representations and contrast calculation, typography scales and font-loading behavior, backgrounds, transitions, shadows, and calculating with units.

### Color Representations

Source: https://academia.sh/en/courses/css-fundamentals/color-representations

Named, hexadecimal, and functional color notations; the conversion between RGB and HSL, and the consequence of color space choice for generating a scale.

The previous topic established the boxes' measure, arrangement, and
boundaries. The resulting page is structured but colorless: every
decision concerned geometry. This topic takes up visual properties,
and the first question is color itself.

A color has four separate notations in CSS, and moving between them
is done by calculation. Choosing the notation is not a style
preference: some notations make **changing** a color more workable
than others, and a design's color scale therefore gets built in a
particular representation.

#### Four Notations

**A named color** is a name from a fixed vocabulary: `red`, `darkslategray`, `transparent`. The vocabulary
has grown a few times over CSS's history (`orange` in CSS2, `rebeccapurple` in Color 4) but is treated as
closed today; the names do not form a color system, they are a historical list. It does not get used in
design.

**Hexadecimal notation** writes the red, green, and blue components
in base sixteen: `#143a52`. The hexadecimal base defined in the How
Computers Work curriculum finds its counterpart here — each component
is one byte, two hexadecimal digits.

**Functional notation** writes the components explicitly: `rgb(20 58
82)`, `hsl(203 61% 20%)`, `oklch(30% 0.06 240)`. The function name
says which **color space** the numbers get read in.

**Keywords** take their color from context: `currentcolor` gives the
element's `color` value, `transparent` gives fully transparent black.

#### The Conversion Between RGB and HSL

RGB and HSL define the same set of colors; their difference is the
coordinate system. RGB uses three light components, HSL uses the hue,
saturation, and lightness axes. The conversion is two-directional and
lossless.

```js
// color.mjs — converts between hex, rgb, and hsl representations
function hexToRgb(hex) {
  let h = hex.replace("#", "");
  if (h.length === 3) h = [...h].map((c) => c + c).join("");
  if (h.length === 4) h = [...h].map((c) => c + c).join("");
  const r = parseInt(h.slice(0, 2), 16);
  const g = parseInt(h.slice(2, 4), 16);
  const b = parseInt(h.slice(4, 6), 16);
  const a = h.length === 8 ? parseInt(h.slice(6, 8), 16) / 255 : 1;
  return { r, g, b, a };
}

function rgbToHsl({ r, g, b }) {
  const rn = r / 255, gn = g / 255, bn = b / 255;
  const max = Math.max(rn, gn, bn), min = Math.min(rn, gn, bn);
  const diff = max - min;
  const l = (max + min) / 2;
  let h = 0, s = 0;
  if (diff !== 0) {
    s = diff / (1 - Math.abs(2 * l - 1));
    if (max === rn) h = 60 * (((gn - bn) / diff) % 6);
    else if (max === gn) h = 60 * ((bn - rn) / diff + 2);
    else h = 60 * ((rn - gn) / diff + 4);
    if (h < 0) h += 360;
  }
  return { h: Math.round(h), s: +(s * 100).toFixed(1), l: +(l * 100).toFixed(1) };
}

const colors = ["#143a52", "#8a1c1c", "#f5f7f8", "#fff", "#1c2733", "#8a5b00", "#d5dbe0"];
console.log("hex         rgb()                  hsl()");
for (const hex of colors) {
  const c = hexToRgb(hex);
  const h = rgbToHsl(c);
  console.log(
    `${hex.padEnd(11)} rgb(${c.r} ${c.g} ${c.b})`.padEnd(34) +
    `hsl(${h.h} ${h.s}% ${h.l}%)`
  );
}

console.log("\n--- expansion of short forms ---");
for (const hex of ["#fff", "#0a3", "#143a52cc"]) {
  const c = hexToRgb(hex);
  console.log(`${hex.padEnd(11)} -> rgb(${c.r} ${c.g} ${c.b}) alpha=${c.a.toFixed(3)}`);
}

console.log("\n--- generating a scale by varying one hue's lightness ---");
const base = rgbToHsl(hexToRgb("#143a52"));
for (const l of [12, 24, 36, 50, 72, 90, 96]) {
  console.log(`hsl(${base.h} ${base.s}% ${l}%)`);
}
```

```
hex         rgb()                  hsl()
#143a52     rgb(20 58 82)         hsl(203 60.8% 20%)
#8a1c1c     rgb(138 28 28)        hsl(0 66.3% 32.5%)
#f5f7f8     rgb(245 247 248)      hsl(200 17.6% 96.7%)
#fff        rgb(255 255 255)      hsl(0 0% 100%)
#1c2733     rgb(28 39 51)         hsl(211 29.1% 15.5%)
#8a5b00     rgb(138 91 0)         hsl(40 100% 27.1%)
#d5dbe0     rgb(213 219 224)      hsl(207 15.1% 85.7%)

--- expansion of short forms ---
#fff        -> rgb(255 255 255) alpha=1.000
#0a3        -> rgb(0 170 51) alpha=1.000
#143a52cc   -> rgb(20 58 82) alpha=0.800

--- generating a scale by varying one hue's lightness ---
hsl(203 60.8% 12%)
hsl(203 60.8% 24%)
hsl(203 60.8% 36%)
hsl(203 60.8% 50%)
hsl(203 60.8% 72%)
hsl(203 60.8% 90%)
hsl(203 60.8% 96%)
```

#### Hexadecimal Notation's Shorthands

The output's second block unpacks three shorthands.

In the three-digit writing, every digit gets **doubled**: `#fff`
means `#ffffff`, `#0a3` means `#00aa33`. This means the number of
colors that can get written with three digits is 4096 — a small
subset of the full set. `#0a3` is not an approximation, it is exactly
`#00aa33`.

In the eight-digit writing, the last two digits carry the **alpha
channel**. In the writing `#143a52cc`, `cc`'s hexadecimal value is
204, and dividing it by 255 gives 0.8. The alpha channel gets taken up
in the next lesson.

#### Color Space Choice Is a Scale Decision

In the output's first block, the color `#143a52` gets written as
`hsl(203 60.8% 20%)`. The two representations give the same color;
their difference shows up once change is needed.

If a lighter tone of this color needs producing, in hexadecimal
notation all three components have to get calculated separately, and
the hue can drift. In HSL notation, only the third number gets
changed; hue and saturation stay fixed. The output's third block shows
this: a seven-step scale got produced from a single hue, by changing
only the lightness value.

A design's color scale therefore gets built in a hue-axis
representation. HSL has a known limitation: equal lightness
differences do not correspond to equal **perceived** differences. At
the same lightness value, yellow looks noticeably brighter than blue.
Perceptually more uniform spaces like `oklch()` are defined to reduce
this problem, and have the same three axes: lightness, chroma, hue.

Whether a representation is recognized by implementations can get
tested:

```css
.warning { background-color: #8a5b00; }

@supports (color: oklch(50% 0.1 40)) {
  .warning { background-color: oklch(45% 0.12 75); }
}
```

The block gets ignored if the condition is not met, and the first
declaration stays in place. The "an invalid declaration gets dropped"
rule seen in previous lessons would give the same result; writing
`@supports` makes the intent explicit.

#### Color Variables

A color scale should get defined once and called by name everywhere.
The tool for this is custom properties:

```css
:root {
  --hue: 203;
  --brand-dark:  hsl(var(--hue) 61% 20%);
  --brand-mid:   hsl(var(--hue) 61% 36%);
  --brand-light: hsl(var(--hue) 61% 90%);
  --surface:     hsl(var(--hue) 18% 97%);
  --text:        hsl(211 29% 16%);
  --warning:     hsl(0 66% 33%);
}
```

Custom properties get defined with a double-hyphen prefix and get
read with `var()`. They are inherited properties: a value defined on
the root element can get read throughout the whole subtree. This also
makes it possible to change the value for a subtree by redefining it
inside a component.

This writing has a side effect: because `--hue` is kept in one place,
the whole scale can get shifted by changing a single number.

One warning is needed: a custom property's value does not get
validated during parsing. If something invalid gets written into
`--brand-dark`'s value, the error shows up where the value gets used,
and that declaration drops.

#### The Station Page's Color Scale

```css
/* station.css — step 16: color scale */
:root {
  --hue: 203;
  --brand-dark: hsl(var(--hue) 61% 20%);
  --brand-light: hsl(var(--hue) 61% 90%);
  --surface:      hsl(var(--hue) 18% 97%);
  --line:         hsl(207 15% 86%);
  --text:         hsl(211 29% 16%);
  --text-muted:   hsl(211 12% 45%);
  --warning:      hsl(0 66% 33%);
  --caution:      hsl(40 100% 27%);
}

body { color: var(--text); }
.masthead { background-color: var(--surface); border-block-end-color: var(--brand-dark); }
.measurement-table th, .measurement-table td { border-block-end-color: var(--line); }
.missing { color: var(--warning); }
.source-note { color: var(--text-muted); }
```

The names in the scale name **function**, not color: `--warning`,
`--line`, `--text-muted`. This is the naming principle from selectors
applied to colors. A variable named `--red` starts lying once the
warning color gets changed to orange.

The `currentcolor` keyword is useful together with this scale: writing
`border-color: currentcolor` on a border ties the border to the text
color, and once the `.missing` class turns the text red, the border
changes along with it. A single declaration keeps two properties
synchronized.

#### Summary

- A color gets written in four forms: from the named vocabulary, in
  hexadecimal notation, in functional notation, and with keywords that
  take color from context.
- In hexadecimal notation, the three-digit form doubles every digit;
  in the eight-digit form, the last two digits carry the alpha
  channel.
- RGB and HSL define the same set of colors with different
  coordinates; the conversion is two-directional, and the choice of
  representation determines how workable changing a color is.
- A color scale gets built in a hue-axis representation: a consistent
  sequence of steps gets produced by changing a single hue's
  lightness.
- Color variables name function, not color; they get defined as
  inherited custom properties and can get changed locally by getting
  redefined in a subtree.

#### Next Step

This lesson treated colors as fully opaque; the alpha channel seen in
the eight-digit writing went by unexplained. What color results when
a transparent color overlaps another, and how does that color's
contrast with text get calculated? The next lesson takes up alpha
blending and the WCAG contrast ratio, numerically.

### Transparency and Blending

Source: https://academia.sh/en/courses/css-fundamentals/transparency-and-blending

The alpha channel's blending calculation, the difference between opacity and alpha, and evaluating a blended color's contrast ratio against WCAG criteria.

The previous lesson treated colors as fully opaque, and the alpha
channel seen in the eight-digit hexadecimal writing went by
unexplained. What color results when a transparent color overlaps a
base?

The answer to this question is a calculation, and the calculation's
result directly affects legibility. A text color made transparent
mixes with the base and turns into a paler color than expected, and
the contrast ratio drops. This lesson takes up the blending
calculation and the contrast ratio, numerically.

#### Alpha Blending

**The alpha channel** declares how opaque a color is: 1 is fully
opaque, 0 is fully transparent. When a transparent color overlaps a
base, a weighted average gets taken for each component:

$$C_{\text{result}} = \alpha \cdot C_{\text{top}} + (1 - \alpha) \cdot C_{\text{base}}$$

The contrast ratio, though, is defined in WCAG. First each color's
**relative luminance** gets calculated — the channels get linearized
and summed with weights according to eye sensitivity — then the two
luminances get put into this relation:

$$\text{ratio} = \frac{L_{\text{light}} + 0.05}{L_{\text{dark}} + 0.05}$$

The result is a number between 1:1 (the same color) and 21:1
(black–white).

```js
// blend.mjs — alpha blending and the WCAG contrast ratio
const hexToRgb = (hex) => {
  let h = hex.replace("#", "");
  if (h.length === 3) h = [...h].map((c) => c + c).join("");
  return { r: parseInt(h.slice(0,2),16), g: parseInt(h.slice(2,4),16), b: parseInt(h.slice(4,6),16) };
};

// the top color gets blended onto the base by alpha: C = a*Ctop + (1-a)*Cbase
function blend(top, alpha, base) {
  const t = hexToRgb(top), b = hexToRgb(base);
  const mix = (x, y) => Math.round(alpha * x + (1 - alpha) * y);
  return { r: mix(t.r, b.r), g: mix(t.g, b.g), b: mix(t.b, b.b) };
}

const toHex = ({ r, g, b }) =>
  "#" + [r, g, b].map((v) => v.toString(16).padStart(2, "0")).join("");

// WCAG 2 relative luminance
function relativeLuminance({ r, g, b }) {
  const channel = (v) => {
    const s = v / 255;
    return s <= 0.04045 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
  };
  return 0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(b);
}

// contrast ratio: (L1 + 0.05) / (L2 + 0.05), L1 >= L2
function contrast(a, b) {
  const la = relativeLuminance(a), lb = relativeLuminance(b);
  const [hi, lo] = la > lb ? [la, lb] : [lb, la];
  return (hi + 0.05) / (lo + 0.05);
}

console.log("--- alpha blending (base #ffffff) ---");
for (const alpha of [1, 0.8, 0.5, 0.2, 0]) {
  const s = blend("#143a52", alpha, "#ffffff");
  console.log(`alpha=${alpha.toFixed(2)} -> rgb(${s.r} ${s.g} ${s.b}) = ${toHex(s)}`);
}

console.log("\n--- same color on a dark base (#1c2733) ---");
for (const alpha of [1, 0.8, 0.5, 0.2]) {
  const s = blend("#143a52", alpha, "#1c2733");
  console.log(`alpha=${alpha.toFixed(2)} -> ${toHex(s)}`);
}

console.log("\n--- contrast ratios ---");
const pairs = [
  ["#1c2733", "#ffffff"], ["#1c2733", "#f5f7f8"], ["#143a52", "#ffffff"],
  ["#8a1c1c", "#ffffff"], ["#8a5b00", "#ffffff"], ["#6b767f", "#ffffff"],
  ["#d5dbe0", "#ffffff"], ["#ffffff", "#143a52"],
];
for (const [fg, bg] of pairs) {
  const o = contrast(hexToRgb(fg), hexToRgb(bg));
  const normal = o >= 7 ? "AAA" : o >= 4.5 ? "AA" : "fails";
  const large  = o >= 4.5 ? "AAA" : o >= 3 ? "AA" : "fails";
  console.log(`${fg} / ${bg} -> ${o.toFixed(2)}:1   normal text ${normal.padEnd(6)} large text ${large}`);
}

console.log("\n--- contrast of transparent text ---");
for (const alpha of [1, 0.7, 0.5, 0.35]) {
  const s = blend("#1c2733", alpha, "#ffffff");
  const o = contrast(s, hexToRgb("#ffffff"));
  console.log(`alpha=${alpha.toFixed(2)} -> ${toHex(s)}  contrast ${o.toFixed(2)}:1  ${o >= 4.5 ? "AA passes" : "AA fails"}`);
}
```

```
--- alpha blending (base #ffffff) ---
alpha=1.00 -> rgb(20 58 82) = #143a52
alpha=0.80 -> rgb(67 97 117) = #436175
alpha=0.50 -> rgb(138 157 169) = #8a9da9
alpha=0.20 -> rgb(208 216 220) = #d0d8dc
alpha=0.00 -> rgb(255 255 255) = #ffffff

--- same color on a dark base (#1c2733) ---
alpha=1.00 -> #143a52
alpha=0.80 -> #16364c
alpha=0.50 -> #183143
alpha=0.20 -> #1a2b39

--- contrast ratios ---
#1c2733 / #ffffff -> 15.14:1   normal text AAA    large text AAA
#1c2733 / #f5f7f8 -> 14.09:1   normal text AAA    large text AAA
#143a52 / #ffffff -> 11.95:1   normal text AAA    large text AAA
#8a1c1c / #ffffff -> 9.28:1   normal text AAA    large text AAA
#8a5b00 / #ffffff -> 5.87:1   normal text AA     large text AAA
#6b767f / #ffffff -> 4.64:1   normal text AA     large text AAA
#d5dbe0 / #ffffff -> 1.40:1   normal text fails  large text fails
#ffffff / #143a52 -> 11.95:1   normal text AAA    large text AAA

--- contrast of transparent text ---
alpha=1.00 -> #1c2733  contrast 15.14:1  AA passes
alpha=0.70 -> #606870  contrast 5.66:1  AA passes
alpha=0.50 -> #8e9399  contrast 3.10:1  AA fails
alpha=0.35 -> #b0b3b8  contrast 2.10:1  AA fails
```

#### Blending Depends on the Base

The output's first two blocks blend the same color onto two different
bases. The color `#143a52` turned into `#8a9da9` on a white base at
0.5 alpha, and into `#183143` on a dark base. The two results do not
resemble each other.

The consequence is this: **a transparent color is not a fixed
color.** Once a component gets a transparent background, the
component's appearance depends on where it gets placed. This is
sometimes the desired behavior — a shadow or an overlay layer should
blend with its base. Sometimes it is not: a warning badge's color
should not change depending on where it sits.

The output's last line shows a boundary case: when alpha is zero, the
result is entirely the base's color. The `transparent` keyword
expresses exactly this.

#### opacity Differs From Alpha

The two mechanisms look similar but do different things.

**Color alpha** only affects the property that color gets applied to.
Writing `background-color: rgb(20 58 82 / 0.5)` only makes the
background transparent; the text and border stay opaque.

**`opacity`**, though, makes the element's **entirety** — background,
text, border, and all its children — transparent as a single layer.
It does not get applied to child elements separately; the element
gets drawn within itself first, then gets blended as a whole.

This has two consequences. First, the transparency of text inside a
box given `opacity: 0.5` cannot get canceled; writing `opacity: 1` on
the child does nothing. Second, as noted in the Stacking Context
lesson, every `opacity` value below 1 establishes a stacking context —
because blending as a whole requires exactly that.

The applicable rule: if a color needs to be made transparent, color
alpha gets written; if an element's entirety needs fading, `opacity`
gets written.

#### Reading the Contrast Ratio

The output's third block gives the ratios evaluated against the WCAG
criteria. The criteria are these:

| Criterion | Normal text | Large text |
|---|---|---|
| AA | 4.5:1 | 3:1 |
| AAA | 7:1 | 4.5:1 |

Large text is text at or above 24 pixels non-bold, or 18.66 pixels
bold. For visual components other than text — form field borders,
icons, focus indicators — the criterion is 3:1.

The rows in the table make three kinds of decision.

The color `#6b767f` gives 4.64:1 on white and passes the AA criterion
**by a hair**. Pale text colors hover around this boundary; a
one-step change can drop below the criterion.

The color `#d5dbe0` passes no criterion, at 1.40:1. This color was
chosen as the line color and cannot get used as a text color. It
stays below the 3:1 criterion even for a border; it can count as
decorative as a divider line, but is insufficient for showing a form
field's boundary.

The last row shows symmetry: `#ffffff / #143a52` and `#143a52 /
#ffffff` give the same ratio. The contrast ratio is directionless; it
does not care which one is the foreground and which is the
background.

#### Transparency Lowers Contrast

The output's last block is this lesson's most practical consequence.
Fully opaque `#1c2733` text gives 15.14:1 on a white base. Once the
same color gets written with 0.5 alpha, the ratio drops to 3.10:1 and
does not pass the AA criterion.

Using alpha to "fade" a text is therefore dangerous: what actually
happens is less a visual adjustment than a loss of legibility, and
the loss stays invisible until it gets calculated. The right approach
is to pick the pale tone from the scale and calculate the chosen
tone's contrast ratio.

This calculation in WCAG 2 has a known limitation: relative luminance
does not reflect perceived contrast with the same accuracy for every
color pair — it drifts especially on dark bases and saturated colors.
The criterion still provides a measurable, testable floor; the
calculation reads as a lower bound that has to get passed, not as the
definition of good design.

```css
/* station.css — step 17: transparency and contrast */
.source-note { color: hsl(211 12% 45%); }        /* opaque tone, not alpha */

.visual-texture { background-color: rgb(20 58 82 / 0.08); }

.overlay {
  position: fixed;
  inset: 0;
  background-color: rgb(28 39 51 / 0.6);
}

.disabled { opacity: 0.55; pointer-events: none; }
```

`.source-note` uses an opaque tone chosen from the scale for its pale
text, instead of alpha. `.visual-texture` and `.overlay`, though, use
alpha in the right place: both are layers **meant** to blend with
their base.

The `.disabled` class uses `opacity` because the element's entirety —
text, border, background — needs to fade together. `pointer-events:
none` keeps the visual state consistent with interaction; but the
disabled information has to get declared in the document with
`disabled` or `aria-disabled`. Transparency alone tells no assistive
technology anything.

#### Summary

- Alpha blending is a weighted average: the result color gets found
  by multiplying the top color by alpha and the base color by the
  remaining share, then summing them.
- A transparent color is not a fixed color; its result depends on the
  base, and the same declaration produces two different colors on two
  different bases.
- Color alpha only affects that declaration; `opacity` makes the
  element and all its children transparent as a single layer and
  establishes a stacking context.
- The contrast ratio gets calculated from relative luminances, falls
  between 1:1 and 21:1, and is directionless; the AA criterion is
  4.5:1 for normal text, 3:1 for large text.
- Fading a text with alpha lowers the contrast ratio; the pale tone
  gets chosen from the scale as a calculated opaque color.

#### Next Step

The color decisions are made, but the text itself has not been
touched yet: which font, what size, what line spacing it gets written
with have not been determined. The next lesson takes up typography
and shows, with calculation, how a font family gets chosen, how a
size scale gets built, and why line height gets written unitless.

### Typography

Source: https://academia.sh/en/courses/css-fundamentals/typography

Resolving a font family list, weight and style axes, a modular size scale, and the calculable rationale for writing line height unitless.

The color decisions are made, but the text itself has not been
touched. The measurement station page has a heading, a masthead
paragraph, a measurement table, and an explanatory note; all of them
stand in the same font, the same size, and the same line spacing.

This lesson focuses on typography's three axes: which font, what
size, what line spacing. All three have a calculable side, and the
decisions get made not one by one, but as a **scale**.

#### A Font Family Is a List

`font-family` does not take a name, it takes a comma-separated
**priority list**. The list gets tried left to right, and the first
family found gets used.

```css
body { font-family: "Inter", system-ui, sans-serif; }
```

Resolution happens at the character level: if the first family on the
list does not contain a character, the next family gets checked for
that character. Two characters on the same line in a document can
come from two different families. In accented text, this can lead to
accented letters getting taken from a different family on the list,
and the line looking inconsistent.

There is always a **generic family** at the end of the list: `serif`,
`sans-serif`, `monospace`, `cursive`, `system-ui`, `ui-monospace`.
These name a class, not a specific font; the environment decides
which file gets used. If a generic family does not get written and no
name on the list gets found, the browser's default font gets used —
this means letting go of control entirely.

If a name contains a space, it gets put in quotes. Generic families
are keywords and do not get quoted; if quoted, they get searched for
like a family name and do not get found.

#### Weight and Style Are Separate Axes

`font-weight` takes a number between 1 and 1000 — the common weights are multiples of 100 — or the
keywords `normal` (400) and `bold` (700). If a family has no file matching the declared weight, the
implementation picks the closest one; it is also possible for it to synthesize a weight that is not found,
and the result looks different from a genuine bold cut.

`font-style` takes three values: `normal`, `italic`, `oblique`. There
is a distinction between the last two — `italic` asks for the
family's separately drawn slanted cut, `oblique` accepts a slanted
version of the upright cut. If the family has no slanted cut,
`italic` also gets produced by slanting.

These two axes' relationship to the document needs establishing
separately. The Web Fundamentals and HTML curriculum established that
the `strong` and `em` elements declare meaning, while `b` and `i`
declare only appearance. Writing `strong` to show a text as bold
loads that text with the meaning "important." If only a visual
emphasis is wanted, the decision gets made in the style sheet.

#### Line Height Gets Written Unitless

`line-height` can take a number, a length, or a percentage. All three
give the same first result, but their **inheritance** behavior
diverges.

```js
// typography.mjs — the unitless-vs-unit line-height difference; modular scale
console.log("--- line-height: a unitless value gets inherited as a ratio ---");
const tree = [
  { name: "body",  fontSize: 16 },
  { name: "main",  fontSize: 16 },
  { name: "h1",    fontSize: 32 },
  { name: "small", fontSize: 12 },
];

for (const format of ["1.5", "24px", "150%"]) {
  console.log(`\nbody { line-height: ${format} }`);
  const root = tree[0];
  for (const node of tree) {
    let lineHeight;
    if (format.endsWith("px"))      lineHeight = parseFloat(format);            // computed: fixed px
    else if (format.endsWith("%"))  lineHeight = root.fontSize * parseFloat(format) / 100; // resolved on body
    else                            lineHeight = node.fontSize * parseFloat(format);  // recomputed on every element
    console.log(`  ${node.name.padEnd(6)} font-size=${String(node.fontSize).padStart(2)}px -> line-height=${lineHeight}px  (ratio ${(lineHeight / node.fontSize).toFixed(2)})`);
  }
}

console.log("\n--- modular scale: base 16px, ratio 1.25 ---");
const base = 16, ratio = 1.25;
const steps = [-2, -1, 0, 1, 2, 3, 4];
for (const n of steps) {
  const px = base * Math.pow(ratio, n);
  console.log(`step ${String(n).padStart(2)}: ${px.toFixed(2)}px = ${(px / base).toFixed(4)}rem`);
}

console.log("\n--- line length ---");
const AVG_CHAR = 0.5; // average character width, in em (example value)
for (const fontSize of [16, 18, 20]) {
  for (const width of [480, 640, 800]) {
    const chars = width / (fontSize * AVG_CHAR);
    const fits = chars >= 45 && chars <= 75 ? "fits" : "does not fit";
    console.log(`font-size=${fontSize}px, box=${width}px -> ~${chars.toFixed(0)} chars/line  ${fits}`);
  }
}
```

```
--- line-height: a unitless value gets inherited as a ratio ---

body { line-height: 1.5 }
  body   font-size=16px -> line-height=24px  (ratio 1.50)
  main   font-size=16px -> line-height=24px  (ratio 1.50)
  h1     font-size=32px -> line-height=48px  (ratio 1.50)
  small  font-size=12px -> line-height=18px  (ratio 1.50)

body { line-height: 24px }
  body   font-size=16px -> line-height=24px  (ratio 1.50)
  main   font-size=16px -> line-height=24px  (ratio 1.50)
  h1     font-size=32px -> line-height=24px  (ratio 0.75)
  small  font-size=12px -> line-height=24px  (ratio 2.00)

body { line-height: 150% }
  body   font-size=16px -> line-height=24px  (ratio 1.50)
  main   font-size=16px -> line-height=24px  (ratio 1.50)
  h1     font-size=32px -> line-height=24px  (ratio 0.75)
  small  font-size=12px -> line-height=24px  (ratio 2.00)

--- modular scale: base 16px, ratio 1.25 ---
step -2: 10.24px = 0.6400rem
step -1: 12.80px = 0.8000rem
step  0: 16.00px = 1.0000rem
step  1: 20.00px = 1.2500rem
step  2: 25.00px = 1.5625rem
step  3: 31.25px = 1.9531rem
step  4: 39.06px = 2.4414rem

--- line length ---
font-size=16px, box=480px -> ~60 chars/line  fits
font-size=16px, box=640px -> ~80 chars/line  does not fit
font-size=16px, box=800px -> ~100 chars/line  does not fit
font-size=18px, box=480px -> ~53 chars/line  fits
font-size=18px, box=640px -> ~71 chars/line  fits
font-size=18px, box=800px -> ~89 chars/line  does not fit
font-size=20px, box=480px -> ~48 chars/line  fits
font-size=20px, box=640px -> ~64 chars/line  fits
font-size=20px, box=800px -> ~80 chars/line  does not fit
```

Comparing the three blocks gives the rule.

When the unitless `1.5` gets written, **the ratio gets inherited**:
every element multiplies its own font size by 1.5. The 32-pixel
heading got a line height of 48 units, the 12-pixel small text 18;
the ratio stayed 1.50 everywhere.

When `24px` or `150%` gets written, though, **the computed value gets
inherited**. The rule established in the Inheritance and Default
Values lesson gives its result here: `150%` gets resolved on the
`body` element and fixed at 24 pixels; child elements inherit that
fixed value. The 32-pixel heading's line height came out to 24 units
— a ratio of 0.75, meaning the line is **shorter** than the text. The
heading's lines overlap.

The applicable rule is this: `line-height` gets written unitless. The
unit writing gets used only on a single element, in a case where
inheritance is not wanted.

#### Modular Scale

Font sizes do not get chosen one by one; they get derived from a base
and a ratio. **A modular scale** is a sequence produced by multiplying
consecutive sizes by a fixed ratio:

$$b_n = b_0 \cdot r^n$$

The output's second block produces seven steps with a 16-pixel base
and a 1.25 ratio. The results also get given converted to `rem`; this
directly shows how the scale gets written into the style sheet:

```css
:root {
  --scale-1: 0.64rem;
  --scale-2: 0.8rem;
  --scale-3: 1rem;
  --scale-4: 1.25rem;
  --scale-5: 1.5625rem;
  --scale-6: 1.9531rem;
  --scale-7: 2.4414rem;
}
```

The scale's value lies in making consistency mandatory: a heading
cannot get "a little bigger" a size; the next step on the scale gets
chosen. As the ratio grows, the difference between steps widens and
the hierarchy becomes clearer; as it shrinks, the scale compresses and
the levels move closer together.

#### Line Length

The output's third block calculates the relationship between size and
box width. The established criterion for a readable line length is
45–75 characters per line; the calculation gets evaluated against this
range.

The output shows a trade-off. In a 640-unit box, 16-pixel text gives
about 80 characters per line — outside the range. In the same box,
18-pixel text gives 71 characters and falls inside the range.
Enlarging the font size **shortens** the line, because fewer
characters fit into the same width.

This is the rationale behind the `max-width: 70ch` declaration written
in the Box Sizing lesson: because the `ch` unit depends on character
width, when the font size changes the box width changes along with
it, and the line length stays inside the range. The 0.5 em average
character width in the calculation is an example value; the real
ratio varies by font.

```css
/* station.css — step 18: typography scale */
:root {
  --font-body: 1rem;
  --font-small: 0.8rem;
  --font-h3: 1.25rem;
  --font-h2: 1.5625rem;
  --font-h1: 2.4414rem;
}

body {
  font-family: system-ui, sans-serif;
  font-size: var(--font-body);
  line-height: 1.5;
}

h1 { font-size: var(--font-h1); line-height: 1.15; }
h2 { font-size: var(--font-h2); line-height: 1.2; }
h3 { font-size: var(--font-h3); line-height: 1.3; }

.source-note { font-size: var(--font-small); }

.measurement-table td.value {
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum";
}
```

Giving headings a **smaller** line-height ratio than the body text is
a typography rule: large-point lines are already far apart, and if
the same ratio gets kept, the spacing looks needlessly loose.

The last rule is specific to the measurement table. `tabular-nums`
asks for digits to get drawn at equal widths, so that the digits of
numbers stacked on top of each other line up. If the font does not
carry this feature, the declaration stays without effect and the
digits stay proportionally spaced — a situation that makes a numeric
table harder to read.

#### Summary

- `font-family` is a priority list resolved at the character level;
  if the list has no generic family at the end, control gets left
  entirely to the environment.
- `font-weight` and `font-style` are separate axes; a weight or slant
  with no counterpart in the family gets synthesized by calculation
  and looks different from a genuine cut.
- When `line-height` gets written unitless, the ratio gets inherited
  and every element multiplies by its own size; in a unit or
  percentage writing, the computed value gets inherited, and lines
  overlap on large-point elements.
- Font sizes do not get chosen one by one; they get derived as a
  modular scale from a base and a ratio, and a hierarchy level
  corresponds to a step on the scale.
- Line length gets kept in the 45–75 character range; enlarging the
  font size shortens the line in the same box, so size and box width
  get decided together.

#### Next Step

This lesson called the font by a name but did not ask where the file
behind that name comes from. How does a font get linked to the
document, what happens to the text while the file downloads, and how
does that wait affect the page's reading? The next lesson takes up
font loading and the display behavior during loading.

### Font Loading

Source: https://academia.sh/en/courses/css-fundamentals/font-loading

Linking a font file to the document, conditional downloading with subset ranges, and choosing the display behavior during loading.

The previous lesson called the font by a name: `font-family: "Inter",
system-ui, sans-serif`. Where the file behind that name comes from did
not get asked. How does a font not installed on the system get linked
to the document, and what happens to the text while the file
downloads?

This lesson takes up font loading. The subject concerns loading order
more than typography, and is an extension of the Resource Loading
Order lesson in the Web Fundamentals and HTML curriculum: a font is a
subresource too, and when it arrives affects the page's reading.

#### Font Definition

The `@font-face` rule ties a family name to a file. It is not a rule
in the usual sense — it has no selector, it does not get applied to
any element. What it does is define a name that can get used in
`font-family` lists.

```css
@font-face {
  font-family: "Station Sans";
  src: url("/fonts/station-sans.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
```

`font-weight` and `font-style` are not a declaration here, they are a
**definition**: they say which weight and style this file provides.
Separate `@font-face` blocks get written for different weights under
the same family name, and the browser, once `font-weight: 700` gets
requested, picks the file that provides that weight.

When no file gets declared for a weight, the browser uses the closest
one and, if needed, bolds it by calculation. This behavior, mentioned
in the previous lesson, shows up when `@font-face` blocks get written
incompletely.

`src` can take multiple sources; the first one that resolves gets
used. The `format()` declaration lets the browser tell whether the
format is recognized without downloading the file — a source in an
unrecognized format gets skipped, not downloaded.

#### Subset Ranges Enable Conditional Downloading

The `unicode-range` definition declares which code points a file
provides. Its consequence is decisive: **if the document has no
character from that range, the file does not get downloaded.**

The program below defines four subset files and calculates which ones
get downloaded for three texts.

```js
// font-loading.mjs — unicode-range subset selection and the font-display timeline
const subsets = [
  { file: "latin.woff2",     ranges: [[0x0000, 0x00ff], [0x0131, 0x0131], [0x2000, 0x206f]] },
  { file: "latin-ext.woff2", ranges: [[0x0100, 0x024f], [0x1e00, 0x1eff]] },
  { file: "greek.woff2",     ranges: [[0x0370, 0x03ff]] },
  { file: "cyrillic.woff2",  ranges: [[0x0400, 0x04ff]] },
];

function neededSubsets(text) {
  const needed = new Map();
  for (const ch of text) {
    const code = ch.codePointAt(0);
    for (const subset of subsets) {
      if (subset.ranges.some(([a, b]) => code >= a && code <= b)) {
        if (!needed.has(subset.file)) needed.set(subset.file, new Set());
        needed.get(subset.file).add(ch);
        break;
      }
    }
  }
  return needed;
}

const texts = [
  "Kuzey Yamac Olcum Istasyonu",
  "Kuzey Yamaç Ölçüm İstasyonu",
  "Sıcaklık: -3,4 °C — bağıl nem %78",
];

for (const text of texts) {
  const needed = neededSubsets(text);
  console.log(`text: ${text}`);
  for (const [file, chars] of needed) {
    console.log(`  ${file.padEnd(17)} downloaded  (${chars.size} distinct character${chars.size === 1 ? "" : "s"})`);
  }
  const notDownloaded = subsets.filter((s) => !needed.has(s.file)).map((s) => s.file);
  console.log(`  not downloaded: ${notDownloaded.join(", ") || "(none)"}`);
  console.log("");
}

console.log("--- font-display: block and swap periods ---");
const BEHAVIOR = {
  "auto":     { blockPeriod: "implementation-dependent", swapPeriod: "implementation-dependent" },
  "block":    { blockPeriod: "short (~3s)",               swapPeriod: "infinite" },
  "swap":     { blockPeriod: "very short (~100ms)",        swapPeriod: "infinite" },
  "fallback": { blockPeriod: "very short (~100ms)",        swapPeriod: "short (~3s)" },
  "optional": { blockPeriod: "very short (~100ms)",        swapPeriod: "none" },
};
for (const [value, periods] of Object.entries(BEHAVIOR)) {
  console.log(`${value.padEnd(9)} block=${periods.blockPeriod.padEnd(22)} swap=${periods.swapPeriod}`);
}

console.log("\n--- is text visible during the block period ---");
for (const [value] of Object.entries(BEHAVIOR)) {
  const visible = value === "block" || value === "auto" ? "no (invisible text)" : "yes (fallback font)";
  console.log(`${value.padEnd(9)} -> ${visible}`);
}
```

```
text: Kuzey Yamac Olcum Istasyonu
  latin.woff2       downloaded  (17 distinct characters)
  not downloaded: latin-ext.woff2, greek.woff2, cyrillic.woff2

text: Kuzey Yamaç Ölçüm İstasyonu
  latin.woff2       downloaded  (17 distinct characters)
  latin-ext.woff2   downloaded  (1 distinct character)
  not downloaded: greek.woff2, cyrillic.woff2

text: Sıcaklık: -3,4 °C — bağıl nem %78
  latin.woff2       downloaded  (22 distinct characters)
  latin-ext.woff2   downloaded  (1 distinct character)
  not downloaded: greek.woff2, cyrillic.woff2

--- font-display: block and swap periods ---
auto      block=implementation-dependent swap=implementation-dependent
block     block=short (~3s)            swap=infinite
swap      block=very short (~100ms)    swap=infinite
fallback  block=very short (~100ms)    swap=short (~3s)
optional  block=very short (~100ms)    swap=none

--- is text visible during the block period ---
auto      -> no (invisible text)
block     -> no (invisible text)
swap      -> yes (fallback font)
fallback  -> yes (fallback font)
optional  -> yes (fallback font)
```

The first text carries only ASCII characters, and a single file gets
downloaded. The second text is in Turkish spelling, and one more file
gets added — because of **a single character**, no less.

That character is the capital dotted İ (U+0130), and it falls into
the extended Latin subset. The lowercase dotless ı (U+0131), though,
is defined within the base subset. The two letters, despite being
part of the same alphabet, come from two separate files. In the third
text, the same situation occurs for ğ (U+011F).

This means that in Turkish text, two files get downloaded at once; a
heading waits for the second file just because it carries an İ. This
cost disappears once subset files get generated to match the text's
own content.

`unicode-range` works with code points, and the distinction between a
character set and a code point, defined in the How Computers Work
curriculum, applies directly here: the ranges are split by code point
number, not by how the characters look.

#### Display Behavior

What happens to the text while the font file downloads gets chosen
with `font-display`. The decision is defined by two periods:

**The block period** — while the file is awaited, the text does not
get **drawn**. The text is invisible during this period.

**The swap period** — the text gets drawn with a fallback font; if
the file arrives within this period, the font gets swapped.

The output's second and third blocks distinguish the five values by
these two periods. The period values are given in the specification as
a **recommendation**; the exact numbers are implementation-dependent.

`block` — grants a long block period. The result is that the text
stays invisible for a while. This is an undesired behavior except in
cases like icon fonts, where a fallback is meaningless.

`swap` — the block period is very short; the text appears immediately
with a fallback font and switches once the file arrives. The text
never becomes invisible, but the layout shifts at the moment of the
switch.

`fallback` — grants a short block period and a short swap period. If
the file arrives late, the fallback font becomes permanent, and the
layout never shifts again.

`optional` — has no swap period. If the file does not arrive very
fast, it does not get used at all on that page load; it gets cached
and used on the next visit.

The choice is a trade-off: `swap` prioritizes the text's legibility
and accepts layout shift; `optional` prioritizes layout stability and
accepts giving up on the intended font.

#### Reducing Layout Shift

If the fallback font's metrics differ from the actual font's, the
text box grows or shrinks at the moment of the switch, and everything
below it shifts. This shift gets reduced in two ways.

The first is bringing the fallback font's metrics closer to the
actual font's. The `size-adjust`, `ascent-override`,
`descent-override`, and `line-gap-override` definitions make it
possible to produce a family from a local font to get used as the
fallback:

```css
@font-face {
  font-family: "Station Sans Fallback";
  src: local("Arial");
  size-adjust: 97%;
  ascent-override: 90%;
}
```

The second is making sure the file gets requested early, while the
document gets parsed:

```html
<link rel="preload" href="/fonts/station-sans.woff2" as="font" type="font/woff2" crossorigin>
```

This link is not a style sheet link; it only declares that the
resource should get requested early. Font requests are subject to
cross-origin rules even when the resource comes from the same origin;
`crossorigin` therefore has to get written, or the resource gets
downloaded twice.

#### Local Hosting

The difference between serving a font file from your own server and
calling it from a third-party server is not a performance detail.

A third-party resource requires a separate connection to get
established: name resolution, connection setup, and the secure
handshake all get done again. Each of these steps, defined in the How
the Internet Works curriculum, adds latency.

A second consequence concerns privacy: every request tells the third
party the visitor's address and which page they are viewing. Local
hosting eliminates this disclosure.

```css
/* station.css — step 19: font definition */
@font-face {
  font-family: "Station Sans";
  src: url("/fonts/station-sans-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+2000-206F;
  font-weight: 400;
  font-display: swap;
}

@font-face {
  font-family: "Station Sans";
  src: url("/fonts/station-sans-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-024F, U+1E00-1EFF;
  font-weight: 400;
  font-display: swap;
}

body { font-family: "Station Sans", system-ui, sans-serif; }
```

The two definitions share the same family name and get distinguished
by their ranges. Because the page has Turkish text, both get
downloaded; in an English version only the first one would download.

#### Summary

- `@font-face` is not a rule, it is a definition: it ties a family
  name to a file and declares which weight and style that file
  provides.
- `unicode-range` declares the code points a file covers; if the
  document has no character from that range, the file never gets
  downloaded.
- In Turkish text, letters like the capital dotted İ and ğ fall into
  the extended Latin subset; a single character can trigger the
  download of a second file.
- `font-display` sets two periods: during the block period the text
  does not get drawn, during the swap period it gets drawn with a
  fallback font and gets swapped if the file arrives.
- Local hosting eliminates the need for a separate connection setup
  and prevents visitor information from getting disclosed to a third
  party.

#### Next Step

The font has arrived and the size scale is built. Decisions on the
text itself remain: how lines get aligned, how whitespace gets
processed, letters' uppercase–lowercase conversion, and how underlined
text gets formatted. The next lesson takes up these decisions and
shows the whitespace-processing rules with a runnable example.

### Text Formatting

Source: https://academia.sh/en/courses/css-fundamentals/text-formatting

Whitespace processing rules, alignment and indentation, text-transform's dependence on language, and controlling underline decoration.

The font has arrived and the size scale is built. Decisions on the
text itself remain: what happens to whitespace in the source text,
how lines get aligned, how letters get converted, how links get
underlined.

The first of these decisions is the most surprising one. In the
document written in the Web Fundamentals and HTML curriculum, the
text was written indented, and the indentation did not show up on
screen. This lesson shows the reason for that, with a rule and a
calculation.

#### Whitespace Processing

In the default behavior, **consecutive whitespace characters in the
source text get collapsed into a single space**, and line breaks
count as a space character too. This is what makes the HTML document
formattable: indenting, line-splitting, and alignment happen freely
in the source text without affecting the output.

The rule gets changed with the `white-space` property. The program
below applies five values to the same source text.

```js
// whitespace.mjs — how white-space values process the same source text
const SOURCE = "North   Slope\n  Station\tmeasurement   readings";

const BEHAVIOR = {
  "normal":   { collapseSpaces: true,  preserveBreaks: false, wrap: true  },
  "nowrap":   { collapseSpaces: true,  preserveBreaks: false, wrap: false },
  "pre":      { collapseSpaces: false, preserveBreaks: true,  wrap: false },
  "pre-wrap": { collapseSpaces: false, preserveBreaks: true,  wrap: true  },
  "pre-line": { collapseSpaces: true,  preserveBreaks: true,  wrap: true  },
};

function process(text, rule) {
  let s = text;
  if (rule.preserveBreaks) {
    if (rule.collapseSpaces) s = s.split("\n").map((p) => p.replace(/[ \t]+/g, " ").trim()).join("\n");
  } else {
    s = s.replace(/\s+/g, " ").trim();
  }
  return s;
}

console.log("source (with escape sequences):");
console.log(JSON.stringify(SOURCE));
console.log("");
for (const [name, rule] of Object.entries(BEHAVIOR)) {
  const result = process(SOURCE, rule);
  console.log(`${name.padEnd(9)} wrap=${String(rule.wrap).padEnd(5)} -> ${JSON.stringify(result)}`);
}

console.log("\n--- text-transform: the Turkish special case ---");
const sample = "istasyon ILIK ışık";
console.log(`source            : ${sample}`);
console.log(`uppercase (en)    : ${sample.toLocaleUpperCase("en-US")}`);
console.log(`uppercase (tr)    : ${sample.toLocaleUpperCase("tr-TR")}`);
console.log(`lowercase (en)    : ${sample.toLocaleLowerCase("en-US")}`);
console.log(`lowercase (tr)    : ${sample.toLocaleLowerCase("tr-TR")}`);

console.log("\n--- text-indent and alignment effect (60-character box) ---");
const BOX = 60;
const line = "Station sits at 1840 meters above sea level";
for (const [name, align] of [["start", (s) => s.padEnd(BOX)], ["end", (s) => s.padStart(BOX)], ["center", (s) => s.padStart((BOX + s.length) / 2).padEnd(BOX)]]) {
  console.log(`${name.padEnd(7)}|${align(line)}|`);
}
```

```
source (with escape sequences):
"North   Slope\n  Station\tmeasurement   readings"

normal    wrap=true  -> "North Slope Station measurement readings"
nowrap    wrap=false -> "North Slope Station measurement readings"
pre       wrap=false -> "North   Slope\n  Station\tmeasurement   readings"
pre-wrap  wrap=true  -> "North   Slope\n  Station\tmeasurement   readings"
pre-line  wrap=true  -> "North Slope\nStation measurement readings"

--- text-transform: the Turkish special case ---
source            : istasyon ILIK ışık
uppercase (en)    : ISTASYON ILIK IŞIK
uppercase (tr)    : İSTASYON ILIK IŞIK
lowercase (en)    : istasyon ilik ışık
lowercase (tr)    : istasyon ılık ışık

--- text-indent and alignment effect (60-character box) ---
start  |Station sits at 1840 meters above sea level                 |
end    |                 Station sits at 1840 meters above sea level|
center |        Station sits at 1840 meters above sea level         |
```

The output distinguishes two axes: whether whitespace gets collapsed,
and whether line-splitting gets allowed.

`normal` — consecutive spaces collapse to one, line breaks count as a
space, the line gets split to fit the box. The three spaces and the
tab character in the source text turned into a single space.

`nowrap` — does the same whitespace processing but does not allow
line-splitting. The text overflows the box; the situation established
in the Overflow Management lesson shows up.

`pre` — changes nothing: whitespace and line breaks are kept exactly
as they are, and no automatic line-splitting happens. This is the
browser's default style for the `pre` element.

`pre-wrap` — keeps whitespace and line breaks but splits lines that
do not fit the box. This value gets written instead of `pre` when
long lines need showing without overflowing.

`pre-line` — collapses whitespace but **keeps line breaks**. It gets
used to show user text coming from a text field with its paragraph
structure.

#### Letter Conversion Depends on Language

The output's second block shows why the `text-transform` property
cannot be considered a purely visual conversion.

The word `istasyon` becomes `ISTASYON` when uppercased by English
rules, and `İSTASYON` when uppercased by Turkish rules. The word
`ILIK` becomes `ilik` when lowercased by English rules, and `ılık` by
Turkish rules — two different words.

The reason is that in the Turkish alphabet, `i` and `ı` are separate
letters, and their uppercase counterparts are `İ` and `I`. The code
point distinction from the How Computers Work curriculum turns into a
difference in meaning here.

This shows that writing `text-transform` depends on the document's
`lang` declaration. In a document where `lang="tr"` has not been
declared on the root element, converting to uppercase can give a
wrong result.

A second warning: the conversion happens only in drawing. The text in
the document does not change; copied text comes in its source form,
and a screen reader reads its source form. If a heading needs to
appear in uppercase, this is a visual decision and gets made in the
style sheet; the text itself should stay in its ordinary spelling in
the source.

#### Alignment

`text-align` takes five basic values: `start`, `end`, `center`,
`justify`, and their physical counterparts `left` and `right`. The
logical values follow the writing direction, and that is the form
used in this course.

The output's third block shows three alignments in a fixed-width box.
The decision gets made by three criteria.

**Centering** works for short text; in long paragraphs, each line's
starting point shifts, making it harder for the eye to find the start
of a line. It suits headings and short captions, not body text.

**Justification** (`justify`) makes lines the same length by widening the word spacing; the last line of a
paragraph is exempt and stays start-aligned by default. In narrow columns, the spacing widens excessively
and vertical white streaks form inside the text. The result is more balanced when used together with
`hyphens: auto`, but left alignment still gets preferred in narrow columns.

**Indentation** (`text-indent`) pulls paragraph starts inward. If
vertical spacing exists between paragraphs, indentation is
unnecessary; when both get used together, the paragraph break gets
marked twice. In print tradition, indentation stands in for spacing.

#### Text Decoration

`text-decoration` is a shorthand and sets four longhands: `-line`
(where the line sits), `-color`, `-style`, `-thickness`.

Links being underlined comes from the browser default and is not a
visual decoration: marking a link within text with color alone makes
the link invisible to a reader who cannot perceive color
distinctions. The underline should not get removed unless another
visual distinguisher gets put in its place.

The underline's legibility gets adjusted with `text-underline-offset`
and `text-decoration-skip-ink`; the second ensures the line does not
cut through letters' descenders.

```css
/* station.css — step 20: text formatting */
.measurement-table th {
  text-transform: uppercase;
  letter-spacing: 0.04em;
  font-size: var(--font-small);
}

.measurement-raw-data {
  white-space: pre-wrap;
  font-family: ui-monospace, monospace;
  tab-size: 2;
}

.observation-note { white-space: pre-line; }

main a {
  text-decoration-line: underline;
  text-underline-offset: 0.15em;
  text-decoration-thickness: 1px;
  text-decoration-skip-ink: auto;
}

main a:hover { text-decoration-thickness: 2px; }
```

The `.measurement-raw-data` class shows the raw measurement record
coming from the station: line breaks and alignment spaces carry
meaning, so they get preserved; writing `pre-wrap` instead of `pre`
makes long lines split instead of overflow.

The letter spacing given to the header cells is a standard correction
for uppercase-transformed text: capital letters look more cramped
than lowercase, and reading gets harder if the spacing does not
widen.

#### Summary

- In the default whitespace processing, consecutive spaces collapse
  to one and line breaks count as a space; the source text's
  indentability rests on this.
- `white-space` sets two axes at once: whether whitespace gets
  collapsed, and whether line-splitting gets allowed. `pre-wrap`
  keeps whitespace and splits the line, `pre-line` collapses
  whitespace and keeps line breaks.
- `text-transform` depends on language; because `i` and `ı` have
  separate uppercase counterparts in Turkish, a wrong conversion can
  result without a `lang` declaration.
- The conversion happens only in drawing; the text in the document
  does not change, and comes in its source form when copied.
- A link's underline is not a decoration, it is a color-independent
  distinguisher; if it gets removed, another visual marker has to get
  put in its place.

#### Next Step

The text decisions are complete. The back of the boxes has not been
looked at yet: a box's background can be not just a color, but a
positionable and repeatable image too. The next lesson takes up
background layers, positioning and repetition rules, and which region
the background extends to.

### Backgrounds

Source: https://academia.sh/en/courses/css-fundamentals/backgrounds

Background layers, the calculation behind percentage positioning, repeat modes, and the numeric counterpart of cover and contain scaling.

The text decisions are complete. The back of the boxes has not been
looked at yet. The Box Model lesson established that the background
extends to the border box and covers the padding; what the background
itself can be was not said.

A background is not just a color. It can be a **layer stack** made of
positionable, repeatable, scalable images. This lesson takes up those
layers' rules and calculations.

#### A Background Is a Layer Stack

`background` is a shorthand and sets eight longhands: `-color`,
`-image`, `-position`, `-size`, `-repeat`, `-origin`, `-clip`,
`-attachment`.

`background-image` can take multiple comma-separated values. Each
value establishes a **layer**, and the layers get ordered **left to
right, top to bottom**: the first member of the list gets drawn on
top.

`background-color` is not a layer; it always gets drawn at the very
bottom, as a single color. This is why it cannot get multiplied with
a comma.

The other longhands also get multiplied with a comma and get matched
to the layers in order. The number of layers is determined by the
length of the `background-image` list; lists that fall short get
repeated from the start.

#### A Percentage Position Is an Alignment

When `background-position` takes a percentage, that percentage is not
a distance, it is an **alignment**: the image's $p$ percent point gets
matched to the area's $p$ percent point. The result:

$$x = (A_{\text{width}} - I_{\text{width}}) \cdot p$$

```js
// background.mjs — background-position percentage and repeat math
// percentage positioning: the image's p% point lands on the area's p% point
function position(areaWidth, imageWidth, value) {
  if (typeof value === "number") return value;                 // px
  const p = parseFloat(value) / 100;
  return (areaWidth - imageWidth) * p;
}

const AREA = { width: 400, height: 200 };
const IMAGE = { width: 120, height: 60 };

console.log(`area ${AREA.width}x${AREA.height}, image ${IMAGE.width}x${IMAGE.height}`);
for (const [x, y] of [["0%","0%"], ["50%","50%"], ["100%","100%"], ["25%","75%"], [16, 16]]) {
  const left = position(AREA.width, IMAGE.width, x);
  const top = position(AREA.height, IMAGE.height, y);
  console.log(`position: ${String(x).padEnd(5)} ${String(y).padEnd(5)} -> top-left (${left}, ${top})`);
}

console.log("\n--- repeat: tile starting points ---");
function tiles(area, image, start, repeatMode) {
  if (repeatMode === "no-repeat") return [start];
  const points = [];
  let x = start;
  while (x > -image) x -= image;   // rewind leftward
  x += image;
  while (x < area) { points.push(x); x += image; }
  return points;
}
for (const mode of ["repeat", "no-repeat"]) {
  const points = tiles(AREA.width, IMAGE.width, position(AREA.width, IMAGE.width, "50%"), mode);
  console.log(`${mode.padEnd(10)} -> ${points.length} tile${points.length === 1 ? "" : "s"}, x = ${points.join(", ")}`);
}

console.log("\n--- repeat: leftover horizontal space ---");
for (const areaWidth of [400, 360, 480]) {
  const whole = Math.floor(areaWidth / IMAGE.width);
  const remainder = areaWidth - whole * IMAGE.width;
  const roundScale = areaWidth / Math.round(areaWidth / IMAGE.width);
  console.log(`area=${areaWidth}: repeat -> ${whole} whole + ${remainder}px cropped | round -> ${Math.round(areaWidth / IMAGE.width)} tiles, each ${roundScale.toFixed(2)}px | space -> ${whole} tiles, ${whole > 1 ? (remainder / (whole - 1)).toFixed(2) : 0}px between`);
}

console.log("\n--- background-size: cover and contain ---");
function scale({ areaWidth, areaHeight, imageWidth, imageHeight }, mode) {
  const scaleX = areaWidth / imageWidth, scaleY = areaHeight / imageHeight;
  const factor = mode === "cover" ? Math.max(scaleX, scaleY) : Math.min(scaleX, scaleY);
  return { width: +(imageWidth * factor).toFixed(1), height: +(imageHeight * factor).toFixed(1), factor: +factor.toFixed(3) };
}
const dims = { areaWidth: 400, areaHeight: 200, imageWidth: 800, imageHeight: 600 };
for (const mode of ["cover", "contain"]) {
  const result = scale(dims, mode);
  console.log(`${mode.padEnd(8)} factor=${result.factor} -> ${result.width}x${result.height}  (area ${dims.areaWidth}x${dims.areaHeight})`);
}
```

```
area 400x200, image 120x60
position: 0%    0%    -> top-left (0, 0)
position: 50%   50%   -> top-left (140, 70)
position: 100%  100%  -> top-left (280, 140)
position: 25%   75%   -> top-left (70, 105)
position: 16    16    -> top-left (16, 16)

--- repeat: tile starting points ---
repeat     -> 5 tiles, x = -100, 20, 140, 260, 380
no-repeat  -> 1 tile, x = 140

--- repeat: leftover horizontal space ---
area=400: repeat -> 3 whole + 40px cropped | round -> 3 tiles, each 133.33px | space -> 3 tiles, 20.00px between
area=360: repeat -> 3 whole + 0px cropped | round -> 3 tiles, each 120.00px | space -> 3 tiles, 0.00px between
area=480: repeat -> 4 whole + 0px cropped | round -> 4 tiles, each 120.00px | space -> 4 tiles, 0.00px between

--- background-size: cover and contain ---
cover    factor=0.5 -> 400x300  (area 400x200)
contain  factor=0.333 -> 266.7x200  (area 400x200)
```

The first block's `50% 50%` value giving 140 and 70 is the
calculation's direct result: half the difference between the area and
the image. The result is the image getting centered. `100% 100%`
pushes it to the bottom-right corner, `0% 0%` to the top-left.

The difference with pixel values shows up in the last row: `16px
16px` is always 16 units from the edge, independent of the area's and
image's dimensions. A percentage declares an alignment, a length
declares a distance.

`background-position` also accepts a four-value writing, which
selects which edge to measure from: the writing `right 16px bottom
16px` places it 16 units in from the bottom-right corner.

#### Repeat Modes

The second block shows that in `repeat` mode, tiling starts
**outside** the visible area: the first tile sits at $-100$.
Repetition is defined as extending the positioned tile to infinity in
both directions; whatever falls outside the visible area gets
cropped.

The third block gives the difference between the three modes in
numbers. A 120-unit image in a 400-unit area:

- `repeat` — 3 whole tiles fit, a 40-unit remainder appears cropped.
- `round` — the tile count gets rounded and the image gets
  **scaled**: 3 tiles, each 133.33 units. No cropping remains, but
  the image's aspect ratio breaks.
- `space` — the tile count gets rounded down and the leftover space
  gets **distributed between the gaps**: 3 tiles, 20 units apart
  each. The scale is preserved, a gap opens up.

When the area is an exact multiple of the image (the 360 and 480
rows), all three modes give the same result.

`repeat-x` and `repeat-y` repeat on only one axis; `no-repeat` does
not repeat at all.

#### cover and contain

The last block compares the two scaling keywords. An 800×600 image in
a 400×200 area:

`cover` — picks the **larger** of the two axes' scale ratios. The
result is 400×300: the image fills the area completely, and the
overflowing part gets cropped.

`contain` — picks the **smaller** one. The result is 266.7×200: the
whole image is visible, and part of the area stays empty.

The decision gets made by this question: is cropping acceptable, or
is leftover space? `cover` gets written for a decorative image,
`contain` for an image that needs to be seen in full.

When `cover` gets used, where the cropping happens gets directed with
`background-position`; if the image's important part is near an
edge, centered cropping cuts it off.

#### The Background's Boundaries

Three properties determine which region the background relates to,
and the region names from the Box Model lesson appear here as values.

`background-origin` — chooses **which box** positioning gets measured
relative to. Its initial value is `padding-box`.

`background-clip` — chooses **how far** the background gets drawn.
Its initial value is `border-box`; this is why the background shows
through the gaps of a dashed border. When `content-box` gets written,
the background does not enter the padding.

`background-attachment` — chooses whether the background scrolls
together with the box (`scroll`, `local`) or gets fixed to the
viewport (`fixed`).

#### Image Backgrounds and Accessibility

A background image does not exist in the document: it has no
alternative text, does not get declared to assistive technology, and
does not get read as content by a search engine. This is the same
boundary as the one for content produced with pseudo-elements.

The consequence is a rule: **no image that carries meaning gets
placed as a background.** A location map has to stand in the document
as an `img` element with alternative text. Backgrounds are for
decorative textures and surfaces that text gets written over.

The legibility of text written over a base image also needs to get
secured. If the image does not download or its download is delayed,
the text faces the background **color**; the contrast between that
color and the text color has to pass the criterion calculated in the
previous lesson.

```css
/* station.css — step 21: background layers */
.masthead {
  background-color: var(--surface);
  background-image: url("/images/topography.svg");
  background-position: right -40px top 50%;
  background-repeat: no-repeat;
  background-size: 320px auto;
}

.measurement-table tbody tr:nth-child(odd) {
  background-color: hsl(var(--hue) 18% 97%);
  background-clip: padding-box;
}

.warning-box {
  background-color: hsl(0 66% 97%);
  background-image: url("/images/warning.svg");
  background-position: 12px center;
  background-repeat: no-repeat;
  background-size: 20px 20px;
  padding-inline-start: 44px;
}
```

The pattern in the masthead is decorative and gets written together
with `background-color`: if the image does not download, the surface
color remains, and the heading text's contrast stays intact.

The icon in the `.warning-box` class is decorative too; the warning
itself is written in the text inside the box. The icon is only a
visual marker for the text, and `padding-inline-start` makes room for
it — the icon not overlapping the text depends on this declaration.

#### Summary

- `background-image` builds a layer stack by getting multiplied with
  a comma; the first member of the list gets drawn on top, and
  `background-color` always stays at the bottom, as a single color.
- A percentage background position is an alignment, not a distance:
  the image's $p$ percent point gets matched to the area's $p$
  percent point; this is why `50%` centers.
- Repeat modes handle the leftover space differently: `repeat` crops
  it, `round` removes the crop by scaling the image, `space`
  distributes the leftover between the tiles.
- `cover` picks the larger of the two scale ratios, fills the area,
  and crops the overflow; `contain` picks the smaller one, shows the
  whole image, and leaves empty space.
- A background image does not exist in the document and does not get
  declared to assistive technology; images that carry meaning belong
  in the markup, backgrounds get written only as a decorative layer.

#### Next Step

Background layers got built with images coming from a file. A layer
can also get produced without a file: with calculated transitions
between colors. The next lesson takes up linear and radial gradients,
the calculation behind color stops, and why gradients count as a
background image, not an image.

### Gradients

Source: https://academia.sh/en/courses/css-fundamentals/gradients

Linear and radial color gradients; the rule for resolving stop positions, calculating intermediate colors, and producing a line with a hard transition.

Background layers got built with images coming from a file. A layer
can also get produced without a file: with calculated transitions
between colors.

**A gradient** is not a color, it is an **image**. This distinction
has consequences: it cannot get written into `background-color`, it
gets written into `background-image`; it has no intrinsic size and
gets sized by the area it gets placed in; `background-size`,
`background-position`, and `background-repeat` apply to it too.

#### Color Stops

A gradient consists of **color stops**. Each stop carries a color and
an optional position. When positions do not get written, they get
filled in by a rule: the first stop lands at 0%, the last at 100%,
and the ones in between get distributed **at equal intervals**.

Every point between two stops is a linear mix of the two colors.

```js
// gradient.mjs — resolving color stops and calculating intermediate colors
const hexToRgb = (h) => ({ r: parseInt(h.slice(1,3),16), g: parseInt(h.slice(3,5),16), b: parseInt(h.slice(5,7),16) });
const hex = ({r,g,b}) => "#" + [r,g,b].map(v => Math.round(v).toString(16).padStart(2,"0")).join("");

// stops with no position get distributed evenly between neighboring known points
function resolveStops(stops) {
  const resolved = stops.map((s) => ({ ...s }));
  if (resolved[0].position == null) resolved[0].position = 0;
  if (resolved.at(-1).position == null) resolved.at(-1).position = 100;
  let i = 0;
  while (i < resolved.length) {
    if (resolved[i].position != null) { i++; continue; }
    let j = i;
    while (resolved[j].position == null) j++;
    const start = resolved[i - 1].position, end = resolved[j].position;
    const step = (end - start) / (j - i + 1);
    for (let t = i; t < j; t++) resolved[t].position = +(start + step * (t - i + 1)).toFixed(2);
    i = j;
  }
  // stop positions cannot decrease: raise anything that falls below the previous value
  for (let t = 1; t < resolved.length; t++) if (resolved[t].position < resolved[t-1].position) resolved[t].position = resolved[t-1].position;
  return resolved;
}

function midColor(stops, percent) {
  const resolved = resolveStops(stops);
  if (percent <= resolved[0].position) return resolved[0].color;
  if (percent >= resolved.at(-1).position) return resolved.at(-1).color;
  for (let i = 1; i < resolved.length; i++) {
    if (percent <= resolved[i].position) {
      const a = resolved[i-1], b = resolved[i];
      const t = b.position === a.position ? 0 : (percent - a.position) / (b.position - a.position);
      const ca = hexToRgb(a.color), cb = hexToRgb(b.color);
      return hex({ r: ca.r + (cb.r-ca.r)*t, g: ca.g + (cb.g-ca.g)*t, b: ca.b + (cb.b-ca.b)*t });
    }
  }
}

const A = [{ color: "#143a52" }, { color: "#8a9da9" }, { color: "#ffffff" }];
const B = [{ color: "#143a52", position: 0 }, { color: "#8a9da9", position: 20 }, { color: "#ffffff", position: 100 }];
const C = [{ color: "#143a52", position: 0 }, { color: "#143a52", position: 50 }, { color: "#ffffff", position: 50 }, { color: "#ffffff", position: 100 }];

for (const [label, stops] of [["equal distribution", A], ["manually positioned", B], ["hard transition (line)", C]]) {
  console.log(`--- ${label} ---`);
  console.log("resolved stops:", resolveStops(stops).map((x) => `${x.color}@${x.position}%`).join(" "));
  const samples = [0, 10, 20, 25, 49, 50, 51, 75, 100];
  console.log(samples.map((p) => `${String(p).padStart(3)}%:${midColor(stops, p)}`).join("  "));
  console.log("");
}

console.log("--- angle direction (linear-gradient) ---");
for (const [angle, direction] of [[0,"bottom to top"],[90,"left to right"],[180,"top to bottom"],[270,"right to left"]]) {
  const rad = (angle - 90) * Math.PI / 180;
  console.log(`${String(angle).padStart(3)}deg -> ${direction.padEnd(18)} unit vector (${Math.cos(rad).toFixed(3)}, ${(-Math.sin(rad)).toFixed(3)})`);
}
```

```
--- equal distribution ---
resolved stops: #143a52@0% #8a9da9@50% #ffffff@100%
  0%:#143a52   10%:#2c4e63   20%:#436275   25%:#4f6c7e   49%:#889ba7   50%:#8a9da9   51%:#8c9fab   75%:#c5ced4  100%:#ffffff

--- manually positioned ---
resolved stops: #143a52@0% #8a9da9@20% #ffffff@100%
  0%:#143a52   10%:#4f6c7e   20%:#8a9da9   25%:#91a3ae   49%:#b4c1c8   50%:#b6c2c9   51%:#b7c3ca   75%:#dae0e4  100%:#ffffff

--- hard transition (line) ---
resolved stops: #143a52@0% #143a52@50% #ffffff@50% #ffffff@100%
  0%:#143a52   10%:#143a52   20%:#143a52   25%:#143a52   49%:#143a52   50%:#143a52   51%:#ffffff   75%:#ffffff  100%:#ffffff

--- angle direction (linear-gradient) ---
  0deg -> bottom to top      unit vector (0.000, 1.000)
 90deg -> left to right      unit vector (1.000, 0.000)
180deg -> top to bottom      unit vector (0.000, -1.000)
270deg -> right to left      unit vector (-1.000, -0.000)
```

The first block shows the equal distribution: three stops got
written, and the middle one landed at 50%. The color at the `10%`
point, `#2c4e63`, is a one-fifth mix of the first two stops.

The second block uses the same three colors with different positions.
Once the middle stop gets pulled to 20%, the transition's **speed**
changes: the color lightens quickly in the first fifth, then slows in
the remainder. At the `10%` point, the color that was `#2c4e63` in
the first block is `#4f6c7e` here — much lighter.

#### Producing a Line With a Hard Transition

The third block shows a pattern that looks like a trick but is a
direct consequence of the definition. When two stops get written at
**the same position**, no distance remains between them to mix over,
and the gradient turns into an edge: still dark at 49%, fully white
at 51%.

This is the way to produce lines, stripes, and checkerboard patterns
with gradients:

```css
.measurement-table {
  background-image: linear-gradient(
    to right,
    var(--line) 0 1px,
    transparent 1px 100%
  );
  background-repeat: repeat-x;
  background-size: 120px 100%;
}
```

Writing two positions on one stop (`0 1px`) declares that the color
stays constant across that range; it is a shorthand for writing two
stops.

#### Three Gradient Types

**`linear-gradient()`** — colors change along a line. Direction gets
given with an angle or a keyword. The output's last block gives the
angle measurement: `0deg` points upward, and the angle increases
clockwise. The writing `to top` is the same as `0deg`, `to right` the
same as `90deg`. Positive y in the vectors points upward.

**`radial-gradient()`** — colors change outward from a center. Shape
(`circle` or `ellipse`), size (like `closest-side`,
`farthest-corner`), and the center can also be given.

**`conic-gradient()`** — colors change by angle around a center. This
is the type that pie charts and color wheels can get written with.

All three have `repeating-` prefixed forms; they run the stop
sequence to its end and restart it from the beginning.

#### Banding and the Mixing Space

In a gradient between two close colors over a large area, color steps
becoming visible is common. In eight-bit channels, intermediate
values get rounded, and over a large area, neighboring regions that
round to the same value look like a band.

Which color space the mix gets done in can get declared:

```css
.surface {
  background-image: linear-gradient(in oklab, #143a52, #8a5b00);
}
```

When the space does not get declared, the mix happens in the sRGB
space. A mix done in a perceptually uniform space reduces the
washing-out of the in-between tones between two colors — a visible
difference especially in transitions between complementary colors.

Whether this writing is recognized can get tested, and if it is not,
the declaration before it stays in place:

```css
.surface { background-image: linear-gradient(#143a52, #8a5b00); }

@supports (background-image: linear-gradient(in oklab, red, blue)) {
  .surface { background-image: linear-gradient(in oklab, #143a52, #8a5b00); }
}
```

#### Gradients Are an Image

The distinction from the start of this lesson has practical
consequences.

A gradient is a `background-image` layer; multiple gradients can get
stacked with a comma, and transparent regions show the layer below.

A gradient has no intrinsic size; when `background-size` does not get
given, it fills the area it gets placed in. When it does get given,
it can get tiled with `background-repeat` — the line example above
rests on this.

A gradient downloads no file. The same visual effect could also get
achieved with an image file; a gradient produces it without an extra
request, and rescales itself whenever the area changes.

```css
/* station.css — step 22: gradients */
.masthead {
  background-image:
    linear-gradient(to bottom, hsl(var(--hue) 61% 96%), hsl(var(--hue) 18% 97%));
}

.measurement-chart {
  background-image:
    linear-gradient(to top, var(--line) 0 1px, transparent 1px 100%);
  background-size: 100% 40px;
}

.loading-bar {
  background-image: linear-gradient(
    to right,
    var(--brand-dark) 0 var(--percent, 0%),
    var(--line) var(--percent, 0%) 100%
  );
}
```

The `.measurement-chart` class places a horizontal grid line every 40
units behind the measurement chart. The lines do not exist in the
document and should not: a grid line carries no information, it is a
visual aid that helps reading.

The `.loading-bar` class uses a custom property as the stop position.
When the `--percent` value changes, the bar's filled portion changes;
the gradient itself does not get rewritten. If the value comes from
the document, the fill ratio also has to get declared accessibly —
with a `progress` element, say, or the appropriate ARIA attributes.
The visual bar alone does not correspond to a state.

#### Summary

- A gradient is not a color, it is a background image; it gets
  written into `background-image`, has no intrinsic size, and gets
  sized by the area it gets placed in.
- Unwritten stop positions get filled in: the first stop at 0%, the
  last at 100%, and the ones in between get distributed at equal
  intervals between neighboring known points.
- Changing the stop positions changes the gradient's speed; when two
  stops get written at the same position, no distance remains to mix
  over, and a sharp edge forms.
- Writing two positions on one stop declares that the color stays
  constant across the range; line and stripe patterns get produced
  with this writing, without downloading a file.
- The color space the mix happens in can get declared; when it does
  not get declared, sRGB gets used, and the in-between tones between
  distant colors can wash out.

#### Next Step

The boxes' inside and back are complete. Their edges are missing: the
border's drawing style, rounding the corners, and shadows falling
outside the box have not been taken up yet. The next lesson combines
these three topics and shows how the corner radius gets calculated
with the border thickness.

### Border, Shadow, and Shape

Source: https://academia.sh/en/courses/css-fundamentals/border-shadow-and-shape

Border components, the corner radius's scaling rule, the inner-radius calculation, and the area shadow layers occupy.

The boxes' inside and back are complete. Their edges remained: how
the border gets drawn, how the corners get rounded, and how far
shadows falling outside the box extend.

These three subjects get taken up together, because they share the
same boundary: the border box. The border gets drawn on that
boundary, the corner radius rounds that boundary, the shadow falls
outside that boundary.

#### The Border's Three Components

`border` is a shorthand and sets three longhands: `-width`, `-style`,
`-color`.

When `border-style` does not get written, its initial value is
`none`, and **the border does not get drawn**. This is the most
frequent reason a border does not show up: the thickness and color
got written, but the style did not.

Style values include `solid`, `dashed`, `dotted`, `double`, `groove`, `ridge`, `inset`, `outset`, and
`hidden`. Of these, `groove`, `ridge`, `inset`, and `outset` give the border calculated light–dark tones
and produce an embossed effect; they do not get used in designed interfaces because color control falls
outside the scale. `hidden` hides the border entirely.

When `border-color` does not get written, `currentcolor` gets used —
that is, the element's text color. This behavior, mentioned in the
Color Representations lesson, means that keeping the border tied to
the text color is the default.

The four edges can get set separately and get written with logical
names: `border-block-end`, `border-inline-start`, and their
relatives.

#### The Corner Radius Has to Fit the Box

`border-radius` rounds the corners. The value is the radius of the
arc at the corner, and it can get given separately for each of the
four corners.

There is a constraint: the sum of the radii of the two corners on an
edge cannot exceed that edge's length. If it does, all the radii get
shrunk by **the same factor** — not just the overflowing corner, all
of them. This preserves the ratio between the corners.

```js
// border.mjs — corner-radius scaling, inner radius, and shadow box math
// rule: if the radius sum exceeds the edge, all radii shrink by the same factor f
function scaleRadius(box, radii) {
  const { width, height } = box;
  const factor = Math.min(
    width  / (radii.topLeft + radii.topRight),
    width  / (radii.bottomLeft + radii.bottomRight),
    height / (radii.topLeft + radii.bottomLeft),
    height / (radii.topRight + radii.bottomRight),
    1
  );
  return Object.fromEntries(Object.entries(radii).map(([k, v]) => [k, +(v * factor).toFixed(2)]));
}

const cases = [
  { box: { width: 200, height: 80 },  radii: { topLeft: 12, topRight: 12, bottomRight: 12, bottomLeft: 12 } },
  { box: { width: 200, height: 80 },  radii: { topLeft: 60, topRight: 60, bottomRight: 60, bottomLeft: 60 } },
  { box: { width: 100, height: 40 },  radii: { topLeft: 999, topRight: 999, bottomRight: 999, bottomLeft: 999 } },
];
for (const c of cases) {
  const scaled = scaleRadius(c.box, c.radii);
  console.log(`box ${c.box.width}x${c.box.height}, requested ${c.radii.topLeft} -> applied ${scaled.topLeft}`);
}

console.log("\n--- inner radius: smaller by the border thickness ---");
for (const [outer, thickness] of [[12, 1], [12, 4], [12, 16], [8, 8]]) {
  const inner = Math.max(0, outer - thickness);
  console.log(`outer radius=${String(outer).padStart(2)} border=${String(thickness).padStart(2)} -> inner radius=${inner}${inner === 0 ? "  (square inner edge)" : ""}`);
}

console.log("\n--- box-shadow: the rectangle the shadow occupies ---");
function shadowBox({ width, height }, { x, y, blur, spread }) {
  // shadow box: grows by spread, shifts by the offset, extends by half the blur
  const bleed = blur / 2;
  return {
    left:   x - spread - bleed,
    top:    y - spread - bleed,
    right:  width + x + spread + bleed,
    bottom: height + y + spread + bleed,
  };
}
const box = { width: 200, height: 80 };
for (const shadow of [
  { x: 0, y: 2,  blur: 4,  spread: 0 },
  { x: 0, y: 8,  blur: 24, spread: -4 },
  { x: 0, y: 0,  blur: 0,  spread: 3 },
]) {
  const rect = shadowBox(box, shadow);
  console.log(`box-shadow: ${shadow.x}px ${shadow.y}px ${shadow.blur}px ${shadow.spread}px -> shadow (${rect.left}, ${rect.top}) .. (${rect.right}, ${rect.bottom})`);
}
console.log(`the box itself: (0, 0) .. (${box.width}, ${box.height})`);
```

```
box 200x80, requested 12 -> applied 12
box 200x80, requested 60 -> applied 40
box 100x40, requested 999 -> applied 20

--- inner radius: smaller by the border thickness ---
outer radius=12 border= 1 -> inner radius=11
outer radius=12 border= 4 -> inner radius=8
outer radius=12 border=16 -> inner radius=0  (square inner edge)
outer radius= 8 border= 8 -> inner radius=0  (square inner edge)

--- box-shadow: the rectangle the shadow occupies ---
box-shadow: 0px 2px 4px 0px -> shadow (-2, 0) .. (202, 84)
box-shadow: 0px 8px 24px -4px -> shadow (-8, 0) .. (208, 96)
box-shadow: 0px 0px 0px 3px -> shadow (-3, -3) .. (203, 83)
the box itself: (0, 0) .. (200, 80)
```

The first block gives three cases. The 12-unit radius fit the 200×80
box and got applied as is. The 60-unit radius did not fit the
vertical edge — the two corners sum to 120, the edge is 80 — and all
of them got reduced to 40.

The third row explains a pattern. The writing `border-radius: 999px`
is the standard way to give a box fully rounded ends: the value
always gets chosen large enough to drop to half the box's size, and
whatever the box's dimensions are, the result is a pill shape. In the
example, 999 got requested, 20 got applied — half of the 40-unit
height.

#### Inner Radius

The second block explains why nested rounded corners sometimes look
wrong.

The radius at the border's **inner** edge is smaller than the outer
radius by the border's thickness. If the thickness is larger than the
radius, the inner radius gets zeroed out and the inner edge becomes
square — a border that is rounded outside and square inside results.

The same calculation applies to nested boxes too. If a second rounded
box gets placed inside a card, sitting inward by the padding amount,
the inner box's radius has to be smaller than the outer radius by the
padding; otherwise the two arcs do not stay parallel:

```css
.card       { border-radius: 12px; padding: 8px; }
.card .area { border-radius: 4px; }   /* 12 - 8 */
```

#### Shadow Layers

`box-shadow` takes four lengths: horizontal offset, vertical offset,
blur radius, and spread. The last two are optional.

The third block calculates the rectangle a shadow occupies. A shadow
with blur radius $b$ extends roughly $b/2$ beyond the box's edge; the
spread value, though, directly grows or shrinks the shadow rectangle.

The negative spread in the second row is a pattern: when a soft
shadow is wanted with a wide blur but the shadow should not spill
from the box's sides, the spread gets given negative. The result is
the shadow falling mostly below.

The third row shows a shadow with no blur and no offset: spread
alone. This draws a sharp ring around the box and behaves like a
border that takes up no space. Its difference from `outline` is that
the shadow follows the corner radius.

Multiple shadows can get written separated by commas; the first one
written gets drawn on top. Layered shadows give a softer sense of
depth than a single shadow.

The `inset` keyword turns the shadow inward, into the box.

There is a distinction between `box-shadow` and
`filter: drop-shadow()`: the first follows the border box's rectangle
(and its radius), the second follows the element's **visible shape**
and skips transparent regions. For an icon with transparency, the
second is the right tool.

#### Giving Shape

`clip-path` restricts the box's drawn region to a geometric shape:

```css
.icon { clip-path: circle(50%); }
.tag { clip-path: polygon(0 0, 100% 0, calc(100% - 12px) 100%, 0 100%); }
```

Clipping does not affect layout: the box still takes up space at its
rectangular size, only its drawing gets clipped. To change the
boundary of **text flowing around** a floated box, `shape-outside` is
needed; when the two properties get written together, the shape and
the flow boundary coincide.

Clipping cuts off everything that falls outside the box — the focus
ring included. A clipped control's focus indicator can stay
invisible; in that case, the indicator has to get brought inside the
box.

```css
/* station.css — step 23: border, corner, and shadow */
.card {
  border: 1px solid var(--line);
  border-radius: 12px;
  padding: 16px;
  box-shadow:
    0 1px 2px rgb(28 39 51 / 0.06),
    0 8px 24px -4px rgb(28 39 51 / 0.08);
}

.card .area { border-radius: 4px; }

.status-badge {
  border-radius: 999px;
  border: 1px solid currentcolor;
  padding-block: 2px;
  padding-inline: 8px;
}

.measurement-table { border-collapse: collapse; }

a:focus-visible {
  outline: 3px solid var(--brand-dark);
  outline-offset: 2px;
  border-radius: 2px;
}
```

The card has two shadow layers: the close, sharp one defines the
edge, the far, soft one gives depth. In the second, negative spread
limits the shadow spilling to the sides.

The badge's border color is `currentcolor` — whatever the text color
is, the border becomes that too. When a warning badge carries red
text, its border also becomes red; no separate rule gets written.

The `border-collapse: collapse` declaration merges table cells'
adjacent borders into a single line. This mode also prevents a radius
from getting applied to the table's corners; if a rounded-corner
table is wanted, the table has to get wrapped in a container and the
radius has to get given there.

#### Summary

- If `border-style` does not get written, the border does not get
  drawn; this is the most frequent reason a border with thickness and
  color written does not show up.
- If the sum of an edge's two corner radii exceeds the edge, all the
  radii get shrunk by the same factor; the writing
  `border-radius: 999px` produces a pill shape by relying on this
  rule.
- The inner corner radius is smaller than the outer radius by the
  border's thickness, and once the difference drops below zero the
  inner edge becomes square; in nested boxes, the radius gets reduced
  by the padding.
- A `box-shadow` shadow extends by half the blur, the spread value
  directly grows or shrinks the shadow rectangle; negative spread
  limits the overflow.
- `clip-path` only clips the drawing, it does not change layout or
  the space taken up; because the focus indicator can also get
  clipped, the indicator has to get brought inside the box.

#### Next Step

Throughout this topic, lengths always got written as pixels and
`rem`, and the `ch` unit got left undefined. In how many separate
forms can a length get written, and why does the difference between
`em` and `rem` produce a chained calculation? The next lesson defines
CSS units and shows the conversions with calculation.

### CSS Units

Source: https://academia.sh/en/courses/css-fundamentals/css-units

Absolute, font-relative, and viewport-relative units; the em chain's calculation, rem's stability, and what a percentage resolves against.

Throughout this topic, lengths got written as pixels and `rem`, and
the `ch` unit got left undefined. A length can get written in several
separate forms, and the choice is not just a number format: some
units track user settings, some do not.

This lesson separates units into three families and shows the
conversions between them with calculation.

#### Three Families

**Absolute units** — `px`, `cm`, `mm`, `in`, `pt`, `pc`, `q`. The
ratios between them are fixed, and `1in = 96px` gets taken by
definition. `px` does not correspond to a device pixel; it is a
reference unit and stays close to the same physical size regardless
of the device pixel ratio.

**Font-relative units** — `em`, `rem`, `ch`, `ex`, `cap`, `lh`, `rlh`.
Their value depends on the font metrics in the context they get
calculated in.

**Viewport-relative units** — `vw`, `vh`, `vmin`, `vmax`, and their
dynamic forms. They are defined as one percent of the viewport.

```js
// units.mjs — the em chain, rem stability, and percentage references
console.log("--- em chain: multiplied again at every generation ---");
const chain = [
  { name: "html",    fontSize: "16px" },
  { name: "body",    fontSize: "1em"  },
  { name: "section", fontSize: "1.25em" },
  { name: "div",     fontSize: "1.25em" },
  { name: "p",       fontSize: "1.25em" },
  { name: "span",    fontSize: "1.25em" },
];
let parentPx = 16;
for (const node of chain) {
  let px;
  if (node.fontSize.endsWith("px")) px = parseFloat(node.fontSize);
  else px = parentPx * parseFloat(node.fontSize);
  console.log(`${node.name.padEnd(8)} font-size: ${node.fontSize.padEnd(7)} -> ${px.toFixed(3)}px  (parent ${parentPx.toFixed(3)}px)`);
  parentPx = px;
}

console.log("\n--- the same chain with rem ---");
const ROOT = 16;
parentPx = 16;
for (const node of chain) {
  const px = node.fontSize.endsWith("px") ? parseFloat(node.fontSize) : ROOT * parseFloat(node.fontSize);
  console.log(`${node.name.padEnd(8)} font-size: ${node.fontSize.replace("em","rem").padEnd(8)} -> ${px.toFixed(3)}px`);
}

console.log("\n--- em on the same element: font-size uses the outer value, padding the inner ---");
const parentFont = 16, ownFont = 1.5 * parentFont;
console.log(`parent font-size = ${parentFont}px`);
console.log(`font-size: 1.5em -> ${ownFont}px   (via the parent's value)`);
console.log(`padding:   1.5em -> ${1.5 * ownFont}px   (via its own font-size)`);
console.log(`line-height: 1.5em -> ${1.5 * ownFont}px`);

console.log("\n--- what a percentage resolves against ---");
const CONTAINING = { width: 640, height: 400, fontSize: 16 };
const REFERENCE = {
  "width":       ["containing width", CONTAINING.width],
  "padding-top": ["containing WIDTH", CONTAINING.width],
  "margin-left": ["containing width", CONTAINING.width],
  "height":      ["containing height", CONTAINING.height],
  "font-size":   ["parent font-size", CONTAINING.fontSize],
  "line-height": ["own font-size", 16],
};
for (const [prop, [description, base]] of Object.entries(REFERENCE)) {
  console.log(`${prop.padEnd(12)} %50 -> ${(base * 0.5).toFixed(1)}px   (reference: ${description} = ${base})`);
}

console.log("\n--- absolute unit conversions (1in = 96px) ---");
const PX = { px: 1, in: 96, cm: 96/2.54, mm: 96/25.4, q: 96/101.6, pt: 96/72, pc: 16 };
for (const [unit, value] of Object.entries(PX)) console.log(`1${unit.padEnd(3)} = ${value.toFixed(4)}px`);

console.log("\n--- ch and ex (in an example font) ---");
const size = 16, chRatio = 0.5, exRatio = 0.52;
console.log(`font-size=${size}px, 1ch=${size*chRatio}px, 1ex=${(size*exRatio).toFixed(2)}px`);
console.log(`max-width: 70ch -> ${70 * size * chRatio}px`);
```

```
--- em chain: multiplied again at every generation ---
html     font-size: 16px    -> 16.000px  (parent 16.000px)
body     font-size: 1em     -> 16.000px  (parent 16.000px)
section  font-size: 1.25em  -> 20.000px  (parent 16.000px)
div      font-size: 1.25em  -> 25.000px  (parent 20.000px)
p        font-size: 1.25em  -> 31.250px  (parent 25.000px)
span     font-size: 1.25em  -> 39.063px  (parent 31.250px)

--- the same chain with rem ---
html     font-size: 16px     -> 16.000px
body     font-size: 1rem     -> 16.000px
section  font-size: 1.25rem  -> 20.000px
div      font-size: 1.25rem  -> 20.000px
p        font-size: 1.25rem  -> 20.000px
span     font-size: 1.25rem  -> 20.000px

--- em on the same element: font-size uses the outer value, padding the inner ---
parent font-size = 16px
font-size: 1.5em -> 24px   (via the parent's value)
padding:   1.5em -> 36px   (via its own font-size)
line-height: 1.5em -> 36px

--- what a percentage resolves against ---
width        %50 -> 320.0px   (reference: containing width = 640)
padding-top  %50 -> 320.0px   (reference: containing WIDTH = 640)
margin-left  %50 -> 320.0px   (reference: containing width = 640)
height       %50 -> 200.0px   (reference: containing height = 400)
font-size    %50 -> 8.0px   (reference: parent font-size = 16)
line-height  %50 -> 8.0px   (reference: own font-size = 16)

--- absolute unit conversions (1in = 96px) ---
1px  = 1.0000px
1in  = 96.0000px
1cm  = 37.7953px
1mm  = 3.7795px
1q   = 0.9449px
1pt  = 1.3333px
1pc  = 16.0000px

--- ch and ex (in an example font) ---
font-size=16px, 1ch=8px, 1ex=8.32px
max-width: 70ch -> 560px
```

#### The em Chain

The first two blocks show the difference at the center of this
lesson. On the same tree, a `font-size: 1.25` declaration got written
at every generation. Written with `em`, the value got multiplied **on
top of the previous one** at every generation, and passed 39 pixels
by the fifth generation. Written with `rem`, every generation used
the root element's size, and the value stayed fixed at 20 pixels.

`em` — depends on the element's **own computed** `font-size` value.
When used in the `font-size` property itself, the **parent's** value
gets used, because the element's own value has not been computed yet.
The chained growth comes from this.

`rem` — always depends on the **root element's** `font-size` value.
There is no chain.

The third block shows the distinction within the same element.
`font-size: 1.5em` gave 24 pixels over the parent's 16 pixels. On the
same element, `padding: 1.5em` gave 36 pixels over the element's
**own** 24 pixels. The two declarations carry the same number but
resolve against different bases.

This behavior of the `em` unit is not a flaw, it is the tool for
component scaling: when a button's padding gets written in `em`, the
padding scales along with it once the button's font size changes.
Written in `rem`, it would stay fixed.

#### A Percentage's Reference Varies by Property

The fourth block breaks a common assumption. A percentage does not
always resolve against the same dimension.

`width`, `margin`, and — note — every `padding` value, **including
the vertical padding**, resolves against the containing block's
**inline dimension**. The writing `padding-top: 50%` gives half the
containing block's width, not its height.

This rule, which looks strange, is the basis for a pattern: giving a
box an aspect ratio. Because `padding-top` depends on width, a box
whose height is proportional to its width can get built this way. The
`aspect-ratio` property does the same job directly, and is the
preferred way today.

`height` resolves against the containing block's height, `font-size`
against the parent's font size, `line-height` against the element's
**own** font size.

#### The Cost of Writing Pixels

The fifth block gives the absolute units' fixed ratios. These ratios
are calculable, but the real dimension of the decision lies
elsewhere.

A user can change the default font size in their browser. This
setting affects the root element's `font-size` value. The consequence
is this:

- Dimensions written with `rem` and `em` grow **together** with this
  setting.
- Dimensions written with `px` **do not change**.

A page with font size written in pixels ignores the user's reading
setting. Zooming still works, but zooming enlarges the whole page —
what the user wants is usually just the text getting bigger.

The applicable rule: **font size, spacing, and box dimensions get
written in `rem` or `em`.** `px` gets used only for dimensions that
genuinely need to stay fixed: thin borders, one-pixel dividers,
shadow offsets.

Writing a fixed pixel value into the root element's `font-size`
brings the same problem back; this value either does not get written
at all, or gets written with a percentage.

#### Other Font-Dependent Units

The last block defines the `ch` unit: the width of the "0" character
in a font. The `max-width: 70ch` declaration written in the Box
Sizing lesson finds its counterpart here — 560 pixels at the
example's ratio.

`ex` corresponds to lowercase letter height, `cap` to uppercase
letter height, `lh` to the element's line height. Their value depends
on the chosen font; the 0.5 and 0.52 ratios in the example are sample
values.

The `lh` unit is useful for building a spacing scale: writing
`margin-block-end: 1lh` makes the gap exactly one line height, and
the text's baseline rhythm gets preserved.

#### Viewport Units

`1vw` is one percent of the viewport's width, `1vh` one percent of
its height. `vmin` and `vmax` use the smaller and the larger of the
two.

There is a distinction in the height units. In some environments, the
browser interface hides and shows on scroll, and the viewport's
height changes. Three families get defined for this: `svh` is the
smallest, `lvh` the largest, `dvh` uses the current height. Writing
`100vh` can produce a box that overflows while the interface is
visible; `100svh` prevents this.

Writing font size directly in `vw` is problematic: when the user
zooms in, the viewport width gets measured with the zoom applied, so
the text does not grow. For scaling typography, the viewport unit
does not get used alone, but together with a lower bound — the tool
for this gets taken up in the next lesson.

```css
/* station.css — step 24: unit choice */
:root { font-size: 100%; }          /* preserve the user's setting */

main { max-width: 70ch; padding-inline: 1rem; }

h1 { font-size: 2.4414rem; margin-block-end: 0.5em; }

.status-badge { padding-block: 0.25em; padding-inline: 0.75em; }

.measurement-table th,
.measurement-table td {
  padding-block: 0.5rem;
  padding-inline: 0.75rem;
  border-block-end: 1px solid var(--line);
}

.card { box-shadow: 0 1px 2px rgb(28 39 51 / 0.06); }
```

The badge's padding got written in `em`: when the badge gets used at
a small font size, the padding shrinks too. The cell padding got
written in `rem`: the table's rhythm has to stay independent of the
cell content's size.

The heading's bottom margin got written in `em` and got tied to the
heading's own size; large headings get large spacing.

The border and shadow offset stayed in `px`: these are dimensions
that blur when scaled and are meant to stay fixed.

#### Summary

- `em` depends on the element's own computed font size; when used in
  the `font-size` property, it resolves against the parent's value
  and grows in a chain by getting multiplied again at every
  generation.
- `rem` always depends on the root element's font size; there is no
  chain, and the value is unaffected by the tree's depth.
- A percentage's reference varies by property: `width` and **every**
  `padding` value resolve against the containing block's inline
  dimension, `height` against the containing block's block dimension,
  `line-height` against the element's own font size.
- Dimensions written in `px` do not grow with the user's font-size
  setting; font, spacing, and box dimensions get written in `rem` or
  `em`, `px` gets reserved for fine details that need to stay fixed.
- Viewport height units have three families; `svh` is the smallest,
  `lvh` the largest, `dvh` uses the current height.

#### Next Step

Units are defined, but individual fixed values still get written one
by one. A dimension may need to stay between two values, the smaller
of two dimensions may need to get chosen, or one value may need to get
subtracted from another. The next lesson — the course's last lesson —
defines the functions that do these calculations and wraps up the
style sheet the course built.

### CSS Functions

Source: https://academia.sh/en/courses/css-fundamentals/css-functions

Mixing units with calc, setting bounds with min and max, clamping a range with clamp, and combining these functions to produce a scale.

Units are defined, but individual fixed values still get written one
by one. The style sheet has dimensions calculated by hand and written
in: `max-width: 70ch`, `padding-inline: 1rem`, `font-size: 2.4414rem`.
Some of these values depend on another, and the link does not show up
in the file.

This lesson defines the functions that carry the calculation into the
style sheet itself. It is the course's last lesson; at the end, the
style sheet built throughout the course gets wrapped up.

#### Four Functions

`calc()` — calculates a length with the four operations. Different
units can get mixed; resolution happens at the stage where the
values are known.

`min()` and `max()` — pick the smallest or largest of comma-separated
values. They can take more than two values.

`clamp()` — takes three values: minimum, preferred, maximum. It
clamps the preferred value between the two bounds.

```js
// functions.mjs — resolving calc, min, max, and clamp values
const px = (v) => +v.toFixed(2);

// clamp(lo, pref, hi) = max(lo, min(pref, hi))
const clamp = (lo, pref, hi) => Math.max(lo, Math.min(pref, hi));

console.log("--- clamp: font size by viewport width ---");
console.log("font-size: clamp(1rem, 0.75rem + 1.2vw, 1.5rem)   (root 16px)");
for (const vw of [320, 480, 640, 800, 1024, 1440]) {
  const preferred = 0.75 * 16 + 1.2 * vw / 100;
  const result = clamp(1 * 16, preferred, 1.5 * 16);
  const which = result === 16 ? "lower bound" : result === 24 ? "upper bound" : "preferred";
  console.log(`viewport ${String(vw).padStart(4)}px -> preferred ${px(preferred)}px, applied ${px(result)}px  (${which})`);
}

console.log("\n--- calc: mixing different units ---");
const CONTAINING = 640, ROOT = 16;
const expressions = [
  ["100% - 2rem",      CONTAINING - 2 * ROOT],
  ["100% / 3",         CONTAINING / 3],
  ["100% / 3 - 16px",  CONTAINING / 3 - 16],
  ["50% + 20px",       CONTAINING * 0.5 + 20],
  ["2rem * 1.5",       2 * ROOT * 1.5],
];
for (const [expr, result] of expressions) {
  console.log(`calc(${expr})`.padEnd(24) + `-> ${px(result)}px   (containing ${CONTAINING}px, root ${ROOT}px)`);
}

console.log("\n--- min and max: which one sets the bound ---");
for (const containing of [320, 640, 1200]) {
  const minResult = Math.min(containing * 0.9, 600);
  const maxResult = Math.max(containing * 0.9, 600);
  console.log(`containing ${String(containing).padStart(4)}px -> min(90%, 600px)=${px(minResult)}  max(90%, 600px)=${px(maxResult)}`);
}

console.log("\n--- clamp's equivalence with min/max ---");
for (const pref of [10, 16, 20, 24, 30]) {
  const a = clamp(16, pref, 24);
  const b = Math.max(16, Math.min(pref, 24));
  console.log(`preferred=${String(pref).padStart(2)} -> clamp=${a}  max(16,min(pref,24))=${b}  equal=${a === b}`);
}

console.log("\n--- nested: a spacing scale ---");
const BASE = 16;
for (const n of [0, 1, 2, 3, 4]) {
  const value = BASE * Math.pow(1.5, n);
  console.log(`--space-${n}: ${px(value / ROOT)}rem = ${px(value)}px`);
}
```

```
--- clamp: font size by viewport width ---
font-size: clamp(1rem, 0.75rem + 1.2vw, 1.5rem)   (root 16px)
viewport  320px -> preferred 15.84px, applied 16px  (lower bound)
viewport  480px -> preferred 17.76px, applied 17.76px  (preferred)
viewport  640px -> preferred 19.68px, applied 19.68px  (preferred)
viewport  800px -> preferred 21.6px, applied 21.6px  (preferred)
viewport 1024px -> preferred 24.29px, applied 24px  (upper bound)
viewport 1440px -> preferred 29.28px, applied 24px  (upper bound)

--- calc: mixing different units ---
calc(100% - 2rem)       -> 608px   (containing 640px, root 16px)
calc(100% / 3)          -> 213.33px   (containing 640px, root 16px)
calc(100% / 3 - 16px)   -> 197.33px   (containing 640px, root 16px)
calc(50% + 20px)        -> 340px   (containing 640px, root 16px)
calc(2rem * 1.5)        -> 48px   (containing 640px, root 16px)

--- min and max: which one sets the bound ---
containing  320px -> min(90%, 600px)=288  max(90%, 600px)=600
containing  640px -> min(90%, 600px)=576  max(90%, 600px)=600
containing 1200px -> min(90%, 600px)=600  max(90%, 600px)=1080

--- clamp's equivalence with min/max ---
preferred=10 -> clamp=16  max(16,min(pref,24))=16  equal=true
preferred=16 -> clamp=16  max(16,min(pref,24))=16  equal=true
preferred=20 -> clamp=20  max(16,min(pref,24))=20  equal=true
preferred=24 -> clamp=24  max(16,min(pref,24))=24  equal=true
preferred=30 -> clamp=24  max(16,min(pref,24))=24  equal=true

--- nested: a spacing scale ---
--space-0: 1rem = 16px
--space-1: 1.5rem = 24px
--space-2: 2.25rem = 36px
--space-3: 3.38rem = 54px
--space-4: 5.06rem = 81px
```

#### clamp Is a Three-Zone Function

The first block shows the `clamp()` function's three zones. At a
320-unit viewport, the preferred value calculated to 15.84 pixels,
but 16 pixels got applied because it fell below the lower bound. From
480 to 800, the preferred value stayed within the range and got used
directly. Past 1024, it hit the upper bound.

The fourth block confirms the equivalence: `clamp(a, t, b)` always
gives the same result as `max(a, min(t, b))`. `clamp()` is not a
separate mechanism, it is the name for this composition.

The writing `0.75rem + 1.2vw` in the preferred value is a pattern:
the sum of a fixed base and a component proportional to the viewport.
If only `vw` got written, with no fixed component, the text would not
grow when the user zoomed in — the problem mentioned in the previous
lesson. The fixed component preserves that link.

#### calc Mixes Units

The second block shows `calc()`'s real job: percentage, `rem`, and
`px` can get mixed in the same expression. This establishes
relationships that could not get written any other way.

`calc(100% - 2rem)` — fill the containing block but leave two base
units of space. `calc(100% / 3 - 16px)` — take a third of the width
and subtract the gap between them.

There is one detail in the writing rules: **a space is required on
both sides** of the `+` and `-` operators. The writing
`calc(100%-2rem)` is invalid, because `-2rem` gets read as a negative
number. A space is not required for `*` and `/`, but gets written for
consistency.

A second constraint: in multiplication, at least one operand has to
be a unitless number, and in division, the divisor has to be. Two
lengths cannot get multiplied.

`calc()` can get written nested, and can also contain `var()` calls.
Used together with custom properties, it lets a scale get derived from
a single base — the fifth block gives the result of this.

#### min and max Set Bounds

The third block corrects a reading habit. `min()` sets an **upper
bound**: the smallest of the given values gets chosen, so the result
cannot exceed any of them. `max()` sets a **lower bound**.

The names looking like the opposite of the function is a frequently
confused point. In the output, the writing `min(90%, 600px)` stopped
at 600 once the containing block was 1200 units — that is, it acted
as an upper bound.

These two functions have an advantage: they set a bound within the
same declaration, without writing separate properties like
`min-width` and `max-width`. They can also get used inside shorthand
values — `padding: min(5%, 2rem)` can get written, whereas there is
no `max-` property for `padding`.

#### The Course's Style Sheet

Throughout the course, the station page's style sheet grew step by
step. Its wrapped-up form carries every decision built in this
course:

```css
/* station.css — wrapped up */

/* 1. Base */
*, *::before, *::after { box-sizing: border-box; }
:root { font-size: 100%; }

/* 2. Scales */
:root {
  --hue: 203;
  --brand-dark: hsl(var(--hue) 61% 20%);
  --surface:    hsl(var(--hue) 18% 97%);
  --line:       hsl(207 15% 86%);
  --text:       hsl(211 29% 16%);
  --text-muted: hsl(211 12% 45%);
  --warning:    hsl(0 66% 33%);

  --space-0: 1rem;
  --space-1: calc(var(--space-0) * 1.5);
  --space-2: calc(var(--space-1) * 1.5);
  --space-3: calc(var(--space-2) * 1.5);

  --font-body: clamp(1rem, 0.9rem + 0.35vw, 1.125rem);
  --font-h1:   clamp(1.75rem, 1.3rem + 2.2vw, 2.5rem);
}

/* 3. Document */
body {
  color: var(--text);
  font-family: "Station Sans", system-ui, sans-serif;
  font-size: var(--font-body);
  line-height: 1.5;
}

main {
  max-width: min(70ch, 100% - var(--space-1));
  margin-inline: auto;
  padding-block: var(--space-2);
}

main > * + *       { margin-block-start: var(--space-0); }
main > section     { margin-block-start: var(--space-3); }

/* 4. Masthead */
.masthead {
  padding: var(--space-1);
  margin-block-end: var(--space-2);
  background-color: var(--surface);
  border-block-end: 4px solid var(--brand-dark);
}
.masthead > :first-child { margin-block-start: 0; }
h1 { font-size: var(--font-h1); line-height: 1.15; }

/* 5. Measurement table */
.table-wrapper { overflow-x: auto; }
.measurement-table { width: 100%; min-width: 40rem; border-collapse: collapse; }
.measurement-table th,
.measurement-table td {
  padding-block: 0.5rem;
  padding-inline: 0.75rem;
  border-block-end: 1px solid var(--line);
}
.measurement-table thead th {
  position: sticky;
  z-index: 10;
  inset-block-start: 0;
  background-color: var(--surface);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
:where(.measurement-table) .value {
  text-align: end;
  font-variant-numeric: tabular-nums;
}
:where(.measurement-table) .missing { color: var(--warning); }
.measurement-table tbody tr:nth-child(odd) { background-color: var(--surface); }

/* 6. Location section */
.location-section { display: flow-root; }
.location-image {
  float: inline-start;
  inline-size: min(200px, 40%);
  margin-inline-end: var(--space-0);
  margin-block-end: 0.5rem;
}
.location-image img { max-width: 100%; height: auto; }
.source-note { clear: both; color: var(--text-muted); font-size: 0.875em; }

/* 7. States */
a:focus-visible,
input:focus-visible {
  outline: 3px solid var(--brand-dark);
  outline-offset: 2px;
}
input, select, textarea, button { font: inherit; color: inherit; }
input[required] { border-inline-start: 3px solid var(--warning); }
input:invalid   { border-color: var(--warning); }

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
```

The file is divided into seven sections, and the order is not
random: base rules at the top, with the lowest specificity; component
rules at the bottom. The order criterion established in the Cascade
lesson makes this arrangement predictable — a conflict at the same
specificity always resolves in favor of whichever gets written later.

The scales are defined in one place and derived from each other. The
`--space-1` value depends on `--space-0`; when the base changes, all
the spacing values change together.

The `main` element's width is the smaller of two constraints: a
readable line length and the space to leave at the screen's edge.
This is the combination of the Box Sizing, Units, and Functions
lessons.

#### Summary

- `calc()` mixes different units in the same expression; a space is
  required on both sides of the addition and subtraction operators,
  and at least one operand in a multiplication has to be unitless.
- `min()` sets an upper bound, `max()` sets a lower bound; even
  though the names look like the reverse of the functions, the chosen
  value is the smallest and the largest, respectively.
- `clamp(a, t, b)` is the name for the composition `max(a, min(t,
  b))` and behaves in three zones: the preferred value gets used if
  it is within range, and gets clamped to the relevant bound if it is
  outside.
- In scaling typography, the preferred value gets written as the sum
  of a fixed base and a component proportional to the viewport;
  writing only a viewport unit breaks the zoom link.
- When functions get combined with custom properties, a scale gets
  derived from a single base, and the entire scale can get shifted by
  changing a single value.

#### Course Wrap-Up

This course styled the measurement station document built in the Web
Fundamentals and HTML curriculum, and did it in three layers.

The first topic established **how rules reach the document**: a
declaration's syntax, the set a selector returns, resolving conflicts
through the cascade and specificity, and filling a value in by
inheritance or its initial value. This topic's distinguishing feature
was that every question asked had a calculable answer: specificity is
a triple, the cascade is an ordering, inheritance is a tree walk.

The second topic established **how boxes get measured and arranged**:
the box model's arithmetic, the numeric difference between the two
sizing models, normal flow's rules, margin collapsing, floating,
positioning, and stacking context. The recurring lesson here was
this: a layout problem first gets attempted with the rules of the
flow; every declaration that pulls an element out of the flow comes
with a cost.

The third topic **tied visual decisions to a scale**: color spaces
and the contrast calculation, the modular typography scale,
font-loading behavior, background layers, gradients, borders, units,
and calculation functions. The principle here was building derived
scales instead of individual values.

One thing the course did not cover remains: **two-dimensional
layout**. In this course, boxes either stacked in the flow or got
pushed aside by floating. Building columns, placing boxes on a grid,
giving them gaps without collapsing, and adapting all of this to
screen width is the job of a separate model.

The Layout Systems and Responsive Design curriculum builds this
model. Two-dimensional placement with flexible box and grid layouts,
responsive design with breakpoints derived from content, and
organizing style architecture in a scalable way get taken up there.
Everything learned in this course holds there too: the specificity
calculation is the same calculation, the box model is the same model,
the scales are the same scales. The only thing that changes is how
the boxes get positioned relative to each other.

