Beyond simple type/class/ID selectors, combinators describe relationships between elements:
.card p { } /* descendant combinator (space) — any <p> anywhere inside .card */
.card > p { } /* child combinator — only a <p> that is a DIRECT child of .card */
h2 + p { } /* adjacent sibling — a <p> immediately after an h2, same parent */
h2 ~ p { } /* general sibling — any <p> after an h2 and sharing the same parent */
The descendant combinator is by far the most common, but it's also the easiest to accidentally make too broad — .card p matches a <p> nested arbitrarily deep, including inside components you didn't intend to style. The child combinator (>) is the fix when you specifically mean "direct child only."
Attribute selectors match on an element's attributes directly, useful for styling by form input type or data attributes without adding extra classes:
input[type="text"] { } /* exact match */
a[href^="https://"] { } /* starts with */
a[href$=".pdf"] { } /* ends with */
[class*="btn-"] { } /* contains, anywhere in the attribute value */
[data-state="open"] { } /* matches a custom data attribute exactly */