CSE 134B Web Client Languages
  1. Presentation
  2. CSS
  3. Values
  4. Values, Units and Functions

Presentation · CSS 04

Values, Units and Functions

Every declaration assigns a value to a property. Those values are typed, they carry units that resolve against the reader's context, and they pass through a pipeline before anything is painted. Knowing how values work is what turns CSS from guesswork into something you can reason about.

The anatomy of a rule showed where a value sits: after the colon, before the semicolon. This page looks inside that slot. It covers the types CSS checks, the units that make a design scale, the stages a value passes through, the functions that compute values, and the custom properties that let you name them. It closes with a map of the at-rules and the page that teaches each one.

1. CSS has a type system

Every value in CSS has a type, and every property accepts a specific set of types. The specifications define each property with a formal value grammar, and the browser checks what you write against it. Here is the definition of width from the CSS Sizing specification:

width = auto | <length-percentage [0,∞]> | min-content | max-content | fit-content( <length-percentage [0,∞]> ) | stretch

Read it like a function signature. A vertical bar means "or". Angle brackets name a type. The range in square brackets is a constraint: a width cannot be negative. Parentheses mark a function. So width takes the keyword auto, a non-negative length or percentage, one of three sizing keywords, or a call to fit-content(). The MDN page for every property opens with the same grammar, under "Formal syntax".

The types you will use most

TypeExample valuesUsed for
<length>1rem, 2ch, 50dvh, 4cqiSizes, distances, spacing
<percentage>50%Sizes relative to something the property defines
<number>1.5, 0.75Ratios, line-height, opacity
<integer>3, -1Grid lines, z-index, counters
<color>crimson, #c41e3a, oklch(55% 0.2 25)Text, backgrounds, borders
<image>url(hero.avif), linear-gradient(…)Backgrounds, list markers, masks
<angle>45deg, 0.25turnRotations, gradient directions, hues
<time>200ms, 0.4sTransition and animation durations
<string>"Open Sans", "→"Font names with spaces, generated content
<custom-ident>sidebar, fade-inNames you choose: grid areas, animations, layers

The specifications define dozens of types, but these cover most daily work. Two of them deserve a closer look.

Lengths are not numbers. 16 and 16px are values of different types, and the unit is part of the value. width: 16 is invalid. The single exception is zero: a length of 0 may drop its unit, because zero of anything is zero.

Numbers are not lengths either. line-height accepts both, and they behave differently: line-height: 1.5 means "one and a half times the element's own font size", while line-height: 1.5em becomes a fixed length. Section 3 shows why that difference matters for every descendant.

Type errors are silent

Write width: red and the browser parses red, finds a <color>, sees that width does not accept colors, and drops the declaration. Nothing appears in the console. This is the forgiving parsing that lets old browsers skip new syntax, seen from the other side: you will not be told about a type error, you will simply be ignored. DevTools is where you find out, since the Styles panel strikes through any declaration it rejected.

2. Units: absolute, relative, viewport and container

A length is a number with a unit, and the unit decides what the number is measured against. CSS has a great many units. The course deck called it the "unit explosion", and the list keeps growing. They fall into a few families, and the family matters far more than the individual unit.

What a CSS pixel really is

The px is not a dot on your screen. CSS defines a reference pixel: the size one pixel appears to be on a display of about 96 dots per inch, held at arm's length. On a phone with a high-density display, one CSS pixel is drawn with two or three device pixels in each direction. On a projector across a lecture hall, one CSS pixel may be a few millimeters wide. The browser reports the ratio as window.devicePixelRatio.

The other absolute units are defined from the pixel: 1in is 96px, 1pt is 1/72 of an inch, 1cm is about 37.8px. That means an "inch" on screen is only an inch by coincidence. Physical units make sense for print, where the page really is measured in centimeters or points, and almost nowhere else.

So the pixel is already an abstraction. Its real limitation is what it ignores. A length in px does not respond to the reader's preferred font size, does not know how wide the column is, and does not know how large the screen is. Page zoom still scales it, but the reader who sets a larger default font in the browser's settings gets nothing from a page sized in pixels.

Relative units

A relative unit resolves against something in the reader's context. That is exactly what makes it the right default: when the reader changes the context, the design follows.

What each relative unit is measured againstFive nested boxes. The outermost is the viewport, which vw, vh, vi, vb, dvh, svh and lvh measure. Inside it is the html root element, whose font size rem measures. Inside that is a main element declared as a container, which cqi and cqb measure. Inside that is a section, the parent, whose inline size a percentage width measures. Innermost is a p element, the element itself, whose own font gives em, ch, lh and ex. Example lines show 60ch as sixty widths of the zero character, 1lh as one line height, and 0.5em as half the element's font size.viewportvw · vh · vi · vb · dvh · svh · lvhhtml · the root font sizeremmain · a query containercqi · cqbsection · the parent%p · the element's own fontem · ch · lh · exmax-inline-size: 60ch → sixty widths of "0"margin-block: 1lh → one line of this textpadding-inline: 0.5em → half this font size
Each relative unit answers "relative to what?" with a different box. (For font-size itself, em and % use the parent's font size.)
UnitRelative toReach for it when
remThe root element's font size (by default, the reader's chosen size)Type scales, spacing tokens, anything that should follow the reader's font setting
emThe element's own font size; for font-size, the parent'sPadding, gaps and icons that should grow with the text beside them
chThe width of the "0" glyph in the element's fontLine length (max-inline-size: 65ch) and input widths
lhThe element's computed line heightVertical rhythm: space measured in lines of text
exThe x-height of the element's fontFine typographic alignment, rarely
%A reference each property defines, usually the containing blockFluid widths inside a parent
vw, vh1% of the viewport's width or heightA fluid term inside clamp(); full-bleed sections
vi, vb1% of the viewport in the inline or block directionThe logical versions of vw and vh
svh, lvh, dvhThe small, large or dynamic viewport heightFull-height layouts on phones, where browser toolbars come and go
vmin, vmaxThe smaller or larger of vw and vhSizes that should fit either orientation
cqi, cqb1% of the nearest query container's inline or block sizeComponents that scale with the slot they are placed in
pxThe reference pixel, 1/96 of an inchA 1px hairline border, and little else

Which of these units suit type, and why, is the subject of Size, spacing and measure in the Fonts section: rem for sizes, unitless line heights, and ch for line length.

Font-relative units: rem, em and ch

Browsers ship with a default font size, usually 16 pixels, and readers can change it. Because rem measures the root element, a page sized in rem grows when the reader asks for larger text. Spacing in rem grows with it, so the proportions of the design survive. This is the main reason rem is the house default.

em is local. It measures the element's own font size, so padding of 0.5em on a button is always half the button's text size, whether the button sits in a heading or a footnote. The catch is that font-size in em is relative to the parent, so it compounds: a nested list with li { font-size: 0.9em; } shrinks by ten percent at every level. Use em where you want that local scaling and rem where you want a fixed step on a global scale.

ch is the unit for reading. Lines longer than about 75 characters are tiring to track, and max-inline-size: 65ch sets the measure in the one unit that actually counts characters. Because ch follows the font, the measure stays right when the font or its size changes.

Percentages

A percentage is always a percentage of something, and each property says what. For inline-size it is the containing block's inline size. For font-size it is the parent's font size. For line-height it is the element's own font size. Two cases surprise people:

  • Margin and padding percentages resolve against the containing block's inline size, even padding-block. That is consistent, not a bug: it lets one percentage keep a box's proportions.
  • block-size: 50% does nothing unless the parent's block size is definite. Heights normally come from content, so there is often nothing to take fifty percent of.

Viewport units, and the mobile toolbar

100vh was meant to be "the height of the screen". On phones, the browser's address bar slides away as you scroll, so the visible height changes. Browsers settled on the large viewport for vh, which made 100vh sections taller than the screen while the toolbar showed. The fix is three explicit units: svh for the small viewport (toolbars showing), lvh for the large one (toolbars hidden), and dvh for whichever is current. They are Baseline widely available. Prefer svh or dvh for full-height layouts, and keep a vh declaration before it if you want an older-browser fallback.

One more caution: on desktops with classic scrollbars, 100vw includes the scrollbar, so a 100vw box can be wider than the page and cause sideways scrolling. 100% of the body is usually what was meant.

Container units

Viewport units answer "how big is the screen". A reusable component needs to know how big its slot is. Container query units do that: once an ancestor declares container-type: inline-size, 1cqi is one percent of that container's inline size. With no container in sight, they fall back to the small viewport units. Container units are Baseline widely available, and the responsive design page pairs them with container queries.

Choosing, in practice

The deck observed that people reach for points and pixels far too often. That habit made sense when screens were all roughly the same; it does not anymore. Here is the habit in its classic form:

body { font-size: 14px; } h1 { font-size: 32px; margin-bottom: 20px; } .content { width: 960px; padding-left: 20px; }

Every number there ignores the reader. The body text refuses the reader's preferred size, the fixed width overflows a phone, and the physical padding-left ends up on the wrong side in a right-to-left language. The same intent, measured against the reader's context:

@layer base { :root { --space: 1rem; --measure: 65ch; } h1 { font-size: clamp(1.75rem, 1.2rem + 2.5vw, 3rem); margin-block-end: var(--space); } main { max-inline-size: var(--measure); margin-inline: auto; padding-inline: var(--space); } }

The body font size is not set at all, so it is the reader's. The heading scales with the screen but keeps a rem floor and ceiling. The column is as wide as a comfortable line and never wider than the screen. This is how a style sheet holds up its end of the negotiation with the reader.

Units that are not lengths

The rest of the unit explosion belongs to other types. Angles use deg, turn, rad and grad. Times use s and ms. Resolutions, used in media queries and image-set(), use dppx (also written x), dpi and dpcm. Grid adds the flexible fr. Some units in older lists never really existed in browsers: Hz and kHz belonged to aural style sheets that were never implemented, and vm was Internet Explorer's name for what became vmin.

3. Specified, computed and used values

A value you write is not the value the browser paints. It passes through stages, and each stage resolves a little more of it:

  • The specified value is the one the cascade chose for the property, as written: 1.5em, 50%. If no rule set the property, it is the inherited value or the property's initial value.
  • The computed value resolves everything that can be resolved without laying out the page. Font-relative and viewport lengths become absolute lengths, var() references are substituted, and keywords such as bolder become numbers. Percentages that depend on layout stay percentages.
  • The used value comes out of layout, when the sizes of boxes are known. Here width: 50% finally becomes a number of pixels.
  • The actual value is what the screen can draw, after rounding to device pixels.
Two values through the pipeline: specified, computed, used, actualFour columns: specified, computed, used and actual. First row: font-size: 1.5em, inside a parent whose font size is 20px, computes to 30px, and stays 30px when used and painted. Second row: width: 50%, inside a parent that is 40rem or 640px wide, computes to 50 percent because the percentage needs layout, becomes 320px after layout, and is painted with 640 device pixels on a 2x display. The first two columns happen at style time, the third at layout time, the fourth at paint time.Specifiedwhat the cascade choseComputedresolved without layoutUsedresolved by layoutActualwhat is paintedfont-size: 1.5em30px1.5 × parent's 20px30px30pxparent font-size: 20pxwidth: 50%50%needs layout to resolve320px50% of 640px320px640 device px at 2×parent inline size: 40rem = 640px (root font 16px)style timelayout timepaint time
The font size is settled at style time; the percentage width has to wait for layout. The highlighted box marks where each value becomes a real length.

Why the stages matter

Inheritance passes computed values. A child that inherits from its parent receives the parent's computed value, not the text the author wrote. This explains the line-height puzzle from section 1:

article { font-size: 1rem; line-height: 1.5; h2 { font-size: 2rem; } }

Here every descendant inherits the number 1.5, so the h2 gets a line height of one and a half times its own large font. Change the article's value to 1.5em and it computes to a length, 24 pixels at the default size, before it is inherited. Now the heading's two-line titles overlap, because they inherit 24 pixels of line height for text that is 32 pixels tall. Unitless line-height is the safe default for exactly this reason.

The same rule explains why em compounds and rem does not: an em font size is resolved against the parent's computed size, which already carries every em above it. The cascade page follows the pipeline back through inheritance.

Scripts read resolved values. getComputedStyle() returns what the specification calls the resolved value. For most properties that is the computed value. For layout-dependent ones such as width and margin on a rendered element, it is the used value, in pixels. Asking for it forces the browser to finish layout first, which is one reason reading styles in a loop can be slow.

4. Functions

Anywhere a property accepts a value, it also accepts a function that returns a value of the right type. CSS ships a large function library: dozens of functions, from arithmetic to gradients to color. Each has a typed signature written in the same grammar as property values. Here is clamp() from the CSS Values specification:

clamp() = clamp( <calc-sum>#{3} )

The # means a comma-separated list, and {3} means exactly three. So clamp() takes three calculations. CSS functions are pure: no side effects, no state, no input or output. They are evaluated during the same pipeline as every other value, so a function's result can be computed at style time or, if it contains a percentage, at layout time.

calc() and dimensional arithmetic

calc() does arithmetic with the four usual operators and parentheses. What makes it interesting is that it respects types, much as physics respects units:

RuleValidInvalid
Add and subtract values of compatible types; lengths and percentages mixcalc(100% - 2rem)calc(1rem + 2)
Multiply or divide by a plain numbercalc(var(--space) * 3), calc(100% / 3)calc(1rem * 1rem)
Put spaces around + and -calc(1rem + 1vw)calc(1rem+1vw)

The space rule exists because -1vw is itself a valid negative length, so the parser needs spaces to tell subtraction from a sign. The result of mixing a length and a percentage is a length-percentage that cannot be resolved until layout, which is why calc(100% - 2rem) is the classic way to say "the available space, minus an inset".

min(), max() and clamp()

These three take calculations as arguments, so they never need a calc() inside them.

  • min(a, b, …) returns the smallest argument: a ceiling. inline-size: min(100% - 2rem, 60rem) fills the space but never exceeds 60rem.
  • max(a, b, …) returns the largest: a floor. inline-size: max(15rem, 30%) is never narrower than 15rem.
  • clamp(min, preferred, max) returns the preferred value, held between the two bounds.

The pattern you will see most is fluid type: a size that grows smoothly with the viewport between a minimum and a maximum, with no breakpoints at all.

clamp(1rem, 0.9rem + 0.4vw, 1.2rem) across viewport widthsA graph of font size against viewport width, with a 16px root font. The preferred value, 0.9rem plus 0.4vw, is a dashed straight line rising from 14.4px at zero width. The clamped result is a solid line: flat at the 16px minimum until a 400px viewport, rising along the preferred line until 1200px, then flat at the 19.2px maximum.viewport widthfont size0.9rem + 0.4vw400px1200pxmin 1rem = 16pxmax 1.2rem = 19.2pxfluid range
The dashed line is the preferred value. clamp() holds it between the floor and the ceiling, so the size is fluid only where fluid makes sense.
@layer base { :root { --step-0: clamp(1rem, 0.9rem + 0.4vw, 1.2rem); --step-3: clamp(1.75rem, 1.2rem + 2.5vw, 3rem); } body { font-size: var(--step-0); } h1 { font-size: var(--step-3); } }

Keep a rem term in the preferred value. Text sized in vw alone does not grow when the reader zooms or raises the default font size, because the viewport does not change. The rem part keeps the reader in control.

The rest of the math library

Beyond the four workhorses, CSS has a small scientific calculator. These take and return the same types as calc():

FamilyFunctionsUsed for
Steppinground()Snapping a value to an interval, rounding up, down or to the nearest
Remaindersmod(), rem()Repeating patterns; the two differ in the sign of the result
Signabs(), sign()Distances and directions
Trigonometrysin(), cos(), tan(), asin(), acos(), atan(), atan2()Placing items around a circle; decorative geometry
Exponentialpow(), sqrt(), hypot(), log(), exp()Modular type scales; distances

The trigonometric, exponential and stepping functions reached every engine in 2023 and 2024. abs() and sign() came last: Chrome added them in mid-2025, so they have been Baseline newly available only since then. A modular type scale shows how they combine with custom properties:

:root { --ratio: 1.25; --base: 1rem; --step-1: calc(var(--base) * var(--ratio)); --step-2: calc(var(--base) * pow(var(--ratio), 2)); --step-3: calc(var(--base) * pow(var(--ratio), 3)); }

Color functions, in brief

Color has more notations than any other type. Named colors, hex codes, rgb() and hsl() are the historical ones. oklch() describes a color by lightness, chroma and hue in a perceptually uniform space, so two colors with the same lightness look about equally bright, which hsl() cannot promise. Three functions build colors from other colors:

  • color-mix(in oklch, var(--accent), white 20%) blends two colors in a chosen space;
  • relative color syntax, oklch(from var(--accent) calc(l + 0.1) c h), takes a color apart into channels and rebuilds it;
  • light-dark(oklch(25% 0.02 250), oklch(92% 0.01 250)) picks the first color in a light color scheme and the second in a dark one.

oklch() and color-mix() are Baseline widely available; relative colors and light-dark() are newly available. The Color section teaches them in full: Color in CSS for every notation, and Deriving color for mixing and relative colors.

The other families

Most remaining functions live where they are used, and other pages teach them in context: gradients and url() for images; translate() and rotate() for transforms; blur() and friends for filters; minmax(), repeat() and fit-content() for grid layout; counter() for generated content; and cubic-bezier() and steps() for easing. Three more read values from outside the style sheet: var() reads a custom property (next section), attr() reads an HTML attribute (as a string in every browser; Chromium-based browsers since 2025 can also read it as a typed value such as a length, which is not yet Baseline), and env() reads a value the browser supplies, such as env(safe-area-inset-bottom) for the area a phone's rounded corners or home indicator cover.

The boundary of the language

For most of its history CSS had a fixed library: if a function was not in the specification, you needed a preprocessor such as Sass or a script. That boundary is moving. A custom-functions specification adds @function, which lets an author define a function that returns a value, and a separate if() function chooses between values inline. Both have shipped in Chromium browsers, but neither was Baseline when this page was written, so treat them as experiments and keep a plain declaration first as a fallback.

Meanwhile, a pattern that works everywhere covers most practical cases: a rule reads its parameters from custom properties with defaults, and the caller sets the ones it cares about.

@layer components { blockquote { border-inline-start: var(--rule-width, 0.25em) solid var(--rule-color, currentColor); padding-inline-start: var(--rule-gap, 1em); } aside blockquote { --rule-color: oklch(55% 0.15 45); --rule-width: 0.125em; } }

That is not a real function: there is no return value and no encapsulation. But it is the right instinct. CSS is a declarative language for describing presentation, not a general-purpose one, and its growth comes from adding precise, typed tools to that job.

5. Custom properties and var()

CSS has variables. The specification calls them custom properties, and the name is exact: they are properties, declared like any other, with a name that begins with two hyphens. You read one with the var() function.

@layer base { :root { --text: oklch(25% 0.02 250); --accent: oklch(50% 0.15 45); --space: 1rem; --measure: 65ch; } article { max-inline-size: var(--measure); padding-inline: var(--space); color: var(--text); a { color: var(--accent); } } }

Names are case-sensitive, so --Accent and --accent are different properties. Any name is allowed after the hyphens; pick names for the role a value plays (--accent, --space) rather than its appearance (--orange, --16), so a redesign changes values without renaming them.

Scope is the document tree

Custom properties inherit. A value declared on :root, the html element, reaches every element in the page. A value declared on any other element reaches that element and its descendants, and overrides the outer value there. There is no block scope and no closure: the scope chain is the document tree.

@layer base { :root { --accent: oklch(50% 0.15 45); } footer { --accent: oklch(78% 0.12 80); background: oklch(22% 0.03 250); } }

Every var(--accent) inside the footer, including the link rule from the previous example, now resolves to the lighter color. Nothing else had to change.

Fallbacks

var() takes an optional second argument, used when the property is not defined. Fallbacks can nest:

nav a { padding: var(--link-pad, var(--space, 0.5rem)); }

Read it as "use --link-pad; if that is not set, --space; if neither is, 0.5rem". Fallbacks let a component work on a page that has not defined the tokens it hopes for.

Untyped tokens, checked late

An ordinary custom property is not checked when it is declared. Its value is just a sequence of tokens, stored as written and substituted wherever var() asks for it. The check happens at the place of use, at computed-value time:

:root { --gap: wide; } ul { display: flex; gap: var(--gap); }

The first rule is perfectly legal. The second declaration parses, because the browser cannot know what --gap holds until it substitutes it. When it does, wide is not a length, and the declaration becomes invalid at computed-value time. It is too late to fall back to an earlier gap declaration in the cascade, so the property acts as if it were unset: inherited if it normally inherits, otherwise its initial value. Here gap becomes 0.

Late substitution has a useful side. Because an unregistered property is stored as tokens, an em inside it resolves where it is used:

:root { --pad: 0.5em; } h2 { padding-block: var(--pad); } small { padding-block: var(--pad); }

The heading gets half of its large font; the small element gets half of its small one. One token, scaled to each context.

Live values

Preprocessor variables are replaced by values when the style sheet is built, then gone. Custom properties exist in the running page. Change one, with a selector or a script, and every element that reads it updates at once. That is what makes theming simple:

:root { --surface: oklch(98% 0.005 80); --text: oklch(25% 0.02 250); &[data-theme="dark"] { --surface: oklch(18% 0.02 250); --text: oklch(92% 0.01 250); } }

A script that sets document.documentElement.dataset.theme = "dark", or element.style.setProperty("--space", "2rem"), changes data, not presentation. The style sheet still decides what the data means. That division, which Basics recommended for inline styles, keeps scripts and CSS from fighting over the same declarations.

6. Typed custom properties with @property

Untyped properties have two weaknesses. Mistakes are not caught where they are made, and the browser cannot animate them: if it does not know whether --fill holds a length, a color or a word, it cannot work out the values halfway between two settings, so a change simply jumps. The @property at-rule fixes both by registering a type.

@property --fill { syntax: "<percentage>"; inherits: false; initial-value: 0%; }

The three descriptors are the type, whether the property inherits, and its starting value. The starting value must be computationally independent: the browser has to be able to resolve it without looking at any element. 0% and 0 qualify; 1rem does not, because it depends on the root font size, and an invalid initial value makes the browser drop the whole @property rule. Register with a neutral initial value and set the real one where it belongs, such as :root { --gap: 1rem; }. Registration changes three things:

  • Values are checked when declared. --fill: wide is now an invalid declaration, dropped at parse time like any other type error, so an earlier valid value wins.
  • There is always a value. var(--fill) resolves to the initial value even if nobody set it.
  • The property can be animated. The browser knows how to interpolate percentages, so a transition on --fill runs smoothly.

The third point is the one that opens new effects. Gradients cannot be transitioned directly, but a typed property used inside a gradient can:

@layer components { nav a { background: linear-gradient(to right, var(--accent) var(--fill), transparent var(--fill)); background-size: 100% 0.15em; background-position: 0 100%; background-repeat: no-repeat; &:where(:hover, :focus-visible) { --fill: 100%; } } } @media (prefers-reduced-motion: no-preference) { @layer components { nav a { transition: --fill 0.3s ease-out; } } }

Hovering a link draws its underline across. Without the registration, the underline would snap from empty to full. The transition is added only for readers who have not asked for reduced motion; everyone else still gets the underline, just without the movement.

Details that trip people up

  • The syntax string uses the type names from section 1, in quotes: "<length>", "<color>", "<number>", "<angle>", unions such as "<length> | auto", and "*" for any tokens.
  • syntax and inherits are required, and so is initial-value unless the syntax is "*".
  • The initial value must be computationally independent: the browser must be able to compute it without any element's context. 0%, 0deg and a color are fine; 1em and 1rem are not, and make the whole rule invalid.
  • A registered length computes to an absolute length where it is declared, so the use-site scaling trick from the previous section no longer applies. Register a property when you want checking and animation; leave it unregistered when you want late, flexible substitution.
  • inherits: false suits values that are local to one element, such as an animation's progress, and saves the browser from passing them down the tree.

Scripts can register the same property with CSS.registerProperty():

CSS.registerProperty({ name: '--fill', syntax: '<percentage>', inherits: false, initialValue: '0%', });

@property is Baseline newly available since 2024. In a browser without it, the rule is ignored and the property behaves like an ordinary untyped one: the underline still works, it just appears without animating. That is progressive enhancement by default.

7. At-rules at a glance

Some of the most important parts of CSS are not rules but at-rules: @layer, @media, @property. Every one shares a single shape. After the @ and its name comes an optional prelude, then either a semicolon or a block. The block holds one of two kinds of content.

@name prelude; statement: @import, @layer (the order list) @name prelude { rules } group: @media, @supports, @container, @layer, @scope @name prelude { descriptors } descriptors: @property, @font-face, @page, @counter-style

A group at-rule holds ordinary rules and applies them conditionally or organizes them: rules inside @media apply only when the media query matches. A descriptor at-rule declares a named object, and its block holds descriptors, which look like declarations but are not properties. syntax inside @property and src inside @font-face mean nothing anywhere else. @keyframes is the odd one out: its block holds keyframe blocks, each a percentage with its own declarations.

Most at-rules are taught where they are used. This table is the map:

At-ruleBodyWhat it doesTaught in
@importstatementPulls in another style sheet, optionally into a layer or under a conditionBasics
@layerstatement or rulesDeclares the order of cascade layers, or fills oneThe cascade
@scoperulesLimits selectors to a subtree, with an optional lower boundThe cascade
@mediarulesApplies rules for a medium, a viewport size or a reader preferenceBasics, then Responsive
@containerrulesApplies rules when an ancestor container matches a queryResponsive
@supportsrulesApplies rules when the browser supports a featureResponsive
@pagedescriptors and margin boxesSets the size and margins of printed pagesResponsive (print)
@propertydescriptorsRegisters a typed custom propertyThis page
@functionfunction bodyDefines a custom function (not yet Baseline)This page, in brief
@counter-styledescriptorsDefines a list-marker styleProperties
@font-facedescriptorsLoads a web font and names itThe Fonts topic (coming)
@font-palette-valuesdescriptorsOverrides the palette of a color fontThe Fonts topic (coming)
@keyframeskeyframe blocksDefines an animation as a sequence of statesThe Animation topic (coming)
@starting-stylerulesGives an element a starting point for entry transitionsThe Animation topic (coming)
@namespace, @charsetstatementXML namespaces and the file's encoding; rarely needed in HTMLNot covered

Group at-rules nest inside each other and, with native nesting, inside style rules too. That is how the examples on this page put a @layer inside a @media preference query: the enhancement stays in the layer it belongs to, and only applies when the reader's settings allow it.

8. Summary

ConceptKey points
TypesEvery value has a type, and each property's grammar lists the types it accepts. Lengths need units (except zero). Type errors are dropped silently; DevTools shows them.
The CSS pixelA reference pixel of about 1/96 inch at arm's length, not a device dot. Physical units derive from it. Pixels ignore the reader's font setting; use them for hairlines.
Relative unitsrem follows the reader's font size, em the element's, ch sets line length, % follows a property-defined reference, dvh and svh handle mobile toolbars, and cqi follows the component's container.
The value pipelineSpecified, computed (relative lengths made absolute, var() substituted), used (percentages resolved by layout), actual. Inheritance passes computed values, so use unitless line-height.
Math functionscalc() respects types. min() is a ceiling, max() a floor, clamp() both. Keep a rem term in fluid sizes so zoom still works.
Color functionsoklch() for perceptual color; color-mix(), relative colors and light-dark() derive colors from tokens. Full treatment in the Color topic.
Custom propertiesNamed with --, read with var() and its fallback, scoped by inheritance through the document tree, and live at run time. Untyped values are checked only where they are used.
@propertyRegisters a type, inheritance and initial value, so values are checked early, always defined, and animatable. Initial values must not depend on context.
At-rulesOne shape: name, prelude, then a semicolon, a block of rules or a block of descriptors. Each is taught on the page where it is used.