Project import generated by Copybara.

GitOrigin-RevId: f2dc67c4d3fe92f26693c02366db1e60cae0db27
Change-Id: I4855950af9746fecb1de5970422479c1d28b23f3
diff --git a/.cargo/config.toml b/.cargo/config.toml
new file mode 100644
index 0000000..39cc9ac
--- /dev/null
+++ b/.cargo/config.toml
@@ -0,0 +1,21 @@
+[target.'cfg(target_env = "gnu")']
+rustflags = ["-C", "link-args=-Wl,-z,nodelete"]
+
+[target.aarch64-unknown-linux-gnu]
+linker = "aarch64-linux-gnu-gcc"
+
+[target.armv7-unknown-linux-gnueabihf]
+linker = "arm-linux-gnueabihf-gcc"
+
+[target.aarch64-unknown-linux-musl]
+linker = "aarch64-linux-musl-gcc"
+rustflags = ["-C", "target-feature=-crt-static"]
+
+[target.wasm32-unknown-unknown]
+rustflags = ["-C", "link-arg=--export-table"]
+
+# Statically link Visual Studio redistributables on Windows builds
+[target.x86_64-pc-windows-msvc]
+rustflags = ["-C", "target-feature=+crt-static"]
+[target.aarch64-pc-windows-msvc]
+rustflags = ["-C", "target-feature=+crt-static"]
diff --git a/.github/workflows/release-crates.yml b/.github/workflows/release-crates.yml
new file mode 100644
index 0000000..6217bc5
--- /dev/null
+++ b/.github/workflows/release-crates.yml
@@ -0,0 +1,19 @@
+name: release-crates
+on:
+  workflow_dispatch:
+
+jobs:
+  release-crates:
+    runs-on: ubuntu-latest
+    name: Release Rust crate
+    steps:
+      - uses: actions/checkout@v3
+      - uses: bahmutov/npm-install@v1.8.32
+      - name: Install Rust
+        uses: dtolnay/rust-toolchain@stable
+      - run: cargo login ${CRATES_IO_TOKEN}
+        env:
+          CRATES_IO_TOKEN: ${{ secrets.CRATES_IO_TOKEN }}
+      - run: |
+          cargo install cargo-workspaces
+          cargo workspaces publish --no-remove-dev-deps --from-git -y
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..95d3043
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,312 @@
+name: release
+on:
+  workflow_dispatch:
+
+jobs:
+  build:
+    strategy:
+      fail-fast: false
+      matrix:
+        include:
+          # Windows
+          - os: windows-latest
+            target: x86_64-pc-windows-msvc
+            binary: lightningcss.exe
+          - os: windows-latest
+            target: aarch64-pc-windows-msvc
+            binary: lightningcss.exe
+          # Mac OS
+          - os: macos-latest
+            target: x86_64-apple-darwin
+            strip: strip -x # Must use -x on macOS. This produces larger results on linux.
+            binary: lightningcss
+
+    name: build-${{ matrix.target }}
+    runs-on: ${{ matrix.os }}
+    steps:
+      - uses: actions/checkout@v3
+      - name: Install Node.JS
+        uses: actions/setup-node@v3
+        with:
+          node-version: 18
+      - name: Install Rust
+        uses: dtolnay/rust-toolchain@stable
+
+      - name: Setup rust target
+        run: rustup target add ${{ matrix.target }}
+
+      - uses: bahmutov/npm-install@v1.8.32
+      - name: Build release
+        run: yarn build-release
+        env:
+          RUST_TARGET: ${{ matrix.target }}
+      - name: Build CLI
+        run: |
+          cargo build --release --features cli --target ${{ matrix.target }}
+          node -e "require('fs').renameSync('target/${{ matrix.target }}/release/${{ matrix.binary }}', '${{ matrix.binary }}')"
+      - name: Strip debug symbols # https://github.com/rust-lang/rust/issues/46034
+        if: ${{ matrix.strip }}
+        run: ${{ matrix.strip }} *.node ${{ matrix.binary }}
+      - name: Upload artifacts
+        uses: actions/upload-artifact@v4
+        with:
+          name: bindings-${{ matrix.target }}
+          path: |
+            *.node
+            ${{ matrix.binary }}
+
+  build-apple-silicon:
+    name: build-apple-silicon
+    runs-on: macos-latest
+    steps:
+      - uses: actions/checkout@v3
+      - name: Install Node.JS
+        uses: actions/setup-node@v3
+        with:
+          node-version: 18
+      - name: Install Rust
+        uses: dtolnay/rust-toolchain@stable
+
+      - name: Setup rust target
+        run: rustup target add aarch64-apple-darwin
+
+      - uses: bahmutov/npm-install@v1.8.32
+      - name: Build release
+        run: yarn build-release
+        env:
+          RUST_TARGET: aarch64-apple-darwin
+          JEMALLOC_SYS_WITH_LG_PAGE: 14
+      - name: Build CLI
+        run: |
+          export CC=$(xcrun -f clang);
+          export CXX=$(xcrun -f clang++);
+          SYSROOT=$(xcrun --sdk macosx --show-sdk-path);
+          export CFLAGS="-isysroot $SYSROOT -isystem $SYSROOT";
+          export MACOSX_DEPLOYMENT_TARGET="10.9";
+          cargo build --release --features cli --target aarch64-apple-darwin
+          mv target/aarch64-apple-darwin/release/lightningcss lightningcss
+        env:
+          JEMALLOC_SYS_WITH_LG_PAGE: 14
+      - name: Strip debug symbols # https://github.com/rust-lang/rust/issues/46034
+        run: strip -x *.node lightningcss
+      - name: Upload artifacts
+        uses: actions/upload-artifact@v4
+        with:
+          name: bindings-aarch64-apple-darwin
+          path: |
+            *.node
+            lightningcss
+
+  build-linux:
+    strategy:
+      fail-fast: false
+      matrix:
+        include:
+          - target: x86_64-unknown-linux-gnu
+            strip: strip
+            image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian
+            setup: npm install --global yarn@1
+          - target: aarch64-unknown-linux-gnu
+            strip: llvm-strip
+            image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-aarch64
+          - target: aarch64-linux-android
+            strip: llvm-strip
+            image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-aarch64
+          - target: armv7-unknown-linux-gnueabihf
+            strip: llvm-strip
+            image: ghcr.io/napi-rs/napi-rs/nodejs-rust@sha256:c22284b2d79092d3e885f64ede00f6afdeb2ccef7e2b6e78be52e7909091cd57
+          - target: aarch64-unknown-linux-musl
+            image: ghcr.io/napi-rs/napi-rs/nodejs-rust@sha256:78c9ab1f117f8c535b93c4b91a2f19063dda6e4dba48a6187df49810625992c1
+            strip: aarch64-linux-musl-strip
+          - target: x86_64-unknown-linux-musl
+            image: ghcr.io/napi-rs/napi-rs/nodejs-rust@sha256:78c9ab1f117f8c535b93c4b91a2f19063dda6e4dba48a6187df49810625992c1
+            strip: strip
+
+    name: build-${{ matrix.target }}
+    runs-on: ubuntu-latest
+    container:
+      image: ${{ matrix.image }}
+
+    steps:
+      - uses: actions/checkout@v3
+      - name: Install Node.JS
+        uses: actions/setup-node@v3
+        with:
+          node-version: 18
+      - name: Install Rust
+        uses: dtolnay/rust-toolchain@stable
+
+      - name: Setup Android NDK
+        if: ${{ matrix.target == 'aarch64-linux-android' }}
+        run: |
+          sudo apt update && sudo apt install unzip -y
+          cd /tmp
+          wget -q https://dl.google.com/android/repository/android-ndk-r28-linux.zip -O /tmp/ndk.zip
+          unzip ndk.zip
+
+      - name: Setup cross compile toolchain
+        if: ${{ matrix.setup }}
+        run: ${{ matrix.setup }}
+
+      - name: Setup rust target
+        run: rustup target add ${{ matrix.target }}
+
+      - uses: bahmutov/npm-install@v1.8.32
+      - name: Build release
+        run: yarn build-release
+        env:
+          ANDROID_NDK_LATEST_HOME: /tmp/android-ndk-r28
+          RUST_TARGET: ${{ matrix.target }}
+      - name: Build CLI
+        env:
+          ANDROID_NDK_LATEST_HOME: /tmp/android-ndk-r28
+        run: |
+          yarn napi build --bin lightningcss --release --features cli --target ${{ matrix.target }}
+          mv target/${{ matrix.target }}/release/lightningcss lightningcss
+      - name: Strip debug symbols # https://github.com/rust-lang/rust/issues/46034
+        if: ${{ matrix.strip }}
+        run: ${{ matrix.strip }} *.node lightningcss
+      - name: Upload artifacts
+        uses: actions/upload-artifact@v4
+        with:
+          name: bindings-${{ matrix.target }}
+          path: |
+            *.node
+            lightningcss
+
+  build-freebsd:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3
+      - name: Build FreeBSD
+        uses: cross-platform-actions/action@v0.25.0
+        env:
+          DEBUG: napi:*
+          RUSTUP_HOME: /usr/local/rustup
+          CARGO_HOME: /usr/local/cargo
+          RUSTUP_IO_THREADS: 1
+        with:
+          operating_system: freebsd
+          version: '14.0'
+          memory: 13G
+          cpu_count: 3
+          environment_variables: 'DEBUG RUSTUP_IO_THREADS'
+          shell: bash
+          run: |
+            sudo pkg install -y -f curl node libnghttp2 npm yarn
+            curl https://sh.rustup.rs -sSf --output rustup.sh
+            sh rustup.sh -y --profile minimal --default-toolchain beta
+            source "$HOME/.cargo/env"
+            echo "~~~~ rustc --version ~~~~"
+            rustc --version
+            echo "~~~~ node -v ~~~~"
+            node -v
+            echo "~~~~ yarn --version ~~~~"
+            yarn --version
+            yarn install || true
+            yarn build-release
+            strip -x *.node
+            cargo build --release --features cli
+            mv target/release/lightningcss lightningcss
+            node -e "require('.')"
+            ./lightningcss --help
+            rm -rf node_modules
+            rm -rf target
+            rm -rf .yarn/cache
+
+      - name: Upload artifacts
+        uses: actions/upload-artifact@v4
+        with:
+          name: bindings-x86_64-unknown-freebsd
+          path: |
+            *.node
+            lightningcss
+
+  build-wasm:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3
+      - name: Install Node.JS
+        uses: actions/setup-node@v3
+        with:
+          node-version: 18
+      - uses: bahmutov/npm-install@v1.8.32
+      - name: Install Rust
+        uses: dtolnay/rust-toolchain@stable
+        with:
+          targets: wasm32-unknown-unknown
+      - name: Setup rust target
+        run: rustup target add wasm32-unknown-unknown
+      - name: Install wasm-opt
+        run: |
+          curl -L -O https://github.com/WebAssembly/binaryen/releases/download/version_111/binaryen-version_111-x86_64-linux.tar.gz
+          tar -xf binaryen-version_111-x86_64-linux.tar.gz
+      - name: Build wasm
+        run: |
+          export PATH="$PATH:./binaryen-version_111/bin"
+          yarn wasm:build-release
+      - name: Upload artifacts
+        uses: actions/upload-artifact@v4
+        with:
+          name: wasm
+          path: wasm/lightningcss_node.wasm
+
+  release:
+    runs-on: ubuntu-latest
+    name: Build and release
+    needs:
+      - build
+      - build-linux
+      - build-apple-silicon
+      - build-freebsd
+      - build-wasm
+    steps:
+      - uses: actions/checkout@v3
+      - uses: bahmutov/npm-install@v1.8.32
+      - name: Download artifacts
+        uses: actions/download-artifact@v4
+        with:
+          path: artifacts
+      - name: Show artifacts
+        run: ls -R artifacts
+      - name: Build npm packages
+        run: |
+          node scripts/build-npm.js
+          cp artifacts/wasm/* wasm/.
+          node scripts/build-wasm.js
+      - run: echo //registry.npmjs.org/:_authToken=${NPM_TOKEN} > ~/.npmrc
+        env:
+          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
+      - name: Publish to npm
+        run: |
+          for pkg in npm/*; do
+            echo "Publishing $pkg..."
+            cd $pkg;
+            npm publish;
+            cd ../..;
+          done
+          cd wasm
+          echo "Publishing lightningcss-wasm...";
+          npm publish
+          cd ..
+          cd cli
+          echo "Publishing lightningcss-cli...";
+          npm publish
+          cd ..
+          echo "Publishing lightningcss...";
+          npm publish
+
+  release-crates:
+    runs-on: ubuntu-latest
+    name: Release Rust crate
+    steps:
+      - uses: actions/checkout@v3
+      - uses: bahmutov/npm-install@v1.8.32
+      - name: Install Rust
+        uses: dtolnay/rust-toolchain@stable
+      - run: cargo login ${CRATES_IO_TOKEN}
+        env:
+          CRATES_IO_TOKEN: ${{ secrets.CRATES_IO_TOKEN }}
+      - run: |
+          cargo install cargo-workspaces
+          cargo workspaces publish --no-remove-dev-deps --from-git -y
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000..275e55d
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,60 @@
+name: test
+
+on:
+  push:
+    branches: [master]
+  pull_request:
+    branches: [master]
+
+jobs:
+  test:
+    runs-on: ubuntu-latest
+    env:
+      CARGO_TERM_COLOR: always
+      RUST_BACKTRACE: full
+      RUSTFLAGS: -D warnings
+    steps:
+      - uses: actions/checkout@v3
+      - uses: dtolnay/rust-toolchain@stable
+      - uses: Swatinem/rust-cache@v2
+      - run: cargo fmt
+      - run: cargo test --all-features
+
+  test-js:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3
+      - uses: actions/setup-node@v3
+        with:
+          node-version: 18
+      - uses: bahmutov/npm-install@v1.8.32
+      - uses: dtolnay/rust-toolchain@stable
+      - uses: Swatinem/rust-cache@v2
+      - run: yarn build
+      - run: yarn test
+      - run: yarn tsc
+
+  test-wasm:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3
+      - uses: actions/setup-node@v3
+        with:
+          node-version: 18
+      - uses: bahmutov/npm-install@v1.8.32
+      - uses: dtolnay/rust-toolchain@stable
+        with:
+          targets: wasm32-unknown-unknown
+      - name: Setup rust target
+        run: rustup target add wasm32-unknown-unknown
+      - uses: Swatinem/rust-cache@v2
+      - name: Install wasm-opt
+        run: |
+          curl -L -O https://github.com/WebAssembly/binaryen/releases/download/version_111/binaryen-version_111-x86_64-linux.tar.gz
+          tar -xf binaryen-version_111-x86_64-linux.tar.gz
+      - name: Build wasm
+        run: |
+          export PATH="$PATH:./binaryen-version_111/bin"
+          yarn wasm:build-release
+      - run: TEST_WASM=node yarn test
+      - run: TEST_WASM=browser yarn test
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..284b8c0
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,11 @@
+.DS_Store
+*.node
+node_modules/
+target/
+pkg/
+dist/
+.parcel-cache
+node/*.flow
+artifacts
+npm
+node/ast.json
diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 0000000..3b23a66
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,6 @@
+{
+  "bracketSpacing": false,
+  "endOfLine": "lf",
+  "singleQuote": true,
+  "trailingComma": "all"
+}
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..e814115
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,56 @@
+# Contributing
+
+Welcome, we really appreciate if you're considering to contribute, the joint effort of our contributors make projects like this possible!
+
+The goal of this document is to provide guidance on how you can get involved.
+
+## Getting started with bug fixing
+
+In order to make it easier to get familiar with the codebase we labeled simpler issues using [Good First Issue](https://github.com/parcel-bundler/lightningcss/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22) and [Help Wanted](https://github.com/parcel-bundler/lightningcss/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22+label%3A%22help+wanted%22).
+
+Before starting make sure you have the following requirements installed: [git](https://git-scm.com), [Node](https://nodejs.org), [Yarn](https://yarnpkg.com) and [Rust](https://www.rust-lang.org/tools/install).
+
+The process starts by [forking](https://docs.github.com/en/github/getting-started-with-github/fork-a-repo) the project and setup a new branch to work in. It's important that the changes are made in separated branches in order to ensure a pull request only includes the commits related to a bug or feature.
+
+Clone the forked repository locally and install the dependencies:
+
+```sh
+git clone https://github.com/USERNAME/lightningcss.git
+cd lightningcss
+yarn install
+```
+
+## Testing
+
+In order to test, you first need to build the core package:
+
+```sh
+yarn build
+```
+
+Then you can run the tests:
+
+```sh
+yarn test # js tests
+cargo test # rust tests
+```
+
+## Building
+
+There are different build targets available, with "release" being a production build:
+
+```sh
+yarn build
+yarn build-release
+
+yarn wasm:build
+yarn wasm:build-release
+```
+
+## Website
+
+The website is built using [Parcel](https://parceljs.org). You can start the development server by running:
+
+```sh
+yarn website:start
+```
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..a451dd9
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,1884 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "ahash"
+version = "0.7.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9"
+dependencies = [
+ "getrandom",
+ "once_cell",
+ "version_check",
+]
+
+[[package]]
+name = "ahash"
+version = "0.8.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
+dependencies = [
+ "cfg-if",
+ "getrandom",
+ "once_cell",
+ "serde",
+ "version_check",
+ "zerocopy",
+]
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "android-tzdata"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
+
+[[package]]
+name = "assert_cmd"
+version = "2.0.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc1835b7f27878de8525dc71410b5a31cdcc5f230aed5ba5df968e09c201b23d"
+dependencies = [
+ "anstyle",
+ "bstr",
+ "doc-comment",
+ "libc",
+ "predicates 3.1.3",
+ "predicates-core",
+ "predicates-tree",
+ "wait-timeout",
+]
+
+[[package]]
+name = "assert_fs"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7efdb1fdb47602827a342857666feb372712cbc64b414172bd6b167a02927674"
+dependencies = [
+ "anstyle",
+ "doc-comment",
+ "globwalk",
+ "predicates 3.1.3",
+ "predicates-core",
+ "predicates-tree",
+ "tempfile",
+]
+
+[[package]]
+name = "atty"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
+dependencies = [
+ "hermit-abi",
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
+
+[[package]]
+name = "base64-simd"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "781dd20c3aff0bd194fe7d2a977dd92f21c173891f3a03b677359e5fa457e5d5"
+dependencies = [
+ "simd-abstraction",
+]
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "bitflags"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
+
+[[package]]
+name = "bitvec"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c"
+dependencies = [
+ "funty",
+ "radium",
+ "tap",
+ "wyz 0.5.1",
+]
+
+[[package]]
+name = "browserslist-rs"
+version = "0.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74c973b79d9b6b89854493185ab760c6ef8e54bcfad10ad4e33991e46b374ac8"
+dependencies = [
+ "ahash 0.8.11",
+ "chrono",
+ "either",
+ "indexmap 2.7.0",
+ "itertools 0.13.0",
+ "nom",
+ "serde",
+ "serde_json",
+ "thiserror",
+]
+
+[[package]]
+name = "browserslist-rs"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f95aff901882c66e4b642f3f788ceee152ef44f8a5ef12cb1ddee5479c483be"
+dependencies = [
+ "ahash 0.8.11",
+ "chrono",
+ "either",
+ "indexmap 2.7.0",
+ "itertools 0.13.0",
+ "nom",
+ "serde",
+ "serde_json",
+ "thiserror",
+]
+
+[[package]]
+name = "bstr"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "786a307d683a5bf92e6fd5fd69a7eb613751668d1d8d67d802846dfe367c62c8"
+dependencies = [
+ "memchr",
+ "regex-automata",
+ "serde",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
+
+[[package]]
+name = "bytecheck"
+version = "0.6.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2"
+dependencies = [
+ "bytecheck_derive",
+ "ptr_meta",
+ "simdutf8",
+]
+
+[[package]]
+name = "bytecheck_derive"
+version = "0.6.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "bytes"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b"
+
+[[package]]
+name = "cbindgen"
+version = "0.24.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b922faaf31122819ec80c4047cc684c6979a087366c069611e33649bf98e18d"
+dependencies = [
+ "clap",
+ "heck",
+ "indexmap 1.9.3",
+ "log",
+ "proc-macro2",
+ "quote",
+ "serde",
+ "serde_json",
+ "syn 1.0.109",
+ "tempfile",
+ "toml",
+]
+
+[[package]]
+name = "cc"
+version = "1.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c31a0499c1dc64f458ad13872de75c0eb7e3fdb0e67964610c914b034fc5956e"
+dependencies = [
+ "shlex",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "chrono"
+version = "0.4.39"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825"
+dependencies = [
+ "android-tzdata",
+ "iana-time-zone",
+ "num-traits",
+ "windows-targets",
+]
+
+[[package]]
+name = "clap"
+version = "3.2.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123"
+dependencies = [
+ "atty",
+ "bitflags 1.3.2",
+ "clap_derive",
+ "clap_lex",
+ "indexmap 1.9.3",
+ "once_cell",
+ "strsim",
+ "termcolor",
+ "textwrap",
+]
+
+[[package]]
+name = "clap_derive"
+version = "3.2.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae6371b8bdc8b7d3959e9cf7b22d4435ef3e79e138688421ec654acf8c81b008"
+dependencies = [
+ "heck",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5"
+dependencies = [
+ "os_str_bytes",
+]
+
+[[package]]
+name = "const-str"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21077772762a1002bb421c3af42ac1725fa56066bfc53d9a55bb79905df2aaf3"
+dependencies = [
+ "const-str-proc-macro",
+]
+
+[[package]]
+name = "const-str-proc-macro"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e1e0fdd2e5d3041e530e1b21158aeeef8b5d0e306bc5c1e3d6cf0930d10e25a"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "convert_case"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
+dependencies = [
+ "unicode-segmentation",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+
+[[package]]
+name = "crossbeam-channel"
+version = "0.5.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-deque"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
+dependencies = [
+ "crossbeam-epoch",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.9.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+
+[[package]]
+name = "cssparser"
+version = "0.33.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9be934d936a0fbed5bcdc01042b770de1398bf79d0e192f49fa7faea0e99281e"
+dependencies = [
+ "cssparser-macros",
+ "dtoa-short",
+ "itoa",
+ "phf",
+ "serde",
+ "smallvec",
+]
+
+[[package]]
+name = "cssparser-color"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "556c099a61d85989d7af52b692e35a8d68a57e7df8c6d07563dc0778b3960c9f"
+dependencies = [
+ "cssparser",
+]
+
+[[package]]
+name = "cssparser-macros"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331"
+dependencies = [
+ "quote",
+ "syn 2.0.90",
+]
+
+[[package]]
+name = "ctor"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501"
+dependencies = [
+ "quote",
+ "syn 2.0.90",
+]
+
+[[package]]
+name = "dashmap"
+version = "5.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856"
+dependencies = [
+ "cfg-if",
+ "hashbrown 0.14.5",
+ "lock_api",
+ "once_cell",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "data-encoding"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2"
+
+[[package]]
+name = "data-url"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a30bfce702bcfa94e906ef82421f2c0e61c076ad76030c16ee5d2e9a32fe193"
+dependencies = [
+ "matches",
+]
+
+[[package]]
+name = "diff"
+version = "0.1.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
+
+[[package]]
+name = "difflib"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
+
+[[package]]
+name = "doc-comment"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
+
+[[package]]
+name = "dtoa"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dcbb2bf8e87535c23f7a8a321e364ce21462d0ff10cb6407820e8e96dfff6653"
+
+[[package]]
+name = "dtoa-short"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87"
+dependencies = [
+ "dtoa",
+]
+
+[[package]]
+name = "dyn-clone"
+version = "1.0.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125"
+
+[[package]]
+name = "either"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
+
+[[package]]
+name = "equivalent"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
+
+[[package]]
+name = "errno"
+version = "0.3.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
+dependencies = [
+ "libc",
+ "windows-sys",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
+
+[[package]]
+name = "float-cmp"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "fs_extra"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
+
+[[package]]
+name = "funty"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
+
+[[package]]
+name = "getrandom"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "globset"
+version = "0.4.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "15f1ce686646e7f1e19bf7d5533fe443a45dbfb990e00629110797578b42fb19"
+dependencies = [
+ "aho-corasick",
+ "bstr",
+ "log",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "globwalk"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757"
+dependencies = [
+ "bitflags 2.6.0",
+ "ignore",
+ "walkdir",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+dependencies = [
+ "ahash 0.7.8",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.14.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
+
+[[package]]
+name = "hashbrown"
+version = "0.15.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
+
+[[package]]
+name = "heck"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
+
+[[package]]
+name = "hermit-abi"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "iana-time-zone"
+version = "0.1.61"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220"
+dependencies = [
+ "android_system_properties",
+ "core-foundation-sys",
+ "iana-time-zone-haiku",
+ "js-sys",
+ "wasm-bindgen",
+ "windows-core",
+]
+
+[[package]]
+name = "iana-time-zone-haiku"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "ignore"
+version = "0.4.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d89fd380afde86567dfba715db065673989d6253f42b88179abd3eae47bda4b"
+dependencies = [
+ "crossbeam-deque",
+ "globset",
+ "log",
+ "memchr",
+ "regex-automata",
+ "same-file",
+ "walkdir",
+ "winapi-util",
+]
+
+[[package]]
+name = "indexmap"
+version = "1.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
+dependencies = [
+ "autocfg",
+ "hashbrown 0.12.3",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.15.2",
+ "serde",
+]
+
+[[package]]
+name = "indoc"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa799dd5ed20a7e349f3b4639aa80d74549c81716d9ec4f994c9b5815598306"
+
+[[package]]
+name = "itertools"
+version = "0.10.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itertools"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
+
+[[package]]
+name = "jemalloc-sys"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d3b9f3f5c9b31aa0f5ed3260385ac205db665baa41d49bb8338008ae94ede45"
+dependencies = [
+ "cc",
+ "fs_extra",
+ "libc",
+]
+
+[[package]]
+name = "jemallocator"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43ae63fcfc45e99ab3d1b29a46782ad679e98436c3169d15a167a1108a724b69"
+dependencies = [
+ "jemalloc-sys",
+ "libc",
+]
+
+[[package]]
+name = "js-sys"
+version = "0.3.76"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7"
+dependencies = [
+ "once_cell",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+
+[[package]]
+name = "libc"
+version = "0.2.169"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
+
+[[package]]
+name = "libloading"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
+dependencies = [
+ "cfg-if",
+ "windows-targets",
+]
+
+[[package]]
+name = "lightningcss"
+version = "1.0.0-alpha.66"
+dependencies = [
+ "ahash 0.8.11",
+ "assert_cmd",
+ "assert_fs",
+ "atty",
+ "bitflags 2.6.0",
+ "browserslist-rs 0.18.1",
+ "clap",
+ "const-str",
+ "cssparser",
+ "cssparser-color",
+ "dashmap",
+ "data-encoding",
+ "getrandom",
+ "indexmap 2.7.0",
+ "indoc",
+ "itertools 0.10.5",
+ "jemallocator",
+ "lazy_static",
+ "lightningcss-derive",
+ "parcel_selectors",
+ "parcel_sourcemap",
+ "paste",
+ "pathdiff",
+ "predicates 2.1.5",
+ "pretty_assertions",
+ "rayon",
+ "schemars",
+ "serde",
+ "serde_json",
+ "smallvec",
+ "static-self",
+]
+
+[[package]]
+name = "lightningcss-derive"
+version = "1.0.0-alpha.43"
+dependencies = [
+ "convert_case",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "lightningcss-napi"
+version = "0.4.4"
+dependencies = [
+ "crossbeam-channel",
+ "cssparser",
+ "lightningcss",
+ "napi",
+ "parcel_sourcemap",
+ "rayon",
+ "serde",
+ "serde-detach",
+ "serde_bytes",
+ "smallvec",
+]
+
+[[package]]
+name = "lightningcss_c_bindings"
+version = "0.1.0"
+dependencies = [
+ "browserslist-rs 0.17.0",
+ "cbindgen",
+ "lightningcss",
+ "parcel_sourcemap",
+]
+
+[[package]]
+name = "lightningcss_node"
+version = "0.1.0"
+dependencies = [
+ "jemallocator",
+ "lightningcss-napi",
+ "napi",
+ "napi-build",
+ "napi-derive",
+]
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
+
+[[package]]
+name = "lock_api"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
+dependencies = [
+ "autocfg",
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
+
+[[package]]
+name = "matches"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
+
+[[package]]
+name = "memchr"
+version = "2.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
+
+[[package]]
+name = "minimal-lexical"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+
+[[package]]
+name = "napi"
+version = "2.16.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "214f07a80874bb96a8433b3cdfc84980d56c7b02e1a0d7ba4ba0db5cef785e2b"
+dependencies = [
+ "bitflags 2.6.0",
+ "ctor",
+ "napi-derive",
+ "napi-sys",
+ "once_cell",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "napi-build"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebd4419172727423cf30351406c54f6cc1b354a2cfb4f1dba3e6cd07f6d5522b"
+
+[[package]]
+name = "napi-derive"
+version = "2.16.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7cbe2585d8ac223f7d34f13701434b9d5f4eb9c332cccce8dee57ea18ab8ab0c"
+dependencies = [
+ "cfg-if",
+ "convert_case",
+ "napi-derive-backend",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.90",
+]
+
+[[package]]
+name = "napi-derive-backend"
+version = "1.0.75"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1639aaa9eeb76e91c6ae66da8ce3e89e921cd3885e99ec85f4abacae72fc91bf"
+dependencies = [
+ "convert_case",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "regex",
+ "semver",
+ "syn 2.0.90",
+]
+
+[[package]]
+name = "napi-sys"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "427802e8ec3a734331fec1035594a210ce1ff4dc5bc1950530920ab717964ea3"
+dependencies = [
+ "libloading",
+]
+
+[[package]]
+name = "nom"
+version = "7.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
+dependencies = [
+ "memchr",
+ "minimal-lexical",
+]
+
+[[package]]
+name = "normalize-line-endings"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be"
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.20.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
+
+[[package]]
+name = "os_str_bytes"
+version = "6.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1"
+
+[[package]]
+name = "outref"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f222829ae9293e33a9f5e9f440c6760a3d450a64affe1846486b140db81c1f4"
+
+[[package]]
+name = "parcel_selectors"
+version = "0.28.2"
+dependencies = [
+ "bitflags 2.6.0",
+ "cssparser",
+ "log",
+ "phf",
+ "phf_codegen",
+ "precomputed-hash",
+ "rustc-hash",
+ "schemars",
+ "serde",
+ "smallvec",
+ "static-self",
+]
+
+[[package]]
+name = "parcel_sourcemap"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "485b74d7218068b2b7c0e3ff12fbc61ae11d57cb5d8224f525bd304c6be05bbb"
+dependencies = [
+ "base64-simd",
+ "data-url",
+ "rkyv",
+ "serde",
+ "serde_json",
+ "vlq",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-targets",
+]
+
+[[package]]
+name = "paste"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
+
+[[package]]
+name = "pathdiff"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
+
+[[package]]
+name = "phf"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc"
+dependencies = [
+ "phf_macros",
+ "phf_shared",
+]
+
+[[package]]
+name = "phf_codegen"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a"
+dependencies = [
+ "phf_generator",
+ "phf_shared",
+]
+
+[[package]]
+name = "phf_generator"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0"
+dependencies = [
+ "phf_shared",
+ "rand",
+]
+
+[[package]]
+name = "phf_macros"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b"
+dependencies = [
+ "phf_generator",
+ "phf_shared",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.90",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b"
+dependencies = [
+ "siphasher",
+]
+
+[[package]]
+name = "precomputed-hash"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
+
+[[package]]
+name = "predicates"
+version = "2.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59230a63c37f3e18569bdb90e4a89cbf5bf8b06fea0b84e65ea10cc4df47addd"
+dependencies = [
+ "difflib",
+ "float-cmp",
+ "itertools 0.10.5",
+ "normalize-line-endings",
+ "predicates-core",
+ "regex",
+]
+
+[[package]]
+name = "predicates"
+version = "3.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573"
+dependencies = [
+ "anstyle",
+ "difflib",
+ "predicates-core",
+]
+
+[[package]]
+name = "predicates-core"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa"
+
+[[package]]
+name = "predicates-tree"
+version = "1.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c"
+dependencies = [
+ "predicates-core",
+ "termtree",
+]
+
+[[package]]
+name = "pretty_assertions"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d"
+dependencies = [
+ "diff",
+ "yansi",
+]
+
+[[package]]
+name = "proc-macro-error"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
+dependencies = [
+ "proc-macro-error-attr",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro-error-attr"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "ptr_meta"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1"
+dependencies = [
+ "ptr_meta_derive",
+]
+
+[[package]]
+name = "ptr_meta_derive"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "radium"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
+
+[[package]]
+name = "rand"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+dependencies = [
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+
+[[package]]
+name = "rayon"
+version = "1.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa"
+dependencies = [
+ "either",
+ "rayon-core",
+]
+
+[[package]]
+name = "rayon-core"
+version = "1.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2"
+dependencies = [
+ "crossbeam-deque",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834"
+dependencies = [
+ "bitflags 2.6.0",
+]
+
+[[package]]
+name = "regex"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
+
+[[package]]
+name = "rend"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c"
+dependencies = [
+ "bytecheck",
+]
+
+[[package]]
+name = "rkyv"
+version = "0.7.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b"
+dependencies = [
+ "bitvec",
+ "bytecheck",
+ "bytes",
+ "hashbrown 0.12.3",
+ "ptr_meta",
+ "rend",
+ "rkyv_derive",
+ "seahash",
+ "tinyvec",
+ "uuid",
+]
+
+[[package]]
+name = "rkyv_derive"
+version = "0.7.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "rustc-hash"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7fb8039b3032c191086b10f11f319a6e99e1e82889c5cc6046f515c9db1d497"
+
+[[package]]
+name = "rustix"
+version = "0.38.42"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85"
+dependencies = [
+ "bitflags 2.6.0",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
+
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "schemars"
+version = "0.8.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09c024468a378b7e36765cd36702b7a90cc3cba11654f6685c8f233408e89e92"
+dependencies = [
+ "dyn-clone",
+ "indexmap 2.7.0",
+ "schemars_derive",
+ "serde",
+ "serde_json",
+ "smallvec",
+]
+
+[[package]]
+name = "schemars_derive"
+version = "0.8.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1eee588578aff73f856ab961cd2f79e36bc45d7ded33a7562adba4667aecc0e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "serde_derive_internals",
+ "syn 2.0.90",
+]
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "seahash"
+version = "4.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
+
+[[package]]
+name = "semver"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba"
+
+[[package]]
+name = "serde"
+version = "1.0.216"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde-detach"
+version = "0.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c621150da442b6a854bb63c431347bcd4de19219a3e1f06fd744208ded057288"
+dependencies = [
+ "serde",
+ "wyz 0.2.0",
+]
+
+[[package]]
+name = "serde_bytes"
+version = "0.11.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "387cc504cb06bb40a96c8e04e951fe01854cf6bc921053c954e4a606d9675c6a"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.216"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.90",
+]
+
+[[package]]
+name = "serde_derive_internals"
+version = "0.29.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.90",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.133"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377"
+dependencies = [
+ "itoa",
+ "memchr",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "shlex"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+
+[[package]]
+name = "simd-abstraction"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9cadb29c57caadc51ff8346233b5cec1d240b68ce55cf1afc764818791876987"
+dependencies = [
+ "outref",
+]
+
+[[package]]
+name = "simdutf8"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
+
+[[package]]
+name = "siphasher"
+version = "0.3.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"
+
+[[package]]
+name = "smallvec"
+version = "1.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "static-self"
+version = "0.1.2"
+dependencies = [
+ "indexmap 2.7.0",
+ "smallvec",
+ "static-self-derive",
+]
+
+[[package]]
+name = "static-self-derive"
+version = "0.1.1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "strsim"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
+
+[[package]]
+name = "syn"
+version = "1.0.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.90"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "tap"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
+
+[[package]]
+name = "tempfile"
+version = "3.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c"
+dependencies = [
+ "cfg-if",
+ "fastrand",
+ "once_cell",
+ "rustix",
+ "windows-sys",
+]
+
+[[package]]
+name = "termcolor"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "termtree"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683"
+
+[[package]]
+name = "textwrap"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9"
+
+[[package]]
+name = "thiserror"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.90",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "022db8904dfa342efe721985167e9fcd16c29b226db4397ed752a761cfce81e8"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+
+[[package]]
+name = "toml"
+version = "0.5.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83"
+
+[[package]]
+name = "unicode-segmentation"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
+
+[[package]]
+name = "uuid"
+version = "1.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a"
+
+[[package]]
+name = "version_check"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
+[[package]]
+name = "vlq"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "65dd7eed29412da847b0f78bcec0ac98588165988a8cfe41d4ea1d429f8ccfff"
+
+[[package]]
+name = "wait-timeout"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "walkdir"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
+dependencies = [
+ "same-file",
+ "winapi-util",
+]
+
+[[package]]
+name = "wasi"
+version = "0.11.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.99"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.99"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79"
+dependencies = [
+ "bumpalo",
+ "log",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.90",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.99"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.99"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.90",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.99"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6"
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
+dependencies = [
+ "windows-sys",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows-core"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.59.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "wyz"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214"
+
+[[package]]
+name = "wyz"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed"
+dependencies = [
+ "tap",
+]
+
+[[package]]
+name = "yansi"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
+
+[[package]]
+name = "zerocopy"
+version = "0.7.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.7.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.90",
+]
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..c3fa888
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,119 @@
+[workspace]
+members = [
+  "node",
+  "napi",
+  "selectors",
+  "c",
+  "derive",
+  "static-self",
+  "static-self-derive",
+]
+
+[package]
+authors = ["Devon Govett <devongovett@gmail.com>"]
+name = "lightningcss"
+version = "1.0.0-alpha.66"
+description = "A CSS parser, transformer, and minifier"
+license = "MPL-2.0"
+edition = "2021"
+keywords = ["CSS", "minifier", "Parcel"]
+repository = "https://github.com/parcel-bundler/lightningcss"
+
+[package.metadata.docs.rs]
+all-features = true
+rustdoc-args = ["--cfg", "docsrs"]
+
+[[bin]]
+name = "lightningcss"
+path = "src/main.rs"
+required-features = ["cli"]
+
+[lib]
+name = "lightningcss"
+path = "src/lib.rs"
+crate-type = ["rlib"]
+
+[features]
+default = ["bundler", "nodejs", "sourcemap"]
+browserslist = ["browserslist-rs"]
+bundler = ["dashmap", "sourcemap", "rayon"]
+cli = ["atty", "clap", "serde_json", "browserslist", "jemallocator"]
+jsonschema = ["schemars", "serde", "parcel_selectors/jsonschema"]
+nodejs = ["dep:serde"]
+serde = [
+  "dep:serde",
+  "smallvec/serde",
+  "cssparser/serde",
+  "parcel_selectors/serde",
+  "into_owned",
+]
+sourcemap = ["parcel_sourcemap"]
+visitor = []
+into_owned = [
+  "static-self",
+  "static-self/smallvec",
+  "static-self/indexmap",
+  "parcel_selectors/into_owned",
+]
+substitute_variables = ["visitor", "into_owned"]
+
+[dependencies]
+serde = { version = "1.0.201", features = ["derive"], optional = true }
+cssparser = "0.33.0"
+cssparser-color = "0.1.0"
+parcel_selectors = { version = "0.28.2", path = "./selectors" }
+itertools = "0.10.1"
+smallvec = { version = "1.7.0", features = ["union"] }
+bitflags = "2.2.1"
+parcel_sourcemap = { version = "2.1.1", features = ["json"], optional = true }
+data-encoding = "2.3.2"
+lazy_static = "1.4.0"
+const-str = "0.3.1"
+pathdiff = "0.2.1"
+ahash = "0.8.7"
+paste = "1.0.12"
+indexmap = { version = "2.2.6", features = ["serde"] }
+# CLI deps
+atty = { version = "0.2", optional = true }
+clap = { version = "3.0.6", features = ["derive"], optional = true }
+browserslist-rs = { version = "0.18.1", optional = true }
+rayon = { version = "1.5.1", optional = true }
+dashmap = { version = "5.0.0", optional = true }
+serde_json = { version = "1.0.78", optional = true }
+lightningcss-derive = { version = "=1.0.0-alpha.43", path = "./derive" }
+schemars = { version = "0.8.19", features = ["smallvec", "indexmap2"], optional = true }
+static-self = { version = "0.1.2", path = "static-self", optional = true }
+
+[target.'cfg(target_os = "macos")'.dependencies]
+jemallocator = { version = "0.3.2", features = [
+  "disable_initial_exec_tls",
+], optional = true }
+
+[target.'cfg(target_arch = "wasm32")'.dependencies]
+getrandom = { version = "0.2", features = ["custom"], default-features = false }
+
+[dev-dependencies]
+indoc = "1.0.3"
+assert_cmd = "2.0"
+assert_fs = "1.0"
+predicates = "2.1"
+serde_json = "1"
+pretty_assertions = "1.4.0"
+
+[[test]]
+name = "cli_integration_tests"
+path = "tests/cli_integration_tests.rs"
+required-features = ["cli"]
+
+[[example]]
+name = "custom_at_rule"
+required-features = ["visitor"]
+
+[[example]]
+name = "serialize"
+required-features = ["serde"]
+
+[profile.release]
+lto = true
+codegen-units = 1
+panic = 'abort'
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..89fe5b2
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,373 @@
+ Mozilla Public License Version 2.0
+==================================
+
+1. Definitions
+--------------
+
+1.1. "Contributor"
+means each individual or legal entity that creates, contributes to
+the creation of, or owns Covered Software.
+
+1.2. "Contributor Version"
+means the combination of the Contributions of others (if any) used
+by a Contributor and that particular Contributor's Contribution.
+
+1.3. "Contribution"
+means Covered Software of a particular Contributor.
+
+1.4. "Covered Software"
+means Source Code Form to which the initial Contributor has attached
+the notice in Exhibit A, the Executable Form of such Source Code
+Form, and Modifications of such Source Code Form, in each case
+including portions thereof.
+
+1.5. "Incompatible With Secondary Licenses"
+means
+
+(a) that the initial Contributor has attached the notice described
+in Exhibit B to the Covered Software; or
+
+(b) that the Covered Software was made available under the terms of
+version 1.1 or earlier of the License, but not also under the
+terms of a Secondary License.
+
+1.6. "Executable Form"
+means any form of the work other than Source Code Form.
+
+1.7. "Larger Work"
+means a work that combines Covered Software with other material, in
+a separate file or files, that is not Covered Software.
+
+1.8. "License"
+means this document.
+
+1.9. "Licensable"
+means having the right to grant, to the maximum extent possible,
+whether at the time of the initial grant or subsequently, any and
+all of the rights conveyed by this License.
+
+1.10. "Modifications"
+means any of the following:
+
+(a) any file in Source Code Form that results from an addition to,
+deletion from, or modification of the contents of Covered
+Software; or
+
+(b) any new file in Source Code Form that contains any Covered
+Software.
+
+1.11. "Patent Claims" of a Contributor
+means any patent claim(s), including without limitation, method,
+process, and apparatus claims, in any patent Licensable by such
+Contributor that would be infringed, but for the grant of the
+License, by the making, using, selling, offering for sale, having
+made, import, or transfer of either its Contributions or its
+Contributor Version.
+
+1.12. "Secondary License"
+means either the GNU General Public License, Version 2.0, the GNU
+Lesser General Public License, Version 2.1, the GNU Affero General
+Public License, Version 3.0, or any later versions of those
+licenses.
+
+1.13. "Source Code Form"
+means the form of the work preferred for making modifications.
+
+1.14. "You" (or "Your")
+means an individual or a legal entity exercising rights under this
+License. For legal entities, "You" includes any entity that
+controls, is controlled by, or is under common control with You. For
+purposes of this definition, "control" means (a) the power, direct
+or indirect, to cause the direction or management of such entity,
+whether by contract or otherwise, or (b) ownership of more than
+fifty percent (50%) of the outstanding shares or beneficial
+ownership of such entity.
+
+2. License Grants and Conditions
+--------------------------------
+
+2.1. Grants
+
+Each Contributor hereby grants You a world-wide, royalty-free,
+non-exclusive license:
+
+(a) under intellectual property rights (other than patent or trademark)
+Licensable by such Contributor to use, reproduce, make available,
+modify, display, perform, distribute, and otherwise exploit its
+Contributions, either on an unmodified basis, with Modifications, or
+as part of a Larger Work; and
+
+(b) under Patent Claims of such Contributor to make, use, sell, offer
+for sale, have made, import, and otherwise transfer either its
+Contributions or its Contributor Version.
+
+2.2. Effective Date
+
+The licenses granted in Section 2.1 with respect to any Contribution
+become effective for each Contribution on the date the Contributor first
+distributes such Contribution.
+
+2.3. Limitations on Grant Scope
+
+The licenses granted in this Section 2 are the only rights granted under
+this License. No additional rights or licenses will be implied from the
+distribution or licensing of Covered Software under this License.
+Notwithstanding Section 2.1(b) above, no patent license is granted by a
+Contributor:
+
+(a) for any code that a Contributor has removed from Covered Software;
+or
+
+(b) for infringements caused by: (i) Your and any other third party's
+modifications of Covered Software, or (ii) the combination of its
+Contributions with other software (except as part of its Contributor
+Version); or
+
+(c) under Patent Claims infringed by Covered Software in the absence of
+its Contributions.
+
+This License does not grant any rights in the trademarks, service marks,
+or logos of any Contributor (except as may be necessary to comply with
+the notice requirements in Section 3.4).
+
+2.4. Subsequent Licenses
+
+No Contributor makes additional grants as a result of Your choice to
+distribute the Covered Software under a subsequent version of this
+License (see Section 10.2) or under the terms of a Secondary License (if
+permitted under the terms of Section 3.3).
+
+2.5. Representation
+
+Each Contributor represents that the Contributor believes its
+Contributions are its original creation(s) or it has sufficient rights
+to grant the rights to its Contributions conveyed by this License.
+
+2.6. Fair Use
+
+This License is not intended to limit any rights You have under
+applicable copyright doctrines of fair use, fair dealing, or other
+equivalents.
+
+2.7. Conditions
+
+Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
+in Section 2.1.
+
+3. Responsibilities
+-------------------
+
+3.1. Distribution of Source Form
+
+All distribution of Covered Software in Source Code Form, including any
+Modifications that You create or to which You contribute, must be under
+the terms of this License. You must inform recipients that the Source
+Code Form of the Covered Software is governed by the terms of this
+License, and how they can obtain a copy of this License. You may not
+attempt to alter or restrict the recipients' rights in the Source Code
+Form.
+
+3.2. Distribution of Executable Form
+
+If You distribute Covered Software in Executable Form then:
+
+(a) such Covered Software must also be made available in Source Code
+Form, as described in Section 3.1, and You must inform recipients of
+the Executable Form how they can obtain a copy of such Source Code
+Form by reasonable means in a timely manner, at a charge no more
+than the cost of distribution to the recipient; and
+
+(b) You may distribute such Executable Form under the terms of this
+License, or sublicense it under different terms, provided that the
+license for the Executable Form does not attempt to limit or alter
+the recipients' rights in the Source Code Form under this License.
+
+3.3. Distribution of a Larger Work
+
+You may create and distribute a Larger Work under terms of Your choice,
+provided that You also comply with the requirements of this License for
+the Covered Software. If the Larger Work is a combination of Covered
+Software with a work governed by one or more Secondary Licenses, and the
+Covered Software is not Incompatible With Secondary Licenses, this
+License permits You to additionally distribute such Covered Software
+under the terms of such Secondary License(s), so that the recipient of
+the Larger Work may, at their option, further distribute the Covered
+Software under the terms of either this License or such Secondary
+License(s).
+
+3.4. Notices
+
+You may not remove or alter the substance of any license notices
+(including copyright notices, patent notices, disclaimers of warranty,
+or limitations of liability) contained within the Source Code Form of
+the Covered Software, except that You may alter any license notices to
+the extent required to remedy known factual inaccuracies.
+
+3.5. Application of Additional Terms
+
+You may choose to offer, and to charge a fee for, warranty, support,
+indemnity or liability obligations to one or more recipients of Covered
+Software. However, You may do so only on Your own behalf, and not on
+behalf of any Contributor. You must make it absolutely clear that any
+such warranty, support, indemnity, or liability obligation is offered by
+You alone, and You hereby agree to indemnify every Contributor for any
+liability incurred by such Contributor as a result of warranty, support,
+indemnity or liability terms You offer. You may include additional
+disclaimers of warranty and limitations of liability specific to any
+jurisdiction.
+
+4. Inability to Comply Due to Statute or Regulation
+---------------------------------------------------
+
+If it is impossible for You to comply with any of the terms of this
+License with respect to some or all of the Covered Software due to
+statute, judicial order, or regulation then You must: (a) comply with
+the terms of this License to the maximum extent possible; and (b)
+describe the limitations and the code they affect. Such description must
+be placed in a text file included with all distributions of the Covered
+Software under this License. Except to the extent prohibited by statute
+or regulation, such description must be sufficiently detailed for a
+recipient of ordinary skill to be able to understand it.
+
+5. Termination
+--------------
+
+5.1. The rights granted under this License will terminate automatically
+if You fail to comply with any of its terms. However, if You become
+compliant, then the rights granted under this License from a particular
+Contributor are reinstated (a) provisionally, unless and until such
+Contributor explicitly and finally terminates Your grants, and (b) on an
+ongoing basis, if such Contributor fails to notify You of the
+non-compliance by some reasonable means prior to 60 days after You have
+come back into compliance. Moreover, Your grants from a particular
+Contributor are reinstated on an ongoing basis if such Contributor
+notifies You of the non-compliance by some reasonable means, this is the
+first time You have received notice of non-compliance with this License
+from such Contributor, and You become compliant prior to 30 days after
+Your receipt of the notice.
+
+5.2. If You initiate litigation against any entity by asserting a patent
+infringement claim (excluding declaratory judgment actions,
+counter-claims, and cross-claims) alleging that a Contributor Version
+directly or indirectly infringes any patent, then the rights granted to
+You by any and all Contributors for the Covered Software under Section
+2.1 of this License shall terminate.
+
+5.3. In the event of termination under Sections 5.1 or 5.2 above, all
+end user license agreements (excluding distributors and resellers) which
+have been validly granted by You or Your distributors under this License
+prior to termination shall survive termination.
+
+************************************************************************
+* *
+* 6. Disclaimer of Warranty *
+* ------------------------- *
+* *
+* Covered Software is provided under this License on an "as is" *
+* basis, without warranty of any kind, either expressed, implied, or *
+* statutory, including, without limitation, warranties that the *
+* Covered Software is free of defects, merchantable, fit for a *
+* particular purpose or non-infringing. The entire risk as to the *
+* quality and performance of the Covered Software is with You. *
+* Should any Covered Software prove defective in any respect, You *
+* (not any Contributor) assume the cost of any necessary servicing, *
+* repair, or correction. This disclaimer of warranty constitutes an *
+* essential part of this License. No use of any Covered Software is *
+* authorized under this License except under this disclaimer. *
+* *
+************************************************************************
+
+************************************************************************
+* *
+* 7. Limitation of Liability *
+* -------------------------- *
+* *
+* Under no circumstances and under no legal theory, whether tort *
+* (including negligence), contract, or otherwise, shall any *
+* Contributor, or anyone who distributes Covered Software as *
+* permitted above, be liable to You for any direct, indirect, *
+* special, incidental, or consequential damages of any character *
+* including, without limitation, damages for lost profits, loss of *
+* goodwill, work stoppage, computer failure or malfunction, or any *
+* and all other commercial damages or losses, even if such party *
+* shall have been informed of the possibility of such damages. This *
+* limitation of liability shall not apply to liability for death or *
+* personal injury resulting from such party's negligence to the *
+* extent applicable law prohibits such limitation. Some *
+* jurisdictions do not allow the exclusion or limitation of *
+* incidental or consequential damages, so this exclusion and *
+* limitation may not apply to You. *
+* *
+************************************************************************
+
+8. Litigation
+-------------
+
+Any litigation relating to this License may be brought only in the
+courts of a jurisdiction where the defendant maintains its principal
+place of business and such litigation shall be governed by laws of that
+jurisdiction, without reference to its conflict-of-law provisions.
+Nothing in this Section shall prevent a party's ability to bring
+cross-claims or counter-claims.
+
+9. Miscellaneous
+----------------
+
+This License represents the complete agreement concerning the subject
+matter hereof. If any provision of this License is held to be
+unenforceable, such provision shall be reformed only to the extent
+necessary to make it enforceable. Any law or regulation which provides
+that the language of a contract shall be construed against the drafter
+shall not be used to construe this License against a Contributor.
+
+10. Versions of the License
+---------------------------
+
+10.1. New Versions
+
+Mozilla Foundation is the license steward. Except as provided in Section
+10.3, no one other than the license steward has the right to modify or
+publish new versions of this License. Each version will be given a
+distinguishing version number.
+
+10.2. Effect of New Versions
+
+You may distribute the Covered Software under the terms of the version
+of the License under which You originally received the Covered Software,
+or under the terms of any subsequent version published by the license
+steward.
+
+10.3. Modified Versions
+
+If you create software not governed by this License, and you want to
+create a new license for such software, you may create and use a
+modified version of this License if you rename the license and remove
+any references to the name of the license steward (except to note that
+such modified license differs from this License).
+
+10.4. Distributing Source Code Form that is Incompatible With Secondary
+Licenses
+
+If You choose to distribute Source Code Form that is Incompatible With
+Secondary Licenses under the terms of this version of the License, the
+notice described in Exhibit B of this License must be attached.
+
+Exhibit A - Source Code Form License Notice
+-------------------------------------------
+
+This Source Code Form is subject to the terms of the Mozilla Public
+License, v. 2.0. If a copy of the MPL was not distributed with this
+file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+If it is not possible or desirable to put the notice in a particular
+file, then You may include the notice in a location (such as a LICENSE
+file in a relevant directory) where a recipient would be likely to look
+for such a notice.
+
+You may add additional accurate notices of copyright ownership.
+
+Exhibit B - "Incompatible With Secondary Licenses" Notice
+---------------------------------------------------------
+
+This Source Code Form is "Incompatible With Secondary Licenses", as
+defined by the Mozilla Public License, v. 2.0.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..f44d7b3
--- /dev/null
+++ b/README.md
@@ -0,0 +1,105 @@
+# ⚡️ Lightning CSS
+
+An extremely fast CSS parser, transformer, and minifier written in Rust. Use it with [Parcel](https://parceljs.org), as a standalone library or CLI, or via a plugin with any other tool.
+
+<img width="680" alt="performance and build size charts" src="https://user-images.githubusercontent.com/19409/189022599-28246659-f94a-46a4-9de0-b6d17adb0e22.png#gh-light-mode-only">
+<img width="680" alt="performance and build size charts" src="https://user-images.githubusercontent.com/19409/189022693-6956b044-422b-4f56-9628-d59c6f791095.png#gh-dark-mode-only">
+
+## Features
+
+- **Extremely fast** – Parsing and minifying large files is completed in milliseconds, often with significantly smaller output than other tools. See [benchmarks](#benchmarks) below.
+- **Typed property values** – many other CSS parsers treat property values as an untyped series of tokens. This means that each transformer that wants to do something with these values must interpret them itself, leading to duplicate work and inconsistencies. Lightning CSS parses all values using the grammar from the CSS specification, and exposes a specific value type for each property.
+- **Browser-grade parser** – Lightning CSS is built on the [cssparser](https://github.com/servo/rust-cssparser) and [selectors](https://github.com/servo/stylo/tree/main/selectors) crates created by Mozilla and used by Firefox and Servo. These provide a solid general purpose CSS-parsing foundation on top of which Lightning CSS implements support for all specific CSS rules and properties.
+- **Minification** – One of the main purposes of Lightning CSS is to minify CSS to make it smaller. This includes many optimizations including:
+  - Combining longhand properties into shorthands where possible.
+  - Merging adjacent rules with the same selectors or declarations when it is safe to do so.
+  - Combining CSS transforms into a single matrix or vice versa when smaller.
+  - Removing vendor prefixes that are not needed, based on the provided browser targets.
+  - Reducing `calc()` expressions where possible.
+  - Converting colors to shorter hex notation where possible.
+  - Minifying gradients.
+  - Minifying CSS grid templates.
+  - Normalizing property value order.
+  - Removing default property sub-values which will be inferred by browsers.
+  - Many micro-optimizations, e.g. converting to shorter units, removing unnecessary quotation marks, etc.
+- **Vendor prefixing** – Lightning CSS accepts a list of browser targets, and automatically adds (and removes) vendor prefixes.
+- **Browserslist configuration** – Lightning CSS supports opt-in browserslist configuration discovery to resolve browser targets and integrate with your existing tools and config setup.
+- **Syntax lowering** – Lightning CSS parses modern CSS syntax, and generates more compatible output where needed, based on browser targets.
+  - CSS Nesting
+  - Custom media queries (draft spec)
+  - Logical properties
+  * [Color Level 5](https://drafts.csswg.org/css-color-5/)
+    - `color-mix()` function
+    - Relative color syntax, e.g. `lab(from purple calc(l * .8) a b)`
+  - [Color Level 4](https://drafts.csswg.org/css-color-4/)
+    - `lab()`, `lch()`, `oklab()`, and `oklch()` colors
+    - `color()` function supporting predefined color spaces such as `display-p3` and `xyz`
+    - Space separated components in `rgb` and `hsl` functions
+    - Hex with alpha syntax
+    - `hwb()` color syntax
+    - Percent syntax for opacity
+    - `#rgba` and `#rrggbbaa` hex colors
+  - Selectors
+    - `:not` with multiple arguments
+    - `:lang` with multiple arguments
+    - `:dir`
+    - `:is`
+  - Double position gradient stops (e.g. `red 40% 80%`)
+  - `clamp()`, `round()`, `rem()`, and `mod()` math functions
+  - Alignment shorthands (e.g. `place-items`)
+  - Two-value `overflow` shorthand
+  - Media query range syntax (e.g. `@media (width <= 100px)` or `@media (100px < width < 500px)`)
+  - Multi-value `display` property (e.g. `inline flex`)
+  - `system-ui` font family fallbacks
+- **CSS modules** – Lightning CSS supports compiling a subset of [CSS modules](https://github.com/css-modules/css-modules) features.
+  - Locally scoped class and id selectors
+  - Locally scoped custom identifiers, e.g. `@keyframes` names, grid lines/areas, `@counter-style` names, etc.
+  - Opt-in support for locally scoped CSS variables and other dashed identifiers.
+  - `:local()` and `:global()` selectors
+  - The `composes` property
+- **Custom transforms** – The Lightning CSS visitor API can be used to implement custom transform plugins.
+
+## Documentation
+
+Lightning CSS can be used from [Parcel](https://parceljs.org), as a standalone library from JavaScript or Rust, using a standalone CLI, or wrapped as a plugin within any other tool. See the [Lightning CSS website](https://lightningcss.dev/docs.html) for documentation.
+
+## Benchmarks
+
+<img width="680" alt="performance and build size charts" src="https://user-images.githubusercontent.com/19409/189022599-28246659-f94a-46a4-9de0-b6d17adb0e22.png#gh-light-mode-only">
+<img width="680" alt="performance and build size charts" src="https://user-images.githubusercontent.com/19409/189022693-6956b044-422b-4f56-9628-d59c6f791095.png#gh-dark-mode-only">
+
+```
+$ node bench.js bootstrap-4.css
+cssnano: 544.809ms
+159636 bytes
+
+esbuild: 17.199ms
+160332 bytes
+
+lightningcss: 4.16ms
+143091 bytes
+
+
+$ node bench.js animate.css
+cssnano: 283.105ms
+71723 bytes
+
+esbuild: 11.858ms
+72183 bytes
+
+lightningcss: 1.973ms
+23666 bytes
+
+
+$ node bench.js tailwind.css
+cssnano: 2.198s
+1925626 bytes
+
+esbuild: 107.668ms
+1961642 bytes
+
+lightningcss: 43.368ms
+1824130 bytes
+```
+
+For more benchmarks comparing more tools and input, see [here](http://goalsmashers.github.io/css-minification-benchmark/). Note that some of the tools shown perform unsafe optimizations that may change the behavior of the original CSS in favor of smaller file size. Lightning CSS does not do this – the output CSS should always behave identically to the input. Keep this in mind when comparing file sizes between tools.
diff --git a/bench.js b/bench.js
new file mode 100644
index 0000000..f2f6107
--- /dev/null
+++ b/bench.js
@@ -0,0 +1,45 @@
+const css = require('./');
+const cssnano = require('cssnano');
+const postcss = require('postcss');
+const esbuild = require('esbuild');
+
+let opts = {
+  filename: process.argv[process.argv.length - 1],
+  code: require('fs').readFileSync(process.argv[process.argv.length - 1]),
+  minify: true,
+  // source_map: true,
+  targets: {
+    chrome: 95 << 16
+  }
+};
+
+async function run() {
+  await doCssNano();
+
+  console.time('esbuild');
+  let r = await esbuild.transform(opts.code.toString(), {
+    sourcefile: opts.filename,
+    loader: 'css',
+    minify: true
+  });
+  console.timeEnd('esbuild');
+  console.log(r.code.length + ' bytes');
+  console.log('');
+
+  console.time('lightningcss');
+  let res = css.transform(opts);
+  console.timeEnd('lightningcss');
+  console.log(res.code.length + ' bytes');
+}
+
+async function doCssNano() {
+  console.time('cssnano');
+  const result = await postcss([
+    cssnano,
+  ]).process(opts.code, {from: opts.filename});
+  console.timeEnd('cssnano');
+  console.log(result.css.length + ' bytes');
+  console.log('');
+}
+
+run();
diff --git a/c/Cargo.toml b/c/Cargo.toml
new file mode 100644
index 0000000..3d20add
--- /dev/null
+++ b/c/Cargo.toml
@@ -0,0 +1,17 @@
+[package]
+authors = ["Devon Govett <devongovett@gmail.com>"]
+name = "lightningcss_c_bindings"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+crate-type = ["cdylib"]
+
+[dependencies]
+lightningcss = { path = "../", features = ["browserslist"] }
+parcel_sourcemap = { version = "2.1.1", features = ["json"] }
+browserslist-rs = { version = "0.17.0" }
+
+[build-dependencies]
+cbindgen = "0.24.3"
diff --git a/c/build.rs b/c/build.rs
new file mode 100644
index 0000000..02fcb75
--- /dev/null
+++ b/c/build.rs
@@ -0,0 +1,9 @@
+use std::env;
+
+fn main() {
+  let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
+
+  cbindgen::generate(crate_dir)
+    .expect("Unable to generate bindings")
+    .write_to_file("lightningcss.h");
+}
diff --git a/c/cbindgen.toml b/c/cbindgen.toml
new file mode 100644
index 0000000..e7c40a6
--- /dev/null
+++ b/c/cbindgen.toml
@@ -0,0 +1,11 @@
+language = "C"
+
+[parse]
+parse_deps = false
+include = ["lightningcss"]
+
+[export.rename]
+StyleSheetWrapper = "StyleSheet"
+
+[enum]
+prefix_with_name = true
diff --git a/c/lightningcss.h b/c/lightningcss.h
new file mode 100644
index 0000000..8f91505
--- /dev/null
+++ b/c/lightningcss.h
@@ -0,0 +1,160 @@
+#include <stdarg.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdlib.h>
+
+typedef struct CssError CssError;
+
+typedef struct StyleSheet StyleSheet;
+
+typedef struct Targets {
+  uint32_t android;
+  uint32_t chrome;
+  uint32_t edge;
+  uint32_t firefox;
+  uint32_t ie;
+  uint32_t ios_saf;
+  uint32_t opera;
+  uint32_t safari;
+  uint32_t samsung;
+} Targets;
+
+typedef struct ParseOptions {
+  const char *filename;
+  bool nesting;
+  bool custom_media;
+  bool css_modules;
+  const char *css_modules_pattern;
+  bool css_modules_dashed_idents;
+  bool error_recovery;
+} ParseOptions;
+
+typedef struct TransformOptions {
+  struct Targets targets;
+  char **unused_symbols;
+  uintptr_t unused_symbols_len;
+} TransformOptions;
+
+typedef struct RawString {
+  char *text;
+  uintptr_t len;
+} RawString;
+
+typedef enum CssModuleReference_Tag {
+  /**
+   * A local reference.
+   */
+  CssModuleReference_Local,
+  /**
+   * A global reference.
+   */
+  CssModuleReference_Global,
+  /**
+   * A reference to an export in a different file.
+   */
+  CssModuleReference_Dependency,
+} CssModuleReference_Tag;
+
+typedef struct CssModuleReference_Local_Body {
+  /**
+   * The local (compiled) name for the reference.
+   */
+  struct RawString name;
+} CssModuleReference_Local_Body;
+
+typedef struct CssModuleReference_Global_Body {
+  /**
+   * The referenced global name.
+   */
+  struct RawString name;
+} CssModuleReference_Global_Body;
+
+typedef struct CssModuleReference_Dependency_Body {
+  /**
+   * The name to reference within the dependency.
+   */
+  struct RawString name;
+  /**
+   * The dependency specifier for the referenced file.
+   */
+  struct RawString specifier;
+} CssModuleReference_Dependency_Body;
+
+typedef struct CssModuleReference {
+  CssModuleReference_Tag tag;
+  union {
+    CssModuleReference_Local_Body local;
+    CssModuleReference_Global_Body global;
+    CssModuleReference_Dependency_Body dependency;
+  };
+} CssModuleReference;
+
+typedef struct CssModuleExport {
+  struct RawString exported;
+  struct RawString local;
+  bool is_referenced;
+  struct CssModuleReference *composes;
+  uintptr_t composes_len;
+} CssModuleExport;
+
+typedef struct CssModulePlaceholder {
+  struct RawString placeholder;
+  struct CssModuleReference reference;
+} CssModulePlaceholder;
+
+typedef struct ToCssResult {
+  struct RawString code;
+  struct RawString map;
+  struct CssModuleExport *exports;
+  uintptr_t exports_len;
+  struct CssModulePlaceholder *references;
+  uintptr_t references_len;
+} ToCssResult;
+
+typedef struct PseudoClasses {
+  const char *hover;
+  const char *active;
+  const char *focus;
+  const char *focus_visible;
+  const char *focus_within;
+} PseudoClasses;
+
+typedef struct ToCssOptions {
+  bool minify;
+  bool source_map;
+  const char *input_source_map;
+  uintptr_t input_source_map_len;
+  const char *project_root;
+  struct Targets targets;
+  bool analyze_dependencies;
+  struct PseudoClasses pseudo_classes;
+} ToCssOptions;
+
+bool lightningcss_browserslist_to_targets(const char *query,
+                                          struct Targets *targets,
+                                          struct CssError **error);
+
+struct StyleSheet *lightningcss_stylesheet_parse(const char *source,
+                                                 uintptr_t len,
+                                                 struct ParseOptions options,
+                                                 struct CssError **error);
+
+bool lightningcss_stylesheet_transform(struct StyleSheet *stylesheet,
+                                       struct TransformOptions options,
+                                       struct CssError **error);
+
+struct ToCssResult lightningcss_stylesheet_to_css(struct StyleSheet *stylesheet,
+                                                  struct ToCssOptions options,
+                                                  struct CssError **error);
+
+void lightningcss_stylesheet_free(struct StyleSheet *stylesheet);
+
+void lightningcss_to_css_result_free(struct ToCssResult result);
+
+const char *lightningcss_error_message(struct CssError *error);
+
+void lightningcss_error_free(struct CssError *error);
+
+uintptr_t lightningcss_stylesheet_get_warning_count(struct StyleSheet *stylesheet);
+
+const char *lightningcss_stylesheet_get_warning(struct StyleSheet *stylesheet, uintptr_t index);
diff --git a/c/src/lib.rs b/c/src/lib.rs
new file mode 100644
index 0000000..759a18d
--- /dev/null
+++ b/c/src/lib.rs
@@ -0,0 +1,609 @@
+#![allow(clippy::not_unsafe_ptr_arg_deref)]
+
+use std::collections::HashSet;
+use std::ffi::{CStr, CString};
+use std::mem::ManuallyDrop;
+use std::os::raw::c_char;
+use std::sync::{Arc, RwLock};
+
+use lightningcss::css_modules::PatternParseError;
+use lightningcss::error::{Error, MinifyErrorKind, ParserError, PrinterError};
+use lightningcss::stylesheet::{MinifyOptions, ParserFlags, ParserOptions, PrinterOptions, StyleSheet};
+use lightningcss::targets::Browsers;
+use parcel_sourcemap::SourceMap;
+
+pub struct StyleSheetWrapper<'i, 'o> {
+  stylesheet: StyleSheet<'i, 'o>,
+  source: &'i str,
+  warnings: Vec<CssError<'i>>,
+}
+
+pub struct CssError<'i> {
+  kind: ErrorKind<'i>,
+  message: Option<CString>,
+}
+
+impl<'i> CssError<'i> {
+  fn message(&mut self) -> *const c_char {
+    if let Some(message) = &self.message {
+      return message.as_ptr();
+    }
+
+    let string: String = match &self.kind {
+      ErrorKind::ParserError(err) => err.to_string().into(),
+      ErrorKind::MinifyError(err) => err.to_string().into(),
+      ErrorKind::PrinterError(err) => err.to_string().into(),
+      ErrorKind::PatternParseError(err) => err.to_string().into(),
+      ErrorKind::BrowserslistError(err) => err.to_string().into(),
+      ErrorKind::SourceMapError(err) => err.to_string().into(),
+    };
+
+    self.message = Some(CString::new(string).unwrap());
+    self.message.as_ref().unwrap().as_ptr()
+  }
+}
+
+pub enum ErrorKind<'i> {
+  ParserError(Error<ParserError<'i>>),
+  MinifyError(Error<MinifyErrorKind>),
+  PrinterError(PrinterError),
+  PatternParseError(PatternParseError),
+  BrowserslistError(browserslist::Error),
+  SourceMapError(parcel_sourcemap::SourceMapError),
+}
+
+macro_rules! impl_from {
+  ($name: ident, $t: ty) => {
+    impl<'i> From<$t> for CssError<'i> {
+      fn from(err: $t) -> Self {
+        CssError {
+          kind: ErrorKind::$name(err),
+          message: None,
+        }
+      }
+    }
+  };
+}
+
+impl_from!(ParserError, Error<ParserError<'i>>);
+impl_from!(MinifyError, Error<MinifyErrorKind>);
+impl_from!(PrinterError, PrinterError);
+impl_from!(PatternParseError, PatternParseError);
+impl_from!(BrowserslistError, browserslist::Error);
+impl_from!(SourceMapError, parcel_sourcemap::SourceMapError);
+
+#[repr(C)]
+pub struct ParseOptions {
+  filename: *const c_char,
+  nesting: bool,
+  custom_media: bool,
+  css_modules: bool,
+  css_modules_pattern: *const c_char,
+  css_modules_dashed_idents: bool,
+  error_recovery: bool,
+}
+
+#[repr(C)]
+#[derive(Default, PartialEq)]
+pub struct Targets {
+  android: u32,
+  chrome: u32,
+  edge: u32,
+  firefox: u32,
+  ie: u32,
+  ios_saf: u32,
+  opera: u32,
+  safari: u32,
+  samsung: u32,
+}
+
+impl Into<Browsers> for Targets {
+  fn into(self) -> Browsers {
+    macro_rules! browser {
+      ($val: expr) => {
+        if $val > 0 {
+          Some($val)
+        } else {
+          None
+        }
+      };
+    }
+
+    Browsers {
+      android: browser!(self.android),
+      chrome: browser!(self.chrome),
+      edge: browser!(self.edge),
+      firefox: browser!(self.firefox),
+      ie: browser!(self.ie),
+      ios_saf: browser!(self.ios_saf),
+      opera: browser!(self.opera),
+      safari: browser!(self.safari),
+      samsung: browser!(self.samsung),
+    }
+  }
+}
+
+macro_rules! unwrap {
+  ($result: expr, $error: ident, $ret: expr) => {
+    match $result {
+      Ok(v) => v,
+      Err(err) => unsafe {
+        *$error = Box::into_raw(Box::new(err.into()));
+        return $ret;
+      },
+    }
+  };
+}
+
+#[no_mangle]
+pub extern "C" fn lightningcss_browserslist_to_targets(
+  query: *const c_char,
+  targets: *mut Targets,
+  error: *mut *mut CssError,
+) -> bool {
+  let string = unsafe { std::str::from_utf8_unchecked(CStr::from_ptr(query).to_bytes()) };
+  match Browsers::from_browserslist([string]) {
+    Ok(Some(browsers)) => {
+      let targets = unsafe { &mut *targets };
+      targets.android = browsers.android.unwrap_or_default();
+      targets.chrome = browsers.chrome.unwrap_or_default();
+      targets.edge = browsers.edge.unwrap_or_default();
+      targets.firefox = browsers.firefox.unwrap_or_default();
+      targets.ie = browsers.ie.unwrap_or_default();
+      targets.ios_saf = browsers.ios_saf.unwrap_or_default();
+      targets.opera = browsers.opera.unwrap_or_default();
+      targets.safari = browsers.safari.unwrap_or_default();
+      targets.samsung = browsers.samsung.unwrap_or_default();
+      true
+    }
+    Ok(None) => true,
+    Err(err) => unsafe {
+      *error = Box::into_raw(Box::new(err.into()));
+      false
+    },
+  }
+}
+
+#[repr(C)]
+pub struct TransformOptions {
+  targets: Targets,
+  unused_symbols: *mut *mut c_char,
+  unused_symbols_len: usize,
+}
+
+impl Into<MinifyOptions> for TransformOptions {
+  fn into(self) -> MinifyOptions {
+    let mut unused_symbols = HashSet::new();
+    let slice = unsafe { std::slice::from_raw_parts(self.unused_symbols, self.unused_symbols_len) };
+    for symbol in slice {
+      let string = unsafe { std::str::from_utf8_unchecked(CStr::from_ptr(*symbol).to_bytes()).to_owned() };
+      unused_symbols.insert(string);
+    }
+
+    MinifyOptions {
+      targets: if self.targets != Targets::default() {
+        Some(self.targets.into()).into()
+      } else {
+        Default::default()
+      },
+      unused_symbols,
+    }
+  }
+}
+
+#[repr(C)]
+pub struct ToCssOptions {
+  minify: bool,
+  source_map: bool,
+  input_source_map: *const c_char,
+  input_source_map_len: usize,
+  project_root: *const c_char,
+  targets: Targets,
+  analyze_dependencies: bool,
+  pseudo_classes: PseudoClasses,
+}
+
+#[derive(PartialEq)]
+#[repr(C)]
+pub struct PseudoClasses {
+  hover: *const c_char,
+  active: *const c_char,
+  focus: *const c_char,
+  focus_visible: *const c_char,
+  focus_within: *const c_char,
+}
+
+impl Default for PseudoClasses {
+  fn default() -> Self {
+    PseudoClasses {
+      hover: std::ptr::null(),
+      active: std::ptr::null(),
+      focus: std::ptr::null(),
+      focus_visible: std::ptr::null(),
+      focus_within: std::ptr::null(),
+    }
+  }
+}
+
+impl<'a> Into<lightningcss::printer::PseudoClasses<'a>> for PseudoClasses {
+  fn into(self) -> lightningcss::printer::PseudoClasses<'a> {
+    macro_rules! pc {
+      ($ptr: expr) => {
+        if $ptr.is_null() {
+          None
+        } else {
+          Some(unsafe { std::str::from_utf8_unchecked(CStr::from_ptr($ptr).to_bytes()) })
+        }
+      };
+    }
+
+    lightningcss::printer::PseudoClasses {
+      hover: pc!(self.hover),
+      active: pc!(self.active),
+      focus: pc!(self.focus),
+      focus_visible: pc!(self.focus_visible),
+      focus_within: pc!(self.focus_within),
+    }
+  }
+}
+
+#[no_mangle]
+pub extern "C" fn lightningcss_stylesheet_parse(
+  source: *const c_char,
+  len: usize,
+  options: ParseOptions,
+  error: *mut *mut CssError,
+) -> *mut StyleSheetWrapper {
+  let slice = unsafe { std::slice::from_raw_parts(source as *const u8, len) };
+  let code = unsafe { std::str::from_utf8_unchecked(slice) };
+  let warnings = Arc::new(RwLock::new(Vec::new()));
+  let mut flags = ParserFlags::empty();
+  flags.set(ParserFlags::CUSTOM_MEDIA, options.custom_media);
+  let opts = ParserOptions {
+    filename: if options.filename.is_null() {
+      String::new()
+    } else {
+      unsafe { std::str::from_utf8_unchecked(CStr::from_ptr(options.filename).to_bytes()).to_owned() }
+    },
+    flags,
+    css_modules: if options.css_modules {
+      let pattern = if !options.css_modules_pattern.is_null() {
+        let pattern =
+          unsafe { std::str::from_utf8_unchecked(CStr::from_ptr(options.css_modules_pattern).to_bytes()) };
+        unwrap!(
+          lightningcss::css_modules::Pattern::parse(pattern),
+          error,
+          std::ptr::null_mut()
+        )
+      } else {
+        lightningcss::css_modules::Pattern::default()
+      };
+      Some(lightningcss::css_modules::Config {
+        pattern,
+        dashed_idents: options.css_modules_dashed_idents,
+        ..Default::default()
+      })
+    } else {
+      None
+    },
+    error_recovery: options.error_recovery,
+    source_index: 0,
+    warnings: Some(warnings.clone()),
+  };
+
+  let stylesheet = unwrap!(StyleSheet::parse(code, opts), error, std::ptr::null_mut());
+  Box::into_raw(Box::new(StyleSheetWrapper {
+    stylesheet,
+    source: code,
+    warnings: warnings.clone().read().unwrap().iter().map(|w| w.clone().into()).collect(),
+  }))
+}
+
+#[no_mangle]
+pub extern "C" fn lightningcss_stylesheet_transform(
+  stylesheet: *mut StyleSheetWrapper,
+  options: TransformOptions,
+  error: *mut *mut CssError,
+) -> bool {
+  let wrapper = unsafe { stylesheet.as_mut() }.unwrap();
+  unwrap!(wrapper.stylesheet.minify(options.into()), error, false);
+  true
+}
+
+#[no_mangle]
+pub extern "C" fn lightningcss_stylesheet_to_css(
+  stylesheet: *mut StyleSheetWrapper,
+  options: ToCssOptions,
+  error: *mut *mut CssError,
+) -> ToCssResult {
+  let wrapper = unsafe { stylesheet.as_mut() }.unwrap();
+  let mut source_map = if options.source_map {
+    let mut sm = SourceMap::new("/");
+    sm.add_source(&wrapper.stylesheet.sources[0]);
+    unwrap!(sm.set_source_content(0, wrapper.source), error, ToCssResult::default());
+    Some(sm)
+  } else {
+    None
+  };
+
+  let opts = PrinterOptions {
+    minify: options.minify,
+    project_root: if options.project_root.is_null() {
+      None
+    } else {
+      Some(unsafe { std::str::from_utf8_unchecked(CStr::from_ptr(options.project_root).to_bytes()) })
+    },
+    source_map: source_map.as_mut(),
+    targets: if options.targets != Targets::default() {
+      Some(options.targets.into()).into()
+    } else {
+      Default::default()
+    },
+    analyze_dependencies: if options.analyze_dependencies {
+      Some(Default::default())
+    } else {
+      None
+    },
+    pseudo_classes: if options.pseudo_classes != PseudoClasses::default() {
+      Some(options.pseudo_classes.into())
+    } else {
+      None
+    },
+  };
+
+  let res = unwrap!(wrapper.stylesheet.to_css(opts), error, ToCssResult::default());
+
+  let map = if let Some(mut source_map) = source_map {
+    if !options.input_source_map.is_null() {
+      let slice =
+        unsafe { std::slice::from_raw_parts(options.input_source_map as *const u8, options.input_source_map_len) };
+      let input_source_map = unsafe { std::str::from_utf8_unchecked(slice) };
+      let mut sm = unwrap!(
+        SourceMap::from_json("/", input_source_map),
+        error,
+        ToCssResult::default()
+      );
+      unwrap!(source_map.extends(&mut sm), error, ToCssResult::default());
+    }
+
+    unwrap!(source_map.to_json(None), error, ToCssResult::default()).into()
+  } else {
+    RawString::default()
+  };
+
+  let (exports, exports_len) = if let Some(exports) = res.exports {
+    let exports: Vec<CssModuleExport> = exports
+      .into_iter()
+      .map(|(k, v)| {
+        let composes_len = v.composes.len();
+        let composes = if !v.composes.is_empty() {
+          let composes: Vec<CssModuleReference> = v.composes.into_iter().map(|composes| composes.into()).collect();
+          ManuallyDrop::new(composes).as_mut_ptr()
+        } else {
+          std::ptr::null_mut()
+        };
+
+        CssModuleExport {
+          exported: k.into(),
+          local: v.name.into(),
+          is_referenced: v.is_referenced,
+          composes,
+          composes_len,
+        }
+      })
+      .collect();
+    let mut exports = ManuallyDrop::new(exports);
+    (exports.as_mut_ptr(), exports.len())
+  } else {
+    (std::ptr::null_mut(), 0)
+  };
+
+  let (references, references_len) = if let Some(references) = res.references {
+    let references: Vec<CssModulePlaceholder> = references
+      .into_iter()
+      .map(|(k, v)| CssModulePlaceholder {
+        placeholder: k.into(),
+        reference: v.into(),
+      })
+      .collect();
+    let mut references = ManuallyDrop::new(references);
+    (references.as_mut_ptr(), references.len())
+  } else {
+    (std::ptr::null_mut(), 0)
+  };
+
+  ToCssResult {
+    code: res.code.into(),
+    map,
+    exports,
+    exports_len,
+    references,
+    references_len,
+  }
+}
+
+#[no_mangle]
+pub extern "C" fn lightningcss_stylesheet_free(stylesheet: *mut StyleSheetWrapper) {
+  if !stylesheet.is_null() {
+    drop(unsafe { Box::from_raw(stylesheet) })
+  }
+}
+
+#[repr(C)]
+pub struct ToCssResult {
+  code: RawString,
+  map: RawString,
+  exports: *mut CssModuleExport,
+  exports_len: usize,
+  references: *mut CssModulePlaceholder,
+  references_len: usize,
+}
+
+impl Default for ToCssResult {
+  fn default() -> Self {
+    ToCssResult {
+      code: RawString::default(),
+      map: RawString::default(),
+      exports: std::ptr::null_mut(),
+      exports_len: 0,
+      references: std::ptr::null_mut(),
+      references_len: 0,
+    }
+  }
+}
+
+impl Drop for ToCssResult {
+  fn drop(&mut self) {
+    if !self.exports.is_null() {
+      let exports = unsafe { Vec::from_raw_parts(self.exports, self.exports_len, self.exports_len) };
+      drop(exports);
+      self.exports = std::ptr::null_mut();
+    }
+
+    if !self.references.is_null() {
+      let references = unsafe { Vec::from_raw_parts(self.references, self.references_len, self.references_len) };
+      drop(references);
+      self.references = std::ptr::null_mut();
+    }
+  }
+}
+
+#[no_mangle]
+pub extern "C" fn lightningcss_to_css_result_free(result: ToCssResult) {
+  drop(result)
+}
+
+#[repr(C)]
+pub struct CssModuleExport {
+  exported: RawString,
+  local: RawString,
+  is_referenced: bool,
+  composes: *mut CssModuleReference,
+  composes_len: usize,
+}
+
+impl Drop for CssModuleExport {
+  fn drop(&mut self) {
+    if !self.composes.is_null() {
+      let composes = unsafe { Vec::from_raw_parts(self.composes, self.composes_len, self.composes_len) };
+      drop(composes);
+      self.composes = std::ptr::null_mut();
+    }
+  }
+}
+
+#[repr(C)]
+pub enum CssModuleReference {
+  /// A local reference.
+  Local {
+    /// The local (compiled) name for the reference.
+    name: RawString,
+  },
+  /// A global reference.
+  Global {
+    /// The referenced global name.
+    name: RawString,
+  },
+  /// A reference to an export in a different file.
+  Dependency {
+    /// The name to reference within the dependency.
+    name: RawString,
+    /// The dependency specifier for the referenced file.
+    specifier: RawString,
+  },
+}
+
+impl From<lightningcss::css_modules::CssModuleReference> for CssModuleReference {
+  fn from(reference: lightningcss::css_modules::CssModuleReference) -> Self {
+    use lightningcss::css_modules::CssModuleReference::*;
+    match reference {
+      Local { name } => CssModuleReference::Local { name: name.into() },
+      Global { name } => CssModuleReference::Global { name: name.into() },
+      Dependency { name, specifier } => CssModuleReference::Dependency {
+        name: name.into(),
+        specifier: specifier.into(),
+      },
+    }
+  }
+}
+
+#[repr(C)]
+pub struct CssModulePlaceholder {
+  placeholder: RawString,
+  reference: CssModuleReference,
+}
+
+#[repr(C)]
+pub struct RawString {
+  text: *mut c_char,
+  len: usize,
+}
+
+impl Default for RawString {
+  fn default() -> Self {
+    RawString {
+      text: std::ptr::null_mut(),
+      len: 0,
+    }
+  }
+}
+
+impl From<String> for RawString {
+  fn from(string: String) -> RawString {
+    RawString {
+      len: string.len(),
+      text: Box::into_raw(string.into_boxed_str()) as *mut c_char,
+    }
+  }
+}
+
+impl Drop for RawString {
+  fn drop(&mut self) {
+    if self.text.is_null() {
+      return;
+    }
+    drop(unsafe { Box::from_raw(self.text) });
+    self.text = std::ptr::null_mut();
+  }
+}
+
+#[no_mangle]
+pub extern "C" fn lightningcss_error_message(error: *mut CssError) -> *const c_char {
+  match unsafe { error.as_mut() } {
+    Some(err) => err.message(),
+    None => std::ptr::null(),
+  }
+}
+
+#[no_mangle]
+pub extern "C" fn lightningcss_error_free(error: *mut CssError) {
+  if !error.is_null() {
+    drop(unsafe { Box::from_raw(error) })
+  }
+}
+
+#[no_mangle]
+pub extern "C" fn lightningcss_stylesheet_get_warning_count<'i>(
+  stylesheet: *mut StyleSheetWrapper<'i, '_>,
+) -> usize {
+  match unsafe { stylesheet.as_mut() } {
+    Some(s) => s.warnings.len(),
+    None => 0,
+  }
+}
+
+#[no_mangle]
+pub extern "C" fn lightningcss_stylesheet_get_warning<'i>(
+  stylesheet: *mut StyleSheetWrapper<'i, '_>,
+  index: usize,
+) -> *const c_char {
+  let stylesheet = match unsafe { stylesheet.as_mut() } {
+    Some(s) => s,
+    None => return std::ptr::null(),
+  };
+
+  match stylesheet.warnings.get_mut(index) {
+    Some(w) => w.message(),
+    None => std::ptr::null(),
+  }
+}
diff --git a/c/test.c b/c/test.c
new file mode 100644
index 0000000..7c325a3
--- /dev/null
+++ b/c/test.c
@@ -0,0 +1,100 @@
+#include <stdio.h>
+#include <string.h>
+#include "lightningcss.h"
+
+int print_error(CssError *error);
+
+int main()
+{
+  char *source =
+      ".foo {"
+      "  color: lch(50.998% 135.363 338);"
+      "}"
+      ".bar {"
+      "  color: yellow;"
+      "  composes: foo from './bar.css';"
+      "}"
+      ".baz:hover {"
+      "  color: var(--foo from './baz.css');"
+      "}";
+
+  ParseOptions parse_opts = {
+      .filename = "test.css",
+      .css_modules = true,
+      .css_modules_pattern = "yo_[name]_[local]",
+      .css_modules_dashed_idents = true};
+
+  CssError *error = NULL;
+  StyleSheet *stylesheet = lightningcss_stylesheet_parse(source, strlen(source), parse_opts, &error);
+  if (!stylesheet)
+    goto cleanup;
+
+  char *unused_symbols[1] = {"bar"};
+  TransformOptions transform_opts = {
+      .unused_symbols = unused_symbols,
+      .unused_symbols_len = 0};
+
+  if (!lightningcss_browserslist_to_targets("last 2 versions, not IE <= 11", &transform_opts.targets, &error))
+    goto cleanup;
+
+  if (!lightningcss_stylesheet_transform(stylesheet, transform_opts, &error))
+    goto cleanup;
+
+  ToCssOptions to_css_opts = {
+      .minify = true,
+      .source_map = true,
+      .pseudo_classes = {
+          .hover = "is-hovered"}};
+
+  ToCssResult result = lightningcss_stylesheet_to_css(stylesheet, to_css_opts, &error);
+  if (error)
+    goto cleanup;
+
+  size_t warning_count = lightningcss_stylesheet_get_warning_count(stylesheet);
+  for (size_t i = 0; i < warning_count; i++)
+  {
+    printf("warning: %s\n", lightningcss_stylesheet_get_warning(stylesheet, i));
+  }
+
+  fwrite(result.code.text, sizeof(char), result.code.len, stdout);
+  printf("\n");
+  fwrite(result.map.text, sizeof(char), result.map.len, stdout);
+  printf("\n");
+
+  for (int i = 0; i < result.exports_len; i++)
+  {
+    printf("%.*s -> %.*s\n", (int)result.exports[i].exported.len, result.exports[i].exported.text, (int)result.exports[i].local.len, result.exports[i].local.text);
+    for (int j = 0; j < result.exports[i].composes_len; j++)
+    {
+      const CssModuleReference *ref = &result.exports[i].composes[j];
+      switch (ref->tag)
+      {
+      case CssModuleReference_Local:
+        printf("  composes local: %.*s\n", (int)ref->local.name.len, ref->local.name.text);
+        break;
+      case CssModuleReference_Global:
+        printf("  composes global: %.*s\n", (int)ref->global.name.len, ref->global.name.text);
+        break;
+      case CssModuleReference_Dependency:
+        printf("  composes dependency: %.*s from %.*s\n", (int)ref->dependency.name.len, ref->dependency.name.text, (int)ref->dependency.specifier.len, ref->dependency.specifier.text);
+        break;
+      }
+    }
+  }
+
+  for (int i = 0; i < result.references_len; i++)
+  {
+    printf("placeholder: %.*s\n", (int)result.references[i].placeholder.len, result.references[i].placeholder.text);
+  }
+
+cleanup:
+  lightningcss_stylesheet_free(stylesheet);
+  lightningcss_to_css_result_free(result);
+
+  if (error)
+  {
+    printf("error: %s\n", lightningcss_error_message(error));
+    lightningcss_error_free(error);
+    return 1;
+  }
+}
diff --git a/cli/.gitignore b/cli/.gitignore
new file mode 100644
index 0000000..ea48d9e
--- /dev/null
+++ b/cli/.gitignore
@@ -0,0 +1,4 @@
+package.json
+README.md
+.DS_Store
+lightningcss.exe
diff --git a/cli/lightningcss b/cli/lightningcss
new file mode 100755
index 0000000..53e1545
--- /dev/null
+++ b/cli/lightningcss
@@ -0,0 +1 @@
+This file is required so that npm creates the lightningcss binary on Windows.
diff --git a/cli/postinstall.js b/cli/postinstall.js
new file mode 100644
index 0000000..19dadc7
--- /dev/null
+++ b/cli/postinstall.js
@@ -0,0 +1,46 @@
+let fs = require('fs');
+let path = require('path');
+
+let parts = [process.platform, process.arch];
+if (process.platform === 'linux') {
+  const {MUSL, familySync} = require('detect-libc');
+  const family = familySync();
+  if (family === MUSL) {
+    parts.push('musl');
+  } else if (process.arch === 'arm') {
+    parts.push('gnueabihf');
+  } else {
+    parts.push('gnu');
+  }
+} else if (process.platform === 'win32') {
+  parts.push('msvc');
+}
+
+let binary = process.platform === 'win32' ? 'lightningcss.exe' : 'lightningcss';
+
+let pkgPath;
+try {
+  pkgPath = path.dirname(require.resolve(`lightningcss-cli-${parts.join('-')}/package.json`));
+} catch (err) {
+  pkgPath = path.join(__dirname, '..', 'target', 'release');
+  if (!fs.existsSync(path.join(pkgPath, binary))) {
+    pkgPath = path.join(__dirname, '..', 'target', 'debug');
+  }
+}
+
+try {
+  fs.linkSync(path.join(pkgPath, binary), path.join(__dirname, binary));
+} catch (err) {
+  try {
+    fs.copyFileSync(path.join(pkgPath, binary), path.join(__dirname, binary));
+  } catch (err) {
+    console.error('Failed to move lightningcss-cli binary into place.');
+    process.exit(1);
+  }
+}
+
+if (process.platform === 'win32') {
+  try {
+    fs.unlinkSync(path.join(__dirname, 'lightningcss'));
+  } catch (err) { }
+}
diff --git a/derive/Cargo.toml b/derive/Cargo.toml
new file mode 100644
index 0000000..ce55e5c
--- /dev/null
+++ b/derive/Cargo.toml
@@ -0,0 +1,17 @@
+[package]
+authors = ["Devon Govett <devongovett@gmail.com>"]
+name = "lightningcss-derive"
+description = "Derive macros for lightningcss"
+version = "1.0.0-alpha.43"
+license = "MPL-2.0"
+edition = "2021"
+repository = "https://github.com/parcel-bundler/lightningcss"
+
+[lib]
+proc-macro = true
+
+[dependencies]
+syn = { version = "1.0", features = ["extra-traits"] }
+quote = "1.0"
+proc-macro2 = "1.0"
+convert_case = "0.6.0"
diff --git a/derive/src/lib.rs b/derive/src/lib.rs
new file mode 100644
index 0000000..1224149
--- /dev/null
+++ b/derive/src/lib.rs
@@ -0,0 +1,20 @@
+use proc_macro::TokenStream;
+
+mod parse;
+mod to_css;
+mod visit;
+
+#[proc_macro_derive(Visit, attributes(visit, skip_visit, skip_type, visit_types))]
+pub fn derive_visit_children(input: TokenStream) -> TokenStream {
+  visit::derive_visit_children(input)
+}
+
+#[proc_macro_derive(Parse, attributes(css))]
+pub fn derive_parse(input: TokenStream) -> TokenStream {
+  parse::derive_parse(input)
+}
+
+#[proc_macro_derive(ToCss, attributes(css))]
+pub fn derive_to_css(input: TokenStream) -> TokenStream {
+  to_css::derive_to_css(input)
+}
diff --git a/derive/src/parse.rs b/derive/src/parse.rs
new file mode 100644
index 0000000..995b344
--- /dev/null
+++ b/derive/src/parse.rs
@@ -0,0 +1,213 @@
+use convert_case::Casing;
+use proc_macro::{self, TokenStream};
+use proc_macro2::{Literal, Span, TokenStream as TokenStream2};
+use quote::quote;
+use syn::{
+  parse::Parse, parse_macro_input, parse_quote, Attribute, Data, DataEnum, DeriveInput, Fields, Ident, Token,
+};
+
+pub fn derive_parse(input: TokenStream) -> TokenStream {
+  let DeriveInput {
+    ident,
+    data,
+    mut generics,
+    attrs,
+    ..
+  } = parse_macro_input!(input);
+  let opts = CssOptions::parse_attributes(&attrs).unwrap();
+  let cloned_generics = generics.clone();
+  let (_, ty_generics, _) = cloned_generics.split_for_impl();
+
+  if generics.lifetimes().next().is_none() {
+    generics.params.insert(0, parse_quote! { 'i })
+  }
+
+  let lifetime = generics.lifetimes().next().unwrap().clone();
+  let (impl_generics, _, where_clause) = generics.split_for_impl();
+
+  let imp = match &data {
+    Data::Enum(data) => derive_enum(&data, &ident, &opts),
+    _ => todo!(),
+  };
+
+  let output = quote! {
+    impl #impl_generics Parse<#lifetime> for #ident #ty_generics #where_clause {
+      fn parse<'t>(input: &mut Parser<#lifetime, 't>) -> Result<Self, ParseError<#lifetime, ParserError<#lifetime>>> {
+        #imp
+      }
+    }
+  };
+
+  output.into()
+}
+
+fn derive_enum(data: &DataEnum, ident: &Ident, opts: &CssOptions) -> TokenStream2 {
+  let mut idents = Vec::new();
+  let mut non_idents = Vec::new();
+  for (index, variant) in data.variants.iter().enumerate() {
+    let name = &variant.ident;
+    let fields = variant
+      .fields
+      .iter()
+      .enumerate()
+      .map(|(index, field)| {
+        field.ident.as_ref().map_or_else(
+          || Ident::new(&format!("_{}", index), Span::call_site()),
+          |ident| ident.clone(),
+        )
+      })
+      .collect::<Vec<_>>();
+
+    let mut expr = match &variant.fields {
+      Fields::Unit => {
+        idents.push((
+          Literal::string(&variant.ident.to_string().to_case(opts.case)),
+          name.clone(),
+        ));
+        continue;
+      }
+      Fields::Named(_) => {
+        quote! {
+          return Ok(#ident::#name { #(#fields),* })
+        }
+      }
+      Fields::Unnamed(_) => {
+        quote! {
+          return Ok(#ident::#name(#(#fields),*))
+        }
+      }
+    };
+
+    // Group multiple ident branches together.
+    if !idents.is_empty() {
+      if idents.len() == 1 {
+        let (s, name) = idents.remove(0);
+        non_idents.push(quote! {
+          if input.try_parse(|input| input.expect_ident_matching(#s)).is_ok() {
+            return Ok(#ident::#name)
+          }
+        });
+      } else {
+        let matches = idents
+          .iter()
+          .map(|(s, name)| {
+            quote! {
+              #s => return Ok(#ident::#name),
+            }
+          })
+          .collect::<Vec<_>>();
+        non_idents.push(quote! {
+          {
+            let state = input.state();
+            if let Ok(ident) = input.try_parse(|input| input.expect_ident_cloned()) {
+              cssparser::match_ignore_ascii_case! { &*ident,
+                #(#matches)*
+                _ => {}
+              }
+              input.reset(&state);
+            }
+          }
+        });
+        idents.clear();
+      }
+    }
+
+    let is_last = index == data.variants.len() - 1;
+
+    for (index, field) in variant.fields.iter().enumerate().rev() {
+      let ty = &field.ty;
+      let field_name = field.ident.as_ref().map_or_else(
+        || Ident::new(&format!("_{}", index), Span::call_site()),
+        |ident| ident.clone(),
+      );
+      if is_last {
+        expr = quote! {
+          let #field_name = <#ty>::parse(input)?;
+          #expr
+        };
+      } else {
+        expr = quote! {
+          if let Ok(#field_name) = input.try_parse(<#ty>::parse) {
+            #expr
+          }
+        };
+      }
+    }
+
+    non_idents.push(expr);
+  }
+
+  let idents = if idents.is_empty() {
+    quote! {}
+  } else if idents.len() == 1 {
+    let (s, name) = idents.remove(0);
+    quote! {
+      input.expect_ident_matching(#s)?;
+      Ok(#ident::#name)
+    }
+  } else {
+    let idents = idents
+      .into_iter()
+      .map(|(s, name)| {
+        quote! {
+          #s => Ok(#ident::#name),
+        }
+      })
+      .collect::<Vec<_>>();
+    quote! {
+      let location = input.current_source_location();
+      let ident = input.expect_ident()?;
+      cssparser::match_ignore_ascii_case! { &*ident,
+        #(#idents)*
+        _ => Err(location.new_unexpected_token_error(
+          cssparser::Token::Ident(ident.clone())
+        ))
+      }
+    }
+  };
+
+  let output = quote! {
+    #(#non_idents)*
+    #idents
+  };
+
+  output.into()
+}
+
+pub struct CssOptions {
+  pub case: convert_case::Case,
+}
+
+impl CssOptions {
+  pub fn parse_attributes(attrs: &Vec<Attribute>) -> syn::Result<Self> {
+    for attr in attrs {
+      if attr.path.is_ident("css") {
+        let opts: CssOptions = attr.parse_args()?;
+        return Ok(opts);
+      }
+    }
+
+    Ok(CssOptions {
+      case: convert_case::Case::Kebab,
+    })
+  }
+}
+
+impl Parse for CssOptions {
+  fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
+    let mut case = convert_case::Case::Kebab;
+    while !input.is_empty() {
+      let k: Ident = input.parse()?;
+      let _: Token![=] = input.parse()?;
+      let v: Ident = input.parse()?;
+
+      if k == "case" {
+        if v == "lower" {
+          case = convert_case::Case::Flat;
+        }
+      }
+    }
+
+    Ok(Self { case })
+  }
+}
diff --git a/derive/src/to_css.rs b/derive/src/to_css.rs
new file mode 100644
index 0000000..739a16d
--- /dev/null
+++ b/derive/src/to_css.rs
@@ -0,0 +1,156 @@
+use convert_case::Casing;
+use proc_macro::{self, TokenStream};
+use proc_macro2::{Literal, Span, TokenStream as TokenStream2};
+use quote::quote;
+use syn::{parse_macro_input, Data, DataEnum, DeriveInput, Fields, Ident, Type};
+
+use crate::parse::CssOptions;
+
+pub fn derive_to_css(input: TokenStream) -> TokenStream {
+  let DeriveInput {
+    ident,
+    data,
+    generics,
+    attrs,
+    ..
+  } = parse_macro_input!(input);
+
+  let opts = CssOptions::parse_attributes(&attrs).unwrap();
+  let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
+
+  let imp = match &data {
+    Data::Enum(data) => derive_enum(&data, &opts),
+    _ => todo!(),
+  };
+
+  let output = quote! {
+    impl #impl_generics ToCss for #ident #ty_generics #where_clause {
+      fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+      where
+        W: std::fmt::Write,
+      {
+        #imp
+      }
+    }
+  };
+
+  output.into()
+}
+
+fn derive_enum(data: &DataEnum, opts: &CssOptions) -> TokenStream2 {
+  let variants = data
+    .variants
+    .iter()
+    .map(|variant| {
+      let name = &variant.ident;
+      let fields = variant
+        .fields
+        .iter()
+        .enumerate()
+        .map(|(index, field)| {
+          field.ident.as_ref().map_or_else(
+            || Ident::new(&format!("_{}", index), Span::call_site()),
+            |ident| ident.clone(),
+          )
+        })
+        .collect::<Vec<_>>();
+
+      #[derive(PartialEq)]
+      enum NeedsSpace {
+        Yes,
+        No,
+        Maybe,
+      }
+
+      let mut needs_space = NeedsSpace::No;
+      let mut fields_iter = variant.fields.iter().zip(fields.iter()).peekable();
+      let mut writes = Vec::new();
+      let mut has_needs_space = false;
+      while let Some((field, name)) = fields_iter.next() {
+        writes.push(if fields.len() > 1 {
+          let space = match needs_space {
+            NeedsSpace::Yes => quote! { dest.write_char(' ')?; },
+            NeedsSpace::No => quote! {},
+            NeedsSpace::Maybe => {
+              has_needs_space = true;
+              quote! {
+                if needs_space {
+                  dest.write_char(' ')?;
+                }
+              }
+            }
+          };
+
+          if is_option(&field.ty) {
+            needs_space = NeedsSpace::Maybe;
+            let after_space = if matches!(fields_iter.peek(), Some((field, _)) if !is_option(&field.ty)) {
+              // If the next field is non-optional, just insert the space here.
+              needs_space = NeedsSpace::No;
+              quote! { dest.write_char(' ')?; }
+            } else {
+              quote! {}
+            };
+            quote! {
+              if let Some(v) = #name {
+                #space
+                v.to_css(dest)?;
+                #after_space
+              }
+            }
+          } else {
+            needs_space = NeedsSpace::Yes;
+            quote! {
+              #space
+              #name.to_css(dest)?;
+            }
+          }
+        } else {
+          quote! { #name.to_css(dest) }
+        });
+      }
+
+      if writes.len() > 1 {
+        writes.push(quote! { Ok(()) });
+      }
+
+      if has_needs_space {
+        writes.insert(0, quote! { let mut needs_space = false });
+      }
+
+      match variant.fields {
+        Fields::Unit => {
+          let s = Literal::string(&variant.ident.to_string().to_case(opts.case));
+          quote! {
+            Self::#name => dest.write_str(#s)
+          }
+        }
+        Fields::Named(_) => {
+          quote! {
+            Self::#name { #(#fields),* } => {
+              #(#writes)*
+            }
+          }
+        }
+        Fields::Unnamed(_) => {
+          quote! {
+            Self::#name(#(#fields),*) => {
+              #(#writes)*
+            }
+          }
+        }
+      }
+    })
+    .collect::<Vec<_>>();
+
+  let output = quote! {
+    match self {
+      #(#variants),*
+    }
+  };
+
+  output.into()
+}
+
+fn is_option(ty: &Type) -> bool {
+  matches!(&ty, Type::Path(p) if p.qself.is_none() && p.path.segments.iter().next().unwrap().ident == "Option")
+}
diff --git a/derive/src/visit.rs b/derive/src/visit.rs
new file mode 100644
index 0000000..0209336
--- /dev/null
+++ b/derive/src/visit.rs
@@ -0,0 +1,292 @@
+use std::collections::HashSet;
+
+use proc_macro::{self, TokenStream};
+use proc_macro2::{Span, TokenStream as TokenStream2};
+use quote::quote;
+use syn::{
+  parse::Parse, parse_macro_input, parse_quote, Attribute, Data, DataEnum, DeriveInput, Field, Fields,
+  GenericParam, Generics, Ident, Member, Token, Type, Visibility,
+};
+
+pub fn derive_visit_children(input: TokenStream) -> TokenStream {
+  let DeriveInput {
+    ident,
+    data,
+    generics,
+    attrs,
+    ..
+  } = parse_macro_input!(input);
+
+  let options: Vec<VisitOptions> = attrs
+    .iter()
+    .filter_map(|attr| {
+      if attr.path.is_ident("visit") {
+        let opts: VisitOptions = attr.parse_args().unwrap();
+        Some(opts)
+      } else {
+        None
+      }
+    })
+    .collect();
+
+  let visit_types = if let Some(attr) = attrs.iter().find(|attr| attr.path.is_ident("visit_types")) {
+    let types: VisitTypes = attr.parse_args().unwrap();
+    let types = types.types;
+    Some(quote! { crate::visit_types!(#(#types)|*) })
+  } else {
+    None
+  };
+
+  if options.is_empty() {
+    derive(&ident, &data, &generics, None, visit_types)
+  } else {
+    options
+      .into_iter()
+      .map(|options| derive(&ident, &data, &generics, Some(options), visit_types.clone()))
+      .collect()
+  }
+}
+
+fn derive(
+  ident: &Ident,
+  data: &Data,
+  generics: &Generics,
+  options: Option<VisitOptions>,
+  visit_types: Option<TokenStream2>,
+) -> TokenStream {
+  let mut impl_generics = generics.clone();
+  let mut type_defs = quote! {};
+  let generics = if let Some(VisitOptions {
+    generic: Some(generic), ..
+  }) = &options
+  {
+    let mappings = generics
+      .type_params()
+      .zip(generic.type_params())
+      .map(|(a, b)| quote! { type #a = #b; });
+    type_defs = quote! { #(#mappings)* };
+    impl_generics.params.clear();
+    generic
+  } else {
+    &generics
+  };
+
+  if impl_generics.lifetimes().next().is_none() {
+    impl_generics.params.insert(0, parse_quote! { 'i })
+  }
+
+  let lifetime = impl_generics.lifetimes().next().unwrap().clone();
+  let t = impl_generics.type_params().find(|g| &g.ident.to_string() == "R");
+  let v = quote! { __V };
+  let t = if let Some(t) = t {
+    GenericParam::Type(t.ident.clone().into())
+  } else {
+    let t: GenericParam = parse_quote! { __T };
+    impl_generics
+      .params
+      .push(parse_quote! { #t: crate::visitor::Visit<#lifetime, __T, #v> });
+    t
+  };
+
+  impl_generics
+    .params
+    .push(parse_quote! { #v: ?Sized + crate::visitor::Visitor<#lifetime, #t> });
+
+  for ty in generics.type_params() {
+    let name = &ty.ident;
+    impl_generics.make_where_clause().predicates.push(parse_quote! {
+      #name: Visit<#lifetime, #t, #v>
+    })
+  }
+
+  let mut seen_types = HashSet::new();
+  let mut child_types = Vec::new();
+  let mut visit = Vec::new();
+  match data {
+    Data::Struct(s) => {
+      for (
+        index,
+        Field {
+          vis, ty, ident, attrs, ..
+        },
+      ) in s.fields.iter().enumerate()
+      {
+        if attrs.iter().any(|attr| attr.path.is_ident("skip_visit")) {
+          continue;
+        }
+
+        if matches!(ty, Type::Reference(_)) || !matches!(vis, Visibility::Public(..)) {
+          continue;
+        }
+
+        if visit_types.is_none() && !seen_types.contains(ty) && !skip_type(attrs) {
+          seen_types.insert(ty.clone());
+          child_types.push(quote! {
+            <#ty as Visit<#lifetime, #t, #v>>::CHILD_TYPES.bits()
+          });
+        }
+
+        let name = ident
+          .as_ref()
+          .map_or_else(|| Member::Unnamed(index.into()), |ident| Member::Named(ident.clone()));
+        visit.push(quote! { self.#name.visit(visitor)?; })
+      }
+    }
+    Data::Enum(DataEnum { variants, .. }) => {
+      let variants = variants
+        .iter()
+        .map(|variant| {
+          let name = &variant.ident;
+          let mut field_names = Vec::new();
+          let mut visit_fields = Vec::new();
+          for (index, Field { ty, ident, attrs, .. }) in variant.fields.iter().enumerate() {
+            let name = ident.as_ref().map_or_else(
+              || Ident::new(&format!("_{}", index), Span::call_site()),
+              |ident| ident.clone(),
+            );
+            field_names.push(name.clone());
+
+            if matches!(ty, Type::Reference(_)) {
+              continue;
+            }
+
+            if visit_types.is_none() && !seen_types.contains(ty) && !skip_type(attrs) && !skip_type(&variant.attrs)
+            {
+              seen_types.insert(ty.clone());
+              child_types.push(quote! {
+                <#ty as Visit<#lifetime, #t, #v>>::CHILD_TYPES.bits()
+              });
+            }
+
+            visit_fields.push(quote! { #name.visit(visitor)?; })
+          }
+
+          match variant.fields {
+            Fields::Unnamed(_) => {
+              quote! {
+                Self::#name(#(#field_names),*) => {
+                  #(#visit_fields)*
+                }
+              }
+            }
+            Fields::Named(_) => {
+              quote! {
+                Self::#name { #(#field_names),* } => {
+                  #(#visit_fields)*
+                }
+              }
+            }
+            Fields::Unit => quote! {},
+          }
+        })
+        .collect::<proc_macro2::TokenStream>();
+
+      visit.push(quote! {
+        match self {
+          #variants
+          _ => {}
+        }
+      })
+    }
+    _ => {}
+  }
+
+  if visit_types.is_none() && child_types.is_empty() {
+    child_types.push(quote! { crate::visitor::VisitTypes::empty().bits() });
+  }
+
+  let (_, ty_generics, _) = generics.split_for_impl();
+  let (impl_generics, _, where_clause) = impl_generics.split_for_impl();
+
+  let self_visit = if let Some(VisitOptions {
+    visit: Some(visit),
+    kind: Some(kind),
+    ..
+  }) = &options
+  {
+    child_types.push(quote! { crate::visitor::VisitTypes::#kind.bits() });
+
+    quote! {
+      fn visit(&mut self, visitor: &mut #v) -> Result<(), #v::Error> {
+        if visitor.visit_types().contains(crate::visitor::VisitTypes::#kind) {
+          visitor.#visit(self)
+        } else {
+          self.visit_children(visitor)
+        }
+      }
+    }
+  } else {
+    quote! {}
+  };
+
+  let child_types = visit_types.unwrap_or_else(|| {
+    quote! {
+      {
+        #type_defs
+        crate::visitor::VisitTypes::from_bits_retain(#(#child_types)|*)
+      }
+    }
+  });
+
+  let output = quote! {
+    impl #impl_generics Visit<#lifetime, #t, #v> for #ident #ty_generics #where_clause {
+      const CHILD_TYPES: crate::visitor::VisitTypes = #child_types;
+
+      #self_visit
+
+      fn visit_children(&mut self, visitor: &mut #v) -> Result<(), #v::Error> {
+        if !<Self as Visit<#lifetime, #t, #v>>::CHILD_TYPES.intersects(visitor.visit_types()) {
+          return Ok(())
+        }
+
+        #(#visit)*
+
+        Ok(())
+      }
+    }
+  };
+
+  output.into()
+}
+
+fn skip_type(attrs: &Vec<Attribute>) -> bool {
+  attrs.iter().any(|attr| attr.path.is_ident("skip_type"))
+}
+
+struct VisitOptions {
+  visit: Option<Ident>,
+  kind: Option<Ident>,
+  generic: Option<Generics>,
+}
+
+impl Parse for VisitOptions {
+  fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
+    let (visit, kind, comma) = if input.peek(Ident) {
+      let visit: Ident = input.parse()?;
+      let _: Token![,] = input.parse()?;
+      let kind: Ident = input.parse()?;
+      let comma: Result<Token![,], _> = input.parse();
+      (Some(visit), Some(kind), comma.is_ok())
+    } else {
+      (None, None, true)
+    };
+    let generic: Option<Generics> = if comma { Some(input.parse()?) } else { None };
+    Ok(Self { visit, kind, generic })
+  }
+}
+
+struct VisitTypes {
+  types: Vec<Ident>,
+}
+
+impl Parse for VisitTypes {
+  fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
+    let first: Ident = input.parse()?;
+    let mut types = vec![first];
+    while input.parse::<Token![|]>().is_ok() {
+      let id: Ident = input.parse()?;
+      types.push(id);
+    }
+    Ok(Self { types })
+  }
+}
diff --git a/examples/custom_at_rule.rs b/examples/custom_at_rule.rs
new file mode 100644
index 0000000..9c091e6
--- /dev/null
+++ b/examples/custom_at_rule.rs
@@ -0,0 +1,309 @@
+use std::{collections::HashMap, convert::Infallible};
+
+use cssparser::*;
+use lightningcss::{
+  declaration::DeclarationBlock,
+  error::PrinterError,
+  printer::Printer,
+  properties::custom::{Token, TokenOrValue},
+  rules::{style::StyleRule, CssRule, CssRuleList, Location},
+  selector::{Component, Selector},
+  stylesheet::{ParserOptions, PrinterOptions, StyleSheet},
+  targets::Browsers,
+  traits::{AtRuleParser, ToCss},
+  values::{
+    color::{CssColor, RGBA},
+    length::LengthValue,
+  },
+  vendor_prefix::VendorPrefix,
+  visit_types,
+  visitor::{Visit, VisitTypes, Visitor},
+};
+
+fn main() {
+  let args: Vec<String> = std::env::args().collect();
+  let source = std::fs::read_to_string(&args[1]).unwrap();
+  let opts = ParserOptions {
+    filename: args[1].clone(),
+    ..Default::default()
+  };
+
+  let mut stylesheet = StyleSheet::parse_with(&source, opts, &mut TailwindAtRuleParser).unwrap();
+
+  println!("{:?}", stylesheet);
+
+  let mut style_rules = HashMap::new();
+  stylesheet
+    .visit(&mut StyleRuleCollector {
+      rules: &mut style_rules,
+    })
+    .unwrap();
+  println!("{:?}", style_rules);
+  stylesheet.visit(&mut ApplyVisitor { rules: &style_rules }).unwrap();
+
+  let result = stylesheet
+    .to_css(PrinterOptions {
+      targets: Browsers {
+        chrome: Some(100 << 16),
+        ..Browsers::default()
+      }
+      .into(),
+      ..PrinterOptions::default()
+    })
+    .unwrap();
+  println!("{}", result.code);
+}
+
+/// An @tailwind directive.
+#[derive(Debug, Clone)]
+enum TailwindDirective {
+  Base,
+  Components,
+  Utilities,
+  Variants,
+}
+
+/// A custom at rule prelude.
+enum Prelude {
+  Tailwind(TailwindDirective),
+  Apply(Vec<String>),
+}
+
+/// A @tailwind rule.
+#[derive(Debug, Clone)]
+struct TailwindRule {
+  directive: TailwindDirective,
+  loc: SourceLocation,
+}
+
+/// An @apply rule.
+#[derive(Debug, Clone)]
+struct ApplyRule {
+  names: Vec<String>,
+  loc: SourceLocation,
+}
+
+/// A custom at rule.
+#[derive(Debug, Clone)]
+enum AtRule {
+  Tailwind(TailwindRule),
+  Apply(ApplyRule),
+}
+
+#[derive(Debug)]
+struct TailwindAtRuleParser;
+impl<'i> AtRuleParser<'i> for TailwindAtRuleParser {
+  type Prelude = Prelude;
+  type Error = Infallible;
+  type AtRule = AtRule;
+
+  fn parse_prelude<'t>(
+    &mut self,
+    name: CowRcStr<'i>,
+    input: &mut Parser<'i, 't>,
+    _options: &ParserOptions<'_, 'i>,
+  ) -> Result<Self::Prelude, ParseError<'i, Self::Error>> {
+    match_ignore_ascii_case! {&*name,
+      "tailwind" => {
+        let location = input.current_source_location();
+        let ident = input.expect_ident()?;
+        let directive = match_ignore_ascii_case! { &*ident,
+          "base" => TailwindDirective::Base,
+          "components" => TailwindDirective::Components,
+          "utilities" => TailwindDirective::Utilities,
+          "variants" => TailwindDirective::Variants,
+          _ => return Err(location.new_unexpected_token_error(
+            cssparser::Token::Ident(ident.clone())
+          ))
+        };
+        Ok(Prelude::Tailwind(directive))
+      },
+      "apply" => {
+        let mut names = Vec::new();
+        loop {
+          if let Ok(name) = input.try_parse(|input| input.expect_ident_cloned()) {
+            names.push(name.as_ref().into());
+          } else {
+            break
+          }
+        }
+
+        Ok(Prelude::Apply(names))
+      },
+      _ => Err(input.new_error(BasicParseErrorKind::AtRuleInvalid(name)))
+    }
+  }
+
+  fn rule_without_block(
+    &mut self,
+    prelude: Self::Prelude,
+    start: &ParserState,
+    _options: &ParserOptions<'_, 'i>,
+    _is_nested: bool,
+  ) -> Result<Self::AtRule, ()> {
+    let loc = start.source_location();
+    match prelude {
+      Prelude::Tailwind(directive) => Ok(AtRule::Tailwind(TailwindRule { directive, loc })),
+      Prelude::Apply(names) => Ok(AtRule::Apply(ApplyRule { names, loc })),
+    }
+  }
+}
+
+struct StyleRuleCollector<'i, 'a> {
+  rules: &'a mut HashMap<String, DeclarationBlock<'i>>,
+}
+
+impl<'i, 'a> Visitor<'i, AtRule> for StyleRuleCollector<'i, 'a> {
+  type Error = Infallible;
+
+  fn visit_types(&self) -> VisitTypes {
+    VisitTypes::RULES
+  }
+
+  fn visit_rule(&mut self, rule: &mut lightningcss::rules::CssRule<'i, AtRule>) -> Result<(), Self::Error> {
+    match rule {
+      CssRule::Style(rule) => {
+        for selector in rule.selectors.0.iter() {
+          if selector.len() != 1 {
+            continue; // TODO
+          }
+          for component in selector.iter_raw_match_order() {
+            match component {
+              Component::Class(name) => {
+                self.rules.insert(name.0.to_string(), rule.declarations.clone());
+              }
+              _ => {}
+            }
+          }
+        }
+      }
+      _ => {}
+    }
+
+    rule.visit_children(self)
+  }
+}
+
+struct ApplyVisitor<'a, 'i> {
+  rules: &'a HashMap<String, DeclarationBlock<'i>>,
+}
+
+impl<'a, 'i> Visitor<'i, AtRule> for ApplyVisitor<'a, 'i> {
+  type Error = Infallible;
+
+  fn visit_types(&self) -> VisitTypes {
+    visit_types!(RULES | COLORS | LENGTHS | DASHED_IDENTS | SELECTORS | TOKENS)
+  }
+
+  fn visit_rule(&mut self, rule: &mut CssRule<'i, AtRule>) -> Result<(), Self::Error> {
+    // Replace @apply rule with nested style rule.
+    if let CssRule::Custom(AtRule::Apply(apply)) = rule {
+      let mut declarations = DeclarationBlock::new();
+      for name in &apply.names {
+        let Some(applied) = self.rules.get(name) else {
+          continue;
+        };
+        declarations
+          .important_declarations
+          .extend(applied.important_declarations.iter().cloned());
+        declarations.declarations.extend(applied.declarations.iter().cloned());
+      }
+      *rule = CssRule::Style(StyleRule {
+        selectors: Component::Nesting.into(),
+        vendor_prefix: VendorPrefix::None,
+        declarations,
+        rules: CssRuleList(vec![]),
+        loc: Location {
+          source_index: 0,
+          line: apply.loc.line,
+          column: apply.loc.column,
+        },
+      })
+    }
+
+    rule.visit_children(self)
+  }
+
+  fn visit_url(&mut self, url: &mut lightningcss::values::url::Url<'i>) -> Result<(), Self::Error> {
+    url.url = format!("https://mywebsite.com/{}", url.url).into();
+    Ok(())
+  }
+
+  fn visit_color(&mut self, color: &mut lightningcss::values::color::CssColor) -> Result<(), Self::Error> {
+    *color = color.to_lab().unwrap();
+    Ok(())
+  }
+
+  fn visit_length(&mut self, length: &mut lightningcss::values::length::LengthValue) -> Result<(), Self::Error> {
+    match length {
+      LengthValue::Px(px) => *length = LengthValue::Rem(*px / 16.0),
+      _ => {}
+    }
+
+    Ok(())
+  }
+
+  fn visit_dashed_ident(
+    &mut self,
+    ident: &mut lightningcss::values::ident::DashedIdent,
+  ) -> Result<(), Self::Error> {
+    ident.0 = format!("--tw-{}", &ident.0[2..]).into();
+    Ok(())
+  }
+
+  fn visit_selector(&mut self, selector: &mut Selector<'i>) -> Result<(), Self::Error> {
+    for c in selector.iter_mut_raw_match_order() {
+      match c {
+        Component::Class(c) => {
+          *c = format!("tw-{}", c).into();
+        }
+        _ => {}
+      }
+    }
+
+    Ok(())
+  }
+
+  fn visit_token(&mut self, token: &mut TokenOrValue<'i>) -> Result<(), Self::Error> {
+    match token {
+      TokenOrValue::Function(f) if f.name == "theme" => match f.arguments.0.first() {
+        Some(TokenOrValue::Token(Token::String(s))) => match s.as_ref() {
+          "blue-500" => *token = TokenOrValue::Color(CssColor::RGBA(RGBA::new(0, 0, 255, 1.0))),
+          "red-500" => *token = TokenOrValue::Color(CssColor::RGBA(RGBA::new(255, 0, 0, 1.0))),
+          _ => {}
+        },
+        _ => {}
+      },
+      _ => {}
+    }
+
+    token.visit_children(self)
+  }
+}
+
+#[cfg(feature = "visitor")]
+impl<'i, V: Visitor<'i, AtRule>> Visit<'i, AtRule, V> for AtRule {
+  const CHILD_TYPES: VisitTypes = VisitTypes::empty();
+
+  fn visit_children(&mut self, _: &mut V) -> Result<(), V::Error> {
+    Ok(())
+  }
+}
+
+impl ToCss for AtRule {
+  fn to_css<W: std::fmt::Write>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError> {
+    match self {
+      AtRule::Tailwind(rule) => {
+        let _ = rule.loc; // TODO: source maps
+        let directive = match rule.directive {
+          TailwindDirective::Base => "TAILWIND BASE HERE",
+          TailwindDirective::Components => "TAILWIND COMPONENTS HERE",
+          TailwindDirective::Utilities => "TAILWIND UTILITIES HERE",
+          TailwindDirective::Variants => "TAILWIND VARIANTS HERE",
+        };
+        dest.write_str(directive)
+      }
+      AtRule::Apply(_) => Ok(()),
+    }
+  }
+}
diff --git a/examples/schema.rs b/examples/schema.rs
new file mode 100644
index 0000000..9bfe3f6
--- /dev/null
+++ b/examples/schema.rs
@@ -0,0 +1,8 @@
+fn main() {
+  #[cfg(feature = "jsonschema")]
+  {
+    let schema = schemars::schema_for!(lightningcss::stylesheet::StyleSheet);
+    let output = serde_json::to_string_pretty(&schema).unwrap();
+    let _ = std::fs::write("node/ast.json", output);
+  }
+}
diff --git a/examples/serialize.rs b/examples/serialize.rs
new file mode 100644
index 0000000..00d3721
--- /dev/null
+++ b/examples/serialize.rs
@@ -0,0 +1,27 @@
+fn main() {
+  parse();
+}
+
+#[cfg(feature = "serde")]
+fn parse() {
+  use lightningcss::stylesheet::{ParserOptions, StyleSheet};
+  use std::{env, fs};
+
+  let args: Vec<String> = env::args().collect();
+  let contents = fs::read_to_string(&args[1]).unwrap();
+  let stylesheet = StyleSheet::parse(
+    &contents,
+    ParserOptions {
+      filename: args[1].clone(),
+      ..ParserOptions::default()
+    },
+  )
+  .unwrap();
+  let json = serde_json::to_string(&stylesheet).unwrap();
+  println!("{}", json);
+}
+
+#[cfg(not(feature = "serde"))]
+fn parse() {
+  panic!("serde feature is not enabled")
+}
diff --git a/napi/Cargo.toml b/napi/Cargo.toml
new file mode 100644
index 0000000..6638dd5
--- /dev/null
+++ b/napi/Cargo.toml
@@ -0,0 +1,32 @@
+[package]
+authors = ["Devon Govett <devongovett@gmail.com>"]
+name = "lightningcss-napi"
+version = "0.4.4"
+description = "Node-API bindings for Lightning CSS"
+license = "MPL-2.0"
+repository = "https://github.com/parcel-bundler/lightningcss"
+edition = "2021"
+
+[features]
+default = []
+visitor = ["lightningcss/visitor"]
+bundler = ["dep:crossbeam-channel", "dep:rayon"]
+
+[dependencies]
+serde = { version = "1.0.201", features = ["derive"] }
+serde_bytes = "0.11.5"
+cssparser = "0.33.0"
+lightningcss = { version = "1.0.0-alpha.66", path = "../", features = [
+  "nodejs",
+  "serde",
+] }
+parcel_sourcemap = { version = "2.1.1", features = ["json"] }
+serde-detach = "0.0.1"
+smallvec = { version = "1.7.0", features = ["union"] }
+napi = { version = "2", default-features = false, features = [
+  "napi4",
+  "napi5",
+  "serde-json",
+] }
+crossbeam-channel = { version = "0.5.6", optional = true }
+rayon = { version = "1.5.1", optional = true }
diff --git a/napi/src/at_rule_parser.rs b/napi/src/at_rule_parser.rs
new file mode 100644
index 0000000..919eda2
--- /dev/null
+++ b/napi/src/at_rule_parser.rs
@@ -0,0 +1,215 @@
+use std::collections::HashMap;
+
+use cssparser::*;
+use lightningcss::{
+  declaration::DeclarationBlock,
+  error::ParserError,
+  rules::{CssRuleList, Location},
+  stylesheet::ParserOptions,
+  traits::{AtRuleParser, ToCss},
+  values::{
+    string::CowArcStr,
+    syntax::{ParsedComponent, SyntaxString},
+  },
+};
+use serde::{Deserialize, Deserializer, Serialize};
+
+#[derive(Deserialize, Debug, Clone)]
+pub struct CustomAtRuleConfig {
+  #[serde(default, deserialize_with = "deserialize_prelude")]
+  prelude: Option<SyntaxString>,
+  body: Option<CustomAtRuleBodyType>,
+}
+
+fn deserialize_prelude<'de, D>(deserializer: D) -> Result<Option<SyntaxString>, D::Error>
+where
+  D: Deserializer<'de>,
+{
+  let s = Option::<CowArcStr<'de>>::deserialize(deserializer)?;
+  if let Some(s) = s {
+    Ok(Some(
+      SyntaxString::parse_string(&s).map_err(|_| serde::de::Error::custom("invalid syntax string"))?,
+    ))
+  } else {
+    Ok(None)
+  }
+}
+
+#[derive(Deserialize, Debug, Clone)]
+#[serde(rename_all = "kebab-case")]
+enum CustomAtRuleBodyType {
+  DeclarationList,
+  RuleList,
+  StyleBlock,
+}
+
+pub struct Prelude<'i> {
+  name: CowArcStr<'i>,
+  prelude: Option<ParsedComponent<'i>>,
+}
+
+#[derive(Serialize, Deserialize, Clone)]
+pub struct AtRule<'i> {
+  #[serde(borrow)]
+  pub name: CowArcStr<'i>,
+  pub prelude: Option<ParsedComponent<'i>>,
+  pub body: Option<AtRuleBody<'i>>,
+  pub loc: Location,
+}
+
+#[derive(Serialize, Deserialize, Clone)]
+#[serde(tag = "type", content = "value", rename_all = "kebab-case")]
+pub enum AtRuleBody<'i> {
+  #[serde(borrow)]
+  DeclarationList(DeclarationBlock<'i>),
+  RuleList(CssRuleList<'i, AtRule<'i>>),
+}
+
+#[derive(Clone)]
+pub struct CustomAtRuleParser {
+  pub configs: HashMap<String, CustomAtRuleConfig>,
+}
+
+impl<'i> AtRuleParser<'i> for CustomAtRuleParser {
+  type Prelude = Prelude<'i>;
+  type Error = ParserError<'i>;
+  type AtRule = AtRule<'i>;
+
+  fn parse_prelude<'t>(
+    &mut self,
+    name: CowRcStr<'i>,
+    input: &mut Parser<'i, 't>,
+    _options: &ParserOptions<'_, 'i>,
+  ) -> Result<Self::Prelude, ParseError<'i, Self::Error>> {
+    if let Some(config) = self.configs.get(name.as_ref()) {
+      let prelude = if let Some(prelude) = &config.prelude {
+        Some(prelude.parse_value(input)?)
+      } else {
+        None
+      };
+      Ok(Prelude {
+        name: name.into(),
+        prelude,
+      })
+    } else {
+      Err(input.new_error(BasicParseErrorKind::AtRuleInvalid(name)))
+    }
+  }
+
+  fn parse_block<'t>(
+    &mut self,
+    prelude: Self::Prelude,
+    start: &ParserState,
+    input: &mut Parser<'i, 't>,
+    options: &ParserOptions<'_, 'i>,
+    is_nested: bool,
+  ) -> Result<Self::AtRule, ParseError<'i, Self::Error>> {
+    let config = self.configs.get(prelude.name.as_ref()).unwrap();
+    let body = if let Some(body) = &config.body {
+      match body {
+        CustomAtRuleBodyType::DeclarationList => {
+          Some(AtRuleBody::DeclarationList(DeclarationBlock::parse(input, options)?))
+        }
+        CustomAtRuleBodyType::RuleList => {
+          Some(AtRuleBody::RuleList(CssRuleList::parse_with(input, options, self)?))
+        }
+        CustomAtRuleBodyType::StyleBlock => Some(AtRuleBody::RuleList(CssRuleList::parse_style_block_with(
+          input, options, self, is_nested,
+        )?)),
+      }
+    } else {
+      return Err(input.new_error(BasicParseErrorKind::AtRuleBodyInvalid));
+    };
+
+    let loc = start.source_location();
+    Ok(AtRule {
+      name: prelude.name,
+      prelude: prelude.prelude,
+      body,
+      loc: Location {
+        source_index: options.source_index,
+        line: loc.line,
+        column: loc.column,
+      },
+    })
+  }
+
+  fn rule_without_block(
+    &mut self,
+    prelude: Self::Prelude,
+    start: &ParserState,
+    options: &ParserOptions<'_, 'i>,
+    _is_nested: bool,
+  ) -> Result<Self::AtRule, ()> {
+    let config = self.configs.get(prelude.name.as_ref()).unwrap();
+    if config.body.is_some() {
+      return Err(());
+    }
+
+    let loc = start.source_location();
+    Ok(AtRule {
+      name: prelude.name,
+      prelude: prelude.prelude,
+      body: None,
+      loc: Location {
+        source_index: options.source_index,
+        line: loc.line,
+        column: loc.column,
+      },
+    })
+  }
+}
+
+impl<'i> ToCss for AtRule<'i> {
+  fn to_css<W>(
+    &self,
+    dest: &mut lightningcss::printer::Printer<W>,
+  ) -> Result<(), lightningcss::error::PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    dest.write_char('@')?;
+    serialize_identifier(&self.name, dest)?;
+    if let Some(prelude) = &self.prelude {
+      dest.write_char(' ')?;
+      prelude.to_css(dest)?;
+    }
+
+    if let Some(body) = &self.body {
+      match body {
+        AtRuleBody::DeclarationList(decls) => {
+          decls.to_css_block(dest)?;
+        }
+        AtRuleBody::RuleList(rules) => {
+          dest.whitespace()?;
+          dest.write_char('{')?;
+          dest.indent();
+          dest.newline()?;
+          rules.to_css(dest)?;
+          dest.dedent();
+          dest.newline()?;
+          dest.write_char('}')?;
+        }
+      }
+    }
+
+    Ok(())
+  }
+}
+
+#[cfg(feature = "visitor")]
+use lightningcss::visitor::{Visit, VisitTypes, Visitor};
+
+#[cfg(feature = "visitor")]
+impl<'i, V: Visitor<'i, AtRule<'i>>> Visit<'i, AtRule<'i>, V> for AtRule<'i> {
+  const CHILD_TYPES: VisitTypes = VisitTypes::empty();
+
+  fn visit_children(&mut self, visitor: &mut V) -> Result<(), V::Error> {
+    self.prelude.visit(visitor)?;
+    match &mut self.body {
+      Some(AtRuleBody::DeclarationList(decls)) => decls.visit(visitor),
+      Some(AtRuleBody::RuleList(rules)) => rules.visit(visitor),
+      None => Ok(()),
+    }
+  }
+}
diff --git a/napi/src/lib.rs b/napi/src/lib.rs
new file mode 100644
index 0000000..dff4880
--- /dev/null
+++ b/napi/src/lib.rs
@@ -0,0 +1,1206 @@
+#[cfg(feature = "bundler")]
+use at_rule_parser::AtRule;
+use at_rule_parser::{CustomAtRuleConfig, CustomAtRuleParser};
+use lightningcss::bundler::BundleErrorKind;
+#[cfg(feature = "bundler")]
+use lightningcss::bundler::{Bundler, SourceProvider};
+use lightningcss::css_modules::{CssModuleExports, CssModuleReferences, PatternParseError};
+use lightningcss::dependencies::{Dependency, DependencyOptions};
+use lightningcss::error::{Error, ErrorLocation, MinifyErrorKind, ParserError, PrinterErrorKind};
+use lightningcss::stylesheet::{
+  MinifyOptions, ParserFlags, ParserOptions, PrinterOptions, PseudoClasses, StyleAttribute, StyleSheet,
+};
+use lightningcss::targets::{Browsers, Features, Targets};
+use napi::bindgen_prelude::{FromNapiValue, ToNapiValue};
+use napi::{CallContext, Env, JsObject, JsUnknown};
+use parcel_sourcemap::SourceMap;
+use serde::{Deserialize, Serialize};
+use std::collections::{HashMap, HashSet};
+use std::sync::{Arc, RwLock};
+
+mod at_rule_parser;
+#[cfg(feature = "bundler")]
+#[cfg(not(target_arch = "wasm32"))]
+mod threadsafe_function;
+#[cfg(feature = "visitor")]
+mod transformer;
+mod utils;
+
+#[cfg(feature = "visitor")]
+use transformer::JsVisitor;
+
+#[cfg(not(feature = "visitor"))]
+struct JsVisitor;
+
+#[cfg(feature = "visitor")]
+use lightningcss::visitor::Visit;
+
+use utils::get_named_property;
+
+#[derive(Serialize)]
+#[serde(rename_all = "camelCase")]
+struct TransformResult<'i> {
+  #[serde(with = "serde_bytes")]
+  code: Vec<u8>,
+  #[serde(with = "serde_bytes")]
+  map: Option<Vec<u8>>,
+  exports: Option<CssModuleExports>,
+  references: Option<CssModuleReferences>,
+  dependencies: Option<Vec<Dependency>>,
+  warnings: Vec<Warning<'i>>,
+}
+
+impl<'i> TransformResult<'i> {
+  fn into_js(self, env: Env) -> napi::Result<JsUnknown> {
+    // Manually construct buffers so we avoid a copy and work around
+    // https://github.com/napi-rs/napi-rs/issues/1124.
+    let mut obj = env.create_object()?;
+    let buf = env.create_buffer_with_data(self.code)?;
+    obj.set_named_property("code", buf.into_raw())?;
+    obj.set_named_property(
+      "map",
+      if let Some(map) = self.map {
+        let buf = env.create_buffer_with_data(map)?;
+        buf.into_raw().into_unknown()
+      } else {
+        env.get_null()?.into_unknown()
+      },
+    )?;
+    obj.set_named_property("exports", env.to_js_value(&self.exports)?)?;
+    obj.set_named_property("references", env.to_js_value(&self.references)?)?;
+    obj.set_named_property("dependencies", env.to_js_value(&self.dependencies)?)?;
+    obj.set_named_property("warnings", env.to_js_value(&self.warnings)?)?;
+    Ok(obj.into_unknown())
+  }
+}
+
+#[cfg(feature = "visitor")]
+fn get_visitor(env: Env, opts: &JsObject) -> Option<JsVisitor> {
+  if let Ok(visitor) = get_named_property::<JsObject>(opts, "visitor") {
+    Some(JsVisitor::new(env, visitor))
+  } else {
+    None
+  }
+}
+
+#[cfg(not(feature = "visitor"))]
+fn get_visitor(_env: Env, _opts: &JsObject) -> Option<JsVisitor> {
+  None
+}
+
+pub fn transform(ctx: CallContext) -> napi::Result<JsUnknown> {
+  let opts = ctx.get::<JsObject>(0)?;
+  let mut visitor = get_visitor(*ctx.env, &opts);
+
+  let config: Config = ctx.env.from_js_value(opts)?;
+  let code = unsafe { std::str::from_utf8_unchecked(&config.code) };
+  let res = compile(code, &config, &mut visitor);
+
+  match res {
+    Ok(res) => res.into_js(*ctx.env),
+    Err(err) => Err(err.into_js_error(*ctx.env, Some(code))?),
+  }
+}
+
+pub fn transform_style_attribute(ctx: CallContext) -> napi::Result<JsUnknown> {
+  let opts = ctx.get::<JsObject>(0)?;
+  let mut visitor = get_visitor(*ctx.env, &opts);
+
+  let config: AttrConfig = ctx.env.from_js_value(opts)?;
+  let code = unsafe { std::str::from_utf8_unchecked(&config.code) };
+  let res = compile_attr(code, &config, &mut visitor);
+
+  match res {
+    Ok(res) => res.into_js(ctx),
+    Err(err) => Err(err.into_js_error(*ctx.env, Some(code))?),
+  }
+}
+
+#[cfg(feature = "bundler")]
+#[cfg(not(target_arch = "wasm32"))]
+mod bundle {
+  use super::*;
+  use crossbeam_channel::{self, Receiver, Sender};
+  use lightningcss::bundler::FileProvider;
+  use napi::{Env, JsFunction, JsString, NapiRaw};
+  use std::path::{Path, PathBuf};
+  use std::str::FromStr;
+  use std::sync::Mutex;
+  use threadsafe_function::{ThreadSafeCallContext, ThreadsafeFunction, ThreadsafeFunctionCallMode};
+
+  pub fn bundle(ctx: CallContext) -> napi::Result<JsUnknown> {
+    let opts = ctx.get::<JsObject>(0)?;
+    let mut visitor = get_visitor(*ctx.env, &opts);
+
+    let config: BundleConfig = ctx.env.from_js_value(opts)?;
+    let fs = FileProvider::new();
+
+    // This is pretty silly, but works around a rust limitation that you cannot
+    // explicitly annotate lifetime bounds on closures.
+    fn annotate<'i, 'o, F>(f: F) -> F
+    where
+      F: FnOnce(&mut StyleSheet<'i, 'o, AtRule<'i>>) -> napi::Result<()>,
+    {
+      f
+    }
+
+    let res = compile_bundle(
+      &fs,
+      &config,
+      visitor.as_mut().map(|visitor| annotate(|stylesheet| stylesheet.visit(visitor))),
+    );
+
+    match res {
+      Ok(res) => res.into_js(*ctx.env),
+      Err(err) => Err(err.into_js_error(*ctx.env, None)?),
+    }
+  }
+
+  // A SourceProvider which calls JavaScript functions to resolve and read files.
+  struct JsSourceProvider {
+    resolve: Option<ThreadsafeFunction<ResolveMessage>>,
+    read: Option<ThreadsafeFunction<ReadMessage>>,
+    inputs: Mutex<Vec<*mut String>>,
+  }
+
+  unsafe impl Sync for JsSourceProvider {}
+  unsafe impl Send for JsSourceProvider {}
+
+  // Allocate a single channel per thread to communicate with the JS thread.
+  thread_local! {
+    static CHANNEL: (Sender<napi::Result<String>>, Receiver<napi::Result<String>>) = crossbeam_channel::unbounded();
+  }
+
+  impl SourceProvider for JsSourceProvider {
+    type Error = napi::Error;
+
+    fn read<'a>(&'a self, file: &Path) -> Result<&'a str, Self::Error> {
+      let source = if let Some(read) = &self.read {
+        CHANNEL.with(|channel| {
+          let message = ReadMessage {
+            file: file.to_str().unwrap().to_owned(),
+            tx: channel.0.clone(),
+          };
+
+          read.call(message, ThreadsafeFunctionCallMode::Blocking);
+          channel.1.recv().unwrap()
+        })
+      } else {
+        Ok(std::fs::read_to_string(file)?)
+      };
+
+      match source {
+        Ok(source) => {
+          // cache the result
+          let ptr = Box::into_raw(Box::new(source));
+          self.inputs.lock().unwrap().push(ptr);
+          // SAFETY: this is safe because the pointer is not dropped
+          // until the JsSourceProvider is, and we never remove from the
+          // list of pointers stored in the vector.
+          Ok(unsafe { &*ptr })
+        }
+        Err(e) => Err(e),
+      }
+    }
+
+    fn resolve(&self, specifier: &str, originating_file: &Path) -> Result<PathBuf, Self::Error> {
+      if let Some(resolve) = &self.resolve {
+        return CHANNEL.with(|channel| {
+          let message = ResolveMessage {
+            specifier: specifier.to_owned(),
+            originating_file: originating_file.to_str().unwrap().to_owned(),
+            tx: channel.0.clone(),
+          };
+
+          resolve.call(message, ThreadsafeFunctionCallMode::Blocking);
+          let result = channel.1.recv().unwrap();
+          match result {
+            Ok(result) => Ok(PathBuf::from_str(&result).unwrap()),
+            Err(e) => Err(e),
+          }
+        });
+      }
+
+      Ok(originating_file.with_file_name(specifier))
+    }
+  }
+
+  struct ResolveMessage {
+    specifier: String,
+    originating_file: String,
+    tx: Sender<napi::Result<String>>,
+  }
+
+  struct ReadMessage {
+    file: String,
+    tx: Sender<napi::Result<String>>,
+  }
+
+  struct VisitMessage {
+    stylesheet: &'static mut StyleSheet<'static, 'static, AtRule<'static>>,
+    tx: Sender<napi::Result<String>>,
+  }
+
+  fn await_promise(env: Env, result: JsUnknown, tx: Sender<napi::Result<String>>) -> napi::Result<()> {
+    // If the result is a promise, wait for it to resolve, and send the result to the channel.
+    // Otherwise, send the result immediately.
+    if result.is_promise()? {
+      let result: JsObject = result.try_into()?;
+      let then: JsFunction = get_named_property(&result, "then")?;
+      let tx2 = tx.clone();
+      let cb = env.create_function_from_closure("callback", move |ctx| {
+        let res = ctx.get::<JsString>(0)?.into_utf8()?;
+        let s = res.into_owned()?;
+        tx.send(Ok(s)).unwrap();
+        ctx.env.get_undefined()
+      })?;
+      let eb = env.create_function_from_closure("error_callback", move |ctx| {
+        let res = ctx.get::<JsUnknown>(0)?;
+        tx2.send(Err(napi::Error::from(res))).unwrap();
+        ctx.env.get_undefined()
+      })?;
+      then.call(Some(&result), &[cb, eb])?;
+    } else {
+      let result: JsString = result.try_into()?;
+      let utf8 = result.into_utf8()?;
+      let s = utf8.into_owned()?;
+      tx.send(Ok(s)).unwrap();
+    }
+
+    Ok(())
+  }
+
+  fn resolve_on_js_thread(ctx: ThreadSafeCallContext<ResolveMessage>) -> napi::Result<()> {
+    let specifier = ctx.env.create_string(&ctx.value.specifier)?;
+    let originating_file = ctx.env.create_string(&ctx.value.originating_file)?;
+    let result = ctx.callback.unwrap().call(None, &[specifier, originating_file])?;
+    await_promise(ctx.env, result, ctx.value.tx)
+  }
+
+  fn handle_error(tx: Sender<napi::Result<String>>, res: napi::Result<()>) -> napi::Result<()> {
+    match res {
+      Ok(_) => Ok(()),
+      Err(e) => {
+        tx.send(Err(e)).expect("send error");
+        Ok(())
+      }
+    }
+  }
+
+  fn resolve_on_js_thread_wrapper(ctx: ThreadSafeCallContext<ResolveMessage>) -> napi::Result<()> {
+    let tx = ctx.value.tx.clone();
+    handle_error(tx, resolve_on_js_thread(ctx))
+  }
+
+  fn read_on_js_thread(ctx: ThreadSafeCallContext<ReadMessage>) -> napi::Result<()> {
+    let file = ctx.env.create_string(&ctx.value.file)?;
+    let result = ctx.callback.unwrap().call(None, &[file])?;
+    await_promise(ctx.env, result, ctx.value.tx)
+  }
+
+  fn read_on_js_thread_wrapper(ctx: ThreadSafeCallContext<ReadMessage>) -> napi::Result<()> {
+    let tx = ctx.value.tx.clone();
+    handle_error(tx, read_on_js_thread(ctx))
+  }
+
+  pub fn bundle_async(ctx: CallContext) -> napi::Result<JsObject> {
+    let opts = ctx.get::<JsObject>(0)?;
+    let visitor = get_visitor(*ctx.env, &opts);
+
+    let config: BundleConfig = ctx.env.from_js_value(&opts)?;
+
+    if let Ok(resolver) = get_named_property::<JsObject>(&opts, "resolver") {
+      let read = if resolver.has_named_property("read")? {
+        let read = get_named_property::<JsFunction>(&resolver, "read")?;
+        Some(ThreadsafeFunction::create(
+          ctx.env.raw(),
+          unsafe { read.raw() },
+          0,
+          read_on_js_thread_wrapper,
+        )?)
+      } else {
+        None
+      };
+
+      let resolve = if resolver.has_named_property("resolve")? {
+        let resolve = get_named_property::<JsFunction>(&resolver, "resolve")?;
+        Some(ThreadsafeFunction::create(
+          ctx.env.raw(),
+          unsafe { resolve.raw() },
+          0,
+          resolve_on_js_thread_wrapper,
+        )?)
+      } else {
+        None
+      };
+
+      let provider = JsSourceProvider {
+        resolve,
+        read,
+        inputs: Mutex::new(Vec::new()),
+      };
+
+      run_bundle_task(provider, config, visitor, *ctx.env)
+    } else {
+      let provider = FileProvider::new();
+      run_bundle_task(provider, config, visitor, *ctx.env)
+    }
+  }
+
+  // Runs bundling on a background thread managed by rayon. This is similar to AsyncTask from napi-rs, however,
+  // because we call back into the JS thread, which might call other tasks in the node threadpool (e.g. fs.readFile),
+  // we may end up deadlocking if the number of rayon threads exceeds node's threadpool size. Therefore, we must
+  // run bundling from a thread not managed by Node.
+  fn run_bundle_task<P: 'static + SourceProvider>(
+    provider: P,
+    config: BundleConfig,
+    visitor: Option<JsVisitor>,
+    env: Env,
+  ) -> napi::Result<JsObject>
+  where
+    P::Error: IntoJsError,
+  {
+    let (deferred, promise) = env.create_deferred()?;
+
+    let tsfn = if let Some(mut visitor) = visitor {
+      Some(ThreadsafeFunction::create(
+        env.raw(),
+        std::ptr::null_mut(),
+        0,
+        move |ctx: ThreadSafeCallContext<VisitMessage>| {
+          if let Err(err) = ctx.value.stylesheet.visit(&mut visitor) {
+            ctx.value.tx.send(Err(err)).expect("send error");
+            return Ok(());
+          }
+          ctx.value.tx.send(Ok(Default::default())).expect("send error");
+          Ok(())
+        },
+      )?)
+    } else {
+      None
+    };
+
+    // Run bundling task in rayon threadpool.
+    rayon::spawn(move || {
+      let res = compile_bundle(
+        unsafe { std::mem::transmute::<&'_ P, &'static P>(&provider) },
+        &config,
+        tsfn.map(move |tsfn| {
+          move |stylesheet: &mut StyleSheet<AtRule>| {
+            CHANNEL.with(|channel| {
+              let message = VisitMessage {
+                // SAFETY: we immediately lock the thread until we get a response,
+                // so stylesheet cannot be dropped in that time.
+                stylesheet: unsafe {
+                  std::mem::transmute::<
+                    &'_ mut StyleSheet<'_, '_, AtRule>,
+                    &'static mut StyleSheet<'static, 'static, AtRule>,
+                  >(stylesheet)
+                },
+                tx: channel.0.clone(),
+              };
+
+              tsfn.call(message, ThreadsafeFunctionCallMode::Blocking);
+              channel.1.recv().expect("recv error").map(|_| ())
+            })
+          }
+        }),
+      );
+
+      deferred.resolve(move |env| match res {
+        Ok(v) => v.into_js(env),
+        Err(err) => Err(err.into_js_error(env, None)?),
+      });
+    });
+
+    Ok(promise)
+  }
+}
+
+#[cfg(feature = "bundler")]
+#[cfg(target_arch = "wasm32")]
+mod bundle {
+  use super::*;
+  use napi::{Env, JsFunction, JsString, NapiRaw, NapiValue, Ref};
+  use std::cell::UnsafeCell;
+  use std::path::{Path, PathBuf};
+  use std::str::FromStr;
+
+  pub fn bundle(ctx: CallContext) -> napi::Result<JsUnknown> {
+    let opts = ctx.get::<JsObject>(0)?;
+    let mut visitor = get_visitor(*ctx.env, &opts);
+
+    let resolver = get_named_property::<JsObject>(&opts, "resolver")?;
+    let read = get_named_property::<JsFunction>(&resolver, "read")?;
+    let resolve = if resolver.has_named_property("resolve")? {
+      let resolve = get_named_property::<JsFunction>(&resolver, "resolve")?;
+      Some(ctx.env.create_reference(resolve)?)
+    } else {
+      None
+    };
+    let config: BundleConfig = ctx.env.from_js_value(opts)?;
+
+    let provider = JsSourceProvider {
+      env: ctx.env.clone(),
+      resolve,
+      read: ctx.env.create_reference(read)?,
+      inputs: UnsafeCell::new(Vec::new()),
+    };
+
+    // This is pretty silly, but works around a rust limitation that you cannot
+    // explicitly annotate lifetime bounds on closures.
+    fn annotate<'i, 'o, F>(f: F) -> F
+    where
+      F: FnOnce(&mut StyleSheet<'i, 'o, AtRule<'i>>) -> napi::Result<()>,
+    {
+      f
+    }
+
+    let res = compile_bundle(
+      &provider,
+      &config,
+      visitor.as_mut().map(|visitor| annotate(|stylesheet| stylesheet.visit(visitor))),
+    );
+
+    match res {
+      Ok(res) => res.into_js(*ctx.env),
+      Err(err) => Err(err.into_js_error(*ctx.env, None)?),
+    }
+  }
+
+  struct JsSourceProvider {
+    env: Env,
+    resolve: Option<Ref<()>>,
+    read: Ref<()>,
+    inputs: UnsafeCell<Vec<*mut String>>,
+  }
+
+  impl Drop for JsSourceProvider {
+    fn drop(&mut self) {
+      if let Some(resolve) = &mut self.resolve {
+        drop(resolve.unref(self.env));
+      }
+      drop(self.read.unref(self.env));
+    }
+  }
+
+  unsafe impl Sync for JsSourceProvider {}
+  unsafe impl Send for JsSourceProvider {}
+
+  // This relies on Binaryen's Asyncify transform to allow Rust to call async JS functions from sync code.
+  // See the comments in async.mjs for more details about how this works.
+  extern "C" {
+    fn await_promise_sync(
+      promise: napi::sys::napi_value,
+      result: *mut napi::sys::napi_value,
+      error: *mut napi::sys::napi_value,
+    );
+  }
+
+  fn get_result(env: Env, mut value: JsUnknown) -> napi::Result<JsString> {
+    if value.is_promise()? {
+      let mut result = std::ptr::null_mut();
+      let mut error = std::ptr::null_mut();
+      unsafe { await_promise_sync(value.raw(), &mut result, &mut error) };
+      if !error.is_null() {
+        let error = unsafe { JsUnknown::from_raw(env.raw(), error)? };
+        return Err(napi::Error::from(error));
+      }
+      if result.is_null() {
+        return Err(napi::Error::new(napi::Status::GenericFailure, "No result".to_string()));
+      }
+
+      value = unsafe { JsUnknown::from_raw(env.raw(), result)? };
+    }
+
+    value.try_into()
+  }
+
+  impl SourceProvider for JsSourceProvider {
+    type Error = napi::Error;
+
+    fn read<'a>(&'a self, file: &Path) -> Result<&'a str, Self::Error> {
+      let read: JsFunction = self.env.get_reference_value_unchecked(&self.read)?;
+      let file = self.env.create_string(file.to_str().unwrap())?;
+      let source: JsUnknown = read.call(None, &[file])?;
+      let source = get_result(self.env, source)?.into_utf8()?.into_owned()?;
+
+      // cache the result
+      let ptr = Box::into_raw(Box::new(source));
+      let inputs = unsafe { &mut *self.inputs.get() };
+      inputs.push(ptr);
+      // SAFETY: this is safe because the pointer is not dropped
+      // until the JsSourceProvider is, and we never remove from the
+      // list of pointers stored in the vector.
+      Ok(unsafe { &*ptr })
+    }
+
+    fn resolve(&self, specifier: &str, originating_file: &Path) -> Result<PathBuf, Self::Error> {
+      if let Some(resolve) = &self.resolve {
+        let resolve: JsFunction = self.env.get_reference_value_unchecked(resolve)?;
+        let specifier = self.env.create_string(specifier)?;
+        let originating_file = self.env.create_string(originating_file.to_str().unwrap())?;
+        let result: JsUnknown = resolve.call(None, &[specifier, originating_file])?;
+        let result = get_result(self.env, result)?.into_utf8()?;
+        Ok(PathBuf::from_str(result.as_str()?).unwrap())
+      } else {
+        Ok(originating_file.with_file_name(specifier))
+      }
+    }
+  }
+}
+
+#[cfg(feature = "bundler")]
+pub use bundle::*;
+
+// ---------------------------------------------
+
+#[derive(Debug, Deserialize)]
+#[serde(rename_all = "camelCase")]
+struct Config {
+  pub filename: Option<String>,
+  pub project_root: Option<String>,
+  #[serde(with = "serde_bytes")]
+  pub code: Vec<u8>,
+  pub targets: Option<Browsers>,
+  #[serde(default)]
+  pub include: u32,
+  #[serde(default)]
+  pub exclude: u32,
+  pub minify: Option<bool>,
+  pub source_map: Option<bool>,
+  pub input_source_map: Option<String>,
+  pub drafts: Option<Drafts>,
+  pub non_standard: Option<NonStandard>,
+  pub css_modules: Option<CssModulesOption>,
+  pub analyze_dependencies: Option<AnalyzeDependenciesOption>,
+  pub pseudo_classes: Option<OwnedPseudoClasses>,
+  pub unused_symbols: Option<HashSet<String>>,
+  pub error_recovery: Option<bool>,
+  pub custom_at_rules: Option<HashMap<String, CustomAtRuleConfig>>,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(untagged)]
+enum AnalyzeDependenciesOption {
+  Bool(bool),
+  Config(AnalyzeDependenciesConfig),
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(rename_all = "camelCase")]
+struct AnalyzeDependenciesConfig {
+  preserve_imports: bool,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(untagged)]
+enum CssModulesOption {
+  Bool(bool),
+  Config(CssModulesConfig),
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(rename_all = "camelCase")]
+struct CssModulesConfig {
+  pattern: Option<String>,
+  dashed_idents: Option<bool>,
+  animation: Option<bool>,
+  container: Option<bool>,
+  grid: Option<bool>,
+  custom_idents: Option<bool>,
+  pure: Option<bool>,
+}
+
+#[cfg(feature = "bundler")]
+#[derive(Debug, Deserialize)]
+#[serde(rename_all = "camelCase")]
+struct BundleConfig {
+  pub filename: String,
+  pub project_root: Option<String>,
+  pub targets: Option<Browsers>,
+  #[serde(default)]
+  pub include: u32,
+  #[serde(default)]
+  pub exclude: u32,
+  pub minify: Option<bool>,
+  pub source_map: Option<bool>,
+  pub drafts: Option<Drafts>,
+  pub non_standard: Option<NonStandard>,
+  pub css_modules: Option<CssModulesOption>,
+  pub analyze_dependencies: Option<AnalyzeDependenciesOption>,
+  pub pseudo_classes: Option<OwnedPseudoClasses>,
+  pub unused_symbols: Option<HashSet<String>>,
+  pub error_recovery: Option<bool>,
+  pub custom_at_rules: Option<HashMap<String, CustomAtRuleConfig>>,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(rename_all = "camelCase")]
+struct OwnedPseudoClasses {
+  pub hover: Option<String>,
+  pub active: Option<String>,
+  pub focus: Option<String>,
+  pub focus_visible: Option<String>,
+  pub focus_within: Option<String>,
+}
+
+impl<'a> Into<PseudoClasses<'a>> for &'a OwnedPseudoClasses {
+  fn into(self) -> PseudoClasses<'a> {
+    PseudoClasses {
+      hover: self.hover.as_deref(),
+      active: self.active.as_deref(),
+      focus: self.focus.as_deref(),
+      focus_visible: self.focus_visible.as_deref(),
+      focus_within: self.focus_within.as_deref(),
+    }
+  }
+}
+
+#[derive(Serialize, Debug, Deserialize, Default)]
+#[serde(rename_all = "camelCase")]
+struct Drafts {
+  #[serde(default)]
+  custom_media: bool,
+}
+
+#[derive(Serialize, Debug, Deserialize, Default)]
+#[serde(rename_all = "camelCase")]
+struct NonStandard {
+  #[serde(default)]
+  deep_selector_combinator: bool,
+}
+
+fn compile<'i>(
+  code: &'i str,
+  config: &Config,
+  #[allow(unused_variables)] visitor: &mut Option<JsVisitor>,
+) -> Result<TransformResult<'i>, CompileError<'i, napi::Error>> {
+  let drafts = config.drafts.as_ref();
+  let non_standard = config.non_standard.as_ref();
+  let warnings = Some(Arc::new(RwLock::new(Vec::new())));
+
+  let filename = config.filename.clone().unwrap_or_default();
+  let project_root = config.project_root.as_ref().map(|p| p.as_ref());
+  let mut source_map = if config.source_map.unwrap_or_default() {
+    let mut sm = SourceMap::new(project_root.unwrap_or("/"));
+    sm.add_source(&filename);
+    sm.set_source_content(0, code)?;
+    Some(sm)
+  } else {
+    None
+  };
+
+  let res = {
+    let mut flags = ParserFlags::empty();
+    flags.set(ParserFlags::CUSTOM_MEDIA, matches!(drafts, Some(d) if d.custom_media));
+    flags.set(
+      ParserFlags::DEEP_SELECTOR_COMBINATOR,
+      matches!(non_standard, Some(v) if v.deep_selector_combinator),
+    );
+
+    let mut stylesheet = StyleSheet::parse_with(
+      &code,
+      ParserOptions {
+        filename: filename.clone(),
+        flags,
+        css_modules: if let Some(css_modules) = &config.css_modules {
+          match css_modules {
+            CssModulesOption::Bool(true) => Some(lightningcss::css_modules::Config::default()),
+            CssModulesOption::Bool(false) => None,
+            CssModulesOption::Config(c) => Some(lightningcss::css_modules::Config {
+              pattern: if let Some(pattern) = c.pattern.as_ref() {
+                match lightningcss::css_modules::Pattern::parse(pattern) {
+                  Ok(p) => p,
+                  Err(e) => return Err(CompileError::PatternError(e)),
+                }
+              } else {
+                Default::default()
+              },
+              dashed_idents: c.dashed_idents.unwrap_or_default(),
+              animation: c.animation.unwrap_or(true),
+              container: c.container.unwrap_or(true),
+              grid: c.grid.unwrap_or(true),
+              custom_idents: c.custom_idents.unwrap_or(true),
+              pure: c.pure.unwrap_or_default(),
+            }),
+          }
+        } else {
+          None
+        },
+        source_index: 0,
+        error_recovery: config.error_recovery.unwrap_or_default(),
+        warnings: warnings.clone(),
+      },
+      &mut CustomAtRuleParser {
+        configs: config.custom_at_rules.clone().unwrap_or_default(),
+      },
+    )?;
+
+    #[cfg(feature = "visitor")]
+    if let Some(visitor) = visitor.as_mut() {
+      stylesheet.visit(visitor).map_err(CompileError::JsError)?;
+    }
+
+    let targets = Targets {
+      browsers: config.targets,
+      include: Features::from_bits_truncate(config.include),
+      exclude: Features::from_bits_truncate(config.exclude),
+    };
+
+    stylesheet.minify(MinifyOptions {
+      targets,
+      unused_symbols: config.unused_symbols.clone().unwrap_or_default(),
+    })?;
+
+    stylesheet.to_css(PrinterOptions {
+      minify: config.minify.unwrap_or_default(),
+      source_map: source_map.as_mut(),
+      project_root,
+      targets,
+      analyze_dependencies: if let Some(d) = &config.analyze_dependencies {
+        match d {
+          AnalyzeDependenciesOption::Bool(b) if *b => Some(DependencyOptions { remove_imports: true }),
+          AnalyzeDependenciesOption::Config(c) => Some(DependencyOptions {
+            remove_imports: !c.preserve_imports,
+          }),
+          _ => None,
+        }
+      } else {
+        None
+      },
+      pseudo_classes: config.pseudo_classes.as_ref().map(|p| p.into()),
+    })?
+  };
+
+  let map = if let Some(mut source_map) = source_map {
+    if let Some(input_source_map) = &config.input_source_map {
+      if let Ok(mut sm) = SourceMap::from_json("/", input_source_map) {
+        let _ = source_map.extends(&mut sm);
+      }
+    }
+
+    source_map.to_json(None).ok()
+  } else {
+    None
+  };
+
+  Ok(TransformResult {
+    code: res.code.into_bytes(),
+    map: map.map(|m| m.into_bytes()),
+    exports: res.exports,
+    references: res.references,
+    dependencies: res.dependencies,
+    warnings: warnings.map_or(Vec::new(), |w| {
+      Arc::try_unwrap(w)
+        .unwrap()
+        .into_inner()
+        .unwrap()
+        .into_iter()
+        .map(|w| w.into())
+        .collect()
+    }),
+  })
+}
+
+#[cfg(feature = "bundler")]
+fn compile_bundle<
+  'i,
+  'o,
+  P: SourceProvider,
+  F: FnOnce(&mut StyleSheet<'i, 'o, AtRule<'i>>) -> napi::Result<()>,
+>(
+  fs: &'i P,
+  config: &'o BundleConfig,
+  visit: Option<F>,
+) -> Result<TransformResult<'i>, CompileError<'i, P::Error>> {
+  use std::path::Path;
+
+  let project_root = config.project_root.as_ref().map(|p| p.as_ref());
+  let mut source_map = if config.source_map.unwrap_or_default() {
+    Some(SourceMap::new(project_root.unwrap_or("/")))
+  } else {
+    None
+  };
+  let warnings = Some(Arc::new(RwLock::new(Vec::new())));
+
+  let res = {
+    let drafts = config.drafts.as_ref();
+    let non_standard = config.non_standard.as_ref();
+    let mut flags = ParserFlags::empty();
+    flags.set(ParserFlags::CUSTOM_MEDIA, matches!(drafts, Some(d) if d.custom_media));
+    flags.set(
+      ParserFlags::DEEP_SELECTOR_COMBINATOR,
+      matches!(non_standard, Some(v) if v.deep_selector_combinator),
+    );
+
+    let parser_options = ParserOptions {
+      flags,
+      css_modules: if let Some(css_modules) = &config.css_modules {
+        match css_modules {
+          CssModulesOption::Bool(true) => Some(lightningcss::css_modules::Config::default()),
+          CssModulesOption::Bool(false) => None,
+          CssModulesOption::Config(c) => Some(lightningcss::css_modules::Config {
+            pattern: if let Some(pattern) = c.pattern.as_ref() {
+              match lightningcss::css_modules::Pattern::parse(pattern) {
+                Ok(p) => p,
+                Err(e) => return Err(CompileError::PatternError(e)),
+              }
+            } else {
+              Default::default()
+            },
+            dashed_idents: c.dashed_idents.unwrap_or_default(),
+            animation: c.animation.unwrap_or(true),
+            container: c.container.unwrap_or(true),
+            grid: c.grid.unwrap_or(true),
+            custom_idents: c.custom_idents.unwrap_or(true),
+            pure: c.pure.unwrap_or_default(),
+          }),
+        }
+      } else {
+        None
+      },
+      error_recovery: config.error_recovery.unwrap_or_default(),
+      warnings: warnings.clone(),
+      filename: String::new(),
+      source_index: 0,
+    };
+
+    let mut at_rule_parser = CustomAtRuleParser {
+      configs: config.custom_at_rules.clone().unwrap_or_default(),
+    };
+
+    let mut bundler =
+      Bundler::new_with_at_rule_parser(fs, source_map.as_mut(), parser_options, &mut at_rule_parser);
+    let mut stylesheet = bundler.bundle(Path::new(&config.filename))?;
+
+    if let Some(visit) = visit {
+      visit(&mut stylesheet).map_err(CompileError::JsError)?;
+    }
+
+    let targets = Targets {
+      browsers: config.targets,
+      include: Features::from_bits_truncate(config.include),
+      exclude: Features::from_bits_truncate(config.exclude),
+    };
+
+    stylesheet.minify(MinifyOptions {
+      targets,
+      unused_symbols: config.unused_symbols.clone().unwrap_or_default(),
+    })?;
+
+    stylesheet.to_css(PrinterOptions {
+      minify: config.minify.unwrap_or_default(),
+      source_map: source_map.as_mut(),
+      project_root,
+      targets,
+      analyze_dependencies: if let Some(d) = &config.analyze_dependencies {
+        match d {
+          AnalyzeDependenciesOption::Bool(b) if *b => Some(DependencyOptions { remove_imports: true }),
+          AnalyzeDependenciesOption::Config(c) => Some(DependencyOptions {
+            remove_imports: !c.preserve_imports,
+          }),
+          _ => None,
+        }
+      } else {
+        None
+      },
+      pseudo_classes: config.pseudo_classes.as_ref().map(|p| p.into()),
+    })?
+  };
+
+  let map = if let Some(source_map) = &mut source_map {
+    source_map.to_json(None).ok()
+  } else {
+    None
+  };
+
+  Ok(TransformResult {
+    code: res.code.into_bytes(),
+    map: map.map(|m| m.into_bytes()),
+    exports: res.exports,
+    references: res.references,
+    dependencies: res.dependencies,
+    warnings: warnings.map_or(Vec::new(), |w| {
+      Arc::try_unwrap(w)
+        .unwrap()
+        .into_inner()
+        .unwrap()
+        .into_iter()
+        .map(|w| w.into())
+        .collect()
+    }),
+  })
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(rename_all = "camelCase")]
+struct AttrConfig {
+  pub filename: Option<String>,
+  #[serde(with = "serde_bytes")]
+  pub code: Vec<u8>,
+  pub targets: Option<Browsers>,
+  #[serde(default)]
+  pub include: u32,
+  #[serde(default)]
+  pub exclude: u32,
+  #[serde(default)]
+  pub minify: bool,
+  #[serde(default)]
+  pub analyze_dependencies: bool,
+  #[serde(default)]
+  pub error_recovery: bool,
+}
+
+#[derive(Serialize)]
+#[serde(rename_all = "camelCase")]
+struct AttrResult<'i> {
+  #[serde(with = "serde_bytes")]
+  code: Vec<u8>,
+  dependencies: Option<Vec<Dependency>>,
+  warnings: Vec<Warning<'i>>,
+}
+
+impl<'i> AttrResult<'i> {
+  fn into_js(self, ctx: CallContext) -> napi::Result<JsUnknown> {
+    // Manually construct buffers so we avoid a copy and work around
+    // https://github.com/napi-rs/napi-rs/issues/1124.
+    let mut obj = ctx.env.create_object()?;
+    let buf = ctx.env.create_buffer_with_data(self.code)?;
+    obj.set_named_property("code", buf.into_raw())?;
+    obj.set_named_property("dependencies", ctx.env.to_js_value(&self.dependencies)?)?;
+    obj.set_named_property("warnings", ctx.env.to_js_value(&self.warnings)?)?;
+    Ok(obj.into_unknown())
+  }
+}
+
+fn compile_attr<'i>(
+  code: &'i str,
+  config: &AttrConfig,
+  #[allow(unused_variables)] visitor: &mut Option<JsVisitor>,
+) -> Result<AttrResult<'i>, CompileError<'i, napi::Error>> {
+  let warnings = if config.error_recovery {
+    Some(Arc::new(RwLock::new(Vec::new())))
+  } else {
+    None
+  };
+  let res = {
+    let filename = config.filename.clone().unwrap_or_default();
+    let mut attr = StyleAttribute::parse(
+      &code,
+      ParserOptions {
+        filename,
+        error_recovery: config.error_recovery,
+        warnings: warnings.clone(),
+        ..ParserOptions::default()
+      },
+    )?;
+
+    #[cfg(feature = "visitor")]
+    if let Some(visitor) = visitor.as_mut() {
+      attr.visit(visitor).unwrap();
+    }
+
+    let targets = Targets {
+      browsers: config.targets,
+      include: Features::from_bits_truncate(config.include),
+      exclude: Features::from_bits_truncate(config.exclude),
+    };
+
+    attr.minify(MinifyOptions {
+      targets,
+      ..MinifyOptions::default()
+    });
+    attr.to_css(PrinterOptions {
+      minify: config.minify,
+      source_map: None,
+      project_root: None,
+      targets,
+      analyze_dependencies: if config.analyze_dependencies {
+        Some(DependencyOptions::default())
+      } else {
+        None
+      },
+      pseudo_classes: None,
+    })?
+  };
+  Ok(AttrResult {
+    code: res.code.into_bytes(),
+    dependencies: res.dependencies,
+    warnings: warnings.map_or(Vec::new(), |w| {
+      Arc::try_unwrap(w)
+        .unwrap()
+        .into_inner()
+        .unwrap()
+        .into_iter()
+        .map(|w| w.into())
+        .collect()
+    }),
+  })
+}
+
+enum CompileError<'i, E: std::error::Error> {
+  ParseError(Error<ParserError<'i>>),
+  MinifyError(Error<MinifyErrorKind>),
+  PrinterError(Error<PrinterErrorKind>),
+  SourceMapError(parcel_sourcemap::SourceMapError),
+  BundleError(Error<BundleErrorKind<'i, E>>),
+  PatternError(PatternParseError),
+  #[cfg(feature = "visitor")]
+  JsError(napi::Error),
+}
+
+impl<'i, E: std::error::Error> std::fmt::Display for CompileError<'i, E> {
+  fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+    match self {
+      CompileError::ParseError(err) => err.kind.fmt(f),
+      CompileError::MinifyError(err) => err.kind.fmt(f),
+      CompileError::PrinterError(err) => err.kind.fmt(f),
+      CompileError::BundleError(err) => err.kind.fmt(f),
+      CompileError::PatternError(err) => err.fmt(f),
+      CompileError::SourceMapError(err) => write!(f, "{}", err.to_string()), // TODO: switch to `fmt::Display` once parcel_sourcemap supports this
+      #[cfg(feature = "visitor")]
+      CompileError::JsError(err) => std::fmt::Debug::fmt(&err, f),
+    }
+  }
+}
+
+impl<'i, E: IntoJsError + std::error::Error> CompileError<'i, E> {
+  fn into_js_error(self, env: Env, code: Option<&str>) -> napi::Result<napi::Error> {
+    let reason = self.to_string();
+    let data = match &self {
+      CompileError::ParseError(Error { kind, .. }) => env.to_js_value(kind)?,
+      CompileError::PrinterError(Error { kind, .. }) => env.to_js_value(kind)?,
+      CompileError::MinifyError(Error { kind, .. }) => env.to_js_value(kind)?,
+      CompileError::BundleError(Error { kind, .. }) => env.to_js_value(kind)?,
+      _ => env.get_null()?.into_unknown(),
+    };
+
+    let (js_error, loc) = match self {
+      CompileError::BundleError(Error {
+        loc,
+        kind: BundleErrorKind::ResolverError(e),
+      }) => {
+        // Add location info to existing JS error if available.
+        (e.into_js_error(env)?, loc)
+      }
+      CompileError::ParseError(Error { loc, .. })
+      | CompileError::PrinterError(Error { loc, .. })
+      | CompileError::MinifyError(Error { loc, .. })
+      | CompileError::BundleError(Error { loc, .. }) => {
+        // Generate an error with location information.
+        let syntax_error = env.get_global()?.get_named_property::<napi::JsFunction>("SyntaxError")?;
+        let reason = env.create_string_from_std(reason)?;
+        let obj = syntax_error.new_instance(&[reason])?;
+        (obj.into_unknown(), loc)
+      }
+      _ => return Ok(self.into()),
+    };
+
+    if js_error.get_type()? == napi::ValueType::Object {
+      let mut obj: JsObject = unsafe { js_error.cast() };
+      if let Some(loc) = loc {
+        let line = env.create_int32((loc.line + 1) as i32)?;
+        let col = env.create_int32(loc.column as i32)?;
+        let filename = env.create_string_from_std(loc.filename)?;
+        obj.set_named_property("fileName", filename)?;
+        if let Some(code) = code {
+          let source = env.create_string(code)?;
+          obj.set_named_property("source", source)?;
+        }
+        let mut loc = env.create_object()?;
+        loc.set_named_property("line", line)?;
+        loc.set_named_property("column", col)?;
+        obj.set_named_property("loc", loc)?;
+      }
+      obj.set_named_property("data", data)?;
+      Ok(obj.into_unknown().into())
+    } else {
+      Ok(js_error.into())
+    }
+  }
+}
+
+trait IntoJsError {
+  fn into_js_error(self, env: Env) -> napi::Result<JsUnknown>;
+}
+
+impl IntoJsError for std::io::Error {
+  fn into_js_error(self, env: Env) -> napi::Result<JsUnknown> {
+    let reason = self.to_string();
+    let syntax_error = env.get_global()?.get_named_property::<napi::JsFunction>("SyntaxError")?;
+    let reason = env.create_string_from_std(reason)?;
+    let obj = syntax_error.new_instance(&[reason])?;
+    Ok(obj.into_unknown())
+  }
+}
+
+impl IntoJsError for napi::Error {
+  fn into_js_error(self, env: Env) -> napi::Result<JsUnknown> {
+    unsafe { JsUnknown::from_napi_value(env.raw(), ToNapiValue::to_napi_value(env.raw(), self)?) }
+  }
+}
+
+impl<'i, E: std::error::Error> From<Error<ParserError<'i>>> for CompileError<'i, E> {
+  fn from(e: Error<ParserError<'i>>) -> CompileError<'i, E> {
+    CompileError::ParseError(e)
+  }
+}
+
+impl<'i, E: std::error::Error> From<Error<MinifyErrorKind>> for CompileError<'i, E> {
+  fn from(err: Error<MinifyErrorKind>) -> CompileError<'i, E> {
+    CompileError::MinifyError(err)
+  }
+}
+
+impl<'i, E: std::error::Error> From<Error<PrinterErrorKind>> for CompileError<'i, E> {
+  fn from(err: Error<PrinterErrorKind>) -> CompileError<'i, E> {
+    CompileError::PrinterError(err)
+  }
+}
+
+impl<'i, E: std::error::Error> From<parcel_sourcemap::SourceMapError> for CompileError<'i, E> {
+  fn from(e: parcel_sourcemap::SourceMapError) -> CompileError<'i, E> {
+    CompileError::SourceMapError(e)
+  }
+}
+
+impl<'i, E: std::error::Error> From<Error<BundleErrorKind<'i, E>>> for CompileError<'i, E> {
+  fn from(e: Error<BundleErrorKind<'i, E>>) -> CompileError<'i, E> {
+    CompileError::BundleError(e)
+  }
+}
+
+impl<'i, E: std::error::Error> From<CompileError<'i, E>> for napi::Error {
+  fn from(e: CompileError<'i, E>) -> napi::Error {
+    match e {
+      CompileError::SourceMapError(e) => napi::Error::from_reason(e.to_string()),
+      CompileError::PatternError(e) => napi::Error::from_reason(e.to_string()),
+      #[cfg(feature = "visitor")]
+      CompileError::JsError(e) => e,
+      _ => napi::Error::new(napi::Status::GenericFailure, e.to_string()),
+    }
+  }
+}
+
+#[derive(Serialize)]
+struct Warning<'i> {
+  message: String,
+  #[serde(flatten)]
+  data: ParserError<'i>,
+  loc: Option<ErrorLocation>,
+}
+
+impl<'i> From<Error<ParserError<'i>>> for Warning<'i> {
+  fn from(mut e: Error<ParserError<'i>>) -> Self {
+    // Convert to 1-based line numbers.
+    if let Some(loc) = &mut e.loc {
+      loc.line += 1;
+    }
+    Warning {
+      message: e.kind.to_string(),
+      data: e.kind,
+      loc: e.loc,
+    }
+  }
+}
diff --git a/napi/src/threadsafe_function.rs b/napi/src/threadsafe_function.rs
new file mode 100644
index 0000000..45ac6ec
--- /dev/null
+++ b/napi/src/threadsafe_function.rs
@@ -0,0 +1,288 @@
+// Fork of threadsafe_function from napi-rs that allows calling JS function manually rather than
+// only returning args. This enables us to use the return value of the function.
+
+#![allow(clippy::single_component_path_imports)]
+
+use std::convert::Into;
+use std::ffi::CString;
+use std::marker::PhantomData;
+use std::os::raw::c_void;
+use std::ptr;
+use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
+use std::sync::Arc;
+
+use napi::{check_status, sys, Env, Result, Status};
+use napi::{JsError, JsFunction, NapiValue};
+
+/// ThreadSafeFunction Context object
+/// the `value` is the value passed to `call` method
+pub struct ThreadSafeCallContext<T: 'static> {
+  pub env: Env,
+  pub value: T,
+  pub callback: Option<JsFunction>,
+}
+
+#[repr(u8)]
+pub enum ThreadsafeFunctionCallMode {
+  NonBlocking,
+  Blocking,
+}
+
+impl From<ThreadsafeFunctionCallMode> for sys::napi_threadsafe_function_call_mode {
+  fn from(value: ThreadsafeFunctionCallMode) -> Self {
+    match value {
+      ThreadsafeFunctionCallMode::Blocking => sys::ThreadsafeFunctionCallMode::blocking,
+      ThreadsafeFunctionCallMode::NonBlocking => sys::ThreadsafeFunctionCallMode::nonblocking,
+    }
+  }
+}
+
+/// Communicate with the addon's main thread by invoking a JavaScript function from other threads.
+///
+/// ## Example
+/// An example of using `ThreadsafeFunction`:
+///
+/// ```rust
+/// #[macro_use]
+/// extern crate napi_derive;
+///
+/// use std::thread;
+///
+/// use napi::{
+///     threadsafe_function::{
+///         ThreadSafeCallContext, ThreadsafeFunctionCallMode, ThreadsafeFunctionReleaseMode,
+///     },
+///     CallContext, Error, JsFunction, JsNumber, JsUndefined, Result, Status,
+/// };
+///
+/// #[js_function(1)]
+/// pub fn test_threadsafe_function(ctx: CallContext) -> Result<JsUndefined> {
+///   let func = ctx.get::<JsFunction>(0)?;
+///
+///   let tsfn =
+///       ctx
+///           .env
+///           .create_threadsafe_function(&func, 0, |ctx: ThreadSafeCallContext<Vec<u32>>| {
+///             ctx.value
+///                 .iter()
+///                 .map(|v| ctx.env.create_uint32(*v))
+///                 .collect::<Result<Vec<JsNumber>>>()
+///           })?;
+///
+///   let tsfn_cloned = tsfn.clone();
+///
+///   thread::spawn(move || {
+///       let output: Vec<u32> = vec![0, 1, 2, 3];
+///       // It's okay to call a threadsafe function multiple times.
+///       tsfn.call(Ok(output.clone()), ThreadsafeFunctionCallMode::Blocking);
+///   });
+///
+///   thread::spawn(move || {
+///       let output: Vec<u32> = vec![3, 2, 1, 0];
+///       // It's okay to call a threadsafe function multiple times.
+///       tsfn_cloned.call(Ok(output.clone()), ThreadsafeFunctionCallMode::NonBlocking);
+///   });
+///
+///   ctx.env.get_undefined()
+/// }
+/// ```
+pub struct ThreadsafeFunction<T: 'static> {
+  raw_tsfn: sys::napi_threadsafe_function,
+  aborted: Arc<AtomicBool>,
+  ref_count: Arc<AtomicUsize>,
+  _phantom: PhantomData<T>,
+}
+
+impl<T: 'static> Clone for ThreadsafeFunction<T> {
+  fn clone(&self) -> Self {
+    if !self.aborted.load(Ordering::Acquire) {
+      let acquire_status = unsafe { sys::napi_acquire_threadsafe_function(self.raw_tsfn) };
+      debug_assert!(
+        acquire_status == sys::Status::napi_ok,
+        "Acquire threadsafe function failed in clone"
+      );
+    }
+
+    Self {
+      raw_tsfn: self.raw_tsfn,
+      aborted: Arc::clone(&self.aborted),
+      ref_count: Arc::clone(&self.ref_count),
+      _phantom: PhantomData,
+    }
+  }
+}
+
+unsafe impl<T> Send for ThreadsafeFunction<T> {}
+unsafe impl<T> Sync for ThreadsafeFunction<T> {}
+
+impl<T: 'static> ThreadsafeFunction<T> {
+  /// See [napi_create_threadsafe_function](https://nodejs.org/api/n-api.html#n_api_napi_create_threadsafe_function)
+  /// for more information.
+  pub(crate) fn create<R: 'static + Send + FnMut(ThreadSafeCallContext<T>) -> Result<()>>(
+    env: sys::napi_env,
+    func: sys::napi_value,
+    max_queue_size: usize,
+    callback: R,
+  ) -> Result<Self> {
+    let mut async_resource_name = ptr::null_mut();
+    let s = "napi_rs_threadsafe_function";
+    let len = s.len();
+    let s = CString::new(s)?;
+    check_status!(unsafe { sys::napi_create_string_utf8(env, s.as_ptr(), len, &mut async_resource_name) })?;
+
+    let initial_thread_count = 1usize;
+    let mut raw_tsfn = ptr::null_mut();
+    let ptr = Box::into_raw(Box::new(callback)) as *mut c_void;
+    check_status!(unsafe {
+      sys::napi_create_threadsafe_function(
+        env,
+        func,
+        ptr::null_mut(),
+        async_resource_name,
+        max_queue_size,
+        initial_thread_count,
+        ptr,
+        Some(thread_finalize_cb::<T, R>),
+        ptr,
+        Some(call_js_cb::<T, R>),
+        &mut raw_tsfn,
+      )
+    })?;
+
+    let aborted = Arc::new(AtomicBool::new(false));
+    let aborted_ptr = Arc::into_raw(aborted.clone()) as *mut c_void;
+    check_status!(unsafe { sys::napi_add_env_cleanup_hook(env, Some(cleanup_cb), aborted_ptr) })?;
+
+    Ok(ThreadsafeFunction {
+      raw_tsfn,
+      aborted,
+      ref_count: Arc::new(AtomicUsize::new(initial_thread_count)),
+      _phantom: PhantomData,
+    })
+  }
+}
+
+impl<T: 'static> ThreadsafeFunction<T> {
+  /// See [napi_call_threadsafe_function](https://nodejs.org/api/n-api.html#n_api_napi_call_threadsafe_function)
+  /// for more information.
+  pub fn call(&self, value: T, mode: ThreadsafeFunctionCallMode) -> Status {
+    if self.aborted.load(Ordering::Acquire) {
+      return Status::Closing;
+    }
+    unsafe {
+      sys::napi_call_threadsafe_function(self.raw_tsfn, Box::into_raw(Box::new(value)) as *mut _, mode.into())
+    }
+    .into()
+  }
+}
+
+impl<T: 'static> Drop for ThreadsafeFunction<T> {
+  fn drop(&mut self) {
+    if !self.aborted.load(Ordering::Acquire) && self.ref_count.load(Ordering::Acquire) > 0usize {
+      let release_status = unsafe {
+        sys::napi_release_threadsafe_function(self.raw_tsfn, sys::ThreadsafeFunctionReleaseMode::release)
+      };
+      assert!(
+        release_status == sys::Status::napi_ok,
+        "Threadsafe Function release failed"
+      );
+    }
+  }
+}
+
+unsafe extern "C" fn cleanup_cb(cleanup_data: *mut c_void) {
+  let aborted = Arc::<AtomicBool>::from_raw(cleanup_data.cast());
+  aborted.store(true, Ordering::SeqCst);
+}
+
+unsafe extern "C" fn thread_finalize_cb<T: 'static, R>(
+  _raw_env: sys::napi_env,
+  finalize_data: *mut c_void,
+  _finalize_hint: *mut c_void,
+) where
+  R: 'static + Send + FnMut(ThreadSafeCallContext<T>) -> Result<()>,
+{
+  // cleanup
+  drop(Box::<R>::from_raw(finalize_data.cast()));
+}
+
+unsafe extern "C" fn call_js_cb<T: 'static, R>(
+  raw_env: sys::napi_env,
+  js_callback: sys::napi_value,
+  context: *mut c_void,
+  data: *mut c_void,
+) where
+  R: 'static + Send + FnMut(ThreadSafeCallContext<T>) -> Result<()>,
+{
+  // env and/or callback can be null when shutting down
+  if raw_env.is_null() {
+    return;
+  }
+
+  let ctx: &mut R = &mut *context.cast::<R>();
+  let val: Result<T> = Ok(*Box::<T>::from_raw(data.cast()));
+
+  let mut recv = ptr::null_mut();
+  sys::napi_get_undefined(raw_env, &mut recv);
+
+  let ret = val.and_then(|v| {
+    (ctx)(ThreadSafeCallContext {
+      env: Env::from_raw(raw_env),
+      value: v,
+      callback: if js_callback.is_null() {
+        None
+      } else {
+        Some(JsFunction::from_raw(raw_env, js_callback).unwrap()) // TODO: unwrap
+      },
+    })
+  });
+
+  let status = match ret {
+    Ok(()) => sys::Status::napi_ok,
+    Err(e) => sys::napi_fatal_exception(raw_env, JsError::from(e).into_value(raw_env)),
+  };
+  if status == sys::Status::napi_ok {
+    return;
+  }
+  if status == sys::Status::napi_pending_exception {
+    let mut error_result = ptr::null_mut();
+    assert_eq!(
+      sys::napi_get_and_clear_last_exception(raw_env, &mut error_result),
+      sys::Status::napi_ok
+    );
+
+    // When shutting down, napi_fatal_exception sometimes returns another exception
+    let stat = sys::napi_fatal_exception(raw_env, error_result);
+    assert!(stat == sys::Status::napi_ok || stat == sys::Status::napi_pending_exception);
+  } else {
+    let error_code: Status = status.into();
+    let error_code_string = format!("{:?}", error_code);
+    let mut error_code_value = ptr::null_mut();
+    assert_eq!(
+      sys::napi_create_string_utf8(
+        raw_env,
+        error_code_string.as_ptr() as *const _,
+        error_code_string.len(),
+        &mut error_code_value,
+      ),
+      sys::Status::napi_ok,
+    );
+    let error_msg = "Call JavaScript callback failed in thread safe function";
+    let mut error_msg_value = ptr::null_mut();
+    assert_eq!(
+      sys::napi_create_string_utf8(
+        raw_env,
+        error_msg.as_ptr() as *const _,
+        error_msg.len(),
+        &mut error_msg_value,
+      ),
+      sys::Status::napi_ok,
+    );
+    let mut error_value = ptr::null_mut();
+    assert_eq!(
+      sys::napi_create_error(raw_env, error_code_value, error_msg_value, &mut error_value),
+      sys::Status::napi_ok,
+    );
+    assert_eq!(sys::napi_fatal_exception(raw_env, error_value), sys::Status::napi_ok);
+  }
+}
diff --git a/napi/src/transformer.rs b/napi/src/transformer.rs
new file mode 100644
index 0000000..29875b8
--- /dev/null
+++ b/napi/src/transformer.rs
@@ -0,0 +1,887 @@
+use std::{
+  marker::PhantomData,
+  ops::{Index, IndexMut},
+};
+
+use lightningcss::{
+  media_query::MediaFeatureValue,
+  properties::{
+    custom::{Token, TokenList, TokenOrValue},
+    Property,
+  },
+  rules::{CssRule, CssRuleList},
+  stylesheet::ParserOptions,
+  traits::ParseWithOptions,
+  values::{
+    ident::Ident,
+    length::{Length, LengthValue},
+    string::CowArcStr,
+  },
+  visitor::{Visit, VisitTypes, Visitor},
+};
+use lightningcss::{stylesheet::StyleSheet, traits::IntoOwned};
+use napi::{Env, JsFunction, JsObject, JsUnknown, Ref, ValueType};
+use serde::{Deserialize, Serialize};
+use smallvec::SmallVec;
+
+use crate::{at_rule_parser::AtRule, utils::get_named_property};
+
+pub struct JsVisitor {
+  env: Env,
+  visit_stylesheet: VisitorsRef,
+  visit_rule: VisitorsRef,
+  rule_map: VisitorsRef,
+  property_map: VisitorsRef,
+  visit_declaration: VisitorsRef,
+  visit_length: Option<Ref<()>>,
+  visit_angle: Option<Ref<()>>,
+  visit_ratio: Option<Ref<()>>,
+  visit_resolution: Option<Ref<()>>,
+  visit_time: Option<Ref<()>>,
+  visit_color: Option<Ref<()>>,
+  visit_image: VisitorsRef,
+  visit_url: Option<Ref<()>>,
+  visit_media_query: VisitorsRef,
+  visit_supports_condition: VisitorsRef,
+  visit_custom_ident: Option<Ref<()>>,
+  visit_dashed_ident: Option<Ref<()>>,
+  visit_selector: Option<Ref<()>>,
+  visit_token: VisitorsRef,
+  token_map: VisitorsRef,
+  visit_function: VisitorsRef,
+  function_map: VisitorsRef,
+  visit_variable: VisitorsRef,
+  visit_env: VisitorsRef,
+  env_map: VisitorsRef,
+  types: VisitTypes,
+}
+
+// This is so that the visitor can work with bundleAsync.
+// We ensure that we only call JsVisitor from the main JS thread.
+unsafe impl Send for JsVisitor {}
+
+#[derive(PartialEq, Eq, Clone, Copy)]
+enum VisitStage {
+  Enter,
+  Exit,
+}
+
+type VisitorsRef = Visitors<Ref<()>>;
+
+struct Visitors<T> {
+  enter: Option<T>,
+  exit: Option<T>,
+}
+
+impl<T> Visitors<T> {
+  fn new(enter: Option<T>, exit: Option<T>) -> Self {
+    Self { enter, exit }
+  }
+
+  fn for_stage(&self, stage: VisitStage) -> Option<&T> {
+    match stage {
+      VisitStage::Enter => self.enter.as_ref(),
+      VisitStage::Exit => self.exit.as_ref(),
+    }
+  }
+}
+
+impl Visitors<Ref<()>> {
+  fn get<U: napi::NapiValue>(&self, env: &Env) -> Visitors<U> {
+    Visitors {
+      enter: self.enter.as_ref().and_then(|p| env.get_reference_value_unchecked(p).ok()),
+      exit: self.exit.as_ref().and_then(|p| env.get_reference_value_unchecked(p).ok()),
+    }
+  }
+}
+
+impl Visitors<JsObject> {
+  fn named(&self, stage: VisitStage, name: &str) -> Option<JsFunction> {
+    self
+      .for_stage(stage)
+      .and_then(|m| get_named_property::<JsFunction>(m, name).ok())
+  }
+
+  fn custom(&self, stage: VisitStage, obj: &str, name: &str) -> Option<JsFunction> {
+    self
+      .for_stage(stage)
+      .and_then(|m| m.get_named_property::<JsUnknown>(obj).ok())
+      .and_then(|v| {
+        match v.get_type() {
+          Ok(ValueType::Function) => return v.try_into().ok(),
+          Ok(ValueType::Object) => {
+            let o: napi::Result<JsObject> = v.try_into();
+            if let Ok(o) = o {
+              return get_named_property::<JsFunction>(&o, name).ok();
+            }
+          }
+          _ => {}
+        }
+
+        None
+      })
+  }
+}
+
+impl Drop for JsVisitor {
+  fn drop(&mut self) {
+    macro_rules! drop {
+      ($id: ident) => {
+        if let Some(v) = &mut self.$id {
+          drop(v.unref(self.env));
+        }
+      };
+    }
+
+    macro_rules! drop_tuple {
+      ($id: ident) => {
+        if let Some(v) = &mut self.$id.enter {
+          drop(v.unref(self.env));
+        }
+        if let Some(v) = &mut self.$id.exit {
+          drop(v.unref(self.env));
+        }
+      };
+    }
+
+    drop_tuple!(visit_stylesheet);
+    drop_tuple!(visit_rule);
+    drop_tuple!(rule_map);
+    drop_tuple!(visit_declaration);
+    drop_tuple!(property_map);
+    drop!(visit_length);
+    drop!(visit_angle);
+    drop!(visit_ratio);
+    drop!(visit_resolution);
+    drop!(visit_time);
+    drop!(visit_color);
+    drop_tuple!(visit_image);
+    drop!(visit_url);
+    drop_tuple!(visit_media_query);
+    drop_tuple!(visit_supports_condition);
+    drop_tuple!(visit_variable);
+    drop_tuple!(visit_env);
+    drop_tuple!(env_map);
+    drop!(visit_custom_ident);
+    drop!(visit_dashed_ident);
+    drop_tuple!(visit_function);
+    drop_tuple!(function_map);
+    drop!(visit_selector);
+    drop_tuple!(visit_token);
+    drop_tuple!(token_map);
+  }
+}
+
+impl JsVisitor {
+  pub fn new(env: Env, visitor: JsObject) -> Self {
+    let mut types = VisitTypes::empty();
+    macro_rules! get {
+      ($name: literal, $( $t: ident )|+) => {{
+        let res: Option<JsFunction> = get_named_property(&visitor, $name).ok();
+
+        if res.is_some() {
+          types |= $( VisitTypes::$t )|+;
+        }
+
+        // We must create a reference so that the garbage collector doesn't destroy
+        // the function before we try to call it (in the async bundle case).
+        res.and_then(|res| env.create_reference(res).ok())
+      }};
+    }
+
+    macro_rules! map {
+      ($name: literal, $( $t: ident )|+) => {{
+        let obj: Option<JsObject> = get_named_property(&visitor, $name).ok();
+
+        if obj.is_some() {
+          types |= $( VisitTypes::$t )|+;
+        }
+
+        obj.and_then(|obj| env.create_reference(obj).ok())
+      }};
+    }
+
+    Self {
+      env,
+      visit_stylesheet: VisitorsRef::new(get!("StyleSheet", RULES), get!("StyleSheetExit", RULES)),
+      visit_rule: VisitorsRef::new(get!("Rule", RULES), get!("RuleExit", RULES)),
+      rule_map: VisitorsRef::new(map!("Rule", RULES), get!("RuleExit", RULES)),
+      visit_declaration: VisitorsRef::new(get!("Declaration", PROPERTIES), get!("DeclarationExit", PROPERTIES)),
+      property_map: VisitorsRef::new(map!("Declaration", PROPERTIES), map!("DeclarationExit", PROPERTIES)),
+      visit_length: get!("Length", LENGTHS),
+      visit_angle: get!("Angle", ANGLES),
+      visit_ratio: get!("Ratio", RATIOS),
+      visit_resolution: get!("Resolution", RESOLUTIONS),
+      visit_time: get!("Time", TIMES),
+      visit_color: get!("Color", COLORS),
+      visit_image: VisitorsRef::new(get!("Image", IMAGES), get!("ImageExit", IMAGES)),
+      visit_url: get!("Url", URLS),
+      visit_media_query: VisitorsRef::new(
+        get!("MediaQuery", MEDIA_QUERIES),
+        get!("MediaQueryExit", MEDIA_QUERIES),
+      ),
+      visit_supports_condition: VisitorsRef::new(
+        get!("SupportsCondition", SUPPORTS_CONDITIONS),
+        get!("SupportsConditionExit", SUPPORTS_CONDITIONS),
+      ),
+      visit_variable: VisitorsRef::new(get!("Variable", TOKENS), get!("VariableExit", TOKENS)),
+      visit_env: VisitorsRef::new(
+        get!("EnvironmentVariable", TOKENS | MEDIA_QUERIES | ENVIRONMENT_VARIABLES),
+        get!(
+          "EnvironmentVariableExit",
+          TOKENS | MEDIA_QUERIES | ENVIRONMENT_VARIABLES
+        ),
+      ),
+      env_map: VisitorsRef::new(
+        map!("EnvironmentVariable", TOKENS | MEDIA_QUERIES | ENVIRONMENT_VARIABLES),
+        map!(
+          "EnvironmentVariableExit",
+          TOKENS | MEDIA_QUERIES | ENVIRONMENT_VARIABLES
+        ),
+      ),
+      visit_custom_ident: get!("CustomIdent", CUSTOM_IDENTS),
+      visit_dashed_ident: get!("DashedIdent", DASHED_IDENTS),
+      visit_function: VisitorsRef::new(get!("Function", TOKENS), get!("FunctionExit", TOKENS)),
+      function_map: VisitorsRef::new(map!("Function", TOKENS), map!("FunctionExit", TOKENS)),
+      visit_selector: get!("Selector", SELECTORS),
+      visit_token: VisitorsRef::new(get!("Token", TOKENS), None),
+      token_map: VisitorsRef::new(map!("Token", TOKENS), None),
+      types,
+    }
+  }
+}
+
+impl<'i> Visitor<'i, AtRule<'i>> for JsVisitor {
+  type Error = napi::Error;
+
+  fn visit_types(&self) -> VisitTypes {
+    self.types
+  }
+
+  fn visit_stylesheet<'o>(&mut self, stylesheet: &mut StyleSheet<'i, 'o, AtRule<'i>>) -> Result<(), Self::Error> {
+    if self.types.contains(VisitTypes::RULES) {
+      let env = self.env;
+      let visit_stylesheet = self.visit_stylesheet.get::<JsFunction>(&env);
+      if let Some(visit) = visit_stylesheet.for_stage(VisitStage::Enter) {
+        call_visitor(&env, stylesheet, visit)?
+      }
+
+      stylesheet.visit_children(self)?;
+
+      if let Some(visit) = visit_stylesheet.for_stage(VisitStage::Exit) {
+        call_visitor(&env, stylesheet, visit)?
+      }
+
+      Ok(())
+    } else {
+      stylesheet.visit_children(self)
+    }
+  }
+
+  fn visit_rule_list(
+    &mut self,
+    rules: &mut lightningcss::rules::CssRuleList<'i, AtRule<'i>>,
+  ) -> Result<(), Self::Error> {
+    if self.types.contains(VisitTypes::RULES) {
+      let env = self.env;
+      let rule_map = self.rule_map.get::<JsObject>(&env);
+      let visit_rule = self.visit_rule.get::<JsFunction>(&env);
+
+      visit_list(
+        rules,
+        |value, stage| {
+          // Use a more specific visitor function if available, but fall back to visit_rule.
+          let name = match value {
+            CssRule::Media(..) => "media",
+            CssRule::Import(..) => "import",
+            CssRule::Style(..) => "style",
+            CssRule::Keyframes(..) => "keyframes",
+            CssRule::FontFace(..) => "font-face",
+            CssRule::FontPaletteValues(..) => "font-palette-values",
+            CssRule::FontFeatureValues(..) => "font-feature-values",
+            CssRule::Page(..) => "page",
+            CssRule::Supports(..) => "supports",
+            CssRule::CounterStyle(..) => "counter-style",
+            CssRule::Namespace(..) => "namespace",
+            CssRule::CustomMedia(..) => "custom-media",
+            CssRule::LayerBlock(..) => "layer-block",
+            CssRule::LayerStatement(..) => "layer-statement",
+            CssRule::Property(..) => "property",
+            CssRule::Container(..) => "container",
+            CssRule::Scope(..) => "scope",
+            CssRule::MozDocument(..) => "moz-document",
+            CssRule::Nesting(..) => "nesting",
+            CssRule::NestedDeclarations(..) => "nested-declarations",
+            CssRule::Viewport(..) => "viewport",
+            CssRule::StartingStyle(..) => "starting-style",
+            CssRule::ViewTransition(..) => "view-transition",
+            CssRule::Unknown(v) => {
+              let name = v.name.as_ref();
+              if let Some(visit) = rule_map.custom(stage, "unknown", name) {
+                let js_value = env.to_js_value(v)?;
+                let res = visit.call(None, &[js_value])?;
+                return env.from_js_value(res).map(serde_detach::detach);
+              } else {
+                "unknown"
+              }
+            }
+            CssRule::Custom(c) => {
+              let name = c.name.as_ref();
+              if let Some(visit) = rule_map.custom(stage, "custom", name) {
+                let js_value = env.to_js_value(c)?;
+                let res = visit.call(None, &[js_value])?;
+                return env.from_js_value(res).map(serde_detach::detach);
+              } else {
+                "custom"
+              }
+            }
+            CssRule::Ignored => return Ok(None),
+          };
+
+          if let Some(visit) = rule_map.named(stage, name).as_ref().or(visit_rule.for_stage(stage)) {
+            let js_value = env.to_js_value(value)?;
+            let res = visit.call(None, &[js_value])?;
+            env.from_js_value(res).map(serde_detach::detach)
+          } else {
+            Ok(None)
+          }
+        },
+        |rule| rule.visit_children(self),
+      )?;
+
+      Ok(())
+    } else {
+      rules.visit_children(self)
+    }
+  }
+
+  fn visit_declaration_block(
+    &mut self,
+    decls: &mut lightningcss::declaration::DeclarationBlock<'i>,
+  ) -> Result<(), Self::Error> {
+    if self.types.contains(VisitTypes::PROPERTIES) {
+      let env = self.env;
+      let property_map = self.property_map.get::<JsObject>(&env);
+      let visit_declaration = self.visit_declaration.get::<JsFunction>(&env);
+      visit_declaration_list(
+        &env,
+        &mut decls.important_declarations,
+        &visit_declaration,
+        &property_map,
+        |property| property.visit_children(self),
+      )?;
+      visit_declaration_list(
+        &env,
+        &mut decls.declarations,
+        &visit_declaration,
+        &property_map,
+        |property| property.visit_children(self),
+      )?;
+      Ok(())
+    } else {
+      decls.visit_children(self)
+    }
+  }
+
+  fn visit_length(&mut self, length: &mut LengthValue) -> Result<(), Self::Error> {
+    visit(&self.env, length, &self.visit_length)
+  }
+
+  fn visit_angle(&mut self, angle: &mut lightningcss::values::angle::Angle) -> Result<(), Self::Error> {
+    visit(&self.env, angle, &self.visit_angle)
+  }
+
+  fn visit_ratio(&mut self, ratio: &mut lightningcss::values::ratio::Ratio) -> Result<(), Self::Error> {
+    visit(&self.env, ratio, &self.visit_ratio)
+  }
+
+  fn visit_resolution(
+    &mut self,
+    resolution: &mut lightningcss::values::resolution::Resolution,
+  ) -> Result<(), Self::Error> {
+    visit(&self.env, resolution, &self.visit_resolution)
+  }
+
+  fn visit_time(&mut self, time: &mut lightningcss::values::time::Time) -> Result<(), Self::Error> {
+    visit(&self.env, time, &self.visit_time)
+  }
+
+  fn visit_color(&mut self, color: &mut lightningcss::values::color::CssColor) -> Result<(), Self::Error> {
+    visit(&self.env, color, &self.visit_color)
+  }
+
+  fn visit_image(&mut self, image: &mut lightningcss::values::image::Image<'i>) -> Result<(), Self::Error> {
+    visit(&self.env, image, &self.visit_image.enter)?;
+    image.visit_children(self)?;
+    visit(&self.env, image, &self.visit_image.exit)
+  }
+
+  fn visit_url(&mut self, url: &mut lightningcss::values::url::Url<'i>) -> Result<(), Self::Error> {
+    visit(&self.env, url, &self.visit_url)
+  }
+
+  fn visit_media_list(&mut self, media: &mut lightningcss::media_query::MediaList<'i>) -> Result<(), Self::Error> {
+    if self.types.contains(VisitTypes::MEDIA_QUERIES) {
+      let env = self.env;
+      let visit_media_query = self.visit_media_query.get::<JsFunction>(&env);
+      visit_list(
+        &mut media.media_queries,
+        |value, stage| {
+          if let Some(visit) = visit_media_query.for_stage(stage) {
+            let js_value = env.to_js_value(value)?;
+            let res = visit.call(None, &[js_value])?;
+            env.from_js_value(res).map(serde_detach::detach)
+          } else {
+            Ok(None)
+          }
+        },
+        |q| q.visit_children(self),
+      )?;
+      Ok(())
+    } else {
+      media.visit_children(self)
+    }
+  }
+
+  fn visit_media_feature_value(&mut self, value: &mut MediaFeatureValue<'i>) -> Result<(), Self::Error> {
+    if self.types.contains(VisitTypes::ENVIRONMENT_VARIABLES) && matches!(value, MediaFeatureValue::Env(_)) {
+      let env_map = self.env_map.get::<JsObject>(&self.env);
+      let visit_env = self.visit_env.get::<JsFunction>(&self.env);
+      let call = |stage: VisitStage, value: &mut MediaFeatureValue, env: &Env| -> napi::Result<()> {
+        let env_var = if let MediaFeatureValue::Env(env) = value {
+          env
+        } else {
+          return Ok(());
+        };
+        let visit_type = env_map.named(stage, env_var.name.name());
+        let visit = visit_env.for_stage(stage);
+        let new_value: Option<TokenOrValue> = if let Some(visit) = visit_type.as_ref().or(visit) {
+          let js_value = env.to_js_value(env_var)?;
+          let res = visit.call(None, &[js_value])?;
+          env.from_js_value(res).map(serde_detach::detach)?
+        } else {
+          None
+        };
+
+        match new_value {
+          None => return Ok(()),
+          Some(TokenOrValue::Length(l)) => *value = MediaFeatureValue::Length(Length::Value(l)),
+          Some(TokenOrValue::Resolution(r)) => *value = MediaFeatureValue::Resolution(r),
+          Some(TokenOrValue::Token(Token::Number { value: n, .. })) => *value = MediaFeatureValue::Number(n),
+          Some(TokenOrValue::Token(Token::Ident(ident))) => *value = MediaFeatureValue::Ident(Ident(ident)),
+          // TODO: ratio
+          _ => {
+            return Err(napi::Error::new(
+              napi::Status::InvalidArg,
+              format!("invalid environment value in media query: {:?}", new_value),
+            ))
+          }
+        }
+
+        Ok(())
+      };
+
+      call(VisitStage::Enter, value, &self.env)?;
+      value.visit_children(self)?;
+      call(VisitStage::Exit, value, &self.env)?;
+      return Ok(());
+    }
+
+    value.visit_children(self)
+  }
+
+  fn visit_supports_condition(
+    &mut self,
+    condition: &mut lightningcss::rules::supports::SupportsCondition<'i>,
+  ) -> Result<(), Self::Error> {
+    visit(&self.env, condition, &self.visit_supports_condition.enter)?;
+    condition.visit_children(self)?;
+    visit(&self.env, condition, &self.visit_supports_condition.exit)
+  }
+
+  fn visit_custom_ident(
+    &mut self,
+    ident: &mut lightningcss::values::ident::CustomIdent,
+  ) -> Result<(), Self::Error> {
+    visit(&self.env, ident, &self.visit_custom_ident)
+  }
+
+  fn visit_dashed_ident(
+    &mut self,
+    ident: &mut lightningcss::values::ident::DashedIdent,
+  ) -> Result<(), Self::Error> {
+    visit(&self.env, ident, &self.visit_dashed_ident)
+  }
+
+  fn visit_selector_list(
+    &mut self,
+    selectors: &mut lightningcss::selector::SelectorList<'i>,
+  ) -> Result<(), Self::Error> {
+    if let Some(visit) = self
+      .visit_selector
+      .as_ref()
+      .and_then(|v| self.env.get_reference_value_unchecked::<JsFunction>(v).ok())
+    {
+      map::<_, _, _, true>(&mut selectors.0, |value| {
+        let js_value = self.env.to_js_value(value)?;
+        let res = visit.call(None, &[js_value])?;
+        self.env.from_js_value(res).map(serde_detach::detach)
+      })?;
+    }
+
+    Ok(())
+  }
+
+  fn visit_token_list(
+    &mut self,
+    tokens: &mut lightningcss::properties::custom::TokenList<'i>,
+  ) -> Result<(), Self::Error> {
+    if self.types.contains(VisitTypes::TOKENS) {
+      let env = self.env;
+      let visit_token = self.visit_token.get::<JsFunction>(&env);
+      let token_map = self.token_map.get::<JsObject>(&env);
+      let visit_function = self.visit_function.get::<JsFunction>(&env);
+      let function_map = self.function_map.get::<JsObject>(&env);
+      let visit_variable = self.visit_variable.get::<JsFunction>(&env);
+      let visit_env = self.visit_env.get::<JsFunction>(&env);
+      let env_map = self.env_map.get::<JsObject>(&env);
+
+      visit_list(
+        &mut tokens.0,
+        |value, stage| {
+          let (visit_type, visit) = match value {
+            TokenOrValue::Function(f) => (
+              function_map.named(stage, f.name.0.as_ref()),
+              visit_function.for_stage(stage),
+            ),
+            TokenOrValue::Var(_) => (None, visit_variable.for_stage(stage)),
+            TokenOrValue::Env(e) => (env_map.named(stage, e.name.name()), visit_env.for_stage(stage)),
+            TokenOrValue::Token(t) => {
+              let name = match t {
+                Token::Ident(_) => Some("ident"),
+                Token::AtKeyword(_) => Some("at-keyword"),
+                Token::Hash(_) => Some("hash"),
+                Token::IDHash(_) => Some("id-hash"),
+                Token::String(_) => Some("string"),
+                Token::Number { .. } => Some("number"),
+                Token::Percentage { .. } => Some("percentage"),
+                Token::Dimension { .. } => Some("dimension"),
+                _ => None,
+              };
+              let visit = if let Some(name) = name {
+                token_map.named(stage, name)
+              } else {
+                None
+              };
+              (visit, visit_token.for_stage(stage))
+            }
+            _ => return Ok(None),
+          };
+
+          if let Some(visit) = visit_type.as_ref().or(visit) {
+            let js_value = match value {
+              TokenOrValue::Function(f) => env.to_js_value(f)?,
+              TokenOrValue::Var(v) => env.to_js_value(v)?,
+              TokenOrValue::Env(v) => env.to_js_value(v)?,
+              TokenOrValue::Token(t) => env.to_js_value(t)?,
+              _ => unreachable!(),
+            };
+
+            let res = visit.call(None, &[js_value])?;
+            let res: Option<TokensOrRaw> = env.from_js_value(res).map(serde_detach::detach)?;
+            Ok(res.map(|r| r.0))
+          } else {
+            Ok(None)
+          }
+        },
+        |value| value.visit_children(self),
+      )?;
+
+      Ok(())
+    } else {
+      tokens.visit_children(self)
+    }
+  }
+}
+
+fn visit<V: Serialize + Deserialize<'static>>(
+  env: &Env,
+  value: &mut V,
+  visit: &Option<Ref<()>>,
+) -> napi::Result<()> {
+  if let Some(visit) = visit
+    .as_ref()
+    .and_then(|v| env.get_reference_value_unchecked::<JsFunction>(v).ok())
+  {
+    call_visitor(env, value, &visit)?;
+  }
+
+  Ok(())
+}
+
+fn call_visitor<V: Serialize + Deserialize<'static>>(
+  env: &Env,
+  value: &mut V,
+  visit: &JsFunction,
+) -> napi::Result<()> {
+  let js_value = env.to_js_value(value)?;
+  let res = visit.call(None, &[js_value])?;
+  let new_value: Option<V> = env.from_js_value(res).map(serde_detach::detach)?;
+  match new_value {
+    Some(new_value) => *value = new_value,
+    None => {}
+  }
+
+  Ok(())
+}
+
+fn visit_declaration_list<'i, C: FnMut(&mut Property<'i>) -> napi::Result<()>>(
+  env: &Env,
+  list: &mut Vec<Property<'i>>,
+  visit_declaration: &Visitors<JsFunction>,
+  property_map: &Visitors<JsObject>,
+  visit_children: C,
+) -> napi::Result<()> {
+  visit_list(
+    list,
+    |value, stage| {
+      // Use a specific property visitor if available, or fall back to Property visitor.
+      let visit = match value {
+        Property::Custom(v) => {
+          if let Some(visit) = property_map.custom(stage, "custom", v.name.as_ref()) {
+            let js_value = env.to_js_value(v)?;
+            let res = visit.call(None, &[js_value])?;
+            return env.from_js_value(res).map(serde_detach::detach);
+          } else {
+            None
+          }
+        }
+        _ => property_map.named(stage, value.property_id().name()),
+      };
+
+      if let Some(visit) = visit.as_ref().or(visit_declaration.for_stage(stage)) {
+        let js_value = env.to_js_value(value)?;
+        let res = visit.call(None, &[js_value])?;
+        env.from_js_value(res).map(serde_detach::detach)
+      } else {
+        Ok(None)
+      }
+    },
+    visit_children,
+  )
+}
+
+fn visit_list<
+  V,
+  L: List<V>,
+  F: Fn(&mut V, VisitStage) -> napi::Result<Option<ValueOrVec<V>>>,
+  C: FnMut(&mut V) -> napi::Result<()>,
+>(
+  list: &mut L,
+  visit: F,
+  mut visit_children: C,
+) -> napi::Result<()> {
+  map(list, |value| {
+    let mut new_value: Option<ValueOrVec<V>> = visit(value, VisitStage::Enter)?;
+
+    match &mut new_value {
+      Some(ValueOrVec::Value(v)) => {
+        visit_children(v)?;
+
+        if let Some(val) = visit(v, VisitStage::Exit)? {
+          new_value = Some(val);
+        }
+      }
+      Some(ValueOrVec::Vec(v)) => {
+        map(v, |value| {
+          visit_children(value)?;
+          visit(value, VisitStage::Exit)
+        })?;
+      }
+      None => {
+        visit_children(value)?;
+        if let Some(val) = visit(value, VisitStage::Exit)? {
+          new_value = Some(val);
+        }
+      }
+    }
+
+    Ok(new_value)
+  })
+}
+
+fn map<V, L: List<V>, F: FnMut(&mut V) -> napi::Result<Option<ValueOrVec<V, IS_VEC>>>, const IS_VEC: bool>(
+  list: &mut L,
+  mut f: F,
+) -> napi::Result<()> {
+  let mut i = 0;
+  while i < list.len() {
+    let value = &mut list[i];
+    let new_value = f(value)?;
+    match new_value {
+      Some(ValueOrVec::Value(v)) => {
+        list[i] = v;
+        i += 1;
+      }
+      Some(ValueOrVec::Vec(vec)) => {
+        if vec.is_empty() {
+          list.remove(i);
+        } else {
+          let len = vec.len();
+          list.replace(i, vec);
+          i += len;
+        }
+      }
+      None => {
+        i += 1;
+      }
+    }
+  }
+  Ok(())
+}
+
+#[derive(serde::Serialize)]
+#[serde(untagged)]
+enum ValueOrVec<V, const IS_VEC: bool = false> {
+  Value(V),
+  Vec(Vec<V>),
+}
+
+// Manually implemented deserialize for better error messages.
+// https://github.com/serde-rs/serde/issues/773
+impl<'de, V: serde::Deserialize<'de>, const IS_VEC: bool> serde::Deserialize<'de> for ValueOrVec<V, IS_VEC> {
+  fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+  where
+    D: serde::Deserializer<'de>,
+  {
+    use serde::Deserializer;
+    let content = serde::__private::de::Content::deserialize(deserializer)?;
+    let de: serde::__private::de::ContentRefDeserializer<D::Error> =
+      serde::__private::de::ContentRefDeserializer::new(&content);
+
+    // Try to deserialize as a sequence first.
+    let mut was_seq = false;
+    let res = de.deserialize_seq(SeqVisitor {
+      was_seq: &mut was_seq,
+      phantom: PhantomData,
+    });
+
+    if was_seq {
+      // Allow fallback if we know the value is also a list (e.g. selector).
+      if res.is_ok() || !IS_VEC {
+        return res.map(ValueOrVec::Vec);
+      }
+    }
+
+    // If it wasn't a sequence, try a value.
+    let de = serde::__private::de::ContentRefDeserializer::new(&content);
+    return V::deserialize(de).map(ValueOrVec::Value);
+
+    struct SeqVisitor<'a, V> {
+      was_seq: &'a mut bool,
+      phantom: PhantomData<V>,
+    }
+
+    impl<'a, 'de, V: serde::Deserialize<'de>> serde::de::Visitor<'de> for SeqVisitor<'a, V> {
+      type Value = Vec<V>;
+
+      fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
+        formatter.write_str("a sequence")
+      }
+
+      fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
+      where
+        A: serde::de::SeqAccess<'de>,
+      {
+        *self.was_seq = true;
+        let mut vec = Vec::with_capacity(seq.size_hint().unwrap_or(1));
+        while let Some(v) = seq.next_element()? {
+          vec.push(v);
+        }
+        Ok(vec)
+      }
+    }
+  }
+}
+
+struct TokensOrRaw<'i>(ValueOrVec<TokenOrValue<'i>>);
+
+impl<'i, 'de: 'i> serde::Deserialize<'de> for TokensOrRaw<'i> {
+  fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+  where
+    D: serde::Deserializer<'de>,
+  {
+    use serde::__private::de::ContentRefDeserializer;
+
+    #[derive(serde::Deserialize)]
+    struct Raw<'i> {
+      #[serde(borrow)]
+      raw: CowArcStr<'i>,
+    }
+
+    let content = serde::__private::de::Content::deserialize(deserializer)?;
+    let de: ContentRefDeserializer<D::Error> = ContentRefDeserializer::new(&content);
+
+    if let Ok(res) = Raw::deserialize(de) {
+      let res = TokenList::parse_string_with_options(res.raw.as_ref(), ParserOptions::default())
+        .map_err(|_| serde::de::Error::custom("Could not parse value"))?;
+      return Ok(TokensOrRaw(ValueOrVec::Vec(res.into_owned().0)));
+    }
+
+    let de = ContentRefDeserializer::new(&content);
+    Ok(TokensOrRaw(ValueOrVec::deserialize(de)?))
+  }
+}
+
+trait List<V>: Index<usize, Output = V> + IndexMut<usize, Output = V> {
+  fn len(&self) -> usize;
+  fn remove(&mut self, i: usize);
+  fn replace(&mut self, i: usize, items: Vec<V>);
+}
+
+impl<V> List<V> for Vec<V> {
+  fn len(&self) -> usize {
+    Vec::len(self)
+  }
+
+  fn remove(&mut self, i: usize) {
+    Vec::remove(self, i);
+  }
+
+  fn replace(&mut self, i: usize, items: Vec<V>) {
+    self.splice(i..i + 1, items);
+  }
+}
+
+impl<V, T: smallvec::Array<Item = V>> List<V> for SmallVec<T> {
+  fn len(&self) -> usize {
+    SmallVec::len(self)
+  }
+
+  fn remove(&mut self, i: usize) {
+    SmallVec::remove(self, i);
+  }
+
+  fn replace(&mut self, i: usize, items: Vec<V>) {
+    let len = items.len();
+    let mut iter = items.into_iter();
+    self[i] = iter.next().unwrap();
+    if len > 1 {
+      self.insert_many(i + 1, iter);
+    }
+  }
+}
+
+impl<'i, R> List<CssRule<'i, R>> for CssRuleList<'i, R> {
+  fn len(&self) -> usize {
+    self.0.len()
+  }
+
+  fn remove(&mut self, i: usize) {
+    self[i] = CssRule::Ignored;
+  }
+
+  fn replace(&mut self, i: usize, items: Vec<CssRule<'i, R>>) {
+    self.0.replace(i, items)
+  }
+}
diff --git a/napi/src/utils.rs b/napi/src/utils.rs
new file mode 100644
index 0000000..90ac149
--- /dev/null
+++ b/napi/src/utils.rs
@@ -0,0 +1,7 @@
+use napi::{Error, JsObject, JsUnknown, Result};
+
+// Workaround for https://github.com/napi-rs/napi-rs/issues/1641
+pub fn get_named_property<T: TryFrom<JsUnknown, Error = Error>>(obj: &JsObject, property: &str) -> Result<T> {
+  let unknown = obj.get_named_property::<JsUnknown>(property)?;
+  T::try_from(unknown)
+}
diff --git a/node/Cargo.toml b/node/Cargo.toml
new file mode 100644
index 0000000..02823cf
--- /dev/null
+++ b/node/Cargo.toml
@@ -0,0 +1,25 @@
+[package]
+authors = ["Devon Govett <devongovett@gmail.com>"]
+name = "lightningcss_node"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+crate-type = ["cdylib"]
+
+[dependencies]
+lightningcss-napi = { version = "0.4.4", path = "../napi", features = [
+  "bundler",
+  "visitor",
+] }
+napi = { version = "2.15.4", default-features = false, features = [
+  "compat-mode",
+] }
+napi-derive = "2"
+
+[target.'cfg(target_os = "macos")'.dependencies]
+jemallocator = { version = "0.3.2", features = ["disable_initial_exec_tls"] }
+
+[target.'cfg(not(target_arch = "wasm32"))'.build-dependencies]
+napi-build = "1"
diff --git a/node/ast.d.ts b/node/ast.d.ts
new file mode 100644
index 0000000..08d9d78
--- /dev/null
+++ b/node/ast.d.ts
@@ -0,0 +1,9739 @@
+/* eslint-disable */
+/**
+ * This file was automatically generated by json-schema-to-typescript.
+ * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,
+ * and run json-schema-to-typescript to regenerate this file.
+ */
+
+export type String = string;
+/**
+ * A CSS rule.
+ */
+export type Rule<D = Declaration, M = MediaQuery> = | {
+    type: "media";
+    value: MediaRule<D, M>;
+  }
+| {
+    type: "import";
+    value: ImportRule<M>;
+  }
+| {
+    type: "style";
+    value: StyleRule<D, M>;
+  }
+| {
+    type: "keyframes";
+    value: KeyframesRule<D>;
+  }
+| {
+    type: "font-face";
+    value: FontFaceRule;
+  }
+| {
+    type: "font-palette-values";
+    value: FontPaletteValuesRule;
+  }
+| {
+    type: "font-feature-values";
+    value: FontFeatureValuesRule;
+  }
+| {
+    type: "page";
+    value: PageRule<D>;
+  }
+| {
+    type: "supports";
+    value: SupportsRule<D, M>;
+  }
+| {
+    type: "counter-style";
+    value: CounterStyleRule<D>;
+  }
+| {
+    type: "namespace";
+    value: NamespaceRule;
+  }
+| {
+    type: "moz-document";
+    value: MozDocumentRule<D, M>;
+  }
+| {
+    type: "nesting";
+    value: NestingRule<D, M>;
+  }
+| {
+    type: "nested-declarations";
+    value: NestedDeclarationsRule<D>;
+  }
+| {
+    type: "viewport";
+    value: ViewportRule<D>;
+  }
+| {
+    type: "custom-media";
+    value: CustomMediaRule<M>;
+  }
+| {
+    type: "layer-statement";
+    value: LayerStatementRule;
+  }
+| {
+    type: "layer-block";
+    value: LayerBlockRule<D, M>;
+  }
+| {
+    type: "property";
+    value: PropertyRule;
+  }
+| {
+    type: "container";
+    value: ContainerRule<D, M>;
+  }
+| {
+    type: "scope";
+    value: ScopeRule<D, M>;
+  }
+| {
+    type: "starting-style";
+    value: StartingStyleRule<D, M>;
+  }
+| {
+    type: "view-transition";
+    value: ViewTransitionRule;
+  }
+| {
+    type: "ignored";
+  }
+| {
+    type: "unknown";
+    value: UnknownAtRule;
+  }
+| {
+    type: "custom";
+    value: DefaultAtRule;
+  };
+/**
+ * Represents a media condition.
+ */
+export type MediaCondition =
+  | {
+      type: "feature";
+      value: QueryFeatureFor_MediaFeatureId;
+    }
+  | {
+      type: "not";
+      value: MediaCondition;
+    }
+  | {
+      /**
+       * The conditions for the operator.
+       */
+      conditions: MediaCondition[];
+      /**
+       * The operator for the conditions.
+       */
+      operator: Operator;
+      type: "operation";
+    };
+/**
+ * A generic media feature or container feature.
+ */
+export type QueryFeatureFor_MediaFeatureId =
+  | {
+      /**
+       * The name of the feature.
+       */
+      name: MediaFeatureNameFor_MediaFeatureId;
+      type: "plain";
+      /**
+       * The feature value.
+       */
+      value: MediaFeatureValue;
+    }
+  | {
+      /**
+       * The name of the feature.
+       */
+      name: MediaFeatureNameFor_MediaFeatureId;
+      type: "boolean";
+    }
+  | {
+      /**
+       * The name of the feature.
+       */
+      name: MediaFeatureNameFor_MediaFeatureId;
+      /**
+       * A comparator.
+       */
+      operator: MediaFeatureComparison;
+      type: "range";
+      /**
+       * The feature value.
+       */
+      value: MediaFeatureValue;
+    }
+  | {
+      /**
+       * The end value.
+       */
+      end: MediaFeatureValue;
+      /**
+       * A comparator for the end value.
+       */
+      endOperator: MediaFeatureComparison;
+      /**
+       * The name of the feature.
+       */
+      name: MediaFeatureNameFor_MediaFeatureId;
+      /**
+       * A start value.
+       */
+      start: MediaFeatureValue;
+      /**
+       * A comparator for the start value.
+       */
+      startOperator: MediaFeatureComparison;
+      type: "interval";
+    };
+/**
+ * A media feature name.
+ */
+export type MediaFeatureNameFor_MediaFeatureId = MediaFeatureId | String | String;
+/**
+ * A media query feature identifier.
+ */
+export type MediaFeatureId =
+  | "width"
+  | "height"
+  | "aspect-ratio"
+  | "orientation"
+  | "overflow-block"
+  | "overflow-inline"
+  | "horizontal-viewport-segments"
+  | "vertical-viewport-segments"
+  | "display-mode"
+  | "resolution"
+  | "scan"
+  | "grid"
+  | "update"
+  | "environment-blending"
+  | "color"
+  | "color-index"
+  | "monochrome"
+  | "color-gamut"
+  | "dynamic-range"
+  | "inverted-colors"
+  | "pointer"
+  | "hover"
+  | "any-pointer"
+  | "any-hover"
+  | "nav-controls"
+  | "video-color-gamut"
+  | "video-dynamic-range"
+  | "scripting"
+  | "prefers-reduced-motion"
+  | "prefers-reduced-transparency"
+  | "prefers-contrast"
+  | "forced-colors"
+  | "prefers-color-scheme"
+  | "prefers-reduced-data"
+  | "device-width"
+  | "device-height"
+  | "device-aspect-ratio"
+  | "-webkit-device-pixel-ratio"
+  | "-moz-device-pixel-ratio";
+/**
+ * [media feature value](https://drafts.csswg.org/mediaqueries/#typedef-mf-value) within a media query.
+ *
+ * See [MediaFeature](MediaFeature).
+ */
+export type MediaFeatureValue =
+  | {
+      type: "length";
+      value: Length;
+    }
+  | {
+      type: "number";
+      value: number;
+    }
+  | {
+      type: "integer";
+      value: number;
+    }
+  | {
+      type: "boolean";
+      value: boolean;
+    }
+  | {
+      type: "resolution";
+      value: Resolution;
+    }
+  | {
+      type: "ratio";
+      value: Ratio;
+    }
+  | {
+      type: "ident";
+      value: String;
+    }
+  | {
+      type: "env";
+      value: EnvironmentVariable;
+    };
+/**
+ * A CSS [`<length>`](https://www.w3.org/TR/css-values-4/#lengths) value, with support for `calc()`.
+ */
+export type Length =
+  | {
+      type: "value";
+      value: LengthValue;
+    }
+  | {
+      type: "calc";
+      value: CalcFor_Length;
+    };
+export type LengthUnit =
+  | "px"
+  | "in"
+  | "cm"
+  | "mm"
+  | "q"
+  | "pt"
+  | "pc"
+  | "em"
+  | "rem"
+  | "ex"
+  | "rex"
+  | "ch"
+  | "rch"
+  | "cap"
+  | "rcap"
+  | "ic"
+  | "ric"
+  | "lh"
+  | "rlh"
+  | "vw"
+  | "lvw"
+  | "svw"
+  | "dvw"
+  | "cqw"
+  | "vh"
+  | "lvh"
+  | "svh"
+  | "dvh"
+  | "cqh"
+  | "vi"
+  | "svi"
+  | "lvi"
+  | "dvi"
+  | "cqi"
+  | "vb"
+  | "svb"
+  | "lvb"
+  | "dvb"
+  | "cqb"
+  | "vmin"
+  | "svmin"
+  | "lvmin"
+  | "dvmin"
+  | "cqmin"
+  | "vmax"
+  | "svmax"
+  | "lvmax"
+  | "dvmax"
+  | "cqmax";
+/**
+ * A mathematical expression used within the [`calc()`](https://www.w3.org/TR/css-values-4/#calc-func) function.
+ *
+ * This type supports generic value types. Values such as [Length](super::length::Length), [Percentage](super::percentage::Percentage), [Time](super::time::Time), and [Angle](super::angle::Angle) support `calc()` expressions.
+ */
+export type CalcFor_Length =
+  | {
+      type: "value";
+      value: Length;
+    }
+  | {
+      type: "number";
+      value: number;
+    }
+  | {
+      type: "sum";
+      /**
+       * @minItems 2
+       * @maxItems 2
+       */
+      value: [CalcFor_Length, CalcFor_Length];
+    }
+  | {
+      type: "product";
+      /**
+       * @minItems 2
+       * @maxItems 2
+       */
+      value: [number, CalcFor_Length];
+    }
+  | {
+      type: "function";
+      value: MathFunctionFor_Length;
+    };
+/**
+ * A CSS [math function](https://www.w3.org/TR/css-values-4/#math-function).
+ *
+ * Math functions may be used in most properties and values that accept numeric values, including lengths, percentages, angles, times, etc.
+ */
+export type MathFunctionFor_Length =
+  | {
+      type: "calc";
+      value: CalcFor_Length;
+    }
+  | {
+      type: "min";
+      value: CalcFor_Length[];
+    }
+  | {
+      type: "max";
+      value: CalcFor_Length[];
+    }
+  | {
+      type: "clamp";
+      /**
+       * @minItems 3
+       * @maxItems 3
+       */
+      value: [CalcFor_Length, CalcFor_Length, CalcFor_Length];
+    }
+  | {
+      type: "round";
+      /**
+       * @minItems 3
+       * @maxItems 3
+       */
+      value: [RoundingStrategy, CalcFor_Length, CalcFor_Length];
+    }
+  | {
+      type: "rem";
+      /**
+       * @minItems 2
+       * @maxItems 2
+       */
+      value: [CalcFor_Length, CalcFor_Length];
+    }
+  | {
+      type: "mod";
+      /**
+       * @minItems 2
+       * @maxItems 2
+       */
+      value: [CalcFor_Length, CalcFor_Length];
+    }
+  | {
+      type: "abs";
+      value: CalcFor_Length;
+    }
+  | {
+      type: "sign";
+      value: CalcFor_Length;
+    }
+  | {
+      type: "hypot";
+      value: CalcFor_Length[];
+    };
+/**
+ * A [rounding strategy](https://www.w3.org/TR/css-values-4/#typedef-rounding-strategy), as used in the `round()` function.
+ */
+export type RoundingStrategy = "nearest" | "up" | "down" | "to-zero";
+/**
+ * A CSS [`<resolution>`](https://www.w3.org/TR/css-values-4/#resolution) value.
+ */
+export type Resolution =
+  | {
+      type: "dpi";
+      value: number;
+    }
+  | {
+      type: "dpcm";
+      value: number;
+    }
+  | {
+      type: "dppx";
+      value: number;
+    };
+/**
+ * A CSS [`<ratio>`](https://www.w3.org/TR/css-values-4/#ratios) value, representing the ratio of two numeric values.
+ *
+ * @minItems 2
+ * @maxItems 2
+ */
+export type Ratio = [number, number];
+/**
+ * A raw CSS token, or a parsed value.
+ */
+export type TokenOrValue =
+  | {
+      type: "token";
+      value: Token;
+    }
+  | {
+      type: "color";
+      value: CssColor;
+    }
+  | {
+      type: "unresolved-color";
+      value: UnresolvedColor;
+    }
+  | {
+      type: "url";
+      value: Url;
+    }
+  | {
+      type: "var";
+      value: Variable;
+    }
+  | {
+      type: "env";
+      value: EnvironmentVariable;
+    }
+  | {
+      type: "function";
+      value: Function;
+    }
+  | {
+      type: "length";
+      value: LengthValue;
+    }
+  | {
+      type: "angle";
+      value: Angle;
+    }
+  | {
+      type: "time";
+      value: Time;
+    }
+  | {
+      type: "resolution";
+      value: Resolution;
+    }
+  | {
+      type: "dashed-ident";
+      value: String;
+    }
+  | {
+      type: "animation-name";
+      value: AnimationName;
+    };
+/**
+ * A raw CSS token.
+ */
+export type Token =
+  | {
+      type: "ident";
+      value: String;
+    }
+  | {
+      type: "at-keyword";
+      value: String;
+    }
+  | {
+      type: "hash";
+      value: String;
+    }
+  | {
+      type: "id-hash";
+      value: String;
+    }
+  | {
+      type: "string";
+      value: String;
+    }
+  | {
+      type: "unquoted-url";
+      value: String;
+    }
+  | {
+      type: "delim";
+      value: string;
+    }
+  | {
+      type: "number";
+      /**
+       * The value as a float
+       */
+      value: number;
+    }
+  | {
+      type: "percentage";
+      /**
+       * The value as a float, divided by 100 so that the nominal range is 0.0 to 1.0.
+       */
+      value: number;
+    }
+  | {
+      type: "dimension";
+      /**
+       * The unit, e.g. "px" in `12px`
+       */
+      unit: String;
+      /**
+       * The value as a float
+       */
+      value: number;
+    }
+  | {
+      type: "white-space";
+      value: String;
+    }
+  | {
+      type: "comment";
+      value: String;
+    }
+  | {
+      type: "colon";
+    }
+  | {
+      type: "semicolon";
+    }
+  | {
+      type: "comma";
+    }
+  | {
+      type: "include-match";
+    }
+  | {
+      type: "dash-match";
+    }
+  | {
+      type: "prefix-match";
+    }
+  | {
+      type: "suffix-match";
+    }
+  | {
+      type: "substring-match";
+    }
+  | {
+      type: "cdo";
+    }
+  | {
+      type: "cdc";
+    }
+  | {
+      type: "function";
+      value: String;
+    }
+  | {
+      type: "parenthesis-block";
+    }
+  | {
+      type: "square-bracket-block";
+    }
+  | {
+      type: "curly-bracket-block";
+    }
+  | {
+      type: "bad-url";
+      value: String;
+    }
+  | {
+      type: "bad-string";
+      value: String;
+    }
+  | {
+      type: "close-parenthesis";
+    }
+  | {
+      type: "close-square-bracket";
+    }
+  | {
+      type: "close-curly-bracket";
+    };
+/**
+ * A CSS [`<color>`](https://www.w3.org/TR/css-color-4/#color-type) value.
+ *
+ * CSS supports many different color spaces to represent colors. The most common values are stored as RGBA using a single byte per component. Less common values are stored using a `Box` to reduce the amount of memory used per color.
+ *
+ * Each color space is represented as a struct that implements the `From` and `Into` traits for all other color spaces, so it is possible to convert between color spaces easily. In addition, colors support [interpolation](#method.interpolate) as in the `color-mix()` function.
+ */
+export type CssColor = CurrentColor | RGBColor | LABColor | PredefinedColor | FloatColor | LightDark | SystemColor;
+export type CurrentColor = {
+  type: "currentcolor";
+};
+export type RGBColor = {
+  /**
+   * The alpha component.
+   */
+  alpha: number;
+  /**
+   * The blue component.
+   */
+  b: number;
+  /**
+   * The green component.
+   */
+  g: number;
+  /**
+   * The red component.
+   */
+  r: number;
+  type: "rgb";
+};
+/**
+ * A color in a LAB color space, including the `lab()`, `lch()`, `oklab()`, and `oklch()` functions.
+ */
+export type LABColor =
+  | {
+      /**
+       * The a component.
+       */
+      a: number;
+      /**
+       * The alpha component.
+       */
+      alpha: number;
+      /**
+       * The b component.
+       */
+      b: number;
+      /**
+       * The lightness component.
+       */
+      l: number;
+      type: "lab";
+    }
+  | {
+      /**
+       * The alpha component.
+       */
+      alpha: number;
+      /**
+       * The chroma component.
+       */
+      c: number;
+      /**
+       * The hue component.
+       */
+      h: number;
+      /**
+       * The lightness component.
+       */
+      l: number;
+      type: "lch";
+    }
+  | {
+      /**
+       * The a component.
+       */
+      a: number;
+      /**
+       * The alpha component.
+       */
+      alpha: number;
+      /**
+       * The b component.
+       */
+      b: number;
+      /**
+       * The lightness component.
+       */
+      l: number;
+      type: "oklab";
+    }
+  | {
+      /**
+       * The alpha component.
+       */
+      alpha: number;
+      /**
+       * The chroma component.
+       */
+      c: number;
+      /**
+       * The hue component.
+       */
+      h: number;
+      /**
+       * The lightness component.
+       */
+      l: number;
+      type: "oklch";
+    };
+/**
+ * A color in a predefined color space, e.g. `display-p3`.
+ */
+export type PredefinedColor =
+  | {
+      /**
+       * The alpha component.
+       */
+      alpha: number;
+      /**
+       * The blue component.
+       */
+      b: number;
+      /**
+       * The green component.
+       */
+      g: number;
+      /**
+       * The red component.
+       */
+      r: number;
+      type: "srgb";
+    }
+  | {
+      /**
+       * The alpha component.
+       */
+      alpha: number;
+      /**
+       * The blue component.
+       */
+      b: number;
+      /**
+       * The green component.
+       */
+      g: number;
+      /**
+       * The red component.
+       */
+      r: number;
+      type: "srgb-linear";
+    }
+  | {
+      /**
+       * The alpha component.
+       */
+      alpha: number;
+      /**
+       * The blue component.
+       */
+      b: number;
+      /**
+       * The green component.
+       */
+      g: number;
+      /**
+       * The red component.
+       */
+      r: number;
+      type: "display-p3";
+    }
+  | {
+      /**
+       * The alpha component.
+       */
+      alpha: number;
+      /**
+       * The blue component.
+       */
+      b: number;
+      /**
+       * The green component.
+       */
+      g: number;
+      /**
+       * The red component.
+       */
+      r: number;
+      type: "a98-rgb";
+    }
+  | {
+      /**
+       * The alpha component.
+       */
+      alpha: number;
+      /**
+       * The blue component.
+       */
+      b: number;
+      /**
+       * The green component.
+       */
+      g: number;
+      /**
+       * The red component.
+       */
+      r: number;
+      type: "prophoto-rgb";
+    }
+  | {
+      /**
+       * The alpha component.
+       */
+      alpha: number;
+      /**
+       * The blue component.
+       */
+      b: number;
+      /**
+       * The green component.
+       */
+      g: number;
+      /**
+       * The red component.
+       */
+      r: number;
+      type: "rec2020";
+    }
+  | {
+      /**
+       * The alpha component.
+       */
+      alpha: number;
+      type: "xyz-d50";
+      /**
+       * The x component.
+       */
+      x: number;
+      /**
+       * The y component.
+       */
+      y: number;
+      /**
+       * The z component.
+       */
+      z: number;
+    }
+  | {
+      /**
+       * The alpha component.
+       */
+      alpha: number;
+      type: "xyz-d65";
+      /**
+       * The x component.
+       */
+      x: number;
+      /**
+       * The y component.
+       */
+      y: number;
+      /**
+       * The z component.
+       */
+      z: number;
+    };
+/**
+ * A floating point representation of color types that are usually stored as RGBA. These are used when there are any `none` components, which are represented as NaN.
+ */
+export type FloatColor =
+  | {
+      /**
+       * The alpha component.
+       */
+      alpha: number;
+      /**
+       * The blue component.
+       */
+      b: number;
+      /**
+       * The green component.
+       */
+      g: number;
+      /**
+       * The red component.
+       */
+      r: number;
+      type: "rgb";
+    }
+  | {
+      /**
+       * The alpha component.
+       */
+      alpha: number;
+      /**
+       * The hue component.
+       */
+      h: number;
+      /**
+       * The lightness component.
+       */
+      l: number;
+      /**
+       * The saturation component.
+       */
+      s: number;
+      type: "hsl";
+    }
+  | {
+      /**
+       * The alpha component.
+       */
+      alpha: number;
+      /**
+       * The blackness component.
+       */
+      b: number;
+      /**
+       * The hue component.
+       */
+      h: number;
+      type: "hwb";
+      /**
+       * The whiteness component.
+       */
+      w: number;
+    };
+export type LightDark = {
+  dark: CssColor;
+  light: CssColor;
+  type: "light-dark";
+};
+/**
+ * A CSS [system color](https://drafts.csswg.org/css-color/#css-system-colors) keyword.
+ */
+export type SystemColor =
+  | "accentcolor"
+  | "accentcolortext"
+  | "activetext"
+  | "buttonborder"
+  | "buttonface"
+  | "buttontext"
+  | "canvas"
+  | "canvastext"
+  | "field"
+  | "fieldtext"
+  | "graytext"
+  | "highlight"
+  | "highlighttext"
+  | "linktext"
+  | "mark"
+  | "marktext"
+  | "selecteditem"
+  | "selecteditemtext"
+  | "visitedtext"
+  | "activeborder"
+  | "activecaption"
+  | "appworkspace"
+  | "background"
+  | "buttonhighlight"
+  | "buttonshadow"
+  | "captiontext"
+  | "inactiveborder"
+  | "inactivecaption"
+  | "inactivecaptiontext"
+  | "infobackground"
+  | "infotext"
+  | "menu"
+  | "menutext"
+  | "scrollbar"
+  | "threeddarkshadow"
+  | "threedface"
+  | "threedhighlight"
+  | "threedlightshadow"
+  | "threedshadow"
+  | "window"
+  | "windowframe"
+  | "windowtext";
+/**
+ * A color value with an unresolved alpha value (e.g. a variable). These can be converted from the modern slash syntax to older comma syntax. This can only be done when the only unresolved component is the alpha since variables can resolve to multiple tokens.
+ */
+export type UnresolvedColor =
+  | {
+      /**
+       * The unresolved alpha component.
+       */
+      alpha: TokenOrValue[];
+      /**
+       * The blue component.
+       */
+      b: number;
+      /**
+       * The green component.
+       */
+      g: number;
+      /**
+       * The red component.
+       */
+      r: number;
+      type: "rgb";
+    }
+  | {
+      /**
+       * The unresolved alpha component.
+       */
+      alpha: TokenOrValue[];
+      /**
+       * The hue component.
+       */
+      h: number;
+      /**
+       * The lightness component.
+       */
+      l: number;
+      /**
+       * The saturation component.
+       */
+      s: number;
+      type: "hsl";
+    }
+  | {
+      /**
+       * The dark value.
+       */
+      dark: TokenOrValue[];
+      /**
+       * The light value.
+       */
+      light: TokenOrValue[];
+      type: "light-dark";
+    };
+/**
+ * Defines where the class names referenced in the `composes` property are located.
+ *
+ * See [Composes](Composes).
+ */
+export type Specifier =
+  | {
+      type: "global";
+    }
+  | {
+      type: "file";
+      value: String;
+    }
+  | {
+      type: "source-index";
+      value: number;
+    };
+/**
+ * A CSS [`<angle>`](https://www.w3.org/TR/css-values-4/#angles) value.
+ *
+ * Angles may be explicit or computed by `calc()`, but are always stored and serialized as their computed value.
+ */
+export type Angle =
+  | {
+      type: "deg";
+      value: number;
+    }
+  | {
+      type: "rad";
+      value: number;
+    }
+  | {
+      type: "grad";
+      value: number;
+    }
+  | {
+      type: "turn";
+      value: number;
+    };
+/**
+ * A CSS [`<time>`](https://www.w3.org/TR/css-values-4/#time) value, in either seconds or milliseconds.
+ *
+ * Time values may be explicit or computed by `calc()`, but are always stored and serialized as their computed value.
+ */
+export type Time =
+  | {
+      type: "seconds";
+      value: number;
+    }
+  | {
+      type: "milliseconds";
+      value: number;
+    };
+/**
+ * A value for the [animation-name](https://drafts.csswg.org/css-animations/#animation-name) property.
+ */
+export type AnimationName =
+  | {
+      type: "none";
+    }
+  | {
+      type: "ident";
+      value: String;
+    }
+  | {
+      type: "string";
+      value: String;
+    };
+/**
+ * A CSS environment variable name.
+ */
+export type EnvironmentVariableName =
+  | {
+      type: "ua";
+      value: UAEnvironmentVariable;
+    }
+  | {
+      /**
+       * CSS modules extension: the filename where the variable is defined. Only enabled when the CSS modules `dashed_idents` option is turned on.
+       */
+      from?: Specifier | null;
+      /**
+       * The referenced identifier.
+       */
+      ident: String;
+      type: "custom";
+    }
+  | {
+      type: "unknown";
+      value: String;
+    };
+/**
+ * A UA-defined environment variable name.
+ */
+export type UAEnvironmentVariable =
+  | "safe-area-inset-top"
+  | "safe-area-inset-right"
+  | "safe-area-inset-bottom"
+  | "safe-area-inset-left"
+  | "viewport-segment-width"
+  | "viewport-segment-height"
+  | "viewport-segment-top"
+  | "viewport-segment-left"
+  | "viewport-segment-bottom"
+  | "viewport-segment-right";
+/**
+ * A [comparator](https://drafts.csswg.org/mediaqueries/#typedef-mf-comparison) within a media query.
+ */
+export type MediaFeatureComparison = "equal" | "greater-than" | "greater-than-equal" | "less-than" | "less-than-equal";
+/**
+ * A binary `and` or `or` operator.
+ */
+export type Operator = "and" | "or";
+export type MediaType = string;
+/**
+ * A [media query qualifier](https://drafts.csswg.org/mediaqueries/#mq-prefix).
+ */
+export type Qualifier = "only" | "not";
+/**
+ * A [`<supports-condition>`](https://drafts.csswg.org/css-conditional-3/#typedef-supports-condition), as used in the `@supports` and `@import` rules.
+ */
+export type SupportsCondition =
+  | {
+      type: "not";
+      value: SupportsCondition;
+    }
+  | {
+      type: "and";
+      value: SupportsCondition[];
+    }
+  | {
+      type: "or";
+      value: SupportsCondition[];
+    }
+  | {
+      /**
+       * The property id for the declaration.
+       */
+      propertyId: PropertyId;
+      type: "declaration";
+      /**
+       * The raw value of the declaration.
+       */
+      value: String;
+    }
+  | {
+      type: "selector";
+      value: String;
+    }
+  | {
+      type: "unknown";
+      value: String;
+    };
+export type PropertyId =
+  | {
+      property: "background-color";
+    }
+  | {
+      property: "background-image";
+    }
+  | {
+      property: "background-position-x";
+    }
+  | {
+      property: "background-position-y";
+    }
+  | {
+      property: "background-position";
+    }
+  | {
+      property: "background-size";
+    }
+  | {
+      property: "background-repeat";
+    }
+  | {
+      property: "background-attachment";
+    }
+  | {
+      property: "background-clip";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "background-origin";
+    }
+  | {
+      property: "background";
+    }
+  | {
+      property: "box-shadow";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "opacity";
+    }
+  | {
+      property: "color";
+    }
+  | {
+      property: "display";
+    }
+  | {
+      property: "visibility";
+    }
+  | {
+      property: "width";
+    }
+  | {
+      property: "height";
+    }
+  | {
+      property: "min-width";
+    }
+  | {
+      property: "min-height";
+    }
+  | {
+      property: "max-width";
+    }
+  | {
+      property: "max-height";
+    }
+  | {
+      property: "block-size";
+    }
+  | {
+      property: "inline-size";
+    }
+  | {
+      property: "min-block-size";
+    }
+  | {
+      property: "min-inline-size";
+    }
+  | {
+      property: "max-block-size";
+    }
+  | {
+      property: "max-inline-size";
+    }
+  | {
+      property: "box-sizing";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "aspect-ratio";
+    }
+  | {
+      property: "overflow";
+    }
+  | {
+      property: "overflow-x";
+    }
+  | {
+      property: "overflow-y";
+    }
+  | {
+      property: "text-overflow";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "position";
+    }
+  | {
+      property: "top";
+    }
+  | {
+      property: "bottom";
+    }
+  | {
+      property: "left";
+    }
+  | {
+      property: "right";
+    }
+  | {
+      property: "inset-block-start";
+    }
+  | {
+      property: "inset-block-end";
+    }
+  | {
+      property: "inset-inline-start";
+    }
+  | {
+      property: "inset-inline-end";
+    }
+  | {
+      property: "inset-block";
+    }
+  | {
+      property: "inset-inline";
+    }
+  | {
+      property: "inset";
+    }
+  | {
+      property: "border-spacing";
+    }
+  | {
+      property: "border-top-color";
+    }
+  | {
+      property: "border-bottom-color";
+    }
+  | {
+      property: "border-left-color";
+    }
+  | {
+      property: "border-right-color";
+    }
+  | {
+      property: "border-block-start-color";
+    }
+  | {
+      property: "border-block-end-color";
+    }
+  | {
+      property: "border-inline-start-color";
+    }
+  | {
+      property: "border-inline-end-color";
+    }
+  | {
+      property: "border-top-style";
+    }
+  | {
+      property: "border-bottom-style";
+    }
+  | {
+      property: "border-left-style";
+    }
+  | {
+      property: "border-right-style";
+    }
+  | {
+      property: "border-block-start-style";
+    }
+  | {
+      property: "border-block-end-style";
+    }
+  | {
+      property: "border-inline-start-style";
+    }
+  | {
+      property: "border-inline-end-style";
+    }
+  | {
+      property: "border-top-width";
+    }
+  | {
+      property: "border-bottom-width";
+    }
+  | {
+      property: "border-left-width";
+    }
+  | {
+      property: "border-right-width";
+    }
+  | {
+      property: "border-block-start-width";
+    }
+  | {
+      property: "border-block-end-width";
+    }
+  | {
+      property: "border-inline-start-width";
+    }
+  | {
+      property: "border-inline-end-width";
+    }
+  | {
+      property: "border-top-left-radius";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "border-top-right-radius";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "border-bottom-left-radius";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "border-bottom-right-radius";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "border-start-start-radius";
+    }
+  | {
+      property: "border-start-end-radius";
+    }
+  | {
+      property: "border-end-start-radius";
+    }
+  | {
+      property: "border-end-end-radius";
+    }
+  | {
+      property: "border-radius";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "border-image-source";
+    }
+  | {
+      property: "border-image-outset";
+    }
+  | {
+      property: "border-image-repeat";
+    }
+  | {
+      property: "border-image-width";
+    }
+  | {
+      property: "border-image-slice";
+    }
+  | {
+      property: "border-image";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "border-color";
+    }
+  | {
+      property: "border-style";
+    }
+  | {
+      property: "border-width";
+    }
+  | {
+      property: "border-block-color";
+    }
+  | {
+      property: "border-block-style";
+    }
+  | {
+      property: "border-block-width";
+    }
+  | {
+      property: "border-inline-color";
+    }
+  | {
+      property: "border-inline-style";
+    }
+  | {
+      property: "border-inline-width";
+    }
+  | {
+      property: "border";
+    }
+  | {
+      property: "border-top";
+    }
+  | {
+      property: "border-bottom";
+    }
+  | {
+      property: "border-left";
+    }
+  | {
+      property: "border-right";
+    }
+  | {
+      property: "border-block";
+    }
+  | {
+      property: "border-block-start";
+    }
+  | {
+      property: "border-block-end";
+    }
+  | {
+      property: "border-inline";
+    }
+  | {
+      property: "border-inline-start";
+    }
+  | {
+      property: "border-inline-end";
+    }
+  | {
+      property: "outline";
+    }
+  | {
+      property: "outline-color";
+    }
+  | {
+      property: "outline-style";
+    }
+  | {
+      property: "outline-width";
+    }
+  | {
+      property: "flex-direction";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "flex-wrap";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "flex-flow";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "flex-grow";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "flex-shrink";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "flex-basis";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "flex";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "order";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "align-content";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "justify-content";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "place-content";
+    }
+  | {
+      property: "align-self";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "justify-self";
+    }
+  | {
+      property: "place-self";
+    }
+  | {
+      property: "align-items";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "justify-items";
+    }
+  | {
+      property: "place-items";
+    }
+  | {
+      property: "row-gap";
+    }
+  | {
+      property: "column-gap";
+    }
+  | {
+      property: "gap";
+    }
+  | {
+      property: "box-orient";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "box-direction";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "box-ordinal-group";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "box-align";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "box-flex";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "box-flex-group";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "box-pack";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "box-lines";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "flex-pack";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "flex-order";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "flex-align";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "flex-item-align";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "flex-line-pack";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "flex-positive";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "flex-negative";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "flex-preferred-size";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "grid-template-columns";
+    }
+  | {
+      property: "grid-template-rows";
+    }
+  | {
+      property: "grid-auto-columns";
+    }
+  | {
+      property: "grid-auto-rows";
+    }
+  | {
+      property: "grid-auto-flow";
+    }
+  | {
+      property: "grid-template-areas";
+    }
+  | {
+      property: "grid-template";
+    }
+  | {
+      property: "grid";
+    }
+  | {
+      property: "grid-row-start";
+    }
+  | {
+      property: "grid-row-end";
+    }
+  | {
+      property: "grid-column-start";
+    }
+  | {
+      property: "grid-column-end";
+    }
+  | {
+      property: "grid-row";
+    }
+  | {
+      property: "grid-column";
+    }
+  | {
+      property: "grid-area";
+    }
+  | {
+      property: "margin-top";
+    }
+  | {
+      property: "margin-bottom";
+    }
+  | {
+      property: "margin-left";
+    }
+  | {
+      property: "margin-right";
+    }
+  | {
+      property: "margin-block-start";
+    }
+  | {
+      property: "margin-block-end";
+    }
+  | {
+      property: "margin-inline-start";
+    }
+  | {
+      property: "margin-inline-end";
+    }
+  | {
+      property: "margin-block";
+    }
+  | {
+      property: "margin-inline";
+    }
+  | {
+      property: "margin";
+    }
+  | {
+      property: "padding-top";
+    }
+  | {
+      property: "padding-bottom";
+    }
+  | {
+      property: "padding-left";
+    }
+  | {
+      property: "padding-right";
+    }
+  | {
+      property: "padding-block-start";
+    }
+  | {
+      property: "padding-block-end";
+    }
+  | {
+      property: "padding-inline-start";
+    }
+  | {
+      property: "padding-inline-end";
+    }
+  | {
+      property: "padding-block";
+    }
+  | {
+      property: "padding-inline";
+    }
+  | {
+      property: "padding";
+    }
+  | {
+      property: "scroll-margin-top";
+    }
+  | {
+      property: "scroll-margin-bottom";
+    }
+  | {
+      property: "scroll-margin-left";
+    }
+  | {
+      property: "scroll-margin-right";
+    }
+  | {
+      property: "scroll-margin-block-start";
+    }
+  | {
+      property: "scroll-margin-block-end";
+    }
+  | {
+      property: "scroll-margin-inline-start";
+    }
+  | {
+      property: "scroll-margin-inline-end";
+    }
+  | {
+      property: "scroll-margin-block";
+    }
+  | {
+      property: "scroll-margin-inline";
+    }
+  | {
+      property: "scroll-margin";
+    }
+  | {
+      property: "scroll-padding-top";
+    }
+  | {
+      property: "scroll-padding-bottom";
+    }
+  | {
+      property: "scroll-padding-left";
+    }
+  | {
+      property: "scroll-padding-right";
+    }
+  | {
+      property: "scroll-padding-block-start";
+    }
+  | {
+      property: "scroll-padding-block-end";
+    }
+  | {
+      property: "scroll-padding-inline-start";
+    }
+  | {
+      property: "scroll-padding-inline-end";
+    }
+  | {
+      property: "scroll-padding-block";
+    }
+  | {
+      property: "scroll-padding-inline";
+    }
+  | {
+      property: "scroll-padding";
+    }
+  | {
+      property: "font-weight";
+    }
+  | {
+      property: "font-size";
+    }
+  | {
+      property: "font-stretch";
+    }
+  | {
+      property: "font-family";
+    }
+  | {
+      property: "font-style";
+    }
+  | {
+      property: "font-variant-caps";
+    }
+  | {
+      property: "line-height";
+    }
+  | {
+      property: "font";
+    }
+  | {
+      property: "vertical-align";
+    }
+  | {
+      property: "font-palette";
+    }
+  | {
+      property: "transition-property";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "transition-duration";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "transition-delay";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "transition-timing-function";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "transition";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "animation-name";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "animation-duration";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "animation-timing-function";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "animation-iteration-count";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "animation-direction";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "animation-play-state";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "animation-delay";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "animation-fill-mode";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "animation-composition";
+    }
+  | {
+      property: "animation-timeline";
+    }
+  | {
+      property: "animation-range-start";
+    }
+  | {
+      property: "animation-range-end";
+    }
+  | {
+      property: "animation-range";
+    }
+  | {
+      property: "animation";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "transform";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "transform-origin";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "transform-style";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "transform-box";
+    }
+  | {
+      property: "backface-visibility";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "perspective";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "perspective-origin";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "translate";
+    }
+  | {
+      property: "rotate";
+    }
+  | {
+      property: "scale";
+    }
+  | {
+      property: "text-transform";
+    }
+  | {
+      property: "white-space";
+    }
+  | {
+      property: "tab-size";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "word-break";
+    }
+  | {
+      property: "line-break";
+    }
+  | {
+      property: "hyphens";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "overflow-wrap";
+    }
+  | {
+      property: "word-wrap";
+    }
+  | {
+      property: "text-align";
+    }
+  | {
+      property: "text-align-last";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "text-justify";
+    }
+  | {
+      property: "word-spacing";
+    }
+  | {
+      property: "letter-spacing";
+    }
+  | {
+      property: "text-indent";
+    }
+  | {
+      property: "text-decoration-line";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "text-decoration-style";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "text-decoration-color";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "text-decoration-thickness";
+    }
+  | {
+      property: "text-decoration";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "text-decoration-skip-ink";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "text-emphasis-style";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "text-emphasis-color";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "text-emphasis";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "text-emphasis-position";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "text-shadow";
+    }
+  | {
+      property: "text-size-adjust";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "direction";
+    }
+  | {
+      property: "unicode-bidi";
+    }
+  | {
+      property: "box-decoration-break";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "resize";
+    }
+  | {
+      property: "cursor";
+    }
+  | {
+      property: "caret-color";
+    }
+  | {
+      property: "caret-shape";
+    }
+  | {
+      property: "caret";
+    }
+  | {
+      property: "user-select";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "accent-color";
+    }
+  | {
+      property: "appearance";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "list-style-type";
+    }
+  | {
+      property: "list-style-image";
+    }
+  | {
+      property: "list-style-position";
+    }
+  | {
+      property: "list-style";
+    }
+  | {
+      property: "marker-side";
+    }
+  | {
+      property: "composes";
+    }
+  | {
+      property: "fill";
+    }
+  | {
+      property: "fill-rule";
+    }
+  | {
+      property: "fill-opacity";
+    }
+  | {
+      property: "stroke";
+    }
+  | {
+      property: "stroke-opacity";
+    }
+  | {
+      property: "stroke-width";
+    }
+  | {
+      property: "stroke-linecap";
+    }
+  | {
+      property: "stroke-linejoin";
+    }
+  | {
+      property: "stroke-miterlimit";
+    }
+  | {
+      property: "stroke-dasharray";
+    }
+  | {
+      property: "stroke-dashoffset";
+    }
+  | {
+      property: "marker-start";
+    }
+  | {
+      property: "marker-mid";
+    }
+  | {
+      property: "marker-end";
+    }
+  | {
+      property: "marker";
+    }
+  | {
+      property: "color-interpolation";
+    }
+  | {
+      property: "color-interpolation-filters";
+    }
+  | {
+      property: "color-rendering";
+    }
+  | {
+      property: "shape-rendering";
+    }
+  | {
+      property: "text-rendering";
+    }
+  | {
+      property: "image-rendering";
+    }
+  | {
+      property: "clip-path";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "clip-rule";
+    }
+  | {
+      property: "mask-image";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "mask-mode";
+    }
+  | {
+      property: "mask-repeat";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "mask-position-x";
+    }
+  | {
+      property: "mask-position-y";
+    }
+  | {
+      property: "mask-position";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "mask-clip";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "mask-origin";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "mask-size";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "mask-composite";
+    }
+  | {
+      property: "mask-type";
+    }
+  | {
+      property: "mask";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "mask-border-source";
+    }
+  | {
+      property: "mask-border-mode";
+    }
+  | {
+      property: "mask-border-slice";
+    }
+  | {
+      property: "mask-border-width";
+    }
+  | {
+      property: "mask-border-outset";
+    }
+  | {
+      property: "mask-border-repeat";
+    }
+  | {
+      property: "mask-border";
+    }
+  | {
+      property: "-webkit-mask-composite";
+    }
+  | {
+      property: "mask-source-type";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "mask-box-image";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "mask-box-image-source";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "mask-box-image-slice";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "mask-box-image-width";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "mask-box-image-outset";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "mask-box-image-repeat";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "filter";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "backdrop-filter";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "z-index";
+    }
+  | {
+      property: "container-type";
+    }
+  | {
+      property: "container-name";
+    }
+  | {
+      property: "container";
+    }
+  | {
+      property: "view-transition-name";
+    }
+  | {
+      property: "view-transition-class";
+    }
+  | {
+      property: "view-transition-group";
+    }
+  | {
+      property: "color-scheme";
+    }
+  | {
+      property: "all";
+    }
+  | {
+      property: string;
+    };
+export type Prefix = "none" | "webkit" | "moz" | "ms" | "o";
+export type VendorPrefix = Prefix[];
+export type Declaration =
+  | {
+      property: "background-color";
+      value: CssColor;
+    }
+  | {
+      property: "background-image";
+      value: Image[];
+    }
+  | {
+      property: "background-position-x";
+      value: PositionComponentFor_HorizontalPositionKeyword[];
+    }
+  | {
+      property: "background-position-y";
+      value: PositionComponentFor_VerticalPositionKeyword[];
+    }
+  | {
+      property: "background-position";
+      value: BackgroundPosition[];
+    }
+  | {
+      property: "background-size";
+      value: BackgroundSize[];
+    }
+  | {
+      property: "background-repeat";
+      value: BackgroundRepeat[];
+    }
+  | {
+      property: "background-attachment";
+      value: BackgroundAttachment[];
+    }
+  | {
+      property: "background-clip";
+      value: BackgroundClip[];
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "background-origin";
+      value: BackgroundOrigin[];
+    }
+  | {
+      property: "background";
+      value: Background[];
+    }
+  | {
+      property: "box-shadow";
+      value: BoxShadow[];
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "opacity";
+      value: number;
+    }
+  | {
+      property: "color";
+      value: CssColor;
+    }
+  | {
+      property: "display";
+      value: Display;
+    }
+  | {
+      property: "visibility";
+      value: Visibility;
+    }
+  | {
+      property: "width";
+      value: Size;
+    }
+  | {
+      property: "height";
+      value: Size;
+    }
+  | {
+      property: "min-width";
+      value: Size;
+    }
+  | {
+      property: "min-height";
+      value: Size;
+    }
+  | {
+      property: "max-width";
+      value: MaxSize;
+    }
+  | {
+      property: "max-height";
+      value: MaxSize;
+    }
+  | {
+      property: "block-size";
+      value: Size;
+    }
+  | {
+      property: "inline-size";
+      value: Size;
+    }
+  | {
+      property: "min-block-size";
+      value: Size;
+    }
+  | {
+      property: "min-inline-size";
+      value: Size;
+    }
+  | {
+      property: "max-block-size";
+      value: MaxSize;
+    }
+  | {
+      property: "max-inline-size";
+      value: MaxSize;
+    }
+  | {
+      property: "box-sizing";
+      value: BoxSizing;
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "aspect-ratio";
+      value: AspectRatio;
+    }
+  | {
+      property: "overflow";
+      value: Overflow;
+    }
+  | {
+      property: "overflow-x";
+      value: OverflowKeyword;
+    }
+  | {
+      property: "overflow-y";
+      value: OverflowKeyword;
+    }
+  | {
+      property: "text-overflow";
+      value: TextOverflow;
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "position";
+      value: Position2;
+    }
+  | {
+      property: "top";
+      value: LengthPercentageOrAuto;
+    }
+  | {
+      property: "bottom";
+      value: LengthPercentageOrAuto;
+    }
+  | {
+      property: "left";
+      value: LengthPercentageOrAuto;
+    }
+  | {
+      property: "right";
+      value: LengthPercentageOrAuto;
+    }
+  | {
+      property: "inset-block-start";
+      value: LengthPercentageOrAuto;
+    }
+  | {
+      property: "inset-block-end";
+      value: LengthPercentageOrAuto;
+    }
+  | {
+      property: "inset-inline-start";
+      value: LengthPercentageOrAuto;
+    }
+  | {
+      property: "inset-inline-end";
+      value: LengthPercentageOrAuto;
+    }
+  | {
+      property: "inset-block";
+      value: InsetBlock;
+    }
+  | {
+      property: "inset-inline";
+      value: InsetInline;
+    }
+  | {
+      property: "inset";
+      value: Inset;
+    }
+  | {
+      property: "border-spacing";
+      value: Size2DFor_Length;
+    }
+  | {
+      property: "border-top-color";
+      value: CssColor;
+    }
+  | {
+      property: "border-bottom-color";
+      value: CssColor;
+    }
+  | {
+      property: "border-left-color";
+      value: CssColor;
+    }
+  | {
+      property: "border-right-color";
+      value: CssColor;
+    }
+  | {
+      property: "border-block-start-color";
+      value: CssColor;
+    }
+  | {
+      property: "border-block-end-color";
+      value: CssColor;
+    }
+  | {
+      property: "border-inline-start-color";
+      value: CssColor;
+    }
+  | {
+      property: "border-inline-end-color";
+      value: CssColor;
+    }
+  | {
+      property: "border-top-style";
+      value: LineStyle;
+    }
+  | {
+      property: "border-bottom-style";
+      value: LineStyle;
+    }
+  | {
+      property: "border-left-style";
+      value: LineStyle;
+    }
+  | {
+      property: "border-right-style";
+      value: LineStyle;
+    }
+  | {
+      property: "border-block-start-style";
+      value: LineStyle;
+    }
+  | {
+      property: "border-block-end-style";
+      value: LineStyle;
+    }
+  | {
+      property: "border-inline-start-style";
+      value: LineStyle;
+    }
+  | {
+      property: "border-inline-end-style";
+      value: LineStyle;
+    }
+  | {
+      property: "border-top-width";
+      value: BorderSideWidth;
+    }
+  | {
+      property: "border-bottom-width";
+      value: BorderSideWidth;
+    }
+  | {
+      property: "border-left-width";
+      value: BorderSideWidth;
+    }
+  | {
+      property: "border-right-width";
+      value: BorderSideWidth;
+    }
+  | {
+      property: "border-block-start-width";
+      value: BorderSideWidth;
+    }
+  | {
+      property: "border-block-end-width";
+      value: BorderSideWidth;
+    }
+  | {
+      property: "border-inline-start-width";
+      value: BorderSideWidth;
+    }
+  | {
+      property: "border-inline-end-width";
+      value: BorderSideWidth;
+    }
+  | {
+      property: "border-top-left-radius";
+      value: Size2DFor_DimensionPercentageFor_LengthValue;
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "border-top-right-radius";
+      value: Size2DFor_DimensionPercentageFor_LengthValue;
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "border-bottom-left-radius";
+      value: Size2DFor_DimensionPercentageFor_LengthValue;
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "border-bottom-right-radius";
+      value: Size2DFor_DimensionPercentageFor_LengthValue;
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "border-start-start-radius";
+      value: Size2DFor_DimensionPercentageFor_LengthValue;
+    }
+  | {
+      property: "border-start-end-radius";
+      value: Size2DFor_DimensionPercentageFor_LengthValue;
+    }
+  | {
+      property: "border-end-start-radius";
+      value: Size2DFor_DimensionPercentageFor_LengthValue;
+    }
+  | {
+      property: "border-end-end-radius";
+      value: Size2DFor_DimensionPercentageFor_LengthValue;
+    }
+  | {
+      property: "border-radius";
+      value: BorderRadius;
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "border-image-source";
+      value: Image;
+    }
+  | {
+      property: "border-image-outset";
+      value: RectFor_LengthOrNumber;
+    }
+  | {
+      property: "border-image-repeat";
+      value: BorderImageRepeat;
+    }
+  | {
+      property: "border-image-width";
+      value: RectFor_BorderImageSideWidth;
+    }
+  | {
+      property: "border-image-slice";
+      value: BorderImageSlice;
+    }
+  | {
+      property: "border-image";
+      value: BorderImage;
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "border-color";
+      value: BorderColor;
+    }
+  | {
+      property: "border-style";
+      value: BorderStyle;
+    }
+  | {
+      property: "border-width";
+      value: BorderWidth;
+    }
+  | {
+      property: "border-block-color";
+      value: BorderBlockColor;
+    }
+  | {
+      property: "border-block-style";
+      value: BorderBlockStyle;
+    }
+  | {
+      property: "border-block-width";
+      value: BorderBlockWidth;
+    }
+  | {
+      property: "border-inline-color";
+      value: BorderInlineColor;
+    }
+  | {
+      property: "border-inline-style";
+      value: BorderInlineStyle;
+    }
+  | {
+      property: "border-inline-width";
+      value: BorderInlineWidth;
+    }
+  | {
+      property: "border";
+      value: GenericBorderFor_LineStyle;
+    }
+  | {
+      property: "border-top";
+      value: GenericBorderFor_LineStyle;
+    }
+  | {
+      property: "border-bottom";
+      value: GenericBorderFor_LineStyle;
+    }
+  | {
+      property: "border-left";
+      value: GenericBorderFor_LineStyle;
+    }
+  | {
+      property: "border-right";
+      value: GenericBorderFor_LineStyle;
+    }
+  | {
+      property: "border-block";
+      value: GenericBorderFor_LineStyle;
+    }
+  | {
+      property: "border-block-start";
+      value: GenericBorderFor_LineStyle;
+    }
+  | {
+      property: "border-block-end";
+      value: GenericBorderFor_LineStyle;
+    }
+  | {
+      property: "border-inline";
+      value: GenericBorderFor_LineStyle;
+    }
+  | {
+      property: "border-inline-start";
+      value: GenericBorderFor_LineStyle;
+    }
+  | {
+      property: "border-inline-end";
+      value: GenericBorderFor_LineStyle;
+    }
+  | {
+      property: "outline";
+      value: GenericBorderFor_OutlineStyleAnd_11;
+    }
+  | {
+      property: "outline-color";
+      value: CssColor;
+    }
+  | {
+      property: "outline-style";
+      value: OutlineStyle;
+    }
+  | {
+      property: "outline-width";
+      value: BorderSideWidth;
+    }
+  | {
+      property: "flex-direction";
+      value: FlexDirection;
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "flex-wrap";
+      value: FlexWrap;
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "flex-flow";
+      value: FlexFlow;
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "flex-grow";
+      value: number;
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "flex-shrink";
+      value: number;
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "flex-basis";
+      value: LengthPercentageOrAuto;
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "flex";
+      value: Flex;
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "order";
+      value: number;
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "align-content";
+      value: AlignContent;
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "justify-content";
+      value: JustifyContent;
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "place-content";
+      value: PlaceContent;
+    }
+  | {
+      property: "align-self";
+      value: AlignSelf;
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "justify-self";
+      value: JustifySelf;
+    }
+  | {
+      property: "place-self";
+      value: PlaceSelf;
+    }
+  | {
+      property: "align-items";
+      value: AlignItems;
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "justify-items";
+      value: JustifyItems;
+    }
+  | {
+      property: "place-items";
+      value: PlaceItems;
+    }
+  | {
+      property: "row-gap";
+      value: GapValue;
+    }
+  | {
+      property: "column-gap";
+      value: GapValue;
+    }
+  | {
+      property: "gap";
+      value: Gap;
+    }
+  | {
+      property: "box-orient";
+      value: BoxOrient;
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "box-direction";
+      value: BoxDirection;
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "box-ordinal-group";
+      value: number;
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "box-align";
+      value: BoxAlign;
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "box-flex";
+      value: number;
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "box-flex-group";
+      value: number;
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "box-pack";
+      value: BoxPack;
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "box-lines";
+      value: BoxLines;
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "flex-pack";
+      value: FlexPack;
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "flex-order";
+      value: number;
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "flex-align";
+      value: BoxAlign;
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "flex-item-align";
+      value: FlexItemAlign;
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "flex-line-pack";
+      value: FlexLinePack;
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "flex-positive";
+      value: number;
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "flex-negative";
+      value: number;
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "flex-preferred-size";
+      value: LengthPercentageOrAuto;
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "grid-template-columns";
+      value: TrackSizing;
+    }
+  | {
+      property: "grid-template-rows";
+      value: TrackSizing;
+    }
+  | {
+      property: "grid-auto-columns";
+      value: TrackSize[];
+    }
+  | {
+      property: "grid-auto-rows";
+      value: TrackSize[];
+    }
+  | {
+      property: "grid-auto-flow";
+      value: GridAutoFlow;
+    }
+  | {
+      property: "grid-template-areas";
+      value: GridTemplateAreas;
+    }
+  | {
+      property: "grid-template";
+      value: GridTemplate;
+    }
+  | {
+      property: "grid";
+      value: Grid;
+    }
+  | {
+      property: "grid-row-start";
+      value: GridLine;
+    }
+  | {
+      property: "grid-row-end";
+      value: GridLine;
+    }
+  | {
+      property: "grid-column-start";
+      value: GridLine;
+    }
+  | {
+      property: "grid-column-end";
+      value: GridLine;
+    }
+  | {
+      property: "grid-row";
+      value: GridRow;
+    }
+  | {
+      property: "grid-column";
+      value: GridColumn;
+    }
+  | {
+      property: "grid-area";
+      value: GridArea;
+    }
+  | {
+      property: "margin-top";
+      value: LengthPercentageOrAuto;
+    }
+  | {
+      property: "margin-bottom";
+      value: LengthPercentageOrAuto;
+    }
+  | {
+      property: "margin-left";
+      value: LengthPercentageOrAuto;
+    }
+  | {
+      property: "margin-right";
+      value: LengthPercentageOrAuto;
+    }
+  | {
+      property: "margin-block-start";
+      value: LengthPercentageOrAuto;
+    }
+  | {
+      property: "margin-block-end";
+      value: LengthPercentageOrAuto;
+    }
+  | {
+      property: "margin-inline-start";
+      value: LengthPercentageOrAuto;
+    }
+  | {
+      property: "margin-inline-end";
+      value: LengthPercentageOrAuto;
+    }
+  | {
+      property: "margin-block";
+      value: MarginBlock;
+    }
+  | {
+      property: "margin-inline";
+      value: MarginInline;
+    }
+  | {
+      property: "margin";
+      value: Margin;
+    }
+  | {
+      property: "padding-top";
+      value: LengthPercentageOrAuto;
+    }
+  | {
+      property: "padding-bottom";
+      value: LengthPercentageOrAuto;
+    }
+  | {
+      property: "padding-left";
+      value: LengthPercentageOrAuto;
+    }
+  | {
+      property: "padding-right";
+      value: LengthPercentageOrAuto;
+    }
+  | {
+      property: "padding-block-start";
+      value: LengthPercentageOrAuto;
+    }
+  | {
+      property: "padding-block-end";
+      value: LengthPercentageOrAuto;
+    }
+  | {
+      property: "padding-inline-start";
+      value: LengthPercentageOrAuto;
+    }
+  | {
+      property: "padding-inline-end";
+      value: LengthPercentageOrAuto;
+    }
+  | {
+      property: "padding-block";
+      value: PaddingBlock;
+    }
+  | {
+      property: "padding-inline";
+      value: PaddingInline;
+    }
+  | {
+      property: "padding";
+      value: Padding;
+    }
+  | {
+      property: "scroll-margin-top";
+      value: LengthPercentageOrAuto;
+    }
+  | {
+      property: "scroll-margin-bottom";
+      value: LengthPercentageOrAuto;
+    }
+  | {
+      property: "scroll-margin-left";
+      value: LengthPercentageOrAuto;
+    }
+  | {
+      property: "scroll-margin-right";
+      value: LengthPercentageOrAuto;
+    }
+  | {
+      property: "scroll-margin-block-start";
+      value: LengthPercentageOrAuto;
+    }
+  | {
+      property: "scroll-margin-block-end";
+      value: LengthPercentageOrAuto;
+    }
+  | {
+      property: "scroll-margin-inline-start";
+      value: LengthPercentageOrAuto;
+    }
+  | {
+      property: "scroll-margin-inline-end";
+      value: LengthPercentageOrAuto;
+    }
+  | {
+      property: "scroll-margin-block";
+      value: ScrollMarginBlock;
+    }
+  | {
+      property: "scroll-margin-inline";
+      value: ScrollMarginInline;
+    }
+  | {
+      property: "scroll-margin";
+      value: ScrollMargin;
+    }
+  | {
+      property: "scroll-padding-top";
+      value: LengthPercentageOrAuto;
+    }
+  | {
+      property: "scroll-padding-bottom";
+      value: LengthPercentageOrAuto;
+    }
+  | {
+      property: "scroll-padding-left";
+      value: LengthPercentageOrAuto;
+    }
+  | {
+      property: "scroll-padding-right";
+      value: LengthPercentageOrAuto;
+    }
+  | {
+      property: "scroll-padding-block-start";
+      value: LengthPercentageOrAuto;
+    }
+  | {
+      property: "scroll-padding-block-end";
+      value: LengthPercentageOrAuto;
+    }
+  | {
+      property: "scroll-padding-inline-start";
+      value: LengthPercentageOrAuto;
+    }
+  | {
+      property: "scroll-padding-inline-end";
+      value: LengthPercentageOrAuto;
+    }
+  | {
+      property: "scroll-padding-block";
+      value: ScrollPaddingBlock;
+    }
+  | {
+      property: "scroll-padding-inline";
+      value: ScrollPaddingInline;
+    }
+  | {
+      property: "scroll-padding";
+      value: ScrollPadding;
+    }
+  | {
+      property: "font-weight";
+      value: FontWeight;
+    }
+  | {
+      property: "font-size";
+      value: FontSize;
+    }
+  | {
+      property: "font-stretch";
+      value: FontStretch;
+    }
+  | {
+      property: "font-family";
+      value: FontFamily[];
+    }
+  | {
+      property: "font-style";
+      value: FontStyle;
+    }
+  | {
+      property: "font-variant-caps";
+      value: FontVariantCaps;
+    }
+  | {
+      property: "line-height";
+      value: LineHeight;
+    }
+  | {
+      property: "font";
+      value: Font;
+    }
+  | {
+      property: "vertical-align";
+      value: VerticalAlign;
+    }
+  | {
+      property: "font-palette";
+      value: DashedIdentReference;
+    }
+  | {
+      property: "transition-property";
+      value: PropertyId[];
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "transition-duration";
+      value: Time[];
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "transition-delay";
+      value: Time[];
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "transition-timing-function";
+      value: EasingFunction[];
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "transition";
+      value: Transition[];
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "animation-name";
+      value: AnimationName[];
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "animation-duration";
+      value: Time[];
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "animation-timing-function";
+      value: EasingFunction[];
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "animation-iteration-count";
+      value: AnimationIterationCount[];
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "animation-direction";
+      value: AnimationDirection[];
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "animation-play-state";
+      value: AnimationPlayState[];
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "animation-delay";
+      value: Time[];
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "animation-fill-mode";
+      value: AnimationFillMode[];
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "animation-composition";
+      value: AnimationComposition[];
+    }
+  | {
+      property: "animation-timeline";
+      value: AnimationTimeline[];
+    }
+  | {
+      property: "animation-range-start";
+      value: AnimationRangeStart[];
+    }
+  | {
+      property: "animation-range-end";
+      value: AnimationRangeEnd[];
+    }
+  | {
+      property: "animation-range";
+      value: AnimationRange[];
+    }
+  | {
+      property: "animation";
+      value: Animation[];
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "transform";
+      value: Transform[];
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "transform-origin";
+      value: Position;
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "transform-style";
+      value: TransformStyle;
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "transform-box";
+      value: TransformBox;
+    }
+  | {
+      property: "backface-visibility";
+      value: BackfaceVisibility;
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "perspective";
+      value: Perspective;
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "perspective-origin";
+      value: Position;
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "translate";
+      value: Translate;
+    }
+  | {
+      property: "rotate";
+      value: Rotate;
+    }
+  | {
+      property: "scale";
+      value: Scale;
+    }
+  | {
+      property: "text-transform";
+      value: TextTransform;
+    }
+  | {
+      property: "white-space";
+      value: WhiteSpace;
+    }
+  | {
+      property: "tab-size";
+      value: LengthOrNumber;
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "word-break";
+      value: WordBreak;
+    }
+  | {
+      property: "line-break";
+      value: LineBreak;
+    }
+  | {
+      property: "hyphens";
+      value: Hyphens;
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "overflow-wrap";
+      value: OverflowWrap;
+    }
+  | {
+      property: "word-wrap";
+      value: OverflowWrap;
+    }
+  | {
+      property: "text-align";
+      value: TextAlign;
+    }
+  | {
+      property: "text-align-last";
+      value: TextAlignLast;
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "text-justify";
+      value: TextJustify;
+    }
+  | {
+      property: "word-spacing";
+      value: Spacing;
+    }
+  | {
+      property: "letter-spacing";
+      value: Spacing;
+    }
+  | {
+      property: "text-indent";
+      value: TextIndent;
+    }
+  | {
+      property: "text-decoration-line";
+      value: TextDecorationLine;
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "text-decoration-style";
+      value: TextDecorationStyle;
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "text-decoration-color";
+      value: CssColor;
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "text-decoration-thickness";
+      value: TextDecorationThickness;
+    }
+  | {
+      property: "text-decoration";
+      value: TextDecoration;
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "text-decoration-skip-ink";
+      value: TextDecorationSkipInk;
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "text-emphasis-style";
+      value: TextEmphasisStyle;
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "text-emphasis-color";
+      value: CssColor;
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "text-emphasis";
+      value: TextEmphasis;
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "text-emphasis-position";
+      value: TextEmphasisPosition;
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "text-shadow";
+      value: TextShadow[];
+    }
+  | {
+      property: "text-size-adjust";
+      value: TextSizeAdjust;
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "direction";
+      value: Direction2;
+    }
+  | {
+      property: "unicode-bidi";
+      value: UnicodeBidi;
+    }
+  | {
+      property: "box-decoration-break";
+      value: BoxDecorationBreak;
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "resize";
+      value: Resize;
+    }
+  | {
+      property: "cursor";
+      value: Cursor;
+    }
+  | {
+      property: "caret-color";
+      value: ColorOrAuto;
+    }
+  | {
+      property: "caret-shape";
+      value: CaretShape;
+    }
+  | {
+      property: "caret";
+      value: Caret;
+    }
+  | {
+      property: "user-select";
+      value: UserSelect;
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "accent-color";
+      value: ColorOrAuto;
+    }
+  | {
+      property: "appearance";
+      value: Appearance;
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "list-style-type";
+      value: ListStyleType;
+    }
+  | {
+      property: "list-style-image";
+      value: Image;
+    }
+  | {
+      property: "list-style-position";
+      value: ListStylePosition;
+    }
+  | {
+      property: "list-style";
+      value: ListStyle;
+    }
+  | {
+      property: "marker-side";
+      value: MarkerSide;
+    }
+  | {
+      property: "composes";
+      value: Composes;
+    }
+  | {
+      property: "fill";
+      value: SVGPaint;
+    }
+  | {
+      property: "fill-rule";
+      value: FillRule;
+    }
+  | {
+      property: "fill-opacity";
+      value: number;
+    }
+  | {
+      property: "stroke";
+      value: SVGPaint;
+    }
+  | {
+      property: "stroke-opacity";
+      value: number;
+    }
+  | {
+      property: "stroke-width";
+      value: DimensionPercentageFor_LengthValue;
+    }
+  | {
+      property: "stroke-linecap";
+      value: StrokeLinecap;
+    }
+  | {
+      property: "stroke-linejoin";
+      value: StrokeLinejoin;
+    }
+  | {
+      property: "stroke-miterlimit";
+      value: number;
+    }
+  | {
+      property: "stroke-dasharray";
+      value: StrokeDasharray;
+    }
+  | {
+      property: "stroke-dashoffset";
+      value: DimensionPercentageFor_LengthValue;
+    }
+  | {
+      property: "marker-start";
+      value: Marker;
+    }
+  | {
+      property: "marker-mid";
+      value: Marker;
+    }
+  | {
+      property: "marker-end";
+      value: Marker;
+    }
+  | {
+      property: "marker";
+      value: Marker;
+    }
+  | {
+      property: "color-interpolation";
+      value: ColorInterpolation;
+    }
+  | {
+      property: "color-interpolation-filters";
+      value: ColorInterpolation;
+    }
+  | {
+      property: "color-rendering";
+      value: ColorRendering;
+    }
+  | {
+      property: "shape-rendering";
+      value: ShapeRendering;
+    }
+  | {
+      property: "text-rendering";
+      value: TextRendering;
+    }
+  | {
+      property: "image-rendering";
+      value: ImageRendering;
+    }
+  | {
+      property: "clip-path";
+      value: ClipPath;
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "clip-rule";
+      value: FillRule;
+    }
+  | {
+      property: "mask-image";
+      value: Image[];
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "mask-mode";
+      value: MaskMode[];
+    }
+  | {
+      property: "mask-repeat";
+      value: BackgroundRepeat[];
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "mask-position-x";
+      value: PositionComponentFor_HorizontalPositionKeyword[];
+    }
+  | {
+      property: "mask-position-y";
+      value: PositionComponentFor_VerticalPositionKeyword[];
+    }
+  | {
+      property: "mask-position";
+      value: Position[];
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "mask-clip";
+      value: MaskClip[];
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "mask-origin";
+      value: GeometryBox[];
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "mask-size";
+      value: BackgroundSize[];
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "mask-composite";
+      value: MaskComposite[];
+    }
+  | {
+      property: "mask-type";
+      value: MaskType;
+    }
+  | {
+      property: "mask";
+      value: Mask[];
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "mask-border-source";
+      value: Image;
+    }
+  | {
+      property: "mask-border-mode";
+      value: MaskBorderMode;
+    }
+  | {
+      property: "mask-border-slice";
+      value: BorderImageSlice;
+    }
+  | {
+      property: "mask-border-width";
+      value: RectFor_BorderImageSideWidth;
+    }
+  | {
+      property: "mask-border-outset";
+      value: RectFor_LengthOrNumber;
+    }
+  | {
+      property: "mask-border-repeat";
+      value: BorderImageRepeat;
+    }
+  | {
+      property: "mask-border";
+      value: MaskBorder;
+    }
+  | {
+      property: "-webkit-mask-composite";
+      value: WebKitMaskComposite[];
+    }
+  | {
+      property: "mask-source-type";
+      value: WebKitMaskSourceType[];
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "mask-box-image";
+      value: BorderImage;
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "mask-box-image-source";
+      value: Image;
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "mask-box-image-slice";
+      value: BorderImageSlice;
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "mask-box-image-width";
+      value: RectFor_BorderImageSideWidth;
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "mask-box-image-outset";
+      value: RectFor_LengthOrNumber;
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "mask-box-image-repeat";
+      value: BorderImageRepeat;
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "filter";
+      value: FilterList;
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "backdrop-filter";
+      value: FilterList;
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      property: "z-index";
+      value: ZIndex;
+    }
+  | {
+      property: "container-type";
+      value: ContainerType;
+    }
+  | {
+      property: "container-name";
+      value: ContainerNameList;
+    }
+  | {
+      property: "container";
+      value: Container;
+    }
+  | {
+      property: "view-transition-name";
+      value: ViewTransitionName;
+    }
+  | {
+      property: "view-transition-class";
+      value: NoneOrCustomIdentList;
+    }
+  | {
+      property: "view-transition-group";
+      value: ViewTransitionGroup;
+    }
+  | {
+      property: "color-scheme";
+      value: ColorScheme;
+    }
+  | {
+      property: "all";
+      value: CSSWideKeyword;
+    }
+  | {
+      property: "unparsed";
+      value: UnparsedProperty;
+    }
+  | {
+      property: "custom";
+      value: CustomProperty;
+    };
+/**
+ * A CSS [`<image>`](https://www.w3.org/TR/css-images-3/#image-values) value.
+ */
+export type Image =
+  | {
+      type: "none";
+    }
+  | {
+      type: "url";
+      value: Url;
+    }
+  | {
+      type: "gradient";
+      value: Gradient;
+    }
+  | {
+      type: "image-set";
+      value: ImageSet;
+    };
+/**
+ * A CSS [`<gradient>`](https://www.w3.org/TR/css-images-3/#gradients) value.
+ */
+export type Gradient =
+  | {
+      /**
+       * The direction of the gradient.
+       */
+      direction: LineDirection;
+      /**
+       * The color stops and transition hints for the gradient.
+       */
+      items: GradientItemFor_DimensionPercentageFor_LengthValue[];
+      type: "linear";
+      /**
+       * The vendor prefixes for the gradient.
+       */
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      /**
+       * The direction of the gradient.
+       */
+      direction: LineDirection;
+      /**
+       * The color stops and transition hints for the gradient.
+       */
+      items: GradientItemFor_DimensionPercentageFor_LengthValue[];
+      type: "repeating-linear";
+      /**
+       * The vendor prefixes for the gradient.
+       */
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      /**
+       * The color stops and transition hints for the gradient.
+       */
+      items: GradientItemFor_DimensionPercentageFor_LengthValue[];
+      /**
+       * The position of the gradient.
+       */
+      position: Position;
+      /**
+       * The shape of the gradient.
+       */
+      shape: EndingShape;
+      type: "radial";
+      /**
+       * The vendor prefixes for the gradient.
+       */
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      /**
+       * The color stops and transition hints for the gradient.
+       */
+      items: GradientItemFor_DimensionPercentageFor_LengthValue[];
+      /**
+       * The position of the gradient.
+       */
+      position: Position;
+      /**
+       * The shape of the gradient.
+       */
+      shape: EndingShape;
+      type: "repeating-radial";
+      /**
+       * The vendor prefixes for the gradient.
+       */
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      /**
+       * The angle of the gradient.
+       */
+      angle: Angle;
+      /**
+       * The color stops and transition hints for the gradient.
+       */
+      items: GradientItemFor_DimensionPercentageFor_Angle[];
+      /**
+       * The position of the gradient.
+       */
+      position: Position;
+      type: "conic";
+    }
+  | {
+      /**
+       * The angle of the gradient.
+       */
+      angle: Angle;
+      /**
+       * The color stops and transition hints for the gradient.
+       */
+      items: GradientItemFor_DimensionPercentageFor_Angle[];
+      /**
+       * The position of the gradient.
+       */
+      position: Position;
+      type: "repeating-conic";
+    }
+  | (
+      | {
+          type: "webkit-gradient";
+          /**
+           * The starting point of the gradient.
+           */
+          from: WebKitGradientPoint;
+          kind: "linear";
+          /**
+           * The color stops in the gradient.
+           */
+          stops: WebKitColorStop[];
+          /**
+           * The ending point of the gradient.
+           */
+          to: WebKitGradientPoint;
+        }
+      | {
+          type: "webkit-gradient";
+          /**
+           * The starting point of the gradient.
+           */
+          from: WebKitGradientPoint;
+          kind: "radial";
+          /**
+           * The starting radius of the gradient.
+           */
+          r0: number;
+          /**
+           * The ending radius of the gradient.
+           */
+          r1: number;
+          /**
+           * The color stops in the gradient.
+           */
+          stops: WebKitColorStop[];
+          /**
+           * The ending point of the gradient.
+           */
+          to: WebKitGradientPoint;
+        }
+    );
+/**
+ * The direction of a CSS `linear-gradient()`.
+ *
+ * See [LinearGradient](LinearGradient).
+ */
+export type LineDirection =
+  | {
+      type: "angle";
+      value: Angle;
+    }
+  | {
+      type: "horizontal";
+      value: HorizontalPositionKeyword;
+    }
+  | {
+      type: "vertical";
+      value: VerticalPositionKeyword;
+    }
+  | {
+      /**
+       * A horizontal position keyword, e.g. `left` or `right.
+       */
+      horizontal: HorizontalPositionKeyword;
+      type: "corner";
+      /**
+       * A vertical posision keyword, e.g. `top` or `bottom`.
+       */
+      vertical: VerticalPositionKeyword;
+    };
+/**
+ * A horizontal position keyword.
+ */
+export type HorizontalPositionKeyword = "left" | "right";
+/**
+ * A vertical position keyword.
+ */
+export type VerticalPositionKeyword = "top" | "bottom";
+/**
+ * Either a color stop or interpolation hint within a gradient.
+ *
+ * This type is generic, and items may be either a [LengthPercentage](super::length::LengthPercentage) or [Angle](super::angle::Angle) depending on what type of gradient it is within.
+ */
+export type GradientItemFor_DimensionPercentageFor_LengthValue =
+  | {
+      /**
+       * The color of the color stop.
+       */
+      color: CssColor;
+      /**
+       * The position of the color stop.
+       */
+      position?: DimensionPercentageFor_LengthValue | null;
+      type: "color-stop";
+    }
+  | {
+      type: "hint";
+      value: DimensionPercentageFor_LengthValue;
+    };
+/**
+ * A generic type that allows any kind of dimension and percentage to be used standalone or mixed within a `calc()` expression.
+ *
+ * <https://drafts.csswg.org/css-values-4/#mixed-percentages>
+ */
+export type DimensionPercentageFor_LengthValue =
+  | {
+      type: "dimension";
+      value: LengthValue;
+    }
+  | {
+      type: "percentage";
+      value: number;
+    }
+  | {
+      type: "calc";
+      value: CalcFor_DimensionPercentageFor_LengthValue;
+    };
+/**
+ * A mathematical expression used within the [`calc()`](https://www.w3.org/TR/css-values-4/#calc-func) function.
+ *
+ * This type supports generic value types. Values such as [Length](super::length::Length), [Percentage](super::percentage::Percentage), [Time](super::time::Time), and [Angle](super::angle::Angle) support `calc()` expressions.
+ */
+export type CalcFor_DimensionPercentageFor_LengthValue =
+  | {
+      type: "value";
+      value: DimensionPercentageFor_LengthValue;
+    }
+  | {
+      type: "number";
+      value: number;
+    }
+  | {
+      type: "sum";
+      /**
+       * @minItems 2
+       * @maxItems 2
+       */
+      value: [CalcFor_DimensionPercentageFor_LengthValue, CalcFor_DimensionPercentageFor_LengthValue];
+    }
+  | {
+      type: "product";
+      /**
+       * @minItems 2
+       * @maxItems 2
+       */
+      value: [number, CalcFor_DimensionPercentageFor_LengthValue];
+    }
+  | {
+      type: "function";
+      value: MathFunctionFor_DimensionPercentageFor_LengthValue;
+    };
+/**
+ * A CSS [math function](https://www.w3.org/TR/css-values-4/#math-function).
+ *
+ * Math functions may be used in most properties and values that accept numeric values, including lengths, percentages, angles, times, etc.
+ */
+export type MathFunctionFor_DimensionPercentageFor_LengthValue =
+  | {
+      type: "calc";
+      value: CalcFor_DimensionPercentageFor_LengthValue;
+    }
+  | {
+      type: "min";
+      value: CalcFor_DimensionPercentageFor_LengthValue[];
+    }
+  | {
+      type: "max";
+      value: CalcFor_DimensionPercentageFor_LengthValue[];
+    }
+  | {
+      type: "clamp";
+      /**
+       * @minItems 3
+       * @maxItems 3
+       */
+      value: [
+        CalcFor_DimensionPercentageFor_LengthValue,
+        CalcFor_DimensionPercentageFor_LengthValue,
+        CalcFor_DimensionPercentageFor_LengthValue
+      ];
+    }
+  | {
+      type: "round";
+      /**
+       * @minItems 3
+       * @maxItems 3
+       */
+      value: [RoundingStrategy, CalcFor_DimensionPercentageFor_LengthValue, CalcFor_DimensionPercentageFor_LengthValue];
+    }
+  | {
+      type: "rem";
+      /**
+       * @minItems 2
+       * @maxItems 2
+       */
+      value: [CalcFor_DimensionPercentageFor_LengthValue, CalcFor_DimensionPercentageFor_LengthValue];
+    }
+  | {
+      type: "mod";
+      /**
+       * @minItems 2
+       * @maxItems 2
+       */
+      value: [CalcFor_DimensionPercentageFor_LengthValue, CalcFor_DimensionPercentageFor_LengthValue];
+    }
+  | {
+      type: "abs";
+      value: CalcFor_DimensionPercentageFor_LengthValue;
+    }
+  | {
+      type: "sign";
+      value: CalcFor_DimensionPercentageFor_LengthValue;
+    }
+  | {
+      type: "hypot";
+      value: CalcFor_DimensionPercentageFor_LengthValue[];
+    };
+/**
+ * A component within a [Position](Position) value, representing a position along either the horizontal or vertical axis of a box.
+ *
+ * This type is generic over side keywords.
+ */
+export type PositionComponentFor_HorizontalPositionKeyword =
+  | {
+      type: "center";
+    }
+  | {
+      type: "length";
+      value: DimensionPercentageFor_LengthValue;
+    }
+  | {
+      /**
+       * Offset from the side.
+       */
+      offset?: DimensionPercentageFor_LengthValue | null;
+      /**
+       * A side keyword.
+       */
+      side: HorizontalPositionKeyword;
+      type: "side";
+    };
+/**
+ * A component within a [Position](Position) value, representing a position along either the horizontal or vertical axis of a box.
+ *
+ * This type is generic over side keywords.
+ */
+export type PositionComponentFor_VerticalPositionKeyword =
+  | {
+      type: "center";
+    }
+  | {
+      type: "length";
+      value: DimensionPercentageFor_LengthValue;
+    }
+  | {
+      /**
+       * Offset from the side.
+       */
+      offset?: DimensionPercentageFor_LengthValue | null;
+      /**
+       * A side keyword.
+       */
+      side: VerticalPositionKeyword;
+      type: "side";
+    };
+/**
+ * A `radial-gradient()` [ending shape](https://www.w3.org/TR/css-images-3/#valdef-radial-gradient-ending-shape).
+ *
+ * See [RadialGradient](RadialGradient).
+ */
+export type EndingShape =
+  | {
+      type: "ellipse";
+      value: Ellipse;
+    }
+  | {
+      type: "circle";
+      value: Circle;
+    };
+/**
+ * An ellipse ending shape for a `radial-gradient()`.
+ *
+ * See [RadialGradient](RadialGradient).
+ */
+export type Ellipse =
+  | {
+      type: "size";
+      /**
+       * The x-radius of the ellipse.
+       */
+      x: DimensionPercentageFor_LengthValue;
+      /**
+       * The y-radius of the ellipse.
+       */
+      y: DimensionPercentageFor_LengthValue;
+    }
+  | {
+      type: "extent";
+      value: ShapeExtent;
+    };
+/**
+ * A shape extent for a `radial-gradient()`.
+ *
+ * See [RadialGradient](RadialGradient).
+ */
+export type ShapeExtent = "closest-side" | "farthest-side" | "closest-corner" | "farthest-corner";
+/**
+ * A circle ending shape for a `radial-gradient()`.
+ *
+ * See [RadialGradient](RadialGradient).
+ */
+export type Circle =
+  | {
+      type: "radius";
+      value: Length;
+    }
+  | {
+      type: "extent";
+      value: ShapeExtent;
+    };
+/**
+ * Either a color stop or interpolation hint within a gradient.
+ *
+ * This type is generic, and items may be either a [LengthPercentage](super::length::LengthPercentage) or [Angle](super::angle::Angle) depending on what type of gradient it is within.
+ */
+export type GradientItemFor_DimensionPercentageFor_Angle =
+  | {
+      /**
+       * The color of the color stop.
+       */
+      color: CssColor;
+      /**
+       * The position of the color stop.
+       */
+      position?: DimensionPercentageFor_Angle | null;
+      type: "color-stop";
+    }
+  | {
+      type: "hint";
+      value: DimensionPercentageFor_Angle;
+    };
+/**
+ * A generic type that allows any kind of dimension and percentage to be used standalone or mixed within a `calc()` expression.
+ *
+ * <https://drafts.csswg.org/css-values-4/#mixed-percentages>
+ */
+export type DimensionPercentageFor_Angle =
+  | {
+      type: "dimension";
+      value: Angle;
+    }
+  | {
+      type: "percentage";
+      value: number;
+    }
+  | {
+      type: "calc";
+      value: CalcFor_DimensionPercentageFor_Angle;
+    };
+/**
+ * A mathematical expression used within the [`calc()`](https://www.w3.org/TR/css-values-4/#calc-func) function.
+ *
+ * This type supports generic value types. Values such as [Length](super::length::Length), [Percentage](super::percentage::Percentage), [Time](super::time::Time), and [Angle](super::angle::Angle) support `calc()` expressions.
+ */
+export type CalcFor_DimensionPercentageFor_Angle =
+  | {
+      type: "value";
+      value: DimensionPercentageFor_Angle;
+    }
+  | {
+      type: "number";
+      value: number;
+    }
+  | {
+      type: "sum";
+      /**
+       * @minItems 2
+       * @maxItems 2
+       */
+      value: [CalcFor_DimensionPercentageFor_Angle, CalcFor_DimensionPercentageFor_Angle];
+    }
+  | {
+      type: "product";
+      /**
+       * @minItems 2
+       * @maxItems 2
+       */
+      value: [number, CalcFor_DimensionPercentageFor_Angle];
+    }
+  | {
+      type: "function";
+      value: MathFunctionFor_DimensionPercentageFor_Angle;
+    };
+/**
+ * A CSS [math function](https://www.w3.org/TR/css-values-4/#math-function).
+ *
+ * Math functions may be used in most properties and values that accept numeric values, including lengths, percentages, angles, times, etc.
+ */
+export type MathFunctionFor_DimensionPercentageFor_Angle =
+  | {
+      type: "calc";
+      value: CalcFor_DimensionPercentageFor_Angle;
+    }
+  | {
+      type: "min";
+      value: CalcFor_DimensionPercentageFor_Angle[];
+    }
+  | {
+      type: "max";
+      value: CalcFor_DimensionPercentageFor_Angle[];
+    }
+  | {
+      type: "clamp";
+      /**
+       * @minItems 3
+       * @maxItems 3
+       */
+      value: [
+        CalcFor_DimensionPercentageFor_Angle,
+        CalcFor_DimensionPercentageFor_Angle,
+        CalcFor_DimensionPercentageFor_Angle
+      ];
+    }
+  | {
+      type: "round";
+      /**
+       * @minItems 3
+       * @maxItems 3
+       */
+      value: [RoundingStrategy, CalcFor_DimensionPercentageFor_Angle, CalcFor_DimensionPercentageFor_Angle];
+    }
+  | {
+      type: "rem";
+      /**
+       * @minItems 2
+       * @maxItems 2
+       */
+      value: [CalcFor_DimensionPercentageFor_Angle, CalcFor_DimensionPercentageFor_Angle];
+    }
+  | {
+      type: "mod";
+      /**
+       * @minItems 2
+       * @maxItems 2
+       */
+      value: [CalcFor_DimensionPercentageFor_Angle, CalcFor_DimensionPercentageFor_Angle];
+    }
+  | {
+      type: "abs";
+      value: CalcFor_DimensionPercentageFor_Angle;
+    }
+  | {
+      type: "sign";
+      value: CalcFor_DimensionPercentageFor_Angle;
+    }
+  | {
+      type: "hypot";
+      value: CalcFor_DimensionPercentageFor_Angle[];
+    };
+/**
+ * A keyword or number within a [WebKitGradientPoint](WebKitGradientPoint).
+ */
+export type WebKitGradientPointComponentFor_HorizontalPositionKeyword =
+  | {
+      type: "center";
+    }
+  | {
+      type: "number";
+      value: NumberOrPercentage;
+    }
+  | {
+      type: "side";
+      value: HorizontalPositionKeyword;
+    };
+/**
+ * Either a `<number>` or `<percentage>`.
+ */
+export type NumberOrPercentage =
+  | {
+      type: "number";
+      value: number;
+    }
+  | {
+      type: "percentage";
+      value: number;
+    };
+/**
+ * A keyword or number within a [WebKitGradientPoint](WebKitGradientPoint).
+ */
+export type WebKitGradientPointComponentFor_VerticalPositionKeyword =
+  | {
+      type: "center";
+    }
+  | {
+      type: "number";
+      value: NumberOrPercentage;
+    }
+  | {
+      type: "side";
+      value: VerticalPositionKeyword;
+    };
+/**
+ * A value for the [background-size](https://www.w3.org/TR/css-backgrounds-3/#background-size) property.
+ */
+export type BackgroundSize =
+  | {
+      /**
+       * The height of the background.
+       */
+      height: LengthPercentageOrAuto;
+      type: "explicit";
+      /**
+       * The width of the background.
+       */
+      width: LengthPercentageOrAuto;
+    }
+  | {
+      type: "cover";
+    }
+  | {
+      type: "contain";
+    };
+/**
+ * Either a [`<length-percentage>`](https://www.w3.org/TR/css-values-4/#typedef-length-percentage), or the `auto` keyword.
+ */
+export type LengthPercentageOrAuto =
+  | {
+      type: "auto";
+    }
+  | {
+      type: "length-percentage";
+      value: DimensionPercentageFor_LengthValue;
+    };
+/**
+ * A [`<repeat-style>`](https://www.w3.org/TR/css-backgrounds-3/#typedef-repeat-style) value, used within the `background-repeat` property to represent how a background image is repeated in a single direction.
+ *
+ * See [BackgroundRepeat](BackgroundRepeat).
+ */
+export type BackgroundRepeatKeyword = "repeat" | "space" | "round" | "no-repeat";
+/**
+ * A value for the [background-attachment](https://www.w3.org/TR/css-backgrounds-3/#background-attachment) property.
+ */
+export type BackgroundAttachment = "scroll" | "fixed" | "local";
+/**
+ * A value for the [background-clip](https://drafts.csswg.org/css-backgrounds-4/#background-clip) property.
+ */
+export type BackgroundClip = "border-box" | "padding-box" | "content-box" | "border" | "text";
+/**
+ * A value for the [background-origin](https://www.w3.org/TR/css-backgrounds-3/#background-origin) property.
+ */
+export type BackgroundOrigin = "border-box" | "padding-box" | "content-box";
+/**
+ * A value for the [display](https://drafts.csswg.org/css-display-3/#the-display-properties) property.
+ */
+export type Display =
+  | {
+      type: "keyword";
+      value: DisplayKeyword;
+    }
+  | {
+      /**
+       * The inside display value.
+       */
+      inside: DisplayInside;
+      /**
+       * Whether this is a list item.
+       */
+      isListItem: boolean;
+      /**
+       * The outside display value.
+       */
+      outside: DisplayOutside;
+      type: "pair";
+    };
+/**
+ * A `display` keyword.
+ *
+ * See [Display](Display).
+ */
+export type DisplayKeyword =
+  | "none"
+  | "contents"
+  | "table-row-group"
+  | "table-header-group"
+  | "table-footer-group"
+  | "table-row"
+  | "table-cell"
+  | "table-column-group"
+  | "table-column"
+  | "table-caption"
+  | "ruby-base"
+  | "ruby-text"
+  | "ruby-base-container"
+  | "ruby-text-container";
+/**
+ * A [`<display-inside>`](https://drafts.csswg.org/css-display-3/#typedef-display-inside) value.
+ */
+export type DisplayInside =
+  | {
+      type: "flow";
+    }
+  | {
+      type: "flow-root";
+    }
+  | {
+      type: "table";
+    }
+  | {
+      type: "flex";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      type: "box";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      type: "grid";
+    }
+  | {
+      type: "ruby";
+    };
+/**
+ * A [`<display-outside>`](https://drafts.csswg.org/css-display-3/#typedef-display-outside) value.
+ */
+export type DisplayOutside = "block" | "inline" | "run-in";
+/**
+ * A value for the [visibility](https://drafts.csswg.org/css-display-3/#visibility) property.
+ */
+export type Visibility = "visible" | "hidden" | "collapse";
+/**
+ * A value for the [preferred size properties](https://drafts.csswg.org/css-sizing-3/#preferred-size-properties), i.e. `width` and `height.
+ */
+export type Size =
+  | {
+      type: "auto";
+    }
+  | {
+      type: "length-percentage";
+      value: DimensionPercentageFor_LengthValue;
+    }
+  | {
+      type: "min-content";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      type: "max-content";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      type: "fit-content";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      type: "fit-content-function";
+      value: DimensionPercentageFor_LengthValue;
+    }
+  | {
+      type: "stretch";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      type: "contain";
+    };
+/**
+ * A value for the [minimum](https://drafts.csswg.org/css-sizing-3/#min-size-properties) and [maximum](https://drafts.csswg.org/css-sizing-3/#max-size-properties) size properties, e.g. `min-width` and `max-height`.
+ */
+export type MaxSize =
+  | {
+      type: "none";
+    }
+  | {
+      type: "length-percentage";
+      value: DimensionPercentageFor_LengthValue;
+    }
+  | {
+      type: "min-content";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      type: "max-content";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      type: "fit-content";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      type: "fit-content-function";
+      value: DimensionPercentageFor_LengthValue;
+    }
+  | {
+      type: "stretch";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      type: "contain";
+    };
+/**
+ * A value for the [box-sizing](https://drafts.csswg.org/css-sizing-3/#box-sizing) property.
+ */
+export type BoxSizing = "content-box" | "border-box";
+/**
+ * An [overflow](https://www.w3.org/TR/css-overflow-3/#overflow-properties) keyword as used in the `overflow-x`, `overflow-y`, and `overflow` properties.
+ */
+export type OverflowKeyword = "visible" | "hidden" | "clip" | "scroll" | "auto";
+/**
+ * A value for the [text-overflow](https://www.w3.org/TR/css-overflow-3/#text-overflow) property.
+ */
+export type TextOverflow = "clip" | "ellipsis";
+/**
+ * A value for the [position](https://www.w3.org/TR/css-position-3/#position-property) property.
+ */
+export type Position2 =
+  | {
+      type: "static";
+    }
+  | {
+      type: "relative";
+    }
+  | {
+      type: "absolute";
+    }
+  | {
+      type: "sticky";
+      value: VendorPrefix;
+    }
+  | {
+      type: "fixed";
+    };
+/**
+ * A generic value that represents a value with two components, e.g. a border radius.
+ *
+ * When serialized, only a single component will be written if both are equal.
+ *
+ * @minItems 2
+ * @maxItems 2
+ */
+export type Size2DFor_Length = [Length, Length];
+/**
+ * A [`<line-style>`](https://drafts.csswg.org/css-backgrounds/#typedef-line-style) value, used in the `border-style` property.
+ */
+export type LineStyle =
+  | "none"
+  | "hidden"
+  | "inset"
+  | "groove"
+  | "outset"
+  | "ridge"
+  | "dotted"
+  | "dashed"
+  | "solid"
+  | "double";
+/**
+ * A value for the [border-width](https://www.w3.org/TR/css-backgrounds-3/#border-width) property.
+ */
+export type BorderSideWidth =
+  | {
+      type: "thin";
+    }
+  | {
+      type: "medium";
+    }
+  | {
+      type: "thick";
+    }
+  | {
+      type: "length";
+      value: Length;
+    };
+/**
+ * A generic value that represents a value with two components, e.g. a border radius.
+ *
+ * When serialized, only a single component will be written if both are equal.
+ *
+ * @minItems 2
+ * @maxItems 2
+ */
+export type Size2DFor_DimensionPercentageFor_LengthValue = [
+  DimensionPercentageFor_LengthValue,
+  DimensionPercentageFor_LengthValue
+];
+/**
+ * A generic value that represents a value for four sides of a box, e.g. border-width, margin, padding, etc.
+ *
+ * When serialized, as few components as possible are written when there are duplicate values.
+ *
+ * @minItems 4
+ * @maxItems 4
+ */
+export type RectFor_LengthOrNumber = [LengthOrNumber, LengthOrNumber, LengthOrNumber, LengthOrNumber];
+/**
+ * Either a [`<length>`](https://www.w3.org/TR/css-values-4/#lengths) or a [`<number>`](https://www.w3.org/TR/css-values-4/#numbers).
+ */
+export type LengthOrNumber =
+  | {
+      type: "number";
+      value: number;
+    }
+  | {
+      type: "length";
+      value: Length;
+    };
+/**
+ * A single [border-image-repeat](https://www.w3.org/TR/css-backgrounds-3/#border-image-repeat) keyword.
+ */
+export type BorderImageRepeatKeyword = "stretch" | "repeat" | "round" | "space";
+/**
+ * A generic value that represents a value for four sides of a box, e.g. border-width, margin, padding, etc.
+ *
+ * When serialized, as few components as possible are written when there are duplicate values.
+ *
+ * @minItems 4
+ * @maxItems 4
+ */
+export type RectFor_BorderImageSideWidth = [
+  BorderImageSideWidth,
+  BorderImageSideWidth,
+  BorderImageSideWidth,
+  BorderImageSideWidth
+];
+/**
+ * A value for the [border-image-width](https://www.w3.org/TR/css-backgrounds-3/#border-image-width) property.
+ */
+export type BorderImageSideWidth =
+  | {
+      type: "number";
+      value: number;
+    }
+  | {
+      type: "length-percentage";
+      value: DimensionPercentageFor_LengthValue;
+    }
+  | {
+      type: "auto";
+    };
+/**
+ * A generic value that represents a value for four sides of a box, e.g. border-width, margin, padding, etc.
+ *
+ * When serialized, as few components as possible are written when there are duplicate values.
+ *
+ * @minItems 4
+ * @maxItems 4
+ */
+export type RectFor_NumberOrPercentage = [
+  NumberOrPercentage,
+  NumberOrPercentage,
+  NumberOrPercentage,
+  NumberOrPercentage
+];
+/**
+ * A value for the [outline-style](https://drafts.csswg.org/css-ui/#outline-style) property.
+ */
+export type OutlineStyle =
+  | {
+      type: "auto";
+    }
+  | {
+      type: "line-style";
+      value: LineStyle;
+    };
+/**
+ * A value for the [flex-direction](https://www.w3.org/TR/2018/CR-css-flexbox-1-20181119/#propdef-flex-direction) property.
+ */
+export type FlexDirection = "row" | "row-reverse" | "column" | "column-reverse";
+/**
+ * A value for the [flex-wrap](https://www.w3.org/TR/2018/CR-css-flexbox-1-20181119/#flex-wrap-property) property.
+ */
+export type FlexWrap = "nowrap" | "wrap" | "wrap-reverse";
+/**
+ * A value for the [align-content](https://www.w3.org/TR/css-align-3/#propdef-align-content) property.
+ */
+export type AlignContent =
+  | {
+      type: "normal";
+    }
+  | {
+      type: "baseline-position";
+      value: BaselinePosition;
+    }
+  | {
+      type: "content-distribution";
+      value: ContentDistribution;
+    }
+  | {
+      /**
+       * An overflow alignment mode.
+       */
+      overflow?: OverflowPosition | null;
+      type: "content-position";
+      /**
+       * A content position keyword.
+       */
+      value: ContentPosition;
+    };
+/**
+ * A [`<baseline-position>`](https://www.w3.org/TR/css-align-3/#typedef-baseline-position) value, as used in the alignment properties.
+ */
+export type BaselinePosition = "first" | "last";
+/**
+ * A [`<content-distribution>`](https://www.w3.org/TR/css-align-3/#typedef-content-distribution) value.
+ */
+export type ContentDistribution = "space-between" | "space-around" | "space-evenly" | "stretch";
+/**
+ * An [`<overflow-position>`](https://www.w3.org/TR/css-align-3/#typedef-overflow-position) value.
+ */
+export type OverflowPosition = "safe" | "unsafe";
+/**
+ * A [`<content-position>`](https://www.w3.org/TR/css-align-3/#typedef-content-position) value.
+ */
+export type ContentPosition = "center" | "start" | "end" | "flex-start" | "flex-end";
+/**
+ * A value for the [justify-content](https://www.w3.org/TR/css-align-3/#propdef-justify-content) property.
+ */
+export type JustifyContent =
+  | {
+      type: "normal";
+    }
+  | {
+      type: "content-distribution";
+      value: ContentDistribution;
+    }
+  | {
+      /**
+       * An overflow alignment mode.
+       */
+      overflow?: OverflowPosition | null;
+      type: "content-position";
+      /**
+       * A content position keyword.
+       */
+      value: ContentPosition;
+    }
+  | {
+      /**
+       * An overflow alignment mode.
+       */
+      overflow?: OverflowPosition | null;
+      type: "left";
+    }
+  | {
+      /**
+       * An overflow alignment mode.
+       */
+      overflow?: OverflowPosition | null;
+      type: "right";
+    };
+/**
+ * A value for the [align-self](https://www.w3.org/TR/css-align-3/#align-self-property) property.
+ */
+export type AlignSelf =
+  | {
+      type: "auto";
+    }
+  | {
+      type: "normal";
+    }
+  | {
+      type: "stretch";
+    }
+  | {
+      type: "baseline-position";
+      value: BaselinePosition;
+    }
+  | {
+      /**
+       * An overflow alignment mode.
+       */
+      overflow?: OverflowPosition | null;
+      type: "self-position";
+      /**
+       * A self position keyword.
+       */
+      value: SelfPosition;
+    };
+/**
+ * A [`<self-position>`](https://www.w3.org/TR/css-align-3/#typedef-self-position) value.
+ */
+export type SelfPosition = "center" | "start" | "end" | "self-start" | "self-end" | "flex-start" | "flex-end";
+/**
+ * A value for the [justify-self](https://www.w3.org/TR/css-align-3/#justify-self-property) property.
+ */
+export type JustifySelf =
+  | {
+      type: "auto";
+    }
+  | {
+      type: "normal";
+    }
+  | {
+      type: "stretch";
+    }
+  | {
+      type: "baseline-position";
+      value: BaselinePosition;
+    }
+  | {
+      /**
+       * An overflow alignment mode.
+       */
+      overflow?: OverflowPosition | null;
+      type: "self-position";
+      /**
+       * A self position keyword.
+       */
+      value: SelfPosition;
+    }
+  | {
+      /**
+       * An overflow alignment mode.
+       */
+      overflow?: OverflowPosition | null;
+      type: "left";
+    }
+  | {
+      /**
+       * An overflow alignment mode.
+       */
+      overflow?: OverflowPosition | null;
+      type: "right";
+    };
+/**
+ * A value for the [align-items](https://www.w3.org/TR/css-align-3/#align-items-property) property.
+ */
+export type AlignItems =
+  | {
+      type: "normal";
+    }
+  | {
+      type: "stretch";
+    }
+  | {
+      type: "baseline-position";
+      value: BaselinePosition;
+    }
+  | {
+      /**
+       * An overflow alignment mode.
+       */
+      overflow?: OverflowPosition | null;
+      type: "self-position";
+      /**
+       * A self position keyword.
+       */
+      value: SelfPosition;
+    };
+/**
+ * A value for the [justify-items](https://www.w3.org/TR/css-align-3/#justify-items-property) property.
+ */
+export type JustifyItems =
+  | {
+      type: "normal";
+    }
+  | {
+      type: "stretch";
+    }
+  | {
+      type: "baseline-position";
+      value: BaselinePosition;
+    }
+  | {
+      /**
+       * An overflow alignment mode.
+       */
+      overflow?: OverflowPosition | null;
+      type: "self-position";
+      /**
+       * A self position keyword.
+       */
+      value: SelfPosition;
+    }
+  | {
+      /**
+       * An overflow alignment mode.
+       */
+      overflow?: OverflowPosition | null;
+      type: "left";
+    }
+  | {
+      /**
+       * An overflow alignment mode.
+       */
+      overflow?: OverflowPosition | null;
+      type: "right";
+    }
+  | {
+      type: "legacy";
+      value: LegacyJustify;
+    };
+/**
+ * A legacy justification keyword, as used in the `justify-items` property.
+ */
+export type LegacyJustify = "left" | "right" | "center";
+/**
+ * A [gap](https://www.w3.org/TR/css-align-3/#column-row-gap) value, as used in the `column-gap` and `row-gap` properties.
+ */
+export type GapValue =
+  | {
+      type: "normal";
+    }
+  | {
+      type: "length-percentage";
+      value: DimensionPercentageFor_LengthValue;
+    };
+/**
+ * A value for the legacy (prefixed) [box-orient](https://www.w3.org/TR/2009/WD-css3-flexbox-20090723/#orientation) property. Partially equivalent to `flex-direction` in the standard syntax.
+ */
+export type BoxOrient = "horizontal" | "vertical" | "inline-axis" | "block-axis";
+/**
+ * A value for the legacy (prefixed) [box-direction](https://www.w3.org/TR/2009/WD-css3-flexbox-20090723/#displayorder) property. Partially equivalent to the `flex-direction` property in the standard syntax.
+ */
+export type BoxDirection = "normal" | "reverse";
+/**
+ * A value for the legacy (prefixed) [box-align](https://www.w3.org/TR/2009/WD-css3-flexbox-20090723/#alignment) property. Equivalent to the `align-items` property in the standard syntax.
+ */
+export type BoxAlign = "start" | "end" | "center" | "baseline" | "stretch";
+/**
+ * A value for the legacy (prefixed) [box-pack](https://www.w3.org/TR/2009/WD-css3-flexbox-20090723/#packing) property. Equivalent to the `justify-content` property in the standard syntax.
+ */
+export type BoxPack = "start" | "end" | "center" | "justify";
+/**
+ * A value for the legacy (prefixed) [box-lines](https://www.w3.org/TR/2009/WD-css3-flexbox-20090723/#multiple) property. Equivalent to the `flex-wrap` property in the standard syntax.
+ */
+export type BoxLines = "single" | "multiple";
+/**
+ * A value for the legacy (prefixed) [flex-pack](https://www.w3.org/TR/2012/WD-css3-flexbox-20120322/#flex-pack) property. Equivalent to the `justify-content` property in the standard syntax.
+ */
+export type FlexPack = "start" | "end" | "center" | "justify" | "distribute";
+/**
+ * A value for the legacy (prefixed) [flex-item-align](https://www.w3.org/TR/2012/WD-css3-flexbox-20120322/#flex-align) property. Equivalent to the `align-self` property in the standard syntax.
+ */
+export type FlexItemAlign = "auto" | "start" | "end" | "center" | "baseline" | "stretch";
+/**
+ * A value for the legacy (prefixed) [flex-line-pack](https://www.w3.org/TR/2012/WD-css3-flexbox-20120322/#flex-line-pack) property. Equivalent to the `align-content` property in the standard syntax.
+ */
+export type FlexLinePack = "start" | "end" | "center" | "justify" | "distribute" | "stretch";
+/**
+ * A [track sizing](https://drafts.csswg.org/css-grid-2/#track-sizing) value for the `grid-template-rows` and `grid-template-columns` properties.
+ */
+export type TrackSizing =
+  | {
+      type: "none";
+    }
+  | {
+      /**
+       * A list of grid track items.
+       */
+      items: TrackListItem[];
+      /**
+       * A list of line names.
+       */
+      lineNames: String[][];
+      type: "track-list";
+    };
+/**
+ * Either a track size or `repeat()` function.
+ *
+ * See [TrackList](TrackList).
+ */
+export type TrackListItem =
+  | {
+      type: "track-size";
+      value: TrackSize;
+    }
+  | {
+      type: "track-repeat";
+      value: TrackRepeat;
+    };
+/**
+ * A [`<track-size>`](https://drafts.csswg.org/css-grid-2/#typedef-track-size) value, as used in the `grid-template-rows` and `grid-template-columns` properties.
+ *
+ * See [TrackListItem](TrackListItem).
+ */
+export type TrackSize =
+  | {
+      type: "track-breadth";
+      value: TrackBreadth;
+    }
+  | {
+      /**
+       * The maximum value.
+       */
+      max: TrackBreadth;
+      /**
+       * The minimum value.
+       */
+      min: TrackBreadth;
+      type: "min-max";
+    }
+  | {
+      type: "fit-content";
+      value: DimensionPercentageFor_LengthValue;
+    };
+/**
+ * A [`<track-breadth>`](https://drafts.csswg.org/css-grid-2/#typedef-track-breadth) value.
+ *
+ * See [TrackSize](TrackSize).
+ */
+export type TrackBreadth =
+  | {
+      type: "length";
+      value: DimensionPercentageFor_LengthValue;
+    }
+  | {
+      type: "flex";
+      value: number;
+    }
+  | {
+      type: "min-content";
+    }
+  | {
+      type: "max-content";
+    }
+  | {
+      type: "auto";
+    };
+/**
+ * A [`<repeat-count>`](https://drafts.csswg.org/css-grid-2/#typedef-track-repeat) value, used in the `repeat()` function.
+ *
+ * See [TrackRepeat](TrackRepeat).
+ */
+export type RepeatCount =
+  | {
+      type: "number";
+      value: number;
+    }
+  | {
+      type: "auto-fill";
+    }
+  | {
+      type: "auto-fit";
+    };
+export type AutoFlowDirection = "row" | "column";
+/**
+ * A value for the [grid-template-areas](https://drafts.csswg.org/css-grid-2/#grid-template-areas-property) property.
+ */
+export type GridTemplateAreas =
+  | {
+      type: "none";
+    }
+  | {
+      /**
+       * A flattened list of grid area names. Unnamed areas specified by the `.` token are represented as `None`.
+       */
+      areas: (string | null)[];
+      /**
+       * The number of columns in the grid.
+       */
+      columns: number;
+      type: "areas";
+    };
+/**
+ * A [`<grid-line>`](https://drafts.csswg.org/css-grid-2/#typedef-grid-row-start-grid-line) value, used in the `grid-row-start`, `grid-row-end`, `grid-column-start`, and `grid-column-end` properties.
+ */
+export type GridLine =
+  | {
+      type: "auto";
+    }
+  | {
+      /**
+       * A grid area name.
+       */
+      name: String;
+      type: "area";
+    }
+  | {
+      /**
+       * A line number.
+       */
+      index: number;
+      /**
+       * A line name to filter by.
+       */
+      name?: String | null;
+      type: "line";
+    }
+  | {
+      /**
+       * A line number.
+       */
+      index: number;
+      /**
+       * A line name to filter by.
+       */
+      name?: String | null;
+      type: "span";
+    };
+/**
+ * A value for the [font-weight](https://www.w3.org/TR/css-fonts-4/#font-weight-prop) property.
+ */
+export type FontWeight =
+  | {
+      type: "absolute";
+      value: AbsoluteFontWeight;
+    }
+  | {
+      type: "bolder";
+    }
+  | {
+      type: "lighter";
+    };
+/**
+ * An [absolute font weight](https://www.w3.org/TR/css-fonts-4/#font-weight-absolute-values), as used in the `font-weight` property.
+ *
+ * See [FontWeight](FontWeight).
+ */
+export type AbsoluteFontWeight =
+  | {
+      type: "weight";
+      value: number;
+    }
+  | {
+      type: "normal";
+    }
+  | {
+      type: "bold";
+    };
+/**
+ * A value for the [font-size](https://www.w3.org/TR/css-fonts-4/#font-size-prop) property.
+ */
+export type FontSize =
+  | {
+      type: "length";
+      value: DimensionPercentageFor_LengthValue;
+    }
+  | {
+      type: "absolute";
+      value: AbsoluteFontSize;
+    }
+  | {
+      type: "relative";
+      value: RelativeFontSize;
+    };
+/**
+ * An [absolute font size](https://www.w3.org/TR/css-fonts-3/#absolute-size-value), as used in the `font-size` property.
+ *
+ * See [FontSize](FontSize).
+ */
+export type AbsoluteFontSize =
+  | "xx-small"
+  | "x-small"
+  | "small"
+  | "medium"
+  | "large"
+  | "x-large"
+  | "xx-large"
+  | "xxx-large";
+/**
+ * A [relative font size](https://www.w3.org/TR/css-fonts-3/#relative-size-value), as used in the `font-size` property.
+ *
+ * See [FontSize](FontSize).
+ */
+export type RelativeFontSize = "smaller" | "larger";
+/**
+ * A value for the [font-stretch](https://www.w3.org/TR/css-fonts-4/#font-stretch-prop) property.
+ */
+export type FontStretch =
+  | {
+      type: "keyword";
+      value: FontStretchKeyword;
+    }
+  | {
+      type: "percentage";
+      value: number;
+    };
+/**
+ * A [font stretch keyword](https://www.w3.org/TR/css-fonts-4/#font-stretch-prop), as used in the `font-stretch` property.
+ *
+ * See [FontStretch](FontStretch).
+ */
+export type FontStretchKeyword =
+  | "normal"
+  | "ultra-condensed"
+  | "extra-condensed"
+  | "condensed"
+  | "semi-condensed"
+  | "semi-expanded"
+  | "expanded"
+  | "extra-expanded"
+  | "ultra-expanded";
+/**
+ * A value for the [font-family](https://www.w3.org/TR/css-fonts-4/#font-family-prop) property.
+ */
+export type FontFamily = GenericFontFamily | String;
+/**
+ * A [generic font family](https://www.w3.org/TR/css-fonts-4/#generic-font-families) name, as used in the `font-family` property.
+ *
+ * See [FontFamily](FontFamily).
+ */
+export type GenericFontFamily =
+  | "serif"
+  | "sans-serif"
+  | "cursive"
+  | "fantasy"
+  | "monospace"
+  | "system-ui"
+  | "emoji"
+  | "math"
+  | "fangsong"
+  | "ui-serif"
+  | "ui-sans-serif"
+  | "ui-monospace"
+  | "ui-rounded"
+  | "initial"
+  | "inherit"
+  | "unset"
+  | "default"
+  | "revert"
+  | "revert-layer";
+/**
+ * A value for the [font-style](https://www.w3.org/TR/css-fonts-4/#font-style-prop) property.
+ */
+export type FontStyle =
+  | {
+      type: "normal";
+    }
+  | {
+      type: "italic";
+    }
+  | {
+      type: "oblique";
+      value: Angle;
+    };
+/**
+ * A value for the [font-variant-caps](https://www.w3.org/TR/css-fonts-4/#font-variant-caps-prop) property.
+ */
+export type FontVariantCaps =
+  | "normal"
+  | "small-caps"
+  | "all-small-caps"
+  | "petite-caps"
+  | "all-petite-caps"
+  | "unicase"
+  | "titling-caps";
+/**
+ * A value for the [line-height](https://www.w3.org/TR/2020/WD-css-inline-3-20200827/#propdef-line-height) property.
+ */
+export type LineHeight =
+  | {
+      type: "normal";
+    }
+  | {
+      type: "number";
+      value: number;
+    }
+  | {
+      type: "length";
+      value: DimensionPercentageFor_LengthValue;
+    };
+/**
+ * A value for the [vertical align](https://drafts.csswg.org/css2/#propdef-vertical-align) property.
+ */
+export type VerticalAlign =
+  | {
+      type: "keyword";
+      value: VerticalAlignKeyword;
+    }
+  | {
+      type: "length";
+      value: DimensionPercentageFor_LengthValue;
+    };
+/**
+ * A keyword for the [vertical align](https://drafts.csswg.org/css2/#propdef-vertical-align) property.
+ */
+export type VerticalAlignKeyword =
+  | "baseline"
+  | "sub"
+  | "super"
+  | "top"
+  | "text-top"
+  | "middle"
+  | "bottom"
+  | "text-bottom";
+/**
+ * A CSS [easing function](https://www.w3.org/TR/css-easing-1/#easing-functions).
+ */
+export type EasingFunction =
+  | {
+      type: "linear";
+    }
+  | {
+      type: "ease";
+    }
+  | {
+      type: "ease-in";
+    }
+  | {
+      type: "ease-out";
+    }
+  | {
+      type: "ease-in-out";
+    }
+  | {
+      type: "cubic-bezier";
+      /**
+       * The x-position of the first point in the curve.
+       */
+      x1: number;
+      /**
+       * The x-position of the second point in the curve.
+       */
+      x2: number;
+      /**
+       * The y-position of the first point in the curve.
+       */
+      y1: number;
+      /**
+       * The y-position of the second point in the curve.
+       */
+      y2: number;
+    }
+  | {
+      /**
+       * The number of intervals in the function.
+       */
+      count: number;
+      /**
+       * The step position.
+       */
+      position?: StepPosition;
+      type: "steps";
+    };
+/**
+ * A [step position](https://www.w3.org/TR/css-easing-1/#step-position), used within the `steps()` function.
+ */
+export type StepPosition =
+  | {
+      type: "start";
+    }
+  | {
+      type: "end";
+    }
+  | {
+      type: "jump-none";
+    }
+  | {
+      type: "jump-both";
+    };
+/**
+ * A value for the [animation-iteration-count](https://drafts.csswg.org/css-animations/#animation-iteration-count) property.
+ */
+export type AnimationIterationCount =
+  | {
+      type: "number";
+      value: number;
+    }
+  | {
+      type: "infinite";
+    };
+/**
+ * A value for the [animation-direction](https://drafts.csswg.org/css-animations/#animation-direction) property.
+ */
+export type AnimationDirection = "normal" | "reverse" | "alternate" | "alternate-reverse";
+/**
+ * A value for the [animation-play-state](https://drafts.csswg.org/css-animations/#animation-play-state) property.
+ */
+export type AnimationPlayState = "running" | "paused";
+/**
+ * A value for the [animation-fill-mode](https://drafts.csswg.org/css-animations/#animation-fill-mode) property.
+ */
+export type AnimationFillMode = "none" | "forwards" | "backwards" | "both";
+/**
+ * A value for the [animation-composition](https://drafts.csswg.org/css-animations-2/#animation-composition) property.
+ */
+export type AnimationComposition = "replace" | "add" | "accumulate";
+/**
+ * A value for the [animation-timeline](https://drafts.csswg.org/css-animations-2/#animation-timeline) property.
+ */
+export type AnimationTimeline =
+  | {
+      type: "auto";
+    }
+  | {
+      type: "none";
+    }
+  | {
+      type: "dashed-ident";
+      value: String;
+    }
+  | {
+      type: "scroll";
+      value: ScrollTimeline;
+    }
+  | {
+      type: "view";
+      value: ViewTimeline;
+    };
+/**
+ * A scroll axis, used in the `scroll()` function.
+ */
+export type ScrollAxis = "block" | "inline" | "x" | "y";
+/**
+ * A scroller, used in the `scroll()` function.
+ */
+export type Scroller = "root" | "nearest" | "self";
+/**
+ * A generic value that represents a value with two components, e.g. a border radius.
+ *
+ * When serialized, only a single component will be written if both are equal.
+ *
+ * @minItems 2
+ * @maxItems 2
+ */
+export type Size2DFor_LengthPercentageOrAuto = [LengthPercentageOrAuto, LengthPercentageOrAuto];
+/**
+ * A value for the [animation-range-start](https://drafts.csswg.org/scroll-animations/#animation-range-start) property.
+ */
+export type AnimationRangeStart = AnimationAttachmentRange;
+/**
+ * A value for the [animation-range-start](https://drafts.csswg.org/scroll-animations/#animation-range-start) or [animation-range-end](https://drafts.csswg.org/scroll-animations/#animation-range-end) property.
+ */
+export type AnimationAttachmentRange =
+  "normal" | DimensionPercentageFor_LengthValue | {
+    /**
+     * The name of the timeline range.
+     */
+    name: TimelineRangeName;
+    /**
+     * The offset from the start of the named timeline range.
+     */
+    offset: DimensionPercentageFor_LengthValue;
+  };
+/**
+ * A [view progress timeline range](https://drafts.csswg.org/scroll-animations/#view-timelines-ranges)
+ */
+export type TimelineRangeName = "cover" | "contain" | "entry" | "exit" | "entry-crossing" | "exit-crossing";
+/**
+ * A value for the [animation-range-end](https://drafts.csswg.org/scroll-animations/#animation-range-end) property.
+ */
+export type AnimationRangeEnd = AnimationAttachmentRange;
+/**
+ * An individual [transform function](https://www.w3.org/TR/2019/CR-css-transforms-1-20190214/#two-d-transform-functions).
+ */
+export type Transform =
+  | {
+      type: "translate";
+      /**
+       * @minItems 2
+       * @maxItems 2
+       */
+      value: [DimensionPercentageFor_LengthValue, DimensionPercentageFor_LengthValue];
+    }
+  | {
+      type: "translateX";
+      value: DimensionPercentageFor_LengthValue;
+    }
+  | {
+      type: "translateY";
+      value: DimensionPercentageFor_LengthValue;
+    }
+  | {
+      type: "translateZ";
+      value: Length;
+    }
+  | {
+      type: "translate3d";
+      /**
+       * @minItems 3
+       * @maxItems 3
+       */
+      value: [DimensionPercentageFor_LengthValue, DimensionPercentageFor_LengthValue, Length];
+    }
+  | {
+      type: "scale";
+      /**
+       * @minItems 2
+       * @maxItems 2
+       */
+      value: [NumberOrPercentage, NumberOrPercentage];
+    }
+  | {
+      type: "scaleX";
+      value: NumberOrPercentage;
+    }
+  | {
+      type: "scaleY";
+      value: NumberOrPercentage;
+    }
+  | {
+      type: "scaleZ";
+      value: NumberOrPercentage;
+    }
+  | {
+      type: "scale3d";
+      /**
+       * @minItems 3
+       * @maxItems 3
+       */
+      value: [NumberOrPercentage, NumberOrPercentage, NumberOrPercentage];
+    }
+  | {
+      type: "rotate";
+      value: Angle;
+    }
+  | {
+      type: "rotateX";
+      value: Angle;
+    }
+  | {
+      type: "rotateY";
+      value: Angle;
+    }
+  | {
+      type: "rotateZ";
+      value: Angle;
+    }
+  | {
+      type: "rotate3d";
+      /**
+       * @minItems 4
+       * @maxItems 4
+       */
+      value: [number, number, number, Angle];
+    }
+  | {
+      type: "skew";
+      /**
+       * @minItems 2
+       * @maxItems 2
+       */
+      value: [Angle, Angle];
+    }
+  | {
+      type: "skewX";
+      value: Angle;
+    }
+  | {
+      type: "skewY";
+      value: Angle;
+    }
+  | {
+      type: "perspective";
+      value: Length;
+    }
+  | {
+      type: "matrix";
+      value: MatrixForFloat;
+    }
+  | {
+      type: "matrix3d";
+      value: Matrix3DForFloat;
+    };
+/**
+ * A value for the [transform-style](https://drafts.csswg.org/css-transforms-2/#transform-style-property) property.
+ */
+export type TransformStyle = "flat" | "preserve3d";
+/**
+ * A value for the [transform-box](https://drafts.csswg.org/css-transforms-1/#transform-box) property.
+ */
+export type TransformBox = "content-box" | "border-box" | "fill-box" | "stroke-box" | "view-box";
+/**
+ * A value for the [backface-visibility](https://drafts.csswg.org/css-transforms-2/#backface-visibility-property) property.
+ */
+export type BackfaceVisibility = "visible" | "hidden";
+/**
+ * A value for the [perspective](https://drafts.csswg.org/css-transforms-2/#perspective-property) property.
+ */
+export type Perspective =
+  | {
+      type: "none";
+    }
+  | {
+      type: "length";
+      value: Length;
+    };
+/**
+ * A value for the [translate](https://drafts.csswg.org/css-transforms-2/#propdef-translate) property.
+ */
+export type Translate =
+  | "none"
+  | {
+    /**
+     * The x translation.
+     */
+    x: DimensionPercentageFor_LengthValue;
+    /**
+     * The y translation.
+     */
+    y: DimensionPercentageFor_LengthValue;
+    /**
+     * The z translation.
+     */
+    z: Length;
+  };
+/**
+ * A value for the [scale](https://drafts.csswg.org/css-transforms-2/#propdef-scale) property.
+ */
+export type Scale =
+  | "none"
+  | {
+    /**
+     * Scale on the x axis.
+     */
+    x: NumberOrPercentage;
+    /**
+     * Scale on the y axis.
+     */
+    y: NumberOrPercentage;
+    /**
+     * Scale on the z axis.
+     */
+    z: NumberOrPercentage;
+  };
+/**
+ * Defines how text case should be transformed in the [text-transform](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#text-transform-property) property.
+ */
+export type TextTransformCase = "none" | "uppercase" | "lowercase" | "capitalize";
+/**
+ * A value for the [white-space](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#white-space-property) property.
+ */
+export type WhiteSpace = "normal" | "pre" | "nowrap" | "pre-wrap" | "break-spaces" | "pre-line";
+/**
+ * A value for the [word-break](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#word-break-property) property.
+ */
+export type WordBreak = "normal" | "keep-all" | "break-all" | "break-word";
+/**
+ * A value for the [line-break](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#line-break-property) property.
+ */
+export type LineBreak = "auto" | "loose" | "normal" | "strict" | "anywhere";
+/**
+ * A value for the [hyphens](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#hyphenation) property.
+ */
+export type Hyphens = "none" | "manual" | "auto";
+/**
+ * A value for the [overflow-wrap](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#overflow-wrap-property) property.
+ */
+export type OverflowWrap = "normal" | "anywhere" | "break-word";
+/**
+ * A value for the [text-align](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#text-align-property) property.
+ */
+export type TextAlign = "start" | "end" | "left" | "right" | "center" | "justify" | "match-parent" | "justify-all";
+/**
+ * A value for the [text-align-last](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#text-align-last-property) property.
+ */
+export type TextAlignLast = "auto" | "start" | "end" | "left" | "right" | "center" | "justify" | "match-parent";
+/**
+ * A value for the [text-justify](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#text-justify-property) property.
+ */
+export type TextJustify = "auto" | "none" | "inter-word" | "inter-character";
+/**
+ * A value for the [word-spacing](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#word-spacing-property) and [letter-spacing](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#letter-spacing-property) properties.
+ */
+export type Spacing =
+  | {
+      type: "normal";
+    }
+  | {
+      type: "length";
+      value: Length;
+    };
+export type TextDecorationLine = ExclusiveTextDecorationLine | OtherTextDecorationLine[];
+export type ExclusiveTextDecorationLine = "none" | "spelling-error" | "grammar-error";
+export type OtherTextDecorationLine = "underline" | "overline" | "line-through" | "blink";
+/**
+ * A value for the [text-decoration-style](https://www.w3.org/TR/2020/WD-css-text-decor-4-20200506/#text-decoration-style-property) property.
+ */
+export type TextDecorationStyle = "solid" | "double" | "dotted" | "dashed" | "wavy";
+/**
+ * A value for the [text-decoration-thickness](https://www.w3.org/TR/2020/WD-css-text-decor-4-20200506/#text-decoration-width-property) property.
+ */
+export type TextDecorationThickness =
+  | {
+      type: "auto";
+    }
+  | {
+      type: "from-font";
+    }
+  | {
+      type: "length-percentage";
+      value: DimensionPercentageFor_LengthValue;
+    };
+/**
+ * A value for the [text-decoration-skip-ink](https://www.w3.org/TR/2020/WD-css-text-decor-4-20200506/#text-decoration-skip-ink-property) property.
+ */
+export type TextDecorationSkipInk = "auto" | "none" | "all";
+/**
+ * A value for the [text-emphasis-style](https://www.w3.org/TR/2020/WD-css-text-decor-4-20200506/#text-emphasis-style-property) property.
+ */
+export type TextEmphasisStyle =
+  | {
+      type: "none";
+    }
+  | {
+      /**
+       * The fill mode for the marks.
+       */
+      fill: TextEmphasisFillMode;
+      /**
+       * The shape of the marks.
+       */
+      shape?: TextEmphasisShape | null;
+      type: "keyword";
+    }
+  | {
+      type: "string";
+      value: String;
+    };
+/**
+ * A keyword for the [text-emphasis-style](https://www.w3.org/TR/2020/WD-css-text-decor-4-20200506/#text-emphasis-style-property) property.
+ *
+ * See [TextEmphasisStyle](TextEmphasisStyle).
+ */
+export type TextEmphasisFillMode = "filled" | "open";
+/**
+ * A text emphasis shape for the [text-emphasis-style](https://www.w3.org/TR/2020/WD-css-text-decor-4-20200506/#text-emphasis-style-property) property.
+ *
+ * See [TextEmphasisStyle](TextEmphasisStyle).
+ */
+export type TextEmphasisShape = "dot" | "circle" | "double-circle" | "triangle" | "sesame";
+/**
+ * A horizontal position keyword for the [text-emphasis-position](https://www.w3.org/TR/2020/WD-css-text-decor-4-20200506/#text-emphasis-position-property) property.
+ *
+ * See [TextEmphasisPosition](TextEmphasisPosition).
+ */
+export type TextEmphasisPositionHorizontal = "left" | "right";
+/**
+ * A vertical position keyword for the [text-emphasis-position](https://www.w3.org/TR/2020/WD-css-text-decor-4-20200506/#text-emphasis-position-property) property.
+ *
+ * See [TextEmphasisPosition](TextEmphasisPosition).
+ */
+export type TextEmphasisPositionVertical = "over" | "under";
+/**
+ * A value for the [text-size-adjust](https://w3c.github.io/csswg-drafts/css-size-adjust/#adjustment-control) property.
+ */
+export type TextSizeAdjust =
+  | {
+      type: "auto";
+    }
+  | {
+      type: "none";
+    }
+  | {
+      type: "percentage";
+      value: number;
+    };
+/**
+ * A value for the [direction](https://drafts.csswg.org/css-writing-modes-3/#direction) property.
+ */
+export type Direction2 = "ltr" | "rtl";
+/**
+ * A value for the [unicode-bidi](https://drafts.csswg.org/css-writing-modes-3/#unicode-bidi) property.
+ */
+export type UnicodeBidi = "normal" | "embed" | "isolate" | "bidi-override" | "isolate-override" | "plaintext";
+/**
+ * A value for the [box-decoration-break](https://www.w3.org/TR/css-break-3/#break-decoration) property.
+ */
+export type BoxDecorationBreak = "slice" | "clone";
+/**
+ * A value for the [resize](https://www.w3.org/TR/2021/WD-css-ui-4-20210316/#resize) property.
+ */
+export type Resize = "none" | "both" | "horizontal" | "vertical" | "block" | "inline";
+/**
+ * A pre-defined [cursor](https://www.w3.org/TR/2021/WD-css-ui-4-20210316/#cursor) value, used in the `cursor` property.
+ *
+ * See [Cursor](Cursor).
+ */
+export type CursorKeyword =
+  | "auto"
+  | "default"
+  | "none"
+  | "context-menu"
+  | "help"
+  | "pointer"
+  | "progress"
+  | "wait"
+  | "cell"
+  | "crosshair"
+  | "text"
+  | "vertical-text"
+  | "alias"
+  | "copy"
+  | "move"
+  | "no-drop"
+  | "not-allowed"
+  | "grab"
+  | "grabbing"
+  | "e-resize"
+  | "n-resize"
+  | "ne-resize"
+  | "nw-resize"
+  | "s-resize"
+  | "se-resize"
+  | "sw-resize"
+  | "w-resize"
+  | "ew-resize"
+  | "ns-resize"
+  | "nesw-resize"
+  | "nwse-resize"
+  | "col-resize"
+  | "row-resize"
+  | "all-scroll"
+  | "zoom-in"
+  | "zoom-out";
+/**
+ * A value for the [caret-color](https://www.w3.org/TR/2021/WD-css-ui-4-20210316/#caret-color) property.
+ */
+export type ColorOrAuto =
+  | {
+      type: "auto";
+    }
+  | {
+      type: "color";
+      value: CssColor;
+    };
+/**
+ * A value for the [caret-shape](https://www.w3.org/TR/2021/WD-css-ui-4-20210316/#caret-shape) property.
+ */
+export type CaretShape = "auto" | "bar" | "block" | "underscore";
+/**
+ * A value for the [user-select](https://www.w3.org/TR/2021/WD-css-ui-4-20210316/#content-selection) property.
+ */
+export type UserSelect = "auto" | "text" | "none" | "contain" | "all";
+export type Appearance = string;
+/**
+ * A value for the [list-style-type](https://www.w3.org/TR/2020/WD-css-lists-3-20201117/#text-markers) property.
+ */
+export type ListStyleType =
+  | {
+      type: "none";
+    }
+  | {
+      type: "string";
+      value: String;
+    }
+  | {
+      type: "counter-style";
+      value: CounterStyle;
+    };
+/**
+ * A [counter-style](https://www.w3.org/TR/css-counter-styles-3/#typedef-counter-style) name.
+ */
+export type CounterStyle =
+  | {
+      type: "predefined";
+      value: PredefinedCounterStyle;
+    }
+  | {
+      type: "name";
+      value: String;
+    }
+  | {
+      /**
+       * The symbols.
+       */
+      symbols: Symbol[];
+      /**
+       * The counter system.
+       */
+      system?: SymbolsType & string;
+      type: "symbols";
+    };
+/**
+ * A [predefined counter](https://www.w3.org/TR/css-counter-styles-3/#predefined-counters) style.
+ */
+export type PredefinedCounterStyle =
+  | "decimal"
+  | "decimal-leading-zero"
+  | "arabic-indic"
+  | "armenian"
+  | "upper-armenian"
+  | "lower-armenian"
+  | "bengali"
+  | "cambodian"
+  | "khmer"
+  | "cjk-decimal"
+  | "devanagari"
+  | "georgian"
+  | "gujarati"
+  | "gurmukhi"
+  | "hebrew"
+  | "kannada"
+  | "lao"
+  | "malayalam"
+  | "mongolian"
+  | "myanmar"
+  | "oriya"
+  | "persian"
+  | "lower-roman"
+  | "upper-roman"
+  | "tamil"
+  | "telugu"
+  | "thai"
+  | "tibetan"
+  | "lower-alpha"
+  | "lower-latin"
+  | "upper-alpha"
+  | "upper-latin"
+  | "lower-greek"
+  | "hiragana"
+  | "hiragana-iroha"
+  | "katakana"
+  | "katakana-iroha"
+  | "disc"
+  | "circle"
+  | "square"
+  | "disclosure-open"
+  | "disclosure-closed"
+  | "cjk-earthly-branch"
+  | "cjk-heavenly-stem"
+  | "japanese-informal"
+  | "japanese-formal"
+  | "korean-hangul-formal"
+  | "korean-hanja-informal"
+  | "korean-hanja-formal"
+  | "simp-chinese-informal"
+  | "simp-chinese-formal"
+  | "trad-chinese-informal"
+  | "trad-chinese-formal"
+  | "ethiopic-numeric";
+/**
+ * A single [symbol](https://www.w3.org/TR/css-counter-styles-3/#funcdef-symbols) as used in the `symbols()` function.
+ *
+ * See [CounterStyle](CounterStyle).
+ */
+export type Symbol =
+  | {
+      type: "string";
+      value: String;
+    }
+  | {
+      type: "image";
+      value: Image;
+    };
+/**
+ * A [`<symbols-type>`](https://www.w3.org/TR/css-counter-styles-3/#typedef-symbols-type) value, as used in the `symbols()` function.
+ *
+ * See [CounterStyle](CounterStyle).
+ */
+export type SymbolsType = "cyclic" | "numeric" | "alphabetic" | "symbolic" | "fixed";
+/**
+ * A value for the [list-style-position](https://www.w3.org/TR/2020/WD-css-lists-3-20201117/#list-style-position-property) property.
+ */
+export type ListStylePosition = "inside" | "outside";
+/**
+ * A value for the [marker-side](https://www.w3.org/TR/2020/WD-css-lists-3-20201117/#marker-side) property.
+ */
+export type MarkerSide = "match-self" | "match-parent";
+/**
+ * An SVG [`<paint>`](https://www.w3.org/TR/SVG2/painting.html#SpecifyingPaint) value used in the `fill` and `stroke` properties.
+ */
+export type SVGPaint =
+  | {
+      /**
+       * A fallback to be used used in case the paint server cannot be resolved.
+       */
+      fallback?: SVGPaintFallback | null;
+      type: "url";
+      /**
+       * The url of the paint server.
+       */
+      url: Url;
+    }
+  | {
+      type: "color";
+      value: CssColor;
+    }
+  | {
+      type: "context-fill";
+    }
+  | {
+      type: "context-stroke";
+    }
+  | {
+      type: "none";
+    };
+/**
+ * A fallback for an SVG paint in case a paint server `url()` cannot be resolved.
+ *
+ * See [SVGPaint](SVGPaint).
+ */
+export type SVGPaintFallback =
+  | {
+      type: "none";
+    }
+  | {
+      type: "color";
+      value: CssColor;
+    };
+/**
+ * A [`<fill-rule>`](https://www.w3.org/TR/css-shapes-1/#typedef-fill-rule) used to determine the interior of a `polygon()` shape.
+ *
+ * See [Polygon](Polygon).
+ */
+export type FillRule = "nonzero" | "evenodd";
+/**
+ * A value for the [stroke-linecap](https://www.w3.org/TR/SVG2/painting.html#LineCaps) property.
+ */
+export type StrokeLinecap = "butt" | "round" | "square";
+/**
+ * A value for the [stroke-linejoin](https://www.w3.org/TR/SVG2/painting.html#LineJoin) property.
+ */
+export type StrokeLinejoin = "miter" | "miter-clip" | "round" | "bevel" | "arcs";
+/**
+ * A value for the [stroke-dasharray](https://www.w3.org/TR/SVG2/painting.html#StrokeDashing) property.
+ */
+export type StrokeDasharray =
+  | {
+      type: "none";
+    }
+  | {
+      type: "values";
+      value: DimensionPercentageFor_LengthValue[];
+    };
+/**
+ * A value for the [marker](https://www.w3.org/TR/SVG2/painting.html#VertexMarkerProperties) properties.
+ */
+export type Marker =
+  | {
+      type: "none";
+    }
+  | {
+      type: "url";
+      value: Url;
+    };
+/**
+ * A value for the [color-interpolation](https://www.w3.org/TR/SVG2/painting.html#ColorInterpolation) property.
+ */
+export type ColorInterpolation = "auto" | "srgb" | "linearrgb";
+/**
+ * A value for the [color-rendering](https://www.w3.org/TR/SVG2/painting.html#ColorRendering) property.
+ */
+export type ColorRendering = "auto" | "optimizespeed" | "optimizequality";
+/**
+ * A value for the [shape-rendering](https://www.w3.org/TR/SVG2/painting.html#ShapeRendering) property.
+ */
+export type ShapeRendering = "auto" | "optimizespeed" | "crispedges" | "geometricprecision";
+/**
+ * A value for the [text-rendering](https://www.w3.org/TR/SVG2/painting.html#TextRendering) property.
+ */
+export type TextRendering = "auto" | "optimizespeed" | "optimizelegibility" | "geometricprecision";
+/**
+ * A value for the [image-rendering](https://www.w3.org/TR/SVG2/painting.html#ImageRendering) property.
+ */
+export type ImageRendering = "auto" | "optimizespeed" | "optimizequality";
+/**
+ * A value for the [clip-path](https://www.w3.org/TR/css-masking-1/#the-clip-path) property.
+ */
+export type ClipPath =
+  | {
+      type: "none";
+    }
+  | {
+      type: "url";
+      value: Url;
+    }
+  | {
+      /**
+       * A reference box that the shape is positioned according to.
+       */
+      referenceBox: GeometryBox;
+      /**
+       * A basic shape.
+       */
+      shape: BasicShape;
+      type: "shape";
+    }
+  | {
+      type: "box";
+      value: GeometryBox;
+    };
+/**
+ * A [`<geometry-box>`](https://www.w3.org/TR/css-masking-1/#typedef-geometry-box) value as used in the `mask-clip` and `clip-path` properties.
+ */
+export type GeometryBox =
+  | "border-box"
+  | "padding-box"
+  | "content-box"
+  | "margin-box"
+  | "fill-box"
+  | "stroke-box"
+  | "view-box";
+/**
+ * A CSS [`<basic-shape>`](https://www.w3.org/TR/css-shapes-1/#basic-shape-functions) value.
+ */
+export type BasicShape =
+  | {
+      type: "inset";
+      value: InsetRect;
+    }
+  | {
+      type: "circle";
+      value: Circle2;
+    }
+  | {
+      type: "ellipse";
+      value: Ellipse2;
+    }
+  | {
+      type: "polygon";
+      value: Polygon;
+    };
+/**
+ * A generic value that represents a value for four sides of a box, e.g. border-width, margin, padding, etc.
+ *
+ * When serialized, as few components as possible are written when there are duplicate values.
+ *
+ * @minItems 4
+ * @maxItems 4
+ */
+export type RectFor_DimensionPercentageFor_LengthValue = [
+  DimensionPercentageFor_LengthValue,
+  DimensionPercentageFor_LengthValue,
+  DimensionPercentageFor_LengthValue,
+  DimensionPercentageFor_LengthValue
+];
+/**
+ * A [`<shape-radius>`](https://www.w3.org/TR/css-shapes-1/#typedef-shape-radius) value that defines the radius of a `circle()` or `ellipse()` shape.
+ */
+export type ShapeRadius =
+  | {
+      type: "length-percentage";
+      value: DimensionPercentageFor_LengthValue;
+    }
+  | {
+      type: "closest-side";
+    }
+  | {
+      type: "farthest-side";
+    };
+/**
+ * A value for the [mask-mode](https://www.w3.org/TR/css-masking-1/#the-mask-mode) property.
+ */
+export type MaskMode = "luminance" | "alpha" | "match-source";
+/**
+ * A value for the [mask-clip](https://www.w3.org/TR/css-masking-1/#the-mask-clip) property.
+ */
+export type MaskClip =
+  | {
+      type: "geometry-box";
+      value: GeometryBox;
+    }
+  | {
+      type: "no-clip";
+    };
+/**
+ * A value for the [mask-composite](https://www.w3.org/TR/css-masking-1/#the-mask-composite) property.
+ */
+export type MaskComposite = "add" | "subtract" | "intersect" | "exclude";
+/**
+ * A value for the [mask-type](https://www.w3.org/TR/css-masking-1/#the-mask-type) property.
+ */
+export type MaskType = "luminance" | "alpha";
+/**
+ * A value for the [mask-border-mode](https://www.w3.org/TR/css-masking-1/#the-mask-border-mode) property.
+ */
+export type MaskBorderMode = "luminance" | "alpha";
+/**
+ * A value for the [-webkit-mask-composite](https://developer.mozilla.org/en-US/docs/Web/CSS/-webkit-mask-composite) property.
+ *
+ * See also [MaskComposite](MaskComposite).
+ */
+export type WebKitMaskComposite =
+  | ("clear" | "copy" | "source-atop" | "destination-over" | "destination-in" | "destination-out" | "destination-atop")
+  | "source-over"
+  | "source-in"
+  | "source-out"
+  | "xor";
+/**
+ * A value for the [-webkit-mask-source-type](https://github.com/WebKit/WebKit/blob/6eece09a1c31e47489811edd003d1e36910e9fd3/Source/WebCore/css/CSSProperties.json#L6578-L6587) property.
+ *
+ * See also [MaskMode](MaskMode).
+ */
+export type WebKitMaskSourceType = "auto" | "luminance" | "alpha";
+/**
+ * A value for the [filter](https://drafts.fxtf.org/filter-effects-1/#FilterProperty) and [backdrop-filter](https://drafts.fxtf.org/filter-effects-2/#BackdropFilterProperty) properties.
+ */
+export type FilterList =
+  | {
+      type: "none";
+    }
+  | {
+      type: "filters";
+      value: Filter[];
+    };
+/**
+ * A [filter](https://drafts.fxtf.org/filter-effects-1/#filter-functions) function.
+ */
+export type Filter =
+  | {
+      type: "blur";
+      value: Length;
+    }
+  | {
+      type: "brightness";
+      value: NumberOrPercentage;
+    }
+  | {
+      type: "contrast";
+      value: NumberOrPercentage;
+    }
+  | {
+      type: "grayscale";
+      value: NumberOrPercentage;
+    }
+  | {
+      type: "hue-rotate";
+      value: Angle;
+    }
+  | {
+      type: "invert";
+      value: NumberOrPercentage;
+    }
+  | {
+      type: "opacity";
+      value: NumberOrPercentage;
+    }
+  | {
+      type: "saturate";
+      value: NumberOrPercentage;
+    }
+  | {
+      type: "sepia";
+      value: NumberOrPercentage;
+    }
+  | {
+      type: "drop-shadow";
+      value: DropShadow;
+    }
+  | {
+      type: "url";
+      value: Url;
+    };
+/**
+ * A value for the [z-index](https://drafts.csswg.org/css2/#z-index) property.
+ */
+export type ZIndex =
+  | {
+      type: "auto";
+    }
+  | {
+      type: "integer";
+      value: number;
+    };
+/**
+ * A value for the [container-type](https://drafts.csswg.org/css-contain-3/#container-type) property. Establishes the element as a query container for the purpose of container queries.
+ */
+export type ContainerType = "normal" | "inline-size" | "size";
+/**
+ * A value for the [container-name](https://drafts.csswg.org/css-contain-3/#container-name) property.
+ */
+export type ContainerNameList =
+  | {
+      type: "none";
+    }
+  | {
+      type: "names";
+      value: String[];
+    };
+/**
+ * A value for the [view-transition-name](https://drafts.csswg.org/css-view-transitions-1/#view-transition-name-prop) property.
+ */
+export type ViewTransitionName =
+  "none" | "auto" | String;
+/**
+ * The `none` keyword, or a space-separated list of custom idents.
+ */
+export type NoneOrCustomIdentList =
+  "none" | String[];
+/**
+ * A value for the [view-transition-group](https://drafts.csswg.org/css-view-transitions-2/#view-transition-group-prop) property.
+ */
+export type ViewTransitionGroup =
+  "normal" | "contain" | "nearest" | String;
+/**
+ * A [CSS-wide keyword](https://drafts.csswg.org/css-cascade-5/#defaulting-keywords).
+ */
+export type CSSWideKeyword = "initial" | "inherit" | "unset" | "revert" | "revert-layer";
+/**
+ * A CSS custom property name.
+ */
+export type CustomPropertyName = String | String;
+export type SelectorComponent =
+  | {
+      type: "combinator";
+      value: Combinator;
+    }
+  | {
+      type: "universal";
+    }
+  | (
+      | {
+          type: "namespace";
+          kind: "none";
+        }
+      | {
+          type: "namespace";
+          kind: "any";
+        }
+      | {
+          type: "namespace";
+          kind: "named";
+          prefix: string;
+        }
+    )
+  | {
+      name: string;
+      type: "type";
+    }
+  | {
+      name: string;
+      type: "id";
+    }
+  | {
+      name: string;
+      type: "class";
+    }
+  | {
+      name: string;
+      namespace?: NamespaceConstraint | null;
+      operation?: AttrOperation | null;
+      type: "attribute";
+    }
+  | ({
+      type: "pseudo-class";
+    } & (TSPseudoClass | PseudoClass))
+  | ({
+      type: "pseudo-element";
+    } & (BuiltinPseudoElement | PseudoElement))
+  | {
+      type: "nesting";
+    };
+export type Combinator =
+  | ("child" | "descendant" | "next-sibling" | "later-sibling")
+  | "pseudo-element"
+  | "slot-assignment"
+  | "part"
+  | "deep-descendant"
+  | "deep";
+export type NamespaceConstraint =
+  | {
+      type: "any";
+    }
+  | {
+      prefix: string;
+      type: "specific";
+      url: string;
+    };
+export type ParsedCaseSensitivity =
+  | "explicit-case-sensitive"
+  | "ascii-case-insensitive"
+  | "case-sensitive"
+  | "ascii-case-insensitive-if-in-html-element-in-html-document";
+export type AttrSelectorOperator = "equal" | "includes" | "dash-match" | "prefix" | "substring" | "suffix";
+export type TSPseudoClass =
+  | {
+      kind: "not";
+      selectors: Selector[];
+    }
+  | {
+      kind: "first-child";
+    }
+  | {
+      kind: "last-child";
+    }
+  | {
+      kind: "only-child";
+    }
+  | {
+      kind: "root";
+    }
+  | {
+      kind: "empty";
+    }
+  | {
+      kind: "scope";
+    }
+  | {
+      a: number;
+      b: number;
+      kind: "nth-child";
+      of?: Selector[] | null;
+    }
+  | {
+      a: number;
+      b: number;
+      kind: "nth-last-child";
+      of?: Selector[] | null;
+    }
+  | {
+      a: number;
+      b: number;
+      kind: "nth-col";
+    }
+  | {
+      a: number;
+      b: number;
+      kind: "nth-last-col";
+    }
+  | {
+      a: number;
+      b: number;
+      kind: "nth-of-type";
+    }
+  | {
+      a: number;
+      b: number;
+      kind: "nth-last-of-type";
+    }
+  | {
+      kind: "first-of-type";
+    }
+  | {
+      kind: "last-of-type";
+    }
+  | {
+      kind: "only-of-type";
+    }
+  | {
+      kind: "host";
+      selectors?: Selector | null;
+    }
+  | {
+      kind: "where";
+      selectors: Selector[];
+    }
+  | {
+      kind: "is";
+      selectors: Selector[];
+    }
+  | {
+      kind: "any";
+      selectors: Selector[];
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      kind: "has";
+      selectors: Selector[];
+    };
+/**
+ * A pseudo class.
+ */
+export type PseudoClass =
+  | {
+      kind: "lang";
+      /**
+       * A list of language codes.
+       */
+      languages: String[];
+    }
+  | {
+      /**
+       * A direction.
+       */
+      direction: Direction;
+      kind: "dir";
+    }
+  | {
+      kind: "hover";
+    }
+  | {
+      kind: "active";
+    }
+  | {
+      kind: "focus";
+    }
+  | {
+      kind: "focus-visible";
+    }
+  | {
+      kind: "focus-within";
+    }
+  | {
+      kind: "current";
+    }
+  | {
+      kind: "past";
+    }
+  | {
+      kind: "future";
+    }
+  | {
+      kind: "playing";
+    }
+  | {
+      kind: "paused";
+    }
+  | {
+      kind: "seeking";
+    }
+  | {
+      kind: "buffering";
+    }
+  | {
+      kind: "stalled";
+    }
+  | {
+      kind: "muted";
+    }
+  | {
+      kind: "volume-locked";
+    }
+  | {
+      kind: "fullscreen";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      kind: "open";
+    }
+  | {
+      kind: "closed";
+    }
+  | {
+      kind: "modal";
+    }
+  | {
+      kind: "picture-in-picture";
+    }
+  | {
+      kind: "popover-open";
+    }
+  | {
+      kind: "defined";
+    }
+  | {
+      kind: "any-link";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      kind: "link";
+    }
+  | {
+      kind: "local-link";
+    }
+  | {
+      kind: "target";
+    }
+  | {
+      kind: "target-within";
+    }
+  | {
+      kind: "visited";
+    }
+  | {
+      kind: "enabled";
+    }
+  | {
+      kind: "disabled";
+    }
+  | {
+      kind: "read-only";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      kind: "read-write";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      kind: "placeholder-shown";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      kind: "default";
+    }
+  | {
+      kind: "checked";
+    }
+  | {
+      kind: "indeterminate";
+    }
+  | {
+      kind: "blank";
+    }
+  | {
+      kind: "valid";
+    }
+  | {
+      kind: "invalid";
+    }
+  | {
+      kind: "in-range";
+    }
+  | {
+      kind: "out-of-range";
+    }
+  | {
+      kind: "required";
+    }
+  | {
+      kind: "optional";
+    }
+  | {
+      kind: "user-valid";
+    }
+  | {
+      kind: "user-invalid";
+    }
+  | {
+      kind: "autofill";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      kind: "active-view-transition";
+    }
+  | {
+      kind: "active-view-transition-type";
+      /**
+       * A view transition type.
+       */
+      type: String[];
+    }
+  | {
+      kind: "local";
+      /**
+       * A local selector.
+       */
+      selector: Selector;
+    }
+  | {
+      kind: "global";
+      /**
+       * A global selector.
+       */
+      selector: Selector;
+    }
+  | {
+      kind: "webkit-scrollbar";
+      value: WebKitScrollbarPseudoClass;
+    }
+  | {
+      kind: "custom";
+      /**
+       * The pseudo class name.
+       */
+      name: String;
+    }
+  | {
+      /**
+       * The arguments of the pseudo class function.
+       */
+      arguments: TokenOrValue[];
+      kind: "custom-function";
+      /**
+       * The pseudo class name.
+       */
+      name: String;
+    };
+/**
+ * The [:dir()](https://drafts.csswg.org/selectors-4/#the-dir-pseudo) pseudo class.
+ */
+export type Direction = "ltr" | "rtl";
+/**
+ * A [webkit scrollbar](https://webkit.org/blog/363/styling-scrollbars/) pseudo class.
+ */
+export type WebKitScrollbarPseudoClass =
+  | "horizontal"
+  | "vertical"
+  | "decrement"
+  | "increment"
+  | "start"
+  | "end"
+  | "double-button"
+  | "single-button"
+  | "no-button"
+  | "corner-present"
+  | "window-inactive";
+export type BuiltinPseudoElement =
+  | {
+      kind: "slotted";
+      selector: Selector;
+    }
+  | {
+      kind: "part";
+      names: string[];
+    };
+/**
+ * A pseudo element.
+ */
+export type PseudoElement =
+  | {
+      kind: "after";
+    }
+  | {
+      kind: "before";
+    }
+  | {
+      kind: "first-line";
+    }
+  | {
+      kind: "first-letter";
+    }
+  | {
+      kind: "details-content";
+    }
+  | {
+      kind: "target-text";
+    }
+  | {
+      kind: "selection";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      kind: "placeholder";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      kind: "marker";
+    }
+  | {
+      kind: "backdrop";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      kind: "file-selector-button";
+      vendorPrefix: VendorPrefix;
+    }
+  | {
+      kind: "webkit-scrollbar";
+      value: WebKitScrollbarPseudoElement;
+    }
+  | {
+      kind: "cue";
+    }
+  | {
+      kind: "cue-region";
+    }
+  | {
+      kind: "cue-function";
+      /**
+       * The selector argument.
+       */
+      selector: Selector;
+    }
+  | {
+      kind: "cue-region-function";
+      /**
+       * The selector argument.
+       */
+      selector: Selector;
+    }
+  | {
+      kind: "view-transition";
+    }
+  | {
+      kind: "view-transition-group";
+      /**
+       * A part name selector.
+       */
+      part: ViewTransitionPartSelector;
+    }
+  | {
+      kind: "view-transition-image-pair";
+      /**
+       * A part name selector.
+       */
+      part: ViewTransitionPartSelector;
+    }
+  | {
+      kind: "view-transition-old";
+      /**
+       * A part name selector.
+       */
+      part: ViewTransitionPartSelector;
+    }
+  | {
+      kind: "view-transition-new";
+      /**
+       * A part name selector.
+       */
+      part: ViewTransitionPartSelector;
+    }
+  | {
+      kind: "custom";
+      /**
+       * The name of the pseudo element.
+       */
+      name: String;
+    }
+  | {
+      /**
+       * The arguments of the pseudo element function.
+       */
+      arguments: TokenOrValue[];
+      kind: "custom-function";
+      /**
+       * The name of the pseudo element.
+       */
+      name: String;
+    };
+/**
+ * A [webkit scrollbar](https://webkit.org/blog/363/styling-scrollbars/) pseudo element.
+ */
+export type WebKitScrollbarPseudoElement =
+  | "scrollbar"
+  | "button"
+  | "track"
+  | "track-piece"
+  | "thumb"
+  | "corner"
+  | "resizer";
+export type ViewTransitionPartName = string;
+export type Selector = SelectorComponent[];
+export type SelectorList = Selector[];
+/**
+ * A [keyframe selector](https://drafts.csswg.org/css-animations/#typedef-keyframe-selector) within an `@keyframes` rule.
+ */
+export type KeyframeSelector =
+  | {
+      type: "percentage";
+      value: number;
+    }
+  | {
+      type: "from";
+    }
+  | {
+      type: "to";
+    }
+  | {
+      type: "timeline-range-percentage";
+      value: TimelineRangePercentage;
+    };
+/**
+ * KeyframesName
+ */
+export type KeyframesName =
+  | {
+      type: "ident";
+      value: String;
+    }
+  | {
+      type: "custom";
+      value: String;
+    };
+/**
+ * A property within an `@font-face` rule.
+ *
+ * See [FontFaceRule](FontFaceRule).
+ */
+export type FontFaceProperty =
+  | {
+      type: "source";
+      value: Source[];
+    }
+  | {
+      type: "font-family";
+      value: FontFamily;
+    }
+  | {
+      type: "font-style";
+      value: FontStyle2;
+    }
+  | {
+      type: "font-weight";
+      value: Size2DFor_FontWeight;
+    }
+  | {
+      type: "font-stretch";
+      value: Size2DFor_FontStretch;
+    }
+  | {
+      type: "unicode-range";
+      value: UnicodeRange[];
+    }
+  | {
+      type: "custom";
+      value: CustomProperty;
+    };
+/**
+ * A value for the [src](https://drafts.csswg.org/css-fonts/#src-desc) property in an `@font-face` rule.
+ */
+export type Source =
+  | {
+      type: "url";
+      value: UrlSource;
+    }
+  | {
+      type: "local";
+      value: FontFamily;
+    };
+/**
+ * A font format keyword in the `format()` function of the the [src](https://drafts.csswg.org/css-fonts/#src-desc) property of an `@font-face` rule.
+ */
+export type FontFormat =
+  | {
+      type: "woff";
+    }
+  | {
+      type: "woff2";
+    }
+  | {
+      type: "truetype";
+    }
+  | {
+      type: "opentype";
+    }
+  | {
+      type: "embedded-opentype";
+    }
+  | {
+      type: "collection";
+    }
+  | {
+      type: "svg";
+    }
+  | {
+      type: "string";
+      value: String;
+    };
+/**
+ * A font format keyword in the `format()` function of the the [src](https://drafts.csswg.org/css-fonts/#src-desc) property of an `@font-face` rule.
+ */
+export type FontTechnology =
+  | "features-opentype"
+  | "features-aat"
+  | "features-graphite"
+  | "color-colrv0"
+  | "color-colrv1"
+  | "color-svg"
+  | "color-sbix"
+  | "color-cbdt"
+  | "variations"
+  | "palettes"
+  | "incremental";
+/**
+ * A value for the [font-style](https://w3c.github.io/csswg-drafts/css-fonts/#descdef-font-face-font-style) descriptor in an `@font-face` rule.
+ */
+export type FontStyle2 =
+  | {
+      type: "normal";
+    }
+  | {
+      type: "italic";
+    }
+  | {
+      type: "oblique";
+      value: Size2DFor_Angle;
+    };
+/**
+ * A generic value that represents a value with two components, e.g. a border radius.
+ *
+ * When serialized, only a single component will be written if both are equal.
+ *
+ * @minItems 2
+ * @maxItems 2
+ */
+export type Size2DFor_Angle = [Angle, Angle];
+/**
+ * A generic value that represents a value with two components, e.g. a border radius.
+ *
+ * When serialized, only a single component will be written if both are equal.
+ *
+ * @minItems 2
+ * @maxItems 2
+ */
+export type Size2DFor_FontWeight = [FontWeight, FontWeight];
+/**
+ * A generic value that represents a value with two components, e.g. a border radius.
+ *
+ * When serialized, only a single component will be written if both are equal.
+ *
+ * @minItems 2
+ * @maxItems 2
+ */
+export type Size2DFor_FontStretch = [FontStretch, FontStretch];
+/**
+ * A property within an `@font-palette-values` rule.
+ *
+ * See [FontPaletteValuesRule](FontPaletteValuesRule).
+ */
+export type FontPaletteValuesProperty =
+  | {
+      type: "font-family";
+      value: FontFamily;
+    }
+  | {
+      type: "base-palette";
+      value: BasePalette;
+    }
+  | {
+      type: "override-colors";
+      value: OverrideColors[];
+    }
+  | {
+      type: "custom";
+      value: CustomProperty;
+    };
+/**
+ * A value for the [base-palette](https://drafts.csswg.org/css-fonts-4/#base-palette-desc) property in an `@font-palette-values` rule.
+ */
+export type BasePalette =
+  | {
+      type: "light";
+    }
+  | {
+      type: "dark";
+    }
+  | {
+      type: "integer";
+      value: number;
+    };
+/**
+ * The name of the `@font-feature-values` sub-rule. font-feature-value-type = <@stylistic> | <@historical-forms> | <@styleset> | <@character-variant> | <@swash> | <@ornaments> | <@annotation>
+ */
+export type FontFeatureSubruleType =
+  | "stylistic"
+  | "historical-forms"
+  | "styleset"
+  | "character-variant"
+  | "swash"
+  | "ornaments"
+  | "annotation";
+/**
+ * A [page margin box](https://www.w3.org/TR/css-page-3/#margin-boxes).
+ */
+export type PageMarginBox =
+  | "top-left-corner"
+  | "top-left"
+  | "top-center"
+  | "top-right"
+  | "top-right-corner"
+  | "left-top"
+  | "left-middle"
+  | "left-bottom"
+  | "right-top"
+  | "right-middle"
+  | "right-bottom"
+  | "bottom-left-corner"
+  | "bottom-left"
+  | "bottom-center"
+  | "bottom-right"
+  | "bottom-right-corner";
+/**
+ * A page pseudo class within an `@page` selector.
+ *
+ * See [PageSelector](PageSelector).
+ */
+export type PagePseudoClass = "left" | "right" | "first" | "last" | "blank";
+/**
+ * A parsed value for a [SyntaxComponent](SyntaxComponent).
+ */
+export type ParsedComponent =
+  | {
+      type: "length";
+      value: Length;
+    }
+  | {
+      type: "number";
+      value: number;
+    }
+  | {
+      type: "percentage";
+      value: number;
+    }
+  | {
+      type: "length-percentage";
+      value: DimensionPercentageFor_LengthValue;
+    }
+  | {
+      type: "color";
+      value: CssColor;
+    }
+  | {
+      type: "image";
+      value: Image;
+    }
+  | {
+      type: "url";
+      value: Url;
+    }
+  | {
+      type: "integer";
+      value: number;
+    }
+  | {
+      type: "angle";
+      value: Angle;
+    }
+  | {
+      type: "time";
+      value: Time;
+    }
+  | {
+      type: "resolution";
+      value: Resolution;
+    }
+  | {
+      type: "transform-function";
+      value: Transform;
+    }
+  | {
+      type: "transform-list";
+      value: Transform[];
+    }
+  | {
+      type: "custom-ident";
+      value: String;
+    }
+  | {
+      type: "literal";
+      value: String;
+    }
+  | {
+      type: "repeated";
+      value: {
+        /**
+         * The components to repeat.
+         */
+        components: ParsedComponent[];
+        /**
+         * A multiplier describing how the components repeat.
+         */
+        multiplier: Multiplier;
+      };
+    }
+  | {
+      type: "token-list";
+      value: TokenOrValue[];
+    };
+/**
+ * A [multiplier](https://drafts.css-houdini.org/css-properties-values-api/#multipliers) for a [SyntaxComponent](SyntaxComponent). Indicates whether and how the component may be repeated.
+ */
+export type Multiplier =
+  | {
+      type: "none";
+    }
+  | {
+      type: "space";
+    }
+  | {
+      type: "comma";
+    };
+/**
+ * A CSS [syntax string](https://drafts.css-houdini.org/css-properties-values-api/#syntax-strings) used to define the grammar for a registered custom property.
+ */
+export type SyntaxString =
+  | {
+      type: "components";
+      value: SyntaxComponent[];
+    }
+  | {
+      type: "universal";
+    };
+/**
+ * A [syntax component component name](https://drafts.css-houdini.org/css-properties-values-api/#supported-names).
+ */
+export type SyntaxComponentKind =
+  | {
+      type: "length";
+    }
+  | {
+      type: "number";
+    }
+  | {
+      type: "percentage";
+    }
+  | {
+      type: "length-percentage";
+    }
+  | {
+      type: "color";
+    }
+  | {
+      type: "image";
+    }
+  | {
+      type: "url";
+    }
+  | {
+      type: "integer";
+    }
+  | {
+      type: "angle";
+    }
+  | {
+      type: "time";
+    }
+  | {
+      type: "resolution";
+    }
+  | {
+      type: "transform-function";
+    }
+  | {
+      type: "transform-list";
+    }
+  | {
+      type: "custom-ident";
+    }
+  | {
+      type: "literal";
+      value: string;
+    };
+/**
+ * Represents a container condition.
+ */
+export type ContainerCondition<D = Declaration> = | {
+    type: "feature";
+    value: QueryFeatureFor_ContainerSizeFeatureId;
+  }
+| {
+    type: "not";
+    value: ContainerCondition<D>;
+  }
+| {
+    /**
+     * The conditions for the operator.
+     */
+    conditions: ContainerCondition<D>[];
+    /**
+     * The operator for the conditions.
+     */
+    operator: Operator;
+    type: "operation";
+  }
+| {
+    type: "style";
+    value: StyleQuery<D>;
+  };
+/**
+ * A generic media feature or container feature.
+ */
+export type QueryFeatureFor_ContainerSizeFeatureId =
+  | {
+      /**
+       * The name of the feature.
+       */
+      name: MediaFeatureNameFor_ContainerSizeFeatureId;
+      type: "plain";
+      /**
+       * The feature value.
+       */
+      value: MediaFeatureValue;
+    }
+  | {
+      /**
+       * The name of the feature.
+       */
+      name: MediaFeatureNameFor_ContainerSizeFeatureId;
+      type: "boolean";
+    }
+  | {
+      /**
+       * The name of the feature.
+       */
+      name: MediaFeatureNameFor_ContainerSizeFeatureId;
+      /**
+       * A comparator.
+       */
+      operator: MediaFeatureComparison;
+      type: "range";
+      /**
+       * The feature value.
+       */
+      value: MediaFeatureValue;
+    }
+  | {
+      /**
+       * The end value.
+       */
+      end: MediaFeatureValue;
+      /**
+       * A comparator for the end value.
+       */
+      endOperator: MediaFeatureComparison;
+      /**
+       * The name of the feature.
+       */
+      name: MediaFeatureNameFor_ContainerSizeFeatureId;
+      /**
+       * A start value.
+       */
+      start: MediaFeatureValue;
+      /**
+       * A comparator for the start value.
+       */
+      startOperator: MediaFeatureComparison;
+      type: "interval";
+    };
+/**
+ * A media feature name.
+ */
+export type MediaFeatureNameFor_ContainerSizeFeatureId = ContainerSizeFeatureId | String | String;
+/**
+ * A container query size feature identifier.
+ */
+export type ContainerSizeFeatureId = "width" | "height" | "inline-size" | "block-size" | "aspect-ratio" | "orientation";
+/**
+ * Represents a style query within a container condition.
+ */
+export type StyleQuery<D = Declaration> = | {
+    type: "declaration";
+    value: D;
+  }
+| {
+    type: "property";
+    value: PropertyId;
+  }
+| {
+    type: "not";
+    value: StyleQuery<D>;
+  }
+| {
+    /**
+     * The conditions for the operator.
+     */
+    conditions: StyleQuery<D>[];
+    /**
+     * The operator for the conditions.
+     */
+    operator: Operator;
+    type: "operation";
+  };
+/**
+ * A property within a `@view-transition` rule.
+ *
+ * See [ViewTransitionRule](ViewTransitionRule).
+ */
+export type ViewTransitionProperty =
+  | {
+      property: "navigation";
+      value: Navigation;
+    }
+  | {
+      property: "types";
+      value: NoneOrCustomIdentList;
+    }
+  | {
+      property: "custom";
+      value: CustomProperty;
+    };
+/**
+ * A value for the [navigation](https://drafts.csswg.org/css-view-transitions-2/#view-transition-navigation-descriptor) property in a `@view-transition` rule.
+ */
+export type Navigation = "none" | "auto";
+export type DefaultAtRule = null;
+
+/**
+ * A CSS style sheet, representing a `.css` file or inline `<style>` element.
+ *
+ * Style sheets can be parsed from a string, constructed from scratch, or created using a [Bundler](super::bundler::Bundler). Then, they can be minified and transformed for a set of target browsers, and serialied to a string.
+ *
+ * # Example
+ *
+ * ``` use lightningcss::stylesheet::{ StyleSheet, ParserOptions, MinifyOptions, PrinterOptions };
+ *
+ * // Parse a style sheet from a string. let mut stylesheet = StyleSheet::parse( r#" .foo { color: red; }
+ *
+ * .bar { color: red; } "#, ParserOptions::default() ).unwrap();
+ *
+ * // Minify the stylesheet. stylesheet.minify(MinifyOptions::default()).unwrap();
+ *
+ * // Serialize it to a string. let res = stylesheet.to_css(PrinterOptions::default()).unwrap(); assert_eq!(res.code, ".foo, .bar {\n  color: red;\n}\n"); ```
+ */
+export interface StyleSheet<D = Declaration, M = MediaQuery> {
+  /**
+   * The license comments that appeared at the start of the file.
+   */
+  licenseComments: String[];
+  /**
+   * A list of top-level rules within the style sheet.
+   */
+  rules: Rule<D, M>[];
+  /**
+   * The source map URL extracted from the original style sheet.
+   */
+  sourceMapUrls: (string | null)[];
+  /**
+   * A list of file names for all source files included within the style sheet. Sources are referenced by index in the `loc` property of each rule.
+   */
+  sources: string[];
+}
+/**
+ * A [@media](https://drafts.csswg.org/css-conditional-3/#at-media) rule.
+ */
+export interface MediaRule<D = Declaration, M = MediaQuery> {
+  /**
+   * The location of the rule in the source file.
+   */
+  loc: Location2;
+  /**
+   * The media query list.
+   */
+  query: MediaList<M>;
+  /**
+   * The rules within the `@media` rule.
+   */
+  rules: Rule<D, M>[];
+}
+/**
+ * A source location.
+ */
+export interface Location2 {
+  /**
+   * The column number within a line, starting at 1 for first the character of the line. Column numbers are counted in UTF-16 code units.
+   */
+  column: number;
+  /**
+   * The line number, starting at 0.
+   */
+  line: number;
+  /**
+   * The index of the source file within the source map.
+   */
+  source_index: number;
+}
+/**
+ * A [media query list](https://drafts.csswg.org/mediaqueries/#mq-list).
+ */
+export interface MediaList<M = MediaQuery> {
+  /**
+   * The list of media queries.
+   */
+  mediaQueries: M[];
+}
+/**
+ * A [media query](https://drafts.csswg.org/mediaqueries/#media).
+ */
+export interface MediaQuery {
+  /**
+   * The condition that this media query contains. This cannot have `or` in the first level.
+   */
+  condition?: MediaCondition | null;
+  /**
+   * The media type for this query, that can be known, unknown, or "all".
+   */
+  mediaType: MediaType;
+  /**
+   * The qualifier for this query.
+   */
+  qualifier?: Qualifier | null;
+}
+export interface LengthValue {
+  /**
+   * The length unit.
+   */
+  unit: LengthUnit;
+  /**
+   * The length value.
+   */
+  value: number;
+}
+/**
+ * A CSS environment variable reference.
+ */
+export interface EnvironmentVariable {
+  /**
+   * A fallback value in case the variable is not defined.
+   */
+  fallback?: TokenOrValue[] | null;
+  /**
+   * Optional indices into the dimensions of the environment variable.
+   */
+  indices?: number[];
+  /**
+   * The environment variable name.
+   */
+  name: EnvironmentVariableName;
+}
+/**
+ * A CSS [url()](https://www.w3.org/TR/css-values-4/#urls) value and its source location.
+ */
+export interface Url {
+  /**
+   * The location where the `url()` was seen in the CSS source file.
+   */
+  loc: Location;
+  /**
+   * The url string.
+   */
+  url: String;
+}
+/**
+ * A line and column position within a source file.
+ */
+export interface Location {
+  /**
+   * The column number, starting from 1.
+   */
+  column: number;
+  /**
+   * The line number, starting from 1.
+   */
+  line: number;
+}
+/**
+ * A CSS variable reference.
+ */
+export interface Variable {
+  /**
+   * A fallback value in case the variable is not defined.
+   */
+  fallback?: TokenOrValue[] | null;
+  /**
+   * The variable name.
+   */
+  name: DashedIdentReference;
+}
+/**
+ * A CSS [`<dashed-ident>`](https://www.w3.org/TR/css-values-4/#dashed-idents) reference.
+ *
+ * Dashed idents are used in cases where an identifier can be either author defined _or_ CSS-defined. Author defined idents must start with two dash characters ("--") or parsing will fail.
+ *
+ * In CSS modules, when the `dashed_idents` option is enabled, the identifier may be followed by the `from` keyword and an argument indicating where the referenced identifier is declared (e.g. a filename).
+ */
+export interface DashedIdentReference {
+  /**
+   * CSS modules extension: the filename where the variable is defined. Only enabled when the CSS modules `dashed_idents` option is turned on.
+   */
+  from?: Specifier | null;
+  /**
+   * The referenced identifier.
+   */
+  ident: String;
+}
+/**
+ * A custom CSS function.
+ */
+export interface Function {
+  /**
+   * The function arguments.
+   */
+  arguments: TokenOrValue[];
+  /**
+   * The function name.
+   */
+  name: String;
+}
+/**
+ * A [@import](https://drafts.csswg.org/css-cascade/#at-import) rule.
+ */
+export interface ImportRule<M = MediaQuery> {
+  /**
+   * An optional cascade layer name, or `None` for an anonymous layer.
+   */
+  layer?: String[] | null;
+  /**
+   * The location of the rule in the source file.
+   */
+  loc: Location2;
+  /**
+   * A media query.
+   */
+  media?: MediaList<M>;
+  /**
+   * An optional `supports()` condition.
+   */
+  supports?: SupportsCondition | null;
+  /**
+   * The url to import.
+   */
+  url: String;
+}
+/**
+ * A CSS [style rule](https://drafts.csswg.org/css-syntax/#style-rules).
+ */
+export interface StyleRule<D = Declaration, M = MediaQuery> {
+  /**
+   * The declarations within the style rule.
+   */
+  declarations?: DeclarationBlock<D>;
+  /**
+   * The location of the rule in the source file.
+   */
+  loc: Location2;
+  /**
+   * Nested rules within the style rule.
+   */
+  rules?: Rule<D, M>[];
+  /**
+   * The selectors for the style rule.
+   */
+  selectors: SelectorList;
+}
+/**
+ * A CSS declaration block.
+ *
+ * Properties are separated into a list of `!important` declararations, and a list of normal declarations. This reduces memory usage compared with storing a boolean along with each property.
+ */
+export interface DeclarationBlock<D = Declaration> {
+  /**
+   * A list of normal declarations in the block.
+   */
+  declarations?: D[];
+  /**
+   * A list of `!important` declarations in the block.
+   */
+  importantDeclarations?: D[];
+}
+/**
+ * A CSS [`<position>`](https://www.w3.org/TR/css3-values/#position) value, as used in the `background-position` property, gradients, masks, etc.
+ */
+export interface Position {
+  /**
+   * The x-position.
+   */
+  x: PositionComponentFor_HorizontalPositionKeyword;
+  /**
+   * The y-position.
+   */
+  y: PositionComponentFor_VerticalPositionKeyword;
+}
+/**
+ * An x/y position within a legacy `-webkit-gradient()`.
+ */
+export interface WebKitGradientPoint {
+  /**
+   * The x-position.
+   */
+  x: WebKitGradientPointComponentFor_HorizontalPositionKeyword;
+  /**
+   * The y-position.
+   */
+  y: WebKitGradientPointComponentFor_VerticalPositionKeyword;
+}
+/**
+ * A color stop within a legacy `-webkit-gradient()`.
+ */
+export interface WebKitColorStop {
+  /**
+   * The color of the color stop.
+   */
+  color: CssColor;
+  /**
+   * The position of the color stop.
+   */
+  position: number;
+}
+/**
+ * A CSS [`image-set()`](https://drafts.csswg.org/css-images-4/#image-set-notation) value.
+ *
+ * `image-set()` allows the user agent to choose between multiple versions of an image to display the most appropriate resolution or file type that it supports.
+ */
+export interface ImageSet {
+  /**
+   * The image options to choose from.
+   */
+  options: ImageSetOption[];
+  /**
+   * The vendor prefix for the `image-set()` function.
+   */
+  vendorPrefix: VendorPrefix;
+}
+/**
+ * An image option within the `image-set()` function. See [ImageSet](ImageSet).
+ */
+export interface ImageSetOption {
+  /**
+   * The mime type of the image.
+   */
+  fileType?: String | null;
+  /**
+   * The image for this option.
+   */
+  image: Image;
+  /**
+   * The resolution of the image.
+   */
+  resolution: Resolution;
+}
+/**
+ * A value for the [background-position](https://drafts.csswg.org/css-backgrounds/#background-position) shorthand property.
+ */
+export interface BackgroundPosition {
+  /**
+   * The x-position.
+   */
+  x: PositionComponentFor_HorizontalPositionKeyword;
+  /**
+   * The y-position.
+   */
+  y: PositionComponentFor_VerticalPositionKeyword;
+}
+/**
+ * A value for the [background-repeat](https://www.w3.org/TR/css-backgrounds-3/#background-repeat) property.
+ */
+export interface BackgroundRepeat {
+  /**
+   * A repeat style for the x direction.
+   */
+  x: BackgroundRepeatKeyword;
+  /**
+   * A repeat style for the y direction.
+   */
+  y: BackgroundRepeatKeyword;
+}
+/**
+ * A value for the [background](https://www.w3.org/TR/css-backgrounds-3/#background) shorthand property.
+ */
+export interface Background {
+  /**
+   * The background attachment.
+   */
+  attachment: BackgroundAttachment;
+  /**
+   * How the background should be clipped.
+   */
+  clip: BackgroundClip;
+  /**
+   * The background color.
+   */
+  color: CssColor;
+  /**
+   * The background image.
+   */
+  image: Image;
+  /**
+   * The background origin.
+   */
+  origin: BackgroundOrigin;
+  /**
+   * The background position.
+   */
+  position: BackgroundPosition;
+  /**
+   * How the background image should repeat.
+   */
+  repeat: BackgroundRepeat;
+  /**
+   * The size of the background image.
+   */
+  size: BackgroundSize;
+}
+/**
+ * A value for the [box-shadow](https://drafts.csswg.org/css-backgrounds/#box-shadow) property.
+ */
+export interface BoxShadow {
+  /**
+   * The blur radius of the shadow.
+   */
+  blur: Length;
+  /**
+   * The color of the box shadow.
+   */
+  color: CssColor;
+  /**
+   * Whether the shadow is inset within the box.
+   */
+  inset: boolean;
+  /**
+   * The spread distance of the shadow.
+   */
+  spread: Length;
+  /**
+   * The x offset of the shadow.
+   */
+  xOffset: Length;
+  /**
+   * The y offset of the shadow.
+   */
+  yOffset: Length;
+}
+/**
+ * A value for the [aspect-ratio](https://drafts.csswg.org/css-sizing-4/#aspect-ratio) property.
+ */
+export interface AspectRatio {
+  /**
+   * The `auto` keyword.
+   */
+  auto: boolean;
+  /**
+   * A preferred aspect ratio for the box, specified as width / height.
+   */
+  ratio?: Ratio | null;
+}
+/**
+ * A value for the [overflow](https://www.w3.org/TR/css-overflow-3/#overflow-properties) shorthand property.
+ */
+export interface Overflow {
+  /**
+   * The overflow mode for the x direction.
+   */
+  x: OverflowKeyword;
+  /**
+   * The overflow mode for the y direction.
+   */
+  y: OverflowKeyword;
+}
+/**
+ * A value for the [inset-block](https://drafts.csswg.org/css-logical/#propdef-inset-block) shorthand property.
+ */
+export interface InsetBlock {
+  /**
+   * The block end value.
+   */
+  blockEnd: LengthPercentageOrAuto;
+  /**
+   * The block start value.
+   */
+  blockStart: LengthPercentageOrAuto;
+}
+/**
+ * A value for the [inset-inline](https://drafts.csswg.org/css-logical/#propdef-inset-inline) shorthand property.
+ */
+export interface InsetInline {
+  /**
+   * The inline end value.
+   */
+  inlineEnd: LengthPercentageOrAuto;
+  /**
+   * The inline start value.
+   */
+  inlineStart: LengthPercentageOrAuto;
+}
+/**
+ * A value for the [inset](https://drafts.csswg.org/css-logical/#propdef-inset) shorthand property.
+ */
+export interface Inset {
+  /**
+   * The bottom value.
+   */
+  bottom: LengthPercentageOrAuto;
+  /**
+   * The left value.
+   */
+  left: LengthPercentageOrAuto;
+  /**
+   * The right value.
+   */
+  right: LengthPercentageOrAuto;
+  /**
+   * The top value.
+   */
+  top: LengthPercentageOrAuto;
+}
+/**
+ * A value for the [border-radius](https://www.w3.org/TR/css-backgrounds-3/#border-radius) property.
+ */
+export interface BorderRadius {
+  /**
+   * The x and y radius values for the bottom left corner.
+   */
+  bottomLeft: Size2DFor_DimensionPercentageFor_LengthValue;
+  /**
+   * The x and y radius values for the bottom right corner.
+   */
+  bottomRight: Size2DFor_DimensionPercentageFor_LengthValue;
+  /**
+   * The x and y radius values for the top left corner.
+   */
+  topLeft: Size2DFor_DimensionPercentageFor_LengthValue;
+  /**
+   * The x and y radius values for the top right corner.
+   */
+  topRight: Size2DFor_DimensionPercentageFor_LengthValue;
+}
+/**
+ * A value for the [border-image-repeat](https://www.w3.org/TR/css-backgrounds-3/#border-image-repeat) property.
+ */
+export interface BorderImageRepeat {
+  /**
+   * The horizontal repeat value.
+   */
+  horizontal: BorderImageRepeatKeyword;
+  /**
+   * The vertical repeat value.
+   */
+  vertical: BorderImageRepeatKeyword;
+}
+/**
+ * A value for the [border-image-slice](https://www.w3.org/TR/css-backgrounds-3/#border-image-slice) property.
+ */
+export interface BorderImageSlice {
+  /**
+   * Whether the middle of the border image should be preserved.
+   */
+  fill: boolean;
+  /**
+   * The offsets from the edges of the image.
+   */
+  offsets: RectFor_NumberOrPercentage;
+}
+/**
+ * A value for the [border-image](https://www.w3.org/TR/css-backgrounds-3/#border-image) shorthand property.
+ */
+export interface BorderImage {
+  /**
+   * The amount that the image extends beyond the border box.
+   */
+  outset: RectFor_LengthOrNumber;
+  /**
+   * How the border image is scaled and tiled.
+   */
+  repeat: BorderImageRepeat;
+  /**
+   * The offsets that define where the image is sliced.
+   */
+  slice: BorderImageSlice;
+  /**
+   * The border image.
+   */
+  source: Image;
+  /**
+   * The width of the border image.
+   */
+  width: RectFor_BorderImageSideWidth;
+}
+/**
+ * A value for the [border-color](https://drafts.csswg.org/css-backgrounds/#propdef-border-color) shorthand property.
+ */
+export interface BorderColor {
+  /**
+   * The bottom value.
+   */
+  bottom: CssColor;
+  /**
+   * The left value.
+   */
+  left: CssColor;
+  /**
+   * The right value.
+   */
+  right: CssColor;
+  /**
+   * The top value.
+   */
+  top: CssColor;
+}
+/**
+ * A value for the [border-style](https://drafts.csswg.org/css-backgrounds/#propdef-border-style) shorthand property.
+ */
+export interface BorderStyle {
+  /**
+   * The bottom value.
+   */
+  bottom: LineStyle;
+  /**
+   * The left value.
+   */
+  left: LineStyle;
+  /**
+   * The right value.
+   */
+  right: LineStyle;
+  /**
+   * The top value.
+   */
+  top: LineStyle;
+}
+/**
+ * A value for the [border-width](https://drafts.csswg.org/css-backgrounds/#propdef-border-width) shorthand property.
+ */
+export interface BorderWidth {
+  /**
+   * The bottom value.
+   */
+  bottom: BorderSideWidth;
+  /**
+   * The left value.
+   */
+  left: BorderSideWidth;
+  /**
+   * The right value.
+   */
+  right: BorderSideWidth;
+  /**
+   * The top value.
+   */
+  top: BorderSideWidth;
+}
+/**
+ * A value for the [border-block-color](https://drafts.csswg.org/css-logical/#propdef-border-block-color) shorthand property.
+ */
+export interface BorderBlockColor {
+  /**
+   * The block end value.
+   */
+  end: CssColor;
+  /**
+   * The block start value.
+   */
+  start: CssColor;
+}
+/**
+ * A value for the [border-block-style](https://drafts.csswg.org/css-logical/#propdef-border-block-style) shorthand property.
+ */
+export interface BorderBlockStyle {
+  /**
+   * The block end value.
+   */
+  end: LineStyle;
+  /**
+   * The block start value.
+   */
+  start: LineStyle;
+}
+/**
+ * A value for the [border-block-width](https://drafts.csswg.org/css-logical/#propdef-border-block-width) shorthand property.
+ */
+export interface BorderBlockWidth {
+  /**
+   * The block end value.
+   */
+  end: BorderSideWidth;
+  /**
+   * The block start value.
+   */
+  start: BorderSideWidth;
+}
+/**
+ * A value for the [border-inline-color](https://drafts.csswg.org/css-logical/#propdef-border-inline-color) shorthand property.
+ */
+export interface BorderInlineColor {
+  /**
+   * The inline end value.
+   */
+  end: CssColor;
+  /**
+   * The inline start value.
+   */
+  start: CssColor;
+}
+/**
+ * A value for the [border-inline-style](https://drafts.csswg.org/css-logical/#propdef-border-inline-style) shorthand property.
+ */
+export interface BorderInlineStyle {
+  /**
+   * The inline end value.
+   */
+  end: LineStyle;
+  /**
+   * The inline start value.
+   */
+  start: LineStyle;
+}
+/**
+ * A value for the [border-inline-width](https://drafts.csswg.org/css-logical/#propdef-border-inline-width) shorthand property.
+ */
+export interface BorderInlineWidth {
+  /**
+   * The inline end value.
+   */
+  end: BorderSideWidth;
+  /**
+   * The inline start value.
+   */
+  start: BorderSideWidth;
+}
+/**
+ * A generic type that represents the `border` and `outline` shorthand properties.
+ */
+export interface GenericBorderFor_LineStyle {
+  /**
+   * The border color.
+   */
+  color: CssColor;
+  /**
+   * The border style.
+   */
+  style: LineStyle;
+  /**
+   * The width of the border.
+   */
+  width: BorderSideWidth;
+}
+/**
+ * A generic type that represents the `border` and `outline` shorthand properties.
+ */
+export interface GenericBorderFor_OutlineStyleAnd_11 {
+  /**
+   * The border color.
+   */
+  color: CssColor;
+  /**
+   * The border style.
+   */
+  style: OutlineStyle;
+  /**
+   * The width of the border.
+   */
+  width: BorderSideWidth;
+}
+/**
+ * A value for the [flex-flow](https://www.w3.org/TR/2018/CR-css-flexbox-1-20181119/#flex-flow-property) shorthand property.
+ */
+export interface FlexFlow {
+  /**
+   * The direction that flex items flow.
+   */
+  direction: FlexDirection;
+  /**
+   * How the flex items wrap.
+   */
+  wrap: FlexWrap;
+}
+/**
+ * A value for the [flex](https://www.w3.org/TR/2018/CR-css-flexbox-1-20181119/#flex-property) shorthand property.
+ */
+export interface Flex {
+  /**
+   * The flex basis.
+   */
+  basis: LengthPercentageOrAuto;
+  /**
+   * The flex grow factor.
+   */
+  grow: number;
+  /**
+   * The flex shrink factor.
+   */
+  shrink: number;
+}
+/**
+ * A value for the [place-content](https://www.w3.org/TR/css-align-3/#place-content) shorthand property.
+ */
+export interface PlaceContent {
+  /**
+   * The content alignment.
+   */
+  align: AlignContent;
+  /**
+   * The content justification.
+   */
+  justify: JustifyContent;
+}
+/**
+ * A value for the [place-self](https://www.w3.org/TR/css-align-3/#place-self-property) shorthand property.
+ */
+export interface PlaceSelf {
+  /**
+   * The item alignment.
+   */
+  align: AlignSelf;
+  /**
+   * The item justification.
+   */
+  justify: JustifySelf;
+}
+/**
+ * A value for the [place-items](https://www.w3.org/TR/css-align-3/#place-items-property) shorthand property.
+ */
+export interface PlaceItems {
+  /**
+   * The item alignment.
+   */
+  align: AlignItems;
+  /**
+   * The item justification.
+   */
+  justify: JustifyItems;
+}
+/**
+ * A value for the [gap](https://www.w3.org/TR/css-align-3/#gap-shorthand) shorthand property.
+ */
+export interface Gap {
+  /**
+   * The column gap.
+   */
+  column: GapValue;
+  /**
+   * The row gap.
+   */
+  row: GapValue;
+}
+/**
+ * A [`<track-repeat>`](https://drafts.csswg.org/css-grid-2/#typedef-track-repeat) value, representing the `repeat()` function in a track list.
+ *
+ * See [TrackListItem](TrackListItem).
+ */
+export interface TrackRepeat {
+  /**
+   * The repeat count.
+   */
+  count: RepeatCount;
+  /**
+   * The line names to repeat.
+   */
+  lineNames: String[][];
+  /**
+   * The track sizes to repeat.
+   */
+  trackSizes: TrackSize[];
+}
+export interface GridAutoFlow {
+  /**
+   * If specified, a dense packing algorithm is used, which fills in holes in the grid.
+   */
+  dense: boolean;
+  /**
+   * The direction of the auto flow.
+   */
+  direction: AutoFlowDirection;
+}
+/**
+ * A value for the [grid-template](https://drafts.csswg.org/css-grid-2/#explicit-grid-shorthand) shorthand property.
+ *
+ * If `areas` is not `None`, then `rows` must also not be `None`.
+ */
+export interface GridTemplate {
+  /**
+   * The named grid areas.
+   */
+  areas: GridTemplateAreas;
+  /**
+   * The grid template columns.
+   */
+  columns: TrackSizing;
+  /**
+   * The grid template rows.
+   */
+  rows: TrackSizing;
+}
+/**
+ * A value for the [grid](https://drafts.csswg.org/css-grid-2/#grid-shorthand) shorthand property.
+ *
+ * Explicit and implicit values may not be combined.
+ */
+export interface Grid {
+  /**
+   * Explicit grid template areas.
+   */
+  areas: GridTemplateAreas;
+  /**
+   * The grid auto columns.
+   */
+  autoColumns: TrackSize[];
+  /**
+   * The grid auto flow.
+   */
+  autoFlow: GridAutoFlow;
+  /**
+   * The grid auto rows.
+   */
+  autoRows: TrackSize[];
+  /**
+   * Explicit grid template columns.
+   */
+  columns: TrackSizing;
+  /**
+   * Explicit grid template rows.
+   */
+  rows: TrackSizing;
+}
+/**
+ * A value for the [grid-row](https://drafts.csswg.org/css-grid-2/#propdef-grid-row) shorthand property.
+ */
+export interface GridRow {
+  /**
+   * The ending line.
+   */
+  end: GridLine;
+  /**
+   * The starting line.
+   */
+  start: GridLine;
+}
+/**
+ * A value for the [grid-row](https://drafts.csswg.org/css-grid-2/#propdef-grid-column) shorthand property.
+ */
+export interface GridColumn {
+  /**
+   * The ending line.
+   */
+  end: GridLine;
+  /**
+   * The starting line.
+   */
+  start: GridLine;
+}
+/**
+ * A value for the [grid-area](https://drafts.csswg.org/css-grid-2/#propdef-grid-area) shorthand property.
+ */
+export interface GridArea {
+  /**
+   * The grid column end placement.
+   */
+  columnEnd: GridLine;
+  /**
+   * The grid column start placement.
+   */
+  columnStart: GridLine;
+  /**
+   * The grid row end placement.
+   */
+  rowEnd: GridLine;
+  /**
+   * The grid row start placement.
+   */
+  rowStart: GridLine;
+}
+/**
+ * A value for the [margin-block](https://drafts.csswg.org/css-logical/#propdef-margin-block) shorthand property.
+ */
+export interface MarginBlock {
+  /**
+   * The block end value.
+   */
+  blockEnd: LengthPercentageOrAuto;
+  /**
+   * The block start value.
+   */
+  blockStart: LengthPercentageOrAuto;
+}
+/**
+ * A value for the [margin-inline](https://drafts.csswg.org/css-logical/#propdef-margin-inline) shorthand property.
+ */
+export interface MarginInline {
+  /**
+   * The inline end value.
+   */
+  inlineEnd: LengthPercentageOrAuto;
+  /**
+   * The inline start value.
+   */
+  inlineStart: LengthPercentageOrAuto;
+}
+/**
+ * A value for the [margin](https://drafts.csswg.org/css-box-4/#propdef-margin) shorthand property.
+ */
+export interface Margin {
+  /**
+   * The bottom value.
+   */
+  bottom: LengthPercentageOrAuto;
+  /**
+   * The left value.
+   */
+  left: LengthPercentageOrAuto;
+  /**
+   * The right value.
+   */
+  right: LengthPercentageOrAuto;
+  /**
+   * The top value.
+   */
+  top: LengthPercentageOrAuto;
+}
+/**
+ * A value for the [padding-block](https://drafts.csswg.org/css-logical/#propdef-padding-block) shorthand property.
+ */
+export interface PaddingBlock {
+  /**
+   * The block end value.
+   */
+  blockEnd: LengthPercentageOrAuto;
+  /**
+   * The block start value.
+   */
+  blockStart: LengthPercentageOrAuto;
+}
+/**
+ * A value for the [padding-inline](https://drafts.csswg.org/css-logical/#propdef-padding-inline) shorthand property.
+ */
+export interface PaddingInline {
+  /**
+   * The inline end value.
+   */
+  inlineEnd: LengthPercentageOrAuto;
+  /**
+   * The inline start value.
+   */
+  inlineStart: LengthPercentageOrAuto;
+}
+/**
+ * A value for the [padding](https://drafts.csswg.org/css-box-4/#propdef-padding) shorthand property.
+ */
+export interface Padding {
+  /**
+   * The bottom value.
+   */
+  bottom: LengthPercentageOrAuto;
+  /**
+   * The left value.
+   */
+  left: LengthPercentageOrAuto;
+  /**
+   * The right value.
+   */
+  right: LengthPercentageOrAuto;
+  /**
+   * The top value.
+   */
+  top: LengthPercentageOrAuto;
+}
+/**
+ * A value for the [scroll-margin-block](https://drafts.csswg.org/css-scroll-snap/#propdef-scroll-margin-block) shorthand property.
+ */
+export interface ScrollMarginBlock {
+  /**
+   * The block end value.
+   */
+  blockEnd: LengthPercentageOrAuto;
+  /**
+   * The block start value.
+   */
+  blockStart: LengthPercentageOrAuto;
+}
+/**
+ * A value for the [scroll-margin-inline](https://drafts.csswg.org/css-scroll-snap/#propdef-scroll-margin-inline) shorthand property.
+ */
+export interface ScrollMarginInline {
+  /**
+   * The inline end value.
+   */
+  inlineEnd: LengthPercentageOrAuto;
+  /**
+   * The inline start value.
+   */
+  inlineStart: LengthPercentageOrAuto;
+}
+/**
+ * A value for the [scroll-margin](https://drafts.csswg.org/css-scroll-snap/#scroll-margin) shorthand property.
+ */
+export interface ScrollMargin {
+  /**
+   * The bottom value.
+   */
+  bottom: LengthPercentageOrAuto;
+  /**
+   * The left value.
+   */
+  left: LengthPercentageOrAuto;
+  /**
+   * The right value.
+   */
+  right: LengthPercentageOrAuto;
+  /**
+   * The top value.
+   */
+  top: LengthPercentageOrAuto;
+}
+/**
+ * A value for the [scroll-padding-block](https://drafts.csswg.org/css-scroll-snap/#propdef-scroll-padding-block) shorthand property.
+ */
+export interface ScrollPaddingBlock {
+  /**
+   * The block end value.
+   */
+  blockEnd: LengthPercentageOrAuto;
+  /**
+   * The block start value.
+   */
+  blockStart: LengthPercentageOrAuto;
+}
+/**
+ * A value for the [scroll-padding-inline](https://drafts.csswg.org/css-scroll-snap/#propdef-scroll-padding-inline) shorthand property.
+ */
+export interface ScrollPaddingInline {
+  /**
+   * The inline end value.
+   */
+  inlineEnd: LengthPercentageOrAuto;
+  /**
+   * The inline start value.
+   */
+  inlineStart: LengthPercentageOrAuto;
+}
+/**
+ * A value for the [scroll-padding](https://drafts.csswg.org/css-scroll-snap/#scroll-padding) shorthand property.
+ */
+export interface ScrollPadding {
+  /**
+   * The bottom value.
+   */
+  bottom: LengthPercentageOrAuto;
+  /**
+   * The left value.
+   */
+  left: LengthPercentageOrAuto;
+  /**
+   * The right value.
+   */
+  right: LengthPercentageOrAuto;
+  /**
+   * The top value.
+   */
+  top: LengthPercentageOrAuto;
+}
+/**
+ * A value for the [font](https://www.w3.org/TR/css-fonts-4/#font-prop) shorthand property.
+ */
+export interface Font {
+  /**
+   * The font family.
+   */
+  family: FontFamily[];
+  /**
+   * The line height.
+   */
+  lineHeight: LineHeight;
+  /**
+   * The font size.
+   */
+  size: FontSize;
+  /**
+   * The font stretch.
+   */
+  stretch: FontStretch;
+  /**
+   * The font style.
+   */
+  style: FontStyle;
+  /**
+   * How the text should be capitalized. Only CSS 2.1 values are supported.
+   */
+  variantCaps: FontVariantCaps;
+  /**
+   * The font weight.
+   */
+  weight: FontWeight;
+}
+/**
+ * A value for the [transition](https://www.w3.org/TR/2018/WD-css-transitions-1-20181011/#transition-shorthand-property) property.
+ */
+export interface Transition {
+  /**
+   * The delay before the transition starts.
+   */
+  delay: Time;
+  /**
+   * The duration of the transition.
+   */
+  duration: Time;
+  /**
+   * The property to transition.
+   */
+  property: PropertyId;
+  /**
+   * The easing function for the transition.
+   */
+  timingFunction: EasingFunction;
+}
+/**
+ * The [scroll()](https://drafts.csswg.org/scroll-animations-1/#scroll-notation) function.
+ */
+export interface ScrollTimeline {
+  /**
+   * Specifies which axis of the scroll container to use as the progress for the timeline.
+   */
+  axis: ScrollAxis;
+  /**
+   * Specifies which element to use as the scroll container.
+   */
+  scroller: Scroller;
+}
+/**
+ * The [view()](https://drafts.csswg.org/scroll-animations-1/#view-notation) function.
+ */
+export interface ViewTimeline {
+  /**
+   * Specifies which axis of the scroll container to use as the progress for the timeline.
+   */
+  axis: ScrollAxis;
+  /**
+   * Provides an adjustment of the view progress visibility range.
+   */
+  inset: Size2DFor_LengthPercentageOrAuto;
+}
+/**
+ * A value for the [animation-range](https://drafts.csswg.org/scroll-animations/#animation-range) shorthand property.
+ */
+export interface AnimationRange {
+  /**
+   * The end of the animation's attachment range.
+   */
+  end: AnimationRangeEnd;
+  /**
+   * The start of the animation's attachment range.
+   */
+  start: AnimationRangeStart;
+}
+/**
+ * A value for the [animation](https://drafts.csswg.org/css-animations/#animation) shorthand property.
+ */
+export interface Animation {
+  /**
+   * The animation delay.
+   */
+  delay: Time;
+  /**
+   * The direction of the animation.
+   */
+  direction: AnimationDirection;
+  /**
+   * The animation duration.
+   */
+  duration: Time;
+  /**
+   * The animation fill mode.
+   */
+  fillMode: AnimationFillMode;
+  /**
+   * The number of times the animation will run.
+   */
+  iterationCount: AnimationIterationCount;
+  /**
+   * The animation name.
+   */
+  name: AnimationName;
+  /**
+   * The current play state of the animation.
+   */
+  playState: AnimationPlayState;
+  /**
+   * The animation timeline.
+   */
+  timeline: AnimationTimeline;
+  /**
+   * The easing function for the animation.
+   */
+  timingFunction: EasingFunction;
+}
+/**
+ * A 2D matrix.
+ */
+export interface MatrixForFloat {
+  a: number;
+  b: number;
+  c: number;
+  d: number;
+  e: number;
+  f: number;
+}
+/**
+ * A 3D matrix.
+ */
+export interface Matrix3DForFloat {
+  m11: number;
+  m12: number;
+  m13: number;
+  m14: number;
+  m21: number;
+  m22: number;
+  m23: number;
+  m24: number;
+  m31: number;
+  m32: number;
+  m33: number;
+  m34: number;
+  m41: number;
+  m42: number;
+  m43: number;
+  m44: number;
+}
+/**
+ * A value for the [rotate](https://drafts.csswg.org/css-transforms-2/#propdef-rotate) property.
+ */
+export interface Rotate {
+  /**
+   * The angle of rotation.
+   */
+  angle: Angle;
+  /**
+   * Rotation around the x axis.
+   */
+  x: number;
+  /**
+   * Rotation around the y axis.
+   */
+  y: number;
+  /**
+   * Rotation around the z axis.
+   */
+  z: number;
+}
+/**
+ * A value for the [text-transform](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#text-transform-property) property.
+ */
+export interface TextTransform {
+  /**
+   * How case should be transformed.
+   */
+  case: TextTransformCase;
+  /**
+   * Converts all small Kana characters to the equivalent full-size Kana.
+   */
+  fullSizeKana: boolean;
+  /**
+   * Puts all typographic character units in full-width form.
+   */
+  fullWidth: boolean;
+}
+/**
+ * A value for the [text-indent](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#text-indent-property) property.
+ */
+export interface TextIndent {
+  /**
+   * Affects the first line after each hard break.
+   */
+  eachLine: boolean;
+  /**
+   * Inverts which lines are affected.
+   */
+  hanging: boolean;
+  /**
+   * The amount to indent.
+   */
+  value: DimensionPercentageFor_LengthValue;
+}
+/**
+ * A value for the [text-decoration](https://www.w3.org/TR/2020/WD-css-text-decor-4-20200506/#text-decoration-property) shorthand property.
+ */
+export interface TextDecoration {
+  /**
+   * The color of the lines.
+   */
+  color: CssColor;
+  /**
+   * The lines to display.
+   */
+  line: TextDecorationLine;
+  /**
+   * The style of the lines.
+   */
+  style: TextDecorationStyle;
+  /**
+   * The thickness of the lines.
+   */
+  thickness: TextDecorationThickness;
+}
+/**
+ * A value for the [text-emphasis](https://www.w3.org/TR/2020/WD-css-text-decor-4-20200506/#text-emphasis-property) shorthand property.
+ */
+export interface TextEmphasis {
+  /**
+   * The text emphasis color.
+   */
+  color: CssColor;
+  /**
+   * The text emphasis style.
+   */
+  style: TextEmphasisStyle;
+}
+/**
+ * A value for the [text-emphasis-position](https://www.w3.org/TR/2020/WD-css-text-decor-4-20200506/#text-emphasis-position-property) property.
+ */
+export interface TextEmphasisPosition {
+  /**
+   * The horizontal position.
+   */
+  horizontal: TextEmphasisPositionHorizontal;
+  /**
+   * The vertical position.
+   */
+  vertical: TextEmphasisPositionVertical;
+}
+/**
+ * A value for the [text-shadow](https://www.w3.org/TR/2020/WD-css-text-decor-4-20200506/#text-shadow-property) property.
+ */
+export interface TextShadow {
+  /**
+   * The blur radius of the text shadow.
+   */
+  blur: Length;
+  /**
+   * The color of the text shadow.
+   */
+  color: CssColor;
+  /**
+   * The spread distance of the text shadow.
+   */
+  spread: Length;
+  /**
+   * The x offset of the text shadow.
+   */
+  xOffset: Length;
+  /**
+   * The y offset of the text shadow.
+   */
+  yOffset: Length;
+}
+/**
+ * A value for the [cursor](https://www.w3.org/TR/2021/WD-css-ui-4-20210316/#cursor) property.
+ */
+export interface Cursor {
+  /**
+   * A list of cursor images.
+   */
+  images: CursorImage[];
+  /**
+   * A pre-defined cursor.
+   */
+  keyword: CursorKeyword;
+}
+/**
+ * A [cursor image](https://www.w3.org/TR/2021/WD-css-ui-4-20210316/#cursor) value, used in the `cursor` property.
+ *
+ * See [Cursor](Cursor).
+ */
+export interface CursorImage {
+  /**
+   * The location in the image where the mouse pointer appears.
+   *
+   * @minItems 2
+   * @maxItems 2
+   */
+  hotspot?: [number, number] | null;
+  /**
+   * A url to the cursor image.
+   */
+  url: Url;
+}
+/**
+ * A value for the [caret](https://www.w3.org/TR/2021/WD-css-ui-4-20210316/#caret) shorthand property.
+ */
+export interface Caret {
+  /**
+   * The caret color.
+   */
+  color: ColorOrAuto;
+  /**
+   * The caret shape.
+   */
+  shape: CaretShape;
+}
+/**
+ * A value for the [list-style](https://www.w3.org/TR/2020/WD-css-lists-3-20201117/#list-style-property) shorthand property.
+ */
+export interface ListStyle {
+  /**
+   * The list marker image.
+   */
+  image: Image;
+  /**
+   * The list style type.
+   */
+  listStyleType: ListStyleType;
+  /**
+   * The position of the list marker.
+   */
+  position: ListStylePosition;
+}
+/**
+ * A value for the [composes](https://github.com/css-modules/css-modules/#dependencies) property from CSS modules.
+ */
+export interface Composes {
+  /**
+   * Where the class names are composed from.
+   */
+  from?: Specifier | null;
+  /**
+   * The source location of the `composes` property.
+   */
+  loc: Location;
+  /**
+   * A list of class names to compose.
+   */
+  names: String[];
+}
+/**
+ * An [`inset()`](https://www.w3.org/TR/css-shapes-1/#funcdef-inset) rectangle shape.
+ */
+export interface InsetRect {
+  /**
+   * A corner radius for the rectangle.
+   */
+  radius: BorderRadius;
+  /**
+   * The rectangle.
+   */
+  rect: RectFor_DimensionPercentageFor_LengthValue;
+}
+/**
+ * A [`circle()`](https://www.w3.org/TR/css-shapes-1/#funcdef-circle) shape.
+ */
+export interface Circle2 {
+  /**
+   * The position of the center of the circle.
+   */
+  position: Position;
+  /**
+   * The radius of the circle.
+   */
+  radius: ShapeRadius;
+}
+/**
+ * An [`ellipse()`](https://www.w3.org/TR/css-shapes-1/#funcdef-ellipse) shape.
+ */
+export interface Ellipse2 {
+  /**
+   * The position of the center of the ellipse.
+   */
+  position: Position;
+  /**
+   * The x-radius of the ellipse.
+   */
+  radiusX: ShapeRadius;
+  /**
+   * The y-radius of the ellipse.
+   */
+  radiusY: ShapeRadius;
+}
+/**
+ * A [`polygon()`](https://www.w3.org/TR/css-shapes-1/#funcdef-polygon) shape.
+ */
+export interface Polygon {
+  /**
+   * The fill rule used to determine the interior of the polygon.
+   */
+  fillRule: FillRule;
+  /**
+   * The points of each vertex of the polygon.
+   */
+  points: Point[];
+}
+/**
+ * A point within a `polygon()` shape.
+ *
+ * See [Polygon](Polygon).
+ */
+export interface Point {
+  /**
+   * The x position of the point.
+   */
+  x: DimensionPercentageFor_LengthValue;
+  /**
+   * the y position of the point.
+   */
+  y: DimensionPercentageFor_LengthValue;
+}
+/**
+ * A value for the [mask](https://www.w3.org/TR/css-masking-1/#the-mask) shorthand property.
+ */
+export interface Mask {
+  /**
+   * The box in which the mask is clipped.
+   */
+  clip: MaskClip;
+  /**
+   * How the mask is composited with the element.
+   */
+  composite: MaskComposite;
+  /**
+   * The mask image.
+   */
+  image: Image;
+  /**
+   * How the mask image is interpreted.
+   */
+  mode: MaskMode;
+  /**
+   * The origin of the mask.
+   */
+  origin: GeometryBox;
+  /**
+   * The position of the mask.
+   */
+  position: Position;
+  /**
+   * How the mask repeats.
+   */
+  repeat: BackgroundRepeat;
+  /**
+   * The size of the mask image.
+   */
+  size: BackgroundSize;
+}
+/**
+ * A value for the [mask-border](https://www.w3.org/TR/css-masking-1/#the-mask-border) shorthand property.
+ */
+export interface MaskBorder {
+  /**
+   * How the mask image is interpreted.
+   */
+  mode: MaskBorderMode;
+  /**
+   * The amount that the image extends beyond the border box.
+   */
+  outset: RectFor_LengthOrNumber;
+  /**
+   * How the mask image is scaled and tiled.
+   */
+  repeat: BorderImageRepeat;
+  /**
+   * The offsets that define where the image is sliced.
+   */
+  slice: BorderImageSlice;
+  /**
+   * The mask image.
+   */
+  source: Image;
+  /**
+   * The width of the mask image.
+   */
+  width: RectFor_BorderImageSideWidth;
+}
+/**
+ * A [`drop-shadow()`](https://drafts.fxtf.org/filter-effects-1/#funcdef-filter-drop-shadow) filter function.
+ */
+export interface DropShadow {
+  /**
+   * The blur radius of the drop shadow.
+   */
+  blur: Length;
+  /**
+   * The color of the drop shadow.
+   */
+  color: CssColor;
+  /**
+   * The x offset of the drop shadow.
+   */
+  xOffset: Length;
+  /**
+   * The y offset of the drop shadow.
+   */
+  yOffset: Length;
+}
+/**
+ * A value for the [container](https://drafts.csswg.org/css-contain-3/#container-shorthand) shorthand property.
+ */
+export interface Container {
+  /**
+   * The container type.
+   */
+  containerType: ContainerType;
+  /**
+   * The container name.
+   */
+  name: ContainerNameList;
+}
+export interface ColorScheme {
+  dark: boolean;
+  light: boolean;
+  only: boolean;
+}
+/**
+ * A known property with an unparsed value.
+ *
+ * This type is used when the value of a known property could not be parsed, e.g. in the case css `var()` references are encountered. In this case, the raw tokens are stored instead.
+ */
+export interface UnparsedProperty {
+  /**
+   * The id of the property.
+   */
+  propertyId: PropertyId;
+  /**
+   * The property value, stored as a raw token list.
+   */
+  value: TokenOrValue[];
+}
+/**
+ * A CSS custom property, representing any unknown property.
+ */
+export interface CustomProperty {
+  /**
+   * The name of the property.
+   */
+  name: CustomPropertyName;
+  /**
+   * The property value, stored as a raw token list.
+   */
+  value: TokenOrValue[];
+}
+export interface AttrOperation {
+  caseSensitivity?: ParsedCaseSensitivity & string;
+  operator: AttrSelectorOperator;
+  value: string;
+}
+/**
+ * A [view transition part selector](https://w3c.github.io/csswg-drafts/css-view-transitions-1/#typedef-pt-name-selector).
+ */
+export interface ViewTransitionPartSelector {
+  /**
+   * A list of view transition classes.
+   */
+  classes: String[];
+  /**
+   * The view transition part name.
+   */
+  name?: ViewTransitionPartName | null;
+}
+/**
+ * A [@keyframes](https://drafts.csswg.org/css-animations/#keyframes) rule.
+ */
+export interface KeyframesRule<D = Declaration> {
+  /**
+   * A list of keyframes in the animation.
+   */
+  keyframes: Keyframe<D>[];
+  /**
+   * The location of the rule in the source file.
+   */
+  loc: Location2;
+  /**
+   * The animation name. <keyframes-name> = <custom-ident> | <string>
+   */
+  name: KeyframesName;
+  /**
+   * A vendor prefix for the rule, e.g. `@-webkit-keyframes`.
+   */
+  vendorPrefix: VendorPrefix;
+}
+/**
+ * An individual keyframe within an `@keyframes` rule.
+ *
+ * See [KeyframesRule](KeyframesRule).
+ */
+export interface Keyframe<D = Declaration> {
+  /**
+   * The declarations for this keyframe.
+   */
+  declarations: DeclarationBlock<D>;
+  /**
+   * A list of keyframe selectors to associate with the declarations in this keyframe.
+   */
+  selectors: KeyframeSelector[];
+}
+/**
+ * A percentage of a given timeline range
+ */
+export interface TimelineRangePercentage {
+  /**
+   * The name of the timeline range.
+   */
+  name: TimelineRangeName;
+  /**
+   * The percentage progress between the start and end of the range.
+   */
+  percentage: number;
+}
+/**
+ * A [@font-face](https://drafts.csswg.org/css-fonts/#font-face-rule) rule.
+ */
+export interface FontFaceRule {
+  /**
+   * The location of the rule in the source file.
+   */
+  loc: Location2;
+  /**
+   * Declarations in the `@font-face` rule.
+   */
+  properties: FontFaceProperty[];
+}
+/**
+ * A `url()` value for the [src](https://drafts.csswg.org/css-fonts/#src-desc) property in an `@font-face` rule.
+ */
+export interface UrlSource {
+  /**
+   * Optional `format()` function.
+   */
+  format?: FontFormat | null;
+  /**
+   * Optional `tech()` function.
+   */
+  tech: FontTechnology[];
+  /**
+   * The URL.
+   */
+  url: Url;
+}
+/**
+ * A contiguous range of Unicode code points.
+ *
+ * Cannot be empty. Can represent a single code point when start == end.
+ */
+export interface UnicodeRange {
+  /**
+   * Inclusive end of the range. In [0, 0x10FFFF].
+   */
+  end: number;
+  /**
+   * Inclusive start of the range. In [0, end].
+   */
+  start: number;
+}
+/**
+ * A [@font-palette-values](https://drafts.csswg.org/css-fonts-4/#font-palette-values) rule.
+ */
+export interface FontPaletteValuesRule {
+  /**
+   * The location of the rule in the source file.
+   */
+  loc: Location2;
+  /**
+   * The name of the font palette.
+   */
+  name: String;
+  /**
+   * Declarations in the `@font-palette-values` rule.
+   */
+  properties: FontPaletteValuesProperty[];
+}
+/**
+ * A value for the [override-colors](https://drafts.csswg.org/css-fonts-4/#override-color) property in an `@font-palette-values` rule.
+ */
+export interface OverrideColors {
+  /**
+   * The replacement color.
+   */
+  color: CssColor;
+  /**
+   * The index of the color within the palette to override.
+   */
+  index: number;
+}
+/**
+ * A [@font-feature-values](https://drafts.csswg.org/css-fonts/#font-feature-values) rule.
+ */
+export interface FontFeatureValuesRule {
+  /**
+   * The location of the rule in the source file.
+   */
+  loc: Location2;
+  /**
+   * The name of the font feature values.
+   */
+  name: String[];
+  /**
+   * The rules within the `@font-feature-values` rule.
+   */
+  rules: {
+    [k: string]: FontFeatureSubrule;
+  };
+}
+/**
+ * A sub-rule of `@font-feature-values` https://drafts.csswg.org/css-fonts/#font-feature-values-syntax
+ */
+export interface FontFeatureSubrule {
+  /**
+   * The declarations within the `@font-feature-values` sub-rules.
+   */
+  declarations: {
+    [k: string]: number[];
+  };
+  /**
+   * The location of the rule in the source file.
+   */
+  loc: Location2;
+  /**
+   * The name of the `@font-feature-values` sub-rule.
+   */
+  name: FontFeatureSubruleType;
+}
+/**
+ * A [@page](https://www.w3.org/TR/css-page-3/#at-page-rule) rule.
+ */
+export interface PageRule<D = Declaration> {
+  /**
+   * The declarations within the `@page` rule.
+   */
+  declarations: DeclarationBlock<D>;
+  /**
+   * The location of the rule in the source file.
+   */
+  loc: Location2;
+  /**
+   * The nested margin rules.
+   */
+  rules: PageMarginRule<D>[];
+  /**
+   * A list of page selectors.
+   */
+  selectors: PageSelector[];
+}
+/**
+ * A [page margin rule](https://www.w3.org/TR/css-page-3/#margin-at-rules) rule.
+ */
+export interface PageMarginRule<D = Declaration> {
+  /**
+   * The declarations within the rule.
+   */
+  declarations: DeclarationBlock<D>;
+  /**
+   * The location of the rule in the source file.
+   */
+  loc: Location2;
+  /**
+   * The margin box identifier for this rule.
+   */
+  marginBox: PageMarginBox;
+}
+/**
+ * A [page selector](https://www.w3.org/TR/css-page-3/#typedef-page-selector) within a `@page` rule.
+ *
+ * Either a name or at least one pseudo class is required.
+ */
+export interface PageSelector {
+  /**
+   * An optional named page type.
+   */
+  name?: String | null;
+  /**
+   * A list of page pseudo classes.
+   */
+  pseudoClasses: PagePseudoClass[];
+}
+/**
+ * A [@supports](https://drafts.csswg.org/css-conditional-3/#at-supports) rule.
+ */
+export interface SupportsRule<D = Declaration, M = MediaQuery> {
+  /**
+   * The supports condition.
+   */
+  condition: SupportsCondition;
+  /**
+   * The location of the rule in the source file.
+   */
+  loc: Location2;
+  /**
+   * The rules within the `@supports` rule.
+   */
+  rules: Rule<D, M>[];
+}
+/**
+ * A [@counter-style](https://drafts.csswg.org/css-counter-styles/#the-counter-style-rule) rule.
+ */
+export interface CounterStyleRule<D = Declaration> {
+  /**
+   * Declarations in the `@counter-style` rule.
+   */
+  declarations: DeclarationBlock<D>;
+  /**
+   * The location of the rule in the source file.
+   */
+  loc: Location2;
+  /**
+   * The name of the counter style to declare.
+   */
+  name: String;
+}
+/**
+ * A [@namespace](https://drafts.csswg.org/css-namespaces/#declaration) rule.
+ */
+export interface NamespaceRule {
+  /**
+   * The location of the rule in the source file.
+   */
+  loc: Location2;
+  /**
+   * An optional namespace prefix to declare, or `None` to declare the default namespace.
+   */
+  prefix?: String | null;
+  /**
+   * The url of the namespace.
+   */
+  url: String;
+}
+/**
+ * A [@-moz-document](https://www.w3.org/TR/2012/WD-css3-conditional-20120911/#at-document) rule.
+ *
+ * Note that only the `url-prefix()` function with no arguments is supported, and only the `-moz` prefix is allowed since Firefox was the only browser that ever implemented this rule.
+ */
+export interface MozDocumentRule<D = Declaration, M = MediaQuery> {
+  /**
+   * The location of the rule in the source file.
+   */
+  loc: Location2;
+  /**
+   * Nested rules within the `@-moz-document` rule.
+   */
+  rules: Rule<D, M>[];
+}
+/**
+ * A [@nest](https://www.w3.org/TR/css-nesting-1/#at-nest) rule.
+ */
+export interface NestingRule<D = Declaration, M = MediaQuery> {
+  /**
+   * The location of the rule in the source file.
+   */
+  loc: Location2;
+  /**
+   * The style rule that defines the selector and declarations for the `@nest` rule.
+   */
+  style: StyleRule<D, M>;
+}
+/**
+ * A [nested declarations](https://drafts.csswg.org/css-nesting/#nested-declarations-rule) rule.
+ */
+export interface NestedDeclarationsRule<D = Declaration> {
+  /**
+   * The style rule that defines the selector and declarations for the `@nest` rule.
+   */
+  declarations: DeclarationBlock<D>;
+  /**
+   * The location of the rule in the source file.
+   */
+  loc: Location2;
+}
+/**
+ * A [@viewport](https://drafts.csswg.org/css-device-adapt/#atviewport-rule) rule.
+ */
+export interface ViewportRule<D = Declaration> {
+  /**
+   * The declarations within the `@viewport` rule.
+   */
+  declarations: DeclarationBlock<D>;
+  /**
+   * The location of the rule in the source file.
+   */
+  loc: Location2;
+  /**
+   * The vendor prefix for this rule, e.g. `@-ms-viewport`.
+   */
+  vendorPrefix: VendorPrefix;
+}
+/**
+ * A [@custom-media](https://drafts.csswg.org/mediaqueries-5/#custom-mq) rule.
+ */
+export interface CustomMediaRule<M = MediaQuery> {
+  /**
+   * The location of the rule in the source file.
+   */
+  loc: Location2;
+  /**
+   * The name of the declared media query.
+   */
+  name: String;
+  /**
+   * The media query to declare.
+   */
+  query: MediaList<M>;
+}
+/**
+ * A [@layer statement](https://drafts.csswg.org/css-cascade-5/#layer-empty) rule.
+ *
+ * See also [LayerBlockRule](LayerBlockRule).
+ */
+export interface LayerStatementRule {
+  /**
+   * The location of the rule in the source file.
+   */
+  loc: Location2;
+  /**
+   * The layer names to declare.
+   */
+  names: String[][];
+}
+/**
+ * A [@layer block](https://drafts.csswg.org/css-cascade-5/#layer-block) rule.
+ */
+export interface LayerBlockRule<D = Declaration, M = MediaQuery> {
+  /**
+   * The location of the rule in the source file.
+   */
+  loc: Location2;
+  /**
+   * The name of the layer to declare, or `None` to declare an anonymous layer.
+   */
+  name?: String[] | null;
+  /**
+   * The rules within the `@layer` rule.
+   */
+  rules: Rule<D, M>[];
+}
+/**
+ * A [@property](https://drafts.css-houdini.org/css-properties-values-api/#at-property-rule) rule.
+ */
+export interface PropertyRule {
+  /**
+   * Whether the custom property is inherited.
+   */
+  inherits: boolean;
+  /**
+   * An optional initial value for the custom property.
+   */
+  initialValue?: ParsedComponent | null;
+  /**
+   * The location of the rule in the source file.
+   */
+  loc: Location2;
+  /**
+   * The name of the custom property to declare.
+   */
+  name: String;
+  /**
+   * A syntax string to specify the grammar for the custom property.
+   */
+  syntax: SyntaxString;
+}
+/**
+ * A [syntax component](https://drafts.css-houdini.org/css-properties-values-api/#syntax-component) within a [SyntaxString](SyntaxString).
+ *
+ * A syntax component consists of a component kind an a multiplier, which indicates how the component may repeat during parsing.
+ */
+export interface SyntaxComponent {
+  /**
+   * The kind of component.
+   */
+  kind: SyntaxComponentKind;
+  /**
+   * A multiplier for the component.
+   */
+  multiplier: Multiplier;
+}
+/**
+ * A [@container](https://drafts.csswg.org/css-contain-3/#container-rule) rule.
+ */
+export interface ContainerRule<D = Declaration, M = MediaQuery> {
+  /**
+   * The container condition.
+   */
+  condition: ContainerCondition<D>;
+  /**
+   * The location of the rule in the source file.
+   */
+  loc: Location2;
+  /**
+   * The name of the container.
+   */
+  name?: String | null;
+  /**
+   * The rules within the `@container` rule.
+   */
+  rules: Rule<D, M>[];
+}
+/**
+ * A [@scope](https://drafts.csswg.org/css-cascade-6/#scope-atrule) rule.
+ *
+ * @scope (<scope-start>) [to (<scope-end>)]? { <stylesheet> }
+ */
+export interface ScopeRule<D = Declaration, M = MediaQuery> {
+  /**
+   * The location of the rule in the source file.
+   */
+  loc: Location2;
+  /**
+   * Nested rules within the `@scope` rule.
+   */
+  rules: Rule<D, M>[];
+  /**
+   * A selector list used to identify any scoping limits.
+   */
+  scopeEnd?: SelectorList | null;
+  /**
+   * A selector list used to identify the scoping root(s).
+   */
+  scopeStart?: SelectorList | null;
+}
+/**
+ * A [@starting-style](https://drafts.csswg.org/css-transitions-2/#defining-before-change-style-the-starting-style-rule) rule.
+ */
+export interface StartingStyleRule<D = Declaration, M = MediaQuery> {
+  /**
+   * The location of the rule in the source file.
+   */
+  loc: Location2;
+  /**
+   * Nested rules within the `@starting-style` rule.
+   */
+  rules: Rule<D, M>[];
+}
+/**
+ * A [@view-transition](https://drafts.csswg.org/css-view-transitions-2/#view-transition-rule) rule.
+ */
+export interface ViewTransitionRule {
+  /**
+   * The location of the rule in the source file.
+   */
+  loc: Location2;
+  /**
+   * Declarations in the `@view-transition` rule.
+   */
+  properties: ViewTransitionProperty[];
+}
+/**
+ * An unknown at-rule, stored as raw tokens.
+ */
+export interface UnknownAtRule {
+  /**
+   * The contents of the block, if any.
+   */
+  block?: TokenOrValue[] | null;
+  /**
+   * The location of the rule in the source file.
+   */
+  loc: Location2;
+  /**
+   * The name of the at-rule (without the @).
+   */
+  name: String;
+  /**
+   * The prelude of the rule.
+   */
+  prelude: TokenOrValue[];
+}
diff --git a/node/browserslistToTargets.js b/node/browserslistToTargets.js
new file mode 100644
index 0000000..fe9b6b4
--- /dev/null
+++ b/node/browserslistToTargets.js
@@ -0,0 +1,48 @@
+const BROWSER_MAPPING = {
+  and_chr: 'chrome',
+  and_ff: 'firefox',
+  ie_mob: 'ie',
+  op_mob: 'opera',
+  and_qq: null,
+  and_uc: null,
+  baidu: null,
+  bb: null,
+  kaios: null,
+  op_mini: null,
+};
+
+function browserslistToTargets(browserslist) {
+  let targets = {};
+  for (let browser of browserslist) {
+    let [name, v] = browser.split(' ');
+    if (BROWSER_MAPPING[name] === null) {
+      continue;
+    }
+
+    let version = parseVersion(v);
+    if (version == null) {
+      continue;
+    }
+
+    if (targets[name] == null || version < targets[name]) {
+      targets[name] = version;
+    }
+  }
+
+  return targets;
+}
+
+function parseVersion(version) {
+  let [major, minor = 0, patch = 0] = version
+    .split('-')[0]
+    .split('.')
+    .map(v => parseInt(v, 10));
+
+  if (isNaN(major) || isNaN(minor) || isNaN(patch)) {
+    return null;
+  }
+
+  return (major << 16) | (minor << 8) | patch;
+}
+
+module.exports = browserslistToTargets;
diff --git a/node/build.rs b/node/build.rs
new file mode 100644
index 0000000..4e9e5a2
--- /dev/null
+++ b/node/build.rs
@@ -0,0 +1,7 @@
+#[cfg(not(target_arch = "wasm32"))]
+extern crate napi_build;
+
+fn main() {
+  #[cfg(not(target_arch = "wasm32"))]
+  napi_build::setup();
+}
diff --git a/node/composeVisitors.js b/node/composeVisitors.js
new file mode 100644
index 0000000..9d5796e
--- /dev/null
+++ b/node/composeVisitors.js
@@ -0,0 +1,442 @@
+// @ts-check
+/** @typedef {import('./index').Visitor} Visitor */
+
+/**
+ * Composes multiple visitor objects into a single one.
+ * @param {Visitor[]} visitors 
+ * @return {Visitor}
+ */
+function composeVisitors(visitors) {
+  if (visitors.length === 1) {
+    return visitors[0];
+  }
+
+  /** @type Visitor */
+  let res = {};
+  composeSimpleVisitors(res, visitors, 'StyleSheet');
+  composeSimpleVisitors(res, visitors, 'StyleSheetExit');
+  composeObjectVisitors(res, visitors, 'Rule', ruleVisitor, wrapCustomAndUnknownAtRule);
+  composeObjectVisitors(res, visitors, 'RuleExit', ruleVisitor, wrapCustomAndUnknownAtRule);
+  composeObjectVisitors(res, visitors, 'Declaration', declarationVisitor, wrapCustomProperty);
+  composeObjectVisitors(res, visitors, 'DeclarationExit', declarationVisitor, wrapCustomProperty);
+  composeSimpleVisitors(res, visitors, 'Url');
+  composeSimpleVisitors(res, visitors, 'Color');
+  composeSimpleVisitors(res, visitors, 'Image');
+  composeSimpleVisitors(res, visitors, 'ImageExit');
+  composeSimpleVisitors(res, visitors, 'Length');
+  composeSimpleVisitors(res, visitors, 'Angle');
+  composeSimpleVisitors(res, visitors, 'Ratio');
+  composeSimpleVisitors(res, visitors, 'Resolution');
+  composeSimpleVisitors(res, visitors, 'Time');
+  composeSimpleVisitors(res, visitors, 'CustomIdent');
+  composeSimpleVisitors(res, visitors, 'DashedIdent');
+  composeArrayFunctions(res, visitors, 'MediaQuery');
+  composeArrayFunctions(res, visitors, 'MediaQueryExit');
+  composeSimpleVisitors(res, visitors, 'SupportsCondition');
+  composeSimpleVisitors(res, visitors, 'SupportsConditionExit');
+  composeArrayFunctions(res, visitors, 'Selector');
+  composeTokenVisitors(res, visitors, 'Token', 'token', false);
+  composeTokenVisitors(res, visitors, 'Function', 'function', false);
+  composeTokenVisitors(res, visitors, 'FunctionExit', 'function', true);
+  composeTokenVisitors(res, visitors, 'Variable', 'var', false);
+  composeTokenVisitors(res, visitors, 'VariableExit', 'var', true);
+  composeTokenVisitors(res, visitors, 'EnvironmentVariable', 'env', false);
+  composeTokenVisitors(res, visitors, 'EnvironmentVariableExit', 'env', true);
+  return res;
+}
+
+module.exports = composeVisitors;
+
+function wrapCustomAndUnknownAtRule(k, f) {
+  if (k === 'unknown') {
+    return (value => f({ type: 'unknown', value }));
+  }
+  if (k === 'custom') {
+    return (value => f({ type: 'custom', value }));
+  }
+  return f;
+}
+
+function wrapCustomProperty(k, f) {
+  return k === 'custom' ? (value => f({ property: 'custom', value })) : f;
+}
+
+/**
+ * @param {import('./index').Visitor['Rule']} f 
+ * @param {import('./ast').Rule} item 
+ */
+function ruleVisitor(f, item) {
+  if (typeof f === 'object') {
+    if (item.type === 'unknown') {
+      let v = f.unknown;
+      if (typeof v === 'object') {
+        v = v[item.value.name];
+      }
+      return v?.(item.value);
+    }
+    if (item.type === 'custom') {
+      let v = f.custom;
+      if (typeof v === 'object') {
+        v = v[item.value.name];
+      }
+      return v?.(item.value);
+    }
+    return f[item.type]?.(item);
+  }
+  return f?.(item);
+}
+
+/**
+ * @param {import('./index').Visitor['Declaration']} f 
+ * @param {import('./ast').Declaration} item 
+ */
+function declarationVisitor(f, item) {
+  if (typeof f === 'object') {
+    /** @type {string} */
+    let name = item.property;
+    if (item.property === 'unparsed') {
+      name = item.value.propertyId.property;
+    } else if (item.property === 'custom') {
+      let v = f.custom;
+      if (typeof v === 'object') {
+        v = v[item.value.name];
+      }
+      return v?.(item.value);
+    }
+    return f[name]?.(item);
+  }
+  return f?.(item);
+}
+
+/**
+ * 
+ * @param {Visitor[]} visitors 
+ * @param {string} key 
+ * @returns {[any[], boolean, Set<string>]}
+ */
+function extractObjectsOrFunctions(visitors, key) {
+  let values = [];
+  let hasFunction = false;
+  let allKeys = new Set();
+  for (let visitor of visitors) {
+    let v = visitor[key];
+    if (v) {
+      if (typeof v === 'function') {
+        hasFunction = true;
+      } else {
+        for (let key in v) {
+          allKeys.add(key);
+        }
+      }
+      values.push(v);
+    }
+  }
+  return [values, hasFunction, allKeys];
+}
+
+/**
+ * @template {keyof Visitor} K
+ * @param {Visitor} res
+ * @param {Visitor[]} visitors
+ * @param {K} key
+ * @param {(visitor: Visitor[K], item: any) => any | any[] | void} apply 
+ * @param {(k: string, f: any) => any} wrapKey 
+ */
+function composeObjectVisitors(res, visitors, key, apply, wrapKey) {
+  let [values, hasFunction, allKeys] = extractObjectsOrFunctions(visitors, key);
+  if (values.length === 0) {
+    return;
+  }
+
+  if (values.length === 1) {
+    res[key] = values[0];
+    return;
+  }
+
+  let f = createArrayVisitor(visitors, (visitor, item) => apply(visitor[key], item));
+  if (hasFunction) {
+    res[key] = f;
+  } else {
+    /** @type {any} */
+    let v = {};
+    for (let k of allKeys) {
+      v[k] = wrapKey(k, f);
+    }
+    res[key] = v;
+  }
+}
+
+/**
+ * @param {Visitor} res 
+ * @param {Visitor[]} visitors 
+ * @param {string} key 
+ * @param {import('./ast').TokenOrValue['type']} type 
+ * @param {boolean} isExit 
+ */
+function composeTokenVisitors(res, visitors, key, type, isExit) {
+  let [values, hasFunction, allKeys] = extractObjectsOrFunctions(visitors, key);
+  if (values.length === 0) {
+    return;
+  }
+
+  if (values.length === 1) {
+    res[key] = values[0];
+    return;
+  }
+
+  let f = createTokenVisitor(visitors, type, isExit);
+  if (hasFunction) {
+    res[key] = f;
+  } else {
+    let v = {};
+    for (let key of allKeys) {
+      v[key] = f;
+    }
+    res[key] = v;
+  }
+}
+
+/**
+ * @param {Visitor[]} visitors 
+ * @param {import('./ast').TokenOrValue['type']} type 
+ */
+function createTokenVisitor(visitors, type, isExit) {
+  let v = createArrayVisitor(visitors, (visitor, /** @type {import('./ast').TokenOrValue} */ item) => {
+    let f;
+    switch (item.type) {
+      case 'token':
+        f = visitor.Token;
+        if (typeof f === 'object') {
+          f = f[item.value.type];
+        }
+        break;
+      case 'function':
+        f = isExit ? visitor.FunctionExit : visitor.Function;
+        if (typeof f === 'object') {
+          f = f[item.value.name];
+        }
+        break;
+      case 'var':
+        f = isExit ? visitor.VariableExit : visitor.Variable;
+        break;
+      case 'env':
+        f = isExit ? visitor.EnvironmentVariableExit : visitor.EnvironmentVariable;
+        if (typeof f === 'object') {
+          let name;
+          switch (item.value.name.type) {
+            case 'ua':
+            case 'unknown':
+              name = item.value.name.value;
+              break;
+            case 'custom':
+              name = item.value.name.ident;
+              break;
+          }
+          f = f[name];
+        }
+        break;
+      case 'color':
+        f = visitor.Color;
+        break;
+      case 'url':
+        f = visitor.Url;
+        break;
+      case 'length':
+        f = visitor.Length;
+        break;
+      case 'angle':
+        f = visitor.Angle;
+        break;
+      case 'time':
+        f = visitor.Time;
+        break;
+      case 'resolution':
+        f = visitor.Resolution;
+        break;
+      case 'dashed-ident':
+        f = visitor.DashedIdent;
+        break;
+    }
+
+    if (!f) {
+      return;
+    }
+
+    let res = f(item.value);
+    switch (item.type) {
+      case 'color':
+      case 'url':
+      case 'length':
+      case 'angle':
+      case 'time':
+      case 'resolution':
+      case 'dashed-ident':
+        if (Array.isArray(res)) {
+          res = res.map(value => ({ type: item.type, value }))
+        } else if (res) {
+          res = { type: item.type, value: res };
+        }
+        break;
+    }
+
+    return res;
+  });
+
+  return value => v({ type, value });
+}
+
+/**
+ * @param {Visitor[]} visitors 
+ * @param {string} key 
+ */
+function extractFunctions(visitors, key) {
+  let functions = [];
+  for (let visitor of visitors) {
+    let f = visitor[key];
+    if (f) {
+      functions.push(f);
+    }
+  }
+  return functions;
+}
+
+/**
+ * @param {Visitor} res 
+ * @param {Visitor[]} visitors 
+ * @param {string} key 
+ */
+function composeSimpleVisitors(res, visitors, key) {
+  let functions = extractFunctions(visitors, key);
+  if (functions.length === 0) {
+    return;
+  }
+
+  if (functions.length === 1) {
+    res[key] = functions[0];
+    return;
+  }
+
+  res[key] = arg => {
+    let mutated = false;
+    for (let f of functions) {
+      let res = f(arg);
+      if (res) {
+        arg = res;
+        mutated = true;
+      }
+    }
+
+    return mutated ? arg : undefined;
+  };
+}
+
+/**
+ * @param {Visitor} res 
+ * @param {Visitor[]} visitors 
+ * @param {string} key 
+ */
+function composeArrayFunctions(res, visitors, key) {
+  let functions = extractFunctions(visitors, key);
+  if (functions.length === 0) {
+    return;
+  }
+
+  if (functions.length === 1) {
+    res[key] = functions[0];
+    return;
+  }
+
+  res[key] = createArrayVisitor(functions, (f, item) => f(item));
+}
+
+/**
+ * @template T
+ * @template V
+ * @param {T[]} visitors 
+ * @param {(visitor: T, item: V) => V | V[] | void} apply 
+ * @returns {(item: V) => V | V[] | void}
+ */
+function createArrayVisitor(visitors, apply) {
+  let seen = new Bitset(visitors.length);
+  return arg => {
+    let arr = [arg];
+    let mutated = false;
+    seen.clear();
+    for (let i = 0; i < arr.length; i++) {
+      // For each value, call all visitors. If a visitor returns a new value,
+      // we start over, but skip the visitor that generated the value or saw
+      // it before (to avoid cycles). This way, visitors can be composed in any order. 
+      for (let v = 0; v < visitors.length;) {
+        if (seen.get(v)) {
+          v++;
+          continue;
+        }
+
+        let item = arr[i];
+        let visitor = visitors[v];
+        let res = apply(visitor, item);
+        if (Array.isArray(res)) {
+          if (res.length === 0) {
+            arr.splice(i, 1);
+          } else if (res.length === 1) {
+            arr[i] = res[0];
+          } else {
+            arr.splice(i, 1, ...res);
+          }
+          mutated = true;
+          seen.set(v);
+          v = 0;
+        } else if (res) {
+          arr[i] = res;
+          mutated = true;
+          seen.set(v);
+          v = 0;
+        } else {
+          v++;
+        }
+      }
+    }
+
+    if (!mutated) {
+      return;
+    }
+
+    return arr.length === 1 ? arr[0] : arr;
+  };
+}
+
+class Bitset {
+  constructor(maxBits = 32) {
+    this.bits = 0;
+    this.more = maxBits > 32 ? new Uint32Array(Math.ceil((maxBits - 32) / 32)) : null;
+  }
+
+  /** @param {number} bit */
+  get(bit) {
+    if (bit >= 32 && this.more) {
+      let i = Math.floor((bit - 32) / 32);
+      let b = bit % 32;
+      return Boolean(this.more[i] & (1 << b));
+    } else {
+      return Boolean(this.bits & (1 << bit));
+    }
+  }
+
+  /** @param {number} bit */
+  set(bit) {
+    if (bit >= 32 && this.more) {
+      let i = Math.floor((bit - 32) / 32);
+      let b = bit % 32;
+      this.more[i] |= 1 << b;
+    } else {
+      this.bits |= 1 << bit;
+    }
+  }
+
+  clear() {
+    this.bits = 0;
+    if (this.more) {
+      this.more.fill(0);
+    }
+  }
+}
diff --git a/node/flags.js b/node/flags.js
new file mode 100644
index 0000000..a636a20
--- /dev/null
+++ b/node/flags.js
@@ -0,0 +1,28 @@
+// This file is autogenerated by build-prefixes.js. DO NOT EDIT!
+
+exports.Features = {
+  Nesting: 1,
+  NotSelectorList: 2,
+  DirSelector: 4,
+  LangSelectorList: 8,
+  IsSelector: 16,
+  TextDecorationThicknessPercent: 32,
+  MediaIntervalSyntax: 64,
+  MediaRangeSyntax: 128,
+  CustomMediaQueries: 256,
+  ClampFunction: 512,
+  ColorFunction: 1024,
+  OklabColors: 2048,
+  LabColors: 4096,
+  P3Colors: 8192,
+  HexAlphaColors: 16384,
+  SpaceSeparatedColorNotation: 32768,
+  FontFamilySystemUi: 65536,
+  DoublePositionGradients: 131072,
+  VendorPrefixes: 262144,
+  LogicalProperties: 524288,
+  LightDark: 1048576,
+  Selectors: 31,
+  MediaQueries: 448,
+  Colors: 1113088,
+};
diff --git a/node/index.d.ts b/node/index.d.ts
new file mode 100644
index 0000000..76d4057
--- /dev/null
+++ b/node/index.d.ts
@@ -0,0 +1,477 @@
+import type { Angle, CssColor, Rule, CustomProperty, EnvironmentVariable, Function, Image, LengthValue, MediaQuery, Declaration, Ratio, Resolution, Selector, SupportsCondition, Time, Token, TokenOrValue, UnknownAtRule, Url, Variable, StyleRule, DeclarationBlock, ParsedComponent, Multiplier, StyleSheet, Location2 } from './ast';
+import { Targets, Features } from './targets';
+
+export * from './ast';
+
+export { Targets, Features };
+
+export interface TransformOptions<C extends CustomAtRules> {
+  /** The filename being transformed. Used for error messages and source maps. */
+  filename: string,
+  /** The source code to transform. */
+  code: Uint8Array,
+  /** Whether to enable minification. */
+  minify?: boolean,
+  /** Whether to output a source map. */
+  sourceMap?: boolean,
+  /** An input source map to extend. */
+  inputSourceMap?: string,
+  /**
+   * An optional project root path, used as the source root in the output source map.
+   * Also used to generate relative paths for sources used in CSS module hashes.
+   */
+  projectRoot?: string,
+  /** The browser targets for the generated code. */
+  targets?: Targets,
+  /** Features that should always be compiled, even when supported by targets. */
+  include?: number,
+  /** Features that should never be compiled, even when unsupported by targets. */
+  exclude?: number,
+  /** Whether to enable parsing various draft syntax. */
+  drafts?: Drafts,
+  /** Whether to enable various non-standard syntax. */
+  nonStandard?: NonStandard,
+  /** Whether to compile this file as a CSS module. */
+  cssModules?: boolean | CSSModulesConfig,
+  /**
+   * Whether to analyze dependencies (e.g. `@import` and `url()`).
+   * When enabled, `@import` rules are removed, and `url()` dependencies
+   * are replaced with hashed placeholders that can be replaced with the final
+   * urls later (after bundling). Dependencies are returned as part of the result.
+   */
+  analyzeDependencies?: boolean | DependencyOptions,
+  /**
+   * Replaces user action pseudo classes with class names that can be applied from JavaScript.
+   * This is useful for polyfills, for example.
+   */
+  pseudoClasses?: PseudoClasses,
+  /**
+   * A list of class names, ids, and custom identifiers (e.g. @keyframes) that are known
+   * to be unused. These will be removed during minification. Note that these are not
+   * selectors but individual names (without any . or # prefixes).
+   */
+  unusedSymbols?: string[],
+  /**
+   * Whether to ignore invalid rules and declarations rather than erroring.
+   * When enabled, warnings are returned, and the invalid rule or declaration is
+   * omitted from the output code.
+   */
+  errorRecovery?: boolean,
+  /**
+   * An AST visitor object. This allows custom transforms or analysis to be implemented in JavaScript.
+   * Multiple visitors can be composed into one using the `composeVisitors` function.
+   * For optimal performance, visitors should be as specific as possible about what types of values
+   * they care about so that JavaScript has to be called as little as possible.
+   */
+  visitor?: Visitor<C>,
+  /**
+   * Defines how to parse custom CSS at-rules. Each at-rule can have a prelude, defined using a CSS
+   * [syntax string](https://drafts.css-houdini.org/css-properties-values-api/#syntax-strings), and
+   * a block body. The body can be a declaration list, rule list, or style block as defined in the
+   * [css spec](https://drafts.csswg.org/css-syntax/#declaration-rule-list).
+   */
+  customAtRules?: C
+}
+
+// This is a hack to make TS still provide autocomplete for `property` vs. just making it `string`.
+type PropertyStart = '-' | '_' | 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm' | 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z';
+export type ReturnedDeclaration = Declaration | {
+  /** The property name. */
+  property: `${PropertyStart}${string}`,
+  /** The raw string value for the declaration. */
+  raw: string
+};
+
+export type ReturnedMediaQuery = MediaQuery | {
+  /** The raw string value for the media query. */
+  raw: string
+};
+
+type FindByType<Union, Name> = Union extends { type: Name } ? Union : never;
+export type ReturnedRule = Rule<ReturnedDeclaration, ReturnedMediaQuery>;
+type RequiredValue<Rule> = Rule extends { value: object }
+  ? Rule['value'] extends StyleRule
+  ? Rule & { value: Required<StyleRule> & { declarations: Required<DeclarationBlock> } }
+  : Rule & { value: Required<Rule['value']> }
+  : Rule;
+type RuleVisitor<R = RequiredValue<Rule>> = ((rule: R) => ReturnedRule | ReturnedRule[] | void);
+type MappedRuleVisitors = {
+  [Name in Exclude<Rule['type'], 'unknown' | 'custom'>]?: RuleVisitor<RequiredValue<FindByType<Rule, Name>>>;
+}
+
+type UnknownVisitors<T> = {
+  [name: string]: RuleVisitor<T>
+}
+
+type CustomVisitors<T extends CustomAtRules> = {
+  [Name in keyof T]?: RuleVisitor<CustomAtRule<Name, T[Name]>>
+};
+
+type AnyCustomAtRule<C extends CustomAtRules> = {
+  [Key in keyof C]: CustomAtRule<Key, C[Key]>
+}[keyof C];
+
+type RuleVisitors<C extends CustomAtRules> = MappedRuleVisitors & {
+  unknown?: UnknownVisitors<UnknownAtRule> | Omit<RuleVisitor<UnknownAtRule>, keyof CallableFunction>,
+  custom?: CustomVisitors<C> | Omit<RuleVisitor<AnyCustomAtRule<C>>, keyof CallableFunction>
+};
+
+type PreludeTypes = Exclude<ParsedComponent['type'], 'literal' | 'repeated' | 'token'>;
+type SyntaxString = `<${PreludeTypes}>` | `<${PreludeTypes}>+` | `<${PreludeTypes}>#` | (string & {});
+type ComponentTypes = {
+  [Key in PreludeTypes as `<${Key}>`]: FindByType<ParsedComponent, Key>
+};
+
+type Repetitions = {
+  [Key in PreludeTypes as `<${Key}>+` | `<${Key}>#`]: {
+    type: "repeated",
+    value: {
+      components: FindByType<ParsedComponent, Key>[],
+      multiplier: Multiplier
+    }
+  }
+};
+
+type MappedPrelude = ComponentTypes & Repetitions;
+type MappedBody<P extends CustomAtRuleDefinition['body']> = P extends 'style-block' ? 'rule-list' : P;
+interface CustomAtRule<N, R extends CustomAtRuleDefinition> {
+  name: N,
+  prelude: R['prelude'] extends keyof MappedPrelude ? MappedPrelude[R['prelude']] : ParsedComponent,
+  body: FindByType<CustomAtRuleBody, MappedBody<R['body']>>,
+  loc: Location2
+}
+
+type CustomAtRuleBody = {
+  type: 'declaration-list',
+  value: Required<DeclarationBlock>
+} | {
+  type: 'rule-list',
+  value: RequiredValue<Rule>[]
+};
+
+type FindProperty<Union, Name> = Union extends { property: Name } ? Union : never;
+type DeclarationVisitor<P = Declaration> = ((property: P) => ReturnedDeclaration | ReturnedDeclaration[] | void);
+type MappedDeclarationVisitors = {
+  [Name in Exclude<Declaration['property'], 'unparsed' | 'custom'>]?: DeclarationVisitor<FindProperty<Declaration, Name> | FindProperty<Declaration, 'unparsed'>>;
+}
+
+type CustomPropertyVisitors = {
+  [name: string]: DeclarationVisitor<CustomProperty>
+}
+
+type DeclarationVisitors = MappedDeclarationVisitors & {
+  custom?: CustomPropertyVisitors | DeclarationVisitor<CustomProperty>
+}
+
+interface RawValue {
+  /** A raw string value which will be parsed like CSS. */
+  raw: string
+}
+
+type TokenReturnValue = TokenOrValue | TokenOrValue[] | RawValue | void;
+type TokenVisitor = (token: Token) => TokenReturnValue;
+type VisitableTokenTypes = 'ident' | 'at-keyword' | 'hash' | 'id-hash' | 'string' | 'number' | 'percentage' | 'dimension';
+type TokenVisitors = {
+  [Name in VisitableTokenTypes]?: (token: FindByType<Token, Name>) => TokenReturnValue;
+}
+
+type FunctionVisitor = (fn: Function) => TokenReturnValue;
+type EnvironmentVariableVisitor = (env: EnvironmentVariable) => TokenReturnValue;
+type EnvironmentVariableVisitors = {
+  [name: string]: EnvironmentVariableVisitor
+};
+
+export interface Visitor<C extends CustomAtRules> {
+  StyleSheet?(stylesheet: StyleSheet): StyleSheet<ReturnedDeclaration, ReturnedMediaQuery> | void;
+  StyleSheetExit?(stylesheet: StyleSheet): StyleSheet<ReturnedDeclaration, ReturnedMediaQuery> | void;
+  Rule?: RuleVisitor | RuleVisitors<C>;
+  RuleExit?: RuleVisitor | RuleVisitors<C>;
+  Declaration?: DeclarationVisitor | DeclarationVisitors;
+  DeclarationExit?: DeclarationVisitor | DeclarationVisitors;
+  Url?(url: Url): Url | void;
+  Color?(color: CssColor): CssColor | void;
+  Image?(image: Image): Image | void;
+  ImageExit?(image: Image): Image | void;
+  Length?(length: LengthValue): LengthValue | void;
+  Angle?(angle: Angle): Angle | void;
+  Ratio?(ratio: Ratio): Ratio | void;
+  Resolution?(resolution: Resolution): Resolution | void;
+  Time?(time: Time): Time | void;
+  CustomIdent?(ident: string): string | void;
+  DashedIdent?(ident: string): string | void;
+  MediaQuery?(query: MediaQuery): ReturnedMediaQuery | ReturnedMediaQuery[] | void;
+  MediaQueryExit?(query: MediaQuery): ReturnedMediaQuery | ReturnedMediaQuery[] | void;
+  SupportsCondition?(condition: SupportsCondition): SupportsCondition;
+  SupportsConditionExit?(condition: SupportsCondition): SupportsCondition;
+  Selector?(selector: Selector): Selector | Selector[] | void;
+  Token?: TokenVisitor | TokenVisitors;
+  Function?: FunctionVisitor | { [name: string]: FunctionVisitor };
+  FunctionExit?: FunctionVisitor | { [name: string]: FunctionVisitor };
+  Variable?(variable: Variable): TokenReturnValue;
+  VariableExit?(variable: Variable): TokenReturnValue;
+  EnvironmentVariable?: EnvironmentVariableVisitor | EnvironmentVariableVisitors;
+  EnvironmentVariableExit?: EnvironmentVariableVisitor | EnvironmentVariableVisitors;
+}
+
+export interface CustomAtRules {
+  [name: string]: CustomAtRuleDefinition
+}
+
+export interface CustomAtRuleDefinition {
+  /**
+   * Defines the syntax for a custom at-rule prelude. The value should be a
+   * CSS [syntax string](https://drafts.css-houdini.org/css-properties-values-api/#syntax-strings)
+   * representing the types of values that are accepted. This property may be omitted or
+   * set to null to indicate that no prelude is accepted.
+   */
+  prelude?: SyntaxString | null,
+  /**
+   * Defines the type of body contained within the at-rule block.
+   *   - declaration-list: A CSS declaration list, as in a style rule.
+   *   - rule-list: A list of CSS rules, as supported within a non-nested
+   *       at-rule such as `@media` or `@supports`.
+   *   - style-block: Both a declaration list and rule list, as accepted within
+   *       a nested at-rule within a style rule (e.g. `@media` inside a style rule
+   *       with directly nested declarations).
+   */
+  body?: 'declaration-list' | 'rule-list' | 'style-block' | null
+}
+
+export interface DependencyOptions {
+  /** Whether to preserve `@import` rules rather than removing them. */
+  preserveImports?: boolean
+}
+
+export type BundleOptions<C extends CustomAtRules> = Omit<TransformOptions<C>, 'code'>;
+
+export interface BundleAsyncOptions<C extends CustomAtRules> extends BundleOptions<C> {
+  resolver?: Resolver;
+}
+
+/** Custom resolver to use when loading CSS files. */
+export interface Resolver {
+  /** Read the given file and return its contents as a string. */
+  read?: (file: string) => string | Promise<string>;
+
+  /**
+   * Resolve the given CSS import specifier from the provided originating file to a
+   * path which gets passed to `read()`.
+   */
+  resolve?: (specifier: string, originatingFile: string) => string | Promise<string>;
+}
+
+export interface Drafts {
+  /** Whether to enable @custom-media rules. */
+  customMedia?: boolean
+}
+
+export interface NonStandard {
+  /** Whether to enable the non-standard >>> and /deep/ selector combinators used by Angular and Vue. */
+  deepSelectorCombinator?: boolean
+}
+
+export interface PseudoClasses {
+  hover?: string,
+  active?: string,
+  focus?: string,
+  focusVisible?: string,
+  focusWithin?: string
+}
+
+export interface TransformResult {
+  /** The transformed code. */
+  code: Uint8Array,
+  /** The generated source map, if enabled. */
+  map: Uint8Array | void,
+  /** CSS module exports, if enabled. */
+  exports: CSSModuleExports | void,
+  /** CSS module references, if `dashedIdents` is enabled. */
+  references: CSSModuleReferences,
+  /** `@import` and `url()` dependencies, if enabled. */
+  dependencies: Dependency[] | void,
+  /** Warnings that occurred during compilation. */
+  warnings: Warning[]
+}
+
+export interface Warning {
+  message: string,
+  type: string,
+  value?: any,
+  loc: ErrorLocation
+}
+
+export interface CSSModulesConfig {
+  /** The pattern to use when renaming class names and other identifiers. Default is `[hash]_[local]`. */
+  pattern?: string,
+  /** Whether to rename dashed identifiers, e.g. custom properties. */
+  dashedIdents?: boolean,
+  /** Whether to enable hashing for `@keyframes`. */
+  animation?: boolean,
+  /** Whether to enable hashing for CSS grid identifiers. */
+  grid?: boolean,
+  /** Whether to enable hashing for `@container` names. */
+  container?: boolean,
+  /** Whether to enable hashing for custom identifiers. */
+  customIdents?: boolean,
+  /** Whether to require at least one class or id selector in each rule. */
+  pure?: boolean
+}
+
+export type CSSModuleExports = {
+  /** Maps exported (i.e. original) names to local names. */
+  [name: string]: CSSModuleExport
+};
+
+export interface CSSModuleExport {
+  /** The local (compiled) name for this export. */
+  name: string,
+  /** Whether the export is referenced in this file. */
+  isReferenced: boolean,
+  /** Other names that are composed by this export. */
+  composes: CSSModuleReference[]
+}
+
+export type CSSModuleReferences = {
+  /** Maps placeholder names to references. */
+  [name: string]: DependencyCSSModuleReference,
+};
+
+export type CSSModuleReference = LocalCSSModuleReference | GlobalCSSModuleReference | DependencyCSSModuleReference;
+
+export interface LocalCSSModuleReference {
+  type: 'local',
+  /** The local (compiled) name for the reference. */
+  name: string,
+}
+
+export interface GlobalCSSModuleReference {
+  type: 'global',
+  /** The referenced global name. */
+  name: string,
+}
+
+export interface DependencyCSSModuleReference {
+  type: 'dependency',
+  /** The name to reference within the dependency. */
+  name: string,
+  /** The dependency specifier for the referenced file. */
+  specifier: string
+}
+
+export type Dependency = ImportDependency | UrlDependency;
+
+export interface ImportDependency {
+  type: 'import',
+  /** The url of the `@import` dependency. */
+  url: string,
+  /** The media query for the `@import` rule. */
+  media: string | null,
+  /** The `supports()` query for the `@import` rule. */
+  supports: string | null,
+  /** The source location where the `@import` rule was found. */
+  loc: SourceLocation,
+  /** The placeholder that the import was replaced with. */
+  placeholder: string
+}
+
+export interface UrlDependency {
+  type: 'url',
+  /** The url of the dependency. */
+  url: string,
+  /** The source location where the `url()` was found. */
+  loc: SourceLocation,
+  /** The placeholder that the url was replaced with. */
+  placeholder: string
+}
+
+export interface SourceLocation {
+  /** The file path in which the dependency exists. */
+  filePath: string,
+  /** The start location of the dependency. */
+  start: Location,
+  /** The end location (inclusive) of the dependency. */
+  end: Location
+}
+
+export interface Location {
+  /** The line number (1-based). */
+  line: number,
+  /** The column number (0-based). */
+  column: number
+}
+
+export interface ErrorLocation extends Location {
+  filename: string
+}
+
+/**
+ * Compiles a CSS file, including optionally minifying and lowering syntax to the given
+ * targets. A source map may also be generated, but this is not enabled by default.
+ */
+export declare function transform<C extends CustomAtRules>(options: TransformOptions<C>): TransformResult;
+
+export interface TransformAttributeOptions {
+  /** The filename in which the style attribute appeared. Used for error messages and dependencies. */
+  filename?: string,
+  /** The source code to transform. */
+  code: Uint8Array,
+  /** Whether to enable minification. */
+  minify?: boolean,
+  /** The browser targets for the generated code. */
+  targets?: Targets,
+  /**
+   * Whether to analyze `url()` dependencies.
+   * When enabled, `url()` dependencies are replaced with hashed placeholders
+   * that can be replaced with the final urls later (after bundling).
+   * Dependencies are returned as part of the result.
+   */
+  analyzeDependencies?: boolean,
+  /**
+   * Whether to ignore invalid rules and declarations rather than erroring.
+   * When enabled, warnings are returned, and the invalid rule or declaration is
+   * omitted from the output code.
+   */
+  errorRecovery?: boolean,
+  /**
+   * An AST visitor object. This allows custom transforms or analysis to be implemented in JavaScript.
+   * Multiple visitors can be composed into one using the `composeVisitors` function.
+   * For optimal performance, visitors should be as specific as possible about what types of values
+   * they care about so that JavaScript has to be called as little as possible.
+   */
+  visitor?: Visitor<never>
+}
+
+export interface TransformAttributeResult {
+  /** The transformed code. */
+  code: Uint8Array,
+  /** `@import` and `url()` dependencies, if enabled. */
+  dependencies: Dependency[] | void,
+  /** Warnings that occurred during compilation. */
+  warnings: Warning[]
+}
+
+/**
+ * Compiles a single CSS declaration list, such as an inline style attribute in HTML.
+ */
+export declare function transformStyleAttribute(options: TransformAttributeOptions): TransformAttributeResult;
+
+/**
+ * Converts a browserslist result into targets that can be passed to lightningcss.
+ * @param browserslist the result of calling `browserslist`
+ */
+export declare function browserslistToTargets(browserslist: string[]): Targets;
+
+/**
+ * Bundles a CSS file and its dependencies, inlining @import rules.
+ */
+export declare function bundle<C extends CustomAtRules>(options: BundleOptions<C>): TransformResult;
+
+/**
+ * Bundles a CSS file and its dependencies asynchronously, inlining @import rules.
+ */
+export declare function bundleAsync<C extends CustomAtRules>(options: BundleAsyncOptions<C>): Promise<TransformResult>;
+
+/**
+ * Composes multiple visitor objects into a single one.
+ */
+export declare function composeVisitors<C extends CustomAtRules>(visitors: Visitor<C>[]): Visitor<C>;
diff --git a/node/index.js b/node/index.js
new file mode 100644
index 0000000..011d04b
--- /dev/null
+++ b/node/index.js
@@ -0,0 +1,28 @@
+let parts = [process.platform, process.arch];
+if (process.platform === 'linux') {
+  const { MUSL, familySync } = require('detect-libc');
+  const family = familySync();
+  if (family === MUSL) {
+    parts.push('musl');
+  } else if (process.arch === 'arm') {
+    parts.push('gnueabihf');
+  } else {
+    parts.push('gnu');
+  }
+} else if (process.platform === 'win32') {
+  parts.push('msvc');
+}
+
+if (process.env.CSS_TRANSFORMER_WASM) {
+  module.exports = require(`../pkg`);
+} else {
+  try {
+    module.exports = require(`lightningcss-${parts.join('-')}`);
+  } catch (err) {
+    module.exports = require(`../lightningcss.${parts.join('-')}.node`);
+  }
+}
+
+module.exports.browserslistToTargets = require('./browserslistToTargets');
+module.exports.composeVisitors = require('./composeVisitors');
+module.exports.Features = require('./flags').Features;
diff --git a/node/index.mjs b/node/index.mjs
new file mode 100644
index 0000000..a1ff934
--- /dev/null
+++ b/node/index.mjs
@@ -0,0 +1,4 @@
+import index from './index.js';
+
+const { transform, transformStyleAttribute, bundle, bundleAsync, browserslistToTargets, composeVisitors, Features } = index;
+export { transform, transformStyleAttribute, bundle, bundleAsync, browserslistToTargets, composeVisitors, Features };
diff --git a/node/src/lib.rs b/node/src/lib.rs
new file mode 100644
index 0000000..e429b0f
--- /dev/null
+++ b/node/src/lib.rs
@@ -0,0 +1,77 @@
+#[cfg(target_os = "macos")]
+#[global_allocator]
+static GLOBAL: jemallocator::Jemalloc = jemallocator::Jemalloc;
+
+use napi::{CallContext, JsObject, JsUnknown};
+use napi_derive::{js_function, module_exports};
+
+#[js_function(1)]
+fn transform(ctx: CallContext) -> napi::Result<JsUnknown> {
+  lightningcss_napi::transform(ctx)
+}
+
+#[js_function(1)]
+fn transform_style_attribute(ctx: CallContext) -> napi::Result<JsUnknown> {
+  lightningcss_napi::transform_style_attribute(ctx)
+}
+
+#[js_function(1)]
+pub fn bundle(ctx: CallContext) -> napi::Result<JsUnknown> {
+  lightningcss_napi::bundle(ctx)
+}
+
+#[cfg(not(target_arch = "wasm32"))]
+#[js_function(1)]
+pub fn bundle_async(ctx: CallContext) -> napi::Result<JsObject> {
+  lightningcss_napi::bundle_async(ctx)
+}
+
+#[cfg_attr(not(target_arch = "wasm32"), module_exports)]
+fn init(mut exports: JsObject) -> napi::Result<()> {
+  exports.create_named_method("transform", transform)?;
+  exports.create_named_method("transformStyleAttribute", transform_style_attribute)?;
+  exports.create_named_method("bundle", bundle)?;
+  #[cfg(not(target_arch = "wasm32"))]
+  {
+    exports.create_named_method("bundleAsync", bundle_async)?;
+  }
+
+  Ok(())
+}
+
+#[cfg(target_arch = "wasm32")]
+#[no_mangle]
+pub fn register_module() {
+  unsafe fn register(raw_env: napi::sys::napi_env, raw_exports: napi::sys::napi_value) -> napi::Result<()> {
+    use napi::{Env, JsObject, NapiValue};
+
+    let env = Env::from_raw(raw_env);
+    let exports = JsObject::from_raw_unchecked(raw_env, raw_exports);
+    init(exports)
+  }
+
+  napi::bindgen_prelude::register_module_exports(register)
+}
+
+#[cfg(target_arch = "wasm32")]
+#[no_mangle]
+pub extern "C" fn napi_wasm_malloc(size: usize) -> *mut u8 {
+  use std::alloc::{alloc, Layout};
+  use std::mem;
+
+  let align = mem::align_of::<usize>();
+  if let Ok(layout) = Layout::from_size_align(size, align) {
+    unsafe {
+      if layout.size() > 0 {
+        let ptr = alloc(layout);
+        if !ptr.is_null() {
+          return ptr;
+        }
+      } else {
+        return align as *mut u8;
+      }
+    }
+  }
+
+  std::process::abort();
+}
diff --git a/node/targets.d.ts b/node/targets.d.ts
new file mode 100644
index 0000000..ccc7c95
--- /dev/null
+++ b/node/targets.d.ts
@@ -0,0 +1,40 @@
+// This file is autogenerated by build-prefixes.js. DO NOT EDIT!
+
+export interface Targets {
+  android?: number,
+  chrome?: number,
+  edge?: number,
+  firefox?: number,
+  ie?: number,
+  ios_saf?: number,
+  opera?: number,
+  safari?: number,
+  samsung?: number
+}
+
+export const Features: {
+  Nesting: 1,
+  NotSelectorList: 2,
+  DirSelector: 4,
+  LangSelectorList: 8,
+  IsSelector: 16,
+  TextDecorationThicknessPercent: 32,
+  MediaIntervalSyntax: 64,
+  MediaRangeSyntax: 128,
+  CustomMediaQueries: 256,
+  ClampFunction: 512,
+  ColorFunction: 1024,
+  OklabColors: 2048,
+  LabColors: 4096,
+  P3Colors: 8192,
+  HexAlphaColors: 16384,
+  SpaceSeparatedColorNotation: 32768,
+  FontFamilySystemUi: 65536,
+  DoublePositionGradients: 131072,
+  VendorPrefixes: 262144,
+  LogicalProperties: 524288,
+  LightDark: 1048576,
+  Selectors: 31,
+  MediaQueries: 448,
+  Colors: 1113088,
+};
diff --git a/node/test/bundle.test.mjs b/node/test/bundle.test.mjs
new file mode 100644
index 0000000..50d113b
--- /dev/null
+++ b/node/test/bundle.test.mjs
@@ -0,0 +1,417 @@
+import path from 'path';
+import fs from 'fs';
+import { test } from 'uvu';
+import * as assert from 'uvu/assert';
+import {webcrypto as crypto} from 'node:crypto';
+
+let bundleAsync;
+if (process.env.TEST_WASM === 'node') {
+  bundleAsync = (await import('../../wasm/wasm-node.mjs')).bundleAsync;
+} else if (process.env.TEST_WASM === 'browser') {
+  // Define crypto globally for old node.
+  // @ts-ignore
+  globalThis.crypto ??= crypto;
+  let wasm = await import('../../wasm/index.mjs');
+  await wasm.default();
+  bundleAsync = function (options) {
+    if (!options.resolver?.read) {
+      options.resolver = {
+        ...options.resolver,
+        read: (filePath) => fs.readFileSync(filePath, 'utf8')
+      };
+    }
+
+    return wasm.bundleAsync(options);
+  }
+} else {
+  bundleAsync = (await import('../index.mjs')).bundleAsync;
+}
+
+test('resolver', async () => {
+  const inMemoryFs = new Map(Object.entries({
+    'foo.css': `
+ @import 'root:bar.css';
+
+ .foo { color: red; }
+         `.trim(),
+
+    'bar.css': `
+ @import 'root:hello/world.css';
+
+ .bar { color: green; }
+         `.trim(),
+
+    'hello/world.css': `
+ .baz { color: blue; }
+         `.trim(),
+  }));
+
+  const { code: buffer } = await bundleAsync({
+    filename: 'foo.css',
+    resolver: {
+      read(file) {
+        const result = inMemoryFs.get(path.normalize(file));
+        if (!result) throw new Error(`Could not find ${file} in ${Array.from(inMemoryFs.keys()).join(', ')}.`);
+        return result;
+      },
+
+      resolve(specifier) {
+        return specifier.slice('root:'.length);
+      },
+    },
+  });
+  const code = buffer.toString('utf-8').trim();
+
+  const expected = `
+.baz {
+  color: #00f;
+}
+
+.bar {
+  color: green;
+}
+
+.foo {
+  color: red;
+}
+     `.trim();
+  if (code !== expected) throw new Error(`\`testResolver()\` failed. Expected:\n${expected}\n\nGot:\n${code}`);
+});
+
+test('only custom read', async () => {
+  const inMemoryFs = new Map(Object.entries({
+    'foo.css': `
+ @import 'hello/world.css';
+
+ .foo { color: red; }
+         `.trim(),
+
+    'hello/world.css': `
+ @import '../bar.css';
+
+ .bar { color: green; }
+         `.trim(),
+
+    'bar.css': `
+ .baz { color: blue; }
+         `.trim(),
+  }));
+
+  const { code: buffer } = await bundleAsync({
+    filename: 'foo.css',
+    resolver: {
+      read(file) {
+        const result = inMemoryFs.get(path.normalize(file));
+        if (!result) throw new Error(`Could not find ${file} in ${Array.from(inMemoryFs.keys()).join(', ')}.`);
+        return result;
+      },
+    },
+  });
+  const code = buffer.toString('utf-8').trim();
+
+  const expected = `
+.baz {
+  color: #00f;
+}
+
+.bar {
+  color: green;
+}
+
+.foo {
+  color: red;
+}
+     `.trim();
+  if (code !== expected) throw new Error(`\`testOnlyCustomRead()\` failed. Expected:\n${expected}\n\nGot:\n${code}`);
+});
+
+test('only custom resolve', async () => {
+  const root = path.join('tests', 'testdata');
+  const { code: buffer } = await bundleAsync({
+    filename: path.join(root, 'foo.css'),
+    resolver: {
+      resolve(specifier) {
+        // Strip `root:` prefix off specifier and resolve it as an absolute path
+        // in the test data root.
+        return path.join(root, specifier.slice('root:'.length));
+      },
+    },
+  });
+  const code = buffer.toString('utf-8').trim();
+
+  const expected = `
+.baz {
+  color: #00f;
+}
+
+.bar {
+  color: green;
+}
+
+.foo {
+  color: red;
+}
+     `.trim();
+  if (code !== expected) throw new Error(`\`testOnlyCustomResolve()\` failed. Expected:\n${expected}\n\nGot:\n${code}`);
+});
+
+test('async read', async () => {
+  const root = path.join('tests', 'testdata');
+  const { code: buffer } = await bundleAsync({
+    filename: path.join(root, 'foo.css'),
+    resolver: {
+      async read(file) {
+        return await fs.promises.readFile(file, 'utf8');
+      },
+      resolve(specifier) {
+        // Strip `root:` prefix off specifier and resolve it as an absolute path
+        // in the test data root.
+        return path.join(root, specifier.slice('root:'.length));
+      },
+    },
+  });
+  const code = buffer.toString('utf-8').trim();
+
+  const expected = `
+.baz {
+  color: #00f;
+}
+
+.bar {
+  color: green;
+}
+
+.foo {
+  color: red;
+}
+     `.trim();
+  if (code !== expected) throw new Error(`\`testAsyncRead()\` failed. Expected:\n${expected}\n\nGot:\n${code}`);
+});
+
+test('read throw', async () => {
+  let error = undefined;
+  try {
+    await bundleAsync({
+      filename: 'foo.css',
+      resolver: {
+        read(file) {
+          throw new Error(`Oh noes! Failed to read \`${file}\`.`);
+        }
+      },
+    });
+  } catch (err) {
+    error = err;
+  }
+
+  if (!error) throw new Error(`\`testReadThrow()\` failed. Expected \`bundleAsync()\` to throw, but it did not.`);
+  assert.equal(error.message, `Oh noes! Failed to read \`foo.css\`.`);
+  assert.equal(error.loc, undefined); // error occurred when reading initial file, no location info available.
+});
+
+test('async read throw', async () => {
+  let error = undefined;
+  try {
+    await bundleAsync({
+      filename: 'foo.css',
+      resolver: {
+        async read(file) {
+          throw new Error(`Oh noes! Failed to read \`${file}\`.`);
+        }
+      },
+    });
+  } catch (err) {
+    error = err;
+  }
+
+  if (!error) throw new Error(`\`testReadThrow()\` failed. Expected \`bundleAsync()\` to throw, but it did not.`);
+  assert.equal(error.message, `Oh noes! Failed to read \`foo.css\`.`);
+  assert.equal(error.loc, undefined); // error occurred when reading initial file, no location info available.
+});
+
+test('read throw with location info', async () => {
+  let error = undefined;
+  try {
+    await bundleAsync({
+      filename: 'foo.css',
+      resolver: {
+        read(file) {
+          if (file === 'foo.css') {
+            return '@import "bar.css"';
+          }
+          throw new Error(`Oh noes! Failed to read \`${file}\`.`);
+        }
+      },
+    });
+  } catch (err) {
+    error = err;
+  }
+
+  if (!error) throw new Error(`\`testReadThrow()\` failed. Expected \`bundleAsync()\` to throw, but it did not.`);
+  assert.equal(error.message, `Oh noes! Failed to read \`bar.css\`.`);
+  assert.equal(error.fileName, 'foo.css');
+  assert.equal(error.loc, {
+    line: 1,
+    column: 1
+  });
+});
+
+test('async read throw with location info', async () => {
+  let error = undefined;
+  try {
+    await bundleAsync({
+      filename: 'foo.css',
+      resolver: {
+        async read(file) {
+          if (file === 'foo.css') {
+            return '@import "bar.css"';
+          }
+          throw new Error(`Oh noes! Failed to read \`${file}\`.`);
+        }
+      },
+    });
+  } catch (err) {
+    error = err;
+  }
+
+  if (!error) throw new Error(`\`testReadThrow()\` failed. Expected \`bundleAsync()\` to throw, but it did not.`);
+  assert.equal(error.message, `Oh noes! Failed to read \`bar.css\`.`);
+  assert.equal(error.fileName, 'foo.css');
+  assert.equal(error.loc, {
+    line: 1,
+    column: 1
+  });
+});
+
+test('resolve throw', async () => {
+  let error = undefined;
+  try {
+    await bundleAsync({
+      filename: 'tests/testdata/foo.css',
+      resolver: {
+        resolve(specifier, originatingFile) {
+          throw new Error(`Oh noes! Failed to resolve \`${specifier}\` from \`${originatingFile}\`.`);
+        }
+      },
+    });
+  } catch (err) {
+    error = err;
+  }
+
+  if (!error) throw new Error(`\`testResolveThrow()\` failed. Expected \`bundleAsync()\` to throw, but it did not.`);
+  assert.equal(error.message, `Oh noes! Failed to resolve \`root:hello/world.css\` from \`tests/testdata/foo.css\`.`);
+  assert.equal(error.fileName, 'tests/testdata/foo.css');
+  assert.equal(error.loc, {
+    line: 1,
+    column: 1
+  });
+});
+
+test('async resolve throw', async () => {
+  let error = undefined;
+  try {
+    await bundleAsync({
+      filename: 'tests/testdata/foo.css',
+      resolver: {
+        async resolve(specifier, originatingFile) {
+          throw new Error(`Oh noes! Failed to resolve \`${specifier}\` from \`${originatingFile}\`.`);
+        }
+      },
+    });
+  } catch (err) {
+    error = err;
+  }
+
+  if (!error) throw new Error(`\`testResolveThrow()\` failed. Expected \`bundleAsync()\` to throw, but it did not.`);
+  assert.equal(error.message, `Oh noes! Failed to resolve \`root:hello/world.css\` from \`tests/testdata/foo.css\`.`);
+  assert.equal(error.fileName, 'tests/testdata/foo.css');
+  assert.equal(error.loc, {
+    line: 1,
+    column: 1
+  });
+});
+
+test('read return non-string', async () => {
+  let error = undefined;
+  try {
+    await bundleAsync({
+      filename: 'foo.css',
+      resolver: {
+        read() {
+          return 1234; // Returns a non-string value.
+        }
+      },
+    });
+  } catch (err) {
+    error = err;
+  }
+
+  if (!error) throw new Error(`\`testReadReturnNonString()\` failed. Expected \`bundleAsync()\` to throw, but it did not.`);
+  assert.equal(error.message, 'expect String, got: Number');
+});
+
+test('resolve return non-string', async () => {
+  let error = undefined;
+  try {
+    await bundleAsync({
+      filename: 'tests/testdata/foo.css',
+      resolver: {
+        resolve() {
+          return 1234; // Returns a non-string value.
+        }
+      },
+    });
+  } catch (err) {
+    error = err;
+  }
+
+  if (!error) throw new Error(`\`testResolveReturnNonString()\` failed. Expected \`bundleAsync()\` to throw, but it did not.`);
+  assert.equal(error.message, 'expect String, got: Number');
+  assert.equal(error.fileName, 'tests/testdata/foo.css');
+  assert.equal(error.loc, {
+    line: 1,
+    column: 1
+  });
+});
+
+test('should throw with location info on syntax errors', async () => {
+  let error = undefined;
+  try {
+    await bundleAsync({
+      filename: 'tests/testdata/foo.css',
+      resolver: {
+        read() {
+          return '.foo'
+        }
+      },
+    });
+  } catch (err) {
+    error = err;
+  }
+
+  assert.equal(error.message, `Unexpected end of input`);
+  assert.equal(error.fileName, 'tests/testdata/foo.css');
+  assert.equal(error.loc, {
+    line: 1,
+    column: 5
+  });
+});
+
+test('should support throwing in visitors', async () => {
+  let error = undefined;
+  try {
+    await bundleAsync({
+      filename: 'tests/testdata/a.css',
+      visitor: {
+        Rule() {
+          throw new Error('Some error')
+        }
+      }
+    });
+  } catch (err) {
+    error = err;
+  }
+
+  assert.equal(error.message, 'Some error');
+});
+
+test.run();
diff --git a/node/test/composeVisitors.test.mjs b/node/test/composeVisitors.test.mjs
new file mode 100644
index 0000000..7718ec0
--- /dev/null
+++ b/node/test/composeVisitors.test.mjs
@@ -0,0 +1,803 @@
+// @ts-check
+
+import { test } from 'uvu';
+import * as assert from 'uvu/assert';
+import {webcrypto as crypto} from 'node:crypto';
+
+let transform, composeVisitors;
+if (process.env.TEST_WASM === 'node') {
+  ({transform, composeVisitors} = await import('../../wasm/wasm-node.mjs'));
+} else if (process.env.TEST_WASM === 'browser') {
+  // Define crypto globally for old node.
+  // @ts-ignore
+  globalThis.crypto ??= crypto;
+  let wasm = await import('../../wasm/index.mjs');
+  await wasm.default();
+  ({transform, composeVisitors} = wasm);
+} else {
+  ({transform, composeVisitors} = await import('../index.mjs'));
+}
+
+test('different types', () => {
+  let res = transform({
+    filename: 'test.css',
+    minify: true,
+    code: Buffer.from(`
+      .foo {
+        width: 16px;
+        color: red;
+      }
+    `),
+    visitor: composeVisitors([
+      {
+        Length(l) {
+          if (l.unit === 'px') {
+            return {
+              unit: 'rem',
+              value: l.value / 16
+            }
+          }
+        }
+      },
+      {
+        Color(c) {
+          if (c.type === 'rgb') {
+            return {
+              type: 'rgb',
+              r: c.g,
+              g: c.r,
+              b: c.b,
+              alpha: c.alpha
+            };
+          }
+        }
+      }
+    ])
+  });
+
+  assert.equal(res.code.toString(), '.foo{color:#0f0;width:1rem}');
+});
+
+test('simple matching types', () => {
+  let res = transform({
+    filename: 'test.css',
+    minify: true,
+    code: Buffer.from(`
+      .foo {
+        width: 16px;
+      }
+    `),
+    visitor: composeVisitors([
+      {
+        Length(l) {
+          return {
+            unit: l.unit,
+            value: l.value * 2
+          };
+        }
+      },
+      {
+        Length(l) {
+          if (l.unit === 'px') {
+            return {
+              unit: 'rem',
+              value: l.value / 16
+            }
+          }
+        }
+      }
+    ])
+  });
+
+  assert.equal(res.code.toString(), '.foo{width:2rem}');
+});
+
+test('different properties', () => {
+  let res = transform({
+    filename: 'test.css',
+    minify: true,
+    code: Buffer.from(`
+      .foo {
+        size: 16px;
+        bg: #ff0;
+      }
+    `),
+    visitor: composeVisitors([
+      {
+        Declaration: {
+          custom: {
+            size(v) {
+              return [
+                { property: 'unparsed', value: { propertyId: { property: 'width' }, value: v.value } },
+                { property: 'unparsed', value: { propertyId: { property: 'height' }, value: v.value } }
+              ];
+            }
+          }
+        }
+      },
+      {
+        Declaration: {
+          custom: {
+            bg(v) {
+              if (v.value[0].type === 'color') {
+                return { property: 'background-color', value: v.value[0].value };
+              }
+            }
+          }
+        }
+      }
+    ])
+  });
+
+  assert.equal(res.code.toString(), '.foo{width:16px;height:16px;background-color:#ff0}');
+});
+
+test('composed properties', () => {
+  /** @type {import('../index').Visitor[]} */
+  let visitors = [
+    {
+      Declaration: {
+        custom: {
+          size(v) {
+            if (v.value[0].type === 'length') {
+              return [
+                { property: 'width', value: { type: 'length-percentage', value: { type: 'dimension', value: v.value[0].value } } },
+                { property: 'height', value: { type: 'length-percentage', value: { type: 'dimension', value: v.value[0].value } } },
+              ];
+            }
+          }
+        }
+      }
+    },
+    {
+      Declaration: {
+        width() {
+          return [];
+        }
+      }
+    }
+  ];
+
+  // Check that it works in any order.
+  for (let i = 0; i < 2; i++) {
+    let res = transform({
+      filename: 'test.css',
+      minify: true,
+      code: Buffer.from(`
+        .foo {
+          size: 16px;
+        }
+      `),
+      visitor: composeVisitors(visitors)
+    });
+
+    assert.equal(res.code.toString(), '.foo{height:16px}');
+    visitors.reverse();
+  }
+});
+
+test('same properties', () => {
+  let res = transform({
+    filename: 'test.css',
+    minify: true,
+    code: Buffer.from(`
+      .foo {
+        color: red;
+      }
+    `),
+    visitor: composeVisitors([
+      {
+        Declaration: {
+          color(v) {
+            if (v.property === 'color' && v.value.type === 'rgb') {
+              return {
+                property: 'color',
+                value: {
+                  type: 'rgb',
+                  r: v.value.g,
+                  g: v.value.r,
+                  b: v.value.b,
+                  alpha: v.value.alpha
+                }
+              };
+            }
+          }
+        }
+      },
+      {
+        Declaration: {
+          color(v) {
+            if (v.property === 'color' && v.value.type === 'rgb' && v.value.g > 0) {
+              v.value.alpha /= 2;
+            }
+            return v;
+          }
+        }
+      }
+    ])
+  });
+
+  assert.equal(res.code.toString(), '.foo{color:#00ff0080}');
+});
+
+test('properties plus values', () => {
+  let res = transform({
+    filename: 'test.css',
+    minify: true,
+    code: Buffer.from(`
+      .foo {
+        size: test;
+      }
+    `),
+    visitor: composeVisitors([
+      {
+        Declaration: {
+          custom: {
+            size() {
+              return [
+                { property: 'width', value: { type: 'length-percentage', value: { type: 'dimension', value: { unit: 'px', value: 32 } } } },
+                { property: 'height', value: { type: 'length-percentage', value: { type: 'dimension', value: { unit: 'px', value: 32 } } } },
+              ];
+            }
+          }
+        }
+      },
+      {
+        Length(l) {
+          if (l.unit === 'px') {
+            return {
+              unit: 'rem',
+              value: l.value / 16
+            }
+          }
+        }
+      }
+    ])
+  });
+
+  assert.equal(res.code.toString(), '.foo{width:2rem;height:2rem}');
+});
+
+test('unparsed properties', () => {
+  let res = transform({
+    filename: 'test.css',
+    minify: true,
+    code: Buffer.from(`
+      .foo {
+        width: test;
+      }
+      .bar {
+        width: 16px;
+      }
+    `),
+    visitor: composeVisitors([
+      {
+        Declaration: {
+          width(v) {
+            if (v.property === 'unparsed') {
+              return [
+                { property: 'width', value: { type: 'length-percentage', value: { type: 'dimension', value: { unit: 'px', value: 32 } } } },
+                { property: 'height', value: { type: 'length-percentage', value: { type: 'dimension', value: { unit: 'px', value: 32 } } } },
+              ];
+            }
+          }
+        }
+      },
+      {
+        Length(l) {
+          if (l.unit === 'px') {
+            return {
+              unit: 'rem',
+              value: l.value / 16
+            }
+          }
+        }
+      }
+    ])
+  });
+
+  assert.equal(res.code.toString(), '.foo{width:2rem;height:2rem}.bar{width:1rem}');
+});
+
+test('returning unparsed properties', () => {
+  let res = transform({
+    filename: 'test.css',
+    minify: true,
+    code: Buffer.from(`
+      .foo {
+        width: test;
+      }
+    `),
+    visitor: composeVisitors([
+      {
+        Declaration: {
+          width(v) {
+            if (v.property === 'unparsed' && v.value.value[0].type === 'token' && v.value.value[0].value.type === 'ident') {
+              return {
+                property: 'unparsed',
+                value: {
+                  propertyId: { property: 'width' },
+                  value: [{
+                    type: 'var',
+                    value: {
+                      name: {
+                        ident: '--' + v.value.value[0].value.value
+                      }
+                    }
+                  }]
+                }
+              }
+            }
+          }
+        }
+      },
+      {
+        Declaration: {
+          width(v) {
+            if (v.property === 'unparsed') {
+              return {
+                property: 'unparsed',
+                value: {
+                  propertyId: { property: 'width' },
+                  value: [{
+                    type: 'function',
+                    value: {
+                      name: 'calc',
+                      arguments: v.value.value
+                    }
+                  }]
+                }
+              }
+            }
+          }
+        }
+      }
+    ])
+  });
+
+  assert.equal(res.code.toString(), '.foo{width:calc(var(--test))}');
+});
+
+test('all property handlers', () => {
+  let res = transform({
+    filename: 'test.css',
+    minify: true,
+    code: Buffer.from(`
+      .foo {
+        width: test;
+        height: test;
+      }
+    `),
+    visitor: composeVisitors([
+      {
+        Declaration(decl) {
+          if (decl.property === 'unparsed' && decl.value.propertyId.property === 'width') {
+            return { property: 'width', value: { type: 'length-percentage', value: { type: 'dimension', value: { unit: 'px', value: 32 } } } };
+          }
+        }
+      },
+      {
+        Declaration(decl) {
+          if (decl.property === 'unparsed' && decl.value.propertyId.property === 'height') {
+            return { property: 'height', value: { type: 'length-percentage', value: { type: 'dimension', value: { unit: 'px', value: 32 } } } };
+          }
+        }
+      }
+    ])
+  });
+
+  assert.equal(res.code.toString(), '.foo{width:32px;height:32px}');
+});
+
+test('all property handlers (exit)', () => {
+  let res = transform({
+    filename: 'test.css',
+    minify: true,
+    code: Buffer.from(`
+      .foo {
+        width: test;
+        height: test;
+      }
+    `),
+    visitor: composeVisitors([
+      {
+        DeclarationExit(decl) {
+          if (decl.property === 'unparsed' && decl.value.propertyId.property === 'width') {
+            return { property: 'width', value: { type: 'length-percentage', value: { type: 'dimension', value: { unit: 'px', value: 32 } } } };
+          }
+        }
+      },
+      {
+        DeclarationExit(decl) {
+          if (decl.property === 'unparsed' && decl.value.propertyId.property === 'height') {
+            return { property: 'height', value: { type: 'length-percentage', value: { type: 'dimension', value: { unit: 'px', value: 32 } } } };
+          }
+        }
+      }
+    ])
+  });
+
+  assert.equal(res.code.toString(), '.foo{width:32px;height:32px}');
+});
+
+test('tokens and functions', () => {
+  let res = transform({
+    filename: 'test.css',
+    minify: true,
+    code: Buffer.from(`
+      .foo {
+        width: f3(f2(f1(test)));
+      }
+    `),
+    visitor: composeVisitors([
+      {
+        FunctionExit: {
+          f1(f) {
+            if (f.arguments.length === 1 && f.arguments[0].type === 'token' && f.arguments[0].value.type === 'ident') {
+              return {
+                type: 'length',
+                value: {
+                  unit: 'px',
+                  value: 32
+                }
+              }
+            }
+          }
+        }
+      },
+      {
+        FunctionExit(f) {
+          return f.arguments[0];
+        }
+      },
+      {
+        Length(l) {
+          if (l.unit === 'px') {
+            return {
+              unit: 'rem',
+              value: l.value / 16
+            }
+          }
+        }
+      }
+    ])
+  });
+
+  assert.equal(res.code.toString(), '.foo{width:2rem}');
+});
+
+test('unknown rules', () => {
+  let declared = new Map();
+  let res = transform({
+    filename: 'test.css',
+    minify: true,
+    code: Buffer.from(`
+      @test #056ef0;
+
+      .menu_link {
+        background: @blue;
+      }
+    `),
+    visitor: composeVisitors([
+      {
+        Rule: {
+          unknown: {
+            test(rule) {
+              rule.name = 'blue';
+              return {
+                type: 'unknown',
+                value: rule
+              };
+            }
+          }
+        }
+      },
+      {
+        Rule: {
+          unknown(rule) {
+            declared.set(rule.name, rule.prelude);
+            return [];
+          }
+        },
+        Token: {
+          'at-keyword'(token) {
+            if (declared.has(token.value)) {
+              return declared.get(token.value);
+            }
+          }
+        }
+      }
+    ])
+  });
+
+  assert.equal(res.code.toString(), '.menu_link{background:#056ef0}');
+});
+
+test('custom at rules', () => {
+  let res = transform({
+    filename: 'test.css',
+    minify: true,
+    code: Buffer.from(`
+      @testA;
+      @testB;
+    `),
+    customAtRules: {
+      testA: {},
+      testB: {}
+    },
+    visitor: composeVisitors([
+      {
+        Rule: {
+          custom: {
+            testA(rule) {
+              return {
+                type: 'style',
+                value: {
+                  loc: rule.loc,
+                  selectors: [
+                    [{ type: 'class', name: 'testA' }]
+                  ],
+                  declarations: {
+                    declarations: [
+                      {
+                        property: 'color',
+                        value: {
+                          type: 'rgb',
+                          r: 0xff,
+                          g: 0x00,
+                          b: 0x00,
+                          alpha: 1,
+                        }
+                      }
+                    ]
+                  }
+                }
+              };
+            }
+          }
+        }
+      },
+      {
+        Rule: {
+          custom: {
+            testB(rule) {
+              return {
+                type: 'style',
+                value: {
+                  loc: rule.loc,
+                  selectors: [
+                    [{ type: 'class', name: 'testB' }]
+                  ],
+                  declarations: {
+                    declarations: [
+                      {
+                        property: 'color',
+                        value: {
+                          type: 'rgb',
+                          r: 0x00,
+                          g: 0xff,
+                          b: 0x00,
+                          alpha: 1,
+                        }
+                      }
+                    ]
+                  }
+                }
+              };
+            }
+          }
+        }
+      }
+    ])
+  });
+
+  assert.equal(res.code.toString(), '.testA{color:red}.testB{color:#0f0}');
+});
+
+test('known rules', () => {
+  let declared = new Map();
+  let res = transform({
+    filename: 'test.css',
+    minify: true,
+    code: Buffer.from(`
+      .test:focus-visible {
+        margin-left: 20px;
+        margin-right: @margin-left;
+      }
+    `),
+    targets: {
+      safari: 14 << 16
+    },
+    visitor: composeVisitors([
+      {
+        Rule: {
+          style(rule) {
+            let valuesByProperty = new Map();
+            for (let decl of rule.value.declarations.declarations) {
+              /** @type string */
+              let name = decl.property;
+              if (decl.property === 'unparsed') {
+                name = decl.value.propertyId.property;
+              }
+              valuesByProperty.set(name, decl);
+            }
+
+            rule.value.declarations.declarations = rule.value.declarations.declarations.map(decl => {
+              // Only single value supported. Would need a way to convert parsed values to unparsed tokens otherwise.
+              if (decl.property === 'unparsed' && decl.value.value.length === 1) {
+                let token = decl.value.value[0];
+                if (token.type === 'token' && token.value.type === 'at-keyword' && valuesByProperty.has(token.value.value)) {
+                  let v = valuesByProperty.get(token.value.value);
+                  return {
+                    /** @type any */
+                    property: decl.value.propertyId.property,
+                    value: v.value
+                  };
+                }
+              }
+              return decl;
+            });
+
+            return rule;
+          }
+        }
+      },
+      {
+        Rule: {
+          style(rule) {
+            let clone = null;
+            for (let selector of rule.value.selectors) {
+              for (let [i, component] of selector.entries()) {
+                if (component.type === 'pseudo-class' && component.kind === 'focus-visible') {
+                  if (clone == null) {
+                    clone = [...rule.value.selectors.map(s => [...s])];
+                  }
+
+                  selector[i] = { type: 'class', name: 'focus-visible' };
+                }
+              }
+            }
+
+            if (clone) {
+              return [rule, { type: 'style', value: { ...rule.value, selectors: clone } }];
+            }
+          }
+        }
+      }
+    ])
+  });
+
+  assert.equal(res.code.toString(), '.test.focus-visible{margin-left:20px;margin-right:20px}.test:focus-visible{margin-left:20px;margin-right:20px}');
+});
+
+test('environment variables', () => {
+  /** @type {Record<string, import('../ast').TokenOrValue>} */
+  let tokens = {
+    '--branding-small': {
+      type: 'length',
+      value: {
+        unit: 'px',
+        value: 600
+      }
+    },
+    '--branding-padding': {
+      type: 'length',
+      value: {
+        unit: 'px',
+        value: 20
+      }
+    }
+  };
+
+  let res = transform({
+    filename: 'test.css',
+    minify: true,
+    errorRecovery: true,
+    code: Buffer.from(`
+      @media (max-width: env(--branding-small)) {
+        body {
+          padding: env(--branding-padding);
+        }
+      }
+    `),
+    visitor: composeVisitors([
+      {
+        EnvironmentVariable: {
+          '--branding-small': () => tokens['--branding-small']
+        }
+      },
+      {
+        EnvironmentVariable: {
+          '--branding-padding': () => tokens['--branding-padding']
+        }
+      }
+    ])
+  });
+
+  assert.equal(res.code.toString(), '@media (width<=600px){body{padding:20px}}');
+});
+
+test('variables', () => {
+  /** @type {Record<string, import('../ast').TokenOrValue>} */
+  let tokens = {
+    '--branding-small': {
+      type: 'length',
+      value: {
+        unit: 'px',
+        value: 600
+      }
+    },
+    '--branding-padding': {
+      type: 'length',
+      value: {
+        unit: 'px',
+        value: 20
+      }
+    }
+  };
+
+  let res = transform({
+    filename: 'test.css',
+    minify: true,
+    errorRecovery: true,
+    code: Buffer.from(`
+      body {
+        padding: var(--branding-padding);
+        width: var(--branding-small);
+      }
+    `),
+    visitor: composeVisitors([
+      {
+        Variable(v) {
+          if (v.name.ident === '--branding-small') {
+            return tokens['--branding-small'];
+          }
+        }
+      },
+      {
+        Variable(v) {
+          if (v.name.ident === '--branding-padding') {
+            return tokens['--branding-padding'];
+          }
+        }
+      }
+    ])
+  });
+
+  assert.equal(res.code.toString(), 'body{padding:20px;width:600px}');
+});
+
+test('StyleSheet', () => {
+  let styleSheetCalledCount = 0;
+  let styleSheetExitCalledCount = 0;
+  transform({
+    filename: 'test.css',
+    code: Buffer.from(`
+      body {
+        color: blue;
+      }
+    `),
+    visitor: composeVisitors([
+      {
+        StyleSheet() {
+          styleSheetCalledCount++
+        },
+        StyleSheetExit() {
+          styleSheetExitCalledCount++
+        }
+      },
+      {
+        StyleSheet() {
+          styleSheetCalledCount++
+        },
+        StyleSheetExit() {
+          styleSheetExitCalledCount++
+        }
+      }
+    ])
+  });
+  assert.equal(styleSheetCalledCount, 2);
+  assert.equal(styleSheetExitCalledCount, 2);
+});
+
+test.run();
diff --git a/node/test/customAtRules.mjs b/node/test/customAtRules.mjs
new file mode 100644
index 0000000..e53f65a
--- /dev/null
+++ b/node/test/customAtRules.mjs
@@ -0,0 +1,317 @@
+// @ts-check
+
+import { test } from 'uvu';
+import * as assert from 'uvu/assert';
+import fs from 'fs';
+import {webcrypto as crypto} from 'node:crypto';
+
+let bundle, transform;
+if (process.env.TEST_WASM === 'node') {
+  ({ bundle, transform } = await import('../../wasm/wasm-node.mjs'));
+} else if (process.env.TEST_WASM === 'browser') {
+  // Define crypto globally for old node.
+  // @ts-ignore
+  globalThis.crypto ??= crypto;
+  let wasm = await import('../../wasm/index.mjs');
+  await wasm.default();
+  transform = wasm.transform;
+  bundle = function(options) {
+    return wasm.bundle({
+      ...options,
+      resolver: {
+        read: (filePath) => fs.readFileSync(filePath, 'utf8')
+      }
+    });
+  }
+} else {
+  ({bundle, transform} = await import('../index.mjs'));
+}
+
+test('declaration list', () => {
+  let definitions = new Map();
+  let res = transform({
+    filename: 'test.css',
+    minify: true,
+    code: Buffer.from(`
+      @theme spacing {
+        foo: 16px;
+        bar: 32px;
+      }
+
+      .foo {
+        width: theme('spacing.foo');
+      }
+    `),
+    customAtRules: {
+      theme: {
+        prelude: '<custom-ident>',
+        body: 'declaration-list'
+      }
+    },
+    visitor: {
+      Rule: {
+        custom: {
+          theme(rule) {
+            for (let decl of rule.body.value.declarations) {
+              if (decl.property === 'custom') {
+                definitions.set(rule.prelude.value + '.' + decl.value.name, decl.value.value);
+              }
+            }
+            return [];
+          }
+        }
+      },
+      Function: {
+        theme(f) {
+          if (f.arguments[0].type === 'token' && f.arguments[0].value.type === 'string') {
+            return definitions.get(f.arguments[0].value.value);
+          }
+        }
+      }
+    }
+  });
+
+  assert.equal(res.code.toString(), '.foo{width:16px}');
+});
+
+test('mixin', () => {
+  let mixins = new Map();
+  let res = transform({
+    filename: 'test.css',
+    minify: true,
+    code: Buffer.from(`
+      @mixin color {
+        color: red;
+
+        &.bar {
+          color: yellow;
+        }
+      }
+
+      .foo {
+        @apply color;
+      }
+    `),
+    targets: { chrome: 100 << 16 },
+    customAtRules: {
+      mixin: {
+        prelude: '<custom-ident>',
+        body: 'style-block'
+      },
+      apply: {
+        prelude: '<custom-ident>'
+      }
+    },
+    visitor: {
+      Rule: {
+        custom: {
+          mixin(rule) {
+            mixins.set(rule.prelude.value, rule.body.value);
+            return [];
+          },
+          apply(rule) {
+            return mixins.get(rule.prelude.value);
+          }
+        }
+      }
+    }
+  });
+
+  assert.equal(res.code.toString(), '.foo{color:red}.foo.bar{color:#ff0}');
+});
+
+test('rule list', () => {
+  let res = transform({
+    filename: 'test.css',
+    minify: true,
+    code: Buffer.from(`
+      @breakpoint 1024px {
+        .foo { color: yellow; }
+      }
+    `),
+    customAtRules: {
+      breakpoint: {
+        prelude: '<length>',
+        body: 'rule-list'
+      }
+    },
+    visitor: {
+      Rule: {
+        custom: {
+          breakpoint(rule) {
+            return {
+              type: 'media',
+              value: {
+                query: {
+                  mediaQueries: [{ mediaType: 'all', condition: { type: 'feature', value: { type: 'range', name: 'width', operator: 'less-than-equal', value: rule.prelude } } }]
+                },
+                rules: rule.body.value,
+                loc: rule.loc
+              }
+            }
+          }
+        }
+      }
+    }
+  });
+
+  assert.equal(res.code.toString(), '@media (width<=1024px){.foo{color:#ff0}}');
+});
+
+
+test('style block', () => {
+  let res = transform({
+    filename: 'test.css',
+    minify: true,
+    code: Buffer.from(`
+      .foo {
+        @breakpoint 1024px {
+          color: yellow;
+
+          &.bar {
+            color: red;
+          }
+        }
+      }
+    `),
+    targets: {
+      chrome: 105 << 16
+    },
+    customAtRules: {
+      breakpoint: {
+        prelude: '<length>',
+        body: 'style-block'
+      }
+    },
+    visitor: {
+      Rule: {
+        custom: {
+          breakpoint(rule) {
+            return {
+              type: 'media',
+              value: {
+                query: {
+                  mediaQueries: [{ mediaType: 'all', condition: { type: 'feature', value: { type: 'range', name: 'width', operator: 'less-than-equal', value: rule.prelude } } }]
+                },
+                rules: rule.body.value,
+                loc: rule.loc
+              }
+            }
+          }
+        }
+      }
+    }
+  });
+
+  assert.equal(res.code.toString(), '@media (width<=1024px){.foo{color:#ff0}.foo.bar{color:red}}');
+});
+
+test('style block top level', () => {
+  let res = transform({
+    filename: 'test.css',
+    minify: true,
+    code: Buffer.from(`
+      @test {
+        .foo {
+          background: black;
+        }
+      }
+    `),
+    customAtRules: {
+      test: {
+        body: 'style-block'
+      }
+    }
+  });
+
+  assert.equal(res.code.toString(), '@test{.foo{background:#000}}');
+});
+
+test('multiple', () => {
+  let res = transform({
+    filename: 'test.css',
+    minify: true,
+    code: Buffer.from(`
+      @breakpoint 1024px {
+        @theme spacing {
+          foo: 16px;
+          bar: 32px;
+        }
+      }
+    `),
+    customAtRules: {
+      breakpoint: {
+        prelude: '<length>',
+        body: 'rule-list'
+      },
+      theme: {
+        prelude: '<custom-ident>',
+        body: 'declaration-list'
+      }
+    },
+    visitor: {
+      Rule: {
+        custom(rule) {
+          if (rule.name === 'breakpoint') {
+            return {
+              type: 'media',
+              value: {
+                query: {
+                  mediaQueries: [{ mediaType: 'all', condition: { type: 'feature', value: { type: 'range', name: 'width', operator: 'less-than-equal', value: rule.prelude } } }]
+                },
+                rules: rule.body.value,
+                loc: rule.loc
+              }
+            }
+          } else {
+            return {
+              type: 'style',
+              value: {
+                selectors: [[{ type: 'pseudo-class', kind: 'root' }]],
+                declarations: rule.body.value,
+                loc: rule.loc
+              }
+            }
+          }
+        }
+      }
+    }
+  });
+
+  assert.equal(res.code.toString(), '@media (width<=1024px){:root{foo:16px;bar:32px}}');
+});
+
+test('bundler', () => {
+  let mixins = new Map();
+  let res = bundle({
+    filename: 'tests/testdata/apply.css',
+    minify: true,
+    targets: { chrome: 100 << 16 },
+    customAtRules: {
+      mixin: {
+        prelude: '<custom-ident>',
+        body: 'style-block'
+      },
+      apply: {
+        prelude: '<custom-ident>'
+      }
+    },
+    visitor: {
+      Rule: {
+        custom: {
+          mixin(rule) {
+            mixins.set(rule.prelude.value, rule.body.value);
+            return [];
+          },
+          apply(rule) {
+            return mixins.get(rule.prelude.value);
+          }
+        }
+      }
+    }
+  });
+
+  assert.equal(res.code.toString(), '.foo{color:red}.foo.bar{color:#ff0}');
+});
+
+test.run();
diff --git a/node/test/transform.test.mjs b/node/test/transform.test.mjs
new file mode 100644
index 0000000..1b56bbc
--- /dev/null
+++ b/node/test/transform.test.mjs
@@ -0,0 +1,71 @@
+import { test } from 'uvu';
+import * as assert from 'uvu/assert';
+import {webcrypto as crypto} from 'node:crypto';
+
+let transform, Features;
+if (process.env.TEST_WASM === 'node') {
+  ({transform, Features} = await import('../../wasm/wasm-node.mjs'));
+} else if (process.env.TEST_WASM === 'browser') {
+  // Define crypto globally for old node.
+  // @ts-ignore
+  globalThis.crypto ??= crypto;
+  let wasm = await import('../../wasm/index.mjs');
+  await wasm.default();
+  ({transform, Features} = wasm);
+} else {
+  ({transform, Features} = await import('../index.mjs'));
+}
+
+test('can enable non-standard syntax', () => {
+  let res = transform({
+    filename: 'test.css',
+    code: Buffer.from('.foo >>> .bar { color: red }'),
+    nonStandard: {
+      deepSelectorCombinator: true
+    },
+    minify: true
+  });
+
+  assert.equal(res.code.toString(), '.foo>>>.bar{color:red}');
+});
+
+test('can enable features without targets', () => {
+  let res = transform({
+    filename: 'test.css',
+    code: Buffer.from('.foo { .bar { color: red }}'),
+    minify: true,
+    include: Features.Nesting
+  });
+
+  assert.equal(res.code.toString(), '.foo .bar{color:red}');
+});
+
+test('can disable features', () => {
+  let res = transform({
+    filename: 'test.css',
+    code: Buffer.from('.foo { color: lch(50.998% 135.363 338) }'),
+    minify: true,
+    targets: {
+      chrome: 80 << 16
+    },
+    exclude: Features.Colors
+  });
+
+  assert.equal(res.code.toString(), '.foo{color:lch(50.998% 135.363 338)}');
+});
+
+test('can disable prefixing', () => {
+  let res = transform({
+    filename: 'test.css',
+    code: Buffer.from('.foo { user-select: none }'),
+    minify: true,
+    targets: {
+      safari: 15 << 16
+    },
+    exclude: Features.VendorPrefixes
+  });
+
+  assert.equal(res.code.toString(), '.foo{user-select:none}');
+});
+
+test.run();
diff --git a/node/test/visitor.test.mjs b/node/test/visitor.test.mjs
new file mode 100644
index 0000000..3a42a69
--- /dev/null
+++ b/node/test/visitor.test.mjs
@@ -0,0 +1,1111 @@
+// @ts-check
+
+import { test } from 'uvu';
+import * as assert from 'uvu/assert';
+import fs from 'fs';
+import {webcrypto as crypto} from 'node:crypto';
+
+let bundle, bundleAsync, transform, transformStyleAttribute;
+if (process.env.TEST_WASM === 'node') {
+  ({ bundle, bundleAsync, transform, transformStyleAttribute } = await import('../../wasm/wasm-node.mjs'));
+} else if (process.env.TEST_WASM === 'browser') {
+  // Define crypto globally for old node.
+  // @ts-ignore
+  globalThis.crypto ??= crypto;
+  let wasm = await import('../../wasm/index.mjs');
+  await wasm.default();
+  ({ transform, transformStyleAttribute } = wasm);
+  bundle = function(options) {
+    return wasm.bundle({
+      ...options,
+      resolver: {
+        read: (filePath) => fs.readFileSync(filePath, 'utf8')
+      }
+    });
+  }
+
+  bundleAsync = function (options) {
+    if (!options.resolver?.read) {
+      options.resolver = {
+        ...options.resolver,
+        read: (filePath) => fs.readFileSync(filePath, 'utf8')
+      };
+    }
+
+    return wasm.bundleAsync(options);
+  }
+} else {
+  ({ bundle, bundleAsync, transform, transformStyleAttribute } = await import('../index.mjs'));
+}
+
+test('px to rem', () => {
+  // Similar to https://github.com/cuth/postcss-pxtorem
+  let res = transform({
+    filename: 'test.css',
+    minify: true,
+    code: Buffer.from(`
+      .foo {
+        width: 32px;
+        height: calc(100vh - 64px);
+        --custom: calc(var(--foo) + 32px);
+      }
+    `),
+    visitor: {
+      Length(length) {
+        if (length.unit === 'px') {
+          return {
+            unit: 'rem',
+            value: length.value / 16
+          };
+        }
+      }
+    }
+  });
+
+  assert.equal(res.code.toString(), '.foo{--custom:calc(var(--foo) + 2rem);width:2rem;height:calc(100vh - 4rem)}');
+});
+
+test('custom units', () => {
+  // https://github.com/csstools/custom-units
+  let res = transform({
+    filename: 'test.css',
+    minify: true,
+    code: Buffer.from(`
+      .foo {
+        --step: .25rem;
+        font-size: 3--step;
+      }
+    `),
+    visitor: {
+      Token: {
+        dimension(token) {
+          if (token.unit.startsWith('--')) {
+            return {
+              type: 'function',
+              value: {
+                name: 'calc',
+                arguments: [
+                  {
+                    type: 'token',
+                    value: {
+                      type: 'number',
+                      value: token.value
+                    }
+                  },
+                  {
+                    type: 'token',
+                    value: {
+                      type: 'delim',
+                      value: '*'
+                    }
+                  },
+                  {
+                    type: 'var',
+                    value: {
+                      name: {
+                        ident: token.unit
+                      }
+                    }
+                  }
+                ]
+              }
+            }
+          }
+        }
+      }
+    }
+  });
+
+  assert.equal(res.code.toString(), '.foo{--step:.25rem;font-size:calc(3*var(--step))}');
+});
+
+test('design tokens', () => {
+  // Similar to https://www.npmjs.com/package/@csstools/postcss-design-tokens
+  let tokens = {
+    'color.background.primary': {
+      type: 'color',
+      value: {
+        type: 'rgb',
+        r: 255,
+        g: 0,
+        b: 0,
+        alpha: 1
+      }
+    },
+    'size.spacing.small': {
+      type: 'length',
+      value: {
+        unit: 'px',
+        value: 16
+      }
+    }
+  };
+
+  let res = transform({
+    filename: 'test.css',
+    minify: true,
+    code: Buffer.from(`
+      .foo {
+        color: design-token('color.background.primary');
+        padding: design-token('size.spacing.small');
+      }
+    `),
+    visitor: {
+      Function: {
+        'design-token'(fn) {
+          if (fn.arguments.length === 1 && fn.arguments[0].type === 'token' && fn.arguments[0].value.type === 'string') {
+            return tokens[fn.arguments[0].value.value];
+          }
+        }
+      }
+    }
+  });
+
+  assert.equal(res.code.toString(), '.foo{color:red;padding:16px}');
+});
+
+test('env function', () => {
+  // https://www.npmjs.com/package/postcss-env-function
+  /** @type {Record<string, import('../ast').TokenOrValue>} */
+  let tokens = {
+    '--branding-small': {
+      type: 'length',
+      value: {
+        unit: 'px',
+        value: 600
+      }
+    },
+    '--branding-padding': {
+      type: 'length',
+      value: {
+        unit: 'px',
+        value: 20
+      }
+    }
+  };
+
+  let res = transform({
+    filename: 'test.css',
+    minify: true,
+    errorRecovery: true,
+    code: Buffer.from(`
+      @media (max-width: env(--branding-small)) {
+        body {
+          padding: env(--branding-padding);
+        }
+      }
+    `),
+    visitor: {
+      EnvironmentVariable(env) {
+        if (env.name.type === 'custom') {
+          return tokens[env.name.ident];
+        }
+      }
+    }
+  });
+
+  assert.equal(res.code.toString(), '@media (width<=600px){body{padding:20px}}');
+});
+
+test('specific environment variables', () => {
+  // https://www.npmjs.com/package/postcss-env-function
+  /** @type {Record<string, import('../ast').TokenOrValue>} */
+  let tokens = {
+    '--branding-small': {
+      type: 'length',
+      value: {
+        unit: 'px',
+        value: 600
+      }
+    },
+    '--branding-padding': {
+      type: 'length',
+      value: {
+        unit: 'px',
+        value: 20
+      }
+    }
+  };
+
+  let res = transform({
+    filename: 'test.css',
+    minify: true,
+    errorRecovery: true,
+    code: Buffer.from(`
+      @media (max-width: env(--branding-small)) {
+        body {
+          padding: env(--branding-padding);
+        }
+      }
+    `),
+    visitor: {
+      EnvironmentVariable: {
+        '--branding-small': () => tokens['--branding-small'],
+        '--branding-padding': () => tokens['--branding-padding']
+      }
+    }
+  });
+
+  assert.equal(res.code.toString(), '@media (width<=600px){body{padding:20px}}');
+});
+
+test('url', () => {
+  // https://www.npmjs.com/package/postcss-url
+  let res = transform({
+    filename: 'test.css',
+    minify: true,
+    code: Buffer.from(`
+      .foo {
+        background: url(foo.png);
+      }
+    `),
+    visitor: {
+      Url(url) {
+        url.url = 'https://mywebsite.com/' + url.url;
+        return url;
+      }
+    }
+  });
+
+  assert.equal(res.code.toString(), '.foo{background:url(https://mywebsite.com/foo.png)}');
+});
+
+test('static vars', () => {
+  // Similar to https://www.npmjs.com/package/postcss-simple-vars
+  let declared = new Map();
+  let res = transform({
+    filename: 'test.css',
+    minify: true,
+    code: Buffer.from(`
+      @blue #056ef0;
+
+      .menu_link {
+        background: @blue;
+      }
+    `),
+    visitor: {
+      Rule: {
+        unknown(rule) {
+          declared.set(rule.name, rule.prelude);
+          return [];
+        }
+      },
+      Token: {
+        'at-keyword'(token) {
+          if (declared.has(token.value)) {
+            return declared.get(token.value);
+          }
+        }
+      }
+    }
+  });
+
+  assert.equal(res.code.toString(), '.menu_link{background:#056ef0}');
+});
+
+test('selector prefix', () => {
+  // Similar to https://www.npmjs.com/package/postcss-prefix-selector
+  let res = transform({
+    filename: 'test.css',
+    minify: true,
+    code: Buffer.from(`
+      .a, .b {
+        color: red;
+      }
+    `),
+    visitor: {
+      Selector(selector) {
+        return [{ type: 'class', name: 'prefix' }, { type: 'combinator', value: 'descendant' }, ...selector];
+      }
+    }
+  });
+
+  assert.equal(res.code.toString(), '.prefix .a,.prefix .b{color:red}');
+});
+
+test('apply', () => {
+  // Similar to https://www.npmjs.com/package/postcss-apply
+  let defined = new Map();
+  let res = transform({
+    filename: 'test.css',
+    minify: true,
+    code: Buffer.from(`
+      --toolbar-theme {
+        color: white;
+        border: 1px solid green;
+      }
+
+      .toolbar {
+        @apply --toolbar-theme;
+      }
+    `),
+    visitor: {
+      Rule: {
+        style(rule) {
+          for (let selector of rule.value.selectors) {
+            if (selector.length === 1 && selector[0].type === 'type' && selector[0].name.startsWith('--')) {
+              defined.set(selector[0].name, rule.value.declarations);
+              return { type: 'ignored', value: null };
+            }
+          }
+
+          rule.value.rules = rule.value.rules.filter(child => {
+            if (child.type === 'unknown' && child.value.name === 'apply') {
+              for (let token of child.value.prelude) {
+                if (token.type === 'dashed-ident' && defined.has(token.value)) {
+                  let r = defined.get(token.value);
+                  let decls = rule.value.declarations;
+                  decls.declarations.push(...r.declarations);
+                  decls.importantDeclarations.push(...r.importantDeclarations);
+                }
+              }
+              return false;
+            }
+            return true;
+          });
+
+          return rule;
+        }
+      }
+    }
+  });
+
+  assert.equal(res.code.toString(), '.toolbar{color:#fff;border:1px solid green}');
+});
+
+test('property lookup', () => {
+  // Similar to https://www.npmjs.com/package/postcss-property-lookup
+  let res = transform({
+    filename: 'test.css',
+    minify: true,
+    code: Buffer.from(`
+     .test {
+        margin-left: 20px;
+        margin-right: @margin-left;
+     }
+    `),
+    visitor: {
+      Rule: {
+        style(rule) {
+          let valuesByProperty = new Map();
+          for (let decl of rule.value.declarations.declarations) {
+            /** @type string */
+            let name = decl.property;
+            if (decl.property === 'unparsed') {
+              name = decl.value.propertyId.property;
+            }
+            valuesByProperty.set(name, decl);
+          }
+
+          rule.value.declarations.declarations = rule.value.declarations.declarations.map(decl => {
+            // Only single value supported. Would need a way to convert parsed values to unparsed tokens otherwise.
+            if (decl.property === 'unparsed' && decl.value.value.length === 1) {
+              let token = decl.value.value[0];
+              if (token.type === 'token' && token.value.type === 'at-keyword' && valuesByProperty.has(token.value.value)) {
+                let v = valuesByProperty.get(token.value.value);
+                return {
+                  /** @type any */
+                  property: decl.value.propertyId.property,
+                  value: v.value
+                };
+              }
+            }
+            return decl;
+          });
+
+          return rule;
+        }
+      }
+    }
+  });
+
+  assert.equal(res.code.toString(), '.test{margin-left:20px;margin-right:20px}');
+});
+
+test('focus visible', () => {
+  // Similar to https://www.npmjs.com/package/postcss-focus-visible
+  let res = transform({
+    filename: 'test.css',
+    minify: true,
+    code: Buffer.from(`
+      .test:focus-visible {
+        color: red;
+      }
+    `),
+    targets: {
+      safari: 14 << 16
+    },
+    visitor: {
+      Rule: {
+        style(rule) {
+          let clone = null;
+          for (let selector of rule.value.selectors) {
+            for (let [i, component] of selector.entries()) {
+              if (component.type === 'pseudo-class' && component.kind === 'focus-visible') {
+                if (clone == null) {
+                  clone = [...rule.value.selectors.map(s => [...s])];
+                }
+
+                selector[i] = { type: 'class', name: 'focus-visible' };
+              }
+            }
+          }
+
+          if (clone) {
+            return [rule, { type: 'style', value: { ...rule.value, selectors: clone } }];
+          }
+        }
+      }
+    }
+  });
+
+  assert.equal(res.code.toString(), '.test.focus-visible{color:red}.test:focus-visible{color:red}');
+});
+
+test('dark theme class', () => {
+  // Similar to https://github.com/postcss/postcss-dark-theme-class
+  let res = transform({
+    filename: 'test.css',
+    minify: true,
+    code: Buffer.from(`
+      @media (prefers-color-scheme: dark) {
+        body {
+          background: black
+        }
+      }
+    `),
+    visitor: {
+      Rule: {
+        media(rule) {
+          let q = rule.value.query.mediaQueries[0];
+          if (q.condition?.type === 'feature' && q.condition.value.type === 'plain' && q.condition.value.name === 'prefers-color-scheme' && q.condition.value.value.value === 'dark') {
+            /** @type {import('../ast').Rule[]} */
+            let clonedRules = [rule];
+            for (let r of rule.value.rules) {
+              if (r.type === 'style') {
+                /** @type {import('../ast').Selector[]} */
+                let clonedSelectors = [];
+                for (let selector of r.value.selectors) {
+                  clonedSelectors.push([
+                    { type: 'type', name: 'html' },
+                    { type: 'attribute', name: 'theme', operation: { operator: 'equal', value: 'dark' } },
+                    { type: 'combinator', value: 'descendant' },
+                    ...selector
+                  ]);
+                  selector.unshift(
+                    { type: 'type', name: 'html' },
+                    {
+                      type: 'pseudo-class',
+                      kind: 'not',
+                      selectors: [
+                        [{ type: 'attribute', name: 'theme', operation: { operator: 'equal', value: 'light' } }]
+                      ]
+                    },
+                    { type: 'combinator', value: 'descendant' }
+                  );
+                }
+
+                clonedRules.push({ type: 'style', value: { ...r.value, selectors: clonedSelectors } });
+              }
+            }
+
+            return clonedRules;
+          }
+        }
+      }
+    }
+  });
+
+  assert.equal(res.code.toString(), '@media (prefers-color-scheme:dark){html:not([theme=light]) body{background:#000}}html[theme=dark] body{background:#000}');
+});
+
+test('100vh fix', () => {
+  // Similar to https://github.com/postcss/postcss-100vh-fix
+  let res = transform({
+    filename: 'test.css',
+    minify: true,
+    code: Buffer.from(`
+      .foo {
+        color: red;
+        height: 100vh;
+      }
+    `),
+    visitor: {
+      Rule: {
+        style(style) {
+          let cloned;
+          for (let property of style.value.declarations.declarations) {
+            if (property.property === 'height' && property.value.type === 'length-percentage' && property.value.value.type === 'dimension' && property.value.value.value.unit === 'vh' && property.value.value.value.value === 100) {
+              if (!cloned) {
+                cloned = structuredClone(style);
+                cloned.value.declarations.declarations = [];
+              }
+              cloned.value.declarations.declarations.push({
+                ...property,
+                value: {
+                  type: 'stretch',
+                  vendorPrefix: ['webkit']
+                }
+              });
+            }
+          }
+
+          if (cloned) {
+            return [style, {
+              type: 'supports',
+              value: {
+                condition: {
+                  type: 'declaration',
+                  propertyId: {
+                    property: '-webkit-touch-callout'
+                  },
+                  value: 'none'
+                },
+                loc: style.value.loc,
+                rules: [cloned]
+              }
+            }];
+          }
+        }
+      }
+    }
+  });
+
+  assert.equal(res.code.toString(), '.foo{color:red;height:100vh}@supports (-webkit-touch-callout:none){.foo{height:-webkit-fill-available}}')
+});
+
+test('logical transforms', () => {
+  // Similar to https://github.com/MohammadYounes/rtlcss
+  let res = transform({
+    filename: 'test.css',
+    minify: true,
+    code: Buffer.from(`
+      .foo {
+        transform: translateX(50px);
+      }
+
+      .bar {
+        transform: translateX(20%);
+      }
+
+      .baz {
+        transform: translateX(calc(100vw - 20px));
+      }
+    `),
+    visitor: {
+      Rule: {
+        style(style) {
+          /** @type any */
+          let cloned;
+          for (let property of style.value.declarations.declarations) {
+            if (property.property === 'transform') {
+              let clonedTransforms = property.value.map(transform => {
+                if (transform.type !== 'translateX') {
+                  return transform;
+                }
+
+                if (!cloned) {
+                  cloned = structuredClone(style);
+                  cloned.value.declarations.declarations = [];
+                }
+
+                let value;
+                switch (transform.value.type) {
+                  case 'dimension':
+                    value = { type: 'dimension', value: { unit: transform.value.value.unit, value: -transform.value.value.value } };
+                    break;
+                  case 'percentage':
+                    value = { type: 'percentage', value: -transform.value.value };
+                    break;
+                  case 'calc':
+                    value = { type: 'calc', value: { type: 'product', value: [-1, transform.value.value] } };
+                    break;
+                }
+
+                return {
+                  type: 'translateX',
+                  value
+                }
+              });
+
+              if (cloned) {
+                cloned.value.selectors.at(-1).push({ type: 'pseudo-class', kind: 'dir', direction: 'rtl' });
+                cloned.value.declarations.declarations.push({
+                  ...property,
+                  value: clonedTransforms
+                });
+              }
+            }
+          }
+
+          if (cloned) {
+            return [style, cloned];
+          }
+        }
+      }
+    }
+  });
+
+  assert.equal(res.code.toString(), '.foo{transform:translate(50px)}.foo:dir(rtl){transform:translate(-50px)}.bar{transform:translate(20%)}.bar:dir(rtl){transform:translate(-20%)}.baz{transform:translate(calc(100vw - 20px))}.baz:dir(rtl){transform:translate(-1*calc(100vw - 20px))}');
+});
+
+test('hover media query', () => {
+  // Similar to https://github.com/twbs/mq4-hover-shim
+  let res = transform({
+    filename: 'test.css',
+    minify: true,
+    code: Buffer.from(`
+      @media (hover) {
+        .foo {
+          color: red;
+        }
+      }
+    `),
+    visitor: {
+      Rule: {
+        media(media) {
+          let mediaQueries = media.value.query.mediaQueries;
+          if (
+            mediaQueries.length === 1 &&
+            mediaQueries[0].condition &&
+            mediaQueries[0].condition.type === 'feature' &&
+            mediaQueries[0].condition.value.type === 'boolean' &&
+            mediaQueries[0].condition.value.name === 'hover'
+          ) {
+            for (let rule of media.value.rules) {
+              if (rule.type === 'style') {
+                for (let selector of rule.value.selectors) {
+                  selector.unshift({ type: 'class', name: 'hoverable' }, { type: 'combinator', value: 'descendant' });
+                }
+              }
+            }
+            return media.value.rules
+          }
+        }
+      }
+    }
+  });
+
+  assert.equal(res.code.toString(), '.hoverable .foo{color:red}');
+});
+
+test('momentum scrolling', () => {
+  // Similar to https://github.com/yunusga/postcss-momentum-scrolling
+  let visitOverflow = decl => [decl, {
+    property: '-webkit-overflow-scrolling',
+    raw: 'touch'
+  }];
+
+  let res = transform({
+    filename: 'test.css',
+    minify: true,
+    code: Buffer.from(`
+      .foo {
+        overflow: auto;
+      }
+    `),
+    visitor: {
+      Declaration: {
+        overflow: visitOverflow,
+        'overflow-x': visitOverflow,
+        'overflow-y': visitOverflow
+      }
+    }
+  });
+
+  assert.equal(res.code.toString(), '.foo{-webkit-overflow-scrolling:touch;overflow:auto}');
+});
+
+test('size', () => {
+  // Similar to https://github.com/postcss/postcss-size
+  let res = transform({
+    filename: 'test.css',
+    minify: true,
+    code: Buffer.from(`
+      .foo {
+        size: 12px;
+      }
+    `),
+    visitor: {
+      Declaration: {
+        custom: {
+          size(property) {
+            if (property.value[0].type === 'length') {
+              /** @type {import('../ast').Size} */
+              let value = { type: 'length-percentage', value: { type: 'dimension', value: property.value[0].value } };
+              return [
+                { property: 'width', value },
+                { property: 'height', value }
+              ];
+            }
+          }
+        }
+      }
+    }
+  });
+
+  assert.equal(res.code.toString(), '.foo{width:12px;height:12px}');
+});
+
+test('works with style attributes', () => {
+  let res = transformStyleAttribute({
+    filename: 'test.css',
+    minify: true,
+    code: Buffer.from('height: calc(100vh - 64px)'),
+    visitor: {
+      Length(length) {
+        if (length.unit === 'px') {
+          return {
+            unit: 'rem',
+            value: length.value / 16
+          };
+        }
+      }
+    }
+  });
+
+  assert.equal(res.code.toString(), 'height:calc(100vh - 4rem)');
+});
+
+test('works with bundler', () => {
+  let res = bundle({
+    filename: 'tests/testdata/a.css',
+    minify: true,
+    visitor: {
+      Length(length) {
+        if (length.unit === 'px') {
+          return {
+            unit: 'rem',
+            value: length.value / 16
+          };
+        }
+      }
+    }
+  });
+
+  assert.equal(res.code.toString(), '.b{height:calc(100vh - 4rem)}.a{width:2rem}');
+});
+
+test('works with async bundler', async () => {
+  let res = await bundleAsync({
+    filename: 'tests/testdata/a.css',
+    minify: true,
+    visitor: {
+      Length(length) {
+        if (length.unit === 'px') {
+          return {
+            unit: 'rem',
+            value: length.value / 16
+          };
+        }
+      }
+    }
+  });
+
+  assert.equal(res.code.toString(), '.b{height:calc(100vh - 4rem)}.a{width:2rem}');
+});
+
+test('dashed idents', () => {
+  let res = transform({
+    filename: 'test.css',
+    minify: true,
+    code: Buffer.from(`
+      .foo {
+        --foo: #ff0;
+        color: var(--foo);
+      }
+    `),
+    visitor: {
+      DashedIdent(ident) {
+        return `--prefix-${ident.slice(2)}`;
+      }
+    }
+  });
+
+  assert.equal(res.code.toString(), '.foo{--prefix-foo:#ff0;color:var(--prefix-foo)}');
+});
+
+test('custom idents', () => {
+  let res = transform({
+    filename: 'test.css',
+    minify: true,
+    code: Buffer.from(`
+      @keyframes test {
+        from { color: red }
+        to { color: green }
+      }
+      .foo {
+        animation: test;
+      }
+    `),
+    visitor: {
+      CustomIdent(ident) {
+        return `prefix-${ident}`;
+      }
+    }
+  });
+
+  assert.equal(res.code.toString(), '@keyframes prefix-test{0%{color:red}to{color:green}}.foo{animation:prefix-test}');
+});
+
+test('returning string values', () => {
+  let res = transform({
+    filename: 'test.css',
+    minify: true,
+    code: Buffer.from(`
+      @tailwind base;
+    `),
+    visitor: {
+      Rule: {
+        unknown(rule) {
+          return {
+            type: 'style',
+            value: {
+              loc: rule.loc,
+              selectors: [
+                [{ type: 'universal' }]
+              ],
+              declarations: {
+                declarations: [
+                  {
+                    property: 'visibility',
+                    raw: 'hi\\64 den' // escapes work for raw but not value
+                  },
+                  {
+                    property: 'background',
+                    raw: 'yellow'
+                  },
+                  {
+                    property: '--custom',
+                    raw: 'hi'
+                  },
+                  {
+                    property: 'transition',
+                    vendorPrefix: ['moz'],
+                    raw: '200ms test'
+                  },
+                  {
+                    property: '-webkit-animation',
+                    raw: '3s cubic-bezier(0.25, 0.1, 0.25, 1) foo'
+                  }
+                ]
+              }
+            }
+          }
+        }
+      }
+    }
+  });
+
+  assert.equal(res.code.toString(), '*{visibility:hidden;--custom:hi;background:#ff0;-moz-transition:test .2s;-webkit-animation:3s foo}');
+});
+
+test('errors on invalid dashed idents', () => {
+  assert.throws(() => {
+    transform({
+      filename: 'test.css',
+      minify: true,
+      code: Buffer.from(`
+        .foo {
+          background: opacity(abcdef);
+        }
+      `),
+      visitor: {
+        Function(fn) {
+          if (fn.arguments[0].type === 'token' && fn.arguments[0].value.type === 'ident') {
+            fn.arguments = [
+              {
+                type: 'var',
+                value: {
+                  name: { ident: fn.arguments[0].value.value }
+                }
+              }
+            ];
+          }
+
+          return {
+            type: 'function',
+            value: fn
+          }
+        }
+      }
+    })
+  }, 'Dashed idents must start with --');
+});
+
+test('supports returning raw values for tokens', () => {
+  let res = transform({
+    filename: 'test.css',
+    minify: true,
+    code: Buffer.from(`
+      .foo {
+        color: theme('red');
+      }
+    `),
+    visitor: {
+      Function: {
+        theme() {
+          return { raw: 'rgba(255, 0, 0)' };
+        }
+      }
+    }
+  });
+
+  assert.equal(res.code.toString(), '.foo{color:red}');
+});
+
+test('supports returning raw values as variables', () => {
+  let res = transform({
+    filename: 'test.css',
+    minify: true,
+    cssModules: {
+      dashedIdents: true
+    },
+    code: Buffer.from(`
+      .foo {
+        color: theme('foo');
+      }
+    `),
+    visitor: {
+      Function: {
+        theme() {
+          return { raw: 'var(--foo)' };
+        }
+      }
+    }
+  });
+
+  assert.equal(res.code.toString(), '.EgL3uq_foo{color:var(--EgL3uq_foo)}');
+});
+
+test('works with currentColor', () => {
+  let res = transform({
+    filename: 'test.css',
+    minify: true,
+    code: Buffer.from(`
+      .foo {
+        color: currentColor;
+      }
+    `),
+    visitor: {
+      Rule(rule) {
+        return rule;
+      }
+    }
+  });
+
+  assert.equal(res.code.toString(), '.foo{color:currentColor}');
+});
+
+test('nth of S to nth-of-type', () => {
+  let res = transform({
+    filename: 'test.css',
+    minify: true,
+    code: Buffer.from(`
+      a:nth-child(even of a) {
+        color: red;
+      }
+    `),
+    visitor: {
+      Selector(selector) {
+        for (let component of selector) {
+          if (component.type === 'pseudo-class' && component.kind === 'nth-child' && component.of) {
+            delete component.of;
+            component.kind = 'nth-of-type';
+          }
+        }
+        return selector;
+      }
+    }
+  });
+
+  assert.equal(res.code.toString(), 'a:nth-of-type(2n){color:red}');
+});
+
+test('media query raw', () => {
+  let res = transform({
+    filename: 'test.css',
+    minify: true,
+    code: Buffer.from(`
+      @breakpoints {
+        .m-1 {
+          margin: 10px;
+        }
+      }
+    `),
+    customAtRules: {
+      breakpoints: {
+        prelude: null,
+        body: "rule-list",
+      },
+    },
+    visitor: {
+      Rule: {
+        custom: {
+          breakpoints({ body, loc }) {
+            /** @type {import('lightningcss').ReturnedRule[]} */
+            const value = [];
+
+            for (let rule of body.value) {
+              if (rule.type !== 'style') {
+                continue;
+              }
+              const clone = structuredClone(rule);
+              for (let selector of clone.value.selectors) {
+                for (let component of selector) {
+                  if (component.type === 'class') {
+                    component.name = `sm:${component.name}`;
+                  }
+                }
+              }
+
+              value.push(rule);
+              value.push({
+                type: "media",
+                value: {
+                  rules: [clone],
+                  loc,
+                  query: {
+                    mediaQueries: [
+                      { raw: '(min-width: 500px)' }
+                    ]
+                  }
+                }
+              });
+            }
+
+            return value;
+          }
+        }
+      }
+    }
+  });
+
+  assert.equal(res.code.toString(), '.m-1{margin:10px}@media (width>=500px){.sm\\:m-1{margin:10px}}');
+});
+
+test('visit stylesheet', () => {
+  let res = transform({
+    filename: 'test.css',
+    minify: true,
+    code: Buffer.from(`
+      .foo {
+        width: 32px;
+      }
+
+      .bar {
+        width: 80px;
+      }
+    `),
+    visitor: {
+      StyleSheetExit(stylesheet) {
+        stylesheet.rules.sort((a, b) => a.value.selectors[0][0].name.localeCompare(b.value.selectors[0][0].name));
+        return stylesheet;
+      }
+    }
+  });
+
+  assert.equal(res.code.toString(), '.bar{width:80px}.foo{width:32px}');
+});
+
+test.run();
diff --git a/node/tsconfig.json b/node/tsconfig.json
new file mode 100644
index 0000000..9b82eaa
--- /dev/null
+++ b/node/tsconfig.json
@@ -0,0 +1,10 @@
+{
+  "include": ["*.d.ts"],
+  "compilerOptions": {
+    "lib": ["ES2020"],
+    "moduleResolution": "node",
+    "isolatedModules": true,
+    "noEmit": true,
+    "strict": true
+  }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..747e67c
--- /dev/null
+++ b/package.json
@@ -0,0 +1,97 @@
+{
+  "name": "lightningcss",
+  "version": "1.30.1",
+  "license": "MPL-2.0",
+  "description": "A CSS parser, transformer, and minifier written in Rust",
+  "main": "node/index.js",
+  "types": "node/index.d.ts",
+  "exports": {
+    "types": "./node/index.d.ts",
+    "import": "./node/index.mjs",
+    "require": "./node/index.js"
+  },
+  "browserslist": "last 2 versions, not dead",
+  "targets": {
+    "main": false,
+    "types": false
+  },
+  "publishConfig": {
+    "access": "public"
+  },
+  "funding": {
+    "type": "opencollective",
+    "url": "https://opencollective.com/parcel"
+  },
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/parcel-bundler/lightningcss.git"
+  },
+  "engines": {
+    "node": ">= 12.0.0"
+  },
+  "napi": {
+    "name": "lightningcss"
+  },
+  "files": [
+    "node/*.js",
+    "node/*.mjs",
+    "node/*.d.ts",
+    "node/*.flow"
+  ],
+  "dependencies": {
+    "detect-libc": "^2.0.3"
+  },
+  "devDependencies": {
+    "@babel/parser": "7.21.4",
+    "@babel/traverse": "7.21.4",
+    "@codemirror/lang-css": "^6.0.1",
+    "@codemirror/lang-javascript": "^6.1.2",
+    "@codemirror/lint": "^6.1.0",
+    "@codemirror/theme-one-dark": "^6.1.0",
+    "@mdn/browser-compat-data": "~6.0.13",
+    "@napi-rs/cli": "^2.14.0",
+    "autoprefixer": "^10.4.21",
+    "caniuse-lite": "^1.0.30001717",
+    "codemirror": "^6.0.1",
+    "cssnano": "^7.0.6",
+    "esbuild": "^0.19.8",
+    "flowgen": "^1.21.0",
+    "jest-diff": "^27.4.2",
+    "json-schema-to-typescript": "^11.0.2",
+    "markdown-it-anchor": "^8.6.6",
+    "markdown-it-prism": "^2.3.0",
+    "markdown-it-table-of-contents": "^0.6.0",
+    "napi-wasm": "^1.0.1",
+    "node-fetch": "^3.1.0",
+    "parcel": "^2.8.2",
+    "patch-package": "^6.5.0",
+    "path-browserify": "^1.0.1",
+    "postcss": "^8.3.11",
+    "posthtml-include": "^1.7.4",
+    "posthtml-markdownit": "^1.3.1",
+    "posthtml-prism": "^1.0.4",
+    "process": "^0.11.10",
+    "puppeteer": "^12.0.1",
+    "recast": "^0.22.0",
+    "sharp": "^0.33.5",
+    "typescript": "^5.7.2",
+    "util": "^0.12.4",
+    "uvu": "^0.5.6"
+  },
+  "resolutions": {
+    "lightningcss": "link:."
+  },
+  "scripts": {
+    "prepare": "patch-package",
+    "build": "node scripts/build.js && node scripts/build-flow.js",
+    "build-release": "node scripts/build.js --release && node scripts/build-flow.js",
+    "prepublishOnly": "node scripts/build-flow.js",
+    "wasm:build": "cargo build --target wasm32-unknown-unknown -p lightningcss_node && wasm-opt target/wasm32-unknown-unknown/debug/lightningcss_node.wasm --asyncify --pass-arg=asyncify-imports@env.await_promise_sync -Oz -o wasm/lightningcss_node.wasm && node scripts/build-wasm.js",
+    "wasm:build-release": "cargo build --target wasm32-unknown-unknown -p lightningcss_node --release && wasm-opt target/wasm32-unknown-unknown/release/lightningcss_node.wasm --asyncify --pass-arg=asyncify-imports@env.await_promise_sync -Oz -o wasm/lightningcss_node.wasm && node scripts/build-wasm.js",
+    "website:start": "parcel 'website/*.html' website/playground/index.html",
+    "website:build": "yarn wasm:build-release && parcel build 'website/*.html' website/playground/index.html",
+    "build-ast": "cargo run --example schema --features jsonschema && node scripts/build-ast.js",
+    "tsc": "tsc -p node/tsconfig.json",
+    "test": "uvu node/test"
+  }
+}
diff --git a/patches/@babel+types+7.26.3.patch b/patches/@babel+types+7.26.3.patch
new file mode 100644
index 0000000..e672fb0
--- /dev/null
+++ b/patches/@babel+types+7.26.3.patch
@@ -0,0 +1,18 @@
+diff --git a/node_modules/@babel/types/lib/retrievers/getBindingIdentifiers.js b/node_modules/@babel/types/lib/retrievers/getBindingIdentifiers.js
+index 31feb1e..a64b83d 100644
+--- a/node_modules/@babel/types/lib/retrievers/getBindingIdentifiers.js
++++ b/node_modules/@babel/types/lib/retrievers/getBindingIdentifiers.js
+@@ -66,6 +66,13 @@ const keys = {
+   InterfaceDeclaration: ["id"],
+   TypeAlias: ["id"],
+   OpaqueType: ["id"],
++  TSDeclareFunction: ["id"],
++  TSEnumDeclaration: ["id"],
++  TSImportEqualsDeclaration: ["id"],
++  TSInterfaceDeclaration: ["id"],
++  TSModuleDeclaration: ["id"],
++  TSNamespaceExportDeclaration: ["id"],
++  TSTypeAliasDeclaration: ["id"],
+   CatchClause: ["param"],
+   LabeledStatement: ["label"],
+   UnaryExpression: ["argument"],
diff --git a/patches/json-schema-to-typescript+11.0.5.patch b/patches/json-schema-to-typescript+11.0.5.patch
new file mode 100644
index 0000000..b1d06ba
--- /dev/null
+++ b/patches/json-schema-to-typescript+11.0.5.patch
@@ -0,0 +1,109 @@
+diff --git a/node_modules/json-schema-to-typescript/dist/src/parser.js b/node_modules/json-schema-to-typescript/dist/src/parser.js
+index fa9d2e4..3f65449 100644
+--- a/node_modules/json-schema-to-typescript/dist/src/parser.js
++++ b/node_modules/json-schema-to-typescript/dist/src/parser.js
+@@ -1,6 +1,6 @@
+ "use strict";
+ var __assign = (this && this.__assign) || function () {
+-    __assign = Object.assign || function(t) {
++    __assign = Object.assign || function (t) {
+         for (var s, i = 1, n = arguments.length; i < n; i++) {
+             s = arguments[i];
+             for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
+@@ -90,14 +90,27 @@ function parseNonLiteral(schema, type, options, keyName, processed, usedNames) {
+             };
+         case 'ANY':
+             return __assign(__assign({}, (options.unknownAny ? AST_1.T_UNKNOWN : AST_1.T_ANY)), { comment: schema.description, keyName: keyName, standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames) });
+-        case 'ANY_OF':
+-            return {
++        case 'ANY_OF': {
++            let union = {
+                 comment: schema.description,
+                 keyName: keyName,
+                 standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames),
+                 params: schema.anyOf.map(function (_) { return parse(_, options, undefined, processed, usedNames); }),
+                 type: 'UNION'
+             };
++
++            if (schema.properties) {
++                let common = newInterface(schema, options, processed, usedNames, keyName, keyNameFromDefinition);
++                return {
++                    comment: schema.description,
++                    keyName,
++                    standaloneName: union.standaloneName,
++                    params: [common, union],
++                    type: 'INTERSECTION'
++                };
++            }
++            return union;
++        }
+         case 'BOOLEAN':
+             return {
+                 comment: schema.description,
+@@ -118,10 +131,12 @@ function parseNonLiteral(schema, type, options, keyName, processed, usedNames) {
+                 comment: schema.description,
+                 keyName: keyName,
+                 standaloneName: standaloneName(schema, keyNameFromDefinition !== null && keyNameFromDefinition !== void 0 ? keyNameFromDefinition : keyName, usedNames),
+-                params: schema.enum.map(function (_, n) { return ({
+-                    ast: parse(_, options, undefined, processed, usedNames),
+-                    keyName: schema.tsEnumNames[n]
+-                }); }),
++                params: schema.enum.map(function (_, n) {
++                    return ({
++                        ast: parse(_, options, undefined, processed, usedNames),
++                        keyName: schema.tsEnumNames[n]
++                    });
++                }),
+                 type: 'ENUM'
+             };
+         case 'NAMED_SCHEMA':
+@@ -147,14 +162,24 @@ function parseNonLiteral(schema, type, options, keyName, processed, usedNames) {
+                 standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames),
+                 type: 'OBJECT'
+             };
+-        case 'ONE_OF':
++        case 'ONE_OF': {
++            let common = schema.properties ? parseSchema(schema, options, processed, usedNames, keyName) : null;
++            let commonKeys = common ? new Set(common.map(p => p.keyName)) : null;
++
+             return {
+                 comment: schema.description,
+                 keyName: keyName,
+                 standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames),
+-                params: schema.oneOf.map(function (_) { return parse(_, options, undefined, processed, usedNames); }),
++                params: schema.oneOf.map(function (_) {
++                    let item = parse(_, options, undefined, processed, usedNames);
++                    if (common && item.type === 'INTERFACE') {
++                        item.params = common.concat(item.params.filter(p => !commonKeys.has(p.keyName)));
++                    }
++                    return item;
++                }),
+                 type: 'UNION'
+             };
++        }
+         case 'REFERENCE':
+             throw Error((0, util_1.format)('Refs should have been resolved by the resolver!', schema));
+         case 'STRING':
+@@ -277,13 +302,15 @@ function parseSuperTypes(schema, options, processed, usedNames) {
+  * Helper to parse schema properties into params on the parent schema's type
+  */
+ function parseSchema(schema, options, processed, usedNames, parentSchemaName) {
+-    var asts = (0, lodash_1.map)(schema.properties, function (value, key) { return ({
+-        ast: parse(value, options, key, processed, usedNames),
+-        isPatternProperty: false,
+-        isRequired: (0, lodash_1.includes)(schema.required || [], key),
+-        isUnreachableDefinition: false,
+-        keyName: key
+-    }); });
++    var asts = (0, lodash_1.map)(schema.properties, function (value, key) {
++        return ({
++            ast: parse(value, options, key, processed, usedNames),
++            isPatternProperty: false,
++            isRequired: (0, lodash_1.includes)(schema.required || [], key),
++            isUnreachableDefinition: false,
++            keyName: key
++        });
++    });
+     var singlePatternProperty = false;
+     if (schema.patternProperties) {
+         // partially support patternProperties. in the case that
diff --git a/rust-toolchain.toml b/rust-toolchain.toml
new file mode 100644
index 0000000..80afd2d
--- /dev/null
+++ b/rust-toolchain.toml
@@ -0,0 +1,3 @@
+[toolchain]
+channel = "1.83.0"
+components = ["rustfmt", "clippy"]
diff --git a/rustfmt.toml b/rustfmt.toml
new file mode 100644
index 0000000..dfcb85c
--- /dev/null
+++ b/rustfmt.toml
@@ -0,0 +1,3 @@
+tab_spaces = 2
+chain_width = 80
+max_width = 115
\ No newline at end of file
diff --git a/scripts/build-ast.js b/scripts/build-ast.js
new file mode 100644
index 0000000..883aa44
--- /dev/null
+++ b/scripts/build-ast.js
@@ -0,0 +1,151 @@
+const { compileFromFile } = require('json-schema-to-typescript');
+const fs = require('fs');
+const recast = require('recast');
+const traverse = require('@babel/traverse').default;
+const {parse} = require('@babel/parser');
+const t = require('@babel/types');
+
+const skip = {
+  FillRule: true,
+  ImportRule: true,
+  FontFaceRule: true,
+  FontPaletteValuesRule: true,
+  NamespaceRule: true,
+  CustomMediaRule: true,
+  LayerStatementRule: true,
+  PropertyRule: true,
+  UnknownAtRule: true,
+  DefaultAtRule: true
+}
+
+compileFromFile('node/ast.json', {
+  additionalProperties: false
+}).then(ts => {
+  ts = ts.replaceAll('For_DefaultAtRule', '');
+
+  // Use recast/babel to make some types generic so we can replace them in index.d.ts.
+  let ast = recast.parse(ts, {
+    parser: {
+      parse() {
+        return parse(ts, {
+          sourceType: 'module',
+          plugins: ['typescript'],
+          tokens: true
+        });
+      }
+    }
+  });
+
+  traverse(ast, {
+    Program(path) {
+      process(path.scope.getBinding('Declaration'));
+      process(path.scope.getBinding('MediaQuery'));
+    },
+    TSInterfaceDeclaration(path) {
+      // Dedupe.
+      if (path.node.id.name.startsWith('GenericBorderFor_LineStyleAnd_')) {
+        if (path.node.id.name.endsWith('_0')) {
+          path.node.id.name = 'GenericBorderFor_LineStyle';
+        } else {
+          path.remove();
+        }
+      }
+    },
+    ReferencedIdentifier(path) {
+      if (path.node.name.startsWith('GenericBorderFor_LineStyleAnd_')) {
+        path.node.name = 'GenericBorderFor_LineStyle';
+      }
+    },
+    TSTypeAliasDeclaration(path) {
+      // Workaround for schemars not supporting untagged variants.
+      // https://github.com/GREsau/schemars/issues/222
+      if (
+        (path.node.id.name === 'Translate' || path.node.id.name === 'Scale') &&
+        path.node.typeAnnotation.type === 'TSUnionType' &&
+        path.node.typeAnnotation.types[1].type === 'TSTypeLiteral' &&
+        path.node.typeAnnotation.types[1].members[0].key.name === 'xyz'
+      ) {
+        path.get('typeAnnotation.types.1').replaceWith(path.node.typeAnnotation.types[1].members[0].typeAnnotation.typeAnnotation);
+      } else if (path.node.id.name === 'AnimationAttachmentRange' && path.node.typeAnnotation.type === 'TSUnionType') {
+        let types = path.node.typeAnnotation.types;
+        if (types[1].type === 'TSTypeLiteral' && types[1].members[0].key.name === 'lengthpercentage') {
+          path.get('typeAnnotation.types.1').replaceWith(path.node.typeAnnotation.types[1].members[0].typeAnnotation.typeAnnotation);
+        }
+
+        if (types[2].type === 'TSTypeLiteral' && types[2].members[0].key.name === 'timelinerange') {
+          path.get('typeAnnotation.types.2').replaceWith(path.node.typeAnnotation.types[2].members[0].typeAnnotation.typeAnnotation);
+        }
+      } else if (
+        path.node.id.name === 'NoneOrCustomIdentList' &&
+        path.node.typeAnnotation.type === 'TSUnionType' &&
+        path.node.typeAnnotation.types[1].type === 'TSTypeLiteral' &&
+        path.node.typeAnnotation.types[1].members[0].key.name === 'idents'
+      ) {
+        path.get('typeAnnotation.types.1').replaceWith(path.node.typeAnnotation.types[1].members[0].typeAnnotation.typeAnnotation);
+      } else if (
+        path.node.id.name === 'ViewTransitionGroup' &&
+        path.node.typeAnnotation.type === 'TSUnionType' &&
+        path.node.typeAnnotation.types[3].type === 'TSTypeLiteral' &&
+        path.node.typeAnnotation.types[3].members[0].key.name === 'custom'
+      ) {
+        path.get('typeAnnotation.types.3').replaceWith(path.node.typeAnnotation.types[3].members[0].typeAnnotation.typeAnnotation);
+      } else if (
+        path.node.id.name === 'ViewTransitionName' &&
+        path.node.typeAnnotation.type === 'TSUnionType' &&
+        path.node.typeAnnotation.types[2].type === 'TSTypeLiteral' &&
+        path.node.typeAnnotation.types[2].members[0].key.name === 'custom'
+      ) {
+        path.get('typeAnnotation.types.2').replaceWith(path.node.typeAnnotation.types[2].members[0].typeAnnotation.typeAnnotation);
+      }
+    }
+  });
+
+  ts = recast.print(ast, {objectCurlySpacing: false}).code;
+  fs.writeFileSync('node/ast.d.ts', ts)
+});
+
+function process(binding) {
+  // Follow the references upward from the binding to add generics.
+  for (let reference of binding.referencePaths) {
+    if (reference.node !== binding.identifier) {
+      genericize(reference, binding.identifier.name);
+    }
+  }
+}
+
+function genericize(path, name, seen = new Set()) {
+  if (seen.has(path.node)) return;
+  seen.add(path.node);
+
+  // Find the parent declaration of the reference, and add a generic if needed.
+  let parent = path.findParent(p => p.isDeclaration());
+  if (!parent.node.typeParameters) {
+    parent.node.typeParameters = t.tsTypeParameterDeclaration([]);
+  }
+  let params = parent.get('typeParameters');
+  let param = params.node.params.find(p => p.default.typeName.name === name);
+  if (!param) {
+    params.pushContainer('params', t.tsTypeParameter(null, t.tsTypeReference(t.identifier(name)), name[0]));
+  }
+
+  // Replace the reference with the generic, or add a type parameter.
+  if (path.node.name === name) {
+    path.replaceWith(t.identifier(name[0]));
+  } else {
+    if (!path.parent.typeParameters) {
+      path.parent.typeParameters = t.tsTypeParameterInstantiation([]);
+    }
+    let param = path.parent.typeParameters.params.find(p => p.typeName.name === name[0]);
+    if (!param) {
+      path.parentPath.get('typeParameters').pushContainer('params', t.tsTypeReference(t.identifier(name[0])));
+    }
+  }
+
+  // Keep going to all references of this reference.
+  let binding = path.scope.getBinding(parent.node.id.name);
+  for (let reference of binding.referencePaths) {
+    if (reference.node !== binding.identifier) {
+      genericize(reference, name, seen);
+    }
+  }
+}
diff --git a/scripts/build-flow.js b/scripts/build-flow.js
new file mode 100644
index 0000000..561ddff
--- /dev/null
+++ b/scripts/build-flow.js
@@ -0,0 +1,18 @@
+const fs = require('fs');
+const { compiler, beautify } = require('flowgen');
+
+let dir = `${__dirname}/../`;
+let contents = fs.readFileSync(dir + '/node/index.d.ts', 'utf8').replace('`${PropertyStart}${string}`', 'string');
+contents = contents.replace(/`.*`/g, 'string');
+contents = contents.replace(/(string & \{\})/g, 'string');
+let index = beautify(compiler.compileDefinitionString(contents, { inexact: false, interfaceRecords: true }));
+index = index.replace('{ code: any }', '{| code: any |}');
+index = index.replace(/from "(.*?)";/g, 'from "$1.js.flow";');
+// This Exclude type isn't right at all, but idk how to get it working for real...
+fs.writeFileSync(dir + '/node/index.js.flow', '// @flow\n\ntype Exclude<A, B> = A;\n' + index)
+
+let ast = beautify(compiler.compileDefinitionFile(dir + '/node/ast.d.ts', { inexact: false }));
+fs.writeFileSync(dir + '/node/ast.js.flow', '// @flow\n\n' + ast)
+
+let targets = beautify(compiler.compileDefinitionFile(dir + '/node/targets.d.ts', { inexact: false }));
+fs.writeFileSync(dir + '/node/targets.js.flow', '// @flow\n\n' + targets)
diff --git a/scripts/build-npm.js b/scripts/build-npm.js
new file mode 100644
index 0000000..ca89d66
--- /dev/null
+++ b/scripts/build-npm.js
@@ -0,0 +1,166 @@
+const fs = require('fs');
+const pkg = require('../package.json');
+
+const dir = `${__dirname}/..`;
+
+// Add `libc` fields only to platforms that have libc(Standard C library).
+const triples = [
+  {
+    name: 'x86_64-apple-darwin',
+  },
+  {
+    name: 'x86_64-unknown-linux-gnu',
+    libc: 'glibc',
+  },
+  {
+    name: 'x86_64-pc-windows-msvc',
+  },
+  {
+    name: 'aarch64-pc-windows-msvc'
+  },
+  {
+    name: 'aarch64-apple-darwin',
+  },
+  {
+    name: 'aarch64-unknown-linux-gnu',
+    libc: 'glibc',
+  },
+  {
+    name: 'armv7-unknown-linux-gnueabihf',
+  },
+  {
+    name: 'aarch64-unknown-linux-musl',
+    libc: 'musl',
+  },
+  {
+    name: 'x86_64-unknown-linux-musl',
+    libc: 'musl',
+  },
+  {
+    name: 'x86_64-unknown-freebsd'
+  }
+];
+const cpuToNodeArch = {
+  x86_64: 'x64',
+  aarch64: 'arm64',
+  i686: 'ia32',
+  armv7: 'arm',
+};
+const sysToNodePlatform = {
+  linux: 'linux',
+  freebsd: 'freebsd',
+  darwin: 'darwin',
+  windows: 'win32',
+};
+
+let optionalDependencies = {};
+let cliOptionalDependencies = {};
+
+try {
+  fs.mkdirSync(dir + '/npm');
+} catch (err) { }
+
+for (let triple of triples) {
+  // Add the libc field to package.json to avoid downloading both
+  // `gnu` and `musl` packages in Linux.
+  const libc = triple.libc;
+  let [cpu, , os, abi] = triple.name.split('-');
+  cpu = cpuToNodeArch[cpu] || cpu;
+  os = sysToNodePlatform[os] || os;
+
+  let t = `${os}-${cpu}`;
+  if (abi) {
+    t += '-' + abi;
+  }
+
+  buildNode(triple.name, cpu, os, libc, t);
+  buildCLI(triple.name, cpu, os, libc, t);
+}
+
+pkg.optionalDependencies = optionalDependencies;
+fs.writeFileSync(`${dir}/package.json`, JSON.stringify(pkg, false, 2) + '\n');
+
+let cliPkg = { ...pkg };
+cliPkg.name += '-cli';
+cliPkg.bin = {
+  'lightningcss': 'lightningcss'
+};
+delete cliPkg.main;
+delete cliPkg.napi;
+delete cliPkg.exports;
+delete cliPkg.devDependencies;
+delete cliPkg.targets;
+delete cliPkg.types;
+cliPkg.files = ['lightningcss', 'postinstall.js'];
+cliPkg.optionalDependencies = cliOptionalDependencies;
+cliPkg.scripts = {
+  postinstall: 'node postinstall.js'
+};
+
+fs.writeFileSync(`${dir}/cli/package.json`, JSON.stringify(cliPkg, false, 2) + '\n');
+fs.copyFileSync(`${dir}/README.md`, `${dir}/cli/README.md`);
+fs.copyFileSync(`${dir}/LICENSE`, `${dir}/cli/LICENSE`);
+
+function buildNode(triple, cpu, os, libc, t) {
+  let name = `lightningcss.${t}.node`;
+
+  let pkg2 = { ...pkg };
+  pkg2.name += '-' + t;
+  pkg2.os = [os];
+  pkg2.cpu = [cpu];
+  if (libc) {
+    pkg2.libc = [libc];
+  }
+  pkg2.main = name;
+  pkg2.files = [name];
+  delete pkg2.exports;
+  delete pkg2.napi;
+  delete pkg2.devDependencies;
+  delete pkg2.dependencies;
+  delete pkg2.optionalDependencies;
+  delete pkg2.targets;
+  delete pkg2.scripts;
+  delete pkg2.types;
+
+  optionalDependencies[pkg2.name] = pkg.version;
+
+  try {
+    fs.mkdirSync(dir + '/npm/node-' + t);
+  } catch (err) { }
+  fs.writeFileSync(`${dir}/npm/node-${t}/package.json`, JSON.stringify(pkg2, false, 2) + '\n');
+  fs.copyFileSync(`${dir}/artifacts/bindings-${triple}/${name}`, `${dir}/npm/node-${t}/${name}`);
+  fs.writeFileSync(`${dir}/npm/node-${t}/README.md`, `This is the ${triple} build of lightningcss. See https://github.com/parcel-bundler/lightningcss for details.`);
+  fs.copyFileSync(`${dir}/LICENSE`, `${dir}/npm/node-${t}/LICENSE`);
+}
+
+function buildCLI(triple, cpu, os, libc, t) {
+  let binary = os === 'win32' ? 'lightningcss.exe' : 'lightningcss';
+  let pkg2 = { ...pkg };
+  pkg2.name += '-cli-' + t;
+  pkg2.os = [os];
+  pkg2.cpu = [cpu];
+  pkg2.files = [binary];
+  if (libc) {
+    pkg2.libc = [libc];
+  }
+  delete pkg2.main;
+  delete pkg2.exports;
+  delete pkg2.napi;
+  delete pkg2.devDependencies;
+  delete pkg2.dependencies;
+  delete pkg2.optionalDependencies;
+  delete pkg2.targets;
+  delete pkg2.scripts;
+  delete pkg2.types;
+
+  cliOptionalDependencies[pkg2.name] = pkg.version;
+
+  try {
+    fs.mkdirSync(dir + '/npm/cli-' + t);
+  } catch (err) { }
+  fs.writeFileSync(`${dir}/npm/cli-${t}/package.json`, JSON.stringify(pkg2, false, 2) + '\n');
+  fs.copyFileSync(`${dir}/artifacts/bindings-${triple}/${binary}`, `${dir}/npm/cli-${t}/${binary}`);
+  fs.chmodSync(`${dir}/npm/cli-${t}/${binary}`, 0o755); // Ensure execute bit is set.
+  fs.writeFileSync(`${dir}/npm/cli-${t}/README.md`, `This is the ${triple} build of lightningcss-cli. See https://github.com/parcel-bundler/lightningcss for details.`);
+  fs.copyFileSync(`${dir}/LICENSE`, `${dir}/npm/cli-${t}/LICENSE`);
+}
diff --git a/scripts/build-prefixes.js b/scripts/build-prefixes.js
new file mode 100644
index 0000000..b4eace3
--- /dev/null
+++ b/scripts/build-prefixes.js
@@ -0,0 +1,675 @@
+const { execSync } = require('child_process');
+const prefixes = require('autoprefixer/data/prefixes');
+const browsers = require('caniuse-lite').agents;
+const unpack = require('caniuse-lite').feature;
+const features = require('caniuse-lite').features;
+const mdn = require('@mdn/browser-compat-data');
+const fs = require('fs');
+
+const BROWSER_MAPPING = {
+  and_chr: 'chrome',
+  and_ff: 'firefox',
+  ie_mob: 'ie',
+  op_mob: 'opera',
+  and_qq: null,
+  and_uc: null,
+  baidu: null,
+  bb: null,
+  kaios: null,
+  op_mini: null,
+  oculus: null,
+};
+
+const MDN_BROWSER_MAPPING = {
+  chrome_android: 'chrome',
+  firefox_android: 'firefox',
+  opera_android: 'opera',
+  safari_ios: 'ios_saf',
+  webview_ios: 'ios_saf',
+  samsunginternet_android: 'samsung',
+  webview_android: 'android',
+  oculus: null,
+};
+
+const latestBrowserVersions = {};
+for (let b in browsers) {
+  let versions = browsers[b].versions.slice(-10);
+  for (let i = versions.length - 1; i >= 0; i--) {
+    if (versions[i] != null && versions[i] != "all" && versions[i] != "TP") {
+      latestBrowserVersions[b] = versions[i];
+      break;
+    }
+  }
+}
+
+// Caniuse data for clip-path is incorrect.
+// https://github.com/Fyrd/caniuse/issues/6209
+prefixes['clip-path'].browsers = prefixes['clip-path'].browsers.filter(b => {
+  let [name, version] = b.split(' ');
+  return !(
+    (name === 'safari' && parseVersion(version) >= (9 << 16 | 1 << 8)) ||
+    (name === 'ios_saf' && parseVersion(version) >= (9 << 16 | 3 << 8))
+  );
+});
+
+prefixes['any-pseudo'] = {
+  browsers: Object.entries(mdn.css.selectors.is.__compat.support)
+    .flatMap(([key, value]) => {
+      if (Array.isArray(value)) {
+        key = MDN_BROWSER_MAPPING[key] || key;
+        let any = value.find(v => v.alternative_name?.includes('-any'))?.version_added;
+        let supported = value.find(x => x.version_added && !x.alternative_name)?.version_added;
+        if (any && supported) {
+          let parts = supported.split('.');
+          parts[0]--;
+          supported = parts.join('.');
+          return [`${key} ${any}}`, `${key} ${supported}`];
+        }
+      }
+
+      return [];
+    })
+}
+
+// Safari 4-13 supports background-clip: text with a prefix.
+prefixes['background-clip'].browsers.push('safari 13');
+prefixes['background-clip'].browsers.push('ios_saf 4', 'ios_saf 13');
+
+let flexSpec = {};
+let oldGradient = {};
+let p = new Map();
+for (let prop in prefixes) {
+  let browserMap = {};
+  for (let b of prefixes[prop].browsers) {
+    let [name, version, variant] = b.split(' ');
+    if (BROWSER_MAPPING[name] === null) {
+      continue;
+    }
+    let prefix = browsers[name].prefix_exceptions?.[version] || browsers[name].prefix;
+
+    // https://github.com/postcss/autoprefixer/blob/main/lib/hacks/backdrop-filter.js#L11
+    if (prefix === 'ms' && prop === 'backdrop-filter') {
+      prefix = 'webkit';
+    }
+
+    let origName = name;
+    let isCurrentVersion = version === latestBrowserVersions[name];
+    name = BROWSER_MAPPING[name] || name;
+    let v = parseVersion(version);
+    if (v == null) {
+      console.log('BAD VERSION', prop, name, version);
+      continue;
+    }
+    if (browserMap[name]?.[prefix] == null) {
+      browserMap[name] = browserMap[name] || {};
+      browserMap[name][prefix] = prefixes[prop].browsers.filter(b => b.startsWith(origName) || b.startsWith(name)).length === 1
+        ? isCurrentVersion ? [null, null] : [null, v]
+        : isCurrentVersion ? [v, null] : [v, v];
+    } else {
+      if (v < browserMap[name][prefix][0]) {
+        browserMap[name][prefix][0] = v;
+      }
+
+      if (isCurrentVersion && browserMap[name][prefix][0] != null) {
+        browserMap[name][prefix][1] = null;
+      } else if (v > browserMap[name][prefix][1] && browserMap[name][prefix][1] != null) {
+        browserMap[name][prefix][1] = v;
+      }
+    }
+
+    if (variant === '2009') {
+      if (flexSpec[name] == null) {
+        flexSpec[name] = [v, v];
+      } else {
+        if (v < flexSpec[name][0]) {
+          flexSpec[name][0] = v;
+        }
+
+        if (v > flexSpec[name][1]) {
+          flexSpec[name][1] = v;
+        }
+      }
+    } else if (variant === 'old' && prop.includes('gradient')) {
+      if (oldGradient[name] == null) {
+        oldGradient[name] = [v, v];
+      } else {
+        if (v < oldGradient[name][0]) {
+          oldGradient[name][0] = v;
+        }
+
+        if (v > oldGradient[name][1]) {
+          oldGradient[name][1] = v;
+        }
+      }
+    }
+  }
+  addValue(p, browserMap, prop);
+}
+
+
+function addValue(map, value, prop) {
+  let s = JSON.stringify(value);
+  let found = false;
+  for (let [key, val] of map) {
+    if (JSON.stringify(val) === s) {
+      key.push(prop);
+      found = true;
+      break;
+    }
+  }
+  if (!found) {
+    map.set([prop], value);
+  }
+}
+
+let cssFeatures = [
+  'css-sel2',
+  'css-sel3',
+  'css-gencontent',
+  'css-first-letter',
+  'css-first-line',
+  'css-in-out-of-range',
+  'form-validation',
+  'css-any-link',
+  'css-default-pseudo',
+  'css-dir-pseudo',
+  'css-focus-within',
+  'css-focus-visible',
+  'css-indeterminate-pseudo',
+  'css-matches-pseudo',
+  'css-optional-pseudo',
+  'css-placeholder-shown',
+  'dialog',
+  'fullscreen',
+  'css-marker-pseudo',
+  'css-placeholder',
+  'css-selection',
+  'css-case-insensitive',
+  'css-read-only-write',
+  'css-autofill',
+  'css-namespaces',
+  'shadowdomv1',
+  'css-rrggbbaa',
+  'css-nesting',
+  'css-not-sel-list',
+  'css-has',
+  'font-family-system-ui',
+  'extended-system-fonts',
+  'calc'
+];
+
+let cssFeatureMappings = {
+  'css-dir-pseudo': 'DirSelector',
+  'css-rrggbbaa': 'HexAlphaColors',
+  'css-not-sel-list': 'NotSelectorList',
+  'css-has': 'HasSelector',
+  'css-matches-pseudo': 'IsSelector',
+  'css-sel2': 'Selectors2',
+  'css-sel3': 'Selectors3',
+  'calc': 'CalcFunction'
+};
+
+let cssFeatureOverrides = {
+  // Safari supports the ::marker pseudo element, but only supports styling some properties.
+  // However this does not break using the selector itself, so ignore for our purposes.
+  // https://bugs.webkit.org/show_bug.cgi?id=204163
+  // https://github.com/parcel-bundler/lightningcss/issues/508
+  'css-marker-pseudo': {
+    safari: {
+      'y #1': 'y'
+    }
+  }
+};
+
+let compat = new Map();
+for (let feature of cssFeatures) {
+  let data = unpack(features[feature]);
+  let overrides = cssFeatureOverrides[feature];
+  let browserMap = {};
+  for (let name in data.stats) {
+    if (BROWSER_MAPPING[name] === null) {
+      continue;
+    }
+
+    name = BROWSER_MAPPING[name] || name;
+    let browserOverrides = overrides?.[name];
+    for (let version in data.stats[name]) {
+      let value = data.stats[name][version];
+      value = browserOverrides?.[value] || value;
+      if (value === 'y') {
+        let v = parseVersion(version);
+        if (v == null) {
+          console.log('BAD VERSION', feature, name, version);
+          continue;
+        }
+
+        if (browserMap[name] == null || v < browserMap[name]) {
+          browserMap[name] = v;
+        }
+      }
+    }
+  }
+
+  let name = (cssFeatureMappings[feature] || feature).replace(/^css-/, '');
+  addValue(compat, browserMap, name);
+}
+
+// No browser supports custom media queries yet.
+addValue(compat, {}, 'custom-media-queries');
+
+let mdnFeatures = {
+  doublePositionGradients: mdn.css.types.gradient['radial-gradient'].doubleposition.__compat.support,
+  clampFunction: mdn.css.types.clamp.__compat.support,
+  placeSelf: mdn.css.properties['place-self'].__compat.support,
+  placeContent: mdn.css.properties['place-content'].__compat.support,
+  placeItems: mdn.css.properties['place-items'].__compat.support,
+  overflowShorthand: mdn.css.properties['overflow'].multiple_keywords.__compat.support,
+  mediaRangeSyntax: mdn.css['at-rules'].media.range_syntax.__compat.support,
+  mediaIntervalSyntax: Object.fromEntries(
+    Object.entries(mdn.css['at-rules'].media.range_syntax.__compat.support)
+      .map(([browser, value]) => {
+        // Firefox supported only ranges and not intervals for a while.
+        if (Array.isArray(value)) {
+          value = value.filter(v => !v.partial_implementation)
+        } else if (value.partial_implementation) {
+          value = undefined;
+        }
+
+        return [browser, value];
+      })
+  ),
+  logicalBorders: mdn.css.properties['border-inline-start'].__compat.support,
+  logicalBorderShorthand: mdn.css.properties['border-inline'].__compat.support,
+  logicalBorderRadius: mdn.css.properties['border-start-start-radius'].__compat.support,
+  logicalMargin: mdn.css.properties['margin-inline-start'].__compat.support,
+  logicalMarginShorthand: mdn.css.properties['margin-inline'].__compat.support,
+  logicalPadding: mdn.css.properties['padding-inline-start'].__compat.support,
+  logicalPaddingShorthand: mdn.css.properties['padding-inline'].__compat.support,
+  logicalInset: mdn.css.properties['inset-inline-start'].__compat.support,
+  logicalSize: mdn.css.properties['inline-size'].__compat.support,
+  logicalTextAlign: mdn.css.properties['text-align'].start.__compat.support,
+  labColors: mdn.css.types.color.lab.__compat.support,
+  oklabColors: mdn.css.types.color.oklab.__compat.support,
+  colorFunction: mdn.css.types.color.color.__compat.support,
+  spaceSeparatedColorNotation: mdn.css.types.color.rgb.space_separated_parameters.__compat.support,
+  textDecorationThicknessPercent: mdn.css.properties['text-decoration-thickness'].percentage.__compat.support,
+  textDecorationThicknessShorthand: mdn.css.properties['text-decoration'].includes_thickness.__compat.support,
+  cue: mdn.css.selectors.cue.__compat.support,
+  cueFunction: mdn.css.selectors.cue.selector_argument.__compat.support,
+  anyPseudo: Object.fromEntries(
+    Object.entries(mdn.css.selectors.is.__compat.support)
+      .map(([key, value]) => {
+        if (Array.isArray(value)) {
+          value = value
+            .filter(v => v.alternative_name?.includes('-any'))
+            .map(({ alternative_name, ...other }) => other);
+        }
+
+        if (value && value.length) {
+          return [key, value];
+        } else {
+          return [key, { version_added: false }];
+        }
+      })
+  ),
+  partPseudo: mdn.css.selectors.part.__compat.support,
+  imageSet: mdn.css.types.image['image-set'].__compat.support,
+  xResolutionUnit: mdn.css.types.resolution.x.__compat.support,
+  nthChildOf: mdn.css.selectors['nth-child'].of_syntax.__compat.support,
+  minFunction: mdn.css.types.min.__compat.support,
+  maxFunction: mdn.css.types.max.__compat.support,
+  roundFunction: mdn.css.types.round.__compat.support,
+  remFunction: mdn.css.types.rem.__compat.support,
+  modFunction: mdn.css.types.mod.__compat.support,
+  absFunction: mdn.css.types.abs.__compat.support,
+  signFunction: mdn.css.types.sign.__compat.support,
+  hypotFunction: mdn.css.types.hypot.__compat.support,
+  gradientInterpolationHints: mdn.css.types.gradient['linear-gradient'].interpolation_hints.__compat.support,
+  borderImageRepeatRound: mdn.css.properties['border-image-repeat'].round.__compat.support,
+  borderImageRepeatSpace: mdn.css.properties['border-image-repeat'].space.__compat.support,
+  fontSizeRem: mdn.css.properties['font-size'].rem_values.__compat.support,
+  fontSizeXXXLarge: mdn.css.properties['font-size']['xxx-large'].__compat.support,
+  fontStyleObliqueAngle: mdn.css.properties['font-style']['oblique-angle'].__compat.support,
+  fontWeightNumber: mdn.css.properties['font-weight'].number.__compat.support,
+  fontStretchPercentage: mdn.css.properties['font-stretch'].percentage.__compat.support,
+  lightDark: mdn.css.types.color['light-dark'].__compat.support,
+  accentSystemColor: mdn.css.types.color['system-color'].accentcolor_accentcolortext.__compat.support,
+  animationTimelineShorthand: mdn.css.properties.animation['animation-timeline_included'].__compat.support,
+  viewTransition: mdn.css.selectors['view-transition'].__compat.support,
+  detailsContent: mdn.css.selectors['details-content'].__compat.support,
+  targetText: mdn.css.selectors['target-text'].__compat.support,
+  picker: mdn.css.selectors.picker.__compat.support,
+  pickerIcon: mdn.css.selectors['picker-icon'].__compat.support,
+  checkmark: mdn.css.selectors.checkmark.__compat.support,
+};
+
+for (let key in mdn.css.types.length) {
+  if (key === '__compat') {
+    continue;
+  }
+
+  let feat = key.includes('_')
+    ? key.replace(/_([a-z])/g, (_, l) => l.toUpperCase())
+    : key + 'Unit';
+
+  mdnFeatures[feat] = mdn.css.types.length[key].__compat.support;
+}
+
+for (let key in mdn.css.types.gradient) {
+  if (key === '__compat') {
+    continue;
+  }
+
+  let feat = key.replace(/-([a-z])/g, (_, l) => l.toUpperCase());
+  mdnFeatures[feat] = mdn.css.types.gradient[key].__compat.support;
+}
+
+const nonStandardListStyleType = new Set([
+  // https://developer.mozilla.org/en-US/docs/Web/CSS/list-style-type#non-standard_extensions
+  'ethiopic-halehame',
+  'ethiopic-halehame-am',
+  'ethiopic-halehame-ti-er',
+  'ethiopic-halehame-ti-et',
+  'hangul',
+  'hangul-consonant',
+  'urdu',
+  'cjk-ideographic',
+  // https://github.com/w3c/csswg-drafts/issues/135
+  'upper-greek'
+]);
+
+for (let key in mdn.css.properties['list-style-type']) {
+  if (key === '__compat' || nonStandardListStyleType.has(key) || mdn.css.properties['list-style-type'][key].__compat.support.chrome.version_removed) {
+    continue;
+  }
+
+  let feat = key[0].toUpperCase() + key.slice(1).replace(/-([a-z])/g, (_, l) => l.toUpperCase()) + 'ListStyleType';
+  mdnFeatures[feat] = mdn.css.properties['list-style-type'][key].__compat.support;
+}
+
+for (let key in mdn.css.properties['width']) {
+  if (key === '__compat' || key === 'animatable') {
+    continue;
+  }
+
+  let feat = key[0].toUpperCase() + key.slice(1).replace(/[-_]([a-z])/g, (_, l) => l.toUpperCase()) + 'Size';
+  mdnFeatures[feat] = mdn.css.properties['width'][key].__compat.support;
+}
+
+Object.entries(mdn.css.properties.width.stretch.__compat.support)
+  .filter(([, v]) => v.alternative_name)
+  .forEach(([k, v]) => {
+    let name = v.alternative_name.slice(1).replace(/[-_]([a-z])/g, (_, l) => l.toUpperCase()) + 'Size';
+    mdnFeatures[name] ??= {};
+    mdnFeatures[name][k] = {version_added: v.version_added};
+  });
+
+for (let feature in mdnFeatures) {
+  let browserMap = {};
+  for (let name in mdnFeatures[feature]) {
+    if (MDN_BROWSER_MAPPING[name] === null) {
+      continue;
+    }
+
+    let feat = mdnFeatures[feature][name];
+    let version;
+    if (Array.isArray(feat)) {
+      version = feat.filter(x => x.version_added && !x.alternative_name && !x.flags).sort((a, b) => parseVersion(a.version_added) < parseVersion(b.version_added) ? -1 : 1)[0].version_added;
+    } else if (!feat.alternative_name && !feat.flags) {
+      version = feat.version_added;
+    }
+
+    if (!version) {
+      continue;
+    }
+
+    let v = parseVersion(version);
+    if (v == null) {
+      console.log('BAD VERSION', feature, name, version);
+      continue;
+    }
+
+    name = MDN_BROWSER_MAPPING[name] || name;
+    browserMap[name] = v;
+  }
+
+  addValue(compat, browserMap, feature);
+}
+
+addValue(compat, {
+  safari: parseVersion('10.1'),
+  ios_saf: parseVersion('10.3')
+}, 'p3Colors');
+
+addValue(compat, {
+  // https://github.com/WebKit/WebKit/commit/baed0d8b0abf366e1d9a6105dc378c59a5f21575
+  safari: parseVersion('10.1'),
+  ios_saf: parseVersion('10.3')
+}, 'LangSelectorList');
+
+let prefixMapping = {
+  webkit: 'WebKit',
+  moz: 'Moz',
+  ms: 'Ms',
+  o: 'O'
+};
+
+let flags = [
+  'Nesting',
+  'NotSelectorList',
+  'DirSelector',
+  'LangSelectorList',
+  'IsSelector',
+  'TextDecorationThicknessPercent',
+  'MediaIntervalSyntax',
+  'MediaRangeSyntax',
+  'CustomMediaQueries',
+  'ClampFunction',
+  'ColorFunction',
+  'OklabColors',
+  'LabColors',
+  'P3Colors',
+  'HexAlphaColors',
+  'SpaceSeparatedColorNotation',
+  'FontFamilySystemUi',
+  'DoublePositionGradients',
+  'VendorPrefixes',
+  'LogicalProperties',
+  'LightDark',
+  ['Selectors', ['Nesting', 'NotSelectorList', 'DirSelector', 'LangSelectorList', 'IsSelector']],
+  ['MediaQueries', ['MediaIntervalSyntax', 'MediaRangeSyntax', 'CustomMediaQueries']],
+  ['Colors', ['ColorFunction', 'OklabColors', 'LabColors', 'P3Colors', 'HexAlphaColors', 'SpaceSeparatedColorNotation', 'LightDark']],
+];
+
+let enumify = (f) => f.replace(/^@([a-z])/, (_, x) => 'At' + x.toUpperCase()).replace(/^::([a-z])/, (_, x) => 'PseudoElement' + x.toUpperCase()).replace(/^:([a-z])/, (_, x) => 'PseudoClass' + x.toUpperCase()).replace(/(^|-)([a-z])/g, (_, a, x) => x.toUpperCase())
+
+let allBrowsers = Object.keys(browsers).filter(b => !(b in BROWSER_MAPPING)).sort();
+let browsersRs = `pub struct Browsers {
+  pub ${allBrowsers.join(': Option<u32>,\n  pub ')}: Option<u32>
+}`;
+let flagsRs = `pub struct Features: u32 {
+    ${flags.map((flag, i) => {
+      if (Array.isArray(flag)) {
+        return `const ${flag[0]} = ${flag[1].map(f => `Self::${f}.bits()`).join(' | ')};`
+      } else {
+        return `const ${flag} = 1 << ${i};`;
+      }
+    }).join('\n    ')}
+  }`;
+let targets = fs.readFileSync('src/targets.rs', 'utf8')
+  .replace(/pub struct Browsers \{((?:.|\n)+?)\}/, browsersRs)
+  .replace(/pub struct Features: u32 \{((?:.|\n)+?)\}/, flagsRs);
+
+fs.writeFileSync('src/targets.rs', targets);
+execSync('rustfmt src/targets.rs');
+
+let targets_dts = `// This file is autogenerated by build-prefixes.js. DO NOT EDIT!
+
+export interface Targets {
+  ${allBrowsers.join('?: number,\n  ')}?: number
+}
+
+export const Features: {
+  ${flags.map((flag, i) => {
+    if (Array.isArray(flag)) {
+      return `${flag[0]}: ${flag[1].reduce((p, f) => p | (1 << flags.indexOf(f)), 0)},`
+    } else {
+      return `${flag}: ${1 << i},`;
+    }
+  }).join('\n  ')}
+};
+`;
+
+fs.writeFileSync('node/targets.d.ts', targets_dts);
+
+let flagsJs = `// This file is autogenerated by build-prefixes.js. DO NOT EDIT!
+
+exports.Features = {
+  ${flags.map((flag, i) => {
+    if (Array.isArray(flag)) {
+      return `${flag[0]}: ${flag[1].reduce((p, f) => p | (1 << flags.indexOf(f)), 0)},`
+    } else {
+      return `${flag}: ${1 << i},`;
+    }
+  }).join('\n  ')}
+};
+`;
+
+fs.writeFileSync('node/flags.js', flagsJs);
+
+let s = `// This file is autogenerated by build-prefixes.js. DO NOT EDIT!
+
+use crate::vendor_prefix::VendorPrefix;
+use crate::targets::Browsers;
+
+#[allow(dead_code)]
+pub enum Feature {
+  ${[...p.keys()].flat().map(enumify).sort().join(',\n  ')}
+}
+
+impl Feature {
+  pub fn prefixes_for(&self, browsers: Browsers) -> VendorPrefix {
+    let mut prefixes = VendorPrefix::None;
+    match self {
+      ${[...p].map(([features, versions]) => {
+  return `${features.map(name => `Feature::${enumify(name)}`).join(' |\n      ')} => {
+        ${Object.entries(versions).map(([name, prefixes]) => {
+          let needsVersion = !Object.values(prefixes).every(([min, max]) => min == null && max == null);
+    return `if ${needsVersion ? `let Some(version) = browsers.${name}` : `browsers.${name}.is_some()`} {
+          ${Object.entries(prefixes).map(([prefix, [min, max]]) => {
+      if (!prefixMapping[prefix]) {
+        throw new Error('Missing prefix ' + prefix);
+      }
+      let addPrefix = `prefixes |= VendorPrefix::${prefixMapping[prefix]};`;
+      let condition;
+      if (min == null && max == null) {
+        return addPrefix;
+      } else if (min == null) {
+        condition = `version <= ${max}`;
+      } else if (max == null) {
+        condition = `version >= ${min}`;
+      } else if (min == max) {
+        condition = `version == ${min}`;
+      } else {
+        condition = `version >= ${min} && version <= ${max}`;
+      }
+
+      return `if ${condition} {
+            ${addPrefix}
+          }`
+    }).join('\n          ')}
+        }`;
+  }).join('\n        ')}
+      }`
+}).join(',\n      ')}
+    }
+    prefixes
+  }
+}
+
+pub fn is_flex_2009(browsers: Browsers) -> bool {
+  ${Object.entries(flexSpec).map(([name, [min, max]]) => {
+  return `if let Some(version) = browsers.${name} {
+    if version >= ${min} && version <= ${max} {
+      return true;
+    }
+  }`;
+}).join('\n  ')}
+  false
+}
+
+pub fn is_webkit_gradient(browsers: Browsers) -> bool {
+  ${Object.entries(oldGradient).map(([name, [min, max]]) => {
+  return `if let Some(version) = browsers.${name} {
+    if version >= ${min} && version <= ${max} {
+      return true;
+    }
+  }`;
+}).join('\n  ')}
+  false
+}
+`;
+
+fs.writeFileSync('src/prefixes.rs', s);
+execSync('rustfmt src/prefixes.rs');
+
+let c = `// This file is autogenerated by build-prefixes.js. DO NOT EDIT!
+
+use crate::targets::Browsers;
+
+#[allow(dead_code)]
+#[derive(Clone, Copy, PartialEq)]
+pub enum Feature {
+  ${[...compat.keys()].flat().map(enumify).sort().join(',\n  ')}
+}
+
+impl Feature {
+  pub fn is_compatible(&self, browsers: Browsers) -> bool {
+    match self {
+      ${[...compat].map(([features, supportedBrowsers]) =>
+  `${features.map(name => `Feature::${enumify(name)}`).join(' |\n      ')} => {` + (Object.entries(supportedBrowsers).length === 0 ? '\n        return false\n      }' : `
+        ${Object.entries(supportedBrowsers).map(([browser, min]) =>
+    `if let Some(version) = browsers.${browser} {
+          if version < ${min} {
+            return false
+          }
+        }`).join('\n        ')}${Object.keys(supportedBrowsers).length === allBrowsers.length ? '' : `\n        if ${allBrowsers.filter(b => !supportedBrowsers[b]).map(browser => `browsers.${browser}.is_some()`).join(' || ')} {
+          return false
+        }`}
+      }`
+  )).join('\n      ')}
+    }
+    true
+  }
+
+  pub fn is_partially_compatible(&self, targets: Browsers) -> bool {
+    let mut browsers = Browsers::default();
+    ${allBrowsers.map(browser => `if targets.${browser}.is_some() {
+      browsers.${browser} = targets.${browser};
+      if self.is_compatible(browsers) {
+        return true
+      }
+      browsers.${browser} = None;
+    }\n`).join('    ')}
+    false
+  }
+}
+`;
+
+fs.writeFileSync('src/compat.rs', c);
+execSync('rustfmt src/compat.rs');
+
+
+function parseVersion(version) {
+  version = version.replace('≤', '');
+  let [major, minor = '0', patch = '0'] = version
+    .split('-')[0]
+    .split('.')
+    .map(v => parseInt(v, 10));
+
+  if (isNaN(major) || isNaN(minor) || isNaN(patch)) {
+    return null;
+  }
+
+  return major << 16 | minor << 8 | patch;
+}
diff --git a/scripts/build-wasm.js b/scripts/build-wasm.js
new file mode 100644
index 0000000..718821d
--- /dev/null
+++ b/scripts/build-wasm.js
@@ -0,0 +1,85 @@
+const esbuild = require('esbuild');
+const exec = require('child_process').execSync;
+const fs = require('fs');
+const pkg = require('../package.json');
+
+const dir = `${__dirname}/..`;
+
+let b = fs.readFileSync(`${dir}/node/browserslistToTargets.js`, 'utf8');
+b = b.replace('module.exports = browserslistToTargets;', 'export {browserslistToTargets};');
+fs.writeFileSync(`${dir}/wasm/browserslistToTargets.js`, b);
+
+let flags = fs.readFileSync(`${dir}/node/flags.js`, 'utf8');
+flags = flags.replace('exports.Features =', 'export const Features =');
+fs.writeFileSync(`${dir}/wasm/flags.js`, flags);
+
+let composeVisitors = fs.readFileSync(`${dir}/node/composeVisitors.js`, 'utf8');
+composeVisitors = composeVisitors.replace('module.exports = composeVisitors', 'export { composeVisitors }');
+fs.writeFileSync(`${dir}/wasm/composeVisitors.js`, composeVisitors);
+
+let dts = fs.readFileSync(`${dir}/node/index.d.ts`, 'utf8');
+dts = dts.replace(/: Buffer/g, ': Uint8Array');
+dts += `
+/** Initializes the web assembly module. */
+export default function init(input?: string | URL | Request): Promise<void>;
+`;
+fs.writeFileSync(`${dir}/wasm/index.d.ts`, dts);
+fs.copyFileSync(`${dir}/node/targets.d.ts`, `${dir}/wasm/targets.d.ts`);
+fs.copyFileSync(`${dir}/node/ast.d.ts`, `${dir}/wasm/ast.d.ts`);
+fs.cpSync(`${dir}/node_modules/napi-wasm`, `${dir}/wasm/node_modules/napi-wasm`, {recursive: true});
+
+let readme = fs.readFileSync(`${dir}/README.md`, 'utf8');
+readme = readme.replace('# ⚡️ Lightning CSS', '# ⚡️ lightningcss-wasm');
+fs.writeFileSync(`${dir}/wasm/README.md`, readme);
+
+const cjsBuild = {
+  entryPoints: [
+    `${dir}/wasm/wasm-node.mjs`,
+    `${dir}/wasm/index.mjs`,
+  ],
+  bundle: true,
+  format: 'cjs',
+  platform: 'node',
+  packages: 'external',
+  outdir: `${dir}/wasm`,
+  outExtension: { '.js': '.cjs' },
+  inject: [`${dir}/wasm/import.meta.url-polyfill.js`],
+  define: { 'import.meta.url': 'import_meta_url' },
+};
+esbuild.build(cjsBuild).catch(console.error);
+
+let wasmPkg = { ...pkg };
+wasmPkg.name = 'lightningcss-wasm';
+wasmPkg.type = 'module';
+wasmPkg.main = 'index.mjs';
+wasmPkg.module = 'index.mjs';
+wasmPkg.exports = {
+  '.': {
+    types: './index.d.ts',
+    node: {
+      import: './wasm-node.mjs',
+      require: './wasm-node.cjs'
+    },
+    default: {
+      import: './index.mjs',
+      require: './index.cjs'
+    }
+  },
+  // Allow esbuild to import the wasm file
+  // without copying it in the src directory.
+  // Simplifies loading it in the browser when used in a library.
+  './lightningcss_node.wasm': './lightningcss_node.wasm'
+};
+wasmPkg.types = 'index.d.ts';
+wasmPkg.sideEffects = false;
+wasmPkg.files = ['*.js', '*.cjs', '*.mjs', '*.d.ts', '*.flow', '*.wasm'];
+wasmPkg.dependencies = {
+  'napi-wasm': pkg.devDependencies['napi-wasm']
+};
+wasmPkg.bundledDependencies = ['napi-wasm']; // for stackblitz
+delete wasmPkg.napi;
+delete wasmPkg.devDependencies;
+delete wasmPkg.optionalDependencies;
+delete wasmPkg.targets;
+delete wasmPkg.scripts;
+fs.writeFileSync(`${dir}/wasm/package.json`, JSON.stringify(wasmPkg, false, 2) + '\n');
diff --git a/scripts/build.js b/scripts/build.js
new file mode 100644
index 0000000..59807fa
--- /dev/null
+++ b/scripts/build.js
@@ -0,0 +1,46 @@
+const { spawn, execSync } = require('child_process');
+
+let release = process.argv.includes('--release');
+build().catch((err) => {
+  console.error(err);
+  process.exit(1);
+});
+
+async function build() {
+  if (process.platform === 'darwin') {
+    setupMacBuild();
+  }
+
+  await new Promise((resolve, reject) => {
+    let args = ['build', '--platform', '--cargo-cwd', 'node'];
+    if (release) {
+      args.push('--release');
+    }
+
+    if (process.env.RUST_TARGET) {
+      args.push('--target', process.env.RUST_TARGET);
+    }
+
+    let yarn = spawn('napi', args, {
+      stdio: 'inherit',
+      cwd: __dirname + '/../',
+      shell: true,
+    });
+
+    yarn.on('error', reject);
+    yarn.on('close', resolve);
+  });
+}
+
+// This forces Clang/LLVM to be used as a C compiler instead of GCC.
+// This is necessary for cross-compilation for Apple Silicon in GitHub Actions.
+function setupMacBuild() {
+  process.env.CC = execSync('xcrun -f clang', { encoding: 'utf8' }).trim();
+  process.env.CXX = execSync('xcrun -f clang++', { encoding: 'utf8' }).trim();
+
+  let sysRoot = execSync('xcrun --sdk macosx --show-sdk-path', {
+    encoding: 'utf8',
+  }).trim();
+  process.env.CFLAGS = `-isysroot ${sysRoot} -isystem ${sysRoot}`;
+  process.env.MACOSX_DEPLOYMENT_TARGET = '10.9';
+}
diff --git a/selectors/Cargo.toml b/selectors/Cargo.toml
new file mode 100644
index 0000000..824b2f2
--- /dev/null
+++ b/selectors/Cargo.toml
@@ -0,0 +1,38 @@
+[package]
+name = "parcel_selectors"
+version = "0.28.2"
+authors = ["The Servo Project Developers"]
+documentation = "https://docs.rs/parcel_selectors/"
+description = "CSS Selectors matching for Rust - forked for lightningcss"
+repository = "https://github.com/parcel-bundler/lightningcss"
+readme = "README.md"
+keywords = ["css", "selectors"]
+license = "MPL-2.0"
+build = "build.rs"
+edition = "2021"
+
+[lib]
+name = "parcel_selectors"
+path = "lib.rs"
+
+[features]
+bench = []
+jsonschema = ["serde", "schemars"]
+into_owned = ["static-self"]
+smallvec = ["static-self/smallvec"]
+serde = ["dep:serde", "smallvec/serde"]
+
+[dependencies]
+bitflags = "2.2.1"
+cssparser = "0.33.0"
+rustc-hash = "2"
+log = "0.4"
+phf = "0.11.2"
+precomputed-hash = "0.1"
+smallvec = "1.0"
+serde = { version = "1.0.201", features = ["derive"], optional = true }
+schemars = { version = "0.8.19", features = ["smallvec"], optional = true }
+static-self = { version = "0.1.2", path = "../static-self", optional = true }
+
+[build-dependencies]
+phf_codegen = "0.11"
diff --git a/selectors/LICENSE b/selectors/LICENSE
new file mode 100644
index 0000000..d8b475b
--- /dev/null
+++ b/selectors/LICENSE
@@ -0,0 +1,374 @@
+ Mozilla Public License Version 2.0
+==================================
+
+1. Definitions
+--------------
+
+1.1. "Contributor"
+means each individual or legal entity that creates, contributes to
+the creation of, or owns Covered Software.
+
+1.2. "Contributor Version"
+means the combination of the Contributions of others (if any) used
+by a Contributor and that particular Contributor's Contribution.
+
+1.3. "Contribution"
+means Covered Software of a particular Contributor.
+
+1.4. "Covered Software"
+means Source Code Form to which the initial Contributor has attached
+the notice in Exhibit A, the Executable Form of such Source Code
+Form, and Modifications of such Source Code Form, in each case
+including portions thereof.
+
+1.5. "Incompatible With Secondary Licenses"
+means
+
+(a) that the initial Contributor has attached the notice described
+in Exhibit B to the Covered Software; or
+
+(b) that the Covered Software was made available under the terms of
+version 1.1 or earlier of the License, but not also under the
+terms of a Secondary License.
+
+1.6. "Executable Form"
+means any form of the work other than Source Code Form.
+
+1.7. "Larger Work"
+means a work that combines Covered Software with other material, in
+a separate file or files, that is not Covered Software.
+
+1.8. "License"
+means this document.
+
+1.9. "Licensable"
+means having the right to grant, to the maximum extent possible,
+whether at the time of the initial grant or subsequently, any and
+all of the rights conveyed by this License.
+
+1.10. "Modifications"
+means any of the following:
+
+(a) any file in Source Code Form that results from an addition to,
+deletion from, or modification of the contents of Covered
+Software; or
+
+(b) any new file in Source Code Form that contains any Covered
+Software.
+
+1.11. "Patent Claims" of a Contributor
+means any patent claim(s), including without limitation, method,
+process, and apparatus claims, in any patent Licensable by such
+Contributor that would be infringed, but for the grant of the
+License, by the making, using, selling, offering for sale, having
+made, import, or transfer of either its Contributions or its
+Contributor Version.
+
+1.12. "Secondary License"
+means either the GNU General Public License, Version 2.0, the GNU
+Lesser General Public License, Version 2.1, the GNU Affero General
+Public License, Version 3.0, or any later versions of those
+licenses.
+
+1.13. "Source Code Form"
+means the form of the work preferred for making modifications.
+
+1.14. "You" (or "Your")
+means an individual or a legal entity exercising rights under this
+License. For legal entities, "You" includes any entity that
+controls, is controlled by, or is under common control with You. For
+purposes of this definition, "control" means (a) the power, direct
+or indirect, to cause the direction or management of such entity,
+whether by contract or otherwise, or (b) ownership of more than
+fifty percent (50%) of the outstanding shares or beneficial
+ownership of such entity.
+
+2. License Grants and Conditions
+--------------------------------
+
+2.1. Grants
+
+Each Contributor hereby grants You a world-wide, royalty-free,
+non-exclusive license:
+
+(a) under intellectual property rights (other than patent or trademark)
+Licensable by such Contributor to use, reproduce, make available,
+modify, display, perform, distribute, and otherwise exploit its
+Contributions, either on an unmodified basis, with Modifications, or
+as part of a Larger Work; and
+
+(b) under Patent Claims of such Contributor to make, use, sell, offer
+for sale, have made, import, and otherwise transfer either its
+Contributions or its Contributor Version.
+
+2.2. Effective Date
+
+The licenses granted in Section 2.1 with respect to any Contribution
+become effective for each Contribution on the date the Contributor first
+distributes such Contribution.
+
+2.3. Limitations on Grant Scope
+
+The licenses granted in this Section 2 are the only rights granted under
+this License. No additional rights or licenses will be implied from the
+distribution or licensing of Covered Software under this License.
+Notwithstanding Section 2.1(b) above, no patent license is granted by a
+Contributor:
+
+(a) for any code that a Contributor has removed from Covered Software;
+or
+
+(b) for infringements caused by: (i) Your and any other third party's
+modifications of Covered Software, or (ii) the combination of its
+Contributions with other software (except as part of its Contributor
+Version); or
+
+(c) under Patent Claims infringed by Covered Software in the absence of
+its Contributions.
+
+This License does not grant any rights in the trademarks, service marks,
+or logos of any Contributor (except as may be necessary to comply with
+the notice requirements in Section 3.4).
+
+2.4. Subsequent Licenses
+
+No Contributor makes additional grants as a result of Your choice to
+distribute the Covered Software under a subsequent version of this
+License (see Section 10.2) or under the terms of a Secondary License (if
+permitted under the terms of Section 3.3).
+
+2.5. Representation
+
+Each Contributor represents that the Contributor believes its
+Contributions are its original creation(s) or it has sufficient rights
+to grant the rights to its Contributions conveyed by this License.
+
+2.6. Fair Use
+
+This License is not intended to limit any rights You have under
+applicable copyright doctrines of fair use, fair dealing, or other
+equivalents.
+
+2.7. Conditions
+
+Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
+in Section 2.1.
+
+3. Responsibilities
+-------------------
+
+3.1. Distribution of Source Form
+
+All distribution of Covered Software in Source Code Form, including any
+Modifications that You create or to which You contribute, must be under
+the terms of this License. You must inform recipients that the Source
+Code Form of the Covered Software is governed by the terms of this
+License, and how they can obtain a copy of this License. You may not
+attempt to alter or restrict the recipients' rights in the Source Code
+Form.
+
+3.2. Distribution of Executable Form
+
+If You distribute Covered Software in Executable Form then:
+
+(a) such Covered Software must also be made available in Source Code
+Form, as described in Section 3.1, and You must inform recipients of
+the Executable Form how they can obtain a copy of such Source Code
+Form by reasonable means in a timely manner, at a charge no more
+than the cost of distribution to the recipient; and
+
+(b) You may distribute such Executable Form under the terms of this
+License, or sublicense it under different terms, provided that the
+license for the Executable Form does not attempt to limit or alter
+the recipients' rights in the Source Code Form under this License.
+
+3.3. Distribution of a Larger Work
+
+You may create and distribute a Larger Work under terms of Your choice,
+provided that You also comply with the requirements of this License for
+the Covered Software. If the Larger Work is a combination of Covered
+Software with a work governed by one or more Secondary Licenses, and the
+Covered Software is not Incompatible With Secondary Licenses, this
+License permits You to additionally distribute such Covered Software
+under the terms of such Secondary License(s), so that the recipient of
+the Larger Work may, at their option, further distribute the Covered
+Software under the terms of either this License or such Secondary
+License(s).
+
+3.4. Notices
+
+You may not remove or alter the substance of any license notices
+(including copyright notices, patent notices, disclaimers of warranty,
+or limitations of liability) contained within the Source Code Form of
+the Covered Software, except that You may alter any license notices to
+the extent required to remedy known factual inaccuracies.
+
+3.5. Application of Additional Terms
+
+You may choose to offer, and to charge a fee for, warranty, support,
+indemnity or liability obligations to one or more recipients of Covered
+Software. However, You may do so only on Your own behalf, and not on
+behalf of any Contributor. You must make it absolutely clear that any
+such warranty, support, indemnity, or liability obligation is offered by
+You alone, and You hereby agree to indemnify every Contributor for any
+liability incurred by such Contributor as a result of warranty, support,
+indemnity or liability terms You offer. You may include additional
+disclaimers of warranty and limitations of liability specific to any
+jurisdiction.
+
+4. Inability to Comply Due to Statute or Regulation
+---------------------------------------------------
+
+If it is impossible for You to comply with any of the terms of this
+License with respect to some or all of the Covered Software due to
+statute, judicial order, or regulation then You must: (a) comply with
+the terms of this License to the maximum extent possible; and (b)
+describe the limitations and the code they affect. Such description must
+be placed in a text file included with all distributions of the Covered
+Software under this License. Except to the extent prohibited by statute
+or regulation, such description must be sufficiently detailed for a
+recipient of ordinary skill to be able to understand it.
+
+5. Termination
+--------------
+
+5.1. The rights granted under this License will terminate automatically
+if You fail to comply with any of its terms. However, if You become
+compliant, then the rights granted under this License from a particular
+Contributor are reinstated (a) provisionally, unless and until such
+Contributor explicitly and finally terminates Your grants, and (b) on an
+ongoing basis, if such Contributor fails to notify You of the
+non-compliance by some reasonable means prior to 60 days after You have
+come back into compliance. Moreover, Your grants from a particular
+Contributor are reinstated on an ongoing basis if such Contributor
+notifies You of the non-compliance by some reasonable means, this is the
+first time You have received notice of non-compliance with this License
+from such Contributor, and You become compliant prior to 30 days after
+Your receipt of the notice.
+
+5.2. If You initiate litigation against any entity by asserting a patent
+infringement claim (excluding declaratory judgment actions,
+counter-claims, and cross-claims) alleging that a Contributor Version
+directly or indirectly infringes any patent, then the rights granted to
+You by any and all Contributors for the Covered Software under Section
+2.1 of this License shall terminate.
+
+5.3. In the event of termination under Sections 5.1 or 5.2 above, all
+end user license agreements (excluding distributors and resellers) which
+have been validly granted by You or Your distributors under this License
+prior to termination shall survive termination.
+
+************************************************************************
+* *
+* 6. Disclaimer of Warranty *
+* ------------------------- *
+* *
+* Covered Software is provided under this License on an "as is" *
+* basis, without warranty of any kind, either expressed, implied, or *
+* statutory, including, without limitation, warranties that the *
+* Covered Software is free of defects, merchantable, fit for a *
+* particular purpose or non-infringing. The entire risk as to the *
+* quality and performance of the Covered Software is with You. *
+* Should any Covered Software prove defective in any respect, You *
+* (not any Contributor) assume the cost of any necessary servicing, *
+* repair, or correction. This disclaimer of warranty constitutes an *
+* essential part of this License. No use of any Covered Software is *
+* authorized under this License except under this disclaimer. *
+* *
+************************************************************************
+
+************************************************************************
+* *
+* 7. Limitation of Liability *
+* -------------------------- *
+* *
+* Under no circumstances and under no legal theory, whether tort *
+* (including negligence), contract, or otherwise, shall any *
+* Contributor, or anyone who distributes Covered Software as *
+* permitted above, be liable to You for any direct, indirect, *
+* special, incidental, or consequential damages of any character *
+* including, without limitation, damages for lost profits, loss of *
+* goodwill, work stoppage, computer failure or malfunction, or any *
+* and all other commercial damages or losses, even if such party *
+* shall have been informed of the possibility of such damages. This *
+* limitation of liability shall not apply to liability for death or *
+* personal injury resulting from such party's negligence to the *
+* extent applicable law prohibits such limitation. Some *
+* jurisdictions do not allow the exclusion or limitation of *
+* incidental or consequential damages, so this exclusion and *
+* limitation may not apply to You. *
+* *
+************************************************************************
+
+8. Litigation
+-------------
+
+Any litigation relating to this License may be brought only in the
+courts of a jurisdiction where the defendant maintains its principal
+place of business and such litigation shall be governed by laws of that
+jurisdiction, without reference to its conflict-of-law provisions.
+Nothing in this Section shall prevent a party's ability to bring
+cross-claims or counter-claims.
+
+9. Miscellaneous
+----------------
+
+This License represents the complete agreement concerning the subject
+matter hereof. If any provision of this License is held to be
+unenforceable, such provision shall be reformed only to the extent
+necessary to make it enforceable. Any law or regulation which provides
+that the language of a contract shall be construed against the drafter
+shall not be used to construe this License against a Contributor.
+
+10. Versions of the License
+---------------------------
+
+10.1. New Versions
+
+Mozilla Foundation is the license steward. Except as provided in Section
+10.3, no one other than the license steward has the right to modify or
+publish new versions of this License. Each version will be given a
+distinguishing version number.
+
+10.2. Effect of New Versions
+
+You may distribute the Covered Software under the terms of the version
+of the License under which You originally received the Covered Software,
+or under the terms of any subsequent version published by the license
+steward.
+
+10.3. Modified Versions
+
+If you create software not governed by this License, and you want to
+create a new license for such software, you may create and use a
+modified version of this License if you rename the license and remove
+any references to the name of the license steward (except to note that
+such modified license differs from this License).
+
+10.4. Distributing Source Code Form that is Incompatible With Secondary
+Licenses
+
+If You choose to distribute Source Code Form that is Incompatible With
+Secondary Licenses under the terms of this version of the License, the
+notice described in Exhibit B of this License must be attached.
+
+Exhibit A - Source Code Form License Notice
+-------------------------------------------
+
+This Source Code Form is subject to the terms of the Mozilla Public
+License, v. 2.0. If a copy of the MPL was not distributed with this
+file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+If it is not possible or desirable to put the notice in a particular
+file, then You may include the notice in a location (such as a LICENSE
+file in a relevant directory) where a recipient would be likely to look
+for such a notice.
+
+You may add additional accurate notices of copyright ownership.
+
+Exhibit B - "Incompatible With Secondary Licenses" Notice
+---------------------------------------------------------
+
+This Source Code Form is "Incompatible With Secondary Licenses", as
+defined by the Mozilla Public License, v. 2.0.
+
diff --git a/selectors/README.md b/selectors/README.md
new file mode 100644
index 0000000..ade2443
--- /dev/null
+++ b/selectors/README.md
@@ -0,0 +1,27 @@
+rust-selectors
+==============
+
+This is a fork of the `selectors` crate, updated to use the latest version of `cssparser`.
+
+* [![Build Status](https://travis-ci.com/servo/rust-selectors.svg?branch=master)](
+  https://travis-ci.com/servo/rust-selectors)
+* [Documentation](https://docs.rs/selectors/)
+* [crates.io](https://crates.io/crates/selectors)
+
+CSS Selectors library for Rust.
+Includes parsing and serialization of selectors,
+as well as matching against a generic tree of elements.
+Pseudo-elements and most pseudo-classes are generic as well.
+
+**Warning:** breaking changes are made to this library fairly frequently
+(13 times in 2016, for example).
+However you can use this crate without updating it that often,
+old versions stay available on crates.io and Cargo will only automatically update
+to versions that are numbered as compatible.
+
+To see how to use this library with your own tree representation,
+see [Kuchiki’s `src/select.rs`](https://github.com/kuchiki-rs/kuchiki/blob/master/src/select.rs).
+(Note however that Kuchiki is not always up to date with the latest rust-selectors version,
+so that code may need to be tweaked.)
+If you don’t already have a tree data structure,
+consider using [Kuchiki](https://github.com/kuchiki-rs/kuchiki) itself.
diff --git a/selectors/attr.rs b/selectors/attr.rs
new file mode 100644
index 0000000..0112b0f
--- /dev/null
+++ b/selectors/attr.rs
@@ -0,0 +1,248 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+use crate::parser::SelectorImpl;
+use cssparser::ToCss;
+use std::fmt;
+
+#[derive(Clone, Eq, PartialEq, Hash)]
+pub struct AttrSelectorWithOptionalNamespace<'i, Impl: SelectorImpl<'i>> {
+  pub namespace: Option<NamespaceConstraint<(Impl::NamespacePrefix, Impl::NamespaceUrl)>>,
+  pub local_name: Impl::LocalName,
+  pub local_name_lower: Impl::LocalName,
+  pub operation: ParsedAttrSelectorOperation<Impl::AttrValue>,
+  pub never_matches: bool,
+}
+
+#[cfg(feature = "into_owned")]
+impl<'any, 'i, Impl: SelectorImpl<'i>, NewSel> static_self::IntoOwned<'any>
+  for AttrSelectorWithOptionalNamespace<'i, Impl>
+where
+  Impl: static_self::IntoOwned<'any, Owned = NewSel>,
+  NewSel: SelectorImpl<'any>,
+  Impl::LocalName: static_self::IntoOwned<'any, Owned = NewSel::LocalName>,
+  Impl::NamespacePrefix: static_self::IntoOwned<'any, Owned = NewSel::NamespacePrefix>,
+  Impl::NamespaceUrl: static_self::IntoOwned<'any, Owned = NewSel::NamespaceUrl>,
+  Impl::AttrValue: static_self::IntoOwned<'any, Owned = NewSel::AttrValue>,
+{
+  type Owned = AttrSelectorWithOptionalNamespace<'any, NewSel>;
+
+  fn into_owned(self) -> Self::Owned {
+    AttrSelectorWithOptionalNamespace {
+      namespace: self.namespace.into_owned(),
+      local_name: self.local_name.into_owned(),
+      local_name_lower: self.local_name_lower.into_owned(),
+      operation: self.operation.into_owned(),
+      never_matches: self.never_matches,
+    }
+  }
+}
+
+impl<'i, Impl: SelectorImpl<'i>> AttrSelectorWithOptionalNamespace<'i, Impl> {
+  pub fn namespace(&self) -> Option<NamespaceConstraint<&Impl::NamespaceUrl>> {
+    self.namespace.as_ref().map(|ns| match ns {
+      NamespaceConstraint::Any => NamespaceConstraint::Any,
+      NamespaceConstraint::Specific((_, ref url)) => NamespaceConstraint::Specific(url),
+    })
+  }
+}
+
+#[derive(Clone, Eq, PartialEq, Hash)]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", rename_all = "kebab-case")
+)]
+#[cfg_attr(
+  feature = "jsonschema",
+  derive(schemars::JsonSchema),
+  schemars(rename = "NamespaceConstraint")
+)]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum NamespaceConstraint<NamespaceUrl> {
+  Any,
+
+  /// Empty string for no namespace
+  Specific(NamespaceUrl),
+}
+
+#[derive(Clone, Eq, PartialEq, Hash)]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum ParsedAttrSelectorOperation<AttrValue> {
+  Exists,
+  WithValue {
+    operator: AttrSelectorOperator,
+    case_sensitivity: ParsedCaseSensitivity,
+    expected_value: AttrValue,
+  },
+}
+
+pub enum AttrSelectorOperation<AttrValue> {
+  Exists,
+  WithValue {
+    operator: AttrSelectorOperator,
+    case_sensitivity: CaseSensitivity,
+    expected_value: AttrValue,
+  },
+}
+
+impl<AttrValue> AttrSelectorOperation<AttrValue> {
+  pub fn eval_str(&self, element_attr_value: &str) -> bool
+  where
+    AttrValue: AsRef<str>,
+  {
+    match *self {
+      AttrSelectorOperation::Exists => true,
+      AttrSelectorOperation::WithValue {
+        operator,
+        case_sensitivity,
+        ref expected_value,
+      } => operator.eval_str(element_attr_value, expected_value.as_ref(), case_sensitivity),
+    }
+  }
+}
+
+#[derive(Clone, Copy, Eq, PartialEq, Hash)]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum AttrSelectorOperator {
+  Equal,
+  Includes,
+  DashMatch,
+  Prefix,
+  Substring,
+  Suffix,
+}
+
+impl ToCss for AttrSelectorOperator {
+  fn to_css<W>(&self, dest: &mut W) -> fmt::Result
+  where
+    W: fmt::Write,
+  {
+    // https://drafts.csswg.org/cssom/#serializing-selectors
+    // See "attribute selector".
+    dest.write_str(match *self {
+      AttrSelectorOperator::Equal => "=",
+      AttrSelectorOperator::Includes => "~=",
+      AttrSelectorOperator::DashMatch => "|=",
+      AttrSelectorOperator::Prefix => "^=",
+      AttrSelectorOperator::Substring => "*=",
+      AttrSelectorOperator::Suffix => "$=",
+    })
+  }
+}
+
+impl AttrSelectorOperator {
+  pub fn eval_str(
+    self,
+    element_attr_value: &str,
+    attr_selector_value: &str,
+    case_sensitivity: CaseSensitivity,
+  ) -> bool {
+    let e = element_attr_value.as_bytes();
+    let s = attr_selector_value.as_bytes();
+    let case = case_sensitivity;
+    match self {
+      AttrSelectorOperator::Equal => case.eq(e, s),
+      AttrSelectorOperator::Prefix => e.len() >= s.len() && case.eq(&e[..s.len()], s),
+      AttrSelectorOperator::Suffix => e.len() >= s.len() && case.eq(&e[(e.len() - s.len())..], s),
+      AttrSelectorOperator::Substring => case.contains(element_attr_value, attr_selector_value),
+      AttrSelectorOperator::Includes => element_attr_value
+        .split(SELECTOR_WHITESPACE)
+        .any(|part| case.eq(part.as_bytes(), s)),
+      AttrSelectorOperator::DashMatch => {
+        case.eq(e, s) || (e.get(s.len()) == Some(&b'-') && case.eq(&e[..s.len()], s))
+      }
+    }
+  }
+}
+
+/// The definition of whitespace per CSS Selectors Level 3 § 4.
+pub static SELECTOR_WHITESPACE: &[char] = &[' ', '\t', '\n', '\r', '\x0C'];
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum ParsedCaseSensitivity {
+  // 's' was specified.
+  ExplicitCaseSensitive,
+  // 'i' was specified.
+  AsciiCaseInsensitive,
+  // No flags were specified and HTML says this is a case-sensitive attribute.
+  CaseSensitive,
+  // No flags were specified and HTML says this is a case-insensitive attribute.
+  AsciiCaseInsensitiveIfInHtmlElementInHtmlDocument,
+}
+
+impl Default for ParsedCaseSensitivity {
+  fn default() -> Self {
+    ParsedCaseSensitivity::CaseSensitive
+  }
+}
+
+impl ParsedCaseSensitivity {
+  pub fn to_unconditional(self, is_html_element_in_html_document: bool) -> CaseSensitivity {
+    match self {
+      ParsedCaseSensitivity::AsciiCaseInsensitiveIfInHtmlElementInHtmlDocument
+        if is_html_element_in_html_document =>
+      {
+        CaseSensitivity::AsciiCaseInsensitive
+      }
+      ParsedCaseSensitivity::AsciiCaseInsensitiveIfInHtmlElementInHtmlDocument => CaseSensitivity::CaseSensitive,
+      ParsedCaseSensitivity::CaseSensitive | ParsedCaseSensitivity::ExplicitCaseSensitive => {
+        CaseSensitivity::CaseSensitive
+      }
+      ParsedCaseSensitivity::AsciiCaseInsensitive => CaseSensitivity::AsciiCaseInsensitive,
+    }
+  }
+}
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
+pub enum CaseSensitivity {
+  CaseSensitive,
+  AsciiCaseInsensitive,
+}
+
+impl CaseSensitivity {
+  pub fn eq(self, a: &[u8], b: &[u8]) -> bool {
+    match self {
+      CaseSensitivity::CaseSensitive => a == b,
+      CaseSensitivity::AsciiCaseInsensitive => a.eq_ignore_ascii_case(b),
+    }
+  }
+
+  pub fn contains(self, haystack: &str, needle: &str) -> bool {
+    match self {
+      CaseSensitivity::CaseSensitive => haystack.contains(needle),
+      CaseSensitivity::AsciiCaseInsensitive => {
+        if let Some((&n_first_byte, n_rest)) = needle.as_bytes().split_first() {
+          haystack.bytes().enumerate().any(|(i, byte)| {
+            if !byte.eq_ignore_ascii_case(&n_first_byte) {
+              return false;
+            }
+            let after_this_byte = &haystack.as_bytes()[i + 1..];
+            match after_this_byte.get(..n_rest.len()) {
+              None => false,
+              Some(haystack_slice) => haystack_slice.eq_ignore_ascii_case(n_rest),
+            }
+          })
+        } else {
+          // any_str.contains("") == true,
+          // though these cases should be handled with *NeverMatches and never go here.
+          true
+        }
+      }
+    }
+  }
+}
diff --git a/selectors/bloom.rs b/selectors/bloom.rs
new file mode 100644
index 0000000..e9d2f73
--- /dev/null
+++ b/selectors/bloom.rs
@@ -0,0 +1,418 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Counting and non-counting Bloom filters tuned for use as ancestor filters
+//! for selector matching.
+
+use std::fmt::{self, Debug};
+
+// The top 8 bits of the 32-bit hash value are not used by the bloom filter.
+// Consumers may rely on this to pack hashes more efficiently.
+pub const BLOOM_HASH_MASK: u32 = 0x00ffffff;
+const KEY_SIZE: usize = 12;
+
+const ARRAY_SIZE: usize = 1 << KEY_SIZE;
+const KEY_MASK: u32 = (1 << KEY_SIZE) - 1;
+
+/// A counting Bloom filter with 8-bit counters.
+pub type BloomFilter = CountingBloomFilter<BloomStorageU8>;
+
+/// A counting Bloom filter with parameterized storage to handle
+/// counters of different sizes.  For now we assume that having two hash
+/// functions is enough, but we may revisit that decision later.
+///
+/// The filter uses an array with 2**KeySize entries.
+///
+/// Assuming a well-distributed hash function, a Bloom filter with
+/// array size M containing N elements and
+/// using k hash function has expected false positive rate exactly
+///
+/// $  (1 - (1 - 1/M)^{kN})^k  $
+///
+/// because each array slot has a
+///
+/// $  (1 - 1/M)^{kN}  $
+///
+/// chance of being 0, and the expected false positive rate is the
+/// probability that all of the k hash functions will hit a nonzero
+/// slot.
+///
+/// For reasonable assumptions (M large, kN large, which should both
+/// hold if we're worried about false positives) about M and kN this
+/// becomes approximately
+///
+/// $$  (1 - \exp(-kN/M))^k   $$
+///
+/// For our special case of k == 2, that's $(1 - \exp(-2N/M))^2$,
+/// or in other words
+///
+/// $$    N/M = -0.5 * \ln(1 - \sqrt(r))   $$
+///
+/// where r is the false positive rate.  This can be used to compute
+/// the desired KeySize for a given load N and false positive rate r.
+///
+/// If N/M is assumed small, then the false positive rate can
+/// further be approximated as 4*N^2/M^2.  So increasing KeySize by
+/// 1, which doubles M, reduces the false positive rate by about a
+/// factor of 4, and a false positive rate of 1% corresponds to
+/// about M/N == 20.
+///
+/// What this means in practice is that for a few hundred keys using a
+/// KeySize of 12 gives false positive rates on the order of 0.25-4%.
+///
+/// Similarly, using a KeySize of 10 would lead to a 4% false
+/// positive rate for N == 100 and to quite bad false positive
+/// rates for larger N.
+#[derive(Clone, Default)]
+pub struct CountingBloomFilter<S>
+where
+  S: BloomStorage,
+{
+  storage: S,
+}
+
+impl<S> CountingBloomFilter<S>
+where
+  S: BloomStorage,
+{
+  /// Creates a new bloom filter.
+  #[inline]
+  pub fn new() -> Self {
+    Default::default()
+  }
+
+  #[inline]
+  pub fn clear(&mut self) {
+    self.storage = Default::default();
+  }
+
+  // Slow linear accessor to make sure the bloom filter is zeroed. This should
+  // never be used in release builds.
+  #[cfg(debug_assertions)]
+  pub fn is_zeroed(&self) -> bool {
+    self.storage.is_zeroed()
+  }
+
+  #[cfg(not(debug_assertions))]
+  pub fn is_zeroed(&self) -> bool {
+    unreachable!()
+  }
+
+  /// Inserts an item with a particular hash into the bloom filter.
+  #[inline]
+  pub fn insert_hash(&mut self, hash: u32) {
+    self.storage.adjust_first_slot(hash, true);
+    self.storage.adjust_second_slot(hash, true);
+  }
+
+  /// Removes an item with a particular hash from the bloom filter.
+  #[inline]
+  pub fn remove_hash(&mut self, hash: u32) {
+    self.storage.adjust_first_slot(hash, false);
+    self.storage.adjust_second_slot(hash, false);
+  }
+
+  /// Check whether the filter might contain an item with the given hash.
+  /// This can sometimes return true even if the item is not in the filter,
+  /// but will never return false for items that are actually in the filter.
+  #[inline]
+  pub fn might_contain_hash(&self, hash: u32) -> bool {
+    !self.storage.first_slot_is_empty(hash) && !self.storage.second_slot_is_empty(hash)
+  }
+}
+
+impl<S> Debug for CountingBloomFilter<S>
+where
+  S: BloomStorage,
+{
+  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+    let mut slots_used = 0;
+    for i in 0..ARRAY_SIZE {
+      if !self.storage.slot_is_empty(i) {
+        slots_used += 1;
+      }
+    }
+    write!(f, "BloomFilter({}/{})", slots_used, ARRAY_SIZE)
+  }
+}
+
+pub trait BloomStorage: Clone + Default {
+  fn slot_is_empty(&self, index: usize) -> bool;
+  fn adjust_slot(&mut self, index: usize, increment: bool);
+  fn is_zeroed(&self) -> bool;
+
+  #[inline]
+  fn first_slot_is_empty(&self, hash: u32) -> bool {
+    self.slot_is_empty(Self::first_slot_index(hash))
+  }
+
+  #[inline]
+  fn second_slot_is_empty(&self, hash: u32) -> bool {
+    self.slot_is_empty(Self::second_slot_index(hash))
+  }
+
+  #[inline]
+  fn adjust_first_slot(&mut self, hash: u32, increment: bool) {
+    self.adjust_slot(Self::first_slot_index(hash), increment)
+  }
+
+  #[inline]
+  fn adjust_second_slot(&mut self, hash: u32, increment: bool) {
+    self.adjust_slot(Self::second_slot_index(hash), increment)
+  }
+
+  #[inline]
+  fn first_slot_index(hash: u32) -> usize {
+    hash1(hash) as usize
+  }
+
+  #[inline]
+  fn second_slot_index(hash: u32) -> usize {
+    hash2(hash) as usize
+  }
+}
+
+/// Storage class for a CountingBloomFilter that has 8-bit counters.
+pub struct BloomStorageU8 {
+  counters: [u8; ARRAY_SIZE],
+}
+
+impl BloomStorage for BloomStorageU8 {
+  #[inline]
+  fn adjust_slot(&mut self, index: usize, increment: bool) {
+    let slot = &mut self.counters[index];
+    if *slot != 0xff {
+      // full
+      if increment {
+        *slot += 1;
+      } else {
+        *slot -= 1;
+      }
+    }
+  }
+
+  #[inline]
+  fn slot_is_empty(&self, index: usize) -> bool {
+    self.counters[index] == 0
+  }
+
+  #[inline]
+  fn is_zeroed(&self) -> bool {
+    self.counters.iter().all(|x| *x == 0)
+  }
+}
+
+impl Default for BloomStorageU8 {
+  fn default() -> Self {
+    BloomStorageU8 {
+      counters: [0; ARRAY_SIZE],
+    }
+  }
+}
+
+impl Clone for BloomStorageU8 {
+  fn clone(&self) -> Self {
+    BloomStorageU8 {
+      counters: self.counters,
+    }
+  }
+}
+
+/// Storage class for a CountingBloomFilter that has 1-bit counters.
+pub struct BloomStorageBool {
+  counters: [u8; ARRAY_SIZE / 8],
+}
+
+impl BloomStorage for BloomStorageBool {
+  #[inline]
+  fn adjust_slot(&mut self, index: usize, increment: bool) {
+    let bit = 1 << (index % 8);
+    let byte = &mut self.counters[index / 8];
+
+    // Since we have only one bit for storage, decrementing it
+    // should never do anything.  Assert against an accidental
+    // decrementing of a bit that was never set.
+    assert!(
+      increment || (*byte & bit) != 0,
+      "should not decrement if slot is already false"
+    );
+
+    if increment {
+      *byte |= bit;
+    }
+  }
+
+  #[inline]
+  fn slot_is_empty(&self, index: usize) -> bool {
+    let bit = 1 << (index % 8);
+    (self.counters[index / 8] & bit) == 0
+  }
+
+  #[inline]
+  fn is_zeroed(&self) -> bool {
+    self.counters.iter().all(|x| *x == 0)
+  }
+}
+
+impl Default for BloomStorageBool {
+  fn default() -> Self {
+    BloomStorageBool {
+      counters: [0; ARRAY_SIZE / 8],
+    }
+  }
+}
+
+impl Clone for BloomStorageBool {
+  fn clone(&self) -> Self {
+    BloomStorageBool {
+      counters: self.counters,
+    }
+  }
+}
+
+#[inline]
+fn hash1(hash: u32) -> u32 {
+  hash & KEY_MASK
+}
+
+#[inline]
+fn hash2(hash: u32) -> u32 {
+  (hash >> KEY_SIZE) & KEY_MASK
+}
+
+#[test]
+fn create_and_insert_some_stuff() {
+  use rustc_hash::FxHasher;
+  use std::hash::{Hash, Hasher};
+  use std::mem::transmute;
+
+  fn hash_as_str(i: usize) -> u32 {
+    let mut hasher = FxHasher::default();
+    let s = i.to_string();
+    s.hash(&mut hasher);
+    let hash: u64 = hasher.finish();
+    (hash >> 32) as u32 ^ (hash as u32)
+  }
+
+  let mut bf = BloomFilter::new();
+
+  // Statically assert that ARRAY_SIZE is a multiple of 8, which
+  // BloomStorageBool relies on.
+  unsafe {
+    transmute::<[u8; ARRAY_SIZE % 8], [u8; 0]>([]);
+  }
+
+  for i in 0_usize..1000 {
+    bf.insert_hash(hash_as_str(i));
+  }
+
+  for i in 0_usize..1000 {
+    assert!(bf.might_contain_hash(hash_as_str(i)));
+  }
+
+  let false_positives = (1001_usize..2000).filter(|i| bf.might_contain_hash(hash_as_str(*i))).count();
+
+  assert!(false_positives < 190, "{} is not < 190", false_positives); // 19%.
+
+  for i in 0_usize..100 {
+    bf.remove_hash(hash_as_str(i));
+  }
+
+  for i in 100_usize..1000 {
+    assert!(bf.might_contain_hash(hash_as_str(i)));
+  }
+
+  let false_positives = (0_usize..100).filter(|i| bf.might_contain_hash(hash_as_str(*i))).count();
+
+  assert!(false_positives < 20, "{} is not < 20", false_positives); // 20%.
+
+  bf.clear();
+
+  for i in 0_usize..2000 {
+    assert!(!bf.might_contain_hash(hash_as_str(i)));
+  }
+}
+
+#[cfg(feature = "bench")]
+#[cfg(test)]
+mod bench {
+  extern crate test;
+  use super::BloomFilter;
+
+  #[derive(Default)]
+  struct HashGenerator(u32);
+
+  impl HashGenerator {
+    fn next(&mut self) -> u32 {
+      // Each hash is split into two twelve-bit segments, which are used
+      // as an index into an array. We increment each by 64 so that we
+      // hit the next cache line, and then another 1 so that our wrapping
+      // behavior leads us to different entries each time.
+      //
+      // Trying to simulate cold caches is rather difficult with the cargo
+      // benchmarking setup, so it may all be moot depending on the number
+      // of iterations that end up being run. But we might as well.
+      self.0 += (65) + (65 << super::KEY_SIZE);
+      self.0
+    }
+  }
+
+  #[bench]
+  fn create_insert_1000_remove_100_lookup_100(b: &mut test::Bencher) {
+    b.iter(|| {
+      let mut gen1 = HashGenerator::default();
+      let mut gen2 = HashGenerator::default();
+      let mut bf = BloomFilter::new();
+      for _ in 0_usize..1000 {
+        bf.insert_hash(gen1.next());
+      }
+      for _ in 0_usize..100 {
+        bf.remove_hash(gen2.next());
+      }
+      for _ in 100_usize..200 {
+        test::black_box(bf.might_contain_hash(gen2.next()));
+      }
+    });
+  }
+
+  #[bench]
+  fn might_contain_10(b: &mut test::Bencher) {
+    let bf = BloomFilter::new();
+    let mut gen = HashGenerator::default();
+    b.iter(|| {
+      for _ in 0..10 {
+        test::black_box(bf.might_contain_hash(gen.next()));
+      }
+    });
+  }
+
+  #[bench]
+  fn clear(b: &mut test::Bencher) {
+    let mut bf = Box::new(BloomFilter::new());
+    b.iter(|| test::black_box(&mut bf).clear());
+  }
+
+  #[bench]
+  fn insert_10(b: &mut test::Bencher) {
+    let mut bf = BloomFilter::new();
+    let mut gen = HashGenerator::default();
+    b.iter(|| {
+      for _ in 0..10 {
+        test::black_box(bf.insert_hash(gen.next()));
+      }
+    });
+  }
+
+  #[bench]
+  fn remove_10(b: &mut test::Bencher) {
+    let mut bf = BloomFilter::new();
+    let mut gen = HashGenerator::default();
+    // Note: this will underflow, and that's ok.
+    b.iter(|| {
+      for _ in 0..10 {
+        bf.remove_hash(gen.next())
+      }
+    });
+  }
+}
diff --git a/selectors/build.rs b/selectors/build.rs
new file mode 100644
index 0000000..787e2d8
--- /dev/null
+++ b/selectors/build.rs
@@ -0,0 +1,74 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+use std::env;
+use std::fs::File;
+use std::io::{BufWriter, Write};
+use std::path::Path;
+
+fn main() {
+  let path = Path::new(&env::var_os("OUT_DIR").unwrap()).join("ascii_case_insensitive_html_attributes.rs");
+  let mut file = BufWriter::new(File::create(&path).unwrap());
+
+  let mut set = phf_codegen::Set::new();
+  for name in ASCII_CASE_INSENSITIVE_HTML_ATTRIBUTES.split_whitespace() {
+    set.entry(name);
+  }
+  write!(
+    &mut file,
+    "{{ static SET: ::phf::Set<&'static str> = {}; &SET }}",
+    set.build(),
+  )
+  .unwrap();
+}
+
+/// <https://html.spec.whatwg.org/multipage/#selectors>
+static ASCII_CASE_INSENSITIVE_HTML_ATTRIBUTES: &str = r#"
+    accept
+    accept-charset
+    align
+    alink
+    axis
+    bgcolor
+    charset
+    checked
+    clear
+    codetype
+    color
+    compact
+    declare
+    defer
+    dir
+    direction
+    disabled
+    enctype
+    face
+    frame
+    hreflang
+    http-equiv
+    lang
+    language
+    link
+    media
+    method
+    multiple
+    nohref
+    noresize
+    noshade
+    nowrap
+    readonly
+    rel
+    rev
+    rules
+    scope
+    scrolling
+    selected
+    shape
+    target
+    text
+    type
+    valign
+    valuetype
+    vlink
+"#;
diff --git a/selectors/builder.rs b/selectors/builder.rs
new file mode 100644
index 0000000..2815875
--- /dev/null
+++ b/selectors/builder.rs
@@ -0,0 +1,377 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Helper module to build up a selector safely and efficiently.
+//!
+//! Our selector representation is designed to optimize matching, and has
+//! several requirements:
+//! * All simple selectors and combinators are stored inline in the same buffer
+//!   as Component instances.
+//! * We store the top-level compound selectors from right to left, i.e. in
+//!   matching order.
+//! * We store the simple selectors for each combinator from left to right, so
+//!   that we match the cheaper simple selectors first.
+//!
+//! Meeting all these constraints without extra memmove traffic during parsing
+//! is non-trivial. This module encapsulates those details and presents an
+//! easy-to-use API for the parser.
+
+use crate::parser::{Combinator, Component, SelectorImpl};
+use crate::sink::Push;
+// use servo_arc::{Arc, HeaderWithLength, ThinArc};
+use smallvec::{self, SmallVec};
+use std::cmp;
+use std::iter;
+use std::ops::{Add, AddAssign};
+use std::ptr;
+use std::slice;
+
+/// Top-level SelectorBuilder struct. This should be stack-allocated by the
+/// consumer and never moved (because it contains a lot of inline data that
+/// would be slow to memmov).
+///
+/// After instantiation, callers may call the push_simple_selector() and
+/// push_combinator() methods to append selector data as it is encountered
+/// (from left to right). Once the process is complete, callers should invoke
+/// build(), which transforms the contents of the SelectorBuilder into a heap-
+/// allocated Selector and leaves the builder in a drained state.
+#[derive(Debug)]
+pub struct SelectorBuilder<'i, Impl: SelectorImpl<'i>> {
+  /// The entire sequence of simple selectors, from left to right, without combinators.
+  ///
+  /// We make this large because the result of parsing a selector is fed into a new
+  /// Arc-ed allocation, so any spilled vec would be a wasted allocation. Also,
+  /// Components are large enough that we don't have much cache locality benefit
+  /// from reserving stack space for fewer of them.
+  simple_selectors: SmallVec<[Component<'i, Impl>; 32]>,
+  /// The combinators, and the length of the compound selector to their left.
+  combinators: SmallVec<[(Combinator, usize); 16]>,
+  /// The length of the current compound selector.
+  current_len: usize,
+}
+
+impl<'i, Impl: SelectorImpl<'i>> Default for SelectorBuilder<'i, Impl> {
+  #[inline(always)]
+  fn default() -> Self {
+    SelectorBuilder {
+      simple_selectors: SmallVec::new(),
+      combinators: SmallVec::new(),
+      current_len: 0,
+    }
+  }
+}
+
+impl<'i, Impl: SelectorImpl<'i>> Push<Component<'i, Impl>> for SelectorBuilder<'i, Impl> {
+  fn push(&mut self, value: Component<'i, Impl>) {
+    self.push_simple_selector(value);
+  }
+}
+
+impl<'i, Impl: SelectorImpl<'i>> SelectorBuilder<'i, Impl> {
+  /// Pushes a simple selector onto the current compound selector.
+  #[inline(always)]
+  pub fn push_simple_selector(&mut self, ss: Component<'i, Impl>) {
+    assert!(!ss.is_combinator());
+    self.simple_selectors.push(ss);
+    self.current_len += 1;
+  }
+
+  /// Completes the current compound selector and starts a new one, delimited
+  /// by the given combinator.
+  #[inline(always)]
+  pub fn push_combinator(&mut self, c: Combinator) {
+    self.combinators.push((c, self.current_len));
+    self.current_len = 0;
+  }
+
+  /// Returns true if combinators have ever been pushed to this builder.
+  #[inline(always)]
+  pub fn has_combinators(&self) -> bool {
+    !self.combinators.is_empty()
+  }
+
+  pub fn add_nesting_prefix(&mut self) {
+    self.combinators.insert(0, (Combinator::Descendant, 1));
+    self.simple_selectors.insert(0, Component::Nesting);
+  }
+
+  /// Consumes the builder, producing a Selector.
+  #[inline(always)]
+  pub fn build(
+    &mut self,
+    parsed_pseudo: bool,
+    parsed_slotted: bool,
+    parsed_part: bool,
+  ) -> (SpecificityAndFlags, Vec<Component<'i, Impl>>) {
+    // Compute the specificity and flags.
+    let specificity = specificity(self.simple_selectors.iter());
+    let mut flags = SelectorFlags::empty();
+    if parsed_pseudo {
+      flags |= SelectorFlags::HAS_PSEUDO;
+    }
+    if parsed_slotted {
+      flags |= SelectorFlags::HAS_SLOTTED;
+    }
+    if parsed_part {
+      flags |= SelectorFlags::HAS_PART;
+    }
+    self.build_with_specificity_and_flags(SpecificityAndFlags { specificity, flags })
+  }
+
+  /// Builds with an explicit SpecificityAndFlags. This is separated from build() so
+  /// that unit tests can pass an explicit specificity.
+  #[inline(always)]
+  pub fn build_with_specificity_and_flags(
+    &mut self,
+    spec: SpecificityAndFlags,
+  ) -> (SpecificityAndFlags, Vec<Component<'i, Impl>>) {
+    // Use a raw pointer to be able to call set_len despite "borrowing" the slice.
+    // This is similar to SmallVec::drain, but we use a slice here because
+    // we’re gonna traverse it non-linearly.
+    let raw_simple_selectors: *const [Component<Impl>] = &*self.simple_selectors;
+    unsafe {
+      // Panic-safety: if SelectorBuilderIter is not iterated to the end,
+      // some simple selectors will safely leak.
+      self.simple_selectors.set_len(0)
+    }
+    let (rest, current) = split_from_end(unsafe { &*raw_simple_selectors }, self.current_len);
+    let iter = SelectorBuilderIter {
+      current_simple_selectors: current.iter(),
+      rest_of_simple_selectors: rest,
+      combinators: self.combinators.drain(..).rev(),
+    };
+
+    (spec, iter.collect())
+  }
+}
+
+struct SelectorBuilderIter<'a, 'i, Impl: SelectorImpl<'i>> {
+  current_simple_selectors: slice::Iter<'a, Component<'i, Impl>>,
+  rest_of_simple_selectors: &'a [Component<'i, Impl>],
+  combinators: iter::Rev<smallvec::Drain<'a, [(Combinator, usize); 16]>>,
+}
+
+impl<'a, 'i, Impl: SelectorImpl<'i>> ExactSizeIterator for SelectorBuilderIter<'a, 'i, Impl> {
+  fn len(&self) -> usize {
+    self.current_simple_selectors.len() + self.rest_of_simple_selectors.len() + self.combinators.len()
+  }
+}
+
+impl<'a, 'i, Impl: SelectorImpl<'i>> Iterator for SelectorBuilderIter<'a, 'i, Impl> {
+  type Item = Component<'i, Impl>;
+  #[inline(always)]
+  fn next(&mut self) -> Option<Self::Item> {
+    if let Some(simple_selector_ref) = self.current_simple_selectors.next() {
+      // Move a simple selector out of this slice iterator.
+      // This is safe because we’ve called SmallVec::set_len(0) above,
+      // so SmallVec::drop won’t drop this simple selector.
+      unsafe { Some(ptr::read(simple_selector_ref)) }
+    } else {
+      self.combinators.next().map(|(combinator, len)| {
+        let (rest, current) = split_from_end(self.rest_of_simple_selectors, len);
+        self.rest_of_simple_selectors = rest;
+        self.current_simple_selectors = current.iter();
+        Component::Combinator(combinator)
+      })
+    }
+  }
+
+  fn size_hint(&self) -> (usize, Option<usize>) {
+    (self.len(), Some(self.len()))
+  }
+}
+
+fn split_from_end<T>(s: &[T], at: usize) -> (&[T], &[T]) {
+  s.split_at(s.len() - at)
+}
+
+bitflags! {
+    /// Flags that indicate at which point of parsing a selector are we.
+    #[derive(Default, Clone, Copy, Debug, Eq, PartialEq, Hash)]
+    pub (crate) struct SelectorFlags : u8 {
+        const HAS_PSEUDO = 1 << 0;
+        const HAS_SLOTTED = 1 << 1;
+        const HAS_PART = 1 << 2;
+    }
+}
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
+pub struct SpecificityAndFlags {
+  /// There are two free bits here, since we use ten bits for each specificity
+  /// kind (id, class, element).
+  pub(crate) specificity: u32,
+  /// There's padding after this field due to the size of the flags.
+  pub(crate) flags: SelectorFlags,
+}
+
+impl SpecificityAndFlags {
+  #[inline]
+  pub fn specificity(&self) -> u32 {
+    self.specificity
+  }
+
+  #[inline]
+  pub fn has_pseudo_element(&self) -> bool {
+    self.flags.intersects(SelectorFlags::HAS_PSEUDO)
+  }
+
+  #[inline]
+  pub fn is_slotted(&self) -> bool {
+    self.flags.intersects(SelectorFlags::HAS_SLOTTED)
+  }
+
+  #[inline]
+  pub fn is_part(&self) -> bool {
+    self.flags.intersects(SelectorFlags::HAS_PART)
+  }
+}
+
+const MAX_10BIT: u32 = (1u32 << 10) - 1;
+
+#[derive(Clone, Copy, Default, Eq, Ord, PartialEq, PartialOrd)]
+struct Specificity {
+  id_selectors: u32,
+  class_like_selectors: u32,
+  element_selectors: u32,
+}
+impl Add for Specificity {
+  type Output = Specificity;
+
+  fn add(self, rhs: Self) -> Self::Output {
+    Specificity {
+      id_selectors: self.id_selectors + rhs.id_selectors,
+      class_like_selectors: self.class_like_selectors + rhs.class_like_selectors,
+      element_selectors: self.element_selectors + rhs.element_selectors,
+    }
+  }
+}
+impl AddAssign for Specificity {
+  fn add_assign(&mut self, rhs: Self) {
+    self.id_selectors += rhs.id_selectors;
+    self.element_selectors += rhs.element_selectors;
+    self.class_like_selectors += rhs.class_like_selectors;
+  }
+}
+
+impl From<u32> for Specificity {
+  #[inline]
+  fn from(value: u32) -> Specificity {
+    assert!(value <= MAX_10BIT << 20 | MAX_10BIT << 10 | MAX_10BIT);
+    Specificity {
+      id_selectors: value >> 20,
+      class_like_selectors: (value >> 10) & MAX_10BIT,
+      element_selectors: value & MAX_10BIT,
+    }
+  }
+}
+
+impl From<Specificity> for u32 {
+  #[inline]
+  fn from(specificity: Specificity) -> u32 {
+    cmp::min(specificity.id_selectors, MAX_10BIT) << 20
+      | cmp::min(specificity.class_like_selectors, MAX_10BIT) << 10
+      | cmp::min(specificity.element_selectors, MAX_10BIT)
+  }
+}
+
+fn specificity<'i, Impl>(iter: slice::Iter<Component<'i, Impl>>) -> u32
+where
+  Impl: SelectorImpl<'i>,
+{
+  complex_selector_specificity(iter).into()
+}
+
+fn complex_selector_specificity<'i, Impl>(iter: slice::Iter<Component<'i, Impl>>) -> Specificity
+where
+  Impl: SelectorImpl<'i>,
+{
+  fn simple_selector_specificity<'i, Impl>(simple_selector: &Component<'i, Impl>, specificity: &mut Specificity)
+  where
+    Impl: SelectorImpl<'i>,
+  {
+    match *simple_selector {
+      Component::Combinator(..) => {
+        unreachable!("Found combinator in simple selectors vector?");
+      }
+      Component::Part(..) | Component::PseudoElement(..) | Component::LocalName(..) => {
+        specificity.element_selectors += 1
+      }
+      Component::Slotted(ref selector) => {
+        specificity.element_selectors += 1;
+        // Note that due to the way ::slotted works we only compete with
+        // other ::slotted rules, so the above rule doesn't really
+        // matter, but we do it still for consistency with other
+        // pseudo-elements.
+        //
+        // See: https://github.com/w3c/csswg-drafts/issues/1915
+        *specificity += Specificity::from(selector.specificity());
+      }
+      Component::Host(ref selector) => {
+        specificity.class_like_selectors += 1;
+        if let Some(ref selector) = *selector {
+          // See: https://github.com/w3c/csswg-drafts/issues/1915
+          *specificity += Specificity::from(selector.specificity());
+        }
+      }
+      Component::ID(..) => {
+        specificity.id_selectors += 1;
+      }
+      Component::Class(..)
+      | Component::AttributeInNoNamespace { .. }
+      | Component::AttributeInNoNamespaceExists { .. }
+      | Component::AttributeOther(..)
+      | Component::Root
+      | Component::Empty
+      | Component::Scope
+      | Component::Nth(..)
+      | Component::NonTSPseudoClass(..) => {
+        specificity.class_like_selectors += 1;
+      }
+      Component::NthOf(ref nth_of_data) => {
+        // https://drafts.csswg.org/selectors/#specificity-rules:
+        //
+        //     The specificity of the :nth-last-child() pseudo-class,
+        //     like the :nth-child() pseudo-class, combines the
+        //     specificity of a regular pseudo-class with that of its
+        //     selector argument S.
+        specificity.class_like_selectors += 1;
+        let mut max = 0;
+        for selector in nth_of_data.selectors() {
+          max = std::cmp::max(selector.specificity(), max);
+        }
+        *specificity += Specificity::from(max);
+      }
+      Component::Negation(ref list) | Component::Is(ref list) | Component::Any(_, ref list) => {
+        // https://drafts.csswg.org/selectors/#specificity-rules:
+        //
+        //     The specificity of an :is() pseudo-class is replaced by the
+        //     specificity of the most specific complex selector in its
+        //     selector list argument.
+        let mut max = 0;
+        for selector in &**list {
+          max = std::cmp::max(selector.specificity(), max);
+        }
+        *specificity += Specificity::from(max);
+      }
+      Component::Where(..)
+      | Component::Has(..)
+      | Component::ExplicitUniversalType
+      | Component::ExplicitAnyNamespace
+      | Component::ExplicitNoNamespace
+      | Component::DefaultNamespace(..)
+      | Component::Namespace(..) => {
+        // Does not affect specificity
+      }
+      Component::Nesting => {
+        // TODO
+      }
+    }
+  }
+
+  let mut specificity = Default::default();
+  for simple_selector in iter {
+    simple_selector_specificity(&simple_selector, &mut specificity);
+  }
+  specificity
+}
diff --git a/selectors/context.rs b/selectors/context.rs
new file mode 100644
index 0000000..bf14278
--- /dev/null
+++ b/selectors/context.rs
@@ -0,0 +1,285 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+use crate::attr::CaseSensitivity;
+use crate::bloom::BloomFilter;
+use crate::nth_index_cache::NthIndexCache;
+use crate::parser::SelectorImpl;
+use crate::tree::{Element, OpaqueElement};
+
+/// What kind of selector matching mode we should use.
+///
+/// There are two modes of selector matching. The difference is only noticeable
+/// in presence of pseudo-elements.
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub enum MatchingMode {
+  /// Don't ignore any pseudo-element selectors.
+  Normal,
+
+  /// Ignores any stateless pseudo-element selectors in the rightmost sequence
+  /// of simple selectors.
+  ///
+  /// This is useful, for example, to match against ::before when you aren't a
+  /// pseudo-element yourself.
+  ///
+  /// For example, in presence of `::before:hover`, it would never match, but
+  /// `::before` would be ignored as in "matching".
+  ///
+  /// It's required for all the selectors you match using this mode to have a
+  /// pseudo-element.
+  ForStatelessPseudoElement,
+}
+
+/// The mode to use when matching unvisited and visited links.
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub enum VisitedHandlingMode {
+  /// All links are matched as if they are unvisted.
+  AllLinksUnvisited,
+  /// All links are matched as if they are visited and unvisited (both :link
+  /// and :visited match).
+  ///
+  /// This is intended to be used from invalidation code, to be conservative
+  /// about whether we need to restyle a link.
+  AllLinksVisitedAndUnvisited,
+  /// A element's "relevant link" is the element being matched if it is a link
+  /// or the nearest ancestor link. The relevant link is matched as though it
+  /// is visited, and all other links are matched as if they are unvisited.
+  RelevantLinkVisited,
+}
+
+impl VisitedHandlingMode {
+  #[inline]
+  pub fn matches_visited(&self) -> bool {
+    matches!(
+      *self,
+      VisitedHandlingMode::RelevantLinkVisited | VisitedHandlingMode::AllLinksVisitedAndUnvisited
+    )
+  }
+
+  #[inline]
+  pub fn matches_unvisited(&self) -> bool {
+    matches!(
+      *self,
+      VisitedHandlingMode::AllLinksUnvisited | VisitedHandlingMode::AllLinksVisitedAndUnvisited
+    )
+  }
+}
+
+/// Which quirks mode is this document in.
+///
+/// See: https://quirks.spec.whatwg.org/
+#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
+pub enum QuirksMode {
+  /// Quirks mode.
+  Quirks,
+  /// Limited quirks mode.
+  LimitedQuirks,
+  /// No quirks mode.
+  NoQuirks,
+}
+
+impl QuirksMode {
+  #[inline]
+  pub fn classes_and_ids_case_sensitivity(self) -> CaseSensitivity {
+    match self {
+      QuirksMode::NoQuirks | QuirksMode::LimitedQuirks => CaseSensitivity::CaseSensitive,
+      QuirksMode::Quirks => CaseSensitivity::AsciiCaseInsensitive,
+    }
+  }
+}
+
+/// Data associated with the matching process for a element.  This context is
+/// used across many selectors for an element, so it's not appropriate for
+/// transient data that applies to only a single selector.
+pub struct MatchingContext<'a, 'i, Impl>
+where
+  Impl: SelectorImpl<'i>,
+{
+  /// Input with the matching mode we should use when matching selectors.
+  matching_mode: MatchingMode,
+  /// Input with the bloom filter used to fast-reject selectors.
+  pub bloom_filter: Option<&'a BloomFilter>,
+  /// An optional cache to speed up nth-index-like selectors.
+  pub nth_index_cache: Option<&'a mut NthIndexCache>,
+  /// The element which is going to match :scope pseudo-class. It can be
+  /// either one :scope element, or the scoping element.
+  ///
+  /// Note that, although in theory there can be multiple :scope elements,
+  /// in current specs, at most one is specified, and when there is one,
+  /// scoping element is not relevant anymore, so we use a single field for
+  /// them.
+  ///
+  /// When this is None, :scope will match the root element.
+  ///
+  /// See https://drafts.csswg.org/selectors-4/#scope-pseudo
+  pub scope_element: Option<OpaqueElement>,
+
+  /// The current shadow host we're collecting :host rules for.
+  pub current_host: Option<OpaqueElement>,
+
+  /// Controls how matching for links is handled.
+  visited_handling: VisitedHandlingMode,
+
+  /// The current nesting level of selectors that we're matching.
+  ///
+  /// FIXME(emilio): Consider putting the mutable stuff in a Cell, then make
+  /// MatchingContext immutable again.
+  nesting_level: usize,
+
+  /// Whether we're inside a negation or not.
+  in_negation: bool,
+
+  /// An optional hook function for checking whether a pseudo-element
+  /// should match when matching_mode is ForStatelessPseudoElement.
+  pub pseudo_element_matching_fn: Option<&'a dyn Fn(&Impl::PseudoElement) -> bool>,
+
+  /// Extra implementation-dependent matching data.
+  pub extra_data: Impl::ExtraMatchingData,
+
+  quirks_mode: QuirksMode,
+  classes_and_ids_case_sensitivity: CaseSensitivity,
+  _impl: ::std::marker::PhantomData<Impl>,
+}
+
+impl<'a, 'i, Impl> MatchingContext<'a, 'i, Impl>
+where
+  Impl: SelectorImpl<'i>,
+{
+  /// Constructs a new `MatchingContext`.
+  pub fn new(
+    matching_mode: MatchingMode,
+    bloom_filter: Option<&'a BloomFilter>,
+    nth_index_cache: Option<&'a mut NthIndexCache>,
+    quirks_mode: QuirksMode,
+  ) -> Self {
+    Self::new_for_visited(
+      matching_mode,
+      bloom_filter,
+      nth_index_cache,
+      VisitedHandlingMode::AllLinksUnvisited,
+      quirks_mode,
+    )
+  }
+
+  /// Constructs a new `MatchingContext` for use in visited matching.
+  pub fn new_for_visited(
+    matching_mode: MatchingMode,
+    bloom_filter: Option<&'a BloomFilter>,
+    nth_index_cache: Option<&'a mut NthIndexCache>,
+    visited_handling: VisitedHandlingMode,
+    quirks_mode: QuirksMode,
+  ) -> Self {
+    Self {
+      matching_mode,
+      bloom_filter,
+      visited_handling,
+      nth_index_cache,
+      quirks_mode,
+      classes_and_ids_case_sensitivity: quirks_mode.classes_and_ids_case_sensitivity(),
+      scope_element: None,
+      current_host: None,
+      nesting_level: 0,
+      in_negation: false,
+      pseudo_element_matching_fn: None,
+      extra_data: Default::default(),
+      _impl: ::std::marker::PhantomData,
+    }
+  }
+
+  /// Whether we're matching a nested selector.
+  #[inline]
+  pub fn is_nested(&self) -> bool {
+    self.nesting_level != 0
+  }
+
+  /// Whether we're matching inside a :not(..) selector.
+  #[inline]
+  pub fn in_negation(&self) -> bool {
+    self.in_negation
+  }
+
+  /// The quirks mode of the document.
+  #[inline]
+  pub fn quirks_mode(&self) -> QuirksMode {
+    self.quirks_mode
+  }
+
+  /// The matching-mode for this selector-matching operation.
+  #[inline]
+  pub fn matching_mode(&self) -> MatchingMode {
+    self.matching_mode
+  }
+
+  /// The case-sensitivity for class and ID selectors
+  #[inline]
+  pub fn classes_and_ids_case_sensitivity(&self) -> CaseSensitivity {
+    self.classes_and_ids_case_sensitivity
+  }
+
+  /// Runs F with a deeper nesting level.
+  #[inline]
+  pub fn nest<F, R>(&mut self, f: F) -> R
+  where
+    F: FnOnce(&mut Self) -> R,
+  {
+    self.nesting_level += 1;
+    let result = f(self);
+    self.nesting_level -= 1;
+    result
+  }
+
+  /// Runs F with a deeper nesting level, and marking ourselves in a negation,
+  /// for a :not(..) selector, for example.
+  #[inline]
+  pub fn nest_for_negation<F, R>(&mut self, f: F) -> R
+  where
+    F: FnOnce(&mut Self) -> R,
+  {
+    let old_in_negation = self.in_negation;
+    self.in_negation = true;
+    let result = self.nest(f);
+    self.in_negation = old_in_negation;
+    result
+  }
+
+  #[inline]
+  pub fn visited_handling(&self) -> VisitedHandlingMode {
+    self.visited_handling
+  }
+
+  /// Runs F with a different VisitedHandlingMode.
+  #[inline]
+  pub fn with_visited_handling_mode<F, R>(&mut self, handling_mode: VisitedHandlingMode, f: F) -> R
+  where
+    F: FnOnce(&mut Self) -> R,
+  {
+    let original_handling_mode = self.visited_handling;
+    self.visited_handling = handling_mode;
+    let result = f(self);
+    self.visited_handling = original_handling_mode;
+    result
+  }
+
+  /// Runs F with a given shadow host which is the root of the tree whose
+  /// rules we're matching.
+  #[inline]
+  pub fn with_shadow_host<F, E, R>(&mut self, host: Option<E>, f: F) -> R
+  where
+    E: Element<'i>,
+    F: FnOnce(&mut Self) -> R,
+  {
+    let original_host = self.current_host.take();
+    self.current_host = host.map(|h| h.opaque());
+    let result = f(self);
+    self.current_host = original_host;
+    result
+  }
+
+  /// Returns the current shadow host whose shadow root we're matching rules
+  /// against.
+  #[inline]
+  pub fn shadow_host(&self) -> Option<OpaqueElement> {
+    self.current_host
+  }
+}
diff --git a/selectors/lib.rs b/selectors/lib.rs
new file mode 100644
index 0000000..2047b4e
--- /dev/null
+++ b/selectors/lib.rs
@@ -0,0 +1,31 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+// Make |cargo bench| work.
+#![cfg_attr(feature = "bench", feature(test))]
+
+#[macro_use]
+extern crate bitflags;
+#[macro_use]
+extern crate cssparser;
+#[macro_use]
+extern crate log;
+
+pub mod attr;
+pub mod bloom;
+mod builder;
+pub mod context;
+pub mod matching;
+mod nth_index_cache;
+pub mod parser;
+pub mod sink;
+mod tree;
+pub mod visitor;
+
+#[cfg(all(feature = "serde"))]
+mod serialization;
+
+pub use crate::nth_index_cache::NthIndexCache;
+pub use crate::parser::{Parser, SelectorImpl, SelectorList};
+pub use crate::tree::{Element, OpaqueElement};
diff --git a/selectors/matching.rs b/selectors/matching.rs
new file mode 100644
index 0000000..61f74a8
--- /dev/null
+++ b/selectors/matching.rs
@@ -0,0 +1,962 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+use crate::attr::{AttrSelectorOperation, NamespaceConstraint, ParsedAttrSelectorOperation};
+use crate::bloom::{BloomFilter, BLOOM_HASH_MASK};
+use crate::nth_index_cache::NthIndexCacheInner;
+use crate::parser::{AncestorHashes, Combinator, Component, LocalName, NthType};
+use crate::parser::{NonTSPseudoClass, Selector, SelectorImpl, SelectorIter, SelectorList};
+use crate::tree::Element;
+use smallvec::SmallVec;
+use std::borrow::Borrow;
+use std::iter;
+
+pub use crate::context::*;
+
+// The bloom filter for descendant CSS selectors will have a <1% false
+// positive rate until it has this many selectors in it, then it will
+// rapidly increase.
+pub static RECOMMENDED_SELECTOR_BLOOM_FILTER_SIZE: usize = 4096;
+
+bitflags! {
+    /// Set of flags that are set on either the element or its parent (depending
+    /// on the flag) if the element could potentially match a selector.
+    pub struct ElementSelectorFlags: usize {
+        /// When a child is added or removed from the parent, all the children
+        /// must be restyled, because they may match :nth-last-child,
+        /// :last-of-type, :nth-last-of-type, or :only-of-type.
+        const HAS_SLOW_SELECTOR = 1 << 0;
+
+        /// When a child is added or removed from the parent, any later
+        /// children must be restyled, because they may match :nth-child,
+        /// :first-of-type, or :nth-of-type.
+        const HAS_SLOW_SELECTOR_LATER_SIBLINGS = 1 << 1;
+
+        /// When a child is added or removed from the parent, the first and
+        /// last children must be restyled, because they may match :first-child,
+        /// :last-child, or :only-child.
+        const HAS_EDGE_CHILD_SELECTOR = 1 << 2;
+
+        /// The element has an empty selector, so when a child is appended we
+        /// might need to restyle the parent completely.
+        const HAS_EMPTY_SELECTOR = 1 << 3;
+    }
+}
+
+impl ElementSelectorFlags {
+  /// Returns the subset of flags that apply to the element.
+  pub fn for_self(self) -> ElementSelectorFlags {
+    self & (ElementSelectorFlags::HAS_EMPTY_SELECTOR)
+  }
+
+  /// Returns the subset of flags that apply to the parent.
+  pub fn for_parent(self) -> ElementSelectorFlags {
+    self
+      & (ElementSelectorFlags::HAS_SLOW_SELECTOR
+        | ElementSelectorFlags::HAS_SLOW_SELECTOR_LATER_SIBLINGS
+        | ElementSelectorFlags::HAS_EDGE_CHILD_SELECTOR)
+  }
+}
+
+/// Holds per-compound-selector data.
+struct LocalMatchingContext<'a, 'b, 'i, Impl: SelectorImpl<'i>> {
+  shared: &'a mut MatchingContext<'b, 'i, Impl>,
+  matches_hover_and_active_quirk: MatchesHoverAndActiveQuirk,
+}
+
+#[inline(always)]
+pub fn matches_selector_list<'i, E>(
+  selector_list: &SelectorList<'i, E::Impl>,
+  element: &E,
+  context: &mut MatchingContext<'_, 'i, E::Impl>,
+) -> bool
+where
+  E: Element<'i>,
+{
+  // This is pretty much any(..) but manually inlined because the compiler
+  // refuses to do so from querySelector / querySelectorAll.
+  for selector in &selector_list.0 {
+    let matches = matches_selector(selector, 0, None, element, context, &mut |_, _| {});
+
+    if matches {
+      return true;
+    }
+  }
+
+  false
+}
+
+#[inline(always)]
+fn may_match(hashes: &AncestorHashes, bf: &BloomFilter) -> bool {
+  // Check the first three hashes. Note that we can check for zero before
+  // masking off the high bits, since if any of the first three hashes is
+  // zero the fourth will be as well. We also take care to avoid the
+  // special-case complexity of the fourth hash until we actually reach it,
+  // because we usually don't.
+  //
+  // To be clear: this is all extremely hot.
+  for i in 0..3 {
+    let packed = hashes.packed_hashes[i];
+    if packed == 0 {
+      // No more hashes left - unable to fast-reject.
+      return true;
+    }
+
+    if !bf.might_contain_hash(packed & BLOOM_HASH_MASK) {
+      // Hooray! We fast-rejected on this hash.
+      return false;
+    }
+  }
+
+  // Now do the slighty-more-complex work of synthesizing the fourth hash,
+  // and check it against the filter if it exists.
+  let fourth = hashes.fourth_hash();
+  fourth == 0 || bf.might_contain_hash(fourth)
+}
+
+/// A result of selector matching, includes 3 failure types,
+///
+///   NotMatchedAndRestartFromClosestLaterSibling
+///   NotMatchedAndRestartFromClosestDescendant
+///   NotMatchedGlobally
+///
+/// When NotMatchedGlobally appears, stop selector matching completely since
+/// the succeeding selectors never matches.
+/// It is raised when
+///   Child combinator cannot find the candidate element.
+///   Descendant combinator cannot find the candidate element.
+///
+/// When NotMatchedAndRestartFromClosestDescendant appears, the selector
+/// matching does backtracking and restarts from the closest Descendant
+/// combinator.
+/// It is raised when
+///   NextSibling combinator cannot find the candidate element.
+///   LaterSibling combinator cannot find the candidate element.
+///   Child combinator doesn't match on the found element.
+///
+/// When NotMatchedAndRestartFromClosestLaterSibling appears, the selector
+/// matching does backtracking and restarts from the closest LaterSibling
+/// combinator.
+/// It is raised when
+///   NextSibling combinator doesn't match on the found element.
+///
+/// For example, when the selector "d1 d2 a" is provided and we cannot *find*
+/// an appropriate ancestor element for "d1", this selector matching raises
+/// NotMatchedGlobally since even if "d2" is moved to more upper element, the
+/// candidates for "d1" becomes less than before and d1 .
+///
+/// The next example is siblings. When the selector "b1 + b2 ~ d1 a" is
+/// provided and we cannot *find* an appropriate brother element for b1,
+/// the selector matching raises NotMatchedAndRestartFromClosestDescendant.
+/// The selectors ("b1 + b2 ~") doesn't match and matching restart from "d1".
+///
+/// The additional example is child and sibling. When the selector
+/// "b1 + c1 > b2 ~ d1 a" is provided and the selector "b1" doesn't match on
+/// the element, this "b1" raises NotMatchedAndRestartFromClosestLaterSibling.
+/// However since the selector "c1" raises
+/// NotMatchedAndRestartFromClosestDescendant. So the selector
+/// "b1 + c1 > b2 ~ " doesn't match and restart matching from "d1".
+#[derive(Clone, Copy, Eq, PartialEq)]
+enum SelectorMatchingResult {
+  Matched,
+  NotMatchedAndRestartFromClosestLaterSibling,
+  NotMatchedAndRestartFromClosestDescendant,
+  NotMatchedGlobally,
+}
+
+/// Whether the :hover and :active quirk applies.
+///
+/// https://quirks.spec.whatwg.org/#the-active-and-hover-quirk
+#[derive(Clone, Copy, Debug, PartialEq)]
+enum MatchesHoverAndActiveQuirk {
+  Yes,
+  No,
+}
+
+/// Matches a selector, fast-rejecting against a bloom filter.
+///
+/// We accept an offset to allow consumers to represent and match against
+/// partial selectors (indexed from the right). We use this API design, rather
+/// than having the callers pass a SelectorIter, because creating a SelectorIter
+/// requires dereferencing the selector to get the length, which adds an
+/// unnecessary cache miss for cases when we can fast-reject with AncestorHashes
+/// (which the caller can store inline with the selector pointer).
+#[inline(always)]
+pub fn matches_selector<'i, E, F>(
+  selector: &Selector<'i, E::Impl>,
+  offset: usize,
+  hashes: Option<&AncestorHashes>,
+  element: &E,
+  context: &mut MatchingContext<'_, 'i, E::Impl>,
+  flags_setter: &mut F,
+) -> bool
+where
+  E: Element<'i>,
+  F: FnMut(&E, ElementSelectorFlags),
+{
+  // Use the bloom filter to fast-reject.
+  if let Some(hashes) = hashes {
+    if let Some(filter) = context.bloom_filter {
+      if !may_match(hashes, filter) {
+        return false;
+      }
+    }
+  }
+
+  matches_complex_selector(selector.iter_from(offset), element, context, flags_setter)
+}
+
+/// Whether a compound selector matched, and whether it was the rightmost
+/// selector inside the complex selector.
+pub enum CompoundSelectorMatchingResult {
+  /// The selector was fully matched.
+  FullyMatched,
+  /// The compound selector matched, and the next combinator offset is
+  /// `next_combinator_offset`.
+  Matched { next_combinator_offset: usize },
+  /// The selector didn't match.
+  NotMatched,
+}
+
+/// Matches a compound selector belonging to `selector`, starting at offset
+/// `from_offset`, matching left to right.
+///
+/// Requires that `from_offset` points to a `Combinator`.
+///
+/// NOTE(emilio): This doesn't allow to match in the leftmost sequence of the
+/// complex selector, but it happens to be the case we don't need it.
+pub fn matches_compound_selector_from<'i, E>(
+  selector: &Selector<'i, E::Impl>,
+  mut from_offset: usize,
+  context: &mut MatchingContext<'_, 'i, E::Impl>,
+  element: &E,
+) -> CompoundSelectorMatchingResult
+where
+  E: Element<'i>,
+{
+  if cfg!(debug_assertions) && from_offset != 0 {
+    selector.combinator_at_parse_order(from_offset - 1); // This asserts.
+  }
+
+  let mut local_context = LocalMatchingContext {
+    shared: context,
+    matches_hover_and_active_quirk: MatchesHoverAndActiveQuirk::No,
+  };
+
+  // Find the end of the selector or the next combinator, then match
+  // backwards, so that we match in the same order as
+  // matches_complex_selector, which is usually faster.
+  let start_offset = from_offset;
+  for component in selector.iter_raw_parse_order_from(from_offset) {
+    if matches!(*component, Component::Combinator(..)) {
+      debug_assert_ne!(from_offset, 0, "Selector started with a combinator?");
+      break;
+    }
+
+    from_offset += 1;
+  }
+
+  debug_assert!(from_offset >= 1);
+  debug_assert!(from_offset <= selector.len());
+
+  let iter = selector.iter_from(selector.len() - from_offset);
+  debug_assert!(
+    iter.clone().next().is_some()
+      || (from_offset != selector.len()
+        && matches!(
+          selector.combinator_at_parse_order(from_offset),
+          Combinator::SlotAssignment | Combinator::PseudoElement
+        )),
+    "Got the math wrong: {:?} | {:?} | {} {}",
+    selector,
+    selector.iter_raw_match_order().as_slice(),
+    from_offset,
+    start_offset
+  );
+
+  for component in iter {
+    if !matches_simple_selector(component, element, &mut local_context, &mut |_, _| {}) {
+      return CompoundSelectorMatchingResult::NotMatched;
+    }
+  }
+
+  if from_offset != selector.len() {
+    return CompoundSelectorMatchingResult::Matched {
+      next_combinator_offset: from_offset,
+    };
+  }
+
+  CompoundSelectorMatchingResult::FullyMatched
+}
+
+/// Matches a complex selector.
+#[inline(always)]
+pub fn matches_complex_selector<'i, E, F>(
+  mut iter: SelectorIter<'_, 'i, E::Impl>,
+  element: &E,
+  context: &mut MatchingContext<'_, 'i, E::Impl>,
+  flags_setter: &mut F,
+) -> bool
+where
+  E: Element<'i>,
+  F: FnMut(&E, ElementSelectorFlags),
+{
+  // If this is the special pseudo-element mode, consume the ::pseudo-element
+  // before proceeding, since the caller has already handled that part.
+  if context.matching_mode() == MatchingMode::ForStatelessPseudoElement && !context.is_nested() {
+    // Consume the pseudo.
+    match *iter.next().unwrap() {
+      Component::PseudoElement(ref pseudo) => {
+        if let Some(ref f) = context.pseudo_element_matching_fn {
+          if !f(pseudo) {
+            return false;
+          }
+        }
+      }
+      _ => {
+        debug_assert!(
+          false,
+          "Used MatchingMode::ForStatelessPseudoElement \
+                     in a non-pseudo selector"
+        );
+      }
+    }
+
+    if !iter.matches_for_stateless_pseudo_element() {
+      return false;
+    }
+
+    // Advance to the non-pseudo-element part of the selector.
+    let next_sequence = iter.next_sequence().unwrap();
+    debug_assert_eq!(next_sequence, Combinator::PseudoElement);
+  }
+
+  let result = matches_complex_selector_internal(iter, element, context, flags_setter, Rightmost::Yes);
+
+  matches!(result, SelectorMatchingResult::Matched)
+}
+
+#[inline]
+fn matches_hover_and_active_quirk<'i, Impl: SelectorImpl<'i>>(
+  selector_iter: &SelectorIter<'_, 'i, Impl>,
+  context: &MatchingContext<'_, 'i, Impl>,
+  rightmost: Rightmost,
+) -> MatchesHoverAndActiveQuirk {
+  if context.quirks_mode() != QuirksMode::Quirks {
+    return MatchesHoverAndActiveQuirk::No;
+  }
+
+  if context.is_nested() {
+    return MatchesHoverAndActiveQuirk::No;
+  }
+
+  // This compound selector had a pseudo-element to the right that we
+  // intentionally skipped.
+  if rightmost == Rightmost::Yes && context.matching_mode() == MatchingMode::ForStatelessPseudoElement {
+    return MatchesHoverAndActiveQuirk::No;
+  }
+
+  let all_match = selector_iter.clone().all(|simple| match *simple {
+    Component::LocalName(_)
+    | Component::AttributeInNoNamespaceExists { .. }
+    | Component::AttributeInNoNamespace { .. }
+    | Component::AttributeOther(_)
+    | Component::ID(_)
+    | Component::Class(_)
+    | Component::PseudoElement(_)
+    | Component::Negation(_)
+    | Component::Nth(_)
+    | Component::NthOf(..)
+    | Component::Empty => false,
+    Component::NonTSPseudoClass(ref pseudo_class) => pseudo_class.is_active_or_hover(),
+    _ => true,
+  });
+
+  if all_match {
+    MatchesHoverAndActiveQuirk::Yes
+  } else {
+    MatchesHoverAndActiveQuirk::No
+  }
+}
+
+#[derive(Clone, Copy, PartialEq)]
+enum Rightmost {
+  Yes,
+  No,
+}
+
+#[inline(always)]
+fn next_element_for_combinator<'i, E>(
+  element: &E,
+  combinator: Combinator,
+  selector: &SelectorIter<'_, 'i, E::Impl>,
+  context: &MatchingContext<'_, 'i, E::Impl>,
+) -> Option<E>
+where
+  E: Element<'i>,
+{
+  match combinator {
+    Combinator::NextSibling | Combinator::LaterSibling => element.prev_sibling_element(),
+    Combinator::Child | Combinator::Descendant | Combinator::Deep | Combinator::DeepDescendant => {
+      match element.parent_element() {
+        Some(e) => return Some(e),
+        None => {}
+      }
+
+      if !element.parent_node_is_shadow_root() {
+        return None;
+      }
+
+      // https://drafts.csswg.org/css-scoping/#host-element-in-tree:
+      //
+      //   For the purpose of Selectors, a shadow host also appears in
+      //   its shadow tree, with the contents of the shadow tree treated
+      //   as its children. (In other words, the shadow host is treated as
+      //   replacing the shadow root node.)
+      //
+      // and also:
+      //
+      //   When considered within its own shadow trees, the shadow host is
+      //   featureless. Only the :host, :host(), and :host-context()
+      //   pseudo-classes are allowed to match it.
+      //
+      // Since we know that the parent is a shadow root, we necessarily
+      // are in a shadow tree of the host, and the next selector will only
+      // match if the selector is a featureless :host selector.
+      if !selector.clone().is_featureless_host_selector() {
+        return None;
+      }
+
+      element.containing_shadow_host()
+    }
+    Combinator::Part => element.containing_shadow_host(),
+    Combinator::SlotAssignment => {
+      debug_assert!(element.assigned_slot().map_or(true, |s| s.is_html_slot_element()));
+      let scope = context.current_host?;
+      let mut current_slot = element.assigned_slot()?;
+      while current_slot.containing_shadow_host().unwrap().opaque() != scope {
+        current_slot = current_slot.assigned_slot()?;
+      }
+      Some(current_slot)
+    }
+    Combinator::PseudoElement => element.pseudo_element_originating_element(),
+  }
+}
+
+fn matches_complex_selector_internal<'i, E, F>(
+  mut selector_iter: SelectorIter<'_, 'i, E::Impl>,
+  element: &E,
+  context: &mut MatchingContext<'_, 'i, E::Impl>,
+  flags_setter: &mut F,
+  rightmost: Rightmost,
+) -> SelectorMatchingResult
+where
+  E: Element<'i>,
+  F: FnMut(&E, ElementSelectorFlags),
+{
+  debug!("Matching complex selector {:?} for {:?}", selector_iter, element);
+
+  let matches_compound_selector =
+    matches_compound_selector(&mut selector_iter, element, context, flags_setter, rightmost);
+
+  let combinator = selector_iter.next_sequence();
+  if combinator.map_or(false, |c| c.is_sibling()) {
+    flags_setter(element, ElementSelectorFlags::HAS_SLOW_SELECTOR_LATER_SIBLINGS);
+  }
+
+  if !matches_compound_selector {
+    return SelectorMatchingResult::NotMatchedAndRestartFromClosestLaterSibling;
+  }
+
+  let combinator = match combinator {
+    None => return SelectorMatchingResult::Matched,
+    Some(c) => c,
+  };
+
+  let candidate_not_found = match combinator {
+    Combinator::NextSibling | Combinator::LaterSibling => {
+      SelectorMatchingResult::NotMatchedAndRestartFromClosestDescendant
+    }
+    Combinator::Child
+    | Combinator::Deep
+    | Combinator::DeepDescendant
+    | Combinator::Descendant
+    | Combinator::SlotAssignment
+    | Combinator::Part
+    | Combinator::PseudoElement => SelectorMatchingResult::NotMatchedGlobally,
+  };
+
+  let mut next_element = next_element_for_combinator(element, combinator, &selector_iter, &context);
+
+  // Stop matching :visited as soon as we find a link, or a combinator for
+  // something that isn't an ancestor.
+  let mut visited_handling = if element.is_link() || combinator.is_sibling() {
+    VisitedHandlingMode::AllLinksUnvisited
+  } else {
+    context.visited_handling()
+  };
+
+  loop {
+    let element = match next_element {
+      None => return candidate_not_found,
+      Some(next_element) => next_element,
+    };
+
+    let result = context.with_visited_handling_mode(visited_handling, |context| {
+      matches_complex_selector_internal(selector_iter.clone(), &element, context, flags_setter, Rightmost::No)
+    });
+
+    match (result, combinator) {
+      // Return the status immediately.
+      (SelectorMatchingResult::Matched, _)
+      | (SelectorMatchingResult::NotMatchedGlobally, _)
+      | (_, Combinator::NextSibling) => {
+        return result;
+      }
+
+      // Upgrade the failure status to
+      // NotMatchedAndRestartFromClosestDescendant.
+      (_, Combinator::PseudoElement) | (_, Combinator::Child) => {
+        return SelectorMatchingResult::NotMatchedAndRestartFromClosestDescendant;
+      }
+
+      // If the failure status is
+      // NotMatchedAndRestartFromClosestDescendant and combinator is
+      // Combinator::LaterSibling, give up this Combinator::LaterSibling
+      // matching and restart from the closest descendant combinator.
+      (SelectorMatchingResult::NotMatchedAndRestartFromClosestDescendant, Combinator::LaterSibling) => {
+        return result;
+      }
+
+      // The Combinator::Descendant combinator and the status is
+      // NotMatchedAndRestartFromClosestLaterSibling or
+      // NotMatchedAndRestartFromClosestDescendant, or the
+      // Combinator::LaterSibling combinator and the status is
+      // NotMatchedAndRestartFromClosestDescendant, we can continue to
+      // matching on the next candidate element.
+      _ => {}
+    }
+
+    if element.is_link() {
+      visited_handling = VisitedHandlingMode::AllLinksUnvisited;
+    }
+
+    next_element = next_element_for_combinator(&element, combinator, &selector_iter, &context);
+  }
+}
+
+#[inline]
+fn matches_local_name<'i, E>(element: &E, local_name: &LocalName<'i, E::Impl>) -> bool
+where
+  E: Element<'i>,
+{
+  let name = select_name(
+    element.is_html_element_in_html_document(),
+    &local_name.name,
+    &local_name.lower_name,
+  )
+  .borrow();
+  element.has_local_name(name)
+}
+
+/// Determines whether the given element matches the given compound selector.
+#[inline]
+fn matches_compound_selector<'i, E, F>(
+  selector_iter: &mut SelectorIter<'_, 'i, E::Impl>,
+  element: &E,
+  context: &mut MatchingContext<'_, 'i, E::Impl>,
+  flags_setter: &mut F,
+  rightmost: Rightmost,
+) -> bool
+where
+  E: Element<'i>,
+  F: FnMut(&E, ElementSelectorFlags),
+{
+  let matches_hover_and_active_quirk = matches_hover_and_active_quirk(&selector_iter, context, rightmost);
+
+  // Handle some common cases first.
+  // We may want to get rid of this at some point if we can make the
+  // generic case fast enough.
+  let mut selector = selector_iter.next();
+  if let Some(&Component::LocalName(ref local_name)) = selector {
+    if !matches_local_name(element, local_name) {
+      return false;
+    }
+    selector = selector_iter.next();
+  }
+  let class_and_id_case_sensitivity = context.classes_and_ids_case_sensitivity();
+  if let Some(&Component::ID(ref id)) = selector {
+    if !element.has_id(id, class_and_id_case_sensitivity) {
+      return false;
+    }
+    selector = selector_iter.next();
+  }
+  while let Some(&Component::Class(ref class)) = selector {
+    if !element.has_class(class, class_and_id_case_sensitivity) {
+      return false;
+    }
+    selector = selector_iter.next();
+  }
+  let selector = match selector {
+    Some(s) => s,
+    None => return true,
+  };
+
+  let mut local_context = LocalMatchingContext {
+    shared: context,
+    matches_hover_and_active_quirk,
+  };
+  iter::once(selector)
+    .chain(selector_iter)
+    .all(|simple| matches_simple_selector(simple, element, &mut local_context, flags_setter))
+}
+
+/// Determines whether the given element matches the given single selector.
+fn matches_simple_selector<'i, E, F>(
+  selector: &Component<'i, E::Impl>,
+  element: &E,
+  context: &mut LocalMatchingContext<'_, '_, 'i, E::Impl>,
+  flags_setter: &mut F,
+) -> bool
+where
+  E: Element<'i>,
+  F: FnMut(&E, ElementSelectorFlags),
+{
+  debug_assert!(context.shared.is_nested() || !context.shared.in_negation());
+
+  match *selector {
+    Component::Combinator(_) => unreachable!(),
+    Component::Part(ref parts) => {
+      let mut hosts = SmallVec::<[E; 4]>::new();
+
+      let mut host = match element.containing_shadow_host() {
+        Some(h) => h,
+        None => return false,
+      };
+
+      let current_host = context.shared.current_host;
+      if current_host != Some(host.opaque()) {
+        loop {
+          let outer_host = host.containing_shadow_host();
+          if outer_host.as_ref().map(|h| h.opaque()) == current_host {
+            break;
+          }
+          let outer_host = match outer_host {
+            Some(h) => h,
+            None => return false,
+          };
+          // TODO(emilio): if worth it, we could early return if
+          // host doesn't have the exportparts attribute.
+          hosts.push(host);
+          host = outer_host;
+        }
+      }
+
+      // Translate the part into the right scope.
+      parts.iter().all(|part| {
+        let mut part = part.clone();
+        for host in hosts.iter().rev() {
+          part = match host.imported_part(&part) {
+            Some(p) => p,
+            None => return false,
+          };
+        }
+        element.is_part(&part)
+      })
+    }
+    Component::Slotted(ref selector) => {
+      // <slots> are never flattened tree slottables.
+      !element.is_html_slot_element()
+        && context
+          .shared
+          .nest(|context| matches_complex_selector(selector.iter(), element, context, flags_setter))
+    }
+    Component::PseudoElement(ref pseudo) => element.match_pseudo_element(pseudo, context.shared),
+    Component::LocalName(ref local_name) => matches_local_name(element, local_name),
+    Component::ExplicitUniversalType | Component::ExplicitAnyNamespace => true,
+    Component::Namespace(_, ref url) | Component::DefaultNamespace(ref url) => {
+      element.has_namespace(&url.borrow())
+    }
+    Component::ExplicitNoNamespace => {
+      let ns = crate::parser::namespace_empty_string::<E::Impl>();
+      element.has_namespace(&ns.borrow())
+    }
+    Component::ID(ref id) => element.has_id(id, context.shared.classes_and_ids_case_sensitivity()),
+    Component::Class(ref class) => element.has_class(class, context.shared.classes_and_ids_case_sensitivity()),
+    Component::AttributeInNoNamespaceExists {
+      ref local_name,
+      ref local_name_lower,
+    } => {
+      let is_html = element.is_html_element_in_html_document();
+      element.attr_matches(
+        &NamespaceConstraint::Specific(&crate::parser::namespace_empty_string::<E::Impl>()),
+        select_name(is_html, local_name, local_name_lower),
+        &AttrSelectorOperation::Exists,
+      )
+    }
+    Component::AttributeInNoNamespace {
+      ref local_name,
+      ref value,
+      operator,
+      case_sensitivity,
+      never_matches,
+    } => {
+      if never_matches {
+        return false;
+      }
+      let is_html = element.is_html_element_in_html_document();
+      element.attr_matches(
+        &NamespaceConstraint::Specific(&crate::parser::namespace_empty_string::<E::Impl>()),
+        local_name,
+        &AttrSelectorOperation::WithValue {
+          operator,
+          case_sensitivity: case_sensitivity.to_unconditional(is_html),
+          expected_value: value,
+        },
+      )
+    }
+    Component::AttributeOther(ref attr_sel) => {
+      if attr_sel.never_matches {
+        return false;
+      }
+      let is_html = element.is_html_element_in_html_document();
+      let empty_string;
+      let namespace = match attr_sel.namespace() {
+        Some(ns) => ns,
+        None => {
+          empty_string = crate::parser::namespace_empty_string::<E::Impl>();
+          NamespaceConstraint::Specific(&empty_string)
+        }
+      };
+      element.attr_matches(
+        &namespace,
+        select_name(is_html, &attr_sel.local_name, &attr_sel.local_name_lower),
+        &match attr_sel.operation {
+          ParsedAttrSelectorOperation::Exists => AttrSelectorOperation::Exists,
+          ParsedAttrSelectorOperation::WithValue {
+            operator,
+            case_sensitivity,
+            ref expected_value,
+          } => AttrSelectorOperation::WithValue {
+            operator,
+            case_sensitivity: case_sensitivity.to_unconditional(is_html),
+            expected_value,
+          },
+        },
+      )
+    }
+    Component::NonTSPseudoClass(ref pc) => {
+      if context.matches_hover_and_active_quirk == MatchesHoverAndActiveQuirk::Yes
+        && !context.shared.is_nested()
+        && pc.is_active_or_hover()
+        && !element.is_link()
+      {
+        return false;
+      }
+
+      element.match_non_ts_pseudo_class(pc, &mut context.shared, flags_setter)
+    }
+    Component::Root => element.is_root(),
+    Component::Empty => {
+      flags_setter(element, ElementSelectorFlags::HAS_EMPTY_SELECTOR);
+      element.is_empty()
+    }
+    Component::Host(ref selector) => {
+      context.shared.shadow_host().map_or(false, |host| host == element.opaque())
+        && selector.as_ref().map_or(true, |selector| {
+          context
+            .shared
+            .nest(|context| matches_complex_selector(selector.iter(), element, context, flags_setter))
+        })
+    }
+    Component::Scope => match context.shared.scope_element {
+      Some(ref scope_element) => element.opaque() == *scope_element,
+      None => element.is_root(),
+    },
+    Component::Nth(data) => match data.ty {
+      NthType::Child => match (data.a, data.b) {
+        (0, 1) => matches_first_child(element, flags_setter),
+        (a, b) => matches_generic_nth_child(element, context, a, b, false, false, flags_setter),
+      },
+      NthType::LastChild => match (data.a, data.b) {
+        (0, 1) => matches_last_child(element, flags_setter),
+        (a, b) => matches_generic_nth_child(element, context, a, b, false, true, flags_setter),
+      },
+      NthType::OnlyChild => {
+        matches_first_child(element, flags_setter) && matches_last_child(element, flags_setter)
+      }
+      NthType::OfType => matches_generic_nth_child(element, context, data.a, data.b, true, false, flags_setter),
+      NthType::LastOfType => matches_generic_nth_child(element, context, data.a, data.b, true, true, flags_setter),
+      NthType::OnlyOfType => {
+        matches_generic_nth_child(element, context, 0, 1, true, false, flags_setter)
+          && matches_generic_nth_child(element, context, 0, 1, true, true, flags_setter)
+      }
+      _ => todo!(),
+    },
+    Component::NthOf(..) => todo!(),
+    Component::Is(ref list) | Component::Where(ref list) | Component::Any(_, ref list) => {
+      context.shared.nest(|context| {
+        for selector in &**list {
+          if matches_complex_selector(selector.iter(), element, context, flags_setter) {
+            return true;
+          }
+        }
+        false
+      })
+    }
+    Component::Negation(ref list) => context.shared.nest_for_negation(|context| {
+      for selector in &**list {
+        if matches_complex_selector(selector.iter(), element, context, flags_setter) {
+          return false;
+        }
+      }
+      true
+    }),
+    Component::Nesting | Component::Has(..) => unreachable!(),
+  }
+}
+
+#[inline(always)]
+fn select_name<'a, T>(is_html: bool, local_name: &'a T, local_name_lower: &'a T) -> &'a T {
+  if is_html {
+    local_name_lower
+  } else {
+    local_name
+  }
+}
+
+#[inline]
+fn matches_generic_nth_child<'i, E, F>(
+  element: &E,
+  context: &mut LocalMatchingContext<'_, '_, 'i, E::Impl>,
+  a: i32,
+  b: i32,
+  is_of_type: bool,
+  is_from_end: bool,
+  flags_setter: &mut F,
+) -> bool
+where
+  E: Element<'i>,
+  F: FnMut(&E, ElementSelectorFlags),
+{
+  if element.ignores_nth_child_selectors() {
+    return false;
+  }
+
+  flags_setter(
+    element,
+    if is_from_end {
+      ElementSelectorFlags::HAS_SLOW_SELECTOR
+    } else {
+      ElementSelectorFlags::HAS_SLOW_SELECTOR_LATER_SIBLINGS
+    },
+  );
+
+  // Grab a reference to the appropriate cache.
+  let mut cache = context.shared.nth_index_cache.as_mut().map(|c| c.get(is_of_type, is_from_end));
+
+  // Lookup or compute the index.
+  let index = if let Some(i) = cache.as_mut().and_then(|c| c.lookup(element.opaque())) {
+    i
+  } else {
+    let i = nth_child_index(element, is_of_type, is_from_end, cache.as_deref_mut());
+    if let Some(c) = cache.as_mut() {
+      c.insert(element.opaque(), i)
+    }
+    i
+  };
+  debug_assert_eq!(
+    index,
+    nth_child_index(element, is_of_type, is_from_end, None),
+    "invalid cache"
+  );
+
+  // Is there a non-negative integer n such that An+B=index?
+  match index.checked_sub(b) {
+    None => false,
+    Some(an) => match an.checked_div(a) {
+            Some(n) => n >= 0 && a * n == an,
+            None /* a == 0 */ => an == 0,
+        },
+  }
+}
+
+#[inline]
+fn nth_child_index<'i, E>(
+  element: &E,
+  is_of_type: bool,
+  is_from_end: bool,
+  mut cache: Option<&mut NthIndexCacheInner>,
+) -> i32
+where
+  E: Element<'i>,
+{
+  // The traversal mostly processes siblings left to right. So when we walk
+  // siblings to the right when computing NthLast/NthLastOfType we're unlikely
+  // to get cache hits along the way. As such, we take the hit of walking the
+  // siblings to the left checking the cache in the is_from_end case (this
+  // matches what Gecko does). The indices-from-the-left is handled during the
+  // regular look further below.
+  if let Some(ref mut c) = cache {
+    if is_from_end && !c.is_empty() {
+      let mut index: i32 = 1;
+      let mut curr = element.clone();
+      while let Some(e) = curr.prev_sibling_element() {
+        curr = e;
+        if !is_of_type || element.is_same_type(&curr) {
+          if let Some(i) = c.lookup(curr.opaque()) {
+            return i - index;
+          }
+          index += 1;
+        }
+      }
+    }
+  }
+
+  let mut index: i32 = 1;
+  let mut curr = element.clone();
+  let next = |e: E| {
+    if is_from_end {
+      e.next_sibling_element()
+    } else {
+      e.prev_sibling_element()
+    }
+  };
+  while let Some(e) = next(curr) {
+    curr = e;
+    if !is_of_type || element.is_same_type(&curr) {
+      // If we're computing indices from the left, check each element in the
+      // cache. We handle the indices-from-the-right case at the top of this
+      // function.
+      if !is_from_end {
+        if let Some(i) = cache.as_mut().and_then(|c| c.lookup(curr.opaque())) {
+          return i + index;
+        }
+      }
+      index += 1;
+    }
+  }
+
+  index
+}
+
+#[inline]
+fn matches_first_child<'i, E, F>(element: &E, flags_setter: &mut F) -> bool
+where
+  E: Element<'i>,
+  F: FnMut(&E, ElementSelectorFlags),
+{
+  flags_setter(element, ElementSelectorFlags::HAS_EDGE_CHILD_SELECTOR);
+  element.prev_sibling_element().is_none()
+}
+
+#[inline]
+fn matches_last_child<'i, E, F>(element: &E, flags_setter: &mut F) -> bool
+where
+  E: Element<'i>,
+  F: FnMut(&E, ElementSelectorFlags),
+{
+  flags_setter(element, ElementSelectorFlags::HAS_EDGE_CHILD_SELECTOR);
+  element.next_sibling_element().is_none()
+}
diff --git a/selectors/nth_index_cache.rs b/selectors/nth_index_cache.rs
new file mode 100644
index 0000000..c5bb8db
--- /dev/null
+++ b/selectors/nth_index_cache.rs
@@ -0,0 +1,52 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+use crate::tree::OpaqueElement;
+use rustc_hash::FxHashMap;
+
+/// A cache to speed up matching of nth-index-like selectors.
+///
+/// See [1] for some discussion around the design tradeoffs.
+///
+/// [1] https://bugzilla.mozilla.org/show_bug.cgi?id=1401855#c3
+#[derive(Default)]
+pub struct NthIndexCache {
+  nth: NthIndexCacheInner,
+  nth_last: NthIndexCacheInner,
+  nth_of_type: NthIndexCacheInner,
+  nth_last_of_type: NthIndexCacheInner,
+}
+
+impl NthIndexCache {
+  /// Gets the appropriate cache for the given parameters.
+  pub fn get(&mut self, is_of_type: bool, is_from_end: bool) -> &mut NthIndexCacheInner {
+    match (is_of_type, is_from_end) {
+      (false, false) => &mut self.nth,
+      (false, true) => &mut self.nth_last,
+      (true, false) => &mut self.nth_of_type,
+      (true, true) => &mut self.nth_last_of_type,
+    }
+  }
+}
+
+/// The concrete per-pseudo-class cache.
+#[derive(Default)]
+pub struct NthIndexCacheInner(FxHashMap<OpaqueElement, i32>);
+
+impl NthIndexCacheInner {
+  /// Does a lookup for a given element in the cache.
+  pub fn lookup(&mut self, el: OpaqueElement) -> Option<i32> {
+    self.0.get(&el).copied()
+  }
+
+  /// Inserts an entry into the cache.
+  pub fn insert(&mut self, element: OpaqueElement, index: i32) {
+    self.0.insert(element, index);
+  }
+
+  /// Returns whether the cache is empty.
+  pub fn is_empty(&self) -> bool {
+    self.0.is_empty()
+  }
+}
diff --git a/selectors/parser.rs b/selectors/parser.rs
new file mode 100644
index 0000000..ed7b97f
--- /dev/null
+++ b/selectors/parser.rs
@@ -0,0 +1,4005 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+use crate::attr::{AttrSelectorOperator, AttrSelectorWithOptionalNamespace};
+use crate::attr::{NamespaceConstraint, ParsedAttrSelectorOperation};
+use crate::attr::{ParsedCaseSensitivity, SELECTOR_WHITESPACE};
+use crate::bloom::BLOOM_HASH_MASK;
+use crate::builder::{SelectorBuilder, SelectorFlags, SpecificityAndFlags};
+use crate::context::QuirksMode;
+use crate::sink::Push;
+pub use crate::visitor::SelectorVisitor;
+use cssparser::parse_nth;
+use cssparser::{BasicParseError, BasicParseErrorKind, ParseError, ParseErrorKind};
+use cssparser::{CowRcStr, Delimiter, SourceLocation};
+use cssparser::{Parser as CssParser, ToCss, Token};
+use precomputed_hash::PrecomputedHash;
+use smallvec::{smallvec, SmallVec};
+use std::borrow::Borrow;
+use std::fmt::{self, Debug};
+use std::iter::Rev;
+use std::slice;
+
+/// A trait that represents a pseudo-element.
+pub trait PseudoElement<'i>: Sized + ToCss {
+  /// The `SelectorImpl` this pseudo-element is used for.
+  type Impl: SelectorImpl<'i>;
+
+  /// Whether the pseudo-element supports a given state selector to the right
+  /// of it.
+  fn accepts_state_pseudo_classes(&self) -> bool {
+    false
+  }
+
+  /// Whether this pseudo-element is valid after a ::slotted(..) pseudo.
+  fn valid_after_slotted(&self) -> bool {
+    false
+  }
+
+  fn is_webkit_scrollbar(&self) -> bool {
+    false
+  }
+
+  fn is_view_transition(&self) -> bool {
+    false
+  }
+
+  fn is_unknown(&self) -> bool {
+    false
+  }
+}
+
+/// A trait that represents a pseudo-class.
+pub trait NonTSPseudoClass<'i>: Sized + ToCss {
+  /// The `SelectorImpl` this pseudo-element is used for.
+  type Impl: SelectorImpl<'i>;
+
+  /// Whether this pseudo-class is :active or :hover.
+  fn is_active_or_hover(&self) -> bool;
+
+  /// Whether this pseudo-class belongs to:
+  ///
+  /// https://drafts.csswg.org/selectors-4/#useraction-pseudos
+  fn is_user_action_state(&self) -> bool;
+
+  fn is_valid_before_webkit_scrollbar(&self) -> bool {
+    true
+  }
+
+  fn is_valid_after_webkit_scrollbar(&self) -> bool {
+    false
+  }
+
+  fn visit<V>(&self, _visitor: &mut V) -> bool
+  where
+    V: SelectorVisitor<'i, Impl = Self::Impl>,
+  {
+    true
+  }
+}
+
+/// Returns a Cow::Borrowed if `s` is already ASCII lowercase, and a
+/// Cow::Owned if `s` had to be converted into ASCII lowercase.
+fn to_ascii_lowercase<'i>(s: CowRcStr<'i>) -> CowRcStr<'i> {
+  if let Some(first_uppercase) = s.bytes().position(|byte| byte >= b'A' && byte <= b'Z') {
+    let mut string = s.to_string();
+    string[first_uppercase..].make_ascii_lowercase();
+    string.into()
+  } else {
+    s
+  }
+}
+
+bitflags! {
+    /// Flags that indicate at which point of parsing a selector are we.
+    #[derive(PartialEq, Eq, Clone, Copy)]
+    struct SelectorParsingState: u16 {
+        /// Whether we should avoid adding default namespaces to selectors that
+        /// aren't type or universal selectors.
+        const SKIP_DEFAULT_NAMESPACE = 1 << 0;
+
+        /// Whether we've parsed a ::slotted() pseudo-element already.
+        ///
+        /// If so, then we can only parse a subset of pseudo-elements, and
+        /// whatever comes after them if so.
+        const AFTER_SLOTTED = 1 << 1;
+        /// Whether we've parsed a ::part() pseudo-element already.
+        ///
+        /// If so, then we can only parse a subset of pseudo-elements, and
+        /// whatever comes after them if so.
+        const AFTER_PART = 1 << 2;
+        /// Whether we've parsed a pseudo-element (as in, an
+        /// `Impl::PseudoElement` thus not accounting for `::slotted` or
+        /// `::part`) already.
+        ///
+        /// If so, then other pseudo-elements and most other selectors are
+        /// disallowed.
+        const AFTER_PSEUDO_ELEMENT = 1 << 3;
+        /// Whether we've parsed a non-stateful pseudo-element (again, as-in
+        /// `Impl::PseudoElement`) already. If so, then other pseudo-classes are
+        /// disallowed. If this flag is set, `AFTER_PSEUDO_ELEMENT` must be set
+        /// as well.
+        const AFTER_NON_STATEFUL_PSEUDO_ELEMENT = 1 << 4;
+
+        /// Whether we are after any of the pseudo-like things.
+        const AFTER_PSEUDO = Self::AFTER_PART.bits() | Self::AFTER_SLOTTED.bits() | Self::AFTER_PSEUDO_ELEMENT.bits();
+
+        /// Whether we explicitly disallow combinators.
+        const DISALLOW_COMBINATORS = 1 << 5;
+
+        /// Whether we explicitly disallow pseudo-element-like things.
+        const DISALLOW_PSEUDOS = 1 << 6;
+
+        /// Whether we have seen a nesting selector.
+        const AFTER_NESTING = 1 << 7;
+
+        const AFTER_WEBKIT_SCROLLBAR = 1 << 8;
+        const AFTER_VIEW_TRANSITION = 1 << 9;
+        const AFTER_UNKNOWN_PSEUDO_ELEMENT = 1 << 10;
+    }
+}
+
+impl SelectorParsingState {
+  #[inline]
+  fn allows_pseudos(self) -> bool {
+    // NOTE(emilio): We allow pseudos after ::part and such.
+    !self.intersects(Self::AFTER_PSEUDO_ELEMENT | Self::DISALLOW_PSEUDOS)
+  }
+
+  #[inline]
+  fn allows_slotted(self) -> bool {
+    !self.intersects(Self::AFTER_PSEUDO | Self::DISALLOW_PSEUDOS)
+  }
+
+  #[inline]
+  fn allows_part(self) -> bool {
+    !self.intersects(Self::AFTER_PSEUDO | Self::DISALLOW_PSEUDOS)
+  }
+
+  // TODO(emilio): Maybe some of these should be allowed, but this gets us on
+  // the safe side for now, matching previous behavior. Gotta be careful with
+  // the ones like :-moz-any, which allow nested selectors but don't carry the
+  // state, and so on.
+  #[inline]
+  fn allows_custom_functional_pseudo_classes(self) -> bool {
+    !self.intersects(Self::AFTER_PSEUDO)
+  }
+
+  #[inline]
+  fn allows_non_functional_pseudo_classes(self) -> bool {
+    !self.intersects(Self::AFTER_SLOTTED | Self::AFTER_NON_STATEFUL_PSEUDO_ELEMENT)
+  }
+
+  #[inline]
+  fn allows_tree_structural_pseudo_classes(self) -> bool {
+    !self.intersects(Self::AFTER_PSEUDO)
+  }
+
+  #[inline]
+  fn allows_combinators(self) -> bool {
+    !self.intersects(Self::DISALLOW_COMBINATORS)
+  }
+}
+
+pub type SelectorParseError<'i> = ParseError<'i, SelectorParseErrorKind<'i>>;
+
+#[derive(Clone, Debug, PartialEq)]
+pub enum SelectorParseErrorKind<'i> {
+  NoQualifiedNameInAttributeSelector(Token<'i>),
+  EmptySelector,
+  DanglingCombinator,
+  InvalidPseudoClassBeforeWebKitScrollbar,
+  InvalidPseudoClassAfterWebKitScrollbar,
+  InvalidPseudoClassAfterPseudoElement,
+  InvalidState,
+  MissingNestingSelector,
+  MissingNestingPrefix,
+  UnexpectedTokenInAttributeSelector(Token<'i>),
+  PseudoElementExpectedIdent(Token<'i>),
+  UnsupportedPseudoElement(CowRcStr<'i>),
+  UnsupportedPseudoClass(CowRcStr<'i>),
+  AmbiguousCssModuleClass(CowRcStr<'i>),
+  UnexpectedIdent(CowRcStr<'i>),
+  ExpectedNamespace(CowRcStr<'i>),
+  ExpectedBarInAttr(Token<'i>),
+  BadValueInAttr(Token<'i>),
+  InvalidQualNameInAttr(Token<'i>),
+  ExplicitNamespaceUnexpectedToken(Token<'i>),
+  ClassNeedsIdent(Token<'i>),
+  UnexpectedSelectorAfterPseudoElement(Token<'i>),
+}
+
+macro_rules! with_all_bounds {
+    (
+        [ $( $InSelector: tt )* ]
+        [ $( $CommonBounds: tt )* ]
+        [ $( $FromStr: tt )* ]
+    ) => {
+        /// This trait allows to define the parser implementation in regards
+        /// of pseudo-classes/elements
+        ///
+        /// NB: We need Clone so that we can derive(Clone) on struct with that
+        /// are parameterized on SelectorImpl. See
+        /// <https://github.com/rust-lang/rust/issues/26925>
+        pub trait SelectorImpl<'i>: Clone + Debug + Sized + 'static {
+            type ExtraMatchingData: Sized + Default + 'static;
+            type AttrValue: $($InSelector)*;
+            type Identifier: $($InSelector)*;
+            type LocalName: $($InSelector)* + Borrow<Self::BorrowedLocalName>;
+            type NamespaceUrl: $($CommonBounds)* + $($FromStr)* + Default + Borrow<Self::BorrowedNamespaceUrl>;
+            type NamespacePrefix: $($InSelector)* + Default;
+            type BorrowedNamespaceUrl: ?Sized + Eq;
+            type BorrowedLocalName: ?Sized + Eq;
+
+            /// non tree-structural pseudo-classes
+            /// (see: https://drafts.csswg.org/selectors/#structural-pseudos)
+            type NonTSPseudoClass: $($CommonBounds)* + NonTSPseudoClass<'i, Impl = Self>;
+            type VendorPrefix: Sized + Eq + $($CommonBounds)* + ToCss;
+
+            /// pseudo-elements
+            type PseudoElement: $($CommonBounds)* + PseudoElement<'i, Impl = Self>;
+
+            fn to_css<W: fmt::Write>(selectors: &SelectorList<'i, Self>, dest: &mut W) -> fmt::Result {
+                serialize_selector_list(selectors.0.iter(), dest)
+            }
+        }
+    }
+}
+
+macro_rules! with_bounds {
+    ( [ $( $CommonBounds: tt )* ] [ $( $FromStr: tt )* ]) => {
+        with_all_bounds! {
+            [$($CommonBounds)* + $($FromStr)* + ToCss]
+            [$($CommonBounds)*]
+            [$($FromStr)*]
+        }
+    }
+}
+
+#[cfg(feature = "serde")]
+with_bounds! {
+    [Clone + PartialEq + Eq + std::hash::Hash]
+    [From<CowRcStr<'i>> + From<std::borrow::Cow<'i, str>> + AsRef<str>]
+}
+
+#[cfg(not(feature = "serde"))]
+with_bounds! {
+    [Clone + PartialEq + Eq + std::hash::Hash]
+    [From<CowRcStr<'i>>]
+}
+
+pub trait Parser<'i> {
+  type Impl: SelectorImpl<'i>;
+  type Error: 'i + From<SelectorParseErrorKind<'i>>;
+
+  /// Whether to parse the `::slotted()` pseudo-element.
+  fn parse_slotted(&self) -> bool {
+    false
+  }
+
+  /// Whether to parse the `::part()` pseudo-element.
+  fn parse_part(&self) -> bool {
+    false
+  }
+
+  /// Whether to parse the `:where` pseudo-class.
+  fn parse_is_and_where(&self) -> bool {
+    false
+  }
+
+  /// The error recovery that selector lists inside :is() and :where() have.
+  fn is_and_where_error_recovery(&self) -> ParseErrorRecovery {
+    ParseErrorRecovery::IgnoreInvalidSelector
+  }
+
+  /// Whether the given function name is an alias for the `:is()` function.
+  fn parse_any_prefix(&self, _name: &str) -> Option<<Self::Impl as SelectorImpl<'i>>::VendorPrefix> {
+    None
+  }
+
+  /// Whether to parse the `:host` pseudo-class.
+  fn parse_host(&self) -> bool {
+    false
+  }
+
+  /// Parses non-tree-structural pseudo-classes. Tree structural pseudo-classes,
+  /// like `:first-child`, are built into this library.
+  ///
+  /// This function can return an "Err" pseudo-element in order to support CSS2.1
+  /// pseudo-elements.
+  fn parse_non_ts_pseudo_class(
+    &self,
+    location: SourceLocation,
+    name: CowRcStr<'i>,
+  ) -> Result<<Self::Impl as SelectorImpl<'i>>::NonTSPseudoClass, ParseError<'i, Self::Error>> {
+    Err(location.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClass(name)))
+  }
+
+  fn parse_non_ts_functional_pseudo_class<'t>(
+    &self,
+    name: CowRcStr<'i>,
+    arguments: &mut CssParser<'i, 't>,
+  ) -> Result<<Self::Impl as SelectorImpl<'i>>::NonTSPseudoClass, ParseError<'i, Self::Error>> {
+    Err(arguments.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClass(name)))
+  }
+
+  fn parse_pseudo_element(
+    &self,
+    location: SourceLocation,
+    name: CowRcStr<'i>,
+  ) -> Result<<Self::Impl as SelectorImpl<'i>>::PseudoElement, ParseError<'i, Self::Error>> {
+    Err(location.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoElement(name)))
+  }
+
+  fn parse_functional_pseudo_element<'t>(
+    &self,
+    name: CowRcStr<'i>,
+    arguments: &mut CssParser<'i, 't>,
+  ) -> Result<<Self::Impl as SelectorImpl<'i>>::PseudoElement, ParseError<'i, Self::Error>> {
+    Err(arguments.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoElement(name)))
+  }
+
+  fn default_namespace(&self) -> Option<<Self::Impl as SelectorImpl<'i>>::NamespaceUrl> {
+    None
+  }
+
+  fn namespace_for_prefix(
+    &self,
+    _prefix: &<Self::Impl as SelectorImpl<'i>>::NamespacePrefix,
+  ) -> Option<<Self::Impl as SelectorImpl<'i>>::NamespaceUrl> {
+    None
+  }
+
+  fn is_nesting_allowed(&self) -> bool {
+    false
+  }
+
+  fn deep_combinator_enabled(&self) -> bool {
+    false
+  }
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Hash)]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(bound(
+    serialize = "Impl::NonTSPseudoClass: serde::Serialize, Impl::PseudoElement: serde::Serialize, Impl::VendorPrefix: serde::Serialize",
+    deserialize = "Impl::NonTSPseudoClass: serde::Deserialize<'de>, Impl::PseudoElement: serde::Deserialize<'de>, Impl::VendorPrefix: serde::Deserialize<'de>"
+  ))
+)]
+#[cfg_attr(
+  feature = "jsonschema",
+  derive(schemars::JsonSchema),
+  schemars(
+    rename = "SelectorList",
+    bound = "Impl: schemars::JsonSchema, Impl::NonTSPseudoClass: schemars::JsonSchema, Impl::PseudoElement: schemars::JsonSchema, Impl::VendorPrefix: schemars::JsonSchema"
+  )
+)]
+pub struct SelectorList<'i, Impl: SelectorImpl<'i>>(
+  #[cfg_attr(feature = "serde", serde(borrow))] pub SmallVec<[Selector<'i, Impl>; 1]>,
+);
+
+#[cfg(feature = "into_owned")]
+impl<'any, 'i, Impl: SelectorImpl<'i>, NewSel> static_self::IntoOwned<'any> for SelectorList<'i, Impl>
+where
+  Impl: static_self::IntoOwned<'any, Owned = NewSel>,
+  NewSel: SelectorImpl<'any>,
+  Component<'i, Impl>: static_self::IntoOwned<'any, Owned = Component<'any, NewSel>>,
+{
+  type Owned = SelectorList<'any, NewSel>;
+
+  fn into_owned(self) -> Self::Owned {
+    SelectorList(self.0.into_owned())
+  }
+}
+
+/// How to treat invalid selectors in a selector list.
+pub enum ParseErrorRecovery {
+  /// Discard the entire selector list, this is the default behavior for
+  /// almost all of CSS.
+  DiscardList,
+  /// Ignore invalid selectors, potentially creating an empty selector list.
+  ///
+  /// This is the error recovery mode of :is() and :where()
+  IgnoreInvalidSelector,
+}
+
+#[derive(Eq, PartialEq, Clone, Copy)]
+pub enum NestingRequirement {
+  None,
+  Prefixed,
+  Contained,
+  Implicit,
+}
+
+impl<'i, Impl: SelectorImpl<'i>> SelectorList<'i, Impl> {
+  /// Parse a comma-separated list of Selectors.
+  /// <https://drafts.csswg.org/selectors/#grouping>
+  ///
+  /// Return the Selectors or Err if there is an invalid selector.
+  pub fn parse<'t, P>(
+    parser: &P,
+    input: &mut CssParser<'i, 't>,
+    error_recovery: ParseErrorRecovery,
+    nesting_requirement: NestingRequirement,
+  ) -> Result<Self, ParseError<'i, P::Error>>
+  where
+    P: Parser<'i, Impl = Impl>,
+  {
+    Self::parse_with_state(
+      parser,
+      input,
+      &mut SelectorParsingState::empty(),
+      error_recovery,
+      nesting_requirement,
+    )
+  }
+
+  #[inline]
+  fn parse_with_state<'t, P>(
+    parser: &P,
+    input: &mut CssParser<'i, 't>,
+    state: &mut SelectorParsingState,
+    recovery: ParseErrorRecovery,
+    nesting_requirement: NestingRequirement,
+  ) -> Result<Self, ParseError<'i, P::Error>>
+  where
+    P: Parser<'i, Impl = Impl>,
+  {
+    let original_state = *state;
+    let mut values = SmallVec::new();
+    loop {
+      let selector = input.parse_until_before(Delimiter::Comma, |input| {
+        let mut selector_state = original_state;
+        let result = parse_selector(parser, input, &mut selector_state, nesting_requirement);
+        if selector_state.contains(SelectorParsingState::AFTER_NESTING) {
+          state.insert(SelectorParsingState::AFTER_NESTING)
+        }
+        result
+      });
+
+      let was_ok = selector.is_ok();
+      match selector {
+        Ok(selector) => values.push(selector),
+        Err(err) => match recovery {
+          ParseErrorRecovery::DiscardList => return Err(err),
+          ParseErrorRecovery::IgnoreInvalidSelector => {}
+        },
+      }
+
+      loop {
+        match input.next() {
+          Err(_) => return Ok(SelectorList(values)),
+          Ok(&Token::Comma) => break,
+          Ok(_) => {
+            debug_assert!(!was_ok, "Shouldn't have got a selector if getting here");
+          }
+        }
+      }
+    }
+  }
+
+  pub fn parse_relative<'t, P>(
+    parser: &P,
+    input: &mut CssParser<'i, 't>,
+    error_recovery: ParseErrorRecovery,
+    nesting_requirement: NestingRequirement,
+  ) -> Result<Self, ParseError<'i, P::Error>>
+  where
+    P: Parser<'i, Impl = Impl>,
+  {
+    Self::parse_relative_with_state(
+      parser,
+      input,
+      &mut SelectorParsingState::empty(),
+      error_recovery,
+      nesting_requirement,
+    )
+  }
+
+  #[inline]
+  fn parse_relative_with_state<'t, P>(
+    parser: &P,
+    input: &mut CssParser<'i, 't>,
+    state: &mut SelectorParsingState,
+    recovery: ParseErrorRecovery,
+    nesting_requirement: NestingRequirement,
+  ) -> Result<Self, ParseError<'i, P::Error>>
+  where
+    P: Parser<'i, Impl = Impl>,
+  {
+    let original_state = *state;
+    let mut values = SmallVec::new();
+    loop {
+      let selector = input.parse_until_before(Delimiter::Comma, |input| {
+        let mut selector_state = original_state;
+        let result = parse_relative_selector(parser, input, &mut selector_state, nesting_requirement);
+        if selector_state.contains(SelectorParsingState::AFTER_NESTING) {
+          state.insert(SelectorParsingState::AFTER_NESTING)
+        }
+        result
+      });
+
+      let was_ok = selector.is_ok();
+      match selector {
+        Ok(selector) => values.push(selector),
+        Err(err) => match recovery {
+          ParseErrorRecovery::DiscardList => return Err(err),
+          ParseErrorRecovery::IgnoreInvalidSelector => {}
+        },
+      }
+
+      loop {
+        match input.next() {
+          Err(_) => return Ok(SelectorList(values)),
+          Ok(&Token::Comma) => break,
+          Ok(_) => {
+            debug_assert!(!was_ok, "Shouldn't have got a selector if getting here");
+          }
+        }
+      }
+    }
+  }
+
+  /// Creates a new SelectorList.
+  pub fn new(v: SmallVec<[Selector<'i, Impl>; 1]>) -> Self {
+    SelectorList(v)
+  }
+
+  /// Creates a SelectorList from a Vec of selectors. Used in tests.
+  pub fn from_vec(v: Vec<Selector<'i, Impl>>) -> Self {
+    SelectorList(SmallVec::from_vec(v))
+  }
+}
+
+impl<'i, Impl: SelectorImpl<'i>> From<Selector<'i, Impl>> for SelectorList<'i, Impl> {
+  fn from(selector: Selector<'i, Impl>) -> Self {
+    SelectorList(smallvec![selector])
+  }
+}
+
+impl<'i, Impl: SelectorImpl<'i>> From<Component<'i, Impl>> for SelectorList<'i, Impl> {
+  fn from(component: Component<'i, Impl>) -> Self {
+    SelectorList::from(Selector::from(component))
+  }
+}
+
+/// Parses one compound selector suitable for nested stuff like :-moz-any, etc.
+fn parse_inner_compound_selector<'i, 't, P, Impl>(
+  parser: &P,
+  input: &mut CssParser<'i, 't>,
+  state: &mut SelectorParsingState,
+) -> Result<Selector<'i, Impl>, ParseError<'i, P::Error>>
+where
+  P: Parser<'i, Impl = Impl>,
+  Impl: SelectorImpl<'i>,
+{
+  let mut child_state =
+    *state | SelectorParsingState::DISALLOW_PSEUDOS | SelectorParsingState::DISALLOW_COMBINATORS;
+  let result = parse_selector(parser, input, &mut child_state, NestingRequirement::None)?;
+  if child_state.contains(SelectorParsingState::AFTER_NESTING) {
+    state.insert(SelectorParsingState::AFTER_NESTING)
+  }
+  Ok(result)
+}
+
+/// Ancestor hashes for the bloom filter. We precompute these and store them
+/// inline with selectors to optimize cache performance during matching.
+/// This matters a lot.
+///
+/// We use 4 hashes, which is copied from Gecko, who copied it from WebKit.
+/// Note that increasing the number of hashes here will adversely affect the
+/// cache hit when fast-rejecting long lists of Rules with inline hashes.
+///
+/// Because the bloom filter only uses the bottom 24 bits of the hash, we pack
+/// the fourth hash into the upper bits of the first three hashes in order to
+/// shrink Rule (whose size matters a lot). This scheme minimizes the runtime
+/// overhead of the packing for the first three hashes (we just need to mask
+/// off the upper bits) at the expense of making the fourth somewhat more
+/// complicated to assemble, because we often bail out before checking all the
+/// hashes.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct AncestorHashes {
+  pub packed_hashes: [u32; 3],
+}
+
+fn collect_ancestor_hashes<'i, Impl: SelectorImpl<'i>>(
+  iter: SelectorIter<'_, 'i, Impl>,
+  quirks_mode: QuirksMode,
+  hashes: &mut [u32; 4],
+  len: &mut usize,
+) -> bool
+where
+  Impl::Identifier: PrecomputedHash,
+  Impl::LocalName: PrecomputedHash,
+  Impl::NamespaceUrl: PrecomputedHash,
+{
+  for component in AncestorIter::new(iter) {
+    let hash = match *component {
+      Component::LocalName(LocalName {
+        ref name,
+        ref lower_name,
+      }) => {
+        // Only insert the local-name into the filter if it's all
+        // lowercase.  Otherwise we would need to test both hashes, and
+        // our data structures aren't really set up for that.
+        if name != lower_name {
+          continue;
+        }
+        name.precomputed_hash()
+      }
+      Component::DefaultNamespace(ref url) | Component::Namespace(_, ref url) => url.precomputed_hash(),
+      // In quirks mode, class and id selectors should match
+      // case-insensitively, so just avoid inserting them into the filter.
+      Component::ID(ref id) if quirks_mode != QuirksMode::Quirks => id.precomputed_hash(),
+      Component::Class(ref class) if quirks_mode != QuirksMode::Quirks => class.precomputed_hash(),
+      Component::Is(ref list) | Component::Where(ref list) => {
+        // :where and :is OR their selectors, so we can't put any hash
+        // in the filter if there's more than one selector, as that'd
+        // exclude elements that may match one of the other selectors.
+        if list.len() == 1 && !collect_ancestor_hashes(list[0].iter(), quirks_mode, hashes, len) {
+          return false;
+        }
+        continue;
+      }
+      _ => continue,
+    };
+
+    hashes[*len] = hash & BLOOM_HASH_MASK;
+    *len += 1;
+    if *len == hashes.len() {
+      return false;
+    }
+  }
+  true
+}
+
+impl AncestorHashes {
+  pub fn new<'i, Impl: SelectorImpl<'i>>(selector: &Selector<'i, Impl>, quirks_mode: QuirksMode) -> Self
+  where
+    Impl::Identifier: PrecomputedHash,
+    Impl::LocalName: PrecomputedHash,
+    Impl::NamespaceUrl: PrecomputedHash,
+  {
+    // Compute ancestor hashes for the bloom filter.
+    let mut hashes = [0u32; 4];
+    let mut len = 0;
+    collect_ancestor_hashes(selector.iter(), quirks_mode, &mut hashes, &mut len);
+    debug_assert!(len <= 4);
+
+    // Now, pack the fourth hash (if it exists) into the upper byte of each of
+    // the other three hashes.
+    if len == 4 {
+      let fourth = hashes[3];
+      hashes[0] |= (fourth & 0x000000ff) << 24;
+      hashes[1] |= (fourth & 0x0000ff00) << 16;
+      hashes[2] |= (fourth & 0x00ff0000) << 8;
+    }
+
+    AncestorHashes {
+      packed_hashes: [hashes[0], hashes[1], hashes[2]],
+    }
+  }
+
+  /// Returns the fourth hash, reassembled from parts.
+  pub fn fourth_hash(&self) -> u32 {
+    ((self.packed_hashes[0] & 0xff000000) >> 24)
+      | ((self.packed_hashes[1] & 0xff000000) >> 16)
+      | ((self.packed_hashes[2] & 0xff000000) >> 8)
+  }
+}
+
+pub fn namespace_empty_string<'i, Impl: SelectorImpl<'i>>() -> Impl::NamespaceUrl {
+  // Rust type’s default, not default namespace
+  Impl::NamespaceUrl::default()
+}
+
+/// A Selector stores a sequence of simple selectors and combinators. The
+/// iterator classes allow callers to iterate at either the raw sequence level or
+/// at the level of sequences of simple selectors separated by combinators. Most
+/// callers want the higher-level iterator.
+///
+/// We store compound selectors internally right-to-left (in matching order).
+/// Additionally, we invert the order of top-level compound selectors so that
+/// each one matches left-to-right. This is because matching namespace, local name,
+/// id, and class are all relatively cheap, whereas matching pseudo-classes might
+/// be expensive (depending on the pseudo-class). Since authors tend to put the
+/// pseudo-classes on the right, it's faster to start matching on the left.
+///
+/// This reordering doesn't change the semantics of selector matching, and we
+/// handle it in to_css to make it invisible to serialization.
+#[derive(Clone, PartialEq, Eq, Hash)]
+pub struct Selector<'i, Impl: SelectorImpl<'i>>(SpecificityAndFlags, Vec<Component<'i, Impl>>);
+
+#[cfg(feature = "into_owned")]
+impl<'any, 'i, Impl: SelectorImpl<'i>, NewSel> static_self::IntoOwned<'any> for Selector<'i, Impl>
+where
+  Impl: static_self::IntoOwned<'any, Owned = NewSel>,
+  NewSel: SelectorImpl<'any>,
+  Component<'i, Impl>: static_self::IntoOwned<'any, Owned = Component<'any, NewSel>>,
+{
+  type Owned = Selector<'any, NewSel>;
+
+  fn into_owned(self) -> Self::Owned {
+    Selector(self.0, self.1.into_owned())
+  }
+}
+
+impl<'i, Impl: SelectorImpl<'i>> Selector<'i, Impl> {
+  #[inline]
+  pub fn specificity(&self) -> u32 {
+    self.0.specificity()
+  }
+
+  #[inline]
+  pub fn has_pseudo_element(&self) -> bool {
+    self.0.has_pseudo_element()
+  }
+
+  #[inline]
+  pub fn is_slotted(&self) -> bool {
+    self.0.is_slotted()
+  }
+
+  #[inline]
+  pub fn is_part(&self) -> bool {
+    self.0.is_part()
+  }
+
+  #[inline]
+  pub fn append(&mut self, component: Component<'i, Impl>) {
+    let index = self
+      .1
+      .iter()
+      .position(|c| matches!(*c, Component::Combinator(..) | Component::PseudoElement(..)))
+      .unwrap_or(self.1.len());
+    self.1.insert(index, component);
+  }
+
+  #[inline]
+  pub fn parts(&self) -> Option<&[Impl::Identifier]> {
+    if !self.is_part() {
+      return None;
+    }
+
+    let mut iter = self.iter();
+    if self.has_pseudo_element() {
+      // Skip the pseudo-element.
+      for _ in &mut iter {}
+
+      let combinator = iter.next_sequence()?;
+      debug_assert_eq!(combinator, Combinator::PseudoElement);
+    }
+
+    for component in iter {
+      if let Component::Part(ref part) = *component {
+        return Some(part);
+      }
+    }
+
+    debug_assert!(false, "is_part() lied somehow?");
+    None
+  }
+
+  #[inline]
+  pub fn pseudo_element(&self) -> Option<&Impl::PseudoElement> {
+    if !self.has_pseudo_element() {
+      return None;
+    }
+
+    for component in self.iter() {
+      if let Component::PseudoElement(ref pseudo) = *component {
+        return Some(pseudo);
+      }
+    }
+
+    debug_assert!(false, "has_pseudo_element lied!");
+    None
+  }
+
+  /// Whether this selector (pseudo-element part excluded) matches every element.
+  ///
+  /// Used for "pre-computed" pseudo-elements in components/style/stylist.rs
+  #[inline]
+  pub fn is_universal(&self) -> bool {
+    self.iter_raw_match_order().all(|c| {
+      matches!(
+        *c,
+        Component::ExplicitUniversalType
+          | Component::ExplicitAnyNamespace
+          | Component::Combinator(Combinator::PseudoElement)
+          | Component::PseudoElement(..)
+      )
+    })
+  }
+
+  #[inline]
+  pub fn has_combinator(&self) -> bool {
+    self
+      .iter_raw_match_order()
+      .any(|c| matches!(*c, Component::Combinator(combinator) if combinator.is_tree_combinator()))
+  }
+
+  /// Returns an iterator over this selector in matching order (right-to-left).
+  /// When a combinator is reached, the iterator will return None, and
+  /// next_sequence() may be called to continue to the next sequence.
+  #[inline]
+  pub fn iter(&self) -> SelectorIter<'_, 'i, Impl> {
+    SelectorIter {
+      iter: self.iter_raw_match_order(),
+      next_combinator: None,
+    }
+  }
+
+  /// Whether this selector is a featureless :host selector, with no
+  /// combinators to the left, and optionally has a pseudo-element to the
+  /// right.
+  #[inline]
+  pub fn is_featureless_host_selector_or_pseudo_element(&self) -> bool {
+    let mut iter = self.iter();
+    if !self.has_pseudo_element() {
+      return iter.is_featureless_host_selector();
+    }
+
+    // Skip the pseudo-element.
+    for _ in &mut iter {}
+
+    match iter.next_sequence() {
+      None => return false,
+      Some(combinator) => {
+        debug_assert_eq!(combinator, Combinator::PseudoElement);
+      }
+    }
+
+    iter.is_featureless_host_selector()
+  }
+
+  /// Returns an iterator over this selector in matching order (right-to-left),
+  /// skipping the rightmost |offset| Components.
+  #[inline]
+  pub fn iter_from(&self, offset: usize) -> SelectorIter<'_, 'i, Impl> {
+    let iter = self.1[offset..].iter();
+    SelectorIter {
+      iter,
+      next_combinator: None,
+    }
+  }
+
+  /// Returns the combinator at index `index` (zero-indexed from the right),
+  /// or panics if the component is not a combinator.
+  #[inline]
+  pub fn combinator_at_match_order(&self, index: usize) -> Combinator {
+    match self.1[index] {
+      Component::Combinator(c) => c,
+      ref other => panic!("Not a combinator: {:?}, {:?}, index: {}", other, self, index),
+    }
+  }
+
+  /// Returns an iterator over the entire sequence of simple selectors and
+  /// combinators, in matching order (from right to left).
+  #[inline]
+  pub fn iter_raw_match_order(&self) -> slice::Iter<Component<'i, Impl>> {
+    self.1.iter()
+  }
+
+  #[inline]
+  pub fn iter_mut_raw_match_order(&mut self) -> slice::IterMut<Component<'i, Impl>> {
+    self.1.iter_mut()
+  }
+
+  /// Returns the combinator at index `index` (zero-indexed from the left),
+  /// or panics if the component is not a combinator.
+  #[inline]
+  pub fn combinator_at_parse_order(&self, index: usize) -> Combinator {
+    match self.1[self.len() - index - 1] {
+      Component::Combinator(c) => c,
+      ref other => panic!("Not a combinator: {:?}, {:?}, index: {}", other, self, index),
+    }
+  }
+
+  /// Returns an iterator over the sequence of simple selectors and
+  /// combinators, in parse order (from left to right), starting from
+  /// `offset`.
+  #[inline]
+  pub fn iter_raw_parse_order_from(&self, offset: usize) -> Rev<slice::Iter<Component<'i, Impl>>> {
+    self.1[..self.len() - offset].iter().rev()
+  }
+
+  /// Creates a Selector from a vec of Components, specified in parse order. Used in tests.
+  #[allow(unused)]
+  pub(crate) fn from_vec(vec: Vec<Component<'i, Impl>>, specificity: u32, flags: SelectorFlags) -> Self {
+    let mut builder = SelectorBuilder::default();
+    for component in vec.into_iter() {
+      if let Some(combinator) = component.as_combinator() {
+        builder.push_combinator(combinator);
+      } else {
+        builder.push_simple_selector(component);
+      }
+    }
+    let spec = SpecificityAndFlags { specificity, flags };
+    let (spec, components) = builder.build_with_specificity_and_flags(spec);
+    Selector(spec, components)
+  }
+
+  #[cfg(feature = "serde")]
+  #[inline]
+  pub(crate) fn new(spec: SpecificityAndFlags, components: Vec<Component<'i, Impl>>) -> Self {
+    Selector(spec, components)
+  }
+
+  /// Returns count of simple selectors and combinators in the Selector.
+  #[inline]
+  pub fn len(&self) -> usize {
+    self.1.len()
+  }
+
+  /// Traverse selector components inside `self`.
+  ///
+  /// Implementations of this method should call `SelectorVisitor` methods
+  /// or other impls of `Visit` as appropriate based on the fields of `Self`.
+  ///
+  /// A return value of `false` indicates terminating the traversal.
+  /// It should be propagated with an early return.
+  /// On the contrary, `true` indicates that all fields of `self` have been traversed:
+  ///
+  /// ```rust,ignore
+  /// if !visitor.visit_simple_selector(&self.some_simple_selector) {
+  ///     return false;
+  /// }
+  /// if !self.some_component.visit(visitor) {
+  ///     return false;
+  /// }
+  /// true
+  /// ```
+  pub fn visit<V>(&self, visitor: &mut V) -> bool
+  where
+    V: SelectorVisitor<'i, Impl = Impl>,
+  {
+    let mut current = self.iter();
+    let mut combinator = None;
+    loop {
+      if !visitor.visit_complex_selector(combinator) {
+        return false;
+      }
+
+      for selector in &mut current {
+        if !selector.visit(visitor) {
+          return false;
+        }
+      }
+
+      combinator = current.next_sequence();
+      if combinator.is_none() {
+        break;
+      }
+    }
+
+    true
+  }
+}
+
+impl<'i, Impl: SelectorImpl<'i>> From<Component<'i, Impl>> for Selector<'i, Impl> {
+  fn from(component: Component<'i, Impl>) -> Self {
+    let mut builder = SelectorBuilder::default();
+    if let Some(combinator) = component.as_combinator() {
+      builder.push_combinator(combinator);
+    } else {
+      builder.push_simple_selector(component);
+    }
+    let (spec, components) = builder.build(false, false, false);
+    Selector(spec, components)
+  }
+}
+
+impl<'i, Impl: SelectorImpl<'i>> From<Vec<Component<'i, Impl>>> for Selector<'i, Impl> {
+  fn from(vec: Vec<Component<'i, Impl>>) -> Self {
+    let mut builder = SelectorBuilder::default();
+    for component in vec.into_iter() {
+      if let Some(combinator) = component.as_combinator() {
+        builder.push_combinator(combinator);
+      } else {
+        builder.push_simple_selector(component);
+      }
+    }
+    let (spec, components) = builder.build(false, false, false);
+    Selector(spec, components)
+  }
+}
+
+#[derive(Clone)]
+pub struct SelectorIter<'a, 'i, Impl: SelectorImpl<'i>> {
+  iter: slice::Iter<'a, Component<'i, Impl>>,
+  next_combinator: Option<Combinator>,
+}
+
+impl<'a, 'i, Impl: 'a + SelectorImpl<'i>> SelectorIter<'a, 'i, Impl> {
+  /// Prepares this iterator to point to the next sequence to the left,
+  /// returning the combinator if the sequence was found.
+  #[inline]
+  pub fn next_sequence(&mut self) -> Option<Combinator> {
+    self.next_combinator.take()
+  }
+
+  /// Whether this selector is a featureless host selector, with no
+  /// combinators to the left.
+  #[inline]
+  pub(crate) fn is_featureless_host_selector(&mut self) -> bool {
+    self.selector_length() > 0
+      && self.all(|component| matches!(*component, Component::Host(..)))
+      && self.next_sequence().is_none()
+  }
+
+  #[inline]
+  pub(crate) fn matches_for_stateless_pseudo_element(&mut self) -> bool {
+    let first = match self.next() {
+      Some(c) => c,
+      // Note that this is the common path that we keep inline: the
+      // pseudo-element not having anything to its right.
+      None => return true,
+    };
+    self.matches_for_stateless_pseudo_element_internal(first)
+  }
+
+  #[inline(never)]
+  fn matches_for_stateless_pseudo_element_internal(&mut self, first: &Component<'i, Impl>) -> bool {
+    if !first.matches_for_stateless_pseudo_element() {
+      return false;
+    }
+    for component in self {
+      // The only other parser-allowed Components in this sequence are
+      // state pseudo-classes, or one of the other things that can contain
+      // them.
+      if !component.matches_for_stateless_pseudo_element() {
+        return false;
+      }
+    }
+    true
+  }
+
+  /// Returns remaining count of the simple selectors and combinators in the Selector.
+  #[inline]
+  pub fn selector_length(&self) -> usize {
+    self.iter.len()
+  }
+}
+
+impl<'a, 'i, Impl: SelectorImpl<'i>> Iterator for SelectorIter<'a, 'i, Impl> {
+  type Item = &'a Component<'i, Impl>;
+
+  #[inline]
+  fn next(&mut self) -> Option<Self::Item> {
+    debug_assert!(self.next_combinator.is_none(), "You should call next_sequence!");
+    match *self.iter.next()? {
+      Component::Combinator(c) => {
+        self.next_combinator = Some(c);
+        None
+      }
+      ref x => Some(x),
+    }
+  }
+}
+
+impl<'a, 'i, Impl: SelectorImpl<'i>> fmt::Debug for SelectorIter<'a, 'i, Impl> {
+  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+    let iter = self.iter.clone().rev();
+    for component in iter {
+      component.to_css(f)?
+    }
+    Ok(())
+  }
+}
+
+/// An iterator over all simple selectors belonging to ancestors.
+struct AncestorIter<'a, 'i, Impl: SelectorImpl<'i>>(SelectorIter<'a, 'i, Impl>);
+impl<'a, 'i, Impl: 'a + SelectorImpl<'i>> AncestorIter<'a, 'i, Impl> {
+  /// Creates an AncestorIter. The passed-in iterator is assumed to point to
+  /// the beginning of the child sequence, which will be skipped.
+  fn new(inner: SelectorIter<'a, 'i, Impl>) -> Self {
+    let mut result = AncestorIter(inner);
+    result.skip_until_ancestor();
+    result
+  }
+
+  /// Skips a sequence of simple selectors and all subsequent sequences until
+  /// a non-pseudo-element ancestor combinator is reached.
+  fn skip_until_ancestor(&mut self) {
+    loop {
+      while self.0.next().is_some() {}
+      // If this is ever changed to stop at the "pseudo-element"
+      // combinator, we will need to fix the way we compute hashes for
+      // revalidation selectors.
+      if self
+        .0
+        .next_sequence()
+        .map_or(true, |x| matches!(x, Combinator::Child | Combinator::Descendant))
+      {
+        break;
+      }
+    }
+  }
+}
+
+impl<'a, 'i, Impl: SelectorImpl<'i>> Iterator for AncestorIter<'a, 'i, Impl> {
+  type Item = &'a Component<'i, Impl>;
+  fn next(&mut self) -> Option<Self::Item> {
+    // Grab the next simple selector in the sequence if available.
+    let next = self.0.next();
+    if next.is_some() {
+      return next;
+    }
+
+    // See if there are more sequences. If so, skip any non-ancestor sequences.
+    if let Some(combinator) = self.0.next_sequence() {
+      if !matches!(combinator, Combinator::Child | Combinator::Descendant) {
+        self.skip_until_ancestor();
+      }
+    }
+
+    self.0.next()
+  }
+}
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum Combinator {
+  Child,        //  >
+  Descendant,   // space
+  NextSibling,  // +
+  LaterSibling, // ~
+  /// A dummy combinator we use to the left of pseudo-elements.
+  ///
+  /// It serializes as the empty string, and acts effectively as a child
+  /// combinator in most cases.  If we ever actually start using a child
+  /// combinator for this, we will need to fix up the way hashes are computed
+  /// for revalidation selectors.
+  PseudoElement,
+  /// Another combinator used for ::slotted(), which represent the jump from
+  /// a node to its assigned slot.
+  SlotAssignment,
+  /// Another combinator used for `::part()`, which represents the jump from
+  /// the part to the containing shadow host.
+  Part,
+
+  /// Non-standard Vue >>> combinator.
+  /// https://vue-loader.vuejs.org/guide/scoped-css.html#deep-selectors
+  DeepDescendant,
+  /// Non-standard /deep/ combinator.
+  /// Appeared in early versions of the css-scoping-1 specification:
+  /// https://www.w3.org/TR/2014/WD-css-scoping-1-20140403/#deep-combinator
+  /// And still supported as an alias for >>> by Vue.
+  Deep,
+}
+
+impl Combinator {
+  /// Returns true if this combinator is a child or descendant combinator.
+  #[inline]
+  pub fn is_ancestor(&self) -> bool {
+    matches!(
+      *self,
+      Combinator::Child | Combinator::Descendant | Combinator::PseudoElement | Combinator::SlotAssignment
+    )
+  }
+
+  /// Returns true if this combinator is a pseudo-element combinator.
+  #[inline]
+  pub fn is_pseudo_element(&self) -> bool {
+    matches!(*self, Combinator::PseudoElement)
+  }
+
+  /// Returns true if this combinator is a next- or later-sibling combinator.
+  #[inline]
+  pub fn is_sibling(&self) -> bool {
+    matches!(*self, Combinator::NextSibling | Combinator::LaterSibling)
+  }
+
+  #[inline]
+  pub fn is_tree_combinator(&self) -> bool {
+    matches!(
+      *self,
+      Combinator::Child | Combinator::Descendant | Combinator::NextSibling | Combinator::LaterSibling
+    )
+  }
+}
+
+/// An enum for the different types of :nth- pseudoclasses
+#[derive(Copy, Clone, Eq, PartialEq, Hash)]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum NthType {
+  Child,
+  LastChild,
+  OnlyChild,
+  OfType,
+  LastOfType,
+  OnlyOfType,
+  Col,
+  LastCol,
+}
+
+impl NthType {
+  pub fn is_only(self) -> bool {
+    self == Self::OnlyChild || self == Self::OnlyOfType
+  }
+
+  pub fn is_of_type(self) -> bool {
+    self == Self::OfType || self == Self::LastOfType || self == Self::OnlyOfType
+  }
+
+  pub fn is_from_end(self) -> bool {
+    self == Self::LastChild || self == Self::LastOfType || self == Self::LastCol
+  }
+
+  pub fn allows_of_selector(self) -> bool {
+    self == Self::Child || self == Self::LastChild
+  }
+}
+
+/// The properties that comprise an :nth- pseudoclass as of Selectors 3 (e.g.,
+/// nth-child(An+B)).
+/// https://www.w3.org/TR/selectors-3/#nth-child-pseudo
+#[derive(Copy, Clone, Eq, PartialEq, Hash)]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub struct NthSelectorData {
+  pub ty: NthType,
+  pub is_function: bool,
+  pub a: i32,
+  pub b: i32,
+}
+
+impl NthSelectorData {
+  /// Returns selector data for :only-{child,of-type}
+  #[inline]
+  pub const fn only(of_type: bool) -> Self {
+    Self {
+      ty: if of_type {
+        NthType::OnlyOfType
+      } else {
+        NthType::OnlyChild
+      },
+      is_function: false,
+      a: 0,
+      b: 1,
+    }
+  }
+
+  /// Returns selector data for :first-{child,of-type}
+  #[inline]
+  pub const fn first(of_type: bool) -> Self {
+    Self {
+      ty: if of_type { NthType::OfType } else { NthType::Child },
+      is_function: false,
+      a: 0,
+      b: 1,
+    }
+  }
+
+  /// Returns selector data for :last-{child,of-type}
+  #[inline]
+  pub const fn last(of_type: bool) -> Self {
+    Self {
+      ty: if of_type {
+        NthType::LastOfType
+      } else {
+        NthType::LastChild
+      },
+      is_function: false,
+      a: 0,
+      b: 1,
+    }
+  }
+
+  #[inline]
+  pub fn is_function(&self) -> bool {
+    self.a != 0 || self.b != 1
+  }
+
+  /// Writes the beginning of the selector.
+  #[inline]
+  pub fn write_start<W: fmt::Write>(&self, dest: &mut W, is_function: bool) -> fmt::Result {
+    dest.write_str(match self.ty {
+      NthType::Child if is_function => ":nth-child(",
+      NthType::Child => ":first-child",
+      NthType::LastChild if is_function => ":nth-last-child(",
+      NthType::LastChild => ":last-child",
+      NthType::OfType if is_function => ":nth-of-type(",
+      NthType::OfType => ":first-of-type",
+      NthType::LastOfType if is_function => ":nth-last-of-type(",
+      NthType::LastOfType => ":last-of-type",
+      NthType::OnlyChild => ":only-child",
+      NthType::OnlyOfType => ":only-of-type",
+      NthType::Col => ":nth-col(",
+      NthType::LastCol => ":nth-last-col(",
+    })
+  }
+
+  /// Serialize <an+b> (part of the CSS Syntax spec, but currently only used here).
+  /// <https://drafts.csswg.org/css-syntax-3/#serialize-an-anb-value>
+  #[inline]
+  pub fn write_affine<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result {
+    match (self.a, self.b) {
+      (0, 0) => dest.write_char('0'),
+
+      (1, 0) => dest.write_char('n'),
+      (-1, 0) => dest.write_str("-n"),
+      (_, 0) => write!(dest, "{}n", self.a),
+
+      (2, 1) => dest.write_str("odd"),
+
+      (0, _) => write!(dest, "{}", self.b),
+      (1, _) => write!(dest, "n{:+}", self.b),
+      (-1, _) => write!(dest, "-n{:+}", self.b),
+      (_, _) => write!(dest, "{}n{:+}", self.a, self.b),
+    }
+  }
+}
+
+/// The properties that comprise an :nth- pseudoclass as of Selectors 4 (e.g.,
+/// nth-child(An+B [of S]?)).
+/// https://www.w3.org/TR/selectors-4/#nth-child-pseudo
+#[derive(Clone, PartialEq, Eq, Hash)]
+pub struct NthOfSelectorData<'i, Impl: SelectorImpl<'i>>(NthSelectorData, Box<[Selector<'i, Impl>]>);
+
+#[cfg(feature = "into_owned")]
+impl<'any, 'i, Impl: SelectorImpl<'i>, NewSel> static_self::IntoOwned<'any> for NthOfSelectorData<'i, Impl>
+where
+  Impl: static_self::IntoOwned<'any, Owned = NewSel>,
+  NewSel: SelectorImpl<'any>,
+  Component<'i, Impl>: static_self::IntoOwned<'any, Owned = Component<'any, NewSel>>,
+{
+  type Owned = NthOfSelectorData<'any, NewSel>;
+
+  fn into_owned(self) -> Self::Owned {
+    NthOfSelectorData(self.0, self.1.into_owned())
+  }
+}
+
+impl<'i, Impl: SelectorImpl<'i>> NthOfSelectorData<'i, Impl> {
+  /// Returns selector data for :nth-{,last-}{child,of-type}(An+B [of S])
+  #[inline]
+  pub fn new(nth_data: NthSelectorData, selectors: Box<[Selector<'i, Impl>]>) -> Self {
+    Self(nth_data, selectors)
+  }
+
+  /// Returns the An+B part of the selector
+  #[inline]
+  pub fn nth_data(&self) -> &NthSelectorData {
+    &self.0
+  }
+
+  /// Returns the selector list part of the selector
+  #[inline]
+  pub fn selectors(&self) -> &[Selector<'i, Impl>] {
+    &*self.1
+  }
+
+  pub fn clone_selectors(&self) -> Box<[Selector<'i, Impl>]> {
+    self.1.clone()
+  }
+}
+
+/// A CSS simple selector or combinator. We store both in the same enum for
+/// optimal packing and cache performance, see [1].
+///
+/// [1] https://bugzilla.mozilla.org/show_bug.cgi?id=1357973
+#[derive(Clone, PartialEq, Eq, Hash)]
+pub enum Component<'i, Impl: SelectorImpl<'i>> {
+  Combinator(Combinator),
+
+  ExplicitAnyNamespace,
+  ExplicitNoNamespace,
+  DefaultNamespace(Impl::NamespaceUrl),
+  Namespace(Impl::NamespacePrefix, Impl::NamespaceUrl),
+
+  ExplicitUniversalType,
+  LocalName(LocalName<'i, Impl>),
+
+  ID(Impl::Identifier),
+  Class(Impl::Identifier),
+
+  AttributeInNoNamespaceExists {
+    local_name: Impl::LocalName,
+    local_name_lower: Impl::LocalName,
+  },
+  // Used only when local_name is already lowercase.
+  AttributeInNoNamespace {
+    local_name: Impl::LocalName,
+    operator: AttrSelectorOperator,
+    value: Impl::AttrValue,
+    case_sensitivity: ParsedCaseSensitivity,
+    never_matches: bool,
+  },
+  // Use a Box in the less common cases with more data to keep size_of::<Component>() small.
+  AttributeOther(Box<AttrSelectorWithOptionalNamespace<'i, Impl>>),
+
+  /// Pseudo-classes
+  Negation(Box<[Selector<'i, Impl>]>),
+  Root,
+  Empty,
+  Scope,
+  Nth(NthSelectorData),
+  NthOf(NthOfSelectorData<'i, Impl>),
+  NonTSPseudoClass(Impl::NonTSPseudoClass),
+  /// The ::slotted() pseudo-element:
+  ///
+  /// https://drafts.csswg.org/css-scoping/#slotted-pseudo
+  ///
+  /// The selector here is a compound selector, that is, no combinators.
+  ///
+  /// NOTE(emilio): This should support a list of selectors, but as of this
+  /// writing no other browser does, and that allows them to put ::slotted()
+  /// in the rule hash, so we do that too.
+  ///
+  /// See https://github.com/w3c/csswg-drafts/issues/2158
+  Slotted(Selector<'i, Impl>),
+  /// The `::part` pseudo-element.
+  ///   https://drafts.csswg.org/css-shadow-parts/#part
+  Part(Box<[Impl::Identifier]>),
+  /// The `:host` pseudo-class:
+  ///
+  /// https://drafts.csswg.org/css-scoping/#host-selector
+  ///
+  /// NOTE(emilio): This should support a list of selectors, but as of this
+  /// writing no other browser does, and that allows them to put :host()
+  /// in the rule hash, so we do that too.
+  ///
+  /// See https://github.com/w3c/csswg-drafts/issues/2158
+  Host(Option<Selector<'i, Impl>>),
+  /// The `:where` pseudo-class.
+  ///
+  /// https://drafts.csswg.org/selectors/#zero-matches
+  ///
+  /// The inner argument is conceptually a SelectorList, but we move the
+  /// selectors to the heap to keep Component small.
+  Where(Box<[Selector<'i, Impl>]>),
+  /// The `:is` pseudo-class.
+  ///
+  /// https://drafts.csswg.org/selectors/#matches-pseudo
+  ///
+  /// Same comment as above re. the argument.
+  Is(Box<[Selector<'i, Impl>]>),
+  Any(Impl::VendorPrefix, Box<[Selector<'i, Impl>]>),
+  /// The `:has` pseudo-class.
+  ///
+  /// https://www.w3.org/TR/selectors/#relational
+  Has(Box<[Selector<'i, Impl>]>),
+  /// An implementation-dependent pseudo-element selector.
+  PseudoElement(Impl::PseudoElement),
+  /// A nesting selector:
+  ///
+  /// https://drafts.csswg.org/css-nesting-1/#nest-selector
+  ///
+  /// NOTE: This is a lightningcss addition.
+  Nesting,
+}
+
+#[cfg(feature = "into_owned")]
+impl<'any, 'i, Impl: SelectorImpl<'i>, NewSel> static_self::IntoOwned<'any> for Component<'i, Impl>
+where
+  Impl: static_self::IntoOwned<'any, Owned = NewSel>,
+  NewSel: SelectorImpl<'any>,
+  Impl::NamespaceUrl: static_self::IntoOwned<'any, Owned = NewSel::NamespaceUrl>,
+  Impl::NamespacePrefix: static_self::IntoOwned<'any, Owned = NewSel::NamespacePrefix>,
+  Impl::Identifier: static_self::IntoOwned<'any, Owned = NewSel::Identifier>,
+  Impl::LocalName: static_self::IntoOwned<'any, Owned = NewSel::LocalName>,
+  Impl::AttrValue: static_self::IntoOwned<'any, Owned = NewSel::AttrValue>,
+  Impl::NonTSPseudoClass: static_self::IntoOwned<'any, Owned = NewSel::NonTSPseudoClass>,
+  Impl::PseudoElement: static_self::IntoOwned<'any, Owned = NewSel::PseudoElement>,
+  Impl::VendorPrefix: static_self::IntoOwned<'any, Owned = NewSel::VendorPrefix>,
+{
+  type Owned = Component<'any, NewSel>;
+
+  fn into_owned(self) -> Self::Owned {
+    match self {
+      Component::Combinator(c) => Component::Combinator(c.into_owned()),
+      Component::ExplicitAnyNamespace => Component::ExplicitAnyNamespace,
+      Component::ExplicitNoNamespace => Component::ExplicitNoNamespace,
+      Component::DefaultNamespace(c) => Component::DefaultNamespace(c.into_owned()),
+      Component::Namespace(a, b) => Component::Namespace(a.into_owned(), b.into_owned()),
+      Component::ExplicitUniversalType => Component::ExplicitUniversalType,
+      Component::LocalName(c) => Component::LocalName(c.into_owned()),
+      Component::ID(c) => Component::ID(c.into_owned()),
+      Component::Class(c) => Component::Class(c.into_owned()),
+      Component::AttributeInNoNamespaceExists {
+        local_name,
+        local_name_lower,
+      } => Component::AttributeInNoNamespaceExists {
+        local_name: local_name.into_owned(),
+        local_name_lower: local_name_lower.into_owned(),
+      },
+      Component::AttributeInNoNamespace {
+        local_name,
+        operator,
+        value,
+        case_sensitivity,
+        never_matches,
+      } => {
+        let value = value.into_owned();
+        Component::AttributeInNoNamespace {
+          local_name: local_name.into_owned(),
+          operator,
+          value,
+          case_sensitivity,
+          never_matches,
+        }
+      }
+      Component::AttributeOther(c) => Component::AttributeOther(c.into_owned()),
+      Component::Negation(c) => Component::Negation(c.into_owned()),
+      Component::Root => Component::Root,
+      Component::Empty => Component::Empty,
+      Component::Scope => Component::Scope,
+      Component::Nth(c) => Component::Nth(c.into_owned()),
+      Component::NthOf(c) => Component::NthOf(c.into_owned()),
+      Component::NonTSPseudoClass(c) => Component::NonTSPseudoClass(c.into_owned()),
+      Component::Slotted(c) => Component::Slotted(c.into_owned()),
+      Component::Part(c) => Component::Part(c.into_owned()),
+      Component::Host(c) => Component::Host(c.into_owned()),
+      Component::Where(c) => Component::Where(c.into_owned()),
+      Component::Is(c) => Component::Is(c.into_owned()),
+      Component::Any(a, b) => Component::Any(a.into_owned(), b.into_owned()),
+      Component::Has(c) => Component::Has(c.into_owned()),
+      Component::PseudoElement(c) => Component::PseudoElement(c.into_owned()),
+      Component::Nesting => Component::Nesting,
+    }
+  }
+}
+
+impl<'i, Impl: SelectorImpl<'i>> Component<'i, Impl> {
+  /// Returns true if this is a combinator.
+  pub fn is_combinator(&self) -> bool {
+    matches!(*self, Component::Combinator(_))
+  }
+
+  /// Returns the value as a combinator if applicable, None otherwise.
+  pub fn as_combinator(&self) -> Option<Combinator> {
+    match *self {
+      Component::Combinator(c) => Some(c),
+      _ => None,
+    }
+  }
+
+  /// Whether this component is valid after a pseudo-element. Only intended
+  /// for sanity-checking.
+  pub fn maybe_allowed_after_pseudo_element(&self) -> bool {
+    match *self {
+      Component::NonTSPseudoClass(..) => true,
+      Component::Negation(ref selectors) | Component::Is(ref selectors) | Component::Where(ref selectors) => {
+        selectors
+          .iter()
+          .all(|selector| selector.iter_raw_match_order().all(|c| c.maybe_allowed_after_pseudo_element()))
+      }
+      _ => false,
+    }
+  }
+
+  /// Whether a given selector should match for stateless pseudo-elements.
+  ///
+  /// This is a bit subtle: Only selectors that return true in
+  /// `maybe_allowed_after_pseudo_element` should end up here, and
+  /// `NonTSPseudoClass` never matches (as it is a stateless pseudo after
+  /// all).
+  fn matches_for_stateless_pseudo_element(&self) -> bool {
+    debug_assert!(
+      self.maybe_allowed_after_pseudo_element(),
+      "Someone messed up pseudo-element parsing: {:?}",
+      *self
+    );
+    match *self {
+      Component::Negation(ref selectors) => !selectors.iter().all(|selector| {
+        selector
+          .iter_raw_match_order()
+          .all(|c| c.matches_for_stateless_pseudo_element())
+      }),
+      Component::Is(ref selectors) | Component::Where(ref selectors) => selectors.iter().any(|selector| {
+        selector
+          .iter_raw_match_order()
+          .all(|c| c.matches_for_stateless_pseudo_element())
+      }),
+      _ => false,
+    }
+  }
+
+  pub fn visit<V>(&self, visitor: &mut V) -> bool
+  where
+    V: SelectorVisitor<'i, Impl = Impl>,
+  {
+    use self::Component::*;
+    if !visitor.visit_simple_selector(self) {
+      return false;
+    }
+
+    match *self {
+      Slotted(ref selector) => {
+        if !selector.visit(visitor) {
+          return false;
+        }
+      }
+      Host(Some(ref selector)) => {
+        if !selector.visit(visitor) {
+          return false;
+        }
+      }
+      AttributeInNoNamespaceExists {
+        ref local_name,
+        ref local_name_lower,
+      } => {
+        if !visitor.visit_attribute_selector(
+          &NamespaceConstraint::Specific(&namespace_empty_string::<Impl>()),
+          local_name,
+          local_name_lower,
+        ) {
+          return false;
+        }
+      }
+      AttributeInNoNamespace {
+        ref local_name,
+        never_matches,
+        ..
+      } if !never_matches => {
+        if !visitor.visit_attribute_selector(
+          &NamespaceConstraint::Specific(&namespace_empty_string::<Impl>()),
+          local_name,
+          local_name,
+        ) {
+          return false;
+        }
+      }
+      AttributeOther(ref attr_selector) if !attr_selector.never_matches => {
+        let empty_string;
+        let namespace = match attr_selector.namespace() {
+          Some(ns) => ns,
+          None => {
+            empty_string = crate::parser::namespace_empty_string::<Impl>();
+            NamespaceConstraint::Specific(&empty_string)
+          }
+        };
+        if !visitor.visit_attribute_selector(
+          &namespace,
+          &attr_selector.local_name,
+          &attr_selector.local_name_lower,
+        ) {
+          return false;
+        }
+      }
+
+      NonTSPseudoClass(ref pseudo_class) => {
+        if !pseudo_class.visit(visitor) {
+          return false;
+        }
+      }
+
+      Negation(ref list) | Is(ref list) | Where(ref list) => {
+        if !visitor.visit_selector_list(&list) {
+          return false;
+        }
+      }
+      NthOf(ref nth_of_data) => {
+        if !visitor.visit_selector_list(nth_of_data.selectors()) {
+          return false;
+        }
+      }
+      _ => {}
+    }
+
+    true
+  }
+}
+
+#[derive(Clone, Eq, PartialEq, Hash)]
+pub struct LocalName<'i, Impl: SelectorImpl<'i>> {
+  pub name: Impl::LocalName,
+  pub lower_name: Impl::LocalName,
+}
+
+#[cfg(feature = "into_owned")]
+impl<'any, 'i, Impl: SelectorImpl<'i>, NewSel> static_self::IntoOwned<'any> for LocalName<'i, Impl>
+where
+  Impl: static_self::IntoOwned<'any, Owned = NewSel>,
+  NewSel: SelectorImpl<'any>,
+  Impl::LocalName: static_self::IntoOwned<'any, Owned = NewSel::LocalName>,
+{
+  type Owned = LocalName<'any, NewSel>;
+
+  fn into_owned(self) -> Self::Owned {
+    LocalName {
+      name: self.name.into_owned(),
+      lower_name: self.lower_name.into_owned(),
+    }
+  }
+}
+
+impl<'i, Impl: SelectorImpl<'i>> Debug for Selector<'i, Impl> {
+  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+    f.write_str("Selector(")?;
+    self.to_css(f)?;
+    write!(f, ", specificity = 0x{:x})", self.specificity())
+  }
+}
+
+impl<'i, Impl: SelectorImpl<'i>> Debug for Component<'i, Impl> {
+  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+    self.to_css(f)
+  }
+}
+impl<'i, Impl: SelectorImpl<'i>> Debug for AttrSelectorWithOptionalNamespace<'i, Impl> {
+  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+    self.to_css(f)
+  }
+}
+impl<'i, Impl: SelectorImpl<'i>> Debug for LocalName<'i, Impl> {
+  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+    self.to_css(f)
+  }
+}
+
+#[cfg(feature = "serde")]
+impl<'i, Impl: SelectorImpl<'i>> serde::Serialize for LocalName<'i, Impl>
+where
+  Impl::LocalName: serde::Serialize,
+{
+  fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+  where
+    S: serde::Serializer,
+  {
+    self.name.serialize(serializer)
+  }
+}
+
+#[cfg(feature = "serde")]
+impl<'i, 'de: 'i, Impl: SelectorImpl<'i>> serde::Deserialize<'de> for LocalName<'i, Impl>
+where
+  Impl::LocalName: serde::Deserialize<'de>,
+{
+  fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+  where
+    D: serde::Deserializer<'de>,
+  {
+    let name = Impl::LocalName::deserialize(deserializer)?;
+    let lower_name = to_ascii_lowercase(name.as_ref().to_string().into()).into();
+    Ok(LocalName { name, lower_name })
+  }
+}
+
+fn serialize_selector_list<'a, 'i: 'a, Impl, I, W>(iter: I, dest: &mut W) -> fmt::Result
+where
+  Impl: SelectorImpl<'i>,
+  I: Iterator<Item = &'a Selector<'i, Impl>>,
+  W: fmt::Write,
+{
+  let mut first = true;
+  for selector in iter {
+    if !first {
+      dest.write_str(", ")?;
+    }
+    first = false;
+    selector.to_css(dest)?;
+  }
+  Ok(())
+}
+
+impl<'i, Impl: SelectorImpl<'i>> ToCss for SelectorList<'i, Impl> {
+  fn to_css<W>(&self, dest: &mut W) -> fmt::Result
+  where
+    W: fmt::Write,
+  {
+    serialize_selector_list(self.0.iter(), dest)
+  }
+}
+
+impl<'i, Impl: SelectorImpl<'i>> fmt::Display for SelectorList<'i, Impl> {
+  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> fmt::Result {
+    Impl::to_css(self, f)
+  }
+}
+
+impl<'i, Impl: SelectorImpl<'i>> ToCss for Selector<'i, Impl> {
+  fn to_css<W>(&self, dest: &mut W) -> fmt::Result
+  where
+    W: fmt::Write,
+  {
+    // Compound selectors invert the order of their contents, so we need to
+    // undo that during serialization.
+    //
+    // This two-iterator strategy involves walking over the selector twice.
+    // We could do something more clever, but selector serialization probably
+    // isn't hot enough to justify it, and the stringification likely
+    // dominates anyway.
+    //
+    // NB: A parse-order iterator is a Rev<>, which doesn't expose as_slice(),
+    // which we need for |split|. So we split by combinators on a match-order
+    // sequence and then reverse.
+
+    let mut combinators = self.iter_raw_match_order().rev().filter_map(|x| x.as_combinator());
+    let compound_selectors = self.iter_raw_match_order().as_slice().split(|x| x.is_combinator()).rev();
+
+    let mut combinators_exhausted = false;
+    for compound in compound_selectors {
+      debug_assert!(!combinators_exhausted);
+
+      // https://drafts.csswg.org/cssom/#serializing-selectors
+      if compound.is_empty() {
+        continue;
+      }
+
+      // 1. If there is only one simple selector in the compound selectors
+      //    which is a universal selector, append the result of
+      //    serializing the universal selector to s.
+      //
+      // Check if `!compound.empty()` first--this can happen if we have
+      // something like `... > ::before`, because we store `>` and `::`
+      // both as combinators internally.
+      //
+      // If we are in this case, after we have serialized the universal
+      // selector, we skip Step 2 and continue with the algorithm.
+      let (can_elide_namespace, first_non_namespace) = match compound[0] {
+        Component::ExplicitAnyNamespace | Component::ExplicitNoNamespace | Component::Namespace(..) => (false, 1),
+        Component::DefaultNamespace(..) => (true, 1),
+        _ => (true, 0),
+      };
+      let mut perform_step_2 = true;
+      let next_combinator = combinators.next();
+      if first_non_namespace == compound.len() - 1 {
+        match (next_combinator, &compound[first_non_namespace]) {
+          // We have to be careful here, because if there is a
+          // pseudo element "combinator" there isn't really just
+          // the one simple selector. Technically this compound
+          // selector contains the pseudo element selector as well
+          // -- Combinator::PseudoElement, just like
+          // Combinator::SlotAssignment, don't exist in the
+          // spec.
+          (Some(Combinator::PseudoElement), _) | (Some(Combinator::SlotAssignment), _) => (),
+          (_, &Component::ExplicitUniversalType) => {
+            // Iterate over everything so we serialize the namespace
+            // too.
+            for simple in compound.iter() {
+              simple.to_css(dest)?;
+            }
+            // Skip step 2, which is an "otherwise".
+            perform_step_2 = false;
+          }
+          _ => (),
+        }
+      }
+
+      // 2. Otherwise, for each simple selector in the compound selectors
+      //    that is not a universal selector of which the namespace prefix
+      //    maps to a namespace that is not the default namespace
+      //    serialize the simple selector and append the result to s.
+      //
+      // See https://github.com/w3c/csswg-drafts/issues/1606, which is
+      // proposing to change this to match up with the behavior asserted
+      // in cssom/serialize-namespaced-type-selectors.html, which the
+      // following code tries to match.
+      if perform_step_2 {
+        for simple in compound.iter() {
+          if let Component::ExplicitUniversalType = *simple {
+            // Can't have a namespace followed by a pseudo-element
+            // selector followed by a universal selector in the same
+            // compound selector, so we don't have to worry about the
+            // real namespace being in a different `compound`.
+            if can_elide_namespace {
+              continue;
+            }
+          }
+          simple.to_css(dest)?;
+        }
+      }
+
+      // 3. If this is not the last part of the chain of the selector
+      //    append a single SPACE (U+0020), followed by the combinator
+      //    ">", "+", "~", ">>", "||", as appropriate, followed by another
+      //    single SPACE (U+0020) if the combinator was not whitespace, to
+      //    s.
+      match next_combinator {
+        Some(c) => c.to_css(dest)?,
+        None => combinators_exhausted = true,
+      };
+
+      // 4. If this is the last part of the chain of the selector and
+      //    there is a pseudo-element, append "::" followed by the name of
+      //    the pseudo-element, to s.
+      //
+      // (we handle this above)
+    }
+
+    Ok(())
+  }
+}
+
+impl ToCss for Combinator {
+  fn to_css<W>(&self, dest: &mut W) -> fmt::Result
+  where
+    W: fmt::Write,
+  {
+    match *self {
+      Combinator::Child => dest.write_str(" > "),
+      Combinator::Descendant => dest.write_str(" "),
+      Combinator::NextSibling => dest.write_str(" + "),
+      Combinator::LaterSibling => dest.write_str(" ~ "),
+      Combinator::DeepDescendant => dest.write_str(" >>> "),
+      Combinator::Deep => dest.write_str(" /deep/ "),
+      Combinator::PseudoElement | Combinator::Part | Combinator::SlotAssignment => Ok(()),
+    }
+  }
+}
+
+impl<'i, Impl: SelectorImpl<'i>> ToCss for Component<'i, Impl> {
+  fn to_css<W>(&self, dest: &mut W) -> fmt::Result
+  where
+    W: fmt::Write,
+  {
+    use self::Component::*;
+
+    match *self {
+      Combinator(ref c) => c.to_css(dest),
+      Slotted(ref selector) => {
+        dest.write_str("::slotted(")?;
+        selector.to_css(dest)?;
+        dest.write_char(')')
+      }
+      Part(ref part_names) => {
+        dest.write_str("::part(")?;
+        for (i, name) in part_names.iter().enumerate() {
+          if i != 0 {
+            dest.write_char(' ')?;
+          }
+          name.to_css(dest)?;
+        }
+        dest.write_char(')')
+      }
+      PseudoElement(ref p) => p.to_css(dest),
+      ID(ref s) => {
+        dest.write_char('#')?;
+        s.to_css(dest)
+      }
+      Class(ref s) => {
+        dest.write_char('.')?;
+        s.to_css(dest)
+      }
+      LocalName(ref s) => s.to_css(dest),
+      ExplicitUniversalType => dest.write_char('*'),
+
+      DefaultNamespace(_) => Ok(()),
+      ExplicitNoNamespace => dest.write_char('|'),
+      ExplicitAnyNamespace => dest.write_str("*|"),
+      Namespace(ref prefix, _) => {
+        prefix.to_css(dest)?;
+        dest.write_char('|')
+      }
+
+      AttributeInNoNamespaceExists { ref local_name, .. } => {
+        dest.write_char('[')?;
+        local_name.to_css(dest)?;
+        dest.write_char(']')
+      }
+      AttributeInNoNamespace {
+        ref local_name,
+        operator,
+        ref value,
+        case_sensitivity,
+        ..
+      } => {
+        dest.write_char('[')?;
+        local_name.to_css(dest)?;
+        operator.to_css(dest)?;
+        value.to_css(dest)?;
+        match case_sensitivity {
+          ParsedCaseSensitivity::CaseSensitive
+          | ParsedCaseSensitivity::AsciiCaseInsensitiveIfInHtmlElementInHtmlDocument => {}
+          ParsedCaseSensitivity::AsciiCaseInsensitive => dest.write_str(" i")?,
+          ParsedCaseSensitivity::ExplicitCaseSensitive => dest.write_str(" s")?,
+        }
+        dest.write_char(']')
+      }
+      AttributeOther(ref attr_selector) => attr_selector.to_css(dest),
+
+      // Pseudo-classes
+      Root => dest.write_str(":root"),
+      Empty => dest.write_str(":empty"),
+      Scope => dest.write_str(":scope"),
+      Host(ref selector) => {
+        dest.write_str(":host")?;
+        if let Some(ref selector) = *selector {
+          dest.write_char('(')?;
+          selector.to_css(dest)?;
+          dest.write_char(')')?;
+        }
+        Ok(())
+      }
+      Nth(ref nth_data) => {
+        nth_data.write_start(dest, nth_data.is_function())?;
+        if nth_data.is_function() {
+          nth_data.write_affine(dest)?;
+          dest.write_char(')')?;
+        }
+        Ok(())
+      }
+      NthOf(ref nth_of_data) => {
+        let nth_data = nth_of_data.nth_data();
+        nth_data.write_start(dest, true)?;
+        debug_assert!(
+          nth_data.is_function,
+          "A selector must be a function to hold An+B notation"
+        );
+        nth_data.write_affine(dest)?;
+        debug_assert!(
+          matches!(nth_data.ty, NthType::Child | NthType::LastChild),
+          "Only :nth-child or :nth-last-child can be of a selector list"
+        );
+        debug_assert!(
+          !nth_of_data.selectors().is_empty(),
+          "The selector list should not be empty"
+        );
+        dest.write_str(" of ")?;
+        serialize_selector_list(nth_of_data.selectors().iter(), dest)?;
+        dest.write_char(')')
+      }
+      Is(ref list) | Where(ref list) | Negation(ref list) | Has(ref list) | Any(_, ref list) => {
+        match *self {
+          Where(..) => dest.write_str(":where(")?,
+          Is(..) => dest.write_str(":is(")?,
+          Negation(..) => dest.write_str(":not(")?,
+          Has(..) => dest.write_str(":has(")?,
+          Any(ref prefix, _) => {
+            dest.write_char(':')?;
+            prefix.to_css(dest)?;
+            dest.write_str("any(")?;
+          }
+          _ => unreachable!(),
+        }
+        serialize_selector_list(list.iter(), dest)?;
+        dest.write_str(")")
+      }
+      NonTSPseudoClass(ref pseudo) => pseudo.to_css(dest),
+      Nesting => dest.write_char('&'),
+    }
+  }
+}
+
+impl<'i, Impl: SelectorImpl<'i>> ToCss for AttrSelectorWithOptionalNamespace<'i, Impl> {
+  fn to_css<W>(&self, dest: &mut W) -> fmt::Result
+  where
+    W: fmt::Write,
+  {
+    dest.write_char('[')?;
+    match self.namespace {
+      Some(NamespaceConstraint::Specific((ref prefix, _))) => {
+        prefix.to_css(dest)?;
+        dest.write_char('|')?
+      }
+      Some(NamespaceConstraint::Any) => dest.write_str("*|")?,
+      None => {}
+    }
+    self.local_name.to_css(dest)?;
+    match self.operation {
+      ParsedAttrSelectorOperation::Exists => {}
+      ParsedAttrSelectorOperation::WithValue {
+        operator,
+        case_sensitivity,
+        ref expected_value,
+      } => {
+        operator.to_css(dest)?;
+        expected_value.to_css(dest)?;
+        match case_sensitivity {
+          ParsedCaseSensitivity::CaseSensitive
+          | ParsedCaseSensitivity::AsciiCaseInsensitiveIfInHtmlElementInHtmlDocument => {}
+          ParsedCaseSensitivity::AsciiCaseInsensitive => dest.write_str(" i")?,
+          ParsedCaseSensitivity::ExplicitCaseSensitive => dest.write_str(" s")?,
+        }
+      }
+    }
+    dest.write_char(']')
+  }
+}
+
+impl<'i, Impl: SelectorImpl<'i>> ToCss for LocalName<'i, Impl> {
+  fn to_css<W>(&self, dest: &mut W) -> fmt::Result
+  where
+    W: fmt::Write,
+  {
+    self.name.to_css(dest)
+  }
+}
+
+/// Build up a Selector.
+/// selector : simple_selector_sequence [ combinator simple_selector_sequence ]* ;
+///
+/// `Err` means invalid selector.
+fn parse_selector<'i, 't, P, Impl>(
+  parser: &P,
+  input: &mut CssParser<'i, 't>,
+  state: &mut SelectorParsingState,
+  nesting_requirement: NestingRequirement,
+) -> Result<Selector<'i, Impl>, ParseError<'i, P::Error>>
+where
+  P: Parser<'i, Impl = Impl>,
+  Impl: SelectorImpl<'i>,
+{
+  if nesting_requirement == NestingRequirement::Prefixed {
+    let state = input.state();
+    if !input.expect_delim('&').is_ok() {
+      return Err(input.new_custom_error(SelectorParseErrorKind::MissingNestingPrefix));
+    }
+    input.reset(&state);
+  }
+
+  let mut builder = SelectorBuilder::default();
+
+  'outer_loop: loop {
+    // Parse a sequence of simple selectors.
+    let empty = parse_compound_selector(parser, state, input, &mut builder)?;
+    if empty {
+      return Err(input.new_custom_error(if builder.has_combinators() {
+        SelectorParseErrorKind::DanglingCombinator
+      } else {
+        SelectorParseErrorKind::EmptySelector
+      }));
+    }
+
+    if state.intersects(SelectorParsingState::AFTER_PSEUDO) {
+      // Input should be exhausted here.
+      let source_location = input.current_source_location();
+      if let Ok(next) = input.next() {
+        let next = next.clone();
+        return Err(
+          source_location.new_custom_error(SelectorParseErrorKind::UnexpectedSelectorAfterPseudoElement(next)),
+        );
+      }
+      break;
+    }
+
+    // Parse a combinator.
+    let combinator;
+    let mut any_whitespace = false;
+    loop {
+      let before_this_token = input.state();
+      match input.next_including_whitespace() {
+        Err(_e) => break 'outer_loop,
+        Ok(&Token::WhiteSpace(_)) => any_whitespace = true,
+        Ok(&Token::Delim('>')) => {
+          if parser.deep_combinator_enabled()
+            && input
+              .try_parse(|input| {
+                input.expect_delim('>')?;
+                input.expect_delim('>')
+              })
+              .is_ok()
+          {
+            combinator = Combinator::DeepDescendant;
+          } else {
+            combinator = Combinator::Child;
+          }
+          break;
+        }
+        Ok(&Token::Delim('+')) => {
+          combinator = Combinator::NextSibling;
+          break;
+        }
+        Ok(&Token::Delim('~')) => {
+          combinator = Combinator::LaterSibling;
+          break;
+        }
+        Ok(&Token::Delim('/')) if parser.deep_combinator_enabled() => {
+          if input
+            .try_parse(|input| {
+              input.expect_ident_matching("deep")?;
+              input.expect_delim('/')
+            })
+            .is_ok()
+          {
+            combinator = Combinator::Deep;
+            break;
+          } else {
+            break 'outer_loop;
+          }
+        }
+        Ok(_) => {
+          input.reset(&before_this_token);
+          if any_whitespace {
+            combinator = Combinator::Descendant;
+            break;
+          } else {
+            break 'outer_loop;
+          }
+        }
+      }
+    }
+
+    if !state.allows_combinators() {
+      return Err(input.new_custom_error(SelectorParseErrorKind::InvalidState));
+    }
+
+    builder.push_combinator(combinator);
+  }
+
+  if !state.contains(SelectorParsingState::AFTER_NESTING) {
+    match nesting_requirement {
+      NestingRequirement::Implicit => {
+        builder.add_nesting_prefix();
+      }
+      NestingRequirement::Contained | NestingRequirement::Prefixed => {
+        return Err(input.new_custom_error(SelectorParseErrorKind::MissingNestingSelector));
+      }
+      _ => {}
+    }
+  }
+
+  let has_pseudo_element = state
+    .intersects(SelectorParsingState::AFTER_PSEUDO_ELEMENT | SelectorParsingState::AFTER_UNKNOWN_PSEUDO_ELEMENT);
+  let slotted = state.intersects(SelectorParsingState::AFTER_SLOTTED);
+  let part = state.intersects(SelectorParsingState::AFTER_PART);
+  let (spec, components) = builder.build(has_pseudo_element, slotted, part);
+  Ok(Selector(spec, components))
+}
+
+impl<'i, Impl: SelectorImpl<'i>> Selector<'i, Impl> {
+  /// Parse a selector, without any pseudo-element.
+  #[inline]
+  pub fn parse<'t, P>(parser: &P, input: &mut CssParser<'i, 't>) -> Result<Self, ParseError<'i, P::Error>>
+  where
+    P: Parser<'i, Impl = Impl>,
+  {
+    parse_selector(
+      parser,
+      input,
+      &mut SelectorParsingState::empty(),
+      NestingRequirement::None,
+    )
+  }
+}
+
+fn parse_relative_selector<'i, 't, P, Impl>(
+  parser: &P,
+  input: &mut CssParser<'i, 't>,
+  state: &mut SelectorParsingState,
+  mut nesting_requirement: NestingRequirement,
+) -> Result<Selector<'i, Impl>, ParseError<'i, P::Error>>
+where
+  P: Parser<'i, Impl = Impl>,
+  Impl: SelectorImpl<'i>,
+{
+  // https://www.w3.org/TR/selectors-4/#parse-relative-selector
+  let s = input.state();
+  let combinator = match input.next()? {
+    Token::Delim('>') => Some(Combinator::Child),
+    Token::Delim('+') => Some(Combinator::NextSibling),
+    Token::Delim('~') => Some(Combinator::LaterSibling),
+    _ => {
+      input.reset(&s);
+      None
+    }
+  };
+
+  let scope = if nesting_requirement == NestingRequirement::Implicit {
+    Component::Nesting
+  } else {
+    Component::Scope
+  };
+
+  if combinator.is_some() {
+    nesting_requirement = NestingRequirement::None;
+  }
+
+  let mut selector = parse_selector(parser, input, state, nesting_requirement)?;
+  if let Some(combinator) = combinator {
+    // https://www.w3.org/TR/selectors/#absolutizing
+    selector.1.push(Component::Combinator(combinator));
+    selector.1.push(scope);
+  }
+
+  Ok(selector)
+}
+
+/// * `Err(())`: Invalid selector, abort
+/// * `Ok(false)`: Not a type selector, could be something else. `input` was not consumed.
+/// * `Ok(true)`: Length 0 (`*|*`), 1 (`*|E` or `ns|*`) or 2 (`|E` or `ns|E`)
+fn parse_type_selector<'i, 't, P, Impl, S>(
+  parser: &P,
+  input: &mut CssParser<'i, 't>,
+  state: SelectorParsingState,
+  sink: &mut S,
+) -> Result<bool, ParseError<'i, P::Error>>
+where
+  P: Parser<'i, Impl = Impl>,
+  Impl: SelectorImpl<'i>,
+  S: Push<Component<'i, Impl>>,
+{
+  match parse_qualified_name(parser, input, /* in_attr_selector = */ false) {
+    Err(ParseError {
+      kind: ParseErrorKind::Basic(BasicParseErrorKind::EndOfInput),
+      ..
+    })
+    | Ok(OptionalQName::None(_)) => Ok(false),
+    Ok(OptionalQName::Some(namespace, local_name)) => {
+      if state.intersects(SelectorParsingState::AFTER_PSEUDO) {
+        return Err(input.new_custom_error(SelectorParseErrorKind::InvalidState));
+      }
+      match namespace {
+        QNamePrefix::ImplicitAnyNamespace => {}
+        QNamePrefix::ImplicitDefaultNamespace(url) => sink.push(Component::DefaultNamespace(url)),
+        QNamePrefix::ExplicitNamespace(prefix, url) => sink.push(match parser.default_namespace() {
+          Some(ref default_url) if url == *default_url => Component::DefaultNamespace(url),
+          _ => Component::Namespace(prefix, url),
+        }),
+        QNamePrefix::ExplicitNoNamespace => sink.push(Component::ExplicitNoNamespace),
+        QNamePrefix::ExplicitAnyNamespace => {
+          // Element type selectors that have no namespace
+          // component (no namespace separator) represent elements
+          // without regard to the element's namespace (equivalent
+          // to "*|") unless a default namespace has been declared
+          // for namespaced selectors (e.g. in CSS, in the style
+          // sheet). If a default namespace has been declared,
+          // such selectors will represent only elements in the
+          // default namespace.
+          // -- Selectors § 6.1.1
+          // So we'll have this act the same as the
+          // QNamePrefix::ImplicitAnyNamespace case.
+          // For lightning css this logic was removed, should be handled when matching.
+          sink.push(Component::ExplicitAnyNamespace)
+        }
+        QNamePrefix::ImplicitNoNamespace => {
+          unreachable!() // Not returned with in_attr_selector = false
+        }
+      }
+      match local_name {
+        Some(name) => sink.push(Component::LocalName(LocalName {
+          lower_name: to_ascii_lowercase(name.clone()).into(),
+          name: name.into(),
+        })),
+        None => sink.push(Component::ExplicitUniversalType),
+      }
+      Ok(true)
+    }
+    Err(e) => Err(e),
+  }
+}
+
+#[derive(Debug)]
+enum SimpleSelectorParseResult<'i, Impl: SelectorImpl<'i>> {
+  SimpleSelector(Component<'i, Impl>),
+  PseudoElement(Impl::PseudoElement),
+  SlottedPseudo(Selector<'i, Impl>),
+  PartPseudo(Box<[Impl::Identifier]>),
+}
+
+#[derive(Debug)]
+enum QNamePrefix<'i, Impl: SelectorImpl<'i>> {
+  ImplicitNoNamespace,                                          // `foo` in attr selectors
+  ImplicitAnyNamespace,                                         // `foo` in type selectors, without a default ns
+  ImplicitDefaultNamespace(Impl::NamespaceUrl),                 // `foo` in type selectors, with a default ns
+  ExplicitNoNamespace,                                          // `|foo`
+  ExplicitAnyNamespace,                                         // `*|foo`
+  ExplicitNamespace(Impl::NamespacePrefix, Impl::NamespaceUrl), // `prefix|foo`
+}
+
+enum OptionalQName<'i, Impl: SelectorImpl<'i>> {
+  Some(QNamePrefix<'i, Impl>, Option<CowRcStr<'i>>),
+  None(Token<'i>),
+}
+
+/// * `Err(())`: Invalid selector, abort
+/// * `Ok(None(token))`: Not a simple selector, could be something else. `input` was not consumed,
+///                      but the token is still returned.
+/// * `Ok(Some(namespace, local_name))`: `None` for the local name means a `*` universal selector
+fn parse_qualified_name<'i, 't, P, Impl>(
+  parser: &P,
+  input: &mut CssParser<'i, 't>,
+  in_attr_selector: bool,
+) -> Result<OptionalQName<'i, Impl>, ParseError<'i, P::Error>>
+where
+  P: Parser<'i, Impl = Impl>,
+  Impl: SelectorImpl<'i>,
+{
+  let default_namespace = |local_name| {
+    let namespace = match parser.default_namespace() {
+      Some(url) => QNamePrefix::ImplicitDefaultNamespace(url),
+      None => QNamePrefix::ImplicitAnyNamespace,
+    };
+    Ok(OptionalQName::Some(namespace, local_name))
+  };
+
+  let explicit_namespace = |input: &mut CssParser<'i, 't>, namespace| {
+    let location = input.current_source_location();
+    match input.next_including_whitespace() {
+      Ok(&Token::Delim('*')) if !in_attr_selector => Ok(OptionalQName::Some(namespace, None)),
+      Ok(&Token::Ident(ref local_name)) => Ok(OptionalQName::Some(namespace, Some(local_name.clone()))),
+      Ok(t) if in_attr_selector => {
+        let e = SelectorParseErrorKind::InvalidQualNameInAttr(t.clone());
+        Err(location.new_custom_error(e))
+      }
+      Ok(t) => Err(location.new_custom_error(SelectorParseErrorKind::ExplicitNamespaceUnexpectedToken(t.clone()))),
+      Err(e) => Err(e.into()),
+    }
+  };
+
+  let start = input.state();
+  // FIXME: remove clone() when lifetimes are non-lexical
+  match input.next_including_whitespace().map(|t| t.clone()) {
+    Ok(Token::Ident(value)) => {
+      let after_ident = input.state();
+      match input.next_including_whitespace() {
+        Ok(&Token::Delim('|')) => {
+          let prefix = value.clone().into();
+          let result = parser.namespace_for_prefix(&prefix);
+          let url = result.ok_or(
+            after_ident
+              .source_location()
+              .new_custom_error(SelectorParseErrorKind::ExpectedNamespace(value)),
+          )?;
+          explicit_namespace(input, QNamePrefix::ExplicitNamespace(prefix, url))
+        }
+        _ => {
+          input.reset(&after_ident);
+          if in_attr_selector {
+            Ok(OptionalQName::Some(QNamePrefix::ImplicitNoNamespace, Some(value)))
+          } else {
+            default_namespace(Some(value))
+          }
+        }
+      }
+    }
+    Ok(Token::Delim('*')) => {
+      let after_star = input.state();
+      // FIXME: remove clone() when lifetimes are non-lexical
+      match input.next_including_whitespace().map(|t| t.clone()) {
+        Ok(Token::Delim('|')) => explicit_namespace(input, QNamePrefix::ExplicitAnyNamespace),
+        result => {
+          input.reset(&after_star);
+          if in_attr_selector {
+            match result {
+              Ok(t) => Err(
+                after_star
+                  .source_location()
+                  .new_custom_error(SelectorParseErrorKind::ExpectedBarInAttr(t)),
+              ),
+              Err(e) => Err(e.into()),
+            }
+          } else {
+            default_namespace(None)
+          }
+        }
+      }
+    }
+    Ok(Token::Delim('|')) => explicit_namespace(input, QNamePrefix::ExplicitNoNamespace),
+    Ok(t) => {
+      input.reset(&start);
+      Ok(OptionalQName::None(t))
+    }
+    Err(e) => {
+      input.reset(&start);
+      Err(e.into())
+    }
+  }
+}
+
+fn parse_attribute_selector<'i, 't, P, Impl>(
+  parser: &P,
+  input: &mut CssParser<'i, 't>,
+) -> Result<Component<'i, Impl>, ParseError<'i, P::Error>>
+where
+  P: Parser<'i, Impl = Impl>,
+  Impl: SelectorImpl<'i>,
+{
+  let namespace;
+  let local_name;
+
+  input.skip_whitespace();
+
+  match parse_qualified_name(parser, input, /* in_attr_selector = */ true)? {
+    OptionalQName::None(t) => {
+      return Err(input.new_custom_error(SelectorParseErrorKind::NoQualifiedNameInAttributeSelector(t)));
+    }
+    OptionalQName::Some(_, None) => unreachable!(),
+    OptionalQName::Some(ns, Some(ln)) => {
+      local_name = ln;
+      namespace = match ns {
+        QNamePrefix::ImplicitNoNamespace | QNamePrefix::ExplicitNoNamespace => None,
+        QNamePrefix::ExplicitNamespace(prefix, url) => Some(NamespaceConstraint::Specific((prefix, url))),
+        QNamePrefix::ExplicitAnyNamespace => Some(NamespaceConstraint::Any),
+        QNamePrefix::ImplicitAnyNamespace | QNamePrefix::ImplicitDefaultNamespace(_) => {
+          unreachable!() // Not returned with in_attr_selector = true
+        }
+      }
+    }
+  }
+
+  let location = input.current_source_location();
+  let operator = match input.next() {
+    // [foo]
+    Err(_) => {
+      let local_name_lower = to_ascii_lowercase(local_name.clone()).into();
+      let local_name = local_name.into();
+      if let Some(namespace) = namespace {
+        return Ok(Component::AttributeOther(Box::new(AttrSelectorWithOptionalNamespace {
+          namespace: Some(namespace),
+          local_name,
+          local_name_lower,
+          operation: ParsedAttrSelectorOperation::Exists,
+          never_matches: false,
+        })));
+      } else {
+        return Ok(Component::AttributeInNoNamespaceExists {
+          local_name,
+          local_name_lower,
+        });
+      }
+    }
+
+    // [foo=bar]
+    Ok(&Token::Delim('=')) => AttrSelectorOperator::Equal,
+    // [foo~=bar]
+    Ok(&Token::IncludeMatch) => AttrSelectorOperator::Includes,
+    // [foo|=bar]
+    Ok(&Token::DashMatch) => AttrSelectorOperator::DashMatch,
+    // [foo^=bar]
+    Ok(&Token::PrefixMatch) => AttrSelectorOperator::Prefix,
+    // [foo*=bar]
+    Ok(&Token::SubstringMatch) => AttrSelectorOperator::Substring,
+    // [foo$=bar]
+    Ok(&Token::SuffixMatch) => AttrSelectorOperator::Suffix,
+    Ok(t) => {
+      return Err(
+        location.new_custom_error(SelectorParseErrorKind::UnexpectedTokenInAttributeSelector(t.clone())),
+      );
+    }
+  };
+
+  let value = match input.expect_ident_or_string() {
+    Ok(t) => t.clone(),
+    Err(BasicParseError {
+      kind: BasicParseErrorKind::UnexpectedToken(t),
+      location,
+    }) => return Err(location.new_custom_error(SelectorParseErrorKind::BadValueInAttr(t))),
+    Err(e) => return Err(e.into()),
+  };
+  let never_matches = match operator {
+    AttrSelectorOperator::Equal | AttrSelectorOperator::DashMatch => false,
+
+    AttrSelectorOperator::Includes => value.is_empty() || value.contains(SELECTOR_WHITESPACE),
+
+    AttrSelectorOperator::Prefix | AttrSelectorOperator::Substring | AttrSelectorOperator::Suffix => {
+      value.is_empty()
+    }
+  };
+
+  let attribute_flags = parse_attribute_flags(input)?;
+
+  let value = value.into();
+  let case_sensitivity;
+  // copied from to_ascii_lowercase function, so we can know whether it is already lower case.
+  let (local_name_lower_cow, local_name_is_ascii_lowercase) =
+    if let Some(first_uppercase) = local_name.bytes().position(|byte| byte >= b'A' && byte <= b'Z') {
+      let mut string = local_name.to_string();
+      string[first_uppercase..].make_ascii_lowercase();
+      (string.into(), false)
+    } else {
+      (local_name.clone(), true)
+    };
+  case_sensitivity = attribute_flags.to_case_sensitivity(local_name_lower_cow.as_ref(), namespace.is_some());
+  let local_name_lower = local_name_lower_cow.into();
+  let local_name = local_name.into();
+  if namespace.is_some() || !local_name_is_ascii_lowercase {
+    Ok(Component::AttributeOther(Box::new(AttrSelectorWithOptionalNamespace {
+      namespace,
+      local_name,
+      local_name_lower,
+      never_matches,
+      operation: ParsedAttrSelectorOperation::WithValue {
+        operator,
+        case_sensitivity,
+        expected_value: value,
+      },
+    })))
+  } else {
+    Ok(Component::AttributeInNoNamespace {
+      local_name,
+      operator,
+      value,
+      case_sensitivity,
+      never_matches,
+    })
+  }
+}
+
+/// An attribute selector can have 's' or 'i' as flags, or no flags at all.
+enum AttributeFlags {
+  // Matching should be case-sensitive ('s' flag).
+  CaseSensitive,
+  // Matching should be case-insensitive ('i' flag).
+  AsciiCaseInsensitive,
+  // No flags.  Matching behavior depends on the name of the attribute.
+  CaseSensitivityDependsOnName,
+}
+
+impl AttributeFlags {
+  fn to_case_sensitivity(self, local_name: &str, have_namespace: bool) -> ParsedCaseSensitivity {
+    match self {
+      AttributeFlags::CaseSensitive => ParsedCaseSensitivity::ExplicitCaseSensitive,
+      AttributeFlags::AsciiCaseInsensitive => ParsedCaseSensitivity::AsciiCaseInsensitive,
+      AttributeFlags::CaseSensitivityDependsOnName => {
+        if !have_namespace
+          && include!(concat!(env!("OUT_DIR"), "/ascii_case_insensitive_html_attributes.rs")).contains(local_name)
+        {
+          ParsedCaseSensitivity::AsciiCaseInsensitiveIfInHtmlElementInHtmlDocument
+        } else {
+          ParsedCaseSensitivity::CaseSensitive
+        }
+      }
+    }
+  }
+}
+
+fn parse_attribute_flags<'i, 't>(input: &mut CssParser<'i, 't>) -> Result<AttributeFlags, BasicParseError<'i>> {
+  let location = input.current_source_location();
+  let token = match input.next() {
+    Ok(t) => t,
+    Err(..) => {
+      // Selectors spec says language-defined; HTML says it depends on the
+      // exact attribute name.
+      return Ok(AttributeFlags::CaseSensitivityDependsOnName);
+    }
+  };
+
+  let ident = match *token {
+    Token::Ident(ref i) => i,
+    ref other => return Err(location.new_basic_unexpected_token_error(other.clone())),
+  };
+
+  Ok(match_ignore_ascii_case! {
+      ident,
+      "i" => AttributeFlags::AsciiCaseInsensitive,
+      "s" => AttributeFlags::CaseSensitive,
+      _ => return Err(location.new_basic_unexpected_token_error(token.clone())),
+  })
+}
+
+/// Level 3: Parse **one** simple_selector.  (Though we might insert a second
+/// implied "<defaultns>|*" type selector.)
+fn parse_negation<'i, 't, P, Impl>(
+  parser: &P,
+  input: &mut CssParser<'i, 't>,
+  state: &mut SelectorParsingState,
+) -> Result<Component<'i, Impl>, ParseError<'i, P::Error>>
+where
+  P: Parser<'i, Impl = Impl>,
+  Impl: SelectorImpl<'i>,
+{
+  let mut child_state =
+    *state | SelectorParsingState::SKIP_DEFAULT_NAMESPACE | SelectorParsingState::DISALLOW_PSEUDOS;
+  let list = SelectorList::parse_with_state(
+    parser,
+    input,
+    &mut child_state,
+    ParseErrorRecovery::DiscardList,
+    NestingRequirement::None,
+  )?;
+
+  if child_state.contains(SelectorParsingState::AFTER_NESTING) {
+    state.insert(SelectorParsingState::AFTER_NESTING)
+  }
+
+  Ok(Component::Negation(list.0.into_vec().into_boxed_slice()))
+}
+
+/// simple_selector_sequence
+/// : [ type_selector | universal ] [ HASH | class | attrib | pseudo | negation ]*
+/// | [ HASH | class | attrib | pseudo | negation ]+
+///
+/// `Err(())` means invalid selector.
+/// `Ok(true)` is an empty selector
+fn parse_compound_selector<'i, 't, P, Impl>(
+  parser: &P,
+  state: &mut SelectorParsingState,
+  input: &mut CssParser<'i, 't>,
+  builder: &mut SelectorBuilder<'i, Impl>,
+) -> Result<bool, ParseError<'i, P::Error>>
+where
+  P: Parser<'i, Impl = Impl>,
+  Impl: SelectorImpl<'i>,
+{
+  input.skip_whitespace();
+
+  let mut empty = true;
+  if parser.is_nesting_allowed() && input.try_parse(|input| input.expect_delim('&')).is_ok() {
+    state.insert(SelectorParsingState::AFTER_NESTING);
+    builder.push_simple_selector(Component::Nesting);
+    empty = false;
+  }
+
+  if parse_type_selector(parser, input, *state, builder)? {
+    empty = false;
+  }
+
+  loop {
+    let result = match parse_one_simple_selector(parser, input, state)? {
+      None => break,
+      Some(result) => result,
+    };
+
+    if empty {
+      if let Some(url) = parser.default_namespace() {
+        // If there was no explicit type selector, but there is a
+        // default namespace, there is an implicit "<defaultns>|*" type
+        // selector. Except for :host() or :not() / :is() / :where(),
+        // where we ignore it.
+        //
+        // https://drafts.csswg.org/css-scoping/#host-element-in-tree:
+        //
+        //     When considered within its own shadow trees, the shadow
+        //     host is featureless. Only the :host, :host(), and
+        //     :host-context() pseudo-classes are allowed to match it.
+        //
+        // https://drafts.csswg.org/selectors-4/#featureless:
+        //
+        //     A featureless element does not match any selector at all,
+        //     except those it is explicitly defined to match. If a
+        //     given selector is allowed to match a featureless element,
+        //     it must do so while ignoring the default namespace.
+        //
+        // https://drafts.csswg.org/selectors-4/#matches
+        //
+        //     Default namespace declarations do not affect the compound
+        //     selector representing the subject of any selector within
+        //     a :is() pseudo-class, unless that compound selector
+        //     contains an explicit universal selector or type selector.
+        //
+        //     (Similar quotes for :where() / :not())
+        //
+        let ignore_default_ns = state.intersects(SelectorParsingState::SKIP_DEFAULT_NAMESPACE)
+          || matches!(result, SimpleSelectorParseResult::SimpleSelector(Component::Host(..)));
+        if !ignore_default_ns {
+          builder.push_simple_selector(Component::DefaultNamespace(url));
+        }
+      }
+    }
+
+    empty = false;
+
+    match result {
+      SimpleSelectorParseResult::SimpleSelector(s) => {
+        builder.push_simple_selector(s);
+      }
+      SimpleSelectorParseResult::PartPseudo(part_names) => {
+        state.insert(SelectorParsingState::AFTER_PART);
+        builder.push_combinator(Combinator::Part);
+        builder.push_simple_selector(Component::Part(part_names));
+      }
+      SimpleSelectorParseResult::SlottedPseudo(selector) => {
+        state.insert(SelectorParsingState::AFTER_SLOTTED);
+        builder.push_combinator(Combinator::SlotAssignment);
+        builder.push_simple_selector(Component::Slotted(selector));
+      }
+      SimpleSelectorParseResult::PseudoElement(p) => {
+        if !p.is_unknown() {
+          state.insert(SelectorParsingState::AFTER_PSEUDO_ELEMENT);
+          builder.push_combinator(Combinator::PseudoElement);
+        } else {
+          state.insert(SelectorParsingState::AFTER_UNKNOWN_PSEUDO_ELEMENT);
+        }
+        if !p.accepts_state_pseudo_classes() {
+          state.insert(SelectorParsingState::AFTER_NON_STATEFUL_PSEUDO_ELEMENT);
+        }
+        if p.is_webkit_scrollbar() {
+          state.insert(SelectorParsingState::AFTER_WEBKIT_SCROLLBAR);
+        }
+        if p.is_view_transition() {
+          state.insert(SelectorParsingState::AFTER_VIEW_TRANSITION);
+        }
+        builder.push_simple_selector(Component::PseudoElement(p));
+      }
+    }
+  }
+  Ok(empty)
+}
+
+fn parse_is_or_where<'i, 't, P, Impl>(
+  parser: &P,
+  input: &mut CssParser<'i, 't>,
+  state: &mut SelectorParsingState,
+  component: impl FnOnce(Box<[Selector<'i, Impl>]>) -> Component<'i, Impl>,
+) -> Result<Component<'i, Impl>, ParseError<'i, P::Error>>
+where
+  P: Parser<'i, Impl = Impl>,
+  Impl: SelectorImpl<'i>,
+{
+  debug_assert!(parser.parse_is_and_where());
+  // https://drafts.csswg.org/selectors/#matches-pseudo:
+  //
+  //     Pseudo-elements cannot be represented by the matches-any
+  //     pseudo-class; they are not valid within :is().
+  //
+  let mut child_state =
+    *state | SelectorParsingState::SKIP_DEFAULT_NAMESPACE | SelectorParsingState::DISALLOW_PSEUDOS;
+  let inner = SelectorList::parse_with_state(
+    parser,
+    input,
+    &mut child_state,
+    parser.is_and_where_error_recovery(),
+    NestingRequirement::None,
+  )?;
+  if child_state.contains(SelectorParsingState::AFTER_NESTING) {
+    state.insert(SelectorParsingState::AFTER_NESTING)
+  }
+  Ok(component(inner.0.into_vec().into_boxed_slice()))
+}
+
+fn parse_has<'i, 't, P, Impl>(
+  parser: &P,
+  input: &mut CssParser<'i, 't>,
+  state: &mut SelectorParsingState,
+) -> Result<Component<'i, Impl>, ParseError<'i, P::Error>>
+where
+  P: Parser<'i, Impl = Impl>,
+  Impl: SelectorImpl<'i>,
+{
+  let mut child_state = *state;
+  let inner = SelectorList::parse_relative_with_state(
+    parser,
+    input,
+    &mut child_state,
+    parser.is_and_where_error_recovery(),
+    NestingRequirement::None,
+  )?;
+  if child_state.contains(SelectorParsingState::AFTER_NESTING) {
+    state.insert(SelectorParsingState::AFTER_NESTING)
+  }
+  Ok(Component::Has(inner.0.into_vec().into_boxed_slice()))
+}
+
+fn parse_functional_pseudo_class<'i, 't, P, Impl>(
+  parser: &P,
+  input: &mut CssParser<'i, 't>,
+  name: CowRcStr<'i>,
+  state: &mut SelectorParsingState,
+) -> Result<Component<'i, Impl>, ParseError<'i, P::Error>>
+where
+  P: Parser<'i, Impl = Impl>,
+  Impl: SelectorImpl<'i>,
+{
+  match_ignore_ascii_case! { &name,
+      "nth-child" => return parse_nth_pseudo_class(parser, input, *state, NthType::Child),
+      "nth-of-type" => return parse_nth_pseudo_class(parser, input, *state, NthType::OfType),
+      "nth-last-child" => return parse_nth_pseudo_class(parser, input, *state, NthType::LastChild),
+      "nth-last-of-type" => return parse_nth_pseudo_class(parser, input, *state, NthType::LastOfType),
+      "nth-col" => return parse_nth_pseudo_class(parser, input, *state, NthType::Col),
+      "nth-last-col" => return parse_nth_pseudo_class(parser, input, *state, NthType::LastCol),
+      "is" if parser.parse_is_and_where() => return parse_is_or_where(parser, input, state, Component::Is),
+      "where" if parser.parse_is_and_where() => return parse_is_or_where(parser, input, state, Component::Where),
+      "has" => return parse_has(parser, input, state),
+      "host" => {
+          if !state.allows_tree_structural_pseudo_classes() {
+              return Err(input.new_custom_error(SelectorParseErrorKind::InvalidState));
+          }
+          return Ok(Component::Host(Some(parse_inner_compound_selector(parser, input, state)?)));
+      },
+      "not" => {
+          return parse_negation(parser, input, state)
+      },
+      _ => {}
+  }
+
+  if let Some(prefix) = parser.parse_any_prefix(&name) {
+    return parse_is_or_where(parser, input, state, |selectors| Component::Any(prefix, selectors));
+  }
+
+  if !state.allows_custom_functional_pseudo_classes() {
+    return Err(input.new_custom_error(SelectorParseErrorKind::InvalidState));
+  }
+
+  P::parse_non_ts_functional_pseudo_class(parser, name, input).map(Component::NonTSPseudoClass)
+}
+
+fn parse_nth_pseudo_class<'i, 't, P, Impl>(
+  parser: &P,
+  input: &mut CssParser<'i, 't>,
+  state: SelectorParsingState,
+  ty: NthType,
+) -> Result<Component<'i, Impl>, ParseError<'i, P::Error>>
+where
+  P: Parser<'i, Impl = Impl>,
+  Impl: SelectorImpl<'i>,
+{
+  if !state.allows_tree_structural_pseudo_classes() {
+    return Err(input.new_custom_error(SelectorParseErrorKind::InvalidState));
+  }
+  let (a, b) = parse_nth(input)?;
+  let nth_data = NthSelectorData {
+    ty,
+    is_function: true,
+    a,
+    b,
+  };
+  if !ty.allows_of_selector() {
+    return Ok(Component::Nth(nth_data));
+  }
+
+  // Try to parse "of <selector-list>".
+  if input.try_parse(|i| i.expect_ident_matching("of")).is_err() {
+    return Ok(Component::Nth(nth_data));
+  }
+  // Whitespace between "of" and the selector list is optional
+  // https://github.com/w3c/csswg-drafts/issues/8285
+  let mut child_state =
+    state | SelectorParsingState::SKIP_DEFAULT_NAMESPACE | SelectorParsingState::DISALLOW_PSEUDOS;
+  let selectors = SelectorList::parse_with_state(
+    parser,
+    input,
+    &mut child_state,
+    ParseErrorRecovery::IgnoreInvalidSelector,
+    NestingRequirement::None,
+  )?;
+  Ok(Component::NthOf(NthOfSelectorData::new(
+    nth_data,
+    selectors.0.into_vec().into_boxed_slice(),
+  )))
+}
+
+/// Returns whether the name corresponds to a CSS2 pseudo-element that
+/// can be specified with the single colon syntax (in addition to the
+/// double-colon syntax, which can be used for all pseudo-elements).
+fn is_css2_pseudo_element(name: &str) -> bool {
+  // ** Do not add to this list! **
+  match_ignore_ascii_case! { name,
+      "before" | "after" | "first-line" | "first-letter" => true,
+      _ => false,
+  }
+}
+
+/// Parse a simple selector other than a type selector.
+///
+/// * `Err(())`: Invalid selector, abort
+/// * `Ok(None)`: Not a simple selector, could be something else. `input` was not consumed.
+/// * `Ok(Some(_))`: Parsed a simple selector or pseudo-element
+fn parse_one_simple_selector<'i, 't, P, Impl>(
+  parser: &P,
+  input: &mut CssParser<'i, 't>,
+  state: &mut SelectorParsingState,
+) -> Result<Option<SimpleSelectorParseResult<'i, Impl>>, ParseError<'i, P::Error>>
+where
+  P: Parser<'i, Impl = Impl>,
+  Impl: SelectorImpl<'i>,
+{
+  let start = input.state();
+  let token_location = input.current_source_location();
+  let token = match input.next_including_whitespace().map(|t| t.clone()) {
+    Ok(t) => t,
+    Err(..) => {
+      input.reset(&start);
+      return Ok(None);
+    }
+  };
+
+  Ok(Some(match token {
+    Token::IDHash(id) => {
+      if state.intersects(SelectorParsingState::AFTER_PSEUDO) {
+        return Err(token_location.new_custom_error(
+          SelectorParseErrorKind::UnexpectedSelectorAfterPseudoElement(Token::IDHash(id)),
+        ));
+      }
+      let id = Component::ID(id.into());
+      SimpleSelectorParseResult::SimpleSelector(id)
+    }
+    Token::Delim('.') => {
+      if state.intersects(SelectorParsingState::AFTER_PSEUDO) {
+        return Err(token_location.new_custom_error(
+          SelectorParseErrorKind::UnexpectedSelectorAfterPseudoElement(Token::Delim('.')),
+        ));
+      }
+      let location = input.current_source_location();
+      let class = match *input.next_including_whitespace()? {
+        Token::Ident(ref class) => class.clone(),
+        ref t => {
+          let e = SelectorParseErrorKind::ClassNeedsIdent(t.clone());
+          return Err(location.new_custom_error(e));
+        }
+      };
+      let class = Component::Class(class.into());
+      SimpleSelectorParseResult::SimpleSelector(class)
+    }
+    Token::SquareBracketBlock => {
+      if state.intersects(SelectorParsingState::AFTER_PSEUDO) {
+        return Err(token_location.new_custom_error(
+          SelectorParseErrorKind::UnexpectedSelectorAfterPseudoElement(Token::SquareBracketBlock),
+        ));
+      }
+      let attr = input.parse_nested_block(|input| parse_attribute_selector(parser, input))?;
+      SimpleSelectorParseResult::SimpleSelector(attr)
+    }
+    Token::Colon => {
+      let location = input.current_source_location();
+      let (is_single_colon, next_token) = match input.next_including_whitespace()?.clone() {
+        Token::Colon => (false, input.next_including_whitespace()?.clone()),
+        t => (true, t),
+      };
+      let (name, is_functional) = match next_token {
+        Token::Ident(name) => (name, false),
+        Token::Function(name) => (name, true),
+        t => {
+          let e = SelectorParseErrorKind::PseudoElementExpectedIdent(t);
+          return Err(input.new_custom_error(e));
+        }
+      };
+      let is_pseudo_element = !is_single_colon || is_css2_pseudo_element(&name);
+      if is_pseudo_element {
+        if !state.allows_pseudos() {
+          return Err(input.new_custom_error(SelectorParseErrorKind::InvalidState));
+        }
+        let pseudo_element = if is_functional {
+          if P::parse_part(parser) && name.eq_ignore_ascii_case("part") {
+            if !state.allows_part() {
+              return Err(input.new_custom_error(SelectorParseErrorKind::InvalidState));
+            }
+            let names = input.parse_nested_block(|input| {
+              let mut result = Vec::with_capacity(1);
+              result.push(input.expect_ident_cloned()?.into());
+              while !input.is_exhausted() {
+                result.push(input.expect_ident_cloned()?.into());
+              }
+              Ok(result.into_boxed_slice())
+            })?;
+            return Ok(Some(SimpleSelectorParseResult::PartPseudo(names)));
+          }
+          if P::parse_slotted(parser) && name.eq_ignore_ascii_case("slotted") {
+            if !state.allows_slotted() {
+              return Err(input.new_custom_error(SelectorParseErrorKind::InvalidState));
+            }
+            let selector =
+              input.parse_nested_block(|input| parse_inner_compound_selector(parser, input, state))?;
+            return Ok(Some(SimpleSelectorParseResult::SlottedPseudo(selector)));
+          }
+          input.parse_nested_block(|input| P::parse_functional_pseudo_element(parser, name, input))?
+        } else {
+          P::parse_pseudo_element(parser, location, name)?
+        };
+
+        if state.intersects(SelectorParsingState::AFTER_SLOTTED) && !pseudo_element.valid_after_slotted() {
+          return Err(input.new_custom_error(SelectorParseErrorKind::InvalidState));
+        }
+        SimpleSelectorParseResult::PseudoElement(pseudo_element)
+      } else {
+        let pseudo_class = if is_functional {
+          input.parse_nested_block(|input| parse_functional_pseudo_class(parser, input, name, state))?
+        } else {
+          parse_simple_pseudo_class(parser, location, name, *state)?
+        };
+        SimpleSelectorParseResult::SimpleSelector(pseudo_class)
+      }
+    }
+    Token::Delim('&') if parser.is_nesting_allowed() => {
+      *state |= SelectorParsingState::AFTER_NESTING;
+      SimpleSelectorParseResult::SimpleSelector(Component::Nesting)
+    }
+    _ => {
+      input.reset(&start);
+      return Ok(None);
+    }
+  }))
+}
+
+fn parse_simple_pseudo_class<'i, P, Impl>(
+  parser: &P,
+  location: SourceLocation,
+  name: CowRcStr<'i>,
+  state: SelectorParsingState,
+) -> Result<Component<'i, Impl>, ParseError<'i, P::Error>>
+where
+  P: Parser<'i, Impl = Impl>,
+  Impl: SelectorImpl<'i>,
+{
+  if !state.allows_non_functional_pseudo_classes() {
+    return Err(location.new_custom_error(SelectorParseErrorKind::InvalidState));
+  }
+
+  if state.allows_tree_structural_pseudo_classes() {
+    match_ignore_ascii_case! { &name,
+        "first-child" => return Ok(Component::Nth(NthSelectorData::first(/* of_type = */ false))),
+        "last-child" => return Ok(Component::Nth(NthSelectorData::last(/* of_type = */ false))),
+        "only-child" => return Ok(Component::Nth(NthSelectorData::only(/* of_type = */ false))),
+        "root" => return Ok(Component::Root),
+        "empty" => return Ok(Component::Empty),
+        "scope" => return Ok(Component::Scope),
+        "host" if P::parse_host(parser) => return Ok(Component::Host(None)),
+        "first-of-type" => return Ok(Component::Nth(NthSelectorData::first(/* of_type = */ true))),
+        "last-of-type" => return Ok(Component::Nth(NthSelectorData::last(/* of_type = */ true))),
+        "only-of-type" => return Ok(Component::Nth(NthSelectorData::only(/* of_type = */ true))),
+        _ => {},
+    }
+  }
+
+  // The view-transition pseudo elements accept the :only-child pseudo class.
+  // https://w3c.github.io/csswg-drafts/css-view-transitions-1/#pseudo-root
+  if state.intersects(SelectorParsingState::AFTER_VIEW_TRANSITION) {
+    match_ignore_ascii_case! { &name,
+        "only-child" => return Ok(Component::Nth(NthSelectorData::only(/* of_type = */ false))),
+        _ => {}
+    }
+  }
+
+  let pseudo_class = P::parse_non_ts_pseudo_class(parser, location, name)?;
+  if state.intersects(SelectorParsingState::AFTER_WEBKIT_SCROLLBAR) {
+    if !pseudo_class.is_valid_after_webkit_scrollbar() {
+      return Err(location.new_custom_error(SelectorParseErrorKind::InvalidPseudoClassAfterWebKitScrollbar));
+    }
+  } else if state.intersects(SelectorParsingState::AFTER_PSEUDO_ELEMENT) {
+    if !pseudo_class.is_user_action_state() {
+      return Err(location.new_custom_error(SelectorParseErrorKind::InvalidPseudoClassAfterPseudoElement));
+    }
+  } else if !pseudo_class.is_valid_before_webkit_scrollbar() {
+    return Err(location.new_custom_error(SelectorParseErrorKind::InvalidPseudoClassBeforeWebKitScrollbar));
+  }
+  Ok(Component::NonTSPseudoClass(pseudo_class))
+}
+
+// NB: pub module in order to access the DummyParser
+#[cfg(test)]
+pub mod tests {
+  use super::*;
+  use crate::builder::SelectorFlags;
+  use crate::parser;
+  use cssparser::{serialize_identifier, serialize_string, Parser as CssParser, ParserInput, ToCss};
+  use std::collections::HashMap;
+  use std::fmt;
+
+  #[derive(Clone, Debug, Eq, PartialEq, Hash)]
+  pub enum PseudoClass {
+    Hover,
+    Active,
+    Lang(String),
+  }
+
+  #[derive(Clone, Debug, Eq, PartialEq, Hash)]
+  pub enum PseudoElement {
+    Before,
+    After,
+  }
+
+  impl<'i> parser::PseudoElement<'i> for PseudoElement {
+    type Impl = DummySelectorImpl;
+
+    fn accepts_state_pseudo_classes(&self) -> bool {
+      true
+    }
+
+    fn valid_after_slotted(&self) -> bool {
+      true
+    }
+  }
+
+  impl<'i> parser::NonTSPseudoClass<'i> for PseudoClass {
+    type Impl = DummySelectorImpl;
+
+    #[inline]
+    fn is_active_or_hover(&self) -> bool {
+      matches!(*self, PseudoClass::Active | PseudoClass::Hover)
+    }
+
+    #[inline]
+    fn is_user_action_state(&self) -> bool {
+      self.is_active_or_hover()
+    }
+  }
+
+  impl ToCss for PseudoClass {
+    fn to_css<W>(&self, dest: &mut W) -> fmt::Result
+    where
+      W: fmt::Write,
+    {
+      match *self {
+        PseudoClass::Hover => dest.write_str(":hover"),
+        PseudoClass::Active => dest.write_str(":active"),
+        PseudoClass::Lang(ref lang) => {
+          dest.write_str(":lang(")?;
+          serialize_identifier(lang, dest)?;
+          dest.write_char(')')
+        }
+      }
+    }
+  }
+
+  impl ToCss for PseudoElement {
+    fn to_css<W>(&self, dest: &mut W) -> fmt::Result
+    where
+      W: fmt::Write,
+    {
+      match *self {
+        PseudoElement::Before => dest.write_str("::before"),
+        PseudoElement::After => dest.write_str("::after"),
+      }
+    }
+  }
+
+  #[derive(Clone, Debug, PartialEq)]
+  pub struct DummySelectorImpl;
+
+  #[derive(Default)]
+  pub struct DummyParser {
+    default_ns: Option<DummyAtom>,
+    ns_prefixes: HashMap<DummyAtom, DummyAtom>,
+  }
+
+  impl DummyParser {
+    fn default_with_namespace(default_ns: DummyAtom) -> DummyParser {
+      DummyParser {
+        default_ns: Some(default_ns),
+        ns_prefixes: Default::default(),
+      }
+    }
+  }
+
+  impl<'i> SelectorImpl<'i> for DummySelectorImpl {
+    type ExtraMatchingData = ();
+    type AttrValue = DummyAttrValue;
+    type Identifier = DummyAtom;
+    type LocalName = DummyAtom;
+    type NamespaceUrl = DummyAtom;
+    type NamespacePrefix = DummyAtom;
+    type BorrowedLocalName = DummyAtom;
+    type BorrowedNamespaceUrl = DummyAtom;
+    type NonTSPseudoClass = PseudoClass;
+    type PseudoElement = PseudoElement;
+    type VendorPrefix = u8;
+  }
+
+  #[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
+  pub struct DummyAttrValue(String);
+
+  impl ToCss for DummyAttrValue {
+    fn to_css<W>(&self, dest: &mut W) -> fmt::Result
+    where
+      W: fmt::Write,
+    {
+      serialize_string(&self.0, dest)
+    }
+  }
+
+  impl AsRef<str> for DummyAttrValue {
+    fn as_ref(&self) -> &str {
+      self.0.as_ref()
+    }
+  }
+
+  impl<'a> From<&'a str> for DummyAttrValue {
+    fn from(string: &'a str) -> Self {
+      Self(string.into())
+    }
+  }
+
+  impl<'a> From<std::borrow::Cow<'a, str>> for DummyAttrValue {
+    fn from(string: std::borrow::Cow<'a, str>) -> Self {
+      Self(string.to_string())
+    }
+  }
+
+  impl<'a> From<CowRcStr<'a>> for DummyAttrValue {
+    fn from(string: CowRcStr<'a>) -> Self {
+      Self(string.to_string())
+    }
+  }
+
+  #[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
+  pub struct DummyAtom(String);
+
+  impl ToCss for DummyAtom {
+    fn to_css<W>(&self, dest: &mut W) -> fmt::Result
+    where
+      W: fmt::Write,
+    {
+      serialize_identifier(&self.0, dest)
+    }
+  }
+
+  impl From<String> for DummyAtom {
+    fn from(string: String) -> Self {
+      DummyAtom(string)
+    }
+  }
+
+  impl<'a> From<&'a str> for DummyAtom {
+    fn from(string: &'a str) -> Self {
+      DummyAtom(string.into())
+    }
+  }
+
+  impl<'a> From<CowRcStr<'a>> for DummyAtom {
+    fn from(string: CowRcStr<'a>) -> Self {
+      DummyAtom(string.to_string())
+    }
+  }
+
+  impl AsRef<str> for DummyAtom {
+    fn as_ref(&self) -> &str {
+      self.0.as_ref()
+    }
+  }
+
+  impl<'a> From<std::borrow::Cow<'a, str>> for DummyAtom {
+    fn from(string: std::borrow::Cow<'a, str>) -> Self {
+      Self(string.to_string())
+    }
+  }
+
+  impl<'i> Parser<'i> for DummyParser {
+    type Impl = DummySelectorImpl;
+    type Error = SelectorParseErrorKind<'i>;
+
+    fn parse_slotted(&self) -> bool {
+      true
+    }
+
+    fn parse_is_and_where(&self) -> bool {
+      true
+    }
+
+    fn is_and_where_error_recovery(&self) -> ParseErrorRecovery {
+      ParseErrorRecovery::DiscardList
+    }
+
+    fn parse_part(&self) -> bool {
+      true
+    }
+
+    fn parse_non_ts_pseudo_class(
+      &self,
+      location: SourceLocation,
+      name: CowRcStr<'i>,
+    ) -> Result<PseudoClass, SelectorParseError<'i>> {
+      match_ignore_ascii_case! { &name,
+          "hover" => return Ok(PseudoClass::Hover),
+          "active" => return Ok(PseudoClass::Active),
+          _ => {}
+      }
+      Err(location.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClass(name)))
+    }
+
+    fn parse_non_ts_functional_pseudo_class<'t>(
+      &self,
+      name: CowRcStr<'i>,
+      parser: &mut CssParser<'i, 't>,
+    ) -> Result<PseudoClass, SelectorParseError<'i>> {
+      match_ignore_ascii_case! { &name,
+          "lang" => {
+              let lang = parser.expect_ident_or_string()?.as_ref().to_owned();
+              return Ok(PseudoClass::Lang(lang));
+          },
+          _ => {}
+      }
+      Err(parser.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClass(name)))
+    }
+
+    fn parse_pseudo_element(
+      &self,
+      location: SourceLocation,
+      name: CowRcStr<'i>,
+    ) -> Result<PseudoElement, SelectorParseError<'i>> {
+      match_ignore_ascii_case! { &name,
+          "before" => return Ok(PseudoElement::Before),
+          "after" => return Ok(PseudoElement::After),
+          _ => {}
+      }
+      Err(location.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoElement(name)))
+    }
+
+    fn default_namespace(&self) -> Option<DummyAtom> {
+      self.default_ns.clone()
+    }
+
+    fn namespace_for_prefix(&self, prefix: &DummyAtom) -> Option<DummyAtom> {
+      self.ns_prefixes.get(prefix).cloned()
+    }
+  }
+
+  fn parse<'i>(input: &'i str) -> Result<SelectorList<'i, DummySelectorImpl>, SelectorParseError<'i>> {
+    parse_ns(input, &DummyParser::default())
+  }
+
+  // fn parse_expected<'i, 'a>(
+  //   input: &'i str,
+  //   expected: Option<&'a str>,
+  // ) -> Result<SelectorList<'i, DummySelectorImpl>, SelectorParseError<'i>> {
+  //   parse_ns_expected(input, &DummyParser::default(), expected)
+  // }
+
+  fn parse_ns<'i>(
+    input: &'i str,
+    parser: &DummyParser,
+  ) -> Result<SelectorList<'i, DummySelectorImpl>, SelectorParseError<'i>> {
+    parse_ns_expected(input, parser, None)
+  }
+
+  fn parse_ns_expected<'i, 'a>(
+    input: &'i str,
+    parser: &DummyParser,
+    expected: Option<&'a str>,
+  ) -> Result<SelectorList<'i, DummySelectorImpl>, SelectorParseError<'i>> {
+    let mut parser_input = ParserInput::new(input);
+    let result = SelectorList::parse(
+      parser,
+      &mut CssParser::new(&mut parser_input),
+      ParseErrorRecovery::DiscardList,
+      NestingRequirement::None,
+    );
+    if let Ok(ref selectors) = result {
+      assert_eq!(selectors.0.len(), 1);
+      // We can't assume that the serialized parsed selector will equal
+      // the input; for example, if there is no default namespace, '*|foo'
+      // should serialize to 'foo'.
+      assert_eq!(
+        selectors.0[0].to_css_string(),
+        match expected {
+          Some(x) => x,
+          None => input,
+        }
+      );
+    }
+    result
+  }
+
+  fn specificity(a: u32, b: u32, c: u32) -> u32 {
+    a << 20 | b << 10 | c
+  }
+
+  #[test]
+  fn test_empty() {
+    let mut input = ParserInput::new(":empty");
+    let list = SelectorList::parse(
+      &DummyParser::default(),
+      &mut CssParser::new(&mut input),
+      ParseErrorRecovery::DiscardList,
+      NestingRequirement::None,
+    );
+    assert!(list.is_ok());
+  }
+
+  const MATHML: &str = "http://www.w3.org/1998/Math/MathML";
+  const SVG: &str = "http://www.w3.org/2000/svg";
+
+  #[test]
+  fn test_parsing() {
+    assert!(parse("").is_err());
+    assert!(parse(":lang(4)").is_err());
+    assert!(parse(":lang(en US)").is_err());
+    assert_eq!(
+      parse("EeÉ"),
+      Ok(SelectorList::from_vec(vec![Selector::from_vec(
+        vec![Component::LocalName(LocalName {
+          name: DummyAtom::from("EeÉ"),
+          lower_name: DummyAtom::from("eeÉ"),
+        })],
+        specificity(0, 0, 1),
+        Default::default(),
+      )]))
+    );
+    assert_eq!(
+      parse("|e"),
+      Ok(SelectorList::from_vec(vec![Selector::from_vec(
+        vec![
+          Component::ExplicitNoNamespace,
+          Component::LocalName(LocalName {
+            name: DummyAtom::from("e"),
+            lower_name: DummyAtom::from("e"),
+          }),
+        ],
+        specificity(0, 0, 1),
+        Default::default(),
+      )]))
+    );
+    // When the default namespace is not set, *| should be elided.
+    // https://github.com/servo/servo/pull/17537
+    // assert_eq!(
+    //   parse_expected("*|e", Some("e")),
+    //   Ok(SelectorList::from_vec(vec![Selector::from_vec(
+    //     vec![Component::LocalName(LocalName {
+    //       name: DummyAtom::from("e"),
+    //       lower_name: DummyAtom::from("e"),
+    //     })],
+    //     specificity(0, 0, 1),
+    //     Default::default(),
+    //   )]))
+    // );
+    // When the default namespace is set, *| should _not_ be elided (as foo
+    // is no longer equivalent to *|foo--the former is only for foo in the
+    // default namespace).
+    // https://github.com/servo/servo/issues/16020
+    // assert_eq!(
+    //   parse_ns(
+    //     "*|e",
+    //     &DummyParser::default_with_namespace(DummyAtom::from("https://mozilla.org"))
+    //   ),
+    //   Ok(SelectorList::from_vec(vec![Selector::from_vec(
+    //     vec![
+    //       Component::ExplicitAnyNamespace,
+    //       Component::LocalName(LocalName {
+    //         name: DummyAtom::from("e"),
+    //         lower_name: DummyAtom::from("e"),
+    //       }),
+    //     ],
+    //     specificity(0, 0, 1),
+    //     Default::default(),
+    //   )]))
+    // );
+    assert_eq!(
+      parse("*"),
+      Ok(SelectorList::from_vec(vec![Selector::from_vec(
+        vec![Component::ExplicitUniversalType],
+        specificity(0, 0, 0),
+        Default::default(),
+      )]))
+    );
+    assert_eq!(
+      parse("|*"),
+      Ok(SelectorList::from_vec(vec![Selector::from_vec(
+        vec![Component::ExplicitNoNamespace, Component::ExplicitUniversalType,],
+        specificity(0, 0, 0),
+        Default::default(),
+      )]))
+    );
+    // assert_eq!(
+    //   parse_expected("*|*", Some("*")),
+    //   Ok(SelectorList::from_vec(vec![Selector::from_vec(
+    //     vec![Component::ExplicitUniversalType],
+    //     specificity(0, 0, 0),
+    //     Default::default(),
+    //   )]))
+    // );
+    assert_eq!(
+      parse_ns(
+        "*|*",
+        &DummyParser::default_with_namespace(DummyAtom::from("https://mozilla.org"))
+      ),
+      Ok(SelectorList::from_vec(vec![Selector::from_vec(
+        vec![Component::ExplicitAnyNamespace, Component::ExplicitUniversalType,],
+        specificity(0, 0, 0),
+        Default::default(),
+      )]))
+    );
+    assert_eq!(
+      parse(".foo:lang(en-US)"),
+      Ok(SelectorList::from_vec(vec![Selector::from_vec(
+        vec![
+          Component::Class(DummyAtom::from("foo")),
+          Component::NonTSPseudoClass(PseudoClass::Lang("en-US".to_owned())),
+        ],
+        specificity(0, 2, 0),
+        Default::default(),
+      )]))
+    );
+    assert_eq!(
+      parse("#bar"),
+      Ok(SelectorList::from_vec(vec![Selector::from_vec(
+        vec![Component::ID(DummyAtom::from("bar"))],
+        specificity(1, 0, 0),
+        Default::default(),
+      )]))
+    );
+    assert_eq!(
+      parse("e.foo#bar"),
+      Ok(SelectorList::from_vec(vec![Selector::from_vec(
+        vec![
+          Component::LocalName(LocalName {
+            name: DummyAtom::from("e"),
+            lower_name: DummyAtom::from("e"),
+          }),
+          Component::Class(DummyAtom::from("foo")),
+          Component::ID(DummyAtom::from("bar")),
+        ],
+        specificity(1, 1, 1),
+        Default::default(),
+      )]))
+    );
+    assert_eq!(
+      parse("e.foo #bar"),
+      Ok(SelectorList::from_vec(vec![Selector::from_vec(
+        vec![
+          Component::LocalName(LocalName {
+            name: DummyAtom::from("e"),
+            lower_name: DummyAtom::from("e"),
+          }),
+          Component::Class(DummyAtom::from("foo")),
+          Component::Combinator(Combinator::Descendant),
+          Component::ID(DummyAtom::from("bar")),
+        ],
+        specificity(1, 1, 1),
+        Default::default(),
+      )]))
+    );
+    // Default namespace does not apply to attribute selectors
+    // https://github.com/mozilla/servo/pull/1652
+    let mut parser = DummyParser::default();
+    assert_eq!(
+      parse_ns("[Foo]", &parser),
+      Ok(SelectorList::from_vec(vec![Selector::from_vec(
+        vec![Component::AttributeInNoNamespaceExists {
+          local_name: DummyAtom::from("Foo"),
+          local_name_lower: DummyAtom::from("foo"),
+        }],
+        specificity(0, 1, 0),
+        Default::default(),
+      )]))
+    );
+    assert!(parse_ns("svg|circle", &parser).is_err());
+    parser.ns_prefixes.insert(DummyAtom("svg".into()), DummyAtom(SVG.into()));
+    assert_eq!(
+      parse_ns("svg|circle", &parser),
+      Ok(SelectorList::from_vec(vec![Selector::from_vec(
+        vec![
+          Component::Namespace(DummyAtom("svg".into()), SVG.into()),
+          Component::LocalName(LocalName {
+            name: DummyAtom::from("circle"),
+            lower_name: DummyAtom::from("circle"),
+          }),
+        ],
+        specificity(0, 0, 1),
+        Default::default(),
+      )]))
+    );
+    assert_eq!(
+      parse_ns("svg|*", &parser),
+      Ok(SelectorList::from_vec(vec![Selector::from_vec(
+        vec![
+          Component::Namespace(DummyAtom("svg".into()), SVG.into()),
+          Component::ExplicitUniversalType,
+        ],
+        specificity(0, 0, 0),
+        Default::default(),
+      )]))
+    );
+    // Default namespace does not apply to attribute selectors
+    // https://github.com/mozilla/servo/pull/1652
+    // but it does apply to implicit type selectors
+    // https://github.com/servo/rust-selectors/pull/82
+    parser.default_ns = Some(MATHML.into());
+    assert_eq!(
+      parse_ns("[Foo]", &parser),
+      Ok(SelectorList::from_vec(vec![Selector::from_vec(
+        vec![
+          Component::DefaultNamespace(MATHML.into()),
+          Component::AttributeInNoNamespaceExists {
+            local_name: DummyAtom::from("Foo"),
+            local_name_lower: DummyAtom::from("foo"),
+          },
+        ],
+        specificity(0, 1, 0),
+        Default::default(),
+      )]))
+    );
+    // Default namespace does apply to type selectors
+    assert_eq!(
+      parse_ns("e", &parser),
+      Ok(SelectorList::from_vec(vec![Selector::from_vec(
+        vec![
+          Component::DefaultNamespace(MATHML.into()),
+          Component::LocalName(LocalName {
+            name: DummyAtom::from("e"),
+            lower_name: DummyAtom::from("e"),
+          }),
+        ],
+        specificity(0, 0, 1),
+        Default::default(),
+      )]))
+    );
+    assert_eq!(
+      parse_ns("*", &parser),
+      Ok(SelectorList::from_vec(vec![Selector::from_vec(
+        vec![
+          Component::DefaultNamespace(MATHML.into()),
+          Component::ExplicitUniversalType,
+        ],
+        specificity(0, 0, 0),
+        Default::default(),
+      )]))
+    );
+    assert_eq!(
+      parse_ns("*|*", &parser),
+      Ok(SelectorList::from_vec(vec![Selector::from_vec(
+        vec![Component::ExplicitAnyNamespace, Component::ExplicitUniversalType,],
+        specificity(0, 0, 0),
+        Default::default(),
+      )]))
+    );
+    // Default namespace applies to universal and type selectors inside :not and :matches,
+    // but not otherwise.
+    assert_eq!(
+      parse_ns(":not(.cl)", &parser),
+      Ok(SelectorList::from_vec(vec![Selector::from_vec(
+        vec![
+          Component::DefaultNamespace(MATHML.into()),
+          Component::Negation(
+            vec![Selector::from_vec(
+              vec![Component::Class(DummyAtom::from("cl"))],
+              specificity(0, 1, 0),
+              Default::default(),
+            )]
+            .into_boxed_slice()
+          ),
+        ],
+        specificity(0, 1, 0),
+        Default::default(),
+      )]))
+    );
+    assert_eq!(
+      parse_ns(":not(*)", &parser),
+      Ok(SelectorList::from_vec(vec![Selector::from_vec(
+        vec![
+          Component::DefaultNamespace(MATHML.into()),
+          Component::Negation(
+            vec![Selector::from_vec(
+              vec![
+                Component::DefaultNamespace(MATHML.into()),
+                Component::ExplicitUniversalType,
+              ],
+              specificity(0, 0, 0),
+              Default::default(),
+            )]
+            .into_boxed_slice(),
+          ),
+        ],
+        specificity(0, 0, 0),
+        Default::default(),
+      )]))
+    );
+    assert_eq!(
+      parse_ns(":not(e)", &parser),
+      Ok(SelectorList::from_vec(vec![Selector::from_vec(
+        vec![
+          Component::DefaultNamespace(MATHML.into()),
+          Component::Negation(
+            vec![Selector::from_vec(
+              vec![
+                Component::DefaultNamespace(MATHML.into()),
+                Component::LocalName(LocalName {
+                  name: DummyAtom::from("e"),
+                  lower_name: DummyAtom::from("e"),
+                }),
+              ],
+              specificity(0, 0, 1),
+              Default::default(),
+            ),]
+            .into_boxed_slice()
+          ),
+        ],
+        specificity(0, 0, 1),
+        Default::default(),
+      )]))
+    );
+    assert_eq!(
+      parse("[attr|=\"foo\"]"),
+      Ok(SelectorList::from_vec(vec![Selector::from_vec(
+        vec![Component::AttributeInNoNamespace {
+          local_name: DummyAtom::from("attr"),
+          operator: AttrSelectorOperator::DashMatch,
+          value: DummyAttrValue::from("foo"),
+          never_matches: false,
+          case_sensitivity: ParsedCaseSensitivity::CaseSensitive,
+        }],
+        specificity(0, 1, 0),
+        Default::default(),
+      )]))
+    );
+    // https://github.com/mozilla/servo/issues/1723
+    assert_eq!(
+      parse("::before"),
+      Ok(SelectorList::from_vec(vec![Selector::from_vec(
+        vec![
+          Component::Combinator(Combinator::PseudoElement),
+          Component::PseudoElement(PseudoElement::Before),
+        ],
+        specificity(0, 0, 1),
+        SelectorFlags::HAS_PSEUDO,
+      )]))
+    );
+    assert_eq!(
+      parse("::before:hover"),
+      Ok(SelectorList::from_vec(vec![Selector::from_vec(
+        vec![
+          Component::Combinator(Combinator::PseudoElement),
+          Component::PseudoElement(PseudoElement::Before),
+          Component::NonTSPseudoClass(PseudoClass::Hover),
+        ],
+        specificity(0, 1, 1),
+        SelectorFlags::HAS_PSEUDO,
+      )]))
+    );
+    assert_eq!(
+      parse("::before:hover:hover"),
+      Ok(SelectorList::from_vec(vec![Selector::from_vec(
+        vec![
+          Component::Combinator(Combinator::PseudoElement),
+          Component::PseudoElement(PseudoElement::Before),
+          Component::NonTSPseudoClass(PseudoClass::Hover),
+          Component::NonTSPseudoClass(PseudoClass::Hover),
+        ],
+        specificity(0, 2, 1),
+        SelectorFlags::HAS_PSEUDO,
+      )]))
+    );
+    assert!(parse("::before:hover:lang(foo)").is_err());
+    assert!(parse("::before:hover .foo").is_err());
+    assert!(parse("::before .foo").is_err());
+    assert!(parse("::before ~ bar").is_err());
+    assert!(parse("::before:active").is_ok());
+
+    // https://github.com/servo/servo/issues/15335
+    assert!(parse(":: before").is_err());
+    assert_eq!(
+      parse("div ::after"),
+      Ok(SelectorList::from_vec(vec![Selector::from_vec(
+        vec![
+          Component::LocalName(LocalName {
+            name: DummyAtom::from("div"),
+            lower_name: DummyAtom::from("div"),
+          }),
+          Component::Combinator(Combinator::Descendant),
+          Component::Combinator(Combinator::PseudoElement),
+          Component::PseudoElement(PseudoElement::After),
+        ],
+        specificity(0, 0, 2),
+        SelectorFlags::HAS_PSEUDO,
+      )]))
+    );
+    assert_eq!(
+      parse("#d1 > .ok"),
+      Ok(SelectorList::from_vec(vec![Selector::from_vec(
+        vec![
+          Component::ID(DummyAtom::from("d1")),
+          Component::Combinator(Combinator::Child),
+          Component::Class(DummyAtom::from("ok")),
+        ],
+        (1 << 20) + (1 << 10) + (0 << 0),
+        Default::default(),
+      )]))
+    );
+    parser.default_ns = None;
+    assert!(parse(":not(#provel.old)").is_ok());
+    assert!(parse(":not(#provel > old)").is_ok());
+    assert!(parse("table[rules]:not([rules=\"none\"]):not([rules=\"\"])").is_ok());
+    // https://github.com/servo/servo/issues/16017
+    assert_eq!(
+      parse_ns(":not(*)", &parser),
+      Ok(SelectorList::from_vec(vec![Selector::from_vec(
+        vec![Component::Negation(
+          vec![Selector::from_vec(
+            vec![Component::ExplicitUniversalType],
+            specificity(0, 0, 0),
+            Default::default(),
+          )]
+          .into_boxed_slice()
+        )],
+        specificity(0, 0, 0),
+        Default::default(),
+      )]))
+    );
+    assert_eq!(
+      parse_ns(":not(|*)", &parser),
+      Ok(SelectorList::from_vec(vec![Selector::from_vec(
+        vec![Component::Negation(
+          vec![Selector::from_vec(
+            vec![Component::ExplicitNoNamespace, Component::ExplicitUniversalType,],
+            specificity(0, 0, 0),
+            Default::default(),
+          )]
+          .into_boxed_slice(),
+        )],
+        specificity(0, 0, 0),
+        Default::default(),
+      )]))
+    );
+    // *| should be elided if there is no default namespace.
+    // https://github.com/servo/servo/pull/17537
+    // assert_eq!(
+    //   parse_ns_expected(":not(*|*)", &parser, Some(":not(*)")),
+    //   Ok(SelectorList::from_vec(vec![Selector::from_vec(
+    //     vec![Component::Negation(
+    //       vec![Selector::from_vec(
+    //         vec![Component::ExplicitUniversalType],
+    //         specificity(0, 0, 0),
+    //         Default::default()
+    //       )]
+    //       .into_boxed_slice()
+    //     )],
+    //     specificity(0, 0, 0),
+    //     Default::default(),
+    //   )]))
+    // );
+
+    assert!(parse("::slotted()").is_err());
+    assert!(parse("::slotted(div)").is_ok());
+    assert!(parse("::slotted(div).foo").is_err());
+    assert!(parse("::slotted(div + bar)").is_err());
+    assert!(parse("::slotted(div) + foo").is_err());
+
+    assert!(parse("::part()").is_err());
+    assert!(parse("::part(42)").is_err());
+    assert!(parse("::part(foo bar)").is_ok());
+    assert!(parse("::part(foo):hover").is_ok());
+    assert!(parse("::part(foo) + bar").is_err());
+
+    assert!(parse("div ::slotted(div)").is_ok());
+    assert!(parse("div + slot::slotted(div)").is_ok());
+    assert!(parse("div + slot::slotted(div.foo)").is_ok());
+    assert!(parse("slot::slotted(div,foo)::first-line").is_err());
+    assert!(parse("::slotted(div)::before").is_ok());
+    assert!(parse("slot::slotted(div,foo)").is_err());
+
+    assert!(parse("foo:where()").is_err());
+    assert!(parse("foo:where(div, foo, .bar baz)").is_ok());
+    assert!(parse("foo:where(::before)").is_err());
+
+    assert!(parse("foo::details-content").is_ok());
+    assert!(parse("foo::target-text").is_ok());
+
+    assert!(parse("select::picker").is_err());
+    assert!(parse("::picker()").is_err());
+    assert!(parse("::picker(select)").is_ok());
+    assert!(parse("select::picker-icon").is_ok());
+    assert!(parse("option::checkmark").is_ok());
+  }
+
+  #[test]
+  fn test_pseudo_iter() {
+    let selector = &parse("q::before").unwrap().0[0];
+    assert!(!selector.is_universal());
+    let mut iter = selector.iter();
+    assert_eq!(iter.next(), Some(&Component::PseudoElement(PseudoElement::Before)));
+    assert_eq!(iter.next(), None);
+    let combinator = iter.next_sequence();
+    assert_eq!(combinator, Some(Combinator::PseudoElement));
+    assert!(matches!(iter.next(), Some(&Component::LocalName(..))));
+    assert_eq!(iter.next(), None);
+    assert_eq!(iter.next_sequence(), None);
+  }
+
+  #[test]
+  fn test_universal() {
+    let selector = &parse_ns(
+      "*|*::before",
+      &DummyParser::default_with_namespace(DummyAtom::from("https://mozilla.org")),
+    )
+    .unwrap()
+    .0[0];
+    assert!(selector.is_universal());
+  }
+
+  #[test]
+  fn test_empty_pseudo_iter() {
+    let selector = &parse("::before").unwrap().0[0];
+    assert!(selector.is_universal());
+    let mut iter = selector.iter();
+    assert_eq!(iter.next(), Some(&Component::PseudoElement(PseudoElement::Before)));
+    assert_eq!(iter.next(), None);
+    assert_eq!(iter.next_sequence(), Some(Combinator::PseudoElement));
+    assert_eq!(iter.next(), None);
+    assert_eq!(iter.next_sequence(), None);
+  }
+
+  struct TestVisitor {
+    seen: Vec<String>,
+  }
+
+  impl<'i> SelectorVisitor<'i> for TestVisitor {
+    type Impl = DummySelectorImpl;
+
+    fn visit_simple_selector(&mut self, s: &Component<DummySelectorImpl>) -> bool {
+      let mut dest = String::new();
+      s.to_css(&mut dest).unwrap();
+      self.seen.push(dest);
+      true
+    }
+  }
+
+  #[test]
+  fn visitor() {
+    let mut test_visitor = TestVisitor { seen: vec![] };
+    parse(":not(:hover) ~ label").unwrap().0[0].visit(&mut test_visitor);
+    assert!(test_visitor.seen.contains(&":hover".into()));
+
+    let mut test_visitor = TestVisitor { seen: vec![] };
+    parse("::before:hover").unwrap().0[0].visit(&mut test_visitor);
+    assert!(test_visitor.seen.contains(&":hover".into()));
+  }
+}
diff --git a/selectors/serialization.rs b/selectors/serialization.rs
new file mode 100644
index 0000000..102ab56
--- /dev/null
+++ b/selectors/serialization.rs
@@ -0,0 +1,730 @@
+use crate::{
+  attr::{
+    AttrSelectorOperator, AttrSelectorWithOptionalNamespace, NamespaceConstraint, ParsedAttrSelectorOperation,
+    ParsedCaseSensitivity,
+  },
+  builder::SelectorBuilder,
+  parser::{Combinator, Component, LocalName, NthOfSelectorData, NthSelectorData, NthType, Selector},
+  SelectorImpl,
+};
+use std::borrow::Cow;
+
+use cssparser::CowRcStr;
+#[cfg(feature = "jsonschema")]
+use schemars::JsonSchema;
+
+#[derive(serde::Serialize, serde::Deserialize)]
+#[serde(tag = "type", rename_all = "kebab-case")]
+#[cfg_attr(
+  feature = "jsonschema",
+  derive(schemars::JsonSchema),
+  schemars(
+    rename = "SelectorComponent",
+    bound = "Impl: JsonSchema, Impl::NonTSPseudoClass: schemars::JsonSchema, Impl::PseudoElement: schemars::JsonSchema, Impl::VendorPrefix: schemars::JsonSchema, PseudoClass: schemars::JsonSchema, PseudoElement: schemars::JsonSchema, VendorPrefix: schemars::JsonSchema"
+  )
+)]
+enum SerializedComponent<'i, 's, Impl: SelectorImpl<'s>, PseudoClass, PseudoElement, VendorPrefix> {
+  Combinator {
+    value: Combinator,
+  },
+  Universal,
+  #[serde(borrow)]
+  Namespace(Namespace<'i>),
+  Type {
+    name: Cow<'i, str>,
+  },
+  #[serde(rename = "id")]
+  ID {
+    name: Cow<'i, str>,
+  },
+  Class {
+    name: Cow<'i, str>,
+  },
+  Attribute(AttrSelector<'i>),
+  #[serde(
+    borrow,
+    bound(
+      serialize = "PseudoClass: serde::Serialize, Impl::NonTSPseudoClass: serde::Serialize, Impl::PseudoElement: serde::Serialize, Impl::VendorPrefix: serde::Serialize, VendorPrefix: serde::Serialize",
+      deserialize = "PseudoClass: serde::Deserialize<'de>, Impl::NonTSPseudoClass: serde::Deserialize<'de>, Impl::PseudoElement: serde::Deserialize<'de>, Impl::VendorPrefix: serde::Deserialize<'de>, VendorPrefix: serde::Deserialize<'de>"
+    )
+  )]
+  PseudoClass(SerializedPseudoClass<'s, Impl, PseudoClass, VendorPrefix>),
+  #[serde(
+    borrow,
+    bound(
+      serialize = "PseudoElement: serde::Serialize",
+      deserialize = "PseudoElement: serde::Deserialize<'de>"
+    )
+  )]
+  PseudoElement(SerializedPseudoElement<'i, 's, Impl, PseudoElement>),
+  Nesting,
+}
+
+#[derive(serde::Serialize, serde::Deserialize)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[serde(tag = "kind", rename_all = "kebab-case")]
+enum Namespace<'i> {
+  None,
+  Any,
+  Named { prefix: Cow<'i, str> },
+}
+
+#[derive(serde::Serialize, serde::Deserialize)]
+#[serde(tag = "kind", rename_all = "kebab-case")]
+#[cfg_attr(
+  feature = "jsonschema",
+  derive(schemars::JsonSchema),
+  schemars(
+    rename = "TSPseudoClass",
+    bound = "Impl: JsonSchema, Impl::NonTSPseudoClass: schemars::JsonSchema, Impl::PseudoElement: schemars::JsonSchema, Impl::VendorPrefix: schemars::JsonSchema, VendorPrefix: schemars::JsonSchema"
+  )
+)]
+enum TSPseudoClass<'s, Impl: SelectorImpl<'s>, VendorPrefix> {
+  Not {
+    #[serde(
+      borrow,
+      bound(
+        serialize = "Impl::NonTSPseudoClass: serde::Serialize, Impl::PseudoElement: serde::Serialize",
+        deserialize = "Impl::NonTSPseudoClass: serde::Deserialize<'de>, Impl::PseudoElement: serde::Deserialize<'de>"
+      )
+    )]
+    selectors: Box<[Selector<'s, Impl>]>,
+  },
+  FirstChild,
+  LastChild,
+  OnlyChild,
+  Root,
+  Empty,
+  Scope,
+  NthChild {
+    a: i32,
+    b: i32,
+    #[serde(
+      borrow,
+      bound(
+        serialize = "Impl::NonTSPseudoClass: serde::Serialize, Impl::PseudoElement: serde::Serialize, Impl::VendorPrefix: serde::Serialize",
+        deserialize = "Impl::NonTSPseudoClass: serde::Deserialize<'de>, Impl::PseudoElement: serde::Deserialize<'de>, Impl::VendorPrefix: serde::Deserialize<'de>"
+      )
+    )]
+    of: Option<Box<[Selector<'s, Impl>]>>,
+  },
+  NthLastChild {
+    a: i32,
+    b: i32,
+    #[serde(
+      borrow,
+      bound(
+        serialize = "Impl::NonTSPseudoClass: serde::Serialize, Impl::PseudoElement: serde::Serialize, Impl::VendorPrefix: serde::Serialize",
+        deserialize = "Impl::NonTSPseudoClass: serde::Deserialize<'de>, Impl::PseudoElement: serde::Deserialize<'de>, Impl::VendorPrefix: serde::Deserialize<'de>"
+      )
+    )]
+    of: Option<Box<[Selector<'s, Impl>]>>,
+  },
+  NthCol {
+    a: i32,
+    b: i32,
+  },
+  NthLastCol {
+    a: i32,
+    b: i32,
+  },
+  NthOfType {
+    a: i32,
+    b: i32,
+  },
+  NthLastOfType {
+    a: i32,
+    b: i32,
+  },
+  FirstOfType,
+  LastOfType,
+  OnlyOfType,
+  Host {
+    #[serde(
+      borrow,
+      bound(
+        serialize = "Impl::NonTSPseudoClass: serde::Serialize, Impl::PseudoElement: serde::Serialize, Impl::VendorPrefix: serde::Serialize",
+        deserialize = "Impl::NonTSPseudoClass: serde::Deserialize<'de>, Impl::PseudoElement: serde::Deserialize<'de>, Impl::VendorPrefix: serde::Deserialize<'de>"
+      )
+    )]
+    selectors: Option<Selector<'s, Impl>>,
+  },
+  Where {
+    #[serde(
+      borrow,
+      bound(
+        serialize = "Impl::NonTSPseudoClass: serde::Serialize, Impl::PseudoElement: serde::Serialize, Impl::VendorPrefix: serde::Serialize",
+        deserialize = "Impl::NonTSPseudoClass: serde::Deserialize<'de>, Impl::PseudoElement: serde::Deserialize<'de>, Impl::VendorPrefix: serde::Deserialize<'de>"
+      )
+    )]
+    selectors: Box<[Selector<'s, Impl>]>,
+  },
+  Is {
+    #[serde(
+      borrow,
+      bound(
+        serialize = "Impl::NonTSPseudoClass: serde::Serialize, Impl::PseudoElement: serde::Serialize, Impl::VendorPrefix: serde::Serialize",
+        deserialize = "Impl::NonTSPseudoClass: serde::Deserialize<'de>, Impl::PseudoElement: serde::Deserialize<'de>, Impl::VendorPrefix: serde::Deserialize<'de>"
+      )
+    )]
+    selectors: Box<[Selector<'s, Impl>]>,
+  },
+  #[serde(rename_all = "camelCase")]
+  Any {
+    vendor_prefix: VendorPrefix,
+    #[serde(
+      borrow,
+      bound(
+        serialize = "Impl::NonTSPseudoClass: serde::Serialize, Impl::PseudoElement: serde::Serialize, Impl::VendorPrefix: serde::Serialize",
+        deserialize = "Impl::NonTSPseudoClass: serde::Deserialize<'de>, Impl::PseudoElement: serde::Deserialize<'de>, Impl::VendorPrefix: serde::Deserialize<'de>"
+      )
+    )]
+    selectors: Box<[Selector<'s, Impl>]>,
+  },
+  Has {
+    #[serde(
+      borrow,
+      bound(
+        serialize = "Impl::NonTSPseudoClass: serde::Serialize, Impl::PseudoElement: serde::Serialize, Impl::VendorPrefix: serde::Serialize",
+        deserialize = "Impl::NonTSPseudoClass: serde::Deserialize<'de>, Impl::PseudoElement: serde::Deserialize<'de>, Impl::VendorPrefix: serde::Deserialize<'de>"
+      )
+    )]
+    selectors: Box<[Selector<'s, Impl>]>,
+  },
+}
+
+#[derive(serde::Serialize, serde::Deserialize)]
+#[serde(untagged, rename_all = "kebab-case")]
+#[cfg_attr(
+  feature = "jsonschema",
+  derive(schemars::JsonSchema),
+  schemars(
+    rename = "PseudoClass",
+    bound = "Impl: JsonSchema, Impl::NonTSPseudoClass: schemars::JsonSchema, Impl::PseudoElement: schemars::JsonSchema, Impl::VendorPrefix: schemars::JsonSchema, PseudoClass: schemars::JsonSchema, VendorPrefix: schemars::JsonSchema"
+  )
+)]
+enum SerializedPseudoClass<'s, Impl: SelectorImpl<'s>, PseudoClass, VendorPrefix> {
+  #[serde(
+    borrow,
+    bound(
+      serialize = "Impl::NonTSPseudoClass: serde::Serialize, Impl::PseudoElement: serde::Serialize, Impl::VendorPrefix: serde::Serialize, VendorPrefix: serde::Serialize",
+      deserialize = "Impl::NonTSPseudoClass: serde::Deserialize<'de>, Impl::PseudoElement: serde::Deserialize<'de>, Impl::VendorPrefix: serde::Deserialize<'de>, VendorPrefix: serde::Deserialize<'de>"
+    )
+  )]
+  TS(TSPseudoClass<'s, Impl, VendorPrefix>),
+  NonTS(PseudoClass),
+}
+
+#[derive(serde::Serialize, serde::Deserialize)]
+#[serde(tag = "kind", rename_all = "kebab-case")]
+#[cfg_attr(
+  feature = "jsonschema",
+  derive(schemars::JsonSchema),
+  schemars(
+    rename = "BuiltinPseudoElement",
+    bound = "Impl: JsonSchema, Impl::NonTSPseudoClass: schemars::JsonSchema, Impl::PseudoElement: schemars::JsonSchema, Impl::VendorPrefix: schemars::JsonSchema"
+  )
+)]
+enum BuiltinPseudoElement<'i, 's, Impl: SelectorImpl<'s>> {
+  Slotted {
+    #[serde(
+      borrow,
+      bound(
+        serialize = "Impl::NonTSPseudoClass: serde::Serialize, Impl::PseudoElement: serde::Serialize, Impl::VendorPrefix: serde::Serialize",
+        deserialize = "Impl::NonTSPseudoClass: serde::Deserialize<'de>, Impl::PseudoElement: serde::Deserialize<'de>, Impl::VendorPrefix: serde::Deserialize<'de>"
+      )
+    )]
+    selector: Selector<'s, Impl>,
+  },
+  Part {
+    names: Vec<Cow<'i, str>>,
+  },
+}
+
+#[derive(serde::Serialize, serde::Deserialize)]
+#[serde(untagged, rename_all = "kebab-case")]
+#[cfg_attr(
+  feature = "jsonschema",
+  derive(schemars::JsonSchema),
+  schemars(
+    rename = "PseudoElement",
+    bound = "Impl: JsonSchema, Impl::NonTSPseudoClass: schemars::JsonSchema, Impl::PseudoElement: schemars::JsonSchema, Impl::VendorPrefix: schemars::JsonSchema, PseudoElement: schemars::JsonSchema"
+  )
+)]
+enum SerializedPseudoElement<'i, 's, Impl: SelectorImpl<'s>, PseudoElement> {
+  #[serde(
+    borrow,
+    bound(
+      serialize = "Impl::NonTSPseudoClass: serde::Serialize, Impl::PseudoElement: serde::Serialize, Impl::VendorPrefix: serde::Serialize",
+      deserialize = "Impl::NonTSPseudoClass: serde::Deserialize<'de>, Impl::PseudoElement: serde::Deserialize<'de>, Impl::VendorPrefix: serde::Deserialize<'de>"
+    )
+  )]
+  Builtin(BuiltinPseudoElement<'i, 's, Impl>),
+  Custom(PseudoElement),
+}
+
+#[derive(serde::Serialize, serde::Deserialize)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+struct AttrSelector<'i> {
+  #[serde(borrow)]
+  namespace: Option<NamespaceConstraint<NamespaceValue<'i>>>,
+  name: Cow<'i, str>,
+  operation: Option<AttrOperation<'i>>,
+}
+
+#[derive(serde::Serialize, serde::Deserialize)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+struct NamespaceValue<'i> {
+  prefix: Cow<'i, str>,
+  url: Cow<'i, str>,
+}
+
+#[derive(serde::Serialize, serde::Deserialize)]
+#[serde(rename_all = "camelCase")]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+struct AttrOperation<'i> {
+  operator: AttrSelectorOperator,
+  value: Cow<'i, str>,
+  #[serde(default)]
+  case_sensitivity: ParsedCaseSensitivity,
+}
+
+impl<'i, Impl: SelectorImpl<'i>> serde::Serialize for Component<'i, Impl>
+where
+  Impl::NonTSPseudoClass: serde::Serialize,
+  Impl::PseudoElement: serde::Serialize,
+  Impl::VendorPrefix: serde::Serialize,
+{
+  fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+  where
+    S: serde::Serializer,
+  {
+    let c: SerializedComponent<'_, 'i, Impl, _, _, _> = match self {
+      Component::Combinator(c) => SerializedComponent::Combinator { value: c.clone() },
+      Component::ExplicitUniversalType => SerializedComponent::Universal,
+      Component::ExplicitAnyNamespace => SerializedComponent::Namespace(Namespace::Any),
+      Component::ExplicitNoNamespace => SerializedComponent::Namespace(Namespace::None),
+      // can't actually happen anymore.
+      Component::DefaultNamespace(_url) => SerializedComponent::Namespace(Namespace::Any),
+      Component::Namespace(prefix, _url) => SerializedComponent::Namespace(Namespace::Named {
+        prefix: prefix.as_ref().into(),
+      }),
+      Component::LocalName(name) => SerializedComponent::Type {
+        name: name.name.as_ref().into(),
+      },
+      Component::ID(name) => SerializedComponent::ID {
+        name: name.as_ref().into(),
+      },
+      Component::Class(name) => SerializedComponent::Class {
+        name: name.as_ref().into(),
+      },
+      Component::AttributeInNoNamespace {
+        local_name,
+        operator,
+        value,
+        case_sensitivity,
+        ..
+      } => SerializedComponent::Attribute(AttrSelector {
+        namespace: None,
+        name: local_name.as_ref().into(),
+        operation: Some(AttrOperation {
+          operator: operator.clone(),
+          case_sensitivity: case_sensitivity.clone(),
+          value: value.as_ref().into(),
+        }),
+      }),
+      Component::AttributeInNoNamespaceExists { local_name, .. } => SerializedComponent::Attribute(AttrSelector {
+        namespace: None,
+        name: local_name.as_ref().into(),
+        operation: None,
+      }),
+      Component::AttributeOther(other) => SerializedComponent::Attribute(AttrSelector {
+        namespace: other.namespace.as_ref().map(|namespace| match namespace {
+          NamespaceConstraint::Any => NamespaceConstraint::Any,
+          NamespaceConstraint::Specific(s) => NamespaceConstraint::Specific(NamespaceValue {
+            prefix: s.0.as_ref().into(),
+            url: s.1.as_ref().into(),
+          }),
+        }),
+        name: other.local_name.as_ref().into(),
+        operation: match &other.operation {
+          ParsedAttrSelectorOperation::Exists => None,
+          ParsedAttrSelectorOperation::WithValue {
+            operator,
+            case_sensitivity,
+            expected_value,
+          } => Some(AttrOperation {
+            operator: operator.clone(),
+            case_sensitivity: case_sensitivity.clone(),
+            value: expected_value.as_ref().into(),
+          }),
+        },
+      }),
+      Component::NonTSPseudoClass(c) => SerializedComponent::PseudoClass(SerializedPseudoClass::NonTS(c)),
+      Component::Negation(s) => {
+        SerializedComponent::PseudoClass(SerializedPseudoClass::TS(TSPseudoClass::Not { selectors: s.clone() }))
+      }
+      Component::Root => SerializedComponent::PseudoClass(SerializedPseudoClass::TS(TSPseudoClass::Root)),
+      Component::Empty => SerializedComponent::PseudoClass(SerializedPseudoClass::TS(TSPseudoClass::Empty)),
+      Component::Scope => SerializedComponent::PseudoClass(SerializedPseudoClass::TS(TSPseudoClass::Scope)),
+      Component::Nth(nth) => serialize_nth(nth, None),
+      Component::NthOf(nth) => serialize_nth(nth.nth_data(), Some(nth.clone_selectors())),
+      Component::Host(s) => {
+        SerializedComponent::PseudoClass(SerializedPseudoClass::TS(TSPseudoClass::Host { selectors: s.clone() }))
+      }
+      Component::Where(s) => {
+        SerializedComponent::PseudoClass(SerializedPseudoClass::TS(TSPseudoClass::Where { selectors: s.clone() }))
+      }
+      Component::Is(s) => {
+        SerializedComponent::PseudoClass(SerializedPseudoClass::TS(TSPseudoClass::Is { selectors: s.clone() }))
+      }
+      Component::Any(v, s) => SerializedComponent::PseudoClass(SerializedPseudoClass::TS(TSPseudoClass::Any {
+        vendor_prefix: v.clone(),
+        selectors: s.clone(),
+      })),
+      Component::Has(s) => {
+        SerializedComponent::PseudoClass(SerializedPseudoClass::TS(TSPseudoClass::Has { selectors: s.clone() }))
+      }
+      Component::PseudoElement(e) => SerializedComponent::PseudoElement(SerializedPseudoElement::Custom(e)),
+      Component::Slotted(s) => {
+        SerializedComponent::PseudoElement(SerializedPseudoElement::Builtin(BuiltinPseudoElement::Slotted {
+          selector: s.clone(),
+        }))
+      }
+      Component::Part(p) => {
+        SerializedComponent::PseudoElement(SerializedPseudoElement::Builtin(BuiltinPseudoElement::Part {
+          names: p.iter().map(|name| name.as_ref().into()).collect(),
+        }))
+      }
+      Component::Nesting => SerializedComponent::Nesting,
+    };
+
+    c.serialize(serializer)
+  }
+}
+
+fn serialize_nth<'i, 's, Impl: SelectorImpl<'s>>(
+  nth: &NthSelectorData,
+  of: Option<Box<[Selector<'s, Impl>]>>,
+) -> SerializedComponent<'i, 's, Impl, &'s Impl::NonTSPseudoClass, &'s Impl::PseudoElement, Impl::VendorPrefix> {
+  match nth.ty {
+    NthType::Child if nth.is_function => {
+      SerializedComponent::PseudoClass(SerializedPseudoClass::TS(TSPseudoClass::NthChild {
+        a: nth.a,
+        b: nth.b,
+        of,
+      }))
+    }
+    NthType::Child => SerializedComponent::PseudoClass(SerializedPseudoClass::TS(TSPseudoClass::FirstChild)),
+    NthType::LastChild if nth.is_function => {
+      SerializedComponent::PseudoClass(SerializedPseudoClass::TS(TSPseudoClass::NthLastChild {
+        a: nth.a,
+        b: nth.b,
+        of,
+      }))
+    }
+    NthType::LastChild => SerializedComponent::PseudoClass(SerializedPseudoClass::TS(TSPseudoClass::LastChild)),
+    NthType::OfType if nth.is_function => {
+      SerializedComponent::PseudoClass(SerializedPseudoClass::TS(TSPseudoClass::NthOfType {
+        a: nth.a,
+        b: nth.b,
+      }))
+    }
+    NthType::OfType => SerializedComponent::PseudoClass(SerializedPseudoClass::TS(TSPseudoClass::FirstOfType)),
+    NthType::LastOfType if nth.is_function => {
+      SerializedComponent::PseudoClass(SerializedPseudoClass::TS(TSPseudoClass::NthLastOfType {
+        a: nth.a,
+        b: nth.b,
+      }))
+    }
+    NthType::LastOfType => SerializedComponent::PseudoClass(SerializedPseudoClass::TS(TSPseudoClass::LastOfType)),
+    NthType::OnlyChild => SerializedComponent::PseudoClass(SerializedPseudoClass::TS(TSPseudoClass::OnlyChild)),
+    NthType::OnlyOfType => SerializedComponent::PseudoClass(SerializedPseudoClass::TS(TSPseudoClass::OnlyOfType)),
+    NthType::Col => {
+      SerializedComponent::PseudoClass(SerializedPseudoClass::TS(TSPseudoClass::NthCol { a: nth.a, b: nth.b }))
+    }
+    NthType::LastCol => SerializedComponent::PseudoClass(SerializedPseudoClass::TS(TSPseudoClass::NthLastCol {
+      a: nth.a,
+      b: nth.b,
+    })),
+  }
+}
+
+impl<'de: 'i, 'i, Impl: SelectorImpl<'i>> serde::Deserialize<'de> for Component<'i, Impl>
+where
+  Impl::NonTSPseudoClass: serde::Deserialize<'de>,
+  Impl::PseudoElement: serde::Deserialize<'de>,
+  Impl::VendorPrefix: serde::Deserialize<'de>,
+{
+  fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+  where
+    D: serde::Deserializer<'de>,
+  {
+    let c: SerializedComponent<'i, '_, Impl, _, _, _> = SerializedComponent::deserialize(deserializer)?;
+    Ok(match c {
+      SerializedComponent::Combinator { value } => Component::Combinator(value),
+      SerializedComponent::Universal => Component::ExplicitUniversalType,
+      SerializedComponent::Namespace(n) => match n {
+        Namespace::Any => Component::ExplicitAnyNamespace,
+        Namespace::None => Component::ExplicitNoNamespace,
+        Namespace::Named { prefix } => Component::Namespace(prefix.into(), CowRcStr::from("").into()),
+      },
+      SerializedComponent::Type { name } => {
+        let name: Impl::LocalName = name.into();
+        Component::LocalName(LocalName {
+          name: name.clone(),
+          lower_name: name,
+        })
+      }
+      SerializedComponent::ID { name } => Component::ID(name.into()),
+      SerializedComponent::Class { name } => Component::Class(name.into()),
+      SerializedComponent::Attribute(attr) => {
+        let (local_name_lower_cow, local_name_is_ascii_lowercase) =
+          if let Some(first_uppercase) = attr.name.bytes().position(|byte| byte >= b'A' && byte <= b'Z') {
+            let mut string = attr.name.to_string();
+            string[first_uppercase..].make_ascii_lowercase();
+            (string.into(), false)
+          } else {
+            (attr.name.clone(), true)
+          };
+
+        if attr.namespace.is_some() || (!local_name_is_ascii_lowercase && attr.operation.is_some()) {
+          Component::AttributeOther(Box::new(AttrSelectorWithOptionalNamespace {
+            namespace: match attr.namespace {
+              Some(NamespaceConstraint::Any) => Some(NamespaceConstraint::Any),
+              Some(NamespaceConstraint::Specific(c)) => {
+                Some(NamespaceConstraint::Specific((c.prefix.into(), c.url.into())))
+              }
+              None => None,
+            },
+            local_name: attr.name.into(),
+            local_name_lower: local_name_lower_cow.into(),
+            operation: match attr.operation {
+              None => ParsedAttrSelectorOperation::Exists,
+              Some(AttrOperation {
+                operator,
+                case_sensitivity,
+                value,
+              }) => ParsedAttrSelectorOperation::WithValue {
+                operator,
+                case_sensitivity,
+                expected_value: value.into(),
+              },
+            },
+            never_matches: false, // TODO
+          }))
+        } else {
+          match attr.operation {
+            None => Component::AttributeInNoNamespaceExists {
+              local_name: attr.name.into(),
+              local_name_lower: local_name_lower_cow.into(),
+            },
+            Some(AttrOperation {
+              operator,
+              case_sensitivity,
+              value,
+            }) => Component::AttributeInNoNamespace {
+              local_name: attr.name.into(),
+              operator,
+              value: value.into(),
+              case_sensitivity,
+              never_matches: false, // TODO
+            },
+          }
+        }
+      }
+      SerializedComponent::PseudoClass(c) => match c {
+        SerializedPseudoClass::NonTS(c) => Component::NonTSPseudoClass(c),
+        SerializedPseudoClass::TS(TSPseudoClass::Not { selectors }) => Component::Negation(selectors),
+        SerializedPseudoClass::TS(TSPseudoClass::FirstChild) => Component::Nth(NthSelectorData::first(false)),
+        SerializedPseudoClass::TS(TSPseudoClass::LastChild) => Component::Nth(NthSelectorData::last(false)),
+        SerializedPseudoClass::TS(TSPseudoClass::OnlyChild) => Component::Nth(NthSelectorData::only(false)),
+        SerializedPseudoClass::TS(TSPseudoClass::Root) => Component::Root,
+        SerializedPseudoClass::TS(TSPseudoClass::Empty) => Component::Empty,
+        SerializedPseudoClass::TS(TSPseudoClass::Scope) => Component::Scope,
+        SerializedPseudoClass::TS(TSPseudoClass::FirstOfType) => Component::Nth(NthSelectorData::first(true)),
+        SerializedPseudoClass::TS(TSPseudoClass::LastOfType) => Component::Nth(NthSelectorData::last(true)),
+        SerializedPseudoClass::TS(TSPseudoClass::OnlyOfType) => Component::Nth(NthSelectorData::only(true)),
+        SerializedPseudoClass::TS(
+          ref c @ TSPseudoClass::NthChild { a, b, ref of } | ref c @ TSPseudoClass::NthLastChild { a, b, ref of },
+        ) => {
+          let data = NthSelectorData {
+            ty: match c {
+              TSPseudoClass::NthChild { .. } => NthType::Child,
+              TSPseudoClass::NthLastChild { .. } => NthType::LastChild,
+              _ => unreachable!(),
+            },
+            is_function: true,
+            a,
+            b,
+          };
+          match of {
+            Some(of) => Component::NthOf(NthOfSelectorData::new(data, of.clone())),
+            None => Component::Nth(data),
+          }
+        }
+        SerializedPseudoClass::TS(
+          ref c @ TSPseudoClass::NthCol { a, b }
+          | ref c @ TSPseudoClass::NthLastCol { a, b }
+          | ref c @ TSPseudoClass::NthOfType { a, b }
+          | ref c @ TSPseudoClass::NthLastOfType { a, b },
+        ) => Component::Nth(NthSelectorData {
+          ty: match c {
+            TSPseudoClass::NthCol { .. } => NthType::Col,
+            TSPseudoClass::NthLastCol { .. } => NthType::LastCol,
+            TSPseudoClass::NthOfType { .. } => NthType::OfType,
+            TSPseudoClass::NthLastOfType { .. } => NthType::LastOfType,
+            _ => unreachable!(),
+          },
+          is_function: true,
+          a,
+          b,
+        }),
+        SerializedPseudoClass::TS(TSPseudoClass::Host { selectors }) => Component::Host(selectors),
+        SerializedPseudoClass::TS(TSPseudoClass::Where { selectors }) => Component::Where(selectors),
+        SerializedPseudoClass::TS(TSPseudoClass::Is { selectors }) => Component::Is(selectors),
+        SerializedPseudoClass::TS(TSPseudoClass::Any {
+          vendor_prefix,
+          selectors,
+        }) => Component::Any(vendor_prefix, selectors),
+        SerializedPseudoClass::TS(TSPseudoClass::Has { selectors }) => Component::Has(selectors),
+      },
+      SerializedComponent::PseudoElement(value) => match value {
+        SerializedPseudoElement::Custom(e) => Component::PseudoElement(e),
+        SerializedPseudoElement::Builtin(BuiltinPseudoElement::Part { names }) => {
+          Component::Part(names.into_iter().map(|name| name.into()).collect())
+        }
+        SerializedPseudoElement::Builtin(BuiltinPseudoElement::Slotted { selector }) => {
+          Component::Slotted(selector)
+        }
+      },
+      SerializedComponent::Nesting => Component::Nesting,
+    })
+  }
+}
+
+impl<'i, Impl: SelectorImpl<'i>> serde::Serialize for Selector<'i, Impl>
+where
+  Impl::NonTSPseudoClass: serde::Serialize,
+  Impl::VendorPrefix: serde::Serialize,
+  Impl::PseudoElement: serde::Serialize,
+{
+  fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+  where
+    S: serde::Serializer,
+  {
+    use serde::ser::SerializeSeq;
+    let skipped_combinators = self
+      .iter_raw_match_order()
+      .filter(|c| {
+        matches!(
+          c,
+          Component::Combinator(Combinator::Part | Combinator::PseudoElement | Combinator::SlotAssignment)
+        )
+      })
+      .count();
+    let mut seq = serializer.serialize_seq(Some(self.len() - skipped_combinators))?;
+
+    let mut combinators = self.iter_raw_match_order().rev().filter(|x| x.is_combinator());
+    let compound_selectors = self.iter_raw_match_order().as_slice().split(|x| x.is_combinator()).rev();
+
+    for compound in compound_selectors {
+      if compound.is_empty() {
+        continue;
+      }
+
+      for component in compound {
+        seq.serialize_element(component)?;
+      }
+
+      if let Some(combinator) = combinators.next() {
+        if !matches!(
+          combinator,
+          Component::Combinator(Combinator::Part | Combinator::PseudoElement | Combinator::SlotAssignment)
+        ) {
+          seq.serialize_element(combinator)?;
+        }
+      }
+    }
+    seq.end()
+  }
+}
+
+impl<'de: 'i, 'i, Impl: SelectorImpl<'i>> serde::Deserialize<'de> for Selector<'i, Impl>
+where
+  Impl::NonTSPseudoClass: serde::Deserialize<'de>,
+  Impl::VendorPrefix: serde::Deserialize<'de>,
+  Impl::PseudoElement: serde::Deserialize<'de>,
+{
+  fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+  where
+    D: serde::Deserializer<'de>,
+  {
+    #[cfg(feature = "serde")]
+    struct SelectorVisitor<'i, Impl: SelectorImpl<'i>> {
+      marker: std::marker::PhantomData<Selector<'i, Impl>>,
+    }
+
+    #[cfg(feature = "serde")]
+    impl<'de: 'i, 'i, Impl: SelectorImpl<'i>> serde::de::Visitor<'de> for SelectorVisitor<'i, Impl>
+    where
+      Impl::NonTSPseudoClass: serde::Deserialize<'de>,
+      Impl::VendorPrefix: serde::Deserialize<'de>,
+      Impl::PseudoElement: serde::Deserialize<'de>,
+    {
+      type Value = Selector<'i, Impl>;
+
+      fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
+        formatter.write_str("a list of components")
+      }
+
+      fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
+      where
+        A: serde::de::SeqAccess<'de>,
+      {
+        let mut builder = SelectorBuilder::default();
+        while let Some(component) = seq.next_element::<Component<'i, Impl>>()? {
+          if let Some(combinator) = component.as_combinator() {
+            builder.push_combinator(combinator);
+          } else {
+            match component {
+              Component::Slotted(_) => builder.push_combinator(Combinator::SlotAssignment),
+              Component::Part(_) => builder.push_combinator(Combinator::Part),
+              Component::PseudoElement(_) => builder.push_combinator(Combinator::PseudoElement),
+              _ => {}
+            }
+            builder.push_simple_selector(component);
+          }
+        }
+
+        let (spec, components) = builder.build(false, false, false);
+        Ok(Selector::new(spec, components))
+      }
+    }
+
+    deserializer.deserialize_seq(SelectorVisitor {
+      marker: std::marker::PhantomData,
+    })
+  }
+}
+
+#[cfg(feature = "jsonschema")]
+impl<'i, Impl: SelectorImpl<'i>> schemars::JsonSchema for Selector<'i, Impl>
+where
+  Impl: schemars::JsonSchema,
+  Impl::NonTSPseudoClass: schemars::JsonSchema,
+  Impl::PseudoElement: schemars::JsonSchema,
+  Impl::VendorPrefix: schemars::JsonSchema,
+{
+  fn is_referenceable() -> bool {
+    true
+  }
+
+  fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
+    Vec::<SerializedComponent<'_, '_, Impl, Impl::NonTSPseudoClass, Impl::PseudoElement, Impl::VendorPrefix>>::json_schema(gen)
+  }
+
+  fn schema_name() -> String {
+    "Selector".into()
+  }
+}
diff --git a/selectors/sink.rs b/selectors/sink.rs
new file mode 100644
index 0000000..e8701c8
--- /dev/null
+++ b/selectors/sink.rs
@@ -0,0 +1,31 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Small helpers to abstract over different containers.
+#![deny(missing_docs)]
+
+use smallvec::{Array, SmallVec};
+
+/// A trait to abstract over a `push` method that may be implemented for
+/// different kind of types.
+///
+/// Used to abstract over `Array`, `SmallVec` and `Vec`, and also to implement a
+/// type which `push` method does only tweak a byte when we only need to check
+/// for the presence of something.
+pub trait Push<T> {
+  /// Push a value into self.
+  fn push(&mut self, value: T);
+}
+
+impl<T> Push<T> for Vec<T> {
+  fn push(&mut self, value: T) {
+    Vec::push(self, value);
+  }
+}
+
+impl<A: Array> Push<A::Item> for SmallVec<A> {
+  fn push(&mut self, value: A::Item) {
+    SmallVec::push(self, value);
+  }
+}
diff --git a/selectors/tree.rs b/selectors/tree.rs
new file mode 100644
index 0000000..a7d2d7d
--- /dev/null
+++ b/selectors/tree.rs
@@ -0,0 +1,139 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Traits that nodes must implement. Breaks the otherwise-cyclic dependency
+//! between layout and style.
+
+use crate::attr::{AttrSelectorOperation, CaseSensitivity, NamespaceConstraint};
+use crate::matching::{ElementSelectorFlags, MatchingContext};
+use crate::parser::SelectorImpl;
+use std::fmt::Debug;
+use std::ptr::NonNull;
+
+/// Opaque representation of an Element, for identity comparisons.
+#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
+pub struct OpaqueElement(NonNull<()>);
+
+unsafe impl Send for OpaqueElement {}
+
+impl OpaqueElement {
+  /// Creates a new OpaqueElement from an arbitrarily-typed pointer.
+  pub fn new<T>(ptr: &T) -> Self {
+    unsafe { OpaqueElement(NonNull::new_unchecked(ptr as *const T as *const () as *mut ())) }
+  }
+}
+
+pub trait Element<'i>: Sized + Clone + Debug {
+  type Impl: SelectorImpl<'i>;
+
+  /// Converts self into an opaque representation.
+  fn opaque(&self) -> OpaqueElement;
+
+  fn parent_element(&self) -> Option<Self>;
+
+  /// Whether the parent node of this element is a shadow root.
+  fn parent_node_is_shadow_root(&self) -> bool;
+
+  /// The host of the containing shadow root, if any.
+  fn containing_shadow_host(&self) -> Option<Self>;
+
+  /// The parent of a given pseudo-element, after matching a pseudo-element
+  /// selector.
+  ///
+  /// This is guaranteed to be called in a pseudo-element.
+  fn pseudo_element_originating_element(&self) -> Option<Self> {
+    debug_assert!(self.is_pseudo_element());
+    self.parent_element()
+  }
+
+  /// Whether we're matching on a pseudo-element.
+  fn is_pseudo_element(&self) -> bool;
+
+  /// Skips non-element nodes
+  fn prev_sibling_element(&self) -> Option<Self>;
+
+  /// Skips non-element nodes
+  fn next_sibling_element(&self) -> Option<Self>;
+
+  fn is_html_element_in_html_document(&self) -> bool;
+
+  fn has_local_name(&self, local_name: &<Self::Impl as SelectorImpl<'i>>::BorrowedLocalName) -> bool;
+
+  /// Empty string for no namespace
+  fn has_namespace(&self, ns: &<Self::Impl as SelectorImpl<'i>>::BorrowedNamespaceUrl) -> bool;
+
+  /// Whether this element and the `other` element have the same local name and namespace.
+  fn is_same_type(&self, other: &Self) -> bool;
+
+  fn attr_matches(
+    &self,
+    ns: &NamespaceConstraint<&<Self::Impl as SelectorImpl<'i>>::NamespaceUrl>,
+    local_name: &<Self::Impl as SelectorImpl<'i>>::LocalName,
+    operation: &AttrSelectorOperation<&<Self::Impl as SelectorImpl<'i>>::AttrValue>,
+  ) -> bool;
+
+  fn match_non_ts_pseudo_class<F>(
+    &self,
+    pc: &<Self::Impl as SelectorImpl<'i>>::NonTSPseudoClass,
+    context: &mut MatchingContext<'_, 'i, Self::Impl>,
+    flags_setter: &mut F,
+  ) -> bool
+  where
+    F: FnMut(&Self, ElementSelectorFlags);
+
+  fn match_pseudo_element(
+    &self,
+    pe: &<Self::Impl as SelectorImpl<'i>>::PseudoElement,
+    context: &mut MatchingContext<'_, 'i, Self::Impl>,
+  ) -> bool;
+
+  /// Whether this element is a `link`.
+  fn is_link(&self) -> bool;
+
+  /// Returns whether the element is an HTML <slot> element.
+  fn is_html_slot_element(&self) -> bool;
+
+  /// Returns the assigned <slot> element this element is assigned to.
+  ///
+  /// Necessary for the `::slotted` pseudo-class.
+  fn assigned_slot(&self) -> Option<Self> {
+    None
+  }
+
+  fn has_id(&self, id: &<Self::Impl as SelectorImpl<'i>>::Identifier, case_sensitivity: CaseSensitivity) -> bool;
+
+  fn has_class(
+    &self,
+    name: &<Self::Impl as SelectorImpl<'i>>::Identifier,
+    case_sensitivity: CaseSensitivity,
+  ) -> bool;
+
+  /// Returns the mapping from the `exportparts` attribute in the reverse
+  /// direction, that is, in an outer-tree -> inner-tree direction.
+  fn imported_part(
+    &self,
+    name: &<Self::Impl as SelectorImpl<'i>>::Identifier,
+  ) -> Option<<Self::Impl as SelectorImpl<'i>>::Identifier>;
+
+  fn is_part(&self, name: &<Self::Impl as SelectorImpl<'i>>::Identifier) -> bool;
+
+  /// Returns whether this element matches `:empty`.
+  ///
+  /// That is, whether it does not contain any child element or any non-zero-length text node.
+  /// See http://dev.w3.org/csswg/selectors-3/#empty-pseudo
+  fn is_empty(&self) -> bool;
+
+  /// Returns whether this element matches `:root`,
+  /// i.e. whether it is the root element of a document.
+  ///
+  /// Note: this can be false even if `.parent_element()` is `None`
+  /// if the parent node is a `DocumentFragment`.
+  fn is_root(&self) -> bool;
+
+  /// Returns whether this element should ignore matching nth child
+  /// selector.
+  fn ignores_nth_child_selectors(&self) -> bool {
+    false
+  }
+}
diff --git a/selectors/visitor.rs b/selectors/visitor.rs
new file mode 100644
index 0000000..3ed03ef
--- /dev/null
+++ b/selectors/visitor.rs
@@ -0,0 +1,57 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Visitor traits for selectors.
+
+#![deny(missing_docs)]
+
+use crate::attr::NamespaceConstraint;
+use crate::parser::{Combinator, Component, Selector, SelectorImpl};
+
+/// A trait to visit selector properties.
+///
+/// All the `visit_foo` methods return a boolean indicating whether the
+/// traversal should continue or not.
+pub trait SelectorVisitor<'i>: Sized {
+  /// The selector implementation this visitor wants to visit.
+  type Impl: SelectorImpl<'i>;
+
+  /// Visit an attribute selector that may match (there are other selectors
+  /// that may never match, like those containing whitespace or the empty
+  /// string).
+  fn visit_attribute_selector(
+    &mut self,
+    _namespace: &NamespaceConstraint<&<Self::Impl as SelectorImpl<'i>>::NamespaceUrl>,
+    _local_name: &<Self::Impl as SelectorImpl<'i>>::LocalName,
+    _local_name_lower: &<Self::Impl as SelectorImpl<'i>>::LocalName,
+  ) -> bool {
+    true
+  }
+
+  /// Visit a simple selector.
+  fn visit_simple_selector(&mut self, _: &Component<'i, Self::Impl>) -> bool {
+    true
+  }
+
+  /// Visit a nested selector list. The caller is responsible to call visit
+  /// into the internal selectors if / as needed.
+  ///
+  /// The default implementation does this.
+  fn visit_selector_list(&mut self, list: &[Selector<'i, Self::Impl>]) -> bool {
+    for nested in list {
+      if !nested.visit(self) {
+        return false;
+      }
+    }
+    true
+  }
+
+  /// Visits a complex selector.
+  ///
+  /// Gets the combinator to the right of the selector, or `None` if the
+  /// selector is the rightmost one.
+  fn visit_complex_selector(&mut self, _combinator_to_right: Option<Combinator>) -> bool {
+    true
+  }
+}
diff --git a/src/bundler.rs b/src/bundler.rs
new file mode 100644
index 0000000..0098848
--- /dev/null
+++ b/src/bundler.rs
@@ -0,0 +1,2117 @@
+//! CSS bundling.
+//!
+//! A [Bundler](Bundler) can be used to combine a CSS file and all of its dependencies
+//! into a single merged style sheet. It works together with a [SourceProvider](SourceProvider)
+//! (e.g. [FileProvider](FileProvider)) to read files from the file system or another source,
+//! and returns a [StyleSheet](super::stylesheet::StyleSheet) containing the rules from all
+//! of the dependencies of the entry file, recursively.
+//!
+//! Rules are bundled following `@import` order, and wrapped in the necessary `@media`, `@supports`,
+//! and `@layer` rules as appropriate to preserve the authored behavior.
+//!
+//! # Example
+//!
+//! ```no_run
+//! use std::path::Path;
+//! use lightningcss::{
+//!   bundler::{Bundler, FileProvider},
+//!   stylesheet::ParserOptions
+//! };
+//!
+//! let fs = FileProvider::new();
+//! let mut bundler = Bundler::new(&fs, None, ParserOptions::default());
+//! let stylesheet = bundler.bundle(Path::new("style.css")).unwrap();
+//! ```
+
+use crate::{
+  error::ErrorLocation,
+  parser::DefaultAtRuleParser,
+  properties::{
+    css_modules::Specifier,
+    custom::{
+      CustomProperty, EnvironmentVariableName, TokenList, TokenOrValue, UnparsedProperty, UnresolvedColor,
+    },
+    Property,
+  },
+  rules::{
+    layer::{LayerBlockRule, LayerName},
+    Location,
+  },
+  traits::{AtRuleParser, ToCss},
+  values::ident::DashedIdentReference,
+};
+use crate::{
+  error::{Error, ParserError},
+  media_query::MediaList,
+  rules::{
+    import::ImportRule,
+    media::MediaRule,
+    supports::{SupportsCondition, SupportsRule},
+    CssRule, CssRuleList,
+  },
+  stylesheet::{ParserOptions, StyleSheet},
+};
+use dashmap::DashMap;
+use parcel_sourcemap::SourceMap;
+use rayon::prelude::*;
+use std::{
+  collections::HashSet,
+  fs,
+  path::{Path, PathBuf},
+  sync::Mutex,
+};
+
+/// A Bundler combines a CSS file and all imported dependencies together into
+/// a single merged style sheet.
+pub struct Bundler<'a, 'o, 's, P, T: AtRuleParser<'a>> {
+  source_map: Option<Mutex<&'s mut SourceMap>>,
+  fs: &'a P,
+  source_indexes: DashMap<PathBuf, u32>,
+  stylesheets: Mutex<Vec<BundleStyleSheet<'a, 'o, T::AtRule>>>,
+  options: ParserOptions<'o, 'a>,
+  at_rule_parser: Mutex<AtRuleParserValue<'s, T>>,
+}
+
+enum AtRuleParserValue<'a, T> {
+  Owned(T),
+  Borrowed(&'a mut T),
+}
+
+struct BundleStyleSheet<'i, 'o, T> {
+  stylesheet: Option<StyleSheet<'i, 'o, T>>,
+  dependencies: Vec<u32>,
+  css_modules_deps: Vec<u32>,
+  parent_source_index: u32,
+  parent_dep_index: u32,
+  layer: Option<Option<LayerName<'i>>>,
+  supports: Option<SupportsCondition<'i>>,
+  media: MediaList<'i>,
+  loc: Location,
+}
+
+/// A trait to provide the contents of files to a Bundler.
+///
+/// See [FileProvider](FileProvider) for an implementation that uses the
+/// file system.
+pub trait SourceProvider: Send + Sync {
+  /// A custom error.
+  type Error: std::error::Error + Send + Sync;
+
+  /// Reads the contents of the given file path to a string.
+  fn read<'a>(&'a self, file: &Path) -> Result<&'a str, Self::Error>;
+
+  /// Resolves the given import specifier to a file path given the file
+  /// which the import originated from.
+  fn resolve(&self, specifier: &str, originating_file: &Path) -> Result<PathBuf, Self::Error>;
+}
+
+/// Provides an implementation of [SourceProvider](SourceProvider)
+/// that reads files from the file system.
+pub struct FileProvider {
+  inputs: Mutex<Vec<*mut String>>,
+}
+
+impl FileProvider {
+  /// Creates a new FileProvider.
+  pub fn new() -> FileProvider {
+    FileProvider {
+      inputs: Mutex::new(Vec::new()),
+    }
+  }
+}
+
+unsafe impl Sync for FileProvider {}
+unsafe impl Send for FileProvider {}
+
+impl SourceProvider for FileProvider {
+  type Error = std::io::Error;
+
+  fn read<'a>(&'a self, file: &Path) -> Result<&'a str, Self::Error> {
+    let source = fs::read_to_string(file)?;
+    let ptr = Box::into_raw(Box::new(source));
+    self.inputs.lock().unwrap().push(ptr);
+    // SAFETY: this is safe because the pointer is not dropped
+    // until the FileProvider is, and we never remove from the
+    // list of pointers stored in the vector.
+    Ok(unsafe { &*ptr })
+  }
+
+  fn resolve(&self, specifier: &str, originating_file: &Path) -> Result<PathBuf, Self::Error> {
+    // Assume the specifier is a relative file path and join it with current path.
+    Ok(originating_file.with_file_name(specifier))
+  }
+}
+
+impl Drop for FileProvider {
+  fn drop(&mut self) {
+    for ptr in self.inputs.lock().unwrap().iter() {
+      std::mem::drop(unsafe { Box::from_raw(*ptr) })
+    }
+  }
+}
+
+/// An error that could occur during bundling.
+#[derive(Debug)]
+#[cfg_attr(any(feature = "serde", feature = "nodejs"), derive(serde::Serialize))]
+pub enum BundleErrorKind<'i, T: std::error::Error> {
+  /// A parser error occurred.
+  ParserError(ParserError<'i>),
+  /// An unsupported `@import` condition was encountered.
+  UnsupportedImportCondition,
+  /// An unsupported cascade layer combination was encountered.
+  UnsupportedLayerCombination,
+  /// Unsupported media query boolean logic was encountered.
+  UnsupportedMediaBooleanLogic,
+  /// A custom resolver error.
+  ResolverError(#[cfg_attr(any(feature = "serde", feature = "nodejs"), serde(skip))] T),
+}
+
+impl<'i, T: std::error::Error> From<Error<ParserError<'i>>> for Error<BundleErrorKind<'i, T>> {
+  fn from(err: Error<ParserError<'i>>) -> Self {
+    Error {
+      kind: BundleErrorKind::ParserError(err.kind),
+      loc: err.loc,
+    }
+  }
+}
+
+impl<'i, T: std::error::Error> std::fmt::Display for BundleErrorKind<'i, T> {
+  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+    use BundleErrorKind::*;
+    match self {
+      ParserError(err) => err.fmt(f),
+      UnsupportedImportCondition => write!(f, "Unsupported import condition"),
+      UnsupportedLayerCombination => write!(f, "Unsupported layer combination in @import"),
+      UnsupportedMediaBooleanLogic => write!(f, "Unsupported boolean logic in @import media query"),
+      ResolverError(err) => std::fmt::Display::fmt(&err, f),
+    }
+  }
+}
+
+impl<'i, T: std::error::Error> BundleErrorKind<'i, T> {
+  #[deprecated(note = "use `BundleErrorKind::to_string()` or `std::fmt::Display` instead")]
+  #[allow(missing_docs)]
+  pub fn reason(&self) -> String {
+    self.to_string()
+  }
+}
+
+impl<'a, 'o, 's, P: SourceProvider> Bundler<'a, 'o, 's, P, DefaultAtRuleParser> {
+  /// Creates a new Bundler using the given source provider.
+  /// If a source map is given, the content of each source file included in the bundle will
+  /// be added accordingly.
+  pub fn new(
+    fs: &'a P,
+    source_map: Option<&'s mut SourceMap>,
+    options: ParserOptions<'o, 'a>,
+  ) -> Bundler<'a, 'o, 's, P, DefaultAtRuleParser> {
+    Bundler {
+      source_map: source_map.map(Mutex::new),
+      fs,
+      source_indexes: DashMap::new(),
+      stylesheets: Mutex::new(Vec::new()),
+      options,
+      at_rule_parser: Mutex::new(AtRuleParserValue::Owned(DefaultAtRuleParser)),
+    }
+  }
+}
+
+impl<'a, 'o, 's, P: SourceProvider, T: AtRuleParser<'a> + Clone + Sync + Send> Bundler<'a, 'o, 's, P, T>
+where
+  T::AtRule: Sync + Send + ToCss + Clone,
+{
+  /// Creates a new Bundler using the given source provider.
+  /// If a source map is given, the content of each source file included in the bundle will
+  /// be added accordingly.
+  pub fn new_with_at_rule_parser(
+    fs: &'a P,
+    source_map: Option<&'s mut SourceMap>,
+    options: ParserOptions<'o, 'a>,
+    at_rule_parser: &'s mut T,
+  ) -> Self {
+    Bundler {
+      source_map: source_map.map(Mutex::new),
+      fs,
+      source_indexes: DashMap::new(),
+      stylesheets: Mutex::new(Vec::new()),
+      options,
+      at_rule_parser: Mutex::new(AtRuleParserValue::Borrowed(at_rule_parser)),
+    }
+  }
+
+  /// Bundles the given entry file and all dependencies into a single style sheet.
+  pub fn bundle<'e>(
+    &mut self,
+    entry: &'e Path,
+  ) -> Result<StyleSheet<'a, 'o, T::AtRule>, Error<BundleErrorKind<'a, P::Error>>> {
+    // Phase 1: load and parse all files. This is done in parallel.
+    self.load_file(
+      &entry,
+      ImportRule {
+        url: "".into(),
+        layer: None,
+        supports: None,
+        media: MediaList::new(),
+        loc: Location {
+          source_index: 0,
+          line: 0,
+          column: 0,
+        },
+      },
+    )?;
+
+    // Phase 2: determine the order that the files should be concatenated.
+    self.order();
+
+    // Phase 3: concatenate.
+    let mut rules: Vec<CssRule<'a, T::AtRule>> = Vec::new();
+    self.inline(&mut rules);
+
+    let sources = self
+      .stylesheets
+      .get_mut()
+      .unwrap()
+      .iter()
+      .flat_map(|s| s.stylesheet.as_ref().unwrap().sources.iter().cloned())
+      .collect();
+
+    let mut stylesheet = StyleSheet::new(sources, CssRuleList(rules), self.options.clone());
+
+    stylesheet.source_map_urls = self
+      .stylesheets
+      .get_mut()
+      .unwrap()
+      .iter()
+      .flat_map(|s| s.stylesheet.as_ref().unwrap().source_map_urls.iter().cloned())
+      .collect();
+
+    stylesheet.license_comments = self
+      .stylesheets
+      .get_mut()
+      .unwrap()
+      .iter()
+      .flat_map(|s| s.stylesheet.as_ref().unwrap().license_comments.iter().cloned())
+      .collect();
+
+    if let Some(config) = &self.options.css_modules {
+      if config.pattern.has_content_hash() {
+        stylesheet.content_hashes = Some(
+          self
+            .stylesheets
+            .get_mut()
+            .unwrap()
+            .iter()
+            .flat_map(|s| {
+              let s = s.stylesheet.as_ref().unwrap();
+              s.content_hashes.as_ref().unwrap().iter().cloned()
+            })
+            .collect(),
+        );
+      }
+    }
+
+    Ok(stylesheet)
+  }
+
+  fn find_filename(&self, source_index: u32) -> String {
+    // This function is only used for error handling, so it's ok if this is a bit slow.
+    let entry = self.source_indexes.iter().find(|x| *x.value() == source_index).unwrap();
+    entry.key().to_str().unwrap().into()
+  }
+
+  fn load_file(&self, file: &Path, rule: ImportRule<'a>) -> Result<u32, Error<BundleErrorKind<'a, P::Error>>> {
+    // Check if we already loaded this file.
+    let mut stylesheets = self.stylesheets.lock().unwrap();
+    let source_index = match self.source_indexes.get(file) {
+      Some(source_index) => {
+        // If we already loaded this file, combine the media queries and supports conditions
+        // from this import rule with the existing ones using a logical or operator.
+        let entry = &mut stylesheets[*source_index as usize];
+
+        // We cannot combine a media query and a supports query from different @import rules.
+        // e.g. @import "a.css" print; @import "a.css" supports(color: red);
+        // This would require duplicating the actual rules in the file.
+        if (!rule.media.media_queries.is_empty() && !entry.supports.is_none())
+          || (!entry.media.media_queries.is_empty() && !rule.supports.is_none())
+        {
+          return Err(Error {
+            kind: BundleErrorKind::UnsupportedImportCondition,
+            loc: Some(ErrorLocation::new(rule.loc, self.find_filename(rule.loc.source_index))),
+          });
+        }
+
+        if rule.media.media_queries.is_empty() {
+          entry.media.media_queries.clear();
+        } else if !entry.media.media_queries.is_empty() {
+          entry.media.or(&rule.media);
+        }
+
+        if let Some(supports) = rule.supports {
+          if let Some(existing_supports) = &mut entry.supports {
+            existing_supports.or(&supports)
+          }
+        } else {
+          entry.supports = None;
+        }
+
+        if let Some(layer) = &rule.layer {
+          if let Some(existing_layer) = &entry.layer {
+            // We can't OR layer names without duplicating all of the nested rules, so error for now.
+            if layer != existing_layer || (layer.is_none() && existing_layer.is_none()) {
+              return Err(Error {
+                kind: BundleErrorKind::UnsupportedLayerCombination,
+                loc: Some(ErrorLocation::new(rule.loc, self.find_filename(rule.loc.source_index))),
+              });
+            }
+          } else {
+            entry.layer = rule.layer;
+          }
+        }
+
+        return Ok(*source_index);
+      }
+      None => {
+        let source_index = stylesheets.len() as u32;
+        self.source_indexes.insert(file.to_owned(), source_index);
+
+        stylesheets.push(BundleStyleSheet {
+          stylesheet: None,
+          layer: rule.layer.clone(),
+          media: rule.media.clone(),
+          supports: rule.supports.clone(),
+          loc: rule.loc.clone(),
+          dependencies: Vec::new(),
+          css_modules_deps: Vec::new(),
+          parent_source_index: 0,
+          parent_dep_index: 0,
+        });
+
+        source_index
+      }
+    };
+
+    drop(stylesheets); // ensure we aren't holding the lock anymore
+
+    let code = self.fs.read(file).map_err(|e| Error {
+      kind: BundleErrorKind::ResolverError(e),
+      loc: if rule.loc.column == 0 {
+        None
+      } else {
+        Some(ErrorLocation::new(rule.loc, self.find_filename(rule.loc.source_index)))
+      },
+    })?;
+
+    let mut opts = self.options.clone();
+    let filename = file.to_str().unwrap();
+    opts.filename = filename.to_owned();
+    opts.source_index = source_index;
+
+    let mut stylesheet = {
+      let mut at_rule_parser = self.at_rule_parser.lock().unwrap();
+      let at_rule_parser = match &mut *at_rule_parser {
+        AtRuleParserValue::Owned(owned) => owned,
+        AtRuleParserValue::Borrowed(borrowed) => *borrowed,
+      };
+
+      StyleSheet::<T::AtRule>::parse_with(code, opts, at_rule_parser)?
+    };
+
+    if let Some(source_map) = &self.source_map {
+      // Only add source if we don't have an input source map.
+      // If we do, this will be handled by the printer when remapping locations.
+      let sm = stylesheet.source_map_url(0);
+      if sm.is_none() || !sm.unwrap().starts_with("data") {
+        let mut source_map = source_map.lock().unwrap();
+        let source_index = source_map.add_source(filename);
+        let _ = source_map.set_source_content(source_index as usize, code);
+      }
+    }
+
+    // Collect and load dependencies for this stylesheet in parallel.
+    let dependencies: Result<Vec<u32>, _> = stylesheet
+      .rules
+      .0
+      .par_iter_mut()
+      .filter_map(|r| {
+        // Prepend parent layer name to @layer statements.
+        if let CssRule::LayerStatement(layer) = r {
+          if let Some(Some(parent_layer)) = &rule.layer {
+            for name in &mut layer.names {
+              name.0.insert_many(0, parent_layer.0.iter().cloned())
+            }
+          }
+        }
+
+        if let CssRule::Import(import) = r {
+          let specifier = &import.url;
+
+          // Combine media queries and supports conditions from parent
+          // stylesheet with @import rule using a logical and operator.
+          let mut media = rule.media.clone();
+          let result = media.and(&import.media).map_err(|_| Error {
+            kind: BundleErrorKind::UnsupportedMediaBooleanLogic,
+            loc: Some(ErrorLocation::new(
+              import.loc,
+              self.find_filename(import.loc.source_index),
+            )),
+          });
+
+          if let Err(e) = result {
+            return Some(Err(e));
+          }
+
+          let layer = if (rule.layer == Some(None) && import.layer.is_some())
+            || (import.layer == Some(None) && rule.layer.is_some())
+          {
+            // Cannot combine anonymous layers
+            return Some(Err(Error {
+              kind: BundleErrorKind::UnsupportedLayerCombination,
+              loc: Some(ErrorLocation::new(
+                import.loc,
+                self.find_filename(import.loc.source_index),
+              )),
+            }));
+          } else if let Some(Some(a)) = &rule.layer {
+            if let Some(Some(b)) = &import.layer {
+              let mut name = a.clone();
+              name.0.extend(b.0.iter().cloned());
+              Some(Some(name))
+            } else {
+              Some(Some(a.clone()))
+            }
+          } else {
+            import.layer.clone()
+          };
+
+          let result = match self.fs.resolve(&specifier, file) {
+            Ok(path) => self.load_file(
+              &path,
+              ImportRule {
+                layer,
+                media,
+                supports: combine_supports(rule.supports.clone(), &import.supports),
+                url: "".into(),
+                loc: import.loc,
+              },
+            ),
+            Err(err) => Err(Error {
+              kind: BundleErrorKind::ResolverError(err),
+              loc: Some(ErrorLocation::new(
+                import.loc,
+                self.find_filename(import.loc.source_index),
+              )),
+            }),
+          };
+
+          Some(result)
+        } else {
+          None
+        }
+      })
+      .collect();
+
+    // Collect CSS modules dependencies from the `composes` property.
+    let css_modules_deps: Result<Vec<u32>, _> = if self.options.css_modules.is_some() {
+      stylesheet
+        .rules
+        .0
+        .par_iter_mut()
+        .filter_map(|r| {
+          if let CssRule::Style(style) = r {
+            Some(
+              style
+                .declarations
+                .declarations
+                .par_iter_mut()
+                .chain(style.declarations.important_declarations.par_iter_mut())
+                .filter_map(|d| match d {
+                  Property::Composes(composes) => self
+                    .add_css_module_dep(file, &rule, style.loc, composes.loc, &mut composes.from)
+                    .map(|result| rayon::iter::Either::Left(rayon::iter::once(result))),
+
+                  // Handle variable references if the dashed_idents option is present.
+                  Property::Custom(CustomProperty { value, .. })
+                  | Property::Unparsed(UnparsedProperty { value, .. })
+                    if matches!(&self.options.css_modules, Some(css_modules) if css_modules.dashed_idents) =>
+                  {
+                    Some(rayon::iter::Either::Right(visit_vars(value).filter_map(|name| {
+                      self.add_css_module_dep(
+                        file,
+                        &rule,
+                        style.loc,
+                        // TODO: store loc in variable reference?
+                        crate::dependencies::Location {
+                          line: style.loc.line,
+                          column: style.loc.column,
+                        },
+                        &mut name.from,
+                      )
+                    })))
+                  }
+                  _ => None,
+                })
+                .flatten(),
+            )
+          } else {
+            None
+          }
+        })
+        .flatten()
+        .collect()
+    } else {
+      Ok(vec![])
+    };
+
+    let entry = &mut self.stylesheets.lock().unwrap()[source_index as usize];
+    entry.stylesheet = Some(stylesheet);
+    entry.dependencies = dependencies?;
+    entry.css_modules_deps = css_modules_deps?;
+
+    Ok(source_index)
+  }
+
+  fn add_css_module_dep(
+    &self,
+    file: &Path,
+    rule: &ImportRule<'a>,
+    style_loc: Location,
+    loc: crate::dependencies::Location,
+    specifier: &mut Option<Specifier>,
+  ) -> Option<Result<u32, Error<BundleErrorKind<'a, P::Error>>>> {
+    if let Some(Specifier::File(f)) = specifier {
+      let result = match self.fs.resolve(&f, file) {
+        Ok(path) => {
+          let res = self.load_file(
+            &path,
+            ImportRule {
+              layer: rule.layer.clone(),
+              media: rule.media.clone(),
+              supports: rule.supports.clone(),
+              url: "".into(),
+              loc: Location {
+                source_index: style_loc.source_index,
+                line: loc.line,
+                column: loc.column,
+              },
+            },
+          );
+
+          if let Ok(source_index) = res {
+            *specifier = Some(Specifier::SourceIndex(source_index));
+          }
+
+          res
+        }
+        Err(err) => Err(Error {
+          kind: BundleErrorKind::ResolverError(err),
+          loc: Some(ErrorLocation::new(
+            style_loc,
+            self.find_filename(style_loc.source_index),
+          )),
+        }),
+      };
+      Some(result)
+    } else {
+      None
+    }
+  }
+
+  fn order(&mut self) {
+    process(self.stylesheets.get_mut().unwrap(), 0, &mut HashSet::new());
+
+    fn process<'i, T>(
+      stylesheets: &mut Vec<BundleStyleSheet<'i, '_, T>>,
+      source_index: u32,
+      visited: &mut HashSet<u32>,
+    ) {
+      if visited.contains(&source_index) {
+        return;
+      }
+
+      visited.insert(source_index);
+
+      let mut dep_index = 0;
+      for i in 0..stylesheets[source_index as usize].css_modules_deps.len() {
+        let dep_source_index = stylesheets[source_index as usize].css_modules_deps[i];
+        let resolved = &mut stylesheets[dep_source_index as usize];
+
+        // CSS modules preserve the first instance of composed stylesheets.
+        if !visited.contains(&dep_source_index) {
+          resolved.parent_dep_index = dep_index;
+          resolved.parent_source_index = source_index;
+          process(stylesheets, dep_source_index, visited);
+        }
+
+        dep_index += 1;
+      }
+
+      for i in 0..stylesheets[source_index as usize].dependencies.len() {
+        let dep_source_index = stylesheets[source_index as usize].dependencies[i];
+        let resolved = &mut stylesheets[dep_source_index as usize];
+
+        // In browsers, every instance of an @import is evaluated, so we preserve the last.
+        resolved.parent_dep_index = dep_index;
+        resolved.parent_source_index = source_index;
+
+        process(stylesheets, dep_source_index, visited);
+        dep_index += 1;
+      }
+    }
+  }
+
+  fn inline(&mut self, dest: &mut Vec<CssRule<'a, T::AtRule>>) {
+    process(self.stylesheets.get_mut().unwrap(), 0, dest);
+
+    fn process<'a, T>(
+      stylesheets: &mut Vec<BundleStyleSheet<'a, '_, T>>,
+      source_index: u32,
+      dest: &mut Vec<CssRule<'a, T>>,
+    ) {
+      let stylesheet = &mut stylesheets[source_index as usize];
+      let mut rules = std::mem::take(&mut stylesheet.stylesheet.as_mut().unwrap().rules.0);
+
+      // Hoist css modules deps
+      let mut dep_index = 0;
+      for i in 0..stylesheet.css_modules_deps.len() {
+        let dep_source_index = stylesheets[source_index as usize].css_modules_deps[i];
+        let resolved = &stylesheets[dep_source_index as usize];
+
+        // Include the dependency if this is the first instance as computed earlier.
+        if resolved.parent_source_index == source_index && resolved.parent_dep_index == dep_index as u32 {
+          process(stylesheets, dep_source_index, dest);
+        }
+
+        dep_index += 1;
+      }
+
+      let mut import_index = 0;
+      for rule in &mut rules {
+        match rule {
+          CssRule::Import(_) => {
+            let dep_source_index = stylesheets[source_index as usize].dependencies[import_index];
+            let resolved = &stylesheets[dep_source_index as usize];
+
+            // Include the dependency if this is the last instance as computed earlier.
+            if resolved.parent_source_index == source_index && resolved.parent_dep_index == dep_index {
+              process(stylesheets, dep_source_index, dest);
+            }
+
+            *rule = CssRule::Ignored;
+            dep_index += 1;
+            import_index += 1;
+          }
+          CssRule::LayerStatement(_) => {
+            // @layer rules are the only rules that may appear before an @import.
+            // We must preserve this order to ensure correctness.
+            let layer = std::mem::replace(rule, CssRule::Ignored);
+            dest.push(layer);
+          }
+          CssRule::Ignored => {}
+          _ => break,
+        }
+      }
+
+      // Wrap rules in the appropriate @layer, @media, and @supports rules.
+      let stylesheet = &mut stylesheets[source_index as usize];
+
+      if stylesheet.layer.is_some() {
+        rules = vec![CssRule::LayerBlock(LayerBlockRule {
+          name: stylesheet.layer.take().unwrap(),
+          rules: CssRuleList(rules),
+          loc: stylesheet.loc,
+        })]
+      }
+
+      if !stylesheet.media.media_queries.is_empty() {
+        rules = vec![CssRule::Media(MediaRule {
+          query: std::mem::replace(&mut stylesheet.media, MediaList::new()),
+          rules: CssRuleList(rules),
+          loc: stylesheet.loc,
+        })]
+      }
+
+      if stylesheet.supports.is_some() {
+        rules = vec![CssRule::Supports(SupportsRule {
+          condition: stylesheet.supports.take().unwrap(),
+          rules: CssRuleList(rules),
+          loc: stylesheet.loc,
+        })]
+      }
+
+      dest.extend(rules);
+    }
+  }
+}
+
+fn combine_supports<'a>(
+  a: Option<SupportsCondition<'a>>,
+  b: &Option<SupportsCondition<'a>>,
+) -> Option<SupportsCondition<'a>> {
+  if let Some(mut a) = a {
+    if let Some(b) = b {
+      a.and(b)
+    }
+    Some(a)
+  } else {
+    b.clone()
+  }
+}
+
+fn visit_vars<'a, 'b>(
+  token_list: &'b mut TokenList<'a>,
+) -> impl ParallelIterator<Item = &'b mut DashedIdentReference<'a>> {
+  let mut stack = vec![token_list.0.iter_mut()];
+  std::iter::from_fn(move || {
+    while !stack.is_empty() {
+      let iter = stack.last_mut().unwrap();
+      match iter.next() {
+        Some(TokenOrValue::Var(var)) => {
+          if let Some(fallback) = &mut var.fallback {
+            stack.push(fallback.0.iter_mut());
+          }
+          return Some(&mut var.name);
+        }
+        Some(TokenOrValue::Env(env)) => {
+          if let Some(fallback) = &mut env.fallback {
+            stack.push(fallback.0.iter_mut());
+          }
+          if let EnvironmentVariableName::Custom(name) = &mut env.name {
+            return Some(name);
+          }
+        }
+        Some(TokenOrValue::UnresolvedColor(color)) => match color {
+          UnresolvedColor::RGB { alpha, .. } | UnresolvedColor::HSL { alpha, .. } => {
+            stack.push(alpha.0.iter_mut());
+          }
+          UnresolvedColor::LightDark { light, dark } => {
+            stack.push(light.0.iter_mut());
+            stack.push(dark.0.iter_mut());
+          }
+        },
+        None => {
+          stack.pop();
+        }
+        _ => {}
+      }
+    }
+    None
+  })
+  .par_bridge()
+}
+
+#[cfg(test)]
+mod tests {
+  use super::*;
+  use crate::{
+    css_modules::{self, CssModuleExports, CssModuleReference},
+    parser::ParserFlags,
+    stylesheet::{MinifyOptions, PrinterOptions},
+    targets::{Browsers, Targets},
+  };
+  use indoc::indoc;
+  use std::collections::HashMap;
+
+  #[derive(Clone)]
+  struct TestProvider {
+    map: HashMap<PathBuf, String>,
+  }
+
+  impl SourceProvider for TestProvider {
+    type Error = std::io::Error;
+
+    fn read<'a>(&'a self, file: &Path) -> Result<&'a str, Self::Error> {
+      Ok(self.map.get(file).unwrap())
+    }
+
+    fn resolve(&self, specifier: &str, originating_file: &Path) -> Result<PathBuf, Self::Error> {
+      Ok(originating_file.with_file_name(specifier))
+    }
+  }
+
+  /// Stand-in for a user-authored `SourceProvider` with application-specific logic.
+  struct CustomProvider {
+    map: HashMap<PathBuf, String>,
+  }
+
+  impl SourceProvider for CustomProvider {
+    type Error = std::io::Error;
+
+    /// Read files from in-memory map.
+    fn read<'a>(&'a self, file: &Path) -> Result<&'a str, Self::Error> {
+      Ok(self.map.get(file).unwrap())
+    }
+
+    /// Resolve by stripping a `foo:` prefix off any import. Specifiers without
+    /// this prefix fail with an error.
+    fn resolve(&self, specifier: &str, _originating_file: &Path) -> Result<PathBuf, Self::Error> {
+      if specifier.starts_with("foo:") {
+        Ok(Path::new(&specifier["foo:".len()..]).to_path_buf())
+      } else {
+        let err = std::io::Error::new(
+          std::io::ErrorKind::NotFound,
+          format!(
+            "Failed to resolve `{}`, specifier does not start with `foo:`.",
+            &specifier
+          ),
+        );
+
+        Err(err)
+      }
+    }
+  }
+
+  macro_rules! fs(
+    { $($key:literal: $value:expr),* } => {
+      {
+        #[allow(unused_mut)]
+        let mut m = HashMap::new();
+        $(
+          m.insert(PathBuf::from($key), $value.to_owned());
+        )*
+        m
+      }
+    };
+  );
+
+  fn bundle<P: SourceProvider>(fs: P, entry: &str) -> String {
+    let mut bundler = Bundler::new(&fs, None, ParserOptions::default());
+    let stylesheet = bundler.bundle(Path::new(entry)).unwrap();
+    stylesheet.to_css(PrinterOptions::default()).unwrap().code
+  }
+
+  fn bundle_css_module<P: SourceProvider>(
+    fs: P,
+    entry: &str,
+    project_root: Option<&str>,
+  ) -> (String, CssModuleExports) {
+    bundle_css_module_with_pattern(fs, entry, project_root, "[hash]_[local]")
+  }
+
+  fn bundle_css_module_with_pattern<P: SourceProvider>(
+    fs: P,
+    entry: &str,
+    project_root: Option<&str>,
+    pattern: &'static str,
+  ) -> (String, CssModuleExports) {
+    let mut bundler = Bundler::new(
+      &fs,
+      None,
+      ParserOptions {
+        css_modules: Some(css_modules::Config {
+          dashed_idents: true,
+          pattern: css_modules::Pattern::parse(pattern).unwrap(),
+          ..Default::default()
+        }),
+        ..ParserOptions::default()
+      },
+    );
+    let mut stylesheet = bundler.bundle(Path::new(entry)).unwrap();
+    stylesheet.minify(MinifyOptions::default()).unwrap();
+    let res = stylesheet
+      .to_css(PrinterOptions {
+        project_root,
+        ..PrinterOptions::default()
+      })
+      .unwrap();
+    (res.code, res.exports.unwrap())
+  }
+
+  fn bundle_custom_media<P: SourceProvider>(fs: P, entry: &str) -> String {
+    let mut bundler = Bundler::new(
+      &fs,
+      None,
+      ParserOptions {
+        flags: ParserFlags::CUSTOM_MEDIA,
+        ..ParserOptions::default()
+      },
+    );
+    let mut stylesheet = bundler.bundle(Path::new(entry)).unwrap();
+    let targets = Targets {
+      browsers: Some(Browsers {
+        safari: Some(13 << 16),
+        ..Browsers::default()
+      }),
+      ..Default::default()
+    };
+    stylesheet
+      .minify(MinifyOptions {
+        targets,
+        ..MinifyOptions::default()
+      })
+      .unwrap();
+    stylesheet
+      .to_css(PrinterOptions {
+        targets,
+        ..PrinterOptions::default()
+      })
+      .unwrap()
+      .code
+  }
+
+  fn error_test<P: SourceProvider>(
+    fs: P,
+    entry: &str,
+    maybe_cb: Option<Box<dyn FnOnce(BundleErrorKind<P::Error>) -> ()>>,
+  ) {
+    let mut bundler = Bundler::new(&fs, None, ParserOptions::default());
+    let res = bundler.bundle(Path::new(entry));
+    match res {
+      Ok(_) => unreachable!(),
+      Err(e) => {
+        if let Some(cb) = maybe_cb {
+          cb(e.kind);
+        }
+      }
+    }
+  }
+
+  fn flatten_exports(exports: CssModuleExports) -> HashMap<String, String> {
+    let mut res = HashMap::new();
+    for (name, export) in &exports {
+      let mut classes = export.name.clone();
+      for composes in &export.composes {
+        classes.push(' ');
+        classes.push_str(match composes {
+          CssModuleReference::Local { name } => name,
+          CssModuleReference::Global { name } => name,
+          _ => unreachable!(),
+        })
+      }
+      res.insert(name.clone(), classes);
+    }
+    res
+  }
+
+  #[test]
+  fn test_bundle() {
+    let res = bundle(
+      TestProvider {
+        map: fs! {
+          "/a.css": r#"
+          @import "b.css";
+          .a { color: red }
+        "#,
+          "/b.css": r#"
+          .b { color: green }
+        "#
+        },
+      },
+      "/a.css",
+    );
+    assert_eq!(
+      res,
+      indoc! { r#"
+      .b {
+        color: green;
+      }
+
+      .a {
+        color: red;
+      }
+    "#}
+    );
+
+    let res = bundle(
+      TestProvider {
+        map: fs! {
+          "/a.css": r#"
+          @import "b.css" print;
+          .a { color: red }
+        "#,
+          "/b.css": r#"
+          .b { color: green }
+        "#
+        },
+      },
+      "/a.css",
+    );
+    assert_eq!(
+      res,
+      indoc! { r#"
+      @media print {
+        .b {
+          color: green;
+        }
+      }
+
+      .a {
+        color: red;
+      }
+    "#}
+    );
+
+    let res = bundle(
+      TestProvider {
+        map: fs! {
+          "/a.css": r#"
+          @import "b.css" supports(color: green);
+          .a { color: red }
+        "#,
+          "/b.css": r#"
+          .b { color: green }
+        "#
+        },
+      },
+      "/a.css",
+    );
+    assert_eq!(
+      res,
+      indoc! { r#"
+      @supports (color: green) {
+        .b {
+          color: green;
+        }
+      }
+
+      .a {
+        color: red;
+      }
+    "#}
+    );
+
+    let res = bundle(
+      TestProvider {
+        map: fs! {
+          "/a.css": r#"
+          @import "b.css" supports(color: green) print;
+          .a { color: red }
+        "#,
+          "/b.css": r#"
+          .b { color: green }
+        "#
+        },
+      },
+      "/a.css",
+    );
+    assert_eq!(
+      res,
+      indoc! { r#"
+      @supports (color: green) {
+        @media print {
+          .b {
+            color: green;
+          }
+        }
+      }
+
+      .a {
+        color: red;
+      }
+    "#}
+    );
+
+    let res = bundle(
+      TestProvider {
+        map: fs! {
+          "/a.css": r#"
+          @import "b.css" print;
+          @import "b.css" screen;
+          .a { color: red }
+        "#,
+          "/b.css": r#"
+          .b { color: green }
+        "#
+        },
+      },
+      "/a.css",
+    );
+    assert_eq!(
+      res,
+      indoc! { r#"
+      @media print, screen {
+        .b {
+          color: green;
+        }
+      }
+
+      .a {
+        color: red;
+      }
+    "#}
+    );
+
+    let res = bundle(
+      TestProvider {
+        map: fs! {
+          "/a.css": r#"
+          @import "b.css" supports(color: red);
+          @import "b.css" supports(foo: bar);
+          .a { color: red }
+        "#,
+          "/b.css": r#"
+          .b { color: green }
+        "#
+        },
+      },
+      "/a.css",
+    );
+    assert_eq!(
+      res,
+      indoc! { r#"
+      @supports (color: red) or (foo: bar) {
+        .b {
+          color: green;
+        }
+      }
+
+      .a {
+        color: red;
+      }
+    "#}
+    );
+
+    let res = bundle(
+      TestProvider {
+        map: fs! {
+          "/a.css": r#"
+          @import "b.css" print;
+          .a { color: red }
+        "#,
+          "/b.css": r#"
+          @import "c.css" (color);
+          .b { color: yellow }
+        "#,
+          "/c.css": r#"
+          .c { color: green }
+        "#
+        },
+      },
+      "/a.css",
+    );
+    assert_eq!(
+      res,
+      indoc! { r#"
+      @media print and (color) {
+        .c {
+          color: green;
+        }
+      }
+
+      @media print {
+        .b {
+          color: #ff0;
+        }
+      }
+
+      .a {
+        color: red;
+      }
+    "#}
+    );
+
+    let res = bundle(
+      TestProvider {
+        map: fs! {
+          "/a.css": r#"
+          @import "b.css";
+          .a { color: red }
+        "#,
+          "/b.css": r#"
+          @import "c.css";
+        "#,
+          "/c.css": r#"
+          @import "a.css";
+          .c { color: green }
+        "#
+        },
+      },
+      "/a.css",
+    );
+    assert_eq!(
+      res,
+      indoc! { r#"
+      .c {
+        color: green;
+      }
+
+      .a {
+        color: red;
+      }
+    "#}
+    );
+
+    let res = bundle(
+      TestProvider {
+        map: fs! {
+          "/a.css": r#"
+          @import "b/c.css";
+          .a { color: red }
+        "#,
+          "/b/c.css": r#"
+          .b { color: green }
+        "#
+        },
+      },
+      "/a.css",
+    );
+    assert_eq!(
+      res,
+      indoc! { r#"
+      .b {
+        color: green;
+      }
+
+      .a {
+        color: red;
+      }
+    "#}
+    );
+
+    let res = bundle(
+      TestProvider {
+        map: fs! {
+          "/a.css": r#"
+          @import "./b/c.css";
+          .a { color: red }
+        "#,
+          "/b/c.css": r#"
+          .b { color: green }
+        "#
+        },
+      },
+      "/a.css",
+    );
+    assert_eq!(
+      res,
+      indoc! { r#"
+      .b {
+        color: green;
+      }
+
+      .a {
+        color: red;
+      }
+    "#}
+    );
+
+    let res = bundle_custom_media(
+      TestProvider {
+        map: fs! {
+          "/a.css": r#"
+          @import "media.css";
+          @import "b.css";
+          .a { color: red }
+        "#,
+          "/media.css": r#"
+          @custom-media --foo print;
+        "#,
+          "/b.css": r#"
+          @media (--foo) {
+            .a { color: green }
+          }
+        "#
+        },
+      },
+      "/a.css",
+    );
+    assert_eq!(
+      res,
+      indoc! { r#"
+      @media print {
+        .a {
+          color: green;
+        }
+      }
+
+      .a {
+        color: red;
+      }
+    "#}
+    );
+
+    let res = bundle(
+      TestProvider {
+        map: fs! {
+          "/a.css": r#"
+          @import "b.css" layer(foo);
+          .a { color: red }
+        "#,
+          "/b.css": r#"
+          .b { color: green }
+        "#
+        },
+      },
+      "/a.css",
+    );
+    assert_eq!(
+      res,
+      indoc! { r#"
+      @layer foo {
+        .b {
+          color: green;
+        }
+      }
+
+      .a {
+        color: red;
+      }
+    "#}
+    );
+
+    let res = bundle(
+      TestProvider {
+        map: fs! {
+          "/a.css": r#"
+          @import "b.css" layer;
+          .a { color: red }
+        "#,
+          "/b.css": r#"
+          .b { color: green }
+        "#
+        },
+      },
+      "/a.css",
+    );
+    assert_eq!(
+      res,
+      indoc! { r#"
+      @layer {
+        .b {
+          color: green;
+        }
+      }
+
+      .a {
+        color: red;
+      }
+    "#}
+    );
+
+    let res = bundle(
+      TestProvider {
+        map: fs! {
+          "/a.css": r#"
+          @import "b.css" layer(foo);
+          .a { color: red }
+        "#,
+          "/b.css": r#"
+          @import "c.css" layer(bar);
+          .b { color: green }
+        "#,
+          "/c.css": r#"
+          .c { color: green }
+        "#
+        },
+      },
+      "/a.css",
+    );
+    assert_eq!(
+      res,
+      indoc! { r#"
+      @layer foo.bar {
+        .c {
+          color: green;
+        }
+      }
+
+      @layer foo {
+        .b {
+          color: green;
+        }
+      }
+
+      .a {
+        color: red;
+      }
+    "#}
+    );
+
+    let res = bundle(
+      TestProvider {
+        map: fs! {
+          "/a.css": r#"
+          @import "b.css" layer(foo);
+          @import "b.css" layer(foo);
+        "#,
+          "/b.css": r#"
+          .b { color: green }
+        "#
+        },
+      },
+      "/a.css",
+    );
+    assert_eq!(
+      res,
+      indoc! { r#"
+      @layer foo {
+        .b {
+          color: green;
+        }
+      }
+    "#}
+    );
+
+    let res = bundle(
+      TestProvider {
+        map: fs! {
+          "/a.css": r#"
+          @layer bar, foo;
+          @import "b.css" layer(foo);
+
+          @layer bar {
+            div {
+              background: red;
+            }
+          }
+        "#,
+          "/b.css": r#"
+          @layer qux, baz;
+          @import "c.css" layer(baz);
+
+          @layer qux {
+            div {
+              background: green;
+            }
+          }
+        "#,
+          "/c.css": r#"
+          div {
+            background: yellow;
+          }
+        "#
+        },
+      },
+      "/a.css",
+    );
+    assert_eq!(
+      res,
+      indoc! { r#"
+      @layer bar, foo;
+      @layer foo.qux, foo.baz;
+
+      @layer foo.baz {
+        div {
+          background: #ff0;
+        }
+      }
+
+      @layer foo {
+        @layer qux {
+          div {
+            background: green;
+          }
+        }
+      }
+
+      @layer bar {
+        div {
+          background: red;
+        }
+      }
+    "#}
+    );
+
+    // Layer order depends on @import conditions.
+    let res = bundle(
+      TestProvider {
+        map: fs! {
+          "/a.css": r#"
+          @import "b.css" layer(bar) (min-width: 1000px);
+
+          @layer baz {
+            #box { background: purple }
+          }
+
+          @layer bar {
+            #box { background: yellow }
+          }
+        "#,
+          "/b.css": r#"
+          #box { background: green }
+        "#
+        },
+      },
+      "/a.css",
+    );
+    assert_eq!(
+      res,
+      indoc! { r#"
+      @media (width >= 1000px) {
+        @layer bar {
+          #box {
+            background: green;
+          }
+        }
+      }
+
+      @layer baz {
+        #box {
+          background: purple;
+        }
+      }
+
+      @layer bar {
+        #box {
+          background: #ff0;
+        }
+      }
+    "#}
+    );
+
+    error_test(
+      TestProvider {
+        map: fs! {
+          "/a.css": r#"
+          @import "b.css" layer(foo);
+          @import "b.css" layer(bar);
+        "#,
+          "/b.css": r#"
+          .b { color: red }
+        "#
+        },
+      },
+      "/a.css",
+      Some(Box::new(|err| {
+        assert!(matches!(err, BundleErrorKind::UnsupportedLayerCombination));
+      })),
+    );
+
+    error_test(
+      TestProvider {
+        map: fs! {
+          "/a.css": r#"
+          @import "b.css" layer;
+          @import "b.css" layer;
+        "#,
+          "/b.css": r#"
+          .b { color: red }
+        "#
+        },
+      },
+      "/a.css",
+      Some(Box::new(|err| {
+        assert!(matches!(err, BundleErrorKind::UnsupportedLayerCombination));
+      })),
+    );
+
+    error_test(
+      TestProvider {
+        map: fs! {
+          "/a.css": r#"
+          @import "b.css" layer;
+          .a { color: red }
+        "#,
+          "/b.css": r#"
+          @import "c.css" layer;
+          .b { color: green }
+        "#,
+          "/c.css": r#"
+          .c { color: green }
+        "#
+        },
+      },
+      "/a.css",
+      Some(Box::new(|err| {
+        assert!(matches!(err, BundleErrorKind::UnsupportedLayerCombination));
+      })),
+    );
+
+    error_test(
+      TestProvider {
+        map: fs! {
+          "/a.css": r#"
+          @import "b.css" layer;
+          .a { color: red }
+        "#,
+          "/b.css": r#"
+          @import "c.css" layer(foo);
+          .b { color: green }
+        "#,
+          "/c.css": r#"
+          .c { color: green }
+        "#
+        },
+      },
+      "/a.css",
+      Some(Box::new(|err| {
+        assert!(matches!(err, BundleErrorKind::UnsupportedLayerCombination));
+      })),
+    );
+
+    let res = bundle(
+      TestProvider {
+        map: fs! {
+          "/index.css": r#"
+          @import "a.css";
+          @import "b.css";
+        "#,
+          "/a.css": r#"
+          @import "./c.css";
+          body { background: red; }
+        "#,
+          "/b.css": r#"
+          @import "./c.css";
+          body { color: red; }
+        "#,
+          "/c.css": r#"
+          body {
+            background: white;
+            color: black;
+          }
+        "#
+        },
+      },
+      "/index.css",
+    );
+    assert_eq!(
+      res,
+      indoc! { r#"
+      body {
+        background: red;
+      }
+
+      body {
+        background: #fff;
+        color: #000;
+      }
+
+      body {
+        color: red;
+      }
+    "#}
+    );
+
+    let res = bundle(
+      TestProvider {
+        map: fs! {
+          "/index.css": r#"
+          @import "a.css";
+          @import "b.css";
+          @import "a.css";
+        "#,
+          "/a.css": r#"
+          body { background: green; }
+        "#,
+          "/b.css": r#"
+          body { background: red; }
+        "#
+        },
+      },
+      "/index.css",
+    );
+    assert_eq!(
+      res,
+      indoc! { r#"
+      body {
+        background: red;
+      }
+
+      body {
+        background: green;
+      }
+    "#}
+    );
+
+    let res = bundle(
+      CustomProvider {
+        map: fs! {
+          "/a.css": r#"
+            @import "foo:/b.css";
+            .a { color: red; }
+          "#,
+          "/b.css": ".b { color: green; }"
+        },
+      },
+      "/a.css",
+    );
+    assert_eq!(
+      res,
+      indoc! { r#"
+        .b {
+          color: green;
+        }
+
+        .a {
+          color: red;
+        }
+      "# }
+    );
+
+    error_test(
+      CustomProvider {
+        map: fs! {
+          "/a.css": r#"
+            /* Forgot to prefix with `foo:`. */
+            @import "/b.css";
+            .a { color: red; }
+          "#,
+          "/b.css": ".b { color: green; }"
+        },
+      },
+      "/a.css",
+      Some(Box::new(|err| {
+        let kind = match err {
+          BundleErrorKind::ResolverError(ref error) => error.kind(),
+          _ => unreachable!(),
+        };
+        assert!(matches!(kind, std::io::ErrorKind::NotFound));
+        assert!(err
+          .to_string()
+          .contains("Failed to resolve `/b.css`, specifier does not start with `foo:`."));
+      })),
+    );
+
+    // let res = bundle(fs! {
+    //   "/a.css": r#"
+    //     @import "b.css" supports(color: red) (color);
+    //     @import "b.css" supports(foo: bar) (orientation: horizontal);
+    //     .a { color: red }
+    //   "#,
+    //   "/b.css": r#"
+    //     .b { color: green }
+    //   "#
+    // }, "/a.css");
+
+    // let res = bundle(fs! {
+    //   "/a.css": r#"
+    //     @import "b.css" not print;
+    //     .a { color: red }
+    //   "#,
+    //   "/b.css": r#"
+    //     @import "c.css" not screen;
+    //     .b { color: green }
+    //   "#,
+    //   "/c.css": r#"
+    //     .c { color: yellow }
+    //   "#
+    // }, "/a.css");
+  }
+
+  #[test]
+  fn test_css_module() {
+    macro_rules! map {
+      { $($key:expr => $val:expr),* } => {
+        HashMap::from([
+          $(($key.to_owned(), $val.to_owned()),)*
+        ])
+      };
+    }
+
+    let (code, exports) = bundle_css_module(
+      TestProvider {
+        map: fs! {
+          "/a.css": r#"
+          @import "b.css";
+          .a { color: red }
+        "#,
+          "/b.css": r#"
+          .a { color: green }
+        "#
+        },
+      },
+      "/a.css",
+      None,
+    );
+    assert_eq!(
+      code,
+      indoc! { r#"
+      ._9z6RGq_a {
+        color: green;
+      }
+
+      ._6lixEq_a {
+        color: red;
+      }
+    "#}
+    );
+    assert_eq!(
+      flatten_exports(exports),
+      map! {
+        "a" => "_6lixEq_a"
+      }
+    );
+
+    let (code, exports) = bundle_css_module(
+      TestProvider {
+        map: fs! {
+          "/a.css": r#"
+          .a { composes: x from './b.css'; color: red; }
+          .b { color: yellow }
+        "#,
+          "/b.css": r#"
+          .x { composes: y; background: green }
+          .y { font: Helvetica }
+        "#
+        },
+      },
+      "/a.css",
+      None,
+    );
+    assert_eq!(
+      code,
+      indoc! { r#"
+      ._8Cs9ZG_x {
+        background: green;
+      }
+
+      ._8Cs9ZG_y {
+        font: Helvetica;
+      }
+
+      ._6lixEq_a {
+        color: red;
+      }
+
+      ._6lixEq_b {
+        color: #ff0;
+      }
+    "#}
+    );
+    assert_eq!(
+      flatten_exports(exports),
+      map! {
+        "a" => "_6lixEq_a _8Cs9ZG_x _8Cs9ZG_y",
+        "b" => "_6lixEq_b"
+      }
+    );
+
+    let (code, exports) = bundle_css_module(
+      TestProvider {
+        map: fs! {
+          "/a.css": r#"
+          .a { composes: x from './b.css'; background: red; }
+        "#,
+          "/b.css": r#"
+          .a { background: red }
+        "#
+        },
+      },
+      "/a.css",
+      None,
+    );
+    assert_eq!(
+      code,
+      indoc! { r#"
+      ._8Cs9ZG_a {
+        background: red;
+      }
+
+      ._6lixEq_a {
+        background: red;
+      }
+    "#}
+    );
+    assert_eq!(
+      flatten_exports(exports),
+      map! {
+        "a" => "_6lixEq_a"
+      }
+    );
+
+    let (code, exports) = bundle_css_module(
+      TestProvider {
+        map: fs! {
+          "/a.css": r#"
+          .a {
+            background: var(--bg from "./b.css", var(--fallback from "./b.css"));
+            color: rgb(255 255 255 / var(--opacity from "./b.css"));
+            width: env(--env, var(--env-fallback from "./env.css"));
+          }
+        "#,
+          "/b.css": r#"
+          .b {
+            --bg: red;
+            --fallback: yellow;
+            --opacity: 0.5;
+          }
+        "#,
+          "/env.css": r#"
+          .env {
+            --env-fallback: 20px;
+          }
+        "#
+        },
+      },
+      "/a.css",
+      None,
+    );
+    assert_eq!(
+      code,
+      indoc! { r#"
+      ._8Cs9ZG_b {
+        --_8Cs9ZG_bg: red;
+        --_8Cs9ZG_fallback: yellow;
+        --_8Cs9ZG_opacity: .5;
+      }
+
+      .GbJUva_env {
+        --GbJUva_env-fallback: 20px;
+      }
+
+      ._6lixEq_a {
+        background: var(--_8Cs9ZG_bg, var(--_8Cs9ZG_fallback));
+        color: rgb(255 255 255 / var(--_8Cs9ZG_opacity));
+        width: env(--_6lixEq_env, var(--GbJUva_env-fallback));
+      }
+    "#}
+    );
+    assert_eq!(
+      flatten_exports(exports),
+      map! {
+        "a" => "_6lixEq_a",
+        "--env" => "--_6lixEq_env"
+      }
+    );
+
+    // Hashes are stable between project roots.
+    let expected = indoc! { r#"
+    .dyGcAa_b {
+      background: #ff0;
+    }
+
+    .CK9avG_a {
+      background: #fff;
+    }
+  "#};
+
+    let (code, _) = bundle_css_module(
+      TestProvider {
+        map: fs! {
+          "/foo/bar/a.css": r#"
+        @import "b.css";
+        .a {
+          background: white;
+        }
+      "#,
+          "/foo/bar/b.css": r#"
+        .b {
+          background: yellow;
+        }
+      "#
+        },
+      },
+      "/foo/bar/a.css",
+      Some("/foo/bar"),
+    );
+    assert_eq!(code, expected);
+
+    let (code, _) = bundle_css_module(
+      TestProvider {
+        map: fs! {
+          "/x/y/z/a.css": r#"
+      @import "b.css";
+      .a {
+        background: white;
+      }
+    "#,
+          "/x/y/z/b.css": r#"
+      .b {
+        background: yellow;
+      }
+    "#
+        },
+      },
+      "/x/y/z/a.css",
+      Some("/x/y/z"),
+    );
+    assert_eq!(code, expected);
+
+    let (code, _) = bundle_css_module_with_pattern(
+      TestProvider {
+        map: fs! {
+          "/a.css": r#"
+          @import "b.css";
+          .a { color: red }
+        "#,
+          "/b.css": r#"
+          .a { color: green }
+        "#
+        },
+      },
+      "/a.css",
+      None,
+      "[content-hash]-[local]",
+    );
+    assert_eq!(
+      code,
+      indoc! { r#"
+      .do5n2W-a {
+        color: green;
+      }
+
+      .pP97eq-a {
+        color: red;
+      }
+    "#}
+    );
+  }
+
+  #[test]
+  fn test_source_map() {
+    let source = r#".imported {
+      content: "yay, file support!";
+    }
+
+    .selector {
+      margin: 1em;
+      background-color: #f60;
+    }
+
+    .selector .nested {
+      margin: 0.5em;
+    }
+
+    /*# sourceMappingURL=data:application/json;base64,ewoJInZlcnNpb24iOiAzLAoJInNvdXJjZVJvb3QiOiAicm9vdCIsCgkiZmlsZSI6ICJzdGRvdXQiLAoJInNvdXJjZXMiOiBbCgkJInN0ZGluIiwKCQkic2Fzcy9fdmFyaWFibGVzLnNjc3MiLAoJCSJzYXNzL19kZW1vLnNjc3MiCgldLAoJInNvdXJjZXNDb250ZW50IjogWwoJCSJAaW1wb3J0IFwiX3ZhcmlhYmxlc1wiO1xuQGltcG9ydCBcIl9kZW1vXCI7XG5cbi5zZWxlY3RvciB7XG4gIG1hcmdpbjogJHNpemU7XG4gIGJhY2tncm91bmQtY29sb3I6ICRicmFuZENvbG9yO1xuXG4gIC5uZXN0ZWQge1xuICAgIG1hcmdpbjogJHNpemUgLyAyO1xuICB9XG59IiwKCQkiJGJyYW5kQ29sb3I6ICNmNjA7XG4kc2l6ZTogMWVtOyIsCgkJIi5pbXBvcnRlZCB7XG4gIGNvbnRlbnQ6IFwieWF5LCBmaWxlIHN1cHBvcnQhXCI7XG59IgoJXSwKCSJtYXBwaW5ncyI6ICJBRUFBLFNBQVMsQ0FBQztFQUNSLE9BQU8sRUFBRSxvQkFBcUI7Q0FDL0I7O0FGQ0QsU0FBUyxDQUFDO0VBQ1IsTUFBTSxFQ0hELEdBQUc7RURJUixnQkFBZ0IsRUNMTCxJQUFJO0NEVWhCOztBQVBELFNBQVMsQ0FJUCxPQUFPLENBQUM7RUFDTixNQUFNLEVDUEgsS0FBRztDRFFQIiwKCSJuYW1lcyI6IFtdCn0= */"#;
+
+    let fs = TestProvider {
+      map: fs! {
+        "/a.css": r#"
+        @import "/b.css";
+        .a { color: red; }
+      "#,
+        "/b.css": source
+      },
+    };
+
+    let mut sm = parcel_sourcemap::SourceMap::new("/");
+    let mut bundler = Bundler::new(&fs, Some(&mut sm), ParserOptions::default());
+    let mut stylesheet = bundler.bundle(Path::new("/a.css")).unwrap();
+    stylesheet.minify(MinifyOptions::default()).unwrap();
+    stylesheet
+      .to_css(PrinterOptions {
+        source_map: Some(&mut sm),
+        minify: true,
+        ..PrinterOptions::default()
+      })
+      .unwrap();
+    let map = sm.to_json(None).unwrap();
+    assert_eq!(
+      map,
+      r#"{"version":3,"sourceRoot":null,"mappings":"ACAA,uCCGA,2CAAA,8BFDQ","sources":["a.css","sass/_demo.scss","stdin"],"sourcesContent":["\n        @import \"/b.css\";\n        .a { color: red; }\n      ",".imported {\n  content: \"yay, file support!\";\n}","@import \"_variables\";\n@import \"_demo\";\n\n.selector {\n  margin: $size;\n  background-color: $brandColor;\n\n  .nested {\n    margin: $size / 2;\n  }\n}"],"names":[]}"#
+    );
+  }
+
+  #[test]
+  fn test_license_comments() {
+    let res = bundle(
+      TestProvider {
+        map: fs! {
+          "/a.css": r#"
+          /*! Copyright 2023 Someone awesome */
+          @import "b.css";
+          .a { color: red }
+        "#,
+          "/b.css": r#"
+          /*! Copyright 2023 Someone else */
+          .b { color: green }
+        "#
+        },
+      },
+      "/a.css",
+    );
+    assert_eq!(
+      res,
+      indoc! { r#"
+      /*! Copyright 2023 Someone awesome */
+      /*! Copyright 2023 Someone else */
+      .b {
+        color: green;
+      }
+
+      .a {
+        color: red;
+      }
+    "#}
+    );
+  }
+}
diff --git a/src/compat.rs b/src/compat.rs
new file mode 100644
index 0000000..592a7ea
--- /dev/null
+++ b/src/compat.rs
@@ -0,0 +1,5692 @@
+// This file is autogenerated by build-prefixes.js. DO NOT EDIT!
+
+use crate::targets::Browsers;
+
+#[allow(dead_code)]
+#[derive(Clone, Copy, PartialEq)]
+pub enum Feature {
+  AbsFunction,
+  AccentSystemColor,
+  AfarListStyleType,
+  AmharicAbegedeListStyleType,
+  AmharicListStyleType,
+  AnchorSizeSize,
+  AnimationTimelineShorthand,
+  AnyLink,
+  AnyPseudo,
+  ArabicIndicListStyleType,
+  ArmenianListStyleType,
+  AsterisksListStyleType,
+  AutoSize,
+  Autofill,
+  BengaliListStyleType,
+  BinaryListStyleType,
+  BorderImageRepeatRound,
+  BorderImageRepeatSpace,
+  CalcFunction,
+  CambodianListStyleType,
+  CapUnit,
+  CaseInsensitive,
+  ChUnit,
+  Checkmark,
+  CircleListStyleType,
+  CjkDecimalListStyleType,
+  CjkEarthlyBranchListStyleType,
+  CjkHeavenlyStemListStyleType,
+  ClampFunction,
+  ColorFunction,
+  ConicGradient,
+  ContainerQueryLengthUnits,
+  Cue,
+  CueFunction,
+  CustomMediaQueries,
+  DecimalLeadingZeroListStyleType,
+  DecimalListStyleType,
+  DefaultPseudo,
+  DetailsContent,
+  DevanagariListStyleType,
+  Dialog,
+  DirSelector,
+  DiscListStyleType,
+  DisclosureClosedListStyleType,
+  DisclosureOpenListStyleType,
+  DoublePositionGradients,
+  EmUnit,
+  EthiopicAbegedeAmEtListStyleType,
+  EthiopicAbegedeGezListStyleType,
+  EthiopicAbegedeListStyleType,
+  EthiopicAbegedeTiErListStyleType,
+  EthiopicAbegedeTiEtListStyleType,
+  EthiopicHalehameAaErListStyleType,
+  EthiopicHalehameAaEtListStyleType,
+  EthiopicHalehameAmEtListStyleType,
+  EthiopicHalehameGezListStyleType,
+  EthiopicHalehameOmEtListStyleType,
+  EthiopicHalehameSidEtListStyleType,
+  EthiopicHalehameSoEtListStyleType,
+  EthiopicHalehameTigListStyleType,
+  EthiopicListStyleType,
+  EthiopicNumericListStyleType,
+  ExUnit,
+  ExtendedSystemFonts,
+  FirstLetter,
+  FirstLine,
+  FitContentFunctionSize,
+  FitContentSize,
+  FocusVisible,
+  FocusWithin,
+  FontFamilySystemUi,
+  FontSizeRem,
+  FontSizeXXXLarge,
+  FontStretchPercentage,
+  FontStyleObliqueAngle,
+  FontWeightNumber,
+  FootnotesListStyleType,
+  FormValidation,
+  Fullscreen,
+  Gencontent,
+  GeorgianListStyleType,
+  GradientInterpolationHints,
+  GujaratiListStyleType,
+  GurmukhiListStyleType,
+  HasSelector,
+  HebrewListStyleType,
+  HexAlphaColors,
+  HiraganaIrohaListStyleType,
+  HiraganaListStyleType,
+  HypotFunction,
+  IcUnit,
+  ImageSet,
+  InOutOfRange,
+  IndeterminatePseudo,
+  IsAnimatableSize,
+  IsSelector,
+  JapaneseFormalListStyleType,
+  JapaneseInformalListStyleType,
+  KannadaListStyleType,
+  KatakanaIrohaListStyleType,
+  KatakanaListStyleType,
+  KhmerListStyleType,
+  KoreanHangulFormalListStyleType,
+  KoreanHanjaFormalListStyleType,
+  KoreanHanjaInformalListStyleType,
+  LabColors,
+  LangSelectorList,
+  LaoListStyleType,
+  LhUnit,
+  LightDark,
+  LinearGradient,
+  LogicalBorderRadius,
+  LogicalBorderShorthand,
+  LogicalBorders,
+  LogicalInset,
+  LogicalMargin,
+  LogicalMarginShorthand,
+  LogicalPadding,
+  LogicalPaddingShorthand,
+  LogicalSize,
+  LogicalTextAlign,
+  LowerAlphaListStyleType,
+  LowerArmenianListStyleType,
+  LowerGreekListStyleType,
+  LowerHexadecimalListStyleType,
+  LowerLatinListStyleType,
+  LowerNorwegianListStyleType,
+  LowerRomanListStyleType,
+  MalayalamListStyleType,
+  MarkerPseudo,
+  MaxContentSize,
+  MaxFunction,
+  MediaIntervalSyntax,
+  MediaRangeSyntax,
+  MinContentSize,
+  MinFunction,
+  ModFunction,
+  MongolianListStyleType,
+  MozAvailableSize,
+  MyanmarListStyleType,
+  Namespaces,
+  Nesting,
+  NoneListStyleType,
+  NotSelectorList,
+  NthChildOf,
+  OctalListStyleType,
+  OklabColors,
+  OptionalPseudo,
+  OriyaListStyleType,
+  OromoListStyleType,
+  OverflowShorthand,
+  P3Colors,
+  PartPseudo,
+  PersianListStyleType,
+  Picker,
+  PickerIcon,
+  PlaceContent,
+  PlaceItems,
+  PlaceSelf,
+  Placeholder,
+  PlaceholderShown,
+  QUnit,
+  RadialGradient,
+  RcapUnit,
+  RchUnit,
+  ReadOnlyWrite,
+  RemFunction,
+  RemUnit,
+  RepeatingConicGradient,
+  RepeatingLinearGradient,
+  RepeatingRadialGradient,
+  RexUnit,
+  RicUnit,
+  RlhUnit,
+  RoundFunction,
+  Selection,
+  Selectors2,
+  Selectors3,
+  Shadowdomv1,
+  SidamaListStyleType,
+  SignFunction,
+  SimpChineseFormalListStyleType,
+  SimpChineseInformalListStyleType,
+  SomaliListStyleType,
+  SpaceSeparatedColorNotation,
+  SquareListStyleType,
+  StretchSize,
+  StringListStyleType,
+  SymbolsListStyleType,
+  TamilListStyleType,
+  TargetText,
+  TeluguListStyleType,
+  TextDecorationThicknessPercent,
+  TextDecorationThicknessShorthand,
+  ThaiListStyleType,
+  TibetanListStyleType,
+  TigreListStyleType,
+  TigrinyaErAbegedeListStyleType,
+  TigrinyaErListStyleType,
+  TigrinyaEtAbegedeListStyleType,
+  TigrinyaEtListStyleType,
+  TradChineseFormalListStyleType,
+  TradChineseInformalListStyleType,
+  UpperAlphaListStyleType,
+  UpperArmenianListStyleType,
+  UpperHexadecimalListStyleType,
+  UpperLatinListStyleType,
+  UpperNorwegianListStyleType,
+  UpperRomanListStyleType,
+  VbUnit,
+  VhUnit,
+  ViUnit,
+  ViewTransition,
+  ViewportPercentageUnitsDynamic,
+  ViewportPercentageUnitsLarge,
+  ViewportPercentageUnitsSmall,
+  VmaxUnit,
+  VminUnit,
+  VwUnit,
+  WebkitFillAvailableSize,
+  XResolutionUnit,
+}
+
+impl Feature {
+  pub fn is_compatible(&self, browsers: Browsers) -> bool {
+    match self {
+      Feature::Selectors2 => {
+        if let Some(version) = browsers.ie {
+          if version < 458752 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 786432 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 131072 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.chrome {
+          if version < 262144 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 196864 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 589824 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 197120 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 131328 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 262144 {
+            return false;
+          }
+        }
+      }
+      Feature::Selectors3 => {
+        if let Some(version) = browsers.ie {
+          if version < 589824 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 786432 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 197888 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.chrome {
+          if version < 262144 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 197120 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 591104 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 197120 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 131328 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 262144 {
+            return false;
+          }
+        }
+      }
+      Feature::Gencontent | Feature::FirstLine => {
+        if let Some(version) = browsers.ie {
+          if version < 589824 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 786432 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 131072 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.chrome {
+          if version < 262144 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 196864 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 589824 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 197120 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 131328 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 262144 {
+            return false;
+          }
+        }
+      }
+      Feature::FirstLetter => {
+        if let Some(version) = browsers.ie {
+          if version < 589824 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 786432 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 197888 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.chrome {
+          if version < 589824 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 327936 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 722432 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 327680 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 196608 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 262144 {
+            return false;
+          }
+        }
+      }
+      Feature::InOutOfRange => {
+        if let Some(version) = browsers.edge {
+          if version < 5177344 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 3276800 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.chrome {
+          if version < 3473408 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 655616 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 2621440 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 656128 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 8847360 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 327680 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::FormValidation => {
+        if let Some(version) = browsers.ie {
+          if version < 655360 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 786432 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 262144 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.chrome {
+          if version < 655360 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 655616 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 655360 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 656128 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 263171 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 262144 {
+            return false;
+          }
+        }
+      }
+      Feature::AnyLink => {
+        if let Some(version) = browsers.edge {
+          if version < 5177344 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 3276800 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.chrome {
+          if version < 4259840 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 589824 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 3407872 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 589824 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 8847360 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 590336 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::DefaultPseudo => {
+        if let Some(version) = browsers.edge {
+          if version < 5177344 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 262144 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.chrome {
+          if version < 3342336 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 655616 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 2490368 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 656128 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 8847360 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 327680 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::DirSelector => {
+        if let Some(version) = browsers.edge {
+          if version < 7864320 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 3211264 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.chrome {
+          if version < 7864320 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 1049600 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 6946816 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 1049600 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 8847360 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 1638400 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::FocusWithin => {
+        if let Some(version) = browsers.edge {
+          if version < 5177344 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 3407872 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.chrome {
+          if version < 3932160 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 655616 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 3080192 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 656128 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 8847360 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 524800 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::FocusVisible => {
+        if let Some(version) = browsers.edge {
+          if version < 5636096 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 5570560 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.chrome {
+          if version < 5636096 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 984064 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 4718592 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 984064 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 8847360 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 917504 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::IndeterminatePseudo => {
+        if let Some(version) = browsers.edge {
+          if version < 5177344 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 3342336 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.chrome {
+          if version < 2555904 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 655616 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 1703936 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 656128 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 8847360 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 262144 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::IsSelector => {
+        if let Some(version) = browsers.edge {
+          if version < 5767168 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 5111808 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.chrome {
+          if version < 5767168 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 917504 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 4915200 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 917504 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 8847360 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 983040 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::OptionalPseudo => {
+        if let Some(version) = browsers.ie {
+          if version < 655360 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 786432 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 262144 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.chrome {
+          if version < 983040 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 327680 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 983040 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 327680 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 131840 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 262144 {
+            return false;
+          }
+        }
+      }
+      Feature::PlaceholderShown => {
+        if let Some(version) = browsers.edge {
+          if version < 5177344 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 3342336 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.chrome {
+          if version < 3080192 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 589824 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 2228224 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 589824 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 8847360 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 327680 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::Dialog => {
+        if let Some(version) = browsers.edge {
+          if version < 5177344 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 6422528 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.chrome {
+          if version < 2424832 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 984064 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 1572864 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 984064 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 8847360 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 262144 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::Fullscreen => {
+        if let Some(version) = browsers.edge {
+          if version < 5177344 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 4194304 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.chrome {
+          if version < 4653056 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 1049600 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 786688 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 655616 {
+            return false;
+          }
+        }
+        if browsers.android.is_some() || browsers.ie.is_some() || browsers.ios_saf.is_some() {
+          return false;
+        }
+      }
+      Feature::MarkerPseudo => {
+        if let Some(version) = browsers.edge {
+          if version < 5636096 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 4456448 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.chrome {
+          if version < 5636096 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 4718592 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 8847360 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 917504 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() || browsers.ios_saf.is_some() || browsers.safari.is_some() {
+          return false;
+        }
+      }
+      Feature::Placeholder => {
+        if let Some(version) = browsers.edge {
+          if version < 5177344 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 3342336 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.chrome {
+          if version < 3735552 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 655616 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 2883584 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 656128 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 8847360 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 459264 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::Selection => {
+        if let Some(version) = browsers.ie {
+          if version < 589824 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 786432 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 4063232 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.chrome {
+          if version < 262144 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 196864 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 591104 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 263168 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 262144 {
+            return false;
+          }
+        }
+        if browsers.ios_saf.is_some() {
+          return false;
+        }
+      }
+      Feature::CaseInsensitive => {
+        if let Some(version) = browsers.edge {
+          if version < 5177344 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 3080192 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.chrome {
+          if version < 3211264 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 589824 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 2359296 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 589824 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 8847360 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 327680 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::ReadOnlyWrite => {
+        if let Some(version) = browsers.edge {
+          if version < 851968 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 5111808 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.chrome {
+          if version < 2359296 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 589824 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 1507328 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 589824 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 8847360 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 262144 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::Autofill => {
+        if let Some(version) = browsers.chrome {
+          if version < 7208960 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 7208960 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 5636096 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 6291456 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 983040 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 983040 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 1376256 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 8847360 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::Namespaces => {
+        if let Some(version) = browsers.ie {
+          if version < 589824 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 786432 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 131072 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.chrome {
+          if version < 262144 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 262144 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 589824 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 262656 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 131328 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 262144 {
+            return false;
+          }
+        }
+      }
+      Feature::Shadowdomv1 => {
+        if let Some(version) = browsers.edge {
+          if version < 5177344 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 4128768 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.chrome {
+          if version < 3473408 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 655360 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 2621440 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 720896 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 8847360 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 393728 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::HexAlphaColors => {
+        if let Some(version) = browsers.edge {
+          if version < 5177344 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 3211264 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.chrome {
+          if version < 4063232 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 655360 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 3407872 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 655360 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 8847360 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 524800 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::Nesting => {
+        if let Some(version) = browsers.edge {
+          if version < 7864320 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 7667712 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.chrome {
+          if version < 7864320 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 1114624 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 6946816 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 1114624 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 8847360 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 1638400 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::NotSelectorList => {
+        if let Some(version) = browsers.edge {
+          if version < 5767168 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 5505024 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.chrome {
+          if version < 5767168 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 589824 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 4915200 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 589824 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 8847360 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 983040 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::HasSelector => {
+        if let Some(version) = browsers.edge {
+          if version < 6881280 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 7929856 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.chrome {
+          if version < 6881280 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 984064 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 5963776 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 984064 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 8847360 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 1310720 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::FontFamilySystemUi => {
+        if let Some(version) = browsers.edge {
+          if version < 5177344 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 6029312 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.chrome {
+          if version < 3670016 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 720896 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 2818048 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 720896 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 8847360 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 393728 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::ExtendedSystemFonts => {
+        if let Some(version) = browsers.safari {
+          if version < 852224 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 852992 {
+            return false;
+          }
+        }
+        if browsers.android.is_some()
+          || browsers.chrome.is_some()
+          || browsers.edge.is_some()
+          || browsers.firefox.is_some()
+          || browsers.ie.is_some()
+          || browsers.opera.is_some()
+          || browsers.samsung.is_some()
+        {
+          return false;
+        }
+      }
+      Feature::CalcFunction => {
+        if let Some(version) = browsers.edge {
+          if version < 786432 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 1048576 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.chrome {
+          if version < 1703936 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 393472 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 983040 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 458752 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 8847360 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 262144 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::CustomMediaQueries | Feature::FitContentFunctionSize | Feature::StretchSize => return false,
+      Feature::DoublePositionGradients => {
+        if let Some(version) = browsers.chrome {
+          if version < 4653056 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 5177344 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 4194304 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 3276800 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 786688 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 786944 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 655360 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 4653056 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::ClampFunction => {
+        if let Some(version) = browsers.chrome {
+          if version < 5177344 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 5177344 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 5177344 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 3735552 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 852224 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 852992 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 786432 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 5177344 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::PlaceSelf | Feature::PlaceItems => {
+        if let Some(version) = browsers.chrome {
+          if version < 3866624 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 5177344 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 2949120 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 2818048 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 720896 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 720896 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 458752 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 3866624 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::PlaceContent => {
+        if let Some(version) = browsers.chrome {
+          if version < 3866624 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 5177344 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 2949120 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 2818048 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 589824 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 589824 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 458752 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 3866624 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::OverflowShorthand => {
+        if let Some(version) = browsers.chrome {
+          if version < 4456448 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 5177344 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 3997696 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 3145728 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 852224 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 852992 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 655360 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 4456448 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::MediaRangeSyntax => {
+        if let Some(version) = browsers.chrome {
+          if version < 6815744 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 6815744 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 4128768 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 4653056 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 1049600 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 1049600 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 1310720 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 6815744 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::MediaIntervalSyntax => {
+        if let Some(version) = browsers.chrome {
+          if version < 6815744 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 6815744 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 6684672 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 4653056 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 1049600 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 1049600 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 1310720 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 6815744 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::LogicalBorders => {
+        if let Some(version) = browsers.chrome {
+          if version < 4521984 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 5177344 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 2686976 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 3145728 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 786688 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 786944 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 655360 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 4521984 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::LogicalBorderShorthand | Feature::LogicalMarginShorthand | Feature::LogicalPaddingShorthand => {
+        if let Some(version) = browsers.chrome {
+          if version < 5701632 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 5701632 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 4325376 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 4063232 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 917760 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 918784 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 917504 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 5701632 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::LogicalBorderRadius => {
+        if let Some(version) = browsers.chrome {
+          if version < 5832704 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 5832704 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 4325376 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 4128768 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 983040 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 983040 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 983040 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 5832704 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::LogicalMargin | Feature::LogicalPadding => {
+        if let Some(version) = browsers.chrome {
+          if version < 4521984 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 5177344 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 2686976 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 3145728 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 786688 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 786944 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 655360 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 5701632 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::LogicalInset => {
+        if let Some(version) = browsers.chrome {
+          if version < 5701632 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 5701632 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 4128768 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 4063232 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 917760 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 918784 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 917504 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 5701632 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::LogicalSize => {
+        if let Some(version) = browsers.chrome {
+          if version < 3735552 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 5177344 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 2686976 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 2818048 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 786688 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 786944 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 327680 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 3735552 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::LogicalTextAlign => {
+        if let Some(version) = browsers.chrome {
+          if version < 1179648 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 5177344 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 262144 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 917504 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 196864 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 131072 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 65536 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 2424832 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::LabColors => {
+        if let Some(version) = browsers.chrome {
+          if version < 7274496 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 7274496 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 7405568 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 4915200 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 983040 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 983040 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 1441792 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 7274496 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::OklabColors => {
+        if let Some(version) = browsers.chrome {
+          if version < 7274496 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 7274496 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 7405568 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 4915200 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 984064 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 984064 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 1441792 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 7274496 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::ColorFunction => {
+        if let Some(version) = browsers.chrome {
+          if version < 7274496 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 7274496 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 7405568 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 4915200 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 655616 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 656128 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 1441792 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 7274496 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::SpaceSeparatedColorNotation => {
+        if let Some(version) = browsers.chrome {
+          if version < 4259840 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 5177344 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 3407872 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 3080192 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 786688 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 786944 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 589824 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 4259840 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::TextDecorationThicknessPercent => {
+        if let Some(version) = browsers.chrome {
+          if version < 5701632 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 5701632 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 5177344 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 4063232 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 1115136 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 1115136 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 917504 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 5701632 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::TextDecorationThicknessShorthand => {
+        if let Some(version) = browsers.chrome {
+          if version < 5701632 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 5701632 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 5177344 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 4063232 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 917504 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 5701632 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() || browsers.ios_saf.is_some() || browsers.safari.is_some() {
+          return false;
+        }
+      }
+      Feature::Cue => {
+        if let Some(version) = browsers.chrome {
+          if version < 1703936 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 5177344 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 3604480 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 917504 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 458752 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 458752 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 66816 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 263168 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::CueFunction => {
+        if let Some(version) = browsers.chrome {
+          if version < 1703936 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 5177344 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 917504 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 458752 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 458752 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 66816 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 263168 {
+            return false;
+          }
+        }
+        if browsers.firefox.is_some() || browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::AnyPseudo => {
+        if let Some(version) = browsers.chrome {
+          if version < 1179648 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 5177344 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 262144 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 917504 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 327680 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 327680 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 65536 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 263168 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::PartPseudo => {
+        if let Some(version) = browsers.chrome {
+          if version < 4784128 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 5177344 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 5177344 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 3407872 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 852224 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 852992 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 720896 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 4784128 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::ImageSet => {
+        if let Some(version) = browsers.chrome {
+          if version < 1638400 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 5177344 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 5767168 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 917504 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 393216 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 393216 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 66816 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 263168 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::XResolutionUnit => {
+        if let Some(version) = browsers.chrome {
+          if version < 4456448 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 5177344 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 4063232 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 3145728 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 1048576 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 1048576 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 655360 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 4456448 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::NthChildOf => {
+        if let Some(version) = browsers.chrome {
+          if version < 7274496 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 7274496 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 7405568 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 4915200 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 589824 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 589824 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 1441792 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 7274496 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::MinFunction | Feature::MaxFunction => {
+        if let Some(version) = browsers.chrome {
+          if version < 5177344 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 5177344 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 5177344 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 3735552 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 721152 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 721664 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 786432 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 5177344 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::RoundFunction | Feature::RemFunction | Feature::ModFunction => {
+        if let Some(version) = browsers.chrome {
+          if version < 8192000 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 8192000 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 7733248 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 5439488 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 984064 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 984064 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 1769472 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 8192000 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::AbsFunction | Feature::SignFunction => {
+        if let Some(version) = browsers.firefox {
+          if version < 7733248 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 984064 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 984064 {
+            return false;
+          }
+        }
+        if browsers.android.is_some()
+          || browsers.chrome.is_some()
+          || browsers.edge.is_some()
+          || browsers.ie.is_some()
+          || browsers.opera.is_some()
+          || browsers.samsung.is_some()
+        {
+          return false;
+        }
+      }
+      Feature::HypotFunction => {
+        if let Some(version) = browsers.chrome {
+          if version < 7864320 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 7864320 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 7733248 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 5242880 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 984064 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 984064 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 1638400 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 7864320 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::GradientInterpolationHints => {
+        if let Some(version) = browsers.chrome {
+          if version < 2621440 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 5177344 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 2359296 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 1769472 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 458752 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 458752 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 262144 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 2621440 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::BorderImageRepeatRound => {
+        if let Some(version) = browsers.chrome {
+          if version < 1966080 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 786432 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 983040 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ie {
+          if version < 720896 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 1179648 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 590080 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 590592 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 131072 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 263168 {
+            return false;
+          }
+        }
+      }
+      Feature::BorderImageRepeatSpace => {
+        if let Some(version) = browsers.chrome {
+          if version < 3670016 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 786432 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 3276800 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ie {
+          if version < 720896 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 2818048 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 590080 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 590592 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 393216 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 3670016 {
+            return false;
+          }
+        }
+      }
+      Feature::FontSizeRem => {
+        if let Some(version) = browsers.chrome {
+          if version < 2752512 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 786432 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 2031616 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ie {
+          if version < 589824 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 1835008 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 458752 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 458752 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 262144 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 2752512 {
+            return false;
+          }
+        }
+      }
+      Feature::FontSizeXXXLarge => {
+        if let Some(version) = browsers.chrome {
+          if version < 5177344 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 5177344 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 5177344 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 3735552 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 1049600 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 1049600 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 786432 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 5177344 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::FontStyleObliqueAngle => {
+        if let Some(version) = browsers.chrome {
+          if version < 4063232 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 5177344 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 3997696 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 3014656 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 721152 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 721664 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 524288 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 4063232 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::FontWeightNumber => {
+        if let Some(version) = browsers.chrome {
+          if version < 4063232 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 1114112 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 3997696 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 3014656 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 720896 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 720896 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 524288 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 4063232 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::FontStretchPercentage => {
+        if let Some(version) = browsers.chrome {
+          if version < 4063232 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 1179648 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 3997696 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 3014656 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 721152 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 721664 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 524288 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 4063232 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::LightDark => {
+        if let Some(version) = browsers.chrome {
+          if version < 8060928 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 8060928 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 7864320 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 5373952 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 1115392 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 1115392 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 1769472 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 8060928 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::AccentSystemColor => {
+        if let Some(version) = browsers.firefox {
+          if version < 6750208 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 1049856 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 1049856 {
+            return false;
+          }
+        }
+        if browsers.android.is_some()
+          || browsers.chrome.is_some()
+          || browsers.edge.is_some()
+          || browsers.ie.is_some()
+          || browsers.opera.is_some()
+          || browsers.samsung.is_some()
+        {
+          return false;
+        }
+      }
+      Feature::AnimationTimelineShorthand => {
+        if let Some(version) = browsers.chrome {
+          if version < 7536640 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 7536640 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 5046272 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 1507328 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 7536640 {
+            return false;
+          }
+        }
+        if browsers.firefox.is_some()
+          || browsers.ie.is_some()
+          || browsers.ios_saf.is_some()
+          || browsers.safari.is_some()
+        {
+          return false;
+        }
+      }
+      Feature::ViewTransition => {
+        if let Some(version) = browsers.chrome {
+          if version < 7143424 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 7143424 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 4849664 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 1179648 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 1179648 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 1376256 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 7143424 {
+            return false;
+          }
+        }
+        if browsers.firefox.is_some() || browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::DetailsContent => {
+        if let Some(version) = browsers.chrome {
+          if version < 8585216 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 8585216 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 5701632 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 1180672 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 1180672 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 8585216 {
+            return false;
+          }
+        }
+        if browsers.firefox.is_some() || browsers.ie.is_some() || browsers.samsung.is_some() {
+          return false;
+        }
+      }
+      Feature::TargetText => {
+        if let Some(version) = browsers.chrome {
+          if version < 5832704 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 5832704 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 8585216 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 4128768 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 1180160 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 1180160 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 983040 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 5832704 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::Picker => {
+        if let Some(version) = browsers.chrome {
+          if version < 8781824 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 8781824 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 5767168 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 8781824 {
+            return false;
+          }
+        }
+        if browsers.firefox.is_some()
+          || browsers.ie.is_some()
+          || browsers.ios_saf.is_some()
+          || browsers.safari.is_some()
+          || browsers.samsung.is_some()
+        {
+          return false;
+        }
+      }
+      Feature::PickerIcon | Feature::Checkmark => {
+        if let Some(version) = browsers.chrome {
+          if version < 8716288 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 8716288 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 5767168 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 8716288 {
+            return false;
+          }
+        }
+        if browsers.firefox.is_some()
+          || browsers.ie.is_some()
+          || browsers.ios_saf.is_some()
+          || browsers.safari.is_some()
+          || browsers.samsung.is_some()
+        {
+          return false;
+        }
+      }
+      Feature::QUnit => {
+        if let Some(version) = browsers.chrome {
+          if version < 4128768 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 5177344 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 3211264 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 3014656 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 852224 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 852992 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 524288 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 4128768 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::CapUnit => {
+        if let Some(version) = browsers.chrome {
+          if version < 7733248 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 7733248 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 6356992 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 5177344 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 1114624 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 1114624 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 1638400 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 7733248 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::ChUnit => {
+        if let Some(version) = browsers.chrome {
+          if version < 1769472 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 786432 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 262144 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ie {
+          if version < 589824 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 983040 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 458752 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 458752 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 66816 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 263168 {
+            return false;
+          }
+        }
+      }
+      Feature::ContainerQueryLengthUnits => {
+        if let Some(version) = browsers.chrome {
+          if version < 6881280 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 6881280 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 7208960 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 4718592 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 1048576 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 1048576 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 1310720 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 6881280 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::EmUnit => {
+        if let Some(version) = browsers.chrome {
+          if version < 1179648 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 786432 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 262144 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ie {
+          if version < 196608 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 655616 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 65536 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 65536 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 65536 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 65536 {
+            return false;
+          }
+        }
+      }
+      Feature::ExUnit
+      | Feature::CircleListStyleType
+      | Feature::DecimalListStyleType
+      | Feature::DiscListStyleType
+      | Feature::SquareListStyleType => {
+        if let Some(version) = browsers.chrome {
+          if version < 1179648 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 786432 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 262144 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ie {
+          if version < 262144 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 655616 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 65536 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 65536 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 65536 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 263168 {
+            return false;
+          }
+        }
+      }
+      Feature::IcUnit => {
+        if let Some(version) = browsers.chrome {
+          if version < 6946816 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 6946816 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 6356992 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 4718592 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 984064 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 984064 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 1310720 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 6946816 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::LhUnit => {
+        if let Some(version) = browsers.chrome {
+          if version < 7143424 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 7143424 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 7864320 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 4849664 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 1049600 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 1049600 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 1376256 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 7143424 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::RcapUnit => {
+        if let Some(version) = browsers.chrome {
+          if version < 7733248 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 7733248 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 5177344 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 1114624 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 1114624 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 1638400 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 7733248 {
+            return false;
+          }
+        }
+        if browsers.firefox.is_some() || browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::RchUnit | Feature::RexUnit | Feature::RicUnit => {
+        if let Some(version) = browsers.chrome {
+          if version < 7274496 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 7274496 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 4915200 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 1114624 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 1114624 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 1441792 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 7274496 {
+            return false;
+          }
+        }
+        if browsers.firefox.is_some() || browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::RemUnit => {
+        if let Some(version) = browsers.chrome {
+          if version < 1179648 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 786432 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 262144 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ie {
+          if version < 589824 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 786432 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 327680 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 262144 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 65536 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 131072 {
+            return false;
+          }
+        }
+      }
+      Feature::RlhUnit => {
+        if let Some(version) = browsers.chrome {
+          if version < 7274496 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 7274496 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 7864320 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 4915200 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 1049600 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 1049600 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 1441792 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 7274496 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::VbUnit
+      | Feature::ViUnit
+      | Feature::ViewportPercentageUnitsDynamic
+      | Feature::ViewportPercentageUnitsLarge
+      | Feature::ViewportPercentageUnitsSmall => {
+        if let Some(version) = browsers.chrome {
+          if version < 7077888 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 7077888 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 6619136 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 4784128 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 984064 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 984064 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 1376256 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 7077888 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::VhUnit | Feature::VwUnit => {
+        if let Some(version) = browsers.chrome {
+          if version < 1638400 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 786432 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 1245184 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ie {
+          if version < 589824 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 917504 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 393216 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 393216 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 66816 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 263168 {
+            return false;
+          }
+        }
+      }
+      Feature::VmaxUnit => {
+        if let Some(version) = browsers.chrome {
+          if version < 1703936 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 1048576 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 1245184 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 917504 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 458752 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 458752 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 66816 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 66816 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::VminUnit => {
+        if let Some(version) = browsers.chrome {
+          if version < 1703936 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 786432 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 1245184 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ie {
+          if version < 655360 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 917504 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 458752 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 458752 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 66816 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 263168 {
+            return false;
+          }
+        }
+      }
+      Feature::ConicGradient | Feature::RepeatingConicGradient => {
+        if let Some(version) = browsers.chrome {
+          if version < 4521984 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 5177344 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 5439488 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 3145728 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 786688 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 786944 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 655360 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 4521984 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::LinearGradient | Feature::RepeatingLinearGradient => {
+        if let Some(version) = browsers.chrome {
+          if version < 1179648 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 786432 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 262144 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ie {
+          if version < 655360 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 720896 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 327936 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 327680 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 65536 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 263168 {
+            return false;
+          }
+        }
+      }
+      Feature::RadialGradient => {
+        if let Some(version) = browsers.chrome {
+          if version < 1179648 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 786432 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 262144 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ie {
+          if version < 655360 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 786432 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 327936 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 327680 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 65536 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 263168 {
+            return false;
+          }
+        }
+      }
+      Feature::RepeatingRadialGradient => {
+        if let Some(version) = browsers.chrome {
+          if version < 1179648 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 786432 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 655360 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ie {
+          if version < 655360 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 786432 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 327936 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 327680 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 65536 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 263168 {
+            return false;
+          }
+        }
+      }
+      Feature::AfarListStyleType
+      | Feature::AmharicListStyleType
+      | Feature::AmharicAbegedeListStyleType
+      | Feature::EthiopicListStyleType
+      | Feature::EthiopicAbegedeListStyleType
+      | Feature::EthiopicAbegedeAmEtListStyleType
+      | Feature::EthiopicAbegedeGezListStyleType
+      | Feature::EthiopicAbegedeTiErListStyleType
+      | Feature::EthiopicAbegedeTiEtListStyleType
+      | Feature::EthiopicHalehameAaErListStyleType
+      | Feature::EthiopicHalehameAaEtListStyleType
+      | Feature::EthiopicHalehameAmEtListStyleType
+      | Feature::EthiopicHalehameGezListStyleType
+      | Feature::EthiopicHalehameOmEtListStyleType
+      | Feature::EthiopicHalehameSidEtListStyleType
+      | Feature::EthiopicHalehameSoEtListStyleType
+      | Feature::EthiopicHalehameTigListStyleType
+      | Feature::LowerHexadecimalListStyleType
+      | Feature::LowerNorwegianListStyleType
+      | Feature::UpperHexadecimalListStyleType
+      | Feature::UpperNorwegianListStyleType => {
+        if let Some(version) = browsers.chrome {
+          if version < 1179648 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 5963776 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 917504 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 327680 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 262656 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 65536 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 196608 {
+            return false;
+          }
+        }
+        if browsers.firefox.is_some() || browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::ArabicIndicListStyleType
+      | Feature::BengaliListStyleType
+      | Feature::CjkEarthlyBranchListStyleType
+      | Feature::CjkHeavenlyStemListStyleType
+      | Feature::DevanagariListStyleType
+      | Feature::GujaratiListStyleType
+      | Feature::GurmukhiListStyleType
+      | Feature::KannadaListStyleType
+      | Feature::KhmerListStyleType
+      | Feature::LaoListStyleType
+      | Feature::MalayalamListStyleType
+      | Feature::MyanmarListStyleType
+      | Feature::OriyaListStyleType
+      | Feature::PersianListStyleType
+      | Feature::TeluguListStyleType
+      | Feature::ThaiListStyleType => {
+        if let Some(version) = browsers.chrome {
+          if version < 1179648 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 5177344 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 262144 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 917504 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 327680 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 262656 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 65536 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 263168 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::ArmenianListStyleType
+      | Feature::DecimalLeadingZeroListStyleType
+      | Feature::GeorgianListStyleType
+      | Feature::LowerAlphaListStyleType
+      | Feature::LowerGreekListStyleType
+      | Feature::LowerRomanListStyleType
+      | Feature::UpperAlphaListStyleType
+      | Feature::UpperLatinListStyleType
+      | Feature::UpperRomanListStyleType => {
+        if let Some(version) = browsers.chrome {
+          if version < 1179648 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 786432 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 262144 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ie {
+          if version < 524288 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 655616 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 65536 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 65536 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 65536 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 263168 {
+            return false;
+          }
+        }
+      }
+      Feature::AsterisksListStyleType | Feature::FootnotesListStyleType => {
+        if let Some(version) = browsers.chrome {
+          if version < 1179648 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 5963776 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 917504 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 327936 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 327680 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 65536 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 263168 {
+            return false;
+          }
+        }
+        if browsers.firefox.is_some() || browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::BinaryListStyleType
+      | Feature::OctalListStyleType
+      | Feature::OromoListStyleType
+      | Feature::SidamaListStyleType
+      | Feature::SomaliListStyleType
+      | Feature::TigreListStyleType
+      | Feature::TigrinyaErListStyleType
+      | Feature::TigrinyaErAbegedeListStyleType
+      | Feature::TigrinyaEtListStyleType
+      | Feature::TigrinyaEtAbegedeListStyleType => {
+        if let Some(version) = browsers.chrome {
+          if version < 1179648 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 5963776 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 917504 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 327680 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 262656 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 65536 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 263168 {
+            return false;
+          }
+        }
+        if browsers.firefox.is_some() || browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::CambodianListStyleType | Feature::MongolianListStyleType | Feature::TibetanListStyleType => {
+        if let Some(version) = browsers.chrome {
+          if version < 1179648 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 5177344 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 2162688 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 917504 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 327680 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 262656 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 65536 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 263168 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::CjkDecimalListStyleType => {
+        if let Some(version) = browsers.chrome {
+          if version < 5963776 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 5963776 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 1835008 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 4194304 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 983040 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 983040 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 1048576 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 5963776 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::DisclosureClosedListStyleType | Feature::DisclosureOpenListStyleType => {
+        if let Some(version) = browsers.chrome {
+          if version < 5832704 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 5832704 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 2162688 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 4128768 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 983040 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 983040 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 983040 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 5832704 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::EthiopicNumericListStyleType
+      | Feature::JapaneseFormalListStyleType
+      | Feature::JapaneseInformalListStyleType
+      | Feature::TamilListStyleType => {
+        if let Some(version) = browsers.chrome {
+          if version < 5963776 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 5963776 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 262144 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 4194304 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 983040 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 983040 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 1048576 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 5963776 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::HebrewListStyleType
+      | Feature::HiraganaListStyleType
+      | Feature::HiraganaIrohaListStyleType
+      | Feature::KatakanaListStyleType
+      | Feature::KatakanaIrohaListStyleType
+      | Feature::NoneListStyleType
+      | Feature::AutoSize => {
+        if let Some(version) = browsers.chrome {
+          if version < 1179648 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 786432 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 262144 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ie {
+          if version < 720896 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 917504 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 65536 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 65536 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 65536 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 263168 {
+            return false;
+          }
+        }
+      }
+      Feature::KoreanHangulFormalListStyleType
+      | Feature::KoreanHanjaFormalListStyleType
+      | Feature::KoreanHanjaInformalListStyleType => {
+        if let Some(version) = browsers.chrome {
+          if version < 2949120 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 5177344 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 1835008 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 2097152 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 983040 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 983040 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 327680 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 2949120 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::LowerArmenianListStyleType | Feature::UpperArmenianListStyleType => {
+        if let Some(version) = browsers.chrome {
+          if version < 1179648 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 5177344 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 2162688 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 917504 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 327936 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 327680 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 65536 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 263168 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::LowerLatinListStyleType => {
+        if let Some(version) = browsers.chrome {
+          if version < 1179648 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 786432 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 262144 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ie {
+          if version < 524288 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 655616 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 65536 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 65536 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 327680 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 263168 {
+            return false;
+          }
+        }
+      }
+      Feature::SimpChineseFormalListStyleType
+      | Feature::SimpChineseInformalListStyleType
+      | Feature::TradChineseFormalListStyleType
+      | Feature::TradChineseInformalListStyleType => {
+        if let Some(version) = browsers.chrome {
+          if version < 2949120 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 5177344 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 262144 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 2097152 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 983040 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 983040 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 327680 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 2949120 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::StringListStyleType => {
+        if let Some(version) = browsers.chrome {
+          if version < 5177344 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 5177344 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 2555904 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 3735552 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 917760 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 918784 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 786432 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 5177344 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::SymbolsListStyleType => {
+        if let Some(version) = browsers.firefox {
+          if version < 2293760 {
+            return false;
+          }
+        }
+        if browsers.android.is_some()
+          || browsers.chrome.is_some()
+          || browsers.edge.is_some()
+          || browsers.ie.is_some()
+          || browsers.ios_saf.is_some()
+          || browsers.opera.is_some()
+          || browsers.safari.is_some()
+          || browsers.samsung.is_some()
+        {
+          return false;
+        }
+      }
+      Feature::AnchorSizeSize => {
+        if let Some(version) = browsers.chrome {
+          if version < 8192000 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 8192000 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 5439488 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 1769472 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 8192000 {
+            return false;
+          }
+        }
+        if browsers.firefox.is_some()
+          || browsers.ie.is_some()
+          || browsers.ios_saf.is_some()
+          || browsers.safari.is_some()
+        {
+          return false;
+        }
+      }
+      Feature::FitContentSize => {
+        if let Some(version) = browsers.chrome {
+          if version < 1638400 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 5177344 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 262144 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 917504 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 458752 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 458752 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 66816 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 263168 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::IsAnimatableSize => {
+        if let Some(version) = browsers.chrome {
+          if version < 1703936 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 786432 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 1048576 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ie {
+          if version < 720896 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 917504 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 458752 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 458752 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 66816 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 263168 {
+            return false;
+          }
+        }
+      }
+      Feature::MaxContentSize => {
+        if let Some(version) = browsers.chrome {
+          if version < 1638400 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 5177344 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 262144 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 2818048 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 720896 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 720896 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 66816 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 263168 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::MinContentSize => {
+        if let Some(version) = browsers.chrome {
+          if version < 3014656 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 5177344 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version < 262144 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 2162688 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 720896 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 720896 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 327680 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 3014656 {
+            return false;
+          }
+        }
+        if browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::WebkitFillAvailableSize => {
+        if let Some(version) = browsers.chrome {
+          if version < 1638400 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version < 5177344 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version < 917504 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version < 458752 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 458752 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version < 327680 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version < 263168 {
+            return false;
+          }
+        }
+        if browsers.firefox.is_some() || browsers.ie.is_some() {
+          return false;
+        }
+      }
+      Feature::MozAvailableSize => {
+        if let Some(version) = browsers.firefox {
+          if version < 262144 {
+            return false;
+          }
+        }
+        if browsers.android.is_some()
+          || browsers.chrome.is_some()
+          || browsers.edge.is_some()
+          || browsers.ie.is_some()
+          || browsers.ios_saf.is_some()
+          || browsers.opera.is_some()
+          || browsers.safari.is_some()
+          || browsers.samsung.is_some()
+        {
+          return false;
+        }
+      }
+      Feature::P3Colors | Feature::LangSelectorList => {
+        if let Some(version) = browsers.safari {
+          if version < 655616 {
+            return false;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version < 656128 {
+            return false;
+          }
+        }
+        if browsers.android.is_some()
+          || browsers.chrome.is_some()
+          || browsers.edge.is_some()
+          || browsers.firefox.is_some()
+          || browsers.ie.is_some()
+          || browsers.opera.is_some()
+          || browsers.samsung.is_some()
+        {
+          return false;
+        }
+      }
+    }
+    true
+  }
+
+  pub fn is_partially_compatible(&self, targets: Browsers) -> bool {
+    let mut browsers = Browsers::default();
+    if targets.android.is_some() {
+      browsers.android = targets.android;
+      if self.is_compatible(browsers) {
+        return true;
+      }
+      browsers.android = None;
+    }
+    if targets.chrome.is_some() {
+      browsers.chrome = targets.chrome;
+      if self.is_compatible(browsers) {
+        return true;
+      }
+      browsers.chrome = None;
+    }
+    if targets.edge.is_some() {
+      browsers.edge = targets.edge;
+      if self.is_compatible(browsers) {
+        return true;
+      }
+      browsers.edge = None;
+    }
+    if targets.firefox.is_some() {
+      browsers.firefox = targets.firefox;
+      if self.is_compatible(browsers) {
+        return true;
+      }
+      browsers.firefox = None;
+    }
+    if targets.ie.is_some() {
+      browsers.ie = targets.ie;
+      if self.is_compatible(browsers) {
+        return true;
+      }
+      browsers.ie = None;
+    }
+    if targets.ios_saf.is_some() {
+      browsers.ios_saf = targets.ios_saf;
+      if self.is_compatible(browsers) {
+        return true;
+      }
+      browsers.ios_saf = None;
+    }
+    if targets.opera.is_some() {
+      browsers.opera = targets.opera;
+      if self.is_compatible(browsers) {
+        return true;
+      }
+      browsers.opera = None;
+    }
+    if targets.safari.is_some() {
+      browsers.safari = targets.safari;
+      if self.is_compatible(browsers) {
+        return true;
+      }
+      browsers.safari = None;
+    }
+    if targets.samsung.is_some() {
+      browsers.samsung = targets.samsung;
+      if self.is_compatible(browsers) {
+        return true;
+      }
+      browsers.samsung = None;
+    }
+
+    false
+  }
+}
diff --git a/src/context.rs b/src/context.rs
new file mode 100644
index 0000000..9fb6201
--- /dev/null
+++ b/src/context.rs
@@ -0,0 +1,234 @@
+use std::collections::HashSet;
+
+use crate::compat::Feature;
+use crate::declaration::DeclarationBlock;
+use crate::media_query::{
+  MediaCondition, MediaFeatureId, MediaFeatureName, MediaFeatureValue, MediaList, MediaQuery, MediaType,
+  QueryFeature,
+};
+use crate::properties::custom::UnparsedProperty;
+use crate::properties::Property;
+use crate::rules::media::MediaRule;
+use crate::rules::supports::{SupportsCondition, SupportsRule};
+use crate::rules::{style::StyleRule, CssRule, CssRuleList};
+use crate::selector::{Direction, PseudoClass};
+use crate::targets::Targets;
+use crate::values::ident::Ident;
+use crate::vendor_prefix::VendorPrefix;
+use parcel_selectors::parser::Component;
+
+#[derive(Debug)]
+pub(crate) struct SupportsEntry<'i> {
+  pub condition: SupportsCondition<'i>,
+  pub declarations: Vec<Property<'i>>,
+  pub important_declarations: Vec<Property<'i>>,
+}
+
+#[derive(Debug, PartialEq)]
+pub(crate) enum DeclarationContext {
+  None,
+  StyleRule,
+  Keyframes,
+  StyleAttribute,
+}
+
+#[derive(Debug)]
+pub(crate) struct PropertyHandlerContext<'i, 'o> {
+  pub targets: Targets,
+  pub is_important: bool,
+  supports: Vec<SupportsEntry<'i>>,
+  ltr: Vec<Property<'i>>,
+  rtl: Vec<Property<'i>>,
+  dark: Vec<Property<'i>>,
+  pub context: DeclarationContext,
+  pub unused_symbols: &'o HashSet<String>,
+}
+
+impl<'i, 'o> PropertyHandlerContext<'i, 'o> {
+  pub fn new(targets: Targets, unused_symbols: &'o HashSet<String>) -> Self {
+    PropertyHandlerContext {
+      targets,
+      is_important: false,
+      supports: Vec::new(),
+      ltr: Vec::new(),
+      rtl: Vec::new(),
+      dark: Vec::new(),
+      context: DeclarationContext::None,
+      unused_symbols,
+    }
+  }
+
+  pub fn child(&self, context: DeclarationContext) -> Self {
+    PropertyHandlerContext {
+      targets: self.targets,
+      is_important: false,
+      supports: Vec::new(),
+      ltr: Vec::new(),
+      rtl: Vec::new(),
+      dark: Vec::new(),
+      context,
+      unused_symbols: self.unused_symbols,
+    }
+  }
+
+  pub fn should_compile_logical(&self, feature: Feature) -> bool {
+    // Don't convert logical properties in style attributes because
+    // our fallbacks rely on extra rules to define --ltr and --rtl.
+    if self.context == DeclarationContext::StyleAttribute {
+      return false;
+    }
+
+    self.targets.should_compile_logical(feature)
+  }
+
+  pub fn add_logical_rule(&mut self, ltr: Property<'i>, rtl: Property<'i>) {
+    self.ltr.push(ltr);
+    self.rtl.push(rtl);
+  }
+
+  pub fn add_dark_rule(&mut self, property: Property<'i>) {
+    self.dark.push(property);
+  }
+
+  pub fn get_additional_rules<T>(&self, style_rule: &StyleRule<'i, T>) -> Vec<CssRule<'i, T>> {
+    // TODO: :dir/:lang raises the specificity of the selector. Use :where to lower it?
+    let mut dest = Vec::new();
+
+    macro_rules! rule {
+      ($dir: ident, $decls: ident) => {
+        let mut selectors = style_rule.selectors.clone();
+        for selector in &mut selectors.0 {
+          selector.append(Component::NonTSPseudoClass(PseudoClass::Dir {
+            direction: Direction::$dir,
+          }));
+        }
+
+        let rule = StyleRule {
+          selectors,
+          vendor_prefix: VendorPrefix::None,
+          declarations: DeclarationBlock {
+            declarations: self.$decls.clone(),
+            important_declarations: vec![],
+          },
+          rules: CssRuleList(vec![]),
+          loc: style_rule.loc.clone(),
+        };
+
+        dest.push(CssRule::Style(rule));
+      };
+    }
+
+    if !self.ltr.is_empty() {
+      rule!(Ltr, ltr);
+    }
+
+    if !self.rtl.is_empty() {
+      rule!(Rtl, rtl);
+    }
+
+    if !self.dark.is_empty() {
+      dest.push(CssRule::Media(MediaRule {
+        query: MediaList {
+          media_queries: vec![MediaQuery {
+            qualifier: None,
+            media_type: MediaType::All,
+            condition: Some(MediaCondition::Feature(QueryFeature::Plain {
+              name: MediaFeatureName::Standard(MediaFeatureId::PrefersColorScheme),
+              value: MediaFeatureValue::Ident(Ident("dark".into())),
+            })),
+          }],
+        },
+        rules: CssRuleList(vec![CssRule::Style(StyleRule {
+          selectors: style_rule.selectors.clone(),
+          vendor_prefix: VendorPrefix::None,
+          declarations: DeclarationBlock {
+            declarations: self.dark.clone(),
+            important_declarations: vec![],
+          },
+          rules: CssRuleList(vec![]),
+          loc: style_rule.loc.clone(),
+        })]),
+        loc: style_rule.loc.clone(),
+      }))
+    }
+
+    dest
+  }
+
+  pub fn add_conditional_property(&mut self, condition: SupportsCondition<'i>, property: Property<'i>) {
+    if self.context != DeclarationContext::StyleRule {
+      return;
+    }
+
+    if let Some(entry) = self.supports.iter_mut().find(|supports| condition == supports.condition) {
+      if self.is_important {
+        entry.important_declarations.push(property);
+      } else {
+        entry.declarations.push(property);
+      }
+    } else {
+      let mut important_declarations = Vec::new();
+      let mut declarations = Vec::new();
+      if self.is_important {
+        important_declarations.push(property);
+      } else {
+        declarations.push(property);
+      }
+      self.supports.push(SupportsEntry {
+        condition,
+        important_declarations,
+        declarations,
+      });
+    }
+  }
+
+  pub fn add_unparsed_fallbacks(&mut self, unparsed: &mut UnparsedProperty<'i>) {
+    if self.context != DeclarationContext::StyleRule && self.context != DeclarationContext::StyleAttribute {
+      return;
+    }
+
+    let fallbacks = unparsed.value.get_fallbacks(self.targets);
+    for (condition, fallback) in fallbacks {
+      self.add_conditional_property(
+        condition,
+        Property::Unparsed(UnparsedProperty {
+          property_id: unparsed.property_id.clone(),
+          value: fallback,
+        }),
+      );
+    }
+  }
+
+  pub fn get_supports_rules<T>(&self, style_rule: &StyleRule<'i, T>) -> Vec<CssRule<'i, T>> {
+    if self.supports.is_empty() {
+      return Vec::new();
+    }
+
+    let mut dest = Vec::new();
+    for entry in &self.supports {
+      dest.push(CssRule::Supports(SupportsRule {
+        condition: entry.condition.clone(),
+        rules: CssRuleList(vec![CssRule::Style(StyleRule {
+          selectors: style_rule.selectors.clone(),
+          vendor_prefix: VendorPrefix::None,
+          declarations: DeclarationBlock {
+            declarations: entry.declarations.clone(),
+            important_declarations: entry.important_declarations.clone(),
+          },
+          rules: CssRuleList(vec![]),
+          loc: style_rule.loc.clone(),
+        })]),
+        loc: style_rule.loc.clone(),
+      }));
+    }
+
+    dest
+  }
+
+  pub fn reset(&mut self) {
+    self.supports.clear();
+    self.ltr.clear();
+    self.rtl.clear();
+    self.dark.clear();
+  }
+}
diff --git a/src/css_modules.rs b/src/css_modules.rs
new file mode 100644
index 0000000..ce7008d
--- /dev/null
+++ b/src/css_modules.rs
@@ -0,0 +1,548 @@
+//! CSS module exports.
+//!
+//! [CSS modules](https://github.com/css-modules/css-modules) are a way of locally scoping names in a
+//! CSS file. This includes class names, ids, keyframe animation names, and any other places where the
+//! [CustomIdent](super::values::ident::CustomIdent) type is used.
+//!
+//! CSS modules can be enabled using the `css_modules` option when parsing a style sheet. When the
+//! style sheet is printed, hashes will be added to any declared names, and references to those names
+//! will be updated accordingly. A map of the original names to compiled (hashed) names will be returned.
+
+use crate::error::PrinterErrorKind;
+use crate::properties::css_modules::{Composes, Specifier};
+use crate::selector::SelectorList;
+use data_encoding::{Encoding, Specification};
+use lazy_static::lazy_static;
+use pathdiff::diff_paths;
+#[cfg(any(feature = "serde", feature = "nodejs"))]
+use serde::Serialize;
+use smallvec::{smallvec, SmallVec};
+use std::borrow::Cow;
+use std::collections::hash_map::DefaultHasher;
+use std::collections::HashMap;
+use std::fmt::Write;
+use std::hash::{Hash, Hasher};
+use std::path::Path;
+
+/// Configuration for CSS modules.
+#[derive(Clone, Debug)]
+pub struct Config<'i> {
+  /// The name pattern to use when renaming class names and other identifiers.
+  /// Default is `[hash]_[local]`.
+  pub pattern: Pattern<'i>,
+  /// Whether to rename dashed identifiers, e.g. custom properties.
+  pub dashed_idents: bool,
+  /// Whether to scope animation names.
+  /// Default is `true`.
+  pub animation: bool,
+  /// Whether to scope grid names.
+  /// Default is `true`.
+  pub grid: bool,
+  /// Whether to scope custom identifiers
+  /// Default is `true`.
+  pub custom_idents: bool,
+  /// Whether to scope container names.
+  /// Default is `true`.
+  pub container: bool,
+  /// Whether to check for pure CSS modules.
+  pub pure: bool,
+}
+
+impl<'i> Default for Config<'i> {
+  fn default() -> Self {
+    Config {
+      pattern: Default::default(),
+      dashed_idents: Default::default(),
+      animation: true,
+      grid: true,
+      container: true,
+      custom_idents: true,
+      pure: false,
+    }
+  }
+}
+
+/// A CSS modules class name pattern.
+#[derive(Clone, Debug)]
+pub struct Pattern<'i> {
+  /// The list of segments in the pattern.
+  pub segments: SmallVec<[Segment<'i>; 2]>,
+}
+
+impl<'i> Default for Pattern<'i> {
+  fn default() -> Self {
+    Pattern {
+      segments: smallvec![Segment::Hash, Segment::Literal("_"), Segment::Local],
+    }
+  }
+}
+
+/// An error that occurred while parsing a CSS modules name pattern.
+#[derive(Debug)]
+pub enum PatternParseError {
+  /// An unknown placeholder segment was encountered at the given index.
+  UnknownPlaceholder(String, usize),
+  /// An opening bracket with no following closing bracket was found at the given index.
+  UnclosedBrackets(usize),
+}
+
+impl std::fmt::Display for PatternParseError {
+  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+    use PatternParseError::*;
+    match self {
+      UnknownPlaceholder(p, i) => write!(
+        f,
+        "Error parsing CSS modules pattern: unknown placeholder \"{}\" at index {}",
+        p, i
+      ),
+      UnclosedBrackets(i) => write!(f, "Error parsing CSS modules pattern: unclosed brackets at index {}", i),
+    }
+  }
+}
+
+impl std::error::Error for PatternParseError {}
+
+impl<'i> Pattern<'i> {
+  /// Parse a pattern from a string.
+  pub fn parse(mut input: &'i str) -> Result<Self, PatternParseError> {
+    let mut segments = SmallVec::new();
+    let mut start_idx: usize = 0;
+    while !input.is_empty() {
+      if input.starts_with('[') {
+        if let Some(end_idx) = input.find(']') {
+          let segment = match &input[0..=end_idx] {
+            "[name]" => Segment::Name,
+            "[local]" => Segment::Local,
+            "[hash]" => Segment::Hash,
+            "[content-hash]" => Segment::ContentHash,
+            s => return Err(PatternParseError::UnknownPlaceholder(s.into(), start_idx)),
+          };
+          segments.push(segment);
+          start_idx += end_idx + 1;
+          input = &input[end_idx + 1..];
+        } else {
+          return Err(PatternParseError::UnclosedBrackets(start_idx));
+        }
+      } else {
+        let end_idx = input.find('[').unwrap_or_else(|| input.len());
+        segments.push(Segment::Literal(&input[0..end_idx]));
+        start_idx += end_idx;
+        input = &input[end_idx..];
+      }
+    }
+
+    Ok(Pattern { segments })
+  }
+
+  /// Whether the pattern contains any `[content-hash]` segments.
+  pub fn has_content_hash(&self) -> bool {
+    self.segments.iter().any(|s| matches!(s, Segment::ContentHash))
+  }
+
+  /// Write the substituted pattern to a destination.
+  pub fn write<W, E>(
+    &self,
+    hash: &str,
+    path: &Path,
+    local: &str,
+    content_hash: &str,
+    mut write: W,
+  ) -> Result<(), E>
+  where
+    W: FnMut(&str) -> Result<(), E>,
+  {
+    for segment in &self.segments {
+      match segment {
+        Segment::Literal(s) => {
+          write(s)?;
+        }
+        Segment::Name => {
+          let stem = path.file_stem().unwrap().to_str().unwrap();
+          if stem.contains('.') {
+            write(&stem.replace('.', "-"))?;
+          } else {
+            write(stem)?;
+          }
+        }
+        Segment::Local => {
+          write(local)?;
+        }
+        Segment::Hash => {
+          write(hash)?;
+        }
+        Segment::ContentHash => {
+          write(content_hash)?;
+        }
+      }
+    }
+    Ok(())
+  }
+
+  #[inline]
+  fn write_to_string(
+    &self,
+    mut res: String,
+    hash: &str,
+    path: &Path,
+    local: &str,
+    content_hash: &str,
+  ) -> Result<String, std::fmt::Error> {
+    self.write(hash, path, local, content_hash, |s| res.write_str(s))?;
+    Ok(res)
+  }
+}
+
+/// A segment in a CSS modules class name pattern.
+///
+/// See [Pattern](Pattern).
+#[derive(Clone, Debug)]
+pub enum Segment<'i> {
+  /// A literal string segment.
+  Literal(&'i str),
+  /// The base file name.
+  Name,
+  /// The original class name.
+  Local,
+  /// A hash of the file name.
+  Hash,
+  /// A hash of the file contents.
+  ContentHash,
+}
+
+/// A referenced name within a CSS module, e.g. via the `composes` property.
+///
+/// See [CssModuleExport](CssModuleExport).
+#[derive(PartialEq, Debug, Clone)]
+#[cfg_attr(any(feature = "serde", feature = "nodejs"), derive(Serialize))]
+#[cfg_attr(
+  any(feature = "serde", feature = "nodejs"),
+  serde(tag = "type", rename_all = "lowercase")
+)]
+pub enum CssModuleReference {
+  /// A local reference.
+  Local {
+    /// The local (compiled) name for the reference.
+    name: String,
+  },
+  /// A global reference.
+  Global {
+    /// The referenced global name.
+    name: String,
+  },
+  /// A reference to an export in a different file.
+  Dependency {
+    /// The name to reference within the dependency.
+    name: String,
+    /// The dependency specifier for the referenced file.
+    specifier: String,
+  },
+}
+
+/// An exported value from a CSS module.
+#[derive(PartialEq, Debug, Clone)]
+#[cfg_attr(any(feature = "serde", feature = "nodejs"), derive(Serialize))]
+#[cfg_attr(any(feature = "serde", feature = "nodejs"), serde(rename_all = "camelCase"))]
+pub struct CssModuleExport {
+  /// The local (compiled) name for this export.
+  pub name: String,
+  /// Other names that are composed by this export.
+  pub composes: Vec<CssModuleReference>,
+  /// Whether the export is referenced in this file.
+  pub is_referenced: bool,
+}
+
+/// A map of exported names to values.
+pub type CssModuleExports = HashMap<String, CssModuleExport>;
+
+/// A map of placeholders to references.
+pub type CssModuleReferences = HashMap<String, CssModuleReference>;
+
+lazy_static! {
+  static ref ENCODER: Encoding = {
+    let mut spec = Specification::new();
+    spec
+      .symbols
+      .push_str("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890_-");
+    spec.encoding().unwrap()
+  };
+}
+
+pub(crate) struct CssModule<'a, 'b, 'c> {
+  pub config: &'a Config<'b>,
+  pub sources: Vec<&'c Path>,
+  pub hashes: Vec<String>,
+  pub content_hashes: &'a Option<Vec<String>>,
+  pub exports_by_source_index: Vec<CssModuleExports>,
+  pub references: &'a mut HashMap<String, CssModuleReference>,
+}
+
+impl<'a, 'b, 'c> CssModule<'a, 'b, 'c> {
+  pub fn new(
+    config: &'a Config<'b>,
+    sources: &'c Vec<String>,
+    project_root: Option<&'c str>,
+    references: &'a mut HashMap<String, CssModuleReference>,
+    content_hashes: &'a Option<Vec<String>>,
+  ) -> Self {
+    let project_root = project_root.map(|p| Path::new(p));
+    let sources: Vec<&Path> = sources.iter().map(|filename| Path::new(filename)).collect();
+    let hashes = sources
+      .iter()
+      .map(|path| {
+        // Make paths relative to project root so hashes are stable.
+        let source = match project_root {
+          Some(project_root) if path.is_absolute() => {
+            diff_paths(path, project_root).map_or(Cow::Borrowed(*path), Cow::Owned)
+          }
+          _ => Cow::Borrowed(*path),
+        };
+        hash(
+          &source.to_string_lossy(),
+          matches!(config.pattern.segments[0], Segment::Hash),
+        )
+      })
+      .collect();
+    Self {
+      config,
+      exports_by_source_index: sources.iter().map(|_| HashMap::new()).collect(),
+      sources,
+      hashes,
+      content_hashes,
+      references,
+    }
+  }
+
+  pub fn add_local(&mut self, exported: &str, local: &str, source_index: u32) {
+    self.exports_by_source_index[source_index as usize]
+      .entry(exported.into())
+      .or_insert_with(|| CssModuleExport {
+        name: self
+          .config
+          .pattern
+          .write_to_string(
+            String::new(),
+            &self.hashes[source_index as usize],
+            &self.sources[source_index as usize],
+            local,
+            if let Some(content_hashes) = &self.content_hashes {
+              &content_hashes[source_index as usize]
+            } else {
+              ""
+            },
+          )
+          .unwrap(),
+        composes: vec![],
+        is_referenced: false,
+      });
+  }
+
+  pub fn add_dashed(&mut self, local: &str, source_index: u32) {
+    self.exports_by_source_index[source_index as usize]
+      .entry(local.into())
+      .or_insert_with(|| CssModuleExport {
+        name: self
+          .config
+          .pattern
+          .write_to_string(
+            "--".into(),
+            &self.hashes[source_index as usize],
+            &self.sources[source_index as usize],
+            &local[2..],
+            if let Some(content_hashes) = &self.content_hashes {
+              &content_hashes[source_index as usize]
+            } else {
+              ""
+            },
+          )
+          .unwrap(),
+        composes: vec![],
+        is_referenced: false,
+      });
+  }
+
+  pub fn reference(&mut self, name: &str, source_index: u32) {
+    match self.exports_by_source_index[source_index as usize].entry(name.into()) {
+      std::collections::hash_map::Entry::Occupied(mut entry) => {
+        entry.get_mut().is_referenced = true;
+      }
+      std::collections::hash_map::Entry::Vacant(entry) => {
+        entry.insert(CssModuleExport {
+          name: self
+            .config
+            .pattern
+            .write_to_string(
+              String::new(),
+              &self.hashes[source_index as usize],
+              &self.sources[source_index as usize],
+              name,
+              if let Some(content_hashes) = &self.content_hashes {
+                &content_hashes[source_index as usize]
+              } else {
+                ""
+              },
+            )
+            .unwrap(),
+          composes: vec![],
+          is_referenced: true,
+        });
+      }
+    }
+  }
+
+  pub fn reference_dashed(&mut self, name: &str, from: &Option<Specifier>, source_index: u32) -> Option<String> {
+    let (reference, key) = match from {
+      Some(Specifier::Global) => return Some(name[2..].into()),
+      Some(Specifier::File(file)) => (
+        CssModuleReference::Dependency {
+          name: name.to_string(),
+          specifier: file.to_string(),
+        },
+        file.as_ref(),
+      ),
+      Some(Specifier::SourceIndex(source_index)) => {
+        return Some(
+          self
+            .config
+            .pattern
+            .write_to_string(
+              String::new(),
+              &self.hashes[*source_index as usize],
+              &self.sources[*source_index as usize],
+              &name[2..],
+              if let Some(content_hashes) = &self.content_hashes {
+                &content_hashes[*source_index as usize]
+              } else {
+                ""
+              },
+            )
+            .unwrap(),
+        )
+      }
+      None => {
+        // Local export. Mark as used.
+        match self.exports_by_source_index[source_index as usize].entry(name.into()) {
+          std::collections::hash_map::Entry::Occupied(mut entry) => {
+            entry.get_mut().is_referenced = true;
+          }
+          std::collections::hash_map::Entry::Vacant(entry) => {
+            entry.insert(CssModuleExport {
+              name: self
+                .config
+                .pattern
+                .write_to_string(
+                  "--".into(),
+                  &self.hashes[source_index as usize],
+                  &self.sources[source_index as usize],
+                  &name[2..],
+                  if let Some(content_hashes) = &self.content_hashes {
+                    &content_hashes[source_index as usize]
+                  } else {
+                    ""
+                  },
+                )
+                .unwrap(),
+              composes: vec![],
+              is_referenced: true,
+            });
+          }
+        }
+        return None;
+      }
+    };
+
+    let hash = hash(
+      &format!("{}_{}_{}", self.hashes[source_index as usize], name, key),
+      false,
+    );
+    let name = format!("--{}", hash);
+
+    self.references.insert(name.clone(), reference);
+    Some(hash)
+  }
+
+  pub fn handle_composes(
+    &mut self,
+    selectors: &SelectorList,
+    composes: &Composes,
+    source_index: u32,
+  ) -> Result<(), PrinterErrorKind> {
+    for sel in &selectors.0 {
+      if sel.len() == 1 {
+        match sel.iter_raw_match_order().next().unwrap() {
+          parcel_selectors::parser::Component::Class(ref id) => {
+            for name in &composes.names {
+              let reference = match &composes.from {
+                None => CssModuleReference::Local {
+                  name: self
+                    .config
+                    .pattern
+                    .write_to_string(
+                      String::new(),
+                      &self.hashes[source_index as usize],
+                      &self.sources[source_index as usize],
+                      name.0.as_ref(),
+                      if let Some(content_hashes) = &self.content_hashes {
+                        &content_hashes[source_index as usize]
+                      } else {
+                        ""
+                      },
+                    )
+                    .unwrap(),
+                },
+                Some(Specifier::SourceIndex(dep_source_index)) => {
+                  if let Some(entry) =
+                    self.exports_by_source_index[*dep_source_index as usize].get(&name.0.as_ref().to_owned())
+                  {
+                    let name = entry.name.clone();
+                    let composes = entry.composes.clone();
+                    let export = self.exports_by_source_index[source_index as usize]
+                      .get_mut(&id.0.as_ref().to_owned())
+                      .unwrap();
+
+                    export.composes.push(CssModuleReference::Local { name });
+                    export.composes.extend(composes);
+                  }
+                  continue;
+                }
+                Some(Specifier::Global) => CssModuleReference::Global {
+                  name: name.0.as_ref().into(),
+                },
+                Some(Specifier::File(file)) => CssModuleReference::Dependency {
+                  name: name.0.to_string(),
+                  specifier: file.to_string(),
+                },
+              };
+
+              let export = self.exports_by_source_index[source_index as usize]
+                .get_mut(&id.0.as_ref().to_owned())
+                .unwrap();
+              if !export.composes.contains(&reference) {
+                export.composes.push(reference);
+              }
+            }
+            continue;
+          }
+          _ => {}
+        }
+      }
+
+      // The composes property can only be used within a simple class selector.
+      return Err(PrinterErrorKind::InvalidComposesSelector);
+    }
+
+    Ok(())
+  }
+}
+
+pub(crate) fn hash(s: &str, at_start: bool) -> String {
+  let mut hasher = DefaultHasher::new();
+  s.hash(&mut hasher);
+  let hash = hasher.finish() as u32;
+
+  let hash = ENCODER.encode(&hash.to_le_bytes());
+  if at_start && matches!(hash.as_bytes()[0], b'0'..=b'9') {
+    format!("_{}", hash)
+  } else {
+    hash
+  }
+}
diff --git a/src/declaration.rs b/src/declaration.rs
new file mode 100644
index 0000000..0dd3da6
--- /dev/null
+++ b/src/declaration.rs
@@ -0,0 +1,731 @@
+//! CSS declarations.
+
+use std::borrow::Cow;
+use std::ops::Range;
+
+use crate::context::{DeclarationContext, PropertyHandlerContext};
+use crate::error::{ParserError, PrinterError, PrinterErrorKind};
+use crate::parser::ParserOptions;
+use crate::printer::Printer;
+use crate::properties::box_shadow::BoxShadowHandler;
+use crate::properties::custom::{CustomProperty, CustomPropertyName};
+use crate::properties::masking::MaskHandler;
+use crate::properties::text::{Direction, UnicodeBidi};
+use crate::properties::{
+  align::AlignHandler,
+  animation::AnimationHandler,
+  background::BackgroundHandler,
+  border::BorderHandler,
+  contain::ContainerHandler,
+  display::DisplayHandler,
+  flex::FlexHandler,
+  font::FontHandler,
+  grid::GridHandler,
+  list::ListStyleHandler,
+  margin_padding::*,
+  outline::OutlineHandler,
+  overflow::OverflowHandler,
+  position::PositionHandler,
+  prefix_handler::{FallbackHandler, PrefixHandler},
+  size::SizeHandler,
+  text::TextDecorationHandler,
+  transform::TransformHandler,
+  transition::TransitionHandler,
+  ui::ColorSchemeHandler,
+};
+use crate::properties::{Property, PropertyId};
+use crate::selector::SelectorList;
+use crate::traits::{PropertyHandler, ToCss};
+use crate::values::ident::DashedIdent;
+use crate::values::string::CowArcStr;
+#[cfg(feature = "visitor")]
+use crate::visitor::Visit;
+use cssparser::*;
+use indexmap::IndexMap;
+use smallvec::SmallVec;
+
+/// A CSS declaration block.
+///
+/// Properties are separated into a list of `!important` declararations,
+/// and a list of normal declarations. This reduces memory usage compared
+/// with storing a boolean along with each property.
+#[derive(Debug, PartialEq, Clone, Default)]
+#[cfg_attr(feature = "visitor", derive(Visit), visit(visit_declaration_block, PROPERTIES))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(rename_all = "camelCase")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub struct DeclarationBlock<'i> {
+  /// A list of `!important` declarations in the block.
+  #[cfg_attr(feature = "serde", serde(borrow, default))]
+  pub important_declarations: Vec<Property<'i>>,
+  /// A list of normal declarations in the block.
+  #[cfg_attr(feature = "serde", serde(default))]
+  pub declarations: Vec<Property<'i>>,
+}
+
+impl<'i> DeclarationBlock<'i> {
+  /// Parses a declaration block from CSS syntax.
+  pub fn parse<'a, 'o, 't>(
+    input: &mut Parser<'i, 't>,
+    options: &'a ParserOptions<'o, 'i>,
+  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let mut important_declarations = DeclarationList::new();
+    let mut declarations = DeclarationList::new();
+    let mut decl_parser = PropertyDeclarationParser {
+      important_declarations: &mut important_declarations,
+      declarations: &mut declarations,
+      options,
+    };
+    let mut parser = RuleBodyParser::new(input, &mut decl_parser);
+    while let Some(res) = parser.next() {
+      if let Err((err, _)) = res {
+        if options.error_recovery {
+          options.warn(err);
+          continue;
+        }
+        return Err(err);
+      }
+    }
+
+    Ok(DeclarationBlock {
+      important_declarations,
+      declarations,
+    })
+  }
+
+  /// Parses a declaration block from a string.
+  pub fn parse_string<'o>(
+    input: &'i str,
+    options: ParserOptions<'o, 'i>,
+  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let mut input = ParserInput::new(input);
+    let mut parser = Parser::new(&mut input);
+    let result = Self::parse(&mut parser, &options)?;
+    parser.expect_exhausted()?;
+    Ok(result)
+  }
+
+  /// Returns an empty declaration block.
+  pub fn new() -> Self {
+    Self {
+      declarations: vec![],
+      important_declarations: vec![],
+    }
+  }
+
+  /// Returns the total number of declarations in the block.
+  pub fn len(&self) -> usize {
+    self.declarations.len() + self.important_declarations.len()
+  }
+}
+
+impl<'i> ToCss for DeclarationBlock<'i> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    let len = self.declarations.len() + self.important_declarations.len();
+    let mut i = 0;
+
+    macro_rules! write {
+      ($decls: expr, $important: literal) => {
+        for decl in &$decls {
+          decl.to_css(dest, $important)?;
+          if i != len - 1 {
+            dest.write_char(';')?;
+            dest.whitespace()?;
+          }
+          i += 1;
+        }
+      };
+    }
+
+    write!(self.declarations, false);
+    write!(self.important_declarations, true);
+    Ok(())
+  }
+}
+
+impl<'i> DeclarationBlock<'i> {
+  /// Writes the declarations to a CSS block, including starting and ending braces.
+  pub fn to_css_block<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    dest.whitespace()?;
+    dest.write_char('{')?;
+    dest.indent();
+    dest.newline()?;
+
+    self.to_css_declarations(dest, false, &parcel_selectors::SelectorList(SmallVec::new()), 0)?;
+
+    dest.dedent();
+    dest.newline()?;
+    dest.write_char('}')
+  }
+
+  pub(crate) fn has_printable_declarations(&self) -> bool {
+    if self.len() > 1 {
+      return true;
+    }
+
+    if self.declarations.len() == 1 {
+      !matches!(self.declarations[0], crate::properties::Property::Composes(_))
+    } else if self.important_declarations.len() == 1 {
+      !matches!(self.important_declarations[0], crate::properties::Property::Composes(_))
+    } else {
+      false
+    }
+  }
+
+  /// Writes the declarations to a CSS declaration block.
+  pub fn to_css_declarations<W>(
+    &self,
+    dest: &mut Printer<W>,
+    has_nested_rules: bool,
+    selectors: &SelectorList,
+    source_index: u32,
+  ) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    let mut i = 0;
+    let len = self.len();
+
+    macro_rules! write {
+      ($decls: expr, $important: literal) => {
+        for decl in &$decls {
+          // The CSS modules `composes` property is handled specially, and omitted during printing.
+          // We need to add the classes it references to the list for the selectors in this rule.
+          if let crate::properties::Property::Composes(composes) = &decl {
+            if dest.is_nested() && dest.css_module.is_some() {
+              return Err(dest.error(PrinterErrorKind::InvalidComposesNesting, composes.loc));
+            }
+
+            if let Some(css_module) = &mut dest.css_module {
+              css_module
+                .handle_composes(&selectors, &composes, source_index)
+                .map_err(|e| dest.error(e, composes.loc))?;
+              continue;
+            }
+          }
+
+          if i > 0 {
+            dest.newline()?;
+          }
+
+          decl.to_css(dest, $important)?;
+          if i != len - 1 || !dest.minify || has_nested_rules {
+            dest.write_char(';')?;
+          }
+
+          i += 1;
+        }
+      };
+    }
+
+    write!(self.declarations, false);
+    write!(self.important_declarations, true);
+    Ok(())
+  }
+}
+
+impl<'i> DeclarationBlock<'i> {
+  pub(crate) fn minify(
+    &mut self,
+    handler: &mut DeclarationHandler<'i>,
+    important_handler: &mut DeclarationHandler<'i>,
+    context: &mut PropertyHandlerContext<'i, '_>,
+  ) {
+    macro_rules! handle {
+      ($decls: expr, $handler: expr, $important: literal) => {
+        for decl in $decls.iter() {
+          context.is_important = $important;
+          let handled = $handler.handle_property(decl, context);
+
+          if !handled {
+            $handler.decls.push(decl.clone());
+          }
+        }
+      };
+    }
+
+    handle!(self.important_declarations, important_handler, true);
+    handle!(self.declarations, handler, false);
+
+    handler.finalize(context);
+    important_handler.finalize(context);
+    self.important_declarations = std::mem::take(&mut important_handler.decls);
+    self.declarations = std::mem::take(&mut handler.decls);
+  }
+
+  /// Returns whether the declaration block is empty.
+  pub fn is_empty(&self) -> bool {
+    return self.declarations.is_empty() && self.important_declarations.is_empty();
+  }
+
+  pub(crate) fn property_location<'t>(
+    &self,
+    input: &mut Parser<'i, 't>,
+    index: usize,
+  ) -> Result<(Range<SourceLocation>, Range<SourceLocation>), ParseError<'i, ParserError<'i>>> {
+    // Skip to the requested property index.
+    for _ in 0..index {
+      input.expect_ident()?;
+      input.expect_colon()?;
+      input.parse_until_after(Delimiter::Semicolon, |parser| {
+        while parser.next().is_ok() {}
+        Ok(())
+      })?;
+    }
+
+    // Get property name range.
+    input.skip_whitespace();
+    let key_start = input.current_source_location();
+    input.expect_ident()?;
+    let key_end = input.current_source_location();
+    let key_range = key_start..key_end;
+
+    input.expect_colon()?;
+    input.skip_whitespace();
+
+    // Get value range.
+    let val_start = input.current_source_location();
+    input.parse_until_before(Delimiter::Semicolon, |parser| {
+      while parser.next().is_ok() {}
+      Ok(())
+    })?;
+    let val_end = input.current_source_location();
+    let val_range = val_start..val_end;
+
+    Ok((key_range, val_range))
+  }
+}
+
+impl<'i> DeclarationBlock<'i> {
+  /// Returns an iterator over all properties in the declaration.
+  pub fn iter(&self) -> impl std::iter::DoubleEndedIterator<Item = (&Property<'i>, bool)> {
+    self
+      .declarations
+      .iter()
+      .map(|property| (property, false))
+      .chain(self.important_declarations.iter().map(|property| (property, true)))
+  }
+
+  /// Returns a mutable iterator over all properties in the declaration.
+  pub fn iter_mut(&mut self) -> impl std::iter::DoubleEndedIterator<Item = &mut Property<'i>> {
+    self.declarations.iter_mut().chain(self.important_declarations.iter_mut())
+  }
+
+  /// Returns the value for a given property id based on the properties in this declaration block.
+  ///
+  /// If the property is a shorthand, the result will be a combined value of all of the included
+  /// longhands, or `None` if some of the longhands are not declared. Otherwise, the value will be
+  /// either an explicitly declared longhand, or a value extracted from a shorthand property.
+  pub fn get<'a>(&'a self, property_id: &PropertyId) -> Option<(Cow<'a, Property<'i>>, bool)> {
+    if property_id.is_shorthand() {
+      if let Some((shorthand, important)) = property_id.shorthand_value(&self) {
+        return Some((Cow::Owned(shorthand), important));
+      }
+    } else {
+      for (property, important) in self.iter().rev() {
+        if property.property_id() == *property_id {
+          return Some((Cow::Borrowed(property), important));
+        }
+
+        if let Some(val) = property.longhand(&property_id) {
+          return Some((Cow::Owned(val), important));
+        }
+      }
+    }
+
+    None
+  }
+
+  /// Sets the value and importance for a given property, replacing any existing declarations.
+  ///
+  /// If the property already exists within the declaration block, it is updated in place. Otherwise,
+  /// a new declaration is appended. When updating a longhand property and a shorthand is defined which
+  /// includes the longhand, the shorthand will be updated rather than appending a new declaration.
+  pub fn set(&mut self, property: Property<'i>, important: bool) {
+    let property_id = property.property_id();
+    let declarations = if important {
+      // Remove any non-important properties with this id.
+      self.declarations.retain(|decl| decl.property_id() != property_id);
+      &mut self.important_declarations
+    } else {
+      // Remove any important properties with this id.
+      self.important_declarations.retain(|decl| decl.property_id() != property_id);
+      &mut self.declarations
+    };
+
+    let longhands = property_id.longhands().unwrap_or_else(|| vec![property.property_id()]);
+
+    for decl in declarations.iter_mut().rev() {
+      {
+        // If any of the longhands being set are in the same logical property group as any of the
+        // longhands in this property, but in a different category (i.e. logical or physical),
+        // then we cannot modify in place, and need to append a new property.
+        let id = decl.property_id();
+        let id_longhands = id.longhands().unwrap_or_else(|| vec![id]);
+        if longhands.iter().any(|longhand| {
+          let logical_group = longhand.logical_group();
+          let category = longhand.category();
+
+          logical_group.is_some()
+            && id_longhands.iter().any(|id_longhand| {
+              logical_group == id_longhand.logical_group() && category != id_longhand.category()
+            })
+        }) {
+          break;
+        }
+      }
+
+      if decl.property_id() == property_id {
+        *decl = property;
+        return;
+      }
+
+      // Update shorthand.
+      if decl.set_longhand(&property).is_ok() {
+        return;
+      }
+    }
+
+    declarations.push(property)
+  }
+
+  /// Removes all declarations of the given property id from the declaration block.
+  ///
+  /// When removing a longhand property and a shorthand is defined which includes the longhand,
+  /// the shorthand will be split apart into its component longhand properties, minus the property
+  /// to remove. When removing a shorthand, all included longhand properties are also removed.
+  pub fn remove(&mut self, property_id: &PropertyId) {
+    fn remove<'i, 'a>(declarations: &mut Vec<Property<'i>>, property_id: &PropertyId<'a>) {
+      let longhands = property_id.longhands().unwrap_or(vec![]);
+      let mut i = 0;
+      while i < declarations.len() {
+        let replacement = {
+          let property = &declarations[i];
+          let id = property.property_id();
+          if id == *property_id || longhands.contains(&id) {
+            // If the property matches the requested property id, or is a longhand
+            // property that is included in the requested shorthand, remove it.
+            None
+          } else if longhands.is_empty() && id.longhands().unwrap_or(vec![]).contains(&property_id) {
+            // If this is a shorthand property that includes the requested longhand,
+            // split it apart into its component longhands, excluding the requested one.
+            Some(
+              id.longhands()
+                .unwrap()
+                .iter()
+                .filter_map(|longhand| {
+                  if *longhand == *property_id {
+                    None
+                  } else {
+                    property.longhand(longhand)
+                  }
+                })
+                .collect::<Vec<Property>>(),
+            )
+          } else {
+            i += 1;
+            continue;
+          }
+        };
+
+        match replacement {
+          Some(properties) => {
+            let count = properties.len();
+            declarations.splice(i..i + 1, properties);
+            i += count;
+          }
+          None => {
+            declarations.remove(i);
+          }
+        }
+      }
+    }
+
+    remove(&mut self.declarations, property_id);
+    remove(&mut self.important_declarations, property_id);
+  }
+}
+
+struct PropertyDeclarationParser<'a, 'o, 'i> {
+  important_declarations: &'a mut Vec<Property<'i>>,
+  declarations: &'a mut Vec<Property<'i>>,
+  options: &'a ParserOptions<'o, 'i>,
+}
+
+/// Parse a declaration within {} block: `color: blue`
+impl<'a, 'o, 'i> cssparser::DeclarationParser<'i> for PropertyDeclarationParser<'a, 'o, 'i> {
+  type Declaration = ();
+  type Error = ParserError<'i>;
+
+  fn parse_value<'t>(
+    &mut self,
+    name: CowRcStr<'i>,
+    input: &mut cssparser::Parser<'i, 't>,
+  ) -> Result<Self::Declaration, cssparser::ParseError<'i, Self::Error>> {
+    parse_declaration(
+      name,
+      input,
+      &mut self.declarations,
+      &mut self.important_declarations,
+      &self.options,
+    )
+  }
+}
+
+/// Default methods reject all at rules.
+impl<'a, 'o, 'i> AtRuleParser<'i> for PropertyDeclarationParser<'a, 'o, 'i> {
+  type Prelude = ();
+  type AtRule = ();
+  type Error = ParserError<'i>;
+}
+
+impl<'a, 'o, 'i> QualifiedRuleParser<'i> for PropertyDeclarationParser<'a, 'o, 'i> {
+  type Prelude = ();
+  type QualifiedRule = ();
+  type Error = ParserError<'i>;
+}
+
+impl<'a, 'o, 'i> RuleBodyItemParser<'i, (), ParserError<'i>> for PropertyDeclarationParser<'a, 'o, 'i> {
+  fn parse_qualified(&self) -> bool {
+    false
+  }
+
+  fn parse_declarations(&self) -> bool {
+    true
+  }
+}
+
+pub(crate) fn parse_declaration<'i, 't>(
+  name: CowRcStr<'i>,
+  input: &mut cssparser::Parser<'i, 't>,
+  declarations: &mut DeclarationList<'i>,
+  important_declarations: &mut DeclarationList<'i>,
+  options: &ParserOptions<'_, 'i>,
+) -> Result<(), cssparser::ParseError<'i, ParserError<'i>>> {
+  // Stop if we hit a `{` token in a non-custom property to
+  // avoid ambiguity between nested rules and declarations.
+  // https://github.com/w3c/csswg-drafts/issues/9317
+  let property_id = PropertyId::from(CowArcStr::from(name));
+  let mut delimiters = Delimiter::Bang;
+  if !matches!(property_id, PropertyId::Custom(CustomPropertyName::Custom(..))) {
+    delimiters = delimiters | Delimiter::CurlyBracketBlock;
+  }
+  let property = input.parse_until_before(delimiters, |input| Property::parse(property_id, input, options))?;
+  let important = input
+    .try_parse(|input| {
+      input.expect_delim('!')?;
+      input.expect_ident_matching("important")
+    })
+    .is_ok();
+  input.expect_exhausted()?;
+  if important {
+    important_declarations.push(property);
+  } else {
+    declarations.push(property);
+  }
+  Ok(())
+}
+
+pub(crate) type DeclarationList<'i> = Vec<Property<'i>>;
+
+#[derive(Default)]
+pub(crate) struct DeclarationHandler<'i> {
+  background: BackgroundHandler<'i>,
+  border: BorderHandler<'i>,
+  outline: OutlineHandler,
+  flex: FlexHandler,
+  grid: GridHandler<'i>,
+  align: AlignHandler,
+  size: SizeHandler,
+  margin: MarginHandler<'i>,
+  padding: PaddingHandler<'i>,
+  scroll_margin: ScrollMarginHandler<'i>,
+  scroll_padding: ScrollPaddingHandler<'i>,
+  font: FontHandler<'i>,
+  text: TextDecorationHandler<'i>,
+  list: ListStyleHandler<'i>,
+  transition: TransitionHandler<'i>,
+  animation: AnimationHandler<'i>,
+  display: DisplayHandler<'i>,
+  position: PositionHandler,
+  inset: InsetHandler<'i>,
+  overflow: OverflowHandler,
+  transform: TransformHandler,
+  box_shadow: BoxShadowHandler,
+  mask: MaskHandler<'i>,
+  container: ContainerHandler<'i>,
+  color_scheme: ColorSchemeHandler,
+  fallback: FallbackHandler,
+  prefix: PrefixHandler,
+  direction: Option<Direction>,
+  unicode_bidi: Option<UnicodeBidi>,
+  custom_properties: IndexMap<DashedIdent<'i>, usize>,
+  decls: DeclarationList<'i>,
+}
+
+impl<'i> DeclarationHandler<'i> {
+  pub fn handle_property(
+    &mut self,
+    property: &Property<'i>,
+    context: &mut PropertyHandlerContext<'i, '_>,
+  ) -> bool {
+    self.background.handle_property(property, &mut self.decls, context)
+      || self.border.handle_property(property, &mut self.decls, context)
+      || self.outline.handle_property(property, &mut self.decls, context)
+      || self.flex.handle_property(property, &mut self.decls, context)
+      || self.grid.handle_property(property, &mut self.decls, context)
+      || self.align.handle_property(property, &mut self.decls, context)
+      || self.size.handle_property(property, &mut self.decls, context)
+      || self.margin.handle_property(property, &mut self.decls, context)
+      || self.padding.handle_property(property, &mut self.decls, context)
+      || self.scroll_margin.handle_property(property, &mut self.decls, context)
+      || self.scroll_padding.handle_property(property, &mut self.decls, context)
+      || self.font.handle_property(property, &mut self.decls, context)
+      || self.text.handle_property(property, &mut self.decls, context)
+      || self.list.handle_property(property, &mut self.decls, context)
+      || self.transition.handle_property(property, &mut self.decls, context)
+      || self.animation.handle_property(property, &mut self.decls, context)
+      || self.display.handle_property(property, &mut self.decls, context)
+      || self.position.handle_property(property, &mut self.decls, context)
+      || self.inset.handle_property(property, &mut self.decls, context)
+      || self.overflow.handle_property(property, &mut self.decls, context)
+      || self.transform.handle_property(property, &mut self.decls, context)
+      || self.box_shadow.handle_property(property, &mut self.decls, context)
+      || self.mask.handle_property(property, &mut self.decls, context)
+      || self.container.handle_property(property, &mut self.decls, context)
+      || self.color_scheme.handle_property(property, &mut self.decls, context)
+      || self.fallback.handle_property(property, &mut self.decls, context)
+      || self.prefix.handle_property(property, &mut self.decls, context)
+      || self.handle_all(property)
+      || self.handle_custom_property(property, context)
+  }
+
+  fn handle_custom_property(
+    &mut self,
+    property: &Property<'i>,
+    context: &mut PropertyHandlerContext<'i, '_>,
+  ) -> bool {
+    if let Property::Custom(custom) = property {
+      if context.unused_symbols.contains(custom.name.as_ref()) {
+        return true;
+      }
+
+      if let CustomPropertyName::Custom(name) = &custom.name {
+        if let Some(index) = self.custom_properties.get(name) {
+          if self.decls[*index] == *property {
+            return true;
+          }
+          let mut custom = custom.clone();
+          self.add_conditional_fallbacks(&mut custom, context);
+          self.decls[*index] = Property::Custom(custom);
+        } else {
+          self.custom_properties.insert(name.clone(), self.decls.len());
+          let mut custom = custom.clone();
+          self.add_conditional_fallbacks(&mut custom, context);
+          self.decls.push(Property::Custom(custom));
+        }
+
+        return true;
+      }
+    }
+
+    false
+  }
+
+  fn handle_all(&mut self, property: &Property<'i>) -> bool {
+    // The `all` property resets all properies except `unicode-bidi`, `direction`, and custom properties.
+    // https://drafts.csswg.org/css-cascade-5/#all-shorthand
+    match property {
+      Property::UnicodeBidi(bidi) => {
+        self.unicode_bidi = Some(*bidi);
+        true
+      }
+      Property::Direction(direction) => {
+        self.direction = Some(*direction);
+        true
+      }
+      Property::All(keyword) => {
+        let mut handler = DeclarationHandler {
+          unicode_bidi: self.unicode_bidi.clone(),
+          direction: self.direction.clone(),
+          ..Default::default()
+        };
+        for (key, index) in self.custom_properties.drain(..) {
+          handler.custom_properties.insert(key, handler.decls.len());
+          handler.decls.push(self.decls[index].clone());
+        }
+        handler.decls.push(Property::All(keyword.clone()));
+        *self = handler;
+        true
+      }
+      _ => false,
+    }
+  }
+
+  fn add_conditional_fallbacks(
+    &self,
+    custom: &mut CustomProperty<'i>,
+    context: &mut PropertyHandlerContext<'i, '_>,
+  ) {
+    if context.context != DeclarationContext::Keyframes {
+      let fallbacks = custom.value.get_fallbacks(context.targets);
+      for (condition, fallback) in fallbacks {
+        context.add_conditional_property(
+          condition,
+          Property::Custom(CustomProperty {
+            name: custom.name.clone(),
+            value: fallback,
+          }),
+        );
+      }
+    }
+  }
+
+  pub fn finalize(&mut self, context: &mut PropertyHandlerContext<'i, '_>) {
+    if let Some(direction) = std::mem::take(&mut self.direction) {
+      self.decls.push(Property::Direction(direction));
+    }
+    if let Some(unicode_bidi) = std::mem::take(&mut self.unicode_bidi) {
+      self.decls.push(Property::UnicodeBidi(unicode_bidi));
+    }
+
+    self.background.finalize(&mut self.decls, context);
+    self.border.finalize(&mut self.decls, context);
+    self.outline.finalize(&mut self.decls, context);
+    self.flex.finalize(&mut self.decls, context);
+    self.grid.finalize(&mut self.decls, context);
+    self.align.finalize(&mut self.decls, context);
+    self.size.finalize(&mut self.decls, context);
+    self.margin.finalize(&mut self.decls, context);
+    self.padding.finalize(&mut self.decls, context);
+    self.scroll_margin.finalize(&mut self.decls, context);
+    self.scroll_padding.finalize(&mut self.decls, context);
+    self.font.finalize(&mut self.decls, context);
+    self.text.finalize(&mut self.decls, context);
+    self.list.finalize(&mut self.decls, context);
+    self.transition.finalize(&mut self.decls, context);
+    self.animation.finalize(&mut self.decls, context);
+    self.display.finalize(&mut self.decls, context);
+    self.position.finalize(&mut self.decls, context);
+    self.inset.finalize(&mut self.decls, context);
+    self.overflow.finalize(&mut self.decls, context);
+    self.transform.finalize(&mut self.decls, context);
+    self.box_shadow.finalize(&mut self.decls, context);
+    self.mask.finalize(&mut self.decls, context);
+    self.container.finalize(&mut self.decls, context);
+    self.color_scheme.finalize(&mut self.decls, context);
+    self.fallback.finalize(&mut self.decls, context);
+    self.prefix.finalize(&mut self.decls, context);
+    self.custom_properties.clear();
+  }
+}
diff --git a/src/dependencies.rs b/src/dependencies.rs
new file mode 100644
index 0000000..9648a5d
--- /dev/null
+++ b/src/dependencies.rs
@@ -0,0 +1,170 @@
+//! Dependency analysis.
+//!
+//! Dependencies in CSS can be analyzed using the `analyze_dependencies` option
+//! when printing a style sheet. These include other style sheets referenved via
+//! the `@import` rule, as well as `url()` references. See [PrinterOptions](PrinterOptions).
+//!
+//! When dependency analysis is enabled, `@import` rules are removed, and `url()`
+//! dependencies are replaced with hashed placeholders that can be substituted with
+//! the final urls later (e.g. after bundling and content hashing).
+
+use crate::css_modules::hash;
+use crate::printer::PrinterOptions;
+use crate::rules::import::ImportRule;
+use crate::traits::ToCss;
+use crate::values::url::Url;
+#[cfg(feature = "visitor")]
+use crate::visitor::Visit;
+use cssparser::SourceLocation;
+#[cfg(any(feature = "serde", feature = "nodejs"))]
+use serde::Serialize;
+
+/// Options for `analyze_dependencies` in `PrinterOptions`.
+#[derive(Default)]
+pub struct DependencyOptions {
+  /// Whether to remove `@import` rules.
+  pub remove_imports: bool,
+}
+
+/// A dependency.
+#[derive(Debug)]
+#[cfg_attr(any(feature = "serde", feature = "nodejs"), derive(Serialize))]
+#[cfg_attr(
+  any(feature = "serde", feature = "nodejs"),
+  serde(tag = "type", rename_all = "lowercase")
+)]
+pub enum Dependency {
+  /// An `@import` dependency.
+  Import(ImportDependency),
+  /// A `url()` dependency.
+  Url(UrlDependency),
+}
+
+/// An `@import` dependency.
+#[derive(Debug)]
+#[cfg_attr(any(feature = "serde", feature = "nodejs"), derive(Serialize))]
+pub struct ImportDependency {
+  /// The url to import.
+  pub url: String,
+  /// The placeholder that the URL was replaced with.
+  pub placeholder: String,
+  /// An optional `supports()` condition.
+  pub supports: Option<String>,
+  /// A media query.
+  pub media: Option<String>,
+  /// The location of the dependency in the source file.
+  pub loc: SourceRange,
+}
+
+impl ImportDependency {
+  /// Creates a new dependency from an `@import` rule.
+  pub fn new(rule: &ImportRule, filename: &str) -> ImportDependency {
+    let supports = if let Some(supports) = &rule.supports {
+      let s = supports.to_css_string(PrinterOptions::default()).unwrap();
+      Some(s)
+    } else {
+      None
+    };
+
+    let media = if !rule.media.media_queries.is_empty() {
+      let s = rule.media.to_css_string(PrinterOptions::default()).unwrap();
+      Some(s)
+    } else {
+      None
+    };
+
+    let placeholder = hash(&format!("{}_{}", filename, rule.url), false);
+
+    ImportDependency {
+      url: rule.url.as_ref().to_owned(),
+      placeholder,
+      supports,
+      media,
+      loc: SourceRange::new(
+        filename,
+        Location {
+          line: rule.loc.line + 1,
+          column: rule.loc.column,
+        },
+        8,
+        rule.url.len() + 2,
+      ), // TODO: what about @import url(...)?
+    }
+  }
+}
+
+/// A `url()` dependency.
+#[derive(Debug)]
+#[cfg_attr(any(feature = "serde", feature = "nodejs"), derive(Serialize))]
+pub struct UrlDependency {
+  /// The url of the dependency.
+  pub url: String,
+  /// The placeholder that the URL was replaced with.
+  pub placeholder: String,
+  /// The location of the dependency in the source file.
+  pub loc: SourceRange,
+}
+
+impl UrlDependency {
+  /// Creates a new url dependency.
+  pub fn new(url: &Url, filename: &str) -> UrlDependency {
+    let placeholder = hash(&format!("{}_{}", filename, url.url), false);
+    UrlDependency {
+      url: url.url.to_string(),
+      placeholder,
+      loc: SourceRange::new(filename, url.loc, 4, url.url.len()),
+    }
+  }
+}
+
+/// Represents the range of source code where a dependency was found.
+#[derive(Debug)]
+#[cfg_attr(any(feature = "serde", feature = "nodejs"), derive(Serialize))]
+#[cfg_attr(any(feature = "serde", feature = "nodejs"), serde(rename_all = "camelCase"))]
+pub struct SourceRange {
+  /// The filename in which the dependency was found.
+  pub file_path: String,
+  /// The starting line and column position of the dependency.
+  pub start: Location,
+  /// The ending line and column position of the dependency.
+  pub end: Location,
+}
+
+/// A line and column position within a source file.
+#[derive(Debug, Clone, Copy, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(any(feature = "serde", feature = "nodejs"), derive(serde::Serialize))]
+#[cfg_attr(any(feature = "serde"), derive(serde::Deserialize))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub struct Location {
+  /// The line number, starting from 1.
+  pub line: u32,
+  /// The column number, starting from 1.
+  pub column: u32,
+}
+
+impl From<SourceLocation> for Location {
+  fn from(loc: SourceLocation) -> Location {
+    Location {
+      line: loc.line + 1,
+      column: loc.column,
+    }
+  }
+}
+
+impl SourceRange {
+  fn new(filename: &str, loc: Location, offset: u32, len: usize) -> SourceRange {
+    SourceRange {
+      file_path: filename.into(),
+      start: Location {
+        line: loc.line,
+        column: loc.column + offset,
+      },
+      end: Location {
+        line: loc.line,
+        column: loc.column + offset + (len as u32) - 1,
+      },
+    }
+  }
+}
diff --git a/src/error.rs b/src/error.rs
new file mode 100644
index 0000000..4cca106
--- /dev/null
+++ b/src/error.rs
@@ -0,0 +1,447 @@
+//! Error types.
+
+use crate::properties::custom::Token;
+use crate::rules::Location;
+use crate::values::string::CowArcStr;
+use cssparser::{BasicParseErrorKind, ParseError, ParseErrorKind};
+use parcel_selectors::parser::SelectorParseErrorKind;
+#[cfg(any(feature = "serde", feature = "nodejs"))]
+use serde::Serialize;
+#[cfg(feature = "into_owned")]
+use static_self::IntoOwned;
+use std::fmt;
+
+/// An error with a source location.
+#[derive(Debug, PartialEq, Clone)]
+#[cfg_attr(any(feature = "serde", feature = "nodejs"), derive(serde::Serialize))]
+#[cfg_attr(any(feature = "serde"), derive(serde::Deserialize))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub struct Error<T> {
+  /// The type of error that occurred.
+  pub kind: T,
+  /// The location where the error occurred.
+  pub loc: Option<ErrorLocation>,
+}
+
+impl<T: fmt::Display> fmt::Display for Error<T> {
+  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+    self.kind.fmt(f)?;
+    if let Some(loc) = &self.loc {
+      write!(f, " at {}", loc)?;
+    }
+    Ok(())
+  }
+}
+
+impl<T: fmt::Display + fmt::Debug> std::error::Error for Error<T> {}
+
+/// A line and column location within a source file.
+#[derive(Debug, PartialEq, Clone)]
+#[cfg_attr(any(feature = "serde", feature = "nodejs"), derive(serde::Serialize))]
+#[cfg_attr(any(feature = "serde"), derive(serde::Deserialize))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub struct ErrorLocation {
+  /// The filename in which the error occurred.
+  pub filename: String,
+  /// The line number, starting from 0.
+  pub line: u32,
+  /// The column number, starting from 1.
+  pub column: u32,
+}
+
+impl ErrorLocation {
+  /// Create a new error location from a source location and filename.
+  pub fn new(loc: Location, filename: String) -> Self {
+    ErrorLocation {
+      filename,
+      line: loc.line,
+      column: loc.column,
+    }
+  }
+}
+
+impl fmt::Display for ErrorLocation {
+  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+    write!(f, "{}:{}:{}", self.filename, self.line, self.column)
+  }
+}
+
+/// A parser error.
+#[derive(Debug, PartialEq, Clone)]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(any(feature = "serde", feature = "nodejs"), derive(Serialize))]
+#[cfg_attr(any(feature = "serde", feature = "nodejs"), serde(tag = "type", content = "value"))]
+pub enum ParserError<'i> {
+  /// An at rule body was invalid.
+  AtRuleBodyInvalid,
+  /// An at rule prelude was invalid
+  AtRulePreludeInvalid,
+  /// An unknown or unsupported at rule was encountered.
+  AtRuleInvalid(CowArcStr<'i>),
+  /// Unexpectedly encountered the end of input data.
+  EndOfInput,
+  /// A declaration was invalid.
+  InvalidDeclaration,
+  /// A media query was invalid.
+  InvalidMediaQuery,
+  /// Invalid CSS nesting.
+  InvalidNesting,
+  /// The @nest rule is deprecated.
+  DeprecatedNestRule,
+  /// The @value rule (of CSS modules) is deprecated.
+  DeprecatedCssModulesValueRule,
+  /// An invalid selector in an `@page` rule.
+  InvalidPageSelector,
+  /// An invalid value was encountered.
+  InvalidValue,
+  /// Invalid qualified rule.
+  QualifiedRuleInvalid,
+  /// A selector was invalid.
+  SelectorError(SelectorError<'i>),
+  /// An `@import` rule was encountered after any rule besides `@charset` or `@layer`.
+  UnexpectedImportRule,
+  /// A `@namespace` rule was encountered after any rules besides `@charset`, `@import`, or `@layer`.
+  UnexpectedNamespaceRule,
+  /// An unexpected token was encountered.
+  UnexpectedToken(#[cfg_attr(any(feature = "serde", feature = "nodejs"), serde(skip))] Token<'i>),
+  /// Maximum nesting depth was reached.
+  MaximumNestingDepth,
+}
+
+impl<'i> fmt::Display for ParserError<'i> {
+  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+    use ParserError::*;
+    match self {
+      AtRuleBodyInvalid => write!(f, "Invalid @ rule body"),
+      AtRulePreludeInvalid => write!(f, "Invalid @ rule prelude"),
+      AtRuleInvalid(name) => write!(f, "Unknown at rule: @{}", name),
+      EndOfInput => write!(f, "Unexpected end of input"),
+      InvalidDeclaration => write!(f, "Invalid declaration"),
+      InvalidMediaQuery => write!(f, "Invalid media query"),
+      InvalidNesting => write!(f, "Invalid nesting"),
+      DeprecatedNestRule => write!(f, "The @nest rule is deprecated"),
+      DeprecatedCssModulesValueRule => write!(f, "The @value rule is deprecated"),
+      InvalidPageSelector => write!(f, "Invalid page selector"),
+      InvalidValue => write!(f, "Invalid value"),
+      QualifiedRuleInvalid => write!(f, "Invalid qualified rule"),
+      SelectorError(s) => s.fmt(f),
+      UnexpectedImportRule => write!(
+        f,
+        "@import rules must precede all rules aside from @charset and @layer statements"
+      ),
+      UnexpectedNamespaceRule => write!(
+        f,
+        "@namespaces rules must precede all rules aside from @charset, @import, and @layer statements"
+      ),
+      UnexpectedToken(token) => write!(f, "Unexpected token {:?}", token),
+      MaximumNestingDepth => write!(f, "Overflowed the maximum nesting depth"),
+    }
+  }
+}
+
+impl<'i> Error<ParserError<'i>> {
+  /// Creates an error from a cssparser error.
+  pub fn from(err: ParseError<'i, ParserError<'i>>, filename: String) -> Error<ParserError<'i>> {
+    let kind = match err.kind {
+      ParseErrorKind::Basic(b) => match &b {
+        BasicParseErrorKind::UnexpectedToken(t) => ParserError::UnexpectedToken(t.into()),
+        BasicParseErrorKind::EndOfInput => ParserError::EndOfInput,
+        BasicParseErrorKind::AtRuleInvalid(a) => ParserError::AtRuleInvalid(a.into()),
+        BasicParseErrorKind::AtRuleBodyInvalid => ParserError::AtRuleBodyInvalid,
+        BasicParseErrorKind::QualifiedRuleInvalid => ParserError::QualifiedRuleInvalid,
+      },
+      ParseErrorKind::Custom(c) => c,
+    };
+
+    Error {
+      kind,
+      loc: Some(ErrorLocation {
+        filename,
+        line: err.location.line,
+        column: err.location.column,
+      }),
+    }
+  }
+
+  /// Consumes the value and returns an owned clone.
+  #[cfg(feature = "into_owned")]
+  pub fn into_owned<'x>(self) -> Error<ParserError<'static>> {
+    Error {
+      kind: self.kind.into_owned(),
+      loc: self.loc,
+    }
+  }
+}
+
+impl<'i> From<SelectorParseErrorKind<'i>> for ParserError<'i> {
+  fn from(err: SelectorParseErrorKind<'i>) -> ParserError<'i> {
+    ParserError::SelectorError(err.into())
+  }
+}
+
+impl<'i> ParserError<'i> {
+  #[deprecated(note = "use `ParserError::to_string()` or `fmt::Display` instead")]
+  #[allow(missing_docs)]
+  pub fn reason(&self) -> String {
+    self.to_string()
+  }
+}
+
+/// A selector parsing error.
+#[derive(Debug, PartialEq, Clone)]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(any(feature = "serde", feature = "nodejs"), derive(Serialize))]
+#[cfg_attr(any(feature = "serde", feature = "nodejs"), serde(tag = "type", content = "value"))]
+pub enum SelectorError<'i> {
+  /// An unexpected token was found in an attribute selector.
+  BadValueInAttr(#[cfg_attr(any(feature = "serde", feature = "nodejs"), serde(skip))] Token<'i>),
+  /// An unexpected token was found in a class selector.
+  ClassNeedsIdent(#[cfg_attr(any(feature = "serde", feature = "nodejs"), serde(skip))] Token<'i>),
+  /// A dangling combinator was found.
+  DanglingCombinator,
+  /// An empty selector.
+  EmptySelector,
+  /// A `|` was expected in an attribute selector.
+  ExpectedBarInAttr(#[cfg_attr(any(feature = "serde", feature = "nodejs"), serde(skip))] Token<'i>),
+  /// A namespace was expected.
+  ExpectedNamespace(CowArcStr<'i>),
+  /// An unexpected token was encountered in a namespace.
+  ExplicitNamespaceUnexpectedToken(#[cfg_attr(any(feature = "serde", feature = "nodejs"), serde(skip))] Token<'i>),
+  /// An invalid pseudo class was encountered after a pseudo element.
+  InvalidPseudoClassAfterPseudoElement,
+  /// An invalid pseudo class was encountered after a `-webkit-scrollbar` pseudo element.
+  InvalidPseudoClassAfterWebKitScrollbar,
+  /// A `-webkit-scrollbar` state was encountered before a `-webkit-scrollbar` pseudo element.
+  InvalidPseudoClassBeforeWebKitScrollbar,
+  /// Invalid qualified name in attribute selector.
+  InvalidQualNameInAttr(#[cfg_attr(any(feature = "serde", feature = "nodejs"), serde(skip))] Token<'i>),
+  /// The current token is not allowed in this state.
+  InvalidState,
+  /// The selector is required to have the `&` nesting selector at the start.
+  MissingNestingPrefix,
+  /// The selector is missing a `&` nesting selector.
+  MissingNestingSelector,
+  /// No qualified name in attribute selector.
+  NoQualifiedNameInAttributeSelector(
+    #[cfg_attr(any(feature = "serde", feature = "nodejs"), serde(skip))] Token<'i>,
+  ),
+  /// An Invalid token was encountered in a pseudo element.
+  PseudoElementExpectedIdent(#[cfg_attr(any(feature = "serde", feature = "nodejs"), serde(skip))] Token<'i>),
+  /// An unexpected identifier was encountered.
+  UnexpectedIdent(CowArcStr<'i>),
+  /// An unexpected token was encountered inside an attribute selector.
+  UnexpectedTokenInAttributeSelector(
+    #[cfg_attr(any(feature = "serde", feature = "nodejs"), serde(skip))] Token<'i>,
+  ),
+
+  /// An unsupported pseudo class was encountered.
+  UnsupportedPseudoClass(CowArcStr<'i>),
+
+  /// An unsupported pseudo element was encountered.
+  UnsupportedPseudoElement(CowArcStr<'i>),
+
+  /// Ambiguous CSS module class.
+  AmbiguousCssModuleClass(CowArcStr<'i>),
+
+  /// An unexpected token was encountered after a pseudo element.
+  UnexpectedSelectorAfterPseudoElement(
+    #[cfg_attr(any(feature = "serde", feature = "nodejs"), serde(skip))] Token<'i>,
+  ),
+}
+
+impl<'i> fmt::Display for SelectorError<'i> {
+  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+    use SelectorError::*;
+    match self {
+      InvalidState => write!(f, "Invalid state"),
+      BadValueInAttr(token) => write!(f, "Invalid value in attribute selector: {:?}", token),
+      ClassNeedsIdent(token) => write!(f, "Expected identifier in class selector, got {:?}", token),
+      DanglingCombinator => write!(f, "Invalid dangling combinator in selector"),
+      EmptySelector => write!(f, "Invalid empty selector"),
+      ExpectedBarInAttr(name) => write!(f, "Expected | in attribute, got {:?}", name),
+      ExpectedNamespace(name) => write!(f, "Expected namespace: {}", name),
+      ExplicitNamespaceUnexpectedToken(token) => write!(f, "Unexpected token in namespace selector: {:?}", token),
+      InvalidPseudoClassAfterPseudoElement => write!(f, "Invalid pseudo class after pseudo element, only user action pseudo classes (e.g. :hover, :active) are allowed"),
+      InvalidPseudoClassAfterWebKitScrollbar => write!(f, "Invalid pseudo class after ::-webkit-scrollbar pseudo element"),
+      InvalidPseudoClassBeforeWebKitScrollbar => write!(f, "Pseudo class must be prefixed by a ::-webkit-scrollbar pseudo element"),
+      InvalidQualNameInAttr(token) => write!(f, "Invalid qualified name in attribute selector: {:?}", token),
+      MissingNestingPrefix => write!(f, "A nested rule must start with a nesting selector (&) as prefix of each selector, or start with @nest"),
+      MissingNestingSelector => write!(f, "A nesting selector (&) is required in each selector of a @nest rule"),
+      NoQualifiedNameInAttributeSelector(token) => write!(f, "No qualified name in attribute selector: {:?}.", token),
+      PseudoElementExpectedIdent(token) => write!(f, "Invalid token in pseudo element: {:?}", token),
+      UnexpectedIdent(name) => write!(f, "Unexpected identifier: {}", name),
+      UnexpectedTokenInAttributeSelector(token) => write!(f, "Unexpected token in attribute selector: {:?}", token),
+      UnsupportedPseudoClass(name) =>write!(f, "'{name}' is not recognized as a valid pseudo-class. Did you mean '::{name}' (pseudo-element) or is this a typo?"),
+      UnsupportedPseudoElement(name) => write!(f, "'{name}' is not recognized as a valid pseudo-element. Did you mean ':{name}' (pseudo-class) or is this a typo?"),
+      AmbiguousCssModuleClass(_) => write!(f, "Ambiguous CSS module class not supported"),
+      UnexpectedSelectorAfterPseudoElement(token) => {
+        write!(
+          f,
+          "Pseudo-elements like '::before' or '::after' can't be followed by selectors like '{token:?}'"
+        )
+      },
+    }
+  }
+}
+
+impl<'i> From<SelectorParseErrorKind<'i>> for SelectorError<'i> {
+  fn from(err: SelectorParseErrorKind<'i>) -> Self {
+    match &err {
+      SelectorParseErrorKind::NoQualifiedNameInAttributeSelector(t) => {
+        SelectorError::NoQualifiedNameInAttributeSelector(t.into())
+      }
+      SelectorParseErrorKind::EmptySelector => SelectorError::EmptySelector,
+      SelectorParseErrorKind::DanglingCombinator => SelectorError::DanglingCombinator,
+      SelectorParseErrorKind::InvalidPseudoClassBeforeWebKitScrollbar => {
+        SelectorError::InvalidPseudoClassBeforeWebKitScrollbar
+      }
+      SelectorParseErrorKind::InvalidPseudoClassAfterWebKitScrollbar => {
+        SelectorError::InvalidPseudoClassAfterWebKitScrollbar
+      }
+      SelectorParseErrorKind::InvalidPseudoClassAfterPseudoElement => {
+        SelectorError::InvalidPseudoClassAfterPseudoElement
+      }
+      SelectorParseErrorKind::InvalidState => SelectorError::InvalidState,
+      SelectorParseErrorKind::MissingNestingSelector => SelectorError::MissingNestingSelector,
+      SelectorParseErrorKind::MissingNestingPrefix => SelectorError::MissingNestingPrefix,
+      SelectorParseErrorKind::UnexpectedTokenInAttributeSelector(t) => {
+        SelectorError::UnexpectedTokenInAttributeSelector(t.into())
+      }
+      SelectorParseErrorKind::PseudoElementExpectedIdent(t) => SelectorError::PseudoElementExpectedIdent(t.into()),
+      SelectorParseErrorKind::UnsupportedPseudoClass(t) => SelectorError::UnsupportedPseudoClass(t.into()),
+      SelectorParseErrorKind::UnsupportedPseudoElement(t) => SelectorError::UnsupportedPseudoElement(t.into()),
+      SelectorParseErrorKind::UnexpectedIdent(t) => SelectorError::UnexpectedIdent(t.into()),
+      SelectorParseErrorKind::ExpectedNamespace(t) => SelectorError::ExpectedNamespace(t.into()),
+      SelectorParseErrorKind::ExpectedBarInAttr(t) => SelectorError::ExpectedBarInAttr(t.into()),
+      SelectorParseErrorKind::BadValueInAttr(t) => SelectorError::BadValueInAttr(t.into()),
+      SelectorParseErrorKind::InvalidQualNameInAttr(t) => SelectorError::InvalidQualNameInAttr(t.into()),
+      SelectorParseErrorKind::ExplicitNamespaceUnexpectedToken(t) => {
+        SelectorError::ExplicitNamespaceUnexpectedToken(t.into())
+      }
+      SelectorParseErrorKind::ClassNeedsIdent(t) => SelectorError::ClassNeedsIdent(t.into()),
+      SelectorParseErrorKind::AmbiguousCssModuleClass(name) => SelectorError::AmbiguousCssModuleClass(name.into()),
+      SelectorParseErrorKind::UnexpectedSelectorAfterPseudoElement(t) => {
+        SelectorError::UnexpectedSelectorAfterPseudoElement(t.into())
+      }
+    }
+  }
+}
+
+#[derive(Debug, PartialEq)]
+pub(crate) struct ErrorWithLocation<T> {
+  pub kind: T,
+  pub loc: Location,
+}
+
+impl<T: fmt::Display> fmt::Display for ErrorWithLocation<T> {
+  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+    self.kind.fmt(f)
+  }
+}
+
+impl<T: fmt::Display + fmt::Debug> std::error::Error for ErrorWithLocation<T> {}
+
+pub(crate) type MinifyError = ErrorWithLocation<MinifyErrorKind>;
+
+/// A transformation error.
+#[derive(Debug, PartialEq)]
+#[cfg_attr(any(feature = "serde", feature = "nodejs"), derive(Serialize))]
+#[cfg_attr(any(feature = "serde", feature = "nodejs"), serde(tag = "type"))]
+pub enum MinifyErrorKind {
+  /// A circular `@custom-media` rule was detected.
+  CircularCustomMedia {
+    /// The name of the `@custom-media` rule that was referenced circularly.
+    name: String,
+  },
+  /// Attempted to reference a custom media rule that doesn't exist.
+  CustomMediaNotDefined {
+    /// The name of the `@custom-media` rule that was not defined.
+    name: String,
+  },
+  /// Boolean logic with media types in @custom-media rules is not supported.
+  UnsupportedCustomMediaBooleanLogic {
+    /// The source location of the `@custom-media` rule with unsupported boolean logic.
+    custom_media_loc: Location,
+  },
+  /// A CSS module selector did not contain at least one class or id selector.
+  ImpureCSSModuleSelector,
+}
+
+impl fmt::Display for MinifyErrorKind {
+  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+    use MinifyErrorKind::*;
+    match self {
+      CircularCustomMedia { name } => write!(f, "Circular custom media query {} detected", name),
+      CustomMediaNotDefined { name } => write!(f, "Custom media query {} is not defined", name),
+      UnsupportedCustomMediaBooleanLogic { .. } => write!(
+        f,
+        "Boolean logic with media types in @custom-media rules is not supported by Lightning CSS"
+      ),
+      ImpureCSSModuleSelector => write!(
+        f,
+        "A selector in CSS modules should contain at least one class or ID selector"
+      ),
+    }
+  }
+}
+
+impl MinifyErrorKind {
+  #[deprecated(note = "use `MinifyErrorKind::to_string()` or `fmt::Display` instead")]
+  #[allow(missing_docs)]
+  pub fn reason(&self) -> String {
+    self.to_string()
+  }
+}
+
+/// A printer error.
+pub type PrinterError = Error<PrinterErrorKind>;
+
+/// A printer error type.
+#[derive(Debug, PartialEq)]
+#[cfg_attr(any(feature = "serde", feature = "nodejs"), derive(Serialize))]
+#[cfg_attr(any(feature = "serde", feature = "nodejs"), serde(tag = "type"))]
+pub enum PrinterErrorKind {
+  /// An ambiguous relative `url()` was encountered in a custom property declaration.
+  AmbiguousUrlInCustomProperty {
+    /// The ambiguous URL.
+    url: String,
+  },
+  /// A [std::fmt::Error](std::fmt::Error) was encountered in the underlying destination.
+  FmtError,
+  /// The CSS modules `composes` property cannot be used within nested rules.
+  InvalidComposesNesting,
+  /// The CSS modules `composes` property can only be used with a simple class selector.
+  InvalidComposesSelector,
+  /// The CSS modules pattern must end with `[local]` for use in CSS grid.
+  InvalidCssModulesPatternInGrid,
+}
+
+impl From<fmt::Error> for PrinterError {
+  fn from(_: fmt::Error) -> PrinterError {
+    PrinterError {
+      kind: PrinterErrorKind::FmtError,
+      loc: None,
+    }
+  }
+}
+
+impl fmt::Display for PrinterErrorKind {
+  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+    use PrinterErrorKind::*;
+    match self {
+      AmbiguousUrlInCustomProperty { url } => write!(f, "Ambiguous url('{}') in custom property. Relative paths are resolved from the location the var() is used, not where the custom property is defined. Use an absolute URL instead", url),
+      FmtError => write!(f, "Printer error"),
+      InvalidComposesNesting => write!(f, "The `composes` property cannot be used within nested rules"),
+      InvalidComposesSelector => write!(f, "The `composes` property cannot be used with a simple class selector"),
+      InvalidCssModulesPatternInGrid => write!(f, "The CSS modules `pattern` config must end with `[local]` for use in CSS grid line names."),
+    }
+  }
+}
+
+impl PrinterErrorKind {
+  #[deprecated(note = "use `PrinterErrorKind::to_string()` or `fmt::Display` instead")]
+  #[allow(missing_docs)]
+  pub fn reason(&self) -> String {
+    self.to_string()
+  }
+}
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000..bcd4764
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,30140 @@
+//! Lightning CSS is a CSS parser, transformer, and minifier based on the
+//! [cssparser](https://github.com/servo/rust-cssparser) crate used in Firefox.
+//! It supports fully parsing all CSS rules, properties, and values into normalized
+//! structures exactly how a browser would. Once parsed, the CSS can be transformed
+//! to add or remove vendor prefixes, or lower syntax for older browsers as appropriate.
+//! The style sheet can also be minified to merge longhand properties into shorthands,
+//! merge adjacent rules, reduce `calc()` expressions, and more. Finally, the style sheet
+//! can be printed back to CSS syntax, either minified to remove whitespace and compress
+//! the output as much as possible, or pretty printed.
+//!
+//! The [StyleSheet](stylesheet::StyleSheet) struct is the main entrypoint for Lightning CSS,
+//! and supports parsing and transforming entire CSS files. You can also parse and manipulate
+//! individual CSS [rules](rules), [properties](properties), or [values](values). The [bundler](bundler)
+//! module also can be used to combine a CSS file and all of its dependencies together into a single
+//! style sheet. See the individual module documentation for more details and examples.
+
+#![deny(missing_docs)]
+#![cfg_attr(docsrs, feature(doc_cfg))]
+
+#[cfg(feature = "bundler")]
+#[cfg_attr(docsrs, doc(cfg(feature = "bundler")))]
+pub mod bundler;
+mod compat;
+mod context;
+pub mod css_modules;
+pub mod declaration;
+pub mod dependencies;
+pub mod error;
+mod logical;
+mod macros;
+pub mod media_query;
+mod parser;
+mod prefixes;
+pub mod printer;
+pub mod properties;
+pub mod rules;
+pub mod selector;
+pub mod stylesheet;
+pub mod targets;
+pub mod traits;
+pub mod values;
+pub mod vendor_prefix;
+#[cfg(feature = "visitor")]
+#[cfg_attr(docsrs, doc(cfg(feature = "visitor")))]
+pub mod visitor;
+
+#[cfg(feature = "serde")]
+mod serialization;
+
+#[cfg(test)]
+mod tests {
+  use crate::css_modules::{CssModuleExport, CssModuleExports, CssModuleReference, CssModuleReferences};
+  use crate::dependencies::Dependency;
+  use crate::error::{Error, ErrorLocation, MinifyErrorKind, ParserError, PrinterErrorKind, SelectorError};
+  use crate::parser::ParserFlags;
+  use crate::properties::custom::Token;
+  use crate::properties::Property;
+  use crate::rules::CssRule;
+  use crate::rules::Location;
+  use crate::stylesheet::*;
+  use crate::targets::{Browsers, Features, Targets};
+  use crate::traits::{Parse, ToCss};
+  use crate::values::color::CssColor;
+  use crate::vendor_prefix::VendorPrefix;
+  use cssparser::SourceLocation;
+  use indoc::indoc;
+  use pretty_assertions::assert_eq;
+  use std::collections::HashMap;
+  use std::sync::{Arc, RwLock};
+
+  fn test(source: &str, expected: &str) {
+    test_with_options(source, expected, ParserOptions::default())
+  }
+
+  fn test_with_options<'i, 'o>(source: &'i str, expected: &'i str, options: ParserOptions<'o, 'i>) {
+    let mut stylesheet = StyleSheet::parse(&source, options).unwrap();
+    stylesheet.minify(MinifyOptions::default()).unwrap();
+    let res = stylesheet.to_css(PrinterOptions::default()).unwrap();
+    assert_eq!(res.code, expected);
+  }
+
+  fn minify_test(source: &str, expected: &str) {
+    minify_test_with_options(source, expected, ParserOptions::default())
+  }
+
+  #[track_caller]
+  fn minify_test_with_options<'i, 'o>(source: &'i str, expected: &'i str, options: ParserOptions<'o, 'i>) {
+    let mut stylesheet = StyleSheet::parse(&source, options.clone()).unwrap();
+    stylesheet.minify(MinifyOptions::default()).unwrap();
+    let res = stylesheet
+      .to_css(PrinterOptions {
+        minify: true,
+        ..PrinterOptions::default()
+      })
+      .unwrap();
+    assert_eq!(res.code, expected);
+  }
+
+  fn minify_error_test_with_options<'i, 'o>(
+    source: &'i str,
+    error: MinifyErrorKind,
+    options: ParserOptions<'o, 'i>,
+  ) {
+    let mut stylesheet = StyleSheet::parse(&source, options.clone()).unwrap();
+    match stylesheet.minify(MinifyOptions::default()) {
+      Err(e) => assert_eq!(e.kind, error),
+      _ => unreachable!(),
+    }
+  }
+
+  fn prefix_test(source: &str, expected: &str, targets: Browsers) {
+    let mut stylesheet = StyleSheet::parse(&source, ParserOptions::default()).unwrap();
+    stylesheet
+      .minify(MinifyOptions {
+        targets: targets.into(),
+        ..MinifyOptions::default()
+      })
+      .unwrap();
+    let res = stylesheet
+      .to_css(PrinterOptions {
+        targets: targets.into(),
+        ..PrinterOptions::default()
+      })
+      .unwrap();
+    assert_eq!(res.code, expected);
+  }
+
+  fn attr_test(source: &str, expected: &str, minify: bool, targets: Option<Browsers>) {
+    let mut attr = StyleAttribute::parse(source, ParserOptions::default()).unwrap();
+    attr.minify(MinifyOptions {
+      targets: targets.into(),
+      ..MinifyOptions::default()
+    });
+    let res = attr
+      .to_css(PrinterOptions {
+        targets: targets.into(),
+        minify,
+        ..PrinterOptions::default()
+      })
+      .unwrap();
+    assert_eq!(res.code, expected);
+  }
+
+  fn nesting_test(source: &str, expected: &str) {
+    nesting_test_with_targets(
+      source,
+      expected,
+      Browsers {
+        chrome: Some(95 << 16),
+        ..Browsers::default()
+      }
+      .into(),
+    );
+  }
+
+  fn nesting_test_with_targets(source: &str, expected: &str, targets: Targets) {
+    let mut stylesheet = StyleSheet::parse(&source, ParserOptions::default()).unwrap();
+    stylesheet
+      .minify(MinifyOptions {
+        targets,
+        ..MinifyOptions::default()
+      })
+      .unwrap();
+    let res = stylesheet
+      .to_css(PrinterOptions {
+        targets,
+        ..PrinterOptions::default()
+      })
+      .unwrap();
+    assert_eq!(res.code, expected);
+  }
+
+  fn nesting_test_no_targets(source: &str, expected: &str) {
+    let mut stylesheet = StyleSheet::parse(&source, ParserOptions::default()).unwrap();
+    stylesheet.minify(MinifyOptions::default()).unwrap();
+    let res = stylesheet.to_css(PrinterOptions::default()).unwrap();
+    assert_eq!(res.code, expected);
+  }
+
+  fn css_modules_test<'i>(
+    source: &'i str,
+    expected: &str,
+    expected_exports: CssModuleExports,
+    expected_references: CssModuleReferences,
+    config: crate::css_modules::Config<'i>,
+    minify: bool,
+  ) {
+    let mut stylesheet = StyleSheet::parse(
+      &source,
+      ParserOptions {
+        filename: "test.css".into(),
+        css_modules: Some(config),
+        ..ParserOptions::default()
+      },
+    )
+    .unwrap();
+    stylesheet.minify(MinifyOptions::default()).unwrap();
+    let res = stylesheet
+      .to_css(PrinterOptions {
+        minify,
+        ..Default::default()
+      })
+      .unwrap();
+    assert_eq!(res.code, expected);
+    assert_eq!(res.exports.unwrap(), expected_exports);
+    assert_eq!(res.references.unwrap(), expected_references);
+  }
+
+  fn custom_media_test(source: &str, expected: &str) {
+    let mut stylesheet = StyleSheet::parse(
+      &source,
+      ParserOptions {
+        flags: ParserFlags::CUSTOM_MEDIA,
+        ..ParserOptions::default()
+      },
+    )
+    .unwrap();
+    stylesheet
+      .minify(MinifyOptions {
+        targets: Browsers {
+          chrome: Some(95 << 16),
+          ..Browsers::default()
+        }
+        .into(),
+        ..MinifyOptions::default()
+      })
+      .unwrap();
+    let res = stylesheet.to_css(PrinterOptions::default()).unwrap();
+    assert_eq!(res.code, expected);
+  }
+
+  fn error_test(source: &str, error: ParserError) {
+    let res = StyleSheet::parse(&source, ParserOptions::default());
+    match res {
+      Ok(_) => unreachable!(),
+      Err(e) => assert_eq!(e.kind, error),
+    }
+  }
+
+  fn error_recovery_test(source: &str) {
+    let warnings = Arc::new(RwLock::default());
+    let res = StyleSheet::parse(
+      &source,
+      ParserOptions {
+        error_recovery: true,
+        warnings: Some(warnings.clone()),
+        ..Default::default()
+      },
+    );
+    match res {
+      Ok(..) => {}
+      Err(e) => unreachable!("parser error should be recovered, but got {e:?}"),
+    }
+  }
+
+  fn css_modules_error_test(source: &str, error: ParserError) {
+    let res = StyleSheet::parse(
+      &source,
+      ParserOptions {
+        css_modules: Some(Default::default()),
+        ..Default::default()
+      },
+    );
+    match res {
+      Ok(_) => unreachable!(),
+      Err(e) => assert_eq!(e.kind, error),
+    }
+  }
+
+  macro_rules! map(
+    { $($key:expr => $name:literal $(referenced: $referenced: literal)? $($value:literal $(global: $global: literal)? $(from $from:literal)?)*),* } => {
+      {
+        #[allow(unused_mut)]
+        let mut m = HashMap::new();
+        $(
+          #[allow(unused_mut)]
+          let mut v = Vec::new();
+          #[allow(unused_macros)]
+          macro_rules! insert {
+            ($local:literal from $specifier:literal) => {
+              v.push(CssModuleReference::Dependency {
+                name: $local.into(),
+                specifier: $specifier.into()
+              });
+            };
+            ($local:literal global: $is_global: literal) => {
+              v.push(CssModuleReference::Global {
+                name: $local.into()
+              });
+            };
+            ($local:literal) => {
+              v.push(CssModuleReference::Local {
+                name: $local.into()
+              });
+            };
+          }
+          $(
+            insert!($value $(global: $global)? $(from $from)?);
+          )*
+
+          macro_rules! is_referenced {
+            ($ref: literal) => {
+              $ref
+            };
+            () => {
+              false
+            };
+          }
+
+          m.insert($key.into(), CssModuleExport {
+            name: $name.into(),
+            composes: v,
+            is_referenced: is_referenced!($($referenced)?)
+          });
+        )*
+        m
+      }
+    };
+  );
+
+  #[test]
+  pub fn test_border_spacing() {
+    minify_test(
+      r#"
+      .foo {
+        border-spacing: 0px;
+      }
+    "#,
+      indoc! {".foo{border-spacing:0}"
+      },
+    );
+    minify_test(
+      r#"
+      .foo {
+        border-spacing: 0px 0px;
+      }
+    "#,
+      indoc! {".foo{border-spacing:0}"
+      },
+    );
+
+    minify_test(
+      r#"
+      .foo {
+        border-spacing: 12px   0px;
+      }
+    "#,
+      indoc! {".foo{border-spacing:12px 0}"
+      },
+    );
+
+    minify_test(
+      r#"
+      .foo {
+        border-spacing: calc(3px * 2) calc(5px * 0);
+      }
+    "#,
+      indoc! {".foo{border-spacing:6px 0}"
+      },
+    );
+
+    minify_test(
+      r#"
+      .foo {
+        border-spacing: calc(3px * 2) max(0px, 8px);
+      }
+    "#,
+      indoc! {".foo{border-spacing:6px 8px}"
+      },
+    );
+
+    // TODO: The `<length>` in border-spacing cannot have a negative value,
+    // we may need to implement NonNegativeLength like Servo does.
+    // Servo Code: https://github.com/servo/servo/blob/08bc2d53579c9ab85415d4363888881b91df073b/components/style/values/specified/length.rs#L875
+    // CSSWG issue: https://lists.w3.org/Archives/Public/www-style/2008Sep/0161.html
+    // `border-spacing = <length> <length>?`
+    minify_test(
+      r#"
+      .foo {
+        border-spacing: -20px;
+      }
+    "#,
+      indoc! {".foo{border-spacing:-20px}"
+      },
+    );
+  }
+
+  #[test]
+  pub fn test_border() {
+    test(
+      r#"
+      .foo {
+        border-left: 2px solid red;
+        border-right: 2px solid red;
+        border-bottom: 2px solid red;
+        border-top: 2px solid red;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border: 2px solid red;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        border-left-color: red;
+        border-right-color: red;
+        border-bottom-color: red;
+        border-top-color: red;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border-color: red;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        border-left-width: thin;
+        border-right-width: thin;
+        border-bottom-width: thin;
+        border-top-width: thin;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border-width: thin;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        border-left-style: dotted;
+        border-right-style: dotted;
+        border-bottom-style: dotted;
+        border-top-style: dotted;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border-style: dotted;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        border-left-width: thin;
+        border-left-style: dotted;
+        border-left-color: red;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border-left: thin dotted red;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        border-left-width: thick;
+        border-left: thin dotted red;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border-left: thin dotted red;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        border-left-width: thick;
+        border: thin dotted red;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border: thin dotted red;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        border: thin dotted red;
+        border-right-width: thick;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border: thin dotted red;
+        border-right-width: thick;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        border: thin dotted red;
+        border-right: thick dotted red;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border: thin dotted red;
+        border-right-width: thick;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        border: thin dotted red;
+        border-right-width: thick;
+        border-right-style: solid;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border: thin dotted red;
+        border-right: thick solid red;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        border-top: thin dotted red;
+        border-block-start: thick solid green;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border-top: thin dotted red;
+        border-block-start: thick solid green;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        border: thin dotted red;
+        border-block-start-width: thick;
+        border-left-width: medium;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border: thin dotted red;
+        border-block-start-width: thick;
+        border-left-width: medium;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        border-block-start: thin dotted red;
+        border-inline-end: thin dotted red;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border-block-start: thin dotted red;
+        border-inline-end: thin dotted red;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        border-block-start-width: thin;
+        border-block-start-style: dotted;
+        border-block-start-color: red;
+        border-inline-end: thin dotted red;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border-block-start: thin dotted red;
+        border-inline-end: thin dotted red;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        border-block-start: thin dotted red;
+        border-block-end: thin dotted red;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border-block: thin dotted red;
+      }
+    "#
+      },
+    );
+
+    minify_test(
+      r#"
+      .foo {
+        border: none;
+      }
+    "#,
+      indoc! {".foo{border:none}"
+      },
+    );
+
+    minify_test(".foo { border-width: 0 0 1px; }", ".foo{border-width:0 0 1px}");
+    test(
+      r#"
+      .foo {
+        border-block-width: 1px;
+        border-inline-width: 1px;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border-width: 1px;
+      }
+    "#
+      },
+    );
+    test(
+      r#"
+      .foo {
+        border-block-start-width: 1px;
+        border-block-end-width: 1px;
+        border-inline-start-width: 1px;
+        border-inline-end-width: 1px;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border-width: 1px;
+      }
+    "#
+      },
+    );
+    test(
+      r#"
+      .foo {
+        border-block-start-width: 1px;
+        border-block-end-width: 1px;
+        border-inline-start-width: 2px;
+        border-inline-end-width: 2px;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border-block-width: 1px;
+        border-inline-width: 2px;
+      }
+    "#
+      },
+    );
+    test(
+      r#"
+      .foo {
+        border-block-start-width: 1px;
+        border-block-end-width: 1px;
+        border-inline-start-width: 2px;
+        border-inline-end-width: 3px;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border-block-width: 1px;
+        border-inline-width: 2px 3px;
+      }
+    "#
+      },
+    );
+
+    minify_test(
+      ".foo { border-bottom: 1px solid var(--spectrum-global-color-gray-200)}",
+      ".foo{border-bottom:1px solid var(--spectrum-global-color-gray-200)}",
+    );
+    test(
+      r#"
+      .foo {
+        border-width: 0;
+        border-bottom: var(--test, 1px) solid;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border-width: 0;
+        border-bottom: var(--test, 1px) solid;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        border: 1px solid black;
+        border-width: 1px 1px 0 0;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border: 1px solid #000;
+        border-width: 1px 1px 0 0;
+      }
+    "#},
+    );
+
+    test(
+      r#"
+      .foo {
+        border-top: 1px solid black;
+        border-bottom: 1px solid black;
+        border-left: 2px solid black;
+        border-right: 2px solid black;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border: 1px solid #000;
+        border-width: 1px 2px;
+      }
+    "#},
+    );
+
+    test(
+      r#"
+      .foo {
+        border-top: 1px solid black;
+        border-bottom: 1px solid black;
+        border-left: 2px solid black;
+        border-right: 1px solid black;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border: 1px solid #000;
+        border-left-width: 2px;
+      }
+    "#},
+    );
+
+    test(
+      r#"
+      .foo {
+        border-top: 1px solid black;
+        border-bottom: 1px solid black;
+        border-left: 1px solid red;
+        border-right: 1px solid red;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border: 1px solid #000;
+        border-color: #000 red;
+      }
+    "#},
+    );
+
+    test(
+      r#"
+      .foo {
+        border-block-start: 1px solid black;
+        border-block-end: 1px solid black;
+        border-inline-start: 1px solid red;
+        border-inline-end: 1px solid red;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border: 1px solid #000;
+        border-inline-color: red;
+      }
+    "#},
+    );
+
+    test(
+      r#"
+      .foo {
+        border-block-start: 1px solid black;
+        border-block-end: 1px solid black;
+        border-inline-start: 2px solid black;
+        border-inline-end: 2px solid black;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border: 1px solid #000;
+        border-inline-width: 2px;
+      }
+    "#},
+    );
+
+    test(
+      r#"
+      .foo {
+        border-block-start: 1px solid black;
+        border-block-end: 1px solid black;
+        border-inline-start: 2px solid red;
+        border-inline-end: 2px solid red;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border: 1px solid #000;
+        border-inline: 2px solid red;
+      }
+    "#},
+    );
+
+    test(
+      r#"
+      .foo {
+        border-block-start: 1px solid black;
+        border-block-end: 1px solid black;
+        border-inline-start: 2px solid red;
+        border-inline-end: 3px solid red;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border: 1px solid #000;
+        border-inline-start: 2px solid red;
+        border-inline-end: 3px solid red;
+      }
+    "#},
+    );
+
+    test(
+      r#"
+      .foo {
+        border-block-start: 2px solid black;
+        border-block-end: 1px solid black;
+        border-inline-start: 2px solid red;
+        border-inline-end: 2px solid red;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border: 2px solid red;
+        border-block-start-color: #000;
+        border-block-end: 1px solid #000;
+      }
+    "#},
+    );
+
+    test(
+      r#"
+      .foo {
+        border-block-start: 2px solid red;
+        border-block-end: 1px solid red;
+        border-inline-start: 2px solid red;
+        border-inline-end: 2px solid red;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border: 2px solid red;
+        border-block-end-width: 1px;
+      }
+    "#},
+    );
+
+    test(
+      r#"
+      .foo {
+        border-block-start: 2px solid red;
+        border-block-end: 2px solid red;
+        border-inline-start: 2px solid red;
+        border-inline-end: 1px solid red;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border: 2px solid red;
+        border-inline-end-width: 1px;
+      }
+    "#},
+    );
+
+    test(
+      r#"
+      .foo {
+        border: 1px solid currentColor;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border: 1px solid;
+      }
+    "#
+      },
+    );
+
+    minify_test(
+      r#"
+      .foo {
+        border: 1px solid currentColor;
+      }
+    "#,
+      ".foo{border:1px solid}",
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        border-block: 2px solid red;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border-top: 2px solid red;
+        border-bottom: 2px solid red;
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        border-block-start: 2px solid red;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border-top: 2px solid red;
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        border-block-end: 2px solid red;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border-bottom: 2px solid red;
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        border-inline: 2px solid red;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border-left: 2px solid red;
+        border-right: 2px solid red;
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        border-block-width: 2px;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border-block-start-width: 2px;
+        border-block-end-width: 2px;
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(13 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        border-block-width: 2px;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border-block-width: 2px;
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(15 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        border-inline-start: 2px solid red;
+      }
+    "#,
+      indoc! {r#"
+      .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) {
+        border-left: 2px solid red;
+      }
+
+      .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) {
+        border-left: 2px solid red;
+      }
+
+      .foo:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) {
+        border-right: 2px solid red;
+      }
+
+      .foo:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) {
+        border-right: 2px solid red;
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        border-inline-start-width: 2px;
+      }
+    "#,
+      indoc! {r#"
+      .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) {
+        border-left-width: 2px;
+      }
+
+      .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) {
+        border-left-width: 2px;
+      }
+
+      .foo:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) {
+        border-right-width: 2px;
+      }
+
+      .foo:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) {
+        border-right-width: 2px;
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        border-inline-end: 2px solid red;
+      }
+    "#,
+      indoc! {r#"
+      .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) {
+        border-right: 2px solid red;
+      }
+
+      .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) {
+        border-right: 2px solid red;
+      }
+
+      .foo:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) {
+        border-left: 2px solid red;
+      }
+
+      .foo:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) {
+        border-left: 2px solid red;
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        border-inline-start: 2px solid red;
+        border-inline-end: 5px solid green;
+      }
+    "#,
+      indoc! {r#"
+      .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) {
+        border-left: 2px solid red;
+        border-right: 5px solid green;
+      }
+
+      .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) {
+        border-left: 2px solid red;
+        border-right: 5px solid green;
+      }
+
+      .foo:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) {
+        border-left: 5px solid green;
+        border-right: 2px solid red;
+      }
+
+      .foo:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) {
+        border-left: 5px solid green;
+        border-right: 2px solid red;
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        border-inline-start: 2px solid red;
+        border-inline-end: 5px solid green;
+      }
+
+      .bar {
+        border-inline-start: 1px dotted gray;
+        border-inline-end: 1px solid black;
+      }
+    "#,
+      indoc! {r#"
+      .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) {
+        border-left: 2px solid red;
+        border-right: 5px solid green;
+      }
+
+      .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) {
+        border-left: 2px solid red;
+        border-right: 5px solid green;
+      }
+
+      .foo:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) {
+        border-left: 5px solid green;
+        border-right: 2px solid red;
+      }
+
+      .foo:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) {
+        border-left: 5px solid green;
+        border-right: 2px solid red;
+      }
+
+      .bar:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) {
+        border-left: 1px dotted gray;
+        border-right: 1px solid #000;
+      }
+
+      .bar:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) {
+        border-left: 1px dotted gray;
+        border-right: 1px solid #000;
+      }
+
+      .bar:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) {
+        border-left: 1px solid #000;
+        border-right: 1px dotted gray;
+      }
+
+      .bar:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) {
+        border-left: 1px solid #000;
+        border-right: 1px dotted gray;
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        border-inline-width: 2px;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border-left-width: 2px;
+        border-right-width: 2px;
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        border-inline-width: 2px;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border-left-width: 2px;
+        border-right-width: 2px;
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        border-inline-style: solid;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border-left-style: solid;
+        border-right-style: solid;
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        border-inline-color: red;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border-left-color: red;
+        border-right-color: red;
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        border-inline-end: var(--test);
+      }
+    "#,
+      indoc! {r#"
+      .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) {
+        border-right: var(--test);
+      }
+
+      .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) {
+        border-right: var(--test);
+      }
+
+      .foo:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) {
+        border-left: var(--test);
+      }
+
+      .foo:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) {
+        border-left: var(--test);
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        border-inline-start: var(--start);
+        border-inline-end: var(--end);
+      }
+    "#,
+      indoc! {r#"
+      .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) {
+        border-left: var(--start);
+        border-right: var(--end);
+      }
+
+      .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) {
+        border-left: var(--start);
+        border-right: var(--end);
+      }
+
+      .foo:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) {
+        border-right: var(--start);
+        border-left: var(--end);
+      }
+
+      .foo:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) {
+        border-right: var(--start);
+        border-left: var(--end);
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    for prop in &[
+      "border-inline-start-color",
+      "border-inline-end-color",
+      "border-block-start-color",
+      "border-block-end-color",
+      "border-top-color",
+      "border-bottom-color",
+      "border-left-color",
+      "border-right-color",
+      "border-color",
+      "border-block-color",
+      "border-inline-color",
+    ] {
+      prefix_test(
+        &format!(
+          r#"
+        .foo {{
+          {}: lab(40% 56.6 39);
+        }}
+      "#,
+          prop
+        ),
+        &format!(
+          indoc! {r#"
+        .foo {{
+          {}: #b32323;
+          {}: lab(40% 56.6 39);
+        }}
+      "#},
+          prop, prop
+        ),
+        Browsers {
+          chrome: Some(90 << 16),
+          ..Browsers::default()
+        },
+      );
+    }
+
+    for prop in &[
+      "border",
+      "border-inline",
+      "border-block",
+      "border-left",
+      "border-right",
+      "border-top",
+      "border-bottom",
+      "border-block-start",
+      "border-block-end",
+      "border-inline-start",
+      "border-inline-end",
+    ] {
+      prefix_test(
+        &format!(
+          r#"
+        .foo {{
+          {}: 2px solid lab(40% 56.6 39);
+        }}
+      "#,
+          prop
+        ),
+        &format!(
+          indoc! {r#"
+        .foo {{
+          {}: 2px solid #b32323;
+          {}: 2px solid lab(40% 56.6 39);
+        }}
+      "#},
+          prop, prop
+        ),
+        Browsers {
+          chrome: Some(90 << 16),
+          ..Browsers::default()
+        },
+      );
+    }
+
+    for prop in &[
+      "border",
+      "border-inline",
+      "border-block",
+      "border-left",
+      "border-right",
+      "border-top",
+      "border-bottom",
+      "border-block-start",
+      "border-block-end",
+      "border-inline-start",
+      "border-inline-end",
+    ] {
+      prefix_test(
+        &format!(
+          r#"
+        .foo {{
+          {}: var(--border-width) solid lab(40% 56.6 39);
+        }}
+      "#,
+          prop
+        ),
+        &format!(
+          indoc! {r#"
+        .foo {{
+          {}: var(--border-width) solid #b32323;
+        }}
+
+        @supports (color: lab(0% 0 0)) {{
+          .foo {{
+            {}: var(--border-width) solid lab(40% 56.6 39);
+          }}
+        }}
+      "#},
+          prop, prop
+        ),
+        Browsers {
+          chrome: Some(90 << 16),
+          ..Browsers::default()
+        },
+      );
+
+      prefix_test(
+        &format!(
+          r#"
+        @supports (color: lab(0% 0 0)) {{
+          .foo {{
+            {}: var(--border-width) solid lab(40% 56.6 39);
+          }}
+        }}
+      "#,
+          prop
+        ),
+        &format!(
+          indoc! {r#"
+        @supports (color: lab(0% 0 0)) {{
+          .foo {{
+            {}: var(--border-width) solid lab(40% 56.6 39);
+          }}
+        }}
+      "#},
+          prop,
+        ),
+        Browsers {
+          chrome: Some(90 << 16),
+          ..Browsers::default()
+        },
+      );
+    }
+
+    prefix_test(
+      r#"
+      .foo {
+        border-inline-start-color: lab(40% 56.6 39);
+      }
+    "#,
+      indoc! {r#"
+      .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) {
+        border-left-color: #b32323;
+        border-left-color: lab(40% 56.6 39);
+      }
+
+      .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) {
+        border-left-color: #b32323;
+        border-left-color: lab(40% 56.6 39);
+      }
+
+      .foo:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) {
+        border-right-color: #b32323;
+        border-right-color: lab(40% 56.6 39);
+      }
+
+      .foo:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) {
+        border-right-color: #b32323;
+        border-right-color: lab(40% 56.6 39);
+      }
+    "#},
+      Browsers {
+        safari: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        border-inline-end-color: lab(40% 56.6 39);
+      }
+    "#,
+      indoc! {r#"
+      .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) {
+        border-right-color: #b32323;
+        border-right-color: lab(40% 56.6 39);
+      }
+
+      .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) {
+        border-right-color: #b32323;
+        border-right-color: lab(40% 56.6 39);
+      }
+
+      .foo:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) {
+        border-left-color: #b32323;
+        border-left-color: lab(40% 56.6 39);
+      }
+
+      .foo:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) {
+        border-left-color: #b32323;
+        border-left-color: lab(40% 56.6 39);
+      }
+    "#},
+      Browsers {
+        safari: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        border-inline-start-color: lab(40% 56.6 39);
+        border-inline-end-color: lch(50.998% 135.363 338);
+      }
+    "#,
+      indoc! {r#"
+      .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) {
+        border-left-color: #b32323;
+        border-left-color: lab(40% 56.6 39);
+        border-right-color: #ee00be;
+        border-right-color: lch(50.998% 135.363 338);
+      }
+
+      .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) {
+        border-left-color: #b32323;
+        border-left-color: lab(40% 56.6 39);
+        border-right-color: #ee00be;
+        border-right-color: lch(50.998% 135.363 338);
+      }
+
+      .foo:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) {
+        border-left-color: #ee00be;
+        border-left-color: lch(50.998% 135.363 338);
+        border-right-color: #b32323;
+        border-right-color: lab(40% 56.6 39);
+      }
+
+      .foo:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) {
+        border-left-color: #ee00be;
+        border-left-color: lch(50.998% 135.363 338);
+        border-right-color: #b32323;
+        border-right-color: lab(40% 56.6 39);
+      }
+    "#},
+      Browsers {
+        safari: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        border-inline-start-color: lab(40% 56.6 39);
+        border-inline-end-color: lch(50.998% 135.363 338);
+      }
+    "#,
+      indoc! {r#"
+      .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) {
+        border-left-color: #b32323;
+        border-left-color: color(display-p3 .643308 .192455 .167712);
+        border-left-color: lab(40% 56.6 39);
+        border-right-color: #ee00be;
+        border-right-color: color(display-p3 .972962 -.362078 .804206);
+        border-right-color: lch(50.998% 135.363 338);
+      }
+
+      .foo:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) {
+        border-left-color: #ee00be;
+        border-left-color: color(display-p3 .972962 -.362078 .804206);
+        border-left-color: lch(50.998% 135.363 338);
+        border-right-color: #b32323;
+        border-right-color: color(display-p3 .643308 .192455 .167712);
+        border-right-color: lab(40% 56.6 39);
+      }
+    "#},
+      Browsers {
+        chrome: Some(8 << 16),
+        safari: Some(14 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        border-inline-start: 2px solid lab(40% 56.6 39);
+      }
+    "#,
+      indoc! {r#"
+      .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) {
+        border-left: 2px solid #b32323;
+        border-left: 2px solid lab(40% 56.6 39);
+      }
+
+      .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) {
+        border-left: 2px solid #b32323;
+        border-left: 2px solid lab(40% 56.6 39);
+      }
+
+      .foo:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) {
+        border-right: 2px solid #b32323;
+        border-right: 2px solid lab(40% 56.6 39);
+      }
+
+      .foo:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) {
+        border-right: 2px solid #b32323;
+        border-right: 2px solid lab(40% 56.6 39);
+      }
+    "#},
+      Browsers {
+        safari: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        border-inline-end: 2px solid lab(40% 56.6 39);
+      }
+    "#,
+      indoc! {r#"
+      .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) {
+        border-right: 2px solid #b32323;
+        border-right: 2px solid lab(40% 56.6 39);
+      }
+
+      .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) {
+        border-right: 2px solid #b32323;
+        border-right: 2px solid lab(40% 56.6 39);
+      }
+
+      .foo:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) {
+        border-left: 2px solid #b32323;
+        border-left: 2px solid lab(40% 56.6 39);
+      }
+
+      .foo:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) {
+        border-left: 2px solid #b32323;
+        border-left: 2px solid lab(40% 56.6 39);
+      }
+    "#},
+      Browsers {
+        safari: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        border-inline-end: var(--border-width) solid lab(40% 56.6 39);
+      }
+    "#,
+      indoc! {r#"
+      .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) {
+        border-right: var(--border-width) solid #b32323;
+      }
+
+      .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) {
+        border-right: var(--border-width) solid #b32323;
+      }
+
+      @supports (color: lab(0% 0 0)) {
+        .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) {
+          border-right: var(--border-width) solid lab(40% 56.6 39);
+        }
+      }
+
+      .foo:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) {
+        border-left: var(--border-width) solid #b32323;
+      }
+
+      .foo:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) {
+        border-left: var(--border-width) solid #b32323;
+      }
+
+      @supports (color: lab(0% 0 0)) {
+        .foo:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) {
+          border-left: var(--border-width) solid lab(40% 56.6 39);
+        }
+      }
+    "#},
+      Browsers {
+        safari: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        border-inline-start: 2px solid red;
+        border-inline-end: 2px solid red;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border-inline-start: 2px solid red;
+        border-inline-end: 2px solid red;
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(13 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        border-inline-start: 2px solid red;
+        border-inline-end: 2px solid red;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border-inline: 2px solid red;
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(15 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        border-width: 22px;
+        border-width: max(2cqw, 22px);
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border-width: 22px;
+        border-width: max(2cqw, 22px);
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(14 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        border-width: 22px;
+        border-width: max(2cqw, 22px);
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border-width: max(2cqw, 22px);
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(16 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        border-color: #4263eb;
+        border-color: color(display-p3 0 .5 1);
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border-color: #4263eb;
+        border-color: color(display-p3 0 .5 1);
+      }
+    "#
+      },
+      Browsers {
+        chrome: Some(99 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        border-color: #4263eb;
+        border-color: color(display-p3 0 .5 1);
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border-color: color(display-p3 0 .5 1);
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(16 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        border: 1px solid #4263eb;
+        border-color: color(display-p3 0 .5 1);
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border: 1px solid #4263eb;
+        border-color: color(display-p3 0 .5 1);
+      }
+    "#
+      },
+      Browsers {
+        chrome: Some(99 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        border: 1px solid #4263eb;
+        border-color: color(display-p3 0 .5 1);
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border: 1px solid color(display-p3 0 .5 1);
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(16 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        border-color: var(--fallback);
+        border-color: color(display-p3 0 .5 1);
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border-color: var(--fallback);
+        border-color: color(display-p3 0 .5 1);
+      }
+    "#
+      },
+      Browsers {
+        chrome: Some(99 << 16),
+        ..Browsers::default()
+      },
+    );
+  }
+
+  #[test]
+  pub fn test_border_image() {
+    test(
+      r#"
+      .foo {
+        border-image: url(test.png) 60;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border-image: url("test.png") 60;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        border-image: url(test.png) 60;
+        border-image-source: url(foo.png);
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border-image: url("foo.png") 60;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        border-image-source: url(foo.png);
+        border-image-slice: 10 40 10 40 fill;
+        border-image-width: 10px;
+        border-image-outset: 0;
+        border-image-repeat: round round;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border-image: url("foo.png") 10 40 fill / 10px round;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        border-image: url(foo.png) 60;
+        border-image-source: var(--test);
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border-image: url("foo.png") 60;
+        border-image-source: var(--test);
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        -webkit-border-image: url("test.png") 60;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        -webkit-border-image: url("test.png") 60;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        -webkit-border-image: url("test.png") 60;
+        border-image: url("test.png") 60;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        -webkit-border-image: url("test.png") 60;
+        border-image: url("test.png") 60;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        -webkit-border-image: url("test.png") 60;
+        border-image-source: url(foo.png);
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        -webkit-border-image: url("test.png") 60;
+        border-image-source: url("foo.png");
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        border: 1px solid red;
+        border-image: url(test.png) 60;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border: 1px solid red;
+        border-image: url("test.png") 60;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        border-image: url(test.png) 60;
+        border: 1px solid red;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border: 1px solid red;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        border: 1px solid red;
+        border-image: var(--border-image);
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border: 1px solid red;
+        border-image: var(--border-image);
+      }
+    "#
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        border-image: url("test.png") 60;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        -webkit-border-image: url("test.png") 60;
+        -moz-border-image: url("test.png") 60;
+        -o-border-image: url("test.png") 60;
+        border-image: url("test.png") 60;
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(4 << 16),
+        firefox: Some(4 << 16),
+        opera: Some(12 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        border-image: url(foo.png) 10 40 fill / 10px round;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border-image: url("foo.png") 10 40 fill / 10px round;
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(4 << 16),
+        firefox: Some(4 << 16),
+        opera: Some(12 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        border-image: var(--test) 60;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        -webkit-border-image: var(--test) 60;
+        -moz-border-image: var(--test) 60;
+        -o-border-image: var(--test) 60;
+        border-image: var(--test) 60;
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(4 << 16),
+        firefox: Some(4 << 16),
+        opera: Some(12 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        -webkit-border-image: url(foo.png) 60;
+        -moz-border-image: url(foo.png) 60;
+        -o-border-image: url(foo.png) 60;
+        border-image: url(foo.png) 60;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border-image: url("foo.png") 60;
+      }
+    "#
+      },
+      Browsers {
+        chrome: Some(15 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        border-image: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)) 60;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        -webkit-border-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ff0f0e), to(#7773ff)) 60;
+        -webkit-border-image: -webkit-linear-gradient(top, #ff0f0e, #7773ff) 60;
+        border-image: linear-gradient(#ff0f0e, #7773ff) 60;
+        border-image: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)) 60;
+      }
+    "#
+      },
+      Browsers {
+        chrome: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        border-image: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)) 60;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        -webkit-border-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ff0f0e), to(#7773ff)) 60;
+        -webkit-border-image: -webkit-linear-gradient(top, #ff0f0e, #7773ff) 60;
+        -moz-border-image: -moz-linear-gradient(top, #ff0f0e, #7773ff) 60;
+        border-image: linear-gradient(#ff0f0e, #7773ff) 60;
+        border-image: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)) 60;
+      }
+    "#
+      },
+      Browsers {
+        chrome: Some(8 << 16),
+        firefox: Some(4 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        border-image: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)) 60;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border-image: -webkit-linear-gradient(top, #ff0f0e, #7773ff) 60;
+        border-image: -moz-linear-gradient(top, #ff0f0e, #7773ff) 60;
+        border-image: linear-gradient(#ff0f0e, #7773ff) 60;
+        border-image: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)) 60;
+      }
+    "#
+      },
+      Browsers {
+        chrome: Some(15 << 16),
+        firefox: Some(15 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        border-image-source: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364));
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border-image-source: -webkit-linear-gradient(top, #ff0f0e, #7773ff);
+        border-image-source: linear-gradient(#ff0f0e, #7773ff);
+        border-image-source: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364));
+      }
+    "#
+      },
+      Browsers {
+        chrome: Some(15 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        border-image: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)) var(--foo);
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border-image: linear-gradient(#ff0f0e, #7773ff) var(--foo);
+      }
+
+      @supports (color: lab(0% 0 0)) {
+        .foo {
+          border-image: linear-gradient(lab(56.208% 94.4644 98.8928), lab(51% 70.4544 -115.586)) var(--foo);
+        }
+      }
+    "#
+      },
+      Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        border-image-source: linear-gradient(red, green);
+        border-image-source: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364));
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border-image-source: linear-gradient(red, green);
+        border-image-source: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364));
+      }
+    "#
+      },
+      Browsers {
+        chrome: Some(95 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        border-image-source: linear-gradient(red, green);
+        border-image-source: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364));
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border-image-source: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364));
+      }
+    "#
+      },
+      Browsers {
+        chrome: Some(112 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        border-image: linear-gradient(red, green);
+        border-image: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364));
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border-image: linear-gradient(red, green);
+        border-image: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364));
+      }
+    "#
+      },
+      Browsers {
+        chrome: Some(95 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        border-image: var(--fallback);
+        border-image: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364));
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border-image: var(--fallback);
+        border-image: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364));
+      }
+    "#
+      },
+      Browsers {
+        chrome: Some(95 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        border-image: url("fallback.png") 10 40 fill / 10px;
+        border-image: url("main.png") 10 40 fill / 10px space;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border-image: url("fallback.png") 10 40 fill / 10px;
+        border-image: url("main.png") 10 40 fill / 10px space;
+      }
+    "#
+      },
+      Browsers {
+        chrome: Some(50 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        border-image: url("fallback.png") 10 40 fill / 10px;
+        border-image: url("main.png") 10 40 fill / 10px space;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border-image: url("main.png") 10 40 fill / 10px space;
+      }
+    "#
+      },
+      Browsers {
+        chrome: Some(56 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    minify_test(".foo { border: none green }", ".foo{border:green}");
+  }
+
+  #[test]
+  pub fn test_border_radius() {
+    test(
+      r#"
+      .foo {
+        border-radius: 10px 100px 10px 100px;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border-radius: 10px 100px;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        border-radius: 10px 100px 10px 100px / 120px 120px;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border-radius: 10px 100px / 120px;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        border-top-left-radius: 10px 120px;
+        border-top-right-radius: 100px 120px;
+        border-bottom-right-radius: 100px 120px;
+        border-bottom-left-radius: 10px 120px;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border-radius: 10px 100px 100px 10px / 120px;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        border-top-left-radius: 4px 2px;
+        border-top-right-radius: 3px 4px;
+        border-bottom-right-radius: 6px 2px;
+        border-bottom-left-radius: 3px 4px;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border-radius: 4px 3px 6px / 2px 4px;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        border-top-left-radius: 1% 2%;
+        border-top-right-radius: 3% 4%;
+        border-bottom-right-radius: 5% 6%;
+        border-bottom-left-radius: 7% 8%;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border-radius: 1% 3% 5% 7% / 2% 4% 6% 8%;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        border-radius: 10px 100px 10px 100px / 120px 120px;
+        border-start-start-radius: 10px;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border-radius: 10px 100px / 120px;
+        border-start-start-radius: 10px;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        border-start-start-radius: 10px;
+        border-radius: 10px 100px 10px 100px / 120px 120px;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border-radius: 10px 100px / 120px;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        border-top-left-radius: 10px 120px;
+        border-top-right-radius: 100px 120px;
+        border-start-start-radius: 10px;
+        border-bottom-right-radius: 100px 120px;
+        border-bottom-left-radius: 10px 120px;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border-top-left-radius: 10px 120px;
+        border-top-right-radius: 100px 120px;
+        border-start-start-radius: 10px;
+        border-bottom-right-radius: 100px 120px;
+        border-bottom-left-radius: 10px 120px;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        border-radius: 10px;
+        border-top-left-radius: 20px;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border-radius: 20px 10px 10px;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        border-radius: 10px;
+        border-top-left-radius: var(--test);
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border-radius: 10px;
+        border-top-left-radius: var(--test);
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        -webkit-border-radius: 10px 100px 10px 100px;
+        -moz-border-radius: 10px 100px 10px 100px;
+        border-radius: 10px 100px 10px 100px;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        -webkit-border-radius: 10px 100px;
+        -moz-border-radius: 10px 100px;
+        border-radius: 10px 100px;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        -webkit-border-radius: 10px 100px 10px 100px;
+        -moz-border-radius: 20px;
+        border-radius: 30px;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        -webkit-border-radius: 10px 100px;
+        -moz-border-radius: 20px;
+        border-radius: 30px;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        -webkit-border-top-left-radius: 10px;
+        -moz-border-top-left-radius: 10px;
+        border-top-left-radius: 10px;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        -webkit-border-top-left-radius: 10px;
+        -moz-border-top-left-radius: 10px;
+        border-top-left-radius: 10px;
+      }
+    "#
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        border-radius: 30px;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        -webkit-border-radius: 30px;
+        -moz-border-radius: 30px;
+        border-radius: 30px;
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(4 << 16),
+        firefox: Some(3 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        border-top-left-radius: 30px;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        -webkit-border-top-left-radius: 30px;
+        -moz-border-top-left-radius: 30px;
+        border-top-left-radius: 30px;
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(4 << 16),
+        firefox: Some(3 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        -webkit-border-radius: 30px;
+        -moz-border-radius: 30px;
+        border-radius: 30px;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border-radius: 30px;
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(14 << 16),
+        firefox: Some(46 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        -webkit-border-top-left-radius: 30px;
+        -moz-border-top-left-radius: 30px;
+        border-top-left-radius: 30px;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        border-top-left-radius: 30px;
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(14 << 16),
+        firefox: Some(46 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        -webkit-border-radius: 30px;
+        -moz-border-radius: 30px;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        -webkit-border-radius: 30px;
+        -moz-border-radius: 30px;
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(14 << 16),
+        firefox: Some(46 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        -webkit-border-top-left-radius: 30px;
+        -moz-border-top-right-radius: 30px;
+        border-bottom-right-radius: 30px;
+        border-bottom-left-radius: 30px;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        -webkit-border-top-left-radius: 30px;
+        -moz-border-top-right-radius: 30px;
+        border-bottom-right-radius: 30px;
+        border-bottom-left-radius: 30px;
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(14 << 16),
+        firefox: Some(46 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        border-radius: var(--test);
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        -webkit-border-radius: var(--test);
+        -moz-border-radius: var(--test);
+        border-radius: var(--test);
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(4 << 16),
+        firefox: Some(3 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        border-start-start-radius: 5px;
+      }
+    "#,
+      indoc! {r#"
+      .foo:not(:lang(ae, ar, arc, bcc, bqi, ckb, dv, fa, glk, he, ku, mzn, nqo, pnb, ps, sd, ug, ur, yi)) {
+        border-top-left-radius: 5px;
+      }
+
+      .foo:lang(ae, ar, arc, bcc, bqi, ckb, dv, fa, glk, he, ku, mzn, nqo, pnb, ps, sd, ug, ur, yi) {
+        border-top-right-radius: 5px;
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(12 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        border-start-start-radius: 5px;
+        border-start-end-radius: 10px;
+      }
+    "#,
+      indoc! {r#"
+      .foo:not(:lang(ae, ar, arc, bcc, bqi, ckb, dv, fa, glk, he, ku, mzn, nqo, pnb, ps, sd, ug, ur, yi)) {
+        border-top-left-radius: 5px;
+        border-top-right-radius: 10px;
+      }
+
+      .foo:lang(ae, ar, arc, bcc, bqi, ckb, dv, fa, glk, he, ku, mzn, nqo, pnb, ps, sd, ug, ur, yi) {
+        border-top-left-radius: 10px;
+        border-top-right-radius: 5px;
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(12 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        border-end-end-radius: 10px;
+        border-end-start-radius: 5px;
+      }
+    "#,
+      indoc! {r#"
+      .foo:not(:lang(ae, ar, arc, bcc, bqi, ckb, dv, fa, glk, he, ku, mzn, nqo, pnb, ps, sd, ug, ur, yi)) {
+        border-bottom-right-radius: 10px;
+        border-bottom-left-radius: 5px;
+      }
+
+      .foo:lang(ae, ar, arc, bcc, bqi, ckb, dv, fa, glk, he, ku, mzn, nqo, pnb, ps, sd, ug, ur, yi) {
+        border-bottom-right-radius: 5px;
+        border-bottom-left-radius: 10px;
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(12 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        border-start-start-radius: var(--radius);
+      }
+    "#,
+      indoc! {r#"
+      .foo:not(:lang(ae, ar, arc, bcc, bqi, ckb, dv, fa, glk, he, ku, mzn, nqo, pnb, ps, sd, ug, ur, yi)) {
+        border-top-left-radius: var(--radius);
+      }
+
+      .foo:lang(ae, ar, arc, bcc, bqi, ckb, dv, fa, glk, he, ku, mzn, nqo, pnb, ps, sd, ug, ur, yi) {
+        border-top-right-radius: var(--radius);
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(12 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        border-start-start-radius: var(--start);
+        border-start-end-radius: var(--end);
+      }
+    "#,
+      indoc! {r#"
+      .foo:not(:lang(ae, ar, arc, bcc, bqi, ckb, dv, fa, glk, he, ku, mzn, nqo, pnb, ps, sd, ug, ur, yi)) {
+        border-top-left-radius: var(--start);
+        border-top-right-radius: var(--end);
+      }
+
+      .foo:lang(ae, ar, arc, bcc, bqi, ckb, dv, fa, glk, he, ku, mzn, nqo, pnb, ps, sd, ug, ur, yi) {
+        border-top-right-radius: var(--start);
+        border-top-left-radius: var(--end);
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(12 << 16),
+        ..Browsers::default()
+      },
+    );
+  }
+
+  #[test]
+  pub fn test_outline() {
+    test(
+      r#"
+      .foo {
+        outline-width: 2px;
+        outline-style: solid;
+        outline-color: blue;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        outline: 2px solid #00f;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        outline: 2px solid blue;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        outline: 2px solid #00f;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        outline: 2px solid red;
+        outline-color: blue;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        outline: 2px solid #00f;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        outline: 2px solid yellow;
+        outline-color: var(--color);
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        outline: 2px solid #ff0;
+        outline-color: var(--color);
+      }
+    "#
+      },
+    );
+
+    prefix_test(
+      ".foo { outline-color: lab(40% 56.6 39) }",
+      indoc! { r#"
+        .foo {
+          outline-color: #b32323;
+          outline-color: lab(40% 56.6 39);
+        }
+      "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { outline: 2px solid lab(40% 56.6 39) }",
+      indoc! { r#"
+        .foo {
+          outline: 2px solid #b32323;
+          outline: 2px solid lab(40% 56.6 39);
+        }
+      "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { outline: var(--width) solid lab(40% 56.6 39) }",
+      indoc! { r#"
+        .foo {
+          outline: var(--width) solid #b32323;
+        }
+
+        @supports (color: lab(0% 0 0)) {
+          .foo {
+            outline: var(--width) solid lab(40% 56.6 39);
+          }
+        }
+      "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+  }
+
+  #[test]
+  pub fn test_margin() {
+    test(
+      r#"
+      .foo {
+        margin-left: 10px;
+        margin-right: 10px;
+        margin-top: 20px;
+        margin-bottom: 20px;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        margin: 20px 10px;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        margin-block-start: 15px;
+        margin-block-end: 15px;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        margin-block: 15px;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        margin-left: 10px;
+        margin-right: 10px;
+        margin-inline-start: 15px;
+        margin-inline-end: 15px;
+        margin-top: 20px;
+        margin-bottom: 20px;
+
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        margin-left: 10px;
+        margin-right: 10px;
+        margin-inline: 15px;
+        margin-top: 20px;
+        margin-bottom: 20px;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        margin: 10px;
+        margin-top: 20px;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        margin: 20px 10px 10px;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        margin: 10px;
+        margin-top: var(--top);
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        margin: 10px;
+        margin-top: var(--top);
+      }
+    "#
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        margin-inline-start: 2px;
+      }
+    "#,
+      indoc! {r#"
+      .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) {
+        margin-left: 2px;
+      }
+
+      .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) {
+        margin-left: 2px;
+      }
+
+      .foo:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) {
+        margin-right: 2px;
+      }
+
+      .foo:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) {
+        margin-right: 2px;
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        margin-inline-start: 2px;
+        margin-inline-end: 4px;
+      }
+    "#,
+      indoc! {r#"
+      .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) {
+        margin-left: 2px;
+        margin-right: 4px;
+      }
+
+      .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) {
+        margin-left: 2px;
+        margin-right: 4px;
+      }
+
+      .foo:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) {
+        margin-left: 4px;
+        margin-right: 2px;
+      }
+
+      .foo:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) {
+        margin-left: 4px;
+        margin-right: 2px;
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        margin-inline: 2px;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        margin-left: 2px;
+        margin-right: 2px;
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        margin-block-start: 2px;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        margin-top: 2px;
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        margin-block-end: 2px;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        margin-bottom: 2px;
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        margin-inline-start: 2px;
+        margin-inline-end: 2px;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        margin-inline-start: 2px;
+        margin-inline-end: 2px;
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(13 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        margin-inline: 2px;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        margin-inline-start: 2px;
+        margin-inline-end: 2px;
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(13 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        margin-inline-start: 2px;
+        margin-inline-end: 2px;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        margin-inline: 2px;
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(15 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        margin-inline: 2px;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        margin-inline: 2px;
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(15 << 16),
+        ..Browsers::default()
+      },
+    );
+  }
+
+  #[test]
+  fn test_length() {
+    for prop in &[
+      "margin-right",
+      "margin",
+      "padding-right",
+      "padding",
+      "width",
+      "height",
+      "min-height",
+      "max-height",
+      "line-height",
+      "border-radius",
+    ] {
+      prefix_test(
+        &format!(
+          r#"
+        .foo {{
+          {}: 22px;
+          {}: max(4%, 22px);
+        }}
+      "#,
+          prop, prop
+        ),
+        &format!(
+          indoc! {r#"
+        .foo {{
+          {}: 22px;
+          {}: max(4%, 22px);
+        }}
+      "#
+          },
+          prop, prop
+        ),
+        Browsers {
+          safari: Some(10 << 16),
+          ..Browsers::default()
+        },
+      );
+
+      prefix_test(
+        &format!(
+          r#"
+        .foo {{
+          {}: 22px;
+          {}: max(4%, 22px);
+        }}
+      "#,
+          prop, prop
+        ),
+        &format!(
+          indoc! {r#"
+        .foo {{
+          {}: max(4%, 22px);
+        }}
+      "#
+          },
+          prop
+        ),
+        Browsers {
+          safari: Some(14 << 16),
+          ..Browsers::default()
+        },
+      );
+
+      prefix_test(
+        &format!(
+          r#"
+        .foo {{
+          {}: 22px;
+          {}: max(2cqw, 22px);
+        }}
+      "#,
+          prop, prop
+        ),
+        &format!(
+          indoc! {r#"
+        .foo {{
+          {}: 22px;
+          {}: max(2cqw, 22px);
+        }}
+      "#
+          },
+          prop, prop
+        ),
+        Browsers {
+          safari: Some(14 << 16),
+          ..Browsers::default()
+        },
+      );
+      prefix_test(
+        &format!(
+          r#"
+        .foo {{
+          {}: 22px;
+          {}: max(2cqw, 22px);
+        }}
+      "#,
+          prop, prop
+        ),
+        &format!(
+          indoc! {r#"
+        .foo {{
+          {}: max(2cqw, 22px);
+        }}
+      "#
+          },
+          prop
+        ),
+        Browsers {
+          safari: Some(16 << 16),
+          ..Browsers::default()
+        },
+      );
+    }
+  }
+
+  #[test]
+  pub fn test_padding() {
+    test(
+      r#"
+      .foo {
+        padding-left: 10px;
+        padding-right: 10px;
+        padding-top: 20px;
+        padding-bottom: 20px;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        padding: 20px 10px;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        padding-block-start: 15px;
+        padding-block-end: 15px;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        padding-block: 15px;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        padding-left: 10px;
+        padding-right: 10px;
+        padding-inline-start: 15px;
+        padding-inline-end: 15px;
+        padding-top: 20px;
+        padding-bottom: 20px;
+
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        padding-left: 10px;
+        padding-right: 10px;
+        padding-inline: 15px;
+        padding-top: 20px;
+        padding-bottom: 20px;
+      }
+    "#
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        padding-inline-start: 2px;
+      }
+    "#,
+      indoc! {r#"
+      .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) {
+        padding-left: 2px;
+      }
+
+      .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) {
+        padding-left: 2px;
+      }
+
+      .foo:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) {
+        padding-right: 2px;
+      }
+
+      .foo:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) {
+        padding-right: 2px;
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        padding-inline-start: 2px;
+        padding-inline-end: 4px;
+      }
+    "#,
+      indoc! {r#"
+      .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) {
+        padding-left: 2px;
+        padding-right: 4px;
+      }
+
+      .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) {
+        padding-left: 2px;
+        padding-right: 4px;
+      }
+
+      .foo:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) {
+        padding-left: 4px;
+        padding-right: 2px;
+      }
+
+      .foo:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) {
+        padding-left: 4px;
+        padding-right: 2px;
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        padding-inline-start: var(--padding);
+      }
+    "#,
+      indoc! {r#"
+      .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) {
+        padding-left: var(--padding);
+      }
+
+      .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) {
+        padding-left: var(--padding);
+      }
+
+      .foo:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) {
+        padding-right: var(--padding);
+      }
+
+      .foo:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) {
+        padding-right: var(--padding);
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        padding-inline: 2px;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        padding-left: 2px;
+        padding-right: 2px;
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        padding-block-start: 2px;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        padding-top: 2px;
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        padding-block-end: 2px;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        padding-bottom: 2px;
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        padding-top: 1px;
+        padding-left: 2px;
+        padding-bottom: 3px;
+        padding-right: 4px;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        padding: 1px 4px 3px 2px;
+      }
+    "#},
+      Browsers {
+        safari: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        padding-inline-start: 2px;
+        padding-inline-end: 2px;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        padding-inline-start: 2px;
+        padding-inline-end: 2px;
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(13 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        padding-inline-start: 2px;
+        padding-inline-end: 2px;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        padding-inline: 2px;
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(15 << 16),
+        ..Browsers::default()
+      },
+    );
+  }
+
+  #[test]
+  fn test_scroll_padding() {
+    prefix_test(
+      r#"
+      .foo {
+        scroll-padding-inline: 2px;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        scroll-padding-inline: 2px;
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+  }
+
+  #[test]
+  fn test_size() {
+    prefix_test(
+      r#"
+      .foo {
+        block-size: 25px;
+        inline-size: 25px;
+        min-block-size: 25px;
+        min-inline-size: 25px;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        height: 25px;
+        min-height: 25px;
+        width: 25px;
+        min-width: 25px;
+      }
+    "#},
+      Browsers {
+        safari: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        block-size: 25px;
+        min-block-size: 25px;
+        inline-size: 25px;
+        min-inline-size: 25px;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        block-size: 25px;
+        min-block-size: 25px;
+        inline-size: 25px;
+        min-inline-size: 25px;
+      }
+    "#},
+      Browsers {
+        safari: Some(14 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        block-size: var(--size);
+        min-block-size: var(--size);
+        inline-size: var(--size);
+        min-inline-size: var(--size);
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        height: var(--size);
+        min-height: var(--size);
+        width: var(--size);
+        min-width: var(--size);
+      }
+    "#},
+      Browsers {
+        safari: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    for (in_prop, out_prop) in [
+      ("width", "width"),
+      ("height", "height"),
+      ("block-size", "height"),
+      ("inline-size", "width"),
+      ("min-width", "min-width"),
+      ("min-height", "min-height"),
+      ("min-block-size", "min-height"),
+      ("min-inline-size", "min-width"),
+      ("max-width", "max-width"),
+      ("max-height", "max-height"),
+      ("max-block-size", "max-height"),
+      ("max-inline-size", "max-width"),
+    ] {
+      prefix_test(
+        &format!(
+          r#"
+        .foo {{
+          {}: stretch;
+        }}
+      "#,
+          in_prop
+        ),
+        &format!(
+          indoc! {r#"
+        .foo {{
+          {}: -webkit-fill-available;
+          {}: -moz-available;
+          {}: stretch;
+        }}
+      "#},
+          out_prop, out_prop, out_prop
+        ),
+        Browsers {
+          safari: Some(8 << 16),
+          firefox: Some(4 << 16),
+          ..Browsers::default()
+        },
+      );
+
+      prefix_test(
+        &format!(
+          r#"
+        .foo {{
+          {}: -webkit-fill-available;
+        }}
+      "#,
+          in_prop
+        ),
+        &format!(
+          indoc! {r#"
+        .foo {{
+          {}: -webkit-fill-available;
+        }}
+      "#},
+          out_prop
+        ),
+        Browsers {
+          safari: Some(8 << 16),
+          firefox: Some(4 << 16),
+          ..Browsers::default()
+        },
+      );
+
+      prefix_test(
+        &format!(
+          r#"
+        .foo {{
+          {}: 100vw;
+          {}: -webkit-fill-available;
+        }}
+      "#,
+          in_prop, in_prop
+        ),
+        &format!(
+          indoc! {r#"
+        .foo {{
+          {}: 100vw;
+          {}: -webkit-fill-available;
+        }}
+      "#},
+          out_prop, out_prop
+        ),
+        Browsers {
+          safari: Some(8 << 16),
+          firefox: Some(4 << 16),
+          ..Browsers::default()
+        },
+      );
+
+      prefix_test(
+        &format!(
+          r#"
+        .foo {{
+          {}: fit-content;
+        }}
+      "#,
+          in_prop
+        ),
+        &format!(
+          indoc! {r#"
+        .foo {{
+          {}: -webkit-fit-content;
+          {}: -moz-fit-content;
+          {}: fit-content;
+        }}
+      "#},
+          out_prop, out_prop, out_prop
+        ),
+        Browsers {
+          safari: Some(8 << 16),
+          firefox: Some(4 << 16),
+          ..Browsers::default()
+        },
+      );
+
+      prefix_test(
+        &format!(
+          r#"
+        .foo {{
+          {}: fit-content(50%);
+        }}
+      "#,
+          in_prop
+        ),
+        &format!(
+          indoc! {r#"
+        .foo {{
+          {}: fit-content(50%);
+        }}
+      "#},
+          out_prop
+        ),
+        Browsers {
+          safari: Some(8 << 16),
+          firefox: Some(4 << 16),
+          ..Browsers::default()
+        },
+      );
+
+      prefix_test(
+        &format!(
+          r#"
+        .foo {{
+          {}: min-content;
+        }}
+      "#,
+          in_prop
+        ),
+        &format!(
+          indoc! {r#"
+        .foo {{
+          {}: -webkit-min-content;
+          {}: -moz-min-content;
+          {}: min-content;
+        }}
+      "#},
+          out_prop, out_prop, out_prop
+        ),
+        Browsers {
+          safari: Some(8 << 16),
+          firefox: Some(4 << 16),
+          ..Browsers::default()
+        },
+      );
+
+      prefix_test(
+        &format!(
+          r#"
+        .foo {{
+          {}: max-content;
+        }}
+      "#,
+          in_prop
+        ),
+        &format!(
+          indoc! {r#"
+        .foo {{
+          {}: -webkit-max-content;
+          {}: -moz-max-content;
+          {}: max-content;
+        }}
+      "#},
+          out_prop, out_prop, out_prop
+        ),
+        Browsers {
+          safari: Some(8 << 16),
+          firefox: Some(4 << 16),
+          ..Browsers::default()
+        },
+      );
+
+      prefix_test(
+        &format!(
+          r#"
+        .foo {{
+          {}: 100%;
+          {}: max-content;
+        }}
+      "#,
+          in_prop, in_prop
+        ),
+        &format!(
+          indoc! {r#"
+        .foo {{
+          {}: 100%;
+          {}: max-content;
+        }}
+      "#},
+          out_prop, out_prop
+        ),
+        Browsers {
+          safari: Some(8 << 16),
+          firefox: Some(4 << 16),
+          ..Browsers::default()
+        },
+      );
+
+      prefix_test(
+        &format!(
+          r#"
+        .foo {{
+          {}: var(--fallback);
+          {}: max-content;
+        }}
+      "#,
+          in_prop, in_prop
+        ),
+        &format!(
+          indoc! {r#"
+        .foo {{
+          {}: var(--fallback);
+          {}: max-content;
+        }}
+      "#},
+          out_prop, out_prop
+        ),
+        Browsers {
+          safari: Some(8 << 16),
+          firefox: Some(4 << 16),
+          ..Browsers::default()
+        },
+      );
+    }
+
+    minify_test(".foo { aspect-ratio: auto }", ".foo{aspect-ratio:auto}");
+    minify_test(".foo { aspect-ratio: 2 / 3 }", ".foo{aspect-ratio:2/3}");
+    minify_test(".foo { aspect-ratio: auto 2 / 3 }", ".foo{aspect-ratio:auto 2/3}");
+    minify_test(".foo { aspect-ratio: 2 / 3 auto }", ".foo{aspect-ratio:auto 2/3}");
+
+    minify_test(
+      ".foo { width: 200px; width: var(--foo); }",
+      ".foo{width:200px;width:var(--foo)}",
+    );
+    minify_test(
+      ".foo { width: var(--foo); width: 200px; }",
+      ".foo{width:var(--foo);width:200px}",
+    );
+  }
+
+  #[test]
+  pub fn test_background() {
+    test(
+      r#"
+      .foo {
+        background: url(img.png);
+        background-position-x: 20px;
+        background-position-y: 10px;
+        background-size: 50px 100px;
+        background-repeat: repeat no-repeat;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        background: url("img.png") 20px 10px / 50px 100px repeat-x;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        background-color: red;
+        background-position: 0% 0%;
+        background-size: auto;
+        background-repeat: repeat;
+        background-clip: border-box;
+        background-origin: padding-box;
+        background-attachment: scroll;
+        background-image: none
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        background: red;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        background-color: gray;
+        background-position: 40% 50%;
+        background-size: 10em auto;
+        background-repeat: round;
+        background-clip: border-box;
+        background-origin: border-box;
+        background-attachment: fixed;
+        background-image: url('chess.png');
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        background: gray url("chess.png") 40% / 10em round fixed border-box;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        background: url(img.png), url(test.jpg) gray;
+        background-position-x: right 20px, 10px;
+        background-position-y: top 20px, 15px;
+        background-size: 50px 50px, auto;
+        background-repeat: repeat no-repeat, no-repeat;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        background: url("img.png") right 20px top 20px / 50px 50px repeat-x, gray url("test.jpg") 10px 15px no-repeat;
+      }
+    "#
+      },
+    );
+
+    minify_test(
+      r#"
+      .foo {
+        background-position: center center;
+      }
+    "#,
+      indoc! {".foo{background-position:50%}"
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        background: url(img.png) gray;
+        background-clip: content-box;
+        -webkit-background-clip: text;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        background: gray url("img.png") padding-box content-box;
+        -webkit-background-clip: text;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        background: url(img.png) gray;
+        -webkit-background-clip: text;
+        background-clip: content-box;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        background: gray url("img.png");
+        -webkit-background-clip: text;
+        background-clip: content-box;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        background: url(img.png) gray;
+        background-position: var(--pos);
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        background: gray url("img.png");
+        background-position: var(--pos);
+      }
+    "#
+      },
+    );
+
+    minify_test(
+      ".foo { background-position: bottom left }",
+      ".foo{background-position:0 100%}",
+    );
+    minify_test(
+      ".foo { background-position: left 10px center }",
+      ".foo{background-position:10px 50%}",
+    );
+    minify_test(
+      ".foo { background-position: right 10px center }",
+      ".foo{background-position:right 10px center}",
+    );
+    minify_test(
+      ".foo { background-position: right 10px top 20px }",
+      ".foo{background-position:right 10px top 20px}",
+    );
+    minify_test(
+      ".foo { background-position: left 10px top 20px }",
+      ".foo{background-position:10px 20px}",
+    );
+    minify_test(
+      ".foo { background-position: left 10px bottom 20px }",
+      ".foo{background-position:left 10px bottom 20px}",
+    );
+    minify_test(
+      ".foo { background-position: left 10px top }",
+      ".foo{background-position:10px 0}",
+    );
+    minify_test(
+      ".foo { background-position: bottom right }",
+      ".foo{background-position:100% 100%}",
+    );
+
+    minify_test(
+      ".foo { background: url('img-sprite.png') no-repeat bottom right }",
+      ".foo{background:url(img-sprite.png) 100% 100% no-repeat}",
+    );
+    minify_test(".foo { background: transparent }", ".foo{background:0 0}");
+
+    minify_test(".foo { background: url(\"data:image/svg+xml,%3Csvg width='168' height='24' xmlns='http://www.w3.org/2000/svg'%3E%3C/svg%3E\") }", ".foo{background:url(\"data:image/svg+xml,%3Csvg width='168' height='24' xmlns='http://www.w3.org/2000/svg'%3E%3C/svg%3E\")}");
+
+    test(
+      r#"
+      .foo {
+        background: url(img.png);
+        background-clip: text;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        background: url("img.png") text;
+      }
+    "#
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        background: url(img.png);
+        background-clip: text;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        background: url("img.png");
+        -webkit-background-clip: text;
+        background-clip: text;
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        background: url(img.png);
+        background-clip: text;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        background: url("img.png") text;
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(14 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        background: url(img.png) text;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        background: url("img.png");
+        -webkit-background-clip: text;
+        background-clip: text;
+      }
+    "#
+      },
+      Browsers {
+        chrome: Some(45 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        background: url(img.png);
+        -webkit-background-clip: text;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        background: url("img.png");
+        -webkit-background-clip: text;
+      }
+    "#
+      },
+      Browsers {
+        chrome: Some(45 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        background: url(img.png);
+        background-clip: text;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        background: url("img.png");
+        -webkit-background-clip: text;
+        background-clip: text;
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(14 << 16),
+        chrome: Some(95 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        background-image: url(img.png);
+        background-clip: text;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        background-image: url("img.png");
+        -webkit-background-clip: text;
+        background-clip: text;
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        -webkit-background-clip: text;
+        background-clip: text;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        -webkit-background-clip: text;
+        background-clip: text;
+      }
+    "#
+      },
+      Browsers {
+        chrome: Some(45 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        background-image: url(img.png);
+        background-clip: text;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        background-image: url("img.png");
+        background-clip: text;
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(14 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    minify_test(".foo { background: none center }", ".foo{background:50%}");
+    minify_test(".foo { background: none }", ".foo{background:0 0}");
+
+    prefix_test(
+      r#"
+      .foo {
+        background: lab(51.5117% 43.3777 -29.0443);
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        background: #af5cae;
+        background: lab(51.5117% 43.3777 -29.0443);
+      }
+    "#
+      },
+      Browsers {
+        chrome: Some(95 << 16),
+        safari: Some(15 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        background: lab(51.5117% 43.3777 -29.0443) url(foo.png);
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        background: #af5cae url("foo.png");
+        background: lab(51.5117% 43.3777 -29.0443) url("foo.png");
+      }
+    "#
+      },
+      Browsers {
+        chrome: Some(95 << 16),
+        safari: Some(15 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        background: lab(51.5117% 43.3777 -29.0443) linear-gradient(lab(52.2319% 40.1449 59.9171), lab(47.7776% -34.2947 -7.65904));
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        background: #af5cae linear-gradient(#c65d07, #00807c);
+        background: lab(51.5117% 43.3777 -29.0443) linear-gradient(lab(52.2319% 40.1449 59.9171), lab(47.7776% -34.2947 -7.65904));
+      }
+    "#
+      },
+      Browsers {
+        chrome: Some(95 << 16),
+        safari: Some(15 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    test(
+      ".foo { background: calc(var(--v) / 0.3)",
+      indoc! {r#"
+      .foo {
+        background: calc(var(--v) / .3);
+      }
+    "#},
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        background-color: #4263eb;
+        background-color: color(display-p3 0 .5 1);
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        background-color: #4263eb;
+        background-color: color(display-p3 0 .5 1);
+      }
+    "#
+      },
+      Browsers {
+        chrome: Some(99 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        background-color: #4263eb;
+        background-color: color(display-p3 0 .5 1);
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        background-color: color(display-p3 0 .5 1);
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(16 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        background-image: linear-gradient(red, green);
+        background-image: linear-gradient(lch(50% 132 50), lch(50% 130 150));
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        background-image: linear-gradient(red, green);
+        background-image: linear-gradient(lch(50% 132 50), lch(50% 130 150));
+      }
+    "#
+      },
+      Browsers {
+        chrome: Some(99 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        background-image: linear-gradient(red, green);
+        background-image: linear-gradient(lch(50% 132 50), lch(50% 130 150));
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        background-image: linear-gradient(lch(50% 132 50), lch(50% 130 150));
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(16 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        background: #4263eb;
+        background: color(display-p3 0 .5 1);
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        background: #4263eb;
+        background: color(display-p3 0 .5 1);
+      }
+    "#
+      },
+      Browsers {
+        chrome: Some(99 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        background: #4263eb;
+        background: color(display-p3 0 .5 1);
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        background: color(display-p3 0 .5 1);
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(16 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        background: linear-gradient(red, green);
+        background: linear-gradient(lch(50% 132 50), lch(50% 130 150));
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        background: linear-gradient(red, green);
+        background: linear-gradient(lch(50% 132 50), lch(50% 130 150));
+      }
+    "#
+      },
+      Browsers {
+        chrome: Some(99 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        background: red;
+        background: linear-gradient(lch(50% 132 50), lch(50% 130 150));
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        background: red;
+        background: linear-gradient(lch(50% 132 50), lch(50% 130 150));
+      }
+    "#
+      },
+      Browsers {
+        chrome: Some(99 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        background: linear-gradient(red, green);
+        background: linear-gradient(lch(50% 132 50), lch(50% 130 150));
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        background: linear-gradient(lch(50% 132 50), lch(50% 130 150));
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(16 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        background: var(--fallback);
+        background: linear-gradient(lch(50% 132 50), lch(50% 130 150));
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        background: var(--fallback);
+        background: linear-gradient(lch(50% 132 50), lch(50% 130 150));
+      }
+    "#
+      },
+      Browsers {
+        chrome: Some(99 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        background: red url(foo.png);
+        background: lch(50% 132 50) url(foo.png);
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        background: red url("foo.png");
+        background: lch(50% 132 50) url("foo.png");
+      }
+    "#
+      },
+      Browsers {
+        chrome: Some(99 << 16),
+        ..Browsers::default()
+      },
+    );
+  }
+
+  #[test]
+  pub fn test_flex() {
+    test(
+      r#"
+      .foo {
+        flex-direction: column;
+        flex-wrap: wrap;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        flex-flow: column wrap;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        flex-direction: row;
+        flex-wrap: wrap;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        flex-flow: wrap;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        flex-direction: row;
+        flex-wrap: nowrap;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        flex-flow: row;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        flex-direction: column;
+        flex-wrap: nowrap;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        flex-flow: column;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        flex-grow: 1;
+        flex-shrink: 1;
+        flex-basis: 0%;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        flex: 1;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        flex-grow: 1;
+        flex-shrink: 1;
+        flex-basis: 0;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        flex: 1 1 0;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        flex-grow: 1;
+        flex-shrink: 1;
+        flex-basis: 0px;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        flex: 1 1 0;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        flex-grow: 1;
+        flex-shrink: 2;
+        flex-basis: 0%;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        flex: 1 2;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        flex-grow: 2;
+        flex-shrink: 1;
+        flex-basis: 0%;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        flex: 2;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        flex-grow: 2;
+        flex-shrink: 2;
+        flex-basis: 0%;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        flex: 2 2;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        flex-grow: 1;
+        flex-shrink: 1;
+        flex-basis: 10px;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        flex: 10px;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        flex-grow: 2;
+        flex-shrink: 1;
+        flex-basis: 10px;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        flex: 2 10px;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        flex-grow: 1;
+        flex-shrink: 0;
+        flex-basis: 0%;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        flex: 1 0;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        flex-grow: 1;
+        flex-shrink: 0;
+        flex-basis: auto;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        flex: 1 0 auto;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        flex-grow: 1;
+        flex-shrink: 1;
+        flex-basis: auto;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        flex: auto;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        flex: 0 0;
+        flex-grow: 1;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        flex: 1 0;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        flex: 0 0;
+        flex-grow: var(--grow);
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        flex: 0 0;
+        flex-grow: var(--grow);
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        align-content: center;
+        justify-content: center;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        place-content: center;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        align-content: first baseline;
+        justify-content: safe right;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        place-content: baseline safe right;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        place-content: first baseline unsafe left;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        place-content: baseline unsafe left;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        place-content: center center;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        place-content: center;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        align-self: center;
+        justify-self: center;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        place-self: center;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        align-self: center;
+        justify-self: unsafe left;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        place-self: center unsafe left;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        align-items: center;
+        justify-items: center;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        place-items: center;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        align-items: center;
+        justify-items: legacy left;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        place-items: center legacy left;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        place-items: center;
+        justify-items: var(--justify);
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        place-items: center;
+        justify-items: var(--justify);
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        row-gap: 10px;
+        column-gap: 20px;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        gap: 10px 20px;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        row-gap: 10px;
+        column-gap: 10px;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        gap: 10px;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        gap: 10px;
+        column-gap: 20px;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        gap: 10px 20px;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        column-gap: 20px;
+        gap: 10px;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        gap: 10px;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        row-gap: normal;
+        column-gap: 20px;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        gap: normal 20px;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        -webkit-flex-grow: 1;
+        -webkit-flex-shrink: 1;
+        -webkit-flex-basis: auto;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        -webkit-flex: auto;
+      }
+    "#
+      },
+    );
+    test(
+      r#"
+      .foo {
+        -webkit-flex-grow: 1;
+        -webkit-flex-shrink: 1;
+        -webkit-flex-basis: auto;
+        flex-grow: 1;
+        flex-shrink: 1;
+        flex-basis: auto;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        -webkit-flex: auto;
+        flex: auto;
+      }
+    "#
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        -webkit-box-orient: horizontal;
+        -webkit-box-direction: normal;
+        flex-direction: row;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        -webkit-box-orient: horizontal;
+        -webkit-box-direction: normal;
+        -webkit-flex-direction: row;
+        flex-direction: row;
+      }
+    "#},
+      Browsers {
+        safari: Some(4 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        flex-direction: row;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        -webkit-box-orient: horizontal;
+        -moz-box-orient: horizontal;
+        -webkit-box-direction: normal;
+        -moz-box-direction: normal;
+        -webkit-flex-direction: row;
+        -ms-flex-direction: row;
+        flex-direction: row;
+      }
+    "#},
+      Browsers {
+        safari: Some(4 << 16),
+        firefox: Some(4 << 16),
+        ie: Some(10 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        -webkit-box-orient: horizontal;
+        -webkit-box-direction: normal;
+        -moz-box-orient: horizontal;
+        -moz-box-direction: normal;
+        -webkit-flex-direction: row;
+        -ms-flex-direction: row;
+        flex-direction: row;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        flex-direction: row;
+      }
+    "#},
+      Browsers {
+        safari: Some(14 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        flex-wrap: wrap;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        -webkit-box-lines: multiple;
+        -moz-box-lines: multiple;
+        -webkit-flex-wrap: wrap;
+        -ms-flex-wrap: wrap;
+        flex-wrap: wrap;
+      }
+    "#},
+      Browsers {
+        safari: Some(4 << 16),
+        firefox: Some(4 << 16),
+        ie: Some(10 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        -webkit-box-lines: multiple;
+        -moz-box-lines: multiple;
+        -webkit-flex-wrap: wrap;
+        -ms-flex-wrap: wrap;
+        flex-wrap: wrap;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        flex-wrap: wrap;
+      }
+    "#},
+      Browsers {
+        safari: Some(11 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        flex-flow: row wrap;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        -webkit-box-orient: horizontal;
+        -moz-box-orient: horizontal;
+        -webkit-box-direction: normal;
+        -moz-box-direction: normal;
+        -webkit-flex-flow: wrap;
+        -ms-flex-flow: wrap;
+        flex-flow: wrap;
+      }
+    "#},
+      Browsers {
+        safari: Some(4 << 16),
+        firefox: Some(4 << 16),
+        ie: Some(10 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        -webkit-box-orient: horizontal;
+        -moz-box-orient: horizontal;
+        -webkit-box-direction: normal;
+        -moz-box-direction: normal;
+        -webkit-flex-flow: wrap;
+        -ms-flex-flow: wrap;
+        flex-flow: wrap;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        flex-flow: wrap;
+      }
+    "#},
+      Browsers {
+        safari: Some(11 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        flex-grow: 1;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        -webkit-box-flex: 1;
+        -moz-box-flex: 1;
+        -ms-flex-positive: 1;
+        -webkit-flex-grow: 1;
+        flex-grow: 1;
+      }
+    "#},
+      Browsers {
+        safari: Some(4 << 16),
+        firefox: Some(4 << 16),
+        ie: Some(10 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        -webkit-box-flex: 1;
+        -moz-box-flex: 1;
+        -ms-flex-positive: 1;
+        -webkit-flex-grow: 1;
+        flex-grow: 1;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        flex-grow: 1;
+      }
+    "#},
+      Browsers {
+        safari: Some(11 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        flex-shrink: 1;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        -ms-flex-negative: 1;
+        -webkit-flex-shrink: 1;
+        flex-shrink: 1;
+      }
+    "#},
+      Browsers {
+        safari: Some(4 << 16),
+        firefox: Some(4 << 16),
+        ie: Some(10 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        -ms-flex-negative: 1;
+        -webkit-flex-shrink: 1;
+        flex-shrink: 1;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        flex-shrink: 1;
+      }
+    "#},
+      Browsers {
+        safari: Some(11 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        flex-basis: 1px;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        -ms-flex-preferred-size: 1px;
+        -webkit-flex-basis: 1px;
+        flex-basis: 1px;
+      }
+    "#},
+      Browsers {
+        safari: Some(4 << 16),
+        firefox: Some(4 << 16),
+        ie: Some(10 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        -ms-flex-preferred-size: 1px;
+        -webkit-flex-basis: 1px;
+        flex-basis: 1px;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        flex-basis: 1px;
+      }
+    "#},
+      Browsers {
+        safari: Some(11 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        flex: 1;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        -webkit-box-flex: 1;
+        -moz-box-flex: 1;
+        -webkit-flex: 1;
+        -ms-flex: 1;
+        flex: 1;
+      }
+    "#},
+      Browsers {
+        safari: Some(4 << 16),
+        firefox: Some(4 << 16),
+        ie: Some(10 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        -webkit-box-flex: 1;
+        -moz-box-flex: 1;
+        -webkit-flex: 1;
+        -ms-flex: 1;
+        flex: 1;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        flex: 1;
+      }
+    "#},
+      Browsers {
+        safari: Some(11 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        align-content: space-between;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        -ms-flex-line-pack: justify;
+        -webkit-align-content: space-between;
+        align-content: space-between;
+      }
+    "#},
+      Browsers {
+        safari: Some(4 << 16),
+        firefox: Some(4 << 16),
+        ie: Some(10 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        -ms-flex-line-pack: justify;
+        -webkit-align-content: space-between;
+        align-content: space-between;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        align-content: space-between;
+      }
+    "#},
+      Browsers {
+        safari: Some(11 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        justify-content: space-between;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        -webkit-box-pack: justify;
+        -moz-box-pack: justify;
+        -ms-flex-pack: justify;
+        -webkit-justify-content: space-between;
+        justify-content: space-between;
+      }
+    "#},
+      Browsers {
+        safari: Some(4 << 16),
+        firefox: Some(4 << 16),
+        ie: Some(10 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        -webkit-box-pack: justify;
+        -moz-box-pack: justify;
+        -ms-flex-pack: justify;
+        -webkit-justify-content: space-between;
+        justify-content: space-between;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        justify-content: space-between;
+      }
+    "#},
+      Browsers {
+        safari: Some(11 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        place-content: space-between flex-end;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        -ms-flex-line-pack: justify;
+        -webkit-box-pack: end;
+        -moz-box-pack: end;
+        -ms-flex-pack: end;
+        -webkit-align-content: space-between;
+        align-content: space-between;
+        -webkit-justify-content: flex-end;
+        justify-content: flex-end;
+      }
+    "#},
+      Browsers {
+        safari: Some(4 << 16),
+        firefox: Some(4 << 16),
+        ie: Some(10 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        -ms-flex-line-pack: justify;
+        -webkit-box-pack: end;
+        -moz-box-pack: end;
+        -ms-flex-pack: end;
+        -webkit-align-content: space-between;
+        -webkit-justify-content: flex-end;
+        place-content: space-between flex-end;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        place-content: space-between flex-end;
+      }
+    "#},
+      Browsers {
+        safari: Some(11 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        place-content: space-between flex-end;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        align-content: space-between;
+        justify-content: flex-end;
+      }
+    "#},
+      Browsers {
+        chrome: Some(30 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        place-content: space-between flex-end;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        place-content: space-between flex-end;
+      }
+    "#},
+      Browsers {
+        chrome: Some(60 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        align-self: flex-end;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        -ms-flex-item-align: end;
+        -webkit-align-self: flex-end;
+        align-self: flex-end;
+      }
+    "#},
+      Browsers {
+        safari: Some(4 << 16),
+        firefox: Some(4 << 16),
+        ie: Some(10 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        -ms-flex-item-align: end;
+        -webkit-align-self: flex-end;
+        align-self: flex-end;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        align-self: flex-end;
+      }
+    "#},
+      Browsers {
+        safari: Some(11 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        place-self: center flex-end;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        -ms-flex-item-align: center;
+        -webkit-align-self: center;
+        align-self: center;
+        justify-self: flex-end;
+      }
+    "#},
+      Browsers {
+        safari: Some(4 << 16),
+        firefox: Some(4 << 16),
+        ie: Some(10 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        -ms-flex-item-align: center;
+        -webkit-align-self: center;
+        place-self: center flex-end;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        place-self: center flex-end;
+      }
+    "#},
+      Browsers {
+        safari: Some(11 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        place-self: center flex-end;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        align-self: center;
+        justify-self: flex-end;
+      }
+    "#},
+      Browsers {
+        chrome: Some(57 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        place-self: center flex-end;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        place-self: center flex-end;
+      }
+    "#},
+      Browsers {
+        chrome: Some(59 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        align-items: flex-end;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        -webkit-box-align: end;
+        -moz-box-align: end;
+        -ms-flex-align: end;
+        -webkit-align-items: flex-end;
+        align-items: flex-end;
+      }
+    "#},
+      Browsers {
+        safari: Some(4 << 16),
+        firefox: Some(4 << 16),
+        ie: Some(10 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        -webkit-box-align: end;
+        -moz-box-align: end;
+        -ms-flex-align: end;
+        -webkit-align-items: flex-end;
+        align-items: flex-end;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        align-items: flex-end;
+      }
+    "#},
+      Browsers {
+        safari: Some(11 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        place-items: flex-end center;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        -webkit-box-align: end;
+        -moz-box-align: end;
+        -ms-flex-align: end;
+        -webkit-align-items: flex-end;
+        align-items: flex-end;
+        justify-items: center;
+      }
+    "#},
+      Browsers {
+        safari: Some(4 << 16),
+        firefox: Some(4 << 16),
+        ie: Some(10 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        -webkit-box-align: end;
+        -moz-box-align: end;
+        -ms-flex-align: end;
+        -webkit-align-items: flex-end;
+        place-items: flex-end center;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        place-items: flex-end center;
+      }
+    "#},
+      Browsers {
+        safari: Some(11 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        place-items: flex-end center;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        align-items: flex-end;
+        justify-items: center;
+      }
+    "#},
+      Browsers {
+        safari: Some(10 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        order: 1;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        -webkit-box-ordinal-group: 1;
+        -moz-box-ordinal-group: 1;
+        -ms-flex-order: 1;
+        -webkit-order: 1;
+        order: 1;
+      }
+    "#},
+      Browsers {
+        safari: Some(4 << 16),
+        firefox: Some(4 << 16),
+        ie: Some(10 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        -webkit-box-ordinal-group: 1;
+        -moz-box-ordinal-group: 1;
+        -ms-flex-order: 1;
+        -webkit-order: 1;
+        order: 1;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        order: 1;
+      }
+    "#},
+      Browsers {
+        safari: Some(11 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        -ms-flex: 0 0 8%;
+        flex: 0 0 5%;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        -ms-flex: 0 0 8%;
+        flex: 0 0 5%;
+      }
+    "#},
+      Browsers {
+        safari: Some(11 << 16),
+        ..Browsers::default()
+      },
+    );
+  }
+
+  #[test]
+  fn test_font() {
+    test(
+      r#"
+      .foo {
+        font-family: "Helvetica", "Times New Roman", sans-serif;
+        font-size: 12px;
+        font-weight: bold;
+        font-style: italic;
+        font-stretch: expanded;
+        font-variant-caps: small-caps;
+        line-height: 1.2em;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        font: italic small-caps bold expanded 12px / 1.2em Helvetica, Times New Roman, sans-serif;
+      }
+    "#
+      },
+    );
+
+    minify_test(
+      r#"
+      .foo {
+        font-family: "Helvetica", "Times New Roman", sans-serif;
+        font-size: 12px;
+        font-weight: bold;
+        font-style: italic;
+        font-stretch: expanded;
+        font-variant-caps: small-caps;
+        line-height: 1.2em;
+      }
+    "#,
+      indoc! {".foo{font:italic small-caps 700 125% 12px/1.2em Helvetica,Times New Roman,sans-serif}"
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        font: 12px "Helvetica", "Times New Roman", sans-serif;
+        line-height: 1.2em;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        font: 12px / 1.2em Helvetica, Times New Roman, sans-serif;
+      }
+    "#
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        font: 12px "Helvetica", "Times New Roman", sans-serif;
+        line-height: var(--lh);
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        font: 12px Helvetica, Times New Roman, sans-serif;
+        line-height: var(--lh);
+      }
+    "#
+      },
+    );
+
+    minify_test(
+      r#"
+      .foo {
+        font-family: "Helvetica", "Times New Roman", sans-serif;
+        font-size: 12px;
+        font-stretch: expanded;
+      }
+    "#,
+      indoc! {".foo{font-family:Helvetica,Times New Roman,sans-serif;font-size:12px;font-stretch:125%}"
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        font-family: "Helvetica", "Times New Roman", sans-serif;
+        font-size: 12px;
+        font-weight: bold;
+        font-style: italic;
+        font-stretch: expanded;
+        font-variant-caps: all-small-caps;
+        line-height: 1.2em;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        font: italic bold expanded 12px / 1.2em Helvetica, Times New Roman, sans-serif;
+        font-variant-caps: all-small-caps;
+      }
+    "#
+      },
+    );
+
+    minify_test(
+      ".foo { font: normal normal 600 9px/normal Charcoal; }",
+      ".foo{font:600 9px Charcoal}",
+    );
+    minify_test(
+      ".foo { font: normal normal 500 medium/normal Charcoal; }",
+      ".foo{font:500 medium Charcoal}",
+    );
+    minify_test(
+      ".foo { font: normal normal 400 medium Charcoal; }",
+      ".foo{font:400 medium Charcoal}",
+    );
+    minify_test(
+      ".foo { font: normal normal 500 medium/10px Charcoal; }",
+      ".foo{font:500 medium/10px Charcoal}",
+    );
+    minify_test(
+      ".foo { font-family: 'sans-serif'; }",
+      ".foo{font-family:\"sans-serif\"}",
+    );
+    minify_test(".foo { font-family: sans-serif; }", ".foo{font-family:sans-serif}");
+    minify_test(".foo { font-family: 'default'; }", ".foo{font-family:\"default\"}");
+    minify_test(".foo { font-family: default; }", ".foo{font-family:default}");
+    minify_test(".foo { font-family: 'inherit'; }", ".foo{font-family:\"inherit\"}");
+    minify_test(".foo { font-family: inherit; }", ".foo{font-family:inherit}");
+    minify_test(".foo { font-family: inherit test; }", ".foo{font-family:inherit test}");
+    minify_test(
+      ".foo { font-family: 'inherit test'; }",
+      ".foo{font-family:inherit test}",
+    );
+    minify_test(".foo { font-family: revert; }", ".foo{font-family:revert}");
+    minify_test(".foo { font-family: 'revert'; }", ".foo{font-family:\"revert\"}");
+    minify_test(".foo { font-family: revert-layer; }", ".foo{font-family:revert-layer}");
+    minify_test(
+      ".foo { font-family: revert-layer, serif; }",
+      ".foo{font-family:revert-layer,serif}",
+    );
+    minify_test(
+      ".foo { font-family: 'revert', sans-serif; }",
+      ".foo{font-family:\"revert\",sans-serif}",
+    );
+    minify_test(
+      ".foo { font-family: 'revert', foo, sans-serif; }",
+      ".foo{font-family:\"revert\",foo,sans-serif}",
+    );
+    minify_test(".foo { font-family: ''; }", ".foo{font-family:\"\"}");
+
+    // font-family in @font-face
+    minify_test(
+      "@font-face { font-family: 'revert'; }",
+      "@font-face{font-family:\"revert\"}",
+    );
+    minify_test(
+      "@font-face { font-family: 'revert-layer'; }",
+      "@font-face{font-family:\"revert-layer\"}",
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        font-family: Helvetica, system-ui, sans-serif;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        font-family: Helvetica, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Noto Sans, Ubuntu, Cantarell, Helvetica Neue, sans-serif;
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        font: 100%/1.5 Helvetica, system-ui, sans-serif;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        font: 100% / 1.5 Helvetica, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Noto Sans, Ubuntu, Cantarell, Helvetica Neue, sans-serif;
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Noto Sans, Ubuntu, Cantarell, Helvetica Neue, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
+      }
+    "#
+      },
+      Browsers {
+        firefox: Some(91 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        font-size: 22px;
+        font-size: max(2cqw, 22px);
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        font-size: 22px;
+        font-size: max(2cqw, 22px);
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(14 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        font-size: 22px;
+        font-size: max(2cqw, 22px);
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        font-size: max(2cqw, 22px);
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(16 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        font-size: 22px;
+        font-size: xxx-large;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        font-size: 22px;
+        font-size: xxx-large;
+      }
+    "#
+      },
+      Browsers {
+        chrome: Some(70 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        font-size: 22px;
+        font-size: xxx-large;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        font-size: xxx-large;
+      }
+    "#
+      },
+      Browsers {
+        chrome: Some(80 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        font-weight: 700;
+        font-weight: 789;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        font-weight: 700;
+        font-weight: 789;
+      }
+    "#
+      },
+      Browsers {
+        chrome: Some(60 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        font-weight: 700;
+        font-weight: 789;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        font-weight: 789;
+      }
+    "#
+      },
+      Browsers {
+        chrome: Some(80 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        font-family: Helvetica;
+        font-family: system-ui;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        font-family: Helvetica;
+        font-family: system-ui;
+      }
+    "#
+      },
+      Browsers {
+        chrome: Some(50 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        font-family: Helvetica;
+        font-family: system-ui;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        font-family: system-ui;
+      }
+    "#
+      },
+      Browsers {
+        chrome: Some(80 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        font-style: oblique;
+        font-style: oblique 40deg;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        font-style: oblique;
+        font-style: oblique 40deg;
+      }
+    "#
+      },
+      Browsers {
+        firefox: Some(50 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        font-style: oblique;
+        font-style: oblique 40deg;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        font-style: oblique 40deg;
+      }
+    "#
+      },
+      Browsers {
+        firefox: Some(80 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        font: 22px Helvetica;
+        font: xxx-large system-ui;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        font: 22px Helvetica;
+        font: xxx-large system-ui;
+      }
+    "#
+      },
+      Browsers {
+        chrome: Some(70 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        font: 22px Helvetica;
+        font: xxx-large system-ui;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        font: xxx-large system-ui;
+      }
+    "#
+      },
+      Browsers {
+        chrome: Some(80 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        font: var(--fallback);
+        font: xxx-large system-ui;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        font: var(--fallback);
+        font: xxx-large system-ui;
+      }
+    "#
+      },
+      Browsers {
+        chrome: Some(50 << 16),
+        ..Browsers::default()
+      },
+    );
+  }
+
+  #[test]
+  fn test_vertical_align() {
+    minify_test(".foo { vertical-align: middle }", ".foo{vertical-align:middle}");
+    minify_test(".foo { vertical-align: 0.3em }", ".foo{vertical-align:.3em}");
+  }
+
+  #[test]
+  fn test_selectors() {
+    minify_test(":nth-col(2n) {width: 20px}", ":nth-col(2n){width:20px}");
+    minify_test(":nth-col(10n-1) {width: 20px}", ":nth-col(10n-1){width:20px}");
+    minify_test(":nth-col(-n+2) {width: 20px}", ":nth-col(-n+2){width:20px}");
+    minify_test(":nth-col(even) {width: 20px}", ":nth-col(2n){width:20px}");
+    minify_test(":nth-col(odd) {width: 20px}", ":nth-col(odd){width:20px}");
+    minify_test(":nth-last-col(2n) {width: 20px}", ":nth-last-col(2n){width:20px}");
+    minify_test(":nth-last-col(10n-1) {width: 20px}", ":nth-last-col(10n-1){width:20px}");
+    minify_test(":nth-last-col(-n+2) {width: 20px}", ":nth-last-col(-n+2){width:20px}");
+    minify_test(":nth-last-col(even) {width: 20px}", ":nth-last-col(2n){width:20px}");
+    minify_test(":nth-last-col(odd) {width: 20px}", ":nth-last-col(odd){width:20px}");
+    minify_test(":nth-child(odd) {width: 20px}", ":nth-child(odd){width:20px}");
+    minify_test(":nth-child(2n) {width: 20px}", ":nth-child(2n){width:20px}");
+    minify_test(":nth-child(2n+1) {width: 20px}", ":nth-child(odd){width:20px}");
+    minify_test(":first-child {width: 20px}", ":first-child{width:20px}");
+    minify_test(":nth-child(1) {width: 20px}", ":first-child{width:20px}");
+    minify_test(":nth-last-child(1) {width: 20px}", ":last-child{width:20px}");
+    minify_test(":nth-of-type(1) {width: 20px}", ":first-of-type{width:20px}");
+    minify_test(":nth-last-of-type(1) {width: 20px}", ":last-of-type{width:20px}");
+    minify_test(
+      ":nth-child(even of li.important) {width: 20px}",
+      ":nth-child(2n of li.important){width:20px}",
+    );
+    minify_test(
+      ":nth-child(1 of li.important) {width: 20px}",
+      ":nth-child(1 of li.important){width:20px}",
+    );
+    minify_test(
+      ":nth-last-child(even of li.important) {width: 20px}",
+      ":nth-last-child(2n of li.important){width:20px}",
+    );
+    minify_test(
+      ":nth-last-child(1 of li.important) {width: 20px}",
+      ":nth-last-child(1 of li.important){width:20px}",
+    );
+    minify_test(
+      ":nth-last-child(1 of.important) {width: 20px}",
+      ":nth-last-child(1 of .important){width:20px}",
+    );
+
+    minify_test("[foo=\"baz\"] {color:red}", "[foo=baz]{color:red}");
+    minify_test("[foo=\"foo bar\"] {color:red}", "[foo=foo\\ bar]{color:red}");
+    minify_test("[foo=\"foo bar baz\"] {color:red}", "[foo=\"foo bar baz\"]{color:red}");
+    minify_test("[foo=\"\"] {color:red}", "[foo=\"\"]{color:red}");
+    minify_test(
+      ".test:not([foo=\"bar\"]) {color:red}",
+      ".test:not([foo=bar]){color:red}",
+    );
+    minify_test(".test + .foo {color:red}", ".test+.foo{color:red}");
+    minify_test(".test ~ .foo {color:red}", ".test~.foo{color:red}");
+    minify_test(".test .foo {color:red}", ".test .foo{color:red}");
+    minify_test(
+      ".custom-range::-webkit-slider-thumb:active {color:red}",
+      ".custom-range::-webkit-slider-thumb:active{color:red}",
+    );
+    minify_test(".test:not(.foo, .bar) {color:red}", ".test:not(.foo,.bar){color:red}");
+    minify_test(".test:is(.foo, .bar) {color:red}", ".test:is(.foo,.bar){color:red}");
+    minify_test(
+      ".test:where(.foo, .bar) {color:red}",
+      ".test:where(.foo,.bar){color:red}",
+    );
+    minify_test(
+      ".test:where(.foo, .bar) {color:red}",
+      ".test:where(.foo,.bar){color:red}",
+    );
+    minify_test(":host {color:red}", ":host{color:red}");
+    minify_test(":host(.foo) {color:red}", ":host(.foo){color:red}");
+    minify_test("::slotted(span) {color:red", "::slotted(span){color:red}");
+    minify_test(
+      "custom-element::part(foo) {color:red}",
+      "custom-element::part(foo){color:red}",
+    );
+    minify_test(".sm\\:text-5xl { font-size: 3rem }", ".sm\\:text-5xl{font-size:3rem}");
+    minify_test("a:has(> img) {color:red}", "a:has(>img){color:red}");
+    minify_test("dt:has(+ dt) {color:red}", "dt:has(+dt){color:red}");
+    minify_test(
+      "section:not(:has(h1, h2, h3, h4, h5, h6)) {color:red}",
+      "section:not(:has(h1,h2,h3,h4,h5,h6)){color:red}",
+    );
+    minify_test(
+      ":has(.sibling ~ .target) {color:red}",
+      ":has(.sibling~.target){color:red}",
+    );
+    minify_test(".x:has(> .a > .b) {color:red}", ".x:has(>.a>.b){color:red}");
+    minify_test(".x:has(.bar, #foo) {color:red}", ".x:has(.bar,#foo){color:red}");
+    minify_test(".x:has(span + span) {color:red}", ".x:has(span+span){color:red}");
+    minify_test("a:has(:visited) {color:red}", "a:has(:visited){color:red}");
+    for element in [
+      "-webkit-scrollbar",
+      "-webkit-scrollbar-button",
+      "-webkit-scrollbar-track",
+      "-webkit-scrollbar-track-piece",
+      "-webkit-scrollbar-thumb",
+      "-webkit-scrollbar-corner",
+      "-webkit-resizer",
+    ] {
+      for class in [
+        "enabled",
+        "disabled",
+        "hover",
+        "active",
+        "horizontal",
+        "vertical",
+        "decrement",
+        "increment",
+        "start",
+        "end",
+        "double-button",
+        "single-button",
+        "no-button",
+        "corner-present",
+        "window-inactive",
+      ] {
+        minify_test(
+          &format!("::{}:{} {{color:red}}", element, class),
+          &format!("::{}:{}{{color:red}}", element, class),
+        );
+      }
+    }
+    for class in [
+      "horizontal",
+      "vertical",
+      "decrement",
+      "increment",
+      "start",
+      "end",
+      "double-button",
+      "single-button",
+      "no-button",
+      "corner-present",
+      "window-inactive",
+    ] {
+      error_test(
+        &format!(":{} {{color:red}}", class),
+        ParserError::SelectorError(SelectorError::InvalidPseudoClassBeforeWebKitScrollbar),
+      );
+    }
+    for element in [
+      "-webkit-scrollbar",
+      "-webkit-scrollbar-button",
+      "-webkit-scrollbar-track",
+      "-webkit-scrollbar-track-piece",
+      "-webkit-scrollbar-thumb",
+      "-webkit-scrollbar-corner",
+      "-webkit-resizer",
+    ] {
+      error_test(
+        &format!("::{}:focus {{color:red}}", element),
+        ParserError::SelectorError(SelectorError::InvalidPseudoClassAfterWebKitScrollbar),
+      );
+    }
+
+    error_test(
+      "a::first-letter:last-child {color:red}",
+      ParserError::SelectorError(SelectorError::InvalidPseudoClassAfterPseudoElement),
+    );
+    minify_test(
+      "a:last-child::first-letter {color:red}",
+      "a:last-child:first-letter{color:red}",
+    );
+
+    prefix_test(
+      ".test:not(.foo, .bar) {color:red}",
+      indoc! {r#"
+      .test:not(:-webkit-any(.foo, .bar)) {
+        color: red;
+      }
+
+      .test:not(:is(.foo, .bar)) {
+        color: red;
+      }
+      "#},
+      Browsers {
+        safari: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      ".test:not(.foo, .bar) {color:red}",
+      indoc! {r#"
+      .test:not(.foo, .bar) {
+        color: red;
+      }
+      "#},
+      Browsers {
+        safari: Some(11 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    minify_test("a:lang(en) {color:red}", "a:lang(en){color:red}");
+    minify_test("a:lang(en, fr) {color:red}", "a:lang(en,fr){color:red}");
+    minify_test("a:lang('en') {color:red}", "a:lang(en){color:red}");
+    minify_test(
+      "a:-webkit-any(.foo, .bar) {color:red}",
+      "a:-webkit-any(.foo,.bar){color:red}",
+    );
+    minify_test("a:-moz-any(.foo, .bar) {color:red}", "a:-moz-any(.foo,.bar){color:red}");
+
+    prefix_test(
+      "a:is(.foo, .bar) {color:red}",
+      indoc! {r#"
+      a:-webkit-any(.foo, .bar) {
+        color: red;
+      }
+
+      a:-moz-any(.foo, .bar) {
+        color: red;
+      }
+
+      a:is(.foo, .bar) {
+        color: red;
+      }
+      "#},
+      Browsers {
+        safari: Some(11 << 16),
+        firefox: Some(50 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      "a:is(.foo > .bar) {color:red}",
+      indoc! {r#"
+      a:is(.foo > .bar) {
+        color: red;
+      }
+      "#},
+      Browsers {
+        safari: Some(11 << 16),
+        firefox: Some(50 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      "a:lang(en, fr) {color:red}",
+      indoc! {r#"
+      a:-webkit-any(:lang(en), :lang(fr)) {
+        color: red;
+      }
+
+      a:-moz-any(:lang(en), :lang(fr)) {
+        color: red;
+      }
+
+      a:is(:lang(en), :lang(fr)) {
+        color: red;
+      }
+      "#},
+      Browsers {
+        safari: Some(11 << 16),
+        firefox: Some(50 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      "a:lang(en, fr) {color:red}",
+      indoc! {r#"
+      a:is(:lang(en), :lang(fr)) {
+        color: red;
+      }
+      "#},
+      Browsers {
+        safari: Some(14 << 16),
+        firefox: Some(88 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      "a:lang(en, fr) {color:red}",
+      indoc! {r#"
+      a:lang(en, fr) {
+        color: red;
+      }
+      "#},
+      Browsers {
+        safari: Some(14 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      "a:dir(rtl) {color:red}",
+      indoc! {r#"
+      a:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) {
+        color: red;
+      }
+
+      a:-moz-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) {
+        color: red;
+      }
+
+      a:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) {
+        color: red;
+      }
+      "#},
+      Browsers {
+        safari: Some(11 << 16),
+        firefox: Some(50 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      "a:dir(ltr) {color:red}",
+      indoc! {r#"
+      a:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) {
+        color: red;
+      }
+
+      a:not(:-moz-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) {
+        color: red;
+      }
+
+      a:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) {
+        color: red;
+      }
+      "#},
+      Browsers {
+        safari: Some(11 << 16),
+        firefox: Some(50 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      "a:dir(rtl) {color:red}",
+      indoc! {r#"
+      a:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) {
+        color: red;
+      }
+      "#},
+      Browsers {
+        safari: Some(14 << 16),
+        firefox: Some(88 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      "a:dir(ltr) {color:red}",
+      indoc! {r#"
+      a:not(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) {
+        color: red;
+      }
+      "#},
+      Browsers {
+        safari: Some(14 << 16),
+        firefox: Some(88 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      "a:dir(rtl) {color:red}",
+      indoc! {r#"
+      a:lang(ae, ar, arc, bcc, bqi, ckb, dv, fa, glk, he, ku, mzn, nqo, pnb, ps, sd, ug, ur, yi) {
+        color: red;
+      }
+      "#},
+      Browsers {
+        safari: Some(14 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      "a:dir(ltr) {color:red}",
+      indoc! {r#"
+      a:not(:lang(ae, ar, arc, bcc, bqi, ckb, dv, fa, glk, he, ku, mzn, nqo, pnb, ps, sd, ug, ur, yi)) {
+        color: red;
+      }
+      "#},
+      Browsers {
+        safari: Some(14 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      "a:is(:dir(rtl)) {color:red}",
+      indoc! {r#"
+      a:lang(ae, ar, arc, bcc, bqi, ckb, dv, fa, glk, he, ku, mzn, nqo, pnb, ps, sd, ug, ur, yi) {
+        color: red;
+      }
+      "#},
+      Browsers {
+        safari: Some(14 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      "a:where(:dir(rtl)) {color:red}",
+      indoc! {r#"
+      a:where(:lang(ae, ar, arc, bcc, bqi, ckb, dv, fa, glk, he, ku, mzn, nqo, pnb, ps, sd, ug, ur, yi)) {
+        color: red;
+      }
+      "#},
+      Browsers {
+        safari: Some(14 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      "a:has(:dir(rtl)) {color:red}",
+      indoc! {r#"
+      a:has(:lang(ae, ar, arc, bcc, bqi, ckb, dv, fa, glk, he, ku, mzn, nqo, pnb, ps, sd, ug, ur, yi)) {
+        color: red;
+      }
+      "#},
+      Browsers {
+        safari: Some(14 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      "a:not(:dir(rtl)) {color:red}",
+      indoc! {r#"
+      a:not(:lang(ae, ar, arc, bcc, bqi, ckb, dv, fa, glk, he, ku, mzn, nqo, pnb, ps, sd, ug, ur, yi)) {
+        color: red;
+      }
+      "#},
+      Browsers {
+        safari: Some(14 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      "a:dir(rtl)::after {color:red}",
+      indoc! {r#"
+      a:lang(ae, ar, arc, bcc, bqi, ckb, dv, fa, glk, he, ku, mzn, nqo, pnb, ps, sd, ug, ur, yi):after {
+        color: red;
+      }
+      "#},
+      Browsers {
+        safari: Some(14 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      "a:dir(rtl) div {color:red}",
+      indoc! {r#"
+      a:lang(ae, ar, arc, bcc, bqi, ckb, dv, fa, glk, he, ku, mzn, nqo, pnb, ps, sd, ug, ur, yi) div {
+        color: red;
+      }
+      "#},
+      Browsers {
+        safari: Some(14 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    minify_test(".foo::cue {color: red}", ".foo::cue{color:red}");
+    minify_test(".foo::cue-region {color: red}", ".foo::cue-region{color:red}");
+    minify_test(".foo::cue(b) {color: red}", ".foo::cue(b){color:red}");
+    minify_test(".foo::cue-region(b) {color: red}", ".foo::cue-region(b){color:red}");
+    minify_test(
+      "::cue(v[voice='active']) {color: yellow;}",
+      "::cue(v[voice=active]){color:#ff0}",
+    );
+    minify_test(":foo(bar) { color: yellow }", ":foo(bar){color:#ff0}");
+    minify_test("::foo(bar) { color: yellow }", "::foo(bar){color:#ff0}");
+    minify_test("::foo(*) { color: yellow }", "::foo(*){color:#ff0}");
+
+    minify_test(":is(.foo) { color: yellow }", ".foo{color:#ff0}");
+    minify_test(":is(#foo) { color: yellow }", "#foo{color:#ff0}");
+    minify_test("a:is(.foo) { color: yellow }", "a.foo{color:#ff0}");
+    minify_test("a:is([data-test]) { color: yellow }", "a[data-test]{color:#ff0}");
+    minify_test(".foo:is(a) { color: yellow }", ".foo:is(a){color:#ff0}");
+    minify_test(".foo:is(*|a) { color: yellow }", ".foo:is(*|a){color:#ff0}");
+    minify_test(".foo:is(*) { color: yellow }", ".foo:is(*){color:#ff0}");
+    minify_test(
+      "@namespace svg url(http://www.w3.org/2000/svg); .foo:is(svg|a) { color: yellow }",
+      "@namespace svg \"http://www.w3.org/2000/svg\";.foo:is(svg|a){color:#ff0}",
+    );
+    minify_test("a:is(.foo .bar) { color: yellow }", "a:is(.foo .bar){color:#ff0}");
+    minify_test(":is(.foo, .bar) { color: yellow }", ":is(.foo,.bar){color:#ff0}");
+    minify_test("a:is(:not(.foo)) { color: yellow }", "a:not(.foo){color:#ff0}");
+    minify_test("a:is(:first-child) { color: yellow }", "a:first-child{color:#ff0}");
+    minify_test("a:is(:has(.foo)) { color: yellow }", "a:has(.foo){color:#ff0}");
+    minify_test("a:is(:is(.foo)) { color: yellow }", "a.foo{color:#ff0}");
+    minify_test(":host(:hover) {color: red}", ":host(:hover){color:red}");
+    minify_test("::slotted(:hover) {color: red}", "::slotted(:hover){color:red}");
+
+    minify_test(
+      ":root::view-transition {position: fixed}",
+      ":root::view-transition{position:fixed}",
+    );
+    minify_test(
+      ":root:active-view-transition {position: fixed}",
+      ":root:active-view-transition{position:fixed}",
+    );
+    minify_test(
+      ":root:active-view-transition-type(slide-in) {position: fixed}",
+      ":root:active-view-transition-type(slide-in){position:fixed}",
+    );
+    minify_test(
+      ":root:active-view-transition-type(slide-in, reverse) {position: fixed}",
+      ":root:active-view-transition-type(slide-in,reverse){position:fixed}",
+    );
+
+    for name in &[
+      "view-transition-group",
+      "view-transition-image-pair",
+      "view-transition-new",
+      "view-transition-old",
+    ] {
+      minify_test(
+        &format!(":root::{}(*) {{position: fixed}}", name),
+        &format!(":root::{}(*){{position:fixed}}", name),
+      );
+      minify_test(
+        &format!(":root::{}(*.class) {{position: fixed}}", name),
+        &format!(":root::{}(*.class){{position:fixed}}", name),
+      );
+      minify_test(
+        &format!(":root::{}(*.class.class) {{position: fixed}}", name),
+        &format!(":root::{}(*.class.class){{position:fixed}}", name),
+      );
+      minify_test(
+        &format!(":root::{}(foo) {{position: fixed}}", name),
+        &format!(":root::{}(foo){{position:fixed}}", name),
+      );
+      minify_test(
+        &format!(":root::{}(foo.class) {{position: fixed}}", name),
+        &format!(":root::{}(foo.class){{position:fixed}}", name),
+      );
+      minify_test(
+        &format!(":root::{}(foo.bar.baz) {{position: fixed}}", name),
+        &format!(":root::{}(foo.bar.baz){{position:fixed}}", name),
+      );
+      minify_test(
+        &format!(":root::{}(foo):only-child {{position: fixed}}", name),
+        &format!(":root::{}(foo):only-child{{position:fixed}}", name),
+      );
+      minify_test(
+        &format!(":root::{}(foo.bar.baz):only-child {{position: fixed}}", name),
+        &format!(":root::{}(foo.bar.baz):only-child{{position:fixed}}", name),
+      );
+      minify_test(
+        &format!(":root::{}(.foo) {{position: fixed}}", name),
+        &format!(":root::{}(.foo){{position:fixed}}", name),
+      );
+      minify_test(
+        &format!(":root::{}(.foo.bar) {{position: fixed}}", name),
+        &format!(":root::{}(.foo.bar){{position:fixed}}", name),
+      );
+      error_test(
+        &format!(":root::{}(foo):first-child {{position: fixed}}", name),
+        ParserError::SelectorError(SelectorError::InvalidPseudoClassAfterPseudoElement),
+      );
+      error_test(
+        &format!(":root::{}(foo)::before {{position: fixed}}", name),
+        ParserError::SelectorError(SelectorError::InvalidState),
+      );
+      error_test(
+        &format!(":root::{}(*.*) {{position: fixed}}", name),
+        ParserError::SelectorError(SelectorError::InvalidState),
+      );
+      error_test(
+        &format!(":root::{}(*. cls) {{position: fixed}}", name),
+        ParserError::SelectorError(SelectorError::InvalidState),
+      );
+      error_test(
+        &format!(":root::{}(foo .bar) {{position: fixed}}", name),
+        ParserError::SelectorError(SelectorError::InvalidState),
+      );
+      error_test(
+        &format!(":root::{}(*.cls. c) {{position: fixed}}", name),
+        ParserError::SelectorError(SelectorError::InvalidState),
+      );
+      error_test(
+        &format!(":root::{}(*.cls>cls) {{position: fixed}}", name),
+        ParserError::SelectorError(SelectorError::InvalidState),
+      );
+      error_test(
+        &format!(":root::{}(*.cls.foo.*) {{position: fixed}}", name),
+        ParserError::SelectorError(SelectorError::InvalidState),
+      );
+    }
+
+    minify_test(".foo ::deep .bar {width: 20px}", ".foo ::deep .bar{width:20px}");
+    minify_test(".foo::deep .bar {width: 20px}", ".foo::deep .bar{width:20px}");
+    minify_test(".foo ::deep.bar {width: 20px}", ".foo ::deep.bar{width:20px}");
+    minify_test(".foo ::unknown .bar {width: 20px}", ".foo ::unknown .bar{width:20px}");
+    minify_test(
+      ".foo ::unknown(foo) .bar {width: 20px}",
+      ".foo ::unknown(foo) .bar{width:20px}",
+    );
+    minify_test(
+      ".foo ::unknown:only-child {width: 20px}",
+      ".foo ::unknown:only-child{width:20px}",
+    );
+    minify_test(
+      ".foo ::unknown(.foo) .bar {width: 20px}",
+      ".foo ::unknown(.foo) .bar{width:20px}",
+    );
+    minify_test(
+      ".foo ::unknown(.foo .bar / .baz) .bar {width: 20px}",
+      ".foo ::unknown(.foo .bar / .baz) .bar{width:20px}",
+    );
+    minify_test(
+      ".foo ::unknown(something(foo)) .bar {width: 20px}",
+      ".foo ::unknown(something(foo)) .bar{width:20px}",
+    );
+    minify_test(
+      ".foo ::unknown([abc]) .bar {width: 20px}",
+      ".foo ::unknown([abc]) .bar{width:20px}",
+    );
+
+    let deep_options = ParserOptions {
+      flags: ParserFlags::DEEP_SELECTOR_COMBINATOR,
+      ..ParserOptions::default()
+    };
+
+    error_test(
+      ".foo >>> .bar {width: 20px}",
+      ParserError::SelectorError(SelectorError::DanglingCombinator),
+    );
+    error_test(
+      ".foo /deep/ .bar {width: 20px}",
+      ParserError::SelectorError(SelectorError::DanglingCombinator),
+    );
+    minify_test_with_options(
+      ".foo >>> .bar {width: 20px}",
+      ".foo>>>.bar{width:20px}",
+      deep_options.clone(),
+    );
+    minify_test_with_options(
+      ".foo /deep/ .bar {width: 20px}",
+      ".foo /deep/ .bar{width:20px}",
+      deep_options.clone(),
+    );
+
+    let pure_css_module_options = ParserOptions {
+      css_modules: Some(crate::css_modules::Config {
+        pure: true,
+        ..Default::default()
+      }),
+      ..ParserOptions::default()
+    };
+
+    minify_error_test_with_options(
+      "div {width: 20px}",
+      MinifyErrorKind::ImpureCSSModuleSelector,
+      pure_css_module_options.clone(),
+    );
+    minify_error_test_with_options(
+      ":global(.foo) {width: 20px}",
+      MinifyErrorKind::ImpureCSSModuleSelector,
+      pure_css_module_options.clone(),
+    );
+    minify_error_test_with_options(
+      "[foo=bar] {width: 20px}",
+      MinifyErrorKind::ImpureCSSModuleSelector,
+      pure_css_module_options.clone(),
+    );
+    minify_error_test_with_options(
+      "div, .foo {width: 20px}",
+      MinifyErrorKind::ImpureCSSModuleSelector,
+      pure_css_module_options.clone(),
+    );
+    minify_test_with_options(
+      ":local(.foo) {width: 20px}",
+      "._8Z4fiW_foo{width:20px}",
+      pure_css_module_options.clone(),
+    );
+    minify_test_with_options(
+      "div.my-class {color: red;}",
+      "div._8Z4fiW_my-class{color:red}",
+      pure_css_module_options.clone(),
+    );
+    minify_test_with_options(
+      "#id {color: red;}",
+      "#_8Z4fiW_id{color:red}",
+      pure_css_module_options.clone(),
+    );
+    minify_test_with_options(
+      "a .my-class{color: red;}",
+      "a ._8Z4fiW_my-class{color:red}",
+      pure_css_module_options.clone(),
+    );
+    minify_test_with_options(
+      ".my-class a {color: red;}",
+      "._8Z4fiW_my-class a{color:red}",
+      pure_css_module_options.clone(),
+    );
+    minify_test_with_options(
+      ".my-class:is(a) {color: red;}",
+      "._8Z4fiW_my-class:is(a){color:red}",
+      pure_css_module_options.clone(),
+    );
+    minify_test_with_options(
+      "div:has(.my-class) {color: red;}",
+      "div:has(._8Z4fiW_my-class){color:red}",
+      pure_css_module_options.clone(),
+    );
+    minify_test_with_options(
+      ".foo { html &:hover { a_value: some-value; } }",
+      "._8Z4fiW_foo{html &:hover{a_value:some-value}}",
+      pure_css_module_options.clone(),
+    );
+    minify_test_with_options(
+      ".foo { span { color: red; } }",
+      "._8Z4fiW_foo{& span{color:red}}",
+      pure_css_module_options.clone(),
+    );
+    minify_error_test_with_options(
+      "html { .foo { span { color: red; } } }",
+      MinifyErrorKind::ImpureCSSModuleSelector,
+      pure_css_module_options.clone(),
+    );
+    minify_test_with_options(
+      ".foo { div { span { color: red; } } }",
+      "._8Z4fiW_foo{& div{& span{color:red}}}",
+      pure_css_module_options.clone(),
+    );
+    minify_error_test_with_options(
+      "@scope (div) { .foo { color: red } }",
+      MinifyErrorKind::ImpureCSSModuleSelector,
+      pure_css_module_options.clone(),
+    );
+    minify_error_test_with_options(
+      "@scope (.a) to (div) { .foo { color: red } }",
+      MinifyErrorKind::ImpureCSSModuleSelector,
+      pure_css_module_options.clone(),
+    );
+    minify_error_test_with_options(
+      "@scope (.a) to (.b) { div { color: red } }",
+      MinifyErrorKind::ImpureCSSModuleSelector,
+      pure_css_module_options.clone(),
+    );
+    minify_test_with_options(
+      "@scope (.a) to (.b) { .foo { color: red } }",
+      "@scope(._8Z4fiW_a) to (._8Z4fiW_b){._8Z4fiW_foo{color:red}}",
+      pure_css_module_options.clone(),
+    );
+    minify_test_with_options(
+      "/* cssmodules-pure-no-check */ :global(.foo) { color: red }",
+      ".foo{color:red}",
+      pure_css_module_options.clone(),
+    );
+    minify_test_with_options(
+      "/*! some license */ /* cssmodules-pure-no-check */ :global(.foo) { color: red }",
+      "/*! some license */\n.foo{color:red}",
+      pure_css_module_options.clone(),
+    );
+
+    error_test(
+      "input.defaultCheckbox::before h1 {width: 20px}",
+      ParserError::SelectorError(SelectorError::UnexpectedSelectorAfterPseudoElement(Token::Ident(
+        "h1".into(),
+      ))),
+    );
+    error_test(
+      "input.defaultCheckbox::before .my-class {width: 20px}",
+      ParserError::SelectorError(SelectorError::UnexpectedSelectorAfterPseudoElement(Token::Delim('.'))),
+    );
+    error_test(
+      "input.defaultCheckbox::before.my-class {width: 20px}",
+      ParserError::SelectorError(SelectorError::UnexpectedSelectorAfterPseudoElement(Token::Delim('.'))),
+    );
+    error_test(
+      "input.defaultCheckbox::before #id {width: 20px}",
+      ParserError::SelectorError(SelectorError::UnexpectedSelectorAfterPseudoElement(Token::IDHash(
+        "id".into(),
+      ))),
+    );
+    error_test(
+      "input.defaultCheckbox::before#id {width: 20px}",
+      ParserError::SelectorError(SelectorError::UnexpectedSelectorAfterPseudoElement(Token::IDHash(
+        "id".into(),
+      ))),
+    );
+    error_test(
+      "input.defaultCheckbox::before [attr] {width: 20px}",
+      ParserError::SelectorError(SelectorError::UnexpectedSelectorAfterPseudoElement(
+        Token::SquareBracketBlock,
+      )),
+    );
+    error_test(
+      "input.defaultCheckbox::before[attr] {width: 20px}",
+      ParserError::SelectorError(SelectorError::UnexpectedSelectorAfterPseudoElement(
+        Token::SquareBracketBlock,
+      )),
+    );
+  }
+
+  #[test]
+  fn test_keyframes() {
+    minify_test(
+      r#"
+      @keyframes "test" {
+        100% {
+          background: blue
+        }
+      }
+    "#,
+      "@keyframes test{to{background:#00f}}",
+    );
+    minify_test(
+      r#"
+      @keyframes test {
+        100% {
+          background: blue
+        }
+      }
+    "#,
+      "@keyframes test{to{background:#00f}}",
+    );
+
+    // named animation range percentages
+    minify_test(
+      r#"
+      @keyframes test {
+        entry 0% {
+          background: blue
+        }
+        exit 100% {
+          background: green
+        }
+      }
+    "#,
+      "@keyframes test{entry 0%{background:#00f}exit 100%{background:green}}",
+    );
+
+    // CSS-wide keywords and `none` cannot remove quotes.
+    minify_test(
+      r#"
+      @keyframes "revert" {
+        from {
+          background: green;
+        }
+      }
+    "#,
+      "@keyframes \"revert\"{0%{background:green}}",
+    );
+
+    minify_test(
+      r#"
+      @keyframes "none" {
+        from {
+          background: green;
+        }
+      }
+    "#,
+      "@keyframes \"none\"{0%{background:green}}",
+    );
+
+    // named animation ranges cannot be used with to or from
+    minify_test(
+      r#"
+      @keyframes test {
+        entry to {
+          background: blue
+        }
+      }
+    "#,
+      "@keyframes test{}",
+    );
+
+    // CSS-wide keywords without quotes throws an error.
+    error_test(
+      r#"
+      @keyframes revert {}
+    "#,
+      ParserError::UnexpectedToken(Token::Ident("revert".into())),
+    );
+
+    error_test(
+      r#"
+      @keyframes revert-layer {}
+    "#,
+      ParserError::UnexpectedToken(Token::Ident("revert-layer".into())),
+    );
+
+    error_test(
+      r#"
+      @keyframes none {}
+    "#,
+      ParserError::UnexpectedToken(Token::Ident("none".into())),
+    );
+
+    error_test(
+      r#"
+      @keyframes NONE {}
+    "#,
+      ParserError::UnexpectedToken(Token::Ident("NONE".into())),
+    );
+
+    minify_test(
+      r#"
+      @-webkit-keyframes test {
+        from {
+          background: green;
+          background-color: red;
+        }
+
+        100% {
+          background: blue
+        }
+      }
+    "#,
+      "@-webkit-keyframes test{0%{background:red}to{background:#00f}}",
+    );
+    minify_test(
+      r#"
+      @-moz-keyframes test {
+        from {
+          background: green;
+          background-color: red;
+        }
+
+        100% {
+          background: blue
+        }
+      }
+    "#,
+      "@-moz-keyframes test{0%{background:red}to{background:#00f}}",
+    );
+    minify_test(r#"
+      @-webkit-keyframes test {
+        from {
+          background: green;
+          background-color: red;
+        }
+
+        100% {
+          background: blue
+        }
+      }
+      @-moz-keyframes test {
+        from {
+          background: green;
+          background-color: red;
+        }
+
+        100% {
+          background: blue
+        }
+      }
+    "#, "@-webkit-keyframes test{0%{background:red}to{background:#00f}}@-moz-keyframes test{0%{background:red}to{background:#00f}}");
+
+    prefix_test(
+      r#"
+      @keyframes test {
+        from {
+          background: green;
+        }
+        to {
+          background: blue
+        }
+      }
+    "#,
+      indoc! { r#"
+      @-webkit-keyframes test {
+        from {
+          background: green;
+        }
+
+        to {
+          background: #00f;
+        }
+      }
+
+      @-moz-keyframes test {
+        from {
+          background: green;
+        }
+
+        to {
+          background: #00f;
+        }
+      }
+
+      @keyframes test {
+        from {
+          background: green;
+        }
+
+        to {
+          background: #00f;
+        }
+      }
+    "#},
+      Browsers {
+        safari: Some(5 << 16),
+        firefox: Some(6 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      @-webkit-keyframes test {
+        from {
+          background: green;
+        }
+
+        to {
+          background: blue;
+        }
+      }
+      @-moz-keyframes test {
+        from {
+          background: green;
+        }
+
+        to {
+          background: blue;
+        }
+      }
+      @keyframes test {
+        from {
+          background: green;
+        }
+        to {
+          background: blue
+        }
+      }
+    "#,
+      indoc! { r#"
+      @keyframes test {
+        from {
+          background: green;
+        }
+
+        to {
+          background: #00f;
+        }
+      }
+    "#},
+      Browsers {
+        safari: Some(10 << 16),
+        firefox: Some(17 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      @-webkit-keyframes test1 {
+        from {
+          background: green;
+        }
+
+        to {
+          background: blue;
+        }
+      }
+
+      @-moz-keyframes test2 {
+        from {
+          background: green;
+        }
+
+        to {
+          background: blue;
+        }
+      }
+
+      @keyframes test3 {
+        from {
+          background: green;
+        }
+        to {
+          background: blue
+        }
+      }
+    "#,
+      indoc! { r#"
+      @-webkit-keyframes test1 {
+        from {
+          background: green;
+        }
+
+        to {
+          background: #00f;
+        }
+      }
+
+      @-moz-keyframes test2 {
+        from {
+          background: green;
+        }
+
+        to {
+          background: #00f;
+        }
+      }
+
+      @keyframes test3 {
+        from {
+          background: green;
+        }
+
+        to {
+          background: #00f;
+        }
+      }
+    "#},
+      Browsers {
+        safari: Some(10 << 16),
+        firefox: Some(17 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      @-webkit-keyframes test {
+        from {
+          background: green;
+        }
+
+        to {
+          background: red;
+        }
+      }
+      @-moz-keyframes test {
+        from {
+          background: green;
+        }
+
+        to {
+          background: pink;
+        }
+      }
+      @keyframes test {
+        from {
+          background: green;
+        }
+        to {
+          background: blue
+        }
+      }
+    "#,
+      indoc! { r#"
+      @-webkit-keyframes test {
+        from {
+          background: green;
+        }
+
+        to {
+          background: red;
+        }
+      }
+
+      @-moz-keyframes test {
+        from {
+          background: green;
+        }
+
+        to {
+          background: pink;
+        }
+      }
+
+      @keyframes test {
+        from {
+          background: green;
+        }
+
+        to {
+          background: #00f;
+        }
+      }
+    "#},
+      Browsers {
+        safari: Some(10 << 16),
+        firefox: Some(17 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    minify_test(
+      r#"
+      @keyframes test {
+        100% {
+          background: blue
+        }
+      }
+
+      @keyframes test {
+        100% {
+          background: red
+        }
+      }
+    "#,
+      "@keyframes test{to{background:red}}",
+    );
+    minify_test(
+      r#"
+      @keyframes test {
+        100% {
+          background: blue
+        }
+      }
+
+      @-webkit-keyframes test {
+        100% {
+          background: red
+        }
+      }
+    "#,
+      "@keyframes test{to{background:#00f}}@-webkit-keyframes test{to{background:red}}",
+    );
+  }
+
+  #[test]
+  fn test_important() {
+    test(
+      r#"
+      .foo {
+        align-items: center;
+        justify-items: center !important;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        align-items: center;
+        justify-items: center !important;
+      }
+    "#},
+    );
+
+    test(
+      r#"
+      .foo {
+        justify-items: center !important;
+        align-items: center;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        align-items: center;
+        justify-items: center !important;
+      }
+    "#},
+    );
+
+    minify_test(
+      r#"
+      .foo {
+        font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace !important;
+      }
+    "#,
+      ".foo{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace!important}",
+    );
+  }
+
+  #[test]
+  fn test_calc() {
+    minify_test(".foo { width: calc(20px * 2) }", ".foo{width:40px}");
+    minify_test(".foo { font-size: calc(100vw / 35) }", ".foo{font-size:2.85714vw}");
+    minify_test(".foo { width: calc(20px * 2 * 3) }", ".foo{width:120px}");
+    minify_test(".foo { width: calc(20px + 30px) }", ".foo{width:50px}");
+    minify_test(".foo { width: calc(20px + 30px + 40px) }", ".foo{width:90px}");
+    minify_test(".foo { width: calc(100% - 30px) }", ".foo{width:calc(100% - 30px)}");
+    minify_test(
+      ".foo { width: calc(100% - 30px + 20px) }",
+      ".foo{width:calc(100% - 10px)}",
+    );
+    minify_test(
+      ".foo { width: calc(20px + 100% - 30px) }",
+      ".foo{width:calc(100% - 10px)}",
+    );
+    minify_test(
+      ".foo { width: calc(20px + 100% + 10vw - 30px) }",
+      ".foo{width:calc(100% - 10px + 10vw)}",
+    );
+    minify_test(
+      ".foo { width: calc(20px + 100% - 30px) }",
+      ".foo{width:calc(100% - 10px)}",
+    );
+    minify_test(
+      ".foo { width: calc(2 * (100% - 20px)) }",
+      ".foo{width:calc(200% - 40px)}",
+    );
+    minify_test(
+      ".foo { width: calc((100% - 20px) * 2) }",
+      ".foo{width:calc(200% - 40px)}",
+    );
+    minify_test(".foo { width: calc(100% - 20px * 2) }", ".foo{width:calc(100% - 40px)}");
+    minify_test(".foo { width: calc(1px + 1px) }", ".foo{width:2px}");
+    minify_test(".foo { width: calc(100vw / 2) }", ".foo{width:50vw}");
+    minify_test(".foo { width: calc(50px - (20px - 30px)) }", ".foo{width:60px}");
+    minify_test(".foo { width: calc(100px - (100px - 100%)) }", ".foo{width:100%}");
+    minify_test(
+      ".foo { width: calc(100px + (100px - 100%)) }",
+      ".foo{width:calc(200px - 100%)}",
+    );
+    minify_test(
+      ".foo { width: calc(1px - (2em + 3%)) }",
+      ".foo{width:calc(1px + -2em - 3%)}",
+    ); // TODO: fix sign
+    minify_test(
+      ".foo { width: calc((100vw - 50em) / 2) }",
+      ".foo{width:calc(50vw - 25em)}",
+    );
+    minify_test(
+      ".foo { width: calc(1px - (2em + 4vh + 3%)) }",
+      ".foo{width:calc(1px + -2em - 4vh - 3%)}",
+    ); // TODO
+    minify_test(
+      ".foo { width: calc(1px + (2em + (3vh + 4px))) }",
+      ".foo{width:calc(2em + 3vh + 5px)}",
+    );
+    minify_test(
+      ".foo { width: calc(1px - (2em + 4px - 6vh) / 2) }",
+      ".foo{width:calc(-1em - 1px + 3vh)}",
+    );
+    minify_test(
+      ".foo { width: calc(100% - calc(50% + 25px)) }",
+      ".foo{width:calc(50% - 25px)}",
+    );
+    minify_test(".foo { width: calc(1px/100) }", ".foo{width:.01px}");
+    minify_test(
+      ".foo { width: calc(100vw / 2 - 6px + 0px) }",
+      ".foo{width:calc(50vw - 6px)}",
+    );
+    minify_test(".foo { width: calc(1px + 1) }", ".foo{width:calc(1px + 1)}");
+    minify_test(
+      ".foo { width: calc( (1em - calc( 10px + 1em)) / 2) }",
+      ".foo{width:-5px}",
+    );
+    minify_test(
+      ".foo { width: calc((100px - 1em) + (-50px + 1em)) }",
+      ".foo{width:50px}",
+    );
+    minify_test(
+      ".foo { width: calc(100% + (2 * 100px) - ((75.37% - 63.5px) - 900px)) }",
+      ".foo{width:calc(24.63% + 1163.5px)}",
+    );
+    minify_test(
+      ".foo { width: calc(((((100% + (2 * 30px) + 63.5px) / 0.7537) - (100vw - 60px)) / 2) + 30px) }",
+      ".foo{width:calc(66.3394% + 141.929px - 50vw)}",
+    );
+    minify_test(
+      ".foo { width: calc(((75.37% - 63.5px) - 900px) + (2 * 100px)) }",
+      ".foo{width:calc(75.37% - 763.5px)}",
+    );
+    minify_test(
+      ".foo { width: calc((900px - (10% - 63.5px)) + (2 * 100px)) }",
+      ".foo{width:calc(1163.5px - 10%)}",
+    );
+    minify_test(".foo { width: calc(500px/0) }", ".foo{width:calc(500px/0)}");
+    minify_test(".foo { width: calc(500px/2px) }", ".foo{width:calc(500px/2px)}");
+    minify_test(".foo { width: calc(100% / 3 * 3) }", ".foo{width:100%}");
+    minify_test(".foo { width: calc(+100px + +100px) }", ".foo{width:200px}");
+    minify_test(".foo { width: calc(+100px - +100px) }", ".foo{width:0}");
+    minify_test(".foo { width: calc(200px * +1) }", ".foo{width:200px}");
+    minify_test(".foo { width: calc(200px / +1) }", ".foo{width:200px}");
+    minify_test(".foo { width: calc(1.1e+1px + 1.1e+1px) }", ".foo{width:22px}");
+    minify_test(".foo { border-width: calc(1px + 2px) }", ".foo{border-width:3px}");
+    minify_test(
+      ".foo { border-width: calc(1em + 2px + 2em + 3px) }",
+      ".foo{border-width:calc(3em + 5px)}",
+    );
+
+    minify_test(
+      ".foo { border-width: min(1em, 2px) }",
+      ".foo{border-width:min(1em,2px)}",
+    );
+    minify_test(
+      ".foo { border-width: min(1em + 2em, 2px + 2px) }",
+      ".foo{border-width:min(3em,4px)}",
+    );
+    minify_test(
+      ".foo { border-width: min(1em + 2px, 2px + 1em) }",
+      ".foo{border-width:min(1em + 2px,2px + 1em)}",
+    );
+    minify_test(
+      ".foo { border-width: min(1em + 2px + 2px, 2px + 1em + 1px) }",
+      ".foo{border-width:min(1em + 4px,3px + 1em)}",
+    );
+    minify_test(
+      ".foo { border-width: min(2px + 1px, 3px + 4px) }",
+      ".foo{border-width:3px}",
+    );
+    minify_test(
+      ".foo { border-width: min(1px, 1em, 2px, 3in) }",
+      ".foo{border-width:min(1px,1em)}",
+    );
+
+    minify_test(
+      ".foo { border-width: max(1em, 2px) }",
+      ".foo{border-width:max(1em,2px)}",
+    );
+    minify_test(
+      ".foo { border-width: max(1em + 2em, 2px + 2px) }",
+      ".foo{border-width:max(3em,4px)}",
+    );
+    minify_test(
+      ".foo { border-width: max(1em + 2px, 2px + 1em) }",
+      ".foo{border-width:max(1em + 2px,2px + 1em)}",
+    );
+    minify_test(
+      ".foo { border-width: max(1em + 2px + 2px, 2px + 1em + 1px) }",
+      ".foo{border-width:max(1em + 4px,3px + 1em)}",
+    );
+    minify_test(
+      ".foo { border-width: max(2px + 1px, 3px + 4px) }",
+      ".foo{border-width:7px}",
+    );
+    minify_test(
+      ".foo { border-width: max(1px, 1em, 2px, 3in) }",
+      ".foo{border-width:max(3in,1em)}",
+    );
+
+    minify_test(".foo { border-width: clamp(1px, 2px, 3px) }", ".foo{border-width:2px}");
+    minify_test(".foo { border-width: clamp(1px, 10px, 3px) }", ".foo{border-width:3px}");
+    minify_test(".foo { border-width: clamp(5px, 2px, 10px) }", ".foo{border-width:5px}");
+    minify_test(
+      ".foo { border-width: clamp(100px, 2px, 10px) }",
+      ".foo{border-width:100px}",
+    );
+    minify_test(
+      ".foo { border-width: clamp(5px + 5px, 5px + 7px, 10px + 20px) }",
+      ".foo{border-width:12px}",
+    );
+
+    minify_test(
+      ".foo { border-width: clamp(1em, 2px, 4vh) }",
+      ".foo{border-width:clamp(1em,2px,4vh)}",
+    );
+    minify_test(
+      ".foo { border-width: clamp(1em, 2em, 4vh) }",
+      ".foo{border-width:clamp(1em,2em,4vh)}",
+    );
+    minify_test(
+      ".foo { border-width: clamp(1em, 2vh, 4vh) }",
+      ".foo{border-width:max(1em,2vh)}",
+    );
+    minify_test(
+      ".foo { border-width: clamp(1px, 1px + 2em, 4px) }",
+      ".foo{border-width:clamp(1px,1px + 2em,4px)}",
+    );
+    minify_test(".foo { border-width: clamp(1px, 2pt, 1in) }", ".foo{border-width:2pt}");
+    minify_test(
+      ".foo { width: clamp(-100px, 0px, 50% - 50vw); }",
+      ".foo{width:clamp(-100px,0px,50% - 50vw)}",
+    );
+
+    minify_test(
+      ".foo { top: calc(-1 * clamp(1.75rem, 8vw, 4rem)) }",
+      ".foo{top:calc(-1*clamp(1.75rem,8vw,4rem))}",
+    );
+    minify_test(
+      ".foo { top: calc(-1 * min(1.75rem, 8vw, 4rem)) }",
+      ".foo{top:calc(-1*min(1.75rem,8vw))}",
+    );
+    minify_test(
+      ".foo { top: calc(-1 * max(1.75rem, 8vw, 4rem)) }",
+      ".foo{top:calc(-1*max(4rem,8vw))}",
+    );
+    minify_test(
+      ".foo { top: calc(clamp(1.75rem, 8vw, 4rem) * -1) }",
+      ".foo{top:calc(-1*clamp(1.75rem,8vw,4rem))}",
+    );
+    minify_test(
+      ".foo { top: calc(min(1.75rem, 8vw, 4rem) * -1) }",
+      ".foo{top:calc(-1*min(1.75rem,8vw))}",
+    );
+    minify_test(
+      ".foo { top: calc(max(1.75rem, 8vw, 4rem) * -1) }",
+      ".foo{top:calc(-1*max(4rem,8vw))}",
+    );
+    minify_test(
+      ".foo { top: calc(clamp(1.75rem, 8vw, 4rem) / 2) }",
+      ".foo{top:calc(clamp(1.75rem,8vw,4rem)/2)}",
+    );
+    minify_test(
+      ".foo { top: calc(min(1.75rem, 8vw, 4rem) / 2) }",
+      ".foo{top:calc(min(1.75rem,8vw)/2)}",
+    );
+    minify_test(
+      ".foo { top: calc(max(1.75rem, 8vw, 4rem) / 2) }",
+      ".foo{top:calc(max(4rem,8vw)/2)}",
+    );
+    minify_test(
+      ".foo { top: calc(0.5 * clamp(1.75rem, 8vw, 4rem)) }",
+      ".foo{top:calc(clamp(1.75rem,8vw,4rem)/2)}",
+    );
+    minify_test(
+      ".foo { top: calc(1 * clamp(1.75rem, 8vw, 4rem)) }",
+      ".foo{top:calc(clamp(1.75rem,8vw,4rem))}",
+    );
+    minify_test(
+      ".foo { top: calc(2 * clamp(1.75rem, 8vw, 4rem) / 2) }",
+      ".foo{top:calc(clamp(1.75rem,8vw,4rem))}",
+    );
+
+    minify_test(".foo { width: max(0px, 1vw) }", ".foo{width:max(0px,1vw)}");
+
+    prefix_test(
+      ".foo { border-width: clamp(1em, 2px, 4vh) }",
+      indoc! { r#"
+        .foo {
+          border-width: max(1em, min(2px, 4vh));
+        }
+      "#},
+      Browsers {
+        safari: Some(12 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { border-width: clamp(1em, 2px, 4vh) }",
+      indoc! { r#"
+        .foo {
+          border-width: clamp(1em, 2px, 4vh);
+        }
+      "#},
+      Browsers {
+        safari: Some(14 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    minify_test(".foo { width: calc(1vh + 2vh) }", ".foo{width:3vh}");
+    minify_test(".foo { width: calc(1dvh + 2dvh) }", ".foo{width:3dvh}");
+    minify_test(".foo { width: calc(1lvh + 2lvh) }", ".foo{width:3lvh}");
+    minify_test(".foo { width: calc(1svh + 2svh) }", ".foo{width:3svh}");
+    minify_test(".foo { width: calc(1sVmin + 2Svmin) }", ".foo{width:3svmin}");
+    minify_test(".foo { width: calc(1ic + 2ic) }", ".foo{width:3ic}");
+    minify_test(".foo { width: calc(1ric + 2ric) }", ".foo{width:3ric}");
+    minify_test(".foo { width: calc(1cap + 2cap) }", ".foo{width:3cap}");
+    minify_test(".foo { width: calc(1lh + 2lh) }", ".foo{width:3lh}");
+    minify_test(".foo { width: calc(1x + 2x) }", ".foo{width:calc(1x + 2x)}");
+    minify_test(
+      ".foo { left: calc(50% - 100px + clamp(0px, calc(50vw - 50px), 100px)) }",
+      ".foo{left:calc(50% - 100px + clamp(0px,50vw - 50px,100px))}",
+    );
+    minify_test(
+      ".foo { left: calc(10px + min(10px, 1rem) + max(2px, 1vw)) }",
+      ".foo{left:calc(10px + min(10px,1rem) + max(2px,1vw))}",
+    );
+    minify_test(".foo { width: round(22px, 5px) }", ".foo{width:20px}");
+    minify_test(".foo { width: round(nearest, 22px, 5px) }", ".foo{width:20px}");
+    minify_test(".foo { width: round(down, 22px, 5px) }", ".foo{width:20px}");
+    minify_test(".foo { width: round(to-zero, 22px, 5px) }", ".foo{width:20px}");
+    minify_test(".foo { width: round(up, 22px, 5px) }", ".foo{width:25px}");
+    minify_test(".foo { width: round(23px, 5px) }", ".foo{width:25px}");
+    minify_test(".foo { width: round(nearest, 23px, 5px) }", ".foo{width:25px}");
+    minify_test(".foo { width: round(down, 23px, 5px) }", ".foo{width:20px}");
+    minify_test(".foo { width: round(to-zero, 23px, 5px) }", ".foo{width:20px}");
+    minify_test(".foo { width: round(up, 23px, 5px) }", ".foo{width:25px}");
+    minify_test(".foo { width: round(22px, 5vw) }", ".foo{width:round(22px,5vw)}");
+    minify_test(".foo { rotate: round(22deg, 5deg) }", ".foo{rotate:20deg}");
+    minify_test(".foo { rotate: round(22deg, 5deg) }", ".foo{rotate:20deg}");
+    minify_test(
+      ".foo { transition-duration: round(22ms, 5ms) }",
+      ".foo{transition-duration:20ms}",
+    );
+    minify_test(".foo { margin: round(to-zero, -23px, 5px) }", ".foo{margin:-20px}");
+    minify_test(".foo { margin: round(nearest, -23px, 5px) }", ".foo{margin:-25px}");
+    minify_test(".foo { margin: calc(10px * round(22, 5)) }", ".foo{margin:200px}");
+    minify_test(".foo { width: rem(18px, 5px) }", ".foo{width:3px}");
+    minify_test(".foo { width: rem(-18px, 5px) }", ".foo{width:-3px}");
+    minify_test(".foo { width: rem(18px, 5vw) }", ".foo{width:rem(18px,5vw)}");
+    minify_test(".foo { rotate: rem(-140deg, -90deg) }", ".foo{rotate:-50deg}");
+    minify_test(".foo { rotate: rem(140deg, -90deg) }", ".foo{rotate:50deg}");
+    minify_test(".foo { width: calc(10px * rem(18, 5)) }", ".foo{width:30px}");
+    minify_test(".foo { width: mod(18px, 5px) }", ".foo{width:3px}");
+    minify_test(".foo { width: mod(-18px, 5px) }", ".foo{width:2px}");
+    minify_test(".foo { rotate: mod(-140deg, -90deg) }", ".foo{rotate:-50deg}");
+    minify_test(".foo { rotate: mod(140deg, -90deg) }", ".foo{rotate:-40deg}");
+    minify_test(".foo { width: mod(18px, 5vw) }", ".foo{width:mod(18px,5vw)}");
+    minify_test(
+      ".foo { transform: rotateX(mod(140deg, -90deg)) rotateY(rem(140deg, -90deg)) }",
+      ".foo{transform:rotateX(-40deg)rotateY(50deg)}",
+    );
+    minify_test(".foo { width: calc(10px * mod(18, 5)) }", ".foo{width:30px}");
+
+    minify_test(
+      ".foo { width: calc(100% - 30px - 0) }",
+      ".foo{width:calc(100% - 30px - 0)}",
+    );
+    minify_test(
+      ".foo { width: calc(100% - 30px - 1 - 2) }",
+      ".foo{width:calc(100% - 30px - 3)}",
+    );
+    minify_test(
+      ".foo { width: calc(1 - 2 - 100% - 30px) }",
+      ".foo{width:calc(-1 - 100% - 30px)}",
+    );
+    minify_test(
+      ".foo { width: calc(2 * min(1px, 1vmin) - min(1px, 1vmin)); }",
+      ".foo{width:calc(2*min(1px,1vmin) - min(1px,1vmin))}",
+    );
+    minify_test(
+      ".foo { width: calc(100% - clamp(1.125rem, 1.25vw, 1.2375rem) - clamp(1.125rem, 1.25vw, 1.2375rem)); }",
+      ".foo{width:calc(100% - clamp(1.125rem,1.25vw,1.2375rem) - clamp(1.125rem,1.25vw,1.2375rem))}",
+    );
+    minify_test(
+      ".foo { width: calc(100% - 2 (2 * var(--card-margin))); }",
+      ".foo{width:calc(100% - 2 (2*var(--card-margin)))}",
+    );
+  }
+
+  #[test]
+  fn test_trig() {
+    minify_test(".foo { width: calc(2px * pi); }", ".foo{width:6.28319px}");
+    minify_test(".foo { width: calc(2px / pi); }", ".foo{width:.63662px}");
+    // minify_test(
+    //   ".foo { width: calc(2px * infinity); }",
+    //   ".foo{width:calc(2px*infinity)}",
+    // );
+    // minify_test(
+    //   ".foo { width: calc(2px * -infinity); }",
+    //   ".foo{width:calc(2px*-infinity)}",
+    // );
+    minify_test(".foo { width: calc(100px * sin(45deg))", ".foo{width:70.7107px}");
+    minify_test(".foo { width: calc(100px * sin(.125turn))", ".foo{width:70.7107px}");
+    minify_test(
+      ".foo { width: calc(100px * sin(3.14159265 / 4))",
+      ".foo{width:70.7107px}",
+    );
+    minify_test(".foo { width: calc(100px * sin(pi / 4))", ".foo{width:70.7107px}");
+    minify_test(
+      ".foo { width: calc(100px * sin(22deg + 23deg))",
+      ".foo{width:70.7107px}",
+    );
+
+    minify_test(".foo { width: calc(2px * cos(45deg))", ".foo{width:1.41421px}");
+    minify_test(".foo { width: calc(2px * tan(45deg))", ".foo{width:2px}");
+
+    minify_test(".foo { rotate: asin(sin(45deg))", ".foo{rotate:45deg}");
+    minify_test(".foo { rotate: asin(1)", ".foo{rotate:90deg}");
+    minify_test(".foo { rotate: asin(-1)", ".foo{rotate:-90deg}");
+    minify_test(".foo { rotate: asin(0.5)", ".foo{rotate:30deg}");
+    minify_test(".foo { rotate: asin(45deg)", ".foo{rotate:asin(45deg)}"); // invalid
+    minify_test(".foo { rotate: asin(-20)", ".foo{rotate:asin(-20)}"); // evaluates to NaN
+    minify_test(".foo { width: asin(sin(45deg))", ".foo{width:asin(sin(45deg))}"); // invalid
+
+    minify_test(".foo { rotate: acos(cos(45deg))", ".foo{rotate:45deg}");
+    minify_test(".foo { rotate: acos(-1)", ".foo{rotate:180deg}");
+    minify_test(".foo { rotate: acos(0)", ".foo{rotate:90deg}");
+    minify_test(".foo { rotate: acos(1)", ".foo{rotate:none}");
+    minify_test(".foo { rotate: acos(45deg)", ".foo{rotate:acos(45deg)}"); // invalid
+    minify_test(".foo { rotate: acos(-20)", ".foo{rotate:acos(-20)}"); // evaluates to NaN
+
+    minify_test(".foo { rotate: atan(tan(45deg))", ".foo{rotate:45deg}");
+    minify_test(".foo { rotate: atan(1)", ".foo{rotate:45deg}");
+    minify_test(".foo { rotate: atan(0)", ".foo{rotate:none}");
+    minify_test(".foo { rotate: atan(45deg)", ".foo{rotate:atan(45deg)}"); // invalid
+
+    minify_test(".foo { rotate: atan2(1px, -1px)", ".foo{rotate:135deg}");
+    minify_test(".foo { rotate: atan2(1vw, -1vw)", ".foo{rotate:135deg}");
+    minify_test(".foo { rotate: atan2(1, -1)", ".foo{rotate:135deg}");
+    minify_test(".foo { rotate: atan2(1ms, -1ms)", ".foo{rotate:135deg}");
+    minify_test(".foo { rotate: atan2(1%, -1%)", ".foo{rotate:135deg}");
+    minify_test(".foo { rotate: atan2(1deg, -1deg)", ".foo{rotate:135deg}");
+    minify_test(".foo { rotate: atan2(1cm, 1mm)", ".foo{rotate:84.2894deg}");
+    minify_test(".foo { rotate: atan2(0, -1)", ".foo{rotate:180deg}");
+    minify_test(".foo { rotate: atan2(-1, 1)", ".foo{rotate:-45deg}");
+    // incompatible units
+    minify_test(".foo { rotate: atan2(1px, -1vw)", ".foo{rotate:atan2(1px,-1vw)}");
+  }
+
+  #[test]
+  fn test_exp() {
+    minify_test(".foo { width: hypot()", ".foo{width:hypot()}");
+    minify_test(".foo { width: hypot(1px)", ".foo{width:1px}");
+    minify_test(".foo { width: hypot(1px, 2px)", ".foo{width:2.23607px}");
+    minify_test(".foo { width: hypot(1px, 2px, 3px)", ".foo{width:3.74166px}");
+    minify_test(".foo { width: hypot(1px, 2vw)", ".foo{width:hypot(1px,2vw)}");
+    minify_test(".foo { width: hypot(1px, 2px, 3vw)", ".foo{width:hypot(1px,2px,3vw)}");
+    minify_test(".foo { width: calc(100px * hypot(3, 4))", ".foo{width:500px}");
+    minify_test(".foo { width: calc(1px * pow(2, sqrt(100))", ".foo{width:1024px}");
+    minify_test(".foo { width: calc(100px * pow(2, pow(2, 2)", ".foo{width:1600px}");
+    minify_test(".foo { width: calc(1px * log(1))", ".foo{width:0}");
+    minify_test(".foo { width: calc(1px * log(10, 10))", ".foo{width:1px}");
+    minify_test(".foo { width: calc(1px * exp(0))", ".foo{width:1px}");
+    minify_test(".foo { width: calc(1px * log(e))", ".foo{width:1px}");
+    minify_test(".foo { width: calc(1px * (e - exp(1)))", ".foo{width:0}");
+    minify_test(
+      ".foo { width: calc(1px * (exp(log(1) + exp(0)*2))",
+      ".foo{width:7.38906px}",
+    );
+  }
+
+  #[test]
+  fn test_sign() {
+    minify_test(".foo { width: abs(1px)", ".foo{width:1px}");
+    minify_test(".foo { width: abs(-1px)", ".foo{width:1px}");
+    minify_test(".foo { width: abs(1%)", ".foo{width:abs(1%)}"); // spec says percentages must be against resolved value
+
+    minify_test(".foo { width: calc(10px * sign(-1vw)", ".foo{width:-10px}");
+    minify_test(".foo { width: calc(10px * sign(1%)", ".foo{width:calc(10px*sign(1%))}");
+  }
+
+  #[test]
+  fn test_box_shadow() {
+    minify_test(
+      ".foo { box-shadow: 64px 64px 12px 40px rgba(0,0,0,0.4) }",
+      ".foo{box-shadow:64px 64px 12px 40px #0006}",
+    );
+    minify_test(
+      ".foo { box-shadow: 12px 12px 0px 8px rgba(0,0,0,0.4) inset }",
+      ".foo{box-shadow:inset 12px 12px 0 8px #0006}",
+    );
+    minify_test(
+      ".foo { box-shadow: inset 12px 12px 0px 8px rgba(0,0,0,0.4) }",
+      ".foo{box-shadow:inset 12px 12px 0 8px #0006}",
+    );
+    minify_test(
+      ".foo { box-shadow: 12px 12px 8px 0px rgba(0,0,0,0.4) }",
+      ".foo{box-shadow:12px 12px 8px #0006}",
+    );
+    minify_test(
+      ".foo { box-shadow: 12px 12px 0px 0px rgba(0,0,0,0.4) }",
+      ".foo{box-shadow:12px 12px #0006}",
+    );
+    minify_test(
+      ".foo { box-shadow: 64px 64px 12px 40px rgba(0,0,0,0.4), 12px 12px 0px 8px rgba(0,0,0,0.4) inset }",
+      ".foo{box-shadow:64px 64px 12px 40px #0006,inset 12px 12px 0 8px #0006}",
+    );
+
+    prefix_test(
+      ".foo { box-shadow: 12px 12px lab(40% 56.6 39) }",
+      indoc! { r#"
+        .foo {
+          box-shadow: 12px 12px #b32323;
+          box-shadow: 12px 12px lab(40% 56.6 39);
+        }
+      "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { box-shadow: 12px 12px lab(40% 56.6 39) }",
+      indoc! { r#"
+        .foo {
+          -webkit-box-shadow: 12px 12px #b32323;
+          box-shadow: 12px 12px #b32323;
+          box-shadow: 12px 12px lab(40% 56.6 39);
+        }
+      "#},
+      Browsers {
+        chrome: Some(4 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { box-shadow: 12px 12px lab(40% 56.6 39), 12px 12px yellow }",
+      indoc! { r#"
+        .foo {
+          -webkit-box-shadow: 12px 12px #b32323, 12px 12px #ff0;
+          box-shadow: 12px 12px #b32323, 12px 12px #ff0;
+          box-shadow: 12px 12px lab(40% 56.6 39), 12px 12px #ff0;
+        }
+      "#},
+      Browsers {
+        chrome: Some(4 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { -webkit-box-shadow: 12px 12px #0006 }",
+      indoc! { r#"
+        .foo {
+          -webkit-box-shadow: 12px 12px rgba(0, 0, 0, .4);
+        }
+      "#},
+      Browsers {
+        chrome: Some(4 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo {
+        -webkit-box-shadow: 12px 12px #0006;
+        -moz-box-shadow: 12px 12px #0009;
+      }",
+      indoc! { r#"
+        .foo {
+          -webkit-box-shadow: 12px 12px rgba(0, 0, 0, .4);
+          -moz-box-shadow: 12px 12px rgba(0, 0, 0, .6);
+        }
+      "#},
+      Browsers {
+        chrome: Some(4 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo {
+        -webkit-box-shadow: 12px 12px #0006;
+        -moz-box-shadow: 12px 12px #0006;
+        box-shadow: 12px 12px #0006;
+      }",
+      indoc! { r#"
+        .foo {
+          box-shadow: 12px 12px #0006;
+        }
+      "#},
+      Browsers {
+        chrome: Some(95 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { box-shadow: var(--foo) 12px lab(40% 56.6 39) }",
+      indoc! { r#"
+        .foo {
+          box-shadow: var(--foo) 12px #b32323;
+        }
+
+        @supports (color: lab(0% 0 0)) {
+          .foo {
+            box-shadow: var(--foo) 12px lab(40% 56.6 39);
+          }
+        }
+      "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        box-shadow: 0px 0px 22px red;
+        box-shadow: 0px 0px max(2cqw, 22px) red;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        box-shadow: 0 0 22px red;
+        box-shadow: 0 0 max(2cqw, 22px) red;
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(14 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        box-shadow: 0px 0px 22px red;
+        box-shadow: 0px 0px max(2cqw, 22px) red;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        box-shadow: 0 0 max(2cqw, 22px) red;
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(16 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        box-shadow: 0px 0px 22px red;
+        box-shadow: 0px 0px 22px lab(40% 56.6 39);
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        box-shadow: 0 0 22px red;
+        box-shadow: 0 0 22px lab(40% 56.6 39);
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(14 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        box-shadow: 0px 0px 22px red;
+        box-shadow: 0px 0px 22px lab(40% 56.6 39);
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        box-shadow: 0 0 22px lab(40% 56.6 39);
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(16 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        box-shadow: var(--fallback);
+        box-shadow: 0px 0px 22px lab(40% 56.6 39);
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        box-shadow: var(--fallback);
+        box-shadow: 0 0 22px lab(40% 56.6 39);
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(16 << 16),
+        ..Browsers::default()
+      },
+    );
+  }
+
+  #[test]
+  fn test_media() {
+    minify_test(
+      "@media (min-width: 240px) { .foo { color: chartreuse }}",
+      "@media (width>=240px){.foo{color:#7fff00}}",
+    );
+    minify_test(
+      "@media (width < 240px) { .foo { color: chartreuse }}",
+      "@media (width<240px){.foo{color:#7fff00}}",
+    );
+    minify_test(
+      "@media (width <= 240px) { .foo { color: chartreuse }}",
+      "@media (width<=240px){.foo{color:#7fff00}}",
+    );
+    minify_test(
+      "@media (width > 240px) { .foo { color: chartreuse }}",
+      "@media (width>240px){.foo{color:#7fff00}}",
+    );
+    minify_test(
+      "@media (width >= 240px) { .foo { color: chartreuse }}",
+      "@media (width>=240px){.foo{color:#7fff00}}",
+    );
+    minify_test(
+      "@media (240px < width) { .foo { color: chartreuse }}",
+      "@media (width>240px){.foo{color:#7fff00}}",
+    );
+    minify_test(
+      "@media (240px <= width) { .foo { color: chartreuse }}",
+      "@media (width>=240px){.foo{color:#7fff00}}",
+    );
+    minify_test(
+      "@media (240px > width) { .foo { color: chartreuse }}",
+      "@media (width<240px){.foo{color:#7fff00}}",
+    );
+    minify_test(
+      "@media (240px >= width) { .foo { color: chartreuse }}",
+      "@media (width<=240px){.foo{color:#7fff00}}",
+    );
+    minify_test(
+      "@media (100px < width < 200px) { .foo { color: chartreuse }}",
+      "@media (100px<width<200px){.foo{color:#7fff00}}",
+    );
+    minify_test(
+      "@media (100px <= width <= 200px) { .foo { color: chartreuse }}",
+      "@media (100px<=width<=200px){.foo{color:#7fff00}}",
+    );
+    minify_test(
+      "@media (min-width: 30em) and (max-width: 50em) { .foo { color: chartreuse }}",
+      "@media (width>=30em) and (width<=50em){.foo{color:#7fff00}}",
+    );
+    minify_test(
+      "@media screen, print { .foo { color: chartreuse }}",
+      "@media screen,print{.foo{color:#7fff00}}",
+    );
+    minify_test(
+      "@media (hover: hover) { .foo { color: chartreuse }}",
+      "@media (hover:hover){.foo{color:#7fff00}}",
+    );
+    minify_test(
+      "@media (hover) { .foo { color: chartreuse }}",
+      "@media (hover){.foo{color:#7fff00}}",
+    );
+    minify_test(
+      "@media (aspect-ratio: 11/5) { .foo { color: chartreuse }}",
+      "@media (aspect-ratio:11/5){.foo{color:#7fff00}}",
+    );
+    minify_test(
+      "@media (aspect-ratio: 2/1) { .foo { color: chartreuse }}",
+      "@media (aspect-ratio:2){.foo{color:#7fff00}}",
+    );
+    minify_test(
+      "@media (aspect-ratio: 2) { .foo { color: chartreuse }}",
+      "@media (aspect-ratio:2){.foo{color:#7fff00}}",
+    );
+    minify_test(
+      "@media not screen and (color) { .foo { color: chartreuse }}",
+      "@media not screen and (color){.foo{color:#7fff00}}",
+    );
+    minify_test(
+      "@media only screen and (color) { .foo { color: chartreuse }}",
+      "@media only screen and (color){.foo{color:#7fff00}}",
+    );
+    minify_test(
+      "@media (update: slow) or (hover: none) { .foo { color: chartreuse }}",
+      "@media (update:slow) or (hover:none){.foo{color:#7fff00}}",
+    );
+    minify_test(
+      "@media (width < 600px) and (height < 600px) { .foo { color: chartreuse }}",
+      "@media (width<600px) and (height<600px){.foo{color:#7fff00}}",
+    );
+    minify_test(
+      "@media (not (color)) or (hover) { .foo { color: chartreuse }}",
+      "@media (not (color)) or (hover){.foo{color:#7fff00}}",
+    );
+    error_test(
+      "@media (example, all,), speech { .foo { color: chartreuse }}",
+      ParserError::UnexpectedToken(Token::Comma),
+    );
+    error_test(
+      "@media &test, speech { .foo { color: chartreuse }}",
+      ParserError::UnexpectedToken(Token::Delim('&')),
+    );
+    error_test(
+      "@media &test { .foo { color: chartreuse }}",
+      ParserError::UnexpectedToken(Token::Delim('&')),
+    );
+    minify_test(
+      "@media (min-width: calc(200px + 40px)) { .foo { color: chartreuse }}",
+      "@media (width>=240px){.foo{color:#7fff00}}",
+    );
+    minify_test(
+      "@media (min-width: calc(1em + 5px)) { .foo { color: chartreuse }}",
+      "@media (width>=calc(1em + 5px)){.foo{color:#7fff00}}",
+    );
+    minify_test("@media { .foo { color: chartreuse }}", ".foo{color:#7fff00}");
+    minify_test("@media all { .foo { color: chartreuse }}", ".foo{color:#7fff00}");
+    minify_test(
+      "@media not (((color) or (hover))) { .foo { color: chartreuse }}",
+      "@media not ((color) or (hover)){.foo{color:#7fff00}}",
+    );
+    minify_test(
+      "@media (hover) and ((color) and (test)) { .foo { color: chartreuse }}",
+      "@media (hover) and (color) and (test){.foo{color:#7fff00}}",
+    );
+    minify_test(
+      "@media (grid: 1) { .foo { color: chartreuse }}",
+      "@media (grid:1){.foo{color:#7fff00}}",
+    );
+    minify_test(
+      "@media (width >= calc(2px + 4px)) { .foo { color: chartreuse }}",
+      "@media (width>=6px){.foo{color:#7fff00}}",
+    );
+
+    prefix_test(
+      r#"
+        @media (width >= 240px) {
+          .foo {
+            color: chartreuse;
+          }
+        }
+      "#,
+      indoc! { r#"
+        @media (min-width: 240px) {
+          .foo {
+            color: #7fff00;
+          }
+        }
+      "#},
+      Browsers {
+        firefox: Some(60 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+        @media (width >= 240px) {
+          .foo {
+            color: chartreuse;
+          }
+        }
+      "#,
+      indoc! { r#"
+        @media (width >= 240px) {
+          .foo {
+            color: #7fff00;
+          }
+        }
+      "#},
+      Browsers {
+        firefox: Some(64 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+        @media (color > 2) {
+          .foo {
+            color: chartreuse;
+          }
+        }
+      "#,
+      indoc! { r#"
+        @media not (max-color: 2) {
+          .foo {
+            color: #7fff00;
+          }
+        }
+      "#},
+      Browsers {
+        firefox: Some(60 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+        @media (color < 2) {
+          .foo {
+            color: chartreuse;
+          }
+        }
+      "#,
+      indoc! { r#"
+        @media not (min-color: 2) {
+          .foo {
+            color: #7fff00;
+          }
+        }
+      "#},
+      Browsers {
+        firefox: Some(60 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+        @media (width > 240px) {
+          .foo {
+            color: chartreuse;
+          }
+        }
+      "#,
+      indoc! { r#"
+        @media not (max-width: 240px) {
+          .foo {
+            color: #7fff00;
+          }
+        }
+      "#},
+      Browsers {
+        firefox: Some(60 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+        @media (width <= 240px) {
+          .foo {
+            color: chartreuse;
+          }
+        }
+      "#,
+      indoc! { r#"
+        @media (max-width: 240px) {
+          .foo {
+            color: #7fff00;
+          }
+        }
+      "#},
+      Browsers {
+        firefox: Some(60 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+        @media (width <= 240px) {
+          .foo {
+            color: chartreuse;
+          }
+        }
+      "#,
+      indoc! { r#"
+        @media (width <= 240px) {
+          .foo {
+            color: #7fff00;
+          }
+        }
+      "#},
+      Browsers {
+        firefox: Some(64 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+        @media (width < 240px) {
+          .foo {
+            color: chartreuse;
+          }
+        }
+      "#,
+      indoc! { r#"
+        @media not (min-width: 240px) {
+          .foo {
+            color: #7fff00;
+          }
+        }
+      "#},
+      Browsers {
+        firefox: Some(60 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+        @media not (width < 240px) {
+          .foo {
+            color: chartreuse;
+          }
+        }
+      "#,
+      indoc! { r#"
+        @media (min-width: 240px) {
+          .foo {
+            color: #7fff00;
+          }
+        }
+      "#},
+      Browsers {
+        firefox: Some(60 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    test(
+      r#"
+        @media not (width < 240px) {
+          .foo {
+            color: chartreuse;
+          }
+        }
+      "#,
+      indoc! { r#"
+        @media (width >= 240px) {
+          .foo {
+            color: #7fff00;
+          }
+        }
+      "#},
+    );
+
+    prefix_test(
+      r#"
+        @media (width < 240px) and (hover) {
+          .foo {
+            color: chartreuse;
+          }
+        }
+      "#,
+      indoc! { r#"
+        @media (not (min-width: 240px)) and (hover) {
+          .foo {
+            color: #7fff00;
+          }
+        }
+      "#},
+      Browsers {
+        firefox: Some(60 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+        @media (100px <= width <= 200px) {
+          .foo {
+            color: chartreuse;
+          }
+        }
+      "#,
+      indoc! { r#"
+        @media (min-width: 100px) and (max-width: 200px) {
+          .foo {
+            color: #7fff00;
+          }
+        }
+      "#},
+      Browsers {
+        firefox: Some(85 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+        @media not (100px <= width <= 200px) {
+          .foo {
+            color: chartreuse;
+          }
+        }
+      "#,
+      indoc! { r#"
+        @media not ((min-width: 100px) and (max-width: 200px)) {
+          .foo {
+            color: #7fff00;
+          }
+        }
+      "#},
+      Browsers {
+        firefox: Some(85 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+        @media (hover) and (100px <= width <= 200px) {
+          .foo {
+            color: chartreuse;
+          }
+        }
+      "#,
+      indoc! { r#"
+        @media (hover) and (min-width: 100px) and (max-width: 200px) {
+          .foo {
+            color: #7fff00;
+          }
+        }
+      "#},
+      Browsers {
+        firefox: Some(85 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+        @media (hover) or (100px <= width <= 200px) {
+          .foo {
+            color: chartreuse;
+          }
+        }
+      "#,
+      indoc! { r#"
+        @media (hover) or ((min-width: 100px) and (max-width: 200px)) {
+          .foo {
+            color: #7fff00;
+          }
+        }
+      "#},
+      Browsers {
+        firefox: Some(85 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+        @media (100px < width < 200px) {
+          .foo {
+            color: chartreuse;
+          }
+        }
+      "#,
+      indoc! { r#"
+        @media (not (max-width: 100px)) and (not (min-width: 200px)) {
+          .foo {
+            color: #7fff00;
+          }
+        }
+      "#},
+      Browsers {
+        firefox: Some(85 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+        @media not (100px < width < 200px) {
+          .foo {
+            color: chartreuse;
+          }
+        }
+      "#,
+      indoc! { r#"
+        @media not ((not (max-width: 100px)) and (not (min-width: 200px))) {
+          .foo {
+            color: #7fff00;
+          }
+        }
+      "#},
+      Browsers {
+        firefox: Some(85 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+        @media (200px >= width >= 100px) {
+          .foo {
+            color: chartreuse;
+          }
+        }
+      "#,
+      indoc! { r#"
+        @media (max-width: 200px) and (min-width: 100px) {
+          .foo {
+            color: #7fff00;
+          }
+        }
+      "#},
+      Browsers {
+        firefox: Some(85 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    test(
+      r#"
+      @media not all {
+        .a {
+          color: green;
+        }
+      }
+      "#,
+      "\n",
+    );
+
+    prefix_test(
+      r#"
+      @media (width > calc(1px + 1rem)) {
+        .foo { color: yellow; }
+      }
+      "#,
+      indoc! { r#"
+        @media not (max-width: calc(1px + 1rem)) {
+          .foo {
+            color: #ff0;
+          }
+        }
+      "#},
+      Browsers {
+        chrome: Some(85 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      @media (width > max(10px, 1rem)) {
+        .foo { color: yellow; }
+      }
+      "#,
+      indoc! { r#"
+        @media not (max-width: max(10px, 1rem)) {
+          .foo {
+            color: #ff0;
+          }
+        }
+      "#},
+      Browsers {
+        chrome: Some(85 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      @media (width > 0) {
+        .foo { color: yellow; }
+      }
+      "#,
+      indoc! { r#"
+        @media not (max-width: 0) {
+          .foo {
+            color: #ff0;
+          }
+        }
+      "#},
+      Browsers {
+        chrome: Some(85 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      @media (min-resolution: 2dppx) {
+        .foo { color: yellow; }
+      }
+      "#,
+      indoc! { r#"
+        @media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 2dppx) {
+          .foo {
+            color: #ff0;
+          }
+        }
+      "#},
+      Browsers {
+        safari: Some(15 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      @media (min-resolution: 2dppx) {
+        .foo { color: yellow; }
+      }
+      "#,
+      indoc! { r#"
+        @media (min--moz-device-pixel-ratio: 2), (min-resolution: 2dppx) {
+          .foo {
+            color: #ff0;
+          }
+        }
+      "#},
+      Browsers {
+        firefox: Some(10 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      @media (resolution > 2dppx) {
+        .foo { color: yellow; }
+      }
+      "#,
+      indoc! { r#"
+        @media not (-webkit-max-device-pixel-ratio: 2), not (max-resolution: 2dppx) {
+          .foo {
+            color: #ff0;
+          }
+        }
+      "#},
+      Browsers {
+        safari: Some(15 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      @media (resolution >= 300dpi) {
+        .foo { color: yellow; }
+      }
+      "#,
+      indoc! { r#"
+        @media (-webkit-min-device-pixel-ratio: 3.125), (min-resolution: 300dpi) {
+          .foo {
+            color: #ff0;
+          }
+        }
+      "#},
+      Browsers {
+        safari: Some(15 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      @media (min-resolution: 113.38dpcm) {
+        .foo { color: yellow; }
+      }
+      "#,
+      indoc! { r#"
+        @media (-webkit-min-device-pixel-ratio: 2.99985), (min--moz-device-pixel-ratio: 2.99985), (min-resolution: 113.38dpcm) {
+          .foo {
+            color: #ff0;
+          }
+        }
+      "#},
+      Browsers {
+        safari: Some(15 << 16),
+        firefox: Some(10 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      @media (color) and (min-resolution: 2dppx) {
+        .foo { color: yellow; }
+      }
+      "#,
+      indoc! { r#"
+        @media (color) and (-webkit-min-device-pixel-ratio: 2), (color) and (min-resolution: 2dppx) {
+          .foo {
+            color: #ff0;
+          }
+        }
+      "#},
+      Browsers {
+        safari: Some(15 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      @media (min-resolution: 2dppx),
+             (min-resolution: 192dpi) {
+        .foo { color: yellow; }
+      }
+      "#,
+      indoc! { r#"
+        @media (-webkit-min-device-pixel-ratio: 2), (min--moz-device-pixel-ratio: 2), (min-resolution: 2dppx), (min-resolution: 192dpi) {
+          .foo {
+            color: #ff0;
+          }
+        }
+      "#},
+      Browsers {
+        safari: Some(15 << 16),
+        firefox: Some(10 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      @media only screen and (min-resolution: 124.8dpi) {
+        .foo { color: yellow; }
+      }
+      "#,
+      indoc! { r#"
+        @media only screen and (-webkit-min-device-pixel-ratio: 1.3), only screen and (min--moz-device-pixel-ratio: 1.3), only screen and (min-resolution: 124.8dpi) {
+          .foo {
+            color: #ff0;
+          }
+        }
+      "#},
+      Browsers {
+        safari: Some(15 << 16),
+        firefox: Some(10 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    error_test(
+      "@media (min-width: hi) { .foo { color: chartreuse }}",
+      ParserError::InvalidMediaQuery,
+    );
+    error_test(
+      "@media (width >= hi) { .foo { color: chartreuse }}",
+      ParserError::InvalidMediaQuery,
+    );
+    error_test(
+      "@media (width >= 2/1) { .foo { color: chartreuse }}",
+      ParserError::UnexpectedToken(Token::Delim('/')),
+    );
+    error_test(
+      "@media (600px <= min-height) { .foo { color: chartreuse }}",
+      ParserError::InvalidMediaQuery,
+    );
+    error_test(
+      "@media (scan >= 1) { .foo { color: chartreuse }}",
+      ParserError::InvalidMediaQuery,
+    );
+    error_test(
+      "@media (min-scan: interlace) { .foo { color: chartreuse }}",
+      ParserError::InvalidMediaQuery,
+    );
+    error_test(
+      "@media (1px <= width <= bar) { .foo { color: chartreuse }}",
+      ParserError::InvalidMediaQuery,
+    );
+    error_test(
+      "@media (1px <= min-width <= 2px) { .foo { color: chartreuse }}",
+      ParserError::InvalidMediaQuery,
+    );
+    error_test(
+      "@media (1px <= scan <= 2px) { .foo { color: chartreuse }}",
+      ParserError::InvalidMediaQuery,
+    );
+    error_test(
+      "@media (grid: 10) { .foo { color: chartreuse }}",
+      ParserError::InvalidMediaQuery,
+    );
+    error_test(
+      "@media (prefers-color-scheme = dark) { .foo { color: chartreuse }}",
+      ParserError::InvalidMediaQuery,
+    );
+  }
+
+  #[test]
+  fn test_merge_layers() {
+    test(
+      r#"
+      @layer foo {
+        .foo {
+          color: red;
+        }
+      }
+      @layer foo {
+        .foo {
+          background: #fff;
+        }
+
+        .baz {
+          color: #fff;
+        }
+      }
+      "#,
+      indoc! {r#"
+      @layer foo {
+        .foo {
+          color: red;
+          background: #fff;
+        }
+
+        .baz {
+          color: #fff;
+        }
+      }
+    "#},
+    );
+    test(
+      r#"
+      @layer a {}
+      @layer b {}
+
+      @layer b {
+        foo {
+          color: red;
+        }
+      }
+
+      @layer a {
+        bar {
+          color: yellow;
+        }
+      }
+      "#,
+      indoc! {r#"
+      @layer a {
+        bar {
+          color: #ff0;
+        }
+      }
+
+      @layer b {
+        foo {
+          color: red;
+        }
+      }
+    "#},
+    );
+
+    test(
+      r#"
+      @layer a {}
+      @layer b {}
+
+      @layer b {
+        foo {
+          color: red;
+        }
+      }
+      "#,
+      indoc! {r#"
+      @layer a;
+
+      @layer b {
+        foo {
+          color: red;
+        }
+      }
+    "#},
+    );
+
+    test(
+      r#"
+      @layer a;
+      @layer b;
+      @layer c;
+      "#,
+      indoc! {r#"
+      @layer a, b, c;
+    "#},
+    );
+
+    test(
+      r#"
+      @layer a {}
+      @layer b {}
+      @layer c {}
+      "#,
+      indoc! {r#"
+      @layer a, b, c;
+    "#},
+    );
+
+    test(
+      r#"
+      @layer a;
+      @layer b {
+        .foo {
+          color: red;
+        }
+      }
+      @layer c {}
+      "#,
+      indoc! {r#"
+      @layer a;
+
+      @layer b {
+        .foo {
+          color: red;
+        }
+      }
+
+      @layer c;
+    "#},
+    );
+
+    test(
+      r#"
+      @layer a, b;
+      @layer c {}
+
+      @layer d {
+        foo {
+          color: red;
+        }
+      }
+      "#,
+      indoc! {r#"
+      @layer a, b, c;
+
+      @layer d {
+        foo {
+          color: red;
+        }
+      }
+    "#},
+    );
+
+    test(
+      r#"
+      @layer a;
+      @layer b;
+      @import "a.css" layer(x);
+      @layer c;
+
+      @layer d {
+        foo {
+          color: red;
+        }
+      }
+      "#,
+      indoc! {r#"
+      @layer a, b;
+      @import "a.css" layer(x);
+      @layer c;
+
+      @layer d {
+        foo {
+          color: red;
+        }
+      }
+    "#},
+    );
+
+    test(
+      r#"
+      @layer a, b, c;
+
+      @layer a {
+        foo {
+          color: red;
+        }
+      }
+      "#,
+      indoc! {r#"
+      @layer a {
+        foo {
+          color: red;
+        }
+      }
+
+      @layer b, c;
+    "#},
+    );
+
+    test(
+      r#"
+      @layer a;
+      @import "foo.css";
+
+      @layer a {
+        foo {
+          color: red;
+        }
+      }
+      "#,
+      indoc! {r#"
+      @layer a;
+      @import "foo.css";
+
+      @layer a {
+        foo {
+          color: red;
+        }
+      }
+    "#},
+    );
+  }
+
+  #[test]
+  fn test_merge_rules() {
+    test(
+      r#"
+      .foo {
+        color: red;
+      }
+      .bar {
+        color: red;
+      }
+    "#,
+      indoc! {r#"
+      .foo, .bar {
+        color: red;
+      }
+    "#},
+    );
+    test(
+      r#"
+      .foo {
+        color: red;
+      }
+      .foo {
+        background: green;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        color: red;
+        background: green;
+      }
+    "#},
+    );
+    test(
+      r#"
+      .foo {
+        color: red;
+      }
+      .foo {
+        background: green !important;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        color: red;
+        background: green !important;
+      }
+    "#},
+    );
+    test(
+      r#"
+      .foo {
+        background: red;
+      }
+      .foo {
+        background: green;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        background: green;
+      }
+    "#},
+    );
+    test(
+      r#"
+      .foo {
+        --foo: red;
+        --foo: purple;
+      }
+      .foo {
+        --foo: green;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        --foo: green;
+      }
+    "#},
+    );
+    test(
+      r#"
+      .foo {
+        color: red;
+      }
+
+      .bar {
+        background: green;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        color: red;
+      }
+
+      .bar {
+        background: green;
+      }
+    "#},
+    );
+    test(
+      r#"
+      .foo {
+        color: red;
+      }
+
+      .baz {
+        color: blue;
+      }
+
+      .bar {
+        color: red;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        color: red;
+      }
+
+      .baz {
+        color: #00f;
+      }
+
+      .bar {
+        color: red;
+      }
+    "#},
+    );
+    test(
+      r#"
+      .foo {
+        background: red;
+      }
+      .bar {
+        background: red;
+      }
+      .foo {
+        color: green;
+      }
+      .bar {
+        color: green;
+      }
+    "#,
+      indoc! {r#"
+      .foo, .bar {
+        color: green;
+        background: red;
+      }
+    "#},
+    );
+    test(
+      r#"
+      .foo, .bar {
+        background: red;
+      }
+      .foo {
+        color: green;
+      }
+      .bar {
+        color: green;
+      }
+    "#,
+      indoc! {r#"
+      .foo, .bar {
+        color: green;
+        background: red;
+      }
+    "#},
+    );
+    test(
+      r#"
+      .foo {
+        background: red;
+      }
+      .foo {
+        color: green;
+      }
+      .bar {
+        background: red;
+      }
+      .bar {
+        color: green;
+      }
+    "#,
+      indoc! {r#"
+      .foo, .bar {
+        color: green;
+        background: red;
+      }
+    "#},
+    );
+    test(
+      r#"
+      [foo="bar"] {
+        color: red;
+      }
+      .bar {
+        color: red;
+      }
+    "#,
+      indoc! {r#"
+      [foo="bar"], .bar {
+        color: red;
+      }
+    "#},
+    );
+    test(
+      r#"
+      .a {
+        color: red;
+      }
+      .b {
+        color: green;
+      }
+      .a {
+        color: red;
+      }
+    "#,
+      indoc! {r#"
+      .b {
+        color: green;
+      }
+
+      .a {
+        color: red;
+      }
+    "#},
+    );
+    test(
+      r#"
+      .a {
+        color: red;
+      }
+      .b {
+        color: green;
+      }
+      .a {
+        color: pink;
+      }
+    "#,
+      indoc! {r#"
+      .b {
+        color: green;
+      }
+
+      .a {
+        color: pink;
+      }
+    "#},
+    );
+    test(
+      r#"
+      .a:foo(#000) {
+        color: red;
+      }
+      .b {
+        color: green;
+      }
+      .a:foo(#ff0) {
+        color: pink;
+      }
+    "#,
+      indoc! {r#"
+      .a:foo(#000) {
+        color: red;
+      }
+
+      .b {
+        color: green;
+      }
+
+      .a:foo(#ff0) {
+        color: pink;
+      }
+    "#},
+    );
+    test(
+      r#"
+      .a {
+        border-radius: 10px;
+      }
+      .b {
+        color: green;
+      }
+      .a {
+        border-radius: 10px;
+      }
+    "#,
+      indoc! {r#"
+      .b {
+        color: green;
+      }
+
+      .a {
+        border-radius: 10px;
+      }
+    "#},
+    );
+    test(
+      r#"
+      .a {
+        border-radius: 10px;
+      }
+      .b {
+        color: green;
+      }
+      .a {
+        -webkit-border-radius: 10px;
+      }
+    "#,
+      indoc! {r#"
+      .a {
+        border-radius: 10px;
+      }
+
+      .b {
+        color: green;
+      }
+
+      .a {
+        -webkit-border-radius: 10px;
+      }
+    "#},
+    );
+    test(
+      r#"
+      .a {
+        border-radius: 10px;
+      }
+      .b {
+        color: green;
+      }
+      .a {
+        border-radius: var(--foo);
+      }
+    "#,
+      indoc! {r#"
+      .b {
+        color: green;
+      }
+
+      .a {
+        border-radius: var(--foo);
+      }
+    "#},
+    );
+    test(
+      r#"
+      .a {
+        border-radius: 10px;
+      }
+      .b {
+        color: green;
+      }
+      .c {
+        border-radius: 20px;
+      }
+    "#,
+      indoc! {r#"
+      .a {
+        border-radius: 10px;
+      }
+
+      .b {
+        color: green;
+      }
+
+      .c {
+        border-radius: 20px;
+      }
+    "#},
+    );
+    test(
+      r#"
+      @media print {
+        .a {
+          color: red;
+        }
+        .b {
+          color: green;
+        }
+        .a {
+          color: red;
+        }
+      }
+    "#,
+      indoc! {r#"
+      @media print {
+        .b {
+          color: green;
+        }
+
+        .a {
+          color: red;
+        }
+      }
+    "#},
+    );
+    test(
+      r#"
+      .a {
+        border-radius: 10px;
+      }
+      .b {
+        color: green;
+      }
+      .a {
+        border-radius: 20px;
+        color: pink;
+      }
+    "#,
+      indoc! {r#"
+      .a {
+        border-radius: 10px;
+      }
+
+      .b {
+        color: green;
+      }
+
+      .a {
+        color: pink;
+        border-radius: 20px;
+      }
+    "#},
+    );
+    test(
+      r#"
+      .a {
+        color: red;
+      }
+      .b {
+        color: green;
+      }
+      .a {
+        color: red;
+      }
+      .b {
+        color: green;
+      }
+    "#,
+      indoc! {r#"
+      .a {
+        color: red;
+      }
+
+      .b {
+        color: green;
+      }
+    "#},
+    );
+
+    prefix_test(
+      r#"
+      [foo="bar"] {
+        color: red;
+      }
+      .bar {
+        color: red;
+      }
+    "#,
+      indoc! {r#"
+      [foo="bar"] {
+        color: red;
+      }
+
+      .bar {
+        color: red;
+      }
+    "#},
+      Browsers {
+        ie: Some(6 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      [foo="bar"] {
+        color: red;
+      }
+      .bar {
+        color: red;
+      }
+    "#,
+      indoc! {r#"
+      [foo="bar"], .bar {
+        color: red;
+      }
+    "#},
+      Browsers {
+        ie: Some(10 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    test(
+      r#"
+      .foo:-moz-read-only {
+        color: red;
+      }
+    "#,
+      indoc! {r#"
+      .foo:-moz-read-only {
+        color: red;
+      }
+    "#},
+    );
+
+    test(
+      r#"
+      .foo:-moz-read-only {
+        color: red;
+      }
+
+      .foo:read-only {
+        color: red;
+      }
+    "#,
+      indoc! {r#"
+      .foo:-moz-read-only {
+        color: red;
+      }
+
+      .foo:read-only {
+        color: red;
+      }
+    "#},
+    );
+
+    prefix_test(
+      r#"
+      .foo:-moz-read-only {
+        color: red;
+      }
+
+      .foo:read-only {
+        color: red;
+      }
+    "#,
+      indoc! {r#"
+      .foo:read-only {
+        color: red;
+      }
+    "#},
+      Browsers {
+        firefox: Some(85 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo:-moz-read-only {
+        color: red;
+      }
+
+      .bar {
+        color: yellow;
+      }
+
+      .foo:read-only {
+        color: red;
+      }
+    "#,
+      indoc! {r#"
+      .foo:-moz-read-only {
+        color: red;
+      }
+
+      .bar {
+        color: #ff0;
+      }
+
+      .foo:read-only {
+        color: red;
+      }
+    "#},
+      Browsers {
+        firefox: Some(85 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo:-moz-read-only {
+        color: red;
+      }
+
+      .foo:read-only {
+        color: red;
+      }
+    "#,
+      indoc! {r#"
+      .foo:-moz-read-only {
+        color: red;
+      }
+
+      .foo:read-only {
+        color: red;
+      }
+    "#},
+      Browsers {
+        firefox: Some(36 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo:read-only {
+        color: red;
+      }
+    "#,
+      indoc! {r#"
+      .foo:-moz-read-only {
+        color: red;
+      }
+
+      .foo:read-only {
+        color: red;
+      }
+    "#},
+      Browsers {
+        firefox: Some(36 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo:-webkit-full-screen {
+        color: red;
+      }
+      .foo:-moz-full-screen {
+        color: red;
+      }
+      .foo:-ms-fullscreen {
+        color: red;
+      }
+      .foo:fullscreen {
+        color: red;
+      }
+    "#,
+      indoc! {r#"
+      .foo:fullscreen {
+        color: red;
+      }
+    "#},
+      Browsers {
+        chrome: Some(96 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo:fullscreen {
+        color: red;
+      }
+    "#,
+      indoc! {r#"
+      .foo:-webkit-full-screen {
+        color: red;
+      }
+
+      .foo:-moz-full-screen {
+        color: red;
+      }
+
+      .foo:-ms-fullscreen {
+        color: red;
+      }
+
+      .foo:fullscreen {
+        color: red;
+      }
+    "#},
+      Browsers {
+        chrome: Some(45 << 16),
+        firefox: Some(45 << 16),
+        ie: Some(11 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo::placeholder {
+        color: red;
+      }
+    "#,
+      indoc! {r#"
+      .foo::-webkit-input-placeholder {
+        color: red;
+      }
+
+      .foo::-moz-placeholder {
+        color: red;
+      }
+
+      .foo::-ms-input-placeholder {
+        color: red;
+      }
+
+      .foo::placeholder {
+        color: red;
+      }
+    "#},
+      Browsers {
+        chrome: Some(45 << 16),
+        firefox: Some(45 << 16),
+        ie: Some(11 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo::file-selector-button {
+        color: red;
+      }
+    "#,
+      indoc! {r#"
+      .foo::-webkit-file-upload-button {
+        color: red;
+      }
+
+      .foo::-ms-browse {
+        color: red;
+      }
+
+      .foo::file-selector-button {
+        color: red;
+      }
+    "#},
+      Browsers {
+        chrome: Some(84 << 16),
+        ie: Some(10 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo::file-selector-button {
+        margin-inline-start: 2px;
+      }
+    "#,
+      indoc! {r#"
+      .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)))::-webkit-file-upload-button {
+        margin-left: 2px;
+      }
+
+      .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)))::-ms-browse {
+        margin-left: 2px;
+      }
+
+      .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)))::file-selector-button {
+        margin-left: 2px;
+      }
+
+      .foo:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))::-webkit-file-upload-button {
+        margin-right: 2px;
+      }
+
+      .foo:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))::-ms-browse {
+        margin-right: 2px;
+      }
+
+      .foo:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))::file-selector-button {
+        margin-right: 2px;
+      }
+    "#},
+      Browsers {
+        chrome: Some(84 << 16),
+        ie: Some(10 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo:placeholder-shown .bar { color: red; }
+      .foo:autofill .baz { color: red; }
+      "#,
+      indoc! {r#"
+      .foo:placeholder-shown .bar {
+        color: red;
+      }
+
+      .foo:-webkit-autofill .baz {
+        color: red;
+      }
+
+      .foo:autofill .baz {
+        color: red;
+      }
+      "#},
+      Browsers {
+        chrome: Some(103 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo:placeholder-shown .bar,.foo:autofill .baz{color:red}
+      "#,
+      indoc! {r#"
+      :-webkit-any(.foo:placeholder-shown .bar, .foo:-webkit-autofill .baz) {
+        color: red;
+      }
+
+      :is(.foo:placeholder-shown .bar, .foo:autofill .baz) {
+        color: red;
+      }
+      "#},
+      Browsers {
+        chrome: Some(103 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo:placeholder-shown .bar, .foo:-webkit-autofill .baz {
+        color: red;
+      }
+
+      .foo:placeholder-shown .bar, .foo:autofill .baz {
+        color: red;
+      }
+      "#,
+      indoc! {r#"
+      :-webkit-any(.foo:placeholder-shown .bar, .foo:-webkit-autofill .baz) {
+        color: red;
+      }
+
+      :is(.foo:placeholder-shown .bar, .foo:autofill .baz) {
+        color: red;
+      }
+      "#},
+      Browsers {
+        chrome: Some(103 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    test(
+      r#"
+      .foo:placeholder-shown .bar, .foo:-webkit-autofill .baz {
+        color: red;
+      }
+
+      .foo:placeholder-shown .bar, .foo:autofill .baz {
+        color: red;
+      }
+      "#,
+      indoc! {r#"
+      .foo:placeholder-shown .bar, .foo:-webkit-autofill .baz {
+        color: red;
+      }
+
+      .foo:placeholder-shown .bar, .foo:autofill .baz {
+        color: red;
+      }
+      "#},
+    );
+
+    prefix_test(
+      r#"
+      :hover, :focus-visible {
+        color: red;
+      }
+      "#,
+      indoc! {r#"
+      :hover {
+        color: red;
+      }
+
+      :focus-visible {
+        color: red;
+      }
+      "#},
+      Browsers {
+        safari: Some(13 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        color: red;
+      }
+
+      :hover, :focus-visible {
+        color: red;
+      }
+      "#,
+      indoc! {r#"
+      .foo, :hover {
+        color: red;
+      }
+
+      :focus-visible {
+        color: red;
+      }
+      "#},
+      Browsers {
+        safari: Some(13 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      :hover, :focus-visible {
+        margin-inline-start: 24px;
+      }
+      "#,
+      indoc! {r#"
+      :hover:not(:lang(ae, ar, arc, bcc, bqi, ckb, dv, fa, glk, he, ku, mzn, nqo, pnb, ps, sd, ug, ur, yi)) {
+        margin-left: 24px;
+      }
+
+      :hover:lang(ae, ar, arc, bcc, bqi, ckb, dv, fa, glk, he, ku, mzn, nqo, pnb, ps, sd, ug, ur, yi) {
+        margin-right: 24px;
+      }
+
+      :focus-visible:not(:lang(ae, ar, arc, bcc, bqi, ckb, dv, fa, glk, he, ku, mzn, nqo, pnb, ps, sd, ug, ur, yi)) {
+        margin-left: 24px;
+      }
+
+      :focus-visible:lang(ae, ar, arc, bcc, bqi, ckb, dv, fa, glk, he, ku, mzn, nqo, pnb, ps, sd, ug, ur, yi) {
+        margin-right: 24px;
+      }
+      "#},
+      Browsers {
+        safari: Some(11 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      :focus-within, :focus-visible {
+        color: red;
+      }
+      "#,
+      indoc! {r#"
+      :focus-within {
+        color: red;
+      }
+
+      :focus-visible {
+        color: red;
+      }
+      "#},
+      Browsers {
+        safari: Some(9 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      :hover, :focus-visible {
+        color: red;
+      }
+      "#,
+      indoc! {r#"
+      :is(:hover, :focus-visible) {
+        color: red;
+      }
+      "#},
+      Browsers {
+        safari: Some(14 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      a::after:hover, a::after:focus-visible {
+        color: red;
+      }
+      "#,
+      indoc! {r#"
+      a:after:hover {
+        color: red;
+      }
+
+      a:after:focus-visible {
+        color: red;
+      }
+      "#},
+      Browsers {
+        safari: Some(14 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      a:not(:hover), a:not(:focus-visible) {
+        color: red;
+      }
+      "#,
+      indoc! {r#"
+      :is(a:not(:hover), a:not(:focus-visible)) {
+        color: red;
+      }
+      "#},
+      Browsers {
+        safari: Some(14 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      a:has(:hover), a:has(:focus-visible) {
+        color: red;
+      }
+      "#,
+      indoc! {r#"
+      :is(a:has(:hover), a:has(:focus-visible)) {
+        color: red;
+      }
+      "#},
+      Browsers {
+        safari: Some(14 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo.foo:hover, .bar:focus-visible {
+        color: red;
+      }
+      "#,
+      indoc! {r#"
+      .foo.foo:hover {
+        color: red;
+      }
+
+      .bar:focus-visible {
+        color: red;
+      }
+      "#},
+      Browsers {
+        safari: Some(14 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      a::unknown-a, a::unknown-b {
+        color: red;
+      }
+      "#,
+      indoc! {r#"
+      a::unknown-a {
+        color: red;
+      }
+
+      a::unknown-b {
+        color: red;
+      }
+      "#},
+      Browsers {
+        safari: Some(14 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    nesting_test_with_targets(
+      r#"
+      .foo {
+        padding-inline-start: 3px;
+
+        .bar {
+          padding-inline-start: 5px;
+        }
+      }
+      "#,
+      indoc! {r#"
+      .foo:not(:lang(ae, ar, arc, bcc, bqi, ckb, dv, fa, glk, he, ku, mzn, nqo, pnb, ps, sd, ug, ur, yi)) {
+        padding-left: 3px;
+      }
+
+      .foo:lang(ae, ar, arc, bcc, bqi, ckb, dv, fa, glk, he, ku, mzn, nqo, pnb, ps, sd, ug, ur, yi) {
+        padding-right: 3px;
+      }
+
+      .foo .bar:not(:lang(ae, ar, arc, bcc, bqi, ckb, dv, fa, glk, he, ku, mzn, nqo, pnb, ps, sd, ug, ur, yi)) {
+        padding-left: 5px;
+      }
+
+      .foo .bar:lang(ae, ar, arc, bcc, bqi, ckb, dv, fa, glk, he, ku, mzn, nqo, pnb, ps, sd, ug, ur, yi) {
+        padding-right: 5px;
+      }
+      "#},
+      Browsers {
+        safari: Some(12 << 16),
+        ..Browsers::default()
+      }
+      .into(),
+    );
+
+    prefix_test(
+      r#"
+      .foo::part(header), .foo::part(body) {
+        display: none
+      }
+      "#,
+      indoc! {r#"
+      .foo::part(header), .foo::part(body) {
+        display: none;
+      }
+      "#},
+      Browsers {
+        safari: Some(14 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo :is(.bar) {
+        color: red;
+      }
+      "#,
+      indoc! {r#"
+        .foo .bar {
+          color: red;
+        }
+      "#},
+      Browsers {
+        chrome: Some(87 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo :is(.bar), .bar :is(.baz) {
+        color: red;
+      }
+      "#,
+      indoc! {r#"
+        .foo .bar, .bar .baz {
+          color: red;
+        }
+      "#},
+      Browsers {
+        chrome: Some(87 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo :is(.bar:focus-visible), .bar :is(.baz:hover) {
+        color: red;
+      }
+      "#,
+      indoc! {r#"
+        .bar .baz:hover {
+          color: red;
+        }
+
+        .foo .bar:focus-visible {
+          color: red;
+        }
+      "#},
+      Browsers {
+        chrome: Some(85 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      *,
+      ::before,
+      ::after,
+      ::backdrop {
+        padding: 5px;
+      }
+      "#,
+      indoc! {r#"
+        *, :before, :after {
+          padding: 5px;
+        }
+
+        ::-webkit-backdrop {
+          padding: 5px;
+        }
+
+        ::backdrop {
+          padding: 5px;
+        }
+      "#},
+      Browsers {
+        chrome: Some(33 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    test(
+      r#"
+      .foo:-webkit-any(.bar, .baz):after {
+        color: red;
+      }
+
+      .foo:is(.bar, .baz):after {
+        color: red;
+      }
+      "#,
+      indoc! {r#"
+        .foo:-webkit-any(.bar, .baz):after {
+          color: red;
+        }
+
+        .foo:is(.bar, .baz):after {
+          color: red;
+        }
+      "#},
+    );
+
+    prefix_test(
+      r#"
+      .foo:-webkit-any(.bar, .baz):after {
+        color: red;
+      }
+
+      .foo:is(.bar, .baz):after {
+        color: red;
+      }
+      "#,
+      indoc! {r#"
+        .foo:is(.bar, .baz):after {
+          color: red;
+        }
+      "#},
+      Browsers {
+        safari: Some(16 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo:-webkit-any(.bar):after {
+        color: red;
+      }
+
+      .foo:is(.bar, .baz):after {
+        color: red;
+      }
+      "#,
+      indoc! {r#"
+        .foo:-webkit-any(.bar):after {
+          color: red;
+        }
+
+        .foo:is(.bar, .baz):after {
+          color: red;
+        }
+      "#},
+      Browsers {
+        safari: Some(16 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo:-webkit-any(.bar, .baz):after {
+        color: red;
+      }
+
+      .foo:is(.bar, .baz):after {
+        color: red;
+      }
+      "#,
+      indoc! {r#"
+        .foo:-webkit-any(.bar, .baz):after {
+          color: red;
+        }
+
+        .foo:is(.bar, .baz):after {
+          color: red;
+        }
+      "#},
+      Browsers {
+        safari: Some(12 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo:-webkit-any(.bar, .baz):after {
+        color: red;
+      }
+
+      .foo:-moz-any(.bar, .baz):after {
+        color: red;
+      }
+      "#,
+      indoc! {r#"
+        .foo:-webkit-any(.bar, .baz):after {
+          color: red;
+        }
+
+        .foo:-moz-any(.bar, .baz):after {
+          color: red;
+        }
+      "#},
+      Browsers {
+        safari: Some(12 << 16),
+        firefox: Some(67 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .a {
+        padding-inline: var(--foo);
+      }
+
+      .a:-webkit-any(.b, .c) {
+        padding-inline: var(--foo);
+      }
+      "#,
+      indoc! {r#"
+        .a {
+          padding-inline: var(--foo);
+        }
+
+        .a:-webkit-any(.b, .c) {
+          padding-inline: var(--foo);
+        }
+      "#},
+      Browsers {
+        safari: Some(12 << 16),
+        ..Browsers::default()
+      },
+    );
+  }
+
+  #[test]
+  fn test_merge_media_rules() {
+    test(
+      r#"
+      @media (hover) {
+        .foo {
+          color: red;
+        }
+      }
+      @media (hover) {
+        .foo {
+          background: #fff;
+        }
+
+        .baz {
+          color: #fff;
+        }
+      }
+      "#,
+      indoc! {r#"
+      @media (hover) {
+        .foo {
+          color: red;
+          background: #fff;
+        }
+
+        .baz {
+          color: #fff;
+        }
+      }
+    "#},
+    );
+
+    test(
+      r#"
+      @media (hover) {
+        .foo {
+          color: red;
+        }
+      }
+      @media (min-width: 250px) {
+        .foo {
+          background: #fff;
+        }
+
+        .baz {
+          color: #fff;
+        }
+      }
+      "#,
+      indoc! {r#"
+      @media (hover) {
+        .foo {
+          color: red;
+        }
+      }
+
+      @media (width >= 250px) {
+        .foo {
+          background: #fff;
+        }
+
+        .baz {
+          color: #fff;
+        }
+      }
+    "#},
+    );
+  }
+
+  #[test]
+  fn test_merge_supports() {
+    test(
+      r#"
+      @supports (flex: 1) {
+        .foo {
+          color: red;
+        }
+      }
+      @supports (flex: 1) {
+        .foo {
+          background: #fff;
+        }
+
+        .baz {
+          color: #fff;
+        }
+      }
+      "#,
+      indoc! {r#"
+      @supports (flex: 1) {
+        .foo {
+          color: red;
+          background: #fff;
+        }
+
+        .baz {
+          color: #fff;
+        }
+      }
+    "#},
+    );
+
+    test(
+      r#"
+      @supports (flex: 1) {
+        .foo {
+          color: red;
+        }
+      }
+      @supports (display: grid) {
+        .foo {
+          background: #fff;
+        }
+
+        .baz {
+          color: #fff;
+        }
+      }
+      "#,
+      indoc! {r#"
+      @supports (flex: 1) {
+        .foo {
+          color: red;
+        }
+      }
+
+      @supports (display: grid) {
+        .foo {
+          background: #fff;
+        }
+
+        .baz {
+          color: #fff;
+        }
+      }
+    "#},
+    );
+  }
+
+  #[test]
+  fn test_opacity() {
+    minify_test(".foo { opacity: 0 }", ".foo{opacity:0}");
+    minify_test(".foo { opacity: 0% }", ".foo{opacity:0}");
+    minify_test(".foo { opacity: 0.5 }", ".foo{opacity:.5}");
+    minify_test(".foo { opacity: 50% }", ".foo{opacity:.5}");
+    minify_test(".foo { opacity: 1 }", ".foo{opacity:1}");
+    minify_test(".foo { opacity: 100% }", ".foo{opacity:1}");
+  }
+
+  #[test]
+  fn test_transitions() {
+    minify_test(".foo { transition-duration: 500ms }", ".foo{transition-duration:.5s}");
+    minify_test(".foo { transition-duration: .5s }", ".foo{transition-duration:.5s}");
+    minify_test(".foo { transition-duration: 99ms }", ".foo{transition-duration:99ms}");
+    minify_test(".foo { transition-duration: .099s }", ".foo{transition-duration:99ms}");
+    minify_test(".foo { transition-duration: 2000ms }", ".foo{transition-duration:2s}");
+    minify_test(".foo { transition-duration: 2s }", ".foo{transition-duration:2s}");
+    minify_test(
+      ".foo { transition-duration: calc(1s - 50ms) }",
+      ".foo{transition-duration:.95s}",
+    );
+    minify_test(
+      ".foo { transition-duration: calc(1s - 50ms + 2s) }",
+      ".foo{transition-duration:2.95s}",
+    );
+    minify_test(
+      ".foo { transition-duration: calc((1s - 50ms) * 2) }",
+      ".foo{transition-duration:1.9s}",
+    );
+    minify_test(
+      ".foo { transition-duration: calc(2 * (1s - 50ms)) }",
+      ".foo{transition-duration:1.9s}",
+    );
+    minify_test(
+      ".foo { transition-duration: calc((2s + 50ms) - (1s - 50ms)) }",
+      ".foo{transition-duration:1.1s}",
+    );
+    minify_test(
+      ".foo { transition-duration: 500ms, 50ms }",
+      ".foo{transition-duration:.5s,50ms}",
+    );
+    minify_test(".foo { transition-delay: 500ms }", ".foo{transition-delay:.5s}");
+    minify_test(
+      ".foo { transition-property: background }",
+      ".foo{transition-property:background}",
+    );
+    minify_test(
+      ".foo { transition-property: background, opacity }",
+      ".foo{transition-property:background,opacity}",
+    );
+    minify_test(
+      ".foo { transition-timing-function: linear }",
+      ".foo{transition-timing-function:linear}",
+    );
+    minify_test(
+      ".foo { transition-timing-function: ease }",
+      ".foo{transition-timing-function:ease}",
+    );
+    minify_test(
+      ".foo { transition-timing-function: ease-in }",
+      ".foo{transition-timing-function:ease-in}",
+    );
+    minify_test(
+      ".foo { transition-timing-function: ease-out }",
+      ".foo{transition-timing-function:ease-out}",
+    );
+    minify_test(
+      ".foo { transition-timing-function: ease-in-out }",
+      ".foo{transition-timing-function:ease-in-out}",
+    );
+    minify_test(
+      ".foo { transition-timing-function: cubic-bezier(0.25, 0.1, 0.25, 1) }",
+      ".foo{transition-timing-function:ease}",
+    );
+    minify_test(
+      ".foo { transition-timing-function: cubic-bezier(0.42, 0, 1, 1) }",
+      ".foo{transition-timing-function:ease-in}",
+    );
+    minify_test(
+      ".foo { transition-timing-function: cubic-bezier(0, 0, 0.58, 1) }",
+      ".foo{transition-timing-function:ease-out}",
+    );
+    minify_test(
+      ".foo { transition-timing-function: cubic-bezier(0.42, 0, 0.58, 1) }",
+      ".foo{transition-timing-function:ease-in-out}",
+    );
+    minify_test(
+      ".foo { transition-timing-function: cubic-bezier(0.58, 0.2, 0.11, 1.2) }",
+      ".foo{transition-timing-function:cubic-bezier(.58,.2,.11,1.2)}",
+    );
+    minify_test(
+      ".foo { transition-timing-function: step-start }",
+      ".foo{transition-timing-function:step-start}",
+    );
+    minify_test(
+      ".foo { transition-timing-function: step-end }",
+      ".foo{transition-timing-function:step-end}",
+    );
+    minify_test(
+      ".foo { transition-timing-function: steps(1, start) }",
+      ".foo{transition-timing-function:step-start}",
+    );
+    minify_test(
+      ".foo { transition-timing-function: steps(1, jump-start) }",
+      ".foo{transition-timing-function:step-start}",
+    );
+    minify_test(
+      ".foo { transition-timing-function: steps(1, end) }",
+      ".foo{transition-timing-function:step-end}",
+    );
+    minify_test(
+      ".foo { transition-timing-function: steps(1, jump-end) }",
+      ".foo{transition-timing-function:step-end}",
+    );
+    minify_test(
+      ".foo { transition-timing-function: steps(5, jump-start) }",
+      ".foo{transition-timing-function:steps(5,start)}",
+    );
+    minify_test(
+      ".foo { transition-timing-function: steps(5, jump-end) }",
+      ".foo{transition-timing-function:steps(5,end)}",
+    );
+    minify_test(
+      ".foo { transition-timing-function: steps(5, jump-both) }",
+      ".foo{transition-timing-function:steps(5,jump-both)}",
+    );
+    minify_test(
+      ".foo { transition-timing-function: ease-in-out, cubic-bezier(0.42, 0, 1, 1) }",
+      ".foo{transition-timing-function:ease-in-out,ease-in}",
+    );
+    minify_test(
+      ".foo { transition-timing-function: cubic-bezier(0.42, 0, 1, 1), cubic-bezier(0.58, 0.2, 0.11, 1.2) }",
+      ".foo{transition-timing-function:ease-in,cubic-bezier(.58,.2,.11,1.2)}",
+    );
+    minify_test(
+      ".foo { transition-timing-function: step-start, steps(5, jump-start) }",
+      ".foo{transition-timing-function:step-start,steps(5,start)}",
+    );
+    minify_test(".foo { transition: width 2s ease }", ".foo{transition:width 2s}");
+    minify_test(
+      ".foo { transition: width 2s ease, height 1000ms cubic-bezier(0.25, 0.1, 0.25, 1) }",
+      ".foo{transition:width 2s,height 1s}",
+    );
+    minify_test(".foo { transition: width 2s 1s }", ".foo{transition:width 2s 1s}");
+    minify_test(".foo { transition: width 2s ease 1s }", ".foo{transition:width 2s 1s}");
+    minify_test(
+      ".foo { transition: ease-in 1s width 4s }",
+      ".foo{transition:width 1s ease-in 4s}",
+    );
+    minify_test(".foo { transition: opacity 0s .6s }", ".foo{transition:opacity 0s .6s}");
+    test(
+      r#"
+      .foo {
+        transition-property: opacity;
+        transition-duration: 0.09s;
+        transition-timing-function: ease-in-out;
+        transition-delay: 500ms;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        transition: opacity 90ms ease-in-out .5s;
+      }
+    "#},
+    );
+    test(
+      r#"
+      .foo {
+        transition: opacity 2s;
+        transition-timing-function: ease;
+        transition-delay: 500ms;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        transition: opacity 2s .5s;
+      }
+    "#},
+    );
+    test(
+      r#"
+      .foo {
+        transition: opacity 500ms;
+        transition-timing-function: var(--ease);
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        transition: opacity .5s;
+        transition-timing-function: var(--ease);
+      }
+    "#},
+    );
+    test(
+      r#"
+      .foo {
+        transition-property: opacity;
+        transition-duration: 0.09s;
+        transition-timing-function: ease-in-out;
+        transition-delay: 500ms;
+        transition: color 2s;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        transition: color 2s;
+      }
+    "#},
+    );
+    test(
+      r#"
+      .foo {
+        transition-property: opacity, color;
+        transition-duration: 2s, 4s;
+        transition-timing-function: ease-in-out, ease-in;
+        transition-delay: 500ms, 0s;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        transition: opacity 2s ease-in-out .5s, color 4s ease-in;
+      }
+    "#},
+    );
+    test(
+      r#"
+      .foo {
+        transition-property: opacity, color;
+        transition-duration: 2s;
+        transition-timing-function: ease-in-out;
+        transition-delay: 500ms;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        transition: opacity 2s ease-in-out .5s, color 2s ease-in-out .5s;
+      }
+    "#},
+    );
+    test(
+      r#"
+      .foo {
+        transition-property: opacity, color, width, height;
+        transition-duration: 2s, 4s;
+        transition-timing-function: ease;
+        transition-delay: 0s;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        transition: opacity 2s, color 4s, width 2s, height 4s;
+      }
+    "#},
+    );
+
+    test(
+      r#"
+      .foo {
+        -webkit-transition-property: opacity, color;
+        -webkit-transition-duration: 2s, 4s;
+        -webkit-transition-timing-function: ease-in-out, ease-in;
+        -webkit-transition-delay: 500ms, 0s;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        -webkit-transition: opacity 2s ease-in-out .5s, color 4s ease-in;
+      }
+    "#},
+    );
+
+    test(
+      r#"
+      .foo {
+        -webkit-transition-property: opacity, color;
+        -webkit-transition-duration: 2s, 4s;
+        -webkit-transition-timing-function: ease-in-out, ease-in;
+        -webkit-transition-delay: 500ms, 0s;
+        -moz-transition-property: opacity, color;
+        -moz-transition-duration: 2s, 4s;
+        -moz-transition-timing-function: ease-in-out, ease-in;
+        -moz-transition-delay: 500ms, 0s;
+        transition-property: opacity, color;
+        transition-duration: 2s, 4s;
+        transition-timing-function: ease-in-out, ease-in;
+        transition-delay: 500ms, 0s;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        -webkit-transition: opacity 2s ease-in-out .5s, color 4s ease-in;
+        -moz-transition: opacity 2s ease-in-out .5s, color 4s ease-in;
+        transition: opacity 2s ease-in-out .5s, color 4s ease-in;
+      }
+    "#},
+    );
+
+    test(
+      r#"
+      .foo {
+        -webkit-transition-property: opacity, color;
+        -moz-transition-property: opacity, color;
+        transition-property: opacity, color;
+        -webkit-transition-duration: 2s, 4s;
+        -moz-transition-duration: 2s, 4s;
+        transition-duration: 2s, 4s;
+        -webkit-transition-timing-function: ease-in-out, ease-in;
+        transition-timing-function: ease-in-out, ease-in;
+        -moz-transition-timing-function: ease-in-out, ease-in;
+        -webkit-transition-delay: 500ms, 0s;
+        -moz-transition-delay: 500ms, 0s;
+        transition-delay: 500ms, 0s;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        -webkit-transition: opacity 2s ease-in-out .5s, color 4s ease-in;
+        -moz-transition: opacity 2s ease-in-out .5s, color 4s ease-in;
+        transition: opacity 2s ease-in-out .5s, color 4s ease-in;
+      }
+    "#},
+    );
+
+    test(
+      r#"
+      .foo {
+        -webkit-transition-property: opacity;
+        -moz-transition-property: color;
+        transition-property: opacity, color;
+        -webkit-transition-duration: 2s;
+        -moz-transition-duration: 4s;
+        transition-duration: 2s, 4s;
+        -webkit-transition-timing-function: ease-in-out;
+        -moz-transition-timing-function: ease-in-out;
+        transition-timing-function: ease-in-out, ease-in;
+        -webkit-transition-delay: 500ms;
+        -moz-transition-delay: 0s;
+        transition-delay: 500ms, 0s;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        -webkit-transition-property: opacity;
+        -moz-transition-property: color;
+        transition-property: opacity, color;
+        -webkit-transition-duration: 2s;
+        -moz-transition-duration: 4s;
+        transition-duration: 2s, 4s;
+        -webkit-transition-timing-function: ease-in-out;
+        -moz-transition-timing-function: ease-in-out;
+        -webkit-transition-delay: .5s;
+        transition-timing-function: ease-in-out, ease-in;
+        -moz-transition-delay: 0s;
+        transition-delay: .5s, 0s;
+      }
+    "#},
+    );
+
+    test(
+      r#"
+      .foo {
+        -webkit-transition-property: opacity;
+        transition-property: opacity, color;
+        -moz-transition-property: color;
+        -webkit-transition-duration: 2s;
+        transition-duration: 2s, 4s;
+        -moz-transition-duration: 4s;
+        -webkit-transition-timing-function: ease-in-out;
+        transition-timing-function: ease-in-out, ease-in;
+        -moz-transition-timing-function: ease-in-out;
+        -webkit-transition-delay: 500ms;
+        transition-delay: 500ms, 0s;
+        -moz-transition-delay: 0s;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        -webkit-transition-property: opacity;
+        transition-property: opacity, color;
+        -moz-transition-property: color;
+        -webkit-transition-duration: 2s;
+        transition-duration: 2s, 4s;
+        -moz-transition-duration: 4s;
+        -webkit-transition-timing-function: ease-in-out;
+        transition-timing-function: ease-in-out, ease-in;
+        -webkit-transition-delay: .5s;
+        -moz-transition-timing-function: ease-in-out;
+        transition-delay: .5s, 0s;
+        -moz-transition-delay: 0s;
+      }
+    "#},
+    );
+
+    test(
+      r#"
+      .foo {
+        transition: opacity 2s;
+        -webkit-transition-duration: 2s;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        transition: opacity 2s;
+        -webkit-transition-duration: 2s;
+      }
+    "#},
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        transition-property: margin-inline-start;
+      }
+    "#,
+      indoc! {r#"
+      .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) {
+        transition-property: margin-left;
+      }
+
+      .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) {
+        transition-property: margin-left;
+      }
+
+      .foo:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) {
+        transition-property: margin-right;
+      }
+
+      .foo:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) {
+        transition-property: margin-right;
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        transition-property: margin-inline-start, padding-inline-start;
+      }
+    "#,
+      indoc! {r#"
+      .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) {
+        transition-property: margin-left, padding-left;
+      }
+
+      .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) {
+        transition-property: margin-left, padding-left;
+      }
+
+      .foo:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) {
+        transition-property: margin-right, padding-right;
+      }
+
+      .foo:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) {
+        transition-property: margin-right, padding-right;
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        transition-property: margin-inline-start, opacity, padding-inline-start, color;
+      }
+    "#,
+      indoc! {r#"
+      .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) {
+        transition-property: margin-left, opacity, padding-left, color;
+      }
+
+      .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) {
+        transition-property: margin-left, opacity, padding-left, color;
+      }
+
+      .foo:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) {
+        transition-property: margin-right, opacity, padding-right, color;
+      }
+
+      .foo:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) {
+        transition-property: margin-right, opacity, padding-right, color;
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        transition-property: margin-block;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        transition-property: margin-top, margin-bottom;
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        transition: margin-inline-start 2s;
+      }
+    "#,
+      indoc! {r#"
+      .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) {
+        transition: margin-left 2s;
+      }
+
+      .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) {
+        transition: margin-left 2s;
+      }
+
+      .foo:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) {
+        transition: margin-right 2s;
+      }
+
+      .foo:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) {
+        transition: margin-right 2s;
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        transition: margin-inline-start 2s, padding-inline-start 2s;
+      }
+    "#,
+      indoc! {r#"
+      .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) {
+        transition: margin-left 2s, padding-left 2s;
+      }
+
+      .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) {
+        transition: margin-left 2s, padding-left 2s;
+      }
+
+      .foo:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) {
+        transition: margin-right 2s, padding-right 2s;
+      }
+
+      .foo:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) {
+        transition: margin-right 2s, padding-right 2s;
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        transition: margin-block-start 2s;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        transition: margin-top 2s;
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        transition: transform;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        -webkit-transition: -webkit-transform, transform;
+        transition: -webkit-transform, transform;
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(6 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        transition: border-start-start-radius;
+      }
+    "#,
+      indoc! {r#"
+      .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) {
+        -webkit-transition: -webkit-border-top-left-radius, border-top-left-radius;
+        transition: -webkit-border-top-left-radius, border-top-left-radius;
+      }
+
+      .foo:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) {
+        -webkit-transition: -webkit-border-top-right-radius, border-top-right-radius;
+        transition: -webkit-border-top-right-radius, border-top-right-radius;
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(4 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        transition: border-start-start-radius;
+      }
+    "#,
+      indoc! {r#"
+      .foo:not(:lang(ae, ar, arc, bcc, bqi, ckb, dv, fa, glk, he, ku, mzn, nqo, pnb, ps, sd, ug, ur, yi)) {
+        transition: border-top-left-radius;
+      }
+
+      .foo:lang(ae, ar, arc, bcc, bqi, ckb, dv, fa, glk, he, ku, mzn, nqo, pnb, ps, sd, ug, ur, yi) {
+        transition: border-top-right-radius;
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(12 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        -webkit-transition: background 200ms;
+        -moz-transition: background 200ms;
+        transition: background 230ms;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        -webkit-transition: background .2s;
+        -moz-transition: background .2s;
+        transition: background .23s;
+      }
+    "#},
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        -webkit-transition: background 200ms;
+        -moz-transition: background 200ms;
+        transition: background 230ms;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        -webkit-transition: background .2s;
+        -moz-transition: background .2s;
+        transition: background .23s;
+      }
+    "#},
+      Browsers {
+        chrome: Some(95 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+       .foo {
+         transition-property: -webkit-backdrop-filter, backdrop-filter;
+       }
+       .bar {
+         transition-property: backdrop-filter;
+       }
+       .baz {
+         transition-property: -webkit-backdrop-filter;
+       }
+     "#,
+      indoc! {r#"
+       .foo, .bar {
+         transition-property: -webkit-backdrop-filter, backdrop-filter;
+       }
+
+       .baz {
+         transition-property: -webkit-backdrop-filter;
+       }
+     "#
+      },
+      Browsers {
+        safari: Some(15 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+       .foo {
+         transition-property: -webkit-border-radius, -webkit-border-radius, -moz-border-radius;
+       }
+     "#,
+      indoc! {r#"
+       .foo {
+         transition-property: -webkit-border-radius, -moz-border-radius;
+       }
+     "#
+      },
+      Browsers {
+        safari: Some(15 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+       .foo {
+         transition: -webkit-backdrop-filter, backdrop-filter;
+       }
+       .bar {
+         transition: backdrop-filter;
+       }
+       .baz {
+         transition: -webkit-backdrop-filter;
+       }
+     "#,
+      indoc! {r#"
+       .foo, .bar {
+         transition: -webkit-backdrop-filter, backdrop-filter;
+       }
+
+       .baz {
+         transition: -webkit-backdrop-filter;
+       }
+     "#
+      },
+      Browsers {
+        safari: Some(15 << 16),
+        ..Browsers::default()
+      },
+    );
+  }
+
+  #[test]
+  fn test_animation() {
+    minify_test(".foo { animation-name: test }", ".foo{animation-name:test}");
+    minify_test(".foo { animation-name: \"test\" }", ".foo{animation-name:test}");
+    minify_test(".foo { animation-name: foo, bar }", ".foo{animation-name:foo,bar}");
+    minify_test(".foo { animation-name: \"none\" }", ".foo{animation-name:\"none\"}");
+    minify_test(
+      ".foo { animation-name: \"none\", foo }",
+      ".foo{animation-name:\"none\",foo}",
+    );
+    let name = crate::properties::animation::AnimationName::parse_string("default");
+    assert!(matches!(name, Err(..)));
+
+    minify_test(".foo { animation-name: none }", ".foo{animation-name:none}");
+    minify_test(".foo { animation-name: none, none }", ".foo{animation-name:none,none}");
+
+    // Test CSS-wide keywords
+    minify_test(".foo { animation-name: unset }", ".foo{animation-name:unset}");
+    minify_test(".foo { animation-name: \"unset\" }", ".foo{animation-name:\"unset\"}");
+    minify_test(".foo { animation-name: \"revert\" }", ".foo{animation-name:\"revert\"}");
+    minify_test(
+      ".foo { animation-name: \"unset\", \"revert\"}",
+      ".foo{animation-name:\"unset\",\"revert\"}",
+    );
+    minify_test(
+      ".foo { animation-name: foo, \"revert\"}",
+      ".foo{animation-name:foo,\"revert\"}",
+    );
+    minify_test(
+      ".foo { animation-name: \"string\", \"revert\"}",
+      ".foo{animation-name:string,\"revert\"}",
+    );
+    minify_test(
+      ".foo { animation-name: \"string\", foo, \"revert\"}",
+      ".foo{animation-name:string,foo,\"revert\"}",
+    );
+    minify_test(
+      ".foo { animation-name: \"default\" }",
+      ".foo{animation-name:\"default\"}",
+    );
+    minify_test(".foo { animation-duration: 100ms }", ".foo{animation-duration:.1s}");
+    minify_test(
+      ".foo { animation-duration: 100ms, 2000ms }",
+      ".foo{animation-duration:.1s,2s}",
+    );
+    minify_test(
+      ".foo { animation-timing-function: ease }",
+      ".foo{animation-timing-function:ease}",
+    );
+    minify_test(
+      ".foo { animation-timing-function: cubic-bezier(0.42, 0, 1, 1) }",
+      ".foo{animation-timing-function:ease-in}",
+    );
+    minify_test(
+      ".foo { animation-timing-function: ease, cubic-bezier(0.42, 0, 1, 1) }",
+      ".foo{animation-timing-function:ease,ease-in}",
+    );
+    minify_test(
+      ".foo { animation-iteration-count: 5 }",
+      ".foo{animation-iteration-count:5}",
+    );
+    minify_test(
+      ".foo { animation-iteration-count: 2.5 }",
+      ".foo{animation-iteration-count:2.5}",
+    );
+    minify_test(
+      ".foo { animation-iteration-count: 2.0 }",
+      ".foo{animation-iteration-count:2}",
+    );
+    minify_test(
+      ".foo { animation-iteration-count: infinite }",
+      ".foo{animation-iteration-count:infinite}",
+    );
+    minify_test(
+      ".foo { animation-iteration-count: 1, infinite }",
+      ".foo{animation-iteration-count:1,infinite}",
+    );
+    minify_test(
+      ".foo { animation-direction: reverse }",
+      ".foo{animation-direction:reverse}",
+    );
+    minify_test(
+      ".foo { animation-direction: alternate, reverse }",
+      ".foo{animation-direction:alternate,reverse}",
+    );
+    minify_test(
+      ".foo { animation-play-state: paused }",
+      ".foo{animation-play-state:paused}",
+    );
+    minify_test(
+      ".foo { animation-play-state: running, paused }",
+      ".foo{animation-play-state:running,paused}",
+    );
+    minify_test(".foo { animation-delay: 100ms }", ".foo{animation-delay:.1s}");
+    minify_test(
+      ".foo { animation-delay: 100ms, 2000ms }",
+      ".foo{animation-delay:.1s,2s}",
+    );
+    minify_test(
+      ".foo { animation-fill-mode: forwards }",
+      ".foo{animation-fill-mode:forwards}",
+    );
+    minify_test(
+      ".foo { animation-fill-mode: Backwards,forwards }",
+      ".foo{animation-fill-mode:backwards,forwards}",
+    );
+    minify_test(".foo { animation: none }", ".foo{animation:none}");
+    minify_test(".foo { animation: \"none\" }", ".foo{animation:\"none\"}");
+    minify_test(".foo { animation: \"None\" }", ".foo{animation:\"None\"}");
+    minify_test(".foo { animation: \"none\", none }", ".foo{animation:\"none\",none}");
+    minify_test(".foo { animation: none, none }", ".foo{animation:none,none}");
+    minify_test(".foo { animation: \"none\" none }", ".foo{animation:\"none\"}");
+    minify_test(".foo { animation: none none }", ".foo{animation:none}");
+
+    // Test animation-name + animation-fill-mode
+    minify_test(
+      ".foo { animation: 2s both \"none\"}",
+      ".foo{animation:2s both \"none\"}",
+    );
+    minify_test(
+      ".foo { animation: both \"none\" 2s}",
+      ".foo{animation:2s both \"none\"}",
+    );
+    minify_test(".foo { animation: \"none\" 2s none}", ".foo{animation:2s \"none\"}");
+    minify_test(".foo { animation: none \"none\" 2s}", ".foo{animation:2s \"none\"}");
+    minify_test(
+      ".foo { animation: none, \"none\" 2s forwards}",
+      ".foo{animation:none,2s forwards \"none\"}",
+    );
+
+    minify_test(".foo { animation: \"unset\" }", ".foo{animation:\"unset\"}");
+    minify_test(".foo { animation: \"string\" .5s }", ".foo{animation:.5s string}");
+    minify_test(".foo { animation: \"unset\" .5s }", ".foo{animation:.5s \"unset\"}");
+    minify_test(
+      ".foo { animation: none, \"unset\" .5s}",
+      ".foo{animation:none,.5s \"unset\"}",
+    );
+    minify_test(
+      ".foo { animation: \"unset\" 0s 3s infinite, none }",
+      ".foo{animation:0s 3s infinite \"unset\",none}",
+    );
+
+    minify_test(".foo { animation: \"infinite\" 2s 1 }", ".foo{animation:2s 1 infinite}");
+    minify_test(".foo { animation: \"paused\" 2s }", ".foo{animation:2s running paused}");
+    minify_test(
+      ".foo { animation: \"forwards\" 2s }",
+      ".foo{animation:2s none forwards}",
+    );
+    minify_test(
+      ".foo { animation: \"reverse\" 2s }",
+      ".foo{animation:2s normal reverse}",
+    );
+    minify_test(
+      ".foo { animation: \"reverse\" 2s alternate }",
+      ".foo{animation:2s alternate reverse}",
+    );
+
+    minify_test(
+      ".foo { animation: 3s ease-in 1s infinite reverse both running slidein }",
+      ".foo{animation:3s ease-in 1s infinite reverse both slidein}",
+    );
+    minify_test(
+      ".foo { animation: 3s slidein paused ease 1s 1 reverse both }",
+      ".foo{animation:3s 1s reverse both paused slidein}",
+    );
+    minify_test(".foo { animation: 3s ease ease }", ".foo{animation:3s ease ease}");
+    minify_test(
+      ".foo { animation: 3s cubic-bezier(0.25, 0.1, 0.25, 1) foo }",
+      ".foo{animation:3s foo}",
+    );
+    minify_test(
+      ".foo { animation: foo 0s 3s infinite }",
+      ".foo{animation:0s 3s infinite foo}",
+    );
+    minify_test(".foo { animation: foo 3s --test }", ".foo{animation:3s foo --test}");
+    minify_test(".foo { animation: foo 3s scroll() }", ".foo{animation:3s foo scroll()}");
+    minify_test(
+      ".foo { animation: foo 3s scroll(block) }",
+      ".foo{animation:3s foo scroll()}",
+    );
+    minify_test(
+      ".foo { animation: foo 3s scroll(root inline) }",
+      ".foo{animation:3s foo scroll(root inline)}",
+    );
+    minify_test(
+      ".foo { animation: foo 3s scroll(inline root) }",
+      ".foo{animation:3s foo scroll(root inline)}",
+    );
+    minify_test(
+      ".foo { animation: foo 3s scroll(inline nearest) }",
+      ".foo{animation:3s foo scroll(inline)}",
+    );
+    minify_test(
+      ".foo { animation: foo 3s view(block) }",
+      ".foo{animation:3s foo view()}",
+    );
+    minify_test(
+      ".foo { animation: foo 3s view(inline) }",
+      ".foo{animation:3s foo view(inline)}",
+    );
+    minify_test(
+      ".foo { animation: foo 3s view(inline 10px 10px) }",
+      ".foo{animation:3s foo view(inline 10px)}",
+    );
+    minify_test(
+      ".foo { animation: foo 3s view(inline 10px 12px) }",
+      ".foo{animation:3s foo view(inline 10px 12px)}",
+    );
+    minify_test(
+      ".foo { animation: foo 3s view(inline auto auto) }",
+      ".foo{animation:3s foo view(inline)}",
+    );
+    minify_test(".foo { animation: foo 3s auto }", ".foo{animation:3s foo}");
+    minify_test(".foo { animation-composition: add }", ".foo{animation-composition:add}");
+    test(
+      r#"
+      .foo {
+        animation-name: foo;
+        animation-duration: 0.09s;
+        animation-timing-function: ease-in-out;
+        animation-iteration-count: 2;
+        animation-direction: alternate;
+        animation-play-state: running;
+        animation-delay: 100ms;
+        animation-fill-mode: forwards;
+        animation-timeline: auto;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        animation: 90ms ease-in-out .1s 2 alternate forwards foo;
+      }
+    "#},
+    );
+    test(
+      r#"
+      .foo {
+        animation-name: foo, bar;
+        animation-duration: 0.09s, 200ms;
+        animation-timing-function: ease-in-out, ease;
+        animation-iteration-count: 2, 1;
+        animation-direction: alternate, normal;
+        animation-play-state: running, paused;
+        animation-delay: 100ms, 0s;
+        animation-fill-mode: forwards, none;
+        animation-timeline: auto, auto;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        animation: 90ms ease-in-out .1s 2 alternate forwards foo, .2s paused bar;
+      }
+    "#},
+    );
+    test(
+      r#"
+      .foo {
+        animation: bar 200ms;
+        animation-timing-function: ease-in-out;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        animation: .2s ease-in-out bar;
+      }
+    "#},
+    );
+    test(
+      r#"
+      .foo {
+        animation: bar 200ms;
+        animation-timing-function: var(--ease);
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        animation: .2s bar;
+        animation-timing-function: var(--ease);
+      }
+    "#},
+    );
+    test(
+      r#"
+      .foo {
+        animation-name: foo, bar;
+        animation-duration: 0.09s;
+        animation-timing-function: ease-in-out;
+        animation-iteration-count: 2;
+        animation-direction: alternate;
+        animation-play-state: running;
+        animation-delay: 100ms;
+        animation-fill-mode: forwards;
+        animation-timeline: auto;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        animation-name: foo, bar;
+        animation-duration: 90ms;
+        animation-timing-function: ease-in-out;
+        animation-iteration-count: 2;
+        animation-direction: alternate;
+        animation-play-state: running;
+        animation-delay: .1s;
+        animation-fill-mode: forwards;
+        animation-timeline: auto;
+      }
+    "#},
+    );
+    test(
+      r#"
+      .foo {
+        animation-name: foo;
+        animation-duration: 0.09s;
+        animation-timing-function: ease-in-out;
+        animation-iteration-count: 2;
+        animation-direction: alternate;
+        animation-play-state: running;
+        animation-delay: 100ms;
+        animation-fill-mode: forwards;
+        animation-timeline: scroll();
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        animation: 90ms ease-in-out .1s 2 alternate forwards foo scroll();
+      }
+    "#},
+    );
+    test(
+      r#"
+      .foo {
+        animation-name: foo;
+        animation-duration: 0.09s;
+        animation-timing-function: ease-in-out;
+        animation-iteration-count: 2;
+        animation-direction: alternate;
+        animation-play-state: running;
+        animation-delay: 100ms;
+        animation-fill-mode: forwards;
+        animation-timeline: scroll(), view();
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        animation-name: foo;
+        animation-duration: 90ms;
+        animation-timing-function: ease-in-out;
+        animation-iteration-count: 2;
+        animation-direction: alternate;
+        animation-play-state: running;
+        animation-delay: .1s;
+        animation-fill-mode: forwards;
+        animation-timeline: scroll(), view();
+      }
+    "#},
+    );
+    test(
+      r#"
+      .foo {
+        -webkit-animation-name: foo;
+        -webkit-animation-duration: 0.09s;
+        -webkit-animation-timing-function: ease-in-out;
+        -webkit-animation-iteration-count: 2;
+        -webkit-animation-direction: alternate;
+        -webkit-animation-play-state: running;
+        -webkit-animation-delay: 100ms;
+        -webkit-animation-fill-mode: forwards;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        -webkit-animation: 90ms ease-in-out .1s 2 alternate forwards foo;
+      }
+    "#},
+    );
+    test(
+      r#"
+      .foo {
+        -moz-animation: bar 200ms;
+        -moz-animation-timing-function: ease-in-out;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        -moz-animation: .2s ease-in-out bar;
+      }
+    "#},
+    );
+    test(
+      r#"
+      .foo {
+        -webkit-animation: bar 200ms;
+        -webkit-animation-timing-function: ease-in-out;
+        -moz-animation: bar 200ms;
+        -moz-animation-timing-function: ease-in-out;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        -webkit-animation: .2s ease-in-out bar;
+        -moz-animation: .2s ease-in-out bar;
+      }
+    "#},
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        animation: .2s ease-in-out bar;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        -webkit-animation: .2s ease-in-out bar;
+        -moz-animation: .2s ease-in-out bar;
+        animation: .2s ease-in-out bar;
+      }
+    "#},
+      Browsers {
+        firefox: Some(6 << 16),
+        safari: Some(6 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        -webkit-animation: .2s ease-in-out bar;
+        -moz-animation: .2s ease-in-out bar;
+        animation: .2s ease-in-out bar;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        animation: .2s ease-in-out bar;
+      }
+    "#},
+      Browsers {
+        firefox: Some(20 << 16),
+        safari: Some(14 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        animation: 200ms var(--ease) bar;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        -webkit-animation: .2s var(--ease) bar;
+        -moz-animation: .2s var(--ease) bar;
+        animation: .2s var(--ease) bar;
+      }
+    "#},
+      Browsers {
+        firefox: Some(6 << 16),
+        safari: Some(6 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        animation: .2s ease-in-out bar scroll();
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        animation: .2s ease-in-out bar;
+        animation-timeline: scroll();
+      }
+    "#},
+      Browsers {
+        safari: Some(16 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        animation: .2s ease-in-out bar scroll();
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        animation: .2s ease-in-out bar scroll();
+      }
+    "#},
+      Browsers {
+        chrome: Some(120 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        animation: .2s ease-in-out bar scroll();
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        -webkit-animation: .2s ease-in-out bar;
+        animation: .2s ease-in-out bar;
+        animation-timeline: scroll();
+      }
+    "#},
+      Browsers {
+        safari: Some(6 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    minify_test(
+      ".foo { animation-range-start: entry 10% }",
+      ".foo{animation-range-start:entry 10%}",
+    );
+    minify_test(
+      ".foo { animation-range-start: entry 0% }",
+      ".foo{animation-range-start:entry}",
+    );
+    minify_test(
+      ".foo { animation-range-start: entry }",
+      ".foo{animation-range-start:entry}",
+    );
+    minify_test(".foo { animation-range-start: 50% }", ".foo{animation-range-start:50%}");
+    minify_test(
+      ".foo { animation-range-end: exit 10% }",
+      ".foo{animation-range-end:exit 10%}",
+    );
+    minify_test(
+      ".foo { animation-range-end: exit 100% }",
+      ".foo{animation-range-end:exit}",
+    );
+    minify_test(".foo { animation-range-end: exit }", ".foo{animation-range-end:exit}");
+    minify_test(".foo { animation-range-end: 50% }", ".foo{animation-range-end:50%}");
+    minify_test(
+      ".foo { animation-range: entry 10% exit 90% }",
+      ".foo{animation-range:entry 10% exit 90%}",
+    );
+    minify_test(
+      ".foo { animation-range: entry 0% exit 100% }",
+      ".foo{animation-range:entry exit}",
+    );
+    minify_test(".foo { animation-range: entry }", ".foo{animation-range:entry}");
+    minify_test(
+      ".foo { animation-range: entry 0% entry 100% }",
+      ".foo{animation-range:entry}",
+    );
+    minify_test(".foo { animation-range: 50% normal }", ".foo{animation-range:50%}");
+    minify_test(
+      ".foo { animation-range: normal normal }",
+      ".foo{animation-range:normal}",
+    );
+    test(
+      r#"
+      .foo {
+        animation-range-start: entry 10%;
+        animation-range-end: exit 90%;
+      }
+      "#,
+      indoc! {r#"
+      .foo {
+        animation-range: entry 10% exit 90%;
+      }
+      "#},
+    );
+    test(
+      r#"
+      .foo {
+        animation-range-start: entry 0%;
+        animation-range-end: entry 100%;
+      }
+      "#,
+      indoc! {r#"
+      .foo {
+        animation-range: entry;
+      }
+      "#},
+    );
+    test(
+      r#"
+      .foo {
+        animation-range-start: entry 0%;
+        animation-range-end: exit 100%;
+      }
+      "#,
+      indoc! {r#"
+      .foo {
+        animation-range: entry exit;
+      }
+      "#},
+    );
+    test(
+      r#"
+      .foo {
+        animation-range-start: 10%;
+        animation-range-end: normal;
+      }
+      "#,
+      indoc! {r#"
+      .foo {
+        animation-range: 10%;
+      }
+      "#},
+    );
+    test(
+      r#"
+      .foo {
+        animation-range-start: 10%;
+        animation-range-end: 90%;
+      }
+      "#,
+      indoc! {r#"
+      .foo {
+        animation-range: 10% 90%;
+      }
+      "#},
+    );
+    test(
+      r#"
+      .foo {
+        animation-range-start: entry 10%;
+        animation-range-end: exit 100%;
+      }
+      "#,
+      indoc! {r#"
+      .foo {
+        animation-range: entry 10% exit;
+      }
+      "#},
+    );
+    test(
+      r#"
+      .foo {
+        animation-range-start: 10%;
+        animation-range-end: exit 90%;
+      }
+      "#,
+      indoc! {r#"
+      .foo {
+        animation-range: 10% exit 90%;
+      }
+      "#},
+    );
+    test(
+      r#"
+      .foo {
+        animation-range-start: entry 10%;
+        animation-range-end: 90%;
+      }
+      "#,
+      indoc! {r#"
+      .foo {
+        animation-range: entry 10% 90%;
+      }
+      "#},
+    );
+    test(
+      r#"
+      .foo {
+        animation-range: entry;
+        animation-range-end: 90%;
+      }
+      "#,
+      indoc! {r#"
+      .foo {
+        animation-range: entry 90%;
+      }
+      "#},
+    );
+    test(
+      r#"
+      .foo {
+        animation-range: entry;
+        animation-range-end: var(--end);
+      }
+      "#,
+      indoc! {r#"
+      .foo {
+        animation-range: entry;
+        animation-range-end: var(--end);
+      }
+      "#},
+    );
+    test(
+      r#"
+      .foo {
+        animation-range-start: entry 10%, entry 50%;
+        animation-range-end: exit 90%;
+      }
+      "#,
+      indoc! {r#"
+      .foo {
+        animation-range-start: entry 10%, entry 50%;
+        animation-range-end: exit 90%;
+      }
+      "#},
+    );
+    test(
+      r#"
+      .foo {
+        animation-range-start: entry 10%, entry 50%;
+        animation-range-end: exit 90%, exit 100%;
+      }
+      "#,
+      indoc! {r#"
+      .foo {
+        animation-range: entry 10% exit 90%, entry 50% exit;
+      }
+      "#},
+    );
+    test(
+      r#"
+      .foo {
+        animation-range: entry;
+        animation-range-end: 90%;
+        animation: spin 100ms;
+      }
+      "#,
+      indoc! {r#"
+      .foo {
+        animation: .1s spin;
+      }
+      "#},
+    );
+    test(
+      r#"
+      .foo {
+        animation: spin 100ms;
+        animation-range: entry;
+        animation-range-end: 90%;
+      }
+      "#,
+      indoc! {r#"
+      .foo {
+        animation: .1s spin;
+        animation-range: entry 90%;
+      }
+      "#},
+    );
+    test(
+      r#"
+      .foo {
+        animation-range: entry;
+        animation-range-end: 90%;
+        animation: var(--animation) 100ms;
+      }
+      "#,
+      indoc! {r#"
+      .foo {
+        animation: var(--animation) .1s;
+      }
+      "#},
+    );
+  }
+
+  #[test]
+  fn test_transform() {
+    minify_test(
+      ".foo { transform: translate(2px, 3px)",
+      ".foo{transform:translate(2px,3px)}",
+    );
+    minify_test(
+      ".foo { transform: translate(2px, 0px)",
+      ".foo{transform:translate(2px)}",
+    );
+    minify_test(
+      ".foo { transform: translate(0px, 2px)",
+      ".foo{transform:translateY(2px)}",
+    );
+    minify_test(".foo { transform: translateX(2px)", ".foo{transform:translate(2px)}");
+    minify_test(".foo { transform: translateY(2px)", ".foo{transform:translateY(2px)}");
+    minify_test(".foo { transform: translateZ(2px)", ".foo{transform:translateZ(2px)}");
+    minify_test(
+      ".foo { transform: translate3d(2px, 3px, 4px)",
+      ".foo{transform:translate3d(2px,3px,4px)}",
+    );
+    minify_test(
+      ".foo { transform: translate3d(10%, 20%, 4px)",
+      ".foo{transform:translate3d(10%,20%,4px)}",
+    );
+    minify_test(
+      ".foo { transform: translate3d(2px, 0px, 0px)",
+      ".foo{transform:translate(2px)}",
+    );
+    minify_test(
+      ".foo { transform: translate3d(0px, 2px, 0px)",
+      ".foo{transform:translateY(2px)}",
+    );
+    minify_test(
+      ".foo { transform: translate3d(0px, 0px, 2px)",
+      ".foo{transform:translateZ(2px)}",
+    );
+    minify_test(
+      ".foo { transform: translate3d(2px, 3px, 0px)",
+      ".foo{transform:translate(2px,3px)}",
+    );
+    minify_test(".foo { transform: scale(2, 3)", ".foo{transform:scale(2,3)}");
+    minify_test(".foo { transform: scale(10%, 20%)", ".foo{transform:scale(.1,.2)}");
+    minify_test(".foo { transform: scale(2, 2)", ".foo{transform:scale(2)}");
+    minify_test(".foo { transform: scale(2, 1)", ".foo{transform:scaleX(2)}");
+    minify_test(".foo { transform: scale(1, 2)", ".foo{transform:scaleY(2)}");
+    minify_test(".foo { transform: scaleX(2)", ".foo{transform:scaleX(2)}");
+    minify_test(".foo { transform: scaleY(2)", ".foo{transform:scaleY(2)}");
+    minify_test(".foo { transform: scaleZ(2)", ".foo{transform:scaleZ(2)}");
+    minify_test(".foo { transform: scale3d(2, 3, 4)", ".foo{transform:scale3d(2,3,4)}");
+    minify_test(".foo { transform: scale3d(2, 1, 1)", ".foo{transform:scaleX(2)}");
+    minify_test(".foo { transform: scale3d(1, 2, 1)", ".foo{transform:scaleY(2)}");
+    minify_test(".foo { transform: scale3d(1, 1, 2)", ".foo{transform:scaleZ(2)}");
+    minify_test(".foo { transform: scale3d(2, 2, 1)", ".foo{transform:scale(2)}");
+    minify_test(".foo { transform: rotate(20deg)", ".foo{transform:rotate(20deg)}");
+    minify_test(".foo { transform: rotateX(20deg)", ".foo{transform:rotateX(20deg)}");
+    minify_test(".foo { transform: rotateY(20deg)", ".foo{transform:rotateY(20deg)}");
+    minify_test(".foo { transform: rotateZ(20deg)", ".foo{transform:rotate(20deg)}");
+    minify_test(".foo { transform: rotate(360deg)", ".foo{transform:rotate(360deg)}");
+    minify_test(
+      ".foo { transform: rotate3d(2, 3, 4, 20deg)",
+      ".foo{transform:rotate3d(2,3,4,20deg)}",
+    );
+    minify_test(
+      ".foo { transform: rotate3d(1, 0, 0, 20deg)",
+      ".foo{transform:rotateX(20deg)}",
+    );
+    minify_test(
+      ".foo { transform: rotate3d(0, 1, 0, 20deg)",
+      ".foo{transform:rotateY(20deg)}",
+    );
+    minify_test(
+      ".foo { transform: rotate3d(0, 0, 1, 20deg)",
+      ".foo{transform:rotate(20deg)}",
+    );
+    minify_test(".foo { transform: rotate(405deg)}", ".foo{transform:rotate(405deg)}");
+    minify_test(".foo { transform: rotateX(405deg)}", ".foo{transform:rotateX(405deg)}");
+    minify_test(".foo { transform: rotateY(405deg)}", ".foo{transform:rotateY(405deg)}");
+    minify_test(".foo { transform: rotate(-200deg)}", ".foo{transform:rotate(-200deg)}");
+    minify_test(".foo { transform: rotate(0)", ".foo{transform:rotate(0)}");
+    minify_test(".foo { transform: rotate(0deg)", ".foo{transform:rotate(0)}");
+    minify_test(
+      ".foo { transform: rotateX(-200deg)}",
+      ".foo{transform:rotateX(-200deg)}",
+    );
+    minify_test(
+      ".foo { transform: rotateY(-200deg)}",
+      ".foo{transform:rotateY(-200deg)}",
+    );
+    minify_test(
+      ".foo { transform: rotate3d(1, 1, 0, -200deg)",
+      ".foo{transform:rotate3d(1,1,0,-200deg)}",
+    );
+    minify_test(".foo { transform: skew(20deg)", ".foo{transform:skew(20deg)}");
+    minify_test(".foo { transform: skew(20deg, 0deg)", ".foo{transform:skew(20deg)}");
+    minify_test(".foo { transform: skew(0deg, 20deg)", ".foo{transform:skewY(20deg)}");
+    minify_test(".foo { transform: skewX(20deg)", ".foo{transform:skew(20deg)}");
+    minify_test(".foo { transform: skewY(20deg)", ".foo{transform:skewY(20deg)}");
+    minify_test(
+      ".foo { transform: perspective(10px)",
+      ".foo{transform:perspective(10px)}",
+    );
+    minify_test(
+      ".foo { transform: matrix(1, 2, -1, 1, 80, 80)",
+      ".foo{transform:matrix(1,2,-1,1,80,80)}",
+    );
+    minify_test(
+      ".foo { transform: matrix3d(1, 0, 0, 0, 0, 1, 6, 0, 0, 0, 1, 0, 50, 100, 0, 1.1)",
+      ".foo{transform:matrix3d(1,0,0,0,0,1,6,0,0,0,1,0,50,100,0,1.1)}",
+    );
+    // TODO: Re-enable with a better solution
+    //       See: https://github.com/parcel-bundler/lightningcss/issues/288
+    // minify_test(
+    //   ".foo{transform:translate(100px,200px) rotate(45deg) skew(10deg) scale(2)}",
+    //   ".foo{transform:matrix(1.41421,1.41421,-1.16485,1.66358,100,200)}",
+    // );
+    // minify_test(
+    //   ".foo{transform:translate(200px,300px) translate(100px,200px) scale(2)}",
+    //   ".foo{transform:matrix(2,0,0,2,300,500)}",
+    // );
+    minify_test(
+      ".foo{transform:translate(100px,200px) rotate(45deg)}",
+      ".foo{transform:translate(100px,200px)rotate(45deg)}",
+    );
+    minify_test(
+      ".foo{transform:rotate3d(1, 1, 1, 45deg) translate3d(100px, 100px, 10px)}",
+      ".foo{transform:rotate3d(1,1,1,45deg)translate3d(100px,100px,10px)}",
+    );
+    // TODO: Re-enable with a better solution
+    //       See: https://github.com/parcel-bundler/lightningcss/issues/288
+    // minify_test(
+    //   ".foo{transform:translate3d(100px, 100px, 10px) skew(10deg) scale3d(2, 3, 4)}",
+    //   ".foo{transform:matrix3d(2,0,0,0,.528981,3,0,0,0,0,4,0,100,100,10,1)}",
+    // );
+    // minify_test(
+    //   ".foo{transform:matrix3d(0.804737854124365, 0.5058793634016805, -0.31061721752604554, 0, -0.31061721752604554, 0.804737854124365, 0.5058793634016805, 0, 0.5058793634016805, -0.31061721752604554, 0.804737854124365, 0, 100, 100, 10, 1)}",
+    //   ".foo{transform:translate3d(100px,100px,10px)rotate3d(1,1,1,45deg)}"
+    // );
+    // minify_test(
+    //   ".foo{transform:matrix3d(1, 0, 0, 0, 0, 0.7071067811865476, 0.7071067811865475, 0, 0, -0.7071067811865475, 0.7071067811865476, 0, 100, 100, 10, 1)}",
+    //   ".foo{transform:translate3d(100px,100px,10px)rotateX(45deg)}"
+    // );
+    // minify_test(
+    //   ".foo{transform:translate3d(100px, 200px, 10px) translate(100px, 100px)}",
+    //   ".foo{transform:translate3d(200px,300px,10px)}",
+    // );
+    // minify_test(
+    //   ".foo{transform:rotate(45deg) rotate(45deg)}",
+    //   ".foo{transform:rotate(90deg)}",
+    // );
+    // minify_test(
+    //   ".foo{transform:matrix(0.7071067811865476, 0.7071067811865475, -0.7071067811865475, 0.7071067811865476, 100, 100)}",
+    //   ".foo{transform:translate(100px,100px)rotate(45deg)}"
+    // );
+    // minify_test(
+    //   ".foo{transform:translateX(2in) translateX(50px)}",
+    //   ".foo{transform:translate(242px)}",
+    // );
+    minify_test(
+      ".foo{transform:translateX(calc(2in + 50px))}",
+      ".foo{transform:translate(242px)}",
+    );
+    minify_test(".foo{transform:translateX(50%)}", ".foo{transform:translate(50%)}");
+    minify_test(
+      ".foo{transform:translateX(calc(50% - 100px + 20px))}",
+      ".foo{transform:translate(calc(50% - 80px))}",
+    );
+    minify_test(
+      ".foo{transform:rotate(calc(10deg + 20deg))}",
+      ".foo{transform:rotate(30deg)}",
+    );
+    minify_test(
+      ".foo{transform:rotate(calc(10deg + 0.349066rad))}",
+      ".foo{transform:rotate(30deg)}",
+    );
+    minify_test(
+      ".foo{transform:rotate(calc(10deg + 1.5turn))}",
+      ".foo{transform:rotate(550deg)}",
+    );
+    minify_test(
+      ".foo{transform:rotate(calc(10deg * 2))}",
+      ".foo{transform:rotate(20deg)}",
+    );
+    minify_test(
+      ".foo{transform:rotate(calc(-10deg * 2))}",
+      ".foo{transform:rotate(-20deg)}",
+    );
+    minify_test(
+      ".foo{transform:rotate(calc(10deg + var(--test)))}",
+      ".foo{transform:rotate(calc(10deg + var(--test)))}",
+    );
+    minify_test(".foo { transform: scale(calc(10% + 20%))", ".foo{transform:scale(.3)}");
+    minify_test(".foo { transform: scale(calc(.1 + .2))", ".foo{transform:scale(.3)}");
+
+    minify_test(
+      ".foo { -webkit-transform: scale(calc(10% + 20%))",
+      ".foo{-webkit-transform:scale(.3)}",
+    );
+
+    minify_test(".foo { translate: 1px 2px 3px }", ".foo{translate:1px 2px 3px}");
+    minify_test(".foo { translate: 1px 0px 0px }", ".foo{translate:1px}");
+    minify_test(".foo { translate: 1px 2px 0px }", ".foo{translate:1px 2px}");
+    minify_test(".foo { translate: 1px 0px 2px }", ".foo{translate:1px 0 2px}");
+    minify_test(".foo { translate: none }", ".foo{translate:none}");
+    minify_test(".foo { rotate: 10deg }", ".foo{rotate:10deg}");
+    minify_test(".foo { rotate: z 10deg }", ".foo{rotate:10deg}");
+    minify_test(".foo { rotate: 0 0 1 10deg }", ".foo{rotate:10deg}");
+    minify_test(".foo { rotate: x 10deg }", ".foo{rotate:x 10deg}");
+    minify_test(".foo { rotate: 1 0 0 10deg }", ".foo{rotate:x 10deg}");
+    minify_test(".foo { rotate: y 10deg }", ".foo{rotate:y 10deg}");
+    minify_test(".foo { rotate: 0 1 0 10deg }", ".foo{rotate:y 10deg}");
+    minify_test(".foo { rotate: 1 1 1 10deg }", ".foo{rotate:1 1 1 10deg}");
+    minify_test(".foo { rotate: 0 0 1 0deg }", ".foo{rotate:none}");
+    minify_test(".foo { rotate: none }", ".foo{rotate:none}");
+    minify_test(".foo { scale: 1 }", ".foo{scale:1}");
+    minify_test(".foo { scale: 1 1 }", ".foo{scale:1}");
+    minify_test(".foo { scale: 1 1 1 }", ".foo{scale:1}");
+    minify_test(".foo { scale: none }", ".foo{scale:none}");
+    minify_test(".foo { scale: 1 0 }", ".foo{scale:1 0}");
+    minify_test(".foo { scale: 1 0 1 }", ".foo{scale:1 0}");
+    minify_test(".foo { scale: 1 0 0 }", ".foo{scale:1 0 0}");
+
+    // TODO: Re-enable with a better solution
+    //       See: https://github.com/parcel-bundler/lightningcss/issues/288
+    // minify_test(".foo { transform: scale(3); scale: 0.5 }", ".foo{transform:scale(1.5)}");
+    minify_test(".foo { scale: 0.5; transform: scale(3); }", ".foo{transform:scale(3)}");
+
+    prefix_test(
+      r#"
+      .foo {
+        transform: scale(0.5);
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        -webkit-transform: scale(.5);
+        -moz-transform: scale(.5);
+        transform: scale(.5);
+      }
+    "#},
+      Browsers {
+        firefox: Some(6 << 16),
+        safari: Some(6 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        transform: var(--transform);
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        -webkit-transform: var(--transform);
+        -moz-transform: var(--transform);
+        transform: var(--transform);
+      }
+    "#},
+      Browsers {
+        firefox: Some(6 << 16),
+        safari: Some(6 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        transform: translateX(-50%);
+        transform: translateX(20px);
+      }
+      "#,
+      indoc! {r#"
+      .foo {
+        transform: translateX(20px);
+      }
+      "#},
+    );
+  }
+
+  #[test]
+  pub fn test_gradients() {
+    minify_test(
+      ".foo { background: linear-gradient(yellow, blue) }",
+      ".foo{background:linear-gradient(#ff0,#00f)}",
+    );
+    minify_test(
+      ".foo { background: linear-gradient(to bottom, yellow, blue); }",
+      ".foo{background:linear-gradient(#ff0,#00f)}",
+    );
+    minify_test(
+      ".foo { background: linear-gradient(180deg, yellow, blue); }",
+      ".foo{background:linear-gradient(#ff0,#00f)}",
+    );
+    minify_test(
+      ".foo { background: linear-gradient(0.5turn, yellow, blue); }",
+      ".foo{background:linear-gradient(#ff0,#00f)}",
+    );
+    minify_test(
+      ".foo { background: linear-gradient(yellow 10%, blue 20%) }",
+      ".foo{background:linear-gradient(#ff0 10%,#00f 20%)}",
+    );
+    minify_test(
+      ".foo { background: linear-gradient(to top, blue, yellow); }",
+      ".foo{background:linear-gradient(#ff0,#00f)}",
+    );
+    minify_test(
+      ".foo { background: linear-gradient(to top, blue 10%, yellow 20%); }",
+      ".foo{background:linear-gradient(#ff0 80%,#00f 90%)}",
+    );
+    minify_test(
+      ".foo { background: linear-gradient(to top, blue 10px, yellow 20px); }",
+      ".foo{background:linear-gradient(0deg,#00f 10px,#ff0 20px)}",
+    );
+    minify_test(
+      ".foo { background: linear-gradient(135deg, yellow, blue); }",
+      ".foo{background:linear-gradient(135deg,#ff0,#00f)}",
+    );
+    minify_test(
+      ".foo { background: linear-gradient(yellow, blue 20%, #0f0); }",
+      ".foo{background:linear-gradient(#ff0,#00f 20%,#0f0)}",
+    );
+    minify_test(
+      ".foo { background: linear-gradient(to top right, red, white, blue) }",
+      ".foo{background:linear-gradient(to top right,red,#fff,#00f)}",
+    );
+    minify_test(
+      ".foo { background: linear-gradient(yellow, blue calc(10% * 2), #0f0); }",
+      ".foo{background:linear-gradient(#ff0,#00f 20%,#0f0)}",
+    );
+    minify_test(
+      ".foo { background: linear-gradient(yellow, 20%, blue); }",
+      ".foo{background:linear-gradient(#ff0,20%,#00f)}",
+    );
+    minify_test(
+      ".foo { background: linear-gradient(yellow, 50%, blue); }",
+      ".foo{background:linear-gradient(#ff0,#00f)}",
+    );
+    minify_test(
+      ".foo { background: linear-gradient(yellow, 20px, blue); }",
+      ".foo{background:linear-gradient(#ff0,20px,#00f)}",
+    );
+    minify_test(
+      ".foo { background: linear-gradient(yellow, 50px, blue); }",
+      ".foo{background:linear-gradient(#ff0,50px,#00f)}",
+    );
+    minify_test(
+      ".foo { background: linear-gradient(yellow, 50px, blue); }",
+      ".foo{background:linear-gradient(#ff0,50px,#00f)}",
+    );
+    minify_test(
+      ".foo { background: linear-gradient(yellow, red 30% 40%, blue); }",
+      ".foo{background:linear-gradient(#ff0,red 30% 40%,#00f)}",
+    );
+    minify_test(
+      ".foo { background: linear-gradient(yellow, red 30%, red 40%, blue); }",
+      ".foo{background:linear-gradient(#ff0,red 30% 40%,#00f)}",
+    );
+    minify_test(
+      ".foo { background: linear-gradient(0, yellow, blue); }",
+      ".foo{background:linear-gradient(#00f,#ff0)}",
+    );
+    minify_test(
+      ".foo { background: -webkit-linear-gradient(yellow, blue) }",
+      ".foo{background:-webkit-linear-gradient(#ff0,#00f)}",
+    );
+    minify_test(
+      ".foo { background: -webkit-linear-gradient(bottom, yellow, blue); }",
+      ".foo{background:-webkit-linear-gradient(#ff0,#00f)}",
+    );
+    minify_test(
+      ".foo { background: -webkit-linear-gradient(top right, red, white, blue) }",
+      ".foo{background:-webkit-linear-gradient(top right,red,#fff,#00f)}",
+    );
+    minify_test(
+      ".foo { background: -moz-linear-gradient(yellow, blue) }",
+      ".foo{background:-moz-linear-gradient(#ff0,#00f)}",
+    );
+    minify_test(
+      ".foo { background: -moz-linear-gradient(bottom, yellow, blue); }",
+      ".foo{background:-moz-linear-gradient(#ff0,#00f)}",
+    );
+    minify_test(
+      ".foo { background: -moz-linear-gradient(top right, red, white, blue) }",
+      ".foo{background:-moz-linear-gradient(top right,red,#fff,#00f)}",
+    );
+    minify_test(
+      ".foo { background: -o-linear-gradient(yellow, blue) }",
+      ".foo{background:-o-linear-gradient(#ff0,#00f)}",
+    );
+    minify_test(
+      ".foo { background: -o-linear-gradient(bottom, yellow, blue); }",
+      ".foo{background:-o-linear-gradient(#ff0,#00f)}",
+    );
+    minify_test(
+      ".foo { background: -o-linear-gradient(top right, red, white, blue) }",
+      ".foo{background:-o-linear-gradient(top right,red,#fff,#00f)}",
+    );
+    minify_test(
+      ".foo { background: -webkit-gradient(linear, left top, left bottom, from(blue), to(yellow)) }",
+      ".foo{background:-webkit-gradient(linear,0 0,0 100%,from(#00f),to(#ff0))}",
+    );
+    minify_test(
+      ".foo { background: -webkit-gradient(linear, left top, left bottom, from(blue), color-stop(50%, red), to(yellow)) }",
+      ".foo{background:-webkit-gradient(linear,0 0,0 100%,from(#00f),color-stop(.5,red),to(#ff0))}"
+    );
+    minify_test(
+      ".foo { background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, blue), color-stop(50%, red), color-stop(100%, yellow)) }",
+      ".foo{background:-webkit-gradient(linear,0 0,0 100%,from(#00f),color-stop(.5,red),to(#ff0))}"
+    );
+    minify_test(
+      ".foo { background: repeating-linear-gradient(yellow 10px, blue 50px) }",
+      ".foo{background:repeating-linear-gradient(#ff0 10px,#00f 50px)}",
+    );
+    minify_test(
+      ".foo { background: -webkit-repeating-linear-gradient(yellow 10px, blue 50px) }",
+      ".foo{background:-webkit-repeating-linear-gradient(#ff0 10px,#00f 50px)}",
+    );
+    minify_test(
+      ".foo { background: -moz-repeating-linear-gradient(yellow 10px, blue 50px) }",
+      ".foo{background:-moz-repeating-linear-gradient(#ff0 10px,#00f 50px)}",
+    );
+    minify_test(
+      ".foo { background: -o-repeating-linear-gradient(yellow 10px, blue 50px) }",
+      ".foo{background:-o-repeating-linear-gradient(#ff0 10px,#00f 50px)}",
+    );
+    minify_test(
+      ".foo { background: radial-gradient(yellow, blue) }",
+      ".foo{background:radial-gradient(#ff0,#00f)}",
+    );
+    minify_test(
+      ".foo { background: radial-gradient(at top left, yellow, blue) }",
+      ".foo{background:radial-gradient(at 0 0,#ff0,#00f)}",
+    );
+    minify_test(
+      ".foo { background: radial-gradient(5em circle at top left, yellow, blue) }",
+      ".foo{background:radial-gradient(5em at 0 0,#ff0,#00f)}",
+    );
+    minify_test(
+      ".foo { background: radial-gradient(circle at 100%, #333, #333 50%, #eee 75%, #333 75%) }",
+      ".foo{background:radial-gradient(circle at 100%,#333,#333 50%,#eee 75%,#333 75%)}",
+    );
+    minify_test(
+      ".foo { background: radial-gradient(farthest-corner circle at 100% 50%, #333, #333 50%, #eee 75%, #333 75%) }",
+      ".foo{background:radial-gradient(circle at 100%,#333,#333 50%,#eee 75%,#333 75%)}"
+    );
+    minify_test(
+      ".foo { background: radial-gradient(farthest-corner circle at 50% 50%, #333, #333 50%, #eee 75%, #333 75%) }",
+      ".foo{background:radial-gradient(circle,#333,#333 50%,#eee 75%,#333 75%)}"
+    );
+    minify_test(
+      ".foo { background: radial-gradient(ellipse at top, #e66465, transparent) }",
+      ".foo{background:radial-gradient(at top,#e66465,#0000)}",
+    );
+    minify_test(
+      ".foo { background: radial-gradient(20px, yellow, blue) }",
+      ".foo{background:radial-gradient(20px,#ff0,#00f)}",
+    );
+    minify_test(
+      ".foo { background: radial-gradient(circle 20px, yellow, blue) }",
+      ".foo{background:radial-gradient(20px,#ff0,#00f)}",
+    );
+    minify_test(
+      ".foo { background: radial-gradient(20px 40px, yellow, blue) }",
+      ".foo{background:radial-gradient(20px 40px,#ff0,#00f)}",
+    );
+    minify_test(
+      ".foo { background: radial-gradient(ellipse 20px 40px, yellow, blue) }",
+      ".foo{background:radial-gradient(20px 40px,#ff0,#00f)}",
+    );
+    minify_test(
+      ".foo { background: radial-gradient(ellipse calc(20px + 10px) 40px, yellow, blue) }",
+      ".foo{background:radial-gradient(30px 40px,#ff0,#00f)}",
+    );
+    minify_test(
+      ".foo { background: radial-gradient(circle farthest-side, yellow, blue) }",
+      ".foo{background:radial-gradient(circle farthest-side,#ff0,#00f)}",
+    );
+    minify_test(
+      ".foo { background: radial-gradient(farthest-side circle, yellow, blue) }",
+      ".foo{background:radial-gradient(circle farthest-side,#ff0,#00f)}",
+    );
+    minify_test(
+      ".foo { background: radial-gradient(ellipse farthest-side, yellow, blue) }",
+      ".foo{background:radial-gradient(farthest-side,#ff0,#00f)}",
+    );
+    minify_test(
+      ".foo { background: radial-gradient(farthest-side ellipse, yellow, blue) }",
+      ".foo{background:radial-gradient(farthest-side,#ff0,#00f)}",
+    );
+    minify_test(
+      ".foo { background: -webkit-radial-gradient(yellow, blue) }",
+      ".foo{background:-webkit-radial-gradient(#ff0,#00f)}",
+    );
+    minify_test(
+      ".foo { background: -moz-radial-gradient(yellow, blue) }",
+      ".foo{background:-moz-radial-gradient(#ff0,#00f)}",
+    );
+    minify_test(
+      ".foo { background: -o-radial-gradient(yellow, blue) }",
+      ".foo{background:-o-radial-gradient(#ff0,#00f)}",
+    );
+    minify_test(
+      ".foo { background: repeating-radial-gradient(circle 20px, yellow, blue) }",
+      ".foo{background:repeating-radial-gradient(20px,#ff0,#00f)}",
+    );
+    minify_test(
+      ".foo { background: -webkit-repeating-radial-gradient(circle 20px, yellow, blue) }",
+      ".foo{background:-webkit-repeating-radial-gradient(20px,#ff0,#00f)}",
+    );
+    minify_test(
+      ".foo { background: -moz-repeating-radial-gradient(circle 20px, yellow, blue) }",
+      ".foo{background:-moz-repeating-radial-gradient(20px,#ff0,#00f)}",
+    );
+    minify_test(
+      ".foo { background: -o-repeating-radial-gradient(circle 20px, yellow, blue) }",
+      ".foo{background:-o-repeating-radial-gradient(20px,#ff0,#00f)}",
+    );
+    minify_test(
+      ".foo { background: -webkit-gradient(radial, center center, 0, center center, 100, from(blue), to(yellow)) }",
+      ".foo{background:-webkit-gradient(radial,50% 50%,0,50% 50%,100,from(#00f),to(#ff0))}"
+    );
+    minify_test(
+      ".foo { background: conic-gradient(#f06, gold) }",
+      ".foo{background:conic-gradient(#f06,gold)}",
+    );
+    minify_test(
+      ".foo { background: conic-gradient(at 50% 50%, #f06, gold) }",
+      ".foo{background:conic-gradient(#f06,gold)}",
+    );
+    minify_test(
+      ".foo { background: conic-gradient(from 0deg, #f06, gold) }",
+      ".foo{background:conic-gradient(#f06,gold)}",
+    );
+    minify_test(
+      ".foo { background: conic-gradient(from 0, #f06, gold) }",
+      ".foo{background:conic-gradient(#f06,gold)}",
+    );
+    minify_test(
+      ".foo { background: conic-gradient(from 0deg at center, #f06, gold) }",
+      ".foo{background:conic-gradient(#f06,gold)}",
+    );
+    minify_test(
+      ".foo { background: conic-gradient(white -50%, black 150%) }",
+      ".foo{background:conic-gradient(#fff -50%,#000 150%)}",
+    );
+    minify_test(
+      ".foo { background: conic-gradient(white -180deg, black 540deg) }",
+      ".foo{background:conic-gradient(#fff -180deg,#000 540deg)}",
+    );
+    minify_test(
+      ".foo { background: conic-gradient(from 45deg, white, black, white) }",
+      ".foo{background:conic-gradient(from 45deg,#fff,#000,#fff)}",
+    );
+    minify_test(
+      ".foo { background: repeating-conic-gradient(from 45deg, white, black, white) }",
+      ".foo{background:repeating-conic-gradient(from 45deg,#fff,#000,#fff)}",
+    );
+    minify_test(
+      ".foo { background: repeating-conic-gradient(black 0deg 25%, white 0deg 50%) }",
+      ".foo{background:repeating-conic-gradient(#000 0deg 25%,#fff 0deg 50%)}",
+    );
+
+    test(
+      r#"
+        .foo {
+          background: -webkit-gradient(linear, left top, left bottom, from(red), to(blue));
+          background: -webkit-linear-gradient(red, blue);
+          background: -moz-linear-gradient(red, blue);
+          background: -o-linear-gradient(red, blue);
+          background: linear-gradient(red, blue);
+        }
+      "#,
+      indoc! {r#"
+        .foo {
+          background: -webkit-gradient(linear, left top, left bottom, from(red), to(#00f));
+          background: -webkit-linear-gradient(red, #00f);
+          background: -moz-linear-gradient(red, #00f);
+          background: -o-linear-gradient(red, #00f);
+          background: linear-gradient(red, #00f);
+        }
+      "#},
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        background: -webkit-gradient(linear, left top, left bottom, from(red), to(blue));
+        background: -webkit-linear-gradient(red, blue);
+        background: -moz-linear-gradient(red, blue);
+        background: -o-linear-gradient(red, blue);
+        background: linear-gradient(red, blue);
+      }
+      "#,
+      indoc! {r#"
+      .foo {
+        background: linear-gradient(red, #00f);
+      }
+      "#},
+      Browsers {
+        chrome: Some(95 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        background: -webkit-gradient(linear, left top, left bottom, from(red), to(blue));
+        background: -webkit-linear-gradient(red, blue);
+        background: -moz-linear-gradient(red, blue);
+        background: -o-linear-gradient(red, blue);
+      }
+      "#,
+      indoc! {r#"
+      .foo {
+        background: -webkit-gradient(linear, left top, left bottom, from(red), to(#00f));
+        background: -webkit-linear-gradient(red, #00f);
+        background: -moz-linear-gradient(red, #00f);
+        background: -o-linear-gradient(red, #00f);
+      }
+      "#},
+      Browsers {
+        chrome: Some(95 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        background-image: linear-gradient(red, blue);
+      }
+      "#,
+      indoc! {r#"
+      .foo {
+        background-image: -webkit-gradient(linear, 0 0, 0 100%, from(red), to(#00f));
+        background-image: -webkit-linear-gradient(top, red, #00f);
+        background-image: linear-gradient(red, #00f);
+      }
+      "#},
+      Browsers {
+        chrome: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        background-image: linear-gradient(to right, red, blue);
+      }
+      "#,
+      indoc! {r#"
+      .foo {
+        background-image: -webkit-gradient(linear, 0 0, 100% 0, from(red), to(#00f));
+        background-image: -webkit-linear-gradient(left, red, #00f);
+        background-image: linear-gradient(to right, red, #00f);
+      }
+      "#},
+      Browsers {
+        chrome: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        background-image: linear-gradient(to top, red, blue);
+      }
+      "#,
+      indoc! {r#"
+      .foo {
+        background-image: -webkit-gradient(linear, 0 100%, 0 0, from(red), to(#00f));
+        background-image: -webkit-linear-gradient(red, #00f);
+        background-image: linear-gradient(to top, red, #00f);
+      }
+      "#},
+      Browsers {
+        chrome: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        background-image: linear-gradient(to left, red, blue);
+      }
+      "#,
+      indoc! {r#"
+      .foo {
+        background-image: -webkit-gradient(linear, 100% 0, 0 0, from(red), to(#00f));
+        background-image: -webkit-linear-gradient(right, red, #00f);
+        background-image: linear-gradient(to left, red, #00f);
+      }
+      "#},
+      Browsers {
+        chrome: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        background-image: linear-gradient(to left bottom, red, blue);
+      }
+      "#,
+      indoc! {r#"
+      .foo {
+        background-image: -webkit-gradient(linear, 100% 0, 0 100%, from(red), to(#00f));
+        background-image: -webkit-linear-gradient(top right, red, #00f);
+        background-image: linear-gradient(to bottom left, red, #00f);
+      }
+      "#},
+      Browsers {
+        chrome: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        background-image: linear-gradient(to top right, red, blue);
+      }
+      "#,
+      indoc! {r#"
+      .foo {
+        background-image: -webkit-gradient(linear, 0 100%, 100% 0, from(red), to(#00f));
+        background-image: -webkit-linear-gradient(bottom left, red, #00f);
+        background-image: linear-gradient(to top right, red, #00f);
+      }
+      "#},
+      Browsers {
+        chrome: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        background-image: linear-gradient(90deg, red, blue);
+      }
+      "#,
+      indoc! {r#"
+      .foo {
+        background-image: -webkit-gradient(linear, 0 0, 100% 0, from(red), to(#00f));
+        background-image: -webkit-linear-gradient(0deg, red, #00f);
+        background-image: linear-gradient(90deg, red, #00f);
+      }
+      "#},
+      Browsers {
+        chrome: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        background-image: linear-gradient(45deg, red, blue);
+      }
+      "#,
+      indoc! {r#"
+      .foo {
+        background-image: -webkit-linear-gradient(45deg, red, #00f);
+        background-image: linear-gradient(45deg, red, #00f);
+      }
+      "#},
+      Browsers {
+        chrome: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        background-image: linear-gradient(red, blue);
+      }
+      "#,
+      indoc! {r#"
+      .foo {
+        background-image: -webkit-linear-gradient(top, red, #00f);
+        background-image: linear-gradient(red, #00f);
+      }
+      "#},
+      Browsers {
+        chrome: Some(10 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        background-image: radial-gradient(20px, red, blue);
+      }
+      "#,
+      indoc! {r#"
+      .foo {
+        background-image: -webkit-gradient(radial, center center, 0, center center, 20, from(red), to(#00f));
+        background-image: -webkit-radial-gradient(20px, red, #00f);
+        background-image: radial-gradient(20px, red, #00f);
+      }
+      "#},
+      Browsers {
+        chrome: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        background-image: radial-gradient(20px at top left, red, blue);
+      }
+      "#,
+      indoc! {r#"
+      .foo {
+        background-image: -webkit-gradient(radial, left top, 0, left top, 20, from(red), to(#00f));
+        background-image: -webkit-radial-gradient(20px at 0 0, red, #00f);
+        background-image: radial-gradient(20px at 0 0, red, #00f);
+      }
+      "#},
+      Browsers {
+        chrome: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        background-image: radial-gradient(red, blue);
+      }
+      "#,
+      indoc! {r#"
+      .foo {
+        background-image: -webkit-radial-gradient(red, #00f);
+        background-image: radial-gradient(red, #00f);
+      }
+      "#},
+      Browsers {
+        chrome: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        background-image: -webkit-gradient(radial, left top, 0, left top, 20, from(red), to(#00f));
+        background-image: -webkit-radial-gradient(20px at 0% 0%, red, #00f);
+        background-image: radial-gradient(20px at 0% 0%, red, #00f);
+      }
+      "#,
+      indoc! {r#"
+      .foo {
+        background-image: radial-gradient(20px at 0 0, red, #00f);
+      }
+      "#},
+      Browsers {
+        chrome: Some(30 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        background: -webkit-gradient(radial, left top, 0, left top, 20, from(red), to(#00f));
+        background: -webkit-radial-gradient(20px at 0% 0%, red, #00f);
+        background: radial-gradient(20px at 0% 0%, red, #00f);
+      }
+      "#,
+      indoc! {r#"
+      .foo {
+        background: radial-gradient(20px at 0 0, red, #00f);
+      }
+      "#},
+      Browsers {
+        chrome: Some(30 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        background: radial-gradient(red, blue);
+      }
+      "#,
+      indoc! {r#"
+      .foo {
+        background: -webkit-radial-gradient(red, #00f);
+        background: radial-gradient(red, #00f);
+      }
+      "#},
+      Browsers {
+        chrome: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        background: radial-gradient(red, blue), linear-gradient(yellow, red), url(bg.jpg);
+      }
+      "#,
+      indoc! {r#"
+      .foo {
+        background: -webkit-gradient(linear, 0 0, 0 100%, from(#ff0), to(red)), url("bg.jpg");
+        background: -webkit-radial-gradient(red, #00f), -webkit-linear-gradient(top, #ff0, red), url("bg.jpg");
+        background: -moz-radial-gradient(red, #00f), -moz-linear-gradient(top, #ff0, red), url("bg.jpg");
+        background: -o-radial-gradient(red, #00f), -o-linear-gradient(top, #ff0, red), url("bg.jpg");
+        background: radial-gradient(red, #00f), linear-gradient(#ff0, red), url("bg.jpg");
+      }
+      "#},
+      Browsers {
+        chrome: Some(8 << 16),
+        firefox: Some(4 << 16),
+        opera: Some(11 << 16 | 5 << 8),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        background: linear-gradient(yellow, red 30% 40%, blue);
+      }
+      "#,
+      indoc! {r#"
+      .foo {
+        background: linear-gradient(#ff0, red 30%, red 40%, #00f);
+      }
+      "#},
+      Browsers {
+        chrome: Some(70 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        background: linear-gradient(yellow, red 30% 40%, blue);
+      }
+      "#,
+      indoc! {r#"
+      .foo {
+        background: linear-gradient(#ff0, red 30% 40%, #00f);
+      }
+      "#},
+      Browsers {
+        chrome: Some(71 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { background: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)) }",
+      indoc! { r#"
+        .foo {
+          background: linear-gradient(#ff0f0e, #7773ff);
+          background: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364));
+        }
+      "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { background: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)) }",
+      indoc! { r#"
+        .foo {
+          background: linear-gradient(#ff0f0e, #7773ff);
+          background: linear-gradient(color(display-p3 1 .0000153435 -.00000303562), color(display-p3 .440289 .28452 1.23485));
+          background: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364));
+        }
+      "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        safari: Some(14 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { background: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)) }",
+      indoc! { r#"
+        .foo {
+          background: -webkit-linear-gradient(top, #ff0f0e, #7773ff);
+          background: linear-gradient(#ff0f0e, #7773ff);
+          background: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364));
+        }
+      "#},
+      Browsers {
+        chrome: Some(20 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { background: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)) }",
+      indoc! { r#"
+        .foo {
+          background: -webkit-gradient(linear, 0 0, 0 100%, from(#ff0f0e), to(#7773ff));
+          background: -webkit-linear-gradient(top, #ff0f0e, #7773ff);
+          background: linear-gradient(#ff0f0e, #7773ff);
+          background: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364));
+        }
+      "#},
+      Browsers {
+        chrome: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { background: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)) }",
+      indoc! { r#"
+        .foo {
+          background: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364));
+        }
+      "#},
+      Browsers {
+        safari: Some(15 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { background-image: linear-gradient(oklab(59.686% 0.1009 0.1192), oklab(54.0% -0.10 -0.02)); }",
+      indoc! { r#"
+        .foo {
+          background-image: linear-gradient(lab(52.2319% 40.1449 59.9171), lab(47.7776% -34.2947 -7.65904));
+        }
+      "#},
+      Browsers {
+        safari: Some(15 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { background-image: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)) }",
+      indoc! { r#"
+        .foo {
+          background-image: linear-gradient(#ff0f0e, #7773ff);
+          background-image: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364));
+        }
+      "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { background-image: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)) }",
+      indoc! { r#"
+        .foo {
+          background-image: linear-gradient(#ff0f0e, #7773ff);
+          background-image: linear-gradient(color(display-p3 1 .0000153435 -.00000303562), color(display-p3 .440289 .28452 1.23485));
+          background-image: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364));
+        }
+      "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        safari: Some(14 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { background-image: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)) }",
+      indoc! { r#"
+        .foo {
+          background-image: -webkit-linear-gradient(top, #ff0f0e, #7773ff);
+          background-image: linear-gradient(#ff0f0e, #7773ff);
+          background-image: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364));
+        }
+      "#},
+      Browsers {
+        chrome: Some(20 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { background-image: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)) }",
+      indoc! { r#"
+        .foo {
+          background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ff0f0e), to(#7773ff));
+          background-image: -webkit-linear-gradient(top, #ff0f0e, #7773ff);
+          background-image: linear-gradient(#ff0f0e, #7773ff);
+          background-image: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364));
+        }
+      "#},
+      Browsers {
+        chrome: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { background-image: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)) }",
+      indoc! { r#"
+        .foo {
+          background-image: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364));
+        }
+      "#},
+      Browsers {
+        safari: Some(15 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { background-image: linear-gradient(oklab(59.686% 0.1009 0.1192), oklab(54.0% -0.10 -0.02)); }",
+      indoc! { r#"
+        .foo {
+          background-image: linear-gradient(lab(52.2319% 40.1449 59.9171), lab(47.7776% -34.2947 -7.65904));
+        }
+      "#},
+      Browsers {
+        safari: Some(15 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    // Test cases from https://github.com/postcss/autoprefixer/blob/541295c0e6dd348db2d3f52772b59cd403c59d29/test/cases/gradient.css
+    prefix_test(
+      r#"
+        a {
+          background: linear-gradient(350.5deg, white, black), linear-gradient(-130deg, black, white), linear-gradient(45deg, black, white);
+        }
+        b {
+          background-image: linear-gradient(rgba(0,0,0,1), white), linear-gradient(white, black);
+        }
+        strong {
+          background: linear-gradient(to top, transparent, rgba(0, 0, 0, 0.8) 20px, #000 30px, #000) no-repeat;
+        }
+        div {
+          background-image: radial-gradient(to left, white, black), repeating-linear-gradient(to bottom right, black, white), repeating-radial-gradient(to top, aqua, red);
+        }
+        .old-radial {
+          background: radial-gradient(0 50%, ellipse farthest-corner, black, white);
+        }
+        .simple1 {
+          background: linear-gradient(black, white);
+        }
+        .simple2 {
+          background: linear-gradient(to left, black 0%, rgba(0, 0, 0, 0.5)50%, white 100%);
+        }
+        .simple3 {
+          background: linear-gradient(to left, black 50%, white 100%);
+        }
+        .simple4 {
+          background: linear-gradient(to right top, black, white);
+        }
+        .direction {
+          background: linear-gradient(top left, black, rgba(0, 0, 0, 0.5), white);
+        }
+        .silent {
+          background: -webkit-linear-gradient(top left, black, white);
+        }
+        .radial {
+          background: radial-gradient(farthest-side at 0 50%, white, black);
+        }
+        .second {
+          background: red linear-gradient(red, blue);
+          background: url('logo.png'), linear-gradient(#fff, #000);
+        }
+        .px {
+          background: linear-gradient(black 0, white 100px);
+        }
+        .list {
+          list-style-image: linear-gradient(white, black);
+        }
+        .mask {
+          mask: linear-gradient(white, black);
+        }
+        .newline {
+          background-image:
+              linear-gradient( white, black ),
+              linear-gradient( black, white );
+        }
+        .convert {
+          background: linear-gradient(0deg, white, black);
+          background: linear-gradient(90deg, white, black);
+          background: linear-gradient(180deg, white, black);
+          background: linear-gradient(270deg, white, black);
+        }
+        .grad {
+          background: linear-gradient(1grad, white, black);
+        }
+        .rad {
+          background: linear-gradient(1rad, white, black);
+        }
+        .turn {
+          background: linear-gradient(0.3turn, white, black);
+        }
+        .norm {
+          background: linear-gradient(-90deg, white, black);
+        }
+        .mask {
+          mask-image: radial-gradient(circle at 86% 86%, transparent 8px, black 8px);
+        }
+        .cover {
+          background: radial-gradient(ellipse cover at center, white, black);
+        }
+        .contain {
+          background: radial-gradient(contain at center, white, black);
+        }
+        .no-div {
+          background: linear-gradient(black);
+        }
+        .background-shorthand {
+          background: radial-gradient(#FFF, transparent) 0 0 / cover no-repeat #F0F;
+        }
+        .background-advanced {
+          background: radial-gradient(ellipse farthest-corner at 5px 15px, rgba(214, 168, 18, 0.7) 0%, rgba(255, 21, 177, 0.7) 50%, rgba(210, 7, 148, 0.7) 95%),
+                      radial-gradient(#FFF, transparent),
+                      url(path/to/image.jpg) 50%/cover;
+        }
+        .multiradial {
+          mask-image: radial-gradient(circle closest-corner at 100% 50%, #000, transparent);
+        }
+        .broken {
+          mask-image: radial-gradient(white, black);
+        }
+        .loop {
+          background-image: url("https://test.com/lol(test.png"), radial-gradient(yellow, black, yellow);
+        }
+        .unitless-zero {
+          background-image: linear-gradient(0, green, blue);
+          background: repeating-linear-gradient(0, blue, red 33.3%)
+        }
+        .zero-grad {
+          background: linear-gradient(0grad, green, blue);
+          background-image: repeating-linear-gradient(0grad, blue, red 33.3%)
+        }
+        .zero-rad {
+          background: linear-gradient(0rad, green, blue);
+        }
+        .zero-turn {
+          background: linear-gradient(0turn, green, blue);
+        }
+      "#,
+      indoc! { r#"
+        a {
+          background: -webkit-linear-gradient(99.5deg, #fff, #000), -webkit-linear-gradient(220deg, #000, #fff), -webkit-linear-gradient(45deg, #000, #fff);
+          background: -o-linear-gradient(99.5deg, #fff, #000), -o-linear-gradient(220deg, #000, #fff), -o-linear-gradient(45deg, #000, #fff);
+          background: linear-gradient(350.5deg, #fff, #000), linear-gradient(-130deg, #000, #fff), linear-gradient(45deg, #000, #fff);
+        }
+
+        b {
+          background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#000), to(#fff)), -webkit-gradient(linear, 0 0, 0 100%, from(#fff), to(#000));
+          background-image: -webkit-linear-gradient(top, #000, #fff), -webkit-linear-gradient(top, #fff, #000);
+          background-image: -o-linear-gradient(top, #000, #fff), -o-linear-gradient(top, #fff, #000);
+          background-image: linear-gradient(#000, #fff), linear-gradient(#fff, #000);
+        }
+
+        strong {
+          background: -webkit-linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, .8) 20px, #000 30px, #000) no-repeat;
+          background: -o-linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, .8) 20px, #000 30px, #000) no-repeat;
+          background: linear-gradient(to top, rgba(0, 0, 0, 0), rgba(0, 0, 0, .8) 20px, #000 30px, #000) no-repeat;
+        }
+
+        div {
+          background-image: radial-gradient(to left, white, black), repeating-linear-gradient(to bottom right, black, white), repeating-radial-gradient(to top, aqua, red);
+        }
+
+        .old-radial {
+          background: radial-gradient(0 50%, ellipse farthest-corner, black, white);
+        }
+
+        .simple1 {
+          background: -webkit-gradient(linear, 0 0, 0 100%, from(#000), to(#fff));
+          background: -webkit-linear-gradient(top, #000, #fff);
+          background: -o-linear-gradient(top, #000, #fff);
+          background: linear-gradient(#000, #fff);
+        }
+
+        .simple2 {
+          background: -webkit-gradient(linear, 100% 0, 0 0, from(#000), color-stop(.5, rgba(0, 0, 0, .5)), to(#fff));
+          background: -webkit-linear-gradient(right, #000 0%, rgba(0, 0, 0, .5) 50%, #fff 100%);
+          background: -o-linear-gradient(right, #000 0%, rgba(0, 0, 0, .5) 50%, #fff 100%);
+          background: linear-gradient(to left, #000 0%, rgba(0, 0, 0, .5) 50%, #fff 100%);
+        }
+
+        .simple3 {
+          background: -webkit-gradient(linear, 100% 0, 0 0, color-stop(.5, #000), to(#fff));
+          background: -webkit-linear-gradient(right, #000 50%, #fff 100%);
+          background: -o-linear-gradient(right, #000 50%, #fff 100%);
+          background: linear-gradient(to left, #000 50%, #fff 100%);
+        }
+
+        .simple4 {
+          background: -webkit-gradient(linear, 0 100%, 100% 0, from(#000), to(#fff));
+          background: -webkit-linear-gradient(bottom left, #000, #fff);
+          background: -o-linear-gradient(bottom left, #000, #fff);
+          background: linear-gradient(to top right, #000, #fff);
+        }
+
+        .direction {
+          background: linear-gradient(top left, black, rgba(0, 0, 0, .5), white);
+        }
+
+        .silent {
+          background: -webkit-gradient(linear, 100% 100%, 0 0, from(#000), to(#fff));
+          background: -webkit-linear-gradient(top left, #000, #fff);
+        }
+
+        .radial {
+          background: -webkit-radial-gradient(farthest-side at 0, #fff, #000);
+          background: -o-radial-gradient(farthest-side at 0, #fff, #000);
+          background: radial-gradient(farthest-side at 0, #fff, #000);
+        }
+
+        .second {
+          background: red -webkit-gradient(linear, 0 0, 0 100%, from(red), to(#00f));
+          background: red -webkit-linear-gradient(top, red, #00f);
+          background: red -o-linear-gradient(top, red, #00f);
+          background: red linear-gradient(red, #00f);
+          background: url("logo.png"), linear-gradient(#fff, #000);
+        }
+
+        .px {
+          background: -webkit-linear-gradient(top, #000 0, #fff 100px);
+          background: -o-linear-gradient(top, #000 0, #fff 100px);
+          background: linear-gradient(#000 0, #fff 100px);
+        }
+
+        .list {
+          list-style-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fff), to(#000));
+          list-style-image: -webkit-linear-gradient(top, #fff, #000);
+          list-style-image: -o-linear-gradient(top, #fff, #000);
+          list-style-image: linear-gradient(#fff, #000);
+        }
+
+        .mask {
+          -webkit-mask: -webkit-gradient(linear, 0 0, 0 100%, from(#fff), to(#000));
+          -webkit-mask: -webkit-linear-gradient(top, #fff, #000);
+          -webkit-mask: -o-linear-gradient(top, #fff, #000);
+          mask: -o-linear-gradient(top, #fff, #000);
+          -webkit-mask: linear-gradient(#fff, #000);
+          mask: linear-gradient(#fff, #000);
+        }
+
+        .newline {
+          background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fff), to(#000)), -webkit-gradient(linear, 0 0, 0 100%, from(#000), to(#fff));
+          background-image: -webkit-linear-gradient(top, #fff, #000), -webkit-linear-gradient(top, #000, #fff);
+          background-image: -o-linear-gradient(top, #fff, #000), -o-linear-gradient(top, #000, #fff);
+          background-image: linear-gradient(#fff, #000), linear-gradient(#000, #fff);
+        }
+
+        .convert {
+          background: -webkit-gradient(linear, 0 100%, 0 0, from(#fff), to(#000));
+          background: -webkit-linear-gradient(90deg, #fff, #000);
+          background: -o-linear-gradient(90deg, #fff, #000);
+          background: linear-gradient(0deg, #fff, #000);
+          background: linear-gradient(90deg, #fff, #000);
+          background: linear-gradient(#fff, #000);
+          background: linear-gradient(270deg, #fff, #000);
+        }
+
+        .grad {
+          background: -webkit-linear-gradient(89.1deg, #fff, #000);
+          background: -o-linear-gradient(89.1deg, #fff, #000);
+          background: linear-gradient(1grad, #fff, #000);
+        }
+
+        .rad {
+          background: -webkit-linear-gradient(32.704deg, #fff, #000);
+          background: -o-linear-gradient(32.704deg, #fff, #000);
+          background: linear-gradient(57.2958deg, #fff, #000);
+        }
+
+        .turn {
+          background: -webkit-linear-gradient(342deg, #fff, #000);
+          background: -o-linear-gradient(342deg, #fff, #000);
+          background: linear-gradient(.3turn, #fff, #000);
+        }
+
+        .norm {
+          background: -webkit-linear-gradient(#fff, #000);
+          background: -o-linear-gradient(#fff, #000);
+          background: linear-gradient(-90deg, #fff, #000);
+        }
+
+        .mask {
+          -webkit-mask-image: -webkit-radial-gradient(circle at 86% 86%, rgba(0, 0, 0, 0) 8px, #000 8px);
+          -webkit-mask-image: -o-radial-gradient(circle at 86% 86%, rgba(0, 0, 0, 0) 8px, #000 8px);
+          mask-image: -o-radial-gradient(circle at 86% 86%, rgba(0, 0, 0, 0) 8px, #000 8px);
+          -webkit-mask-image: radial-gradient(circle at 86% 86%, rgba(0, 0, 0, 0) 8px, #000 8px);
+          mask-image: radial-gradient(circle at 86% 86%, rgba(0, 0, 0, 0) 8px, #000 8px);
+        }
+
+        .cover {
+          background: radial-gradient(ellipse cover at center, white, black);
+        }
+
+        .contain {
+          background: radial-gradient(contain at center, white, black);
+        }
+
+        .no-div {
+          background: -webkit-gradient(linear, 0 0, 0 100%, from(#000));
+          background: -webkit-linear-gradient(top, #000);
+          background: -o-linear-gradient(top, #000);
+          background: linear-gradient(#000);
+        }
+
+        .background-shorthand {
+          background: #f0f -webkit-radial-gradient(#fff, rgba(0, 0, 0, 0)) 0 0 / cover no-repeat;
+          background: #f0f -o-radial-gradient(#fff, rgba(0, 0, 0, 0)) 0 0 / cover no-repeat;
+          background: #f0f radial-gradient(#fff, rgba(0, 0, 0, 0)) 0 0 / cover no-repeat;
+        }
+
+        .background-advanced {
+          background: url("path/to/image.jpg") 50% / cover;
+          background: -webkit-radial-gradient(at 5px 15px, rgba(214, 168, 18, .7) 0%, rgba(255, 21, 177, .7) 50%, rgba(210, 7, 148, .7) 95%), -webkit-radial-gradient(#fff, rgba(0, 0, 0, 0)), url("path/to/image.jpg") 50% / cover;
+          background: -o-radial-gradient(at 5px 15px, rgba(214, 168, 18, .7) 0%, rgba(255, 21, 177, .7) 50%, rgba(210, 7, 148, .7) 95%), -o-radial-gradient(#fff, rgba(0, 0, 0, 0)), url("path/to/image.jpg") 50% / cover;
+          background: radial-gradient(at 5px 15px, rgba(214, 168, 18, .7) 0%, rgba(255, 21, 177, .7) 50%, rgba(210, 7, 148, .7) 95%), radial-gradient(#fff, rgba(0, 0, 0, 0)), url("path/to/image.jpg") 50% / cover;
+        }
+
+        .multiradial {
+          -webkit-mask-image: -webkit-radial-gradient(circle closest-corner at 100%, #000, rgba(0, 0, 0, 0));
+          -webkit-mask-image: -o-radial-gradient(circle closest-corner at 100%, #000, rgba(0, 0, 0, 0));
+          mask-image: -o-radial-gradient(circle closest-corner at 100%, #000, rgba(0, 0, 0, 0));
+          -webkit-mask-image: radial-gradient(circle closest-corner at 100%, #000, rgba(0, 0, 0, 0));
+          mask-image: radial-gradient(circle closest-corner at 100%, #000, rgba(0, 0, 0, 0));
+        }
+
+        .broken {
+          -webkit-mask-image: -webkit-radial-gradient(#fff, #000);
+          -webkit-mask-image: -o-radial-gradient(#fff, #000);
+          mask-image: -o-radial-gradient(#fff, #000);
+          -webkit-mask-image: radial-gradient(#fff, #000);
+          mask-image: radial-gradient(#fff, #000);
+        }
+
+        .loop {
+          background-image: url("https://test.com/lol(test.png");
+          background-image: url("https://test.com/lol(test.png"), -webkit-radial-gradient(#ff0, #000, #ff0);
+          background-image: url("https://test.com/lol(test.png"), -o-radial-gradient(#ff0, #000, #ff0);
+          background-image: url("https://test.com/lol(test.png"), radial-gradient(#ff0, #000, #ff0);
+        }
+
+        .unitless-zero {
+          background-image: -webkit-gradient(linear, 0 100%, 0 0, from(green), to(#00f));
+          background-image: -webkit-linear-gradient(90deg, green, #00f);
+          background-image: -o-linear-gradient(90deg, green, #00f);
+          background-image: linear-gradient(0deg, green, #00f);
+          background: repeating-linear-gradient(0deg, #00f, red 33.3%);
+        }
+
+        .zero-grad {
+          background: -webkit-gradient(linear, 0 100%, 0 0, from(green), to(#00f));
+          background: -webkit-linear-gradient(90deg, green, #00f);
+          background: -o-linear-gradient(90deg, green, #00f);
+          background: linear-gradient(0grad, green, #00f);
+          background-image: repeating-linear-gradient(0grad, #00f, red 33.3%);
+        }
+
+        .zero-rad, .zero-turn {
+          background: -webkit-gradient(linear, 0 100%, 0 0, from(green), to(#00f));
+          background: -webkit-linear-gradient(90deg, green, #00f);
+          background: -o-linear-gradient(90deg, green, #00f);
+          background: linear-gradient(0deg, green, #00f);
+        }
+      "#},
+      Browsers {
+        chrome: Some(25 << 16),
+        opera: Some(12 << 16),
+        android: Some(2 << 16 | 3 << 8),
+        ..Browsers::default()
+      },
+    );
+  }
+
+  #[test]
+  fn test_font_face() {
+    minify_test(
+      r#"@font-face {
+      src: url("test.woff");
+      font-family: "Helvetica";
+      font-weight: bold;
+      font-style: italic;
+    }"#,
+      "@font-face{src:url(test.woff);font-family:Helvetica;font-weight:700;font-style:italic}",
+    );
+    minify_test("@font-face {src: url(test.woff);}", "@font-face{src:url(test.woff)}");
+    minify_test("@font-face {src: local(\"Test\");}", "@font-face{src:local(Test)}");
+    minify_test(
+      "@font-face {src: local(\"Foo Bar\");}",
+      "@font-face{src:local(Foo Bar)}",
+    );
+    minify_test("@font-face {src: local(Test);}", "@font-face{src:local(Test)}");
+    minify_test("@font-face {src: local(Foo Bar);}", "@font-face{src:local(Foo Bar)}");
+
+    minify_test(
+      "@font-face {src: url(\"test.woff\") format(woff);}",
+      "@font-face{src:url(test.woff)format(\"woff\")}",
+    );
+    minify_test(
+      "@font-face {src: url(\"test.ttc\") format(collection), url(test.ttf) format(truetype);}",
+      "@font-face{src:url(test.ttc)format(\"collection\"),url(test.ttf)format(\"truetype\")}",
+    );
+    minify_test(
+      "@font-face {src: url(\"test.otf\") format(opentype) tech(features-aat);}",
+      "@font-face{src:url(test.otf)format(\"opentype\")tech(features-aat)}",
+    );
+    minify_test(
+      "@font-face {src: url(\"test.woff\") format(woff) tech(color-colrv1);}",
+      "@font-face{src:url(test.woff)format(\"woff\")tech(color-colrv1)}",
+    );
+    minify_test(
+      "@font-face {src: url(\"test.woff2\") format(woff2) tech(variations);}",
+      "@font-face{src:url(test.woff2)format(\"woff2\")tech(variations)}",
+    );
+    minify_test(
+      "@font-face {src: url(\"test.woff\") format(woff) tech(palettes);}",
+      "@font-face{src:url(test.woff)format(\"woff\")tech(palettes)}",
+    );
+    // multiple tech
+    minify_test(
+      "@font-face {src: url(\"test.woff\") format(woff) tech(features-opentype, color-sbix);}",
+      "@font-face{src:url(test.woff)format(\"woff\")tech(features-opentype,color-sbix)}",
+    );
+    minify_test(
+      "@font-face {src: url(\"test.woff\")   format(woff)    tech(incremental, color-svg, features-graphite, features-aat);}",
+      "@font-face{src:url(test.woff)format(\"woff\")tech(incremental,color-svg,features-graphite,features-aat)}",
+    );
+    // format() function must precede tech() if both are present
+    minify_test(
+      "@font-face {src: url(\"foo.ttf\") format(opentype) tech(color-colrv1);}",
+      "@font-face{src:url(foo.ttf)format(\"opentype\")tech(color-colrv1)}",
+    );
+    // only have tech is valid
+    minify_test(
+      "@font-face {src: url(\"foo.ttf\") tech(color-SVG);}",
+      "@font-face{src:url(foo.ttf)tech(color-svg)}",
+    );
+    // CGQAQ: if tech and format both presence, order is matter, tech before format is invalid
+    // but now just return raw token, we don't have strict mode yet.
+    // ref: https://github.com/parcel-bundler/lightningcss/pull/255#issuecomment-1219049998
+    minify_test(
+      "@font-face {src: url(\"foo.ttf\") tech(palettes  color-colrv0  variations) format(opentype);}",
+      "@font-face{src:url(foo.ttf) tech(palettes color-colrv0 variations)format(opentype)}",
+    );
+    // TODO(CGQAQ): make this test pass when we have strict mode
+    // ref: https://github.com/web-platform-tests/wpt/blob/9f8a6ccc41aa725e8f51f4f096f686313bb88d8d/css/css-fonts/parsing/font-face-src-tech.html#L45
+    // error_test(
+    //   "@font-face {src: url(\"foo.ttf\") tech(features-opentype) format(opentype);}",
+    //   ParserError::AtRuleBodyInvalid,
+    // );
+    // error_test(
+    //   "@font-face {src: url(\"foo.ttf\") tech();}",
+    //   ParserError::AtRuleBodyInvalid,
+    // );
+    // error_test(
+    //   "@font-face {src: url(\"foo.ttf\") tech(\"features-opentype\");}",
+    //   ParserError::AtRuleBodyInvalid,
+    // );
+    // error_test(
+    //   "@font-face {src: url(\"foo.ttf\") tech(\"color-colrv0\");}",
+    //   ParserError::AtRuleBodyInvalid,
+    // );
+    minify_test(
+      "@font-face {src: local(\"\") url(\"test.woff\");}",
+      "@font-face{src:local(\"\")url(test.woff)}",
+    );
+    minify_test("@font-face {font-weight: 200 400}", "@font-face{font-weight:200 400}");
+    minify_test("@font-face {font-weight: 400 400}", "@font-face{font-weight:400}");
+    minify_test(
+      "@font-face {font-stretch: 50% 200%}",
+      "@font-face{font-stretch:50% 200%}",
+    );
+    minify_test("@font-face {font-stretch: 50% 50%}", "@font-face{font-stretch:50%}");
+    minify_test("@font-face {unicode-range: U+26;}", "@font-face{unicode-range:U+26}");
+    minify_test("@font-face {unicode-range: u+26;}", "@font-face{unicode-range:U+26}");
+    minify_test(
+      "@font-face {unicode-range: U+0-7F;}",
+      "@font-face{unicode-range:U+0-7F}",
+    );
+    minify_test(
+      "@font-face {unicode-range: U+0025-00FF;}",
+      "@font-face{unicode-range:U+25-FF}",
+    );
+    minify_test("@font-face {unicode-range: U+4??;}", "@font-face{unicode-range:U+4??}");
+    minify_test(
+      "@font-face {unicode-range: U+400-4FF;}",
+      "@font-face{unicode-range:U+4??}",
+    );
+    minify_test(
+      "@font-face {unicode-range: U+0025-00FF, U+4??;}",
+      "@font-face{unicode-range:U+25-FF,U+4??}",
+    );
+    minify_test(
+      "@font-face {unicode-range: U+A5, U+4E00-9FFF, U+30??, U+FF00-FF9F;}",
+      "@font-face{unicode-range:U+A5,U+4E00-9FFF,U+30??,U+FF00-FF9F}",
+    );
+    minify_test(
+      "@font-face {unicode-range: U+????;}",
+      "@font-face{unicode-range:U+????}",
+    );
+    minify_test(
+      "@font-face {unicode-range: U+0000-FFFF;}",
+      "@font-face{unicode-range:U+????}",
+    );
+    minify_test(
+      "@font-face {unicode-range: U+10????;}",
+      "@font-face{unicode-range:U+10????}",
+    );
+    minify_test(
+      "@font-face {unicode-range: U+100000-10FFFF;}",
+      "@font-face{unicode-range:U+10????}",
+    );
+    minify_test(
+      "@font-face {unicode-range: U+1e1e?;}",
+      "@font-face{unicode-range:U+1E1E?}",
+    );
+    minify_test(
+      "@font-face {unicode-range: u+????, U+1????, U+10????;}",
+      "@font-face{unicode-range:U+????,U+1????,U+10????}",
+    );
+    minify_test(r#"
+      @font-face {
+        font-family: Inter;
+        font-style: oblique 0deg 10deg;
+        font-weight: 100 900;
+        src: url("../fonts/Inter.var.woff2?v=3.19") format("woff2");
+        font-display: swap;
+      }
+    "#, "@font-face{font-family:Inter;font-style:oblique 0deg 10deg;font-weight:100 900;src:url(../fonts/Inter.var.woff2?v=3.19)format(\"woff2\");font-display:swap}");
+    minify_test(r#"
+    @font-face {
+      font-family: Inter;
+      font-style: oblique 14deg 14deg;
+      font-weight: 100 900;
+      src: url("../fonts/Inter.var.woff2?v=3.19") format("woff2");
+      font-display: swap;
+    }
+  "#, "@font-face{font-family:Inter;font-style:oblique;font-weight:100 900;src:url(../fonts/Inter.var.woff2?v=3.19)format(\"woff2\");font-display:swap}");
+  }
+
+  #[test]
+  fn test_font_palette_values() {
+    minify_test(
+      r#"@font-palette-values --Cooler {
+      font-family: Bixa;
+      base-palette: 1;
+      override-colors: 1 #7EB7E4;
+    }"#,
+      "@font-palette-values --Cooler{font-family:Bixa;base-palette:1;override-colors:1 #7eb7e4}",
+    );
+    minify_test(
+      r#"@font-palette-values --Cooler {
+      font-family: Handover Sans;
+      base-palette: 3;
+      override-colors: 1 rgb(43, 12, 9), 3 lime;
+    }"#,
+      "@font-palette-values --Cooler{font-family:Handover Sans;base-palette:3;override-colors:1 #2b0c09,3 #0f0}",
+    );
+    minify_test(r#"@font-palette-values --Cooler {
+      font-family: Handover Sans;
+      base-palette: 3;
+      override-colors: 1 rgb(43, 12, 9), 3 var(--highlight);
+    }"#, "@font-palette-values --Cooler{font-family:Handover Sans;base-palette:3;override-colors:1 #2b0c09,3 var(--highlight)}");
+    prefix_test(
+      r#"@font-palette-values --Cooler {
+      font-family: Handover Sans;
+      base-palette: 3;
+      override-colors: 1 rgb(43, 12, 9), 3 lch(50.998% 135.363 338);
+    }"#,
+      indoc! {r#"@font-palette-values --Cooler {
+      font-family: Handover Sans;
+      base-palette: 3;
+      override-colors: 1 #2b0c09, 3 #ee00be;
+      override-colors: 1 #2b0c09, 3 lch(50.998% 135.363 338);
+    }
+    "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"@font-palette-values --Cooler {
+      font-family: Handover Sans;
+      base-palette: 3;
+      override-colors: 1 var(--foo), 3 lch(50.998% 135.363 338);
+    }"#,
+      indoc! {r#"@font-palette-values --Cooler {
+      font-family: Handover Sans;
+      base-palette: 3;
+      override-colors: 1 var(--foo), 3 #ee00be;
+    }
+
+    @supports (color: lab(0% 0 0)) {
+      @font-palette-values --Cooler {
+        font-family: Handover Sans;
+        base-palette: 3;
+        override-colors: 1 var(--foo), 3 lab(50.998% 125.506 -50.7078);
+      }
+    }
+    "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"@supports (color: lab(0% 0 0)) {
+      @font-palette-values --Cooler {
+        font-family: Handover Sans;
+        base-palette: 3;
+        override-colors: 1 var(--foo), 3 lab(50.998% 125.506 -50.7078);
+      }
+    }"#,
+      indoc! {r#"@supports (color: lab(0% 0 0)) {
+      @font-palette-values --Cooler {
+        font-family: Handover Sans;
+        base-palette: 3;
+        override-colors: 1 var(--foo), 3 lab(50.998% 125.506 -50.7078);
+      }
+    }
+    "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+    minify_test(".foo { font-palette: --Custom; }", ".foo{font-palette:--Custom}");
+  }
+
+  #[test]
+  fn test_font_feature_values() {
+    // https://github.com/clagnut/TODS/blob/e693d52ad411507b960cf01a9734265e3efab102/tods.css#L116-L142
+    minify_test(
+      r#"
+@font-feature-values "Fancy Font Name" {
+  @styleset { cursive: 1; swoopy: 7 16; }
+  @character-variant { ampersand: 1; capital-q: 2; }
+  @stylistic { two-story-g: 1; straight-y: 2; }
+  @swash { swishy: 1; flowing: 2; }
+  @ornaments { clover: 1; fleuron: 2; }
+  @annotation { circled: 1; boxed: 2; }
+}
+    "#,
+      r#"@font-feature-values Fancy Font Name{@styleset{cursive:1;swoopy:7 16}@character-variant{ampersand:1;capital-q:2}@stylistic{two-story-g:1;straight-y:2}@swash{swishy:1;flowing:2}@ornaments{clover:1;fleuron:2}@annotation{circled:1;boxed:2}}"#,
+    );
+
+    // https://github.com/Sorixelle/srxl.me/blob/4eb4f4a15cb2d21356df24c096d6a819cfdc1a99/public/fonts/inter/inter.css#L201-L222
+    minify_test(
+      r#"
+@font-feature-values "Inter", "Inter var", "Inter var experimental" {
+  @styleset {
+    open-digits: 1;
+    disambiguation: 2;
+    curved-r: 3;
+    disambiguation-without-zero: 4;
+  }
+
+  @character-variant {
+    alt-one: 1;
+    open-four: 2;
+    open-six: 3;
+    open-nine: 4;
+    lower-l-with-tail: 5;
+    curved-lower-r: 6;
+    german-double-s: 7;
+    upper-i-with-serif: 8;
+    flat-top-three: 9;
+    upper-g-with-spur: 10;
+    single-storey-a: 11;
+  }
+}
+      "#,
+      r#"@font-feature-values Inter,Inter var,Inter var experimental{@styleset{open-digits:1;disambiguation:2;curved-r:3;disambiguation-without-zero:4}@character-variant{alt-one:1;open-four:2;open-six:3;open-nine:4;lower-l-with-tail:5;curved-lower-r:6;german-double-s:7;upper-i-with-serif:8;flat-top-three:9;upper-g-with-spur:10;single-storey-a:11}}"#,
+    );
+
+    // https://github.com/MihailJP/Inconsolata-LGC/blob/7c53cf455787096c93d82d9a51018f12ec39a6e9/Inconsolata-LGC.css#L65-L91
+    minify_test(
+      r#"
+@font-feature-values "Inconsolata LGC" {
+	@styleset {
+		alternative-umlaut: 1;
+	}
+	@character-variant {
+		zero-plain: 1 1;
+		zero-dotted: 1 2;
+		zero-longslash: 1 3;
+		r-with-serif: 2 1;
+		eng-descender: 3 1;
+		eng-uppercase: 3 2;
+		dollar-open: 4 1;
+		dollar-oldstyle: 4 2;
+		dollar-cifrao: 4 2;
+		ezh-no-descender: 5 1;
+		ezh-reversed-sigma: 5 2;
+		triangle-text-form: 6 1;
+		el-with-hook-old: 7 1;
+		qa-enlarged-lowercase: 8 1;
+		qa-reversed-p: 8 2;
+		che-with-hook: 9 1;
+		che-with-hook-alt: 9 2;
+		ge-with-hook: 10 1;
+		ge-with-hook-alt: 10 2;
+		ge-with-stroke-and-descender: 11 1;
+	}
+}
+    "#,
+      r#"@font-feature-values Inconsolata LGC{@styleset{alternative-umlaut:1}@character-variant{zero-plain:1 1;zero-dotted:1 2;zero-longslash:1 3;r-with-serif:2 1;eng-descender:3 1;eng-uppercase:3 2;dollar-open:4 1;dollar-oldstyle:4 2;dollar-cifrao:4 2;ezh-no-descender:5 1;ezh-reversed-sigma:5 2;triangle-text-form:6 1;el-with-hook-old:7 1;qa-enlarged-lowercase:8 1;qa-reversed-p:8 2;che-with-hook:9 1;che-with-hook-alt:9 2;ge-with-hook:10 1;ge-with-hook-alt:10 2;ge-with-stroke-and-descender:11 1}}"#,
+    );
+
+    minify_test(
+      r#"
+      @font-feature-values "Fancy Font Name" {
+        @styleset { cursive: 1; swoopy: 7 16; }
+        @character-variant { ampersand: 1; capital-q: 2; }
+      }
+      "#,
+      r#"@font-feature-values Fancy Font Name{@styleset{cursive:1;swoopy:7 16}@character-variant{ampersand:1;capital-q:2}}"#,
+    );
+    minify_test(
+      r#"
+      @font-feature-values foo {
+          @swash { pretty: 0; pretty: 1; cool: 2; }
+      }
+      "#,
+      "@font-feature-values foo{@swash{pretty:1;cool:2}}",
+    );
+    minify_test(
+      r#"
+      @font-feature-values foo {
+          @swash { pretty: 1; }
+          @swash { cool: 2; }
+      }
+      "#,
+      "@font-feature-values foo{@swash{pretty:1;cool:2}}",
+    );
+    minify_test(
+      r#"
+      @font-feature-values foo {
+          @swash { pretty: 1; }
+      }
+      @font-feature-values foo {
+          @swash { cool: 2; }
+      }
+      "#,
+      "@font-feature-values foo{@swash{pretty:1;cool:2}}",
+    );
+  }
+
+  #[test]
+  fn test_page_rule() {
+    minify_test("@page {margin: 0.5cm}", "@page{margin:.5cm}");
+    minify_test("@page :left {margin: 0.5cm}", "@page:left{margin:.5cm}");
+    minify_test("@page :right {margin: 0.5cm}", "@page:right{margin:.5cm}");
+    minify_test(
+      "@page LandscapeTable {margin: 0.5cm}",
+      "@page LandscapeTable{margin:.5cm}",
+    );
+    minify_test(
+      "@page CompanyLetterHead:first {margin: 0.5cm}",
+      "@page CompanyLetterHead:first{margin:.5cm}",
+    );
+    minify_test("@page:first {margin: 0.5cm}", "@page:first{margin:.5cm}");
+    minify_test("@page :blank:first {margin: 0.5cm}", "@page:blank:first{margin:.5cm}");
+    minify_test("@page toc, index {margin: 0.5cm}", "@page toc,index{margin:.5cm}");
+    minify_test(
+      r#"
+    @page :right {
+      @bottom-left {
+        margin: 10pt;
+      }
+    }
+    "#,
+      "@page:right{@bottom-left{margin:10pt}}",
+    );
+    minify_test(
+      r#"
+    @page :right {
+      margin: 1in;
+
+      @bottom-left {
+        margin: 10pt;
+      }
+    }
+    "#,
+      "@page:right{margin:1in;@bottom-left{margin:10pt}}",
+    );
+
+    test(
+      r#"
+    @page :right {
+      @bottom-left {
+        margin: 10pt;
+      }
+    }
+    "#,
+      indoc! {r#"
+      @page :right {
+        @bottom-left {
+          margin: 10pt;
+        }
+      }
+      "#},
+    );
+
+    test(
+      r#"
+    @page :right {
+      margin: 1in;
+
+      @bottom-left-corner { content: "Foo"; }
+      @bottom-right-corner { content: "Bar"; }
+    }
+    "#,
+      indoc! {r#"
+      @page :right {
+        margin: 1in;
+
+        @bottom-left-corner {
+          content: "Foo";
+        }
+
+        @bottom-right-corner {
+          content: "Bar";
+        }
+      }
+      "#},
+    );
+
+    error_test(
+      r#"
+      @page {
+        @foo {
+          margin: 1in;
+        }
+      }
+      "#,
+      ParserError::AtRuleInvalid("foo".into()),
+    );
+
+    error_test(
+      r#"
+      @page {
+        @top-left-corner {
+          @bottom-left {
+            margin: 1in;
+          }
+        }
+      }
+      "#,
+      ParserError::AtRuleInvalid("bottom-left".into()),
+    );
+  }
+
+  #[test]
+  fn test_supports_rule() {
+    test(
+      r#"
+      @supports (foo: bar) {
+        .test {
+          foo: bar;
+        }
+      }
+    "#,
+      indoc! { r#"
+      @supports (foo: bar) {
+        .test {
+          foo: bar;
+        }
+      }
+    "#},
+    );
+    test(
+      r#"
+      @supports not (foo: bar) {
+        .test {
+          foo: bar;
+        }
+      }
+    "#,
+      indoc! { r#"
+      @supports not (foo: bar) {
+        .test {
+          foo: bar;
+        }
+      }
+    "#},
+    );
+    test(
+      r#"
+      @supports (foo: bar) or (bar: baz) {
+        .test {
+          foo: bar;
+        }
+      }
+    "#,
+      indoc! { r#"
+      @supports (foo: bar) or (bar: baz) {
+        .test {
+          foo: bar;
+        }
+      }
+    "#},
+    );
+    test(
+      r#"
+      @supports (((foo: bar) or (bar: baz))) {
+        .test {
+          foo: bar;
+        }
+      }
+    "#,
+      indoc! { r#"
+      @supports (foo: bar) or (bar: baz) {
+        .test {
+          foo: bar;
+        }
+      }
+    "#},
+    );
+    test(
+      r#"
+      @supports (foo: bar) and (bar: baz) {
+        .test {
+          foo: bar;
+        }
+      }
+    "#,
+      indoc! { r#"
+      @supports (foo: bar) and (bar: baz) {
+        .test {
+          foo: bar;
+        }
+      }
+    "#},
+    );
+    test(
+      r#"
+      @supports (((foo: bar) and (bar: baz))) {
+        .test {
+          foo: bar;
+        }
+      }
+    "#,
+      indoc! { r#"
+      @supports (foo: bar) and (bar: baz) {
+        .test {
+          foo: bar;
+        }
+      }
+    "#},
+    );
+    test(
+      r#"
+      @supports (foo: bar) and (((bar: baz) or (test: foo))) {
+        .test {
+          foo: bar;
+        }
+      }
+    "#,
+      indoc! { r#"
+      @supports (foo: bar) and ((bar: baz) or (test: foo)) {
+        .test {
+          foo: bar;
+        }
+      }
+    "#},
+    );
+    test(
+      r#"
+      @supports not (((foo: bar) and (bar: baz))) {
+        .test {
+          foo: bar;
+        }
+      }
+    "#,
+      indoc! { r#"
+      @supports not ((foo: bar) and (bar: baz)) {
+        .test {
+          foo: bar;
+        }
+      }
+    "#},
+    );
+    test(
+      r#"
+      @supports selector(a > b) {
+        .test {
+          foo: bar;
+        }
+      }
+    "#,
+      indoc! { r#"
+      @supports selector(a > b) {
+        .test {
+          foo: bar;
+        }
+      }
+    "#},
+    );
+    test(
+      r#"
+      @supports unknown(test) {
+        .test {
+          foo: bar;
+        }
+      }
+    "#,
+      indoc! { r#"
+      @supports unknown(test) {
+        .test {
+          foo: bar;
+        }
+      }
+    "#},
+    );
+    test(
+      r#"
+      @supports (unknown) {
+        .test {
+          foo: bar;
+        }
+      }
+    "#,
+      indoc! { r#"
+      @supports (unknown) {
+        .test {
+          foo: bar;
+        }
+      }
+    "#},
+    );
+    test(
+      r#"
+      @supports (display: grid) and (not (display: inline-grid)) {
+        .test {
+          foo: bar;
+        }
+      }
+    "#,
+      indoc! { r#"
+      @supports (display: grid) and (not (display: inline-grid)) {
+        .test {
+          foo: bar;
+        }
+      }
+    "#},
+    );
+    prefix_test(
+      r#"
+      @supports (backdrop-filter: blur(10px)) {
+        div {
+          backdrop-filter: blur(10px);
+        }
+      }
+    "#,
+      indoc! { r#"
+      @supports ((-webkit-backdrop-filter: blur(10px)) or (backdrop-filter: blur(10px))) {
+        div {
+          -webkit-backdrop-filter: blur(10px);
+          backdrop-filter: blur(10px);
+        }
+      }
+    "#},
+      Browsers {
+        safari: Some(14 << 16),
+        ..Default::default()
+      },
+    );
+    prefix_test(
+      r#"
+      @supports ((-webkit-backdrop-filter: blur(10px)) or (backdrop-filter: blur(10px))) {
+        div {
+          backdrop-filter: blur(10px);
+        }
+      }
+    "#,
+      indoc! { r#"
+      @supports ((-webkit-backdrop-filter: blur(10px)) or (backdrop-filter: blur(10px))) {
+        div {
+          -webkit-backdrop-filter: blur(10px);
+          backdrop-filter: blur(10px);
+        }
+      }
+    "#},
+      Browsers {
+        safari: Some(14 << 16),
+        ..Default::default()
+      },
+    );
+    prefix_test(
+      r#"
+      @supports ((-webkit-backdrop-filter: blur(20px)) or (backdrop-filter: blur(10px))) {
+        div {
+          backdrop-filter: blur(10px);
+        }
+      }
+    "#,
+      indoc! { r#"
+      @supports ((-webkit-backdrop-filter: blur(20px))) or ((-webkit-backdrop-filter: blur(10px)) or (backdrop-filter: blur(10px))) {
+        div {
+          -webkit-backdrop-filter: blur(10px);
+          backdrop-filter: blur(10px);
+        }
+      }
+    "#},
+      Browsers {
+        safari: Some(14 << 16),
+        ..Default::default()
+      },
+    );
+    prefix_test(
+      r#"
+      @supports ((-webkit-backdrop-filter: blur(10px)) or (backdrop-filter: blur(10px))) {
+        div {
+          backdrop-filter: blur(10px);
+        }
+      }
+    "#,
+      indoc! { r#"
+      @supports (backdrop-filter: blur(10px)) {
+        div {
+          backdrop-filter: blur(10px);
+        }
+      }
+    "#},
+      Browsers {
+        chrome: Some(80 << 16),
+        ..Default::default()
+      },
+    );
+    minify_test(
+      r#"
+      @supports (width: calc(10px * 2)) {
+        .test {
+          width: calc(10px * 2);
+        }
+      }
+    "#,
+      "@supports (width:calc(10px * 2)){.test{width:20px}}",
+    );
+    minify_test(
+      r#"
+      @supports (color: hsl(0deg, 0%, 0%)) {
+        .test {
+          color: hsl(0deg, 0%, 0%);
+        }
+      }
+    "#,
+      "@supports (color:hsl(0deg, 0%, 0%)){.test{color:#000}}",
+    );
+  }
+
+  #[test]
+  fn test_counter_style() {
+    test(
+      r#"
+      @counter-style circled-alpha {
+        system: fixed;
+        symbols: Ⓐ Ⓑ Ⓒ;
+        suffix: " ";
+      }
+    "#,
+      indoc! { r#"
+      @counter-style circled-alpha {
+        system: fixed;
+        symbols: Ⓐ Ⓑ Ⓒ;
+        suffix: " ";
+      }
+    "#},
+    );
+  }
+
+  #[test]
+  fn test_namespace() {
+    minify_test(
+      "@namespace url(http://toto.example.org);",
+      "@namespace \"http://toto.example.org\";",
+    );
+    minify_test(
+      "@namespace \"http://toto.example.org\";",
+      "@namespace \"http://toto.example.org\";",
+    );
+    minify_test(
+      "@namespace toto \"http://toto.example.org\";",
+      "@namespace toto \"http://toto.example.org\";",
+    );
+    minify_test(
+      "@namespace toto url(http://toto.example.org);",
+      "@namespace toto \"http://toto.example.org\";",
+    );
+
+    test(
+      r#"
+      @namespace "http://example.com/foo";
+
+      x {
+        color: red;
+      }
+    "#,
+      indoc! {r#"
+      @namespace "http://example.com/foo";
+
+      x {
+        color: red;
+      }
+    "#},
+    );
+
+    test(
+      r#"
+      @namespace toto "http://toto.example.org";
+
+      toto|x {
+        color: red;
+      }
+
+      [toto|att=val] {
+        color: blue
+      }
+    "#,
+      indoc! {r#"
+      @namespace toto "http://toto.example.org";
+
+      toto|x {
+        color: red;
+      }
+
+      [toto|att="val"] {
+        color: #00f;
+      }
+    "#},
+    );
+
+    test(
+      r#"
+      @namespace "http://example.com/foo";
+
+      |x {
+        color: red;
+      }
+
+      [|att=val] {
+        color: blue
+      }
+    "#,
+      indoc! {r#"
+      @namespace "http://example.com/foo";
+
+      |x {
+        color: red;
+      }
+
+      [att="val"] {
+        color: #00f;
+      }
+    "#},
+    );
+
+    test(
+      r#"
+      @namespace "http://example.com/foo";
+
+      *|x {
+        color: red;
+      }
+
+      [*|att=val] {
+        color: blue
+      }
+    "#,
+      indoc! {r#"
+      @namespace "http://example.com/foo";
+
+      *|x {
+        color: red;
+      }
+
+      [*|att="val"] {
+        color: #00f;
+      }
+    "#},
+    );
+
+    error_test(
+      ".foo { color: red } @namespace \"http://example.com/foo\";",
+      ParserError::UnexpectedNamespaceRule,
+    );
+  }
+
+  #[test]
+  fn test_import() {
+    minify_test("@import url(foo.css);", "@import \"foo.css\";");
+    minify_test("@import \"foo.css\";", "@import \"foo.css\";");
+    minify_test("@import url(foo.css) print;", "@import \"foo.css\" print;");
+    minify_test("@import \"foo.css\" print;", "@import \"foo.css\" print;");
+    minify_test(
+      "@import \"foo.css\" screen and (orientation: landscape);",
+      "@import \"foo.css\" screen and (orientation:landscape);",
+    );
+    minify_test(
+      "@import url(foo.css) supports(display: flex);",
+      "@import \"foo.css\" supports(display:flex);",
+    );
+    minify_test(
+      "@import url(foo.css) supports(display: flex) print;",
+      "@import \"foo.css\" supports(display:flex) print;",
+    );
+    minify_test(
+      "@import url(foo.css) supports(not (display: flex));",
+      "@import \"foo.css\" supports(not (display:flex));",
+    );
+    minify_test(
+      "@import url(foo.css) supports((display: flex));",
+      "@import \"foo.css\" supports(display:flex);",
+    );
+    minify_test("@charset \"UTF-8\"; @import url(foo.css);", "@import \"foo.css\";");
+    minify_test("@layer foo; @import url(foo.css);", "@layer foo;@import \"foo.css\";");
+    error_test(
+      ".foo { color: red } @import url(bar.css);",
+      ParserError::UnexpectedImportRule,
+    );
+    error_test(
+      "@namespace \"http://example.com/foo\"; @import url(bar.css);",
+      ParserError::UnexpectedImportRule,
+    );
+    error_test(
+      "@media print { .foo { color: red }} @import url(bar.css);",
+      ParserError::UnexpectedImportRule,
+    );
+    error_test(
+      "@layer foo; @import url(foo.css); @layer bar; @import url(bar.css)",
+      ParserError::UnexpectedImportRule,
+    );
+  }
+
+  #[test]
+  fn test_prefixes() {
+    prefix_test(
+      r#"
+      .foo {
+        -webkit-transition: opacity 200ms;
+        -moz-transition: opacity 200ms;
+        transition: opacity 200ms;
+      }
+      "#,
+      indoc! {r#"
+      .foo {
+        transition: opacity .2s;
+      }
+      "#},
+      Browsers {
+        chrome: Some(95 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo{transition:opacity 200ms}",
+      indoc! {r#"
+      .foo {
+        -webkit-transition: opacity .2s;
+        -moz-transition: opacity .2s;
+        transition: opacity .2s;
+      }
+      "#},
+      Browsers {
+        safari: Some(5 << 16),
+        firefox: Some(14 << 16),
+        ..Browsers::default()
+      },
+    );
+  }
+
+  #[test]
+  fn test_display() {
+    minify_test(".foo { display: block }", ".foo{display:block}");
+    minify_test(".foo { display: block flow }", ".foo{display:block}");
+    minify_test(".foo { display: flow-root }", ".foo{display:flow-root}");
+    minify_test(".foo { display: block flow-root }", ".foo{display:flow-root}");
+    minify_test(".foo { display: inline }", ".foo{display:inline}");
+    minify_test(".foo { display: inline flow }", ".foo{display:inline}");
+    minify_test(".foo { display: inline-block }", ".foo{display:inline-block}");
+    minify_test(".foo { display: inline flow-root }", ".foo{display:inline-block}");
+    minify_test(".foo { display: run-in }", ".foo{display:run-in}");
+    minify_test(".foo { display: run-in flow }", ".foo{display:run-in}");
+    minify_test(".foo { display: list-item }", ".foo{display:list-item}");
+    minify_test(".foo { display: block flow list-item }", ".foo{display:list-item}");
+    minify_test(".foo { display: inline list-item }", ".foo{display:inline list-item}");
+    minify_test(
+      ".foo { display: inline flow list-item }",
+      ".foo{display:inline list-item}",
+    );
+    minify_test(".foo { display: flex }", ".foo{display:flex}");
+    minify_test(".foo { display: block flex }", ".foo{display:flex}");
+    minify_test(".foo { display: inline-flex }", ".foo{display:inline-flex}");
+    minify_test(".foo { display: inline flex }", ".foo{display:inline-flex}");
+    minify_test(".foo { display: grid }", ".foo{display:grid}");
+    minify_test(".foo { display: block grid }", ".foo{display:grid}");
+    minify_test(".foo { display: inline-grid }", ".foo{display:inline-grid}");
+    minify_test(".foo { display: inline grid }", ".foo{display:inline-grid}");
+    minify_test(".foo { display: ruby }", ".foo{display:ruby}");
+    minify_test(".foo { display: inline ruby }", ".foo{display:ruby}");
+    minify_test(".foo { display: block ruby }", ".foo{display:block ruby}");
+    minify_test(".foo { display: table }", ".foo{display:table}");
+    minify_test(".foo { display: block table }", ".foo{display:table}");
+    minify_test(".foo { display: inline-table }", ".foo{display:inline-table}");
+    minify_test(".foo { display: inline table }", ".foo{display:inline-table}");
+    minify_test(".foo { display: table-row-group }", ".foo{display:table-row-group}");
+    minify_test(".foo { display: contents }", ".foo{display:contents}");
+    minify_test(".foo { display: none }", ".foo{display:none}");
+    minify_test(".foo { display: -webkit-flex }", ".foo{display:-webkit-flex}");
+    minify_test(".foo { display: -ms-flexbox }", ".foo{display:-ms-flexbox}");
+    minify_test(".foo { display: -webkit-box }", ".foo{display:-webkit-box}");
+    minify_test(".foo { display: -moz-box }", ".foo{display:-moz-box}");
+    minify_test(
+      ".foo { display: -webkit-flex; display: -moz-box; display: flex }",
+      ".foo{display:-webkit-flex;display:-moz-box;display:flex}",
+    );
+    minify_test(
+      ".foo { display: -webkit-flex; display: flex; display: -moz-box }",
+      ".foo{display:-webkit-flex;display:flex;display:-moz-box}",
+    );
+    minify_test(".foo { display: flex; display: grid }", ".foo{display:grid}");
+    minify_test(
+      ".foo { display: -webkit-inline-flex; display: -moz-inline-box; display: inline-flex }",
+      ".foo{display:-webkit-inline-flex;display:-moz-inline-box;display:inline-flex}",
+    );
+    minify_test(
+      ".foo { display: flex; display: var(--grid); }",
+      ".foo{display:flex;display:var(--grid)}",
+    );
+    prefix_test(
+      ".foo{ display: flex }",
+      indoc! {r#"
+      .foo {
+        display: -webkit-box;
+        display: -moz-box;
+        display: -webkit-flex;
+        display: -ms-flexbox;
+        display: flex;
+      }
+      "#},
+      Browsers {
+        safari: Some(4 << 16),
+        firefox: Some(14 << 16),
+        ie: Some(10 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      ".foo{ display: flex; display: -webkit-box; }",
+      indoc! {r#"
+      .foo {
+        display: -webkit-box;
+      }
+      "#},
+      Browsers {
+        safari: Some(4 << 16),
+        firefox: Some(14 << 16),
+        ie: Some(10 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      ".foo{ display: -webkit-box; display: flex; }",
+      indoc! {r#"
+      .foo {
+        display: -webkit-box;
+        display: -moz-box;
+        display: -webkit-flex;
+        display: -ms-flexbox;
+        display: flex;
+      }
+      "#},
+      Browsers {
+        safari: Some(4 << 16),
+        firefox: Some(14 << 16),
+        ie: Some(10 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        display: -webkit-box;
+        display: -moz-box;
+        display: -webkit-flex;
+        display: -ms-flexbox;
+        display: flex;
+      }
+      "#,
+      indoc! {r#"
+      .foo {
+        display: flex;
+      }
+      "#},
+      Browsers {
+        safari: Some(14 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        display: -webkit-box;
+        display: flex;
+        display: -moz-box;
+        display: -webkit-flex;
+        display: -ms-flexbox;
+      }
+      "#,
+      indoc! {r#"
+      .foo {
+        display: -moz-box;
+        display: -webkit-flex;
+        display: -ms-flexbox;
+      }
+      "#},
+      Browsers {
+        safari: Some(14 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      ".foo{ display: inline-flex }",
+      indoc! {r#"
+      .foo {
+        display: -webkit-inline-box;
+        display: -moz-inline-box;
+        display: -webkit-inline-flex;
+        display: -ms-inline-flexbox;
+        display: inline-flex;
+      }
+      "#},
+      Browsers {
+        safari: Some(4 << 16),
+        firefox: Some(14 << 16),
+        ie: Some(10 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        display: -webkit-inline-box;
+        display: -moz-inline-box;
+        display: -webkit-inline-flex;
+        display: -ms-inline-flexbox;
+        display: inline-flex;
+      }
+      "#,
+      indoc! {r#"
+      .foo {
+        display: inline-flex;
+      }
+      "#},
+      Browsers {
+        safari: Some(14 << 16),
+        ..Browsers::default()
+      },
+    );
+  }
+
+  #[test]
+  fn test_visibility() {
+    minify_test(".foo { visibility: visible }", ".foo{visibility:visible}");
+    minify_test(".foo { visibility: hidden }", ".foo{visibility:hidden}");
+    minify_test(".foo { visibility: collapse }", ".foo{visibility:collapse}");
+    minify_test(".foo { visibility: Visible }", ".foo{visibility:visible}");
+  }
+
+  #[test]
+  fn test_text_transform() {
+    minify_test(".foo { text-transform: uppercase }", ".foo{text-transform:uppercase}");
+    minify_test(".foo { text-transform: lowercase }", ".foo{text-transform:lowercase}");
+    minify_test(".foo { text-transform: capitalize }", ".foo{text-transform:capitalize}");
+    minify_test(".foo { text-transform: none }", ".foo{text-transform:none}");
+    minify_test(".foo { text-transform: full-width }", ".foo{text-transform:full-width}");
+    minify_test(
+      ".foo { text-transform: full-size-kana }",
+      ".foo{text-transform:full-size-kana}",
+    );
+    minify_test(
+      ".foo { text-transform: uppercase full-width }",
+      ".foo{text-transform:uppercase full-width}",
+    );
+    minify_test(
+      ".foo { text-transform: full-width uppercase }",
+      ".foo{text-transform:uppercase full-width}",
+    );
+    minify_test(
+      ".foo { text-transform: uppercase full-width full-size-kana }",
+      ".foo{text-transform:uppercase full-width full-size-kana}",
+    );
+    minify_test(
+      ".foo { text-transform: full-width uppercase full-size-kana }",
+      ".foo{text-transform:uppercase full-width full-size-kana}",
+    );
+  }
+
+  #[test]
+  fn test_whitespace() {
+    minify_test(".foo { white-space: normal }", ".foo{white-space:normal}");
+    minify_test(".foo { white-space: pre }", ".foo{white-space:pre}");
+    minify_test(".foo { white-space: nowrap }", ".foo{white-space:nowrap}");
+    minify_test(".foo { white-space: pre-wrap }", ".foo{white-space:pre-wrap}");
+    minify_test(".foo { white-space: break-spaces }", ".foo{white-space:break-spaces}");
+    minify_test(".foo { white-space: pre-line }", ".foo{white-space:pre-line}");
+    minify_test(".foo { white-space: NoWrAp }", ".foo{white-space:nowrap}");
+  }
+
+  #[test]
+  fn test_tab_size() {
+    minify_test(".foo { tab-size: 8 }", ".foo{tab-size:8}");
+    minify_test(".foo { tab-size: 4px }", ".foo{tab-size:4px}");
+    minify_test(".foo { -moz-tab-size: 4px }", ".foo{-moz-tab-size:4px}");
+    minify_test(".foo { -o-tab-size: 4px }", ".foo{-o-tab-size:4px}");
+    prefix_test(
+      ".foo{ tab-size: 4 }",
+      indoc! {r#"
+      .foo {
+        -moz-tab-size: 4;
+        -o-tab-size: 4;
+        tab-size: 4;
+      }
+      "#},
+      Browsers {
+        safari: Some(8 << 16),
+        firefox: Some(50 << 16),
+        opera: Some(12 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        -moz-tab-size: 4;
+        -o-tab-size: 4;
+        tab-size: 4;
+      }
+      "#,
+      indoc! {r#"
+      .foo {
+        tab-size: 4;
+      }
+      "#},
+      Browsers {
+        safari: Some(8 << 16),
+        firefox: Some(94 << 16),
+        opera: Some(30 << 16),
+        ..Browsers::default()
+      },
+    );
+  }
+
+  #[test]
+  fn test_word_break() {
+    minify_test(".foo { word-break: normal }", ".foo{word-break:normal}");
+    minify_test(".foo { word-break: keep-all }", ".foo{word-break:keep-all}");
+    minify_test(".foo { word-break: break-all }", ".foo{word-break:break-all}");
+    minify_test(".foo { word-break: break-word }", ".foo{word-break:break-word}");
+  }
+
+  #[test]
+  fn test_line_break() {
+    minify_test(".foo { line-break: auto }", ".foo{line-break:auto}");
+    minify_test(".foo { line-break: Loose }", ".foo{line-break:loose}");
+    minify_test(".foo { line-break: anywhere }", ".foo{line-break:anywhere}");
+  }
+
+  #[test]
+  fn test_wrap() {
+    minify_test(".foo { overflow-wrap: nOrmal }", ".foo{overflow-wrap:normal}");
+    minify_test(".foo { overflow-wrap: break-Word }", ".foo{overflow-wrap:break-word}");
+    minify_test(".foo { overflow-wrap: Anywhere }", ".foo{overflow-wrap:anywhere}");
+    minify_test(".foo { word-wrap: Normal }", ".foo{word-wrap:normal}");
+    minify_test(".foo { word-wrap: Break-wOrd }", ".foo{word-wrap:break-word}");
+    minify_test(".foo { word-wrap: Anywhere }", ".foo{word-wrap:anywhere}");
+  }
+
+  #[test]
+  fn test_hyphens() {
+    minify_test(".foo { hyphens: manual }", ".foo{hyphens:manual}");
+    minify_test(".foo { hyphens: auto }", ".foo{hyphens:auto}");
+    minify_test(".foo { hyphens: none }", ".foo{hyphens:none}");
+    minify_test(".foo { -webkit-hyphens: manual }", ".foo{-webkit-hyphens:manual}");
+    minify_test(".foo { -moz-hyphens: manual }", ".foo{-moz-hyphens:manual}");
+    minify_test(".foo { -ms-hyphens: manual }", ".foo{-ms-hyphens:manual}");
+    prefix_test(
+      ".foo{ hyphens: manual }",
+      indoc! {r#"
+      .foo {
+        -webkit-hyphens: manual;
+        -moz-hyphens: manual;
+        -ms-hyphens: manual;
+        hyphens: manual;
+      }
+      "#},
+      Browsers {
+        safari: Some(14 << 16),
+        firefox: Some(40 << 16),
+        ie: Some(10 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        -webkit-hyphens: manual;
+        -moz-hyphens: manual;
+        -ms-hyphens: manual;
+        hyphens: manual;
+      }
+      "#,
+      indoc! {r#"
+      .foo {
+        -webkit-hyphens: manual;
+        hyphens: manual;
+      }
+      "#},
+      Browsers {
+        safari: Some(14 << 16),
+        chrome: Some(88 << 16),
+        firefox: Some(88 << 16),
+        edge: Some(79 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        -webkit-hyphens: manual;
+        -moz-hyphens: manual;
+        -ms-hyphens: manual;
+        hyphens: manual;
+      }
+      "#,
+      indoc! {r#"
+      .foo {
+        hyphens: manual;
+      }
+      "#},
+      Browsers {
+        chrome: Some(88 << 16),
+        firefox: Some(88 << 16),
+        edge: Some(79 << 16),
+        ..Browsers::default()
+      },
+    );
+  }
+
+  #[test]
+  fn test_text_align() {
+    minify_test(".foo { text-align: left }", ".foo{text-align:left}");
+    minify_test(".foo { text-align: Left }", ".foo{text-align:left}");
+    minify_test(".foo { text-align: END }", ".foo{text-align:end}");
+    minify_test(".foo { text-align: left }", ".foo{text-align:left}");
+
+    prefix_test(
+      r#"
+      .foo {
+        text-align: start;
+      }
+    "#,
+      indoc! {r#"
+      .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) {
+        text-align: left;
+      }
+
+      .foo:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) {
+        text-align: right;
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(2 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        text-align: end;
+      }
+    "#,
+      indoc! {r#"
+      .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) {
+        text-align: right;
+      }
+
+      .foo:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) {
+        text-align: left;
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(2 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        text-align: start;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        text-align: start;
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(14 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo > .bar {
+        text-align: start;
+      }
+    "#,
+      indoc! {r#"
+      .foo > .bar:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) {
+        text-align: left;
+      }
+
+      .foo > .bar:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) {
+        text-align: right;
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(2 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo:after {
+        text-align: start;
+      }
+    "#,
+      indoc! {r#"
+      .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))):after {
+        text-align: left;
+      }
+
+      .foo:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)):after {
+        text-align: right;
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(2 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo:hover {
+        text-align: start;
+      }
+    "#,
+      indoc! {r#"
+      .foo:hover:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) {
+        text-align: left;
+      }
+
+      .foo:hover:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) {
+        text-align: right;
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(2 << 16),
+        ..Browsers::default()
+      },
+    );
+  }
+
+  #[test]
+  fn test_text_align_last() {
+    minify_test(".foo { text-align-last: left }", ".foo{text-align-last:left}");
+    minify_test(".foo { text-align-last: justify }", ".foo{text-align-last:justify}");
+    prefix_test(
+      ".foo{ text-align-last: left }",
+      indoc! {r#"
+      .foo {
+        -moz-text-align-last: left;
+        text-align-last: left;
+      }
+      "#},
+      Browsers {
+        firefox: Some(40 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        -moz-text-align-last: left;
+        text-align-last: left;
+      }
+      "#,
+      indoc! {r#"
+      .foo {
+        text-align-last: left;
+      }
+      "#},
+      Browsers {
+        firefox: Some(88 << 16),
+        ..Browsers::default()
+      },
+    );
+  }
+
+  #[test]
+  fn test_text_justify() {
+    minify_test(".foo { text-justify: auto }", ".foo{text-justify:auto}");
+    minify_test(".foo { text-justify: inter-word }", ".foo{text-justify:inter-word}");
+  }
+
+  #[test]
+  fn test_word_spacing() {
+    minify_test(".foo { word-spacing: normal }", ".foo{word-spacing:normal}");
+    minify_test(".foo { word-spacing: 3px }", ".foo{word-spacing:3px}");
+  }
+
+  #[test]
+  fn test_letter_spacing() {
+    minify_test(".foo { letter-spacing: normal }", ".foo{letter-spacing:normal}");
+    minify_test(".foo { letter-spacing: 3px }", ".foo{letter-spacing:3px}");
+  }
+
+  #[test]
+  fn test_text_indent() {
+    minify_test(".foo { text-indent: 20px }", ".foo{text-indent:20px}");
+    minify_test(".foo { text-indent: 10% }", ".foo{text-indent:10%}");
+    minify_test(".foo { text-indent: 3em hanging }", ".foo{text-indent:3em hanging}");
+    minify_test(".foo { text-indent: 3em each-line }", ".foo{text-indent:3em each-line}");
+    minify_test(
+      ".foo { text-indent: 3em hanging each-line }",
+      ".foo{text-indent:3em hanging each-line}",
+    );
+    minify_test(
+      ".foo { text-indent: 3em each-line hanging }",
+      ".foo{text-indent:3em hanging each-line}",
+    );
+    minify_test(
+      ".foo { text-indent: each-line 3em hanging }",
+      ".foo{text-indent:3em hanging each-line}",
+    );
+    minify_test(
+      ".foo { text-indent: each-line hanging 3em }",
+      ".foo{text-indent:3em hanging each-line}",
+    );
+  }
+
+  #[test]
+  fn test_text_size_adjust() {
+    minify_test(".foo { text-size-adjust: none }", ".foo{text-size-adjust:none}");
+    minify_test(".foo { text-size-adjust: auto }", ".foo{text-size-adjust:auto}");
+    minify_test(".foo { text-size-adjust: 80% }", ".foo{text-size-adjust:80%}");
+    prefix_test(
+      r#"
+      .foo {
+        text-size-adjust: none;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        -webkit-text-size-adjust: none;
+        -moz-text-size-adjust: none;
+        -ms-text-size-adjust: none;
+        text-size-adjust: none;
+      }
+    "#},
+      Browsers {
+        ios_saf: Some(16 << 16),
+        edge: Some(15 << 16),
+        firefox: Some(20 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        -webkit-text-size-adjust: none;
+        -moz-text-size-adjust: none;
+        -ms-text-size-adjust: none;
+        text-size-adjust: none;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        text-size-adjust: none;
+      }
+    "#},
+      Browsers {
+        chrome: Some(110 << 16),
+        ..Browsers::default()
+      },
+    );
+  }
+
+  #[test]
+  fn test_text_decoration() {
+    minify_test(".foo { text-decoration-line: none }", ".foo{text-decoration-line:none}");
+    minify_test(
+      ".foo { text-decoration-line: underline }",
+      ".foo{text-decoration-line:underline}",
+    );
+    minify_test(
+      ".foo { text-decoration-line: overline }",
+      ".foo{text-decoration-line:overline}",
+    );
+    minify_test(
+      ".foo { text-decoration-line: line-through }",
+      ".foo{text-decoration-line:line-through}",
+    );
+    minify_test(
+      ".foo { text-decoration-line: blink }",
+      ".foo{text-decoration-line:blink}",
+    );
+    minify_test(
+      ".foo { text-decoration-line: underline overline }",
+      ".foo{text-decoration-line:underline overline}",
+    );
+    minify_test(
+      ".foo { text-decoration-line: overline underline }",
+      ".foo{text-decoration-line:underline overline}",
+    );
+    minify_test(
+      ".foo { text-decoration-line: overline line-through underline }",
+      ".foo{text-decoration-line:underline overline line-through}",
+    );
+    minify_test(
+      ".foo { text-decoration-line: spelling-error }",
+      ".foo{text-decoration-line:spelling-error}",
+    );
+    minify_test(
+      ".foo { text-decoration-line: grammar-error }",
+      ".foo{text-decoration-line:grammar-error}",
+    );
+    minify_test(
+      ".foo { -webkit-text-decoration-line: overline underline }",
+      ".foo{-webkit-text-decoration-line:underline overline}",
+    );
+    minify_test(
+      ".foo { -moz-text-decoration-line: overline underline }",
+      ".foo{-moz-text-decoration-line:underline overline}",
+    );
+
+    minify_test(
+      ".foo { text-decoration-style: solid }",
+      ".foo{text-decoration-style:solid}",
+    );
+    minify_test(
+      ".foo { text-decoration-style: dotted }",
+      ".foo{text-decoration-style:dotted}",
+    );
+    minify_test(
+      ".foo { -webkit-text-decoration-style: solid }",
+      ".foo{-webkit-text-decoration-style:solid}",
+    );
+
+    minify_test(
+      ".foo { text-decoration-color: yellow }",
+      ".foo{text-decoration-color:#ff0}",
+    );
+    minify_test(
+      ".foo { -webkit-text-decoration-color: yellow }",
+      ".foo{-webkit-text-decoration-color:#ff0}",
+    );
+
+    minify_test(".foo { text-decoration: none }", ".foo{text-decoration:none}");
+    minify_test(
+      ".foo { text-decoration: underline dotted }",
+      ".foo{text-decoration:underline dotted}",
+    );
+    minify_test(
+      ".foo { text-decoration: underline dotted yellow }",
+      ".foo{text-decoration:underline dotted #ff0}",
+    );
+    minify_test(
+      ".foo { text-decoration: yellow dotted underline }",
+      ".foo{text-decoration:underline dotted #ff0}",
+    );
+    minify_test(
+      ".foo { text-decoration: underline overline dotted yellow }",
+      ".foo{text-decoration:underline overline dotted #ff0}",
+    );
+    minify_test(
+      ".foo { -webkit-text-decoration: yellow dotted underline }",
+      ".foo{-webkit-text-decoration:underline dotted #ff0}",
+    );
+    minify_test(
+      ".foo { -moz-text-decoration: yellow dotted underline }",
+      ".foo{-moz-text-decoration:underline dotted #ff0}",
+    );
+
+    test(
+      r#"
+      .foo {
+        text-decoration-line: underline;
+        text-decoration-style: dotted;
+        text-decoration-color: yellow;
+        text-decoration-thickness: 2px;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        text-decoration: underline 2px dotted #ff0;
+      }
+    "#},
+    );
+
+    test(
+      r#"
+      .foo {
+        text-decoration: underline;
+        text-decoration-style: dotted;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        text-decoration: underline dotted;
+      }
+    "#},
+    );
+
+    test(
+      r#"
+      .foo {
+        text-decoration: underline;
+        text-decoration-style: var(--style);
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        text-decoration: underline;
+        text-decoration-style: var(--style);
+      }
+    "#},
+    );
+
+    test(
+      r#"
+      .foo {
+        -webkit-text-decoration: underline;
+        -webkit-text-decoration-style: dotted;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        -webkit-text-decoration: underline dotted;
+      }
+    "#},
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        text-decoration: underline dotted;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        -webkit-text-decoration: underline dotted;
+        text-decoration: underline dotted;
+      }
+    "#},
+      Browsers {
+        safari: Some(8 << 16),
+        firefox: Some(30 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        text-decoration-line: underline;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        -webkit-text-decoration-line: underline;
+        -moz-text-decoration-line: underline;
+        text-decoration-line: underline;
+      }
+    "#},
+      Browsers {
+        safari: Some(8 << 16),
+        firefox: Some(30 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        text-decoration-style: dotted;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        -webkit-text-decoration-style: dotted;
+        -moz-text-decoration-style: dotted;
+        text-decoration-style: dotted;
+      }
+    "#},
+      Browsers {
+        safari: Some(8 << 16),
+        firefox: Some(30 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        text-decoration-color: yellow;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        -webkit-text-decoration-color: #ff0;
+        -moz-text-decoration-color: #ff0;
+        text-decoration-color: #ff0;
+      }
+    "#},
+      Browsers {
+        safari: Some(8 << 16),
+        firefox: Some(30 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        text-decoration: underline;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        text-decoration: underline;
+      }
+    "#},
+      Browsers {
+        safari: Some(8 << 16),
+        firefox: Some(30 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        -webkit-text-decoration: underline dotted;
+        text-decoration: underline dotted;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        -webkit-text-decoration: underline dotted;
+        text-decoration: underline dotted;
+      }
+    "#},
+      Browsers {
+        safari: Some(14 << 16),
+        firefox: Some(45 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        text-decoration: double underline;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        -webkit-text-decoration: underline double;
+        text-decoration: underline double;
+      }
+    "#},
+      Browsers {
+        safari: Some(16 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        text-decoration: underline;
+        text-decoration-style: double;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        -webkit-text-decoration: underline double;
+        text-decoration: underline double;
+      }
+    "#},
+      Browsers {
+        safari: Some(16 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        text-decoration: underline;
+        text-decoration-color: red;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        -webkit-text-decoration: underline red;
+        text-decoration: underline red;
+      }
+    "#},
+      Browsers {
+        safari: Some(16 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        text-decoration: var(--test);
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        -webkit-text-decoration: var(--test);
+        text-decoration: var(--test);
+      }
+    "#},
+      Browsers {
+        safari: Some(8 << 16),
+        firefox: Some(30 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    minify_test(
+      ".foo { text-decoration-skip-ink: all }",
+      ".foo{text-decoration-skip-ink:all}",
+    );
+    minify_test(
+      ".foo { -webkit-text-decoration-skip-ink: all }",
+      ".foo{-webkit-text-decoration-skip-ink:all}",
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        text-decoration: lch(50.998% 135.363 338) underline;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        -webkit-text-decoration: underline #ee00be;
+        text-decoration: underline #ee00be;
+        -webkit-text-decoration: underline lch(50.998% 135.363 338);
+        text-decoration: underline lch(50.998% 135.363 338);
+      }
+    "#},
+      Browsers {
+        safari: Some(8 << 16),
+        firefox: Some(30 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        text-decoration-color: lch(50.998% 135.363 338);
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        -webkit-text-decoration-color: #ee00be;
+        -moz-text-decoration-color: #ee00be;
+        text-decoration-color: #ee00be;
+        -webkit-text-decoration-color: lch(50.998% 135.363 338);
+        -moz-text-decoration-color: lch(50.998% 135.363 338);
+        text-decoration-color: lch(50.998% 135.363 338);
+      }
+    "#},
+      Browsers {
+        safari: Some(8 << 16),
+        firefox: Some(30 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        text-decoration: lch(50.998% 135.363 338) var(--style);
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        text-decoration: #ee00be var(--style);
+      }
+
+      @supports (color: lab(0% 0 0)) {
+        .foo {
+          text-decoration: lab(50.998% 125.506 -50.7078) var(--style);
+        }
+      }
+    "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      @supports (color: lab(0% 0 0)) {
+        .foo {
+          text-decoration: lab(50.998% 125.506 -50.7078) var(--style);
+        }
+      }
+    "#,
+      indoc! {r#"
+      @supports (color: lab(0% 0 0)) {
+        .foo {
+          text-decoration: lab(50.998% 125.506 -50.7078) var(--style);
+        }
+      }
+    "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        text-decoration: underline 10px;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        text-decoration: underline;
+        text-decoration-thickness: 10px;
+      }
+    "#},
+      Browsers {
+        safari: Some(15 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        text-decoration: underline 10px;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        text-decoration: underline 10px;
+      }
+    "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        text-decoration: underline 10%;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        text-decoration: underline;
+        text-decoration-thickness: calc(1em / 10);
+      }
+    "#},
+      Browsers {
+        safari: Some(12 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        text-decoration: underline 10%;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        text-decoration: underline 10%;
+      }
+    "#},
+      Browsers {
+        firefox: Some(89 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        text-decoration-thickness: 10%;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        text-decoration-thickness: calc(1em / 10);
+      }
+    "#},
+      Browsers {
+        safari: Some(12 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        text-decoration-thickness: 10%;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        text-decoration-thickness: 10%;
+      }
+    "#},
+      Browsers {
+        firefox: Some(89 << 16),
+        ..Browsers::default()
+      },
+    );
+  }
+
+  #[test]
+  fn test_text_emphasis() {
+    minify_test(".foo { text-emphasis-style: none }", ".foo{text-emphasis-style:none}");
+    minify_test(
+      ".foo { text-emphasis-style: filled }",
+      ".foo{text-emphasis-style:filled}",
+    );
+    minify_test(".foo { text-emphasis-style: open }", ".foo{text-emphasis-style:open}");
+    minify_test(".foo { text-emphasis-style: dot }", ".foo{text-emphasis-style:dot}");
+    minify_test(
+      ".foo { text-emphasis-style: filled dot }",
+      ".foo{text-emphasis-style:dot}",
+    );
+    minify_test(
+      ".foo { text-emphasis-style: dot filled }",
+      ".foo{text-emphasis-style:dot}",
+    );
+    minify_test(
+      ".foo { text-emphasis-style: open dot }",
+      ".foo{text-emphasis-style:open dot}",
+    );
+    minify_test(
+      ".foo { text-emphasis-style: dot open }",
+      ".foo{text-emphasis-style:open dot}",
+    );
+    minify_test(".foo { text-emphasis-style: \"x\" }", ".foo{text-emphasis-style:\"x\"}");
+
+    minify_test(".foo { text-emphasis-color: yellow }", ".foo{text-emphasis-color:#ff0}");
+
+    minify_test(".foo { text-emphasis: none }", ".foo{text-emphasis:none}");
+    minify_test(".foo { text-emphasis: filled }", ".foo{text-emphasis:filled}");
+    minify_test(
+      ".foo { text-emphasis: filled yellow }",
+      ".foo{text-emphasis:filled #ff0}",
+    );
+    minify_test(
+      ".foo { text-emphasis: dot filled yellow }",
+      ".foo{text-emphasis:dot #ff0}",
+    );
+
+    test(
+      r#"
+      .foo {
+        text-emphasis-style: filled;
+        text-emphasis-color: yellow;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        text-emphasis: filled #ff0;
+      }
+    "#},
+    );
+
+    test(
+      r#"
+      .foo {
+        text-emphasis: filled red;
+        text-emphasis-color: yellow;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        text-emphasis: filled #ff0;
+      }
+    "#},
+    );
+
+    test(
+      r#"
+      .foo {
+        text-emphasis: filled yellow;
+        text-emphasis-color: var(--color);
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        text-emphasis: filled #ff0;
+        text-emphasis-color: var(--color);
+      }
+    "#},
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        text-emphasis-style: filled;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        -webkit-text-emphasis-style: filled;
+        text-emphasis-style: filled;
+      }
+    "#},
+      Browsers {
+        safari: Some(10 << 16),
+        chrome: Some(30 << 16),
+        firefox: Some(45 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        -webkit-text-emphasis-style: filled;
+        text-emphasis-style: filled;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        text-emphasis-style: filled;
+      }
+    "#},
+      Browsers {
+        safari: Some(10 << 16),
+        firefox: Some(45 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    minify_test(
+      ".foo { text-emphasis-position: over }",
+      ".foo{text-emphasis-position:over}",
+    );
+    minify_test(
+      ".foo { text-emphasis-position: under }",
+      ".foo{text-emphasis-position:under}",
+    );
+    minify_test(
+      ".foo { text-emphasis-position: over right }",
+      ".foo{text-emphasis-position:over}",
+    );
+    minify_test(
+      ".foo { text-emphasis-position: over left }",
+      ".foo{text-emphasis-position:over left}",
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        text-emphasis-position: over;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        -webkit-text-emphasis-position: over;
+        text-emphasis-position: over;
+      }
+    "#},
+      Browsers {
+        safari: Some(10 << 16),
+        chrome: Some(30 << 16),
+        firefox: Some(45 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        text-emphasis-position: over left;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        text-emphasis-position: over left;
+      }
+    "#},
+      Browsers {
+        safari: Some(10 << 16),
+        chrome: Some(30 << 16),
+        firefox: Some(45 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        text-emphasis-position: var(--test);
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        -webkit-text-emphasis-position: var(--test);
+        text-emphasis-position: var(--test);
+      }
+    "#},
+      Browsers {
+        safari: Some(10 << 16),
+        chrome: Some(30 << 16),
+        firefox: Some(45 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        text-emphasis: filled lch(50.998% 135.363 338);
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        -webkit-text-emphasis: filled #ee00be;
+        text-emphasis: filled #ee00be;
+        -webkit-text-emphasis: filled lch(50.998% 135.363 338);
+        text-emphasis: filled lch(50.998% 135.363 338);
+      }
+    "#},
+      Browsers {
+        chrome: Some(25 << 16),
+        firefox: Some(48 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        text-emphasis-color: lch(50.998% 135.363 338);
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        -webkit-text-emphasis-color: #ee00be;
+        text-emphasis-color: #ee00be;
+        -webkit-text-emphasis-color: lch(50.998% 135.363 338);
+        text-emphasis-color: lch(50.998% 135.363 338);
+      }
+    "#},
+      Browsers {
+        chrome: Some(25 << 16),
+        firefox: Some(48 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        text-emphasis: lch(50.998% 135.363 338) var(--style);
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        text-emphasis: #ee00be var(--style);
+      }
+
+      @supports (color: lab(0% 0 0)) {
+        .foo {
+          text-emphasis: lab(50.998% 125.506 -50.7078) var(--style);
+        }
+      }
+    "#},
+      Browsers {
+        safari: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      @supports (color: lab(0% 0 0)) {
+        .foo {
+          text-emphasis: lab(50.998% 125.506 -50.7078) var(--style);
+        }
+      }
+    "#,
+      indoc! {r#"
+      @supports (color: lab(0% 0 0)) {
+        .foo {
+          text-emphasis: lab(50.998% 125.506 -50.7078) var(--style);
+        }
+      }
+    "#},
+      Browsers {
+        safari: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+  }
+
+  #[test]
+  fn test_text_shadow() {
+    minify_test(
+      ".foo { text-shadow: 1px 1px 2px yellow; }",
+      ".foo{text-shadow:1px 1px 2px #ff0}",
+    );
+    minify_test(
+      ".foo { text-shadow: 1px 1px 2px 3px yellow; }",
+      ".foo{text-shadow:1px 1px 2px 3px #ff0}",
+    );
+    minify_test(
+      ".foo { text-shadow: 1px 1px 0 yellow; }",
+      ".foo{text-shadow:1px 1px #ff0}",
+    );
+    minify_test(
+      ".foo { text-shadow: 1px 1px yellow; }",
+      ".foo{text-shadow:1px 1px #ff0}",
+    );
+    minify_test(
+      ".foo { text-shadow: 1px 1px yellow, 2px 3px red; }",
+      ".foo{text-shadow:1px 1px #ff0,2px 3px red}",
+    );
+
+    prefix_test(
+      ".foo { text-shadow: 12px 12px lab(40% 56.6 39) }",
+      indoc! { r#"
+        .foo {
+          text-shadow: 12px 12px #b32323;
+          text-shadow: 12px 12px lab(40% 56.6 39);
+        }
+      "#},
+      Browsers {
+        chrome: Some(4 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { text-shadow: 12px 12px lab(40% 56.6 39) }",
+      indoc! { r#"
+        .foo {
+          text-shadow: 12px 12px #b32323;
+          text-shadow: 12px 12px color(display-p3 .643308 .192455 .167712);
+          text-shadow: 12px 12px lab(40% 56.6 39);
+        }
+      "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        safari: Some(14 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { text-shadow: 12px 12px lab(40% 56.6 39), 12px 12px yellow }",
+      indoc! { r#"
+        .foo {
+          text-shadow: 12px 12px #b32323, 12px 12px #ff0;
+          text-shadow: 12px 12px lab(40% 56.6 39), 12px 12px #ff0;
+        }
+      "#},
+      Browsers {
+        chrome: Some(4 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { text-shadow: var(--foo) 12px lab(40% 56.6 39) }",
+      indoc! { r#"
+        .foo {
+          text-shadow: var(--foo) 12px #b32323;
+        }
+
+        @supports (color: lab(0% 0 0)) {
+          .foo {
+            text-shadow: var(--foo) 12px lab(40% 56.6 39);
+          }
+        }
+      "#},
+      Browsers {
+        chrome: Some(4 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+        @supports (color: lab(0% 0 0)) {
+          .foo {
+            text-shadow: var(--foo) 12px lab(40% 56.6 39);
+          }
+        }
+      "#,
+      indoc! { r#"
+        @supports (color: lab(0% 0 0)) {
+          .foo {
+            text-shadow: var(--foo) 12px lab(40% 56.6 39);
+          }
+        }
+      "#},
+      Browsers {
+        chrome: Some(4 << 16),
+        ..Browsers::default()
+      },
+    );
+  }
+
+  #[test]
+  fn test_break() {
+    prefix_test(
+      r#"
+      .foo {
+        box-decoration-break: clone;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        -webkit-box-decoration-break: clone;
+        box-decoration-break: clone;
+      }
+    "#},
+      Browsers {
+        safari: Some(15 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        box-decoration-break: clone;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        box-decoration-break: clone;
+      }
+    "#},
+      Browsers {
+        firefox: Some(95 << 16),
+        ..Browsers::default()
+      },
+    );
+  }
+
+  #[test]
+  fn test_position() {
+    test(
+      r#"
+      .foo {
+        position: relative;
+        position: absolute;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        position: absolute;
+      }
+    "#},
+    );
+
+    test(
+      r#"
+      .foo {
+        position: -webkit-sticky;
+        position: sticky;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        position: -webkit-sticky;
+        position: sticky;
+      }
+    "#},
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        position: sticky;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        position: -webkit-sticky;
+        position: sticky;
+      }
+    "#},
+      Browsers {
+        safari: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        position: -webkit-sticky;
+        position: sticky;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        position: sticky;
+      }
+    "#},
+      Browsers {
+        safari: Some(13 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        top: 0;
+        left: 0;
+        bottom: 0;
+        right: 0;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        inset: 0;
+      }
+    "#},
+    );
+
+    test(
+      r#"
+      .foo {
+        top: 2px;
+        left: 4px;
+        bottom: 2px;
+        right: 4px;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        inset: 2px 4px;
+      }
+    "#},
+    );
+
+    test(
+      r#"
+      .foo {
+        top: 1px;
+        left: 2px;
+        bottom: 3px;
+        right: 4px;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        inset: 1px 4px 3px 2px;
+      }
+    "#},
+    );
+
+    test(
+      r#"
+      .foo {
+        inset-block-start: 2px;
+        inset-block-end: 2px;
+        inset-inline-start: 4px;
+        inset-inline-end: 4px;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        inset-block: 2px;
+        inset-inline: 4px;
+      }
+    "#},
+    );
+
+    test(
+      r#"
+      .foo {
+        inset-block-start: 2px;
+        inset-block-end: 3px;
+        inset-inline-start: 4px;
+        inset-inline-end: 5px;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        inset-block: 2px 3px;
+        inset-inline: 4px 5px;
+      }
+    "#},
+    );
+
+    test(
+      r#"
+      .foo {
+        inset-block-start: 2px;
+        inset-block-end: 3px;
+        inset: 4px;
+        inset-inline-start: 4px;
+        inset-inline-end: 5px;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        inset: 4px;
+        inset-inline: 4px 5px;
+      }
+    "#},
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        inset-inline-start: 2px;
+      }
+    "#,
+      indoc! {r#"
+      .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) {
+        left: 2px;
+      }
+
+      .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) {
+        left: 2px;
+      }
+
+      .foo:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) {
+        right: 2px;
+      }
+
+      .foo:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) {
+        right: 2px;
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        inset-inline-start: 2px;
+        inset-inline-end: 4px;
+      }
+    "#,
+      indoc! {r#"
+      .foo:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) {
+        left: 2px;
+        right: 4px;
+      }
+
+      .foo:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) {
+        left: 2px;
+        right: 4px;
+      }
+
+      .foo:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) {
+        left: 4px;
+        right: 2px;
+      }
+
+      .foo:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) {
+        left: 4px;
+        right: 2px;
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        inset-inline: 2px;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        left: 2px;
+        right: 2px;
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        inset-block-start: 2px;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        top: 2px;
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        inset-block-end: 2px;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        bottom: 2px;
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        top: 1px;
+        left: 2px;
+        bottom: 3px;
+        right: 4px;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        top: 1px;
+        bottom: 3px;
+        left: 2px;
+        right: 4px;
+      }
+    "#},
+      Browsers {
+        safari: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+  }
+
+  #[test]
+  fn test_overflow() {
+    minify_test(".foo { overflow: hidden }", ".foo{overflow:hidden}");
+    minify_test(".foo { overflow: hidden hidden }", ".foo{overflow:hidden}");
+    minify_test(".foo { overflow: hidden auto }", ".foo{overflow:hidden auto}");
+
+    test(
+      r#"
+      .foo {
+        overflow-x: hidden;
+        overflow-y: auto;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        overflow: hidden auto;
+      }
+    "#},
+    );
+
+    test(
+      r#"
+      .foo {
+        overflow: hidden;
+        overflow-y: auto;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        overflow: hidden auto;
+      }
+    "#},
+    );
+    test(
+      r#"
+      .foo {
+        overflow: hidden;
+        overflow-y: var(--y);
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        overflow: hidden;
+        overflow-y: var(--y);
+      }
+    "#},
+    );
+    prefix_test(
+      r#"
+      .foo {
+        overflow: hidden auto;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        overflow-x: hidden;
+        overflow-y: auto;
+      }
+    "#},
+      Browsers {
+        chrome: Some(67 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        overflow: hidden hidden;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        overflow: hidden;
+      }
+    "#},
+      Browsers {
+        chrome: Some(67 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        overflow: hidden auto;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        overflow: hidden auto;
+      }
+    "#},
+      Browsers {
+        chrome: Some(68 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    minify_test(".foo { text-overflow: ellipsis }", ".foo{text-overflow:ellipsis}");
+    prefix_test(
+      r#"
+      .foo {
+        text-overflow: ellipsis;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        -o-text-overflow: ellipsis;
+        text-overflow: ellipsis;
+      }
+    "#},
+      Browsers {
+        safari: Some(4 << 16),
+        opera: Some(10 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        -o-text-overflow: ellipsis;
+        text-overflow: ellipsis;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        text-overflow: ellipsis;
+      }
+    "#},
+      Browsers {
+        safari: Some(4 << 16),
+        opera: Some(14 << 16),
+        ..Browsers::default()
+      },
+    );
+  }
+
+  #[test]
+  fn test_ui() {
+    minify_test(".foo { resize: both }", ".foo{resize:both}");
+    minify_test(".foo { resize: Horizontal }", ".foo{resize:horizontal}");
+    minify_test(".foo { cursor: ew-resize }", ".foo{cursor:ew-resize}");
+    minify_test(
+      ".foo { cursor: url(\"test.cur\"), ew-resize }",
+      ".foo{cursor:url(test.cur),ew-resize}",
+    );
+    minify_test(
+      ".foo { cursor: url(\"test.cur\"), url(\"foo.cur\"), ew-resize }",
+      ".foo{cursor:url(test.cur),url(foo.cur),ew-resize}",
+    );
+    minify_test(".foo { caret-color: auto }", ".foo{caret-color:auto}");
+    minify_test(".foo { caret-color: yellow }", ".foo{caret-color:#ff0}");
+    minify_test(".foo { caret-shape: block }", ".foo{caret-shape:block}");
+    minify_test(".foo { caret: yellow block }", ".foo{caret:#ff0 block}");
+    minify_test(".foo { caret: block yellow }", ".foo{caret:#ff0 block}");
+    minify_test(".foo { caret: block }", ".foo{caret:block}");
+    minify_test(".foo { caret: yellow }", ".foo{caret:#ff0}");
+    minify_test(".foo { caret: auto auto }", ".foo{caret:auto}");
+    minify_test(".foo { caret: auto }", ".foo{caret:auto}");
+    minify_test(".foo { caret: yellow auto }", ".foo{caret:#ff0}");
+    minify_test(".foo { caret: auto block }", ".foo{caret:block}");
+    minify_test(".foo { user-select: none }", ".foo{user-select:none}");
+    minify_test(".foo { -webkit-user-select: none }", ".foo{-webkit-user-select:none}");
+    minify_test(".foo { accent-color: auto }", ".foo{accent-color:auto}");
+    minify_test(".foo { accent-color: yellow }", ".foo{accent-color:#ff0}");
+    minify_test(".foo { appearance: None }", ".foo{appearance:none}");
+    minify_test(
+      ".foo { -webkit-appearance: textfield }",
+      ".foo{-webkit-appearance:textfield}",
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        user-select: none;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        -webkit-user-select: none;
+        -moz-user-select: none;
+        -ms-user-select: none;
+        user-select: none;
+      }
+    "#},
+      Browsers {
+        safari: Some(8 << 16),
+        opera: Some(5 << 16),
+        firefox: Some(10 << 16),
+        ie: Some(10 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        -webkit-user-select: none;
+        -moz-user-select: none;
+        -ms-user-select: none;
+        user-select: none;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        -webkit-user-select: none;
+        user-select: none;
+      }
+    "#},
+      Browsers {
+        safari: Some(8 << 16),
+        opera: Some(80 << 16),
+        firefox: Some(80 << 16),
+        edge: Some(80 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        -webkit-user-select: none;
+        -moz-user-select: none;
+        -ms-user-select: none;
+        user-select: none;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        user-select: none;
+      }
+    "#},
+      Browsers {
+        opera: Some(80 << 16),
+        firefox: Some(80 << 16),
+        edge: Some(80 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        appearance: none;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        -webkit-appearance: none;
+        -moz-appearance: none;
+        -ms-appearance: none;
+        appearance: none;
+      }
+    "#},
+      Browsers {
+        safari: Some(8 << 16),
+        chrome: Some(80 << 16),
+        firefox: Some(10 << 16),
+        ie: Some(11 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        -webkit-appearance: none;
+        -moz-appearance: none;
+        -ms-appearance: none;
+        appearance: none;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        -webkit-appearance: none;
+        appearance: none;
+      }
+    "#},
+      Browsers {
+        safari: Some(15 << 16),
+        chrome: Some(85 << 16),
+        firefox: Some(80 << 16),
+        edge: Some(85 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        -webkit-appearance: none;
+        -moz-appearance: none;
+        -ms-appearance: none;
+        appearance: none;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        appearance: none;
+      }
+    "#},
+      Browsers {
+        chrome: Some(85 << 16),
+        firefox: Some(80 << 16),
+        edge: Some(85 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { caret-color: lch(50.998% 135.363 338) }",
+      indoc! { r#"
+        .foo {
+          caret-color: #ee00be;
+          caret-color: color(display-p3 .972962 -.362078 .804206);
+          caret-color: lch(50.998% 135.363 338);
+        }
+      "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        safari: Some(14 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { caret: lch(50.998% 135.363 338) block }",
+      indoc! { r#"
+        .foo {
+          caret: #ee00be block;
+          caret: color(display-p3 .972962 -.362078 .804206) block;
+          caret: lch(50.998% 135.363 338) block;
+        }
+      "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        safari: Some(14 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { caret: lch(50.998% 135.363 338) var(--foo) }",
+      indoc! { r#"
+        .foo {
+          caret: #ee00be var(--foo);
+        }
+
+        @supports (color: lab(0% 0 0)) {
+          .foo {
+            caret: lab(50.998% 125.506 -50.7078) var(--foo);
+          }
+        }
+      "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+        @supports (color: lab(0% 0 0)) {
+          .foo {
+            caret: lab(50.998% 125.506 -50.7078) var(--foo);
+          }
+        }
+      "#,
+      indoc! { r#"
+        @supports (color: lab(0% 0 0)) {
+          .foo {
+            caret: lab(50.998% 125.506 -50.7078) var(--foo);
+          }
+        }
+      "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+  }
+
+  #[test]
+  fn test_list() {
+    minify_test(".foo { list-style-type: disc; }", ".foo{list-style-type:disc}");
+    minify_test(".foo { list-style-type: \"★\"; }", ".foo{list-style-type:\"★\"}");
+    minify_test(
+      ".foo { list-style-type: symbols(cyclic '○' '●'); }",
+      ".foo{list-style-type:symbols(cyclic \"○\" \"●\")}",
+    );
+    minify_test(
+      ".foo { list-style-type: symbols('○' '●'); }",
+      ".foo{list-style-type:symbols(\"○\" \"●\")}",
+    );
+    minify_test(
+      ".foo { list-style-type: symbols(symbolic '○' '●'); }",
+      ".foo{list-style-type:symbols(\"○\" \"●\")}",
+    );
+    minify_test(
+      ".foo { list-style-type: symbols(symbolic url('ellipse.png')); }",
+      ".foo{list-style-type:symbols(url(ellipse.png))}",
+    );
+    minify_test(
+      ".foo { list-style-image: url('ellipse.png'); }",
+      ".foo{list-style-image:url(ellipse.png)}",
+    );
+    minify_test(
+      ".foo { list-style-position: outside; }",
+      ".foo{list-style-position:outside}",
+    );
+    minify_test(
+      ".foo { list-style: \"★\" url(ellipse.png) outside; }",
+      ".foo{list-style:url(ellipse.png) \"★\"}",
+    );
+    minify_test(".foo { list-style: none; }", ".foo{list-style:none}");
+    minify_test(".foo { list-style: none none outside; }", ".foo{list-style:none}");
+    minify_test(".foo { list-style: none none inside; }", ".foo{list-style:inside none}");
+    minify_test(".foo { list-style: none inside; }", ".foo{list-style:inside none}");
+    minify_test(".foo { list-style: none disc; }", ".foo{list-style:outside}");
+    minify_test(".foo { list-style: none inside disc; }", ".foo{list-style:inside}");
+    minify_test(".foo { list-style: none \"★\"; }", ".foo{list-style:\"★\"}");
+    minify_test(
+      ".foo { list-style: none url(foo.png); }",
+      ".foo{list-style:url(foo.png) none}",
+    );
+
+    test(
+      r#"
+      .foo {
+        list-style-type: disc;
+        list-style-image: url(ellipse.png);
+        list-style-position: outside;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        list-style: url("ellipse.png");
+      }
+    "#},
+    );
+
+    test(
+      r#"
+      .foo {
+        list-style: \"★\" url(ellipse.png) outside;
+        list-style-image: none;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        list-style: \"★\";
+      }
+    "#},
+    );
+
+    test(
+      r#"
+      .foo {
+        list-style: \"★\" url(ellipse.png) outside;
+        list-style-image: var(--img);
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        list-style: url("ellipse.png") \"★\";
+        list-style-image: var(--img);
+      }
+    "#},
+    );
+
+    prefix_test(
+      ".foo { list-style-image: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)) }",
+      indoc! { r#"
+        .foo {
+          list-style-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ff0f0e), to(#7773ff));
+          list-style-image: -webkit-linear-gradient(top, #ff0f0e, #7773ff);
+          list-style-image: linear-gradient(#ff0f0e, #7773ff);
+          list-style-image: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364));
+        }
+      "#},
+      Browsers {
+        chrome: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { list-style: \"★\" linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)) }",
+      indoc! { r#"
+        .foo {
+          list-style: linear-gradient(#ff0f0e, #7773ff) "★";
+          list-style: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)) "★";
+        }
+      "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { list-style: var(--foo) linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)) }",
+      indoc! { r#"
+        .foo {
+          list-style: var(--foo) linear-gradient(#ff0f0e, #7773ff);
+        }
+
+        @supports (color: lab(0% 0 0)) {
+          .foo {
+            list-style: var(--foo) linear-gradient(lab(56.208% 94.4644 98.8928), lab(51% 70.4544 -115.586));
+          }
+        }
+      "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+        @supports (color: lab(0% 0 0)) {
+          .foo {
+            list-style: var(--foo) linear-gradient(lab(56.208% 94.4644 98.8928), lab(51% 70.4544 -115.586));
+          }
+        }
+      "#,
+      indoc! { r#"
+        @supports (color: lab(0% 0 0)) {
+          .foo {
+            list-style: var(--foo) linear-gradient(lab(56.208% 94.4644 98.8928), lab(51% 70.4544 -115.586));
+          }
+        }
+      "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        list-style: inside;
+        list-style-type: disc;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        list-style: inside;
+      }
+    "#},
+    );
+    test(
+      r#"
+      .foo {
+        list-style: inside;
+        list-style-type: decimal;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        list-style: inside decimal;
+      }
+    "#},
+    );
+  }
+
+  #[test]
+  fn test_image_set() {
+    // Spec: https://drafts.csswg.org/css-images-4/#image-set-notation
+    // WPT: https://github.com/web-platform-tests/wpt/blob/master/css/css-images/image-set/image-set-parsing.html
+    // test image-set(<string>)
+    minify_test(
+      ".foo { background: image-set(\"foo.png\" 2x, url(bar.png) 1x) }",
+      ".foo{background:image-set(\"foo.png\" 2x,\"bar.png\" 1x)}",
+    );
+
+    // test image-set(type(<string>))
+    minify_test(
+      ".foo { background: image-set('foo.webp' type('webp'), url(foo.jpg)) }",
+      ".foo{background:image-set(\"foo.webp\" 1x type(\"webp\"),\"foo.jpg\" 1x)}",
+    );
+    minify_test(
+      ".foo { background: image-set('foo.avif' 2x type('image/avif'), url(foo.png)) }",
+      ".foo{background:image-set(\"foo.avif\" 2x type(\"image/avif\"),\"foo.png\" 1x)}",
+    );
+    minify_test(
+      ".foo { background: image-set(url('example.png') 3x type('image/png')) }",
+      ".foo{background:image-set(\"example.png\" 3x type(\"image/png\"))}",
+    );
+
+    minify_test(
+      ".foo { background: image-set(url(example.png) type('image/png') 1x) }",
+      ".foo{background:image-set(\"example.png\" 1x type(\"image/png\"))}",
+    );
+
+    minify_test(
+      ".foo { background: -webkit-image-set(url(\"foo.png\") 2x, url(bar.png) 1x) }",
+      ".foo{background:-webkit-image-set(url(foo.png) 2x,url(bar.png) 1x)}",
+    );
+
+    test(
+      r#"
+      .foo {
+        background: -webkit-image-set(url("foo.png") 2x, url(bar.png) 1x);
+        background: image-set(url("foo.png") 2x, url(bar.png) 1x);
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        background: -webkit-image-set(url("foo.png") 2x, url("bar.png") 1x);
+        background: image-set("foo.png" 2x, "bar.png" 1x);
+      }
+    "#},
+    );
+
+    // test image-set(<gradient>)
+    test(
+      r#"
+      .foo {
+        background: image-set(linear-gradient(cornflowerblue, white) 1x, url("detailed-gradient.png") 3x);
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        background: image-set(linear-gradient(#6495ed, #fff) 1x, "detailed-gradient.png" 3x);
+      }
+    "#},
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        background: image-set(url("foo.png") 2x, url(bar.png) 1x);
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        background: -webkit-image-set(url("foo.png") 2x, url("bar.png") 1x);
+        background: image-set("foo.png" 2x, "bar.png" 1x);
+      }
+    "#},
+      Browsers {
+        chrome: Some(85 << 16),
+        firefox: Some(80 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        background: -webkit-image-set(url("foo.png") 2x, url(bar.png) 1x);
+        background: image-set(url("foo.png") 2x, url(bar.png) 1x);
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        background: -webkit-image-set(url("foo.png") 2x, url("bar.png") 1x);
+        background: image-set("foo.png" 2x, "bar.png" 1x);
+      }
+    "#},
+      Browsers {
+        firefox: Some(80 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        background: -webkit-image-set(url("foo.png") 2x, url(bar.png) 1x);
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        background: -webkit-image-set(url("foo.png") 2x, url("bar.png") 1x);
+      }
+    "#},
+      Browsers {
+        chrome: Some(95 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    for property in &[
+      "background",
+      "background-image",
+      "border-image-source",
+      "border-image",
+      "border-image-source",
+      "-webkit-mask-image",
+      "-webkit-mask",
+      "list-style-image",
+      "list-style",
+    ] {
+      prefix_test(
+        &format!(
+          r#"
+        .foo {{
+          {}: url(foo.png);
+          {}: image-set(url("foo.png") 2x, url(bar.png) 1x);
+        }}
+      "#,
+          property, property
+        ),
+        &format!(
+          indoc! {r#"
+        .foo {{
+          {}: url("foo.png");
+          {}: image-set("foo.png" 2x, "bar.png" 1x);
+        }}
+      "#},
+          property, property
+        ),
+        Browsers {
+          ie: Some(11 << 16),
+          chrome: Some(95 << 16),
+          ..Browsers::default()
+        },
+      );
+
+      prefix_test(
+        &format!(
+          r#"
+        .foo {{
+          {}: url(foo.png);
+          {}: image-set(url("foo.png") 2x, url(bar.png) 1x);
+        }}
+      "#,
+          property, property
+        ),
+        &format!(
+          indoc! {r#"
+        .foo {{
+          {}: -webkit-image-set(url("foo.png") 2x, url("bar.png") 1x);
+          {}: image-set("foo.png" 2x, "bar.png" 1x);
+        }}
+      "#},
+          property, property
+        ),
+        Browsers {
+          chrome: Some(95 << 16),
+          ..Browsers::default()
+        },
+      );
+    }
+  }
+
+  #[test]
+  fn test_color() {
+    minify_test(".foo { color: yellow }", ".foo{color:#ff0}");
+    minify_test(".foo { color: rgb(255, 255, 0) }", ".foo{color:#ff0}");
+    minify_test(".foo { color: rgba(255, 255, 0, 1) }", ".foo{color:#ff0}");
+    minify_test(".foo { color: rgba(255, 255, 0, 0.8) }", ".foo{color:#ff0c}");
+    minify_test(".foo { color: rgb(128, 128, 128) }", ".foo{color:gray}");
+    minify_test(".foo { color: rgb(123, 255, 255) }", ".foo{color:#7bffff}");
+    minify_test(".foo { color: rgba(123, 255, 255, 0.5) }", ".foo{color:#7bffff80}");
+    minify_test(".foo { color: rgb(123 255 255) }", ".foo{color:#7bffff}");
+    minify_test(".foo { color: rgb(123 255 255 / .5) }", ".foo{color:#7bffff80}");
+    minify_test(".foo { color: rgb(123 255 255 / 50%) }", ".foo{color:#7bffff80}");
+    minify_test(".foo { color: rgb(48% 100% 100% / 50%) }", ".foo{color:#7affff80}");
+    minify_test(".foo { color: hsl(100deg, 100%, 50%) }", ".foo{color:#5f0}");
+    minify_test(".foo { color: hsl(100, 100%, 50%) }", ".foo{color:#5f0}");
+    minify_test(".foo { color: hsl(100 100% 50%) }", ".foo{color:#5f0}");
+    minify_test(".foo { color: hsl(100 100 50) }", ".foo{color:#5f0}");
+    minify_test(".foo { color: hsl(100, 100%, 50%, .8) }", ".foo{color:#5f0c}");
+    minify_test(".foo { color: hsl(100 100% 50% / .8) }", ".foo{color:#5f0c}");
+    minify_test(".foo { color: hsla(100, 100%, 50%, .8) }", ".foo{color:#5f0c}");
+    minify_test(".foo { color: hsla(100 100% 50% / .8) }", ".foo{color:#5f0c}");
+    minify_test(".foo { color: transparent }", ".foo{color:#0000}");
+    minify_test(".foo { color: currentColor }", ".foo{color:currentColor}");
+    minify_test(".foo { color: ButtonBorder }", ".foo{color:buttonborder}");
+    minify_test(".foo { color: hwb(194 0% 0%) }", ".foo{color:#00c4ff}");
+    minify_test(".foo { color: hwb(194 0% 0% / 50%) }", ".foo{color:#00c4ff80}");
+    minify_test(".foo { color: hwb(194 0% 50%) }", ".foo{color:#006280}");
+    minify_test(".foo { color: hwb(194 50% 0%) }", ".foo{color:#80e1ff}");
+    minify_test(".foo { color: hwb(194 50 0) }", ".foo{color:#80e1ff}");
+    minify_test(".foo { color: hwb(194 50% 50%) }", ".foo{color:gray}");
+    // minify_test(".foo { color: ActiveText }", ".foo{color:ActiveTet}");
+    minify_test(
+      ".foo { color: lab(29.2345% 39.3825 20.0664); }",
+      ".foo{color:lab(29.2345% 39.3825 20.0664)}",
+    );
+    minify_test(
+      ".foo { color: lab(29.2345 39.3825 20.0664); }",
+      ".foo{color:lab(29.2345% 39.3825 20.0664)}",
+    );
+    minify_test(
+      ".foo { color: lab(29.2345% 39.3825% 20.0664%); }",
+      ".foo{color:lab(29.2345% 49.2281 25.083)}",
+    );
+    minify_test(
+      ".foo { color: lab(29.2345% 39.3825 20.0664 / 100%); }",
+      ".foo{color:lab(29.2345% 39.3825 20.0664)}",
+    );
+    minify_test(
+      ".foo { color: lab(29.2345% 39.3825 20.0664 / 50%); }",
+      ".foo{color:lab(29.2345% 39.3825 20.0664/.5)}",
+    );
+    minify_test(
+      ".foo { color: lch(29.2345% 44.2 27); }",
+      ".foo{color:lch(29.2345% 44.2 27)}",
+    );
+    minify_test(
+      ".foo { color: lch(29.2345 44.2 27); }",
+      ".foo{color:lch(29.2345% 44.2 27)}",
+    );
+    minify_test(
+      ".foo { color: lch(29.2345% 44.2% 27deg); }",
+      ".foo{color:lch(29.2345% 66.3 27)}",
+    );
+    minify_test(
+      ".foo { color: lch(29.2345% 44.2 45deg); }",
+      ".foo{color:lch(29.2345% 44.2 45)}",
+    );
+    minify_test(
+      ".foo { color: lch(29.2345% 44.2 .5turn); }",
+      ".foo{color:lch(29.2345% 44.2 180)}",
+    );
+    minify_test(
+      ".foo { color: lch(29.2345% 44.2 27 / 100%); }",
+      ".foo{color:lch(29.2345% 44.2 27)}",
+    );
+    minify_test(
+      ".foo { color: lch(29.2345% 44.2 27 / 50%); }",
+      ".foo{color:lch(29.2345% 44.2 27/.5)}",
+    );
+    minify_test(
+      ".foo { color: oklab(40.101% 0.1147 0.0453); }",
+      ".foo{color:oklab(40.101% .1147 .0453)}",
+    );
+    minify_test(
+      ".foo { color: oklab(.40101 0.1147 0.0453); }",
+      ".foo{color:oklab(40.101% .1147 .0453)}",
+    );
+    minify_test(
+      ".foo { color: oklab(40.101% 0.1147% 0.0453%); }",
+      ".foo{color:oklab(40.101% .0004588 .0001812)}",
+    );
+    minify_test(
+      ".foo { color: oklch(40.101% 0.12332 21.555); }",
+      ".foo{color:oklch(40.101% .12332 21.555)}",
+    );
+    minify_test(
+      ".foo { color: oklch(.40101 0.12332 21.555); }",
+      ".foo{color:oklch(40.101% .12332 21.555)}",
+    );
+    minify_test(
+      ".foo { color: oklch(40.101% 0.12332% 21.555); }",
+      ".foo{color:oklch(40.101% .00049328 21.555)}",
+    );
+    minify_test(
+      ".foo { color: oklch(40.101% 0.12332 .5turn); }",
+      ".foo{color:oklch(40.101% .12332 180)}",
+    );
+    minify_test(
+      ".foo { color: color(display-p3 1 0.5 0); }",
+      ".foo{color:color(display-p3 1 .5 0)}",
+    );
+    minify_test(
+      ".foo { color: color(display-p3 100% 50% 0%); }",
+      ".foo{color:color(display-p3 1 .5 0)}",
+    );
+    minify_test(
+      ".foo { color: color(xyz-d50 0.2005 0.14089 0.4472); }",
+      ".foo{color:color(xyz-d50 .2005 .14089 .4472)}",
+    );
+    minify_test(
+      ".foo { color: color(xyz-d50 20.05% 14.089% 44.72%); }",
+      ".foo{color:color(xyz-d50 .2005 .14089 .4472)}",
+    );
+    minify_test(
+      ".foo { color: color(xyz-d65 0.2005 0.14089 0.4472); }",
+      ".foo{color:color(xyz .2005 .14089 .4472)}",
+    );
+    minify_test(
+      ".foo { color: color(xyz-d65 20.05% 14.089% 44.72%); }",
+      ".foo{color:color(xyz .2005 .14089 .4472)}",
+    );
+    minify_test(
+      ".foo { color: color(xyz 0.2005 0.14089 0.4472); }",
+      ".foo{color:color(xyz .2005 .14089 .4472)}",
+    );
+    minify_test(
+      ".foo { color: color(xyz 20.05% 14.089% 44.72%); }",
+      ".foo{color:color(xyz .2005 .14089 .4472)}",
+    );
+    minify_test(
+      ".foo { color: color(xyz 0.2005 0 0); }",
+      ".foo{color:color(xyz .2005 0 0)}",
+    );
+    minify_test(".foo { color: color(xyz 0 0 0); }", ".foo{color:color(xyz 0 0 0)}");
+    minify_test(".foo { color: color(xyz 0 1 0); }", ".foo{color:color(xyz 0 1 0)}");
+    minify_test(
+      ".foo { color: color(xyz 0 1 0 / 20%); }",
+      ".foo{color:color(xyz 0 1 0/.2)}",
+    );
+    minify_test(
+      ".foo { color: color(xyz 0 0 0 / 20%); }",
+      ".foo{color:color(xyz 0 0 0/.2)}",
+    );
+    minify_test(
+      ".foo { color: color(display-p3 100% 50% 0 / 20%); }",
+      ".foo{color:color(display-p3 1 .5 0/.2)}",
+    );
+    minify_test(
+      ".foo { color: color(display-p3 100% 0 0 / 20%); }",
+      ".foo{color:color(display-p3 1 0 0/.2)}",
+    );
+    minify_test(".foo { color: hsl(none none none) }", ".foo{color:#000}");
+    minify_test(".foo { color: hwb(none none none) }", ".foo{color:red}");
+    minify_test(".foo { color: rgb(none none none) }", ".foo{color:#000}");
+
+    // If the browser doesn't support `#rrggbbaa` color syntax, it is converted to `transparent`.
+    attr_test(
+      "color: rgba(0, 0, 0, 0)",
+      "color:transparent",
+      true,
+      Some(Browsers {
+        chrome: Some(61 << 16), // Chrome >= 62 supports `#rrggbbaa` color.
+        ..Browsers::default()
+      }),
+    );
+
+    attr_test(
+      "color: #0000",
+      "color:transparent",
+      true,
+      Some(Browsers {
+        chrome: Some(61 << 16), // Chrome >= 62 supports `#rrggbbaa` color.
+        ..Browsers::default()
+      }),
+    );
+
+    attr_test(
+      "color: transparent",
+      "color:transparent",
+      true,
+      Some(Browsers {
+        chrome: Some(61 << 16),
+        ..Browsers::default()
+      }),
+    );
+
+    attr_test(
+      "color: rgba(0, 0, 0, 0)",
+      "color: rgba(0, 0, 0, 0)",
+      false,
+      Some(Browsers {
+        chrome: Some(61 << 16),
+        ..Browsers::default()
+      }),
+    );
+
+    attr_test(
+      "color: rgba(255, 0, 0, 0)",
+      "color:rgba(255,0,0,0)",
+      true,
+      Some(Browsers {
+        chrome: Some(61 << 16),
+        ..Browsers::default()
+      }),
+    );
+
+    attr_test(
+      "color: rgba(255, 0, 0, 0)",
+      "color:#f000",
+      true,
+      Some(Browsers {
+        chrome: Some(62 << 16),
+        ..Browsers::default()
+      }),
+    );
+
+    prefix_test(
+      ".foo { color: rgba(123, 456, 789, 0.5) }",
+      indoc! { r#"
+        .foo {
+          color: #7bffff80;
+        }
+      "#},
+      Browsers {
+        chrome: Some(95 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { color: rgba(123, 255, 255, 0.5) }",
+      indoc! { r#"
+        .foo {
+          color: rgba(123, 255, 255, .5);
+        }
+      "#},
+      Browsers {
+        ie: Some(11 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { color: #7bffff80 }",
+      indoc! { r#"
+        .foo {
+          color: rgba(123, 255, 255, .5);
+        }
+      "#},
+      Browsers {
+        ie: Some(11 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { color: rgba(123, 456, 789, 0.5) }",
+      indoc! { r#"
+        .foo {
+          color: rgba(123, 255, 255, .5);
+        }
+      "#},
+      Browsers {
+        firefox: Some(48 << 16),
+        safari: Some(10 << 16),
+        ios_saf: Some(9 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { color: rgba(123, 456, 789, 0.5) }",
+      indoc! { r#"
+        .foo {
+          color: #7bffff80;
+        }
+      "#},
+      Browsers {
+        firefox: Some(49 << 16),
+        safari: Some(10 << 16),
+        ios_saf: Some(10 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { background-color: lab(40% 56.6 39) }",
+      indoc! { r#"
+        .foo {
+          background-color: #b32323;
+          background-color: lab(40% 56.6 39);
+        }
+      "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { background-color: lch(40% 68.735435 34.568626) }",
+      indoc! { r#"
+        .foo {
+          background-color: #b32323;
+          background-color: lch(40% 68.7354 34.5686);
+        }
+      "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { background-color: oklab(59.686% 0.1009 0.1192); }",
+      indoc! { r#"
+        .foo {
+          background-color: #c65d07;
+          background-color: lab(52.2319% 40.1449 59.9171);
+        }
+      "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { background-color: oklch(40% 0.1268735435 34.568626) }",
+      indoc! { r#"
+        .foo {
+          background-color: #7e250f;
+          background-color: lab(29.2661% 38.2437 35.3889);
+        }
+      "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { background-color: lab(40% 56.6 39) }",
+      indoc! { r#"
+        .foo {
+          background-color: lab(40% 56.6 39);
+        }
+      "#},
+      Browsers {
+        safari: Some(15 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { background-color: oklab(59.686% 0.1009 0.1192); }",
+      indoc! { r#"
+        .foo {
+          background-color: #c65d07;
+          background-color: lab(52.2319% 40.1449 59.9171);
+        }
+      "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        safari: Some(15 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { background-color: oklab(59.686% 0.1009 0.1192); }",
+      indoc! { r#"
+        .foo {
+          background-color: #c65d07;
+          background-color: color(display-p3 .724144 .386777 .148795);
+          background-color: lab(52.2319% 40.1449 59.9171);
+        }
+      "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        safari: Some(14 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { background-color: lab(40% 56.6 39) }",
+      indoc! { r#"
+        .foo {
+          background-color: #b32323;
+          background-color: color(display-p3 .643308 .192455 .167712);
+          background-color: lab(40% 56.6 39);
+        }
+      "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        safari: Some(14 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { background-color: oklch(59.686% 0.15619 49.7694); }",
+      indoc! { r#"
+        .foo {
+          background-color: #c65d06;
+          background-color: lab(52.2321% 40.1417 59.9527);
+        }
+      "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        safari: Some(15 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { background-color: color(sRGB 0.41587 0.503670 0.36664); }",
+      indoc! { r#"
+        .foo {
+          background-color: #6a805d;
+          background-color: color(srgb .41587 .50367 .36664);
+        }
+      "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { background-color: color(display-p3 0.43313 0.50108 0.37950); }",
+      indoc! { r#"
+        .foo {
+          background-color: #6a805d;
+          background-color: color(display-p3 .43313 .50108 .3795);
+        }
+      "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { background-color: color(display-p3 0.43313 0.50108 0.37950); }",
+      indoc! { r#"
+        .foo {
+          background-color: #6a805d;
+          background-color: color(display-p3 .43313 .50108 .3795);
+        }
+      "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        safari: Some(14 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { background-color: color(display-p3 0.43313 0.50108 0.37950); }",
+      indoc! { r#"
+        .foo {
+          background-color: color(display-p3 .43313 .50108 .3795);
+        }
+      "#},
+      Browsers {
+        safari: Some(14 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { background-color: color(display-p3 0.43313 0.50108 0.37950); }",
+      indoc! { r#"
+        .foo {
+          background-color: #6a805d;
+          background-color: color(display-p3 .43313 .50108 .3795);
+        }
+      "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        safari: Some(15 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { background-color: color(display-p3 0.43313 0.50108 0.37950); }",
+      indoc! { r#"
+        .foo {
+          background-color: #6a805d;
+          background-color: color(display-p3 .43313 .50108 .3795);
+        }
+      "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { background-color: color(a98-rgb 0.44091 0.49971 0.37408); }",
+      indoc! { r#"
+        .foo {
+          background-color: #6a805d;
+          background-color: color(a98-rgb .44091 .49971 .37408);
+        }
+      "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { background-color: color(a98-rgb 0.44091 0.49971 0.37408); }",
+      indoc! { r#"
+        .foo {
+          background-color: color(a98-rgb .44091 .49971 .37408);
+        }
+      "#},
+      Browsers {
+        safari: Some(15 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { background-color: color(prophoto-rgb 0.36589 0.41717 0.31333); }",
+      indoc! { r#"
+        .foo {
+          background-color: #6a805d;
+          background-color: color(prophoto-rgb .36589 .41717 .31333);
+        }
+      "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { background-color: color(rec2020 0.42210 0.47580 0.35605); }",
+      indoc! { r#"
+        .foo {
+          background-color: #728765;
+          background-color: color(rec2020 .4221 .4758 .35605);
+        }
+      "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { background-color: color(xyz-d50 0.2005 0.14089 0.4472); }",
+      indoc! { r#"
+        .foo {
+          background-color: #7654cd;
+          background-color: color(xyz-d50 .2005 .14089 .4472);
+        }
+      "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { background-color: color(xyz-d65 0.21661 0.14602 0.59452); }",
+      indoc! { r#"
+        .foo {
+          background-color: #7654cd;
+          background-color: color(xyz .21661 .14602 .59452);
+        }
+      "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { background-color: lch(50.998% 135.363 338) }",
+      indoc! { r#"
+        .foo {
+          background-color: #ee00be;
+          background-color: color(display-p3 .972962 -.362078 .804206);
+          background-color: lch(50.998% 135.363 338);
+        }
+      "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        safari: Some(14 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { color: lch(50.998% 135.363 338) }",
+      indoc! { r#"
+        .foo {
+          color: #ee00be;
+          color: color(display-p3 .972962 -.362078 .804206);
+          color: lch(50.998% 135.363 338);
+        }
+      "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        safari: Some(14 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { background: var(--image) lch(40% 68.735435 34.568626) }",
+      indoc! { r#"
+        .foo {
+          background: var(--image) #b32323;
+        }
+
+        @supports (color: lab(0% 0 0)) {
+          .foo {
+            background: var(--image) lab(40% 56.6 39);
+          }
+        }
+      "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+        @supports (color: lab(0% 0 0)) {
+          .foo {
+            background: var(--image) lab(40% 56.6 39);
+          }
+        }
+      "#,
+      indoc! { r#"
+        @supports (color: lab(0% 0 0)) {
+          .foo {
+            background: var(--image) lab(40% 56.6 39);
+          }
+        }
+      "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        color: red;
+        color: lab(40% 56.6 39);
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        color: red;
+        color: lab(40% 56.6 39);
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(14 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        color: red;
+        color: lab(40% 56.6 39);
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        color: lab(40% 56.6 39);
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(16 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        color: var(--fallback);
+        color: lab(40% 56.6 39);
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        color: var(--fallback);
+        color: lab(40% 56.6 39);
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(14 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        color: var(--fallback);
+        color: lab(40% 56.6 39);
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        color: lab(40% 56.6 39);
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(16 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        color: red;
+        color: var(--foo, lab(40% 56.6 39));
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        color: var(--foo, color(display-p3 .643308 .192455 .167712));
+      }
+
+      @supports (color: lab(0% 0 0)) {
+        .foo {
+          color: var(--foo, lab(40% 56.6 39));
+        }
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(14 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      @supports (color: lab(0% 0 0)) {
+        .foo {
+          color: var(--foo, lab(40% 56.6 39));
+        }
+      }
+    "#,
+      indoc! {r#"
+      @supports (color: lab(0% 0 0)) {
+        .foo {
+          color: var(--foo, lab(40% 56.6 39));
+        }
+      }
+    "#
+      },
+      Browsers {
+        safari: Some(14 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        --a: rgb(0 0 0 / var(--alpha));
+        --b: rgb(50% 50% 50% / var(--alpha));
+        --c: rgb(var(--x) 0 0);
+        --d: rgb(0 var(--x) 0);
+        --e: rgb(0 0 var(--x));
+        --f: rgb(var(--x) 0 0 / var(--alpha));
+        --g: rgb(0 var(--x) 0 / var(--alpha));
+        --h: rgb(0 0 var(--x) / var(--alpha));
+        --i: rgb(none 0 0 / var(--alpha));
+        --j: rgb(from yellow r g b / var(--alpha));
+      }
+      "#,
+      indoc! { r#"
+        .foo {
+          --a: rgba(0, 0, 0, var(--alpha));
+          --b: rgba(128, 128, 128, var(--alpha));
+          --c: rgb(var(--x) 0 0);
+          --d: rgb(0 var(--x) 0);
+          --e: rgb(0 0 var(--x));
+          --f: rgb(var(--x) 0 0 / var(--alpha));
+          --g: rgb(0 var(--x) 0 / var(--alpha));
+          --h: rgb(0 0 var(--x) / var(--alpha));
+          --i: rgb(none 0 0 / var(--alpha));
+          --j: rgba(255, 255, 0, var(--alpha));
+        }
+      "#},
+      Browsers {
+        safari: Some(11 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        --a: rgb(0 0 0 / var(--alpha));
+        --b: rgb(50% 50% 50% / var(--alpha));
+        --c: rgb(var(--x) 0 0);
+        --d: rgb(0 var(--x) 0);
+        --e: rgb(0 0 var(--x));
+        --f: rgb(var(--x) 0 0 / var(--alpha));
+        --g: rgb(0 var(--x) 0 / var(--alpha));
+        --h: rgb(0 0 var(--x) / var(--alpha));
+        --i: rgb(none 0 0 / var(--alpha));
+        --j: rgb(from yellow r g b / var(--alpha));
+      }
+      "#,
+      indoc! { r#"
+        .foo {
+          --a: rgb(0 0 0 / var(--alpha));
+          --b: rgb(128 128 128 / var(--alpha));
+          --c: rgb(var(--x) 0 0);
+          --d: rgb(0 var(--x) 0);
+          --e: rgb(0 0 var(--x));
+          --f: rgb(var(--x) 0 0 / var(--alpha));
+          --g: rgb(0 var(--x) 0 / var(--alpha));
+          --h: rgb(0 0 var(--x) / var(--alpha));
+          --i: rgb(none 0 0 / var(--alpha));
+          --j: rgb(255 255 0 / var(--alpha));
+        }
+      "#},
+      Browsers {
+        safari: Some(13 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        --a: hsl(270 100% 50% / var(--alpha));
+        --b: hsl(var(--x) 0 0);
+        --c: hsl(0 var(--x) 0);
+        --d: hsl(0 0 var(--x));
+        --e: hsl(var(--x) 0 0 / var(--alpha));
+        --f: hsl(0 var(--x) 0 / var(--alpha));
+        --g: hsl(0 0 var(--x) / var(--alpha));
+        --h: hsl(270 100% 50% / calc(var(--alpha) / 2));
+        --i: hsl(none 100% 50% / var(--alpha));
+        --j: hsl(from yellow h s l / var(--alpha));
+      }
+      "#,
+      indoc! { r#"
+        .foo {
+          --a: hsla(270, 100%, 50%, var(--alpha));
+          --b: hsl(var(--x) 0 0);
+          --c: hsl(0 var(--x) 0);
+          --d: hsl(0 0 var(--x));
+          --e: hsl(var(--x) 0 0 / var(--alpha));
+          --f: hsl(0 var(--x) 0 / var(--alpha));
+          --g: hsl(0 0 var(--x) / var(--alpha));
+          --h: hsla(270, 100%, 50%, calc(var(--alpha) / 2));
+          --i: hsl(none 100% 50% / var(--alpha));
+          --j: hsla(60, 100%, 50%, var(--alpha));
+        }
+      "#},
+      Browsers {
+        safari: Some(11 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        --a: hsl(270 100% 50% / var(--alpha));
+        --b: hsl(var(--x) 0 0);
+        --c: hsl(0 var(--x) 0);
+        --d: hsl(0 0 var(--x));
+        --e: hsl(var(--x) 0 0 / var(--alpha));
+        --f: hsl(0 var(--x) 0 / var(--alpha));
+        --g: hsl(0 0 var(--x) / var(--alpha));
+        --h: hsl(270 100% 50% / calc(var(--alpha) / 2));
+        --i: hsl(none 100% 50% / var(--alpha));
+      }
+      "#,
+      indoc! { r#"
+        .foo {
+          --a: hsl(270 100% 50% / var(--alpha));
+          --b: hsl(var(--x) 0 0);
+          --c: hsl(0 var(--x) 0);
+          --d: hsl(0 0 var(--x));
+          --e: hsl(var(--x) 0 0 / var(--alpha));
+          --f: hsl(0 var(--x) 0 / var(--alpha));
+          --g: hsl(0 0 var(--x) / var(--alpha));
+          --h: hsl(270 100% 50% / calc(var(--alpha) / 2));
+          --i: hsl(none 100% 50% / var(--alpha));
+        }
+      "#},
+      Browsers {
+        safari: Some(13 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    test(
+      r#"
+      .foo {
+        --a: rgb(50% 50% 50% / calc(100% / 2));
+        --b: hsl(calc(360deg / 2) 50% 50%);
+        --c: oklab(40.101% calc(0.1 + 0.2) 0.0453);
+        --d: color(display-p3 0.43313 0.50108 calc(0.1 + 0.2));
+        --e: rgb(calc(255 / 2), calc(255 / 2), calc(255 / 2));
+      }
+      "#,
+      indoc! { r#"
+        .foo {
+          --a: #80808080;
+          --b: #40bfbf;
+          --c: oklab(40.101% .3 .0453);
+          --d: color(display-p3 .43313 .50108 .3);
+          --e: gray;
+        }
+      "#},
+    );
+  }
+
+  #[test]
+  fn test_relative_color() {
+    fn test(input: &str, output: &str) {
+      let output = CssColor::parse_string(output)
+        .unwrap()
+        .to_css_string(PrinterOptions {
+          minify: true,
+          ..PrinterOptions::default()
+        })
+        .unwrap();
+      minify_test(
+        &format!(".foo {{ color: {} }}", input),
+        &format!(".foo{{color:{}}}", output),
+      );
+    }
+
+    test("lab(from indianred calc(l * .8) a b)", "lab(43.1402% 45.7516 23.1557)");
+    test("lch(from indianred calc(l + 10) c h)", "lch(63.9252% 51.2776 26.8448)");
+    test("lch(from indianred l calc(c - 50) h)", "lch(53.9252% 1.27763 26.8448)");
+    test(
+      "lch(from indianred l c calc(h + 180deg))",
+      "lch(53.9252% 51.2776 206.845)",
+    );
+    test("lch(from orchid l 30 h)", "lch(62.7526% 30 326.969)");
+    test("lch(from orchid l 30 h)", "lch(62.7526% 30 326.969)");
+    test("lch(from peru calc(l * 0.8) c h)", "lch(49.8022% 54.0117 63.6804)");
+    test("rgb(from indianred 255 g b)", "rgb(255, 92, 92)");
+    test("rgb(from indianred r g b / .5)", "rgba(205, 92, 92, .5)");
+    test(
+      "rgb(from rgba(205, 92, 92, .5) r g b / calc(alpha + .2))",
+      "rgba(205, 92, 92, .7)",
+    );
+    test(
+      "rgb(from rgba(205, 92, 92, .5) r g b / calc(alpha + .2))",
+      "rgba(205, 92, 92, .7)",
+    );
+    test("lch(from indianred l sin(c) h)", "lch(53.9252% .84797 26.8448)");
+    test("lch(from indianred l sqrt(c) h)", "lch(53.9252% 7.16084 26.8448)");
+    test("lch(from indianred l c sin(h))", "lch(53.9252% 51.2776 .451575)");
+    test("lch(from indianred calc(10% + 20%) c h)", "lch(30% 51.2776 26.8448)");
+    test("lch(from indianred calc(10 + 20) c h)", "lch(30% 51.2776 26.8448)");
+    test("lch(from indianred l c calc(10 + 20))", "lch(53.9252% 51.2776 30)");
+    test(
+      "lch(from indianred l c calc(10deg + 20deg))",
+      "lch(53.9252% 51.2776 30)",
+    );
+    test(
+      "lch(from indianred l c calc(10deg + 0.35rad))",
+      "lch(53.9252% 51.2776 30.0535)",
+    );
+    minify_test(
+      ".foo{color:lch(from currentColor l c sin(h))}",
+      ".foo{color:lch(from currentColor l c sin(h))}",
+    );
+
+    // The following tests were converted from WPT: https://github.com/web-platform-tests/wpt/blob/master/css/css-color/parsing/relative-color-valid.html
+    // Find: test_valid_value\(`color`, `(.*?)`,\s*`(.*?)`\)
+    // Replace: test("$1", "$2")
+
+    // Testing no modifications.
+    test("rgb(from rebeccapurple r g b)", "#639");
+    test("rgb(from rebeccapurple r g b / alpha)", "#639");
+    test("rgb(from rgb(20%, 40%, 60%, 80%) r g b / alpha)", "#369c");
+    test("rgb(from hsl(120deg 20% 50% / .5) r g b / alpha)", "#66996680");
+
+    // Test nesting relative colors.
+    test("rgb(from rgb(from rebeccapurple r g b) r g b)", "#639");
+
+    // Testing non-sRGB origin colors to see gamut mapping.
+    test("rgb(from color(display-p3 0 1 0) r g b / alpha)", "#00f942"); // Naive clip based mapping would give rgb(0, 255, 0).
+    test("rgb(from lab(100% 104.3 -50.9) r g b)", "#fff"); // Naive clip based mapping would give rgb(255, 150, 255).
+    test("rgb(from lab(0% 104.3 -50.9) r g b)", "#2a0022"); // Naive clip based mapping would give rgb(90, 0, 76). NOTE: 0% lightness in Lab/LCH does not automatically correspond with sRGB black.
+    test("rgb(from lch(100% 116 334) r g b)", "#fff"); // Naive clip based mapping would give rgb(255, 150, 255).
+    test("rgb(from lch(0% 116 334) r g b)", "#2a0022"); // Naive clip based mapping would give rgb(90, 0, 76). NOTE: 0% lightness in Lab/LCH does not automatically correspond with sRGB black.
+    test("rgb(from oklab(100% 0.365 -0.16) r g b)", "#fff"); // Naive clip based mapping would give rgb(255, 92, 255).
+    test("rgb(from oklab(0% 0.365 -0.16) r g b)", "#000"); // Naive clip based mapping would give rgb(19, 0, 24).
+    test("rgb(from oklch(100% 0.399 336.3) r g b)", "#fff"); // Naive clip based mapping would give rgb(255, 91, 255).
+    test("rgb(from oklch(0% 0.399 336.3) r g b)", "#000"); // Naive clip based mapping would give rgb(20, 0, 24).
+
+    // Testing replacement with 0.
+    test("rgb(from rebeccapurple 0 0 0)", "rgb(0, 0, 0)");
+    test("rgb(from rebeccapurple 0 0 0 / 0)", "rgba(0, 0, 0, 0)");
+    test("rgb(from rebeccapurple 0 g b / alpha)", "rgb(0, 51, 153)");
+    test("rgb(from rebeccapurple r 0 b / alpha)", "rgb(102, 0, 153)");
+    test("rgb(from rebeccapurple r g 0 / alpha)", "rgb(102, 51, 0)");
+    test("rgb(from rebeccapurple r g b / 0)", "rgba(102, 51, 153, 0)");
+    test(
+      "rgb(from rgb(20%, 40%, 60%, 80%) 0 g b / alpha)",
+      "rgba(0, 102, 153, 0.8)",
+    );
+    test(
+      "rgb(from rgb(20%, 40%, 60%, 80%) r 0 b / alpha)",
+      "rgba(51, 0, 153, 0.8)",
+    );
+    test(
+      "rgb(from rgb(20%, 40%, 60%, 80%) r g 0 / alpha)",
+      "rgba(51, 102, 0, 0.8)",
+    );
+    test("rgb(from rgb(20%, 40%, 60%, 80%) r g b / 0)", "rgba(51, 102, 153, 0)");
+
+    // Testing replacement with a number.
+    test("rgb(from rebeccapurple 25 g b / alpha)", "rgb(25, 51, 153)");
+    test("rgb(from rebeccapurple r 25 b / alpha)", "rgb(102, 25, 153)");
+    test("rgb(from rebeccapurple r g 25 / alpha)", "rgb(102, 51, 25)");
+    test("rgb(from rebeccapurple r g b / .25)", "rgba(102, 51, 153, 0.25)");
+    test(
+      "rgb(from rgb(20%, 40%, 60%, 80%) 25 g b / alpha)",
+      "rgba(25, 102, 153, 0.8)",
+    );
+    test(
+      "rgb(from rgb(20%, 40%, 60%, 80%) r 25 b / alpha)",
+      "rgba(51, 25, 153, 0.8)",
+    );
+    test(
+      "rgb(from rgb(20%, 40%, 60%, 80%) r g 25 / alpha)",
+      "rgba(51, 102, 25, 0.8)",
+    );
+    test(
+      "rgb(from rgb(20%, 40%, 60%, 80%) r g b / .20)",
+      "rgba(51, 102, 153, 0.2)",
+    );
+
+    // Testing replacement with a percentage.
+    test("rgb(from rebeccapurple 20% g b / alpha)", "rgb(51, 51, 153)");
+    test("rgb(from rebeccapurple r 20% b / alpha)", "rgb(102, 51, 153)");
+    test("rgb(from rebeccapurple r g 20% / alpha)", "rgb(102, 51, 51)");
+    test("rgb(from rebeccapurple r g b / 20%)", "rgba(102, 51, 153, 0.2)");
+    test(
+      "rgb(from rgb(20%, 40%, 60%, 80%) 20% g b / alpha)",
+      "rgba(51, 102, 153, 0.8)",
+    );
+    test(
+      "rgb(from rgb(20%, 40%, 60%, 80%) r 20% b / alpha)",
+      "rgba(51, 51, 153, 0.8)",
+    );
+    test(
+      "rgb(from rgb(20%, 40%, 60%, 80%) r g 20% / alpha)",
+      "rgba(51, 102, 51, 0.8)",
+    );
+    test(
+      "rgb(from rgb(20%, 40%, 60%, 80%) r g b / 20%)",
+      "rgba(51, 102, 153, 0.2)",
+    );
+
+    // Testing replacement with a number for r, g, b but percent for alpha.
+    test("rgb(from rebeccapurple 25 g b / 25%)", "rgba(25, 51, 153, 0.25)");
+    test("rgb(from rebeccapurple r 25 b / 25%)", "rgba(102, 25, 153, 0.25)");
+    test("rgb(from rebeccapurple r g 25 / 25%)", "rgba(102, 51, 25, 0.25)");
+    test(
+      "rgb(from rgb(20%, 40%, 60%, 80%) 25 g b / 25%)",
+      "rgba(25, 102, 153, 0.25)",
+    );
+    test(
+      "rgb(from rgb(20%, 40%, 60%, 80%) r 25 b / 25%)",
+      "rgba(51, 25, 153, 0.25)",
+    );
+    test(
+      "rgb(from rgb(20%, 40%, 60%, 80%) r g 25 / 25%)",
+      "rgba(51, 102, 25, 0.25)",
+    );
+
+    // Testing permutation.
+    test("rgb(from rebeccapurple g b r)", "rgb(51, 153, 102)");
+    test("rgb(from rebeccapurple b alpha r / g)", "rgba(153, 1, 102, 1)");
+    test("rgb(from rebeccapurple r r r / r)", "rgba(102, 102, 102, 1)");
+    test("rgb(from rebeccapurple alpha alpha alpha / alpha)", "rgb(1, 1, 1)");
+    test("rgb(from rgb(20%, 40%, 60%, 80%) g b r)", "rgb(102, 153, 51)");
+    test("rgb(from rgb(20%, 40%, 60%, 80%) b alpha r / g)", "rgba(153, 1, 51, 1)");
+    test("rgb(from rgb(20%, 40%, 60%, 80%) r r r / r)", "rgba(51, 51, 51, 1)");
+    test(
+      "rgb(from rgb(20%, 40%, 60%, 80%) alpha alpha alpha / alpha)",
+      "rgba(1, 1, 1, 0.8)",
+    );
+
+    // Testing mixes of number and percentage. (These would not be allowed in the non-relative syntax).
+    test("rgb(from rebeccapurple r 20% 10)", "rgb(102, 51, 10)");
+    test("rgb(from rebeccapurple r 10 20%)", "rgb(102, 10, 51)");
+    test("rgb(from rebeccapurple 0% 10 10)", "rgb(0, 10, 10)");
+    test("rgb(from rgb(20%, 40%, 60%, 80%) r 20% 10)", "rgb(51, 51, 10)");
+    test("rgb(from rgb(20%, 40%, 60%, 80%) r 10 20%)", "rgb(51, 10, 51)");
+    test("rgb(from rgb(20%, 40%, 60%, 80%) 0% 10 10)", "rgb(0, 10, 10)");
+
+    // Testing with calc().
+    test("rgb(from rebeccapurple calc(r) calc(g) calc(b))", "rgb(102, 51, 153)");
+    test("rgb(from rebeccapurple r calc(g * 2) 10)", "rgb(102, 102, 10)");
+    test("rgb(from rebeccapurple b calc(r * .5) 10)", "rgb(153, 51, 10)");
+    test("rgb(from rebeccapurple r calc(g * .5 + g * .5) 10)", "rgb(102, 51, 10)");
+    test("rgb(from rebeccapurple r calc(b * .5 - g * .5) 10)", "rgb(102, 51, 10)");
+    test(
+      "rgb(from rgb(20%, 40%, 60%, 80%) calc(r) calc(g) calc(b) / calc(alpha))",
+      "rgba(51, 102, 153, 0.8)",
+    );
+
+    // Testing with 'none'.
+    test("rgb(from rebeccapurple none none none)", "rgb(0, 0, 0)");
+    test("rgb(from rebeccapurple none none none / none)", "rgba(0, 0, 0, 0)");
+    test("rgb(from rebeccapurple r g none)", "rgb(102, 51, 0)");
+    test("rgb(from rebeccapurple r g none / alpha)", "rgb(102, 51, 0)");
+    test("rgb(from rebeccapurple r g b / none)", "rgba(102, 51, 153, 0)");
+    test(
+      "rgb(from rgb(20% 40% 60% / 80%) r g none / alpha)",
+      "rgba(51, 102, 0, 0.8)",
+    );
+    test("rgb(from rgb(20% 40% 60% / 80%) r g b / none)", "rgba(51, 102, 153, 0)");
+    // FIXME: Clarify with spec editors if 'none' should pass through to the constants.
+    test("rgb(from rgb(none none none) r g b)", "rgb(0, 0, 0)");
+    test("rgb(from rgb(none none none / none) r g b / alpha)", "rgba(0, 0, 0, 0)");
+    test("rgb(from rgb(20% none 60%) r g b)", "rgb(51, 0, 153)");
+    test(
+      "rgb(from rgb(20% 40% 60% / none) r g b / alpha)",
+      "rgba(51, 102, 153, 0)",
+    );
+
+    // hsl(from ...)
+
+    // Testing no modifications.
+    test("hsl(from rebeccapurple h s l)", "rgb(102, 51, 153)");
+    test("hsl(from rebeccapurple h s l / alpha)", "rgb(102, 51, 153)");
+    test(
+      "hsl(from rgb(20%, 40%, 60%, 80%) h s l / alpha)",
+      "rgba(51, 102, 153, 0.8)",
+    );
+    test(
+      "hsl(from hsl(120deg 20% 50% / .5) h s l / alpha)",
+      "rgba(102, 153, 102, 0.5)",
+    );
+
+    // Test nesting relative colors.
+    test("hsl(from hsl(from rebeccapurple h s l) h s l)", "rgb(102, 51, 153)");
+
+    // Testing non-sRGB origin colors to see gamut mapping.
+    test("hsl(from color(display-p3 0 1 0) h s l / alpha)", "rgb(0, 249, 66)"); // Naive clip based mapping would give rgb(0, 255, 0).
+    test("hsl(from lab(100% 104.3 -50.9) h s l)", "rgb(255, 255, 255)"); // Naive clip based mapping would give rgb(255, 150, 255).
+    test("hsl(from lab(0% 104.3 -50.9) h s l)", "rgb(42, 0, 34)"); // Naive clip based mapping would give rgb(90, 0, 76). NOTE: 0% lightness in Lab/LCH does not automatically correspond with sRGB black,
+    test("hsl(from lch(100% 116 334) h s l)", "rgb(255, 255, 255)"); // Naive clip based mapping would give rgb(255, 150, 255).
+    test("hsl(from lch(0% 116 334) h s l)", "rgb(42, 0, 34)"); // Naive clip based mapping would give rgb(90, 0, 76). NOTE: 0% lightness in Lab/LCH does not automatically correspond with sRGB black,
+    test("hsl(from oklab(100% 0.365 -0.16) h s l)", "rgb(255, 255, 255)"); // Naive clip based mapping would give rgb(255, 92, 255).
+    test("hsl(from oklab(0% 0.365 -0.16) h s l)", "rgb(0, 0, 0)"); // Naive clip based mapping would give rgb(19, 0, 24).
+    test("hsl(from oklch(100% 0.399 336.3) h s l)", "rgb(255, 255, 255)"); // Naive clip based mapping would give rgb(255, 91, 255).
+    test("hsl(from oklch(0% 0.399 336.3) h s l)", "rgb(0, 0, 0)"); // Naive clip based mapping would give rgb(20, 0, 24).
+
+    // Testing replacement with 0.
+    test("hsl(from rebeccapurple 0 0% 0%)", "rgb(0, 0, 0)");
+    test("hsl(from rebeccapurple 0deg 0% 0%)", "rgb(0, 0, 0)");
+    test("hsl(from rebeccapurple 0 0% 0% / 0)", "rgba(0, 0, 0, 0)");
+    test("hsl(from rebeccapurple 0deg 0% 0% / 0)", "rgba(0, 0, 0, 0)");
+    test("hsl(from rebeccapurple 0 s l / alpha)", "rgb(153, 51, 51)");
+    test("hsl(from rebeccapurple 0deg s l / alpha)", "rgb(153, 51, 51)");
+    test("hsl(from rebeccapurple h 0% l / alpha)", "rgb(102, 102, 102)");
+    test("hsl(from rebeccapurple h s 0% / alpha)", "rgb(0, 0, 0)");
+    test("hsl(from rebeccapurple h s l / 0)", "rgba(102, 51, 153, 0)");
+    test(
+      "hsl(from rgb(20%, 40%, 60%, 80%) 0 s l / alpha)",
+      "rgba(153, 51, 51, 0.8)",
+    );
+    test(
+      "hsl(from rgb(20%, 40%, 60%, 80%) 0deg s l / alpha)",
+      "rgba(153, 51, 51, 0.8)",
+    );
+    test(
+      "hsl(from rgb(20%, 40%, 60%, 80%) h 0% l / alpha)",
+      "rgba(102, 102, 102, 0.8)",
+    );
+    test("hsl(from rgb(20%, 40%, 60%, 80%) h s 0% / alpha)", "rgba(0, 0, 0, 0.8)");
+    test("hsl(from rgb(20%, 40%, 60%, 80%) h s l / 0)", "rgba(51, 102, 153, 0)");
+
+    // Testing replacement with a constant.
+    test("hsl(from rebeccapurple 25 s l / alpha)", "rgb(153, 94, 51)");
+    test("hsl(from rebeccapurple 25deg s l / alpha)", "rgb(153, 94, 51)");
+    test("hsl(from rebeccapurple h 20% l / alpha)", "rgb(102, 82, 122)");
+    test("hsl(from rebeccapurple h s 20% / alpha)", "rgb(51, 25, 77)");
+    test("hsl(from rebeccapurple h s l / .25)", "rgba(102, 51, 153, 0.25)");
+    test(
+      "hsl(from rgb(20%, 40%, 60%, 80%) 25 s l / alpha)",
+      "rgba(153, 94, 51, 0.8)",
+    );
+    test(
+      "hsl(from rgb(20%, 40%, 60%, 80%) 25deg s l / alpha)",
+      "rgba(153, 94, 51, 0.8)",
+    );
+    test(
+      "hsl(from rgb(20%, 40%, 60%, 80%) h 20% l / alpha)",
+      "rgba(82, 102, 122, 0.8)",
+    );
+    test(
+      "hsl(from rgb(20%, 40%, 60%, 80%) h s 20% / alpha)",
+      "rgba(25, 51, 77, 0.8)",
+    );
+    test(
+      "hsl(from rgb(20%, 40%, 60%, 80%) h s l / .2)",
+      "rgba(51, 102, 153, 0.2)",
+    );
+
+    // Testing valid permutation (types match).
+    test("hsl(from rebeccapurple h l s)", "rgb(128, 77, 179)");
+    test(
+      "hsl(from rebeccapurple h calc(alpha * 100) l / calc(s / 100))",
+      "rgba(102, 0, 204, 0.5)",
+    );
+    test(
+      "hsl(from rebeccapurple h l l / calc(l / 100))",
+      "rgba(102, 61, 143, 0.4)",
+    );
+    test(
+      "hsl(from rebeccapurple h calc(alpha * 100) calc(alpha * 100) / calc(alpha * 100))",
+      "rgb(255, 255, 255)",
+    );
+    test("hsl(from rgb(20%, 40%, 60%, 80%) h l s)", "rgb(77, 128, 179)");
+    test(
+      "hsl(from rgb(20%, 40%, 60%, 80%) h calc(alpha * 100) l / calc(s / 100))",
+      "rgba(20, 102, 184, 0.5)",
+    );
+    test(
+      "hsl(from rgb(20%, 40%, 60%, 80%) h l l / calc(l / 100))",
+      "rgba(61, 102, 143, 0.4)",
+    );
+    test(
+      "hsl(from rgb(20%, 40%, 60%, 80%) h calc(alpha * 100) calc(alpha * 100) / alpha)",
+      "rgba(163, 204, 245, 0.8)",
+    );
+
+    // Testing with calc().
+    test("hsl(from rebeccapurple calc(h) calc(s) calc(l))", "rgb(102, 51, 153)");
+    test(
+      "hsl(from rgb(20%, 40%, 60%, 80%) calc(h) calc(s) calc(l) / calc(alpha))",
+      "rgba(51, 102, 153, 0.8)",
+    );
+
+    // Testing with 'none'.
+    test("hsl(from rebeccapurple none none none)", "rgb(0, 0, 0)");
+    test("hsl(from rebeccapurple none none none / none)", "rgba(0, 0, 0, 0)");
+    test("hsl(from rebeccapurple h s none)", "rgb(0, 0, 0)");
+    test("hsl(from rebeccapurple h s none / alpha)", "rgb(0, 0, 0)");
+    test("hsl(from rebeccapurple h s l / none)", "rgba(102, 51, 153, 0)");
+    test("hsl(from rebeccapurple none s l / alpha)", "rgb(153, 51, 51)");
+    test(
+      "hsl(from hsl(120deg 20% 50% / .5) h s none / alpha)",
+      "rgba(0, 0, 0, 0.5)",
+    );
+    test(
+      "hsl(from hsl(120deg 20% 50% / .5) h s l / none)",
+      "rgba(102, 153, 102, 0)",
+    );
+    test(
+      "hsl(from hsl(120deg 20% 50% / .5) none s l / alpha)",
+      "rgba(153, 102, 102, 0.5)",
+    );
+    // FIXME: Clarify with spec editors if 'none' should pass through to the constants.
+    test("hsl(from hsl(none none none) h s l)", "rgb(0, 0, 0)");
+    test("hsl(from hsl(none none none / none) h s l / alpha)", "rgba(0, 0, 0, 0)");
+    test("hsl(from hsl(120deg none 50% / .5) h s l)", "rgb(128, 128, 128)");
+    test(
+      "hsl(from hsl(120deg 20% 50% / none) h s l / alpha)",
+      "rgba(102, 153, 102, 0)",
+    );
+    test(
+      "hsl(from hsl(none 20% 50% / .5) h s l / alpha)",
+      "rgba(153, 102, 102, 0.5)",
+    );
+
+    // hwb(from ...)
+
+    // Testing no modifications.
+    test("hwb(from rebeccapurple h w b)", "rgb(102, 51, 153)");
+    test("hwb(from rebeccapurple h w b / alpha)", "rgb(102, 51, 153)");
+    test(
+      "hwb(from rgb(20%, 40%, 60%, 80%) h w b / alpha)",
+      "rgba(51, 102, 153, 0.8)",
+    );
+    test(
+      "hwb(from hsl(120deg 20% 50% / .5) h w b / alpha)",
+      "rgba(102, 153, 102, 0.5)",
+    );
+
+    // Test nesting relative colors.
+    test("hwb(from hwb(from rebeccapurple h w b) h w b)", "rgb(102, 51, 153)");
+
+    // Testing non-sRGB origin colors to see gamut mapping.
+    test("hwb(from color(display-p3 0 1 0) h w b / alpha)", "rgb(0, 249, 66)"); // Naive clip based mapping would give rgb(0, 255, 0).
+    test("hwb(from lab(100% 104.3 -50.9) h w b)", "rgb(255, 255, 255)"); // Naive clip based mapping would give rgb(255, 150, 255).
+    test("hwb(from lab(0% 104.3 -50.9) h w b)", "rgb(42, 0, 34)"); // Naive clip based mapping would give rgb(90, 0, 76). NOTE: 0% lightness in Lab/LCH does not automatically correspond with sRGB black,
+    test("hwb(from lch(100% 116 334) h w b)", "rgb(255, 255, 255)"); // Naive clip based mapping would give rgb(255, 150, 255).
+    test("hwb(from lch(0% 116 334) h w b)", "rgb(42, 0, 34)"); // Naive clip based mapping would give rgb(90, 0, 76). NOTE: 0% lightness in Lab/LCH does not automatically correspond with sRGB black,
+    test("hwb(from oklab(100% 0.365 -0.16) h w b)", "rgb(255, 255, 255)"); // Naive clip based mapping would give rgb(255, 92, 255).
+    test("hwb(from oklab(0% 0.365 -0.16) h w b)", "rgb(0, 0, 0)"); // Naive clip based mapping would give rgb(19, 0, 24).
+    test("hwb(from oklch(100% 0.399 336.3) h w b)", "rgb(255, 255, 255)"); // Naive clip based mapping would give rgb(255, 91, 255).
+    test("hwb(from oklch(0% 0.399 336.3) h w b)", "rgb(0, 0, 0)"); // Naive clip based mapping would give rgb(20, 0, 24).
+
+    // Testing replacement with 0.
+    test("hwb(from rebeccapurple 0 0% 0%)", "rgb(255, 0, 0)");
+    test("hwb(from rebeccapurple 0deg 0% 0%)", "rgb(255, 0, 0)");
+    test("hwb(from rebeccapurple 0 0% 0% / 0)", "rgba(255, 0, 0, 0)");
+    test("hwb(from rebeccapurple 0deg 0% 0% / 0)", "rgba(255, 0, 0, 0)");
+    test("hwb(from rebeccapurple 0 w b / alpha)", "rgb(153, 51, 51)");
+    test("hwb(from rebeccapurple 0deg w b / alpha)", "rgb(153, 51, 51)");
+    test("hwb(from rebeccapurple h 0% b / alpha)", "rgb(77, 0, 153)");
+    test("hwb(from rebeccapurple h w 0% / alpha)", "rgb(153, 51, 255)");
+    test("hwb(from rebeccapurple h w b / 0)", "rgba(102, 51, 153, 0)");
+    test(
+      "hwb(from rgb(20%, 40%, 60%, 80%) 0 w b / alpha)",
+      "rgba(153, 51, 51, 0.8)",
+    );
+    test(
+      "hwb(from rgb(20%, 40%, 60%, 80%) 0deg w b / alpha)",
+      "rgba(153, 51, 51, 0.8)",
+    );
+    test(
+      "hwb(from rgb(20%, 40%, 60%, 80%) h 0% b / alpha)",
+      "rgba(0, 77, 153, 0.8)",
+    );
+    test(
+      "hwb(from rgb(20%, 40%, 60%, 80%) h w 0% / alpha)",
+      "rgba(51, 153, 255, 0.8)",
+    );
+    test("hwb(from rgb(20%, 40%, 60%, 80%) h w b / 0)", "rgba(51, 102, 153, 0)");
+
+    // Testing replacement with a constant.
+    test("hwb(from rebeccapurple 25 w b / alpha)", "rgb(153, 94, 51)");
+    test("hwb(from rebeccapurple 25deg w b / alpha)", "rgb(153, 94, 51)");
+    test("hwb(from rebeccapurple h 20% b / alpha)", "rgb(102, 51, 153)");
+    test("hwb(from rebeccapurple h w 20% / alpha)", "rgb(128, 51, 204)");
+    test("hwb(from rebeccapurple h w b / .2)", "rgba(102, 51, 153, 0.2)");
+    test(
+      "hwb(from rgb(20%, 40%, 60%, 80%) 25 w b / alpha)",
+      "rgba(153, 94, 51, 0.8)",
+    );
+    test(
+      "hwb(from rgb(20%, 40%, 60%, 80%) 25deg w b / alpha)",
+      "rgba(153, 94, 51, 0.8)",
+    );
+    test(
+      "hwb(from rgb(20%, 40%, 60%, 80%) h 20% b / alpha)",
+      "rgba(51, 102, 153, 0.8)",
+    );
+    test(
+      "hwb(from rgb(20%, 40%, 60%, 80%) h w 20% / alpha)",
+      "rgba(51, 128, 204, 0.8)",
+    );
+    test(
+      "hwb(from rgb(20%, 40%, 60%, 80%) h w b / .2)",
+      "rgba(51, 102, 153, 0.2)",
+    );
+
+    // Testing valid permutation (types match).
+    test("hwb(from rebeccapurple h b w)", "rgb(153, 102, 204)");
+    test(
+      "hwb(from rebeccapurple h calc(alpha * 100) w / calc(b / 100))",
+      "rgba(213, 213, 213, 0.4)",
+    );
+    test(
+      "hwb(from rebeccapurple h w w / calc(w / 100))",
+      "rgba(128, 51, 204, 0.2)",
+    );
+    test(
+      "hwb(from rebeccapurple h calc(alpha * 100) calc(alpha * 100) / alpha)",
+      "rgb(128, 128, 128)",
+    );
+    test("hwb(from rgb(20%, 40%, 60%, 80%) h b w)", "rgb(102, 153, 204)");
+    test(
+      "hwb(from rgb(20%, 40%, 60%, 80%) h calc(alpha * 100) w / calc(b / 100))",
+      "rgba(204, 204, 204, 0.4)",
+    );
+    test(
+      "hwb(from rgb(20%, 40%, 60%, 80%) h w w / calc(w / 100))",
+      "rgba(51, 128, 204, 0.2)",
+    );
+    test(
+      "hwb(from rgb(20%, 40%, 60%, 80%) h calc(alpha * 100) calc(alpha * 100) / alpha)",
+      "rgba(128, 128, 128, 0.8)",
+    );
+
+    // Testing with calc().
+    test("hwb(from rebeccapurple calc(h) calc(w) calc(b))", "rgb(102, 51, 153)");
+    test(
+      "hwb(from rgb(20%, 40%, 60%, 80%) calc(h) calc(w) calc(b) / calc(alpha))",
+      "rgba(51, 102, 153, 0.8)",
+    );
+
+    // Testing with 'none'.
+    test("hwb(from rebeccapurple none none none)", "rgb(255, 0, 0)");
+    test("hwb(from rebeccapurple none none none / none)", "rgba(255, 0, 0, 0)");
+    test("hwb(from rebeccapurple h w none)", "rgb(153, 51, 255)");
+    test("hwb(from rebeccapurple h w none / alpha)", "rgb(153, 51, 255)");
+    test("hwb(from rebeccapurple h w b / none)", "rgba(102, 51, 153, 0)");
+    test("hwb(from rebeccapurple none w b / alpha)", "rgb(153, 51, 51)");
+    test(
+      "hwb(from hwb(120deg 20% 50% / .5) h w none / alpha)",
+      "rgba(51, 255, 51, 0.5)",
+    );
+    test(
+      "hwb(from hwb(120deg 20% 50% / .5) h w b / none)",
+      "rgba(51, 128, 51, 0)",
+    );
+    test(
+      "hwb(from hwb(120deg 20% 50% / .5) none w b / alpha)",
+      "rgba(128, 51, 51, 0.5)",
+    );
+    // FIXME: Clarify with spec editors if 'none' should pass through to the constants.
+    test("hwb(from hwb(none none none) h w b)", "rgb(255, 0, 0)");
+    test(
+      "hwb(from hwb(none none none / none) h w b / alpha)",
+      "rgba(255, 0, 0, 0)",
+    );
+    test("hwb(from hwb(120deg none 50% / .5) h w b)", "rgb(0, 128, 0)");
+    test(
+      "hwb(from hwb(120deg 20% 50% / none) h w b / alpha)",
+      "rgba(51, 128, 51, 0)",
+    );
+    test(
+      "hwb(from hwb(none 20% 50% / .5) h w b / alpha)",
+      "rgba(128, 51, 51, 0.5)",
+    );
+
+    for color_space in &["lab", "oklab"] {
+      // Testing no modifications.
+      test(
+        &format!("{}(from {}(25% 20 50) l a b)", color_space, color_space),
+        &format!("{}(25% 20 50)", color_space),
+      );
+      test(
+        &format!("{}(from {}(25% 20 50) l a b / alpha)", color_space, color_space),
+        &format!("{}(25% 20 50)", color_space),
+      );
+      test(
+        &format!("{}(from {}(25% 20 50 / 40%) l a b / alpha)", color_space, color_space),
+        &format!("{}(25% 20 50 / 0.4)", color_space),
+      );
+      test(
+        &format!(
+          "{}(from {}(200% 300 400 / 500%) l a b / alpha)",
+          color_space, color_space
+        ),
+        &format!("{}(200% 300 400)", color_space),
+      );
+      test(
+        &format!(
+          "{}(from {}(-200% -300 -400 / -500%) l a b / alpha)",
+          color_space, color_space
+        ),
+        &format!("{}(0% -300 -400 / 0)", color_space),
+      );
+
+      // Test nesting relative colors.
+      test(
+        &format!(
+          "{}(from {}(from {}(25% 20 50) l a b) l a b)",
+          color_space, color_space, color_space
+        ),
+        &format!("{}(25% 20 50)", color_space),
+      );
+
+      // Testing non-${colorSpace} origin to see conversion.
+      test(
+        &format!("{}(from color(display-p3 0 0 0) l a b / alpha)", color_space),
+        &format!("{}(0% 0 0)", color_space),
+      );
+
+      // Testing replacement with 0.
+      test(
+        &format!("{}(from {}(25% 20 50) 0% 0 0)", color_space, color_space),
+        &format!("{}(0% 0 0)", color_space),
+      );
+      test(
+        &format!("{}(from {}(25% 20 50) 0% 0 0 / 0)", color_space, color_space),
+        &format!("{}(0% 0 0 / 0)", color_space),
+      );
+      test(
+        &format!("{}(from {}(25% 20 50) 0% a b / alpha)", color_space, color_space),
+        &format!("{}(0% 20 50)", color_space),
+      );
+      test(
+        &format!("{}(from {}(25% 20 50) l 0 b / alpha)", color_space, color_space),
+        &format!("{}(25% 0 50)", color_space),
+      );
+      test(
+        &format!("{}(from {}(25% 20 50) l a 0 / alpha)", color_space, color_space),
+        &format!("{}(25% 20 0)", color_space),
+      );
+      test(
+        &format!("{}(from {}(25% 20 50) l a b / 0)", color_space, color_space),
+        &format!("{}(25% 20 50 / 0)", color_space),
+      );
+      test(
+        &format!("{}(from {}(25% 20 50 / 40%) 0% a b / alpha)", color_space, color_space),
+        &format!("{}(0% 20 50 / 0.4)", color_space),
+      );
+      test(
+        &format!("{}(from {}(25% 20 50 / 40%) l 0 b / alpha)", color_space, color_space),
+        &format!("{}(25% 0 50 / 0.4)", color_space),
+      );
+      test(
+        &format!("{}(from {}(25% 20 50 / 40%) l a 0 / alpha)", color_space, color_space),
+        &format!("{}(25% 20 0 / 0.4)", color_space),
+      );
+      test(
+        &format!("{}(from {}(25% 20 50 / 40%) l a b / 0)", color_space, color_space),
+        &format!("{}(25% 20 50 / 0)", color_space),
+      );
+
+      // Testing replacement with a constant.
+      test(
+        &format!("{}(from {}(25% 20 50) 35% a b / alpha)", color_space, color_space),
+        &format!("{}(35% 20 50)", color_space),
+      );
+      test(
+        &format!("{}(from {}(25% 20 50) l 35 b / alpha)", color_space, color_space),
+        &format!("{}(25% 35 50)", color_space),
+      );
+      test(
+        &format!("{}(from {}(25% 20 50) l a 35 / alpha)", color_space, color_space),
+        &format!("{}(25% 20 35)", color_space),
+      );
+      test(
+        &format!("{}(from {}(25% 20 50) l a b / .35)", color_space, color_space),
+        &format!("{}(25% 20 50 / 0.35)", color_space),
+      );
+      test(
+        &format!("{}(from {}(25% 20 50 / 40%) 35% a b / alpha)", color_space, color_space),
+        &format!("{}(35% 20 50 / 0.4)", color_space),
+      );
+      test(
+        &format!("{}(from {}(25% 20 50 / 40%) l 35 b / alpha)", color_space, color_space),
+        &format!("{}(25% 35 50 / 0.4)", color_space),
+      );
+      test(
+        &format!("{}(from {}(25% 20 50 / 40%) l a 35 / alpha)", color_space, color_space),
+        &format!("{}(25% 20 35 / 0.4)", color_space),
+      );
+      test(
+        &format!("{}(from {}(25% 20 50 / 40%) l a b / .35)", color_space, color_space),
+        &format!("{}(25% 20 50 / 0.35)", color_space),
+      );
+      test(
+        &format!(
+          "{}(from {}(70% 45 30 / 40%) 200% 300 400 / 500)",
+          color_space, color_space
+        ),
+        &format!("{}(200% 300 400)", color_space),
+      );
+      test(
+        &format!(
+          "{}(from {}(70% 45 30 / 40%) -200% -300 -400 / -500)",
+          color_space, color_space
+        ),
+        &format!("{}(0% -300 -400 / 0)", color_space),
+      );
+
+      // Testing valid permutation (types match).
+      test(
+        &format!("{}(from {}(25% 20 50) l b a)", color_space, color_space),
+        &format!("{}(25% 50 20)", color_space),
+      );
+      test(
+        &format!("{}(from {}(25% 20 50) l a a / a)", color_space, color_space),
+        &format!("{}(25% 20 20)", color_space),
+      );
+      test(
+        &format!("{}(from {}(25% 20 50 / 40%) l b a)", color_space, color_space),
+        &format!("{}(25% 50 20)", color_space),
+      );
+      test(
+        &format!("{}(from {}(25% 20 50 / 40%) l a a / a)", color_space, color_space),
+        &format!("{}(25% 20 20)", color_space),
+      );
+
+      // Testing with calc().
+      test(
+        &format!(
+          "{}(from {}(25% 20 50) calc(l) calc(a) calc(b))",
+          color_space, color_space
+        ),
+        &format!("{}(25% 20 50)", color_space),
+      );
+      test(
+        &format!(
+          "{}(from {}(25% 20 50 / 40%) calc(l) calc(a) calc(b) / calc(alpha))",
+          color_space, color_space
+        ),
+        &format!("{}(25% 20 50 / 0.4)", color_space),
+      );
+
+      // Testing with 'none'.
+      test(
+        &format!("{}(from {}(25% 20 50) none none none)", color_space, color_space),
+        &format!("{}(none none none)", color_space),
+      );
+      test(
+        &format!("{}(from {}(25% 20 50) none none none / none)", color_space, color_space),
+        &format!("{}(none none none / none)", color_space),
+      );
+      test(
+        &format!("{}(from {}(25% 20 50) l a none)", color_space, color_space),
+        &format!("{}(25% 20 none)", color_space),
+      );
+      test(
+        &format!("{}(from {}(25% 20 50) l a none / alpha)", color_space, color_space),
+        &format!("{}(25% 20 none)", color_space),
+      );
+      test(
+        &format!("{}(from {}(25% 20 50) l a b / none)", color_space, color_space),
+        &format!("{}(25% 20 50 / none)", color_space),
+      );
+      test(
+        &format!(
+          "{}(from {}(25% 20 50 / 40%) l a none / alpha)",
+          color_space, color_space
+        ),
+        &format!("{}(25% 20 none / 0.4)", color_space),
+      );
+      test(
+        &format!("{}(from {}(25% 20 50 / 40%) l a b / none)", color_space, color_space),
+        &format!("{}(25% 20 50 / none)", color_space),
+      );
+      // FIXME: Clarify with spec editors if 'none' should pass through to the constants.
+      test(
+        &format!("{}(from {}(none none none) l a b)", color_space, color_space),
+        &format!("{}(0% 0 0)", color_space),
+      );
+      test(
+        &format!(
+          "{}(from {}(none none none / none) l a b / alpha)",
+          color_space, color_space
+        ),
+        &format!("{}(0% 0 0 / 0)", color_space),
+      );
+      test(
+        &format!("{}(from {}(25% none 50) l a b)", color_space, color_space),
+        &format!("{}(25% 0 50)", color_space),
+      );
+      test(
+        &format!("{}(from {}(25% 20 50 / none) l a b / alpha)", color_space, color_space),
+        &format!("{}(25% 20 50 / 0)", color_space),
+      );
+    }
+
+    // test_valid_value\(`color`, `\$\{colorSpace\}\(from \$\{colorSpace\}\((.*?)`,\s*`\$\{colorSpace\}(.*?)`\)
+    // test(&format!("{}(from {}($1", color_space, color_space), &format!("{}$2", color_space))
+
+    for color_space in &["lch", "oklch"] {
+      // Testing no modifications.
+      test(
+        &format!("{}(from {}(70% 45 30) l c h)", color_space, color_space),
+        &format!("{}(70% 45 30)", color_space),
+      );
+      test(
+        &format!("{}(from {}(70% 45 30) l c h / alpha)", color_space, color_space),
+        &format!("{}(70% 45 30)", color_space),
+      );
+      test(
+        &format!("{}(from {}(70% 45 30 / 40%) l c h / alpha)", color_space, color_space),
+        &format!("{}(70% 45 30 / 0.4)", color_space),
+      );
+      test(
+        &format!(
+          "{}(from {}(200% 300 400 / 500%) l c h / alpha)",
+          color_space, color_space
+        ),
+        &format!("{}(200% 300 40)", color_space),
+      );
+      test(
+        &format!(
+          "{}(from {}(-200% -300 -400 / -500%) l c h / alpha)",
+          color_space, color_space
+        ),
+        &format!("{}(0% 0 320 / 0)", color_space),
+      );
+
+      // Test nesting relative colors.
+      test(
+        &format!(
+          "{}(from {}(from {}(70% 45 30) l c h) l c h)",
+          color_space, color_space, color_space
+        ),
+        &format!("{}(70% 45 30)", color_space),
+      );
+
+      // Testing non-sRGB origin colors (no gamut mapping will happen since the destination is not a bounded RGB color space).
+      test(
+        &format!("{}(from color(display-p3 0 0 0) l c h / alpha)", color_space),
+        &format!("{}(0% 0 0)", color_space),
+      );
+
+      // Testing replacement with 0.
+      test(
+        &format!("{}(from {}(70% 45 30) 0% 0 0)", color_space, color_space),
+        &format!("{}(0% 0 0)", color_space),
+      );
+      test(
+        &format!("{}(from {}(70% 45 30) 0% 0 0deg)", color_space, color_space),
+        &format!("{}(0% 0 0)", color_space),
+      );
+      test(
+        &format!("{}(from {}(70% 45 30) 0% 0 0 / 0)", color_space, color_space),
+        &format!("{}(0% 0 0 / 0)", color_space),
+      );
+      test(
+        &format!("{}(from {}(70% 45 30) 0% 0 0deg / 0)", color_space, color_space),
+        &format!("{}(0% 0 0 / 0)", color_space),
+      );
+      test(
+        &format!("{}(from {}(70% 45 30) 0% c h / alpha)", color_space, color_space),
+        &format!("{}(0% 45 30)", color_space),
+      );
+      test(
+        &format!("{}(from {}(70% 45 30) l 0 h / alpha)", color_space, color_space),
+        &format!("{}(70% 0 30)", color_space),
+      );
+      test(
+        &format!("{}(from {}(70% 45 30) l c 0 / alpha)", color_space, color_space),
+        &format!("{}(70% 45 0)", color_space),
+      );
+      test(
+        &format!("{}(from {}(70% 45 30) l c 0deg / alpha)", color_space, color_space),
+        &format!("{}(70% 45 0)", color_space),
+      );
+      test(
+        &format!("{}(from {}(70% 45 30) l c h / 0)", color_space, color_space),
+        &format!("{}(70% 45 30 / 0)", color_space),
+      );
+      test(
+        &format!("{}(from {}(70% 45 30 / 40%) 0% c h / alpha)", color_space, color_space),
+        &format!("{}(0% 45 30 / 0.4)", color_space),
+      );
+      test(
+        &format!("{}(from {}(70% 45 30 / 40%) l 0 h / alpha)", color_space, color_space),
+        &format!("{}(70% 0 30 / 0.4)", color_space),
+      );
+      test(
+        &format!("{}(from {}(70% 45 30 / 40%) l c 0 / alpha)", color_space, color_space),
+        &format!("{}(70% 45 0 / 0.4)", color_space),
+      );
+      test(
+        &format!(
+          "{}(from {}(70% 45 30 / 40%) l c 0deg / alpha)",
+          color_space, color_space
+        ),
+        &format!("{}(70% 45 0 / 0.4)", color_space),
+      );
+      test(
+        &format!("{}(from {}(70% 45 30 / 40%) l c h / 0)", color_space, color_space),
+        &format!("{}(70% 45 30 / 0)", color_space),
+      );
+
+      // Testing replacement with a constant.
+      test(
+        &format!("{}(from {}(70% 45 30) 25% c h / alpha)", color_space, color_space),
+        &format!("{}(25% 45 30)", color_space),
+      );
+      test(
+        &format!("{}(from {}(70% 45 30) l 25 h / alpha)", color_space, color_space),
+        &format!("{}(70% 25 30)", color_space),
+      );
+      test(
+        &format!("{}(from {}(70% 45 30) l c 25 / alpha)", color_space, color_space),
+        &format!("{}(70% 45 25)", color_space),
+      );
+      test(
+        &format!("{}(from {}(70% 45 30) l c 25deg / alpha)", color_space, color_space),
+        &format!("{}(70% 45 25)", color_space),
+      );
+      test(
+        &format!("{}(from {}(70% 45 30) l c h / .25)", color_space, color_space),
+        &format!("{}(70% 45 30 / 0.25)", color_space),
+      );
+      test(
+        &format!("{}(from {}(70% 45 30 / 40%) 25% c h / alpha)", color_space, color_space),
+        &format!("{}(25% 45 30 / 0.4)", color_space),
+      );
+      test(
+        &format!("{}(from {}(70% 45 30 / 40%) l 25 h / alpha)", color_space, color_space),
+        &format!("{}(70% 25 30 / 0.4)", color_space),
+      );
+      test(
+        &format!("{}(from {}(70% 45 30 / 40%) l c 25 / alpha)", color_space, color_space),
+        &format!("{}(70% 45 25 / 0.4)", color_space),
+      );
+      test(
+        &format!(
+          "{}(from {}(70% 45 30 / 40%) l c 25deg / alpha)",
+          color_space, color_space
+        ),
+        &format!("{}(70% 45 25 / 0.4)", color_space),
+      );
+      test(
+        &format!("{}(from {}(70% 45 30 / 40%) l c h / .25)", color_space, color_space),
+        &format!("{}(70% 45 30 / 0.25)", color_space),
+      );
+      test(
+        &format!(
+          "{}(from {}(70% 45 30 / 40%) 200% 300 400 / 500)",
+          color_space, color_space
+        ),
+        &format!("{}(200% 300 400)", color_space),
+      );
+      test(
+        &format!(
+          "{}(from {}(70% 45 30 / 40%) -200% -300 -400 / -500)",
+          color_space, color_space
+        ),
+        &format!("{}(0% 0 -400 / 0)", color_space),
+      );
+      test(
+        &format!(
+          "{}(from {}(70% 45 30 / 40%) 50% 120 400deg / 500)",
+          color_space, color_space
+        ),
+        &format!("{}(50% 120 400)", color_space),
+      );
+      test(
+        &format!(
+          "{}(from {}(70% 45 30 / 40%) 50% 120 -400deg / -500)",
+          color_space, color_space
+        ),
+        &format!("{}(50% 120 -400 / 0)", color_space),
+      );
+
+      // Testing valid permutation (types match).
+      // NOTE: 'c' is a valid hue, as hue is <angle>|<number>.
+      test(
+        &format!("{}(from {}(70% 45 30) alpha c h / l)", color_space, color_space),
+        &format!(
+          "{}(1 45 30 / {})",
+          color_space,
+          if *color_space == "lch" { "1" } else { ".7" }
+        ),
+      );
+      test(
+        &format!("{}(from {}(70% 45 30) l c c / alpha)", color_space, color_space),
+        &format!("{}(70% 45 45)", color_space),
+      );
+      test(
+        &format!("{}(from {}(70% 45 30) alpha c h / alpha)", color_space, color_space),
+        &format!("{}(1 45 30)", color_space),
+      );
+      test(
+        &format!("{}(from {}(70% 45 30) alpha c c / alpha)", color_space, color_space),
+        &format!("{}(1 45 45)", color_space),
+      );
+      test(
+        &format!("{}(from {}(70% 45 30 / 40%) alpha c h / l)", color_space, color_space),
+        &format!(
+          "{}(.4 45 30 / {})",
+          color_space,
+          if *color_space == "lch" { "1" } else { ".7" }
+        ),
+      );
+      test(
+        &format!("{}(from {}(70% 45 30 / 40%) l c c / alpha)", color_space, color_space),
+        &format!("{}(70% 45 45 / 0.4)", color_space),
+      );
+      test(
+        &format!(
+          "{}(from {}(70% 45 30 / 40%) alpha c h / alpha)",
+          color_space, color_space
+        ),
+        &format!("{}(.4 45 30 / 0.4)", color_space),
+      );
+      test(
+        &format!(
+          "{}(from {}(70% 45 30 / 40%) alpha c c / alpha)",
+          color_space, color_space
+        ),
+        &format!("{}(.4 45 45 / 0.4)", color_space),
+      );
+
+      // Testing with calc().
+      test(
+        &format!(
+          "{}(from {}(70% 45 30) calc(l) calc(c) calc(h))",
+          color_space, color_space
+        ),
+        &format!("{}(70% 45 30)", color_space),
+      );
+      test(
+        &format!(
+          "{}(from {}(70% 45 30 / 40%) calc(l) calc(c) calc(h) / calc(alpha))",
+          color_space, color_space
+        ),
+        &format!("{}(70% 45 30 / 0.4)", color_space),
+      );
+
+      // Testing with 'none'.
+      test(
+        &format!("{}(from {}(70% 45 30) none none none)", color_space, color_space),
+        &format!("{}(none none none)", color_space),
+      );
+      test(
+        &format!("{}(from {}(70% 45 30) none none none / none)", color_space, color_space),
+        &format!("{}(none none none / none)", color_space),
+      );
+      test(
+        &format!("{}(from {}(70% 45 30) l c none)", color_space, color_space),
+        &format!("{}(70% 45 none)", color_space),
+      );
+      test(
+        &format!("{}(from {}(70% 45 30) l c none / alpha)", color_space, color_space),
+        &format!("{}(70% 45 none)", color_space),
+      );
+      test(
+        &format!("{}(from {}(70% 45 30) l c h / none)", color_space, color_space),
+        &format!("{}(70% 45 30 / none)", color_space),
+      );
+      test(
+        &format!(
+          "{}(from {}(70% 45 30 / 40%) l c none / alpha)",
+          color_space, color_space
+        ),
+        &format!("{}(70% 45 none / 0.4)", color_space),
+      );
+      test(
+        &format!("{}(from {}(70% 45 30 / 40%) l c h / none)", color_space, color_space),
+        &format!("{}(70% 45 30 / none)", color_space),
+      );
+      // FIXME: Clarify with spec editors if 'none' should pass through to the constants.
+      test(
+        &format!("{}(from {}(none none none) l c h)", color_space, color_space),
+        &format!("{}(0% 0 0)", color_space),
+      );
+      test(
+        &format!(
+          "{}(from {}(none none none / none) l c h / alpha)",
+          color_space, color_space
+        ),
+        &format!("{}(0% 0 0 / 0)", color_space),
+      );
+      test(
+        &format!("{}(from {}(70% none 30) l c h)", color_space, color_space),
+        &format!("{}(70% 0 30)", color_space),
+      );
+      test(
+        &format!("{}(from {}(70% 45 30 / none) l c h / alpha)", color_space, color_space),
+        &format!("{}(70% 45 30 / 0)", color_space),
+      );
+    }
+
+    // test_valid_value\(`color`, `color\(from color\(\$\{colorSpace\}(.*?) \$\{colorSpace\}(.*?)`,\s*`color\(\$\{colorSpace\}(.*?)`\)
+    // test(&format!("color(from color({}$1 {}$2", color_space, color_space), &format!("color({}$3", color_space))
+
+    for color_space in &["srgb", "srgb-linear", "a98-rgb", "rec2020", "prophoto-rgb"] {
+      // Testing no modifications.
+      test(
+        &format!("color(from color({} 0.7 0.5 0.3) {} r g b)", color_space, color_space),
+        &format!("color({} 0.7 0.5 0.3)", color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 0.7 0.5 0.3) {} r g b / alpha)",
+          color_space, color_space
+        ),
+        &format!("color({} 0.7 0.5 0.3)", color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 0.7 0.5 0.3 / 40%) {} r g b)",
+          color_space, color_space
+        ),
+        &format!("color({} 0.7 0.5 0.3)", color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 0.7 0.5 0.3 / 40%) {} r g b / alpha)",
+          color_space, color_space
+        ),
+        &format!("color({} 0.7 0.5 0.3 / 0.4)", color_space),
+      );
+
+      // Test nesting relative colors.
+      test(
+        &format!(
+          "color(from color(from color({} 0.7 0.5 0.3) {} r g b) {} r g b)",
+          color_space, color_space, color_space
+        ),
+        &format!("color({} 0.7 0.5 0.3)", color_space),
+      );
+
+      // Testing replacement with 0.
+      test(
+        &format!("color(from color({} 0.7 0.5 0.3) {} 0 0 0)", color_space, color_space),
+        &format!("color({} 0 0 0)", color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 0.7 0.5 0.3) {} 0 0 0 / 0)",
+          color_space, color_space
+        ),
+        &format!("color({} 0 0 0 / 0)", color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 0.7 0.5 0.3) {} 0 g b / alpha)",
+          color_space, color_space
+        ),
+        &format!("color({} 0 0.5 0.3)", color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 0.7 0.5 0.3) {} r 0 b / alpha)",
+          color_space, color_space
+        ),
+        &format!("color({} 0.7 0 0.3)", color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 0.7 0.5 0.3) {} r g 0 / alpha)",
+          color_space, color_space
+        ),
+        &format!("color({} 0.7 0.5 0)", color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 0.7 0.5 0.3) {} r g b / 0)",
+          color_space, color_space
+        ),
+        &format!("color({} 0.7 0.5 0.3 / 0)", color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 0.7 0.5 0.3 / 40%) {} 0 g b / alpha)",
+          color_space, color_space
+        ),
+        &format!("color({} 0 0.5 0.3 / 0.4)", color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 0.7 0.5 0.3 / 40%) {} r 0 b / alpha)",
+          color_space, color_space
+        ),
+        &format!("color({} 0.7 0 0.3 / 0.4)", color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 0.7 0.5 0.3 / 40%) {} r g 0 / alpha)",
+          color_space, color_space
+        ),
+        &format!("color({} 0.7 0.5 0 / 0.4)", color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 0.7 0.5 0.3 / 40%) {} r g b / 0)",
+          color_space, color_space
+        ),
+        &format!("color({} 0.7 0.5 0.3 / 0)", color_space),
+      );
+
+      // Testing replacement with a constant.
+      test(
+        &format!(
+          "color(from color({} 0.7 0.5 0.3) {} 0.2 g b / alpha)",
+          color_space, color_space
+        ),
+        &format!("color({} 0.2 0.5 0.3)", color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 0.7 0.5 0.3) {} 20% g b / alpha)",
+          color_space, color_space
+        ),
+        &format!("color({} 0.2 0.5 0.3)", color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 0.7 0.5 0.3) {} r 0.2 b / alpha)",
+          color_space, color_space
+        ),
+        &format!("color({} 0.7 0.2 0.3)", color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 0.7 0.5 0.3) {} r 20% b / alpha)",
+          color_space, color_space
+        ),
+        &format!("color({} 0.7 0.2 0.3)", color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 0.7 0.5 0.3) {} r g 0.2 / alpha)",
+          color_space, color_space
+        ),
+        &format!("color({} 0.7 0.5 0.2)", color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 0.7 0.5 0.3) {} r g 20% / alpha)",
+          color_space, color_space
+        ),
+        &format!("color({} 0.7 0.5 0.2)", color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 0.7 0.5 0.3) {} r g b / 0.2)",
+          color_space, color_space
+        ),
+        &format!("color({} 0.7 0.5 0.3 / 0.2)", color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 0.7 0.5 0.3) {} r g b / 20%)",
+          color_space, color_space
+        ),
+        &format!("color({} 0.7 0.5 0.3 / 0.2)", color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 0.7 0.5 0.3 / 40%) {} 0.2 g b / alpha)",
+          color_space, color_space
+        ),
+        &format!("color({} 0.2 0.5 0.3 / 0.4)", color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 0.7 0.5 0.3 / 40%) {} 20% g b / alpha)",
+          color_space, color_space
+        ),
+        &format!("color({} 0.2 0.5 0.3 / 0.4)", color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 0.7 0.5 0.3 / 40%) {} r 0.2 b / alpha)",
+          color_space, color_space
+        ),
+        &format!("color({} 0.7 0.2 0.3 / 0.4)", color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 0.7 0.5 0.3 / 40%) {} r 20% b / alpha)",
+          color_space, color_space
+        ),
+        &format!("color({} 0.7 0.2 0.3 / 0.4)", color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 0.7 0.5 0.3 / 40%) {} r g 0.2 / alpha)",
+          color_space, color_space
+        ),
+        &format!("color({} 0.7 0.5 0.2 / 0.4)", color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 0.7 0.5 0.3 / 40%) {} r g 20% / alpha)",
+          color_space, color_space
+        ),
+        &format!("color({} 0.7 0.5 0.2 / 0.4)", color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 0.7 0.5 0.3 / 40%) {} r g b / 0.2)",
+          color_space, color_space
+        ),
+        &format!("color({} 0.7 0.5 0.3 / 0.2)", color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 0.7 0.5 0.3 / 40%) {} r g b / 20%)",
+          color_space, color_space
+        ),
+        &format!("color({} 0.7 0.5 0.3 / 0.2)", color_space),
+      );
+      test(
+        &format!("color(from color({} 0.7 0.5 0.3) {} 2 3 4)", color_space, color_space),
+        &format!("color({} 2 3 4)", color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 0.7 0.5 0.3) {} 2 3 4 / 5)",
+          color_space, color_space
+        ),
+        &format!("color({} 2 3 4)", color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 0.7 0.5 0.3) {} -2 -3 -4)",
+          color_space, color_space
+        ),
+        &format!("color({} -2 -3 -4)", color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 0.7 0.5 0.3) {} -2 -3 -4 / -5)",
+          color_space, color_space
+        ),
+        &format!("color({} -2 -3 -4 / 0)", color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 0.7 0.5 0.3) {} 200% 300% 400%)",
+          color_space, color_space
+        ),
+        &format!("color({} 2 3 4)", color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 0.7 0.5 0.3) {} 200% 300% 400% / 500%)",
+          color_space, color_space
+        ),
+        &format!("color({} 2 3 4)", color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 0.7 0.5 0.3) {} -200% -300% -400%)",
+          color_space, color_space
+        ),
+        &format!("color({} -2 -3 -4)", color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 0.7 0.5 0.3) {} -200% -300% -400% / -500%)",
+          color_space, color_space
+        ),
+        &format!("color({} -2 -3 -4 / 0)", color_space),
+      );
+
+      // Testing valid permutation (types match).
+      test(
+        &format!("color(from color({} 0.7 0.5 0.3) {} g b r)", color_space, color_space),
+        &format!("color({} 0.5 0.3 0.7)", color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 0.7 0.5 0.3) {} b alpha r / g)",
+          color_space, color_space
+        ),
+        &format!("color({} 0.3 1 0.7 / 0.5)", color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 0.7 0.5 0.3) {} r r r / r)",
+          color_space, color_space
+        ),
+        &format!("color({} 0.7 0.7 0.7 / 0.7)", color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 0.7 0.5 0.3) {} alpha alpha alpha / alpha)",
+          color_space, color_space
+        ),
+        &format!("color({} 1 1 1)", color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 0.7 0.5 0.3 / 40%) {} g b r)",
+          color_space, color_space
+        ),
+        &format!("color({} 0.5 0.3 0.7)", color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 0.7 0.5 0.3 / 40%) {} b alpha r / g)",
+          color_space, color_space
+        ),
+        &format!("color({} 0.3 0.4 0.7 / 0.5)", color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 0.7 0.5 0.3 / 40%) {} r r r / r)",
+          color_space, color_space
+        ),
+        &format!("color({} 0.7 0.7 0.7 / 0.7)", color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 0.7 0.5 0.3 / 40%) {} alpha alpha alpha / alpha)",
+          color_space, color_space
+        ),
+        &format!("color({} 0.4 0.4 0.4 / 0.4)", color_space),
+      );
+
+      // Testing out of gamut components.
+      test(
+        &format!("color(from color({} 1.7 1.5 1.3) {} r g b)", color_space, color_space),
+        &format!("color({} 1.7 1.5 1.3)", color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 1.7 1.5 1.3) {} r g b / alpha)",
+          color_space, color_space
+        ),
+        &format!("color({} 1.7 1.5 1.3)", color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 1.7 1.5 1.3 / 140%) {} r g b)",
+          color_space, color_space
+        ),
+        &format!("color({} 1.7 1.5 1.3)", color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 1.7 1.5 1.3 / 140%) {} r g b / alpha)",
+          color_space, color_space
+        ),
+        &format!("color({} 1.7 1.5 1.3)", color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} -0.7 -0.5 -0.3) {} r g b)",
+          color_space, color_space
+        ),
+        &format!("color({} -0.7 -0.5 -0.3)", color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} -0.7 -0.5 -0.3) {} r g b / alpha)",
+          color_space, color_space
+        ),
+        &format!("color({} -0.7 -0.5 -0.3)", color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} -0.7 -0.5 -0.3 / -40%) {} r g b)",
+          color_space, color_space
+        ),
+        &format!("color({} -0.7 -0.5 -0.3)", color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} -0.7 -0.5 -0.3 / -40%) {} r g b / alpha)",
+          color_space, color_space
+        ),
+        &format!("color({} -0.7 -0.5 -0.3 / 0)", color_space),
+      );
+
+      // Testing with calc().
+      test(
+        &format!(
+          "color(from color({} 0.7 0.5 0.3) {} calc(r) calc(g) calc(b))",
+          color_space, color_space
+        ),
+        &format!("color({} 0.7 0.5 0.3)", color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 0.7 0.5 0.3 / 40%) {} calc(r) calc(g) calc(b) / calc(alpha))",
+          color_space, color_space
+        ),
+        &format!("color({} 0.7 0.5 0.3 / 0.4)", color_space),
+      );
+
+      // Testing with 'none'.
+      test(
+        &format!(
+          "color(from color({} 0.7 0.5 0.3) {} none none none)",
+          color_space, color_space
+        ),
+        &format!("color({} none none none)", color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 0.7 0.5 0.3) {} none none none / none)",
+          color_space, color_space
+        ),
+        &format!("color({} none none none / none)", color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 0.7 0.5 0.3) {} r g none)",
+          color_space, color_space
+        ),
+        &format!("color({} 0.7 0.5 none)", color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 0.7 0.5 0.3) {} r g none / alpha)",
+          color_space, color_space
+        ),
+        &format!("color({} 0.7 0.5 none)", color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 0.7 0.5 0.3) {} r g b / none)",
+          color_space, color_space
+        ),
+        &format!("color({} 0.7 0.5 0.3 / none)", color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 0.7 0.5 0.3 / 40%) {} r g none / alpha)",
+          color_space, color_space
+        ),
+        &format!("color({} 0.7 0.5 none / 0.4)", color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 0.7 0.5 0.3 / 40%) {} r g b / none)",
+          color_space, color_space
+        ),
+        &format!("color({} 0.7 0.5 0.3 / none)", color_space),
+      );
+      // FIXME: Clarify with spec editors if 'none' should pass through to the constants.
+      test(
+        &format!(
+          "color(from color({} none none none) {} r g b)",
+          color_space, color_space
+        ),
+        &format!("color({} 0 0 0)", color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} none none none / none) {} r g b / alpha)",
+          color_space, color_space
+        ),
+        &format!("color({} 0 0 0 / 0)", color_space),
+      );
+      test(
+        &format!("color(from color({} 0.7 none 0.3) {} r g b)", color_space, color_space),
+        &format!("color({} 0.7 0 0.3)", color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 0.7 0.5 0.3 / none) {} r g b / alpha)",
+          color_space, color_space
+        ),
+        &format!("color({} 0.7 0.5 0.3 / 0)", color_space),
+      );
+    }
+
+    // test_valid_value\(`color`, `color\(from color\(\$\{colorSpace\}(.*?) \$\{colorSpace\}(.*?)`,\s*`color\(\$\{resultColorSpace\}(.*?)`\)
+    // test(&format!("color(from color({}$1 {}$2", color_space, color_space), &format!("color({}$3", result_color_space))
+
+    for color_space in &["xyz", "xyz-d50", "xyz-d65"] {
+      let result_color_space = if *color_space == "xyz" { "xyz-d65" } else { color_space };
+
+      // Testing no modifications.
+      test(
+        &format!("color(from color({} 7 -20.5 100) {} x y z)", color_space, color_space),
+        &format!("color({} 7 -20.5 100)", result_color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 7 -20.5 100) {} x y z / alpha)",
+          color_space, color_space
+        ),
+        &format!("color({} 7 -20.5 100)", result_color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 7 -20.5 100 / 40%) {} x y z)",
+          color_space, color_space
+        ),
+        &format!("color({} 7 -20.5 100)", result_color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 7 -20.5 100 / 40%) {} x y z / alpha)",
+          color_space, color_space
+        ),
+        &format!("color({} 7 -20.5 100 / 0.4)", result_color_space),
+      );
+
+      // Test nesting relative colors.
+      test(
+        &format!(
+          "color(from color(from color({} 7 -20.5 100) {} x y z) {} x y z)",
+          color_space, color_space, color_space
+        ),
+        &format!("color({} 7 -20.5 100)", result_color_space),
+      );
+
+      // Testing replacement with 0.
+      test(
+        &format!("color(from color({} 7 -20.5 100) {} 0 0 0)", color_space, color_space),
+        &format!("color({} 0 0 0)", result_color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 7 -20.5 100) {} 0 0 0 / 0)",
+          color_space, color_space
+        ),
+        &format!("color({} 0 0 0 / 0)", result_color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 7 -20.5 100) {} 0 y z / alpha)",
+          color_space, color_space
+        ),
+        &format!("color({} 0 -20.5 100)", result_color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 7 -20.5 100) {} x 0 z / alpha)",
+          color_space, color_space
+        ),
+        &format!("color({} 7 0 100)", result_color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 7 -20.5 100) {} x y 0 / alpha)",
+          color_space, color_space
+        ),
+        &format!("color({} 7 -20.5 0)", result_color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 7 -20.5 100) {} x y z / 0)",
+          color_space, color_space
+        ),
+        &format!("color({} 7 -20.5 100 / 0)", result_color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 7 -20.5 100 / 40%) {} 0 y z / alpha)",
+          color_space, color_space
+        ),
+        &format!("color({} 0 -20.5 100 / 0.4)", result_color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 7 -20.5 100 / 40%) {} x 0 z / alpha)",
+          color_space, color_space
+        ),
+        &format!("color({} 7 0 100 / 0.4)", result_color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 7 -20.5 100 / 40%) {} x y 0 / alpha)",
+          color_space, color_space
+        ),
+        &format!("color({} 7 -20.5 0 / 0.4)", result_color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 7 -20.5 100 / 40%) {} x y z / 0)",
+          color_space, color_space
+        ),
+        &format!("color({} 7 -20.5 100 / 0)", result_color_space),
+      );
+
+      // Testing replacement with a constant.
+      test(
+        &format!(
+          "color(from color({} 7 -20.5 100) {} 0.2 y z / alpha)",
+          color_space, color_space
+        ),
+        &format!("color({} 0.2 -20.5 100)", result_color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 7 -20.5 100) {} x 0.2 z / alpha)",
+          color_space, color_space
+        ),
+        &format!("color({} 7 0.2 100)", result_color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 7 -20.5 100) {} x y 0.2 / alpha)",
+          color_space, color_space
+        ),
+        &format!("color({} 7 -20.5 0.2)", result_color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 7 -20.5 100) {} x y z / 0.2)",
+          color_space, color_space
+        ),
+        &format!("color({} 7 -20.5 100 / 0.2)", result_color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 7 -20.5 100) {} x y z / 20%)",
+          color_space, color_space
+        ),
+        &format!("color({} 7 -20.5 100 / 0.2)", result_color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 7 -20.5 100 / 40%) {} 0.2 y z / alpha)",
+          color_space, color_space
+        ),
+        &format!("color({} 0.2 -20.5 100 / 0.4)", result_color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 7 -20.5 100 / 40%) {} x 0.2 z / alpha)",
+          color_space, color_space
+        ),
+        &format!("color({} 7 0.2 100 / 0.4)", result_color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 7 -20.5 100 / 40%) {} x y 0.2 / alpha)",
+          color_space, color_space
+        ),
+        &format!("color({} 7 -20.5 0.2 / 0.4)", result_color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 7 -20.5 100 / 40%) {} x y z / 0.2)",
+          color_space, color_space
+        ),
+        &format!("color({} 7 -20.5 100 / 0.2)", result_color_space),
+      );
+
+      // Testing valid permutation (types match).
+      test(
+        &format!("color(from color({} 7 -20.5 100) {} y z x)", color_space, color_space),
+        &format!("color({} -20.5 100 7)", result_color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 7 -20.5 100) {} x x x / x)",
+          color_space, color_space
+        ),
+        &format!("color({} 7 7 7)", result_color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 7 -20.5 100 / 40%) {} y z x)",
+          color_space, color_space
+        ),
+        &format!("color({} -20.5 100 7)", result_color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 7 -20.5 100 / 40%) {} x x x / x)",
+          color_space, color_space
+        ),
+        &format!("color({} 7 7 7)", result_color_space),
+      );
+
+      // Testing with calc().
+      test(
+        &format!(
+          "color(from color({} 7 -20.5 100) {} calc(x) calc(y) calc(z))",
+          color_space, color_space
+        ),
+        &format!("color({} 7 -20.5 100)", result_color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 7 -20.5 100 / 40%) {} calc(x) calc(y) calc(z) / calc(alpha))",
+          color_space, color_space
+        ),
+        &format!("color({} 7 -20.5 100 / 0.4)", result_color_space),
+      );
+
+      // Testing with 'none'.
+      test(
+        &format!(
+          "color(from color({} 7 -20.5 100) {} none none none)",
+          color_space, color_space
+        ),
+        &format!("color({} none none none)", result_color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 7 -20.5 100) {} none none none / none)",
+          color_space, color_space
+        ),
+        &format!("color({} none none none / none)", result_color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 7 -20.5 100) {} x y none)",
+          color_space, color_space
+        ),
+        &format!("color({} 7 -20.5 none)", result_color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 7 -20.5 100) {} x y none / alpha)",
+          color_space, color_space
+        ),
+        &format!("color({} 7 -20.5 none)", result_color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 7 -20.5 100) {} x y z / none)",
+          color_space, color_space
+        ),
+        &format!("color({} 7 -20.5 100 / none)", result_color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 7 -20.5 100 / 40%) {} x y none / alpha)",
+          color_space, color_space
+        ),
+        &format!("color({} 7 -20.5 none / 0.4)", result_color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 7 -20.5 100 / 40%) {} x y z / none)",
+          color_space, color_space
+        ),
+        &format!("color({} 7 -20.5 100 / none)", result_color_space),
+      );
+      // FIXME: Clarify with spec editors if 'none' should pass through to the constants.
+      test(
+        &format!(
+          "color(from color({} none none none) {} x y z)",
+          color_space, color_space
+        ),
+        &format!("color({} 0 0 0)", result_color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} none none none / none) {} x y z / alpha)",
+          color_space, color_space
+        ),
+        &format!("color({} 0 0 0 / 0)", result_color_space),
+      );
+      test(
+        &format!("color(from color({} 7 none 100) {} x y z)", color_space, color_space),
+        &format!("color({} 7 0 100)", result_color_space),
+      );
+      test(
+        &format!(
+          "color(from color({} 7 -20.5 100 / none) {} x y z / alpha)",
+          color_space, color_space
+        ),
+        &format!("color({} 7 -20.5 100 / 0)", result_color_space),
+      );
+
+      // https://github.com/web-platform-tests/wpt/blob/master/css/css-color/parsing/relative-color-invalid.html
+      minify_test(
+        ".foo{color:rgb(from rebeccapurple r 10deg 10)}",
+        ".foo{color:rgb(from rebeccapurple r 10deg 10)}",
+      );
+      minify_test(
+        ".foo{color:rgb(from rebeccapurple l g b)}",
+        ".foo{color:rgb(from rebeccapurple l g b)}",
+      );
+      minify_test(
+        ".foo{color:hsl(from rebeccapurple s h l)}",
+        ".foo{color:hsl(from rebeccapurple s h l)}",
+      );
+      minify_test(".foo{color:hsl(from rebeccapurple s s s / s)}", ".foo{color:#bfaa40}");
+      minify_test(
+        ".foo{color:hsl(from rebeccapurple calc(alpha * 100) calc(alpha * 100) calc(alpha * 100) / alpha)}",
+        ".foo{color:#fff}",
+      );
+    }
+  }
+
+  #[test]
+  fn test_color_mix() {
+    minify_test(
+      ".foo { color: color-mix(in lab, purple 50%, plum 50%); }",
+      ".foo{color:lab(51.5117% 43.3777 -29.0443)}",
+    );
+    minify_test(
+      ".foo { color: color-mix(in lch, peru 40%, palegoldenrod); }",
+      ".foo{color:lch(79.7255% 40.4542 84.7634)}",
+    );
+    minify_test(
+      ".foo { color: color-mix(in lch, teal 65%, olive); }",
+      ".foo{color:lch(49.4431% 40.4806 162.546)}",
+    );
+    minify_test(
+      ".foo { color: color-mix(in lch, white, black); }",
+      ".foo{color:lch(50% 0 none)}",
+    );
+    minify_test(
+      ".foo { color: color-mix(in xyz, rgb(82.02% 30.21% 35.02%) 75.23%, rgb(5.64% 55.94% 85.31%)); }",
+      ".foo{color:color(xyz .287458 .208776 .260566)}",
+    );
+    minify_test(
+      ".foo { color: color-mix(in lch, white, blue); }",
+      ".foo{color:lch(64.7842% 65.6007 301.364)}",
+    );
+    minify_test(
+      ".foo { color: color-mix(in oklch, white, blue); }",
+      ".foo{color:oklch(72.6007% .156607 264.052)}",
+    );
+    minify_test(
+      ".foo { color: color-mix(in srgb, white, blue); }",
+      ".foo{color:#8080ff}",
+    );
+    minify_test(
+      ".foo { color: color-mix(in lch, blue, white); }",
+      ".foo{color:lch(64.7842% 65.6007 301.364)}",
+    );
+    minify_test(
+      ".foo { color: color-mix(in oklch, blue, white); }",
+      ".foo{color:oklch(72.6007% .156607 264.052)}",
+    );
+    minify_test(
+      ".foo { color: color-mix(in srgb, blue, white); }",
+      ".foo{color:#8080ff}",
+    );
+    // minify_test(".foo { color: color-mix(in hsl, color(display-p3 0 1 0) 80%, yellow); }", ".foo{color:hsl(108 100% 49.9184%) }");
+    minify_test(
+      ".foo { color: color-mix(in hsl, hsl(120 100% 49.898%) 80%, yellow); }",
+      ".foo{color:#33fe00}",
+    );
+    minify_test(
+      ".foo { color: color-mix(in srgb, rgb(100% 0% 0% / 0.7) 25%, rgb(0% 100% 0% / 0.2)); }",
+      ".foo{color:#89760053}",
+    );
+    minify_test(
+      ".foo { color: color-mix(in srgb, rgb(100% 0% 0% / 0.7) 20%, rgb(0% 100% 0% / 0.2) 60%); }",
+      ".foo{color:#89760042}",
+    );
+    minify_test(
+      ".foo { color: color-mix(in lch, color(display-p3 0 1 none), color(display-p3 0 0 1)); }",
+      ".foo{color:lch(58.8143% 141.732 218.684)}",
+    );
+    minify_test(
+      ".foo { color: color-mix(in srgb, rgb(128 128 none), rgb(none none 128)); }",
+      ".foo{color:gray}",
+    );
+    minify_test(
+      ".foo { color: color-mix(in srgb, rgb(50% 50% none), rgb(none none 50%)); }",
+      ".foo{color:gray}",
+    );
+    minify_test(
+      ".foo { color: color-mix(in srgb, rgb(none 50% none), rgb(50% none 50%)); }",
+      ".foo{color:gray}",
+    );
+    minify_test(
+      ".foo { --color: color-mix(in lch, teal 65%, olive); }",
+      ".foo{--color:lch(49.4431% 40.4806 162.546)}",
+    );
+    minify_test(
+      ".foo { color: color-mix(in xyz, transparent, green 65%); }",
+      ".foo{color:color(xyz .0771883 .154377 .0257295/.65)}",
+    );
+    prefix_test(
+      ".foo { color: color-mix(in xyz, transparent, green 65%); }",
+      indoc! { r#"
+      .foo {
+        color: #008000a6;
+        color: color(xyz .0771883 .154377 .0257295 / .65);
+      }
+      "# },
+      Browsers {
+        chrome: Some(95 << 16),
+        ..Default::default()
+      },
+    );
+    minify_test(
+      ".foo { color: color-mix(in srgb, currentColor, blue); }",
+      ".foo{color:color-mix(in srgb,currentColor,blue)}",
+    );
+    minify_test(
+      ".foo { color: color-mix(in srgb, blue, currentColor); }",
+      ".foo{color:color-mix(in srgb,blue,currentColor)}",
+    );
+    minify_test(
+      ".foo { color: color-mix(in srgb, accentcolor, blue); }",
+      ".foo{color:color-mix(in srgb,accentcolor,blue)}",
+    );
+    minify_test(
+      ".foo { color: color-mix(in srgb, blue, accentcolor); }",
+      ".foo{color:color-mix(in srgb,blue,accentcolor)}",
+    );
+
+    // regex for converting web platform tests:
+    // test_computed_value\(.*?, `(.*?)`, `(.*?)`\);
+    // minify_test(".foo { color: $1 }", ".foo{color:$2}");
+
+    // https://github.com/web-platform-tests/wpt/blob/f8c76b11cff66a7adc87264a18e39353cb5a60c9/css/css-color/parsing/color-mix-computed.html
+    minify_test(
+      ".foo { color: color-mix(in hsl, hsl(120deg 10% 20%), hsl(30deg 30% 40%)) }",
+      ".foo{color:#545c3d}",
+    );
+    minify_test(
+      ".foo { color: color-mix(in hsl, hsl(120deg 10% 20%) 25%, hsl(30deg 30% 40%)) }",
+      ".foo{color:#706a43}",
+    );
+    minify_test(
+      ".foo { color: color-mix(in hsl, 25% hsl(120deg 10% 20%), hsl(30deg 30% 40%)) }",
+      ".foo{color:#706a43}",
+    );
+    minify_test(
+      ".foo { color: color-mix(in hsl, hsl(120deg 10% 20%), 25% hsl(30deg 30% 40%)) }",
+      ".foo{color:#3d4936}",
+    );
+    minify_test(
+      ".foo { color: color-mix(in hsl, hsl(120deg 10% 20%), hsl(30deg 30% 40%) 25%) }",
+      ".foo{color:#3d4936}",
+    );
+    minify_test(
+      ".foo { color: color-mix(in hsl, hsl(120deg 10% 20%) 25%, hsl(30deg 30% 40%) 75%) }",
+      ".foo{color:#706a43}",
+    );
+    minify_test(
+      ".foo { color: color-mix(in hsl, hsl(120deg 10% 20%) 30%, hsl(30deg 30% 40%) 90%) }",
+      ".foo{color:#706a43}",
+    ); // Scale down > 100% sum.
+    minify_test(
+      ".foo { color: color-mix(in hsl, hsl(120deg 10% 20%) 12.5%, hsl(30deg 30% 40%) 37.5%) }",
+      ".foo{color:#706a4380}",
+    ); // Scale up < 100% sum, causes alpha multiplication.
+    minify_test(
+      ".foo { color: color-mix(in hsl, hsl(120deg 10% 20%) 0%, hsl(30deg 30% 40%)) }",
+      ".foo{color:#856647}",
+    );
+
+    minify_test(
+      ".foo { color: color-mix(in hsl, hsl(120deg 10% 20% / .4), hsl(30deg 30% 40% / .8)) }",
+      ".foo{color:#5f694199}",
+    );
+    minify_test(
+      ".foo { color: color-mix(in hsl, hsl(120deg 10% 20%) 25%, hsl(30deg 30% 40% / .8)) }",
+      ".foo{color:#6c6742d9}",
+    );
+    minify_test(
+      ".foo { color: color-mix(in hsl, 25% hsl(120deg 10% 20% / .4), hsl(30deg 30% 40% / .8)) }",
+      ".foo{color:#797245b3}",
+    );
+    minify_test(
+      ".foo { color: color-mix(in hsl, hsl(120deg 10% 20% / .4), 25% hsl(30deg 30% 40% / .8)) }",
+      ".foo{color:#44543b80}",
+    );
+    minify_test(
+      ".foo { color: color-mix(in hsl, hsl(120deg 10% 20% / .4), hsl(30deg 30% 40% / .8) 25%) }",
+      ".foo{color:#44543b80}",
+    );
+    minify_test(
+      ".foo { color: color-mix(in hsl, hsl(120deg 10% 20% / .4) 25%, hsl(30deg 30% 40% / .8) 75%) }",
+      ".foo{color:#797245b3}",
+    );
+    minify_test(
+      ".foo { color: color-mix(in hsl, hsl(120deg 10% 20% / .4) 30%, hsl(30deg 30% 40% / .8) 90%) }",
+      ".foo{color:#797245b3}",
+    ); // Scale down > 100% sum.
+    minify_test(
+      ".foo { color: color-mix(in hsl, hsl(120deg 10% 20% / .4) 12.5%, hsl(30deg 30% 40% / .8) 37.5%) }",
+      ".foo{color:#79724559}",
+    ); // Scale up < 100% sum, causes alpha multiplication.
+    minify_test(
+      ".foo { color: color-mix(in hsl, hsl(120deg 10% 20% / .4) 0%, hsl(30deg 30% 40% / .8)) }",
+      ".foo{color:#856647cc}",
+    );
+
+    fn canonicalize(s: &str) -> String {
+      use crate::traits::{Parse, ToCss};
+      use crate::values::color::CssColor;
+      use cssparser::{Parser, ParserInput};
+
+      let mut input = ParserInput::new(s);
+      let mut parser = Parser::new(&mut input);
+      let v = CssColor::parse(&mut parser).unwrap().to_rgb().unwrap();
+      format!(".foo{{color:{}}}", v.to_css_string(PrinterOptions::default()).unwrap())
+    }
+
+    // regex for converting web platform tests:
+    // test_computed_value\(.*?, `(.*?)`, canonicalize\(`(.*?)`\)\);
+    // minify_test(".foo { color: $1 }", &canonicalize("$2"));
+
+    minify_test(
+      ".foo { color: color-mix(in hsl, hsl(40deg 50% 50%), hsl(60deg 50% 50%)) }",
+      &canonicalize("hsl(50deg 50% 50%)"),
+    );
+    minify_test(
+      ".foo { color: color-mix(in hsl, hsl(60deg 50% 50%), hsl(40deg 50% 50%)) }",
+      &canonicalize("hsl(50deg 50% 50%)"),
+    );
+    minify_test(
+      ".foo { color: color-mix(in hsl, hsl(50deg 50% 50%), hsl(330deg 50% 50%)) }",
+      &canonicalize("hsl(10deg 50% 50%)"),
+    );
+    minify_test(
+      ".foo { color: color-mix(in hsl, hsl(330deg 50% 50%), hsl(50deg 50% 50%)) }",
+      &canonicalize("hsl(10deg 50% 50%)"),
+    );
+    minify_test(
+      ".foo { color: color-mix(in hsl, hsl(20deg 50% 50%), hsl(320deg 50% 50%)) }",
+      &canonicalize("hsl(350deg 50% 50%)"),
+    );
+    minify_test(
+      ".foo { color: color-mix(in hsl, hsl(320deg 50% 50%), hsl(20deg 50% 50%)) }",
+      &canonicalize("hsl(350deg 50% 50%)"),
+    );
+
+    minify_test(
+      ".foo { color: color-mix(in hsl shorter hue, hsl(40deg 50% 50%), hsl(60deg 50% 50%)) }",
+      &canonicalize("hsl(50deg 50% 50%)"),
+    );
+    minify_test(
+      ".foo { color: color-mix(in hsl shorter hue, hsl(60deg 50% 50%), hsl(40deg 50% 50%)) }",
+      &canonicalize("hsl(50deg 50% 50%)"),
+    );
+    minify_test(
+      ".foo { color: color-mix(in hsl shorter hue, hsl(50deg 50% 50%), hsl(330deg 50% 50%)) }",
+      &canonicalize("hsl(10deg 50% 50%)"),
+    );
+    minify_test(
+      ".foo { color: color-mix(in hsl shorter hue, hsl(330deg 50% 50%), hsl(50deg 50% 50%)) }",
+      &canonicalize("hsl(10deg 50% 50%)"),
+    );
+    minify_test(
+      ".foo { color: color-mix(in hsl shorter hue, hsl(20deg 50% 50%), hsl(320deg 50% 50%)) }",
+      &canonicalize("hsl(350deg 50% 50%)"),
+    );
+    minify_test(
+      ".foo { color: color-mix(in hsl shorter hue, hsl(320deg 50% 50%), hsl(20deg 50% 50%)) }",
+      &canonicalize("hsl(350deg 50% 50%)"),
+    );
+
+    minify_test(
+      ".foo { color: color-mix(in hsl longer hue, hsl(40deg 50% 50%), hsl(60deg 50% 50%)) }",
+      &canonicalize("hsl(230deg 50% 50%)"),
+    );
+    minify_test(
+      ".foo { color: color-mix(in hsl longer hue, hsl(60deg 50% 50%), hsl(40deg 50% 50%)) }",
+      &canonicalize("hsl(230deg 50% 50%)"),
+    );
+    minify_test(
+      ".foo { color: color-mix(in hsl longer hue, hsl(50deg 50% 50%), hsl(330deg 50% 50%)) }",
+      &canonicalize("hsl(190deg 50% 50%)"),
+    );
+    minify_test(
+      ".foo { color: color-mix(in hsl longer hue, hsl(330deg 50% 50%), hsl(50deg 50% 50%)) }",
+      &canonicalize("hsl(190deg 50% 50%)"),
+    );
+    // minify_test(".foo { color: color-mix(in hsl longer hue, hsl(20deg 50% 50%), hsl(320deg 50% 50%)) }", &canonicalize("hsl(170deg 50% 50%)"));
+    // minify_test(".foo { color: color-mix(in hsl longer hue, hsl(320deg 50% 50%), hsl(20deg 50% 50%)) }", &canonicalize("hsl(170deg 50% 50%)"));
+
+    minify_test(
+      ".foo { color: color-mix(in hsl increasing hue, hsl(40deg 50% 50%), hsl(60deg 50% 50%)) }",
+      &canonicalize("hsl(50deg 50% 50%)"),
+    );
+    minify_test(
+      ".foo { color: color-mix(in hsl increasing hue, hsl(60deg 50% 50%), hsl(40deg 50% 50%)) }",
+      &canonicalize("hsl(230deg 50% 50%)"),
+    );
+    minify_test(
+      ".foo { color: color-mix(in hsl increasing hue, hsl(50deg 50% 50%), hsl(330deg 50% 50%)) }",
+      &canonicalize("hsl(190deg 50% 50%)"),
+    );
+    minify_test(
+      ".foo { color: color-mix(in hsl increasing hue, hsl(330deg 50% 50%), hsl(50deg 50% 50%)) }",
+      &canonicalize("hsl(10deg 50% 50%)"),
+    );
+    // minify_test(".foo { color: color-mix(in hsl increasing hue, hsl(20deg 50% 50%), hsl(320deg 50% 50%)) }", &canonicalize("hsl(170deg 50% 50%)"));
+    // minify_test(".foo { color: color-mix(in hsl increasing hue, hsl(320deg 50% 50%), hsl(20deg 50% 50%)) }", &canonicalize("hsl(350deg 50% 50%)"));
+
+    minify_test(
+      ".foo { color: color-mix(in hsl decreasing hue, hsl(40deg 50% 50%), hsl(60deg 50% 50%)) }",
+      &canonicalize("hsl(230deg 50% 50%)"),
+    );
+    minify_test(
+      ".foo { color: color-mix(in hsl decreasing hue, hsl(60deg 50% 50%), hsl(40deg 50% 50%)) }",
+      &canonicalize("hsl(50deg 50% 50%)"),
+    );
+    minify_test(
+      ".foo { color: color-mix(in hsl decreasing hue, hsl(50deg 50% 50%), hsl(330deg 50% 50%)) }",
+      &canonicalize("hsl(10deg 50% 50%)"),
+    );
+    minify_test(
+      ".foo { color: color-mix(in hsl decreasing hue, hsl(330deg 50% 50%), hsl(50deg 50% 50%)) }",
+      &canonicalize("hsl(190deg 50% 50%)"),
+    );
+    minify_test(
+      ".foo { color: color-mix(in hsl decreasing hue, hsl(20deg 50% 50%), hsl(320deg 50% 50%)) }",
+      &canonicalize("hsl(350deg 50% 50%)"),
+    );
+    // minify_test(".foo { color: color-mix(in hsl decreasing hue, hsl(320deg 50% 50%), hsl(20deg 50% 50%)) }", &canonicalize("hsl(170deg 50% 50%)"));
+
+    minify_test(
+      ".foo { color: color-mix(in hsl specified hue, hsl(40deg 50% 50%), hsl(60deg 50% 50%)) }",
+      &canonicalize("hsl(50deg 50% 50%)"),
+    );
+    minify_test(
+      ".foo { color: color-mix(in hsl specified hue, hsl(60deg 50% 50%), hsl(40deg 50% 50%)) }",
+      &canonicalize("hsl(50deg 50% 50%)"),
+    );
+    minify_test(
+      ".foo { color: color-mix(in hsl specified hue, hsl(50deg 50% 50%), hsl(330deg 50% 50%)) }",
+      &canonicalize("hsl(190deg 50% 50%)"),
+    );
+    minify_test(
+      ".foo { color: color-mix(in hsl specified hue, hsl(330deg 50% 50%), hsl(50deg 50% 50%)) }",
+      &canonicalize("hsl(190deg 50% 50%)"),
+    );
+    // minify_test(".foo { color: color-mix(in hsl specified hue, hsl(20deg 50% 50%), hsl(320deg 50% 50%)) }", &canonicalize("hsl(170deg 50% 50%)"));
+    // minify_test(".foo { color: color-mix(in hsl specified hue, hsl(320deg 50% 50%), hsl(20deg 50% 50%)) }", &canonicalize("hsl(170deg 50% 50%)"));
+
+    minify_test(
+      ".foo { color: color-mix(in hsl, hsl(none none none), hsl(none none none)) }",
+      &canonicalize("hsl(none none none)"),
+    );
+    minify_test(
+      ".foo { color: color-mix(in hsl, hsl(none none none), hsl(30deg 40% 80%)) }",
+      &canonicalize("hsl(30deg 40% 80%)"),
+    );
+    minify_test(
+      ".foo { color: color-mix(in hsl, hsl(120deg 20% 40%), hsl(none none none)) }",
+      &canonicalize("hsl(120deg 20% 40%)"),
+    );
+    minify_test(
+      ".foo { color: color-mix(in hsl, hsl(120deg 20% none), hsl(30deg 40% 60%)) }",
+      &canonicalize("hsl(75deg 30% 60%)"),
+    );
+    minify_test(
+      ".foo { color: color-mix(in hsl, hsl(120deg 20% 40%), hsl(30deg 20% none)) }",
+      &canonicalize("hsl(75deg 20% 40%)"),
+    );
+    minify_test(
+      ".foo { color: color-mix(in hsl, hsl(none 20% 40%), hsl(30deg none 80%)) }",
+      &canonicalize("hsl(30deg 20% 60%)"),
+    );
+
+    minify_test(
+      ".foo { color: color-mix(in hsl, color(display-p3 0 1 0) 100%, rgb(0, 0, 0) 0%) }",
+      &canonicalize("rgb(0, 249, 66)"),
+    ); // Naive clip based mapping would give rgb(0, 255, 0).
+    minify_test(
+      ".foo { color: color-mix(in hsl, lab(100% 104.3 -50.9) 100%, rgb(0, 0, 0) 0%) }",
+      &canonicalize("rgb(255, 255, 255)"),
+    ); // Naive clip based mapping would give rgb(255, 150, 255).
+    minify_test(
+      ".foo { color: color-mix(in hsl, lab(0% 104.3 -50.9) 100%, rgb(0, 0, 0) 0%) }",
+      &canonicalize("rgb(42, 0, 34)"),
+    ); // Naive clip based mapping would give rgb(90, 0, 76). NOTE: 0% lightness in Lab/LCH does not automatically correspond with sRGB black,
+    minify_test(
+      ".foo { color: color-mix(in hsl, lch(100% 116 334) 100%, rgb(0, 0, 0) 0%) }",
+      &canonicalize("rgb(255, 255, 255)"),
+    ); // Naive clip based mapping would give rgb(255, 150, 255).
+    minify_test(
+      ".foo { color: color-mix(in hsl, lch(0% 116 334) 100%, rgb(0, 0, 0) 0%) }",
+      &canonicalize("rgb(42, 0, 34)"),
+    ); // Naive clip based mapping would give rgb(90, 0, 76). NOTE: 0% lightness in Lab/LCH does not automatically correspond with sRGB black,
+    minify_test(
+      ".foo { color: color-mix(in hsl, oklab(100% 0.365 -0.16) 100%, rgb(0, 0, 0) 0%) }",
+      &canonicalize("rgb(255, 255, 255)"),
+    ); // Naive clip based mapping would give rgb(255, 92, 255).
+    minify_test(
+      ".foo { color: color-mix(in hsl, oklab(0% 0.365 -0.16) 100%, rgb(0, 0, 0) 0%) }",
+      &canonicalize("rgb(0, 0, 0)"),
+    ); // Naive clip based mapping would give rgb(19, 0, 24).
+    minify_test(
+      ".foo { color: color-mix(in hsl, oklch(100% 0.399 336.3) 100%, rgb(0, 0, 0) 0%) }",
+      &canonicalize("rgb(255, 255, 255)"),
+    ); // Naive clip based mapping would give rgb(255, 91, 255).
+    minify_test(
+      ".foo { color: color-mix(in hsl, oklch(0% 0.399 336.3) 100%, rgb(0, 0, 0) 0%) }",
+      &canonicalize("rgb(0, 0, 0)"),
+    ); // Naive clip based mapping would give rgb(20, 0, 24).
+
+    minify_test(
+      ".foo { color: color-mix(in hwb, hwb(120deg 10% 20%), hwb(30deg 30% 40%)) }",
+      &canonicalize("rgb(147, 179, 52)"),
+    );
+    minify_test(
+      ".foo { color: color-mix(in hwb, hwb(120deg 10% 20%) 25%, hwb(30deg 30% 40%)) }",
+      &canonicalize("rgb(166, 153, 64)"),
+    );
+    minify_test(
+      ".foo { color: color-mix(in hwb, 25% hwb(120deg 10% 20%), hwb(30deg 30% 40%)) }",
+      &canonicalize("rgb(166, 153, 64)"),
+    );
+    minify_test(
+      ".foo { color: color-mix(in hwb, hwb(120deg 10% 20%), 25% hwb(30deg 30% 40%)) }",
+      &canonicalize("rgb(96, 191, 39)"),
+    );
+    minify_test(
+      ".foo { color: color-mix(in hwb, hwb(120deg 10% 20%), hwb(30deg 30% 40%) 25%) }",
+      &canonicalize("rgb(96, 191, 39)"),
+    );
+    minify_test(
+      ".foo { color: color-mix(in hwb, hwb(120deg 10% 20%) 25%, hwb(30deg 30% 40%) 75%) }",
+      &canonicalize("rgb(166, 153, 64)"),
+    );
+    minify_test(
+      ".foo { color: color-mix(in hwb, hwb(120deg 10% 20%) 30%, hwb(30deg 30% 40%) 90%) }",
+      &canonicalize("rgb(166, 153, 64)"),
+    ); // Scale down > 100% sum.
+    minify_test(
+      ".foo { color: color-mix(in hwb, hwb(120deg 10% 20%) 12.5%, hwb(30deg 30% 40%) 37.5%) }",
+      &canonicalize("rgba(166, 153, 64, 0.5)"),
+    ); // Scale up < 100% sum, causes alpha multiplication.
+    minify_test(
+      ".foo { color: color-mix(in hwb, hwb(120deg 10% 20%) 0%, hwb(30deg 30% 40%)) }",
+      &canonicalize("rgb(153, 115, 77)"),
+    );
+
+    minify_test(
+      ".foo { color: color-mix(in hwb, hwb(120deg 10% 20% / .4), hwb(30deg 30% 40% / .8)) }",
+      &canonicalize("rgba(143, 170, 60, 0.6)"),
+    );
+    minify_test(
+      ".foo { color: color-mix(in hwb, hwb(120deg 10% 20% / .4) 25%, hwb(30deg 30% 40% / .8)) }",
+      &canonicalize("rgba(160, 149, 70, 0.7)"),
+    );
+    minify_test(
+      ".foo { color: color-mix(in hwb, 25% hwb(120deg 10% 20% / .4), hwb(30deg 30% 40% / .8)) }",
+      &canonicalize("rgba(160, 149, 70, 0.7)"),
+    );
+    minify_test(
+      ".foo { color: color-mix(in hwb, hwb(120deg 10% 20%), 25% hwb(30deg 30% 40% / .8)) }",
+      &canonicalize("rgba(95, 193, 37, 0.95)"),
+    );
+    minify_test(
+      ".foo { color: color-mix(in hwb, hwb(120deg 10% 20% / .4), hwb(30deg 30% 40% / .8) 25%) }",
+      &canonicalize("rgba(98, 184, 46, 0.5)"),
+    );
+    minify_test(
+      ".foo { color: color-mix(in hwb, hwb(120deg 10% 20% / .4) 25%, hwb(30deg 30% 40% / .8) 75%) }",
+      &canonicalize("rgba(160, 149, 70, 0.7)"),
+    );
+    minify_test(
+      ".foo { color: color-mix(in hwb, hwb(120deg 10% 20% / .4) 30%, hwb(30deg 30% 40% / .8) 90%) }",
+      &canonicalize("rgba(160, 149, 70, 0.7)"),
+    ); // Scale down > 100% sum.
+    minify_test(
+      ".foo { color: color-mix(in hwb, hwb(120deg 10% 20% / .4) 12.5%, hwb(30deg 30% 40% / .8) 37.5%) }",
+      &canonicalize("rgba(160, 149, 70, 0.35)"),
+    ); // Scale up < 100% sum, causes alpha multiplication.
+    minify_test(
+      ".foo { color: color-mix(in hwb, hwb(120deg 10% 20% / .4) 0%, hwb(30deg 30% 40% / .8)) }",
+      &canonicalize("rgba(153, 115, 77, 0.8)"),
+    );
+
+    //  minify_test(".foo { color: color-mix(in hwb, hwb(40deg 30% 40%), hwb(60deg 30% 40%)) }", &canonicalize("hwb(50deg 30% 40%)"));
+    //  minify_test(".foo { color: color-mix(in hwb, hwb(60deg 30% 40%), hwb(40deg 30% 40%)) }", &canonicalize("hwb(50deg 30% 40%)"));
+    minify_test(
+      ".foo { color: color-mix(in hwb, hwb(50deg 30% 40%), hwb(330deg 30% 40%)) }",
+      &canonicalize("hwb(10deg 30% 40%)"),
+    );
+    minify_test(
+      ".foo { color: color-mix(in hwb, hwb(330deg 30% 40%), hwb(50deg 30% 40%)) }",
+      &canonicalize("hwb(10deg 30% 40%)"),
+    );
+    minify_test(
+      ".foo { color: color-mix(in hwb, hwb(20deg 30% 40%), hwb(320deg 30% 40%)) }",
+      &canonicalize("hwb(350deg 30% 40%)"),
+    );
+    minify_test(
+      ".foo { color: color-mix(in hwb, hwb(320deg 30% 40%), hwb(20deg 30% 40%)) }",
+      &canonicalize("hwb(350deg 30% 40%)"),
+    );
+
+    //  minify_test(".foo { color: color-mix(in hwb shorter hue, hwb(40deg 30% 40%), hwb(60deg 30% 40%)) }", &canonicalize("hwb(50deg 30% 40%)"));
+    //  minify_test(".foo { color: color-mix(in hwb shorter hue, hwb(60deg 30% 40%), hwb(40deg 30% 40%)) }", &canonicalize("hwb(50deg 30% 40%)"));
+    minify_test(
+      ".foo { color: color-mix(in hwb shorter hue, hwb(50deg 30% 40%), hwb(330deg 30% 40%)) }",
+      &canonicalize("hwb(10deg 30% 40%)"),
+    );
+    minify_test(
+      ".foo { color: color-mix(in hwb shorter hue, hwb(330deg 30% 40%), hwb(50deg 30% 40%)) }",
+      &canonicalize("hwb(10deg 30% 40%)"),
+    );
+    minify_test(
+      ".foo { color: color-mix(in hwb shorter hue, hwb(20deg 30% 40%), hwb(320deg 30% 40%)) }",
+      &canonicalize("hwb(350deg 30% 40%)"),
+    );
+    minify_test(
+      ".foo { color: color-mix(in hwb shorter hue, hwb(320deg 30% 40%), hwb(20deg 30% 40%)) }",
+      &canonicalize("hwb(350deg 30% 40%)"),
+    );
+
+    minify_test(
+      ".foo { color: color-mix(in hwb longer hue, hwb(40deg 30% 40%), hwb(60deg 30% 40%)) }",
+      &canonicalize("hwb(230deg 30% 40%)"),
+    );
+    minify_test(
+      ".foo { color: color-mix(in hwb longer hue, hwb(60deg 30% 40%), hwb(40deg 30% 40%)) }",
+      &canonicalize("hwb(230deg 30% 40%)"),
+    );
+    //  minify_test(".foo { color: color-mix(in hwb longer hue, hwb(50deg 30% 40%), hwb(330deg 30% 40%)) }", &canonicalize("hwb(190deg 30% 40%)"));
+    //  minify_test(".foo { color: color-mix(in hwb longer hue, hwb(330deg 30% 40%), hwb(50deg 30% 40%)) }", &canonicalize("hwb(190deg 30% 40%)"));
+    //  minify_test(".foo { color: color-mix(in hwb longer hue, hwb(20deg 30% 40%), hwb(320deg 30% 40%)) }", &canonicalize("hwb(170deg 30% 40%)"));
+    //  minify_test(".foo { color: color-mix(in hwb longer hue, hwb(320deg 30% 40%), hwb(20deg 30% 40%)) }", &canonicalize("hwb(170deg 30% 40%)"));
+
+    // minify_test(".foo { color: color-mix(in hwb increasing hue, hwb(40deg 30% 40%), hwb(60deg 30% 40%)) }", &canonicalize("hwb(50deg 30% 40%)"));
+    minify_test(
+      ".foo { color: color-mix(in hwb increasing hue, hwb(60deg 30% 40%), hwb(40deg 30% 40%)) }",
+      &canonicalize("hwb(230deg 30% 40%)"),
+    );
+    // minify_test(".foo { color: color-mix(in hwb increasing hue, hwb(50deg 30% 40%), hwb(330deg 30% 40%)) }", &canonicalize("hwb(190deg 30% 40%)"));
+    minify_test(
+      ".foo { color: color-mix(in hwb increasing hue, hwb(330deg 30% 40%), hwb(50deg 30% 40%)) }",
+      &canonicalize("hwb(10deg 30% 40%)"),
+    );
+    // minify_test(".foo { color: color-mix(in hwb increasing hue, hwb(20deg 30% 40%), hwb(320deg 30% 40%)) }", &canonicalize("hwb(170deg 30% 40%)"));
+    minify_test(
+      ".foo { color: color-mix(in hwb increasing hue, hwb(320deg 30% 40%), hwb(20deg 30% 40%)) }",
+      &canonicalize("hwb(350deg 30% 40%)"),
+    );
+
+    minify_test(
+      ".foo { color: color-mix(in hwb decreasing hue, hwb(40deg 30% 40%), hwb(60deg 30% 40%)) }",
+      &canonicalize("hwb(230deg 30% 40%)"),
+    );
+    // minify_test(".foo { color: color-mix(in hwb decreasing hue, hwb(60deg 30% 40%), hwb(40deg 30% 40%)) }", &canonicalize("hwb(50deg 30% 40%)"));
+    minify_test(
+      ".foo { color: color-mix(in hwb decreasing hue, hwb(50deg 30% 40%), hwb(330deg 30% 40%)) }",
+      &canonicalize("hwb(10deg 30% 40%)"),
+    );
+    // minify_test(".foo { color: color-mix(in hwb decreasing hue, hwb(330deg 30% 40%), hwb(50deg 30% 40%)) }", &canonicalize("hwb(190deg 30% 40%)"));
+    minify_test(
+      ".foo { color: color-mix(in hwb decreasing hue, hwb(20deg 30% 40%), hwb(320deg 30% 40%)) }",
+      &canonicalize("hwb(350deg 30% 40%)"),
+    );
+    // minify_test(".foo { color: color-mix(in hwb decreasing hue, hwb(320deg 30% 40%), hwb(20deg 30% 40%)) }", &canonicalize("hwb(170deg 30% 40%)"));
+
+    // minify_test(".foo { color: color-mix(in hwb specified hue, hwb(40deg 30% 40%), hwb(60deg 30% 40%)) }", &canonicalize("hwb(50deg 30% 40%)"));
+    // minify_test(".foo { color: color-mix(in hwb specified hue, hwb(60deg 30% 40%), hwb(40deg 30% 40%)) }", &canonicalize("hwb(50deg 30% 40%)"));
+    // minify_test(".foo { color: color-mix(in hwb specified hue, hwb(50deg 30% 40%), hwb(330deg 30% 40%)) }", &canonicalize("hwb(190deg 30% 40%)"));
+    // minify_test(".foo { color: color-mix(in hwb specified hue, hwb(330deg 30% 40%), hwb(50deg 30% 40%)) }", &canonicalize("hwb(190deg 30% 40%)"));
+    // minify_test(".foo { color: color-mix(in hwb specified hue, hwb(20deg 30% 40%), hwb(320deg 30% 40%)) }", &canonicalize("hwb(170deg 30% 40%)"));
+    // minify_test(".foo { color: color-mix(in hwb specified hue, hwb(320deg 30% 40%), hwb(20deg 30% 40%)) }", &canonicalize("hwb(170deg 30% 40%)"));
+
+    minify_test(
+      ".foo { color: color-mix(in hwb, color(display-p3 0 1 0) 100%, rgb(0, 0, 0) 0%) }",
+      &canonicalize("rgb(0, 249, 66)"),
+    ); // Naive clip based mapping would give rgb(0, 255, 0).
+    minify_test(
+      ".foo { color: color-mix(in hwb, lab(100% 104.3 -50.9) 100%, rgb(0, 0, 0) 0%) }",
+      &canonicalize("rgb(255, 255, 255)"),
+    ); // Naive clip based mapping would give rgb(255, 150, 255).
+    minify_test(
+      ".foo { color: color-mix(in hwb, lab(0% 104.3 -50.9) 100%, rgb(0, 0, 0) 0%) }",
+      &canonicalize("rgb(42, 0, 34)"),
+    ); // Naive clip based mapping would give rgb(90, 0, 76). NOTE: 0% lightness in Lab/LCH does not automatically correspond with sRGB black,
+    minify_test(
+      ".foo { color: color-mix(in hwb, lch(100% 116 334) 100%, rgb(0, 0, 0) 0%) }",
+      &canonicalize("rgb(255, 255, 255)"),
+    ); // Naive clip based mapping would give rgb(255, 150, 255).
+    minify_test(
+      ".foo { color: color-mix(in hwb, lch(0% 116 334) 100%, rgb(0, 0, 0) 0%) }",
+      &canonicalize("rgb(42, 0, 34)"),
+    ); // Naive clip based mapping would give rgb(90, 0, 76). NOTE: 0% lightness in Lab/LCH does not automatically correspond with sRGB black,
+    minify_test(
+      ".foo { color: color-mix(in hwb, oklab(100% 0.365 -0.16) 100%, rgb(0, 0, 0) 0%) }",
+      &canonicalize("rgb(255, 255, 255)"),
+    ); // Naive clip based mapping would give rgb(255, 92, 255).
+    minify_test(
+      ".foo { color: color-mix(in hwb, oklab(0% 0.365 -0.16) 100%, rgb(0, 0, 0) 0%) }",
+      &canonicalize("rgb(0, 0, 0)"),
+    ); // Naive clip based mapping would give rgb(19, 0, 24).
+    minify_test(
+      ".foo { color: color-mix(in hwb, oklch(100% 0.399 336.3) 100%, rgb(0, 0, 0) 0%) }",
+      &canonicalize("rgb(255, 255, 255)"),
+    ); // Naive clip based mapping would give rgb(255, 91, 255).
+    minify_test(
+      ".foo { color: color-mix(in hwb, oklch(0% 0.399 336.3) 100%, rgb(0, 0, 0) 0%) }",
+      &canonicalize("rgb(0, 0, 0)"),
+    ); // Naive clip based mapping would give rgb(20, 0, 24).
+
+    for color_space in &["lch", "oklch"] {
+      // regex for converting web platform tests:
+      // test_computed_value\(.*?, `color-mix\(in \$\{colorSpace\}(.*?), (.*?)\$\{colorSpace\}(.*?) \$\{colorSpace\}(.*?)`, `\$\{colorSpace\}(.*?)`\);
+      // minify_test(&format!(".foo {{ color: color-mix(in {0}$1, $2{0}$3 {0}$4 }}", color_space), &format!(".foo{{color:{}$5}}", color_space));
+
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, {0}(10% 20 30deg), {0}(50% 60 70deg)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(30% 40 50)}}", color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, {0}(10% 20 30deg) 25%, {0}(50% 60 70deg)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(40% 50 60)}}", color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, 25% {0}(10% 20 30deg), {0}(50% 60 70deg)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(40% 50 60)}}", color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, {0}(10% 20 30deg), 25% {0}(50% 60 70deg)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(20% 30 40)}}", color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, {0}(10% 20 30deg), {0}(50% 60 70deg) 25%) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(20% 30 40)}}", color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, {0}(10% 20 30deg) 25%, {0}(50% 60 70deg) 75%) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(40% 50 60)}}", color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, {0}(10% 20 30deg) 30%, {0}(50% 60 70deg) 90%) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(40% 50 60)}}", color_space),
+      ); // Scale down > 100% sum.
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, {0}(10% 20 30deg) 12.5%, {0}(50% 60 70deg) 37.5%) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(40% 50 60/.5)}}", color_space),
+      ); // Scale up < 100% sum, causes alpha multiplication.
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, {0}(10% 20 30deg) 0%, {0}(50% 60 70deg)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(50% 60 70)}}", color_space),
+      );
+
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, {0}(10% 20 30deg / .4), {0}(50% 60 70deg / .8)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(36.6667% 46.6667 50/.6)}}", color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, {0}(10% 20 30deg / .4) 25%, {0}(50% 60 70deg / .8)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(44.2857% 54.2857 60/.7)}}", color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, 25% {0}(10% 20 30deg / .4), {0}(50% 60 70deg / .8)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(44.2857% 54.2857 60/.7)}}", color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, {0}(10% 20 30deg / .4), 25% {0}(50% 60 70deg / .8)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(26% 36 40/.5)}}", color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, {0}(10% 20 30deg / .4), {0}(50% 60 70deg / .8) 25%) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(26% 36 40/.5)}}", color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, {0}(10% 20 30deg / .4) 25%, {0}(50% 60 70deg / .8) 75%) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(44.2857% 54.2857 60/.7)}}", color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, {0}(10% 20 30deg / .4) 30%, {0}(50% 60 70deg / .8) 90%) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(44.2857% 54.2857 60/.7)}}", color_space),
+      ); // Scale down > 100% sum.
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, {0}(10% 20 30deg / .4) 12.5%, {0}(50% 60 70deg / .8) 37.5%) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(44.2857% 54.2857 60/.35)}}", color_space),
+      ); // Scale up < 100% sum, causes alpha multiplication.
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, {0}(10% 20 30deg / .4) 0%, {0}(50% 60 70deg / .8)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(50% 60 70/.8)}}", color_space),
+      );
+
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, {0}(100% 0 40deg), {0}(100% 0 60deg)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(100% 0 50)}}", color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, {0}(100% 0 60deg), {0}(100% 0 40deg)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(100% 0 50)}}", color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, {0}(100% 0 50deg), {0}(100% 0 330deg)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(100% 0 10)}}", color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, {0}(100% 0 330deg), {0}(100% 0 50deg)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(100% 0 10)}}", color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, {0}(100% 0 20deg), {0}(100% 0 320deg)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(100% 0 350)}}", color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, {0}(100% 0 320deg), {0}(100% 0 20deg)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(100% 0 350)}}", color_space),
+      );
+
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0} shorter hue, {0}(100% 0 40deg), {0}(100% 0 60deg)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(100% 0 50)}}", color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0} shorter hue, {0}(100% 0 60deg), {0}(100% 0 40deg)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(100% 0 50)}}", color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0} shorter hue, {0}(100% 0 50deg), {0}(100% 0 330deg)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(100% 0 10)}}", color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0} shorter hue, {0}(100% 0 330deg), {0}(100% 0 50deg)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(100% 0 10)}}", color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0} shorter hue, {0}(100% 0 20deg), {0}(100% 0 320deg)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(100% 0 350)}}", color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0} shorter hue, {0}(100% 0 320deg), {0}(100% 0 20deg)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(100% 0 350)}}", color_space),
+      );
+
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0} longer hue, {0}(100% 0 40deg), {0}(100% 0 60deg)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(100% 0 230)}}", color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0} longer hue, {0}(100% 0 60deg), {0}(100% 0 40deg)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(100% 0 230)}}", color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0} longer hue, {0}(100% 0 50deg), {0}(100% 0 330deg)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(100% 0 190)}}", color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0} longer hue, {0}(100% 0 330deg), {0}(100% 0 50deg)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(100% 0 190)}}", color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0} longer hue, {0}(100% 0 20deg), {0}(100% 0 320deg)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(100% 0 170)}}", color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0} longer hue, {0}(100% 0 320deg), {0}(100% 0 20deg)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(100% 0 170)}}", color_space),
+      );
+
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0} increasing hue, {0}(100% 0 40deg), {0}(100% 0 60deg)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(100% 0 50)}}", color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0} increasing hue, {0}(100% 0 60deg), {0}(100% 0 40deg)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(100% 0 230)}}", color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0} increasing hue, {0}(100% 0 50deg), {0}(100% 0 330deg)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(100% 0 190)}}", color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0} increasing hue, {0}(100% 0 330deg), {0}(100% 0 50deg)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(100% 0 10)}}", color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0} increasing hue, {0}(100% 0 20deg), {0}(100% 0 320deg)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(100% 0 170)}}", color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0} increasing hue, {0}(100% 0 320deg), {0}(100% 0 20deg)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(100% 0 350)}}", color_space),
+      );
+
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0} decreasing hue, {0}(100% 0 40deg), {0}(100% 0 60deg)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(100% 0 230)}}", color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0} decreasing hue, {0}(100% 0 60deg), {0}(100% 0 40deg)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(100% 0 50)}}", color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0} decreasing hue, {0}(100% 0 50deg), {0}(100% 0 330deg)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(100% 0 10)}}", color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0} decreasing hue, {0}(100% 0 330deg), {0}(100% 0 50deg)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(100% 0 190)}}", color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0} decreasing hue, {0}(100% 0 20deg), {0}(100% 0 320deg)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(100% 0 350)}}", color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0} decreasing hue, {0}(100% 0 320deg), {0}(100% 0 20deg)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(100% 0 170)}}", color_space),
+      );
+
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0} specified hue, {0}(100% 0 40deg), {0}(100% 0 60deg)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(100% 0 50)}}", color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0} specified hue, {0}(100% 0 60deg), {0}(100% 0 40deg)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(100% 0 50)}}", color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0} specified hue, {0}(100% 0 50deg), {0}(100% 0 330deg)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(100% 0 190)}}", color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0} specified hue, {0}(100% 0 330deg), {0}(100% 0 50deg)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(100% 0 190)}}", color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0} specified hue, {0}(100% 0 20deg), {0}(100% 0 320deg)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(100% 0 170)}}", color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0} specified hue, {0}(100% 0 320deg), {0}(100% 0 20deg)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(100% 0 170)}}", color_space),
+      );
+
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, {0}(none none none), {0}(none none none)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(none none none)}}", color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, {0}(none none none), {0}(50% 60 70deg)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(50% 60 70)}}", color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, {0}(10% 20 30deg), {0}(none none none)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(10% 20 30)}}", color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, {0}(10% 20 none), {0}(50% 60 70deg)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(30% 40 70)}}", color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, {0}(10% 20 30deg), {0}(50% 60 none)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(30% 40 30)}}", color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, {0}(none 20 30deg), {0}(50% none 70deg)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(50% 20 50)}}", color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, {0}(10% 20 30deg / none), {0}(50% 60 70deg)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(30% 40 50)}}", color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, {0}(10% 20 30deg / none), {0}(50% 60 70deg / 0.5)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(30% 40 50/.5)}}", color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, {0}(10% 20 30deg / none), {0}(50% 60 70deg / none)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(30% 40 50/none)}}", color_space),
+      );
+    }
+
+    for color_space in ["lab", "oklab"] {
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, {0}(10% 20 30), {0}(50% 60 70)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(30% 40 50)}}", color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, {0}(10% 20 30) 25%, {0}(50% 60 70)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(40% 50 60)}}", color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, 25% {0}(10% 20 30), {0}(50% 60 70)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(40% 50 60)}}", color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, {0}(10% 20 30), 25% {0}(50% 60 70)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(20% 30 40)}}", color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, {0}(10% 20 30), {0}(50% 60 70) 25%) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(20% 30 40)}}", color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, {0}(10% 20 30) 25%, {0}(50% 60 70) 75%) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(40% 50 60)}}", color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, {0}(10% 20 30) 30%, {0}(50% 60 70) 90%) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(40% 50 60)}}", color_space),
+      ); // Scale down > 100% sum.
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, {0}(10% 20 30) 12.5%, {0}(50% 60 70) 37.5%) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(40% 50 60/.5)}}", color_space),
+      ); // Scale up < 100% sum, causes alpha multiplication.
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, {0}(10% 20 30) 0%, {0}(50% 60 70)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(50% 60 70)}}", color_space),
+      );
+
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, {0}(10% 20 30 / .4), {0}(50% 60 70 / .8)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(36.6667% 46.6667 56.6667/.6)}}", color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, {0}(10% 20 30 / .4) 25%, {0}(50% 60 70 / .8)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(44.2857% 54.2857 64.2857/.7)}}", color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, 25% {0}(10% 20 30 / .4), {0}(50% 60 70 / .8)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(44.2857% 54.2857 64.2857/.7)}}", color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, {0}(10% 20 30 / .4), 25% {0}(50% 60 70 / .8)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(26% 36 46/.5)}}", color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, {0}(10% 20 30 / .4), {0}(50% 60 70 / .8) 25%) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(26% 36 46/.5)}}", color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, {0}(10% 20 30 / .4) 25%, {0}(50% 60 70 / .8) 75%) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(44.2857% 54.2857 64.2857/.7)}}", color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, {0}(10% 20 30 / .4) 30%, {0}(50% 60 70 / .8) 90%) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(44.2857% 54.2857 64.2857/.7)}}", color_space),
+      ); // Scale down > 100% sum.
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, {0}(10% 20 30 / .4) 12.5%, {0}(50% 60 70 / .8) 37.5%) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(44.2857% 54.2857 64.2857/.35)}}", color_space),
+      ); // Scale up < 100% sum, causes alpha multiplication.
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, {0}(10% 20 30 / .4) 0%, {0}(50% 60 70 / .8)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(50% 60 70/.8)}}", color_space),
+      );
+
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, {0}(none none none), {0}(none none none)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(none none none)}}", color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, {0}(none none none), {0}(50% 60 70)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(50% 60 70)}}", color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, {0}(10% 20 30), {0}(none none none)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(10% 20 30)}}", color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, {0}(10% 20 none), {0}(50% 60 70)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(30% 40 70)}}", color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, {0}(10% 20 30), {0}(50% 60 none)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(30% 40 30)}}", color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, {0}(none 20 30), {0}(50% none 70)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(50% 20 50)}}", color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, {0}(10% 20 30 / none), {0}(50% 60 70)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(30% 40 50)}}", color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, {0}(10% 20 30 / none), {0}(50% 60 70 / 0.5)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(30% 40 50/.5)}}", color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, {0}(10% 20 30 / none), {0}(50% 60 70 / none)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:{}(30% 40 50/none)}}", color_space),
+      );
+    }
+
+    for color_space in [/*"srgb", */ "srgb-linear", "xyz", "xyz-d50", "xyz-d65"] {
+      // regex for converting web platform tests:
+      // test_computed_value\(.*?, `color-mix\(in \$\{colorSpace\}(.*?), (.*?)color\(\$\{colorSpace\}(.*?) color\(\$\{colorSpace\}(.*?)`, `color\(\$\{resultColorSpace\}(.*?)`\);
+      // minify_test(&format!(".foo {{ color: color-mix(in {0}$1, $2color({0}$3 color({0}$4 }}", color_space), &format!(".foo{{color:color({}$5}}", result_color_space));
+
+      let result_color_space = if color_space == "xyz-d65" { "xyz" } else { color_space };
+
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, color({0} .1 .2 .3), color({0} .5 .6 .7)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:color({} .3 .4 .5)}}", result_color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, color({0} .1 .2 .3) 25%, color({0} .5 .6 .7)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:color({} .4 .5 .6)}}", result_color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, 25% color({0} .1 .2 .3), color({0} .5 .6 .7)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:color({} .4 .5 .6)}}", result_color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, color({0} .1 .2 .3), color({0} .5 .6 .7) 25%) }}",
+          color_space
+        ),
+        &format!(".foo{{color:color({} .2 .3 .4)}}", result_color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, color({0} .1 .2 .3), 25% color({0} .5 .6 .7)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:color({} .2 .3 .4)}}", result_color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, color({0} .1 .2 .3) 25%, color({0} .5 .6 .7) 75%) }}",
+          color_space
+        ),
+        &format!(".foo{{color:color({} .4 .5 .6)}}", result_color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, color({0} .1 .2 .3) 30%, color({0} .5 .6 .7) 90%) }}",
+          color_space
+        ),
+        &format!(".foo{{color:color({} .4 .5 .6)}}", result_color_space),
+      ); // Scale down > 100% sum.
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, color({0} .1 .2 .3) 12.5%, color({0} .5 .6 .7) 37.5%) }}",
+          color_space
+        ),
+        &format!(".foo{{color:color({} .4 .5 .6/.5)}}", result_color_space),
+      ); // Scale up < 100% sum, causes alpha multiplication.
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, color({0} .1 .2 .3) 0%, color({0} .5 .6 .7)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:color({} .5 .6 .7)}}", result_color_space),
+      );
+
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, color({0} .1 .2 .3 / .5), color({0} .5 .6 .7 / .8)) }}",
+          color_space
+        ),
+        &format!(
+          ".foo{{color:color({} .346154 .446154 .546154/.65)}}",
+          result_color_space
+        ),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, color({0} .1 .2 .3 / .4) 25%, color({0} .5 .6 .7 / .8)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:color({} .442857 .542857 .642857/.7)}}", result_color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, 25% color({0} .1 .2 .3 / .4), color({0} .5 .6 .7 / .8)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:color({} .442857 .542857 .642857/.7)}}", result_color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, color({0} .1 .2 .3 / .4), color({0} .5 .6 .7 / .8) 25%) }}",
+          color_space
+        ),
+        &format!(".foo{{color:color({} .26 .36 .46/.5)}}", result_color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, color({0} .1 .2 .3 / .4), 25% color({0} .5 .6 .7 / .8)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:color({} .26 .36 .46/.5)}}", result_color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, color({0} .1 .2 .3 / .4) 25%, color({0} .5 .6 .7 / .8) 75%) }}",
+          color_space
+        ),
+        &format!(".foo{{color:color({} .442857 .542857 .642857/.7)}}", result_color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, color({0} .1 .2 .3 / .4) 30%, color({0} .5 .6 .7 / .8) 90%) }}",
+          color_space
+        ),
+        &format!(".foo{{color:color({} .442857 .542857 .642857/.7)}}", result_color_space),
+      ); // Scale down > 100% sum.
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, color({0} .1 .2 .3 / .4) 12.5%, color({0} .5 .6 .7 / .8) 37.5%) }}",
+          color_space
+        ),
+        &format!(
+          ".foo{{color:color({} .442857 .542857 .642857/.35)}}",
+          result_color_space
+        ),
+      ); // Scale up < 100% sum, causes alpha multiplication.
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, color({0} .1 .2 .3 / .4) 0%, color({0} .5 .6 .7 / .8)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:color({} .5 .6 .7/.8)}}", result_color_space),
+      );
+
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, color({0} 2 3 4 / 5), color({0} 4 6 8 / 10)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:color({} 3 4.5 6)}}", result_color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, color({0} -2 -3 -4), color({0} -4 -6 -8)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:color({} -3 -4.5 -6)}}", result_color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, color({0} -2 -3 -4 / -5), color({0} -4 -6 -8 / -10)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:color({} 0 0 0/0)}}", result_color_space),
+      );
+
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, color({0} none none none), color({0} none none none)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:color({} none none none)}}", result_color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, color({0} none none none), color({0} .5 .6 .7)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:color({} .5 .6 .7)}}", result_color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, color({0} .1 .2 .3), color({0} none none none)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:color({} .1 .2 .3)}}", result_color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, color({0} .1 .2 none), color({0} .5 .6 .7)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:color({} .3 .4 .7)}}", result_color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, color({0} .1 .2 .3), color({0} .5 .6 none)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:color({} .3 .4 .3)}}", result_color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, color({0} none .2 .3), color({0} .5 none .7)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:color({} .5 .2 .5)}}", result_color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, color({0} .1 .2 .3 / none), color({0} .5 .6 .7)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:color({} .3 .4 .5)}}", result_color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, color({0} .1 .2 .3 / none), color({0} .5 .6 .7 / 0.5)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:color({} .3 .4 .5/.5)}}", result_color_space),
+      );
+      minify_test(
+        &format!(
+          ".foo {{ color: color-mix(in {0}, color({0} .1 .2 .3 / none), color({0} .5 .6 .7 / none)) }}",
+          color_space
+        ),
+        &format!(".foo{{color:color({} .3 .4 .5/none)}}", result_color_space),
+      );
+    }
+  }
+
+  #[test]
+  fn test_grid() {
+    minify_test(
+      ".foo { grid-template-columns: [first nav-start]  150px [main-start] 1fr [last]; }",
+      ".foo{grid-template-columns:[first nav-start]150px[main-start]1fr[last]}",
+    );
+    minify_test(
+      ".foo { grid-template-columns: 150px 1fr; }",
+      ".foo{grid-template-columns:150px 1fr}",
+    );
+    minify_test(
+      ".foo { grid-template-columns: repeat(4, 1fr); }",
+      ".foo{grid-template-columns:repeat(4,1fr)}",
+    );
+    minify_test(
+      ".foo { grid-template-columns: repeat(2, [e] 40px); }",
+      ".foo{grid-template-columns:repeat(2,[e]40px)}",
+    );
+    minify_test(
+      ".foo { grid-template-columns: repeat(4, [col-start] 250px [col-end]); }",
+      ".foo{grid-template-columns:repeat(4,[col-start]250px[col-end])}",
+    );
+    minify_test(
+      ".foo { grid-template-columns: repeat(4, [col-start] 60% [col-end]); }",
+      ".foo{grid-template-columns:repeat(4,[col-start]60%[col-end])}",
+    );
+    minify_test(
+      ".foo { grid-template-columns: repeat(4, [col-start] 1fr [col-end]); }",
+      ".foo{grid-template-columns:repeat(4,[col-start]1fr[col-end])}",
+    );
+    minify_test(
+      ".foo { grid-template-columns: repeat(4, [col-start] min-content [col-end]); }",
+      ".foo{grid-template-columns:repeat(4,[col-start]min-content[col-end])}",
+    );
+    minify_test(
+      ".foo { grid-template-columns: repeat(4, [col-start] max-content [col-end]); }",
+      ".foo{grid-template-columns:repeat(4,[col-start]max-content[col-end])}",
+    );
+    minify_test(
+      ".foo { grid-template-columns: repeat(4, [col-start] auto [col-end]); }",
+      ".foo{grid-template-columns:repeat(4,[col-start]auto[col-end])}",
+    );
+    minify_test(
+      ".foo { grid-template-columns: repeat(4, [col-start] minmax(100px, 1fr) [col-end]); }",
+      ".foo{grid-template-columns:repeat(4,[col-start]minmax(100px,1fr)[col-end])}",
+    );
+    minify_test(
+      ".foo { grid-template-columns: repeat(4, [col-start] fit-content(200px) [col-end]); }",
+      ".foo{grid-template-columns:repeat(4,[col-start]fit-content(200px)[col-end])}",
+    );
+    minify_test(
+      ".foo { grid-template-columns: repeat(4, 10px [col-start] 30% [col-middle] auto [col-end]); }",
+      ".foo{grid-template-columns:repeat(4,10px[col-start]30%[col-middle]auto[col-end])}",
+    );
+    minify_test(
+      ".foo { grid-template-columns: repeat(5, auto); }",
+      ".foo{grid-template-columns:repeat(5,auto)}",
+    );
+    minify_test(
+      ".foo { grid-template-columns: repeat(auto-fill, 250px); }",
+      ".foo{grid-template-columns:repeat(auto-fill,250px)}",
+    );
+    minify_test(
+      ".foo { grid-template-columns: repeat(auto-fit, 250px); }",
+      ".foo{grid-template-columns:repeat(auto-fit,250px)}",
+    );
+    minify_test(
+      ".foo { grid-template-columns: repeat(auto-fill, [col-start] 250px [col-end]); }",
+      ".foo{grid-template-columns:repeat(auto-fill,[col-start]250px[col-end])}",
+    );
+    minify_test(
+      ".foo { grid-template-columns: repeat(auto-fill, [col-start] minmax(100px, 1fr) [col-end]); }",
+      ".foo{grid-template-columns:repeat(auto-fill,[col-start]minmax(100px,1fr)[col-end])}",
+    );
+    minify_test(
+      ".foo { grid-template-columns: minmax(min-content, 1fr); }",
+      ".foo{grid-template-columns:minmax(min-content,1fr)}",
+    );
+    minify_test(
+      ".foo { grid-template-columns: 200px repeat(auto-fill, 100px) 300px; }",
+      ".foo{grid-template-columns:200px repeat(auto-fill,100px) 300px}",
+    );
+    minify_test(".foo { grid-template-columns: [linename1 linename2] 100px repeat(auto-fit, [linename1] 300px) [linename3]; }", ".foo{grid-template-columns:[linename1 linename2]100px repeat(auto-fit,[linename1]300px)[linename3]}");
+    minify_test(
+      ".foo { grid-template-rows: [linename1 linename2] 100px repeat(auto-fit, [linename1] 300px) [linename3]; }",
+      ".foo{grid-template-rows:[linename1 linename2]100px repeat(auto-fit,[linename1]300px)[linename3]}",
+    );
+
+    minify_test(".foo { grid-auto-rows: auto; }", ".foo{grid-auto-rows:auto}");
+    minify_test(".foo { grid-auto-rows: 1fr; }", ".foo{grid-auto-rows:1fr}");
+    minify_test(".foo { grid-auto-rows: 100px; }", ".foo{grid-auto-rows:100px}");
+    minify_test(
+      ".foo { grid-auto-rows: min-content; }",
+      ".foo{grid-auto-rows:min-content}",
+    );
+    minify_test(
+      ".foo { grid-auto-rows: max-content; }",
+      ".foo{grid-auto-rows:max-content}",
+    );
+    minify_test(
+      ".foo { grid-auto-rows: minmax(100px,auto); }",
+      ".foo{grid-auto-rows:minmax(100px,auto)}",
+    );
+    minify_test(
+      ".foo { grid-auto-rows: fit-content(20%); }",
+      ".foo{grid-auto-rows:fit-content(20%)}",
+    );
+    minify_test(
+      ".foo { grid-auto-rows: 100px minmax(100px, auto) 10% 0.5fr fit-content(400px); }",
+      ".foo{grid-auto-rows:100px minmax(100px,auto) 10% .5fr fit-content(400px)}",
+    );
+    minify_test(
+      ".foo { grid-auto-columns: 100px minmax(100px, auto) 10% 0.5fr fit-content(400px); }",
+      ".foo{grid-auto-columns:100px minmax(100px,auto) 10% .5fr fit-content(400px)}",
+    );
+
+    minify_test(
+      r#"
+      .foo {
+        grid-template-areas: "head head"
+                             "nav  main"
+                             "foot ....";
+      }
+    "#,
+      ".foo{grid-template-areas:\"head head\"\"nav main\"\"foot.\"}",
+    );
+    minify_test(
+      r#"
+      .foo {
+        grid-template-areas: "head head"
+                             "nav  main"
+                             ".... foot";
+      }
+    "#,
+      ".foo{grid-template-areas:\"head head\"\"nav main\"\".foot\"}",
+    );
+    minify_test(
+      r#"
+      .foo {
+        grid-template-areas: "head head"
+                             "nav  main"
+                             ".... ....";
+      }
+    "#,
+      ".foo{grid-template-areas:\"head head\"\"nav main\"\". .\"}",
+    );
+
+    test(
+      r#"
+      .foo {
+        grid-template-areas: "head head" "nav  main" "foot ....";
+      }
+    "#,
+      indoc! { r#"
+      .foo {
+        grid-template-areas: "head head"
+                             "nav main"
+                             "foot .";
+      }
+    "#},
+    );
+
+    minify_test(
+      r#"
+      .foo {
+        grid-template: [header-top] "a   a   a"     [header-bottom]
+                       [main-top] "b   b   b" 1fr [main-bottom];
+      }
+    "#,
+      ".foo{grid-template:[header-top]\"a a a\"[header-bottom main-top]\"b b b\"1fr[main-bottom]}",
+    );
+    minify_test(
+      r#"
+      .foo {
+        grid-template: "head head"
+                       "nav  main" 1fr
+                       "foot ....";
+      }
+    "#,
+      ".foo{grid-template:\"head head\"\"nav main\"1fr\"foot.\"}",
+    );
+    minify_test(
+      r#"
+      .foo {
+        grid-template: [header-top] "a   a   a"     [header-bottom]
+                         [main-top] "b   b   b" 1fr [main-bottom]
+                                  / auto 1fr auto;
+      }
+    "#,
+      ".foo{grid-template:[header-top]\"a a a\"[header-bottom main-top]\"b b b\"1fr[main-bottom]/auto 1fr auto}",
+    );
+
+    minify_test(
+      ".foo { grid-template: auto 1fr / auto 1fr auto; }",
+      ".foo{grid-template:auto 1fr/auto 1fr auto}",
+    );
+    minify_test(
+      ".foo { grid-template: [linename1 linename2] 100px repeat(auto-fit, [linename1] 300px) [linename3] / [linename1 linename2] 100px repeat(auto-fit, [linename1] 300px) [linename3]; }",
+      ".foo{grid-template:[linename1 linename2]100px repeat(auto-fit,[linename1]300px)[linename3]/[linename1 linename2]100px repeat(auto-fit,[linename1]300px)[linename3]}"
+    );
+
+    test(
+      ".foo{grid-template:[header-top]\"a a a\"[header-bottom main-top]\"b b b\"1fr[main-bottom]/auto 1fr auto}",
+      indoc! {r#"
+        .foo {
+          grid-template: [header-top] "a a a" [header-bottom]
+                         [main-top] "b b b" 1fr [main-bottom]
+                         / auto 1fr auto;
+        }
+      "#},
+    );
+    test(
+      ".foo{grid-template:[header-top]\"a a a\"[main-top]\"b b b\"1fr/auto 1fr auto}",
+      indoc! {r#"
+        .foo {
+          grid-template: [header-top] "a a a"
+                         [main-top] "b b b" 1fr
+                         / auto 1fr auto;
+        }
+      "#},
+    );
+
+    minify_test(".foo { grid-auto-flow: row }", ".foo{grid-auto-flow:row}");
+    minify_test(".foo { grid-auto-flow: column }", ".foo{grid-auto-flow:column}");
+    minify_test(".foo { grid-auto-flow: row dense }", ".foo{grid-auto-flow:dense}");
+    minify_test(".foo { grid-auto-flow: dense row }", ".foo{grid-auto-flow:dense}");
+    minify_test(
+      ".foo { grid-auto-flow: column dense }",
+      ".foo{grid-auto-flow:column dense}",
+    );
+    minify_test(
+      ".foo { grid-auto-flow: dense column }",
+      ".foo{grid-auto-flow:column dense}",
+    );
+
+    minify_test(".foo { grid: none }", ".foo{grid:none}");
+    minify_test(".foo { grid: \"a\" 100px \"b\" 1fr }", ".foo{grid:\"a\"100px\"b\"1fr}");
+    minify_test(
+      ".foo { grid: [linename1] \"a\" 100px [linename2] }",
+      ".foo{grid:[linename1]\"a\"100px[linename2]}",
+    );
+    minify_test(
+      ".foo { grid: \"a\" 200px \"b\" min-content }",
+      ".foo{grid:\"a\"200px\"b\"min-content}",
+    );
+    minify_test(
+      ".foo { grid: \"a\" minmax(100px, max-content) \"b\" 20% }",
+      ".foo{grid:\"a\"minmax(100px,max-content)\"b\"20%}",
+    );
+    minify_test(".foo { grid: 100px / 200px }", ".foo{grid:100px/200px}");
+    minify_test(
+      ".foo { grid: minmax(400px, min-content) / repeat(auto-fill, 50px) }",
+      ".foo{grid:minmax(400px,min-content)/repeat(auto-fill,50px)}",
+    );
+
+    minify_test(".foo { grid: 200px / auto-flow }", ".foo{grid:200px/auto-flow}");
+    minify_test(".foo { grid: 30% / auto-flow dense }", ".foo{grid:30%/auto-flow dense}");
+    minify_test(".foo { grid: 30% / dense auto-flow }", ".foo{grid:30%/auto-flow dense}");
+    minify_test(
+      ".foo { grid: repeat(3, [line1 line2 line3] 200px) / auto-flow 300px }",
+      ".foo{grid:repeat(3,[line1 line2 line3]200px)/auto-flow 300px}",
+    );
+    minify_test(
+      ".foo { grid: [line1] minmax(20em, max-content) / auto-flow dense 40% }",
+      ".foo{grid:[line1]minmax(20em,max-content)/auto-flow dense 40%}",
+    );
+    minify_test(".foo { grid: none / auto-flow 1fr }", ".foo{grid:none/auto-flow 1fr}");
+
+    minify_test(".foo { grid: auto-flow / 200px }", ".foo{grid:none/200px}");
+    minify_test(".foo { grid: auto-flow dense / 30% }", ".foo{grid:auto-flow dense/30%}");
+    minify_test(".foo { grid: dense auto-flow / 30% }", ".foo{grid:auto-flow dense/30%}");
+    minify_test(
+      ".foo { grid: auto-flow 300px / repeat(3, [line1 line2 line3] 200px) }",
+      ".foo{grid:auto-flow 300px/repeat(3,[line1 line2 line3]200px)}",
+    );
+    minify_test(
+      ".foo { grid: auto-flow dense 40% / [line1] minmax(20em, max-content) }",
+      ".foo{grid:auto-flow dense 40%/[line1]minmax(20em,max-content)}",
+    );
+
+    minify_test(".foo { grid-row-start: auto }", ".foo{grid-row-start:auto}");
+    minify_test(".foo { grid-row-start: some-area }", ".foo{grid-row-start:some-area}");
+    minify_test(".foo { grid-row-start: 2 }", ".foo{grid-row-start:2}");
+    minify_test(
+      ".foo { grid-row-start: 2 some-line }",
+      ".foo{grid-row-start:2 some-line}",
+    );
+    minify_test(
+      ".foo { grid-row-start: some-line 2 }",
+      ".foo{grid-row-start:2 some-line}",
+    );
+    minify_test(".foo { grid-row-start: span 3 }", ".foo{grid-row-start:span 3}");
+    minify_test(
+      ".foo { grid-row-start: span some-line }",
+      ".foo{grid-row-start:span some-line}",
+    );
+    minify_test(
+      ".foo { grid-row-start: span some-line 1 }",
+      ".foo{grid-row-start:span some-line}",
+    );
+    minify_test(
+      ".foo { grid-row-start: span 1 some-line }",
+      ".foo{grid-row-start:span some-line}",
+    );
+    minify_test(
+      ".foo { grid-row-start: span 5 some-line }",
+      ".foo{grid-row-start:span 5 some-line}",
+    );
+    minify_test(
+      ".foo { grid-row-start: span some-line 5 }",
+      ".foo{grid-row-start:span 5 some-line}",
+    );
+
+    minify_test(
+      ".foo { grid-row-end: span 1 some-line }",
+      ".foo{grid-row-end:span some-line}",
+    );
+    minify_test(
+      ".foo { grid-column-start: span 1 some-line }",
+      ".foo{grid-column-start:span some-line}",
+    );
+    minify_test(
+      ".foo { grid-column-end: span 1 some-line }",
+      ".foo{grid-column-end:span some-line}",
+    );
+
+    minify_test(".foo { grid-row: 1 }", ".foo{grid-row:1}");
+    minify_test(".foo { grid-row: 1 / auto }", ".foo{grid-row:1}");
+    minify_test(".foo { grid-row: 1 / 1 }", ".foo{grid-row:1/1}");
+    minify_test(".foo { grid-row: 1 / 3 }", ".foo{grid-row:1/3}");
+    minify_test(".foo { grid-row: 1 / span 2 }", ".foo{grid-row:1/span 2}");
+    minify_test(".foo { grid-row: main-start }", ".foo{grid-row:main-start}");
+    minify_test(
+      ".foo { grid-row: main-start / main-end }",
+      ".foo{grid-row:main-start/main-end}",
+    );
+    minify_test(
+      ".foo { grid-row: main-start / main-start }",
+      ".foo{grid-row:main-start}",
+    );
+    minify_test(".foo { grid-column: 1 / auto }", ".foo{grid-column:1}");
+
+    minify_test(".foo { grid-area: a }", ".foo{grid-area:a}");
+    minify_test(".foo { grid-area: a / a / a / a }", ".foo{grid-area:a}");
+    minify_test(".foo { grid-area: a / b / a / b }", ".foo{grid-area:a/b}");
+    minify_test(".foo { grid-area: a / b / c / b }", ".foo{grid-area:a/b/c}");
+    minify_test(".foo { grid-area: a / b / c / d }", ".foo{grid-area:a/b/c/d}");
+
+    minify_test(".foo { grid-area: auto / auto / auto / auto }", ".foo{grid-area:auto}");
+    minify_test(".foo { grid-area: 1 / auto }", ".foo{grid-area:1}");
+    minify_test(".foo { grid-area: 1 / 2 / 3 / 4 }", ".foo{grid-area:1/2/3/4}");
+    minify_test(".foo { grid-area: 1 / 1 / 1 / 1 }", ".foo{grid-area:1/1/1/1}");
+
+    test(
+      r#"
+        .foo{
+          grid-template-rows: auto 1fr;
+          grid-template-columns: auto 1fr auto;
+          grid-template-areas: none;
+        }
+      "#,
+      indoc! {r#"
+        .foo {
+          grid-template: auto 1fr / auto 1fr auto;
+        }
+      "#},
+    );
+
+    test(
+      r#"
+        .foo{
+          grid-template-areas: "a a a"
+                               "b b b";
+          grid-template-rows: [header-top] auto [header-bottom main-top] 1fr [main-bottom];
+          grid-template-columns: auto 1fr auto;
+        }
+      "#,
+      indoc! {r#"
+        .foo {
+          grid-template: [header-top] "a a a" [header-bottom]
+                         [main-top] "b b b" 1fr [main-bottom]
+                         / auto 1fr auto;
+        }
+      "#},
+    );
+
+    test(
+      r#"
+        .foo{
+          grid-template-areas: "a a a"
+                               "b b b";
+          grid-template-columns: repeat(3, 1fr);
+          grid-template-rows: auto 1fr;
+        }
+      "#,
+      indoc! {r#"
+        .foo {
+          grid-template-rows: auto 1fr;
+          grid-template-columns: repeat(3, 1fr);
+          grid-template-areas: "a a a"
+                               "b b b";
+        }
+      "#},
+    );
+
+    test(
+      r#"
+        .foo{
+          grid-template-areas: "a a a"
+                               "b b b";
+          grid-template-columns: auto 1fr auto;
+          grid-template-rows: repeat(2, 1fr);
+        }
+      "#,
+      indoc! {r#"
+        .foo {
+          grid-template-rows: repeat(2, 1fr);
+          grid-template-columns: auto 1fr auto;
+          grid-template-areas: "a a a"
+                               "b b b";
+        }
+      "#},
+    );
+
+    test(
+      r#"
+        .foo{
+          grid-template-areas: ". a a ."
+                               ". b b .";
+          grid-template-rows: auto 1fr;
+          grid-template-columns: 10px 1fr 1fr 10px;
+        }
+      "#,
+      indoc! {r#"
+        .foo {
+          grid-template: ". a a ."
+                         ". b b ." 1fr
+                         / 10px 1fr 1fr 10px;
+        }
+      "#},
+    );
+
+    test(
+      r#"
+        .foo{
+          grid-template-areas: none;
+          grid-template-columns: auto 1fr auto;
+          grid-template-rows: repeat(2, 1fr);
+        }
+      "#,
+      indoc! {r#"
+        .foo {
+          grid-template: repeat(2, 1fr) / auto 1fr auto;
+        }
+      "#},
+    );
+
+    test(
+      r#"
+        .foo{
+          grid-template-areas: none;
+          grid-template-columns: none;
+          grid-template-rows: none;
+        }
+      "#,
+      indoc! {r#"
+        .foo {
+          grid-template: none;
+        }
+      "#},
+    );
+
+    test(
+      r#"
+        .foo{
+          grid-template-areas: "a a a"
+                               "b b b";
+          grid-template-rows: [header-top] auto [header-bottom main-top] 1fr [main-bottom];
+          grid-template-columns: auto 1fr auto;
+          grid-auto-flow: row;
+          grid-auto-rows: auto;
+          grid-auto-columns: auto;
+        }
+      "#,
+      indoc! {r#"
+        .foo {
+          grid: [header-top] "a a a" [header-bottom]
+                [main-top] "b b b" 1fr [main-bottom]
+                / auto 1fr auto;
+        }
+      "#},
+    );
+
+    test(
+      r#"
+        .foo{
+          grid-template-areas: none;
+          grid-template-columns: auto 1fr auto;
+          grid-template-rows: repeat(2, 1fr);
+          grid-auto-flow: row;
+          grid-auto-rows: auto;
+          grid-auto-columns: auto;
+        }
+      "#,
+      indoc! {r#"
+        .foo {
+          grid: repeat(2, 1fr) / auto 1fr auto;
+        }
+      "#},
+    );
+
+    test(
+      r#"
+        .foo{
+          grid-template-areas: none;
+          grid-template-columns: none;
+          grid-template-rows: none;
+          grid-auto-flow: row;
+          grid-auto-rows: auto;
+          grid-auto-columns: auto;
+        }
+      "#,
+      indoc! {r#"
+        .foo {
+          grid: none;
+        }
+      "#},
+    );
+
+    test(
+      r#"
+        .foo{
+          grid-template-areas: "a a a"
+                               "b b b";
+          grid-template-rows: [header-top] auto [header-bottom main-top] 1fr [main-bottom];
+          grid-template-columns: auto 1fr auto;
+          grid-auto-flow: column;
+          grid-auto-rows: 1fr;
+          grid-auto-columns: 1fr;
+        }
+      "#,
+      indoc! {r#"
+        .foo {
+          grid-template: [header-top] "a a a" [header-bottom]
+                         [main-top] "b b b" 1fr [main-bottom]
+                         / auto 1fr auto;
+          grid-auto-rows: 1fr;
+          grid-auto-columns: 1fr;
+          grid-auto-flow: column;
+        }
+      "#},
+    );
+
+    test(
+      r#"
+        .foo{
+          grid-template-rows: auto 1fr;
+          grid-template-columns: auto 1fr auto;
+          grid-template-areas: none;
+          grid-auto-flow: row;
+          grid-auto-rows: auto;
+          grid-auto-columns: auto;
+        }
+      "#,
+      indoc! {r#"
+        .foo {
+          grid: auto 1fr / auto 1fr auto;
+        }
+      "#},
+    );
+
+    test(
+      r#"
+        .foo{
+          grid-template-rows: auto 1fr;
+          grid-template-columns: auto 1fr auto;
+          grid-template-areas: none;
+          grid-auto-flow: column;
+          grid-auto-rows: 1fr;
+          grid-auto-columns: 1fr;
+        }
+      "#,
+      indoc! {r#"
+        .foo {
+          grid-template: auto 1fr / auto 1fr auto;
+          grid-auto-rows: 1fr;
+          grid-auto-columns: 1fr;
+          grid-auto-flow: column;
+        }
+      "#},
+    );
+
+    test(
+      r#"
+        .foo{
+          grid-template-rows: none;
+          grid-template-columns: auto 1fr auto;
+          grid-template-areas: none;
+          grid-auto-flow: column;
+          grid-auto-rows: 1fr;
+          grid-auto-columns: 1fr;
+        }
+      "#,
+      indoc! {r#"
+        .foo {
+          grid-template: none / auto 1fr auto;
+          grid-auto-rows: 1fr;
+          grid-auto-columns: 1fr;
+          grid-auto-flow: column;
+        }
+      "#},
+    );
+
+    test(
+      r#"
+        .foo{
+          grid-template-rows: none;
+          grid-template-columns: auto 1fr auto;
+          grid-template-areas: none;
+          grid-auto-flow: row;
+          grid-auto-rows: 1fr;
+          grid-auto-columns: auto;
+        }
+      "#,
+      indoc! {r#"
+        .foo {
+          grid: auto-flow 1fr / auto 1fr auto;
+        }
+      "#},
+    );
+
+    test(
+      r#"
+        .foo{
+          grid-template-rows: none;
+          grid-template-columns: auto 1fr auto;
+          grid-template-areas: none;
+          grid-auto-flow: row dense;
+          grid-auto-rows: 1fr;
+          grid-auto-columns: auto;
+        }
+      "#,
+      indoc! {r#"
+        .foo {
+          grid: auto-flow dense 1fr / auto 1fr auto;
+        }
+      "#},
+    );
+
+    test(
+      r#"
+        .foo{
+          grid-template-rows: auto 1fr auto;
+          grid-template-columns: none;
+          grid-template-areas: none;
+          grid-auto-flow: column;
+          grid-auto-rows: auto;
+          grid-auto-columns: 1fr;
+        }
+      "#,
+      indoc! {r#"
+        .foo {
+          grid: auto 1fr auto / auto-flow 1fr;
+        }
+      "#},
+    );
+
+    test(
+      r#"
+        .foo{
+          grid-template-rows: auto 1fr auto;
+          grid-template-columns: none;
+          grid-template-areas: none;
+          grid-auto-flow: column dense;
+          grid-auto-rows: auto;
+          grid-auto-columns: 1fr;
+        }
+      "#,
+      indoc! {r#"
+        .foo {
+          grid: auto 1fr auto / auto-flow dense 1fr;
+        }
+      "#},
+    );
+
+    test(
+      r#"
+        .foo{
+          grid-template-rows: auto 1fr auto;
+          grid-template-columns: none;
+          grid-template-areas: none;
+          grid-auto-flow: var(--auto-flow);
+          grid-auto-rows: auto;
+          grid-auto-columns: 1fr;
+        }
+      "#,
+      indoc! {r#"
+        .foo {
+          grid-template: auto 1fr auto / none;
+          grid-auto-flow: var(--auto-flow);
+          grid-auto-rows: auto;
+          grid-auto-columns: 1fr;
+        }
+      "#},
+    );
+
+    test(
+      r#"
+        .foo{
+          grid: auto 1fr auto / auto-flow dense 1fr;
+          grid-template-rows: 1fr 1fr 1fr;
+        }
+      "#,
+      indoc! {r#"
+        .foo {
+          grid: 1fr 1fr 1fr / auto-flow dense 1fr;
+        }
+      "#},
+    );
+
+    test(
+      r#"
+        .foo{
+          grid-row-start: a;
+          grid-row-end: a;
+          grid-column-start: a;
+          grid-column-end: a;
+        }
+      "#,
+      indoc! {r#"
+        .foo {
+          grid-area: a;
+        }
+      "#},
+    );
+
+    test(
+      r#"
+        .foo{
+          grid-row-start: 1;
+          grid-row-end: 2;
+          grid-column-start: 3;
+          grid-column-end: 4;
+        }
+      "#,
+      indoc! {r#"
+        .foo {
+          grid-area: 1 / 3 / 2 / 4;
+        }
+      "#},
+    );
+
+    test(
+      r#"
+        .foo{
+          grid-row-start: a;
+          grid-row-end: a;
+        }
+      "#,
+      indoc! {r#"
+        .foo {
+          grid-row: a;
+        }
+      "#},
+    );
+
+    test(
+      r#"
+        .foo{
+          grid-column-start: a;
+          grid-column-end: a;
+        }
+      "#,
+      indoc! {r#"
+        .foo {
+          grid-column: a;
+        }
+      "#},
+    );
+  }
+
+  #[test]
+  fn test_moz_document() {
+    minify_test(
+      r#"
+      @-moz-document url-prefix() {
+        h1 {
+          color: yellow;
+        }
+      }
+    "#,
+      "@-moz-document url-prefix(){h1{color:#ff0}}",
+    );
+    minify_test(
+      r#"
+      @-moz-document url-prefix("") {
+        h1 {
+          color: yellow;
+        }
+      }
+    "#,
+      "@-moz-document url-prefix(){h1{color:#ff0}}",
+    );
+    error_test(
+      "@-moz-document url-prefix(foo) {}",
+      ParserError::UnexpectedToken(crate::properties::custom::Token::Ident("foo".into())),
+    );
+    error_test(
+      "@-moz-document url-prefix(\"foo\") {}",
+      ParserError::UnexpectedToken(crate::properties::custom::Token::String("foo".into())),
+    );
+  }
+
+  #[test]
+  fn test_custom_properties() {
+    minify_test(".foo { --test: ; }", ".foo{--test: }");
+    minify_test(".foo { --test:  ; }", ".foo{--test: }");
+    minify_test(".foo { --test: foo; }", ".foo{--test:foo}");
+    minify_test(".foo { --test:  foo; }", ".foo{--test:foo}");
+    minify_test(".foo { --test: foo ; }", ".foo{--test:foo}");
+    minify_test(".foo { --test: foo  ; }", ".foo{--test:foo}");
+    minify_test(".foo { --test:foo; }", ".foo{--test:foo}");
+    minify_test(".foo { --test:foo ; }", ".foo{--test:foo}");
+    minify_test(".foo { --test: var(--foo, 20px); }", ".foo{--test:var(--foo,20px)}");
+    minify_test(
+      ".foo { transition: var(--foo, 20px),\nvar(--bar, 40px); }",
+      ".foo{transition:var(--foo,20px),var(--bar,40px)}",
+    );
+    minify_test(
+      ".foo { background: var(--color) var(--image); }",
+      ".foo{background:var(--color)var(--image)}",
+    );
+    minify_test(
+      ".foo { height: calc(var(--spectrum-global-dimension-size-300) / 2);",
+      ".foo{height:calc(var(--spectrum-global-dimension-size-300)/2)}",
+    );
+    minify_test(
+      ".foo { color: var(--color, rgb(255, 255, 0)); }",
+      ".foo{color:var(--color,#ff0)}",
+    );
+    minify_test(
+      ".foo { color: var(--color, #ffff00); }",
+      ".foo{color:var(--color,#ff0)}",
+    );
+    minify_test(
+      ".foo { color: var(--color, rgb(var(--red), var(--green), 0)); }",
+      ".foo{color:var(--color,rgb(var(--red),var(--green),0))}",
+    );
+    minify_test(".foo { --test: .5s; }", ".foo{--test:.5s}");
+    minify_test(".foo { --theme-sizes-1\\/12: 2 }", ".foo{--theme-sizes-1\\/12:2}");
+    minify_test(".foo { --test: 0px; }", ".foo{--test:0px}");
+
+    prefix_test(
+      r#"
+      .foo {
+        --custom: lab(40% 56.6 39);
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        --custom: #b32323;
+      }
+
+      @supports (color: lab(0% 0 0)) {
+        .foo {
+          --custom: lab(40% 56.6 39);
+        }
+      }
+    "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      @supports (color: lab(0% 0 0)) {
+        .foo {
+          --custom: lab(40% 56.6 39);
+        }
+      }
+    "#,
+      indoc! {r#"
+      @supports (color: lab(0% 0 0)) {
+        .foo {
+          --custom: lab(40% 56.6 39);
+        }
+      }
+    "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        --custom: lab(40% 56.6 39) !important;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        --custom: #b32323 !important;
+      }
+
+      @supports (color: lab(0% 0 0)) {
+        .foo {
+          --custom: lab(40% 56.6 39) !important;
+        }
+      }
+    "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      @supports (color: lab(0% 0 0)) {
+        .foo {
+          --custom: lab(40% 56.6 39) !important;
+        }
+      }
+    "#,
+      indoc! {r#"
+      @supports (color: lab(0% 0 0)) {
+        .foo {
+          --custom: lab(40% 56.6 39) !important;
+        }
+      }
+    "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        --custom: lab(40% 56.6 39);
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        --custom: #b32323;
+      }
+
+      @supports (color: color(display-p3 0 0 0)) {
+        .foo {
+          --custom: color(display-p3 .643308 .192455 .167712);
+        }
+      }
+
+      @supports (color: lab(0% 0 0)) {
+        .foo {
+          --custom: lab(40% 56.6 39);
+        }
+      }
+    "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        safari: Some(14 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      @supports (color: color(display-p3 0 0 0)) {
+        .foo {
+          --custom: color(display-p3 .643308 .192455 .167712);
+        }
+      }
+
+      @supports (color: lab(0% 0 0)) {
+        .foo {
+          --custom: lab(40% 56.6 39);
+        }
+      }
+    "#,
+      indoc! {r#"
+      @supports (color: color(display-p3 0 0 0)) {
+        .foo {
+          --custom: color(display-p3 .643308 .192455 .167712);
+        }
+      }
+
+      @supports (color: lab(0% 0 0)) {
+        .foo {
+          --custom: lab(40% 56.6 39);
+        }
+      }
+    "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        safari: Some(14 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        --custom: lab(40% 56.6 39);
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        --custom: color(display-p3 .643308 .192455 .167712);
+      }
+
+      @supports (color: lab(0% 0 0)) {
+        .foo {
+          --custom: lab(40% 56.6 39);
+        }
+      }
+    "#},
+      Browsers {
+        safari: Some(14 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        --custom: lab(40% 56.6 39);
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        --custom: lab(40% 56.6 39);
+      }
+    "#},
+      Browsers {
+        safari: Some(15 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        --custom: oklab(59.686% 0.1009 0.1192);
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        --custom: lab(52.2319% 40.1449 59.9171);
+      }
+    "#},
+      Browsers {
+        safari: Some(15 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        --custom: oklab(59.686% 0.1009 0.1192);
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        --custom: color(display-p3 .724144 .386777 .148795);
+      }
+
+      @supports (color: lab(0% 0 0)) {
+        .foo {
+          --custom: lab(52.2319% 40.1449 59.9171);
+        }
+      }
+    "#},
+      Browsers {
+        safari: Some(14 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        --custom: oklab(59.686% 0.1009 0.1192);
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        --custom: #c65d07;
+      }
+
+      @supports (color: color(display-p3 0 0 0)) {
+        .foo {
+          --custom: color(display-p3 .724144 .386777 .148795);
+        }
+      }
+
+      @supports (color: lab(0% 0 0)) {
+        .foo {
+          --custom: lab(52.2319% 40.1449 59.9171);
+        }
+      }
+    "#},
+      Browsers {
+        safari: Some(14 << 16),
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        --foo: oklab(59.686% 0.1009 0.1192);
+        --bar: lab(40% 56.6 39);
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        --foo: #c65d07;
+        --bar: #b32323;
+      }
+
+      @supports (color: color(display-p3 0 0 0)) {
+        .foo {
+          --foo: color(display-p3 .724144 .386777 .148795);
+          --bar: color(display-p3 .643308 .192455 .167712);
+        }
+      }
+
+      @supports (color: lab(0% 0 0)) {
+        .foo {
+          --foo: lab(52.2319% 40.1449 59.9171);
+          --bar: lab(40% 56.6 39);
+        }
+      }
+    "#},
+      Browsers {
+        safari: Some(14 << 16),
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        --foo: color(display-p3 0 1 0);
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        --foo: #00f942;
+      }
+
+      @supports (color: color(display-p3 0 0 0)) {
+        .foo {
+          --foo: color(display-p3 0 1 0);
+        }
+      }
+    "#},
+      Browsers {
+        safari: Some(14 << 16),
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      @supports (color: color(display-p3 0 0 0)) {
+        .foo {
+          --foo: color(display-p3 0 1 0);
+        }
+      }
+    "#,
+      indoc! {r#"
+      @supports (color: color(display-p3 0 0 0)) {
+        .foo {
+          --foo: color(display-p3 0 1 0);
+        }
+      }
+    "#},
+      Browsers {
+        safari: Some(14 << 16),
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        --foo: color(display-p3 0 1 0);
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        --foo: color(display-p3 0 1 0);
+      }
+    "#},
+      Browsers {
+        safari: Some(14 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        --foo: color(display-p3 0 1 0);
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        --foo: #00f942;
+      }
+
+      @supports (color: color(display-p3 0 0 0)) {
+        .foo {
+          --foo: color(display-p3 0 1 0);
+        }
+      }
+    "#},
+      Browsers {
+        safari: Some(15 << 16),
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        --foo: color(display-p3 0 1 0);
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        --foo: #00f942;
+      }
+
+      @supports (color: color(display-p3 0 0 0)) {
+        .foo {
+          --foo: color(display-p3 0 1 0);
+        }
+      }
+    "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        text-decoration: underline;
+      }
+
+      .foo {
+        --custom: lab(40% 56.6 39);
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        --custom: #b32323;
+        text-decoration: underline;
+      }
+
+      @supports (color: lab(0% 0 0)) {
+        .foo {
+          --custom: lab(40% 56.6 39);
+        }
+      }
+    "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        --custom: lab(40% 56.6 39);
+      }
+
+      .foo {
+        text-decoration: underline;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        --custom: #b32323;
+      }
+
+      @supports (color: lab(0% 0 0)) {
+        .foo {
+          --custom: lab(40% 56.6 39);
+        }
+      }
+
+      .foo {
+        text-decoration: underline;
+      }
+    "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      @keyframes foo {
+        from {
+          --custom: lab(40% 56.6 39);
+        }
+
+        to {
+          --custom: lch(50.998% 135.363 338);
+        }
+      }
+    "#,
+      indoc! {r#"
+      @keyframes foo {
+        from {
+          --custom: #b32323;
+        }
+
+        to {
+          --custom: #ee00be;
+        }
+      }
+
+      @supports (color: lab(0% 0 0)) {
+        @keyframes foo {
+          from {
+            --custom: lab(40% 56.6 39);
+          }
+
+          to {
+            --custom: lab(50.998% 125.506 -50.7078);
+          }
+        }
+      }
+    "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      @supports (color: lab(0% 0 0)) {
+        @keyframes foo {
+          from {
+            --custom: lab(40% 56.6 39);
+          }
+
+          to {
+            --custom: lab(50.998% 125.506 -50.7078);
+          }
+        }
+      }
+    "#,
+      indoc! {r#"
+      @supports (color: lab(0% 0 0)) {
+        @keyframes foo {
+          from {
+            --custom: lab(40% 56.6 39);
+          }
+
+          to {
+            --custom: lab(50.998% 125.506 -50.7078);
+          }
+        }
+      }
+    "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      @keyframes foo {
+        from {
+          --custom: lab(40% 56.6 39);
+        }
+
+        to {
+          --custom: lch(50.998% 135.363 338);
+        }
+      }
+    "#,
+      indoc! {r#"
+      @keyframes foo {
+        from {
+          --custom: #b32323;
+        }
+
+        to {
+          --custom: #ee00be;
+        }
+      }
+
+      @supports (color: color(display-p3 0 0 0)) {
+        @keyframes foo {
+          from {
+            --custom: color(display-p3 .643308 .192455 .167712);
+          }
+
+          to {
+            --custom: color(display-p3 .972962 -.362078 .804206);
+          }
+        }
+      }
+
+      @supports (color: lab(0% 0 0)) {
+        @keyframes foo {
+          from {
+            --custom: lab(40% 56.6 39);
+          }
+
+          to {
+            --custom: lab(50.998% 125.506 -50.7078);
+          }
+        }
+      }
+    "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        safari: Some(14 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      @supports (color: color(display-p3 0 0 0)) {
+        @keyframes foo {
+          from {
+            --custom: color(display-p3 .643308 .192455 .167712);
+          }
+
+          to {
+            --custom: color(display-p3 .972962 -.362078 .804206);
+          }
+        }
+      }
+
+      @supports (color: lab(0% 0 0)) {
+        @keyframes foo {
+          from {
+            --custom: lab(40% 56.6 39);
+          }
+
+          to {
+            --custom: lab(50.998% 125.506 -50.7078);
+          }
+        }
+      }
+    "#,
+      indoc! {r#"
+      @supports (color: color(display-p3 0 0 0)) {
+        @keyframes foo {
+          from {
+            --custom: color(display-p3 .643308 .192455 .167712);
+          }
+
+          to {
+            --custom: color(display-p3 .972962 -.362078 .804206);
+          }
+        }
+      }
+
+      @supports (color: lab(0% 0 0)) {
+        @keyframes foo {
+          from {
+            --custom: lab(40% 56.6 39);
+          }
+
+          to {
+            --custom: lab(50.998% 125.506 -50.7078);
+          }
+        }
+      }
+    "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        safari: Some(14 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      @keyframes foo {
+        from {
+          --custom: #ff0;
+          opacity: 0;
+        }
+
+        to {
+          --custom: lch(50.998% 135.363 338);
+          opacity: 1;
+        }
+      }
+    "#,
+      indoc! {r#"
+      @keyframes foo {
+        from {
+          --custom: #ff0;
+          opacity: 0;
+        }
+
+        to {
+          --custom: #ee00be;
+          opacity: 1;
+        }
+      }
+
+      @supports (color: lab(0% 0 0)) {
+        @keyframes foo {
+          from {
+            --custom: #ff0;
+            opacity: 0;
+          }
+
+          to {
+            --custom: lab(50.998% 125.506 -50.7078);
+            opacity: 1;
+          }
+        }
+      }
+    "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      @keyframes foo {
+        from {
+          text-decoration: var(--foo) lab(29.2345% 39.3825 20.0664);
+        }
+      }
+    "#,
+      indoc! {r#"
+      @keyframes foo {
+        from {
+          text-decoration: var(--foo) #7d2329;
+        }
+      }
+
+      @supports (color: lab(0% 0 0)) {
+        @keyframes foo {
+          from {
+            text-decoration: var(--foo) lab(29.2345% 39.3825 20.0664);
+          }
+        }
+      }
+    "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+  }
+
+  #[test]
+  fn test_charset() {
+    test(
+      r#"
+      @charset "UTF-8";
+
+      .foo {
+        color: red;
+      }
+
+      @charset "UTF-8";
+
+      .bar {
+        color: yellow;
+      }
+    "#,
+      indoc! { r#"
+      .foo {
+        color: red;
+      }
+
+      .bar {
+        color: #ff0;
+      }
+    "#},
+    )
+  }
+
+  #[test]
+  fn test_style_attr() {
+    attr_test("color: yellow; flex: 1 1 auto", "color: #ff0; flex: auto", false, None);
+    attr_test("color: yellow; flex: 1 1 auto", "color:#ff0;flex:auto", true, None);
+    attr_test(
+      "border-inline-start: 2px solid red",
+      "border-inline-start: 2px solid red",
+      false,
+      Some(Browsers {
+        safari: Some(12 << 16),
+        ..Browsers::default()
+      }),
+    );
+    attr_test(
+      "color: lab(40% 56.6 39);",
+      "color:#b32323;color:lab(40% 56.6 39)",
+      true,
+      Some(Browsers {
+        safari: Some(8 << 16),
+        ..Browsers::default()
+      }),
+    );
+    attr_test(
+      "--foo: lab(40% 56.6 39);",
+      "--foo:#b32323",
+      true,
+      Some(Browsers {
+        safari: Some(8 << 16),
+        ..Browsers::default()
+      }),
+    );
+    attr_test(
+      "text-decoration: var(--foo) lab(40% 56.6 39);",
+      "text-decoration:var(--foo)#b32323",
+      true,
+      Some(Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      }),
+    );
+  }
+
+  #[test]
+  fn test_nesting() {
+    nesting_test(
+      r#"
+        .foo {
+          color: blue;
+          & > .bar { color: red; }
+        }
+      "#,
+      indoc! {r#"
+        .foo {
+          color: #00f;
+        }
+
+        .foo > .bar {
+          color: red;
+        }
+      "#},
+    );
+
+    nesting_test(
+      r#"
+        .foo {
+          color: blue;
+          &.bar { color: red; }
+        }
+      "#,
+      indoc! {r#"
+        .foo {
+          color: #00f;
+        }
+
+        .foo.bar {
+          color: red;
+        }
+      "#},
+    );
+
+    nesting_test(
+      r#"
+        .foo, .bar {
+          color: blue;
+          & + .baz, &.qux { color: red; }
+        }
+      "#,
+      indoc! {r#"
+        .foo, .bar {
+          color: #00f;
+        }
+
+        :is(.foo, .bar) + .baz, :is(.foo, .bar).qux {
+          color: red;
+        }
+      "#},
+    );
+
+    nesting_test(
+      r#"
+        .foo {
+          color: blue;
+          & .bar & .baz & .qux { color: red; }
+        }
+      "#,
+      indoc! {r#"
+        .foo {
+          color: #00f;
+        }
+
+        .foo .bar .foo .baz .foo .qux {
+          color: red;
+        }
+      "#},
+    );
+
+    nesting_test(
+      r#"
+        .foo {
+          color: blue;
+          & { padding: 2ch; }
+        }
+      "#,
+      indoc! {r#"
+        .foo {
+          color: #00f;
+        }
+
+        .foo {
+          padding: 2ch;
+        }
+      "#},
+    );
+
+    nesting_test(
+      r#"
+        .foo {
+          color: blue;
+          && { padding: 2ch; }
+        }
+      "#,
+      indoc! {r#"
+        .foo {
+          color: #00f;
+        }
+
+        .foo.foo {
+          padding: 2ch;
+        }
+      "#},
+    );
+
+    nesting_test(
+      r#"
+        .error, .invalid {
+          &:hover > .baz { color: red; }
+        }
+      "#,
+      indoc! {r#"
+        :is(.error, .invalid):hover > .baz {
+          color: red;
+        }
+      "#},
+    );
+
+    nesting_test(
+      r#"
+        .foo {
+          &:is(.bar, &.baz) { color: red; }
+        }
+      "#,
+      indoc! {r#"
+        .foo:is(.bar, .foo.baz) {
+          color: red;
+        }
+      "#},
+    );
+
+    nesting_test(
+      r#"
+        figure {
+          margin: 0;
+
+          & > figcaption {
+            background: hsl(0 0% 0% / 50%);
+
+            & > p {
+              font-size: .9rem;
+            }
+          }
+        }
+      "#,
+      indoc! {r#"
+        figure {
+          margin: 0;
+        }
+
+        figure > figcaption {
+          background: #00000080;
+        }
+
+        figure > figcaption > p {
+          font-size: .9rem;
+        }
+      "#},
+    );
+
+    nesting_test(
+      r#"
+        .foo {
+          display: grid;
+
+          @media (orientation: landscape) {
+            grid-auto-flow: column;
+          }
+        }
+      "#,
+      indoc! {r#"
+        .foo {
+          display: grid;
+        }
+
+        @media (orientation: landscape) {
+          .foo {
+            grid-auto-flow: column;
+          }
+        }
+      "#},
+    );
+
+    nesting_test(
+      r#"
+        .foo {
+          display: grid;
+
+          @media (orientation: landscape) {
+            grid-auto-flow: column;
+
+            @media (width > 1024px) {
+              max-inline-size: 1024px;
+            }
+          }
+        }
+      "#,
+      indoc! {r#"
+        .foo {
+          display: grid;
+        }
+
+        @media (orientation: landscape) {
+          .foo {
+            grid-auto-flow: column;
+          }
+
+          @media not (max-width: 1024px) {
+            .foo {
+              max-inline-size: 1024px;
+            }
+          }
+        }
+      "#},
+    );
+
+    nesting_test(
+      r#"
+        .foo {
+          @media (min-width: 640px) {
+            color: red !important;
+          }
+        }
+      "#,
+      indoc! {r#"
+        @media (min-width: 640px) {
+          .foo {
+            color: red !important;
+          }
+        }
+      "#},
+    );
+
+    nesting_test(
+      r#"
+        .foo {
+          display: grid;
+
+          @supports (foo: bar) {
+            grid-auto-flow: column;
+          }
+        }
+      "#,
+      indoc! {r#"
+        .foo {
+          display: grid;
+        }
+
+        @supports (foo: bar) {
+          .foo {
+            grid-auto-flow: column;
+          }
+        }
+      "#},
+    );
+
+    nesting_test(
+      r#"
+        .foo {
+          display: grid;
+
+          @container (min-width: 100px) {
+            grid-auto-flow: column;
+          }
+        }
+      "#,
+      indoc! {r#"
+        .foo {
+          display: grid;
+        }
+
+        @container (width >= 100px) {
+          .foo {
+            grid-auto-flow: column;
+          }
+        }
+      "#},
+    );
+
+    nesting_test(
+      r#"
+        .foo {
+          display: grid;
+
+          @layer test {
+            grid-auto-flow: column;
+          }
+        }
+      "#,
+      indoc! {r#"
+        .foo {
+          display: grid;
+        }
+
+        @layer test {
+          .foo {
+            grid-auto-flow: column;
+          }
+        }
+      "#},
+    );
+
+    nesting_test(
+      r#"
+        .foo {
+          display: grid;
+
+          @layer {
+            grid-auto-flow: column;
+          }
+        }
+      "#,
+      indoc! {r#"
+        .foo {
+          display: grid;
+        }
+
+        @layer {
+          .foo {
+            grid-auto-flow: column;
+          }
+        }
+      "#},
+    );
+
+    nesting_test(
+      r#"
+        @namespace "http://example.com/foo";
+        @namespace toto "http://toto.example.org";
+
+        .foo {
+          &div {
+            color: red;
+          }
+
+          &* {
+            color: green;
+          }
+
+          &|x {
+            color: red;
+          }
+
+          &*|x {
+            color: green;
+          }
+
+          &toto|x {
+            color: red;
+          }
+        }
+      "#,
+      indoc! {r#"
+        @namespace "http://example.com/foo";
+        @namespace toto "http://toto.example.org";
+
+        div.foo {
+          color: red;
+        }
+
+        *.foo {
+          color: green;
+        }
+
+        |x.foo {
+          color: red;
+        }
+
+        *|x.foo {
+          color: green;
+        }
+
+        toto|x.foo {
+          color: red;
+        }
+      "#},
+    );
+
+    nesting_test(
+      r#"
+        .foo {
+          &article > figure {
+            color: red;
+          }
+        }
+      "#,
+      indoc! {r#"
+        article.foo > figure {
+          color: red;
+        }
+      "#},
+    );
+
+    nesting_test(
+      r#"
+        div {
+          &.bar {
+            background: green;
+          }
+        }
+      "#,
+      indoc! {r#"
+        div.bar {
+          background: green;
+        }
+      "#},
+    );
+
+    nesting_test(
+      r#"
+        div > .foo {
+          &span {
+            background: green;
+          }
+        }
+      "#,
+      indoc! {r#"
+        span:is(div > .foo) {
+          background: green;
+        }
+      "#},
+    );
+
+    nesting_test(
+      r#"
+        .foo {
+          & h1 {
+            background: green;
+          }
+        }
+      "#,
+      indoc! {r#"
+        .foo h1 {
+          background: green;
+        }
+      "#},
+    );
+
+    nesting_test(
+      r#"
+        .foo .bar {
+          &h1 {
+            background: green;
+          }
+        }
+      "#,
+      indoc! {r#"
+        h1:is(.foo .bar) {
+          background: green;
+        }
+      "#},
+    );
+
+    nesting_test(
+      r#"
+        .foo.bar {
+          &h1 {
+            background: green;
+          }
+        }
+      "#,
+      indoc! {r#"
+        h1.foo.bar {
+          background: green;
+        }
+      "#},
+    );
+
+    nesting_test(
+      r#"
+        .foo .bar {
+          &h1 .baz {
+            background: green;
+          }
+        }
+      "#,
+      indoc! {r#"
+        h1:is(.foo .bar) .baz {
+          background: green;
+        }
+      "#},
+    );
+
+    nesting_test(
+      r#"
+        .foo .bar {
+          &.baz {
+            background: green;
+          }
+        }
+      "#,
+      indoc! {r#"
+        .foo .bar.baz {
+          background: green;
+        }
+      "#},
+    );
+
+    nesting_test(
+      r#"
+        .foo {
+          color: red;
+          @nest .parent & {
+            color: blue;
+          }
+        }
+      "#,
+      indoc! {r#"
+        .foo {
+          color: red;
+        }
+
+        .parent .foo {
+          color: #00f;
+        }
+      "#},
+    );
+
+    nesting_test(
+      r#"
+        .foo {
+          color: red;
+          @nest :not(&) {
+            color: blue;
+          }
+        }
+      "#,
+      indoc! {r#"
+        .foo {
+          color: red;
+        }
+
+        :not(.foo) {
+          color: #00f;
+        }
+      "#},
+    );
+
+    nesting_test(
+      r#"
+        .foo {
+          color: blue;
+          @nest .bar & {
+            color: red;
+            &.baz {
+              color: green;
+            }
+          }
+        }
+      "#,
+      indoc! {r#"
+        .foo {
+          color: #00f;
+        }
+
+        .bar .foo {
+          color: red;
+        }
+
+        .bar .foo.baz {
+          color: green;
+        }
+      "#},
+    );
+
+    nesting_test(
+      r#"
+        .foo {
+          @nest :not(&) {
+            color: red;
+          }
+
+          & h1 {
+            background: green;
+          }
+        }
+      "#,
+      indoc! {r#"
+        :not(.foo) {
+          color: red;
+        }
+
+        .foo h1 {
+          background: green;
+        }
+      "#},
+    );
+
+    nesting_test(
+      r#"
+        .foo {
+          & h1 {
+            background: green;
+          }
+
+          @nest :not(&) {
+            color: red;
+          }
+        }
+      "#,
+      indoc! {r#"
+        .foo h1 {
+          background: green;
+        }
+
+        :not(.foo) {
+          color: red;
+        }
+      "#},
+    );
+
+    nesting_test(
+      r#"
+        .foo .bar {
+          @nest h1& {
+            background: green;
+          }
+        }
+      "#,
+      indoc! {r#"
+        h1:is(.foo .bar) {
+          background: green;
+        }
+      "#},
+    );
+
+    nesting_test(
+      r#"
+        @namespace "http://example.com/foo";
+        @namespace toto "http://toto.example.org";
+
+        div {
+          @nest .foo& {
+            color: red;
+          }
+        }
+
+        * {
+          @nest .foo& {
+            color: red;
+          }
+        }
+
+        |x {
+          @nest .foo& {
+            color: red;
+          }
+        }
+
+        *|x {
+          @nest .foo& {
+            color: red;
+          }
+        }
+
+        toto|x {
+          @nest .foo& {
+            color: red;
+          }
+        }
+      "#,
+      indoc! {r#"
+        @namespace "http://example.com/foo";
+        @namespace toto "http://toto.example.org";
+
+        .foo:is(div) {
+          color: red;
+        }
+
+        .foo:is(*) {
+          color: red;
+        }
+
+        .foo:is(|x) {
+          color: red;
+        }
+
+        .foo:is(*|x) {
+          color: red;
+        }
+
+        .foo:is(toto|x) {
+          color: red;
+        }
+      "#},
+    );
+
+    nesting_test(
+      r#"
+        .foo .bar {
+          @nest h1 .baz& {
+            background: green;
+          }
+        }
+      "#,
+      indoc! {r#"
+        h1 .baz:is(.foo .bar) {
+          background: green;
+        }
+      "#},
+    );
+
+    nesting_test(
+      r#"
+        .foo .bar {
+          @nest .baz& {
+            background: green;
+          }
+        }
+      "#,
+      indoc! {r#"
+        .baz:is(.foo .bar) {
+          background: green;
+        }
+      "#},
+    );
+
+    nesting_test(
+      r#"
+        .foo .bar {
+          @nest .baz & {
+            background: green;
+          }
+        }
+      "#,
+      indoc! {r#"
+        .baz :is(.foo .bar) {
+          background: green;
+        }
+      "#},
+    );
+
+    nesting_test(
+      r#"
+        .foo {
+          color: red;
+          @nest & > .bar {
+            color: blue;
+          }
+        }
+      "#,
+      indoc! {r#"
+        .foo {
+          color: red;
+        }
+
+        .foo > .bar {
+          color: #00f;
+        }
+      "#},
+    );
+
+    nesting_test(
+      r#"
+      .foo {
+        color: red;
+        .bar {
+          color: blue;
+        }
+      }
+      "#,
+      indoc! {r#"
+      .foo {
+        color: red;
+      }
+
+      .foo .bar {
+        color: #00f;
+      }
+      "#},
+    );
+
+    nesting_test(
+      r#"
+      .foo {
+        color: red;
+        .bar & {
+          color: blue;
+        }
+      }
+      "#,
+      indoc! {r#"
+      .foo {
+        color: red;
+      }
+
+      .bar .foo {
+        color: #00f;
+      }
+      "#},
+    );
+
+    nesting_test(
+      r#"
+      .foo {
+        color: red;
+        + .bar + & { color: blue; }
+      }
+      "#,
+      indoc! {r#"
+      .foo {
+        color: red;
+      }
+
+      .foo + .bar + .foo {
+        color: #00f;
+      }
+      "#},
+    );
+
+    nesting_test(
+      r#"
+      .foo {
+        color: red;
+        .bar & {
+          color: blue;
+        }
+      }
+      "#,
+      indoc! {r#"
+      .foo {
+        color: red;
+      }
+
+      .bar .foo {
+        color: #00f;
+      }
+      "#},
+    );
+
+    nesting_test(
+      r#"
+        .foo {
+          color: red;
+          .parent & {
+            color: blue;
+          }
+        }
+      "#,
+      indoc! {r#"
+        .foo {
+          color: red;
+        }
+
+        .parent .foo {
+          color: #00f;
+        }
+      "#},
+    );
+
+    nesting_test(
+      r#"
+        .foo {
+          color: red;
+          :not(&) {
+            color: blue;
+          }
+        }
+      "#,
+      indoc! {r#"
+        .foo {
+          color: red;
+        }
+
+        :not(.foo) {
+          color: #00f;
+        }
+      "#},
+    );
+
+    nesting_test(
+      r#"
+        .foo {
+          color: blue;
+          .bar & {
+            color: red;
+            &.baz {
+              color: green;
+            }
+          }
+        }
+      "#,
+      indoc! {r#"
+        .foo {
+          color: #00f;
+        }
+
+        .bar .foo {
+          color: red;
+        }
+
+        .bar .foo.baz {
+          color: green;
+        }
+      "#},
+    );
+
+    nesting_test(
+      r#"
+        .foo {
+          :not(&) {
+            color: red;
+          }
+
+          & h1 {
+            background: green;
+          }
+        }
+      "#,
+      indoc! {r#"
+        :not(.foo) {
+          color: red;
+        }
+
+        .foo h1 {
+          background: green;
+        }
+      "#},
+    );
+
+    nesting_test(
+      r#"
+        .foo {
+          & h1 {
+            background: green;
+          }
+
+          :not(&) {
+            color: red;
+          }
+        }
+      "#,
+      indoc! {r#"
+        .foo h1 {
+          background: green;
+        }
+
+        :not(.foo) {
+          color: red;
+        }
+      "#},
+    );
+
+    nesting_test(
+      r#"
+        .foo .bar {
+          :is(h1)& {
+            background: green;
+          }
+        }
+      "#,
+      indoc! {r#"
+        :is(h1):is(.foo .bar) {
+          background: green;
+        }
+      "#},
+    );
+
+    nesting_test(
+      r#"
+        @namespace "http://example.com/foo";
+        @namespace toto "http://toto.example.org";
+
+        div {
+          .foo& {
+            color: red;
+          }
+        }
+
+        * {
+          .foo& {
+            color: red;
+          }
+        }
+
+        |x {
+          .foo& {
+            color: red;
+          }
+        }
+
+        *|x {
+          .foo& {
+            color: red;
+          }
+        }
+
+        toto|x {
+          .foo& {
+            color: red;
+          }
+        }
+      "#,
+      indoc! {r#"
+        @namespace "http://example.com/foo";
+        @namespace toto "http://toto.example.org";
+
+        .foo:is(div) {
+          color: red;
+        }
+
+        .foo:is(*) {
+          color: red;
+        }
+
+        .foo:is(|x) {
+          color: red;
+        }
+
+        .foo:is(*|x) {
+          color: red;
+        }
+
+        .foo:is(toto|x) {
+          color: red;
+        }
+      "#},
+    );
+
+    nesting_test(
+      r#"
+        .foo .bar {
+          :is(h1) .baz& {
+            background: green;
+          }
+        }
+      "#,
+      indoc! {r#"
+        :is(h1) .baz:is(.foo .bar) {
+          background: green;
+        }
+      "#},
+    );
+
+    nesting_test(
+      r#"
+        .foo .bar {
+          .baz& {
+            background: green;
+          }
+        }
+      "#,
+      indoc! {r#"
+        .baz:is(.foo .bar) {
+          background: green;
+        }
+      "#},
+    );
+
+    nesting_test(
+      r#"
+        .foo .bar {
+          .baz & {
+            background: green;
+          }
+        }
+      "#,
+      indoc! {r#"
+        .baz :is(.foo .bar) {
+          background: green;
+        }
+      "#},
+    );
+
+    nesting_test(
+      r#"
+        .foo {
+          .bar {
+            color: blue;
+          }
+          color: red;
+        }
+      "#,
+      indoc! {r#"
+        .foo .bar {
+          color: #00f;
+        }
+
+        .foo {
+          color: red;
+        }
+      "#},
+    );
+
+    nesting_test(
+      r#"
+        article {
+          color: green;
+          & { color: blue; }
+          color: red;
+        }
+      "#,
+      indoc! {r#"
+        article {
+          color: green;
+        }
+
+        article {
+          color: #00f;
+        }
+
+        article {
+          color: red;
+        }
+      "#},
+    );
+
+    nesting_test(
+      r#"
+        & .foo {
+          color: red;
+        }
+      "#,
+      indoc! {r#"
+        :scope .foo {
+          color: red;
+        }
+      "#},
+    );
+
+    nesting_test(
+      r#"
+        &.foo {
+          color: red;
+        }
+      "#,
+      indoc! {r#"
+        :scope.foo {
+          color: red;
+        }
+      "#},
+    );
+
+    nesting_test(
+      r#"
+        .foo& {
+          color: red;
+        }
+      "#,
+      indoc! {r#"
+        .foo:scope {
+          color: red;
+        }
+      "#},
+    );
+
+    nesting_test(
+      r#"
+        &html {
+          color: red;
+        }
+      "#,
+      indoc! {r#"
+        html:scope {
+          color: red;
+        }
+      "#},
+    );
+
+    nesting_test(
+      r#"
+        .foo {
+          color: blue;
+          div {
+            color: red;
+          }
+        }
+      "#,
+      indoc! {r#"
+        .foo {
+          color: #00f;
+        }
+
+        .foo div {
+          color: red;
+        }
+      "#},
+    );
+
+    nesting_test(
+      r#"
+        div {
+          color: blue;
+
+          button:focus {
+            color: red;
+          }
+        }
+      "#,
+      indoc! {r#"
+        div {
+          color: #00f;
+        }
+
+        div button:focus {
+          color: red;
+        }
+      "#},
+    );
+    nesting_test(
+      r#"
+        div {
+          color: blue;
+
+          --button:focus {
+            color: red;
+          }
+        }
+      "#,
+      indoc! {r#"
+        div {
+          color: #00f;
+          --button: focus { color: red; };
+        }
+      "#},
+    );
+    nesting_test(
+      r#"
+      .foo {
+        &::before, &::after {
+          background: blue;
+          @media screen {
+            background: orange;
+          }
+        }
+      }
+      "#,
+      indoc! {r#"
+      .foo:before, .foo:after {
+        background: #00f;
+      }
+
+      @media screen {
+        .foo:before, .foo:after {
+          background: orange;
+        }
+      }
+      "#},
+    );
+
+    nesting_test_no_targets(
+      r#"
+        .foo {
+          color: blue;
+          @nest .bar & {
+            color: red;
+            &.baz {
+              color: green;
+            }
+          }
+        }
+      "#,
+      indoc! {r#"
+        .foo {
+          color: #00f;
+
+          @nest .bar & {
+            color: red;
+
+            &.baz {
+              color: green;
+            }
+          }
+        }
+      "#},
+    );
+
+    nesting_test_no_targets(
+      r#"
+        .foo {
+          color: blue;
+          &div {
+            color: red;
+          }
+
+          &span {
+            color: purple;
+          }
+        }
+      "#,
+      indoc! {r#"
+        .foo {
+          color: #00f;
+
+          &div {
+            color: red;
+          }
+
+          &span {
+            color: purple;
+          }
+        }
+      "#},
+    );
+
+    nesting_test_no_targets(
+      r#"
+        .error, .invalid {
+          &:hover > .baz { color: red; }
+        }
+      "#,
+      indoc! {r#"
+        .error, .invalid {
+          &:hover > .baz {
+            color: red;
+          }
+        }
+      "#},
+    );
+
+    nesting_test_with_targets(
+      r#"
+        .foo {
+          color: blue;
+          & > .bar { color: red; }
+        }
+      "#,
+      indoc! {r#"
+        .foo {
+          color: #00f;
+        }
+
+        .foo > .bar {
+          color: red;
+        }
+      "#},
+      Targets {
+        browsers: Some(Browsers {
+          chrome: Some(112 << 16),
+          ..Browsers::default()
+        }),
+        include: Features::Nesting,
+        exclude: Features::empty(),
+      },
+    );
+    nesting_test_with_targets(
+      r#"
+        .foo {
+          color: blue;
+          & > .bar { color: red; }
+        }
+      "#,
+      indoc! {r#"
+        .foo {
+          color: #00f;
+
+          & > .bar {
+            color: red;
+          }
+        }
+      "#},
+      Targets {
+        browsers: Some(Browsers {
+          chrome: Some(50 << 16),
+          ..Browsers::default()
+        }),
+        include: Features::empty(),
+        exclude: Features::Nesting,
+      },
+    );
+
+    let mut stylesheet = StyleSheet::parse(
+      r#"
+      .foo {
+        color: blue;
+        .bar {
+          color: red;
+        }
+      }
+      "#,
+      ParserOptions::default(),
+    )
+    .unwrap();
+    stylesheet.minify(MinifyOptions::default()).unwrap();
+    let res = stylesheet
+      .to_css(PrinterOptions {
+        minify: true,
+        ..PrinterOptions::default()
+      })
+      .unwrap();
+    assert_eq!(res.code, ".foo{color:#00f;& .bar{color:red}}");
+
+    nesting_test_with_targets(
+      r#"
+        .a {
+          &.b,
+          &.c {
+            &.d {
+              color: red;
+            }
+          }
+        }
+      "#,
+      indoc! {r#"
+        .a.b.d {
+          color: red;
+        }
+
+        .a.c.d {
+          color: red;
+        }
+      "#},
+      Targets {
+        browsers: Some(Browsers {
+          safari: Some(13 << 16),
+          ..Browsers::default()
+        }),
+        include: Features::Nesting,
+        exclude: Features::empty(),
+      },
+    );
+  }
+
+  #[test]
+  fn test_nesting_error_recovery() {
+    error_recovery_test(
+      "
+    .container {
+      padding: 3rem;
+      @media (max-width: --styled-jsx-placeholder-0__) {
+        .responsive {
+          color: purple;
+        }
+      }
+    }
+    ",
+    );
+  }
+
+  #[test]
+  fn test_css_variable_error_recovery() {
+    error_recovery_test("
+    .container {
+      --local-var: --styled-jsx-placeholder-0__;
+      color: var(--text-color);
+      background: linear-gradient(to right, --styled-jsx-placeholder-1__, --styled-jsx-placeholder-2__);
+
+      .item {
+        transform: translate(calc(var(--x) + --styled-jsx-placeholder-3__px), calc(var(--y) + --styled-jsx-placeholder-4__px));
+      }
+
+      div {
+        margin: calc(10px + --styled-jsx-placeholder-5__px);
+      }
+    }
+  ");
+  }
+
+  #[test]
+  fn test_css_modules() {
+    css_modules_test(
+      r#"
+      .foo {
+        color: red;
+      }
+
+      #id {
+        animation: 2s test;
+      }
+
+      @keyframes test {
+        from { color: red }
+        to { color: yellow }
+      }
+
+      @counter-style circles {
+        symbols: Ⓐ Ⓑ Ⓒ;
+      }
+
+      ul {
+        list-style: circles;
+      }
+
+      ol {
+        list-style-type: none;
+      }
+
+      li {
+        list-style-type: disc;
+      }
+
+      @keyframes fade {
+        from { opacity: 0 }
+        to { opacity: 1 }
+      }
+    "#,
+      indoc! {r#"
+      .EgL3uq_foo {
+        color: red;
+      }
+
+      #EgL3uq_id {
+        animation: 2s EgL3uq_test;
+      }
+
+      @keyframes EgL3uq_test {
+        from {
+          color: red;
+        }
+
+        to {
+          color: #ff0;
+        }
+      }
+
+      @counter-style EgL3uq_circles {
+        symbols: Ⓐ Ⓑ Ⓒ;
+      }
+
+      ul {
+        list-style: EgL3uq_circles;
+      }
+
+      ol {
+        list-style-type: none;
+      }
+
+      li {
+        list-style-type: disc;
+      }
+
+      @keyframes EgL3uq_fade {
+        from {
+          opacity: 0;
+        }
+
+        to {
+          opacity: 1;
+        }
+      }
+    "#},
+      map! {
+        "foo" => "EgL3uq_foo",
+        "id" => "EgL3uq_id",
+        "test" => "EgL3uq_test" referenced: true,
+        "circles" => "EgL3uq_circles" referenced: true,
+        "fade" => "EgL3uq_fade"
+      },
+      HashMap::new(),
+      Default::default(),
+      false,
+    );
+
+    css_modules_test(
+      r#"
+      .foo {
+        color: red;
+      }
+
+      #id {
+        animation: 2s test;
+      }
+
+      @keyframes test {
+        from { color: red }
+        to { color: yellow }
+      }
+    "#,
+      indoc! {r#"
+      .EgL3uq_foo {
+        color: red;
+      }
+
+      #EgL3uq_id {
+        animation: 2s test;
+      }
+
+      @keyframes test {
+        from {
+          color: red;
+        }
+
+        to {
+          color: #ff0;
+        }
+      }
+    "#},
+      map! {
+        "foo" => "EgL3uq_foo",
+        "id" => "EgL3uq_id"
+      },
+      HashMap::new(),
+      crate::css_modules::Config {
+        animation: false,
+        // custom_idents: false,
+        ..Default::default()
+      },
+      false,
+    );
+
+    css_modules_test(
+      r#"
+      @counter-style circles {
+        symbols: Ⓐ Ⓑ Ⓒ;
+      }
+
+      ul {
+        list-style: circles;
+      }
+
+      ol {
+        list-style-type: none;
+      }
+
+      li {
+        list-style-type: disc;
+      }
+    "#,
+      indoc! {r#"
+      @counter-style circles {
+        symbols: Ⓐ Ⓑ Ⓒ;
+      }
+
+      ul {
+        list-style: circles;
+      }
+
+      ol {
+        list-style-type: none;
+      }
+
+      li {
+        list-style-type: disc;
+      }
+    "#},
+      map! {
+        "circles" => "EgL3uq_circles" referenced: true
+      },
+      HashMap::new(),
+      crate::css_modules::Config {
+        custom_idents: false,
+        ..Default::default()
+      },
+      false,
+    );
+
+    css_modules_test(
+      r#"
+      body {
+        grid: [header-top] "a a a" [header-bottom]
+              [main-top] "b b b" 1fr [main-bottom]
+              / auto 1fr auto;
+      }
+
+      header {
+        grid-area: a;
+      }
+
+      main {
+        grid-row: main-top / main-bottom;
+      }
+    "#,
+      indoc! {r#"
+      body {
+        grid: [EgL3uq_header-top] "EgL3uq_a EgL3uq_a EgL3uq_a" [EgL3uq_header-bottom]
+              [EgL3uq_main-top] "EgL3uq_b EgL3uq_b EgL3uq_b" 1fr [EgL3uq_main-bottom]
+              / auto 1fr auto;
+      }
+
+      header {
+        grid-area: EgL3uq_a;
+      }
+
+      main {
+        grid-row: EgL3uq_main-top / EgL3uq_main-bottom;
+      }
+    "#},
+      map! {
+        "header-top" => "EgL3uq_header-top",
+        "header-bottom" => "EgL3uq_header-bottom",
+        "main-top" => "EgL3uq_main-top",
+        "main-bottom" => "EgL3uq_main-bottom",
+        "a" => "EgL3uq_a",
+        "b" => "EgL3uq_b"
+      },
+      HashMap::new(),
+      Default::default(),
+      false,
+    );
+
+    css_modules_test(
+      r#"
+        .grid {
+          grid-template-areas: "foo";
+        }
+
+        .foo {
+          grid-area: foo;
+        }
+
+        .bar {
+          grid-column-start: foo-start;
+        }
+      "#,
+      indoc! {r#"
+        .EgL3uq_grid {
+          grid-template-areas: "EgL3uq_foo";
+        }
+
+        .EgL3uq_foo {
+          grid-area: EgL3uq_foo;
+        }
+
+        .EgL3uq_bar {
+          grid-column-start: EgL3uq_foo-start;
+        }
+      "#},
+      map! {
+        "foo" => "EgL3uq_foo",
+        "foo-start" => "EgL3uq_foo-start",
+        "grid" => "EgL3uq_grid",
+        "bar" => "EgL3uq_bar"
+      },
+      HashMap::new(),
+      Default::default(),
+      false,
+    );
+
+    css_modules_test(
+      r#"
+        .grid {
+          grid-template-areas: "foo";
+        }
+
+        .foo {
+          grid-area: foo;
+        }
+
+        .bar {
+          grid-column-start: foo-start;
+        }
+      "#,
+      indoc! {r#"
+        .EgL3uq_grid {
+          grid-template-areas: "foo";
+        }
+
+        .EgL3uq_foo {
+          grid-area: foo;
+        }
+
+        .EgL3uq_bar {
+          grid-column-start: foo-start;
+        }
+      "#},
+      map! {
+        "foo" => "EgL3uq_foo",
+        "grid" => "EgL3uq_grid",
+        "bar" => "EgL3uq_bar"
+      },
+      HashMap::new(),
+      crate::css_modules::Config {
+        grid: false,
+        ..Default::default()
+      },
+      false,
+    );
+
+    css_modules_test(
+      r#"
+      test {
+        transition-property: opacity;
+      }
+    "#,
+      indoc! {r#"
+      test {
+        transition-property: opacity;
+      }
+    "#},
+      map! {},
+      HashMap::new(),
+      Default::default(),
+      false,
+    );
+
+    css_modules_test(
+      r#"
+      :global(.foo) {
+        color: red;
+      }
+
+      :local(.bar) {
+        color: yellow;
+      }
+
+      .bar :global(.baz) {
+        color: purple;
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        color: red;
+      }
+
+      .EgL3uq_bar {
+        color: #ff0;
+      }
+
+      .EgL3uq_bar .baz {
+        color: purple;
+      }
+    "#},
+      map! {
+        "bar" => "EgL3uq_bar"
+      },
+      HashMap::new(),
+      Default::default(),
+      false,
+    );
+
+    // :global(:local(.hi)) {
+    //   color: green;
+    // }
+
+    css_modules_test(
+      r#"
+      .test {
+        composes: foo;
+        background: white;
+      }
+
+      .foo {
+        color: red;
+      }
+    "#,
+      indoc! {r#"
+      .EgL3uq_test {
+        background: #fff;
+      }
+
+      .EgL3uq_foo {
+        color: red;
+      }
+    "#},
+      map! {
+        "test" => "EgL3uq_test" "EgL3uq_foo",
+        "foo" => "EgL3uq_foo"
+      },
+      HashMap::new(),
+      Default::default(),
+      false,
+    );
+
+    css_modules_test(
+      r#"
+      .a, .b {
+        composes: foo;
+        background: white;
+      }
+
+      .foo {
+        color: red;
+      }
+    "#,
+      indoc! {r#"
+      .EgL3uq_a, .EgL3uq_b {
+        background: #fff;
+      }
+
+      .EgL3uq_foo {
+        color: red;
+      }
+    "#},
+      map! {
+        "a" => "EgL3uq_a" "EgL3uq_foo",
+        "b" => "EgL3uq_b" "EgL3uq_foo",
+        "foo" => "EgL3uq_foo"
+      },
+      HashMap::new(),
+      Default::default(),
+      false,
+    );
+
+    css_modules_test(
+      r#"
+      .test {
+        composes: foo bar;
+        background: white;
+      }
+
+      .foo {
+        color: red;
+      }
+
+      .bar {
+        color: yellow;
+      }
+    "#,
+      indoc! {r#"
+      .EgL3uq_test {
+        background: #fff;
+      }
+
+      .EgL3uq_foo {
+        color: red;
+      }
+
+      .EgL3uq_bar {
+        color: #ff0;
+      }
+    "#},
+      map! {
+        "test" => "EgL3uq_test" "EgL3uq_foo" "EgL3uq_bar",
+        "foo" => "EgL3uq_foo",
+        "bar" => "EgL3uq_bar"
+      },
+      HashMap::new(),
+      Default::default(),
+      false,
+    );
+
+    css_modules_test(
+      r#"
+      .test {
+        composes: foo from global;
+        background: white;
+      }
+    "#,
+      indoc! {r#"
+      .EgL3uq_test {
+        background: #fff;
+      }
+    "#},
+      map! {
+        "test" => "EgL3uq_test" "foo" global: true
+      },
+      HashMap::new(),
+      Default::default(),
+      false,
+    );
+
+    css_modules_test(
+      r#"
+      .test {
+        composes: foo bar from global;
+        background: white;
+      }
+    "#,
+      indoc! {r#"
+      .EgL3uq_test {
+        background: #fff;
+      }
+    "#},
+      map! {
+        "test" => "EgL3uq_test" "foo" global: true "bar" global: true
+      },
+      HashMap::new(),
+      Default::default(),
+      false,
+    );
+
+    css_modules_test(
+      r#"
+      .test {
+        composes: foo from "foo.css";
+        background: white;
+      }
+    "#,
+      indoc! {r#"
+      .EgL3uq_test {
+        background: #fff;
+      }
+    "#},
+      map! {
+        "test" => "EgL3uq_test" "foo" from "foo.css"
+      },
+      HashMap::new(),
+      Default::default(),
+      false,
+    );
+
+    css_modules_test(
+      r#"
+      .test {
+        composes: foo bar from "foo.css";
+        background: white;
+      }
+    "#,
+      indoc! {r#"
+      .EgL3uq_test {
+        background: #fff;
+      }
+    "#},
+      map! {
+        "test" => "EgL3uq_test" "foo" from "foo.css" "bar" from "foo.css"
+      },
+      HashMap::new(),
+      Default::default(),
+      false,
+    );
+
+    css_modules_test(
+      r#"
+      .test {
+        composes: foo;
+        composes: foo from "foo.css";
+        composes: bar from "bar.css";
+        background: white;
+      }
+
+      .foo {
+        color: red;
+      }
+    "#,
+      indoc! {r#"
+      .EgL3uq_test {
+        background: #fff;
+      }
+
+      .EgL3uq_foo {
+        color: red;
+      }
+    "#},
+      map! {
+        "test" => "EgL3uq_test" "EgL3uq_foo" "foo" from "foo.css" "bar" from "bar.css",
+        "foo" => "EgL3uq_foo"
+      },
+      HashMap::new(),
+      Default::default(),
+      false,
+    );
+
+    css_modules_test(
+      r#"
+      .foo {
+        color: red;
+      }
+    "#,
+      indoc! {r#"
+      .test-EgL3uq-foo {
+        color: red;
+      }
+    "#},
+      map! {
+        "foo" => "test-EgL3uq-foo"
+      },
+      HashMap::new(),
+      crate::css_modules::Config {
+        pattern: crate::css_modules::Pattern::parse("test-[hash]-[local]").unwrap(),
+        ..Default::default()
+      },
+      false,
+    );
+
+    let stylesheet = StyleSheet::parse(
+      r#"
+        .grid {
+          grid-template-areas: "foo";
+        }
+
+        .foo {
+          grid-area: foo;
+        }
+
+        .bar {
+          grid-column-start: foo-start;
+        }
+      "#,
+      ParserOptions {
+        css_modules: Some(crate::css_modules::Config {
+          pattern: crate::css_modules::Pattern::parse("test-[local]-[hash]").unwrap(),
+          ..Default::default()
+        }),
+        ..ParserOptions::default()
+      },
+    )
+    .unwrap();
+    if let Err(err) = stylesheet.to_css(PrinterOptions::default()) {
+      assert_eq!(err.kind, PrinterErrorKind::InvalidCssModulesPatternInGrid);
+    } else {
+      unreachable!()
+    }
+
+    css_modules_test(
+      r#"
+      @property --foo {
+        syntax: '<color>';
+        inherits: false;
+        initial-value: yellow;
+      }
+
+      .foo {
+        --foo: red;
+        color: var(--foo);
+      }
+    "#,
+      indoc! {r#"
+      @property --foo {
+        syntax: "<color>";
+        inherits: false;
+        initial-value: #ff0;
+      }
+
+      .EgL3uq_foo {
+        --foo: red;
+        color: var(--foo);
+      }
+    "#},
+      map! {
+        "foo" => "EgL3uq_foo"
+      },
+      HashMap::new(),
+      Default::default(),
+      false,
+    );
+
+    css_modules_test(
+      r#"
+      @property --foo {
+        syntax: '<color>';
+        inherits: false;
+        initial-value: yellow;
+      }
+
+      @font-palette-values --Cooler {
+        font-family: Bixa;
+        base-palette: 1;
+        override-colors: 1 #7EB7E4;
+      }
+
+      .foo {
+        --foo: red;
+        --bar: green;
+        color: var(--foo);
+        font-palette: --Cooler;
+      }
+
+      .bar {
+        color: var(--color from "./b.css");
+      }
+    "#,
+      indoc! {r#"
+      @property --EgL3uq_foo {
+        syntax: "<color>";
+        inherits: false;
+        initial-value: #ff0;
+      }
+
+      @font-palette-values --EgL3uq_Cooler {
+        font-family: Bixa;
+        base-palette: 1;
+        override-colors: 1 #7eb7e4;
+      }
+
+      .EgL3uq_foo {
+        --EgL3uq_foo: red;
+        --EgL3uq_bar: green;
+        color: var(--EgL3uq_foo);
+        font-palette: --EgL3uq_Cooler;
+      }
+
+      .EgL3uq_bar {
+        color: var(--ma1CsG);
+      }
+    "#},
+      map! {
+        "foo" => "EgL3uq_foo",
+        "--foo" => "--EgL3uq_foo" referenced: true,
+        "--bar" => "--EgL3uq_bar",
+        "bar" => "EgL3uq_bar",
+        "--Cooler" => "--EgL3uq_Cooler" referenced: true
+      },
+      HashMap::from([(
+        "--ma1CsG".into(),
+        CssModuleReference::Dependency {
+          name: "--color".into(),
+          specifier: "./b.css".into(),
+        },
+      )]),
+      crate::css_modules::Config {
+        dashed_idents: true,
+        ..Default::default()
+      },
+      false,
+    );
+
+    css_modules_test(
+      r#"
+      .test {
+        animation: rotate var(--duration) linear infinite;
+      }
+    "#,
+      indoc! {r#"
+      .EgL3uq_test {
+        animation: EgL3uq_rotate var(--duration) linear infinite;
+      }
+    "#},
+      map! {
+        "test" => "EgL3uq_test",
+        "rotate" => "EgL3uq_rotate" referenced: true
+      },
+      HashMap::new(),
+      Default::default(),
+      false,
+    );
+    css_modules_test(
+      r#"
+      .test {
+        animation: none var(--duration);
+      }
+    "#,
+      indoc! {r#"
+      .EgL3uq_test {
+        animation: none var(--duration);
+      }
+    "#},
+      map! {
+        "test" => "EgL3uq_test"
+      },
+      HashMap::new(),
+      Default::default(),
+      false,
+    );
+    css_modules_test(
+      r#"
+      .test {
+        animation: var(--animation);
+      }
+    "#,
+      indoc! {r#"
+      .EgL3uq_test {
+        animation: var(--animation);
+      }
+    "#},
+      map! {
+        "test" => "EgL3uq_test"
+      },
+      HashMap::new(),
+      Default::default(),
+      false,
+    );
+    css_modules_test(
+      r#"
+      .test {
+        animation: rotate var(--duration);
+      }
+    "#,
+      indoc! {r#"
+      .EgL3uq_test {
+        animation: rotate var(--duration);
+      }
+    "#},
+      map! {
+        "test" => "EgL3uq_test"
+      },
+      HashMap::new(),
+      crate::css_modules::Config {
+        animation: false,
+        ..Default::default()
+      },
+      false,
+    );
+    css_modules_test(
+      r#"
+      .test {
+        animation: "rotate" var(--duration);
+      }
+    "#,
+      indoc! {r#"
+      .EgL3uq_test {
+        animation: EgL3uq_rotate var(--duration);
+      }
+    "#},
+      map! {
+        "test" => "EgL3uq_test",
+        "rotate" => "EgL3uq_rotate" referenced: true
+      },
+      HashMap::new(),
+      crate::css_modules::Config { ..Default::default() },
+      false,
+    );
+
+    css_modules_test(
+      r#"
+      .test {
+        composes: foo bar from "foo.css";
+        background: white;
+      }
+    "#,
+      indoc! {r#"
+      ._5h2kwG-test {
+        background: #fff;
+      }
+    "#},
+      map! {
+        "test" => "_5h2kwG-test" "foo" from "foo.css" "bar" from "foo.css"
+      },
+      HashMap::new(),
+      crate::css_modules::Config {
+        pattern: crate::css_modules::Pattern::parse("[content-hash]-[local]").unwrap(),
+        ..Default::default()
+      },
+      false,
+    );
+
+    css_modules_test(
+      r#"
+      .box2 {
+        @container main (width >= 0) {
+          background-color: #90ee90;
+        }
+      }
+    "#,
+      indoc! {r#"
+      .EgL3uq_box2 {
+        @container EgL3uq_main (width >= 0) {
+          background-color: #90ee90;
+        }
+      }
+    "#},
+      map! {
+        "main" => "EgL3uq_main",
+        "box2" => "EgL3uq_box2"
+      },
+      HashMap::new(),
+      crate::css_modules::Config { ..Default::default() },
+      false,
+    );
+
+    css_modules_test(
+      r#"
+      .box2 {
+        @container main (width >= 0) {
+          background-color: #90ee90;
+        }
+      }
+    "#,
+      indoc! {r#"
+      .EgL3uq_box2 {
+        @container main (width >= 0) {
+          background-color: #90ee90;
+        }
+      }
+    "#},
+      map! {
+        "box2" => "EgL3uq_box2"
+      },
+      HashMap::new(),
+      crate::css_modules::Config {
+        container: false,
+        ..Default::default()
+      },
+      false,
+    );
+
+    css_modules_test(
+      ".foo { view-transition-name: bar }",
+      ".EgL3uq_foo{view-transition-name:EgL3uq_bar}",
+      map! {
+        "foo" => "EgL3uq_foo",
+        "bar" => "EgL3uq_bar"
+      },
+      HashMap::new(),
+      Default::default(),
+      true,
+    );
+    css_modules_test(
+      ".foo { view-transition-name: none }",
+      ".EgL3uq_foo{view-transition-name:none}",
+      map! {
+        "foo" => "EgL3uq_foo"
+      },
+      HashMap::new(),
+      Default::default(),
+      true,
+    );
+    css_modules_test(
+      ".foo { view-transition-name: auto }",
+      ".EgL3uq_foo{view-transition-name:auto}",
+      map! {
+        "foo" => "EgL3uq_foo"
+      },
+      HashMap::new(),
+      Default::default(),
+      true,
+    );
+
+    css_modules_test(
+      ".foo { view-transition-class: bar baz qux }",
+      ".EgL3uq_foo{view-transition-class:EgL3uq_bar EgL3uq_baz EgL3uq_qux}",
+      map! {
+        "foo" => "EgL3uq_foo",
+        "bar" => "EgL3uq_bar",
+        "baz" => "EgL3uq_baz",
+        "qux" => "EgL3uq_qux"
+      },
+      HashMap::new(),
+      Default::default(),
+      true,
+    );
+
+    css_modules_test(
+      ".foo { view-transition-group: contain }",
+      ".EgL3uq_foo{view-transition-group:contain}",
+      map! {
+        "foo" => "EgL3uq_foo"
+      },
+      HashMap::new(),
+      Default::default(),
+      true,
+    );
+    css_modules_test(
+      ".foo { view-transition-group: bar }",
+      ".EgL3uq_foo{view-transition-group:EgL3uq_bar}",
+      map! {
+        "foo" => "EgL3uq_foo",
+        "bar" => "EgL3uq_bar"
+      },
+      HashMap::new(),
+      Default::default(),
+      true,
+    );
+
+    css_modules_test(
+      "@view-transition { types: foo bar baz }",
+      "@view-transition{types:EgL3uq_foo EgL3uq_bar EgL3uq_baz}",
+      map! {
+        "foo" => "EgL3uq_foo",
+        "bar" => "EgL3uq_bar",
+        "baz" => "EgL3uq_baz"
+      },
+      HashMap::new(),
+      Default::default(),
+      true,
+    );
+
+    css_modules_test(
+      ":root:active-view-transition-type(foo, bar) { color: red }",
+      ":root:active-view-transition-type(EgL3uq_foo,EgL3uq_bar){color:red}",
+      map! {
+        "foo" => "EgL3uq_foo",
+        "bar" => "EgL3uq_bar"
+      },
+      HashMap::new(),
+      Default::default(),
+      true,
+    );
+
+    for name in &[
+      "view-transition-group",
+      "view-transition-image-pair",
+      "view-transition-new",
+      "view-transition-old",
+    ] {
+      css_modules_test(
+        &format!(":root::{}(foo) {{position: fixed}}", name),
+        &format!(":root::{}(EgL3uq_foo){{position:fixed}}", name),
+        map! {
+          "foo" => "EgL3uq_foo"
+        },
+        HashMap::new(),
+        Default::default(),
+        true,
+      );
+      css_modules_test(
+        &format!(":root::{}(.bar) {{position: fixed}}", name),
+        &format!(":root::{}(.EgL3uq_bar){{position:fixed}}", name),
+        map! {
+          "bar" => "EgL3uq_bar"
+        },
+        HashMap::new(),
+        Default::default(),
+        true,
+      );
+      css_modules_test(
+        &format!(":root::{}(foo.bar.baz) {{position: fixed}}", name),
+        &format!(":root::{}(EgL3uq_foo.EgL3uq_bar.EgL3uq_baz){{position:fixed}}", name),
+        map! {
+          "foo" => "EgL3uq_foo",
+          "bar" => "EgL3uq_bar",
+          "baz" => "EgL3uq_baz"
+        },
+        HashMap::new(),
+        Default::default(),
+        true,
+      );
+
+      css_modules_test(
+        ":nth-child(1 of .foo) {width: 20px}",
+        ":nth-child(1 of .EgL3uq_foo){width:20px}",
+        map! {
+          "foo" => "EgL3uq_foo"
+        },
+        HashMap::new(),
+        Default::default(),
+        true,
+      );
+      css_modules_test(
+        ":nth-last-child(1 of .foo) {width: 20px}",
+        ":nth-last-child(1 of .EgL3uq_foo){width:20px}",
+        map! {
+          "foo" => "EgL3uq_foo"
+        },
+        HashMap::new(),
+        Default::default(),
+        true,
+      );
+    }
+
+    // Stable hashes between project roots.
+    fn test_project_root(project_root: &str, filename: &str, hash: &str) {
+      let stylesheet = StyleSheet::parse(
+        r#"
+        .foo {
+          background: red;
+        }
+        "#,
+        ParserOptions {
+          filename: filename.into(),
+          css_modules: Some(Default::default()),
+          ..ParserOptions::default()
+        },
+      )
+      .unwrap();
+      let res = stylesheet
+        .to_css(PrinterOptions {
+          project_root: Some(project_root),
+          ..PrinterOptions::default()
+        })
+        .unwrap();
+      assert_eq!(
+        res.code,
+        format!(
+          indoc! {r#"
+      .{}_foo {{
+        background: red;
+      }}
+      "#},
+          hash
+        )
+      );
+    }
+
+    test_project_root("/foo/bar", "/foo/bar/test.css", "EgL3uq");
+    test_project_root("/foo", "/foo/test.css", "EgL3uq");
+    test_project_root("/foo/bar", "/foo/bar/baz/test.css", "xLEkNW");
+    test_project_root("/foo", "/foo/baz/test.css", "xLEkNW");
+
+    let mut stylesheet = StyleSheet::parse(
+      r#"
+      .foo {
+        color: red;
+        .bar {
+          color: green;
+        }
+        composes: test from "foo.css";
+      }
+      "#,
+      ParserOptions {
+        filename: "test.css".into(),
+        css_modules: Some(Default::default()),
+        ..ParserOptions::default()
+      },
+    )
+    .unwrap();
+    stylesheet.minify(MinifyOptions::default()).unwrap();
+    let res = stylesheet
+      .to_css(PrinterOptions {
+        targets: Browsers {
+          chrome: Some(95 << 16),
+          ..Browsers::default()
+        }
+        .into(),
+        ..Default::default()
+      })
+      .unwrap();
+    assert_eq!(
+      res.code,
+      indoc! {r#"
+    .EgL3uq_foo {
+      color: red;
+    }
+
+    .EgL3uq_foo .EgL3uq_bar {
+      color: green;
+    }
+
+
+    "#}
+    );
+    assert_eq!(
+      res.exports.unwrap(),
+      map! {
+        "foo" => "EgL3uq_foo" "test" from "foo.css",
+        "bar" => "EgL3uq_bar"
+      }
+    );
+  }
+
+  #[test]
+  fn test_pseudo_replacement() {
+    let source = r#"
+      .foo:hover {
+        color: red;
+      }
+
+      .foo:active {
+        color: yellow;
+      }
+
+      .foo:focus-visible {
+        color: purple;
+      }
+    "#;
+
+    let expected = indoc! { r#"
+      .foo.is-hovered {
+        color: red;
+      }
+
+      .foo.is-active {
+        color: #ff0;
+      }
+
+      .foo.focus-visible {
+        color: purple;
+      }
+    "#};
+
+    let stylesheet = StyleSheet::parse(&source, ParserOptions::default()).unwrap();
+    let res = stylesheet
+      .to_css(PrinterOptions {
+        pseudo_classes: Some(PseudoClasses {
+          hover: Some("is-hovered"),
+          active: Some("is-active"),
+          focus_visible: Some("focus-visible"),
+          ..PseudoClasses::default()
+        }),
+        ..PrinterOptions::default()
+      })
+      .unwrap();
+    assert_eq!(res.code, expected);
+
+    let source = r#"
+      .foo:hover {
+        color: red;
+      }
+    "#;
+
+    let expected = indoc! { r#"
+      .EgL3uq_foo.EgL3uq_is-hovered {
+        color: red;
+      }
+    "#};
+
+    let stylesheet = StyleSheet::parse(
+      &source,
+      ParserOptions {
+        filename: "test.css".into(),
+        css_modules: Some(Default::default()),
+        ..ParserOptions::default()
+      },
+    )
+    .unwrap();
+    let res = stylesheet
+      .to_css(PrinterOptions {
+        pseudo_classes: Some(PseudoClasses {
+          hover: Some("is-hovered"),
+          ..PseudoClasses::default()
+        }),
+        ..PrinterOptions::default()
+      })
+      .unwrap();
+    assert_eq!(res.code, expected);
+  }
+
+  #[test]
+  fn test_unused_symbols() {
+    let source = r#"
+      .foo {
+        color: red;
+      }
+
+      .bar {
+        color: green;
+      }
+
+      .bar:hover {
+        color: purple;
+      }
+
+      .bar .baz {
+        background: red;
+      }
+
+      .baz:is(.bar) {
+        background: green;
+      }
+
+      #id {
+        animation: 2s test;
+      }
+
+      #other_id {
+        color: red;
+      }
+
+      @keyframes test {
+        from { color: red }
+        to { color: yellow }
+      }
+
+      @counter-style circles {
+        symbols: Ⓐ Ⓑ Ⓒ;
+      }
+
+      @keyframes fade {
+        from { opacity: 0 }
+        to { opacity: 1 }
+      }
+    "#;
+
+    let expected = indoc! {r#"
+      .foo {
+        color: red;
+      }
+
+      #id {
+        animation: 2s test;
+      }
+
+      @keyframes test {
+        from {
+          color: red;
+        }
+
+        to {
+          color: #ff0;
+        }
+      }
+    "#};
+
+    let mut stylesheet = StyleSheet::parse(&source, ParserOptions::default()).unwrap();
+    stylesheet
+      .minify(MinifyOptions {
+        unused_symbols: vec!["bar", "other_id", "fade", "circles"]
+          .iter()
+          .map(|s| String::from(*s))
+          .collect(),
+        ..MinifyOptions::default()
+      })
+      .unwrap();
+    let res = stylesheet.to_css(PrinterOptions::default()).unwrap();
+    assert_eq!(res.code, expected);
+
+    let source = r#"
+      .foo {
+        color: red;
+
+        &.bar {
+          color: green;
+        }
+      }
+    "#;
+
+    let expected = indoc! {r#"
+      .foo {
+        color: red;
+      }
+    "#};
+
+    let mut stylesheet = StyleSheet::parse(&source, ParserOptions::default()).unwrap();
+    stylesheet
+      .minify(MinifyOptions {
+        unused_symbols: vec!["bar"].iter().map(|s| String::from(*s)).collect(),
+        ..MinifyOptions::default()
+      })
+      .unwrap();
+    let res = stylesheet.to_css(PrinterOptions::default()).unwrap();
+    assert_eq!(res.code, expected);
+
+    let source = r#"
+      .foo {
+        color: red;
+
+        &.bar {
+          color: purple;
+        }
+
+        @nest &.bar {
+          color: orange;
+        }
+
+        @nest :not(&) {
+          color: green;
+        }
+
+        @media (orientation: portrait) {
+          color: brown;
+        }
+      }
+
+      .x {
+        color: purple;
+
+        &.y {
+          color: green;
+        }
+      }
+    "#;
+
+    let expected = indoc! {r#"
+      :not(.foo) {
+        color: green;
+      }
+    "#};
+
+    let mut stylesheet = StyleSheet::parse(&source, ParserOptions::default()).unwrap();
+    stylesheet
+      .minify(MinifyOptions {
+        unused_symbols: vec!["foo", "x"].iter().map(|s| String::from(*s)).collect(),
+        ..MinifyOptions::default()
+      })
+      .unwrap();
+    let res = stylesheet
+      .to_css(PrinterOptions {
+        targets: Browsers {
+          chrome: Some(95 << 16),
+          ..Browsers::default()
+        }
+        .into(),
+        ..PrinterOptions::default()
+      })
+      .unwrap();
+    assert_eq!(res.code, expected);
+
+    let source = r#"
+      @property --EgL3uq_foo {
+        syntax: "<color>";
+        inherits: false;
+        initial-value: #ff0;
+      }
+
+      @font-palette-values --EgL3uq_Cooler {
+        font-family: Bixa;
+        base-palette: 1;
+        override-colors: 1 #7EB7E4;
+      }
+
+      .EgL3uq_foo {
+        --EgL3uq_foo: red;
+      }
+
+      .EgL3uq_bar {
+        color: green;
+      }
+    "#;
+
+    let expected = indoc! {r#"
+      .EgL3uq_bar {
+        color: green;
+      }
+    "#};
+
+    let mut stylesheet = StyleSheet::parse(&source, ParserOptions::default()).unwrap();
+    stylesheet
+      .minify(MinifyOptions {
+        unused_symbols: vec!["--EgL3uq_foo", "--EgL3uq_Cooler"]
+          .iter()
+          .map(|s| String::from(*s))
+          .collect(),
+        ..MinifyOptions::default()
+      })
+      .unwrap();
+    let res = stylesheet.to_css(PrinterOptions::default()).unwrap();
+    assert_eq!(res.code, expected);
+  }
+
+  #[test]
+  fn test_svg() {
+    minify_test(".foo { fill: yellow; }", ".foo{fill:#ff0}");
+    minify_test(".foo { fill: url(#foo); }", ".foo{fill:url(#foo)}");
+    minify_test(".foo { fill: url(#foo) none; }", ".foo{fill:url(#foo) none}");
+    minify_test(".foo { fill: url(#foo) yellow; }", ".foo{fill:url(#foo) #ff0}");
+    minify_test(".foo { fill: none; }", ".foo{fill:none}");
+    minify_test(".foo { fill: context-fill; }", ".foo{fill:context-fill}");
+    minify_test(".foo { fill: context-stroke; }", ".foo{fill:context-stroke}");
+
+    minify_test(".foo { stroke: yellow; }", ".foo{stroke:#ff0}");
+    minify_test(".foo { stroke: url(#foo); }", ".foo{stroke:url(#foo)}");
+    minify_test(".foo { stroke: url(#foo) none; }", ".foo{stroke:url(#foo) none}");
+    minify_test(".foo { stroke: url(#foo) yellow; }", ".foo{stroke:url(#foo) #ff0}");
+    minify_test(".foo { stroke: none; }", ".foo{stroke:none}");
+    minify_test(".foo { stroke: context-fill; }", ".foo{stroke:context-fill}");
+    minify_test(".foo { stroke: context-stroke; }", ".foo{stroke:context-stroke}");
+
+    minify_test(".foo { marker-start: url(#foo); }", ".foo{marker-start:url(#foo)}");
+
+    minify_test(".foo { stroke-dasharray: 4 1 2; }", ".foo{stroke-dasharray:4 1 2}");
+    minify_test(".foo { stroke-dasharray: 4,1,2; }", ".foo{stroke-dasharray:4 1 2}");
+    minify_test(".foo { stroke-dasharray: 4, 1, 2; }", ".foo{stroke-dasharray:4 1 2}");
+    minify_test(
+      ".foo { stroke-dasharray: 4px, 1px, 2px; }",
+      ".foo{stroke-dasharray:4 1 2}",
+    );
+
+    minify_test(".foo { mask: url('foo.svg'); }", ".foo{mask:url(foo.svg)}");
+    minify_test(
+      ".foo { mask: url(masks.svg#star) luminance }",
+      ".foo{mask:url(masks.svg#star) luminance}",
+    );
+    minify_test(
+      ".foo { mask: url(masks.svg#star) 40px 20px }",
+      ".foo{mask:url(masks.svg#star) 40px 20px}",
+    );
+    minify_test(
+      ".foo { mask: url(masks.svg#star) 0 0 / 50px 50px }",
+      ".foo{mask:url(masks.svg#star) 0 0/50px 50px}",
+    );
+    minify_test(
+      ".foo { mask: url(masks.svg#star) repeat-x }",
+      ".foo{mask:url(masks.svg#star) repeat-x}",
+    );
+    minify_test(
+      ".foo { mask: url(masks.svg#star) stroke-box }",
+      ".foo{mask:url(masks.svg#star) stroke-box}",
+    );
+    minify_test(
+      ".foo { mask: url(masks.svg#star) stroke-box stroke-box }",
+      ".foo{mask:url(masks.svg#star) stroke-box}",
+    );
+    minify_test(
+      ".foo { mask: url(masks.svg#star) border-box }",
+      ".foo{mask:url(masks.svg#star)}",
+    );
+    minify_test(
+      ".foo { mask: url(masks.svg#star) left / 16px repeat-y, url(masks.svg#circle) right / 16px repeat-y }",
+      ".foo{mask:url(masks.svg#star) 0/16px repeat-y,url(masks.svg#circle) 100%/16px repeat-y}",
+    );
+
+    minify_test(
+      ".foo { mask-border: url('border-mask.png') 25; }",
+      ".foo{mask-border:url(border-mask.png) 25}",
+    );
+    minify_test(
+      ".foo { mask-border: url('border-mask.png') 25 / 35px / 12px space alpha; }",
+      ".foo{mask-border:url(border-mask.png) 25/35px/12px space}",
+    );
+    minify_test(
+      ".foo { mask-border: url('border-mask.png') 25 / 35px / 12px space luminance; }",
+      ".foo{mask-border:url(border-mask.png) 25/35px/12px space luminance}",
+    );
+    minify_test(
+      ".foo { mask-border: url('border-mask.png') luminance 25 / 35px / 12px space; }",
+      ".foo{mask-border:url(border-mask.png) 25/35px/12px space luminance}",
+    );
+
+    minify_test(
+      ".foo { clip-path: url('clip.svg#star'); }",
+      ".foo{clip-path:url(clip.svg#star)}",
+    );
+    minify_test(".foo { clip-path: margin-box; }", ".foo{clip-path:margin-box}");
+    minify_test(
+      ".foo { clip-path: inset(100px 50px); }",
+      ".foo{clip-path:inset(100px 50px)}",
+    );
+    minify_test(
+      ".foo { clip-path: inset(100px 50px round 5px); }",
+      ".foo{clip-path:inset(100px 50px round 5px)}",
+    );
+    minify_test(
+      ".foo { clip-path: inset(100px 50px round 5px 5px 5px 5px); }",
+      ".foo{clip-path:inset(100px 50px round 5px)}",
+    );
+    minify_test(".foo { clip-path: circle(50px); }", ".foo{clip-path:circle(50px)}");
+    minify_test(
+      ".foo { clip-path: circle(50px at center center); }",
+      ".foo{clip-path:circle(50px)}",
+    );
+    minify_test(
+      ".foo { clip-path: circle(50px at 50% 50%); }",
+      ".foo{clip-path:circle(50px)}",
+    );
+    minify_test(
+      ".foo { clip-path: circle(50px at 0 100px); }",
+      ".foo{clip-path:circle(50px at 0 100px)}",
+    );
+    minify_test(
+      ".foo { clip-path: circle(closest-side at 0 100px); }",
+      ".foo{clip-path:circle(at 0 100px)}",
+    );
+    minify_test(
+      ".foo { clip-path: circle(farthest-side at 0 100px); }",
+      ".foo{clip-path:circle(farthest-side at 0 100px)}",
+    );
+    minify_test(
+      ".foo { clip-path: circle(closest-side at 50% 50%); }",
+      ".foo{clip-path:circle()}",
+    );
+    minify_test(
+      ".foo { clip-path: ellipse(50px 60px at 0 10% 20%); }",
+      ".foo{clip-path:ellipse(50px 60px at 0 10% 20%)}",
+    );
+    minify_test(
+      ".foo { clip-path: ellipse(50px 60px at center center); }",
+      ".foo{clip-path:ellipse(50px 60px)}",
+    );
+    minify_test(
+      ".foo { clip-path: ellipse(closest-side closest-side at 50% 50%); }",
+      ".foo{clip-path:ellipse()}",
+    );
+    minify_test(
+      ".foo { clip-path: ellipse(closest-side closest-side at 10% 20%); }",
+      ".foo{clip-path:ellipse(at 10% 20%)}",
+    );
+    minify_test(
+      ".foo { clip-path: ellipse(farthest-side closest-side at 10% 20%); }",
+      ".foo{clip-path:ellipse(farthest-side closest-side at 10% 20%)}",
+    );
+    minify_test(
+      ".foo { clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%); }",
+      ".foo{clip-path:polygon(50% 0%,100% 50%,50% 100%,0% 50%)}",
+    );
+    minify_test(
+      ".foo { clip-path: polygon(nonzero, 50% 0%, 100% 50%, 50% 100%, 0% 50%); }",
+      ".foo{clip-path:polygon(50% 0%,100% 50%,50% 100%,0% 50%)}",
+    );
+    minify_test(
+      ".foo { clip-path: polygon(evenodd, 50% 0%, 100% 50%, 50% 100%, 0% 50%); }",
+      ".foo{clip-path:polygon(evenodd,50% 0%,100% 50%,50% 100%,0% 50%)}",
+    );
+    minify_test(
+      ".foo { clip-path: padding-box circle(50px at 0 100px); }",
+      ".foo{clip-path:circle(50px at 0 100px) padding-box}",
+    );
+    minify_test(
+      ".foo { clip-path: circle(50px at 0 100px) padding-box; }",
+      ".foo{clip-path:circle(50px at 0 100px) padding-box}",
+    );
+    minify_test(
+      ".foo { clip-path: circle(50px at 0 100px) border-box; }",
+      ".foo{clip-path:circle(50px at 0 100px)}",
+    );
+
+    prefix_test(
+      ".foo { clip-path: circle(50px); }",
+      indoc! { r#"
+        .foo {
+          -webkit-clip-path: circle(50px);
+          clip-path: circle(50px);
+        }
+      "#},
+      Browsers {
+        chrome: Some(30 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { clip-path: circle(50px); }",
+      indoc! { r#"
+        .foo {
+          clip-path: circle(50px);
+        }
+      "#},
+      Browsers {
+        chrome: Some(80 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { clip-path: circle(50px); }",
+      indoc! { r#"
+        .foo {
+          -webkit-clip-path: circle(50px);
+          clip-path: circle(50px);
+        }
+      "#},
+      Browsers {
+        safari: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { clip-path: circle(50px); }",
+      indoc! { r#"
+        .foo {
+          clip-path: circle(50px);
+        }
+      "#},
+      Browsers {
+        safari: Some(14 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { fill: lch(50.998% 135.363 338) }",
+      indoc! { r#"
+        .foo {
+          fill: #ee00be;
+          fill: color(display-p3 .972962 -.362078 .804206);
+          fill: lch(50.998% 135.363 338);
+        }
+      "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        safari: Some(14 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { stroke: lch(50.998% 135.363 338) }",
+      indoc! { r#"
+        .foo {
+          stroke: #ee00be;
+          stroke: color(display-p3 .972962 -.362078 .804206);
+          stroke: lch(50.998% 135.363 338);
+        }
+      "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        safari: Some(14 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { fill: url(#foo) lch(50.998% 135.363 338) }",
+      indoc! { r##"
+        .foo {
+          fill: url("#foo") #ee00be;
+          fill: url("#foo") color(display-p3 .972962 -.362078 .804206);
+          fill: url("#foo") lch(50.998% 135.363 338);
+        }
+      "##},
+      Browsers {
+        chrome: Some(90 << 16),
+        safari: Some(14 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { fill: var(--url) lch(50.998% 135.363 338) }",
+      indoc! { r#"
+        .foo {
+          fill: var(--url) #ee00be;
+        }
+
+        @supports (color: lab(0% 0 0)) {
+          .foo {
+            fill: var(--url) lab(50.998% 125.506 -50.7078);
+          }
+        }
+      "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+        @supports (color: lab(0% 0 0)) {
+          .foo {
+            fill: var(--url) lab(50.998% 125.506 -50.7078);
+          }
+        }
+      "#,
+      indoc! { r#"
+        @supports (color: lab(0% 0 0)) {
+          .foo {
+            fill: var(--url) lab(50.998% 125.506 -50.7078);
+          }
+        }
+      "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { mask-image: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)) }",
+      indoc! { r#"
+        .foo {
+          -webkit-mask-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ff0f0e), to(#7773ff));
+          -webkit-mask-image: -webkit-linear-gradient(top, #ff0f0e, #7773ff);
+          -webkit-mask-image: linear-gradient(#ff0f0e, #7773ff);
+          mask-image: linear-gradient(#ff0f0e, #7773ff);
+          -webkit-mask-image: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364));
+          mask-image: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364));
+        }
+      "#},
+      Browsers {
+        chrome: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { mask-image: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)) }",
+      indoc! { r#"
+        .foo {
+          -webkit-mask-image: linear-gradient(#ff0f0e, #7773ff);
+          mask-image: linear-gradient(#ff0f0e, #7773ff);
+          -webkit-mask-image: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364));
+          mask-image: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364));
+        }
+      "#},
+      Browsers {
+        chrome: Some(95 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { mask-image: linear-gradient(red, green) }",
+      indoc! { r#"
+        .foo {
+          -webkit-mask-image: linear-gradient(red, green);
+          mask-image: linear-gradient(red, green);
+        }
+      "#},
+      Browsers {
+        chrome: Some(95 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { -webkit-mask-image: url(x.svg); mask-image: url(x.svg); }",
+      indoc! { r#"
+        .foo {
+          -webkit-mask-image: url("x.svg");
+          mask-image: url("x.svg");
+        }
+      "#},
+      Browsers {
+        chrome: Some(95 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { mask: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)) 40px 20px }",
+      indoc! { r#"
+        .foo {
+          -webkit-mask: -webkit-gradient(linear, 0 0, 0 100%, from(#ff0f0e), to(#7773ff)) 40px 20px;
+          -webkit-mask: -webkit-linear-gradient(top, #ff0f0e, #7773ff) 40px 20px;
+          -webkit-mask: linear-gradient(#ff0f0e, #7773ff) 40px 20px;
+          mask: linear-gradient(#ff0f0e, #7773ff) 40px 20px;
+          -webkit-mask: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)) 40px 20px;
+          mask: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)) 40px 20px;
+        }
+      "#},
+      Browsers {
+        chrome: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { mask: -webkit-linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)) 40px 20px }",
+      indoc! { r#"
+        .foo {
+          -webkit-mask: -webkit-gradient(linear, 0 0, 0 100%, from(#ff0f0e), to(#7773ff)) 40px 20px;
+          -webkit-mask: -webkit-linear-gradient(#ff0f0e, #7773ff) 40px 20px;
+        }
+      "#},
+      Browsers {
+        chrome: Some(8 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { mask: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)) 40px var(--foo) }",
+      indoc! { r#"
+        .foo {
+          -webkit-mask: linear-gradient(#ff0f0e, #7773ff) 40px var(--foo);
+          mask: linear-gradient(#ff0f0e, #7773ff) 40px var(--foo);
+        }
+
+        @supports (color: lab(0% 0 0)) {
+          .foo {
+            -webkit-mask: linear-gradient(lab(56.208% 94.4644 98.8928), lab(51% 70.4544 -115.586)) 40px var(--foo);
+            mask: linear-gradient(lab(56.208% 94.4644 98.8928), lab(51% 70.4544 -115.586)) 40px var(--foo);
+          }
+        }
+      "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+        @supports (color: lab(0% 0 0)) {
+          .foo {
+            mask: linear-gradient(lab(56.208% 94.4644 98.8928), lab(51% 70.4544 -115.586)) 40px var(--foo);
+          }
+        }
+      "#,
+      indoc! { r#"
+        @supports (color: lab(0% 0 0)) {
+          .foo {
+            -webkit-mask: linear-gradient(lab(56.208% 94.4644 98.8928), lab(51% 70.4544 -115.586)) 40px var(--foo);
+            mask: linear-gradient(lab(56.208% 94.4644 98.8928), lab(51% 70.4544 -115.586)) 40px var(--foo);
+          }
+        }
+      "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { mask: url(masks.svg#star) luminance }",
+      indoc! { r#"
+        .foo {
+          -webkit-mask: url("masks.svg#star");
+          -webkit-mask-source-type: luminance;
+          mask: url("masks.svg#star") luminance;
+        }
+    "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { mask-image: url(masks.svg#star) }",
+      indoc! { r#"
+        .foo {
+          -webkit-mask-image: url("masks.svg#star");
+          mask-image: url("masks.svg#star");
+        }
+    "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+        .foo {
+          mask-image: url(masks.svg#star);
+          mask-position: 25% 75%;
+          mask-size: cover;
+          mask-repeat: no-repeat;
+          mask-clip: padding-box;
+          mask-origin: content-box;
+          mask-composite: subtract;
+          mask-mode: luminance;
+        }
+      "#,
+      indoc! { r#"
+        .foo {
+          -webkit-mask: url("masks.svg#star") 25% 75% / cover no-repeat content-box padding-box;
+          -webkit-mask-composite: source-out;
+          -webkit-mask-source-type: luminance;
+          mask: url("masks.svg#star") 25% 75% / cover no-repeat content-box padding-box subtract luminance;
+        }
+    "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+        .foo {
+          mask-image: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364));
+          mask-position: 25% 75%;
+          mask-size: cover;
+          mask-repeat: no-repeat;
+          mask-clip: padding-box;
+          mask-origin: content-box;
+          mask-composite: subtract;
+          mask-mode: luminance;
+        }
+      "#,
+      indoc! { r#"
+        .foo {
+          -webkit-mask: linear-gradient(#ff0f0e, #7773ff) 25% 75% / cover no-repeat content-box padding-box;
+          -webkit-mask-composite: source-out;
+          -webkit-mask-source-type: luminance;
+          mask: linear-gradient(#ff0f0e, #7773ff) 25% 75% / cover no-repeat content-box padding-box subtract luminance;
+          -webkit-mask: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)) 25% 75% / cover no-repeat content-box padding-box;
+          -webkit-mask-composite: source-out;
+          -webkit-mask-source-type: luminance;
+          mask: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)) 25% 75% / cover no-repeat content-box padding-box subtract luminance;
+        }
+    "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    test(
+      r#"
+        .foo {
+          mask: none center / 100% no-repeat;
+          mask-image: var(--svg);
+        }
+      "#,
+      indoc! { r#"
+        .foo {
+          mask: none center / 100% no-repeat;
+          mask-image: var(--svg);
+        }
+      "#},
+    );
+
+    prefix_test(
+      r#"
+        .foo {
+          mask-composite: subtract;
+        }
+      "#,
+      indoc! { r#"
+        .foo {
+          -webkit-mask-composite: source-out;
+          mask-composite: subtract;
+        }
+    "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+        .foo {
+          mask-mode: luminance;
+        }
+      "#,
+      indoc! { r#"
+        .foo {
+          -webkit-mask-source-type: luminance;
+          mask-mode: luminance;
+        }
+    "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+        .foo {
+          mask-border: url('border-mask.png') 25 / 35px / 12px space luminance;
+        }
+      "#,
+      indoc! { r#"
+        .foo {
+          -webkit-mask-box-image: url("border-mask.png") 25 / 35px / 12px space;
+          mask-border: url("border-mask.png") 25 / 35px / 12px space luminance;
+        }
+    "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+        .foo {
+          mask-border: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)) 25 / 35px / 12px space luminance;
+        }
+      "#,
+      indoc! { r#"
+        .foo {
+          -webkit-mask-box-image: linear-gradient(#ff0f0e, #7773ff) 25 / 35px / 12px space;
+          mask-border: linear-gradient(#ff0f0e, #7773ff) 25 / 35px / 12px space luminance;
+          -webkit-mask-box-image: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)) 25 / 35px / 12px space;
+          mask-border: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)) 25 / 35px / 12px space luminance;
+        }
+    "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+        .foo {
+          mask-border-source: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364));
+        }
+      "#,
+      indoc! { r#"
+        .foo {
+          -webkit-mask-box-image-source: linear-gradient(#ff0f0e, #7773ff);
+          mask-border-source: linear-gradient(#ff0f0e, #7773ff);
+          -webkit-mask-box-image-source: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364));
+          mask-border-source: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364));
+        }
+    "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+        .foo {
+          mask-border-source: url(foo.png);
+          mask-border-slice: 10 40 10 40;
+          mask-border-width: 10px;
+          mask-border-outset: 0;
+          mask-border-repeat: round round;
+          mask-border-mode: luminance;
+        }
+      "#,
+      indoc! { r#"
+        .foo {
+          -webkit-mask-box-image: url("foo.png") 10 40 / 10px round;
+          mask-border: url("foo.png") 10 40 / 10px round luminance;
+        }
+    "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+        .foo {
+          -webkit-mask-box-image-source: url(foo.png);
+          -webkit-mask-box-image-slice: 10 40 10 40;
+          -webkit-mask-box-image-width: 10px;
+          -webkit-mask-box-image-outset: 0;
+          -webkit-mask-box-image-repeat: round round;
+        }
+      "#,
+      indoc! { r#"
+        .foo {
+          -webkit-mask-box-image: url("foo.png") 10 40 / 10px round;
+        }
+    "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+        .foo {
+          mask-border-slice: 10 40 10 40;
+        }
+      "#,
+      indoc! { r#"
+        .foo {
+          -webkit-mask-box-image-slice: 10 40;
+          mask-border-slice: 10 40;
+        }
+    "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+        .foo {
+          mask-border-slice: var(--foo);
+        }
+      "#,
+      indoc! { r#"
+        .foo {
+          -webkit-mask-box-image-slice: var(--foo);
+          mask-border-slice: var(--foo);
+        }
+    "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+        .foo {
+          mask-border: linear-gradient(lch(56.208% 136.76 46.312), lch(51% 135.366 301.364)) var(--foo);
+        }
+      "#,
+      indoc! { r#"
+        .foo {
+          -webkit-mask-box-image: linear-gradient(#ff0f0e, #7773ff) var(--foo);
+          mask-border: linear-gradient(#ff0f0e, #7773ff) var(--foo);
+        }
+
+        @supports (color: lab(0% 0 0)) {
+          .foo {
+            -webkit-mask-box-image: linear-gradient(lab(56.208% 94.4644 98.8928), lab(51% 70.4544 -115.586)) var(--foo);
+            mask-border: linear-gradient(lab(56.208% 94.4644 98.8928), lab(51% 70.4544 -115.586)) var(--foo);
+          }
+        }
+    "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+        .foo {
+          transition: mask 200ms;
+        }
+      "#,
+      indoc! { r#"
+        .foo {
+          transition: -webkit-mask .2s, mask .2s;
+        }
+    "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+        .foo {
+          transition: mask-border 200ms;
+        }
+      "#,
+      indoc! { r#"
+        .foo {
+          transition: -webkit-mask-box-image .2s, mask-border .2s;
+        }
+    "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+        .foo {
+          transition-property: mask;
+        }
+      "#,
+      indoc! { r#"
+        .foo {
+          transition-property: -webkit-mask, mask;
+        }
+    "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+        .foo {
+          transition-property: mask-border;
+        }
+      "#,
+      indoc! { r#"
+        .foo {
+          transition-property: -webkit-mask-box-image, mask-border;
+        }
+    "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+        .foo {
+          transition-property: mask-composite, mask-mode;
+        }
+      "#,
+      indoc! { r#"
+        .foo {
+          transition-property: -webkit-mask-composite, mask-composite, -webkit-mask-source-type, mask-mode;
+        }
+    "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+  }
+
+  #[test]
+  fn test_filter() {
+    minify_test(
+      ".foo { filter: url('filters.svg#filter-id'); }",
+      ".foo{filter:url(filters.svg#filter-id)}",
+    );
+    minify_test(".foo { filter: blur(5px); }", ".foo{filter:blur(5px)}");
+    minify_test(".foo { filter: blur(0px); }", ".foo{filter:blur()}");
+    minify_test(".foo { filter: brightness(10%); }", ".foo{filter:brightness(10%)}");
+    minify_test(".foo { filter: brightness(100%); }", ".foo{filter:brightness()}");
+    minify_test(
+      ".foo { filter: drop-shadow(16px 16px 20px yellow); }",
+      ".foo{filter:drop-shadow(16px 16px 20px #ff0)}",
+    );
+    minify_test(
+      ".foo { filter: contrast(175%) brightness(3%); }",
+      ".foo{filter:contrast(175%)brightness(3%)}",
+    );
+    minify_test(".foo { filter: hue-rotate(0) }", ".foo{filter:hue-rotate()}");
+
+    prefix_test(
+      ".foo { filter: blur(5px) }",
+      indoc! { r#"
+        .foo {
+          -webkit-filter: blur(5px);
+          filter: blur(5px);
+        }
+      "#},
+      Browsers {
+        chrome: Some(20 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { filter: blur(5px) }",
+      indoc! { r#"
+        .foo {
+          filter: blur(5px);
+        }
+      "#},
+      Browsers {
+        chrome: Some(80 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { backdrop-filter: blur(5px) }",
+      indoc! { r#"
+        .foo {
+          backdrop-filter: blur(5px);
+        }
+      "#},
+      Browsers {
+        chrome: Some(80 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { backdrop-filter: blur(5px) }",
+      indoc! { r#"
+        .foo {
+          -webkit-backdrop-filter: blur(5px);
+          backdrop-filter: blur(5px);
+        }
+      "#},
+      Browsers {
+        safari: Some(15 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        -webkit-backdrop-filter: blur(8px);
+        backdrop-filter: blur(8px);
+      }
+      "#,
+      indoc! {r#"
+      .foo {
+        -webkit-backdrop-filter: blur(8px);
+        backdrop-filter: blur(8px);
+      }
+      "#},
+      Browsers {
+        safari: Some(16 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { filter: var(--foo) }",
+      indoc! { r#"
+        .foo {
+          -webkit-filter: var(--foo);
+          filter: var(--foo);
+        }
+      "#},
+      Browsers {
+        chrome: Some(20 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { filter: drop-shadow(16px 16px 20px lab(40% 56.6 39)) }",
+      indoc! { r#"
+        .foo {
+          -webkit-filter: drop-shadow(16px 16px 20px #b32323);
+          filter: drop-shadow(16px 16px 20px #b32323);
+          filter: drop-shadow(16px 16px 20px lab(40% 56.6 39));
+        }
+      "#},
+      Browsers {
+        chrome: Some(20 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { filter: contrast(175%) drop-shadow(16px 16px 20px lab(40% 56.6 39)) }",
+      indoc! { r#"
+        .foo {
+          filter: contrast(175%) drop-shadow(16px 16px 20px #b32323);
+          filter: contrast(175%) drop-shadow(16px 16px 20px lab(40% 56.6 39));
+        }
+      "#},
+      Browsers {
+        chrome: Some(4 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { filter: drop-shadow(16px 16px 20px lab(40% 56.6 39)) drop-shadow(16px 16px 20px yellow) }",
+      indoc! { r#"
+        .foo {
+          filter: drop-shadow(16px 16px 20px #b32323) drop-shadow(16px 16px 20px #ff0);
+          filter: drop-shadow(16px 16px 20px lab(40% 56.6 39)) drop-shadow(16px 16px 20px #ff0);
+        }
+      "#},
+      Browsers {
+        chrome: Some(4 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { filter: var(--foo) drop-shadow(16px 16px 20px lab(40% 56.6 39)) }",
+      indoc! { r#"
+        .foo {
+          filter: var(--foo) drop-shadow(16px 16px 20px #b32323);
+        }
+
+        @supports (color: lab(0% 0 0)) {
+          .foo {
+            filter: var(--foo) drop-shadow(16px 16px 20px lab(40% 56.6 39));
+          }
+        }
+      "#},
+      Browsers {
+        chrome: Some(4 << 16),
+        ..Browsers::default()
+      },
+    );
+  }
+
+  #[test]
+  fn test_viewport() {
+    minify_test(
+      r#"
+    @viewport {
+      width: 100vw;
+    }"#,
+      "@viewport{width:100vw}",
+    );
+    minify_test(
+      r#"
+    @-ms-viewport {
+      width: device-width;
+    }"#,
+      "@-ms-viewport{width:device-width}",
+    );
+  }
+
+  #[test]
+  fn test_at_scope() {
+    minify_test(
+      r#"
+      @scope {
+        .foo {
+          display: flex;
+        }
+      }
+      "#,
+      "@scope{.foo{display:flex}}",
+    );
+    minify_test(
+      r#"
+      @scope {
+        :scope {
+          display: flex;
+          color: lightblue;
+        }
+      }"#,
+      "@scope{:scope{color:#add8e6;display:flex}}",
+    );
+    minify_test(
+      r#"
+      @scope (.light-scheme) {
+        a { color: yellow; }
+      }
+      "#,
+      "@scope(.light-scheme){a{color:#ff0}}",
+    );
+    minify_test(
+      r#"
+      @scope (.media-object) to (.content > *) {
+        a { color: yellow; }
+      }
+      "#,
+      "@scope(.media-object) to (.content>*){a{color:#ff0}}",
+    );
+    minify_test(
+      r#"
+      @scope to (.content > *) {
+        a { color: yellow; }
+      }
+      "#,
+      "@scope to (.content>*){a{color:#ff0}}",
+    );
+    minify_test(
+      r#"
+      @scope (#my-component) {
+        & { color: yellow; }
+      }
+      "#,
+      "@scope(#my-component){&{color:#ff0}}",
+    );
+    minify_test(
+      r#"
+      @scope (.parent-scope) {
+        @scope (:scope > .child-scope) to (:scope .limit) {
+          .content { color: yellow; }
+        }
+      }
+      "#,
+      "@scope(.parent-scope){@scope(:scope>.child-scope) to (:scope .limit){.content{color:#ff0}}}",
+    );
+    minify_test(
+      r#"
+      .foo {
+        @scope (.bar) {
+          color: yellow;
+        }
+      }
+      "#,
+      ".foo{@scope(.bar){color:#ff0}}",
+    );
+    nesting_test(
+      r#"
+      .foo {
+        @scope (.bar) {
+          color: yellow;
+        }
+      }
+      "#,
+      indoc! {r#"
+        @scope (.bar) {
+          color: #ff0;
+        }
+      "#},
+    );
+    nesting_test(
+      r#"
+      .parent {
+        color: blue;
+
+        @scope (& > .scope) to (& .limit) {
+          & .content {
+            color: yellow;
+          }
+        }
+      }
+      "#,
+      indoc! {r#"
+        .parent {
+          color: #00f;
+        }
+
+        @scope (.parent > .scope) to (.parent > .scope .limit) {
+          :scope .content {
+            color: #ff0;
+          }
+        }
+      "#},
+    );
+  }
+
+  #[test]
+  fn test_custom_media() {
+    custom_media_test(
+      r#"
+      @custom-media --modern (color), (hover);
+
+      @media (--modern) and (width > 1024px) {
+        .a {
+          color: green;
+        }
+      }
+      "#,
+      indoc! {r#"
+      @media ((color) or (hover)) and (width > 1024px) {
+        .a {
+          color: green;
+        }
+      }
+      "#},
+    );
+
+    custom_media_test(
+      r#"
+      @custom-media --color (color);
+
+      @media (--color) and (width > 1024px) {
+        .a {
+          color: green;
+        }
+      }
+      "#,
+      indoc! {r#"
+      @media (color) and (width > 1024px) {
+        .a {
+          color: green;
+        }
+      }
+      "#},
+    );
+
+    custom_media_test(
+      r#"
+      @custom-media --a (color);
+      @custom-media --b (--a);
+
+      @media (--b) and (width > 1024px) {
+        .a {
+          color: green;
+        }
+      }
+      "#,
+      indoc! {r#"
+      @media (color) and (width > 1024px) {
+        .a {
+          color: green;
+        }
+      }
+      "#},
+    );
+
+    custom_media_test(
+      r#"
+      @custom-media --not-color not (color);
+
+      @media not (--not-color) {
+        .a {
+          color: green;
+        }
+      }
+      "#,
+      indoc! {r#"
+      @media (color) {
+        .a {
+          color: green;
+        }
+      }
+      "#},
+    );
+
+    custom_media_test(
+      r#"
+      @custom-media --color-print print and (color);
+
+      @media (--color-print) {
+        .a {
+          color: green;
+        }
+      }
+      "#,
+      indoc! {r#"
+      @media print and (color) {
+        .a {
+          color: green;
+        }
+      }
+      "#},
+    );
+
+    custom_media_test(
+      r#"
+      @custom-media --color-print print and (color);
+
+      @media print and (--color-print) {
+        .a {
+          color: green;
+        }
+      }
+      "#,
+      indoc! {r#"
+      @media print and (color) {
+        .a {
+          color: green;
+        }
+      }
+      "#},
+    );
+
+    custom_media_test(
+      r#"
+      @custom-media --not-color-print not print and (color);
+
+      @media not print and (--not-color-print) {
+        .a {
+          color: green;
+        }
+      }
+      "#,
+      indoc! {r#"
+      @media not print and (color) {
+        .a {
+          color: green;
+        }
+      }
+      "#},
+    );
+
+    custom_media_test(
+      r#"
+      @custom-media --print print;
+
+      @media (--print) {
+        .a {
+          color: green;
+        }
+      }
+      "#,
+      indoc! {r#"
+      @media print {
+        .a {
+          color: green;
+        }
+      }
+      "#},
+    );
+
+    custom_media_test(
+      r#"
+      @custom-media --print print;
+
+      @media not (--print) {
+        .a {
+          color: green;
+        }
+      }
+      "#,
+      indoc! {r#"
+      @media not print {
+        .a {
+          color: green;
+        }
+      }
+      "#},
+    );
+
+    custom_media_test(
+      r#"
+      @custom-media --print not print;
+
+      @media not (--print) {
+        .a {
+          color: green;
+        }
+      }
+      "#,
+      indoc! {r#"
+      @media print {
+        .a {
+          color: green;
+        }
+      }
+      "#},
+    );
+
+    custom_media_test(
+      r#"
+      @custom-media --print print;
+
+      @media ((--print)) {
+        .a {
+          color: green;
+        }
+      }
+      "#,
+      indoc! {r#"
+      @media print {
+        .a {
+          color: green;
+        }
+      }
+      "#},
+    );
+
+    custom_media_test(
+      r#"
+      @custom-media --color (color);
+      @custom-media --print print;
+
+      @media (--print) and (--color) {
+        .a {
+          color: green;
+        }
+      }
+      "#,
+      indoc! {r#"
+      @media print and (color) {
+        .a {
+          color: green;
+        }
+      }
+      "#},
+    );
+
+    custom_media_test(
+      r#"
+      @custom-media --color (color);
+      @custom-media --not-print not print;
+
+      @media (--not-print) and (--color) {
+        .a {
+          color: green;
+        }
+      }
+      "#,
+      indoc! {r#"
+      @media not print and (color) {
+        .a {
+          color: green;
+        }
+      }
+      "#},
+    );
+
+    custom_media_test(
+      r#"
+      @custom-media --color (color);
+      @custom-media --screen screen;
+      @custom-media --print print;
+
+      @media (--print) and (--color), (--screen) and (--color) {
+        .a {
+          color: green;
+        }
+      }
+      "#,
+      indoc! {r#"
+      @media print and (color), screen and (color) {
+        .a {
+          color: green;
+        }
+      }
+      "#},
+    );
+
+    custom_media_test(
+      r#"
+      @custom-media --color print and (color), print and (script);
+
+      @media (--color) {
+        .a {
+          color: green;
+        }
+      }
+      "#,
+      indoc! {r#"
+      @media print and ((color) or (script)) {
+        .a {
+          color: green;
+        }
+      }
+      "#},
+    );
+
+    custom_media_test(
+      r#"
+      @custom-media --color (color);
+      @custom-media --not-color not all and (--color);
+
+      @media (--not-color) {
+        .a {
+          color: green;
+        }
+      }
+      "#,
+      indoc! {r#"
+        @media not all and (color) {
+          .a {
+            color: green;
+          }
+        }
+      "#},
+    );
+
+    custom_media_test(
+      r#"
+      @custom-media --color (color);
+
+      @media not all and (--color) {
+        .a {
+          color: green;
+        }
+      }
+      "#,
+      indoc! {r#"
+        @media not all and (color) {
+          .a {
+            color: green;
+          }
+        }
+      "#},
+    );
+
+    custom_media_test(
+      r#"
+      @media (--print) {
+        .a {
+          color: green;
+        }
+      }
+
+      @custom-media --print print;
+      "#,
+      indoc! {r#"
+      @media print {
+        .a {
+          color: green;
+        }
+      }
+      "#},
+    );
+
+    custom_media_test(
+      r#"
+      @custom-media --not-width not (min-width: 300px);
+      @media screen and ((prefers-color-scheme: dark) or (--not-width)) {
+        .foo {
+          order: 6;
+        }
+      }
+      "#,
+      indoc! {r#"
+      @media screen and ((prefers-color-scheme: dark) or ((width < 300px))) {
+        .foo {
+          order: 6;
+        }
+      }
+      "#},
+    );
+
+    fn custom_media_error_test(source: &str, err: Error<MinifyErrorKind>) {
+      let mut stylesheet = StyleSheet::parse(
+        &source,
+        ParserOptions {
+          filename: "test.css".into(),
+          flags: ParserFlags::CUSTOM_MEDIA,
+          ..ParserOptions::default()
+        },
+      )
+      .unwrap();
+      let res = stylesheet.minify(MinifyOptions {
+        targets: Browsers {
+          chrome: Some(95 << 16),
+          ..Browsers::default()
+        }
+        .into(),
+        ..MinifyOptions::default()
+      });
+      assert_eq!(res, Err(err))
+    }
+
+    custom_media_error_test(
+      r#"
+      @custom-media --color-print print and (color);
+
+      @media screen and (--color-print) {
+        .a {
+          color: green;
+        }
+      }
+      "#,
+      Error {
+        kind: MinifyErrorKind::UnsupportedCustomMediaBooleanLogic {
+          custom_media_loc: Location {
+            source_index: 0,
+            line: 1,
+            column: 7,
+          },
+        },
+        loc: Some(ErrorLocation {
+          filename: "test.css".into(),
+          line: 3,
+          column: 7,
+        }),
+      },
+    );
+
+    custom_media_error_test(
+      r#"
+      @custom-media --color-print print and (color);
+
+      @media not print and (--color-print) {
+        .a {
+          color: green;
+        }
+      }
+      "#,
+      Error {
+        kind: MinifyErrorKind::UnsupportedCustomMediaBooleanLogic {
+          custom_media_loc: Location {
+            source_index: 0,
+            line: 1,
+            column: 7,
+          },
+        },
+        loc: Some(ErrorLocation {
+          filename: "test.css".into(),
+          line: 3,
+          column: 7,
+        }),
+      },
+    );
+
+    custom_media_error_test(
+      r#"
+      @custom-media --color-print print and (color);
+      @custom-media --color-screen screen and (color);
+
+      @media (--color-print) or (--color-screen) {}
+      "#,
+      Error {
+        kind: MinifyErrorKind::UnsupportedCustomMediaBooleanLogic {
+          custom_media_loc: Location {
+            source_index: 0,
+            line: 2,
+            column: 7,
+          },
+        },
+        loc: Some(ErrorLocation {
+          filename: "test.css".into(),
+          line: 4,
+          column: 7,
+        }),
+      },
+    );
+
+    custom_media_error_test(
+      r#"
+      @custom-media --color-print print and (color);
+      @custom-media --color-screen screen and (color);
+
+      @media (--color-print) and (--color-screen) {}
+      "#,
+      Error {
+        kind: MinifyErrorKind::UnsupportedCustomMediaBooleanLogic {
+          custom_media_loc: Location {
+            source_index: 0,
+            line: 2,
+            column: 7,
+          },
+        },
+        loc: Some(ErrorLocation {
+          filename: "test.css".into(),
+          line: 4,
+          column: 7,
+        }),
+      },
+    );
+
+    custom_media_error_test(
+      r#"
+      @custom-media --screen screen;
+      @custom-media --print print;
+
+      @media (--print) and (--screen) {}
+      "#,
+      Error {
+        kind: MinifyErrorKind::UnsupportedCustomMediaBooleanLogic {
+          custom_media_loc: Location {
+            source_index: 0,
+            line: 1,
+            column: 7,
+          },
+        },
+        loc: Some(ErrorLocation {
+          filename: "test.css".into(),
+          line: 4,
+          column: 7,
+        }),
+      },
+    );
+
+    custom_media_error_test(
+      r#"
+      @custom-media --not-print not print and (color);
+      @custom-media --not-screen not screen and (color);
+
+      @media ((script) or ((--not-print) and (--not-screen))) {
+        .a {
+          color: green;
+        }
+      }
+      "#,
+      Error {
+        kind: MinifyErrorKind::UnsupportedCustomMediaBooleanLogic {
+          custom_media_loc: Location {
+            source_index: 0,
+            line: 2,
+            column: 7,
+          },
+        },
+        loc: Some(ErrorLocation {
+          filename: "test.css".into(),
+          line: 4,
+          column: 7,
+        }),
+      },
+    );
+
+    custom_media_error_test(
+      r#"
+      @custom-media --color screen and (color), print and (color);
+
+      @media (--color) {
+        .a {
+          color: green;
+        }
+      }
+      "#,
+      Error {
+        kind: MinifyErrorKind::UnsupportedCustomMediaBooleanLogic {
+          custom_media_loc: Location {
+            source_index: 0,
+            line: 1,
+            column: 7,
+          },
+        },
+        loc: Some(ErrorLocation {
+          filename: "test.css".into(),
+          line: 3,
+          column: 7,
+        }),
+      },
+    );
+
+    custom_media_error_test(
+      r#"
+      @media (--not-defined) {
+        .a {
+          color: green;
+        }
+      }
+      "#,
+      Error {
+        kind: MinifyErrorKind::CustomMediaNotDefined {
+          name: "--not-defined".into(),
+        },
+        loc: Some(ErrorLocation {
+          filename: "test.css".into(),
+          line: 1,
+          column: 7,
+        }),
+      },
+    );
+
+    custom_media_error_test(
+      r#"
+      @custom-media --circular-mq-a (--circular-mq-b);
+      @custom-media --circular-mq-b (--circular-mq-a);
+
+      @media (--circular-mq-a) {
+        body {
+          order: 3;
+        }
+      }
+      "#,
+      Error {
+        kind: MinifyErrorKind::CircularCustomMedia {
+          name: "--circular-mq-a".into(),
+        },
+        loc: Some(ErrorLocation {
+          filename: "test.css".into(),
+          line: 4,
+          column: 7,
+        }),
+      },
+    );
+  }
+
+  #[test]
+  fn test_dependencies() {
+    fn dep_test(source: &str, expected: &str, deps: Vec<(&str, &str)>) {
+      let mut stylesheet = StyleSheet::parse(
+        &source,
+        ParserOptions {
+          filename: "test.css".into(),
+          ..ParserOptions::default()
+        },
+      )
+      .unwrap();
+      stylesheet.minify(MinifyOptions::default()).unwrap();
+      let res = stylesheet
+        .to_css(PrinterOptions {
+          analyze_dependencies: Some(Default::default()),
+          minify: true,
+          ..PrinterOptions::default()
+        })
+        .unwrap();
+      assert_eq!(res.code, expected);
+      let dependencies = res.dependencies.unwrap();
+      assert_eq!(dependencies.len(), deps.len());
+      for (i, (url, placeholder)) in deps.into_iter().enumerate() {
+        match &dependencies[i] {
+          Dependency::Url(dep) => {
+            assert_eq!(dep.url, url);
+            assert_eq!(dep.placeholder, placeholder);
+          }
+          Dependency::Import(dep) => {
+            assert_eq!(dep.url, url);
+            assert_eq!(dep.placeholder, placeholder);
+          }
+        }
+      }
+    }
+
+    fn dep_error_test(source: &str, error: PrinterErrorKind) {
+      let stylesheet = StyleSheet::parse(&source, ParserOptions::default()).unwrap();
+      let res = stylesheet.to_css(PrinterOptions {
+        analyze_dependencies: Some(Default::default()),
+        ..PrinterOptions::default()
+      });
+      match res {
+        Err(e) => assert_eq!(e.kind, error),
+        _ => unreachable!(),
+      }
+    }
+
+    dep_test(
+      ".foo { background: image-set('./img12x.png', './img21x.png' 2x)}",
+      ".foo{background:image-set(\"hXFI8W\" 1x,\"5TkpBa\" 2x)}",
+      vec![("./img12x.png", "hXFI8W"), ("./img21x.png", "5TkpBa")],
+    );
+
+    dep_test(
+      ".foo { background: image-set(url(./img12x.png), url('./img21x.png') 2x)}",
+      ".foo{background:image-set(\"hXFI8W\" 1x,\"5TkpBa\" 2x)}",
+      vec![("./img12x.png", "hXFI8W"), ("./img21x.png", "5TkpBa")],
+    );
+
+    dep_test(
+      ".foo { --test: url(/foo.png) }",
+      ".foo{--test:url(\"lDnnrG\")}",
+      vec![("/foo.png", "lDnnrG")],
+    );
+
+    dep_test(
+      ".foo { --test: url(\"/foo.png\") }",
+      ".foo{--test:url(\"lDnnrG\")}",
+      vec![("/foo.png", "lDnnrG")],
+    );
+
+    dep_test(
+      ".foo { --test: url(\"http://example.com/foo.png\") }",
+      ".foo{--test:url(\"3X1zSW\")}",
+      vec![("http://example.com/foo.png", "3X1zSW")],
+    );
+
+    dep_test(
+      ".foo { --test: url(\"data:image/svg+xml;utf8,<svg></svg>\") }",
+      ".foo{--test:url(\"-vl-rG\")}",
+      vec![("data:image/svg+xml;utf8,<svg></svg>", "-vl-rG")],
+    );
+
+    dep_test(
+      ".foo { background: url(\"foo.png\") var(--test) }",
+      ".foo{background:url(\"Vwkwkq\") var(--test)}",
+      vec![("foo.png", "Vwkwkq")],
+    );
+
+    dep_error_test(
+      ".foo { --test: url(\"foo.png\") }",
+      PrinterErrorKind::AmbiguousUrlInCustomProperty { url: "foo.png".into() },
+    );
+
+    dep_error_test(
+      ".foo { --test: url(foo.png) }",
+      PrinterErrorKind::AmbiguousUrlInCustomProperty { url: "foo.png".into() },
+    );
+
+    dep_error_test(
+      ".foo { --test: url(./foo.png) }",
+      PrinterErrorKind::AmbiguousUrlInCustomProperty {
+        url: "./foo.png".into(),
+      },
+    );
+
+    dep_test(
+      ".foo { behavior: url(#foo) }",
+      ".foo{behavior:url(\"Zn9-2q\")}",
+      vec![("#foo", "Zn9-2q")],
+    );
+
+    dep_test(
+      ".foo { --foo: url(#foo) }",
+      ".foo{--foo:url(\"Zn9-2q\")}",
+      vec![("#foo", "Zn9-2q")],
+    );
+
+    dep_test(
+      "@import \"test.css\"; .foo { color: red }",
+      "@import \"hHsogW\";.foo{color:red}",
+      vec![("test.css", "hHsogW")],
+    );
+  }
+
+  #[test]
+  fn test_api() {
+    let stylesheet = StyleSheet::parse(".foo:hover { color: red }", ParserOptions::default()).unwrap();
+    match &stylesheet.rules.0[0] {
+      CssRule::Style(s) => {
+        assert_eq!(&s.selectors.to_string(), ".foo:hover");
+      }
+      _ => unreachable!(),
+    }
+
+    let color = CssColor::parse_string("#f0f").unwrap();
+    assert_eq!(color.to_css_string(PrinterOptions::default()).unwrap(), "#f0f");
+
+    let rule = CssRule::parse_string(".foo { color: red }", ParserOptions::default()).unwrap();
+    assert_eq!(
+      rule.to_css_string(PrinterOptions::default()).unwrap(),
+      indoc! {r#"
+    .foo {
+      color: red;
+    }"#}
+    );
+
+    let property = Property::parse_string("color".into(), "#f0f", ParserOptions::default()).unwrap();
+    assert_eq!(
+      property.to_css_string(false, PrinterOptions::default()).unwrap(),
+      "color: #f0f"
+    );
+    assert_eq!(
+      property.to_css_string(true, PrinterOptions::default()).unwrap(),
+      "color: #f0f !important"
+    );
+
+    let code = indoc! { r#"
+      .foo {
+        color: green;
+      }
+
+      .bar {
+        color: red;
+        background: pink;
+      }
+
+      @media print {
+        .baz {
+          color: green;
+        }
+      }
+    "#};
+    let stylesheet = StyleSheet::parse(code, ParserOptions::default()).unwrap();
+    if let CssRule::Style(style) = &stylesheet.rules.0[1] {
+      let (key, val) = style.property_location(code, 0).unwrap();
+      assert_eq!(
+        key,
+        SourceLocation { line: 5, column: 3 }..SourceLocation { line: 5, column: 8 }
+      );
+      assert_eq!(
+        val,
+        SourceLocation { line: 5, column: 10 }..SourceLocation { line: 5, column: 13 }
+      );
+    }
+
+    if let CssRule::Style(style) = &stylesheet.rules.0[1] {
+      let (key, val) = style.property_location(code, 1).unwrap();
+      assert_eq!(
+        key,
+        SourceLocation { line: 6, column: 3 }..SourceLocation { line: 6, column: 13 }
+      );
+      assert_eq!(
+        val,
+        SourceLocation { line: 6, column: 15 }..SourceLocation { line: 6, column: 19 }
+      );
+    }
+    if let CssRule::Media(media) = &stylesheet.rules.0[2] {
+      if let CssRule::Style(style) = &media.rules.0[0] {
+        let (key, val) = style.property_location(code, 0).unwrap();
+        assert_eq!(
+          key,
+          SourceLocation { line: 11, column: 5 }..SourceLocation { line: 11, column: 10 }
+        );
+        assert_eq!(
+          val,
+          SourceLocation { line: 11, column: 12 }..SourceLocation { line: 11, column: 17 }
+        );
+      }
+    }
+
+    let mut property = Property::Transform(Default::default(), VendorPrefix::WebKit);
+    property.set_prefix(VendorPrefix::None);
+    assert_eq!(property, Property::Transform(Default::default(), VendorPrefix::None));
+    property.set_prefix(VendorPrefix::Moz);
+    assert_eq!(property, Property::Transform(Default::default(), VendorPrefix::Moz));
+    property.set_prefix(VendorPrefix::WebKit | VendorPrefix::Moz);
+    assert_eq!(
+      property,
+      Property::Transform(Default::default(), VendorPrefix::WebKit | VendorPrefix::Moz)
+    );
+
+    let mut property = Property::TextDecorationLine(Default::default(), VendorPrefix::None);
+    property.set_prefix(VendorPrefix::Ms);
+    assert_eq!(
+      property,
+      Property::TextDecorationLine(Default::default(), VendorPrefix::None)
+    );
+    property.set_prefix(VendorPrefix::WebKit | VendorPrefix::Moz | VendorPrefix::Ms);
+    assert_eq!(
+      property,
+      Property::TextDecorationLine(Default::default(), VendorPrefix::WebKit | VendorPrefix::Moz)
+    );
+
+    let mut property = Property::AccentColor(Default::default());
+    property.set_prefix(VendorPrefix::WebKit);
+    assert_eq!(property, Property::AccentColor(Default::default()));
+  }
+
+  #[cfg(feature = "substitute_variables")]
+  #[test]
+  fn test_substitute_vars() {
+    use crate::properties::custom::TokenList;
+    use crate::traits::ParseWithOptions;
+
+    fn test(property: Property, vars: HashMap<&str, &str>, expected: &str) {
+      if let Property::Unparsed(unparsed) = property {
+        let vars = vars
+          .into_iter()
+          .map(|(k, v)| {
+            (
+              k,
+              TokenList::parse_string_with_options(v, ParserOptions::default()).unwrap(),
+            )
+          })
+          .collect();
+        let substituted = unparsed.substitute_variables(&vars).unwrap();
+        assert_eq!(
+          substituted.to_css_string(false, PrinterOptions::default()).unwrap(),
+          expected
+        );
+      } else {
+        panic!("Not an unparsed property");
+      }
+    }
+
+    let property = Property::parse_string("color".into(), "var(--test)", ParserOptions::default()).unwrap();
+    test(property, HashMap::from([("--test", "yellow")]), "color: #ff0");
+
+    let property =
+      Property::parse_string("color".into(), "var(--test, var(--foo))", ParserOptions::default()).unwrap();
+    test(property, HashMap::from([("--foo", "yellow")]), "color: #ff0");
+    let property = Property::parse_string(
+      "color".into(),
+      "var(--test, var(--foo, yellow))",
+      ParserOptions::default(),
+    )
+    .unwrap();
+    test(property, HashMap::new(), "color: #ff0");
+
+    let property =
+      Property::parse_string("width".into(), "calc(var(--a) + var(--b))", ParserOptions::default()).unwrap();
+    test(property, HashMap::from([("--a", "2px"), ("--b", "4px")]), "width: 6px");
+
+    let property = Property::parse_string("color".into(), "var(--a)", ParserOptions::default()).unwrap();
+    test(
+      property,
+      HashMap::from([("--a", "var(--b)"), ("--b", "yellow")]),
+      "color: #ff0",
+    );
+
+    let property = Property::parse_string("color".into(), "var(--a)", ParserOptions::default()).unwrap();
+    test(
+      property,
+      HashMap::from([("--a", "var(--b)"), ("--b", "var(--c)"), ("--c", "var(--a)")]),
+      "color: var(--a)",
+    );
+  }
+
+  #[test]
+  fn test_layer() {
+    minify_test("@layer foo;", "@layer foo;");
+    minify_test("@layer foo, bar;", "@layer foo,bar;");
+    minify_test("@layer foo.bar;", "@layer foo.bar;");
+    minify_test("@layer foo.bar, baz;", "@layer foo.bar,baz;");
+
+    minify_test(
+      r#"
+      @layer foo {
+        .bar {
+          color: red;
+        }
+      }
+    "#,
+      "@layer foo{.bar{color:red}}",
+    );
+    minify_test(
+      r#"
+      @layer foo.bar {
+        .bar {
+          color: red;
+        }
+      }
+    "#,
+      "@layer foo.bar{.bar{color:red}}",
+    );
+    minify_test(r#"
+      @layer base {
+        p { max-width: 70ch; }
+      }
+
+      @layer framework {
+        @layer base {
+          p { margin-block: 0.75em; }
+        }
+
+        @layer theme {
+          p { color: #222; }
+        }
+      }
+    "#, "@layer base{p{max-width:70ch}}@layer framework{@layer base{p{margin-block:.75em}}@layer theme{p{color:#222}}}");
+    minify_test(
+      r#"
+      @layer {
+        .bar {
+          color: red;
+        }
+      }
+    "#,
+      "@layer{.bar{color:red}}",
+    );
+    minify_test(
+      r#"
+      @layer foo\20 bar, baz;
+    "#,
+      "@layer foo\\ bar,baz;",
+    );
+    minify_test(
+      r#"
+      @layer one.two\20 three\#four\.five {
+        .bar {
+          color: red;
+        }
+      }
+    "#,
+      "@layer one.two\\ three\\#four\\.five{.bar{color:red}}",
+    );
+
+    error_test("@layer;", ParserError::UnexpectedToken(Token::Semicolon));
+    error_test("@layer foo, bar {};", ParserError::AtRuleBodyInvalid);
+    minify_test("@import 'test.css' layer;", "@import \"test.css\" layer;");
+    minify_test("@import 'test.css' layer(foo);", "@import \"test.css\" layer(foo);");
+    minify_test(
+      "@import 'test.css' layer(foo.bar);",
+      "@import \"test.css\" layer(foo.bar);",
+    );
+    minify_test(
+      "@import 'test.css' layer(foo\\20 bar);",
+      "@import \"test.css\" layer(foo\\ bar);",
+    );
+    error_test(
+      "@import 'test.css' layer(foo, bar) {};",
+      ParserError::UnexpectedToken(Token::Comma),
+    );
+    minify_test(
+      r#"
+      @layer one {
+        body {
+          background: red;
+        }
+      }
+
+      body {
+        background: red;
+      }
+
+      @layer two {
+        body {
+          background: green;
+        }
+      }
+
+      @layer one {
+        body {
+          background: yellow;
+        }
+      }
+      "#,
+      "@layer one{body{background:#ff0}}body{background:red}@layer two{body{background:green}}",
+    );
+  }
+
+  #[test]
+  fn test_property() {
+    minify_test(
+      r#"
+      @property --property-name {
+        syntax: '<color>';
+        inherits: false;
+        initial-value: yellow;
+      }
+    "#,
+      "@property --property-name{syntax:\"<color>\";inherits:false;initial-value:#ff0}",
+    );
+
+    test(
+      r#"
+      @property --property-name {
+        syntax: '*';
+        inherits: false;
+        initial-value: ;
+      }
+    "#,
+      indoc! {r#"
+      @property --property-name {
+        syntax: "*";
+        inherits: false;
+        initial-value: ;
+      }
+    "#},
+    );
+
+    minify_test(
+      r#"
+      @property --property-name {
+        syntax: '*';
+        inherits: false;
+        initial-value: ;
+      }
+    "#,
+      "@property --property-name{syntax:\"*\";inherits:false;initial-value:}",
+    );
+
+    test(
+      r#"
+      @property --property-name {
+        syntax: '*';
+        inherits: false;
+        initial-value:;
+      }
+    "#,
+      indoc! {r#"
+      @property --property-name {
+        syntax: "*";
+        inherits: false;
+        initial-value: ;
+      }
+    "#},
+    );
+
+    minify_test(
+      r#"
+      @property --property-name {
+        syntax: '*';
+        inherits: false;
+        initial-value:;
+      }
+    "#,
+      "@property --property-name{syntax:\"*\";inherits:false;initial-value:}",
+    );
+    minify_test(
+      r#"
+      @property --property-name {
+        syntax: '*';
+        inherits: false;
+        initial-value: foo bar;
+      }
+    "#,
+      "@property --property-name{syntax:\"*\";inherits:false;initial-value:foo bar}",
+    );
+
+    minify_test(
+      r#"
+      @property --property-name {
+        syntax: '<length>';
+        inherits: true;
+        initial-value: 25px;
+      }
+    "#,
+      "@property --property-name{syntax:\"<length>\";inherits:true;initial-value:25px}",
+    );
+
+    error_test(
+      r#"
+      @property --property-name {
+        syntax: '<color>';
+        inherits: false;
+        initial-value: 25px;
+      }
+    "#,
+      ParserError::UnexpectedToken(crate::properties::custom::Token::Dimension {
+        has_sign: false,
+        value: 25.0,
+        int_value: Some(25),
+        unit: "px".into(),
+      }),
+    );
+
+    error_test(
+      r#"
+      @property --property-name {
+        syntax: '<length>';
+        inherits: false;
+        initial-value: var(--some-value);
+      }
+    "#,
+      ParserError::UnexpectedToken(crate::properties::custom::Token::Function("var".into())),
+    );
+
+    error_test(
+      r#"
+      @property --property-name {
+        syntax: '<color>';
+        inherits: false;
+      }
+    "#,
+      ParserError::AtRuleBodyInvalid,
+    );
+
+    minify_test(
+      r#"
+      @property --property-name {
+        syntax: '*';
+        inherits: false;
+      }
+    "#,
+      "@property --property-name{syntax:\"*\";inherits:false}",
+    );
+
+    error_test(
+      r#"
+      @property --property-name {
+        syntax: '*';
+      }
+    "#,
+      ParserError::AtRuleBodyInvalid,
+    );
+
+    error_test(
+      r#"
+      @property --property-name {
+        inherits: false;
+      }
+    "#,
+      ParserError::AtRuleBodyInvalid,
+    );
+
+    error_test(
+      r#"
+      @property property-name {
+        syntax: '*';
+        inherits: false;
+      }
+    "#,
+      ParserError::UnexpectedToken(crate::properties::custom::Token::Ident("property-name".into())),
+    );
+
+    minify_test(
+      r#"
+      @property --property-name {
+        syntax: 'custom | <color>';
+        inherits: false;
+        initial-value: yellow;
+      }
+    "#,
+      "@property --property-name{syntax:\"custom|<color>\";inherits:false;initial-value:#ff0}",
+    );
+
+    // TODO: Re-enable with a better solution
+    //       See: https://github.com/parcel-bundler/lightningcss/issues/288
+    // minify_test(r#"
+    //   @property --property-name {
+    //     syntax: '<transform-list>';
+    //     inherits: false;
+    //     initial-value: translate(200px,300px) translate(100px,200px) scale(2);
+    //   }
+    // "#, "@property --property-name{syntax:\"<transform-list>\";inherits:false;initial-value:matrix(2,0,0,2,300,500)}");
+
+    minify_test(
+      r#"
+      @property --property-name {
+        syntax: '<time>';
+        inherits: false;
+        initial-value: 1000ms;
+      }
+    "#,
+      "@property --property-name{syntax:\"<time>\";inherits:false;initial-value:1s}",
+    );
+
+    minify_test(
+      r#"
+      @property --property-name {
+        syntax: '<url>';
+        inherits: false;
+        initial-value: url("foo.png");
+      }
+    "#,
+      "@property --property-name{syntax:\"<url>\";inherits:false;initial-value:url(foo.png)}",
+    );
+
+    minify_test(
+      r#"
+      @property --property-name {
+        syntax: '<image>';
+        inherits: false;
+        initial-value: linear-gradient(yellow, blue);
+      }
+    "#,
+      "@property --property-name{syntax:\"<image>\";inherits:false;initial-value:linear-gradient(#ff0,#00f)}",
+    );
+
+    minify_test(
+      r#"
+      @property --property-name {
+        initial-value: linear-gradient(yellow, blue);
+        inherits: false;
+        syntax: '<image>';
+      }
+    "#,
+      "@property --property-name{syntax:\"<image>\";inherits:false;initial-value:linear-gradient(#ff0,#00f)}",
+    );
+
+    test(
+      r#"
+      @property --property-name {
+        syntax: '<length>|none';
+        inherits: false;
+        initial-value: none;
+      }
+    "#,
+      indoc! {r#"
+      @property --property-name {
+        syntax: "<length> | none";
+        inherits: false;
+        initial-value: none;
+      }
+    "#},
+    );
+
+    minify_test(
+      r#"
+      @property --property-name {
+        syntax: '<color>#';
+        inherits: false;
+        initial-value: yellow, blue;
+      }
+    "#,
+      "@property --property-name{syntax:\"<color>#\";inherits:false;initial-value:#ff0,#00f}",
+    );
+    minify_test(
+      r#"
+      @property --property-name {
+        syntax: '<color>+';
+        inherits: false;
+        initial-value: yellow blue;
+      }
+    "#,
+      "@property --property-name{syntax:\"<color>+\";inherits:false;initial-value:#ff0 #00f}",
+    );
+    minify_test(
+      r#"
+      @property --property-name {
+        syntax: '<color>';
+        inherits: false;
+        initial-value: yellow;
+      }
+      .foo {
+        color: var(--property-name)
+      }
+      @property --property-name {
+        syntax: '<color>';
+        inherits: true;
+        initial-value: blue;
+      }
+    "#,
+      "@property --property-name{syntax:\"<color>\";inherits:true;initial-value:#00f}.foo{color:var(--property-name)}",
+    );
+  }
+
+  #[test]
+  fn test_quoting_unquoting_urls() {
+    // Quotes remain double quotes when not minifying
+    test(
+      r#".foo {
+      background-image: url("0123abcd");
+    }"#,
+      r#".foo {
+  background-image: url("0123abcd");
+}
+"#,
+    );
+
+    // Quotes removed when minifying
+    minify_test(
+      r#".foo {
+      background-image: url("0123abcd");
+    }"#,
+      r#".foo{background-image:url(0123abcd)}"#,
+    );
+
+    // Doubles quotes added if not present when not minifying
+    test(
+      r#".foo {
+      background-image: url(0123abcd);
+    }"#,
+      r#".foo {
+  background-image: url("0123abcd");
+}
+"#,
+    );
+
+    // No quotes changed if not present when not minifying
+    minify_test(
+      r#".foo {
+      background-image: url(0123abcd);
+    }"#,
+      r#".foo{background-image:url(0123abcd)}"#,
+    );
+  }
+
+  #[test]
+  fn test_zindex() {
+    minify_test(".foo { z-index: 2 }", ".foo{z-index:2}");
+    minify_test(".foo { z-index: -2 }", ".foo{z-index:-2}");
+    minify_test(".foo { z-index: 999999 }", ".foo{z-index:999999}");
+    minify_test(".foo { z-index: 9999999 }", ".foo{z-index:9999999}");
+    minify_test(".foo { z-index: -9999999 }", ".foo{z-index:-9999999}");
+  }
+
+  #[test]
+  #[cfg(feature = "sourcemap")]
+  fn test_input_source_map() {
+    let source = r#".imported {
+      content: "yay, file support!";
+    }
+
+    .selector {
+      margin: 1em;
+      background-color: #f60;
+    }
+
+    .selector .nested {
+      margin: 0.5em;
+    }
+
+    /*# sourceMappingURL=data:application/json;base64,ewoJInZlcnNpb24iOiAzLAoJInNvdXJjZVJvb3QiOiAicm9vdCIsCgkiZmlsZSI6ICJzdGRvdXQiLAoJInNvdXJjZXMiOiBbCgkJInN0ZGluIiwKCQkic2Fzcy9fdmFyaWFibGVzLnNjc3MiLAoJCSJzYXNzL19kZW1vLnNjc3MiCgldLAoJInNvdXJjZXNDb250ZW50IjogWwoJCSJAaW1wb3J0IFwiX3ZhcmlhYmxlc1wiO1xuQGltcG9ydCBcIl9kZW1vXCI7XG5cbi5zZWxlY3RvciB7XG4gIG1hcmdpbjogJHNpemU7XG4gIGJhY2tncm91bmQtY29sb3I6ICRicmFuZENvbG9yO1xuXG4gIC5uZXN0ZWQge1xuICAgIG1hcmdpbjogJHNpemUgLyAyO1xuICB9XG59IiwKCQkiJGJyYW5kQ29sb3I6ICNmNjA7XG4kc2l6ZTogMWVtOyIsCgkJIi5pbXBvcnRlZCB7XG4gIGNvbnRlbnQ6IFwieWF5LCBmaWxlIHN1cHBvcnQhXCI7XG59IgoJXSwKCSJtYXBwaW5ncyI6ICJBRUFBLFNBQVMsQ0FBQztFQUNSLE9BQU8sRUFBRSxvQkFBcUI7Q0FDL0I7O0FGQ0QsU0FBUyxDQUFDO0VBQ1IsTUFBTSxFQ0hELEdBQUc7RURJUixnQkFBZ0IsRUNMTCxJQUFJO0NEVWhCOztBQVBELFNBQVMsQ0FJUCxPQUFPLENBQUM7RUFDTixNQUFNLEVDUEgsS0FBRztDRFFQIiwKCSJuYW1lcyI6IFtdCn0= */"#;
+
+    let mut stylesheet = StyleSheet::parse(&source, ParserOptions::default()).unwrap();
+    stylesheet.minify(MinifyOptions::default()).unwrap();
+    let mut sm = parcel_sourcemap::SourceMap::new("/");
+    stylesheet
+      .to_css(PrinterOptions {
+        source_map: Some(&mut sm),
+        minify: true,
+        ..PrinterOptions::default()
+      })
+      .unwrap();
+    let map = sm.to_json(None).unwrap();
+    assert_eq!(
+      map,
+      r#"{"version":3,"sourceRoot":null,"mappings":"AAAA,uCCGA,2CAAA","sources":["sass/_demo.scss","stdin"],"sourcesContent":[".imported {\n  content: \"yay, file support!\";\n}","@import \"_variables\";\n@import \"_demo\";\n\n.selector {\n  margin: $size;\n  background-color: $brandColor;\n\n  .nested {\n    margin: $size / 2;\n  }\n}"],"names":[]}"#
+    );
+  }
+
+  #[test]
+  #[cfg(feature = "sourcemap")]
+  fn test_source_maps_with_license_comments() {
+    let source = r#"/*! a single line comment */
+    /*!
+      a comment
+      containing
+      multiple
+      lines
+    */
+    .a {
+      display: flex;
+    }
+
+    .b {
+      display: hidden;
+    }
+    "#;
+
+    let mut sm = parcel_sourcemap::SourceMap::new("/");
+    let source_index = sm.add_source("input.css");
+    sm.set_source_content(source_index as usize, source).unwrap();
+
+    let mut stylesheet = StyleSheet::parse(
+      &source,
+      ParserOptions {
+        source_index,
+        ..Default::default()
+      },
+    )
+    .unwrap();
+    stylesheet.minify(MinifyOptions::default()).unwrap();
+    stylesheet
+      .to_css(PrinterOptions {
+        source_map: Some(&mut sm),
+        minify: true,
+        ..PrinterOptions::default()
+      })
+      .unwrap();
+    let map = sm.to_json(None).unwrap();
+    assert_eq!(
+      map,
+      r#"{"version":3,"sourceRoot":null,"mappings":";;;;;;;AAOI,gBAIA","sources":["input.css"],"sourcesContent":["/*! a single line comment */\n    /*!\n      a comment\n      containing\n      multiple\n      lines\n    */\n    .a {\n      display: flex;\n    }\n\n    .b {\n      display: hidden;\n    }\n    "],"names":[]}"#
+    );
+  }
+
+  #[test]
+  fn test_error_recovery() {
+    use std::sync::{Arc, RwLock};
+    let warnings = Some(Arc::new(RwLock::new(Vec::new())));
+    test_with_options(
+      r#"
+      h1(>h1) {
+        color: red;
+      }
+
+      .foo {
+        color: red;
+      }
+
+      .clearfix {
+        *zoom: 1;
+        background: red;
+      }
+
+      @media (hover) {
+        h1(>h1) {
+          color: red;
+        }
+
+        .bar {
+          color: red;
+        }
+      }
+
+      input:placeholder {
+        color: red;
+      }
+
+      input::hover {
+        color: red;
+      }
+    "#,
+      indoc! { r#"
+      .foo {
+        color: red;
+      }
+
+      .clearfix {
+        background: red;
+      }
+
+      @media (hover) {
+        .bar {
+          color: red;
+        }
+      }
+
+      input:placeholder {
+        color: red;
+      }
+
+      input::hover {
+        color: red;
+      }
+    "#},
+      ParserOptions {
+        filename: "test.css".into(),
+        error_recovery: true,
+        warnings: warnings.clone(),
+        ..ParserOptions::default()
+      },
+    );
+    let w = warnings.unwrap();
+    let warnings = w.read().unwrap();
+    assert_eq!(
+      *warnings,
+      vec![
+        Error {
+          kind: ParserError::SelectorError(SelectorError::EmptySelector),
+          loc: Some(ErrorLocation {
+            filename: "test.css".into(),
+            line: 1,
+            column: 7
+          })
+        },
+        Error {
+          kind: ParserError::UnexpectedToken(Token::Semicolon),
+          loc: Some(ErrorLocation {
+            filename: "test.css".into(),
+            line: 10,
+            column: 17
+          })
+        },
+        Error {
+          kind: ParserError::SelectorError(SelectorError::EmptySelector),
+          loc: Some(ErrorLocation {
+            filename: "test.css".into(),
+            line: 15,
+            column: 9
+          })
+        },
+        Error {
+          kind: ParserError::SelectorError(SelectorError::UnsupportedPseudoClass("placeholder".into())),
+          loc: Some(ErrorLocation {
+            filename: "test.css".into(),
+            line: 24,
+            column: 13,
+          }),
+        },
+        Error {
+          kind: ParserError::SelectorError(SelectorError::UnsupportedPseudoElement("hover".into())),
+          loc: Some(ErrorLocation {
+            filename: "test.css".into(),
+            line: 28,
+            column: 13,
+          }),
+        },
+      ]
+    )
+  }
+
+  #[test]
+  fn test_invalid() {
+    error_test(
+      ".a{color: hsla(120, 62.32%;}",
+      ParserError::UnexpectedToken(Token::CloseCurlyBracket),
+    );
+    error_test(
+      ".a{--foo: url(foo\\) b\\)ar)}",
+      ParserError::UnexpectedToken(Token::BadUrl("foo\\) b\\)ar".into())),
+    );
+  }
+
+  #[test]
+  fn test_container_queries() {
+    // with name
+    minify_test(
+      r#"
+      @container my-layout (inline-size > 45em) {
+        .foo {
+          color: red;
+        }
+      }
+    "#,
+      "@container my-layout (inline-size>45em){.foo{color:red}}",
+    );
+
+    minify_test(
+      r#"
+      @container my-layout ( not (width > 500px) ) {
+        .foo {
+          color: red;
+        }
+      }
+    "#,
+      "@container my-layout not (width>500px){.foo{color:red}}",
+    );
+
+    minify_test(
+      r#"
+      @container my-layout not (width > 500px) {
+        .foo {
+          color: red;
+        }
+      }
+    "#,
+      "@container my-layout not (width>500px){.foo{color:red}}",
+    );
+
+    minify_test(
+      r#"
+      @container not (width > 500px) {
+        .foo {
+          color: red;
+        }
+      }
+    "#,
+      "@container not (width>500px){.foo{color:red}}",
+    );
+
+    minify_test(
+      r#"
+      @container my-layout ((width: 100px) and (not (height: 100px))) {
+        .foo {
+          color: red;
+        }
+      }
+    "#,
+      "@container my-layout (width:100px) and (not (height:100px)){.foo{color:red}}",
+    );
+
+    minify_test(
+      r#"
+      @container my-layout (width = max(10px, 10em)) {
+        .foo {
+          color: red;
+        }
+      }
+    "#,
+      "@container my-layout (width=max(10px,10em)){.foo{color:red}}",
+    );
+
+    // without name
+    minify_test(
+      r#"
+      @container (inline-size > 45em) {
+        .foo {
+          color: red;
+        }
+      }
+    "#,
+      "@container (inline-size>45em){.foo{color:red}}",
+    );
+
+    minify_test(
+      r#"
+      @container (inline-size > 45em) and (inline-size < 100em) {
+        .foo {
+          color: red;
+        }
+      }
+    "#,
+      "@container (inline-size>45em) and (inline-size<100em){.foo{color:red}}",
+    );
+
+    // calc()
+    minify_test(
+      r#"
+      @container (width > calc(100vw - 50px)) {
+        .foo {
+          color: red;
+        }
+      }
+    "#,
+      "@container (width>calc(100vw - 50px)){.foo{color:red}}",
+    );
+
+    minify_test(
+      r#"
+      @container (calc(100vh - 50px) <= height ) {
+        .foo {
+          color: red;
+        }
+      }
+    "#,
+      "@container (height>=calc(100vh - 50px)){.foo{color:red}}",
+    );
+
+    // merge adjacent
+    minify_test(
+      r#"
+      @container my-layout (inline-size > 45em) {
+        .foo {
+          color: red;
+        }
+      }
+
+      @container my-layout (inline-size > 45em) {
+        .foo {
+          background: yellow;
+        }
+
+        .bar {
+          color: white;
+        }
+      }
+    "#,
+      "@container my-layout (inline-size>45em){.foo{color:red;background:#ff0}.bar{color:#fff}}",
+    );
+
+    minify_test(
+      r#"
+    .foo {
+      container-name: foo bar;
+      container-type: size;
+    }
+    "#,
+      ".foo{container:foo bar/size}",
+    );
+    minify_test(
+      r#"
+    .foo {
+      container-name: foo bar;
+      container-type: normal;
+    }
+    "#,
+      ".foo{container:foo bar}",
+    );
+    minify_test(
+      ".foo{ container-type: inline-size }",
+      ".foo{container-type:inline-size}",
+    );
+    minify_test(".foo{ container-name: none; }", ".foo{container-name:none}");
+    minify_test(".foo{ container-name: foo; }", ".foo{container-name:foo}");
+    minify_test(".foo{ container: foo / normal; }", ".foo{container:foo}");
+    minify_test(
+      ".foo{ container: foo / inline-size; }",
+      ".foo{container:foo/inline-size}",
+    );
+    minify_test(".foo { width: calc(1cqw + 2cqw) }", ".foo{width:3cqw}");
+    minify_test(".foo { width: calc(1cqh + 2cqh) }", ".foo{width:3cqh}");
+    minify_test(".foo { width: calc(1cqi + 2cqi) }", ".foo{width:3cqi}");
+    minify_test(".foo { width: calc(1cqb + 2cqb) }", ".foo{width:3cqb}");
+    minify_test(".foo { width: calc(1cqmin + 2cqmin) }", ".foo{width:3cqmin}");
+    minify_test(".foo { width: calc(1cqmax + 2cqmax) }", ".foo{width:3cqmax}");
+
+    // Unlike in @media, there is no need to convert the range syntax in @container,
+    // because browsers all support this syntax.
+    prefix_test(
+      r#"
+      @container (width > 100px) {
+        .foo { padding: 5px; }
+      }
+      "#,
+      indoc! { r#"
+        @container (width > 100px) {
+          .foo {
+            padding: 5px;
+          }
+        }
+      "#},
+      Browsers {
+        chrome: Some(105 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      @container (min-width: 100px) {
+        .foo { padding: 5px; }
+      }
+      "#,
+      indoc! { r#"
+        @container (width >= 100px) {
+          .foo {
+            padding: 5px;
+          }
+        }
+      "#},
+      Browsers {
+        chrome: Some(105 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    minify_test(
+      r#"
+      @container style(--responsive: true) {
+        .foo {
+          color: red;
+        }
+      }
+    "#,
+      "@container style(--responsive:true){.foo{color:red}}",
+    );
+    minify_test(
+      r#"
+      @container style(--responsive: true) and style(color: yellow) {
+        .foo {
+          color: red;
+        }
+      }
+    "#,
+      "@container style(--responsive:true) and style(color:#ff0){.foo{color:red}}",
+    );
+    minify_test(
+      r#"
+      @container not style(--responsive: true) {
+        .foo {
+          color: red;
+        }
+      }
+    "#,
+      "@container not style(--responsive:true){.foo{color:red}}",
+    );
+    minify_test(
+      r#"
+      @container (inline-size > 45em) and style(--responsive: true) {
+        .foo {
+          color: red;
+        }
+      }
+    "#,
+      "@container (inline-size>45em) and style(--responsive:true){.foo{color:red}}",
+    );
+    minify_test(
+      r#"
+      @container style((accent-color: yellow) or (--bar: 10px)) {
+        .foo {
+          color: red;
+        }
+      }
+    "#,
+      "@container style((accent-color:#ff0) or (--bar:10px)){.foo{color:red}}",
+    );
+    minify_test(
+      r#"
+      @container style(not ((width: calc(10px + 20px)) and ((--bar: url(x))))) {
+        .foo {
+          color: red;
+        }
+      }
+    "#,
+      "@container style(not ((width:30px) and (--bar:url(x)))){.foo{color:red}}",
+    );
+    minify_test(
+      r#"
+      @container style(color: yellow !important) {
+        .foo {
+          color: red;
+        }
+      }
+    "#,
+      "@container style(color:yellow){.foo{color:red}}",
+    );
+    minify_test(
+      r#"
+      @container style(--foo:) {
+        .foo {
+          color: red;
+        }
+      }
+    "#,
+      "@container style(--foo:){.foo{color:red}}",
+    );
+    minify_test(
+      r#"
+      @container style(--foo: ) {
+        .foo {
+          color: red;
+        }
+      }
+    "#,
+      "@container style(--foo:){.foo{color:red}}",
+    );
+    minify_test(
+      r#"
+      @container style(--my-prop: foo - bar ()) {
+        .foo {
+          color: red;
+        }
+      }
+    "#,
+      "@container style(--my-prop:foo - bar ()){.foo{color:red}}",
+    );
+    minify_test(
+      r#"
+      @container style(--test) {
+        .foo {
+          color: red;
+        }
+      }
+    "#,
+      "@container style(--test){.foo{color:red}}",
+    );
+    minify_test(
+      r#"
+      @container style(width) {
+        .foo {
+          color: red;
+        }
+      }
+    "#,
+      "@container style(width){.foo{color:red}}",
+    );
+
+    // Disallow 'none', 'not', 'and', 'or' as a `<container-name>`
+    // https://github.com/w3c/csswg-drafts/issues/7203#issuecomment-1144257312
+    // https://chromium-review.googlesource.com/c/chromium/src/+/3698402
+    error_test(
+      "@container none (width < 100vw) {}",
+      ParserError::UnexpectedToken(crate::properties::custom::Token::Ident("none".into())),
+    );
+
+    error_test(
+      "@container and (width < 100vw) {}",
+      ParserError::UnexpectedToken(crate::properties::custom::Token::Ident("and".into())),
+    );
+
+    error_test(
+      "@container or (width < 100vw) {}",
+      ParserError::UnexpectedToken(crate::properties::custom::Token::Ident("or".into())),
+    );
+
+    // Disallow CSS wide keywords as a `<container-name>`
+    error_test(
+      "@container revert-layer (width < 100vw) {}",
+      ParserError::UnexpectedToken(crate::properties::custom::Token::Ident("revert-layer".into())),
+    );
+
+    error_test(
+      "@container initial (width < 100vw) {}",
+      ParserError::UnexpectedToken(crate::properties::custom::Token::Ident("initial".into())),
+    );
+
+    // <ident> contains spaces
+    // https://github.com/web-platform-tests/wpt/blob/39f0da08fbbe33d0582a35749b6dbf8bd067a52d/css/css-contain/container-queries/at-container-parsing.html#L160-L178
+    error_test(
+      "@container foo bar (width < 100vw) {}",
+      ParserError::UnexpectedToken(crate::properties::custom::Token::Ident("bar".into())),
+    );
+
+    error_test("@container (inline-size <= foo) {}", ParserError::InvalidMediaQuery);
+    error_test("@container (orientation <= 10px) {}", ParserError::InvalidMediaQuery);
+
+    error_test(
+      "@container style(style(--foo: bar)) {}",
+      ParserError::UnexpectedToken(crate::properties::custom::Token::Function("style".into())),
+    );
+  }
+
+  #[test]
+  fn test_css_modules_value_rule() {
+    css_modules_error_test(
+      "@value compact: (max-width: 37.4375em);",
+      ParserError::DeprecatedCssModulesValueRule,
+    );
+  }
+
+  #[test]
+  fn test_unknown_at_rules() {
+    minify_test("@foo;", "@foo;");
+    minify_test("@foo bar;", "@foo bar;");
+    minify_test("@foo (bar: baz);", "@foo (bar: baz);");
+    test(
+      r#"@foo test {
+      div {
+        color: red;
+      }
+    }"#,
+      indoc! {r#"
+      @foo test {
+        div { color: red; }
+      }
+      "#},
+    );
+    minify_test(
+      r#"@foo test {
+      div {
+        color: red;
+      }
+    }"#,
+      "@foo test{div { color: red; }}",
+    );
+    minify_test(
+      r#"@foo test {
+        foo: bar;
+      }"#,
+      "@foo test{foo: bar;}",
+    );
+    test(
+      r#"@foo {
+        foo: bar;
+      }"#,
+      indoc! {r#"
+      @foo {
+        foo: bar;
+      }
+      "#},
+    );
+    minify_test(
+      r#"@foo {
+        foo: bar;
+      }"#,
+      "@foo{foo: bar;}",
+    );
+  }
+
+  #[test]
+  fn test_resolution() {
+    prefix_test(
+      r#"
+      @media (resolution: 1dppx) {
+        body {
+          background: red;
+        }
+      }
+      "#,
+      indoc! { r#"
+      @media (resolution: 1dppx) {
+        body {
+          background: red;
+        }
+      }
+      "#},
+      Browsers {
+        chrome: Some(50 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      @media (resolution: 1dppx) {
+        body {
+          background: red;
+        }
+      }
+      "#,
+      indoc! { r#"
+      @media (resolution: 1x) {
+        body {
+          background: red;
+        }
+      }
+      "#},
+      Browsers {
+        chrome: Some(95 << 16),
+        ..Browsers::default()
+      },
+    );
+  }
+
+  #[test]
+  fn test_environment() {
+    minify_test(
+      r#"
+      @media (max-width: env(--branding-small)) {
+        body {
+          padding: env(--branding-padding);
+        }
+      }
+    "#,
+      "@media (width<=env(--branding-small)){body{padding:env(--branding-padding)}}",
+    );
+
+    minify_test(
+      r#"
+      @media (max-width: env(--branding-small 1)) {
+        body {
+          padding: env(--branding-padding 2);
+        }
+      }
+    "#,
+      "@media (width<=env(--branding-small 1)){body{padding:env(--branding-padding 2)}}",
+    );
+
+    minify_test(
+      r#"
+      @media (max-width: env(--branding-small 1, 20px)) {
+        body {
+          padding: env(--branding-padding 2, 20px);
+        }
+      }
+    "#,
+      "@media (width<=env(--branding-small 1,20px)){body{padding:env(--branding-padding 2,20px)}}",
+    );
+
+    minify_test(
+      r#"
+      @media (max-width: env(safe-area-inset-top)) {
+        body {
+          padding: env(safe-area-inset-top);
+        }
+      }
+    "#,
+      "@media (width<=env(safe-area-inset-top)){body{padding:env(safe-area-inset-top)}}",
+    );
+
+    minify_test(
+      r#"
+      @media (max-width: env(unknown)) {
+        body {
+          padding: env(unknown);
+        }
+      }
+    "#,
+      "@media (width<=env(unknown)){body{padding:env(unknown)}}",
+    );
+
+    prefix_test(
+      r#"
+      .foo {
+        color: env(--brand-color, color(display-p3 0 1 0));
+      }
+    "#,
+      indoc! {r#"
+      .foo {
+        color: env(--brand-color, #00f942);
+      }
+
+      @supports (color: color(display-p3 0 0 0)) {
+        .foo {
+          color: env(--brand-color, color(display-p3 0 1 0));
+        }
+      }
+    "#},
+      Browsers {
+        safari: Some(15 << 16),
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      @supports (color: color(display-p3 0 0 0)) {
+        .foo {
+          color: env(--brand-color, color(display-p3 0 1 0));
+        }
+      }
+    "#,
+      indoc! {r#"
+      @supports (color: color(display-p3 0 0 0)) {
+        .foo {
+          color: env(--brand-color, color(display-p3 0 1 0));
+        }
+      }
+    "#},
+      Browsers {
+        safari: Some(15 << 16),
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    css_modules_test(
+      r#"
+      @media (max-width: env(--branding-small)) {
+        .foo {
+          color: env(--brand-color);
+        }
+      }
+    "#,
+      indoc! {r#"
+      @media (width <= env(--EgL3uq_branding-small)) {
+        .EgL3uq_foo {
+          color: env(--EgL3uq_brand-color);
+        }
+      }
+    "#},
+      map! {
+        "foo" => "EgL3uq_foo",
+        "--brand-color" => "--EgL3uq_brand-color" referenced: true,
+        "--branding-small" => "--EgL3uq_branding-small" referenced: true
+      },
+      HashMap::new(),
+      crate::css_modules::Config {
+        dashed_idents: true,
+        ..Default::default()
+      },
+      false,
+    );
+  }
+
+  #[test]
+  fn test_license_comments() {
+    minify_test(
+      r#"
+      /*! Copyright 2023 Someone awesome */
+      /* Some other comment */
+      .foo {
+        color: red;
+      }
+    "#,
+      indoc! {r#"
+      /*! Copyright 2023 Someone awesome */
+      .foo{color:red}"#},
+    );
+
+    minify_test(
+      r#"
+      /*! Copyright 2023 Someone awesome */
+      /*! Copyright 2023 Someone else */
+      .foo {
+        color: red;
+      }
+    "#,
+      indoc! {r#"
+      /*! Copyright 2023 Someone awesome */
+      /*! Copyright 2023 Someone else */
+      .foo{color:red}"#},
+    );
+  }
+
+  #[test]
+  fn test_starting_style() {
+    minify_test(
+      r#"
+      @starting-style {
+        h1 {
+          background: yellow;
+        }
+      }
+      "#,
+      "@starting-style{h1{background:#ff0}}",
+    );
+    minify_test("@starting-style {}", "");
+
+    nesting_test(
+      r#"
+      h1 {
+        background: red;
+        @starting-style {
+          background: yellow;
+        }
+      }
+      "#,
+      indoc! {r#"
+      h1 {
+        background: red;
+      }
+
+      @starting-style {
+        h1 {
+          background: #ff0;
+        }
+      }
+      "#},
+    );
+  }
+
+  #[test]
+  fn test_color_scheme() {
+    minify_test(".foo { color-scheme: normal; }", ".foo{color-scheme:normal}");
+    minify_test(".foo { color-scheme: light; }", ".foo{color-scheme:light}");
+    minify_test(".foo { color-scheme: dark; }", ".foo{color-scheme:dark}");
+    minify_test(".foo { color-scheme: light dark; }", ".foo{color-scheme:light dark}");
+    minify_test(".foo { color-scheme: dark light; }", ".foo{color-scheme:light dark}");
+    minify_test(".foo { color-scheme: only light; }", ".foo{color-scheme:light only}");
+    minify_test(".foo { color-scheme: only dark; }", ".foo{color-scheme:dark only}");
+    minify_test(
+      ".foo { color-scheme: dark light only; }",
+      ".foo{color-scheme:light dark only}",
+    );
+    minify_test(".foo { color-scheme: foo bar light; }", ".foo{color-scheme:light}");
+    minify_test(
+      ".foo { color-scheme: only foo dark bar; }",
+      ".foo{color-scheme:dark only}",
+    );
+    prefix_test(
+      ".foo { color-scheme: dark; }",
+      indoc! { r#"
+      .foo {
+        --lightningcss-light: ;
+        --lightningcss-dark: initial;
+        color-scheme: dark;
+      }
+      "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      ".foo { color-scheme: light; }",
+      indoc! { r#"
+      .foo {
+        --lightningcss-light: initial;
+        --lightningcss-dark: ;
+        color-scheme: light;
+      }
+      "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      ".foo { color-scheme: light dark; }",
+      indoc! { r#"
+      .foo {
+        --lightningcss-light: initial;
+        --lightningcss-dark: ;
+        color-scheme: light dark;
+      }
+
+      @media (prefers-color-scheme: dark) {
+        .foo {
+          --lightningcss-light: ;
+          --lightningcss-dark: initial;
+        }
+      }
+      "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      ".foo { color-scheme: light dark; }",
+      indoc! { r#"
+      .foo {
+        color-scheme: light dark;
+      }
+      "#},
+      Browsers {
+        firefox: Some(120 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    minify_test(
+      ".foo { color: light-dark(yellow, red); }",
+      ".foo{color:light-dark(#ff0,red)}",
+    );
+    minify_test(
+      ".foo { color: light-dark(light-dark(yellow, red), light-dark(yellow, red)); }",
+      ".foo{color:light-dark(#ff0,red)}",
+    );
+    minify_test(
+      ".foo { color: light-dark(rgb(0, 0, 255), hsl(120deg, 50%, 50%)); }",
+      ".foo{color:light-dark(#00f,#40bf40)}",
+    );
+    prefix_test(
+      ".foo { color: light-dark(oklch(40% 0.1268735435 34.568626), oklab(59.686% 0.1009 0.1192)); }",
+      indoc! { r#"
+      .foo {
+        color: var(--lightningcss-light, #7e250f) var(--lightningcss-dark, #c65d07);
+        color: var(--lightningcss-light, lab(29.2661% 38.2437 35.3889)) var(--lightningcss-dark, lab(52.2319% 40.1449 59.9171));
+      }
+      "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      ".foo { color: light-dark(oklch(40% 0.1268735435 34.568626), oklab(59.686% 0.1009 0.1192)); }",
+      indoc! { r#"
+      .foo {
+        color: light-dark(oklch(40% .126874 34.5686), oklab(59.686% .1009 .1192));
+      }
+      "#},
+      Browsers {
+        firefox: Some(120 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        box-shadow:
+            oklch(100% 0 0deg / 50%) 0 0.63rem 0.94rem -0.19rem,
+            currentColor 0 0.44rem 0.8rem -0.58rem;
+      }
+    "#,
+      indoc! { r#"
+      .foo {
+        box-shadow: 0 .63rem .94rem -.19rem #ffffff80, 0 .44rem .8rem -.58rem;
+        box-shadow: 0 .63rem .94rem -.19rem lab(100% 0 0 / .5), 0 .44rem .8rem -.58rem;
+      }
+      "#},
+      Browsers {
+        chrome: Some(95 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      r#"
+      .foo {
+        box-shadow:
+            oklch(100% 0 0deg / 50%) 0 0.63rem 0.94rem -0.19rem,
+            currentColor 0 0.44rem 0.8rem -0.58rem;
+      }
+    "#,
+      indoc! { r#"
+      .foo {
+        box-shadow: 0 .63rem .94rem -.19rem color(display-p3 1 1 1 / .5), 0 .44rem .8rem -.58rem;
+        box-shadow: 0 .63rem .94rem -.19rem lab(100% 0 0 / .5), 0 .44rem .8rem -.58rem;
+      }
+      "#},
+      Browsers {
+        safari: Some(14 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      ".foo { color: light-dark(var(--light), var(--dark)); }",
+      indoc! { r#"
+      .foo {
+        color: var(--lightningcss-light, var(--light)) var(--lightningcss-dark, var(--dark));
+      }
+      "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      ".foo { color: rgb(from light-dark(yellow, red) r g b / 10%); }",
+      indoc! { r#"
+      .foo {
+        color: var(--lightningcss-light, #ffff001a) var(--lightningcss-dark, #ff00001a);
+      }
+      "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      ".foo { color: rgb(from light-dark(yellow, red) r g b / var(--alpha)); }",
+      indoc! { r#"
+      .foo {
+        color: var(--lightningcss-light, rgb(255 255 0 / var(--alpha))) var(--lightningcss-dark, rgb(255 0 0 / var(--alpha)));
+      }
+      "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      ".foo { color: color(from light-dark(yellow, red) srgb r g b / 10%); }",
+      indoc! { r#"
+      .foo {
+        color: var(--lightningcss-light, #ffff001a) var(--lightningcss-dark, #ff00001a);
+        color: var(--lightningcss-light, color(srgb 1 1 0 / .1)) var(--lightningcss-dark, color(srgb 1 0 0 / .1));
+      }
+      "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+    prefix_test(
+      ".foo { color: color-mix(in srgb, light-dark(yellow, red), light-dark(red, pink)); }",
+      indoc! { r#"
+      .foo {
+        color: var(--lightningcss-light, #ff8000) var(--lightningcss-dark, #ff6066);
+      }
+      "#},
+      Browsers {
+        chrome: Some(90 << 16),
+        ..Browsers::default()
+      },
+    );
+    nesting_test_with_targets(
+      r#"
+        .foo { color-scheme: light; }
+        .bar { color: light-dark(red, green); }
+      "#,
+      indoc! {r#"
+        .foo {
+          color-scheme: light;
+        }
+
+        .bar {
+          color: light-dark(red, green);
+        }
+      "#},
+      Targets {
+        browsers: Some(Browsers {
+          safari: Some(13 << 16),
+          ..Browsers::default()
+        }),
+        include: Features::empty(),
+        exclude: Features::LightDark,
+      },
+    );
+  }
+
+  #[test]
+  fn test_all() {
+    minify_test(".foo { all: initial; all: initial }", ".foo{all:initial}");
+    minify_test(".foo { all: initial; all: revert }", ".foo{all:revert}");
+    minify_test(".foo { background: red; all: revert-layer }", ".foo{all:revert-layer}");
+    minify_test(
+      ".foo { background: red; all: revert-layer; background: green }",
+      ".foo{all:revert-layer;background:green}",
+    );
+    minify_test(
+      ".foo { --test: red; all: revert-layer }",
+      ".foo{--test:red;all:revert-layer}",
+    );
+    minify_test(
+      ".foo { unicode-bidi: embed; all: revert-layer }",
+      ".foo{all:revert-layer;unicode-bidi:embed}",
+    );
+    minify_test(
+      ".foo { direction: rtl; all: revert-layer }",
+      ".foo{all:revert-layer;direction:rtl}",
+    );
+    minify_test(
+      ".foo { direction: rtl; all: revert-layer; direction: ltr }",
+      ".foo{all:revert-layer;direction:ltr}",
+    );
+    minify_test(".foo { background: var(--foo); all: unset; }", ".foo{all:unset}");
+    minify_test(
+      ".foo { all: unset; background: var(--foo); }",
+      ".foo{all:unset;background:var(--foo)}",
+    );
+    minify_test(
+      ".foo {--bar:currentcolor; --foo:1.1em; all:unset}",
+      ".foo{--bar:currentcolor;--foo:1.1em;all:unset}",
+    );
+  }
+
+  #[test]
+  fn test_view_transition() {
+    minify_test(
+      "@view-transition { navigation: auto }",
+      "@view-transition{navigation:auto}",
+    );
+    minify_test(
+      "@view-transition { navigation: auto; types: none; }",
+      "@view-transition{navigation:auto;types:none}",
+    );
+    minify_test(
+      "@view-transition { navigation: auto; types: foo bar; }",
+      "@view-transition{navigation:auto;types:foo bar}",
+    );
+    minify_test(
+      "@layer { @view-transition { navigation: auto; types: foo bar; } }",
+      "@layer{@view-transition{navigation:auto;types:foo bar}}",
+    );
+  }
+
+  #[test]
+  fn test_skip_generating_unnecessary_fallbacks() {
+    prefix_test(
+      r#"
+      @supports (color: lab(0% 0 0)) and (color: color(display-p3 0 0 0)) {
+        .foo {
+          color: lab(40% 56.6 39);
+        }
+
+        .bar {
+          color: color(display-p3 .643308 .192455 .167712);
+        }
+      }
+      "#,
+      indoc! {r#"
+      @supports (color: lab(0% 0 0)) and (color: color(display-p3 0 0 0)) {
+        .foo {
+          color: lab(40% 56.6 39);
+        }
+
+        .bar {
+          color: color(display-p3 .643308 .192455 .167712);
+        }
+      }
+      "#},
+      Browsers {
+        chrome: Some(4 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      @supports (color: lab(40% 56.6 39)) {
+        .foo {
+          color: lab(40% 56.6 39);
+        }
+      }
+      "#,
+      indoc! {r#"
+      @supports (color: lab(40% 56.6 39)) {
+        .foo {
+          color: lab(40% 56.6 39);
+        }
+      }
+      "#},
+      Browsers {
+        chrome: Some(4 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      @supports (background-color: lab(40% 56.6 39)) {
+        .foo {
+          background-color: lab(40% 56.6 39);
+        }
+      }
+      "#,
+      indoc! {r#"
+      @supports (background-color: lab(40% 56.6 39)) {
+        .foo {
+          background-color: lab(40% 56.6 39);
+        }
+      }
+      "#},
+      Browsers {
+        chrome: Some(4 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      @supports (color: light-dark(#f00, #00f)) {
+        .foo {
+          color: light-dark(#ff0, #0ff);
+        }
+      }
+      "#,
+      indoc! {r#"
+      @supports (color: light-dark(#f00, #00f)) {
+        .foo {
+          color: light-dark(#ff0, #0ff);
+        }
+      }
+      "#},
+      Browsers {
+        chrome: Some(4 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    // NOTE: fallback for lab is not necessary
+    prefix_test(
+      r#"
+      @supports (color: lab(0% 0 0)) and (not (color: color(display-p3 0 0 0))) {
+        .foo {
+          color: lab(40% 56.6 39);
+        }
+
+        .bar {
+          color: color(display-p3 .643308 .192455 .167712);
+        }
+      }
+      "#,
+      indoc! {r#"
+      @supports (color: lab(0% 0 0)) and (not (color: color(display-p3 0 0 0))) {
+        .foo {
+          color: #b32323;
+          color: lab(40% 56.6 39);
+        }
+
+        .bar {
+          color: #b32323;
+          color: color(display-p3 .643308 .192455 .167712);
+        }
+      }
+      "#},
+      Browsers {
+        chrome: Some(4 << 16),
+        ..Browsers::default()
+      },
+    );
+
+    prefix_test(
+      r#"
+      @supports (color: lab(0% 0 0)) or (color: color(display-p3 0 0 0)) {
+        .foo {
+          color: lab(40% 56.6 39);
+        }
+
+        .bar {
+          color: color(display-p3 .643308 .192455 .167712);
+        }
+      }
+      "#,
+      indoc! {r#"
+      @supports (color: lab(0% 0 0)) or (color: color(display-p3 0 0 0)) {
+        .foo {
+          color: #b32323;
+          color: lab(40% 56.6 39);
+        }
+
+        .bar {
+          color: #b32323;
+          color: color(display-p3 .643308 .192455 .167712);
+        }
+      }
+      "#},
+      Browsers {
+        chrome: Some(4 << 16),
+        ..Browsers::default()
+      },
+    );
+  }
+}
diff --git a/src/logical.rs b/src/logical.rs
new file mode 100644
index 0000000..f662d2f
--- /dev/null
+++ b/src/logical.rs
@@ -0,0 +1,27 @@
+#[derive(Debug, PartialEq)]
+pub enum PropertyCategory {
+  Logical,
+  Physical,
+}
+
+impl Default for PropertyCategory {
+  fn default() -> PropertyCategory {
+    PropertyCategory::Physical
+  }
+}
+
+#[derive(PartialEq)]
+pub enum LogicalGroup {
+  BorderColor,
+  BorderStyle,
+  BorderWidth,
+  BorderRadius,
+  Margin,
+  ScrollMargin,
+  Padding,
+  ScrollPadding,
+  Inset,
+  Size,
+  MinSize,
+  MaxSize,
+}
diff --git a/src/macros.rs b/src/macros.rs
new file mode 100644
index 0000000..8019c58
--- /dev/null
+++ b/src/macros.rs
@@ -0,0 +1,848 @@
+macro_rules! enum_property {
+  (
+    $(#[$outer:meta])*
+    $vis:vis enum $name:ident {
+      $(
+        $(#[$meta: meta])*
+        $x: ident,
+      )+
+    }
+  ) => {
+    #[derive(Debug, Clone, Copy, PartialEq, Parse, ToCss)]
+    #[cfg_attr(feature = "visitor", derive(Visit))]
+    #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(rename_all = "kebab-case"))]
+    #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+    #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+    $(#[$outer])*
+    $vis enum $name {
+      $(
+        $(#[$meta])*
+        $x,
+      )+
+    }
+
+    impl $name {
+      /// Returns a string representation of the value.
+      pub fn as_str(&self) -> &str {
+        use $name::*;
+        match self {
+          $(
+            $x => const_str::convert_ascii_case!(kebab, stringify!($x)),
+          )+
+        }
+      }
+    }
+  };
+  (
+    $(#[$outer:meta])*
+    $vis:vis enum $name:ident {
+      $(
+        $(#[$meta: meta])*
+        $str: literal: $id: ident,
+      )+
+    }
+  ) => {
+    $(#[$outer])*
+    #[derive(Debug, Clone, Copy, PartialEq)] #[cfg_attr(feature = "visitor", derive(Visit))]
+    #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+    #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+    #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+    $vis enum $name {
+      $(
+        $(#[$meta])*
+        #[cfg_attr(feature = "serde", serde(rename = $str))]
+        $id,
+      )+
+    }
+
+    impl<'i> Parse<'i> for $name {
+      fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+        let location = input.current_source_location();
+        let ident = input.expect_ident()?;
+        cssparser::match_ignore_ascii_case! { &*ident,
+          $(
+            $str => Ok($name::$id),
+          )+
+          _ => Err(location.new_unexpected_token_error(
+            cssparser::Token::Ident(ident.clone())
+          )),
+        }
+      }
+    }
+
+    impl $name {
+      /// Returns a string representation of the value.
+      pub fn as_str(&self) -> &str {
+        use $name::*;
+        match self {
+          $(
+            $id => $str,
+          )+
+        }
+      }
+    }
+
+    impl ToCss for $name {
+      fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError> where W: std::fmt::Write {
+        dest.write_str(self.as_str())
+      }
+    }
+  };
+}
+
+pub(crate) use enum_property;
+
+macro_rules! shorthand_property {
+  (
+    $(#[$outer:meta])*
+    $vis:vis struct $name: ident$(<$l: lifetime>)? {
+      $(#[$first_meta: meta])*
+      $first_key: ident: $first_prop: ident($first_type: ty $(, $first_vp: ty)?),
+      $(
+        $(#[$meta: meta])*
+        $key: ident: $prop: ident($type: ty $(, $vp: ty)?),
+      )*
+    }
+  ) => {
+    define_shorthand! {
+      $(#[$outer])*
+      pub struct $name$(<$l>)? {
+        $(#[$first_meta])*
+        $first_key: $first_prop($first_type $($first_vp)?),
+        $(
+          $(#[$meta])*
+          $key: $prop($type $($vp)?),
+        )*
+      }
+    }
+
+    impl<'i> Parse<'i> for $name$(<$l>)? {
+      fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+        let mut $first_key = None;
+        $(
+          let mut $key = None;
+        )*
+
+        macro_rules! parse_one {
+          ($k: ident, $t: ty) => {
+            if $k.is_none() {
+              if let Ok(val) = input.try_parse(<$t>::parse) {
+                $k = Some(val);
+                continue
+              }
+            }
+          };
+        }
+
+        loop {
+          parse_one!($first_key, $first_type);
+          $(
+            parse_one!($key, $type);
+          )*
+          break
+        }
+
+        Ok($name {
+          $first_key: $first_key.unwrap_or_default(),
+          $(
+            $key: $key.unwrap_or_default(),
+          )*
+        })
+      }
+    }
+
+    impl$(<$l>)? ToCss for $name$(<$l>)? {
+      fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError> where W: std::fmt::Write {
+        let mut needs_space = false;
+        macro_rules! print_one {
+          ($k: ident, $t: ty) => {
+            if self.$k != <$t>::default() {
+              if needs_space {
+                dest.write_char(' ')?;
+              }
+              self.$k.to_css(dest)?;
+              needs_space = true;
+            }
+          };
+        }
+
+        print_one!($first_key, $first_type);
+        $(
+          print_one!($key, $type);
+        )*
+        if !needs_space {
+          self.$first_key.to_css(dest)?;
+        }
+        Ok(())
+      }
+    }
+  };
+}
+
+pub(crate) use shorthand_property;
+
+macro_rules! shorthand_property_bitflags {
+  ($name:ident, $first:ident, $($rest:ident),*) => {
+    crate::macros::shorthand_property_bitflags!($name, [$first,$($rest),+] $($rest),+ ; 0; $first = 0);
+  };
+  ($name:ident, [$($all:ident),*] $cur:ident, $($rest:ident),* ; $last_index: expr ; $($var:ident = $index:expr)+) => {
+    crate::macros::shorthand_property_bitflags!($name, [$($all),*] $($rest),* ; $last_index + 1; $($var = $index)* $cur = $last_index + 1);
+  };
+  ($name:ident, [$($all:ident),*] $cur:ident; $last_index:expr ; $($var:ident = $index:expr)+) => {
+    paste::paste! {
+      crate::macros::property_bitflags! {
+        #[derive(Default, Debug)]
+        struct [<$name Property>]: u8 {
+          $(const $var = 1 << $index);*;
+          const $cur = 1 << ($last_index + 1);
+          const $name = $(Self::$all.bits())|*;
+        }
+      }
+    }
+  };
+}
+
+pub(crate) use shorthand_property_bitflags;
+
+macro_rules! shorthand_handler {
+  (
+    $name: ident -> $shorthand: ident$(<$l: lifetime>)? $(fallbacks: $shorthand_fallback: literal)?
+    { $( $key: ident: $prop: ident($type: ty $(, fallback: $fallback: literal)? $(, image: $image: literal)?), )+ }
+  ) => {
+    crate::macros::shorthand_property_bitflags!($shorthand, $($prop),*);
+
+    #[derive(Default)]
+    pub(crate) struct $name$(<$l>)? {
+      $(
+        pub $key: Option<$type>,
+      )*
+      flushed_properties: paste::paste!([<$shorthand Property>]),
+      has_any: bool
+    }
+
+    impl<'i> PropertyHandler<'i> for $name$(<$l>)? {
+      fn handle_property(&mut self, property: &Property<'i>, dest: &mut DeclarationList<'i>, context: &mut PropertyHandlerContext<'i, '_>) -> bool {
+        use crate::traits::IsCompatible;
+
+        match property {
+          $(
+            Property::$prop(val) => {
+              if self.$key.is_some() && matches!(context.targets.browsers, Some(targets) if !val.is_compatible(targets)) {
+                self.flush(dest, context);
+              }
+              self.$key = Some(val.clone());
+              self.has_any = true;
+            },
+          )+
+          Property::$shorthand(val) => {
+            $(
+              if self.$key.is_some() && matches!(context.targets.browsers, Some(targets) if !val.$key.is_compatible(targets)) {
+                self.flush(dest, context);
+              }
+            )+
+            $(
+              self.$key = Some(val.$key.clone());
+            )+
+            self.has_any = true;
+          }
+          Property::Unparsed(val) if matches!(val.property_id, $( PropertyId::$prop | )+ PropertyId::$shorthand) => {
+            self.flush(dest, context);
+
+            let mut unparsed = val.clone();
+            context.add_unparsed_fallbacks(&mut unparsed);
+            paste::paste! {
+              self.flushed_properties.insert([<$shorthand Property>]::try_from(&unparsed.property_id).unwrap());
+            };
+            dest.push(Property::Unparsed(unparsed));
+          }
+          _ => return false
+        }
+
+        true
+      }
+
+      fn finalize(&mut self, dest: &mut DeclarationList<'i>, context: &mut PropertyHandlerContext<'i, '_>) {
+        self.flush(dest, context);
+        self.flushed_properties = paste::paste!([<$shorthand Property>]::empty());
+      }
+    }
+
+    impl<'i> $name$(<$l>)? {
+      #[allow(unused_variables)]
+      fn flush(&mut self, dest: &mut DeclarationList<'i>, context: &mut PropertyHandlerContext<'i, '_>) {
+        if !self.has_any {
+          return
+        }
+
+        self.has_any = false;
+
+        $(
+          let $key = std::mem::take(&mut self.$key);
+        )+
+
+        if $( $key.is_some() && )* true {
+          #[allow(unused_mut)]
+          let mut shorthand = $shorthand {
+            $(
+              $key: $key.unwrap(),
+            )+
+          };
+
+          $(
+            if $shorthand_fallback && !self.flushed_properties.intersects(paste::paste!([<$shorthand Property>]::$shorthand)) {
+              let fallbacks = shorthand.get_fallbacks(context.targets);
+              for fallback in fallbacks {
+                dest.push(Property::$shorthand(fallback));
+              }
+            }
+          )?
+
+          dest.push(Property::$shorthand(shorthand));
+          paste::paste! {
+            self.flushed_properties.insert([<$shorthand Property>]::$shorthand);
+          };
+        } else {
+          $(
+            #[allow(unused_mut)]
+            if let Some(mut val) = $key {
+              $(
+                if $fallback && !self.flushed_properties.intersects(paste::paste!([<$shorthand Property>]::$prop)) {
+                  let fallbacks = val.get_fallbacks(context.targets);
+                  for fallback in fallbacks {
+                    dest.push(Property::$prop(fallback));
+                  }
+                }
+              )?
+
+              dest.push(Property::$prop(val));
+              paste::paste! {
+                self.flushed_properties.insert([<$shorthand Property>]::$prop);
+              };
+            }
+          )+
+        }
+      }
+    }
+  };
+}
+
+pub(crate) use shorthand_handler;
+
+macro_rules! define_shorthand {
+  (
+    $(#[$outer:meta])*
+    $vis:vis struct $name: ident$(<$l: lifetime>)?$(($prefix: ty))? {
+      $(
+        $(#[$meta: meta])*
+        $key: ident: $prop: ident($type: ty $(, $vp: ty)?),
+      )+
+    }
+  ) => {
+    $(#[$outer])*
+    #[derive(Debug, Clone, PartialEq)]
+    #[cfg_attr(feature = "visitor", derive(Visit))]
+    #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(rename_all = "camelCase"))]
+    #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+    #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+    pub struct $name$(<$l>)? {
+      $(
+        $(#[$meta])*
+        pub $key: $type,
+      )+
+    }
+
+    crate::macros::impl_shorthand! {
+      $name($name$(<$l>)? $(, $prefix)?) {
+        $(
+          $key: [ $prop$(($vp))?, ],
+        )+
+      }
+    }
+  };
+}
+
+pub(crate) use define_shorthand;
+
+macro_rules! impl_shorthand {
+  (
+    $name: ident($t: ty $(, $prefix: ty)?) {
+      $(
+        $key: ident: [ $( $prop: ident$(($vp: ty))? $(,)?)+ ],
+      )+
+    }
+
+    $(
+      fn is_valid($v: ident) {
+        $($body: tt)+
+      }
+    )?
+  ) => {
+    #[allow(unused_macros)]
+    macro_rules! vp_name {
+      ($x: ty, $n: ident) => {
+        $n
+      };
+      ($x: ty, $n: expr) => {
+        $n
+      };
+    }
+
+    impl<'i> Shorthand<'i> for $t {
+      #[allow(unused_variables)]
+      fn from_longhands(decls: &DeclarationBlock<'i>, vendor_prefix: crate::vendor_prefix::VendorPrefix) -> Option<(Self, bool)> {
+        use paste::paste;
+
+        $(
+          $(
+            paste! {
+              let mut [<$prop:snake _value>] = None;
+            }
+          )+
+        )+
+
+        let mut count = 0;
+        let mut important_count = 0;
+        for (property, important) in decls.iter() {
+          match property {
+            $(
+              $(
+                Property::$prop(val $(, vp_name!($vp, p))?) => {
+                  $(
+                    if *vp_name!($vp, p) != vendor_prefix {
+                      return None
+                    }
+                  )?
+
+                  paste! {
+                    [<$prop:snake _value>] = Some(val.clone());
+                  }
+                  count += 1;
+                  if important {
+                    important_count += 1;
+                  }
+                }
+              )+
+            )+
+            Property::$name(val $(, vp_name!($prefix, p))?) => {
+              $(
+                if *vp_name!($prefix, p) != vendor_prefix {
+                  return None
+                }
+              )?
+
+              $(
+                $(
+                  paste! {
+                    [<$prop:snake _value>] = Some(val.$key.clone());
+                  }
+                  count += 1;
+                  if important {
+                    important_count += 1;
+                  }
+                )+
+              )+
+            }
+            _ => {
+              $(
+                $(
+                  if let Some(Property::$prop(longhand $(, vp_name!($vp, _p))?)) = property.longhand(&PropertyId::$prop$((vp_name!($vp, vendor_prefix)))?) {
+                    paste! {
+                      [<$prop:snake _value>] = Some(longhand);
+                    }
+                    count += 1;
+                    if important {
+                      important_count += 1;
+                    }
+                  }
+                )+
+              )+
+            }
+          }
+        }
+
+        // !important flags must match to produce a shorthand.
+        if important_count > 0 && important_count != count {
+          return None
+        }
+
+        if $($(paste! { [<$prop:snake _value>].is_some() } &&)+)+ true {
+          // All properties in the group must have a matching value to produce a shorthand.
+          $(
+            let mut $key = None;
+            $(
+              if $key == None {
+                paste! {
+                  $key = [<$prop:snake _value>];
+                }
+              } else if paste! { $key != [<$prop:snake _value>] } {
+                return None
+              }
+            )+
+          )+
+
+          let value = $name {
+            $(
+              $key: $key.unwrap(),
+            )+
+          };
+
+          $(
+            #[inline]
+            fn is_valid($v: &$name) -> bool {
+              $($body)+
+            }
+
+            if !is_valid(&value) {
+              return None
+            }
+          )?
+
+          return Some((value, important_count > 0));
+        }
+
+        None
+      }
+
+      #[allow(unused_variables)]
+      fn longhands(vendor_prefix: crate::vendor_prefix::VendorPrefix) -> Vec<PropertyId<'static>> {
+        vec![$($(PropertyId::$prop$((vp_name!($vp, vendor_prefix)))?, )+)+]
+      }
+
+      fn longhand(&self, property_id: &PropertyId) -> Option<Property<'i>> {
+        match property_id {
+          $(
+            $(
+              PropertyId::$prop$((vp_name!($vp, p)))? => {
+                Some(Property::$prop(self.$key.clone() $(, *vp_name!($vp, p))?))
+              }
+            )+
+          )+
+          _ => None
+        }
+      }
+
+      fn set_longhand(&mut self, property: &Property<'i>) -> Result<(), ()> {
+        macro_rules! count {
+          ($p: ident) => {
+            1
+          }
+        }
+
+        $(
+          #[allow(non_upper_case_globals)]
+          const $key: u8 = 0 $( + count!($prop))+;
+        )+
+
+        match property {
+          $(
+            $(
+              Property::$prop(val $(, vp_name!($vp, _p))?) => {
+                // If more than one longhand maps to this key, bail.
+                if $key > 1 {
+                  return Err(())
+                }
+                self.$key = val.clone();
+                return Ok(())
+              }
+            )+
+          )+
+          _ => {}
+        }
+        Err(())
+      }
+    }
+  }
+}
+
+pub(crate) use impl_shorthand;
+
+macro_rules! define_list_shorthand {
+  (
+    $(#[$outer:meta])*
+    $vis:vis struct $name: ident$(<$l: lifetime>)?$(($prefix: ty))? {
+      $(
+        $(#[$meta: meta])*
+        $key: ident: $prop: ident($type: ty $(, $vp: ty)?),
+      )+
+    }
+  ) => {
+    $(#[$outer])*
+    #[derive(Debug, Clone, PartialEq)]
+    #[cfg_attr(feature = "visitor", derive(Visit))]
+    #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(rename_all = "camelCase"))]
+    #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+    #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+    pub struct $name$(<$l>)? {
+      $(
+        $(#[$meta])*
+        pub $key: $type,
+      )+
+    }
+
+    #[allow(unused_macros)]
+    macro_rules! vp_name {
+      ($x: ty, $n: ident) => {
+        $n
+      };
+      ($x: ty, $n: expr) => {
+        $n
+      };
+    }
+
+    impl<'i> Shorthand<'i> for SmallVec<[$name$(<$l>)?; 1]> {
+      #[allow(unused_variables)]
+      fn from_longhands(decls: &DeclarationBlock<'i>, vendor_prefix: crate::vendor_prefix::VendorPrefix) -> Option<(Self, bool)> {
+        $(
+          let mut $key = None;
+        )+
+
+        let mut count = 0;
+        let mut important_count = 0;
+        let mut length = None;
+        for (property, important) in decls.iter() {
+          let mut len = 0;
+          match property {
+            $(
+              Property::$prop(val $(, vp_name!($vp, p))?) => {
+                $(
+                  if *vp_name!($vp, p) != vendor_prefix {
+                    return None
+                  }
+                )?
+
+                $key = Some(val.clone());
+                len = val.len();
+                count += 1;
+                if important {
+                  important_count += 1;
+                }
+              }
+            )+
+            Property::$name(val $(, vp_name!($prefix, p))?) => {
+              $(
+                if *vp_name!($prefix, p) != vendor_prefix {
+                  return None
+                }
+              )?
+              $(
+                $key = Some(val.iter().map(|b| b.$key.clone()).collect());
+              )+
+              len = val.len();
+              count += 1;
+              if important {
+                important_count += 1;
+              }
+            }
+            _ => {
+              $(
+                if let Some(Property::$prop(longhand $(, vp_name!($vp, _p))?)) = property.longhand(&PropertyId::$prop$((vp_name!($vp, vendor_prefix)))?) {
+                  len = longhand.len();
+                  $key = Some(longhand);
+                  count += 1;
+                  if important {
+                    important_count += 1;
+                  }
+                }
+              )+
+            }
+          }
+
+          // Lengths must be equal.
+          if length.is_none() {
+            length = Some(len);
+          } else if length.unwrap() != len {
+            return None
+          }
+        }
+
+        // !important flags must match to produce a shorthand.
+        if important_count > 0 && important_count != count {
+          return None
+        }
+
+        if $($key.is_some() &&)+ true {
+          let values = izip!(
+            $(
+              $key.unwrap().drain(..),
+            )+
+          ).map(|($($key,)+)| {
+            $name {
+              $(
+                $key,
+              )+
+            }
+          }).collect();
+          return Some((values, important_count > 0))
+        }
+
+        None
+      }
+
+      #[allow(unused_variables)]
+      fn longhands(vendor_prefix: crate::vendor_prefix::VendorPrefix) -> Vec<PropertyId<'static>> {
+        vec![$(PropertyId::$prop$((vp_name!($vp, vendor_prefix)))?, )+]
+      }
+
+      fn longhand(&self, property_id: &PropertyId) -> Option<Property<'i>> {
+        match property_id {
+          $(
+            PropertyId::$prop$((vp_name!($vp, p)))? => {
+              Some(Property::$prop(self.iter().map(|v| v.$key.clone()).collect() $(, *vp_name!($vp, p))?))
+            }
+          )+
+          _ => None
+        }
+      }
+
+      fn set_longhand(&mut self, property: &Property<'i>) -> Result<(), ()> {
+        match property {
+          $(
+            Property::$prop(val $(, vp_name!($vp, _p))?) => {
+              if val.len() != self.len() {
+                return Err(())
+              }
+
+              for (i, item) in self.iter_mut().enumerate() {
+                item.$key = val[i].clone();
+              }
+              return Ok(())
+            }
+          )+
+          _ => {}
+        }
+        Err(())
+      }
+    }
+  };
+}
+
+pub(crate) use define_list_shorthand;
+
+macro_rules! rect_shorthand {
+  (
+    $(#[$meta: meta])*
+    $vis:vis struct $name: ident<$t: ty> {
+      $top: ident,
+      $right: ident,
+      $bottom: ident,
+      $left: ident
+    }
+  ) => {
+    define_shorthand! {
+      $(#[$meta])*
+      pub struct $name {
+        /// The top value.
+        top: $top($t),
+        /// The right value.
+        right: $right($t),
+        /// The bottom value.
+        bottom: $bottom($t),
+        /// The left value.
+        left: $left($t),
+      }
+    }
+
+    impl<'i> Parse<'i> for $name {
+      fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+        let rect = Rect::parse(input)?;
+        Ok(Self {
+          top: rect.0,
+          right: rect.1,
+          bottom: rect.2,
+          left: rect.3,
+        })
+      }
+    }
+
+    impl ToCss for $name {
+      fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+      where
+        W: std::fmt::Write,
+      {
+        Rect::new(&self.top, &self.right, &self.bottom, &self.left).to_css(dest)
+      }
+    }
+  };
+}
+
+pub(crate) use rect_shorthand;
+
+macro_rules! size_shorthand {
+  (
+    $(#[$outer:meta])*
+    $vis:vis struct $name: ident<$t: ty> {
+      $(#[$a_meta: meta])*
+      $a_key: ident: $a_prop: ident,
+      $(#[$b_meta: meta])*
+      $b_key: ident: $b_prop: ident,
+    }
+  ) => {
+    define_shorthand! {
+      $(#[$outer])*
+      $vis struct $name {
+        $(#[$a_meta])*
+        $a_key: $a_prop($t),
+        $(#[$b_meta])*
+        $b_key: $b_prop($t),
+      }
+    }
+
+    impl<'i> Parse<'i> for $name {
+      fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+        let size = Size2D::parse(input)?;
+        Ok(Self {
+          $a_key: size.0,
+          $b_key: size.1,
+        })
+      }
+    }
+
+    impl ToCss for $name {
+      fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+      where
+        W: std::fmt::Write,
+      {
+        Size2D(&self.$a_key, &self.$b_key).to_css(dest)
+      }
+    }
+  };
+}
+
+pub(crate) use size_shorthand;
+
+macro_rules! property_bitflags {
+  (
+    $(#[$outer:meta])*
+    $vis:vis struct $BitFlags:ident: $T:ty {
+      $(
+        $(#[$inner:ident $($args:tt)*])*
+        const $Flag:ident $(($vp:ident))? = $value:expr;
+      )*
+    }
+  ) => {
+    bitflags::bitflags! {
+      $(#[$outer])*
+      $vis struct $BitFlags: $T {
+        $(
+          $(#[$inner $($args)*])*
+            const $Flag = $value;
+        )*
+      }
+    }
+
+    impl<'i> TryFrom<&PropertyId<'i>> for $BitFlags {
+      type Error = ();
+
+      fn try_from(value: &PropertyId<'i>) -> Result<$BitFlags, Self::Error> {
+        match value {
+          $(
+            PropertyId::$Flag $(($vp))? => Ok($BitFlags::$Flag),
+          )*
+          _ => Err(())
+        }
+      }
+    }
+  };
+}
+
+pub(crate) use property_bitflags;
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..ff7216a
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,297 @@
+use atty::Stream;
+use clap::{ArgGroup, Parser};
+use lightningcss::bundler::{Bundler, FileProvider};
+use lightningcss::stylesheet::{MinifyOptions, ParserFlags, ParserOptions, PrinterOptions, StyleSheet};
+use lightningcss::targets::Browsers;
+use parcel_sourcemap::SourceMap;
+use serde::Serialize;
+use std::borrow::Cow;
+use std::sync::{Arc, RwLock};
+use std::{ffi, fs, io, path::Path};
+
+#[cfg(target_os = "macos")]
+#[global_allocator]
+static GLOBAL: jemallocator::Jemalloc = jemallocator::Jemalloc;
+
+#[derive(Parser, Debug)]
+#[clap(author, version, about, long_about = None)]
+#[clap(group(
+  ArgGroup::new("targets-resolution")
+      .args(&["targets", "browserslist"]),
+))]
+struct CliArgs {
+  /// Target CSS file (default: stdin)
+  #[clap(value_parser)]
+  input_file: Vec<String>,
+  /// Destination file for the output
+  #[clap(short, long, group = "output_file", value_parser)]
+  output_file: Option<String>,
+  /// Destination directory to output into.
+  #[clap(short = 'd', long, group = "output_file", value_parser)]
+  output_dir: Option<String>,
+  /// Minify the output
+  #[clap(short, long, value_parser)]
+  minify: bool,
+  /// Enable parsing CSS nesting
+  // Now on by default, but left for backward compatibility.
+  #[clap(long, value_parser, hide = true)]
+  nesting: bool,
+  /// Enable parsing custom media queries
+  #[clap(long, value_parser)]
+  custom_media: bool,
+  /// Enable CSS modules in output.
+  /// If no filename is provided, <output_file>.json will be used.
+  /// If no --output-file is specified, code and exports will be printed to stdout as JSON.
+  #[clap(long, group = "css_modules", value_parser)]
+  css_modules: Option<Option<String>>,
+  #[clap(long, requires = "css_modules", value_parser)]
+  css_modules_pattern: Option<String>,
+  #[clap(long, requires = "css_modules", value_parser)]
+  css_modules_dashed_idents: bool,
+  /// Enable sourcemap, at <output_file>.map
+  #[clap(long, requires = "output_file", value_parser)]
+  sourcemap: bool,
+  #[clap(long, value_parser)]
+  bundle: bool,
+  #[clap(short, long, value_parser)]
+  targets: Vec<String>,
+  #[clap(long, value_parser)]
+  browserslist: bool,
+  #[clap(long, value_parser)]
+  error_recovery: bool,
+}
+
+#[derive(Serialize)]
+#[serde(rename_all = "camelCase")]
+struct SourceMapJson<'a> {
+  version: u8,
+  mappings: String,
+  sources: &'a Vec<String>,
+  sources_content: &'a Vec<String>,
+  names: &'a Vec<String>,
+}
+
+pub fn main() -> Result<(), std::io::Error> {
+  let cli_args = CliArgs::parse();
+  let project_root = std::env::current_dir()?;
+
+  // If we're given an input file, read from it and adjust its name.
+  //
+  // If we're not given an input file and stdin was redirected, read
+  // from it and create a fake name. Return an error if stdin was not
+  // redirected (otherwise the program will hang waiting for input).
+  //
+  let inputs = if !cli_args.input_file.is_empty() {
+    if cli_args.input_file.len() > 1 && cli_args.output_file.is_some() {
+      eprintln!("Cannot use the --output-file option with multiple inputs. Use --output-dir instead.");
+      std::process::exit(1);
+    }
+
+    if cli_args.input_file.len() > 1 && cli_args.output_file.is_none() && cli_args.output_dir.is_none() {
+      eprintln!("Cannot output to stdout with multiple inputs. Use --output-dir instead.");
+      std::process::exit(1);
+    }
+
+    cli_args
+      .input_file
+      .into_iter()
+      .map(|ref f| -> Result<_, std::io::Error> {
+        let absolute_path = fs::canonicalize(f)?;
+        let filename = pathdiff::diff_paths(absolute_path, &project_root).unwrap();
+        let filename = filename.to_string_lossy().into_owned();
+        let contents = fs::read_to_string(f)?;
+        Ok((filename, contents))
+      })
+      .collect::<Result<_, _>>()?
+  } else {
+    // Don't silently wait for input if stdin was not redirected.
+    if atty::is(Stream::Stdin) {
+      return Err(io::Error::new(
+        io::ErrorKind::Other,
+        "Not reading from stdin as it was not redirected",
+      ));
+    }
+    let filename = format!("stdin-{}", std::process::id());
+    let contents = io::read_to_string(io::stdin())?;
+    vec![(filename, contents)]
+  };
+
+  let css_modules = if let Some(_) = cli_args.css_modules {
+    let pattern = if let Some(pattern) = cli_args.css_modules_pattern.as_ref() {
+      match lightningcss::css_modules::Pattern::parse(pattern) {
+        Ok(p) => p,
+        Err(e) => {
+          eprintln!("{}", e);
+          std::process::exit(1);
+        }
+      }
+    } else {
+      Default::default()
+    };
+
+    Some(lightningcss::css_modules::Config {
+      pattern,
+      dashed_idents: cli_args.css_modules_dashed_idents,
+      ..Default::default()
+    })
+  } else {
+    cli_args.css_modules.as_ref().map(|_| Default::default())
+  };
+
+  let fs = FileProvider::new();
+
+  for (filename, source) in inputs {
+    let warnings = if cli_args.error_recovery {
+      Some(Arc::new(RwLock::new(Vec::new())))
+    } else {
+      None
+    };
+
+    let mut source_map = if cli_args.sourcemap {
+      Some(SourceMap::new(&project_root.to_string_lossy()))
+    } else {
+      None
+    };
+
+    let output_file = if let Some(output_file) = &cli_args.output_file {
+      Some(Cow::Borrowed(Path::new(output_file)))
+    } else if let Some(dir) = &cli_args.output_dir {
+      Some(Cow::Owned(
+        Path::new(dir).join(Path::new(&filename).file_name().unwrap()),
+      ))
+    } else {
+      None
+    };
+
+    let res = {
+      let mut flags = ParserFlags::empty();
+      flags.set(ParserFlags::CUSTOM_MEDIA, cli_args.custom_media);
+
+      let mut options = ParserOptions {
+        flags,
+        css_modules: css_modules.clone(),
+        error_recovery: cli_args.error_recovery,
+        warnings: warnings.clone(),
+        ..ParserOptions::default()
+      };
+
+      let mut stylesheet = if cli_args.bundle {
+        let mut bundler = Bundler::new(&fs, source_map.as_mut(), options);
+        bundler.bundle(Path::new(&filename)).unwrap()
+      } else {
+        if let Some(sm) = &mut source_map {
+          sm.add_source(&filename);
+          let _ = sm.set_source_content(0, &source);
+        }
+        options.filename = filename;
+        StyleSheet::parse(&source, options).unwrap()
+      };
+
+      let targets = if !cli_args.targets.is_empty() {
+        Browsers::from_browserslist(&cli_args.targets).unwrap()
+      } else if cli_args.browserslist {
+        Browsers::load_browserslist().unwrap()
+      } else {
+        None
+      }
+      .into();
+
+      stylesheet
+        .minify(MinifyOptions {
+          targets,
+          ..MinifyOptions::default()
+        })
+        .unwrap();
+
+      stylesheet
+        .to_css(PrinterOptions {
+          minify: cli_args.minify,
+          source_map: source_map.as_mut(),
+          project_root: Some(&project_root.to_string_lossy()),
+          targets,
+          ..PrinterOptions::default()
+        })
+        .unwrap()
+    };
+
+    let map = if let Some(ref mut source_map) = source_map {
+      let mut vlq_output: Vec<u8> = Vec::new();
+      source_map
+        .write_vlq(&mut vlq_output)
+        .map_err(|_| io::Error::new(io::ErrorKind::Other, "Error writing sourcemap vlq"))?;
+
+      let sm = SourceMapJson {
+        version: 3,
+        mappings: unsafe { String::from_utf8_unchecked(vlq_output) },
+        sources: source_map.get_sources(),
+        sources_content: source_map.get_sources_content(),
+        names: source_map.get_names(),
+      };
+
+      serde_json::to_vec(&sm).ok()
+    } else {
+      None
+    };
+
+    if let Some(warnings) = warnings {
+      let warnings = Arc::try_unwrap(warnings).unwrap().into_inner().unwrap();
+      for warning in warnings {
+        eprintln!("{}", warning);
+      }
+    }
+
+    if let Some(output_file) = &output_file {
+      let mut code = res.code;
+      if cli_args.sourcemap {
+        if let Some(map_buf) = map {
+          let map_filename = output_file.to_string_lossy() + ".map";
+          code += &format!("\n/*# sourceMappingURL={} */\n", map_filename);
+          fs::write(map_filename.as_ref(), map_buf)?;
+        }
+      }
+
+      if let Some(p) = output_file.parent() {
+        fs::create_dir_all(p)?
+      };
+      fs::write(output_file, code.as_bytes())?;
+
+      if let Some(css_modules) = &cli_args.css_modules {
+        let css_modules_filename = if let Some(name) = css_modules {
+          Cow::Borrowed(name)
+        } else {
+          Cow::Owned(infer_css_modules_filename(output_file.as_ref())?)
+        };
+        if let Some(exports) = res.exports {
+          let css_modules_json = serde_json::to_string(&exports)?;
+          fs::write(css_modules_filename.as_ref(), css_modules_json)?;
+        }
+      }
+    } else {
+      if let Some(exports) = res.exports {
+        println!(
+          "{}",
+          serde_json::json!({
+            "code": res.code,
+            "exports": exports
+          })
+        );
+      } else {
+        println!("{}", res.code);
+      }
+    }
+  }
+
+  Ok(())
+}
+
+fn infer_css_modules_filename(path: &Path) -> Result<String, std::io::Error> {
+  if path.extension() == Some(ffi::OsStr::new("json")) {
+    Err(io::Error::new(
+      io::ErrorKind::Other,
+      "Cannot infer a css modules json filename, since the output file extension is '.json'",
+    ))
+  } else {
+    // unwrap: the filename option is a String from clap, so is valid utf-8
+    Ok(path.with_extension("json").to_str().unwrap().into())
+  }
+}
diff --git a/src/media_query.rs b/src/media_query.rs
new file mode 100644
index 0000000..10657c1
--- /dev/null
+++ b/src/media_query.rs
@@ -0,0 +1,1919 @@
+//! Media queries.
+use crate::error::{ErrorWithLocation, MinifyError, MinifyErrorKind, ParserError, PrinterError};
+use crate::macros::enum_property;
+use crate::parser::starts_with_ignore_ascii_case;
+use crate::printer::Printer;
+use crate::properties::custom::EnvironmentVariable;
+#[cfg(feature = "visitor")]
+use crate::rules::container::ContainerSizeFeatureId;
+use crate::rules::custom_media::CustomMediaRule;
+use crate::rules::Location;
+use crate::stylesheet::ParserOptions;
+use crate::targets::{should_compile, Targets};
+use crate::traits::{Parse, ParseWithOptions, ToCss};
+use crate::values::ident::{DashedIdent, Ident};
+use crate::values::number::{CSSInteger, CSSNumber};
+use crate::values::string::CowArcStr;
+use crate::values::{length::Length, ratio::Ratio, resolution::Resolution};
+use crate::vendor_prefix::VendorPrefix;
+#[cfg(feature = "visitor")]
+use crate::visitor::Visit;
+use bitflags::bitflags;
+use cssparser::*;
+#[cfg(feature = "into_owned")]
+use static_self::IntoOwned;
+use std::borrow::Cow;
+use std::collections::{HashMap, HashSet};
+
+#[cfg(feature = "serde")]
+use crate::serialization::ValueWrapper;
+
+/// A [media query list](https://drafts.csswg.org/mediaqueries/#mq-list).
+#[derive(Clone, Debug, PartialEq, Default)]
+#[cfg_attr(feature = "visitor", derive(Visit), visit(visit_media_list, MEDIA_QUERIES))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(rename_all = "camelCase")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub struct MediaList<'i> {
+  /// The list of media queries.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  pub media_queries: Vec<MediaQuery<'i>>,
+}
+
+impl<'i> MediaList<'i> {
+  /// Creates an empty media query list.
+  pub fn new() -> Self {
+    MediaList { media_queries: vec![] }
+  }
+
+  /// Parse a media query list from CSS.
+  pub fn parse<'t>(
+    input: &mut Parser<'i, 't>,
+    options: &ParserOptions<'_, 'i>,
+  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let mut media_queries = vec![];
+    loop {
+      match input.parse_until_before(Delimiter::Comma, |i| MediaQuery::parse_with_options(i, options)) {
+        Ok(mq) => {
+          media_queries.push(mq);
+        }
+        Err(err) => match err.kind {
+          ParseErrorKind::Basic(BasicParseErrorKind::EndOfInput) => break,
+          _ => return Err(err),
+        },
+      }
+
+      match input.next() {
+        Ok(&Token::Comma) => {}
+        Ok(_) => unreachable!(),
+        Err(_) => break,
+      }
+    }
+
+    Ok(MediaList { media_queries })
+  }
+
+  pub(crate) fn transform_custom_media(
+    &mut self,
+    loc: Location,
+    custom_media: &HashMap<CowArcStr<'i>, CustomMediaRule<'i>>,
+  ) -> Result<(), MinifyError> {
+    for query in self.media_queries.iter_mut() {
+      query.transform_custom_media(loc, custom_media)?;
+    }
+    Ok(())
+  }
+
+  pub(crate) fn transform_resolution(&mut self, targets: Targets) {
+    let mut i = 0;
+    while i < self.media_queries.len() {
+      let query = &self.media_queries[i];
+      let mut prefixes = query.get_necessary_prefixes(targets);
+      prefixes.remove(VendorPrefix::None);
+      if !prefixes.is_empty() {
+        let query = query.clone();
+        for prefix in prefixes {
+          let mut transformed = query.clone();
+          transformed.transform_resolution(prefix);
+          if !self.media_queries.contains(&transformed) {
+            self.media_queries.insert(i, transformed);
+          }
+          i += 1;
+        }
+      }
+
+      i += 1;
+    }
+  }
+
+  /// Returns whether the media query list always matches.
+  pub fn always_matches(&self) -> bool {
+    // If the media list is empty, it always matches.
+    self.media_queries.is_empty() || self.media_queries.iter().all(|mq| mq.always_matches())
+  }
+
+  /// Returns whether the media query list never matches.
+  pub fn never_matches(&self) -> bool {
+    !self.media_queries.is_empty() && self.media_queries.iter().all(|mq| mq.never_matches())
+  }
+
+  /// Attempts to combine the given media query list into this one. The resulting media query
+  /// list matches if both the original media query lists would have matched.
+  ///
+  /// Returns an error if the boolean logic is not possible.
+  pub fn and(&mut self, b: &MediaList<'i>) -> Result<(), ()> {
+    if self.media_queries.is_empty() {
+      self.media_queries.extend(b.media_queries.iter().cloned());
+      return Ok(());
+    }
+
+    for b in &b.media_queries {
+      if self.media_queries.contains(&b) {
+        continue;
+      }
+
+      for a in &mut self.media_queries {
+        a.and(&b)?;
+      }
+    }
+
+    Ok(())
+  }
+
+  /// Combines the given media query list into this one. The resulting media query list
+  /// matches if either of the original media query lists would have matched.
+  pub fn or(&mut self, b: &MediaList<'i>) {
+    for mq in &b.media_queries {
+      if !self.media_queries.contains(&mq) {
+        self.media_queries.push(mq.clone())
+      }
+    }
+  }
+}
+
+impl<'i> ToCss for MediaList<'i> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    if self.media_queries.is_empty() {
+      dest.write_str("not all")?;
+      return Ok(());
+    }
+
+    let mut first = true;
+    for query in &self.media_queries {
+      if !first {
+        dest.delim(',', false)?;
+      }
+      first = false;
+      query.to_css(dest)?;
+    }
+    Ok(())
+  }
+}
+
+enum_property! {
+  /// A [media query qualifier](https://drafts.csswg.org/mediaqueries/#mq-prefix).
+  pub enum Qualifier {
+    /// Prevents older browsers from matching the media query.
+    Only,
+    /// Negates a media query.
+    Not,
+  }
+}
+
+/// A [media type](https://drafts.csswg.org/mediaqueries/#media-types) within a media query.
+#[derive(Clone, Debug, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(rename_all = "kebab-case", into = "CowArcStr", from = "CowArcStr")
+)]
+pub enum MediaType<'i> {
+  /// Matches all devices.
+  All,
+  /// Matches printers, and devices intended to reproduce a printed
+  /// display, such as a web browser showing a document in “Print Preview”.
+  Print,
+  /// Matches all devices that aren’t matched by print.
+  Screen,
+  /// An unknown media type.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  Custom(CowArcStr<'i>),
+}
+
+impl<'i> From<CowArcStr<'i>> for MediaType<'i> {
+  fn from(name: CowArcStr<'i>) -> Self {
+    match_ignore_ascii_case! { &*name,
+      "all" => MediaType::All,
+      "print" => MediaType::Print,
+      "screen" => MediaType::Screen,
+      _ => MediaType::Custom(name)
+    }
+  }
+}
+
+impl<'i> Into<CowArcStr<'i>> for MediaType<'i> {
+  fn into(self) -> CowArcStr<'i> {
+    match self {
+      MediaType::All => "all".into(),
+      MediaType::Print => "print".into(),
+      MediaType::Screen => "screen".into(),
+      MediaType::Custom(desc) => desc,
+    }
+  }
+}
+
+impl<'i> Parse<'i> for MediaType<'i> {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let name: CowArcStr = input.expect_ident()?.into();
+    Ok(Self::from(name))
+  }
+}
+
+#[cfg(feature = "jsonschema")]
+#[cfg_attr(docsrs, doc(cfg(feature = "jsonschema")))]
+impl<'a> schemars::JsonSchema for MediaType<'a> {
+  fn is_referenceable() -> bool {
+    true
+  }
+
+  fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
+    str::json_schema(gen)
+  }
+
+  fn schema_name() -> String {
+    "MediaType".into()
+  }
+}
+
+/// A [media query](https://drafts.csswg.org/mediaqueries/#media).
+#[derive(Clone, Debug, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(feature = "visitor", visit(visit_media_query, MEDIA_QUERIES))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize), serde(rename_all = "camelCase"))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub struct MediaQuery<'i> {
+  /// The qualifier for this query.
+  pub qualifier: Option<Qualifier>,
+  /// The media type for this query, that can be known, unknown, or "all".
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  pub media_type: MediaType<'i>,
+  /// The condition that this media query contains. This cannot have `or`
+  /// in the first level.
+  pub condition: Option<MediaCondition<'i>>,
+}
+
+impl<'i> ParseWithOptions<'i> for MediaQuery<'i> {
+  fn parse_with_options<'t>(
+    input: &mut Parser<'i, 't>,
+    options: &ParserOptions<'_, 'i>,
+  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let (qualifier, explicit_media_type) = input
+      .try_parse(|input| -> Result<_, ParseError<'i, ParserError<'i>>> {
+        let qualifier = input.try_parse(Qualifier::parse).ok();
+        let media_type = MediaType::parse(input)?;
+        Ok((qualifier, Some(media_type)))
+      })
+      .unwrap_or_default();
+
+    let condition = if explicit_media_type.is_none() {
+      Some(MediaCondition::parse_with_flags(
+        input,
+        QueryConditionFlags::ALLOW_OR,
+        options,
+      )?)
+    } else if input.try_parse(|i| i.expect_ident_matching("and")).is_ok() {
+      Some(MediaCondition::parse_with_flags(
+        input,
+        QueryConditionFlags::empty(),
+        options,
+      )?)
+    } else {
+      None
+    };
+
+    let media_type = explicit_media_type.unwrap_or(MediaType::All);
+    Ok(Self {
+      qualifier,
+      media_type,
+      condition,
+    })
+  }
+}
+
+impl<'i> MediaQuery<'i> {
+  fn transform_custom_media(
+    &mut self,
+    loc: Location,
+    custom_media: &HashMap<CowArcStr<'i>, CustomMediaRule<'i>>,
+  ) -> Result<(), MinifyError> {
+    if let Some(condition) = &mut self.condition {
+      let used = process_condition(
+        loc,
+        custom_media,
+        &mut self.media_type,
+        &mut self.qualifier,
+        condition,
+        &mut HashSet::new(),
+      )?;
+      if !used {
+        self.condition = None;
+      }
+    }
+    Ok(())
+  }
+
+  fn get_necessary_prefixes(&self, targets: Targets) -> VendorPrefix {
+    if let Some(condition) = &self.condition {
+      condition.get_necessary_prefixes(targets)
+    } else {
+      VendorPrefix::empty()
+    }
+  }
+
+  fn transform_resolution(&mut self, prefix: VendorPrefix) {
+    if let Some(condition) = &mut self.condition {
+      condition.transform_resolution(prefix)
+    }
+  }
+
+  /// Returns whether the media query is guaranteed to always match.
+  pub fn always_matches(&self) -> bool {
+    self.qualifier == None && self.media_type == MediaType::All && self.condition == None
+  }
+
+  /// Returns whether the media query is guaranteed to never match.
+  pub fn never_matches(&self) -> bool {
+    self.qualifier == Some(Qualifier::Not) && self.media_type == MediaType::All && self.condition == None
+  }
+
+  /// Attempts to combine the given media query into this one. The resulting media query
+  /// matches if both of the original media queries would have matched.
+  ///
+  /// Returns an error if the boolean logic is not possible.
+  pub fn and<'a>(&mut self, b: &MediaQuery<'i>) -> Result<(), ()> {
+    let at = (&self.qualifier, &self.media_type);
+    let bt = (&b.qualifier, &b.media_type);
+    let (qualifier, media_type) = match (at, bt) {
+      // `not all and screen` => not all
+      // `screen and not all` => not all
+      ((&Some(Qualifier::Not), &MediaType::All), _) |
+      (_, (&Some(Qualifier::Not), &MediaType::All)) => (Some(Qualifier::Not), MediaType::All),
+      // `not screen and not print` => ERROR
+      // `not screen and not screen` => not screen
+      ((&Some(Qualifier::Not), a), (&Some(Qualifier::Not), b)) => {
+        if a == b {
+          (Some(Qualifier::Not), a.clone())
+        } else {
+          return Err(())
+        }
+      },
+      // `all and print` => print
+      // `print and all` => print
+      // `all and not print` => not print
+      ((_, MediaType::All), (q, t)) |
+      ((q, t), (_, MediaType::All)) |
+      // `not screen and print` => print
+      // `print and not screen` => print
+      ((&Some(Qualifier::Not), _), (q, t)) |
+      ((q, t), (&Some(Qualifier::Not), _)) => (q.clone(), t.clone()),
+      // `print and screen` => not all
+      ((_, a), (_, b)) if a != b => (Some(Qualifier::Not), MediaType::All),
+      ((_, a), _) => (None, a.clone())
+    };
+
+    self.qualifier = qualifier;
+    self.media_type = media_type;
+
+    if let Some(cond) = &b.condition {
+      self.condition = if let Some(condition) = &self.condition {
+        if condition != cond {
+          Some(MediaCondition::Operation {
+            conditions: vec![condition.clone(), cond.clone()],
+            operator: Operator::And,
+          })
+        } else {
+          Some(condition.clone())
+        }
+      } else {
+        Some(cond.clone())
+      }
+    }
+
+    Ok(())
+  }
+}
+
+impl<'i> ToCss for MediaQuery<'i> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    if let Some(qual) = self.qualifier {
+      qual.to_css(dest)?;
+      dest.write_char(' ')?;
+    }
+
+    match self.media_type {
+      MediaType::All => {
+        // We need to print "all" if there's a qualifier, or there's
+        // just an empty list of expressions.
+        //
+        // Otherwise, we'd serialize media queries like "(min-width:
+        // 40px)" in "all (min-width: 40px)", which is unexpected.
+        if self.qualifier.is_some() || self.condition.is_none() {
+          dest.write_str("all")?;
+        }
+      }
+      MediaType::Print => dest.write_str("print")?,
+      MediaType::Screen => dest.write_str("screen")?,
+      MediaType::Custom(ref desc) => dest.write_str(desc)?,
+    }
+
+    let condition = match self.condition {
+      Some(ref c) => c,
+      None => return Ok(()),
+    };
+
+    let needs_parens = if self.media_type != MediaType::All || self.qualifier.is_some() {
+      dest.write_str(" and ")?;
+      matches!(condition, MediaCondition::Operation { operator, .. } if *operator != Operator::And)
+    } else {
+      false
+    };
+
+    to_css_with_parens_if_needed(condition, dest, needs_parens)
+  }
+}
+
+#[cfg(feature = "serde")]
+#[derive(serde::Deserialize)]
+#[serde(untagged)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+enum MediaQueryOrRaw<'i> {
+  #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
+  MediaQuery {
+    qualifier: Option<Qualifier>,
+    #[cfg_attr(feature = "serde", serde(borrow))]
+    media_type: MediaType<'i>,
+    condition: Option<MediaCondition<'i>>,
+  },
+  Raw {
+    raw: CowArcStr<'i>,
+  },
+}
+
+#[cfg(feature = "serde")]
+impl<'i, 'de: 'i> serde::Deserialize<'de> for MediaQuery<'i> {
+  fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+  where
+    D: serde::Deserializer<'de>,
+  {
+    let mq = MediaQueryOrRaw::deserialize(deserializer)?;
+    match mq {
+      MediaQueryOrRaw::MediaQuery {
+        qualifier,
+        media_type,
+        condition,
+      } => Ok(MediaQuery {
+        qualifier,
+        media_type,
+        condition,
+      }),
+      MediaQueryOrRaw::Raw { raw } => {
+        let res = MediaQuery::parse_string_with_options(raw.as_ref(), ParserOptions::default())
+          .map_err(|_| serde::de::Error::custom("Could not parse value"))?;
+        Ok(res.into_owned())
+      }
+    }
+  }
+}
+
+enum_property! {
+  /// A binary `and` or `or` operator.
+  pub enum Operator {
+    /// The `and` operator.
+    And,
+    /// The `or` operator.
+    Or,
+  }
+}
+
+/// Represents a media condition.
+#[derive(Clone, Debug, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub enum MediaCondition<'i> {
+  /// A media feature, implicitly parenthesized.
+  #[cfg_attr(feature = "serde", serde(borrow, with = "ValueWrapper::<MediaFeature>"))]
+  Feature(MediaFeature<'i>),
+  /// A negation of a condition.
+  #[cfg_attr(feature = "visitor", skip_type)]
+  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<Box<MediaCondition>>"))]
+  Not(Box<MediaCondition<'i>>),
+  /// A set of joint operations.
+  #[cfg_attr(feature = "visitor", skip_type)]
+  Operation {
+    /// The operator for the conditions.
+    operator: Operator,
+    /// The conditions for the operator.
+    conditions: Vec<MediaCondition<'i>>,
+  },
+}
+
+/// A trait for conditions such as media queries and container queries.
+pub(crate) trait QueryCondition<'i>: Sized {
+  fn parse_feature<'t>(
+    input: &mut Parser<'i, 't>,
+    options: &ParserOptions<'_, 'i>,
+  ) -> Result<Self, ParseError<'i, ParserError<'i>>>;
+  fn create_negation(condition: Box<Self>) -> Self;
+  fn create_operation(operator: Operator, conditions: Vec<Self>) -> Self;
+  fn parse_style_query<'t>(
+    input: &mut Parser<'i, 't>,
+    _options: &ParserOptions<'_, 'i>,
+  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    Err(input.new_error_for_next_token())
+  }
+
+  fn needs_parens(&self, parent_operator: Option<Operator>, targets: &Targets) -> bool;
+}
+
+impl<'i> QueryCondition<'i> for MediaCondition<'i> {
+  #[inline]
+  fn parse_feature<'t>(
+    input: &mut Parser<'i, 't>,
+    options: &ParserOptions<'_, 'i>,
+  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let feature = MediaFeature::parse_with_options(input, options)?;
+    Ok(Self::Feature(feature))
+  }
+
+  #[inline]
+  fn create_negation(condition: Box<MediaCondition<'i>>) -> Self {
+    Self::Not(condition)
+  }
+
+  #[inline]
+  fn create_operation(operator: Operator, conditions: Vec<MediaCondition<'i>>) -> Self {
+    Self::Operation { operator, conditions }
+  }
+
+  fn needs_parens(&self, parent_operator: Option<Operator>, targets: &Targets) -> bool {
+    match self {
+      MediaCondition::Not(_) => true,
+      MediaCondition::Operation { operator, .. } => Some(*operator) != parent_operator,
+      MediaCondition::Feature(f) => f.needs_parens(parent_operator, targets),
+    }
+  }
+}
+
+bitflags! {
+  /// Flags for `parse_query_condition`.
+  #[derive(PartialEq, Eq, Clone, Copy)]
+  pub(crate) struct QueryConditionFlags: u8 {
+    /// Whether to allow top-level "or" boolean logic.
+    const ALLOW_OR = 1 << 0;
+    /// Whether to allow style container queries.
+    const ALLOW_STYLE = 1 << 1;
+  }
+}
+
+impl<'i> MediaCondition<'i> {
+  /// Parse a single media condition.
+  fn parse_with_flags<'t>(
+    input: &mut Parser<'i, 't>,
+    flags: QueryConditionFlags,
+    options: &ParserOptions<'_, 'i>,
+  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    parse_query_condition(input, flags, options)
+  }
+
+  fn get_necessary_prefixes(&self, targets: Targets) -> VendorPrefix {
+    match self {
+      MediaCondition::Feature(MediaFeature::Range {
+        name: MediaFeatureName::Standard(MediaFeatureId::Resolution),
+        ..
+      }) => targets.prefixes(VendorPrefix::None, crate::prefixes::Feature::AtResolution),
+      MediaCondition::Not(not) => not.get_necessary_prefixes(targets),
+      MediaCondition::Operation { conditions, .. } => {
+        let mut prefixes = VendorPrefix::empty();
+        for condition in conditions {
+          prefixes |= condition.get_necessary_prefixes(targets);
+        }
+        prefixes
+      }
+      _ => VendorPrefix::empty(),
+    }
+  }
+
+  fn transform_resolution(&mut self, prefix: VendorPrefix) {
+    match self {
+      MediaCondition::Feature(MediaFeature::Range {
+        name: MediaFeatureName::Standard(MediaFeatureId::Resolution),
+        operator,
+        value: MediaFeatureValue::Resolution(value),
+      }) => match prefix {
+        VendorPrefix::WebKit | VendorPrefix::Moz => {
+          *self = MediaCondition::Feature(MediaFeature::Range {
+            name: MediaFeatureName::Standard(match prefix {
+              VendorPrefix::WebKit => MediaFeatureId::WebKitDevicePixelRatio,
+              VendorPrefix::Moz => MediaFeatureId::MozDevicePixelRatio,
+              _ => unreachable!(),
+            }),
+            operator: *operator,
+            value: MediaFeatureValue::Number(match value {
+              Resolution::Dpi(dpi) => *dpi / 96.0,
+              Resolution::Dpcm(dpcm) => *dpcm * 2.54 / 96.0,
+              Resolution::Dppx(dppx) => *dppx,
+            }),
+          });
+        }
+        _ => {}
+      },
+      MediaCondition::Not(not) => not.transform_resolution(prefix),
+      MediaCondition::Operation { conditions, .. } => {
+        for condition in conditions {
+          condition.transform_resolution(prefix);
+        }
+      }
+      _ => {}
+    }
+  }
+}
+
+impl<'i> ParseWithOptions<'i> for MediaCondition<'i> {
+  fn parse_with_options<'t>(
+    input: &mut Parser<'i, 't>,
+    options: &ParserOptions<'_, 'i>,
+  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    Self::parse_with_flags(input, QueryConditionFlags::ALLOW_OR, options)
+  }
+}
+
+/// Parse a single query condition.
+pub(crate) fn parse_query_condition<'t, 'i, P: QueryCondition<'i>>(
+  input: &mut Parser<'i, 't>,
+  flags: QueryConditionFlags,
+  options: &ParserOptions<'_, 'i>,
+) -> Result<P, ParseError<'i, ParserError<'i>>> {
+  let location = input.current_source_location();
+  let (is_negation, is_style) = match *input.next()? {
+    Token::ParenthesisBlock => (false, false),
+    Token::Ident(ref ident) if ident.eq_ignore_ascii_case("not") => (true, false),
+    Token::Function(ref f)
+      if flags.contains(QueryConditionFlags::ALLOW_STYLE) && f.eq_ignore_ascii_case("style") =>
+    {
+      (false, true)
+    }
+    ref t => return Err(location.new_unexpected_token_error(t.clone())),
+  };
+
+  let first_condition = match (is_negation, is_style) {
+    (true, false) => {
+      let inner_condition = parse_parens_or_function(input, flags, options)?;
+      return Ok(P::create_negation(Box::new(inner_condition)));
+    }
+    (true, true) => {
+      let inner_condition = P::parse_style_query(input, options)?;
+      return Ok(P::create_negation(Box::new(inner_condition)));
+    }
+    (false, false) => parse_paren_block(input, flags, options)?,
+    (false, true) => P::parse_style_query(input, options)?,
+  };
+
+  let operator = match input.try_parse(Operator::parse) {
+    Ok(op) => op,
+    Err(..) => return Ok(first_condition),
+  };
+
+  if !flags.contains(QueryConditionFlags::ALLOW_OR) && operator == Operator::Or {
+    return Err(location.new_unexpected_token_error(Token::Ident("or".into())));
+  }
+
+  let mut conditions = vec![];
+  conditions.push(first_condition);
+  conditions.push(parse_parens_or_function(input, flags, options)?);
+
+  let delim = match operator {
+    Operator::And => "and",
+    Operator::Or => "or",
+  };
+
+  loop {
+    if input.try_parse(|i| i.expect_ident_matching(delim)).is_err() {
+      return Ok(P::create_operation(operator, conditions));
+    }
+
+    conditions.push(parse_parens_or_function(input, flags, options)?);
+  }
+}
+
+/// Parse a media condition in parentheses, or a style() function.
+fn parse_parens_or_function<'t, 'i, P: QueryCondition<'i>>(
+  input: &mut Parser<'i, 't>,
+  flags: QueryConditionFlags,
+  options: &ParserOptions<'_, 'i>,
+) -> Result<P, ParseError<'i, ParserError<'i>>> {
+  let location = input.current_source_location();
+  match *input.next()? {
+    Token::ParenthesisBlock => parse_paren_block(input, flags, options),
+    Token::Function(ref f)
+      if flags.contains(QueryConditionFlags::ALLOW_STYLE) && f.eq_ignore_ascii_case("style") =>
+    {
+      P::parse_style_query(input, options)
+    }
+    ref t => return Err(location.new_unexpected_token_error(t.clone())),
+  }
+}
+
+fn parse_paren_block<'t, 'i, P: QueryCondition<'i>>(
+  input: &mut Parser<'i, 't>,
+  flags: QueryConditionFlags,
+  options: &ParserOptions<'_, 'i>,
+) -> Result<P, ParseError<'i, ParserError<'i>>> {
+  input.parse_nested_block(|input| {
+    if let Ok(inner) =
+      input.try_parse(|i| parse_query_condition(i, flags | QueryConditionFlags::ALLOW_OR, options))
+    {
+      return Ok(inner);
+    }
+
+    P::parse_feature(input, options)
+  })
+}
+
+pub(crate) fn to_css_with_parens_if_needed<V: ToCss, W>(
+  value: V,
+  dest: &mut Printer<W>,
+  needs_parens: bool,
+) -> Result<(), PrinterError>
+where
+  W: std::fmt::Write,
+{
+  if needs_parens {
+    dest.write_char('(')?;
+  }
+  value.to_css(dest)?;
+  if needs_parens {
+    dest.write_char(')')?;
+  }
+  Ok(())
+}
+
+pub(crate) fn operation_to_css<'i, V: ToCss + QueryCondition<'i>, W>(
+  operator: Operator,
+  conditions: &Vec<V>,
+  dest: &mut Printer<W>,
+) -> Result<(), PrinterError>
+where
+  W: std::fmt::Write,
+{
+  let mut iter = conditions.iter();
+  let first = iter.next().unwrap();
+  to_css_with_parens_if_needed(first, dest, first.needs_parens(Some(operator), &dest.targets.current))?;
+  for item in iter {
+    dest.write_char(' ')?;
+    operator.to_css(dest)?;
+    dest.write_char(' ')?;
+    to_css_with_parens_if_needed(item, dest, item.needs_parens(Some(operator), &dest.targets.current))?;
+  }
+
+  Ok(())
+}
+
+impl<'i> MediaCondition<'i> {
+  fn negate(&self) -> Option<MediaCondition<'i>> {
+    match self {
+      MediaCondition::Not(not) => Some((**not).clone()),
+      MediaCondition::Feature(f) => f.negate().map(MediaCondition::Feature),
+      _ => None,
+    }
+  }
+}
+
+impl<'i> ToCss for MediaCondition<'i> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    match *self {
+      MediaCondition::Feature(ref f) => f.to_css(dest),
+      MediaCondition::Not(ref c) => {
+        if let Some(negated) = c.negate() {
+          negated.to_css(dest)
+        } else {
+          dest.write_str("not ")?;
+          to_css_with_parens_if_needed(&**c, dest, c.needs_parens(None, &dest.targets.current))
+        }
+      }
+      MediaCondition::Operation {
+        ref conditions,
+        operator,
+      } => operation_to_css(operator, conditions, dest),
+    }
+  }
+}
+
+/// A [comparator](https://drafts.csswg.org/mediaqueries/#typedef-mf-comparison) within a media query.
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum MediaFeatureComparison {
+  /// `=`
+  Equal,
+  /// `>`
+  GreaterThan,
+  /// `>=`
+  GreaterThanEqual,
+  /// `<`
+  LessThan,
+  /// `<=`
+  LessThanEqual,
+}
+
+impl ToCss for MediaFeatureComparison {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    use MediaFeatureComparison::*;
+    match self {
+      Equal => dest.delim('=', true),
+      GreaterThan => dest.delim('>', true),
+      GreaterThanEqual => {
+        dest.whitespace()?;
+        dest.write_str(">=")?;
+        dest.whitespace()
+      }
+      LessThan => dest.delim('<', true),
+      LessThanEqual => {
+        dest.whitespace()?;
+        dest.write_str("<=")?;
+        dest.whitespace()
+      }
+    }
+  }
+}
+
+impl MediaFeatureComparison {
+  fn opposite(&self) -> MediaFeatureComparison {
+    match self {
+      MediaFeatureComparison::GreaterThan => MediaFeatureComparison::LessThan,
+      MediaFeatureComparison::GreaterThanEqual => MediaFeatureComparison::LessThanEqual,
+      MediaFeatureComparison::LessThan => MediaFeatureComparison::GreaterThan,
+      MediaFeatureComparison::LessThanEqual => MediaFeatureComparison::GreaterThanEqual,
+      MediaFeatureComparison::Equal => MediaFeatureComparison::Equal,
+    }
+  }
+
+  fn negate(&self) -> MediaFeatureComparison {
+    match self {
+      MediaFeatureComparison::GreaterThan => MediaFeatureComparison::LessThanEqual,
+      MediaFeatureComparison::GreaterThanEqual => MediaFeatureComparison::LessThan,
+      MediaFeatureComparison::LessThan => MediaFeatureComparison::GreaterThanEqual,
+      MediaFeatureComparison::LessThanEqual => MediaFeatureComparison::GreaterThan,
+      MediaFeatureComparison::Equal => MediaFeatureComparison::Equal,
+    }
+  }
+}
+
+/// A generic media feature or container feature.
+#[derive(Clone, Debug, PartialEq)]
+#[cfg_attr(
+  feature = "visitor",
+  derive(Visit),
+  visit(visit_media_feature, MEDIA_QUERIES, <'i, MediaFeatureId>),
+  visit(<'i, ContainerSizeFeatureId>)
+)]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub enum QueryFeature<'i, FeatureId> {
+  /// A plain media feature, e.g. `(min-width: 240px)`.
+  Plain {
+    /// The name of the feature.
+    #[cfg_attr(feature = "serde", serde(borrow))]
+    name: MediaFeatureName<'i, FeatureId>,
+    /// The feature value.
+    value: MediaFeatureValue<'i>,
+  },
+  /// A boolean feature, e.g. `(hover)`.
+  Boolean {
+    /// The name of the feature.
+    name: MediaFeatureName<'i, FeatureId>,
+  },
+  /// A range, e.g. `(width > 240px)`.
+  Range {
+    /// The name of the feature.
+    name: MediaFeatureName<'i, FeatureId>,
+    /// A comparator.
+    operator: MediaFeatureComparison,
+    /// The feature value.
+    value: MediaFeatureValue<'i>,
+  },
+  /// An interval, e.g. `(120px < width < 240px)`.
+  #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
+  Interval {
+    /// The name of the feature.
+    name: MediaFeatureName<'i, FeatureId>,
+    /// A start value.
+    start: MediaFeatureValue<'i>,
+    /// A comparator for the start value.
+    start_operator: MediaFeatureComparison,
+    /// The end value.
+    end: MediaFeatureValue<'i>,
+    /// A comparator for the end value.
+    end_operator: MediaFeatureComparison,
+  },
+}
+
+/// A [media feature](https://drafts.csswg.org/mediaqueries/#typedef-media-feature)
+pub type MediaFeature<'i> = QueryFeature<'i, MediaFeatureId>;
+
+impl<'i, FeatureId> ParseWithOptions<'i> for QueryFeature<'i, FeatureId>
+where
+  FeatureId: for<'x> Parse<'x> + std::fmt::Debug + PartialEq + ValueType + Clone,
+{
+  fn parse_with_options<'t>(
+    input: &mut Parser<'i, 't>,
+    options: &ParserOptions<'_, 'i>,
+  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    match input.try_parse(|input| Self::parse_name_first(input, options)) {
+      Ok(res) => Ok(res),
+      Err(
+        err @ ParseError {
+          kind: ParseErrorKind::Custom(ParserError::InvalidMediaQuery),
+          ..
+        },
+      ) => Err(err),
+      _ => Self::parse_value_first(input),
+    }
+  }
+}
+
+impl<'i, FeatureId> QueryFeature<'i, FeatureId>
+where
+  FeatureId: for<'x> Parse<'x> + std::fmt::Debug + PartialEq + ValueType + Clone,
+{
+  fn parse_name_first<'t>(
+    input: &mut Parser<'i, 't>,
+    options: &ParserOptions<'_, 'i>,
+  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let (name, legacy_op) = MediaFeatureName::parse(input)?;
+
+    let operator = input.try_parse(|input| consume_operation_or_colon(input, true));
+    let operator = match operator {
+      Err(..) => return Ok(QueryFeature::Boolean { name }),
+      Ok(operator) => operator,
+    };
+
+    if operator.is_some() && legacy_op.is_some() {
+      dbg!();
+      return Err(input.new_custom_error(ParserError::InvalidMediaQuery));
+    }
+
+    let value = MediaFeatureValue::parse(input, name.value_type())?;
+    if !value.check_type(name.value_type()) {
+      if options.error_recovery {
+        options.warn(ParseError {
+          kind: ParseErrorKind::Custom(ParserError::InvalidMediaQuery),
+          location: input.current_source_location(),
+        });
+      } else {
+        return Err(input.new_custom_error(ParserError::InvalidMediaQuery));
+      }
+    }
+
+    if let Some(operator) = operator.or(legacy_op) {
+      if !name.value_type().allows_ranges() {
+        dbg!();
+
+        return Err(input.new_custom_error(ParserError::InvalidMediaQuery));
+      }
+
+      Ok(QueryFeature::Range { name, operator, value })
+    } else {
+      Ok(QueryFeature::Plain { name, value })
+    }
+  }
+
+  fn parse_value_first<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    // We need to find the feature name first so we know the type.
+    let start = input.state();
+    let name = loop {
+      if let Ok((name, legacy_op)) = MediaFeatureName::parse(input) {
+        if legacy_op.is_some() {
+          dbg!();
+
+          return Err(input.new_custom_error(ParserError::InvalidMediaQuery));
+        }
+        break name;
+      }
+      if input.is_exhausted() {
+        dbg!();
+
+        return Err(input.new_custom_error(ParserError::InvalidMediaQuery));
+      }
+    };
+
+    input.reset(&start);
+
+    // Now we can parse the first value.
+    let value = MediaFeatureValue::parse(input, name.value_type())?;
+    let operator = consume_operation_or_colon(input, false)?;
+
+    // Skip over the feature name again.
+    {
+      let (feature_name, _) = MediaFeatureName::parse(input)?;
+      debug_assert_eq!(name, feature_name);
+    }
+
+    if !name.value_type().allows_ranges() || !value.check_type(name.value_type()) {
+      dbg!();
+      return Err(input.new_custom_error(ParserError::InvalidMediaQuery));
+    }
+
+    if let Ok(end_operator) = input.try_parse(|input| consume_operation_or_colon(input, false)) {
+      let start_operator = operator.unwrap();
+      let end_operator = end_operator.unwrap();
+      // Start and end operators must be matching.
+      match (start_operator, end_operator) {
+        (MediaFeatureComparison::GreaterThan, MediaFeatureComparison::GreaterThan)
+        | (MediaFeatureComparison::GreaterThan, MediaFeatureComparison::GreaterThanEqual)
+        | (MediaFeatureComparison::GreaterThanEqual, MediaFeatureComparison::GreaterThanEqual)
+        | (MediaFeatureComparison::GreaterThanEqual, MediaFeatureComparison::GreaterThan)
+        | (MediaFeatureComparison::LessThan, MediaFeatureComparison::LessThan)
+        | (MediaFeatureComparison::LessThan, MediaFeatureComparison::LessThanEqual)
+        | (MediaFeatureComparison::LessThanEqual, MediaFeatureComparison::LessThanEqual)
+        | (MediaFeatureComparison::LessThanEqual, MediaFeatureComparison::LessThan) => {}
+        _ => return Err(input.new_custom_error(ParserError::InvalidMediaQuery)),
+      };
+
+      let end_value = MediaFeatureValue::parse(input, name.value_type())?;
+      if !end_value.check_type(name.value_type()) {
+        return Err(input.new_custom_error(ParserError::InvalidMediaQuery));
+      }
+
+      Ok(QueryFeature::Interval {
+        name,
+        start: value,
+        start_operator,
+        end: end_value,
+        end_operator,
+      })
+    } else {
+      let operator = operator.unwrap().opposite();
+      Ok(QueryFeature::Range { name, operator, value })
+    }
+  }
+
+  pub(crate) fn needs_parens(&self, parent_operator: Option<Operator>, targets: &Targets) -> bool {
+    if !should_compile!(targets, MediaIntervalSyntax) {
+      return false;
+    }
+
+    match self {
+      QueryFeature::Interval { .. } => parent_operator != Some(Operator::And),
+      QueryFeature::Range { operator, .. } => {
+        matches!(
+          operator,
+          MediaFeatureComparison::GreaterThan | MediaFeatureComparison::LessThan
+        )
+      }
+      _ => false,
+    }
+  }
+
+  fn negate(&self) -> Option<QueryFeature<'i, FeatureId>> {
+    match self {
+      QueryFeature::Range { name, operator, value } => Some(QueryFeature::Range {
+        name: (*name).clone(),
+        operator: operator.negate(),
+        value: value.clone(),
+      }),
+      _ => None,
+    }
+  }
+}
+
+impl<'i, FeatureId: FeatureToCss> ToCss for QueryFeature<'i, FeatureId> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    match self {
+      QueryFeature::Boolean { name } => {
+        dest.write_char('(')?;
+        name.to_css(dest)?;
+      }
+      QueryFeature::Plain { name, value } => {
+        dest.write_char('(')?;
+        name.to_css(dest)?;
+        dest.delim(':', false)?;
+        value.to_css(dest)?;
+      }
+      QueryFeature::Range { name, operator, value } => {
+        // If range syntax is unsupported, use min/max prefix if possible.
+        if should_compile!(dest.targets.current, MediaRangeSyntax) {
+          return write_min_max(operator, name, value, dest, false);
+        }
+
+        dest.write_char('(')?;
+        name.to_css(dest)?;
+        operator.to_css(dest)?;
+        value.to_css(dest)?;
+      }
+      QueryFeature::Interval {
+        name,
+        start,
+        start_operator,
+        end,
+        end_operator,
+      } => {
+        if should_compile!(dest.targets.current, MediaIntervalSyntax) {
+          write_min_max(&start_operator.opposite(), name, start, dest, true)?;
+          dest.write_str(" and ")?;
+          return write_min_max(end_operator, name, end, dest, true);
+        }
+
+        dest.write_char('(')?;
+        start.to_css(dest)?;
+        start_operator.to_css(dest)?;
+        name.to_css(dest)?;
+        end_operator.to_css(dest)?;
+        end.to_css(dest)?;
+      }
+    }
+
+    dest.write_char(')')
+  }
+}
+
+/// A media feature name.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(untagged))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub enum MediaFeatureName<'i, FeatureId> {
+  /// A standard media query feature identifier.
+  Standard(FeatureId),
+  /// A custom author-defined environment variable.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  Custom(DashedIdent<'i>),
+  /// An unknown environment variable.
+  Unknown(Ident<'i>),
+}
+
+impl<'i, FeatureId: for<'x> Parse<'x>> MediaFeatureName<'i, FeatureId> {
+  /// Parses a media feature name.
+  pub fn parse<'t>(
+    input: &mut Parser<'i, 't>,
+  ) -> Result<(Self, Option<MediaFeatureComparison>), ParseError<'i, ParserError<'i>>> {
+    let ident = input.expect_ident()?;
+
+    if ident.starts_with("--") {
+      return Ok((MediaFeatureName::Custom(DashedIdent(ident.into())), None));
+    }
+
+    let mut name = ident.as_ref();
+
+    // Webkit places its prefixes before "min" and "max". Remove it first, and
+    // re-add after removing min/max.
+    let is_webkit = starts_with_ignore_ascii_case(&name, "-webkit-");
+    if is_webkit {
+      name = &name[8..];
+    }
+
+    let comparator = if starts_with_ignore_ascii_case(&name, "min-") {
+      name = &name[4..];
+      Some(MediaFeatureComparison::GreaterThanEqual)
+    } else if starts_with_ignore_ascii_case(&name, "max-") {
+      name = &name[4..];
+      Some(MediaFeatureComparison::LessThanEqual)
+    } else {
+      None
+    };
+
+    let name = if is_webkit {
+      Cow::Owned(format!("-webkit-{}", name))
+    } else {
+      Cow::Borrowed(name)
+    };
+
+    if let Ok(standard) = FeatureId::parse_string(&name) {
+      return Ok((MediaFeatureName::Standard(standard), comparator));
+    }
+
+    Ok((MediaFeatureName::Unknown(Ident(ident.into())), None))
+  }
+}
+
+mod private {
+  use super::*;
+
+  /// A trait for feature ids which can get a value type.
+  pub trait ValueType {
+    /// Returns the value type for this feature id.
+    fn value_type(&self) -> MediaFeatureType;
+  }
+}
+
+pub(crate) use private::ValueType;
+
+impl<'i, FeatureId: ValueType> ValueType for MediaFeatureName<'i, FeatureId> {
+  fn value_type(&self) -> MediaFeatureType {
+    match self {
+      Self::Standard(standard) => standard.value_type(),
+      _ => MediaFeatureType::Unknown,
+    }
+  }
+}
+
+impl<'i, FeatureId: FeatureToCss> ToCss for MediaFeatureName<'i, FeatureId> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    match self {
+      Self::Standard(v) => v.to_css(dest),
+      Self::Custom(v) => v.to_css(dest),
+      Self::Unknown(v) => v.to_css(dest),
+    }
+  }
+}
+
+impl<'i, FeatureId: FeatureToCss> FeatureToCss for MediaFeatureName<'i, FeatureId> {
+  fn to_css_with_prefix<W>(&self, prefix: &str, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    match self {
+      Self::Standard(v) => v.to_css_with_prefix(prefix, dest),
+      Self::Custom(v) => {
+        dest.write_str(prefix)?;
+        v.to_css(dest)
+      }
+      Self::Unknown(v) => {
+        dest.write_str(prefix)?;
+        v.to_css(dest)
+      }
+    }
+  }
+}
+
+/// The type of a media feature.
+#[derive(PartialEq)]
+pub enum MediaFeatureType {
+  /// A length value.
+  Length,
+  /// A number value.
+  Number,
+  /// An integer value.
+  Integer,
+  /// A boolean value, either 0 or 1.
+  Boolean,
+  /// A resolution.
+  Resolution,
+  /// A ratio.
+  Ratio,
+  /// An identifier.
+  Ident,
+  /// An unknown type.
+  Unknown,
+}
+
+impl MediaFeatureType {
+  fn allows_ranges(&self) -> bool {
+    use MediaFeatureType::*;
+    match self {
+      Length => true,
+      Number => true,
+      Integer => true,
+      Boolean => false,
+      Resolution => true,
+      Ratio => true,
+      Ident => false,
+      Unknown => true,
+    }
+  }
+}
+
+macro_rules! define_query_features {
+  (
+    $(#[$outer:meta])*
+    $vis:vis enum $name:ident {
+      $(
+        $(#[$meta: meta])*
+        $str: literal: $id: ident = $ty: ident,
+      )+
+    }
+  ) => {
+    crate::macros::enum_property! {
+      $(#[$outer])*
+      $vis enum $name {
+        $(
+          $(#[$meta])*
+          $str: $id,
+        )+
+      }
+    }
+
+    impl ValueType for $name {
+      fn value_type(&self) -> MediaFeatureType {
+        match self {
+          $(
+            Self::$id => MediaFeatureType::$ty,
+          )+
+        }
+      }
+    }
+  }
+}
+
+pub(crate) use define_query_features;
+
+define_query_features! {
+  /// A media query feature identifier.
+  pub enum MediaFeatureId {
+    /// The [width](https://w3c.github.io/csswg-drafts/mediaqueries-5/#width) media feature.
+    "width": Width = Length,
+    /// The [height](https://w3c.github.io/csswg-drafts/mediaqueries-5/#height) media feature.
+    "height": Height = Length,
+    /// The [aspect-ratio](https://w3c.github.io/csswg-drafts/mediaqueries-5/#aspect-ratio) media feature.
+    "aspect-ratio": AspectRatio = Ratio,
+    /// The [orientation](https://w3c.github.io/csswg-drafts/mediaqueries-5/#orientation) media feature.
+    "orientation": Orientation = Ident,
+    /// The [overflow-block](https://w3c.github.io/csswg-drafts/mediaqueries-5/#overflow-block) media feature.
+    "overflow-block": OverflowBlock = Ident,
+    /// The [overflow-inline](https://w3c.github.io/csswg-drafts/mediaqueries-5/#overflow-inline) media feature.
+    "overflow-inline": OverflowInline = Ident,
+    /// The [horizontal-viewport-segments](https://w3c.github.io/csswg-drafts/mediaqueries-5/#horizontal-viewport-segments) media feature.
+    "horizontal-viewport-segments": HorizontalViewportSegments = Integer,
+    /// The [vertical-viewport-segments](https://w3c.github.io/csswg-drafts/mediaqueries-5/#vertical-viewport-segments) media feature.
+    "vertical-viewport-segments": VerticalViewportSegments = Integer,
+    /// The [display-mode](https://w3c.github.io/csswg-drafts/mediaqueries-5/#display-mode) media feature.
+    "display-mode": DisplayMode = Ident,
+    /// The [resolution](https://w3c.github.io/csswg-drafts/mediaqueries-5/#resolution) media feature.
+    "resolution": Resolution = Resolution, // | infinite??
+    /// The [scan](https://w3c.github.io/csswg-drafts/mediaqueries-5/#scan) media feature.
+    "scan": Scan = Ident,
+    /// The [grid](https://w3c.github.io/csswg-drafts/mediaqueries-5/#grid) media feature.
+    "grid": Grid = Boolean,
+    /// The [update](https://w3c.github.io/csswg-drafts/mediaqueries-5/#update) media feature.
+    "update": Update = Ident,
+    /// The [environment-blending](https://w3c.github.io/csswg-drafts/mediaqueries-5/#environment-blending) media feature.
+    "environment-blending": EnvironmentBlending = Ident,
+    /// The [color](https://w3c.github.io/csswg-drafts/mediaqueries-5/#color) media feature.
+    "color": Color = Integer,
+    /// The [color-index](https://w3c.github.io/csswg-drafts/mediaqueries-5/#color-index) media feature.
+    "color-index": ColorIndex = Integer,
+    /// The [monochrome](https://w3c.github.io/csswg-drafts/mediaqueries-5/#monochrome) media feature.
+    "monochrome": Monochrome = Integer,
+    /// The [color-gamut](https://w3c.github.io/csswg-drafts/mediaqueries-5/#color-gamut) media feature.
+    "color-gamut": ColorGamut = Ident,
+    /// The [dynamic-range](https://w3c.github.io/csswg-drafts/mediaqueries-5/#dynamic-range) media feature.
+    "dynamic-range": DynamicRange = Ident,
+    /// The [inverted-colors](https://w3c.github.io/csswg-drafts/mediaqueries-5/#inverted-colors) media feature.
+    "inverted-colors": InvertedColors = Ident,
+    /// The [pointer](https://w3c.github.io/csswg-drafts/mediaqueries-5/#pointer) media feature.
+    "pointer": Pointer = Ident,
+    /// The [hover](https://w3c.github.io/csswg-drafts/mediaqueries-5/#hover) media feature.
+    "hover": Hover = Ident,
+    /// The [any-pointer](https://w3c.github.io/csswg-drafts/mediaqueries-5/#any-pointer) media feature.
+    "any-pointer": AnyPointer = Ident,
+    /// The [any-hover](https://w3c.github.io/csswg-drafts/mediaqueries-5/#any-hover) media feature.
+    "any-hover": AnyHover = Ident,
+    /// The [nav-controls](https://w3c.github.io/csswg-drafts/mediaqueries-5/#nav-controls) media feature.
+    "nav-controls": NavControls = Ident,
+    /// The [video-color-gamut](https://w3c.github.io/csswg-drafts/mediaqueries-5/#video-color-gamut) media feature.
+    "video-color-gamut": VideoColorGamut = Ident,
+    /// The [video-dynamic-range](https://w3c.github.io/csswg-drafts/mediaqueries-5/#video-dynamic-range) media feature.
+    "video-dynamic-range": VideoDynamicRange = Ident,
+    /// The [scripting](https://w3c.github.io/csswg-drafts/mediaqueries-5/#scripting) media feature.
+    "scripting": Scripting = Ident,
+    /// The [prefers-reduced-motion](https://w3c.github.io/csswg-drafts/mediaqueries-5/#prefers-reduced-motion) media feature.
+    "prefers-reduced-motion": PrefersReducedMotion = Ident,
+    /// The [prefers-reduced-transparency](https://w3c.github.io/csswg-drafts/mediaqueries-5/#prefers-reduced-transparency) media feature.
+    "prefers-reduced-transparency": PrefersReducedTransparency = Ident,
+    /// The [prefers-contrast](https://w3c.github.io/csswg-drafts/mediaqueries-5/#prefers-contrast) media feature.
+    "prefers-contrast": PrefersContrast = Ident,
+    /// The [forced-colors](https://w3c.github.io/csswg-drafts/mediaqueries-5/#forced-colors) media feature.
+    "forced-colors": ForcedColors = Ident,
+    /// The [prefers-color-scheme](https://w3c.github.io/csswg-drafts/mediaqueries-5/#prefers-color-scheme) media feature.
+    "prefers-color-scheme": PrefersColorScheme = Ident,
+    /// The [prefers-reduced-data](https://w3c.github.io/csswg-drafts/mediaqueries-5/#prefers-reduced-data) media feature.
+    "prefers-reduced-data": PrefersReducedData = Ident,
+    /// The [device-width](https://w3c.github.io/csswg-drafts/mediaqueries-5/#device-width) media feature.
+    "device-width": DeviceWidth = Length,
+    /// The [device-height](https://w3c.github.io/csswg-drafts/mediaqueries-5/#device-height) media feature.
+    "device-height": DeviceHeight = Length,
+    /// The [device-aspect-ratio](https://w3c.github.io/csswg-drafts/mediaqueries-5/#device-aspect-ratio) media feature.
+    "device-aspect-ratio": DeviceAspectRatio = Ratio,
+
+    /// The non-standard -webkit-device-pixel-ratio media feature.
+    "-webkit-device-pixel-ratio": WebKitDevicePixelRatio = Number,
+    /// The non-standard -moz-device-pixel-ratio media feature.
+    "-moz-device-pixel-ratio": MozDevicePixelRatio = Number,
+
+    // TODO: parse non-standard media queries?
+    // -moz-device-orientation
+    // -webkit-transform-3d
+  }
+}
+
+pub(crate) trait FeatureToCss: ToCss {
+  fn to_css_with_prefix<W>(&self, prefix: &str, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write;
+}
+
+impl FeatureToCss for MediaFeatureId {
+  fn to_css_with_prefix<W>(&self, prefix: &str, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    match self {
+      MediaFeatureId::WebKitDevicePixelRatio => {
+        dest.write_str("-webkit-")?;
+        dest.write_str(prefix)?;
+        dest.write_str("device-pixel-ratio")
+      }
+      _ => {
+        dest.write_str(prefix)?;
+        self.to_css(dest)
+      }
+    }
+  }
+}
+
+#[inline]
+fn write_min_max<W, FeatureId: FeatureToCss>(
+  operator: &MediaFeatureComparison,
+  name: &MediaFeatureName<FeatureId>,
+  value: &MediaFeatureValue,
+  dest: &mut Printer<W>,
+  is_range: bool,
+) -> Result<(), PrinterError>
+where
+  W: std::fmt::Write,
+{
+  let prefix = match operator {
+    MediaFeatureComparison::GreaterThan => {
+      if is_range {
+        dest.write_char('(')?;
+      }
+      dest.write_str("not ")?;
+      Some("max-")
+    }
+    MediaFeatureComparison::GreaterThanEqual => Some("min-"),
+    MediaFeatureComparison::LessThan => {
+      if is_range {
+        dest.write_char('(')?;
+      }
+      dest.write_str("not ")?;
+      Some("min-")
+    }
+    MediaFeatureComparison::LessThanEqual => Some("max-"),
+    MediaFeatureComparison::Equal => None,
+  };
+
+  dest.write_char('(')?;
+  if let Some(prefix) = prefix {
+    name.to_css_with_prefix(prefix, dest)?;
+  } else {
+    name.to_css(dest)?;
+  }
+
+  dest.delim(':', false)?;
+  value.to_css(dest)?;
+
+  if is_range
+    && matches!(
+      operator,
+      MediaFeatureComparison::GreaterThan | MediaFeatureComparison::LessThan
+    )
+  {
+    dest.write_char(')')?;
+  }
+
+  dest.write_char(')')?;
+  Ok(())
+}
+
+/// [media feature value](https://drafts.csswg.org/mediaqueries/#typedef-mf-value) within a media query.
+///
+/// See [MediaFeature](MediaFeature).
+#[derive(Clone, Debug, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit), visit(visit_media_feature_value, MEDIA_QUERIES))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", content = "value", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub enum MediaFeatureValue<'i> {
+  /// A length value.
+  Length(Length),
+  /// A number value.
+  Number(CSSNumber),
+  /// An integer value.
+  Integer(CSSInteger),
+  /// A boolean value.
+  Boolean(bool),
+  /// A resolution.
+  Resolution(Resolution),
+  /// A ratio.
+  Ratio(Ratio),
+  /// An identifier.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  Ident(Ident<'i>),
+  /// An environment variable reference.
+  Env(EnvironmentVariable<'i>),
+}
+
+impl<'i> MediaFeatureValue<'i> {
+  fn value_type(&self) -> MediaFeatureType {
+    use MediaFeatureValue::*;
+    match self {
+      Length(..) => MediaFeatureType::Length,
+      Number(..) => MediaFeatureType::Number,
+      Integer(..) => MediaFeatureType::Integer,
+      Boolean(..) => MediaFeatureType::Boolean,
+      Resolution(..) => MediaFeatureType::Resolution,
+      Ratio(..) => MediaFeatureType::Ratio,
+      Ident(..) => MediaFeatureType::Ident,
+      Env(..) => MediaFeatureType::Unknown,
+    }
+  }
+
+  fn check_type(&self, expected_type: MediaFeatureType) -> bool {
+    match (expected_type, self.value_type()) {
+      (_, MediaFeatureType::Unknown) | (MediaFeatureType::Unknown, _) => true,
+      (a, b) => a == b,
+    }
+  }
+}
+
+impl<'i> MediaFeatureValue<'i> {
+  /// Parses a single media query feature value, with an expected type.
+  /// If the type is unknown, pass MediaFeatureType::Unknown instead.
+  pub fn parse<'t>(
+    input: &mut Parser<'i, 't>,
+    expected_type: MediaFeatureType,
+  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    if let Ok(value) = input.try_parse(|input| Self::parse_known(input, expected_type)) {
+      return Ok(value);
+    }
+
+    Self::parse_unknown(input)
+  }
+
+  fn parse_known<'t>(
+    input: &mut Parser<'i, 't>,
+    expected_type: MediaFeatureType,
+  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    match expected_type {
+      MediaFeatureType::Boolean => {
+        let value = CSSInteger::parse(input)?;
+        if value != 0 && value != 1 {
+          return Err(input.new_custom_error(ParserError::InvalidValue));
+        }
+        Ok(MediaFeatureValue::Boolean(value == 1))
+      }
+      MediaFeatureType::Number => Ok(MediaFeatureValue::Number(CSSNumber::parse(input)?)),
+      MediaFeatureType::Integer => Ok(MediaFeatureValue::Integer(CSSInteger::parse(input)?)),
+      MediaFeatureType::Length => Ok(MediaFeatureValue::Length(Length::parse(input)?)),
+      MediaFeatureType::Resolution => Ok(MediaFeatureValue::Resolution(Resolution::parse(input)?)),
+      MediaFeatureType::Ratio => Ok(MediaFeatureValue::Ratio(Ratio::parse(input)?)),
+      MediaFeatureType::Ident => Ok(MediaFeatureValue::Ident(Ident::parse(input)?)),
+      MediaFeatureType::Unknown => Err(input.new_custom_error(ParserError::InvalidValue)),
+    }
+  }
+
+  fn parse_unknown<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    // Ratios are ambiguous with numbers because the second param is optional (e.g. 2/1 == 2).
+    // We require the / delimiter when parsing ratios so that 2/1 ends up as a ratio and 2 is
+    // parsed as a number.
+    if let Ok(ratio) = input.try_parse(Ratio::parse_required) {
+      return Ok(MediaFeatureValue::Ratio(ratio));
+    }
+
+    // Parse number next so that unitless values are not parsed as lengths.
+    if let Ok(num) = input.try_parse(CSSNumber::parse) {
+      return Ok(MediaFeatureValue::Number(num));
+    }
+
+    if let Ok(length) = input.try_parse(Length::parse) {
+      return Ok(MediaFeatureValue::Length(length));
+    }
+
+    if let Ok(res) = input.try_parse(Resolution::parse) {
+      return Ok(MediaFeatureValue::Resolution(res));
+    }
+
+    if let Ok(env) = input.try_parse(|input| EnvironmentVariable::parse(input, &ParserOptions::default(), 0)) {
+      return Ok(MediaFeatureValue::Env(env));
+    }
+
+    let ident = Ident::parse(input)?;
+    Ok(MediaFeatureValue::Ident(ident))
+  }
+}
+
+impl<'i> ToCss for MediaFeatureValue<'i> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    match self {
+      MediaFeatureValue::Length(len) => len.to_css(dest),
+      MediaFeatureValue::Number(num) => num.to_css(dest),
+      MediaFeatureValue::Integer(num) => num.to_css(dest),
+      MediaFeatureValue::Boolean(b) => {
+        if *b {
+          dest.write_char('1')
+        } else {
+          dest.write_char('0')
+        }
+      }
+      MediaFeatureValue::Resolution(res) => res.to_css(dest),
+      MediaFeatureValue::Ratio(ratio) => ratio.to_css(dest),
+      MediaFeatureValue::Ident(id) => {
+        id.to_css(dest)?;
+        Ok(())
+      }
+      MediaFeatureValue::Env(env) => env.to_css(dest, false),
+    }
+  }
+}
+
+/// Consumes an operation or a colon, or returns an error.
+fn consume_operation_or_colon<'i, 't>(
+  input: &mut Parser<'i, 't>,
+  allow_colon: bool,
+) -> Result<Option<MediaFeatureComparison>, ParseError<'i, ParserError<'i>>> {
+  let location = input.current_source_location();
+  let first_delim = {
+    let location = input.current_source_location();
+    let next_token = input.next()?;
+    match next_token {
+      Token::Colon if allow_colon => return Ok(None),
+      Token::Delim(oper) => oper,
+      t => return Err(location.new_unexpected_token_error(t.clone())),
+    }
+  };
+  Ok(Some(match first_delim {
+    '=' => MediaFeatureComparison::Equal,
+    '>' => {
+      if input.try_parse(|i| i.expect_delim('=')).is_ok() {
+        MediaFeatureComparison::GreaterThanEqual
+      } else {
+        MediaFeatureComparison::GreaterThan
+      }
+    }
+    '<' => {
+      if input.try_parse(|i| i.expect_delim('=')).is_ok() {
+        MediaFeatureComparison::LessThanEqual
+      } else {
+        MediaFeatureComparison::LessThan
+      }
+    }
+    d => return Err(location.new_unexpected_token_error(Token::Delim(*d))),
+  }))
+}
+
+fn process_condition<'i>(
+  loc: Location,
+  custom_media: &HashMap<CowArcStr<'i>, CustomMediaRule<'i>>,
+  media_type: &mut MediaType<'i>,
+  qualifier: &mut Option<Qualifier>,
+  condition: &mut MediaCondition<'i>,
+  seen: &mut HashSet<DashedIdent<'i>>,
+) -> Result<bool, MinifyError> {
+  match condition {
+    MediaCondition::Not(cond) => {
+      let used = process_condition(loc, custom_media, media_type, qualifier, &mut *cond, seen)?;
+      if !used {
+        // If unused, only a media type remains so apply a not qualifier.
+        // If it is already not, then it cancels out.
+        *qualifier = if *qualifier == Some(Qualifier::Not) {
+          None
+        } else {
+          Some(Qualifier::Not)
+        };
+        return Ok(false);
+      }
+
+      // Unwrap nested nots
+      match &**cond {
+        MediaCondition::Not(cond) => {
+          *condition = (**cond).clone();
+        }
+        _ => {}
+      }
+    }
+    MediaCondition::Operation { conditions, .. } => {
+      let mut res = Ok(true);
+      conditions.retain_mut(|condition| {
+        let r = process_condition(loc, custom_media, media_type, qualifier, condition, seen);
+        if let Ok(used) = r {
+          used
+        } else {
+          res = r;
+          false
+        }
+      });
+      return res;
+    }
+    MediaCondition::Feature(QueryFeature::Boolean { name }) => {
+      let name = match name {
+        MediaFeatureName::Custom(name) => name,
+        _ => return Ok(true),
+      };
+
+      if seen.contains(name) {
+        return Err(ErrorWithLocation {
+          kind: MinifyErrorKind::CircularCustomMedia { name: name.to_string() },
+          loc,
+        });
+      }
+
+      let rule = custom_media.get(&name.0).ok_or_else(|| ErrorWithLocation {
+        kind: MinifyErrorKind::CustomMediaNotDefined { name: name.to_string() },
+        loc,
+      })?;
+
+      seen.insert(name.clone());
+
+      let mut res = Ok(true);
+      let mut conditions: Vec<MediaCondition> = rule
+        .query
+        .media_queries
+        .iter()
+        .filter_map(|query| {
+          if query.media_type != MediaType::All || query.qualifier != None {
+            if *media_type == MediaType::All {
+              // `not all` will never match.
+              if *qualifier == Some(Qualifier::Not) {
+                res = Ok(false);
+                return None;
+              }
+
+              // Propagate media type and qualifier to @media rule.
+              *media_type = query.media_type.clone();
+              *qualifier = query.qualifier.clone();
+            } else if query.media_type != *media_type || query.qualifier != *qualifier {
+              // Boolean logic with media types is hard to emulate, so we error for now.
+              res = Err(ErrorWithLocation {
+                kind: MinifyErrorKind::UnsupportedCustomMediaBooleanLogic {
+                  custom_media_loc: rule.loc,
+                },
+                loc,
+              });
+              return None;
+            }
+          }
+
+          if let Some(condition) = &query.condition {
+            let mut condition = condition.clone();
+            let r = process_condition(loc, custom_media, media_type, qualifier, &mut condition, seen);
+            if r.is_err() {
+              res = r;
+            }
+            // Parentheses are required around the condition unless there is a single media feature.
+            match condition {
+              MediaCondition::Feature(..) => Some(condition),
+              _ => Some(condition),
+            }
+          } else {
+            None
+          }
+        })
+        .collect();
+
+      seen.remove(name);
+
+      if res.is_err() {
+        return res;
+      }
+
+      if conditions.is_empty() {
+        return Ok(false);
+      }
+
+      if conditions.len() == 1 {
+        *condition = conditions.pop().unwrap();
+      } else {
+        *condition = MediaCondition::Operation {
+          conditions,
+          operator: Operator::Or,
+        };
+      }
+    }
+    _ => {}
+  }
+
+  Ok(true)
+}
+
+#[cfg(test)]
+mod tests {
+  use super::*;
+  use crate::{
+    stylesheet::PrinterOptions,
+    targets::{Browsers, Targets},
+  };
+
+  fn parse(s: &str) -> MediaQuery {
+    let mut input = ParserInput::new(&s);
+    let mut parser = Parser::new(&mut input);
+    MediaQuery::parse_with_options(&mut parser, &ParserOptions::default()).unwrap()
+  }
+
+  fn and(a: &str, b: &str) -> String {
+    let mut a = parse(a);
+    let b = parse(b);
+    a.and(&b).unwrap();
+    a.to_css_string(PrinterOptions::default()).unwrap()
+  }
+
+  #[test]
+  fn test_and() {
+    assert_eq!(and("(min-width: 250px)", "(color)"), "(width >= 250px) and (color)");
+    assert_eq!(
+      and("(min-width: 250px) or (color)", "(orientation: landscape)"),
+      "((width >= 250px) or (color)) and (orientation: landscape)"
+    );
+    assert_eq!(
+      and("(min-width: 250px) and (color)", "(orientation: landscape)"),
+      "(width >= 250px) and (color) and (orientation: landscape)"
+    );
+    assert_eq!(and("all", "print"), "print");
+    assert_eq!(and("print", "all"), "print");
+    assert_eq!(and("all", "not print"), "not print");
+    assert_eq!(and("not print", "all"), "not print");
+    assert_eq!(and("not all", "print"), "not all");
+    assert_eq!(and("print", "not all"), "not all");
+    assert_eq!(and("print", "screen"), "not all");
+    assert_eq!(and("not print", "screen"), "screen");
+    assert_eq!(and("print", "not screen"), "print");
+    assert_eq!(and("not screen", "print"), "print");
+    assert_eq!(and("not screen", "not all"), "not all");
+    assert_eq!(and("print", "(min-width: 250px)"), "print and (width >= 250px)");
+    assert_eq!(and("(min-width: 250px)", "print"), "print and (width >= 250px)");
+    assert_eq!(
+      and("print and (min-width: 250px)", "(color)"),
+      "print and (width >= 250px) and (color)"
+    );
+    assert_eq!(and("all", "only screen"), "only screen");
+    assert_eq!(and("only screen", "all"), "only screen");
+    assert_eq!(and("print", "print"), "print");
+  }
+
+  #[test]
+  fn test_negated_interval_parens() {
+    let media_query = parse("screen and not (200px <= width < 500px)");
+    let printer_options = PrinterOptions {
+      targets: Targets {
+        browsers: Some(Browsers {
+          chrome: Some(95 << 16),
+          ..Default::default()
+        }),
+        ..Default::default()
+      },
+      ..Default::default()
+    };
+    assert_eq!(
+      media_query.to_css_string(printer_options).unwrap(),
+      "screen and not ((min-width: 200px) and (not (min-width: 500px)))"
+    );
+  }
+}
diff --git a/src/parser.rs b/src/parser.rs
new file mode 100644
index 0000000..af93d97
--- /dev/null
+++ b/src/parser.rs
@@ -0,0 +1,1151 @@
+use crate::declaration::{parse_declaration, DeclarationBlock, DeclarationList};
+use crate::error::{Error, ParserError, PrinterError};
+use crate::media_query::*;
+use crate::printer::Printer;
+use crate::properties::custom::TokenList;
+use crate::rules::container::{ContainerCondition, ContainerName, ContainerRule};
+use crate::rules::font_feature_values::FontFeatureValuesRule;
+use crate::rules::font_palette_values::FontPaletteValuesRule;
+use crate::rules::layer::{LayerBlockRule, LayerStatementRule};
+use crate::rules::nesting::NestedDeclarationsRule;
+use crate::rules::property::PropertyRule;
+use crate::rules::scope::ScopeRule;
+use crate::rules::starting_style::StartingStyleRule;
+use crate::rules::view_transition::ViewTransitionRule;
+use crate::rules::viewport::ViewportRule;
+
+use crate::properties::font::FamilyName;
+use crate::rules::{
+  counter_style::CounterStyleRule,
+  custom_media::CustomMediaRule,
+  document::MozDocumentRule,
+  font_face::{FontFaceDeclarationParser, FontFaceRule},
+  import::ImportRule,
+  keyframes::{KeyframeListParser, KeyframesName, KeyframesRule},
+  layer::LayerName,
+  media::MediaRule,
+  namespace::NamespaceRule,
+  nesting::NestingRule,
+  page::{PageRule, PageSelector},
+  style::StyleRule,
+  supports::{SupportsCondition, SupportsRule},
+  unknown::UnknownAtRule,
+  CssRule, CssRuleList, Location,
+};
+use crate::selector::{SelectorList, SelectorParser};
+use crate::traits::{Parse, ParseWithOptions};
+use crate::values::ident::{CustomIdent, DashedIdent};
+use crate::values::string::CowArcStr;
+use crate::vendor_prefix::VendorPrefix;
+#[cfg(feature = "visitor")]
+use crate::visitor::{Visit, VisitTypes, Visitor};
+use bitflags::bitflags;
+use cssparser::*;
+use parcel_selectors::parser::{NestingRequirement, ParseErrorRecovery};
+use std::sync::{Arc, RwLock};
+
+bitflags! {
+  /// Parser feature flags to enable.
+  #[derive(Clone, Debug, Default)]
+  pub struct ParserFlags: u8 {
+    /// Whether the enable the [CSS nesting](https://www.w3.org/TR/css-nesting-1/) draft syntax.
+    const NESTING = 1 << 0;
+    /// Whether to enable the [custom media](https://drafts.csswg.org/mediaqueries-5/#custom-mq) draft syntax.
+    const CUSTOM_MEDIA = 1 << 1;
+    /// Whether to enable the non-standard >>> and /deep/ selector combinators used by Vue and Angular.
+    const DEEP_SELECTOR_COMBINATOR = 1 << 2;
+  }
+}
+
+/// CSS parsing options.
+#[derive(Clone, Debug, Default)]
+pub struct ParserOptions<'o, 'i> {
+  /// Filename to use in error messages.
+  pub filename: String,
+  /// Whether the enable [CSS modules](https://github.com/css-modules/css-modules).
+  pub css_modules: Option<crate::css_modules::Config<'o>>,
+  /// The source index to assign to all parsed rules. Impacts the source map when
+  /// the style sheet is serialized.
+  pub source_index: u32,
+  /// Whether to ignore invalid rules and declarations rather than erroring.
+  pub error_recovery: bool,
+  /// A list that will be appended to when a warning occurs.
+  pub warnings: Option<Arc<RwLock<Vec<Error<ParserError<'i>>>>>>,
+  /// Feature flags to enable.
+  pub flags: ParserFlags,
+}
+
+impl<'o, 'i> ParserOptions<'o, 'i> {
+  #[inline]
+  pub(crate) fn warn(&self, warning: ParseError<'i, ParserError<'i>>) {
+    if let Some(warnings) = &self.warnings {
+      if let Ok(mut warnings) = warnings.write() {
+        warnings.push(Error::from(warning, self.filename.clone()));
+      }
+    }
+  }
+}
+
+#[derive(Clone, Default)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub struct DefaultAtRuleParser;
+impl<'i> crate::traits::AtRuleParser<'i> for DefaultAtRuleParser {
+  type AtRule = DefaultAtRule;
+  type Error = ();
+  type Prelude = ();
+}
+
+#[derive(PartialEq, Clone, Debug)]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub struct DefaultAtRule;
+impl crate::traits::ToCss for DefaultAtRule {
+  fn to_css<W: std::fmt::Write>(&self, _: &mut Printer<W>) -> Result<(), PrinterError> {
+    Err(PrinterError {
+      kind: crate::error::PrinterErrorKind::FmtError,
+      loc: None,
+    })
+  }
+}
+
+#[cfg(feature = "into_owned")]
+impl<'any> static_self::IntoOwned<'any> for DefaultAtRule {
+  type Owned = Self;
+  fn into_owned(self) -> Self {
+    self
+  }
+}
+
+#[cfg(feature = "visitor")]
+#[cfg_attr(docsrs, doc(cfg(feature = "visitor")))]
+impl<'i, V: Visitor<'i, DefaultAtRule>> Visit<'i, DefaultAtRule, V> for DefaultAtRule {
+  const CHILD_TYPES: VisitTypes = VisitTypes::empty();
+  fn visit_children(&mut self, _: &mut V) -> Result<(), V::Error> {
+    Ok(())
+  }
+}
+
+#[derive(PartialEq, PartialOrd)]
+enum State {
+  Start = 1,
+  Layers = 2,
+  Imports = 3,
+  Namespaces = 4,
+  Body = 5,
+}
+
+/// The parser for the top-level rules in a stylesheet.
+pub struct TopLevelRuleParser<'a, 'o, 'i, T: crate::traits::AtRuleParser<'i>> {
+  pub options: &'a ParserOptions<'o, 'i>,
+  state: State,
+  at_rule_parser: &'a mut T,
+  rules: &'a mut CssRuleList<'i, T::AtRule>,
+}
+
+impl<'a, 'o, 'b, 'i, T: crate::traits::AtRuleParser<'i>> TopLevelRuleParser<'a, 'o, 'i, T> {
+  pub fn new(
+    options: &'a ParserOptions<'o, 'i>,
+    at_rule_parser: &'a mut T,
+    rules: &'a mut CssRuleList<'i, T::AtRule>,
+  ) -> Self {
+    TopLevelRuleParser {
+      options,
+      state: State::Start,
+      at_rule_parser,
+      rules,
+    }
+  }
+
+  pub fn nested<'x: 'b>(&'x mut self) -> NestedRuleParser<'x, 'o, 'i, T> {
+    NestedRuleParser {
+      options: &self.options,
+      at_rule_parser: self.at_rule_parser,
+      declarations: DeclarationList::new(),
+      important_declarations: DeclarationList::new(),
+      rules: &mut self.rules,
+      is_in_style_rule: false,
+      allow_declarations: false,
+    }
+  }
+}
+
+/// A rule prelude for at-rule with block.
+#[derive(Debug)]
+#[allow(dead_code)]
+pub enum AtRulePrelude<'i, T> {
+  /// A @font-face rule prelude.
+  FontFace,
+  /// A @font-feature-values rule prelude, with its FamilyName list.
+  FontFeatureValues(Vec<FamilyName<'i>>),
+  /// A @font-palette-values rule prelude, with its name.
+  FontPaletteValues(DashedIdent<'i>),
+  /// A @counter-style rule prelude, with its counter style name.
+  CounterStyle(CustomIdent<'i>),
+  /// A @media rule prelude, with its media queries.
+  Media(MediaList<'i>),
+  /// A @custom-media rule prelude.
+  CustomMedia(DashedIdent<'i>, MediaList<'i>),
+  /// An @supports rule, with its conditional
+  Supports(SupportsCondition<'i>),
+  /// A @viewport rule prelude.
+  Viewport(VendorPrefix),
+  /// A @keyframes rule, with its animation name and vendor prefix if exists.
+  Keyframes(KeyframesName<'i>, VendorPrefix),
+  /// A @page rule prelude.
+  Page(Vec<PageSelector<'i>>),
+  /// A @-moz-document rule.
+  MozDocument,
+  /// A @import rule prelude.
+  Import(
+    CowRcStr<'i>,
+    MediaList<'i>,
+    Option<SupportsCondition<'i>>,
+    Option<Option<LayerName<'i>>>,
+  ),
+  /// A @namespace rule prelude.
+  Namespace(Option<CowRcStr<'i>>, CowRcStr<'i>),
+  /// A @charset rule prelude.
+  Charset,
+  /// A @nest prelude.
+  Nest(SelectorList<'i>),
+  /// An @layer prelude.
+  Layer(Vec<LayerName<'i>>),
+  /// An @property prelude.
+  Property(DashedIdent<'i>),
+  /// A @container prelude.
+  Container(Option<ContainerName<'i>>, ContainerCondition<'i>),
+  /// A @starting-style prelude.
+  StartingStyle,
+  /// A @scope rule prelude.
+  Scope(Option<SelectorList<'i>>, Option<SelectorList<'i>>),
+  /// A @view-transition rule prelude.
+  ViewTransition,
+  /// An unknown prelude.
+  Unknown(CowArcStr<'i>, TokenList<'i>),
+  /// A custom prelude.
+  Custom(T),
+}
+
+impl<'i, T> AtRulePrelude<'i, T> {
+  // https://drafts.csswg.org/css-nesting/#conditionals
+  //     In addition to nested style rules, this specification allows nested group rules inside
+  //     of style rules: any at-rule whose body contains style rules can be nested inside of a
+  //     style rule as well.
+  fn allowed_in_style_rule(&self) -> bool {
+    match *self {
+      Self::Media(..)
+      | Self::Supports(..)
+      | Self::Container(..)
+      | Self::MozDocument
+      | Self::Layer(..)
+      | Self::StartingStyle
+      | Self::Scope(..)
+      | Self::Nest(..)
+      | Self::Unknown(..)
+      | Self::Custom(..) => true,
+
+      Self::Namespace(..)
+      | Self::FontFace
+      | Self::FontFeatureValues(..)
+      | Self::FontPaletteValues(..)
+      | Self::CounterStyle(..)
+      | Self::Keyframes(..)
+      | Self::Page(..)
+      | Self::Property(..)
+      | Self::Import(..)
+      | Self::CustomMedia(..)
+      | Self::Viewport(..)
+      | Self::Charset
+      | Self::ViewTransition => false,
+    }
+  }
+}
+
+impl<'a, 'o, 'i, T: crate::traits::AtRuleParser<'i>> AtRuleParser<'i> for TopLevelRuleParser<'a, 'o, 'i, T> {
+  type Prelude = AtRulePrelude<'i, T::Prelude>;
+  type AtRule = ();
+  type Error = ParserError<'i>;
+
+  fn parse_prelude<'t>(
+    &mut self,
+    name: CowRcStr<'i>,
+    input: &mut Parser<'i, 't>,
+  ) -> Result<Self::Prelude, ParseError<'i, Self::Error>> {
+    match_ignore_ascii_case! { &*name,
+      "import" => {
+        if self.state > State::Imports {
+          return Err(input.new_custom_error(ParserError::UnexpectedImportRule))
+        }
+
+        let url_string = input.expect_url_or_string()?.clone();
+
+        let layer = if input.try_parse(|input| input.expect_ident_matching("layer")).is_ok() {
+          Some(None)
+        } else if input.try_parse(|input| input.expect_function_matching("layer")).is_ok() {
+          let name = input.parse_nested_block(LayerName::parse).map(|name| Some(name))?;
+          Some(name)
+        } else {
+          None
+        };
+
+        let supports = if input.try_parse(|input| input.expect_function_matching("supports")).is_ok() {
+          Some(input.parse_nested_block(|input| {
+            input.try_parse(SupportsCondition::parse).or_else(|_| SupportsCondition::parse_declaration(input))
+          })?)
+        } else {
+          None
+        };
+        let media = MediaList::parse(input, &self.options)?;
+        return Ok(AtRulePrelude::Import(url_string, media, supports, layer));
+      },
+      "namespace" => {
+        if self.state > State::Namespaces {
+          return Err(input.new_custom_error(ParserError::UnexpectedNamespaceRule))
+        }
+
+        let prefix = input.try_parse(|input| input.expect_ident_cloned()).ok();
+        let namespace = input.expect_url_or_string()?;
+        let prelude = AtRulePrelude::Namespace(prefix, namespace);
+        return Ok(prelude);
+      },
+      "charset" => {
+        // @charset is removed by rust-cssparser if it’s the first rule in the stylesheet.
+        // Anything left is technically invalid, however, users often concatenate CSS files
+        // together, so we are more lenient and simply ignore @charset rules in the middle of a file.
+        input.expect_string()?;
+        return Ok(AtRulePrelude::Charset)
+      },
+      "custom-media" if self.options.flags.contains(ParserFlags::CUSTOM_MEDIA) => {
+        let name = DashedIdent::parse(input)?;
+        let media = MediaList::parse(input, &self.options)?;
+        return Ok(AtRulePrelude::CustomMedia(name, media))
+      },
+      "property" => {
+        let name = DashedIdent::parse(input)?;
+        return Ok(AtRulePrelude::Property(name))
+      },
+      _ => {}
+    }
+
+    AtRuleParser::parse_prelude(&mut self.nested(), name, input)
+  }
+
+  #[inline]
+  fn parse_block<'t>(
+    &mut self,
+    prelude: Self::Prelude,
+    start: &ParserState,
+    input: &mut Parser<'i, 't>,
+  ) -> Result<Self::AtRule, ParseError<'i, Self::Error>> {
+    self.state = State::Body;
+    AtRuleParser::parse_block(&mut self.nested(), prelude, start, input)
+  }
+
+  #[inline]
+  fn rule_without_block(
+    &mut self,
+    prelude: AtRulePrelude<'i, T::Prelude>,
+    start: &ParserState,
+  ) -> Result<Self::AtRule, ()> {
+    let loc = start.source_location();
+    let loc = Location {
+      source_index: self.options.source_index,
+      line: loc.line,
+      column: loc.column,
+    };
+
+    match prelude {
+      AtRulePrelude::Import(url, media, supports, layer) => {
+        self.state = State::Imports;
+        self.rules.0.push(CssRule::Import(ImportRule {
+          url: url.into(),
+          layer,
+          supports,
+          media,
+          loc,
+        }));
+        Ok(())
+      }
+      AtRulePrelude::Namespace(prefix, url) => {
+        self.state = State::Namespaces;
+
+        self.rules.0.push(CssRule::Namespace(NamespaceRule {
+          prefix: prefix.map(|x| x.into()),
+          url: url.into(),
+          loc,
+        }));
+        Ok(())
+      }
+      AtRulePrelude::CustomMedia(name, query) => {
+        self.state = State::Body;
+        self.rules.0.push(CssRule::CustomMedia(CustomMediaRule { name, query, loc }));
+        Ok(())
+      }
+      AtRulePrelude::Layer(_) => {
+        // @layer statements are allowed before @import rules, but cannot be interleaved.
+        if self.state <= State::Layers {
+          self.state = State::Layers;
+        } else {
+          self.state = State::Body;
+        }
+        AtRuleParser::rule_without_block(&mut self.nested(), prelude, start)
+      }
+      AtRulePrelude::Charset => Ok(()),
+      AtRulePrelude::Unknown(name, prelude) => {
+        self.rules.0.push(CssRule::Unknown(UnknownAtRule {
+          name,
+          prelude,
+          block: None,
+          loc,
+        }));
+        Ok(())
+      }
+      AtRulePrelude::Custom(_) => {
+        self.state = State::Body;
+        AtRuleParser::rule_without_block(&mut self.nested(), prelude, start)
+      }
+      _ => Err(()),
+    }
+  }
+}
+
+impl<'a, 'o, 'i, T: crate::traits::AtRuleParser<'i>> QualifiedRuleParser<'i>
+  for TopLevelRuleParser<'a, 'o, 'i, T>
+{
+  type Prelude = SelectorList<'i>;
+  type QualifiedRule = ();
+  type Error = ParserError<'i>;
+
+  #[inline]
+  fn parse_prelude<'t>(
+    &mut self,
+    input: &mut Parser<'i, 't>,
+  ) -> Result<Self::Prelude, ParseError<'i, Self::Error>> {
+    self.state = State::Body;
+    QualifiedRuleParser::parse_prelude(&mut self.nested(), input)
+  }
+
+  #[inline]
+  fn parse_block<'t>(
+    &mut self,
+    prelude: Self::Prelude,
+    start: &ParserState,
+    input: &mut Parser<'i, 't>,
+  ) -> Result<Self::QualifiedRule, ParseError<'i, Self::Error>> {
+    QualifiedRuleParser::parse_block(&mut self.nested(), prelude, start, input)
+  }
+}
+
+pub struct NestedRuleParser<'a, 'o, 'i, T: crate::traits::AtRuleParser<'i>> {
+  pub options: &'a ParserOptions<'o, 'i>,
+  pub at_rule_parser: &'a mut T,
+  declarations: DeclarationList<'i>,
+  important_declarations: DeclarationList<'i>,
+  rules: &'a mut CssRuleList<'i, T::AtRule>,
+  is_in_style_rule: bool,
+  allow_declarations: bool,
+}
+
+impl<'a, 'o, 'b, 'i, T: crate::traits::AtRuleParser<'i>> NestedRuleParser<'a, 'o, 'i, T> {
+  pub fn parse_nested<'t>(
+    &mut self,
+    input: &mut Parser<'i, 't>,
+    is_style_rule: bool,
+  ) -> Result<(DeclarationBlock<'i>, CssRuleList<'i, T::AtRule>), ParseError<'i, ParserError<'i>>> {
+    let mut rules = CssRuleList(vec![]);
+    let mut nested_parser = NestedRuleParser {
+      options: self.options,
+      at_rule_parser: self.at_rule_parser,
+      declarations: DeclarationList::new(),
+      important_declarations: DeclarationList::new(),
+      rules: &mut rules,
+      is_in_style_rule: self.is_in_style_rule || is_style_rule,
+      allow_declarations: self.allow_declarations || self.is_in_style_rule || is_style_rule,
+    };
+
+    let parse_declarations = nested_parser.parse_declarations();
+    let mut errors = Vec::new();
+    let mut iter = RuleBodyParser::new(input, &mut nested_parser);
+    while let Some(result) = iter.next() {
+      match result {
+        Ok(()) => {}
+        Err((e, _)) => {
+          if parse_declarations {
+            iter.parser.declarations.clear();
+            iter.parser.important_declarations.clear();
+            errors.push(e);
+          } else {
+            if iter.parser.options.error_recovery {
+              iter.parser.options.warn(e);
+              continue;
+            }
+            return Err(e);
+          }
+        }
+      }
+    }
+
+    if parse_declarations {
+      if !errors.is_empty() {
+        if self.options.error_recovery {
+          for err in errors {
+            self.options.warn(err);
+          }
+        } else {
+          return Err(errors.remove(0));
+        }
+      }
+    }
+
+    Ok((
+      DeclarationBlock {
+        declarations: nested_parser.declarations,
+        important_declarations: nested_parser.important_declarations,
+      },
+      rules,
+    ))
+  }
+
+  fn parse_style_block<'t>(
+    &mut self,
+    input: &mut Parser<'i, 't>,
+  ) -> Result<CssRuleList<'i, T::AtRule>, ParseError<'i, ParserError<'i>>> {
+    let loc = input.current_source_location();
+    let loc = Location {
+      source_index: self.options.source_index,
+      line: loc.line,
+      column: loc.column,
+    };
+
+    // Declarations can be immediately within @media and @supports blocks that are nested within a parent style rule.
+    // These are wrapped in an (invisible) NestedDeclarationsRule.
+    let (declarations, mut rules) = self.parse_nested(input, false)?;
+
+    if declarations.len() > 0 {
+      rules.0.insert(
+        0,
+        CssRule::NestedDeclarations(NestedDeclarationsRule { declarations, loc }),
+      )
+    }
+
+    Ok(rules)
+  }
+
+  fn loc(&self, start: &ParserState) -> Location {
+    let loc = start.source_location();
+    Location {
+      source_index: self.options.source_index,
+      line: loc.line,
+      column: loc.column,
+    }
+  }
+}
+
+impl<'a, 'o, 'b, 'i, T: crate::traits::AtRuleParser<'i>> AtRuleParser<'i> for NestedRuleParser<'a, 'o, 'i, T> {
+  type Prelude = AtRulePrelude<'i, T::Prelude>;
+  type AtRule = ();
+  type Error = ParserError<'i>;
+
+  fn parse_prelude<'t>(
+    &mut self,
+    name: CowRcStr<'i>,
+    input: &mut Parser<'i, 't>,
+  ) -> Result<Self::Prelude, ParseError<'i, Self::Error>> {
+    let result = match_ignore_ascii_case! { &*name,
+      "media" => {
+        let media = MediaList::parse(input, &self.options)?;
+        AtRulePrelude::Media(media)
+      },
+      "supports" => {
+        let cond = SupportsCondition::parse(input, )?;
+        AtRulePrelude::Supports(cond)
+      },
+      "font-face" => {
+        AtRulePrelude::FontFace
+      },
+      // "font-feature-values" => {
+      //     if !cfg!(feature = "gecko") {
+      //         // Support for this rule is not fully implemented in Servo yet.
+      //         return Err(input.new_custom_error(StyleParseErrorKind::UnsupportedAtRule(name.clone())))
+      //     }
+      //     let family_names = parse_family_name_list(self.context, input)?;
+      //     Ok(AtRuleType::WithBlock(AtRuleBlockPrelude::FontFeatureValues(family_names)))
+      // },
+      "font-feature-values" => {
+        let names = match Vec::<FamilyName>::parse(input) {
+          Ok(names) => names,
+          Err(e) => return Err(e)
+        };
+
+        AtRulePrelude::FontFeatureValues(names)
+      },
+      "font-palette-values" => {
+        let name = DashedIdent::parse(input)?;
+        AtRulePrelude::FontPaletteValues(name)
+      },
+      "counter-style" => {
+        let name = CustomIdent::parse(input)?;
+        AtRulePrelude::CounterStyle(name)
+      },
+      "viewport" | "-ms-viewport" => {
+        let prefix = if starts_with_ignore_ascii_case(&*name, "-ms") {
+          VendorPrefix::Ms
+        } else {
+          VendorPrefix::None
+        };
+        AtRulePrelude::Viewport(prefix)
+      },
+      "keyframes" | "-webkit-keyframes" | "-moz-keyframes" | "-o-keyframes" | "-ms-keyframes" => {
+        let prefix = if starts_with_ignore_ascii_case(&*name, "-webkit-") {
+          VendorPrefix::WebKit
+        } else if starts_with_ignore_ascii_case(&*name, "-moz-") {
+          VendorPrefix::Moz
+        } else if starts_with_ignore_ascii_case(&*name, "-o-") {
+          VendorPrefix::O
+        } else if starts_with_ignore_ascii_case(&*name, "-ms-") {
+          VendorPrefix::Ms
+        } else {
+          VendorPrefix::None
+        };
+
+        let name = input.try_parse(KeyframesName::parse)?;
+        AtRulePrelude::Keyframes(name, prefix)
+      },
+      "page" => {
+        let selectors = input.try_parse(|input| input.parse_comma_separated(PageSelector::parse)).unwrap_or_default();
+        AtRulePrelude::Page(selectors)
+      },
+      "-moz-document" => {
+        // Firefox only supports the url-prefix() function with no arguments as a legacy CSS hack.
+        // See https://css-tricks.com/snippets/css/css-hacks-targeting-firefox/
+        input.expect_function_matching("url-prefix")?;
+        input.parse_nested_block(|input| {
+          // Firefox also allows an empty string as an argument...
+          // https://github.com/mozilla/gecko-dev/blob/0077f2248712a1b45bf02f0f866449f663538164/servo/components/style/stylesheets/document_rule.rs#L303
+          let _ = input.try_parse(|input| -> Result<(), ParseError<'i, Self::Error>> {
+            let s = input.expect_string()?;
+            if !s.is_empty() {
+              return Err(input.new_custom_error(ParserError::InvalidValue))
+            }
+            Ok(())
+          });
+          input.expect_exhausted()?;
+          Ok(())
+        })?;
+
+        AtRulePrelude::MozDocument
+      },
+      "layer" => {
+        let names = match Vec::<LayerName>::parse(input) {
+          Ok(names) => names,
+          Err(ParseError { kind: ParseErrorKind::Basic(BasicParseErrorKind::EndOfInput), .. }) => Vec::new(),
+          Err(e) => return Err(e)
+        };
+        AtRulePrelude::Layer(names)
+      },
+      "container" => {
+        let name = input.try_parse(ContainerName::parse).ok();
+        let condition = ContainerCondition::parse_with_options(input, &self.options)?;
+        AtRulePrelude::Container(name, condition)
+      },
+      "starting-style" => {
+        AtRulePrelude::StartingStyle
+      },
+      "scope" => {
+        let selector_parser = SelectorParser {
+          is_nesting_allowed: true,
+          options: &self.options,
+        };
+
+        let scope_start = if input.try_parse(|input| input.expect_parenthesis_block()).is_ok() {
+          Some(input.parse_nested_block(|input| {
+            // https://drafts.csswg.org/css-cascade-6/#scoped-rules
+            // TODO: disallow pseudo elements?
+            SelectorList::parse_relative(&selector_parser, input, ParseErrorRecovery::IgnoreInvalidSelector, NestingRequirement::None)
+          })?)
+        } else {
+          None
+        };
+
+        let scope_end = if input.try_parse(|input| input.expect_ident_matching("to")).is_ok() {
+          input.expect_parenthesis_block()?;
+          Some(input.parse_nested_block(|input| {
+            SelectorList::parse_relative(&selector_parser, input, ParseErrorRecovery::IgnoreInvalidSelector, NestingRequirement::None)
+          })?)
+        } else {
+          None
+        };
+
+        AtRulePrelude::Scope(scope_start, scope_end)
+      },
+      "view-transition" => {
+        AtRulePrelude::ViewTransition
+      },
+      "nest" if self.is_in_style_rule => {
+        self.options.warn(input.new_custom_error(ParserError::DeprecatedNestRule));
+        let selector_parser = SelectorParser {
+          is_nesting_allowed: true,
+          options: &self.options,
+        };
+        let selectors = SelectorList::parse(&selector_parser, input, ParseErrorRecovery::DiscardList, NestingRequirement::Contained)?;
+        AtRulePrelude::Nest(selectors)
+      },
+
+      "value" if self.options.css_modules.is_some() => {
+        return Err(input.new_custom_error(ParserError::DeprecatedCssModulesValueRule));
+      },
+
+
+      _ => parse_custom_at_rule_prelude(&name, input, self.options, self.at_rule_parser)?
+    };
+
+    if self.is_in_style_rule && !result.allowed_in_style_rule() {
+      return Err(input.new_error(BasicParseErrorKind::AtRuleInvalid(name.clone())));
+    }
+
+    Ok(result)
+  }
+
+  #[inline]
+  fn rule_without_block(
+    &mut self,
+    prelude: AtRulePrelude<'i, T::Prelude>,
+    start: &ParserState,
+  ) -> Result<Self::AtRule, ()> {
+    let loc = self.loc(start);
+    match prelude {
+      AtRulePrelude::Layer(names) => {
+        if self.is_in_style_rule || names.is_empty() {
+          return Err(());
+        }
+
+        self.rules.0.push(CssRule::LayerStatement(LayerStatementRule { names, loc }));
+        Ok(())
+      }
+      AtRulePrelude::Unknown(name, prelude) => {
+        self.rules.0.push(CssRule::Unknown(UnknownAtRule {
+          name,
+          prelude,
+          block: None,
+          loc,
+        }));
+        Ok(())
+      }
+      AtRulePrelude::Custom(prelude) => {
+        self.rules.0.push(parse_custom_at_rule_without_block(
+          prelude,
+          start,
+          self.options,
+          self.at_rule_parser,
+          self.is_in_style_rule,
+        )?);
+        Ok(())
+      }
+      _ => Err(()),
+    }
+  }
+
+  fn parse_block<'t>(
+    &mut self,
+    prelude: Self::Prelude,
+    start: &ParserState,
+    input: &mut Parser<'i, 't>,
+  ) -> Result<(), ParseError<'i, Self::Error>> {
+    let loc = self.loc(start);
+    match prelude {
+      AtRulePrelude::FontFace => {
+        let mut decl_parser = FontFaceDeclarationParser;
+        let mut parser = RuleBodyParser::new(input, &mut decl_parser);
+        let mut properties = vec![];
+        while let Some(decl) = parser.next() {
+          if let Ok(decl) = decl {
+            properties.push(decl);
+          }
+        }
+        self.rules.0.push(CssRule::FontFace(FontFaceRule { properties, loc }));
+        Ok(())
+      }
+      // AtRuleBlockPrelude::FontFeatureValues(family_names) => {
+      //     let context = ParserContext::new_with_rule_type(
+      //         self.context,
+      //         CssRuleType::FontFeatureValues,
+      //         self.namespaces,
+      //     );
+
+      //     Ok(CssRule::FontFeatureValues(Arc::new(self.shared_lock.wrap(
+      //         FontFeatureValuesRule::parse(
+      //             &context,
+      //             input,
+      //             family_names,
+      //             start.source_location(),
+      //         ),
+      //     ))))
+      // },
+      AtRulePrelude::FontPaletteValues(name) => {
+        let rule = FontPaletteValuesRule::parse(name, input, loc)?;
+        self.rules.0.push(CssRule::FontPaletteValues(rule));
+        Ok(())
+      }
+      AtRulePrelude::CounterStyle(name) => {
+        self.rules.0.push(CssRule::CounterStyle(CounterStyleRule {
+          name,
+          declarations: DeclarationBlock::parse(input, self.options)?,
+          loc,
+        }));
+        Ok(())
+      }
+      AtRulePrelude::Media(query) => {
+        let rules = self.parse_style_block(input)?;
+        self.rules.0.push(CssRule::Media(MediaRule { query, rules, loc }));
+        Ok(())
+      }
+      AtRulePrelude::Supports(condition) => {
+        let rules = self.parse_style_block(input)?;
+        self.rules.0.push(CssRule::Supports(SupportsRule { condition, rules, loc }));
+        Ok(())
+      }
+      AtRulePrelude::Container(name, condition) => {
+        let rules = self.parse_style_block(input)?;
+        self.rules.0.push(CssRule::Container(ContainerRule {
+          name,
+          condition,
+          rules,
+          loc,
+        }));
+        Ok(())
+      }
+      AtRulePrelude::Scope(scope_start, scope_end) => {
+        let rules = self.parse_style_block(input)?;
+        self.rules.0.push(CssRule::Scope(ScopeRule {
+          scope_start,
+          scope_end,
+          rules,
+          loc,
+        }));
+        Ok(())
+      }
+      AtRulePrelude::Viewport(vendor_prefix) => {
+        self.rules.0.push(CssRule::Viewport(ViewportRule {
+          vendor_prefix,
+          // TODO: parse viewport descriptors rather than properties
+          // https://drafts.csswg.org/css-device-adapt/#viewport-desc
+          declarations: DeclarationBlock::parse(input, self.options)?,
+          loc,
+        }));
+        Ok(())
+      }
+      AtRulePrelude::Keyframes(name, vendor_prefix) => {
+        let mut parser = KeyframeListParser;
+        let iter = RuleBodyParser::new(input, &mut parser);
+        self.rules.0.push(CssRule::Keyframes(KeyframesRule {
+          name,
+          keyframes: iter.filter_map(Result::ok).collect(),
+          vendor_prefix,
+          loc,
+        }));
+        Ok(())
+      }
+      AtRulePrelude::Page(selectors) => {
+        let rule = PageRule::parse(selectors, input, loc, self.options)?;
+        self.rules.0.push(CssRule::Page(rule));
+        Ok(())
+      }
+      AtRulePrelude::MozDocument => {
+        let rules = self.parse_style_block(input)?;
+        self.rules.0.push(CssRule::MozDocument(MozDocumentRule { rules, loc }));
+        Ok(())
+      }
+      AtRulePrelude::Layer(names) => {
+        let name = if names.is_empty() {
+          None
+        } else if names.len() == 1 {
+          names.into_iter().next()
+        } else {
+          return Err(input.new_error(BasicParseErrorKind::AtRuleBodyInvalid));
+        };
+
+        let rules = self.parse_style_block(input)?;
+        self.rules.0.push(CssRule::LayerBlock(LayerBlockRule { name, rules, loc }));
+        Ok(())
+      }
+      AtRulePrelude::Property(name) => {
+        self.rules.0.push(CssRule::Property(PropertyRule::parse(name, input, loc)?));
+        Ok(())
+      }
+      AtRulePrelude::Import(..)
+      | AtRulePrelude::Namespace(..)
+      | AtRulePrelude::CustomMedia(..)
+      | AtRulePrelude::Charset => {
+        // These rules don't have blocks.
+        Err(input.new_unexpected_token_error(Token::CurlyBracketBlock))
+      }
+      AtRulePrelude::StartingStyle => {
+        let rules = self.parse_style_block(input)?;
+        self.rules.0.push(CssRule::StartingStyle(StartingStyleRule { rules, loc }));
+        Ok(())
+      }
+      AtRulePrelude::ViewTransition => {
+        self
+          .rules
+          .0
+          .push(CssRule::ViewTransition(ViewTransitionRule::parse(input, loc)?));
+        Ok(())
+      }
+      AtRulePrelude::Nest(selectors) => {
+        let (declarations, rules) = self.parse_nested(input, true)?;
+        self.rules.0.push(CssRule::Nesting(NestingRule {
+          style: StyleRule {
+            selectors,
+            declarations,
+            vendor_prefix: VendorPrefix::empty(),
+            rules,
+            loc,
+          },
+          loc,
+        }));
+        Ok(())
+      }
+      AtRulePrelude::FontFeatureValues(family_names) => {
+        let rule = FontFeatureValuesRule::parse(family_names, input, loc, self.options)?;
+        self.rules.0.push(CssRule::FontFeatureValues(rule));
+        Ok(())
+      }
+      AtRulePrelude::Unknown(name, prelude) => {
+        self.rules.0.push(CssRule::Unknown(UnknownAtRule {
+          name,
+          prelude,
+          block: Some(TokenList::parse(input, &self.options, 0)?),
+          loc,
+        }));
+        Ok(())
+      }
+      AtRulePrelude::Custom(prelude) => {
+        self.rules.0.push(parse_custom_at_rule_body(
+          prelude,
+          input,
+          start,
+          self.options,
+          self.at_rule_parser,
+          self.is_in_style_rule,
+        )?);
+        Ok(())
+      }
+    }
+  }
+}
+
+impl<'a, 'o, 'b, 'i, T: crate::traits::AtRuleParser<'i>> QualifiedRuleParser<'i>
+  for NestedRuleParser<'a, 'o, 'i, T>
+{
+  type Prelude = SelectorList<'i>;
+  type QualifiedRule = ();
+  type Error = ParserError<'i>;
+
+  fn parse_prelude<'t>(
+    &mut self,
+    input: &mut Parser<'i, 't>,
+  ) -> Result<Self::Prelude, ParseError<'i, Self::Error>> {
+    let selector_parser = SelectorParser {
+      is_nesting_allowed: true,
+      options: &self.options,
+    };
+    if self.is_in_style_rule {
+      SelectorList::parse_relative(
+        &selector_parser,
+        input,
+        ParseErrorRecovery::DiscardList,
+        NestingRequirement::Implicit,
+      )
+    } else {
+      SelectorList::parse(
+        &selector_parser,
+        input,
+        ParseErrorRecovery::DiscardList,
+        NestingRequirement::None,
+      )
+    }
+  }
+
+  fn parse_block<'t>(
+    &mut self,
+    selectors: Self::Prelude,
+    start: &ParserState,
+    input: &mut Parser<'i, 't>,
+  ) -> Result<(), ParseError<'i, Self::Error>> {
+    let loc = self.loc(start);
+    let (declarations, rules) = self.parse_nested(input, true)?;
+    self.rules.0.push(CssRule::Style(StyleRule {
+      selectors,
+      vendor_prefix: VendorPrefix::empty(),
+      declarations,
+      rules,
+      loc,
+    }));
+    Ok(())
+  }
+}
+
+/// Parse a declaration within {} block: `color: blue`
+impl<'a, 'o, 'i, T: crate::traits::AtRuleParser<'i>> cssparser::DeclarationParser<'i>
+  for NestedRuleParser<'a, 'o, 'i, T>
+{
+  type Declaration = ();
+  type Error = ParserError<'i>;
+
+  fn parse_value<'t>(
+    &mut self,
+    name: CowRcStr<'i>,
+    input: &mut cssparser::Parser<'i, 't>,
+  ) -> Result<Self::Declaration, cssparser::ParseError<'i, Self::Error>> {
+    if self.rules.0.is_empty() {
+      parse_declaration(
+        name,
+        input,
+        &mut self.declarations,
+        &mut self.important_declarations,
+        &self.options,
+      )
+    } else if let Some(CssRule::NestedDeclarations(last)) = self.rules.0.last_mut() {
+      parse_declaration(
+        name,
+        input,
+        &mut last.declarations.declarations,
+        &mut last.declarations.important_declarations,
+        &self.options,
+      )
+    } else {
+      let loc = self.loc(&input.state());
+      let mut nested = NestedDeclarationsRule {
+        declarations: DeclarationBlock::new(),
+        loc,
+      };
+
+      parse_declaration(
+        name,
+        input,
+        &mut nested.declarations.declarations,
+        &mut nested.declarations.important_declarations,
+        &self.options,
+      )?;
+
+      self.rules.0.push(CssRule::NestedDeclarations(nested));
+      Ok(())
+    }
+  }
+}
+
+impl<'a, 'o, 'b, 'i, T: crate::traits::AtRuleParser<'i>> RuleBodyItemParser<'i, (), ParserError<'i>>
+  for NestedRuleParser<'a, 'o, 'i, T>
+{
+  fn parse_qualified(&self) -> bool {
+    true
+  }
+
+  fn parse_declarations(&self) -> bool {
+    self.allow_declarations
+  }
+}
+
+fn parse_custom_at_rule_prelude<'i, 't, T: crate::traits::AtRuleParser<'i>>(
+  name: &CowRcStr<'i>,
+  input: &mut Parser<'i, 't>,
+  options: &ParserOptions<'_, 'i>,
+  at_rule_parser: &mut T,
+) -> Result<AtRulePrelude<'i, T::Prelude>, ParseError<'i, ParserError<'i>>> {
+  match at_rule_parser.parse_prelude(name.clone(), input, options) {
+    Ok(prelude) => return Ok(AtRulePrelude::Custom(prelude)),
+    Err(ParseError {
+      kind: ParseErrorKind::Basic(BasicParseErrorKind::AtRuleInvalid(..)),
+      ..
+    }) => {}
+    Err(err) => {
+      return Err(match &err.kind {
+        ParseErrorKind::Basic(kind) => ParseError {
+          kind: ParseErrorKind::Basic(kind.clone()),
+          location: err.location,
+        },
+        _ => input.new_custom_error(ParserError::AtRulePreludeInvalid),
+      })
+    }
+  }
+
+  options.warn(input.new_error(BasicParseErrorKind::AtRuleInvalid(name.clone())));
+  input.skip_whitespace();
+  let tokens = TokenList::parse(input, &options, 0)?;
+  Ok(AtRulePrelude::Unknown(name.into(), tokens))
+}
+
+fn parse_custom_at_rule_body<'i, 't, T: crate::traits::AtRuleParser<'i>>(
+  prelude: T::Prelude,
+  input: &mut Parser<'i, 't>,
+  start: &ParserState,
+  options: &ParserOptions<'_, 'i>,
+  at_rule_parser: &mut T,
+  is_nested: bool,
+) -> Result<CssRule<'i, T::AtRule>, ParseError<'i, ParserError<'i>>> {
+  at_rule_parser
+    .parse_block(prelude, start, input, options, is_nested)
+    .map(|prelude| CssRule::Custom(prelude))
+    .map_err(|err| match &err.kind {
+      ParseErrorKind::Basic(kind) => ParseError {
+        kind: ParseErrorKind::Basic(kind.clone()),
+        location: err.location,
+      },
+      _ => input.new_error(BasicParseErrorKind::AtRuleBodyInvalid),
+    })
+}
+
+fn parse_custom_at_rule_without_block<'i, 't, T: crate::traits::AtRuleParser<'i>>(
+  prelude: T::Prelude,
+  start: &ParserState,
+  options: &ParserOptions<'_, 'i>,
+  at_rule_parser: &mut T,
+  is_nested: bool,
+) -> Result<CssRule<'i, T::AtRule>, ()> {
+  at_rule_parser
+    .rule_without_block(prelude, start, options, is_nested)
+    .map(|prelude| CssRule::Custom(prelude))
+}
+
+pub fn parse_rule_list<'a, 'o, 'i, 't, T: crate::traits::AtRuleParser<'i>>(
+  input: &mut Parser<'i, 't>,
+  options: &'a ParserOptions<'o, 'i>,
+  at_rule_parser: &mut T,
+) -> Result<CssRuleList<'i, T::AtRule>, ParseError<'i, ParserError<'i>>> {
+  let mut parser = NestedRuleParser {
+    options,
+    at_rule_parser,
+    declarations: DeclarationList::new(),
+    important_declarations: DeclarationList::new(),
+    rules: &mut CssRuleList(Vec::new()),
+    is_in_style_rule: false,
+    allow_declarations: false,
+  };
+
+  let (_, rules) = parser.parse_nested(input, false)?;
+  Ok(rules)
+}
+
+pub fn parse_style_block<'a, 'o, 'i, 't, T: crate::traits::AtRuleParser<'i>>(
+  input: &mut Parser<'i, 't>,
+  options: &'a ParserOptions<'o, 'i>,
+  at_rule_parser: &mut T,
+  is_nested: bool,
+) -> Result<CssRuleList<'i, T::AtRule>, ParseError<'i, ParserError<'i>>> {
+  let mut parser = NestedRuleParser {
+    options,
+    at_rule_parser,
+    declarations: DeclarationList::new(),
+    important_declarations: DeclarationList::new(),
+    rules: &mut CssRuleList(Vec::new()),
+    is_in_style_rule: is_nested,
+    allow_declarations: true,
+  };
+
+  parser.parse_style_block(input)
+}
+
+#[inline]
+pub fn starts_with_ignore_ascii_case(string: &str, prefix: &str) -> bool {
+  string.len() >= prefix.len() && string.as_bytes()[0..prefix.len()].eq_ignore_ascii_case(prefix.as_bytes())
+}
diff --git a/src/prefixes.rs b/src/prefixes.rs
new file mode 100644
index 0000000..7d64337
--- /dev/null
+++ b/src/prefixes.rs
@@ -0,0 +1,2292 @@
+// This file is autogenerated by build-prefixes.js. DO NOT EDIT!
+
+use crate::targets::Browsers;
+use crate::vendor_prefix::VendorPrefix;
+
+#[allow(dead_code)]
+pub enum Feature {
+  AlignContent,
+  AlignItems,
+  AlignSelf,
+  Animation,
+  AnimationDelay,
+  AnimationDirection,
+  AnimationDuration,
+  AnimationFillMode,
+  AnimationIterationCount,
+  AnimationName,
+  AnimationPlayState,
+  AnimationTimingFunction,
+  AnyPseudo,
+  Appearance,
+  AtKeyframes,
+  AtResolution,
+  AtViewport,
+  BackdropFilter,
+  BackfaceVisibility,
+  BackgroundClip,
+  BackgroundOrigin,
+  BackgroundSize,
+  BorderBlockEnd,
+  BorderBlockStart,
+  BorderBottomLeftRadius,
+  BorderBottomRightRadius,
+  BorderImage,
+  BorderInlineEnd,
+  BorderInlineStart,
+  BorderRadius,
+  BorderTopLeftRadius,
+  BorderTopRightRadius,
+  BoxDecorationBreak,
+  BoxShadow,
+  BoxSizing,
+  BreakAfter,
+  BreakBefore,
+  BreakInside,
+  Calc,
+  ClipPath,
+  ColorAdjust,
+  ColumnCount,
+  ColumnFill,
+  ColumnGap,
+  ColumnRule,
+  ColumnRuleColor,
+  ColumnRuleStyle,
+  ColumnRuleWidth,
+  ColumnSpan,
+  ColumnWidth,
+  Columns,
+  CrossFade,
+  DisplayFlex,
+  DisplayGrid,
+  Element,
+  Fill,
+  FillAvailable,
+  Filter,
+  FilterFunction,
+  FitContent,
+  Flex,
+  FlexBasis,
+  FlexDirection,
+  FlexFlow,
+  FlexGrow,
+  FlexShrink,
+  FlexWrap,
+  FlowFrom,
+  FlowInto,
+  FontFeatureSettings,
+  FontKerning,
+  FontLanguageOverride,
+  FontVariantLigatures,
+  Grab,
+  Grabbing,
+  GridArea,
+  GridColumn,
+  GridColumnAlign,
+  GridColumnEnd,
+  GridColumnStart,
+  GridRow,
+  GridRowAlign,
+  GridRowEnd,
+  GridRowStart,
+  GridTemplate,
+  GridTemplateAreas,
+  GridTemplateColumns,
+  GridTemplateRows,
+  Hyphens,
+  ImageRendering,
+  ImageSet,
+  InlineFlex,
+  InlineGrid,
+  Isolate,
+  IsolateOverride,
+  JustifyContent,
+  LinearGradient,
+  MarginBlockEnd,
+  MarginBlockStart,
+  MarginInlineEnd,
+  MarginInlineStart,
+  Mask,
+  MaskBorder,
+  MaskBorderOutset,
+  MaskBorderRepeat,
+  MaskBorderSlice,
+  MaskBorderSource,
+  MaskBorderWidth,
+  MaskClip,
+  MaskComposite,
+  MaskImage,
+  MaskOrigin,
+  MaskPosition,
+  MaskRepeat,
+  MaskSize,
+  MaxContent,
+  MinContent,
+  ObjectFit,
+  ObjectPosition,
+  Order,
+  OverscrollBehavior,
+  PaddingBlockEnd,
+  PaddingBlockStart,
+  PaddingInlineEnd,
+  PaddingInlineStart,
+  Perspective,
+  PerspectiveOrigin,
+  Pixelated,
+  PlaceSelf,
+  Plaintext,
+  PrintColorAdjust,
+  PseudoClassAnyLink,
+  PseudoClassAutofill,
+  PseudoClassFullscreen,
+  PseudoClassPlaceholderShown,
+  PseudoClassReadOnly,
+  PseudoClassReadWrite,
+  PseudoElementBackdrop,
+  PseudoElementFileSelectorButton,
+  PseudoElementPlaceholder,
+  PseudoElementSelection,
+  RadialGradient,
+  RegionFragment,
+  RepeatingLinearGradient,
+  RepeatingRadialGradient,
+  ScrollSnapCoordinate,
+  ScrollSnapDestination,
+  ScrollSnapPointsX,
+  ScrollSnapPointsY,
+  ScrollSnapType,
+  ShapeImageThreshold,
+  ShapeMargin,
+  ShapeOutside,
+  Sticky,
+  Stretch,
+  TabSize,
+  TextAlignLast,
+  TextDecoration,
+  TextDecorationColor,
+  TextDecorationLine,
+  TextDecorationSkip,
+  TextDecorationSkipInk,
+  TextDecorationStyle,
+  TextEmphasis,
+  TextEmphasisColor,
+  TextEmphasisPosition,
+  TextEmphasisStyle,
+  TextOrientation,
+  TextOverflow,
+  TextSizeAdjust,
+  TextSpacing,
+  TouchAction,
+  Transform,
+  TransformOrigin,
+  TransformStyle,
+  Transition,
+  TransitionDelay,
+  TransitionDuration,
+  TransitionProperty,
+  TransitionTimingFunction,
+  UserSelect,
+  WritingMode,
+  ZoomIn,
+  ZoomOut,
+}
+
+impl Feature {
+  pub fn prefixes_for(&self, browsers: Browsers) -> VendorPrefix {
+    let mut prefixes = VendorPrefix::None;
+    match self {
+      Feature::BorderRadius
+      | Feature::BorderTopLeftRadius
+      | Feature::BorderTopRightRadius
+      | Feature::BorderBottomRightRadius
+      | Feature::BorderBottomLeftRadius => {
+        if let Some(version) = browsers.android {
+          if version <= 131328 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.chrome {
+          if version <= 262144 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version >= 131072 && version <= 198144 {
+            prefixes |= VendorPrefix::Moz;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version <= 197120 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version >= 196864 && version <= 262144 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+      }
+      Feature::BoxShadow => {
+        if let Some(version) = browsers.android {
+          if version >= 131328 && version <= 196608 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.chrome {
+          if version >= 262144 && version <= 589824 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version >= 197888 && version <= 198144 {
+            prefixes |= VendorPrefix::Moz;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version >= 197120 && version <= 262656 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version >= 196864 && version <= 327680 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+      }
+      Feature::Animation
+      | Feature::AnimationName
+      | Feature::AnimationDuration
+      | Feature::AnimationDelay
+      | Feature::AnimationDirection
+      | Feature::AnimationFillMode
+      | Feature::AnimationIterationCount
+      | Feature::AnimationPlayState
+      | Feature::AnimationTimingFunction
+      | Feature::AtKeyframes => {
+        if let Some(version) = browsers.android {
+          if version >= 131328 && version <= 263171 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.chrome {
+          if version >= 262144 && version <= 2752512 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version >= 327680 && version <= 983040 {
+            prefixes |= VendorPrefix::Moz;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version >= 197120 && version <= 524544 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version == 786432 {
+            prefixes |= VendorPrefix::O;
+          }
+          if version >= 983040 && version <= 1900544 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version >= 262144 && version <= 524288 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+      }
+      Feature::Transition
+      | Feature::TransitionProperty
+      | Feature::TransitionDuration
+      | Feature::TransitionDelay
+      | Feature::TransitionTimingFunction => {
+        if let Some(version) = browsers.android {
+          if version >= 131328 && version <= 262656 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.chrome {
+          if version >= 262144 && version <= 1638400 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version >= 262144 && version <= 983040 {
+            prefixes |= VendorPrefix::Moz;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version >= 197120 && version <= 393216 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version >= 655360 && version <= 786432 {
+            prefixes |= VendorPrefix::O;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version >= 196864 && version <= 393216 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+      }
+      Feature::Transform | Feature::TransformOrigin => {
+        if let Some(version) = browsers.android {
+          if version >= 131328 && version <= 263171 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.chrome {
+          if version >= 262144 && version <= 2293760 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version >= 197888 && version <= 983040 {
+            prefixes |= VendorPrefix::Moz;
+          }
+        }
+        if let Some(version) = browsers.ie {
+          if version <= 589824 {
+            prefixes |= VendorPrefix::Ms;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version >= 197120 && version <= 524544 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version >= 656640 && version <= 786432 {
+            prefixes |= VendorPrefix::O;
+          }
+          if version >= 983040 && version <= 1441792 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version >= 196864 && version <= 524288 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+      }
+      Feature::Perspective | Feature::PerspectiveOrigin | Feature::TransformStyle => {
+        if let Some(version) = browsers.android {
+          if version >= 196608 && version <= 263171 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.chrome {
+          if version >= 786432 && version <= 2293760 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version >= 655360 && version <= 983040 {
+            prefixes |= VendorPrefix::Moz;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version >= 197120 && version <= 524544 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version >= 983040 && version <= 1441792 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version >= 262144 && version <= 524288 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+      }
+      Feature::BackfaceVisibility => {
+        if let Some(version) = browsers.android {
+          if version >= 196608 && version <= 263171 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.chrome {
+          if version >= 786432 && version <= 2293760 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version >= 655360 && version <= 983040 {
+            prefixes |= VendorPrefix::Moz;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version >= 197120 && version <= 983552 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version >= 983040 && version <= 1441792 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version >= 262144 && version <= 983552 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+      }
+      Feature::LinearGradient
+      | Feature::RepeatingLinearGradient
+      | Feature::RadialGradient
+      | Feature::RepeatingRadialGradient => {
+        if let Some(version) = browsers.android {
+          if version >= 131328 && version <= 262656 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.chrome {
+          if version >= 262144 && version <= 1638400 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version >= 198144 && version <= 983040 {
+            prefixes |= VendorPrefix::Moz;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version >= 197120 && version <= 393216 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version >= 721152 && version <= 786432 {
+            prefixes |= VendorPrefix::O;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version >= 262144 && version <= 393216 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+      }
+      Feature::BoxSizing => {
+        if let Some(version) = browsers.android {
+          if version >= 131328 && version <= 196608 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.chrome {
+          if version >= 262144 && version <= 589824 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version >= 131072 && version <= 1835008 {
+            prefixes |= VendorPrefix::Moz;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version >= 197120 && version <= 262656 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version >= 196864 && version <= 327680 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+      }
+      Feature::Filter => {
+        if let Some(version) = browsers.android {
+          if version >= 263168 && version <= 263171 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.chrome {
+          if version >= 1179648 && version <= 3407872 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version >= 393216 && version <= 589824 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version >= 983040 && version <= 2555904 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version >= 393216 && version <= 589824 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version >= 262144 && version <= 393728 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+      }
+      Feature::FilterFunction => {
+        if let Some(version) = browsers.ios_saf {
+          if version >= 589824 && version <= 590592 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version <= 589824 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+      }
+      Feature::BackdropFilter => {
+        if let Some(version) = browsers.edge {
+          if version >= 1114112 && version <= 1179648 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version >= 589824 && version <= 1115648 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version >= 589824 && version <= 1115648 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+      }
+      Feature::Element => {
+        if let Some(version) = browsers.firefox {
+          if version >= 131072 && version <= 9043968 {
+            prefixes |= VendorPrefix::Moz;
+          }
+        }
+      }
+      Feature::Columns
+      | Feature::ColumnWidth
+      | Feature::ColumnGap
+      | Feature::ColumnRule
+      | Feature::ColumnRuleColor
+      | Feature::ColumnRuleWidth
+      | Feature::ColumnCount
+      | Feature::ColumnRuleStyle
+      | Feature::ColumnSpan
+      | Feature::ColumnFill => {
+        if let Some(version) = browsers.android {
+          if version >= 131328 && version <= 263171 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.chrome {
+          if version >= 262144 && version <= 3211264 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version >= 131072 && version <= 3342336 {
+            prefixes |= VendorPrefix::Moz;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version >= 197120 && version <= 524544 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version >= 983040 && version <= 2359296 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version >= 196864 && version <= 524288 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version <= 262144 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+      }
+      Feature::BreakBefore | Feature::BreakAfter | Feature::BreakInside => {
+        if let Some(version) = browsers.android {
+          if version >= 131328 && version <= 263171 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.chrome {
+          if version >= 262144 && version <= 3211264 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version >= 197120 && version <= 524544 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version >= 983040 && version <= 2359296 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version >= 196864 && version <= 524288 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version <= 262144 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+      }
+      Feature::UserSelect => {
+        if let Some(version) = browsers.android {
+          if version >= 131328 && version <= 263171 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.chrome {
+          if version >= 262144 && version <= 3473408 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version >= 786432 && version <= 1179648 {
+            prefixes |= VendorPrefix::Ms;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version >= 131072 && version <= 4456448 {
+            prefixes |= VendorPrefix::Moz;
+          }
+        }
+        if let Some(version) = browsers.ie {
+          if version >= 655360 {
+            prefixes |= VendorPrefix::Ms;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version >= 197120 && version <= 1180672 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version >= 983040 && version <= 2621440 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version >= 196864 && version <= 1180672 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version >= 262144 && version <= 327680 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+      }
+      Feature::DisplayFlex
+      | Feature::InlineFlex
+      | Feature::Flex
+      | Feature::FlexGrow
+      | Feature::FlexShrink
+      | Feature::FlexBasis
+      | Feature::FlexDirection
+      | Feature::FlexWrap
+      | Feature::FlexFlow
+      | Feature::JustifyContent
+      | Feature::Order
+      | Feature::AlignItems
+      | Feature::AlignSelf
+      | Feature::AlignContent => {
+        if let Some(version) = browsers.android {
+          if version >= 131328 && version <= 262656 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.chrome {
+          if version >= 262144 && version <= 1835008 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version >= 131072 && version <= 1376256 {
+            prefixes |= VendorPrefix::Moz;
+          }
+        }
+        if let Some(version) = browsers.ie {
+          if version == 655360 {
+            prefixes |= VendorPrefix::Ms;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version >= 197120 && version <= 524544 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version >= 983040 && version <= 1048576 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version >= 196864 && version <= 524288 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+      }
+      Feature::Calc => {
+        if let Some(version) = browsers.chrome {
+          if version >= 1245184 && version <= 1638400 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version >= 262144 && version <= 983040 {
+            prefixes |= VendorPrefix::Moz;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version <= 393216 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version <= 393216 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+      }
+      Feature::BackgroundOrigin | Feature::BackgroundSize => {
+        if let Some(version) = browsers.android {
+          if version >= 131328 && version <= 131840 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version <= 198144 {
+            prefixes |= VendorPrefix::Moz;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version <= 655360 {
+            prefixes |= VendorPrefix::O;
+          }
+        }
+      }
+      Feature::BackgroundClip => {
+        if let Some(version) = browsers.android {
+          if version >= 262144 && version <= 263171 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.chrome {
+          if version >= 262144 && version <= 7798784 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version >= 786432 && version <= 917504 {
+            prefixes |= VendorPrefix::Ms;
+          }
+          if version >= 5177344 && version <= 7798784 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version >= 983040 && version <= 6881280 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version >= 197120 && version <= 851968 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version >= 262144 && version <= 1572864 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version >= 262144 && version <= 851968 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+      }
+      Feature::FontFeatureSettings | Feature::FontVariantLigatures | Feature::FontLanguageOverride => {
+        if let Some(version) = browsers.android {
+          if version >= 263168 && version <= 263171 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.chrome {
+          if version >= 1048576 && version <= 3080192 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version >= 262144 && version <= 2162688 {
+            prefixes |= VendorPrefix::Moz;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version >= 983040 && version <= 2228224 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version <= 262144 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+      }
+      Feature::FontKerning => {
+        if let Some(version) = browsers.android {
+          if version <= 263168 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.chrome {
+          if version >= 1900544 && version <= 2097152 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version >= 524288 && version <= 721664 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version >= 1048576 && version <= 1245184 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version >= 458752 && version <= 589824 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+      }
+      Feature::BorderImage => {
+        if let Some(version) = browsers.android {
+          if version >= 131328 && version <= 262656 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.chrome {
+          if version >= 262144 && version <= 917504 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version >= 197888 && version <= 917504 {
+            prefixes |= VendorPrefix::Moz;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version >= 197120 && version <= 327680 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version >= 720896 && version <= 786688 {
+            prefixes |= VendorPrefix::O;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version >= 196864 && version <= 327936 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+      }
+      Feature::PseudoElementSelection => {
+        if let Some(version) = browsers.firefox {
+          if version >= 131072 && version <= 3997696 {
+            prefixes |= VendorPrefix::Moz;
+          }
+        }
+      }
+      Feature::PseudoElementPlaceholder => {
+        if let Some(version) = browsers.android {
+          if version >= 131328 && version <= 263171 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.chrome {
+          if version >= 262144 && version <= 3670016 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version >= 786432 && version <= 1179648 {
+            prefixes |= VendorPrefix::Ms;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version >= 1179648 && version <= 3276800 {
+            prefixes |= VendorPrefix::Moz;
+          }
+        }
+        if let Some(version) = browsers.ie {
+          if version >= 655360 {
+            prefixes |= VendorPrefix::Ms;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version >= 262656 && version <= 655360 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version >= 983040 && version <= 2818048 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version >= 327680 && version <= 655360 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version >= 262144 && version <= 393728 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+      }
+      Feature::PseudoClassPlaceholderShown => {
+        if let Some(version) = browsers.firefox {
+          if version >= 262144 && version <= 3276800 {
+            prefixes |= VendorPrefix::Moz;
+          }
+        }
+        if let Some(version) = browsers.ie {
+          if version >= 655360 {
+            prefixes |= VendorPrefix::Ms;
+          }
+        }
+      }
+      Feature::Hyphens => {
+        if let Some(version) = browsers.edge {
+          if version >= 786432 && version <= 1179648 {
+            prefixes |= VendorPrefix::Ms;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version >= 393216 && version <= 2752512 {
+            prefixes |= VendorPrefix::Moz;
+          }
+        }
+        if let Some(version) = browsers.ie {
+          if version >= 655360 {
+            prefixes |= VendorPrefix::Ms;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version >= 262656 && version <= 1050112 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version >= 327936 && version <= 1050112 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+      }
+      Feature::PseudoClassFullscreen => {
+        if let Some(version) = browsers.chrome {
+          if version >= 983040 && version <= 4587520 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version >= 655360 && version <= 4128768 {
+            prefixes |= VendorPrefix::Moz;
+          }
+        }
+        if let Some(version) = browsers.ie {
+          if version >= 720896 {
+            prefixes |= VendorPrefix::Ms;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version >= 983040 && version <= 4128768 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version >= 327936 && version <= 1049344 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version >= 262144 && version <= 590336 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+      }
+      Feature::PseudoElementBackdrop => {
+        if let Some(version) = browsers.android {
+          if version >= 263168 && version <= 263171 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.chrome {
+          if version >= 2097152 && version <= 2359296 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version >= 786432 && version <= 1179648 {
+            prefixes |= VendorPrefix::Ms;
+          }
+        }
+        if browsers.ie.is_some() {
+          prefixes |= VendorPrefix::Ms;
+        }
+        if let Some(version) = browsers.opera {
+          if version >= 1245184 && version <= 1507328 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+      }
+      Feature::PseudoElementFileSelectorButton => {
+        if let Some(version) = browsers.android {
+          if version >= 263168 && version <= 263171 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.chrome {
+          if version >= 262144 && version <= 5767168 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version >= 786432 && version <= 1179648 {
+            prefixes |= VendorPrefix::Ms;
+          }
+          if version >= 5177344 && version <= 5767168 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.ie {
+          if version >= 655360 {
+            prefixes |= VendorPrefix::Ms;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version >= 197120 && version <= 917504 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version >= 983040 && version <= 4849664 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version >= 196864 && version <= 917504 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version >= 262144 && version <= 917504 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+      }
+      Feature::PseudoClassAutofill => {
+        if let Some(version) = browsers.android {
+          if version >= 263168 && version <= 263171 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.chrome {
+          if version >= 262144 && version <= 7143424 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version >= 5177344 && version <= 7143424 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version >= 197120 && version <= 918784 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version >= 983040 && version <= 6225920 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version >= 196864 && version <= 917760 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version >= 262144 && version <= 1310720 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+      }
+      Feature::TabSize => {
+        if let Some(version) = browsers.firefox {
+          if version >= 262144 && version <= 5898240 {
+            prefixes |= VendorPrefix::Moz;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version >= 656896 && version <= 786688 {
+            prefixes |= VendorPrefix::O;
+          }
+        }
+      }
+      Feature::MaxContent | Feature::MinContent => {
+        if let Some(version) = browsers.android {
+          if version >= 263168 && version <= 263171 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.chrome {
+          if version >= 1441792 && version <= 2949120 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version >= 196608 && version <= 4259840 {
+            prefixes |= VendorPrefix::Moz;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version >= 458752 && version <= 852992 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version >= 983040 && version <= 2097152 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version >= 393472 && version <= 655616 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version <= 262144 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+      }
+      Feature::Fill | Feature::FillAvailable => {
+        if let Some(version) = browsers.chrome {
+          if version >= 1441792 && version <= 8912896 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version >= 263168 && version <= 8716288 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version >= 5177344 && version <= 8716288 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version >= 196608 && version <= 4259840 {
+            prefixes |= VendorPrefix::Moz;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version >= 458752 && version <= 852992 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version >= 983040 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version >= 393472 && version <= 655616 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version >= 262144 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+      }
+      Feature::FitContent => {
+        if let Some(version) = browsers.android {
+          if version >= 263168 && version <= 263171 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.chrome {
+          if version >= 1441792 && version <= 2949120 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version >= 196608 && version <= 6094848 {
+            prefixes |= VendorPrefix::Moz;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version >= 458752 && version <= 852992 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version >= 983040 && version <= 2097152 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version >= 393472 && version <= 655616 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version <= 262144 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+      }
+      Feature::Stretch => {
+        if let Some(version) = browsers.chrome {
+          if version >= 1441792 && version <= 8912896 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version >= 196608 && version <= 9043968 {
+            prefixes |= VendorPrefix::Moz;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version >= 263168 && version <= 8716288 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version >= 5177344 && version <= 8716288 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version >= 458752 && version <= 1180672 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version >= 983040 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version >= 458752 && version <= 1180672 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version >= 327680 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+      }
+      Feature::ZoomIn | Feature::ZoomOut => {
+        if let Some(version) = browsers.chrome {
+          if version >= 262144 && version <= 2359296 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version >= 131072 && version <= 1507328 {
+            prefixes |= VendorPrefix::Moz;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version >= 983040 && version <= 1507328 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version >= 196864 && version <= 524288 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+      }
+      Feature::Grab | Feature::Grabbing => {
+        if let Some(version) = browsers.chrome {
+          if version >= 262144 && version <= 4390912 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version >= 131072 && version <= 1703936 {
+            prefixes |= VendorPrefix::Moz;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version >= 983040 && version <= 3538944 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version >= 196864 && version <= 655616 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+      }
+      Feature::Sticky => {
+        if let Some(version) = browsers.ios_saf {
+          if version >= 393216 && version <= 786944 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version >= 393472 && version <= 786688 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+      }
+      Feature::TouchAction => {
+        if let Some(version) = browsers.ie {
+          if version == 655360 {
+            prefixes |= VendorPrefix::Ms;
+          }
+        }
+      }
+      Feature::TextDecorationSkip | Feature::TextDecorationSkipInk => {
+        if let Some(version) = browsers.ios_saf {
+          if version >= 524288 && version <= 1180672 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version >= 459008 && version <= 786432 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+      }
+      Feature::TextDecoration => {
+        if let Some(version) = browsers.ios_saf {
+          if version >= 524288 && version <= 1180672 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version >= 524288 && version <= 1180672 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+      }
+      Feature::TextDecorationColor | Feature::TextDecorationLine | Feature::TextDecorationStyle => {
+        if let Some(version) = browsers.firefox {
+          if version >= 393216 && version <= 2293760 {
+            prefixes |= VendorPrefix::Moz;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version >= 524288 && version <= 786432 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version >= 524288 && version <= 786432 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+      }
+      Feature::TextSizeAdjust => {
+        if let Some(version) = browsers.firefox {
+          if version <= 8847360 {
+            prefixes |= VendorPrefix::Moz;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version >= 786432 && version <= 1179648 {
+            prefixes |= VendorPrefix::Ms;
+          }
+        }
+        if let Some(version) = browsers.ie {
+          if version >= 655360 {
+            prefixes |= VendorPrefix::Ms;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version >= 327680 && version <= 1180672 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+      }
+      Feature::MaskClip
+      | Feature::MaskComposite
+      | Feature::MaskImage
+      | Feature::MaskOrigin
+      | Feature::MaskRepeat
+      | Feature::MaskBorderRepeat
+      | Feature::MaskBorderSource
+      | Feature::Mask
+      | Feature::MaskPosition
+      | Feature::MaskSize
+      | Feature::MaskBorder
+      | Feature::MaskBorderOutset
+      | Feature::MaskBorderWidth
+      | Feature::MaskBorderSlice => {
+        if let Some(version) = browsers.android {
+          if version >= 131328 && version <= 263171 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.chrome {
+          if version >= 262144 && version <= 7798784 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version >= 5177344 && version <= 7798784 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version >= 197120 && version <= 983552 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version >= 983040 && version <= 6881280 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version >= 262144 && version <= 983552 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version >= 262144 && version <= 1572864 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+      }
+      Feature::ClipPath => {
+        if let Some(version) = browsers.android {
+          if version >= 263168 && version <= 263171 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.chrome {
+          if version >= 1572864 && version <= 3538944 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version >= 458752 && version <= 589824 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version >= 983040 && version <= 2686976 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version >= 458752 && version <= 589824 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version >= 262144 && version <= 327680 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+      }
+      Feature::BoxDecorationBreak => {
+        if let Some(version) = browsers.android {
+          if version >= 263168 && version <= 263171 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.chrome {
+          if version >= 1441792 && version <= 8454144 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version >= 5177344 && version <= 8454144 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version >= 458752 && version <= 1180672 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version >= 983040 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version >= 393472 && version <= 1180672 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version >= 262144 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+      }
+      Feature::ObjectFit | Feature::ObjectPosition => {
+        if let Some(version) = browsers.opera {
+          if version >= 656896 && version <= 786688 {
+            prefixes |= VendorPrefix::O;
+          }
+        }
+      }
+      Feature::ShapeMargin | Feature::ShapeOutside | Feature::ShapeImageThreshold => {
+        if let Some(version) = browsers.ios_saf {
+          if version >= 524288 && version <= 655360 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version >= 459008 && version <= 655360 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+      }
+      Feature::TextOverflow => {
+        if let Some(version) = browsers.opera {
+          if version >= 589824 && version <= 786432 {
+            prefixes |= VendorPrefix::O;
+          }
+        }
+      }
+      Feature::AtViewport => {
+        if let Some(version) = browsers.edge {
+          if version >= 786432 && version <= 1179648 {
+            prefixes |= VendorPrefix::Ms;
+          }
+        }
+        if let Some(version) = browsers.ie {
+          if version >= 655360 {
+            prefixes |= VendorPrefix::Ms;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version >= 720896 && version <= 786688 {
+            prefixes |= VendorPrefix::O;
+          }
+        }
+      }
+      Feature::AtResolution => {
+        if let Some(version) = browsers.android {
+          if version >= 131840 && version <= 262656 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.chrome {
+          if version >= 262144 && version <= 1835008 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version >= 197888 && version <= 983040 {
+            prefixes |= VendorPrefix::Moz;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version >= 262144 && version <= 984576 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version >= 591104 && version <= 786432 {
+            prefixes |= VendorPrefix::O;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version >= 262144 && version <= 984576 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+      }
+      Feature::TextAlignLast => {
+        if let Some(version) = browsers.firefox {
+          if version >= 786432 && version <= 3145728 {
+            prefixes |= VendorPrefix::Moz;
+          }
+        }
+      }
+      Feature::Pixelated => {
+        if let Some(version) = browsers.firefox {
+          if version >= 198144 && version <= 4194304 {
+            prefixes |= VendorPrefix::Moz;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version >= 327680 && version <= 393216 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version >= 722432 && version <= 786688 {
+            prefixes |= VendorPrefix::O;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version <= 393216 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+      }
+      Feature::ImageRendering => {
+        if let Some(version) = browsers.ie {
+          if version >= 458752 {
+            prefixes |= VendorPrefix::Ms;
+          }
+        }
+      }
+      Feature::BorderInlineStart
+      | Feature::BorderInlineEnd
+      | Feature::MarginInlineStart
+      | Feature::MarginInlineEnd
+      | Feature::PaddingInlineStart
+      | Feature::PaddingInlineEnd => {
+        if let Some(version) = browsers.android {
+          if version >= 131328 && version <= 263171 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.chrome {
+          if version >= 262144 && version <= 4456448 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version >= 196608 && version <= 2621440 {
+            prefixes |= VendorPrefix::Moz;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version >= 197120 && version <= 786432 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version >= 983040 && version <= 3604480 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version >= 196864 && version <= 786432 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version >= 262144 && version <= 590336 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+      }
+      Feature::BorderBlockStart
+      | Feature::BorderBlockEnd
+      | Feature::MarginBlockStart
+      | Feature::MarginBlockEnd
+      | Feature::PaddingBlockStart
+      | Feature::PaddingBlockEnd => {
+        if let Some(version) = browsers.android {
+          if version >= 131328 && version <= 263171 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.chrome {
+          if version >= 262144 && version <= 4456448 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version >= 197120 && version <= 786432 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version >= 983040 && version <= 3604480 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version >= 196864 && version <= 786432 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version >= 262144 && version <= 590336 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+      }
+      Feature::Appearance => {
+        if let Some(version) = browsers.android {
+          if version >= 131328 && version <= 263171 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.chrome {
+          if version >= 262144 && version <= 5439488 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version >= 786432 && version <= 1179648 {
+            prefixes |= VendorPrefix::Ms;
+          }
+          if version >= 5177344 && version <= 5439488 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version >= 131072 && version <= 5177344 {
+            prefixes |= VendorPrefix::Moz;
+          }
+        }
+        if browsers.ie.is_some() {
+          prefixes |= VendorPrefix::Ms;
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version >= 197120 && version <= 983552 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version >= 983040 && version <= 4718592 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version >= 196864 && version <= 983552 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version >= 262144 && version <= 851968 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+      }
+      Feature::ScrollSnapType
+      | Feature::ScrollSnapCoordinate
+      | Feature::ScrollSnapDestination
+      | Feature::ScrollSnapPointsX
+      | Feature::ScrollSnapPointsY => {
+        if let Some(version) = browsers.edge {
+          if version >= 786432 && version <= 1179648 {
+            prefixes |= VendorPrefix::Ms;
+          }
+        }
+        if let Some(version) = browsers.ie {
+          if version >= 655360 {
+            prefixes |= VendorPrefix::Ms;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version >= 589824 && version <= 656128 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version >= 589824 && version <= 655616 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+      }
+      Feature::FlowInto | Feature::FlowFrom | Feature::RegionFragment => {
+        if let Some(version) = browsers.chrome {
+          if version >= 983040 && version <= 1179648 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version >= 786432 && version <= 1179648 {
+            prefixes |= VendorPrefix::Ms;
+          }
+        }
+        if let Some(version) = browsers.ie {
+          if version >= 655360 {
+            prefixes |= VendorPrefix::Ms;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version >= 458752 && version <= 720896 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version >= 393472 && version <= 720896 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+      }
+      Feature::ImageSet => {
+        if let Some(version) = browsers.android {
+          if version >= 263168 && version <= 263171 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.chrome {
+          if version >= 1376256 && version <= 7340032 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version >= 5177344 && version <= 7340032 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version >= 393216 && version <= 590592 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version >= 983040 && version <= 6422528 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version >= 393216 && version <= 590080 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version >= 262144 && version <= 1441792 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+      }
+      Feature::WritingMode => {
+        if let Some(version) = browsers.android {
+          if version >= 196608 && version <= 263171 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.chrome {
+          if version >= 524288 && version <= 3080192 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.ie {
+          if version >= 328960 {
+            prefixes |= VendorPrefix::Ms;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version >= 327680 && version <= 656128 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version >= 983040 && version <= 2228224 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version >= 327936 && version <= 655616 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version <= 262144 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+      }
+      Feature::CrossFade => {
+        if let Some(version) = browsers.chrome {
+          if version >= 1114112 && version <= 8912896 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version >= 263168 && version <= 8716288 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version >= 5177344 && version <= 8716288 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version >= 327680 && version <= 590592 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version >= 983040 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version >= 327936 && version <= 590080 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version >= 262144 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+      }
+      Feature::PseudoClassReadOnly | Feature::PseudoClassReadWrite => {
+        if let Some(version) = browsers.firefox {
+          if version >= 196608 && version <= 5046272 {
+            prefixes |= VendorPrefix::Moz;
+          }
+        }
+      }
+      Feature::TextEmphasis
+      | Feature::TextEmphasisPosition
+      | Feature::TextEmphasisStyle
+      | Feature::TextEmphasisColor => {
+        if let Some(version) = browsers.android {
+          if version >= 263168 && version <= 263171 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.chrome {
+          if version >= 1638400 && version <= 6422528 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version >= 5177344 && version <= 6422528 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version >= 983040 && version <= 5570560 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version >= 393472 && version <= 458752 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version >= 262144 && version <= 1114112 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+      }
+      Feature::DisplayGrid
+      | Feature::InlineGrid
+      | Feature::GridTemplateColumns
+      | Feature::GridTemplateRows
+      | Feature::GridRowStart
+      | Feature::GridColumnStart
+      | Feature::GridRowEnd
+      | Feature::GridColumnEnd
+      | Feature::GridRow
+      | Feature::GridColumn
+      | Feature::GridArea
+      | Feature::GridTemplate
+      | Feature::GridTemplateAreas
+      | Feature::PlaceSelf
+      | Feature::GridColumnAlign
+      | Feature::GridRowAlign => {
+        if let Some(version) = browsers.edge {
+          if version >= 786432 && version <= 983040 {
+            prefixes |= VendorPrefix::Ms;
+          }
+        }
+        if let Some(version) = browsers.ie {
+          if version >= 655360 {
+            prefixes |= VendorPrefix::Ms;
+          }
+        }
+      }
+      Feature::TextSpacing => {
+        if let Some(version) = browsers.edge {
+          if version >= 786432 && version <= 1179648 {
+            prefixes |= VendorPrefix::Ms;
+          }
+        }
+        if let Some(version) = browsers.ie {
+          if version >= 524288 {
+            prefixes |= VendorPrefix::Ms;
+          }
+        }
+      }
+      Feature::PseudoClassAnyLink => {
+        if let Some(version) = browsers.android {
+          if version >= 263168 && version <= 263171 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.chrome {
+          if version >= 983040 && version <= 4194304 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version >= 196608 && version <= 3211264 {
+            prefixes |= VendorPrefix::Moz;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version >= 393216 && version <= 524544 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version >= 983040 && version <= 3342336 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version >= 393472 && version <= 524288 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version >= 327680 && version <= 524800 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+      }
+      Feature::Isolate => {
+        if let Some(version) = browsers.chrome {
+          if version >= 1048576 && version <= 3080192 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version >= 655360 && version <= 3211264 {
+            prefixes |= VendorPrefix::Moz;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version >= 393216 && version <= 656128 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version >= 983040 && version <= 2228224 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version >= 393216 && version <= 655616 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+      }
+      Feature::Plaintext => {
+        if let Some(version) = browsers.firefox {
+          if version >= 655360 && version <= 3211264 {
+            prefixes |= VendorPrefix::Moz;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version >= 393216 && version <= 656128 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version >= 393216 && version <= 655616 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+      }
+      Feature::IsolateOverride => {
+        if let Some(version) = browsers.firefox {
+          if version >= 1114112 && version <= 3211264 {
+            prefixes |= VendorPrefix::Moz;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version >= 458752 && version <= 656128 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version >= 458752 && version <= 655616 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+      }
+      Feature::OverscrollBehavior => {
+        if let Some(version) = browsers.edge {
+          if version >= 786432 && version <= 1114112 {
+            prefixes |= VendorPrefix::Ms;
+          }
+        }
+        if let Some(version) = browsers.ie {
+          if version >= 655360 {
+            prefixes |= VendorPrefix::Ms;
+          }
+        }
+      }
+      Feature::TextOrientation => {
+        if let Some(version) = browsers.safari {
+          if version >= 655616 && version <= 852224 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+      }
+      Feature::PrintColorAdjust | Feature::ColorAdjust => {
+        if let Some(version) = browsers.chrome {
+          if version >= 1114112 && version <= 8912896 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version >= 263168 && version <= 8716288 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version >= 5177344 && version <= 8716288 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version >= 3145728 && version <= 6291456 {
+            prefixes |= VendorPrefix::Moz;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version >= 393216 && version <= 983552 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version >= 983040 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version >= 393216 && version <= 983552 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version >= 262144 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+      }
+      Feature::AnyPseudo => {
+        if let Some(version) = browsers.chrome {
+          if version >= 786432 && version <= 5701632 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.edge {
+          if version >= 5177344 && version <= 5701632 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.firefox {
+          if version >= 262144 && version <= 5111808 {
+            prefixes |= VendorPrefix::Moz;
+          }
+        }
+        if let Some(version) = browsers.opera {
+          if version >= 917504 && version <= 4784128 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.safari {
+          if version >= 327680 && version <= 851968 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.ios_saf {
+          if version >= 327680 && version <= 851968 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.samsung {
+          if version >= 65536 && version <= 917504 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+        if let Some(version) = browsers.android {
+          if version >= 263168 && version <= 5701632 {
+            prefixes |= VendorPrefix::WebKit;
+          }
+        }
+      }
+    }
+    prefixes
+  }
+}
+
+pub fn is_flex_2009(browsers: Browsers) -> bool {
+  if let Some(version) = browsers.android {
+    if version >= 131328 && version <= 262656 {
+      return true;
+    }
+  }
+  if let Some(version) = browsers.chrome {
+    if version >= 262144 && version <= 1310720 {
+      return true;
+    }
+  }
+  if let Some(version) = browsers.ios_saf {
+    if version >= 197120 && version <= 393216 {
+      return true;
+    }
+  }
+  if let Some(version) = browsers.safari {
+    if version >= 196864 && version <= 393216 {
+      return true;
+    }
+  }
+  false
+}
+
+pub fn is_webkit_gradient(browsers: Browsers) -> bool {
+  if let Some(version) = browsers.android {
+    if version >= 131328 && version <= 196608 {
+      return true;
+    }
+  }
+  if let Some(version) = browsers.chrome {
+    if version >= 262144 && version <= 589824 {
+      return true;
+    }
+  }
+  if let Some(version) = browsers.ios_saf {
+    if version >= 197120 && version <= 393216 {
+      return true;
+    }
+  }
+  if let Some(version) = browsers.safari {
+    if version >= 262144 && version <= 393216 {
+      return true;
+    }
+  }
+  false
+}
diff --git a/src/printer.rs b/src/printer.rs
new file mode 100644
index 0000000..b231fbe
--- /dev/null
+++ b/src/printer.rs
@@ -0,0 +1,421 @@
+//! CSS serialization and source map generation.
+
+use crate::css_modules::CssModule;
+use crate::dependencies::{Dependency, DependencyOptions};
+use crate::error::{Error, ErrorLocation, PrinterError, PrinterErrorKind};
+use crate::rules::{Location, StyleContext};
+use crate::selector::SelectorList;
+use crate::targets::{Targets, TargetsWithSupportsScope};
+use crate::vendor_prefix::VendorPrefix;
+use cssparser::{serialize_identifier, serialize_name};
+#[cfg(feature = "sourcemap")]
+use parcel_sourcemap::{OriginalLocation, SourceMap};
+
+/// Options that control how CSS is serialized to a string.
+#[derive(Default)]
+pub struct PrinterOptions<'a> {
+  /// Whether to minify the CSS, i.e. remove white space.
+  pub minify: bool,
+  /// An optional reference to a source map to write mappings into.
+  #[cfg(feature = "sourcemap")]
+  #[cfg_attr(docsrs, doc(cfg(feature = "sourcemap")))]
+  pub source_map: Option<&'a mut SourceMap>,
+  /// An optional project root path, used to generate relative paths for sources used in CSS module hashes.
+  pub project_root: Option<&'a str>,
+  /// Targets to output the CSS for.
+  pub targets: Targets,
+  /// Whether to analyze dependencies (i.e. `@import` and `url()`).
+  /// If true, the dependencies are returned as part of the
+  /// [ToCssResult](super::stylesheet::ToCssResult).
+  ///
+  /// When enabled, `@import` and `url()` dependencies
+  /// are replaced with hashed placeholders that can be replaced with the final
+  /// urls later (after bundling).
+  pub analyze_dependencies: Option<DependencyOptions>,
+  /// A mapping of pseudo classes to replace with class names that can be applied
+  /// from JavaScript. Useful for polyfills, for example.
+  pub pseudo_classes: Option<PseudoClasses<'a>>,
+}
+
+/// A mapping of user action pseudo classes to replace with class names.
+///
+/// See [PrinterOptions](PrinterOptions).
+#[derive(Default, Debug)]
+pub struct PseudoClasses<'a> {
+  /// The class name to replace `:hover` with.
+  pub hover: Option<&'a str>,
+  /// The class name to replace `:active` with.
+  pub active: Option<&'a str>,
+  /// The class name to replace `:focus` with.
+  pub focus: Option<&'a str>,
+  /// The class name to replace `:focus-visible` with.
+  pub focus_visible: Option<&'a str>,
+  /// The class name to replace `:focus-within` with.
+  pub focus_within: Option<&'a str>,
+}
+
+/// A `Printer` represents a destination to output serialized CSS, as used in
+/// the [ToCss](super::traits::ToCss) trait. It can wrap any destination that
+/// implements [std::fmt::Write](std::fmt::Write), such as a [String](String).
+///
+/// A `Printer` keeps track of the current line and column position, and uses
+/// this to generate a source map if provided in the options.
+///
+/// `Printer` also includes helper functions that assist with writing output
+/// that respects options such as `minify`, and `css_modules`.
+pub struct Printer<'a, 'b, 'c, W> {
+  pub(crate) sources: Option<&'c Vec<String>>,
+  dest: &'a mut W,
+  #[cfg(feature = "sourcemap")]
+  #[cfg_attr(docsrs, doc(cfg(feature = "sourcemap")))]
+  pub(crate) source_map: Option<&'a mut SourceMap>,
+  #[cfg(feature = "sourcemap")]
+  #[cfg_attr(docsrs, doc(cfg(feature = "sourcemap")))]
+  pub(crate) source_maps: Vec<Option<SourceMap>>,
+  pub(crate) loc: Location,
+  indent: u8,
+  line: u32,
+  col: u32,
+  pub(crate) minify: bool,
+  pub(crate) targets: TargetsWithSupportsScope,
+  /// Vendor prefix override. When non-empty, it overrides
+  /// the vendor prefix of whatever is being printed.
+  pub(crate) vendor_prefix: VendorPrefix,
+  pub(crate) in_calc: bool,
+  pub(crate) css_module: Option<CssModule<'a, 'b, 'c>>,
+  pub(crate) dependencies: Option<Vec<Dependency>>,
+  pub(crate) remove_imports: bool,
+  pub(crate) pseudo_classes: Option<PseudoClasses<'a>>,
+  context: Option<&'a StyleContext<'a, 'b>>,
+}
+
+impl<'a, 'b, 'c, W: std::fmt::Write + Sized> Printer<'a, 'b, 'c, W> {
+  /// Create a new Printer wrapping the given destination.
+  pub fn new(dest: &'a mut W, options: PrinterOptions<'a>) -> Self {
+    Printer {
+      sources: None,
+      dest,
+      #[cfg(feature = "sourcemap")]
+      source_map: options.source_map,
+      #[cfg(feature = "sourcemap")]
+      source_maps: Vec::new(),
+      loc: Location {
+        source_index: 0,
+        line: 0,
+        column: 1,
+      },
+      indent: 0,
+      line: 0,
+      col: 0,
+      minify: options.minify,
+      targets: TargetsWithSupportsScope::new(options.targets),
+      vendor_prefix: VendorPrefix::empty(),
+      in_calc: false,
+      css_module: None,
+      dependencies: if options.analyze_dependencies.is_some() {
+        Some(Vec::new())
+      } else {
+        None
+      },
+      remove_imports: matches!(&options.analyze_dependencies, Some(d) if d.remove_imports),
+      pseudo_classes: options.pseudo_classes,
+      context: None,
+    }
+  }
+
+  /// Returns the current source filename that is being printed.
+  pub fn filename(&self) -> &'c str {
+    if let Some(sources) = self.sources {
+      if let Some(f) = sources.get(self.loc.source_index as usize) {
+        f
+      } else {
+        "unknown.css"
+      }
+    } else {
+      "unknown.css"
+    }
+  }
+
+  /// Writes a raw string to the underlying destination.
+  ///
+  /// NOTE: Is is assumed that the string does not contain any newline characters.
+  /// If such a string is written, it will break source maps.
+  pub fn write_str(&mut self, s: &str) -> Result<(), PrinterError> {
+    self.col += s.len() as u32;
+    self.dest.write_str(s)?;
+    Ok(())
+  }
+
+  /// Writes a raw string which may contain newlines to the underlying destination.
+  pub fn write_str_with_newlines(&mut self, s: &str) -> Result<(), PrinterError> {
+    let mut last_line_start: usize = 0;
+
+    for (idx, n) in s.char_indices() {
+      if n == '\n' {
+        self.line += 1;
+        self.col = 0;
+
+        // Keep track of where the *next* line starts
+        last_line_start = idx + 1;
+      }
+    }
+
+    self.col += (s.len() - last_line_start) as u32;
+    self.dest.write_str(s)?;
+    Ok(())
+  }
+
+  /// Write a single character to the underlying destination.
+  pub fn write_char(&mut self, c: char) -> Result<(), PrinterError> {
+    if c == '\n' {
+      self.line += 1;
+      self.col = 0;
+    } else {
+      self.col += 1;
+    }
+    self.dest.write_char(c)?;
+    Ok(())
+  }
+
+  /// Writes a single whitespace character, unless the `minify` option is enabled.
+  ///
+  /// Use `write_char` instead if you wish to force a space character to be written,
+  /// regardless of the `minify` option.
+  pub fn whitespace(&mut self) -> Result<(), PrinterError> {
+    if self.minify {
+      return Ok(());
+    }
+
+    self.write_char(' ')
+  }
+
+  /// Writes a delimiter character, followed by whitespace (depending on the `minify` option).
+  /// If `ws_before` is true, then whitespace is also written before the delimiter.
+  pub fn delim(&mut self, delim: char, ws_before: bool) -> Result<(), PrinterError> {
+    if ws_before {
+      self.whitespace()?;
+    }
+    self.write_char(delim)?;
+    self.whitespace()
+  }
+
+  /// Writes a newline character followed by indentation.
+  /// If the `minify` option is enabled, then nothing is printed.
+  pub fn newline(&mut self) -> Result<(), PrinterError> {
+    if self.minify {
+      return Ok(());
+    }
+
+    self.write_char('\n')?;
+    if self.indent > 0 {
+      self.write_str(&" ".repeat(self.indent as usize))?;
+    }
+
+    Ok(())
+  }
+
+  /// Increases the current indent level.
+  pub fn indent(&mut self) {
+    self.indent += 2;
+  }
+
+  /// Decreases the current indent level.
+  pub fn dedent(&mut self) {
+    self.indent -= 2;
+  }
+
+  /// Increases the current indent level by the given number of characters.
+  pub fn indent_by(&mut self, amt: u8) {
+    self.indent += amt;
+  }
+
+  /// Decreases the current indent level by the given number of characters.
+  pub fn dedent_by(&mut self, amt: u8) {
+    self.indent -= amt;
+  }
+
+  /// Returns whether the indent level is greater than one.
+  pub fn is_nested(&self) -> bool {
+    self.indent > 2
+  }
+
+  /// Adds a mapping to the source map, if any.
+  #[cfg(feature = "sourcemap")]
+  #[cfg_attr(docsrs, doc(cfg(feature = "sourcemap")))]
+  pub fn add_mapping(&mut self, loc: Location) {
+    self.loc = loc;
+
+    if let Some(map) = &mut self.source_map {
+      let mut original = OriginalLocation {
+        original_line: loc.line,
+        original_column: loc.column - 1,
+        source: loc.source_index,
+        name: None,
+      };
+
+      // Remap using input source map if possible.
+      if let Some(Some(sm)) = self.source_maps.get_mut(loc.source_index as usize) {
+        let mut found_mapping = false;
+        if let Some(mapping) = sm.find_closest_mapping(loc.line, loc.column - 1) {
+          if let Some(orig) = mapping.original {
+            let sources_len = map.get_sources().len();
+            let source_index = map.add_source(sm.get_source(orig.source).unwrap());
+            let name = orig.name.map(|name| map.add_name(sm.get_name(name).unwrap()));
+            original.original_line = orig.original_line;
+            original.original_column = orig.original_column;
+            original.source = source_index;
+            original.name = name;
+
+            if map.get_sources().len() > sources_len {
+              let content = sm.get_source_content(orig.source).unwrap().to_owned();
+              let _ = map.set_source_content(source_index as usize, &content);
+            }
+
+            found_mapping = true;
+          }
+        }
+
+        if !found_mapping {
+          return;
+        }
+      }
+
+      map.add_mapping(self.line, self.col, Some(original))
+    }
+  }
+
+  /// Writes a CSS identifier to the underlying destination, escaping it
+  /// as appropriate. If the `css_modules` option was enabled, then a hash
+  /// is added, and the mapping is added to the CSS module.
+  pub fn write_ident(&mut self, ident: &str, handle_css_module: bool) -> Result<(), PrinterError> {
+    if handle_css_module {
+      if let Some(css_module) = &mut self.css_module {
+        let dest = &mut self.dest;
+        let mut first = true;
+        css_module.config.pattern.write(
+          &css_module.hashes[self.loc.source_index as usize],
+          &css_module.sources[self.loc.source_index as usize],
+          ident,
+          if let Some(content_hashes) = &css_module.content_hashes {
+            &content_hashes[self.loc.source_index as usize]
+          } else {
+            ""
+          },
+          |s| {
+            self.col += s.len() as u32;
+            if first {
+              first = false;
+              serialize_identifier(s, dest)
+            } else {
+              serialize_name(s, dest)
+            }
+          },
+        )?;
+
+        css_module.add_local(&ident, &ident, self.loc.source_index);
+        return Ok(());
+      }
+    }
+
+    serialize_identifier(ident, self)?;
+    Ok(())
+  }
+
+  pub(crate) fn write_dashed_ident(&mut self, ident: &str, is_declaration: bool) -> Result<(), PrinterError> {
+    self.write_str("--")?;
+
+    match &mut self.css_module {
+      Some(css_module) if css_module.config.dashed_idents => {
+        let dest = &mut self.dest;
+        css_module.config.pattern.write(
+          &css_module.hashes[self.loc.source_index as usize],
+          &css_module.sources[self.loc.source_index as usize],
+          &ident[2..],
+          if let Some(content_hashes) = &css_module.content_hashes {
+            &content_hashes[self.loc.source_index as usize]
+          } else {
+            ""
+          },
+          |s| {
+            self.col += s.len() as u32;
+            serialize_name(s, dest)
+          },
+        )?;
+
+        if is_declaration {
+          css_module.add_dashed(ident, self.loc.source_index);
+        }
+      }
+      _ => {
+        serialize_name(&ident[2..], self)?;
+      }
+    }
+
+    Ok(())
+  }
+
+  /// Returns an error of the given kind at the provided location in the current source file.
+  pub fn error(&self, kind: PrinterErrorKind, loc: crate::dependencies::Location) -> Error<PrinterErrorKind> {
+    Error {
+      kind,
+      loc: Some(ErrorLocation {
+        filename: self.filename().into(),
+        line: loc.line - 1,
+        column: loc.column,
+      }),
+    }
+  }
+
+  pub(crate) fn with_context<T, U, F: FnOnce(&mut Printer<'a, 'b, 'c, W>) -> Result<T, U>>(
+    &mut self,
+    selectors: &SelectorList,
+    f: F,
+  ) -> Result<T, U> {
+    let parent = std::mem::take(&mut self.context);
+    let ctx = StyleContext {
+      selectors: unsafe { std::mem::transmute(selectors) },
+      parent,
+    };
+
+    // I can't figure out what lifetime to use here to convince the compiler that
+    // the reference doesn't live beyond the function call.
+    self.context = Some(unsafe { std::mem::transmute(&ctx) });
+    let res = f(self);
+    self.context = parent;
+    res
+  }
+
+  pub(crate) fn with_cleared_context<T, U, F: FnOnce(&mut Printer<'a, 'b, 'c, W>) -> Result<T, U>>(
+    &mut self,
+    f: F,
+  ) -> Result<T, U> {
+    let parent = std::mem::take(&mut self.context);
+    let res = f(self);
+    self.context = parent;
+    res
+  }
+
+  pub(crate) fn with_parent_context<T, U, F: FnOnce(&mut Printer<'a, 'b, 'c, W>) -> Result<T, U>>(
+    &mut self,
+    f: F,
+  ) -> Result<T, U> {
+    let parent = std::mem::take(&mut self.context);
+    if let Some(parent) = parent {
+      self.context = parent.parent;
+    }
+    let res = f(self);
+    self.context = parent;
+    res
+  }
+
+  pub(crate) fn context(&self) -> Option<&'a StyleContext<'a, 'b>> {
+    self.context.clone()
+  }
+}
+
+impl<'a, 'b, 'c, W: std::fmt::Write + Sized> std::fmt::Write for Printer<'a, 'b, 'c, W> {
+  fn write_str(&mut self, s: &str) -> std::fmt::Result {
+    self.col += s.len() as u32;
+    self.dest.write_str(s)
+  }
+}
diff --git a/src/properties/align.rs b/src/properties/align.rs
new file mode 100644
index 0000000..819e9ac
--- /dev/null
+++ b/src/properties/align.rs
@@ -0,0 +1,1207 @@
+//! CSS properties related to box alignment.
+
+use super::flex::{BoxAlign, BoxPack, FlexAlign, FlexItemAlign, FlexLinePack, FlexPack};
+use super::{Property, PropertyId};
+use crate::compat;
+use crate::context::PropertyHandlerContext;
+use crate::declaration::{DeclarationBlock, DeclarationList};
+use crate::error::{ParserError, PrinterError};
+use crate::macros::*;
+use crate::prefixes::{is_flex_2009, Feature};
+use crate::printer::Printer;
+use crate::traits::{FromStandard, Parse, PropertyHandler, Shorthand, ToCss};
+use crate::values::length::LengthPercentage;
+use crate::vendor_prefix::VendorPrefix;
+#[cfg(feature = "visitor")]
+use crate::visitor::Visit;
+use cssparser::*;
+
+#[cfg(feature = "serde")]
+use crate::serialization::ValueWrapper;
+
+/// A [`<baseline-position>`](https://www.w3.org/TR/css-align-3/#typedef-baseline-position) value,
+/// as used in the alignment properties.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub enum BaselinePosition {
+  /// The first baseline.
+  First,
+  /// The last baseline.
+  Last,
+}
+
+impl<'i> Parse<'i> for BaselinePosition {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let location = input.current_source_location();
+    let ident = input.expect_ident()?;
+    match_ignore_ascii_case! { &*ident,
+      "baseline" => Ok(BaselinePosition::First),
+      "first" => {
+        input.expect_ident_matching("baseline")?;
+        Ok(BaselinePosition::First)
+      },
+      "last" => {
+        input.expect_ident_matching("baseline")?;
+        Ok(BaselinePosition::Last)
+      },
+      _ => Err(location.new_unexpected_token_error(
+        cssparser::Token::Ident(ident.clone())
+      ))
+    }
+  }
+}
+
+impl ToCss for BaselinePosition {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    match self {
+      BaselinePosition::First => dest.write_str("baseline"),
+      BaselinePosition::Last => dest.write_str("last baseline"),
+    }
+  }
+}
+
+enum_property! {
+  /// A [`<content-distribution>`](https://www.w3.org/TR/css-align-3/#typedef-content-distribution) value.
+  pub enum ContentDistribution {
+    /// Items are spaced evenly, with the first and last items against the edge of the container.
+    SpaceBetween,
+    /// Items are spaced evenly, with half-size spaces at the start and end.
+    SpaceAround,
+    /// Items are spaced evenly, with full-size spaces at the start and end.
+    SpaceEvenly,
+    /// Items are stretched evenly to fill free space.
+    Stretch,
+  }
+}
+
+enum_property! {
+  /// An [`<overflow-position>`](https://www.w3.org/TR/css-align-3/#typedef-overflow-position) value.
+  pub enum OverflowPosition {
+    /// If the size of the alignment subject overflows the alignment container,
+    /// the alignment subject is instead aligned as if the alignment mode were start.
+    Safe,
+    /// Regardless of the relative sizes of the alignment subject and alignment
+    /// container, the given alignment value is honored.
+    Unsafe,
+  }
+}
+
+enum_property! {
+  /// A [`<content-position>`](https://www.w3.org/TR/css-align-3/#typedef-content-position) value.
+  pub enum ContentPosition {
+    /// Content is centered within the container.
+    Center,
+    /// Content is aligned to the start of the container.
+    Start,
+    /// Content is aligned to the end of the container.
+    End,
+    /// Same as `start` when within a flexbox container.
+    FlexStart,
+    /// Same as `end` when within a flexbox container.
+    FlexEnd,
+  }
+}
+
+/// A value for the [align-content](https://www.w3.org/TR/css-align-3/#propdef-align-content) property.
+#[derive(Debug, Clone, PartialEq, Parse, ToCss)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum AlignContent {
+  /// Default alignment.
+  Normal,
+  /// A baseline position.
+  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<BaselinePosition>"))]
+  BaselinePosition(BaselinePosition),
+  /// A content distribution keyword.
+  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<ContentDistribution>"))]
+  ContentDistribution(ContentDistribution),
+  /// A content position keyword.
+  ContentPosition {
+    /// An overflow alignment mode.
+    overflow: Option<OverflowPosition>,
+    /// A content position keyword.
+    value: ContentPosition,
+  },
+}
+
+/// A value for the [justify-content](https://www.w3.org/TR/css-align-3/#propdef-justify-content) property.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum JustifyContent {
+  /// Default justification.
+  Normal,
+  /// A content distribution keyword.
+  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<ContentDistribution>"))]
+  ContentDistribution(ContentDistribution),
+  /// A content position keyword.
+  ContentPosition {
+    /// A content position keyword.
+    value: ContentPosition,
+    /// An overflow alignment mode.
+    overflow: Option<OverflowPosition>,
+  },
+  /// Justify to the left.
+  Left {
+    /// An overflow alignment mode.
+    overflow: Option<OverflowPosition>,
+  },
+  /// Justify to the right.
+  Right {
+    /// An overflow alignment mode.
+    overflow: Option<OverflowPosition>,
+  },
+}
+
+impl<'i> Parse<'i> for JustifyContent {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    if input.try_parse(|input| input.expect_ident_matching("normal")).is_ok() {
+      return Ok(JustifyContent::Normal);
+    }
+
+    if let Ok(val) = input.try_parse(ContentDistribution::parse) {
+      return Ok(JustifyContent::ContentDistribution(val));
+    }
+
+    let overflow = input.try_parse(OverflowPosition::parse).ok();
+    if let Ok(content_position) = input.try_parse(ContentPosition::parse) {
+      return Ok(JustifyContent::ContentPosition {
+        overflow,
+        value: content_position,
+      });
+    }
+
+    let location = input.current_source_location();
+    let ident = input.expect_ident()?;
+    match_ignore_ascii_case! { &*ident,
+      "left" => Ok(JustifyContent::Left { overflow }),
+      "right" => Ok(JustifyContent::Right { overflow }),
+      _ => Err(location.new_unexpected_token_error(
+        cssparser::Token::Ident(ident.clone())
+      ))
+    }
+  }
+}
+
+impl ToCss for JustifyContent {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    match self {
+      JustifyContent::Normal => dest.write_str("normal"),
+      JustifyContent::ContentDistribution(value) => value.to_css(dest),
+      JustifyContent::ContentPosition { overflow, value } => {
+        if let Some(overflow) = overflow {
+          overflow.to_css(dest)?;
+          dest.write_str(" ")?;
+        }
+
+        value.to_css(dest)
+      }
+      JustifyContent::Left { overflow } => {
+        if let Some(overflow) = overflow {
+          overflow.to_css(dest)?;
+          dest.write_str(" ")?;
+        }
+
+        dest.write_str("left")
+      }
+      JustifyContent::Right { overflow } => {
+        if let Some(overflow) = overflow {
+          overflow.to_css(dest)?;
+          dest.write_str(" ")?;
+        }
+
+        dest.write_str("right")
+      }
+    }
+  }
+}
+
+define_shorthand! {
+  /// A value for the [place-content](https://www.w3.org/TR/css-align-3/#place-content) shorthand property.
+  pub struct PlaceContent {
+    /// The content alignment.
+    align: AlignContent(AlignContent, VendorPrefix),
+    /// The content justification.
+    justify: JustifyContent(JustifyContent, VendorPrefix),
+  }
+}
+
+impl<'i> Parse<'i> for PlaceContent {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let align = AlignContent::parse(input)?;
+    let justify = match input.try_parse(JustifyContent::parse) {
+      Ok(j) => j,
+      Err(_) => {
+        // The second value is assigned to justify-content; if omitted, it is copied
+        // from the first value, unless that value is a <baseline-position> in which
+        // case it is defaulted to start.
+        match align {
+          AlignContent::BaselinePosition(_) => JustifyContent::ContentPosition {
+            overflow: None,
+            value: ContentPosition::Start,
+          },
+          AlignContent::Normal => JustifyContent::Normal,
+          AlignContent::ContentDistribution(value) => JustifyContent::ContentDistribution(value.clone()),
+          AlignContent::ContentPosition { overflow, value } => JustifyContent::ContentPosition {
+            overflow: overflow.clone(),
+            value: value.clone(),
+          },
+        }
+      }
+    };
+
+    Ok(PlaceContent { align, justify })
+  }
+}
+
+impl ToCss for PlaceContent {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    self.align.to_css(dest)?;
+    let is_equal = match self.justify {
+      JustifyContent::Normal if self.align == AlignContent::Normal => true,
+      JustifyContent::ContentDistribution(d) if matches!(self.align, AlignContent::ContentDistribution(d2) if d == d2) => {
+        true
+      }
+      JustifyContent::ContentPosition { overflow: o, value: c } if matches!(self.align, AlignContent::ContentPosition { overflow: o2, value: c2 } if o == o2 && c == c2) => {
+        true
+      }
+      _ => false,
+    };
+
+    if !is_equal {
+      dest.write_str(" ")?;
+      self.justify.to_css(dest)?;
+    }
+
+    Ok(())
+  }
+}
+
+enum_property! {
+  /// A [`<self-position>`](https://www.w3.org/TR/css-align-3/#typedef-self-position) value.
+  pub enum SelfPosition {
+    /// Item is centered within the container.
+    Center,
+    /// Item is aligned to the start of the container.
+    Start,
+    /// Item is aligned to the end of the container.
+    End,
+    /// Item is aligned to the edge of the container corresponding to the start side of the item.
+    SelfStart,
+    /// Item is aligned to the edge of the container corresponding to the end side of the item.
+    SelfEnd,
+    /// Item  is aligned to the start of the container, within flexbox layouts.
+    FlexStart,
+    /// Item  is aligned to the end of the container, within flexbox layouts.
+    FlexEnd,
+  }
+}
+
+/// A value for the [align-self](https://www.w3.org/TR/css-align-3/#align-self-property) property.
+#[derive(Debug, Clone, PartialEq, Parse, ToCss)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum AlignSelf {
+  /// Automatic alignment.
+  Auto,
+  /// Default alignment.
+  Normal,
+  /// Item is stretched.
+  Stretch,
+  /// A baseline position keyword.
+  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<BaselinePosition>"))]
+  BaselinePosition(BaselinePosition),
+  /// A self position keyword.
+  SelfPosition {
+    /// An overflow alignment mode.
+    overflow: Option<OverflowPosition>,
+    /// A self position keyword.
+    value: SelfPosition,
+  },
+}
+
+/// A value for the [justify-self](https://www.w3.org/TR/css-align-3/#justify-self-property) property.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum JustifySelf {
+  /// Automatic justification.
+  Auto,
+  /// Default justification.
+  Normal,
+  /// Item is stretched.
+  Stretch,
+  /// A baseline position keyword.
+  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<BaselinePosition>"))]
+  BaselinePosition(BaselinePosition),
+  /// A self position keyword.
+  SelfPosition {
+    /// A self position keyword.
+    value: SelfPosition,
+    /// An overflow alignment mode.
+    overflow: Option<OverflowPosition>,
+  },
+  /// Item is justified to the left.
+  Left {
+    /// An overflow alignment mode.
+    overflow: Option<OverflowPosition>,
+  },
+  /// Item is justified to the right.
+  Right {
+    /// An overflow alignment mode.
+    overflow: Option<OverflowPosition>,
+  },
+}
+
+impl<'i> Parse<'i> for JustifySelf {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    if input.try_parse(|input| input.expect_ident_matching("auto")).is_ok() {
+      return Ok(JustifySelf::Auto);
+    }
+
+    if input.try_parse(|input| input.expect_ident_matching("normal")).is_ok() {
+      return Ok(JustifySelf::Normal);
+    }
+
+    if input.try_parse(|input| input.expect_ident_matching("stretch")).is_ok() {
+      return Ok(JustifySelf::Stretch);
+    }
+
+    if let Ok(val) = input.try_parse(BaselinePosition::parse) {
+      return Ok(JustifySelf::BaselinePosition(val));
+    }
+
+    let overflow = input.try_parse(OverflowPosition::parse).ok();
+    if let Ok(value) = input.try_parse(SelfPosition::parse) {
+      return Ok(JustifySelf::SelfPosition { overflow, value });
+    }
+
+    let location = input.current_source_location();
+    let ident = input.expect_ident()?;
+    match_ignore_ascii_case! { &*ident,
+      "left" => Ok(JustifySelf::Left { overflow }),
+      "right" => Ok(JustifySelf::Right { overflow }),
+      _ => Err(location.new_unexpected_token_error(
+        cssparser::Token::Ident(ident.clone())
+      ))
+    }
+  }
+}
+
+impl ToCss for JustifySelf {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    match self {
+      JustifySelf::Auto => dest.write_str("auto"),
+      JustifySelf::Normal => dest.write_str("normal"),
+      JustifySelf::Stretch => dest.write_str("stretch"),
+      JustifySelf::BaselinePosition(val) => val.to_css(dest),
+      JustifySelf::SelfPosition { overflow, value } => {
+        if let Some(overflow) = overflow {
+          overflow.to_css(dest)?;
+          dest.write_str(" ")?;
+        }
+
+        value.to_css(dest)
+      }
+      JustifySelf::Left { overflow } => {
+        if let Some(overflow) = overflow {
+          overflow.to_css(dest)?;
+          dest.write_str(" ")?;
+        }
+
+        dest.write_str("left")
+      }
+      JustifySelf::Right { overflow } => {
+        if let Some(overflow) = overflow {
+          overflow.to_css(dest)?;
+          dest.write_str(" ")?;
+        }
+
+        dest.write_str("right")
+      }
+    }
+  }
+}
+
+define_shorthand! {
+  /// A value for the [place-self](https://www.w3.org/TR/css-align-3/#place-self-property) shorthand property.
+  pub struct PlaceSelf {
+    /// The item alignment.
+    align: AlignSelf(AlignSelf, VendorPrefix),
+    /// The item justification.
+    justify: JustifySelf(JustifySelf),
+  }
+}
+
+impl<'i> Parse<'i> for PlaceSelf {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let align = AlignSelf::parse(input)?;
+    let justify = match input.try_parse(JustifySelf::parse) {
+      Ok(j) => j,
+      Err(_) => {
+        // The second value is assigned to justify-self; if omitted, it is copied from the first value.
+        match &align {
+          AlignSelf::Auto => JustifySelf::Auto,
+          AlignSelf::Normal => JustifySelf::Normal,
+          AlignSelf::Stretch => JustifySelf::Stretch,
+          AlignSelf::BaselinePosition(p) => JustifySelf::BaselinePosition(p.clone()),
+          AlignSelf::SelfPosition { overflow, value } => JustifySelf::SelfPosition {
+            overflow: overflow.clone(),
+            value: value.clone(),
+          },
+        }
+      }
+    };
+
+    Ok(PlaceSelf { align, justify })
+  }
+}
+
+impl ToCss for PlaceSelf {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    self.align.to_css(dest)?;
+    let is_equal = match &self.justify {
+      JustifySelf::Auto => true,
+      JustifySelf::Normal => self.align == AlignSelf::Normal,
+      JustifySelf::Stretch => self.align == AlignSelf::Normal,
+      JustifySelf::BaselinePosition(p) if matches!(&self.align, AlignSelf::BaselinePosition(p2) if p == p2) => {
+        true
+      }
+      JustifySelf::SelfPosition { overflow: o, value: c } if matches!(&self.align, AlignSelf::SelfPosition  { overflow: o2, value: c2 } if o == o2 && c == c2) => {
+        true
+      }
+      _ => false,
+    };
+
+    if !is_equal {
+      dest.write_str(" ")?;
+      self.justify.to_css(dest)?;
+    }
+
+    Ok(())
+  }
+}
+
+/// A value for the [align-items](https://www.w3.org/TR/css-align-3/#align-items-property) property.
+#[derive(Debug, Clone, PartialEq, Parse, ToCss)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum AlignItems {
+  /// Default alignment.
+  Normal,
+  /// Items are stretched.
+  Stretch,
+  /// A baseline position keyword.
+  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<BaselinePosition>"))]
+  BaselinePosition(BaselinePosition),
+  /// A self position keyword.
+  SelfPosition {
+    /// An overflow alignment mode.
+    overflow: Option<OverflowPosition>,
+    /// A self position keyword.
+    value: SelfPosition,
+  },
+}
+
+/// A legacy justification keyword, as used in the `justify-items` property.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub enum LegacyJustify {
+  /// Left justify.
+  Left,
+  /// Right justify.
+  Right,
+  /// Centered.
+  Center,
+}
+
+impl<'i> Parse<'i> for LegacyJustify {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let location = input.current_source_location();
+    let ident = input.expect_ident()?;
+    match_ignore_ascii_case! { &*ident,
+      "legacy" => {
+        let location = input.current_source_location();
+        let ident = input.expect_ident()?;
+        match_ignore_ascii_case! { &*ident,
+          "left" => Ok(LegacyJustify::Left),
+          "right" => Ok(LegacyJustify::Right),
+          "center" => Ok(LegacyJustify::Center),
+          _ => Err(location.new_unexpected_token_error(
+            cssparser::Token::Ident(ident.clone())
+          ))
+        }
+      },
+      "left" => {
+        input.expect_ident_matching("legacy")?;
+        Ok(LegacyJustify::Left)
+      },
+      "right" => {
+        input.expect_ident_matching("legacy")?;
+        Ok(LegacyJustify::Right)
+      },
+      "center" => {
+        input.expect_ident_matching("legacy")?;
+        Ok(LegacyJustify::Center)
+      },
+      _ => Err(location.new_unexpected_token_error(
+        cssparser::Token::Ident(ident.clone())
+      ))
+    }
+  }
+}
+
+impl ToCss for LegacyJustify {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    dest.write_str("legacy ")?;
+    match self {
+      LegacyJustify::Left => dest.write_str("left"),
+      LegacyJustify::Right => dest.write_str("right"),
+      LegacyJustify::Center => dest.write_str("center"),
+    }
+  }
+}
+
+/// A value for the [justify-items](https://www.w3.org/TR/css-align-3/#justify-items-property) property.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum JustifyItems {
+  /// Default justification.
+  Normal,
+  /// Items are stretched.
+  Stretch,
+  /// A baseline position keyword.
+  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<BaselinePosition>"))]
+  BaselinePosition(BaselinePosition),
+  /// A self position keyword, with optional overflow position.
+  SelfPosition {
+    /// A self position keyword.
+    value: SelfPosition,
+    /// An overflow alignment mode.
+    overflow: Option<OverflowPosition>,
+  },
+  /// Items are justified to the left, with an optional overflow position.
+  Left {
+    /// An overflow alignment mode.
+    overflow: Option<OverflowPosition>,
+  },
+  /// Items are justified to the right, with an optional overflow position.
+  Right {
+    /// An overflow alignment mode.
+    overflow: Option<OverflowPosition>,
+  },
+  /// A legacy justification keyword.
+  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<LegacyJustify>"))]
+  Legacy(LegacyJustify),
+}
+
+impl<'i> Parse<'i> for JustifyItems {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    if input.try_parse(|input| input.expect_ident_matching("normal")).is_ok() {
+      return Ok(JustifyItems::Normal);
+    }
+
+    if input.try_parse(|input| input.expect_ident_matching("stretch")).is_ok() {
+      return Ok(JustifyItems::Stretch);
+    }
+
+    if let Ok(val) = input.try_parse(BaselinePosition::parse) {
+      return Ok(JustifyItems::BaselinePosition(val));
+    }
+
+    if let Ok(val) = input.try_parse(LegacyJustify::parse) {
+      return Ok(JustifyItems::Legacy(val));
+    }
+
+    let overflow = input.try_parse(OverflowPosition::parse).ok();
+    if let Ok(value) = input.try_parse(SelfPosition::parse) {
+      return Ok(JustifyItems::SelfPosition { overflow, value });
+    }
+
+    let location = input.current_source_location();
+    let ident = input.expect_ident()?;
+    match_ignore_ascii_case! { &*ident,
+      "left" => Ok(JustifyItems::Left { overflow }),
+      "right" => Ok(JustifyItems::Right { overflow }),
+      _ => Err(location.new_unexpected_token_error(
+        cssparser::Token::Ident(ident.clone())
+      ))
+    }
+  }
+}
+
+impl ToCss for JustifyItems {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    match self {
+      JustifyItems::Normal => dest.write_str("normal"),
+      JustifyItems::Stretch => dest.write_str("stretch"),
+      JustifyItems::BaselinePosition(val) => val.to_css(dest),
+      JustifyItems::Legacy(val) => val.to_css(dest),
+      JustifyItems::SelfPosition { overflow, value } => {
+        if let Some(overflow) = overflow {
+          overflow.to_css(dest)?;
+          dest.write_str(" ")?;
+        }
+
+        value.to_css(dest)
+      }
+      JustifyItems::Left { overflow } => {
+        if let Some(overflow) = overflow {
+          overflow.to_css(dest)?;
+          dest.write_str(" ")?;
+        }
+
+        dest.write_str("left")
+      }
+      JustifyItems::Right { overflow } => {
+        if let Some(overflow) = overflow {
+          overflow.to_css(dest)?;
+          dest.write_str(" ")?;
+        }
+
+        dest.write_str("right")
+      }
+    }
+  }
+}
+
+define_shorthand! {
+  /// A value for the [place-items](https://www.w3.org/TR/css-align-3/#place-items-property) shorthand property.
+  pub struct PlaceItems {
+    /// The item alignment.
+    align: AlignItems(AlignItems, VendorPrefix),
+    /// The item justification.
+    justify: JustifyItems(JustifyItems),
+  }
+}
+
+impl<'i> Parse<'i> for PlaceItems {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let align = AlignItems::parse(input)?;
+    let justify = match input.try_parse(JustifyItems::parse) {
+      Ok(j) => j,
+      Err(_) => {
+        // The second value is assigned to justify-items; if omitted, it is copied from the first value.
+        match &align {
+          AlignItems::Normal => JustifyItems::Normal,
+          AlignItems::Stretch => JustifyItems::Stretch,
+          AlignItems::BaselinePosition(p) => JustifyItems::BaselinePosition(p.clone()),
+          AlignItems::SelfPosition { overflow, value } => JustifyItems::SelfPosition {
+            overflow: overflow.clone(),
+            value: value.clone(),
+          },
+        }
+      }
+    };
+
+    Ok(PlaceItems { align, justify })
+  }
+}
+
+impl ToCss for PlaceItems {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    self.align.to_css(dest)?;
+    let is_equal = match &self.justify {
+      JustifyItems::Normal => self.align == AlignItems::Normal,
+      JustifyItems::Stretch => self.align == AlignItems::Normal,
+      JustifyItems::BaselinePosition(p) if matches!(&self.align, AlignItems::BaselinePosition(p2) if p == p2) => {
+        true
+      }
+      JustifyItems::SelfPosition { overflow: o, value: c } if matches!(&self.align, AlignItems::SelfPosition { overflow: o2, value: c2 } if o == o2 && c == c2) => {
+        true
+      }
+      _ => false,
+    };
+
+    if !is_equal {
+      dest.write_str(" ")?;
+      self.justify.to_css(dest)?;
+    }
+
+    Ok(())
+  }
+}
+
+/// A [gap](https://www.w3.org/TR/css-align-3/#column-row-gap) value, as used in the
+/// `column-gap` and `row-gap` properties.
+#[derive(Debug, Clone, PartialEq, Parse, ToCss)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", content = "value", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum GapValue {
+  /// Equal to `1em` for multi-column containers, and zero otherwise.
+  Normal,
+  /// An explicit length.
+  LengthPercentage(LengthPercentage),
+}
+
+define_shorthand! {
+  /// A value for the [gap](https://www.w3.org/TR/css-align-3/#gap-shorthand) shorthand property.
+  pub struct Gap {
+    /// The row gap.
+    row: RowGap(GapValue),
+    /// The column gap.
+    column: ColumnGap(GapValue),
+  }
+}
+
+impl<'i> Parse<'i> for Gap {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let row = GapValue::parse(input)?;
+    let column = input.try_parse(GapValue::parse).unwrap_or(row.clone());
+    Ok(Gap { row, column })
+  }
+}
+
+impl ToCss for Gap {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    self.row.to_css(dest)?;
+    if self.column != self.row {
+      dest.write_str(" ")?;
+      self.column.to_css(dest)?;
+    }
+    Ok(())
+  }
+}
+
+#[derive(Default, Debug)]
+pub(crate) struct AlignHandler {
+  align_content: Option<(AlignContent, VendorPrefix)>,
+  flex_line_pack: Option<(FlexLinePack, VendorPrefix)>,
+  justify_content: Option<(JustifyContent, VendorPrefix)>,
+  box_pack: Option<(BoxPack, VendorPrefix)>,
+  flex_pack: Option<(FlexPack, VendorPrefix)>,
+  align_self: Option<(AlignSelf, VendorPrefix)>,
+  flex_item_align: Option<(FlexItemAlign, VendorPrefix)>,
+  justify_self: Option<JustifySelf>,
+  align_items: Option<(AlignItems, VendorPrefix)>,
+  box_align: Option<(BoxAlign, VendorPrefix)>,
+  flex_align: Option<(FlexAlign, VendorPrefix)>,
+  justify_items: Option<JustifyItems>,
+  row_gap: Option<GapValue>,
+  column_gap: Option<GapValue>,
+  has_any: bool,
+}
+
+impl<'i> PropertyHandler<'i> for AlignHandler {
+  fn handle_property(
+    &mut self,
+    property: &Property<'i>,
+    dest: &mut DeclarationList<'i>,
+    context: &mut PropertyHandlerContext<'i, '_>,
+  ) -> bool {
+    use Property::*;
+
+    macro_rules! maybe_flush {
+      ($prop: ident, $val: expr, $vp: expr) => {{
+        // If two vendor prefixes for the same property have different
+        // values, we need to flush what we have immediately to preserve order.
+        if let Some((val, prefixes)) = &self.$prop {
+          if val != $val && !prefixes.contains(*$vp) {
+            self.flush(dest, context);
+          }
+        }
+      }};
+    }
+
+    macro_rules! property {
+      ($prop: ident, $val: expr, $vp: expr) => {{
+        maybe_flush!($prop, $val, $vp);
+
+        // Otherwise, update the value and add the prefix.
+        if let Some((val, prefixes)) = &mut self.$prop {
+          *val = $val.clone();
+          *prefixes |= *$vp;
+        } else {
+          self.$prop = Some(($val.clone(), *$vp));
+          self.has_any = true;
+        }
+      }};
+    }
+
+    match property {
+      AlignContent(val, vp) => {
+        self.flex_line_pack = None;
+        property!(align_content, val, vp);
+      }
+      FlexLinePack(val, vp) => property!(flex_line_pack, val, vp),
+      JustifyContent(val, vp) => {
+        self.box_pack = None;
+        self.flex_pack = None;
+        property!(justify_content, val, vp);
+      }
+      BoxPack(val, vp) => property!(box_pack, val, vp),
+      FlexPack(val, vp) => property!(flex_pack, val, vp),
+      PlaceContent(val) => {
+        self.flex_line_pack = None;
+        self.box_pack = None;
+        self.flex_pack = None;
+        maybe_flush!(align_content, &val.align, &VendorPrefix::None);
+        maybe_flush!(justify_content, &val.justify, &VendorPrefix::None);
+        property!(align_content, &val.align, &VendorPrefix::None);
+        property!(justify_content, &val.justify, &VendorPrefix::None);
+      }
+      AlignSelf(val, vp) => {
+        self.flex_item_align = None;
+        property!(align_self, val, vp);
+      }
+      FlexItemAlign(val, vp) => property!(flex_item_align, val, vp),
+      JustifySelf(val) => {
+        self.justify_self = Some(val.clone());
+        self.has_any = true;
+      }
+      PlaceSelf(val) => {
+        self.flex_item_align = None;
+        property!(align_self, &val.align, &VendorPrefix::None);
+        self.justify_self = Some(val.justify.clone());
+      }
+      AlignItems(val, vp) => {
+        self.box_align = None;
+        self.flex_align = None;
+        property!(align_items, val, vp);
+      }
+      BoxAlign(val, vp) => property!(box_align, val, vp),
+      FlexAlign(val, vp) => property!(flex_align, val, vp),
+      JustifyItems(val) => {
+        self.justify_items = Some(val.clone());
+        self.has_any = true;
+      }
+      PlaceItems(val) => {
+        self.box_align = None;
+        self.flex_align = None;
+        property!(align_items, &val.align, &VendorPrefix::None);
+        self.justify_items = Some(val.justify.clone());
+      }
+      RowGap(val) => {
+        self.row_gap = Some(val.clone());
+        self.has_any = true;
+      }
+      ColumnGap(val) => {
+        self.column_gap = Some(val.clone());
+        self.has_any = true;
+      }
+      Gap(val) => {
+        self.row_gap = Some(val.row.clone());
+        self.column_gap = Some(val.column.clone());
+        self.has_any = true;
+      }
+      Unparsed(val) if is_align_property(&val.property_id) => {
+        self.flush(dest, context);
+        dest.push(property.clone()) // TODO: prefix?
+      }
+      _ => return false,
+    }
+
+    true
+  }
+
+  fn finalize(&mut self, dest: &mut DeclarationList<'i>, context: &mut PropertyHandlerContext<'i, '_>) {
+    self.flush(dest, context);
+  }
+}
+
+impl AlignHandler {
+  fn flush<'i>(&mut self, dest: &mut DeclarationList<'i>, context: &mut PropertyHandlerContext<'i, '_>) {
+    if !self.has_any {
+      return;
+    }
+
+    self.has_any = false;
+
+    let mut align_content = std::mem::take(&mut self.align_content);
+    let mut justify_content = std::mem::take(&mut self.justify_content);
+    let mut align_self = std::mem::take(&mut self.align_self);
+    let mut justify_self = std::mem::take(&mut self.justify_self);
+    let mut align_items = std::mem::take(&mut self.align_items);
+    let mut justify_items = std::mem::take(&mut self.justify_items);
+    let row_gap = std::mem::take(&mut self.row_gap);
+    let column_gap = std::mem::take(&mut self.column_gap);
+    let box_align = std::mem::take(&mut self.box_align);
+    let box_pack = std::mem::take(&mut self.box_pack);
+    let flex_line_pack = std::mem::take(&mut self.flex_line_pack);
+    let flex_pack = std::mem::take(&mut self.flex_pack);
+    let flex_align = std::mem::take(&mut self.flex_align);
+    let flex_item_align = std::mem::take(&mut self.flex_item_align);
+
+    // Gets prefixes for standard properties.
+    macro_rules! prefixes {
+      ($prop: ident) => {{
+        let mut prefix = context.targets.prefixes(VendorPrefix::None, Feature::$prop);
+        // Firefox only implemented the 2009 spec prefixed.
+        // Microsoft only implemented the 2012 spec prefixed.
+        prefix.remove(VendorPrefix::Moz | VendorPrefix::Ms);
+        prefix
+      }};
+    }
+
+    macro_rules! standard_property {
+      ($prop: ident, $key: ident) => {
+        if let Some((val, prefix)) = $key {
+          // If we have an unprefixed property, override necessary prefixes.
+          let prefix = if prefix.contains(VendorPrefix::None) {
+            prefixes!($prop)
+          } else {
+            prefix
+          };
+          dest.push(Property::$prop(val, prefix))
+        }
+      };
+    }
+
+    macro_rules! legacy_property {
+      ($prop: ident, $key: ident, $( $prop_2009: ident )?, $prop_2012: ident) => {
+        if let Some((val, prefix)) = &$key {
+          // If we have an unprefixed standard property, generate legacy prefixed versions.
+          let mut prefix = context.targets.prefixes(*prefix, Feature::$prop);
+
+          if prefix.contains(VendorPrefix::None) {
+            $(
+              // 2009 spec, implemented by webkit and firefox.
+              if let Some(targets) = context.targets.browsers {
+                let mut prefixes_2009 = VendorPrefix::empty();
+                if is_flex_2009(targets) {
+                  prefixes_2009 |= VendorPrefix::WebKit;
+                }
+                if prefix.contains(VendorPrefix::Moz) {
+                  prefixes_2009 |= VendorPrefix::Moz;
+                }
+                if !prefixes_2009.is_empty() {
+                  if let Some(v) = $prop_2009::from_standard(&val) {
+                    dest.push(Property::$prop_2009(v, prefixes_2009));
+                  }
+                }
+              }
+            )?
+          }
+
+          // 2012 spec, implemented by microsoft.
+          if prefix.contains(VendorPrefix::Ms) {
+            if let Some(v) = $prop_2012::from_standard(&val) {
+              dest.push(Property::$prop_2012(v, VendorPrefix::Ms));
+            }
+          }
+
+          // Remove Firefox and IE from standard prefixes.
+          prefix.remove(VendorPrefix::Moz | VendorPrefix::Ms);
+        }
+      };
+    }
+
+    macro_rules! prefixed_property {
+      ($prop: ident, $key: expr) => {
+        if let Some((val, prefix)) = $key {
+          dest.push(Property::$prop(val, prefix))
+        }
+      };
+    }
+
+    macro_rules! unprefixed_property {
+      ($prop: ident, $key: expr) => {
+        if let Some(val) = $key {
+          dest.push(Property::$prop(val))
+        }
+      };
+    }
+
+    macro_rules! shorthand {
+      ($prop: ident, $align_prop: ident, $align: ident, $justify: ident $(, $justify_prop: ident )?) => {
+        if let (Some((align, align_prefix)), Some(justify)) = (&mut $align, &mut $justify) {
+          let intersection = *align_prefix $( & {
+            // Hack for conditional compilation. Have to use a variable.
+            #[allow(non_snake_case)]
+            let $justify_prop = justify.1;
+            $justify_prop
+          })?;
+
+          // Only use shorthand if unprefixed.
+          if intersection.contains(VendorPrefix::None) {
+            // Add prefixed longhands if needed.
+            *align_prefix = prefixes!($align_prop);
+            align_prefix.remove(VendorPrefix::None);
+            if !align_prefix.is_empty() {
+              dest.push(Property::$align_prop(align.clone(), *align_prefix))
+            }
+
+            $(
+              let (justify, justify_prefix) = justify;
+              *justify_prefix = prefixes!($justify_prop);
+              justify_prefix.remove(VendorPrefix::None);
+
+              if !justify_prefix.is_empty() {
+                dest.push(Property::$justify_prop(justify.clone(), *justify_prefix))
+              }
+            )?
+
+            // Add shorthand.
+            dest.push(Property::$prop($prop {
+              align: align.clone(),
+              justify: justify.clone()
+            }));
+
+            $align = None;
+            $justify = None;
+          }
+        }
+      };
+    }
+
+    // 2009 properties
+    prefixed_property!(BoxAlign, box_align);
+    prefixed_property!(BoxPack, box_pack);
+
+    // 2012 properties
+    prefixed_property!(FlexPack, flex_pack);
+    prefixed_property!(FlexAlign, flex_align);
+    prefixed_property!(FlexItemAlign, flex_item_align);
+    prefixed_property!(FlexLinePack, flex_line_pack);
+
+    legacy_property!(AlignContent, align_content, , FlexLinePack);
+    legacy_property!(JustifyContent, justify_content, BoxPack, FlexPack);
+    if context.targets.is_compatible(compat::Feature::PlaceContent) {
+      shorthand!(
+        PlaceContent,
+        AlignContent,
+        align_content,
+        justify_content,
+        JustifyContent
+      );
+    }
+    standard_property!(AlignContent, align_content);
+    standard_property!(JustifyContent, justify_content);
+
+    legacy_property!(AlignSelf, align_self, , FlexItemAlign);
+    if context.targets.is_compatible(compat::Feature::PlaceSelf) {
+      shorthand!(PlaceSelf, AlignSelf, align_self, justify_self);
+    }
+    standard_property!(AlignSelf, align_self);
+    unprefixed_property!(JustifySelf, justify_self);
+
+    legacy_property!(AlignItems, align_items, BoxAlign, FlexAlign);
+    if context.targets.is_compatible(compat::Feature::PlaceItems) {
+      shorthand!(PlaceItems, AlignItems, align_items, justify_items);
+    }
+    standard_property!(AlignItems, align_items);
+    unprefixed_property!(JustifyItems, justify_items);
+
+    if row_gap.is_some() && column_gap.is_some() {
+      dest.push(Property::Gap(Gap {
+        row: row_gap.unwrap(),
+        column: column_gap.unwrap(),
+      }))
+    } else {
+      if let Some(gap) = row_gap {
+        dest.push(Property::RowGap(gap))
+      }
+
+      if let Some(gap) = column_gap {
+        dest.push(Property::ColumnGap(gap))
+      }
+    }
+  }
+}
+
+#[inline]
+fn is_align_property(property_id: &PropertyId) -> bool {
+  match property_id {
+    PropertyId::AlignContent(_)
+    | PropertyId::FlexLinePack(_)
+    | PropertyId::JustifyContent(_)
+    | PropertyId::BoxPack(_)
+    | PropertyId::FlexPack(_)
+    | PropertyId::PlaceContent
+    | PropertyId::AlignSelf(_)
+    | PropertyId::FlexItemAlign(_)
+    | PropertyId::JustifySelf
+    | PropertyId::PlaceSelf
+    | PropertyId::AlignItems(_)
+    | PropertyId::BoxAlign(_)
+    | PropertyId::FlexAlign(_)
+    | PropertyId::JustifyItems
+    | PropertyId::PlaceItems
+    | PropertyId::RowGap
+    | PropertyId::ColumnGap
+    | PropertyId::Gap => true,
+    _ => false,
+  }
+}
diff --git a/src/properties/animation.rs b/src/properties/animation.rs
new file mode 100644
index 0000000..51bd1ea
--- /dev/null
+++ b/src/properties/animation.rs
@@ -0,0 +1,1095 @@
+//! CSS properties related to keyframe animations.
+
+use std::borrow::Cow;
+
+use crate::context::PropertyHandlerContext;
+use crate::declaration::{DeclarationBlock, DeclarationList};
+use crate::error::{ParserError, PrinterError};
+use crate::macros::*;
+use crate::prefixes::Feature;
+use crate::printer::Printer;
+use crate::properties::{Property, PropertyId, TokenOrValue, VendorPrefix};
+use crate::traits::{Parse, PropertyHandler, Shorthand, ToCss, Zero};
+use crate::values::ident::DashedIdent;
+use crate::values::number::CSSNumber;
+use crate::values::percentage::Percentage;
+use crate::values::size::Size2D;
+use crate::values::string::CSSString;
+use crate::values::{easing::EasingFunction, ident::CustomIdent, time::Time};
+#[cfg(feature = "visitor")]
+use crate::visitor::Visit;
+use cssparser::*;
+use itertools::izip;
+use smallvec::SmallVec;
+
+use super::{LengthPercentage, LengthPercentageOrAuto};
+
+/// A value for the [animation-name](https://drafts.csswg.org/css-animations/#animation-name) property.
+#[derive(Debug, Clone, PartialEq, Parse)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", content = "value", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub enum AnimationName<'i> {
+  /// The `none` keyword.
+  None,
+  /// An identifier of a `@keyframes` rule.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  Ident(CustomIdent<'i>),
+  /// A `<string>` name of a `@keyframes` rule.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  String(CSSString<'i>),
+}
+
+impl<'i> ToCss for AnimationName<'i> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    let css_module_animation_enabled =
+      dest.css_module.as_ref().map_or(false, |css_module| css_module.config.animation);
+
+    match self {
+      AnimationName::None => dest.write_str("none"),
+      AnimationName::Ident(s) => {
+        if css_module_animation_enabled {
+          if let Some(css_module) = &mut dest.css_module {
+            css_module.reference(&s.0, dest.loc.source_index)
+          }
+        }
+        s.to_css_with_options(dest, css_module_animation_enabled)
+      }
+      AnimationName::String(s) => {
+        if css_module_animation_enabled {
+          if let Some(css_module) = &mut dest.css_module {
+            css_module.reference(&s, dest.loc.source_index)
+          }
+        }
+
+        // CSS-wide keywords and `none` cannot remove quotes.
+        match_ignore_ascii_case! { &*s,
+          "none" | "initial" | "inherit" | "unset" | "default" | "revert" | "revert-layer" => {
+            serialize_string(&s, dest)?;
+            Ok(())
+          },
+          _ => {
+            dest.write_ident(s.as_ref(), css_module_animation_enabled)
+          }
+        }
+      }
+    }
+  }
+}
+
+/// A list of animation names.
+pub type AnimationNameList<'i> = SmallVec<[AnimationName<'i>; 1]>;
+
+/// A value for the [animation-iteration-count](https://drafts.csswg.org/css-animations/#animation-iteration-count) property.
+#[derive(Debug, Clone, PartialEq, Parse, ToCss)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", content = "value", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum AnimationIterationCount {
+  /// The animation will repeat the specified number of times.
+  Number(CSSNumber),
+  /// The animation will repeat forever.
+  Infinite,
+}
+
+impl Default for AnimationIterationCount {
+  fn default() -> Self {
+    AnimationIterationCount::Number(1.0)
+  }
+}
+
+enum_property! {
+  /// A value for the [animation-direction](https://drafts.csswg.org/css-animations/#animation-direction) property.
+  pub enum AnimationDirection {
+    /// The animation is played as specified
+    Normal,
+    /// The animation is played in reverse.
+    Reverse,
+    /// The animation iterations alternate between forward and reverse.
+    Alternate,
+    /// The animation iterations alternate between forward and reverse, with reverse occurring first.
+    AlternateReverse,
+  }
+}
+
+impl Default for AnimationDirection {
+  fn default() -> Self {
+    AnimationDirection::Normal
+  }
+}
+
+enum_property! {
+  /// A value for the [animation-play-state](https://drafts.csswg.org/css-animations/#animation-play-state) property.
+  pub enum AnimationPlayState {
+    /// The animation is playing.
+    Running,
+    /// The animation is paused.
+    Paused,
+  }
+}
+
+impl Default for AnimationPlayState {
+  fn default() -> Self {
+    AnimationPlayState::Running
+  }
+}
+
+enum_property! {
+  /// A value for the [animation-fill-mode](https://drafts.csswg.org/css-animations/#animation-fill-mode) property.
+  pub enum AnimationFillMode {
+    /// The animation has no effect while not playing.
+    None,
+    /// After the animation, the ending values are applied.
+    Forwards,
+    /// Before the animation, the starting values are applied.
+    Backwards,
+    /// Both forwards and backwards apply.
+    Both,
+  }
+}
+
+impl Default for AnimationFillMode {
+  fn default() -> Self {
+    AnimationFillMode::None
+  }
+}
+
+enum_property! {
+  /// A value for the [animation-composition](https://drafts.csswg.org/css-animations-2/#animation-composition) property.
+  pub enum AnimationComposition {
+    /// The result of compositing the effect value with the underlying value is simply the effect value.
+    Replace,
+    /// The effect value is added to the underlying value.
+    Add,
+    /// The effect value is accumulated onto the underlying value.
+    Accumulate,
+  }
+}
+
+/// A value for the [animation-timeline](https://drafts.csswg.org/css-animations-2/#animation-timeline) property.
+#[derive(Debug, Clone, PartialEq, Parse, ToCss)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", content = "value", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum AnimationTimeline<'i> {
+  /// The animation’s timeline is a DocumentTimeline, more specifically the default document timeline.
+  Auto,
+  /// The animation is not associated with a timeline.
+  None,
+  /// A timeline referenced by name.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  DashedIdent(DashedIdent<'i>),
+  /// The scroll() function.
+  Scroll(ScrollTimeline),
+  /// The view() function.
+  View(ViewTimeline),
+}
+
+impl<'i> Default for AnimationTimeline<'i> {
+  fn default() -> Self {
+    AnimationTimeline::Auto
+  }
+}
+
+/// The [scroll()](https://drafts.csswg.org/scroll-animations-1/#scroll-notation) function.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub struct ScrollTimeline {
+  /// Specifies which element to use as the scroll container.
+  pub scroller: Scroller,
+  /// Specifies which axis of the scroll container to use as the progress for the timeline.
+  pub axis: ScrollAxis,
+}
+
+impl<'i> Parse<'i> for ScrollTimeline {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    input.expect_function_matching("scroll")?;
+    input.parse_nested_block(|input| {
+      let mut scroller = None;
+      let mut axis = None;
+      loop {
+        if scroller.is_none() {
+          scroller = input.try_parse(Scroller::parse).ok();
+        }
+
+        if axis.is_none() {
+          axis = input.try_parse(ScrollAxis::parse).ok();
+          if axis.is_some() {
+            continue;
+          }
+        }
+        break;
+      }
+
+      Ok(ScrollTimeline {
+        scroller: scroller.unwrap_or_default(),
+        axis: axis.unwrap_or_default(),
+      })
+    })
+  }
+}
+
+impl ToCss for ScrollTimeline {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    dest.write_str("scroll(")?;
+
+    let mut needs_space = false;
+    if self.scroller != Scroller::default() {
+      self.scroller.to_css(dest)?;
+      needs_space = true;
+    }
+
+    if self.axis != ScrollAxis::default() {
+      if needs_space {
+        dest.write_char(' ')?;
+      }
+      self.axis.to_css(dest)?;
+    }
+
+    dest.write_char(')')
+  }
+}
+
+enum_property! {
+  /// A scroller, used in the `scroll()` function.
+  pub enum Scroller {
+    /// Specifies to use the document viewport as the scroll container.
+    "root": Root,
+    /// Specifies to use the nearest ancestor scroll container.
+    "nearest": Nearest,
+    /// Specifies to use the element’s own principal box as the scroll container.
+    "self": SelfElement,
+  }
+}
+
+impl Default for Scroller {
+  fn default() -> Self {
+    Scroller::Nearest
+  }
+}
+
+enum_property! {
+  /// A scroll axis, used in the `scroll()` function.
+  pub enum ScrollAxis {
+    /// Specifies to use the measure of progress along the block axis of the scroll container.
+    Block,
+    /// Specifies to use the measure of progress along the inline axis of the scroll container.
+    Inline,
+    /// Specifies to use the measure of progress along the horizontal axis of the scroll container.
+    X,
+    /// Specifies to use the measure of progress along the vertical axis of the scroll container.
+    Y,
+  }
+}
+
+impl Default for ScrollAxis {
+  fn default() -> Self {
+    ScrollAxis::Block
+  }
+}
+
+/// The [view()](https://drafts.csswg.org/scroll-animations-1/#view-notation) function.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub struct ViewTimeline {
+  /// Specifies which axis of the scroll container to use as the progress for the timeline.
+  pub axis: ScrollAxis,
+  /// Provides an adjustment of the view progress visibility range.
+  pub inset: Size2D<LengthPercentageOrAuto>,
+}
+
+impl<'i> Parse<'i> for ViewTimeline {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    input.expect_function_matching("view")?;
+    input.parse_nested_block(|input| {
+      let mut axis = None;
+      let mut inset = None;
+      loop {
+        if axis.is_none() {
+          axis = input.try_parse(ScrollAxis::parse).ok();
+        }
+
+        if inset.is_none() {
+          inset = input.try_parse(Size2D::parse).ok();
+          if inset.is_some() {
+            continue;
+          }
+        }
+        break;
+      }
+
+      Ok(ViewTimeline {
+        axis: axis.unwrap_or_default(),
+        inset: inset.unwrap_or(Size2D(LengthPercentageOrAuto::Auto, LengthPercentageOrAuto::Auto)),
+      })
+    })
+  }
+}
+
+impl ToCss for ViewTimeline {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    dest.write_str("view(")?;
+    let mut needs_space = false;
+    if self.axis != ScrollAxis::default() {
+      self.axis.to_css(dest)?;
+      needs_space = true;
+    }
+
+    if self.inset.0 != LengthPercentageOrAuto::Auto || self.inset.1 != LengthPercentageOrAuto::Auto {
+      if needs_space {
+        dest.write_char(' ')?;
+      }
+      self.inset.to_css(dest)?;
+    }
+
+    dest.write_char(')')
+  }
+}
+
+/// A [view progress timeline range](https://drafts.csswg.org/scroll-animations/#view-timelines-ranges)
+#[derive(Debug, Clone, PartialEq, Parse, ToCss)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum TimelineRangeName {
+  /// Represents the full range of the view progress timeline.
+  Cover,
+  /// Represents the range during which the principal box is either fully contained by,
+  /// or fully covers, its view progress visibility range within the scrollport.
+  Contain,
+  /// Represents the range during which the principal box is entering the view progress visibility range.
+  Entry,
+  /// Represents the range during which the principal box is exiting the view progress visibility range.
+  Exit,
+  /// Represents the range during which the principal box crosses the end border edge.
+  EntryCrossing,
+  /// Represents the range during which the principal box crosses the start border edge.
+  ExitCrossing,
+}
+
+/// A value for the [animation-range-start](https://drafts.csswg.org/scroll-animations/#animation-range-start)
+/// or [animation-range-end](https://drafts.csswg.org/scroll-animations/#animation-range-end) property.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(rename_all = "lowercase")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum AnimationAttachmentRange {
+  /// The start of the animation’s attachment range is the start of its associated timeline.
+  Normal,
+  /// The animation attachment range starts at the specified point on the timeline measuring from the start of the timeline.
+  #[cfg_attr(feature = "serde", serde(untagged))]
+  LengthPercentage(LengthPercentage),
+  /// The animation attachment range starts at the specified point on the timeline measuring from the start of the specified named timeline range.
+  #[cfg_attr(feature = "serde", serde(untagged))]
+  TimelineRange {
+    /// The name of the timeline range.
+    name: TimelineRangeName,
+    /// The offset from the start of the named timeline range.
+    offset: LengthPercentage,
+  },
+}
+
+impl<'i> AnimationAttachmentRange {
+  fn parse<'t>(input: &mut Parser<'i, 't>, default: f32) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    if input.try_parse(|input| input.expect_ident_matching("normal")).is_ok() {
+      return Ok(AnimationAttachmentRange::Normal);
+    }
+
+    if let Ok(val) = input.try_parse(LengthPercentage::parse) {
+      return Ok(AnimationAttachmentRange::LengthPercentage(val));
+    }
+
+    let name = TimelineRangeName::parse(input)?;
+    let offset = input
+      .try_parse(LengthPercentage::parse)
+      .unwrap_or(LengthPercentage::Percentage(Percentage(default)));
+    Ok(AnimationAttachmentRange::TimelineRange { name, offset })
+  }
+
+  fn to_css<W>(&self, dest: &mut Printer<W>, default: f32) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    match self {
+      Self::Normal => dest.write_str("normal"),
+      Self::LengthPercentage(val) => val.to_css(dest),
+      Self::TimelineRange { name, offset } => {
+        name.to_css(dest)?;
+        if *offset != LengthPercentage::Percentage(Percentage(default)) {
+          dest.write_char(' ')?;
+          offset.to_css(dest)?;
+        }
+        Ok(())
+      }
+    }
+  }
+}
+
+impl Default for AnimationAttachmentRange {
+  fn default() -> Self {
+    AnimationAttachmentRange::Normal
+  }
+}
+
+/// A value for the [animation-range-start](https://drafts.csswg.org/scroll-animations/#animation-range-start) property.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub struct AnimationRangeStart(pub AnimationAttachmentRange);
+
+impl<'i> Parse<'i> for AnimationRangeStart {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let range = AnimationAttachmentRange::parse(input, 0.0)?;
+    Ok(Self(range))
+  }
+}
+
+impl ToCss for AnimationRangeStart {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    self.0.to_css(dest, 0.0)
+  }
+}
+
+/// A value for the [animation-range-end](https://drafts.csswg.org/scroll-animations/#animation-range-end) property.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub struct AnimationRangeEnd(pub AnimationAttachmentRange);
+
+impl<'i> Parse<'i> for AnimationRangeEnd {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let range = AnimationAttachmentRange::parse(input, 1.0)?;
+    Ok(Self(range))
+  }
+}
+
+impl ToCss for AnimationRangeEnd {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    self.0.to_css(dest, 1.0)
+  }
+}
+
+/// A value for the [animation-range](https://drafts.csswg.org/scroll-animations/#animation-range) shorthand property.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub struct AnimationRange {
+  /// The start of the animation's attachment range.
+  pub start: AnimationRangeStart,
+  /// The end of the animation's attachment range.
+  pub end: AnimationRangeEnd,
+}
+
+impl<'i> Parse<'i> for AnimationRange {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let start = AnimationRangeStart::parse(input)?;
+    let end = input
+      .try_parse(AnimationRangeStart::parse)
+      .map(|r| AnimationRangeEnd(r.0))
+      .unwrap_or_else(|_| {
+        // If <'animation-range-end'> is omitted and <'animation-range-start'> includes a <timeline-range-name> component, then
+        // animation-range-end is set to that same <timeline-range-name> and 100%. Otherwise, any omitted longhand is set to its initial value.
+        match &start.0 {
+          AnimationAttachmentRange::TimelineRange { name, .. } => {
+            AnimationRangeEnd(AnimationAttachmentRange::TimelineRange {
+              name: name.clone(),
+              offset: LengthPercentage::Percentage(Percentage(1.0)),
+            })
+          }
+          _ => AnimationRangeEnd(AnimationAttachmentRange::default()),
+        }
+      });
+    Ok(AnimationRange { start, end })
+  }
+}
+
+impl ToCss for AnimationRange {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    self.start.to_css(dest)?;
+
+    let omit_end = match (&self.start.0, &self.end.0) {
+      (
+        AnimationAttachmentRange::TimelineRange { name: start_name, .. },
+        AnimationAttachmentRange::TimelineRange {
+          name: end_name,
+          offset: end_offset,
+        },
+      ) => start_name == end_name && *end_offset == LengthPercentage::Percentage(Percentage(1.0)),
+      (_, end) => *end == AnimationAttachmentRange::default(),
+    };
+
+    if !omit_end {
+      dest.write_char(' ')?;
+      self.end.to_css(dest)?;
+    }
+    Ok(())
+  }
+}
+
+define_list_shorthand! {
+  /// A value for the [animation](https://drafts.csswg.org/css-animations/#animation) shorthand property.
+  pub struct Animation<'i>(VendorPrefix) {
+    /// The animation name.
+    #[cfg_attr(feature = "serde", serde(borrow))]
+    name: AnimationName(AnimationName<'i>, VendorPrefix),
+    /// The animation duration.
+    duration: AnimationDuration(Time, VendorPrefix),
+    /// The easing function for the animation.
+    timing_function: AnimationTimingFunction(EasingFunction, VendorPrefix),
+    /// The number of times the animation will run.
+    iteration_count: AnimationIterationCount(AnimationIterationCount, VendorPrefix),
+    /// The direction of the animation.
+    direction: AnimationDirection(AnimationDirection, VendorPrefix),
+    /// The current play state of the animation.
+    play_state: AnimationPlayState(AnimationPlayState, VendorPrefix),
+    /// The animation delay.
+    delay: AnimationDelay(Time, VendorPrefix),
+    /// The animation fill mode.
+    fill_mode: AnimationFillMode(AnimationFillMode, VendorPrefix),
+    /// The animation timeline.
+    timeline: AnimationTimeline(AnimationTimeline<'i>),
+  }
+}
+
+impl<'i> Parse<'i> for Animation<'i> {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let mut name = None;
+    let mut duration = None;
+    let mut timing_function = None;
+    let mut iteration_count = None;
+    let mut direction = None;
+    let mut play_state = None;
+    let mut delay = None;
+    let mut fill_mode = None;
+    let mut timeline = None;
+
+    macro_rules! parse_prop {
+      ($var: ident, $type: ident) => {
+        if $var.is_none() {
+          if let Ok(value) = input.try_parse($type::parse) {
+            $var = Some(value);
+            continue;
+          }
+        }
+      };
+    }
+
+    loop {
+      parse_prop!(duration, Time);
+      parse_prop!(timing_function, EasingFunction);
+      parse_prop!(delay, Time);
+      parse_prop!(iteration_count, AnimationIterationCount);
+      parse_prop!(direction, AnimationDirection);
+      parse_prop!(fill_mode, AnimationFillMode);
+      parse_prop!(play_state, AnimationPlayState);
+      parse_prop!(name, AnimationName);
+      parse_prop!(timeline, AnimationTimeline);
+      break;
+    }
+
+    Ok(Animation {
+      name: name.unwrap_or(AnimationName::None),
+      duration: duration.unwrap_or(Time::Seconds(0.0)),
+      timing_function: timing_function.unwrap_or(EasingFunction::Ease),
+      iteration_count: iteration_count.unwrap_or(AnimationIterationCount::Number(1.0)),
+      direction: direction.unwrap_or(AnimationDirection::Normal),
+      play_state: play_state.unwrap_or(AnimationPlayState::Running),
+      delay: delay.unwrap_or(Time::Seconds(0.0)),
+      fill_mode: fill_mode.unwrap_or(AnimationFillMode::None),
+      timeline: timeline.unwrap_or(AnimationTimeline::Auto),
+    })
+  }
+}
+
+impl<'i> ToCss for Animation<'i> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    match &self.name {
+      AnimationName::None => {}
+      AnimationName::Ident(CustomIdent(name)) | AnimationName::String(CSSString(name)) => {
+        if !self.duration.is_zero() || !self.delay.is_zero() {
+          self.duration.to_css(dest)?;
+          dest.write_char(' ')?;
+        }
+
+        if !self.timing_function.is_ease() || EasingFunction::is_ident(&name) {
+          self.timing_function.to_css(dest)?;
+          dest.write_char(' ')?;
+        }
+
+        if !self.delay.is_zero() {
+          self.delay.to_css(dest)?;
+          dest.write_char(' ')?;
+        }
+
+        if self.iteration_count != AnimationIterationCount::default() || name.as_ref() == "infinite" {
+          self.iteration_count.to_css(dest)?;
+          dest.write_char(' ')?;
+        }
+
+        if self.direction != AnimationDirection::default() || AnimationDirection::parse_string(&name).is_ok() {
+          self.direction.to_css(dest)?;
+          dest.write_char(' ')?;
+        }
+
+        if self.fill_mode != AnimationFillMode::default()
+          || (!name.eq_ignore_ascii_case("none") && AnimationFillMode::parse_string(&name).is_ok())
+        {
+          self.fill_mode.to_css(dest)?;
+          dest.write_char(' ')?;
+        }
+
+        if self.play_state != AnimationPlayState::default() || AnimationPlayState::parse_string(&name).is_ok() {
+          self.play_state.to_css(dest)?;
+          dest.write_char(' ')?;
+        }
+      }
+    }
+
+    // Eventually we could output a string here to avoid duplicating some properties above.
+    // Chrome does not yet support strings, however.
+    self.name.to_css(dest)?;
+
+    if self.name != AnimationName::None && self.timeline != AnimationTimeline::default() {
+      dest.write_char(' ')?;
+      self.timeline.to_css(dest)?;
+    }
+
+    Ok(())
+  }
+}
+
+/// A list of animations.
+pub type AnimationList<'i> = SmallVec<[Animation<'i>; 1]>;
+
+#[derive(Default)]
+pub(crate) struct AnimationHandler<'i> {
+  names: Option<(SmallVec<[AnimationName<'i>; 1]>, VendorPrefix)>,
+  durations: Option<(SmallVec<[Time; 1]>, VendorPrefix)>,
+  timing_functions: Option<(SmallVec<[EasingFunction; 1]>, VendorPrefix)>,
+  iteration_counts: Option<(SmallVec<[AnimationIterationCount; 1]>, VendorPrefix)>,
+  directions: Option<(SmallVec<[AnimationDirection; 1]>, VendorPrefix)>,
+  play_states: Option<(SmallVec<[AnimationPlayState; 1]>, VendorPrefix)>,
+  delays: Option<(SmallVec<[Time; 1]>, VendorPrefix)>,
+  fill_modes: Option<(SmallVec<[AnimationFillMode; 1]>, VendorPrefix)>,
+  timelines: Option<SmallVec<[AnimationTimeline<'i>; 1]>>,
+  range_starts: Option<SmallVec<[AnimationRangeStart; 1]>>,
+  range_ends: Option<SmallVec<[AnimationRangeEnd; 1]>>,
+  has_any: bool,
+}
+
+impl<'i> PropertyHandler<'i> for AnimationHandler<'i> {
+  fn handle_property(
+    &mut self,
+    property: &Property<'i>,
+    dest: &mut DeclarationList<'i>,
+    context: &mut PropertyHandlerContext<'i, '_>,
+  ) -> bool {
+    macro_rules! maybe_flush {
+      ($prop: ident, $val: expr, $vp: ident) => {{
+        // If two vendor prefixes for the same property have different
+        // values, we need to flush what we have immediately to preserve order.
+        if let Some((val, prefixes)) = &self.$prop {
+          if val != $val && !prefixes.contains(*$vp) {
+            self.flush(dest, context);
+          }
+        }
+      }};
+    }
+
+    macro_rules! property {
+      ($prop: ident, $val: expr, $vp: ident) => {{
+        maybe_flush!($prop, $val, $vp);
+
+        // Otherwise, update the value and add the prefix.
+        if let Some((val, prefixes)) = &mut self.$prop {
+          *val = $val.clone();
+          *prefixes |= *$vp;
+        } else {
+          self.$prop = Some(($val.clone(), *$vp));
+          self.has_any = true;
+        }
+      }};
+    }
+
+    match property {
+      Property::AnimationName(val, vp) => property!(names, val, vp),
+      Property::AnimationDuration(val, vp) => property!(durations, val, vp),
+      Property::AnimationTimingFunction(val, vp) => property!(timing_functions, val, vp),
+      Property::AnimationIterationCount(val, vp) => property!(iteration_counts, val, vp),
+      Property::AnimationDirection(val, vp) => property!(directions, val, vp),
+      Property::AnimationPlayState(val, vp) => property!(play_states, val, vp),
+      Property::AnimationDelay(val, vp) => property!(delays, val, vp),
+      Property::AnimationFillMode(val, vp) => property!(fill_modes, val, vp),
+      Property::AnimationTimeline(val) => {
+        self.timelines = Some(val.clone());
+        self.has_any = true;
+      }
+      Property::AnimationRangeStart(val) => {
+        self.range_starts = Some(val.clone());
+        self.has_any = true;
+      }
+      Property::AnimationRangeEnd(val) => {
+        self.range_ends = Some(val.clone());
+        self.has_any = true;
+      }
+      Property::AnimationRange(val) => {
+        self.range_starts = Some(val.iter().map(|v| v.start.clone()).collect());
+        self.range_ends = Some(val.iter().map(|v| v.end.clone()).collect());
+        self.has_any = true;
+      }
+      Property::Animation(val, vp) => {
+        let names = val.iter().map(|b| b.name.clone()).collect();
+        maybe_flush!(names, &names, vp);
+
+        let durations = val.iter().map(|b| b.duration.clone()).collect();
+        maybe_flush!(durations, &durations, vp);
+
+        let timing_functions = val.iter().map(|b| b.timing_function.clone()).collect();
+        maybe_flush!(timing_functions, &timing_functions, vp);
+
+        let iteration_counts = val.iter().map(|b| b.iteration_count.clone()).collect();
+        maybe_flush!(iteration_counts, &iteration_counts, vp);
+
+        let directions = val.iter().map(|b| b.direction.clone()).collect();
+        maybe_flush!(directions, &directions, vp);
+
+        let play_states = val.iter().map(|b| b.play_state.clone()).collect();
+        maybe_flush!(play_states, &play_states, vp);
+
+        let delays = val.iter().map(|b| b.delay.clone()).collect();
+        maybe_flush!(delays, &delays, vp);
+
+        let fill_modes = val.iter().map(|b| b.fill_mode.clone()).collect();
+        maybe_flush!(fill_modes, &fill_modes, vp);
+
+        self.timelines = Some(val.iter().map(|b| b.timeline.clone()).collect());
+
+        property!(names, &names, vp);
+        property!(durations, &durations, vp);
+        property!(timing_functions, &timing_functions, vp);
+        property!(iteration_counts, &iteration_counts, vp);
+        property!(directions, &directions, vp);
+        property!(play_states, &play_states, vp);
+        property!(delays, &delays, vp);
+        property!(fill_modes, &fill_modes, vp);
+
+        // The animation shorthand resets animation-range
+        // https://drafts.csswg.org/scroll-animations/#named-range-animation-declaration
+        self.range_starts = None;
+        self.range_ends = None;
+      }
+      Property::Unparsed(val) if is_animation_property(&val.property_id) => {
+        let mut val = Cow::Borrowed(val);
+        if matches!(val.property_id, PropertyId::Animation(_)) {
+          use crate::properties::custom::Token;
+
+          // Find an identifier that isn't a keyword and replace it with an
+          // AnimationName token so it is scoped in CSS modules.
+          for token in &mut val.to_mut().value.0 {
+            match token {
+              TokenOrValue::Token(Token::Ident(id)) => {
+                if AnimationDirection::parse_string(&id).is_err()
+                  && AnimationPlayState::parse_string(&id).is_err()
+                  && AnimationFillMode::parse_string(&id).is_err()
+                  && !EasingFunction::is_ident(&id)
+                  && id.as_ref() != "infinite"
+                  && id.as_ref() != "auto"
+                {
+                  *token = TokenOrValue::AnimationName(AnimationName::Ident(CustomIdent(id.clone())));
+                }
+              }
+              TokenOrValue::Token(Token::String(s)) => {
+                *token = TokenOrValue::AnimationName(AnimationName::String(CSSString(s.clone())));
+              }
+              _ => {}
+            }
+          }
+
+          self.range_starts = None;
+          self.range_ends = None;
+        }
+
+        self.flush(dest, context);
+        dest.push(Property::Unparsed(
+          val.get_prefixed(context.targets, Feature::Animation),
+        ));
+      }
+      _ => return false,
+    }
+
+    true
+  }
+
+  fn finalize(&mut self, dest: &mut DeclarationList<'i>, context: &mut PropertyHandlerContext<'i, '_>) {
+    self.flush(dest, context);
+  }
+}
+
+impl<'i> AnimationHandler<'i> {
+  fn flush(&mut self, dest: &mut DeclarationList<'i>, context: &mut PropertyHandlerContext<'i, '_>) {
+    if !self.has_any {
+      return;
+    }
+
+    self.has_any = false;
+
+    let mut names = std::mem::take(&mut self.names);
+    let mut durations = std::mem::take(&mut self.durations);
+    let mut timing_functions = std::mem::take(&mut self.timing_functions);
+    let mut iteration_counts = std::mem::take(&mut self.iteration_counts);
+    let mut directions = std::mem::take(&mut self.directions);
+    let mut play_states = std::mem::take(&mut self.play_states);
+    let mut delays = std::mem::take(&mut self.delays);
+    let mut fill_modes = std::mem::take(&mut self.fill_modes);
+    let mut timelines_value = std::mem::take(&mut self.timelines);
+    let range_starts = std::mem::take(&mut self.range_starts);
+    let range_ends = std::mem::take(&mut self.range_ends);
+
+    if let (
+      Some((names, names_vp)),
+      Some((durations, durations_vp)),
+      Some((timing_functions, timing_functions_vp)),
+      Some((iteration_counts, iteration_counts_vp)),
+      Some((directions, directions_vp)),
+      Some((play_states, play_states_vp)),
+      Some((delays, delays_vp)),
+      Some((fill_modes, fill_modes_vp)),
+    ) = (
+      &mut names,
+      &mut durations,
+      &mut timing_functions,
+      &mut iteration_counts,
+      &mut directions,
+      &mut play_states,
+      &mut delays,
+      &mut fill_modes,
+    ) {
+      // Only use shorthand syntax if the number of animations matches on all properties.
+      let len = names.len();
+      let intersection = *names_vp
+        & *durations_vp
+        & *timing_functions_vp
+        & *iteration_counts_vp
+        & *directions_vp
+        & *play_states_vp
+        & *delays_vp
+        & *fill_modes_vp;
+      let mut timelines = if let Some(timelines) = &mut timelines_value {
+        Cow::Borrowed(timelines)
+      } else if !intersection.contains(VendorPrefix::None) {
+        // Prefixed animation shorthand does not support animation-timeline
+        Cow::Owned(std::iter::repeat(AnimationTimeline::Auto).take(len).collect())
+      } else {
+        Cow::Owned(SmallVec::new())
+      };
+
+      if !intersection.is_empty()
+        && durations.len() == len
+        && timing_functions.len() == len
+        && iteration_counts.len() == len
+        && directions.len() == len
+        && play_states.len() == len
+        && delays.len() == len
+        && fill_modes.len() == len
+        && timelines.len() == len
+      {
+        let timeline_property = if timelines.iter().any(|t| *t != AnimationTimeline::Auto)
+          && (intersection != VendorPrefix::None
+            || !context
+              .targets
+              .is_compatible(crate::compat::Feature::AnimationTimelineShorthand))
+        {
+          Some(Property::AnimationTimeline(timelines.clone().into_owned()))
+        } else {
+          None
+        };
+
+        let animations = izip!(
+          names.drain(..),
+          durations.drain(..),
+          timing_functions.drain(..),
+          iteration_counts.drain(..),
+          directions.drain(..),
+          play_states.drain(..),
+          delays.drain(..),
+          fill_modes.drain(..),
+          timelines.to_mut().drain(..)
+        )
+        .map(
+          |(
+            name,
+            duration,
+            timing_function,
+            iteration_count,
+            direction,
+            play_state,
+            delay,
+            fill_mode,
+            timeline,
+          )| {
+            Animation {
+              name,
+              duration,
+              timing_function,
+              iteration_count,
+              direction,
+              play_state,
+              delay,
+              fill_mode,
+              timeline: if timeline_property.is_some() {
+                AnimationTimeline::Auto
+              } else {
+                timeline
+              },
+            }
+          },
+        )
+        .collect();
+        let prefix = context.targets.prefixes(intersection, Feature::Animation);
+        dest.push(Property::Animation(animations, prefix));
+        names_vp.remove(intersection);
+        durations_vp.remove(intersection);
+        timing_functions_vp.remove(intersection);
+        iteration_counts_vp.remove(intersection);
+        directions_vp.remove(intersection);
+        play_states_vp.remove(intersection);
+        delays_vp.remove(intersection);
+        fill_modes_vp.remove(intersection);
+
+        if let Some(p) = timeline_property {
+          dest.push(p);
+        }
+        timelines_value = None;
+      }
+    }
+
+    macro_rules! prop {
+      ($var: ident, $property: ident) => {
+        if let Some((val, vp)) = $var {
+          if !vp.is_empty() {
+            let prefix = context.targets.prefixes(vp, Feature::$property);
+            dest.push(Property::$property(val, prefix))
+          }
+        }
+      };
+    }
+
+    prop!(names, AnimationName);
+    prop!(durations, AnimationDuration);
+    prop!(timing_functions, AnimationTimingFunction);
+    prop!(iteration_counts, AnimationIterationCount);
+    prop!(directions, AnimationDirection);
+    prop!(play_states, AnimationPlayState);
+    prop!(delays, AnimationDelay);
+    prop!(fill_modes, AnimationFillMode);
+
+    if let Some(val) = timelines_value {
+      dest.push(Property::AnimationTimeline(val));
+    }
+
+    match (range_starts, range_ends) {
+      (Some(range_starts), Some(range_ends)) => {
+        if range_starts.len() == range_ends.len() {
+          dest.push(Property::AnimationRange(
+            range_starts
+              .into_iter()
+              .zip(range_ends.into_iter())
+              .map(|(start, end)| AnimationRange { start, end })
+              .collect(),
+          ));
+        } else {
+          dest.push(Property::AnimationRangeStart(range_starts));
+          dest.push(Property::AnimationRangeEnd(range_ends));
+        }
+      }
+      (range_starts, range_ends) => {
+        if let Some(range_starts) = range_starts {
+          dest.push(Property::AnimationRangeStart(range_starts));
+        }
+
+        if let Some(range_ends) = range_ends {
+          dest.push(Property::AnimationRangeEnd(range_ends));
+        }
+      }
+    }
+  }
+}
+
+#[inline]
+fn is_animation_property(property_id: &PropertyId) -> bool {
+  match property_id {
+    PropertyId::AnimationName(_)
+    | PropertyId::AnimationDuration(_)
+    | PropertyId::AnimationTimingFunction(_)
+    | PropertyId::AnimationIterationCount(_)
+    | PropertyId::AnimationDirection(_)
+    | PropertyId::AnimationPlayState(_)
+    | PropertyId::AnimationDelay(_)
+    | PropertyId::AnimationFillMode(_)
+    | PropertyId::AnimationComposition
+    | PropertyId::AnimationTimeline
+    | PropertyId::AnimationRange
+    | PropertyId::AnimationRangeStart
+    | PropertyId::AnimationRangeEnd
+    | PropertyId::Animation(_) => true,
+    _ => false,
+  }
+}
diff --git a/src/properties/background.rs b/src/properties/background.rs
new file mode 100644
index 0000000..f47821d
--- /dev/null
+++ b/src/properties/background.rs
@@ -0,0 +1,1145 @@
+//! CSS properties related to backgrounds.
+
+use crate::context::PropertyHandlerContext;
+use crate::declaration::{DeclarationBlock, DeclarationList};
+use crate::error::{ParserError, PrinterError};
+use crate::macros::*;
+use crate::prefixes::Feature;
+use crate::printer::Printer;
+use crate::properties::{Property, PropertyId, VendorPrefix};
+use crate::targets::{Browsers, Targets};
+use crate::traits::{FallbackValues, IsCompatible, Parse, PropertyHandler, Shorthand, ToCss};
+use crate::values::color::ColorFallbackKind;
+use crate::values::image::ImageFallback;
+use crate::values::{color::CssColor, image::Image, length::LengthPercentageOrAuto, position::*};
+#[cfg(feature = "visitor")]
+use crate::visitor::Visit;
+use cssparser::*;
+use itertools::izip;
+use smallvec::SmallVec;
+
+/// A value for the [background-size](https://www.w3.org/TR/css-backgrounds-3/#background-size) property.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum BackgroundSize {
+  /// An explicit background size.
+  Explicit {
+    /// The width of the background.
+    width: LengthPercentageOrAuto,
+    /// The height of the background.
+    height: LengthPercentageOrAuto,
+  },
+  /// The `cover` keyword. Scales the background image to cover both the width and height of the element.
+  Cover,
+  /// The `contain` keyword. Scales the background image so that it fits within the element.
+  Contain,
+}
+
+impl Default for BackgroundSize {
+  fn default() -> BackgroundSize {
+    BackgroundSize::Explicit {
+      width: LengthPercentageOrAuto::Auto,
+      height: LengthPercentageOrAuto::Auto,
+    }
+  }
+}
+
+impl<'i> Parse<'i> for BackgroundSize {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    if let Ok(width) = input.try_parse(LengthPercentageOrAuto::parse) {
+      let height = input
+        .try_parse(LengthPercentageOrAuto::parse)
+        .unwrap_or(LengthPercentageOrAuto::Auto);
+      return Ok(BackgroundSize::Explicit { width, height });
+    }
+
+    let location = input.current_source_location();
+    let ident = input.expect_ident()?;
+    Ok(match_ignore_ascii_case! { ident,
+      "cover" => BackgroundSize::Cover,
+      "contain" => BackgroundSize::Contain,
+      _ => return Err(location.new_unexpected_token_error(
+        cssparser::Token::Ident(ident.clone())
+      ))
+    })
+  }
+}
+
+impl ToCss for BackgroundSize {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    use BackgroundSize::*;
+
+    match &self {
+      Cover => dest.write_str("cover"),
+      Contain => dest.write_str("contain"),
+      Explicit { width, height } => {
+        width.to_css(dest)?;
+        if *height != LengthPercentageOrAuto::Auto {
+          dest.write_str(" ")?;
+          height.to_css(dest)?;
+        }
+        Ok(())
+      }
+    }
+  }
+}
+
+impl IsCompatible for BackgroundSize {
+  fn is_compatible(&self, browsers: Browsers) -> bool {
+    match self {
+      BackgroundSize::Explicit { width, height } => {
+        width.is_compatible(browsers) && height.is_compatible(browsers)
+      }
+      BackgroundSize::Cover | BackgroundSize::Contain => true,
+    }
+  }
+}
+
+enum_property! {
+  /// A [`<repeat-style>`](https://www.w3.org/TR/css-backgrounds-3/#typedef-repeat-style) value,
+  /// used within the `background-repeat` property to represent how a background image is repeated
+  /// in a single direction.
+  ///
+  /// See [BackgroundRepeat](BackgroundRepeat).
+  pub enum BackgroundRepeatKeyword {
+    /// The image is repeated in this direction.
+    Repeat,
+    /// The image is repeated so that it fits, and then spaced apart evenly.
+    Space,
+    /// The image is scaled so that it repeats an even number of times.
+    Round,
+    /// The image is placed once and not repeated in this direction.
+    NoRepeat,
+  }
+}
+
+/// A value for the [background-repeat](https://www.w3.org/TR/css-backgrounds-3/#background-repeat) property.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub struct BackgroundRepeat {
+  /// A repeat style for the x direction.
+  pub x: BackgroundRepeatKeyword,
+  /// A repeat style for the y direction.
+  pub y: BackgroundRepeatKeyword,
+}
+
+impl Default for BackgroundRepeat {
+  fn default() -> BackgroundRepeat {
+    BackgroundRepeat {
+      x: BackgroundRepeatKeyword::Repeat,
+      y: BackgroundRepeatKeyword::Repeat,
+    }
+  }
+}
+
+impl<'i> Parse<'i> for BackgroundRepeat {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    use BackgroundRepeatKeyword::*;
+    let state = input.state();
+    let ident = input.expect_ident()?;
+
+    match_ignore_ascii_case! { ident,
+      "repeat-x" => return Ok(BackgroundRepeat { x: Repeat, y: NoRepeat }),
+      "repeat-y" => return Ok(BackgroundRepeat { x: NoRepeat, y: Repeat }),
+      _ => {}
+    }
+
+    input.reset(&state);
+
+    let x = BackgroundRepeatKeyword::parse(input)?;
+    let y = input.try_parse(BackgroundRepeatKeyword::parse).unwrap_or(x.clone());
+    Ok(BackgroundRepeat { x, y })
+  }
+}
+
+impl ToCss for BackgroundRepeat {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    use BackgroundRepeatKeyword::*;
+    match (&self.x, &self.y) {
+      (Repeat, NoRepeat) => dest.write_str("repeat-x"),
+      (NoRepeat, Repeat) => dest.write_str("repeat-y"),
+      (x, y) => {
+        x.to_css(dest)?;
+        if y != x {
+          dest.write_str(" ")?;
+          y.to_css(dest)?;
+        }
+        Ok(())
+      }
+    }
+  }
+}
+
+impl IsCompatible for BackgroundRepeat {
+  fn is_compatible(&self, _browsers: Browsers) -> bool {
+    true
+  }
+}
+
+enum_property! {
+  /// A value for the [background-attachment](https://www.w3.org/TR/css-backgrounds-3/#background-attachment) property.
+  pub enum BackgroundAttachment {
+    /// The background scrolls with the container.
+    Scroll,
+    /// The background is fixed to the viewport.
+    Fixed,
+    /// The background is fixed with regard to the element’s contents.
+    Local,
+  }
+}
+
+impl Default for BackgroundAttachment {
+  fn default() -> BackgroundAttachment {
+    BackgroundAttachment::Scroll
+  }
+}
+
+enum_property! {
+  /// A value for the [background-origin](https://www.w3.org/TR/css-backgrounds-3/#background-origin) property.
+  pub enum BackgroundOrigin {
+    /// The position is relative to the border box.
+    BorderBox,
+    /// The position is relative to the padding box.
+    PaddingBox,
+    /// The position is relative to the content box.
+    ContentBox,
+  }
+}
+
+enum_property! {
+  /// A value for the [background-clip](https://drafts.csswg.org/css-backgrounds-4/#background-clip) property.
+  pub enum BackgroundClip {
+    /// The background is clipped to the border box.
+    BorderBox,
+    /// The background is clipped to the padding box.
+    PaddingBox,
+    /// The background is clipped to the content box.
+    ContentBox,
+    /// The background is clipped to the area painted by the border.
+    Border,
+    /// The background is clipped to the text content of the element.
+    Text,
+  }
+}
+
+impl PartialEq<BackgroundOrigin> for BackgroundClip {
+  fn eq(&self, other: &BackgroundOrigin) -> bool {
+    match (self, other) {
+      (BackgroundClip::BorderBox, BackgroundOrigin::BorderBox)
+      | (BackgroundClip::PaddingBox, BackgroundOrigin::PaddingBox)
+      | (BackgroundClip::ContentBox, BackgroundOrigin::ContentBox) => true,
+      _ => false,
+    }
+  }
+}
+
+impl Into<BackgroundClip> for BackgroundOrigin {
+  fn into(self) -> BackgroundClip {
+    match self {
+      BackgroundOrigin::BorderBox => BackgroundClip::BorderBox,
+      BackgroundOrigin::PaddingBox => BackgroundClip::PaddingBox,
+      BackgroundOrigin::ContentBox => BackgroundClip::ContentBox,
+    }
+  }
+}
+
+impl Default for BackgroundClip {
+  fn default() -> BackgroundClip {
+    BackgroundClip::BorderBox
+  }
+}
+
+impl BackgroundClip {
+  fn is_background_box(&self) -> bool {
+    matches!(
+      self,
+      BackgroundClip::BorderBox | BackgroundClip::PaddingBox | BackgroundClip::ContentBox
+    )
+  }
+}
+
+define_list_shorthand! {
+  /// A value for the [background-position](https://drafts.csswg.org/css-backgrounds/#background-position) shorthand property.
+  pub struct BackgroundPosition {
+    /// The x-position.
+    x: BackgroundPositionX(HorizontalPosition),
+    /// The y-position.
+    y: BackgroundPositionY(VerticalPosition),
+  }
+}
+
+impl From<Position> for BackgroundPosition {
+  fn from(pos: Position) -> Self {
+    BackgroundPosition { x: pos.x, y: pos.y }
+  }
+}
+
+impl Into<Position> for &BackgroundPosition {
+  fn into(self) -> Position {
+    Position {
+      x: self.x.clone(),
+      y: self.y.clone(),
+    }
+  }
+}
+
+impl Default for BackgroundPosition {
+  fn default() -> Self {
+    Position::default().into()
+  }
+}
+
+impl<'i> Parse<'i> for BackgroundPosition {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let pos = Position::parse(input)?;
+    Ok(pos.into())
+  }
+}
+
+impl ToCss for BackgroundPosition {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    let pos: Position = self.into();
+    pos.to_css(dest)
+  }
+}
+
+/// A value for the [background](https://www.w3.org/TR/css-backgrounds-3/#background) shorthand property.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub struct Background<'i> {
+  /// The background image.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  pub image: Image<'i>,
+  /// The background color.
+  pub color: CssColor,
+  /// The background position.
+  pub position: BackgroundPosition,
+  /// How the background image should repeat.
+  pub repeat: BackgroundRepeat,
+  /// The size of the background image.
+  pub size: BackgroundSize,
+  /// The background attachment.
+  pub attachment: BackgroundAttachment,
+  /// The background origin.
+  pub origin: BackgroundOrigin,
+  /// How the background should be clipped.
+  pub clip: BackgroundClip,
+}
+
+impl<'i> Parse<'i> for Background<'i> {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let mut color: Option<CssColor> = None;
+    let mut position: Option<BackgroundPosition> = None;
+    let mut size: Option<BackgroundSize> = None;
+    let mut image: Option<Image> = None;
+    let mut repeat: Option<BackgroundRepeat> = None;
+    let mut attachment: Option<BackgroundAttachment> = None;
+    let mut origin: Option<BackgroundOrigin> = None;
+    let mut clip: Option<BackgroundClip> = None;
+
+    loop {
+      // TODO: only allowed on the last background.
+      if color.is_none() {
+        if let Ok(value) = input.try_parse(CssColor::parse) {
+          color = Some(value);
+          continue;
+        }
+      }
+
+      if position.is_none() {
+        if let Ok(value) = input.try_parse(BackgroundPosition::parse) {
+          position = Some(value);
+
+          size = input
+            .try_parse(|input| {
+              input.expect_delim('/')?;
+              BackgroundSize::parse(input)
+            })
+            .ok();
+
+          continue;
+        }
+      }
+
+      if image.is_none() {
+        if let Ok(value) = input.try_parse(Image::parse) {
+          image = Some(value);
+          continue;
+        }
+      }
+
+      if repeat.is_none() {
+        if let Ok(value) = input.try_parse(BackgroundRepeat::parse) {
+          repeat = Some(value);
+          continue;
+        }
+      }
+
+      if attachment.is_none() {
+        if let Ok(value) = input.try_parse(BackgroundAttachment::parse) {
+          attachment = Some(value);
+          continue;
+        }
+      }
+
+      if origin.is_none() {
+        if let Ok(value) = input.try_parse(BackgroundOrigin::parse) {
+          origin = Some(value);
+          continue;
+        }
+      }
+
+      if clip.is_none() {
+        if let Ok(value) = input.try_parse(BackgroundClip::parse) {
+          clip = Some(value);
+          continue;
+        }
+      }
+
+      break;
+    }
+
+    if clip.is_none() {
+      if let Some(origin) = origin {
+        clip = Some(origin.into());
+      }
+    }
+
+    Ok(Background {
+      image: image.unwrap_or_default(),
+      color: color.unwrap_or_default(),
+      position: position.unwrap_or_default(),
+      repeat: repeat.unwrap_or_default(),
+      size: size.unwrap_or_default(),
+      attachment: attachment.unwrap_or_default(),
+      origin: origin.unwrap_or(BackgroundOrigin::PaddingBox),
+      clip: clip.unwrap_or(BackgroundClip::BorderBox),
+    })
+  }
+}
+
+impl<'i> ToCss for Background<'i> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    let mut has_output = false;
+
+    if self.color != CssColor::default() {
+      self.color.to_css(dest)?;
+      has_output = true;
+    }
+
+    if self.image != Image::default() {
+      if has_output {
+        dest.write_str(" ")?;
+      }
+      self.image.to_css(dest)?;
+      has_output = true;
+    }
+
+    let position: Position = (&self.position).into();
+    if !position.is_zero() || self.size != BackgroundSize::default() {
+      if has_output {
+        dest.write_str(" ")?;
+      }
+      position.to_css(dest)?;
+
+      if self.size != BackgroundSize::default() {
+        dest.delim('/', true)?;
+        self.size.to_css(dest)?;
+      }
+
+      has_output = true;
+    }
+
+    if self.repeat != BackgroundRepeat::default() {
+      if has_output {
+        dest.write_str(" ")?;
+      }
+
+      self.repeat.to_css(dest)?;
+      has_output = true;
+    }
+
+    if self.attachment != BackgroundAttachment::default() {
+      if has_output {
+        dest.write_str(" ")?;
+      }
+
+      self.attachment.to_css(dest)?;
+      has_output = true;
+    }
+
+    let output_padding_box = self.origin != BackgroundOrigin::PaddingBox
+      || (self.clip != BackgroundOrigin::BorderBox && self.clip.is_background_box());
+    if output_padding_box {
+      if has_output {
+        dest.write_str(" ")?;
+      }
+
+      self.origin.to_css(dest)?;
+      has_output = true;
+    }
+
+    if (output_padding_box && self.clip != self.origin) || self.clip != BackgroundOrigin::BorderBox {
+      if has_output {
+        dest.write_str(" ")?;
+      }
+
+      self.clip.to_css(dest)?;
+      has_output = true;
+    }
+
+    // If nothing was output, then this is the initial value, e.g. background: transparent
+    if !has_output {
+      if dest.minify {
+        // `0 0` is the shortest valid background value
+        self.position.to_css(dest)?;
+      } else {
+        dest.write_str("none")?;
+      }
+    }
+
+    Ok(())
+  }
+}
+
+impl<'i> ImageFallback<'i> for Background<'i> {
+  #[inline]
+  fn get_image(&self) -> &Image<'i> {
+    &self.image
+  }
+
+  #[inline]
+  fn with_image(&self, image: Image<'i>) -> Self {
+    Background { image, ..self.clone() }
+  }
+
+  #[inline]
+  fn get_necessary_fallbacks(&self, targets: Targets) -> ColorFallbackKind {
+    self.color.get_necessary_fallbacks(targets) | self.get_image().get_necessary_fallbacks(targets)
+  }
+
+  #[inline]
+  fn get_fallback(&self, kind: ColorFallbackKind) -> Self {
+    Background {
+      color: self.color.get_fallback(kind),
+      image: self.image.get_fallback(kind),
+      ..self.clone()
+    }
+  }
+}
+
+impl<'i> Shorthand<'i> for SmallVec<[Background<'i>; 1]> {
+  fn from_longhands(decls: &DeclarationBlock<'i>, vendor_prefix: VendorPrefix) -> Option<(Self, bool)> {
+    let mut color = None;
+    let mut images = None;
+    let mut x_positions = None;
+    let mut y_positions = None;
+    let mut repeats = None;
+    let mut sizes = None;
+    let mut attachments = None;
+    let mut origins = None;
+    let mut clips = None;
+
+    let mut count = 0;
+    let mut important_count = 0;
+    let mut length = None;
+    for (property, important) in decls.iter() {
+      let len = match property {
+        Property::BackgroundColor(value) => {
+          color = Some(value.clone());
+          count += 1;
+          if important {
+            important_count += 1;
+          }
+          continue;
+        }
+        Property::BackgroundImage(value) => {
+          images = Some(value.clone());
+          value.len()
+        }
+        Property::BackgroundPosition(value) => {
+          x_positions = Some(value.iter().map(|v| v.x.clone()).collect());
+          y_positions = Some(value.iter().map(|v| v.y.clone()).collect());
+          value.len()
+        }
+        Property::BackgroundPositionX(value) => {
+          x_positions = Some(value.clone());
+          value.len()
+        }
+        Property::BackgroundPositionY(value) => {
+          y_positions = Some(value.clone());
+          value.len()
+        }
+        Property::BackgroundRepeat(value) => {
+          repeats = Some(value.clone());
+          value.len()
+        }
+        Property::BackgroundSize(value) => {
+          sizes = Some(value.clone());
+          value.len()
+        }
+        Property::BackgroundAttachment(value) => {
+          attachments = Some(value.clone());
+          value.len()
+        }
+        Property::BackgroundOrigin(value) => {
+          origins = Some(value.clone());
+          value.len()
+        }
+        Property::BackgroundClip(value, vp) => {
+          if *vp != vendor_prefix {
+            return None;
+          }
+          clips = Some(value.clone());
+          value.len()
+        }
+        Property::Background(val) => {
+          color = Some(val.last().unwrap().color.clone());
+          images = Some(val.iter().map(|b| b.image.clone()).collect());
+          x_positions = Some(val.iter().map(|b| b.position.x.clone()).collect());
+          y_positions = Some(val.iter().map(|b| b.position.y.clone()).collect());
+          repeats = Some(val.iter().map(|b| b.repeat.clone()).collect());
+          sizes = Some(val.iter().map(|b| b.size.clone()).collect());
+          attachments = Some(val.iter().map(|b| b.attachment.clone()).collect());
+          origins = Some(val.iter().map(|b| b.origin.clone()).collect());
+          clips = Some(val.iter().map(|b| b.clip.clone()).collect());
+          val.len()
+        }
+        _ => continue,
+      };
+
+      // Lengths must be equal.
+      if length.is_none() {
+        length = Some(len);
+      } else if length.unwrap() != len {
+        return None;
+      }
+
+      count += 1;
+      if important {
+        important_count += 1;
+      }
+    }
+
+    // !important flags must match to produce a shorthand.
+    if important_count > 0 && important_count != count {
+      return None;
+    }
+
+    if color.is_some()
+      && images.is_some()
+      && x_positions.is_some()
+      && y_positions.is_some()
+      && repeats.is_some()
+      && sizes.is_some()
+      && attachments.is_some()
+      && origins.is_some()
+      && clips.is_some()
+    {
+      let length = length.unwrap();
+      let values = izip!(
+        images.unwrap().drain(..),
+        x_positions.unwrap().drain(..),
+        y_positions.unwrap().drain(..),
+        repeats.unwrap().drain(..),
+        sizes.unwrap().drain(..),
+        attachments.unwrap().drain(..),
+        origins.unwrap().drain(..),
+        clips.unwrap().drain(..),
+      )
+      .enumerate()
+      .map(
+        |(i, (image, x_position, y_position, repeat, size, attachment, origin, clip))| Background {
+          color: if i == length - 1 {
+            color.clone().unwrap()
+          } else {
+            CssColor::default()
+          },
+          image,
+          position: BackgroundPosition {
+            x: x_position,
+            y: y_position,
+          },
+          repeat,
+          size,
+          attachment,
+          origin,
+          clip: clip,
+        },
+      )
+      .collect();
+      return Some((values, important_count > 0));
+    }
+
+    None
+  }
+
+  fn longhands(vendor_prefix: VendorPrefix) -> Vec<PropertyId<'static>> {
+    vec![
+      PropertyId::BackgroundColor,
+      PropertyId::BackgroundImage,
+      PropertyId::BackgroundPositionX,
+      PropertyId::BackgroundPositionY,
+      PropertyId::BackgroundRepeat,
+      PropertyId::BackgroundSize,
+      PropertyId::BackgroundAttachment,
+      PropertyId::BackgroundOrigin,
+      PropertyId::BackgroundClip(vendor_prefix),
+    ]
+  }
+
+  fn longhand(&self, property_id: &PropertyId) -> Option<Property<'i>> {
+    match property_id {
+      PropertyId::BackgroundColor => Some(Property::BackgroundColor(self.last().unwrap().color.clone())),
+      PropertyId::BackgroundImage => Some(Property::BackgroundImage(
+        self.iter().map(|v| v.image.clone()).collect(),
+      )),
+      PropertyId::BackgroundPositionX => Some(Property::BackgroundPositionX(
+        self.iter().map(|v| v.position.x.clone()).collect(),
+      )),
+      PropertyId::BackgroundPositionY => Some(Property::BackgroundPositionY(
+        self.iter().map(|v| v.position.y.clone()).collect(),
+      )),
+      PropertyId::BackgroundRepeat => Some(Property::BackgroundRepeat(
+        self.iter().map(|v| v.repeat.clone()).collect(),
+      )),
+      PropertyId::BackgroundSize => Some(Property::BackgroundSize(self.iter().map(|v| v.size.clone()).collect())),
+      PropertyId::BackgroundAttachment => Some(Property::BackgroundAttachment(
+        self.iter().map(|v| v.attachment.clone()).collect(),
+      )),
+      PropertyId::BackgroundOrigin => Some(Property::BackgroundOrigin(
+        self.iter().map(|v| v.origin.clone()).collect(),
+      )),
+      PropertyId::BackgroundClip(vp) => Some(Property::BackgroundClip(
+        self.iter().map(|v| v.clip.clone()).collect(),
+        *vp,
+      )),
+      _ => None,
+    }
+  }
+
+  fn set_longhand(&mut self, property: &Property<'i>) -> Result<(), ()> {
+    macro_rules! longhand {
+      ($value: ident, $key: ident $(.$k: ident)*) => {{
+        if $value.len() != self.len() {
+          return Err(());
+        }
+        for (i, item) in self.iter_mut().enumerate() {
+          item.$key$(.$k)* = $value[i].clone();
+        }
+      }};
+    }
+
+    match property {
+      Property::BackgroundColor(value) => self.last_mut().unwrap().color = value.clone(),
+      Property::BackgroundImage(value) => longhand!(value, image),
+      Property::BackgroundPositionX(value) => longhand!(value, position.x),
+      Property::BackgroundPositionY(value) => longhand!(value, position.y),
+      Property::BackgroundPosition(value) => longhand!(value, position),
+      Property::BackgroundRepeat(value) => longhand!(value, repeat),
+      Property::BackgroundSize(value) => longhand!(value, size),
+      Property::BackgroundAttachment(value) => longhand!(value, attachment),
+      Property::BackgroundOrigin(value) => longhand!(value, origin),
+      Property::BackgroundClip(value, _vp) => longhand!(value, clip),
+      _ => return Err(()),
+    }
+
+    Ok(())
+  }
+}
+
+property_bitflags! {
+  #[derive(Default)]
+  struct BackgroundProperty: u16 {
+    const BackgroundColor = 1 << 0;
+    const BackgroundImage = 1 << 1;
+    const BackgroundPositionX = 1 << 2;
+    const BackgroundPositionY = 1 << 3;
+    const BackgroundPosition = Self::BackgroundPositionX.bits() | Self::BackgroundPositionY.bits();
+    const BackgroundRepeat = 1 << 4;
+    const BackgroundSize = 1 << 5;
+    const BackgroundAttachment = 1 << 6;
+    const BackgroundOrigin = 1 << 7;
+    const BackgroundClip(_vp) = 1 << 8;
+    const Background = Self::BackgroundColor.bits() | Self::BackgroundImage.bits() | Self::BackgroundPosition.bits() | Self::BackgroundRepeat.bits() | Self::BackgroundSize.bits() | Self::BackgroundAttachment.bits() | Self::BackgroundOrigin.bits() | Self::BackgroundClip.bits();
+  }
+}
+
+#[derive(Default)]
+pub(crate) struct BackgroundHandler<'i> {
+  color: Option<CssColor>,
+  images: Option<SmallVec<[Image<'i>; 1]>>,
+  has_prefix: bool,
+  x_positions: Option<SmallVec<[HorizontalPosition; 1]>>,
+  y_positions: Option<SmallVec<[VerticalPosition; 1]>>,
+  repeats: Option<SmallVec<[BackgroundRepeat; 1]>>,
+  sizes: Option<SmallVec<[BackgroundSize; 1]>>,
+  attachments: Option<SmallVec<[BackgroundAttachment; 1]>>,
+  origins: Option<SmallVec<[BackgroundOrigin; 1]>>,
+  clips: Option<(SmallVec<[BackgroundClip; 1]>, VendorPrefix)>,
+  decls: Vec<Property<'i>>,
+  flushed_properties: BackgroundProperty,
+  has_any: bool,
+}
+
+impl<'i> PropertyHandler<'i> for BackgroundHandler<'i> {
+  fn handle_property(
+    &mut self,
+    property: &Property<'i>,
+    dest: &mut DeclarationList<'i>,
+    context: &mut PropertyHandlerContext<'i, '_>,
+  ) -> bool {
+    macro_rules! background_image {
+      ($val: expr) => {
+        flush!(images, $val);
+
+        // Store prefixed properties. Clear if we hit an unprefixed property and we have
+        // targets. In this case, the necessary prefixes will be generated.
+        self.has_prefix = $val.iter().any(|x| x.has_vendor_prefix());
+        if self.has_prefix {
+          self.decls.push(property.clone())
+        } else if context.targets.browsers.is_some() {
+          self.decls.clear();
+        }
+      };
+    }
+
+    macro_rules! flush {
+      ($key: ident, $val: expr) => {{
+        if self.$key.is_some() && self.$key.as_ref().unwrap() != $val && matches!(context.targets.browsers, Some(targets) if !$val.is_compatible(targets)) {
+          self.flush(dest, context);
+        }
+      }};
+    }
+
+    match &property {
+      Property::BackgroundColor(val) => {
+        flush!(color, val);
+        self.color = Some(val.clone());
+      }
+      Property::BackgroundImage(val) => {
+        background_image!(val);
+        self.images = Some(val.clone())
+      }
+      Property::BackgroundPosition(val) => {
+        self.x_positions = Some(val.iter().map(|p| p.x.clone()).collect());
+        self.y_positions = Some(val.iter().map(|p| p.y.clone()).collect());
+      }
+      Property::BackgroundPositionX(val) => self.x_positions = Some(val.clone()),
+      Property::BackgroundPositionY(val) => self.y_positions = Some(val.clone()),
+      Property::BackgroundRepeat(val) => self.repeats = Some(val.clone()),
+      Property::BackgroundSize(val) => self.sizes = Some(val.clone()),
+      Property::BackgroundAttachment(val) => self.attachments = Some(val.clone()),
+      Property::BackgroundOrigin(val) => self.origins = Some(val.clone()),
+      Property::BackgroundClip(val, vendor_prefix) => {
+        if let Some((clips, vp)) = &mut self.clips {
+          if vendor_prefix != vp && val != clips {
+            self.flush(dest, context);
+            self.clips = Some((val.clone(), *vendor_prefix))
+          } else {
+            if val != clips {
+              *clips = val.clone();
+            }
+            *vp |= *vendor_prefix;
+          }
+        } else {
+          self.clips = Some((val.clone(), *vendor_prefix))
+        }
+      }
+      Property::Background(val) => {
+        let images: SmallVec<[Image; 1]> = val.iter().map(|b| b.image.clone()).collect();
+        background_image!(&images);
+        let color = val.last().unwrap().color.clone();
+        flush!(color, &color);
+        let clips = val.iter().map(|b| b.clip.clone()).collect();
+        let mut clips_vp = VendorPrefix::None;
+        if let Some((cur_clips, cur_clips_vp)) = &mut self.clips {
+          if clips_vp != *cur_clips_vp && *cur_clips != clips {
+            self.flush(dest, context);
+          } else {
+            clips_vp |= *cur_clips_vp;
+          }
+        }
+        self.color = Some(color);
+        self.images = Some(images);
+        self.x_positions = Some(val.iter().map(|b| b.position.x.clone()).collect());
+        self.y_positions = Some(val.iter().map(|b| b.position.y.clone()).collect());
+        self.repeats = Some(val.iter().map(|b| b.repeat.clone()).collect());
+        self.sizes = Some(val.iter().map(|b| b.size.clone()).collect());
+        self.attachments = Some(val.iter().map(|b| b.attachment.clone()).collect());
+        self.origins = Some(val.iter().map(|b| b.origin.clone()).collect());
+        self.clips = Some((clips, clips_vp));
+      }
+      Property::Unparsed(val) if is_background_property(&val.property_id) => {
+        self.flush(dest, context);
+        let mut unparsed = val.clone();
+        context.add_unparsed_fallbacks(&mut unparsed);
+        self
+          .flushed_properties
+          .insert(BackgroundProperty::try_from(&unparsed.property_id).unwrap());
+        dest.push(Property::Unparsed(unparsed));
+      }
+      _ => return false,
+    }
+
+    self.has_any = true;
+    true
+  }
+
+  fn finalize(&mut self, dest: &mut DeclarationList<'i>, context: &mut PropertyHandlerContext<'i, '_>) {
+    // If the last declaration is prefixed, pop the last value
+    // so it isn't duplicated when we flush.
+    if self.has_prefix {
+      self.decls.pop();
+    }
+
+    dest.extend(self.decls.drain(..));
+    self.flush(dest, context);
+    self.flushed_properties = BackgroundProperty::empty();
+  }
+}
+
+impl<'i> BackgroundHandler<'i> {
+  fn flush(&mut self, dest: &mut DeclarationList<'i>, context: &mut PropertyHandlerContext<'i, '_>) {
+    if !self.has_any {
+      return;
+    }
+
+    self.has_any = false;
+
+    macro_rules! push {
+      ($prop: ident, $val: expr) => {
+        dest.push(Property::$prop($val));
+        self.flushed_properties.insert(BackgroundProperty::$prop);
+      };
+    }
+
+    let color = std::mem::take(&mut self.color);
+    let mut images = std::mem::take(&mut self.images);
+    let mut x_positions = std::mem::take(&mut self.x_positions);
+    let mut y_positions = std::mem::take(&mut self.y_positions);
+    let mut repeats = std::mem::take(&mut self.repeats);
+    let mut sizes = std::mem::take(&mut self.sizes);
+    let mut attachments = std::mem::take(&mut self.attachments);
+    let mut origins = std::mem::take(&mut self.origins);
+    let mut clips = std::mem::take(&mut self.clips);
+
+    if let (
+      Some(color),
+      Some(images),
+      Some(x_positions),
+      Some(y_positions),
+      Some(repeats),
+      Some(sizes),
+      Some(attachments),
+      Some(origins),
+      Some(clips),
+    ) = (
+      &color,
+      &mut images,
+      &mut x_positions,
+      &mut y_positions,
+      &mut repeats,
+      &mut sizes,
+      &mut attachments,
+      &mut origins,
+      &mut clips,
+    ) {
+      // Only use shorthand syntax if the number of layers matches on all properties.
+      let len = images.len();
+      if x_positions.len() == len
+        && y_positions.len() == len
+        && repeats.len() == len
+        && sizes.len() == len
+        && attachments.len() == len
+        && origins.len() == len
+        && clips.0.len() == len
+      {
+        let clip_prefixes = if clips.0.iter().any(|clip| *clip == BackgroundClip::Text) {
+          context.targets.prefixes(clips.1, Feature::BackgroundClip)
+        } else {
+          clips.1
+        };
+
+        let clip_property = if clip_prefixes != VendorPrefix::None {
+          Some(Property::BackgroundClip(clips.0.clone(), clip_prefixes))
+        } else {
+          None
+        };
+
+        let mut backgrounds: SmallVec<[Background<'i>; 1]> = izip!(
+          images.drain(..),
+          x_positions.drain(..),
+          y_positions.drain(..),
+          repeats.drain(..),
+          sizes.drain(..),
+          attachments.drain(..),
+          origins.drain(..),
+          clips.0.drain(..)
+        )
+        .enumerate()
+        .map(
+          |(i, (image, x_position, y_position, repeat, size, attachment, origin, clip))| Background {
+            color: if i == len - 1 {
+              color.clone()
+            } else {
+              CssColor::default()
+            },
+            image,
+            position: BackgroundPosition {
+              x: x_position,
+              y: y_position,
+            },
+            repeat,
+            size,
+            attachment,
+            origin,
+            clip: if clip_prefixes == VendorPrefix::None {
+              clip
+            } else {
+              BackgroundClip::default()
+            },
+          },
+        )
+        .collect();
+
+        if !self.flushed_properties.intersects(BackgroundProperty::Background) {
+          for fallback in backgrounds.get_fallbacks(context.targets) {
+            push!(Background, fallback);
+          }
+        }
+
+        push!(Background, backgrounds);
+
+        if let Some(clip) = clip_property {
+          dest.push(clip);
+          self.flushed_properties.insert(BackgroundProperty::BackgroundClip);
+        }
+
+        self.reset();
+        return;
+      }
+    }
+
+    if let Some(mut color) = color {
+      if !self.flushed_properties.contains(BackgroundProperty::BackgroundColor) {
+        for fallback in color.get_fallbacks(context.targets) {
+          push!(BackgroundColor, fallback);
+        }
+      }
+
+      push!(BackgroundColor, color);
+    }
+
+    if let Some(mut images) = images {
+      if !self.flushed_properties.contains(BackgroundProperty::BackgroundImage) {
+        for fallback in images.get_fallbacks(context.targets) {
+          push!(BackgroundImage, fallback);
+        }
+      }
+
+      push!(BackgroundImage, images);
+    }
+
+    match (&mut x_positions, &mut y_positions) {
+      (Some(x_positions), Some(y_positions)) if x_positions.len() == y_positions.len() => {
+        let positions = izip!(x_positions.drain(..), y_positions.drain(..))
+          .map(|(x, y)| BackgroundPosition { x, y })
+          .collect();
+        push!(BackgroundPosition, positions);
+      }
+      _ => {
+        if let Some(x_positions) = x_positions {
+          push!(BackgroundPositionX, x_positions);
+        }
+
+        if let Some(y_positions) = y_positions {
+          push!(BackgroundPositionY, y_positions);
+        }
+      }
+    }
+
+    if let Some(repeats) = repeats {
+      push!(BackgroundRepeat, repeats);
+    }
+
+    if let Some(sizes) = sizes {
+      push!(BackgroundSize, sizes);
+    }
+
+    if let Some(attachments) = attachments {
+      push!(BackgroundAttachment, attachments);
+    }
+
+    if let Some(origins) = origins {
+      push!(BackgroundOrigin, origins);
+    }
+
+    if let Some((clips, vp)) = clips {
+      let prefixes = if clips.iter().any(|clip| *clip == BackgroundClip::Text) {
+        context.targets.prefixes(vp, Feature::BackgroundClip)
+      } else {
+        vp
+      };
+      dest.push(Property::BackgroundClip(clips, prefixes));
+      self.flushed_properties.insert(BackgroundProperty::BackgroundClip);
+    }
+
+    self.reset();
+  }
+
+  fn reset(&mut self) {
+    self.color = None;
+    self.images = None;
+    self.x_positions = None;
+    self.y_positions = None;
+    self.repeats = None;
+    self.sizes = None;
+    self.attachments = None;
+    self.origins = None;
+    self.clips = None
+  }
+}
+
+#[inline]
+fn is_background_property(property_id: &PropertyId) -> bool {
+  match property_id {
+    PropertyId::BackgroundColor
+    | PropertyId::BackgroundImage
+    | PropertyId::BackgroundPosition
+    | PropertyId::BackgroundPositionX
+    | PropertyId::BackgroundPositionY
+    | PropertyId::BackgroundRepeat
+    | PropertyId::BackgroundSize
+    | PropertyId::BackgroundAttachment
+    | PropertyId::BackgroundOrigin
+    | PropertyId::BackgroundClip(_)
+    | PropertyId::Background => true,
+    _ => false,
+  }
+}
diff --git a/src/properties/border.rs b/src/properties/border.rs
new file mode 100644
index 0000000..9308eeb
--- /dev/null
+++ b/src/properties/border.rs
@@ -0,0 +1,1419 @@
+//! CSS properties related to borders.
+
+use super::border_image::*;
+use super::border_radius::*;
+use crate::compat::Feature;
+use crate::context::PropertyHandlerContext;
+use crate::declaration::{DeclarationBlock, DeclarationList};
+use crate::error::{ParserError, PrinterError};
+use crate::logical::PropertyCategory;
+use crate::macros::*;
+use crate::printer::Printer;
+use crate::properties::custom::UnparsedProperty;
+use crate::properties::{Property, PropertyId};
+use crate::targets::Browsers;
+use crate::targets::Targets;
+use crate::traits::{FallbackValues, IsCompatible, Parse, PropertyHandler, Shorthand, ToCss};
+use crate::values::color::{ColorFallbackKind, CssColor};
+use crate::values::length::*;
+use crate::values::rect::Rect;
+use crate::values::size::Size2D;
+#[cfg(feature = "visitor")]
+use crate::visitor::Visit;
+use cssparser::*;
+
+/// A value for the [border-width](https://www.w3.org/TR/css-backgrounds-3/#border-width) property.
+#[derive(Debug, Clone, PartialEq, Parse, ToCss)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", content = "value", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum BorderSideWidth {
+  /// A UA defined `thin` value.
+  Thin,
+  /// A UA defined `medium` value.
+  Medium,
+  /// A UA defined `thick` value.
+  Thick,
+  /// An explicit width.
+  Length(Length),
+}
+
+impl Default for BorderSideWidth {
+  fn default() -> BorderSideWidth {
+    BorderSideWidth::Medium
+  }
+}
+
+impl IsCompatible for BorderSideWidth {
+  fn is_compatible(&self, browsers: Browsers) -> bool {
+    match self {
+      BorderSideWidth::Length(length) => length.is_compatible(browsers),
+      _ => true,
+    }
+  }
+}
+
+enum_property! {
+  /// A [`<line-style>`](https://drafts.csswg.org/css-backgrounds/#typedef-line-style) value, used in the `border-style` property.
+  pub enum LineStyle {
+    /// No border.
+    None,
+    /// Similar to `none` but with different rules for tables.
+    Hidden,
+    /// Looks as if the content on the inside of the border is sunken into the canvas.
+    Inset,
+    /// Looks as if it were carved in the canvas.
+    Groove,
+    /// Looks as if the content on the inside of the border is coming out of the canvas.
+    Outset,
+    /// Looks as if it were coming out of the canvas.
+    Ridge,
+    /// A series of round dots.
+    Dotted,
+    /// A series of square-ended dashes.
+    Dashed,
+    /// A single line segment.
+    Solid,
+    /// Two parallel solid lines with some space between them.
+    Double,
+  }
+}
+
+impl Default for LineStyle {
+  fn default() -> LineStyle {
+    LineStyle::None
+  }
+}
+
+impl IsCompatible for LineStyle {
+  fn is_compatible(&self, _browsers: Browsers) -> bool {
+    true
+  }
+}
+
+/// A generic type that represents the `border` and `outline` shorthand properties.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub struct GenericBorder<S, const P: u8> {
+  /// The width of the border.
+  pub width: BorderSideWidth,
+  /// The border style.
+  pub style: S,
+  /// The border color.
+  pub color: CssColor,
+}
+
+#[cfg(feature = "into_owned")]
+impl<'any, S, const P: u8> static_self::IntoOwned<'any> for GenericBorder<S, P>
+where
+  S: static_self::IntoOwned<'any>,
+{
+  type Owned = GenericBorder<S::Owned, P>;
+  fn into_owned(self) -> Self::Owned {
+    GenericBorder {
+      width: self.width,
+      style: self.style.into_owned(),
+      color: self.color,
+    }
+  }
+}
+
+impl<S: Default, const P: u8> Default for GenericBorder<S, P> {
+  fn default() -> GenericBorder<S, P> {
+    GenericBorder {
+      width: BorderSideWidth::Medium,
+      style: S::default(),
+      color: CssColor::current_color(),
+    }
+  }
+}
+
+impl<'i, S: Parse<'i> + Default, const P: u8> Parse<'i> for GenericBorder<S, P> {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    // Order doesn't matter...
+    let mut color = None;
+    let mut style = None;
+    let mut width = None;
+    let mut any = false;
+    loop {
+      if width.is_none() {
+        if let Ok(value) = input.try_parse(|i| BorderSideWidth::parse(i)) {
+          width = Some(value);
+          any = true;
+        }
+      }
+      if style.is_none() {
+        if let Ok(value) = input.try_parse(S::parse) {
+          style = Some(value);
+          any = true;
+          continue;
+        }
+      }
+      if color.is_none() {
+        if let Ok(value) = input.try_parse(|i| CssColor::parse(i)) {
+          color = Some(value);
+          any = true;
+          continue;
+        }
+      }
+      break;
+    }
+    if any {
+      Ok(GenericBorder {
+        width: width.unwrap_or(BorderSideWidth::Medium),
+        style: style.unwrap_or_default(),
+        color: color.unwrap_or_else(|| CssColor::current_color()),
+      })
+    } else {
+      Err(input.new_custom_error(ParserError::InvalidDeclaration))
+    }
+  }
+}
+
+impl<S: ToCss + Default + PartialEq, const P: u8> ToCss for GenericBorder<S, P> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    if *self == Self::default() {
+      self.style.to_css(dest)?;
+      return Ok(());
+    }
+
+    let mut needs_space = false;
+    if self.width != BorderSideWidth::default() {
+      self.width.to_css(dest)?;
+      needs_space = true;
+    }
+    if self.style != S::default() {
+      if needs_space {
+        dest.write_str(" ")?;
+      }
+      self.style.to_css(dest)?;
+      needs_space = true;
+    }
+    if self.color != CssColor::current_color() {
+      if needs_space {
+        dest.write_str(" ")?;
+      }
+      self.color.to_css(dest)?;
+    }
+    Ok(())
+  }
+}
+
+impl<S: Clone, const P: u8> FallbackValues for GenericBorder<S, P> {
+  fn get_fallbacks(&mut self, targets: Targets) -> Vec<Self> {
+    self
+      .color
+      .get_fallbacks(targets)
+      .into_iter()
+      .map(|color| GenericBorder {
+        color,
+        width: self.width.clone(),
+        style: self.style.clone(),
+      })
+      .collect()
+  }
+}
+
+/// A value for the [border-top](https://www.w3.org/TR/css-backgrounds-3/#propdef-border-top) shorthand property.
+pub type BorderTop = GenericBorder<LineStyle, 0>;
+/// A value for the [border-right](https://www.w3.org/TR/css-backgrounds-3/#propdef-border-right) shorthand property.
+pub type BorderRight = GenericBorder<LineStyle, 1>;
+/// A value for the [border-bottom](https://www.w3.org/TR/css-backgrounds-3/#propdef-border-bottom) shorthand property.
+pub type BorderBottom = GenericBorder<LineStyle, 2>;
+/// A value for the [border-left](https://www.w3.org/TR/css-backgrounds-3/#propdef-border-left) shorthand property.
+pub type BorderLeft = GenericBorder<LineStyle, 3>;
+/// A value for the [border-block-start](https://drafts.csswg.org/css-logical/#propdef-border-block-start) shorthand property.
+pub type BorderBlockStart = GenericBorder<LineStyle, 4>;
+/// A value for the [border-block-end](https://drafts.csswg.org/css-logical/#propdef-border-block-end) shorthand property.
+pub type BorderBlockEnd = GenericBorder<LineStyle, 5>;
+/// A value for the [border-inline-start](https://drafts.csswg.org/css-logical/#propdef-border-inline-start) shorthand property.
+pub type BorderInlineStart = GenericBorder<LineStyle, 6>;
+/// A value for the [border-inline-end](https://drafts.csswg.org/css-logical/#propdef-border-inline-end) shorthand property.
+pub type BorderInlineEnd = GenericBorder<LineStyle, 7>;
+/// A value for the [border-block](https://drafts.csswg.org/css-logical/#propdef-border-block) shorthand property.
+pub type BorderBlock = GenericBorder<LineStyle, 8>;
+/// A value for the [border-inline](https://drafts.csswg.org/css-logical/#propdef-border-inline) shorthand property.
+pub type BorderInline = GenericBorder<LineStyle, 9>;
+/// A value for the [border](https://www.w3.org/TR/css-backgrounds-3/#propdef-border) shorthand property.
+pub type Border = GenericBorder<LineStyle, 10>;
+
+impl_shorthand! {
+  BorderTop(BorderTop) {
+    width: [BorderTopWidth],
+    style: [BorderTopStyle],
+    color: [BorderTopColor],
+  }
+}
+
+impl_shorthand! {
+  BorderRight(BorderRight) {
+    width: [BorderRightWidth],
+    style: [BorderRightStyle],
+    color: [BorderRightColor],
+  }
+}
+
+impl_shorthand! {
+  BorderBottom(BorderBottom) {
+    width: [BorderBottomWidth],
+    style: [BorderBottomStyle],
+    color: [BorderBottomColor],
+  }
+}
+
+impl_shorthand! {
+  BorderLeft(BorderLeft) {
+    width: [BorderLeftWidth],
+    style: [BorderLeftStyle],
+    color: [BorderLeftColor],
+  }
+}
+
+impl_shorthand! {
+  BorderBlockStart(BorderBlockStart) {
+    width: [BorderBlockStartWidth],
+    style: [BorderBlockStartStyle],
+    color: [BorderBlockStartColor],
+  }
+}
+
+impl_shorthand! {
+  BorderBlockEnd(BorderBlockEnd) {
+    width: [BorderBlockEndWidth],
+    style: [BorderBlockEndStyle],
+    color: [BorderBlockEndColor],
+  }
+}
+
+impl_shorthand! {
+  BorderInlineStart(BorderInlineStart) {
+    width: [BorderInlineStartWidth],
+    style: [BorderInlineStartStyle],
+    color: [BorderInlineStartColor],
+  }
+}
+
+impl_shorthand! {
+  BorderInlineEnd(BorderInlineEnd) {
+    width: [BorderInlineEndWidth],
+    style: [BorderInlineEndStyle],
+    color: [BorderInlineEndColor],
+  }
+}
+
+impl_shorthand! {
+  BorderBlock(BorderBlock) {
+    width: [BorderBlockStartWidth, BorderBlockEndWidth],
+    style: [BorderBlockStartStyle, BorderBlockEndStyle],
+    color: [BorderBlockStartColor, BorderBlockEndColor],
+  }
+}
+
+impl_shorthand! {
+  BorderInline(BorderInline) {
+    width: [BorderInlineStartWidth, BorderInlineEndWidth],
+    style: [BorderInlineStartStyle, BorderInlineEndStyle],
+    color: [BorderInlineStartColor, BorderInlineEndColor],
+  }
+}
+
+impl_shorthand! {
+  Border(Border) {
+    width: [BorderTopWidth, BorderRightWidth, BorderBottomWidth, BorderLeftWidth],
+    style: [BorderTopStyle, BorderRightStyle, BorderBottomStyle, BorderLeftStyle],
+    color: [BorderTopColor, BorderRightColor, BorderBottomColor, BorderLeftColor],
+  }
+}
+
+size_shorthand! {
+  /// A value for the [border-block-color](https://drafts.csswg.org/css-logical/#propdef-border-block-color) shorthand property.
+  pub struct BorderBlockColor<CssColor> {
+    /// The block start value.
+    start: BorderBlockStartColor,
+    /// The block end value.
+    end: BorderBlockEndColor,
+  }
+}
+
+size_shorthand! {
+  /// A value for the [border-block-style](https://drafts.csswg.org/css-logical/#propdef-border-block-style) shorthand property.
+  pub struct BorderBlockStyle<LineStyle> {
+    /// The block start value.
+    start: BorderBlockStartStyle,
+    /// The block end value.
+    end: BorderBlockEndStyle,
+  }
+}
+
+size_shorthand! {
+  /// A value for the [border-block-width](https://drafts.csswg.org/css-logical/#propdef-border-block-width) shorthand property.
+  pub struct BorderBlockWidth<BorderSideWidth> {
+    /// The block start value.
+    start: BorderBlockStartWidth,
+    /// The block end value.
+    end: BorderBlockEndWidth,
+  }
+}
+
+size_shorthand! {
+  /// A value for the [border-inline-color](https://drafts.csswg.org/css-logical/#propdef-border-inline-color) shorthand property.
+  pub struct BorderInlineColor<CssColor> {
+    /// The inline start value.
+    start: BorderInlineStartColor,
+    /// The inline end value.
+    end: BorderInlineEndColor,
+  }
+}
+
+size_shorthand! {
+  /// A value for the [border-inline-style](https://drafts.csswg.org/css-logical/#propdef-border-inline-style) shorthand property.
+  pub struct BorderInlineStyle<LineStyle> {
+    /// The inline start value.
+    start: BorderInlineStartStyle,
+    /// The inline end value.
+    end: BorderInlineEndStyle,
+  }
+}
+
+size_shorthand! {
+  /// A value for the [border-inline-width](https://drafts.csswg.org/css-logical/#propdef-border-inline-width) shorthand property.
+  pub struct BorderInlineWidth<BorderSideWidth> {
+    /// The inline start value.
+    start: BorderInlineStartWidth,
+    /// The inline end value.
+    end: BorderInlineEndWidth,
+  }
+}
+
+rect_shorthand! {
+  /// A value for the [border-color](https://drafts.csswg.org/css-backgrounds/#propdef-border-color) shorthand property.
+  pub struct BorderColor<CssColor> {
+    BorderTopColor,
+    BorderRightColor,
+    BorderBottomColor,
+    BorderLeftColor
+  }
+}
+
+rect_shorthand! {
+  /// A value for the [border-style](https://drafts.csswg.org/css-backgrounds/#propdef-border-style) shorthand property.
+  pub struct BorderStyle<LineStyle> {
+    BorderTopStyle,
+    BorderRightStyle,
+    BorderBottomStyle,
+    BorderLeftStyle
+  }
+}
+
+rect_shorthand! {
+  /// A value for the [border-width](https://drafts.csswg.org/css-backgrounds/#propdef-border-width) shorthand property.
+  pub struct BorderWidth<BorderSideWidth> {
+    BorderTopWidth,
+    BorderRightWidth,
+    BorderBottomWidth,
+    BorderLeftWidth
+  }
+}
+
+macro_rules! impl_fallbacks {
+  ($t: ident $(, $name: ident)+) => {
+    impl FallbackValues for $t {
+      fn get_fallbacks(&mut self, targets: Targets) -> Vec<Self> {
+        let mut fallbacks = ColorFallbackKind::empty();
+        $(
+          fallbacks |= self.$name.get_necessary_fallbacks(targets);
+        )+
+
+        let mut res = Vec::new();
+        if fallbacks.contains(ColorFallbackKind::RGB) {
+          res.push($t {
+            $(
+              $name: self.$name.get_fallback(ColorFallbackKind::RGB),
+            )+
+          });
+        }
+
+        if fallbacks.contains(ColorFallbackKind::P3) {
+          res.push($t {
+            $(
+              $name: self.$name.get_fallback(ColorFallbackKind::P3),
+            )+
+          });
+        }
+
+        if fallbacks.contains(ColorFallbackKind::LAB) {
+          $(
+            self.$name = self.$name.get_fallback(ColorFallbackKind::LAB);
+          )+
+        }
+
+        res
+      }
+    }
+  }
+}
+
+impl_fallbacks!(BorderBlockColor, start, end);
+impl_fallbacks!(BorderInlineColor, start, end);
+impl_fallbacks!(BorderColor, top, right, bottom, left);
+
+#[derive(Default, Debug, PartialEq)]
+struct BorderShorthand {
+  pub width: Option<BorderSideWidth>,
+  pub style: Option<LineStyle>,
+  pub color: Option<CssColor>,
+}
+
+impl BorderShorthand {
+  pub fn set_border<const P: u8>(&mut self, border: &GenericBorder<LineStyle, P>) {
+    self.width = Some(border.width.clone());
+    self.style = Some(border.style.clone());
+    self.color = Some(border.color.clone());
+  }
+
+  pub fn is_valid(&self) -> bool {
+    self.width.is_some() && self.style.is_some() && self.color.is_some()
+  }
+
+  pub fn reset(&mut self) {
+    self.width = None;
+    self.style = None;
+    self.color = None;
+  }
+
+  pub fn to_border<const P: u8>(&self) -> GenericBorder<LineStyle, P> {
+    GenericBorder {
+      width: self.width.clone().unwrap(),
+      style: self.style.clone().unwrap(),
+      color: self.color.clone().unwrap(),
+    }
+  }
+}
+
+property_bitflags! {
+  #[derive(Debug, Default)]
+  struct BorderProperty: u32 {
+    const BorderTopColor = 1 << 0;
+    const BorderBottomColor = 1 << 1;
+    const BorderLeftColor = 1 << 2;
+    const BorderRightColor = 1 << 3;
+    const BorderBlockStartColor = 1 << 4;
+    const BorderBlockEndColor = 1 << 5;
+    const BorderBlockColor = Self::BorderBlockStartColor.bits() | Self::BorderBlockEndColor.bits();
+    const BorderInlineStartColor = 1 << 6;
+    const BorderInlineEndColor = 1 << 7;
+    const BorderInlineColor = Self::BorderInlineStartColor.bits() | Self::BorderInlineEndColor.bits();
+    const BorderTopWidth = 1 << 8;
+    const BorderBottomWidth = 1 << 9;
+    const BorderLeftWidth = 1 << 10;
+    const BorderRightWidth = 1 << 11;
+    const BorderBlockStartWidth = 1 << 12;
+    const BorderBlockEndWidth = 1 << 13;
+    const BorderBlockWidth = Self::BorderBlockStartWidth.bits() | Self::BorderBlockEndWidth.bits();
+    const BorderInlineStartWidth = 1 << 14;
+    const BorderInlineEndWidth = 1 << 15;
+    const BorderInlineWidth = Self::BorderInlineStartWidth.bits() | Self::BorderInlineEndWidth.bits();
+    const BorderTopStyle = 1 << 16;
+    const BorderBottomStyle = 1 << 17;
+    const BorderLeftStyle = 1 << 18;
+    const BorderRightStyle = 1 << 19;
+    const BorderBlockStartStyle = 1 << 20;
+    const BorderBlockEndStyle = 1 << 21;
+    const BorderBlockStyle = Self::BorderBlockStartStyle.bits() | Self::BorderBlockEndStyle.bits();
+    const BorderInlineStartStyle = 1 << 22;
+    const BorderInlineEndStyle = 1 << 23;
+    const BorderInlineStyle = Self::BorderInlineStartStyle.bits() | Self::BorderInlineEndStyle.bits();
+    const BorderTop = Self::BorderTopColor.bits() | Self::BorderTopWidth.bits() | Self::BorderTopStyle.bits();
+    const BorderBottom = Self::BorderBottomColor.bits() | Self::BorderBottomWidth.bits() | Self::BorderBottomStyle.bits();
+    const BorderLeft = Self::BorderLeftColor.bits() | Self::BorderLeftWidth.bits() | Self::BorderLeftStyle.bits();
+    const BorderRight = Self::BorderRightColor.bits() | Self::BorderRightWidth.bits() | Self::BorderRightStyle.bits();
+    const BorderBlockStart = Self::BorderBlockStartColor.bits() | Self::BorderBlockStartWidth.bits() | Self::BorderBlockStartStyle.bits();
+    const BorderBlockEnd = Self::BorderBlockEndColor.bits() | Self::BorderBlockEndWidth.bits() | Self::BorderBlockEndStyle.bits();
+    const BorderInlineStart = Self::BorderInlineStartColor.bits() | Self::BorderInlineStartWidth.bits() | Self::BorderInlineStartStyle.bits();
+    const BorderInlineEnd = Self::BorderInlineEndColor.bits() | Self::BorderInlineEndWidth.bits() | Self::BorderInlineEndStyle.bits();
+    const BorderBlock = Self::BorderBlockStart.bits() | Self::BorderBlockEnd.bits();
+    const BorderInline = Self::BorderInlineStart.bits() | Self::BorderInlineEnd.bits();
+    const BorderWidth = Self::BorderLeftWidth.bits() | Self::BorderRightWidth.bits() | Self::BorderTopWidth.bits() | Self::BorderBottomWidth.bits();
+    const BorderStyle = Self::BorderLeftStyle.bits() | Self::BorderRightStyle.bits() | Self::BorderTopStyle.bits() | Self::BorderBottomStyle.bits();
+    const BorderColor = Self::BorderLeftColor.bits() | Self::BorderRightColor.bits() | Self::BorderTopColor.bits() | Self::BorderBottomColor.bits();
+    const Border = Self::BorderWidth.bits() | Self::BorderStyle.bits() | Self::BorderColor.bits();
+  }
+}
+
+#[derive(Debug, Default)]
+pub(crate) struct BorderHandler<'i> {
+  border_top: BorderShorthand,
+  border_bottom: BorderShorthand,
+  border_left: BorderShorthand,
+  border_right: BorderShorthand,
+  border_block_start: BorderShorthand,
+  border_block_end: BorderShorthand,
+  border_inline_start: BorderShorthand,
+  border_inline_end: BorderShorthand,
+  category: PropertyCategory,
+  border_image_handler: BorderImageHandler<'i>,
+  border_radius_handler: BorderRadiusHandler<'i>,
+  flushed_properties: BorderProperty,
+  has_any: bool,
+}
+
+impl<'i> PropertyHandler<'i> for BorderHandler<'i> {
+  fn handle_property(
+    &mut self,
+    property: &Property<'i>,
+    dest: &mut DeclarationList<'i>,
+    context: &mut PropertyHandlerContext<'i, '_>,
+  ) -> bool {
+    use Property::*;
+
+    macro_rules! flush {
+      ($key: ident, $prop: ident, $val: expr, $category: ident) => {{
+        if PropertyCategory::$category != self.category {
+          self.flush(dest, context);
+        }
+
+        if self.$key.$prop.is_some() && self.$key.$prop.as_ref().unwrap() != $val && matches!(context.targets.browsers, Some(targets) if !$val.is_compatible(targets)) {
+          self.flush(dest, context);
+        }
+      }};
+    }
+
+    macro_rules! property {
+      ($key: ident, $prop: ident, $val: expr, $category: ident) => {{
+        flush!($key, $prop, $val, $category);
+        self.$key.$prop = Some($val.clone());
+        self.category = PropertyCategory::$category;
+        self.has_any = true;
+      }};
+    }
+
+    macro_rules! set_border {
+      ($key: ident, $val: ident, $category: ident) => {{
+        if PropertyCategory::$category != self.category {
+          self.flush(dest, context);
+        }
+        self.$key.set_border($val);
+        self.category = PropertyCategory::$category;
+        self.has_any = true;
+      }};
+    }
+
+    match &property {
+      BorderTopColor(val) => property!(border_top, color, val, Physical),
+      BorderBottomColor(val) => property!(border_bottom, color, val, Physical),
+      BorderLeftColor(val) => property!(border_left, color, val, Physical),
+      BorderRightColor(val) => property!(border_right, color, val, Physical),
+      BorderBlockStartColor(val) => property!(border_block_start, color, val, Logical),
+      BorderBlockEndColor(val) => property!(border_block_end, color, val, Logical),
+      BorderBlockColor(val) => {
+        property!(border_block_start, color, &val.start, Logical);
+        property!(border_block_end, color, &val.end, Logical);
+      }
+      BorderInlineStartColor(val) => property!(border_inline_start, color, val, Logical),
+      BorderInlineEndColor(val) => property!(border_inline_end, color, val, Logical),
+      BorderInlineColor(val) => {
+        property!(border_inline_start, color, &val.start, Logical);
+        property!(border_inline_end, color, &val.end, Logical);
+      }
+      BorderTopWidth(val) => property!(border_top, width, val, Physical),
+      BorderBottomWidth(val) => property!(border_bottom, width, val, Physical),
+      BorderLeftWidth(val) => property!(border_left, width, val, Physical),
+      BorderRightWidth(val) => property!(border_right, width, val, Physical),
+      BorderBlockStartWidth(val) => property!(border_block_start, width, val, Logical),
+      BorderBlockEndWidth(val) => property!(border_block_end, width, val, Logical),
+      BorderBlockWidth(val) => {
+        property!(border_block_start, width, &val.start, Logical);
+        property!(border_block_end, width, &val.end, Logical);
+      }
+      BorderInlineStartWidth(val) => property!(border_inline_start, width, val, Logical),
+      BorderInlineEndWidth(val) => property!(border_inline_end, width, val, Logical),
+      BorderInlineWidth(val) => {
+        property!(border_inline_start, width, &val.start, Logical);
+        property!(border_inline_end, width, &val.end, Logical);
+      }
+      BorderTopStyle(val) => property!(border_top, style, val, Physical),
+      BorderBottomStyle(val) => property!(border_bottom, style, val, Physical),
+      BorderLeftStyle(val) => property!(border_left, style, val, Physical),
+      BorderRightStyle(val) => property!(border_right, style, val, Physical),
+      BorderBlockStartStyle(val) => property!(border_block_start, style, val, Logical),
+      BorderBlockEndStyle(val) => property!(border_block_end, style, val, Logical),
+      BorderBlockStyle(val) => {
+        property!(border_block_start, style, &val.start, Logical);
+        property!(border_block_end, style, &val.end, Logical);
+      }
+      BorderInlineStartStyle(val) => property!(border_inline_start, style, val, Logical),
+      BorderInlineEndStyle(val) => property!(border_inline_end, style, val, Logical),
+      BorderInlineStyle(val) => {
+        property!(border_inline_start, style, &val.start, Logical);
+        property!(border_inline_end, style, &val.end, Logical);
+      }
+      BorderTop(val) => set_border!(border_top, val, Physical),
+      BorderBottom(val) => set_border!(border_bottom, val, Physical),
+      BorderLeft(val) => set_border!(border_left, val, Physical),
+      BorderRight(val) => set_border!(border_right, val, Physical),
+      BorderBlockStart(val) => set_border!(border_block_start, val, Logical),
+      BorderBlockEnd(val) => set_border!(border_block_end, val, Logical),
+      BorderInlineStart(val) => set_border!(border_inline_start, val, Logical),
+      BorderInlineEnd(val) => set_border!(border_inline_end, val, Logical),
+      BorderBlock(val) => {
+        set_border!(border_block_start, val, Logical);
+        set_border!(border_block_end, val, Logical);
+      }
+      BorderInline(val) => {
+        set_border!(border_inline_start, val, Logical);
+        set_border!(border_inline_end, val, Logical);
+      }
+      BorderWidth(val) => {
+        property!(border_top, width, &val.top, Physical);
+        property!(border_right, width, &val.right, Physical);
+        property!(border_bottom, width, &val.bottom, Physical);
+        property!(border_left, width, &val.left, Physical);
+        self.border_block_start.width = None;
+        self.border_block_end.width = None;
+        self.border_inline_start.width = None;
+        self.border_inline_end.width = None;
+        self.has_any = true;
+      }
+      BorderStyle(val) => {
+        property!(border_top, style, &val.top, Physical);
+        property!(border_right, style, &val.right, Physical);
+        property!(border_bottom, style, &val.bottom, Physical);
+        property!(border_left, style, &val.left, Physical);
+        self.border_block_start.style = None;
+        self.border_block_end.style = None;
+        self.border_inline_start.style = None;
+        self.border_inline_end.style = None;
+        self.has_any = true;
+      }
+      BorderColor(val) => {
+        property!(border_top, color, &val.top, Physical);
+        property!(border_right, color, &val.right, Physical);
+        property!(border_bottom, color, &val.bottom, Physical);
+        property!(border_left, color, &val.left, Physical);
+        self.border_block_start.color = None;
+        self.border_block_end.color = None;
+        self.border_inline_start.color = None;
+        self.border_inline_end.color = None;
+        self.has_any = true;
+      }
+      Border(val) => {
+        // dest.clear();
+        self.border_top.set_border(val);
+        self.border_bottom.set_border(val);
+        self.border_left.set_border(val);
+        self.border_right.set_border(val);
+        self.border_block_start.reset();
+        self.border_block_end.reset();
+        self.border_inline_start.reset();
+        self.border_inline_end.reset();
+
+        // Setting the `border` property resets `border-image`.
+        self.border_image_handler.reset();
+        self.has_any = true;
+      }
+      Unparsed(val) if is_border_property(&val.property_id) => {
+        self.flush(dest, context);
+        self.flush_unparsed(&val, dest, context);
+      }
+      _ => {
+        if self.border_image_handler.will_flush(property) {
+          self.flush(dest, context);
+        }
+
+        return self.border_image_handler.handle_property(property, dest, context)
+          || self.border_radius_handler.handle_property(property, dest, context);
+      }
+    }
+
+    true
+  }
+
+  fn finalize(&mut self, dest: &mut DeclarationList<'i>, context: &mut PropertyHandlerContext<'i, '_>) {
+    self.flush(dest, context);
+    self.flushed_properties = BorderProperty::empty();
+    self.border_image_handler.finalize(dest, context);
+    self.border_radius_handler.finalize(dest, context);
+  }
+}
+
+impl<'i> BorderHandler<'i> {
+  fn flush(&mut self, dest: &mut DeclarationList, context: &mut PropertyHandlerContext<'i, '_>) {
+    if !self.has_any {
+      return;
+    }
+
+    self.has_any = false;
+
+    let logical_supported = !context.should_compile_logical(Feature::LogicalBorders);
+    let logical_shorthand_supported = !context.should_compile_logical(Feature::LogicalBorderShorthand);
+    macro_rules! logical_prop {
+      ($ltr: ident, $ltr_key: ident, $rtl: ident, $rtl_key: ident, $val: expr) => {{
+        context.add_logical_rule(Property::$ltr($val.clone()), Property::$rtl($val.clone()));
+      }};
+    }
+
+    macro_rules! push {
+      ($prop: ident, $val: expr) => {{
+        self.flushed_properties.insert(BorderProperty::$prop);
+        dest.push(Property::$prop($val));
+      }};
+    }
+
+    macro_rules! fallbacks {
+      ($prop: ident => $val: expr) => {{
+        let mut val = $val;
+        if !self.flushed_properties.contains(BorderProperty::$prop) {
+          let fallbacks = val.get_fallbacks(context.targets);
+          for fallback in fallbacks {
+            dest.push(Property::$prop(fallback))
+          }
+        }
+        push!($prop, val);
+      }};
+    }
+
+    macro_rules! prop {
+      (BorderInlineStart => $val: expr) => {
+        if logical_supported {
+          fallbacks!(BorderInlineStart => $val);
+        } else {
+          logical_prop!(BorderLeft, border_left, BorderRight, border_right, $val);
+        }
+      };
+      (BorderInlineStartWidth => $val: expr) => {
+        if logical_supported {
+          push!(BorderInlineStartWidth, $val);
+        } else {
+          logical_prop!(BorderLeftWidth, border_left_width, BorderRightWidth, border_right_width, $val);
+        }
+      };
+      (BorderInlineStartColor => $val: expr) => {
+        if logical_supported {
+          fallbacks!(BorderInlineStartColor => $val);
+        } else {
+          logical_prop!(BorderLeftColor, border_left_color, BorderRightColor, border_right_color, $val);
+        }
+      };
+      (BorderInlineStartStyle => $val: expr) => {
+        if logical_supported {
+          push!(BorderInlineStartStyle, $val);
+        } else {
+          logical_prop!(BorderLeftStyle, border_left_style, BorderRightStyle, border_right_style, $val);
+        }
+      };
+      (BorderInlineEnd => $val: expr) => {
+        if logical_supported {
+          fallbacks!(BorderInlineEnd => $val);
+        } else {
+          logical_prop!(BorderRight, border_right, BorderLeft, border_left, $val);
+        }
+      };
+      (BorderInlineEndWidth => $val: expr) => {
+        if logical_supported {
+          push!(BorderInlineEndWidth, $val);
+        } else {
+          logical_prop!(BorderRightWidth, border_right_width, BorderLeftWidth, border_left_width, $val);
+        }
+      };
+      (BorderInlineEndColor => $val: expr) => {
+        if logical_supported {
+          fallbacks!(BorderInlineEndColor => $val);
+        } else {
+          logical_prop!(BorderRightColor, border_right_color, BorderLeftColor, border_left_color, $val);
+        }
+      };
+      (BorderInlineEndStyle => $val: expr) => {
+        if logical_supported {
+          push!(BorderInlineEndStyle, $val);
+        } else {
+          logical_prop!(BorderRightStyle, border_right_style, BorderLeftStyle, border_left_style, $val);
+        }
+      };
+      (BorderBlockStart => $val: expr) => {
+        if logical_supported {
+          fallbacks!(BorderBlockStart => $val);
+        } else {
+          fallbacks!(BorderTop => $val);
+        }
+      };
+      (BorderBlockStartWidth => $val: expr) => {
+        if logical_supported {
+          push!(BorderBlockStartWidth, $val);
+        } else {
+          push!(BorderTopWidth, $val);
+        }
+      };
+      (BorderBlockStartColor => $val: expr) => {
+        if logical_supported {
+          fallbacks!(BorderBlockStartColor => $val);
+        } else {
+          fallbacks!(BorderTopColor => $val);
+        }
+      };
+      (BorderBlockStartStyle => $val: expr) => {
+        if logical_supported {
+          push!(BorderBlockStartStyle, $val);
+        } else {
+          push!(BorderTopStyle, $val);
+        }
+      };
+      (BorderBlockEnd => $val: expr) => {
+        if logical_supported {
+          fallbacks!(BorderBlockEnd => $val);
+        } else {
+          fallbacks!(BorderBottom => $val);
+        }
+      };
+      (BorderBlockEndWidth => $val: expr) => {
+        if logical_supported {
+          push!(BorderBlockEndWidth, $val);
+        } else {
+          push!(BorderBottomWidth, $val);
+        }
+      };
+      (BorderBlockEndColor => $val: expr) => {
+        if logical_supported {
+          fallbacks!(BorderBlockEndColor => $val);
+        } else {
+          fallbacks!(BorderBottomColor => $val);
+        }
+      };
+      (BorderBlockEndStyle => $val: expr) => {
+        if logical_supported {
+          push!(BorderBlockEndStyle, $val);
+        } else {
+          push!(BorderBottomStyle, $val);
+        }
+      };
+      (BorderLeftColor => $val: expr) => {
+        fallbacks!(BorderLeftColor => $val);
+      };
+      (BorderRightColor => $val: expr) => {
+        fallbacks!(BorderRightColor => $val);
+      };
+      (BorderTopColor => $val: expr) => {
+        fallbacks!(BorderTopColor => $val);
+      };
+      (BorderBottomColor => $val: expr) => {
+        fallbacks!(BorderBottomColor => $val);
+      };
+      (BorderColor => $val: expr) => {
+        fallbacks!(BorderColor => $val);
+      };
+      (BorderBlockColor => $val: expr) => {
+        fallbacks!(BorderBlockColor => $val);
+      };
+      (BorderInlineColor => $val: expr) => {
+        fallbacks!(BorderInlineColor => $val);
+      };
+      (BorderLeft => $val: expr) => {
+        fallbacks!(BorderLeft => $val);
+      };
+      (BorderRight => $val: expr) => {
+        fallbacks!(BorderRight => $val);
+      };
+      (BorderTop => $val: expr) => {
+        fallbacks!(BorderTop => $val);
+      };
+      (BorderBottom => $val: expr) => {
+        fallbacks!(BorderBottom => $val);
+      };
+      (BorderBlockStart => $val: expr) => {
+        fallbacks!(BorderBlockStart => $val);
+      };
+      (BorderBlockEnd => $val: expr) => {
+        fallbacks!(BorderBlockEnd => $val);
+      };
+      (BorderInlineStart => $val: expr) => {
+        fallbacks!(BorderInlineStart => $val);
+      };
+      (BorderInlineEnd => $val: expr) => {
+        fallbacks!(BorderInlineEnd => $val);
+      };
+      (BorderInline => $val: expr) => {
+        fallbacks!(BorderInline => $val);
+      };
+      (BorderBlock => $val: expr) => {
+        fallbacks!(BorderBlock => $val);
+      };
+      (Border => $val: expr) => {
+        fallbacks!(Border => $val);
+      };
+      ($prop: ident => $val: expr) => {
+        push!($prop, $val);
+      };
+    }
+
+    macro_rules! flush_category {
+      (
+        $block_start_prop: ident,
+        $block_start_width: ident,
+        $block_start_style: ident,
+        $block_start_color: ident,
+        $block_start: expr,
+        $block_end_prop: ident,
+        $block_end_width: ident,
+        $block_end_style: ident,
+        $block_end_color: ident,
+        $block_end: expr,
+        $inline_start_prop: ident,
+        $inline_start_width: ident,
+        $inline_start_style: ident,
+        $inline_start_color: ident,
+        $inline_start: expr,
+        $inline_end_prop: ident,
+        $inline_end_width: ident,
+        $inline_end_style: ident,
+        $inline_end_color: ident,
+        $inline_end: expr,
+        $is_logical: expr
+      ) => {
+        macro_rules! shorthand {
+          ($prop: ident, $key: ident) => {{
+            let has_prop = $block_start.$key.is_some() && $block_end.$key.is_some() && $inline_start.$key.is_some() && $inline_end.$key.is_some();
+            if has_prop {
+              if !$is_logical || ($block_start.$key == $block_end.$key && $block_end.$key == $inline_start.$key && $inline_start.$key == $inline_end.$key) {
+                let rect = $prop {
+                  top: std::mem::take(&mut $block_start.$key).unwrap(),
+                  right: std::mem::take(&mut $inline_end.$key).unwrap(),
+                  bottom: std::mem::take(&mut $block_end.$key).unwrap(),
+                  left: std::mem::take(&mut $inline_start.$key).unwrap()
+                };
+                prop!($prop => rect);
+              }
+            }
+          }};
+        }
+
+        macro_rules! logical_shorthand {
+          ($prop: ident, $key: ident, $start: expr, $end: expr) => {{
+            let has_prop = $start.$key.is_some() && $end.$key.is_some();
+            if has_prop {
+              prop!($prop => $prop {
+                start: std::mem::take(&mut $start.$key).unwrap(),
+                end: std::mem::take(&mut $end.$key).unwrap(),
+              });
+              $end.$key = None;
+            }
+            has_prop
+          }};
+        }
+
+        if $block_start.is_valid() && $block_end.is_valid() && $inline_start.is_valid() && $inline_end.is_valid() {
+          let top_eq_bottom = $block_start == $block_end;
+          let left_eq_right = $inline_start == $inline_end;
+          let top_eq_left = $block_start == $inline_start;
+          let top_eq_right = $block_start == $inline_end;
+          let bottom_eq_left = $block_end == $inline_start;
+          let bottom_eq_right = $block_end == $inline_end;
+
+          macro_rules! is_eq {
+            ($key: ident) => {
+              $block_start.$key == $block_end.$key &&
+              $inline_start.$key == $inline_end.$key &&
+              $inline_start.$key == $block_start.$key
+            };
+          }
+
+          macro_rules! prop_diff {
+            ($border: expr, $fallback: expr, $border_fallback: literal) => {
+              if !$is_logical && is_eq!(color) && is_eq!(style) {
+                prop!(Border => $border.to_border());
+                shorthand!(BorderWidth, width);
+              } else if !$is_logical && is_eq!(width) && is_eq!(style) {
+                prop!(Border => $border.to_border());
+                shorthand!(BorderColor, color);
+              } else if !$is_logical && is_eq!(width) && is_eq!(color) {
+                prop!(Border => $border.to_border());
+                shorthand!(BorderStyle, style);
+              } else {
+                if $border_fallback {
+                  prop!(Border => $border.to_border());
+                }
+                $fallback
+              }
+            };
+          }
+
+          macro_rules! side_diff {
+            ($border: expr, $other: expr, $prop: ident, $width: ident, $style: ident, $color: ident) => {
+              let eq_width = $border.width == $other.width;
+              let eq_style = $border.style == $other.style;
+              let eq_color = $border.color == $other.color;
+
+              // If only one of the sub-properties is different, only emit that.
+              // Otherwise, emit the full border value.
+              if eq_width && eq_style {
+                prop!($color => $other.color.clone().unwrap());
+              } else if eq_width && eq_color {
+                prop!($style => $other.style.clone().unwrap());
+              } else if eq_style && eq_color {
+                prop!($width => $other.width.clone().unwrap());
+              } else {
+                prop!($prop => $other.to_border());
+              }
+            };
+          }
+
+          if top_eq_bottom && top_eq_left && top_eq_right {
+            prop!(Border => $block_start.to_border());
+          } else if top_eq_bottom && top_eq_left {
+            prop!(Border => $block_start.to_border());
+            side_diff!($block_start, $inline_end, $inline_end_prop, $inline_end_width, $inline_end_style, $inline_end_color);
+          } else if top_eq_bottom && top_eq_right {
+            prop!(Border => $block_start.to_border());
+            side_diff!($block_start, $inline_start, $inline_start_prop, $inline_start_width, $inline_start_style, $inline_start_color);
+          } else if left_eq_right && bottom_eq_left {
+            prop!(Border => $inline_start.to_border());
+            side_diff!($inline_start, $block_start, $block_start_prop, $block_start_width, $block_start_style, $block_start_color);
+          } else if left_eq_right && top_eq_left {
+            prop!(Border => $inline_start.to_border());
+            side_diff!($inline_start, $block_end, $block_end_prop, $block_end_width, $block_end_style, $block_end_color);
+          } else if top_eq_bottom {
+            prop_diff!($block_start, {
+              // Try to use border-inline shorthands for the opposide direction if possible.
+              let mut handled = false;
+              if $is_logical {
+                let mut diff = 0;
+                if $inline_start.width != $block_start.width || $inline_end.width != $block_start.width {
+                  diff += 1;
+                }
+                if $inline_start.style != $block_start.style || $inline_end.style != $block_start.style {
+                  diff += 1;
+                }
+                if $inline_start.color != $block_start.color || $inline_end.color != $block_start.color {
+                  diff += 1;
+                }
+
+                if diff == 1 {
+                  if $inline_start.width != $block_start.width {
+                    prop!(BorderInlineWidth => BorderInlineWidth {
+                      start: $inline_start.width.clone().unwrap(),
+                      end: $inline_end.width.clone().unwrap(),
+                    });
+                    handled = true;
+                  } else if $inline_start.style != $block_start.style {
+                    prop!(BorderInlineStyle => BorderInlineStyle {
+                      start: $inline_start.style.clone().unwrap(),
+                      end: $inline_end.style.clone().unwrap()
+                    });
+                    handled = true;
+                  } else if $inline_start.color != $block_start.color {
+                    prop!(BorderInlineColor => BorderInlineColor {
+                      start: $inline_start.color.clone().unwrap(),
+                      end: $inline_end.color.clone().unwrap()
+                    });
+                    handled = true;
+                  }
+                } else if diff > 1 && $inline_start.width == $inline_end.width && $inline_start.style == $inline_end.style && $inline_start.color == $inline_end.color {
+                  prop!(BorderInline => $inline_start.to_border());
+                  handled = true;
+                }
+              }
+
+              if !handled {
+                side_diff!($block_start, $inline_start, $inline_start_prop, $inline_start_width, $inline_start_style, $inline_start_color);
+                side_diff!($block_start, $inline_end, $inline_end_prop, $inline_end_width, $inline_end_style, $inline_end_color);
+              }
+            }, true);
+          } else if left_eq_right {
+            prop_diff!($inline_start, {
+              // We know already that top != bottom, so no need to try to use border-block.
+              side_diff!($inline_start, $block_start, $block_start_prop, $block_start_width, $block_start_style, $block_start_color);
+              side_diff!($inline_start, $block_end, $block_end_prop, $block_end_width, $block_end_style, $block_end_color);
+            }, true);
+          } else if bottom_eq_right {
+            prop_diff!($block_end, {
+              side_diff!($block_end, $block_start, $block_start_prop, $block_start_width, $block_start_style, $block_start_color);
+              side_diff!($block_end, $inline_start, $inline_start_prop, $inline_start_width, $inline_start_style, $inline_start_color);
+            }, true);
+          } else {
+            prop_diff!($block_start, {
+              prop!($block_start_prop => $block_start.to_border());
+              prop!($block_end_prop => $block_end.to_border());
+              prop!($inline_start_prop => $inline_start.to_border());
+              prop!($inline_end_prop => $inline_end.to_border());
+            }, false);
+          }
+        } else {
+          shorthand!(BorderStyle, style);
+          shorthand!(BorderWidth, width);
+          shorthand!(BorderColor, color);
+
+          macro_rules! side {
+            ($val: expr, $shorthand: ident, $width: ident, $style: ident, $color: ident) => {
+              if $val.is_valid() {
+                prop!($shorthand => $val.to_border());
+              } else {
+                if let Some(style) = &$val.style {
+                  prop!($style => style.clone());
+                }
+
+                if let Some(width) = &$val.width {
+                  prop!($width => width.clone());
+                }
+
+                if let Some(color) = &$val.color {
+                  prop!($color => color.clone());
+                }
+              }
+            };
+          }
+
+          if $is_logical && $block_start == $block_end && $block_start.is_valid() {
+            if logical_supported {
+              if logical_shorthand_supported {
+                prop!(BorderBlock => $block_start.to_border());
+              } else {
+                prop!(BorderBlockStart => $block_start.to_border());
+                prop!(BorderBlockEnd => $block_start.to_border());
+              }
+            } else {
+              prop!(BorderTop => $block_start.to_border());
+              prop!(BorderBottom => $block_start.to_border());
+            }
+          } else {
+            if $is_logical && logical_shorthand_supported && !$block_start.is_valid() && !$block_end.is_valid() {
+              logical_shorthand!(BorderBlockStyle, style, $block_start, $block_end);
+              logical_shorthand!(BorderBlockWidth, width, $block_start, $block_end);
+              logical_shorthand!(BorderBlockColor, color, $block_start, $block_end);
+            }
+
+            side!($block_start, $block_start_prop, $block_start_width, $block_start_style, $block_start_color);
+            side!($block_end, $block_end_prop, $block_end_width, $block_end_style, $block_end_color);
+          }
+
+          if $is_logical && $inline_start == $inline_end && $inline_start.is_valid() {
+            if logical_supported {
+              if logical_shorthand_supported {
+                prop!(BorderInline => $inline_start.to_border());
+              } else {
+                prop!(BorderInlineStart => $inline_start.to_border());
+                prop!(BorderInlineEnd => $inline_start.to_border());
+              }
+            } else {
+              prop!(BorderLeft => $inline_start.to_border());
+              prop!(BorderRight => $inline_start.to_border());
+            }
+          } else {
+            if $is_logical && !$inline_start.is_valid() && !$inline_end.is_valid() {
+              if logical_shorthand_supported {
+                logical_shorthand!(BorderInlineStyle, style, $inline_start, $inline_end);
+                logical_shorthand!(BorderInlineWidth, width, $inline_start, $inline_end);
+                logical_shorthand!(BorderInlineColor, color, $inline_start, $inline_end);
+              } else {
+                // If both values of an inline logical property are equal, then we can just convert them to physical properties.
+                macro_rules! inline_prop {
+                  ($key: ident, $left: ident, $right: ident) => {
+                    if $inline_start.$key.is_some() && $inline_start.$key == $inline_end.$key {
+                      prop!($left => std::mem::take(&mut $inline_start.$key).unwrap());
+                      prop!($right => std::mem::take(&mut $inline_end.$key).unwrap());
+                    }
+                  }
+                }
+
+                inline_prop!(style, BorderLeftStyle, BorderRightStyle);
+                inline_prop!(width, BorderLeftWidth, BorderRightWidth);
+                inline_prop!(color, BorderLeftColor, BorderRightColor);
+              }
+            }
+
+            side!($inline_start, $inline_start_prop, $inline_start_width, $inline_start_style, $inline_start_color);
+            side!($inline_end, $inline_end_prop, $inline_end_width, $inline_end_style, $inline_end_color);
+          }
+        }
+      };
+    }
+
+    flush_category!(
+      BorderTop,
+      BorderTopWidth,
+      BorderTopStyle,
+      BorderTopColor,
+      self.border_top,
+      BorderBottom,
+      BorderBottomWidth,
+      BorderBottomStyle,
+      BorderBottomColor,
+      self.border_bottom,
+      BorderLeft,
+      BorderLeftWidth,
+      BorderLeftStyle,
+      BorderLeftColor,
+      self.border_left,
+      BorderRight,
+      BorderRightWidth,
+      BorderRightStyle,
+      BorderRightColor,
+      self.border_right,
+      false
+    );
+
+    flush_category!(
+      BorderBlockStart,
+      BorderBlockStartWidth,
+      BorderBlockStartStyle,
+      BorderBlockStartColor,
+      self.border_block_start,
+      BorderBlockEnd,
+      BorderBlockEndWidth,
+      BorderBlockEndStyle,
+      BorderBlockEndColor,
+      self.border_block_end,
+      BorderInlineStart,
+      BorderInlineStartWidth,
+      BorderInlineStartStyle,
+      BorderInlineStartColor,
+      self.border_inline_start,
+      BorderInlineEnd,
+      BorderInlineEndWidth,
+      BorderInlineEndStyle,
+      BorderInlineEndColor,
+      self.border_inline_end,
+      true
+    );
+
+    self.border_top.reset();
+    self.border_bottom.reset();
+    self.border_left.reset();
+    self.border_right.reset();
+    self.border_block_start.reset();
+    self.border_block_end.reset();
+    self.border_inline_start.reset();
+    self.border_inline_end.reset();
+  }
+
+  fn flush_unparsed(
+    &mut self,
+    unparsed: &UnparsedProperty<'i>,
+    dest: &mut DeclarationList<'i>,
+    context: &mut PropertyHandlerContext<'i, '_>,
+  ) {
+    let logical_supported = !context.should_compile_logical(Feature::LogicalBorders);
+    if logical_supported {
+      let mut unparsed = unparsed.clone();
+      context.add_unparsed_fallbacks(&mut unparsed);
+      self
+        .flushed_properties
+        .insert(BorderProperty::try_from(&unparsed.property_id).unwrap());
+      dest.push(Property::Unparsed(unparsed));
+      return;
+    }
+
+    macro_rules! prop {
+      ($id: ident) => {{
+        let mut unparsed = unparsed.with_property_id(PropertyId::$id);
+        context.add_unparsed_fallbacks(&mut unparsed);
+        dest.push(Property::Unparsed(unparsed));
+        self.flushed_properties.insert(BorderProperty::$id);
+      }};
+    }
+
+    macro_rules! logical_prop {
+      ($ltr: ident, $ltr_key: ident, $rtl: ident, $rtl_key: ident) => {{
+        context.add_logical_rule(
+          Property::Unparsed(unparsed.with_property_id(PropertyId::$ltr)),
+          Property::Unparsed(unparsed.with_property_id(PropertyId::$rtl)),
+        );
+      }};
+    }
+
+    use PropertyId::*;
+    match &unparsed.property_id {
+      BorderInlineStart => logical_prop!(BorderLeft, border_left, BorderRight, border_right),
+      BorderInlineStartWidth => {
+        logical_prop!(BorderLeftWidth, border_left_width, BorderRightWidth, border_right_width)
+      }
+      BorderInlineStartColor => {
+        logical_prop!(BorderLeftColor, border_left_color, BorderRightColor, border_right_color)
+      }
+      BorderInlineStartStyle => {
+        logical_prop!(BorderLeftStyle, border_left_style, BorderRightStyle, border_right_style)
+      }
+      BorderInlineEnd => logical_prop!(BorderRight, border_right, BorderLeft, border_left),
+      BorderInlineEndWidth => {
+        logical_prop!(BorderRightWidth, border_right_width, BorderLeftWidth, border_left_width)
+      }
+      BorderInlineEndColor => {
+        logical_prop!(BorderRightColor, border_right_color, BorderLeftColor, border_left_color)
+      }
+      BorderInlineEndStyle => {
+        logical_prop!(BorderRightStyle, border_right_style, BorderLeftStyle, border_left_style)
+      }
+      BorderBlockStart => prop!(BorderTop),
+      BorderBlockStartWidth => prop!(BorderTopWidth),
+      BorderBlockStartColor => prop!(BorderTopColor),
+      BorderBlockStartStyle => prop!(BorderTopStyle),
+      BorderBlockEnd => prop!(BorderBottom),
+      BorderBlockEndWidth => prop!(BorderBottomWidth),
+      BorderBlockEndColor => prop!(BorderBottomColor),
+      BorderBlockEndStyle => prop!(BorderBottomStyle),
+      property_id => {
+        let mut unparsed = unparsed.clone();
+        context.add_unparsed_fallbacks(&mut unparsed);
+        dest.push(Property::Unparsed(unparsed));
+        self.flushed_properties.insert(BorderProperty::try_from(property_id).unwrap());
+      }
+    }
+  }
+}
+
+fn is_border_property(property_id: &PropertyId) -> bool {
+  match property_id {
+    PropertyId::BorderTopColor
+    | PropertyId::BorderBottomColor
+    | PropertyId::BorderLeftColor
+    | PropertyId::BorderRightColor
+    | PropertyId::BorderBlockStartColor
+    | PropertyId::BorderBlockEndColor
+    | PropertyId::BorderBlockColor
+    | PropertyId::BorderInlineStartColor
+    | PropertyId::BorderInlineEndColor
+    | PropertyId::BorderInlineColor
+    | PropertyId::BorderTopWidth
+    | PropertyId::BorderBottomWidth
+    | PropertyId::BorderLeftWidth
+    | PropertyId::BorderRightWidth
+    | PropertyId::BorderBlockStartWidth
+    | PropertyId::BorderBlockEndWidth
+    | PropertyId::BorderBlockWidth
+    | PropertyId::BorderInlineStartWidth
+    | PropertyId::BorderInlineEndWidth
+    | PropertyId::BorderInlineWidth
+    | PropertyId::BorderTopStyle
+    | PropertyId::BorderBottomStyle
+    | PropertyId::BorderLeftStyle
+    | PropertyId::BorderRightStyle
+    | PropertyId::BorderBlockStartStyle
+    | PropertyId::BorderBlockEndStyle
+    | PropertyId::BorderBlockStyle
+    | PropertyId::BorderInlineStartStyle
+    | PropertyId::BorderInlineEndStyle
+    | PropertyId::BorderInlineStyle
+    | PropertyId::BorderTop
+    | PropertyId::BorderBottom
+    | PropertyId::BorderLeft
+    | PropertyId::BorderRight
+    | PropertyId::BorderBlockStart
+    | PropertyId::BorderBlockEnd
+    | PropertyId::BorderInlineStart
+    | PropertyId::BorderInlineEnd
+    | PropertyId::BorderBlock
+    | PropertyId::BorderInline
+    | PropertyId::BorderWidth
+    | PropertyId::BorderStyle
+    | PropertyId::BorderColor
+    | PropertyId::Border => true,
+    _ => false,
+  }
+}
diff --git a/src/properties/border_image.rs b/src/properties/border_image.rs
new file mode 100644
index 0000000..300d458
--- /dev/null
+++ b/src/properties/border_image.rs
@@ -0,0 +1,589 @@
+//! CSS properties related to border images.
+
+use crate::context::PropertyHandlerContext;
+use crate::declaration::{DeclarationBlock, DeclarationList};
+use crate::error::{ParserError, PrinterError};
+use crate::prefixes::Feature;
+use crate::printer::Printer;
+use crate::properties::{Property, PropertyId, VendorPrefix};
+use crate::targets::{Browsers, Targets};
+use crate::traits::{IsCompatible, Parse, PropertyHandler, Shorthand, ToCss};
+use crate::values::image::Image;
+use crate::values::number::CSSNumber;
+use crate::values::rect::Rect;
+#[cfg(feature = "visitor")]
+use crate::visitor::Visit;
+use crate::{compat, macros::*};
+use crate::{
+  traits::FallbackValues,
+  values::{
+    length::*,
+    percentage::{NumberOrPercentage, Percentage},
+  },
+};
+use cssparser::*;
+
+enum_property! {
+  /// A single [border-image-repeat](https://www.w3.org/TR/css-backgrounds-3/#border-image-repeat) keyword.
+  pub enum BorderImageRepeatKeyword {
+    /// The image is stretched to fill the area.
+    Stretch,
+    /// The image is tiled (repeated) to fill the area.
+    Repeat,
+     /// The image is scaled so that it repeats an even number of times.
+    Round,
+    /// The image is repeated so that it fits, and then spaced apart evenly.
+    Space,
+  }
+}
+
+impl IsCompatible for BorderImageRepeatKeyword {
+  fn is_compatible(&self, browsers: Browsers) -> bool {
+    use BorderImageRepeatKeyword::*;
+    match self {
+      Round => compat::Feature::BorderImageRepeatRound.is_compatible(browsers),
+      Space => compat::Feature::BorderImageRepeatSpace.is_compatible(browsers),
+      Stretch | Repeat => true,
+    }
+  }
+}
+
+/// A value for the [border-image-repeat](https://www.w3.org/TR/css-backgrounds-3/#border-image-repeat) property.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub struct BorderImageRepeat {
+  /// The horizontal repeat value.
+  pub horizontal: BorderImageRepeatKeyword,
+  /// The vertical repeat value.
+  pub vertical: BorderImageRepeatKeyword,
+}
+
+impl Default for BorderImageRepeat {
+  fn default() -> BorderImageRepeat {
+    BorderImageRepeat {
+      horizontal: BorderImageRepeatKeyword::Stretch,
+      vertical: BorderImageRepeatKeyword::Stretch,
+    }
+  }
+}
+
+impl<'i> Parse<'i> for BorderImageRepeat {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let horizontal = BorderImageRepeatKeyword::parse(input)?;
+    let vertical = input.try_parse(BorderImageRepeatKeyword::parse).ok();
+    Ok(BorderImageRepeat {
+      horizontal,
+      vertical: vertical.unwrap_or(horizontal),
+    })
+  }
+}
+
+impl ToCss for BorderImageRepeat {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    self.horizontal.to_css(dest)?;
+    if self.horizontal != self.vertical {
+      dest.write_str(" ")?;
+      self.vertical.to_css(dest)?;
+    }
+    Ok(())
+  }
+}
+
+impl IsCompatible for BorderImageRepeat {
+  fn is_compatible(&self, browsers: Browsers) -> bool {
+    self.horizontal.is_compatible(browsers) && self.vertical.is_compatible(browsers)
+  }
+}
+
+/// A value for the [border-image-width](https://www.w3.org/TR/css-backgrounds-3/#border-image-width) property.
+#[derive(Debug, Clone, PartialEq, Parse, ToCss)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", content = "value", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum BorderImageSideWidth {
+  /// A number representing a multiple of the border width.
+  Number(CSSNumber),
+  /// An explicit length or percentage.
+  LengthPercentage(LengthPercentage),
+  /// The `auto` keyword, representing the natural width of the image slice.
+  Auto,
+}
+
+impl Default for BorderImageSideWidth {
+  fn default() -> BorderImageSideWidth {
+    BorderImageSideWidth::Number(1.0)
+  }
+}
+
+impl IsCompatible for BorderImageSideWidth {
+  fn is_compatible(&self, browsers: Browsers) -> bool {
+    match self {
+      BorderImageSideWidth::LengthPercentage(l) => l.is_compatible(browsers),
+      _ => true,
+    }
+  }
+}
+
+/// A value for the [border-image-slice](https://www.w3.org/TR/css-backgrounds-3/#border-image-slice) property.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub struct BorderImageSlice {
+  /// The offsets from the edges of the image.
+  pub offsets: Rect<NumberOrPercentage>,
+  /// Whether the middle of the border image should be preserved.
+  pub fill: bool,
+}
+
+impl Default for BorderImageSlice {
+  fn default() -> BorderImageSlice {
+    BorderImageSlice {
+      offsets: Rect::all(NumberOrPercentage::Percentage(Percentage(1.0))),
+      fill: false,
+    }
+  }
+}
+
+impl<'i> Parse<'i> for BorderImageSlice {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let mut fill = input.try_parse(|i| i.expect_ident_matching("fill")).is_ok();
+    let offsets = Rect::parse(input)?;
+    if !fill {
+      fill = input.try_parse(|i| i.expect_ident_matching("fill")).is_ok();
+    }
+    Ok(BorderImageSlice { offsets, fill })
+  }
+}
+
+impl ToCss for BorderImageSlice {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    self.offsets.to_css(dest)?;
+    if self.fill {
+      dest.write_str(" fill")?;
+    }
+    Ok(())
+  }
+}
+
+impl IsCompatible for BorderImageSlice {
+  fn is_compatible(&self, _browsers: Browsers) -> bool {
+    true
+  }
+}
+
+define_shorthand! {
+  /// A value for the [border-image](https://www.w3.org/TR/css-backgrounds-3/#border-image) shorthand property.
+  #[derive(Default)]
+  pub struct BorderImage<'i>(VendorPrefix) {
+    /// The border image.
+    #[cfg_attr(feature = "serde", serde(borrow))]
+    source: BorderImageSource(Image<'i>),
+    /// The offsets that define where the image is sliced.
+    slice: BorderImageSlice(BorderImageSlice),
+    /// The width of the border image.
+    width: BorderImageWidth(Rect<BorderImageSideWidth>),
+    /// The amount that the image extends beyond the border box.
+    outset: BorderImageOutset(Rect<LengthOrNumber>),
+    /// How the border image is scaled and tiled.
+    repeat: BorderImageRepeat(BorderImageRepeat),
+  }
+}
+
+impl<'i> Parse<'i> for BorderImage<'i> {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    BorderImage::parse_with_callback(input, |_| false)
+  }
+}
+
+impl<'i> BorderImage<'i> {
+  pub(crate) fn parse_with_callback<'t, F>(
+    input: &mut Parser<'i, 't>,
+    mut callback: F,
+  ) -> Result<Self, ParseError<'i, ParserError<'i>>>
+  where
+    F: FnMut(&mut Parser<'i, 't>) -> bool,
+  {
+    let mut source: Option<Image> = None;
+    let mut slice: Option<BorderImageSlice> = None;
+    let mut width: Option<Rect<BorderImageSideWidth>> = None;
+    let mut outset: Option<Rect<LengthOrNumber>> = None;
+    let mut repeat: Option<BorderImageRepeat> = None;
+    loop {
+      if slice.is_none() {
+        if let Ok(value) = input.try_parse(|input| BorderImageSlice::parse(input)) {
+          slice = Some(value);
+          // Parse border image width and outset, if applicable.
+          let maybe_width_outset: Result<_, cssparser::ParseError<'_, ParserError<'i>>> =
+            input.try_parse(|input| {
+              input.expect_delim('/')?;
+
+              // Parse border image width, if applicable.
+              let w = input.try_parse(|input| Rect::parse(input)).ok();
+
+              // Parse border image outset if applicable.
+              let o = input
+                .try_parse(|input| {
+                  input.expect_delim('/')?;
+                  Rect::parse(input)
+                })
+                .ok();
+              if w.is_none() && o.is_none() {
+                Err(input.new_custom_error(ParserError::InvalidDeclaration))
+              } else {
+                Ok((w, o))
+              }
+            });
+          if let Ok((w, o)) = maybe_width_outset {
+            width = w;
+            outset = o;
+          }
+          continue;
+        }
+      }
+
+      if source.is_none() {
+        if let Ok(value) = input.try_parse(|input| Image::parse(input)) {
+          source = Some(value);
+          continue;
+        }
+      }
+
+      if repeat.is_none() {
+        if let Ok(value) = input.try_parse(|input| BorderImageRepeat::parse(input)) {
+          repeat = Some(value);
+          continue;
+        }
+      }
+
+      if callback(input) {
+        continue;
+      }
+
+      break;
+    }
+
+    if source.is_some() || slice.is_some() || width.is_some() || outset.is_some() || repeat.is_some() {
+      Ok(BorderImage {
+        source: source.unwrap_or_default(),
+        slice: slice.unwrap_or_default(),
+        width: width.unwrap_or(Rect::all(BorderImageSideWidth::default())),
+        outset: outset.unwrap_or(Rect::all(LengthOrNumber::default())),
+        repeat: repeat.unwrap_or_default(),
+      })
+    } else {
+      Err(input.new_custom_error(ParserError::InvalidDeclaration))
+    }
+  }
+
+  pub(crate) fn to_css_internal<W>(
+    source: &Image<'i>,
+    slice: &BorderImageSlice,
+    width: &Rect<BorderImageSideWidth>,
+    outset: &Rect<LengthOrNumber>,
+    repeat: &BorderImageRepeat,
+    dest: &mut Printer<W>,
+  ) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    if *source != Image::default() {
+      source.to_css(dest)?;
+    }
+    let has_slice = *slice != BorderImageSlice::default();
+    let has_width = *width != Rect::all(BorderImageSideWidth::default());
+    let has_outset = *outset != Rect::all(LengthOrNumber::Number(0.0));
+    if has_slice || has_width || has_outset {
+      dest.write_str(" ")?;
+      slice.to_css(dest)?;
+      if has_width || has_outset {
+        dest.delim('/', true)?;
+      }
+      if has_width {
+        width.to_css(dest)?;
+      }
+
+      if has_outset {
+        dest.delim('/', true)?;
+        outset.to_css(dest)?;
+      }
+    }
+
+    if *repeat != BorderImageRepeat::default() {
+      dest.write_str(" ")?;
+      repeat.to_css(dest)?;
+    }
+
+    Ok(())
+  }
+}
+
+impl<'i> ToCss for BorderImage<'i> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    BorderImage::to_css_internal(&self.source, &self.slice, &self.width, &self.outset, &self.repeat, dest)
+  }
+}
+
+impl<'i> FallbackValues for BorderImage<'i> {
+  fn get_fallbacks(&mut self, targets: Targets) -> Vec<Self> {
+    self
+      .source
+      .get_fallbacks(targets)
+      .into_iter()
+      .map(|source| BorderImage { source, ..self.clone() })
+      .collect()
+  }
+}
+
+property_bitflags! {
+  #[derive(Default, Debug)]
+  struct BorderImageProperty: u16 {
+    const BorderImageSource = 1 << 0;
+    const BorderImageSlice = 1 << 1;
+    const BorderImageWidth = 1 << 2;
+    const BorderImageOutset = 1 << 3;
+    const BorderImageRepeat = 1 << 4;
+    const BorderImage(_vp) = Self::BorderImageSource.bits() | Self::BorderImageSlice.bits() | Self::BorderImageWidth.bits() | Self::BorderImageOutset.bits() | Self::BorderImageRepeat.bits();
+  }
+}
+
+#[derive(Debug)]
+pub(crate) struct BorderImageHandler<'i> {
+  source: Option<Image<'i>>,
+  slice: Option<BorderImageSlice>,
+  width: Option<Rect<BorderImageSideWidth>>,
+  outset: Option<Rect<LengthOrNumber>>,
+  repeat: Option<BorderImageRepeat>,
+  vendor_prefix: VendorPrefix,
+  flushed_properties: BorderImageProperty,
+  has_any: bool,
+}
+
+impl<'i> Default for BorderImageHandler<'i> {
+  fn default() -> Self {
+    BorderImageHandler {
+      vendor_prefix: VendorPrefix::empty(),
+      source: None,
+      slice: None,
+      width: None,
+      outset: None,
+      repeat: None,
+      flushed_properties: BorderImageProperty::empty(),
+      has_any: false,
+    }
+  }
+}
+
+impl<'i> PropertyHandler<'i> for BorderImageHandler<'i> {
+  fn handle_property(
+    &mut self,
+    property: &Property<'i>,
+    dest: &mut DeclarationList<'i>,
+    context: &mut PropertyHandlerContext<'i, '_>,
+  ) -> bool {
+    use Property::*;
+    macro_rules! property {
+      ($name: ident, $val: ident) => {{
+        if self.vendor_prefix != VendorPrefix::None {
+          self.flush(dest, context);
+        }
+        flush!($name, $val);
+        self.vendor_prefix = VendorPrefix::None;
+        self.$name = Some($val.clone());
+        self.has_any = true;
+      }};
+    }
+
+    macro_rules! flush {
+      ($name: ident, $val: expr) => {{
+        if self.$name.is_some() && self.$name.as_ref().unwrap() != $val && matches!(context.targets.browsers, Some(targets) if !$val.is_compatible(targets)) {
+          self.flush(dest, context);
+        }
+      }};
+    }
+
+    match property {
+      BorderImageSource(val) => property!(source, val),
+      BorderImageSlice(val) => property!(slice, val),
+      BorderImageWidth(val) => property!(width, val),
+      BorderImageOutset(val) => property!(outset, val),
+      BorderImageRepeat(val) => property!(repeat, val),
+      BorderImage(val, vp) => {
+        flush!(source, &val.source);
+        flush!(slice, &val.slice);
+        flush!(width, &val.width);
+        flush!(outset, &val.outset);
+        flush!(repeat, &val.repeat);
+        self.source = Some(val.source.clone());
+        self.slice = Some(val.slice.clone());
+        self.width = Some(val.width.clone());
+        self.outset = Some(val.outset.clone());
+        self.repeat = Some(val.repeat.clone());
+        self.vendor_prefix |= *vp;
+        self.has_any = true;
+      }
+      Unparsed(val) if is_border_image_property(&val.property_id) => {
+        self.flush(dest, context);
+
+        // Even if we weren't able to parse the value (e.g. due to var() references),
+        // we can still add vendor prefixes to the property itself.
+        let mut unparsed = if matches!(val.property_id, PropertyId::BorderImage(_)) {
+          val.get_prefixed(context.targets, Feature::BorderImage)
+        } else {
+          val.clone()
+        };
+
+        context.add_unparsed_fallbacks(&mut unparsed);
+        self
+          .flushed_properties
+          .insert(BorderImageProperty::try_from(&unparsed.property_id).unwrap());
+        dest.push(Property::Unparsed(unparsed));
+      }
+      _ => return false,
+    }
+
+    true
+  }
+
+  fn finalize(&mut self, dest: &mut DeclarationList<'i>, context: &mut PropertyHandlerContext<'i, '_>) {
+    self.flush(dest, context);
+    self.flushed_properties = BorderImageProperty::empty();
+  }
+}
+
+impl<'i> BorderImageHandler<'i> {
+  pub fn reset(&mut self) {
+    self.source = None;
+    self.slice = None;
+    self.width = None;
+    self.outset = None;
+    self.repeat = None;
+  }
+
+  pub fn will_flush(&self, property: &Property<'i>) -> bool {
+    use Property::*;
+    match property {
+      BorderImageSource(_) | BorderImageSlice(_) | BorderImageWidth(_) | BorderImageOutset(_)
+      | BorderImageRepeat(_) => self.vendor_prefix != VendorPrefix::None,
+      Unparsed(val) => is_border_image_property(&val.property_id),
+      _ => false,
+    }
+  }
+
+  fn flush(&mut self, dest: &mut DeclarationList<'i>, context: &mut PropertyHandlerContext<'i, '_>) {
+    if !self.has_any {
+      return;
+    }
+
+    self.has_any = false;
+
+    macro_rules! push {
+      ($prop: ident, $val: expr) => {
+        dest.push(Property::$prop($val));
+        self.flushed_properties.insert(BorderImageProperty::$prop);
+      };
+    }
+
+    let source = std::mem::take(&mut self.source);
+    let slice = std::mem::take(&mut self.slice);
+    let width = std::mem::take(&mut self.width);
+    let outset = std::mem::take(&mut self.outset);
+    let repeat = std::mem::take(&mut self.repeat);
+
+    if source.is_some() && slice.is_some() && width.is_some() && outset.is_some() && repeat.is_some() {
+      let mut border_image = BorderImage {
+        source: source.unwrap(),
+        slice: slice.unwrap(),
+        width: width.unwrap(),
+        outset: outset.unwrap(),
+        repeat: repeat.unwrap(),
+      };
+
+      let mut prefix = self.vendor_prefix;
+      if prefix.contains(VendorPrefix::None) && !border_image.slice.fill {
+        prefix = context.targets.prefixes(self.vendor_prefix, Feature::BorderImage);
+        if !self.flushed_properties.intersects(BorderImageProperty::BorderImage) {
+          let fallbacks = border_image.get_fallbacks(context.targets);
+          for fallback in fallbacks {
+            // Match prefix of fallback. e.g. -webkit-linear-gradient
+            // can only be used in -webkit-border-image, not -moz-border-image.
+            // However, if border-image is unprefixed, gradients can still be.
+            let mut p = fallback.source.get_vendor_prefix() & prefix;
+            if p.is_empty() {
+              p = prefix;
+            }
+            dest.push(Property::BorderImage(fallback, p));
+          }
+        }
+      }
+
+      let p = border_image.source.get_vendor_prefix() & prefix;
+      if !p.is_empty() {
+        prefix = p;
+      }
+
+      dest.push(Property::BorderImage(border_image, prefix));
+      self.flushed_properties.insert(BorderImageProperty::BorderImage);
+    } else {
+      if let Some(mut source) = source {
+        if !self.flushed_properties.contains(BorderImageProperty::BorderImageSource) {
+          let fallbacks = source.get_fallbacks(context.targets);
+          for fallback in fallbacks {
+            dest.push(Property::BorderImageSource(fallback));
+          }
+        }
+
+        push!(BorderImageSource, source);
+      }
+
+      if let Some(slice) = slice {
+        push!(BorderImageSlice, slice);
+      }
+
+      if let Some(width) = width {
+        push!(BorderImageWidth, width);
+      }
+
+      if let Some(outset) = outset {
+        push!(BorderImageOutset, outset);
+      }
+
+      if let Some(repeat) = repeat {
+        push!(BorderImageRepeat, repeat);
+      }
+    }
+
+    self.vendor_prefix = VendorPrefix::empty();
+  }
+}
+
+#[inline]
+fn is_border_image_property(property_id: &PropertyId) -> bool {
+  match property_id {
+    PropertyId::BorderImageSource
+    | PropertyId::BorderImageSlice
+    | PropertyId::BorderImageWidth
+    | PropertyId::BorderImageOutset
+    | PropertyId::BorderImageRepeat
+    | PropertyId::BorderImage(_) => true,
+    _ => false,
+  }
+}
diff --git a/src/properties/border_radius.rs b/src/properties/border_radius.rs
new file mode 100644
index 0000000..e3cf1bc
--- /dev/null
+++ b/src/properties/border_radius.rs
@@ -0,0 +1,353 @@
+//! The CSS border radius property.
+
+use crate::compat;
+use crate::context::PropertyHandlerContext;
+use crate::declaration::{DeclarationBlock, DeclarationList};
+use crate::error::{ParserError, PrinterError};
+use crate::logical::PropertyCategory;
+use crate::macros::define_shorthand;
+use crate::prefixes::Feature;
+use crate::printer::Printer;
+use crate::properties::{Property, PropertyId, VendorPrefix};
+use crate::traits::{IsCompatible, Parse, PropertyHandler, Shorthand, ToCss, Zero};
+use crate::values::length::*;
+use crate::values::rect::Rect;
+use crate::values::size::Size2D;
+#[cfg(feature = "visitor")]
+use crate::visitor::Visit;
+use cssparser::*;
+
+define_shorthand! {
+  /// A value for the [border-radius](https://www.w3.org/TR/css-backgrounds-3/#border-radius) property.
+  pub struct BorderRadius(VendorPrefix) {
+    /// The x and y radius values for the top left corner.
+    top_left: BorderTopLeftRadius(Size2D<LengthPercentage>, VendorPrefix),
+    /// The x and y radius values for the top right corner.
+    top_right: BorderTopRightRadius(Size2D<LengthPercentage>, VendorPrefix),
+    /// The x and y radius values for the bottom right corner.
+    bottom_right: BorderBottomRightRadius(Size2D<LengthPercentage>, VendorPrefix),
+    /// The x and y radius values for the bottom left corner.
+    bottom_left: BorderBottomLeftRadius(Size2D<LengthPercentage>, VendorPrefix),
+  }
+}
+
+impl Default for BorderRadius {
+  fn default() -> BorderRadius {
+    let zero = Size2D(LengthPercentage::zero(), LengthPercentage::zero());
+    BorderRadius {
+      top_left: zero.clone(),
+      top_right: zero.clone(),
+      bottom_right: zero.clone(),
+      bottom_left: zero,
+    }
+  }
+}
+
+impl<'i> Parse<'i> for BorderRadius {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let widths: Rect<LengthPercentage> = Rect::parse(input)?;
+    let heights = if input.try_parse(|input| input.expect_delim('/')).is_ok() {
+      Rect::parse(input)?
+    } else {
+      widths.clone()
+    };
+
+    Ok(BorderRadius {
+      top_left: Size2D(widths.0, heights.0),
+      top_right: Size2D(widths.1, heights.1),
+      bottom_right: Size2D(widths.2, heights.2),
+      bottom_left: Size2D(widths.3, heights.3),
+    })
+  }
+}
+
+impl ToCss for BorderRadius {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    let widths = Rect::new(
+      &self.top_left.0,
+      &self.top_right.0,
+      &self.bottom_right.0,
+      &self.bottom_left.0,
+    );
+    let heights = Rect::new(
+      &self.top_left.1,
+      &self.top_right.1,
+      &self.bottom_right.1,
+      &self.bottom_left.1,
+    );
+
+    widths.to_css(dest)?;
+    if widths != heights {
+      dest.delim('/', true)?;
+      heights.to_css(dest)?;
+    }
+
+    Ok(())
+  }
+}
+
+#[derive(Default, Debug)]
+pub(crate) struct BorderRadiusHandler<'i> {
+  top_left: Option<(Size2D<LengthPercentage>, VendorPrefix)>,
+  top_right: Option<(Size2D<LengthPercentage>, VendorPrefix)>,
+  bottom_right: Option<(Size2D<LengthPercentage>, VendorPrefix)>,
+  bottom_left: Option<(Size2D<LengthPercentage>, VendorPrefix)>,
+  start_start: Option<Property<'i>>,
+  start_end: Option<Property<'i>>,
+  end_end: Option<Property<'i>>,
+  end_start: Option<Property<'i>>,
+  category: PropertyCategory,
+  has_any: bool,
+}
+
+impl<'i> PropertyHandler<'i> for BorderRadiusHandler<'i> {
+  fn handle_property(
+    &mut self,
+    property: &Property<'i>,
+    dest: &mut DeclarationList<'i>,
+    context: &mut PropertyHandlerContext<'i, '_>,
+  ) -> bool {
+    use Property::*;
+
+    macro_rules! maybe_flush {
+      ($prop: ident, $val: expr, $vp: expr) => {{
+        // If two vendor prefixes for the same property have different
+        // values, we need to flush what we have immediately to preserve order.
+        if let Some((val, prefixes)) = &self.$prop {
+          if val != $val && !prefixes.contains(*$vp) {
+            self.flush(dest, context);
+          }
+        }
+
+        if self.$prop.is_some() && matches!(context.targets.browsers, Some(targets) if !$val.is_compatible(targets)) {
+          self.flush(dest, context);
+        }
+      }};
+    }
+
+    macro_rules! property {
+      ($prop: ident, $val: expr, $vp: ident) => {{
+        if self.category != PropertyCategory::Physical {
+          self.flush(dest, context);
+        }
+
+        maybe_flush!($prop, $val, $vp);
+
+        // Otherwise, update the value and add the prefix.
+        if let Some((val, prefixes)) = &mut self.$prop {
+          *val = $val.clone();
+          *prefixes |= *$vp;
+        } else {
+          self.$prop = Some(($val.clone(), *$vp));
+          self.has_any = true;
+        }
+
+        self.category = PropertyCategory::Physical;
+      }};
+    }
+
+    macro_rules! logical_property {
+      ($prop: ident) => {{
+        if self.category != PropertyCategory::Logical {
+          self.flush(dest, context);
+        }
+
+        self.$prop = Some(property.clone());
+        self.category = PropertyCategory::Logical;
+        self.has_any = true;
+      }};
+    }
+
+    match property {
+      BorderTopLeftRadius(val, vp) => property!(top_left, val, vp),
+      BorderTopRightRadius(val, vp) => property!(top_right, val, vp),
+      BorderBottomRightRadius(val, vp) => property!(bottom_right, val, vp),
+      BorderBottomLeftRadius(val, vp) => property!(bottom_left, val, vp),
+      BorderStartStartRadius(_) => logical_property!(start_start),
+      BorderStartEndRadius(_) => logical_property!(start_end),
+      BorderEndEndRadius(_) => logical_property!(end_end),
+      BorderEndStartRadius(_) => logical_property!(end_start),
+      BorderRadius(val, vp) => {
+        self.start_start = None;
+        self.start_end = None;
+        self.end_end = None;
+        self.end_start = None;
+        maybe_flush!(top_left, &val.top_left, vp);
+        maybe_flush!(top_right, &val.top_right, vp);
+        maybe_flush!(bottom_right, &val.bottom_right, vp);
+        maybe_flush!(bottom_left, &val.bottom_left, vp);
+        property!(top_left, &val.top_left, vp);
+        property!(top_right, &val.top_right, vp);
+        property!(bottom_right, &val.bottom_right, vp);
+        property!(bottom_left, &val.bottom_left, vp);
+      }
+      Unparsed(val) if is_border_radius_property(&val.property_id) => {
+        // Even if we weren't able to parse the value (e.g. due to var() references),
+        // we can still add vendor prefixes to the property itself.
+        match &val.property_id {
+          PropertyId::BorderStartStartRadius => logical_property!(start_start),
+          PropertyId::BorderStartEndRadius => logical_property!(start_end),
+          PropertyId::BorderEndEndRadius => logical_property!(end_end),
+          PropertyId::BorderEndStartRadius => logical_property!(end_start),
+          _ => {
+            self.flush(dest, context);
+            dest.push(Property::Unparsed(
+              val.get_prefixed(context.targets, Feature::BorderRadius),
+            ));
+          }
+        }
+      }
+      _ => return false,
+    }
+
+    true
+  }
+
+  fn finalize(&mut self, dest: &mut DeclarationList<'i>, context: &mut PropertyHandlerContext<'i, '_>) {
+    self.flush(dest, context);
+  }
+}
+
+impl<'i> BorderRadiusHandler<'i> {
+  fn flush(&mut self, dest: &mut DeclarationList<'i>, context: &mut PropertyHandlerContext<'i, '_>) {
+    if !self.has_any {
+      return;
+    }
+
+    self.has_any = false;
+
+    let mut top_left = std::mem::take(&mut self.top_left);
+    let mut top_right = std::mem::take(&mut self.top_right);
+    let mut bottom_right = std::mem::take(&mut self.bottom_right);
+    let mut bottom_left = std::mem::take(&mut self.bottom_left);
+    let start_start = std::mem::take(&mut self.start_start);
+    let start_end = std::mem::take(&mut self.start_end);
+    let end_end = std::mem::take(&mut self.end_end);
+    let end_start = std::mem::take(&mut self.end_start);
+
+    if let (
+      Some((top_left, tl_prefix)),
+      Some((top_right, tr_prefix)),
+      Some((bottom_right, br_prefix)),
+      Some((bottom_left, bl_prefix)),
+    ) = (&mut top_left, &mut top_right, &mut bottom_right, &mut bottom_left)
+    {
+      let intersection = *tl_prefix & *tr_prefix & *br_prefix & *bl_prefix;
+      if !intersection.is_empty() {
+        let prefix = context.targets.prefixes(intersection, Feature::BorderRadius);
+        dest.push(Property::BorderRadius(
+          BorderRadius {
+            top_left: top_left.clone(),
+            top_right: top_right.clone(),
+            bottom_right: bottom_right.clone(),
+            bottom_left: bottom_left.clone(),
+          },
+          prefix,
+        ));
+        tl_prefix.remove(intersection);
+        tr_prefix.remove(intersection);
+        br_prefix.remove(intersection);
+        bl_prefix.remove(intersection);
+      }
+    }
+
+    macro_rules! single_property {
+      ($prop: ident, $key: ident) => {
+        if let Some((val, mut vp)) = $key {
+          if !vp.is_empty() {
+            vp = context.targets.prefixes(vp, Feature::$prop);
+            dest.push(Property::$prop(val, vp))
+          }
+        }
+      };
+    }
+
+    let logical_supported = !context.should_compile_logical(compat::Feature::LogicalBorderRadius);
+
+    macro_rules! logical_property {
+      ($prop: ident, $key: ident, $ltr: ident, $rtl: ident) => {
+        if let Some(val) = $key {
+          if logical_supported {
+            dest.push(val);
+          } else {
+            let vp = context.targets.prefixes(VendorPrefix::None, Feature::$ltr);
+            match val {
+              Property::BorderStartStartRadius(val)
+              | Property::BorderStartEndRadius(val)
+              | Property::BorderEndEndRadius(val)
+              | Property::BorderEndStartRadius(val) => {
+                context.add_logical_rule(Property::$ltr(val.clone(), vp), Property::$rtl(val, vp));
+              }
+              Property::Unparsed(val) => {
+                context.add_logical_rule(
+                  Property::Unparsed(val.with_property_id(PropertyId::$ltr(vp))),
+                  Property::Unparsed(val.with_property_id(PropertyId::$rtl(vp))),
+                );
+              }
+              _ => {}
+            }
+          }
+        }
+      };
+    }
+
+    single_property!(BorderTopLeftRadius, top_left);
+    single_property!(BorderTopRightRadius, top_right);
+    single_property!(BorderBottomRightRadius, bottom_right);
+    single_property!(BorderBottomLeftRadius, bottom_left);
+    logical_property!(
+      BorderStartStartRadius,
+      start_start,
+      BorderTopLeftRadius,
+      BorderTopRightRadius
+    );
+    logical_property!(
+      BorderStartEndRadius,
+      start_end,
+      BorderTopRightRadius,
+      BorderTopLeftRadius
+    );
+    logical_property!(
+      BorderEndEndRadius,
+      end_end,
+      BorderBottomRightRadius,
+      BorderBottomLeftRadius
+    );
+    logical_property!(
+      BorderEndStartRadius,
+      end_start,
+      BorderBottomLeftRadius,
+      BorderBottomRightRadius
+    );
+  }
+}
+
+#[inline]
+fn is_border_radius_property(property_id: &PropertyId) -> bool {
+  if is_logical_border_radius_property(property_id) {
+    return true;
+  }
+
+  match property_id {
+    PropertyId::BorderTopLeftRadius(_)
+    | PropertyId::BorderTopRightRadius(_)
+    | PropertyId::BorderBottomRightRadius(_)
+    | PropertyId::BorderBottomLeftRadius(_)
+    | PropertyId::BorderRadius(_) => true,
+    _ => false,
+  }
+}
+
+#[inline]
+fn is_logical_border_radius_property(property_id: &PropertyId) -> bool {
+  match property_id {
+    PropertyId::BorderStartStartRadius
+    | PropertyId::BorderStartEndRadius
+    | PropertyId::BorderEndEndRadius
+    | PropertyId::BorderEndStartRadius => true,
+    _ => false,
+  }
+}
diff --git a/src/properties/box_shadow.rs b/src/properties/box_shadow.rs
new file mode 100644
index 0000000..6637616
--- /dev/null
+++ b/src/properties/box_shadow.rs
@@ -0,0 +1,254 @@
+//! The CSS box-shadow property.
+
+use super::PropertyId;
+use crate::context::PropertyHandlerContext;
+use crate::declaration::DeclarationList;
+use crate::error::{ParserError, PrinterError};
+use crate::prefixes::Feature;
+use crate::printer::Printer;
+use crate::properties::Property;
+use crate::targets::Browsers;
+use crate::traits::{IsCompatible, Parse, PropertyHandler, ToCss, Zero};
+use crate::values::color::{ColorFallbackKind, CssColor};
+use crate::values::length::Length;
+use crate::vendor_prefix::VendorPrefix;
+#[cfg(feature = "visitor")]
+use crate::visitor::Visit;
+use cssparser::*;
+use smallvec::SmallVec;
+
+/// A value for the [box-shadow](https://drafts.csswg.org/css-backgrounds/#box-shadow) property.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(rename_all = "camelCase")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub struct BoxShadow {
+  /// The color of the box shadow.
+  pub color: CssColor,
+  /// The x offset of the shadow.
+  pub x_offset: Length,
+  /// The y offset of the shadow.
+  pub y_offset: Length,
+  /// The blur radius of the shadow.
+  pub blur: Length,
+  /// The spread distance of the shadow.
+  pub spread: Length,
+  /// Whether the shadow is inset within the box.
+  pub inset: bool,
+}
+
+impl<'i> Parse<'i> for BoxShadow {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let mut color = None;
+    let mut lengths = None;
+    let mut inset = false;
+
+    loop {
+      if !inset {
+        if input.try_parse(|input| input.expect_ident_matching("inset")).is_ok() {
+          inset = true;
+          continue;
+        }
+      }
+
+      if lengths.is_none() {
+        let value = input.try_parse::<_, _, ParseError<ParserError<'i>>>(|input| {
+          let horizontal = Length::parse(input)?;
+          let vertical = Length::parse(input)?;
+          let blur = input.try_parse(Length::parse).unwrap_or(Length::zero());
+          let spread = input.try_parse(Length::parse).unwrap_or(Length::zero());
+          Ok((horizontal, vertical, blur, spread))
+        });
+
+        if let Ok(value) = value {
+          lengths = Some(value);
+          continue;
+        }
+      }
+
+      if color.is_none() {
+        if let Ok(value) = input.try_parse(CssColor::parse) {
+          color = Some(value);
+          continue;
+        }
+      }
+
+      break;
+    }
+
+    let lengths = lengths.ok_or(input.new_error(BasicParseErrorKind::QualifiedRuleInvalid))?;
+    Ok(BoxShadow {
+      color: color.unwrap_or(CssColor::current_color()),
+      x_offset: lengths.0,
+      y_offset: lengths.1,
+      blur: lengths.2,
+      spread: lengths.3,
+      inset,
+    })
+  }
+}
+
+impl ToCss for BoxShadow {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    if self.inset {
+      dest.write_str("inset ")?;
+    }
+
+    self.x_offset.to_css(dest)?;
+    dest.write_char(' ')?;
+    self.y_offset.to_css(dest)?;
+
+    if self.blur != Length::zero() || self.spread != Length::zero() {
+      dest.write_char(' ')?;
+      self.blur.to_css(dest)?;
+
+      if self.spread != Length::zero() {
+        dest.write_char(' ')?;
+        self.spread.to_css(dest)?;
+      }
+    }
+
+    if self.color != CssColor::current_color() {
+      dest.write_char(' ')?;
+      self.color.to_css(dest)?;
+    }
+
+    Ok(())
+  }
+}
+
+impl IsCompatible for BoxShadow {
+  fn is_compatible(&self, browsers: Browsers) -> bool {
+    self.color.is_compatible(browsers)
+      && self.x_offset.is_compatible(browsers)
+      && self.y_offset.is_compatible(browsers)
+      && self.blur.is_compatible(browsers)
+      && self.spread.is_compatible(browsers)
+  }
+}
+
+#[derive(Default)]
+pub(crate) struct BoxShadowHandler {
+  box_shadows: Option<(SmallVec<[BoxShadow; 1]>, VendorPrefix)>,
+  flushed: bool,
+}
+
+impl<'i> PropertyHandler<'i> for BoxShadowHandler {
+  fn handle_property(
+    &mut self,
+    property: &Property<'i>,
+    dest: &mut DeclarationList<'i>,
+    context: &mut PropertyHandlerContext<'i, '_>,
+  ) -> bool {
+    match property {
+      Property::BoxShadow(box_shadows, prefix) => {
+        if self.box_shadows.is_some()
+          && matches!(context.targets.browsers, Some(browsers) if !box_shadows.is_compatible(browsers))
+        {
+          self.flush(dest, context);
+        }
+
+        if let Some((val, prefixes)) = &mut self.box_shadows {
+          if val != box_shadows && !prefixes.contains(*prefix) {
+            self.flush(dest, context);
+            self.box_shadows = Some((box_shadows.clone(), *prefix));
+          } else {
+            *val = box_shadows.clone();
+            *prefixes |= *prefix;
+          }
+        } else {
+          self.box_shadows = Some((box_shadows.clone(), *prefix));
+        }
+      }
+      Property::Unparsed(unparsed) if matches!(unparsed.property_id, PropertyId::BoxShadow(_)) => {
+        self.flush(dest, context);
+
+        let mut unparsed = unparsed.clone();
+        context.add_unparsed_fallbacks(&mut unparsed);
+        dest.push(Property::Unparsed(unparsed));
+        self.flushed = true;
+      }
+      _ => return false,
+    }
+
+    true
+  }
+
+  fn finalize(&mut self, dest: &mut DeclarationList<'i>, context: &mut PropertyHandlerContext<'i, '_>) {
+    self.flush(dest, context);
+    self.flushed = false;
+  }
+}
+
+impl BoxShadowHandler {
+  fn flush<'i>(&mut self, dest: &mut DeclarationList<'i>, context: &mut PropertyHandlerContext<'i, '_>) {
+    if self.box_shadows.is_none() {
+      return;
+    }
+
+    let box_shadows = std::mem::take(&mut self.box_shadows);
+
+    if let Some((box_shadows, prefixes)) = box_shadows {
+      if !self.flushed {
+        let mut prefixes = context.targets.prefixes(prefixes, Feature::BoxShadow);
+        let mut fallbacks = ColorFallbackKind::empty();
+        for shadow in &box_shadows {
+          fallbacks |= shadow.color.get_necessary_fallbacks(context.targets);
+        }
+
+        if fallbacks.contains(ColorFallbackKind::RGB) {
+          let rgb = box_shadows
+            .iter()
+            .map(|shadow| BoxShadow {
+              color: shadow.color.to_rgb().unwrap_or_else(|_| shadow.color.clone()),
+              ..shadow.clone()
+            })
+            .collect();
+          dest.push(Property::BoxShadow(rgb, prefixes));
+          if prefixes.contains(VendorPrefix::None) {
+            prefixes = VendorPrefix::None;
+          } else {
+            // Only output RGB for prefixed property (e.g. -webkit-box-shadow)
+            return;
+          }
+        }
+
+        if fallbacks.contains(ColorFallbackKind::P3) {
+          let p3 = box_shadows
+            .iter()
+            .map(|shadow| BoxShadow {
+              color: shadow.color.to_p3().unwrap_or_else(|_| shadow.color.clone()),
+              ..shadow.clone()
+            })
+            .collect();
+          dest.push(Property::BoxShadow(p3, VendorPrefix::None));
+        }
+
+        if fallbacks.contains(ColorFallbackKind::LAB) {
+          let lab = box_shadows
+            .iter()
+            .map(|shadow| BoxShadow {
+              color: shadow.color.to_lab().unwrap_or_else(|_| shadow.color.clone()),
+              ..shadow.clone()
+            })
+            .collect();
+          dest.push(Property::BoxShadow(lab, VendorPrefix::None));
+        } else {
+          dest.push(Property::BoxShadow(box_shadows, prefixes))
+        }
+      } else {
+        dest.push(Property::BoxShadow(box_shadows, prefixes))
+      }
+    }
+
+    self.flushed = true;
+  }
+}
diff --git a/src/properties/contain.rs b/src/properties/contain.rs
new file mode 100644
index 0000000..1c57bab
--- /dev/null
+++ b/src/properties/contain.rs
@@ -0,0 +1,160 @@
+//! CSS properties related to containment.
+
+#![allow(non_upper_case_globals)]
+
+use cssparser::*;
+use smallvec::SmallVec;
+
+#[cfg(feature = "visitor")]
+use crate::visitor::Visit;
+use crate::{
+  context::PropertyHandlerContext,
+  declaration::{DeclarationBlock, DeclarationList},
+  error::{ParserError, PrinterError},
+  macros::{define_shorthand, enum_property, shorthand_handler},
+  printer::Printer,
+  properties::{Property, PropertyId},
+  rules::container::ContainerName as ContainerIdent,
+  targets::Browsers,
+  traits::{IsCompatible, Parse, PropertyHandler, Shorthand, ToCss},
+};
+
+enum_property! {
+  /// A value for the [container-type](https://drafts.csswg.org/css-contain-3/#container-type) property.
+  /// Establishes the element as a query container for the purpose of container queries.
+  pub enum ContainerType {
+    /// The element is not a query container for any container size queries,
+    /// but remains a query container for container style queries.
+    Normal,
+    /// Establishes a query container for container size queries on the container’s own inline axis.
+    InlineSize,
+    /// Establishes a query container for container size queries on both the inline and block axis.
+    Size,
+  }
+}
+
+impl Default for ContainerType {
+  fn default() -> Self {
+    ContainerType::Normal
+  }
+}
+
+impl IsCompatible for ContainerType {
+  fn is_compatible(&self, _browsers: Browsers) -> bool {
+    true
+  }
+}
+
+/// A value for the [container-name](https://drafts.csswg.org/css-contain-3/#container-name) property.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", content = "value", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub enum ContainerNameList<'i> {
+  /// The `none` keyword.
+  None,
+  /// A list of container names.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  Names(SmallVec<[ContainerIdent<'i>; 1]>),
+}
+
+impl<'i> Default for ContainerNameList<'i> {
+  fn default() -> Self {
+    ContainerNameList::None
+  }
+}
+
+impl<'i> Parse<'i> for ContainerNameList<'i> {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    if input.try_parse(|input| input.expect_ident_matching("none")).is_ok() {
+      return Ok(ContainerNameList::None);
+    }
+
+    let mut names = SmallVec::new();
+    while let Ok(name) = input.try_parse(ContainerIdent::parse) {
+      names.push(name);
+    }
+
+    if names.is_empty() {
+      return Err(input.new_error_for_next_token());
+    } else {
+      return Ok(ContainerNameList::Names(names));
+    }
+  }
+}
+
+impl<'i> ToCss for ContainerNameList<'i> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    match self {
+      ContainerNameList::None => dest.write_str("none"),
+      ContainerNameList::Names(names) => {
+        let mut first = true;
+        for name in names {
+          if first {
+            first = false;
+          } else {
+            dest.write_char(' ')?;
+          }
+          name.to_css(dest)?;
+        }
+        Ok(())
+      }
+    }
+  }
+}
+
+impl IsCompatible for ContainerNameList<'_> {
+  fn is_compatible(&self, _browsers: Browsers) -> bool {
+    true
+  }
+}
+
+define_shorthand! {
+  /// A value for the [container](https://drafts.csswg.org/css-contain-3/#container-shorthand) shorthand property.
+  pub struct Container<'i> {
+    /// The container name.
+    #[cfg_attr(feature = "serde", serde(borrow))]
+    name: ContainerName(ContainerNameList<'i>),
+    /// The container type.
+    container_type: ContainerType(ContainerType),
+  }
+}
+
+impl<'i> Parse<'i> for Container<'i> {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let name = ContainerNameList::parse(input)?;
+    let container_type = if input.try_parse(|input| input.expect_delim('/')).is_ok() {
+      ContainerType::parse(input)?
+    } else {
+      ContainerType::default()
+    };
+    Ok(Container { name, container_type })
+  }
+}
+
+impl<'i> ToCss for Container<'i> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    self.name.to_css(dest)?;
+    if self.container_type != ContainerType::default() {
+      dest.delim('/', true)?;
+      self.container_type.to_css(dest)?;
+    }
+    Ok(())
+  }
+}
+
+shorthand_handler!(ContainerHandler -> Container<'i> {
+  name: ContainerName(ContainerNameList<'i>),
+  container_type: ContainerType(ContainerType),
+});
diff --git a/src/properties/css_modules.rs b/src/properties/css_modules.rs
new file mode 100644
index 0000000..dc8046f
--- /dev/null
+++ b/src/properties/css_modules.rs
@@ -0,0 +1,136 @@
+//! Properties related to CSS modules.
+
+use crate::dependencies::Location;
+use crate::error::{ParserError, PrinterError};
+use crate::printer::Printer;
+use crate::traits::{Parse, ToCss};
+use crate::values::ident::{CustomIdent, CustomIdentList};
+use crate::values::string::CowArcStr;
+#[cfg(feature = "visitor")]
+use crate::visitor::Visit;
+use cssparser::*;
+use smallvec::SmallVec;
+
+/// A value for the [composes](https://github.com/css-modules/css-modules/#dependencies) property from CSS modules.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub struct Composes<'i> {
+  /// A list of class names to compose.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  pub names: CustomIdentList<'i>,
+  /// Where the class names are composed from.
+  pub from: Option<Specifier<'i>>,
+  /// The source location of the `composes` property.
+  pub loc: Location,
+}
+
+/// Defines where the class names referenced in the `composes` property are located.
+///
+/// See [Composes](Composes).
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", content = "value", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub enum Specifier<'i> {
+  /// The referenced name is global.
+  Global,
+  /// The referenced name comes from the specified file.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  File(CowArcStr<'i>),
+  /// The referenced name comes from a source index (used during bundling).
+  SourceIndex(u32),
+}
+
+impl<'i> Parse<'i> for Composes<'i> {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let loc = input.current_source_location();
+    let mut names = SmallVec::new();
+    while let Ok(name) = input.try_parse(parse_one_ident) {
+      names.push(name);
+    }
+
+    if names.is_empty() {
+      return Err(input.new_custom_error(ParserError::InvalidDeclaration));
+    }
+
+    let from = if input.try_parse(|input| input.expect_ident_matching("from")).is_ok() {
+      Some(Specifier::parse(input)?)
+    } else {
+      None
+    };
+
+    Ok(Composes {
+      names,
+      from,
+      loc: loc.into(),
+    })
+  }
+}
+
+fn parse_one_ident<'i, 't>(
+  input: &mut Parser<'i, 't>,
+) -> Result<CustomIdent<'i>, ParseError<'i, ParserError<'i>>> {
+  let name = CustomIdent::parse(input)?;
+  if name.0.eq_ignore_ascii_case("from") {
+    return Err(input.new_error_for_next_token());
+  }
+
+  Ok(name)
+}
+
+impl ToCss for Composes<'_> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    let mut first = true;
+    for name in &self.names {
+      if first {
+        first = false;
+      } else {
+        dest.write_char(' ')?;
+      }
+      name.to_css(dest)?;
+    }
+
+    if let Some(from) = &self.from {
+      dest.write_str(" from ")?;
+      from.to_css(dest)?;
+    }
+
+    Ok(())
+  }
+}
+
+impl<'i> Parse<'i> for Specifier<'i> {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    if let Ok(file) = input.try_parse(|input| input.expect_string_cloned()) {
+      Ok(Specifier::File(file.into()))
+    } else {
+      input.expect_ident_matching("global")?;
+      Ok(Specifier::Global)
+    }
+  }
+}
+
+impl<'i> ToCss for Specifier<'i> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    match self {
+      Specifier::Global => dest.write_str("global")?,
+      Specifier::File(file) => serialize_string(&file, dest)?,
+      Specifier::SourceIndex(..) => {}
+    }
+    Ok(())
+  }
+}
diff --git a/src/properties/custom.rs b/src/properties/custom.rs
new file mode 100644
index 0000000..83a2927
--- /dev/null
+++ b/src/properties/custom.rs
@@ -0,0 +1,1709 @@
+//! CSS custom properties and unparsed token values.
+
+use crate::error::{ParserError, PrinterError, PrinterErrorKind};
+use crate::macros::enum_property;
+use crate::prefixes::Feature;
+use crate::printer::Printer;
+use crate::properties::PropertyId;
+use crate::rules::supports::SupportsCondition;
+use crate::stylesheet::ParserOptions;
+use crate::targets::{should_compile, Features, Targets};
+use crate::traits::{Parse, ParseWithOptions, ToCss};
+use crate::values::angle::Angle;
+use crate::values::color::{
+  parse_hsl_hwb_components, parse_rgb_components, ColorFallbackKind, ComponentParser, CssColor, LightDarkColor,
+  HSL, RGB, RGBA,
+};
+use crate::values::ident::{CustomIdent, DashedIdent, DashedIdentReference, Ident};
+use crate::values::length::{serialize_dimension, LengthValue};
+use crate::values::number::CSSInteger;
+use crate::values::percentage::Percentage;
+use crate::values::resolution::Resolution;
+use crate::values::string::CowArcStr;
+use crate::values::time::Time;
+use crate::values::url::Url;
+#[cfg(feature = "visitor")]
+use crate::visitor::Visit;
+use cssparser::color::parse_hash_color;
+use cssparser::*;
+
+use super::AnimationName;
+#[cfg(feature = "serde")]
+use crate::serialization::ValueWrapper;
+
+/// A CSS custom property, representing any unknown property.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub struct CustomProperty<'i> {
+  /// The name of the property.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  pub name: CustomPropertyName<'i>,
+  /// The property value, stored as a raw token list.
+  pub value: TokenList<'i>,
+}
+
+impl<'i> CustomProperty<'i> {
+  /// Parses a custom property with the given name.
+  pub fn parse<'t>(
+    name: CustomPropertyName<'i>,
+    input: &mut Parser<'i, 't>,
+    options: &ParserOptions<'_, 'i>,
+  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let value = input.parse_until_before(Delimiter::Bang | Delimiter::Semicolon, |input| {
+      TokenList::parse(input, options, 0)
+    })?;
+    Ok(CustomProperty { name, value })
+  }
+}
+
+/// A CSS custom property name.
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize), serde(untagged))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub enum CustomPropertyName<'i> {
+  /// An author-defined CSS custom property.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  Custom(DashedIdent<'i>),
+  /// An unknown CSS property.
+  Unknown(Ident<'i>),
+}
+
+impl<'i> From<CowArcStr<'i>> for CustomPropertyName<'i> {
+  fn from(name: CowArcStr<'i>) -> Self {
+    if name.starts_with("--") {
+      CustomPropertyName::Custom(DashedIdent(name))
+    } else {
+      CustomPropertyName::Unknown(Ident(name))
+    }
+  }
+}
+
+impl<'i> From<CowRcStr<'i>> for CustomPropertyName<'i> {
+  fn from(name: CowRcStr<'i>) -> Self {
+    CustomPropertyName::from(CowArcStr::from(name))
+  }
+}
+
+impl<'i> AsRef<str> for CustomPropertyName<'i> {
+  #[inline]
+  fn as_ref(&self) -> &str {
+    match self {
+      CustomPropertyName::Custom(c) => c.as_ref(),
+      CustomPropertyName::Unknown(u) => u.as_ref(),
+    }
+  }
+}
+
+impl<'i> ToCss for CustomPropertyName<'i> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    match self {
+      CustomPropertyName::Custom(c) => c.to_css(dest),
+      CustomPropertyName::Unknown(u) => u.to_css(dest),
+    }
+  }
+}
+
+#[cfg(feature = "serde")]
+#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
+impl<'i, 'de: 'i> serde::Deserialize<'de> for CustomPropertyName<'i> {
+  fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+  where
+    D: serde::Deserializer<'de>,
+  {
+    let name = CowArcStr::deserialize(deserializer)?;
+    Ok(name.into())
+  }
+}
+
+/// A known property with an unparsed value.
+///
+/// This type is used when the value of a known property could not
+/// be parsed, e.g. in the case css `var()` references are encountered.
+/// In this case, the raw tokens are stored instead.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(rename_all = "camelCase")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub struct UnparsedProperty<'i> {
+  /// The id of the property.
+  pub property_id: PropertyId<'i>,
+  /// The property value, stored as a raw token list.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  pub value: TokenList<'i>,
+}
+
+impl<'i> UnparsedProperty<'i> {
+  /// Parses a property with the given id as a token list.
+  pub fn parse<'t>(
+    property_id: PropertyId<'i>,
+    input: &mut Parser<'i, 't>,
+    options: &ParserOptions<'_, 'i>,
+  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let value = input.parse_until_before(Delimiter::Bang | Delimiter::Semicolon, |input| {
+      TokenList::parse(input, options, 0)
+    })?;
+    Ok(UnparsedProperty { property_id, value })
+  }
+
+  pub(crate) fn get_prefixed(&self, targets: Targets, feature: Feature) -> UnparsedProperty<'i> {
+    let mut clone = self.clone();
+    let prefix = self.property_id.prefix();
+    clone.property_id = clone.property_id.with_prefix(targets.prefixes(prefix.or_none(), feature));
+    clone
+  }
+
+  /// Returns a new UnparsedProperty with the same value and the given property id.
+  pub fn with_property_id(&self, property_id: PropertyId<'i>) -> UnparsedProperty<'i> {
+    UnparsedProperty {
+      property_id,
+      value: self.value.clone(),
+    }
+  }
+
+  /// Substitutes variables and re-parses the property.
+  #[cfg(feature = "substitute_variables")]
+  #[cfg_attr(docsrs, doc(cfg(feature = "substitute_variables")))]
+  pub fn substitute_variables<'x>(
+    mut self,
+    vars: &std::collections::HashMap<&str, TokenList<'i>>,
+  ) -> Result<super::Property<'x>, ()> {
+    use super::Property;
+    use crate::stylesheet::PrinterOptions;
+    use static_self::IntoOwned;
+
+    // Substitute variables in the token list.
+    self.value.substitute_variables(vars);
+
+    // Now stringify and re-parse the property to its fully parsed form.
+    // Ideally we'd be able to reuse the tokens rather than printing, but cssparser doesn't provide a way to do that.
+    let mut css = String::new();
+    let mut dest = Printer::new(&mut css, PrinterOptions::default());
+    self.value.to_css(&mut dest, false).unwrap();
+    let property =
+      Property::parse_string(self.property_id.clone(), &css, ParserOptions::default()).map_err(|_| ())?;
+    Ok(property.into_owned())
+  }
+}
+
+/// A raw list of CSS tokens, with embedded parsed values.
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+#[cfg_attr(feature = "visitor", derive(Visit), visit(visit_token_list, TOKENS))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(transparent))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub struct TokenList<'i>(#[cfg_attr(feature = "serde", serde(borrow))] pub Vec<TokenOrValue<'i>>);
+
+/// A raw CSS token, or a parsed value.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit), visit(visit_token, TOKENS), visit_types(TOKENS | COLORS | URLS | VARIABLES | ENVIRONMENT_VARIABLES | FUNCTIONS | LENGTHS | ANGLES | TIMES | RESOLUTIONS | DASHED_IDENTS))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", content = "value", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub enum TokenOrValue<'i> {
+  /// A token.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  Token(Token<'i>),
+  /// A parsed CSS color.
+  Color(CssColor),
+  /// A color with unresolved components.
+  UnresolvedColor(UnresolvedColor<'i>),
+  /// A parsed CSS url.
+  Url(Url<'i>),
+  /// A CSS variable reference.
+  Var(Variable<'i>),
+  /// A CSS environment variable reference.
+  Env(EnvironmentVariable<'i>),
+  /// A custom CSS function.
+  Function(Function<'i>),
+  /// A length.
+  Length(LengthValue),
+  /// An angle.
+  Angle(Angle),
+  /// A time.
+  Time(Time),
+  /// A resolution.
+  Resolution(Resolution),
+  /// A dashed ident.
+  DashedIdent(DashedIdent<'i>),
+  /// An animation name.
+  AnimationName(AnimationName<'i>),
+}
+
+impl<'i> From<Token<'i>> for TokenOrValue<'i> {
+  fn from(token: Token<'i>) -> TokenOrValue<'i> {
+    TokenOrValue::Token(token)
+  }
+}
+
+impl<'i> TokenOrValue<'i> {
+  /// Returns whether the token is whitespace.
+  pub fn is_whitespace(&self) -> bool {
+    matches!(self, TokenOrValue::Token(Token::WhiteSpace(_)))
+  }
+}
+
+impl<'a> Eq for TokenOrValue<'a> {}
+
+impl<'a> std::hash::Hash for TokenOrValue<'a> {
+  fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+    let tag = std::mem::discriminant(self);
+    tag.hash(state);
+    match self {
+      TokenOrValue::Token(t) => t.hash(state),
+      _ => {
+        // This function is primarily used to deduplicate selectors.
+        // Values inside selectors should be exceedingly rare and implementing
+        // Hash for them is somewhat complex due to floating point values.
+        // For now, we just ignore them, which only means there are more
+        // hash collisions. For such a rare case this is probably fine.
+      }
+    }
+  }
+}
+
+impl<'i> ParseWithOptions<'i> for TokenList<'i> {
+  fn parse_with_options<'t>(
+    input: &mut Parser<'i, 't>,
+    options: &ParserOptions<'_, 'i>,
+  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    TokenList::parse(input, options, 0)
+  }
+}
+
+impl<'i> TokenList<'i> {
+  pub(crate) fn parse<'t>(
+    input: &mut Parser<'i, 't>,
+    options: &ParserOptions<'_, 'i>,
+    depth: usize,
+  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let mut tokens = vec![];
+    TokenList::parse_into(input, &mut tokens, options, depth)?;
+
+    // Slice off leading and trailing whitespace if there are at least two tokens.
+    // If there is only one token, we must preserve it. e.g. `--foo: ;` is valid.
+    if tokens.len() >= 2 {
+      let mut slice = &tokens[..];
+      if matches!(tokens.first(), Some(token) if token.is_whitespace()) {
+        slice = &slice[1..];
+      }
+      if matches!(tokens.last(), Some(token) if token.is_whitespace()) {
+        slice = &slice[..slice.len() - 1];
+      }
+      return Ok(TokenList(slice.to_vec()));
+    }
+
+    return Ok(TokenList(tokens));
+  }
+
+  pub(crate) fn parse_raw<'t>(
+    input: &mut Parser<'i, 't>,
+    tokens: &mut Vec<TokenOrValue<'i>>,
+    options: &ParserOptions<'_, 'i>,
+    depth: usize,
+  ) -> Result<(), ParseError<'i, ParserError<'i>>> {
+    if depth > 500 {
+      return Err(input.new_custom_error(ParserError::MaximumNestingDepth));
+    }
+
+    loop {
+      let state = input.state();
+      match input.next_including_whitespace_and_comments() {
+        Ok(token @ &cssparser::Token::ParenthesisBlock)
+        | Ok(token @ &cssparser::Token::SquareBracketBlock)
+        | Ok(token @ &cssparser::Token::CurlyBracketBlock) => {
+          tokens.push(Token::from(token).into());
+          let closing_delimiter = match token {
+            cssparser::Token::ParenthesisBlock => Token::CloseParenthesis,
+            cssparser::Token::SquareBracketBlock => Token::CloseSquareBracket,
+            cssparser::Token::CurlyBracketBlock => Token::CloseCurlyBracket,
+            _ => unreachable!(),
+          };
+
+          input.parse_nested_block(|input| TokenList::parse_raw(input, tokens, options, depth + 1))?;
+          tokens.push(closing_delimiter.into());
+        }
+        Ok(token @ &cssparser::Token::Function(_)) => {
+          tokens.push(Token::from(token).into());
+          input.parse_nested_block(|input| TokenList::parse_raw(input, tokens, options, depth + 1))?;
+          tokens.push(Token::CloseParenthesis.into());
+        }
+        Ok(token) if token.is_parse_error() => {
+          return Err(ParseError {
+            kind: ParseErrorKind::Basic(BasicParseErrorKind::UnexpectedToken(token.clone())),
+            location: state.source_location(),
+          })
+        }
+        Ok(token) => {
+          tokens.push(Token::from(token).into());
+        }
+        Err(_) => break,
+      }
+    }
+
+    Ok(())
+  }
+
+  fn parse_into<'t>(
+    input: &mut Parser<'i, 't>,
+    tokens: &mut Vec<TokenOrValue<'i>>,
+    options: &ParserOptions<'_, 'i>,
+    depth: usize,
+  ) -> Result<(), ParseError<'i, ParserError<'i>>> {
+    if depth > 500 {
+      return Err(input.new_custom_error(ParserError::MaximumNestingDepth));
+    }
+
+    let mut last_is_delim = false;
+    let mut last_is_whitespace = false;
+    loop {
+      let state = input.state();
+      match input.next_including_whitespace_and_comments() {
+        Ok(&cssparser::Token::WhiteSpace(..)) | Ok(&cssparser::Token::Comment(..)) => {
+          // Skip whitespace if the last token was a delimiter.
+          // Otherwise, replace all whitespace and comments with a single space character.
+          if !last_is_delim {
+            tokens.push(Token::WhiteSpace(" ".into()).into());
+            last_is_whitespace = true;
+          }
+        }
+        Ok(&cssparser::Token::Function(ref f)) => {
+          // Attempt to parse embedded color values into hex tokens.
+          let f = f.into();
+          if let Some(color) = try_parse_color_token(&f, &state, input) {
+            tokens.push(TokenOrValue::Color(color));
+            last_is_delim = false;
+            last_is_whitespace = false;
+          } else if let Ok(color) = input.try_parse(|input| UnresolvedColor::parse(&f, input, options)) {
+            tokens.push(TokenOrValue::UnresolvedColor(color));
+            last_is_delim = true;
+            last_is_whitespace = false;
+          } else if f == "url" {
+            input.reset(&state);
+            tokens.push(TokenOrValue::Url(Url::parse(input)?));
+            last_is_delim = false;
+            last_is_whitespace = false;
+          } else if f == "var" {
+            let var = input.parse_nested_block(|input| {
+              let var = Variable::parse(input, options, depth + 1)?;
+              Ok(TokenOrValue::Var(var))
+            })?;
+            tokens.push(var);
+            last_is_delim = true;
+            last_is_whitespace = false;
+          } else if f == "env" {
+            let env = input.parse_nested_block(|input| {
+              let env = EnvironmentVariable::parse_nested(input, options, depth + 1)?;
+              Ok(TokenOrValue::Env(env))
+            })?;
+            tokens.push(env);
+            last_is_delim = true;
+            last_is_whitespace = false;
+          } else {
+            let arguments = input.parse_nested_block(|input| TokenList::parse(input, options, depth + 1))?;
+            tokens.push(TokenOrValue::Function(Function {
+              name: Ident(f),
+              arguments,
+            }));
+            last_is_delim = true; // Whitespace is not required after any of these chars.
+            last_is_whitespace = false;
+          }
+        }
+        Ok(&cssparser::Token::Hash(ref h)) | Ok(&cssparser::Token::IDHash(ref h)) => {
+          if let Ok((r, g, b, a)) = parse_hash_color(h.as_bytes()) {
+            tokens.push(TokenOrValue::Color(CssColor::RGBA(RGBA::new(r, g, b, a))));
+          } else {
+            tokens.push(Token::Hash(h.into()).into());
+          }
+          last_is_delim = false;
+          last_is_whitespace = false;
+        }
+        Ok(&cssparser::Token::UnquotedUrl(_)) => {
+          input.reset(&state);
+          tokens.push(TokenOrValue::Url(Url::parse(input)?));
+          last_is_delim = false;
+          last_is_whitespace = false;
+        }
+        Ok(&cssparser::Token::Ident(ref name)) if name.starts_with("--") => {
+          tokens.push(TokenOrValue::DashedIdent(name.into()));
+          last_is_delim = false;
+          last_is_whitespace = false;
+        }
+        Ok(token @ &cssparser::Token::ParenthesisBlock)
+        | Ok(token @ &cssparser::Token::SquareBracketBlock)
+        | Ok(token @ &cssparser::Token::CurlyBracketBlock) => {
+          tokens.push(Token::from(token).into());
+          let closing_delimiter = match token {
+            cssparser::Token::ParenthesisBlock => Token::CloseParenthesis,
+            cssparser::Token::SquareBracketBlock => Token::CloseSquareBracket,
+            cssparser::Token::CurlyBracketBlock => Token::CloseCurlyBracket,
+            _ => unreachable!(),
+          };
+
+          input.parse_nested_block(|input| TokenList::parse_into(input, tokens, options, depth + 1))?;
+
+          tokens.push(closing_delimiter.into());
+          last_is_delim = true; // Whitespace is not required after any of these chars.
+          last_is_whitespace = false;
+        }
+        Ok(token @ cssparser::Token::Dimension { .. }) => {
+          let value = if let Ok(length) = LengthValue::try_from(token) {
+            TokenOrValue::Length(length)
+          } else if let Ok(angle) = Angle::try_from(token) {
+            TokenOrValue::Angle(angle)
+          } else if let Ok(time) = Time::try_from(token) {
+            TokenOrValue::Time(time)
+          } else if let Ok(resolution) = Resolution::try_from(token) {
+            TokenOrValue::Resolution(resolution)
+          } else {
+            TokenOrValue::Token(token.into())
+          };
+          tokens.push(value);
+          last_is_delim = false;
+          last_is_whitespace = false;
+        }
+        Ok(token) if token.is_parse_error() => {
+          return Err(ParseError {
+            kind: ParseErrorKind::Basic(BasicParseErrorKind::UnexpectedToken(token.clone())),
+            location: state.source_location(),
+          })
+        }
+        Ok(token) => {
+          last_is_delim = matches!(token, cssparser::Token::Delim(_) | cssparser::Token::Comma);
+
+          // If this is a delimiter, and the last token was whitespace,
+          // replace the whitespace with the delimiter since both are not required.
+          if last_is_delim && last_is_whitespace {
+            let last = tokens.last_mut().unwrap();
+            *last = Token::from(token).into();
+          } else {
+            tokens.push(Token::from(token).into());
+          }
+
+          last_is_whitespace = false;
+        }
+        Err(_) => break,
+      }
+    }
+
+    Ok(())
+  }
+}
+
+#[inline]
+fn try_parse_color_token<'i, 't>(
+  f: &CowArcStr<'i>,
+  state: &ParserState,
+  input: &mut Parser<'i, 't>,
+) -> Option<CssColor> {
+  match_ignore_ascii_case! { &*f,
+    "rgb" | "rgba" | "hsl" | "hsla" | "hwb" | "lab" | "lch" | "oklab" | "oklch" | "color" | "color-mix" | "light-dark" => {
+      let s = input.state();
+      input.reset(&state);
+      if let Ok(color) = CssColor::parse(input) {
+        return Some(color)
+      }
+      input.reset(&s);
+    },
+    _ => {}
+  }
+
+  None
+}
+
+impl<'i> TokenList<'i> {
+  pub(crate) fn to_css<W>(&self, dest: &mut Printer<W>, is_custom_property: bool) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    if !dest.minify && self.0.len() == 1 && matches!(self.0.first(), Some(token) if token.is_whitespace()) {
+      return Ok(());
+    }
+
+    let mut has_whitespace = false;
+    for (i, token_or_value) in self.0.iter().enumerate() {
+      has_whitespace = match token_or_value {
+        TokenOrValue::Color(color) => {
+          color.to_css(dest)?;
+          false
+        }
+        TokenOrValue::UnresolvedColor(color) => {
+          color.to_css(dest, is_custom_property)?;
+          false
+        }
+        TokenOrValue::Url(url) => {
+          if dest.dependencies.is_some() && is_custom_property && !url.is_absolute() {
+            return Err(dest.error(
+              PrinterErrorKind::AmbiguousUrlInCustomProperty {
+                url: url.url.as_ref().to_owned(),
+              },
+              url.loc,
+            ));
+          }
+          url.to_css(dest)?;
+          false
+        }
+        TokenOrValue::Var(var) => {
+          var.to_css(dest, is_custom_property)?;
+          self.write_whitespace_if_needed(i, dest)?
+        }
+        TokenOrValue::Env(env) => {
+          env.to_css(dest, is_custom_property)?;
+          self.write_whitespace_if_needed(i, dest)?
+        }
+        TokenOrValue::Function(f) => {
+          f.to_css(dest, is_custom_property)?;
+          self.write_whitespace_if_needed(i, dest)?
+        }
+        TokenOrValue::Length(v) => {
+          // Do not serialize unitless zero lengths in custom properties as it may break calc().
+          let (value, unit) = v.to_unit_value();
+          serialize_dimension(value, unit, dest)?;
+          false
+        }
+        TokenOrValue::Angle(v) => {
+          v.to_css(dest)?;
+          false
+        }
+        TokenOrValue::Time(v) => {
+          v.to_css(dest)?;
+          false
+        }
+        TokenOrValue::Resolution(v) => {
+          v.to_css(dest)?;
+          false
+        }
+        TokenOrValue::DashedIdent(v) => {
+          v.to_css(dest)?;
+          false
+        }
+        TokenOrValue::AnimationName(v) => {
+          v.to_css(dest)?;
+          false
+        }
+        TokenOrValue::Token(token) => match token {
+          Token::Delim(d) => {
+            if *d == '+' || *d == '-' {
+              dest.write_char(' ')?;
+              dest.write_char(*d)?;
+              dest.write_char(' ')?;
+            } else {
+              let ws_before = !has_whitespace && (*d == '/' || *d == '*');
+              dest.delim(*d, ws_before)?;
+            }
+            true
+          }
+          Token::Comma => {
+            dest.delim(',', false)?;
+            true
+          }
+          Token::CloseParenthesis | Token::CloseSquareBracket | Token::CloseCurlyBracket => {
+            token.to_css(dest)?;
+            self.write_whitespace_if_needed(i, dest)?
+          }
+          Token::Dimension { value, unit, .. } => {
+            serialize_dimension(*value, unit, dest)?;
+            false
+          }
+          Token::Number { value, .. } => {
+            value.to_css(dest)?;
+            false
+          }
+          _ => {
+            token.to_css(dest)?;
+            matches!(token, Token::WhiteSpace(..))
+          }
+        },
+      };
+    }
+
+    Ok(())
+  }
+
+  pub(crate) fn to_css_raw<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    for token_or_value in &self.0 {
+      match token_or_value {
+        TokenOrValue::Token(token) => {
+          token.to_css(dest)?;
+        }
+        _ => {
+          return Err(PrinterError {
+            kind: PrinterErrorKind::FmtError,
+            loc: None,
+          })
+        }
+      }
+    }
+
+    Ok(())
+  }
+
+  #[inline]
+  fn write_whitespace_if_needed<W>(&self, i: usize, dest: &mut Printer<W>) -> Result<bool, PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    if !dest.minify
+      && i != self.0.len() - 1
+      && !matches!(
+        self.0[i + 1],
+        TokenOrValue::Token(Token::Comma) | TokenOrValue::Token(Token::CloseParenthesis)
+      )
+    {
+      // Whitespace is removed during parsing, so add it back if we aren't minifying.
+      dest.write_char(' ')?;
+      Ok(true)
+    } else {
+      Ok(false)
+    }
+  }
+}
+
+/// A raw CSS token.
+// Copied from cssparser to change CowRcStr to CowArcStr
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub enum Token<'a> {
+  /// A [`<ident-token>`](https://drafts.csswg.org/css-syntax/#ident-token-diagram)
+  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<CowArcStr>"))]
+  Ident(#[cfg_attr(feature = "serde", serde(borrow))] CowArcStr<'a>),
+
+  /// A [`<at-keyword-token>`](https://drafts.csswg.org/css-syntax/#at-keyword-token-diagram)
+  ///
+  /// The value does not include the `@` marker.
+  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<CowArcStr>"))]
+  AtKeyword(CowArcStr<'a>),
+
+  /// A [`<hash-token>`](https://drafts.csswg.org/css-syntax/#hash-token-diagram) with the type flag set to "unrestricted"
+  ///
+  /// The value does not include the `#` marker.
+  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<CowArcStr>"))]
+  Hash(CowArcStr<'a>),
+
+  /// A [`<hash-token>`](https://drafts.csswg.org/css-syntax/#hash-token-diagram) with the type flag set to "id"
+  ///
+  /// The value does not include the `#` marker.
+  #[cfg_attr(feature = "serde", serde(rename = "id-hash", with = "ValueWrapper::<CowArcStr>"))]
+  IDHash(CowArcStr<'a>), // Hash that is a valid ID selector.
+
+  /// A [`<string-token>`](https://drafts.csswg.org/css-syntax/#string-token-diagram)
+  ///
+  /// The value does not include the quotes.
+  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<CowArcStr>"))]
+  String(CowArcStr<'a>),
+
+  /// A [`<url-token>`](https://drafts.csswg.org/css-syntax/#url-token-diagram)
+  ///
+  /// The value does not include the `url(` `)` markers.  Note that `url( <string-token> )` is represented by a
+  /// `Function` token.
+  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<CowArcStr>"))]
+  UnquotedUrl(CowArcStr<'a>),
+
+  /// A `<delim-token>`
+  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<char>"))]
+  Delim(char),
+
+  /// A [`<number-token>`](https://drafts.csswg.org/css-syntax/#number-token-diagram)
+  Number {
+    /// Whether the number had a `+` or `-` sign.
+    ///
+    /// This is used is some cases like the <An+B> micro syntax. (See the `parse_nth` function.)
+    #[cfg_attr(feature = "serde", serde(skip))]
+    has_sign: bool,
+
+    /// The value as a float
+    value: f32,
+
+    /// If the origin source did not include a fractional part, the value as an integer.
+    #[cfg_attr(feature = "serde", serde(skip))]
+    int_value: Option<i32>,
+  },
+
+  /// A [`<percentage-token>`](https://drafts.csswg.org/css-syntax/#percentage-token-diagram)
+  Percentage {
+    /// Whether the number had a `+` or `-` sign.
+    #[cfg_attr(feature = "serde", serde(skip))]
+    has_sign: bool,
+
+    /// The value as a float, divided by 100 so that the nominal range is 0.0 to 1.0.
+    #[cfg_attr(feature = "serde", serde(rename = "value"))]
+    unit_value: f32,
+
+    /// If the origin source did not include a fractional part, the value as an integer.
+    /// It is **not** divided by 100.
+    #[cfg_attr(feature = "serde", serde(skip))]
+    int_value: Option<i32>,
+  },
+
+  /// A [`<dimension-token>`](https://drafts.csswg.org/css-syntax/#dimension-token-diagram)
+  Dimension {
+    /// Whether the number had a `+` or `-` sign.
+    ///
+    /// This is used is some cases like the <An+B> micro syntax. (See the `parse_nth` function.)
+    #[cfg_attr(feature = "serde", serde(skip))]
+    has_sign: bool,
+
+    /// The value as a float
+    value: f32,
+
+    /// If the origin source did not include a fractional part, the value as an integer.
+    #[cfg_attr(feature = "serde", serde(skip))]
+    int_value: Option<i32>,
+
+    /// The unit, e.g. "px" in `12px`
+    unit: CowArcStr<'a>,
+  },
+
+  /// A [`<whitespace-token>`](https://drafts.csswg.org/css-syntax/#whitespace-token-diagram)
+  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<CowArcStr>"))]
+  WhiteSpace(CowArcStr<'a>),
+
+  /// A comment.
+  ///
+  /// The CSS Syntax spec does not generate tokens for comments,
+  /// But we do, because we can (borrowed &str makes it cheap).
+  ///
+  /// The value does not include the `/*` `*/` markers.
+  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<CowArcStr>"))]
+  Comment(CowArcStr<'a>),
+
+  /// A `:` `<colon-token>`
+  Colon, // :
+
+  /// A `;` `<semicolon-token>`
+  Semicolon, // ;
+
+  /// A `,` `<comma-token>`
+  Comma, // ,
+
+  /// A `~=` [`<include-match-token>`](https://drafts.csswg.org/css-syntax/#include-match-token-diagram)
+  IncludeMatch,
+
+  /// A `|=` [`<dash-match-token>`](https://drafts.csswg.org/css-syntax/#dash-match-token-diagram)
+  DashMatch,
+
+  /// A `^=` [`<prefix-match-token>`](https://drafts.csswg.org/css-syntax/#prefix-match-token-diagram)
+  PrefixMatch,
+
+  /// A `$=` [`<suffix-match-token>`](https://drafts.csswg.org/css-syntax/#suffix-match-token-diagram)
+  SuffixMatch,
+
+  /// A `*=` [`<substring-match-token>`](https://drafts.csswg.org/css-syntax/#substring-match-token-diagram)
+  SubstringMatch,
+
+  /// A `<!--` [`<CDO-token>`](https://drafts.csswg.org/css-syntax/#CDO-token-diagram)
+  #[cfg_attr(feature = "serde", serde(rename = "cdo"))]
+  CDO,
+
+  /// A `-->` [`<CDC-token>`](https://drafts.csswg.org/css-syntax/#CDC-token-diagram)
+  #[cfg_attr(feature = "serde", serde(rename = "cdc"))]
+  CDC,
+
+  /// A [`<function-token>`](https://drafts.csswg.org/css-syntax/#function-token-diagram)
+  ///
+  /// The value (name) does not include the `(` marker.
+  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<CowArcStr>"))]
+  Function(CowArcStr<'a>),
+
+  /// A `<(-token>`
+  ParenthesisBlock,
+
+  /// A `<[-token>`
+  SquareBracketBlock,
+
+  /// A `<{-token>`
+  CurlyBracketBlock,
+
+  /// A `<bad-url-token>`
+  ///
+  /// This token always indicates a parse error.
+  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<CowArcStr>"))]
+  BadUrl(CowArcStr<'a>),
+
+  /// A `<bad-string-token>`
+  ///
+  /// This token always indicates a parse error.
+  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<CowArcStr>"))]
+  BadString(CowArcStr<'a>),
+
+  /// A `<)-token>`
+  ///
+  /// When obtained from one of the `Parser::next*` methods,
+  /// this token is always unmatched and indicates a parse error.
+  CloseParenthesis,
+
+  /// A `<]-token>`
+  ///
+  /// When obtained from one of the `Parser::next*` methods,
+  /// this token is always unmatched and indicates a parse error.
+  CloseSquareBracket,
+
+  /// A `<}-token>`
+  ///
+  /// When obtained from one of the `Parser::next*` methods,
+  /// this token is always unmatched and indicates a parse error.
+  CloseCurlyBracket,
+}
+
+impl<'a> From<&cssparser::Token<'a>> for Token<'a> {
+  #[inline]
+  fn from(t: &cssparser::Token<'a>) -> Token<'a> {
+    match t {
+      cssparser::Token::Ident(x) => Token::Ident(x.into()),
+      cssparser::Token::AtKeyword(x) => Token::AtKeyword(x.into()),
+      cssparser::Token::Hash(x) => Token::Hash(x.into()),
+      cssparser::Token::IDHash(x) => Token::IDHash(x.into()),
+      cssparser::Token::QuotedString(x) => Token::String(x.into()),
+      cssparser::Token::UnquotedUrl(x) => Token::UnquotedUrl(x.into()),
+      cssparser::Token::Function(x) => Token::Function(x.into()),
+      cssparser::Token::BadUrl(x) => Token::BadUrl(x.into()),
+      cssparser::Token::BadString(x) => Token::BadString(x.into()),
+      cssparser::Token::Delim(c) => Token::Delim(*c),
+      cssparser::Token::Number {
+        has_sign,
+        value,
+        int_value,
+      } => Token::Number {
+        has_sign: *has_sign,
+        value: *value,
+        int_value: *int_value,
+      },
+      cssparser::Token::Dimension {
+        has_sign,
+        value,
+        int_value,
+        unit,
+      } => Token::Dimension {
+        has_sign: *has_sign,
+        value: *value,
+        int_value: *int_value,
+        unit: unit.into(),
+      },
+      cssparser::Token::Percentage {
+        has_sign,
+        unit_value,
+        int_value,
+      } => Token::Percentage {
+        has_sign: *has_sign,
+        unit_value: *unit_value,
+        int_value: *int_value,
+      },
+      cssparser::Token::WhiteSpace(w) => Token::WhiteSpace((*w).into()),
+      cssparser::Token::Comment(c) => Token::Comment((*c).into()),
+      cssparser::Token::Colon => Token::Colon,
+      cssparser::Token::Semicolon => Token::Semicolon,
+      cssparser::Token::Comma => Token::Comma,
+      cssparser::Token::IncludeMatch => Token::IncludeMatch,
+      cssparser::Token::DashMatch => Token::DashMatch,
+      cssparser::Token::PrefixMatch => Token::PrefixMatch,
+      cssparser::Token::SuffixMatch => Token::SuffixMatch,
+      cssparser::Token::SubstringMatch => Token::SubstringMatch,
+      cssparser::Token::CDO => Token::CDO,
+      cssparser::Token::CDC => Token::CDC,
+      cssparser::Token::ParenthesisBlock => Token::ParenthesisBlock,
+      cssparser::Token::SquareBracketBlock => Token::SquareBracketBlock,
+      cssparser::Token::CurlyBracketBlock => Token::CurlyBracketBlock,
+      cssparser::Token::CloseParenthesis => Token::CloseParenthesis,
+      cssparser::Token::CloseSquareBracket => Token::CloseSquareBracket,
+      cssparser::Token::CloseCurlyBracket => Token::CloseCurlyBracket,
+    }
+  }
+}
+
+impl<'a> ToCss for Token<'a> {
+  #[inline]
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    use cssparser::ToCss;
+    match self {
+      Token::Ident(x) => cssparser::Token::Ident(x.as_ref().into()).to_css(dest)?,
+      Token::AtKeyword(x) => cssparser::Token::AtKeyword(x.as_ref().into()).to_css(dest)?,
+      Token::Hash(x) => cssparser::Token::Hash(x.as_ref().into()).to_css(dest)?,
+      Token::IDHash(x) => cssparser::Token::IDHash(x.as_ref().into()).to_css(dest)?,
+      Token::String(x) => cssparser::Token::QuotedString(x.as_ref().into()).to_css(dest)?,
+      Token::UnquotedUrl(x) => cssparser::Token::UnquotedUrl(x.as_ref().into()).to_css(dest)?,
+      Token::Function(x) => cssparser::Token::Function(x.as_ref().into()).to_css(dest)?,
+      Token::BadUrl(x) => cssparser::Token::BadUrl(x.as_ref().into()).to_css(dest)?,
+      Token::BadString(x) => cssparser::Token::BadString(x.as_ref().into()).to_css(dest)?,
+      Token::Delim(c) => cssparser::Token::Delim(*c).to_css(dest)?,
+      Token::Number {
+        has_sign,
+        value,
+        int_value,
+      } => cssparser::Token::Number {
+        has_sign: *has_sign,
+        value: *value,
+        int_value: *int_value,
+      }
+      .to_css(dest)?,
+      Token::Dimension {
+        has_sign,
+        value,
+        int_value,
+        unit,
+      } => cssparser::Token::Dimension {
+        has_sign: *has_sign,
+        value: *value,
+        int_value: *int_value,
+        unit: unit.as_ref().into(),
+      }
+      .to_css(dest)?,
+      Token::Percentage {
+        has_sign,
+        unit_value,
+        int_value,
+      } => cssparser::Token::Percentage {
+        has_sign: *has_sign,
+        unit_value: *unit_value,
+        int_value: *int_value,
+      }
+      .to_css(dest)?,
+      Token::WhiteSpace(w) => cssparser::Token::WhiteSpace(w).to_css(dest)?,
+      Token::Comment(c) => cssparser::Token::Comment(c).to_css(dest)?,
+      Token::Colon => cssparser::Token::Colon.to_css(dest)?,
+      Token::Semicolon => cssparser::Token::Semicolon.to_css(dest)?,
+      Token::Comma => cssparser::Token::Comma.to_css(dest)?,
+      Token::IncludeMatch => cssparser::Token::IncludeMatch.to_css(dest)?,
+      Token::DashMatch => cssparser::Token::DashMatch.to_css(dest)?,
+      Token::PrefixMatch => cssparser::Token::PrefixMatch.to_css(dest)?,
+      Token::SuffixMatch => cssparser::Token::SuffixMatch.to_css(dest)?,
+      Token::SubstringMatch => cssparser::Token::SubstringMatch.to_css(dest)?,
+      Token::CDO => cssparser::Token::CDO.to_css(dest)?,
+      Token::CDC => cssparser::Token::CDC.to_css(dest)?,
+      Token::ParenthesisBlock => cssparser::Token::ParenthesisBlock.to_css(dest)?,
+      Token::SquareBracketBlock => cssparser::Token::SquareBracketBlock.to_css(dest)?,
+      Token::CurlyBracketBlock => cssparser::Token::CurlyBracketBlock.to_css(dest)?,
+      Token::CloseParenthesis => cssparser::Token::CloseParenthesis.to_css(dest)?,
+      Token::CloseSquareBracket => cssparser::Token::CloseSquareBracket.to_css(dest)?,
+      Token::CloseCurlyBracket => cssparser::Token::CloseCurlyBracket.to_css(dest)?,
+    }
+
+    Ok(())
+  }
+}
+
+impl<'a> Eq for Token<'a> {}
+
+impl<'a> std::hash::Hash for Token<'a> {
+  fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+    let tag = std::mem::discriminant(self);
+    tag.hash(state);
+    match self {
+      Token::Ident(x) => x.hash(state),
+      Token::AtKeyword(x) => x.hash(state),
+      Token::Hash(x) => x.hash(state),
+      Token::IDHash(x) => x.hash(state),
+      Token::String(x) => x.hash(state),
+      Token::UnquotedUrl(x) => x.hash(state),
+      Token::Function(x) => x.hash(state),
+      Token::BadUrl(x) => x.hash(state),
+      Token::BadString(x) => x.hash(state),
+      Token::Delim(x) => x.hash(state),
+      Token::Number {
+        has_sign,
+        value,
+        int_value,
+      } => {
+        has_sign.hash(state);
+        integer_decode(*value).hash(state);
+        int_value.hash(state);
+      }
+      Token::Dimension {
+        has_sign,
+        value,
+        int_value,
+        unit,
+      } => {
+        has_sign.hash(state);
+        integer_decode(*value).hash(state);
+        int_value.hash(state);
+        unit.hash(state);
+      }
+      Token::Percentage {
+        has_sign,
+        unit_value,
+        int_value,
+      } => {
+        has_sign.hash(state);
+        integer_decode(*unit_value).hash(state);
+        int_value.hash(state);
+      }
+      Token::WhiteSpace(w) => w.hash(state),
+      Token::Comment(c) => c.hash(state),
+      Token::Colon
+      | Token::Semicolon
+      | Token::Comma
+      | Token::IncludeMatch
+      | Token::DashMatch
+      | Token::PrefixMatch
+      | Token::SuffixMatch
+      | Token::SubstringMatch
+      | Token::CDO
+      | Token::CDC
+      | Token::ParenthesisBlock
+      | Token::SquareBracketBlock
+      | Token::CurlyBracketBlock
+      | Token::CloseParenthesis
+      | Token::CloseSquareBracket
+      | Token::CloseCurlyBracket => {}
+    }
+  }
+}
+
+/// Converts a floating point value into its mantissa, exponent,
+/// and sign components so that it can be hashed.
+fn integer_decode(v: f32) -> (u32, i16, i8) {
+  let bits: u32 = unsafe { std::mem::transmute(v) };
+  let sign: i8 = if bits >> 31 == 0 { 1 } else { -1 };
+  let mut exponent: i16 = ((bits >> 23) & 0xff) as i16;
+  let mantissa = if exponent == 0 {
+    (bits & 0x7fffff) << 1
+  } else {
+    (bits & 0x7fffff) | 0x800000
+  };
+  // Exponent bias + mantissa shift
+  exponent -= 127 + 23;
+  (mantissa, exponent, sign)
+}
+
+impl<'i> TokenList<'i> {
+  pub(crate) fn get_necessary_fallbacks(&self, targets: Targets) -> ColorFallbackKind {
+    let mut fallbacks = ColorFallbackKind::empty();
+    for token in &self.0 {
+      match token {
+        TokenOrValue::Color(color) => {
+          fallbacks |= color.get_possible_fallbacks(targets);
+        }
+        TokenOrValue::Function(f) => {
+          fallbacks |= f.arguments.get_necessary_fallbacks(targets);
+        }
+        TokenOrValue::Var(v) => {
+          if let Some(fallback) = &v.fallback {
+            fallbacks |= fallback.get_necessary_fallbacks(targets);
+          }
+        }
+        TokenOrValue::Env(v) => {
+          if let Some(fallback) = &v.fallback {
+            fallbacks |= fallback.get_necessary_fallbacks(targets);
+          }
+        }
+        _ => {}
+      }
+    }
+
+    fallbacks
+  }
+
+  pub(crate) fn get_fallback(&self, kind: ColorFallbackKind) -> Self {
+    let tokens = self
+      .0
+      .iter()
+      .map(|token| match token {
+        TokenOrValue::Color(color) => TokenOrValue::Color(color.get_fallback(kind)),
+        TokenOrValue::Function(f) => TokenOrValue::Function(f.get_fallback(kind)),
+        TokenOrValue::Var(v) => TokenOrValue::Var(v.get_fallback(kind)),
+        TokenOrValue::Env(e) => TokenOrValue::Env(e.get_fallback(kind)),
+        _ => token.clone(),
+      })
+      .collect();
+    TokenList(tokens)
+  }
+
+  pub(crate) fn get_fallbacks(&mut self, targets: Targets) -> Vec<(SupportsCondition<'i>, Self)> {
+    // Get the full list of possible fallbacks, and remove the lowest one, which will replace
+    // the original declaration. The remaining fallbacks need to be added as @supports rules.
+    let mut fallbacks = self.get_necessary_fallbacks(targets);
+    let lowest_fallback = fallbacks.lowest();
+    fallbacks.remove(lowest_fallback);
+
+    let mut res = Vec::new();
+    if fallbacks.contains(ColorFallbackKind::P3) {
+      res.push((
+        ColorFallbackKind::P3.supports_condition(),
+        self.get_fallback(ColorFallbackKind::P3),
+      ));
+    }
+
+    if fallbacks.contains(ColorFallbackKind::LAB) {
+      res.push((
+        ColorFallbackKind::LAB.supports_condition(),
+        self.get_fallback(ColorFallbackKind::LAB),
+      ));
+    }
+
+    if !lowest_fallback.is_empty() {
+      for token in self.0.iter_mut() {
+        match token {
+          TokenOrValue::Color(color) => {
+            *color = color.get_fallback(lowest_fallback);
+          }
+          TokenOrValue::Function(f) => *f = f.get_fallback(lowest_fallback),
+          TokenOrValue::Var(v) if v.fallback.is_some() => *v = v.get_fallback(lowest_fallback),
+          TokenOrValue::Env(v) if v.fallback.is_some() => *v = v.get_fallback(lowest_fallback),
+          _ => {}
+        }
+      }
+    }
+
+    res
+  }
+
+  pub(crate) fn get_features(&self) -> Features {
+    let mut features = Features::empty();
+    for token in &self.0 {
+      match token {
+        TokenOrValue::Color(color) => {
+          features |= color.get_features();
+        }
+        TokenOrValue::UnresolvedColor(unresolved_color) => {
+          features |= Features::SpaceSeparatedColorNotation;
+          match unresolved_color {
+            UnresolvedColor::LightDark { light, dark } => {
+              features |= Features::LightDark;
+              features |= light.get_features();
+              features |= dark.get_features();
+            }
+            _ => {}
+          }
+        }
+        TokenOrValue::Function(f) => {
+          features |= f.arguments.get_features();
+        }
+        TokenOrValue::Var(v) => {
+          if let Some(fallback) = &v.fallback {
+            features |= fallback.get_features();
+          }
+        }
+        TokenOrValue::Env(v) => {
+          if let Some(fallback) = &v.fallback {
+            features |= fallback.get_features();
+          }
+        }
+        _ => {}
+      }
+    }
+
+    features
+  }
+
+  /// Substitutes variables with the provided values.
+  #[cfg(feature = "substitute_variables")]
+  #[cfg_attr(docsrs, doc(cfg(feature = "substitute_variables")))]
+  pub fn substitute_variables(&mut self, vars: &std::collections::HashMap<&str, TokenList<'i>>) {
+    self.visit(&mut VarInliner { vars }).unwrap()
+  }
+}
+
+#[cfg(feature = "substitute_variables")]
+struct VarInliner<'a, 'i> {
+  vars: &'a std::collections::HashMap<&'a str, TokenList<'i>>,
+}
+
+#[cfg(feature = "substitute_variables")]
+impl<'a, 'i> crate::visitor::Visitor<'i> for VarInliner<'a, 'i> {
+  type Error = std::convert::Infallible;
+
+  fn visit_types(&self) -> crate::visitor::VisitTypes {
+    crate::visit_types!(TOKENS | VARIABLES)
+  }
+
+  fn visit_token_list(&mut self, tokens: &mut TokenList<'i>) -> Result<(), Self::Error> {
+    let mut i = 0;
+    let mut seen = std::collections::HashSet::new();
+    while i < tokens.0.len() {
+      let token = &mut tokens.0[i];
+      token.visit(self).unwrap();
+      if let TokenOrValue::Var(var) = token {
+        if let Some(value) = self.vars.get(var.name.ident.0.as_ref()) {
+          // Ignore circular references.
+          if seen.insert(var.name.ident.0.clone()) {
+            tokens.0.splice(i..i + 1, value.0.iter().cloned());
+            // Don't advance. We need to replace any variables in the value.
+            continue;
+          }
+        } else if let Some(fallback) = &var.fallback {
+          let fallback = fallback.0.clone();
+          if seen.insert(var.name.ident.0.clone()) {
+            tokens.0.splice(i..i + 1, fallback.into_iter());
+            continue;
+          }
+        }
+      }
+      seen.clear();
+      i += 1;
+    }
+    Ok(())
+  }
+}
+
+/// A CSS variable reference.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(feature = "visitor", visit(visit_variable, VARIABLES))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub struct Variable<'i> {
+  /// The variable name.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  pub name: DashedIdentReference<'i>,
+  /// A fallback value in case the variable is not defined.
+  pub fallback: Option<TokenList<'i>>,
+}
+
+impl<'i> Variable<'i> {
+  fn parse<'t>(
+    input: &mut Parser<'i, 't>,
+    options: &ParserOptions<'_, 'i>,
+    depth: usize,
+  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let name = DashedIdentReference::parse_with_options(input, options)?;
+
+    let fallback = if input.try_parse(|input| input.expect_comma()).is_ok() {
+      Some(TokenList::parse(input, options, depth)?)
+    } else {
+      None
+    };
+
+    Ok(Variable { name, fallback })
+  }
+
+  fn to_css<W>(&self, dest: &mut Printer<W>, is_custom_property: bool) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    dest.write_str("var(")?;
+    self.name.to_css(dest)?;
+    if let Some(fallback) = &self.fallback {
+      dest.delim(',', false)?;
+      fallback.to_css(dest, is_custom_property)?;
+    }
+    dest.write_char(')')
+  }
+
+  fn get_fallback(&self, kind: ColorFallbackKind) -> Self {
+    Variable {
+      name: self.name.clone(),
+      fallback: self.fallback.as_ref().map(|fallback| fallback.get_fallback(kind)),
+    }
+  }
+}
+
+/// A CSS environment variable reference.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(
+  feature = "visitor",
+  derive(Visit),
+  visit(visit_environment_variable, ENVIRONMENT_VARIABLES)
+)]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub struct EnvironmentVariable<'i> {
+  /// The environment variable name.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  pub name: EnvironmentVariableName<'i>,
+  /// Optional indices into the dimensions of the environment variable.
+  #[cfg_attr(feature = "serde", serde(default))]
+  pub indices: Vec<CSSInteger>,
+  /// A fallback value in case the variable is not defined.
+  pub fallback: Option<TokenList<'i>>,
+}
+
+/// A CSS environment variable name.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", rename_all = "lowercase")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub enum EnvironmentVariableName<'i> {
+  /// A UA-defined environment variable.
+  #[cfg_attr(
+    feature = "serde",
+    serde(with = "crate::serialization::ValueWrapper::<UAEnvironmentVariable>")
+  )]
+  UA(UAEnvironmentVariable),
+  /// A custom author-defined environment variable.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  Custom(DashedIdentReference<'i>),
+  /// An unknown environment variable.
+  #[cfg_attr(feature = "serde", serde(with = "crate::serialization::ValueWrapper::<CustomIdent>"))]
+  Unknown(CustomIdent<'i>),
+}
+
+enum_property! {
+  /// A UA-defined environment variable name.
+  pub enum UAEnvironmentVariable {
+    /// The safe area inset from the top of the viewport.
+    SafeAreaInsetTop,
+    /// The safe area inset from the right of the viewport.
+    SafeAreaInsetRight,
+    /// The safe area inset from the bottom of the viewport.
+    SafeAreaInsetBottom,
+    /// The safe area inset from the left of the viewport.
+    SafeAreaInsetLeft,
+    /// The viewport segment width.
+    ViewportSegmentWidth,
+    /// The viewport segment height.
+    ViewportSegmentHeight,
+    /// The viewport segment top position.
+    ViewportSegmentTop,
+    /// The viewport segment left position.
+    ViewportSegmentLeft,
+    /// The viewport segment bottom position.
+    ViewportSegmentBottom,
+    /// The viewport segment right position.
+    ViewportSegmentRight,
+  }
+}
+
+impl<'i> EnvironmentVariableName<'i> {
+  /// Returns the name of the environment variable as a string.
+  pub fn name(&self) -> &str {
+    match self {
+      EnvironmentVariableName::UA(ua) => ua.as_str(),
+      EnvironmentVariableName::Custom(c) => c.ident.as_ref(),
+      EnvironmentVariableName::Unknown(u) => u.0.as_ref(),
+    }
+  }
+}
+
+impl<'i> Parse<'i> for EnvironmentVariableName<'i> {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    if let Ok(ua) = input.try_parse(UAEnvironmentVariable::parse) {
+      return Ok(EnvironmentVariableName::UA(ua));
+    }
+
+    if let Ok(dashed) =
+      input.try_parse(|input| DashedIdentReference::parse_with_options(input, &ParserOptions::default()))
+    {
+      return Ok(EnvironmentVariableName::Custom(dashed));
+    }
+
+    let ident = CustomIdent::parse(input)?;
+    return Ok(EnvironmentVariableName::Unknown(ident));
+  }
+}
+
+impl<'i> ToCss for EnvironmentVariableName<'i> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    match self {
+      EnvironmentVariableName::UA(ua) => ua.to_css(dest),
+      EnvironmentVariableName::Custom(custom) => custom.to_css(dest),
+      EnvironmentVariableName::Unknown(unknown) => unknown.to_css(dest),
+    }
+  }
+}
+
+impl<'i> EnvironmentVariable<'i> {
+  pub(crate) fn parse<'t>(
+    input: &mut Parser<'i, 't>,
+    options: &ParserOptions<'_, 'i>,
+    depth: usize,
+  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    input.expect_function_matching("env")?;
+    input.parse_nested_block(|input| Self::parse_nested(input, options, depth))
+  }
+
+  pub(crate) fn parse_nested<'t>(
+    input: &mut Parser<'i, 't>,
+    options: &ParserOptions<'_, 'i>,
+    depth: usize,
+  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let name = EnvironmentVariableName::parse(input)?;
+    let mut indices = Vec::new();
+    while let Ok(index) = input.try_parse(CSSInteger::parse) {
+      indices.push(index);
+    }
+
+    let fallback = if input.try_parse(|input| input.expect_comma()).is_ok() {
+      Some(TokenList::parse(input, options, depth + 1)?)
+    } else {
+      None
+    };
+
+    Ok(EnvironmentVariable {
+      name,
+      indices,
+      fallback,
+    })
+  }
+
+  pub(crate) fn to_css<W>(&self, dest: &mut Printer<W>, is_custom_property: bool) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    dest.write_str("env(")?;
+    self.name.to_css(dest)?;
+
+    for item in &self.indices {
+      dest.write_char(' ')?;
+      item.to_css(dest)?;
+    }
+
+    if let Some(fallback) = &self.fallback {
+      dest.delim(',', false)?;
+      fallback.to_css(dest, is_custom_property)?;
+    }
+    dest.write_char(')')
+  }
+
+  fn get_fallback(&self, kind: ColorFallbackKind) -> Self {
+    EnvironmentVariable {
+      name: self.name.clone(),
+      indices: self.indices.clone(),
+      fallback: self.fallback.as_ref().map(|fallback| fallback.get_fallback(kind)),
+    }
+  }
+}
+
+/// A custom CSS function.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(feature = "visitor", visit(visit_function, FUNCTIONS))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub struct Function<'i> {
+  /// The function name.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  pub name: Ident<'i>,
+  /// The function arguments.
+  pub arguments: TokenList<'i>,
+}
+
+impl<'i> Function<'i> {
+  fn to_css<W>(&self, dest: &mut Printer<W>, is_custom_property: bool) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    self.name.to_css(dest)?;
+    dest.write_char('(')?;
+    self.arguments.to_css(dest, is_custom_property)?;
+    dest.write_char(')')
+  }
+
+  fn get_fallback(&self, kind: ColorFallbackKind) -> Self {
+    Function {
+      name: self.name.clone(),
+      arguments: self.arguments.get_fallback(kind),
+    }
+  }
+}
+
+/// A color value with an unresolved alpha value (e.g. a variable).
+/// These can be converted from the modern slash syntax to older comma syntax.
+/// This can only be done when the only unresolved component is the alpha
+/// since variables can resolve to multiple tokens.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", rename_all = "lowercase")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub enum UnresolvedColor<'i> {
+  /// An rgb() color.
+  RGB {
+    /// The red component.
+    r: f32,
+    /// The green component.
+    g: f32,
+    /// The blue component.
+    b: f32,
+    /// The unresolved alpha component.
+    #[cfg_attr(feature = "serde", serde(borrow))]
+    alpha: TokenList<'i>,
+  },
+  /// An hsl() color.
+  HSL {
+    /// The hue component.
+    h: f32,
+    /// The saturation component.
+    s: f32,
+    /// The lightness component.
+    l: f32,
+    /// The unresolved alpha component.
+    #[cfg_attr(feature = "serde", serde(borrow))]
+    alpha: TokenList<'i>,
+  },
+  /// The light-dark() function.
+  #[cfg_attr(feature = "serde", serde(rename = "light-dark"))]
+  LightDark {
+    /// The light value.
+    light: TokenList<'i>,
+    /// The dark value.
+    dark: TokenList<'i>,
+  },
+}
+
+impl<'i> LightDarkColor for UnresolvedColor<'i> {
+  #[inline]
+  fn light_dark(light: Self, dark: Self) -> Self {
+    UnresolvedColor::LightDark {
+      light: TokenList(vec![TokenOrValue::UnresolvedColor(light)]),
+      dark: TokenList(vec![TokenOrValue::UnresolvedColor(dark)]),
+    }
+  }
+}
+
+impl<'i> UnresolvedColor<'i> {
+  fn parse<'t>(
+    f: &CowArcStr<'i>,
+    input: &mut Parser<'i, 't>,
+    options: &ParserOptions<'_, 'i>,
+  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let mut parser = ComponentParser::new(false);
+    match_ignore_ascii_case! { &*f,
+      "rgb" => {
+        input.parse_nested_block(|input| {
+          parser.parse_relative::<RGB, _, _>(input, |input, parser| {
+            let (r, g, b, is_legacy) = parse_rgb_components(input, parser)?;
+            if is_legacy {
+              return Err(input.new_custom_error(ParserError::InvalidValue))
+            }
+            input.expect_delim('/')?;
+            let alpha = TokenList::parse(input, options, 0)?;
+            Ok(UnresolvedColor::RGB { r, g, b, alpha })
+          })
+        })
+      },
+      "hsl" => {
+        input.parse_nested_block(|input| {
+          parser.parse_relative::<HSL, _, _>(input, |input, parser| {
+            let (h, s, l, is_legacy) = parse_hsl_hwb_components::<HSL>(input, parser, false)?;
+            if is_legacy {
+              return Err(input.new_custom_error(ParserError::InvalidValue))
+            }
+            input.expect_delim('/')?;
+            let alpha = TokenList::parse(input, options, 0)?;
+            Ok(UnresolvedColor::HSL { h, s, l, alpha })
+          })
+        })
+      },
+      "light-dark" => {
+        input.parse_nested_block(|input| {
+          let light = input.parse_until_before(Delimiter::Comma, |input|
+            TokenList::parse(input, options, 0)
+          )?;
+          input.expect_comma()?;
+          let dark = TokenList::parse(input, options, 0)?;
+          Ok(UnresolvedColor::LightDark { light, dark })
+        })
+      },
+      _ => Err(input.new_custom_error(ParserError::InvalidValue))
+    }
+  }
+
+  fn to_css<W>(&self, dest: &mut Printer<W>, is_custom_property: bool) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    match self {
+      UnresolvedColor::RGB { r, g, b, alpha } => {
+        if should_compile!(dest.targets.current, SpaceSeparatedColorNotation) {
+          dest.write_str("rgba(")?;
+          r.to_css(dest)?;
+          dest.delim(',', false)?;
+          g.to_css(dest)?;
+          dest.delim(',', false)?;
+          b.to_css(dest)?;
+          dest.delim(',', false)?;
+          alpha.to_css(dest, is_custom_property)?;
+          dest.write_char(')')?;
+          return Ok(());
+        }
+
+        dest.write_str("rgb(")?;
+        r.to_css(dest)?;
+        dest.write_char(' ')?;
+        g.to_css(dest)?;
+        dest.write_char(' ')?;
+        b.to_css(dest)?;
+        dest.delim('/', true)?;
+        alpha.to_css(dest, is_custom_property)?;
+        dest.write_char(')')
+      }
+      UnresolvedColor::HSL { h, s, l, alpha } => {
+        if should_compile!(dest.targets.current, SpaceSeparatedColorNotation) {
+          dest.write_str("hsla(")?;
+          h.to_css(dest)?;
+          dest.delim(',', false)?;
+          Percentage(*s / 100.0).to_css(dest)?;
+          dest.delim(',', false)?;
+          Percentage(*l / 100.0).to_css(dest)?;
+          dest.delim(',', false)?;
+          alpha.to_css(dest, is_custom_property)?;
+          dest.write_char(')')?;
+          return Ok(());
+        }
+
+        dest.write_str("hsl(")?;
+        h.to_css(dest)?;
+        dest.write_char(' ')?;
+        Percentage(*s / 100.0).to_css(dest)?;
+        dest.write_char(' ')?;
+        Percentage(*l / 100.0).to_css(dest)?;
+        dest.delim('/', true)?;
+        alpha.to_css(dest, is_custom_property)?;
+        dest.write_char(')')
+      }
+      UnresolvedColor::LightDark { light, dark } => {
+        if should_compile!(dest.targets.current, LightDark) {
+          dest.write_str("var(--lightningcss-light")?;
+          dest.delim(',', false)?;
+          light.to_css(dest, is_custom_property)?;
+          dest.write_char(')')?;
+          dest.whitespace()?;
+          dest.write_str("var(--lightningcss-dark")?;
+          dest.delim(',', false)?;
+          dark.to_css(dest, is_custom_property)?;
+          return dest.write_char(')');
+        }
+
+        dest.write_str("light-dark(")?;
+        light.to_css(dest, is_custom_property)?;
+        dest.delim(',', false)?;
+        dark.to_css(dest, is_custom_property)?;
+        dest.write_char(')')
+      }
+    }
+  }
+}
diff --git a/src/properties/display.rs b/src/properties/display.rs
new file mode 100644
index 0000000..3df7c81
--- /dev/null
+++ b/src/properties/display.rs
@@ -0,0 +1,475 @@
+//! CSS properties related to display.
+
+use super::custom::UnparsedProperty;
+use super::{Property, PropertyId};
+use crate::context::PropertyHandlerContext;
+use crate::declaration::DeclarationList;
+use crate::error::{ParserError, PrinterError};
+use crate::macros::enum_property;
+use crate::prefixes::{is_flex_2009, Feature};
+use crate::printer::Printer;
+use crate::traits::{Parse, PropertyHandler, ToCss};
+use crate::vendor_prefix::VendorPrefix;
+#[cfg(feature = "visitor")]
+use crate::visitor::Visit;
+use cssparser::*;
+
+enum_property! {
+  /// A [`<display-outside>`](https://drafts.csswg.org/css-display-3/#typedef-display-outside) value.
+  #[allow(missing_docs)]
+  pub enum DisplayOutside {
+    Block,
+    Inline,
+    RunIn,
+  }
+}
+
+/// A [`<display-inside>`](https://drafts.csswg.org/css-display-3/#typedef-display-inside) value.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", content = "vendorPrefix", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[allow(missing_docs)]
+pub enum DisplayInside {
+  Flow,
+  FlowRoot,
+  Table,
+  Flex(VendorPrefix),
+  Box(VendorPrefix),
+  Grid,
+  Ruby,
+}
+
+impl<'i> Parse<'i> for DisplayInside {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let location = input.current_source_location();
+    let ident = input.expect_ident()?;
+    match_ignore_ascii_case! { &*ident,
+      "flow" => Ok(DisplayInside::Flow),
+      "flow-root" => Ok(DisplayInside::FlowRoot),
+      "table" => Ok(DisplayInside::Table),
+      "flex" => Ok(DisplayInside::Flex(VendorPrefix::None)),
+      "-webkit-flex" => Ok(DisplayInside::Flex(VendorPrefix::WebKit)),
+      "-ms-flexbox" => Ok(DisplayInside::Flex(VendorPrefix::Ms)),
+      "-webkit-box" => Ok(DisplayInside::Box(VendorPrefix::WebKit)),
+      "-moz-box" => Ok(DisplayInside::Box(VendorPrefix::Moz)),
+      "grid" => Ok(DisplayInside::Grid),
+      "ruby" => Ok(DisplayInside::Ruby),
+      _ => Err(location.new_unexpected_token_error(
+        cssparser::Token::Ident(ident.clone())
+      ))
+    }
+  }
+}
+
+impl ToCss for DisplayInside {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    match self {
+      DisplayInside::Flow => dest.write_str("flow"),
+      DisplayInside::FlowRoot => dest.write_str("flow-root"),
+      DisplayInside::Table => dest.write_str("table"),
+      DisplayInside::Flex(prefix) => {
+        prefix.to_css(dest)?;
+        if *prefix == VendorPrefix::Ms {
+          dest.write_str("flexbox")
+        } else {
+          dest.write_str("flex")
+        }
+      }
+      DisplayInside::Box(prefix) => {
+        prefix.to_css(dest)?;
+        dest.write_str("box")
+      }
+      DisplayInside::Grid => dest.write_str("grid"),
+      DisplayInside::Ruby => dest.write_str("ruby"),
+    }
+  }
+}
+
+impl DisplayInside {
+  fn is_equivalent(&self, other: &DisplayInside) -> bool {
+    match (self, other) {
+      (DisplayInside::Flex(_), DisplayInside::Flex(_)) => true,
+      (DisplayInside::Box(_), DisplayInside::Box(_)) => true,
+      (DisplayInside::Flex(_), DisplayInside::Box(_)) => true,
+      (DisplayInside::Box(_), DisplayInside::Flex(_)) => true,
+      _ => self == other,
+    }
+  }
+}
+
+/// A pair of inside and outside display values, as used in the `display` property.
+///
+/// See [Display](Display).
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(rename_all = "camelCase")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub struct DisplayPair {
+  /// The outside display value.
+  pub outside: DisplayOutside,
+  /// The inside display value.
+  pub inside: DisplayInside,
+  /// Whether this is a list item.
+  pub is_list_item: bool,
+}
+
+impl<'i> Parse<'i> for DisplayPair {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let mut list_item = false;
+    let mut outside = None;
+    let mut inside = None;
+
+    loop {
+      if input.try_parse(|input| input.expect_ident_matching("list-item")).is_ok() {
+        list_item = true;
+        continue;
+      }
+
+      if outside.is_none() {
+        if let Ok(o) = input.try_parse(DisplayOutside::parse) {
+          outside = Some(o);
+          continue;
+        }
+      }
+
+      if inside.is_none() {
+        if let Ok(i) = input.try_parse(DisplayInside::parse) {
+          inside = Some(i);
+          continue;
+        }
+      }
+
+      break;
+    }
+
+    if list_item || inside.is_some() || outside.is_some() {
+      let inside = inside.unwrap_or(DisplayInside::Flow);
+      let outside = outside.unwrap_or(match inside {
+        // "If <display-outside> is omitted, the element’s outside display type
+        // defaults to block — except for ruby, which defaults to inline."
+        // https://drafts.csswg.org/css-display/#inside-model
+        DisplayInside::Ruby => DisplayOutside::Inline,
+        _ => DisplayOutside::Block,
+      });
+
+      if list_item && !matches!(inside, DisplayInside::Flow | DisplayInside::FlowRoot) {
+        return Err(input.new_custom_error(ParserError::InvalidDeclaration));
+      }
+
+      return Ok(DisplayPair {
+        outside,
+        inside,
+        is_list_item: list_item,
+      });
+    }
+
+    let location = input.current_source_location();
+    let ident = input.expect_ident()?;
+    match_ignore_ascii_case! { &*ident,
+      "inline-block" => Ok(DisplayPair {
+        outside: DisplayOutside::Inline,
+        inside: DisplayInside::FlowRoot,
+        is_list_item: false
+      }),
+      "inline-table" => Ok(DisplayPair {
+        outside: DisplayOutside::Inline,
+        inside: DisplayInside::Table,
+        is_list_item: false
+      }),
+      "inline-flex" => Ok(DisplayPair {
+        outside: DisplayOutside::Inline,
+        inside: DisplayInside::Flex(VendorPrefix::None),
+        is_list_item: false
+      }),
+      "-webkit-inline-flex" => Ok(DisplayPair {
+        outside: DisplayOutside::Inline,
+        inside: DisplayInside::Flex(VendorPrefix::WebKit),
+        is_list_item: false
+      }),
+      "-ms-inline-flexbox" => Ok(DisplayPair {
+        outside: DisplayOutside::Inline,
+        inside: DisplayInside::Flex(VendorPrefix::Ms),
+        is_list_item: false
+      }),
+      "-webkit-inline-box" => Ok(DisplayPair {
+        outside: DisplayOutside::Inline,
+        inside: DisplayInside::Box(VendorPrefix::WebKit),
+        is_list_item: false
+      }),
+      "-moz-inline-box" => Ok(DisplayPair {
+        outside: DisplayOutside::Inline,
+        inside: DisplayInside::Box(VendorPrefix::Moz),
+        is_list_item: false
+      }),
+      "inline-grid" => Ok(DisplayPair {
+        outside: DisplayOutside::Inline,
+        inside: DisplayInside::Grid,
+        is_list_item: false
+      }),
+      _ => Err(location.new_unexpected_token_error(
+        cssparser::Token::Ident(ident.clone())
+      ))
+    }
+  }
+}
+
+impl ToCss for DisplayPair {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    match self {
+      DisplayPair {
+        outside: DisplayOutside::Inline,
+        inside: DisplayInside::FlowRoot,
+        is_list_item: false,
+      } => dest.write_str("inline-block"),
+      DisplayPair {
+        outside: DisplayOutside::Inline,
+        inside: DisplayInside::Table,
+        is_list_item: false,
+      } => dest.write_str("inline-table"),
+      DisplayPair {
+        outside: DisplayOutside::Inline,
+        inside: DisplayInside::Flex(prefix),
+        is_list_item: false,
+      } => {
+        prefix.to_css(dest)?;
+        if *prefix == VendorPrefix::Ms {
+          dest.write_str("inline-flexbox")
+        } else {
+          dest.write_str("inline-flex")
+        }
+      }
+      DisplayPair {
+        outside: DisplayOutside::Inline,
+        inside: DisplayInside::Box(prefix),
+        is_list_item: false,
+      } => {
+        prefix.to_css(dest)?;
+        dest.write_str("inline-box")
+      }
+      DisplayPair {
+        outside: DisplayOutside::Inline,
+        inside: DisplayInside::Grid,
+        is_list_item: false,
+      } => dest.write_str("inline-grid"),
+      DisplayPair {
+        outside,
+        inside,
+        is_list_item,
+      } => {
+        let default_outside = match inside {
+          DisplayInside::Ruby => DisplayOutside::Inline,
+          _ => DisplayOutside::Block,
+        };
+
+        let mut needs_space = false;
+        if *outside != default_outside || (*inside == DisplayInside::Flow && !*is_list_item) {
+          outside.to_css(dest)?;
+          needs_space = true;
+        }
+
+        if *inside != DisplayInside::Flow {
+          if needs_space {
+            dest.write_char(' ')?;
+          }
+          inside.to_css(dest)?;
+          needs_space = true;
+        }
+
+        if *is_list_item {
+          if needs_space {
+            dest.write_char(' ')?;
+          }
+          dest.write_str("list-item")?;
+        }
+
+        Ok(())
+      }
+    }
+  }
+}
+
+enum_property! {
+  /// A `display` keyword.
+  ///
+  /// See [Display](Display).
+  #[allow(missing_docs)]
+  pub enum DisplayKeyword {
+    None,
+    Contents,
+    TableRowGroup,
+    TableHeaderGroup,
+    TableFooterGroup,
+    TableRow,
+    TableCell,
+    TableColumnGroup,
+    TableColumn,
+    TableCaption,
+    RubyBase,
+    RubyText,
+    RubyBaseContainer,
+    RubyTextContainer,
+  }
+}
+
+/// A value for the [display](https://drafts.csswg.org/css-display-3/#the-display-properties) property.
+#[derive(Debug, Clone, PartialEq, Parse, ToCss)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum Display {
+  /// A display keyword.
+  #[cfg_attr(
+    feature = "serde",
+    serde(with = "crate::serialization::ValueWrapper::<DisplayKeyword>")
+  )]
+  Keyword(DisplayKeyword),
+  /// The inside and outside display values.
+  Pair(DisplayPair),
+}
+
+enum_property! {
+  /// A value for the [visibility](https://drafts.csswg.org/css-display-3/#visibility) property.
+  pub enum Visibility {
+    /// The element is visible.
+    Visible,
+    /// The element is hidden.
+    Hidden,
+    /// The element is collapsed.
+    Collapse,
+  }
+}
+
+#[derive(Default)]
+pub(crate) struct DisplayHandler<'i> {
+  decls: Vec<Property<'i>>,
+  display: Option<Display>,
+}
+
+impl<'i> PropertyHandler<'i> for DisplayHandler<'i> {
+  fn handle_property(
+    &mut self,
+    property: &Property<'i>,
+    dest: &mut DeclarationList<'i>,
+    context: &mut PropertyHandlerContext<'i, '_>,
+  ) -> bool {
+    if let Property::Display(display) = property {
+      match (&self.display, display) {
+        (Some(Display::Pair(cur)), Display::Pair(new)) => {
+          // If the new value is different but equivalent (e.g. different vendor prefix),
+          // we need to preserve multiple values.
+          if cur.outside == new.outside
+            && cur.is_list_item == new.is_list_item
+            && cur.inside != new.inside
+            && cur.inside.is_equivalent(&new.inside)
+          {
+            // If we have targets, and there is no vendor prefix, clear the existing
+            // declarations. The prefixes will be filled in later. Otherwise, if there
+            // are no targets, or there is a vendor prefix, add a new declaration.
+            if context.targets.browsers.is_some() && new.inside == DisplayInside::Flex(VendorPrefix::None) {
+              self.decls.clear();
+            } else if context.targets.browsers.is_none() || cur.inside != DisplayInside::Flex(VendorPrefix::None) {
+              self.decls.push(Property::Display(self.display.clone().unwrap()));
+            }
+          }
+        }
+        _ => {}
+      }
+
+      self.display = Some(display.clone());
+      return true;
+    }
+
+    if matches!(
+      property,
+      Property::Unparsed(UnparsedProperty {
+        property_id: PropertyId::Display,
+        ..
+      })
+    ) {
+      self.finalize(dest, context);
+      dest.push(property.clone());
+      return true;
+    }
+
+    false
+  }
+
+  fn finalize(&mut self, dest: &mut DeclarationList<'i>, context: &mut PropertyHandlerContext<'i, '_>) {
+    if self.display.is_none() {
+      return;
+    }
+
+    dest.extend(self.decls.drain(..));
+
+    if let Some(display) = std::mem::take(&mut self.display) {
+      // If we have an unprefixed `flex` value, then add the necessary prefixed values.
+      if let Display::Pair(DisplayPair {
+        inside: DisplayInside::Flex(VendorPrefix::None),
+        outside,
+        ..
+      }) = display
+      {
+        let prefixes = context.targets.prefixes(VendorPrefix::None, Feature::DisplayFlex);
+
+        if let Some(targets) = context.targets.browsers {
+          // Handle legacy -webkit-box/-moz-box values if needed.
+          if is_flex_2009(targets) {
+            if prefixes.contains(VendorPrefix::WebKit) {
+              dest.push(Property::Display(Display::Pair(DisplayPair {
+                inside: DisplayInside::Box(VendorPrefix::WebKit),
+                outside: outside.clone(),
+                is_list_item: false,
+              })));
+            }
+
+            if prefixes.contains(VendorPrefix::Moz) {
+              dest.push(Property::Display(Display::Pair(DisplayPair {
+                inside: DisplayInside::Box(VendorPrefix::Moz),
+                outside: outside.clone(),
+                is_list_item: false,
+              })));
+            }
+          }
+        }
+
+        if prefixes.contains(VendorPrefix::WebKit) {
+          dest.push(Property::Display(Display::Pair(DisplayPair {
+            inside: DisplayInside::Flex(VendorPrefix::WebKit),
+            outside: outside.clone(),
+            is_list_item: false,
+          })));
+        }
+
+        if prefixes.contains(VendorPrefix::Ms) {
+          dest.push(Property::Display(Display::Pair(DisplayPair {
+            inside: DisplayInside::Flex(VendorPrefix::Ms),
+            outside: outside.clone(),
+            is_list_item: false,
+          })));
+        }
+      }
+
+      dest.push(Property::Display(display))
+    }
+  }
+}
diff --git a/src/properties/effects.rs b/src/properties/effects.rs
new file mode 100644
index 0000000..1ba69f6
--- /dev/null
+++ b/src/properties/effects.rs
@@ -0,0 +1,412 @@
+//! CSS properties related to filters and effects.
+
+use crate::error::{ParserError, PrinterError};
+use crate::printer::Printer;
+use crate::targets::{Browsers, Targets};
+use crate::traits::{FallbackValues, IsCompatible, Parse, ToCss, Zero};
+use crate::values::color::ColorFallbackKind;
+use crate::values::{angle::Angle, color::CssColor, length::Length, percentage::NumberOrPercentage, url::Url};
+#[cfg(feature = "visitor")]
+use crate::visitor::Visit;
+use cssparser::*;
+use smallvec::SmallVec;
+
+/// A [filter](https://drafts.fxtf.org/filter-effects-1/#filter-functions) function.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", content = "value", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub enum Filter<'i> {
+  /// A `blur()` filter.
+  Blur(Length),
+  /// A `brightness()` filter.
+  Brightness(NumberOrPercentage),
+  /// A `contrast()` filter.
+  Contrast(NumberOrPercentage),
+  /// A `grayscale()` filter.
+  Grayscale(NumberOrPercentage),
+  /// A `hue-rotate()` filter.
+  HueRotate(Angle),
+  /// An `invert()` filter.
+  Invert(NumberOrPercentage),
+  /// An `opacity()` filter.
+  Opacity(NumberOrPercentage),
+  /// A `saturate()` filter.
+  Saturate(NumberOrPercentage),
+  /// A `sepia()` filter.
+  Sepia(NumberOrPercentage),
+  /// A `drop-shadow()` filter.
+  DropShadow(DropShadow),
+  /// A `url()` reference to an SVG filter.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  Url(Url<'i>),
+}
+
+impl<'i> Parse<'i> for Filter<'i> {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    if let Ok(url) = input.try_parse(Url::parse) {
+      return Ok(Filter::Url(url));
+    }
+
+    let location = input.current_source_location();
+    let function = input.expect_function()?;
+    match_ignore_ascii_case! { &function,
+      "blur" => {
+        input.parse_nested_block(|input| {
+          Ok(Filter::Blur(input.try_parse(Length::parse).unwrap_or(Length::zero())))
+        })
+      },
+      "brightness" => {
+        input.parse_nested_block(|input| {
+          Ok(Filter::Brightness(input.try_parse(NumberOrPercentage::parse).unwrap_or(NumberOrPercentage::Number(1.0))))
+        })
+      },
+      "contrast" => {
+        input.parse_nested_block(|input| {
+          Ok(Filter::Contrast(input.try_parse(NumberOrPercentage::parse).unwrap_or(NumberOrPercentage::Number(1.0))))
+        })
+      },
+      "grayscale" => {
+        input.parse_nested_block(|input| {
+          Ok(Filter::Grayscale(input.try_parse(NumberOrPercentage::parse).unwrap_or(NumberOrPercentage::Number(1.0))))
+        })
+      },
+      "hue-rotate" => {
+        input.parse_nested_block(|input| {
+          // Spec has an exception for unitless zero angles: https://github.com/w3c/fxtf-drafts/issues/228
+          Ok(Filter::HueRotate(input.try_parse(Angle::parse_with_unitless_zero).unwrap_or(Angle::zero())))
+        })
+      },
+      "invert" => {
+        input.parse_nested_block(|input| {
+          Ok(Filter::Invert(input.try_parse(NumberOrPercentage::parse).unwrap_or(NumberOrPercentage::Number(1.0))))
+        })
+      },
+      "opacity" => {
+        input.parse_nested_block(|input| {
+          Ok(Filter::Opacity(input.try_parse(NumberOrPercentage::parse).unwrap_or(NumberOrPercentage::Number(1.0))))
+        })
+      },
+      "saturate" => {
+        input.parse_nested_block(|input| {
+          Ok(Filter::Saturate(input.try_parse(NumberOrPercentage::parse).unwrap_or(NumberOrPercentage::Number(1.0))))
+        })
+      },
+      "sepia" => {
+        input.parse_nested_block(|input| {
+          Ok(Filter::Sepia(input.try_parse(NumberOrPercentage::parse).unwrap_or(NumberOrPercentage::Number(1.0))))
+        })
+      },
+      "drop-shadow" => {
+        input.parse_nested_block(|input| {
+          Ok(Filter::DropShadow(DropShadow::parse(input)?))
+        })
+      },
+      _ => Err(location.new_unexpected_token_error(
+        cssparser::Token::Ident(function.clone())
+      ))
+    }
+  }
+}
+
+impl<'i> ToCss for Filter<'i> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    match self {
+      Filter::Blur(val) => {
+        dest.write_str("blur(")?;
+        if *val != Length::zero() {
+          val.to_css(dest)?;
+        }
+        dest.write_char(')')
+      }
+      Filter::Brightness(val) => {
+        dest.write_str("brightness(")?;
+        let v: f32 = val.into();
+        if v != 1.0 {
+          val.to_css(dest)?;
+        }
+        dest.write_char(')')
+      }
+      Filter::Contrast(val) => {
+        dest.write_str("contrast(")?;
+        let v: f32 = val.into();
+        if v != 1.0 {
+          val.to_css(dest)?;
+        }
+        dest.write_char(')')
+      }
+      Filter::Grayscale(val) => {
+        dest.write_str("grayscale(")?;
+        let v: f32 = val.into();
+        if v != 1.0 {
+          val.to_css(dest)?;
+        }
+        dest.write_char(')')
+      }
+      Filter::HueRotate(val) => {
+        dest.write_str("hue-rotate(")?;
+        if !val.is_zero() {
+          val.to_css(dest)?;
+        }
+        dest.write_char(')')
+      }
+      Filter::Invert(val) => {
+        dest.write_str("invert(")?;
+        let v: f32 = val.into();
+        if v != 1.0 {
+          val.to_css(dest)?;
+        }
+        dest.write_char(')')
+      }
+      Filter::Opacity(val) => {
+        dest.write_str("opacity(")?;
+        let v: f32 = val.into();
+        if v != 1.0 {
+          val.to_css(dest)?;
+        }
+        dest.write_char(')')
+      }
+      Filter::Saturate(val) => {
+        dest.write_str("saturate(")?;
+        let v: f32 = val.into();
+        if v != 1.0 {
+          val.to_css(dest)?;
+        }
+        dest.write_char(')')
+      }
+      Filter::Sepia(val) => {
+        dest.write_str("sepia(")?;
+        let v: f32 = val.into();
+        if v != 1.0 {
+          val.to_css(dest)?;
+        }
+        dest.write_char(')')
+      }
+      Filter::DropShadow(val) => {
+        dest.write_str("drop-shadow(")?;
+        val.to_css(dest)?;
+        dest.write_char(')')
+      }
+      Filter::Url(url) => url.to_css(dest),
+    }
+  }
+}
+
+impl<'i> Filter<'i> {
+  fn get_fallback(&self, kind: ColorFallbackKind) -> Self {
+    match self {
+      Filter::DropShadow(shadow) => Filter::DropShadow(shadow.get_fallback(kind)),
+      _ => self.clone(),
+    }
+  }
+}
+
+impl IsCompatible for Filter<'_> {
+  fn is_compatible(&self, _browsers: Browsers) -> bool {
+    true
+  }
+}
+
+/// A [`drop-shadow()`](https://drafts.fxtf.org/filter-effects-1/#funcdef-filter-drop-shadow) filter function.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(rename_all = "camelCase")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub struct DropShadow {
+  /// The color of the drop shadow.
+  pub color: CssColor,
+  /// The x offset of the drop shadow.
+  pub x_offset: Length,
+  /// The y offset of the drop shadow.
+  pub y_offset: Length,
+  /// The blur radius of the drop shadow.
+  pub blur: Length,
+}
+
+impl<'i> Parse<'i> for DropShadow {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let mut color = None;
+    let mut lengths = None;
+
+    loop {
+      if lengths.is_none() {
+        let value = input.try_parse::<_, _, ParseError<ParserError<'i>>>(|input| {
+          let horizontal = Length::parse(input)?;
+          let vertical = Length::parse(input)?;
+          let blur = input.try_parse(Length::parse).unwrap_or(Length::zero());
+          Ok((horizontal, vertical, blur))
+        });
+
+        if let Ok(value) = value {
+          lengths = Some(value);
+          continue;
+        }
+      }
+
+      if color.is_none() {
+        if let Ok(value) = input.try_parse(CssColor::parse) {
+          color = Some(value);
+          continue;
+        }
+      }
+
+      break;
+    }
+
+    let lengths = lengths.ok_or(input.new_error(BasicParseErrorKind::QualifiedRuleInvalid))?;
+    Ok(DropShadow {
+      color: color.unwrap_or(CssColor::current_color()),
+      x_offset: lengths.0,
+      y_offset: lengths.1,
+      blur: lengths.2,
+    })
+  }
+}
+
+impl ToCss for DropShadow {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    self.x_offset.to_css(dest)?;
+    dest.write_char(' ')?;
+    self.y_offset.to_css(dest)?;
+
+    if self.blur != Length::zero() {
+      dest.write_char(' ')?;
+      self.blur.to_css(dest)?;
+    }
+
+    if self.color != CssColor::current_color() {
+      dest.write_char(' ')?;
+      self.color.to_css(dest)?;
+    }
+
+    Ok(())
+  }
+}
+
+impl DropShadow {
+  fn get_fallback(&self, kind: ColorFallbackKind) -> DropShadow {
+    DropShadow {
+      color: self.color.get_fallback(kind),
+      ..self.clone()
+    }
+  }
+}
+
+/// A value for the [filter](https://drafts.fxtf.org/filter-effects-1/#FilterProperty) and
+/// [backdrop-filter](https://drafts.fxtf.org/filter-effects-2/#BackdropFilterProperty) properties.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", content = "value", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub enum FilterList<'i> {
+  /// The `none` keyword.
+  None,
+  /// A list of filter functions.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  Filters(SmallVec<[Filter<'i>; 1]>),
+}
+
+impl<'i> Parse<'i> for FilterList<'i> {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    if input.try_parse(|input| input.expect_ident_matching("none")).is_ok() {
+      return Ok(FilterList::None);
+    }
+
+    let mut filters = SmallVec::new();
+    while let Ok(filter) = input.try_parse(Filter::parse) {
+      filters.push(filter);
+    }
+
+    Ok(FilterList::Filters(filters))
+  }
+}
+
+impl<'i> ToCss for FilterList<'i> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    match self {
+      FilterList::None => dest.write_str("none"),
+      FilterList::Filters(filters) => {
+        let mut first = true;
+        for filter in filters {
+          if first {
+            first = false;
+          } else {
+            dest.whitespace()?;
+          }
+          filter.to_css(dest)?;
+        }
+        Ok(())
+      }
+    }
+  }
+}
+
+impl<'i> FallbackValues for FilterList<'i> {
+  fn get_fallbacks(&mut self, targets: Targets) -> Vec<Self> {
+    let mut res = Vec::new();
+    let mut fallbacks = ColorFallbackKind::empty();
+    if let FilterList::Filters(filters) = self {
+      for shadow in filters.iter() {
+        if let Filter::DropShadow(shadow) = &shadow {
+          fallbacks |= shadow.color.get_necessary_fallbacks(targets);
+        }
+      }
+
+      if fallbacks.contains(ColorFallbackKind::RGB) {
+        res.push(FilterList::Filters(
+          filters
+            .iter()
+            .map(|filter| filter.get_fallback(ColorFallbackKind::RGB))
+            .collect(),
+        ));
+      }
+
+      if fallbacks.contains(ColorFallbackKind::P3) {
+        res.push(FilterList::Filters(
+          filters
+            .iter()
+            .map(|filter| filter.get_fallback(ColorFallbackKind::P3))
+            .collect(),
+        ));
+      }
+
+      if fallbacks.contains(ColorFallbackKind::LAB) {
+        for filter in filters.iter_mut() {
+          *filter = filter.get_fallback(ColorFallbackKind::LAB);
+        }
+      }
+    }
+
+    res
+  }
+}
+
+impl IsCompatible for FilterList<'_> {
+  fn is_compatible(&self, _browsers: Browsers) -> bool {
+    true
+  }
+}
diff --git a/src/properties/flex.rs b/src/properties/flex.rs
new file mode 100644
index 0000000..e885c59
--- /dev/null
+++ b/src/properties/flex.rs
@@ -0,0 +1,810 @@
+//! CSS properties related to flexbox layout.
+
+use super::align::{
+  AlignContent, AlignItems, AlignSelf, ContentDistribution, ContentPosition, JustifyContent, SelfPosition,
+};
+use super::{Property, PropertyId};
+use crate::context::PropertyHandlerContext;
+use crate::declaration::{DeclarationBlock, DeclarationList};
+use crate::error::{ParserError, PrinterError};
+use crate::macros::*;
+use crate::prefixes::{is_flex_2009, Feature};
+use crate::printer::Printer;
+use crate::traits::{FromStandard, Parse, PropertyHandler, Shorthand, ToCss, Zero};
+use crate::values::number::{CSSInteger, CSSNumber};
+use crate::values::{
+  length::{LengthPercentage, LengthPercentageOrAuto},
+  percentage::Percentage,
+};
+use crate::vendor_prefix::VendorPrefix;
+#[cfg(feature = "visitor")]
+use crate::visitor::Visit;
+use cssparser::*;
+
+enum_property! {
+  /// A value for the [flex-direction](https://www.w3.org/TR/2018/CR-css-flexbox-1-20181119/#propdef-flex-direction) property.
+  pub enum FlexDirection {
+    /// Flex items are laid out in a row.
+    Row,
+    /// Flex items are laid out in a row, and reversed.
+    RowReverse,
+    /// Flex items are laid out in a column.
+    Column,
+    /// Flex items are laid out in a column, and reversed.
+    ColumnReverse,
+  }
+}
+
+impl Default for FlexDirection {
+  fn default() -> FlexDirection {
+    FlexDirection::Row
+  }
+}
+
+enum_property! {
+  /// A value for the [flex-wrap](https://www.w3.org/TR/2018/CR-css-flexbox-1-20181119/#flex-wrap-property) property.
+  pub enum FlexWrap {
+    /// The flex items do not wrap.
+    "nowrap": NoWrap,
+    /// The flex items wrap.
+    "wrap": Wrap,
+    /// The flex items wrap, in reverse.
+    "wrap-reverse": WrapReverse,
+  }
+}
+
+impl Default for FlexWrap {
+  fn default() -> FlexWrap {
+    FlexWrap::NoWrap
+  }
+}
+
+impl FromStandard<FlexWrap> for FlexWrap {
+  fn from_standard(wrap: &FlexWrap) -> Option<FlexWrap> {
+    Some(wrap.clone())
+  }
+}
+
+define_shorthand! {
+  /// A value for the [flex-flow](https://www.w3.org/TR/2018/CR-css-flexbox-1-20181119/#flex-flow-property) shorthand property.
+  pub struct FlexFlow(VendorPrefix) {
+    /// The direction that flex items flow.
+    direction: FlexDirection(FlexDirection, VendorPrefix),
+    /// How the flex items wrap.
+    wrap: FlexWrap(FlexWrap, VendorPrefix),
+  }
+}
+
+impl<'i> Parse<'i> for FlexFlow {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let mut direction = None;
+    let mut wrap = None;
+    loop {
+      if direction.is_none() {
+        if let Ok(value) = input.try_parse(FlexDirection::parse) {
+          direction = Some(value);
+          continue;
+        }
+      }
+      if wrap.is_none() {
+        if let Ok(value) = input.try_parse(FlexWrap::parse) {
+          wrap = Some(value);
+          continue;
+        }
+      }
+      break;
+    }
+
+    Ok(FlexFlow {
+      direction: direction.unwrap_or_default(),
+      wrap: wrap.unwrap_or_default(),
+    })
+  }
+}
+
+impl ToCss for FlexFlow {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    let mut needs_space = false;
+    if self.direction != FlexDirection::default() || self.wrap == FlexWrap::default() {
+      self.direction.to_css(dest)?;
+      needs_space = true;
+    }
+
+    if self.wrap != FlexWrap::default() {
+      if needs_space {
+        dest.write_str(" ")?;
+      }
+      self.wrap.to_css(dest)?;
+    }
+
+    Ok(())
+  }
+}
+
+define_shorthand! {
+/// A value for the [flex](https://www.w3.org/TR/2018/CR-css-flexbox-1-20181119/#flex-property) shorthand property.
+  pub struct Flex(VendorPrefix) {
+    /// The flex grow factor.
+    grow: FlexGrow(CSSNumber, VendorPrefix),
+    /// The flex shrink factor.
+    shrink: FlexShrink(CSSNumber, VendorPrefix),
+    /// The flex basis.
+    basis: FlexBasis(LengthPercentageOrAuto, VendorPrefix),
+  }
+}
+
+impl<'i> Parse<'i> for Flex {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    if input.try_parse(|input| input.expect_ident_matching("none")).is_ok() {
+      return Ok(Flex {
+        grow: 0.0,
+        shrink: 0.0,
+        basis: LengthPercentageOrAuto::Auto,
+      });
+    }
+
+    let mut grow = None;
+    let mut shrink = None;
+    let mut basis = None;
+
+    loop {
+      if grow.is_none() {
+        if let Ok(val) = input.try_parse(CSSNumber::parse) {
+          grow = Some(val);
+          shrink = input.try_parse(CSSNumber::parse).ok();
+          continue;
+        }
+      }
+
+      if basis.is_none() {
+        if let Ok(val) = input.try_parse(LengthPercentageOrAuto::parse) {
+          basis = Some(val);
+          continue;
+        }
+      }
+
+      break;
+    }
+
+    Ok(Flex {
+      grow: grow.unwrap_or(1.0),
+      shrink: shrink.unwrap_or(1.0),
+      basis: basis.unwrap_or(LengthPercentageOrAuto::LengthPercentage(LengthPercentage::Percentage(
+        Percentage(0.0),
+      ))),
+    })
+  }
+}
+
+impl ToCss for Flex {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    if self.grow == 0.0 && self.shrink == 0.0 && self.basis == LengthPercentageOrAuto::Auto {
+      dest.write_str("none")?;
+      return Ok(());
+    }
+
+    #[derive(PartialEq)]
+    enum ZeroKind {
+      NonZero,
+      Length,
+      Percentage,
+    }
+
+    // If the basis is unitless 0, we must write all three components to disambiguate.
+    // If the basis is 0%, we can omit the basis.
+    let basis_kind = match &self.basis {
+      LengthPercentageOrAuto::LengthPercentage(lp) => match lp {
+        LengthPercentage::Dimension(l) if l.is_zero() => ZeroKind::Length,
+        LengthPercentage::Percentage(p) if p.is_zero() => ZeroKind::Percentage,
+        _ => ZeroKind::NonZero,
+      },
+      _ => ZeroKind::NonZero,
+    };
+
+    if self.grow != 1.0 || self.shrink != 1.0 || basis_kind != ZeroKind::NonZero {
+      self.grow.to_css(dest)?;
+      if self.shrink != 1.0 || basis_kind == ZeroKind::Length {
+        dest.write_str(" ")?;
+        self.shrink.to_css(dest)?;
+      }
+    }
+
+    if basis_kind != ZeroKind::Percentage {
+      if self.grow != 1.0 || self.shrink != 1.0 || basis_kind == ZeroKind::Length {
+        dest.write_str(" ")?;
+      }
+      self.basis.to_css(dest)?;
+    }
+
+    Ok(())
+  }
+}
+
+// Old flex (2009): https://www.w3.org/TR/2009/WD-css3-flexbox-20090723/
+
+enum_property! {
+  /// A value for the legacy (prefixed) [box-orient](https://www.w3.org/TR/2009/WD-css3-flexbox-20090723/#orientation) property.
+  /// Partially equivalent to `flex-direction` in the standard syntax.
+  pub enum BoxOrient {
+    /// Items are laid out horizontally.
+    Horizontal,
+    /// Items are laid out vertically.
+    Vertical,
+    /// Items are laid out along the inline axis, according to the writing direction.
+    InlineAxis,
+    /// Items are laid out along the block axis, according to the writing direction.
+    BlockAxis,
+  }
+}
+
+impl FlexDirection {
+  fn to_2009(&self) -> (BoxOrient, BoxDirection) {
+    match self {
+      FlexDirection::Row => (BoxOrient::Horizontal, BoxDirection::Normal),
+      FlexDirection::Column => (BoxOrient::Vertical, BoxDirection::Normal),
+      FlexDirection::RowReverse => (BoxOrient::Horizontal, BoxDirection::Reverse),
+      FlexDirection::ColumnReverse => (BoxOrient::Vertical, BoxDirection::Reverse),
+    }
+  }
+}
+
+enum_property! {
+  /// A value for the legacy (prefixed) [box-direction](https://www.w3.org/TR/2009/WD-css3-flexbox-20090723/#displayorder) property.
+  /// Partially equivalent to the `flex-direction` property in the standard syntax.
+  pub enum BoxDirection {
+    /// Items flow in the natural direction.
+    Normal,
+    /// Items flow in the reverse direction.
+    Reverse,
+  }
+}
+
+enum_property! {
+  /// A value for the legacy (prefixed) [box-align](https://www.w3.org/TR/2009/WD-css3-flexbox-20090723/#alignment) property.
+  /// Equivalent to the `align-items` property in the standard syntax.
+  pub enum BoxAlign {
+    /// Items are aligned to the start.
+    Start,
+    /// Items are aligned to the end.
+    End,
+    /// Items are centered.
+    Center,
+    /// Items are aligned to the baseline.
+    Baseline,
+    /// Items are stretched.
+    Stretch,
+  }
+}
+
+impl FromStandard<AlignItems> for BoxAlign {
+  fn from_standard(align: &AlignItems) -> Option<BoxAlign> {
+    match align {
+      AlignItems::SelfPosition { overflow: None, value } => match value {
+        SelfPosition::Start | SelfPosition::FlexStart => Some(BoxAlign::Start),
+        SelfPosition::End | SelfPosition::FlexEnd => Some(BoxAlign::End),
+        SelfPosition::Center => Some(BoxAlign::Center),
+        _ => None,
+      },
+      AlignItems::Stretch => Some(BoxAlign::Stretch),
+      _ => None,
+    }
+  }
+}
+
+enum_property! {
+  /// A value for the legacy (prefixed) [box-pack](https://www.w3.org/TR/2009/WD-css3-flexbox-20090723/#packing) property.
+  /// Equivalent to the `justify-content` property in the standard syntax.
+  pub enum BoxPack {
+    /// Items are justified to the start.
+    Start,
+    /// Items are justified to the end.
+    End,
+    /// Items are centered.
+    Center,
+    /// Items are justified to the start and end.
+    Justify,
+  }
+}
+
+impl FromStandard<JustifyContent> for BoxPack {
+  fn from_standard(justify: &JustifyContent) -> Option<BoxPack> {
+    match justify {
+      JustifyContent::ContentDistribution(cd) => match cd {
+        ContentDistribution::SpaceBetween => Some(BoxPack::Justify),
+        _ => None,
+      },
+      JustifyContent::ContentPosition { overflow: None, value } => match value {
+        ContentPosition::Start | ContentPosition::FlexStart => Some(BoxPack::Start),
+        ContentPosition::End | ContentPosition::FlexEnd => Some(BoxPack::End),
+        ContentPosition::Center => Some(BoxPack::Center),
+      },
+      _ => None,
+    }
+  }
+}
+
+enum_property! {
+  /// A value for the legacy (prefixed) [box-lines](https://www.w3.org/TR/2009/WD-css3-flexbox-20090723/#multiple) property.
+  /// Equivalent to the `flex-wrap` property in the standard syntax.
+  pub enum BoxLines {
+    /// Items are laid out in a single line.
+    Single,
+    /// Items may wrap into multiple lines.
+    Multiple,
+  }
+}
+
+impl FromStandard<FlexWrap> for BoxLines {
+  fn from_standard(wrap: &FlexWrap) -> Option<BoxLines> {
+    match wrap {
+      FlexWrap::NoWrap => Some(BoxLines::Single),
+      FlexWrap::Wrap => Some(BoxLines::Multiple),
+      _ => None,
+    }
+  }
+}
+
+type BoxOrdinalGroup = CSSInteger;
+impl FromStandard<CSSInteger> for BoxOrdinalGroup {
+  fn from_standard(order: &CSSInteger) -> Option<BoxOrdinalGroup> {
+    Some(*order)
+  }
+}
+
+// Old flex (2012): https://www.w3.org/TR/2012/WD-css3-flexbox-20120322/
+
+enum_property! {
+  /// A value for the legacy (prefixed) [flex-pack](https://www.w3.org/TR/2012/WD-css3-flexbox-20120322/#flex-pack) property.
+  /// Equivalent to the `justify-content` property in the standard syntax.
+  pub enum FlexPack {
+    /// Items are justified to the start.
+    Start,
+    /// Items are justified to the end.
+    End,
+    /// Items are centered.
+    Center,
+    /// Items are justified to the start and end.
+    Justify,
+    /// Items are distributed evenly, with half size spaces on either end.
+    Distribute,
+  }
+}
+
+impl FromStandard<JustifyContent> for FlexPack {
+  fn from_standard(justify: &JustifyContent) -> Option<FlexPack> {
+    match justify {
+      JustifyContent::ContentDistribution(cd) => match cd {
+        ContentDistribution::SpaceBetween => Some(FlexPack::Justify),
+        ContentDistribution::SpaceAround => Some(FlexPack::Distribute),
+        _ => None,
+      },
+      JustifyContent::ContentPosition { overflow: None, value } => match value {
+        ContentPosition::Start | ContentPosition::FlexStart => Some(FlexPack::Start),
+        ContentPosition::End | ContentPosition::FlexEnd => Some(FlexPack::End),
+        ContentPosition::Center => Some(FlexPack::Center),
+      },
+      _ => None,
+    }
+  }
+}
+
+/// A value for the legacy (prefixed) [flex-align](https://www.w3.org/TR/2012/WD-css3-flexbox-20120322/#flex-align) property.
+pub type FlexAlign = BoxAlign;
+
+enum_property! {
+  /// A value for the legacy (prefixed) [flex-item-align](https://www.w3.org/TR/2012/WD-css3-flexbox-20120322/#flex-align) property.
+  /// Equivalent to the `align-self` property in the standard syntax.
+  pub enum FlexItemAlign {
+    /// Equivalent to the value of `flex-align`.
+    Auto,
+    /// The item is aligned to the start.
+    Start,
+    /// The item is aligned to the end.
+    End,
+    /// The item is centered.
+    Center,
+    /// The item is aligned to the baseline.
+    Baseline,
+    /// The item is stretched.
+    Stretch,
+  }
+}
+
+impl FromStandard<AlignSelf> for FlexItemAlign {
+  fn from_standard(justify: &AlignSelf) -> Option<FlexItemAlign> {
+    match justify {
+      AlignSelf::Auto => Some(FlexItemAlign::Auto),
+      AlignSelf::Stretch => Some(FlexItemAlign::Stretch),
+      AlignSelf::SelfPosition { overflow: None, value } => match value {
+        SelfPosition::Start | SelfPosition::FlexStart => Some(FlexItemAlign::Start),
+        SelfPosition::End | SelfPosition::FlexEnd => Some(FlexItemAlign::End),
+        SelfPosition::Center => Some(FlexItemAlign::Center),
+        _ => None,
+      },
+      _ => None,
+    }
+  }
+}
+
+enum_property! {
+  /// A value for the legacy (prefixed) [flex-line-pack](https://www.w3.org/TR/2012/WD-css3-flexbox-20120322/#flex-line-pack) property.
+  /// Equivalent to the `align-content` property in the standard syntax.
+  pub enum FlexLinePack {
+    /// Content is aligned to the start.
+    Start,
+    /// Content is aligned to the end.
+    End,
+    /// Content is centered.
+    Center,
+    /// Content is justified.
+    Justify,
+    /// Content is distributed evenly, with half size spaces on either end.
+    Distribute,
+    /// Content is stretched.
+    Stretch,
+  }
+}
+
+impl FromStandard<AlignContent> for FlexLinePack {
+  fn from_standard(justify: &AlignContent) -> Option<FlexLinePack> {
+    match justify {
+      AlignContent::ContentDistribution(cd) => match cd {
+        ContentDistribution::SpaceBetween => Some(FlexLinePack::Justify),
+        ContentDistribution::SpaceAround => Some(FlexLinePack::Distribute),
+        ContentDistribution::Stretch => Some(FlexLinePack::Stretch),
+        _ => None,
+      },
+      AlignContent::ContentPosition { overflow: None, value } => match value {
+        ContentPosition::Start | ContentPosition::FlexStart => Some(FlexLinePack::Start),
+        ContentPosition::End | ContentPosition::FlexEnd => Some(FlexLinePack::End),
+        ContentPosition::Center => Some(FlexLinePack::Center),
+      },
+      _ => None,
+    }
+  }
+}
+
+#[derive(Default, Debug)]
+pub(crate) struct FlexHandler {
+  direction: Option<(FlexDirection, VendorPrefix)>,
+  box_orient: Option<(BoxOrient, VendorPrefix)>,
+  box_direction: Option<(BoxDirection, VendorPrefix)>,
+  wrap: Option<(FlexWrap, VendorPrefix)>,
+  box_lines: Option<(BoxLines, VendorPrefix)>,
+  grow: Option<(CSSNumber, VendorPrefix)>,
+  box_flex: Option<(CSSNumber, VendorPrefix)>,
+  flex_positive: Option<(CSSNumber, VendorPrefix)>,
+  shrink: Option<(CSSNumber, VendorPrefix)>,
+  flex_negative: Option<(CSSNumber, VendorPrefix)>,
+  basis: Option<(LengthPercentageOrAuto, VendorPrefix)>,
+  preferred_size: Option<(LengthPercentageOrAuto, VendorPrefix)>,
+  order: Option<(CSSInteger, VendorPrefix)>,
+  box_ordinal_group: Option<(BoxOrdinalGroup, VendorPrefix)>,
+  flex_order: Option<(CSSInteger, VendorPrefix)>,
+  has_any: bool,
+}
+
+impl<'i> PropertyHandler<'i> for FlexHandler {
+  fn handle_property(
+    &mut self,
+    property: &Property<'i>,
+    dest: &mut DeclarationList<'i>,
+    context: &mut PropertyHandlerContext<'i, '_>,
+  ) -> bool {
+    use Property::*;
+
+    macro_rules! maybe_flush {
+      ($prop: ident, $val: expr, $vp: ident) => {{
+        // If two vendor prefixes for the same property have different
+        // values, we need to flush what we have immediately to preserve order.
+        if let Some((val, prefixes)) = &self.$prop {
+          if val != $val && !prefixes.contains(*$vp) {
+            self.flush(dest, context);
+          }
+        }
+      }};
+    }
+
+    macro_rules! property {
+      ($prop: ident, $val: expr, $vp: ident) => {{
+        maybe_flush!($prop, $val, $vp);
+
+        // Otherwise, update the value and add the prefix.
+        if let Some((val, prefixes)) = &mut self.$prop {
+          *val = $val.clone();
+          *prefixes |= *$vp;
+        } else {
+          self.$prop = Some(($val.clone(), *$vp));
+          self.has_any = true;
+        }
+      }};
+    }
+
+    match property {
+      FlexDirection(val, vp) => {
+        if context.targets.browsers.is_some() {
+          self.box_direction = None;
+          self.box_orient = None;
+        }
+        property!(direction, val, vp);
+      }
+      BoxOrient(val, vp) => property!(box_orient, val, vp),
+      BoxDirection(val, vp) => property!(box_direction, val, vp),
+      FlexWrap(val, vp) => {
+        if context.targets.browsers.is_some() {
+          self.box_lines = None;
+        }
+        property!(wrap, val, vp);
+      }
+      BoxLines(val, vp) => property!(box_lines, val, vp),
+      FlexFlow(val, vp) => {
+        if context.targets.browsers.is_some() {
+          self.box_direction = None;
+          self.box_orient = None;
+        }
+        property!(direction, &val.direction, vp);
+        property!(wrap, &val.wrap, vp);
+      }
+      FlexGrow(val, vp) => {
+        if context.targets.browsers.is_some() {
+          self.box_flex = None;
+          self.flex_positive = None;
+        }
+        property!(grow, val, vp);
+      }
+      BoxFlex(val, vp) => property!(box_flex, val, vp),
+      FlexPositive(val, vp) => property!(flex_positive, val, vp),
+      FlexShrink(val, vp) => {
+        if context.targets.browsers.is_some() {
+          self.flex_negative = None;
+        }
+        property!(shrink, val, vp);
+      }
+      FlexNegative(val, vp) => property!(flex_negative, val, vp),
+      FlexBasis(val, vp) => {
+        if context.targets.browsers.is_some() {
+          self.preferred_size = None;
+        }
+        property!(basis, val, vp);
+      }
+      FlexPreferredSize(val, vp) => property!(preferred_size, val, vp),
+      Flex(val, vp) => {
+        if context.targets.browsers.is_some() {
+          self.box_flex = None;
+          self.flex_positive = None;
+          self.flex_negative = None;
+          self.preferred_size = None;
+        }
+        maybe_flush!(grow, &val.grow, vp);
+        maybe_flush!(shrink, &val.shrink, vp);
+        maybe_flush!(basis, &val.basis, vp);
+        property!(grow, &val.grow, vp);
+        property!(shrink, &val.shrink, vp);
+        property!(basis, &val.basis, vp);
+      }
+      Order(val, vp) => {
+        if context.targets.browsers.is_some() {
+          self.box_ordinal_group = None;
+          self.flex_order = None;
+        }
+        property!(order, val, vp);
+      }
+      BoxOrdinalGroup(val, vp) => property!(box_ordinal_group, val, vp),
+      FlexOrder(val, vp) => property!(flex_order, val, vp),
+      Unparsed(val) if is_flex_property(&val.property_id) => {
+        self.flush(dest, context);
+        dest.push(property.clone()) // TODO: prefix?
+      }
+      _ => return false,
+    }
+
+    true
+  }
+
+  fn finalize(&mut self, dest: &mut DeclarationList<'i>, context: &mut PropertyHandlerContext<'i, '_>) {
+    self.flush(dest, context);
+  }
+}
+
+impl FlexHandler {
+  fn flush<'i>(&mut self, dest: &mut DeclarationList<'i>, context: &mut PropertyHandlerContext<'i, '_>) {
+    if !self.has_any {
+      return;
+    }
+
+    self.has_any = false;
+
+    let mut direction = std::mem::take(&mut self.direction);
+    let mut wrap = std::mem::take(&mut self.wrap);
+    let mut grow = std::mem::take(&mut self.grow);
+    let mut shrink = std::mem::take(&mut self.shrink);
+    let mut basis = std::mem::take(&mut self.basis);
+    let box_orient = std::mem::take(&mut self.box_orient);
+    let box_direction = std::mem::take(&mut self.box_direction);
+    let box_flex = std::mem::take(&mut self.box_flex);
+    let box_ordinal_group = std::mem::take(&mut self.box_ordinal_group);
+    let box_lines = std::mem::take(&mut self.box_lines);
+    let flex_positive = std::mem::take(&mut self.flex_positive);
+    let flex_negative = std::mem::take(&mut self.flex_negative);
+    let preferred_size = std::mem::take(&mut self.preferred_size);
+    let order = std::mem::take(&mut self.order);
+    let flex_order = std::mem::take(&mut self.flex_order);
+
+    macro_rules! single_property {
+      ($prop: ident, $key: ident $(, 2012: $prop_2012: ident )? $(, 2009: $prop_2009: ident )?) => {
+        if let Some((val, prefix)) = $key {
+          if !prefix.is_empty() {
+            let mut prefix = context.targets.prefixes(prefix, Feature::$prop);
+            if prefix.contains(VendorPrefix::None) {
+              $(
+                // 2009 spec, implemented by webkit and firefox.
+                if let Some(targets) = context.targets.browsers {
+                  let mut prefixes_2009 = VendorPrefix::empty();
+                  if is_flex_2009(targets) {
+                    prefixes_2009 |= VendorPrefix::WebKit;
+                  }
+                  if prefix.contains(VendorPrefix::Moz) {
+                    prefixes_2009 |= VendorPrefix::Moz;
+                  }
+                  if !prefixes_2009.is_empty() {
+                    if let Some(v) = $prop_2009::from_standard(&val) {
+                      dest.push(Property::$prop_2009(v, prefixes_2009));
+                    }
+                  }
+                }
+              )?
+            }
+
+            $(
+              let mut ms = true;
+              if prefix.contains(VendorPrefix::Ms) {
+                dest.push(Property::$prop_2012(val.clone(), VendorPrefix::Ms));
+                ms = false;
+              }
+              if !ms {
+                prefix.remove(VendorPrefix::Ms);
+              }
+            )?
+
+            // Firefox only implemented the 2009 spec prefixed.
+            prefix.remove(VendorPrefix::Moz);
+            dest.push(Property::$prop(val, prefix))
+          }
+        }
+      };
+    }
+
+    macro_rules! legacy_property {
+      ($prop: ident, $key: expr) => {
+        if let Some((val, prefix)) = $key {
+          if !prefix.is_empty() {
+            dest.push(Property::$prop(val, prefix))
+          }
+        }
+      };
+    }
+
+    // Legacy properties. These are only set if the final standard properties were unset.
+    legacy_property!(BoxOrient, box_orient);
+    legacy_property!(BoxDirection, box_direction);
+    legacy_property!(BoxOrdinalGroup, box_ordinal_group);
+    legacy_property!(BoxFlex, box_flex);
+    legacy_property!(BoxLines, box_lines);
+    legacy_property!(FlexPositive, flex_positive);
+    legacy_property!(FlexNegative, flex_negative);
+    legacy_property!(FlexPreferredSize, preferred_size.clone());
+    legacy_property!(FlexOrder, flex_order.clone());
+
+    if let Some((direction, _)) = direction {
+      if let Some(targets) = context.targets.browsers {
+        let prefixes = context.targets.prefixes(VendorPrefix::None, Feature::FlexDirection);
+        let mut prefixes_2009 = VendorPrefix::empty();
+        if is_flex_2009(targets) {
+          prefixes_2009 |= VendorPrefix::WebKit;
+        }
+        if prefixes.contains(VendorPrefix::Moz) {
+          prefixes_2009 |= VendorPrefix::Moz;
+        }
+        if !prefixes_2009.is_empty() {
+          let (orient, dir) = direction.to_2009();
+          dest.push(Property::BoxOrient(orient, prefixes_2009));
+          dest.push(Property::BoxDirection(dir, prefixes_2009));
+        }
+      }
+    }
+
+    if let (Some((direction, dir_prefix)), Some((wrap, wrap_prefix))) = (&mut direction, &mut wrap) {
+      let intersection = *dir_prefix & *wrap_prefix;
+      if !intersection.is_empty() {
+        let mut prefix = context.targets.prefixes(intersection, Feature::FlexFlow);
+        // Firefox only implemented the 2009 spec prefixed.
+        prefix.remove(VendorPrefix::Moz);
+        dest.push(Property::FlexFlow(
+          FlexFlow {
+            direction: *direction,
+            wrap: *wrap,
+          },
+          prefix,
+        ));
+        dir_prefix.remove(intersection);
+        wrap_prefix.remove(intersection);
+      }
+    }
+
+    single_property!(FlexDirection, direction);
+    single_property!(FlexWrap, wrap, 2009: BoxLines);
+
+    if let Some(targets) = context.targets.browsers {
+      if let Some((grow, _)) = grow {
+        let prefixes = context.targets.prefixes(VendorPrefix::None, Feature::FlexGrow);
+        let mut prefixes_2009 = VendorPrefix::empty();
+        if is_flex_2009(targets) {
+          prefixes_2009 |= VendorPrefix::WebKit;
+        }
+        if prefixes.contains(VendorPrefix::Moz) {
+          prefixes_2009 |= VendorPrefix::Moz;
+        }
+        if !prefixes_2009.is_empty() {
+          dest.push(Property::BoxFlex(grow, prefixes_2009));
+        }
+      }
+    }
+
+    if let (Some((grow, grow_prefix)), Some((shrink, shrink_prefix)), Some((basis, basis_prefix))) =
+      (&mut grow, &mut shrink, &mut basis)
+    {
+      let intersection = *grow_prefix & *shrink_prefix & *basis_prefix;
+      if !intersection.is_empty() {
+        let mut prefix = context.targets.prefixes(intersection, Feature::Flex);
+        // Firefox only implemented the 2009 spec prefixed.
+        prefix.remove(VendorPrefix::Moz);
+        dest.push(Property::Flex(
+          Flex {
+            grow: *grow,
+            shrink: *shrink,
+            basis: basis.clone(),
+          },
+          prefix,
+        ));
+        grow_prefix.remove(intersection);
+        shrink_prefix.remove(intersection);
+        basis_prefix.remove(intersection);
+      }
+    }
+
+    single_property!(FlexGrow, grow, 2012: FlexPositive);
+    single_property!(FlexShrink, shrink, 2012: FlexNegative);
+    single_property!(FlexBasis, basis, 2012: FlexPreferredSize);
+    single_property!(Order, order, 2012: FlexOrder, 2009: BoxOrdinalGroup);
+  }
+}
+
+#[inline]
+fn is_flex_property(property_id: &PropertyId) -> bool {
+  match property_id {
+    PropertyId::FlexDirection(_)
+    | PropertyId::BoxOrient(_)
+    | PropertyId::BoxDirection(_)
+    | PropertyId::FlexWrap(_)
+    | PropertyId::BoxLines(_)
+    | PropertyId::FlexFlow(_)
+    | PropertyId::FlexGrow(_)
+    | PropertyId::BoxFlex(_)
+    | PropertyId::FlexPositive(_)
+    | PropertyId::FlexShrink(_)
+    | PropertyId::FlexNegative(_)
+    | PropertyId::FlexBasis(_)
+    | PropertyId::FlexPreferredSize(_)
+    | PropertyId::Flex(_)
+    | PropertyId::Order(_)
+    | PropertyId::BoxOrdinalGroup(_)
+    | PropertyId::FlexOrder(_) => true,
+    _ => false,
+  }
+}
diff --git a/src/properties/font.rs b/src/properties/font.rs
new file mode 100644
index 0000000..55581d8
--- /dev/null
+++ b/src/properties/font.rs
@@ -0,0 +1,1041 @@
+//! CSS properties related to fonts.
+
+use std::collections::HashSet;
+
+use super::{Property, PropertyId};
+use crate::compat::Feature;
+use crate::context::PropertyHandlerContext;
+use crate::declaration::{DeclarationBlock, DeclarationList};
+use crate::error::{ParserError, PrinterError};
+use crate::macros::*;
+use crate::printer::Printer;
+use crate::targets::should_compile;
+use crate::traits::{IsCompatible, Parse, PropertyHandler, Shorthand, ToCss};
+use crate::values::length::LengthValue;
+use crate::values::number::CSSNumber;
+use crate::values::string::CowArcStr;
+use crate::values::{angle::Angle, length::LengthPercentage, percentage::Percentage};
+#[cfg(feature = "visitor")]
+use crate::visitor::Visit;
+use cssparser::*;
+
+/// A value for the [font-weight](https://www.w3.org/TR/css-fonts-4/#font-weight-prop) property.
+#[derive(Debug, Clone, PartialEq, Parse, ToCss)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", content = "value", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum FontWeight {
+  /// An absolute font weight.
+  Absolute(AbsoluteFontWeight),
+  /// The `bolder` keyword.
+  Bolder,
+  /// The `lighter` keyword.
+  Lighter,
+}
+
+impl Default for FontWeight {
+  fn default() -> FontWeight {
+    FontWeight::Absolute(AbsoluteFontWeight::default())
+  }
+}
+
+impl IsCompatible for FontWeight {
+  fn is_compatible(&self, browsers: crate::targets::Browsers) -> bool {
+    match self {
+      FontWeight::Absolute(a) => a.is_compatible(browsers),
+      FontWeight::Bolder | FontWeight::Lighter => true,
+    }
+  }
+}
+
+/// An [absolute font weight](https://www.w3.org/TR/css-fonts-4/#font-weight-absolute-values),
+/// as used in the `font-weight` property.
+///
+/// See [FontWeight](FontWeight).
+#[derive(Debug, Clone, PartialEq, Parse)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", content = "value", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub enum AbsoluteFontWeight {
+  /// An explicit weight.
+  Weight(CSSNumber),
+  /// Same as `400`.
+  Normal,
+  /// Same as `700`.
+  Bold,
+}
+
+impl Default for AbsoluteFontWeight {
+  fn default() -> AbsoluteFontWeight {
+    AbsoluteFontWeight::Normal
+  }
+}
+
+impl ToCss for AbsoluteFontWeight {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    use AbsoluteFontWeight::*;
+    match self {
+      Weight(val) => val.to_css(dest),
+      Normal => dest.write_str(if dest.minify { "400" } else { "normal" }),
+      Bold => dest.write_str(if dest.minify { "700" } else { "bold" }),
+    }
+  }
+}
+
+impl IsCompatible for AbsoluteFontWeight {
+  fn is_compatible(&self, browsers: crate::targets::Browsers) -> bool {
+    match self {
+      // Older browsers only supported 100, 200, 300, ...900 rather than arbitrary values.
+      AbsoluteFontWeight::Weight(val) if !(*val >= 100.0 && *val <= 900.0 && *val % 100.0 == 0.0) => {
+        Feature::FontWeightNumber.is_compatible(browsers)
+      }
+      _ => true,
+    }
+  }
+}
+
+enum_property! {
+  /// An [absolute font size](https://www.w3.org/TR/css-fonts-3/#absolute-size-value),
+  /// as used in the `font-size` property.
+  ///
+  /// See [FontSize](FontSize).
+  #[allow(missing_docs)]
+  pub enum AbsoluteFontSize {
+    "xx-small": XXSmall,
+    "x-small": XSmall,
+    "small": Small,
+    "medium": Medium,
+    "large": Large,
+    "x-large": XLarge,
+    "xx-large": XXLarge,
+    "xxx-large": XXXLarge,
+  }
+}
+
+impl IsCompatible for AbsoluteFontSize {
+  fn is_compatible(&self, browsers: crate::targets::Browsers) -> bool {
+    use AbsoluteFontSize::*;
+    match self {
+      XXXLarge => Feature::FontSizeXXXLarge.is_compatible(browsers),
+      _ => true,
+    }
+  }
+}
+
+enum_property! {
+  /// A [relative font size](https://www.w3.org/TR/css-fonts-3/#relative-size-value),
+  /// as used in the `font-size` property.
+  ///
+  /// See [FontSize](FontSize).
+  #[allow(missing_docs)]
+  pub enum RelativeFontSize {
+    Smaller,
+    Larger,
+  }
+}
+
+/// A value for the [font-size](https://www.w3.org/TR/css-fonts-4/#font-size-prop) property.
+#[derive(Debug, Clone, PartialEq, Parse, ToCss)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", content = "value", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum FontSize {
+  /// An explicit size.
+  Length(LengthPercentage),
+  /// An absolute font size keyword.
+  Absolute(AbsoluteFontSize),
+  /// A relative font size keyword.
+  Relative(RelativeFontSize),
+}
+
+impl IsCompatible for FontSize {
+  fn is_compatible(&self, browsers: crate::targets::Browsers) -> bool {
+    match self {
+      FontSize::Length(LengthPercentage::Dimension(LengthValue::Rem(..))) => {
+        Feature::FontSizeRem.is_compatible(browsers)
+      }
+      FontSize::Length(l) => l.is_compatible(browsers),
+      FontSize::Absolute(a) => a.is_compatible(browsers),
+      FontSize::Relative(..) => true,
+    }
+  }
+}
+
+enum_property! {
+  /// A [font stretch keyword](https://www.w3.org/TR/css-fonts-4/#font-stretch-prop),
+  /// as used in the `font-stretch` property.
+  ///
+  /// See [FontStretch](FontStretch).
+  pub enum FontStretchKeyword {
+    /// 100%
+    "normal": Normal,
+    /// 50%
+    "ultra-condensed": UltraCondensed,
+    /// 62.5%
+    "extra-condensed": ExtraCondensed,
+    /// 75%
+    "condensed": Condensed,
+    /// 87.5%
+    "semi-condensed": SemiCondensed,
+    /// 112.5%
+    "semi-expanded": SemiExpanded,
+    /// 125%
+    "expanded": Expanded,
+    /// 150%
+    "extra-expanded": ExtraExpanded,
+    /// 200%
+    "ultra-expanded": UltraExpanded,
+  }
+}
+
+impl Default for FontStretchKeyword {
+  fn default() -> FontStretchKeyword {
+    FontStretchKeyword::Normal
+  }
+}
+
+impl Into<Percentage> for &FontStretchKeyword {
+  fn into(self) -> Percentage {
+    use FontStretchKeyword::*;
+    let val = match self {
+      UltraCondensed => 0.5,
+      ExtraCondensed => 0.625,
+      Condensed => 0.75,
+      SemiCondensed => 0.875,
+      Normal => 1.0,
+      SemiExpanded => 1.125,
+      Expanded => 1.25,
+      ExtraExpanded => 1.5,
+      UltraExpanded => 2.0,
+    };
+    Percentage(val)
+  }
+}
+
+/// A value for the [font-stretch](https://www.w3.org/TR/css-fonts-4/#font-stretch-prop) property.
+#[derive(Debug, Clone, PartialEq, Parse)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", content = "value", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum FontStretch {
+  /// A font stretch keyword.
+  Keyword(FontStretchKeyword),
+  /// A percentage.
+  Percentage(Percentage),
+}
+
+impl Default for FontStretch {
+  fn default() -> FontStretch {
+    FontStretch::Keyword(FontStretchKeyword::default())
+  }
+}
+
+impl Into<Percentage> for &FontStretch {
+  fn into(self) -> Percentage {
+    match self {
+      FontStretch::Percentage(val) => val.clone(),
+      FontStretch::Keyword(keyword) => keyword.into(),
+    }
+  }
+}
+
+impl ToCss for FontStretch {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    if dest.minify {
+      let percentage: Percentage = self.into();
+      return percentage.to_css(dest);
+    }
+
+    match self {
+      FontStretch::Percentage(val) => val.to_css(dest),
+      FontStretch::Keyword(val) => val.to_css(dest),
+    }
+  }
+}
+
+impl IsCompatible for FontStretch {
+  fn is_compatible(&self, browsers: crate::targets::Browsers) -> bool {
+    match self {
+      FontStretch::Percentage(..) => Feature::FontStretchPercentage.is_compatible(browsers),
+      FontStretch::Keyword(..) => true,
+    }
+  }
+}
+
+enum_property! {
+  /// A [generic font family](https://www.w3.org/TR/css-fonts-4/#generic-font-families) name,
+  /// as used in the `font-family` property.
+  ///
+  /// See [FontFamily](FontFamily).
+  #[allow(missing_docs)]
+  #[derive(Eq, Hash)]
+  pub enum GenericFontFamily {
+    "serif": Serif,
+    "sans-serif": SansSerif,
+    "cursive": Cursive,
+    "fantasy": Fantasy,
+    "monospace": Monospace,
+    "system-ui": SystemUI,
+    "emoji": Emoji,
+    "math": Math,
+    "fangsong": FangSong,
+    "ui-serif": UISerif,
+    "ui-sans-serif": UISansSerif,
+    "ui-monospace": UIMonospace,
+    "ui-rounded": UIRounded,
+
+    // CSS wide keywords. These must be parsed as identifiers so they
+    // don't get serialized as strings.
+    // https://www.w3.org/TR/css-values-4/#common-keywords
+    "initial": Initial,
+    "inherit": Inherit,
+    "unset": Unset,
+    // Default is also reserved by the <custom-ident> type.
+    // https://www.w3.org/TR/css-values-4/#custom-idents
+    "default": Default,
+
+    // CSS defaulting keywords
+    // https://drafts.csswg.org/css-cascade-5/#defaulting-keywords
+    "revert": Revert,
+    "revert-layer": RevertLayer,
+  }
+}
+
+impl IsCompatible for GenericFontFamily {
+  fn is_compatible(&self, browsers: crate::targets::Browsers) -> bool {
+    use GenericFontFamily::*;
+    match self {
+      SystemUI => Feature::FontFamilySystemUi.is_compatible(browsers),
+      UISerif | UISansSerif | UIMonospace | UIRounded => Feature::ExtendedSystemFonts.is_compatible(browsers),
+      _ => true,
+    }
+  }
+}
+
+/// A value for the [font-family](https://www.w3.org/TR/css-fonts-4/#font-family-prop) property.
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(untagged))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub enum FontFamily<'i> {
+  /// A generic family name.
+  Generic(GenericFontFamily),
+  /// A custom family name.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  FamilyName(FamilyName<'i>),
+}
+
+impl<'i> Parse<'i> for FontFamily<'i> {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    if let Ok(value) = input.try_parse(GenericFontFamily::parse) {
+      return Ok(FontFamily::Generic(value));
+    }
+
+    let family = FamilyName::parse(input)?;
+    Ok(FontFamily::FamilyName(family))
+  }
+}
+
+impl<'i> ToCss for FontFamily<'i> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    match self {
+      FontFamily::Generic(val) => val.to_css(dest),
+      FontFamily::FamilyName(val) => val.to_css(dest),
+    }
+  }
+}
+
+/// A font [family name](https://drafts.csswg.org/css-fonts/#family-name-syntax).
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(transparent))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub struct FamilyName<'i>(#[cfg_attr(feature = "serde", serde(borrow))] CowArcStr<'i>);
+
+impl<'i> Parse<'i> for FamilyName<'i> {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    if let Ok(value) = input.try_parse(|i| i.expect_string_cloned()) {
+      return Ok(FamilyName(value.into()));
+    }
+
+    let value: CowArcStr<'i> = input.expect_ident()?.into();
+    let mut string = None;
+    while let Ok(ident) = input.try_parse(|i| i.expect_ident_cloned()) {
+      if string.is_none() {
+        string = Some(value.to_string());
+      }
+
+      if let Some(string) = &mut string {
+        string.push(' ');
+        string.push_str(&ident);
+      }
+    }
+
+    let value = if let Some(string) = string {
+      string.into()
+    } else {
+      value
+    };
+
+    Ok(FamilyName(value))
+  }
+}
+
+impl<'i> ToCss for FamilyName<'i> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    // Generic family names such as sans-serif must be quoted if parsed as a string.
+    // CSS wide keywords, as well as "default", must also be quoted.
+    // https://www.w3.org/TR/css-fonts-4/#family-name-syntax
+    let val = &self.0;
+    if !val.is_empty() && !GenericFontFamily::parse_string(val).is_ok() {
+      let mut id = String::new();
+      let mut first = true;
+      for slice in val.split(' ') {
+        if first {
+          first = false;
+        } else {
+          id.push(' ');
+        }
+        serialize_identifier(slice, &mut id)?;
+      }
+      if id.len() < val.len() + 2 {
+        return dest.write_str(&id);
+      }
+    }
+    serialize_string(&val, dest)?;
+    Ok(())
+  }
+}
+
+impl IsCompatible for FontFamily<'_> {
+  fn is_compatible(&self, browsers: crate::targets::Browsers) -> bool {
+    match self {
+      FontFamily::Generic(g) => g.is_compatible(browsers),
+      FontFamily::FamilyName(..) => true,
+    }
+  }
+}
+
+/// A value for the [font-style](https://www.w3.org/TR/css-fonts-4/#font-style-prop) property.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", content = "value", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum FontStyle {
+  /// Normal font style.
+  Normal,
+  /// Italic font style.
+  Italic,
+  /// Oblique font style, with a custom angle.
+  Oblique(#[cfg_attr(feature = "serde", serde(default = "FontStyle::default_oblique_angle"))] Angle),
+}
+
+impl Default for FontStyle {
+  fn default() -> FontStyle {
+    FontStyle::Normal
+  }
+}
+
+impl FontStyle {
+  #[inline]
+  pub(crate) fn default_oblique_angle() -> Angle {
+    Angle::Deg(14.0)
+  }
+}
+
+impl<'i> Parse<'i> for FontStyle {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let location = input.current_source_location();
+    let ident = input.expect_ident()?;
+    match_ignore_ascii_case! { &*ident,
+      "normal" => Ok(FontStyle::Normal),
+      "italic" => Ok(FontStyle::Italic),
+      "oblique" => {
+        let angle = input.try_parse(Angle::parse).unwrap_or(FontStyle::default_oblique_angle());
+        Ok(FontStyle::Oblique(angle))
+      },
+      _ => Err(location.new_unexpected_token_error(
+        cssparser::Token::Ident(ident.clone())
+      ))
+    }
+  }
+}
+
+impl ToCss for FontStyle {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    match self {
+      FontStyle::Normal => dest.write_str("normal"),
+      FontStyle::Italic => dest.write_str("italic"),
+      FontStyle::Oblique(angle) => {
+        dest.write_str("oblique")?;
+        if *angle != FontStyle::default_oblique_angle() {
+          dest.write_char(' ')?;
+          angle.to_css(dest)?;
+        }
+        Ok(())
+      }
+    }
+  }
+}
+
+impl IsCompatible for FontStyle {
+  fn is_compatible(&self, browsers: crate::targets::Browsers) -> bool {
+    match self {
+      FontStyle::Oblique(angle) if *angle != FontStyle::default_oblique_angle() => {
+        Feature::FontStyleObliqueAngle.is_compatible(browsers)
+      }
+      FontStyle::Normal | FontStyle::Italic | FontStyle::Oblique(..) => true,
+    }
+  }
+}
+
+enum_property! {
+  /// A value for the [font-variant-caps](https://www.w3.org/TR/css-fonts-4/#font-variant-caps-prop) property.
+  pub enum FontVariantCaps {
+    /// No special capitalization features are applied.
+    Normal,
+    /// The small capitals feature is used for lower case letters.
+    SmallCaps,
+    /// Small capitals are used for both upper and lower case letters.
+    AllSmallCaps,
+    /// Petite capitals are used.
+    PetiteCaps,
+    /// Petite capitals are used for both upper and lower case letters.
+    AllPetiteCaps,
+    /// Enables display of mixture of small capitals for uppercase letters with normal lowercase letters.
+    Unicase,
+    /// Uses titling capitals.
+    TitlingCaps,
+  }
+}
+
+impl Default for FontVariantCaps {
+  fn default() -> FontVariantCaps {
+    FontVariantCaps::Normal
+  }
+}
+
+impl FontVariantCaps {
+  fn is_css2(&self) -> bool {
+    matches!(self, FontVariantCaps::Normal | FontVariantCaps::SmallCaps)
+  }
+
+  fn parse_css2<'i, 't>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let value = Self::parse(input)?;
+    if !value.is_css2() {
+      return Err(input.new_custom_error(ParserError::InvalidValue));
+    }
+    Ok(value)
+  }
+}
+
+impl IsCompatible for FontVariantCaps {
+  fn is_compatible(&self, _browsers: crate::targets::Browsers) -> bool {
+    true
+  }
+}
+
+/// A value for the [line-height](https://www.w3.org/TR/2020/WD-css-inline-3-20200827/#propdef-line-height) property.
+#[derive(Debug, Clone, PartialEq, Parse, ToCss)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", content = "value", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum LineHeight {
+  /// The UA sets the line height based on the font.
+  Normal,
+  /// A multiple of the element's font size.
+  Number(CSSNumber),
+  /// An explicit height.
+  Length(LengthPercentage),
+}
+
+impl Default for LineHeight {
+  fn default() -> LineHeight {
+    LineHeight::Normal
+  }
+}
+
+impl IsCompatible for LineHeight {
+  fn is_compatible(&self, browsers: crate::targets::Browsers) -> bool {
+    match self {
+      LineHeight::Length(l) => l.is_compatible(browsers),
+      LineHeight::Normal | LineHeight::Number(..) => true,
+    }
+  }
+}
+
+enum_property! {
+  /// A keyword for the [vertical align](https://drafts.csswg.org/css2/#propdef-vertical-align) property.
+  pub enum VerticalAlignKeyword {
+    /// Align the baseline of the box with the baseline of the parent box.
+    Baseline,
+    /// Lower the baseline of the box to the proper position for subscripts of the parent’s box.
+    Sub,
+    /// Raise the baseline of the box to the proper position for superscripts of the parent’s box.
+    Super,
+    /// Align the top of the aligned subtree with the top of the line box.
+    Top,
+    /// Align the top of the box with the top of the parent’s content area.
+    TextTop,
+    /// Align the vertical midpoint of the box with the baseline of the parent box plus half the x-height of the parent.
+    Middle,
+    /// Align the bottom of the aligned subtree with the bottom of the line box.
+    Bottom,
+    /// Align the bottom of the box with the bottom of the parent’s content area.
+    TextBottom,
+  }
+}
+
+/// A value for the [vertical align](https://drafts.csswg.org/css2/#propdef-vertical-align) property.
+// TODO: there is a more extensive spec in CSS3 but it doesn't seem any browser implements it? https://www.w3.org/TR/css-inline-3/#transverse-alignment
+#[derive(Debug, Clone, PartialEq, Parse, ToCss)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", content = "value", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum VerticalAlign {
+  /// A vertical align keyword.
+  Keyword(VerticalAlignKeyword),
+  /// An explicit length.
+  Length(LengthPercentage),
+}
+
+define_shorthand! {
+  /// A value for the [font](https://www.w3.org/TR/css-fonts-4/#font-prop) shorthand property.
+  pub struct Font<'i> {
+    /// The font family.
+    #[cfg_attr(feature = "serde", serde(borrow))]
+    family: FontFamily(Vec<FontFamily<'i>>),
+    /// The font size.
+    size: FontSize(FontSize),
+    /// The font style.
+    style: FontStyle(FontStyle),
+    /// The font weight.
+    weight: FontWeight(FontWeight),
+    /// The font stretch.
+    stretch: FontStretch(FontStretch),
+    /// The line height.
+    line_height: LineHeight(LineHeight),
+    /// How the text should be capitalized. Only CSS 2.1 values are supported.
+    variant_caps: FontVariantCaps(FontVariantCaps),
+  }
+}
+
+impl<'i> Parse<'i> for Font<'i> {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let mut style = None;
+    let mut weight = None;
+    let mut stretch = None;
+    let size;
+    let mut variant_caps = None;
+    let mut count = 0;
+
+    loop {
+      // Skip "normal" since it is valid for several properties, but we don't know which ones it will be used for yet.
+      if input.try_parse(|input| input.expect_ident_matching("normal")).is_ok() {
+        count += 1;
+        continue;
+      }
+      if style.is_none() {
+        if let Ok(value) = input.try_parse(FontStyle::parse) {
+          style = Some(value);
+          count += 1;
+          continue;
+        }
+      }
+      if weight.is_none() {
+        if let Ok(value) = input.try_parse(FontWeight::parse) {
+          weight = Some(value);
+          count += 1;
+          continue;
+        }
+      }
+      if variant_caps.is_none() {
+        if let Ok(value) = input.try_parse(FontVariantCaps::parse_css2) {
+          variant_caps = Some(value);
+          count += 1;
+          continue;
+        }
+      }
+
+      if stretch.is_none() {
+        if let Ok(value) = input.try_parse(FontStretchKeyword::parse) {
+          stretch = Some(FontStretch::Keyword(value));
+          count += 1;
+          continue;
+        }
+      }
+      size = Some(FontSize::parse(input)?);
+      break;
+    }
+
+    if count > 4 {
+      return Err(input.new_custom_error(ParserError::InvalidDeclaration));
+    }
+
+    let size = match size {
+      Some(s) => s,
+      None => return Err(input.new_custom_error(ParserError::InvalidDeclaration)),
+    };
+
+    let line_height = if input.try_parse(|input| input.expect_delim('/')).is_ok() {
+      Some(LineHeight::parse(input)?)
+    } else {
+      None
+    };
+
+    let family = input.parse_comma_separated(FontFamily::parse)?;
+    Ok(Font {
+      family,
+      size,
+      style: style.unwrap_or_default(),
+      weight: weight.unwrap_or_default(),
+      stretch: stretch.unwrap_or_default(),
+      line_height: line_height.unwrap_or_default(),
+      variant_caps: variant_caps.unwrap_or_default(),
+    })
+  }
+}
+
+impl<'i> ToCss for Font<'i> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    if self.style != FontStyle::default() {
+      self.style.to_css(dest)?;
+      dest.write_char(' ')?;
+    }
+
+    if self.variant_caps != FontVariantCaps::default() {
+      self.variant_caps.to_css(dest)?;
+      dest.write_char(' ')?;
+    }
+
+    if self.weight != FontWeight::default() {
+      self.weight.to_css(dest)?;
+      dest.write_char(' ')?;
+    }
+
+    if self.stretch != FontStretch::default() {
+      self.stretch.to_css(dest)?;
+      dest.write_char(' ')?;
+    }
+
+    self.size.to_css(dest)?;
+
+    if self.line_height != LineHeight::default() {
+      dest.delim('/', true)?;
+      self.line_height.to_css(dest)?;
+    }
+
+    dest.write_char(' ')?;
+
+    let len = self.family.len();
+    for (idx, val) in self.family.iter().enumerate() {
+      val.to_css(dest)?;
+      if idx < len - 1 {
+        dest.delim(',', false)?;
+      }
+    }
+
+    Ok(())
+  }
+}
+
+property_bitflags! {
+  #[derive(Default, Debug)]
+  struct FontProperty: u8 {
+    const FontFamily = 1 << 0;
+    const FontSize = 1 << 1;
+    const FontStyle = 1 << 2;
+    const FontWeight = 1 << 3;
+    const FontStretch = 1 << 4;
+    const LineHeight = 1 << 5;
+    const FontVariantCaps = 1 << 6;
+    const Font = Self::FontFamily.bits() | Self::FontSize.bits() | Self::FontStyle.bits() | Self::FontWeight.bits() | Self::FontStretch.bits() | Self::LineHeight.bits() | Self::FontVariantCaps.bits();
+  }
+}
+
+#[derive(Default, Debug)]
+pub(crate) struct FontHandler<'i> {
+  family: Option<Vec<FontFamily<'i>>>,
+  size: Option<FontSize>,
+  style: Option<FontStyle>,
+  weight: Option<FontWeight>,
+  stretch: Option<FontStretch>,
+  line_height: Option<LineHeight>,
+  variant_caps: Option<FontVariantCaps>,
+  flushed_properties: FontProperty,
+  has_any: bool,
+}
+
+impl<'i> PropertyHandler<'i> for FontHandler<'i> {
+  fn handle_property(
+    &mut self,
+    property: &Property<'i>,
+    dest: &mut DeclarationList<'i>,
+    context: &mut PropertyHandlerContext<'i, '_>,
+  ) -> bool {
+    use Property::*;
+
+    macro_rules! flush {
+      ($prop: ident, $val: expr) => {{
+        if self.$prop.is_some() && self.$prop.as_ref().unwrap() != $val && matches!(context.targets.browsers, Some(targets) if !$val.is_compatible(targets)) {
+          self.flush(dest, context);
+        }
+      }};
+    }
+
+    macro_rules! property {
+      ($prop: ident, $val: ident) => {{
+        flush!($prop, $val);
+        self.$prop = Some($val.clone());
+        self.has_any = true;
+      }};
+    }
+
+    match property {
+      FontFamily(val) => property!(family, val),
+      FontSize(val) => property!(size, val),
+      FontStyle(val) => property!(style, val),
+      FontWeight(val) => property!(weight, val),
+      FontStretch(val) => property!(stretch, val),
+      FontVariantCaps(val) => property!(variant_caps, val),
+      LineHeight(val) => property!(line_height, val),
+      Font(val) => {
+        flush!(family, &val.family);
+        flush!(size, &val.size);
+        flush!(style, &val.style);
+        flush!(weight, &val.weight);
+        flush!(stretch, &val.stretch);
+        flush!(line_height, &val.line_height);
+        flush!(variant_caps, &val.variant_caps);
+        self.family = Some(val.family.clone());
+        self.size = Some(val.size.clone());
+        self.style = Some(val.style.clone());
+        self.weight = Some(val.weight.clone());
+        self.stretch = Some(val.stretch.clone());
+        self.line_height = Some(val.line_height.clone());
+        self.variant_caps = Some(val.variant_caps.clone());
+        self.has_any = true;
+        // TODO: reset other properties
+      }
+      Unparsed(val) if is_font_property(&val.property_id) => {
+        self.flush(dest, context);
+        self
+          .flushed_properties
+          .insert(FontProperty::try_from(&val.property_id).unwrap());
+        dest.push(property.clone());
+      }
+      _ => return false,
+    }
+
+    true
+  }
+
+  fn finalize(&mut self, decls: &mut DeclarationList<'i>, context: &mut PropertyHandlerContext<'i, '_>) {
+    self.flush(decls, context);
+    self.flushed_properties = FontProperty::empty();
+  }
+}
+
+impl<'i> FontHandler<'i> {
+  fn flush(&mut self, decls: &mut DeclarationList<'i>, context: &mut PropertyHandlerContext<'i, '_>) {
+    if !self.has_any {
+      return;
+    }
+
+    self.has_any = false;
+
+    macro_rules! push {
+      ($prop: ident, $val: expr) => {
+        decls.push(Property::$prop($val));
+        self.flushed_properties.insert(FontProperty::$prop);
+      };
+    }
+
+    let mut family = std::mem::take(&mut self.family);
+    if !self.flushed_properties.contains(FontProperty::FontFamily) {
+      family = compatible_font_family(family, !should_compile!(context.targets, FontFamilySystemUi));
+    }
+    let size = std::mem::take(&mut self.size);
+    let style = std::mem::take(&mut self.style);
+    let weight = std::mem::take(&mut self.weight);
+    let stretch = std::mem::take(&mut self.stretch);
+    let line_height = std::mem::take(&mut self.line_height);
+    let variant_caps = std::mem::take(&mut self.variant_caps);
+
+    if let Some(family) = &mut family {
+      if family.len() > 1 {
+        // Dedupe.
+        let mut seen = HashSet::new();
+        family.retain(|f| seen.insert(f.clone()));
+      }
+    }
+
+    if family.is_some()
+      && size.is_some()
+      && style.is_some()
+      && weight.is_some()
+      && stretch.is_some()
+      && line_height.is_some()
+      && variant_caps.is_some()
+    {
+      let caps = variant_caps.unwrap();
+      push!(
+        Font,
+        Font {
+          family: family.unwrap(),
+          size: size.unwrap(),
+          style: style.unwrap(),
+          weight: weight.unwrap(),
+          stretch: stretch.unwrap(),
+          line_height: line_height.unwrap(),
+          variant_caps: if caps.is_css2() {
+            caps
+          } else {
+            FontVariantCaps::default()
+          },
+        }
+      );
+
+      // The `font` property only accepts CSS 2.1 values for font-variant caps.
+      // If we have a CSS 3+ value, we need to add a separate property.
+      if !caps.is_css2() {
+        push!(FontVariantCaps, variant_caps.unwrap());
+      }
+    } else {
+      if let Some(val) = family {
+        push!(FontFamily, val);
+      }
+
+      if let Some(val) = size {
+        push!(FontSize, val);
+      }
+
+      if let Some(val) = style {
+        push!(FontStyle, val);
+      }
+
+      if let Some(val) = variant_caps {
+        push!(FontVariantCaps, val);
+      }
+
+      if let Some(val) = weight {
+        push!(FontWeight, val);
+      }
+
+      if let Some(val) = stretch {
+        push!(FontStretch, val);
+      }
+
+      if let Some(val) = line_height {
+        push!(LineHeight, val);
+      }
+    }
+  }
+}
+
+const SYSTEM_UI: FontFamily = FontFamily::Generic(GenericFontFamily::SystemUI);
+
+const DEFAULT_SYSTEM_FONTS: &[&str] = &[
+  // #1: Supported as the '-apple-system' value (macOS, Safari >= 9.2 < 11, Firefox >= 43)
+  "-apple-system",
+  // #2: Supported as the 'BlinkMacSystemFont' value (macOS, Chrome < 56)
+  "BlinkMacSystemFont",
+  "Segoe UI",  // Windows >= Vista
+  "Roboto",    // Android >= 4
+  "Noto Sans", // Plasma >= 5.5
+  "Ubuntu",    // Ubuntu >= 10.10
+  "Cantarell", // GNOME >= 3
+  "Helvetica Neue",
+];
+
+/// [`system-ui`](https://www.w3.org/TR/css-fonts-4/#system-ui-def) is a special generic font family
+/// It is platform dependent but if not supported by the target will simply be ignored
+/// This list is an attempt at providing that support
+#[inline]
+fn compatible_font_family(mut family: Option<Vec<FontFamily>>, is_supported: bool) -> Option<Vec<FontFamily>> {
+  if is_supported {
+    return family;
+  }
+
+  if let Some(families) = &mut family {
+    if let Some(position) = families.iter().position(|v| *v == SYSTEM_UI) {
+      families.splice(
+        (position + 1)..(position + 1),
+        DEFAULT_SYSTEM_FONTS
+          .iter()
+          .map(|name| FontFamily::FamilyName(FamilyName(CowArcStr::from(*name)))),
+      );
+    }
+  }
+
+  return family;
+}
+
+#[inline]
+fn is_font_property(property_id: &PropertyId) -> bool {
+  match property_id {
+    PropertyId::FontFamily
+    | PropertyId::FontSize
+    | PropertyId::FontStyle
+    | PropertyId::FontWeight
+    | PropertyId::FontStretch
+    | PropertyId::FontVariantCaps
+    | PropertyId::LineHeight
+    | PropertyId::Font => true,
+    _ => false,
+  }
+}
diff --git a/src/properties/grid.rs b/src/properties/grid.rs
new file mode 100644
index 0000000..6e70631
--- /dev/null
+++ b/src/properties/grid.rs
@@ -0,0 +1,1786 @@
+//! CSS properties related to grid layout.
+
+#![allow(non_upper_case_globals)]
+
+use crate::context::PropertyHandlerContext;
+use crate::declaration::{DeclarationBlock, DeclarationList};
+use crate::error::{Error, ErrorLocation, ParserError, PrinterError, PrinterErrorKind};
+use crate::macros::{define_shorthand, impl_shorthand};
+use crate::printer::Printer;
+use crate::properties::{Property, PropertyId};
+use crate::traits::{Parse, PropertyHandler, Shorthand, ToCss};
+use crate::values::ident::CustomIdent;
+use crate::values::length::serialize_dimension;
+use crate::values::number::{CSSInteger, CSSNumber};
+use crate::values::{ident::CustomIdentList, length::LengthPercentage};
+#[cfg(feature = "visitor")]
+use crate::visitor::Visit;
+use bitflags::bitflags;
+use cssparser::*;
+use smallvec::SmallVec;
+
+#[cfg(feature = "serde")]
+use crate::serialization::ValueWrapper;
+
+/// A [track sizing](https://drafts.csswg.org/css-grid-2/#track-sizing) value
+/// for the `grid-template-rows` and `grid-template-columns` properties.
+#[derive(Debug, Clone, PartialEq, Parse, ToCss)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub enum TrackSizing<'i> {
+  /// No explicit grid tracks.
+  None,
+  /// A list of grid tracks.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  TrackList(TrackList<'i>),
+}
+
+/// A [`<track-list>`](https://drafts.csswg.org/css-grid-2/#typedef-track-list) value,
+/// as used in the `grid-template-rows` and `grid-template-columns` properties.
+///
+/// See [TrackSizing](TrackSizing).
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(rename_all = "camelCase")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub struct TrackList<'i> {
+  /// A list of line names.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  pub line_names: Vec<CustomIdentList<'i>>,
+  /// A list of grid track items.
+  pub items: Vec<TrackListItem<'i>>,
+}
+
+/// Either a track size or `repeat()` function.
+///
+/// See [TrackList](TrackList).
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", content = "value", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub enum TrackListItem<'i> {
+  /// A track size.
+  TrackSize(TrackSize),
+  /// A `repeat()` function.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  TrackRepeat(TrackRepeat<'i>),
+}
+
+/// A [`<track-size>`](https://drafts.csswg.org/css-grid-2/#typedef-track-size) value,
+/// as used in the `grid-template-rows` and `grid-template-columns` properties.
+///
+/// See [TrackListItem](TrackListItem).
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum TrackSize {
+  /// An explicit track breadth.
+  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<TrackBreadth>"))]
+  TrackBreadth(TrackBreadth),
+  /// The `minmax()` function.
+  MinMax {
+    /// The minimum value.
+    min: TrackBreadth,
+    /// The maximum value.
+    max: TrackBreadth,
+  },
+  /// The `fit-content()` function.
+  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<LengthPercentage>"))]
+  FitContent(LengthPercentage),
+}
+
+impl Default for TrackSize {
+  fn default() -> TrackSize {
+    TrackSize::TrackBreadth(TrackBreadth::Auto)
+  }
+}
+
+/// A [track size list](https://drafts.csswg.org/css-grid-2/#auto-tracks), as used
+/// in the `grid-auto-rows` and `grid-auto-columns` properties.
+#[derive(Debug, Clone, PartialEq, Default)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(transparent))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub struct TrackSizeList(pub SmallVec<[TrackSize; 1]>);
+
+/// A [`<track-breadth>`](https://drafts.csswg.org/css-grid-2/#typedef-track-breadth) value.
+///
+/// See [TrackSize](TrackSize).
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", content = "value", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum TrackBreadth {
+  /// An explicit length.
+  Length(LengthPercentage),
+  /// A flex factor.
+  Flex(CSSNumber),
+  /// The `min-content` keyword.
+  MinContent,
+  /// The `max-content` keyword.
+  MaxContent,
+  /// The `auto` keyword.
+  Auto,
+}
+
+/// A [`<track-repeat>`](https://drafts.csswg.org/css-grid-2/#typedef-track-repeat) value,
+/// representing the `repeat()` function in a track list.
+///
+/// See [TrackListItem](TrackListItem).
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(rename_all = "camelCase")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub struct TrackRepeat<'i> {
+  /// The repeat count.
+  pub count: RepeatCount,
+  /// The line names to repeat.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  pub line_names: Vec<CustomIdentList<'i>>,
+  /// The track sizes to repeat.
+  pub track_sizes: Vec<TrackSize>,
+}
+
+/// A [`<repeat-count>`](https://drafts.csswg.org/css-grid-2/#typedef-track-repeat) value,
+/// used in the `repeat()` function.
+///
+/// See [TrackRepeat](TrackRepeat).
+#[derive(Debug, Clone, PartialEq, Parse, ToCss)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", content = "value", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum RepeatCount {
+  /// The number of times to repeat.
+  Number(CSSInteger),
+  /// The `auto-fill` keyword.
+  AutoFill,
+  /// The `auto-fit` keyword.
+  AutoFit,
+}
+
+impl<'i> Parse<'i> for TrackSize {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    if let Ok(breadth) = input.try_parse(TrackBreadth::parse) {
+      return Ok(TrackSize::TrackBreadth(breadth));
+    }
+
+    if input.try_parse(|input| input.expect_function_matching("minmax")).is_ok() {
+      return input.parse_nested_block(|input| {
+        let min = TrackBreadth::parse_internal(input, false)?;
+        input.expect_comma()?;
+        Ok(TrackSize::MinMax {
+          min,
+          max: TrackBreadth::parse(input)?,
+        })
+      });
+    }
+
+    input.expect_function_matching("fit-content")?;
+    let len = input.parse_nested_block(LengthPercentage::parse)?;
+    Ok(TrackSize::FitContent(len))
+  }
+}
+
+impl ToCss for TrackSize {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    match self {
+      TrackSize::TrackBreadth(breadth) => breadth.to_css(dest),
+      TrackSize::MinMax { min, max } => {
+        dest.write_str("minmax(")?;
+        min.to_css(dest)?;
+        dest.delim(',', false)?;
+        max.to_css(dest)?;
+        dest.write_char(')')
+      }
+      TrackSize::FitContent(len) => {
+        dest.write_str("fit-content(")?;
+        len.to_css(dest)?;
+        dest.write_char(')')
+      }
+    }
+  }
+}
+
+impl<'i> Parse<'i> for TrackBreadth {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    Self::parse_internal(input, true)
+  }
+}
+
+impl TrackBreadth {
+  fn parse_internal<'i, 't>(
+    input: &mut Parser<'i, 't>,
+    allow_flex: bool,
+  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    if let Ok(len) = input.try_parse(LengthPercentage::parse) {
+      return Ok(TrackBreadth::Length(len));
+    }
+
+    if allow_flex {
+      if let Ok(flex) = input.try_parse(Self::parse_flex) {
+        return Ok(TrackBreadth::Flex(flex));
+      }
+    }
+
+    let location = input.current_source_location();
+    let ident = input.expect_ident()?;
+    match_ignore_ascii_case! { &*ident,
+      "auto" => Ok(TrackBreadth::Auto),
+      "min-content" => Ok(TrackBreadth::MinContent),
+      "max-content" => Ok(TrackBreadth::MaxContent),
+      _ => Err(location.new_unexpected_token_error(
+        cssparser::Token::Ident(ident.clone())
+      ))
+    }
+  }
+
+  fn parse_flex<'i, 't>(input: &mut Parser<'i, 't>) -> Result<CSSNumber, ParseError<'i, ParserError<'i>>> {
+    let location = input.current_source_location();
+    match *input.next()? {
+      Token::Dimension { value, ref unit, .. } if unit.eq_ignore_ascii_case("fr") && value.is_sign_positive() => {
+        Ok(value)
+      }
+      ref t => Err(location.new_unexpected_token_error(t.clone())),
+    }
+  }
+}
+
+impl ToCss for TrackBreadth {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    match self {
+      TrackBreadth::Auto => dest.write_str("auto"),
+      TrackBreadth::MinContent => dest.write_str("min-content"),
+      TrackBreadth::MaxContent => dest.write_str("max-content"),
+      TrackBreadth::Length(len) => len.to_css(dest),
+      TrackBreadth::Flex(flex) => serialize_dimension(*flex, "fr", dest),
+    }
+  }
+}
+
+impl<'i> Parse<'i> for TrackRepeat<'i> {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    input.expect_function_matching("repeat")?;
+    input.parse_nested_block(|input| {
+      let count = RepeatCount::parse(input)?;
+      input.expect_comma()?;
+
+      let mut line_names = Vec::new();
+      let mut track_sizes = Vec::new();
+
+      loop {
+        let line_name = input.try_parse(parse_line_names).unwrap_or_default();
+        line_names.push(line_name);
+
+        if let Ok(track_size) = input.try_parse(TrackSize::parse) {
+          // TODO: error handling
+          track_sizes.push(track_size)
+        } else {
+          break;
+        }
+      }
+
+      Ok(TrackRepeat {
+        count,
+        line_names,
+        track_sizes,
+      })
+    })
+  }
+}
+
+impl<'i> ToCss for TrackRepeat<'i> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    dest.write_str("repeat(")?;
+    self.count.to_css(dest)?;
+    dest.delim(',', false)?;
+
+    let mut track_sizes_iter = self.track_sizes.iter();
+    let mut first = true;
+    for names in self.line_names.iter() {
+      if !names.is_empty() {
+        serialize_line_names(names, dest)?;
+      }
+
+      if let Some(size) = track_sizes_iter.next() {
+        // Whitespace is required if there are no line names.
+        if !names.is_empty() {
+          dest.whitespace()?;
+        } else if !first {
+          dest.write_char(' ')?;
+        }
+        size.to_css(dest)?;
+      }
+
+      first = false;
+    }
+
+    dest.write_char(')')
+  }
+}
+
+fn parse_line_names<'i, 't>(
+  input: &mut Parser<'i, 't>,
+) -> Result<CustomIdentList<'i>, ParseError<'i, ParserError<'i>>> {
+  input.expect_square_bracket_block()?;
+  input.parse_nested_block(|input| {
+    let mut values = SmallVec::new();
+    while let Ok(ident) = input.try_parse(CustomIdent::parse) {
+      values.push(ident)
+    }
+    Ok(values)
+  })
+}
+
+fn serialize_line_names<W>(names: &[CustomIdent], dest: &mut Printer<W>) -> Result<(), PrinterError>
+where
+  W: std::fmt::Write,
+{
+  dest.write_char('[')?;
+  let mut first = true;
+  for name in names {
+    if first {
+      first = false;
+    } else {
+      dest.write_char(' ')?;
+    }
+    write_ident(&name.0, dest)?;
+  }
+  dest.write_char(']')
+}
+
+fn write_ident<W>(name: &str, dest: &mut Printer<W>) -> Result<(), PrinterError>
+where
+  W: std::fmt::Write,
+{
+  let css_module_grid_enabled = dest.css_module.as_ref().map_or(false, |css_module| css_module.config.grid);
+  if css_module_grid_enabled {
+    if let Some(css_module) = &mut dest.css_module {
+      if let Some(last) = css_module.config.pattern.segments.last() {
+        if !matches!(last, crate::css_modules::Segment::Local) {
+          return Err(Error {
+            kind: PrinterErrorKind::InvalidCssModulesPatternInGrid,
+            loc: Some(ErrorLocation {
+              filename: dest.filename().into(),
+              line: dest.loc.line,
+              column: dest.loc.column,
+            }),
+          });
+        }
+      }
+    }
+  }
+  dest.write_ident(name, css_module_grid_enabled)?;
+  Ok(())
+}
+
+impl<'i> Parse<'i> for TrackList<'i> {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let mut line_names = Vec::new();
+    let mut items = Vec::new();
+
+    loop {
+      let line_name = input.try_parse(parse_line_names).unwrap_or_default();
+      line_names.push(line_name);
+
+      if let Ok(track_size) = input.try_parse(TrackSize::parse) {
+        // TODO: error handling
+        items.push(TrackListItem::TrackSize(track_size));
+      } else if let Ok(repeat) = input.try_parse(TrackRepeat::parse) {
+        // TODO: error handling
+        items.push(TrackListItem::TrackRepeat(repeat))
+      } else {
+        break;
+      }
+    }
+
+    if items.is_empty() {
+      return Err(input.new_custom_error(ParserError::InvalidDeclaration));
+    }
+
+    Ok(TrackList { line_names, items })
+  }
+}
+
+impl<'i> ToCss for TrackList<'i> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    let mut items_iter = self.items.iter();
+    let line_names_iter = self.line_names.iter();
+    let mut first = true;
+
+    for names in line_names_iter {
+      if !names.is_empty() {
+        serialize_line_names(names, dest)?;
+      }
+
+      if let Some(item) = items_iter.next() {
+        // Whitespace is required if there are no line names.
+        if !names.is_empty() {
+          dest.whitespace()?;
+        } else if !first {
+          dest.write_char(' ')?;
+        }
+        match item {
+          TrackListItem::TrackRepeat(repeat) => repeat.to_css(dest)?,
+          TrackListItem::TrackSize(size) => size.to_css(dest)?,
+        };
+      }
+
+      first = false;
+    }
+
+    Ok(())
+  }
+}
+
+impl<'i> TrackList<'i> {
+  fn is_explicit(&self) -> bool {
+    self.items.iter().all(|item| matches!(item, TrackListItem::TrackSize(_)))
+  }
+}
+
+impl<'i> TrackSizing<'i> {
+  fn is_explicit(&self) -> bool {
+    match self {
+      TrackSizing::None => true,
+      TrackSizing::TrackList(list) => list.is_explicit(),
+    }
+  }
+}
+
+impl<'i> Parse<'i> for TrackSizeList {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let mut res = SmallVec::new();
+    while let Ok(size) = input.try_parse(TrackSize::parse) {
+      res.push(size)
+    }
+    if res.len() == 1 && res[0] == TrackSize::default() {
+      res.clear();
+    }
+    Ok(TrackSizeList(res))
+  }
+}
+
+impl ToCss for TrackSizeList {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    if self.0.len() == 0 {
+      return dest.write_str("auto");
+    }
+
+    let mut first = true;
+    for item in &self.0 {
+      if first {
+        first = false;
+      } else {
+        dest.write_char(' ')?;
+      }
+      item.to_css(dest)?;
+    }
+    Ok(())
+  }
+}
+
+/// A value for the [grid-template-areas](https://drafts.csswg.org/css-grid-2/#grid-template-areas-property) property.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum GridTemplateAreas {
+  /// No named grid areas.
+  None,
+  /// Defines the list of named grid areas.
+  Areas {
+    /// The number of columns in the grid.
+    columns: u32,
+    /// A flattened list of grid area names.
+    /// Unnamed areas specified by the `.` token are represented as `None`.
+    areas: Vec<Option<String>>,
+  },
+}
+
+impl<'i> Parse<'i> for GridTemplateAreas {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    if input.try_parse(|input| input.expect_ident_matching("none")).is_ok() {
+      return Ok(GridTemplateAreas::None);
+    }
+
+    let mut tokens = Vec::new();
+    let mut row = 0;
+    let mut columns = 0;
+    while let Ok(s) = input.try_parse(|input| input.expect_string().map(|s| s.as_ref().to_owned())) {
+      let parsed_columns = Self::parse_string(&s, &mut tokens)
+        .map_err(|()| input.new_error(BasicParseErrorKind::QualifiedRuleInvalid))?;
+
+      if row == 0 {
+        columns = parsed_columns;
+      } else if parsed_columns != columns {
+        return Err(input.new_custom_error(ParserError::InvalidDeclaration));
+      }
+
+      row += 1;
+    }
+
+    Ok(GridTemplateAreas::Areas { columns, areas: tokens })
+  }
+}
+
+impl GridTemplateAreas {
+  fn parse_string(string: &str, tokens: &mut Vec<Option<String>>) -> Result<u32, ()> {
+    let mut string = string;
+    let mut column = 0;
+    loop {
+      let rest = string.trim_start_matches(HTML_SPACE_CHARACTERS);
+      if rest.is_empty() {
+        // Each string must produce a valid token.
+        if column == 0 {
+          return Err(());
+        }
+        break;
+      }
+
+      column += 1;
+
+      if rest.starts_with('.') {
+        string = &rest[rest.find(|c| c != '.').unwrap_or(rest.len())..];
+        tokens.push(None);
+        continue;
+      }
+
+      if !rest.starts_with(is_name_code_point) {
+        return Err(());
+      }
+
+      let token_len = rest.find(|c| !is_name_code_point(c)).unwrap_or(rest.len());
+      let token = &rest[..token_len];
+      tokens.push(Some(token.into()));
+      string = &rest[token_len..];
+    }
+
+    Ok(column)
+  }
+}
+
+static HTML_SPACE_CHARACTERS: &'static [char] = &['\u{0020}', '\u{0009}', '\u{000a}', '\u{000c}', '\u{000d}'];
+
+fn is_name_code_point(c: char) -> bool {
+  c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z' || c >= '\u{80}' || c == '_' || c >= '0' && c <= '9' || c == '-'
+}
+
+impl ToCss for GridTemplateAreas {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    match self {
+      GridTemplateAreas::None => dest.write_str("none"),
+      GridTemplateAreas::Areas { areas, .. } => {
+        let mut iter = areas.iter();
+        let mut next = iter.next();
+        let mut first = true;
+        while next.is_some() {
+          if !first && !dest.minify {
+            dest.newline()?;
+          }
+
+          self.write_string(dest, &mut iter, &mut next)?;
+
+          if first {
+            first = false;
+            if !dest.minify {
+              // Indent by the width of "grid-template-areas: ", so the rows line up.
+              dest.indent_by(21);
+            }
+          }
+        }
+
+        if !dest.minify {
+          dest.dedent_by(21);
+        }
+
+        Ok(())
+      }
+    }
+  }
+}
+
+impl GridTemplateAreas {
+  fn write_string<'a, W>(
+    &self,
+    dest: &mut Printer<W>,
+    iter: &mut std::slice::Iter<'a, Option<String>>,
+    next: &mut Option<&'a Option<String>>,
+  ) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    let columns = match self {
+      GridTemplateAreas::Areas { columns, .. } => *columns,
+      _ => unreachable!(),
+    };
+
+    dest.write_char('"')?;
+
+    let mut last_was_null = false;
+    for i in 0..columns {
+      if let Some(token) = next {
+        if let Some(string) = token {
+          if i > 0 && (!last_was_null || !dest.minify) {
+            dest.write_char(' ')?;
+          }
+          write_ident(string, dest)?;
+          last_was_null = false;
+        } else {
+          if i > 0 && (last_was_null || !dest.minify) {
+            dest.write_char(' ')?;
+          }
+          dest.write_char('.')?;
+          last_was_null = true;
+        }
+      }
+
+      *next = iter.next();
+    }
+
+    dest.write_char('"')
+  }
+}
+
+/// A value for the [grid-template](https://drafts.csswg.org/css-grid-2/#explicit-grid-shorthand) shorthand property.
+///
+/// If `areas` is not `None`, then `rows` must also not be `None`.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub struct GridTemplate<'i> {
+  /// The grid template rows.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  pub rows: TrackSizing<'i>,
+  /// The grid template columns.
+  pub columns: TrackSizing<'i>,
+  /// The named grid areas.
+  pub areas: GridTemplateAreas,
+}
+
+impl<'i> Parse<'i> for GridTemplate<'i> {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    if input.try_parse(|input| input.expect_ident_matching("none")).is_ok() {
+      input.expect_exhausted()?;
+      return Ok(GridTemplate {
+        rows: TrackSizing::None,
+        columns: TrackSizing::None,
+        areas: GridTemplateAreas::None,
+      });
+    }
+
+    let start = input.state();
+    let mut line_names: Vec<CustomIdentList<'i>> = Vec::new();
+    let mut items = Vec::new();
+    let mut columns = 0;
+    let mut row = 0;
+    let mut tokens = Vec::new();
+
+    loop {
+      if let Ok(first_names) = input.try_parse(parse_line_names) {
+        if let Some(last_names) = line_names.last_mut() {
+          last_names.extend(first_names);
+        } else {
+          line_names.push(first_names);
+        }
+      }
+
+      if let Ok(string) = input.try_parse(|input| input.expect_string().map(|s| s.as_ref().to_owned())) {
+        let parsed_columns = GridTemplateAreas::parse_string(&string, &mut tokens)
+          .map_err(|()| input.new_custom_error(ParserError::InvalidDeclaration))?;
+
+        if row == 0 {
+          columns = parsed_columns;
+        } else if parsed_columns != columns {
+          return Err(input.new_custom_error(ParserError::InvalidDeclaration));
+        }
+
+        row += 1;
+
+        let track_size = input.try_parse(TrackSize::parse).unwrap_or_default();
+        items.push(TrackListItem::TrackSize(track_size));
+
+        let last_names = input.try_parse(parse_line_names).unwrap_or_default();
+        line_names.push(last_names);
+      } else {
+        break;
+      }
+    }
+
+    if !tokens.is_empty() {
+      if line_names.len() == items.len() {
+        line_names.push(Default::default());
+      }
+
+      let areas = GridTemplateAreas::Areas { columns, areas: tokens };
+      let rows = TrackSizing::TrackList(TrackList { line_names, items });
+      let columns = if input.try_parse(|input| input.expect_delim('/')).is_ok() {
+        let list = TrackList::parse(input)?;
+        if !list.is_explicit() {
+          return Err(input.new_custom_error(ParserError::InvalidDeclaration));
+        }
+        TrackSizing::TrackList(list)
+      } else {
+        TrackSizing::None
+      };
+      Ok(GridTemplate { rows, columns, areas })
+    } else {
+      input.reset(&start);
+      let rows = TrackSizing::parse(input)?;
+      input.expect_delim('/')?;
+      let columns = TrackSizing::parse(input)?;
+      Ok(GridTemplate {
+        rows,
+        columns,
+        areas: GridTemplateAreas::None,
+      })
+    }
+  }
+}
+
+impl ToCss for GridTemplate<'_> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    self.to_css_with_indent(dest, 15)
+  }
+}
+
+impl GridTemplate<'_> {
+  fn to_css_with_indent<W>(&self, dest: &mut Printer<W>, indent: u8) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    match &self.areas {
+      GridTemplateAreas::None => {
+        if self.rows == TrackSizing::None && self.columns == TrackSizing::None {
+          dest.write_str("none")?;
+        } else {
+          self.rows.to_css(dest)?;
+          dest.delim('/', true)?;
+          self.columns.to_css(dest)?;
+        }
+      }
+      GridTemplateAreas::Areas { areas, .. } => {
+        let track_list = match &self.rows {
+          TrackSizing::TrackList(list) => list,
+          _ => unreachable!(),
+        };
+
+        let mut areas_iter = areas.iter();
+        let mut line_names_iter = track_list.line_names.iter();
+        let mut items_iter = track_list.items.iter();
+
+        let mut next = areas_iter.next();
+        let mut first = true;
+        let mut indented = false;
+        while next.is_some() {
+          macro_rules! newline {
+            () => {
+              if !dest.minify {
+                if !indented {
+                  // Indent by the width of "grid-template: ", so the rows line up.
+                  dest.indent_by(indent);
+                  indented = true;
+                }
+                dest.newline()?;
+              }
+            };
+          }
+
+          if let Some(line_names) = line_names_iter.next() {
+            if !line_names.is_empty() {
+              if !dest.minify && line_names.len() == 2 {
+                dest.whitespace()?;
+                serialize_line_names(&line_names[0..1], dest)?;
+                newline!();
+                serialize_line_names(&line_names[1..], dest)?;
+              } else {
+                if !first {
+                  newline!();
+                }
+                serialize_line_names(line_names, dest)?;
+              }
+              dest.whitespace()?;
+            } else if !first {
+              newline!();
+            }
+          } else if !first {
+            newline!();
+          }
+
+          self.areas.write_string(dest, &mut areas_iter, &mut next)?;
+
+          if let Some(item) = items_iter.next() {
+            if *item != TrackListItem::TrackSize(TrackSize::default()) {
+              dest.whitespace()?;
+              match item {
+                TrackListItem::TrackSize(size) => size.to_css(dest)?,
+                _ => unreachable!(),
+              }
+            }
+          }
+
+          first = false;
+        }
+
+        if let Some(line_names) = line_names_iter.next() {
+          if !line_names.is_empty() {
+            dest.whitespace()?;
+            serialize_line_names(line_names, dest)?;
+          }
+        }
+
+        if let TrackSizing::TrackList(track_list) = &self.columns {
+          dest.newline()?;
+          dest.delim('/', false)?;
+          track_list.to_css(dest)?;
+        }
+
+        if indented {
+          dest.dedent_by(indent);
+        }
+      }
+    }
+
+    Ok(())
+  }
+}
+
+impl<'i> GridTemplate<'i> {
+  #[inline]
+  fn is_valid(rows: &TrackSizing, columns: &TrackSizing, areas: &GridTemplateAreas) -> bool {
+    // The `grid-template` shorthand supports only explicit track values (i.e. no `repeat()`)
+    // combined with grid-template-areas. If there are no areas, then any track values are allowed.
+    *areas == GridTemplateAreas::None
+      || (*rows != TrackSizing::None && rows.is_explicit() && columns.is_explicit())
+  }
+}
+
+impl_shorthand! {
+  GridTemplate(GridTemplate<'i>) {
+    rows: [GridTemplateRows],
+    columns: [GridTemplateColumns],
+    areas: [GridTemplateAreas],
+  }
+
+  fn is_valid(shorthand) {
+    GridTemplate::is_valid(&shorthand.rows, &shorthand.columns, &shorthand.areas)
+  }
+}
+
+bitflags! {
+  /// A value for the [grid-auto-flow](https://drafts.csswg.org/css-grid-2/#grid-auto-flow-property) property.
+  ///
+  /// The `Row` or `Column` flags may be combined with the `Dense` flag, but the `Row` and `Column` flags may
+  /// not be combined.
+  #[cfg_attr(feature = "visitor", derive(Visit))]
+  #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(from = "SerializedGridAutoFlow", into = "SerializedGridAutoFlow"))]
+  #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+  #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Clone, Copy)]
+  pub struct GridAutoFlow: u8 {
+    /// The auto-placement algorithm places items by filling each row, adding new rows as necessary.
+    const Row    = 0b00;
+    /// The auto-placement algorithm places items by filling each column, adding new columns as necessary.
+    const Column = 0b01;
+    /// If specified, a dense packing algorithm is used, which fills in holes in the grid.
+    const Dense  = 0b10;
+  }
+}
+
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+struct SerializedGridAutoFlow {
+  /// The direction of the auto flow.
+  direction: AutoFlowDirection,
+  /// If specified, a dense packing algorithm is used, which fills in holes in the grid.
+  dense: bool,
+}
+
+impl From<GridAutoFlow> for SerializedGridAutoFlow {
+  fn from(flow: GridAutoFlow) -> Self {
+    Self {
+      direction: if flow.contains(GridAutoFlow::Column) {
+        AutoFlowDirection::Column
+      } else {
+        AutoFlowDirection::Row
+      },
+      dense: flow.contains(GridAutoFlow::Dense),
+    }
+  }
+}
+
+impl From<SerializedGridAutoFlow> for GridAutoFlow {
+  fn from(s: SerializedGridAutoFlow) -> GridAutoFlow {
+    let mut flow = match s.direction {
+      AutoFlowDirection::Row => GridAutoFlow::Row,
+      AutoFlowDirection::Column => GridAutoFlow::Column,
+    };
+    if s.dense {
+      flow |= GridAutoFlow::Dense
+    }
+
+    flow
+  }
+}
+
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(rename_all = "lowercase")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+enum AutoFlowDirection {
+  Row,
+  Column,
+}
+
+#[cfg(feature = "jsonschema")]
+#[cfg_attr(docsrs, doc(cfg(feature = "jsonschema")))]
+impl<'a> schemars::JsonSchema for GridAutoFlow {
+  fn is_referenceable() -> bool {
+    true
+  }
+
+  fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
+    SerializedGridAutoFlow::json_schema(gen)
+  }
+
+  fn schema_name() -> String {
+    "GridAutoFlow".into()
+  }
+}
+
+impl Default for GridAutoFlow {
+  fn default() -> GridAutoFlow {
+    GridAutoFlow::Row
+  }
+}
+
+impl GridAutoFlow {
+  fn direction(self) -> GridAutoFlow {
+    self & GridAutoFlow::Column
+  }
+}
+
+impl<'i> Parse<'i> for GridAutoFlow {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let mut flow = GridAutoFlow::Row;
+
+    macro_rules! match_dense {
+      () => {
+        if input.try_parse(|input| input.expect_ident_matching("dense")).is_ok() {
+          flow |= GridAutoFlow::Dense;
+        }
+      };
+    }
+
+    let location = input.current_source_location();
+    let ident = input.expect_ident()?;
+    match_ignore_ascii_case! { &ident,
+      "row" => {
+        match_dense!();
+      },
+      "column" => {
+        flow = GridAutoFlow::Column;
+        match_dense!();
+      },
+      "dense" => {
+        let location = input.current_source_location();
+        input.try_parse(|input| {
+          let ident = input.expect_ident()?;
+          match_ignore_ascii_case! { &ident,
+            "row" => {},
+            "column" => {
+              flow = GridAutoFlow::Column;
+            },
+            _ => return Err(location.new_unexpected_token_error(
+              cssparser::Token::Ident(ident.clone())
+            ))
+          }
+          Ok(())
+        })?;
+        flow |= GridAutoFlow::Dense;
+      },
+      _ => return Err(location.new_unexpected_token_error(
+        cssparser::Token::Ident(ident.clone())
+      ))
+    }
+
+    Ok(flow)
+  }
+}
+
+impl ToCss for GridAutoFlow {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    let s = if *self == GridAutoFlow::Row {
+      "row"
+    } else if *self == GridAutoFlow::Column {
+      "column"
+    } else if *self == GridAutoFlow::Row | GridAutoFlow::Dense {
+      if dest.minify {
+        "dense"
+      } else {
+        "row dense"
+      }
+    } else if *self == GridAutoFlow::Column | GridAutoFlow::Dense {
+      "column dense"
+    } else {
+      unreachable!();
+    };
+
+    dest.write_str(s)
+  }
+}
+
+/// A value for the [grid](https://drafts.csswg.org/css-grid-2/#grid-shorthand) shorthand property.
+///
+/// Explicit and implicit values may not be combined.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(rename_all = "camelCase")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub struct Grid<'i> {
+  /// Explicit grid template rows.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  pub rows: TrackSizing<'i>,
+  /// Explicit grid template columns.
+  pub columns: TrackSizing<'i>,
+  /// Explicit grid template areas.
+  pub areas: GridTemplateAreas,
+  /// The grid auto rows.
+  pub auto_rows: TrackSizeList,
+  /// The grid auto columns.
+  pub auto_columns: TrackSizeList,
+  /// The grid auto flow.
+  pub auto_flow: GridAutoFlow,
+}
+
+impl<'i> Parse<'i> for Grid<'i> {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    // <'grid-template'>
+    if let Ok(template) = input.try_parse(GridTemplate::parse) {
+      Ok(Grid {
+        rows: template.rows,
+        columns: template.columns,
+        areas: template.areas,
+        auto_rows: TrackSizeList::default(),
+        auto_columns: TrackSizeList::default(),
+        auto_flow: GridAutoFlow::default(),
+      })
+
+    // <'grid-template-rows'> / [ auto-flow && dense? ] <'grid-auto-columns'>?
+    } else if let Ok(rows) = input.try_parse(TrackSizing::parse) {
+      input.expect_delim('/')?;
+      let auto_flow = parse_grid_auto_flow(input, GridAutoFlow::Column)?;
+      let auto_columns = TrackSizeList::parse(input).unwrap_or_default();
+      Ok(Grid {
+        rows,
+        columns: TrackSizing::None,
+        areas: GridTemplateAreas::None,
+        auto_rows: TrackSizeList::default(),
+        auto_columns,
+        auto_flow,
+      })
+
+    // [ auto-flow && dense? ] <'grid-auto-rows'>? / <'grid-template-columns'>
+    } else {
+      let auto_flow = parse_grid_auto_flow(input, GridAutoFlow::Row)?;
+      let auto_rows = input.try_parse(TrackSizeList::parse).unwrap_or_default();
+      input.expect_delim('/')?;
+      let columns = TrackSizing::parse(input)?;
+      Ok(Grid {
+        rows: TrackSizing::None,
+        columns,
+        areas: GridTemplateAreas::None,
+        auto_rows,
+        auto_columns: TrackSizeList::default(),
+        auto_flow,
+      })
+    }
+  }
+}
+
+fn parse_grid_auto_flow<'i, 't>(
+  input: &mut Parser<'i, 't>,
+  flow: GridAutoFlow,
+) -> Result<GridAutoFlow, ParseError<'i, ParserError<'i>>> {
+  if input.try_parse(|input| input.expect_ident_matching("auto-flow")).is_ok() {
+    if input.try_parse(|input| input.expect_ident_matching("dense")).is_ok() {
+      Ok(flow | GridAutoFlow::Dense)
+    } else {
+      Ok(flow)
+    }
+  } else if input.try_parse(|input| input.expect_ident_matching("dense")).is_ok() {
+    input.expect_ident_matching("auto-flow")?;
+    Ok(flow | GridAutoFlow::Dense)
+  } else {
+    Err(input.new_error_for_next_token())
+  }
+}
+
+impl ToCss for Grid<'_> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    let is_auto_initial = self.auto_rows == TrackSizeList::default()
+      && self.auto_columns == TrackSizeList::default()
+      && self.auto_flow == GridAutoFlow::default();
+
+    if self.areas != GridTemplateAreas::None
+      || (self.rows != TrackSizing::None && self.columns != TrackSizing::None)
+      || (self.areas == GridTemplateAreas::None && is_auto_initial)
+    {
+      if !is_auto_initial {
+        unreachable!("invalid grid shorthand: mixed implicit and explicit values");
+      }
+      let template = GridTemplate {
+        rows: self.rows.clone(),
+        columns: self.columns.clone(),
+        areas: self.areas.clone(),
+      };
+      template.to_css_with_indent(dest, 6)?;
+    } else if self.auto_flow.direction() == GridAutoFlow::Column {
+      if self.columns != TrackSizing::None || self.auto_rows != TrackSizeList::default() {
+        unreachable!("invalid grid shorthand: mixed implicit and explicit values");
+      }
+      self.rows.to_css(dest)?;
+      dest.delim('/', true)?;
+      dest.write_str("auto-flow")?;
+      if self.auto_flow.contains(GridAutoFlow::Dense) {
+        dest.write_str(" dense")?;
+      }
+      if self.auto_columns != TrackSizeList::default() {
+        dest.write_char(' ')?;
+        self.auto_columns.to_css(dest)?;
+      }
+    } else {
+      if self.rows != TrackSizing::None || self.auto_columns != TrackSizeList::default() {
+        unreachable!("invalid grid shorthand: mixed implicit and explicit values");
+      }
+      dest.write_str("auto-flow")?;
+      if self.auto_flow.contains(GridAutoFlow::Dense) {
+        dest.write_str(" dense")?;
+      }
+      if self.auto_rows != TrackSizeList::default() {
+        dest.write_char(' ')?;
+        self.auto_rows.to_css(dest)?;
+      }
+      dest.delim('/', true)?;
+      self.columns.to_css(dest)?;
+    }
+
+    Ok(())
+  }
+}
+
+impl<'i> Grid<'i> {
+  #[inline]
+  fn is_valid(
+    rows: &TrackSizing,
+    columns: &TrackSizing,
+    areas: &GridTemplateAreas,
+    auto_rows: &TrackSizeList,
+    auto_columns: &TrackSizeList,
+    auto_flow: &GridAutoFlow,
+  ) -> bool {
+    // The `grid` shorthand can either be fully explicit (e.g. same as `grid-template`),
+    // or explicit along a single axis. If there are auto rows, then there cannot be explicit rows, for example.
+    let is_template = GridTemplate::is_valid(rows, columns, areas);
+    let default_track_size_list = TrackSizeList::default();
+    let is_explicit = *auto_rows == default_track_size_list
+      && *auto_columns == default_track_size_list
+      && *auto_flow == GridAutoFlow::default();
+    let is_auto_rows = auto_flow.direction() == GridAutoFlow::Row
+      && *rows == TrackSizing::None
+      && *auto_columns == default_track_size_list;
+    let is_auto_columns = auto_flow.direction() == GridAutoFlow::Column
+      && *columns == TrackSizing::None
+      && *auto_rows == default_track_size_list;
+
+    (is_template && is_explicit) || is_auto_rows || is_auto_columns
+  }
+}
+
+impl_shorthand! {
+  Grid(Grid<'i>) {
+    rows: [GridTemplateRows],
+    columns: [GridTemplateColumns],
+    areas: [GridTemplateAreas],
+    auto_rows: [GridAutoRows],
+    auto_columns: [GridAutoColumns],
+    auto_flow: [GridAutoFlow],
+  }
+
+  fn is_valid(grid) {
+    Grid::is_valid(&grid.rows, &grid.columns, &grid.areas, &grid.auto_rows, &grid.auto_columns, &grid.auto_flow)
+  }
+}
+
+/// A [`<grid-line>`](https://drafts.csswg.org/css-grid-2/#typedef-grid-row-start-grid-line) value,
+/// used in the `grid-row-start`, `grid-row-end`, `grid-column-start`, and `grid-column-end` properties.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub enum GridLine<'i> {
+  /// Automatic placement.
+  Auto,
+  /// A named grid area name (automatically postfixed by `-start` or `-end`), or and explicit grid line name.
+  Area {
+    /// A grid area name.
+    name: CustomIdent<'i>,
+  },
+  /// The Nth grid line, optionally filtered by line name. Negative numbers count backwards from the end.
+  Line {
+    /// A line number.
+    index: CSSInteger,
+    /// A line name to filter by.
+    #[cfg_attr(feature = "serde", serde(borrow))]
+    name: Option<CustomIdent<'i>>,
+  },
+  /// A grid span based on the Nth grid line from the opposite edge, optionally filtered by line name.
+  Span {
+    /// A line number.
+    index: CSSInteger,
+    /// A line name to filter by.
+    name: Option<CustomIdent<'i>>,
+  },
+}
+
+impl<'i> Parse<'i> for GridLine<'i> {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    if input.try_parse(|input| input.expect_ident_matching("auto")).is_ok() {
+      return Ok(GridLine::Auto);
+    }
+
+    if input.try_parse(|input| input.expect_ident_matching("span")).is_ok() {
+      // TODO: is calc() supported here??
+      let (index, name) = if let Ok(line_number) = input.try_parse(CSSInteger::parse) {
+        let ident = input.try_parse(CustomIdent::parse).ok();
+        (line_number, ident)
+      } else if let Ok(ident) = input.try_parse(CustomIdent::parse) {
+        let line_number = input.try_parse(CSSInteger::parse).unwrap_or(1);
+        (line_number, Some(ident))
+      } else {
+        return Err(input.new_custom_error(ParserError::InvalidDeclaration));
+      };
+
+      if index == 0 {
+        return Err(input.new_custom_error(ParserError::InvalidDeclaration));
+      }
+
+      return Ok(GridLine::Span { index, name });
+    }
+
+    if let Ok(index) = input.try_parse(CSSInteger::parse) {
+      if index == 0 {
+        return Err(input.new_custom_error(ParserError::InvalidDeclaration));
+      }
+      let name = input.try_parse(CustomIdent::parse).ok();
+      return Ok(GridLine::Line { index, name });
+    }
+
+    let name = CustomIdent::parse(input)?;
+    if let Ok(index) = input.try_parse(CSSInteger::parse) {
+      if index == 0 {
+        return Err(input.new_custom_error(ParserError::InvalidDeclaration));
+      }
+      return Ok(GridLine::Line {
+        index,
+        name: Some(name),
+      });
+    }
+
+    Ok(GridLine::Area { name })
+  }
+}
+
+impl ToCss for GridLine<'_> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    match self {
+      GridLine::Auto => dest.write_str("auto"),
+      GridLine::Area { name } => write_ident(&name.0, dest),
+      GridLine::Line { index, name } => {
+        index.to_css(dest)?;
+        if let Some(id) = name {
+          dest.write_char(' ')?;
+          write_ident(&id.0, dest)?;
+        }
+        Ok(())
+      }
+      GridLine::Span { index, name } => {
+        dest.write_str("span ")?;
+        if *index != 1 || name.is_none() {
+          index.to_css(dest)?;
+          if name.is_some() {
+            dest.write_char(' ')?;
+          }
+        }
+
+        if let Some(id) = name {
+          write_ident(&id.0, dest)?;
+        }
+        Ok(())
+      }
+    }
+  }
+}
+
+impl<'i> GridLine<'i> {
+  fn default_end_value(&self) -> GridLine<'i> {
+    if matches!(self, GridLine::Area { .. }) {
+      self.clone()
+    } else {
+      GridLine::Auto
+    }
+  }
+
+  fn can_omit_end(&self, end: &GridLine) -> bool {
+    if let GridLine::Area { name: start_id } = &self {
+      matches!(end, GridLine::Area { name: end_id } if end_id == start_id)
+    } else if matches!(end, GridLine::Auto) {
+      true
+    } else {
+      false
+    }
+  }
+}
+
+macro_rules! impl_grid_placement {
+  ($name: ident) => {
+    impl<'i> Parse<'i> for $name<'i> {
+      fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+        let start = GridLine::parse(input)?;
+        let end = if input.try_parse(|input| input.expect_delim('/')).is_ok() {
+          GridLine::parse(input)?
+        } else {
+          start.default_end_value()
+        };
+
+        Ok($name { start, end })
+      }
+    }
+
+    impl ToCss for $name<'_> {
+      fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+      where
+        W: std::fmt::Write,
+      {
+        self.start.to_css(dest)?;
+
+        if !self.start.can_omit_end(&self.end) {
+          dest.delim('/', true)?;
+          self.end.to_css(dest)?;
+        }
+        Ok(())
+      }
+    }
+  };
+}
+
+define_shorthand! {
+  /// A value for the [grid-row](https://drafts.csswg.org/css-grid-2/#propdef-grid-row) shorthand property.
+  pub struct GridRow<'i> {
+    /// The starting line.
+    #[cfg_attr(feature = "serde", serde(borrow))]
+    start: GridRowStart(GridLine<'i>),
+    /// The ending line.
+    end: GridRowEnd(GridLine<'i>),
+  }
+}
+
+define_shorthand! {
+  /// A value for the [grid-row](https://drafts.csswg.org/css-grid-2/#propdef-grid-column) shorthand property.
+  pub struct GridColumn<'i> {
+    /// The starting line.
+    #[cfg_attr(feature = "serde", serde(borrow))]
+    start: GridColumnStart(GridLine<'i>),
+    /// The ending line.
+    end: GridColumnEnd(GridLine<'i>),
+  }
+}
+
+impl_grid_placement!(GridRow);
+impl_grid_placement!(GridColumn);
+
+define_shorthand! {
+  /// A value for the [grid-area](https://drafts.csswg.org/css-grid-2/#propdef-grid-area) shorthand property.
+  pub struct GridArea<'i> {
+    /// The grid row start placement.
+    #[cfg_attr(feature = "serde", serde(borrow))]
+    row_start: GridRowStart(GridLine<'i>),
+    /// The grid column start placement.
+    column_start: GridColumnStart(GridLine<'i>),
+    /// The grid row end placement.
+    row_end: GridRowEnd(GridLine<'i>),
+    /// The grid column end placement.
+    column_end: GridColumnEnd(GridLine<'i>),
+  }
+}
+
+impl<'i> Parse<'i> for GridArea<'i> {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let row_start = GridLine::parse(input)?;
+    let column_start = if input.try_parse(|input| input.expect_delim('/')).is_ok() {
+      GridLine::parse(input)?
+    } else {
+      let opposite = row_start.default_end_value();
+      return Ok(GridArea {
+        row_start,
+        column_start: opposite.clone(),
+        row_end: opposite.clone(),
+        column_end: opposite,
+      });
+    };
+
+    let row_end = if input.try_parse(|input| input.expect_delim('/')).is_ok() {
+      GridLine::parse(input)?
+    } else {
+      let row_end = row_start.default_end_value();
+      let column_end = column_start.default_end_value();
+      return Ok(GridArea {
+        row_start,
+        column_start,
+        row_end,
+        column_end,
+      });
+    };
+
+    let column_end = if input.try_parse(|input| input.expect_delim('/')).is_ok() {
+      GridLine::parse(input)?
+    } else {
+      let column_end = column_start.default_end_value();
+      return Ok(GridArea {
+        row_start,
+        column_start,
+        row_end,
+        column_end,
+      });
+    };
+
+    Ok(GridArea {
+      row_start,
+      column_start,
+      row_end,
+      column_end,
+    })
+  }
+}
+
+impl ToCss for GridArea<'_> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    self.row_start.to_css(dest)?;
+
+    let can_omit_column_end = self.column_start.can_omit_end(&self.column_end);
+    let can_omit_row_end = can_omit_column_end && self.row_start.can_omit_end(&self.row_end);
+    let can_omit_column_start = can_omit_row_end && self.row_start.can_omit_end(&self.column_start);
+
+    if !can_omit_column_start {
+      dest.delim('/', true)?;
+      self.column_start.to_css(dest)?;
+    }
+
+    if !can_omit_row_end {
+      dest.delim('/', true)?;
+      self.row_end.to_css(dest)?;
+    }
+
+    if !can_omit_column_end {
+      dest.delim('/', true)?;
+      self.column_end.to_css(dest)?;
+    }
+
+    Ok(())
+  }
+}
+
+#[derive(Default, Debug)]
+pub(crate) struct GridHandler<'i> {
+  rows: Option<TrackSizing<'i>>,
+  columns: Option<TrackSizing<'i>>,
+  areas: Option<GridTemplateAreas>,
+  auto_rows: Option<TrackSizeList>,
+  auto_columns: Option<TrackSizeList>,
+  auto_flow: Option<GridAutoFlow>,
+  row_start: Option<GridLine<'i>>,
+  column_start: Option<GridLine<'i>>,
+  row_end: Option<GridLine<'i>>,
+  column_end: Option<GridLine<'i>>,
+  has_any: bool,
+}
+
+impl<'i> PropertyHandler<'i> for GridHandler<'i> {
+  fn handle_property(
+    &mut self,
+    property: &Property<'i>,
+    dest: &mut DeclarationList<'i>,
+    context: &mut PropertyHandlerContext<'i, '_>,
+  ) -> bool {
+    use Property::*;
+
+    match property {
+      GridTemplateColumns(columns) => self.columns = Some(columns.clone()),
+      GridTemplateRows(rows) => self.rows = Some(rows.clone()),
+      GridTemplateAreas(areas) => self.areas = Some(areas.clone()),
+      GridAutoColumns(auto_columns) => self.auto_columns = Some(auto_columns.clone()),
+      GridAutoRows(auto_rows) => self.auto_rows = Some(auto_rows.clone()),
+      GridAutoFlow(auto_flow) => self.auto_flow = Some(auto_flow.clone()),
+      GridTemplate(template) => {
+        self.rows = Some(template.rows.clone());
+        self.columns = Some(template.columns.clone());
+        self.areas = Some(template.areas.clone());
+      }
+      Grid(grid) => {
+        self.rows = Some(grid.rows.clone());
+        self.columns = Some(grid.columns.clone());
+        self.areas = Some(grid.areas.clone());
+        self.auto_rows = Some(grid.auto_rows.clone());
+        self.auto_columns = Some(grid.auto_columns.clone());
+        self.auto_flow = Some(grid.auto_flow.clone());
+      }
+      GridRowStart(row_start) => self.row_start = Some(row_start.clone()),
+      GridRowEnd(row_end) => self.row_end = Some(row_end.clone()),
+      GridColumnStart(column_start) => self.column_start = Some(column_start.clone()),
+      GridColumnEnd(column_end) => self.column_end = Some(column_end.clone()),
+      GridRow(row) => {
+        self.row_start = Some(row.start.clone());
+        self.row_end = Some(row.end.clone());
+      }
+      GridColumn(column) => {
+        self.column_start = Some(column.start.clone());
+        self.column_end = Some(column.end.clone());
+      }
+      GridArea(area) => {
+        self.row_start = Some(area.row_start.clone());
+        self.row_end = Some(area.row_end.clone());
+        self.column_start = Some(area.column_start.clone());
+        self.column_end = Some(area.column_end.clone());
+      }
+      Unparsed(val) if is_grid_property(&val.property_id) => {
+        self.finalize(dest, context);
+        dest.push(property.clone());
+      }
+      _ => return false,
+    }
+
+    self.has_any = true;
+    true
+  }
+
+  fn finalize(&mut self, dest: &mut DeclarationList<'i>, _: &mut PropertyHandlerContext<'i, '_>) {
+    if !self.has_any {
+      return;
+    }
+
+    self.has_any = false;
+
+    let mut rows = std::mem::take(&mut self.rows);
+    let mut columns = std::mem::take(&mut self.columns);
+    let mut areas = std::mem::take(&mut self.areas);
+    let mut auto_rows = std::mem::take(&mut self.auto_rows);
+    let mut auto_columns = std::mem::take(&mut self.auto_columns);
+    let mut auto_flow = std::mem::take(&mut self.auto_flow);
+    let mut row_start = std::mem::take(&mut self.row_start);
+    let mut row_end = std::mem::take(&mut self.row_end);
+    let mut column_start = std::mem::take(&mut self.column_start);
+    let mut column_end = std::mem::take(&mut self.column_end);
+
+    if let (Some(rows_val), Some(columns_val), Some(areas_val)) = (&rows, &columns, &areas) {
+      let mut has_template = true;
+      if let (Some(auto_rows_val), Some(auto_columns_val), Some(auto_flow_val)) =
+        (&auto_rows, &auto_columns, &auto_flow)
+      {
+        // The `grid` shorthand can either be fully explicit (e.g. same as `grid-template`),
+        // or explicit along a single axis. If there are auto rows, then there cannot be explicit rows, for example.
+        if Grid::is_valid(
+          rows_val,
+          columns_val,
+          areas_val,
+          auto_rows_val,
+          auto_columns_val,
+          auto_flow_val,
+        ) {
+          dest.push(Property::Grid(Grid {
+            rows: rows_val.clone(),
+            columns: columns_val.clone(),
+            areas: areas_val.clone(),
+            auto_rows: auto_rows_val.clone(),
+            auto_columns: auto_columns_val.clone(),
+            auto_flow: auto_flow_val.clone(),
+          }));
+
+          has_template = false;
+          auto_rows = None;
+          auto_columns = None;
+          auto_flow = None;
+        }
+      }
+
+      // The `grid-template` shorthand supports only explicit track values (i.e. no `repeat()`)
+      // combined with grid-template-areas. If there are no areas, then any track values are allowed.
+      if has_template && GridTemplate::is_valid(rows_val, columns_val, areas_val) {
+        dest.push(Property::GridTemplate(GridTemplate {
+          rows: rows_val.clone(),
+          columns: columns_val.clone(),
+          areas: areas_val.clone(),
+        }));
+
+        has_template = false;
+      }
+
+      if !has_template {
+        rows = None;
+        columns = None;
+        areas = None;
+      }
+    }
+
+    if row_start.is_some() && row_end.is_some() && column_start.is_some() && column_end.is_some() {
+      dest.push(Property::GridArea(GridArea {
+        row_start: std::mem::take(&mut row_start).unwrap(),
+        row_end: std::mem::take(&mut row_end).unwrap(),
+        column_start: std::mem::take(&mut column_start).unwrap(),
+        column_end: std::mem::take(&mut column_end).unwrap(),
+      }))
+    } else {
+      if row_start.is_some() && row_end.is_some() {
+        dest.push(Property::GridRow(GridRow {
+          start: std::mem::take(&mut row_start).unwrap(),
+          end: std::mem::take(&mut row_end).unwrap(),
+        }))
+      }
+
+      if column_start.is_some() && column_end.is_some() {
+        dest.push(Property::GridColumn(GridColumn {
+          start: std::mem::take(&mut column_start).unwrap(),
+          end: std::mem::take(&mut column_end).unwrap(),
+        }))
+      }
+    }
+
+    macro_rules! single_property {
+      ($prop: ident, $key: ident) => {
+        if let Some(val) = $key {
+          dest.push(Property::$prop(val))
+        }
+      };
+    }
+
+    single_property!(GridTemplateRows, rows);
+    single_property!(GridTemplateColumns, columns);
+    single_property!(GridTemplateAreas, areas);
+    single_property!(GridAutoRows, auto_rows);
+    single_property!(GridAutoColumns, auto_columns);
+    single_property!(GridAutoFlow, auto_flow);
+    single_property!(GridRowStart, row_start);
+    single_property!(GridRowEnd, row_end);
+    single_property!(GridColumnStart, column_start);
+    single_property!(GridColumnEnd, column_end);
+  }
+}
+
+#[inline]
+fn is_grid_property(property_id: &PropertyId) -> bool {
+  match property_id {
+    PropertyId::GridTemplateColumns
+    | PropertyId::GridTemplateRows
+    | PropertyId::GridTemplateAreas
+    | PropertyId::GridAutoColumns
+    | PropertyId::GridAutoRows
+    | PropertyId::GridAutoFlow
+    | PropertyId::GridTemplate
+    | PropertyId::Grid
+    | PropertyId::GridRowStart
+    | PropertyId::GridRowEnd
+    | PropertyId::GridColumnStart
+    | PropertyId::GridColumnEnd
+    | PropertyId::GridRow
+    | PropertyId::GridColumn
+    | PropertyId::GridArea => true,
+    _ => false,
+  }
+}
diff --git a/src/properties/list.rs b/src/properties/list.rs
new file mode 100644
index 0000000..e411667
--- /dev/null
+++ b/src/properties/list.rs
@@ -0,0 +1,464 @@
+//! CSS properties related to lists and counters.
+
+use super::{Property, PropertyId};
+use crate::context::PropertyHandlerContext;
+use crate::declaration::{DeclarationBlock, DeclarationList};
+use crate::error::{ParserError, PrinterError};
+use crate::macros::{define_shorthand, enum_property, shorthand_handler};
+use crate::printer::Printer;
+use crate::targets::{Browsers, Targets};
+use crate::traits::{FallbackValues, IsCompatible, Parse, PropertyHandler, Shorthand, ToCss};
+use crate::values::string::CSSString;
+use crate::values::{ident::CustomIdent, image::Image};
+#[cfg(feature = "visitor")]
+use crate::visitor::Visit;
+use cssparser::*;
+
+/// A value for the [list-style-type](https://www.w3.org/TR/2020/WD-css-lists-3-20201117/#text-markers) property.
+#[derive(Debug, Clone, PartialEq, Parse, ToCss)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", content = "value", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub enum ListStyleType<'i> {
+  /// No marker.
+  None,
+  /// An explicit marker string.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  String(CSSString<'i>),
+  /// A named counter style.
+  CounterStyle(CounterStyle<'i>),
+}
+
+impl Default for ListStyleType<'_> {
+  fn default() -> Self {
+    ListStyleType::CounterStyle(CounterStyle::Predefined(PredefinedCounterStyle::Disc))
+  }
+}
+
+impl IsCompatible for ListStyleType<'_> {
+  fn is_compatible(&self, browsers: Browsers) -> bool {
+    match self {
+      ListStyleType::CounterStyle(c) => c.is_compatible(browsers),
+      ListStyleType::String(..) => crate::compat::Feature::StringListStyleType.is_compatible(browsers),
+      ListStyleType::None => true,
+    }
+  }
+}
+
+/// A [counter-style](https://www.w3.org/TR/css-counter-styles-3/#typedef-counter-style) name.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub enum CounterStyle<'i> {
+  /// A predefined counter style name.
+  #[cfg_attr(
+    feature = "serde",
+    serde(with = "crate::serialization::ValueWrapper::<PredefinedCounterStyle>")
+  )]
+  Predefined(PredefinedCounterStyle),
+  /// A custom counter style name.
+  #[cfg_attr(
+    feature = "serde",
+    serde(borrow, with = "crate::serialization::ValueWrapper::<CustomIdent>")
+  )]
+  Name(CustomIdent<'i>),
+  /// An inline [`symbols()`](https://www.w3.org/TR/css-counter-styles-3/#symbols-function) definition.
+  Symbols {
+    /// The counter system.
+    #[cfg_attr(feature = "serde", serde(default))]
+    system: SymbolsType,
+    /// The symbols.
+    symbols: Vec<Symbol<'i>>,
+  },
+}
+
+macro_rules! counter_styles {
+  (
+    $(#[$outer:meta])*
+    $vis:vis enum $name:ident {
+      $(
+        $(#[$meta: meta])*
+        $id: ident,
+      )+
+    }
+  ) => {
+    enum_property! {
+      /// A [predefined counter](https://www.w3.org/TR/css-counter-styles-3/#predefined-counters) style.
+      #[allow(missing_docs)]
+      pub enum PredefinedCounterStyle {
+        $(
+           $(#[$meta])*
+           $id,
+        )+
+      }
+    }
+
+    impl IsCompatible for PredefinedCounterStyle {
+      fn is_compatible(&self, browsers: Browsers) -> bool {
+        match self {
+          $(
+            PredefinedCounterStyle::$id => paste::paste! {
+              crate::compat::Feature::[<$id ListStyleType>].is_compatible(browsers)
+            },
+          )+
+        }
+      }
+    }
+  };
+}
+
+counter_styles! {
+  /// A [predefined counter](https://www.w3.org/TR/css-counter-styles-3/#predefined-counters) style.
+  #[allow(missing_docs)]
+  pub enum PredefinedCounterStyle {
+    // https://www.w3.org/TR/css-counter-styles-3/#simple-numeric
+    Decimal,
+    DecimalLeadingZero,
+    ArabicIndic,
+    Armenian,
+    UpperArmenian,
+    LowerArmenian,
+    Bengali,
+    Cambodian,
+    Khmer,
+    CjkDecimal,
+    Devanagari,
+    Georgian,
+    Gujarati,
+    Gurmukhi,
+    Hebrew,
+    Kannada,
+    Lao,
+    Malayalam,
+    Mongolian,
+    Myanmar,
+    Oriya,
+    Persian,
+    LowerRoman,
+    UpperRoman,
+    Tamil,
+    Telugu,
+    Thai,
+    Tibetan,
+
+    // https://www.w3.org/TR/css-counter-styles-3/#simple-alphabetic
+    LowerAlpha,
+    LowerLatin,
+    UpperAlpha,
+    UpperLatin,
+    LowerGreek,
+    Hiragana,
+    HiraganaIroha,
+    Katakana,
+    KatakanaIroha,
+
+    // https://www.w3.org/TR/css-counter-styles-3/#simple-symbolic
+    Disc,
+    Circle,
+    Square,
+    DisclosureOpen,
+    DisclosureClosed,
+
+    // https://www.w3.org/TR/css-counter-styles-3/#simple-fixed
+    CjkEarthlyBranch,
+    CjkHeavenlyStem,
+
+    // https://www.w3.org/TR/css-counter-styles-3/#complex-cjk
+    JapaneseInformal,
+    JapaneseFormal,
+    KoreanHangulFormal,
+    KoreanHanjaInformal,
+    KoreanHanjaFormal,
+    SimpChineseInformal,
+    SimpChineseFormal,
+    TradChineseInformal,
+    TradChineseFormal,
+    EthiopicNumeric,
+  }
+}
+
+impl<'i> Parse<'i> for CounterStyle<'i> {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    if let Ok(predefined) = input.try_parse(PredefinedCounterStyle::parse) {
+      return Ok(CounterStyle::Predefined(predefined));
+    }
+
+    if input.try_parse(|input| input.expect_function_matching("symbols")).is_ok() {
+      return input.parse_nested_block(|input| {
+        let t = input.try_parse(SymbolsType::parse).unwrap_or_default();
+
+        let mut symbols = Vec::new();
+        while let Ok(s) = input.try_parse(Symbol::parse) {
+          symbols.push(s);
+        }
+
+        Ok(CounterStyle::Symbols { system: t, symbols })
+      });
+    }
+
+    let name = CustomIdent::parse(input)?;
+    Ok(CounterStyle::Name(name))
+  }
+}
+
+impl ToCss for CounterStyle<'_> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    match self {
+      CounterStyle::Predefined(style) => style.to_css(dest),
+      CounterStyle::Name(name) => {
+        if let Some(css_module) = &mut dest.css_module {
+          css_module.reference(&name.0, dest.loc.source_index)
+        }
+        name.to_css(dest)
+      }
+      CounterStyle::Symbols { system: t, symbols } => {
+        dest.write_str("symbols(")?;
+        let mut needs_space = false;
+        if *t != SymbolsType::Symbolic {
+          t.to_css(dest)?;
+          needs_space = true;
+        }
+
+        for symbol in symbols {
+          if needs_space {
+            dest.write_char(' ')?;
+          }
+          symbol.to_css(dest)?;
+          needs_space = true;
+        }
+        dest.write_char(')')
+      }
+    }
+  }
+}
+
+impl IsCompatible for CounterStyle<'_> {
+  fn is_compatible(&self, browsers: Browsers) -> bool {
+    match self {
+      CounterStyle::Name(..) => true,
+      CounterStyle::Predefined(p) => p.is_compatible(browsers),
+      CounterStyle::Symbols { .. } => crate::compat::Feature::SymbolsListStyleType.is_compatible(browsers),
+    }
+  }
+}
+
+enum_property! {
+  /// A [`<symbols-type>`](https://www.w3.org/TR/css-counter-styles-3/#typedef-symbols-type) value,
+  /// as used in the `symbols()` function.
+  ///
+  /// See [CounterStyle](CounterStyle).
+  #[allow(missing_docs)]
+  pub enum SymbolsType {
+    Cyclic,
+    Numeric,
+    Alphabetic,
+    Symbolic,
+    Fixed,
+  }
+}
+
+impl Default for SymbolsType {
+  fn default() -> Self {
+    SymbolsType::Symbolic
+  }
+}
+
+/// A single [symbol](https://www.w3.org/TR/css-counter-styles-3/#funcdef-symbols) as used in the
+/// `symbols()` function.
+///
+/// See [CounterStyle](CounterStyle).
+#[derive(Debug, Clone, PartialEq, Parse, ToCss)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", content = "value", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub enum Symbol<'i> {
+  /// A string.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  String(CSSString<'i>),
+  /// An image.
+  Image(Image<'i>),
+}
+
+enum_property! {
+  /// A value for the [list-style-position](https://www.w3.org/TR/2020/WD-css-lists-3-20201117/#list-style-position-property) property.
+  pub enum ListStylePosition {
+    /// The list marker is placed inside the element.
+    Inside,
+    /// The list marker is placed outside the element.
+    Outside,
+  }
+}
+
+impl Default for ListStylePosition {
+  fn default() -> ListStylePosition {
+    ListStylePosition::Outside
+  }
+}
+
+impl IsCompatible for ListStylePosition {
+  fn is_compatible(&self, _browsers: Browsers) -> bool {
+    true
+  }
+}
+
+enum_property! {
+  /// A value for the [marker-side](https://www.w3.org/TR/2020/WD-css-lists-3-20201117/#marker-side) property.
+  #[allow(missing_docs)]
+  pub enum MarkerSide {
+    MatchSelf,
+    MatchParent,
+  }
+}
+
+define_shorthand! {
+  /// A value for the [list-style](https://www.w3.org/TR/2020/WD-css-lists-3-20201117/#list-style-property) shorthand property.
+  pub struct ListStyle<'i> {
+    /// The position of the list marker.
+    position: ListStylePosition(ListStylePosition),
+    /// The list marker image.
+    #[cfg_attr(feature = "serde", serde(borrow))]
+    image: ListStyleImage(Image<'i>),
+    /// The list style type.
+    list_style_type: ListStyleType(ListStyleType<'i>),
+  }
+}
+
+impl<'i> Parse<'i> for ListStyle<'i> {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let mut position = None;
+    let mut image = None;
+    let mut list_style_type = None;
+    let mut nones = 0;
+
+    loop {
+      // `none` is ambiguous - both list-style-image and list-style-type support it.
+      if input.try_parse(|input| input.expect_ident_matching("none")).is_ok() {
+        nones += 1;
+        if nones > 2 {
+          return Err(input.new_custom_error(ParserError::InvalidValue));
+        }
+        continue;
+      }
+
+      if image.is_none() {
+        if let Ok(val) = input.try_parse(Image::parse) {
+          image = Some(val);
+          continue;
+        }
+      }
+
+      if position.is_none() {
+        if let Ok(val) = input.try_parse(ListStylePosition::parse) {
+          position = Some(val);
+          continue;
+        }
+      }
+
+      if list_style_type.is_none() {
+        if let Ok(val) = input.try_parse(ListStyleType::parse) {
+          list_style_type = Some(val);
+          continue;
+        }
+      }
+
+      break;
+    }
+
+    // Assign the `none` to the opposite property from the one we have a value for,
+    // or both in case neither list-style-image or list-style-type have a value.
+    match (nones, image, list_style_type) {
+      (2, None, None) | (1, None, None) => Ok(ListStyle {
+        position: position.unwrap_or_default(),
+        image: Image::None,
+        list_style_type: ListStyleType::None,
+      }),
+      (1, Some(image), None) => Ok(ListStyle {
+        position: position.unwrap_or_default(),
+        image,
+        list_style_type: ListStyleType::None,
+      }),
+      (1, None, Some(list_style_type)) => Ok(ListStyle {
+        position: position.unwrap_or_default(),
+        image: Image::None,
+        list_style_type,
+      }),
+      (0, image, list_style_type) => Ok(ListStyle {
+        position: position.unwrap_or_default(),
+        image: image.unwrap_or_default(),
+        list_style_type: list_style_type.unwrap_or_default(),
+      }),
+      _ => Err(input.new_custom_error(ParserError::InvalidValue)),
+    }
+  }
+}
+
+impl<'i> ToCss for ListStyle<'i> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    let mut needs_space = false;
+    if self.position != ListStylePosition::default() {
+      self.position.to_css(dest)?;
+      needs_space = true;
+    }
+
+    if self.image != Image::default() {
+      if needs_space {
+        dest.write_char(' ')?;
+      }
+      self.image.to_css(dest)?;
+      needs_space = true;
+    }
+
+    if self.list_style_type != ListStyleType::default() {
+      if needs_space {
+        dest.write_char(' ')?;
+      }
+      self.list_style_type.to_css(dest)?;
+      needs_space = true;
+    }
+
+    if !needs_space {
+      self.position.to_css(dest)?;
+    }
+
+    Ok(())
+  }
+}
+
+impl<'i> FallbackValues for ListStyle<'i> {
+  fn get_fallbacks(&mut self, targets: Targets) -> Vec<Self> {
+    self
+      .image
+      .get_fallbacks(targets)
+      .into_iter()
+      .map(|image| ListStyle { image, ..self.clone() })
+      .collect()
+  }
+}
+
+shorthand_handler!(ListStyleHandler -> ListStyle<'i> fallbacks: true {
+  image: ListStyleImage(Image<'i>, fallback: true, image: true),
+  list_style_type: ListStyleType(ListStyleType<'i>),
+  position: ListStylePosition(ListStylePosition),
+});
diff --git a/src/properties/margin_padding.rs b/src/properties/margin_padding.rs
new file mode 100644
index 0000000..0728601
--- /dev/null
+++ b/src/properties/margin_padding.rs
@@ -0,0 +1,496 @@
+use crate::compat::Feature;
+use crate::context::PropertyHandlerContext;
+use crate::declaration::{DeclarationBlock, DeclarationList};
+use crate::error::{ParserError, PrinterError};
+use crate::logical::PropertyCategory;
+use crate::macros::{define_shorthand, rect_shorthand, size_shorthand};
+use crate::printer::Printer;
+use crate::properties::{Property, PropertyId};
+use crate::traits::{IsCompatible, Parse, PropertyHandler, Shorthand, ToCss};
+use crate::values::{length::LengthPercentageOrAuto, rect::Rect, size::Size2D};
+#[cfg(feature = "visitor")]
+use crate::visitor::Visit;
+use cssparser::*;
+
+rect_shorthand! {
+  /// A value for the [margin](https://drafts.csswg.org/css-box-4/#propdef-margin) shorthand property.
+  pub struct Margin<LengthPercentageOrAuto> {
+    MarginTop,
+    MarginRight,
+    MarginBottom,
+    MarginLeft
+  }
+}
+
+rect_shorthand! {
+  /// A value for the [padding](https://drafts.csswg.org/css-box-4/#propdef-padding) shorthand property.
+  pub struct Padding<LengthPercentageOrAuto> {
+    PaddingTop,
+    PaddingRight,
+    PaddingBottom,
+    PaddingLeft
+  }
+}
+
+rect_shorthand! {
+  /// A value for the [scroll-margin](https://drafts.csswg.org/css-scroll-snap/#scroll-margin) shorthand property.
+  pub struct ScrollMargin<LengthPercentageOrAuto> {
+    ScrollMarginTop,
+    ScrollMarginRight,
+    ScrollMarginBottom,
+    ScrollMarginLeft
+  }
+}
+
+rect_shorthand! {
+  /// A value for the [scroll-padding](https://drafts.csswg.org/css-scroll-snap/#scroll-padding) shorthand property.
+  pub struct ScrollPadding<LengthPercentageOrAuto> {
+    ScrollPaddingTop,
+    ScrollPaddingRight,
+    ScrollPaddingBottom,
+    ScrollPaddingLeft
+  }
+}
+
+rect_shorthand! {
+  /// A value for the [inset](https://drafts.csswg.org/css-logical/#propdef-inset) shorthand property.
+  pub struct Inset<LengthPercentageOrAuto> {
+    Top,
+    Right,
+    Bottom,
+    Left
+  }
+}
+
+size_shorthand! {
+  /// A value for the [margin-block](https://drafts.csswg.org/css-logical/#propdef-margin-block) shorthand property.
+  pub struct MarginBlock<LengthPercentageOrAuto> {
+    /// The block start value.
+    block_start: MarginBlockStart,
+    /// The block end value.
+    block_end: MarginBlockEnd,
+  }
+}
+
+size_shorthand! {
+  /// A value for the [margin-inline](https://drafts.csswg.org/css-logical/#propdef-margin-inline) shorthand property.
+  pub struct MarginInline<LengthPercentageOrAuto> {
+    /// The inline start value.
+    inline_start: MarginInlineStart,
+    /// The inline end value.
+    inline_end: MarginInlineEnd,
+  }
+}
+
+size_shorthand! {
+  /// A value for the [padding-block](https://drafts.csswg.org/css-logical/#propdef-padding-block) shorthand property.
+  pub struct PaddingBlock<LengthPercentageOrAuto> {
+     /// The block start value.
+    block_start: PaddingBlockStart,
+    /// The block end value.
+    block_end: PaddingBlockEnd,
+  }
+}
+
+size_shorthand! {
+  /// A value for the [padding-inline](https://drafts.csswg.org/css-logical/#propdef-padding-inline) shorthand property.
+  pub struct PaddingInline<LengthPercentageOrAuto> {
+    /// The inline start value.
+    inline_start: PaddingInlineStart,
+    /// The inline end value.
+    inline_end: PaddingInlineEnd,
+  }
+}
+
+size_shorthand! {
+  /// A value for the [scroll-margin-block](https://drafts.csswg.org/css-scroll-snap/#propdef-scroll-margin-block) shorthand property.
+  pub struct ScrollMarginBlock<LengthPercentageOrAuto> {
+     /// The block start value.
+    block_start: ScrollMarginBlockStart,
+    /// The block end value.
+    block_end: ScrollMarginBlockEnd,
+  }
+}
+
+size_shorthand! {
+  /// A value for the [scroll-margin-inline](https://drafts.csswg.org/css-scroll-snap/#propdef-scroll-margin-inline) shorthand property.
+  pub struct ScrollMarginInline<LengthPercentageOrAuto> {
+    /// The inline start value.
+    inline_start: ScrollMarginInlineStart,
+    /// The inline end value.
+    inline_end: ScrollMarginInlineEnd,
+  }
+}
+
+size_shorthand! {
+  /// A value for the [scroll-padding-block](https://drafts.csswg.org/css-scroll-snap/#propdef-scroll-padding-block) shorthand property.
+  pub struct ScrollPaddingBlock<LengthPercentageOrAuto> {
+     /// The block start value.
+    block_start: ScrollPaddingBlockStart,
+    /// The block end value.
+    block_end: ScrollPaddingBlockEnd,
+  }
+}
+
+size_shorthand! {
+  /// A value for the [scroll-padding-inline](https://drafts.csswg.org/css-scroll-snap/#propdef-scroll-padding-inline) shorthand property.
+  pub struct ScrollPaddingInline<LengthPercentageOrAuto> {
+    /// The inline start value.
+    inline_start: ScrollPaddingInlineStart,
+    /// The inline end value.
+    inline_end: ScrollPaddingInlineEnd,
+  }
+}
+
+size_shorthand! {
+  /// A value for the [inset-block](https://drafts.csswg.org/css-logical/#propdef-inset-block) shorthand property.
+  pub struct InsetBlock<LengthPercentageOrAuto> {
+     /// The block start value.
+    block_start: InsetBlockStart,
+    /// The block end value.
+    block_end: InsetBlockEnd,
+  }
+}
+
+size_shorthand! {
+  /// A value for the [inset-inline](https://drafts.csswg.org/css-logical/#propdef-inset-inline) shorthand property.
+  pub struct InsetInline<LengthPercentageOrAuto> {
+    /// The inline start value.
+    inline_start: InsetInlineStart,
+    /// The inline end value.
+    inline_end: InsetInlineEnd,
+  }
+}
+
+macro_rules! side_handler {
+  ($name: ident, $top: ident, $bottom: ident, $left: ident, $right: ident, $block_start: ident, $block_end: ident, $inline_start: ident, $inline_end: ident, $shorthand: ident, $block_shorthand: ident, $inline_shorthand: ident, $shorthand_category: ident $(, $feature: ident, $shorthand_feature: ident)?) => {
+    #[derive(Debug, Default)]
+    pub(crate) struct $name<'i> {
+      top: Option<LengthPercentageOrAuto>,
+      bottom: Option<LengthPercentageOrAuto>,
+      left: Option<LengthPercentageOrAuto>,
+      right: Option<LengthPercentageOrAuto>,
+      block_start: Option<Property<'i>>,
+      block_end: Option<Property<'i>>,
+      inline_start: Option<Property<'i>>,
+      inline_end: Option<Property<'i>>,
+      has_any: bool,
+      category: PropertyCategory
+    }
+
+    impl<'i> PropertyHandler<'i> for $name<'i> {
+      fn handle_property(&mut self, property: &Property<'i>, dest: &mut DeclarationList<'i>, context: &mut PropertyHandlerContext<'i, '_>) -> bool {
+        use Property::*;
+
+        macro_rules! flush {
+          ($key: ident, $val: expr, $category: ident) => {{
+            // If the category changes betweet logical and physical,
+            // or if the value contains syntax that isn't supported across all targets,
+            // preserve the previous value as a fallback.
+            if PropertyCategory::$category != self.category || (self.$key.is_some() && matches!(context.targets.browsers, Some(targets) if !$val.is_compatible(targets))) {
+              self.flush(dest, context);
+            }
+          }}
+        }
+
+        macro_rules! property {
+          ($key: ident, $val: ident, $category: ident) => {{
+            flush!($key, $val, $category);
+            self.$key = Some($val.clone());
+            self.category = PropertyCategory::$category;
+            self.has_any = true;
+          }};
+        }
+
+        macro_rules! logical_property {
+          ($prop: ident, $val: expr) => {{
+            // Assume unparsed properties might contain unsupported syntax that we must preserve as a fallback.
+            if self.category != PropertyCategory::Logical || (self.$prop.is_some() && matches!($val, Property::Unparsed(_))) {
+              self.flush(dest, context);
+            }
+
+            self.$prop = Some($val);
+            self.category = PropertyCategory::Logical;
+            self.has_any = true;
+          }};
+        }
+
+        match &property {
+          $top(val) => property!(top, val, Physical),
+          $bottom(val) => property!(bottom, val, Physical),
+          $left(val) => property!(left, val, Physical),
+          $right(val) => property!(right, val, Physical),
+          $block_start(val) => {
+            flush!(block_start, val, Logical);
+            logical_property!(block_start, property.clone());
+          },
+          $block_end(val) => {
+            flush!(block_end, val, Logical);
+            logical_property!(block_end, property.clone());
+          },
+          $inline_start(val) => {
+            flush!(inline_start, val, Logical);
+            logical_property!(inline_start, property.clone())
+          },
+          $inline_end(val) => {
+            flush!(inline_end, val, Logical);
+            logical_property!(inline_end, property.clone());
+          },
+          $block_shorthand(val) => {
+            flush!(block_start, val.block_start, Logical);
+            flush!(block_end, val.block_end, Logical);
+            logical_property!(block_start, Property::$block_start(val.block_start.clone()));
+            logical_property!(block_end, Property::$block_end(val.block_end.clone()));
+          },
+          $inline_shorthand(val) => {
+            flush!(inline_start, val.inline_start, Logical);
+            flush!(inline_end, val.inline_end, Logical);
+            logical_property!(inline_start, Property::$inline_start(val.inline_start.clone()));
+            logical_property!(inline_end, Property::$inline_end(val.inline_end.clone()));
+          },
+          $shorthand(val) => {
+            flush!(top, val.top, $shorthand_category);
+            flush!(right, val.right, $shorthand_category);
+            flush!(bottom, val.bottom, $shorthand_category);
+            flush!(left, val.left, $shorthand_category);
+            self.top = Some(val.top.clone());
+            self.right = Some(val.right.clone());
+            self.bottom = Some(val.bottom.clone());
+            self.left = Some(val.left.clone());
+            self.block_start = None;
+            self.block_end = None;
+            self.inline_start = None;
+            self.inline_end = None;
+            self.has_any = true;
+          }
+          Unparsed(val) if matches!(val.property_id, PropertyId::$top | PropertyId::$bottom | PropertyId::$left | PropertyId::$right | PropertyId::$block_start | PropertyId::$block_end | PropertyId::$inline_start | PropertyId::$inline_end | PropertyId::$block_shorthand | PropertyId::$inline_shorthand | PropertyId::$shorthand) => {
+            // Even if we weren't able to parse the value (e.g. due to var() references),
+            // we can still add vendor prefixes to the property itself.
+            match &val.property_id {
+              PropertyId::$block_start => logical_property!(block_start, property.clone()),
+              PropertyId::$block_end => logical_property!(block_end, property.clone()),
+              PropertyId::$inline_start => logical_property!(inline_start, property.clone()),
+              PropertyId::$inline_end => logical_property!(inline_end, property.clone()),
+              _ => {
+                self.flush(dest, context);
+                dest.push(property.clone());
+              }
+            }
+          }
+          _ => return false
+        }
+
+        true
+      }
+
+      fn finalize(&mut self, dest: &mut DeclarationList<'i>, context: &mut PropertyHandlerContext<'i, '_>) {
+        self.flush(dest, context);
+      }
+    }
+
+    impl<'i> $name<'i> {
+      fn flush(&mut self, dest: &mut DeclarationList<'i>, context: &mut PropertyHandlerContext<'i, '_>) {
+        if !self.has_any {
+          return
+        }
+
+        self.has_any = false;
+
+        let top = std::mem::take(&mut self.top);
+        let bottom = std::mem::take(&mut self.bottom);
+        let left = std::mem::take(&mut self.left);
+        let right = std::mem::take(&mut self.right);
+        let logical_supported = true $(&& !context.should_compile_logical(Feature::$feature))?;
+
+        if (PropertyCategory::$shorthand_category != PropertyCategory::Logical || logical_supported) && top.is_some() && bottom.is_some() && left.is_some() && right.is_some() {
+          dest.push(Property::$shorthand($shorthand {
+            top: top.unwrap(),
+            right: right.unwrap(),
+            bottom: bottom.unwrap(),
+            left: left.unwrap()
+          }));
+        } else {
+          if let Some(val) = top {
+            dest.push(Property::$top(val));
+          }
+
+          if let Some(val) = bottom {
+            dest.push(Property::$bottom(val));
+          }
+
+          if let Some(val) = left {
+            dest.push(Property::$left(val));
+          }
+
+          if let Some(val) = right {
+            dest.push(Property::$right(val));
+          }
+        }
+
+        let block_start = std::mem::take(&mut self.block_start);
+        let block_end = std::mem::take(&mut self.block_end);
+        let inline_start = std::mem::take(&mut self.inline_start);
+        let inline_end = std::mem::take(&mut self.inline_end);
+
+        macro_rules! logical_side {
+          ($start: ident, $end: ident, $shorthand_prop: ident, $start_prop: ident, $end_prop: ident) => {
+            let shorthand_supported = logical_supported $(&& !context.should_compile_logical(Feature::$shorthand_feature))?;
+            if let (Some(Property::$start_prop(start)), Some(Property::$end_prop(end)), true) = (&$start, &$end, shorthand_supported) {
+              dest.push(Property::$shorthand_prop($shorthand_prop {
+                $start: start.clone(),
+                $end: end.clone()
+              }));
+            } else {
+              if let Some(val) = $start {
+                dest.push(val);
+              }
+
+              if let Some(val) = $end {
+                dest.push(val);
+              }
+            }
+          };
+        }
+
+        macro_rules! prop {
+          ($val: ident, $logical: ident, $physical: ident) => {
+            match $val {
+              Some(Property::$logical(val)) => {
+                dest.push(Property::$physical(val));
+              }
+              Some(Property::Unparsed(val)) => {
+                dest.push(Property::Unparsed(val.with_property_id(PropertyId::$physical)));
+              }
+              _ => {}
+            }
+          }
+        }
+
+        if logical_supported {
+          logical_side!(block_start, block_end, $block_shorthand, $block_start, $block_end);
+        } else {
+          prop!(block_start, $block_start, $top);
+          prop!(block_end, $block_end, $bottom);
+        }
+
+        if logical_supported {
+          logical_side!(inline_start, inline_end, $inline_shorthand, $inline_start, $inline_end);
+        } else if inline_start.is_some() || inline_end.is_some() {
+          if matches!((&inline_start, &inline_end), (Some(Property::$inline_start(start)), Some(Property::$inline_end(end))) if start == end) {
+            prop!(inline_start, $inline_start, $left);
+            prop!(inline_end, $inline_end, $right);
+          } else {
+            macro_rules! logical_prop {
+              ($val: ident, $logical: ident, $ltr: ident, $rtl: ident) => {
+                match $val {
+                  Some(Property::$logical(val)) => {
+                    context.add_logical_rule(
+                      Property::$ltr(val.clone()),
+                      Property::$rtl(val)
+                    );
+                  }
+                  Some(Property::Unparsed(val)) => {
+                    context.add_logical_rule(
+                      Property::Unparsed(val.with_property_id(PropertyId::$ltr)),
+                      Property::Unparsed(val.with_property_id(PropertyId::$rtl))
+                    );
+                  }
+                  _ => {}
+                }
+              }
+            }
+
+            logical_prop!(inline_start, $inline_start, $left, $right);
+            logical_prop!(inline_end, $inline_end, $right, $left);
+          }
+        }
+      }
+    }
+  };
+}
+
+side_handler!(
+  MarginHandler,
+  MarginTop,
+  MarginBottom,
+  MarginLeft,
+  MarginRight,
+  MarginBlockStart,
+  MarginBlockEnd,
+  MarginInlineStart,
+  MarginInlineEnd,
+  Margin,
+  MarginBlock,
+  MarginInline,
+  Physical,
+  LogicalMargin,
+  LogicalMarginShorthand
+);
+
+side_handler!(
+  PaddingHandler,
+  PaddingTop,
+  PaddingBottom,
+  PaddingLeft,
+  PaddingRight,
+  PaddingBlockStart,
+  PaddingBlockEnd,
+  PaddingInlineStart,
+  PaddingInlineEnd,
+  Padding,
+  PaddingBlock,
+  PaddingInline,
+  Physical,
+  LogicalPadding,
+  LogicalPaddingShorthand
+);
+
+side_handler!(
+  ScrollMarginHandler,
+  ScrollMarginTop,
+  ScrollMarginBottom,
+  ScrollMarginLeft,
+  ScrollMarginRight,
+  ScrollMarginBlockStart,
+  ScrollMarginBlockEnd,
+  ScrollMarginInlineStart,
+  ScrollMarginInlineEnd,
+  ScrollMargin,
+  ScrollMarginBlock,
+  ScrollMarginInline,
+  Physical
+);
+
+side_handler!(
+  ScrollPaddingHandler,
+  ScrollPaddingTop,
+  ScrollPaddingBottom,
+  ScrollPaddingLeft,
+  ScrollPaddingRight,
+  ScrollPaddingBlockStart,
+  ScrollPaddingBlockEnd,
+  ScrollPaddingInlineStart,
+  ScrollPaddingInlineEnd,
+  ScrollPadding,
+  ScrollPaddingBlock,
+  ScrollPaddingInline,
+  Physical
+);
+
+side_handler!(
+  InsetHandler,
+  Top,
+  Bottom,
+  Left,
+  Right,
+  InsetBlockStart,
+  InsetBlockEnd,
+  InsetInlineStart,
+  InsetInlineEnd,
+  Inset,
+  InsetBlock,
+  InsetInline,
+  Logical,
+  LogicalInset,
+  LogicalInset
+);
diff --git a/src/properties/masking.rs b/src/properties/masking.rs
new file mode 100644
index 0000000..ff05e1f
--- /dev/null
+++ b/src/properties/masking.rs
@@ -0,0 +1,1183 @@
+//! CSS properties related to clipping and masking.
+
+use super::background::{BackgroundRepeat, BackgroundSize};
+use super::border_image::{BorderImage, BorderImageRepeat, BorderImageSideWidth, BorderImageSlice};
+use super::PropertyId;
+use crate::context::PropertyHandlerContext;
+use crate::declaration::{DeclarationBlock, DeclarationList};
+use crate::error::{ParserError, PrinterError};
+use crate::macros::{define_list_shorthand, define_shorthand, enum_property, property_bitflags};
+use crate::prefixes::Feature;
+use crate::printer::Printer;
+use crate::properties::Property;
+use crate::targets::{Browsers, Targets};
+use crate::traits::{FallbackValues, IsCompatible, Parse, PropertyHandler, Shorthand, ToCss};
+use crate::values::image::ImageFallback;
+use crate::values::length::LengthOrNumber;
+use crate::values::rect::Rect;
+use crate::values::{image::Image, position::Position, shape::BasicShape, url::Url};
+use crate::vendor_prefix::VendorPrefix;
+#[cfg(feature = "visitor")]
+use crate::visitor::Visit;
+use cssparser::*;
+use itertools::izip;
+use smallvec::SmallVec;
+
+enum_property! {
+  /// A value for the [mask-type](https://www.w3.org/TR/css-masking-1/#the-mask-type) property.
+  pub enum MaskType {
+    /// The luminance values of the mask is used.
+    Luminance,
+    /// The alpha values of the mask is used.
+    Alpha,
+  }
+}
+
+enum_property! {
+  /// A value for the [mask-mode](https://www.w3.org/TR/css-masking-1/#the-mask-mode) property.
+  pub enum MaskMode {
+    /// The luminance values of the mask image is used.
+    Luminance,
+    /// The alpha values of the mask image is used.
+    Alpha,
+    /// If an SVG source is used, the value matches the `mask-type` property. Otherwise, the alpha values are used.
+    MatchSource,
+  }
+}
+
+impl Default for MaskMode {
+  fn default() -> MaskMode {
+    MaskMode::MatchSource
+  }
+}
+
+enum_property! {
+  /// A value for the [-webkit-mask-source-type](https://github.com/WebKit/WebKit/blob/6eece09a1c31e47489811edd003d1e36910e9fd3/Source/WebCore/css/CSSProperties.json#L6578-L6587)
+  /// property.
+  ///
+  /// See also [MaskMode](MaskMode).
+  pub enum WebKitMaskSourceType {
+    /// Equivalent to `match-source` in the standard `mask-mode` syntax.
+    Auto,
+    /// The luminance values of the mask image is used.
+    Luminance,
+    /// The alpha values of the mask image is used.
+    Alpha,
+  }
+}
+
+impl From<MaskMode> for WebKitMaskSourceType {
+  fn from(mode: MaskMode) -> WebKitMaskSourceType {
+    match mode {
+      MaskMode::Luminance => WebKitMaskSourceType::Luminance,
+      MaskMode::Alpha => WebKitMaskSourceType::Alpha,
+      MaskMode::MatchSource => WebKitMaskSourceType::Auto,
+    }
+  }
+}
+
+enum_property! {
+  /// A [`<geometry-box>`](https://www.w3.org/TR/css-masking-1/#typedef-geometry-box) value
+  /// as used in the `mask-clip` and `clip-path` properties.
+  pub enum GeometryBox {
+    /// The painted content is clipped to the content box.
+    BorderBox,
+    /// The painted content is clipped to the padding box.
+    PaddingBox,
+    /// The painted content is clipped to the border box.
+    ContentBox,
+    /// The painted content is clipped to the margin box.
+    MarginBox,
+    /// The painted content is clipped to the object bounding box.
+    FillBox,
+    /// The painted content is clipped to the stroke bounding box.
+    StrokeBox,
+    /// Uses the nearest SVG viewport as reference box.
+    ViewBox,
+  }
+}
+
+impl Default for GeometryBox {
+  fn default() -> GeometryBox {
+    GeometryBox::BorderBox
+  }
+}
+
+/// A value for the [mask-clip](https://www.w3.org/TR/css-masking-1/#the-mask-clip) property.
+#[derive(Debug, Clone, PartialEq, Parse, ToCss)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", content = "value", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum MaskClip {
+  /// A geometry box.
+  GeometryBox(GeometryBox),
+  /// The painted content is not clipped.
+  NoClip,
+}
+
+impl IsCompatible for MaskClip {
+  fn is_compatible(&self, browsers: Browsers) -> bool {
+    match self {
+      MaskClip::GeometryBox(g) => g.is_compatible(browsers),
+      MaskClip::NoClip => true,
+    }
+  }
+}
+
+impl Into<MaskClip> for GeometryBox {
+  fn into(self) -> MaskClip {
+    MaskClip::GeometryBox(self.clone())
+  }
+}
+
+impl IsCompatible for GeometryBox {
+  fn is_compatible(&self, _browsers: Browsers) -> bool {
+    true
+  }
+}
+
+enum_property! {
+  /// A value for the [mask-composite](https://www.w3.org/TR/css-masking-1/#the-mask-composite) property.
+  pub enum MaskComposite {
+    /// The source is placed over the destination.
+    Add,
+    /// The source is placed, where it falls outside of the destination.
+    Subtract,
+    /// The parts of source that overlap the destination, replace the destination.
+    Intersect,
+    /// The non-overlapping regions of source and destination are combined.
+    Exclude,
+  }
+}
+
+impl Default for MaskComposite {
+  fn default() -> MaskComposite {
+    MaskComposite::Add
+  }
+}
+
+enum_property! {
+  /// A value for the [-webkit-mask-composite](https://developer.mozilla.org/en-US/docs/Web/CSS/-webkit-mask-composite)
+  /// property.
+  ///
+  /// See also [MaskComposite](MaskComposite).
+  #[allow(missing_docs)]
+  pub enum WebKitMaskComposite {
+    Clear,
+    Copy,
+    /// Equivalent to `add` in the standard `mask-composite` syntax.
+    SourceOver,
+    /// Equivalent to `intersect` in the standard `mask-composite` syntax.
+    SourceIn,
+    /// Equivalent to `subtract` in the standard `mask-composite` syntax.
+    SourceOut,
+    SourceAtop,
+    DestinationOver,
+    DestinationIn,
+    DestinationOut,
+    DestinationAtop,
+    /// Equivalent to `exclude` in the standard `mask-composite` syntax.
+    Xor,
+  }
+}
+
+impl From<MaskComposite> for WebKitMaskComposite {
+  fn from(composite: MaskComposite) -> WebKitMaskComposite {
+    match composite {
+      MaskComposite::Add => WebKitMaskComposite::SourceOver,
+      MaskComposite::Subtract => WebKitMaskComposite::SourceOut,
+      MaskComposite::Intersect => WebKitMaskComposite::SourceIn,
+      MaskComposite::Exclude => WebKitMaskComposite::Xor,
+    }
+  }
+}
+
+define_list_shorthand! {
+  /// A value for the [mask](https://www.w3.org/TR/css-masking-1/#the-mask) shorthand property.
+  pub struct Mask<'i>(VendorPrefix) {
+    /// The mask image.
+    #[cfg_attr(feature = "serde", serde(borrow))]
+    image: MaskImage(Image<'i>, VendorPrefix),
+    /// The position of the mask.
+    position: MaskPosition(Position, VendorPrefix),
+    /// The size of the mask image.
+    size: MaskSize(BackgroundSize, VendorPrefix),
+    /// How the mask repeats.
+    repeat: MaskRepeat(BackgroundRepeat, VendorPrefix),
+    /// The box in which the mask is clipped.
+    clip: MaskClip(MaskClip, VendorPrefix),
+    /// The origin of the mask.
+    origin: MaskOrigin(GeometryBox, VendorPrefix),
+    /// How the mask is composited with the element.
+    composite: MaskComposite(MaskComposite),
+    /// How the mask image is interpreted.
+    mode: MaskMode(MaskMode),
+  }
+}
+
+impl<'i> Parse<'i> for Mask<'i> {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let mut image: Option<Image> = None;
+    let mut position: Option<Position> = None;
+    let mut size: Option<BackgroundSize> = None;
+    let mut repeat: Option<BackgroundRepeat> = None;
+    let mut clip: Option<MaskClip> = None;
+    let mut origin: Option<GeometryBox> = None;
+    let mut composite: Option<MaskComposite> = None;
+    let mut mode: Option<MaskMode> = None;
+
+    loop {
+      if image.is_none() {
+        if let Ok(value) = input.try_parse(Image::parse) {
+          image = Some(value);
+          continue;
+        }
+      }
+
+      if position.is_none() {
+        if let Ok(value) = input.try_parse(Position::parse) {
+          position = Some(value);
+          size = input
+            .try_parse(|input| {
+              input.expect_delim('/')?;
+              BackgroundSize::parse(input)
+            })
+            .ok();
+          continue;
+        }
+      }
+
+      if repeat.is_none() {
+        if let Ok(value) = input.try_parse(BackgroundRepeat::parse) {
+          repeat = Some(value);
+          continue;
+        }
+      }
+
+      if origin.is_none() {
+        if let Ok(value) = input.try_parse(GeometryBox::parse) {
+          origin = Some(value);
+          continue;
+        }
+      }
+
+      if clip.is_none() {
+        if let Ok(value) = input.try_parse(MaskClip::parse) {
+          clip = Some(value);
+          continue;
+        }
+      }
+
+      if composite.is_none() {
+        if let Ok(value) = input.try_parse(MaskComposite::parse) {
+          composite = Some(value);
+          continue;
+        }
+      }
+
+      if mode.is_none() {
+        if let Ok(value) = input.try_parse(MaskMode::parse) {
+          mode = Some(value);
+          continue;
+        }
+      }
+
+      break;
+    }
+
+    if clip.is_none() {
+      if let Some(origin) = origin {
+        clip = Some(origin.into());
+      }
+    }
+
+    Ok(Mask {
+      image: image.unwrap_or_default(),
+      position: position.unwrap_or_default(),
+      repeat: repeat.unwrap_or_default(),
+      size: size.unwrap_or_default(),
+      origin: origin.unwrap_or(GeometryBox::BorderBox),
+      clip: clip.unwrap_or(GeometryBox::BorderBox.into()),
+      composite: composite.unwrap_or(MaskComposite::Add),
+      mode: mode.unwrap_or(MaskMode::MatchSource),
+    })
+  }
+}
+
+impl<'i> ToCss for Mask<'i> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    self.image.to_css(dest)?;
+
+    if self.position != Position::default() || self.size != BackgroundSize::default() {
+      dest.write_char(' ')?;
+      self.position.to_css(dest)?;
+
+      if self.size != BackgroundSize::default() {
+        dest.delim('/', true)?;
+        self.size.to_css(dest)?;
+      }
+    }
+
+    if self.repeat != BackgroundRepeat::default() {
+      dest.write_char(' ')?;
+      self.repeat.to_css(dest)?;
+    }
+
+    if self.origin != GeometryBox::BorderBox || self.clip != GeometryBox::BorderBox.into() {
+      dest.write_char(' ')?;
+      self.origin.to_css(dest)?;
+
+      if self.clip != self.origin.into() {
+        dest.write_char(' ')?;
+        self.clip.to_css(dest)?;
+      }
+    }
+
+    if self.composite != MaskComposite::default() {
+      dest.write_char(' ')?;
+      self.composite.to_css(dest)?;
+    }
+
+    if self.mode != MaskMode::default() {
+      dest.write_char(' ')?;
+      self.mode.to_css(dest)?;
+    }
+
+    Ok(())
+  }
+}
+
+// TODO: shorthand handler?
+impl<'i> ImageFallback<'i> for Mask<'i> {
+  #[inline]
+  fn get_image(&self) -> &Image<'i> {
+    &self.image
+  }
+
+  #[inline]
+  fn with_image(&self, image: Image<'i>) -> Self {
+    Mask { image, ..self.clone() }
+  }
+}
+
+/// A value for the [clip-path](https://www.w3.org/TR/css-masking-1/#the-clip-path) property.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub enum ClipPath<'i> {
+  /// No clip path.
+  None,
+  /// A url reference to an SVG path element.
+  #[cfg_attr(feature = "serde", serde(borrow, with = "crate::serialization::ValueWrapper::<Url>"))]
+  Url(Url<'i>),
+  /// A basic shape, positioned according to the reference box.
+  #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
+  Shape {
+    /// A basic shape.
+    shape: Box<BasicShape>,
+    /// A reference box that the shape is positioned according to.
+    reference_box: GeometryBox,
+  },
+  /// A reference box.
+  #[cfg_attr(feature = "serde", serde(with = "crate::serialization::ValueWrapper::<GeometryBox>"))]
+  Box(GeometryBox),
+}
+
+impl<'i> Parse<'i> for ClipPath<'i> {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    if let Ok(url) = input.try_parse(Url::parse) {
+      return Ok(ClipPath::Url(url));
+    }
+
+    if let Ok(shape) = input.try_parse(BasicShape::parse) {
+      let b = input.try_parse(GeometryBox::parse).unwrap_or_default();
+      return Ok(ClipPath::Shape {
+        shape: Box::new(shape),
+        reference_box: b,
+      });
+    }
+
+    if let Ok(b) = input.try_parse(GeometryBox::parse) {
+      if let Ok(shape) = input.try_parse(BasicShape::parse) {
+        return Ok(ClipPath::Shape {
+          shape: Box::new(shape),
+          reference_box: b,
+        });
+      }
+      return Ok(ClipPath::Box(b));
+    }
+
+    input.expect_ident_matching("none")?;
+    Ok(ClipPath::None)
+  }
+}
+
+impl<'i> ToCss for ClipPath<'i> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    match self {
+      ClipPath::None => dest.write_str("none"),
+      ClipPath::Url(url) => url.to_css(dest),
+      ClipPath::Shape {
+        shape,
+        reference_box: b,
+      } => {
+        shape.to_css(dest)?;
+        if *b != GeometryBox::default() {
+          dest.write_char(' ')?;
+          b.to_css(dest)?;
+        }
+        Ok(())
+      }
+      ClipPath::Box(b) => b.to_css(dest),
+    }
+  }
+}
+
+enum_property! {
+  /// A value for the [mask-border-mode](https://www.w3.org/TR/css-masking-1/#the-mask-border-mode) property.
+  pub enum MaskBorderMode {
+    /// The luminance values of the mask image is used.
+    Luminance,
+    /// The alpha values of the mask image is used.
+    Alpha,
+  }
+}
+
+impl Default for MaskBorderMode {
+  fn default() -> MaskBorderMode {
+    MaskBorderMode::Alpha
+  }
+}
+
+define_shorthand! {
+  /// A value for the [mask-border](https://www.w3.org/TR/css-masking-1/#the-mask-border) shorthand property.
+  #[derive(Default)]
+  pub struct MaskBorder<'i> {
+    /// The mask image.
+    #[cfg_attr(feature = "serde", serde(borrow))]
+    source: MaskBorderSource(Image<'i>),
+    /// The offsets that define where the image is sliced.
+    slice: MaskBorderSlice(BorderImageSlice),
+    /// The width of the mask image.
+    width: MaskBorderWidth(Rect<BorderImageSideWidth>),
+    /// The amount that the image extends beyond the border box.
+    outset: MaskBorderOutset(Rect<LengthOrNumber>),
+    /// How the mask image is scaled and tiled.
+    repeat: MaskBorderRepeat(BorderImageRepeat),
+    /// How the mask image is interpreted.
+    mode: MaskBorderMode(MaskBorderMode),
+  }
+}
+
+impl<'i> Parse<'i> for MaskBorder<'i> {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let mut mode: Option<MaskBorderMode> = None;
+    let border_image = BorderImage::parse_with_callback(input, |input| {
+      if mode.is_none() {
+        if let Ok(value) = input.try_parse(MaskBorderMode::parse) {
+          mode = Some(value);
+          return true;
+        }
+      }
+      false
+    });
+
+    if border_image.is_ok() || mode.is_some() {
+      let border_image = border_image.unwrap_or_default();
+      Ok(MaskBorder {
+        source: border_image.source,
+        slice: border_image.slice,
+        width: border_image.width,
+        outset: border_image.outset,
+        repeat: border_image.repeat,
+        mode: mode.unwrap_or_default(),
+      })
+    } else {
+      Err(input.new_custom_error(ParserError::InvalidDeclaration))
+    }
+  }
+}
+
+impl<'i> ToCss for MaskBorder<'i> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    BorderImage::to_css_internal(&self.source, &self.slice, &self.width, &self.outset, &self.repeat, dest)?;
+    if self.mode != MaskBorderMode::default() {
+      dest.write_char(' ')?;
+      self.mode.to_css(dest)?;
+    }
+    Ok(())
+  }
+}
+
+impl<'i> FallbackValues for MaskBorder<'i> {
+  fn get_fallbacks(&mut self, targets: Targets) -> Vec<Self> {
+    self
+      .source
+      .get_fallbacks(targets)
+      .into_iter()
+      .map(|source| MaskBorder { source, ..self.clone() })
+      .collect()
+  }
+}
+
+impl<'i> Into<BorderImage<'i>> for MaskBorder<'i> {
+  fn into(self) -> BorderImage<'i> {
+    BorderImage {
+      source: self.source,
+      slice: self.slice,
+      width: self.width,
+      outset: self.outset,
+      repeat: self.repeat,
+    }
+  }
+}
+
+property_bitflags! {
+  #[derive(Default, Debug)]
+  struct MaskProperty: u16 {
+    const MaskImage(_vp) = 1 << 0;
+    const MaskPosition(_vp) = 1 << 1;
+    const MaskSize(_vp) = 1 << 2;
+    const MaskRepeat(_vp) = 1 << 3;
+    const MaskClip(_vp) = 1 << 4;
+    const MaskOrigin(_vp) = 1 << 5;
+    const MaskComposite = 1 << 6;
+    const MaskMode = 1 << 7;
+    const Mask(_vp) = Self::MaskImage.bits() | Self::MaskPosition.bits() | Self::MaskSize.bits() | Self::MaskRepeat.bits() | Self::MaskClip.bits() | Self::MaskOrigin.bits() | Self::MaskComposite.bits() | Self::MaskMode.bits();
+
+    const MaskBorderSource = 1 << 7;
+    const MaskBorderMode = 1 << 8;
+    const MaskBorderSlice = 1 << 9;
+    const MaskBorderWidth = 1 << 10;
+    const MaskBorderOutset = 1 << 11;
+    const MaskBorderRepeat = 1 << 12;
+    const MaskBorder = Self::MaskBorderSource.bits() | Self::MaskBorderMode.bits() | Self::MaskBorderSlice.bits() | Self::MaskBorderWidth.bits() | Self::MaskBorderOutset.bits() | Self::MaskBorderRepeat.bits();
+  }
+}
+
+#[derive(Default)]
+pub(crate) struct MaskHandler<'i> {
+  images: Option<(SmallVec<[Image<'i>; 1]>, VendorPrefix)>,
+  positions: Option<(SmallVec<[Position; 1]>, VendorPrefix)>,
+  sizes: Option<(SmallVec<[BackgroundSize; 1]>, VendorPrefix)>,
+  repeats: Option<(SmallVec<[BackgroundRepeat; 1]>, VendorPrefix)>,
+  clips: Option<(SmallVec<[MaskClip; 1]>, VendorPrefix)>,
+  origins: Option<(SmallVec<[GeometryBox; 1]>, VendorPrefix)>,
+  composites: Option<SmallVec<[MaskComposite; 1]>>,
+  modes: Option<SmallVec<[MaskMode; 1]>>,
+  border_source: Option<(Image<'i>, VendorPrefix)>,
+  border_mode: Option<MaskBorderMode>,
+  border_slice: Option<(BorderImageSlice, VendorPrefix)>,
+  border_width: Option<(Rect<BorderImageSideWidth>, VendorPrefix)>,
+  border_outset: Option<(Rect<LengthOrNumber>, VendorPrefix)>,
+  border_repeat: Option<(BorderImageRepeat, VendorPrefix)>,
+  flushed_properties: MaskProperty,
+  has_any: bool,
+}
+
+impl<'i> PropertyHandler<'i> for MaskHandler<'i> {
+  fn handle_property(
+    &mut self,
+    property: &Property<'i>,
+    dest: &mut DeclarationList<'i>,
+    context: &mut PropertyHandlerContext<'i, '_>,
+  ) -> bool {
+    macro_rules! maybe_flush {
+      ($prop: ident, $val: expr, $vp: expr) => {{
+        // If two vendor prefixes for the same property have different
+        // values, we need to flush what we have immediately to preserve order.
+        if let Some((val, prefixes)) = &self.$prop {
+          if val != $val && !prefixes.contains(*$vp) {
+            self.flush(dest, context);
+          }
+        }
+
+        if self.$prop.is_some() && matches!(context.targets.browsers, Some(targets) if !$val.is_compatible(targets)) {
+          self.flush(dest, context);
+        }
+      }};
+    }
+
+    macro_rules! property {
+      ($prop: ident, $val: expr, $vp: expr) => {{
+        maybe_flush!($prop, $val, $vp);
+
+        // Otherwise, update the value and add the prefix.
+        if let Some((val, prefixes)) = &mut self.$prop {
+          *val = $val.clone();
+          *prefixes |= *$vp;
+        } else {
+          self.$prop = Some(($val.clone(), *$vp));
+          self.has_any = true;
+        }
+      }};
+    }
+
+    macro_rules! border_shorthand {
+      ($val: expr, $vp: expr) => {
+        let source = $val.source.clone();
+        maybe_flush!(border_source, &source, &$vp);
+
+        let slice = $val.slice.clone();
+        maybe_flush!(border_slice, &slice, &$vp);
+
+        let width = $val.width.clone();
+        maybe_flush!(border_width, &width, &$vp);
+
+        let outset = $val.outset.clone();
+        maybe_flush!(border_outset, &outset, &$vp);
+
+        let repeat = $val.repeat.clone();
+        maybe_flush!(border_repeat, &repeat, &$vp);
+
+        property!(border_source, &source, &$vp);
+        property!(border_slice, &slice, &$vp);
+        property!(border_width, &width, &$vp);
+        property!(border_outset, &outset, &$vp);
+        property!(border_repeat, &repeat, &$vp);
+      };
+    }
+
+    match property {
+      Property::MaskImage(val, vp) => property!(images, val, vp),
+      Property::MaskPosition(val, vp) => property!(positions, val, vp),
+      Property::MaskSize(val, vp) => property!(sizes, val, vp),
+      Property::MaskRepeat(val, vp) => property!(repeats, val, vp),
+      Property::MaskClip(val, vp) => property!(clips, val, vp),
+      Property::MaskOrigin(val, vp) => property!(origins, val, vp),
+      Property::MaskComposite(val) => self.composites = Some(val.clone()),
+      Property::MaskMode(val) => self.modes = Some(val.clone()),
+      Property::Mask(val, prefix) => {
+        let images = val.iter().map(|b| b.image.clone()).collect();
+        maybe_flush!(images, &images, prefix);
+
+        let positions = val.iter().map(|b| b.position.clone()).collect();
+        maybe_flush!(positions, &positions, prefix);
+
+        let sizes = val.iter().map(|b| b.size.clone()).collect();
+        maybe_flush!(sizes, &sizes, prefix);
+
+        let repeats = val.iter().map(|b| b.repeat.clone()).collect();
+        maybe_flush!(repeats, &repeats, prefix);
+
+        let clips = val.iter().map(|b| b.clip.clone()).collect();
+        maybe_flush!(clips, &clips, prefix);
+
+        let origins = val.iter().map(|b| b.origin.clone()).collect();
+        maybe_flush!(origins, &origins, prefix);
+
+        self.composites = Some(val.iter().map(|b| b.composite.clone()).collect());
+        self.modes = Some(val.iter().map(|b| b.mode.clone()).collect());
+
+        property!(images, &images, prefix);
+        property!(positions, &positions, prefix);
+        property!(sizes, &sizes, prefix);
+        property!(repeats, &repeats, prefix);
+        property!(clips, &clips, prefix);
+        property!(origins, &origins, prefix);
+      }
+      Property::Unparsed(val) if is_mask_property(&val.property_id) => {
+        self.flush(dest, context);
+        let mut unparsed = val.get_prefixed(context.targets, Feature::Mask);
+        context.add_unparsed_fallbacks(&mut unparsed);
+        self
+          .flushed_properties
+          .insert(MaskProperty::try_from(&val.property_id).unwrap());
+        dest.push(Property::Unparsed(unparsed));
+      }
+      Property::MaskBorderSource(val) => property!(border_source, val, &VendorPrefix::None),
+      Property::WebKitMaskBoxImageSource(val, _) => property!(border_source, val, &VendorPrefix::WebKit),
+      Property::MaskBorderMode(val) => self.border_mode = Some(val.clone()),
+      Property::MaskBorderSlice(val) => property!(border_slice, val, &VendorPrefix::None),
+      Property::WebKitMaskBoxImageSlice(val, _) => property!(border_slice, val, &VendorPrefix::WebKit),
+      Property::MaskBorderWidth(val) => property!(border_width, val, &VendorPrefix::None),
+      Property::WebKitMaskBoxImageWidth(val, _) => property!(border_width, val, &VendorPrefix::WebKit),
+      Property::MaskBorderOutset(val) => property!(border_outset, val, &VendorPrefix::None),
+      Property::WebKitMaskBoxImageOutset(val, _) => property!(border_outset, val, &VendorPrefix::WebKit),
+      Property::MaskBorderRepeat(val) => property!(border_repeat, val, &VendorPrefix::None),
+      Property::WebKitMaskBoxImageRepeat(val, _) => property!(border_repeat, val, &VendorPrefix::WebKit),
+      Property::MaskBorder(val) => {
+        border_shorthand!(val, VendorPrefix::None);
+        self.border_mode = Some(val.mode.clone());
+      }
+      Property::WebKitMaskBoxImage(val, _) => {
+        border_shorthand!(val, VendorPrefix::WebKit);
+      }
+      Property::Unparsed(val) if is_mask_border_property(&val.property_id) => {
+        self.flush(dest, context);
+        // Add vendor prefixes and expand color fallbacks.
+        let mut val = val.clone();
+        let prefix = context
+          .targets
+          .prefixes(val.property_id.prefix().or_none(), Feature::MaskBorder);
+        if prefix.contains(VendorPrefix::WebKit) {
+          if let Some(property_id) = get_webkit_mask_property(&val.property_id) {
+            let mut clone = val.clone();
+            clone.property_id = property_id;
+            context.add_unparsed_fallbacks(&mut clone);
+            dest.push(Property::Unparsed(clone));
+          }
+        }
+
+        context.add_unparsed_fallbacks(&mut val);
+        self
+          .flushed_properties
+          .insert(MaskProperty::try_from(&val.property_id).unwrap());
+        dest.push(Property::Unparsed(val));
+      }
+      _ => return false,
+    }
+
+    self.has_any = true;
+    true
+  }
+
+  fn finalize(&mut self, dest: &mut DeclarationList<'i>, context: &mut PropertyHandlerContext<'i, '_>) {
+    self.flush(dest, context);
+    self.flushed_properties = MaskProperty::empty();
+  }
+}
+
+impl<'i> MaskHandler<'i> {
+  fn flush(&mut self, dest: &mut DeclarationList<'i>, context: &mut PropertyHandlerContext<'i, '_>) {
+    if !self.has_any {
+      return;
+    }
+
+    self.has_any = false;
+
+    self.flush_mask(dest, context);
+    self.flush_mask_border(dest, context);
+  }
+
+  fn flush_mask(&mut self, dest: &mut DeclarationList<'i>, context: &mut PropertyHandlerContext<'i, '_>) {
+    let mut images = std::mem::take(&mut self.images);
+    let mut positions = std::mem::take(&mut self.positions);
+    let mut sizes = std::mem::take(&mut self.sizes);
+    let mut repeats = std::mem::take(&mut self.repeats);
+    let mut clips = std::mem::take(&mut self.clips);
+    let mut origins = std::mem::take(&mut self.origins);
+    let mut composites = std::mem::take(&mut self.composites);
+    let mut modes = std::mem::take(&mut self.modes);
+
+    if let (
+      Some((images, images_vp)),
+      Some((positions, positions_vp)),
+      Some((sizes, sizes_vp)),
+      Some((repeats, repeats_vp)),
+      Some((clips, clips_vp)),
+      Some((origins, origins_vp)),
+      Some(composites_val),
+      Some(mode_vals),
+    ) = (
+      &mut images,
+      &mut positions,
+      &mut sizes,
+      &mut repeats,
+      &mut clips,
+      &mut origins,
+      &mut composites,
+      &mut modes,
+    ) {
+      // Only use shorthand syntax if the number of masks matches on all properties.
+      let len = images.len();
+      let intersection = *images_vp & *positions_vp & *sizes_vp & *repeats_vp & *clips_vp & *origins_vp;
+      if !intersection.is_empty()
+        && positions.len() == len
+        && sizes.len() == len
+        && repeats.len() == len
+        && clips.len() == len
+        && origins.len() == len
+        && composites_val.len() == len
+        && mode_vals.len() == len
+      {
+        let mut masks: SmallVec<[Mask<'i>; 1]> = izip!(
+          images.drain(..),
+          positions.drain(..),
+          sizes.drain(..),
+          repeats.drain(..),
+          clips.drain(..),
+          origins.drain(..),
+          composites_val.drain(..),
+          mode_vals.drain(..)
+        )
+        .map(|(image, position, size, repeat, clip, origin, composite, mode)| Mask {
+          image,
+          position,
+          size,
+          repeat,
+          clip,
+          origin,
+          composite,
+          mode,
+        })
+        .collect();
+
+        let mut prefix = context.targets.prefixes(intersection, Feature::Mask);
+        if !self.flushed_properties.intersects(MaskProperty::Mask) {
+          for fallback in masks.get_fallbacks(context.targets) {
+            // Match prefix of fallback. e.g. -webkit-linear-gradient
+            // can only be used in -webkit-mask-image.
+            // However, if mask-image is unprefixed, gradients can still be.
+            let mut p = fallback
+              .iter()
+              .fold(VendorPrefix::empty(), |p, mask| p | mask.image.get_vendor_prefix())
+              - VendorPrefix::None
+              & prefix;
+            if p.is_empty() {
+              p = prefix;
+            }
+            self.flush_mask_shorthand(fallback, p, dest);
+          }
+
+          let p = masks
+            .iter()
+            .fold(VendorPrefix::empty(), |p, mask| p | mask.image.get_vendor_prefix())
+            - VendorPrefix::None
+            & prefix;
+          if !p.is_empty() {
+            prefix = p;
+          }
+        }
+
+        self.flush_mask_shorthand(masks, prefix, dest);
+        self.flushed_properties.insert(MaskProperty::Mask);
+
+        images_vp.remove(intersection);
+        positions_vp.remove(intersection);
+        sizes_vp.remove(intersection);
+        repeats_vp.remove(intersection);
+        clips_vp.remove(intersection);
+        origins_vp.remove(intersection);
+        composites = None;
+        modes = None;
+      }
+    }
+
+    macro_rules! prop {
+      ($var: ident, $property: ident) => {
+        if let Some((val, vp)) = $var {
+          if !vp.is_empty() {
+            let prefix = context.targets.prefixes(vp, Feature::$property);
+            dest.push(Property::$property(val, prefix));
+            self.flushed_properties.insert(MaskProperty::$property);
+          }
+        }
+      };
+    }
+
+    if let Some((mut images, vp)) = images {
+      if !vp.is_empty() {
+        let mut prefix = vp;
+        if !self.flushed_properties.contains(MaskProperty::MaskImage) {
+          prefix = context.targets.prefixes(prefix, Feature::MaskImage);
+          for fallback in images.get_fallbacks(context.targets) {
+            // Match prefix of fallback. e.g. -webkit-linear-gradient
+            // can only be used in -webkit-mask-image.
+            // However, if mask-image is unprefixed, gradients can still be.
+            let mut p = fallback
+              .iter()
+              .fold(VendorPrefix::empty(), |p, image| p | image.get_vendor_prefix())
+              - VendorPrefix::None
+              & prefix;
+            if p.is_empty() {
+              p = prefix;
+            }
+            dest.push(Property::MaskImage(fallback, p))
+          }
+
+          let p = images
+            .iter()
+            .fold(VendorPrefix::empty(), |p, image| p | image.get_vendor_prefix())
+            - VendorPrefix::None
+            & prefix;
+          if !p.is_empty() {
+            prefix = p;
+          }
+        }
+
+        dest.push(Property::MaskImage(images, prefix));
+        self.flushed_properties.insert(MaskProperty::MaskImage);
+      }
+    }
+
+    prop!(positions, MaskPosition);
+    prop!(sizes, MaskSize);
+    prop!(repeats, MaskRepeat);
+    prop!(clips, MaskClip);
+    prop!(origins, MaskOrigin);
+
+    if let Some(composites) = composites {
+      let prefix = context.targets.prefixes(VendorPrefix::None, Feature::MaskComposite);
+      if prefix.contains(VendorPrefix::WebKit) {
+        dest.push(Property::WebKitMaskComposite(
+          composites.iter().map(|c| (*c).into()).collect(),
+        ));
+      }
+
+      dest.push(Property::MaskComposite(composites));
+      self.flushed_properties.insert(MaskProperty::MaskComposite);
+    }
+
+    if let Some(modes) = modes {
+      let prefix = context.targets.prefixes(VendorPrefix::None, Feature::Mask);
+      if prefix.contains(VendorPrefix::WebKit) {
+        dest.push(Property::WebKitMaskSourceType(
+          modes.iter().map(|c| (*c).into()).collect(),
+          VendorPrefix::WebKit,
+        ));
+      }
+
+      dest.push(Property::MaskMode(modes));
+      self.flushed_properties.insert(MaskProperty::MaskMode);
+    }
+  }
+
+  fn flush_mask_shorthand(
+    &self,
+    masks: SmallVec<[Mask<'i>; 1]>,
+    prefix: VendorPrefix,
+    dest: &mut DeclarationList<'i>,
+  ) {
+    if prefix.contains(VendorPrefix::WebKit)
+      && masks
+        .iter()
+        .any(|mask| mask.composite != MaskComposite::default() || mask.mode != MaskMode::default())
+    {
+      // Prefixed shorthand syntax did not support mask-composite or mask-mode. These map to different webkit-specific properties.
+      // -webkit-mask-composite uses a different syntax than mask-composite.
+      // -webkit-mask-source-type is equivalent to mask-mode, but only supported in Safari, not Chrome.
+      let mut webkit = masks.clone();
+      let mut composites: SmallVec<[WebKitMaskComposite; 1]> = SmallVec::new();
+      let mut modes: SmallVec<[WebKitMaskSourceType; 1]> = SmallVec::new();
+      let mut needs_composites = false;
+      let mut needs_modes = false;
+      for mask in &mut webkit {
+        let composite = std::mem::take(&mut mask.composite);
+        if composite != MaskComposite::default() {
+          needs_composites = true;
+        }
+        composites.push(composite.into());
+
+        let mode = std::mem::take(&mut mask.mode);
+        if mode != MaskMode::default() {
+          needs_modes = true;
+        }
+        modes.push(mode.into());
+      }
+
+      dest.push(Property::Mask(webkit, VendorPrefix::WebKit));
+      if needs_composites {
+        dest.push(Property::WebKitMaskComposite(composites));
+      }
+      if needs_modes {
+        dest.push(Property::WebKitMaskSourceType(modes, VendorPrefix::WebKit));
+      }
+
+      let prefix = prefix - VendorPrefix::WebKit;
+      if !prefix.is_empty() {
+        dest.push(Property::Mask(masks, prefix));
+      }
+    } else {
+      dest.push(Property::Mask(masks, prefix));
+    }
+  }
+
+  fn flush_mask_border(&mut self, dest: &mut DeclarationList<'i>, context: &mut PropertyHandlerContext<'i, '_>) {
+    let mut source = std::mem::take(&mut self.border_source);
+    let mut slice = std::mem::take(&mut self.border_slice);
+    let mut width = std::mem::take(&mut self.border_width);
+    let mut outset = std::mem::take(&mut self.border_outset);
+    let mut repeat = std::mem::take(&mut self.border_repeat);
+    let mut mode = std::mem::take(&mut self.border_mode);
+
+    if let (
+      Some((source, source_vp)),
+      Some((slice, slice_vp)),
+      Some((width, width_vp)),
+      Some((outset, outset_vp)),
+      Some((repeat, repeat_vp)),
+    ) = (&mut source, &mut slice, &mut width, &mut outset, &mut repeat)
+    {
+      let intersection = *source_vp & *slice_vp & *width_vp & *outset_vp & *repeat_vp;
+      if !intersection.is_empty() && (!intersection.contains(VendorPrefix::None) || mode.is_some()) {
+        let mut mask_border = MaskBorder {
+          source: source.clone(),
+          slice: slice.clone(),
+          width: width.clone(),
+          outset: outset.clone(),
+          repeat: repeat.clone(),
+          mode: mode.unwrap_or_default(),
+        };
+
+        let mut prefix = context.targets.prefixes(intersection, Feature::MaskBorder);
+        if !self.flushed_properties.intersects(MaskProperty::MaskBorder) {
+          // Get vendor prefix and color fallbacks.
+          let fallbacks = mask_border.get_fallbacks(context.targets);
+          for fallback in fallbacks {
+            let mut p = fallback.source.get_vendor_prefix() - VendorPrefix::None & prefix;
+            if p.is_empty() {
+              p = prefix;
+            }
+
+            if p.contains(VendorPrefix::WebKit) {
+              dest.push(Property::WebKitMaskBoxImage(
+                fallback.clone().into(),
+                VendorPrefix::WebKit,
+              ));
+            }
+
+            if p.contains(VendorPrefix::None) {
+              dest.push(Property::MaskBorder(fallback));
+            }
+          }
+        }
+
+        let p = mask_border.source.get_vendor_prefix() - VendorPrefix::None & prefix;
+        if !p.is_empty() {
+          prefix = p;
+        }
+
+        if prefix.contains(VendorPrefix::WebKit) {
+          dest.push(Property::WebKitMaskBoxImage(
+            mask_border.clone().into(),
+            VendorPrefix::WebKit,
+          ));
+        }
+
+        if prefix.contains(VendorPrefix::None) {
+          dest.push(Property::MaskBorder(mask_border));
+          self.flushed_properties.insert(MaskProperty::MaskBorder);
+          mode = None;
+        }
+
+        source_vp.remove(intersection);
+        slice_vp.remove(intersection);
+        width_vp.remove(intersection);
+        outset_vp.remove(intersection);
+        repeat_vp.remove(intersection);
+      }
+    }
+
+    if let Some((mut source, mut prefix)) = source {
+      prefix = context.targets.prefixes(prefix, Feature::MaskBorderSource);
+
+      if !self.flushed_properties.contains(MaskProperty::MaskBorderSource) {
+        // Get vendor prefix and color fallbacks.
+        let fallbacks = source.get_fallbacks(context.targets);
+        for fallback in fallbacks {
+          if prefix.contains(VendorPrefix::WebKit) {
+            dest.push(Property::WebKitMaskBoxImageSource(
+              fallback.clone(),
+              VendorPrefix::WebKit,
+            ));
+          }
+
+          if prefix.contains(VendorPrefix::None) {
+            dest.push(Property::MaskBorderSource(fallback));
+          }
+        }
+      }
+
+      if prefix.contains(VendorPrefix::WebKit) {
+        dest.push(Property::WebKitMaskBoxImageSource(source.clone(), VendorPrefix::WebKit));
+      }
+
+      if prefix.contains(VendorPrefix::None) {
+        dest.push(Property::MaskBorderSource(source));
+        self.flushed_properties.insert(MaskProperty::MaskBorderSource);
+      }
+    }
+
+    macro_rules! prop {
+      ($val: expr, $prop: ident, $webkit: ident) => {
+        if let Some((val, mut prefix)) = $val {
+          prefix = context.targets.prefixes(prefix, Feature::$prop);
+          if prefix.contains(VendorPrefix::WebKit) {
+            dest.push(Property::$webkit(val.clone(), VendorPrefix::WebKit));
+          }
+
+          if prefix.contains(VendorPrefix::None) {
+            dest.push(Property::$prop(val));
+          }
+          self.flushed_properties.insert(MaskProperty::$prop);
+        }
+      };
+    }
+
+    prop!(slice, MaskBorderSlice, WebKitMaskBoxImageSlice);
+    prop!(width, MaskBorderWidth, WebKitMaskBoxImageWidth);
+    prop!(outset, MaskBorderOutset, WebKitMaskBoxImageOutset);
+    prop!(repeat, MaskBorderRepeat, WebKitMaskBoxImageRepeat);
+
+    if let Some(mode) = mode {
+      dest.push(Property::MaskBorderMode(mode));
+      self.flushed_properties.insert(MaskProperty::MaskBorderMode);
+    }
+  }
+}
+
+#[inline]
+fn is_mask_property(property_id: &PropertyId) -> bool {
+  match property_id {
+    PropertyId::MaskImage(_)
+    | PropertyId::MaskPosition(_)
+    | PropertyId::MaskSize(_)
+    | PropertyId::MaskRepeat(_)
+    | PropertyId::MaskClip(_)
+    | PropertyId::MaskOrigin(_)
+    | PropertyId::MaskComposite
+    | PropertyId::MaskMode
+    | PropertyId::Mask(_) => true,
+    _ => false,
+  }
+}
+
+#[inline]
+fn is_mask_border_property(property_id: &PropertyId) -> bool {
+  match property_id {
+    PropertyId::MaskBorderSource
+    | PropertyId::MaskBorderSlice
+    | PropertyId::MaskBorderWidth
+    | PropertyId::MaskBorderOutset
+    | PropertyId::MaskBorderRepeat
+    | PropertyId::MaskBorderMode
+    | PropertyId::MaskBorder => true,
+    _ => false,
+  }
+}
+
+#[inline]
+pub(crate) fn get_webkit_mask_property(property_id: &PropertyId) -> Option<PropertyId<'static>> {
+  Some(match property_id {
+    PropertyId::MaskBorderSource => PropertyId::WebKitMaskBoxImageSource(VendorPrefix::WebKit),
+    PropertyId::MaskBorderSlice => PropertyId::WebKitMaskBoxImageSlice(VendorPrefix::WebKit),
+    PropertyId::MaskBorderWidth => PropertyId::WebKitMaskBoxImageWidth(VendorPrefix::WebKit),
+    PropertyId::MaskBorderOutset => PropertyId::WebKitMaskBoxImageOutset(VendorPrefix::WebKit),
+    PropertyId::MaskBorderRepeat => PropertyId::WebKitMaskBoxImageRepeat(VendorPrefix::WebKit),
+    PropertyId::MaskBorder => PropertyId::WebKitMaskBoxImage(VendorPrefix::WebKit),
+    PropertyId::MaskComposite => PropertyId::WebKitMaskComposite,
+    PropertyId::MaskMode => PropertyId::WebKitMaskSourceType(VendorPrefix::WebKit),
+    _ => return None,
+  })
+}
diff --git a/src/properties/mod.rs b/src/properties/mod.rs
new file mode 100644
index 0000000..fb55277
--- /dev/null
+++ b/src/properties/mod.rs
@@ -0,0 +1,1689 @@
+//! CSS property values.
+//!
+//! Each property provides parsing and serialization support using the [Parse](super::traits::Parse)
+//! and [ToCss](super::traits::ToCss) traits. Properties are fully parsed as defined by the CSS spec,
+//! and printed in their canonical form. For example, most CSS properties are case-insensitive, and
+//! may be written in various orders, but when printed they are lower cased as appropriate and in a
+//! standard order.
+//!
+//! CSS properties often also contain many implicit values that are automatically filled in during
+//! parsing when omitted. These are also omitted when possible during serialization. Many properties
+//! also implement the [Default](std::default::Default) trait, which returns the initial value for the property.
+//!
+//! Shorthand properties are represented as structs containing fields for each of the sub-properties.
+//! If some of the sub-properties are not specified in the shorthand, their default values are filled in.
+//!
+//! The [Property](Property) enum contains the values of all properties, and can be used to parse property values by name.
+//! The [PropertyId](PropertyId) enum represents only property names, and not values and is used to refer to known properties.
+//!
+//! # Example
+//!
+//! This example shows how the `background` shorthand property is parsed and serialized. The `parse_string`
+//! function parses the background into a structure with all missing fields filled in with their default values.
+//! When printed using the `to_css_string` function, the components are in their canonical order, and default
+//! values are removed.
+//!
+//! ```
+//! use smallvec::smallvec;
+//! use lightningcss::{
+//!   properties::{Property, PropertyId, background::*},
+//!   values::{url::Url, image::Image, color::{CssColor, RGBA}, position::*, length::*},
+//!   stylesheet::{ParserOptions, PrinterOptions},
+//!   dependencies::Location,
+//! };
+//!
+//! let background = Property::parse_string(
+//!   PropertyId::from("background"),
+//!   "url('img.png') repeat fixed 20px 10px / 50px 100px",
+//!   ParserOptions::default()
+//! ).unwrap();
+//!
+//! assert_eq!(
+//!   background,
+//!   Property::Background(smallvec![Background {
+//!     image: Image::Url(Url {
+//!       url: "img.png".into(),
+//!       loc: Location { line: 1, column: 1 }
+//!     }),
+//!     color: CssColor::RGBA(RGBA {
+//!       red: 0,
+//!       green: 0,
+//!       blue: 0,
+//!       alpha: 0
+//!     }),
+//!     position: BackgroundPosition {
+//!       x: HorizontalPosition::Length(LengthPercentage::px(20.0)),
+//!       y: VerticalPosition::Length(LengthPercentage::px(10.0)),
+//!     },
+//!     repeat: BackgroundRepeat {
+//!       x: BackgroundRepeatKeyword::Repeat,
+//!       y: BackgroundRepeatKeyword::Repeat,
+//!     },
+//!     size: BackgroundSize::Explicit {
+//!       width: LengthPercentageOrAuto::LengthPercentage(LengthPercentage::px(50.0)),
+//!       height: LengthPercentageOrAuto::LengthPercentage(LengthPercentage::px(100.0)),
+//!     },
+//!     attachment: BackgroundAttachment::Fixed,
+//!     origin: BackgroundOrigin::PaddingBox,
+//!     clip: BackgroundClip::BorderBox,
+//!   }])
+//! );
+//!
+//! assert_eq!(
+//!   background.to_css_string(false, PrinterOptions::default()).unwrap(),
+//!   r#"background: url("img.png") 20px 10px / 50px 100px fixed"#
+//! );
+//! ```
+//!
+//! If you have a [cssparser::Parser](cssparser::Parser) already, you can also use the `parse` and `to_css`
+//! methods instead, rather than parsing from a string.
+//!
+//! # Unparsed and custom properties
+//!
+//! Custom and unknown properties are represented by the [CustomProperty](custom::CustomProperty) struct, and the
+//! `Property::Custom` variant. The value of these properties is not parsed, and is stored as a raw
+//! [TokenList](custom::TokenList), with the name as a string.
+//!
+//! If a known property is unable to be parsed, e.g. it contains `var()` references, then it is represented by the
+//! [UnparsedProperty](custom::UnparsedProperty) struct, and the `Property::Unparsed` variant. The value is stored
+//! as a raw [TokenList](custom::TokenList), with a [PropertyId](PropertyId) as the name.
+
+#![deny(missing_docs)]
+
+pub mod align;
+pub mod animation;
+pub mod background;
+pub mod border;
+pub mod border_image;
+pub mod border_radius;
+pub mod box_shadow;
+pub mod contain;
+pub mod css_modules;
+pub mod custom;
+pub mod display;
+pub mod effects;
+pub mod flex;
+pub mod font;
+pub mod grid;
+pub mod list;
+pub(crate) mod margin_padding;
+pub mod masking;
+pub mod outline;
+pub mod overflow;
+pub mod position;
+pub(crate) mod prefix_handler;
+pub mod size;
+pub mod svg;
+pub mod text;
+pub mod transform;
+pub mod transition;
+pub mod ui;
+
+use crate::declaration::DeclarationBlock;
+use crate::error::{ParserError, PrinterError};
+use crate::logical::{LogicalGroup, PropertyCategory};
+use crate::macros::enum_property;
+use crate::parser::starts_with_ignore_ascii_case;
+use crate::parser::ParserOptions;
+use crate::prefixes::Feature;
+use crate::printer::{Printer, PrinterOptions};
+use crate::targets::Targets;
+use crate::traits::{Parse, ParseWithOptions, Shorthand, ToCss};
+use crate::values::number::{CSSInteger, CSSNumber};
+use crate::values::string::CowArcStr;
+use crate::values::{
+  alpha::*, color::*, easing::EasingFunction, ident::DashedIdentReference, ident::NoneOrCustomIdentList, image::*,
+  length::*, position::*, rect::*, shape::FillRule, size::Size2D, time::Time,
+};
+use crate::vendor_prefix::VendorPrefix;
+#[cfg(feature = "visitor")]
+use crate::visitor::Visit;
+use align::*;
+use animation::*;
+use background::*;
+use border::*;
+use border_image::*;
+use border_radius::*;
+use box_shadow::*;
+use contain::*;
+use css_modules::*;
+use cssparser::*;
+use custom::*;
+use display::*;
+use effects::*;
+use flex::*;
+use font::*;
+use grid::*;
+use list::*;
+use margin_padding::*;
+use masking::*;
+use outline::*;
+use overflow::*;
+use size::*;
+use smallvec::{smallvec, SmallVec};
+#[cfg(feature = "into_owned")]
+use static_self::IntoOwned;
+use svg::*;
+use text::*;
+use transform::*;
+use transition::*;
+use ui::*;
+
+macro_rules! define_properties {
+  (
+    $(
+      $(#[$meta: meta])*
+      $name: literal: $property: ident($type: ty $(, $vp: ty)?) $( / $prefix: ident )* $( unprefixed: $unprefixed: literal )? $( options: $options: literal )? $( shorthand: $shorthand: literal )? $( [ logical_group: $logical_group: ident, category: $logical_category: ident ] )? $( if $condition: ident )?,
+    )+
+  ) => {
+    /// A CSS property id.
+    #[derive(Debug, Clone, PartialEq, Eq, Hash)]
+    #[cfg_attr(feature = "visitor", derive(Visit))]
+    #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+    pub enum PropertyId<'i> {
+      $(
+        #[doc=concat!("The `", $name, "` property.")]
+        $(#[$meta])*
+        $property$(($vp))?,
+      )+
+      /// The `all` property.
+      All,
+      /// An unknown or custom property name.
+      Custom(CustomPropertyName<'i>)
+    }
+
+    macro_rules! vp_name {
+      ($x: ty, $n: ident) => {
+        $n
+      };
+      ($x: ty, $n: expr) => {
+        $n
+      };
+    }
+
+    macro_rules! get_allowed_prefixes {
+      ($v: literal) => {
+        VendorPrefix::empty()
+      };
+      () => {
+        VendorPrefix::None
+      };
+    }
+
+    impl<'i> From<CowArcStr<'i>> for PropertyId<'i> {
+      fn from(name: CowArcStr<'i>) -> PropertyId<'i> {
+        let name_ref = name.as_ref();
+        let (prefix, name_ref) = if starts_with_ignore_ascii_case(name_ref, "-webkit-") {
+          (VendorPrefix::WebKit, &name_ref[8..])
+        } else if starts_with_ignore_ascii_case(name_ref, "-moz-") {
+          (VendorPrefix::Moz, &name_ref[5..])
+        } else if starts_with_ignore_ascii_case(name_ref, "-o-") {
+          (VendorPrefix::O, &name_ref[3..])
+        } else if starts_with_ignore_ascii_case(name_ref, "-ms-") {
+          (VendorPrefix::Ms, &name_ref[4..])
+        } else {
+          (VendorPrefix::None, name_ref)
+        };
+
+        Self::from_name_and_prefix(name_ref, prefix)
+          .unwrap_or_else(|_| PropertyId::Custom(name.into()))
+      }
+    }
+
+    impl<'i> From<&'i str> for PropertyId<'i> {
+      #[inline]
+      fn from(name: &'i str) -> PropertyId<'i> {
+        PropertyId::from(CowArcStr::from(name))
+      }
+    }
+
+    impl<'i> Parse<'i> for PropertyId<'i> {
+      fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+        let name = input.expect_ident()?;
+        Ok(CowArcStr::from(name).into())
+      }
+    }
+
+    impl<'i> ToCss for PropertyId<'i> {
+      fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError> where W: std::fmt::Write {
+        let mut first = true;
+        macro_rules! delim {
+          () => {
+            #[allow(unused_assignments)]
+            if first {
+              first = false;
+            } else {
+              dest.delim(',', false)?;
+            }
+          };
+        }
+
+        let name = self.name();
+        for p in self.prefix().or_none() {
+          delim!();
+          p.to_css(dest)?;
+          dest.write_str(name)?;
+        }
+
+        Ok(())
+      }
+    }
+
+    impl<'i> PropertyId<'i> {
+      fn from_name_and_prefix(name: &str, prefix: VendorPrefix) -> Result<Self, ()> {
+        match_ignore_ascii_case! { name.as_ref(),
+          $(
+            $(#[$meta])*
+            $name => {
+              macro_rules! get_propertyid {
+                ($v: ty) => {
+                  PropertyId::$property(prefix)
+                };
+                () => {
+                  PropertyId::$property
+                };
+              }
+
+              let allowed_prefixes = get_allowed_prefixes!($($unprefixed)?) $(| VendorPrefix::$prefix)*;
+              if allowed_prefixes.contains(prefix) {
+                return Ok(get_propertyid!($($vp)?))
+              }
+            },
+          )+
+          "all" => return Ok(PropertyId::All),
+          _ => {}
+        }
+
+        Err(())
+      }
+
+      /// Returns the vendor prefix for this property id.
+      pub fn prefix(&self) -> VendorPrefix {
+        use PropertyId::*;
+        match self {
+          $(
+            $(#[$meta])*
+            $property$((vp_name!($vp, prefix)))? => {
+              $(
+                macro_rules! return_prefix {
+                  ($v: ty) => {
+                    return *prefix;
+                  };
+                }
+
+                return_prefix!($vp);
+              )?
+              #[allow(unreachable_code)]
+              VendorPrefix::empty()
+            },
+          )+
+          _ => VendorPrefix::empty()
+        }
+      }
+
+      pub(crate) fn with_prefix(&self, prefix: VendorPrefix) -> PropertyId<'i> {
+        use PropertyId::*;
+        match self {
+          $(
+            $(#[$meta])*
+            $property$((vp_name!($vp, _p)))? => {
+              macro_rules! get_prefixed {
+                ($v: ty) => {
+                  PropertyId::$property(prefix)
+                };
+                () => {
+                  PropertyId::$property
+                }
+              }
+
+              get_prefixed!($($vp)?)
+            },
+          )+
+          _ => self.clone()
+        }
+      }
+
+      pub(crate) fn add_prefix(&mut self, prefix: VendorPrefix) {
+        use PropertyId::*;
+        match self {
+          $(
+            $(#[$meta])*
+            $property$((vp_name!($vp, p)))? => {
+              macro_rules! get_prefixed {
+                ($v: ty) => {{
+                  *p |= prefix;
+                }};
+                () => {{}};
+              }
+
+              get_prefixed!($($vp)?)
+            },
+          )+
+          _ => {}
+        }
+      }
+
+      pub(crate) fn set_prefixes_for_targets(&mut self, targets: Targets) {
+        match self {
+          $(
+            $(#[$meta])*
+            #[allow(unused_variables)]
+            PropertyId::$property$((vp_name!($vp, prefix)))? => {
+              macro_rules! get_prefixed {
+                ($v: ty, $u: literal) => {};
+                ($v: ty) => {{
+                  *prefix = targets.prefixes(*prefix, Feature::$property);
+                }};
+                () => {};
+              }
+
+              get_prefixed!($($vp)? $(, $unprefixed)?);
+            },
+          )+
+          _ => {}
+        }
+      }
+
+      /// Returns the property name, without any vendor prefixes.
+      pub fn name(&self) -> &str {
+        use PropertyId::*;
+
+        match self {
+          $(
+            $(#[$meta])*
+            $property$((vp_name!($vp, _p)))? => $name,
+          )+
+          All => "all",
+          Custom(name) => name.as_ref()
+        }
+      }
+
+      /// Returns whether a property is a shorthand.
+      pub fn is_shorthand(&self) -> bool {
+        $(
+          macro_rules! shorthand {
+            ($s: literal) => {
+              if let PropertyId::$property$((vp_name!($vp, _prefix)))? = self {
+                return true
+              }
+            };
+            () => {}
+          }
+
+          shorthand!($($shorthand)?);
+        )+
+
+        false
+      }
+
+      /// Returns a shorthand value for this property id from the given declaration block.
+      pub(crate) fn shorthand_value<'a>(&self, decls: &DeclarationBlock<'a>) -> Option<(Property<'a>, bool)> {
+        // Inline function to remap lifetime names.
+        #[inline]
+        fn shorthand_value<'a, 'i>(property_id: &PropertyId<'a>, decls: &DeclarationBlock<'i>) -> Option<(Property<'i>, bool)> {
+          $(
+            #[allow(unused_macros)]
+            macro_rules! prefix {
+              ($v: ty, $p: ident) => {
+                *$p
+              };
+              ($p: ident) => {
+                VendorPrefix::None
+              };
+            }
+
+            macro_rules! shorthand {
+              ($s: literal) => {
+                if let PropertyId::$property$((vp_name!($vp, prefix)))? = &property_id {
+                  if let Some((val, important)) = <$type>::from_longhands(decls, prefix!($($vp,)? prefix)) {
+                    return Some((Property::$property(val $(, *vp_name!($vp, prefix))?), important))
+                  }
+                }
+              };
+              () => {}
+            }
+
+            shorthand!($($shorthand)?);
+          )+
+
+          None
+        }
+
+        shorthand_value(self, decls)
+      }
+
+      /// Returns a list of longhand property ids for a shorthand.
+      pub fn longhands(&self) -> Option<Vec<PropertyId<'static>>> {
+        macro_rules! prefix_default {
+          ($x: ty, $p: ident) => {
+            *$p
+          };
+          () => {
+            VendorPrefix::None
+          };
+        }
+
+        $(
+          macro_rules! shorthand {
+            ($s: literal) => {
+              if let PropertyId::$property$((vp_name!($vp, prefix)))? = self {
+                return Some(<$type>::longhands(prefix_default!($($vp, prefix)?)));
+              }
+            };
+            () => {}
+          }
+
+          shorthand!($($shorthand)?);
+        )+
+
+        None
+      }
+
+      /// Returns the logical property group for this property.
+      pub(crate) fn logical_group(&self) -> Option<LogicalGroup> {
+        $(
+          macro_rules! group {
+            ($g: ident) => {
+              if let PropertyId::$property$((vp_name!($vp, _prefix)))? = self {
+                return Some(LogicalGroup::$g)
+              }
+            };
+            () => {}
+          }
+
+          group!($($logical_group)?);
+        )+
+
+        None
+      }
+
+      /// Returns whether the property is logical or physical.
+      pub(crate) fn category(&self) -> Option<PropertyCategory> {
+        $(
+          macro_rules! category {
+            ($c: ident) => {
+              if let PropertyId::$property$((vp_name!($vp, _prefix)))? = self {
+                return Some(PropertyCategory::$c)
+              }
+            };
+            () => {}
+          }
+
+          category!($($logical_category)?);
+        )+
+
+        None
+      }
+    }
+
+    #[cfg(feature = "serde")]
+    #[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
+    impl<'i> serde::Serialize for PropertyId<'i> {
+      fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+      where
+        S: serde::Serializer,
+      {
+        use serde::ser::SerializeStruct;
+
+        let name = self.name();
+        let prefix = self.prefix();
+
+        if prefix.is_empty() {
+          let mut s = serializer.serialize_struct("PropertyId", 1)?;
+          s.serialize_field("property", name)?;
+          s.end()
+        } else {
+          let mut s = serializer.serialize_struct("PropertyId", 2)?;
+          s.serialize_field("property", name)?;
+          s.serialize_field("vendor_prefix", &prefix)?;
+          s.end()
+        }
+      }
+    }
+
+    #[cfg(feature = "serde")]
+    #[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
+    impl<'i, 'de: 'i> serde::Deserialize<'de> for PropertyId<'i> {
+      fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+      where
+        D: serde::Deserializer<'de>,
+      {
+        #[derive(serde::Deserialize)]
+        #[serde(field_identifier, rename_all = "snake_case")]
+        enum Field {
+          Property,
+          VendorPrefix
+        }
+
+        struct PropertyIdVisitor;
+        impl<'de> serde::de::Visitor<'de> for PropertyIdVisitor {
+          type Value = PropertyId<'de>;
+
+          fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
+            formatter.write_str("a PropertyId")
+          }
+
+          fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
+          where
+            A: serde::de::MapAccess<'de>,
+          {
+            let mut property: Option<CowArcStr> = None;
+            let mut vendor_prefix = None;
+            while let Some(key) = map.next_key()? {
+              match key {
+                Field::Property => {
+                  property = Some(map.next_value()?);
+                }
+                Field::VendorPrefix => {
+                  vendor_prefix = Some(map.next_value()?);
+                }
+              }
+            }
+
+            let property = property.ok_or_else(|| serde::de::Error::missing_field("property"))?;
+            let vendor_prefix = vendor_prefix.unwrap_or(VendorPrefix::None);
+            let property_id = PropertyId::from_name_and_prefix(property.as_ref(), vendor_prefix)
+              .unwrap_or_else(|_| PropertyId::Custom(property.into()));
+            Ok(property_id)
+          }
+        }
+
+        deserializer.deserialize_any(PropertyIdVisitor)
+      }
+    }
+
+    #[cfg(feature = "jsonschema")]
+    #[cfg_attr(docsrs, doc(cfg(feature = "jsonschema")))]
+    impl<'i> schemars::JsonSchema for PropertyId<'i> {
+      fn is_referenceable() -> bool {
+        true
+      }
+
+      fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
+        macro_rules! property {
+          ($n: literal) => {
+            fn property(_: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
+              schemars::schema::Schema::Object(schemars::schema::SchemaObject {
+                instance_type: Some(schemars::schema::InstanceType::String.into()),
+                enum_values: Some(vec![$n.into()]),
+                ..Default::default()
+              })
+            }
+          }
+        }
+
+        schemars::schema::Schema::Object(schemars::schema::SchemaObject {
+          subschemas: Some(Box::new(schemars::schema::SubschemaValidation {
+            one_of: Some(vec![
+              $(
+                {
+                  property!($name);
+
+                  macro_rules! with_prefix {
+                    ($v: ty) => {{
+                      #[derive(schemars::JsonSchema)]
+                      struct T<'i> {
+                        #[schemars(rename = "property", schema_with = "property")]
+                        _property: &'i u8,
+                        #[schemars(rename = "vendorPrefix")]
+                        _vendor_prefix: VendorPrefix,
+                      }
+
+                      T::json_schema(gen)
+                    }};
+                    () => {{
+                      #[derive(schemars::JsonSchema)]
+                      struct T<'i> {
+                        #[schemars(rename = "property", schema_with = "property")]
+                        _property: &'i u8,
+                      }
+
+                      T::json_schema(gen)
+                    }};
+                  }
+
+                  with_prefix!($($vp)?)
+                },
+              )+
+              {
+                property!("all");
+
+                #[derive(schemars::JsonSchema)]
+                struct T<'i> {
+                  #[schemars(rename = "property", schema_with = "property")]
+                  _property: &'i u8,
+                }
+
+                T::json_schema(gen)
+              },
+              {
+                #[derive(schemars::JsonSchema)]
+                struct T {
+                  #[schemars(rename = "property")]
+                  _property: String,
+                }
+
+                T::json_schema(gen)
+              }
+            ]),
+            ..Default::default()
+          })),
+          ..Default::default()
+        })
+      }
+
+      fn schema_name() -> String {
+        "PropertyId".into()
+      }
+    }
+
+    /// A CSS property.
+    #[derive(Debug, Clone, PartialEq)]
+    #[cfg_attr(feature = "visitor", derive(Visit), visit(visit_property, PROPERTIES))]
+    #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+    pub enum Property<'i> {
+      $(
+        #[doc=concat!("The `", $name, "` property.")]
+        $(#[$meta])*
+        $property($type, $($vp)?),
+      )+
+      /// The [all](https://drafts.csswg.org/css-cascade-5/#all-shorthand) shorthand property.
+      All(CSSWideKeyword),
+      /// An unparsed property.
+      Unparsed(UnparsedProperty<'i>),
+      /// A custom or unknown property.
+      Custom(CustomProperty<'i>),
+    }
+
+    impl<'i> Property<'i> {
+      /// Parses a CSS property by name.
+      pub fn parse<'t>(property_id: PropertyId<'i>, input: &mut Parser<'i, 't>, options: &ParserOptions<'_, 'i>) -> Result<Property<'i>, ParseError<'i, ParserError<'i>>> {
+        let state = input.state();
+
+        match property_id {
+          $(
+            $(#[$meta])*
+            PropertyId::$property$((vp_name!($vp, prefix)))? $(if options.$condition.is_some())? => {
+              if let Ok(c) = <$type>::parse_with_options(input, options) {
+                if input.expect_exhausted().is_ok() {
+                  return Ok(Property::$property(c $(, vp_name!($vp, prefix))?))
+                }
+              }
+            },
+          )+
+          PropertyId::All => return Ok(Property::All(CSSWideKeyword::parse(input)?)),
+          PropertyId::Custom(name) => return Ok(Property::Custom(CustomProperty::parse(name, input, options)?)),
+          _ => {}
+        };
+
+        // If a value was unable to be parsed, treat as an unparsed property.
+        // This is different from a custom property, handled below, in that the property name is known
+        // and stored as an enum rather than a string. This lets property handlers more easily deal with it.
+        // Ideally we'd only do this if var() or env() references were seen, but err on the safe side for now.
+        input.reset(&state);
+        return Ok(Property::Unparsed(UnparsedProperty::parse(property_id, input, options)?))
+      }
+
+      /// Returns the property id for this property.
+      pub fn property_id(&self) -> PropertyId<'i> {
+        use Property::*;
+
+        match self {
+          $(
+            $(#[$meta])*
+            $property(_, $(vp_name!($vp, p))?) => PropertyId::$property$((*vp_name!($vp, p)))?,
+          )+
+          All(_) => PropertyId::All,
+          Unparsed(unparsed) => unparsed.property_id.clone(),
+          Custom(custom) => PropertyId::Custom(custom.name.clone())
+        }
+      }
+
+      /// Parses a CSS property from a string.
+      pub fn parse_string(property_id: PropertyId<'i>, input: &'i str, options: ParserOptions<'_, 'i>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+        let mut input = ParserInput::new(input);
+        let mut parser = Parser::new(&mut input);
+        Self::parse(property_id, &mut parser, &options)
+      }
+
+      /// Sets the vendor prefixes for this property.
+      ///
+      /// If the property doesn't support vendor prefixes, this function does nothing.
+      /// If vendor prefixes are set which do not exist for the property, they are ignored
+      /// and only the valid prefixes are set.
+      pub fn set_prefix(&mut self, prefix: VendorPrefix) {
+        use Property::*;
+        match self {
+          $(
+            $(#[$meta])*
+            $property(_, $(vp_name!($vp, p))?) => {
+              macro_rules! set {
+                ($v: ty) => {
+                  *p = (prefix & (get_allowed_prefixes!($($unprefixed)?) $(| VendorPrefix::$prefix)*)).or(*p);
+                };
+                () => {};
+              }
+
+              set!($($vp)?);
+            },
+          )+
+          _ => {}
+        }
+      }
+
+      /// Serializes the value of a CSS property without its name or `!important` flag.
+      pub fn value_to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError> where W: std::fmt::Write {
+        use Property::*;
+
+        match self {
+          $(
+            $(#[$meta])*
+            $property(val, $(vp_name!($vp, _p))?) => {
+              val.to_css(dest)
+            }
+          )+
+          All(keyword) => keyword.to_css(dest),
+          Unparsed(unparsed) => {
+            unparsed.value.to_css(dest, false)
+          }
+          Custom(custom) => {
+            custom.value.to_css(dest, matches!(custom.name, CustomPropertyName::Custom(..)))
+          }
+        }
+      }
+
+      /// Serializes the value of a CSS property as a string.
+      pub fn value_to_css_string(&self, options: PrinterOptions) -> Result<String, PrinterError> {
+        let mut s = String::new();
+        let mut printer = Printer::new(&mut s, options);
+        self.value_to_css(&mut printer)?;
+        Ok(s)
+      }
+
+      /// Serializes the CSS property, with an optional `!important` flag.
+      pub fn to_css<W>(&self, dest: &mut Printer<W>, important: bool) -> Result<(), PrinterError> where W: std::fmt::Write {
+        use Property::*;
+
+        let mut first = true;
+        macro_rules! start {
+          () => {
+            #[allow(unused_assignments)]
+            if first {
+              first = false;
+            } else {
+              dest.write_char(';')?;
+              dest.newline()?;
+            }
+          };
+        }
+
+        macro_rules! write_important {
+          () => {
+            if important {
+              dest.whitespace()?;
+              dest.write_str("!important")?;
+            }
+          }
+        }
+
+        let (name, prefix) = match self {
+          $(
+            $(#[$meta])*
+            $property(_, $(vp_name!($vp, prefix))?) => {
+              macro_rules! get_prefix {
+                ($v: ty) => {
+                  *prefix
+                };
+                () => {
+                  VendorPrefix::None
+                };
+              }
+
+              ($name, get_prefix!($($vp)?))
+            },
+          )+
+          All(_) => ("all", VendorPrefix::None),
+          Unparsed(unparsed) => {
+            let mut prefix = unparsed.property_id.prefix();
+            if prefix.is_empty() {
+              prefix = VendorPrefix::None;
+            }
+            (unparsed.property_id.name(), prefix)
+          },
+          Custom(custom) => {
+            custom.name.to_css(dest)?;
+            dest.delim(':', false)?;
+            self.value_to_css(dest)?;
+            write_important!();
+            return Ok(())
+          }
+        };
+        for p in prefix {
+          start!();
+          p.to_css(dest)?;
+          dest.write_str(name)?;
+          dest.delim(':', false)?;
+          self.value_to_css(dest)?;
+          write_important!();
+        }
+        Ok(())
+      }
+
+      /// Serializes the CSS property to a string, with an optional `!important` flag.
+      pub fn to_css_string(&self, important: bool, options: PrinterOptions) -> Result<String, PrinterError> {
+        let mut s = String::new();
+        let mut printer = Printer::new(&mut s, options);
+        self.to_css(&mut printer, important)?;
+        Ok(s)
+      }
+
+      /// Returns the given longhand property for a shorthand.
+      pub fn longhand(&self, property_id: &PropertyId) -> Option<Property<'i>> {
+        $(
+          macro_rules! shorthand {
+            ($s: literal) => {
+              if let Property::$property(val $(, vp_name!($vp, prefix))?) = self {
+                $(
+                  if *vp_name!($vp, prefix) != property_id.prefix() {
+                    return None
+                  }
+                )?
+                return val.longhand(property_id)
+              }
+            };
+            () => {}
+          }
+
+          shorthand!($($shorthand)?);
+        )+
+
+        None
+      }
+
+      /// Updates this shorthand from a longhand property.
+      pub fn set_longhand(&mut self, property: &Property<'i>) -> Result<(), ()> {
+        $(
+          macro_rules! shorthand {
+            ($s: literal) => {
+              if let Property::$property(val $(, vp_name!($vp, prefix))?) = self {
+                $(
+                  if *vp_name!($vp, prefix) != property.property_id().prefix() {
+                    return Err(())
+                  }
+                )?
+                return val.set_longhand(property)
+              }
+            };
+            () => {}
+          }
+
+          shorthand!($($shorthand)?);
+        )+
+        Err(())
+      }
+    }
+
+    #[cfg(feature = "serde")]
+    #[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
+    impl<'i> serde::Serialize for Property<'i> {
+      fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+      where
+        S: serde::Serializer,
+      {
+        use serde::ser::SerializeStruct;
+        use Property::*;
+
+        match self {
+          Unparsed(unparsed) => {
+            let mut s = serializer.serialize_struct("Property", 2)?;
+            s.serialize_field("property", "unparsed")?;
+            s.serialize_field("value", unparsed)?;
+            return s.end()
+          }
+          Custom(unparsed) => {
+            let mut s = serializer.serialize_struct("Property", 2)?;
+            s.serialize_field("property", "custom")?;
+            s.serialize_field("value", unparsed)?;
+            return s.end()
+          }
+          _ => {}
+        }
+
+        let id = self.property_id();
+        let name = id.name();
+        let prefix = id.prefix();
+
+        let mut s = if prefix.is_empty() {
+          let mut s = serializer.serialize_struct("Property", 2)?;
+          s.serialize_field("property", name)?;
+          s
+        } else {
+          let mut s = serializer.serialize_struct("Property", 3)?;
+          s.serialize_field("property", name)?;
+          s.serialize_field("vendorPrefix", &prefix)?;
+          s
+        };
+
+        match self {
+          $(
+            $(#[$meta])*
+            $property(value, $(vp_name!($vp, _p))?) => {
+              s.serialize_field("value", value)?;
+            }
+          )+
+          All(value) => {
+            s.serialize_field("value", value)?;
+          }
+          Unparsed(_) | Custom(_) => unreachable!()
+        }
+
+        s.end()
+      }
+    }
+
+    #[cfg(feature = "serde")]
+    #[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
+    impl<'i, 'de: 'i> serde::Deserialize<'de> for Property<'i> {
+      fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+      where
+        D: serde::Deserializer<'de>,
+      {
+        enum ContentOrRaw<'de> {
+          Content(serde::__private::de::Content<'de>),
+          Raw(CowArcStr<'de>)
+        }
+
+        struct PartialProperty<'de> {
+          property_id: PropertyId<'de>,
+          value: ContentOrRaw<'de>,
+        }
+
+        #[derive(serde::Deserialize)]
+        #[serde(field_identifier, rename_all = "camelCase")]
+        enum Field {
+          Property,
+          VendorPrefix,
+          Value,
+          Raw
+        }
+
+        struct PropertyIdVisitor;
+        impl<'de> serde::de::Visitor<'de> for PropertyIdVisitor {
+          type Value = PartialProperty<'de>;
+
+          fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
+            formatter.write_str("a Property")
+          }
+
+          fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
+          where
+            A: serde::de::MapAccess<'de>,
+          {
+            let mut property: Option<CowArcStr> = None;
+            let mut vendor_prefix = None;
+            let mut value: Option<ContentOrRaw<'de>> = None;
+            while let Some(key) = map.next_key()? {
+              match key {
+                Field::Property => {
+                  property = Some(map.next_value()?);
+                }
+                Field::VendorPrefix => {
+                  vendor_prefix = Some(map.next_value()?);
+                }
+                Field::Value => {
+                  value = Some(ContentOrRaw::Content(map.next_value()?));
+                }
+                Field::Raw => {
+                  value = Some(ContentOrRaw::Raw(map.next_value()?));
+                }
+              }
+            }
+
+            let property = property.ok_or_else(|| serde::de::Error::missing_field("property"))?;
+            let vendor_prefix = vendor_prefix.unwrap_or(VendorPrefix::None);
+            let value = value.ok_or_else(|| serde::de::Error::missing_field("value"))?;
+            let property_id = PropertyId::from_name_and_prefix(property.as_ref(), vendor_prefix)
+              .unwrap_or_else(|_| PropertyId::from(property));
+            Ok(PartialProperty {
+              property_id,
+              value,
+            })
+          }
+        }
+
+        let partial = deserializer.deserialize_any(PropertyIdVisitor)?;
+
+        let content = match partial.value {
+          ContentOrRaw::Raw(raw) => {
+            let res = Property::parse_string(partial.property_id, raw.as_ref(), ParserOptions::default())
+              .map_err(|_| serde::de::Error::custom("Could not parse value"))?;
+            return Ok(res.into_owned())
+          }
+          ContentOrRaw::Content(content) => content
+        };
+
+        let deserializer = serde::__private::de::ContentDeserializer::new(content);
+        match partial.property_id {
+          $(
+            $(#[$meta])*
+            PropertyId::$property$((vp_name!($vp, prefix)))? => {
+              let value = <$type>::deserialize(deserializer)?;
+              Ok(Property::$property(value $(, vp_name!($vp, prefix))?))
+            },
+          )+
+          PropertyId::Custom(name) => {
+            if name.as_ref() == "unparsed" {
+              let value = UnparsedProperty::deserialize(deserializer)?;
+              Ok(Property::Unparsed(value))
+            } else {
+              let value = CustomProperty::deserialize(deserializer)?;
+              Ok(Property::Custom(value))
+            }
+          }
+          PropertyId::All => {
+            let value = CSSWideKeyword::deserialize(deserializer)?;
+            Ok(Property::All(value))
+          }
+        }
+      }
+    }
+
+    #[cfg(feature = "jsonschema")]
+    #[cfg_attr(docsrs, doc(cfg(feature = "jsonschema")))]
+    impl<'i> schemars::JsonSchema for Property<'i> {
+      fn is_referenceable() -> bool {
+        true
+      }
+
+      fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
+        macro_rules! property {
+          ($n: literal) => {
+            fn property(_: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
+              schemars::schema::Schema::Object(schemars::schema::SchemaObject {
+                instance_type: Some(schemars::schema::InstanceType::String.into()),
+                enum_values: Some(vec![$n.into()]),
+                ..Default::default()
+              })
+            }
+          }
+        }
+
+        schemars::schema::Schema::Object(schemars::schema::SchemaObject {
+          subschemas: Some(Box::new(schemars::schema::SubschemaValidation {
+            one_of: Some(vec![
+              $(
+                {
+                  property!($name);
+
+                  macro_rules! with_prefix {
+                    ($v: ty) => {{
+                      #[derive(schemars::JsonSchema)]
+                      struct T<'i> {
+                        #[schemars(rename = "property", schema_with = "property")]
+                        _property: &'i u8,
+                        #[schemars(rename = "vendorPrefix")]
+                        _vendor_prefix: VendorPrefix,
+                        #[schemars(rename = "value")]
+                        _value: $type,
+                      }
+
+                      T::json_schema(gen)
+                    }};
+                    () => {{
+                      #[derive(schemars::JsonSchema)]
+                      struct T<'i> {
+                        #[schemars(rename = "property", schema_with = "property")]
+                        _property: &'i u8,
+                        #[schemars(rename = "value")]
+                        _value: $type,
+                      }
+
+                      T::json_schema(gen)
+                    }};
+                  }
+
+                  with_prefix!($($vp)?)
+                },
+              )+
+              {
+                property!("all");
+                #[derive(schemars::JsonSchema)]
+                struct T {
+                  #[schemars(rename = "property", schema_with = "property")]
+                  _property: u8,
+                  #[schemars(rename = "value")]
+                  _value: CSSWideKeyword
+                }
+                T::json_schema(gen)
+              },
+              {
+                property!("unparsed");
+
+                #[derive(schemars::JsonSchema)]
+                struct T<'i> {
+                  #[schemars(rename = "property", schema_with = "property")]
+                  _property: &'i u8,
+                  #[schemars(rename = "value")]
+                  _value: UnparsedProperty<'i>,
+                }
+
+                T::json_schema(gen)
+              },
+              {
+                property!("custom");
+
+                #[derive(schemars::JsonSchema)]
+                struct T<'i> {
+                  #[schemars(rename = "property", schema_with = "property")]
+                  _property: &'i u8,
+                  #[schemars(rename = "value")]
+                  _value: CustomProperty<'i>,
+                }
+
+                T::json_schema(gen)
+              }
+            ]),
+            ..Default::default()
+          })),
+          ..Default::default()
+        })
+      }
+
+      fn schema_name() -> String {
+        "Declaration".into()
+      }
+    }
+  };
+}
+
+define_properties! {
+  "background-color": BackgroundColor(CssColor),
+  "background-image": BackgroundImage(SmallVec<[Image<'i>; 1]>),
+  "background-position-x": BackgroundPositionX(SmallVec<[HorizontalPosition; 1]>),
+  "background-position-y": BackgroundPositionY(SmallVec<[VerticalPosition; 1]>),
+  "background-position": BackgroundPosition(SmallVec<[BackgroundPosition; 1]>) shorthand: true,
+  "background-size": BackgroundSize(SmallVec<[BackgroundSize; 1]>),
+  "background-repeat": BackgroundRepeat(SmallVec<[BackgroundRepeat; 1]>),
+  "background-attachment": BackgroundAttachment(SmallVec<[BackgroundAttachment; 1]>),
+  "background-clip": BackgroundClip(SmallVec<[BackgroundClip; 1]>, VendorPrefix) / WebKit / Moz,
+  "background-origin": BackgroundOrigin(SmallVec<[BackgroundOrigin; 1]>),
+  "background": Background(SmallVec<[Background<'i>; 1]>) shorthand: true,
+
+  "box-shadow": BoxShadow(SmallVec<[BoxShadow; 1]>, VendorPrefix) / WebKit / Moz,
+  "opacity": Opacity(AlphaValue),
+  "color": Color(CssColor),
+  "display": Display(Display),
+  "visibility": Visibility(Visibility),
+
+  "width": Width(Size) [logical_group: Size, category: Physical],
+  "height": Height(Size) [logical_group: Size, category: Physical],
+  "min-width": MinWidth(Size) [logical_group: MinSize, category: Physical],
+  "min-height": MinHeight(Size) [logical_group: MinSize, category: Physical],
+  "max-width": MaxWidth(MaxSize) [logical_group: MaxSize, category: Physical],
+  "max-height": MaxHeight(MaxSize) [logical_group: MaxSize, category: Physical],
+  "block-size": BlockSize(Size) [logical_group: Size, category: Logical],
+  "inline-size": InlineSize(Size) [logical_group: Size, category: Logical],
+  "min-block-size": MinBlockSize(Size) [logical_group: MinSize, category: Logical],
+  "min-inline-size": MinInlineSize(Size) [logical_group: MinSize, category: Logical],
+  "max-block-size": MaxBlockSize(MaxSize) [logical_group: MaxSize, category: Logical],
+  "max-inline-size": MaxInlineSize(MaxSize) [logical_group: MaxSize, category: Logical],
+  "box-sizing": BoxSizing(BoxSizing, VendorPrefix) / WebKit / Moz,
+  "aspect-ratio": AspectRatio(AspectRatio),
+
+  "overflow": Overflow(Overflow) shorthand: true,
+  "overflow-x": OverflowX(OverflowKeyword),
+  "overflow-y": OverflowY(OverflowKeyword),
+  "text-overflow": TextOverflow(TextOverflow, VendorPrefix) / O,
+
+  // https://www.w3.org/TR/2020/WD-css-position-3-20200519
+  "position": Position(position::Position),
+  "top": Top(LengthPercentageOrAuto) [logical_group: Inset, category: Physical],
+  "bottom": Bottom(LengthPercentageOrAuto) [logical_group: Inset, category: Physical],
+  "left": Left(LengthPercentageOrAuto) [logical_group: Inset, category: Physical],
+  "right": Right(LengthPercentageOrAuto) [logical_group: Inset, category: Physical],
+  "inset-block-start": InsetBlockStart(LengthPercentageOrAuto) [logical_group: Inset, category: Logical],
+  "inset-block-end": InsetBlockEnd(LengthPercentageOrAuto) [logical_group: Inset, category: Logical],
+  "inset-inline-start": InsetInlineStart(LengthPercentageOrAuto) [logical_group: Inset, category: Logical],
+  "inset-inline-end": InsetInlineEnd(LengthPercentageOrAuto) [logical_group: Inset, category: Logical],
+  "inset-block": InsetBlock(InsetBlock) shorthand: true,
+  "inset-inline": InsetInline(InsetInline) shorthand: true,
+  "inset": Inset(Inset) shorthand: true,
+
+  "border-spacing": BorderSpacing(Size2D<Length>),
+
+  "border-top-color": BorderTopColor(CssColor) [logical_group: BorderColor, category: Physical],
+  "border-bottom-color": BorderBottomColor(CssColor) [logical_group: BorderColor, category: Physical],
+  "border-left-color": BorderLeftColor(CssColor) [logical_group: BorderColor, category: Physical],
+  "border-right-color": BorderRightColor(CssColor) [logical_group: BorderColor, category: Physical],
+  "border-block-start-color": BorderBlockStartColor(CssColor) [logical_group: BorderColor, category: Logical],
+  "border-block-end-color": BorderBlockEndColor(CssColor) [logical_group: BorderColor, category: Logical],
+  "border-inline-start-color": BorderInlineStartColor(CssColor) [logical_group: BorderColor, category: Logical],
+  "border-inline-end-color": BorderInlineEndColor(CssColor) [logical_group: BorderColor, category: Logical],
+
+  "border-top-style": BorderTopStyle(LineStyle) [logical_group: BorderStyle, category: Physical],
+  "border-bottom-style": BorderBottomStyle(LineStyle) [logical_group: BorderStyle, category: Physical],
+  "border-left-style": BorderLeftStyle(LineStyle) [logical_group: BorderStyle, category: Physical],
+  "border-right-style": BorderRightStyle(LineStyle) [logical_group: BorderStyle, category: Physical],
+  "border-block-start-style": BorderBlockStartStyle(LineStyle) [logical_group: BorderStyle, category: Logical],
+  "border-block-end-style": BorderBlockEndStyle(LineStyle) [logical_group: BorderStyle, category: Logical],
+  "border-inline-start-style": BorderInlineStartStyle(LineStyle) [logical_group: BorderStyle, category: Logical],
+  "border-inline-end-style": BorderInlineEndStyle(LineStyle) [logical_group: BorderStyle, category: Logical],
+
+  "border-top-width": BorderTopWidth(BorderSideWidth) [logical_group: BorderWidth, category: Physical],
+  "border-bottom-width": BorderBottomWidth(BorderSideWidth) [logical_group: BorderWidth, category: Physical],
+  "border-left-width": BorderLeftWidth(BorderSideWidth) [logical_group: BorderWidth, category: Physical],
+  "border-right-width": BorderRightWidth(BorderSideWidth) [logical_group: BorderWidth, category: Physical],
+  "border-block-start-width": BorderBlockStartWidth(BorderSideWidth) [logical_group: BorderWidth, category: Logical],
+  "border-block-end-width": BorderBlockEndWidth(BorderSideWidth) [logical_group: BorderWidth, category: Logical],
+  "border-inline-start-width": BorderInlineStartWidth(BorderSideWidth) [logical_group: BorderWidth, category: Logical],
+  "border-inline-end-width": BorderInlineEndWidth(BorderSideWidth) [logical_group: BorderWidth, category: Logical],
+
+  "border-top-left-radius": BorderTopLeftRadius(Size2D<LengthPercentage>, VendorPrefix) / WebKit / Moz [logical_group: BorderRadius, category: Physical],
+  "border-top-right-radius": BorderTopRightRadius(Size2D<LengthPercentage>, VendorPrefix) / WebKit / Moz [logical_group: BorderRadius, category: Physical],
+  "border-bottom-left-radius": BorderBottomLeftRadius(Size2D<LengthPercentage>, VendorPrefix) / WebKit / Moz [logical_group: BorderRadius, category: Physical],
+  "border-bottom-right-radius": BorderBottomRightRadius(Size2D<LengthPercentage>, VendorPrefix) / WebKit / Moz [logical_group: BorderRadius, category: Physical],
+  "border-start-start-radius": BorderStartStartRadius(Size2D<LengthPercentage>) [logical_group: BorderRadius, category: Logical],
+  "border-start-end-radius": BorderStartEndRadius(Size2D<LengthPercentage>) [logical_group: BorderRadius, category: Logical],
+  "border-end-start-radius": BorderEndStartRadius(Size2D<LengthPercentage>) [logical_group: BorderRadius, category: Logical],
+  "border-end-end-radius": BorderEndEndRadius(Size2D<LengthPercentage>) [logical_group: BorderRadius, category: Logical],
+  "border-radius": BorderRadius(BorderRadius, VendorPrefix) / WebKit / Moz shorthand: true,
+
+  "border-image-source": BorderImageSource(Image<'i>),
+  "border-image-outset": BorderImageOutset(Rect<LengthOrNumber>),
+  "border-image-repeat": BorderImageRepeat(BorderImageRepeat),
+  "border-image-width": BorderImageWidth(Rect<BorderImageSideWidth>),
+  "border-image-slice": BorderImageSlice(BorderImageSlice),
+  "border-image": BorderImage(BorderImage<'i>, VendorPrefix) / WebKit / Moz / O shorthand: true,
+
+  "border-color": BorderColor(BorderColor) shorthand: true,
+  "border-style": BorderStyle(BorderStyle) shorthand: true,
+  "border-width": BorderWidth(BorderWidth) shorthand: true,
+
+  "border-block-color": BorderBlockColor(BorderBlockColor) shorthand: true,
+  "border-block-style": BorderBlockStyle(BorderBlockStyle) shorthand: true,
+  "border-block-width": BorderBlockWidth(BorderBlockWidth) shorthand: true,
+
+  "border-inline-color": BorderInlineColor(BorderInlineColor) shorthand: true,
+  "border-inline-style": BorderInlineStyle(BorderInlineStyle) shorthand: true,
+  "border-inline-width": BorderInlineWidth(BorderInlineWidth) shorthand: true,
+
+  "border": Border(Border) shorthand: true,
+  "border-top": BorderTop(BorderTop) shorthand: true,
+  "border-bottom": BorderBottom(BorderBottom) shorthand: true,
+  "border-left": BorderLeft(BorderLeft) shorthand: true,
+  "border-right": BorderRight(BorderRight) shorthand: true,
+  "border-block": BorderBlock(BorderBlock) shorthand: true,
+  "border-block-start": BorderBlockStart(BorderBlockStart) shorthand: true,
+  "border-block-end": BorderBlockEnd(BorderBlockEnd) shorthand: true,
+  "border-inline": BorderInline(BorderInline) shorthand: true,
+  "border-inline-start": BorderInlineStart(BorderInlineStart) shorthand: true,
+  "border-inline-end": BorderInlineEnd(BorderInlineEnd) shorthand: true,
+
+  "outline": Outline(Outline) shorthand: true,
+  "outline-color": OutlineColor(CssColor),
+  "outline-style": OutlineStyle(OutlineStyle),
+  "outline-width": OutlineWidth(BorderSideWidth),
+
+  // Flex properties: https://www.w3.org/TR/2018/CR-css-flexbox-1-20181119
+  "flex-direction": FlexDirection(FlexDirection, VendorPrefix) / WebKit / Ms,
+  "flex-wrap": FlexWrap(FlexWrap, VendorPrefix) / WebKit / Ms,
+  "flex-flow": FlexFlow(FlexFlow, VendorPrefix) / WebKit / Ms shorthand: true,
+  "flex-grow": FlexGrow(CSSNumber, VendorPrefix) / WebKit,
+  "flex-shrink": FlexShrink(CSSNumber, VendorPrefix) / WebKit,
+  "flex-basis": FlexBasis(LengthPercentageOrAuto, VendorPrefix) / WebKit,
+  "flex": Flex(Flex, VendorPrefix) / WebKit / Ms shorthand: true,
+  "order": Order(CSSInteger, VendorPrefix) / WebKit,
+
+  // Align properties: https://www.w3.org/TR/2020/WD-css-align-3-20200421
+  "align-content": AlignContent(AlignContent, VendorPrefix) / WebKit,
+  "justify-content": JustifyContent(JustifyContent, VendorPrefix) / WebKit,
+  "place-content": PlaceContent(PlaceContent) shorthand: true,
+  "align-self": AlignSelf(AlignSelf, VendorPrefix) / WebKit,
+  "justify-self": JustifySelf(JustifySelf),
+  "place-self": PlaceSelf(PlaceSelf) shorthand: true,
+  "align-items": AlignItems(AlignItems, VendorPrefix) / WebKit,
+  "justify-items": JustifyItems(JustifyItems),
+  "place-items": PlaceItems(PlaceItems) shorthand: true,
+  "row-gap": RowGap(GapValue),
+  "column-gap": ColumnGap(GapValue),
+  "gap": Gap(Gap) shorthand: true,
+
+  // Old flex (2009): https://www.w3.org/TR/2009/WD-css3-flexbox-20090723/
+  "box-orient": BoxOrient(BoxOrient, VendorPrefix) / WebKit / Moz unprefixed: false,
+  "box-direction": BoxDirection(BoxDirection, VendorPrefix) / WebKit / Moz unprefixed: false,
+  "box-ordinal-group": BoxOrdinalGroup(CSSInteger, VendorPrefix) / WebKit / Moz unprefixed: false,
+  "box-align": BoxAlign(BoxAlign, VendorPrefix) / WebKit / Moz unprefixed: false,
+  "box-flex": BoxFlex(CSSNumber, VendorPrefix) / WebKit / Moz unprefixed: false,
+  "box-flex-group": BoxFlexGroup(CSSInteger, VendorPrefix) / WebKit unprefixed: false,
+  "box-pack": BoxPack(BoxPack, VendorPrefix) / WebKit / Moz unprefixed: false,
+  "box-lines": BoxLines(BoxLines, VendorPrefix) / WebKit / Moz unprefixed: false,
+
+  // Old flex (2012): https://www.w3.org/TR/2012/WD-css3-flexbox-20120322/
+  "flex-pack": FlexPack(FlexPack, VendorPrefix) / Ms unprefixed: false,
+  "flex-order": FlexOrder(CSSInteger, VendorPrefix) / Ms unprefixed: false,
+  "flex-align": FlexAlign(BoxAlign, VendorPrefix) / Ms unprefixed: false,
+  "flex-item-align": FlexItemAlign(FlexItemAlign, VendorPrefix) / Ms unprefixed: false,
+  "flex-line-pack": FlexLinePack(FlexLinePack, VendorPrefix) / Ms unprefixed: false,
+
+  // Microsoft extensions
+  "flex-positive": FlexPositive(CSSNumber, VendorPrefix) / Ms unprefixed: false,
+  "flex-negative": FlexNegative(CSSNumber, VendorPrefix) / Ms unprefixed: false,
+  "flex-preferred-size": FlexPreferredSize(LengthPercentageOrAuto, VendorPrefix) / Ms unprefixed: false,
+
+  "grid-template-columns": GridTemplateColumns(TrackSizing<'i>),
+  "grid-template-rows": GridTemplateRows(TrackSizing<'i>),
+  "grid-auto-columns": GridAutoColumns(TrackSizeList),
+  "grid-auto-rows": GridAutoRows(TrackSizeList),
+  "grid-auto-flow": GridAutoFlow(GridAutoFlow),
+  "grid-template-areas": GridTemplateAreas(GridTemplateAreas),
+  "grid-template": GridTemplate(GridTemplate<'i>) shorthand: true,
+  "grid": Grid(Grid<'i>) shorthand: true,
+  "grid-row-start": GridRowStart(GridLine<'i>),
+  "grid-row-end": GridRowEnd(GridLine<'i>),
+  "grid-column-start": GridColumnStart(GridLine<'i>),
+  "grid-column-end": GridColumnEnd(GridLine<'i>),
+  "grid-row": GridRow(GridRow<'i>) shorthand: true,
+  "grid-column": GridColumn(GridColumn<'i>) shorthand: true,
+  "grid-area": GridArea(GridArea<'i>) shorthand: true,
+
+  "margin-top": MarginTop(LengthPercentageOrAuto) [logical_group: Margin, category: Physical],
+  "margin-bottom": MarginBottom(LengthPercentageOrAuto) [logical_group: Margin, category: Physical],
+  "margin-left": MarginLeft(LengthPercentageOrAuto) [logical_group: Margin, category: Physical],
+  "margin-right": MarginRight(LengthPercentageOrAuto) [logical_group: Margin, category: Physical],
+  "margin-block-start": MarginBlockStart(LengthPercentageOrAuto) [logical_group: Margin, category: Logical],
+  "margin-block-end": MarginBlockEnd(LengthPercentageOrAuto) [logical_group: Margin, category: Logical],
+  "margin-inline-start": MarginInlineStart(LengthPercentageOrAuto) [logical_group: Margin, category: Logical],
+  "margin-inline-end": MarginInlineEnd(LengthPercentageOrAuto) [logical_group: Margin, category: Logical],
+  "margin-block": MarginBlock(MarginBlock) shorthand: true,
+  "margin-inline": MarginInline(MarginInline) shorthand: true,
+  "margin": Margin(Margin) shorthand: true,
+
+  "padding-top": PaddingTop(LengthPercentageOrAuto) [logical_group: Padding, category: Physical],
+  "padding-bottom": PaddingBottom(LengthPercentageOrAuto) [logical_group: Padding, category: Physical],
+  "padding-left": PaddingLeft(LengthPercentageOrAuto) [logical_group: Padding, category: Physical],
+  "padding-right": PaddingRight(LengthPercentageOrAuto) [logical_group: Padding, category: Physical],
+  "padding-block-start": PaddingBlockStart(LengthPercentageOrAuto) [logical_group: Padding, category: Logical],
+  "padding-block-end": PaddingBlockEnd(LengthPercentageOrAuto) [logical_group: Padding, category: Logical],
+  "padding-inline-start": PaddingInlineStart(LengthPercentageOrAuto) [logical_group: Padding, category: Logical],
+  "padding-inline-end": PaddingInlineEnd(LengthPercentageOrAuto) [logical_group: Padding, category: Logical],
+  "padding-block": PaddingBlock(PaddingBlock) shorthand: true,
+  "padding-inline": PaddingInline(PaddingInline) shorthand: true,
+  "padding": Padding(Padding) shorthand: true,
+
+  "scroll-margin-top": ScrollMarginTop(LengthPercentageOrAuto) [logical_group: ScrollMargin, category: Physical],
+  "scroll-margin-bottom": ScrollMarginBottom(LengthPercentageOrAuto) [logical_group: ScrollMargin, category: Physical],
+  "scroll-margin-left": ScrollMarginLeft(LengthPercentageOrAuto) [logical_group: ScrollMargin, category: Physical],
+  "scroll-margin-right": ScrollMarginRight(LengthPercentageOrAuto) [logical_group: ScrollMargin, category: Physical],
+  "scroll-margin-block-start": ScrollMarginBlockStart(LengthPercentageOrAuto) [logical_group: ScrollMargin, category: Logical],
+  "scroll-margin-block-end": ScrollMarginBlockEnd(LengthPercentageOrAuto) [logical_group: ScrollMargin, category: Logical],
+  "scroll-margin-inline-start": ScrollMarginInlineStart(LengthPercentageOrAuto) [logical_group: ScrollMargin, category: Logical],
+  "scroll-margin-inline-end": ScrollMarginInlineEnd(LengthPercentageOrAuto) [logical_group: ScrollMargin, category: Logical],
+  "scroll-margin-block": ScrollMarginBlock(ScrollMarginBlock) shorthand: true,
+  "scroll-margin-inline": ScrollMarginInline(ScrollMarginInline) shorthand: true,
+  "scroll-margin": ScrollMargin(ScrollMargin) shorthand: true,
+
+  "scroll-padding-top": ScrollPaddingTop(LengthPercentageOrAuto) [logical_group: ScrollPadding, category: Physical],
+  "scroll-padding-bottom": ScrollPaddingBottom(LengthPercentageOrAuto) [logical_group: ScrollPadding, category: Physical],
+  "scroll-padding-left": ScrollPaddingLeft(LengthPercentageOrAuto) [logical_group: ScrollPadding, category: Physical],
+  "scroll-padding-right": ScrollPaddingRight(LengthPercentageOrAuto) [logical_group: ScrollPadding, category: Physical],
+  "scroll-padding-block-start": ScrollPaddingBlockStart(LengthPercentageOrAuto) [logical_group: ScrollPadding, category: Logical],
+  "scroll-padding-block-end": ScrollPaddingBlockEnd(LengthPercentageOrAuto) [logical_group: ScrollPadding, category: Logical],
+  "scroll-padding-inline-start": ScrollPaddingInlineStart(LengthPercentageOrAuto) [logical_group: ScrollPadding, category: Logical],
+  "scroll-padding-inline-end": ScrollPaddingInlineEnd(LengthPercentageOrAuto) [logical_group: ScrollPadding, category: Logical],
+  "scroll-padding-block": ScrollPaddingBlock(ScrollPaddingBlock) shorthand: true,
+  "scroll-padding-inline": ScrollPaddingInline(ScrollPaddingInline) shorthand: true,
+  "scroll-padding": ScrollPadding(ScrollPadding) shorthand: true,
+
+  "font-weight": FontWeight(FontWeight),
+  "font-size": FontSize(FontSize),
+  "font-stretch": FontStretch(FontStretch),
+  "font-family": FontFamily(Vec<FontFamily<'i>>),
+  "font-style": FontStyle(FontStyle),
+  "font-variant-caps": FontVariantCaps(FontVariantCaps),
+  "line-height": LineHeight(LineHeight),
+  "font": Font(Font<'i>) shorthand: true,
+  "vertical-align": VerticalAlign(VerticalAlign),
+  "font-palette": FontPalette(DashedIdentReference<'i>),
+
+  "transition-property": TransitionProperty(SmallVec<[PropertyId<'i>; 1]>, VendorPrefix) / WebKit / Moz / Ms,
+  "transition-duration": TransitionDuration(SmallVec<[Time; 1]>, VendorPrefix) / WebKit / Moz / Ms,
+  "transition-delay": TransitionDelay(SmallVec<[Time; 1]>, VendorPrefix) / WebKit / Moz / Ms,
+  "transition-timing-function": TransitionTimingFunction(SmallVec<[EasingFunction; 1]>, VendorPrefix) / WebKit / Moz / Ms,
+  "transition": Transition(SmallVec<[Transition<'i>; 1]>, VendorPrefix) / WebKit / Moz / Ms shorthand: true,
+
+  "animation-name": AnimationName(AnimationNameList<'i>, VendorPrefix) / WebKit / Moz / O,
+  "animation-duration": AnimationDuration(SmallVec<[Time; 1]>, VendorPrefix) / WebKit / Moz / O,
+  "animation-timing-function": AnimationTimingFunction(SmallVec<[EasingFunction; 1]>, VendorPrefix) / WebKit / Moz / O,
+  "animation-iteration-count": AnimationIterationCount(SmallVec<[AnimationIterationCount; 1]>, VendorPrefix) / WebKit / Moz / O,
+  "animation-direction": AnimationDirection(SmallVec<[AnimationDirection; 1]>, VendorPrefix) / WebKit / Moz / O,
+  "animation-play-state": AnimationPlayState(SmallVec<[AnimationPlayState; 1]>, VendorPrefix) / WebKit / Moz / O,
+  "animation-delay": AnimationDelay(SmallVec<[Time; 1]>, VendorPrefix) / WebKit / Moz / O,
+  "animation-fill-mode": AnimationFillMode(SmallVec<[AnimationFillMode; 1]>, VendorPrefix) / WebKit / Moz / O,
+  "animation-composition": AnimationComposition(SmallVec<[AnimationComposition; 1]>),
+  "animation-timeline": AnimationTimeline(SmallVec<[AnimationTimeline<'i>; 1]>),
+  "animation-range-start": AnimationRangeStart(SmallVec<[AnimationRangeStart; 1]>),
+  "animation-range-end": AnimationRangeEnd(SmallVec<[AnimationRangeEnd; 1]>),
+  "animation-range": AnimationRange(SmallVec<[AnimationRange; 1]>),
+  "animation": Animation(AnimationList<'i>, VendorPrefix) / WebKit / Moz / O shorthand: true,
+
+  // https://drafts.csswg.org/css-transforms-2/
+  "transform": Transform(TransformList, VendorPrefix) / WebKit / Moz / Ms / O,
+  "transform-origin": TransformOrigin(Position, VendorPrefix) / WebKit / Moz / Ms / O, // TODO: handle z offset syntax
+  "transform-style": TransformStyle(TransformStyle, VendorPrefix) / WebKit / Moz,
+  "transform-box": TransformBox(TransformBox),
+  "backface-visibility": BackfaceVisibility(BackfaceVisibility, VendorPrefix) / WebKit / Moz,
+  "perspective": Perspective(Perspective, VendorPrefix) / WebKit / Moz,
+  "perspective-origin": PerspectiveOrigin(Position, VendorPrefix) / WebKit / Moz,
+  "translate": Translate(Translate),
+  "rotate": Rotate(Rotate),
+  "scale": Scale(Scale),
+
+  // https://www.w3.org/TR/2021/CRD-css-text-3-20210422
+  "text-transform": TextTransform(TextTransform),
+  "white-space": WhiteSpace(WhiteSpace),
+  "tab-size": TabSize(LengthOrNumber, VendorPrefix) / Moz / O,
+  "word-break": WordBreak(WordBreak),
+  "line-break": LineBreak(LineBreak),
+  "hyphens": Hyphens(Hyphens, VendorPrefix) / WebKit / Moz / Ms,
+  "overflow-wrap": OverflowWrap(OverflowWrap),
+  "word-wrap": WordWrap(OverflowWrap),
+  "text-align": TextAlign(TextAlign),
+  "text-align-last": TextAlignLast(TextAlignLast, VendorPrefix) / Moz,
+  "text-justify": TextJustify(TextJustify),
+  "word-spacing": WordSpacing(Spacing),
+  "letter-spacing": LetterSpacing(Spacing),
+  "text-indent": TextIndent(TextIndent),
+
+  // https://www.w3.org/TR/2020/WD-css-text-decor-4-20200506
+  "text-decoration-line": TextDecorationLine(TextDecorationLine, VendorPrefix) / WebKit / Moz,
+  "text-decoration-style": TextDecorationStyle(TextDecorationStyle, VendorPrefix) / WebKit / Moz,
+  "text-decoration-color": TextDecorationColor(CssColor, VendorPrefix) / WebKit / Moz,
+  "text-decoration-thickness": TextDecorationThickness(TextDecorationThickness),
+  "text-decoration": TextDecoration(TextDecoration, VendorPrefix) / WebKit / Moz shorthand: true,
+  "text-decoration-skip-ink": TextDecorationSkipInk(TextDecorationSkipInk, VendorPrefix) / WebKit,
+  "text-emphasis-style": TextEmphasisStyle(TextEmphasisStyle<'i>, VendorPrefix) / WebKit,
+  "text-emphasis-color": TextEmphasisColor(CssColor, VendorPrefix) / WebKit,
+  "text-emphasis": TextEmphasis(TextEmphasis<'i>, VendorPrefix) / WebKit shorthand: true,
+  "text-emphasis-position": TextEmphasisPosition(TextEmphasisPosition, VendorPrefix) / WebKit,
+  "text-shadow": TextShadow(SmallVec<[TextShadow; 1]>),
+
+  // https://w3c.github.io/csswg-drafts/css-size-adjust/
+  "text-size-adjust": TextSizeAdjust(TextSizeAdjust, VendorPrefix) / WebKit / Moz / Ms,
+
+  // https://drafts.csswg.org/css-writing-modes-3/
+  "direction": Direction(Direction),
+  "unicode-bidi": UnicodeBidi(UnicodeBidi),
+
+  // https://www.w3.org/TR/css-break-3/
+  "box-decoration-break": BoxDecorationBreak(BoxDecorationBreak, VendorPrefix) / WebKit,
+
+  // https://www.w3.org/TR/2021/WD-css-ui-4-20210316
+  "resize": Resize(Resize),
+  "cursor": Cursor(Cursor<'i>),
+  "caret-color": CaretColor(ColorOrAuto),
+  "caret-shape": CaretShape(CaretShape),
+  "caret": Caret(Caret) shorthand: true,
+  "user-select": UserSelect(UserSelect, VendorPrefix) / WebKit / Moz / Ms,
+  "accent-color": AccentColor(ColorOrAuto),
+  "appearance": Appearance(Appearance<'i>, VendorPrefix) / WebKit / Moz / Ms,
+
+  // https://www.w3.org/TR/2020/WD-css-lists-3-20201117
+  "list-style-type": ListStyleType(ListStyleType<'i>),
+  "list-style-image": ListStyleImage(Image<'i>),
+  "list-style-position": ListStylePosition(ListStylePosition),
+  "list-style": ListStyle(ListStyle<'i>) shorthand: true,
+  "marker-side": MarkerSide(MarkerSide),
+
+  // CSS modules
+  "composes": Composes(Composes<'i>) if css_modules,
+
+  // https://www.w3.org/TR/SVG2/painting.html
+  "fill": Fill(SVGPaint<'i>),
+  "fill-rule": FillRule(FillRule),
+  "fill-opacity": FillOpacity(AlphaValue),
+  "stroke": Stroke(SVGPaint<'i>),
+  "stroke-opacity": StrokeOpacity(AlphaValue),
+  "stroke-width": StrokeWidth(LengthPercentage),
+  "stroke-linecap": StrokeLinecap(StrokeLinecap),
+  "stroke-linejoin": StrokeLinejoin(StrokeLinejoin),
+  "stroke-miterlimit": StrokeMiterlimit(CSSNumber),
+  "stroke-dasharray": StrokeDasharray(StrokeDasharray),
+  "stroke-dashoffset": StrokeDashoffset(LengthPercentage),
+  "marker-start": MarkerStart(Marker<'i>),
+  "marker-mid": MarkerMid(Marker<'i>),
+  "marker-end": MarkerEnd(Marker<'i>),
+  "marker": Marker(Marker<'i>),
+  "color-interpolation": ColorInterpolation(ColorInterpolation),
+  "color-interpolation-filters": ColorInterpolationFilters(ColorInterpolation),
+  "color-rendering": ColorRendering(ColorRendering),
+  "shape-rendering": ShapeRendering(ShapeRendering),
+  "text-rendering": TextRendering(TextRendering),
+  "image-rendering": ImageRendering(ImageRendering),
+
+  // https://www.w3.org/TR/css-masking-1/
+  "clip-path": ClipPath(ClipPath<'i>, VendorPrefix) / WebKit,
+  "clip-rule": ClipRule(FillRule),
+  "mask-image": MaskImage(SmallVec<[Image<'i>; 1]>, VendorPrefix) / WebKit,
+  "mask-mode": MaskMode(SmallVec<[MaskMode; 1]>),
+  "mask-repeat": MaskRepeat(SmallVec<[BackgroundRepeat; 1]>, VendorPrefix) / WebKit,
+  "mask-position-x": MaskPositionX(SmallVec<[HorizontalPosition; 1]>),
+  "mask-position-y": MaskPositionY(SmallVec<[VerticalPosition; 1]>),
+  "mask-position": MaskPosition(SmallVec<[Position; 1]>, VendorPrefix) / WebKit,
+  "mask-clip": MaskClip(SmallVec<[MaskClip; 1]>, VendorPrefix) / WebKit,
+  "mask-origin": MaskOrigin(SmallVec<[GeometryBox; 1]>, VendorPrefix) / WebKit,
+  "mask-size": MaskSize(SmallVec<[BackgroundSize; 1]>, VendorPrefix) / WebKit,
+  "mask-composite": MaskComposite(SmallVec<[MaskComposite; 1]>),
+  "mask-type": MaskType(MaskType),
+  "mask": Mask(SmallVec<[Mask<'i>; 1]>, VendorPrefix) / WebKit shorthand: true,
+  "mask-border-source": MaskBorderSource(Image<'i>),
+  "mask-border-mode": MaskBorderMode(MaskBorderMode),
+  "mask-border-slice": MaskBorderSlice(BorderImageSlice),
+  "mask-border-width": MaskBorderWidth(Rect<BorderImageSideWidth>),
+  "mask-border-outset": MaskBorderOutset(Rect<LengthOrNumber>),
+  "mask-border-repeat": MaskBorderRepeat(BorderImageRepeat),
+  "mask-border": MaskBorder(MaskBorder<'i>) shorthand: true,
+
+  // WebKit additions
+  "-webkit-mask-composite": WebKitMaskComposite(SmallVec<[WebKitMaskComposite; 1]>),
+  "mask-source-type": WebKitMaskSourceType(SmallVec<[WebKitMaskSourceType; 1]>, VendorPrefix) / WebKit unprefixed: false,
+  "mask-box-image": WebKitMaskBoxImage(BorderImage<'i>, VendorPrefix) / WebKit unprefixed: false,
+  "mask-box-image-source": WebKitMaskBoxImageSource(Image<'i>, VendorPrefix) / WebKit unprefixed: false,
+  "mask-box-image-slice": WebKitMaskBoxImageSlice(BorderImageSlice, VendorPrefix) / WebKit unprefixed: false,
+  "mask-box-image-width": WebKitMaskBoxImageWidth(Rect<BorderImageSideWidth>, VendorPrefix) / WebKit unprefixed: false,
+  "mask-box-image-outset": WebKitMaskBoxImageOutset(Rect<LengthOrNumber>, VendorPrefix) / WebKit unprefixed: false,
+  "mask-box-image-repeat": WebKitMaskBoxImageRepeat(BorderImageRepeat, VendorPrefix) / WebKit unprefixed: false,
+
+  // https://drafts.fxtf.org/filter-effects-1/
+  "filter": Filter(FilterList<'i>, VendorPrefix) / WebKit,
+  "backdrop-filter": BackdropFilter(FilterList<'i>, VendorPrefix) / WebKit,
+
+  // https://drafts.csswg.org/css2/
+  "z-index": ZIndex(position::ZIndex),
+
+  // https://drafts.csswg.org/css-contain-3/
+  "container-type": ContainerType(ContainerType),
+  "container-name": ContainerName(ContainerNameList<'i>),
+  "container": Container(Container<'i>) shorthand: true,
+
+  // https://w3c.github.io/csswg-drafts/css-view-transitions-1/
+  "view-transition-name": ViewTransitionName(ViewTransitionName<'i>),
+  // https://drafts.csswg.org/css-view-transitions-2/
+  "view-transition-class": ViewTransitionClass(NoneOrCustomIdentList<'i>),
+  "view-transition-group": ViewTransitionGroup(ViewTransitionGroup<'i>),
+
+  // https://drafts.csswg.org/css-color-adjust/
+  "color-scheme": ColorScheme(ColorScheme),
+}
+
+impl<'i, T: smallvec::Array<Item = V>, V: Parse<'i>> Parse<'i> for SmallVec<T> {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    // Copied from cssparser `parse_comma_separated` but using SmallVec instead of Vec.
+    let mut values = smallvec![];
+    loop {
+      input.skip_whitespace(); // Unnecessary for correctness, but may help try() in parse_one rewind less.
+      match input.parse_until_before(Delimiter::Comma, &mut V::parse) {
+        Ok(v) => values.push(v),
+        Err(err) => return Err(err),
+      }
+      match input.next() {
+        Err(_) => return Ok(values),
+        Ok(&cssparser::Token::Comma) => continue,
+        Ok(_) => unreachable!(),
+      }
+    }
+  }
+}
+
+impl<T: smallvec::Array<Item = V>, V: ToCss> ToCss for SmallVec<T> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    let len = self.len();
+    for (idx, val) in self.iter().enumerate() {
+      val.to_css(dest)?;
+      if idx < len - 1 {
+        dest.delim(',', false)?;
+      }
+    }
+    Ok(())
+  }
+}
+
+impl<'i, T: Parse<'i>> Parse<'i> for Vec<T> {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    input.parse_comma_separated(|input| T::parse(input))
+  }
+}
+
+impl<T: ToCss> ToCss for Vec<T> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    let len = self.len();
+    for (idx, val) in self.iter().enumerate() {
+      val.to_css(dest)?;
+      if idx < len - 1 {
+        dest.delim(',', false)?;
+      }
+    }
+    Ok(())
+  }
+}
+
+enum_property! {
+  /// A [CSS-wide keyword](https://drafts.csswg.org/css-cascade-5/#defaulting-keywords).
+  pub enum CSSWideKeyword {
+    /// The property's initial value.
+    "initial": Initial,
+    /// The property's computed value on the parent element.
+    "inherit": Inherit,
+    /// Either inherit or initial depending on whether the property is inherited.
+    "unset": Unset,
+    /// Rolls back the cascade to the cascaded value of the earlier origin.
+    "revert": Revert,
+    /// Rolls back the cascade to the value of the previous cascade layer.
+    "revert-layer": RevertLayer,
+  }
+}
diff --git a/src/properties/outline.rs b/src/properties/outline.rs
new file mode 100644
index 0000000..2ecdb20
--- /dev/null
+++ b/src/properties/outline.rs
@@ -0,0 +1,61 @@
+//! CSS properties related to outlines.
+
+use super::border::{BorderSideWidth, GenericBorder, LineStyle};
+use super::{Property, PropertyId};
+use crate::context::PropertyHandlerContext;
+use crate::declaration::{DeclarationBlock, DeclarationList};
+use crate::error::{ParserError, PrinterError};
+use crate::macros::{impl_shorthand, shorthand_handler};
+use crate::printer::Printer;
+use crate::targets::Browsers;
+use crate::traits::{FallbackValues, IsCompatible, Parse, PropertyHandler, Shorthand, ToCss};
+use crate::values::color::CssColor;
+#[cfg(feature = "visitor")]
+use crate::visitor::Visit;
+use cssparser::*;
+
+/// A value for the [outline-style](https://drafts.csswg.org/css-ui/#outline-style) property.
+#[derive(Debug, Clone, PartialEq, Parse, ToCss)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", content = "value", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum OutlineStyle {
+  /// The `auto` keyword.
+  Auto,
+  /// A value equivalent to the `border-style` property.
+  LineStyle(LineStyle),
+}
+
+impl Default for OutlineStyle {
+  fn default() -> OutlineStyle {
+    OutlineStyle::LineStyle(LineStyle::None)
+  }
+}
+
+impl IsCompatible for OutlineStyle {
+  fn is_compatible(&self, _browsers: Browsers) -> bool {
+    true
+  }
+}
+
+/// A value for the [outline](https://drafts.csswg.org/css-ui/#outline) shorthand property.
+pub type Outline = GenericBorder<OutlineStyle, 11>;
+
+impl_shorthand! {
+  Outline(Outline) {
+    width: [OutlineWidth],
+    style: [OutlineStyle],
+    color: [OutlineColor],
+  }
+}
+
+shorthand_handler!(OutlineHandler -> Outline fallbacks: true {
+  width: OutlineWidth(BorderSideWidth),
+  style: OutlineStyle(OutlineStyle),
+  color: OutlineColor(CssColor, fallback: true),
+});
diff --git a/src/properties/overflow.rs b/src/properties/overflow.rs
new file mode 100644
index 0000000..bb5e54b
--- /dev/null
+++ b/src/properties/overflow.rs
@@ -0,0 +1,136 @@
+//! CSS properties related to overflow.
+
+use super::{Property, PropertyId};
+use crate::compat::Feature;
+use crate::context::PropertyHandlerContext;
+use crate::declaration::{DeclarationBlock, DeclarationList};
+use crate::error::{ParserError, PrinterError};
+use crate::macros::{define_shorthand, enum_property};
+use crate::printer::Printer;
+use crate::traits::{Parse, PropertyHandler, Shorthand, ToCss};
+#[cfg(feature = "visitor")]
+use crate::visitor::Visit;
+use cssparser::*;
+
+enum_property! {
+  /// An [overflow](https://www.w3.org/TR/css-overflow-3/#overflow-properties) keyword
+  /// as used in the `overflow-x`, `overflow-y`, and `overflow` properties.
+  pub enum OverflowKeyword {
+    /// Overflowing content is visible.
+    Visible,
+    /// Overflowing content is hidden. Programmatic scrolling is allowed.
+    Hidden,
+    /// Overflowing content is clipped. Programmatic scrolling is not allowed.
+    Clip,
+    /// The element is scrollable.
+    Scroll,
+    /// Overflowing content scrolls if needed.
+    Auto,
+  }
+}
+
+define_shorthand! {
+  /// A value for the [overflow](https://www.w3.org/TR/css-overflow-3/#overflow-properties) shorthand property.
+  pub struct Overflow {
+    /// The overflow mode for the x direction.
+    x: OverflowX(OverflowKeyword),
+    /// The overflow mode for the y direction.
+    y: OverflowY(OverflowKeyword),
+  }
+}
+
+impl<'i> Parse<'i> for Overflow {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let x = OverflowKeyword::parse(input)?;
+    let y = input.try_parse(OverflowKeyword::parse).unwrap_or_else(|_| x.clone());
+    Ok(Overflow { x, y })
+  }
+}
+
+impl ToCss for Overflow {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    self.x.to_css(dest)?;
+    if self.y != self.x {
+      dest.write_char(' ')?;
+      self.y.to_css(dest)?;
+    }
+    Ok(())
+  }
+}
+
+enum_property! {
+  /// A value for the [text-overflow](https://www.w3.org/TR/css-overflow-3/#text-overflow) property.
+  pub enum TextOverflow {
+    /// Overflowing text is clipped.
+    Clip,
+    /// Overflowing text is truncated with an ellipsis.
+    Ellipsis,
+  }
+}
+
+#[derive(Default)]
+pub(crate) struct OverflowHandler {
+  x: Option<OverflowKeyword>,
+  y: Option<OverflowKeyword>,
+}
+
+impl<'i> PropertyHandler<'i> for OverflowHandler {
+  fn handle_property(
+    &mut self,
+    property: &Property<'i>,
+    dest: &mut DeclarationList<'i>,
+    context: &mut PropertyHandlerContext<'i, '_>,
+  ) -> bool {
+    use Property::*;
+
+    match property {
+      OverflowX(val) => self.x = Some(*val),
+      OverflowY(val) => self.y = Some(*val),
+      Overflow(val) => {
+        self.x = Some(val.x);
+        self.y = Some(val.y);
+      }
+      Unparsed(val)
+        if matches!(
+          val.property_id,
+          PropertyId::OverflowX | PropertyId::OverflowY | PropertyId::Overflow
+        ) =>
+      {
+        self.finalize(dest, context);
+        dest.push(property.clone());
+      }
+      _ => return false,
+    }
+
+    true
+  }
+
+  fn finalize(&mut self, dest: &mut DeclarationList, context: &mut PropertyHandlerContext<'i, '_>) {
+    if self.x.is_none() && self.y.is_none() {
+      return;
+    }
+
+    let x = std::mem::take(&mut self.x);
+    let y = std::mem::take(&mut self.y);
+
+    match (x, y) {
+      // Only use shorthand syntax if the x and y values are the
+      // same or the two-value syntax is supported by all targets.
+      (Some(x), Some(y)) if x == y || context.targets.is_compatible(Feature::OverflowShorthand) => {
+        dest.push(Property::Overflow(Overflow { x, y }))
+      }
+      _ => {
+        if let Some(x) = x {
+          dest.push(Property::OverflowX(x))
+        }
+
+        if let Some(y) = y {
+          dest.push(Property::OverflowY(y))
+        }
+      }
+    }
+  }
+}
diff --git a/src/properties/position.rs b/src/properties/position.rs
new file mode 100644
index 0000000..34a0cf3
--- /dev/null
+++ b/src/properties/position.rs
@@ -0,0 +1,138 @@
+//! CSS properties related to positioning.
+
+use super::Property;
+use crate::context::PropertyHandlerContext;
+use crate::declaration::DeclarationList;
+use crate::error::{ParserError, PrinterError};
+use crate::prefixes::Feature;
+use crate::printer::Printer;
+use crate::traits::{Parse, PropertyHandler, ToCss};
+use crate::values::number::CSSInteger;
+use crate::vendor_prefix::VendorPrefix;
+#[cfg(feature = "visitor")]
+use crate::visitor::Visit;
+use cssparser::*;
+
+/// A value for the [position](https://www.w3.org/TR/css-position-3/#position-property) property.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", content = "value", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum Position {
+  /// The box is laid in the document flow.
+  Static,
+  /// The box is laid out in the document flow and offset from the resulting position.
+  Relative,
+  /// The box is taken out of document flow and positioned in reference to its relative ancestor.
+  Absolute,
+  /// Similar to relative but adjusted according to the ancestor scrollable element.
+  Sticky(VendorPrefix),
+  /// The box is taken out of the document flow and positioned in reference to the page viewport.
+  Fixed,
+}
+
+impl<'i> Parse<'i> for Position {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let location = input.current_source_location();
+    let ident = input.expect_ident()?;
+    match_ignore_ascii_case! { &*ident,
+      "static" => Ok(Position::Static),
+      "relative" => Ok(Position::Relative),
+      "absolute" => Ok(Position::Absolute),
+      "fixed" => Ok(Position::Fixed),
+      "sticky" => Ok(Position::Sticky(VendorPrefix::None)),
+      "-webkit-sticky" => Ok(Position::Sticky(VendorPrefix::WebKit)),
+      _ => Err(location.new_unexpected_token_error(
+        cssparser::Token::Ident(ident.clone())
+      ))
+    }
+  }
+}
+
+impl ToCss for Position {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    match self {
+      Position::Static => dest.write_str("static"),
+      Position::Relative => dest.write_str("relative"),
+      Position::Absolute => dest.write_str("absolute"),
+      Position::Fixed => dest.write_str("fixed"),
+      Position::Sticky(prefix) => {
+        prefix.to_css(dest)?;
+        dest.write_str("sticky")
+      }
+    }
+  }
+}
+
+/// A value for the [z-index](https://drafts.csswg.org/css2/#z-index) property.
+#[derive(Debug, Clone, PartialEq, Parse, ToCss)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", content = "value", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum ZIndex {
+  /// The `auto` keyword.
+  Auto,
+  /// An integer value.
+  Integer(CSSInteger),
+}
+
+#[derive(Default)]
+pub(crate) struct PositionHandler {
+  position: Option<Position>,
+}
+
+impl<'i> PropertyHandler<'i> for PositionHandler {
+  fn handle_property(
+    &mut self,
+    property: &Property<'i>,
+    _: &mut DeclarationList<'i>,
+    _: &mut PropertyHandlerContext<'i, '_>,
+  ) -> bool {
+    if let Property::Position(position) = property {
+      if let (Some(Position::Sticky(cur)), Position::Sticky(new)) = (&mut self.position, position) {
+        *cur |= *new;
+      } else {
+        self.position = Some(position.clone());
+      }
+
+      return true;
+    }
+
+    false
+  }
+
+  fn finalize(&mut self, dest: &mut DeclarationList, context: &mut PropertyHandlerContext<'i, '_>) {
+    if self.position.is_none() {
+      return;
+    }
+
+    if let Some(position) = std::mem::take(&mut self.position) {
+      match position {
+        Position::Sticky(mut prefix) => {
+          prefix = context.targets.prefixes(prefix, Feature::Sticky);
+          if prefix.contains(VendorPrefix::WebKit) {
+            dest.push(Property::Position(Position::Sticky(VendorPrefix::WebKit)))
+          }
+
+          if prefix.contains(VendorPrefix::None) {
+            dest.push(Property::Position(Position::Sticky(VendorPrefix::None)))
+          }
+        }
+        _ => dest.push(Property::Position(position)),
+      }
+    }
+  }
+}
diff --git a/src/properties/prefix_handler.rs b/src/properties/prefix_handler.rs
new file mode 100644
index 0000000..7ad2dfe
--- /dev/null
+++ b/src/properties/prefix_handler.rs
@@ -0,0 +1,180 @@
+#![allow(non_snake_case)]
+use super::{Property, PropertyId};
+use crate::context::PropertyHandlerContext;
+use crate::declaration::DeclarationList;
+use crate::prefixes::Feature;
+use crate::traits::{FallbackValues, IsCompatible, PropertyHandler};
+use crate::vendor_prefix::VendorPrefix;
+
+macro_rules! define_prefixes {
+  (
+    $( $name: ident, )+
+  ) => {
+    #[derive(Default)]
+    pub(crate) struct PrefixHandler {
+      $(
+        $name: Option<usize>,
+      )+
+    }
+
+    impl<'i> PropertyHandler<'i> for PrefixHandler {
+      fn handle_property(&mut self, property: &Property<'i>, dest: &mut DeclarationList<'i>, context: &mut PropertyHandlerContext) -> bool {
+        match property {
+          $(
+            Property::$name(val, prefix) => {
+              if let Some(i) = self.$name {
+                if let Some(decl) = dest.get_mut(i) {
+                  if let Property::$name(cur, prefixes) = decl {
+                    // If the value is the same, update the prefix.
+                    // If the prefix is the same, then update the value.
+                    if val == cur || prefixes.contains(*prefix) {
+                      *cur = val.clone();
+                      *prefixes |= *prefix;
+                      *prefixes = context.targets.prefixes(*prefixes, Feature::$name);
+                      return true
+                    }
+                  }
+                }
+              }
+
+              // Update the prefixes based on the targets.
+              let prefixes = context.targets.prefixes(*prefix, Feature::$name);
+
+              // Store the index of the property, so we can update it later.
+              self.$name = Some(dest.len());
+              dest.push(Property::$name(val.clone(), prefixes))
+            }
+          )+
+          _ => return false
+        }
+
+        true
+      }
+
+      fn finalize(&mut self, _: &mut DeclarationList, _: &mut PropertyHandlerContext) {}
+    }
+  };
+}
+
+define_prefixes! {
+  TransformOrigin,
+  TransformStyle,
+  BackfaceVisibility,
+  Perspective,
+  PerspectiveOrigin,
+  BoxSizing,
+  TabSize,
+  Hyphens,
+  TextAlignLast,
+  TextDecorationSkipInk,
+  TextOverflow,
+  UserSelect,
+  Appearance,
+  ClipPath,
+  BoxDecorationBreak,
+  TextSizeAdjust,
+}
+
+macro_rules! define_fallbacks {
+  (
+    $( $name: ident$(($p: ident))?, )+
+  ) => {
+    paste::paste! {
+      #[derive(Default)]
+      pub(crate) struct FallbackHandler {
+        $(
+          [<$name:snake>]: Option<usize>
+        ),+
+      }
+    }
+
+    impl<'i> PropertyHandler<'i> for FallbackHandler {
+      fn handle_property(&mut self, property: &Property<'i>, dest: &mut DeclarationList<'i>, context: &mut PropertyHandlerContext<'i, '_>) -> bool {
+        match property {
+          $(
+            Property::$name(val $(, mut $p)?) => {
+              let mut val = val.clone();
+              $(
+                $p = context.targets.prefixes($p, Feature::$name);
+              )?
+              if paste::paste! { self.[<$name:snake>] }.is_none() {
+                let fallbacks = val.get_fallbacks(context.targets);
+                #[allow(unused_variables)]
+                let has_fallbacks = !fallbacks.is_empty();
+                for fallback in fallbacks {
+                  dest.push(Property::$name(fallback $(, $p)?))
+                }
+
+                $(
+                  if has_fallbacks && $p.contains(VendorPrefix::None) {
+                    $p = VendorPrefix::None;
+                  }
+                )?
+              }
+
+              if paste::paste! { self.[<$name:snake>] }.is_none() || matches!(context.targets.browsers, Some(targets) if !val.is_compatible(targets)) {
+                paste::paste! { self.[<$name:snake>] = Some(dest.len()) };
+                dest.push(Property::$name(val $(, $p)?));
+              } else if let Some(index) = paste::paste! { self.[<$name:snake>] } {
+                dest[index] = Property::$name(val $(, $p)?);
+              }
+            }
+          )+
+          Property::Unparsed(val) => {
+            let (mut unparsed, index) = match val.property_id {
+              $(
+                PropertyId::$name$(($p))? => {
+                  macro_rules! get_prefixed {
+                    ($vp: ident) => {
+                      if $vp.contains(VendorPrefix::None) {
+                        val.get_prefixed(context.targets, Feature::$name)
+                      } else {
+                        val.clone()
+                      }
+                    };
+                    () => {
+                      val.clone()
+                    };
+                  }
+
+                  let val = get_prefixed!($($p)?);
+                  (val, paste::paste! { &mut self.[<$name:snake>] })
+                }
+              )+
+              _ => return false
+            };
+
+            // Unparsed properties are always "valid", meaning they always override previous declarations.
+            context.add_unparsed_fallbacks(&mut unparsed);
+            if let Some(index) = *index {
+              dest[index] = Property::Unparsed(unparsed);
+            } else {
+              *index = Some(dest.len());
+              dest.push(Property::Unparsed(unparsed));
+            }
+          }
+          _ => return false
+        }
+
+        true
+      }
+
+      fn finalize(&mut self, _: &mut DeclarationList, _: &mut PropertyHandlerContext) {
+        $(
+          paste::paste! { self.[<$name:snake>] = None };
+        )+
+      }
+    }
+  };
+}
+
+define_fallbacks! {
+  Color,
+  TextShadow,
+  Filter(prefix),
+  BackdropFilter(prefix),
+  Fill,
+  Stroke,
+  CaretColor,
+  Caret,
+}
diff --git a/src/properties/size.rs b/src/properties/size.rs
new file mode 100644
index 0000000..0c87335
--- /dev/null
+++ b/src/properties/size.rs
@@ -0,0 +1,546 @@
+//! CSS properties related to box sizing.
+
+use crate::compat::Feature;
+use crate::context::PropertyHandlerContext;
+use crate::declaration::DeclarationList;
+use crate::error::{ParserError, PrinterError};
+use crate::logical::PropertyCategory;
+use crate::macros::{enum_property, property_bitflags};
+use crate::printer::Printer;
+use crate::properties::{Property, PropertyId};
+use crate::traits::{IsCompatible, Parse, PropertyHandler, ToCss};
+use crate::values::length::LengthPercentage;
+use crate::values::ratio::Ratio;
+use crate::vendor_prefix::VendorPrefix;
+#[cfg(feature = "visitor")]
+use crate::visitor::Visit;
+use cssparser::*;
+
+#[cfg(feature = "serde")]
+use crate::serialization::*;
+
+// https://drafts.csswg.org/css-sizing-3/#specifying-sizes
+// https://www.w3.org/TR/css-sizing-4/#sizing-values
+
+/// A value for the [preferred size properties](https://drafts.csswg.org/css-sizing-3/#preferred-size-properties),
+/// i.e. `width` and `height.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum Size {
+  /// The `auto` keyword.
+  Auto,
+  /// An explicit length or percentage.
+  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<LengthPercentage>"))]
+  LengthPercentage(LengthPercentage),
+  /// The `min-content` keyword.
+  #[cfg_attr(feature = "serde", serde(with = "PrefixWrapper"))]
+  MinContent(VendorPrefix),
+  /// The `max-content` keyword.
+  #[cfg_attr(feature = "serde", serde(with = "PrefixWrapper"))]
+  MaxContent(VendorPrefix),
+  /// The `fit-content` keyword.
+  #[cfg_attr(feature = "serde", serde(with = "PrefixWrapper"))]
+  FitContent(VendorPrefix),
+  /// The `fit-content()` function.
+  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<LengthPercentage>"))]
+  FitContentFunction(LengthPercentage),
+  /// The `stretch` keyword, or the `-webkit-fill-available` or `-moz-available` prefixed keywords.
+  #[cfg_attr(feature = "serde", serde(with = "PrefixWrapper"))]
+  Stretch(VendorPrefix),
+  /// The `contain` keyword.
+  Contain,
+}
+
+impl<'i> Parse<'i> for Size {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let res = input.try_parse(|input| {
+      let ident = input.expect_ident()?;
+      Ok(match_ignore_ascii_case! { &*ident,
+        "auto" => Size::Auto,
+        "min-content" => Size::MinContent(VendorPrefix::None),
+        "-webkit-min-content" => Size::MinContent(VendorPrefix::WebKit),
+        "-moz-min-content" => Size::MinContent(VendorPrefix::Moz),
+        "max-content" => Size::MaxContent(VendorPrefix::None),
+        "-webkit-max-content" => Size::MaxContent(VendorPrefix::WebKit),
+        "-moz-max-content" => Size::MaxContent(VendorPrefix::Moz),
+        "stretch" => Size::Stretch(VendorPrefix::None),
+        "-webkit-fill-available" => Size::Stretch(VendorPrefix::WebKit),
+        "-moz-available" => Size::Stretch(VendorPrefix::Moz),
+        "fit-content" => Size::FitContent(VendorPrefix::None),
+        "-webkit-fit-content" => Size::FitContent(VendorPrefix::WebKit),
+        "-moz-fit-content" => Size::FitContent(VendorPrefix::Moz),
+        "contain" => Size::Contain,
+        _ => return Err(input.new_custom_error(ParserError::InvalidValue))
+      })
+    });
+
+    if res.is_ok() {
+      return res;
+    }
+
+    if let Ok(res) = input.try_parse(parse_fit_content) {
+      return Ok(Size::FitContentFunction(res));
+    }
+
+    let lp = input.try_parse(LengthPercentage::parse)?;
+    Ok(Size::LengthPercentage(lp))
+  }
+}
+
+impl ToCss for Size {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    use Size::*;
+    match self {
+      Auto => dest.write_str("auto"),
+      Contain => dest.write_str("contain"),
+      MinContent(vp) => {
+        vp.to_css(dest)?;
+        dest.write_str("min-content")
+      }
+      MaxContent(vp) => {
+        vp.to_css(dest)?;
+        dest.write_str("max-content")
+      }
+      FitContent(vp) => {
+        vp.to_css(dest)?;
+        dest.write_str("fit-content")
+      }
+      Stretch(vp) => match *vp {
+        VendorPrefix::None => dest.write_str("stretch"),
+        VendorPrefix::WebKit => dest.write_str("-webkit-fill-available"),
+        VendorPrefix::Moz => dest.write_str("-moz-available"),
+        _ => unreachable!(),
+      },
+      FitContentFunction(l) => {
+        dest.write_str("fit-content(")?;
+        l.to_css(dest)?;
+        dest.write_str(")")
+      }
+      LengthPercentage(l) => l.to_css(dest),
+    }
+  }
+}
+
+impl IsCompatible for Size {
+  fn is_compatible(&self, browsers: crate::targets::Browsers) -> bool {
+    use Size::*;
+    match self {
+      LengthPercentage(l) => l.is_compatible(browsers),
+      MinContent(..) => Feature::MinContentSize.is_compatible(browsers),
+      MaxContent(..) => Feature::MaxContentSize.is_compatible(browsers),
+      FitContent(..) => Feature::FitContentSize.is_compatible(browsers),
+      FitContentFunction(l) => {
+        Feature::FitContentFunctionSize.is_compatible(browsers) && l.is_compatible(browsers)
+      }
+      Stretch(vp) => match *vp {
+        VendorPrefix::None => Feature::StretchSize,
+        VendorPrefix::WebKit => Feature::WebkitFillAvailableSize,
+        VendorPrefix::Moz => Feature::MozAvailableSize,
+        _ => return false,
+      }
+      .is_compatible(browsers),
+      Contain => false, // ??? no data in mdn
+      Auto => true,
+    }
+  }
+}
+
+/// A value for the [minimum](https://drafts.csswg.org/css-sizing-3/#min-size-properties)
+/// and [maximum](https://drafts.csswg.org/css-sizing-3/#max-size-properties) size properties,
+/// e.g. `min-width` and `max-height`.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum MaxSize {
+  /// The `none` keyword.
+  None,
+  /// An explicit length or percentage.
+  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<LengthPercentage>"))]
+  LengthPercentage(LengthPercentage),
+  /// The `min-content` keyword.
+  #[cfg_attr(feature = "serde", serde(with = "PrefixWrapper"))]
+  MinContent(VendorPrefix),
+  /// The `max-content` keyword.
+  #[cfg_attr(feature = "serde", serde(with = "PrefixWrapper"))]
+  MaxContent(VendorPrefix),
+  /// The `fit-content` keyword.
+  #[cfg_attr(feature = "serde", serde(with = "PrefixWrapper"))]
+  FitContent(VendorPrefix),
+  /// The `fit-content()` function.
+  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<LengthPercentage>"))]
+  FitContentFunction(LengthPercentage),
+  /// The `stretch` keyword, or the `-webkit-fill-available` or `-moz-available` prefixed keywords.
+  #[cfg_attr(feature = "serde", serde(with = "PrefixWrapper"))]
+  Stretch(VendorPrefix),
+  /// The `contain` keyword.
+  Contain,
+}
+
+impl<'i> Parse<'i> for MaxSize {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let res = input.try_parse(|input| {
+      let ident = input.expect_ident()?;
+      Ok(match_ignore_ascii_case! { &*ident,
+        "none" => MaxSize::None,
+        "min-content" => MaxSize::MinContent(VendorPrefix::None),
+        "-webkit-min-content" => MaxSize::MinContent(VendorPrefix::WebKit),
+        "-moz-min-content" => MaxSize::MinContent(VendorPrefix::Moz),
+        "max-content" => MaxSize::MaxContent(VendorPrefix::None),
+        "-webkit-max-content" => MaxSize::MaxContent(VendorPrefix::WebKit),
+        "-moz-max-content" => MaxSize::MaxContent(VendorPrefix::Moz),
+        "stretch" => MaxSize::Stretch(VendorPrefix::None),
+        "-webkit-fill-available" => MaxSize::Stretch(VendorPrefix::WebKit),
+        "-moz-available" => MaxSize::Stretch(VendorPrefix::Moz),
+        "fit-content" => MaxSize::FitContent(VendorPrefix::None),
+        "-webkit-fit-content" => MaxSize::FitContent(VendorPrefix::WebKit),
+        "-moz-fit-content" => MaxSize::FitContent(VendorPrefix::Moz),
+        "contain" => MaxSize::Contain,
+        _ => return Err(input.new_custom_error(ParserError::InvalidValue))
+      })
+    });
+
+    if res.is_ok() {
+      return res;
+    }
+
+    if let Ok(res) = input.try_parse(parse_fit_content) {
+      return Ok(MaxSize::FitContentFunction(res));
+    }
+
+    let lp = input.try_parse(LengthPercentage::parse)?;
+    Ok(MaxSize::LengthPercentage(lp))
+  }
+}
+
+impl ToCss for MaxSize {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    use MaxSize::*;
+    match self {
+      None => dest.write_str("none"),
+      Contain => dest.write_str("contain"),
+      MinContent(vp) => {
+        vp.to_css(dest)?;
+        dest.write_str("min-content")
+      }
+      MaxContent(vp) => {
+        vp.to_css(dest)?;
+        dest.write_str("max-content")
+      }
+      FitContent(vp) => {
+        vp.to_css(dest)?;
+        dest.write_str("fit-content")
+      }
+      Stretch(vp) => match *vp {
+        VendorPrefix::None => dest.write_str("stretch"),
+        VendorPrefix::WebKit => dest.write_str("-webkit-fill-available"),
+        VendorPrefix::Moz => dest.write_str("-moz-available"),
+        _ => unreachable!(),
+      },
+      FitContentFunction(l) => {
+        dest.write_str("fit-content(")?;
+        l.to_css(dest)?;
+        dest.write_str(")")
+      }
+      LengthPercentage(l) => l.to_css(dest),
+    }
+  }
+}
+
+impl IsCompatible for MaxSize {
+  fn is_compatible(&self, browsers: crate::targets::Browsers) -> bool {
+    use MaxSize::*;
+    match self {
+      LengthPercentage(l) => l.is_compatible(browsers),
+      MinContent(..) => Feature::MinContentSize.is_compatible(browsers),
+      MaxContent(..) => Feature::MaxContentSize.is_compatible(browsers),
+      FitContent(..) => Feature::FitContentSize.is_compatible(browsers),
+      FitContentFunction(l) => {
+        Feature::FitContentFunctionSize.is_compatible(browsers) && l.is_compatible(browsers)
+      }
+      Stretch(vp) => match *vp {
+        VendorPrefix::None => Feature::StretchSize,
+        VendorPrefix::WebKit => Feature::WebkitFillAvailableSize,
+        VendorPrefix::Moz => Feature::MozAvailableSize,
+        _ => return false,
+      }
+      .is_compatible(browsers),
+      Contain => false, // ??? no data in mdn
+      None => true,
+    }
+  }
+}
+
+fn parse_fit_content<'i, 't>(
+  input: &mut Parser<'i, 't>,
+) -> Result<LengthPercentage, ParseError<'i, ParserError<'i>>> {
+  input.expect_function_matching("fit-content")?;
+  input.parse_nested_block(|input| LengthPercentage::parse(input))
+}
+
+enum_property! {
+  /// A value for the [box-sizing](https://drafts.csswg.org/css-sizing-3/#box-sizing) property.
+  pub enum BoxSizing {
+    /// Exclude the margin/border/padding from the width and height.
+    ContentBox,
+    /// Include the padding and border (but not the margin) in the width and height.
+    BorderBox,
+  }
+}
+
+/// A value for the [aspect-ratio](https://drafts.csswg.org/css-sizing-4/#aspect-ratio) property.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub struct AspectRatio {
+  /// The `auto` keyword.
+  pub auto: bool,
+  /// A preferred aspect ratio for the box, specified as width / height.
+  pub ratio: Option<Ratio>,
+}
+
+impl<'i> Parse<'i> for AspectRatio {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let location = input.current_source_location();
+    let mut auto = input.try_parse(|i| i.expect_ident_matching("auto"));
+    let ratio = input.try_parse(Ratio::parse);
+    if auto.is_err() {
+      auto = input.try_parse(|i| i.expect_ident_matching("auto"));
+    }
+    if auto.is_err() && ratio.is_err() {
+      return Err(location.new_custom_error(ParserError::InvalidValue));
+    }
+
+    Ok(AspectRatio {
+      auto: auto.is_ok(),
+      ratio: ratio.ok(),
+    })
+  }
+}
+
+impl ToCss for AspectRatio {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    if self.auto {
+      dest.write_str("auto")?;
+    }
+
+    if let Some(ratio) = &self.ratio {
+      if self.auto {
+        dest.write_char(' ')?;
+      }
+      ratio.to_css(dest)?;
+    }
+
+    Ok(())
+  }
+}
+
+property_bitflags! {
+  #[derive(Default)]
+  struct SizeProperty: u16 {
+    const Width = 1 << 0;
+    const Height = 1 << 1;
+    const MinWidth = 1 << 2;
+    const MinHeight = 1 << 3;
+    const MaxWidth = 1 << 4;
+    const MaxHeight = 1 << 5;
+    const BlockSize = 1 << 6;
+    const InlineSize = 1 << 7;
+    const MinBlockSize  = 1 << 8;
+    const MinInlineSize = 1 << 9;
+    const MaxBlockSize = 1 << 10;
+    const MaxInlineSize = 1 << 11;
+  }
+}
+
+#[derive(Default)]
+pub(crate) struct SizeHandler {
+  width: Option<Size>,
+  height: Option<Size>,
+  min_width: Option<Size>,
+  min_height: Option<Size>,
+  max_width: Option<MaxSize>,
+  max_height: Option<MaxSize>,
+  block_size: Option<Size>,
+  inline_size: Option<Size>,
+  min_block_size: Option<Size>,
+  min_inline_size: Option<Size>,
+  max_block_size: Option<MaxSize>,
+  max_inline_size: Option<MaxSize>,
+  has_any: bool,
+  flushed_properties: SizeProperty,
+  category: PropertyCategory,
+}
+
+impl<'i> PropertyHandler<'i> for SizeHandler {
+  fn handle_property(
+    &mut self,
+    property: &Property<'i>,
+    dest: &mut DeclarationList<'i>,
+    context: &mut PropertyHandlerContext<'i, '_>,
+  ) -> bool {
+    let logical_supported = !context.should_compile_logical(Feature::LogicalSize);
+
+    macro_rules! property {
+      ($prop: ident, $val: ident, $category: ident) => {{
+        // If the category changes betweet logical and physical,
+        // or if the value contains syntax that isn't supported across all targets,
+        // preserve the previous value as a fallback.
+        if PropertyCategory::$category != self.category || (self.$prop.is_some() && matches!(context.targets.browsers, Some(targets) if !$val.is_compatible(targets))) {
+          self.flush(dest, context);
+        }
+
+        self.$prop = Some($val.clone());
+        self.category = PropertyCategory::$category;
+        self.has_any = true;
+      }};
+    }
+
+    match property {
+      Property::Width(v) => property!(width, v, Physical),
+      Property::Height(v) => property!(height, v, Physical),
+      Property::MinWidth(v) => property!(min_width, v, Physical),
+      Property::MinHeight(v) => property!(min_height, v, Physical),
+      Property::MaxWidth(v) => property!(max_width, v, Physical),
+      Property::MaxHeight(v) => property!(max_height, v, Physical),
+      Property::BlockSize(size) => property!(block_size, size, Logical),
+      Property::MinBlockSize(size) => property!(min_block_size, size, Logical),
+      Property::MaxBlockSize(size) => property!(max_block_size, size, Logical),
+      Property::InlineSize(size) => property!(inline_size, size, Logical),
+      Property::MinInlineSize(size) => property!(min_inline_size, size, Logical),
+      Property::MaxInlineSize(size) => property!(max_inline_size, size, Logical),
+      Property::Unparsed(unparsed) => {
+        self.flush(dest, context);
+        macro_rules! logical_unparsed {
+          ($physical: ident) => {
+            if logical_supported {
+              self
+                .flushed_properties
+                .insert(SizeProperty::try_from(&unparsed.property_id).unwrap());
+              dest.push(property.clone());
+            } else {
+              dest.push(Property::Unparsed(
+                unparsed.with_property_id(PropertyId::$physical),
+              ));
+              self.flushed_properties.insert(SizeProperty::$physical);
+            }
+          };
+        }
+
+        match &unparsed.property_id {
+          PropertyId::Width
+          | PropertyId::Height
+          | PropertyId::MinWidth
+          | PropertyId::MaxWidth
+          | PropertyId::MinHeight
+          | PropertyId::MaxHeight => {
+            self
+              .flushed_properties
+              .insert(SizeProperty::try_from(&unparsed.property_id).unwrap());
+            dest.push(property.clone());
+          }
+          PropertyId::BlockSize => logical_unparsed!(Height),
+          PropertyId::MinBlockSize => logical_unparsed!(MinHeight),
+          PropertyId::MaxBlockSize => logical_unparsed!(MaxHeight),
+          PropertyId::InlineSize => logical_unparsed!(Width),
+          PropertyId::MinInlineSize => logical_unparsed!(MinWidth),
+          PropertyId::MaxInlineSize => logical_unparsed!(MaxWidth),
+          _ => return false,
+        }
+      }
+      _ => return false,
+    }
+
+    true
+  }
+
+  fn finalize(&mut self, dest: &mut DeclarationList, context: &mut PropertyHandlerContext<'i, '_>) {
+    self.flush(dest, context);
+    self.flushed_properties = SizeProperty::empty();
+  }
+}
+
+impl SizeHandler {
+  fn flush<'i>(&mut self, dest: &mut DeclarationList, context: &mut PropertyHandlerContext<'i, '_>) {
+    if !self.has_any {
+      return;
+    }
+
+    self.has_any = false;
+    let logical_supported = !context.should_compile_logical(Feature::LogicalSize);
+
+    macro_rules! prefix {
+      ($prop: ident, $size: ident, $feature: ident) => {
+        if !self.flushed_properties.contains(SizeProperty::$prop) {
+          let prefixes =
+            context.targets.prefixes(VendorPrefix::None, crate::prefixes::Feature::$feature) - VendorPrefix::None;
+          for prefix in prefixes {
+            dest.push(Property::$prop($size::$feature(prefix)));
+          }
+        }
+      };
+    }
+
+    macro_rules! property {
+      ($prop: ident, $val: ident, $size: ident) => {{
+        if let Some(val) = std::mem::take(&mut self.$val) {
+          match val {
+            $size::Stretch(VendorPrefix::None) => prefix!($prop, $size, Stretch),
+            $size::MinContent(VendorPrefix::None) => prefix!($prop, $size, MinContent),
+            $size::MaxContent(VendorPrefix::None) => prefix!($prop, $size, MaxContent),
+            $size::FitContent(VendorPrefix::None) => prefix!($prop, $size, FitContent),
+            _ => {}
+          }
+          dest.push(Property::$prop(val.clone()));
+          self.flushed_properties.insert(SizeProperty::$prop);
+        }
+      }};
+    }
+
+    macro_rules! logical {
+      ($prop: ident, $val: ident, $physical: ident, $size: ident) => {
+        if logical_supported {
+          property!($prop, $val, $size);
+        } else {
+          property!($physical, $val, $size);
+        }
+      };
+    }
+
+    property!(Width, width, Size);
+    property!(MinWidth, min_width, Size);
+    property!(MaxWidth, max_width, MaxSize);
+    property!(Height, height, Size);
+    property!(MinHeight, min_height, Size);
+    property!(MaxHeight, max_height, MaxSize);
+    logical!(BlockSize, block_size, Height, Size);
+    logical!(MinBlockSize, min_block_size, MinHeight, Size);
+    logical!(MaxBlockSize, max_block_size, MaxHeight, MaxSize);
+    logical!(InlineSize, inline_size, Width, Size);
+    logical!(MinInlineSize, min_inline_size, MinWidth, Size);
+    logical!(MaxInlineSize, max_inline_size, MaxWidth, MaxSize);
+  }
+}
diff --git a/src/properties/svg.rs b/src/properties/svg.rs
new file mode 100644
index 0000000..16b3845
--- /dev/null
+++ b/src/properties/svg.rs
@@ -0,0 +1,309 @@
+//! CSS properties used in SVG.
+
+use crate::error::{ParserError, PrinterError};
+use crate::macros::enum_property;
+use crate::printer::Printer;
+use crate::targets::{Browsers, Targets};
+use crate::traits::{FallbackValues, IsCompatible, Parse, ToCss};
+use crate::values::length::LengthPercentage;
+use crate::values::{color::CssColor, url::Url};
+#[cfg(feature = "visitor")]
+use crate::visitor::Visit;
+use cssparser::*;
+
+/// An SVG [`<paint>`](https://www.w3.org/TR/SVG2/painting.html#SpecifyingPaint) value
+/// used in the `fill` and `stroke` properties.
+#[derive(Debug, Clone, PartialEq, Parse, ToCss)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub enum SVGPaint<'i> {
+  /// A URL reference to a paint server element, e.g. `linearGradient`, `radialGradient`, and `pattern`.
+  Url {
+    #[cfg_attr(feature = "serde", serde(borrow))]
+    /// The url of the paint server.
+    url: Url<'i>,
+    /// A fallback to be used used in case the paint server cannot be resolved.
+    fallback: Option<SVGPaintFallback>,
+  },
+  /// A solid color paint.
+  #[cfg_attr(feature = "serde", serde(with = "crate::serialization::ValueWrapper::<CssColor>"))]
+  Color(CssColor),
+  /// Use the paint value of fill from a context element.
+  ContextFill,
+  /// Use the paint value of stroke from a context element.
+  ContextStroke,
+  /// No paint.
+  None,
+}
+
+/// A fallback for an SVG paint in case a paint server `url()` cannot be resolved.
+///
+/// See [SVGPaint](SVGPaint).
+#[derive(Debug, Clone, PartialEq, Parse, ToCss)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", content = "value", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum SVGPaintFallback {
+  /// No fallback.
+  None,
+  /// A solid color.
+  Color(CssColor),
+}
+
+impl<'i> FallbackValues for SVGPaint<'i> {
+  fn get_fallbacks(&mut self, targets: Targets) -> Vec<Self> {
+    match self {
+      SVGPaint::Color(color) => color
+        .get_fallbacks(targets)
+        .into_iter()
+        .map(|color| SVGPaint::Color(color))
+        .collect(),
+      SVGPaint::Url {
+        url,
+        fallback: Some(SVGPaintFallback::Color(color)),
+      } => color
+        .get_fallbacks(targets)
+        .into_iter()
+        .map(|color| SVGPaint::Url {
+          url: url.clone(),
+          fallback: Some(SVGPaintFallback::Color(color)),
+        })
+        .collect(),
+      _ => Vec::new(),
+    }
+  }
+}
+
+impl IsCompatible for SVGPaint<'_> {
+  fn is_compatible(&self, browsers: Browsers) -> bool {
+    match self {
+      SVGPaint::Color(c)
+      | SVGPaint::Url {
+        fallback: Some(SVGPaintFallback::Color(c)),
+        ..
+      } => c.is_compatible(browsers),
+      SVGPaint::Url { .. } | SVGPaint::None | SVGPaint::ContextFill | SVGPaint::ContextStroke => true,
+    }
+  }
+}
+
+enum_property! {
+  /// A value for the [stroke-linecap](https://www.w3.org/TR/SVG2/painting.html#LineCaps) property.
+  pub enum StrokeLinecap {
+    /// The stroke does not extend beyond its endpoints.
+    Butt,
+    /// The ends of the stroke are rounded.
+    Round,
+    /// The ends of the stroke are squared.
+    Square,
+  }
+}
+
+enum_property! {
+  /// A value for the [stroke-linejoin](https://www.w3.org/TR/SVG2/painting.html#LineJoin) property.
+  pub enum StrokeLinejoin {
+    /// A sharp corner is to be used to join path segments.
+    Miter,
+    /// Same as `miter` but clipped beyond `stroke-miterlimit`.
+    MiterClip,
+    /// A round corner is to be used to join path segments.
+    Round,
+    /// A bevelled corner is to be used to join path segments.
+    Bevel,
+    /// An arcs corner is to be used to join path segments.
+    Arcs,
+  }
+}
+
+/// A value for the [stroke-dasharray](https://www.w3.org/TR/SVG2/painting.html#StrokeDashing) property.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", content = "value", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum StrokeDasharray {
+  /// No dashing is used.
+  None,
+  /// Specifies a dashing pattern to use.
+  Values(Vec<LengthPercentage>),
+}
+
+impl<'i> Parse<'i> for StrokeDasharray {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    if input.try_parse(|input| input.expect_ident_matching("none")).is_ok() {
+      return Ok(StrokeDasharray::None);
+    }
+
+    input.skip_whitespace();
+    let mut results = vec![LengthPercentage::parse(input)?];
+    loop {
+      input.skip_whitespace();
+      let comma_location = input.current_source_location();
+      let comma = input.try_parse(|i| i.expect_comma()).is_ok();
+      if let Ok(item) = input.try_parse(LengthPercentage::parse) {
+        results.push(item);
+      } else if comma {
+        return Err(comma_location.new_unexpected_token_error(Token::Comma));
+      } else {
+        break;
+      }
+    }
+
+    Ok(StrokeDasharray::Values(results))
+  }
+}
+
+impl ToCss for StrokeDasharray {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    match self {
+      StrokeDasharray::None => dest.write_str("none"),
+      StrokeDasharray::Values(values) => {
+        let mut first = true;
+        for value in values {
+          if first {
+            first = false;
+          } else {
+            dest.write_char(' ')?;
+          }
+          value.to_css_unitless(dest)?;
+        }
+        Ok(())
+      }
+    }
+  }
+}
+
+/// A value for the [marker](https://www.w3.org/TR/SVG2/painting.html#VertexMarkerProperties) properties.
+#[derive(Debug, Clone, PartialEq, Parse, ToCss)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", content = "value", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub enum Marker<'i> {
+  /// No marker.
+  None,
+  /// A url reference to a `<marker>` element.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  Url(Url<'i>),
+}
+
+/// A value for the [color-interpolation](https://www.w3.org/TR/SVG2/painting.html#ColorInterpolation) property.
+#[derive(Debug, Clone, Copy, PartialEq, Parse, ToCss)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(rename_all = "lowercase")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum ColorInterpolation {
+  /// The UA can choose between sRGB or linearRGB.
+  Auto,
+  /// Color interpolation occurs in the sRGB color space.
+  SRGB,
+  /// Color interpolation occurs in the linearized RGB color space
+  LinearRGB,
+}
+
+/// A value for the [color-rendering](https://www.w3.org/TR/SVG2/painting.html#ColorRendering) property.
+#[derive(Debug, Clone, Copy, PartialEq, Parse, ToCss)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(rename_all = "lowercase")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum ColorRendering {
+  /// The UA can choose a tradeoff between speed and quality.
+  Auto,
+  /// The UA shall optimize speed over quality.
+  OptimizeSpeed,
+  /// The UA shall optimize quality over speed.
+  OptimizeQuality,
+}
+
+/// A value for the [shape-rendering](https://www.w3.org/TR/SVG2/painting.html#ShapeRendering) property.
+#[derive(Debug, Clone, Copy, PartialEq, Parse, ToCss)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(rename_all = "lowercase")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum ShapeRendering {
+  /// The UA can choose an appropriate tradeoff.
+  Auto,
+  /// The UA shall optimize speed.
+  OptimizeSpeed,
+  /// The UA shall optimize crisp edges.
+  CrispEdges,
+  /// The UA shall optimize geometric precision.
+  GeometricPrecision,
+}
+
+/// A value for the [text-rendering](https://www.w3.org/TR/SVG2/painting.html#TextRendering) property.
+#[derive(Debug, Clone, Copy, PartialEq, Parse, ToCss)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(rename_all = "lowercase")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum TextRendering {
+  /// The UA can choose an appropriate tradeoff.
+  Auto,
+  /// The UA shall optimize speed.
+  OptimizeSpeed,
+  /// The UA shall optimize legibility.
+  OptimizeLegibility,
+  /// The UA shall optimize geometric precision.
+  GeometricPrecision,
+}
+
+/// A value for the [image-rendering](https://www.w3.org/TR/SVG2/painting.html#ImageRendering) property.
+#[derive(Debug, Clone, Copy, PartialEq, Parse, ToCss)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(rename_all = "lowercase")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum ImageRendering {
+  /// The UA can choose a tradeoff between speed and quality.
+  Auto,
+  /// The UA shall optimize speed over quality.
+  OptimizeSpeed,
+  /// The UA shall optimize quality over speed.
+  OptimizeQuality,
+}
diff --git a/src/properties/text.rs b/src/properties/text.rs
new file mode 100644
index 0000000..622b870
--- /dev/null
+++ b/src/properties/text.rs
@@ -0,0 +1,1550 @@
+//! CSS properties related to text.
+
+#![allow(non_upper_case_globals)]
+
+use super::{Property, PropertyId};
+use crate::compat;
+use crate::context::PropertyHandlerContext;
+use crate::declaration::{DeclarationBlock, DeclarationList};
+use crate::error::{ParserError, PrinterError};
+use crate::macros::{define_shorthand, enum_property};
+use crate::prefixes::Feature;
+use crate::printer::Printer;
+use crate::targets::{should_compile, Browsers, Targets};
+use crate::traits::{FallbackValues, IsCompatible, Parse, PropertyHandler, Shorthand, ToCss, Zero};
+use crate::values::calc::{Calc, MathFunction};
+use crate::values::color::{ColorFallbackKind, CssColor};
+use crate::values::length::{Length, LengthPercentage, LengthValue};
+use crate::values::percentage::Percentage;
+use crate::values::string::CSSString;
+use crate::vendor_prefix::VendorPrefix;
+#[cfg(feature = "visitor")]
+use crate::visitor::Visit;
+use bitflags::bitflags;
+use cssparser::*;
+use smallvec::SmallVec;
+
+enum_property! {
+  /// Defines how text case should be transformed in the
+  /// [text-transform](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#text-transform-property) property.
+  pub enum TextTransformCase {
+    /// Text should not be transformed.
+    None,
+    /// Text should be uppercased.
+    Uppercase,
+    /// Text should be lowercased.
+    Lowercase,
+    /// Each word should be capitalized.
+    Capitalize,
+  }
+}
+
+impl Default for TextTransformCase {
+  fn default() -> TextTransformCase {
+    TextTransformCase::None
+  }
+}
+
+bitflags! {
+  /// Defines how ideographic characters should be transformed in the
+  /// [text-transform](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#text-transform-property) property.
+  ///
+  /// All combinations of flags is supported.
+  #[cfg_attr(feature = "visitor", derive(Visit))]
+  #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(from = "SerializedTextTransformOther", into = "SerializedTextTransformOther"))]
+  #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Clone, Copy)]
+  pub struct TextTransformOther: u8 {
+    /// Puts all typographic character units in full-width form.
+    const FullWidth    = 0b00000001;
+    /// Converts all small Kana characters to the equivalent full-size Kana.
+    const FullSizeKana = 0b00000010;
+  }
+}
+
+impl<'i> Parse<'i> for TextTransformOther {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let location = input.current_source_location();
+    let ident = input.expect_ident()?;
+    match_ignore_ascii_case! { &ident,
+      "full-width" => Ok(TextTransformOther::FullWidth),
+      "full-size-kana" => Ok(TextTransformOther::FullSizeKana),
+      _ => Err(location.new_unexpected_token_error(
+        cssparser::Token::Ident(ident.clone())
+      ))
+    }
+  }
+}
+
+impl ToCss for TextTransformOther {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    let mut needs_space = false;
+    if self.contains(TextTransformOther::FullWidth) {
+      dest.write_str("full-width")?;
+      needs_space = true;
+    }
+
+    if self.contains(TextTransformOther::FullSizeKana) {
+      if needs_space {
+        dest.write_char(' ')?;
+      }
+      dest.write_str("full-size-kana")?;
+    }
+
+    Ok(())
+  }
+}
+
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(rename_all = "camelCase")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+struct SerializedTextTransformOther {
+  /// Puts all typographic character units in full-width form.
+  full_width: bool,
+  /// Converts all small Kana characters to the equivalent full-size Kana.
+  full_size_kana: bool,
+}
+
+impl From<TextTransformOther> for SerializedTextTransformOther {
+  fn from(t: TextTransformOther) -> Self {
+    Self {
+      full_width: t.contains(TextTransformOther::FullWidth),
+      full_size_kana: t.contains(TextTransformOther::FullSizeKana),
+    }
+  }
+}
+
+impl From<SerializedTextTransformOther> for TextTransformOther {
+  fn from(t: SerializedTextTransformOther) -> Self {
+    let mut res = TextTransformOther::empty();
+    if t.full_width {
+      res |= TextTransformOther::FullWidth;
+    }
+    if t.full_size_kana {
+      res |= TextTransformOther::FullSizeKana;
+    }
+    res
+  }
+}
+
+#[cfg(feature = "jsonschema")]
+#[cfg_attr(docsrs, doc(cfg(feature = "jsonschema")))]
+impl<'a> schemars::JsonSchema for TextTransformOther {
+  fn is_referenceable() -> bool {
+    true
+  }
+
+  fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
+    SerializedTextTransformOther::json_schema(gen)
+  }
+
+  fn schema_name() -> String {
+    "TextTransformOther".into()
+  }
+}
+
+/// A value for the [text-transform](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#text-transform-property) property.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub struct TextTransform {
+  /// How case should be transformed.
+  pub case: TextTransformCase,
+  /// How ideographic characters should be transformed.
+  #[cfg_attr(feature = "serde", serde(flatten))]
+  pub other: TextTransformOther,
+}
+
+impl<'i> Parse<'i> for TextTransform {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let mut case = None;
+    let mut other = TextTransformOther::empty();
+
+    loop {
+      if case.is_none() {
+        if let Ok(c) = input.try_parse(TextTransformCase::parse) {
+          case = Some(c);
+          if c == TextTransformCase::None {
+            other = TextTransformOther::empty();
+            break;
+          }
+          continue;
+        }
+      }
+
+      if let Ok(o) = input.try_parse(TextTransformOther::parse) {
+        other |= o;
+        continue;
+      }
+
+      break;
+    }
+
+    Ok(TextTransform {
+      case: case.unwrap_or_default(),
+      other,
+    })
+  }
+}
+
+impl ToCss for TextTransform {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    let mut needs_space = false;
+    if self.case != TextTransformCase::None || self.other.is_empty() {
+      self.case.to_css(dest)?;
+      needs_space = true;
+    }
+
+    if !self.other.is_empty() {
+      if needs_space {
+        dest.write_char(' ')?;
+      }
+      self.other.to_css(dest)?;
+    }
+    Ok(())
+  }
+}
+
+enum_property! {
+  /// A value for the [white-space](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#white-space-property) property.
+  pub enum WhiteSpace {
+    /// Sequences of white space are collapsed into a single character.
+    "normal": Normal,
+    /// White space is not collapsed.
+    "pre": Pre,
+    /// White space is collapsed, but no line wrapping occurs.
+    "nowrap": NoWrap,
+    /// White space is preserved, but line wrapping occurs.
+    "pre-wrap": PreWrap,
+    /// Like pre-wrap, but with different line breaking rules.
+    "break-spaces": BreakSpaces,
+    /// White space is collapsed, but with different line breaking rules.
+    "pre-line": PreLine,
+  }
+}
+
+enum_property! {
+  /// A value for the [word-break](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#word-break-property) property.
+  pub enum WordBreak {
+    /// Words break according to their customary rules.
+    Normal,
+    /// Breaking is forbidden within “words”.
+    KeepAll,
+    /// Breaking is allowed within “words”.
+    BreakAll,
+    /// Breaking is allowed if there is no otherwise acceptable break points in a line.
+    BreakWord,
+  }
+}
+
+enum_property! {
+  /// A value for the [line-break](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#line-break-property) property.
+  pub enum LineBreak {
+    /// The UA determines the set of line-breaking restrictions to use.
+    Auto,
+    /// Breaks text using the least restrictive set of line-breaking rules.
+    Loose,
+    /// Breaks text using the most common set of line-breaking rules.
+    Normal,
+    /// Breaks text using the most stringent set of line-breaking rules.
+    Strict,
+    /// There is a soft wrap opportunity around every typographic character unit.
+    Anywhere,
+  }
+}
+enum_property! {
+  /// A value for the [hyphens](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#hyphenation) property.
+  pub enum Hyphens {
+    /// Words are not hyphenated.
+    None,
+    /// Words are only hyphenated where there are characters inside the word that explicitly suggest hyphenation opportunities.
+    Manual,
+    /// Words may be broken at hyphenation opportunities determined automatically by the UA.
+    Auto,
+  }
+}
+
+enum_property! {
+  /// A value for the [overflow-wrap](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#overflow-wrap-property) property.
+  pub enum OverflowWrap {
+    /// Lines may break only at allowed break points.
+    Normal,
+    /// Breaking is allowed if there is no otherwise acceptable break points in a line.
+    Anywhere,
+    /// As for anywhere except that soft wrap opportunities introduced by break-word are
+    /// not considered when calculating min-content intrinsic sizes.
+    BreakWord,
+  }
+}
+
+enum_property! {
+  /// A value for the [text-align](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#text-align-property) property.
+  pub enum TextAlign {
+    /// Inline-level content is aligned to the start edge of the line box.
+    Start,
+    /// Inline-level content is aligned to the end edge of the line box.
+    End,
+    /// Inline-level content is aligned to the line-left edge of the line box.
+    Left,
+    /// Inline-level content is aligned to the line-right edge of the line box.
+    Right,
+    /// Inline-level content is centered within the line box.
+    Center,
+    /// Text is justified according to the method specified by the text-justify property.
+    Justify,
+    /// Matches the parent element.
+    MatchParent,
+    /// Same as justify, but also justifies the last line.
+    JustifyAll,
+  }
+}
+
+enum_property! {
+  /// A value for the [text-align-last](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#text-align-last-property) property.
+  pub enum TextAlignLast {
+    /// Content on the affected line is aligned per `text-align-all` unless set to `justify`, in which case it is start-aligned.
+    Auto,
+    /// Inline-level content is aligned to the start edge of the line box.
+    Start,
+    /// Inline-level content is aligned to the end edge of the line box.
+    End,
+    /// Inline-level content is aligned to the line-left edge of the line box.
+    Left,
+    /// Inline-level content is aligned to the line-right edge of the line box.
+    Right,
+    /// Inline-level content is centered within the line box.
+    Center,
+    /// Text is justified according to the method specified by the text-justify property.
+    Justify,
+    /// Matches the parent element.
+    MatchParent,
+  }
+}
+
+enum_property! {
+  /// A value for the [text-justify](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#text-justify-property) property.
+  pub enum TextJustify {
+    /// The UA determines the justification algorithm to follow.
+    Auto,
+    /// Justification is disabled.
+    None,
+    /// Justification adjusts spacing at word separators only.
+    InterWord,
+    /// Justification adjusts spacing between each character.
+    InterCharacter,
+  }
+}
+
+/// A value for the [word-spacing](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#word-spacing-property)
+/// and [letter-spacing](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#letter-spacing-property) properties.
+#[derive(Debug, Clone, PartialEq, Parse, ToCss)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", content = "value", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum Spacing {
+  /// No additional spacing is applied.
+  Normal,
+  /// Additional spacing between each word or letter.
+  Length(Length),
+}
+
+/// A value for the [text-indent](https://www.w3.org/TR/2021/CRD-css-text-3-20210422/#text-indent-property) property.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(rename_all = "camelCase")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub struct TextIndent {
+  /// The amount to indent.
+  pub value: LengthPercentage,
+  /// Inverts which lines are affected.
+  pub hanging: bool,
+  /// Affects the first line after each hard break.
+  pub each_line: bool,
+}
+
+impl<'i> Parse<'i> for TextIndent {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let mut value = None;
+    let mut hanging = false;
+    let mut each_line = false;
+
+    loop {
+      if value.is_none() {
+        if let Ok(val) = input.try_parse(LengthPercentage::parse) {
+          value = Some(val);
+          continue;
+        }
+      }
+
+      if !hanging {
+        if input.try_parse(|input| input.expect_ident_matching("hanging")).is_ok() {
+          hanging = true;
+          continue;
+        }
+      }
+
+      if !each_line {
+        if input.try_parse(|input| input.expect_ident_matching("each-line")).is_ok() {
+          each_line = true;
+          continue;
+        }
+      }
+
+      break;
+    }
+
+    if let Some(value) = value {
+      Ok(TextIndent {
+        value,
+        hanging,
+        each_line,
+      })
+    } else {
+      Err(input.new_custom_error(ParserError::InvalidDeclaration))
+    }
+  }
+}
+
+impl ToCss for TextIndent {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    self.value.to_css(dest)?;
+    if self.hanging {
+      dest.write_str(" hanging")?;
+    }
+    if self.each_line {
+      dest.write_str(" each-line")?;
+    }
+    Ok(())
+  }
+}
+
+/// A value for the [text-size-adjust](https://w3c.github.io/csswg-drafts/css-size-adjust/#adjustment-control) property.
+#[derive(Debug, Clone, PartialEq, Parse, ToCss)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", content = "value", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum TextSizeAdjust {
+  /// Use the default size adjustment when displaying on a small device.
+  Auto,
+  /// No size adjustment when displaying on a small device.
+  None,
+  /// When displaying on a small device, the font size is multiplied by this percentage.
+  Percentage(Percentage),
+}
+
+bitflags! {
+  /// A value for the [text-decoration-line](https://www.w3.org/TR/2020/WD-css-text-decor-4-20200506/#text-decoration-line-property) property.
+  ///
+  /// Multiple lines may be specified by combining the flags.
+  #[cfg_attr(feature = "visitor", derive(Visit))]
+  #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(from = "SerializedTextDecorationLine", into = "SerializedTextDecorationLine"))]
+  #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+  #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Clone, Copy)]
+  pub struct TextDecorationLine: u8 {
+    /// Each line of text is underlined.
+    const Underline     = 0b00000001;
+    /// Each line of text has a line over it.
+    const Overline      = 0b00000010;
+    /// Each line of text has a line through the middle.
+    const LineThrough   = 0b00000100;
+    /// The text blinks.
+    const Blink         = 0b00001000;
+    /// The text is decorated as a spelling error.
+    const SpellingError = 0b00010000;
+    /// The text is decorated as a grammar error.
+    const GrammarError  = 0b00100000;
+  }
+}
+
+impl Default for TextDecorationLine {
+  fn default() -> TextDecorationLine {
+    TextDecorationLine::empty()
+  }
+}
+
+impl<'i> Parse<'i> for TextDecorationLine {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let mut value = TextDecorationLine::empty();
+    let mut any = false;
+
+    loop {
+      let flag: Result<_, ParseError<'i, ParserError<'i>>> = input.try_parse(|input| {
+        let location = input.current_source_location();
+        let ident = input.expect_ident()?;
+        Ok(match_ignore_ascii_case! { &ident,
+          "none" if value.is_empty() => TextDecorationLine::empty(),
+          "underline" => TextDecorationLine::Underline,
+          "overline" => TextDecorationLine::Overline,
+          "line-through" => TextDecorationLine::LineThrough,
+          "blink" =>TextDecorationLine::Blink,
+          "spelling-error" if value.is_empty() => TextDecorationLine::SpellingError,
+          "grammar-error" if value.is_empty() => TextDecorationLine::GrammarError,
+          _ => return Err(location.new_unexpected_token_error(
+            cssparser::Token::Ident(ident.clone())
+          ))
+        })
+      });
+
+      if let Ok(flag) = flag {
+        value |= flag;
+        any = true;
+      } else {
+        break;
+      }
+    }
+
+    if !any {
+      return Err(input.new_custom_error(ParserError::InvalidDeclaration));
+    }
+
+    Ok(value)
+  }
+}
+
+impl ToCss for TextDecorationLine {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    if self.is_empty() {
+      return dest.write_str("none");
+    }
+
+    if self.contains(TextDecorationLine::SpellingError) {
+      return dest.write_str("spelling-error");
+    }
+
+    if self.contains(TextDecorationLine::GrammarError) {
+      return dest.write_str("grammar-error");
+    }
+
+    let mut needs_space = false;
+    macro_rules! val {
+      ($val: ident, $str: expr) => {
+        #[allow(unused_assignments)]
+        if self.contains(TextDecorationLine::$val) {
+          if needs_space {
+            dest.write_char(' ')?;
+          }
+          dest.write_str($str)?;
+          needs_space = true;
+        }
+      };
+    }
+
+    val!(Underline, "underline");
+    val!(Overline, "overline");
+    val!(LineThrough, "line-through");
+    val!(Blink, "blink");
+    Ok(())
+  }
+}
+
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(untagged))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+enum SerializedTextDecorationLine {
+  Exclusive(ExclusiveTextDecorationLine),
+  Other(Vec<OtherTextDecorationLine>),
+}
+
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+enum ExclusiveTextDecorationLine {
+  None,
+  SpellingError,
+  GrammarError,
+}
+
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+enum OtherTextDecorationLine {
+  Underline,
+  Overline,
+  LineThrough,
+  Blink,
+}
+
+impl From<TextDecorationLine> for SerializedTextDecorationLine {
+  fn from(l: TextDecorationLine) -> Self {
+    if l.is_empty() {
+      return Self::Exclusive(ExclusiveTextDecorationLine::None);
+    }
+
+    macro_rules! exclusive {
+      ($t: ident) => {
+        if l.contains(TextDecorationLine::$t) {
+          return Self::Exclusive(ExclusiveTextDecorationLine::$t);
+        }
+      };
+    }
+
+    exclusive!(SpellingError);
+    exclusive!(GrammarError);
+
+    let mut v = Vec::new();
+    macro_rules! other {
+      ($t: ident) => {
+        if l.contains(TextDecorationLine::$t) {
+          v.push(OtherTextDecorationLine::$t)
+        }
+      };
+    }
+
+    other!(Underline);
+    other!(Overline);
+    other!(LineThrough);
+    other!(Blink);
+    Self::Other(v)
+  }
+}
+
+impl From<SerializedTextDecorationLine> for TextDecorationLine {
+  fn from(l: SerializedTextDecorationLine) -> Self {
+    match l {
+      SerializedTextDecorationLine::Exclusive(v) => match v {
+        ExclusiveTextDecorationLine::None => TextDecorationLine::empty(),
+        ExclusiveTextDecorationLine::SpellingError => TextDecorationLine::SpellingError,
+        ExclusiveTextDecorationLine::GrammarError => TextDecorationLine::GrammarError,
+      },
+      SerializedTextDecorationLine::Other(v) => {
+        let mut res = TextDecorationLine::empty();
+        for val in v {
+          res |= match val {
+            OtherTextDecorationLine::Underline => TextDecorationLine::Underline,
+            OtherTextDecorationLine::Overline => TextDecorationLine::Overline,
+            OtherTextDecorationLine::LineThrough => TextDecorationLine::LineThrough,
+            OtherTextDecorationLine::Blink => TextDecorationLine::Blink,
+          }
+        }
+        res
+      }
+    }
+  }
+}
+
+#[cfg(feature = "jsonschema")]
+#[cfg_attr(docsrs, doc(cfg(feature = "jsonschema")))]
+impl<'a> schemars::JsonSchema for TextDecorationLine {
+  fn is_referenceable() -> bool {
+    true
+  }
+
+  fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
+    SerializedTextDecorationLine::json_schema(gen)
+  }
+
+  fn schema_name() -> String {
+    "TextDecorationLine".into()
+  }
+}
+
+enum_property! {
+  /// A value for the [text-decoration-style](https://www.w3.org/TR/2020/WD-css-text-decor-4-20200506/#text-decoration-style-property) property.
+  pub enum TextDecorationStyle {
+    /// A single line segment.
+    Solid,
+    /// Two parallel solid lines with some space between them.
+    Double,
+    /// A series of round dots.
+    Dotted,
+    /// A series of square-ended dashes.
+    Dashed,
+    /// A wavy line.
+    Wavy,
+  }
+}
+
+impl Default for TextDecorationStyle {
+  fn default() -> TextDecorationStyle {
+    TextDecorationStyle::Solid
+  }
+}
+
+/// A value for the [text-decoration-thickness](https://www.w3.org/TR/2020/WD-css-text-decor-4-20200506/#text-decoration-width-property) property.
+#[derive(Debug, Clone, PartialEq, Parse, ToCss)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", content = "value", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum TextDecorationThickness {
+  /// The UA chooses an appropriate thickness for text decoration lines.
+  Auto,
+  /// Use the thickness defined in the current font.
+  FromFont,
+  /// An explicit length.
+  LengthPercentage(LengthPercentage),
+}
+
+impl Default for TextDecorationThickness {
+  fn default() -> TextDecorationThickness {
+    TextDecorationThickness::Auto
+  }
+}
+
+define_shorthand! {
+  /// A value for the [text-decoration](https://www.w3.org/TR/2020/WD-css-text-decor-4-20200506/#text-decoration-property) shorthand property.
+  pub struct TextDecoration(VendorPrefix) {
+    /// The lines to display.
+    line: TextDecorationLine(TextDecorationLine, VendorPrefix),
+    /// The thickness of the lines.
+    thickness: TextDecorationThickness(TextDecorationThickness),
+    /// The style of the lines.
+    style: TextDecorationStyle(TextDecorationStyle, VendorPrefix),
+    /// The color of the lines.
+    color: TextDecorationColor(CssColor, VendorPrefix),
+  }
+}
+
+impl<'i> Parse<'i> for TextDecoration {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let mut line = None;
+    let mut thickness = None;
+    let mut style = None;
+    let mut color = None;
+
+    loop {
+      macro_rules! prop {
+        ($key: ident, $type: ident) => {
+          if $key.is_none() {
+            if let Ok(val) = input.try_parse($type::parse) {
+              $key = Some(val);
+              continue;
+            }
+          }
+        };
+      }
+
+      prop!(line, TextDecorationLine);
+      prop!(thickness, TextDecorationThickness);
+      prop!(style, TextDecorationStyle);
+      prop!(color, CssColor);
+      break;
+    }
+
+    Ok(TextDecoration {
+      line: line.unwrap_or_default(),
+      thickness: thickness.unwrap_or_default(),
+      style: style.unwrap_or_default(),
+      color: color.unwrap_or(CssColor::current_color()),
+    })
+  }
+}
+
+impl ToCss for TextDecoration {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    self.line.to_css(dest)?;
+    if self.line.is_empty() {
+      return Ok(());
+    }
+
+    let mut needs_space = true;
+    if self.thickness != TextDecorationThickness::default() {
+      dest.write_char(' ')?;
+      self.thickness.to_css(dest)?;
+      needs_space = true;
+    }
+
+    if self.style != TextDecorationStyle::default() {
+      if needs_space {
+        dest.write_char(' ')?;
+      }
+      self.style.to_css(dest)?;
+      needs_space = true;
+    }
+
+    if self.color != CssColor::current_color() {
+      if needs_space {
+        dest.write_char(' ')?;
+      }
+      self.color.to_css(dest)?;
+    }
+
+    Ok(())
+  }
+}
+
+impl FallbackValues for TextDecoration {
+  fn get_fallbacks(&mut self, targets: Targets) -> Vec<Self> {
+    self
+      .color
+      .get_fallbacks(targets)
+      .into_iter()
+      .map(|color| TextDecoration { color, ..self.clone() })
+      .collect()
+  }
+}
+
+enum_property! {
+  /// A value for the [text-decoration-skip-ink](https://www.w3.org/TR/2020/WD-css-text-decor-4-20200506/#text-decoration-skip-ink-property) property.
+  pub enum TextDecorationSkipInk {
+    /// UAs may interrupt underlines and overlines.
+    Auto,
+    /// UAs must interrupt underlines and overlines.
+    None,
+    /// UA must draw continuous underlines and overlines.
+    All,
+  }
+}
+
+enum_property! {
+  /// A keyword for the [text-emphasis-style](https://www.w3.org/TR/2020/WD-css-text-decor-4-20200506/#text-emphasis-style-property) property.
+  ///
+  /// See [TextEmphasisStyle](TextEmphasisStyle).
+  pub enum TextEmphasisFillMode {
+    /// The shape is filled with solid color.
+    Filled,
+    /// The shape is hollow.
+    Open,
+  }
+}
+
+enum_property! {
+  /// A text emphasis shape for the [text-emphasis-style](https://www.w3.org/TR/2020/WD-css-text-decor-4-20200506/#text-emphasis-style-property) property.
+  ///
+  /// See [TextEmphasisStyle](TextEmphasisStyle).
+  pub enum TextEmphasisShape {
+    /// Display small circles as marks.
+    Dot,
+    /// Display large circles as marks.
+    Circle,
+    /// Display double circles as marks.
+    DoubleCircle,
+    /// Display triangles as marks.
+    Triangle,
+    /// Display sesames as marks.
+    Sesame,
+  }
+}
+
+/// A value for the [text-emphasis-style](https://www.w3.org/TR/2020/WD-css-text-decor-4-20200506/#text-emphasis-style-property) property.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub enum TextEmphasisStyle<'i> {
+  /// No emphasis.
+  None,
+  /// Defines the fill and shape of the marks.
+  Keyword {
+    /// The fill mode for the marks.
+    fill: TextEmphasisFillMode,
+    /// The shape of the marks.
+    shape: Option<TextEmphasisShape>,
+  },
+  /// Display the given string as marks.
+  #[cfg_attr(
+    feature = "serde",
+    serde(borrow, with = "crate::serialization::ValueWrapper::<CSSString>")
+  )]
+  String(CSSString<'i>),
+}
+
+impl<'i> Default for TextEmphasisStyle<'i> {
+  fn default() -> TextEmphasisStyle<'i> {
+    TextEmphasisStyle::None
+  }
+}
+
+impl<'i> Parse<'i> for TextEmphasisStyle<'i> {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    if input.try_parse(|input| input.expect_ident_matching("none")).is_ok() {
+      return Ok(TextEmphasisStyle::None);
+    }
+
+    if let Ok(s) = input.try_parse(CSSString::parse) {
+      return Ok(TextEmphasisStyle::String(s));
+    }
+
+    let mut shape = input.try_parse(TextEmphasisShape::parse).ok();
+    let fill = input.try_parse(TextEmphasisFillMode::parse).ok();
+    if shape.is_none() {
+      shape = input.try_parse(TextEmphasisShape::parse).ok();
+    }
+
+    if shape.is_none() && fill.is_none() {
+      return Err(input.new_custom_error(ParserError::InvalidDeclaration));
+    }
+
+    let fill = fill.unwrap_or(TextEmphasisFillMode::Filled);
+    Ok(TextEmphasisStyle::Keyword { fill, shape })
+  }
+}
+
+impl<'i> ToCss for TextEmphasisStyle<'i> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    match self {
+      TextEmphasisStyle::None => dest.write_str("none"),
+      TextEmphasisStyle::String(s) => s.to_css(dest),
+      TextEmphasisStyle::Keyword { fill, shape } => {
+        let mut needs_space = false;
+        if *fill != TextEmphasisFillMode::Filled || shape.is_none() {
+          fill.to_css(dest)?;
+          needs_space = true;
+        }
+
+        if let Some(shape) = shape {
+          if needs_space {
+            dest.write_char(' ')?;
+          }
+          shape.to_css(dest)?;
+        }
+        Ok(())
+      }
+    }
+  }
+}
+
+define_shorthand! {
+  /// A value for the [text-emphasis](https://www.w3.org/TR/2020/WD-css-text-decor-4-20200506/#text-emphasis-property) shorthand property.
+  pub struct TextEmphasis<'i>(VendorPrefix) {
+    /// The text emphasis style.
+    #[cfg_attr(feature = "serde", serde(borrow))]
+    style: TextEmphasisStyle(TextEmphasisStyle<'i>, VendorPrefix),
+    /// The text emphasis color.
+    color: TextEmphasisColor(CssColor, VendorPrefix),
+  }
+}
+
+impl<'i> Parse<'i> for TextEmphasis<'i> {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let mut style = None;
+    let mut color = None;
+
+    loop {
+      if style.is_none() {
+        if let Ok(s) = input.try_parse(TextEmphasisStyle::parse) {
+          style = Some(s);
+          continue;
+        }
+      }
+
+      if color.is_none() {
+        if let Ok(c) = input.try_parse(CssColor::parse) {
+          color = Some(c);
+          continue;
+        }
+      }
+
+      break;
+    }
+
+    Ok(TextEmphasis {
+      style: style.unwrap_or_default(),
+      color: color.unwrap_or(CssColor::current_color()),
+    })
+  }
+}
+
+impl<'i> ToCss for TextEmphasis<'i> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    self.style.to_css(dest)?;
+
+    if self.style != TextEmphasisStyle::None && self.color != CssColor::current_color() {
+      dest.write_char(' ')?;
+      self.color.to_css(dest)?;
+    }
+
+    Ok(())
+  }
+}
+
+impl<'i> FallbackValues for TextEmphasis<'i> {
+  fn get_fallbacks(&mut self, targets: Targets) -> Vec<Self> {
+    self
+      .color
+      .get_fallbacks(targets)
+      .into_iter()
+      .map(|color| TextEmphasis { color, ..self.clone() })
+      .collect()
+  }
+}
+
+enum_property! {
+  /// A vertical position keyword for the [text-emphasis-position](https://www.w3.org/TR/2020/WD-css-text-decor-4-20200506/#text-emphasis-position-property) property.
+  ///
+  /// See [TextEmphasisPosition](TextEmphasisPosition).
+  pub enum TextEmphasisPositionVertical {
+    /// Draw marks over the text in horizontal typographic modes.
+    Over,
+    /// Draw marks under the text in horizontal typographic modes.
+    Under,
+  }
+}
+
+enum_property! {
+  /// A horizontal position keyword for the [text-emphasis-position](https://www.w3.org/TR/2020/WD-css-text-decor-4-20200506/#text-emphasis-position-property) property.
+  ///
+  /// See [TextEmphasisPosition](TextEmphasisPosition).
+  pub enum TextEmphasisPositionHorizontal {
+    /// Draw marks to the right of the text in vertical typographic modes.
+    Left,
+    /// Draw marks to the left of the text in vertical typographic modes.
+    Right,
+  }
+}
+
+/// A value for the [text-emphasis-position](https://www.w3.org/TR/2020/WD-css-text-decor-4-20200506/#text-emphasis-position-property) property.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub struct TextEmphasisPosition {
+  /// The vertical position.
+  pub vertical: TextEmphasisPositionVertical,
+  /// The horizontal position.
+  pub horizontal: TextEmphasisPositionHorizontal,
+}
+
+impl<'i> Parse<'i> for TextEmphasisPosition {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    if let Ok(horizontal) = input.try_parse(TextEmphasisPositionHorizontal::parse) {
+      let vertical = TextEmphasisPositionVertical::parse(input)?;
+      Ok(TextEmphasisPosition { horizontal, vertical })
+    } else {
+      let vertical = TextEmphasisPositionVertical::parse(input)?;
+      let horizontal = input
+        .try_parse(TextEmphasisPositionHorizontal::parse)
+        .unwrap_or(TextEmphasisPositionHorizontal::Right);
+      Ok(TextEmphasisPosition { horizontal, vertical })
+    }
+  }
+}
+
+enum_property! {
+  /// A value for the [box-decoration-break](https://www.w3.org/TR/css-break-3/#break-decoration) property.
+  pub enum BoxDecorationBreak {
+    /// The element is rendered with no breaks present, and then sliced by the breaks afterward.
+    Slice,
+    /// Each box fragment is independently wrapped with the border, padding, and margin.
+    Clone,
+  }
+}
+
+impl Default for BoxDecorationBreak {
+  fn default() -> Self {
+    BoxDecorationBreak::Slice
+  }
+}
+
+impl ToCss for TextEmphasisPosition {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    self.vertical.to_css(dest)?;
+    if self.horizontal != TextEmphasisPositionHorizontal::Right {
+      dest.write_char(' ')?;
+      self.horizontal.to_css(dest)?;
+    }
+    Ok(())
+  }
+}
+
+#[derive(Default)]
+pub(crate) struct TextDecorationHandler<'i> {
+  line: Option<(TextDecorationLine, VendorPrefix)>,
+  thickness: Option<TextDecorationThickness>,
+  style: Option<(TextDecorationStyle, VendorPrefix)>,
+  color: Option<(CssColor, VendorPrefix)>,
+  emphasis_style: Option<(TextEmphasisStyle<'i>, VendorPrefix)>,
+  emphasis_color: Option<(CssColor, VendorPrefix)>,
+  emphasis_position: Option<(TextEmphasisPosition, VendorPrefix)>,
+  has_any: bool,
+}
+
+impl<'i> PropertyHandler<'i> for TextDecorationHandler<'i> {
+  fn handle_property(
+    &mut self,
+    property: &Property<'i>,
+    dest: &mut DeclarationList<'i>,
+    context: &mut PropertyHandlerContext<'i, '_>,
+  ) -> bool {
+    use Property::*;
+
+    macro_rules! maybe_flush {
+      ($prop: ident, $val: expr, $vp: expr) => {{
+        // If two vendor prefixes for the same property have different
+        // values, we need to flush what we have immediately to preserve order.
+        if let Some((val, prefixes)) = &self.$prop {
+          if val != $val && !prefixes.contains(*$vp) {
+            self.finalize(dest, context);
+          }
+        }
+      }};
+    }
+
+    macro_rules! property {
+      ($prop: ident, $val: expr, $vp: expr) => {{
+        maybe_flush!($prop, $val, $vp);
+
+        // Otherwise, update the value and add the prefix.
+        if let Some((val, prefixes)) = &mut self.$prop {
+          *val = $val.clone();
+          *prefixes |= *$vp;
+        } else {
+          self.$prop = Some(($val.clone(), *$vp));
+          self.has_any = true;
+        }
+      }};
+    }
+
+    match property {
+      TextDecorationLine(val, vp) => property!(line, val, vp),
+      TextDecorationThickness(val) => {
+        self.thickness = Some(val.clone());
+        self.has_any = true;
+      }
+      TextDecorationStyle(val, vp) => property!(style, val, vp),
+      TextDecorationColor(val, vp) => property!(color, val, vp),
+      TextDecoration(val, vp) => {
+        maybe_flush!(line, &val.line, vp);
+        maybe_flush!(style, &val.style, vp);
+        maybe_flush!(color, &val.color, vp);
+        property!(line, &val.line, vp);
+        self.thickness = Some(val.thickness.clone());
+        property!(style, &val.style, vp);
+        property!(color, &val.color, vp);
+      }
+      TextEmphasisStyle(val, vp) => property!(emphasis_style, val, vp),
+      TextEmphasisColor(val, vp) => property!(emphasis_color, val, vp),
+      TextEmphasis(val, vp) => {
+        maybe_flush!(emphasis_style, &val.style, vp);
+        maybe_flush!(emphasis_color, &val.color, vp);
+        property!(emphasis_style, &val.style, vp);
+        property!(emphasis_color, &val.color, vp);
+      }
+      TextEmphasisPosition(val, vp) => property!(emphasis_position, val, vp),
+      TextAlign(align) => {
+        use super::text::*;
+        macro_rules! logical {
+          ($ltr: ident, $rtl: ident) => {{
+            let logical_supported = !context.should_compile_logical(compat::Feature::LogicalTextAlign);
+            if logical_supported {
+              dest.push(property.clone());
+            } else {
+              context.add_logical_rule(
+                Property::TextAlign(TextAlign::$ltr),
+                Property::TextAlign(TextAlign::$rtl),
+              );
+            }
+          }};
+        }
+
+        match align {
+          TextAlign::Start => logical!(Left, Right),
+          TextAlign::End => logical!(Right, Left),
+          _ => dest.push(property.clone()),
+        }
+      }
+      Unparsed(val) if is_text_decoration_property(&val.property_id) => {
+        self.finalize(dest, context);
+        let mut unparsed = val.get_prefixed(context.targets, Feature::TextDecoration);
+        context.add_unparsed_fallbacks(&mut unparsed);
+        dest.push(Property::Unparsed(unparsed))
+      }
+      Unparsed(val) if is_text_emphasis_property(&val.property_id) => {
+        self.finalize(dest, context);
+        let mut unparsed = val.get_prefixed(context.targets, Feature::TextEmphasis);
+        context.add_unparsed_fallbacks(&mut unparsed);
+        dest.push(Property::Unparsed(unparsed))
+      }
+      _ => return false,
+    }
+
+    true
+  }
+
+  fn finalize(&mut self, dest: &mut DeclarationList<'i>, context: &mut PropertyHandlerContext<'i, '_>) {
+    if !self.has_any {
+      return;
+    }
+
+    self.has_any = false;
+
+    let mut line = std::mem::take(&mut self.line);
+    let mut thickness = std::mem::take(&mut self.thickness);
+    let mut style = std::mem::take(&mut self.style);
+    let mut color = std::mem::take(&mut self.color);
+    let mut emphasis_style = std::mem::take(&mut self.emphasis_style);
+    let mut emphasis_color = std::mem::take(&mut self.emphasis_color);
+    let emphasis_position = std::mem::take(&mut self.emphasis_position);
+
+    if let (Some((line, line_vp)), Some(thickness_val), Some((style, style_vp)), Some((color, color_vp))) =
+      (&mut line, &mut thickness, &mut style, &mut color)
+    {
+      let intersection = *line_vp | *style_vp | *color_vp;
+      if !intersection.is_empty() {
+        let mut prefix = intersection;
+
+        // Some browsers don't support thickness in the shorthand property yet.
+        let supports_thickness = context.targets.is_compatible(compat::Feature::TextDecorationThicknessShorthand);
+        let mut decoration = TextDecoration {
+          line: line.clone(),
+          thickness: if supports_thickness {
+            thickness_val.clone()
+          } else {
+            TextDecorationThickness::default()
+          },
+          style: style.clone(),
+          color: color.clone(),
+        };
+
+        // Only add prefixes if one of the new sub-properties was used
+        if prefix.contains(VendorPrefix::None)
+          && (*style != TextDecorationStyle::default() || *color != CssColor::current_color())
+        {
+          prefix = context.targets.prefixes(VendorPrefix::None, Feature::TextDecoration);
+
+          let fallbacks = decoration.get_fallbacks(context.targets);
+          for fallback in fallbacks {
+            dest.push(Property::TextDecoration(fallback, prefix))
+          }
+        }
+
+        dest.push(Property::TextDecoration(decoration, prefix));
+        line_vp.remove(intersection);
+        style_vp.remove(intersection);
+        color_vp.remove(intersection);
+        if supports_thickness || *thickness_val == TextDecorationThickness::default() {
+          thickness = None;
+        }
+      }
+    }
+
+    macro_rules! color {
+      ($key: ident, $prop: ident) => {
+        if let Some((mut val, vp)) = $key {
+          if !vp.is_empty() {
+            let prefix = context.targets.prefixes(vp, Feature::$prop);
+            if prefix.contains(VendorPrefix::None) {
+              let fallbacks = val.get_fallbacks(context.targets);
+              for fallback in fallbacks {
+                dest.push(Property::$prop(fallback, prefix))
+              }
+            }
+            dest.push(Property::$prop(val, prefix))
+          }
+        }
+      };
+    }
+
+    macro_rules! single_property {
+      ($key: ident, $prop: ident) => {
+        if let Some((val, vp)) = $key {
+          if !vp.is_empty() {
+            let prefix = context.targets.prefixes(vp, Feature::$prop);
+            dest.push(Property::$prop(val, prefix))
+          }
+        }
+      };
+    }
+
+    single_property!(line, TextDecorationLine);
+    single_property!(style, TextDecorationStyle);
+    color!(color, TextDecorationColor);
+
+    if let Some(thickness) = thickness {
+      // Percentages in the text-decoration-thickness property are based on 1em.
+      // If unsupported, compile this to a calc() instead.
+      match thickness {
+        TextDecorationThickness::LengthPercentage(LengthPercentage::Percentage(p))
+          if should_compile!(context.targets, TextDecorationThicknessPercent) =>
+        {
+          let calc = Calc::Function(Box::new(MathFunction::Calc(Calc::Product(
+            p.0,
+            Box::new(Calc::Value(Box::new(LengthPercentage::Dimension(LengthValue::Em(1.0))))),
+          ))));
+          let thickness = TextDecorationThickness::LengthPercentage(LengthPercentage::Calc(Box::new(calc)));
+          dest.push(Property::TextDecorationThickness(thickness));
+        }
+        thickness => dest.push(Property::TextDecorationThickness(thickness)),
+      }
+    }
+
+    if let (Some((style, style_vp)), Some((color, color_vp))) = (&mut emphasis_style, &mut emphasis_color) {
+      let intersection = *style_vp | *color_vp;
+      if !intersection.is_empty() {
+        let prefix = context.targets.prefixes(intersection, Feature::TextEmphasis);
+        let mut emphasis = TextEmphasis {
+          style: style.clone(),
+          color: color.clone(),
+        };
+
+        if prefix.contains(VendorPrefix::None) {
+          let fallbacks = emphasis.get_fallbacks(context.targets);
+          for fallback in fallbacks {
+            dest.push(Property::TextEmphasis(fallback, prefix))
+          }
+        }
+
+        dest.push(Property::TextEmphasis(emphasis, prefix));
+        style_vp.remove(intersection);
+        color_vp.remove(intersection);
+      }
+    }
+
+    single_property!(emphasis_style, TextEmphasisStyle);
+    color!(emphasis_color, TextEmphasisColor);
+
+    if let Some((pos, vp)) = emphasis_position {
+      if !vp.is_empty() {
+        let mut prefix = context.targets.prefixes(vp, Feature::TextEmphasisPosition);
+        // Prefixed version does not support horizontal keyword.
+        if pos.horizontal != TextEmphasisPositionHorizontal::Right {
+          prefix = VendorPrefix::None;
+        }
+        dest.push(Property::TextEmphasisPosition(pos, prefix))
+      }
+    }
+  }
+}
+
+/// A value for the [text-shadow](https://www.w3.org/TR/2020/WD-css-text-decor-4-20200506/#text-shadow-property) property.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(rename_all = "camelCase")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub struct TextShadow {
+  /// The color of the text shadow.
+  pub color: CssColor,
+  /// The x offset of the text shadow.
+  pub x_offset: Length,
+  /// The y offset of the text shadow.
+  pub y_offset: Length,
+  /// The blur radius of the text shadow.
+  pub blur: Length,
+  /// The spread distance of the text shadow.
+  pub spread: Length, // added in Level 4 spec
+}
+
+impl<'i> Parse<'i> for TextShadow {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let mut color = None;
+    let mut lengths = None;
+
+    loop {
+      if lengths.is_none() {
+        let value = input.try_parse::<_, _, ParseError<ParserError<'i>>>(|input| {
+          let horizontal = Length::parse(input)?;
+          let vertical = Length::parse(input)?;
+          let blur = input.try_parse(Length::parse).unwrap_or(Length::zero());
+          let spread = input.try_parse(Length::parse).unwrap_or(Length::zero());
+          Ok((horizontal, vertical, blur, spread))
+        });
+
+        if let Ok(value) = value {
+          lengths = Some(value);
+          continue;
+        }
+      }
+
+      if color.is_none() {
+        if let Ok(value) = input.try_parse(CssColor::parse) {
+          color = Some(value);
+          continue;
+        }
+      }
+
+      break;
+    }
+
+    let lengths = lengths.ok_or(input.new_error(BasicParseErrorKind::QualifiedRuleInvalid))?;
+    Ok(TextShadow {
+      color: color.unwrap_or(CssColor::current_color()),
+      x_offset: lengths.0,
+      y_offset: lengths.1,
+      blur: lengths.2,
+      spread: lengths.3,
+    })
+  }
+}
+
+impl ToCss for TextShadow {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    self.x_offset.to_css(dest)?;
+    dest.write_char(' ')?;
+    self.y_offset.to_css(dest)?;
+
+    if self.blur != Length::zero() || self.spread != Length::zero() {
+      dest.write_char(' ')?;
+      self.blur.to_css(dest)?;
+
+      if self.spread != Length::zero() {
+        dest.write_char(' ')?;
+        self.spread.to_css(dest)?;
+      }
+    }
+
+    if self.color != CssColor::current_color() {
+      dest.write_char(' ')?;
+      self.color.to_css(dest)?;
+    }
+
+    Ok(())
+  }
+}
+
+impl IsCompatible for TextShadow {
+  fn is_compatible(&self, browsers: Browsers) -> bool {
+    self.color.is_compatible(browsers)
+      && self.x_offset.is_compatible(browsers)
+      && self.y_offset.is_compatible(browsers)
+      && self.blur.is_compatible(browsers)
+      && self.spread.is_compatible(browsers)
+  }
+}
+
+#[inline]
+fn is_text_decoration_property(property_id: &PropertyId) -> bool {
+  match property_id {
+    PropertyId::TextDecorationLine(_)
+    | PropertyId::TextDecorationThickness
+    | PropertyId::TextDecorationStyle(_)
+    | PropertyId::TextDecorationColor(_)
+    | PropertyId::TextDecoration(_) => true,
+    _ => false,
+  }
+}
+
+#[inline]
+fn is_text_emphasis_property(property_id: &PropertyId) -> bool {
+  match property_id {
+    PropertyId::TextEmphasisStyle(_)
+    | PropertyId::TextEmphasisColor(_)
+    | PropertyId::TextEmphasis(_)
+    | PropertyId::TextEmphasisPosition(_) => true,
+    _ => false,
+  }
+}
+
+impl FallbackValues for SmallVec<[TextShadow; 1]> {
+  fn get_fallbacks(&mut self, targets: Targets) -> Vec<Self> {
+    let mut fallbacks = ColorFallbackKind::empty();
+    for shadow in self.iter() {
+      fallbacks |= shadow.color.get_necessary_fallbacks(targets);
+    }
+
+    let mut res = Vec::new();
+    if fallbacks.contains(ColorFallbackKind::RGB) {
+      let rgb = self
+        .iter()
+        .map(|shadow| TextShadow {
+          color: shadow.color.to_rgb().unwrap(),
+          ..shadow.clone()
+        })
+        .collect();
+      res.push(rgb);
+    }
+
+    if fallbacks.contains(ColorFallbackKind::P3) {
+      let p3 = self
+        .iter()
+        .map(|shadow| TextShadow {
+          color: shadow.color.to_p3().unwrap(),
+          ..shadow.clone()
+        })
+        .collect();
+      res.push(p3);
+    }
+
+    if fallbacks.contains(ColorFallbackKind::LAB) {
+      for shadow in self.iter_mut() {
+        shadow.color = shadow.color.to_lab().unwrap();
+      }
+    }
+
+    res
+  }
+}
+
+enum_property! {
+  /// A value for the [direction](https://drafts.csswg.org/css-writing-modes-3/#direction) property.
+  pub enum Direction {
+    /// This value sets inline base direction (bidi directionality) to line-left-to-line-right.
+    Ltr,
+    /// This value sets inline base direction (bidi directionality) to line-right-to-line-left.
+    Rtl,
+  }
+}
+
+enum_property! {
+  /// A value for the [unicode-bidi](https://drafts.csswg.org/css-writing-modes-3/#unicode-bidi) property.
+  pub enum UnicodeBidi {
+    /// The box does not open an additional level of embedding.
+    Normal,
+    /// If the box is inline, this value creates a directional embedding by opening an additional level of embedding.
+    Embed,
+    /// On an inline box, this bidi-isolates its contents.
+    Isolate,
+    /// This value puts the box’s immediate inline content in a directional override.
+    BidiOverride,
+    /// This combines the isolation behavior of isolate with the directional override behavior of bidi-override.
+    IsolateOverride,
+    /// This value behaves as isolate except that the base directionality is determined using a heuristic rather than the direction property.
+    Plaintext,
+  }
+}
diff --git a/src/properties/transform.rs b/src/properties/transform.rs
new file mode 100644
index 0000000..c8f5a7f
--- /dev/null
+++ b/src/properties/transform.rs
@@ -0,0 +1,1804 @@
+//! CSS properties related to 2D and 3D transforms.
+
+use super::{Property, PropertyId};
+use crate::context::PropertyHandlerContext;
+use crate::declaration::DeclarationList;
+use crate::error::{ParserError, PrinterError};
+use crate::macros::enum_property;
+use crate::prefixes::Feature;
+use crate::printer::Printer;
+use crate::stylesheet::PrinterOptions;
+use crate::traits::{Parse, PropertyHandler, ToCss, Zero};
+use crate::values::{
+  angle::Angle,
+  length::{Length, LengthPercentage},
+  percentage::NumberOrPercentage,
+};
+use crate::vendor_prefix::VendorPrefix;
+#[cfg(feature = "visitor")]
+use crate::visitor::Visit;
+use cssparser::*;
+use std::f32::consts::PI;
+
+/// A value for the [transform](https://www.w3.org/TR/2019/CR-css-transforms-1-20190214/#propdef-transform) property.
+#[derive(Debug, Clone, PartialEq, Default)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(transparent))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub struct TransformList(pub Vec<Transform>);
+
+impl<'i> Parse<'i> for TransformList {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    if input.try_parse(|input| input.expect_ident_matching("none")).is_ok() {
+      return Ok(TransformList(vec![]));
+    }
+
+    input.skip_whitespace();
+    let mut results = vec![Transform::parse(input)?];
+    loop {
+      input.skip_whitespace();
+      if let Ok(item) = input.try_parse(Transform::parse) {
+        results.push(item);
+      } else {
+        return Ok(TransformList(results));
+      }
+    }
+  }
+}
+
+impl ToCss for TransformList {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    if self.0.is_empty() {
+      dest.write_str("none")?;
+      return Ok(());
+    }
+
+    // TODO: Re-enable with a better solution
+    //       See: https://github.com/parcel-bundler/lightningcss/issues/288
+    if dest.minify {
+      let mut base = String::new();
+      self.to_css_base(&mut Printer::new(
+        &mut base,
+        PrinterOptions {
+          minify: true,
+          ..PrinterOptions::default()
+        },
+      ))?;
+
+      dest.write_str(&base)?;
+
+      return Ok(());
+    }
+    // if dest.minify {
+    //   // Combine transforms into a single matrix.
+    //   if let Some(matrix) = self.to_matrix() {
+    //     // Generate based on the original transforms.
+    //     let mut base = String::new();
+    //     self.to_css_base(&mut Printer::new(
+    //       &mut base,
+    //       PrinterOptions {
+    //         minify: true,
+    //         ..PrinterOptions::default()
+    //       },
+    //     ))?;
+    //
+    //     // Decompose the matrix into transform functions if possible.
+    //     // If the resulting length is shorter than the original, use it.
+    //     if let Some(d) = matrix.decompose() {
+    //       let mut decomposed = String::new();
+    //       d.to_css_base(&mut Printer::new(
+    //         &mut decomposed,
+    //         PrinterOptions {
+    //           minify: true,
+    //           ..PrinterOptions::default()
+    //         },
+    //       ))?;
+    //       if decomposed.len() < base.len() {
+    //         base = decomposed;
+    //       }
+    //     }
+    //
+    //     // Also generate a matrix() or matrix3d() representation and compare that.
+    //     let mut mat = String::new();
+    //     if let Some(matrix) = matrix.to_matrix2d() {
+    //       Transform::Matrix(matrix).to_css(&mut Printer::new(
+    //         &mut mat,
+    //         PrinterOptions {
+    //           minify: true,
+    //           ..PrinterOptions::default()
+    //         },
+    //       ))?
+    //     } else {
+    //       Transform::Matrix3d(matrix).to_css(&mut Printer::new(
+    //         &mut mat,
+    //         PrinterOptions {
+    //           minify: true,
+    //           ..PrinterOptions::default()
+    //         },
+    //       ))?
+    //     }
+    //
+    //     if mat.len() < base.len() {
+    //       dest.write_str(&mat)?;
+    //     } else {
+    //       dest.write_str(&base)?;
+    //     }
+    //
+    //     return Ok(());
+    //   }
+    // }
+
+    self.to_css_base(dest)
+  }
+}
+
+impl TransformList {
+  fn to_css_base<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    for item in &self.0 {
+      item.to_css(dest)?;
+    }
+    Ok(())
+  }
+
+  /// Converts the transform list to a 3D matrix if possible.
+  pub fn to_matrix(&self) -> Option<Matrix3d<f32>> {
+    let mut matrix = Matrix3d::identity();
+    for transform in &self.0 {
+      if let Some(m) = transform.to_matrix() {
+        matrix = m.multiply(&matrix);
+      } else {
+        return None;
+      }
+    }
+    Some(matrix)
+  }
+}
+
+/// An individual [transform function](https://www.w3.org/TR/2019/CR-css-transforms-1-20190214/#two-d-transform-functions).
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", content = "value", rename_all = "camelCase")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum Transform {
+  /// A 2D translation.
+  Translate(LengthPercentage, LengthPercentage),
+  /// A translation in the X direction.
+  TranslateX(LengthPercentage),
+  /// A translation in the Y direction.
+  TranslateY(LengthPercentage),
+  /// A translation in the Z direction.
+  TranslateZ(Length),
+  /// A 3D translation.
+  Translate3d(LengthPercentage, LengthPercentage, Length),
+  /// A 2D scale.
+  Scale(NumberOrPercentage, NumberOrPercentage),
+  /// A scale in the X direction.
+  ScaleX(NumberOrPercentage),
+  /// A scale in the Y direction.
+  ScaleY(NumberOrPercentage),
+  /// A scale in the Z direction.
+  ScaleZ(NumberOrPercentage),
+  /// A 3D scale.
+  Scale3d(NumberOrPercentage, NumberOrPercentage, NumberOrPercentage),
+  /// A 2D rotation.
+  Rotate(Angle),
+  /// A rotation around the X axis.
+  RotateX(Angle),
+  /// A rotation around the Y axis.
+  RotateY(Angle),
+  /// A rotation around the Z axis.
+  RotateZ(Angle),
+  /// A 3D rotation.
+  Rotate3d(f32, f32, f32, Angle),
+  /// A 2D skew.
+  Skew(Angle, Angle),
+  /// A skew along the X axis.
+  SkewX(Angle),
+  /// A skew along the Y axis.
+  SkewY(Angle),
+  /// A perspective transform.
+  Perspective(Length),
+  /// A 2D matrix transform.
+  Matrix(Matrix<f32>),
+  /// A 3D matrix transform.
+  Matrix3d(Matrix3d<f32>),
+}
+
+/// A 2D matrix.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[allow(missing_docs)]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub struct Matrix<T> {
+  pub a: T,
+  pub b: T,
+  pub c: T,
+  pub d: T,
+  pub e: T,
+  pub f: T,
+}
+
+impl Matrix<f32> {
+  /// Converts the matrix to a 3D matrix.
+  pub fn to_matrix3d(&self) -> Matrix3d<f32> {
+    Matrix3d {
+      m11: self.a,
+      m12: self.b,
+      m13: 0.0,
+      m14: 0.0,
+      m21: self.c,
+      m22: self.d,
+      m23: 0.0,
+      m24: 0.0,
+      m31: 0.0,
+      m32: 0.0,
+      m33: 1.0,
+      m34: 0.0,
+      m41: self.e,
+      m42: self.f,
+      m43: 0.0,
+      m44: 1.0,
+    }
+  }
+}
+
+/// A 3D matrix.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[allow(missing_docs)]
+pub struct Matrix3d<T> {
+  pub m11: T,
+  pub m12: T,
+  pub m13: T,
+  pub m14: T,
+  pub m21: T,
+  pub m22: T,
+  pub m23: T,
+  pub m24: T,
+  pub m31: T,
+  pub m32: T,
+  pub m33: T,
+  pub m34: T,
+  pub m41: T,
+  pub m42: T,
+  pub m43: T,
+  pub m44: T,
+}
+
+// https://drafts.csswg.org/css-transforms-2/#mathematical-description
+impl Matrix3d<f32> {
+  /// Creates an identity matrix.
+  pub fn identity() -> Matrix3d<f32> {
+    Matrix3d {
+      m11: 1.0,
+      m12: 0.0,
+      m13: 0.0,
+      m14: 0.0,
+      m21: 0.0,
+      m22: 1.0,
+      m23: 0.0,
+      m24: 0.0,
+      m31: 0.0,
+      m32: 0.0,
+      m33: 1.0,
+      m34: 0.0,
+      m41: 0.0,
+      m42: 0.0,
+      m43: 0.0,
+      m44: 1.0,
+    }
+  }
+
+  /// Creates a translation matrix.
+  pub fn translate(x: f32, y: f32, z: f32) -> Matrix3d<f32> {
+    Matrix3d {
+      m11: 1.0,
+      m12: 0.0,
+      m13: 0.0,
+      m14: 0.0,
+      m21: 0.0,
+      m22: 1.0,
+      m23: 0.0,
+      m24: 0.0,
+      m31: 0.0,
+      m32: 0.0,
+      m33: 1.0,
+      m34: 0.0,
+      m41: x,
+      m42: y,
+      m43: z,
+      m44: 1.0,
+    }
+  }
+
+  /// Creates a scale matrix.
+  pub fn scale(x: f32, y: f32, z: f32) -> Matrix3d<f32> {
+    Matrix3d {
+      m11: x,
+      m12: 0.0,
+      m13: 0.0,
+      m14: 0.0,
+      m21: 0.0,
+      m22: y,
+      m23: 0.0,
+      m24: 0.0,
+      m31: 0.0,
+      m32: 0.0,
+      m33: z,
+      m34: 0.0,
+      m41: 0.0,
+      m42: 0.0,
+      m43: 0.0,
+      m44: 1.0,
+    }
+  }
+
+  /// Creates a rotation matrix.
+  pub fn rotate(x: f32, y: f32, z: f32, angle: f32) -> Matrix3d<f32> {
+    // Normalize the vector.
+    let length = (x * x + y * y + z * z).sqrt();
+    if length == 0.0 {
+      // A direction vector that cannot be normalized, such as [0,0,0], will cause the rotation to not be applied.
+      return Matrix3d::identity();
+    }
+
+    let x = x / length;
+    let y = y / length;
+    let z = z / length;
+
+    let half_angle = angle / 2.0;
+    let sin = half_angle.sin();
+    let sc = sin * half_angle.cos();
+    let sq = sin * sin;
+    let m11 = 1.0 - 2.0 * (y * y + z * z) * sq;
+    let m12 = 2.0 * (x * y * sq + z * sc);
+    let m13 = 2.0 * (x * z * sq - y * sc);
+    let m21 = 2.0 * (x * y * sq - z * sc);
+    let m22 = 1.0 - 2.0 * (x * x + z * z) * sq;
+    let m23 = 2.0 * (y * z * sq + x * sc);
+    let m31 = 2.0 * (x * z * sq + y * sc);
+    let m32 = 2.0 * (y * z * sq - x * sc);
+    let m33 = 1.0 - 2.0 * (x * x + y * y) * sq;
+    Matrix3d {
+      m11,
+      m12,
+      m13,
+      m14: 0.0,
+      m21,
+      m22,
+      m23,
+      m24: 0.0,
+      m31,
+      m32,
+      m33,
+      m34: 0.0,
+      m41: 0.0,
+      m42: 0.0,
+      m43: 0.0,
+      m44: 1.0,
+    }
+  }
+
+  /// Creates a skew matrix.
+  pub fn skew(a: f32, b: f32) -> Matrix3d<f32> {
+    Matrix3d {
+      m11: 1.0,
+      m12: b.tan(),
+      m13: 0.0,
+      m14: 0.0,
+      m21: a.tan(),
+      m22: 1.0,
+      m23: 0.0,
+      m24: 0.0,
+      m31: 0.0,
+      m32: 0.0,
+      m33: 1.0,
+      m34: 0.0,
+      m41: 0.0,
+      m42: 0.0,
+      m43: 0.0,
+      m44: 1.0,
+    }
+  }
+
+  /// Creates a perspective matrix.
+  pub fn perspective(d: f32) -> Matrix3d<f32> {
+    Matrix3d {
+      m11: 1.0,
+      m12: 0.0,
+      m13: 0.0,
+      m14: 0.0,
+      m21: 0.0,
+      m22: 1.0,
+      m23: 0.0,
+      m24: 0.0,
+      m31: 0.0,
+      m32: 0.0,
+      m33: 1.0,
+      m34: -1.0 / d,
+      m41: 0.0,
+      m42: 0.0,
+      m43: 0.0,
+      m44: 1.0,
+    }
+  }
+
+  /// Multiplies this matrix by another, returning a new matrix.
+  pub fn multiply(&self, other: &Self) -> Self {
+    Matrix3d {
+      m11: self.m11 * other.m11 + self.m12 * other.m21 + self.m13 * other.m31 + self.m14 * other.m41,
+      m12: self.m11 * other.m12 + self.m12 * other.m22 + self.m13 * other.m32 + self.m14 * other.m42,
+      m13: self.m11 * other.m13 + self.m12 * other.m23 + self.m13 * other.m33 + self.m14 * other.m43,
+      m14: self.m11 * other.m14 + self.m12 * other.m24 + self.m13 * other.m34 + self.m14 * other.m44,
+      m21: self.m21 * other.m11 + self.m22 * other.m21 + self.m23 * other.m31 + self.m24 * other.m41,
+      m22: self.m21 * other.m12 + self.m22 * other.m22 + self.m23 * other.m32 + self.m24 * other.m42,
+      m23: self.m21 * other.m13 + self.m22 * other.m23 + self.m23 * other.m33 + self.m24 * other.m43,
+      m24: self.m21 * other.m14 + self.m22 * other.m24 + self.m23 * other.m34 + self.m24 * other.m44,
+      m31: self.m31 * other.m11 + self.m32 * other.m21 + self.m33 * other.m31 + self.m34 * other.m41,
+      m32: self.m31 * other.m12 + self.m32 * other.m22 + self.m33 * other.m32 + self.m34 * other.m42,
+      m33: self.m31 * other.m13 + self.m32 * other.m23 + self.m33 * other.m33 + self.m34 * other.m43,
+      m34: self.m31 * other.m14 + self.m32 * other.m24 + self.m33 * other.m34 + self.m34 * other.m44,
+      m41: self.m41 * other.m11 + self.m42 * other.m21 + self.m43 * other.m31 + self.m44 * other.m41,
+      m42: self.m41 * other.m12 + self.m42 * other.m22 + self.m43 * other.m32 + self.m44 * other.m42,
+      m43: self.m41 * other.m13 + self.m42 * other.m23 + self.m43 * other.m33 + self.m44 * other.m43,
+      m44: self.m41 * other.m14 + self.m42 * other.m24 + self.m43 * other.m34 + self.m44 * other.m44,
+    }
+  }
+
+  /// Returns whether this matrix could be converted to a 2D matrix.
+  pub fn is_2d(&self) -> bool {
+    self.m31 == 0.0
+      && self.m32 == 0.0
+      && self.m13 == 0.0
+      && self.m23 == 0.0
+      && self.m43 == 0.0
+      && self.m14 == 0.0
+      && self.m24 == 0.0
+      && self.m34 == 0.0
+      && self.m33 == 1.0
+      && self.m44 == 1.0
+  }
+
+  /// Attempts to convert the matrix to 2D.
+  /// Returns `None` if the conversion is not possible.
+  pub fn to_matrix2d(&self) -> Option<Matrix<f32>> {
+    if self.is_2d() {
+      return Some(Matrix {
+        a: self.m11,
+        b: self.m12,
+        c: self.m21,
+        d: self.m22,
+        e: self.m41,
+        f: self.m42,
+      });
+    }
+    None
+  }
+
+  /// Scales the matrix by the given factor.
+  pub fn scale_by_factor(&mut self, scaling_factor: f32) {
+    self.m11 *= scaling_factor;
+    self.m12 *= scaling_factor;
+    self.m13 *= scaling_factor;
+    self.m14 *= scaling_factor;
+    self.m21 *= scaling_factor;
+    self.m22 *= scaling_factor;
+    self.m23 *= scaling_factor;
+    self.m24 *= scaling_factor;
+    self.m31 *= scaling_factor;
+    self.m32 *= scaling_factor;
+    self.m33 *= scaling_factor;
+    self.m34 *= scaling_factor;
+    self.m41 *= scaling_factor;
+    self.m42 *= scaling_factor;
+    self.m43 *= scaling_factor;
+    self.m44 *= scaling_factor;
+  }
+
+  /// Returns the determinant of the matrix.
+  pub fn determinant(&self) -> f32 {
+    self.m14 * self.m23 * self.m32 * self.m41
+      - self.m13 * self.m24 * self.m32 * self.m41
+      - self.m14 * self.m22 * self.m33 * self.m41
+      + self.m12 * self.m24 * self.m33 * self.m41
+      + self.m13 * self.m22 * self.m34 * self.m41
+      - self.m12 * self.m23 * self.m34 * self.m41
+      - self.m14 * self.m23 * self.m31 * self.m42
+      + self.m13 * self.m24 * self.m31 * self.m42
+      + self.m14 * self.m21 * self.m33 * self.m42
+      - self.m11 * self.m24 * self.m33 * self.m42
+      - self.m13 * self.m21 * self.m34 * self.m42
+      + self.m11 * self.m23 * self.m34 * self.m42
+      + self.m14 * self.m22 * self.m31 * self.m43
+      - self.m12 * self.m24 * self.m31 * self.m43
+      - self.m14 * self.m21 * self.m32 * self.m43
+      + self.m11 * self.m24 * self.m32 * self.m43
+      + self.m12 * self.m21 * self.m34 * self.m43
+      - self.m11 * self.m22 * self.m34 * self.m43
+      - self.m13 * self.m22 * self.m31 * self.m44
+      + self.m12 * self.m23 * self.m31 * self.m44
+      + self.m13 * self.m21 * self.m32 * self.m44
+      - self.m11 * self.m23 * self.m32 * self.m44
+      - self.m12 * self.m21 * self.m33 * self.m44
+      + self.m11 * self.m22 * self.m33 * self.m44
+  }
+
+  /// Returns the inverse of the matrix if possible.
+  pub fn inverse(&self) -> Option<Matrix3d<f32>> {
+    let mut det = self.determinant();
+    if det == 0.0 {
+      return None;
+    }
+
+    det = 1.0 / det;
+    Some(Matrix3d {
+      m11: det
+        * (self.m23 * self.m34 * self.m42 - self.m24 * self.m33 * self.m42 + self.m24 * self.m32 * self.m43
+          - self.m22 * self.m34 * self.m43
+          - self.m23 * self.m32 * self.m44
+          + self.m22 * self.m33 * self.m44),
+      m12: det
+        * (self.m14 * self.m33 * self.m42 - self.m13 * self.m34 * self.m42 - self.m14 * self.m32 * self.m43
+          + self.m12 * self.m34 * self.m43
+          + self.m13 * self.m32 * self.m44
+          - self.m12 * self.m33 * self.m44),
+      m13: det
+        * (self.m13 * self.m24 * self.m42 - self.m14 * self.m23 * self.m42 + self.m14 * self.m22 * self.m43
+          - self.m12 * self.m24 * self.m43
+          - self.m13 * self.m22 * self.m44
+          + self.m12 * self.m23 * self.m44),
+      m14: det
+        * (self.m14 * self.m23 * self.m32 - self.m13 * self.m24 * self.m32 - self.m14 * self.m22 * self.m33
+          + self.m12 * self.m24 * self.m33
+          + self.m13 * self.m22 * self.m34
+          - self.m12 * self.m23 * self.m34),
+      m21: det
+        * (self.m24 * self.m33 * self.m41 - self.m23 * self.m34 * self.m41 - self.m24 * self.m31 * self.m43
+          + self.m21 * self.m34 * self.m43
+          + self.m23 * self.m31 * self.m44
+          - self.m21 * self.m33 * self.m44),
+      m22: det
+        * (self.m13 * self.m34 * self.m41 - self.m14 * self.m33 * self.m41 + self.m14 * self.m31 * self.m43
+          - self.m11 * self.m34 * self.m43
+          - self.m13 * self.m31 * self.m44
+          + self.m11 * self.m33 * self.m44),
+      m23: det
+        * (self.m14 * self.m23 * self.m41 - self.m13 * self.m24 * self.m41 - self.m14 * self.m21 * self.m43
+          + self.m11 * self.m24 * self.m43
+          + self.m13 * self.m21 * self.m44
+          - self.m11 * self.m23 * self.m44),
+      m24: det
+        * (self.m13 * self.m24 * self.m31 - self.m14 * self.m23 * self.m31 + self.m14 * self.m21 * self.m33
+          - self.m11 * self.m24 * self.m33
+          - self.m13 * self.m21 * self.m34
+          + self.m11 * self.m23 * self.m34),
+      m31: det
+        * (self.m22 * self.m34 * self.m41 - self.m24 * self.m32 * self.m41 + self.m24 * self.m31 * self.m42
+          - self.m21 * self.m34 * self.m42
+          - self.m22 * self.m31 * self.m44
+          + self.m21 * self.m32 * self.m44),
+      m32: det
+        * (self.m14 * self.m32 * self.m41 - self.m12 * self.m34 * self.m41 - self.m14 * self.m31 * self.m42
+          + self.m11 * self.m34 * self.m42
+          + self.m12 * self.m31 * self.m44
+          - self.m11 * self.m32 * self.m44),
+      m33: det
+        * (self.m12 * self.m24 * self.m41 - self.m14 * self.m22 * self.m41 + self.m14 * self.m21 * self.m42
+          - self.m11 * self.m24 * self.m42
+          - self.m12 * self.m21 * self.m44
+          + self.m11 * self.m22 * self.m44),
+      m34: det
+        * (self.m14 * self.m22 * self.m31 - self.m12 * self.m24 * self.m31 - self.m14 * self.m21 * self.m32
+          + self.m11 * self.m24 * self.m32
+          + self.m12 * self.m21 * self.m34
+          - self.m11 * self.m22 * self.m34),
+      m41: det
+        * (self.m23 * self.m32 * self.m41 - self.m22 * self.m33 * self.m41 - self.m23 * self.m31 * self.m42
+          + self.m21 * self.m33 * self.m42
+          + self.m22 * self.m31 * self.m43
+          - self.m21 * self.m32 * self.m43),
+      m42: det
+        * (self.m12 * self.m33 * self.m41 - self.m13 * self.m32 * self.m41 + self.m13 * self.m31 * self.m42
+          - self.m11 * self.m33 * self.m42
+          - self.m12 * self.m31 * self.m43
+          + self.m11 * self.m32 * self.m43),
+      m43: det
+        * (self.m13 * self.m22 * self.m41 - self.m12 * self.m23 * self.m41 - self.m13 * self.m21 * self.m42
+          + self.m11 * self.m23 * self.m42
+          + self.m12 * self.m21 * self.m43
+          - self.m11 * self.m22 * self.m43),
+      m44: det
+        * (self.m12 * self.m23 * self.m31 - self.m13 * self.m22 * self.m31 + self.m13 * self.m21 * self.m32
+          - self.m11 * self.m23 * self.m32
+          - self.m12 * self.m21 * self.m33
+          + self.m11 * self.m22 * self.m33),
+    })
+  }
+
+  /// Transposes the matrix.
+  pub fn transpose(&self) -> Self {
+    Self {
+      m11: self.m11,
+      m12: self.m21,
+      m13: self.m31,
+      m14: self.m41,
+      m21: self.m12,
+      m22: self.m22,
+      m23: self.m32,
+      m24: self.m42,
+      m31: self.m13,
+      m32: self.m23,
+      m33: self.m33,
+      m34: self.m43,
+      m41: self.m14,
+      m42: self.m24,
+      m43: self.m34,
+      m44: self.m44,
+    }
+  }
+
+  /// Multiplies a vector by the matrix.
+  pub fn multiply_vector(&self, pin: &[f32; 4]) -> [f32; 4] {
+    [
+      pin[0] * self.m11 + pin[1] * self.m21 + pin[2] * self.m31 + pin[3] * self.m41,
+      pin[0] * self.m12 + pin[1] * self.m22 + pin[2] * self.m32 + pin[3] * self.m42,
+      pin[0] * self.m13 + pin[1] * self.m23 + pin[2] * self.m33 + pin[3] * self.m43,
+      pin[0] * self.m14 + pin[1] * self.m24 + pin[2] * self.m34 + pin[3] * self.m44,
+    ]
+  }
+
+  /// Decomposes the matrix into a list of transform functions if possible.
+  pub fn decompose(&self) -> Option<TransformList> {
+    // https://drafts.csswg.org/css-transforms-2/#decomposing-a-3d-matrix
+    // Combine 2 point.
+    let combine = |a: [f32; 3], b: [f32; 3], ascl: f32, bscl: f32| {
+      [
+        (ascl * a[0]) + (bscl * b[0]),
+        (ascl * a[1]) + (bscl * b[1]),
+        (ascl * a[2]) + (bscl * b[2]),
+      ]
+    };
+
+    // Dot product.
+    let dot = |a: [f32; 3], b: [f32; 3]| a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
+
+    // Cross product.
+    let cross = |row1: [f32; 3], row2: [f32; 3]| {
+      [
+        row1[1] * row2[2] - row1[2] * row2[1],
+        row1[2] * row2[0] - row1[0] * row2[2],
+        row1[0] * row2[1] - row1[1] * row2[0],
+      ]
+    };
+
+    if self.m44 == 0.0 {
+      return None;
+    }
+
+    let scaling_factor = self.m44;
+
+    // Normalize the matrix.
+    let mut matrix = self.clone();
+    matrix.scale_by_factor(1.0 / scaling_factor);
+
+    // perspective_matrix is used to solve for perspective, but it also provides
+    // an easy way to test for singularity of the upper 3x3 component.
+    let mut perspective_matrix = matrix.clone();
+    perspective_matrix.m14 = 0.0;
+    perspective_matrix.m24 = 0.0;
+    perspective_matrix.m34 = 0.0;
+    perspective_matrix.m44 = 1.0;
+
+    if perspective_matrix.determinant() == 0.0 {
+      return None;
+    }
+
+    let mut transforms = vec![];
+
+    // First, isolate perspective.
+    if matrix.m14 != 0.0 || matrix.m24 != 0.0 || matrix.m34 != 0.0 {
+      let right_hand_side: [f32; 4] = [matrix.m14, matrix.m24, matrix.m34, matrix.m44];
+
+      perspective_matrix = perspective_matrix.inverse().unwrap().transpose();
+      let perspective = perspective_matrix.multiply_vector(&right_hand_side);
+      if perspective[0] == 0.0 && perspective[1] == 0.0 && perspective[3] == 0.0 {
+        transforms.push(Transform::Perspective(Length::px(-1.0 / perspective[2])))
+      } else {
+        return None;
+      }
+    }
+
+    // Next take care of translation (easy).
+    // let translate = Translate3D(matrix.m41, matrix.m42, matrix.m43);
+    if matrix.m41 != 0.0 || matrix.m42 != 0.0 || matrix.m43 != 0.0 {
+      transforms.push(Transform::Translate3d(
+        LengthPercentage::px(matrix.m41),
+        LengthPercentage::px(matrix.m42),
+        Length::px(matrix.m43),
+      ));
+    }
+
+    // Now get scale and shear. 'row' is a 3 element array of 3 component vectors
+    let mut row = [
+      [matrix.m11, matrix.m12, matrix.m13],
+      [matrix.m21, matrix.m22, matrix.m23],
+      [matrix.m31, matrix.m32, matrix.m33],
+    ];
+
+    // Compute X scale factor and normalize first row.
+    let row0len = (row[0][0] * row[0][0] + row[0][1] * row[0][1] + row[0][2] * row[0][2]).sqrt();
+    let mut scale_x = row0len;
+    row[0] = [row[0][0] / row0len, row[0][1] / row0len, row[0][2] / row0len];
+
+    // Compute XY shear factor and make 2nd row orthogonal to 1st.
+    let mut skew_x = dot(row[0], row[1]);
+    row[1] = combine(row[1], row[0], 1.0, -skew_x);
+
+    // Now, compute Y scale and normalize 2nd row.
+    let row1len = (row[1][0] * row[1][0] + row[1][1] * row[1][1] + row[1][2] * row[1][2]).sqrt();
+    let mut scale_y = row1len;
+    row[1] = [row[1][0] / row1len, row[1][1] / row1len, row[1][2] / row1len];
+    skew_x /= scale_y;
+
+    // Compute XZ and YZ shears, orthogonalize 3rd row
+    let mut skew_y = dot(row[0], row[2]);
+    row[2] = combine(row[2], row[0], 1.0, -skew_y);
+    let mut skew_z = dot(row[1], row[2]);
+    row[2] = combine(row[2], row[1], 1.0, -skew_z);
+
+    // Next, get Z scale and normalize 3rd row.
+    let row2len = (row[2][0] * row[2][0] + row[2][1] * row[2][1] + row[2][2] * row[2][2]).sqrt();
+    let mut scale_z = row2len;
+    row[2] = [row[2][0] / row2len, row[2][1] / row2len, row[2][2] / row2len];
+    skew_y /= scale_z;
+    skew_z /= scale_z;
+
+    if skew_z != 0.0 {
+      return None; // ???
+    }
+
+    // Round to 5 digits of precision, which is what we print.
+    macro_rules! round {
+      ($var: ident) => {
+        $var = ($var * 100000.0).round() / 100000.0;
+      };
+    }
+
+    round!(skew_x);
+    round!(skew_y);
+    round!(skew_z);
+
+    if skew_x != 0.0 || skew_y != 0.0 || skew_z != 0.0 {
+      transforms.push(Transform::Skew(Angle::Rad(skew_x), Angle::Rad(skew_y)));
+    }
+
+    // At this point, the matrix (in rows) is orthonormal.
+    // Check for a coordinate system flip.  If the determinant
+    // is -1, then negate the matrix and the scaling factors.
+    if dot(row[0], cross(row[1], row[2])) < 0.0 {
+      scale_x = -scale_x;
+      scale_y = -scale_y;
+      scale_z = -scale_z;
+      for i in 0..3 {
+        row[i][0] *= -1.0;
+        row[i][1] *= -1.0;
+        row[i][2] *= -1.0;
+      }
+    }
+
+    round!(scale_x);
+    round!(scale_y);
+    round!(scale_z);
+
+    if scale_x != 1.0 || scale_y != 1.0 || scale_z != 1.0 {
+      transforms.push(Transform::Scale3d(
+        NumberOrPercentage::Number(scale_x),
+        NumberOrPercentage::Number(scale_y),
+        NumberOrPercentage::Number(scale_z),
+      ))
+    }
+
+    // Now, get the rotations out.
+    let mut rotate_x = 0.5 * ((1.0 + row[0][0] - row[1][1] - row[2][2]).max(0.0)).sqrt();
+    let mut rotate_y = 0.5 * ((1.0 - row[0][0] + row[1][1] - row[2][2]).max(0.0)).sqrt();
+    let mut rotate_z = 0.5 * ((1.0 - row[0][0] - row[1][1] + row[2][2]).max(0.0)).sqrt();
+    let rotate_w = 0.5 * ((1.0 + row[0][0] + row[1][1] + row[2][2]).max(0.0)).sqrt();
+
+    if row[2][1] > row[1][2] {
+      rotate_x = -rotate_x
+    }
+
+    if row[0][2] > row[2][0] {
+      rotate_y = -rotate_y
+    }
+
+    if row[1][0] > row[0][1] {
+      rotate_z = -rotate_z
+    }
+
+    let len = (rotate_x * rotate_x + rotate_y * rotate_y + rotate_z * rotate_z).sqrt();
+    if len != 0.0 {
+      rotate_x /= len;
+      rotate_y /= len;
+      rotate_z /= len;
+    }
+    let a = 2.0 * len.atan2(rotate_w);
+
+    // normalize the vector so one of the values is 1
+    let max = rotate_x.max(rotate_y).max(rotate_z);
+    rotate_x /= max;
+    rotate_y /= max;
+    rotate_z /= max;
+
+    if a != 0.0 {
+      transforms.push(Transform::Rotate3d(rotate_x, rotate_y, rotate_z, Angle::Rad(a)))
+    }
+
+    if transforms.is_empty() {
+      return None;
+    }
+
+    Some(TransformList(transforms))
+  }
+}
+
+impl<'i> Parse<'i> for Transform {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let function = input.expect_function()?.clone();
+    input.parse_nested_block(|input| {
+      let location = input.current_source_location();
+      match_ignore_ascii_case! { &function,
+        "matrix" => {
+          let a = f32::parse(input)?;
+          input.expect_comma()?;
+          let b = f32::parse(input)?;
+          input.expect_comma()?;
+          let c = f32::parse(input)?;
+          input.expect_comma()?;
+          let d = f32::parse(input)?;
+          input.expect_comma()?;
+          let e = f32::parse(input)?;
+          input.expect_comma()?;
+          let f = f32::parse(input)?;
+          Ok(Transform::Matrix(Matrix { a, b, c, d, e, f }))
+        },
+        "matrix3d" => {
+          let m11 = f32::parse(input)?;
+          input.expect_comma()?;
+          let m12 = f32::parse(input)?;
+          input.expect_comma()?;
+          let m13 = f32::parse(input)?;
+          input.expect_comma()?;
+          let m14 = f32::parse(input)?;
+          input.expect_comma()?;
+          let m21 = f32::parse(input)?;
+          input.expect_comma()?;
+          let m22 = f32::parse(input)?;
+          input.expect_comma()?;
+          let m23 = f32::parse(input)?;
+          input.expect_comma()?;
+          let m24 = f32::parse(input)?;
+          input.expect_comma()?;
+          let m31 = f32::parse(input)?;
+          input.expect_comma()?;
+          let m32 = f32::parse(input)?;
+          input.expect_comma()?;
+          let m33 = f32::parse(input)?;
+          input.expect_comma()?;
+          let m34 = f32::parse(input)?;
+          input.expect_comma()?;
+          let m41 = f32::parse(input)?;
+          input.expect_comma()?;
+          let m42 = f32::parse(input)?;
+          input.expect_comma()?;
+          let m43 = f32::parse(input)?;
+          input.expect_comma()?;
+          let m44 = f32::parse(input)?;
+          Ok(Transform::Matrix3d(Matrix3d {
+            m11, m12, m13, m14,
+            m21, m22, m23, m24,
+            m31, m32, m33, m34,
+            m41, m42, m43, m44
+          }))
+        },
+        "translate" => {
+          let x = LengthPercentage::parse(input)?;
+          if input.try_parse(|input| input.expect_comma()).is_ok() {
+            let y = LengthPercentage::parse(input)?;
+            Ok(Transform::Translate(x, y))
+          } else {
+            Ok(Transform::Translate(x, LengthPercentage::zero()))
+          }
+        },
+        "translatex" => {
+          let x = LengthPercentage::parse(input)?;
+          Ok(Transform::TranslateX(x))
+        },
+        "translatey" => {
+          let y = LengthPercentage::parse(input)?;
+          Ok(Transform::TranslateY(y))
+        },
+        "translatez" => {
+          let z = Length::parse(input)?;
+          Ok(Transform::TranslateZ(z))
+        },
+        "translate3d" => {
+          let x = LengthPercentage::parse(input)?;
+          input.expect_comma()?;
+          let y = LengthPercentage::parse(input)?;
+          input.expect_comma()?;
+          let z = Length::parse(input)?;
+          Ok(Transform::Translate3d(x, y, z))
+        },
+        "scale" => {
+          let x = NumberOrPercentage::parse(input)?;
+          if input.try_parse(|input| input.expect_comma()).is_ok() {
+            let y = NumberOrPercentage::parse(input)?;
+            Ok(Transform::Scale(x, y))
+          } else {
+            Ok(Transform::Scale(x.clone(), x))
+          }
+        },
+        "scalex" => {
+          let x = NumberOrPercentage::parse(input)?;
+          Ok(Transform::ScaleX(x))
+        },
+        "scaley" => {
+          let y = NumberOrPercentage::parse(input)?;
+          Ok(Transform::ScaleY(y))
+        },
+        "scalez" => {
+          let z = NumberOrPercentage::parse(input)?;
+          Ok(Transform::ScaleZ(z))
+        },
+        "scale3d" => {
+          let x = NumberOrPercentage::parse(input)?;
+          input.expect_comma()?;
+          let y = NumberOrPercentage::parse(input)?;
+          input.expect_comma()?;
+          let z = NumberOrPercentage::parse(input)?;
+          Ok(Transform::Scale3d(x, y, z))
+        },
+        "rotate" => {
+          let angle = Angle::parse_with_unitless_zero(input)?;
+          Ok(Transform::Rotate(angle))
+        },
+        "rotatex" => {
+          let angle = Angle::parse_with_unitless_zero(input)?;
+          Ok(Transform::RotateX(angle))
+        },
+        "rotatey" => {
+          let angle = Angle::parse_with_unitless_zero(input)?;
+          Ok(Transform::RotateY(angle))
+        },
+        "rotatez" => {
+          let angle = Angle::parse_with_unitless_zero(input)?;
+          Ok(Transform::RotateZ(angle))
+        },
+        "rotate3d" => {
+          let x = f32::parse(input)?;
+          input.expect_comma()?;
+          let y = f32::parse(input)?;
+          input.expect_comma()?;
+          let z = f32::parse(input)?;
+          input.expect_comma()?;
+          let angle = Angle::parse_with_unitless_zero(input)?;
+          Ok(Transform::Rotate3d(x, y, z, angle))
+        },
+        "skew" => {
+          let x = Angle::parse_with_unitless_zero(input)?;
+          if input.try_parse(|input| input.expect_comma()).is_ok() {
+            let y = Angle::parse_with_unitless_zero(input)?;
+            Ok(Transform::Skew(x, y))
+          } else {
+            Ok(Transform::Skew(x, Angle::Deg(0.0)))
+          }
+        },
+        "skewx" => {
+          let angle = Angle::parse_with_unitless_zero(input)?;
+          Ok(Transform::SkewX(angle))
+        },
+        "skewy" => {
+          let angle = Angle::parse_with_unitless_zero(input)?;
+          Ok(Transform::SkewY(angle))
+        },
+        "perspective" => {
+          let len = Length::parse(input)?;
+          Ok(Transform::Perspective(len))
+        },
+        _ => Err(location.new_unexpected_token_error(
+          cssparser::Token::Ident(function.clone())
+        ))
+      }
+    })
+  }
+}
+
+impl ToCss for Transform {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    use Transform::*;
+    match self {
+      Translate(x, y) => {
+        if dest.minify && x.is_zero() && !y.is_zero() {
+          dest.write_str("translateY(")?;
+          y.to_css(dest)?
+        } else {
+          dest.write_str("translate(")?;
+          x.to_css(dest)?;
+          if !y.is_zero() {
+            dest.delim(',', false)?;
+            y.to_css(dest)?;
+          }
+        }
+        dest.write_char(')')
+      }
+      TranslateX(x) => {
+        dest.write_str(if dest.minify { "translate(" } else { "translateX(" })?;
+        x.to_css(dest)?;
+        dest.write_char(')')
+      }
+      TranslateY(y) => {
+        dest.write_str("translateY(")?;
+        y.to_css(dest)?;
+        dest.write_char(')')
+      }
+      TranslateZ(z) => {
+        dest.write_str("translateZ(")?;
+        z.to_css(dest)?;
+        dest.write_char(')')
+      }
+      Translate3d(x, y, z) => {
+        if dest.minify && !x.is_zero() && y.is_zero() && z.is_zero() {
+          dest.write_str("translate(")?;
+          x.to_css(dest)?;
+        } else if dest.minify && x.is_zero() && !y.is_zero() && z.is_zero() {
+          dest.write_str("translateY(")?;
+          y.to_css(dest)?;
+        } else if dest.minify && x.is_zero() && y.is_zero() && !z.is_zero() {
+          dest.write_str("translateZ(")?;
+          z.to_css(dest)?;
+        } else if dest.minify && z.is_zero() {
+          dest.write_str("translate(")?;
+          x.to_css(dest)?;
+          dest.delim(',', false)?;
+          y.to_css(dest)?;
+        } else {
+          dest.write_str("translate3d(")?;
+          x.to_css(dest)?;
+          dest.delim(',', false)?;
+          y.to_css(dest)?;
+          dest.delim(',', false)?;
+          z.to_css(dest)?;
+        }
+        dest.write_char(')')
+      }
+      Scale(x, y) => {
+        let x: f32 = x.into();
+        let y: f32 = y.into();
+        if dest.minify && x == 1.0 && y != 1.0 {
+          dest.write_str("scaleY(")?;
+          y.to_css(dest)?;
+        } else if dest.minify && x != 1.0 && y == 1.0 {
+          dest.write_str("scaleX(")?;
+          x.to_css(dest)?;
+        } else {
+          dest.write_str("scale(")?;
+          x.to_css(dest)?;
+          if y != x {
+            dest.delim(',', false)?;
+            y.to_css(dest)?;
+          }
+        }
+        dest.write_char(')')
+      }
+      ScaleX(x) => {
+        dest.write_str("scaleX(")?;
+        x.to_css(dest)?;
+        dest.write_char(')')
+      }
+      ScaleY(y) => {
+        dest.write_str("scaleY(")?;
+        y.to_css(dest)?;
+        dest.write_char(')')
+      }
+      ScaleZ(z) => {
+        dest.write_str("scaleZ(")?;
+        z.to_css(dest)?;
+        dest.write_char(')')
+      }
+      Scale3d(x, y, z) => {
+        let x: f32 = x.into();
+        let y: f32 = y.into();
+        let z: f32 = z.into();
+        if dest.minify && z == 1.0 && x == y {
+          // scale3d(x, x, 1) => scale(x)
+          dest.write_str("scale(")?;
+          x.to_css(dest)?;
+        } else if dest.minify && x != 1.0 && y == 1.0 && z == 1.0 {
+          // scale3d(x, 1, 1) => scaleX(x)
+          dest.write_str("scaleX(")?;
+          x.to_css(dest)?;
+        } else if dest.minify && x == 1.0 && y != 1.0 && z == 1.0 {
+          // scale3d(1, y, 1) => scaleY(y)
+          dest.write_str("scaleY(")?;
+          y.to_css(dest)?;
+        } else if dest.minify && x == 1.0 && y == 1.0 && z != 1.0 {
+          // scale3d(1, 1, z) => scaleZ(z)
+          dest.write_str("scaleZ(")?;
+          z.to_css(dest)?;
+        } else if dest.minify && z == 1.0 {
+          // scale3d(x, y, 1) => scale(x, y)
+          dest.write_str("scale(")?;
+          x.to_css(dest)?;
+          dest.delim(',', false)?;
+          y.to_css(dest)?;
+        } else {
+          dest.write_str("scale3d(")?;
+          x.to_css(dest)?;
+          dest.delim(',', false)?;
+          y.to_css(dest)?;
+          dest.delim(',', false)?;
+          z.to_css(dest)?;
+        }
+        dest.write_char(')')
+      }
+      Rotate(angle) => {
+        dest.write_str("rotate(")?;
+        angle.to_css_with_unitless_zero(dest)?;
+        dest.write_char(')')
+      }
+      RotateX(angle) => {
+        dest.write_str("rotateX(")?;
+        angle.to_css_with_unitless_zero(dest)?;
+        dest.write_char(')')
+      }
+      RotateY(angle) => {
+        dest.write_str("rotateY(")?;
+        angle.to_css_with_unitless_zero(dest)?;
+        dest.write_char(')')
+      }
+      RotateZ(angle) => {
+        dest.write_str(if dest.minify { "rotate(" } else { "rotateZ(" })?;
+        angle.to_css_with_unitless_zero(dest)?;
+        dest.write_char(')')
+      }
+      Rotate3d(x, y, z, angle) => {
+        if dest.minify && *x == 1.0 && *y == 0.0 && *z == 0.0 {
+          // rotate3d(1, 0, 0, a) => rotateX(a)
+          dest.write_str("rotateX(")?;
+          angle.to_css_with_unitless_zero(dest)?;
+        } else if dest.minify && *x == 0.0 && *y == 1.0 && *z == 0.0 {
+          // rotate3d(0, 1, 0, a) => rotateY(a)
+          dest.write_str("rotateY(")?;
+          angle.to_css_with_unitless_zero(dest)?;
+        } else if dest.minify && *x == 0.0 && *y == 0.0 && *z == 1.0 {
+          // rotate3d(0, 0, 1, a) => rotate(a)
+          dest.write_str("rotate(")?;
+          angle.to_css_with_unitless_zero(dest)?;
+        } else {
+          dest.write_str("rotate3d(")?;
+          x.to_css(dest)?;
+          dest.delim(',', false)?;
+          y.to_css(dest)?;
+          dest.delim(',', false)?;
+          z.to_css(dest)?;
+          dest.delim(',', false)?;
+          angle.to_css_with_unitless_zero(dest)?;
+        }
+        dest.write_char(')')
+      }
+      Skew(x, y) => {
+        if dest.minify && x.is_zero() && !y.is_zero() {
+          dest.write_str("skewY(")?;
+          y.to_css_with_unitless_zero(dest)?
+        } else {
+          dest.write_str("skew(")?;
+          x.to_css(dest)?;
+          if !y.is_zero() {
+            dest.delim(',', false)?;
+            y.to_css_with_unitless_zero(dest)?;
+          }
+        }
+        dest.write_char(')')
+      }
+      SkewX(angle) => {
+        dest.write_str(if dest.minify { "skew(" } else { "skewX(" })?;
+        angle.to_css_with_unitless_zero(dest)?;
+        dest.write_char(')')
+      }
+      SkewY(angle) => {
+        dest.write_str("skewY(")?;
+        angle.to_css_with_unitless_zero(dest)?;
+        dest.write_char(')')
+      }
+      Perspective(len) => {
+        dest.write_str("perspective(")?;
+        len.to_css(dest)?;
+        dest.write_char(')')
+      }
+      Matrix(super::transform::Matrix { a, b, c, d, e, f }) => {
+        dest.write_str("matrix(")?;
+        a.to_css(dest)?;
+        dest.delim(',', false)?;
+        b.to_css(dest)?;
+        dest.delim(',', false)?;
+        c.to_css(dest)?;
+        dest.delim(',', false)?;
+        d.to_css(dest)?;
+        dest.delim(',', false)?;
+        e.to_css(dest)?;
+        dest.delim(',', false)?;
+        f.to_css(dest)?;
+        dest.write_char(')')
+      }
+      Matrix3d(super::transform::Matrix3d {
+        m11,
+        m12,
+        m13,
+        m14,
+        m21,
+        m22,
+        m23,
+        m24,
+        m31,
+        m32,
+        m33,
+        m34,
+        m41,
+        m42,
+        m43,
+        m44,
+      }) => {
+        dest.write_str("matrix3d(")?;
+        m11.to_css(dest)?;
+        dest.delim(',', false)?;
+        m12.to_css(dest)?;
+        dest.delim(',', false)?;
+        m13.to_css(dest)?;
+        dest.delim(',', false)?;
+        m14.to_css(dest)?;
+        dest.delim(',', false)?;
+        m21.to_css(dest)?;
+        dest.delim(',', false)?;
+        m22.to_css(dest)?;
+        dest.delim(',', false)?;
+        m23.to_css(dest)?;
+        dest.delim(',', false)?;
+        m24.to_css(dest)?;
+        dest.delim(',', false)?;
+        m31.to_css(dest)?;
+        dest.delim(',', false)?;
+        m32.to_css(dest)?;
+        dest.delim(',', false)?;
+        m33.to_css(dest)?;
+        dest.delim(',', false)?;
+        m34.to_css(dest)?;
+        dest.delim(',', false)?;
+        m41.to_css(dest)?;
+        dest.delim(',', false)?;
+        m42.to_css(dest)?;
+        dest.delim(',', false)?;
+        m43.to_css(dest)?;
+        dest.delim(',', false)?;
+        m44.to_css(dest)?;
+        dest.write_char(')')
+      }
+    }
+  }
+}
+
+impl Transform {
+  /// Converts the transform to a 3D matrix.
+  pub fn to_matrix(&self) -> Option<Matrix3d<f32>> {
+    macro_rules! to_radians {
+      ($angle: ident) => {{
+        // If the angle is negative or more than a full circle, we cannot
+        // safely convert to a matrix. Transforms are interpolated numerically
+        // when types match, and this will have different results than
+        // when interpolating matrices with large angles.
+        // https://www.w3.org/TR/css-transforms-1/#matrix-interpolation
+        let rad = $angle.to_radians();
+        if rad < 0.0 || rad >= 2.0 * PI {
+          return None;
+        }
+
+        rad
+      }};
+    }
+
+    match &self {
+      Transform::Translate(LengthPercentage::Dimension(x), LengthPercentage::Dimension(y)) => {
+        if let (Some(x), Some(y)) = (x.to_px(), y.to_px()) {
+          return Some(Matrix3d::translate(x, y, 0.0));
+        }
+      }
+      Transform::TranslateX(LengthPercentage::Dimension(x)) => {
+        if let Some(x) = x.to_px() {
+          return Some(Matrix3d::translate(x, 0.0, 0.0));
+        }
+      }
+      Transform::TranslateY(LengthPercentage::Dimension(y)) => {
+        if let Some(y) = y.to_px() {
+          return Some(Matrix3d::translate(0.0, y, 0.0));
+        }
+      }
+      Transform::TranslateZ(z) => {
+        if let Some(z) = z.to_px() {
+          return Some(Matrix3d::translate(0.0, 0.0, z));
+        }
+      }
+      Transform::Translate3d(LengthPercentage::Dimension(x), LengthPercentage::Dimension(y), z) => {
+        if let (Some(x), Some(y), Some(z)) = (x.to_px(), y.to_px(), z.to_px()) {
+          return Some(Matrix3d::translate(x, y, z));
+        }
+      }
+      Transform::Scale(x, y) => return Some(Matrix3d::scale(x.into(), y.into(), 1.0)),
+      Transform::ScaleX(x) => return Some(Matrix3d::scale(x.into(), 1.0, 1.0)),
+      Transform::ScaleY(y) => return Some(Matrix3d::scale(1.0, y.into(), 1.0)),
+      Transform::ScaleZ(z) => return Some(Matrix3d::scale(1.0, 1.0, z.into())),
+      Transform::Scale3d(x, y, z) => return Some(Matrix3d::scale(x.into(), y.into(), z.into())),
+      Transform::Rotate(angle) | Transform::RotateZ(angle) => {
+        return Some(Matrix3d::rotate(0.0, 0.0, 1.0, to_radians!(angle)))
+      }
+      Transform::RotateX(angle) => return Some(Matrix3d::rotate(1.0, 0.0, 0.0, to_radians!(angle))),
+      Transform::RotateY(angle) => return Some(Matrix3d::rotate(0.0, 1.0, 0.0, to_radians!(angle))),
+      Transform::Rotate3d(x, y, z, angle) => return Some(Matrix3d::rotate(*x, *y, *z, to_radians!(angle))),
+      Transform::Skew(x, y) => return Some(Matrix3d::skew(to_radians!(x), to_radians!(y))),
+      Transform::SkewX(x) => return Some(Matrix3d::skew(to_radians!(x), 0.0)),
+      Transform::SkewY(y) => return Some(Matrix3d::skew(0.0, to_radians!(y))),
+      Transform::Perspective(len) => {
+        if let Some(len) = len.to_px() {
+          return Some(Matrix3d::perspective(len));
+        }
+      }
+      Transform::Matrix(m) => return Some(m.to_matrix3d()),
+      Transform::Matrix3d(m) => return Some(m.clone()),
+      _ => {}
+    }
+    None
+  }
+}
+
+enum_property! {
+  /// A value for the [transform-style](https://drafts.csswg.org/css-transforms-2/#transform-style-property) property.
+  #[allow(missing_docs)]
+  pub enum TransformStyle {
+    Flat,
+    Preserve3d,
+  }
+}
+
+enum_property! {
+  /// A value for the [transform-box](https://drafts.csswg.org/css-transforms-1/#transform-box) property.
+  pub enum TransformBox {
+    /// Uses the content box as reference box.
+    ContentBox,
+    /// Uses the border box as reference box.
+    BorderBox,
+    /// Uses the object bounding box as reference box.
+    FillBox,
+    /// Uses the stroke bounding box as reference box.
+    StrokeBox,
+    /// Uses the nearest SVG viewport as reference box.
+    ViewBox,
+  }
+}
+
+enum_property! {
+  /// A value for the [backface-visibility](https://drafts.csswg.org/css-transforms-2/#backface-visibility-property) property.
+  #[allow(missing_docs)]
+  pub enum BackfaceVisibility {
+    Visible,
+    Hidden,
+  }
+}
+
+/// A value for the [perspective](https://drafts.csswg.org/css-transforms-2/#perspective-property) property.
+#[derive(Debug, Clone, PartialEq, Parse, ToCss)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", content = "value", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum Perspective {
+  /// No perspective transform is applied.
+  None,
+  /// Distance to the center of projection.
+  Length(Length),
+}
+
+/// A value for the [translate](https://drafts.csswg.org/css-transforms-2/#propdef-translate) property.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(rename_all = "lowercase")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum Translate {
+  /// The "none" keyword.
+  None,
+
+  /// The x, y, and z translations.
+  #[cfg_attr(feature = "serde", serde(untagged))]
+  XYZ {
+    /// The x translation.
+    x: LengthPercentage,
+    /// The y translation.
+    y: LengthPercentage,
+    /// The z translation.
+    z: Length,
+  },
+}
+
+impl<'i> Parse<'i> for Translate {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() {
+      return Ok(Translate::None);
+    }
+
+    let x = LengthPercentage::parse(input)?;
+    let y = input.try_parse(LengthPercentage::parse);
+    let z = if y.is_ok() {
+      input.try_parse(Length::parse).ok()
+    } else {
+      None
+    };
+
+    Ok(Translate::XYZ {
+      x,
+      y: y.unwrap_or(LengthPercentage::zero()),
+      z: z.unwrap_or(Length::zero()),
+    })
+  }
+}
+
+impl ToCss for Translate {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    match self {
+      Translate::None => {
+        dest.write_str("none")?;
+      }
+      Translate::XYZ { x, y, z } => {
+        x.to_css(dest)?;
+        if !y.is_zero() || !z.is_zero() {
+          dest.write_char(' ')?;
+          y.to_css(dest)?;
+          if !z.is_zero() {
+            dest.write_char(' ')?;
+            z.to_css(dest)?;
+          }
+        }
+      }
+    };
+
+    Ok(())
+  }
+}
+
+impl Translate {
+  /// Converts the translation to a transform function.
+  pub fn to_transform(&self) -> Transform {
+    match self {
+      Translate::None => {
+        Transform::Translate3d(LengthPercentage::zero(), LengthPercentage::zero(), Length::zero())
+      }
+      Translate::XYZ { x, y, z } => Transform::Translate3d(x.clone(), y.clone(), z.clone()),
+    }
+  }
+}
+
+/// A value for the [rotate](https://drafts.csswg.org/css-transforms-2/#propdef-rotate) property.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub struct Rotate {
+  /// Rotation around the x axis.
+  pub x: f32,
+  /// Rotation around the y axis.
+  pub y: f32,
+  /// Rotation around the z axis.
+  pub z: f32,
+  /// The angle of rotation.
+  pub angle: Angle,
+}
+
+impl<'i> Parse<'i> for Rotate {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() {
+      return Ok(Rotate {
+        x: 0.0,
+        y: 0.0,
+        z: 1.0,
+        angle: Angle::Deg(0.0),
+      });
+    }
+
+    let angle = input.try_parse(Angle::parse);
+    let (x, y, z) = input
+      .try_parse(|input| {
+        let location = input.current_source_location();
+        let ident = input.expect_ident()?;
+        match_ignore_ascii_case! { &*ident,
+          "x" => Ok((1.0, 0.0, 0.0)),
+          "y" => Ok((0.0, 1.0, 0.0)),
+          "z" => Ok((0.0, 0.0, 1.0)),
+          _ => Err(location.new_unexpected_token_error(
+            cssparser::Token::Ident(ident.clone())
+          ))
+        }
+      })
+      .or_else(
+        |_: ParseError<'i, ParserError<'i>>| -> Result<_, ParseError<'i, ParserError<'i>>> {
+          input.try_parse(|input| Ok((f32::parse(input)?, f32::parse(input)?, f32::parse(input)?)))
+        },
+      )
+      .unwrap_or((0.0, 0.0, 1.0));
+    let angle = angle.or_else(|_| Angle::parse(input))?;
+    Ok(Rotate { x, y, z, angle })
+  }
+}
+
+impl ToCss for Rotate {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    if self.x == 0.0 && self.y == 0.0 && self.z == 1.0 && self.angle.is_zero() {
+      dest.write_str("none")?;
+      return Ok(());
+    }
+
+    if self.x == 1.0 && self.y == 0.0 && self.z == 0.0 {
+      dest.write_str("x ")?;
+    } else if self.x == 0.0 && self.y == 1.0 && self.z == 0.0 {
+      dest.write_str("y ")?;
+    } else if !(self.x == 0.0 && self.y == 0.0 && self.z == 1.0) {
+      self.x.to_css(dest)?;
+      dest.write_char(' ')?;
+      self.y.to_css(dest)?;
+      dest.write_char(' ')?;
+      self.z.to_css(dest)?;
+      dest.write_char(' ')?;
+    }
+
+    self.angle.to_css(dest)
+  }
+}
+
+impl Rotate {
+  /// Converts the rotation to a transform function.
+  pub fn to_transform(&self) -> Transform {
+    Transform::Rotate3d(self.x, self.y, self.z, self.angle.clone())
+  }
+}
+
+/// A value for the [scale](https://drafts.csswg.org/css-transforms-2/#propdef-scale) property.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(rename_all = "lowercase")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum Scale {
+  /// The "none" keyword.
+  None,
+
+  /// Scale on the x, y, and z axis.
+  #[cfg_attr(feature = "serde", serde(untagged))]
+  XYZ {
+    /// Scale on the x axis.
+    x: NumberOrPercentage,
+    /// Scale on the y axis.
+    y: NumberOrPercentage,
+    /// Scale on the z axis.
+    z: NumberOrPercentage,
+  },
+}
+
+impl<'i> Parse<'i> for Scale {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() {
+      return Ok(Scale::None);
+    }
+
+    let x = NumberOrPercentage::parse(input)?;
+    let y = input.try_parse(NumberOrPercentage::parse);
+    let z = if y.is_ok() {
+      input.try_parse(NumberOrPercentage::parse).ok()
+    } else {
+      None
+    };
+
+    Ok(Scale::XYZ {
+      x: x.clone(),
+      y: y.unwrap_or(x),
+      z: z.unwrap_or(NumberOrPercentage::Number(1.0)),
+    })
+  }
+}
+
+impl ToCss for Scale {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    match self {
+      Scale::None => {
+        dest.write_str("none")?;
+      }
+      Scale::XYZ { x, y, z } => {
+        x.to_css(dest)?;
+        let zv: f32 = z.into();
+        if y != x || zv != 1.0 {
+          dest.write_char(' ')?;
+          y.to_css(dest)?;
+          if zv != 1.0 {
+            dest.write_char(' ')?;
+            z.to_css(dest)?;
+          }
+        }
+      }
+    }
+
+    Ok(())
+  }
+}
+
+impl Scale {
+  /// Converts the scale to a transform function.
+  pub fn to_transform(&self) -> Transform {
+    match self {
+      Scale::None => Transform::Scale3d(
+        NumberOrPercentage::Number(1.0),
+        NumberOrPercentage::Number(1.0),
+        NumberOrPercentage::Number(1.0),
+      ),
+      Scale::XYZ { x, y, z } => Transform::Scale3d(x.clone(), y.clone(), z.clone()),
+    }
+  }
+}
+
+#[derive(Default)]
+pub(crate) struct TransformHandler {
+  transform: Option<(TransformList, VendorPrefix)>,
+  translate: Option<Translate>,
+  rotate: Option<Rotate>,
+  scale: Option<Scale>,
+  has_any: bool,
+}
+
+impl<'i> PropertyHandler<'i> for TransformHandler {
+  fn handle_property(
+    &mut self,
+    property: &Property<'i>,
+    dest: &mut DeclarationList<'i>,
+    context: &mut PropertyHandlerContext<'i, '_>,
+  ) -> bool {
+    use Property::*;
+
+    macro_rules! individual_property {
+      ($prop: ident, $val: ident) => {
+        if let Some((transform, _)) = &mut self.transform {
+          transform.0.push($val.to_transform())
+        } else {
+          self.$prop = Some($val.clone());
+          self.has_any = true;
+        }
+      };
+    }
+
+    match property {
+      Transform(val, vp) => {
+        // If two vendor prefixes for the same property have different
+        // values, we need to flush what we have immediately to preserve order.
+        if let Some((cur, prefixes)) = &self.transform {
+          if cur != val && !prefixes.contains(*vp) {
+            self.flush(dest, context);
+          }
+        }
+
+        // Otherwise, update the value and add the prefix.
+        if let Some((transform, prefixes)) = &mut self.transform {
+          *transform = val.clone();
+          *prefixes |= *vp;
+        } else {
+          self.transform = Some((val.clone(), *vp));
+          self.has_any = true;
+        }
+
+        self.translate = None;
+        self.rotate = None;
+        self.scale = None;
+      }
+      Translate(val) => individual_property!(translate, val),
+      Rotate(val) => individual_property!(rotate, val),
+      Scale(val) => individual_property!(scale, val),
+      Unparsed(val)
+        if matches!(
+          val.property_id,
+          PropertyId::Transform(_) | PropertyId::Translate | PropertyId::Rotate | PropertyId::Scale
+        ) =>
+      {
+        self.flush(dest, context);
+        let prop = if matches!(val.property_id, PropertyId::Transform(_)) {
+          Property::Unparsed(val.get_prefixed(context.targets, Feature::Transform))
+        } else {
+          property.clone()
+        };
+        dest.push(prop)
+      }
+      _ => return false,
+    }
+
+    true
+  }
+
+  fn finalize(&mut self, dest: &mut DeclarationList<'i>, context: &mut PropertyHandlerContext<'i, '_>) {
+    self.flush(dest, context);
+  }
+}
+
+impl TransformHandler {
+  fn flush<'i>(&mut self, dest: &mut DeclarationList<'i>, context: &mut PropertyHandlerContext<'i, '_>) {
+    if !self.has_any {
+      return;
+    }
+
+    self.has_any = false;
+
+    let transform = std::mem::take(&mut self.transform);
+    let translate = std::mem::take(&mut self.translate);
+    let rotate = std::mem::take(&mut self.rotate);
+    let scale = std::mem::take(&mut self.scale);
+
+    if let Some((transform, prefix)) = transform {
+      let prefix = context.targets.prefixes(prefix, Feature::Transform);
+      dest.push(Property::Transform(transform, prefix))
+    }
+
+    if let Some(translate) = translate {
+      dest.push(Property::Translate(translate))
+    }
+
+    if let Some(rotate) = rotate {
+      dest.push(Property::Rotate(rotate))
+    }
+
+    if let Some(scale) = scale {
+      dest.push(Property::Scale(scale))
+    }
+  }
+}
diff --git a/src/properties/transition.rs b/src/properties/transition.rs
new file mode 100644
index 0000000..8d6a662
--- /dev/null
+++ b/src/properties/transition.rs
@@ -0,0 +1,572 @@
+//! CSS properties related to transitions.
+
+use super::{Property, PropertyId};
+use crate::compat;
+use crate::context::PropertyHandlerContext;
+use crate::declaration::{DeclarationBlock, DeclarationList};
+use crate::error::{ParserError, PrinterError};
+use crate::macros::define_list_shorthand;
+use crate::prefixes::Feature;
+use crate::printer::Printer;
+use crate::properties::masking::get_webkit_mask_property;
+use crate::traits::{Parse, PropertyHandler, Shorthand, ToCss, Zero};
+use crate::values::ident::CustomIdent;
+use crate::values::{easing::EasingFunction, time::Time};
+use crate::vendor_prefix::VendorPrefix;
+#[cfg(feature = "visitor")]
+use crate::visitor::Visit;
+use cssparser::*;
+use itertools::izip;
+use smallvec::SmallVec;
+
+define_list_shorthand! {
+  /// A value for the [transition](https://www.w3.org/TR/2018/WD-css-transitions-1-20181011/#transition-shorthand-property) property.
+  pub struct Transition<'i>(VendorPrefix) {
+    /// The property to transition.
+    #[cfg_attr(feature = "serde", serde(borrow))]
+    property: TransitionProperty(PropertyId<'i>, VendorPrefix),
+    /// The duration of the transition.
+    duration: TransitionDuration(Time, VendorPrefix),
+    /// The delay before the transition starts.
+    delay: TransitionDelay(Time, VendorPrefix),
+    /// The easing function for the transition.
+    timing_function: TransitionTimingFunction(EasingFunction, VendorPrefix),
+  }
+}
+
+impl<'i> Parse<'i> for Transition<'i> {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let mut property = None;
+    let mut duration = None;
+    let mut delay = None;
+    let mut timing_function = None;
+
+    loop {
+      if duration.is_none() {
+        if let Ok(value) = input.try_parse(Time::parse) {
+          duration = Some(value);
+          continue;
+        }
+      }
+
+      if timing_function.is_none() {
+        if let Ok(value) = input.try_parse(EasingFunction::parse) {
+          timing_function = Some(value);
+          continue;
+        }
+      }
+
+      if delay.is_none() {
+        if let Ok(value) = input.try_parse(Time::parse) {
+          delay = Some(value);
+          continue;
+        }
+      }
+
+      if property.is_none() {
+        if let Ok(value) = input.try_parse(PropertyId::parse) {
+          property = Some(value);
+          continue;
+        }
+      }
+
+      break;
+    }
+
+    Ok(Transition {
+      property: property.unwrap_or(PropertyId::All),
+      duration: duration.unwrap_or(Time::Seconds(0.0)),
+      delay: delay.unwrap_or(Time::Seconds(0.0)),
+      timing_function: timing_function.unwrap_or(EasingFunction::Ease),
+    })
+  }
+}
+
+impl<'i> ToCss for Transition<'i> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    self.property.to_css(dest)?;
+    if !self.duration.is_zero() || !self.delay.is_zero() {
+      dest.write_char(' ')?;
+      self.duration.to_css(dest)?;
+    }
+
+    if !self.timing_function.is_ease() {
+      dest.write_char(' ')?;
+      self.timing_function.to_css(dest)?;
+    }
+
+    if !self.delay.is_zero() {
+      dest.write_char(' ')?;
+      self.delay.to_css(dest)?;
+    }
+
+    Ok(())
+  }
+}
+
+/// A value for the [view-transition-name](https://drafts.csswg.org/css-view-transitions-1/#view-transition-name-prop) property.
+#[derive(Debug, Clone, PartialEq, Default, Parse, ToCss)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum ViewTransitionName<'i> {
+  /// The element will not participate independently in a view transition.
+  #[default]
+  None,
+  /// The `auto` keyword.
+  Auto,
+  /// A custom name.
+  #[cfg_attr(feature = "serde", serde(borrow, untagged))]
+  Custom(CustomIdent<'i>),
+}
+
+/// A value for the [view-transition-group](https://drafts.csswg.org/css-view-transitions-2/#view-transition-group-prop) property.
+#[derive(Debug, Clone, PartialEq, Default, Parse, ToCss)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum ViewTransitionGroup<'i> {
+  /// The `normal` keyword.
+  #[default]
+  Normal,
+  /// The `contain` keyword.
+  Contain,
+  /// The `nearest` keyword.
+  Nearest,
+  /// A custom group.
+  #[cfg_attr(feature = "serde", serde(borrow, untagged))]
+  Custom(CustomIdent<'i>),
+}
+
+#[derive(Default)]
+pub(crate) struct TransitionHandler<'i> {
+  properties: Option<(SmallVec<[PropertyId<'i>; 1]>, VendorPrefix)>,
+  durations: Option<(SmallVec<[Time; 1]>, VendorPrefix)>,
+  delays: Option<(SmallVec<[Time; 1]>, VendorPrefix)>,
+  timing_functions: Option<(SmallVec<[EasingFunction; 1]>, VendorPrefix)>,
+  has_any: bool,
+}
+
+impl<'i> PropertyHandler<'i> for TransitionHandler<'i> {
+  fn handle_property(
+    &mut self,
+    property: &Property<'i>,
+    dest: &mut DeclarationList<'i>,
+    context: &mut PropertyHandlerContext<'i, '_>,
+  ) -> bool {
+    use Property::*;
+
+    macro_rules! maybe_flush {
+      ($prop: ident, $val: expr, $vp: ident) => {{
+        // If two vendor prefixes for the same property have different
+        // values, we need to flush what we have immediately to preserve order.
+        if let Some((val, prefixes)) = &self.$prop {
+          if val != $val && !prefixes.contains(*$vp) {
+            self.flush(dest, context);
+          }
+        }
+      }};
+    }
+
+    macro_rules! property {
+      ($feature: ident, $prop: ident, $val: expr, $vp: ident) => {{
+        maybe_flush!($prop, $val, $vp);
+
+        // Otherwise, update the value and add the prefix.
+        if let Some((val, prefixes)) = &mut self.$prop {
+          *val = $val.clone();
+          *prefixes |= *$vp;
+          *prefixes = context.targets.prefixes(*prefixes, Feature::$feature);
+        } else {
+          let prefixes = context.targets.prefixes(*$vp, Feature::$feature);
+          self.$prop = Some(($val.clone(), prefixes));
+          self.has_any = true;
+        }
+      }};
+    }
+
+    match property {
+      TransitionProperty(val, vp) => {
+        let merged_values = merge_properties(val.iter());
+        property!(TransitionProperty, properties, &merged_values, vp);
+      }
+      TransitionDuration(val, vp) => property!(TransitionDuration, durations, val, vp),
+      TransitionDelay(val, vp) => property!(TransitionDelay, delays, val, vp),
+      TransitionTimingFunction(val, vp) => property!(TransitionTimingFunction, timing_functions, val, vp),
+      Transition(val, vp) => {
+        let properties: SmallVec<[PropertyId; 1]> = merge_properties(val.iter().map(|b| &b.property));
+        maybe_flush!(properties, &properties, vp);
+
+        let durations: SmallVec<[Time; 1]> = val.iter().map(|b| b.duration.clone()).collect();
+        maybe_flush!(durations, &durations, vp);
+
+        let delays: SmallVec<[Time; 1]> = val.iter().map(|b| b.delay.clone()).collect();
+        maybe_flush!(delays, &delays, vp);
+
+        let timing_functions: SmallVec<[EasingFunction; 1]> =
+          val.iter().map(|b| b.timing_function.clone()).collect();
+        maybe_flush!(timing_functions, &timing_functions, vp);
+
+        property!(TransitionProperty, properties, &properties, vp);
+        property!(TransitionDuration, durations, &durations, vp);
+        property!(TransitionDelay, delays, &delays, vp);
+        property!(TransitionTimingFunction, timing_functions, &timing_functions, vp);
+      }
+      Unparsed(val) if is_transition_property(&val.property_id) => {
+        self.flush(dest, context);
+        dest.push(Property::Unparsed(
+          val.get_prefixed(context.targets, Feature::Transition),
+        ));
+      }
+      _ => return false,
+    }
+
+    true
+  }
+
+  fn finalize(&mut self, dest: &mut DeclarationList<'i>, context: &mut PropertyHandlerContext<'i, '_>) {
+    self.flush(dest, context);
+  }
+}
+
+impl<'i> TransitionHandler<'i> {
+  fn flush(&mut self, dest: &mut DeclarationList<'i>, context: &mut PropertyHandlerContext<'i, '_>) {
+    if !self.has_any {
+      return;
+    }
+
+    self.has_any = false;
+
+    let mut properties = std::mem::take(&mut self.properties);
+    let mut durations = std::mem::take(&mut self.durations);
+    let mut delays = std::mem::take(&mut self.delays);
+    let mut timing_functions = std::mem::take(&mut self.timing_functions);
+
+    let rtl_properties = if let Some((properties, _)) = &mut properties {
+      expand_properties(properties, context)
+    } else {
+      None
+    };
+
+    if let (
+      Some((properties, property_prefixes)),
+      Some((durations, duration_prefixes)),
+      Some((delays, delay_prefixes)),
+      Some((timing_functions, timing_prefixes)),
+    ) = (&mut properties, &mut durations, &mut delays, &mut timing_functions)
+    {
+      // Find the intersection of prefixes with the same value.
+      // Remove that from the prefixes of each of the properties. The remaining
+      // prefixes will be handled by outputting individual properties below.
+      let intersection = *property_prefixes & *duration_prefixes & *delay_prefixes & *timing_prefixes;
+      if !intersection.is_empty() {
+        macro_rules! get_transitions {
+          ($properties: ident) => {{
+            // transition-property determines the number of transitions. The values of other
+            // properties are repeated to match this length.
+            let mut transitions = SmallVec::with_capacity($properties.len());
+            let mut durations_iter = durations.iter().cycle().cloned();
+            let mut delays_iter = delays.iter().cycle().cloned();
+            let mut timing_iter = timing_functions.iter().cycle().cloned();
+            for property_id in $properties {
+              let duration = durations_iter.next().unwrap_or(Time::Seconds(0.0));
+              let delay = delays_iter.next().unwrap_or(Time::Seconds(0.0));
+              let timing_function = timing_iter.next().unwrap_or(EasingFunction::Ease);
+              let transition = Transition {
+                property: property_id.clone(),
+                duration,
+                delay,
+                timing_function,
+              };
+
+              // Expand vendor prefixes into multiple transitions.
+              for p in property_id.prefix().or_none() {
+                let mut t = transition.clone();
+                t.property = property_id.with_prefix(p);
+                transitions.push(t);
+              }
+            }
+            transitions
+          }};
+        }
+
+        let transitions: SmallVec<[Transition; 1]> = get_transitions!(properties);
+
+        if let Some(rtl_properties) = &rtl_properties {
+          let rtl_transitions = get_transitions!(rtl_properties);
+          context.add_logical_rule(
+            Property::Transition(transitions, intersection),
+            Property::Transition(rtl_transitions, intersection),
+          );
+        } else {
+          dest.push(Property::Transition(transitions.clone(), intersection));
+        }
+
+        property_prefixes.remove(intersection);
+        duration_prefixes.remove(intersection);
+        delay_prefixes.remove(intersection);
+        timing_prefixes.remove(intersection);
+      }
+    }
+
+    if let Some((properties, prefix)) = properties {
+      if !prefix.is_empty() {
+        if let Some(rtl_properties) = rtl_properties {
+          context.add_logical_rule(
+            Property::TransitionProperty(properties, prefix),
+            Property::TransitionProperty(rtl_properties, prefix),
+          );
+        } else {
+          dest.push(Property::TransitionProperty(properties, prefix));
+        }
+      }
+    }
+
+    if let Some((durations, prefix)) = durations {
+      if !prefix.is_empty() {
+        dest.push(Property::TransitionDuration(durations, prefix));
+      }
+    }
+
+    if let Some((delays, prefix)) = delays {
+      if !prefix.is_empty() {
+        dest.push(Property::TransitionDelay(delays, prefix));
+      }
+    }
+
+    if let Some((timing_functions, prefix)) = timing_functions {
+      if !prefix.is_empty() {
+        dest.push(Property::TransitionTimingFunction(timing_functions, prefix));
+      }
+    }
+
+    self.reset();
+  }
+
+  fn reset(&mut self) {
+    self.properties = None;
+    self.durations = None;
+    self.delays = None;
+    self.timing_functions = None;
+  }
+}
+
+#[inline]
+fn is_transition_property(property_id: &PropertyId) -> bool {
+  match property_id {
+    PropertyId::TransitionProperty(_)
+    | PropertyId::TransitionDuration(_)
+    | PropertyId::TransitionDelay(_)
+    | PropertyId::TransitionTimingFunction(_)
+    | PropertyId::Transition(_) => true,
+    _ => false,
+  }
+}
+
+fn merge_properties<'i: 'a, 'a>(val: impl Iterator<Item = &'a PropertyId<'i>>) -> SmallVec<[PropertyId<'i>; 1]> {
+  let mut merged_values = SmallVec::<[PropertyId<'_>; 1]>::with_capacity(val.size_hint().1.unwrap_or(1));
+  for p in val {
+    let without_prefix = p.with_prefix(VendorPrefix::empty());
+    if let Some(idx) = merged_values
+      .iter()
+      .position(|c| c.with_prefix(VendorPrefix::empty()) == without_prefix)
+    {
+      merged_values[idx].add_prefix(p.prefix());
+    } else {
+      merged_values.push(p.clone());
+    }
+  }
+
+  merged_values
+}
+
+fn expand_properties<'i>(
+  properties: &mut SmallVec<[PropertyId<'i>; 1]>,
+  context: &mut PropertyHandlerContext,
+) -> Option<SmallVec<[PropertyId<'i>; 1]>> {
+  let mut rtl_properties: Option<SmallVec<[PropertyId; 1]>> = None;
+  let mut i = 0;
+
+  macro_rules! replace {
+    ($properties: ident, $props: ident) => {
+      $properties[i] = $props[0].clone();
+      if $props.len() > 1 {
+        $properties.insert_many(i + 1, $props[1..].into_iter().cloned());
+      }
+    };
+  }
+
+  // Expand logical properties in place.
+  while i < properties.len() {
+    match get_logical_properties(&properties[i]) {
+      LogicalPropertyId::Block(feature, props) if context.should_compile_logical(feature) => {
+        replace!(properties, props);
+        if let Some(rtl_properties) = &mut rtl_properties {
+          replace!(rtl_properties, props);
+        }
+        i += props.len();
+      }
+      LogicalPropertyId::Inline(feature, ltr, rtl) if context.should_compile_logical(feature) => {
+        // Clone properties to create RTL version only when needed.
+        if rtl_properties.is_none() {
+          rtl_properties = Some(properties.clone());
+        }
+
+        replace!(properties, ltr);
+        if let Some(rtl_properties) = &mut rtl_properties {
+          replace!(rtl_properties, rtl);
+        }
+
+        i += ltr.len();
+      }
+      _ => {
+        // Expand vendor prefixes for targets.
+        properties[i].set_prefixes_for_targets(context.targets);
+
+        // Expand mask properties, which use different vendor-prefixed names.
+        if let Some(property_id) = get_webkit_mask_property(&properties[i]) {
+          if context
+            .targets
+            .prefixes(VendorPrefix::None, Feature::MaskBorder)
+            .contains(VendorPrefix::WebKit)
+          {
+            properties.insert(i, property_id);
+            i += 1;
+          }
+        }
+
+        if let Some(rtl_properties) = &mut rtl_properties {
+          rtl_properties[i].set_prefixes_for_targets(context.targets);
+
+          if let Some(property_id) = get_webkit_mask_property(&rtl_properties[i]) {
+            if context
+              .targets
+              .prefixes(VendorPrefix::None, Feature::MaskBorder)
+              .contains(VendorPrefix::WebKit)
+            {
+              rtl_properties.insert(i, property_id);
+            }
+          }
+        }
+        i += 1;
+      }
+    }
+  }
+
+  rtl_properties
+}
+
+enum LogicalPropertyId {
+  None,
+  Block(compat::Feature, &'static [PropertyId<'static>]),
+  Inline(
+    compat::Feature,
+    &'static [PropertyId<'static>],
+    &'static [PropertyId<'static>],
+  ),
+}
+
+#[inline]
+fn get_logical_properties(property_id: &PropertyId) -> LogicalPropertyId {
+  use compat::Feature::*;
+  use LogicalPropertyId::*;
+  use PropertyId::*;
+  match property_id {
+    BlockSize => Block(LogicalSize, &[Height]),
+    InlineSize => Inline(LogicalSize, &[Width], &[Height]),
+    MinBlockSize => Block(LogicalSize, &[MinHeight]),
+    MaxBlockSize => Block(LogicalSize, &[MaxHeight]),
+    MinInlineSize => Inline(LogicalSize, &[MinWidth], &[MinHeight]),
+    MaxInlineSize => Inline(LogicalSize, &[MaxWidth], &[MaxHeight]),
+
+    InsetBlockStart => Block(LogicalInset, &[Top]),
+    InsetBlockEnd => Block(LogicalInset, &[Bottom]),
+    InsetInlineStart => Inline(LogicalInset, &[Left], &[Right]),
+    InsetInlineEnd => Inline(LogicalInset, &[Right], &[Left]),
+    InsetBlock => Block(LogicalInset, &[Top, Bottom]),
+    InsetInline => Block(LogicalInset, &[Left, Right]),
+    Inset => Block(LogicalInset, &[Top, Bottom, Left, Right]),
+
+    MarginBlockStart => Block(LogicalMargin, &[MarginTop]),
+    MarginBlockEnd => Block(LogicalMargin, &[MarginBottom]),
+    MarginInlineStart => Inline(LogicalMargin, &[MarginLeft], &[MarginRight]),
+    MarginInlineEnd => Inline(LogicalMargin, &[MarginRight], &[MarginLeft]),
+    MarginBlock => Block(LogicalMargin, &[MarginTop, MarginBottom]),
+    MarginInline => Block(LogicalMargin, &[MarginLeft, MarginRight]),
+
+    PaddingBlockStart => Block(LogicalPadding, &[PaddingTop]),
+    PaddingBlockEnd => Block(LogicalPadding, &[PaddingBottom]),
+    PaddingInlineStart => Inline(LogicalPadding, &[PaddingLeft], &[PaddingRight]),
+    PaddingInlineEnd => Inline(LogicalPadding, &[PaddingRight], &[PaddingLeft]),
+    PaddingBlock => Block(LogicalPadding, &[PaddingTop, PaddingBottom]),
+    PaddingInline => Block(LogicalPadding, &[PaddingLeft, PaddingRight]),
+
+    BorderBlockStart => Block(LogicalBorders, &[BorderTop]),
+    BorderBlockStartWidth => Block(LogicalBorders, &[BorderTopWidth]),
+    BorderBlockStartColor => Block(LogicalBorders, &[BorderTopColor]),
+    BorderBlockStartStyle => Block(LogicalBorders, &[BorderTopStyle]),
+
+    BorderBlockEnd => Block(LogicalBorders, &[BorderBottom]),
+    BorderBlockEndWidth => Block(LogicalBorders, &[BorderBottomWidth]),
+    BorderBlockEndColor => Block(LogicalBorders, &[BorderBottomColor]),
+    BorderBlockEndStyle => Block(LogicalBorders, &[BorderBottomStyle]),
+
+    BorderInlineStart => Inline(LogicalBorders, &[BorderLeft], &[BorderRight]),
+    BorderInlineStartWidth => Inline(LogicalBorders, &[BorderLeftWidth], &[BorderRightWidth]),
+    BorderInlineStartColor => Inline(LogicalBorders, &[BorderLeftColor], &[BorderRightColor]),
+    BorderInlineStartStyle => Inline(LogicalBorders, &[BorderLeftStyle], &[BorderRightStyle]),
+
+    BorderInlineEnd => Inline(LogicalBorders, &[BorderRight], &[BorderLeft]),
+    BorderInlineEndWidth => Inline(LogicalBorders, &[BorderRightWidth], &[BorderLeftWidth]),
+    BorderInlineEndColor => Inline(LogicalBorders, &[BorderRightColor], &[BorderLeftColor]),
+    BorderInlineEndStyle => Inline(LogicalBorders, &[BorderRightStyle], &[BorderLeftStyle]),
+
+    BorderBlock => Block(LogicalBorders, &[BorderTop, BorderBottom]),
+    BorderBlockColor => Block(LogicalBorders, &[BorderTopColor, BorderBottomColor]),
+    BorderBlockWidth => Block(LogicalBorders, &[BorderTopWidth, BorderBottomWidth]),
+    BorderBlockStyle => Block(LogicalBorders, &[BorderTopStyle, BorderBottomStyle]),
+
+    BorderInline => Block(LogicalBorders, &[BorderLeft, BorderRight]),
+    BorderInlineColor => Block(LogicalBorders, &[BorderLeftColor, BorderRightColor]),
+    BorderInlineWidth => Block(LogicalBorders, &[BorderLeftWidth, BorderRightWidth]),
+    BorderInlineStyle => Block(LogicalBorders, &[BorderLeftStyle, BorderRightStyle]),
+
+    // Not worth using vendor prefixes for these since border-radius is supported
+    // everywhere custom properties (which are used to polyfill logical properties) are.
+    BorderStartStartRadius => Inline(
+      LogicalBorders,
+      &[BorderTopLeftRadius(VendorPrefix::None)],
+      &[BorderTopRightRadius(VendorPrefix::None)],
+    ),
+    BorderStartEndRadius => Inline(
+      LogicalBorders,
+      &[BorderTopRightRadius(VendorPrefix::None)],
+      &[BorderTopLeftRadius(VendorPrefix::None)],
+    ),
+    BorderEndStartRadius => Inline(
+      LogicalBorders,
+      &[BorderBottomLeftRadius(VendorPrefix::None)],
+      &[BorderBottomRightRadius(VendorPrefix::None)],
+    ),
+    BorderEndEndRadius => Inline(
+      LogicalBorders,
+      &[BorderBottomRightRadius(VendorPrefix::None)],
+      &[BorderBottomLeftRadius(VendorPrefix::None)],
+    ),
+
+    _ => None,
+  }
+}
diff --git a/src/properties/ui.rs b/src/properties/ui.rs
new file mode 100644
index 0000000..9b0bb71
--- /dev/null
+++ b/src/properties/ui.rs
@@ -0,0 +1,580 @@
+//! CSS properties related to user interface.
+
+use crate::context::PropertyHandlerContext;
+use crate::declaration::{DeclarationBlock, DeclarationList};
+use crate::error::{ParserError, PrinterError};
+use crate::macros::{define_shorthand, enum_property, shorthand_property};
+use crate::printer::Printer;
+use crate::properties::{Property, PropertyId};
+use crate::targets::{should_compile, Browsers, Targets};
+use crate::traits::{FallbackValues, IsCompatible, Parse, PropertyHandler, Shorthand, ToCss};
+use crate::values::color::CssColor;
+use crate::values::number::CSSNumber;
+use crate::values::string::CowArcStr;
+use crate::values::url::Url;
+#[cfg(feature = "visitor")]
+use crate::visitor::Visit;
+use bitflags::bitflags;
+use cssparser::*;
+use smallvec::SmallVec;
+
+use super::custom::Token;
+use super::{CustomProperty, CustomPropertyName, TokenList, TokenOrValue};
+
+enum_property! {
+  /// A value for the [resize](https://www.w3.org/TR/2021/WD-css-ui-4-20210316/#resize) property.
+  pub enum Resize {
+    /// The element does not allow resizing.
+    None,
+    /// The element is resizable in both the x and y directions.
+    Both,
+    /// The element is resizable in the x direction.
+    Horizontal,
+    /// The element is resizable in the y direction.
+    Vertical,
+    /// The element is resizable in the block direction, according to the writing mode.
+    Block,
+    /// The element is resizable in the inline direction, according to the writing mode.
+    Inline,
+  }
+}
+
+/// A [cursor image](https://www.w3.org/TR/2021/WD-css-ui-4-20210316/#cursor) value, used in the `cursor` property.
+///
+/// See [Cursor](Cursor).
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub struct CursorImage<'i> {
+  /// A url to the cursor image.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  pub url: Url<'i>,
+  /// The location in the image where the mouse pointer appears.
+  pub hotspot: Option<(CSSNumber, CSSNumber)>,
+}
+
+impl<'i> Parse<'i> for CursorImage<'i> {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let url = Url::parse(input)?;
+    let hotspot = if let Ok(x) = input.try_parse(CSSNumber::parse) {
+      let y = CSSNumber::parse(input)?;
+      Some((x, y))
+    } else {
+      None
+    };
+
+    Ok(CursorImage { url, hotspot })
+  }
+}
+
+impl<'i> ToCss for CursorImage<'i> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    self.url.to_css(dest)?;
+
+    if let Some((x, y)) = self.hotspot {
+      dest.write_char(' ')?;
+      x.to_css(dest)?;
+      dest.write_char(' ')?;
+      y.to_css(dest)?;
+    }
+    Ok(())
+  }
+}
+
+enum_property! {
+  /// A pre-defined [cursor](https://www.w3.org/TR/2021/WD-css-ui-4-20210316/#cursor) value,
+  /// used in the `cursor` property.
+  ///
+  /// See [Cursor](Cursor).
+  #[allow(missing_docs)]
+  pub enum CursorKeyword {
+    Auto,
+    Default,
+    None,
+    ContextMenu,
+    Help,
+    Pointer,
+    Progress,
+    Wait,
+    Cell,
+    Crosshair,
+    Text,
+    VerticalText,
+    Alias,
+    Copy,
+    Move,
+    NoDrop,
+    NotAllowed,
+    Grab,
+    Grabbing,
+    EResize,
+    NResize,
+    NeResize,
+    NwResize,
+    SResize,
+    SeResize,
+    SwResize,
+    WResize,
+    EwResize,
+    NsResize,
+    NeswResize,
+    NwseResize,
+    ColResize,
+    RowResize,
+    AllScroll,
+    ZoomIn,
+    ZoomOut,
+  }
+}
+
+/// A value for the [cursor](https://www.w3.org/TR/2021/WD-css-ui-4-20210316/#cursor) property.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub struct Cursor<'i> {
+  /// A list of cursor images.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  pub images: SmallVec<[CursorImage<'i>; 1]>,
+  /// A pre-defined cursor.
+  pub keyword: CursorKeyword,
+}
+
+impl<'i> Parse<'i> for Cursor<'i> {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let mut images = SmallVec::new();
+    loop {
+      match input.try_parse(CursorImage::parse) {
+        Ok(image) => images.push(image),
+        Err(_) => break,
+      }
+      input.expect_comma()?;
+    }
+
+    Ok(Cursor {
+      images,
+      keyword: CursorKeyword::parse(input)?,
+    })
+  }
+}
+
+impl<'i> ToCss for Cursor<'i> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    for image in &self.images {
+      image.to_css(dest)?;
+      dest.delim(',', false)?;
+    }
+    self.keyword.to_css(dest)
+  }
+}
+
+/// A value for the [caret-color](https://www.w3.org/TR/2021/WD-css-ui-4-20210316/#caret-color) property.
+#[derive(Debug, Clone, PartialEq, Parse, ToCss)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", content = "value", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum ColorOrAuto {
+  /// The `currentColor`, adjusted by the UA to ensure contrast against the background.
+  Auto,
+  /// A color.
+  Color(CssColor),
+}
+
+impl Default for ColorOrAuto {
+  fn default() -> ColorOrAuto {
+    ColorOrAuto::Auto
+  }
+}
+
+impl FallbackValues for ColorOrAuto {
+  fn get_fallbacks(&mut self, targets: Targets) -> Vec<Self> {
+    match self {
+      ColorOrAuto::Color(color) => color
+        .get_fallbacks(targets)
+        .into_iter()
+        .map(|color| ColorOrAuto::Color(color))
+        .collect(),
+      ColorOrAuto::Auto => Vec::new(),
+    }
+  }
+}
+
+impl IsCompatible for ColorOrAuto {
+  fn is_compatible(&self, browsers: Browsers) -> bool {
+    match self {
+      ColorOrAuto::Color(color) => color.is_compatible(browsers),
+      ColorOrAuto::Auto => true,
+    }
+  }
+}
+
+enum_property! {
+  /// A value for the [caret-shape](https://www.w3.org/TR/2021/WD-css-ui-4-20210316/#caret-shape) property.
+  pub enum CaretShape {
+    /// The UA determines the caret shape.
+    Auto,
+    /// A thin bar caret.
+    Bar,
+    /// A rectangle caret.
+    Block,
+    /// An underscore caret.
+    Underscore,
+  }
+}
+
+impl Default for CaretShape {
+  fn default() -> CaretShape {
+    CaretShape::Auto
+  }
+}
+
+shorthand_property! {
+  /// A value for the [caret](https://www.w3.org/TR/2021/WD-css-ui-4-20210316/#caret) shorthand property.
+  pub struct Caret {
+    /// The caret color.
+    color: CaretColor(ColorOrAuto),
+    /// The caret shape.
+    shape: CaretShape(CaretShape),
+  }
+}
+
+impl FallbackValues for Caret {
+  fn get_fallbacks(&mut self, targets: Targets) -> Vec<Self> {
+    self
+      .color
+      .get_fallbacks(targets)
+      .into_iter()
+      .map(|color| Caret {
+        color,
+        shape: self.shape.clone(),
+      })
+      .collect()
+  }
+}
+
+impl IsCompatible for Caret {
+  fn is_compatible(&self, browsers: Browsers) -> bool {
+    self.color.is_compatible(browsers)
+  }
+}
+
+enum_property! {
+  /// A value for the [user-select](https://www.w3.org/TR/2021/WD-css-ui-4-20210316/#content-selection) property.
+  pub enum UserSelect {
+    /// The UA determines whether text is selectable.
+    Auto,
+    /// Text is selectable.
+    Text,
+    /// Text is not selectable.
+    None,
+    /// Text selection is contained to the element.
+    Contain,
+    /// Only the entire element is selectable.
+    All,
+  }
+}
+
+/// A value for the [appearance](https://www.w3.org/TR/2021/WD-css-ui-4-20210316/#appearance-switching) property.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[allow(missing_docs)]
+pub enum Appearance<'i> {
+  None,
+  Auto,
+  Textfield,
+  MenulistButton,
+  Button,
+  Checkbox,
+  Listbox,
+  Menulist,
+  Meter,
+  ProgressBar,
+  PushButton,
+  Radio,
+  Searchfield,
+  SliderHorizontal,
+  SquareButton,
+  Textarea,
+  NonStandard(CowArcStr<'i>),
+}
+
+impl<'i> Appearance<'i> {
+  fn from_str(name: &str) -> Option<Self> {
+    Some(match_ignore_ascii_case! { &name,
+      "none" => Appearance::None,
+      "auto" => Appearance::Auto,
+      "textfield" => Appearance::Textfield,
+      "menulist-button" => Appearance::MenulistButton,
+      "button" => Appearance::Button,
+      "checkbox" => Appearance::Checkbox,
+      "listbox" => Appearance::Listbox,
+      "menulist" => Appearance::Menulist,
+      "meter" => Appearance::Meter,
+      "progress-bar" => Appearance::ProgressBar,
+      "push-button" => Appearance::PushButton,
+      "radio" => Appearance::Radio,
+      "searchfield" => Appearance::Searchfield,
+      "slider-horizontal" => Appearance::SliderHorizontal,
+      "square-button" => Appearance::SquareButton,
+      "textarea" => Appearance::Textarea,
+      _ => return None
+    })
+  }
+
+  fn to_str(&self) -> &str {
+    match self {
+      Appearance::None => "none",
+      Appearance::Auto => "auto",
+      Appearance::Textfield => "textfield",
+      Appearance::MenulistButton => "menulist-button",
+      Appearance::Button => "button",
+      Appearance::Checkbox => "checkbox",
+      Appearance::Listbox => "listbox",
+      Appearance::Menulist => "menulist",
+      Appearance::Meter => "meter",
+      Appearance::ProgressBar => "progress-bar",
+      Appearance::PushButton => "push-button",
+      Appearance::Radio => "radio",
+      Appearance::Searchfield => "searchfield",
+      Appearance::SliderHorizontal => "slider-horizontal",
+      Appearance::SquareButton => "square-button",
+      Appearance::Textarea => "textarea",
+      Appearance::NonStandard(s) => s.as_ref(),
+    }
+  }
+}
+
+impl<'i> Parse<'i> for Appearance<'i> {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let ident = input.expect_ident()?;
+    Ok(Self::from_str(ident.as_ref()).unwrap_or_else(|| Appearance::NonStandard(ident.into())))
+  }
+}
+
+impl<'i> ToCss for Appearance<'i> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    dest.write_str(self.to_str())
+  }
+}
+
+#[cfg(feature = "serde")]
+#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
+impl<'i> serde::Serialize for Appearance<'i> {
+  fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+  where
+    S: serde::Serializer,
+  {
+    serializer.serialize_str(self.to_str())
+  }
+}
+
+#[cfg(feature = "serde")]
+#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
+impl<'i, 'de: 'i> serde::Deserialize<'de> for Appearance<'i> {
+  fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+  where
+    D: serde::Deserializer<'de>,
+  {
+    let s = CowArcStr::deserialize(deserializer)?;
+    Ok(Self::from_str(s.as_ref()).unwrap_or_else(|| Appearance::NonStandard(s)))
+  }
+}
+
+#[cfg(feature = "jsonschema")]
+#[cfg_attr(docsrs, doc(cfg(feature = "jsonschema")))]
+impl<'a> schemars::JsonSchema for Appearance<'a> {
+  fn is_referenceable() -> bool {
+    true
+  }
+
+  fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
+    str::json_schema(gen)
+  }
+
+  fn schema_name() -> String {
+    "Appearance".into()
+  }
+}
+
+bitflags! {
+  /// A value for the [color-scheme](https://drafts.csswg.org/css-color-adjust/#color-scheme-prop) property.
+  #[cfg_attr(feature = "visitor", derive(Visit))]
+  #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(from = "SerializedColorScheme", into = "SerializedColorScheme"))]
+  #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+  #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Clone, Copy)]
+  pub struct ColorScheme: u8 {
+    /// Indicates that the element supports a light color scheme.
+    const Light    = 0b01;
+    /// Indicates that the element supports a dark color scheme.
+    const Dark     = 0b10;
+    /// Forbids the user agent from overriding the color scheme for the element.
+    const Only     = 0b100;
+  }
+}
+
+impl<'i> Parse<'i> for ColorScheme {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let mut res = ColorScheme::empty();
+    let ident = input.expect_ident()?;
+    match_ignore_ascii_case! { &ident,
+      "normal" => return Ok(res),
+      "only" => res |= ColorScheme::Only,
+      "light" => res |= ColorScheme::Light,
+      "dark" => res |= ColorScheme::Dark,
+      _ => {}
+    };
+
+    while let Ok(ident) = input.try_parse(|input| input.expect_ident_cloned()) {
+      match_ignore_ascii_case! { &ident,
+        "normal" => return Err(input.new_custom_error(ParserError::InvalidValue)),
+        "only" => {
+          // Only must be at the start or the end, not in the middle.
+          if res.contains(ColorScheme::Only) {
+            return Err(input.new_custom_error(ParserError::InvalidValue));
+          }
+          res |= ColorScheme::Only;
+          return Ok(res);
+        },
+        "light" => res |= ColorScheme::Light,
+        "dark" => res |= ColorScheme::Dark,
+        _ => {}
+      };
+    }
+
+    Ok(res)
+  }
+}
+
+impl ToCss for ColorScheme {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    if self.is_empty() {
+      return dest.write_str("normal");
+    }
+
+    if self.contains(ColorScheme::Light) {
+      dest.write_str("light")?;
+      if self.contains(ColorScheme::Dark) {
+        dest.write_char(' ')?;
+      }
+    }
+
+    if self.contains(ColorScheme::Dark) {
+      dest.write_str("dark")?;
+    }
+
+    if self.contains(ColorScheme::Only) {
+      dest.write_str(" only")?;
+    }
+
+    Ok(())
+  }
+}
+
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+struct SerializedColorScheme {
+  light: bool,
+  dark: bool,
+  only: bool,
+}
+
+impl From<ColorScheme> for SerializedColorScheme {
+  fn from(color_scheme: ColorScheme) -> Self {
+    Self {
+      light: color_scheme.contains(ColorScheme::Light),
+      dark: color_scheme.contains(ColorScheme::Dark),
+      only: color_scheme.contains(ColorScheme::Only),
+    }
+  }
+}
+
+impl From<SerializedColorScheme> for ColorScheme {
+  fn from(s: SerializedColorScheme) -> ColorScheme {
+    let mut color_scheme = ColorScheme::empty();
+    color_scheme.set(ColorScheme::Light, s.light);
+    color_scheme.set(ColorScheme::Dark, s.dark);
+    color_scheme.set(ColorScheme::Only, s.only);
+    color_scheme
+  }
+}
+
+#[cfg(feature = "jsonschema")]
+#[cfg_attr(docsrs, doc(cfg(feature = "jsonschema")))]
+impl<'a> schemars::JsonSchema for ColorScheme {
+  fn is_referenceable() -> bool {
+    true
+  }
+
+  fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
+    SerializedColorScheme::json_schema(gen)
+  }
+
+  fn schema_name() -> String {
+    "ColorScheme".into()
+  }
+}
+
+#[derive(Default)]
+pub(crate) struct ColorSchemeHandler;
+
+impl<'i> PropertyHandler<'i> for ColorSchemeHandler {
+  fn handle_property(
+    &mut self,
+    property: &Property<'i>,
+    dest: &mut DeclarationList<'i>,
+    context: &mut PropertyHandlerContext<'i, '_>,
+  ) -> bool {
+    match property {
+      Property::ColorScheme(color_scheme) => {
+        if should_compile!(context.targets, LightDark) {
+          if color_scheme.contains(ColorScheme::Light) {
+            dest.push(define_var("--lightningcss-light", Token::Ident("initial".into())));
+            dest.push(define_var("--lightningcss-dark", Token::WhiteSpace(" ".into())));
+
+            if color_scheme.contains(ColorScheme::Dark) {
+              context.add_dark_rule(define_var("--lightningcss-light", Token::WhiteSpace(" ".into())));
+              context.add_dark_rule(define_var("--lightningcss-dark", Token::Ident("initial".into())));
+            }
+          } else if color_scheme.contains(ColorScheme::Dark) {
+            dest.push(define_var("--lightningcss-light", Token::WhiteSpace(" ".into())));
+            dest.push(define_var("--lightningcss-dark", Token::Ident("initial".into())));
+          }
+        }
+        dest.push(property.clone());
+        true
+      }
+      _ => false,
+    }
+  }
+
+  fn finalize(&mut self, _: &mut DeclarationList<'i>, _: &mut PropertyHandlerContext<'i, '_>) {}
+}
+
+#[inline]
+fn define_var<'i>(name: &'static str, value: Token<'static>) -> Property<'i> {
+  Property::Custom(CustomProperty {
+    name: CustomPropertyName::Custom(name.into()),
+    value: TokenList(vec![TokenOrValue::Token(value)]),
+  })
+}
diff --git a/src/rules/container.rs b/src/rules/container.rs
new file mode 100644
index 0000000..a3ac87f
--- /dev/null
+++ b/src/rules/container.rs
@@ -0,0 +1,348 @@
+//! The `@container` rule.
+
+use cssparser::*;
+
+use super::Location;
+use super::{CssRuleList, MinifyContext};
+use crate::error::{MinifyError, ParserError, PrinterError};
+use crate::media_query::{
+  define_query_features, operation_to_css, parse_query_condition, to_css_with_parens_if_needed, FeatureToCss,
+  MediaFeatureType, Operator, QueryCondition, QueryConditionFlags, QueryFeature, ValueType,
+};
+use crate::parser::{DefaultAtRule, ParserOptions};
+use crate::printer::Printer;
+use crate::properties::{Property, PropertyId};
+#[cfg(feature = "serde")]
+use crate::serialization::ValueWrapper;
+use crate::targets::{Features, Targets};
+use crate::traits::{Parse, ParseWithOptions, ToCss};
+use crate::values::ident::CustomIdent;
+#[cfg(feature = "visitor")]
+use crate::visitor::Visit;
+
+/// A [@container](https://drafts.csswg.org/css-contain-3/#container-rule) rule.
+#[derive(Debug, PartialEq, Clone)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub struct ContainerRule<'i, R = DefaultAtRule> {
+  /// The name of the container.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  pub name: Option<ContainerName<'i>>,
+  /// The container condition.
+  pub condition: ContainerCondition<'i>,
+  /// The rules within the `@container` rule.
+  pub rules: CssRuleList<'i, R>,
+  /// The location of the rule in the source file.
+  #[cfg_attr(feature = "visitor", skip_visit)]
+  pub loc: Location,
+}
+
+/// Represents a container condition.
+#[derive(Clone, Debug, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub enum ContainerCondition<'i> {
+  /// A size container feature, implicitly parenthesized.
+  #[cfg_attr(feature = "serde", serde(borrow, with = "ValueWrapper::<ContainerSizeFeature>"))]
+  Feature(ContainerSizeFeature<'i>),
+  /// A negation of a condition.
+  #[cfg_attr(feature = "visitor", skip_type)]
+  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<Box<ContainerCondition>>"))]
+  Not(Box<ContainerCondition<'i>>),
+  /// A set of joint operations.
+  #[cfg_attr(feature = "visitor", skip_type)]
+  Operation {
+    /// The operator for the conditions.
+    operator: Operator,
+    /// The conditions for the operator.
+    conditions: Vec<ContainerCondition<'i>>,
+  },
+  /// A style query.
+  #[cfg_attr(feature = "serde", serde(borrow, with = "ValueWrapper::<StyleQuery>"))]
+  Style(StyleQuery<'i>),
+}
+
+/// A container query size feature.
+pub type ContainerSizeFeature<'i> = QueryFeature<'i, ContainerSizeFeatureId>;
+
+define_query_features! {
+  /// A container query size feature identifier.
+  pub enum ContainerSizeFeatureId {
+    /// The [width](https://w3c.github.io/csswg-drafts/css-contain-3/#width) size container feature.
+    "width": Width = Length,
+    /// The [height](https://w3c.github.io/csswg-drafts/css-contain-3/#height) size container feature.
+    "height": Height = Length,
+    /// The [inline-size](https://w3c.github.io/csswg-drafts/css-contain-3/#inline-size) size container feature.
+    "inline-size": InlineSize = Length,
+    /// The [block-size](https://w3c.github.io/csswg-drafts/css-contain-3/#block-size) size container feature.
+    "block-size": BlockSize = Length,
+    /// The [aspect-ratio](https://w3c.github.io/csswg-drafts/css-contain-3/#aspect-ratio) size container feature.
+    "aspect-ratio": AspectRatio = Ratio,
+    /// The [orientation](https://w3c.github.io/csswg-drafts/css-contain-3/#orientation) size container feature.
+    "orientation": Orientation = Ident,
+  }
+}
+
+impl FeatureToCss for ContainerSizeFeatureId {
+  fn to_css_with_prefix<W>(&self, prefix: &str, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    dest.write_str(prefix)?;
+    self.to_css(dest)
+  }
+}
+
+/// Represents a style query within a container condition.
+#[derive(Clone, Debug, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub enum StyleQuery<'i> {
+  /// A property declaration.
+  #[cfg_attr(feature = "serde", serde(borrow, with = "ValueWrapper::<Property>"))]
+  Declaration(Property<'i>),
+  /// A property name, without a value.
+  /// This matches if the property value is different from the initial value.
+  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<PropertyId>"))]
+  Property(PropertyId<'i>),
+  /// A negation of a condition.
+  #[cfg_attr(feature = "visitor", skip_type)]
+  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<Box<StyleQuery>>"))]
+  Not(Box<StyleQuery<'i>>),
+  /// A set of joint operations.
+  #[cfg_attr(feature = "visitor", skip_type)]
+  Operation {
+    /// The operator for the conditions.
+    operator: Operator,
+    /// The conditions for the operator.
+    conditions: Vec<StyleQuery<'i>>,
+  },
+}
+
+impl<'i> QueryCondition<'i> for ContainerCondition<'i> {
+  #[inline]
+  fn parse_feature<'t>(
+    input: &mut Parser<'i, 't>,
+    options: &ParserOptions<'_, 'i>,
+  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let feature = QueryFeature::parse_with_options(input, options)?;
+    Ok(Self::Feature(feature))
+  }
+
+  #[inline]
+  fn create_negation(condition: Box<ContainerCondition<'i>>) -> Self {
+    Self::Not(condition)
+  }
+
+  #[inline]
+  fn create_operation(operator: Operator, conditions: Vec<Self>) -> Self {
+    Self::Operation { operator, conditions }
+  }
+
+  fn parse_style_query<'t>(
+    input: &mut Parser<'i, 't>,
+    options: &ParserOptions<'_, 'i>,
+  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    input.parse_nested_block(|input| {
+      if let Ok(res) =
+        input.try_parse(|input| parse_query_condition(input, QueryConditionFlags::ALLOW_OR, options))
+      {
+        return Ok(Self::Style(res));
+      }
+
+      Ok(Self::Style(StyleQuery::parse_feature(input, options)?))
+    })
+  }
+
+  fn needs_parens(&self, parent_operator: Option<Operator>, targets: &Targets) -> bool {
+    match self {
+      ContainerCondition::Not(_) => true,
+      ContainerCondition::Operation { operator, .. } => Some(*operator) != parent_operator,
+      ContainerCondition::Feature(f) => f.needs_parens(parent_operator, targets),
+      ContainerCondition::Style(_) => false,
+    }
+  }
+}
+
+impl<'i> QueryCondition<'i> for StyleQuery<'i> {
+  #[inline]
+  fn parse_feature<'t>(
+    input: &mut Parser<'i, 't>,
+    options: &ParserOptions<'_, 'i>,
+  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let property_id = PropertyId::parse(input)?;
+    if input.try_parse(|input| input.expect_colon()).is_ok() {
+      input.skip_whitespace();
+      let feature = Self::Declaration(Property::parse(property_id, input, options)?);
+      let _ = input.try_parse(|input| parse_important(input));
+      Ok(feature)
+    } else {
+      Ok(Self::Property(property_id))
+    }
+  }
+
+  #[inline]
+  fn create_negation(condition: Box<Self>) -> Self {
+    Self::Not(condition)
+  }
+
+  #[inline]
+  fn create_operation(operator: Operator, conditions: Vec<Self>) -> Self {
+    Self::Operation { operator, conditions }
+  }
+
+  fn needs_parens(&self, parent_operator: Option<Operator>, _targets: &Targets) -> bool {
+    match self {
+      StyleQuery::Not(_) => true,
+      StyleQuery::Operation { operator, .. } => Some(*operator) != parent_operator,
+      StyleQuery::Declaration(_) | StyleQuery::Property(_) => true,
+    }
+  }
+}
+
+impl<'i> ParseWithOptions<'i> for ContainerCondition<'i> {
+  fn parse_with_options<'t>(
+    input: &mut Parser<'i, 't>,
+    options: &ParserOptions<'_, 'i>,
+  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    parse_query_condition(
+      input,
+      QueryConditionFlags::ALLOW_OR | QueryConditionFlags::ALLOW_STYLE,
+      options,
+    )
+  }
+}
+
+impl<'i> ToCss for ContainerCondition<'i> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    match *self {
+      ContainerCondition::Feature(ref f) => f.to_css(dest),
+      ContainerCondition::Not(ref c) => {
+        dest.write_str("not ")?;
+        to_css_with_parens_if_needed(&**c, dest, c.needs_parens(None, &dest.targets.current))
+      }
+      ContainerCondition::Operation {
+        ref conditions,
+        operator,
+      } => operation_to_css(operator, conditions, dest),
+      ContainerCondition::Style(ref query) => {
+        dest.write_str("style(")?;
+        query.to_css(dest)?;
+        dest.write_char(')')
+      }
+    }
+  }
+}
+
+impl<'i> ToCss for StyleQuery<'i> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    match *self {
+      StyleQuery::Declaration(ref f) => f.to_css(dest, false),
+      StyleQuery::Property(ref f) => f.to_css(dest),
+      StyleQuery::Not(ref c) => {
+        dest.write_str("not ")?;
+        to_css_with_parens_if_needed(&**c, dest, c.needs_parens(None, &dest.targets.current))
+      }
+      StyleQuery::Operation {
+        ref conditions,
+        operator,
+      } => operation_to_css(operator, conditions, dest),
+    }
+  }
+}
+
+/// A [`<container-name>`](https://drafts.csswg.org/css-contain-3/#typedef-container-name) in a `@container` rule.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(transparent))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub struct ContainerName<'i>(#[cfg_attr(feature = "serde", serde(borrow))] pub CustomIdent<'i>);
+
+impl<'i> Parse<'i> for ContainerName<'i> {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let ident = CustomIdent::parse(input)?;
+    match_ignore_ascii_case! { &*ident.0,
+      "none" | "and" | "not" | "or" => Err(input.new_unexpected_token_error(Token::Ident(ident.0.as_ref().to_owned().into()))),
+      _ => Ok(ContainerName(ident))
+    }
+  }
+}
+
+impl<'i> ToCss for ContainerName<'i> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    // Container name should not be hashed
+    // https://github.com/vercel/next.js/issues/71233
+    self.0.to_css_with_options(
+      dest,
+      match &dest.css_module {
+        Some(css_module) => css_module.config.container,
+        None => false,
+      },
+    )
+  }
+}
+
+impl<'i, T: Clone> ContainerRule<'i, T> {
+  pub(crate) fn minify(
+    &mut self,
+    context: &mut MinifyContext<'_, 'i>,
+    parent_is_unused: bool,
+  ) -> Result<bool, MinifyError> {
+    self.rules.minify(context, parent_is_unused)?;
+    Ok(self.rules.0.is_empty())
+  }
+}
+
+impl<'a, 'i, T: ToCss> ToCss for ContainerRule<'i, T> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    #[cfg(feature = "sourcemap")]
+    dest.add_mapping(self.loc);
+    dest.write_str("@container ")?;
+    if let Some(name) = &self.name {
+      name.to_css(dest)?;
+      dest.write_char(' ')?;
+    }
+
+    // Don't downlevel range syntax in container queries.
+    let exclude = dest.targets.current.exclude;
+    dest.targets.current.exclude.insert(Features::MediaQueries);
+    self.condition.to_css(dest)?;
+    dest.targets.current.exclude = exclude;
+
+    dest.whitespace()?;
+    dest.write_char('{')?;
+    dest.indent();
+    dest.newline()?;
+    self.rules.to_css(dest)?;
+    dest.dedent();
+    dest.newline()?;
+    dest.write_char('}')
+  }
+}
diff --git a/src/rules/counter_style.rs b/src/rules/counter_style.rs
new file mode 100644
index 0000000..bb54707
--- /dev/null
+++ b/src/rules/counter_style.rs
@@ -0,0 +1,41 @@
+//! The `@counter-style` rule.
+
+use super::Location;
+use crate::declaration::DeclarationBlock;
+use crate::error::PrinterError;
+use crate::printer::Printer;
+use crate::traits::ToCss;
+use crate::values::ident::CustomIdent;
+#[cfg(feature = "visitor")]
+use crate::visitor::Visit;
+
+/// A [@counter-style](https://drafts.csswg.org/css-counter-styles/#the-counter-style-rule) rule.
+#[derive(Debug, PartialEq, Clone)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub struct CounterStyleRule<'i> {
+  /// The name of the counter style to declare.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  pub name: CustomIdent<'i>,
+  // TODO: eventually parse these properties
+  /// Declarations in the `@counter-style` rule.
+  pub declarations: DeclarationBlock<'i>,
+  /// The location of the rule in the source file.
+  #[cfg_attr(feature = "visitor", skip_visit)]
+  pub loc: Location,
+}
+
+impl<'i> ToCss for CounterStyleRule<'i> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    #[cfg(feature = "sourcemap")]
+    dest.add_mapping(self.loc);
+    dest.write_str("@counter-style ")?;
+    self.name.to_css(dest)?;
+    self.declarations.to_css_block(dest)
+  }
+}
diff --git a/src/rules/custom_media.rs b/src/rules/custom_media.rs
new file mode 100644
index 0000000..1304296
--- /dev/null
+++ b/src/rules/custom_media.rs
@@ -0,0 +1,42 @@
+//! The `@custom-media` rule.
+
+use super::Location;
+use crate::error::PrinterError;
+use crate::media_query::MediaList;
+use crate::printer::Printer;
+use crate::traits::ToCss;
+use crate::values::ident::DashedIdent;
+#[cfg(feature = "visitor")]
+use crate::visitor::Visit;
+
+/// A [@custom-media](https://drafts.csswg.org/mediaqueries-5/#custom-mq) rule.
+#[derive(Debug, PartialEq, Clone)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub struct CustomMediaRule<'i> {
+  /// The name of the declared media query.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  pub name: DashedIdent<'i>,
+  /// The media query to declare.
+  pub query: MediaList<'i>,
+  /// The location of the rule in the source file.
+  #[cfg_attr(feature = "visitor", skip_visit)]
+  pub loc: Location,
+}
+
+impl<'i> ToCss for CustomMediaRule<'i> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    #[cfg(feature = "sourcemap")]
+    dest.add_mapping(self.loc);
+    dest.write_str("@custom-media ")?;
+    self.name.to_css(dest)?;
+    dest.write_char(' ')?;
+    self.query.to_css(dest)?;
+    dest.write_char(';')
+  }
+}
diff --git a/src/rules/document.rs b/src/rules/document.rs
new file mode 100644
index 0000000..e118ec4
--- /dev/null
+++ b/src/rules/document.rs
@@ -0,0 +1,53 @@
+//! The `@-moz-document` rule.
+
+use super::Location;
+use super::{CssRuleList, MinifyContext};
+use crate::error::{MinifyError, PrinterError};
+use crate::parser::DefaultAtRule;
+use crate::printer::Printer;
+use crate::traits::ToCss;
+#[cfg(feature = "visitor")]
+use crate::visitor::Visit;
+
+/// A [@-moz-document](https://www.w3.org/TR/2012/WD-css3-conditional-20120911/#at-document) rule.
+///
+/// Note that only the `url-prefix()` function with no arguments is supported, and only the `-moz` prefix
+/// is allowed since Firefox was the only browser that ever implemented this rule.
+#[derive(Debug, PartialEq, Clone)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub struct MozDocumentRule<'i, R = DefaultAtRule> {
+  /// Nested rules within the `@-moz-document` rule.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  pub rules: CssRuleList<'i, R>,
+  /// The location of the rule in the source file.
+  #[cfg_attr(feature = "visitor", skip_visit)]
+  pub loc: Location,
+}
+
+impl<'i, T: Clone> MozDocumentRule<'i, T> {
+  pub(crate) fn minify(&mut self, context: &mut MinifyContext<'_, 'i>) -> Result<(), MinifyError> {
+    self.rules.minify(context, false)
+  }
+}
+
+impl<'i, T: ToCss> ToCss for MozDocumentRule<'i, T> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    #[cfg(feature = "sourcemap")]
+    dest.add_mapping(self.loc);
+    dest.write_str("@-moz-document url-prefix()")?;
+    dest.whitespace()?;
+    dest.write_char('{')?;
+    dest.indent();
+    dest.newline()?;
+    self.rules.to_css(dest)?;
+    dest.dedent();
+    dest.newline()?;
+    dest.write_char('}')
+  }
+}
diff --git a/src/rules/font_face.rs b/src/rules/font_face.rs
new file mode 100644
index 0000000..2472009
--- /dev/null
+++ b/src/rules/font_face.rs
@@ -0,0 +1,576 @@
+//! The `@font-face` rule.
+
+use super::Location;
+use crate::error::{ParserError, PrinterError};
+use crate::macros::enum_property;
+use crate::printer::Printer;
+use crate::properties::custom::CustomProperty;
+use crate::properties::font::{FontFamily, FontStretch, FontStyle as FontStyleProperty, FontWeight};
+use crate::stylesheet::ParserOptions;
+use crate::traits::{Parse, ToCss};
+use crate::values::angle::Angle;
+use crate::values::size::Size2D;
+use crate::values::string::CowArcStr;
+use crate::values::url::Url;
+#[cfg(feature = "visitor")]
+use crate::visitor::Visit;
+use cssparser::*;
+use std::fmt::Write;
+
+/// A [@font-face](https://drafts.csswg.org/css-fonts/#font-face-rule) rule.
+#[derive(Debug, PartialEq, Clone)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub struct FontFaceRule<'i> {
+  /// Declarations in the `@font-face` rule.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  pub properties: Vec<FontFaceProperty<'i>>,
+  /// The location of the rule in the source file.
+  #[cfg_attr(feature = "visitor", skip_visit)]
+  pub loc: Location,
+}
+
+/// A property within an `@font-face` rule.
+///
+/// See [FontFaceRule](FontFaceRule).
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", content = "value", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub enum FontFaceProperty<'i> {
+  /// The `src` property.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  Source(Vec<Source<'i>>),
+  /// The `font-family` property.
+  FontFamily(FontFamily<'i>),
+  /// The `font-style` property.
+  FontStyle(FontStyle),
+  /// The `font-weight` property.
+  FontWeight(Size2D<FontWeight>),
+  /// The `font-stretch` property.
+  FontStretch(Size2D<FontStretch>),
+  /// The `unicode-range` property.
+  UnicodeRange(Vec<UnicodeRange>),
+  /// An unknown or unsupported property.
+  Custom(CustomProperty<'i>),
+}
+
+/// A value for the [src](https://drafts.csswg.org/css-fonts/#src-desc)
+/// property in an `@font-face` rule.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", content = "value", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub enum Source<'i> {
+  /// A `url()` with optional format metadata.
+  Url(UrlSource<'i>),
+  /// The `local()` function.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  Local(FontFamily<'i>),
+}
+
+impl<'i> Parse<'i> for Source<'i> {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    match input.try_parse(UrlSource::parse) {
+      Ok(url) => return Ok(Source::Url(url)),
+      e @ Err(ParseError {
+        kind: ParseErrorKind::Basic(BasicParseErrorKind::AtRuleBodyInvalid),
+        ..
+      }) => {
+        return Err(e.err().unwrap());
+      }
+      _ => {}
+    }
+
+    input.expect_function_matching("local")?;
+    let local = input.parse_nested_block(FontFamily::parse)?;
+    Ok(Source::Local(local))
+  }
+}
+
+impl<'i> ToCss for Source<'i> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    match self {
+      Source::Url(url) => url.to_css(dest),
+      Source::Local(local) => {
+        dest.write_str("local(")?;
+        local.to_css(dest)?;
+        dest.write_char(')')
+      }
+    }
+  }
+}
+
+/// A `url()` value for the [src](https://drafts.csswg.org/css-fonts/#src-desc)
+/// property in an `@font-face` rule.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub struct UrlSource<'i> {
+  /// The URL.
+  pub url: Url<'i>,
+  /// Optional `format()` function.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  pub format: Option<FontFormat<'i>>,
+  /// Optional `tech()` function.
+  pub tech: Vec<FontTechnology>,
+}
+
+impl<'i> Parse<'i> for UrlSource<'i> {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let url = Url::parse(input)?;
+
+    let format = if input.try_parse(|input| input.expect_function_matching("format")).is_ok() {
+      Some(input.parse_nested_block(FontFormat::parse)?)
+    } else {
+      None
+    };
+
+    let tech = if input.try_parse(|input| input.expect_function_matching("tech")).is_ok() {
+      input.parse_nested_block(Vec::<FontTechnology>::parse)?
+    } else {
+      vec![]
+    };
+
+    Ok(UrlSource { url, format, tech })
+  }
+}
+
+impl<'i> ToCss for UrlSource<'i> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    self.url.to_css(dest)?;
+    if let Some(format) = &self.format {
+      dest.whitespace()?;
+      dest.write_str("format(")?;
+      format.to_css(dest)?;
+      dest.write_char(')')?;
+    }
+
+    if !self.tech.is_empty() {
+      dest.whitespace()?;
+      dest.write_str("tech(")?;
+      self.tech.to_css(dest)?;
+      dest.write_char(')')?;
+    }
+    Ok(())
+  }
+}
+
+/// A font format keyword in the `format()` function of the the
+/// [src](https://drafts.csswg.org/css-fonts/#src-desc)
+/// property of an `@font-face` rule.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", content = "value", rename_all = "lowercase")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub enum FontFormat<'i> {
+  /// [src](https://drafts.csswg.org/css-fonts/#font-format-definitions)
+  /// A WOFF 1.0 font.
+  WOFF,
+  /// A WOFF 2.0 font.
+  WOFF2,
+  /// A TrueType font.
+  TrueType,
+  /// An OpenType font.
+  OpenType,
+  /// An Embedded OpenType (.eot) font.
+  #[cfg_attr(feature = "serde", serde(rename = "embedded-opentype"))]
+  EmbeddedOpenType,
+  /// OpenType Collection.
+  Collection,
+  /// An SVG font.
+  SVG,
+  /// An unknown format.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  String(CowArcStr<'i>),
+}
+
+impl<'i> Parse<'i> for FontFormat<'i> {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let s = input.expect_ident_or_string()?;
+    match_ignore_ascii_case! { &s,
+      "woff" => Ok(FontFormat::WOFF),
+      "woff2" => Ok(FontFormat::WOFF2),
+      "truetype" => Ok(FontFormat::TrueType),
+      "opentype" => Ok(FontFormat::OpenType),
+      "embedded-opentype" => Ok(FontFormat::EmbeddedOpenType),
+      "collection" => Ok(FontFormat::Collection),
+      "svg" => Ok(FontFormat::SVG),
+      _ => Ok(FontFormat::String(s.into()))
+    }
+  }
+}
+
+impl<'i> ToCss for FontFormat<'i> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    use FontFormat::*;
+    let s = match self {
+      WOFF => "woff",
+      WOFF2 => "woff2",
+      TrueType => "truetype",
+      OpenType => "opentype",
+      EmbeddedOpenType => "embedded-opentype",
+      Collection => "collection",
+      SVG => "svg",
+      String(s) => &s,
+    };
+    // Browser support for keywords rather than strings is very limited.
+    // https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/src
+    serialize_string(&s, dest)?;
+    Ok(())
+  }
+}
+
+enum_property! {
+  /// A font format keyword in the `format()` function of the the
+  /// [src](https://drafts.csswg.org/css-fonts/#src-desc)
+  /// property of an `@font-face` rule.
+  pub enum FontTechnology {
+    /// A font features tech descriptor in the `tech()`function of the
+    /// [src](https://drafts.csswg.org/css-fonts/#font-features-tech-values)
+    /// property of an `@font-face` rule.
+    /// Supports OpenType Features.
+    /// https://docs.microsoft.com/en-us/typography/opentype/spec/featurelist
+    "features-opentype": FeaturesOpentype,
+    /// Supports Apple Advanced Typography Font Features.
+    /// https://developer.apple.com/fonts/TrueType-Reference-Manual/RM09/AppendixF.html
+    "features-aat": FeaturesAat,
+    /// Supports Graphite Table Format.
+    /// https://scripts.sil.org/cms/scripts/render_download.php?site_id=nrsi&format=file&media_id=GraphiteBinaryFormat_3_0&filename=GraphiteBinaryFormat_3_0.pdf
+    "features-graphite": FeaturesGraphite,
+
+    /// A color font tech descriptor in the `tech()`function of the
+    /// [src](https://drafts.csswg.org/css-fonts/#src-desc)
+    /// property of an `@font-face` rule.
+    /// Supports the `COLR` v0 table.
+    "color-colrv0": ColorCOLRv0,
+    /// Supports the `COLR` v1 table.
+    "color-colrv1": ColorCOLRv1,
+    /// Supports the `SVG` table.
+    "color-svg": ColorSVG,
+    /// Supports the `sbix` table.
+    "color-sbix": ColorSbix,
+    /// Supports the `CBDT` table.
+    "color-cbdt": ColorCBDT,
+
+    /// Supports Variations
+    /// The variations tech refers to the support of font variations
+    "variations": Variations,
+    /// Supports Palettes
+    /// The palettes tech refers to support for font palettes
+    "palettes": Palettes,
+    /// Supports Incremental
+    /// The incremental tech refers to client support for incremental font loading, using either the range-request or the patch-subset method
+    "incremental": Incremental,
+  }
+}
+
+/// A contiguous range of Unicode code points.
+///
+/// Cannot be empty. Can represent a single code point when start == end.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub struct UnicodeRange {
+  /// Inclusive start of the range. In [0, end].
+  pub start: u32,
+  /// Inclusive end of the range. In [0, 0x10FFFF].
+  pub end: u32,
+}
+
+impl<'i> Parse<'i> for UnicodeRange {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let range = cssparser::UnicodeRange::parse(input)?;
+    Ok(UnicodeRange {
+      start: range.start,
+      end: range.end,
+    })
+  }
+}
+
+impl ToCss for UnicodeRange {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    // Attempt to optimize the range to use question mark syntax.
+    if self.start != self.end {
+      // Find the first hex digit that differs between the start and end values.
+      let mut shift = 24;
+      let mut mask = 0xf << shift;
+      while shift > 0 {
+        let c1 = self.start & mask;
+        let c2 = self.end & mask;
+        if c1 != c2 {
+          break;
+        }
+
+        mask = mask >> 4;
+        shift -= 4;
+      }
+
+      // Get the remainder of the value. This must be 0x0 to 0xf for the rest
+      // of the value to use the question mark syntax.
+      shift += 4;
+      let remainder_mask = (1 << shift) - 1;
+      let start_remainder = self.start & remainder_mask;
+      let end_remainder = self.end & remainder_mask;
+
+      if start_remainder == 0 && end_remainder == remainder_mask {
+        let start = (self.start & !remainder_mask) >> shift;
+        if start != 0 {
+          write!(dest, "U+{:X}", start)?;
+        } else {
+          dest.write_str("U+")?;
+        }
+
+        while shift > 0 {
+          dest.write_char('?')?;
+          shift -= 4;
+        }
+
+        return Ok(());
+      }
+    }
+
+    write!(dest, "U+{:X}", self.start)?;
+    if self.end != self.start {
+      write!(dest, "-{:X}", self.end)?;
+    }
+    Ok(())
+  }
+}
+
+/// A value for the [font-style](https://w3c.github.io/csswg-drafts/css-fonts/#descdef-font-face-font-style) descriptor in an `@font-face` rule.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", content = "value", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum FontStyle {
+  /// Normal font style.
+  Normal,
+  /// Italic font style.
+  Italic,
+  /// Oblique font style, with a custom angle.
+  Oblique(#[cfg_attr(feature = "serde", serde(default = "FontStyle::default_oblique_angle"))] Size2D<Angle>),
+}
+
+impl Default for FontStyle {
+  fn default() -> FontStyle {
+    FontStyle::Normal
+  }
+}
+
+impl FontStyle {
+  #[inline]
+  fn default_oblique_angle() -> Size2D<Angle> {
+    Size2D(
+      FontStyleProperty::default_oblique_angle(),
+      FontStyleProperty::default_oblique_angle(),
+    )
+  }
+}
+
+impl<'i> Parse<'i> for FontStyle {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    Ok(match FontStyleProperty::parse(input)? {
+      FontStyleProperty::Normal => FontStyle::Normal,
+      FontStyleProperty::Italic => FontStyle::Italic,
+      FontStyleProperty::Oblique(angle) => {
+        let second_angle = input.try_parse(Angle::parse).unwrap_or_else(|_| angle.clone());
+        FontStyle::Oblique(Size2D(angle, second_angle))
+      }
+    })
+  }
+}
+
+impl ToCss for FontStyle {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    match self {
+      FontStyle::Normal => dest.write_str("normal"),
+      FontStyle::Italic => dest.write_str("italic"),
+      FontStyle::Oblique(angle) => {
+        dest.write_str("oblique")?;
+        if *angle != FontStyle::default_oblique_angle() {
+          dest.write_char(' ')?;
+          angle.to_css(dest)?;
+        }
+        Ok(())
+      }
+    }
+  }
+}
+
+pub(crate) struct FontFaceDeclarationParser;
+
+/// Parse a declaration within {} block: `color: blue`
+impl<'i> cssparser::DeclarationParser<'i> for FontFaceDeclarationParser {
+  type Declaration = FontFaceProperty<'i>;
+  type Error = ParserError<'i>;
+
+  fn parse_value<'t>(
+    &mut self,
+    name: CowRcStr<'i>,
+    input: &mut cssparser::Parser<'i, 't>,
+  ) -> Result<Self::Declaration, cssparser::ParseError<'i, Self::Error>> {
+    macro_rules! property {
+      ($property: ident, $type: ty) => {
+        if let Ok(c) = <$type>::parse(input) {
+          if input.expect_exhausted().is_ok() {
+            return Ok(FontFaceProperty::$property(c));
+          }
+        }
+      };
+    }
+
+    let state = input.state();
+    match_ignore_ascii_case! { &name,
+      "src" => {
+        if let Ok(sources) = input.parse_comma_separated(Source::parse) {
+          return Ok(FontFaceProperty::Source(sources))
+        }
+      },
+      "font-family" => property!(FontFamily, FontFamily),
+      "font-weight" => property!(FontWeight, Size2D<FontWeight>),
+      "font-style" => property!(FontStyle, FontStyle),
+      "font-stretch" => property!(FontStretch, Size2D<FontStretch>),
+      "unicode-range" => property!(UnicodeRange, Vec<UnicodeRange>),
+      _ => {}
+    }
+
+    input.reset(&state);
+    return Ok(FontFaceProperty::Custom(CustomProperty::parse(
+      name.into(),
+      input,
+      &ParserOptions::default(),
+    )?));
+  }
+}
+
+/// Default methods reject all at rules.
+impl<'i> AtRuleParser<'i> for FontFaceDeclarationParser {
+  type Prelude = ();
+  type AtRule = FontFaceProperty<'i>;
+  type Error = ParserError<'i>;
+}
+
+impl<'i> QualifiedRuleParser<'i> for FontFaceDeclarationParser {
+  type Prelude = ();
+  type QualifiedRule = FontFaceProperty<'i>;
+  type Error = ParserError<'i>;
+}
+
+impl<'i> RuleBodyItemParser<'i, FontFaceProperty<'i>, ParserError<'i>> for FontFaceDeclarationParser {
+  fn parse_qualified(&self) -> bool {
+    false
+  }
+
+  fn parse_declarations(&self) -> bool {
+    true
+  }
+}
+
+impl<'i> ToCss for FontFaceRule<'i> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    #[cfg(feature = "sourcemap")]
+    dest.add_mapping(self.loc);
+    dest.write_str("@font-face")?;
+    dest.whitespace()?;
+    dest.write_char('{')?;
+    dest.indent();
+    let len = self.properties.len();
+    for (i, prop) in self.properties.iter().enumerate() {
+      dest.newline()?;
+      prop.to_css(dest)?;
+      if i != len - 1 || !dest.minify {
+        dest.write_char(';')?;
+      }
+    }
+    dest.dedent();
+    dest.newline()?;
+    dest.write_char('}')
+  }
+}
+
+impl<'i> ToCss for FontFaceProperty<'i> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    use FontFaceProperty::*;
+    macro_rules! property {
+      ($prop: literal, $value: expr) => {{
+        dest.write_str($prop)?;
+        dest.delim(':', false)?;
+        $value.to_css(dest)
+      }};
+      ($prop: literal, $value: expr, $multi: expr) => {{
+        dest.write_str($prop)?;
+        dest.delim(':', false)?;
+        let len = $value.len();
+        for (idx, val) in $value.iter().enumerate() {
+          val.to_css(dest)?;
+          if idx < len - 1 {
+            dest.delim(',', false)?;
+          }
+        }
+        Ok(())
+      }};
+    }
+
+    match self {
+      Source(value) => property!("src", value, true),
+      FontFamily(value) => property!("font-family", value),
+      FontStyle(value) => property!("font-style", value),
+      FontWeight(value) => property!("font-weight", value),
+      FontStretch(value) => property!("font-stretch", value),
+      UnicodeRange(value) => property!("unicode-range", value),
+      Custom(custom) => {
+        dest.write_str(custom.name.as_ref())?;
+        dest.delim(':', false)?;
+        custom.value.to_css(dest, true)
+      }
+    }
+  }
+}
diff --git a/src/rules/font_feature_values.rs b/src/rules/font_feature_values.rs
new file mode 100644
index 0000000..9aeedbf
--- /dev/null
+++ b/src/rules/font_feature_values.rs
@@ -0,0 +1,331 @@
+//! The `@font-feature-values` rule.
+
+use super::Location;
+use crate::error::{ParserError, PrinterError};
+use crate::parser::ParserOptions;
+use crate::printer::Printer;
+use crate::properties::font::FamilyName;
+use crate::traits::{Parse, ToCss};
+use crate::values::ident::Ident;
+use crate::values::number::CSSInteger;
+#[cfg(feature = "visitor")]
+use crate::visitor::Visit;
+use cssparser::*;
+use indexmap::IndexMap;
+use smallvec::SmallVec;
+use std::fmt::Write;
+
+/// A [@font-feature-values](https://drafts.csswg.org/css-fonts/#font-feature-values) rule.
+#[derive(Debug, PartialEq, Clone)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub struct FontFeatureValuesRule<'i> {
+  /// The name of the font feature values.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  pub name: Vec<FamilyName<'i>>,
+  /// The rules within the `@font-feature-values` rule.
+  pub rules: IndexMap<FontFeatureSubruleType, FontFeatureSubrule<'i>>,
+  /// The location of the rule in the source file.
+  #[cfg_attr(feature = "visitor", skip_visit)]
+  pub loc: Location,
+}
+
+impl<'i> FontFeatureValuesRule<'i> {
+  pub(crate) fn parse<'t, 'o>(
+    family_names: Vec<FamilyName<'i>>,
+    input: &mut Parser<'i, 't>,
+    loc: Location,
+    options: &ParserOptions<'o, 'i>,
+  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let mut rules = IndexMap::new();
+    let mut rule_parser = FontFeatureValuesRuleParser {
+      rules: &mut rules,
+      options,
+    };
+    let mut parser = RuleBodyParser::new(input, &mut rule_parser);
+
+    while let Some(decl_or_rule) = parser.next() {
+      if let Err((err, _)) = decl_or_rule {
+        if parser.parser.options.error_recovery {
+          parser.parser.options.warn(err);
+          continue;
+        }
+        return Err(err);
+      }
+    }
+
+    Ok(FontFeatureValuesRule {
+      name: family_names,
+      rules,
+      loc,
+    })
+  }
+}
+
+struct FontFeatureValuesRuleParser<'a, 'o, 'i> {
+  rules: &'a mut IndexMap<FontFeatureSubruleType, FontFeatureSubrule<'i>>,
+  options: &'a ParserOptions<'o, 'i>,
+}
+
+impl<'a, 'o, 'i> cssparser::DeclarationParser<'i> for FontFeatureValuesRuleParser<'a, 'o, 'i> {
+  type Declaration = ();
+  type Error = ParserError<'i>;
+}
+
+impl<'a, 'o, 'i> cssparser::AtRuleParser<'i> for FontFeatureValuesRuleParser<'a, 'o, 'i> {
+  type Prelude = FontFeatureSubruleType;
+  type AtRule = ();
+  type Error = ParserError<'i>;
+
+  fn parse_prelude<'t>(
+    &mut self,
+    name: CowRcStr<'i>,
+    input: &mut Parser<'i, 't>,
+  ) -> Result<Self::Prelude, ParseError<'i, Self::Error>> {
+    let loc = input.current_source_location();
+    FontFeatureSubruleType::parse_string(&name)
+      .map_err(|_| loc.new_custom_error(ParserError::AtRuleInvalid(name.clone().into())))
+  }
+
+  fn parse_block<'t>(
+    &mut self,
+    prelude: Self::Prelude,
+    start: &ParserState,
+    input: &mut Parser<'i, 't>,
+  ) -> Result<Self::AtRule, ParseError<'i, Self::Error>> {
+    let loc = start.source_location();
+    let mut decls = IndexMap::new();
+    let mut has_existing = false;
+    let declarations = if let Some(rule) = self.rules.get_mut(&prelude) {
+      has_existing = true;
+      &mut rule.declarations
+    } else {
+      &mut decls
+    };
+    let mut decl_parser = FontFeatureDeclarationParser { declarations };
+    let mut parser = RuleBodyParser::new(input, &mut decl_parser);
+    while let Some(decl) = parser.next() {
+      if let Err((err, _)) = decl {
+        if self.options.error_recovery {
+          self.options.warn(err);
+          continue;
+        }
+        return Err(err);
+      }
+    }
+
+    if !has_existing {
+      self.rules.insert(
+        prelude,
+        FontFeatureSubrule {
+          name: prelude,
+          declarations: decls,
+          loc: Location {
+            source_index: self.options.source_index,
+            line: loc.line,
+            column: loc.column,
+          },
+        },
+      );
+    }
+
+    Ok(())
+  }
+}
+
+impl<'a, 'o, 'i> QualifiedRuleParser<'i> for FontFeatureValuesRuleParser<'a, 'o, 'i> {
+  type Prelude = ();
+  type QualifiedRule = ();
+  type Error = ParserError<'i>;
+}
+
+impl<'a, 'o, 'i> RuleBodyItemParser<'i, (), ParserError<'i>> for FontFeatureValuesRuleParser<'a, 'o, 'i> {
+  fn parse_declarations(&self) -> bool {
+    false
+  }
+
+  fn parse_qualified(&self) -> bool {
+    false
+  }
+}
+
+impl<'i> ToCss for FontFeatureValuesRule<'i> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    #[cfg(feature = "sourcemap")]
+    dest.add_mapping(self.loc);
+    dest.write_str("@font-feature-values ")?;
+    self.name.to_css(dest)?;
+    dest.whitespace()?;
+    dest.write_char('{')?;
+    if !self.rules.is_empty() {
+      dest.newline()?;
+      for rule in self.rules.values() {
+        rule.to_css(dest)?;
+        dest.newline()?;
+      }
+    }
+    dest.write_char('}')
+  }
+}
+
+impl<'i> FontFeatureValuesRule<'i> {
+  pub(crate) fn merge(&mut self, other: &FontFeatureValuesRule<'i>) {
+    debug_assert_eq!(self.name, other.name);
+    for (prelude, rule) in &other.rules {
+      if let Some(existing) = self.rules.get_mut(prelude) {
+        existing
+          .declarations
+          .extend(rule.declarations.iter().map(|(k, v)| (k.clone(), v.clone())));
+      } else {
+        self.rules.insert(*prelude, rule.clone());
+      }
+    }
+  }
+}
+
+/// The name of the `@font-feature-values` sub-rule.
+/// font-feature-value-type = <@stylistic> | <@historical-forms> | <@styleset> | <@character-variant>
+///   | <@swash> | <@ornaments> | <@annotation>
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Parse, ToCss)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum FontFeatureSubruleType {
+  /// @stylistic = @stylistic { <declaration-list> }
+  Stylistic,
+  /// @historical-forms = @historical-forms { <declaration-list> }
+  HistoricalForms,
+  /// @styleset = @styleset { <declaration-list> }
+  Styleset,
+  /// @character-variant = @character-variant { <declaration-list> }
+  CharacterVariant,
+  /// @swash = @swash { <declaration-list> }
+  Swash,
+  /// @ornaments = @ornaments { <declaration-list> }
+  Ornaments,
+  /// @annotation = @annotation { <declaration-list> }
+  Annotation,
+}
+
+/// A sub-rule of `@font-feature-values`
+/// https://drafts.csswg.org/css-fonts/#font-feature-values-syntax
+#[derive(Debug, PartialEq, Clone)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(rename_all = "camelCase")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub struct FontFeatureSubrule<'i> {
+  /// The name of the `@font-feature-values` sub-rule.
+  pub name: FontFeatureSubruleType,
+  /// The declarations within the `@font-feature-values` sub-rules.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  pub declarations: IndexMap<Ident<'i>, SmallVec<[CSSInteger; 1]>>,
+  /// The location of the rule in the source file.
+  #[cfg_attr(feature = "visitor", skip_visit)]
+  pub loc: Location,
+}
+
+impl<'i> ToCss for FontFeatureSubrule<'i> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: Write,
+  {
+    #[cfg(feature = "sourcemap")]
+    dest.add_mapping(self.loc);
+    dest.write_char('@')?;
+    self.name.to_css(dest)?;
+    dest.write_char('{')?;
+    dest.indent();
+    let len = self.declarations.len();
+    for (i, (name, value)) in self.declarations.iter().enumerate() {
+      dest.newline()?;
+      name.to_css(dest)?;
+      dest.delim(':', false)?;
+
+      let mut first = true;
+      for index in value {
+        if first {
+          first = false;
+        } else {
+          dest.write_char(' ')?;
+        }
+        index.to_css(dest)?;
+      }
+
+      if i != len - 1 || !dest.minify {
+        dest.write_char(';')?;
+      }
+    }
+    dest.dedent();
+    dest.newline()?;
+    dest.write_char('}')
+  }
+}
+
+struct FontFeatureDeclarationParser<'a, 'i> {
+  declarations: &'a mut IndexMap<Ident<'i>, SmallVec<[CSSInteger; 1]>>,
+}
+
+impl<'a, 'i> cssparser::DeclarationParser<'i> for FontFeatureDeclarationParser<'a, 'i> {
+  type Declaration = ();
+  type Error = ParserError<'i>;
+
+  fn parse_value<'t>(
+    &mut self,
+    name: CowRcStr<'i>,
+    input: &mut cssparser::Parser<'i, 't>,
+  ) -> Result<Self::Declaration, cssparser::ParseError<'i, Self::Error>> {
+    let mut indices = SmallVec::new();
+    loop {
+      if let Ok(value) = CSSInteger::parse(input) {
+        indices.push(value);
+      } else {
+        break;
+      }
+    }
+
+    if indices.is_empty() {
+      return Err(input.new_custom_error(ParserError::InvalidValue));
+    }
+
+    self.declarations.insert(Ident(name.into()), indices);
+    Ok(())
+  }
+}
+
+/// Default methods reject all at rules.
+impl<'a, 'i> AtRuleParser<'i> for FontFeatureDeclarationParser<'a, 'i> {
+  type Prelude = ();
+  type AtRule = ();
+  type Error = ParserError<'i>;
+}
+
+impl<'a, 'i> QualifiedRuleParser<'i> for FontFeatureDeclarationParser<'a, 'i> {
+  type Prelude = ();
+  type QualifiedRule = ();
+  type Error = ParserError<'i>;
+}
+
+impl<'a, 'i> RuleBodyItemParser<'i, (), ParserError<'i>> for FontFeatureDeclarationParser<'a, 'i> {
+  fn parse_qualified(&self) -> bool {
+    false
+  }
+
+  fn parse_declarations(&self) -> bool {
+    true
+  }
+}
diff --git a/src/rules/font_palette_values.rs b/src/rules/font_palette_values.rs
new file mode 100644
index 0000000..af06c48
--- /dev/null
+++ b/src/rules/font_palette_values.rs
@@ -0,0 +1,409 @@
+//! The `@font-palette-values` rule.
+
+use super::supports::SupportsRule;
+use super::{CssRule, CssRuleList, Location, MinifyContext};
+use crate::error::{ParserError, PrinterError};
+use crate::printer::Printer;
+use crate::properties::custom::CustomProperty;
+use crate::properties::font::FontFamily;
+use crate::stylesheet::ParserOptions;
+use crate::targets::Targets;
+use crate::traits::{Parse, ToCss};
+use crate::values::color::{ColorFallbackKind, CssColor};
+use crate::values::ident::DashedIdent;
+use crate::values::number::CSSInteger;
+#[cfg(feature = "visitor")]
+use crate::visitor::Visit;
+use cssparser::*;
+
+/// A [@font-palette-values](https://drafts.csswg.org/css-fonts-4/#font-palette-values) rule.
+#[derive(Debug, PartialEq, Clone)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub struct FontPaletteValuesRule<'i> {
+  /// The name of the font palette.
+  pub name: DashedIdent<'i>,
+  /// Declarations in the `@font-palette-values` rule.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  pub properties: Vec<FontPaletteValuesProperty<'i>>,
+  /// The location of the rule in the source file.
+  #[cfg_attr(feature = "visitor", skip_visit)]
+  pub loc: Location,
+}
+
+/// A property within an `@font-palette-values` rule.
+///
+///  See [FontPaletteValuesRule](FontPaletteValuesRule).
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", content = "value", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub enum FontPaletteValuesProperty<'i> {
+  /// The `font-family` property.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  FontFamily(FontFamily<'i>),
+  /// The `base-palette` property.
+  BasePalette(BasePalette),
+  /// The `override-colors` property.
+  OverrideColors(Vec<OverrideColors>),
+  /// An unknown or unsupported property.
+  Custom(CustomProperty<'i>),
+}
+
+/// A value for the [base-palette](https://drafts.csswg.org/css-fonts-4/#base-palette-desc)
+/// property in an `@font-palette-values` rule.
+#[derive(Debug, PartialEq, Clone)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", content = "value", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum BasePalette {
+  /// A light color palette as defined within the font.
+  Light,
+  /// A dark color palette as defined within the font.
+  Dark,
+  /// A palette index within the font.
+  Integer(u16),
+}
+
+/// A value for the [override-colors](https://drafts.csswg.org/css-fonts-4/#override-color)
+/// property in an `@font-palette-values` rule.
+#[derive(Debug, PartialEq, Clone)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub struct OverrideColors {
+  /// The index of the color within the palette to override.
+  index: u16,
+  /// The replacement color.
+  color: CssColor,
+}
+
+pub(crate) struct FontPaletteValuesDeclarationParser;
+
+impl<'i> cssparser::DeclarationParser<'i> for FontPaletteValuesDeclarationParser {
+  type Declaration = FontPaletteValuesProperty<'i>;
+  type Error = ParserError<'i>;
+
+  fn parse_value<'t>(
+    &mut self,
+    name: CowRcStr<'i>,
+    input: &mut cssparser::Parser<'i, 't>,
+  ) -> Result<Self::Declaration, cssparser::ParseError<'i, Self::Error>> {
+    let state = input.state();
+    match_ignore_ascii_case! { &name,
+      "font-family" => {
+        // https://drafts.csswg.org/css-fonts-4/#font-family-2-desc
+        if let Ok(font_family) = FontFamily::parse(input) {
+          return match font_family {
+            FontFamily::Generic(_) => Err(input.new_custom_error(ParserError::InvalidDeclaration)),
+            _ => Ok(FontPaletteValuesProperty::FontFamily(font_family))
+          }
+        }
+      },
+      "base-palette" => {
+        // https://drafts.csswg.org/css-fonts-4/#base-palette-desc
+        if let Ok(base_palette) = BasePalette::parse(input) {
+          return Ok(FontPaletteValuesProperty::BasePalette(base_palette))
+        }
+      },
+      "override-colors" => {
+        // https://drafts.csswg.org/css-fonts-4/#override-color
+        if let Ok(override_colors) = input.parse_comma_separated(OverrideColors::parse) {
+          return Ok(FontPaletteValuesProperty::OverrideColors(override_colors))
+        }
+      },
+      _ => return Err(input.new_custom_error(ParserError::InvalidDeclaration))
+    }
+
+    input.reset(&state);
+    return Ok(FontPaletteValuesProperty::Custom(CustomProperty::parse(
+      name.into(),
+      input,
+      &ParserOptions::default(),
+    )?));
+  }
+}
+
+/// Default methods reject all at rules.
+impl<'i> AtRuleParser<'i> for FontPaletteValuesDeclarationParser {
+  type Prelude = ();
+  type AtRule = FontPaletteValuesProperty<'i>;
+  type Error = ParserError<'i>;
+}
+
+impl<'i> QualifiedRuleParser<'i> for FontPaletteValuesDeclarationParser {
+  type Prelude = ();
+  type QualifiedRule = FontPaletteValuesProperty<'i>;
+  type Error = ParserError<'i>;
+}
+
+impl<'i> RuleBodyItemParser<'i, FontPaletteValuesProperty<'i>, ParserError<'i>>
+  for FontPaletteValuesDeclarationParser
+{
+  fn parse_qualified(&self) -> bool {
+    false
+  }
+
+  fn parse_declarations(&self) -> bool {
+    true
+  }
+}
+
+impl<'i> FontPaletteValuesRule<'i> {
+  pub(crate) fn parse<'t>(
+    name: DashedIdent<'i>,
+    input: &mut Parser<'i, 't>,
+    loc: Location,
+  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let mut decl_parser = FontPaletteValuesDeclarationParser;
+    let mut parser = RuleBodyParser::new(input, &mut decl_parser);
+    let mut properties = vec![];
+    while let Some(decl) = parser.next() {
+      if let Ok(decl) = decl {
+        properties.push(decl);
+      }
+    }
+
+    Ok(FontPaletteValuesRule { name, properties, loc })
+  }
+}
+
+impl<'i> Parse<'i> for BasePalette {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    if let Ok(i) = input.try_parse(CSSInteger::parse) {
+      if i.is_negative() {
+        return Err(input.new_custom_error(ParserError::InvalidValue));
+      }
+      return Ok(BasePalette::Integer(i as u16));
+    }
+
+    let location = input.current_source_location();
+    let ident = input.expect_ident()?;
+    match_ignore_ascii_case! { &*ident,
+      "light" => Ok(BasePalette::Light),
+      "dark" => Ok(BasePalette::Dark),
+      _ => Err(location.new_unexpected_token_error(Token::Ident(ident.clone())))
+    }
+  }
+}
+
+impl ToCss for BasePalette {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    match self {
+      BasePalette::Light => dest.write_str("light"),
+      BasePalette::Dark => dest.write_str("dark"),
+      BasePalette::Integer(i) => (*i as CSSInteger).to_css(dest),
+    }
+  }
+}
+
+impl<'i> Parse<'i> for OverrideColors {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let index = CSSInteger::parse(input)?;
+    if index.is_negative() {
+      return Err(input.new_custom_error(ParserError::InvalidValue));
+    }
+
+    let color = CssColor::parse(input)?;
+    if matches!(color, CssColor::CurrentColor) {
+      return Err(input.new_custom_error(ParserError::InvalidValue));
+    }
+
+    Ok(OverrideColors {
+      index: index as u16,
+      color,
+    })
+  }
+}
+
+impl ToCss for OverrideColors {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    (self.index as CSSInteger).to_css(dest)?;
+    dest.write_char(' ')?;
+    self.color.to_css(dest)
+  }
+}
+
+impl OverrideColors {
+  fn get_fallback(&self, kind: ColorFallbackKind) -> OverrideColors {
+    OverrideColors {
+      index: self.index,
+      color: self.color.get_fallback(kind),
+    }
+  }
+}
+
+impl<'i> FontPaletteValuesRule<'i> {
+  pub(crate) fn minify(&mut self, context: &mut MinifyContext<'_, 'i>, _: bool) {
+    let mut properties = Vec::with_capacity(self.properties.len());
+    for property in &self.properties {
+      match property {
+        FontPaletteValuesProperty::OverrideColors(override_colors) => {
+          // Generate color fallbacks.
+          let mut fallbacks = ColorFallbackKind::empty();
+          for o in override_colors {
+            fallbacks |= o.color.get_necessary_fallbacks(context.targets.current);
+          }
+
+          if fallbacks.contains(ColorFallbackKind::RGB) {
+            properties.push(FontPaletteValuesProperty::OverrideColors(
+              override_colors.iter().map(|o| o.get_fallback(ColorFallbackKind::RGB)).collect(),
+            ));
+          }
+
+          if fallbacks.contains(ColorFallbackKind::P3) {
+            properties.push(FontPaletteValuesProperty::OverrideColors(
+              override_colors.iter().map(|o| o.get_fallback(ColorFallbackKind::P3)).collect(),
+            ));
+          }
+
+          let override_colors = if fallbacks.contains(ColorFallbackKind::LAB) {
+            override_colors.iter().map(|o| o.get_fallback(ColorFallbackKind::P3)).collect()
+          } else {
+            override_colors.clone()
+          };
+
+          properties.push(FontPaletteValuesProperty::OverrideColors(override_colors));
+        }
+        _ => properties.push(property.clone()),
+      }
+    }
+
+    self.properties = properties;
+  }
+
+  pub(crate) fn get_fallbacks<T>(&mut self, targets: Targets) -> Vec<CssRule<'i, T>> {
+    // Get fallbacks for unparsed properties. These will generate @supports rules
+    // containing duplicate @font-palette-values rules.
+    let mut fallbacks = ColorFallbackKind::empty();
+    for property in &self.properties {
+      match property {
+        FontPaletteValuesProperty::Custom(CustomProperty { value, .. }) => {
+          fallbacks |= value.get_necessary_fallbacks(targets);
+        }
+        _ => {}
+      }
+    }
+
+    let mut res = Vec::new();
+    let lowest_fallback = fallbacks.lowest();
+    fallbacks.remove(lowest_fallback);
+
+    if fallbacks.contains(ColorFallbackKind::P3) {
+      res.push(self.get_fallback(ColorFallbackKind::P3));
+    }
+
+    if fallbacks.contains(ColorFallbackKind::LAB)
+      || (!lowest_fallback.is_empty() && lowest_fallback != ColorFallbackKind::LAB)
+    {
+      res.push(self.get_fallback(ColorFallbackKind::LAB));
+    }
+
+    if !lowest_fallback.is_empty() {
+      for property in &mut self.properties {
+        match property {
+          FontPaletteValuesProperty::Custom(CustomProperty { value, .. }) => {
+            *value = value.get_fallback(lowest_fallback);
+          }
+          _ => {}
+        }
+      }
+    }
+
+    res
+  }
+
+  fn get_fallback<T>(&self, kind: ColorFallbackKind) -> CssRule<'i, T> {
+    let properties = self
+      .properties
+      .iter()
+      .map(|property| match property {
+        FontPaletteValuesProperty::Custom(custom) => FontPaletteValuesProperty::Custom(CustomProperty {
+          name: custom.name.clone(),
+          value: custom.value.get_fallback(kind),
+        }),
+        _ => property.clone(),
+      })
+      .collect();
+    CssRule::Supports(SupportsRule {
+      condition: kind.supports_condition(),
+      rules: CssRuleList(vec![CssRule::FontPaletteValues(FontPaletteValuesRule {
+        name: self.name.clone(),
+        properties,
+        loc: self.loc.clone(),
+      })]),
+      loc: self.loc.clone(),
+    })
+  }
+}
+
+impl<'i> ToCss for FontPaletteValuesRule<'i> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    #[cfg(feature = "sourcemap")]
+    dest.add_mapping(self.loc);
+    dest.write_str("@font-palette-values ")?;
+    self.name.to_css(dest)?;
+    dest.whitespace()?;
+    dest.write_char('{')?;
+    dest.indent();
+    let len = self.properties.len();
+    for (i, prop) in self.properties.iter().enumerate() {
+      dest.newline()?;
+      prop.to_css(dest)?;
+      if i != len - 1 || !dest.minify {
+        dest.write_char(';')?;
+      }
+    }
+    dest.dedent();
+    dest.newline()?;
+    dest.write_char('}')
+  }
+}
+
+impl<'i> ToCss for FontPaletteValuesProperty<'i> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    macro_rules! property {
+      ($prop: literal, $value: expr) => {{
+        dest.write_str($prop)?;
+        dest.delim(':', false)?;
+        $value.to_css(dest)
+      }};
+    }
+
+    match self {
+      FontPaletteValuesProperty::FontFamily(f) => property!("font-family", f),
+      FontPaletteValuesProperty::BasePalette(b) => property!("base-palette", b),
+      FontPaletteValuesProperty::OverrideColors(o) => property!("override-colors", o),
+      FontPaletteValuesProperty::Custom(custom) => {
+        dest.write_str(custom.name.as_ref())?;
+        dest.delim(':', false)?;
+        custom.value.to_css(dest, true)
+      }
+    }
+  }
+}
diff --git a/src/rules/import.rs b/src/rules/import.rs
new file mode 100644
index 0000000..e240a6d
--- /dev/null
+++ b/src/rules/import.rs
@@ -0,0 +1,89 @@
+//! The `@import` rule.
+
+use super::layer::LayerName;
+use super::supports::SupportsCondition;
+use super::Location;
+use crate::dependencies::{Dependency, ImportDependency};
+use crate::error::PrinterError;
+use crate::media_query::MediaList;
+use crate::printer::Printer;
+use crate::traits::ToCss;
+use crate::values::string::CowArcStr;
+#[cfg(feature = "visitor")]
+use crate::visitor::Visit;
+use cssparser::*;
+
+/// A [@import](https://drafts.csswg.org/css-cascade/#at-import) rule.
+#[derive(Debug, PartialEq, Clone)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub struct ImportRule<'i> {
+  /// The url to import.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  #[cfg_attr(feature = "visitor", skip_visit)]
+  pub url: CowArcStr<'i>,
+  /// An optional cascade layer name, or `None` for an anonymous layer.
+  #[cfg_attr(feature = "visitor", skip_visit)]
+  pub layer: Option<Option<LayerName<'i>>>,
+  /// An optional `supports()` condition.
+  pub supports: Option<SupportsCondition<'i>>,
+  /// A media query.
+  #[cfg_attr(feature = "serde", serde(default))]
+  pub media: MediaList<'i>,
+  /// The location of the rule in the source file.
+  #[cfg_attr(feature = "visitor", skip_visit)]
+  pub loc: Location,
+}
+
+impl<'i> ToCss for ImportRule<'i> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    let dep = if dest.dependencies.is_some() {
+      Some(ImportDependency::new(self, dest.filename()))
+    } else {
+      None
+    };
+
+    #[cfg(feature = "sourcemap")]
+    dest.add_mapping(self.loc);
+    dest.write_str("@import ")?;
+    if let Some(dep) = dep {
+      serialize_string(&dep.placeholder, dest)?;
+
+      if let Some(dependencies) = &mut dest.dependencies {
+        dependencies.push(Dependency::Import(dep))
+      }
+    } else {
+      serialize_string(&self.url, dest)?;
+    }
+
+    if let Some(layer) = &self.layer {
+      dest.write_str(" layer")?;
+      if let Some(name) = layer {
+        dest.write_char('(')?;
+        name.to_css(dest)?;
+        dest.write_char(')')?;
+      }
+    }
+
+    if let Some(supports) = &self.supports {
+      dest.write_str(" supports")?;
+      if matches!(supports, SupportsCondition::Declaration { .. }) {
+        supports.to_css(dest)?;
+      } else {
+        dest.write_char('(')?;
+        supports.to_css(dest)?;
+        dest.write_char(')')?;
+      }
+    }
+    if !self.media.media_queries.is_empty() {
+      dest.write_char(' ')?;
+      self.media.to_css(dest)?;
+    }
+    dest.write_str(";")
+  }
+}
diff --git a/src/rules/keyframes.rs b/src/rules/keyframes.rs
new file mode 100644
index 0000000..02d0943
--- /dev/null
+++ b/src/rules/keyframes.rs
@@ -0,0 +1,431 @@
+//! The `@keyframes` rule.
+
+use super::supports::SupportsRule;
+use super::MinifyContext;
+use super::{CssRule, CssRuleList, Location};
+use crate::context::DeclarationContext;
+use crate::declaration::DeclarationBlock;
+use crate::error::{ParserError, PrinterError};
+use crate::parser::ParserOptions;
+use crate::printer::Printer;
+use crate::properties::animation::TimelineRangeName;
+use crate::properties::custom::{CustomProperty, UnparsedProperty};
+use crate::properties::Property;
+use crate::targets::Targets;
+use crate::traits::{Parse, ToCss};
+use crate::values::color::ColorFallbackKind;
+use crate::values::ident::CustomIdent;
+use crate::values::percentage::Percentage;
+use crate::values::string::CowArcStr;
+use crate::vendor_prefix::VendorPrefix;
+#[cfg(feature = "visitor")]
+use crate::visitor::Visit;
+use cssparser::*;
+
+/// A [@keyframes](https://drafts.csswg.org/css-animations/#keyframes) rule.
+#[derive(Debug, PartialEq, Clone)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(rename_all = "camelCase")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub struct KeyframesRule<'i> {
+  /// The animation name.
+  /// <keyframes-name> = <custom-ident> | <string>
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  pub name: KeyframesName<'i>,
+  /// A list of keyframes in the animation.
+  pub keyframes: Vec<Keyframe<'i>>,
+  /// A vendor prefix for the rule, e.g. `@-webkit-keyframes`.
+  #[cfg_attr(feature = "visitor", skip_visit)]
+  pub vendor_prefix: VendorPrefix,
+  /// The location of the rule in the source file.
+  #[cfg_attr(feature = "visitor", skip_visit)]
+  pub loc: Location,
+}
+
+/// KeyframesName
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", content = "value", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub enum KeyframesName<'i> {
+  /// `<custom-ident>` of a `@keyframes` name.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  Ident(CustomIdent<'i>),
+
+  /// `<string>` of a `@keyframes` name.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  Custom(CowArcStr<'i>),
+}
+
+impl<'i> Parse<'i> for KeyframesName<'i> {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    match input.next()?.clone() {
+      Token::Ident(ref s) => {
+        // CSS-wide keywords without quotes throws an error.
+        match_ignore_ascii_case! { &*s,
+          "none" | "initial" | "inherit" | "unset" | "default" | "revert" | "revert-layer" => {
+            Err(input.new_unexpected_token_error(Token::Ident(s.clone())))
+          },
+          _ => {
+            Ok(KeyframesName::Ident(CustomIdent(s.into())))
+          }
+        }
+      }
+
+      Token::QuotedString(ref s) => Ok(KeyframesName::Custom(s.into())),
+      t => return Err(input.new_unexpected_token_error(t.clone())),
+    }
+  }
+}
+
+impl<'i> ToCss for KeyframesName<'i> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    let css_module_animation_enabled =
+      dest.css_module.as_ref().map_or(false, |css_module| css_module.config.animation);
+
+    match self {
+      KeyframesName::Ident(ident) => {
+        dest.write_ident(ident.0.as_ref(), css_module_animation_enabled)?;
+      }
+      KeyframesName::Custom(s) => {
+        // CSS-wide keywords and `none` cannot remove quotes.
+        match_ignore_ascii_case! { &*s,
+          "none" | "initial" | "inherit" | "unset" | "default" | "revert" | "revert-layer" => {
+            serialize_string(&s, dest)?;
+          },
+          _ => {
+            dest.write_ident(s.as_ref(), css_module_animation_enabled)?;
+          }
+        }
+      }
+    }
+    Ok(())
+  }
+}
+
+impl<'i> KeyframesRule<'i> {
+  pub(crate) fn minify(&mut self, context: &mut MinifyContext<'_, 'i>) {
+    context.handler_context.context = DeclarationContext::Keyframes;
+
+    for keyframe in &mut self.keyframes {
+      keyframe
+        .declarations
+        .minify(context.handler, context.important_handler, &mut context.handler_context)
+    }
+
+    context.handler_context.context = DeclarationContext::None;
+  }
+
+  pub(crate) fn get_fallbacks<T>(&mut self, targets: &Targets) -> Vec<CssRule<'i, T>> {
+    let mut fallbacks = ColorFallbackKind::empty();
+    for keyframe in &self.keyframes {
+      for property in &keyframe.declarations.declarations {
+        match property {
+          Property::Custom(CustomProperty { value, .. }) | Property::Unparsed(UnparsedProperty { value, .. }) => {
+            fallbacks |= value.get_necessary_fallbacks(*targets);
+          }
+          _ => {}
+        }
+      }
+    }
+
+    let mut res = Vec::new();
+    let lowest_fallback = fallbacks.lowest();
+    fallbacks.remove(lowest_fallback);
+
+    if fallbacks.contains(ColorFallbackKind::P3) {
+      res.push(self.get_fallback(ColorFallbackKind::P3));
+    }
+
+    if fallbacks.contains(ColorFallbackKind::LAB)
+      || (!lowest_fallback.is_empty() && lowest_fallback != ColorFallbackKind::LAB)
+    {
+      res.push(self.get_fallback(ColorFallbackKind::LAB));
+    }
+
+    if !lowest_fallback.is_empty() {
+      for keyframe in &mut self.keyframes {
+        for property in &mut keyframe.declarations.declarations {
+          match property {
+            Property::Custom(CustomProperty { value, .. })
+            | Property::Unparsed(UnparsedProperty { value, .. }) => {
+              *value = value.get_fallback(lowest_fallback);
+            }
+            _ => {}
+          }
+        }
+      }
+    }
+
+    res
+  }
+
+  fn get_fallback<T>(&self, kind: ColorFallbackKind) -> CssRule<'i, T> {
+    let keyframes = self
+      .keyframes
+      .iter()
+      .map(|keyframe| Keyframe {
+        selectors: keyframe.selectors.clone(),
+        declarations: DeclarationBlock {
+          important_declarations: vec![],
+          declarations: keyframe
+            .declarations
+            .declarations
+            .iter()
+            .map(|property| match property {
+              Property::Custom(custom) => Property::Custom(CustomProperty {
+                name: custom.name.clone(),
+                value: custom.value.get_fallback(kind),
+              }),
+              Property::Unparsed(unparsed) => Property::Unparsed(UnparsedProperty {
+                property_id: unparsed.property_id.clone(),
+                value: unparsed.value.get_fallback(kind),
+              }),
+              _ => property.clone(),
+            })
+            .collect(),
+        },
+      })
+      .collect();
+
+    CssRule::Supports(SupportsRule {
+      condition: kind.supports_condition(),
+      rules: CssRuleList(vec![CssRule::Keyframes(KeyframesRule {
+        name: self.name.clone(),
+        keyframes,
+        vendor_prefix: self.vendor_prefix,
+        loc: self.loc.clone(),
+      })]),
+      loc: self.loc.clone(),
+    })
+  }
+}
+
+impl<'i> ToCss for KeyframesRule<'i> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    #[cfg(feature = "sourcemap")]
+    dest.add_mapping(self.loc);
+    let mut first_rule = true;
+    macro_rules! write_prefix {
+      ($prefix: ident) => {
+        if self.vendor_prefix.contains(VendorPrefix::$prefix) {
+          #[allow(unused_assignments)]
+          if first_rule {
+            first_rule = false;
+          } else {
+            if !dest.minify {
+              dest.write_char('\n')?; // no indent
+            }
+            dest.newline()?;
+          }
+          dest.write_char('@')?;
+          VendorPrefix::$prefix.to_css(dest)?;
+          dest.write_str("keyframes ")?;
+          self.name.to_css(dest)?;
+          dest.whitespace()?;
+          dest.write_char('{')?;
+          dest.indent();
+          let mut first = true;
+          for keyframe in &self.keyframes {
+            if first {
+              first = false;
+            } else if !dest.minify {
+              dest.write_char('\n')?; // no indent
+            }
+            dest.newline()?;
+            keyframe.to_css(dest)?;
+          }
+          dest.dedent();
+          dest.newline()?;
+          dest.write_char('}')?;
+        }
+      };
+    }
+
+    write_prefix!(WebKit);
+    write_prefix!(Moz);
+    write_prefix!(O);
+    write_prefix!(None);
+    Ok(())
+  }
+}
+
+/// A percentage of a given timeline range
+#[derive(Debug, PartialEq, Clone)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", rename_all = "camelCase")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub struct TimelineRangePercentage {
+  /// The name of the timeline range.
+  name: TimelineRangeName,
+  /// The percentage progress between the start and end of the range.
+  percentage: Percentage,
+}
+
+impl<'i> Parse<'i> for TimelineRangePercentage {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let name = TimelineRangeName::parse(input)?;
+    let percentage = Percentage::parse(input)?;
+    Ok(TimelineRangePercentage { name, percentage })
+  }
+}
+
+/// A [keyframe selector](https://drafts.csswg.org/css-animations/#typedef-keyframe-selector)
+/// within an `@keyframes` rule.
+#[derive(Debug, PartialEq, Clone, Parse)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", content = "value", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum KeyframeSelector {
+  /// An explicit percentage.
+  Percentage(Percentage),
+  /// The `from` keyword. Equivalent to 0%.
+  From,
+  /// The `to` keyword. Equivalent to 100%.
+  To,
+  /// A [named timeline range selector](https://drafts.csswg.org/scroll-animations-1/#named-range-keyframes)
+  TimelineRangePercentage(TimelineRangePercentage),
+}
+
+impl ToCss for KeyframeSelector {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    match self {
+      KeyframeSelector::Percentage(p) => {
+        if dest.minify && *p == Percentage(1.0) {
+          dest.write_str("to")
+        } else {
+          p.to_css(dest)
+        }
+      }
+      KeyframeSelector::From => {
+        if dest.minify {
+          dest.write_str("0%")
+        } else {
+          dest.write_str("from")
+        }
+      }
+      KeyframeSelector::To => dest.write_str("to"),
+      KeyframeSelector::TimelineRangePercentage(TimelineRangePercentage {
+        name: timeline_range_name,
+        percentage,
+      }) => {
+        timeline_range_name.to_css(dest)?;
+        dest.write_char(' ')?;
+        percentage.to_css(dest)
+      }
+    }
+  }
+}
+
+/// An individual keyframe within an `@keyframes` rule.
+///
+/// See [KeyframesRule](KeyframesRule).
+#[derive(Debug, PartialEq, Clone)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub struct Keyframe<'i> {
+  /// A list of keyframe selectors to associate with the declarations in this keyframe.
+  pub selectors: Vec<KeyframeSelector>,
+  /// The declarations for this keyframe.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  pub declarations: DeclarationBlock<'i>,
+}
+
+impl<'i> ToCss for Keyframe<'i> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    let mut first = true;
+    for selector in &self.selectors {
+      if !first {
+        dest.delim(',', false)?;
+      }
+      first = false;
+      selector.to_css(dest)?;
+    }
+
+    self.declarations.to_css_block(dest)
+  }
+}
+
+pub(crate) struct KeyframeListParser;
+
+impl<'a, 'i> AtRuleParser<'i> for KeyframeListParser {
+  type Prelude = ();
+  type AtRule = Keyframe<'i>;
+  type Error = ParserError<'i>;
+}
+
+impl<'a, 'i> QualifiedRuleParser<'i> for KeyframeListParser {
+  type Prelude = Vec<KeyframeSelector>;
+  type QualifiedRule = Keyframe<'i>;
+  type Error = ParserError<'i>;
+
+  fn parse_prelude<'t>(
+    &mut self,
+    input: &mut Parser<'i, 't>,
+  ) -> Result<Self::Prelude, ParseError<'i, ParserError<'i>>> {
+    input.parse_comma_separated(KeyframeSelector::parse)
+  }
+
+  fn parse_block<'t>(
+    &mut self,
+    selectors: Self::Prelude,
+    _: &ParserState,
+    input: &mut Parser<'i, 't>,
+  ) -> Result<Self::QualifiedRule, ParseError<'i, ParserError<'i>>> {
+    // For now there are no options that apply within @keyframes
+    let options = ParserOptions::default();
+    Ok(Keyframe {
+      selectors,
+      declarations: DeclarationBlock::parse(input, &options)?,
+    })
+  }
+}
+
+impl<'i> DeclarationParser<'i> for KeyframeListParser {
+  type Declaration = Keyframe<'i>;
+  type Error = ParserError<'i>;
+}
+
+impl<'i> RuleBodyItemParser<'i, Keyframe<'i>, ParserError<'i>> for KeyframeListParser {
+  fn parse_qualified(&self) -> bool {
+    true
+  }
+
+  fn parse_declarations(&self) -> bool {
+    false
+  }
+}
diff --git a/src/rules/layer.rs b/src/rules/layer.rs
new file mode 100644
index 0000000..9a66e53
--- /dev/null
+++ b/src/rules/layer.rs
@@ -0,0 +1,166 @@
+//! The `@layer` rule.
+
+use super::{CssRuleList, Location, MinifyContext};
+use crate::error::{MinifyError, ParserError, PrinterError};
+use crate::parser::DefaultAtRule;
+use crate::printer::Printer;
+use crate::traits::{Parse, ToCss};
+use crate::values::string::CowArcStr;
+#[cfg(feature = "visitor")]
+use crate::visitor::Visit;
+use cssparser::*;
+use smallvec::SmallVec;
+
+/// A [`<layer-name>`](https://drafts.csswg.org/css-cascade-5/#typedef-layer-name) within
+/// a `@layer` or `@import` rule.
+///
+/// Nested layers are represented using a list of identifiers. In CSS syntax, these are dot-separated.
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(transparent))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub struct LayerName<'i>(#[cfg_attr(feature = "serde", serde(borrow))] pub SmallVec<[CowArcStr<'i>; 1]>);
+
+macro_rules! expect_non_whitespace {
+  ($parser: ident, $($branches: tt)+) => {{
+    let start_location = $parser.current_source_location();
+    match *$parser.next_including_whitespace()? {
+      $($branches)+
+      ref token => {
+        return Err(start_location.new_basic_unexpected_token_error(token.clone()))
+      }
+    }
+  }}
+}
+
+impl<'i> Parse<'i> for LayerName<'i> {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let mut parts = SmallVec::new();
+    let ident = input.expect_ident()?;
+    parts.push(ident.into());
+
+    loop {
+      let name = input.try_parse(|input| {
+        expect_non_whitespace! {input,
+          Token::Delim('.') => Ok(()),
+        }?;
+
+        expect_non_whitespace! {input,
+          Token::Ident(ref id) => Ok(id.into()),
+        }
+      });
+
+      match name {
+        Ok(name) => parts.push(name),
+        Err(_) => break,
+      }
+    }
+
+    Ok(LayerName(parts))
+  }
+}
+
+impl<'i> ToCss for LayerName<'i> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    let mut first = true;
+    for name in &self.0 {
+      if first {
+        first = false;
+      } else {
+        dest.write_char('.')?;
+      }
+
+      serialize_identifier(name, dest)?;
+    }
+
+    Ok(())
+  }
+}
+
+/// A [@layer statement](https://drafts.csswg.org/css-cascade-5/#layer-empty) rule.
+///
+/// See also [LayerBlockRule](LayerBlockRule).
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub struct LayerStatementRule<'i> {
+  /// The layer names to declare.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  #[cfg_attr(feature = "visitor", skip_visit)]
+  pub names: Vec<LayerName<'i>>,
+  /// The location of the rule in the source file.
+  #[cfg_attr(feature = "visitor", skip_visit)]
+  pub loc: Location,
+}
+
+impl<'i> ToCss for LayerStatementRule<'i> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    #[cfg(feature = "sourcemap")]
+    dest.add_mapping(self.loc);
+    dest.write_str("@layer ")?;
+    self.names.to_css(dest)?;
+    dest.write_char(';')
+  }
+}
+
+/// A [@layer block](https://drafts.csswg.org/css-cascade-5/#layer-block) rule.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub struct LayerBlockRule<'i, R = DefaultAtRule> {
+  /// The name of the layer to declare, or `None` to declare an anonymous layer.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  #[cfg_attr(feature = "visitor", skip_visit)]
+  pub name: Option<LayerName<'i>>,
+  /// The rules within the `@layer` rule.
+  pub rules: CssRuleList<'i, R>,
+  /// The location of the rule in the source file.
+  #[cfg_attr(feature = "visitor", skip_visit)]
+  pub loc: Location,
+}
+
+impl<'i, T: Clone> LayerBlockRule<'i, T> {
+  pub(crate) fn minify(
+    &mut self,
+    context: &mut MinifyContext<'_, 'i>,
+    parent_is_unused: bool,
+  ) -> Result<bool, MinifyError> {
+    self.rules.minify(context, parent_is_unused)?;
+
+    Ok(self.rules.0.is_empty())
+  }
+}
+
+impl<'a, 'i, T: ToCss> ToCss for LayerBlockRule<'i, T> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    #[cfg(feature = "sourcemap")]
+    dest.add_mapping(self.loc);
+    dest.write_str("@layer")?;
+    if let Some(name) = &self.name {
+      dest.write_char(' ')?;
+      name.to_css(dest)?;
+    }
+
+    dest.whitespace()?;
+    dest.write_char('{')?;
+    dest.indent();
+    dest.newline()?;
+    self.rules.to_css(dest)?;
+    dest.dedent();
+    dest.newline()?;
+    dest.write_char('}')
+  }
+}
diff --git a/src/rules/media.rs b/src/rules/media.rs
new file mode 100644
index 0000000..c398b9b
--- /dev/null
+++ b/src/rules/media.rs
@@ -0,0 +1,71 @@
+//! The `@media` rule.
+
+use super::Location;
+use super::{CssRuleList, MinifyContext};
+use crate::error::{MinifyError, PrinterError};
+use crate::media_query::MediaList;
+use crate::parser::DefaultAtRule;
+use crate::printer::Printer;
+use crate::traits::ToCss;
+#[cfg(feature = "visitor")]
+use crate::visitor::Visit;
+
+/// A [@media](https://drafts.csswg.org/css-conditional-3/#at-media) rule.
+#[derive(Debug, PartialEq, Clone)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub struct MediaRule<'i, R = DefaultAtRule> {
+  /// The media query list.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  pub query: MediaList<'i>,
+  /// The rules within the `@media` rule.
+  pub rules: CssRuleList<'i, R>,
+  /// The location of the rule in the source file.
+  #[cfg_attr(feature = "visitor", skip_visit)]
+  pub loc: Location,
+}
+
+impl<'i, T: Clone> MediaRule<'i, T> {
+  pub(crate) fn minify(
+    &mut self,
+    context: &mut MinifyContext<'_, 'i>,
+    parent_is_unused: bool,
+  ) -> Result<bool, MinifyError> {
+    self.rules.minify(context, parent_is_unused)?;
+
+    if let Some(custom_media) = &context.custom_media {
+      self.query.transform_custom_media(self.loc, custom_media)?;
+    }
+
+    self.query.transform_resolution(context.targets.current);
+    Ok(self.rules.0.is_empty() || self.query.never_matches())
+  }
+}
+
+impl<'a, 'i, T: ToCss> ToCss for MediaRule<'i, T> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    // If the media query always matches, we can just output the nested rules.
+    if dest.minify && self.query.always_matches() {
+      self.rules.to_css(dest)?;
+      return Ok(());
+    }
+
+    #[cfg(feature = "sourcemap")]
+    dest.add_mapping(self.loc);
+    dest.write_str("@media ")?;
+    self.query.to_css(dest)?;
+    dest.whitespace()?;
+    dest.write_char('{')?;
+    dest.indent();
+    dest.newline()?;
+    self.rules.to_css(dest)?;
+    dest.dedent();
+    dest.newline()?;
+    dest.write_char('}')
+  }
+}
diff --git a/src/rules/mod.rs b/src/rules/mod.rs
new file mode 100644
index 0000000..5981a42
--- /dev/null
+++ b/src/rules/mod.rs
@@ -0,0 +1,1145 @@
+//! CSS rules.
+//!
+//! The [CssRule](CssRule) enum includes all supported rules, and can be used to parse
+//! and serialize rules from CSS. Lists of rules (i.e. within a stylesheet, or inside
+//! another rule such as `@media`) are represented by [CssRuleList](CssRuleList).
+//!
+//! Each rule includes a source location, which indicates the line and column within
+//! the source file where it was parsed. This is used when generating source maps.
+//!
+//! # Example
+//!
+//! This example shows how you could parse a single CSS rule, and serialize it to a string.
+//!
+//! ```
+//! use lightningcss::{
+//!   rules::CssRule,
+//!   traits::ToCss,
+//!   stylesheet::{ParserOptions, PrinterOptions}
+//! };
+//!
+//! let rule = CssRule::parse_string(
+//!   ".foo { color: red; }",
+//!   ParserOptions::default()
+//! ).unwrap();
+//!
+//! assert_eq!(
+//!   rule.to_css_string(PrinterOptions::default()).unwrap(),
+//!   ".foo {\n  color: red;\n}"
+//! );
+//! ```
+//!
+//! If you have a [cssparser::Parser](cssparser::Parser) already, you can also use the `parse` and `to_css`
+//! methods instead, rather than parsing from a string.
+//!
+//! See [StyleSheet](super::stylesheet::StyleSheet) to parse an entire file of multiple rules.
+
+#![deny(missing_docs)]
+
+pub mod container;
+pub mod counter_style;
+pub mod custom_media;
+pub mod document;
+pub mod font_face;
+pub mod font_feature_values;
+pub mod font_palette_values;
+pub mod import;
+pub mod keyframes;
+pub mod layer;
+pub mod media;
+pub mod namespace;
+pub mod nesting;
+pub mod page;
+pub mod property;
+pub mod scope;
+pub mod starting_style;
+pub mod style;
+pub mod supports;
+pub mod unknown;
+pub mod view_transition;
+pub mod viewport;
+
+use self::font_feature_values::FontFeatureValuesRule;
+use self::font_palette_values::FontPaletteValuesRule;
+use self::layer::{LayerBlockRule, LayerStatementRule};
+use self::property::PropertyRule;
+use crate::context::PropertyHandlerContext;
+use crate::declaration::{DeclarationBlock, DeclarationHandler};
+use crate::dependencies::{Dependency, ImportDependency};
+use crate::error::{MinifyError, ParserError, PrinterError, PrinterErrorKind};
+use crate::parser::{parse_rule_list, parse_style_block, DefaultAtRule, DefaultAtRuleParser, TopLevelRuleParser};
+use crate::prefixes::Feature;
+use crate::printer::Printer;
+use crate::rules::keyframes::KeyframesName;
+use crate::selector::{is_compatible, is_equivalent, Component, Selector, SelectorList};
+use crate::stylesheet::ParserOptions;
+use crate::targets::TargetsWithSupportsScope;
+use crate::traits::{AtRuleParser, ToCss};
+use crate::values::string::CowArcStr;
+use crate::vendor_prefix::VendorPrefix;
+#[cfg(feature = "visitor")]
+use crate::visitor::{Visit, VisitTypes, Visitor};
+use container::ContainerRule;
+use counter_style::CounterStyleRule;
+use cssparser::{parse_one_rule, ParseError, Parser, ParserInput};
+use custom_media::CustomMediaRule;
+use document::MozDocumentRule;
+use font_face::FontFaceRule;
+use import::ImportRule;
+use itertools::Itertools;
+use keyframes::KeyframesRule;
+use media::MediaRule;
+use namespace::NamespaceRule;
+use nesting::{NestedDeclarationsRule, NestingRule};
+use page::PageRule;
+use scope::ScopeRule;
+use smallvec::{smallvec, SmallVec};
+use starting_style::StartingStyleRule;
+use std::collections::{HashMap, HashSet};
+use std::hash::{BuildHasherDefault, Hasher};
+use style::StyleRule;
+use supports::SupportsRule;
+use unknown::UnknownAtRule;
+use view_transition::ViewTransitionRule;
+use viewport::ViewportRule;
+
+#[derive(Clone)]
+pub(crate) struct StyleContext<'a, 'i> {
+  pub selectors: &'a SelectorList<'i>,
+  pub parent: Option<&'a StyleContext<'a, 'i>>,
+}
+
+/// A source location.
+#[derive(PartialEq, Eq, Debug, Clone, Copy)]
+#[cfg_attr(any(feature = "serde", feature = "nodejs"), derive(serde::Serialize))]
+#[cfg_attr(feature = "serde", derive(serde::Deserialize))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub struct Location {
+  /// The index of the source file within the source map.
+  pub source_index: u32,
+  /// The line number, starting at 0.
+  pub line: u32,
+  /// The column number within a line, starting at 1 for first the character of the line.
+  /// Column numbers are counted in UTF-16 code units.
+  pub column: u32,
+}
+
+/// A CSS rule.
+#[derive(Debug, PartialEq, Clone)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "visitor", visit(visit_rule, RULES))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize),
+  serde(tag = "type", content = "value", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema), schemars(rename = "Rule"))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum CssRule<'i, R = DefaultAtRule> {
+  /// A `@media` rule.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  Media(MediaRule<'i, R>),
+  /// An `@import` rule.
+  Import(ImportRule<'i>),
+  /// A style rule.
+  Style(StyleRule<'i, R>),
+  /// A `@keyframes` rule.
+  Keyframes(KeyframesRule<'i>),
+  /// A `@font-face` rule.
+  FontFace(FontFaceRule<'i>),
+  /// A `@font-palette-values` rule.
+  FontPaletteValues(FontPaletteValuesRule<'i>),
+  /// A `@font-feature-values` rule.
+  FontFeatureValues(FontFeatureValuesRule<'i>),
+  /// A `@page` rule.
+  Page(PageRule<'i>),
+  /// A `@supports` rule.
+  Supports(SupportsRule<'i, R>),
+  /// A `@counter-style` rule.
+  CounterStyle(CounterStyleRule<'i>),
+  /// A `@namespace` rule.
+  Namespace(NamespaceRule<'i>),
+  /// A `@-moz-document` rule.
+  MozDocument(MozDocumentRule<'i, R>),
+  /// A `@nest` rule.
+  Nesting(NestingRule<'i, R>),
+  /// A nested declarations rule.
+  NestedDeclarations(NestedDeclarationsRule<'i>),
+  /// A `@viewport` rule.
+  Viewport(ViewportRule<'i>),
+  /// A `@custom-media` rule.
+  CustomMedia(CustomMediaRule<'i>),
+  /// A `@layer` statement rule.
+  LayerStatement(LayerStatementRule<'i>),
+  /// A `@layer` block rule.
+  LayerBlock(LayerBlockRule<'i, R>),
+  /// A `@property` rule.
+  Property(PropertyRule<'i>),
+  /// A `@container` rule.
+  Container(ContainerRule<'i, R>),
+  /// A `@scope` rule.
+  Scope(ScopeRule<'i, R>),
+  /// A `@starting-style` rule.
+  StartingStyle(StartingStyleRule<'i, R>),
+  /// A `@view-transition` rule.
+  ViewTransition(ViewTransitionRule<'i>),
+  /// A placeholder for a rule that was removed.
+  Ignored,
+  /// An unknown at-rule.
+  Unknown(UnknownAtRule<'i>),
+  /// A custom at-rule.
+  Custom(R),
+}
+
+// Manually implemented deserialize to reduce binary size.
+#[cfg(feature = "serde")]
+#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
+impl<'i, 'de: 'i, R: serde::Deserialize<'de>> serde::Deserialize<'de> for CssRule<'i, R> {
+  fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+  where
+    D: serde::Deserializer<'de>,
+  {
+    #[derive(serde::Deserialize)]
+    #[serde(field_identifier, rename_all = "snake_case")]
+    enum Field {
+      Type,
+      Value,
+    }
+
+    struct PartialRule<'de> {
+      rule_type: CowArcStr<'de>,
+      content: serde::__private::de::Content<'de>,
+    }
+
+    struct CssRuleVisitor;
+
+    impl<'de> serde::de::Visitor<'de> for CssRuleVisitor {
+      type Value = PartialRule<'de>;
+
+      fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
+        formatter.write_str("a CssRule")
+      }
+
+      fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
+      where
+        A: serde::de::MapAccess<'de>,
+      {
+        let mut rule_type: Option<CowArcStr<'de>> = None;
+        let mut value: Option<serde::__private::de::Content> = None;
+        while let Some(key) = map.next_key()? {
+          match key {
+            Field::Type => {
+              rule_type = Some(map.next_value()?);
+            }
+            Field::Value => {
+              value = Some(map.next_value()?);
+            }
+          }
+        }
+
+        let rule_type = rule_type.ok_or_else(|| serde::de::Error::missing_field("type"))?;
+        let content = value.ok_or_else(|| serde::de::Error::missing_field("value"))?;
+        Ok(PartialRule { rule_type, content })
+      }
+    }
+
+    let partial = deserializer.deserialize_map(CssRuleVisitor)?;
+    let deserializer = serde::__private::de::ContentDeserializer::new(partial.content);
+
+    match partial.rule_type.as_ref() {
+      "media" => {
+        let rule = MediaRule::deserialize(deserializer)?;
+        Ok(CssRule::Media(rule))
+      }
+      "import" => {
+        let rule = ImportRule::deserialize(deserializer)?;
+        Ok(CssRule::Import(rule))
+      }
+      "style" => {
+        let rule = StyleRule::deserialize(deserializer)?;
+        Ok(CssRule::Style(rule))
+      }
+      "keyframes" => {
+        let rule = KeyframesRule::deserialize(deserializer)?;
+        Ok(CssRule::Keyframes(rule))
+      }
+      "font-face" => {
+        let rule = FontFaceRule::deserialize(deserializer)?;
+        Ok(CssRule::FontFace(rule))
+      }
+      "font-palette-values" => {
+        let rule = FontPaletteValuesRule::deserialize(deserializer)?;
+        Ok(CssRule::FontPaletteValues(rule))
+      }
+      "font-feature-values" => {
+        let rule = FontFeatureValuesRule::deserialize(deserializer)?;
+        Ok(CssRule::FontFeatureValues(rule))
+      }
+      "page" => {
+        let rule = PageRule::deserialize(deserializer)?;
+        Ok(CssRule::Page(rule))
+      }
+      "supports" => {
+        let rule = SupportsRule::deserialize(deserializer)?;
+        Ok(CssRule::Supports(rule))
+      }
+      "counter-style" => {
+        let rule = CounterStyleRule::deserialize(deserializer)?;
+        Ok(CssRule::CounterStyle(rule))
+      }
+      "namespace" => {
+        let rule = NamespaceRule::deserialize(deserializer)?;
+        Ok(CssRule::Namespace(rule))
+      }
+      "moz-document" => {
+        let rule = MozDocumentRule::deserialize(deserializer)?;
+        Ok(CssRule::MozDocument(rule))
+      }
+      "nesting" => {
+        let rule = NestingRule::deserialize(deserializer)?;
+        Ok(CssRule::Nesting(rule))
+      }
+      "nested-declarations" => {
+        let rule = NestedDeclarationsRule::deserialize(deserializer)?;
+        Ok(CssRule::NestedDeclarations(rule))
+      }
+      "viewport" => {
+        let rule = ViewportRule::deserialize(deserializer)?;
+        Ok(CssRule::Viewport(rule))
+      }
+      "custom-media" => {
+        let rule = CustomMediaRule::deserialize(deserializer)?;
+        Ok(CssRule::CustomMedia(rule))
+      }
+      "layer-statement" => {
+        let rule = LayerStatementRule::deserialize(deserializer)?;
+        Ok(CssRule::LayerStatement(rule))
+      }
+      "layer-block" => {
+        let rule = LayerBlockRule::deserialize(deserializer)?;
+        Ok(CssRule::LayerBlock(rule))
+      }
+      "property" => {
+        let rule = PropertyRule::deserialize(deserializer)?;
+        Ok(CssRule::Property(rule))
+      }
+      "container" => {
+        let rule = ContainerRule::deserialize(deserializer)?;
+        Ok(CssRule::Container(rule))
+      }
+      "scope" => {
+        let rule = ScopeRule::deserialize(deserializer)?;
+        Ok(CssRule::Scope(rule))
+      }
+      "starting-style" => {
+        let rule = StartingStyleRule::deserialize(deserializer)?;
+        Ok(CssRule::StartingStyle(rule))
+      }
+      "view-transition" => {
+        let rule = ViewTransitionRule::deserialize(deserializer)?;
+        Ok(CssRule::ViewTransition(rule))
+      }
+      "ignored" => Ok(CssRule::Ignored),
+      "unknown" => {
+        let rule = UnknownAtRule::deserialize(deserializer)?;
+        Ok(CssRule::Unknown(rule))
+      }
+      "custom" => {
+        let rule = R::deserialize(deserializer)?;
+        Ok(CssRule::Custom(rule))
+      }
+      t => Err(serde::de::Error::unknown_variant(t, &[])),
+    }
+  }
+}
+
+impl<'a, 'i, T: ToCss> ToCss for CssRule<'i, T> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    match self {
+      CssRule::Media(media) => media.to_css(dest),
+      CssRule::Import(import) => import.to_css(dest),
+      CssRule::Style(style) => style.to_css(dest),
+      CssRule::Keyframes(keyframes) => keyframes.to_css(dest),
+      CssRule::FontFace(font_face) => font_face.to_css(dest),
+      CssRule::FontPaletteValues(f) => f.to_css(dest),
+      CssRule::FontFeatureValues(font_feature_values) => font_feature_values.to_css(dest),
+      CssRule::Page(font_face) => font_face.to_css(dest),
+      CssRule::Supports(supports) => supports.to_css(dest),
+      CssRule::CounterStyle(counter_style) => counter_style.to_css(dest),
+      CssRule::Namespace(namespace) => namespace.to_css(dest),
+      CssRule::MozDocument(document) => document.to_css(dest),
+      CssRule::Nesting(nesting) => nesting.to_css(dest),
+      CssRule::NestedDeclarations(nested) => nested.to_css(dest),
+      CssRule::Viewport(viewport) => viewport.to_css(dest),
+      CssRule::CustomMedia(custom_media) => custom_media.to_css(dest),
+      CssRule::LayerStatement(layer) => layer.to_css(dest),
+      CssRule::LayerBlock(layer) => layer.to_css(dest),
+      CssRule::Property(property) => property.to_css(dest),
+      CssRule::StartingStyle(rule) => rule.to_css(dest),
+      CssRule::Container(container) => container.to_css(dest),
+      CssRule::Scope(scope) => scope.to_css(dest),
+      CssRule::ViewTransition(rule) => rule.to_css(dest),
+      CssRule::Unknown(unknown) => unknown.to_css(dest),
+      CssRule::Custom(rule) => rule.to_css(dest).map_err(|_| PrinterError {
+        kind: PrinterErrorKind::FmtError,
+        loc: None,
+      }),
+      CssRule::Ignored => Ok(()),
+    }
+  }
+}
+
+impl<'i> CssRule<'i, DefaultAtRule> {
+  /// Parse a single rule.
+  pub fn parse<'t>(
+    input: &mut Parser<'i, 't>,
+    options: &ParserOptions<'_, 'i>,
+  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    Self::parse_with(input, options, &mut DefaultAtRuleParser)
+  }
+
+  /// Parse a single rule from a string.
+  pub fn parse_string(
+    input: &'i str,
+    options: ParserOptions<'_, 'i>,
+  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    Self::parse_string_with(input, options, &mut DefaultAtRuleParser)
+  }
+}
+
+impl<'i, T> CssRule<'i, T> {
+  /// Parse a single rule.
+  pub fn parse_with<'t, P: AtRuleParser<'i, AtRule = T>>(
+    input: &mut Parser<'i, 't>,
+    options: &ParserOptions<'_, 'i>,
+    at_rule_parser: &mut P,
+  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let mut rules = CssRuleList(Vec::new());
+    parse_one_rule(input, &mut TopLevelRuleParser::new(options, at_rule_parser, &mut rules))?;
+    Ok(rules.0.pop().unwrap())
+  }
+
+  /// Parse a single rule from a string.
+  pub fn parse_string_with<P: AtRuleParser<'i, AtRule = T>>(
+    input: &'i str,
+    options: ParserOptions<'_, 'i>,
+    at_rule_parser: &mut P,
+  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let mut input = ParserInput::new(input);
+    let mut parser = Parser::new(&mut input);
+    Self::parse_with(&mut parser, &options, at_rule_parser)
+  }
+}
+
+/// A list of CSS rules.
+#[derive(Debug, PartialEq, Clone, Default)]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(transparent))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub struct CssRuleList<'i, R = DefaultAtRule>(
+  #[cfg_attr(feature = "serde", serde(borrow))] pub Vec<CssRule<'i, R>>,
+);
+
+impl<'i> CssRuleList<'i, DefaultAtRule> {
+  /// Parse a rule list.
+  pub fn parse<'t>(
+    input: &mut Parser<'i, 't>,
+    options: &ParserOptions<'_, 'i>,
+  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    Self::parse_with(input, options, &mut DefaultAtRuleParser)
+  }
+
+  /// Parse a style block, with both declarations and rules.
+  /// Resulting declarations are returned in a nested style rule.
+  pub fn parse_style_block<'t>(
+    input: &mut Parser<'i, 't>,
+    options: &ParserOptions<'_, 'i>,
+    is_nested: bool,
+  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    Self::parse_style_block_with(input, options, &mut DefaultAtRuleParser, is_nested)
+  }
+}
+
+impl<'i, T> CssRuleList<'i, T> {
+  /// Parse a rule list with a custom at rule parser.
+  pub fn parse_with<'t, P: AtRuleParser<'i, AtRule = T>>(
+    input: &mut Parser<'i, 't>,
+    options: &ParserOptions<'_, 'i>,
+    at_rule_parser: &mut P,
+  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    parse_rule_list(input, options, at_rule_parser)
+  }
+
+  /// Parse a style block, with both declarations and rules.
+  /// Resulting declarations are returned in a nested style rule.
+  pub fn parse_style_block_with<'t, P: AtRuleParser<'i, AtRule = T>>(
+    input: &mut Parser<'i, 't>,
+    options: &ParserOptions<'_, 'i>,
+    at_rule_parser: &mut P,
+    is_nested: bool,
+  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    parse_style_block(input, options, at_rule_parser, is_nested)
+  }
+}
+
+// Manually implemented to avoid circular child types.
+#[cfg(feature = "visitor")]
+#[cfg_attr(docsrs, doc(cfg(feature = "visitor")))]
+impl<'i, T: Visit<'i, T, V>, V: ?Sized + Visitor<'i, T>> Visit<'i, T, V> for CssRuleList<'i, T> {
+  const CHILD_TYPES: VisitTypes = VisitTypes::all();
+
+  fn visit(&mut self, visitor: &mut V) -> Result<(), V::Error> {
+    if visitor.visit_types().contains(VisitTypes::RULES) {
+      visitor.visit_rule_list(self)
+    } else {
+      self.0.visit(visitor)
+    }
+  }
+
+  fn visit_children(&mut self, visitor: &mut V) -> Result<(), V::Error> {
+    self.0.visit(visitor)
+  }
+}
+
+pub(crate) struct MinifyContext<'a, 'i> {
+  pub targets: TargetsWithSupportsScope,
+  pub handler: &'a mut DeclarationHandler<'i>,
+  pub important_handler: &'a mut DeclarationHandler<'i>,
+  pub handler_context: PropertyHandlerContext<'i, 'a>,
+  pub unused_symbols: &'a HashSet<String>,
+  pub custom_media: Option<HashMap<CowArcStr<'i>, CustomMediaRule<'i>>>,
+  pub css_modules: bool,
+  pub pure_css_modules: bool,
+}
+
+impl<'i, T: Clone> CssRuleList<'i, T> {
+  pub(crate) fn minify(
+    &mut self,
+    context: &mut MinifyContext<'_, 'i>,
+    parent_is_unused: bool,
+  ) -> Result<(), MinifyError> {
+    let mut keyframe_rules = HashMap::new();
+    let mut layer_rules = HashMap::new();
+    let mut has_layers = false;
+    let mut property_rules = HashMap::new();
+    let mut font_feature_values_rules = Vec::new();
+    let mut style_rules =
+      HashMap::with_capacity_and_hasher(self.0.len(), BuildHasherDefault::<PrecomputedHasher>::default());
+    let mut rules = Vec::new();
+    for mut rule in self.0.drain(..) {
+      match &mut rule {
+        CssRule::Keyframes(keyframes) => {
+          if context.unused_symbols.contains(match &keyframes.name {
+            KeyframesName::Ident(ident) => ident.0.as_ref(),
+            KeyframesName::Custom(string) => string.as_ref(),
+          }) {
+            continue;
+          }
+          keyframes.minify(context);
+
+          macro_rules! set_prefix {
+            ($keyframes: ident) => {
+              $keyframes.vendor_prefix =
+                context.targets.current.prefixes($keyframes.vendor_prefix, Feature::AtKeyframes);
+            };
+          }
+
+          // Merge @keyframes rules with the same name.
+          if let Some(existing_idx) = keyframe_rules.get(&keyframes.name) {
+            if let Some(CssRule::Keyframes(existing)) = &mut rules.get_mut(*existing_idx) {
+              // If the existing rule has the same vendor prefixes, replace it with this rule.
+              if existing.vendor_prefix == keyframes.vendor_prefix {
+                *existing = keyframes.clone();
+                continue;
+              }
+              // Otherwise, if the keyframes are identical, merge the prefixes.
+              if existing.keyframes == keyframes.keyframes {
+                existing.vendor_prefix |= keyframes.vendor_prefix;
+                set_prefix!(existing);
+                continue;
+              }
+            }
+          }
+
+          set_prefix!(keyframes);
+          keyframe_rules.insert(keyframes.name.clone(), rules.len());
+
+          let fallbacks = keyframes.get_fallbacks(&context.targets.current);
+          rules.push(rule);
+          rules.extend(fallbacks);
+          continue;
+        }
+        CssRule::CustomMedia(_) => {
+          if context.custom_media.is_some() {
+            continue;
+          }
+        }
+        CssRule::Media(media) => {
+          if let Some(CssRule::Media(last_rule)) = rules.last_mut() {
+            if last_rule.query == media.query {
+              last_rule.rules.0.extend(media.rules.0.drain(..));
+              last_rule.minify(context, parent_is_unused)?;
+              continue;
+            }
+          }
+
+          if media.minify(context, parent_is_unused)? {
+            continue;
+          }
+        }
+        CssRule::Supports(supports) => {
+          if let Some(CssRule::Supports(last_rule)) = rules.last_mut() {
+            if last_rule.condition == supports.condition {
+              last_rule.rules.0.extend(supports.rules.0.drain(..));
+              last_rule.minify(context, parent_is_unused)?;
+              continue;
+            }
+          }
+
+          supports.minify(context, parent_is_unused)?;
+          if supports.rules.0.is_empty() {
+            continue;
+          }
+        }
+        CssRule::Container(container) => {
+          if let Some(CssRule::Container(last_rule)) = rules.last_mut() {
+            if last_rule.name == container.name && last_rule.condition == container.condition {
+              last_rule.rules.0.extend(container.rules.0.drain(..));
+              last_rule.minify(context, parent_is_unused)?;
+              continue;
+            }
+          }
+
+          if container.minify(context, parent_is_unused)? {
+            continue;
+          }
+        }
+        CssRule::LayerBlock(layer) => {
+          // Merging non-adjacent layer rules is safe because they are applied
+          // in the order they are first defined.
+          if let Some(name) = &layer.name {
+            if let Some(idx) = layer_rules.get(name) {
+              if let Some(CssRule::LayerBlock(last_rule)) = rules.get_mut(*idx) {
+                last_rule.rules.0.extend(layer.rules.0.drain(..));
+                continue;
+              }
+            }
+
+            layer_rules.insert(name.clone(), rules.len());
+            has_layers = true;
+          }
+        }
+        CssRule::LayerStatement(layer) => {
+          // Create @layer block rules for each declared layer name,
+          // so we can merge other blocks into it later on.
+          for name in &layer.names {
+            if !layer_rules.contains_key(name) {
+              layer_rules.insert(name.clone(), rules.len());
+              has_layers = true;
+              rules.push(CssRule::LayerBlock(LayerBlockRule {
+                name: Some(name.clone()),
+                rules: CssRuleList(vec![]),
+                loc: layer.loc.clone(),
+              }));
+            }
+          }
+          continue;
+        }
+        CssRule::MozDocument(document) => document.minify(context)?,
+        CssRule::Style(style) => {
+          if parent_is_unused || style.minify(context, parent_is_unused)? {
+            continue;
+          }
+
+          // If some of the selectors in this rule are not compatible with the targets,
+          // we need to either wrap in :is() or split them into multiple rules.
+          let incompatible = if style.selectors.0.len() > 1
+            && context.targets.current.should_compile_selectors()
+            && !style.is_compatible(context.targets.current)
+          {
+            // The :is() selector accepts a forgiving selector list, so use that if possible.
+            // Note that :is() does not allow pseudo elements, so we need to check for that.
+            // In addition, :is() takes the highest specificity of its arguments, so if the selectors
+            // have different weights, we need to split them into separate rules as well.
+            if context.targets.current.is_compatible(crate::compat::Feature::IsSelector)
+              && !style.selectors.0.iter().any(|selector| selector.has_pseudo_element())
+              && style.selectors.0.iter().map(|selector| selector.specificity()).all_equal()
+            {
+              style.selectors =
+                SelectorList::new(smallvec![
+                  Component::Is(style.selectors.0.clone().into_boxed_slice()).into()
+                ]);
+              smallvec![]
+            } else {
+              // Otherwise, partition the selectors and keep the compatible ones in this rule.
+              // We will generate additional rules for incompatible selectors later.
+              let (compatible, incompatible) = style
+                .selectors
+                .0
+                .iter()
+                .cloned()
+                .partition::<SmallVec<[Selector; 1]>, _>(|selector| {
+                  let list = SelectorList::new(smallvec![selector.clone()]);
+                  is_compatible(&list.0, context.targets.current)
+                });
+              style.selectors = SelectorList::new(compatible);
+              incompatible
+            }
+          } else {
+            smallvec![]
+          };
+
+          style.update_prefix(context);
+
+          // Attempt to merge the new rule with the last rule we added.
+          let mut merged = false;
+          if let Some(CssRule::Style(last_style_rule)) = rules.last_mut() {
+            if merge_style_rules(style, last_style_rule, context) {
+              // If that was successful, then the last rule has been updated to include the
+              // selectors/declarations of the new rule. This might mean that we can merge it
+              // with the previous rule, so continue trying while we have style rules available.
+              while rules.len() >= 2 {
+                let len = rules.len();
+                let (a, b) = rules.split_at_mut(len - 1);
+                if let (CssRule::Style(last), CssRule::Style(prev)) = (&mut b[0], &mut a[len - 2]) {
+                  if merge_style_rules(last, prev, context) {
+                    // If we were able to merge the last rule into the previous one, remove the last.
+                    rules.pop();
+                    continue;
+                  }
+                }
+                // If we didn't see a style rule, or were unable to merge, stop.
+                break;
+              }
+              merged = true;
+            }
+          }
+
+          // Create additional rules for logical properties, @supports overrides, and incompatible selectors.
+          let supports = context.handler_context.get_supports_rules(&style);
+          let logical = context.handler_context.get_additional_rules(&style);
+
+          let incompatible_rules = incompatible
+            .into_iter()
+            .map(|selector| {
+              // Create a clone of the rule with only the one incompatible selector.
+              let list = SelectorList::new(smallvec![selector]);
+              let mut clone = style.clone();
+              clone.selectors = list;
+              clone.update_prefix(context);
+
+              // Also add rules for logical properties and @supports overrides.
+              let supports = context.handler_context.get_supports_rules(&clone);
+              let logical = context.handler_context.get_additional_rules(&clone);
+              (clone, logical, supports)
+            })
+            .collect::<Vec<_>>();
+
+          context.handler_context.reset();
+
+          // If the rule has nested rules, and we have extra rules to insert such as for logical properties,
+          // we need to split the rule in two so we can insert the extra rules in between the declarations from
+          // the main rule and the nested rules.
+          let nested_rule = if !style.rules.0.is_empty()
+            // can happen if there are no compatible rules, above.
+            && !style.selectors.0.is_empty()
+            && (!logical.is_empty() || !supports.is_empty() || !incompatible_rules.is_empty())
+          {
+            let mut rules = CssRuleList(vec![]);
+            std::mem::swap(&mut style.rules, &mut rules);
+            Some(StyleRule {
+              selectors: style.selectors.clone(),
+              declarations: DeclarationBlock::default(),
+              rules,
+              vendor_prefix: style.vendor_prefix,
+              loc: style.loc,
+            })
+          } else {
+            None
+          };
+
+          if !merged && !style.is_empty() {
+            let source_index = style.loc.source_index;
+            let has_no_rules = style.rules.0.is_empty();
+            let idx = rules.len();
+            rules.push(rule);
+
+            // Check if this rule is a duplicate of an earlier rule, meaning it has
+            // the same selectors and defines the same properties. If so, remove the
+            // earlier rule because this one completely overrides it.
+            if has_no_rules {
+              // SAFETY: StyleRuleKeys never live beyond this method.
+              let key = StyleRuleKey::new(unsafe { &*(&rules as *const _) }, idx);
+              if idx > 0 {
+                if let Some(i) = style_rules.remove(&key) {
+                  if let CssRule::Style(other) = &rules[i] {
+                    // Don't remove the rule if this is a CSS module and the other rule came from a different file.
+                    if !context.css_modules || source_index == other.loc.source_index {
+                      // Only mark the rule as ignored so we don't need to change all of the indices.
+                      rules[i] = CssRule::Ignored;
+                    }
+                  }
+                }
+              }
+
+              style_rules.insert(key, idx);
+            }
+          }
+
+          if !logical.is_empty() {
+            let mut logical = CssRuleList(logical);
+            logical.minify(context, parent_is_unused)?;
+            rules.extend(logical.0)
+          }
+
+          rules.extend(supports);
+          for (rule, logical, supports) in incompatible_rules {
+            if !rule.is_empty() {
+              rules.push(CssRule::Style(rule));
+            }
+            if !logical.is_empty() {
+              let mut logical = CssRuleList(logical);
+              logical.minify(context, parent_is_unused)?;
+              rules.extend(logical.0)
+            }
+            rules.extend(supports);
+          }
+
+          if let Some(nested_rule) = nested_rule {
+            rules.push(CssRule::Style(nested_rule));
+          }
+
+          continue;
+        }
+        CssRule::CounterStyle(counter_style) => {
+          if context.unused_symbols.contains(counter_style.name.0.as_ref()) {
+            continue;
+          }
+        }
+        CssRule::Scope(scope) => scope.minify(context)?,
+        CssRule::Nesting(nesting) => {
+          if nesting.minify(context, parent_is_unused)? {
+            continue;
+          }
+        }
+        CssRule::NestedDeclarations(nested) => {
+          if nested.minify(context, parent_is_unused) {
+            continue;
+          }
+        }
+        CssRule::StartingStyle(rule) => {
+          if rule.minify(context, parent_is_unused)? {
+            continue;
+          }
+        }
+        CssRule::FontPaletteValues(f) => {
+          if context.unused_symbols.contains(f.name.0.as_ref()) {
+            continue;
+          }
+
+          f.minify(context, parent_is_unused);
+
+          let fallbacks = f.get_fallbacks(context.targets.current);
+          rules.push(rule);
+          rules.extend(fallbacks);
+          continue;
+        }
+        CssRule::FontFeatureValues(rule) => {
+          if let Some(index) = font_feature_values_rules
+            .iter()
+            .find(|index| matches!(&rules[**index], CssRule::FontFeatureValues(r) if r.name == rule.name))
+          {
+            if let CssRule::FontFeatureValues(existing) = &mut rules[*index] {
+              existing.merge(rule);
+            }
+            continue;
+          } else {
+            font_feature_values_rules.push(rules.len());
+          }
+        }
+        CssRule::Property(property) => {
+          if context.unused_symbols.contains(property.name.0.as_ref()) {
+            continue;
+          }
+
+          if let Some(index) = property_rules.get(&property.name) {
+            rules[*index] = rule;
+            continue;
+          } else {
+            property_rules.insert(property.name.clone(), rules.len());
+          }
+        }
+        CssRule::Import(_) => {
+          // @layer blocks can't be inlined into layers declared before imports.
+          layer_rules.clear();
+        }
+        _ => {}
+      }
+
+      rules.push(rule)
+    }
+
+    // Optimize @layer rules. Combine subsequent empty layer blocks into a single @layer statement
+    // so that layers are declared in the correct order.
+    if has_layers {
+      let mut declared_layers = HashSet::new();
+      let mut layer_statement = None;
+      for index in 0..rules.len() {
+        match &mut rules[index] {
+          CssRule::LayerBlock(layer) => {
+            if layer.minify(context, parent_is_unused)? {
+              if let Some(name) = &layer.name {
+                if declared_layers.contains(name) {
+                  // Remove empty layer that has already been declared.
+                  rules[index] = CssRule::Ignored;
+                  continue;
+                }
+
+                let name = name.clone();
+                declared_layers.insert(name.clone());
+
+                if let Some(layer_index) = layer_statement {
+                  if let CssRule::LayerStatement(layer) = &mut rules[layer_index] {
+                    // Add name to previous layer statement rule and remove this one.
+                    layer.names.push(name);
+                    rules[index] = CssRule::Ignored;
+                  }
+                } else {
+                  // Create a new layer statement rule to declare the name.
+                  rules[index] = CssRule::LayerStatement(LayerStatementRule {
+                    names: vec![name],
+                    loc: layer.loc,
+                  });
+                  layer_statement = Some(index);
+                }
+              } else {
+                // Remove empty anonymous layer.
+                rules[index] = CssRule::Ignored;
+              }
+            } else {
+              // Non-empty @layer block. Start a new statement.
+              layer_statement = None;
+            }
+          }
+          CssRule::Import(import) => {
+            if let Some(layer) = &import.layer {
+              // Start a new @layer statement so the import layer is in the right order.
+              layer_statement = None;
+              if let Some(name) = layer {
+                declared_layers.insert(name.clone());
+              }
+            }
+          }
+          _ => {}
+        }
+      }
+    }
+
+    self.0 = rules;
+    Ok(())
+  }
+}
+
+fn merge_style_rules<'i, T>(
+  style: &mut StyleRule<'i, T>,
+  last_style_rule: &mut StyleRule<'i, T>,
+  context: &mut MinifyContext<'_, 'i>,
+) -> bool {
+  // Merge declarations if the selectors are equivalent, and both are compatible with all targets.
+  if style.selectors == last_style_rule.selectors
+    && style.is_compatible(context.targets.current)
+    && last_style_rule.is_compatible(context.targets.current)
+    && style.rules.0.is_empty()
+    && last_style_rule.rules.0.is_empty()
+    && (!context.css_modules || style.loc.source_index == last_style_rule.loc.source_index)
+  {
+    last_style_rule
+      .declarations
+      .declarations
+      .extend(style.declarations.declarations.drain(..));
+    last_style_rule
+      .declarations
+      .important_declarations
+      .extend(style.declarations.important_declarations.drain(..));
+    last_style_rule
+      .declarations
+      .minify(context.handler, context.important_handler, &mut context.handler_context);
+    return true;
+  } else if style.declarations == last_style_rule.declarations
+    && style.rules.0.is_empty()
+    && last_style_rule.rules.0.is_empty()
+  {
+    // If both selectors are potentially vendor prefixable, and they are
+    // equivalent minus prefixes, add the prefix to the last rule.
+    if !style.vendor_prefix.is_empty()
+      && !last_style_rule.vendor_prefix.is_empty()
+      && is_equivalent(&style.selectors.0, &last_style_rule.selectors.0)
+    {
+      // If the new rule is unprefixed, replace the prefixes of the last rule.
+      // Otherwise, add the new prefix.
+      if style.vendor_prefix.contains(VendorPrefix::None) && context.targets.current.should_compile_selectors() {
+        last_style_rule.vendor_prefix = style.vendor_prefix;
+      } else {
+        last_style_rule.vendor_prefix |= style.vendor_prefix;
+      }
+      return true;
+    }
+
+    // Append the selectors to the last rule if the declarations are the same, and all selectors are compatible.
+    if style.is_compatible(context.targets.current) && last_style_rule.is_compatible(context.targets.current) {
+      last_style_rule.selectors.0.extend(style.selectors.0.drain(..));
+      if style.vendor_prefix.contains(VendorPrefix::None) && context.targets.current.should_compile_selectors() {
+        last_style_rule.vendor_prefix = style.vendor_prefix;
+      } else {
+        last_style_rule.vendor_prefix |= style.vendor_prefix;
+      }
+      return true;
+    }
+  }
+  false
+}
+
+impl<'a, 'i, T: ToCss> ToCss for CssRuleList<'i, T> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    let mut first = true;
+    let mut last_without_block = false;
+
+    for rule in &self.0 {
+      if let CssRule::Ignored = &rule {
+        continue;
+      }
+
+      // Skip @import rules if collecting dependencies.
+      if let CssRule::Import(rule) = &rule {
+        if dest.remove_imports {
+          let dep = if dest.dependencies.is_some() {
+            Some(Dependency::Import(ImportDependency::new(&rule, dest.filename())))
+          } else {
+            None
+          };
+
+          if let Some(dependencies) = &mut dest.dependencies {
+            dependencies.push(dep.unwrap());
+            continue;
+          }
+        }
+      }
+
+      if first {
+        first = false;
+      } else {
+        if !dest.minify
+          && !(last_without_block
+            && matches!(
+              rule,
+              CssRule::Import(..) | CssRule::Namespace(..) | CssRule::LayerStatement(..)
+            ))
+        {
+          dest.write_char('\n')?;
+        }
+        dest.newline()?;
+      }
+      rule.to_css(dest)?;
+      last_without_block = matches!(
+        rule,
+        CssRule::Import(..) | CssRule::Namespace(..) | CssRule::LayerStatement(..)
+      );
+    }
+
+    Ok(())
+  }
+}
+
+impl<'i, T> std::ops::Index<usize> for CssRuleList<'i, T> {
+  type Output = CssRule<'i, T>;
+
+  fn index(&self, index: usize) -> &Self::Output {
+    &self.0[index]
+  }
+}
+
+impl<'i, T> std::ops::IndexMut<usize> for CssRuleList<'i, T> {
+  fn index_mut(&mut self, index: usize) -> &mut Self::Output {
+    &mut self.0[index]
+  }
+}
+
+/// A hasher that expects to be called with a single u64, which is already a hash.
+#[derive(Default)]
+struct PrecomputedHasher {
+  hash: Option<u64>,
+}
+
+impl Hasher for PrecomputedHasher {
+  #[inline]
+  fn write(&mut self, _: &[u8]) {
+    unreachable!()
+  }
+
+  #[inline]
+  fn write_u64(&mut self, i: u64) {
+    debug_assert!(self.hash.is_none());
+    self.hash = Some(i);
+  }
+
+  #[inline]
+  fn finish(&self) -> u64 {
+    self.hash.unwrap()
+  }
+}
+
+/// A key to a StyleRule meant for use in a HashMap for quickly detecting duplicates.
+/// It stores a reference to a list and an index so it can access items without cloning
+/// even when the list is reallocated. A hash is also pre-computed for fast lookups.
+#[derive(Clone)]
+pub(crate) struct StyleRuleKey<'a, 'i, R> {
+  list: &'a Vec<CssRule<'i, R>>,
+  index: usize,
+  hash: u64,
+}
+
+impl<'a, 'i, R> StyleRuleKey<'a, 'i, R> {
+  fn new(list: &'a Vec<CssRule<'i, R>>, index: usize) -> Self {
+    let rule = match &list[index] {
+      CssRule::Style(style) => style,
+      _ => unreachable!(),
+    };
+
+    Self {
+      list,
+      index,
+      hash: rule.hash_key(),
+    }
+  }
+}
+
+impl<'a, 'i, R> PartialEq for StyleRuleKey<'a, 'i, R> {
+  fn eq(&self, other: &Self) -> bool {
+    let rule = match self.list.get(self.index) {
+      Some(CssRule::Style(style)) => style,
+      _ => return false,
+    };
+
+    let other_rule = match other.list.get(other.index) {
+      Some(CssRule::Style(style)) => style,
+      _ => return false,
+    };
+
+    rule.is_duplicate(other_rule)
+  }
+}
+
+impl<'a, 'i, R> Eq for StyleRuleKey<'a, 'i, R> {}
+
+impl<'a, 'i, R> std::hash::Hash for StyleRuleKey<'a, 'i, R> {
+  fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+    state.write_u64(self.hash);
+  }
+}
diff --git a/src/rules/namespace.rs b/src/rules/namespace.rs
new file mode 100644
index 0000000..c6c66a5
--- /dev/null
+++ b/src/rules/namespace.rs
@@ -0,0 +1,48 @@
+//! The `@namespace` rule.
+
+use super::Location;
+use crate::error::PrinterError;
+use crate::printer::Printer;
+use crate::traits::ToCss;
+use crate::values::ident::Ident;
+use crate::values::string::CSSString;
+#[cfg(feature = "visitor")]
+use crate::visitor::Visit;
+
+/// A [@namespace](https://drafts.csswg.org/css-namespaces/#declaration) rule.
+#[derive(Debug, PartialEq, Clone)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub struct NamespaceRule<'i> {
+  /// An optional namespace prefix to declare, or `None` to declare the default namespace.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  #[cfg_attr(feature = "visitor", skip_visit)]
+  pub prefix: Option<Ident<'i>>,
+  /// The url of the namespace.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  #[cfg_attr(feature = "visitor", skip_visit)]
+  pub url: CSSString<'i>,
+  /// The location of the rule in the source file.
+  #[cfg_attr(feature = "visitor", skip_visit)]
+  pub loc: Location,
+}
+
+impl<'i> ToCss for NamespaceRule<'i> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    #[cfg(feature = "sourcemap")]
+    dest.add_mapping(self.loc);
+    dest.write_str("@namespace ")?;
+    if let Some(prefix) = &self.prefix {
+      prefix.to_css(dest)?;
+      dest.write_char(' ')?;
+    }
+
+    self.url.to_css(dest)?;
+    dest.write_char(';')
+  }
+}
diff --git a/src/rules/nesting.rs b/src/rules/nesting.rs
new file mode 100644
index 0000000..679e586
--- /dev/null
+++ b/src/rules/nesting.rs
@@ -0,0 +1,123 @@
+//! The `@nest` rule.
+
+use smallvec::SmallVec;
+
+use super::style::StyleRule;
+use super::Location;
+use super::MinifyContext;
+use crate::context::DeclarationContext;
+use crate::declaration::DeclarationBlock;
+use crate::error::{MinifyError, PrinterError};
+use crate::parser::DefaultAtRule;
+use crate::printer::Printer;
+use crate::targets::should_compile;
+use crate::traits::ToCss;
+#[cfg(feature = "visitor")]
+use crate::visitor::Visit;
+
+/// A [@nest](https://www.w3.org/TR/css-nesting-1/#at-nest) rule.
+#[derive(Debug, PartialEq, Clone)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub struct NestingRule<'i, R = DefaultAtRule> {
+  /// The style rule that defines the selector and declarations for the `@nest` rule.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  pub style: StyleRule<'i, R>,
+  /// The location of the rule in the source file.
+  #[cfg_attr(feature = "visitor", skip_visit)]
+  pub loc: Location,
+}
+
+impl<'i, T: Clone> NestingRule<'i, T> {
+  pub(crate) fn minify(
+    &mut self,
+    context: &mut MinifyContext<'_, 'i>,
+    parent_is_unused: bool,
+  ) -> Result<bool, MinifyError> {
+    self.style.minify(context, parent_is_unused)
+  }
+}
+
+impl<'a, 'i, T: ToCss> ToCss for NestingRule<'i, T> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    #[cfg(feature = "sourcemap")]
+    dest.add_mapping(self.loc);
+    if dest.context().is_none() {
+      dest.write_str("@nest ")?;
+    }
+    self.style.to_css(dest)
+  }
+}
+
+/// A [nested declarations](https://drafts.csswg.org/css-nesting/#nested-declarations-rule) rule.
+#[derive(Debug, PartialEq, Clone)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub struct NestedDeclarationsRule<'i> {
+  /// The style rule that defines the selector and declarations for the `@nest` rule.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  pub declarations: DeclarationBlock<'i>,
+  /// The location of the rule in the source file.
+  #[cfg_attr(feature = "visitor", skip_visit)]
+  pub loc: Location,
+}
+
+impl<'i> NestedDeclarationsRule<'i> {
+  pub(crate) fn minify(&mut self, context: &mut MinifyContext<'_, 'i>, parent_is_unused: bool) -> bool {
+    if parent_is_unused {
+      return true;
+    }
+
+    context.handler_context.context = DeclarationContext::StyleRule;
+    self
+      .declarations
+      .minify(context.handler, context.important_handler, &mut context.handler_context);
+    context.handler_context.context = DeclarationContext::None;
+    return false;
+  }
+}
+
+impl<'i> ToCss for NestedDeclarationsRule<'i> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    #[cfg(feature = "sourcemap")]
+    dest.add_mapping(self.loc);
+
+    if should_compile!(dest.targets.current, Nesting) {
+      if let Some(context) = dest.context() {
+        let has_printable_declarations = self.declarations.has_printable_declarations();
+        if has_printable_declarations {
+          dest.with_parent_context(|dest| context.selectors.to_css(dest))?;
+          dest.whitespace()?;
+          dest.write_char('{')?;
+          dest.indent();
+          dest.newline()?;
+        }
+
+        self
+          .declarations
+          .to_css_declarations(dest, false, &context.selectors, self.loc.source_index)?;
+
+        if has_printable_declarations {
+          dest.dedent();
+          dest.newline()?;
+          dest.write_char('}')?;
+        }
+        return Ok(());
+      }
+    }
+
+    self
+      .declarations
+      .to_css_declarations(dest, false, &parcel_selectors::SelectorList(SmallVec::new()), 0)
+  }
+}
diff --git a/src/rules/page.rs b/src/rules/page.rs
new file mode 100644
index 0000000..d95c94c
--- /dev/null
+++ b/src/rules/page.rs
@@ -0,0 +1,377 @@
+//! The `@page` rule.
+
+use super::Location;
+use crate::declaration::{parse_declaration, DeclarationBlock};
+use crate::error::{ParserError, PrinterError};
+use crate::macros::enum_property;
+use crate::printer::Printer;
+use crate::stylesheet::ParserOptions;
+use crate::traits::{Parse, ToCss};
+use crate::values::string::CowArcStr;
+#[cfg(feature = "visitor")]
+use crate::visitor::Visit;
+use cssparser::*;
+
+/// A [page selector](https://www.w3.org/TR/css-page-3/#typedef-page-selector)
+/// within a `@page` rule.
+///
+/// Either a name or at least one pseudo class is required.
+#[derive(Debug, PartialEq, Clone)]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(rename_all = "camelCase")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub struct PageSelector<'i> {
+  /// An optional named page type.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  pub name: Option<CowArcStr<'i>>,
+  /// A list of page pseudo classes.
+  pub pseudo_classes: Vec<PagePseudoClass>,
+}
+
+enum_property! {
+  /// A page pseudo class within an `@page` selector.
+  ///
+  /// See [PageSelector](PageSelector).
+  pub enum PagePseudoClass {
+    /// The `:left` pseudo class.
+    Left,
+    /// The `:right` pseudo class.
+    Right,
+    /// The `:first` pseudo class.
+    First,
+    /// The `:last` pseudo class.
+    Last,
+    /// The `:blank` pseudo class.
+    Blank,
+  }
+}
+
+impl<'i> Parse<'i> for PageSelector<'i> {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let name = input.try_parse(|input| input.expect_ident().map(|x| x.into())).ok();
+    let mut pseudo_classes = vec![];
+
+    loop {
+      // Whitespace is not allowed between pseudo classes
+      let state = input.state();
+      match input.next_including_whitespace() {
+        Ok(Token::Colon) => {
+          pseudo_classes.push(PagePseudoClass::parse(input)?);
+        }
+        _ => {
+          input.reset(&state);
+          break;
+        }
+      }
+    }
+
+    if name.is_none() && pseudo_classes.is_empty() {
+      return Err(input.new_custom_error(ParserError::InvalidPageSelector));
+    }
+
+    Ok(PageSelector { name, pseudo_classes })
+  }
+}
+
+enum_property! {
+  /// A [page margin box](https://www.w3.org/TR/css-page-3/#margin-boxes).
+  pub enum PageMarginBox {
+    /// A fixed-size box defined by the intersection of the top and left margins of the page box.
+    TopLeftCorner,
+    /// A variable-width box filling the top page margin between the top-left-corner and top-center page-margin boxes.
+    TopLeft,
+    /// A variable-width box centered horizontally between the page’s left and right border edges and filling the
+    /// page top margin between the top-left and top-right page-margin boxes.
+    TopCenter,
+    /// A variable-width box filling the top page margin between the top-center and top-right-corner page-margin boxes.
+    TopRight,
+    /// A fixed-size box defined by the intersection of the top and right margins of the page box.
+    TopRightCorner,
+    /// A variable-height box filling the left page margin between the top-left-corner and left-middle page-margin boxes.
+    LeftTop,
+    /// A variable-height box centered vertically between the page’s top and bottom border edges and filling the
+    /// left page margin between the left-top and left-bottom page-margin boxes.
+    LeftMiddle,
+    /// A variable-height box filling the left page margin between the left-middle and bottom-left-corner page-margin boxes.
+    LeftBottom,
+    /// A variable-height box filling the right page margin between the top-right-corner and right-middle page-margin boxes.
+    RightTop,
+    /// A variable-height box centered vertically between the page’s top and bottom border edges and filling the right
+    /// page margin between the right-top and right-bottom page-margin boxes.
+    RightMiddle,
+    /// A variable-height box filling the right page margin between the right-middle and bottom-right-corner page-margin boxes.
+    RightBottom,
+    /// A fixed-size box defined by the intersection of the bottom and left margins of the page box.
+    BottomLeftCorner,
+    /// A variable-width box filling the bottom page margin between the bottom-left-corner and bottom-center page-margin boxes.
+    BottomLeft,
+    /// A variable-width box centered horizontally between the page’s left and right border edges and filling the bottom
+    /// page margin between the bottom-left and bottom-right page-margin boxes.
+    BottomCenter,
+    /// A variable-width box filling the bottom page margin between the bottom-center and bottom-right-corner page-margin boxes.
+    BottomRight,
+    /// A fixed-size box defined by the intersection of the bottom and right margins of the page box.
+    BottomRightCorner,
+  }
+}
+
+/// A [page margin rule](https://www.w3.org/TR/css-page-3/#margin-at-rules) rule.
+#[derive(Debug, PartialEq, Clone)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(rename_all = "camelCase")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub struct PageMarginRule<'i> {
+  /// The margin box identifier for this rule.
+  pub margin_box: PageMarginBox,
+  /// The declarations within the rule.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  pub declarations: DeclarationBlock<'i>,
+  /// The location of the rule in the source file.
+  #[cfg_attr(feature = "visitor", skip_visit)]
+  pub loc: Location,
+}
+
+impl<'i> ToCss for PageMarginRule<'i> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    #[cfg(feature = "sourcemap")]
+    dest.add_mapping(self.loc);
+    dest.write_char('@')?;
+    self.margin_box.to_css(dest)?;
+    self.declarations.to_css_block(dest)
+  }
+}
+
+/// A [@page](https://www.w3.org/TR/css-page-3/#at-page-rule) rule.
+#[derive(Debug, PartialEq, Clone)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub struct PageRule<'i> {
+  /// A list of page selectors.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  #[cfg_attr(feature = "visitor", skip_visit)]
+  pub selectors: Vec<PageSelector<'i>>,
+  /// The declarations within the `@page` rule.
+  pub declarations: DeclarationBlock<'i>,
+  /// The nested margin rules.
+  pub rules: Vec<PageMarginRule<'i>>,
+  /// The location of the rule in the source file.
+  #[cfg_attr(feature = "visitor", skip_visit)]
+  pub loc: Location,
+}
+
+impl<'i> PageRule<'i> {
+  pub(crate) fn parse<'t, 'o>(
+    selectors: Vec<PageSelector<'i>>,
+    input: &mut Parser<'i, 't>,
+    loc: Location,
+    options: &ParserOptions<'o, 'i>,
+  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let mut declarations = DeclarationBlock::new();
+    let mut rules = Vec::new();
+    let mut rule_parser = PageRuleParser {
+      declarations: &mut declarations,
+      rules: &mut rules,
+      options: &options,
+    };
+    let mut parser = RuleBodyParser::new(input, &mut rule_parser);
+
+    while let Some(decl) = parser.next() {
+      if let Err((err, _)) = decl {
+        if parser.parser.options.error_recovery {
+          parser.parser.options.warn(err);
+          continue;
+        }
+        return Err(err);
+      }
+    }
+
+    Ok(PageRule {
+      selectors,
+      declarations,
+      rules,
+      loc,
+    })
+  }
+}
+
+impl<'i> ToCss for PageRule<'i> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    #[cfg(feature = "sourcemap")]
+    dest.add_mapping(self.loc);
+    dest.write_str("@page")?;
+    if let Some(first) = self.selectors.first() {
+      // Space is only required if the first selector has a name.
+      if !dest.minify || first.name.is_some() {
+        dest.write_char(' ')?;
+      }
+      let mut first = true;
+      for selector in &self.selectors {
+        if first {
+          first = false;
+        } else {
+          dest.delim(',', false)?;
+        }
+        selector.to_css(dest)?;
+      }
+    }
+
+    dest.whitespace()?;
+    dest.write_char('{')?;
+    dest.indent();
+
+    let mut i = 0;
+    let len = self.declarations.len() + self.rules.len();
+
+    macro_rules! write {
+      ($decls: expr, $important: literal) => {
+        for decl in &$decls {
+          dest.newline()?;
+          decl.to_css(dest, $important)?;
+          if i != len - 1 || !dest.minify {
+            dest.write_char(';')?;
+          }
+          i += 1;
+        }
+      };
+    }
+
+    write!(self.declarations.declarations, false);
+    write!(self.declarations.important_declarations, true);
+
+    if !self.rules.is_empty() {
+      if !dest.minify && self.declarations.len() > 0 {
+        dest.write_char('\n')?;
+      }
+      dest.newline()?;
+
+      let mut first = true;
+      for rule in &self.rules {
+        if first {
+          first = false;
+        } else {
+          if !dest.minify {
+            dest.write_char('\n')?;
+          }
+          dest.newline()?;
+        }
+        rule.to_css(dest)?;
+      }
+    }
+
+    dest.dedent();
+    dest.newline()?;
+    dest.write_char('}')
+  }
+}
+
+impl<'i> ToCss for PageSelector<'i> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    if let Some(name) = &self.name {
+      dest.write_str(&name)?;
+    }
+
+    for pseudo in &self.pseudo_classes {
+      dest.write_char(':')?;
+      pseudo.to_css(dest)?;
+    }
+
+    Ok(())
+  }
+}
+
+struct PageRuleParser<'a, 'o, 'i> {
+  declarations: &'a mut DeclarationBlock<'i>,
+  rules: &'a mut Vec<PageMarginRule<'i>>,
+  options: &'a ParserOptions<'o, 'i>,
+}
+
+impl<'a, 'o, 'i> cssparser::DeclarationParser<'i> for PageRuleParser<'a, 'o, 'i> {
+  type Declaration = ();
+  type Error = ParserError<'i>;
+
+  fn parse_value<'t>(
+    &mut self,
+    name: CowRcStr<'i>,
+    input: &mut cssparser::Parser<'i, 't>,
+  ) -> Result<Self::Declaration, cssparser::ParseError<'i, Self::Error>> {
+    parse_declaration(
+      name,
+      input,
+      &mut self.declarations.declarations,
+      &mut self.declarations.important_declarations,
+      &self.options,
+    )
+  }
+}
+
+impl<'a, 'o, 'i> AtRuleParser<'i> for PageRuleParser<'a, 'o, 'i> {
+  type Prelude = PageMarginBox;
+  type AtRule = ();
+  type Error = ParserError<'i>;
+
+  fn parse_prelude<'t>(
+    &mut self,
+    name: CowRcStr<'i>,
+    input: &mut Parser<'i, 't>,
+  ) -> Result<Self::Prelude, ParseError<'i, Self::Error>> {
+    let loc = input.current_source_location();
+    PageMarginBox::parse_string(&name)
+      .map_err(|_| loc.new_custom_error(ParserError::AtRuleInvalid(name.clone().into())))
+  }
+
+  fn parse_block<'t>(
+    &mut self,
+    prelude: Self::Prelude,
+    start: &ParserState,
+    input: &mut Parser<'i, 't>,
+  ) -> Result<Self::AtRule, ParseError<'i, Self::Error>> {
+    let loc = start.source_location();
+    let declarations = DeclarationBlock::parse(input, self.options)?;
+    self.rules.push(PageMarginRule {
+      margin_box: prelude,
+      declarations,
+      loc: Location {
+        source_index: self.options.source_index,
+        line: loc.line,
+        column: loc.column,
+      },
+    });
+    Ok(())
+  }
+}
+
+impl<'a, 'o, 'i> QualifiedRuleParser<'i> for PageRuleParser<'a, 'o, 'i> {
+  type Prelude = ();
+  type QualifiedRule = ();
+  type Error = ParserError<'i>;
+}
+
+impl<'a, 'o, 'i> RuleBodyItemParser<'i, (), ParserError<'i>> for PageRuleParser<'a, 'o, 'i> {
+  fn parse_qualified(&self) -> bool {
+    false
+  }
+
+  fn parse_declarations(&self) -> bool {
+    true
+  }
+}
diff --git a/src/rules/property.rs b/src/rules/property.rs
new file mode 100644
index 0000000..f775a69
--- /dev/null
+++ b/src/rules/property.rs
@@ -0,0 +1,225 @@
+//! The `@property` rule.
+
+use super::Location;
+#[cfg(feature = "visitor")]
+use crate::visitor::Visit;
+use crate::{
+  error::{ParserError, PrinterError},
+  printer::Printer,
+  properties::custom::TokenList,
+  traits::{Parse, ToCss},
+  values::{
+    ident::DashedIdent,
+    syntax::{ParsedComponent, SyntaxString},
+  },
+};
+use cssparser::*;
+
+/// A [@property](https://drafts.css-houdini.org/css-properties-values-api/#at-property-rule) rule.
+#[derive(Debug, PartialEq, Clone)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(rename_all = "camelCase")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub struct PropertyRule<'i> {
+  /// The name of the custom property to declare.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  pub name: DashedIdent<'i>,
+  /// A syntax string to specify the grammar for the custom property.
+  #[cfg_attr(feature = "visitor", skip_visit)]
+  pub syntax: SyntaxString,
+  /// Whether the custom property is inherited.
+  #[cfg_attr(feature = "visitor", skip_visit)]
+  pub inherits: bool,
+  /// An optional initial value for the custom property.
+  #[cfg_attr(feature = "visitor", skip_visit)]
+  pub initial_value: Option<ParsedComponent<'i>>,
+  /// The location of the rule in the source file.
+  #[cfg_attr(feature = "visitor", skip_visit)]
+  pub loc: Location,
+}
+
+impl<'i> PropertyRule<'i> {
+  pub(crate) fn parse<'t>(
+    name: DashedIdent<'i>,
+    input: &mut Parser<'i, 't>,
+    loc: Location,
+  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let mut parser = PropertyRuleDeclarationParser {
+      syntax: None,
+      inherits: None,
+      initial_value: None,
+    };
+
+    let mut decl_parser = RuleBodyParser::new(input, &mut parser);
+    while let Some(decl) = decl_parser.next() {
+      match decl {
+        Ok(()) => {}
+        Err((e, _)) => return Err(e),
+      }
+    }
+
+    // `syntax` and `inherits` are always required.
+    let parser = decl_parser.parser;
+    let syntax = parser
+      .syntax
+      .clone()
+      .ok_or(decl_parser.input.new_custom_error(ParserError::AtRuleBodyInvalid))?;
+    let inherits = parser
+      .inherits
+      .clone()
+      .ok_or(decl_parser.input.new_custom_error(ParserError::AtRuleBodyInvalid))?;
+
+    // `initial-value` is required unless the syntax is a universal definition.
+    let initial_value = match syntax {
+      SyntaxString::Universal => match parser.initial_value {
+        None => None,
+        Some(val) => {
+          let mut input = ParserInput::new(val);
+          let mut parser = Parser::new(&mut input);
+
+          if parser.is_exhausted() {
+            Some(ParsedComponent::TokenList(TokenList(vec![])))
+          } else {
+            Some(syntax.parse_value(&mut parser)?)
+          }
+        }
+      },
+      _ => {
+        let val = parser
+          .initial_value
+          .ok_or(input.new_custom_error(ParserError::AtRuleBodyInvalid))?;
+        let mut input = ParserInput::new(val);
+        let mut parser = Parser::new(&mut input);
+        Some(syntax.parse_value(&mut parser)?)
+      }
+    };
+
+    return Ok(PropertyRule {
+      name,
+      syntax,
+      inherits,
+      initial_value,
+      loc,
+    });
+  }
+}
+
+impl<'i> ToCss for PropertyRule<'i> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    #[cfg(feature = "sourcemap")]
+    dest.add_mapping(self.loc);
+    dest.write_str("@property ")?;
+    self.name.to_css(dest)?;
+    dest.whitespace()?;
+    dest.write_char('{')?;
+    dest.indent();
+    dest.newline()?;
+
+    dest.write_str("syntax:")?;
+    dest.whitespace()?;
+    self.syntax.to_css(dest)?;
+    dest.write_char(';')?;
+    dest.newline()?;
+
+    dest.write_str("inherits:")?;
+    dest.whitespace()?;
+    match self.inherits {
+      true => dest.write_str("true")?,
+      false => dest.write_str("false")?,
+    }
+
+    if let Some(initial_value) = &self.initial_value {
+      dest.write_char(';')?;
+      dest.newline()?;
+
+      dest.write_str("initial-value:")?;
+      dest.whitespace()?;
+      initial_value.to_css(dest)?;
+
+      if !dest.minify {
+        dest.write_char(';')?;
+      }
+    }
+
+    dest.dedent();
+    dest.newline()?;
+    dest.write_char('}')
+  }
+}
+
+pub(crate) struct PropertyRuleDeclarationParser<'i> {
+  syntax: Option<SyntaxString>,
+  inherits: Option<bool>,
+  initial_value: Option<&'i str>,
+}
+
+impl<'i> cssparser::DeclarationParser<'i> for PropertyRuleDeclarationParser<'i> {
+  type Declaration = ();
+  type Error = ParserError<'i>;
+
+  fn parse_value<'t>(
+    &mut self,
+    name: CowRcStr<'i>,
+    input: &mut cssparser::Parser<'i, 't>,
+  ) -> Result<Self::Declaration, cssparser::ParseError<'i, Self::Error>> {
+    match_ignore_ascii_case! { &name,
+      "syntax" => {
+        let syntax = SyntaxString::parse(input)?;
+        self.syntax = Some(syntax);
+      },
+      "inherits" => {
+        let location = input.current_source_location();
+        let ident = input.expect_ident()?;
+        let inherits = match_ignore_ascii_case! {&*ident,
+          "true" => true,
+          "false" => false,
+          _ => return Err(location.new_unexpected_token_error(
+            cssparser::Token::Ident(ident.clone())
+          ))
+        };
+        self.inherits = Some(inherits);
+      },
+      "initial-value" => {
+        // Buffer the value into a string. We will parse it later.
+        let start = input.position();
+        while input.next().is_ok() {}
+        let initial_value = input.slice_from(start);
+        self.initial_value = Some(initial_value);
+      },
+      _ => return Err(input.new_custom_error(ParserError::InvalidDeclaration))
+    }
+
+    return Ok(());
+  }
+}
+
+/// Default methods reject all at rules.
+impl<'i> AtRuleParser<'i> for PropertyRuleDeclarationParser<'i> {
+  type Prelude = ();
+  type AtRule = ();
+  type Error = ParserError<'i>;
+}
+
+impl<'i> QualifiedRuleParser<'i> for PropertyRuleDeclarationParser<'i> {
+  type Prelude = ();
+  type QualifiedRule = ();
+  type Error = ParserError<'i>;
+}
+
+impl<'i> RuleBodyItemParser<'i, (), ParserError<'i>> for PropertyRuleDeclarationParser<'i> {
+  fn parse_qualified(&self) -> bool {
+    false
+  }
+
+  fn parse_declarations(&self) -> bool {
+    true
+  }
+}
diff --git a/src/rules/scope.rs b/src/rules/scope.rs
new file mode 100644
index 0000000..ab7d100
--- /dev/null
+++ b/src/rules/scope.rs
@@ -0,0 +1,107 @@
+//! The `@scope` rule.
+
+use super::Location;
+use super::{CssRuleList, MinifyContext};
+use crate::error::{MinifyError, PrinterError};
+use crate::parser::DefaultAtRule;
+use crate::printer::Printer;
+use crate::selector::{is_pure_css_modules_selector, SelectorList};
+use crate::traits::ToCss;
+#[cfg(feature = "visitor")]
+use crate::visitor::Visit;
+
+/// A [@scope](https://drafts.csswg.org/css-cascade-6/#scope-atrule) rule.
+///
+/// @scope (<scope-start>) [to (<scope-end>)]? {
+///  <stylesheet>
+/// }
+#[derive(Debug, PartialEq, Clone)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(rename_all = "camelCase")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub struct ScopeRule<'i, R = DefaultAtRule> {
+  /// A selector list used to identify the scoping root(s).
+  pub scope_start: Option<SelectorList<'i>>,
+  /// A selector list used to identify any scoping limits.
+  pub scope_end: Option<SelectorList<'i>>,
+  /// Nested rules within the `@scope` rule.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  pub rules: CssRuleList<'i, R>,
+  /// The location of the rule in the source file.
+  #[cfg_attr(feature = "visitor", skip_visit)]
+  pub loc: Location,
+}
+
+impl<'i, T: Clone> ScopeRule<'i, T> {
+  pub(crate) fn minify(&mut self, context: &mut MinifyContext<'_, 'i>) -> Result<(), MinifyError> {
+    if context.pure_css_modules {
+      if let Some(scope_start) = &self.scope_start {
+        if !scope_start.0.iter().all(is_pure_css_modules_selector) {
+          return Err(MinifyError {
+            kind: crate::error::MinifyErrorKind::ImpureCSSModuleSelector,
+            loc: self.loc,
+          });
+        }
+      }
+
+      if let Some(scope_end) = &self.scope_end {
+        if !scope_end.0.iter().all(is_pure_css_modules_selector) {
+          return Err(MinifyError {
+            kind: crate::error::MinifyErrorKind::ImpureCSSModuleSelector,
+            loc: self.loc,
+          });
+        }
+      }
+    }
+
+    self.rules.minify(context, false)
+  }
+}
+
+impl<'i, T: ToCss> ToCss for ScopeRule<'i, T> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    #[cfg(feature = "sourcemap")]
+    dest.add_mapping(self.loc);
+    dest.write_str("@scope")?;
+    dest.whitespace()?;
+    if let Some(scope_start) = &self.scope_start {
+      dest.write_char('(')?;
+      scope_start.to_css(dest)?;
+      dest.write_char(')')?;
+      dest.whitespace()?;
+    }
+    if let Some(scope_end) = &self.scope_end {
+      if dest.minify {
+        dest.write_char(' ')?;
+      }
+      dest.write_str("to (")?;
+      // <scope-start> is treated as an ancestor of scope end.
+      // https://drafts.csswg.org/css-nesting/#nesting-at-scope
+      if let Some(scope_start) = &self.scope_start {
+        dest.with_context(scope_start, |dest| scope_end.to_css(dest))?;
+      } else {
+        scope_end.to_css(dest)?;
+      }
+      dest.write_char(')')?;
+      dest.whitespace()?;
+    }
+    dest.write_char('{')?;
+    dest.indent();
+    dest.newline()?;
+    // Nested style rules within @scope are implicitly relative to the <scope-start>
+    // so clear our style context while printing them to avoid replacing & ourselves.
+    // https://drafts.csswg.org/css-cascade-6/#scoped-rules
+    dest.with_cleared_context(|dest| self.rules.to_css(dest))?;
+    dest.dedent();
+    dest.newline()?;
+    dest.write_char('}')
+  }
+}
diff --git a/src/rules/starting_style.rs b/src/rules/starting_style.rs
new file mode 100644
index 0000000..267156f
--- /dev/null
+++ b/src/rules/starting_style.rs
@@ -0,0 +1,55 @@
+//! The `@starting-style` rule.
+
+use super::Location;
+use super::{CssRuleList, MinifyContext};
+use crate::error::{MinifyError, PrinterError};
+use crate::parser::DefaultAtRule;
+use crate::printer::Printer;
+use crate::traits::ToCss;
+#[cfg(feature = "visitor")]
+use crate::visitor::Visit;
+
+/// A [@starting-style](https://drafts.csswg.org/css-transitions-2/#defining-before-change-style-the-starting-style-rule) rule.
+#[derive(Debug, PartialEq, Clone)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub struct StartingStyleRule<'i, R = DefaultAtRule> {
+  /// Nested rules within the `@starting-style` rule.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  pub rules: CssRuleList<'i, R>,
+  /// The location of the rule in the source file.
+  #[cfg_attr(feature = "visitor", skip_visit)]
+  pub loc: Location,
+}
+
+impl<'i, T: Clone> StartingStyleRule<'i, T> {
+  pub(crate) fn minify(
+    &mut self,
+    context: &mut MinifyContext<'_, 'i>,
+    parent_is_unused: bool,
+  ) -> Result<bool, MinifyError> {
+    self.rules.minify(context, parent_is_unused)?;
+    Ok(self.rules.0.is_empty())
+  }
+}
+
+impl<'i, T: ToCss> ToCss for StartingStyleRule<'i, T> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    #[cfg(feature = "sourcemap")]
+    dest.add_mapping(self.loc);
+    dest.write_str("@starting-style")?;
+    dest.whitespace()?;
+    dest.write_char('{')?;
+    dest.indent();
+    dest.newline()?;
+    self.rules.to_css(dest)?;
+    dest.dedent();
+    dest.newline()?;
+    dest.write_char('}')
+  }
+}
diff --git a/src/rules/style.rs b/src/rules/style.rs
new file mode 100644
index 0000000..ce3cd45
--- /dev/null
+++ b/src/rules/style.rs
@@ -0,0 +1,305 @@
+//! Style rules.
+
+use std::hash::{Hash, Hasher};
+use std::ops::Range;
+
+use super::Location;
+use super::MinifyContext;
+use crate::context::DeclarationContext;
+use crate::declaration::DeclarationBlock;
+use crate::error::ParserError;
+use crate::error::{MinifyError, PrinterError};
+use crate::parser::DefaultAtRule;
+use crate::printer::Printer;
+use crate::rules::CssRuleList;
+use crate::selector::{
+  downlevel_selectors, get_prefix, is_compatible, is_pure_css_modules_selector, is_unused, SelectorList,
+};
+use crate::targets::{should_compile, Targets};
+use crate::traits::ToCss;
+use crate::vendor_prefix::VendorPrefix;
+#[cfg(feature = "visitor")]
+use crate::visitor::Visit;
+use cssparser::*;
+
+/// A CSS [style rule](https://drafts.csswg.org/css-syntax/#style-rules).
+#[derive(Debug, PartialEq, Clone)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub struct StyleRule<'i, R = DefaultAtRule> {
+  /// The selectors for the style rule.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  pub selectors: SelectorList<'i>,
+  /// A vendor prefix override, used during selector printing.
+  #[cfg_attr(feature = "serde", serde(skip, default = "VendorPrefix::empty"))]
+  #[cfg_attr(feature = "visitor", skip_visit)]
+  pub vendor_prefix: VendorPrefix,
+  /// The declarations within the style rule.
+  #[cfg_attr(feature = "serde", serde(default))]
+  pub declarations: DeclarationBlock<'i>,
+  /// Nested rules within the style rule.
+  #[cfg_attr(feature = "serde", serde(default = "default_rule_list::<R>"))]
+  pub rules: CssRuleList<'i, R>,
+  /// The location of the rule in the source file.
+  #[cfg_attr(feature = "visitor", skip_visit)]
+  pub loc: Location,
+}
+
+#[cfg(feature = "serde")]
+fn default_rule_list<'i, R>() -> CssRuleList<'i, R> {
+  CssRuleList(Vec::new())
+}
+
+impl<'i, T: Clone> StyleRule<'i, T> {
+  pub(crate) fn minify(
+    &mut self,
+    context: &mut MinifyContext<'_, 'i>,
+    parent_is_unused: bool,
+  ) -> Result<bool, MinifyError> {
+    let mut unused = false;
+    if !context.unused_symbols.is_empty() {
+      if is_unused(&mut self.selectors.0.iter(), &context.unused_symbols, parent_is_unused) {
+        if self.rules.0.is_empty() {
+          return Ok(true);
+        }
+
+        self.declarations.declarations.clear();
+        self.declarations.important_declarations.clear();
+        unused = true;
+      }
+    }
+
+    let pure_css_modules = context.pure_css_modules;
+    if context.pure_css_modules {
+      if !self.selectors.0.iter().all(is_pure_css_modules_selector) {
+        return Err(MinifyError {
+          kind: crate::error::MinifyErrorKind::ImpureCSSModuleSelector,
+          loc: self.loc,
+        });
+      }
+
+      // Parent rule contained id or class, so child rules don't need to.
+      context.pure_css_modules = false;
+    }
+
+    context.handler_context.context = DeclarationContext::StyleRule;
+    self
+      .declarations
+      .minify(context.handler, context.important_handler, &mut context.handler_context);
+    context.handler_context.context = DeclarationContext::None;
+
+    if !self.rules.0.is_empty() {
+      let mut handler_context = context.handler_context.child(DeclarationContext::StyleRule);
+      std::mem::swap(&mut context.handler_context, &mut handler_context);
+      self.rules.minify(context, unused)?;
+      context.handler_context = handler_context;
+      if unused && self.rules.0.is_empty() {
+        return Ok(true);
+      }
+    }
+
+    context.pure_css_modules = pure_css_modules;
+    Ok(false)
+  }
+}
+
+impl<'i, T> StyleRule<'i, T> {
+  /// Returns whether the rule is empty.
+  pub fn is_empty(&self) -> bool {
+    self.selectors.0.is_empty() || (self.declarations.is_empty() && self.rules.0.is_empty())
+  }
+
+  /// Returns whether the selectors in the rule are compatible
+  /// with all of the given browser targets.
+  pub fn is_compatible(&self, targets: Targets) -> bool {
+    is_compatible(&self.selectors.0, targets)
+  }
+
+  /// Returns the line and column range of the property key and value at the given index in this style rule.
+  ///
+  /// For performance and memory efficiency in non-error cases, source locations are not stored during parsing.
+  /// Instead, they are computed lazily using the original source string that was used to parse the stylesheet/rule.
+  pub fn property_location<'t>(
+    &self,
+    code: &'i str,
+    index: usize,
+  ) -> Result<(Range<SourceLocation>, Range<SourceLocation>), ParseError<'i, ParserError<'i>>> {
+    let mut input = ParserInput::new(code);
+    let mut parser = Parser::new(&mut input);
+
+    // advance until start location of this rule.
+    parse_at(&mut parser, self.loc, |parser| {
+      // skip selector
+      parser.parse_until_before(Delimiter::CurlyBracketBlock, |parser| {
+        while parser.next().is_ok() {}
+        Ok(())
+      })?;
+
+      parser.expect_curly_bracket_block()?;
+      parser.parse_nested_block(|parser| {
+        let loc = self.declarations.property_location(parser, index);
+        while parser.next().is_ok() {}
+        loc
+      })
+    })
+  }
+
+  /// Returns a hash of this rule for use when deduplicating.
+  /// Includes the selectors and properties.
+  #[inline]
+  pub(crate) fn hash_key(&self) -> u64 {
+    let mut hasher = ahash::AHasher::default();
+    self.selectors.hash(&mut hasher);
+    for (property, _) in self.declarations.iter() {
+      property.property_id().hash(&mut hasher);
+    }
+    hasher.finish()
+  }
+
+  /// Returns whether this rule is a duplicate of another rule.
+  /// This means it has the same selectors and properties.
+  #[inline]
+  pub(crate) fn is_duplicate(&self, other_rule: &StyleRule<'i, T>) -> bool {
+    self.declarations.len() == other_rule.declarations.len()
+      && self.selectors == other_rule.selectors
+      && self
+        .declarations
+        .iter()
+        .zip(other_rule.declarations.iter())
+        .all(|((a, _), (b, _))| a.property_id() == b.property_id())
+  }
+
+  pub(crate) fn update_prefix(&mut self, context: &mut MinifyContext<'_, 'i>) {
+    self.vendor_prefix = get_prefix(&self.selectors);
+    if self.vendor_prefix.contains(VendorPrefix::None) && context.targets.current.should_compile_selectors() {
+      self.vendor_prefix = downlevel_selectors(self.selectors.0.as_mut_slice(), context.targets.current);
+    }
+  }
+}
+
+fn parse_at<'i, 't, T, F>(
+  parser: &mut Parser<'i, 't>,
+  dest: Location,
+  parse: F,
+) -> Result<T, ParseError<'i, ParserError<'i>>>
+where
+  F: Copy + for<'tt> FnOnce(&mut Parser<'i, 'tt>) -> Result<T, ParseError<'i, ParserError<'i>>>,
+{
+  loop {
+    let loc = parser.current_source_location();
+    if loc.line >= dest.line || (loc.line == dest.line && loc.column >= dest.column) {
+      return parse(parser);
+    }
+
+    match parser.next()? {
+      Token::CurlyBracketBlock => {
+        // Recursively parse nested blocks.
+        let res = parser.parse_nested_block(|parser| {
+          let res = parse_at(parser, dest, parse);
+          while parser.next().is_ok() {}
+          res
+        });
+
+        if let Ok(v) = res {
+          return Ok(v);
+        }
+      }
+      _ => {}
+    }
+  }
+}
+
+impl<'a, 'i, T: ToCss> ToCss for StyleRule<'i, T> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    if self.vendor_prefix.is_empty() {
+      self.to_css_base(dest)
+    } else {
+      let mut first_rule = true;
+      for prefix in self.vendor_prefix {
+        if first_rule {
+          first_rule = false;
+        } else {
+          if !dest.minify {
+            dest.write_char('\n')?; // no indent
+          }
+          dest.newline()?;
+        }
+        dest.vendor_prefix = prefix;
+        self.to_css_base(dest)?;
+      }
+
+      dest.vendor_prefix = VendorPrefix::empty();
+      Ok(())
+    }
+  }
+}
+
+impl<'a, 'i, T: ToCss> StyleRule<'i, T> {
+  fn to_css_base<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    // If supported, or there are no targets, preserve nesting. Otherwise, write nested rules after parent.
+    let supports_nesting = self.rules.0.is_empty() || !should_compile!(dest.targets.current, Nesting);
+    let len = self.declarations.declarations.len() + self.declarations.important_declarations.len();
+    let has_declarations = supports_nesting || len > 0 || self.rules.0.is_empty();
+
+    if has_declarations {
+      #[cfg(feature = "sourcemap")]
+      dest.add_mapping(self.loc);
+      self.selectors.to_css(dest)?;
+      dest.whitespace()?;
+      dest.write_char('{')?;
+      dest.indent();
+      if len > 0 {
+        dest.newline()?;
+      }
+
+      self.declarations.to_css_declarations(
+        dest,
+        supports_nesting && !self.rules.0.is_empty(),
+        &self.selectors,
+        self.loc.source_index,
+      )?;
+    }
+
+    macro_rules! newline {
+      () => {
+        if !dest.minify && (supports_nesting || len > 0) && !self.rules.0.is_empty() {
+          if len > 0 {
+            dest.write_char('\n')?;
+          }
+          dest.newline()?;
+        }
+      };
+    }
+
+    macro_rules! end {
+      () => {
+        if has_declarations {
+          dest.dedent();
+          dest.newline()?;
+          dest.write_char('}')?;
+        }
+      };
+    }
+
+    // Write nested rules after the parent.
+    if supports_nesting {
+      newline!();
+      self.rules.to_css(dest)?;
+      end!();
+    } else {
+      end!();
+      newline!();
+      dest.with_context(&self.selectors, |dest| self.rules.to_css(dest))?;
+    }
+
+    Ok(())
+  }
+}
diff --git a/src/rules/supports.rs b/src/rules/supports.rs
new file mode 100644
index 0000000..a8deccd
--- /dev/null
+++ b/src/rules/supports.rs
@@ -0,0 +1,426 @@
+//! The `@supports` rule.
+
+use std::collections::HashMap;
+
+use super::Location;
+use super::{CssRuleList, MinifyContext};
+use crate::error::{MinifyError, ParserError, PrinterError};
+use crate::parser::DefaultAtRule;
+use crate::printer::Printer;
+use crate::properties::custom::TokenList;
+use crate::properties::PropertyId;
+use crate::targets::{Features, FeaturesIterator, Targets};
+use crate::traits::{Parse, ToCss};
+use crate::values::string::CowArcStr;
+use crate::vendor_prefix::VendorPrefix;
+#[cfg(feature = "visitor")]
+use crate::visitor::Visit;
+use cssparser::*;
+
+#[cfg(feature = "serde")]
+use crate::serialization::ValueWrapper;
+
+/// A [@supports](https://drafts.csswg.org/css-conditional-3/#at-supports) rule.
+#[derive(Debug, PartialEq, Clone)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub struct SupportsRule<'i, R = DefaultAtRule> {
+  /// The supports condition.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  pub condition: SupportsCondition<'i>,
+  /// The rules within the `@supports` rule.
+  pub rules: CssRuleList<'i, R>,
+  /// The location of the rule in the source file.
+  #[cfg_attr(feature = "visitor", skip_visit)]
+  pub loc: Location,
+}
+
+impl<'i, T: Clone> SupportsRule<'i, T> {
+  pub(crate) fn minify(
+    &mut self,
+    context: &mut MinifyContext<'_, 'i>,
+    parent_is_unused: bool,
+  ) -> Result<(), MinifyError> {
+    let inserted = context.targets.enter_supports(self.condition.get_supported_features());
+    if inserted {
+      context.handler_context.targets = context.targets.current;
+    }
+
+    self.condition.set_prefixes_for_targets(&context.targets.current);
+    let result = self.rules.minify(context, parent_is_unused);
+
+    if inserted {
+      context.targets.exit_supports();
+      context.handler_context.targets = context.targets.current;
+    }
+    result
+  }
+}
+
+impl<'a, 'i, T: ToCss> ToCss for SupportsRule<'i, T> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    #[cfg(feature = "sourcemap")]
+    dest.add_mapping(self.loc);
+    dest.write_str("@supports ")?;
+    self.condition.to_css(dest)?;
+    dest.whitespace()?;
+    dest.write_char('{')?;
+    dest.indent();
+    dest.newline()?;
+
+    let inserted = dest.targets.enter_supports(self.condition.get_supported_features());
+    self.rules.to_css(dest)?;
+    if inserted {
+      dest.targets.exit_supports();
+    }
+
+    dest.dedent();
+    dest.newline()?;
+    dest.write_char('}')
+  }
+}
+
+/// A [`<supports-condition>`](https://drafts.csswg.org/css-conditional-3/#typedef-supports-condition),
+/// as used in the `@supports` and `@import` rules.
+#[derive(Debug, PartialEq, Clone)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(feature = "visitor", visit(visit_supports_condition, SUPPORTS_CONDITIONS))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub enum SupportsCondition<'i> {
+  /// A `not` expression.
+  #[cfg_attr(feature = "visitor", skip_type)]
+  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<Box<SupportsCondition>>"))]
+  Not(Box<SupportsCondition<'i>>),
+  /// An `and` expression.
+  #[cfg_attr(feature = "visitor", skip_type)]
+  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<Vec<SupportsCondition>>"))]
+  And(Vec<SupportsCondition<'i>>),
+  /// An `or` expression.
+  #[cfg_attr(feature = "visitor", skip_type)]
+  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<Vec<SupportsCondition>>"))]
+  Or(Vec<SupportsCondition<'i>>),
+  /// A declaration to evaluate.
+  Declaration {
+    /// The property id for the declaration.
+    #[cfg_attr(feature = "serde", serde(borrow, rename = "propertyId"))]
+    property_id: PropertyId<'i>,
+    /// The raw value of the declaration.
+    value: CowArcStr<'i>,
+  },
+  /// A selector to evaluate.
+  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<CowArcStr>"))]
+  Selector(CowArcStr<'i>),
+  // FontTechnology()
+  /// An unknown condition.
+  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<CowArcStr>"))]
+  Unknown(CowArcStr<'i>),
+}
+
+impl<'i> SupportsCondition<'i> {
+  /// Combines the given supports condition into this one with an `and` expression.
+  pub fn and(&mut self, b: &SupportsCondition<'i>) {
+    if let SupportsCondition::And(a) = self {
+      if !a.contains(&b) {
+        a.push(b.clone());
+      }
+    } else if self != b {
+      *self = SupportsCondition::And(vec![self.clone(), b.clone()])
+    }
+  }
+
+  /// Combines the given supports condition into this one with an `or` expression.
+  pub fn or(&mut self, b: &SupportsCondition<'i>) {
+    if let SupportsCondition::Or(a) = self {
+      if !a.contains(&b) {
+        a.push(b.clone());
+      }
+    } else if self != b {
+      *self = SupportsCondition::Or(vec![self.clone(), b.clone()])
+    }
+  }
+
+  fn set_prefixes_for_targets(&mut self, targets: &Targets) {
+    match self {
+      SupportsCondition::Not(cond) => cond.set_prefixes_for_targets(targets),
+      SupportsCondition::And(items) | SupportsCondition::Or(items) => {
+        for item in items {
+          item.set_prefixes_for_targets(targets);
+        }
+      }
+      SupportsCondition::Declaration { property_id, .. } => {
+        let prefix = property_id.prefix();
+        if prefix.is_empty() || prefix.contains(VendorPrefix::None) {
+          property_id.set_prefixes_for_targets(*targets);
+        }
+      }
+      _ => {}
+    }
+  }
+
+  fn get_supported_features(&self) -> Features {
+    fn get_supported_features_internal(value: &SupportsCondition) -> Option<Features> {
+      match value {
+        SupportsCondition::And(list) => list.iter().map(|c| get_supported_features_internal(c)).try_union_all(),
+        SupportsCondition::Declaration { value, .. } => {
+          let mut input = ParserInput::new(&value);
+          let mut parser = Parser::new(&mut input);
+          if let Ok(tokens) = TokenList::parse(&mut parser, &Default::default(), 0) {
+            Some(tokens.get_features())
+          } else {
+            Some(Features::empty())
+          }
+        }
+        // bail out if "not" or "or" exists for now
+        SupportsCondition::Not(_) | SupportsCondition::Or(_) => None,
+        SupportsCondition::Selector(_) | SupportsCondition::Unknown(_) => Some(Features::empty()),
+      }
+    }
+
+    get_supported_features_internal(self).unwrap_or(Features::empty())
+  }
+}
+
+impl<'i> Parse<'i> for SupportsCondition<'i> {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    if input.try_parse(|input| input.expect_ident_matching("not")).is_ok() {
+      let in_parens = Self::parse_in_parens(input)?;
+      return Ok(SupportsCondition::Not(Box::new(in_parens)));
+    }
+
+    let in_parens = Self::parse_in_parens(input)?;
+    let mut expected_type = None;
+    let mut conditions = Vec::new();
+    let mut seen_declarations = HashMap::new();
+
+    loop {
+      let condition = input.try_parse(|input| {
+        let location = input.current_source_location();
+        let s = input.expect_ident()?;
+        let found_type = match_ignore_ascii_case! { &s,
+          "and" => 1,
+          "or" => 2,
+          _ => return Err(location.new_unexpected_token_error(
+            cssparser::Token::Ident(s.clone())
+          ))
+        };
+
+        if let Some(expected) = expected_type {
+          if found_type != expected {
+            return Err(location.new_unexpected_token_error(cssparser::Token::Ident(s.clone())));
+          }
+        } else {
+          expected_type = Some(found_type);
+        }
+
+        Self::parse_in_parens(input)
+      });
+
+      if let Ok(condition) = condition {
+        if conditions.is_empty() {
+          conditions.push(in_parens.clone());
+          if let SupportsCondition::Declaration { property_id, value } = &in_parens {
+            seen_declarations.insert((property_id.with_prefix(VendorPrefix::None), value.clone()), 0);
+          }
+        }
+
+        if let SupportsCondition::Declaration { property_id, value } = condition {
+          // Merge multiple declarations with the same property id (minus prefix) and value together.
+          let property_id = property_id.with_prefix(VendorPrefix::None);
+          let key = (property_id.clone(), value.clone());
+          if let Some(index) = seen_declarations.get(&key) {
+            if let SupportsCondition::Declaration {
+              property_id: cur_property,
+              ..
+            } = &mut conditions[*index]
+            {
+              cur_property.add_prefix(property_id.prefix());
+            }
+          } else {
+            seen_declarations.insert(key, conditions.len());
+            conditions.push(SupportsCondition::Declaration { property_id, value });
+          }
+        } else {
+          conditions.push(condition);
+        }
+      } else {
+        break;
+      }
+    }
+
+    if conditions.len() == 1 {
+      return Ok(conditions.pop().unwrap());
+    }
+
+    match expected_type {
+      Some(1) => Ok(SupportsCondition::And(conditions)),
+      Some(2) => Ok(SupportsCondition::Or(conditions)),
+      _ => Ok(in_parens),
+    }
+  }
+}
+
+impl<'i> SupportsCondition<'i> {
+  fn parse_in_parens<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    input.skip_whitespace();
+    let location = input.current_source_location();
+    let pos = input.position();
+    match input.next()? {
+      Token::Function(ref f) => {
+        match_ignore_ascii_case! { &*f,
+          "selector" => {
+            let res = input.try_parse(|input| {
+              input.parse_nested_block(|input| {
+                let pos = input.position();
+                input.expect_no_error_token()?;
+                Ok(SupportsCondition::Selector(input.slice_from(pos).into()))
+              })
+            });
+            if res.is_ok() {
+              return res
+            }
+          },
+          _ => {}
+        }
+      }
+      Token::ParenthesisBlock => {
+        let res = input.try_parse(|input| {
+          input.parse_nested_block(|input| {
+            if let Ok(condition) = input.try_parse(SupportsCondition::parse) {
+              return Ok(condition);
+            }
+
+            Self::parse_declaration(input)
+          })
+        });
+        if res.is_ok() {
+          return res;
+        }
+      }
+      t => return Err(location.new_unexpected_token_error(t.clone())),
+    };
+
+    input.parse_nested_block(|input| input.expect_no_error_token().map_err(|err| err.into()))?;
+    Ok(SupportsCondition::Unknown(input.slice_from(pos).into()))
+  }
+
+  pub(crate) fn parse_declaration<'t>(
+    input: &mut Parser<'i, 't>,
+  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let property_id = PropertyId::parse(input)?;
+    input.expect_colon()?;
+    input.skip_whitespace();
+    let pos = input.position();
+    input.expect_no_error_token()?;
+    Ok(SupportsCondition::Declaration {
+      property_id,
+      value: input.slice_from(pos).into(),
+    })
+  }
+
+  fn needs_parens(&self, parent: &SupportsCondition) -> bool {
+    match self {
+      SupportsCondition::Not(_) => true,
+      SupportsCondition::And(_) => !matches!(parent, SupportsCondition::And(_)),
+      SupportsCondition::Or(_) => !matches!(parent, SupportsCondition::Or(_)),
+      _ => false,
+    }
+  }
+
+  fn to_css_with_parens_if_needed<W>(&self, dest: &mut Printer<W>, needs_parens: bool) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    if needs_parens {
+      dest.write_char('(')?;
+    }
+    self.to_css(dest)?;
+    if needs_parens {
+      dest.write_char(')')?;
+    }
+    Ok(())
+  }
+}
+
+impl<'i> ToCss for SupportsCondition<'i> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    match self {
+      SupportsCondition::Not(condition) => {
+        dest.write_str("not ")?;
+        condition.to_css_with_parens_if_needed(dest, condition.needs_parens(self))
+      }
+      SupportsCondition::And(conditions) => {
+        let mut first = true;
+        for condition in conditions {
+          if first {
+            first = false;
+          } else {
+            dest.write_str(" and ")?;
+          }
+          condition.to_css_with_parens_if_needed(dest, condition.needs_parens(self))?;
+        }
+        Ok(())
+      }
+      SupportsCondition::Or(conditions) => {
+        let mut first = true;
+        for condition in conditions {
+          if first {
+            first = false;
+          } else {
+            dest.write_str(" or ")?;
+          }
+          condition.to_css_with_parens_if_needed(dest, condition.needs_parens(self))?;
+        }
+        Ok(())
+      }
+      SupportsCondition::Declaration { property_id, value } => {
+        dest.write_char('(')?;
+
+        let prefix = property_id.prefix().or_none();
+        if prefix != VendorPrefix::None {
+          dest.write_char('(')?;
+        }
+
+        let name = property_id.name();
+        let mut first = true;
+        for p in prefix {
+          if first {
+            first = false;
+          } else {
+            dest.write_str(") or (")?;
+          }
+
+          p.to_css(dest)?;
+          serialize_name(name, dest)?;
+          dest.delim(':', false)?;
+          dest.write_str(value)?;
+        }
+
+        if prefix != VendorPrefix::None {
+          dest.write_char(')')?;
+        }
+
+        dest.write_char(')')
+      }
+      SupportsCondition::Selector(sel) => {
+        dest.write_str("selector(")?;
+        dest.write_str(sel)?;
+        dest.write_char(')')
+      }
+      SupportsCondition::Unknown(unknown) => dest.write_str(&unknown),
+    }
+  }
+}
diff --git a/src/rules/unknown.rs b/src/rules/unknown.rs
new file mode 100644
index 0000000..a3a2263
--- /dev/null
+++ b/src/rules/unknown.rs
@@ -0,0 +1,60 @@
+//! An unknown at-rule.
+
+use super::Location;
+use crate::error::PrinterError;
+use crate::printer::Printer;
+use crate::properties::custom::TokenList;
+use crate::traits::ToCss;
+use crate::values::string::CowArcStr;
+#[cfg(feature = "visitor")]
+use crate::visitor::Visit;
+
+/// An unknown at-rule, stored as raw tokens.
+#[derive(Debug, PartialEq, Clone)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub struct UnknownAtRule<'i> {
+  /// The name of the at-rule (without the @).
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  #[cfg_attr(feature = "visitor", skip_visit)]
+  pub name: CowArcStr<'i>,
+  /// The prelude of the rule.
+  pub prelude: TokenList<'i>,
+  /// The contents of the block, if any.
+  pub block: Option<TokenList<'i>>,
+  /// The location of the rule in the source file.
+  #[cfg_attr(feature = "visitor", skip_visit)]
+  pub loc: Location,
+}
+
+impl<'i> ToCss for UnknownAtRule<'i> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    #[cfg(feature = "sourcemap")]
+    dest.add_mapping(self.loc);
+    dest.write_char('@')?;
+    dest.write_str(&self.name)?;
+
+    if !self.prelude.0.is_empty() {
+      dest.write_char(' ')?;
+      self.prelude.to_css(dest, false)?;
+    }
+
+    if let Some(block) = &self.block {
+      dest.whitespace()?;
+      dest.write_char('{')?;
+      dest.indent();
+      dest.newline()?;
+      block.to_css(dest, false)?;
+      dest.dedent();
+      dest.newline()?;
+      dest.write_char('}')
+    } else {
+      dest.write_char(';')
+    }
+  }
+}
diff --git a/src/rules/view_transition.rs b/src/rules/view_transition.rs
new file mode 100644
index 0000000..fac6ec6
--- /dev/null
+++ b/src/rules/view_transition.rs
@@ -0,0 +1,196 @@
+//! The `@view-transition` rule.
+
+use super::Location;
+use crate::error::{ParserError, PrinterError};
+use crate::printer::Printer;
+use crate::properties::custom::CustomProperty;
+use crate::stylesheet::ParserOptions;
+use crate::traits::{Parse, ToCss};
+use crate::values::ident::NoneOrCustomIdentList;
+#[cfg(feature = "visitor")]
+use crate::visitor::Visit;
+use cssparser::*;
+
+/// A [@view-transition](https://drafts.csswg.org/css-view-transitions-2/#view-transition-rule) rule.
+#[derive(Debug, PartialEq, Clone)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub struct ViewTransitionRule<'i> {
+  /// Declarations in the `@view-transition` rule.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  pub properties: Vec<ViewTransitionProperty<'i>>,
+  /// The location of the rule in the source file.
+  #[cfg_attr(feature = "visitor", skip_visit)]
+  pub loc: Location,
+}
+
+/// A property within a `@view-transition` rule.
+///
+///  See [ViewTransitionRule](ViewTransitionRule).
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "property", content = "value", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub enum ViewTransitionProperty<'i> {
+  /// The `navigation` property.
+  Navigation(Navigation),
+  /// The `types` property.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  Types(NoneOrCustomIdentList<'i>),
+  /// An unknown or unsupported property.
+  Custom(CustomProperty<'i>),
+}
+
+/// A value for the [navigation](https://drafts.csswg.org/css-view-transitions-2/#view-transition-navigation-descriptor)
+/// property in a `@view-transition` rule.
+#[derive(Debug, Clone, PartialEq, Default, Parse, ToCss)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub enum Navigation {
+  /// There will be no transition.
+  #[default]
+  None,
+  /// The transition will be enabled if the navigation is same-origin.
+  Auto,
+}
+
+pub(crate) struct ViewTransitionDeclarationParser;
+
+impl<'i> cssparser::DeclarationParser<'i> for ViewTransitionDeclarationParser {
+  type Declaration = ViewTransitionProperty<'i>;
+  type Error = ParserError<'i>;
+
+  fn parse_value<'t>(
+    &mut self,
+    name: CowRcStr<'i>,
+    input: &mut cssparser::Parser<'i, 't>,
+  ) -> Result<Self::Declaration, cssparser::ParseError<'i, Self::Error>> {
+    let state = input.state();
+    match_ignore_ascii_case! { &name,
+      "navigation" => {
+        // https://drafts.csswg.org/css-view-transitions-2/#view-transition-navigation-descriptor
+        if let Ok(navigation) = Navigation::parse(input) {
+          return Ok(ViewTransitionProperty::Navigation(navigation));
+        }
+      },
+      "types" => {
+        // https://drafts.csswg.org/css-view-transitions-2/#types-cross-doc
+        if let Ok(types) = NoneOrCustomIdentList::parse(input) {
+          return Ok(ViewTransitionProperty::Types(types));
+        }
+      },
+      _ => return Err(input.new_custom_error(ParserError::InvalidDeclaration))
+    }
+
+    input.reset(&state);
+    return Ok(ViewTransitionProperty::Custom(CustomProperty::parse(
+      name.into(),
+      input,
+      &ParserOptions::default(),
+    )?));
+  }
+}
+
+/// Default methods reject all at rules.
+impl<'i> AtRuleParser<'i> for ViewTransitionDeclarationParser {
+  type Prelude = ();
+  type AtRule = ViewTransitionProperty<'i>;
+  type Error = ParserError<'i>;
+}
+
+impl<'i> QualifiedRuleParser<'i> for ViewTransitionDeclarationParser {
+  type Prelude = ();
+  type QualifiedRule = ViewTransitionProperty<'i>;
+  type Error = ParserError<'i>;
+}
+
+impl<'i> RuleBodyItemParser<'i, ViewTransitionProperty<'i>, ParserError<'i>> for ViewTransitionDeclarationParser {
+  fn parse_qualified(&self) -> bool {
+    false
+  }
+
+  fn parse_declarations(&self) -> bool {
+    true
+  }
+}
+
+impl<'i> ViewTransitionRule<'i> {
+  pub(crate) fn parse<'t>(
+    input: &mut Parser<'i, 't>,
+    loc: Location,
+  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let mut decl_parser = ViewTransitionDeclarationParser;
+    let mut parser = RuleBodyParser::new(input, &mut decl_parser);
+    let mut properties = vec![];
+    while let Some(decl) = parser.next() {
+      if let Ok(decl) = decl {
+        properties.push(decl);
+      }
+    }
+
+    Ok(ViewTransitionRule { properties, loc })
+  }
+}
+
+impl<'i> ToCss for ViewTransitionRule<'i> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    #[cfg(feature = "sourcemap")]
+    dest.add_mapping(self.loc);
+    dest.write_str("@view-transition")?;
+    dest.whitespace()?;
+    dest.write_char('{')?;
+    dest.indent();
+    let len = self.properties.len();
+    for (i, prop) in self.properties.iter().enumerate() {
+      dest.newline()?;
+      prop.to_css(dest)?;
+      if i != len - 1 || !dest.minify {
+        dest.write_char(';')?;
+      }
+    }
+    dest.dedent();
+    dest.newline()?;
+    dest.write_char('}')
+  }
+}
+
+impl<'i> ToCss for ViewTransitionProperty<'i> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    macro_rules! property {
+      ($prop: literal, $value: expr) => {{
+        dest.write_str($prop)?;
+        dest.delim(':', false)?;
+        $value.to_css(dest)
+      }};
+    }
+
+    match self {
+      ViewTransitionProperty::Navigation(f) => property!("navigation", f),
+      ViewTransitionProperty::Types(t) => property!("types", t),
+      ViewTransitionProperty::Custom(custom) => {
+        dest.write_str(custom.name.as_ref())?;
+        dest.delim(':', false)?;
+        custom.value.to_css(dest, true)
+      }
+    }
+  }
+}
diff --git a/src/rules/viewport.rs b/src/rules/viewport.rs
new file mode 100644
index 0000000..eca6d14
--- /dev/null
+++ b/src/rules/viewport.rs
@@ -0,0 +1,46 @@
+//! The `@viewport` rule.
+
+use super::Location;
+use crate::declaration::DeclarationBlock;
+use crate::error::PrinterError;
+use crate::printer::Printer;
+use crate::traits::ToCss;
+use crate::vendor_prefix::VendorPrefix;
+#[cfg(feature = "visitor")]
+use crate::visitor::Visit;
+
+/// A [@viewport](https://drafts.csswg.org/css-device-adapt/#atviewport-rule) rule.
+#[derive(Debug, PartialEq, Clone)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(rename_all = "camelCase")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub struct ViewportRule<'i> {
+  /// The vendor prefix for this rule, e.g. `@-ms-viewport`.
+  #[cfg_attr(feature = "visitor", skip_visit)]
+  pub vendor_prefix: VendorPrefix,
+  /// The declarations within the `@viewport` rule.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  pub declarations: DeclarationBlock<'i>,
+  /// The location of the rule in the source file.
+  #[cfg_attr(feature = "visitor", skip_visit)]
+  pub loc: Location,
+}
+
+impl<'i> ToCss for ViewportRule<'i> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    #[cfg(feature = "sourcemap")]
+    dest.add_mapping(self.loc);
+    dest.write_char('@')?;
+    self.vendor_prefix.to_css(dest)?;
+    dest.write_str("viewport")?;
+    self.declarations.to_css_block(dest)
+  }
+}
diff --git a/src/selector.rs b/src/selector.rs
new file mode 100644
index 0000000..d501107
--- /dev/null
+++ b/src/selector.rs
@@ -0,0 +1,2272 @@
+//! CSS selectors.
+
+use crate::compat::Feature;
+use crate::error::{ParserError, PrinterError, SelectorError};
+use crate::parser::ParserFlags;
+use crate::printer::Printer;
+use crate::properties::custom::TokenList;
+use crate::rules::StyleContext;
+use crate::stylesheet::{ParserOptions, PrinterOptions};
+use crate::targets::{should_compile, Targets};
+use crate::traits::{Parse, ParseWithOptions, ToCss};
+use crate::values::ident::{CustomIdent, Ident};
+use crate::values::string::CSSString;
+use crate::vendor_prefix::VendorPrefix;
+#[cfg(feature = "visitor")]
+use crate::visitor::{Visit, VisitTypes, Visitor};
+use crate::{macros::enum_property, values::string::CowArcStr};
+use cssparser::*;
+use parcel_selectors::parser::{NthType, SelectorParseErrorKind};
+use parcel_selectors::{
+  attr::{AttrSelectorOperator, ParsedAttrSelectorOperation, ParsedCaseSensitivity},
+  parser::SelectorImpl,
+};
+use smallvec::SmallVec;
+use std::collections::HashSet;
+use std::fmt;
+
+#[cfg(feature = "serde")]
+use crate::serialization::*;
+
+mod private {
+  #[derive(Debug, Clone, PartialEq, Eq, Hash)]
+  #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+  pub struct Selectors;
+
+  #[cfg(feature = "into_owned")]
+  impl<'any> ::static_self::IntoOwned<'any> for Selectors {
+    type Owned = Self;
+
+    fn into_owned(self) -> Self::Owned {
+      self
+    }
+  }
+}
+
+#[cfg(feature = "into_owned")]
+fn _assert_into_owned() {
+  // Ensure that we provide into_owned
+
+  use static_self::IntoOwned;
+  fn _assert<'any, T: IntoOwned<'any>>() {}
+
+  _assert::<SelectorList>();
+}
+
+use private::Selectors;
+
+/// A list of selectors.
+pub type SelectorList<'i> = parcel_selectors::SelectorList<'i, Selectors>;
+/// A CSS selector, including a list of components.
+pub type Selector<'i> = parcel_selectors::parser::Selector<'i, Selectors>;
+/// An individual component within a selector.
+pub type Component<'i> = parcel_selectors::parser::Component<'i, Selectors>;
+/// A combinator.
+pub use parcel_selectors::parser::Combinator;
+
+impl<'i> SelectorImpl<'i> for Selectors {
+  type AttrValue = CSSString<'i>;
+  type Identifier = Ident<'i>;
+  type LocalName = Ident<'i>;
+  type NamespacePrefix = Ident<'i>;
+  type NamespaceUrl = CowArcStr<'i>;
+  type BorrowedNamespaceUrl = CowArcStr<'i>;
+  type BorrowedLocalName = Ident<'i>;
+
+  type NonTSPseudoClass = PseudoClass<'i>;
+  type PseudoElement = PseudoElement<'i>;
+  type VendorPrefix = VendorPrefix;
+
+  type ExtraMatchingData = ();
+
+  fn to_css<W: fmt::Write>(selectors: &SelectorList<'i>, dest: &mut W) -> std::fmt::Result {
+    let mut printer = Printer::new(dest, PrinterOptions::default());
+    serialize_selector_list(selectors.0.iter(), &mut printer, None, false).map_err(|_| std::fmt::Error)
+  }
+}
+
+pub(crate) struct SelectorParser<'a, 'o, 'i> {
+  pub is_nesting_allowed: bool,
+  pub options: &'a ParserOptions<'o, 'i>,
+}
+
+impl<'a, 'o, 'i> parcel_selectors::parser::Parser<'i> for SelectorParser<'a, 'o, 'i> {
+  type Impl = Selectors;
+  type Error = ParserError<'i>;
+
+  fn parse_non_ts_pseudo_class(
+    &self,
+    loc: SourceLocation,
+    name: CowRcStr<'i>,
+  ) -> Result<PseudoClass<'i>, ParseError<'i, Self::Error>> {
+    use PseudoClass::*;
+    let pseudo_class = match_ignore_ascii_case! { &name,
+      // https://drafts.csswg.org/selectors-4/#useraction-pseudos
+      "hover" => Hover,
+      "active" => Active,
+      "focus" => Focus,
+      "focus-visible" => FocusVisible,
+      "focus-within" => FocusWithin,
+
+      // https://drafts.csswg.org/selectors-4/#time-pseudos
+      "current" => Current,
+      "past" => Past,
+      "future" => Future,
+
+      // https://drafts.csswg.org/selectors-4/#resource-pseudos
+      "playing" => Playing,
+      "paused" => Paused,
+      "seeking" => Seeking,
+      "buffering" => Buffering,
+      "stalled" => Stalled,
+      "muted" => Muted,
+      "volume-locked" => VolumeLocked,
+
+      // https://fullscreen.spec.whatwg.org/#:fullscreen-pseudo-class
+      "fullscreen" => Fullscreen(VendorPrefix::None),
+      "-webkit-full-screen" => Fullscreen(VendorPrefix::WebKit),
+      "-moz-full-screen" => Fullscreen(VendorPrefix::Moz),
+      "-ms-fullscreen" => Fullscreen(VendorPrefix::Ms),
+
+      // https://drafts.csswg.org/selectors/#display-state-pseudos
+      "open" => Open,
+      "closed" => Closed,
+      "modal" => Modal,
+      "picture-in-picture" => PictureInPicture,
+
+      // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-popover-open
+      "popover-open" => PopoverOpen,
+
+      // https://drafts.csswg.org/selectors-4/#the-defined-pseudo
+      "defined" => Defined,
+
+      // https://drafts.csswg.org/selectors-4/#location
+      "any-link" => AnyLink(VendorPrefix::None),
+      "-webkit-any-link" => AnyLink(VendorPrefix::WebKit),
+      "-moz-any-link" => AnyLink(VendorPrefix::Moz),
+      "link" => Link,
+      "local-link" => LocalLink,
+      "target" => Target,
+      "target-within" => TargetWithin,
+      "visited" => Visited,
+
+      // https://drafts.csswg.org/selectors-4/#input-pseudos
+      "enabled" => Enabled,
+      "disabled" => Disabled,
+      "read-only" => ReadOnly(VendorPrefix::None),
+      "-moz-read-only" => ReadOnly(VendorPrefix::Moz),
+      "read-write" => ReadWrite(VendorPrefix::None),
+      "-moz-read-write" => ReadWrite(VendorPrefix::Moz),
+      "placeholder-shown" => PlaceholderShown(VendorPrefix::None),
+      "-moz-placeholder" => PlaceholderShown(VendorPrefix::Moz),
+      "-ms-input-placeholder" => PlaceholderShown(VendorPrefix::Ms),
+      "default" => Default,
+      "checked" => Checked,
+      "indeterminate" => Indeterminate,
+      "blank" => Blank,
+      "valid" => Valid,
+      "invalid" => Invalid,
+      "in-range" => InRange,
+      "out-of-range" => OutOfRange,
+      "required" => Required,
+      "optional" => Optional,
+      "user-valid" => UserValid,
+      "user-invalid" => UserInvalid,
+
+      // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-autofill
+      "autofill" => Autofill(VendorPrefix::None),
+      "-webkit-autofill" => Autofill(VendorPrefix::WebKit),
+      "-o-autofill" => Autofill(VendorPrefix::O),
+
+      // https://drafts.csswg.org/css-view-transitions-2/#pseudo-classes-for-selective-vt
+      "active-view-transition" => ActiveViewTransition,
+
+      // https://webkit.org/blog/363/styling-scrollbars/
+      "horizontal" => WebKitScrollbar(WebKitScrollbarPseudoClass::Horizontal),
+      "vertical" => WebKitScrollbar(WebKitScrollbarPseudoClass::Vertical),
+      "decrement" => WebKitScrollbar(WebKitScrollbarPseudoClass::Decrement),
+      "increment" => WebKitScrollbar(WebKitScrollbarPseudoClass::Increment),
+      "start" => WebKitScrollbar(WebKitScrollbarPseudoClass::Start),
+      "end" => WebKitScrollbar(WebKitScrollbarPseudoClass::End),
+      "double-button" => WebKitScrollbar(WebKitScrollbarPseudoClass::DoubleButton),
+      "single-button" => WebKitScrollbar(WebKitScrollbarPseudoClass::SingleButton),
+      "no-button" => WebKitScrollbar(WebKitScrollbarPseudoClass::NoButton),
+      "corner-present" => WebKitScrollbar(WebKitScrollbarPseudoClass::CornerPresent),
+      "window-inactive" => WebKitScrollbar(WebKitScrollbarPseudoClass::WindowInactive),
+
+      "local" | "global" if self.options.css_modules.is_some() => {
+        return Err(loc.new_custom_error(SelectorParseErrorKind::AmbiguousCssModuleClass(name.clone())))
+      },
+
+      _ => {
+        if !name.starts_with('-') {
+          self.options.warn(loc.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClass(name.clone())));
+        }
+        Custom { name: name.into() }
+      }
+    };
+
+    Ok(pseudo_class)
+  }
+
+  fn parse_non_ts_functional_pseudo_class<'t>(
+    &self,
+    name: CowRcStr<'i>,
+    parser: &mut cssparser::Parser<'i, 't>,
+  ) -> Result<PseudoClass<'i>, ParseError<'i, Self::Error>> {
+    use PseudoClass::*;
+    let pseudo_class = match_ignore_ascii_case! { &name,
+      "lang" => {
+        let languages = parser.parse_comma_separated(|parser| {
+          parser.expect_ident_or_string()
+            .map(|s| s.into())
+            .map_err(|e| e.into())
+        })?;
+        Lang { languages }
+      },
+      "dir" => Dir { direction: Direction::parse(parser)? },
+      // https://drafts.csswg.org/css-view-transitions-2/#the-active-view-transition-type-pseudo
+      "active-view-transition-type" => {
+        let kind = Parse::parse(parser)?;
+        ActiveViewTransitionType { kind }
+      },
+      "local" if self.options.css_modules.is_some() => Local { selector: Box::new(Selector::parse(self, parser)?) },
+      "global" if self.options.css_modules.is_some() => Global { selector: Box::new(Selector::parse(self, parser)?) },
+      _ => {
+        if !name.starts_with('-') {
+          self.options.warn(parser.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClass(name.clone())));
+        }
+        let mut args = Vec::new();
+        TokenList::parse_raw(parser, &mut args, &self.options, 0)?;
+        CustomFunction {
+          name: name.into(),
+          arguments: TokenList(args)
+        }
+      },
+    };
+
+    Ok(pseudo_class)
+  }
+
+  fn parse_any_prefix<'t>(&self, name: &str) -> Option<VendorPrefix> {
+    match_ignore_ascii_case! { &name,
+      "-webkit-any" => Some(VendorPrefix::WebKit),
+      "-moz-any" => Some(VendorPrefix::Moz),
+      _ => None
+    }
+  }
+
+  fn parse_pseudo_element(
+    &self,
+    loc: SourceLocation,
+    name: CowRcStr<'i>,
+  ) -> Result<PseudoElement<'i>, ParseError<'i, Self::Error>> {
+    use PseudoElement::*;
+    let pseudo_element = match_ignore_ascii_case! { &name,
+      "before" => Before,
+      "after" => After,
+      "first-line" => FirstLine,
+      "first-letter" => FirstLetter,
+      "details-content" => DetailsContent,
+      "target-text" => TargetText,
+      "cue" => Cue,
+      "cue-region" => CueRegion,
+      "selection" => Selection(VendorPrefix::None),
+      "-moz-selection" => Selection(VendorPrefix::Moz),
+      "placeholder" => Placeholder(VendorPrefix::None),
+      "-webkit-input-placeholder" => Placeholder(VendorPrefix::WebKit),
+      "-moz-placeholder" => Placeholder(VendorPrefix::Moz),
+      "-ms-input-placeholder" => Placeholder(VendorPrefix::Moz),
+      "marker" => Marker,
+      "backdrop" => Backdrop(VendorPrefix::None),
+      "-webkit-backdrop" => Backdrop(VendorPrefix::WebKit),
+      "file-selector-button" => FileSelectorButton(VendorPrefix::None),
+      "-webkit-file-upload-button" => FileSelectorButton(VendorPrefix::WebKit),
+      "-ms-browse" => FileSelectorButton(VendorPrefix::Ms),
+
+      "-webkit-scrollbar" => WebKitScrollbar(WebKitScrollbarPseudoElement::Scrollbar),
+      "-webkit-scrollbar-button" => WebKitScrollbar(WebKitScrollbarPseudoElement::Button),
+      "-webkit-scrollbar-track" => WebKitScrollbar(WebKitScrollbarPseudoElement::Track),
+      "-webkit-scrollbar-track-piece" => WebKitScrollbar(WebKitScrollbarPseudoElement::TrackPiece),
+      "-webkit-scrollbar-thumb" => WebKitScrollbar(WebKitScrollbarPseudoElement::Thumb),
+      "-webkit-scrollbar-corner" => WebKitScrollbar(WebKitScrollbarPseudoElement::Corner),
+      "-webkit-resizer" => WebKitScrollbar(WebKitScrollbarPseudoElement::Resizer),
+
+      "picker-icon" => PickerIcon,
+      "checkmark" => Checkmark,
+
+      "view-transition" => ViewTransition,
+
+      _ => {
+        if !name.starts_with('-') {
+          self.options.warn(loc.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoElement(name.clone())));
+        }
+        Custom { name: name.into() }
+      }
+    };
+
+    Ok(pseudo_element)
+  }
+
+  fn parse_functional_pseudo_element<'t>(
+    &self,
+    name: CowRcStr<'i>,
+    arguments: &mut Parser<'i, 't>,
+  ) -> Result<<Self::Impl as SelectorImpl<'i>>::PseudoElement, ParseError<'i, Self::Error>> {
+    use PseudoElement::*;
+    let pseudo_element = match_ignore_ascii_case! { &name,
+      "cue" => CueFunction { selector: Box::new(Selector::parse(self, arguments)?) },
+      "cue-region" => CueRegionFunction { selector: Box::new(Selector::parse(self, arguments)?) },
+      "picker" => PickerFunction { identifier: Ident::parse(arguments)? },
+      "view-transition-group" => ViewTransitionGroup { part: ViewTransitionPartSelector::parse(arguments)? },
+      "view-transition-image-pair" => ViewTransitionImagePair { part: ViewTransitionPartSelector::parse(arguments)? },
+      "view-transition-old" => ViewTransitionOld { part: ViewTransitionPartSelector::parse(arguments)? },
+      "view-transition-new" => ViewTransitionNew { part: ViewTransitionPartSelector::parse(arguments)? },
+      _ => {
+        if !name.starts_with('-') {
+          self.options.warn(arguments.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoElement(name.clone())));
+        }
+        let mut args = Vec::new();
+        TokenList::parse_raw(arguments, &mut args, &self.options, 0)?;
+        CustomFunction { name: name.into(), arguments: TokenList(args) }
+      }
+    };
+
+    Ok(pseudo_element)
+  }
+
+  #[inline]
+  fn parse_slotted(&self) -> bool {
+    true
+  }
+
+  #[inline]
+  fn parse_host(&self) -> bool {
+    true
+  }
+
+  #[inline]
+  fn parse_is_and_where(&self) -> bool {
+    true
+  }
+
+  #[inline]
+  fn parse_part(&self) -> bool {
+    true
+  }
+
+  fn default_namespace(&self) -> Option<CowArcStr<'i>> {
+    None
+  }
+
+  fn namespace_for_prefix(&self, prefix: &Ident<'i>) -> Option<CowArcStr<'i>> {
+    Some(prefix.0.clone())
+  }
+
+  #[inline]
+  fn is_nesting_allowed(&self) -> bool {
+    self.is_nesting_allowed
+  }
+
+  fn deep_combinator_enabled(&self) -> bool {
+    self.options.flags.contains(ParserFlags::DEEP_SELECTOR_COMBINATOR)
+  }
+}
+
+enum_property! {
+  /// The [:dir()](https://drafts.csswg.org/selectors-4/#the-dir-pseudo) pseudo class.
+  #[derive(Eq, Hash)]
+  pub enum Direction {
+    /// Left to right
+    Ltr,
+    /// Right to left
+    Rtl,
+  }
+}
+
+/// A pseudo class.
+#[derive(Clone, PartialEq, Eq, Hash)]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "kind", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum PseudoClass<'i> {
+  // https://drafts.csswg.org/selectors-4/#linguistic-pseudos
+  /// The [:lang()](https://drafts.csswg.org/selectors-4/#the-lang-pseudo) pseudo class.
+  Lang {
+    /// A list of language codes.
+    #[cfg_attr(feature = "serde", serde(borrow))]
+    languages: Vec<CowArcStr<'i>>,
+  },
+  /// The [:dir()](https://drafts.csswg.org/selectors-4/#the-dir-pseudo) pseudo class.
+  Dir {
+    /// A direction.
+    direction: Direction,
+  },
+
+  // https://drafts.csswg.org/selectors-4/#useraction-pseudos
+  /// The [:hover](https://drafts.csswg.org/selectors-4/#the-hover-pseudo) pseudo class.
+  Hover,
+  /// The [:active](https://drafts.csswg.org/selectors-4/#the-active-pseudo) pseudo class.
+  Active,
+  /// The [:focus](https://drafts.csswg.org/selectors-4/#the-focus-pseudo) pseudo class.
+  Focus,
+  /// The [:focus-visible](https://drafts.csswg.org/selectors-4/#the-focus-visible-pseudo) pseudo class.
+  FocusVisible,
+  /// The [:focus-within](https://drafts.csswg.org/selectors-4/#the-focus-within-pseudo) pseudo class.
+  FocusWithin,
+
+  // https://drafts.csswg.org/selectors-4/#time-pseudos
+  /// The [:current](https://drafts.csswg.org/selectors-4/#the-current-pseudo) pseudo class.
+  Current,
+  /// The [:past](https://drafts.csswg.org/selectors-4/#the-past-pseudo) pseudo class.
+  Past,
+  /// The [:future](https://drafts.csswg.org/selectors-4/#the-future-pseudo) pseudo class.
+  Future,
+
+  // https://drafts.csswg.org/selectors-4/#resource-pseudos
+  /// The [:playing](https://drafts.csswg.org/selectors-4/#selectordef-playing) pseudo class.
+  Playing,
+  /// The [:paused](https://drafts.csswg.org/selectors-4/#selectordef-paused) pseudo class.
+  Paused,
+  /// The [:seeking](https://drafts.csswg.org/selectors-4/#selectordef-seeking) pseudo class.
+  Seeking,
+  /// The [:buffering](https://drafts.csswg.org/selectors-4/#selectordef-buffering) pseudo class.
+  Buffering,
+  /// The [:stalled](https://drafts.csswg.org/selectors-4/#selectordef-stalled) pseudo class.
+  Stalled,
+  /// The [:muted](https://drafts.csswg.org/selectors-4/#selectordef-muted) pseudo class.
+  Muted,
+  /// The [:volume-locked](https://drafts.csswg.org/selectors-4/#selectordef-volume-locked) pseudo class.
+  VolumeLocked,
+
+  /// The [:fullscreen](https://fullscreen.spec.whatwg.org/#:fullscreen-pseudo-class) pseudo class.
+  #[cfg_attr(feature = "serde", serde(with = "PrefixWrapper"))]
+  Fullscreen(VendorPrefix),
+
+  // https://drafts.csswg.org/selectors/#display-state-pseudos
+  /// The [:open](https://drafts.csswg.org/selectors/#selectordef-open) pseudo class.
+  Open,
+  /// The [:closed](https://drafts.csswg.org/selectors/#selectordef-closed) pseudo class.
+  Closed,
+  /// The [:modal](https://drafts.csswg.org/selectors/#modal-state) pseudo class.
+  Modal,
+  /// The [:picture-in-picture](https://drafts.csswg.org/selectors/#pip-state) pseudo class.
+  PictureInPicture,
+
+  // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-popover-open
+  /// The [:popover-open](https://html.spec.whatwg.org/multipage/semantics-other.html#selector-popover-open) pseudo class.
+  PopoverOpen,
+
+  /// The [:defined](https://drafts.csswg.org/selectors-4/#the-defined-pseudo) pseudo class.
+  Defined,
+
+  // https://drafts.csswg.org/selectors-4/#location
+  /// The [:any-link](https://drafts.csswg.org/selectors-4/#the-any-link-pseudo) pseudo class.
+  #[cfg_attr(feature = "serde", serde(with = "PrefixWrapper"))]
+  AnyLink(VendorPrefix),
+  /// The [:link](https://drafts.csswg.org/selectors-4/#link-pseudo) pseudo class.
+  Link,
+  /// The [:local-link](https://drafts.csswg.org/selectors-4/#the-local-link-pseudo) pseudo class.
+  LocalLink,
+  /// The [:target](https://drafts.csswg.org/selectors-4/#the-target-pseudo) pseudo class.
+  Target,
+  /// The [:target-within](https://drafts.csswg.org/selectors-4/#the-target-within-pseudo) pseudo class.
+  TargetWithin,
+  /// The [:visited](https://drafts.csswg.org/selectors-4/#visited-pseudo) pseudo class.
+  Visited,
+
+  // https://drafts.csswg.org/selectors-4/#input-pseudos
+  /// The [:enabled](https://drafts.csswg.org/selectors-4/#enabled-pseudo) pseudo class.
+  Enabled,
+  /// The [:disabled](https://drafts.csswg.org/selectors-4/#disabled-pseudo) pseudo class.
+  Disabled,
+  /// The [:read-only](https://drafts.csswg.org/selectors-4/#read-only-pseudo) pseudo class.
+  #[cfg_attr(feature = "serde", serde(with = "PrefixWrapper"))]
+  ReadOnly(VendorPrefix),
+  /// The [:read-write](https://drafts.csswg.org/selectors-4/#read-write-pseudo) pseudo class.
+  #[cfg_attr(feature = "serde", serde(with = "PrefixWrapper"))]
+  ReadWrite(VendorPrefix),
+  /// The [:placeholder-shown](https://drafts.csswg.org/selectors-4/#placeholder) pseudo class.
+  #[cfg_attr(feature = "serde", serde(with = "PrefixWrapper"))]
+  PlaceholderShown(VendorPrefix),
+  /// The [:default](https://drafts.csswg.org/selectors-4/#the-default-pseudo) pseudo class.
+  Default,
+  /// The [:checked](https://drafts.csswg.org/selectors-4/#checked) pseudo class.
+  Checked,
+  /// The [:indeterminate](https://drafts.csswg.org/selectors-4/#indeterminate) pseudo class.
+  Indeterminate,
+  /// The [:blank](https://drafts.csswg.org/selectors-4/#blank) pseudo class.
+  Blank,
+  /// The [:valid](https://drafts.csswg.org/selectors-4/#valid-pseudo) pseudo class.
+  Valid,
+  /// The [:invalid](https://drafts.csswg.org/selectors-4/#invalid-pseudo) pseudo class.
+  Invalid,
+  /// The [:in-range](https://drafts.csswg.org/selectors-4/#in-range-pseudo) pseudo class.
+  InRange,
+  /// The [:out-of-range](https://drafts.csswg.org/selectors-4/#out-of-range-pseudo) pseudo class.
+  OutOfRange,
+  /// The [:required](https://drafts.csswg.org/selectors-4/#required-pseudo) pseudo class.
+  Required,
+  /// The [:optional](https://drafts.csswg.org/selectors-4/#optional-pseudo) pseudo class.
+  Optional,
+  /// The [:user-valid](https://drafts.csswg.org/selectors-4/#user-valid-pseudo) pseudo class.
+  UserValid,
+  /// The [:used-invalid](https://drafts.csswg.org/selectors-4/#user-invalid-pseudo) pseudo class.
+  UserInvalid,
+
+  /// The [:autofill](https://html.spec.whatwg.org/multipage/semantics-other.html#selector-autofill) pseudo class.
+  #[cfg_attr(feature = "serde", serde(with = "PrefixWrapper"))]
+  Autofill(VendorPrefix),
+
+  /// The [:active-view-transition](https://drafts.csswg.org/css-view-transitions-2/#the-active-view-transition-pseudo) pseudo class.
+  ActiveViewTransition,
+  /// The [:active-view-transition-type()](https://drafts.csswg.org/css-view-transitions-2/#the-active-view-transition-type-pseudo) pseudo class.
+  ActiveViewTransitionType {
+    /// A view transition type.
+    #[cfg_attr(feature = "serde", serde(rename = "type"))]
+    kind: SmallVec<[CustomIdent<'i>; 1]>,
+  },
+
+  // CSS modules
+  /// The CSS modules :local() pseudo class.
+  Local {
+    /// A local selector.
+    selector: Box<Selector<'i>>,
+  },
+  /// The CSS modules :global() pseudo class.
+  Global {
+    /// A global selector.
+    selector: Box<Selector<'i>>,
+  },
+
+  /// A [webkit scrollbar](https://webkit.org/blog/363/styling-scrollbars/) pseudo class.
+  // https://webkit.org/blog/363/styling-scrollbars/
+  #[cfg_attr(
+    feature = "serde",
+    serde(rename = "webkit-scrollbar", with = "ValueWrapper::<WebKitScrollbarPseudoClass>")
+  )]
+  WebKitScrollbar(WebKitScrollbarPseudoClass),
+  /// An unknown pseudo class.
+  Custom {
+    /// The pseudo class name.
+    name: CowArcStr<'i>,
+  },
+  /// An unknown functional pseudo class.
+  CustomFunction {
+    /// The pseudo class name.
+    name: CowArcStr<'i>,
+    /// The arguments of the pseudo class function.
+    arguments: TokenList<'i>,
+  },
+}
+
+/// A [webkit scrollbar](https://webkit.org/blog/363/styling-scrollbars/) pseudo class.
+#[derive(Clone, Eq, PartialEq, Hash)]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum WebKitScrollbarPseudoClass {
+  /// :horizontal
+  Horizontal,
+  /// :vertical
+  Vertical,
+  /// :decrement
+  Decrement,
+  /// :increment
+  Increment,
+  /// :start
+  Start,
+  /// :end
+  End,
+  /// :double-button
+  DoubleButton,
+  /// :single-button
+  SingleButton,
+  /// :no-button
+  NoButton,
+  /// :corner-present
+  CornerPresent,
+  /// :window-inactive
+  WindowInactive,
+}
+
+impl<'i> parcel_selectors::parser::NonTSPseudoClass<'i> for PseudoClass<'i> {
+  type Impl = Selectors;
+
+  fn is_active_or_hover(&self) -> bool {
+    matches!(*self, PseudoClass::Active | PseudoClass::Hover)
+  }
+
+  fn is_user_action_state(&self) -> bool {
+    matches!(
+      *self,
+      PseudoClass::Active
+        | PseudoClass::Hover
+        | PseudoClass::Focus
+        | PseudoClass::FocusWithin
+        | PseudoClass::FocusVisible
+    )
+  }
+
+  fn is_valid_before_webkit_scrollbar(&self) -> bool {
+    !matches!(*self, PseudoClass::WebKitScrollbar(..))
+  }
+
+  fn is_valid_after_webkit_scrollbar(&self) -> bool {
+    // https://github.com/WebKit/WebKit/blob/02fbf9b7aa435edb96cbf563a8d4dcf1aa73b4b3/Source/WebCore/css/parser/CSSSelectorParser.cpp#L285
+    matches!(
+      *self,
+      PseudoClass::WebKitScrollbar(..)
+        | PseudoClass::Enabled
+        | PseudoClass::Disabled
+        | PseudoClass::Hover
+        | PseudoClass::Active
+    )
+  }
+}
+
+impl<'i> cssparser::ToCss for PseudoClass<'i> {
+  fn to_css<W>(&self, dest: &mut W) -> std::fmt::Result
+  where
+    W: fmt::Write,
+  {
+    let mut s = String::new();
+    serialize_pseudo_class(self, &mut Printer::new(&mut s, Default::default()), None)
+      .map_err(|_| std::fmt::Error)?;
+    write!(dest, "{}", s)
+  }
+}
+
+fn serialize_pseudo_class<'a, 'i, W>(
+  pseudo_class: &PseudoClass<'i>,
+  dest: &mut Printer<W>,
+  context: Option<&StyleContext>,
+) -> Result<(), PrinterError>
+where
+  W: fmt::Write,
+{
+  use PseudoClass::*;
+  match pseudo_class {
+    Lang { languages: lang } => {
+      dest.write_str(":lang(")?;
+      let mut first = true;
+      for lang in lang {
+        if first {
+          first = false;
+        } else {
+          dest.delim(',', false)?;
+        }
+        serialize_identifier(lang, dest)?;
+      }
+      return dest.write_str(")");
+    }
+    Dir { direction: dir } => {
+      dest.write_str(":dir(")?;
+      dir.to_css(dest)?;
+      return dest.write_str(")");
+    }
+    _ => {}
+  }
+
+  macro_rules! write_prefixed {
+    ($prefix: ident, $val: expr) => {{
+      dest.write_char(':')?;
+      // If the printer has a vendor prefix override, use that.
+      let vp = if !dest.vendor_prefix.is_empty() {
+        (dest.vendor_prefix & *$prefix).or_none()
+      } else {
+        *$prefix
+      };
+      vp.to_css(dest)?;
+      dest.write_str($val)
+    }};
+  }
+
+  macro_rules! pseudo {
+    ($key: ident, $s: literal) => {{
+      let class = if let Some(pseudo_classes) = &dest.pseudo_classes {
+        pseudo_classes.$key
+      } else {
+        None
+      };
+
+      if let Some(class) = class {
+        dest.write_char('.')?;
+        dest.write_ident(class, true)
+      } else {
+        dest.write_str($s)
+      }
+    }};
+  }
+
+  match pseudo_class {
+    // https://drafts.csswg.org/selectors-4/#useraction-pseudos
+    Hover => pseudo!(hover, ":hover"),
+    Active => pseudo!(active, ":active"),
+    Focus => pseudo!(focus, ":focus"),
+    FocusVisible => pseudo!(focus_visible, ":focus-visible"),
+    FocusWithin => pseudo!(focus_within, ":focus-within"),
+
+    // https://drafts.csswg.org/selectors-4/#time-pseudos
+    Current => dest.write_str(":current"),
+    Past => dest.write_str(":past"),
+    Future => dest.write_str(":future"),
+
+    // https://drafts.csswg.org/selectors-4/#resource-pseudos
+    Playing => dest.write_str(":playing"),
+    Paused => dest.write_str(":paused"),
+    Seeking => dest.write_str(":seeking"),
+    Buffering => dest.write_str(":buffering"),
+    Stalled => dest.write_str(":stalled"),
+    Muted => dest.write_str(":muted"),
+    VolumeLocked => dest.write_str(":volume-locked"),
+
+    // https://fullscreen.spec.whatwg.org/#:fullscreen-pseudo-class
+    Fullscreen(prefix) => {
+      dest.write_char(':')?;
+      let vp = if !dest.vendor_prefix.is_empty() {
+        (dest.vendor_prefix & *prefix).or_none()
+      } else {
+        *prefix
+      };
+      vp.to_css(dest)?;
+      if vp == VendorPrefix::WebKit || vp == VendorPrefix::Moz {
+        dest.write_str("full-screen")
+      } else {
+        dest.write_str("fullscreen")
+      }
+    }
+
+    // https://drafts.csswg.org/selectors/#display-state-pseudos
+    Open => dest.write_str(":open"),
+    Closed => dest.write_str(":closed"),
+    Modal => dest.write_str(":modal"),
+    PictureInPicture => dest.write_str(":picture-in-picture"),
+
+    // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-popover-open
+    PopoverOpen => dest.write_str(":popover-open"),
+
+    // https://drafts.csswg.org/selectors-4/#the-defined-pseudo
+    Defined => dest.write_str(":defined"),
+
+    // https://drafts.csswg.org/selectors-4/#location
+    AnyLink(prefix) => write_prefixed!(prefix, "any-link"),
+    Link => dest.write_str(":link"),
+    LocalLink => dest.write_str(":local-link"),
+    Target => dest.write_str(":target"),
+    TargetWithin => dest.write_str(":target-within"),
+    Visited => dest.write_str(":visited"),
+
+    // https://drafts.csswg.org/selectors-4/#input-pseudos
+    Enabled => dest.write_str(":enabled"),
+    Disabled => dest.write_str(":disabled"),
+    ReadOnly(prefix) => write_prefixed!(prefix, "read-only"),
+    ReadWrite(prefix) => write_prefixed!(prefix, "read-write"),
+    PlaceholderShown(prefix) => write_prefixed!(prefix, "placeholder-shown"),
+    Default => dest.write_str(":default"),
+    Checked => dest.write_str(":checked"),
+    Indeterminate => dest.write_str(":indeterminate"),
+    Blank => dest.write_str(":blank"),
+    Valid => dest.write_str(":valid"),
+    Invalid => dest.write_str(":invalid"),
+    InRange => dest.write_str(":in-range"),
+    OutOfRange => dest.write_str(":out-of-range"),
+    Required => dest.write_str(":required"),
+    Optional => dest.write_str(":optional"),
+    UserValid => dest.write_str(":user-valid"),
+    UserInvalid => dest.write_str(":user-invalid"),
+
+    // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-autofill
+    Autofill(prefix) => write_prefixed!(prefix, "autofill"),
+
+    ActiveViewTransition => dest.write_str(":active-view-transition"),
+    ActiveViewTransitionType { kind } => {
+      dest.write_str(":active-view-transition-type(")?;
+      kind.to_css(dest)?;
+      dest.write_char(')')
+    }
+
+    Local { selector } => serialize_selector(selector, dest, context, false),
+    Global { selector } => {
+      let css_module = std::mem::take(&mut dest.css_module);
+      serialize_selector(selector, dest, context, false)?;
+      dest.css_module = css_module;
+      Ok(())
+    }
+
+    // https://webkit.org/blog/363/styling-scrollbars/
+    WebKitScrollbar(s) => {
+      use WebKitScrollbarPseudoClass::*;
+      dest.write_str(match s {
+        Horizontal => ":horizontal",
+        Vertical => ":vertical",
+        Decrement => ":decrement",
+        Increment => ":increment",
+        Start => ":start",
+        End => ":end",
+        DoubleButton => ":double-button",
+        SingleButton => ":single-button",
+        NoButton => ":no-button",
+        CornerPresent => ":corner-present",
+        WindowInactive => ":window-inactive",
+      })
+    }
+
+    Lang { languages: _ } | Dir { direction: _ } => unreachable!(),
+    Custom { name } => {
+      dest.write_char(':')?;
+      return dest.write_str(&name);
+    }
+    CustomFunction { name, arguments: args } => {
+      dest.write_char(':')?;
+      dest.write_str(name)?;
+      dest.write_char('(')?;
+      args.to_css_raw(dest)?;
+      dest.write_char(')')
+    }
+  }
+}
+
+impl<'i> PseudoClass<'i> {
+  pub(crate) fn is_equivalent(&self, other: &PseudoClass<'i>) -> bool {
+    use PseudoClass::*;
+    match (self, other) {
+      (Fullscreen(_), Fullscreen(_))
+      | (AnyLink(_), AnyLink(_))
+      | (ReadOnly(_), ReadOnly(_))
+      | (ReadWrite(_), ReadWrite(_))
+      | (PlaceholderShown(_), PlaceholderShown(_))
+      | (Autofill(_), Autofill(_)) => true,
+      (a, b) => a == b,
+    }
+  }
+
+  pub(crate) fn get_prefix(&self) -> VendorPrefix {
+    use PseudoClass::*;
+    match self {
+      Fullscreen(p) | AnyLink(p) | ReadOnly(p) | ReadWrite(p) | PlaceholderShown(p) | Autofill(p) => *p,
+      _ => VendorPrefix::empty(),
+    }
+  }
+
+  pub(crate) fn get_necessary_prefixes(&mut self, targets: Targets) -> VendorPrefix {
+    use crate::prefixes::Feature;
+    use PseudoClass::*;
+    let (p, feature) = match self {
+      Fullscreen(p) => (p, Feature::PseudoClassFullscreen),
+      AnyLink(p) => (p, Feature::PseudoClassAnyLink),
+      ReadOnly(p) => (p, Feature::PseudoClassReadOnly),
+      ReadWrite(p) => (p, Feature::PseudoClassReadWrite),
+      PlaceholderShown(p) => (p, Feature::PseudoClassPlaceholderShown),
+      Autofill(p) => (p, Feature::PseudoClassAutofill),
+      _ => return VendorPrefix::empty(),
+    };
+
+    *p = targets.prefixes(*p, feature);
+    *p
+  }
+}
+
+/// A pseudo element.
+#[derive(PartialEq, Eq, Hash, Clone, Debug)]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "kind", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum PseudoElement<'i> {
+  /// The [::after](https://drafts.csswg.org/css-pseudo-4/#selectordef-after) pseudo element.
+  After,
+  /// The [::before](https://drafts.csswg.org/css-pseudo-4/#selectordef-before) pseudo element.
+  Before,
+  /// The [::first-line](https://drafts.csswg.org/css-pseudo-4/#first-line-pseudo) pseudo element.
+  FirstLine,
+  /// The [::first-letter](https://drafts.csswg.org/css-pseudo-4/#first-letter-pseudo) pseudo element.
+  FirstLetter,
+  /// The [::details-content](https://drafts.csswg.org/css-pseudo-4/#details-content-pseudo)
+  DetailsContent,
+  /// The [::target-text](https://drafts.csswg.org/css-pseudo-4/#selectordef-target-text)
+  TargetText,
+  /// The [::selection](https://drafts.csswg.org/css-pseudo-4/#selectordef-selection) pseudo element.
+  #[cfg_attr(feature = "serde", serde(with = "PrefixWrapper"))]
+  Selection(VendorPrefix),
+  /// The [::placeholder](https://drafts.csswg.org/css-pseudo-4/#placeholder-pseudo) pseudo element.
+  #[cfg_attr(feature = "serde", serde(with = "PrefixWrapper"))]
+  Placeholder(VendorPrefix),
+  ///  The [::marker](https://drafts.csswg.org/css-pseudo-4/#marker-pseudo) pseudo element.
+  Marker,
+  /// The [::backdrop](https://fullscreen.spec.whatwg.org/#::backdrop-pseudo-element) pseudo element.
+  #[cfg_attr(feature = "serde", serde(with = "PrefixWrapper"))]
+  Backdrop(VendorPrefix),
+  /// The [::file-selector-button](https://drafts.csswg.org/css-pseudo-4/#file-selector-button-pseudo) pseudo element.
+  #[cfg_attr(feature = "serde", serde(with = "PrefixWrapper"))]
+  FileSelectorButton(VendorPrefix),
+  /// A [webkit scrollbar](https://webkit.org/blog/363/styling-scrollbars/) pseudo element.
+  #[cfg_attr(
+    feature = "serde",
+    serde(rename = "webkit-scrollbar", with = "ValueWrapper::<WebKitScrollbarPseudoElement>")
+  )]
+  WebKitScrollbar(WebKitScrollbarPseudoElement),
+  /// The [::cue](https://w3c.github.io/webvtt/#the-cue-pseudo-element) pseudo element.
+  Cue,
+  /// The [::cue-region](https://w3c.github.io/webvtt/#the-cue-region-pseudo-element) pseudo element.
+  CueRegion,
+  /// The [::cue()](https://w3c.github.io/webvtt/#cue-selector) functional pseudo element.
+  CueFunction {
+    /// The selector argument.
+    selector: Box<Selector<'i>>,
+  },
+  /// The [::cue-region()](https://w3c.github.io/webvtt/#cue-region-selector) functional pseudo element.
+  CueRegionFunction {
+    /// The selector argument.
+    selector: Box<Selector<'i>>,
+  },
+  /// The [::view-transition](https://w3c.github.io/csswg-drafts/css-view-transitions-1/#view-transition) pseudo element.
+  ViewTransition,
+  /// The [::view-transition-group()](https://w3c.github.io/csswg-drafts/css-view-transitions-1/#view-transition-group-pt-name-selector) functional pseudo element.
+  #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
+  ViewTransitionGroup {
+    /// A part name selector.
+    part: ViewTransitionPartSelector<'i>,
+  },
+  /// The [::view-transition-image-pair()](https://w3c.github.io/csswg-drafts/css-view-transitions-1/#view-transition-image-pair-pt-name-selector) functional pseudo element.
+  #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
+  ViewTransitionImagePair {
+    /// A part name selector.
+    part: ViewTransitionPartSelector<'i>,
+  },
+  /// The [::view-transition-old()](https://w3c.github.io/csswg-drafts/css-view-transitions-1/#view-transition-old-pt-name-selector) functional pseudo element.
+  #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
+  ViewTransitionOld {
+    /// A part name selector.
+    part: ViewTransitionPartSelector<'i>,
+  },
+  /// The [::view-transition-new()](https://w3c.github.io/csswg-drafts/css-view-transitions-1/#view-transition-new-pt-name-selector) functional pseudo element.
+  #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
+  ViewTransitionNew {
+    /// A part name selector.
+    part: ViewTransitionPartSelector<'i>,
+  },
+  /// The [::picker()](https://drafts.csswg.org/css-forms-1/#the-picker-pseudo-element) functional pseudo element.
+  PickerFunction {
+    /// A form control identifier.
+    identifier: Ident<'i>,
+  },
+  /// The [::picker-icon](https://drafts.csswg.org/css-forms-1/#picker-opener-icon-the-picker-icon-pseudo-element) pseudo element.
+  PickerIcon,
+  /// The [::checkmark](https://drafts.csswg.org/css-forms-1/#styling-checkmarks-the-checkmark-pseudo-element) pseudo element.
+  Checkmark,
+  /// An unknown pseudo element.
+  Custom {
+    /// The name of the pseudo element.
+    #[cfg_attr(feature = "serde", serde(borrow))]
+    name: CowArcStr<'i>,
+  },
+  /// An unknown functional pseudo element.
+  CustomFunction {
+    ///The name of the pseudo element.
+    name: CowArcStr<'i>,
+    /// The arguments of the pseudo element function.
+    arguments: TokenList<'i>,
+  },
+}
+
+/// A [webkit scrollbar](https://webkit.org/blog/363/styling-scrollbars/) pseudo element.
+#[derive(PartialEq, Eq, Clone, Debug, Hash)]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum WebKitScrollbarPseudoElement {
+  /// ::-webkit-scrollbar
+  Scrollbar,
+  /// ::-webkit-scrollbar-button
+  Button,
+  /// ::-webkit-scrollbar-track
+  Track,
+  /// ::-webkit-scrollbar-track-piece
+  TrackPiece,
+  /// ::-webkit-scrollbar-thumb
+  Thumb,
+  /// ::-webkit-scrollbar-corner
+  Corner,
+  /// ::-webkit-resizer
+  Resizer,
+}
+
+/// A [view transition part name](https://w3c.github.io/csswg-drafts/css-view-transitions-1/#typedef-pt-name-selector).
+#[derive(PartialEq, Eq, Clone, Debug, Hash)]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum ViewTransitionPartName<'i> {
+  /// *
+  #[cfg_attr(feature = "serde", serde(rename = "*"))]
+  All,
+  /// <custom-ident>
+  #[cfg_attr(feature = "serde", serde(borrow, untagged))]
+  Name(CustomIdent<'i>),
+}
+
+#[cfg(feature = "jsonschema")]
+#[cfg_attr(docsrs, doc(cfg(feature = "jsonschema")))]
+impl<'a> schemars::JsonSchema for ViewTransitionPartName<'a> {
+  fn is_referenceable() -> bool {
+    true
+  }
+
+  fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
+    str::json_schema(gen)
+  }
+
+  fn schema_name() -> String {
+    "ViewTransitionPartName".into()
+  }
+}
+
+impl<'i> Parse<'i> for ViewTransitionPartName<'i> {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    if input.try_parse(|input| input.expect_delim('*')).is_ok() {
+      return Ok(ViewTransitionPartName::All);
+    }
+
+    Ok(ViewTransitionPartName::Name(CustomIdent::parse(input)?))
+  }
+}
+
+impl<'i> ToCss for ViewTransitionPartName<'i> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    match self {
+      ViewTransitionPartName::All => dest.write_char('*'),
+      ViewTransitionPartName::Name(name) => name.to_css(dest),
+    }
+  }
+}
+
+/// A [view transition part selector](https://w3c.github.io/csswg-drafts/css-view-transitions-1/#typedef-pt-name-selector).
+#[derive(PartialEq, Eq, Clone, Debug, Hash)]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub struct ViewTransitionPartSelector<'i> {
+  /// The view transition part name.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  name: Option<ViewTransitionPartName<'i>>,
+  /// A list of view transition classes.
+  classes: Vec<CustomIdent<'i>>,
+}
+
+impl<'i> Parse<'i> for ViewTransitionPartSelector<'i> {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    input.skip_whitespace();
+    let name = input.try_parse(ViewTransitionPartName::parse).ok();
+    let mut classes = Vec::new();
+    while let Ok(token) = input.next_including_whitespace() {
+      if matches!(token, Token::Delim('.')) {
+        match input.next_including_whitespace() {
+          Ok(Token::Ident(id)) => classes.push(CustomIdent(id.into())),
+          _ => return Err(input.new_custom_error(ParserError::SelectorError(SelectorError::InvalidState))),
+        }
+      } else {
+        return Err(input.new_custom_error(ParserError::SelectorError(SelectorError::InvalidState)));
+      }
+    }
+
+    Ok(ViewTransitionPartSelector { name, classes })
+  }
+}
+
+impl<'i> ToCss for ViewTransitionPartSelector<'i> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    if let Some(name) = &self.name {
+      name.to_css(dest)?;
+    }
+    for class in &self.classes {
+      dest.write_char('.')?;
+      class.to_css(dest)?;
+    }
+    Ok(())
+  }
+}
+
+impl<'i> cssparser::ToCss for PseudoElement<'i> {
+  fn to_css<W>(&self, dest: &mut W) -> std::fmt::Result
+  where
+    W: fmt::Write,
+  {
+    let mut s = String::new();
+    serialize_pseudo_element(self, &mut Printer::new(&mut s, Default::default()), None)
+      .map_err(|_| std::fmt::Error)?;
+    write!(dest, "{}", s)
+  }
+}
+
+fn serialize_pseudo_element<'a, 'i, W>(
+  pseudo_element: &PseudoElement,
+  dest: &mut Printer<W>,
+  context: Option<&StyleContext>,
+) -> Result<(), PrinterError>
+where
+  W: fmt::Write,
+{
+  use PseudoElement::*;
+
+  macro_rules! write_prefix {
+    ($prefix: ident) => {{
+      dest.write_str("::")?;
+      // If the printer has a vendor prefix override, use that.
+      let vp = if !dest.vendor_prefix.is_empty() {
+        (dest.vendor_prefix & *$prefix).or_none()
+      } else {
+        *$prefix
+      };
+      vp.to_css(dest)?;
+      vp
+    }};
+  }
+
+  macro_rules! write_prefixed {
+    ($prefix: ident, $val: expr) => {{
+      write_prefix!($prefix);
+      dest.write_str($val)
+    }};
+  }
+
+  match pseudo_element {
+    // CSS2 pseudo elements support a single colon syntax in addition
+    // to the more correct double colon for other pseudo elements.
+    // We use that here because it's supported everywhere and is shorter.
+    After => dest.write_str(":after"),
+    Before => dest.write_str(":before"),
+    FirstLine => dest.write_str(":first-line"),
+    FirstLetter => dest.write_str(":first-letter"),
+    DetailsContent => dest.write_str("::details-content"),
+    TargetText => dest.write_str("::target-text"),
+    Marker => dest.write_str("::marker"),
+    Selection(prefix) => write_prefixed!(prefix, "selection"),
+    Cue => dest.write_str("::cue"),
+    CueRegion => dest.write_str("::cue-region"),
+    CueFunction { selector } => {
+      dest.write_str("::cue(")?;
+      serialize_selector(selector, dest, context, false)?;
+      dest.write_char(')')
+    }
+    CueRegionFunction { selector } => {
+      dest.write_str("::cue-region(")?;
+      serialize_selector(selector, dest, context, false)?;
+      dest.write_char(')')
+    }
+    Placeholder(prefix) => {
+      let vp = write_prefix!(prefix);
+      if vp == VendorPrefix::WebKit || vp == VendorPrefix::Ms {
+        dest.write_str("input-placeholder")
+      } else {
+        dest.write_str("placeholder")
+      }
+    }
+    Backdrop(prefix) => write_prefixed!(prefix, "backdrop"),
+    FileSelectorButton(prefix) => {
+      let vp = write_prefix!(prefix);
+      if vp == VendorPrefix::WebKit {
+        dest.write_str("file-upload-button")
+      } else if vp == VendorPrefix::Ms {
+        dest.write_str("browse")
+      } else {
+        dest.write_str("file-selector-button")
+      }
+    }
+    WebKitScrollbar(s) => {
+      use WebKitScrollbarPseudoElement::*;
+      dest.write_str(match s {
+        Scrollbar => "::-webkit-scrollbar",
+        Button => "::-webkit-scrollbar-button",
+        Track => "::-webkit-scrollbar-track",
+        TrackPiece => "::-webkit-scrollbar-track-piece",
+        Thumb => "::-webkit-scrollbar-thumb",
+        Corner => "::-webkit-scrollbar-corner",
+        Resizer => "::-webkit-resizer",
+      })
+    }
+    ViewTransition => dest.write_str("::view-transition"),
+    ViewTransitionGroup { part } => {
+      dest.write_str("::view-transition-group(")?;
+      part.to_css(dest)?;
+      dest.write_char(')')
+    }
+    ViewTransitionImagePair { part } => {
+      dest.write_str("::view-transition-image-pair(")?;
+      part.to_css(dest)?;
+      dest.write_char(')')
+    }
+    ViewTransitionOld { part } => {
+      dest.write_str("::view-transition-old(")?;
+      part.to_css(dest)?;
+      dest.write_char(')')
+    }
+    ViewTransitionNew { part } => {
+      dest.write_str("::view-transition-new(")?;
+      part.to_css(dest)?;
+      dest.write_char(')')
+    }
+    PickerFunction { identifier } => {
+      dest.write_str("::picker(")?;
+      identifier.to_css(dest)?;
+      dest.write_char(')')
+    }
+    PickerIcon => dest.write_str("::picker-icon"),
+    Checkmark => dest.write_str("::checkmark"),
+    Custom { name: val } => {
+      dest.write_str("::")?;
+      return dest.write_str(val);
+    }
+    CustomFunction { name, arguments: args } => {
+      dest.write_str("::")?;
+      dest.write_str(name)?;
+      dest.write_char('(')?;
+      args.to_css_raw(dest)?;
+      dest.write_char(')')
+    }
+  }
+}
+
+impl<'i> parcel_selectors::parser::PseudoElement<'i> for PseudoElement<'i> {
+  type Impl = Selectors;
+
+  fn accepts_state_pseudo_classes(&self) -> bool {
+    // Be lenient.
+    true
+  }
+
+  fn valid_after_slotted(&self) -> bool {
+    // ::slotted() should support all tree-abiding pseudo-elements, see
+    // https://drafts.csswg.org/css-scoping/#slotted-pseudo
+    // https://drafts.csswg.org/css-pseudo-4/#treelike
+    matches!(
+      *self,
+      PseudoElement::Before
+        | PseudoElement::After
+        | PseudoElement::Marker
+        | PseudoElement::Placeholder(_)
+        | PseudoElement::FileSelectorButton(_)
+    )
+  }
+
+  fn is_webkit_scrollbar(&self) -> bool {
+    matches!(*self, PseudoElement::WebKitScrollbar(..))
+  }
+
+  fn is_view_transition(&self) -> bool {
+    matches!(
+      *self,
+      PseudoElement::ViewTransitionGroup { .. }
+        | PseudoElement::ViewTransitionImagePair { .. }
+        | PseudoElement::ViewTransitionNew { .. }
+        | PseudoElement::ViewTransitionOld { .. }
+    )
+  }
+
+  fn is_unknown(&self) -> bool {
+    matches!(
+      *self,
+      PseudoElement::Custom { .. } | PseudoElement::CustomFunction { .. },
+    )
+  }
+}
+
+impl<'i> PseudoElement<'i> {
+  pub(crate) fn is_equivalent(&self, other: &PseudoElement<'i>) -> bool {
+    use PseudoElement::*;
+    match (self, other) {
+      (Selection(_), Selection(_))
+      | (Placeholder(_), Placeholder(_))
+      | (Backdrop(_), Backdrop(_))
+      | (FileSelectorButton(_), FileSelectorButton(_)) => true,
+      (a, b) => a == b,
+    }
+  }
+
+  pub(crate) fn get_prefix(&self) -> VendorPrefix {
+    use PseudoElement::*;
+    match self {
+      Selection(p) | Placeholder(p) | Backdrop(p) | FileSelectorButton(p) => *p,
+      _ => VendorPrefix::empty(),
+    }
+  }
+
+  pub(crate) fn get_necessary_prefixes(&mut self, targets: Targets) -> VendorPrefix {
+    use crate::prefixes::Feature;
+    use PseudoElement::*;
+    let (p, feature) = match self {
+      Selection(p) => (p, Feature::PseudoElementSelection),
+      Placeholder(p) => (p, Feature::PseudoElementPlaceholder),
+      Backdrop(p) => (p, Feature::PseudoElementBackdrop),
+      FileSelectorButton(p) => (p, Feature::PseudoElementFileSelectorButton),
+      _ => return VendorPrefix::empty(),
+    };
+
+    *p = targets.prefixes(*p, feature);
+    *p
+  }
+}
+
+impl<'a, 'i> ToCss for SelectorList<'i> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: fmt::Write,
+  {
+    serialize_selector_list(self.0.iter(), dest, dest.context(), false)
+  }
+}
+
+impl ToCss for Combinator {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: fmt::Write,
+  {
+    match *self {
+      Combinator::Child => dest.delim('>', true),
+      Combinator::Descendant => dest.write_str(" "),
+      Combinator::NextSibling => dest.delim('+', true),
+      Combinator::LaterSibling => dest.delim('~', true),
+      Combinator::Deep => dest.write_str(" /deep/ "),
+      Combinator::DeepDescendant => {
+        dest.whitespace()?;
+        dest.write_str(">>>")?;
+        dest.whitespace()
+      }
+      Combinator::PseudoElement | Combinator::Part | Combinator::SlotAssignment => Ok(()),
+    }
+  }
+}
+
+// Copied from the selectors crate and modified to override to_css implementation.
+impl<'a, 'i> ToCss for Selector<'i> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: fmt::Write,
+  {
+    serialize_selector(self, dest, dest.context(), false)
+  }
+}
+
+fn serialize_selector<'a, 'i, W>(
+  selector: &Selector<'i>,
+  dest: &mut Printer<W>,
+  context: Option<&StyleContext>,
+  mut is_relative: bool,
+) -> Result<(), PrinterError>
+where
+  W: fmt::Write,
+{
+  use parcel_selectors::parser::*;
+  // Compound selectors invert the order of their contents, so we need to
+  // undo that during serialization.
+  //
+  // This two-iterator strategy involves walking over the selector twice.
+  // We could do something more clever, but selector serialization probably
+  // isn't hot enough to justify it, and the stringification likely
+  // dominates anyway.
+  //
+  // NB: A parse-order iterator is a Rev<>, which doesn't expose as_slice(),
+  // which we need for |split|. So we split by combinators on a match-order
+  // sequence and then reverse.
+
+  let mut combinators = selector.iter_raw_match_order().rev().filter_map(|x| x.as_combinator());
+  let compound_selectors = selector.iter_raw_match_order().as_slice().split(|x| x.is_combinator()).rev();
+  let should_compile_nesting = should_compile!(dest.targets.current, Nesting);
+
+  let mut first = true;
+  let mut combinators_exhausted = false;
+  for mut compound in compound_selectors {
+    debug_assert!(!combinators_exhausted);
+
+    // Skip implicit :scope in relative selectors (e.g. :has(:scope > foo) -> :has(> foo))
+    if is_relative && matches!(compound.get(0), Some(Component::Scope)) {
+      if let Some(combinator) = combinators.next() {
+        combinator.to_css(dest)?;
+      }
+      compound = &compound[1..];
+      is_relative = false;
+    }
+
+    // https://drafts.csswg.org/cssom/#serializing-selectors
+    if compound.is_empty() {
+      continue;
+    }
+
+    let has_leading_nesting = first && matches!(compound[0], Component::Nesting);
+    let first_index = if has_leading_nesting { 1 } else { 0 };
+    first = false;
+
+    // 1. If there is only one simple selector in the compound selectors
+    //    which is a universal selector, append the result of
+    //    serializing the universal selector to s.
+    //
+    // Check if `!compound.empty()` first--this can happen if we have
+    // something like `... > ::before`, because we store `>` and `::`
+    // both as combinators internally.
+    //
+    // If we are in this case, after we have serialized the universal
+    // selector, we skip Step 2 and continue with the algorithm.
+    let (can_elide_namespace, first_non_namespace) = match compound.get(first_index) {
+      Some(Component::ExplicitAnyNamespace)
+      | Some(Component::ExplicitNoNamespace)
+      | Some(Component::Namespace(..)) => (false, first_index + 1),
+      Some(Component::DefaultNamespace(..)) => (true, first_index + 1),
+      _ => (true, first_index),
+    };
+    let mut perform_step_2 = true;
+    let next_combinator = combinators.next();
+    if first_non_namespace == compound.len() - 1 {
+      match (next_combinator, &compound[first_non_namespace]) {
+        // We have to be careful here, because if there is a
+        // pseudo element "combinator" there isn't really just
+        // the one simple selector. Technically this compound
+        // selector contains the pseudo element selector as well
+        // -- Combinator::PseudoElement, just like
+        // Combinator::SlotAssignment, don't exist in the
+        // spec.
+        (Some(Combinator::PseudoElement), _) | (Some(Combinator::SlotAssignment), _) => (),
+        (_, &Component::ExplicitUniversalType) => {
+          // Iterate over everything so we serialize the namespace
+          // too.
+          let mut iter = compound.iter();
+          let swap_nesting = has_leading_nesting && should_compile_nesting;
+          if swap_nesting {
+            // Swap nesting and type selector (e.g. &div -> div&).
+            iter.next();
+          }
+
+          for simple in iter {
+            serialize_component(simple, dest, context)?;
+          }
+
+          if swap_nesting {
+            serialize_nesting(dest, context, false)?;
+          }
+
+          // Skip step 2, which is an "otherwise".
+          perform_step_2 = false;
+        }
+        _ => (),
+      }
+    }
+
+    // 2. Otherwise, for each simple selector in the compound selectors
+    //    that is not a universal selector of which the namespace prefix
+    //    maps to a namespace that is not the default namespace
+    //    serialize the simple selector and append the result to s.
+    //
+    // See https://github.com/w3c/csswg-drafts/issues/1606, which is
+    // proposing to change this to match up with the behavior asserted
+    // in cssom/serialize-namespaced-type-selectors.html, which the
+    // following code tries to match.
+    if perform_step_2 {
+      let mut iter = compound.iter();
+      if has_leading_nesting && should_compile_nesting && is_type_selector(compound.get(first_non_namespace)) {
+        // Swap nesting and type selector (e.g. &div -> div&).
+        // This ensures that the compiled selector is valid. e.g. (div.foo is valid, .foodiv is not).
+        let nesting = iter.next().unwrap();
+        let local = iter.next().unwrap();
+        serialize_component(local, dest, context)?;
+
+        // Also check the next item in case of namespaces.
+        if first_non_namespace > first_index {
+          let local = iter.next().unwrap();
+          serialize_component(local, dest, context)?;
+        }
+
+        serialize_component(nesting, dest, context)?;
+      } else if has_leading_nesting && should_compile_nesting {
+        // Nesting selector may serialize differently if it is leading, due to type selectors.
+        iter.next();
+        serialize_nesting(dest, context, true)?;
+      }
+
+      for simple in iter {
+        if let Component::ExplicitUniversalType = *simple {
+          // Can't have a namespace followed by a pseudo-element
+          // selector followed by a universal selector in the same
+          // compound selector, so we don't have to worry about the
+          // real namespace being in a different `compound`.
+          if can_elide_namespace {
+            continue;
+          }
+        }
+        serialize_component(simple, dest, context)?;
+      }
+    }
+
+    // 3. If this is not the last part of the chain of the selector
+    //    append a single SPACE (U+0020), followed by the combinator
+    //    ">", "+", "~", ">>", "||", as appropriate, followed by another
+    //    single SPACE (U+0020) if the combinator was not whitespace, to
+    //    s.
+    match next_combinator {
+      Some(c) => c.to_css(dest)?,
+      None => combinators_exhausted = true,
+    };
+
+    // 4. If this is the last part of the chain of the selector and
+    //    there is a pseudo-element, append "::" followed by the name of
+    //    the pseudo-element, to s.
+    //
+    // (we handle this above)
+  }
+
+  Ok(())
+}
+
+fn serialize_component<'a, 'i, W>(
+  component: &Component,
+  dest: &mut Printer<W>,
+  context: Option<&StyleContext>,
+) -> Result<(), PrinterError>
+where
+  W: fmt::Write,
+{
+  match component {
+    Component::Combinator(ref c) => c.to_css(dest),
+    Component::AttributeInNoNamespace {
+      ref local_name,
+      operator,
+      ref value,
+      case_sensitivity,
+      ..
+    } => {
+      dest.write_char('[')?;
+      cssparser::ToCss::to_css(local_name, dest)?;
+      cssparser::ToCss::to_css(operator, dest)?;
+
+      if dest.minify {
+        // Serialize as both an identifier and a string and choose the shorter one.
+        let mut id = String::new();
+        serialize_identifier(&value, &mut id)?;
+
+        let s = value.to_css_string(Default::default())?;
+
+        if id.len() > 0 && id.len() < s.len() {
+          dest.write_str(&id)?;
+        } else {
+          dest.write_str(&s)?;
+        }
+      } else {
+        value.to_css(dest)?;
+      }
+
+      match case_sensitivity {
+        parcel_selectors::attr::ParsedCaseSensitivity::CaseSensitive
+        | parcel_selectors::attr::ParsedCaseSensitivity::AsciiCaseInsensitiveIfInHtmlElementInHtmlDocument => {}
+        parcel_selectors::attr::ParsedCaseSensitivity::AsciiCaseInsensitive => dest.write_str(" i")?,
+        parcel_selectors::attr::ParsedCaseSensitivity::ExplicitCaseSensitive => dest.write_str(" s")?,
+      }
+      dest.write_char(']')
+    }
+    Component::Is(ref list)
+    | Component::Where(ref list)
+    | Component::Negation(ref list)
+    | Component::Any(_, ref list) => {
+      match *component {
+        Component::Where(..) => dest.write_str(":where(")?,
+        Component::Is(ref selectors) => {
+          // If there's only one simple selector, serialize it directly.
+          if should_unwrap_is(selectors) {
+            serialize_selector(selectors.first().unwrap(), dest, context, false)?;
+            return Ok(());
+          }
+
+          let vp = dest.vendor_prefix;
+          if vp.intersects(VendorPrefix::WebKit | VendorPrefix::Moz) {
+            dest.write_char(':')?;
+            vp.to_css(dest)?;
+            dest.write_str("any(")?;
+          } else {
+            dest.write_str(":is(")?;
+          }
+        }
+        Component::Negation(_) => {
+          dest.write_str(":not(")?;
+        }
+        Component::Any(prefix, ..) => {
+          let vp = dest.vendor_prefix.or(prefix);
+          if vp.intersects(VendorPrefix::WebKit | VendorPrefix::Moz) {
+            dest.write_char(':')?;
+            vp.to_css(dest)?;
+            dest.write_str("any(")?;
+          } else {
+            dest.write_str(":is(")?;
+          }
+        }
+        _ => unreachable!(),
+      }
+      serialize_selector_list(list.iter(), dest, context, false)?;
+      dest.write_str(")")
+    }
+    Component::Has(ref list) => {
+      dest.write_str(":has(")?;
+      serialize_selector_list(list.iter(), dest, context, true)?;
+      dest.write_str(")")
+    }
+    Component::NonTSPseudoClass(pseudo) => serialize_pseudo_class(pseudo, dest, context),
+    Component::PseudoElement(pseudo) => serialize_pseudo_element(pseudo, dest, context),
+    Component::Nesting => serialize_nesting(dest, context, false),
+    Component::Class(ref class) => {
+      dest.write_char('.')?;
+      dest.write_ident(&class.0, true)
+    }
+    Component::ID(ref id) => {
+      dest.write_char('#')?;
+      dest.write_ident(&id.0, true)
+    }
+    Component::Host(selector) => {
+      dest.write_str(":host")?;
+      if let Some(ref selector) = *selector {
+        dest.write_char('(')?;
+        selector.to_css(dest)?;
+        dest.write_char(')')?;
+      }
+      Ok(())
+    }
+    Component::Slotted(ref selector) => {
+      dest.write_str("::slotted(")?;
+      selector.to_css(dest)?;
+      dest.write_char(')')
+    }
+    Component::NthOf(ref nth_of_data) => {
+      let nth_data = nth_of_data.nth_data();
+      nth_data.write_start(dest, true)?;
+      nth_data.write_affine(dest)?;
+      dest.write_str(" of ")?;
+      serialize_selector_list(nth_of_data.selectors().iter(), dest, context, true)?;
+      dest.write_char(')')
+    }
+    _ => {
+      cssparser::ToCss::to_css(component, dest)?;
+      Ok(())
+    }
+  }
+}
+
+fn should_unwrap_is<'i>(selectors: &Box<[Selector<'i>]>) -> bool {
+  if selectors.len() == 1 {
+    let first = selectors.first().unwrap();
+    if !has_type_selector(first) && is_simple(first) {
+      return true;
+    }
+  }
+
+  false
+}
+
+fn serialize_nesting<W>(
+  dest: &mut Printer<W>,
+  context: Option<&StyleContext>,
+  first: bool,
+) -> Result<(), PrinterError>
+where
+  W: fmt::Write,
+{
+  if let Some(ctx) = context {
+    // If there's only one simple selector, just serialize it directly.
+    // Otherwise, use an :is() pseudo class.
+    // Type selectors are only allowed at the start of a compound selector,
+    // so use :is() if that is not the case.
+    if ctx.selectors.0.len() == 1
+      && (first || (!has_type_selector(&ctx.selectors.0[0]) && is_simple(&ctx.selectors.0[0])))
+    {
+      serialize_selector(ctx.selectors.0.first().unwrap(), dest, ctx.parent, false)
+    } else {
+      dest.write_str(":is(")?;
+      serialize_selector_list(ctx.selectors.0.iter(), dest, ctx.parent, false)?;
+      dest.write_char(')')
+    }
+  } else {
+    // If there is no context, we are at the root if nesting is supported. This is equivalent to :scope.
+    // Otherwise, if nesting is supported, serialize the nesting selector directly.
+    if should_compile!(dest.targets.current, Nesting) {
+      dest.write_str(":scope")
+    } else {
+      dest.write_char('&')
+    }
+  }
+}
+
+#[inline]
+fn has_type_selector(selector: &Selector) -> bool {
+  // For input:checked the component vector is
+  // [input, :checked] so we have to check it using matching order.
+  //
+  // This both happens for input:checked and is(input:checked)
+  let mut iter = selector.iter_raw_match_order();
+  let first = iter.next();
+
+  if is_namespace(first) {
+    is_type_selector(iter.next())
+  } else {
+    is_type_selector(first)
+  }
+}
+
+#[inline]
+fn is_simple(selector: &Selector) -> bool {
+  !selector.iter_raw_match_order().any(|component| component.is_combinator())
+}
+
+#[inline]
+fn is_type_selector(component: Option<&Component>) -> bool {
+  matches!(
+    component,
+    Some(Component::LocalName(_)) | Some(Component::ExplicitUniversalType)
+  )
+}
+
+#[inline]
+fn is_namespace(component: Option<&Component>) -> bool {
+  matches!(
+    component,
+    Some(Component::ExplicitAnyNamespace)
+      | Some(Component::ExplicitNoNamespace)
+      | Some(Component::Namespace(..))
+      | Some(Component::DefaultNamespace(_))
+  )
+}
+
+fn serialize_selector_list<'a, 'i: 'a, I, W>(
+  iter: I,
+  dest: &mut Printer<W>,
+  context: Option<&StyleContext>,
+  is_relative: bool,
+) -> Result<(), PrinterError>
+where
+  I: Iterator<Item = &'a Selector<'i>>,
+  W: fmt::Write,
+{
+  let mut first = true;
+  for selector in iter {
+    if !first {
+      dest.delim(',', false)?;
+    }
+    first = false;
+    serialize_selector(selector, dest, context, is_relative)?;
+  }
+  Ok(())
+}
+
+pub(crate) fn is_compatible(selectors: &[Selector], targets: Targets) -> bool {
+  for selector in selectors {
+    let iter = selector.iter_raw_match_order();
+    for component in iter {
+      let feature = match component {
+        Component::ID(_) | Component::Class(_) | Component::LocalName(_) => continue,
+
+        Component::ExplicitAnyNamespace
+        | Component::ExplicitNoNamespace
+        | Component::DefaultNamespace(_)
+        | Component::Namespace(_, _) => Feature::Namespaces,
+
+        Component::ExplicitUniversalType => Feature::Selectors2,
+
+        Component::AttributeInNoNamespaceExists { .. } => Feature::Selectors2,
+        Component::AttributeInNoNamespace {
+          operator,
+          case_sensitivity,
+          ..
+        } => {
+          if *case_sensitivity != ParsedCaseSensitivity::CaseSensitive {
+            Feature::CaseInsensitive
+          } else {
+            match operator {
+              AttrSelectorOperator::Equal | AttrSelectorOperator::Includes | AttrSelectorOperator::DashMatch => {
+                Feature::Selectors2
+              }
+              AttrSelectorOperator::Prefix | AttrSelectorOperator::Substring | AttrSelectorOperator::Suffix => {
+                Feature::Selectors3
+              }
+            }
+          }
+        }
+        Component::AttributeOther(attr) => match attr.operation {
+          ParsedAttrSelectorOperation::Exists => Feature::Selectors2,
+          ParsedAttrSelectorOperation::WithValue {
+            operator,
+            case_sensitivity,
+            ..
+          } => {
+            if case_sensitivity != ParsedCaseSensitivity::CaseSensitive {
+              Feature::CaseInsensitive
+            } else {
+              match operator {
+                AttrSelectorOperator::Equal | AttrSelectorOperator::Includes | AttrSelectorOperator::DashMatch => {
+                  Feature::Selectors2
+                }
+                AttrSelectorOperator::Prefix | AttrSelectorOperator::Substring | AttrSelectorOperator::Suffix => {
+                  Feature::Selectors3
+                }
+              }
+            }
+          }
+        },
+
+        Component::Empty | Component::Root => Feature::Selectors3,
+        Component::Negation(selectors) => {
+          // :not() selector list is not forgiving.
+          if !targets.is_compatible(Feature::Selectors3) || !is_compatible(&*selectors, targets) {
+            return false;
+          }
+          continue;
+        }
+
+        Component::Nth(data) => match data.ty {
+          NthType::Child if data.a == 0 && data.b == 1 => Feature::Selectors2,
+          NthType::Col | NthType::LastCol => return false,
+          _ => Feature::Selectors3,
+        },
+        Component::NthOf(n) => {
+          if !targets.is_compatible(Feature::NthChildOf) || !is_compatible(n.selectors(), targets) {
+            return false;
+          }
+          continue;
+        }
+
+        // These support forgiving selector lists, so no need to check nested selectors.
+        Component::Is(selectors) => {
+          // ... except if we are going to unwrap them.
+          if should_unwrap_is(selectors) && is_compatible(selectors, targets) {
+            continue;
+          }
+
+          Feature::IsSelector
+        }
+        Component::Where(_) | Component::Nesting => Feature::IsSelector,
+        Component::Any(..) => return false,
+        Component::Has(selectors) => {
+          if !targets.is_compatible(Feature::HasSelector) || !is_compatible(&*selectors, targets) {
+            return false;
+          }
+          continue;
+        }
+
+        Component::Scope | Component::Host(_) | Component::Slotted(_) => Feature::Shadowdomv1,
+
+        Component::Part(_) => Feature::PartPseudo,
+
+        Component::NonTSPseudoClass(pseudo) => {
+          match pseudo {
+            PseudoClass::Link
+            | PseudoClass::Visited
+            | PseudoClass::Active
+            | PseudoClass::Hover
+            | PseudoClass::Focus
+            | PseudoClass::Lang { languages: _ } => Feature::Selectors2,
+
+            PseudoClass::Checked | PseudoClass::Disabled | PseudoClass::Enabled | PseudoClass::Target => {
+              Feature::Selectors3
+            }
+
+            PseudoClass::AnyLink(prefix) if *prefix == VendorPrefix::None => Feature::AnyLink,
+            PseudoClass::Indeterminate => Feature::IndeterminatePseudo,
+
+            PseudoClass::Fullscreen(prefix) if *prefix == VendorPrefix::None => Feature::Fullscreen,
+
+            PseudoClass::FocusVisible => Feature::FocusVisible,
+            PseudoClass::FocusWithin => Feature::FocusWithin,
+            PseudoClass::Default => Feature::DefaultPseudo,
+            PseudoClass::Dir { direction: _ } => Feature::DirSelector,
+            PseudoClass::Optional => Feature::OptionalPseudo,
+            PseudoClass::PlaceholderShown(prefix) if *prefix == VendorPrefix::None => Feature::PlaceholderShown,
+
+            PseudoClass::ReadOnly(prefix) | PseudoClass::ReadWrite(prefix) if *prefix == VendorPrefix::None => {
+              Feature::ReadOnlyWrite
+            }
+
+            PseudoClass::Valid | PseudoClass::Invalid | PseudoClass::Required => Feature::FormValidation,
+
+            PseudoClass::InRange | PseudoClass::OutOfRange => Feature::InOutOfRange,
+
+            PseudoClass::Autofill(prefix) if *prefix == VendorPrefix::None => Feature::Autofill,
+
+            // Experimental, no browser support.
+            PseudoClass::Current
+            | PseudoClass::Past
+            | PseudoClass::Future
+            | PseudoClass::Playing
+            | PseudoClass::Paused
+            | PseudoClass::Seeking
+            | PseudoClass::Stalled
+            | PseudoClass::Buffering
+            | PseudoClass::Muted
+            | PseudoClass::VolumeLocked
+            | PseudoClass::TargetWithin
+            | PseudoClass::LocalLink
+            | PseudoClass::Blank
+            | PseudoClass::UserInvalid
+            | PseudoClass::UserValid
+            | PseudoClass::Defined
+            | PseudoClass::ActiveViewTransition
+            | PseudoClass::ActiveViewTransitionType { .. } => return false,
+
+            PseudoClass::Custom { .. } | _ => return false,
+          }
+        }
+
+        Component::PseudoElement(pseudo) => match pseudo {
+          PseudoElement::After | PseudoElement::Before => Feature::Gencontent,
+          PseudoElement::FirstLine => Feature::FirstLine,
+          PseudoElement::FirstLetter => Feature::FirstLetter,
+          PseudoElement::DetailsContent => Feature::DetailsContent,
+          PseudoElement::TargetText => Feature::TargetText,
+          PseudoElement::Selection(prefix) if *prefix == VendorPrefix::None => Feature::Selection,
+          PseudoElement::Placeholder(prefix) if *prefix == VendorPrefix::None => Feature::Placeholder,
+          PseudoElement::Marker => Feature::MarkerPseudo,
+          PseudoElement::Backdrop(prefix) if *prefix == VendorPrefix::None => Feature::Dialog,
+          PseudoElement::Cue => Feature::Cue,
+          PseudoElement::CueFunction { selector: _ } => Feature::CueFunction,
+          PseudoElement::ViewTransition
+          | PseudoElement::ViewTransitionNew { .. }
+          | PseudoElement::ViewTransitionOld { .. }
+          | PseudoElement::ViewTransitionGroup { .. }
+          | PseudoElement::ViewTransitionImagePair { .. } => Feature::ViewTransition,
+          PseudoElement::PickerFunction { identifier: _ } => Feature::Picker,
+          PseudoElement::PickerIcon => Feature::PickerIcon,
+          PseudoElement::Checkmark => Feature::Checkmark,
+          PseudoElement::Custom { name: _ } | _ => return false,
+        },
+
+        Component::Combinator(combinator) => match combinator {
+          Combinator::Child | Combinator::NextSibling => Feature::Selectors2,
+          Combinator::LaterSibling => Feature::Selectors3,
+          _ => continue,
+        },
+      };
+
+      if !targets.is_compatible(feature) {
+        return false;
+      }
+    }
+  }
+
+  true
+}
+
+/// Returns whether two selector lists are equivalent, i.e. the same minus any vendor prefix differences.
+pub(crate) fn is_equivalent<'i>(selectors: &[Selector<'i>], other: &[Selector<'i>]) -> bool {
+  if selectors.len() != other.len() {
+    return false;
+  }
+
+  for (i, a) in selectors.iter().enumerate() {
+    let b = &other[i];
+    if a.len() != b.len() {
+      return false;
+    }
+
+    for (a, b) in a.iter_raw_match_order().zip(b.iter_raw_match_order()) {
+      let is_equivalent = match (a, b) {
+        (Component::NonTSPseudoClass(a_ps), Component::NonTSPseudoClass(b_ps)) => a_ps.is_equivalent(b_ps),
+        (Component::PseudoElement(a_pe), Component::PseudoElement(b_pe)) => a_pe.is_equivalent(b_pe),
+        (Component::Any(_, a), Component::Is(b))
+        | (Component::Is(a), Component::Any(_, b))
+        | (Component::Any(_, a), Component::Any(_, b))
+        | (Component::Is(a), Component::Is(b)) => is_equivalent(&*a, &*b),
+        (a, b) => a == b,
+      };
+
+      if !is_equivalent {
+        return false;
+      }
+    }
+  }
+
+  true
+}
+
+/// Returns the vendor prefix (if any) used in the given selector list.
+/// If multiple vendor prefixes are seen, this is invalid, and an empty result is returned.
+pub(crate) fn get_prefix(selectors: &SelectorList) -> VendorPrefix {
+  let mut prefix = VendorPrefix::empty();
+  for selector in &selectors.0 {
+    for component in selector.iter_raw_match_order() {
+      let p = match component {
+        // Return none rather than empty for these so that we call downlevel_selectors.
+        Component::NonTSPseudoClass(PseudoClass::Lang { .. })
+        | Component::NonTSPseudoClass(PseudoClass::Dir { .. })
+        | Component::Is(..)
+        | Component::Where(..)
+        | Component::Has(..)
+        | Component::Negation(..) => VendorPrefix::None,
+        Component::Any(prefix, _) => *prefix,
+        Component::NonTSPseudoClass(pc) => pc.get_prefix(),
+        Component::PseudoElement(pe) => pe.get_prefix(),
+        _ => VendorPrefix::empty(),
+      };
+
+      if !p.is_empty() {
+        // Allow none to be mixed with a prefix.
+        let prefix_without_none = prefix - VendorPrefix::None;
+        if prefix_without_none.is_empty() || prefix_without_none == p {
+          prefix |= p;
+        } else {
+          return VendorPrefix::empty();
+        }
+      }
+    }
+  }
+
+  prefix
+}
+
+const RTL_LANGS: &[&str] = &[
+  "ae", "ar", "arc", "bcc", "bqi", "ckb", "dv", "fa", "glk", "he", "ku", "mzn", "nqo", "pnb", "ps", "sd", "ug",
+  "ur", "yi",
+];
+
+/// Downlevels the given selectors to be compatible with the given browser targets.
+/// Returns the necessary vendor prefixes.
+pub(crate) fn downlevel_selectors(selectors: &mut [Selector], targets: Targets) -> VendorPrefix {
+  let mut necessary_prefixes = VendorPrefix::empty();
+  for selector in selectors {
+    for component in selector.iter_mut_raw_match_order() {
+      necessary_prefixes |= downlevel_component(component, targets);
+    }
+  }
+
+  necessary_prefixes
+}
+
+fn downlevel_component<'i>(component: &mut Component<'i>, targets: Targets) -> VendorPrefix {
+  match component {
+    Component::NonTSPseudoClass(pc) => {
+      match pc {
+        PseudoClass::Dir { direction: dir } => {
+          if should_compile!(targets, DirSelector) {
+            *component = downlevel_dir(*dir, targets);
+            downlevel_component(component, targets)
+          } else {
+            VendorPrefix::empty()
+          }
+        }
+        PseudoClass::Lang { languages: langs } => {
+          // :lang() with multiple languages is not supported everywhere.
+          // compile this to :is(:lang(a), :lang(b)) etc.
+          if langs.len() > 1 && should_compile!(targets, LangSelectorList) {
+            *component = Component::Is(lang_list_to_selectors(&langs));
+            downlevel_component(component, targets)
+          } else {
+            VendorPrefix::empty()
+          }
+        }
+        _ => pc.get_necessary_prefixes(targets),
+      }
+    }
+    Component::PseudoElement(pe) => pe.get_necessary_prefixes(targets),
+    Component::Is(selectors) => {
+      let mut necessary_prefixes = downlevel_selectors(&mut **selectors, targets);
+
+      // Convert :is to :-webkit-any/:-moz-any if needed.
+      // All selectors must be simple, no combinators are supported.
+      if should_compile!(targets, IsSelector)
+        && !should_unwrap_is(selectors)
+        && selectors.iter().all(|selector| !selector.has_combinator())
+      {
+        necessary_prefixes |= targets.prefixes(VendorPrefix::None, crate::prefixes::Feature::AnyPseudo)
+      } else {
+        necessary_prefixes |= VendorPrefix::None
+      }
+
+      necessary_prefixes
+    }
+    Component::Negation(selectors) => {
+      let mut necessary_prefixes = downlevel_selectors(&mut **selectors, targets);
+
+      // Downlevel :not(.a, .b) -> :not(:is(.a, .b)) if not list is unsupported.
+      // We need to use :is() / :-webkit-any() rather than :not(.a):not(.b) to ensure the specificity is equivalent.
+      // https://drafts.csswg.org/selectors/#specificity-rules
+      if selectors.len() > 1 && should_compile!(targets, NotSelectorList) {
+        *component =
+          Component::Negation(vec![Selector::from(Component::Is(selectors.clone()))].into_boxed_slice());
+
+        if should_compile!(targets, IsSelector) {
+          necessary_prefixes |= targets.prefixes(VendorPrefix::None, crate::prefixes::Feature::AnyPseudo)
+        } else {
+          necessary_prefixes |= VendorPrefix::None
+        }
+      }
+
+      necessary_prefixes
+    }
+    Component::Where(selectors) | Component::Any(_, selectors) | Component::Has(selectors) => {
+      downlevel_selectors(&mut **selectors, targets)
+    }
+    _ => VendorPrefix::empty(),
+  }
+}
+
+fn lang_list_to_selectors<'i>(langs: &Vec<CowArcStr<'i>>) -> Box<[Selector<'i>]> {
+  langs
+    .iter()
+    .map(|lang| {
+      Selector::from(Component::NonTSPseudoClass(PseudoClass::Lang {
+        languages: vec![lang.clone()],
+      }))
+    })
+    .collect::<Vec<Selector>>()
+    .into_boxed_slice()
+}
+
+fn downlevel_dir<'i>(dir: Direction, targets: Targets) -> Component<'i> {
+  // Convert :dir to :lang. If supported, use a list of languages in a single :lang,
+  // otherwise, use :is/:not, which may be further downleveled to e.g. :-webkit-any.
+  let langs = RTL_LANGS.iter().map(|lang| (*lang).into()).collect();
+  if !should_compile!(targets, LangSelectorList) {
+    let c = Component::NonTSPseudoClass(PseudoClass::Lang { languages: langs });
+    if dir == Direction::Ltr {
+      Component::Negation(vec![Selector::from(c)].into_boxed_slice())
+    } else {
+      c
+    }
+  } else {
+    if dir == Direction::Ltr {
+      Component::Negation(lang_list_to_selectors(&langs))
+    } else {
+      Component::Is(lang_list_to_selectors(&langs))
+    }
+  }
+}
+
+/// Determines whether a selector list contains only unused selectors.
+/// A selector is considered unused if it contains a class or id component that exists in the set of unused symbols.
+pub(crate) fn is_unused(
+  selectors: &mut std::slice::Iter<Selector>,
+  unused_symbols: &HashSet<String>,
+  parent_is_unused: bool,
+) -> bool {
+  if unused_symbols.is_empty() {
+    return false;
+  }
+
+  selectors.all(|selector| {
+    for component in selector.iter_raw_match_order() {
+      match component {
+        Component::Class(name) | Component::ID(name) => {
+          if unused_symbols.contains(&name.0.to_string()) {
+            return true;
+          }
+        }
+        Component::Is(is) | Component::Where(is) | Component::Any(_, is) => {
+          if is_unused(&mut is.iter(), unused_symbols, parent_is_unused) {
+            return true;
+          }
+        }
+        Component::Nesting => {
+          if parent_is_unused {
+            return true;
+          }
+        }
+        _ => {}
+      }
+    }
+
+    false
+  })
+}
+
+/// Returns whether the selector has any class or id components.
+pub(crate) fn is_pure_css_modules_selector(selector: &Selector) -> bool {
+  use parcel_selectors::parser::Component;
+  selector.iter_raw_match_order().any(|c| match c {
+    Component::Class(_) | Component::ID(_) => true,
+    Component::Is(s) | Component::Where(s) | Component::Has(s) | Component::Any(_, s) | Component::Negation(s) => {
+      s.iter().any(is_pure_css_modules_selector)
+    }
+    Component::NthOf(nth) => nth.selectors().iter().any(is_pure_css_modules_selector),
+    Component::Slotted(s) => is_pure_css_modules_selector(&s),
+    Component::Host(s) => s.as_ref().map(is_pure_css_modules_selector).unwrap_or(false),
+    Component::NonTSPseudoClass(pc) => match pc {
+      PseudoClass::Local { selector } => is_pure_css_modules_selector(&*selector),
+      _ => false,
+    },
+    _ => false,
+  })
+}
+
+#[cfg(feature = "visitor")]
+#[cfg_attr(docsrs, doc(cfg(feature = "visitor")))]
+impl<'i, T: Visit<'i, T, V>, V: ?Sized + Visitor<'i, T>> Visit<'i, T, V> for SelectorList<'i> {
+  const CHILD_TYPES: VisitTypes = VisitTypes::SELECTORS;
+
+  fn visit(&mut self, visitor: &mut V) -> Result<(), V::Error> {
+    if visitor.visit_types().contains(VisitTypes::SELECTORS) {
+      visitor.visit_selector_list(self)
+    } else {
+      self.visit_children(visitor)
+    }
+  }
+
+  fn visit_children(&mut self, visitor: &mut V) -> Result<(), V::Error> {
+    self.0.iter_mut().try_for_each(|selector| Visit::visit(selector, visitor))
+  }
+}
+
+#[cfg(feature = "visitor")]
+#[cfg_attr(docsrs, doc(cfg(feature = "visitor")))]
+impl<'i, T: Visit<'i, T, V>, V: ?Sized + Visitor<'i, T>> Visit<'i, T, V> for Selector<'i> {
+  const CHILD_TYPES: VisitTypes = VisitTypes::SELECTORS;
+
+  fn visit(&mut self, visitor: &mut V) -> Result<(), V::Error> {
+    visitor.visit_selector(self)
+  }
+
+  fn visit_children(&mut self, _visitor: &mut V) -> Result<(), V::Error> {
+    Ok(())
+  }
+}
+
+impl<'i> ParseWithOptions<'i> for Selector<'i> {
+  fn parse_with_options<'t>(
+    input: &mut Parser<'i, 't>,
+    options: &ParserOptions<'_, 'i>,
+  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    Selector::parse(
+      &SelectorParser {
+        is_nesting_allowed: true,
+        options: &options,
+      },
+      input,
+    )
+  }
+}
+
+impl<'i> ParseWithOptions<'i> for SelectorList<'i> {
+  fn parse_with_options<'t>(
+    input: &mut Parser<'i, 't>,
+    options: &ParserOptions<'_, 'i>,
+  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    SelectorList::parse(
+      &SelectorParser {
+        is_nesting_allowed: true,
+        options: &options,
+      },
+      input,
+      parcel_selectors::parser::ParseErrorRecovery::DiscardList,
+      parcel_selectors::parser::NestingRequirement::None,
+    )
+  }
+}
diff --git a/src/serialization.rs b/src/serialization.rs
new file mode 100644
index 0000000..08483c7
--- /dev/null
+++ b/src/serialization.rs
@@ -0,0 +1,34 @@
+#![allow(non_snake_case)]
+
+macro_rules! wrapper {
+  ($name: ident, $value: ident $(, $t: ty)?) => {
+    #[derive(serde::Serialize, serde::Deserialize)]
+    #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+    pub struct $name<T $(= $t)?> {
+      $value: T,
+    }
+
+    impl<'de, T> $name<T> {
+      pub fn serialize<S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
+      where
+        S: serde::Serializer,
+        T: serde::Serialize,
+      {
+        let wrapper = $name { $value: value };
+        serde::Serialize::serialize(&wrapper, serializer)
+      }
+
+      pub fn deserialize<D>(deserializer: D) -> Result<T, D::Error>
+      where
+        D: serde::Deserializer<'de>,
+        T: serde::Deserialize<'de>,
+      {
+        let v: $name<T> = serde::Deserialize::deserialize(deserializer)?;
+        Ok(v.$value)
+      }
+    }
+  };
+}
+
+wrapper!(ValueWrapper, value);
+wrapper!(PrefixWrapper, vendorPrefix, crate::vendor_prefix::VendorPrefix);
diff --git a/src/stylesheet.rs b/src/stylesheet.rs
new file mode 100644
index 0000000..990a09b
--- /dev/null
+++ b/src/stylesheet.rs
@@ -0,0 +1,426 @@
+//! CSS style sheets and style attributes.
+//!
+//! A [StyleSheet](StyleSheet) represents a `.css` file or `<style>` element in HTML.
+//! A [StyleAttribute](StyleAttribute) represents an inline `style` attribute in HTML.
+
+use crate::context::{DeclarationContext, PropertyHandlerContext};
+use crate::css_modules::{hash, CssModule, CssModuleExports, CssModuleReferences};
+use crate::declaration::{DeclarationBlock, DeclarationHandler};
+use crate::dependencies::Dependency;
+use crate::error::{Error, ErrorLocation, MinifyErrorKind, ParserError, PrinterError, PrinterErrorKind};
+use crate::parser::{DefaultAtRule, DefaultAtRuleParser, TopLevelRuleParser};
+use crate::printer::Printer;
+use crate::rules::{CssRule, CssRuleList, MinifyContext};
+use crate::targets::{should_compile, Targets, TargetsWithSupportsScope};
+use crate::traits::{AtRuleParser, ToCss};
+use crate::values::string::CowArcStr;
+#[cfg(feature = "visitor")]
+use crate::visitor::{Visit, VisitTypes, Visitor};
+use cssparser::{Parser, ParserInput, StyleSheetParser};
+#[cfg(feature = "sourcemap")]
+use parcel_sourcemap::SourceMap;
+use std::collections::{HashMap, HashSet};
+
+pub use crate::parser::{ParserFlags, ParserOptions};
+pub use crate::printer::PrinterOptions;
+pub use crate::printer::PseudoClasses;
+
+/// A CSS style sheet, representing a `.css` file or inline `<style>` element.
+///
+/// Style sheets can be parsed from a string, constructed from scratch,
+/// or created using a [Bundler](super::bundler::Bundler). Then, they can be
+/// minified and transformed for a set of target browsers, and serialied to a string.
+///
+/// # Example
+///
+/// ```
+/// use lightningcss::stylesheet::{
+///   StyleSheet, ParserOptions, MinifyOptions, PrinterOptions
+/// };
+///
+/// // Parse a style sheet from a string.
+/// let mut stylesheet = StyleSheet::parse(
+///   r#"
+///   .foo {
+///     color: red;
+///   }
+///
+///   .bar {
+///     color: red;
+///   }
+///   "#,
+///   ParserOptions::default()
+/// ).unwrap();
+///
+/// // Minify the stylesheet.
+/// stylesheet.minify(MinifyOptions::default()).unwrap();
+///
+/// // Serialize it to a string.
+/// let res = stylesheet.to_css(PrinterOptions::default()).unwrap();
+/// assert_eq!(res.code, ".foo, .bar {\n  color: red;\n}\n");
+/// ```
+#[derive(Debug)]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(rename_all = "camelCase")
+)]
+#[cfg_attr(
+  feature = "jsonschema",
+  derive(schemars::JsonSchema),
+  schemars(rename = "StyleSheet", bound = "T: schemars::JsonSchema")
+)]
+pub struct StyleSheet<'i, 'o, T = DefaultAtRule> {
+  /// A list of top-level rules within the style sheet.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  pub rules: CssRuleList<'i, T>,
+  /// A list of file names for all source files included within the style sheet.
+  /// Sources are referenced by index in the `loc` property of each rule.
+  pub sources: Vec<String>,
+  /// The source map URL extracted from the original style sheet.
+  pub(crate) source_map_urls: Vec<Option<String>>,
+  /// The license comments that appeared at the start of the file.
+  pub license_comments: Vec<CowArcStr<'i>>,
+  /// A list of content hashes for all source files included within the style sheet.
+  /// This is only set if CSS modules are enabled and the pattern includes [content-hash].
+  #[cfg_attr(feature = "serde", serde(skip))]
+  pub(crate) content_hashes: Option<Vec<String>>,
+  #[cfg_attr(feature = "serde", serde(skip))]
+  /// The options the style sheet was originally parsed with.
+  options: ParserOptions<'o, 'i>,
+}
+
+/// Options for the `minify` function of a [StyleSheet](StyleSheet)
+/// or [StyleAttribute](StyleAttribute).
+#[derive(Default)]
+pub struct MinifyOptions {
+  /// Targets to compile the CSS for.
+  pub targets: Targets,
+  /// A list of known unused symbols, including CSS class names,
+  /// ids, and `@keyframe` names. The declarations of these will be removed.
+  pub unused_symbols: HashSet<String>,
+}
+
+/// A result returned from `to_css`, including the serialize CSS
+/// and other metadata depending on the input options.
+#[derive(Debug)]
+pub struct ToCssResult {
+  /// Serialized CSS code.
+  pub code: String,
+  /// A map of CSS module exports, if the `css_modules` option was
+  /// enabled during parsing.
+  pub exports: Option<CssModuleExports>,
+  /// A map of CSS module references, if the `css_modules` config
+  /// had `dashed_idents` enabled.
+  pub references: Option<CssModuleReferences>,
+  /// A list of dependencies (e.g. `@import` or `url()`) found in
+  /// the style sheet, if the `analyze_dependencies` option is enabled.
+  pub dependencies: Option<Vec<Dependency>>,
+}
+
+impl<'i, 'o> StyleSheet<'i, 'o, DefaultAtRule> {
+  /// Parse a style sheet from a string.
+  pub fn parse(code: &'i str, options: ParserOptions<'o, 'i>) -> Result<Self, Error<ParserError<'i>>> {
+    Self::parse_with(code, options, &mut DefaultAtRuleParser)
+  }
+}
+
+impl<'i, 'o, T> StyleSheet<'i, 'o, T>
+where
+  T: ToCss + Clone,
+{
+  /// Creates a new style sheet with the given source filenames and rules.
+  pub fn new(
+    sources: Vec<String>,
+    rules: CssRuleList<'i, T>,
+    options: ParserOptions<'o, 'i>,
+  ) -> StyleSheet<'i, 'o, T> {
+    StyleSheet {
+      sources,
+      source_map_urls: Vec::new(),
+      license_comments: Vec::new(),
+      content_hashes: None,
+      rules,
+      options,
+    }
+  }
+
+  /// Parse a style sheet from a string.
+  pub fn parse_with<P: AtRuleParser<'i, AtRule = T>>(
+    code: &'i str,
+    mut options: ParserOptions<'o, 'i>,
+    at_rule_parser: &mut P,
+  ) -> Result<Self, Error<ParserError<'i>>> {
+    let mut input = ParserInput::new(&code);
+    let mut parser = Parser::new(&mut input);
+    let mut license_comments = Vec::new();
+
+    let mut content_hashes = None;
+    if let Some(config) = &options.css_modules {
+      if config.pattern.has_content_hash() {
+        content_hashes = Some(vec![hash(
+          &code,
+          matches!(config.pattern.segments[0], crate::css_modules::Segment::ContentHash),
+        )]);
+      }
+    }
+
+    let mut state = parser.state();
+    while let Ok(token) = parser.next_including_whitespace_and_comments() {
+      match token {
+        cssparser::Token::WhiteSpace(..) => {}
+        cssparser::Token::Comment(comment) if comment.starts_with('!') => {
+          license_comments.push((*comment).into());
+        }
+        cssparser::Token::Comment(comment) if comment.contains("cssmodules-pure-no-check") => {
+          if let Some(css_modules) = &mut options.css_modules {
+            css_modules.pure = false;
+          }
+        }
+        _ => break,
+      }
+      state = parser.state();
+    }
+    parser.reset(&state);
+
+    let mut rules = CssRuleList(vec![]);
+    let mut rule_parser = TopLevelRuleParser::new(&mut options, at_rule_parser, &mut rules);
+    let mut rule_list_parser = StyleSheetParser::new(&mut parser, &mut rule_parser);
+
+    while let Some(rule) = rule_list_parser.next() {
+      match rule {
+        Ok(()) => {}
+        Err((e, _)) => {
+          let options = &mut rule_list_parser.parser.options;
+          if options.error_recovery {
+            options.warn(e);
+            continue;
+          }
+
+          return Err(Error::from(e, options.filename.clone()));
+        }
+      }
+    }
+
+    Ok(StyleSheet {
+      sources: vec![options.filename.clone()],
+      source_map_urls: vec![parser.current_source_map_url().map(|s| s.to_owned())],
+      content_hashes,
+      rules,
+      license_comments,
+      options,
+    })
+  }
+
+  /// Returns the source map URL for the source at the given index.
+  pub fn source_map_url(&self, source_index: usize) -> Option<&String> {
+    self.source_map_urls.get(source_index)?.as_ref()
+  }
+
+  /// Returns the inline source map associated with the source at the given index.
+  #[cfg(feature = "sourcemap")]
+  #[cfg_attr(docsrs, doc(cfg(feature = "sourcemap")))]
+  pub fn source_map(&self, source_index: usize) -> Option<SourceMap> {
+    SourceMap::from_data_url("/", self.source_map_url(source_index)?).ok()
+  }
+
+  /// Minify and transform the style sheet for the provided browser targets.
+  pub fn minify(&mut self, options: MinifyOptions) -> Result<(), Error<MinifyErrorKind>> {
+    let context = PropertyHandlerContext::new(options.targets, &options.unused_symbols);
+    let mut handler = DeclarationHandler::default();
+    let mut important_handler = DeclarationHandler::default();
+
+    // @custom-media rules may be defined after they are referenced, but may only be defined at the top level
+    // of a stylesheet. Do a pre-scan here and create a lookup table by name.
+    let custom_media = if self.options.flags.contains(ParserFlags::CUSTOM_MEDIA)
+      && should_compile!(options.targets, CustomMediaQueries)
+    {
+      let mut custom_media = HashMap::new();
+      for rule in &self.rules.0 {
+        if let CssRule::CustomMedia(rule) = rule {
+          custom_media.insert(rule.name.0.clone(), rule.clone());
+        }
+      }
+      Some(custom_media)
+    } else {
+      None
+    };
+
+    let mut ctx = MinifyContext {
+      targets: TargetsWithSupportsScope::new(options.targets),
+      handler: &mut handler,
+      important_handler: &mut important_handler,
+      handler_context: context,
+      unused_symbols: &options.unused_symbols,
+      custom_media,
+      css_modules: self.options.css_modules.is_some(),
+      pure_css_modules: self.options.css_modules.as_ref().map(|c| c.pure).unwrap_or_default(),
+    };
+
+    self.rules.minify(&mut ctx, false).map_err(|e| Error {
+      kind: e.kind,
+      loc: Some(ErrorLocation::new(
+        e.loc,
+        self.sources[e.loc.source_index as usize].clone(),
+      )),
+    })?;
+
+    Ok(())
+  }
+
+  /// Serialize the style sheet to a CSS string.
+  pub fn to_css(&self, options: PrinterOptions) -> Result<ToCssResult, Error<PrinterErrorKind>> {
+    // Make sure we always have capacity > 0: https://github.com/napi-rs/napi-rs/issues/1124.
+    let mut dest = String::with_capacity(1);
+    let project_root = options.project_root.clone();
+    let mut printer = Printer::new(&mut dest, options);
+
+    #[cfg(feature = "sourcemap")]
+    {
+      printer.sources = Some(&self.sources);
+    }
+
+    #[cfg(feature = "sourcemap")]
+    if printer.source_map.is_some() {
+      printer.source_maps = self.sources.iter().enumerate().map(|(i, _)| self.source_map(i)).collect();
+    }
+
+    for comment in &self.license_comments {
+      printer.write_str("/*")?;
+      printer.write_str_with_newlines(comment)?;
+      printer.write_str_with_newlines("*/\n")?;
+    }
+
+    if let Some(config) = &self.options.css_modules {
+      let mut references = HashMap::new();
+      printer.css_module = Some(CssModule::new(
+        config,
+        &self.sources,
+        project_root,
+        &mut references,
+        &self.content_hashes,
+      ));
+
+      self.rules.to_css(&mut printer)?;
+      printer.newline()?;
+
+      Ok(ToCssResult {
+        dependencies: printer.dependencies,
+        exports: Some(std::mem::take(
+          &mut printer.css_module.unwrap().exports_by_source_index[0],
+        )),
+        code: dest,
+        references: Some(references),
+      })
+    } else {
+      self.rules.to_css(&mut printer)?;
+      printer.newline()?;
+
+      Ok(ToCssResult {
+        dependencies: printer.dependencies,
+        code: dest,
+        exports: None,
+        references: None,
+      })
+    }
+  }
+}
+
+#[cfg(feature = "visitor")]
+#[cfg_attr(docsrs, doc(cfg(feature = "visitor")))]
+impl<'i, 'o, T, V> Visit<'i, T, V> for StyleSheet<'i, 'o, T>
+where
+  T: Visit<'i, T, V>,
+  V: ?Sized + Visitor<'i, T>,
+{
+  const CHILD_TYPES: VisitTypes = VisitTypes::all();
+
+  fn visit(&mut self, visitor: &mut V) -> Result<(), V::Error> {
+    visitor.visit_stylesheet(self)
+  }
+
+  fn visit_children(&mut self, visitor: &mut V) -> Result<(), V::Error> {
+    self.rules.visit(visitor)
+  }
+}
+
+/// An inline style attribute, as in HTML or SVG.
+///
+/// Style attributes can be parsed from a string, minified and transformed
+/// for a set of target browsers, and serialied to a string.
+///
+/// # Example
+///
+/// ```
+/// use lightningcss::stylesheet::{
+///   StyleAttribute, ParserOptions, MinifyOptions, PrinterOptions
+/// };
+///
+/// // Parse a style sheet from a string.
+/// let mut style = StyleAttribute::parse(
+///   "color: yellow; font-family: 'Helvetica';",
+///   ParserOptions::default()
+/// ).unwrap();
+///
+/// // Minify the stylesheet.
+/// style.minify(MinifyOptions::default());
+///
+/// // Serialize it to a string.
+/// let res = style.to_css(PrinterOptions::default()).unwrap();
+/// assert_eq!(res.code, "color: #ff0; font-family: Helvetica");
+/// ```
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub struct StyleAttribute<'i> {
+  /// The declarations in the style attribute.
+  pub declarations: DeclarationBlock<'i>,
+  #[cfg_attr(feature = "visitor", skip_visit)]
+  sources: Vec<String>,
+}
+
+impl<'i> StyleAttribute<'i> {
+  /// Parses a style attribute from a string.
+  pub fn parse(
+    code: &'i str,
+    options: ParserOptions<'_, 'i>,
+  ) -> Result<StyleAttribute<'i>, Error<ParserError<'i>>> {
+    let mut input = ParserInput::new(&code);
+    let mut parser = Parser::new(&mut input);
+    Ok(StyleAttribute {
+      declarations: DeclarationBlock::parse(&mut parser, &options).map_err(|e| Error::from(e, "".into()))?,
+      sources: vec![options.filename],
+    })
+  }
+
+  /// Minify and transform the style attribute for the provided browser targets.
+  pub fn minify(&mut self, options: MinifyOptions) {
+    let mut context = PropertyHandlerContext::new(options.targets, &options.unused_symbols);
+    let mut handler = DeclarationHandler::default();
+    let mut important_handler = DeclarationHandler::default();
+    context.context = DeclarationContext::StyleAttribute;
+    self.declarations.minify(&mut handler, &mut important_handler, &mut context);
+  }
+
+  /// Serializes the style attribute to a CSS string.
+  pub fn to_css(&self, options: PrinterOptions) -> Result<ToCssResult, PrinterError> {
+    #[cfg(feature = "sourcemap")]
+    assert!(
+      options.source_map.is_none(),
+      "Source maps are not supported for style attributes"
+    );
+
+    // Make sure we always have capacity > 0: https://github.com/napi-rs/napi-rs/issues/1124.
+    let mut dest = String::with_capacity(1);
+    let mut printer = Printer::new(&mut dest, options);
+    printer.sources = Some(&self.sources);
+
+    self.declarations.to_css(&mut printer)?;
+
+    Ok(ToCssResult {
+      dependencies: printer.dependencies,
+      code: dest,
+      exports: None,
+      references: None,
+    })
+  }
+}
diff --git a/src/targets.rs b/src/targets.rs
new file mode 100644
index 0000000..c568e28
--- /dev/null
+++ b/src/targets.rs
@@ -0,0 +1,313 @@
+//! Browser target options.
+
+#![allow(missing_docs)]
+
+use std::borrow::Borrow;
+
+use crate::vendor_prefix::VendorPrefix;
+use bitflags::bitflags;
+#[cfg(any(feature = "serde", feature = "nodejs"))]
+use serde::{Deserialize, Serialize};
+
+/// Browser versions to compile CSS for.
+///
+/// Versions are represented as a single 24-bit integer, with one byte
+/// per `major.minor.patch` component.
+///
+/// # Example
+///
+/// This example represents a target of Safari 13.2.0.
+///
+/// ```
+/// use lightningcss::targets::Browsers;
+///
+/// let targets = Browsers {
+///   safari: Some((13 << 16) | (2 << 8)),
+///   ..Browsers::default()
+/// };
+/// ```
+#[derive(Debug, Clone, Copy, Default)]
+#[cfg_attr(any(feature = "serde", feature = "nodejs"), derive(Serialize, Deserialize))]
+#[allow(missing_docs)]
+pub struct Browsers {
+  pub android: Option<u32>,
+  pub chrome: Option<u32>,
+  pub edge: Option<u32>,
+  pub firefox: Option<u32>,
+  pub ie: Option<u32>,
+  pub ios_saf: Option<u32>,
+  pub opera: Option<u32>,
+  pub safari: Option<u32>,
+  pub samsung: Option<u32>,
+}
+
+#[cfg(feature = "browserslist")]
+#[cfg_attr(docsrs, doc(cfg(feature = "browserslist")))]
+impl Browsers {
+  /// Parses a list of browserslist queries into Lightning CSS targets.
+  pub fn from_browserslist<S: AsRef<str>, I: IntoIterator<Item = S>>(
+    query: I,
+  ) -> Result<Option<Browsers>, browserslist::Error> {
+    use browserslist::{resolve, Opts};
+
+    Self::from_distribs(resolve(query, &Opts::default())?)
+  }
+
+  #[cfg(not(target_arch = "wasm32"))]
+  /// Finds browserslist configuration, selects queries by environment and loads the resulting queries into LightningCSS targets.
+  ///
+  /// Configuration resolution is modeled after the original `browserslist` nodeJS package.
+  /// The configuration is resolved in the following order:
+  ///
+  /// - If a `BROWSERSLIST` environment variable is present, then load targets from its value. This is analog to the `--targets` CLI option.
+  ///   Example: `BROWSERSLIST="firefox ESR" lightningcss [OPTIONS] <INPUT_FILE>`
+  /// - If a `BROWSERSLIST_CONFIG` environment variable is present, then resolve the file at the provided path.
+  ///   Then parse and use targets from `package.json` or any browserslist configuration file pointed to by the environment variable.
+  ///   Example: `BROWSERSLIST_CONFIG="../config/browserslist" lightningcss [OPTIONS] <INPUT_FILE>`
+  /// - If none of the above apply, then find, parse and use targets from the first `browserslist`, `.browserslistrc`
+  ///   or `package.json` configuration file in any parent directory.
+  ///
+  /// When using parsed configuration from `browserslist`, `.browserslistrc` or `package.json` configuration files,
+  /// the environment determined by:
+  ///
+  /// - the `BROWSERSLIST_ENV` environment variable if present,
+  /// - otherwise the `NODE_ENV` environment varialbe if present,
+  /// - otherwise `production` is used.
+  ///
+  /// If no targets are found for the resulting environment, then the `defaults` configuration section is used.
+  pub fn load_browserslist() -> Result<Option<Browsers>, browserslist::Error> {
+    use browserslist::{execute, Opts};
+
+    Self::from_distribs(execute(&Opts::default())?)
+  }
+
+  fn from_distribs(distribs: Vec<browserslist::Distrib>) -> Result<Option<Browsers>, browserslist::Error> {
+    let mut browsers = Browsers::default();
+    let mut has_any = false;
+    for distrib in distribs {
+      macro_rules! browser {
+        ($browser: ident) => {{
+          if let Some(v) = parse_version(distrib.version()) {
+            if browsers.$browser.is_none() || v < browsers.$browser.unwrap() {
+              browsers.$browser = Some(v);
+              has_any = true;
+            }
+          }
+        }};
+      }
+
+      match distrib.name() {
+        "android" => browser!(android),
+        "chrome" | "and_chr" => browser!(chrome),
+        "edge" => browser!(edge),
+        "firefox" | "and_ff" => browser!(firefox),
+        "ie" => browser!(ie),
+        "ios_saf" => browser!(ios_saf),
+        "opera" | "op_mob" => browser!(opera),
+        "safari" => browser!(safari),
+        "samsung" => browser!(samsung),
+        _ => {}
+      }
+    }
+
+    if !has_any {
+      return Ok(None);
+    }
+
+    Ok(Some(browsers))
+  }
+}
+
+#[cfg(feature = "browserslist")]
+fn parse_version(version: &str) -> Option<u32> {
+  let version = version.split('-').next();
+  if version.is_none() {
+    return None;
+  }
+
+  let mut version = version.unwrap().split('.');
+  let major = version.next().and_then(|v| v.parse::<u32>().ok());
+  if let Some(major) = major {
+    let minor = version.next().and_then(|v| v.parse::<u32>().ok()).unwrap_or(0);
+    let patch = version.next().and_then(|v| v.parse::<u32>().ok()).unwrap_or(0);
+    let v: u32 = (major & 0xff) << 16 | (minor & 0xff) << 8 | (patch & 0xff);
+    return Some(v);
+  }
+
+  None
+}
+
+bitflags! {
+  /// Features to explicitly enable or disable.
+  #[derive(Debug, Default, Clone, Copy, Hash, Eq, PartialEq)]
+  pub struct Features: u32 {
+    const Nesting = 1 << 0;
+    const NotSelectorList = 1 << 1;
+    const DirSelector = 1 << 2;
+    const LangSelectorList = 1 << 3;
+    const IsSelector = 1 << 4;
+    const TextDecorationThicknessPercent = 1 << 5;
+    const MediaIntervalSyntax = 1 << 6;
+    const MediaRangeSyntax = 1 << 7;
+    const CustomMediaQueries = 1 << 8;
+    const ClampFunction = 1 << 9;
+    const ColorFunction = 1 << 10;
+    const OklabColors = 1 << 11;
+    const LabColors = 1 << 12;
+    const P3Colors = 1 << 13;
+    const HexAlphaColors = 1 << 14;
+    const SpaceSeparatedColorNotation = 1 << 15;
+    const FontFamilySystemUi = 1 << 16;
+    const DoublePositionGradients = 1 << 17;
+    const VendorPrefixes = 1 << 18;
+    const LogicalProperties = 1 << 19;
+    const LightDark = 1 << 20;
+    const Selectors = Self::Nesting.bits() | Self::NotSelectorList.bits() | Self::DirSelector.bits() | Self::LangSelectorList.bits() | Self::IsSelector.bits();
+    const MediaQueries = Self::MediaIntervalSyntax.bits() | Self::MediaRangeSyntax.bits() | Self::CustomMediaQueries.bits();
+    const Colors = Self::ColorFunction.bits() | Self::OklabColors.bits() | Self::LabColors.bits() | Self::P3Colors.bits() | Self::HexAlphaColors.bits() | Self::SpaceSeparatedColorNotation.bits() | Self::LightDark.bits();
+  }
+}
+
+pub(crate) trait FeaturesIterator: Sized + Iterator {
+  fn try_union_all<T>(&mut self) -> Option<Features>
+  where
+    Self: Iterator<Item = Option<T>>,
+    T: Borrow<Features>,
+  {
+    self.try_fold(Features::empty(), |a, b| b.map(|b| a | *b.borrow()))
+  }
+}
+
+impl<I> FeaturesIterator for I where I: Iterator {}
+
+/// Target browsers and features to compile.
+#[derive(Debug, Clone, Copy, Default)]
+pub struct Targets {
+  /// Browser targets to compile the CSS for.
+  pub browsers: Option<Browsers>,
+  /// Features that should always be compiled, even when supported by targets.
+  pub include: Features,
+  /// Features that should never be compiled, even when unsupported by targets.
+  pub exclude: Features,
+}
+
+impl From<Browsers> for Targets {
+  fn from(browsers: Browsers) -> Self {
+    Self {
+      browsers: Some(browsers),
+      ..Default::default()
+    }
+  }
+}
+
+impl From<Option<Browsers>> for Targets {
+  fn from(browsers: Option<Browsers>) -> Self {
+    Self {
+      browsers,
+      ..Default::default()
+    }
+  }
+}
+
+impl Targets {
+  pub(crate) fn is_compatible(&self, feature: crate::compat::Feature) -> bool {
+    self.browsers.map(|targets| feature.is_compatible(targets)).unwrap_or(true)
+  }
+
+  pub(crate) fn should_compile(&self, feature: crate::compat::Feature, flag: Features) -> bool {
+    self.include.contains(flag) || (!self.exclude.contains(flag) && !self.is_compatible(feature))
+  }
+
+  pub(crate) fn should_compile_logical(&self, feature: crate::compat::Feature) -> bool {
+    self.should_compile(feature, Features::LogicalProperties)
+  }
+
+  pub(crate) fn should_compile_selectors(&self) -> bool {
+    self.include.intersects(Features::Selectors)
+      || (!self.exclude.intersects(Features::Selectors) && self.browsers.is_some())
+  }
+
+  pub(crate) fn prefixes(&self, prefix: VendorPrefix, feature: crate::prefixes::Feature) -> VendorPrefix {
+    if prefix.contains(VendorPrefix::None) && !self.exclude.contains(Features::VendorPrefixes) {
+      if self.include.contains(Features::VendorPrefixes) {
+        VendorPrefix::all()
+      } else {
+        self.browsers.map(|browsers| feature.prefixes_for(browsers)).unwrap_or(prefix)
+      }
+    } else {
+      prefix
+    }
+  }
+}
+
+#[derive(Debug)]
+pub(crate) struct TargetsWithSupportsScope {
+  stack: Vec<Features>,
+  pub(crate) current: Targets,
+}
+
+impl TargetsWithSupportsScope {
+  pub fn new(targets: Targets) -> Self {
+    Self {
+      stack: Vec::new(),
+      current: targets,
+    }
+  }
+
+  /// Returns true if inserted
+  pub fn enter_supports(&mut self, features: Features) -> bool {
+    if features.is_empty() || self.current.exclude.contains(features) {
+      // Already excluding all features
+      return false;
+    }
+
+    let newly_excluded = features - self.current.exclude;
+    self.stack.push(newly_excluded);
+    self.current.exclude.insert(newly_excluded);
+    true
+  }
+
+  /// Should be only called if inserted
+  pub fn exit_supports(&mut self) {
+    if let Some(last) = self.stack.pop() {
+      self.current.exclude.remove(last);
+    }
+  }
+}
+
+#[test]
+fn supports_scope_correctly() {
+  let mut targets = TargetsWithSupportsScope::new(Targets::default());
+  assert!(!targets.current.exclude.contains(Features::OklabColors));
+  assert!(!targets.current.exclude.contains(Features::LabColors));
+  assert!(!targets.current.exclude.contains(Features::P3Colors));
+
+  targets.enter_supports(Features::OklabColors | Features::LabColors);
+  assert!(targets.current.exclude.contains(Features::OklabColors));
+  assert!(targets.current.exclude.contains(Features::LabColors));
+
+  targets.enter_supports(Features::P3Colors | Features::LabColors);
+  assert!(targets.current.exclude.contains(Features::OklabColors));
+  assert!(targets.current.exclude.contains(Features::LabColors));
+  assert!(targets.current.exclude.contains(Features::P3Colors));
+
+  targets.exit_supports();
+  assert!(targets.current.exclude.contains(Features::OklabColors));
+  assert!(targets.current.exclude.contains(Features::LabColors));
+  assert!(!targets.current.exclude.contains(Features::P3Colors));
+
+  targets.exit_supports();
+  assert!(!targets.current.exclude.contains(Features::OklabColors));
+  assert!(!targets.current.exclude.contains(Features::LabColors));
+}
+
+macro_rules! should_compile {
+  ($targets: expr, $feature: ident) => {
+    $targets.should_compile(
+      crate::compat::Feature::$feature,
+      crate::targets::Features::$feature,
+    )
+  };
+}
+
+pub(crate) use should_compile;
diff --git a/src/traits.rs b/src/traits.rs
new file mode 100644
index 0000000..4aecaf7
--- /dev/null
+++ b/src/traits.rs
@@ -0,0 +1,392 @@
+//! Traits for parsing and serializing CSS.
+
+use crate::context::PropertyHandlerContext;
+use crate::declaration::{DeclarationBlock, DeclarationList};
+use crate::error::{ParserError, PrinterError};
+use crate::printer::Printer;
+use crate::properties::{Property, PropertyId};
+use crate::stylesheet::{ParserOptions, PrinterOptions};
+use crate::targets::{Browsers, Targets};
+use crate::vendor_prefix::VendorPrefix;
+use cssparser::*;
+
+#[cfg(feature = "into_owned")]
+pub use static_self::IntoOwned;
+
+/// Trait for things that can be parsed from CSS syntax.
+pub trait Parse<'i>: Sized {
+  /// Parse a value of this type using an existing parser.
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>>;
+
+  /// Parse a value from a string.
+  ///
+  /// (This is a convenience wrapper for `parse` and probably should not be overridden.)
+  fn parse_string(input: &'i str) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let mut input = ParserInput::new(input);
+    let mut parser = Parser::new(&mut input);
+    let result = Self::parse(&mut parser)?;
+    parser.expect_exhausted()?;
+    Ok(result)
+  }
+}
+
+pub(crate) use lightningcss_derive::Parse;
+
+impl<'i, T: Parse<'i>> Parse<'i> for Option<T> {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    Ok(input.try_parse(T::parse).ok())
+  }
+}
+
+impl<'i, T: Parse<'i>> Parse<'i> for Box<T> {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    Ok(Box::new(T::parse(input)?))
+  }
+}
+
+/// Trait for things that can be parsed from CSS syntax and require ParserOptions.
+pub trait ParseWithOptions<'i>: Sized {
+  /// Parse a value of this type with the given options.
+  fn parse_with_options<'t>(
+    input: &mut Parser<'i, 't>,
+    options: &ParserOptions<'_, 'i>,
+  ) -> Result<Self, ParseError<'i, ParserError<'i>>>;
+
+  /// Parse a value from a string with the given options.
+  fn parse_string_with_options(
+    input: &'i str,
+    options: ParserOptions<'_, 'i>,
+  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let mut input = ParserInput::new(input);
+    let mut parser = Parser::new(&mut input);
+    Self::parse_with_options(&mut parser, &options)
+  }
+}
+
+impl<'i, T: Parse<'i>> ParseWithOptions<'i> for T {
+  #[inline]
+  fn parse_with_options<'t>(
+    input: &mut Parser<'i, 't>,
+    _options: &ParserOptions,
+  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    T::parse(input)
+  }
+}
+
+/// Trait for things the can serialize themselves in CSS syntax.
+pub trait ToCss {
+  /// Serialize `self` in CSS syntax, writing to `dest`.
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write;
+
+  /// Serialize `self` in CSS syntax and return a string.
+  ///
+  /// (This is a convenience wrapper for `to_css` and probably should not be overridden.)
+  #[inline]
+  fn to_css_string(&self, options: PrinterOptions) -> Result<String, PrinterError> {
+    let mut s = String::new();
+    let mut printer = Printer::new(&mut s, options);
+    self.to_css(&mut printer)?;
+    Ok(s)
+  }
+}
+
+pub(crate) use lightningcss_derive::ToCss;
+
+impl<'a, T> ToCss for &'a T
+where
+  T: ToCss + ?Sized,
+{
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    (*self).to_css(dest)
+  }
+}
+
+impl<T: ToCss> ToCss for Box<T> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    (**self).to_css(dest)
+  }
+}
+
+impl<T: ToCss> ToCss for Option<T> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    if let Some(v) = self {
+      v.to_css(dest)?;
+    }
+    Ok(())
+  }
+}
+
+pub(crate) trait PropertyHandler<'i>: Sized {
+  fn handle_property(
+    &mut self,
+    property: &Property<'i>,
+    dest: &mut DeclarationList<'i>,
+    context: &mut PropertyHandlerContext<'i, '_>,
+  ) -> bool;
+  fn finalize(&mut self, dest: &mut DeclarationList<'i>, context: &mut PropertyHandlerContext<'i, '_>);
+}
+
+pub(crate) mod private {
+  pub trait TryAdd<T> {
+    fn try_add(&self, other: &T) -> Option<T>;
+  }
+
+  pub trait AddInternal {
+    fn add(self, other: Self) -> Self;
+  }
+}
+
+pub(crate) trait FromStandard<T>: Sized {
+  fn from_standard(val: &T) -> Option<Self>;
+}
+
+pub(crate) trait FallbackValues: Sized {
+  fn get_fallbacks(&mut self, targets: Targets) -> Vec<Self>;
+}
+
+/// Trait for shorthand properties.
+pub(crate) trait Shorthand<'i>: Sized {
+  /// Returns a shorthand from the longhand properties defined in the given declaration block.
+  fn from_longhands(decls: &DeclarationBlock<'i>, vendor_prefix: VendorPrefix) -> Option<(Self, bool)>;
+
+  /// Returns a list of longhand property ids for this shorthand.
+  fn longhands(vendor_prefix: VendorPrefix) -> Vec<PropertyId<'static>>;
+
+  /// Returns a longhand property for this shorthand.
+  fn longhand(&self, property_id: &PropertyId) -> Option<Property<'i>>;
+
+  /// Updates this shorthand from a longhand property.
+  fn set_longhand(&mut self, property: &Property<'i>) -> Result<(), ()>;
+}
+
+/// A trait for values that support binary operations.
+pub trait Op {
+  /// Returns the result of the operation in the same type.
+  fn op<F: FnOnce(f32, f32) -> f32>(&self, rhs: &Self, op: F) -> Self;
+  /// Returns the result of the operation in a different type.
+  fn op_to<T, F: FnOnce(f32, f32) -> T>(&self, rhs: &Self, op: F) -> T;
+}
+
+macro_rules! impl_op {
+  ($t: ty, $trait: ident $(:: $x: ident)*, $op: ident) => {
+    impl $trait$(::$x)* for $t {
+      type Output = $t;
+
+      fn $op(self, rhs: Self) -> Self::Output {
+        self.op(&rhs, $trait$(::$x)*::$op)
+      }
+    }
+  };
+}
+
+pub(crate) use impl_op;
+use smallvec::SmallVec;
+
+/// A trait for values that potentially support a binary operation (e.g. if they have the same unit).
+pub trait TryOp: Sized {
+  /// Returns the result of the operation in the same type, if possible.
+  fn try_op<F: FnOnce(f32, f32) -> f32>(&self, rhs: &Self, op: F) -> Option<Self>;
+  /// Returns the result of the operation in a different type, if possible.
+  fn try_op_to<T, F: FnOnce(f32, f32) -> T>(&self, rhs: &Self, op: F) -> Option<T>;
+}
+
+impl<T: Op> TryOp for T {
+  fn try_op<F: FnOnce(f32, f32) -> f32>(&self, rhs: &Self, op: F) -> Option<Self> {
+    Some(self.op(rhs, op))
+  }
+
+  fn try_op_to<U, F: FnOnce(f32, f32) -> U>(&self, rhs: &Self, op: F) -> Option<U> {
+    Some(self.op_to(rhs, op))
+  }
+}
+
+/// A trait for values that can be mapped by applying a function.
+pub trait Map {
+  /// Returns the result of the operation.
+  fn map<F: FnOnce(f32) -> f32>(&self, op: F) -> Self;
+}
+
+/// A trait for values that can potentially be mapped.
+pub trait TryMap: Sized {
+  /// Returns the result of the operation, if possible.
+  fn try_map<F: FnOnce(f32) -> f32>(&self, op: F) -> Option<Self>;
+}
+
+impl<T: Map> TryMap for T {
+  fn try_map<F: FnOnce(f32) -> f32>(&self, op: F) -> Option<Self> {
+    Some(self.map(op))
+  }
+}
+
+/// A trait for values that can return a sign.
+pub trait Sign {
+  /// Returns the sign of the value.
+  fn sign(&self) -> f32;
+
+  /// Returns whether the value is positive.
+  fn is_sign_positive(&self) -> bool {
+    f32::is_sign_positive(self.sign())
+  }
+
+  /// Returns whether the value is negative.
+  fn is_sign_negative(&self) -> bool {
+    f32::is_sign_negative(self.sign())
+  }
+}
+
+/// A trait for values that can potentially return a sign.
+pub trait TrySign {
+  /// Returns the sign of the value, if possible.
+  fn try_sign(&self) -> Option<f32>;
+
+  /// Returns whether the value is positive. If not possible, returns false.
+  fn is_sign_positive(&self) -> bool {
+    self.try_sign().map_or(false, |s| f32::is_sign_positive(s))
+  }
+
+  /// Returns whether the value is negative. If not possible, returns false.
+  fn is_sign_negative(&self) -> bool {
+    self.try_sign().map_or(false, |s| f32::is_sign_negative(s))
+  }
+}
+
+impl<T: Sign> TrySign for T {
+  fn try_sign(&self) -> Option<f32> {
+    Some(self.sign())
+  }
+}
+
+/// A trait for values that can be zero.
+pub trait Zero {
+  /// Returns the zero value.
+  fn zero() -> Self;
+
+  /// Returns whether the value is zero.
+  fn is_zero(&self) -> bool;
+}
+
+/// A trait for values that can check if they are compatible with browser targets.
+pub trait IsCompatible {
+  /// Returns whether the value is compatible with all of the given browser targets.
+  fn is_compatible(&self, browsers: Browsers) -> bool;
+}
+
+impl<T: IsCompatible> IsCompatible for SmallVec<[T; 1]> {
+  fn is_compatible(&self, browsers: Browsers) -> bool {
+    self.iter().all(|v| v.is_compatible(browsers))
+  }
+}
+
+impl<T: IsCompatible> IsCompatible for Vec<T> {
+  fn is_compatible(&self, browsers: Browsers) -> bool {
+    self.iter().all(|v| v.is_compatible(browsers))
+  }
+}
+
+/// A trait to provide parsing of custom at-rules.
+///
+/// For example, there could be different implementations for top-level at-rules
+/// (`@media`, `@font-face`, …)
+/// and for page-margin rules inside `@page`.
+///
+/// Default implementations that reject all at-rules are provided,
+/// so that `impl AtRuleParser<(), ()> for ... {}` can be used
+/// for using `DeclarationListParser` to parse a declarations list with only qualified rules.
+///
+/// Note: this trait is copied from cssparser and modified to provide parser options.
+pub trait AtRuleParser<'i>: Sized {
+  /// The intermediate representation of prelude of an at-rule.
+  type Prelude;
+
+  /// The finished representation of an at-rule.
+  type AtRule;
+
+  /// The error type that is included in the ParseError value that can be returned.
+  type Error: 'i;
+
+  /// Parse the prelude of an at-rule with the given `name`.
+  ///
+  /// Return the representation of the prelude and the type of at-rule,
+  /// or `Err(())` to ignore the entire at-rule as invalid.
+  ///
+  /// The prelude is the part after the at-keyword
+  /// and before the `;` semicolon or `{ /* ... */ }` block.
+  ///
+  /// At-rule name matching should be case-insensitive in the ASCII range.
+  /// This can be done with `std::ascii::Ascii::eq_ignore_ascii_case`,
+  /// or with the `match_ignore_ascii_case!` macro.
+  ///
+  /// The given `input` is a "delimited" parser
+  /// that ends wherever the prelude should end.
+  /// (Before the next semicolon, the next `{`, or the end of the current block.)
+  fn parse_prelude<'t>(
+    &mut self,
+    name: CowRcStr<'i>,
+    input: &mut Parser<'i, 't>,
+    options: &ParserOptions<'_, 'i>,
+  ) -> Result<Self::Prelude, ParseError<'i, Self::Error>> {
+    let _ = name;
+    let _ = input;
+    let _ = options;
+    Err(input.new_error(BasicParseErrorKind::AtRuleInvalid(name)))
+  }
+
+  /// End an at-rule which doesn't have block. Return the finished
+  /// representation of the at-rule.
+  ///
+  /// The location passed in is source location of the start of the prelude.
+  /// `is_nested` indicates whether the rule is nested inside a style rule.
+  ///
+  /// This is only called when either the `;` semicolon indeed follows the prelude,
+  /// or parser is at the end of the input.
+  fn rule_without_block(
+    &mut self,
+    prelude: Self::Prelude,
+    start: &ParserState,
+    options: &ParserOptions<'_, 'i>,
+    is_nested: bool,
+  ) -> Result<Self::AtRule, ()> {
+    let _ = prelude;
+    let _ = start;
+    let _ = options;
+    let _ = is_nested;
+    Err(())
+  }
+
+  /// Parse the content of a `{ /* ... */ }` block for the body of the at-rule.
+  ///
+  /// The location passed in is source location of the start of the prelude.
+  /// `is_nested` indicates whether the rule is nested inside a style rule.
+  ///
+  /// Return the finished representation of the at-rule
+  /// as returned by `RuleListParser::next` or `DeclarationListParser::next`,
+  /// or `Err(())` to ignore the entire at-rule as invalid.
+  ///
+  /// This is only called when a block was found following the prelude.
+  fn parse_block<'t>(
+    &mut self,
+    prelude: Self::Prelude,
+    start: &ParserState,
+    input: &mut Parser<'i, 't>,
+    options: &ParserOptions<'_, 'i>,
+    is_nested: bool,
+  ) -> Result<Self::AtRule, ParseError<'i, Self::Error>> {
+    let _ = prelude;
+    let _ = start;
+    let _ = input;
+    let _ = options;
+    let _ = is_nested;
+    Err(input.new_error(BasicParseErrorKind::AtRuleBodyInvalid))
+  }
+}
diff --git a/src/values/alpha.rs b/src/values/alpha.rs
new file mode 100644
index 0000000..3649339
--- /dev/null
+++ b/src/values/alpha.rs
@@ -0,0 +1,38 @@
+//! CSS alpha values, used to represent opacity.
+
+use super::percentage::NumberOrPercentage;
+use crate::error::{ParserError, PrinterError};
+use crate::printer::Printer;
+use crate::traits::{Parse, ToCss};
+#[cfg(feature = "visitor")]
+use crate::visitor::Visit;
+use cssparser::*;
+
+/// A CSS [`<alpha-value>`](https://www.w3.org/TR/css-color-4/#typedef-alpha-value),
+/// used to represent opacity.
+///
+/// Parses either a `<number>` or `<percentage>`, but is always stored and serialized as a number.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(transparent))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub struct AlphaValue(pub f32);
+
+impl<'i> Parse<'i> for AlphaValue {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    match NumberOrPercentage::parse(input)? {
+      NumberOrPercentage::Percentage(percent) => Ok(AlphaValue(percent.0)),
+      NumberOrPercentage::Number(number) => Ok(AlphaValue(number)),
+    }
+  }
+}
+
+impl ToCss for AlphaValue {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    self.0.to_css(dest)
+  }
+}
diff --git a/src/values/angle.rs b/src/values/angle.rs
new file mode 100644
index 0000000..dff23a2
--- /dev/null
+++ b/src/values/angle.rs
@@ -0,0 +1,300 @@
+//! CSS angle values.
+
+use super::calc::Calc;
+use super::length::serialize_dimension;
+use super::number::CSSNumber;
+use super::percentage::DimensionPercentage;
+use crate::error::{ParserError, PrinterError};
+use crate::printer::Printer;
+use crate::traits::{
+  impl_op,
+  private::{AddInternal, TryAdd},
+  Map, Op, Parse, Sign, ToCss, Zero,
+};
+#[cfg(feature = "visitor")]
+use crate::visitor::Visit;
+use cssparser::*;
+use std::f32::consts::PI;
+
+/// A CSS [`<angle>`](https://www.w3.org/TR/css-values-4/#angles) value.
+///
+/// Angles may be explicit or computed by `calc()`, but are always stored and serialized
+/// as their computed value.
+#[derive(Debug, Clone)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "visitor", visit(visit_angle, ANGLES))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", content = "value", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum Angle {
+  /// An angle in degrees. There are 360 degrees in a full circle.
+  Deg(CSSNumber),
+  /// An angle in radians. There are 2π radians in a full circle.
+  Rad(CSSNumber),
+  /// An angle in gradians. There are 400 gradians in a full circle.
+  Grad(CSSNumber),
+  /// An angle in turns. There is 1 turn in a full circle.
+  Turn(CSSNumber),
+}
+
+impl<'i> Parse<'i> for Angle {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    Self::parse_internal(input, false)
+  }
+}
+
+impl Angle {
+  /// Parses an angle, allowing unitless zero values.
+  pub fn parse_with_unitless_zero<'i, 't>(
+    input: &mut Parser<'i, 't>,
+  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    Self::parse_internal(input, true)
+  }
+
+  fn parse_internal<'i, 't>(
+    input: &mut Parser<'i, 't>,
+    allow_unitless_zero: bool,
+  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    match input.try_parse(Calc::parse) {
+      Ok(Calc::Value(v)) => return Ok(*v),
+      // Angles are always compatible, so they will always compute to a value.
+      Ok(_) => return Err(input.new_custom_error(ParserError::InvalidValue)),
+      _ => {}
+    }
+
+    let location = input.current_source_location();
+    let token = input.next()?;
+    match *token {
+      Token::Dimension { value, ref unit, .. } => {
+        match_ignore_ascii_case! { unit,
+          "deg" => Ok(Angle::Deg(value)),
+          "grad" => Ok(Angle::Grad(value)),
+          "turn" => Ok(Angle::Turn(value)),
+          "rad" => Ok(Angle::Rad(value)),
+          _ => return Err(location.new_unexpected_token_error(token.clone())),
+        }
+      }
+      Token::Number { value, .. } if value == 0.0 && allow_unitless_zero => Ok(Angle::zero()),
+      ref token => return Err(location.new_unexpected_token_error(token.clone())),
+    }
+  }
+}
+
+impl<'i> TryFrom<&Token<'i>> for Angle {
+  type Error = ();
+
+  fn try_from(token: &Token) -> Result<Self, Self::Error> {
+    match token {
+      Token::Dimension { value, ref unit, .. } => match_ignore_ascii_case! { unit,
+        "deg" => Ok(Angle::Deg(*value)),
+        "grad" => Ok(Angle::Grad(*value)),
+        "turn" => Ok(Angle::Turn(*value)),
+        "rad" => Ok(Angle::Rad(*value)),
+        _ => Err(()),
+      },
+      _ => Err(()),
+    }
+  }
+}
+
+impl ToCss for Angle {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    let (value, unit) = match self {
+      Angle::Deg(val) => (*val, "deg"),
+      Angle::Grad(val) => (*val, "grad"),
+      Angle::Rad(val) => {
+        let deg = self.to_degrees();
+        // We print 5 digits of precision by default.
+        // Switch to degrees if there are an even number of them.
+        if (deg * 100000.0).round().fract() == 0.0 {
+          (deg, "deg")
+        } else {
+          (*val, "rad")
+        }
+      }
+      Angle::Turn(val) => (*val, "turn"),
+    };
+
+    serialize_dimension(value, unit, dest)
+  }
+}
+
+impl Angle {
+  /// Prints the angle, allowing unitless zero values.
+  pub fn to_css_with_unitless_zero<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    if self.is_zero() {
+      (0.0).to_css(dest)
+    } else {
+      self.to_css(dest)
+    }
+  }
+}
+
+impl Angle {
+  /// Returns the angle in radians.
+  pub fn to_radians(&self) -> CSSNumber {
+    const RAD_PER_DEG: f32 = PI / 180.0;
+    match self {
+      Angle::Deg(deg) => deg * RAD_PER_DEG,
+      Angle::Rad(rad) => *rad,
+      Angle::Grad(grad) => grad * 180.0 / 200.0 * RAD_PER_DEG,
+      Angle::Turn(turn) => turn * 360.0 * RAD_PER_DEG,
+    }
+  }
+
+  /// Returns the angle in degrees.
+  pub fn to_degrees(&self) -> CSSNumber {
+    const DEG_PER_RAD: f32 = 180.0 / PI;
+    match self {
+      Angle::Deg(deg) => *deg,
+      Angle::Rad(rad) => rad * DEG_PER_RAD,
+      Angle::Grad(grad) => grad * 180.0 / 200.0,
+      Angle::Turn(turn) => turn * 360.0,
+    }
+  }
+}
+
+impl Zero for Angle {
+  fn is_zero(&self) -> bool {
+    use Angle::*;
+    match self {
+      Deg(v) | Rad(v) | Grad(v) | Turn(v) => *v == 0.0,
+    }
+  }
+
+  fn zero() -> Self {
+    Angle::Deg(0.0)
+  }
+}
+
+impl Into<Calc<Angle>> for Angle {
+  fn into(self) -> Calc<Angle> {
+    Calc::Value(Box::new(self))
+  }
+}
+
+impl TryFrom<Calc<Angle>> for Angle {
+  type Error = ();
+
+  fn try_from(calc: Calc<Angle>) -> Result<Angle, ()> {
+    match calc {
+      Calc::Value(v) => Ok(*v),
+      _ => Err(()),
+    }
+  }
+}
+
+impl std::ops::Mul<CSSNumber> for Angle {
+  type Output = Self;
+
+  fn mul(self, other: CSSNumber) -> Angle {
+    match self {
+      Angle::Deg(v) => Angle::Deg(v * other),
+      Angle::Rad(v) => Angle::Rad(v * other),
+      Angle::Grad(v) => Angle::Grad(v * other),
+      Angle::Turn(v) => Angle::Turn(v * other),
+    }
+  }
+}
+
+impl AddInternal for Angle {
+  fn add(self, other: Self) -> Self {
+    self + other
+  }
+}
+
+impl TryAdd<Angle> for Angle {
+  fn try_add(&self, other: &Angle) -> Option<Angle> {
+    Some(Angle::Deg(self.to_degrees() + other.to_degrees()))
+  }
+}
+
+impl std::cmp::PartialEq<Angle> for Angle {
+  fn eq(&self, other: &Angle) -> bool {
+    self.to_degrees() == other.to_degrees()
+  }
+}
+
+impl std::cmp::PartialOrd<Angle> for Angle {
+  fn partial_cmp(&self, other: &Angle) -> Option<std::cmp::Ordering> {
+    self.to_degrees().partial_cmp(&other.to_degrees())
+  }
+}
+
+impl Op for Angle {
+  fn op<F: FnOnce(f32, f32) -> f32>(&self, other: &Self, op: F) -> Self {
+    match (self, other) {
+      (Angle::Deg(a), Angle::Deg(b)) => Angle::Deg(op(*a, *b)),
+      (Angle::Rad(a), Angle::Rad(b)) => Angle::Rad(op(*a, *b)),
+      (Angle::Grad(a), Angle::Grad(b)) => Angle::Grad(op(*a, *b)),
+      (Angle::Turn(a), Angle::Turn(b)) => Angle::Turn(op(*a, *b)),
+      (a, b) => Angle::Deg(op(a.to_degrees(), b.to_degrees())),
+    }
+  }
+
+  fn op_to<T, F: FnOnce(f32, f32) -> T>(&self, other: &Self, op: F) -> T {
+    match (self, other) {
+      (Angle::Deg(a), Angle::Deg(b)) => op(*a, *b),
+      (Angle::Rad(a), Angle::Rad(b)) => op(*a, *b),
+      (Angle::Grad(a), Angle::Grad(b)) => op(*a, *b),
+      (Angle::Turn(a), Angle::Turn(b)) => op(*a, *b),
+      (a, b) => op(a.to_degrees(), b.to_degrees()),
+    }
+  }
+}
+
+impl Map for Angle {
+  fn map<F: FnOnce(f32) -> f32>(&self, op: F) -> Self {
+    match self {
+      Angle::Deg(deg) => Angle::Deg(op(*deg)),
+      Angle::Rad(rad) => Angle::Rad(op(*rad)),
+      Angle::Grad(grad) => Angle::Grad(op(*grad)),
+      Angle::Turn(turn) => Angle::Turn(op(*turn)),
+    }
+  }
+}
+
+impl Sign for Angle {
+  fn sign(&self) -> f32 {
+    match self {
+      Angle::Deg(v) | Angle::Rad(v) | Angle::Grad(v) | Angle::Turn(v) => v.sign(),
+    }
+  }
+}
+
+impl_op!(Angle, std::ops::Rem, rem);
+impl_op!(Angle, std::ops::Add, add);
+
+/// A CSS [`<angle-percentage>`](https://www.w3.org/TR/css-values-4/#typedef-angle-percentage) value.
+/// May be specified as either an angle or a percentage that resolves to an angle.
+pub type AnglePercentage = DimensionPercentage<Angle>;
+
+macro_rules! impl_try_from_angle {
+  ($t: ty) => {
+    impl TryFrom<crate::values::angle::Angle> for $t {
+      type Error = ();
+      fn try_from(_: crate::values::angle::Angle) -> Result<Self, Self::Error> {
+        Err(())
+      }
+    }
+
+    impl TryInto<crate::values::angle::Angle> for $t {
+      type Error = ();
+      fn try_into(self) -> Result<crate::values::angle::Angle, Self::Error> {
+        Err(())
+      }
+    }
+  };
+}
+
+pub(crate) use impl_try_from_angle;
diff --git a/src/values/calc.rs b/src/values/calc.rs
new file mode 100644
index 0000000..6022cf2
--- /dev/null
+++ b/src/values/calc.rs
@@ -0,0 +1,1045 @@
+//! Mathematical calculation functions and expressions.
+
+use crate::compat::Feature;
+use crate::error::{ParserError, PrinterError};
+use crate::macros::enum_property;
+use crate::printer::Printer;
+use crate::targets::{should_compile, Browsers};
+use crate::traits::private::AddInternal;
+use crate::traits::{IsCompatible, Parse, Sign, ToCss, TryMap, TryOp, TrySign};
+#[cfg(feature = "visitor")]
+use crate::visitor::Visit;
+use cssparser::*;
+
+use super::angle::Angle;
+use super::length::Length;
+use super::number::CSSNumber;
+use super::percentage::Percentage;
+use super::time::Time;
+
+/// A CSS [math function](https://www.w3.org/TR/css-values-4/#math-function).
+///
+/// Math functions may be used in most properties and values that accept numeric
+/// values, including lengths, percentages, angles, times, etc.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", content = "value", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum MathFunction<V> {
+  /// The [`calc()`](https://www.w3.org/TR/css-values-4/#calc-func) function.
+  Calc(Calc<V>),
+  /// The [`min()`](https://www.w3.org/TR/css-values-4/#funcdef-min) function.
+  Min(Vec<Calc<V>>),
+  /// The [`max()`](https://www.w3.org/TR/css-values-4/#funcdef-max) function.
+  Max(Vec<Calc<V>>),
+  /// The [`clamp()`](https://www.w3.org/TR/css-values-4/#funcdef-clamp) function.
+  Clamp(Calc<V>, Calc<V>, Calc<V>),
+  /// The [`round()`](https://www.w3.org/TR/css-values-4/#funcdef-round) function.
+  Round(RoundingStrategy, Calc<V>, Calc<V>),
+  /// The [`rem()`](https://www.w3.org/TR/css-values-4/#funcdef-rem) function.
+  Rem(Calc<V>, Calc<V>),
+  /// The [`mod()`](https://www.w3.org/TR/css-values-4/#funcdef-mod) function.
+  Mod(Calc<V>, Calc<V>),
+  /// The [`abs()`](https://drafts.csswg.org/css-values-4/#funcdef-abs) function.
+  Abs(Calc<V>),
+  /// The [`sign()`](https://drafts.csswg.org/css-values-4/#funcdef-sign) function.
+  Sign(Calc<V>),
+  /// The [`hypot()`](https://drafts.csswg.org/css-values-4/#funcdef-hypot) function.
+  Hypot(Vec<Calc<V>>),
+}
+
+impl<V: IsCompatible> IsCompatible for MathFunction<V> {
+  fn is_compatible(&self, browsers: Browsers) -> bool {
+    match self {
+      MathFunction::Calc(v) => Feature::CalcFunction.is_compatible(browsers) && v.is_compatible(browsers),
+      MathFunction::Min(v) => {
+        Feature::MinFunction.is_compatible(browsers) && v.iter().all(|v| v.is_compatible(browsers))
+      }
+      MathFunction::Max(v) => {
+        Feature::MaxFunction.is_compatible(browsers) && v.iter().all(|v| v.is_compatible(browsers))
+      }
+      MathFunction::Clamp(a, b, c) => {
+        Feature::ClampFunction.is_compatible(browsers)
+          && a.is_compatible(browsers)
+          && b.is_compatible(browsers)
+          && c.is_compatible(browsers)
+      }
+      MathFunction::Round(_, a, b) => {
+        Feature::RoundFunction.is_compatible(browsers) && a.is_compatible(browsers) && b.is_compatible(browsers)
+      }
+      MathFunction::Rem(a, b) => {
+        Feature::RemFunction.is_compatible(browsers) && a.is_compatible(browsers) && b.is_compatible(browsers)
+      }
+      MathFunction::Mod(a, b) => {
+        Feature::ModFunction.is_compatible(browsers) && a.is_compatible(browsers) && b.is_compatible(browsers)
+      }
+      MathFunction::Abs(v) => Feature::AbsFunction.is_compatible(browsers) && v.is_compatible(browsers),
+      MathFunction::Sign(v) => Feature::SignFunction.is_compatible(browsers) && v.is_compatible(browsers),
+      MathFunction::Hypot(v) => {
+        Feature::HypotFunction.is_compatible(browsers) && v.iter().all(|v| v.is_compatible(browsers))
+      }
+    }
+  }
+}
+
+enum_property! {
+  /// A [rounding strategy](https://www.w3.org/TR/css-values-4/#typedef-rounding-strategy),
+  /// as used in the `round()` function.
+  pub enum RoundingStrategy {
+    /// Round to the nearest integer.
+    Nearest,
+    /// Round up (ceil).
+    Up,
+    /// Round down (floor).
+    Down,
+    /// Round toward zero (truncate).
+    ToZero,
+  }
+}
+
+impl Default for RoundingStrategy {
+  fn default() -> Self {
+    RoundingStrategy::Nearest
+  }
+}
+
+fn round(value: f32, to: f32, strategy: RoundingStrategy) -> f32 {
+  let v = value / to;
+  match strategy {
+    RoundingStrategy::Down => v.floor() * to,
+    RoundingStrategy::Up => v.ceil() * to,
+    RoundingStrategy::Nearest => v.round() * to,
+    RoundingStrategy::ToZero => v.trunc() * to,
+  }
+}
+
+fn modulo(a: f32, b: f32) -> f32 {
+  ((a % b) + b) % b
+}
+
+impl<V: ToCss + std::ops::Mul<f32, Output = V> + TrySign + Clone + std::fmt::Debug> ToCss for MathFunction<V> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    match self {
+      MathFunction::Calc(calc) => {
+        dest.write_str("calc(")?;
+        calc.to_css(dest)?;
+        dest.write_char(')')
+      }
+      MathFunction::Min(args) => {
+        dest.write_str("min(")?;
+        let mut first = true;
+        for arg in args {
+          if first {
+            first = false;
+          } else {
+            dest.delim(',', false)?;
+          }
+          arg.to_css(dest)?;
+        }
+        dest.write_char(')')
+      }
+      MathFunction::Max(args) => {
+        dest.write_str("max(")?;
+        let mut first = true;
+        for arg in args {
+          if first {
+            first = false;
+          } else {
+            dest.delim(',', false)?;
+          }
+          arg.to_css(dest)?;
+        }
+        dest.write_char(')')
+      }
+      MathFunction::Clamp(a, b, c) => {
+        // If clamp() is unsupported by targets, output min()/max()
+        if should_compile!(dest.targets.current, ClampFunction) {
+          dest.write_str("max(")?;
+          a.to_css(dest)?;
+          dest.delim(',', false)?;
+          dest.write_str("min(")?;
+          b.to_css(dest)?;
+          dest.delim(',', false)?;
+          c.to_css(dest)?;
+          dest.write_str("))")?;
+          return Ok(());
+        }
+
+        dest.write_str("clamp(")?;
+        a.to_css(dest)?;
+        dest.delim(',', false)?;
+        b.to_css(dest)?;
+        dest.delim(',', false)?;
+        c.to_css(dest)?;
+        dest.write_char(')')
+      }
+      MathFunction::Round(strategy, a, b) => {
+        dest.write_str("round(")?;
+        if *strategy != RoundingStrategy::default() {
+          strategy.to_css(dest)?;
+          dest.delim(',', false)?;
+        }
+        a.to_css(dest)?;
+        dest.delim(',', false)?;
+        b.to_css(dest)?;
+        dest.write_char(')')
+      }
+      MathFunction::Rem(a, b) => {
+        dest.write_str("rem(")?;
+        a.to_css(dest)?;
+        dest.delim(',', false)?;
+        b.to_css(dest)?;
+        dest.write_char(')')
+      }
+      MathFunction::Mod(a, b) => {
+        dest.write_str("mod(")?;
+        a.to_css(dest)?;
+        dest.delim(',', false)?;
+        b.to_css(dest)?;
+        dest.write_char(')')
+      }
+      MathFunction::Abs(v) => {
+        dest.write_str("abs(")?;
+        v.to_css(dest)?;
+        dest.write_char(')')
+      }
+      MathFunction::Sign(v) => {
+        dest.write_str("sign(")?;
+        v.to_css(dest)?;
+        dest.write_char(')')
+      }
+      MathFunction::Hypot(args) => {
+        dest.write_str("hypot(")?;
+        let mut first = true;
+        for arg in args {
+          if first {
+            first = false;
+          } else {
+            dest.delim(',', false)?;
+          }
+          arg.to_css(dest)?;
+        }
+        dest.write_char(')')
+      }
+    }
+  }
+}
+
+/// A mathematical expression used within the [`calc()`](https://www.w3.org/TR/css-values-4/#calc-func) function.
+///
+/// This type supports generic value types. Values such as [Length](super::length::Length), [Percentage](super::percentage::Percentage),
+/// [Time](super::time::Time), and [Angle](super::angle::Angle) support `calc()` expressions.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", content = "value", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum Calc<V> {
+  /// A literal value.
+  Value(Box<V>),
+  /// A literal number.
+  Number(CSSNumber),
+  /// A sum of two calc expressions.
+  #[cfg_attr(feature = "visitor", skip_type)]
+  Sum(Box<Calc<V>>, Box<Calc<V>>),
+  /// A product of a number and another calc expression.
+  #[cfg_attr(feature = "visitor", skip_type)]
+  Product(CSSNumber, Box<Calc<V>>),
+  /// A math function, such as `calc()`, `min()`, or `max()`.
+  #[cfg_attr(feature = "visitor", skip_type)]
+  Function(Box<MathFunction<V>>),
+}
+
+impl<V: IsCompatible> IsCompatible for Calc<V> {
+  fn is_compatible(&self, browsers: Browsers) -> bool {
+    match self {
+      Calc::Sum(a, b) => a.is_compatible(browsers) && b.is_compatible(browsers),
+      Calc::Product(_, v) => v.is_compatible(browsers),
+      Calc::Function(f) => f.is_compatible(browsers),
+      Calc::Value(v) => v.is_compatible(browsers),
+      Calc::Number(..) => true,
+    }
+  }
+}
+
+enum_property! {
+  /// A mathematical constant.
+  pub enum Constant {
+    /// The base of the natural logarithm
+    "e": E,
+    /// The ratio of a circle’s circumference to its diameter
+    "pi": Pi,
+    /// infinity
+    "infinity": Infinity,
+    /// -infinity
+    "-infinity": NegativeInfinity,
+    /// Not a number.
+    "nan": Nan,
+  }
+}
+
+impl Into<f32> for Constant {
+  fn into(self) -> f32 {
+    use std::f32::consts;
+    use Constant::*;
+    match self {
+      E => consts::E,
+      Pi => consts::PI,
+      Infinity => f32::INFINITY,
+      NegativeInfinity => -f32::INFINITY,
+      Nan => f32::NAN,
+    }
+  }
+}
+
+impl<
+    'i,
+    V: Parse<'i>
+      + std::ops::Mul<f32, Output = V>
+      + AddInternal
+      + TryOp
+      + TryMap
+      + TrySign
+      + std::cmp::PartialOrd<V>
+      + Into<Calc<V>>
+      + TryFrom<Calc<V>>
+      + TryFrom<Angle>
+      + TryInto<Angle>
+      + Clone
+      + std::fmt::Debug,
+  > Parse<'i> for Calc<V>
+{
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    Self::parse_with(input, |_| None)
+  }
+}
+
+impl<
+    'i,
+    V: Parse<'i>
+      + std::ops::Mul<f32, Output = V>
+      + AddInternal
+      + TryOp
+      + TryMap
+      + TrySign
+      + std::cmp::PartialOrd<V>
+      + Into<Calc<V>>
+      + TryFrom<Calc<V>>
+      + TryFrom<Angle>
+      + TryInto<Angle>
+      + Clone
+      + std::fmt::Debug,
+  > Calc<V>
+{
+  pub(crate) fn parse_with<'t, Parse: Copy + Fn(&str) -> Option<Calc<V>>>(
+    input: &mut Parser<'i, 't>,
+    parse_ident: Parse,
+  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let location = input.current_source_location();
+    let f = input.expect_function()?;
+    match_ignore_ascii_case! { &f,
+      "calc" => {
+        let calc = input.parse_nested_block(|input| Calc::parse_sum(input, parse_ident))?;
+        match calc {
+          Calc::Value(_) | Calc::Number(_) => Ok(calc),
+          _ => Ok(Calc::Function(Box::new(MathFunction::Calc(calc))))
+        }
+      },
+      "min" => {
+        let mut args = input.parse_nested_block(|input| input.parse_comma_separated(|input| Calc::parse_sum(input, parse_ident)))?;
+        let mut reduced = Calc::reduce_args(&mut args, std::cmp::Ordering::Less);
+        if reduced.len() == 1 {
+          return Ok(reduced.remove(0))
+        }
+        Ok(Calc::Function(Box::new(MathFunction::Min(reduced))))
+      },
+      "max" => {
+        let mut args = input.parse_nested_block(|input| input.parse_comma_separated(|input| Calc::parse_sum(input, parse_ident)))?;
+        let mut reduced = Calc::reduce_args(&mut args, std::cmp::Ordering::Greater);
+        if reduced.len() == 1 {
+          return Ok(reduced.remove(0))
+        }
+        Ok(Calc::Function(Box::new(MathFunction::Max(reduced))))
+      },
+      "clamp" => {
+        let (mut min, mut center, mut max) = input.parse_nested_block(|input| {
+          let min = Some(Calc::parse_sum(input, parse_ident)?);
+          input.expect_comma()?;
+          let center: Calc<V> = Calc::parse_sum(input, parse_ident)?;
+          input.expect_comma()?;
+          let max = Some(Calc::parse_sum(input, parse_ident)?);
+          Ok((min, center, max))
+        })?;
+
+        // According to the spec, the minimum should "win" over the maximum if they are in the wrong order.
+        let cmp = if let (Some(Calc::Value(max_val)), Calc::Value(center_val)) = (&max, &center) {
+          center_val.partial_cmp(&max_val)
+        } else {
+          None
+        };
+
+        // If center is known to be greater than the maximum, replace it with maximum and remove the max argument.
+        // Otherwise, if center is known to be less than the maximum, remove the max argument.
+        match cmp {
+          Some(std::cmp::Ordering::Greater) => {
+            center = std::mem::take(&mut max).unwrap();
+          }
+          Some(_) => {
+            max = None;
+          }
+          None => {}
+        }
+
+        if cmp.is_some() {
+          let cmp = if let (Some(Calc::Value(min_val)), Calc::Value(center_val)) = (&min, &center) {
+            center_val.partial_cmp(&min_val)
+          } else {
+            None
+          };
+
+          // If center is known to be less than the minimum, replace it with minimum and remove the min argument.
+          // Otherwise, if center is known to be greater than the minimum, remove the min argument.
+          match cmp {
+            Some(std::cmp::Ordering::Less) => {
+              center = std::mem::take(&mut min).unwrap();
+            }
+            Some(_) => {
+              min = None;
+            }
+            None => {}
+          }
+        }
+
+        // Generate clamp(), min(), max(), or value depending on which arguments are left.
+        match (min, max) {
+          (None, None) => Ok(center),
+          (Some(min), None) => Ok(Calc::Function(Box::new(MathFunction::Max(vec![min, center])))),
+          (None, Some(max)) => Ok(Calc::Function(Box::new(MathFunction::Min(vec![center, max])))),
+          (Some(min), Some(max)) => Ok(Calc::Function(Box::new(MathFunction::Clamp(min, center, max))))
+        }
+      },
+      "round" => {
+        input.parse_nested_block(|input| {
+          let strategy = if let Ok(s) = input.try_parse(RoundingStrategy::parse) {
+            input.expect_comma()?;
+            s
+          } else {
+            RoundingStrategy::default()
+          };
+
+          Self::parse_math_fn(
+            input,
+            |a, b| round(a, b, strategy),
+            |a, b| MathFunction::Round(strategy, a, b),
+            parse_ident
+          )
+        })
+      },
+      "rem" => {
+        input.parse_nested_block(|input| {
+          Self::parse_math_fn(input, std::ops::Rem::rem, MathFunction::Rem, parse_ident)
+        })
+      },
+      "mod" => {
+        input.parse_nested_block(|input| {
+          Self::parse_math_fn(input, modulo, MathFunction::Mod, parse_ident)
+        })
+      },
+      "sin" => Self::parse_trig(input, f32::sin, false, parse_ident),
+      "cos" => Self::parse_trig(input, f32::cos, false, parse_ident),
+      "tan" => Self::parse_trig(input, f32::tan, false, parse_ident),
+      "asin" => Self::parse_trig(input, f32::asin, true, parse_ident),
+      "acos" => Self::parse_trig(input, f32::acos, true, parse_ident),
+      "atan" => Self::parse_trig(input, f32::atan, true, parse_ident),
+      "atan2" => {
+        input.parse_nested_block(|input| {
+          let res = Self::parse_atan2(input, parse_ident)?;
+          if let Ok(v) = V::try_from(res) {
+            return Ok(Calc::Value(Box::new(v)))
+          }
+
+          Err(input.new_custom_error(ParserError::InvalidValue))
+        })
+      },
+      "pow" => {
+        input.parse_nested_block(|input| {
+          let a = Self::parse_numeric(input, parse_ident)?;
+          input.expect_comma()?;
+          let b = Self::parse_numeric(input, parse_ident)?;
+          Ok(Calc::Number(a.powf(b)))
+        })
+      },
+      "log" => {
+        input.parse_nested_block(|input| {
+          let value = Self::parse_numeric(input, parse_ident)?;
+          if input.try_parse(|input| input.expect_comma()).is_ok() {
+            let base = Self::parse_numeric(input, parse_ident)?;
+            Ok(Calc::Number(value.log(base)))
+          } else {
+            Ok(Calc::Number(value.ln()))
+          }
+        })
+      },
+      "sqrt" => Self::parse_numeric_fn(input, f32::sqrt, parse_ident),
+      "exp" => Self::parse_numeric_fn(input, f32::exp, parse_ident),
+      "hypot" => {
+        input.parse_nested_block(|input| {
+          let args: Vec<Self> = input.parse_comma_separated(|input| Calc::parse_sum(input, parse_ident))?;
+          Self::parse_hypot(&args)?
+            .map_or_else(
+              || Ok(Calc::Function(Box::new(MathFunction::Hypot(args)))),
+              |v| Ok(v)
+            )
+        })
+      },
+      "abs" => {
+        input.parse_nested_block(|input| {
+          let v: Calc<V> = Self::parse_sum(input, parse_ident)?;
+          Self::apply_map(&v, f32::abs)
+            .map_or_else(
+              || Ok(Calc::Function(Box::new(MathFunction::Abs(v)))),
+              |v| Ok(v)
+            )
+        })
+      },
+      "sign" => {
+        input.parse_nested_block(|input| {
+          let v: Calc<V> = Self::parse_sum(input, parse_ident)?;
+          match &v {
+            Calc::Number(n) => return Ok(Calc::Number(n.sign())),
+            Calc::Value(v) => {
+              // First map so we ignore percentages, which must be resolved to their
+              // computed value in order to determine the sign.
+              if let Some(v) = v.try_map(|s| s.sign()) {
+                // sign() always resolves to a number.
+                return Ok(Calc::Number(v.try_sign().unwrap()));
+              }
+            }
+            _ => {}
+          }
+
+          Ok(Calc::Function(Box::new(MathFunction::Sign(v))))
+        })
+      },
+       _ => Err(location.new_unexpected_token_error(Token::Ident(f.clone()))),
+    }
+  }
+
+  fn parse_sum<'t, Parse: Copy + Fn(&str) -> Option<Calc<V>>>(
+    input: &mut Parser<'i, 't>,
+    parse_ident: Parse,
+  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let mut cur: Calc<V> = Calc::parse_product(input, parse_ident)?;
+    loop {
+      let start = input.state();
+      match input.next_including_whitespace() {
+        Ok(&Token::WhiteSpace(_)) => {
+          if input.is_exhausted() {
+            break; // allow trailing whitespace
+          }
+          match *input.next()? {
+            Token::Delim('+') => {
+              let next = Calc::parse_product(input, parse_ident)?;
+              cur = cur.add(next).map_err(|_| input.new_custom_error(ParserError::InvalidValue))?;
+            }
+            Token::Delim('-') => {
+              let mut rhs = Calc::parse_product(input, parse_ident)?;
+              rhs = rhs * -1.0;
+              cur = cur.add(rhs).map_err(|_| input.new_custom_error(ParserError::InvalidValue))?;
+            }
+            ref t => {
+              let t = t.clone();
+              return Err(input.new_unexpected_token_error(t));
+            }
+          }
+        }
+        _ => {
+          input.reset(&start);
+          break;
+        }
+      }
+    }
+    Ok(cur)
+  }
+
+  fn parse_product<'t, Parse: Copy + Fn(&str) -> Option<Calc<V>>>(
+    input: &mut Parser<'i, 't>,
+    parse_ident: Parse,
+  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let mut node = Calc::parse_value(input, parse_ident)?;
+    loop {
+      let start = input.state();
+      match input.next() {
+        Ok(&Token::Delim('*')) => {
+          // At least one of the operands must be a number.
+          let rhs = Self::parse_value(input, parse_ident)?;
+          if let Calc::Number(val) = rhs {
+            node = node * val;
+          } else if let Calc::Number(val) = node {
+            node = rhs;
+            node = node * val;
+          } else {
+            return Err(input.new_unexpected_token_error(Token::Delim('*')));
+          }
+        }
+        Ok(&Token::Delim('/')) => {
+          let rhs = Self::parse_value(input, parse_ident)?;
+          if let Calc::Number(val) = rhs {
+            if val != 0.0 {
+              node = node * (1.0 / val);
+              continue;
+            }
+          }
+          return Err(input.new_custom_error(ParserError::InvalidValue));
+        }
+        _ => {
+          input.reset(&start);
+          break;
+        }
+      }
+    }
+    Ok(node)
+  }
+
+  fn parse_value<'t, Parse: Copy + Fn(&str) -> Option<Calc<V>>>(
+    input: &mut Parser<'i, 't>,
+    parse_ident: Parse,
+  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    // Parse nested calc() and other math functions.
+    if let Ok(calc) = input.try_parse(Self::parse) {
+      match calc {
+        Calc::Function(f) => {
+          return Ok(match *f {
+            MathFunction::Calc(c) => c,
+            _ => Calc::Function(f),
+          })
+        }
+        c => return Ok(c),
+      }
+    }
+
+    if input.try_parse(|input| input.expect_parenthesis_block()).is_ok() {
+      return input.parse_nested_block(|input| Calc::parse_sum(input, parse_ident));
+    }
+
+    if let Ok(num) = input.try_parse(|input| input.expect_number()) {
+      return Ok(Calc::Number(num));
+    }
+
+    if let Ok(constant) = input.try_parse(Constant::parse) {
+      return Ok(Calc::Number(constant.into()));
+    }
+
+    let location = input.current_source_location();
+    if let Ok(ident) = input.try_parse(|input| input.expect_ident_cloned()) {
+      if let Some(v) = parse_ident(ident.as_ref()) {
+        return Ok(v);
+      }
+
+      return Err(location.new_unexpected_token_error(Token::Ident(ident.clone())));
+    }
+
+    let value = input.try_parse(V::parse)?;
+    Ok(Calc::Value(Box::new(value)))
+  }
+
+  fn reduce_args(args: &mut Vec<Calc<V>>, cmp: std::cmp::Ordering) -> Vec<Calc<V>> {
+    // Reduces the arguments of a min() or max() expression, combining compatible values.
+    // e.g. min(1px, 1em, 2px, 3in) => min(1px, 1em)
+    let mut reduced: Vec<Calc<V>> = vec![];
+    for arg in args.drain(..) {
+      let mut found = None;
+      match &arg {
+        Calc::Value(val) => {
+          for b in reduced.iter_mut() {
+            if let Calc::Value(v) = b {
+              match val.partial_cmp(v) {
+                Some(ord) if ord == cmp => {
+                  found = Some(Some(b));
+                  break;
+                }
+                Some(_) => {
+                  found = Some(None);
+                  break;
+                }
+                None => {}
+              }
+            }
+          }
+        }
+        _ => {}
+      }
+      if let Some(r) = found {
+        if let Some(r) = r {
+          *r = arg
+        }
+      } else {
+        reduced.push(arg)
+      }
+    }
+    reduced
+  }
+
+  fn parse_math_fn<
+    't,
+    O: FnOnce(f32, f32) -> f32,
+    F: FnOnce(Calc<V>, Calc<V>) -> MathFunction<V>,
+    Parse: Copy + Fn(&str) -> Option<Calc<V>>,
+  >(
+    input: &mut Parser<'i, 't>,
+    op: O,
+    fallback: F,
+    parse_ident: Parse,
+  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let a: Calc<V> = Calc::parse_sum(input, parse_ident)?;
+    input.expect_comma()?;
+    let b: Calc<V> = Calc::parse_sum(input, parse_ident)?;
+
+    Ok(Self::apply_op(&a, &b, op).unwrap_or_else(|| Calc::Function(Box::new(fallback(a, b)))))
+  }
+
+  fn apply_op<'t, O: FnOnce(f32, f32) -> f32>(a: &Calc<V>, b: &Calc<V>, op: O) -> Option<Self> {
+    match (a, b) {
+      (Calc::Value(a), Calc::Value(b)) => {
+        if let Some(v) = a.try_op(&**b, op) {
+          return Some(Calc::Value(Box::new(v)));
+        }
+      }
+      (Calc::Number(a), Calc::Number(b)) => return Some(Calc::Number(op(*a, *b))),
+      _ => {}
+    }
+
+    None
+  }
+
+  fn apply_map<'t, O: FnOnce(f32) -> f32>(v: &Calc<V>, op: O) -> Option<Self> {
+    match v {
+      Calc::Number(n) => return Some(Calc::Number(op(*n))),
+      Calc::Value(v) => {
+        if let Some(v) = v.try_map(op) {
+          return Some(Calc::Value(Box::new(v)));
+        }
+      }
+      _ => {}
+    }
+
+    None
+  }
+
+  fn parse_trig<'t, F: FnOnce(f32) -> f32, Parse: Copy + Fn(&str) -> Option<Calc<V>>>(
+    input: &mut Parser<'i, 't>,
+    f: F,
+    to_angle: bool,
+    parse_ident: Parse,
+  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    input.parse_nested_block(|input| {
+      let v: Calc<Angle> = Calc::parse_sum(input, |v| {
+        parse_ident(v).and_then(|v| -> Option<Calc<Angle>> {
+          match v {
+            Calc::Number(v) => Some(Calc::Number(v)),
+            Calc::Value(v) => (*v).try_into().ok().map(|v| Calc::Value(Box::new(v))),
+            _ => None,
+          }
+        })
+      })?;
+      let rad = match v {
+        Calc::Value(angle) if !to_angle => f(angle.to_radians()),
+        Calc::Number(v) => f(v),
+        _ => return Err(input.new_custom_error(ParserError::InvalidValue)),
+      };
+
+      if to_angle && !rad.is_nan() {
+        if let Ok(v) = V::try_from(Angle::Rad(rad)) {
+          return Ok(Calc::Value(Box::new(v)));
+        } else {
+          return Err(input.new_custom_error(ParserError::InvalidValue));
+        }
+      } else {
+        Ok(Calc::Number(rad))
+      }
+    })
+  }
+
+  fn parse_numeric<'t, Parse: Copy + Fn(&str) -> Option<Calc<V>>>(
+    input: &mut Parser<'i, 't>,
+    parse_ident: Parse,
+  ) -> Result<f32, ParseError<'i, ParserError<'i>>> {
+    let v: Calc<CSSNumber> = Calc::parse_sum(input, |v| {
+      parse_ident(v).and_then(|v| match v {
+        Calc::Number(v) => Some(Calc::Number(v)),
+        _ => None,
+      })
+    })?;
+    match v {
+      Calc::Number(n) => Ok(n),
+      Calc::Value(v) => Ok(*v),
+      _ => Err(input.new_custom_error(ParserError::InvalidValue)),
+    }
+  }
+
+  fn parse_numeric_fn<'t, F: FnOnce(f32) -> f32, Parse: Copy + Fn(&str) -> Option<Calc<V>>>(
+    input: &mut Parser<'i, 't>,
+    f: F,
+    parse_ident: Parse,
+  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    input.parse_nested_block(|input| {
+      let v = Self::parse_numeric(input, parse_ident)?;
+      Ok(Calc::Number(f(v)))
+    })
+  }
+
+  fn parse_atan2<'t, Parse: Copy + Fn(&str) -> Option<Calc<V>>>(
+    input: &mut Parser<'i, 't>,
+    parse_ident: Parse,
+  ) -> Result<Angle, ParseError<'i, ParserError<'i>>> {
+    // atan2 supports arguments of any <number>, <dimension>, or <percentage>, even ones that wouldn't
+    // normally be supported by V. The only requirement is that the arguments be of the same type.
+    // Try parsing with each type, and return the first one that parses successfully.
+    if let Ok(v) = input.try_parse(|input| Calc::<Length>::parse_atan2_args(input, |_| None)) {
+      return Ok(v);
+    }
+
+    if let Ok(v) = input.try_parse(|input| Calc::<Percentage>::parse_atan2_args(input, |_| None)) {
+      return Ok(v);
+    }
+
+    if let Ok(v) = input.try_parse(|input| Calc::<Angle>::parse_atan2_args(input, |_| None)) {
+      return Ok(v);
+    }
+
+    if let Ok(v) = input.try_parse(|input| Calc::<Time>::parse_atan2_args(input, |_| None)) {
+      return Ok(v);
+    }
+
+    Calc::<CSSNumber>::parse_atan2_args(input, |v| {
+      parse_ident(v).and_then(|v| match v {
+        Calc::Number(v) => Some(Calc::Number(v)),
+        _ => None,
+      })
+    })
+  }
+
+  fn parse_atan2_args<'t, Parse: Copy + Fn(&str) -> Option<Calc<V>>>(
+    input: &mut Parser<'i, 't>,
+    parse_ident: Parse,
+  ) -> Result<Angle, ParseError<'i, ParserError<'i>>> {
+    let a = Calc::<V>::parse_sum(input, parse_ident)?;
+    input.expect_comma()?;
+    let b = Calc::<V>::parse_sum(input, parse_ident)?;
+
+    match (&a, &b) {
+      (Calc::Value(a), Calc::Value(b)) => {
+        if let Some(v) = a.try_op_to(&**b, |a, b| Angle::Rad(a.atan2(b))) {
+          return Ok(v);
+        }
+      }
+      (Calc::Number(a), Calc::Number(b)) => return Ok(Angle::Rad(a.atan2(*b))),
+      _ => {}
+    }
+
+    // We don't have a way to represent arguments that aren't angles, so just error.
+    // This will fall back to an unparsed property, leaving the atan2() function intact.
+    Err(input.new_custom_error(ParserError::InvalidValue))
+  }
+
+  fn parse_hypot<'t>(args: &Vec<Self>) -> Result<Option<Self>, ParseError<'i, ParserError<'i>>> {
+    if args.len() == 1 {
+      return Ok(Some(args[0].clone()));
+    }
+
+    if args.len() == 2 {
+      return Ok(Self::apply_op(&args[0], &args[1], f32::hypot));
+    }
+
+    let mut iter = args.iter();
+    let first = match Self::apply_map(&iter.next().unwrap(), |v| v.powi(2)) {
+      Some(v) => v,
+      None => return Ok(None),
+    };
+    let sum = iter.try_fold(first, |acc, arg| {
+      Self::apply_op(&acc, &arg, |a, b| a + b.powi(2)).map_or_else(|| Err(()), |v| Ok(v))
+    });
+
+    let sum = match sum {
+      Ok(s) => s,
+      Err(_) => return Ok(None),
+    };
+
+    Ok(Self::apply_map(&sum, f32::sqrt))
+  }
+}
+
+impl<V: std::ops::Mul<f32, Output = V>> std::ops::Mul<f32> for Calc<V> {
+  type Output = Self;
+
+  fn mul(self, other: f32) -> Self {
+    if other == 1.0 {
+      return self;
+    }
+
+    match self {
+      Calc::Value(v) => Calc::Value(Box::new(*v * other)),
+      Calc::Number(n) => Calc::Number(n * other),
+      Calc::Sum(a, b) => Calc::Sum(Box::new(*a * other), Box::new(*b * other)),
+      Calc::Product(num, calc) => {
+        let num = num * other;
+        if num == 1.0 {
+          return *calc;
+        }
+        Calc::Product(num, calc)
+      }
+      Calc::Function(f) => match *f {
+        MathFunction::Calc(c) => Calc::Function(Box::new(MathFunction::Calc(c * other))),
+        _ => Calc::Product(other, Box::new(Calc::Function(f))),
+      },
+    }
+  }
+}
+
+impl<V: AddInternal + std::convert::Into<Calc<V>> + std::convert::TryFrom<Calc<V>> + std::fmt::Debug> Calc<V> {
+  pub(crate) fn add(self, other: Calc<V>) -> Result<Calc<V>, <V as TryFrom<Calc<V>>>::Error> {
+    Ok(match (self, other) {
+      (Calc::Value(a), Calc::Value(b)) => (a.add(*b)).into(),
+      (Calc::Number(a), Calc::Number(b)) => Calc::Number(a + b),
+      (Calc::Sum(a, b), Calc::Number(c)) => {
+        if let Calc::Number(a) = *a {
+          Calc::Sum(Box::new(Calc::Number(a + c)), b)
+        } else if let Calc::Number(b) = *b {
+          Calc::Sum(a, Box::new(Calc::Number(b + c)))
+        } else {
+          Calc::Sum(Box::new(Calc::Sum(a, b)), Box::new(Calc::Number(c)))
+        }
+      }
+      (Calc::Number(a), Calc::Sum(b, c)) => {
+        if let Calc::Number(b) = *b {
+          Calc::Sum(Box::new(Calc::Number(a + b)), c)
+        } else if let Calc::Number(c) = *c {
+          Calc::Sum(Box::new(Calc::Number(a + c)), b)
+        } else {
+          Calc::Sum(Box::new(Calc::Number(a)), Box::new(Calc::Sum(b, c)))
+        }
+      }
+      (a @ Calc::Number(_), b)
+      | (a, b @ Calc::Number(_))
+      | (a @ Calc::Product(..), b)
+      | (a, b @ Calc::Product(..)) => Calc::Sum(Box::new(a), Box::new(b)),
+      (Calc::Function(a), b) => Calc::Sum(Box::new(Calc::Function(a)), Box::new(b)),
+      (a, Calc::Function(b)) => Calc::Sum(Box::new(a), Box::new(Calc::Function(b))),
+      (Calc::Value(a), b) => (a.add(V::try_from(b)?)).into(),
+      (a, Calc::Value(b)) => (V::try_from(a)?.add(*b)).into(),
+      (a @ Calc::Sum(..), b @ Calc::Sum(..)) => V::try_from(a)?.add(V::try_from(b)?).into(),
+    })
+  }
+}
+
+impl<V: ToCss + std::ops::Mul<f32, Output = V> + TrySign + Clone + std::fmt::Debug> ToCss for Calc<V> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    let was_in_calc = dest.in_calc;
+    dest.in_calc = true;
+
+    let res = match self {
+      Calc::Value(v) => v.to_css(dest),
+      Calc::Number(n) => n.to_css(dest),
+      Calc::Sum(a, b) => {
+        a.to_css(dest)?;
+        // Whitespace is always required.
+        let b = &**b;
+        if b.is_sign_negative() {
+          dest.write_str(" - ")?;
+          let b = b.clone() * -1.0;
+          b.to_css(dest)
+        } else {
+          dest.write_str(" + ")?;
+          b.to_css(dest)
+        }
+      }
+      Calc::Product(num, calc) => {
+        if num.abs() < 1.0 {
+          let div = 1.0 / num;
+          calc.to_css(dest)?;
+          dest.delim('/', true)?;
+          div.to_css(dest)
+        } else {
+          num.to_css(dest)?;
+          dest.delim('*', true)?;
+          calc.to_css(dest)
+        }
+      }
+      Calc::Function(f) => f.to_css(dest),
+    };
+
+    dest.in_calc = was_in_calc;
+    res
+  }
+}
+
+impl<V: TrySign> TrySign for Calc<V> {
+  fn try_sign(&self) -> Option<f32> {
+    match self {
+      Calc::Number(v) => v.try_sign(),
+      Calc::Value(v) => v.try_sign(),
+      Calc::Product(c, v) => v.try_sign().map(|s| s * c.sign()),
+      Calc::Function(f) => f.try_sign(),
+      _ => None,
+    }
+  }
+}
+
+impl<V: TrySign> TrySign for MathFunction<V> {
+  fn try_sign(&self) -> Option<f32> {
+    match self {
+      MathFunction::Abs(_) => Some(1.0),
+      MathFunction::Max(values) | MathFunction::Min(values) => {
+        let mut iter = values.iter();
+        if let Some(sign) = iter.next().and_then(|f| f.try_sign()) {
+          for value in iter {
+            if let Some(s) = value.try_sign() {
+              if s != sign {
+                return None;
+              }
+            } else {
+              return None;
+            }
+          }
+          return Some(sign);
+        } else {
+          return None;
+        }
+      }
+      MathFunction::Clamp(a, b, c) => {
+        if let (Some(a), Some(b), Some(c)) = (a.try_sign(), b.try_sign(), c.try_sign()) {
+          if a == b && b == c {
+            return Some(a);
+          }
+        }
+        return None;
+      }
+      MathFunction::Round(_, a, b) => {
+        if let (Some(a), Some(b)) = (a.try_sign(), b.try_sign()) {
+          if a == b {
+            return Some(a);
+          }
+        }
+        return None;
+      }
+      MathFunction::Sign(v) => v.try_sign(),
+      MathFunction::Calc(v) => v.try_sign(),
+      _ => None,
+    }
+  }
+}
diff --git a/src/values/color.rs b/src/values/color.rs
new file mode 100644
index 0000000..b21eb19
--- /dev/null
+++ b/src/values/color.rs
@@ -0,0 +1,3803 @@
+//! CSS color values.
+
+#![allow(non_upper_case_globals)]
+
+use super::angle::Angle;
+use super::calc::Calc;
+use super::number::CSSNumber;
+use super::percentage::Percentage;
+use crate::compat::Feature;
+use crate::error::{ParserError, PrinterError};
+use crate::macros::enum_property;
+use crate::printer::Printer;
+use crate::properties::PropertyId;
+use crate::rules::supports::SupportsCondition;
+use crate::targets::{should_compile, Browsers, Features, Targets};
+use crate::traits::{FallbackValues, IsCompatible, Parse, ToCss};
+#[cfg(feature = "visitor")]
+use crate::visitor::{Visit, VisitTypes, Visitor};
+use bitflags::bitflags;
+use cssparser::color::{parse_hash_color, parse_named_color};
+use cssparser::*;
+use cssparser_color::{hsl_to_rgb, AngleOrNumber, ColorParser, NumberOrPercentage};
+use std::any::TypeId;
+use std::f32::consts::PI;
+use std::fmt::Write;
+
+/// A CSS [`<color>`](https://www.w3.org/TR/css-color-4/#color-type) value.
+///
+/// CSS supports many different color spaces to represent colors. The most common values
+/// are stored as RGBA using a single byte per component. Less common values are stored
+/// using a `Box` to reduce the amount of memory used per color.
+///
+/// Each color space is represented as a struct that implements the `From` and `Into` traits
+/// for all other color spaces, so it is possible to convert between color spaces easily.
+/// In addition, colors support [interpolation](#method.interpolate) as in the `color-mix()` function.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "visitor", visit(visit_color, COLORS))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(untagged, rename_all = "lowercase")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum CssColor {
+  /// The [`currentColor`](https://www.w3.org/TR/css-color-4/#currentcolor-color) keyword.
+  #[cfg_attr(feature = "serde", serde(with = "CurrentColor"))]
+  CurrentColor,
+  /// An value in the RGB color space, including values parsed as hex colors, or the `rgb()`, `hsl()`, and `hwb()` functions.
+  #[cfg_attr(
+    feature = "serde",
+    serde(serialize_with = "serialize_rgba", deserialize_with = "deserialize_rgba")
+  )]
+  #[cfg_attr(feature = "jsonschema", schemars(with = "RGBColor"))]
+  RGBA(RGBA),
+  /// A value in a LAB color space, including the `lab()`, `lch()`, `oklab()`, and `oklch()` functions.
+  LAB(Box<LABColor>),
+  /// A value in a predefined color space, e.g. `display-p3`.
+  Predefined(Box<PredefinedColor>),
+  /// A floating point representation of an RGB, HSL, or HWB color when it contains `none` components.
+  Float(Box<FloatColor>),
+  /// The [`light-dark()`](https://drafts.csswg.org/css-color-5/#light-dark) function.
+  #[cfg_attr(feature = "visitor", skip_type)]
+  #[cfg_attr(feature = "serde", serde(with = "LightDark"))]
+  LightDark(Box<CssColor>, Box<CssColor>),
+  /// A [system color](https://drafts.csswg.org/css-color/#css-system-colors) keyword.
+  System(SystemColor),
+}
+
+#[cfg(feature = "serde")]
+#[derive(serde::Serialize, serde::Deserialize)]
+#[serde(tag = "type", rename_all = "lowercase")]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+enum CurrentColor {
+  CurrentColor,
+}
+
+#[cfg(feature = "serde")]
+impl CurrentColor {
+  fn serialize<S>(serializer: S) -> Result<S::Ok, S::Error>
+  where
+    S: serde::Serializer,
+  {
+    serde::Serialize::serialize(&CurrentColor::CurrentColor, serializer)
+  }
+
+  fn deserialize<'de, D>(deserializer: D) -> Result<(), D::Error>
+  where
+    D: serde::Deserializer<'de>,
+  {
+    use serde::Deserialize;
+    let _: CurrentColor = Deserialize::deserialize(deserializer)?;
+    Ok(())
+  }
+}
+
+// Convert RGBA to SRGB to serialize so we get a tagged struct.
+#[cfg(feature = "serde")]
+#[derive(serde::Serialize, serde::Deserialize)]
+#[serde(tag = "type", rename_all = "lowercase")]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+enum RGBColor {
+  RGB(RGB),
+}
+
+#[cfg(feature = "serde")]
+fn serialize_rgba<S>(rgba: &RGBA, serializer: S) -> Result<S::Ok, S::Error>
+where
+  S: serde::Serializer,
+{
+  use serde::Serialize;
+  RGBColor::RGB(rgba.into()).serialize(serializer)
+}
+
+#[cfg(feature = "serde")]
+fn deserialize_rgba<'de, D>(deserializer: D) -> Result<RGBA, D::Error>
+where
+  D: serde::Deserializer<'de>,
+{
+  use serde::Deserialize;
+  match RGBColor::deserialize(deserializer)? {
+    RGBColor::RGB(srgb) => Ok(srgb.into()),
+  }
+}
+
+// For AST serialization.
+#[cfg(feature = "serde")]
+#[derive(serde::Serialize, serde::Deserialize)]
+#[serde(tag = "type", rename_all = "kebab-case")]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+enum LightDark {
+  LightDark { light: CssColor, dark: CssColor },
+}
+
+#[cfg(feature = "serde")]
+impl<'de> LightDark {
+  pub fn serialize<S>(light: &Box<CssColor>, dark: &Box<CssColor>, serializer: S) -> Result<S::Ok, S::Error>
+  where
+    S: serde::Serializer,
+  {
+    let wrapper = LightDark::LightDark {
+      light: (**light).clone(),
+      dark: (**dark).clone(),
+    };
+    serde::Serialize::serialize(&wrapper, serializer)
+  }
+
+  pub fn deserialize<D>(deserializer: D) -> Result<(Box<CssColor>, Box<CssColor>), D::Error>
+  where
+    D: serde::Deserializer<'de>,
+  {
+    let v: LightDark = serde::Deserialize::deserialize(deserializer)?;
+    match v {
+      LightDark::LightDark { light, dark } => Ok((Box::new(light), Box::new(dark))),
+    }
+  }
+}
+
+/// A color in a LAB color space, including the `lab()`, `lch()`, `oklab()`, and `oklch()` functions.
+#[derive(Debug, Clone, Copy, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", rename_all = "lowercase")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub enum LABColor {
+  /// A `lab()` color.
+  LAB(LAB),
+  /// An `lch()` color.
+  LCH(LCH),
+  /// An `oklab()` color.
+  OKLAB(OKLAB),
+  /// An `oklch()` color.
+  OKLCH(OKLCH),
+}
+
+/// A color in a predefined color space, e.g. `display-p3`.
+#[derive(Debug, Clone, Copy, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(tag = "type"))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub enum PredefinedColor {
+  /// A color in the `srgb` color space.
+  #[cfg_attr(feature = "serde", serde(rename = "srgb"))]
+  SRGB(SRGB),
+  /// A color in the `srgb-linear` color space.
+  #[cfg_attr(feature = "serde", serde(rename = "srgb-linear"))]
+  SRGBLinear(SRGBLinear),
+  /// A color in the `display-p3` color space.
+  #[cfg_attr(feature = "serde", serde(rename = "display-p3"))]
+  DisplayP3(P3),
+  /// A color in the `a98-rgb` color space.
+  #[cfg_attr(feature = "serde", serde(rename = "a98-rgb"))]
+  A98(A98),
+  /// A color in the `prophoto-rgb` color space.
+  #[cfg_attr(feature = "serde", serde(rename = "prophoto-rgb"))]
+  ProPhoto(ProPhoto),
+  /// A color in the `rec2020` color space.
+  #[cfg_attr(feature = "serde", serde(rename = "rec2020"))]
+  Rec2020(Rec2020),
+  /// A color in the `xyz-d50` color space.
+  #[cfg_attr(feature = "serde", serde(rename = "xyz-d50"))]
+  XYZd50(XYZd50),
+  /// A color in the `xyz-d65` color space.
+  #[cfg_attr(feature = "serde", serde(rename = "xyz-d65"))]
+  XYZd65(XYZd65),
+}
+
+/// A floating point representation of color types that
+/// are usually stored as RGBA. These are used when there
+/// are any `none` components, which are represented as NaN.
+#[derive(Debug, Clone, Copy, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", rename_all = "lowercase")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub enum FloatColor {
+  /// An RGB color.
+  RGB(RGB),
+  /// An HSL color.
+  HSL(HSL),
+  /// An HWB color.
+  HWB(HWB),
+}
+
+bitflags! {
+  /// A color type that is used as a fallback when compiling colors for older browsers.
+  #[derive(PartialEq, Eq, Clone, Copy)]
+  pub struct ColorFallbackKind: u8 {
+    /// An RGB color fallback.
+    const RGB    = 0b01;
+    /// A P3 color fallback.
+    const P3     = 0b10;
+    /// A LAB color fallback.
+    const LAB    = 0b100;
+    /// An OKLAB color fallback.
+    const OKLAB  = 0b1000;
+  }
+}
+
+enum_property! {
+  /// A [color space](https://www.w3.org/TR/css-color-4/#interpolation-space) keyword
+  /// used in interpolation functions such as `color-mix()`.
+  enum ColorSpaceName {
+    "srgb": SRGB,
+    "srgb-linear": SRGBLinear,
+    "lab": LAB,
+    "oklab": OKLAB,
+    "xyz": XYZ,
+    "xyz-d50": XYZd50,
+    "xyz-d65": XYZd65,
+    "hsl": Hsl,
+    "hwb": Hwb,
+    "lch": LCH,
+    "oklch": OKLCH,
+  }
+}
+
+enum_property! {
+  /// A hue [interpolation method](https://www.w3.org/TR/css-color-4/#typedef-hue-interpolation-method)
+  /// used in interpolation functions such as `color-mix()`.
+  pub enum HueInterpolationMethod {
+    /// Angles are adjusted so that θ₂ - θ₁ ∈ [-180, 180].
+    Shorter,
+    /// Angles are adjusted so that θ₂ - θ₁ ∈ {0, [180, 360)}.
+    Longer,
+    /// Angles are adjusted so that θ₂ - θ₁ ∈ [0, 360).
+    Increasing,
+    /// Angles are adjusted so that θ₂ - θ₁ ∈ (-360, 0].
+    Decreasing,
+    /// No fixup is performed. Angles are interpolated in the same way as every other component.
+    Specified,
+  }
+}
+
+impl ColorFallbackKind {
+  pub(crate) fn lowest(&self) -> ColorFallbackKind {
+    // This finds the lowest set bit.
+    *self & ColorFallbackKind::from_bits_truncate(self.bits().wrapping_neg())
+  }
+
+  pub(crate) fn highest(&self) -> ColorFallbackKind {
+    // This finds the highest set bit.
+    if self.is_empty() {
+      return ColorFallbackKind::empty();
+    }
+
+    let zeros = 7 - self.bits().leading_zeros();
+    ColorFallbackKind::from_bits_truncate(1 << zeros)
+  }
+
+  pub(crate) fn and_below(&self) -> ColorFallbackKind {
+    if self.is_empty() {
+      return ColorFallbackKind::empty();
+    }
+
+    *self | ColorFallbackKind::from_bits_truncate(self.bits() - 1)
+  }
+
+  pub(crate) fn supports_condition<'i>(&self) -> SupportsCondition<'i> {
+    let s = match *self {
+      ColorFallbackKind::P3 => "color(display-p3 0 0 0)",
+      ColorFallbackKind::LAB => "lab(0% 0 0)",
+      _ => unreachable!(),
+    };
+
+    SupportsCondition::Declaration {
+      property_id: PropertyId::Color,
+      value: s.into(),
+    }
+  }
+}
+
+impl CssColor {
+  /// Returns the `currentColor` keyword.
+  pub fn current_color() -> CssColor {
+    CssColor::CurrentColor
+  }
+
+  /// Returns the `transparent` keyword.
+  pub fn transparent() -> CssColor {
+    CssColor::RGBA(RGBA::transparent())
+  }
+
+  /// Converts the color to RGBA.
+  pub fn to_rgb(&self) -> Result<CssColor, ()> {
+    match self {
+      CssColor::LightDark(light, dark) => {
+        Ok(CssColor::LightDark(Box::new(light.to_rgb()?), Box::new(dark.to_rgb()?)))
+      }
+      _ => Ok(RGBA::try_from(self)?.into()),
+    }
+  }
+
+  /// Converts the color to the LAB color space.
+  pub fn to_lab(&self) -> Result<CssColor, ()> {
+    match self {
+      CssColor::LightDark(light, dark) => {
+        Ok(CssColor::LightDark(Box::new(light.to_lab()?), Box::new(dark.to_lab()?)))
+      }
+      _ => Ok(LAB::try_from(self)?.into()),
+    }
+  }
+
+  /// Converts the color to the P3 color space.
+  pub fn to_p3(&self) -> Result<CssColor, ()> {
+    match self {
+      CssColor::LightDark(light, dark) => {
+        Ok(CssColor::LightDark(Box::new(light.to_p3()?), Box::new(dark.to_p3()?)))
+      }
+      _ => Ok(P3::try_from(self)?.into()),
+    }
+  }
+
+  pub(crate) fn get_possible_fallbacks(&self, targets: Targets) -> ColorFallbackKind {
+    // Fallbacks occur in levels: Oklab -> Lab -> P3 -> RGB. We start with all levels
+    // below and including the authored color space, and remove the ones that aren't
+    // compatible with our browser targets.
+    let mut fallbacks = match self {
+      CssColor::CurrentColor | CssColor::RGBA(_) | CssColor::Float(..) | CssColor::System(..) => {
+        return ColorFallbackKind::empty()
+      }
+      CssColor::LAB(lab) => match &**lab {
+        LABColor::LAB(..) | LABColor::LCH(..) if should_compile!(targets, LabColors) => {
+          ColorFallbackKind::LAB.and_below()
+        }
+        LABColor::OKLAB(..) | LABColor::OKLCH(..) if should_compile!(targets, OklabColors) => {
+          ColorFallbackKind::OKLAB.and_below()
+        }
+        _ => return ColorFallbackKind::empty(),
+      },
+      CssColor::Predefined(predefined) => match &**predefined {
+        PredefinedColor::DisplayP3(..) if should_compile!(targets, P3Colors) => ColorFallbackKind::P3.and_below(),
+        _ if should_compile!(targets, ColorFunction) => ColorFallbackKind::LAB.and_below(),
+        _ => return ColorFallbackKind::empty(),
+      },
+      CssColor::LightDark(light, dark) => {
+        return light.get_possible_fallbacks(targets) | dark.get_possible_fallbacks(targets);
+      }
+    };
+
+    if fallbacks.contains(ColorFallbackKind::OKLAB) {
+      if !should_compile!(targets, OklabColors) {
+        fallbacks.remove(ColorFallbackKind::LAB.and_below());
+      }
+    }
+
+    if fallbacks.contains(ColorFallbackKind::LAB) {
+      if !should_compile!(targets, LabColors) {
+        fallbacks.remove(ColorFallbackKind::P3.and_below());
+      } else if targets
+        .browsers
+        .map(|targets| Feature::LabColors.is_partially_compatible(targets))
+        .unwrap_or(false)
+      {
+        // We don't need P3 if Lab is supported by some of our targets.
+        // No browser implements Lab but not P3.
+        fallbacks.remove(ColorFallbackKind::P3);
+      }
+    }
+
+    if fallbacks.contains(ColorFallbackKind::P3) {
+      if !should_compile!(targets, P3Colors) {
+        fallbacks.remove(ColorFallbackKind::RGB);
+      } else if fallbacks.highest() != ColorFallbackKind::P3
+        && !targets
+          .browsers
+          .map(|targets| Feature::P3Colors.is_partially_compatible(targets))
+          .unwrap_or(false)
+      {
+        // Remove P3 if it isn't supported by any targets, and wasn't the
+        // original authored color.
+        fallbacks.remove(ColorFallbackKind::P3);
+      }
+    }
+
+    fallbacks
+  }
+
+  /// Returns the color fallback types needed for the given browser targets.
+  pub fn get_necessary_fallbacks(&self, targets: Targets) -> ColorFallbackKind {
+    // Get the full set of possible fallbacks, and remove the highest one, which
+    // will replace the original declaration. The remaining fallbacks need to be added.
+    let fallbacks = self.get_possible_fallbacks(targets);
+    fallbacks - fallbacks.highest()
+  }
+
+  /// Returns a fallback color for the given fallback type.
+  pub fn get_fallback(&self, kind: ColorFallbackKind) -> CssColor {
+    if matches!(self, CssColor::RGBA(_)) {
+      return self.clone();
+    }
+
+    match kind {
+      ColorFallbackKind::RGB => self.to_rgb().unwrap(),
+      ColorFallbackKind::P3 => self.to_p3().unwrap(),
+      ColorFallbackKind::LAB => self.to_lab().unwrap(),
+      _ => unreachable!(),
+    }
+  }
+
+  pub(crate) fn get_features(&self) -> Features {
+    let mut features = Features::empty();
+    match self {
+      CssColor::LAB(labcolor) => match &**labcolor {
+        LABColor::LAB(_) | LABColor::LCH(_) => {
+          features |= Features::LabColors;
+        }
+        LABColor::OKLAB(_) | LABColor::OKLCH(_) => {
+          features |= Features::OklabColors;
+        }
+      },
+      CssColor::Predefined(predefined_color) => {
+        features |= Features::ColorFunction;
+        match &**predefined_color {
+          PredefinedColor::DisplayP3(_) => {
+            features |= Features::P3Colors;
+          }
+          _ => {}
+        }
+      }
+      CssColor::Float(_) => {
+        features |= Features::SpaceSeparatedColorNotation;
+      }
+      CssColor::LightDark(light, dark) => {
+        features |= Features::LightDark;
+        features |= light.get_features();
+        features |= dark.get_features();
+      }
+      _ => {}
+    }
+
+    features
+  }
+}
+
+impl IsCompatible for CssColor {
+  fn is_compatible(&self, browsers: Browsers) -> bool {
+    match self {
+      CssColor::CurrentColor | CssColor::RGBA(_) | CssColor::Float(..) => true,
+      CssColor::LAB(lab) => match &**lab {
+        LABColor::LAB(..) | LABColor::LCH(..) => Feature::LabColors.is_compatible(browsers),
+        LABColor::OKLAB(..) | LABColor::OKLCH(..) => Feature::OklabColors.is_compatible(browsers),
+      },
+      CssColor::Predefined(predefined) => match &**predefined {
+        PredefinedColor::DisplayP3(..) => Feature::P3Colors.is_compatible(browsers),
+        _ => Feature::ColorFunction.is_compatible(browsers),
+      },
+      CssColor::LightDark(light, dark) => {
+        Feature::LightDark.is_compatible(browsers) && light.is_compatible(browsers) && dark.is_compatible(browsers)
+      }
+      CssColor::System(system) => system.is_compatible(browsers),
+    }
+  }
+}
+
+impl FallbackValues for CssColor {
+  fn get_fallbacks(&mut self, targets: Targets) -> Vec<CssColor> {
+    let fallbacks = self.get_necessary_fallbacks(targets);
+
+    let mut res = Vec::new();
+    if fallbacks.contains(ColorFallbackKind::RGB) {
+      res.push(self.to_rgb().unwrap());
+    }
+
+    if fallbacks.contains(ColorFallbackKind::P3) {
+      res.push(self.to_p3().unwrap());
+    }
+
+    if fallbacks.contains(ColorFallbackKind::LAB) {
+      *self = self.to_lab().unwrap();
+    }
+
+    res
+  }
+}
+
+impl Default for CssColor {
+  fn default() -> CssColor {
+    CssColor::transparent()
+  }
+}
+
+impl<'i> Parse<'i> for CssColor {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let location = input.current_source_location();
+    let token = input.next()?;
+    match *token {
+      Token::Hash(ref value) | Token::IDHash(ref value) => parse_hash_color(value.as_bytes())
+        .map(|(r, g, b, a)| CssColor::RGBA(RGBA::new(r, g, b, a)))
+        .map_err(|_| location.new_unexpected_token_error(token.clone())),
+      Token::Ident(ref value) => Ok(match_ignore_ascii_case! { value,
+        "currentcolor" => CssColor::CurrentColor,
+        "transparent" => CssColor::RGBA(RGBA::transparent()),
+        _ => {
+          if let Ok((r, g, b)) = parse_named_color(value) {
+            CssColor::RGBA(RGBA { red: r, green: g, blue: b, alpha: 255 })
+          } else if let Ok(system_color) = SystemColor::parse_string(&value) {
+            CssColor::System(system_color)
+          } else {
+            return Err(location.new_unexpected_token_error(token.clone()))
+          }
+        }
+      }),
+      Token::Function(ref name) => parse_color_function(location, name.clone(), input),
+      _ => Err(location.new_unexpected_token_error(token.clone())),
+    }
+  }
+}
+
+impl ToCss for CssColor {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    match self {
+      CssColor::CurrentColor => dest.write_str("currentColor"),
+      CssColor::RGBA(color) => {
+        if color.alpha == 255 {
+          let hex: u32 = ((color.red as u32) << 16) | ((color.green as u32) << 8) | (color.blue as u32);
+          if let Some(name) = short_color_name(hex) {
+            return dest.write_str(name);
+          }
+
+          let compact = compact_hex(hex);
+          if hex == expand_hex(compact) {
+            write!(dest, "#{:03x}", compact)?;
+          } else {
+            write!(dest, "#{:06x}", hex)?;
+          }
+        } else {
+          // If the #rrggbbaa syntax is not supported by the browser targets, output rgba()
+          if should_compile!(dest.targets.current, HexAlphaColors) {
+            // If the browser doesn't support `#rrggbbaa` color syntax, it is converted to `transparent` when compressed(minify = true).
+            // https://www.w3.org/TR/css-color-4/#transparent-black
+            if dest.minify && color.red == 0 && color.green == 0 && color.blue == 0 && color.alpha == 0 {
+              return dest.write_str("transparent");
+            } else {
+              dest.write_str("rgba(")?;
+              write!(dest, "{}", color.red)?;
+              dest.delim(',', false)?;
+              write!(dest, "{}", color.green)?;
+              dest.delim(',', false)?;
+              write!(dest, "{}", color.blue)?;
+              dest.delim(',', false)?;
+
+              // Try first with two decimal places, then with three.
+              let mut rounded_alpha = (color.alpha_f32() * 100.0).round() / 100.0;
+              let clamped = (rounded_alpha * 255.0).round().max(0.).min(255.0) as u8;
+              if clamped != color.alpha {
+                rounded_alpha = (color.alpha_f32() * 1000.).round() / 1000.;
+              }
+
+              rounded_alpha.to_css(dest)?;
+              dest.write_char(')')?;
+              return Ok(());
+            }
+          }
+
+          let hex: u32 = ((color.red as u32) << 24)
+            | ((color.green as u32) << 16)
+            | ((color.blue as u32) << 8)
+            | (color.alpha as u32);
+          let compact = compact_hex(hex);
+          if hex == expand_hex(compact) {
+            write!(dest, "#{:04x}", compact)?;
+          } else {
+            write!(dest, "#{:08x}", hex)?;
+          }
+        }
+        Ok(())
+      }
+      CssColor::LAB(lab) => match &**lab {
+        LABColor::LAB(lab) => write_components("lab", lab.l / 100.0, lab.a, lab.b, lab.alpha, dest),
+        LABColor::LCH(lch) => write_components("lch", lch.l / 100.0, lch.c, lch.h, lch.alpha, dest),
+        LABColor::OKLAB(lab) => write_components("oklab", lab.l, lab.a, lab.b, lab.alpha, dest),
+        LABColor::OKLCH(lch) => write_components("oklch", lch.l, lch.c, lch.h, lch.alpha, dest),
+      },
+      CssColor::Predefined(predefined) => write_predefined(predefined, dest),
+      CssColor::Float(float) => {
+        // Serialize as hex.
+        let rgb = RGB::from(**float);
+        CssColor::from(rgb).to_css(dest)
+      }
+      CssColor::LightDark(light, dark) => {
+        if should_compile!(dest.targets.current, LightDark) {
+          dest.write_str("var(--lightningcss-light")?;
+          dest.delim(',', false)?;
+          light.to_css(dest)?;
+          dest.write_char(')')?;
+          dest.whitespace()?;
+          dest.write_str("var(--lightningcss-dark")?;
+          dest.delim(',', false)?;
+          dark.to_css(dest)?;
+          return dest.write_char(')');
+        }
+
+        dest.write_str("light-dark(")?;
+        light.to_css(dest)?;
+        dest.delim(',', false)?;
+        dark.to_css(dest)?;
+        dest.write_char(')')
+      }
+      CssColor::System(system) => system.to_css(dest),
+    }
+  }
+}
+
+// From esbuild: https://github.com/evanw/esbuild/blob/18e13bdfdca5cd3c7a2fae1a8bd739f8f891572c/internal/css_parser/css_decls_color.go#L218
+// 0xAABBCCDD => 0xABCD
+fn compact_hex(v: u32) -> u32 {
+  return ((v & 0x0FF00000) >> 12) | ((v & 0x00000FF0) >> 4);
+}
+
+// 0xABCD => 0xAABBCCDD
+fn expand_hex(v: u32) -> u32 {
+  return ((v & 0xF000) << 16) | ((v & 0xFF00) << 12) | ((v & 0x0FF0) << 8) | ((v & 0x00FF) << 4) | (v & 0x000F);
+}
+
+fn short_color_name(v: u32) -> Option<&'static str> {
+  // These names are shorter than their hex codes
+  let s = match v {
+    0x000080 => "navy",
+    0x008000 => "green",
+    0x008080 => "teal",
+    0x4b0082 => "indigo",
+    0x800000 => "maroon",
+    0x800080 => "purple",
+    0x808000 => "olive",
+    0x808080 => "gray",
+    0xa0522d => "sienna",
+    0xa52a2a => "brown",
+    0xc0c0c0 => "silver",
+    0xcd853f => "peru",
+    0xd2b48c => "tan",
+    0xda70d6 => "orchid",
+    0xdda0dd => "plum",
+    0xee82ee => "violet",
+    0xf0e68c => "khaki",
+    0xf0ffff => "azure",
+    0xf5deb3 => "wheat",
+    0xf5f5dc => "beige",
+    0xfa8072 => "salmon",
+    0xfaf0e6 => "linen",
+    0xff0000 => "red",
+    0xff6347 => "tomato",
+    0xff7f50 => "coral",
+    0xffa500 => "orange",
+    0xffc0cb => "pink",
+    0xffd700 => "gold",
+    0xffe4c4 => "bisque",
+    0xfffafa => "snow",
+    0xfffff0 => "ivory",
+    _ => return None,
+  };
+
+  Some(s)
+}
+
+struct RelativeComponentParser {
+  names: (&'static str, &'static str, &'static str),
+  components: (f32, f32, f32, f32),
+  types: (ChannelType, ChannelType, ChannelType),
+}
+
+impl RelativeComponentParser {
+  fn new<T: ColorSpace>(color: &T) -> Self {
+    Self {
+      names: color.channels(),
+      components: color.components(),
+      types: color.types(),
+    }
+  }
+
+  fn get_ident(&self, ident: &str, allowed_types: ChannelType) -> Option<(f32, ChannelType)> {
+    if ident.eq_ignore_ascii_case(self.names.0) && allowed_types.intersects(self.types.0) {
+      return Some((self.components.0, self.types.0));
+    }
+
+    if ident.eq_ignore_ascii_case(self.names.1) && allowed_types.intersects(self.types.1) {
+      return Some((self.components.1, self.types.1));
+    }
+
+    if ident.eq_ignore_ascii_case(self.names.2) && allowed_types.intersects(self.types.2) {
+      return Some((self.components.2, self.types.2));
+    }
+
+    if ident.eq_ignore_ascii_case("alpha")
+      && allowed_types.intersects(ChannelType::Number | ChannelType::Percentage)
+    {
+      return Some((self.components.3, ChannelType::Number));
+    }
+
+    None
+  }
+
+  fn parse_ident<'i, 't>(
+    &self,
+    input: &mut Parser<'i, 't>,
+    allowed_types: ChannelType,
+  ) -> Result<(f32, ChannelType), ParseError<'i, ParserError<'i>>> {
+    match self.get_ident(input.expect_ident()?.as_ref(), allowed_types) {
+      Some(v) => Ok(v),
+      None => Err(input.new_error_for_next_token()),
+    }
+  }
+}
+
+impl<'i> ColorParser<'i> for RelativeComponentParser {
+  type Output = cssparser_color::Color;
+  type Error = ParserError<'i>;
+
+  fn parse_angle_or_number<'t>(
+    &self,
+    input: &mut Parser<'i, 't>,
+  ) -> Result<AngleOrNumber, ParseError<'i, Self::Error>> {
+    if let Ok((value, ty)) =
+      input.try_parse(|input| self.parse_ident(input, ChannelType::Angle | ChannelType::Number))
+    {
+      return Ok(match ty {
+        ChannelType::Angle => AngleOrNumber::Angle { degrees: value },
+        ChannelType::Number => AngleOrNumber::Number { value },
+        _ => unreachable!(),
+      });
+    }
+
+    if let Ok(value) = input.try_parse(|input| -> Result<AngleOrNumber, ParseError<'i, ParserError<'i>>> {
+      match Calc::parse_with(input, |ident| {
+        self
+          .get_ident(ident, ChannelType::Angle | ChannelType::Number)
+          .map(|(value, ty)| match ty {
+            ChannelType::Angle => Calc::Value(Box::new(Angle::Deg(value))),
+            ChannelType::Number => Calc::Number(value),
+            _ => unreachable!(),
+          })
+      }) {
+        Ok(Calc::Value(v)) => Ok(AngleOrNumber::Angle {
+          degrees: v.to_degrees(),
+        }),
+        Ok(Calc::Number(v)) => Ok(AngleOrNumber::Number { value: v }),
+        _ => Err(input.new_custom_error(ParserError::InvalidValue)),
+      }
+    }) {
+      return Ok(value);
+    }
+
+    Err(input.new_error_for_next_token())
+  }
+
+  fn parse_number<'t>(&self, input: &mut Parser<'i, 't>) -> Result<f32, ParseError<'i, Self::Error>> {
+    if let Ok((value, _)) = input.try_parse(|input| self.parse_ident(input, ChannelType::Number)) {
+      return Ok(value);
+    }
+
+    match Calc::parse_with(input, |ident| {
+      self.get_ident(ident, ChannelType::Number).map(|(v, _)| Calc::Number(v))
+    }) {
+      Ok(Calc::Value(v)) => Ok(*v),
+      Ok(Calc::Number(n)) => Ok(n),
+      _ => Err(input.new_error_for_next_token()),
+    }
+  }
+
+  fn parse_percentage<'t>(&self, input: &mut Parser<'i, 't>) -> Result<f32, ParseError<'i, Self::Error>> {
+    if let Ok((value, _)) = input.try_parse(|input| self.parse_ident(input, ChannelType::Percentage)) {
+      return Ok(value);
+    }
+
+    if let Ok(value) = input.try_parse(|input| -> Result<Percentage, ParseError<'i, ParserError<'i>>> {
+      match Calc::parse_with(input, |ident| {
+        self
+          .get_ident(ident, ChannelType::Percentage)
+          .map(|(v, _)| Calc::Value(Box::new(Percentage(v))))
+      }) {
+        Ok(Calc::Value(v)) => Ok(*v),
+        _ => Err(input.new_custom_error(ParserError::InvalidValue)),
+      }
+    }) {
+      return Ok(value.0);
+    }
+
+    Err(input.new_error_for_next_token())
+  }
+
+  fn parse_number_or_percentage<'t>(
+    &self,
+    input: &mut Parser<'i, 't>,
+  ) -> Result<NumberOrPercentage, ParseError<'i, Self::Error>> {
+    if let Ok((value, ty)) =
+      input.try_parse(|input| self.parse_ident(input, ChannelType::Percentage | ChannelType::Number))
+    {
+      return Ok(match ty {
+        ChannelType::Percentage => NumberOrPercentage::Percentage { unit_value: value },
+        ChannelType::Number => NumberOrPercentage::Number { value },
+        _ => unreachable!(),
+      });
+    }
+
+    if let Ok(value) = input.try_parse(|input| -> Result<NumberOrPercentage, ParseError<'i, ParserError<'i>>> {
+      match Calc::parse_with(input, |ident| {
+        self
+          .get_ident(ident, ChannelType::Percentage | ChannelType::Number)
+          .map(|(value, ty)| match ty {
+            ChannelType::Percentage => Calc::Value(Box::new(Percentage(value))),
+            ChannelType::Number => Calc::Number(value),
+            _ => unreachable!(),
+          })
+      }) {
+        Ok(Calc::Value(v)) => Ok(NumberOrPercentage::Percentage { unit_value: v.0 }),
+        Ok(Calc::Number(v)) => Ok(NumberOrPercentage::Number { value: v }),
+        _ => Err(input.new_custom_error(ParserError::InvalidValue)),
+      }
+    }) {
+      return Ok(value);
+    }
+
+    Err(input.new_error_for_next_token())
+  }
+}
+
+pub(crate) trait LightDarkColor {
+  fn light_dark(light: Self, dark: Self) -> Self;
+}
+
+impl LightDarkColor for CssColor {
+  #[inline]
+  fn light_dark(light: Self, dark: Self) -> Self {
+    CssColor::LightDark(Box::new(light), Box::new(dark))
+  }
+}
+
+pub(crate) struct ComponentParser {
+  pub allow_none: bool,
+  from: Option<RelativeComponentParser>,
+}
+
+impl ComponentParser {
+  pub fn new(allow_none: bool) -> Self {
+    Self { allow_none, from: None }
+  }
+
+  pub fn parse_relative<
+    'i,
+    't,
+    T: TryFrom<CssColor> + ColorSpace,
+    C: LightDarkColor,
+    P: Fn(&mut Parser<'i, 't>, &mut Self) -> Result<C, ParseError<'i, ParserError<'i>>>,
+  >(
+    &mut self,
+    input: &mut Parser<'i, 't>,
+    parse: P,
+  ) -> Result<C, ParseError<'i, ParserError<'i>>> {
+    if input.try_parse(|input| input.expect_ident_matching("from")).is_ok() {
+      let from = CssColor::parse(input)?;
+      return self.parse_from::<T, C, P>(from, input, &parse);
+    }
+
+    parse(input, self)
+  }
+
+  fn parse_from<
+    'i,
+    't,
+    T: TryFrom<CssColor> + ColorSpace,
+    C: LightDarkColor,
+    P: Fn(&mut Parser<'i, 't>, &mut Self) -> Result<C, ParseError<'i, ParserError<'i>>>,
+  >(
+    &mut self,
+    from: CssColor,
+    input: &mut Parser<'i, 't>,
+    parse: &P,
+  ) -> Result<C, ParseError<'i, ParserError<'i>>> {
+    if let CssColor::LightDark(light, dark) = from {
+      let state = input.state();
+      let light = self.parse_from::<T, C, P>(*light, input, parse)?;
+      input.reset(&state);
+      let dark = self.parse_from::<T, C, P>(*dark, input, parse)?;
+      return Ok(C::light_dark(light, dark));
+    }
+
+    let from = T::try_from(from)
+      .map_err(|_| input.new_custom_error(ParserError::InvalidValue))?
+      .resolve();
+    self.from = Some(RelativeComponentParser::new(&from));
+
+    parse(input, self)
+  }
+}
+
+impl<'i> ColorParser<'i> for ComponentParser {
+  type Output = cssparser_color::Color;
+  type Error = ParserError<'i>;
+
+  fn parse_angle_or_number<'t>(
+    &self,
+    input: &mut Parser<'i, 't>,
+  ) -> Result<AngleOrNumber, ParseError<'i, Self::Error>> {
+    if let Some(from) = &self.from {
+      if let Ok(res) = input.try_parse(|input| from.parse_angle_or_number(input)) {
+        return Ok(res);
+      }
+    }
+
+    if let Ok(angle) = input.try_parse(Angle::parse) {
+      Ok(AngleOrNumber::Angle {
+        degrees: angle.to_degrees(),
+      })
+    } else if let Ok(value) = input.try_parse(CSSNumber::parse) {
+      Ok(AngleOrNumber::Number { value })
+    } else if self.allow_none {
+      input.expect_ident_matching("none")?;
+      Ok(AngleOrNumber::Number { value: f32::NAN })
+    } else {
+      Err(input.new_custom_error(ParserError::InvalidValue))
+    }
+  }
+
+  fn parse_number<'t>(&self, input: &mut Parser<'i, 't>) -> Result<f32, ParseError<'i, Self::Error>> {
+    if let Some(from) = &self.from {
+      if let Ok(res) = input.try_parse(|input| from.parse_number(input)) {
+        return Ok(res);
+      }
+    }
+
+    if let Ok(val) = input.try_parse(CSSNumber::parse) {
+      return Ok(val);
+    } else if self.allow_none {
+      input.expect_ident_matching("none")?;
+      Ok(f32::NAN)
+    } else {
+      Err(input.new_custom_error(ParserError::InvalidValue))
+    }
+  }
+
+  fn parse_percentage<'t>(&self, input: &mut Parser<'i, 't>) -> Result<f32, ParseError<'i, Self::Error>> {
+    if let Some(from) = &self.from {
+      if let Ok(res) = input.try_parse(|input| from.parse_percentage(input)) {
+        return Ok(res);
+      }
+    }
+
+    if let Ok(val) = input.try_parse(Percentage::parse) {
+      return Ok(val.0);
+    } else if self.allow_none {
+      input.expect_ident_matching("none")?;
+      Ok(f32::NAN)
+    } else {
+      Err(input.new_custom_error(ParserError::InvalidValue))
+    }
+  }
+
+  fn parse_number_or_percentage<'t>(
+    &self,
+    input: &mut Parser<'i, 't>,
+  ) -> Result<NumberOrPercentage, ParseError<'i, Self::Error>> {
+    if let Some(from) = &self.from {
+      if let Ok(res) = input.try_parse(|input| from.parse_number_or_percentage(input)) {
+        return Ok(res);
+      }
+    }
+
+    if let Ok(value) = input.try_parse(CSSNumber::parse) {
+      Ok(NumberOrPercentage::Number { value })
+    } else if let Ok(value) = input.try_parse(Percentage::parse) {
+      Ok(NumberOrPercentage::Percentage { unit_value: value.0 })
+    } else if self.allow_none {
+      input.expect_ident_matching("none")?;
+      Ok(NumberOrPercentage::Number { value: f32::NAN })
+    } else {
+      Err(input.new_custom_error(ParserError::InvalidValue))
+    }
+  }
+}
+
+// https://www.w3.org/TR/css-color-4/#lab-colors
+fn parse_color_function<'i, 't>(
+  location: SourceLocation,
+  function: CowRcStr<'i>,
+  input: &mut Parser<'i, 't>,
+) -> Result<CssColor, ParseError<'i, ParserError<'i>>> {
+  let mut parser = ComponentParser::new(true);
+
+  match_ignore_ascii_case! {&*function,
+    "lab" => {
+      parse_lab::<LAB, _>(input, &mut parser, 100.0, 125.0, |l, a, b, alpha| {
+        LABColor::LAB(LAB { l, a, b, alpha })
+      })
+    },
+    "oklab" => {
+      parse_lab::<OKLAB, _>(input, &mut parser, 1.0, 0.4, |l, a, b, alpha| {
+        LABColor::OKLAB(OKLAB { l, a, b, alpha })
+      })
+    },
+    "lch" => {
+      parse_lch::<LCH, _>(input, &mut parser, 100.0, 150.0, |l, c, h, alpha| {
+        LABColor::LCH(LCH { l, c, h, alpha })
+      })
+    },
+    "oklch" => {
+      parse_lch::<OKLCH, _>(input, &mut parser, 1.0, 0.4, |l, c, h, alpha| {
+        LABColor::OKLCH(OKLCH { l, c, h, alpha })
+      })
+    },
+    "color" => {
+      let predefined = parse_predefined(input, &mut parser)?;
+      Ok(predefined)
+    },
+    "hsl" | "hsla" => {
+      parse_hsl_hwb::<HSL, _>(input, &mut parser, true, |h, s, l, a| {
+        let hsl = HSL { h, s, l, alpha: a };
+        if !h.is_nan() && !s.is_nan() && !l.is_nan() && !a.is_nan() {
+          CssColor::RGBA(hsl.into())
+        } else {
+          CssColor::Float(Box::new(FloatColor::HSL(hsl)))
+        }
+      })
+    },
+    "hwb" => {
+      parse_hsl_hwb::<HWB, _>(input, &mut parser, false, |h, w, b, a| {
+        let hwb = HWB { h, w, b, alpha: a };
+        if !h.is_nan() && !w.is_nan() && !b.is_nan() && !a.is_nan() {
+          CssColor::RGBA(hwb.into())
+        } else {
+          CssColor::Float(Box::new(FloatColor::HWB(hwb)))
+        }
+      })
+    },
+    "rgb" | "rgba" => {
+       parse_rgb(input, &mut parser)
+    },
+    "color-mix" => {
+      input.parse_nested_block(parse_color_mix)
+    },
+    "light-dark" => {
+      input.parse_nested_block(|input| {
+        let light = match CssColor::parse(input)? {
+          CssColor::LightDark(light, _) => light,
+          light => Box::new(light)
+        };
+        input.expect_comma()?;
+        let dark = match CssColor::parse(input)? {
+          CssColor::LightDark(_, dark) => dark,
+          dark => Box::new(dark)
+        };
+        Ok(CssColor::LightDark(light, dark))
+      })
+    },
+    _ => Err(location.new_unexpected_token_error(
+      cssparser::Token::Ident(function.clone())
+    ))
+  }
+}
+
+/// Parses the lab() and oklab() functions.
+#[inline]
+fn parse_lab<'i, 't, T: TryFrom<CssColor> + ColorSpace, F: Fn(f32, f32, f32, f32) -> LABColor>(
+  input: &mut Parser<'i, 't>,
+  parser: &mut ComponentParser,
+  l_basis: f32,
+  ab_basis: f32,
+  f: F,
+) -> Result<CssColor, ParseError<'i, ParserError<'i>>> {
+  // https://www.w3.org/TR/css-color-4/#funcdef-lab
+  input.parse_nested_block(|input| {
+    parser.parse_relative::<T, _, _>(input, |input, parser| {
+      // f32::max() does not propagate NaN, so use clamp for now until f32::maximum() is stable.
+      let l = parse_number_or_percentage(input, parser, l_basis)?.clamp(0.0, f32::MAX);
+      let a = parse_number_or_percentage(input, parser, ab_basis)?;
+      let b = parse_number_or_percentage(input, parser, ab_basis)?;
+      let alpha = parse_alpha(input, parser)?;
+      let lab = f(l, a, b, alpha);
+
+      Ok(CssColor::LAB(Box::new(lab)))
+    })
+  })
+}
+
+/// Parses the lch() and oklch() functions.
+#[inline]
+fn parse_lch<'i, 't, T: TryFrom<CssColor> + ColorSpace, F: Fn(f32, f32, f32, f32) -> LABColor>(
+  input: &mut Parser<'i, 't>,
+  parser: &mut ComponentParser,
+  l_basis: f32,
+  c_basis: f32,
+  f: F,
+) -> Result<CssColor, ParseError<'i, ParserError<'i>>> {
+  // https://www.w3.org/TR/css-color-4/#funcdef-lch
+  input.parse_nested_block(|input| {
+    parser.parse_relative::<T, _, _>(input, |input, parser| {
+      if let Some(from) = &mut parser.from {
+        // Relative angles should be normalized.
+        // https://www.w3.org/TR/css-color-5/#relative-LCH
+        from.components.2 %= 360.0;
+        if from.components.2 < 0.0 {
+          from.components.2 += 360.0;
+        }
+      }
+
+      let l = parse_number_or_percentage(input, parser, l_basis)?.clamp(0.0, f32::MAX);
+      let c = parse_number_or_percentage(input, parser, c_basis)?.clamp(0.0, f32::MAX);
+      let h = parse_angle_or_number(input, parser)?;
+      let alpha = parse_alpha(input, parser)?;
+      let lab = f(l, c, h, alpha);
+
+      Ok(CssColor::LAB(Box::new(lab)))
+    })
+  })
+}
+
+#[inline]
+fn parse_predefined<'i, 't>(
+  input: &mut Parser<'i, 't>,
+  parser: &mut ComponentParser,
+) -> Result<CssColor, ParseError<'i, ParserError<'i>>> {
+  // https://www.w3.org/TR/css-color-4/#color-function
+  let res = input.parse_nested_block(|input| {
+    let from = if input.try_parse(|input| input.expect_ident_matching("from")).is_ok() {
+      Some(CssColor::parse(input)?)
+    } else {
+      None
+    };
+
+    let colorspace = input.expect_ident_cloned()?;
+
+    if let Some(CssColor::LightDark(light, dark)) = from {
+      let state = input.state();
+      let light = parse_predefined_relative(input, parser, &colorspace, Some(&*light))?;
+      input.reset(&state);
+      let dark = parse_predefined_relative(input, parser, &colorspace, Some(&*dark))?;
+      return Ok(CssColor::LightDark(Box::new(light), Box::new(dark)));
+    }
+
+    parse_predefined_relative(input, parser, &colorspace, from.as_ref())
+  })?;
+
+  Ok(res)
+}
+
+#[inline]
+fn parse_predefined_relative<'i, 't>(
+  input: &mut Parser<'i, 't>,
+  parser: &mut ComponentParser,
+  colorspace: &CowRcStr<'i>,
+  from: Option<&CssColor>,
+) -> Result<CssColor, ParseError<'i, ParserError<'i>>> {
+  let location = input.current_source_location();
+  if let Some(from) = from {
+    let handle_error = |_| input.new_custom_error(ParserError::InvalidValue);
+    parser.from = Some(match_ignore_ascii_case! { &*&colorspace,
+      "srgb" => RelativeComponentParser::new(&SRGB::try_from(from).map_err(handle_error)?.resolve_missing()),
+      "srgb-linear" => RelativeComponentParser::new(&SRGBLinear::try_from(from).map_err(handle_error)?.resolve_missing()),
+      "display-p3" => RelativeComponentParser::new(&P3::try_from(from).map_err(handle_error)?.resolve_missing()),
+      "a98-rgb" => RelativeComponentParser::new(&A98::try_from(from).map_err(handle_error)?.resolve_missing()),
+      "prophoto-rgb" => RelativeComponentParser::new(&ProPhoto::try_from(from).map_err(handle_error)?.resolve_missing()),
+      "rec2020" => RelativeComponentParser::new(&Rec2020::try_from(from).map_err(handle_error)?.resolve_missing()),
+      "xyz-d50" => RelativeComponentParser::new(&XYZd50::try_from(from).map_err(handle_error)?.resolve_missing()),
+      "xyz" | "xyz-d65" => RelativeComponentParser::new(&XYZd65::try_from(from).map_err(handle_error)?.resolve_missing()),
+      _ => return Err(location.new_unexpected_token_error(
+        cssparser::Token::Ident(colorspace.clone())
+      ))
+    });
+  }
+
+  // Out of gamut values should not be clamped, i.e. values < 0 or > 1 should be preserved.
+  // The browser will gamut-map the color for the target device that it is rendered on.
+  let a = input.try_parse(|input| parse_number_or_percentage(input, parser, 1.0))?;
+  let b = input.try_parse(|input| parse_number_or_percentage(input, parser, 1.0))?;
+  let c = input.try_parse(|input| parse_number_or_percentage(input, parser, 1.0))?;
+  let alpha = parse_alpha(input, parser)?;
+
+  let res = match_ignore_ascii_case! { &*&colorspace,
+    "srgb" => PredefinedColor::SRGB(SRGB { r: a, g: b, b: c, alpha }),
+    "srgb-linear" => PredefinedColor::SRGBLinear(SRGBLinear { r: a, g: b, b: c, alpha }),
+    "display-p3" => PredefinedColor::DisplayP3(P3 { r: a, g: b, b: c, alpha }),
+    "a98-rgb" => PredefinedColor::A98(A98 { r: a, g: b, b: c, alpha }),
+    "prophoto-rgb" => PredefinedColor::ProPhoto(ProPhoto { r: a, g: b, b: c, alpha }),
+    "rec2020" => PredefinedColor::Rec2020(Rec2020 { r: a, g: b, b: c, alpha }),
+    "xyz-d50" => PredefinedColor::XYZd50(XYZd50 { x: a, y: b, z: c, alpha}),
+    "xyz" | "xyz-d65" => PredefinedColor::XYZd65(XYZd65 { x: a, y: b, z: c, alpha }),
+    _ => return Err(location.new_unexpected_token_error(
+      cssparser::Token::Ident(colorspace.clone())
+    ))
+  };
+
+  Ok(CssColor::Predefined(Box::new(res)))
+}
+
+/// Parses the hsl() and hwb() functions.
+/// The results of this function are stored as floating point if there are any `none` components.
+#[inline]
+fn parse_hsl_hwb<'i, 't, T: TryFrom<CssColor> + ColorSpace, F: Fn(f32, f32, f32, f32) -> CssColor>(
+  input: &mut Parser<'i, 't>,
+  parser: &mut ComponentParser,
+  allows_legacy: bool,
+  f: F,
+) -> Result<CssColor, ParseError<'i, ParserError<'i>>> {
+  // https://drafts.csswg.org/css-color-4/#the-hsl-notation
+  input.parse_nested_block(|input| {
+    parser.parse_relative::<T, _, _>(input, |input, parser| {
+      let (h, a, b, is_legacy) = parse_hsl_hwb_components::<T>(input, parser, allows_legacy)?;
+      let alpha = if is_legacy {
+        parse_legacy_alpha(input, parser)?
+      } else {
+        parse_alpha(input, parser)?
+      };
+
+      Ok(f(h, a, b, alpha))
+    })
+  })
+}
+
+#[inline]
+pub(crate) fn parse_hsl_hwb_components<'i, 't, T: TryFrom<CssColor> + ColorSpace>(
+  input: &mut Parser<'i, 't>,
+  parser: &mut ComponentParser,
+  allows_legacy: bool,
+) -> Result<(f32, f32, f32, bool), ParseError<'i, ParserError<'i>>> {
+  let h = parse_angle_or_number(input, parser)?;
+  let is_legacy_syntax =
+    allows_legacy && parser.from.is_none() && !h.is_nan() && input.try_parse(|p| p.expect_comma()).is_ok();
+  let a = parse_number_or_percentage(input, parser, 100.0)?.clamp(0.0, 100.0);
+  if is_legacy_syntax {
+    input.expect_comma()?;
+  }
+  let b = parse_number_or_percentage(input, parser, 100.0)?.clamp(0.0, 100.0);
+  if is_legacy_syntax && (a.is_nan() || b.is_nan()) {
+    return Err(input.new_custom_error(ParserError::InvalidValue));
+  }
+  Ok((h, a, b, is_legacy_syntax))
+}
+
+#[inline]
+fn parse_rgb<'i, 't>(
+  input: &mut Parser<'i, 't>,
+  parser: &mut ComponentParser,
+) -> Result<CssColor, ParseError<'i, ParserError<'i>>> {
+  // https://drafts.csswg.org/css-color-4/#rgb-functions
+  input.parse_nested_block(|input| {
+    parser.parse_relative::<RGB, _, _>(input, |input, parser| {
+      let (r, g, b, is_legacy) = parse_rgb_components(input, parser)?;
+      let alpha = if is_legacy {
+        parse_legacy_alpha(input, parser)?
+      } else {
+        parse_alpha(input, parser)?
+      };
+
+      if !r.is_nan() && !g.is_nan() && !b.is_nan() && !alpha.is_nan() {
+        if is_legacy {
+          Ok(CssColor::RGBA(RGBA::new(r as u8, g as u8, b as u8, alpha)))
+        } else {
+          Ok(CssColor::RGBA(RGBA::from_floats(
+            r / 255.0,
+            g / 255.0,
+            b / 255.0,
+            alpha,
+          )))
+        }
+      } else {
+        Ok(CssColor::Float(Box::new(FloatColor::RGB(RGB { r, g, b, alpha }))))
+      }
+    })
+  })
+}
+
+#[inline]
+pub(crate) fn parse_rgb_components<'i, 't>(
+  input: &mut Parser<'i, 't>,
+  parser: &mut ComponentParser,
+) -> Result<(f32, f32, f32, bool), ParseError<'i, ParserError<'i>>> {
+  let red = parser.parse_number_or_percentage(input)?;
+  let is_legacy_syntax =
+    parser.from.is_none() && !red.unit_value().is_nan() && input.try_parse(|p| p.expect_comma()).is_ok();
+  let (r, g, b) = if is_legacy_syntax {
+    match red {
+      NumberOrPercentage::Number { value } => {
+        let r = value.round().clamp(0.0, 255.0);
+        let g = parser.parse_number(input)?.round().clamp(0.0, 255.0);
+        input.expect_comma()?;
+        let b = parser.parse_number(input)?.round().clamp(0.0, 255.0);
+        (r, g, b)
+      }
+      NumberOrPercentage::Percentage { unit_value } => {
+        let r = (unit_value * 255.0).round().clamp(0.0, 255.0);
+        let g = (parser.parse_percentage(input)? * 255.0).round().clamp(0.0, 255.0);
+        input.expect_comma()?;
+        let b = (parser.parse_percentage(input)? * 255.0).round().clamp(0.0, 255.0);
+        (r, g, b)
+      }
+    }
+  } else {
+    #[inline]
+    fn get_component<'i, 't>(value: NumberOrPercentage) -> f32 {
+      match value {
+        NumberOrPercentage::Number { value } if value.is_nan() => value,
+        NumberOrPercentage::Number { value } => value.round().clamp(0.0, 255.0),
+        NumberOrPercentage::Percentage { unit_value } => (unit_value * 255.0).round().clamp(0.0, 255.0),
+      }
+    }
+
+    let r = get_component(red);
+    let g = get_component(parser.parse_number_or_percentage(input)?);
+    let b = get_component(parser.parse_number_or_percentage(input)?);
+    (r, g, b)
+  };
+
+  if is_legacy_syntax && (g.is_nan() || b.is_nan()) {
+    return Err(input.new_custom_error(ParserError::InvalidValue));
+  }
+  Ok((r, g, b, is_legacy_syntax))
+}
+
+#[inline]
+fn parse_angle_or_number<'i, 't>(
+  input: &mut Parser<'i, 't>,
+  parser: &ComponentParser,
+) -> Result<f32, ParseError<'i, ParserError<'i>>> {
+  Ok(match parser.parse_angle_or_number(input)? {
+    AngleOrNumber::Number { value } => value,
+    AngleOrNumber::Angle { degrees } => degrees,
+  })
+}
+
+#[inline]
+fn parse_number_or_percentage<'i, 't>(
+  input: &mut Parser<'i, 't>,
+  parser: &ComponentParser,
+  percent_basis: f32,
+) -> Result<f32, ParseError<'i, ParserError<'i>>> {
+  Ok(match parser.parse_number_or_percentage(input)? {
+    NumberOrPercentage::Number { value } => value,
+    NumberOrPercentage::Percentage { unit_value } => unit_value * percent_basis,
+  })
+}
+
+#[inline]
+fn parse_alpha<'i, 't>(
+  input: &mut Parser<'i, 't>,
+  parser: &ComponentParser,
+) -> Result<f32, ParseError<'i, ParserError<'i>>> {
+  let res = if input.try_parse(|input| input.expect_delim('/')).is_ok() {
+    parse_number_or_percentage(input, parser, 1.0)?.clamp(0.0, 1.0)
+  } else {
+    1.0
+  };
+  Ok(res)
+}
+
+#[inline]
+fn parse_legacy_alpha<'i, 't>(
+  input: &mut Parser<'i, 't>,
+  parser: &ComponentParser,
+) -> Result<f32, ParseError<'i, ParserError<'i>>> {
+  Ok(if !input.is_exhausted() {
+    input.expect_comma()?;
+    parse_number_or_percentage(input, parser, 1.0)?.clamp(0.0, 1.0)
+  } else {
+    1.0
+  })
+}
+
+#[inline]
+fn write_components<W>(
+  name: &str,
+  a: f32,
+  b: f32,
+  c: f32,
+  alpha: f32,
+  dest: &mut Printer<W>,
+) -> Result<(), PrinterError>
+where
+  W: std::fmt::Write,
+{
+  dest.write_str(name)?;
+  dest.write_char('(')?;
+  if a.is_nan() {
+    dest.write_str("none")?;
+  } else {
+    Percentage(a).to_css(dest)?;
+  }
+  dest.write_char(' ')?;
+  write_component(b, dest)?;
+  dest.write_char(' ')?;
+  write_component(c, dest)?;
+  if alpha.is_nan() || (alpha - 1.0).abs() > f32::EPSILON {
+    dest.delim('/', true)?;
+    write_component(alpha, dest)?;
+  }
+
+  dest.write_char(')')
+}
+
+#[inline]
+fn write_component<W>(c: f32, dest: &mut Printer<W>) -> Result<(), PrinterError>
+where
+  W: std::fmt::Write,
+{
+  if c.is_nan() {
+    dest.write_str("none")?;
+  } else {
+    c.to_css(dest)?;
+  }
+  Ok(())
+}
+
+#[inline]
+fn write_predefined<W>(predefined: &PredefinedColor, dest: &mut Printer<W>) -> Result<(), PrinterError>
+where
+  W: std::fmt::Write,
+{
+  use PredefinedColor::*;
+
+  let (name, a, b, c, alpha) = match predefined {
+    SRGB(rgb) => ("srgb", rgb.r, rgb.g, rgb.b, rgb.alpha),
+    SRGBLinear(rgb) => ("srgb-linear", rgb.r, rgb.g, rgb.b, rgb.alpha),
+    DisplayP3(rgb) => ("display-p3", rgb.r, rgb.g, rgb.b, rgb.alpha),
+    A98(rgb) => ("a98-rgb", rgb.r, rgb.g, rgb.b, rgb.alpha),
+    ProPhoto(rgb) => ("prophoto-rgb", rgb.r, rgb.g, rgb.b, rgb.alpha),
+    Rec2020(rgb) => ("rec2020", rgb.r, rgb.g, rgb.b, rgb.alpha),
+    XYZd50(xyz) => ("xyz-d50", xyz.x, xyz.y, xyz.z, xyz.alpha),
+    // "xyz" has better compatibility (Safari 15) than "xyz-d65", and it is shorter.
+    XYZd65(xyz) => ("xyz", xyz.x, xyz.y, xyz.z, xyz.alpha),
+  };
+
+  dest.write_str("color(")?;
+  dest.write_str(name)?;
+  dest.write_char(' ')?;
+  write_component(a, dest)?;
+  dest.write_char(' ')?;
+  write_component(b, dest)?;
+  dest.write_char(' ')?;
+  write_component(c, dest)?;
+
+  if alpha.is_nan() || (alpha - 1.0).abs() > f32::EPSILON {
+    dest.delim('/', true)?;
+    write_component(alpha, dest)?;
+  }
+
+  dest.write_char(')')
+}
+
+bitflags! {
+  /// A channel type for a color space.
+  #[derive(PartialEq, Eq, Clone, Copy)]
+  pub struct ChannelType: u8 {
+    /// Channel represents a percentage.
+    const Percentage = 0b001;
+    /// Channel represents an angle.
+    const Angle = 0b010;
+    /// Channel represents a number.
+    const Number = 0b100;
+  }
+}
+
+/// A trait for color spaces.
+pub trait ColorSpace {
+  /// Returns the raw color component values.
+  fn components(&self) -> (f32, f32, f32, f32);
+  /// Returns the channel names for this color space.
+  fn channels(&self) -> (&'static str, &'static str, &'static str);
+  /// Returns the channel types for this color space.
+  fn types(&self) -> (ChannelType, ChannelType, ChannelType);
+  /// Resolves missing color components (e.g. `none` keywords) in the color.
+  fn resolve_missing(&self) -> Self;
+  /// Returns a resolved color by replacing missing (i.e. `none`) components with zero,
+  /// and performing gamut mapping to ensure the color can be represented within the color space.
+  fn resolve(&self) -> Self;
+}
+
+macro_rules! define_colorspace {
+  (
+    $(#[$outer:meta])*
+    $vis:vis struct $name:ident {
+      $(#[$a_meta: meta])*
+      $a: ident: $at: ident,
+      $(#[$b_meta: meta])*
+      $b: ident: $bt: ident,
+      $(#[$c_meta: meta])*
+      $c: ident: $ct: ident
+    }
+  ) => {
+    $(#[$outer])*
+    #[derive(Debug, Clone, Copy, PartialEq)] #[cfg_attr(feature = "visitor", derive(Visit))]
+    #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+    #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+    pub struct $name {
+      $(#[$a_meta])*
+      pub $a: f32,
+      $(#[$b_meta])*
+      pub $b: f32,
+      $(#[$c_meta])*
+      pub $c: f32,
+      /// The alpha component.
+      pub alpha: f32,
+    }
+
+    impl ColorSpace for $name {
+      fn components(&self) -> (f32, f32, f32, f32) {
+        (self.$a, self.$b, self.$c, self.alpha)
+      }
+
+      fn channels(&self) -> (&'static str, &'static str, &'static str) {
+        (stringify!($a), stringify!($b), stringify!($c))
+      }
+
+      fn types(&self) -> (ChannelType, ChannelType, ChannelType) {
+        (ChannelType::$at, ChannelType::$bt, ChannelType::$ct)
+      }
+
+      #[inline]
+      fn resolve_missing(&self) -> Self {
+        Self {
+          $a: if self.$a.is_nan() { 0.0 } else { self.$a },
+          $b: if self.$b.is_nan() { 0.0 } else { self.$b },
+          $c: if self.$c.is_nan() { 0.0 } else { self.$c },
+          alpha: if self.alpha.is_nan() { 0.0 } else { self.alpha },
+        }
+      }
+
+      #[inline]
+      fn resolve(&self) -> Self {
+        let mut resolved = self.resolve_missing();
+        if !resolved.in_gamut() {
+          resolved = map_gamut(resolved);
+        }
+        resolved
+      }
+    }
+  };
+}
+
+define_colorspace! {
+  /// A color in the [`sRGB`](https://www.w3.org/TR/css-color-4/#predefined-sRGB) color space.
+  pub struct SRGB {
+    /// The red component.
+    r: Number,
+    /// The green component.
+    g: Number,
+    /// The blue component.
+    b: Number
+  }
+}
+
+// Copied from an older version of cssparser.
+/// A color with red, green, blue, and alpha components, in a byte each.
+#[derive(Clone, Copy, PartialEq, Debug)]
+pub struct RGBA {
+  /// The red component.
+  pub red: u8,
+  /// The green component.
+  pub green: u8,
+  /// The blue component.
+  pub blue: u8,
+  /// The alpha component.
+  pub alpha: u8,
+}
+
+impl RGBA {
+  /// Constructs a new RGBA value from float components. It expects the red,
+  /// green, blue and alpha channels in that order, and all values will be
+  /// clamped to the 0.0 ... 1.0 range.
+  #[inline]
+  pub fn from_floats(red: f32, green: f32, blue: f32, alpha: f32) -> Self {
+    Self::new(clamp_unit_f32(red), clamp_unit_f32(green), clamp_unit_f32(blue), alpha)
+  }
+
+  /// Returns a transparent color.
+  #[inline]
+  pub fn transparent() -> Self {
+    Self::new(0, 0, 0, 0.0)
+  }
+
+  /// Same thing, but with `u8` values instead of floats in the 0 to 1 range.
+  #[inline]
+  pub fn new(red: u8, green: u8, blue: u8, alpha: f32) -> Self {
+    RGBA {
+      red,
+      green,
+      blue,
+      alpha: clamp_unit_f32(alpha),
+    }
+  }
+
+  /// Returns the red channel in a floating point number form, from 0 to 1.
+  #[inline]
+  pub fn red_f32(&self) -> f32 {
+    self.red as f32 / 255.0
+  }
+
+  /// Returns the green channel in a floating point number form, from 0 to 1.
+  #[inline]
+  pub fn green_f32(&self) -> f32 {
+    self.green as f32 / 255.0
+  }
+
+  /// Returns the blue channel in a floating point number form, from 0 to 1.
+  #[inline]
+  pub fn blue_f32(&self) -> f32 {
+    self.blue as f32 / 255.0
+  }
+
+  /// Returns the alpha channel in a floating point number form, from 0 to 1.
+  #[inline]
+  pub fn alpha_f32(&self) -> f32 {
+    self.alpha as f32 / 255.0
+  }
+}
+
+fn clamp_unit_f32(val: f32) -> u8 {
+  // Whilst scaling by 256 and flooring would provide
+  // an equal distribution of integers to percentage inputs,
+  // this is not what Gecko does so we instead multiply by 255
+  // and round (adding 0.5 and flooring is equivalent to rounding)
+  //
+  // Chrome does something similar for the alpha value, but not
+  // the rgb values.
+  //
+  // See https://bugzilla.mozilla.org/show_bug.cgi?id=1340484
+  //
+  // Clamping to 256 and rounding after would let 1.0 map to 256, and
+  // `256.0_f32 as u8` is undefined behavior:
+  //
+  // https://github.com/rust-lang/rust/issues/10184
+  clamp_floor_256_f32(val * 255.)
+}
+
+fn clamp_floor_256_f32(val: f32) -> u8 {
+  val.round().max(0.).min(255.) as u8
+}
+
+define_colorspace! {
+  /// A color in the [`RGB`](https://w3c.github.io/csswg-drafts/css-color-4/#rgb-functions) color space.
+  /// Components are in the 0-255 range.
+  pub struct RGB {
+    /// The red component.
+    r: Number,
+    /// The green component.
+    g: Number,
+    /// The blue component.
+    b: Number
+  }
+}
+
+define_colorspace! {
+  /// A color in the [`sRGB-linear`](https://www.w3.org/TR/css-color-4/#predefined-sRGB-linear) color space.
+  pub struct SRGBLinear {
+    /// The red component.
+    r: Number,
+    /// The green component.
+    g: Number,
+    /// The blue component.
+    b: Number
+  }
+}
+
+define_colorspace! {
+  /// A color in the [`display-p3`](https://www.w3.org/TR/css-color-4/#predefined-display-p3) color space.
+  pub struct P3 {
+    /// The red component.
+    r: Number,
+    /// The green component.
+    g: Number,
+    /// The blue component.
+    b: Number
+  }
+}
+
+define_colorspace! {
+  /// A color in the [`a98-rgb`](https://www.w3.org/TR/css-color-4/#predefined-a98-rgb) color space.
+  pub struct A98 {
+    /// The red component.
+    r: Number,
+    /// The green component.
+    g: Number,
+    /// The blue component.
+    b: Number
+  }
+}
+
+define_colorspace! {
+  /// A color in the [`prophoto-rgb`](https://www.w3.org/TR/css-color-4/#predefined-prophoto-rgb) color space.
+  pub struct ProPhoto {
+    /// The red component.
+    r: Number,
+    /// The green component.
+    g: Number,
+    /// The blue component.
+    b: Number
+  }
+}
+
+define_colorspace! {
+  /// A color in the [`rec2020`](https://www.w3.org/TR/css-color-4/#predefined-rec2020) color space.
+  pub struct Rec2020 {
+    /// The red component.
+    r: Number,
+    /// The green component.
+    g: Number,
+    /// The blue component.
+    b: Number
+  }
+}
+
+define_colorspace! {
+  /// A color in the [CIE Lab](https://www.w3.org/TR/css-color-4/#cie-lab) color space.
+  pub struct LAB {
+    /// The lightness component.
+    l: Number,
+    /// The a component.
+    a: Number,
+    /// The b component.
+    b: Number
+  }
+}
+
+define_colorspace! {
+  /// A color in the [CIE LCH](https://www.w3.org/TR/css-color-4/#cie-lab) color space.
+  pub struct LCH {
+    /// The lightness component.
+    l: Number,
+    /// The chroma component.
+    c: Number,
+    /// The hue component.
+    h: Angle
+  }
+}
+
+define_colorspace! {
+  /// A color in the [OKLab](https://www.w3.org/TR/css-color-4/#ok-lab) color space.
+  pub struct OKLAB {
+    /// The lightness component.
+    l: Number,
+    /// The a component.
+    a: Number,
+    /// The b component.
+    b: Number
+  }
+}
+
+define_colorspace! {
+  /// A color in the [OKLCH](https://www.w3.org/TR/css-color-4/#ok-lab) color space.
+  pub struct OKLCH {
+    /// The lightness component.
+    l: Number,
+    /// The chroma component.
+    c: Number,
+    /// The hue component.
+    h: Angle
+  }
+}
+
+define_colorspace! {
+  /// A color in the [`xyz-d50`](https://www.w3.org/TR/css-color-4/#predefined-xyz) color space.
+  pub struct XYZd50 {
+    /// The x component.
+    x: Number,
+    /// The y component.
+    y: Number,
+    /// The z component.
+    z: Number
+  }
+}
+
+define_colorspace! {
+  /// A color in the [`xyz-d65`](https://www.w3.org/TR/css-color-4/#predefined-xyz) color space.
+  pub struct XYZd65 {
+    /// The x component.
+    x: Number,
+    /// The y component.
+    y: Number,
+    /// The z component.
+    z: Number
+  }
+}
+
+define_colorspace! {
+  /// A color in the [`hsl`](https://www.w3.org/TR/css-color-4/#the-hsl-notation) color space.
+  pub struct HSL {
+    /// The hue component.
+    h: Angle,
+    /// The saturation component.
+    s: Number,
+    /// The lightness component.
+    l: Number
+  }
+}
+
+define_colorspace! {
+  /// A color in the [`hwb`](https://www.w3.org/TR/css-color-4/#the-hwb-notation) color space.
+  pub struct HWB {
+    /// The hue component.
+    h: Angle,
+    /// The whiteness component.
+    w: Number,
+    /// The blackness component.
+    b: Number
+  }
+}
+
+macro_rules! via {
+  ($t: ident -> $u: ident -> $v: ident) => {
+    impl From<$t> for $v {
+      #[inline]
+      fn from(t: $t) -> $v {
+        let xyz: $u = t.into();
+        xyz.into()
+      }
+    }
+
+    impl From<$v> for $t {
+      #[inline]
+      fn from(t: $v) -> $t {
+        let xyz: $u = t.into();
+        xyz.into()
+      }
+    }
+  };
+}
+
+#[inline]
+fn rectangular_to_polar(l: f32, a: f32, b: f32) -> (f32, f32, f32) {
+  // https://github.com/w3c/csswg-drafts/blob/fba005e2ce9bcac55b49e4aa19b87208b3a0631e/css-color-4/conversions.js#L375
+  let mut h = b.atan2(a) * 180.0 / PI;
+  if h < 0.0 {
+    h += 360.0;
+  }
+  let c = (a.powi(2) + b.powi(2)).sqrt();
+  h = h % 360.0;
+  (l, c, h)
+}
+
+#[inline]
+fn polar_to_rectangular(l: f32, c: f32, h: f32) -> (f32, f32, f32) {
+  // https://github.com/w3c/csswg-drafts/blob/fba005e2ce9bcac55b49e4aa19b87208b3a0631e/css-color-4/conversions.js#L385
+  let a = c * (h * PI / 180.0).cos();
+  let b = c * (h * PI / 180.0).sin();
+  (l, a, b)
+}
+
+impl From<LCH> for LAB {
+  fn from(lch: LCH) -> LAB {
+    let lch = lch.resolve_missing();
+    let (l, a, b) = polar_to_rectangular(lch.l, lch.c, lch.h);
+    LAB {
+      l,
+      a,
+      b,
+      alpha: lch.alpha,
+    }
+  }
+}
+
+impl From<LAB> for LCH {
+  fn from(lab: LAB) -> LCH {
+    let lab = lab.resolve_missing();
+    let (l, c, h) = rectangular_to_polar(lab.l, lab.a, lab.b);
+    LCH {
+      l,
+      c,
+      h,
+      alpha: lab.alpha,
+    }
+  }
+}
+
+impl From<OKLCH> for OKLAB {
+  fn from(lch: OKLCH) -> OKLAB {
+    let lch = lch.resolve_missing();
+    let (l, a, b) = polar_to_rectangular(lch.l, lch.c, lch.h);
+    OKLAB {
+      l,
+      a,
+      b,
+      alpha: lch.alpha,
+    }
+  }
+}
+
+impl From<OKLAB> for OKLCH {
+  fn from(lab: OKLAB) -> OKLCH {
+    let lab = lab.resolve_missing();
+    let (l, c, h) = rectangular_to_polar(lab.l, lab.a, lab.b);
+    OKLCH {
+      l,
+      c,
+      h,
+      alpha: lab.alpha,
+    }
+  }
+}
+
+const D50: &[f32] = &[0.3457 / 0.3585, 1.00000, (1.0 - 0.3457 - 0.3585) / 0.3585];
+
+impl From<LAB> for XYZd50 {
+  fn from(lab: LAB) -> XYZd50 {
+    // https://github.com/w3c/csswg-drafts/blob/fba005e2ce9bcac55b49e4aa19b87208b3a0631e/css-color-4/conversions.js#L352
+    const K: f32 = 24389.0 / 27.0; // 29^3/3^3
+    const E: f32 = 216.0 / 24389.0; // 6^3/29^3
+
+    let lab = lab.resolve_missing();
+    let l = lab.l;
+    let a = lab.a;
+    let b = lab.b;
+
+    // compute f, starting with the luminance-related term
+    let f1 = (l + 16.0) / 116.0;
+    let f0 = a / 500.0 + f1;
+    let f2 = f1 - b / 200.0;
+
+    // compute xyz
+    let x = if f0.powi(3) > E {
+      f0.powi(3)
+    } else {
+      (116.0 * f0 - 16.0) / K
+    };
+
+    let y = if l > K * E { ((l + 16.0) / 116.0).powi(3) } else { l / K };
+
+    let z = if f2.powi(3) > E {
+      f2.powi(3)
+    } else {
+      (116.0 * f2 - 16.0) / K
+    };
+
+    // Compute XYZ by scaling xyz by reference white
+    XYZd50 {
+      x: x * D50[0],
+      y: y * D50[1],
+      z: z * D50[2],
+      alpha: lab.alpha,
+    }
+  }
+}
+
+impl From<XYZd50> for XYZd65 {
+  fn from(xyz: XYZd50) -> XYZd65 {
+    // https://github.com/w3c/csswg-drafts/blob/fba005e2ce9bcac55b49e4aa19b87208b3a0631e/css-color-4/conversions.js#L319
+    const MATRIX: &[f32] = &[
+      0.9554734527042182,
+      -0.023098536874261423,
+      0.0632593086610217,
+      -0.028369706963208136,
+      1.0099954580058226,
+      0.021041398966943008,
+      0.012314001688319899,
+      -0.020507696433477912,
+      1.3303659366080753,
+    ];
+
+    let xyz = xyz.resolve_missing();
+    let (x, y, z) = multiply_matrix(MATRIX, xyz.x, xyz.y, xyz.z);
+    XYZd65 {
+      x,
+      y,
+      z,
+      alpha: xyz.alpha,
+    }
+  }
+}
+
+impl From<XYZd65> for XYZd50 {
+  fn from(xyz: XYZd65) -> XYZd50 {
+    // https://github.com/w3c/csswg-drafts/blob/fba005e2ce9bcac55b49e4aa19b87208b3a0631e/css-color-4/conversions.js#L319
+    const MATRIX: &[f32] = &[
+      1.0479298208405488,
+      0.022946793341019088,
+      -0.05019222954313557,
+      0.029627815688159344,
+      0.990434484573249,
+      -0.01707382502938514,
+      -0.009243058152591178,
+      0.015055144896577895,
+      0.7518742899580008,
+    ];
+
+    let xyz = xyz.resolve_missing();
+    let (x, y, z) = multiply_matrix(MATRIX, xyz.x, xyz.y, xyz.z);
+    XYZd50 {
+      x,
+      y,
+      z,
+      alpha: xyz.alpha,
+    }
+  }
+}
+
+impl From<XYZd65> for SRGBLinear {
+  fn from(xyz: XYZd65) -> SRGBLinear {
+    // https://github.com/w3c/csswg-drafts/blob/fba005e2ce9bcac55b49e4aa19b87208b3a0631e/css-color-4/conversions.js#L62
+    const MATRIX: &[f32] = &[
+      3.2409699419045226,
+      -1.537383177570094,
+      -0.4986107602930034,
+      -0.9692436362808796,
+      1.8759675015077202,
+      0.04155505740717559,
+      0.05563007969699366,
+      -0.20397695888897652,
+      1.0569715142428786,
+    ];
+
+    let xyz = xyz.resolve_missing();
+    let (r, g, b) = multiply_matrix(MATRIX, xyz.x, xyz.y, xyz.z);
+    SRGBLinear {
+      r,
+      g,
+      b,
+      alpha: xyz.alpha,
+    }
+  }
+}
+
+#[inline]
+fn multiply_matrix(m: &[f32], x: f32, y: f32, z: f32) -> (f32, f32, f32) {
+  let a = m[0] * x + m[1] * y + m[2] * z;
+  let b = m[3] * x + m[4] * y + m[5] * z;
+  let c = m[6] * x + m[7] * y + m[8] * z;
+  (a, b, c)
+}
+
+impl From<SRGBLinear> for SRGB {
+  #[inline]
+  fn from(rgb: SRGBLinear) -> SRGB {
+    let rgb = rgb.resolve_missing();
+    let (r, g, b) = gam_srgb(rgb.r, rgb.g, rgb.b);
+    SRGB {
+      r,
+      g,
+      b,
+      alpha: rgb.alpha,
+    }
+  }
+}
+
+fn gam_srgb(r: f32, g: f32, b: f32) -> (f32, f32, f32) {
+  // https://github.com/w3c/csswg-drafts/blob/fba005e2ce9bcac55b49e4aa19b87208b3a0631e/css-color-4/conversions.js#L31
+  // convert an array of linear-light sRGB values in the range 0.0-1.0
+  // to gamma corrected form
+  // https://en.wikipedia.org/wiki/SRGB
+  // Extended transfer function:
+  // For negative values, linear portion extends on reflection
+  // of axis, then uses reflected pow below that
+
+  #[inline]
+  fn gam_srgb_component(c: f32) -> f32 {
+    let abs = c.abs();
+    if abs > 0.0031308 {
+      let sign = if c < 0.0 { -1.0 } else { 1.0 };
+      return sign * (1.055 * abs.powf(1.0 / 2.4) - 0.055);
+    }
+
+    return 12.92 * c;
+  }
+
+  let r = gam_srgb_component(r);
+  let g = gam_srgb_component(g);
+  let b = gam_srgb_component(b);
+  (r, g, b)
+}
+
+impl From<OKLAB> for XYZd65 {
+  fn from(lab: OKLAB) -> XYZd65 {
+    // https://github.com/w3c/csswg-drafts/blob/fba005e2ce9bcac55b49e4aa19b87208b3a0631e/css-color-4/conversions.js#L418
+    const LMS_TO_XYZ: &[f32] = &[
+      1.2268798733741557,
+      -0.5578149965554813,
+      0.28139105017721583,
+      -0.04057576262431372,
+      1.1122868293970594,
+      -0.07171106666151701,
+      -0.07637294974672142,
+      -0.4214933239627914,
+      1.5869240244272418,
+    ];
+
+    const OKLAB_TO_LMS: &[f32] = &[
+      0.99999999845051981432,
+      0.39633779217376785678,
+      0.21580375806075880339,
+      1.0000000088817607767,
+      -0.1055613423236563494,
+      -0.063854174771705903402,
+      1.0000000546724109177,
+      -0.089484182094965759684,
+      -1.2914855378640917399,
+    ];
+
+    let lab = lab.resolve_missing();
+    let (a, b, c) = multiply_matrix(OKLAB_TO_LMS, lab.l, lab.a, lab.b);
+    let (x, y, z) = multiply_matrix(LMS_TO_XYZ, a.powi(3), b.powi(3), c.powi(3));
+    XYZd65 {
+      x,
+      y,
+      z,
+      alpha: lab.alpha,
+    }
+  }
+}
+
+impl From<XYZd65> for OKLAB {
+  fn from(xyz: XYZd65) -> OKLAB {
+    // https://github.com/w3c/csswg-drafts/blob/fba005e2ce9bcac55b49e4aa19b87208b3a0631e/css-color-4/conversions.js#L400
+    const XYZ_TO_LMS: &[f32] = &[
+      0.8190224432164319,
+      0.3619062562801221,
+      -0.12887378261216414,
+      0.0329836671980271,
+      0.9292868468965546,
+      0.03614466816999844,
+      0.048177199566046255,
+      0.26423952494422764,
+      0.6335478258136937,
+    ];
+
+    const LMS_TO_OKLAB: &[f32] = &[
+      0.2104542553,
+      0.7936177850,
+      -0.0040720468,
+      1.9779984951,
+      -2.4285922050,
+      0.4505937099,
+      0.0259040371,
+      0.7827717662,
+      -0.8086757660,
+    ];
+
+    let xyz = xyz.resolve_missing();
+    let (a, b, c) = multiply_matrix(XYZ_TO_LMS, xyz.x, xyz.y, xyz.z);
+    let (l, a, b) = multiply_matrix(LMS_TO_OKLAB, a.cbrt(), b.cbrt(), c.cbrt());
+    OKLAB {
+      l,
+      a,
+      b,
+      alpha: xyz.alpha,
+    }
+  }
+}
+
+impl From<XYZd50> for LAB {
+  fn from(xyz: XYZd50) -> LAB {
+    // https://github.com/w3c/csswg-drafts/blob/fba005e2ce9bcac55b49e4aa19b87208b3a0631e/css-color-4/conversions.js#L332
+    // Assuming XYZ is relative to D50, convert to CIE LAB
+    // from CIE standard, which now defines these as a rational fraction
+    const E: f32 = 216.0 / 24389.0; // 6^3/29^3
+    const K: f32 = 24389.0 / 27.0; // 29^3/3^3
+
+    // compute xyz, which is XYZ scaled relative to reference white
+    let xyz = xyz.resolve_missing();
+    let x = xyz.x / D50[0];
+    let y = xyz.y / D50[1];
+    let z = xyz.z / D50[2];
+
+    // now compute f
+    let f0 = if x > E { x.cbrt() } else { (K * x + 16.0) / 116.0 };
+
+    let f1 = if y > E { y.cbrt() } else { (K * y + 16.0) / 116.0 };
+
+    let f2 = if z > E { z.cbrt() } else { (K * z + 16.0) / 116.0 };
+
+    let l = (116.0 * f1) - 16.0;
+    let a = 500.0 * (f0 - f1);
+    let b = 200.0 * (f1 - f2);
+    LAB {
+      l,
+      a,
+      b,
+      alpha: xyz.alpha,
+    }
+  }
+}
+
+impl From<SRGB> for SRGBLinear {
+  fn from(rgb: SRGB) -> SRGBLinear {
+    let rgb = rgb.resolve_missing();
+    let (r, g, b) = lin_srgb(rgb.r, rgb.g, rgb.b);
+    SRGBLinear {
+      r,
+      g,
+      b,
+      alpha: rgb.alpha,
+    }
+  }
+}
+
+fn lin_srgb(r: f32, g: f32, b: f32) -> (f32, f32, f32) {
+  // https://github.com/w3c/csswg-drafts/blob/fba005e2ce9bcac55b49e4aa19b87208b3a0631e/css-color-4/conversions.js#L11
+  // convert sRGB values where in-gamut values are in the range [0 - 1]
+  // to linear light (un-companded) form.
+  // https://en.wikipedia.org/wiki/SRGB
+  // Extended transfer function:
+  // for negative values, linear portion is extended on reflection of axis,
+  // then reflected power function is used.
+
+  #[inline]
+  fn lin_srgb_component(c: f32) -> f32 {
+    let abs = c.abs();
+    if abs < 0.04045 {
+      return c / 12.92;
+    }
+
+    let sign = if c < 0.0 { -1.0 } else { 1.0 };
+    sign * ((abs + 0.055) / 1.055).powf(2.4)
+  }
+
+  let r = lin_srgb_component(r);
+  let g = lin_srgb_component(g);
+  let b = lin_srgb_component(b);
+  (r, g, b)
+}
+
+impl From<SRGBLinear> for XYZd65 {
+  fn from(rgb: SRGBLinear) -> XYZd65 {
+    // https://github.com/w3c/csswg-drafts/blob/fba005e2ce9bcac55b49e4aa19b87208b3a0631e/css-color-4/conversions.js#L50
+    // convert an array of linear-light sRGB values to CIE XYZ
+    // using sRGB's own white, D65 (no chromatic adaptation)
+    const MATRIX: &[f32] = &[
+      0.41239079926595934,
+      0.357584339383878,
+      0.1804807884018343,
+      0.21263900587151027,
+      0.715168678767756,
+      0.07219231536073371,
+      0.01933081871559182,
+      0.11919477979462598,
+      0.9505321522496607,
+    ];
+
+    let rgb = rgb.resolve_missing();
+    let (x, y, z) = multiply_matrix(MATRIX, rgb.r, rgb.g, rgb.b);
+    XYZd65 {
+      x,
+      y,
+      z,
+      alpha: rgb.alpha,
+    }
+  }
+}
+
+impl From<XYZd65> for P3 {
+  fn from(xyz: XYZd65) -> P3 {
+    // https://github.com/w3c/csswg-drafts/blob/fba005e2ce9bcac55b49e4aa19b87208b3a0631e/css-color-4/conversions.js#L105
+    const MATRIX: &[f32] = &[
+      2.493496911941425,
+      -0.9313836179191239,
+      -0.40271078445071684,
+      -0.8294889695615747,
+      1.7626640603183463,
+      0.023624685841943577,
+      0.03584583024378447,
+      -0.07617238926804182,
+      0.9568845240076872,
+    ];
+
+    let xyz = xyz.resolve_missing();
+    let (r, g, b) = multiply_matrix(MATRIX, xyz.x, xyz.y, xyz.z);
+    let (r, g, b) = gam_srgb(r, g, b); // same as sRGB
+    P3 {
+      r,
+      g,
+      b,
+      alpha: xyz.alpha,
+    }
+  }
+}
+
+impl From<P3> for XYZd65 {
+  fn from(p3: P3) -> XYZd65 {
+    // https://github.com/w3c/csswg-drafts/blob/fba005e2ce9bcac55b49e4aa19b87208b3a0631e/css-color-4/conversions.js#L91
+    // convert linear-light display-p3 values to CIE XYZ
+    // using D65 (no chromatic adaptation)
+    // http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
+    const MATRIX: &[f32] = &[
+      0.4865709486482162,
+      0.26566769316909306,
+      0.1982172852343625,
+      0.2289745640697488,
+      0.6917385218365064,
+      0.079286914093745,
+      0.0000000000000000,
+      0.04511338185890264,
+      1.043944368900976,
+    ];
+
+    let p3 = p3.resolve_missing();
+    let (r, g, b) = lin_srgb(p3.r, p3.g, p3.b);
+    let (x, y, z) = multiply_matrix(MATRIX, r, g, b);
+    XYZd65 {
+      x,
+      y,
+      z,
+      alpha: p3.alpha,
+    }
+  }
+}
+
+impl From<A98> for XYZd65 {
+  fn from(a98: A98) -> XYZd65 {
+    // https://github.com/w3c/csswg-drafts/blob/fba005e2ce9bcac55b49e4aa19b87208b3a0631e/css-color-4/conversions.js#L181
+    #[inline]
+    fn lin_a98rgb_component(c: f32) -> f32 {
+      let sign = if c < 0.0 { -1.0 } else { 1.0 };
+      sign * c.abs().powf(563.0 / 256.0)
+    }
+
+    // convert an array of a98-rgb values in the range 0.0 - 1.0
+    // to linear light (un-companded) form.
+    // negative values are also now accepted
+    let a98 = a98.resolve_missing();
+    let r = lin_a98rgb_component(a98.r);
+    let g = lin_a98rgb_component(a98.g);
+    let b = lin_a98rgb_component(a98.b);
+
+    // convert an array of linear-light a98-rgb values to CIE XYZ
+    // http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
+    // has greater numerical precision than section 4.3.5.3 of
+    // https://www.adobe.com/digitalimag/pdfs/AdobeRGB1998.pdf
+    // but the values below were calculated from first principles
+    // from the chromaticity coordinates of R G B W
+    // see matrixmaker.html
+    const MATRIX: &[f32] = &[
+      0.5766690429101305,
+      0.1855582379065463,
+      0.1882286462349947,
+      0.29734497525053605,
+      0.6273635662554661,
+      0.07529145849399788,
+      0.02703136138641234,
+      0.07068885253582723,
+      0.9913375368376388,
+    ];
+
+    let (x, y, z) = multiply_matrix(MATRIX, r, g, b);
+    XYZd65 {
+      x,
+      y,
+      z,
+      alpha: a98.alpha,
+    }
+  }
+}
+
+impl From<XYZd65> for A98 {
+  fn from(xyz: XYZd65) -> A98 {
+    // https://github.com/w3c/csswg-drafts/blob/fba005e2ce9bcac55b49e4aa19b87208b3a0631e/css-color-4/conversions.js#L222
+    // convert XYZ to linear-light a98-rgb
+    const MATRIX: &[f32] = &[
+      2.0415879038107465,
+      -0.5650069742788596,
+      -0.34473135077832956,
+      -0.9692436362808795,
+      1.8759675015077202,
+      0.04155505740717557,
+      0.013444280632031142,
+      -0.11836239223101838,
+      1.0151749943912054,
+    ];
+
+    #[inline]
+    fn gam_a98_component(c: f32) -> f32 {
+      // https://github.com/w3c/csswg-drafts/blob/fba005e2ce9bcac55b49e4aa19b87208b3a0631e/css-color-4/conversions.js#L193
+      // convert linear-light a98-rgb  in the range 0.0-1.0
+      // to gamma corrected form
+      // negative values are also now accepted
+      let sign = if c < 0.0 { -1.0 } else { 1.0 };
+      sign * c.abs().powf(256.0 / 563.0)
+    }
+
+    let xyz = xyz.resolve_missing();
+    let (r, g, b) = multiply_matrix(MATRIX, xyz.x, xyz.y, xyz.z);
+    let r = gam_a98_component(r);
+    let g = gam_a98_component(g);
+    let b = gam_a98_component(b);
+    A98 {
+      r,
+      g,
+      b,
+      alpha: xyz.alpha,
+    }
+  }
+}
+
+impl From<ProPhoto> for XYZd50 {
+  fn from(prophoto: ProPhoto) -> XYZd50 {
+    // https://github.com/w3c/csswg-drafts/blob/fba005e2ce9bcac55b49e4aa19b87208b3a0631e/css-color-4/conversions.js#L118
+    // convert an array of prophoto-rgb values
+    // where in-gamut colors are in the range [0.0 - 1.0]
+    // to linear light (un-companded) form.
+    // Transfer curve is gamma 1.8 with a small linear portion
+    // Extended transfer function
+
+    #[inline]
+    fn lin_prophoto_component(c: f32) -> f32 {
+      const ET2: f32 = 16.0 / 512.0;
+      let abs = c.abs();
+      if abs <= ET2 {
+        return c / 16.0;
+      }
+
+      let sign = if c < 0.0 { -1.0 } else { 1.0 };
+      sign * c.powf(1.8)
+    }
+
+    let prophoto = prophoto.resolve_missing();
+    let r = lin_prophoto_component(prophoto.r);
+    let g = lin_prophoto_component(prophoto.g);
+    let b = lin_prophoto_component(prophoto.b);
+
+    // https://github.com/w3c/csswg-drafts/blob/fba005e2ce9bcac55b49e4aa19b87208b3a0631e/css-color-4/conversions.js#L155
+    // convert an array of linear-light prophoto-rgb values to CIE XYZ
+    // using  D50 (so no chromatic adaptation needed afterwards)
+    // http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
+    const MATRIX: &[f32] = &[
+      0.7977604896723027,
+      0.13518583717574031,
+      0.0313493495815248,
+      0.2880711282292934,
+      0.7118432178101014,
+      0.00008565396060525902,
+      0.0,
+      0.0,
+      0.8251046025104601,
+    ];
+
+    let (x, y, z) = multiply_matrix(MATRIX, r, g, b);
+    XYZd50 {
+      x,
+      y,
+      z,
+      alpha: prophoto.alpha,
+    }
+  }
+}
+
+impl From<XYZd50> for ProPhoto {
+  fn from(xyz: XYZd50) -> ProPhoto {
+    // convert XYZ to linear-light prophoto-rgb
+    const MATRIX: &[f32] = &[
+      1.3457989731028281,
+      -0.25558010007997534,
+      -0.05110628506753401,
+      -0.5446224939028347,
+      1.5082327413132781,
+      0.02053603239147973,
+      0.0,
+      0.0,
+      1.2119675456389454,
+    ];
+
+    #[inline]
+    fn gam_prophoto_component(c: f32) -> f32 {
+      // https://github.com/w3c/csswg-drafts/blob/fba005e2ce9bcac55b49e4aa19b87208b3a0631e/css-color-4/conversions.js#L137
+      // convert linear-light prophoto-rgb  in the range 0.0-1.0
+      // to gamma corrected form
+      // Transfer curve is gamma 1.8 with a small linear portion
+      // TODO for negative values, extend linear portion on reflection of axis, then add pow below that
+      const ET: f32 = 1.0 / 512.0;
+      let abs = c.abs();
+      if abs >= ET {
+        let sign = if c < 0.0 { -1.0 } else { 1.0 };
+        return sign * abs.powf(1.0 / 1.8);
+      }
+
+      16.0 * c
+    }
+
+    let xyz = xyz.resolve_missing();
+    let (r, g, b) = multiply_matrix(MATRIX, xyz.x, xyz.y, xyz.z);
+    let r = gam_prophoto_component(r);
+    let g = gam_prophoto_component(g);
+    let b = gam_prophoto_component(b);
+    ProPhoto {
+      r,
+      g,
+      b,
+      alpha: xyz.alpha,
+    }
+  }
+}
+
+impl From<Rec2020> for XYZd65 {
+  fn from(rec2020: Rec2020) -> XYZd65 {
+    // https://github.com/w3c/csswg-drafts/blob/fba005e2ce9bcac55b49e4aa19b87208b3a0631e/css-color-4/conversions.js#L235
+    // convert an array of rec2020 RGB values in the range 0.0 - 1.0
+    // to linear light (un-companded) form.
+    // ITU-R BT.2020-2 p.4
+
+    #[inline]
+    fn lin_rec2020_component(c: f32) -> f32 {
+      const A: f32 = 1.09929682680944;
+      const B: f32 = 0.018053968510807;
+
+      let abs = c.abs();
+      if abs < B * 4.5 {
+        return c / 4.5;
+      }
+
+      let sign = if c < 0.0 { -1.0 } else { 1.0 };
+      sign * ((abs + A - 1.0) / A).powf(1.0 / 0.45)
+    }
+
+    let rec2020 = rec2020.resolve_missing();
+    let r = lin_rec2020_component(rec2020.r);
+    let g = lin_rec2020_component(rec2020.g);
+    let b = lin_rec2020_component(rec2020.b);
+
+    // https://github.com/w3c/csswg-drafts/blob/fba005e2ce9bcac55b49e4aa19b87208b3a0631e/css-color-4/conversions.js#L276
+    // convert an array of linear-light rec2020 values to CIE XYZ
+    // using  D65 (no chromatic adaptation)
+    // http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
+    const MATRIX: &[f32] = &[
+      0.6369580483012914,
+      0.14461690358620832,
+      0.1688809751641721,
+      0.2627002120112671,
+      0.6779980715188708,
+      0.05930171646986196,
+      0.000000000000000,
+      0.028072693049087428,
+      1.060985057710791,
+    ];
+
+    let (x, y, z) = multiply_matrix(MATRIX, r, g, b);
+    XYZd65 {
+      x,
+      y,
+      z,
+      alpha: rec2020.alpha,
+    }
+  }
+}
+
+impl From<XYZd65> for Rec2020 {
+  fn from(xyz: XYZd65) -> Rec2020 {
+    // convert XYZ to linear-light rec2020
+    const MATRIX: &[f32] = &[
+      1.7166511879712674,
+      -0.35567078377639233,
+      -0.25336628137365974,
+      -0.6666843518324892,
+      1.6164812366349395,
+      0.01576854581391113,
+      0.017639857445310783,
+      -0.042770613257808524,
+      0.9421031212354738,
+    ];
+
+    #[inline]
+    fn gam_rec2020_component(c: f32) -> f32 {
+      // convert linear-light rec2020 RGB  in the range 0.0-1.0
+      // to gamma corrected form
+      // ITU-R BT.2020-2 p.4
+
+      const A: f32 = 1.09929682680944;
+      const B: f32 = 0.018053968510807;
+
+      let abs = c.abs();
+      if abs > B {
+        let sign = if c < 0.0 { -1.0 } else { 1.0 };
+        return sign * (A * abs.powf(0.45) - (A - 1.0));
+      }
+
+      4.5 * c
+    }
+
+    let xyz = xyz.resolve_missing();
+    let (r, g, b) = multiply_matrix(MATRIX, xyz.x, xyz.y, xyz.z);
+    let r = gam_rec2020_component(r);
+    let g = gam_rec2020_component(g);
+    let b = gam_rec2020_component(b);
+    Rec2020 {
+      r,
+      g,
+      b,
+      alpha: xyz.alpha,
+    }
+  }
+}
+
+impl From<SRGB> for HSL {
+  fn from(rgb: SRGB) -> HSL {
+    // https://drafts.csswg.org/css-color/#rgb-to-hsl
+    let rgb = rgb.resolve();
+    let r = rgb.r;
+    let g = rgb.g;
+    let b = rgb.b;
+    let max = r.max(g).max(b);
+    let min = r.min(g).min(b);
+    let mut h = f32::NAN;
+    let mut s: f32 = 0.0;
+    let l = (min + max) / 2.0;
+    let d = max - min;
+
+    if d != 0.0 {
+      s = if l == 0.0 || l == 1.0 {
+        0.0
+      } else {
+        (max - l) / l.min(1.0 - l)
+      };
+
+      if max == r {
+        h = (g - b) / d + (if g < b { 6.0 } else { 0.0 });
+      } else if max == g {
+        h = (b - r) / d + 2.0;
+      } else if max == b {
+        h = (r - g) / d + 4.0;
+      }
+
+      h = h * 60.0;
+    }
+
+    HSL {
+      h,
+      s: s * 100.0,
+      l: l * 100.0,
+      alpha: rgb.alpha,
+    }
+  }
+}
+
+impl From<HSL> for SRGB {
+  fn from(hsl: HSL) -> SRGB {
+    // https://drafts.csswg.org/css-color/#hsl-to-rgb
+    let hsl = hsl.resolve_missing();
+    let h = (hsl.h - 360.0 * (hsl.h / 360.0).floor()) / 360.0;
+    let (r, g, b) = hsl_to_rgb(h, hsl.s / 100.0, hsl.l / 100.0);
+    SRGB {
+      r,
+      g,
+      b,
+      alpha: hsl.alpha,
+    }
+  }
+}
+
+impl From<SRGB> for HWB {
+  fn from(rgb: SRGB) -> HWB {
+    let rgb = rgb.resolve();
+    let hsl = HSL::from(rgb);
+    let r = rgb.r;
+    let g = rgb.g;
+    let b = rgb.b;
+    let w = r.min(g).min(b);
+    let b = 1.0 - r.max(g).max(b);
+    HWB {
+      h: hsl.h,
+      w: w * 100.0,
+      b: b * 100.0,
+      alpha: rgb.alpha,
+    }
+  }
+}
+
+impl From<HWB> for SRGB {
+  fn from(hwb: HWB) -> SRGB {
+    // https://drafts.csswg.org/css-color/#hwb-to-rgb
+    let hwb = hwb.resolve_missing();
+    let h = hwb.h;
+    let w = hwb.w / 100.0;
+    let b = hwb.b / 100.0;
+
+    if w + b >= 1.0 {
+      let gray = w / (w + b);
+      return SRGB {
+        r: gray,
+        g: gray,
+        b: gray,
+        alpha: hwb.alpha,
+      };
+    }
+
+    let mut rgba = SRGB::from(HSL {
+      h,
+      s: 100.0,
+      l: 50.0,
+      alpha: hwb.alpha,
+    });
+    let x = 1.0 - w - b;
+    rgba.r = rgba.r * x + w;
+    rgba.g = rgba.g * x + w;
+    rgba.b = rgba.b * x + w;
+    rgba
+  }
+}
+
+impl From<RGBA> for SRGB {
+  fn from(rgb: RGBA) -> SRGB {
+    SRGB {
+      r: rgb.red_f32(),
+      g: rgb.green_f32(),
+      b: rgb.blue_f32(),
+      alpha: rgb.alpha_f32(),
+    }
+  }
+}
+
+impl From<SRGB> for RGBA {
+  fn from(rgb: SRGB) -> RGBA {
+    let rgb = rgb.resolve();
+    RGBA::from_floats(rgb.r, rgb.g, rgb.b, rgb.alpha)
+  }
+}
+
+impl From<SRGB> for RGB {
+  fn from(rgb: SRGB) -> Self {
+    RGB {
+      r: rgb.r * 255.0,
+      g: rgb.g * 255.0,
+      b: rgb.b * 255.0,
+      alpha: rgb.alpha,
+    }
+  }
+}
+
+impl From<RGB> for SRGB {
+  fn from(rgb: RGB) -> Self {
+    SRGB {
+      r: rgb.r / 255.0,
+      g: rgb.g / 255.0,
+      b: rgb.b / 255.0,
+      alpha: rgb.alpha,
+    }
+  }
+}
+
+impl From<RGBA> for RGB {
+  fn from(rgb: RGBA) -> Self {
+    RGB::from(&rgb)
+  }
+}
+
+impl From<&RGBA> for RGB {
+  fn from(rgb: &RGBA) -> Self {
+    RGB {
+      r: rgb.red as f32,
+      g: rgb.green as f32,
+      b: rgb.blue as f32,
+      alpha: rgb.alpha_f32(),
+    }
+  }
+}
+
+impl From<RGB> for RGBA {
+  fn from(rgb: RGB) -> Self {
+    let rgb = rgb.resolve();
+    RGBA::new(
+      clamp_floor_256_f32(rgb.r),
+      clamp_floor_256_f32(rgb.g),
+      clamp_floor_256_f32(rgb.b),
+      rgb.alpha,
+    )
+  }
+}
+
+// Once Rust specialization is stable, this could be simplified.
+via!(LAB -> XYZd50 -> XYZd65);
+via!(ProPhoto -> XYZd50 -> XYZd65);
+via!(OKLCH -> OKLAB -> XYZd65);
+
+via!(LAB -> XYZd65 -> OKLAB);
+via!(LAB -> XYZd65 -> OKLCH);
+via!(LAB -> XYZd65 -> SRGB);
+via!(LAB -> XYZd65 -> SRGBLinear);
+via!(LAB -> XYZd65 -> P3);
+via!(LAB -> XYZd65 -> A98);
+via!(LAB -> XYZd65 -> ProPhoto);
+via!(LAB -> XYZd65 -> Rec2020);
+via!(LAB -> XYZd65 -> HSL);
+via!(LAB -> XYZd65 -> HWB);
+
+via!(LCH -> LAB -> XYZd65);
+via!(LCH -> XYZd65 -> OKLAB);
+via!(LCH -> XYZd65 -> OKLCH);
+via!(LCH -> XYZd65 -> SRGB);
+via!(LCH -> XYZd65 -> SRGBLinear);
+via!(LCH -> XYZd65 -> P3);
+via!(LCH -> XYZd65 -> A98);
+via!(LCH -> XYZd65 -> ProPhoto);
+via!(LCH -> XYZd65 -> Rec2020);
+via!(LCH -> XYZd65 -> XYZd50);
+via!(LCH -> XYZd65 -> HSL);
+via!(LCH -> XYZd65 -> HWB);
+
+via!(SRGB -> SRGBLinear -> XYZd65);
+via!(SRGB -> XYZd65 -> OKLAB);
+via!(SRGB -> XYZd65 -> OKLCH);
+via!(SRGB -> XYZd65 -> P3);
+via!(SRGB -> XYZd65 -> A98);
+via!(SRGB -> XYZd65 -> ProPhoto);
+via!(SRGB -> XYZd65 -> Rec2020);
+via!(SRGB -> XYZd65 -> XYZd50);
+
+via!(P3 -> XYZd65 -> SRGBLinear);
+via!(P3 -> XYZd65 -> OKLAB);
+via!(P3 -> XYZd65 -> OKLCH);
+via!(P3 -> XYZd65 -> A98);
+via!(P3 -> XYZd65 -> ProPhoto);
+via!(P3 -> XYZd65 -> Rec2020);
+via!(P3 -> XYZd65 -> XYZd50);
+via!(P3 -> XYZd65 -> HSL);
+via!(P3 -> XYZd65 -> HWB);
+
+via!(SRGBLinear -> XYZd65 -> OKLAB);
+via!(SRGBLinear -> XYZd65 -> OKLCH);
+via!(SRGBLinear -> XYZd65 -> A98);
+via!(SRGBLinear -> XYZd65 -> ProPhoto);
+via!(SRGBLinear -> XYZd65 -> Rec2020);
+via!(SRGBLinear -> XYZd65 -> XYZd50);
+via!(SRGBLinear -> XYZd65 -> HSL);
+via!(SRGBLinear -> XYZd65 -> HWB);
+
+via!(A98 -> XYZd65 -> OKLAB);
+via!(A98 -> XYZd65 -> OKLCH);
+via!(A98 -> XYZd65 -> ProPhoto);
+via!(A98 -> XYZd65 -> Rec2020);
+via!(A98 -> XYZd65 -> XYZd50);
+via!(A98 -> XYZd65 -> HSL);
+via!(A98 -> XYZd65 -> HWB);
+
+via!(ProPhoto -> XYZd65 -> OKLAB);
+via!(ProPhoto -> XYZd65 -> OKLCH);
+via!(ProPhoto -> XYZd65 -> Rec2020);
+via!(ProPhoto -> XYZd65 -> HSL);
+via!(ProPhoto -> XYZd65 -> HWB);
+
+via!(XYZd50 -> XYZd65 -> OKLAB);
+via!(XYZd50 -> XYZd65 -> OKLCH);
+via!(XYZd50 -> XYZd65 -> Rec2020);
+via!(XYZd50 -> XYZd65 -> HSL);
+via!(XYZd50 -> XYZd65 -> HWB);
+
+via!(Rec2020 -> XYZd65 -> OKLAB);
+via!(Rec2020 -> XYZd65 -> OKLCH);
+via!(Rec2020 -> XYZd65 -> HSL);
+via!(Rec2020 -> XYZd65 -> HWB);
+
+via!(HSL -> XYZd65 -> OKLAB);
+via!(HSL -> XYZd65 -> OKLCH);
+via!(HSL -> SRGB -> XYZd65);
+via!(HSL -> SRGB -> HWB);
+
+via!(HWB -> SRGB -> XYZd65);
+via!(HWB -> XYZd65 -> OKLAB);
+via!(HWB -> XYZd65 -> OKLCH);
+
+via!(RGB -> SRGB -> LAB);
+via!(RGB -> SRGB -> LCH);
+via!(RGB -> SRGB -> OKLAB);
+via!(RGB -> SRGB -> OKLCH);
+via!(RGB -> SRGB -> P3);
+via!(RGB -> SRGB -> SRGBLinear);
+via!(RGB -> SRGB -> A98);
+via!(RGB -> SRGB -> ProPhoto);
+via!(RGB -> SRGB -> XYZd50);
+via!(RGB -> SRGB -> XYZd65);
+via!(RGB -> SRGB -> Rec2020);
+via!(RGB -> SRGB -> HSL);
+via!(RGB -> SRGB -> HWB);
+
+// RGBA is an 8-bit version. Convert to SRGB, which is a
+// more accurate floating point representation for all operations.
+via!(RGBA -> SRGB -> LAB);
+via!(RGBA -> SRGB -> LCH);
+via!(RGBA -> SRGB -> OKLAB);
+via!(RGBA -> SRGB -> OKLCH);
+via!(RGBA -> SRGB -> P3);
+via!(RGBA -> SRGB -> SRGBLinear);
+via!(RGBA -> SRGB -> A98);
+via!(RGBA -> SRGB -> ProPhoto);
+via!(RGBA -> SRGB -> XYZd50);
+via!(RGBA -> SRGB -> XYZd65);
+via!(RGBA -> SRGB -> Rec2020);
+via!(RGBA -> SRGB -> HSL);
+via!(RGBA -> SRGB -> HWB);
+
+macro_rules! color_space {
+  ($space: ty) => {
+    impl From<LABColor> for $space {
+      fn from(color: LABColor) -> $space {
+        use LABColor::*;
+
+        match color {
+          LAB(v) => v.into(),
+          LCH(v) => v.into(),
+          OKLAB(v) => v.into(),
+          OKLCH(v) => v.into(),
+        }
+      }
+    }
+
+    impl From<PredefinedColor> for $space {
+      fn from(color: PredefinedColor) -> $space {
+        use PredefinedColor::*;
+
+        match color {
+          SRGB(v) => v.into(),
+          SRGBLinear(v) => v.into(),
+          DisplayP3(v) => v.into(),
+          A98(v) => v.into(),
+          ProPhoto(v) => v.into(),
+          Rec2020(v) => v.into(),
+          XYZd50(v) => v.into(),
+          XYZd65(v) => v.into(),
+        }
+      }
+    }
+
+    impl From<FloatColor> for $space {
+      fn from(color: FloatColor) -> $space {
+        use FloatColor::*;
+
+        match color {
+          RGB(v) => v.into(),
+          HSL(v) => v.into(),
+          HWB(v) => v.into(),
+        }
+      }
+    }
+
+    impl TryFrom<&CssColor> for $space {
+      type Error = ();
+      fn try_from(color: &CssColor) -> Result<$space, ()> {
+        Ok(match color {
+          CssColor::RGBA(rgba) => (*rgba).into(),
+          CssColor::LAB(lab) => (**lab).into(),
+          CssColor::Predefined(predefined) => (**predefined).into(),
+          CssColor::Float(float) => (**float).into(),
+          CssColor::CurrentColor => return Err(()),
+          CssColor::LightDark(..) => return Err(()),
+          CssColor::System(..) => return Err(()),
+        })
+      }
+    }
+
+    impl TryFrom<CssColor> for $space {
+      type Error = ();
+      fn try_from(color: CssColor) -> Result<$space, ()> {
+        Ok(match color {
+          CssColor::RGBA(rgba) => rgba.into(),
+          CssColor::LAB(lab) => (*lab).into(),
+          CssColor::Predefined(predefined) => (*predefined).into(),
+          CssColor::Float(float) => (*float).into(),
+          CssColor::CurrentColor => return Err(()),
+          CssColor::LightDark(..) => return Err(()),
+          CssColor::System(..) => return Err(()),
+        })
+      }
+    }
+  };
+}
+
+color_space!(LAB);
+color_space!(LCH);
+color_space!(OKLAB);
+color_space!(OKLCH);
+color_space!(SRGB);
+color_space!(SRGBLinear);
+color_space!(XYZd50);
+color_space!(XYZd65);
+color_space!(P3);
+color_space!(A98);
+color_space!(ProPhoto);
+color_space!(Rec2020);
+color_space!(HSL);
+color_space!(HWB);
+color_space!(RGB);
+color_space!(RGBA);
+
+macro_rules! predefined {
+  ($key: ident, $t: ty) => {
+    impl From<$t> for PredefinedColor {
+      fn from(color: $t) -> PredefinedColor {
+        PredefinedColor::$key(color)
+      }
+    }
+
+    impl From<$t> for CssColor {
+      fn from(color: $t) -> CssColor {
+        CssColor::Predefined(Box::new(PredefinedColor::$key(color)))
+      }
+    }
+  };
+}
+
+predefined!(SRGBLinear, SRGBLinear);
+predefined!(XYZd50, XYZd50);
+predefined!(XYZd65, XYZd65);
+predefined!(DisplayP3, P3);
+predefined!(A98, A98);
+predefined!(ProPhoto, ProPhoto);
+predefined!(Rec2020, Rec2020);
+
+macro_rules! lab {
+  ($key: ident, $t: ty) => {
+    impl From<$t> for LABColor {
+      fn from(color: $t) -> LABColor {
+        LABColor::$key(color)
+      }
+    }
+
+    impl From<$t> for CssColor {
+      fn from(color: $t) -> CssColor {
+        CssColor::LAB(Box::new(LABColor::$key(color)))
+      }
+    }
+  };
+}
+
+lab!(LAB, LAB);
+lab!(LCH, LCH);
+lab!(OKLAB, OKLAB);
+lab!(OKLCH, OKLCH);
+
+macro_rules! rgb {
+  ($t: ty) => {
+    impl From<$t> for CssColor {
+      fn from(color: $t) -> CssColor {
+        // TODO: should we serialize as color(srgb, ...)?
+        // would be more precise than 8-bit color.
+        CssColor::RGBA(color.into())
+      }
+    }
+  };
+}
+
+rgb!(SRGB);
+rgb!(HSL);
+rgb!(HWB);
+rgb!(RGB);
+
+impl From<RGBA> for CssColor {
+  fn from(color: RGBA) -> CssColor {
+    CssColor::RGBA(color)
+  }
+}
+
+/// A trait that colors implement to support [gamut mapping](https://www.w3.org/TR/css-color-4/#gamut-mapping).
+pub trait ColorGamut {
+  /// Returns whether the color is within the gamut of the color space.
+  fn in_gamut(&self) -> bool;
+  /// Clips the color so that it is within the gamut of the color space.
+  fn clip(&self) -> Self;
+}
+
+macro_rules! bounded_color_gamut {
+  ($t: ty, $a: ident, $b: ident, $c: ident) => {
+    impl ColorGamut for $t {
+      #[inline]
+      fn in_gamut(&self) -> bool {
+        self.$a >= 0.0 && self.$a <= 1.0 && self.$b >= 0.0 && self.$b <= 1.0 && self.$c >= 0.0 && self.$c <= 1.0
+      }
+
+      #[inline]
+      fn clip(&self) -> Self {
+        Self {
+          $a: self.$a.clamp(0.0, 1.0),
+          $b: self.$b.clamp(0.0, 1.0),
+          $c: self.$c.clamp(0.0, 1.0),
+          alpha: self.alpha.clamp(0.0, 1.0),
+        }
+      }
+    }
+  };
+}
+
+macro_rules! unbounded_color_gamut {
+  ($t: ty, $a: ident, $b: ident, $c: ident) => {
+    impl ColorGamut for $t {
+      #[inline]
+      fn in_gamut(&self) -> bool {
+        true
+      }
+
+      #[inline]
+      fn clip(&self) -> Self {
+        *self
+      }
+    }
+  };
+}
+
+macro_rules! hsl_hwb_color_gamut {
+  ($t: ty, $a: ident, $b: ident) => {
+    impl ColorGamut for $t {
+      #[inline]
+      fn in_gamut(&self) -> bool {
+        self.$a >= 0.0 && self.$a <= 100.0 && self.$b >= 0.0 && self.$b <= 100.0
+      }
+
+      #[inline]
+      fn clip(&self) -> Self {
+        Self {
+          h: self.h % 360.0,
+          $a: self.$a.clamp(0.0, 100.0),
+          $b: self.$b.clamp(0.0, 100.0),
+          alpha: self.alpha.clamp(0.0, 1.0),
+        }
+      }
+    }
+  };
+}
+
+bounded_color_gamut!(SRGB, r, g, b);
+bounded_color_gamut!(SRGBLinear, r, g, b);
+bounded_color_gamut!(P3, r, g, b);
+bounded_color_gamut!(A98, r, g, b);
+bounded_color_gamut!(ProPhoto, r, g, b);
+bounded_color_gamut!(Rec2020, r, g, b);
+unbounded_color_gamut!(LAB, l, a, b);
+unbounded_color_gamut!(OKLAB, l, a, b);
+unbounded_color_gamut!(XYZd50, x, y, z);
+unbounded_color_gamut!(XYZd65, x, y, z);
+unbounded_color_gamut!(LCH, l, c, h);
+unbounded_color_gamut!(OKLCH, l, c, h);
+hsl_hwb_color_gamut!(HSL, s, l);
+hsl_hwb_color_gamut!(HWB, w, b);
+
+impl ColorGamut for RGB {
+  #[inline]
+  fn in_gamut(&self) -> bool {
+    self.r >= 0.0 && self.r <= 255.0 && self.g >= 0.0 && self.g <= 255.0 && self.b >= 0.0 && self.b <= 255.0
+  }
+
+  #[inline]
+  fn clip(&self) -> Self {
+    Self {
+      r: self.r.clamp(0.0, 255.0),
+      g: self.g.clamp(0.0, 255.0),
+      b: self.b.clamp(0.0, 255.0),
+      alpha: self.alpha.clamp(0.0, 1.0),
+    }
+  }
+}
+
+fn delta_eok<T: Into<OKLAB>>(a: T, b: OKLCH) -> f32 {
+  // https://www.w3.org/TR/css-color-4/#color-difference-OK
+  let a: OKLAB = a.into();
+  let b: OKLAB = b.into();
+  let delta_l = a.l - b.l;
+  let delta_a = a.a - b.a;
+  let delta_b = a.b - b.b;
+
+  (delta_l.powi(2) + delta_a.powi(2) + delta_b.powi(2)).sqrt()
+}
+
+fn map_gamut<T>(color: T) -> T
+where
+  T: Into<OKLCH> + ColorGamut + Into<OKLAB> + From<OKLCH> + Copy,
+{
+  const JND: f32 = 0.02;
+  const EPSILON: f32 = 0.00001;
+
+  // https://www.w3.org/TR/css-color-4/#binsearch
+  let mut current: OKLCH = color.into();
+
+  // If lightness is >= 100%, return pure white.
+  if (current.l - 1.0).abs() < EPSILON || current.l > 1.0 {
+    return OKLCH {
+      l: 1.0,
+      c: 0.0,
+      h: 0.0,
+      alpha: current.alpha,
+    }
+    .into();
+  }
+
+  // If lightness <= 0%, return pure black.
+  if current.l < EPSILON {
+    return OKLCH {
+      l: 0.0,
+      c: 0.0,
+      h: 0.0,
+      alpha: current.alpha,
+    }
+    .into();
+  }
+
+  let mut min = 0.0;
+  let mut max = current.c;
+
+  while (max - min) > EPSILON {
+    let chroma = (min + max) / 2.0;
+    current.c = chroma;
+
+    let converted = T::from(current);
+    if converted.in_gamut() {
+      min = chroma;
+      continue;
+    }
+
+    let clipped = converted.clip();
+    let delta_e = delta_eok(clipped, current);
+    if delta_e < JND {
+      return clipped;
+    }
+
+    max = chroma;
+  }
+
+  current.into()
+}
+
+fn parse_color_mix<'i, 't>(input: &mut Parser<'i, 't>) -> Result<CssColor, ParseError<'i, ParserError<'i>>> {
+  input.expect_ident_matching("in")?;
+  let method = ColorSpaceName::parse(input)?;
+
+  let hue_method = if matches!(
+    method,
+    ColorSpaceName::Hsl | ColorSpaceName::Hwb | ColorSpaceName::LCH | ColorSpaceName::OKLCH
+  ) {
+    let hue_method = input.try_parse(HueInterpolationMethod::parse);
+    if hue_method.is_ok() {
+      input.expect_ident_matching("hue")?;
+    }
+    hue_method
+  } else {
+    Ok(HueInterpolationMethod::Shorter)
+  };
+
+  let hue_method = hue_method.unwrap_or(HueInterpolationMethod::Shorter);
+  input.expect_comma()?;
+
+  let first_percent = input.try_parse(|input| input.expect_percentage());
+  let first_color = CssColor::parse(input)?;
+  let first_percent = first_percent
+    .or_else(|_| input.try_parse(|input| input.expect_percentage()))
+    .ok();
+  input.expect_comma()?;
+
+  let second_percent = input.try_parse(|input| input.expect_percentage());
+  let second_color = CssColor::parse(input)?;
+  let second_percent = second_percent
+    .or_else(|_| input.try_parse(|input| input.expect_percentage()))
+    .ok();
+
+  // https://drafts.csswg.org/css-color-5/#color-mix-percent-norm
+  let (p1, p2) = if first_percent.is_none() && second_percent.is_none() {
+    (0.5, 0.5)
+  } else {
+    let p2 = second_percent.unwrap_or_else(|| 1.0 - first_percent.unwrap());
+    let p1 = first_percent.unwrap_or_else(|| 1.0 - second_percent.unwrap());
+    (p1, p2)
+  };
+
+  if (p1 + p2) == 0.0 {
+    return Err(input.new_custom_error(ParserError::InvalidValue));
+  }
+
+  match method {
+    ColorSpaceName::SRGB => first_color.interpolate::<SRGB>(p1, &second_color, p2, hue_method),
+    ColorSpaceName::SRGBLinear => first_color.interpolate::<SRGBLinear>(p1, &second_color, p2, hue_method),
+    ColorSpaceName::Hsl => first_color.interpolate::<HSL>(p1, &second_color, p2, hue_method),
+    ColorSpaceName::Hwb => first_color.interpolate::<HWB>(p1, &second_color, p2, hue_method),
+    ColorSpaceName::LAB => first_color.interpolate::<LAB>(p1, &second_color, p2, hue_method),
+    ColorSpaceName::LCH => first_color.interpolate::<LCH>(p1, &second_color, p2, hue_method),
+    ColorSpaceName::OKLAB => first_color.interpolate::<OKLAB>(p1, &second_color, p2, hue_method),
+    ColorSpaceName::OKLCH => first_color.interpolate::<OKLCH>(p1, &second_color, p2, hue_method),
+    ColorSpaceName::XYZ | ColorSpaceName::XYZd65 => {
+      first_color.interpolate::<XYZd65>(p1, &second_color, p2, hue_method)
+    }
+    ColorSpaceName::XYZd50 => first_color.interpolate::<XYZd50>(p1, &second_color, p2, hue_method),
+  }
+  .map_err(|_| input.new_custom_error(ParserError::InvalidValue))
+}
+
+impl CssColor {
+  fn get_type_id(&self) -> TypeId {
+    match self {
+      CssColor::RGBA(..) => TypeId::of::<SRGB>(),
+      CssColor::LAB(lab) => match &**lab {
+        LABColor::LAB(..) => TypeId::of::<LAB>(),
+        LABColor::LCH(..) => TypeId::of::<LCH>(),
+        LABColor::OKLAB(..) => TypeId::of::<OKLAB>(),
+        LABColor::OKLCH(..) => TypeId::of::<OKLCH>(),
+      },
+      CssColor::Predefined(predefined) => match &**predefined {
+        PredefinedColor::SRGB(..) => TypeId::of::<SRGB>(),
+        PredefinedColor::SRGBLinear(..) => TypeId::of::<SRGBLinear>(),
+        PredefinedColor::DisplayP3(..) => TypeId::of::<P3>(),
+        PredefinedColor::A98(..) => TypeId::of::<A98>(),
+        PredefinedColor::ProPhoto(..) => TypeId::of::<ProPhoto>(),
+        PredefinedColor::Rec2020(..) => TypeId::of::<Rec2020>(),
+        PredefinedColor::XYZd50(..) => TypeId::of::<XYZd50>(),
+        PredefinedColor::XYZd65(..) => TypeId::of::<XYZd65>(),
+      },
+      CssColor::Float(float) => match &**float {
+        FloatColor::RGB(..) => TypeId::of::<SRGB>(),
+        FloatColor::HSL(..) => TypeId::of::<HSL>(),
+        FloatColor::HWB(..) => TypeId::of::<HWB>(),
+      },
+      _ => unreachable!(),
+    }
+  }
+
+  fn to_light_dark(&self) -> CssColor {
+    match self {
+      CssColor::LightDark(..) => self.clone(),
+      _ => CssColor::LightDark(Box::new(self.clone()), Box::new(self.clone())),
+    }
+  }
+
+  /// Mixes this color with another color, including the specified amount of each.
+  /// Implemented according to the [`color-mix()`](https://www.w3.org/TR/css-color-5/#color-mix) function.
+  pub fn interpolate<T>(
+    &self,
+    mut p1: f32,
+    other: &CssColor,
+    mut p2: f32,
+    method: HueInterpolationMethod,
+  ) -> Result<CssColor, ()>
+  where
+    for<'a> T: 'static
+      + TryFrom<&'a CssColor>
+      + Interpolate
+      + Into<CssColor>
+      + Into<OKLCH>
+      + ColorGamut
+      + Into<OKLAB>
+      + From<OKLCH>
+      + Copy,
+  {
+    if matches!(self, CssColor::CurrentColor | CssColor::System(..))
+      || matches!(other, CssColor::CurrentColor | CssColor::System(..))
+    {
+      return Err(());
+    }
+
+    if matches!(self, CssColor::LightDark(..)) || matches!(other, CssColor::LightDark(..)) {
+      if let (CssColor::LightDark(al, ad), CssColor::LightDark(bl, bd)) =
+        (self.to_light_dark(), other.to_light_dark())
+      {
+        return Ok(CssColor::LightDark(
+          Box::new(al.interpolate::<T>(p1, &bl, p2, method)?),
+          Box::new(ad.interpolate::<T>(p1, &bd, p2, method)?),
+        ));
+      }
+    }
+
+    let type_id = TypeId::of::<T>();
+    let converted_first = self.get_type_id() != type_id;
+    let converted_second = other.get_type_id() != type_id;
+
+    // https://drafts.csswg.org/css-color-5/#color-mix-result
+    let mut first_color = T::try_from(self).map_err(|_| ())?;
+    let mut second_color = T::try_from(other).map_err(|_| ())?;
+
+    if converted_first && !first_color.in_gamut() {
+      first_color = map_gamut(first_color);
+    }
+
+    if converted_second && !second_color.in_gamut() {
+      second_color = map_gamut(second_color);
+    }
+
+    // https://www.w3.org/TR/css-color-4/#powerless
+    if converted_first {
+      first_color.adjust_powerless_components();
+    }
+
+    if converted_second {
+      second_color.adjust_powerless_components();
+    }
+
+    // https://drafts.csswg.org/css-color-4/#interpolation-missing
+    first_color.fill_missing_components(&second_color);
+    second_color.fill_missing_components(&first_color);
+
+    // https://www.w3.org/TR/css-color-4/#hue-interpolation
+    first_color.adjust_hue(&mut second_color, method);
+
+    // https://www.w3.org/TR/css-color-4/#interpolation-alpha
+    first_color.premultiply();
+    second_color.premultiply();
+
+    // https://drafts.csswg.org/css-color-5/#color-mix-percent-norm
+    let mut alpha_multiplier = p1 + p2;
+    if alpha_multiplier != 1.0 {
+      p1 = p1 / alpha_multiplier;
+      p2 = p2 / alpha_multiplier;
+      if alpha_multiplier > 1.0 {
+        alpha_multiplier = 1.0;
+      }
+    }
+
+    let mut result_color = first_color.interpolate(p1, &second_color, p2);
+    result_color.unpremultiply(alpha_multiplier);
+
+    Ok(result_color.into())
+  }
+}
+
+/// A trait that colors implement to support interpolation.
+pub trait Interpolate {
+  /// Adjusts components that are powerless to be NaN.
+  fn adjust_powerless_components(&mut self) {}
+  /// Fills missing components (represented as NaN) to match the other color to interpolate with.
+  fn fill_missing_components(&mut self, other: &Self);
+  /// Adjusts the color hue according to the given hue interpolation method.
+  fn adjust_hue(&mut self, _: &mut Self, _: HueInterpolationMethod) {}
+  /// Premultiplies the color by its alpha value.
+  fn premultiply(&mut self);
+  /// Un-premultiplies the color by the given alpha multiplier.
+  fn unpremultiply(&mut self, alpha_multiplier: f32);
+  /// Interpolates the color with another using the given amounts of each.
+  fn interpolate(&self, p1: f32, other: &Self, p2: f32) -> Self;
+}
+
+macro_rules! interpolate {
+  ($a: ident, $b: ident, $c: ident) => {
+    fn fill_missing_components(&mut self, other: &Self) {
+      if self.$a.is_nan() {
+        self.$a = other.$a;
+      }
+
+      if self.$b.is_nan() {
+        self.$b = other.$b;
+      }
+
+      if self.$c.is_nan() {
+        self.$c = other.$c;
+      }
+
+      if self.alpha.is_nan() {
+        self.alpha = other.alpha;
+      }
+    }
+
+    fn interpolate(&self, p1: f32, other: &Self, p2: f32) -> Self {
+      Self {
+        $a: self.$a * p1 + other.$a * p2,
+        $b: self.$b * p1 + other.$b * p2,
+        $c: self.$c * p1 + other.$c * p2,
+        alpha: self.alpha * p1 + other.alpha * p2,
+      }
+    }
+  };
+}
+
+macro_rules! rectangular_premultiply {
+  ($a: ident, $b: ident, $c: ident) => {
+    fn premultiply(&mut self) {
+      if !self.alpha.is_nan() {
+        self.$a *= self.alpha;
+        self.$b *= self.alpha;
+        self.$c *= self.alpha;
+      }
+    }
+
+    fn unpremultiply(&mut self, alpha_multiplier: f32) {
+      if !self.alpha.is_nan() && self.alpha != 0.0 {
+        self.$a /= self.alpha;
+        self.$b /= self.alpha;
+        self.$c /= self.alpha;
+        self.alpha *= alpha_multiplier;
+      }
+    }
+  };
+}
+
+macro_rules! polar_premultiply {
+  ($a: ident, $b: ident) => {
+    fn premultiply(&mut self) {
+      if !self.alpha.is_nan() {
+        self.$a *= self.alpha;
+        self.$b *= self.alpha;
+      }
+    }
+
+    fn unpremultiply(&mut self, alpha_multiplier: f32) {
+      self.h %= 360.0;
+      if !self.alpha.is_nan() {
+        self.$a /= self.alpha;
+        self.$b /= self.alpha;
+        self.alpha *= alpha_multiplier;
+      }
+    }
+  };
+}
+
+macro_rules! adjust_powerless_lab {
+  () => {
+    fn adjust_powerless_components(&mut self) {
+      // If the lightness of a LAB color is 0%, both the a and b components are powerless.
+      if self.l.abs() < f32::EPSILON {
+        self.a = f32::NAN;
+        self.b = f32::NAN;
+      }
+    }
+  };
+}
+
+macro_rules! adjust_powerless_lch {
+  () => {
+    fn adjust_powerless_components(&mut self) {
+      // If the chroma of an LCH color is 0%, the hue component is powerless.
+      // If the lightness of an LCH color is 0%, both the hue and chroma components are powerless.
+      if self.c.abs() < f32::EPSILON {
+        self.h = f32::NAN;
+      }
+
+      if self.l.abs() < f32::EPSILON {
+        self.c = f32::NAN;
+        self.h = f32::NAN;
+      }
+    }
+
+    fn adjust_hue(&mut self, other: &mut Self, method: HueInterpolationMethod) {
+      method.interpolate(&mut self.h, &mut other.h);
+    }
+  };
+}
+
+impl Interpolate for SRGB {
+  rectangular_premultiply!(r, g, b);
+  interpolate!(r, g, b);
+}
+
+impl Interpolate for SRGBLinear {
+  rectangular_premultiply!(r, g, b);
+  interpolate!(r, g, b);
+}
+
+impl Interpolate for XYZd50 {
+  rectangular_premultiply!(x, y, z);
+  interpolate!(x, y, z);
+}
+
+impl Interpolate for XYZd65 {
+  rectangular_premultiply!(x, y, z);
+  interpolate!(x, y, z);
+}
+
+impl Interpolate for LAB {
+  adjust_powerless_lab!();
+  rectangular_premultiply!(l, a, b);
+  interpolate!(l, a, b);
+}
+
+impl Interpolate for OKLAB {
+  adjust_powerless_lab!();
+  rectangular_premultiply!(l, a, b);
+  interpolate!(l, a, b);
+}
+
+impl Interpolate for LCH {
+  adjust_powerless_lch!();
+  polar_premultiply!(l, c);
+  interpolate!(l, c, h);
+}
+
+impl Interpolate for OKLCH {
+  adjust_powerless_lch!();
+  polar_premultiply!(l, c);
+  interpolate!(l, c, h);
+}
+
+impl Interpolate for HSL {
+  polar_premultiply!(s, l);
+
+  fn adjust_powerless_components(&mut self) {
+    // If the saturation of an HSL color is 0%, then the hue component is powerless.
+    // If the lightness of an HSL color is 0% or 100%, both the saturation and hue components are powerless.
+    if self.s.abs() < f32::EPSILON {
+      self.h = f32::NAN;
+    }
+
+    if self.l.abs() < f32::EPSILON || (self.l - 100.0).abs() < f32::EPSILON {
+      self.h = f32::NAN;
+      self.s = f32::NAN;
+    }
+  }
+
+  fn adjust_hue(&mut self, other: &mut Self, method: HueInterpolationMethod) {
+    method.interpolate(&mut self.h, &mut other.h);
+  }
+
+  interpolate!(h, s, l);
+}
+
+impl Interpolate for HWB {
+  polar_premultiply!(w, b);
+
+  fn adjust_powerless_components(&mut self) {
+    // If white+black is equal to 100% (after normalization), it defines an achromatic color,
+    // i.e. some shade of gray, without any hint of the chosen hue. In this case, the hue component is powerless.
+    if (self.w + self.b - 100.0).abs() < f32::EPSILON {
+      self.h = f32::NAN;
+    }
+  }
+
+  fn adjust_hue(&mut self, other: &mut Self, method: HueInterpolationMethod) {
+    method.interpolate(&mut self.h, &mut other.h);
+  }
+
+  interpolate!(h, w, b);
+}
+
+impl HueInterpolationMethod {
+  fn interpolate(&self, a: &mut f32, b: &mut f32) {
+    // https://drafts.csswg.org/css-color/#hue-interpolation
+    if *self != HueInterpolationMethod::Specified {
+      *a = ((*a % 360.0) + 360.0) % 360.0;
+      *b = ((*b % 360.0) + 360.0) % 360.0;
+    }
+
+    match self {
+      HueInterpolationMethod::Shorter => {
+        // https://www.w3.org/TR/css-color-4/#hue-shorter
+        let delta = *b - *a;
+        if delta > 180.0 {
+          *a += 360.0;
+        } else if delta < -180.0 {
+          *b += 360.0;
+        }
+      }
+      HueInterpolationMethod::Longer => {
+        // https://www.w3.org/TR/css-color-4/#hue-longer
+        let delta = *b - *a;
+        if 0.0 < delta && delta < 180.0 {
+          *a += 360.0;
+        } else if -180.0 < delta && delta < 0.0 {
+          *b += 360.0;
+        }
+      }
+      HueInterpolationMethod::Increasing => {
+        // https://www.w3.org/TR/css-color-4/#hue-increasing
+        if *b < *a {
+          *b += 360.0;
+        }
+      }
+      HueInterpolationMethod::Decreasing => {
+        // https://www.w3.org/TR/css-color-4/#hue-decreasing
+        if *a < *b {
+          *a += 360.0;
+        }
+      }
+      HueInterpolationMethod::Specified => {}
+    }
+  }
+}
+
+#[cfg(feature = "visitor")]
+#[cfg_attr(docsrs, doc(cfg(feature = "visitor")))]
+impl<'i, V: ?Sized + Visitor<'i, T>, T: Visit<'i, T, V>> Visit<'i, T, V> for RGBA {
+  const CHILD_TYPES: VisitTypes = VisitTypes::empty();
+  fn visit_children(&mut self, _: &mut V) -> Result<(), V::Error> {
+    Ok(())
+  }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Parse, ToCss)]
+#[css(case = lower)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(rename_all = "lowercase")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+/// A CSS [system color](https://drafts.csswg.org/css-color/#css-system-colors) keyword.
+pub enum SystemColor {
+  /// Background of accented user interface controls.
+  AccentColor,
+  /// Text of accented user interface controls.
+  AccentColorText,
+  /// Text in active links. For light backgrounds, traditionally red.
+  ActiveText,
+  /// The base border color for push buttons.
+  ButtonBorder,
+  /// The face background color for push buttons.
+  ButtonFace,
+  /// Text on push buttons.
+  ButtonText,
+  /// Background of application content or documents.
+  Canvas,
+  /// Text in application content or documents.
+  CanvasText,
+  /// Background of input fields.
+  Field,
+  /// Text in input fields.
+  FieldText,
+  /// Disabled text. (Often, but not necessarily, gray.)
+  GrayText,
+  /// Background of selected text, for example from ::selection.
+  Highlight,
+  /// Text of selected text.
+  HighlightText,
+  /// Text in non-active, non-visited links. For light backgrounds, traditionally blue.
+  LinkText,
+  /// Background of text that has been specially marked (such as by the HTML mark element).
+  Mark,
+  /// Text that has been specially marked (such as by the HTML mark element).
+  MarkText,
+  /// Background of selected items, for example a selected checkbox.
+  SelectedItem,
+  /// Text of selected items.
+  SelectedItemText,
+  /// Text in visited links. For light backgrounds, traditionally purple.
+  VisitedText,
+
+  // Deprecated colors: https://drafts.csswg.org/css-color/#deprecated-system-colors
+  /// Active window border. Same as ButtonBorder.
+  ActiveBorder,
+  /// Active window caption. Same as Canvas.
+  ActiveCaption,
+  /// Background color of multiple document interface. Same as Canvas.
+  AppWorkspace,
+  /// Desktop background. Same as Canvas.
+  Background,
+  /// The color of the border facing the light source for 3-D elements that appear 3-D due to one layer of surrounding border. Same as ButtonFace.
+  ButtonHighlight,
+  /// The color of the border away from the light source for 3-D elements that appear 3-D due to one layer of surrounding border. Same as ButtonFace.
+  ButtonShadow,
+  /// Text in caption, size box, and scrollbar arrow box. Same as CanvasText.
+  CaptionText,
+  /// Inactive window border. Same as ButtonBorder.
+  InactiveBorder,
+  /// Inactive window caption. Same as Canvas.
+  InactiveCaption,
+  /// Color of text in an inactive caption. Same as GrayText.
+  InactiveCaptionText,
+  /// Background color for tooltip controls. Same as Canvas.
+  InfoBackground,
+  /// Text color for tooltip controls. Same as CanvasText.
+  InfoText,
+  /// Menu background. Same as Canvas.
+  Menu,
+  /// Text in menus. Same as CanvasText.
+  MenuText,
+  /// Scroll bar gray area. Same as Canvas.
+  Scrollbar,
+  /// The color of the darker (generally outer) of the two borders away from the light source for 3-D elements that appear 3-D due to two concentric layers of surrounding border. Same as ButtonBorder.
+  ThreeDDarkShadow,
+  /// The face background color for 3-D elements that appear 3-D due to two concentric layers of surrounding border. Same as ButtonFace.
+  ThreeDFace,
+  /// The color of the lighter (generally outer) of the two borders facing the light source for 3-D elements that appear 3-D due to two concentric layers of surrounding border. Same as ButtonBorder.
+  ThreeDHighlight,
+  /// The color of the darker (generally inner) of the two borders facing the light source for 3-D elements that appear 3-D due to two concentric layers of surrounding border. Same as ButtonBorder.
+  ThreeDLightShadow,
+  /// The color of the lighter (generally inner) of the two borders away from the light source for 3-D elements that appear 3-D due to two concentric layers of surrounding border. Same as ButtonBorder.
+  ThreeDShadow,
+  /// Window background. Same as Canvas.
+  Window,
+  /// Window frame. Same as ButtonBorder.
+  WindowFrame,
+  /// Text in windows. Same as CanvasText.
+  WindowText,
+}
+
+impl IsCompatible for SystemColor {
+  fn is_compatible(&self, browsers: Browsers) -> bool {
+    use SystemColor::*;
+    match self {
+      AccentColor | AccentColorText => Feature::AccentSystemColor.is_compatible(browsers),
+      _ => true,
+    }
+  }
+}
diff --git a/src/values/easing.rs b/src/values/easing.rs
new file mode 100644
index 0000000..4db5c06
--- /dev/null
+++ b/src/values/easing.rs
@@ -0,0 +1,235 @@
+//! CSS easing functions.
+
+use crate::error::{ParserError, PrinterError};
+use crate::printer::Printer;
+use crate::traits::{Parse, ToCss};
+use crate::values::number::{CSSInteger, CSSNumber};
+#[cfg(feature = "visitor")]
+use crate::visitor::Visit;
+use cssparser::*;
+use std::fmt::Write;
+
+/// A CSS [easing function](https://www.w3.org/TR/css-easing-1/#easing-functions).
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum EasingFunction {
+  /// A linear easing function.
+  Linear,
+  /// Equivalent to `cubic-bezier(0.25, 0.1, 0.25, 1)`.
+  Ease,
+  /// Equivalent to `cubic-bezier(0.42, 0, 1, 1)`.
+  EaseIn,
+  /// Equivalent to `cubic-bezier(0, 0, 0.58, 1)`.
+  EaseOut,
+  /// Equivalent to `cubic-bezier(0.42, 0, 0.58, 1)`.
+  EaseInOut,
+  /// A custom cubic Bézier easing function.
+  CubicBezier {
+    /// The x-position of the first point in the curve.
+    x1: CSSNumber,
+    /// The y-position of the first point in the curve.
+    y1: CSSNumber,
+    /// The x-position of the second point in the curve.
+    x2: CSSNumber,
+    /// The y-position of the second point in the curve.
+    y2: CSSNumber,
+  },
+  /// A step easing function.
+  Steps {
+    /// The number of intervals in the function.
+    count: CSSInteger,
+    /// The step position.
+    #[cfg_attr(feature = "serde", serde(default))]
+    position: StepPosition,
+  },
+}
+
+impl EasingFunction {
+  /// Returns whether the easing function is equivalent to the `ease` keyword.
+  pub fn is_ease(&self) -> bool {
+    *self == EasingFunction::Ease
+      || *self
+        == EasingFunction::CubicBezier {
+          x1: 0.25,
+          y1: 0.1,
+          x2: 0.25,
+          y2: 1.0,
+        }
+  }
+}
+
+impl<'i> Parse<'i> for EasingFunction {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let location = input.current_source_location();
+    if let Ok(ident) = input.try_parse(|i| i.expect_ident_cloned()) {
+      let keyword = match_ignore_ascii_case! { &ident,
+        "linear" => EasingFunction::Linear,
+        "ease" => EasingFunction::Ease,
+        "ease-in" => EasingFunction::EaseIn,
+        "ease-out" => EasingFunction::EaseOut,
+        "ease-in-out" => EasingFunction::EaseInOut,
+        "step-start" => EasingFunction::Steps { count: 1, position: StepPosition::Start },
+        "step-end" => EasingFunction::Steps { count: 1, position: StepPosition::End },
+        _ => return Err(location.new_unexpected_token_error(Token::Ident(ident.clone())))
+      };
+      return Ok(keyword);
+    }
+
+    let function = input.expect_function()?.clone();
+    input.parse_nested_block(|input| {
+      match_ignore_ascii_case! { &function,
+        "cubic-bezier" => {
+          let x1 = CSSNumber::parse(input)?;
+          input.expect_comma()?;
+          let y1 = CSSNumber::parse(input)?;
+          input.expect_comma()?;
+          let x2 = CSSNumber::parse(input)?;
+          input.expect_comma()?;
+          let y2 = CSSNumber::parse(input)?;
+          Ok(EasingFunction::CubicBezier { x1, y1, x2, y2 })
+        },
+        "steps" => {
+          let count = CSSInteger::parse(input)?;
+          let position = input.try_parse(|input| {
+            input.expect_comma()?;
+            StepPosition::parse(input)
+          }).unwrap_or_default();
+          Ok(EasingFunction::Steps { count, position })
+        },
+        _ => return Err(location.new_unexpected_token_error(Token::Ident(function.clone())))
+      }
+    })
+  }
+}
+
+impl ToCss for EasingFunction {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    match self {
+      EasingFunction::Linear => dest.write_str("linear"),
+      EasingFunction::Ease => dest.write_str("ease"),
+      EasingFunction::EaseIn => dest.write_str("ease-in"),
+      EasingFunction::EaseOut => dest.write_str("ease-out"),
+      EasingFunction::EaseInOut => dest.write_str("ease-in-out"),
+      _ if self.is_ease() => dest.write_str("ease"),
+      x if *x
+        == EasingFunction::CubicBezier {
+          x1: 0.42,
+          y1: 0.0,
+          x2: 1.0,
+          y2: 1.0,
+        } =>
+      {
+        dest.write_str("ease-in")
+      }
+      x if *x
+        == EasingFunction::CubicBezier {
+          x1: 0.0,
+          y1: 0.0,
+          x2: 0.58,
+          y2: 1.0,
+        } =>
+      {
+        dest.write_str("ease-out")
+      }
+      x if *x
+        == EasingFunction::CubicBezier {
+          x1: 0.42,
+          y1: 0.0,
+          x2: 0.58,
+          y2: 1.0,
+        } =>
+      {
+        dest.write_str("ease-in-out")
+      }
+      EasingFunction::CubicBezier { x1, y1, x2, y2 } => {
+        dest.write_str("cubic-bezier(")?;
+        x1.to_css(dest)?;
+        dest.delim(',', false)?;
+        y1.to_css(dest)?;
+        dest.delim(',', false)?;
+        x2.to_css(dest)?;
+        dest.delim(',', false)?;
+        y2.to_css(dest)?;
+        dest.write_char(')')
+      }
+      EasingFunction::Steps {
+        count: 1,
+        position: StepPosition::Start,
+      } => dest.write_str("step-start"),
+      EasingFunction::Steps {
+        count: 1,
+        position: StepPosition::End,
+      } => dest.write_str("step-end"),
+      EasingFunction::Steps { count, position } => {
+        dest.write_str("steps(")?;
+        write!(dest, "{}", count)?;
+        dest.delim(',', false)?;
+        position.to_css(dest)?;
+        dest.write_char(')')
+      }
+    }
+  }
+}
+
+impl EasingFunction {
+  /// Returns whether the given string is a valid easing function name.
+  pub fn is_ident(s: &str) -> bool {
+    match s {
+      "linear" | "ease" | "ease-in" | "ease-out" | "ease-in-out" | "step-start" | "step-end" => true,
+      _ => false,
+    }
+  }
+}
+
+/// A [step position](https://www.w3.org/TR/css-easing-1/#step-position), used within the `steps()` function.
+#[derive(Debug, Clone, PartialEq, ToCss)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", content = "value", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub enum StepPosition {
+  /// The first rise occurs at input progress value of 0.
+  Start,
+  /// The last rise occurs at input progress value of 1.
+  End,
+  /// All rises occur within the range (0, 1).
+  JumpNone,
+  /// The first rise occurs at input progress value of 0 and the last rise occurs at input progress value of 1.
+  JumpBoth,
+}
+
+impl Default for StepPosition {
+  fn default() -> Self {
+    StepPosition::End
+  }
+}
+
+impl<'i> Parse<'i> for StepPosition {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let location = input.current_source_location();
+    let ident = input.expect_ident()?;
+    let keyword = match_ignore_ascii_case! { &ident,
+      "start" => StepPosition::Start,
+      "end" => StepPosition::End,
+      "jump-start" => StepPosition::Start,
+      "jump-end" => StepPosition::End,
+      "jump-none" => StepPosition::JumpNone,
+      "jump-both" => StepPosition::JumpBoth,
+      _ => return Err(location.new_unexpected_token_error(Token::Ident(ident.clone())))
+    };
+    Ok(keyword)
+  }
+}
diff --git a/src/values/gradient.rs b/src/values/gradient.rs
new file mode 100644
index 0000000..22d748e
--- /dev/null
+++ b/src/values/gradient.rs
@@ -0,0 +1,1524 @@
+//! CSS gradient values.
+
+use super::angle::{Angle, AnglePercentage};
+use super::color::{ColorFallbackKind, CssColor};
+use super::length::{Length, LengthPercentage};
+use super::number::CSSNumber;
+use super::percentage::{DimensionPercentage, NumberOrPercentage, Percentage};
+use super::position::{HorizontalPositionKeyword, VerticalPositionKeyword};
+use super::position::{Position, PositionComponent};
+use crate::compat;
+use crate::error::{ParserError, PrinterError};
+use crate::macros::enum_property;
+use crate::prefixes::Feature;
+use crate::printer::Printer;
+use crate::targets::{should_compile, Browsers, Targets};
+use crate::traits::{IsCompatible, Parse, ToCss, TrySign, Zero};
+use crate::vendor_prefix::VendorPrefix;
+#[cfg(feature = "visitor")]
+use crate::visitor::Visit;
+use cssparser::*;
+use std::f32::consts::PI;
+
+#[cfg(feature = "serde")]
+use crate::serialization::ValueWrapper;
+
+/// A CSS [`<gradient>`](https://www.w3.org/TR/css-images-3/#gradients) value.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum Gradient {
+  /// A `linear-gradient()`, and its vendor prefix.
+  Linear(LinearGradient),
+  /// A `repeating-linear-gradient()`, and its vendor prefix.
+  RepeatingLinear(LinearGradient),
+  /// A `radial-gradient()`, and its vendor prefix.
+  Radial(RadialGradient),
+  /// A `repeating-radial-gradient`, and its vendor prefix.
+  RepeatingRadial(RadialGradient),
+  /// A `conic-gradient()`.
+  Conic(ConicGradient),
+  /// A `repeating-conic-gradient()`.
+  RepeatingConic(ConicGradient),
+  /// A legacy `-webkit-gradient()`.
+  #[cfg_attr(feature = "serde", serde(rename = "webkit-gradient"))]
+  WebKitGradient(WebKitGradient),
+}
+
+impl Gradient {
+  /// Returns the vendor prefix of the gradient.
+  pub fn get_vendor_prefix(&self) -> VendorPrefix {
+    match self {
+      Gradient::Linear(LinearGradient { vendor_prefix, .. })
+      | Gradient::RepeatingLinear(LinearGradient { vendor_prefix, .. })
+      | Gradient::Radial(RadialGradient { vendor_prefix, .. })
+      | Gradient::RepeatingRadial(RadialGradient { vendor_prefix, .. }) => *vendor_prefix,
+      Gradient::WebKitGradient(_) => VendorPrefix::WebKit,
+      _ => VendorPrefix::None,
+    }
+  }
+
+  /// Returns the vendor prefixes needed for the given browser targets.
+  pub fn get_necessary_prefixes(&self, targets: Targets) -> VendorPrefix {
+    macro_rules! get_prefixes {
+      ($feature: ident, $prefix: expr) => {
+        targets.prefixes($prefix, Feature::$feature)
+      };
+    }
+
+    match self {
+      Gradient::Linear(linear) => get_prefixes!(LinearGradient, linear.vendor_prefix),
+      Gradient::RepeatingLinear(linear) => get_prefixes!(RepeatingLinearGradient, linear.vendor_prefix),
+      Gradient::Radial(radial) => get_prefixes!(RadialGradient, radial.vendor_prefix),
+      Gradient::RepeatingRadial(radial) => get_prefixes!(RepeatingRadialGradient, radial.vendor_prefix),
+      _ => VendorPrefix::None,
+    }
+  }
+
+  /// Returns a copy of the gradient with the given vendor prefix.
+  pub fn get_prefixed(&self, prefix: VendorPrefix) -> Gradient {
+    match self {
+      Gradient::Linear(linear) => {
+        let mut new_linear = linear.clone();
+        let needs_legacy_direction = linear.vendor_prefix == VendorPrefix::None && prefix != VendorPrefix::None;
+        if needs_legacy_direction {
+          new_linear.direction = convert_to_legacy_direction(&new_linear.direction);
+        }
+        new_linear.vendor_prefix = prefix;
+        Gradient::Linear(new_linear)
+      }
+      Gradient::RepeatingLinear(linear) => {
+        let mut new_linear = linear.clone();
+        let needs_legacy_direction = linear.vendor_prefix == VendorPrefix::None && prefix != VendorPrefix::None;
+        if needs_legacy_direction {
+          new_linear.direction = convert_to_legacy_direction(&new_linear.direction);
+        }
+        new_linear.vendor_prefix = prefix;
+        Gradient::RepeatingLinear(new_linear)
+      }
+      Gradient::Radial(radial) => Gradient::Radial(RadialGradient {
+        vendor_prefix: prefix,
+        ..radial.clone()
+      }),
+      Gradient::RepeatingRadial(radial) => Gradient::RepeatingRadial(RadialGradient {
+        vendor_prefix: prefix,
+        ..radial.clone()
+      }),
+      _ => self.clone(),
+    }
+  }
+
+  /// Attempts to convert the gradient to the legacy `-webkit-gradient()` syntax.
+  ///
+  /// Returns an error in case the conversion is not possible.
+  pub fn get_legacy_webkit(&self) -> Result<Gradient, ()> {
+    Ok(Gradient::WebKitGradient(WebKitGradient::from_standard(self)?))
+  }
+
+  /// Returns the color fallback types needed for the given browser targets.
+  pub fn get_necessary_fallbacks(&self, targets: Targets) -> ColorFallbackKind {
+    match self {
+      Gradient::Linear(LinearGradient { items, .. })
+      | Gradient::Radial(RadialGradient { items, .. })
+      | Gradient::RepeatingLinear(LinearGradient { items, .. })
+      | Gradient::RepeatingRadial(RadialGradient { items, .. }) => {
+        let mut fallbacks = ColorFallbackKind::empty();
+        for item in items {
+          fallbacks |= item.get_necessary_fallbacks(targets)
+        }
+        fallbacks
+      }
+      Gradient::Conic(ConicGradient { items, .. }) | Gradient::RepeatingConic(ConicGradient { items, .. }) => {
+        let mut fallbacks = ColorFallbackKind::empty();
+        for item in items {
+          fallbacks |= item.get_necessary_fallbacks(targets)
+        }
+        fallbacks
+      }
+      Gradient::WebKitGradient(..) => ColorFallbackKind::empty(),
+    }
+  }
+
+  /// Returns a fallback gradient for the given color fallback type.
+  pub fn get_fallback(&self, kind: ColorFallbackKind) -> Gradient {
+    match self {
+      Gradient::Linear(g) => Gradient::Linear(g.get_fallback(kind)),
+      Gradient::RepeatingLinear(g) => Gradient::RepeatingLinear(g.get_fallback(kind)),
+      Gradient::Radial(g) => Gradient::Radial(g.get_fallback(kind)),
+      Gradient::RepeatingRadial(g) => Gradient::RepeatingRadial(g.get_fallback(kind)),
+      Gradient::Conic(g) => Gradient::Conic(g.get_fallback(kind)),
+      Gradient::RepeatingConic(g) => Gradient::RepeatingConic(g.get_fallback(kind)),
+      Gradient::WebKitGradient(g) => Gradient::WebKitGradient(g.get_fallback(kind)),
+    }
+  }
+}
+
+impl<'i> Parse<'i> for Gradient {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let location = input.current_source_location();
+    let func = input.expect_function()?.clone();
+    input.parse_nested_block(|input| {
+      match_ignore_ascii_case! { &func,
+        "linear-gradient" => Ok(Gradient::Linear(LinearGradient::parse(input, VendorPrefix::None)?)),
+        "repeating-linear-gradient" => Ok(Gradient::RepeatingLinear(LinearGradient::parse(input, VendorPrefix::None)?)),
+        "radial-gradient" => Ok(Gradient::Radial(RadialGradient::parse(input, VendorPrefix::None)?)),
+        "repeating-radial-gradient" => Ok(Gradient::RepeatingRadial(RadialGradient::parse(input, VendorPrefix::None)?)),
+        "conic-gradient" => Ok(Gradient::Conic(ConicGradient::parse(input)?)),
+        "repeating-conic-gradient" => Ok(Gradient::RepeatingConic(ConicGradient::parse(input)?)),
+        "-webkit-linear-gradient" => Ok(Gradient::Linear(LinearGradient::parse(input, VendorPrefix::WebKit)?)),
+        "-webkit-repeating-linear-gradient" => Ok(Gradient::RepeatingLinear(LinearGradient::parse(input, VendorPrefix::WebKit)?)),
+        "-webkit-radial-gradient" => Ok(Gradient::Radial(RadialGradient::parse(input, VendorPrefix::WebKit)?)),
+        "-webkit-repeating-radial-gradient" => Ok(Gradient::RepeatingRadial(RadialGradient::parse(input, VendorPrefix::WebKit)?)),
+        "-moz-linear-gradient" => Ok(Gradient::Linear(LinearGradient::parse(input, VendorPrefix::Moz)?)),
+        "-moz-repeating-linear-gradient" => Ok(Gradient::RepeatingLinear(LinearGradient::parse(input, VendorPrefix::Moz)?)),
+        "-moz-radial-gradient" => Ok(Gradient::Radial(RadialGradient::parse(input, VendorPrefix::Moz)?)),
+        "-moz-repeating-radial-gradient" => Ok(Gradient::RepeatingRadial(RadialGradient::parse(input, VendorPrefix::Moz)?)),
+        "-o-linear-gradient" => Ok(Gradient::Linear(LinearGradient::parse(input, VendorPrefix::O)?)),
+        "-o-repeating-linear-gradient" => Ok(Gradient::RepeatingLinear(LinearGradient::parse(input, VendorPrefix::O)?)),
+        "-o-radial-gradient" => Ok(Gradient::Radial(RadialGradient::parse(input, VendorPrefix::O)?)),
+        "-o-repeating-radial-gradient" => Ok(Gradient::RepeatingRadial(RadialGradient::parse(input, VendorPrefix::O)?)),
+        "-webkit-gradient" => Ok(Gradient::WebKitGradient(WebKitGradient::parse(input)?)),
+        _ => Err(location.new_unexpected_token_error(cssparser::Token::Ident(func.clone())))
+      }
+    })
+  }
+}
+
+impl ToCss for Gradient {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    let (f, prefix) = match self {
+      Gradient::Linear(g) => ("linear-gradient(", Some(g.vendor_prefix)),
+      Gradient::RepeatingLinear(g) => ("repeating-linear-gradient(", Some(g.vendor_prefix)),
+      Gradient::Radial(g) => ("radial-gradient(", Some(g.vendor_prefix)),
+      Gradient::RepeatingRadial(g) => ("repeating-radial-gradient(", Some(g.vendor_prefix)),
+      Gradient::Conic(_) => ("conic-gradient(", None),
+      Gradient::RepeatingConic(_) => ("repeating-conic-gradient(", None),
+      Gradient::WebKitGradient(_) => ("-webkit-gradient(", None),
+    };
+
+    if let Some(prefix) = prefix {
+      prefix.to_css(dest)?;
+    }
+
+    dest.write_str(f)?;
+
+    match self {
+      Gradient::Linear(linear) | Gradient::RepeatingLinear(linear) => {
+        linear.to_css(dest, linear.vendor_prefix != VendorPrefix::None)?
+      }
+      Gradient::Radial(radial) | Gradient::RepeatingRadial(radial) => radial.to_css(dest)?,
+      Gradient::Conic(conic) | Gradient::RepeatingConic(conic) => conic.to_css(dest)?,
+      Gradient::WebKitGradient(g) => g.to_css(dest)?,
+    }
+
+    dest.write_char(')')
+  }
+}
+
+/// A CSS [`linear-gradient()`](https://www.w3.org/TR/css-images-3/#linear-gradients) or `repeating-linear-gradient()`.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(rename_all = "camelCase")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub struct LinearGradient {
+  /// The vendor prefixes for the gradient.
+  pub vendor_prefix: VendorPrefix,
+  /// The direction of the gradient.
+  pub direction: LineDirection,
+  /// The color stops and transition hints for the gradient.
+  pub items: Vec<GradientItem<LengthPercentage>>,
+}
+
+impl LinearGradient {
+  fn parse<'i, 't>(
+    input: &mut Parser<'i, 't>,
+    vendor_prefix: VendorPrefix,
+  ) -> Result<LinearGradient, ParseError<'i, ParserError<'i>>> {
+    let direction = if let Ok(direction) =
+      input.try_parse(|input| LineDirection::parse(input, vendor_prefix != VendorPrefix::None))
+    {
+      input.expect_comma()?;
+      direction
+    } else {
+      LineDirection::Vertical(VerticalPositionKeyword::Bottom)
+    };
+    let items = parse_items(input)?;
+    Ok(LinearGradient {
+      direction,
+      items,
+      vendor_prefix,
+    })
+  }
+
+  fn to_css<W>(&self, dest: &mut Printer<W>, is_prefixed: bool) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    let angle = match &self.direction {
+      LineDirection::Vertical(VerticalPositionKeyword::Bottom) => 180.0,
+      LineDirection::Vertical(VerticalPositionKeyword::Top) => 0.0,
+      LineDirection::Angle(angle) => angle.to_degrees(),
+      _ => -1.0,
+    };
+
+    // We can omit `to bottom` or `180deg` because it is the default.
+    if angle == 180.0 {
+      serialize_items(&self.items, dest)
+
+    // If we have `to top` or `0deg`, and all of the positions and hints are percentages,
+    // we can flip the gradient the other direction and omit the direction.
+    } else if angle == 0.0
+      && dest.minify
+      && self.items.iter().all(|item| {
+        matches!(
+          item,
+          GradientItem::Hint(LengthPercentage::Percentage(_))
+            | GradientItem::ColorStop(ColorStop {
+              position: None | Some(LengthPercentage::Percentage(_)),
+              ..
+            })
+        )
+      })
+    {
+      let items: Vec<GradientItem<LengthPercentage>> = self
+        .items
+        .iter()
+        .rev()
+        .map(|item| {
+          // Flip percentages.
+          match item {
+            GradientItem::Hint(LengthPercentage::Percentage(p)) => {
+              GradientItem::Hint(LengthPercentage::Percentage(Percentage(1.0 - p.0)))
+            }
+            GradientItem::ColorStop(ColorStop { color, position }) => GradientItem::ColorStop(ColorStop {
+              color: color.clone(),
+              position: position.clone().map(|p| match p {
+                LengthPercentage::Percentage(p) => LengthPercentage::Percentage(Percentage(1.0 - p.0)),
+                _ => unreachable!(),
+              }),
+            }),
+            _ => unreachable!(),
+          }
+        })
+        .collect();
+      serialize_items(&items, dest)
+    } else {
+      if self.direction != LineDirection::Vertical(VerticalPositionKeyword::Bottom)
+        && self.direction != LineDirection::Angle(Angle::Deg(180.0))
+      {
+        self.direction.to_css(dest, is_prefixed)?;
+        dest.delim(',', false)?;
+      }
+
+      serialize_items(&self.items, dest)
+    }
+  }
+
+  fn get_fallback(&self, kind: ColorFallbackKind) -> LinearGradient {
+    LinearGradient {
+      direction: self.direction.clone(),
+      items: self.items.iter().map(|item| item.get_fallback(kind)).collect(),
+      vendor_prefix: self.vendor_prefix,
+    }
+  }
+}
+
+impl IsCompatible for LinearGradient {
+  fn is_compatible(&self, browsers: Browsers) -> bool {
+    self.items.iter().all(|item| item.is_compatible(browsers))
+  }
+}
+
+/// A CSS [`radial-gradient()`](https://www.w3.org/TR/css-images-3/#radial-gradients) or `repeating-radial-gradient()`.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(rename_all = "camelCase")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub struct RadialGradient {
+  /// The vendor prefixes for the gradient.
+  pub vendor_prefix: VendorPrefix,
+  /// The shape of the gradient.
+  pub shape: EndingShape,
+  /// The position of the gradient.
+  pub position: Position,
+  /// The color stops and transition hints for the gradient.
+  pub items: Vec<GradientItem<LengthPercentage>>,
+}
+
+impl<'i> RadialGradient {
+  fn parse<'t>(
+    input: &mut Parser<'i, 't>,
+    vendor_prefix: VendorPrefix,
+  ) -> Result<RadialGradient, ParseError<'i, ParserError<'i>>> {
+    let shape = input.try_parse(EndingShape::parse).ok();
+    let position = input
+      .try_parse(|input| {
+        input.expect_ident_matching("at")?;
+        Position::parse(input)
+      })
+      .ok();
+
+    if shape.is_some() || position.is_some() {
+      input.expect_comma()?;
+    }
+
+    let items = parse_items(input)?;
+    Ok(RadialGradient {
+      shape: shape.unwrap_or_default(),
+      position: position.unwrap_or(Position::center()),
+      items,
+      vendor_prefix,
+    })
+  }
+}
+
+impl ToCss for RadialGradient {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    if self.shape != EndingShape::default() {
+      self.shape.to_css(dest)?;
+      if self.position.is_center() {
+        dest.delim(',', false)?;
+      } else {
+        dest.write_char(' ')?;
+      }
+    }
+
+    if !self.position.is_center() {
+      dest.write_str("at ")?;
+      self.position.to_css(dest)?;
+      dest.delim(',', false)?;
+    }
+
+    serialize_items(&self.items, dest)
+  }
+}
+
+impl RadialGradient {
+  fn get_fallback(&self, kind: ColorFallbackKind) -> RadialGradient {
+    RadialGradient {
+      shape: self.shape.clone(),
+      position: self.position.clone(),
+      items: self.items.iter().map(|item| item.get_fallback(kind)).collect(),
+      vendor_prefix: self.vendor_prefix,
+    }
+  }
+}
+
+impl IsCompatible for RadialGradient {
+  fn is_compatible(&self, browsers: Browsers) -> bool {
+    self.items.iter().all(|item| item.is_compatible(browsers))
+  }
+}
+
+/// The direction of a CSS `linear-gradient()`.
+///
+/// See [LinearGradient](LinearGradient).
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub enum LineDirection {
+  /// An angle.
+  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<Angle>"))]
+  Angle(Angle),
+  /// A horizontal position keyword, e.g. `left` or `right.
+  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<HorizontalPositionKeyword>"))]
+  Horizontal(HorizontalPositionKeyword),
+  /// A vertical posision keyword, e.g. `top` or `bottom`.
+  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<VerticalPositionKeyword>"))]
+  Vertical(VerticalPositionKeyword),
+  /// A corner, e.g. `bottom left` or `top right`.
+  Corner {
+    /// A horizontal position keyword, e.g. `left` or `right.
+    horizontal: HorizontalPositionKeyword,
+    /// A vertical posision keyword, e.g. `top` or `bottom`.
+    vertical: VerticalPositionKeyword,
+  },
+}
+
+impl LineDirection {
+  fn parse<'i, 't>(
+    input: &mut Parser<'i, 't>,
+    is_prefixed: bool,
+  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    // Spec allows unitless zero angles for gradients.
+    // https://w3c.github.io/csswg-drafts/css-images-3/#linear-gradient-syntax
+    if let Ok(angle) = input.try_parse(Angle::parse_with_unitless_zero) {
+      return Ok(LineDirection::Angle(angle));
+    }
+
+    if !is_prefixed {
+      input.expect_ident_matching("to")?;
+    }
+
+    if let Ok(x) = input.try_parse(HorizontalPositionKeyword::parse) {
+      if let Ok(y) = input.try_parse(VerticalPositionKeyword::parse) {
+        return Ok(LineDirection::Corner {
+          horizontal: x,
+          vertical: y,
+        });
+      }
+      return Ok(LineDirection::Horizontal(x));
+    }
+
+    let y = VerticalPositionKeyword::parse(input)?;
+    if let Ok(x) = input.try_parse(HorizontalPositionKeyword::parse) {
+      return Ok(LineDirection::Corner {
+        horizontal: x,
+        vertical: y,
+      });
+    }
+    Ok(LineDirection::Vertical(y))
+  }
+
+  fn to_css<W>(&self, dest: &mut Printer<W>, is_prefixed: bool) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    match self {
+      LineDirection::Angle(angle) => angle.to_css(dest),
+      LineDirection::Horizontal(k) => {
+        if dest.minify {
+          match k {
+            HorizontalPositionKeyword::Left => dest.write_str("270deg"),
+            HorizontalPositionKeyword::Right => dest.write_str("90deg"),
+          }
+        } else {
+          if !is_prefixed {
+            dest.write_str("to ")?;
+          }
+          k.to_css(dest)
+        }
+      }
+      LineDirection::Vertical(k) => {
+        if dest.minify {
+          match k {
+            VerticalPositionKeyword::Top => dest.write_str("0deg"),
+            VerticalPositionKeyword::Bottom => dest.write_str("180deg"),
+          }
+        } else {
+          if !is_prefixed {
+            dest.write_str("to ")?;
+          }
+          k.to_css(dest)
+        }
+      }
+      LineDirection::Corner { horizontal, vertical } => {
+        if !is_prefixed {
+          dest.write_str("to ")?;
+        }
+        vertical.to_css(dest)?;
+        dest.write_char(' ')?;
+        horizontal.to_css(dest)
+      }
+    }
+  }
+}
+
+/// Converts a standard gradient direction to its legacy vendor-prefixed form.
+///
+/// Inverts keyword-based directions (e.g., `to bottom` → `top`) for compatibility
+/// with legacy prefixed syntaxes.
+///
+/// See: https://github.com/parcel-bundler/lightningcss/issues/918
+fn convert_to_legacy_direction(direction: &LineDirection) -> LineDirection {
+  match direction {
+    LineDirection::Horizontal(HorizontalPositionKeyword::Left) => {
+      LineDirection::Horizontal(HorizontalPositionKeyword::Right)
+    }
+    LineDirection::Horizontal(HorizontalPositionKeyword::Right) => {
+      LineDirection::Horizontal(HorizontalPositionKeyword::Left)
+    }
+    LineDirection::Vertical(VerticalPositionKeyword::Top) => {
+      LineDirection::Vertical(VerticalPositionKeyword::Bottom)
+    }
+    LineDirection::Vertical(VerticalPositionKeyword::Bottom) => {
+      LineDirection::Vertical(VerticalPositionKeyword::Top)
+    }
+    LineDirection::Corner { horizontal, vertical } => LineDirection::Corner {
+      horizontal: match horizontal {
+        HorizontalPositionKeyword::Left => HorizontalPositionKeyword::Right,
+        HorizontalPositionKeyword::Right => HorizontalPositionKeyword::Left,
+      },
+      vertical: match vertical {
+        VerticalPositionKeyword::Top => VerticalPositionKeyword::Bottom,
+        VerticalPositionKeyword::Bottom => VerticalPositionKeyword::Top,
+      },
+    },
+    LineDirection::Angle(angle) => {
+      let angle = angle.clone();
+      let deg = match angle {
+        Angle::Deg(n) => convert_to_legacy_degree(n),
+        Angle::Rad(n) => {
+          let n = n / (2.0 * PI) * 360.0;
+          convert_to_legacy_degree(n)
+        }
+        Angle::Grad(n) => {
+          let n = n / 400.0 * 360.0;
+          convert_to_legacy_degree(n)
+        }
+        Angle::Turn(n) => {
+          let n = n * 360.0;
+          convert_to_legacy_degree(n)
+        }
+      };
+      LineDirection::Angle(Angle::Deg(deg))
+    }
+  }
+}
+
+fn convert_to_legacy_degree(degree: f32) -> f32 {
+  // Add 90 degrees
+  let n = (450.0 - degree).abs() % 360.0;
+  // Round the number to 3 decimal places
+  (n * 1000.0).round() / 1000.0
+}
+
+/// A `radial-gradient()` [ending shape](https://www.w3.org/TR/css-images-3/#valdef-radial-gradient-ending-shape).
+///
+/// See [RadialGradient](RadialGradient).
+#[derive(Debug, Clone, PartialEq, Parse, ToCss)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", content = "value", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub enum EndingShape {
+  // Note: Ellipse::parse MUST run before Circle::parse for this to be correct.
+  /// An ellipse.
+  Ellipse(Ellipse),
+  /// A circle.
+  Circle(Circle),
+}
+
+impl Default for EndingShape {
+  fn default() -> EndingShape {
+    EndingShape::Ellipse(Ellipse::Extent(ShapeExtent::FarthestCorner))
+  }
+}
+
+/// A circle ending shape for a `radial-gradient()`.
+///
+/// See [RadialGradient](RadialGradient).
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", content = "value", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub enum Circle {
+  /// A circle with a specified radius.
+  Radius(Length),
+  /// A shape extent keyword.
+  Extent(ShapeExtent),
+}
+
+impl<'i> Parse<'i> for Circle {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    if let Ok(extent) = input.try_parse(ShapeExtent::parse) {
+      // The `circle` keyword is required. If it's not there, then it's an ellipse.
+      input.expect_ident_matching("circle")?;
+      return Ok(Circle::Extent(extent));
+    }
+
+    if let Ok(length) = input.try_parse(Length::parse) {
+      // The `circle` keyword is optional if there is only a single length.
+      // We are assuming here that Ellipse::parse ran first.
+      let _ = input.try_parse(|input| input.expect_ident_matching("circle"));
+      return Ok(Circle::Radius(length));
+    }
+
+    if input.try_parse(|input| input.expect_ident_matching("circle")).is_ok() {
+      if let Ok(extent) = input.try_parse(ShapeExtent::parse) {
+        return Ok(Circle::Extent(extent));
+      }
+
+      if let Ok(length) = input.try_parse(Length::parse) {
+        return Ok(Circle::Radius(length));
+      }
+
+      // If only the `circle` keyword was given, default to `farthest-corner`.
+      return Ok(Circle::Extent(ShapeExtent::FarthestCorner));
+    }
+
+    return Err(input.new_error_for_next_token());
+  }
+}
+
+impl ToCss for Circle {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    match self {
+      Circle::Radius(r) => r.to_css(dest),
+      Circle::Extent(extent) => {
+        dest.write_str("circle")?;
+        if *extent != ShapeExtent::FarthestCorner {
+          dest.write_char(' ')?;
+          extent.to_css(dest)?;
+        }
+        Ok(())
+      }
+    }
+  }
+}
+
+/// An ellipse ending shape for a `radial-gradient()`.
+///
+/// See [RadialGradient](RadialGradient).
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub enum Ellipse {
+  /// An ellipse with a specified horizontal and vertical radius.
+  Size {
+    /// The x-radius of the ellipse.
+    x: LengthPercentage,
+    /// The y-radius of the ellipse.
+    y: LengthPercentage,
+  },
+  /// A shape extent keyword.
+  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<ShapeExtent>"))]
+  Extent(ShapeExtent),
+}
+
+impl<'i> Parse<'i> for Ellipse {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    if let Ok(extent) = input.try_parse(ShapeExtent::parse) {
+      // The `ellipse` keyword is optional, but only if the `circle` keyword is not present.
+      // If it is, then we'll re-parse as a circle.
+      if input.try_parse(|input| input.expect_ident_matching("circle")).is_ok() {
+        return Err(input.new_error_for_next_token());
+      }
+      let _ = input.try_parse(|input| input.expect_ident_matching("ellipse"));
+      return Ok(Ellipse::Extent(extent));
+    }
+
+    if let Ok(x) = input.try_parse(LengthPercentage::parse) {
+      let y = LengthPercentage::parse(input)?;
+      // The `ellipse` keyword is optional if there are two lengths.
+      let _ = input.try_parse(|input| input.expect_ident_matching("ellipse"));
+      return Ok(Ellipse::Size { x, y });
+    }
+
+    if input.try_parse(|input| input.expect_ident_matching("ellipse")).is_ok() {
+      if let Ok(extent) = input.try_parse(ShapeExtent::parse) {
+        return Ok(Ellipse::Extent(extent));
+      }
+
+      if let Ok(x) = input.try_parse(LengthPercentage::parse) {
+        let y = LengthPercentage::parse(input)?;
+        return Ok(Ellipse::Size { x, y });
+      }
+
+      // Assume `farthest-corner` if only the `ellipse` keyword is present.
+      return Ok(Ellipse::Extent(ShapeExtent::FarthestCorner));
+    }
+
+    return Err(input.new_error_for_next_token());
+  }
+}
+
+impl ToCss for Ellipse {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    // The `ellipse` keyword is optional, so we don't emit it.
+    match self {
+      Ellipse::Size { x, y } => {
+        x.to_css(dest)?;
+        dest.write_char(' ')?;
+        y.to_css(dest)
+      }
+      Ellipse::Extent(extent) => extent.to_css(dest),
+    }
+  }
+}
+
+enum_property! {
+  /// A shape extent for a `radial-gradient()`.
+  ///
+  /// See [RadialGradient](RadialGradient).
+  pub enum ShapeExtent {
+    /// The closest side of the box to the gradient's center.
+    ClosestSide,
+    /// The farthest side of the box from the gradient's center.
+    FarthestSide,
+    /// The closest cornder of the box to the gradient's center.
+    ClosestCorner,
+    /// The farthest corner of the box from the gradient's center.
+    FarthestCorner,
+  }
+}
+
+/// A CSS [`conic-gradient()`](https://www.w3.org/TR/css-images-4/#conic-gradients) or `repeating-conic-gradient()`.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub struct ConicGradient {
+  /// The angle of the gradient.
+  pub angle: Angle,
+  /// The position of the gradient.
+  pub position: Position,
+  /// The color stops and transition hints for the gradient.
+  pub items: Vec<GradientItem<AnglePercentage>>,
+}
+
+impl ConicGradient {
+  fn parse<'i, 't>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let angle = input.try_parse(|input| {
+      input.expect_ident_matching("from")?;
+      // Spec allows unitless zero angles for gradients.
+      // https://w3c.github.io/csswg-drafts/css-images-4/#valdef-conic-gradient-angle
+      Angle::parse_with_unitless_zero(input)
+    });
+
+    let position = input.try_parse(|input| {
+      input.expect_ident_matching("at")?;
+      Position::parse(input)
+    });
+
+    if angle.is_ok() || position.is_ok() {
+      input.expect_comma()?;
+    }
+
+    let items = parse_items(input)?;
+    Ok(ConicGradient {
+      angle: angle.unwrap_or(Angle::Deg(0.0)),
+      position: position.unwrap_or(Position::center()),
+      items,
+    })
+  }
+}
+
+impl ToCss for ConicGradient {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    if !self.angle.is_zero() {
+      dest.write_str("from ")?;
+      self.angle.to_css(dest)?;
+
+      if self.position.is_center() {
+        dest.delim(',', false)?;
+      } else {
+        dest.write_char(' ')?;
+      }
+    }
+
+    if !self.position.is_center() {
+      dest.write_str("at ")?;
+      self.position.to_css(dest)?;
+      dest.delim(',', false)?;
+    }
+
+    serialize_items(&self.items, dest)
+  }
+}
+
+impl ConicGradient {
+  fn get_fallback(&self, kind: ColorFallbackKind) -> ConicGradient {
+    ConicGradient {
+      angle: self.angle.clone(),
+      position: self.position.clone(),
+      items: self.items.iter().map(|item| item.get_fallback(kind)).collect(),
+    }
+  }
+}
+
+impl IsCompatible for ConicGradient {
+  fn is_compatible(&self, browsers: Browsers) -> bool {
+    self.items.iter().all(|item| item.is_compatible(browsers))
+  }
+}
+
+/// A [`<color-stop>`](https://www.w3.org/TR/css-images-4/#color-stop-syntax) within a gradient.
+///
+/// This type is generic, and may be either a [LengthPercentage](super::length::LengthPercentage)
+/// or [Angle](super::angle::Angle) depending on what type of gradient it is within.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub struct ColorStop<D> {
+  /// The color of the color stop.
+  pub color: CssColor,
+  /// The position of the color stop.
+  pub position: Option<D>,
+}
+
+impl<'i, D: Parse<'i>> Parse<'i> for ColorStop<D> {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let color = CssColor::parse(input)?;
+    let position = input.try_parse(D::parse).ok();
+    Ok(ColorStop { color, position })
+  }
+}
+
+impl<D: ToCss> ToCss for ColorStop<D> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    self.color.to_css(dest)?;
+    if let Some(position) = &self.position {
+      dest.write_char(' ')?;
+      position.to_css(dest)?;
+    }
+    Ok(())
+  }
+}
+
+/// Either a color stop or interpolation hint within a gradient.
+///
+/// This type is generic, and items may be either a [LengthPercentage](super::length::LengthPercentage)
+/// or [Angle](super::angle::Angle) depending on what type of gradient it is within.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub enum GradientItem<D> {
+  /// A color stop.
+  ColorStop(ColorStop<D>),
+  /// A color interpolation hint.
+  #[cfg_attr(
+    feature = "serde",
+    serde(
+      bound(serialize = "D: serde::Serialize", deserialize = "D: serde::Deserialize<'de>"),
+      with = "ValueWrapper::<D>"
+    )
+  )]
+  Hint(D),
+}
+
+impl<D: ToCss> ToCss for GradientItem<D> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    match self {
+      GradientItem::ColorStop(stop) => stop.to_css(dest),
+      GradientItem::Hint(hint) => hint.to_css(dest),
+    }
+  }
+}
+
+impl<D: Clone> GradientItem<D> {
+  /// Returns the color fallback types needed for the given browser targets.
+  pub fn get_necessary_fallbacks(&self, targets: Targets) -> ColorFallbackKind {
+    match self {
+      GradientItem::ColorStop(stop) => stop.color.get_necessary_fallbacks(targets),
+      GradientItem::Hint(..) => ColorFallbackKind::empty(),
+    }
+  }
+
+  /// Returns a fallback gradient item for the given color fallback type.
+  pub fn get_fallback(&self, kind: ColorFallbackKind) -> GradientItem<D> {
+    match self {
+      GradientItem::ColorStop(stop) => GradientItem::ColorStop(ColorStop {
+        color: stop.color.get_fallback(kind),
+        position: stop.position.clone(),
+      }),
+      GradientItem::Hint(..) => self.clone(),
+    }
+  }
+}
+
+impl<D> IsCompatible for GradientItem<D> {
+  fn is_compatible(&self, browsers: Browsers) -> bool {
+    match self {
+      GradientItem::ColorStop(c) => c.color.is_compatible(browsers),
+      GradientItem::Hint(..) => compat::Feature::GradientInterpolationHints.is_compatible(browsers),
+    }
+  }
+}
+
+fn parse_items<'i, 't, D: Parse<'i>>(
+  input: &mut Parser<'i, 't>,
+) -> Result<Vec<GradientItem<D>>, ParseError<'i, ParserError<'i>>> {
+  let mut items = Vec::new();
+  let mut seen_stop = false;
+
+  loop {
+    input.parse_until_before(Delimiter::Comma, |input| {
+      if seen_stop {
+        if let Ok(hint) = input.try_parse(D::parse) {
+          seen_stop = false;
+          items.push(GradientItem::Hint(hint));
+          return Ok(());
+        }
+      }
+
+      let stop = ColorStop::parse(input)?;
+
+      if let Ok(position) = input.try_parse(D::parse) {
+        let color = stop.color.clone();
+        items.push(GradientItem::ColorStop(stop));
+
+        items.push(GradientItem::ColorStop(ColorStop {
+          color,
+          position: Some(position),
+        }))
+      } else {
+        items.push(GradientItem::ColorStop(stop));
+      }
+
+      seen_stop = true;
+      Ok(())
+    })?;
+
+    match input.next() {
+      Err(_) => break,
+      Ok(Token::Comma) => continue,
+      _ => unreachable!(),
+    }
+  }
+
+  Ok(items)
+}
+
+fn serialize_items<
+  D: ToCss + std::cmp::PartialEq<D> + std::ops::Mul<f32, Output = D> + TrySign + Clone + std::fmt::Debug,
+  W,
+>(
+  items: &Vec<GradientItem<DimensionPercentage<D>>>,
+  dest: &mut Printer<W>,
+) -> Result<(), PrinterError>
+where
+  W: std::fmt::Write,
+{
+  let mut first = true;
+  let mut last: Option<&GradientItem<DimensionPercentage<D>>> = None;
+  for item in items {
+    // Skip useless hints
+    if *item == GradientItem::Hint(DimensionPercentage::Percentage(Percentage(0.5))) {
+      continue;
+    }
+
+    // Use double position stop if the last stop is the same color and all targets support it.
+    if let Some(prev) = last {
+      if !should_compile!(dest.targets.current, DoublePositionGradients) {
+        match (prev, item) {
+          (
+            GradientItem::ColorStop(ColorStop {
+              position: Some(_),
+              color: ca,
+            }),
+            GradientItem::ColorStop(ColorStop {
+              position: Some(p),
+              color: cb,
+            }),
+          ) if ca == cb => {
+            dest.write_char(' ')?;
+            p.to_css(dest)?;
+            last = None;
+            continue;
+          }
+          _ => {}
+        }
+      }
+    }
+
+    if first {
+      first = false;
+    } else {
+      dest.delim(',', false)?;
+    }
+    item.to_css(dest)?;
+    last = Some(item)
+  }
+  Ok(())
+}
+
+/// A legacy `-webkit-gradient()`.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "kind", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum WebKitGradient {
+  /// A linear `-webkit-gradient()`.
+  Linear {
+    /// The starting point of the gradient.
+    from: WebKitGradientPoint,
+    /// The ending point of the gradient.
+    to: WebKitGradientPoint,
+    /// The color stops in the gradient.
+    stops: Vec<WebKitColorStop>,
+  },
+  /// A radial `-webkit-gradient()`.
+  Radial {
+    /// The starting point of the gradient.
+    from: WebKitGradientPoint,
+    /// The starting radius of the gradient.
+    r0: CSSNumber,
+    /// The ending point of the gradient.
+    to: WebKitGradientPoint,
+    /// The ending radius of the gradient.
+    r1: CSSNumber,
+    /// The color stops in the gradient.
+    stops: Vec<WebKitColorStop>,
+  },
+}
+
+impl<'i> Parse<'i> for WebKitGradient {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let location = input.current_source_location();
+    let ident = input.expect_ident_cloned()?;
+    input.expect_comma()?;
+
+    match_ignore_ascii_case! { &ident,
+      "linear" => {
+        let from = WebKitGradientPoint::parse(input)?;
+        input.expect_comma()?;
+        let to = WebKitGradientPoint::parse(input)?;
+        input.expect_comma()?;
+        let stops = input.parse_comma_separated(WebKitColorStop::parse)?;
+        Ok(WebKitGradient::Linear {
+          from,
+          to,
+          stops
+        })
+      },
+      "radial" => {
+        let from = WebKitGradientPoint::parse(input)?;
+        input.expect_comma()?;
+        let r0 = CSSNumber::parse(input)?;
+        input.expect_comma()?;
+        let to = WebKitGradientPoint::parse(input)?;
+        input.expect_comma()?;
+        let r1 = CSSNumber::parse(input)?;
+        input.expect_comma()?;
+        let stops = input.parse_comma_separated(WebKitColorStop::parse)?;
+        Ok(WebKitGradient::Radial {
+          from,
+          r0,
+          to,
+          r1,
+          stops
+        })
+      },
+      _ => Err(location.new_unexpected_token_error(cssparser::Token::Ident(ident.clone())))
+    }
+  }
+}
+
+impl ToCss for WebKitGradient {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    match self {
+      WebKitGradient::Linear { from, to, stops } => {
+        dest.write_str("linear")?;
+        dest.delim(',', false)?;
+        from.to_css(dest)?;
+        dest.delim(',', false)?;
+        to.to_css(dest)?;
+        for stop in stops {
+          dest.delim(',', false)?;
+          stop.to_css(dest)?;
+        }
+        Ok(())
+      }
+      WebKitGradient::Radial {
+        from,
+        r0,
+        to,
+        r1,
+        stops,
+      } => {
+        dest.write_str("radial")?;
+        dest.delim(',', false)?;
+        from.to_css(dest)?;
+        dest.delim(',', false)?;
+        r0.to_css(dest)?;
+        dest.delim(',', false)?;
+        to.to_css(dest)?;
+        dest.delim(',', false)?;
+        r1.to_css(dest)?;
+        for stop in stops {
+          dest.delim(',', false)?;
+          stop.to_css(dest)?;
+        }
+        Ok(())
+      }
+    }
+  }
+}
+
+impl WebKitGradient {
+  fn get_fallback(&self, kind: ColorFallbackKind) -> WebKitGradient {
+    let stops = match self {
+      WebKitGradient::Linear { stops, .. } => stops,
+      WebKitGradient::Radial { stops, .. } => stops,
+    };
+
+    let stops = stops.iter().map(|stop| stop.get_fallback(kind)).collect();
+
+    match self {
+      WebKitGradient::Linear { from, to, .. } => WebKitGradient::Linear {
+        from: from.clone(),
+        to: to.clone(),
+        stops,
+      },
+      WebKitGradient::Radial { from, r0, to, r1, .. } => WebKitGradient::Radial {
+        from: from.clone(),
+        r0: *r0,
+        to: to.clone(),
+        r1: *r1,
+        stops,
+      },
+    }
+  }
+}
+
+/// A color stop within a legacy `-webkit-gradient()`.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub struct WebKitColorStop {
+  /// The color of the color stop.
+  pub color: CssColor,
+  /// The position of the color stop.
+  pub position: CSSNumber,
+}
+
+impl<'i> Parse<'i> for WebKitColorStop {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let location = input.current_source_location();
+    let function = input.expect_function()?.clone();
+    input.parse_nested_block(|input| {
+      let position = match_ignore_ascii_case! { &function,
+        "color-stop" => {
+          let p = NumberOrPercentage::parse(input)?;
+          input.expect_comma()?;
+          (&p).into()
+        },
+        "from" => 0.0,
+        "to" => 1.0,
+        _ => return Err(location.new_unexpected_token_error(cssparser::Token::Ident(function.clone())))
+      };
+      let color = CssColor::parse(input)?;
+      Ok(WebKitColorStop { color, position })
+    })
+  }
+}
+
+impl ToCss for WebKitColorStop {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    if self.position == 0.0 {
+      dest.write_str("from(")?;
+      self.color.to_css(dest)?;
+    } else if self.position == 1.0 {
+      dest.write_str("to(")?;
+      self.color.to_css(dest)?;
+    } else {
+      dest.write_str("color-stop(")?;
+      self.position.to_css(dest)?;
+      dest.delim(',', false)?;
+      self.color.to_css(dest)?;
+    }
+    dest.write_char(')')
+  }
+}
+
+impl WebKitColorStop {
+  fn get_fallback(&self, kind: ColorFallbackKind) -> WebKitColorStop {
+    WebKitColorStop {
+      color: self.color.get_fallback(kind),
+      position: self.position,
+    }
+  }
+}
+
+/// An x/y position within a legacy `-webkit-gradient()`.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub struct WebKitGradientPoint {
+  /// The x-position.
+  pub x: WebKitGradientPointComponent<HorizontalPositionKeyword>,
+  /// The y-position.
+  pub y: WebKitGradientPointComponent<VerticalPositionKeyword>,
+}
+
+impl<'i> Parse<'i> for WebKitGradientPoint {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let x = WebKitGradientPointComponent::parse(input)?;
+    let y = WebKitGradientPointComponent::parse(input)?;
+    Ok(WebKitGradientPoint { x, y })
+  }
+}
+
+impl ToCss for WebKitGradientPoint {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    self.x.to_css(dest)?;
+    dest.write_char(' ')?;
+    self.y.to_css(dest)
+  }
+}
+
+/// A keyword or number within a [WebKitGradientPoint](WebKitGradientPoint).
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", content = "value", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum WebKitGradientPointComponent<S> {
+  /// The `center` keyword.
+  Center,
+  /// A number or percentage.
+  Number(NumberOrPercentage),
+  /// A side keyword.
+  Side(S),
+}
+
+impl<'i, S: Parse<'i>> Parse<'i> for WebKitGradientPointComponent<S> {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    if input.try_parse(|i| i.expect_ident_matching("center")).is_ok() {
+      return Ok(WebKitGradientPointComponent::Center);
+    }
+
+    if let Ok(lp) = input.try_parse(NumberOrPercentage::parse) {
+      return Ok(WebKitGradientPointComponent::Number(lp));
+    }
+
+    let keyword = S::parse(input)?;
+    Ok(WebKitGradientPointComponent::Side(keyword))
+  }
+}
+
+impl<S: ToCss + Clone + Into<LengthPercentage>> ToCss for WebKitGradientPointComponent<S> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    use WebKitGradientPointComponent::*;
+    match &self {
+      Center => {
+        if dest.minify {
+          dest.write_str("50%")
+        } else {
+          dest.write_str("center")
+        }
+      }
+      Number(lp) => {
+        if matches!(lp, NumberOrPercentage::Percentage(Percentage(p)) if *p == 0.0) {
+          dest.write_char('0')
+        } else {
+          lp.to_css(dest)
+        }
+      }
+      Side(s) => {
+        if dest.minify {
+          let lp: LengthPercentage = s.clone().into();
+          lp.to_css(dest)?;
+        } else {
+          s.to_css(dest)?;
+        }
+        Ok(())
+      }
+    }
+  }
+}
+
+impl<S: Clone> WebKitGradientPointComponent<S> {
+  /// Attempts to convert a standard position to a webkit gradient point.
+  fn from_position(pos: &PositionComponent<S>) -> Result<WebKitGradientPointComponent<S>, ()> {
+    match pos {
+      PositionComponent::Center => Ok(WebKitGradientPointComponent::Center),
+      PositionComponent::Length(len) => {
+        Ok(WebKitGradientPointComponent::Number(match len {
+          LengthPercentage::Percentage(p) => NumberOrPercentage::Percentage(p.clone()),
+          LengthPercentage::Dimension(d) => {
+            // Webkit gradient points can only be specified in pixels.
+            if let Some(px) = d.to_px() {
+              NumberOrPercentage::Number(px)
+            } else {
+              return Err(());
+            }
+          }
+          _ => return Err(()),
+        }))
+      }
+      PositionComponent::Side { side, offset } => {
+        if offset.is_some() {
+          return Err(());
+        }
+        Ok(WebKitGradientPointComponent::Side(side.clone()))
+      }
+    }
+  }
+}
+
+impl WebKitGradient {
+  /// Attempts to convert a standard gradient to a legacy -webkit-gradient()
+  pub fn from_standard(gradient: &Gradient) -> Result<WebKitGradient, ()> {
+    match gradient {
+      Gradient::Linear(linear) => {
+        // Convert from line direction to a from and to point, if possible.
+        let (from, to) = match &linear.direction {
+          LineDirection::Horizontal(horizontal) => match horizontal {
+            HorizontalPositionKeyword::Left => ((1.0, 0.0), (0.0, 0.0)),
+            HorizontalPositionKeyword::Right => ((0.0, 0.0), (1.0, 0.0)),
+          },
+          LineDirection::Vertical(vertical) => match vertical {
+            VerticalPositionKeyword::Top => ((0.0, 1.0), (0.0, 0.0)),
+            VerticalPositionKeyword::Bottom => ((0.0, 0.0), (0.0, 1.0)),
+          },
+          LineDirection::Corner { horizontal, vertical } => match (horizontal, vertical) {
+            (HorizontalPositionKeyword::Left, VerticalPositionKeyword::Top) => ((1.0, 1.0), (0.0, 0.0)),
+            (HorizontalPositionKeyword::Left, VerticalPositionKeyword::Bottom) => ((1.0, 0.0), (0.0, 1.0)),
+            (HorizontalPositionKeyword::Right, VerticalPositionKeyword::Top) => ((0.0, 1.0), (1.0, 0.0)),
+            (HorizontalPositionKeyword::Right, VerticalPositionKeyword::Bottom) => ((0.0, 0.0), (1.0, 1.0)),
+          },
+          LineDirection::Angle(angle) => {
+            let degrees = angle.to_degrees();
+            if degrees == 0.0 {
+              ((0.0, 1.0), (0.0, 0.0))
+            } else if degrees == 90.0 {
+              ((0.0, 0.0), (1.0, 0.0))
+            } else if degrees == 180.0 {
+              ((0.0, 0.0), (0.0, 1.0))
+            } else if degrees == 270.0 {
+              ((1.0, 0.0), (0.0, 0.0))
+            } else {
+              return Err(());
+            }
+          }
+        };
+
+        Ok(WebKitGradient::Linear {
+          from: WebKitGradientPoint {
+            x: WebKitGradientPointComponent::Number(NumberOrPercentage::Percentage(Percentage(from.0))),
+            y: WebKitGradientPointComponent::Number(NumberOrPercentage::Percentage(Percentage(from.1))),
+          },
+          to: WebKitGradientPoint {
+            x: WebKitGradientPointComponent::Number(NumberOrPercentage::Percentage(Percentage(to.0))),
+            y: WebKitGradientPointComponent::Number(NumberOrPercentage::Percentage(Percentage(to.1))),
+          },
+          stops: convert_stops_to_webkit(&linear.items)?,
+        })
+      }
+      Gradient::Radial(radial) => {
+        // Webkit radial gradients are always circles, not ellipses, and must be specified in pixels.
+        let radius = match &radial.shape {
+          EndingShape::Circle(Circle::Radius(radius)) => {
+            if let Some(r) = radius.to_px() {
+              r
+            } else {
+              return Err(());
+            }
+          }
+          _ => return Err(()),
+        };
+
+        let x = WebKitGradientPointComponent::from_position(&radial.position.x)?;
+        let y = WebKitGradientPointComponent::from_position(&radial.position.y)?;
+        let point = WebKitGradientPoint { x, y };
+        Ok(WebKitGradient::Radial {
+          from: point.clone(),
+          r0: 0.0,
+          to: point,
+          r1: radius,
+          stops: convert_stops_to_webkit(&radial.items)?,
+        })
+      }
+      _ => Err(()),
+    }
+  }
+}
+
+fn convert_stops_to_webkit(items: &Vec<GradientItem<LengthPercentage>>) -> Result<Vec<WebKitColorStop>, ()> {
+  let mut stops = Vec::with_capacity(items.len());
+  for (i, item) in items.iter().enumerate() {
+    match item {
+      GradientItem::ColorStop(stop) => {
+        // webkit stops must always be percentage based, not length based.
+        let position = if let Some(pos) = &stop.position {
+          if let LengthPercentage::Percentage(position) = pos {
+            position.0
+          } else {
+            return Err(());
+          }
+        } else if i == 0 {
+          0.0
+        } else if i == items.len() - 1 {
+          1.0
+        } else {
+          return Err(());
+        };
+
+        stops.push(WebKitColorStop {
+          color: stop.color.clone(),
+          position,
+        })
+      }
+      _ => return Err(()),
+    }
+  }
+
+  Ok(stops)
+}
diff --git a/src/values/ident.rs b/src/values/ident.rs
new file mode 100644
index 0000000..be850f8
--- /dev/null
+++ b/src/values/ident.rs
@@ -0,0 +1,288 @@
+//! CSS identifiers.
+
+use crate::error::{ParserError, PrinterError};
+use crate::printer::Printer;
+use crate::properties::css_modules::Specifier;
+use crate::traits::{Parse, ParseWithOptions, ToCss};
+use crate::values::string::CowArcStr;
+#[cfg(feature = "visitor")]
+use crate::visitor::Visit;
+use cssparser::*;
+use smallvec::SmallVec;
+use std::borrow::Borrow;
+use std::ops::Deref;
+
+use super::string::impl_string_type;
+
+/// A CSS [`<custom-ident>`](https://www.w3.org/TR/css-values-4/#custom-idents).
+///
+/// Custom idents are author defined, and allow any valid identifier except the
+/// [CSS-wide keywords](https://www.w3.org/TR/css-values-4/#css-wide-keywords).
+/// They may be renamed to include a hash when compiled as part of a CSS module.
+#[derive(Debug, Clone, Eq, Hash)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(feature = "visitor", visit(visit_custom_ident, CUSTOM_IDENTS))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(transparent))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub struct CustomIdent<'i>(#[cfg_attr(feature = "serde", serde(borrow))] pub CowArcStr<'i>);
+
+impl<'i> Parse<'i> for CustomIdent<'i> {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let location = input.current_source_location();
+    let ident = input.expect_ident()?;
+    let valid = match_ignore_ascii_case! { &ident,
+      "initial" | "inherit" | "unset" | "default" | "revert" | "revert-layer" => false,
+      _ => true
+    };
+
+    if !valid {
+      return Err(location.new_unexpected_token_error(Token::Ident(ident.clone())));
+    }
+
+    Ok(CustomIdent(ident.into()))
+  }
+}
+
+impl<'i> ToCss for CustomIdent<'i> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    self.to_css_with_options(dest, true)
+  }
+}
+
+impl<'i> CustomIdent<'i> {
+  /// Write the custom ident to CSS.
+  pub(crate) fn to_css_with_options<W>(
+    &self,
+    dest: &mut Printer<W>,
+    enabled_css_modules: bool,
+  ) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    let css_module_custom_idents_enabled = enabled_css_modules
+      && dest
+        .css_module
+        .as_mut()
+        .map_or(false, |css_module| css_module.config.custom_idents);
+    dest.write_ident(&self.0, css_module_custom_idents_enabled)
+  }
+}
+
+/// A list of CSS [`<custom-ident>`](https://www.w3.org/TR/css-values-4/#custom-idents) values.
+pub type CustomIdentList<'i> = SmallVec<[CustomIdent<'i>; 1]>;
+
+/// The `none` keyword, or a space-separated list of custom idents.
+#[derive(Debug, Clone, PartialEq, Default)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub enum NoneOrCustomIdentList<'i> {
+  /// None.
+  #[default]
+  None,
+  /// A list of idents.
+  #[cfg_attr(feature = "serde", serde(borrow, untagged))]
+  Idents(SmallVec<[CustomIdent<'i>; 1]>),
+}
+
+impl<'i> Parse<'i> for NoneOrCustomIdentList<'i> {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let mut types = SmallVec::new();
+    loop {
+      if let Ok(ident) = input.try_parse(CustomIdent::parse) {
+        if ident == "none" {
+          if types.is_empty() {
+            return Ok(NoneOrCustomIdentList::None);
+          } else {
+            return Err(input.new_custom_error(ParserError::InvalidValue));
+          }
+        }
+
+        types.push(ident);
+      } else {
+        return Ok(NoneOrCustomIdentList::Idents(types));
+      }
+    }
+  }
+}
+
+impl<'i> ToCss for NoneOrCustomIdentList<'i> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    match self {
+      NoneOrCustomIdentList::None => dest.write_str("none"),
+      NoneOrCustomIdentList::Idents(types) => {
+        let mut first = true;
+        for ident in types {
+          if !first {
+            dest.write_char(' ')?;
+          } else {
+            first = false;
+          }
+          ident.to_css(dest)?;
+        }
+        Ok(())
+      }
+    }
+  }
+}
+
+/// A CSS [`<dashed-ident>`](https://www.w3.org/TR/css-values-4/#dashed-idents) declaration.
+///
+/// Dashed idents are used in cases where an identifier can be either author defined _or_ CSS-defined.
+/// Author defined idents must start with two dash characters ("--") or parsing will fail.
+#[derive(Debug, Clone, Eq, Hash)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(feature = "visitor", visit(visit_dashed_ident, DASHED_IDENTS))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize), serde(transparent))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub struct DashedIdent<'i>(#[cfg_attr(feature = "serde", serde(borrow))] pub CowArcStr<'i>);
+
+impl<'i> Parse<'i> for DashedIdent<'i> {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let location = input.current_source_location();
+    let ident = input.expect_ident()?;
+    if !ident.starts_with("--") {
+      return Err(location.new_unexpected_token_error(Token::Ident(ident.clone())));
+    }
+
+    Ok(DashedIdent(ident.into()))
+  }
+}
+
+impl<'i> ToCss for DashedIdent<'i> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    dest.write_dashed_ident(&self.0, true)
+  }
+}
+
+#[cfg(feature = "serde")]
+impl<'i, 'de: 'i> serde::Deserialize<'de> for DashedIdent<'i> {
+  fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+  where
+    D: serde::Deserializer<'de>,
+  {
+    let ident = CowArcStr::deserialize(deserializer)?;
+    if !ident.starts_with("--") {
+      return Err(serde::de::Error::custom("Dashed idents must start with --"));
+    }
+
+    Ok(DashedIdent(ident))
+  }
+}
+
+/// A CSS [`<dashed-ident>`](https://www.w3.org/TR/css-values-4/#dashed-idents) reference.
+///
+/// Dashed idents are used in cases where an identifier can be either author defined _or_ CSS-defined.
+/// Author defined idents must start with two dash characters ("--") or parsing will fail.
+///
+/// In CSS modules, when the `dashed_idents` option is enabled, the identifier may be followed by the
+/// `from` keyword and an argument indicating where the referenced identifier is declared (e.g. a filename).
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub struct DashedIdentReference<'i> {
+  /// The referenced identifier.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  pub ident: DashedIdent<'i>,
+  /// CSS modules extension: the filename where the variable is defined.
+  /// Only enabled when the CSS modules `dashed_idents` option is turned on.
+  pub from: Option<Specifier<'i>>,
+}
+
+impl<'i> ParseWithOptions<'i> for DashedIdentReference<'i> {
+  fn parse_with_options<'t>(
+    input: &mut Parser<'i, 't>,
+    options: &crate::stylesheet::ParserOptions,
+  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let ident = DashedIdent::parse(input)?;
+
+    let from = match &options.css_modules {
+      Some(config) if config.dashed_idents => {
+        if input.try_parse(|input| input.expect_ident_matching("from")).is_ok() {
+          Some(Specifier::parse(input)?)
+        } else {
+          None
+        }
+      }
+      _ => None,
+    };
+
+    Ok(DashedIdentReference { ident, from })
+  }
+}
+
+impl<'i> ToCss for DashedIdentReference<'i> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    match &mut dest.css_module {
+      Some(css_module) if css_module.config.dashed_idents => {
+        if let Some(name) = css_module.reference_dashed(&self.ident.0, &self.from, dest.loc.source_index) {
+          dest.write_str("--")?;
+          serialize_name(&name, dest)?;
+          return Ok(());
+        }
+      }
+      _ => {}
+    }
+
+    dest.write_dashed_ident(&self.ident.0, false)
+  }
+}
+
+/// A CSS [`<ident>`](https://www.w3.org/TR/css-values-4/#css-css-identifier).
+#[derive(Debug, Clone, Eq, Hash, Default)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(transparent))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub struct Ident<'i>(#[cfg_attr(feature = "serde", serde(borrow))] pub CowArcStr<'i>);
+
+impl<'i> Parse<'i> for Ident<'i> {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let ident = input.expect_ident()?;
+    Ok(Ident(ident.into()))
+  }
+}
+
+impl<'i> ToCss for Ident<'i> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    serialize_identifier(&self.0, dest)?;
+    Ok(())
+  }
+}
+
+impl<'i> cssparser::ToCss for Ident<'i> {
+  fn to_css<W>(&self, dest: &mut W) -> std::fmt::Result
+  where
+    W: std::fmt::Write,
+  {
+    serialize_identifier(&self.0, dest)
+  }
+}
+
+impl_string_type!(Ident);
+impl_string_type!(CustomIdent);
+impl_string_type!(DashedIdent);
diff --git a/src/values/image.rs b/src/values/image.rs
new file mode 100644
index 0000000..4964c98
--- /dev/null
+++ b/src/values/image.rs
@@ -0,0 +1,498 @@
+//! CSS image values.
+
+use super::color::ColorFallbackKind;
+use super::gradient::*;
+use super::resolution::Resolution;
+use crate::compat;
+use crate::dependencies::{Dependency, UrlDependency};
+use crate::error::{ParserError, PrinterError};
+use crate::prefixes::{is_webkit_gradient, Feature};
+use crate::printer::Printer;
+use crate::targets::{Browsers, Targets};
+use crate::traits::{FallbackValues, IsCompatible, Parse, ToCss};
+use crate::values::string::CowArcStr;
+use crate::values::url::Url;
+use crate::vendor_prefix::VendorPrefix;
+#[cfg(feature = "visitor")]
+use crate::visitor::Visit;
+use cssparser::*;
+use smallvec::SmallVec;
+
+/// A CSS [`<image>`](https://www.w3.org/TR/css-images-3/#image-values) value.
+#[derive(Debug, Clone, PartialEq, Parse, ToCss)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(feature = "visitor", visit(visit_image, IMAGES))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", content = "value", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub enum Image<'i> {
+  /// The `none` keyword.
+  None,
+  /// A `url()`.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  Url(Url<'i>),
+  /// A gradient.
+  Gradient(Box<Gradient>),
+  /// An `image-set()`.
+  ImageSet(ImageSet<'i>),
+}
+
+impl<'i> Default for Image<'i> {
+  fn default() -> Image<'i> {
+    Image::None
+  }
+}
+
+impl<'i> Image<'i> {
+  /// Returns whether the image includes any vendor prefixed values.
+  pub fn has_vendor_prefix(&self) -> bool {
+    let prefix = self.get_vendor_prefix();
+    !prefix.is_empty() && prefix != VendorPrefix::None
+  }
+
+  /// Returns the vendor prefix used in the image value.
+  pub fn get_vendor_prefix(&self) -> VendorPrefix {
+    match self {
+      Image::Gradient(a) => a.get_vendor_prefix(),
+      Image::ImageSet(a) => a.get_vendor_prefix(),
+      _ => VendorPrefix::empty(),
+    }
+  }
+
+  /// Returns the vendor prefixes that are needed for the given browser targets.
+  pub fn get_necessary_prefixes(&self, targets: Targets) -> VendorPrefix {
+    match self {
+      Image::Gradient(grad) => grad.get_necessary_prefixes(targets),
+      Image::ImageSet(image_set) => image_set.get_necessary_prefixes(targets),
+      _ => VendorPrefix::None,
+    }
+  }
+
+  /// Returns a vendor prefixed version of the image for the given vendor prefixes.
+  pub fn get_prefixed(&self, prefix: VendorPrefix) -> Image<'i> {
+    match self {
+      Image::Gradient(grad) => Image::Gradient(Box::new(grad.get_prefixed(prefix))),
+      Image::ImageSet(image_set) => Image::ImageSet(image_set.get_prefixed(prefix)),
+      _ => self.clone(),
+    }
+  }
+
+  /// Returns a legacy `-webkit-gradient()` value for the image.
+  ///
+  /// May return an error in case the gradient cannot be converted.
+  pub fn get_legacy_webkit(&self) -> Result<Image<'i>, ()> {
+    match self {
+      Image::Gradient(grad) => Ok(Image::Gradient(Box::new(grad.get_legacy_webkit()?))),
+      _ => Ok(self.clone()),
+    }
+  }
+
+  /// Returns the color fallbacks that are needed for the given browser targets.
+  pub fn get_necessary_fallbacks(&self, targets: Targets) -> ColorFallbackKind {
+    match self {
+      Image::Gradient(grad) => grad.get_necessary_fallbacks(targets),
+      _ => ColorFallbackKind::empty(),
+    }
+  }
+
+  /// Returns a fallback version of the image for the given color fallback type.
+  pub fn get_fallback(&self, kind: ColorFallbackKind) -> Image<'i> {
+    match self {
+      Image::Gradient(grad) => Image::Gradient(Box::new(grad.get_fallback(kind))),
+      _ => self.clone(),
+    }
+  }
+}
+
+impl<'i> IsCompatible for Image<'i> {
+  fn is_compatible(&self, browsers: Browsers) -> bool {
+    match self {
+      Image::Gradient(g) => match &**g {
+        Gradient::Linear(g) => {
+          compat::Feature::LinearGradient.is_compatible(browsers) && g.is_compatible(browsers)
+        }
+        Gradient::RepeatingLinear(g) => {
+          compat::Feature::RepeatingLinearGradient.is_compatible(browsers) && g.is_compatible(browsers)
+        }
+        Gradient::Radial(g) => {
+          compat::Feature::RadialGradient.is_compatible(browsers) && g.is_compatible(browsers)
+        }
+        Gradient::RepeatingRadial(g) => {
+          compat::Feature::RepeatingRadialGradient.is_compatible(browsers) && g.is_compatible(browsers)
+        }
+        Gradient::Conic(g) => compat::Feature::ConicGradient.is_compatible(browsers) && g.is_compatible(browsers),
+        Gradient::RepeatingConic(g) => {
+          compat::Feature::RepeatingConicGradient.is_compatible(browsers) && g.is_compatible(browsers)
+        }
+        Gradient::WebKitGradient(..) => is_webkit_gradient(browsers),
+      },
+      Image::ImageSet(i) => i.is_compatible(browsers),
+      Image::Url(..) | Image::None => true,
+    }
+  }
+}
+
+pub(crate) trait ImageFallback<'i>: Sized {
+  fn get_image(&self) -> &Image<'i>;
+  fn with_image(&self, image: Image<'i>) -> Self;
+
+  #[inline]
+  fn get_necessary_fallbacks(&self, targets: Targets) -> ColorFallbackKind {
+    self.get_image().get_necessary_fallbacks(targets)
+  }
+
+  #[inline]
+  fn get_fallback(&self, kind: ColorFallbackKind) -> Self {
+    self.with_image(self.get_image().get_fallback(kind))
+  }
+}
+
+impl<'i> ImageFallback<'i> for Image<'i> {
+  #[inline]
+  fn get_image(&self) -> &Image<'i> {
+    self
+  }
+
+  #[inline]
+  fn with_image(&self, image: Image<'i>) -> Self {
+    image
+  }
+}
+
+impl<'i> FallbackValues for Image<'i> {
+  fn get_fallbacks(&mut self, targets: Targets) -> Vec<Self> {
+    // Determine which prefixes and color fallbacks are needed.
+    let prefixes = self.get_necessary_prefixes(targets);
+    let fallbacks = self.get_necessary_fallbacks(targets);
+    let mut res = Vec::new();
+
+    // Get RGB fallbacks if needed.
+    let rgb = if fallbacks.contains(ColorFallbackKind::RGB) {
+      Some(self.get_fallback(ColorFallbackKind::RGB))
+    } else {
+      None
+    };
+
+    // Prefixed properties only support RGB.
+    let prefix_image = rgb.as_ref().unwrap_or(self);
+
+    // Legacy -webkit-gradient()
+    if prefixes.contains(VendorPrefix::WebKit)
+      && targets.browsers.map(is_webkit_gradient).unwrap_or(false)
+      && matches!(prefix_image, Image::Gradient(_))
+    {
+      if let Ok(legacy) = prefix_image.get_legacy_webkit() {
+        res.push(legacy);
+      }
+    }
+
+    // Standard syntax, with prefixes.
+    if prefixes.contains(VendorPrefix::WebKit) {
+      res.push(prefix_image.get_prefixed(VendorPrefix::WebKit))
+    }
+
+    if prefixes.contains(VendorPrefix::Moz) {
+      res.push(prefix_image.get_prefixed(VendorPrefix::Moz))
+    }
+
+    if prefixes.contains(VendorPrefix::O) {
+      res.push(prefix_image.get_prefixed(VendorPrefix::O))
+    }
+
+    if prefixes.contains(VendorPrefix::None) {
+      // Unprefixed, rgb fallback.
+      if let Some(rgb) = rgb {
+        res.push(rgb);
+      }
+
+      // P3 fallback.
+      if fallbacks.contains(ColorFallbackKind::P3) {
+        res.push(self.get_fallback(ColorFallbackKind::P3));
+      }
+
+      // Convert original to lab if needed (e.g. if oklab is not supported but lab is).
+      if fallbacks.contains(ColorFallbackKind::LAB) {
+        *self = self.get_fallback(ColorFallbackKind::LAB);
+      }
+    } else if let Some(last) = res.pop() {
+      // Prefixed property with no unprefixed version.
+      // Replace self with the last prefixed version so that it doesn't
+      // get duplicated when the caller pushes the original value.
+      *self = last;
+    }
+
+    res
+  }
+}
+
+impl<'i, T: ImageFallback<'i>> FallbackValues for SmallVec<[T; 1]> {
+  fn get_fallbacks(&mut self, targets: Targets) -> Vec<Self> {
+    // Determine what vendor prefixes and color fallbacks are needed.
+    let mut prefixes = VendorPrefix::empty();
+    let mut fallbacks = ColorFallbackKind::empty();
+    let mut res = Vec::new();
+    for item in self.iter() {
+      prefixes |= item.get_image().get_necessary_prefixes(targets);
+      fallbacks |= item.get_necessary_fallbacks(targets);
+    }
+
+    // Get RGB fallbacks if needed.
+    let rgb: Option<SmallVec<[T; 1]>> = if fallbacks.contains(ColorFallbackKind::RGB) {
+      Some(self.iter().map(|item| item.get_fallback(ColorFallbackKind::RGB)).collect())
+    } else {
+      None
+    };
+
+    // Prefixed properties only support RGB.
+    let prefix_images = rgb.as_ref().unwrap_or(&self);
+
+    // Legacy -webkit-gradient()
+    if prefixes.contains(VendorPrefix::WebKit) && targets.browsers.map(is_webkit_gradient).unwrap_or(false) {
+      let images: SmallVec<[T; 1]> = prefix_images
+        .iter()
+        .map(|item| item.get_image().get_legacy_webkit().map(|image| item.with_image(image)))
+        .flatten()
+        .collect();
+      if !images.is_empty() {
+        res.push(images)
+      }
+    }
+
+    // Standard syntax, with prefixes.
+    macro_rules! prefix {
+      ($prefix: ident) => {
+        if prefixes.contains(VendorPrefix::$prefix) {
+          let images = prefix_images
+            .iter()
+            .map(|item| {
+              let image = item.get_image().get_prefixed(VendorPrefix::$prefix);
+              item.with_image(image)
+            })
+            .collect();
+          res.push(images)
+        }
+      };
+    }
+
+    prefix!(WebKit);
+    prefix!(Moz);
+    prefix!(O);
+    if prefixes.contains(VendorPrefix::None) {
+      if let Some(rgb) = rgb {
+        res.push(rgb);
+      }
+
+      if fallbacks.contains(ColorFallbackKind::P3) {
+        let p3_images = self.iter().map(|item| item.get_fallback(ColorFallbackKind::P3)).collect();
+
+        res.push(p3_images)
+      }
+
+      // Convert to lab if needed (e.g. if oklab is not supported but lab is).
+      if fallbacks.contains(ColorFallbackKind::LAB) {
+        for item in self.iter_mut() {
+          *item = item.get_fallback(ColorFallbackKind::LAB);
+        }
+      }
+    } else if let Some(last) = res.pop() {
+      // Prefixed property with no unprefixed version.
+      // Replace self with the last prefixed version so that it doesn't
+      // get duplicated when the caller pushes the original value.
+      *self = last;
+    }
+
+    res
+  }
+}
+
+/// A CSS [`image-set()`](https://drafts.csswg.org/css-images-4/#image-set-notation) value.
+///
+/// `image-set()` allows the user agent to choose between multiple versions of an image to
+/// display the most appropriate resolution or file type that it supports.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(rename_all = "camelCase")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub struct ImageSet<'i> {
+  /// The image options to choose from.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  pub options: Vec<ImageSetOption<'i>>,
+  /// The vendor prefix for the `image-set()` function.
+  pub vendor_prefix: VendorPrefix,
+}
+
+impl<'i> ImageSet<'i> {
+  /// Returns the vendor prefix for the `image-set()`.
+  pub fn get_vendor_prefix(&self) -> VendorPrefix {
+    self.vendor_prefix
+  }
+
+  /// Returns the vendor prefixes needed for the given browser targets.
+  pub fn get_necessary_prefixes(&self, targets: Targets) -> VendorPrefix {
+    targets.prefixes(self.vendor_prefix, Feature::ImageSet)
+  }
+
+  /// Returns the `image-set()` value with the given vendor prefix.
+  pub fn get_prefixed(&self, prefix: VendorPrefix) -> ImageSet<'i> {
+    ImageSet {
+      options: self.options.clone(),
+      vendor_prefix: prefix,
+    }
+  }
+}
+
+impl<'i> Parse<'i> for ImageSet<'i> {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let location = input.current_source_location();
+    let f = input.expect_function()?;
+    let vendor_prefix = match_ignore_ascii_case! { &*f,
+      "image-set" => VendorPrefix::None,
+      "-webkit-image-set" => VendorPrefix::WebKit,
+      _ => return Err(location.new_unexpected_token_error(
+        cssparser::Token::Ident(f.clone())
+      ))
+    };
+
+    let options = input.parse_nested_block(|input| input.parse_comma_separated(ImageSetOption::parse))?;
+    Ok(ImageSet { options, vendor_prefix })
+  }
+}
+
+impl<'i> ToCss for ImageSet<'i> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    self.vendor_prefix.to_css(dest)?;
+    dest.write_str("image-set(")?;
+    let mut first = true;
+    for option in &self.options {
+      if first {
+        first = false;
+      } else {
+        dest.delim(',', false)?;
+      }
+      option.to_css(dest, self.vendor_prefix != VendorPrefix::None)?;
+    }
+    dest.write_char(')')
+  }
+}
+
+impl<'i> IsCompatible for ImageSet<'i> {
+  fn is_compatible(&self, browsers: Browsers) -> bool {
+    compat::Feature::ImageSet.is_compatible(browsers)
+      && self.options.iter().all(|opt| opt.image.is_compatible(browsers))
+  }
+}
+
+/// An image option within the `image-set()` function. See [ImageSet](ImageSet).
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(rename_all = "camelCase")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub struct ImageSetOption<'i> {
+  /// The image for this option.
+  #[cfg_attr(feature = "visitor", skip_type)]
+  pub image: Image<'i>,
+  /// The resolution of the image.
+  pub resolution: Resolution,
+  /// The mime type of the image.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  pub file_type: Option<CowArcStr<'i>>,
+}
+
+impl<'i> Parse<'i> for ImageSetOption<'i> {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let loc = input.current_source_location();
+    let image = if let Ok(url) = input.try_parse(|input| input.expect_url_or_string()) {
+      Image::Url(Url {
+        url: url.into(),
+        loc: loc.into(),
+      })
+    } else {
+      Image::parse(input)?
+    };
+
+    let (resolution, file_type) = if let Ok(res) = input.try_parse(Resolution::parse) {
+      let file_type = input.try_parse(parse_file_type).ok();
+      (res, file_type)
+    } else {
+      let file_type = input.try_parse(parse_file_type).ok();
+      let resolution = input.try_parse(Resolution::parse).unwrap_or(Resolution::Dppx(1.0));
+      (resolution, file_type)
+    };
+
+    Ok(ImageSetOption {
+      image,
+      resolution,
+      file_type: file_type.map(|x| x.into()),
+    })
+  }
+}
+
+impl<'i> ImageSetOption<'i> {
+  fn to_css<W>(&self, dest: &mut Printer<W>, is_prefixed: bool) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    match &self.image {
+      // Prefixed syntax didn't allow strings, only url()
+      Image::Url(url) if !is_prefixed => {
+        // Add dependency if needed. Normally this is handled by the Url type.
+        let dep = if dest.dependencies.is_some() {
+          Some(UrlDependency::new(url, dest.filename()))
+        } else {
+          None
+        };
+        if let Some(dep) = dep {
+          serialize_string(&dep.placeholder, dest)?;
+          if let Some(dependencies) = &mut dest.dependencies {
+            dependencies.push(Dependency::Url(dep))
+          }
+        } else {
+          serialize_string(&url.url, dest)?;
+        }
+      }
+      _ => self.image.to_css(dest)?,
+    }
+
+    // TODO: Throwing an error when `self.resolution = Resolution::Dppx(0.0)`
+    // TODO: -webkit-image-set() does not support `<image()> | <image-set()> |
+    // <cross-fade()> | <element()> | <gradient>` and `type(<string>)`.
+    dest.write_char(' ')?;
+
+    // Safari only supports the x resolution unit in image-set().
+    // In other places, x was added as an alias later.
+    // Temporarily ignore the targets while printing here.
+    let targets = std::mem::take(&mut dest.targets.current);
+    self.resolution.to_css(dest)?;
+    dest.targets.current = targets;
+
+    if let Some(file_type) = &self.file_type {
+      dest.write_str(" type(")?;
+      serialize_string(&file_type, dest)?;
+      dest.write_char(')')?;
+    }
+
+    Ok(())
+  }
+}
+
+fn parse_file_type<'i, 't>(input: &mut Parser<'i, 't>) -> Result<CowRcStr<'i>, ParseError<'i, ParserError<'i>>> {
+  input.expect_function_matching("type")?;
+  input.parse_nested_block(|input| Ok(input.expect_string_cloned()?))
+}
diff --git a/src/values/length.rs b/src/values/length.rs
new file mode 100644
index 0000000..2ce01b0
--- /dev/null
+++ b/src/values/length.rs
@@ -0,0 +1,848 @@
+//! CSS length values.
+
+use super::angle::impl_try_from_angle;
+use super::calc::{Calc, MathFunction};
+use super::number::CSSNumber;
+use super::percentage::DimensionPercentage;
+use crate::error::{ParserError, PrinterError};
+use crate::printer::Printer;
+use crate::targets::Browsers;
+use crate::traits::{
+  private::{AddInternal, TryAdd},
+  Map, Parse, Sign, ToCss, TryMap, TryOp, Zero,
+};
+use crate::traits::{IsCompatible, TrySign};
+#[cfg(feature = "visitor")]
+use crate::visitor::Visit;
+use const_str;
+use cssparser::*;
+
+/// A CSS [`<length-percentage>`](https://www.w3.org/TR/css-values-4/#typedef-length-percentage) value.
+/// May be specified as either a length or a percentage that resolves to an length.
+pub type LengthPercentage = DimensionPercentage<LengthValue>;
+
+impl LengthPercentage {
+  /// Constructs a `LengthPercentage` with the given pixel value.
+  pub fn px(val: CSSNumber) -> LengthPercentage {
+    LengthPercentage::Dimension(LengthValue::Px(val))
+  }
+
+  pub(crate) fn to_css_unitless<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    match self {
+      DimensionPercentage::Dimension(d) => d.to_css_unitless(dest),
+      _ => self.to_css(dest),
+    }
+  }
+}
+
+impl IsCompatible for LengthPercentage {
+  fn is_compatible(&self, browsers: Browsers) -> bool {
+    match self {
+      LengthPercentage::Dimension(d) => d.is_compatible(browsers),
+      LengthPercentage::Calc(c) => c.is_compatible(browsers),
+      LengthPercentage::Percentage(..) => true,
+    }
+  }
+}
+
+/// Either a [`<length-percentage>`](https://www.w3.org/TR/css-values-4/#typedef-length-percentage), or the `auto` keyword.
+#[derive(Debug, Clone, PartialEq, Parse, ToCss)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", content = "value", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum LengthPercentageOrAuto {
+  /// The `auto` keyword.
+  Auto,
+  /// A [`<length-percentage>`](https://www.w3.org/TR/css-values-4/#typedef-length-percentage).
+  LengthPercentage(LengthPercentage),
+}
+
+impl IsCompatible for LengthPercentageOrAuto {
+  fn is_compatible(&self, browsers: Browsers) -> bool {
+    match self {
+      LengthPercentageOrAuto::LengthPercentage(p) => p.is_compatible(browsers),
+      _ => true,
+    }
+  }
+}
+
+const PX_PER_IN: f32 = 96.0;
+const PX_PER_CM: f32 = PX_PER_IN / 2.54;
+const PX_PER_MM: f32 = PX_PER_CM / 10.0;
+const PX_PER_Q: f32 = PX_PER_CM / 40.0;
+const PX_PER_PT: f32 = PX_PER_IN / 72.0;
+const PX_PER_PC: f32 = PX_PER_IN / 6.0;
+
+macro_rules! define_length_units {
+  (
+    $(
+      $(#[$meta: meta])*
+      $name: ident $(/ $feature: ident)?,
+    )+
+  ) => {
+    /// A CSS [`<length>`](https://www.w3.org/TR/css-values-4/#lengths) value,
+    /// without support for `calc()`. See also: [Length](Length).
+    #[derive(Debug, Clone, PartialEq)]
+    #[cfg_attr(feature = "visitor", derive(Visit))]
+    #[cfg_attr(feature = "visitor", visit(visit_length, LENGTHS))]
+    #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(tag = "unit", content = "value", rename_all = "kebab-case"))]
+    #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+    pub enum LengthValue {
+      $(
+        $(#[$meta])*
+        $name(CSSNumber),
+      )+
+    }
+
+    impl<'i> Parse<'i> for LengthValue {
+      fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+        let location = input.current_source_location();
+        let token = input.next()?;
+        match *token {
+          Token::Dimension { value, ref unit, .. } => {
+            Ok(match unit {
+              $(
+                s if s.eq_ignore_ascii_case(stringify!($name)) => LengthValue::$name(value),
+              )+
+              _ => return Err(location.new_unexpected_token_error(token.clone())),
+            })
+          },
+          Token::Number { value, .. } => {
+            // TODO: quirks mode only?
+            Ok(LengthValue::Px(value))
+          }
+          ref token => return Err(location.new_unexpected_token_error(token.clone())),
+        }
+      }
+    }
+
+    impl<'i> TryFrom<&Token<'i>> for LengthValue {
+      type Error = ();
+
+      fn try_from(token: &Token) -> Result<Self, Self::Error> {
+        match token {
+          Token::Dimension { value, ref unit, .. } => {
+            Ok(match unit {
+              $(
+                s if s.eq_ignore_ascii_case(stringify!($name)) => LengthValue::$name(*value),
+              )+
+              _ => return Err(()),
+            })
+          },
+          _ => Err(())
+        }
+      }
+    }
+
+    impl LengthValue {
+      /// Returns the numeric value and unit string for the length value.
+      pub fn to_unit_value(&self) -> (CSSNumber, &str) {
+        match self {
+          $(
+            LengthValue::$name(value) => (*value, const_str::convert_ascii_case!(lower, stringify!($name))),
+          )+
+        }
+      }
+    }
+
+    impl IsCompatible for LengthValue {
+      fn is_compatible(&self, browsers: Browsers) -> bool {
+        macro_rules! is_compatible {
+          ($f: ident) => {
+            crate::compat::Feature::$f.is_compatible(browsers)
+          };
+          () => {
+            true
+          };
+        }
+
+        match self {
+          $(
+            LengthValue::$name(_) => {
+              is_compatible!($($feature)?)
+            }
+          )+
+        }
+      }
+    }
+
+    impl TryAdd<LengthValue> for LengthValue {
+      fn try_add(&self, other: &LengthValue) -> Option<LengthValue> {
+        use LengthValue::*;
+        match (self, other) {
+          $(
+            ($name(a), $name(b)) => Some($name(a + b)),
+          )+
+          (a, b) => {
+            if let (Some(a), Some(b)) = (a.to_px(), b.to_px()) {
+              Some(Px(a + b))
+            } else {
+              None
+            }
+          }
+        }
+      }
+    }
+
+    impl std::ops::Mul<CSSNumber> for LengthValue {
+      type Output = Self;
+
+      fn mul(self, other: CSSNumber) -> LengthValue {
+        use LengthValue::*;
+        match self {
+          $(
+            $name(value) => $name(value * other),
+          )+
+        }
+      }
+    }
+
+    impl std::cmp::PartialOrd<LengthValue> for LengthValue {
+      fn partial_cmp(&self, other: &LengthValue) -> Option<std::cmp::Ordering> {
+        use LengthValue::*;
+        match (self, other) {
+          $(
+            ($name(a), $name(b)) => a.partial_cmp(b),
+          )+
+          (a, b) => {
+            if let (Some(a), Some(b)) = (a.to_px(), b.to_px()) {
+              a.partial_cmp(&b)
+            } else {
+              None
+            }
+          }
+        }
+      }
+    }
+
+    impl TryOp for LengthValue {
+      fn try_op<F: FnOnce(f32, f32) -> f32>(&self, rhs: &Self, op: F) -> Option<Self> {
+        use LengthValue::*;
+        match (self, rhs) {
+          $(
+            ($name(a), $name(b)) => Some($name(op(*a, *b))),
+          )+
+          (a, b) => {
+            if let (Some(a), Some(b)) = (a.to_px(), b.to_px()) {
+              Some(Px(op(a, b)))
+            } else {
+              None
+            }
+          }
+        }
+      }
+
+      fn try_op_to<T, F: FnOnce(f32, f32) -> T>(&self, rhs: &Self, op: F) -> Option<T> {
+        use LengthValue::*;
+        match (self, rhs) {
+          $(
+            ($name(a), $name(b)) => Some(op(*a, *b)),
+          )+
+          (a, b) => {
+            if let (Some(a), Some(b)) = (a.to_px(), b.to_px()) {
+              Some(op(a, b))
+            } else {
+              None
+            }
+          }
+        }
+      }
+    }
+
+    impl Map for LengthValue {
+      fn map<F: FnOnce(f32) -> f32>(&self, op: F) -> Self {
+        use LengthValue::*;
+        match self {
+          $(
+            $name(value) => $name(op(*value)),
+          )+
+        }
+      }
+    }
+
+    impl Sign for LengthValue {
+      fn sign(&self) -> f32 {
+        use LengthValue::*;
+        match self {
+          $(
+            $name(value) => value.sign(),
+          )+
+        }
+      }
+    }
+
+    impl Zero for LengthValue {
+      fn zero() -> Self {
+        LengthValue::Px(0.0)
+      }
+
+      fn is_zero(&self) -> bool {
+        use LengthValue::*;
+        match self {
+          $(
+            $name(value) => value.is_zero(),
+          )+
+        }
+      }
+    }
+
+    impl_try_from_angle!(LengthValue);
+
+    #[cfg(feature = "jsonschema")]
+    #[cfg_attr(docsrs, doc(cfg(feature = "jsonschema")))]
+    impl schemars::JsonSchema for LengthValue {
+      fn is_referenceable() -> bool {
+        true
+      }
+
+      fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
+        #[derive(schemars::JsonSchema)]
+        #[schemars(rename_all = "lowercase")]
+        #[allow(dead_code)]
+        enum LengthUnit {
+          $(
+            $(#[$meta])*
+            $name,
+          )+
+        }
+
+        #[derive(schemars::JsonSchema)]
+        #[allow(dead_code)]
+        struct LengthValue {
+          /// The length unit.
+          unit: LengthUnit,
+          /// The length value.
+          value: CSSNumber
+        }
+
+        LengthValue::json_schema(gen)
+      }
+
+      fn schema_name() -> String {
+        "LengthValue".into()
+      }
+    }
+  };
+}
+
+define_length_units! {
+  // https://www.w3.org/TR/css-values-4/#absolute-lengths
+  /// A length in pixels.
+  Px,
+  /// A length in inches. 1in = 96px.
+  In,
+  /// A length in centimeters. 1cm = 96px / 2.54.
+  Cm,
+  /// A length in millimeters. 1mm = 1/10th of 1cm.
+  Mm,
+  /// A length in quarter-millimeters. 1Q = 1/40th of 1cm.
+  Q / QUnit,
+  /// A length in points. 1pt = 1/72nd of 1in.
+  Pt,
+  /// A length in picas. 1pc = 1/6th of 1in.
+  Pc,
+
+  // https://www.w3.org/TR/css-values-4/#font-relative-lengths
+  /// A length in the `em` unit. An `em` is equal to the computed value of the
+  /// font-size property of the element on which it is used.
+  Em,
+  /// A length in the `rem` unit. A `rem` is equal to the computed value of the
+  /// `em` unit on the root element.
+  Rem / RemUnit,
+  /// A length in `ex` unit. An `ex` is equal to the x-height of the font.
+  Ex / ExUnit,
+  /// A length in the `rex` unit. A `rex` is equal to the value of the `ex` unit on the root element.
+  Rex,
+  /// A length in the `ch` unit. A `ch` is equal to the width of the zero ("0") character in the current font.
+  Ch / ChUnit,
+  /// A length in the `rch` unit. An `rch` is equal to the value of the `ch` unit on the root element.
+  Rch,
+  /// A length in the `cap` unit. A `cap` is equal to the cap-height of the font.
+  Cap / CapUnit,
+  /// A length in the `rcap` unit. An `rcap` is equal to the value of the `cap` unit on the root element.
+  Rcap,
+  /// A length in the `ic` unit. An `ic` is equal to the width of the “水” (CJK water ideograph) character in the current font.
+  Ic / IcUnit,
+  /// A length in the `ric` unit. An `ric` is equal to the value of the `ic` unit on the root element.
+  Ric,
+  /// A length in the `lh` unit. An `lh` is equal to the computed value of the `line-height` property.
+  Lh / LhUnit,
+  /// A length in the `rlh` unit. An `rlh` is equal to the value of the `lh` unit on the root element.
+  Rlh / RlhUnit,
+
+  // https://www.w3.org/TR/css-values-4/#viewport-relative-units
+  /// A length in the `vw` unit. A `vw` is equal to 1% of the [viewport width](https://www.w3.org/TR/css-values-4/#ua-default-viewport-size).
+  Vw / VwUnit,
+  /// A length in the `lvw` unit. An `lvw` is equal to 1% of the [large viewport width](https://www.w3.org/TR/css-values-4/#large-viewport-size).
+  Lvw / ViewportPercentageUnitsLarge,
+  /// A length in the `svw` unit. An `svw` is equal to 1% of the [small viewport width](https://www.w3.org/TR/css-values-4/#small-viewport-size).
+  Svw / ViewportPercentageUnitsSmall,
+  /// A length in the `dvw` unit. An `dvw` is equal to 1% of the [dynamic viewport width](https://www.w3.org/TR/css-values-4/#dynamic-viewport-size).
+  Dvw / ViewportPercentageUnitsDynamic,
+  /// A length in the `cqw` unit. An `cqw` is equal to 1% of the [query container](https://drafts.csswg.org/css-contain-3/#query-container) width.
+  Cqw / ContainerQueryLengthUnits,
+
+  /// A length in the `vh` unit. A `vh` is equal to 1% of the [viewport height](https://www.w3.org/TR/css-values-4/#ua-default-viewport-size).
+  Vh / VhUnit,
+  /// A length in the `lvh` unit. An `lvh` is equal to 1% of the [large viewport height](https://www.w3.org/TR/css-values-4/#large-viewport-size).
+  Lvh / ViewportPercentageUnitsLarge,
+  /// A length in the `svh` unit. An `svh` is equal to 1% of the [small viewport height](https://www.w3.org/TR/css-values-4/#small-viewport-size).
+  Svh / ViewportPercentageUnitsSmall,
+  /// A length in the `dvh` unit. An `dvh` is equal to 1% of the [dynamic viewport height](https://www.w3.org/TR/css-values-4/#dynamic-viewport-size).
+  Dvh / ViewportPercentageUnitsDynamic,
+  /// A length in the `cqh` unit. An `cqh` is equal to 1% of the [query container](https://drafts.csswg.org/css-contain-3/#query-container) height.
+  Cqh / ContainerQueryLengthUnits,
+
+  /// A length in the `vi` unit. A `vi` is equal to 1% of the [viewport size](https://www.w3.org/TR/css-values-4/#ua-default-viewport-size)
+  /// in the box's [inline axis](https://www.w3.org/TR/css-writing-modes-4/#inline-axis).
+  Vi / ViUnit,
+  /// A length in the `svi` unit. A `svi` is equal to 1% of the [small viewport size](https://www.w3.org/TR/css-values-4/#small-viewport-size)
+  /// in the box's [inline axis](https://www.w3.org/TR/css-writing-modes-4/#inline-axis).
+  Svi / ViewportPercentageUnitsSmall,
+  /// A length in the `lvi` unit. A `lvi` is equal to 1% of the [large viewport size](https://www.w3.org/TR/css-values-4/#large-viewport-size)
+  /// in the box's [inline axis](https://www.w3.org/TR/css-writing-modes-4/#inline-axis).
+  Lvi / ViewportPercentageUnitsLarge,
+  /// A length in the `dvi` unit. A `dvi` is equal to 1% of the [dynamic viewport size](https://www.w3.org/TR/css-values-4/#dynamic-viewport-size)
+  /// in the box's [inline axis](https://www.w3.org/TR/css-writing-modes-4/#inline-axis).
+  Dvi / ViewportPercentageUnitsDynamic,
+  /// A length in the `cqi` unit. An `cqi` is equal to 1% of the [query container](https://drafts.csswg.org/css-contain-3/#query-container) inline size.
+  Cqi / ContainerQueryLengthUnits,
+
+  /// A length in the `vb` unit. A `vb` is equal to 1% of the [viewport size](https://www.w3.org/TR/css-values-4/#ua-default-viewport-size)
+  /// in the box's [block axis](https://www.w3.org/TR/css-writing-modes-4/#block-axis).
+  Vb / VbUnit,
+  /// A length in the `svb` unit. A `svb` is equal to 1% of the [small viewport size](https://www.w3.org/TR/css-values-4/#small-viewport-size)
+  /// in the box's [block axis](https://www.w3.org/TR/css-writing-modes-4/#block-axis).
+  Svb / ViewportPercentageUnitsSmall,
+  /// A length in the `lvb` unit. A `lvb` is equal to 1% of the [large viewport size](https://www.w3.org/TR/css-values-4/#large-viewport-size)
+  /// in the box's [block axis](https://www.w3.org/TR/css-writing-modes-4/#block-axis).
+  Lvb / ViewportPercentageUnitsLarge,
+  /// A length in the `dvb` unit. A `dvb` is equal to 1% of the [dynamic viewport size](https://www.w3.org/TR/css-values-4/#dynamic-viewport-size)
+  /// in the box's [block axis](https://www.w3.org/TR/css-writing-modes-4/#block-axis).
+  Dvb / ViewportPercentageUnitsDynamic,
+  /// A length in the `cqb` unit. An `cqb` is equal to 1% of the [query container](https://drafts.csswg.org/css-contain-3/#query-container) block size.
+  Cqb / ContainerQueryLengthUnits,
+
+  /// A length in the `vmin` unit. A `vmin` is equal to the smaller of `vw` and `vh`.
+  Vmin / VminUnit,
+  /// A length in the `svmin` unit. An `svmin` is equal to the smaller of `svw` and `svh`.
+  Svmin / ViewportPercentageUnitsSmall,
+  /// A length in the `lvmin` unit. An `lvmin` is equal to the smaller of `lvw` and `lvh`.
+  Lvmin / ViewportPercentageUnitsLarge,
+  /// A length in the `dvmin` unit. A `dvmin` is equal to the smaller of `dvw` and `dvh`.
+  Dvmin / ViewportPercentageUnitsDynamic,
+  /// A length in the `cqmin` unit. An `cqmin` is equal to the smaller of `cqi` and `cqb`.
+  Cqmin / ContainerQueryLengthUnits,
+
+  /// A length in the `vmax` unit. A `vmax` is equal to the larger of `vw` and `vh`.
+  Vmax / VmaxUnit,
+  /// A length in the `svmax` unit. An `svmax` is equal to the larger of `svw` and `svh`.
+  Svmax / ViewportPercentageUnitsSmall,
+  /// A length in the `lvmax` unit. An `lvmax` is equal to the larger of `lvw` and `lvh`.
+  Lvmax / ViewportPercentageUnitsLarge,
+  /// A length in the `dvmax` unit. An `dvmax` is equal to the larger of `dvw` and `dvh`.
+  Dvmax / ViewportPercentageUnitsDynamic,
+  /// A length in the `cqmax` unit. An `cqmin` is equal to the larger of `cqi` and `cqb`.
+  Cqmax / ContainerQueryLengthUnits,
+}
+
+impl ToCss for LengthValue {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    let (value, unit) = self.to_unit_value();
+
+    // The unit can be omitted if the value is zero, except inside calc()
+    // expressions, where unitless numbers won't be parsed as dimensions.
+    if !dest.in_calc && value == 0.0 {
+      return dest.write_char('0');
+    }
+
+    serialize_dimension(value, unit, dest)
+  }
+}
+
+impl LengthValue {
+  pub(crate) fn to_css_unitless<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    match self {
+      LengthValue::Px(value) => value.to_css(dest),
+      _ => self.to_css(dest),
+    }
+  }
+}
+
+pub(crate) fn serialize_dimension<W>(value: f32, unit: &str, dest: &mut Printer<W>) -> Result<(), PrinterError>
+where
+  W: std::fmt::Write,
+{
+  use cssparser::ToCss;
+  let int_value = if value.fract() == 0.0 { Some(value as i32) } else { None };
+  let token = Token::Dimension {
+    has_sign: value < 0.0,
+    value,
+    int_value,
+    unit: CowRcStr::from(unit),
+  };
+  if value != 0.0 && value.abs() < 1.0 {
+    let mut s = String::new();
+    token.to_css(&mut s)?;
+    if value < 0.0 {
+      dest.write_char('-')?;
+      dest.write_str(s.trim_start_matches("-0"))
+    } else {
+      dest.write_str(s.trim_start_matches('0'))
+    }
+  } else {
+    token.to_css(dest)?;
+    Ok(())
+  }
+}
+
+impl LengthValue {
+  /// Attempts to convert the value to pixels.
+  /// Returns `None` if the conversion is not possible.
+  pub fn to_px(&self) -> Option<CSSNumber> {
+    use LengthValue::*;
+    match self {
+      Px(value) => Some(*value),
+      In(value) => Some(value * PX_PER_IN),
+      Cm(value) => Some(value * PX_PER_CM),
+      Mm(value) => Some(value * PX_PER_MM),
+      Q(value) => Some(value * PX_PER_Q),
+      Pt(value) => Some(value * PX_PER_PT),
+      Pc(value) => Some(value * PX_PER_PC),
+      _ => None,
+    }
+  }
+}
+
+/// A CSS [`<length>`](https://www.w3.org/TR/css-values-4/#lengths) value, with support for `calc()`.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", content = "value", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum Length {
+  /// An explicitly specified length value.
+  Value(LengthValue),
+  /// A computed length value using `calc()`.
+  #[cfg_attr(feature = "visitor", skip_type)]
+  Calc(Box<Calc<Length>>),
+}
+
+impl<'i> Parse<'i> for Length {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    match input.try_parse(Calc::parse) {
+      Ok(Calc::Value(v)) => return Ok(*v),
+      Ok(calc) => return Ok(Length::Calc(Box::new(calc))),
+      _ => {}
+    }
+
+    let len = LengthValue::parse(input)?;
+    Ok(Length::Value(len))
+  }
+}
+
+impl ToCss for Length {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    match self {
+      Length::Value(a) => a.to_css(dest),
+      Length::Calc(c) => c.to_css(dest),
+    }
+  }
+}
+
+impl std::ops::Mul<CSSNumber> for Length {
+  type Output = Self;
+
+  fn mul(self, other: CSSNumber) -> Length {
+    match self {
+      Length::Value(a) => Length::Value(a * other),
+      Length::Calc(a) => Length::Calc(Box::new(*a * other)),
+    }
+  }
+}
+
+impl std::ops::Add<Length> for Length {
+  type Output = Self;
+
+  fn add(self, other: Length) -> Length {
+    // Unwrap calc(...) functions so we can add inside.
+    // Then wrap the result in a calc(...) again if necessary.
+    let a = unwrap_calc(self);
+    let b = unwrap_calc(other);
+    let res = AddInternal::add(a, b);
+    match res {
+      Length::Calc(c) => match *c {
+        Calc::Value(l) => *l,
+        Calc::Function(f) if !matches!(*f, MathFunction::Calc(_)) => Length::Calc(Box::new(Calc::Function(f))),
+        c => Length::Calc(Box::new(Calc::Function(Box::new(MathFunction::Calc(c))))),
+      },
+      _ => res,
+    }
+  }
+}
+
+fn unwrap_calc(length: Length) -> Length {
+  match length {
+    Length::Calc(c) => match *c {
+      Calc::Function(f) => match *f {
+        MathFunction::Calc(c) => Length::Calc(Box::new(c)),
+        c => Length::Calc(Box::new(Calc::Function(Box::new(c)))),
+      },
+      _ => Length::Calc(c),
+    },
+    _ => length,
+  }
+}
+
+impl AddInternal for Length {
+  fn add(self, other: Self) -> Self {
+    match self.try_add(&other) {
+      Some(r) => r,
+      None => self.add(other),
+    }
+  }
+}
+
+impl Length {
+  /// Constructs a length with the given pixel value.
+  pub fn px(px: CSSNumber) -> Length {
+    Length::Value(LengthValue::Px(px))
+  }
+
+  /// Attempts to convert the length to pixels.
+  /// Returns `None` if the conversion is not possible.
+  pub fn to_px(&self) -> Option<CSSNumber> {
+    match self {
+      Length::Value(a) => a.to_px(),
+      _ => None,
+    }
+  }
+
+  fn add(self, other: Length) -> Length {
+    let mut a = self;
+    let mut b = other;
+
+    if a.is_zero() {
+      return b;
+    }
+
+    if b.is_zero() {
+      return a;
+    }
+
+    if a.is_sign_negative() && b.is_sign_positive() {
+      std::mem::swap(&mut a, &mut b);
+    }
+
+    match (a, b) {
+      (Length::Calc(a), Length::Calc(b)) => return Length::Calc(Box::new(a.add(*b).unwrap())),
+      (Length::Calc(calc), b) => {
+        if let Calc::Value(a) = *calc {
+          a.add(b)
+        } else {
+          Length::Calc(Box::new(Calc::Sum(Box::new((*calc).into()), Box::new(b.into()))))
+        }
+      }
+      (a, Length::Calc(calc)) => {
+        if let Calc::Value(b) = *calc {
+          a.add(*b)
+        } else {
+          Length::Calc(Box::new(Calc::Sum(Box::new(a.into()), Box::new((*calc).into()))))
+        }
+      }
+      (a, b) => Length::Calc(Box::new(Calc::Sum(Box::new(a.into()), Box::new(b.into())))),
+    }
+  }
+}
+
+impl IsCompatible for Length {
+  fn is_compatible(&self, browsers: Browsers) -> bool {
+    match self {
+      Length::Value(v) => v.is_compatible(browsers),
+      Length::Calc(calc) => calc.is_compatible(browsers),
+    }
+  }
+}
+
+impl Zero for Length {
+  fn zero() -> Length {
+    Length::Value(LengthValue::Px(0.0))
+  }
+
+  fn is_zero(&self) -> bool {
+    match self {
+      Length::Value(v) => v.is_zero(),
+      _ => false,
+    }
+  }
+}
+
+impl TryAdd<Length> for Length {
+  fn try_add(&self, other: &Length) -> Option<Length> {
+    match (self, other) {
+      (Length::Value(a), Length::Value(b)) => {
+        if let Some(res) = a.try_add(b) {
+          Some(Length::Value(res))
+        } else {
+          None
+        }
+      }
+      (Length::Calc(a), other) => match &**a {
+        Calc::Value(v) => v.try_add(other),
+        Calc::Sum(a, b) => {
+          if let Some(res) = Length::Calc(Box::new(*a.clone())).try_add(other) {
+            return Some(res.add(Length::from(*b.clone())));
+          }
+
+          if let Some(res) = Length::Calc(Box::new(*b.clone())).try_add(other) {
+            return Some(Length::from(*a.clone()).add(res));
+          }
+
+          None
+        }
+        _ => None,
+      },
+      (other, Length::Calc(b)) => match &**b {
+        Calc::Value(v) => other.try_add(&*v),
+        Calc::Sum(a, b) => {
+          if let Some(res) = other.try_add(&Length::Calc(Box::new(*a.clone()))) {
+            return Some(res.add(Length::from(*b.clone())));
+          }
+
+          if let Some(res) = other.try_add(&Length::Calc(Box::new(*b.clone()))) {
+            return Some(Length::from(*a.clone()).add(res));
+          }
+
+          None
+        }
+        _ => None,
+      },
+    }
+  }
+}
+
+impl std::convert::Into<Calc<Length>> for Length {
+  fn into(self) -> Calc<Length> {
+    match self {
+      Length::Calc(c) => *c,
+      b => Calc::Value(Box::new(b)),
+    }
+  }
+}
+
+impl std::convert::From<Calc<Length>> for Length {
+  fn from(calc: Calc<Length>) -> Length {
+    Length::Calc(Box::new(calc))
+  }
+}
+
+impl std::cmp::PartialOrd<Length> for Length {
+  fn partial_cmp(&self, other: &Length) -> Option<std::cmp::Ordering> {
+    match (self, other) {
+      (Length::Value(a), Length::Value(b)) => a.partial_cmp(b),
+      _ => None,
+    }
+  }
+}
+
+impl TryOp for Length {
+  fn try_op<F: FnOnce(f32, f32) -> f32>(&self, rhs: &Self, op: F) -> Option<Self> {
+    match (self, rhs) {
+      (Length::Value(a), Length::Value(b)) => a.try_op(b, op).map(Length::Value),
+      _ => None,
+    }
+  }
+
+  fn try_op_to<T, F: FnOnce(f32, f32) -> T>(&self, rhs: &Self, op: F) -> Option<T> {
+    match (self, rhs) {
+      (Length::Value(a), Length::Value(b)) => a.try_op_to(b, op),
+      _ => None,
+    }
+  }
+}
+
+impl TryMap for Length {
+  fn try_map<F: FnOnce(f32) -> f32>(&self, op: F) -> Option<Self> {
+    match self {
+      Length::Value(v) => v.try_map(op).map(Length::Value),
+      _ => None,
+    }
+  }
+}
+
+impl TrySign for Length {
+  fn try_sign(&self) -> Option<f32> {
+    match self {
+      Length::Value(v) => Some(v.sign()),
+      Length::Calc(c) => c.try_sign(),
+    }
+  }
+}
+
+impl_try_from_angle!(Length);
+
+/// Either a [`<length>`](https://www.w3.org/TR/css-values-4/#lengths) or a [`<number>`](https://www.w3.org/TR/css-values-4/#numbers).
+#[derive(Debug, Clone, PartialEq, Parse, ToCss)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", content = "value", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum LengthOrNumber {
+  /// A number.
+  Number(CSSNumber),
+  /// A length.
+  Length(Length),
+}
+
+impl Default for LengthOrNumber {
+  fn default() -> LengthOrNumber {
+    LengthOrNumber::Number(0.0)
+  }
+}
+
+impl Zero for LengthOrNumber {
+  fn zero() -> Self {
+    LengthOrNumber::Number(0.0)
+  }
+
+  fn is_zero(&self) -> bool {
+    match self {
+      LengthOrNumber::Length(l) => l.is_zero(),
+      LengthOrNumber::Number(v) => v.is_zero(),
+    }
+  }
+}
+
+impl IsCompatible for LengthOrNumber {
+  fn is_compatible(&self, browsers: Browsers) -> bool {
+    match self {
+      LengthOrNumber::Length(l) => l.is_compatible(browsers),
+      LengthOrNumber::Number(..) => true,
+    }
+  }
+}
diff --git a/src/values/mod.rs b/src/values/mod.rs
new file mode 100644
index 0000000..93c2af7
--- /dev/null
+++ b/src/values/mod.rs
@@ -0,0 +1,50 @@
+//! Common [CSS values](https://www.w3.org/TR/css3-values/) used across many properties.
+//!
+//! Each value provides parsing and serialization support using the [Parse](super::traits::Parse)
+//! and [ToCss](super::traits::ToCss) traits. In addition, many values support ways of manipulating
+//! them, including converting between representations and units, generating fallbacks for legacy
+//! browsers, minifying them, etc.
+//!
+//! # Example
+//!
+//! This example shows how you could parse a CSS color value, convert it to RGB, and re-serialize it.
+//! Similar patterns for parsing and serializing are possible across all value types.
+//!
+//! ```
+//! use lightningcss::{
+//!   traits::{Parse, ToCss},
+//!   values::color::CssColor,
+//!   printer::PrinterOptions
+//! };
+//!
+//! let color = CssColor::parse_string("lch(50% 75 0)").unwrap();
+//! let rgb = color.to_rgb().unwrap();
+//! assert_eq!(rgb.to_css_string(PrinterOptions::default()).unwrap(), "#e1157b");
+//! ```
+//!
+//! If you have a [cssparser::Parser](cssparser::Parser) already, you can also use the `parse` and `to_css`
+//! methods instead, rather than parsing from a string.
+
+#![deny(missing_docs)]
+
+pub mod alpha;
+pub mod angle;
+pub mod calc;
+pub mod color;
+pub mod easing;
+pub mod gradient;
+pub mod ident;
+pub mod image;
+pub mod length;
+pub mod number;
+pub mod percentage;
+pub mod position;
+pub mod ratio;
+pub mod rect;
+pub mod resolution;
+pub mod shape;
+pub mod size;
+pub mod string;
+pub mod syntax;
+pub mod time;
+pub mod url;
diff --git a/src/values/number.rs b/src/values/number.rs
new file mode 100644
index 0000000..60506ea
--- /dev/null
+++ b/src/values/number.rs
@@ -0,0 +1,142 @@
+//! CSS number values.
+
+use super::angle::impl_try_from_angle;
+use super::calc::Calc;
+use crate::error::{ParserError, PrinterError};
+use crate::printer::Printer;
+use crate::traits::private::AddInternal;
+use crate::traits::{Map, Op, Parse, Sign, ToCss, Zero};
+use cssparser::*;
+
+/// A CSS [`<number>`](https://www.w3.org/TR/css-values-4/#numbers) value.
+///
+/// Numbers may be explicit or computed by `calc()`, but are always stored and serialized
+/// as their computed value.
+pub type CSSNumber = f32;
+
+impl<'i> Parse<'i> for CSSNumber {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    match input.try_parse(Calc::parse) {
+      Ok(Calc::Value(v)) => return Ok(*v),
+      Ok(Calc::Number(n)) => return Ok(n),
+      // Numbers are always compatible, so they will always compute to a value.
+      Ok(_) => return Err(input.new_custom_error(ParserError::InvalidValue)),
+      _ => {}
+    }
+
+    let number = input.expect_number()?;
+    Ok(number)
+  }
+}
+
+impl ToCss for CSSNumber {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    let number = *self;
+    if number != 0.0 && number.abs() < 1.0 {
+      let mut s = String::new();
+      cssparser::ToCss::to_css(self, &mut s)?;
+      if number < 0.0 {
+        dest.write_char('-')?;
+        dest.write_str(s.trim_start_matches("-").trim_start_matches("0"))
+      } else {
+        dest.write_str(s.trim_start_matches('0'))
+      }
+    } else {
+      cssparser::ToCss::to_css(self, dest)?;
+      Ok(())
+    }
+  }
+}
+
+impl std::convert::Into<Calc<CSSNumber>> for CSSNumber {
+  fn into(self) -> Calc<CSSNumber> {
+    Calc::Value(Box::new(self))
+  }
+}
+
+impl std::convert::From<Calc<CSSNumber>> for CSSNumber {
+  fn from(calc: Calc<CSSNumber>) -> CSSNumber {
+    match calc {
+      Calc::Value(v) => *v,
+      Calc::Number(n) => n,
+      _ => unreachable!(),
+    }
+  }
+}
+
+impl AddInternal for CSSNumber {
+  fn add(self, other: Self) -> Self {
+    self + other
+  }
+}
+
+impl Op for CSSNumber {
+  fn op<F: FnOnce(f32, f32) -> f32>(&self, to: &Self, op: F) -> Self {
+    op(*self, *to)
+  }
+
+  fn op_to<T, F: FnOnce(f32, f32) -> T>(&self, rhs: &Self, op: F) -> T {
+    op(*self, *rhs)
+  }
+}
+
+impl Map for CSSNumber {
+  fn map<F: FnOnce(f32) -> f32>(&self, op: F) -> Self {
+    op(*self)
+  }
+}
+
+impl Sign for CSSNumber {
+  fn sign(&self) -> f32 {
+    if *self == 0.0 {
+      return if f32::is_sign_positive(*self) { 0.0 } else { -0.0 };
+    }
+    self.signum()
+  }
+}
+
+impl Zero for CSSNumber {
+  fn zero() -> Self {
+    0.0
+  }
+
+  fn is_zero(&self) -> bool {
+    *self == 0.0
+  }
+}
+
+impl_try_from_angle!(CSSNumber);
+
+/// A CSS [`<integer>`](https://www.w3.org/TR/css-values-4/#integers) value.
+pub type CSSInteger = i32;
+
+impl<'i> Parse<'i> for CSSInteger {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    // TODO: calc??
+    let integer = input.expect_integer()?;
+    Ok(integer)
+  }
+}
+
+impl ToCss for CSSInteger {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    cssparser::ToCss::to_css(self, dest)?;
+    Ok(())
+  }
+}
+
+impl Zero for CSSInteger {
+  fn zero() -> Self {
+    0
+  }
+
+  fn is_zero(&self) -> bool {
+    *self == 0
+  }
+}
diff --git a/src/values/percentage.rs b/src/values/percentage.rs
new file mode 100644
index 0000000..5e05945
--- /dev/null
+++ b/src/values/percentage.rs
@@ -0,0 +1,488 @@
+//! CSS percentage values.
+
+use super::angle::{impl_try_from_angle, Angle};
+use super::calc::{Calc, MathFunction};
+use super::number::CSSNumber;
+use crate::error::{ParserError, PrinterError};
+use crate::printer::Printer;
+use crate::traits::private::AddInternal;
+use crate::traits::{impl_op, private::TryAdd, Op, Parse, Sign, ToCss, TryMap, TryOp, TrySign, Zero};
+#[cfg(feature = "visitor")]
+use crate::visitor::Visit;
+use cssparser::*;
+
+/// A CSS [`<percentage>`](https://www.w3.org/TR/css-values-4/#percentages) value.
+///
+/// Percentages may be explicit or computed by `calc()`, but are always stored and serialized
+/// as their computed value.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(transparent))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub struct Percentage(pub CSSNumber);
+
+impl<'i> Parse<'i> for Percentage {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    match input.try_parse(Calc::parse) {
+      Ok(Calc::Value(v)) => return Ok(*v),
+      // Percentages are always compatible, so they will always compute to a value.
+      Ok(_) => unreachable!(),
+      _ => {}
+    }
+
+    let percent = input.expect_percentage()?;
+    Ok(Percentage(percent))
+  }
+}
+
+impl ToCss for Percentage {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    use cssparser::ToCss;
+    let int_value = if (self.0 * 100.0).fract() == 0.0 {
+      Some(self.0 as i32)
+    } else {
+      None
+    };
+    let percent = Token::Percentage {
+      has_sign: self.0 < 0.0,
+      unit_value: self.0,
+      int_value,
+    };
+    if self.0 != 0.0 && self.0.abs() < 0.01 {
+      let mut s = String::new();
+      percent.to_css(&mut s)?;
+      if self.0 < 0.0 {
+        dest.write_char('-')?;
+        dest.write_str(s.trim_start_matches("-0"))
+      } else {
+        dest.write_str(s.trim_start_matches('0'))
+      }
+    } else {
+      percent.to_css(dest)?;
+      Ok(())
+    }
+  }
+}
+
+impl std::convert::Into<Calc<Percentage>> for Percentage {
+  fn into(self) -> Calc<Percentage> {
+    Calc::Value(Box::new(self))
+  }
+}
+
+impl std::convert::TryFrom<Calc<Percentage>> for Percentage {
+  type Error = ();
+
+  fn try_from(calc: Calc<Percentage>) -> Result<Percentage, Self::Error> {
+    match calc {
+      Calc::Value(v) => Ok(*v),
+      _ => Err(()),
+    }
+  }
+}
+
+impl std::ops::Mul<CSSNumber> for Percentage {
+  type Output = Self;
+
+  fn mul(self, other: CSSNumber) -> Percentage {
+    Percentage(self.0 * other)
+  }
+}
+
+impl AddInternal for Percentage {
+  fn add(self, other: Self) -> Self {
+    self + other
+  }
+}
+
+impl std::cmp::PartialOrd<Percentage> for Percentage {
+  fn partial_cmp(&self, other: &Percentage) -> Option<std::cmp::Ordering> {
+    self.0.partial_cmp(&other.0)
+  }
+}
+
+impl Op for Percentage {
+  fn op<F: FnOnce(f32, f32) -> f32>(&self, to: &Self, op: F) -> Self {
+    Percentage(op(self.0, to.0))
+  }
+
+  fn op_to<T, F: FnOnce(f32, f32) -> T>(&self, rhs: &Self, op: F) -> T {
+    op(self.0, rhs.0)
+  }
+}
+
+impl TryMap for Percentage {
+  fn try_map<F: FnOnce(f32) -> f32>(&self, _: F) -> Option<Self> {
+    // Percentages cannot be mapped because we don't know what they will resolve to.
+    // For example, they might be positive or negative depending on what they are a
+    // percentage of, which we don't know.
+    None
+  }
+}
+
+impl Zero for Percentage {
+  fn zero() -> Self {
+    Percentage(0.0)
+  }
+
+  fn is_zero(&self) -> bool {
+    self.0.is_zero()
+  }
+}
+
+impl Sign for Percentage {
+  fn sign(&self) -> f32 {
+    self.0.sign()
+  }
+}
+
+impl_op!(Percentage, std::ops::Rem, rem);
+impl_op!(Percentage, std::ops::Add, add);
+
+impl_try_from_angle!(Percentage);
+
+/// Either a `<number>` or `<percentage>`.
+#[derive(Debug, Clone, PartialEq, Parse, ToCss)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", content = "value", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum NumberOrPercentage {
+  /// A number.
+  Number(CSSNumber),
+  /// A percentage.
+  Percentage(Percentage),
+}
+
+impl std::convert::Into<CSSNumber> for &NumberOrPercentage {
+  fn into(self) -> CSSNumber {
+    match self {
+      NumberOrPercentage::Number(a) => *a,
+      NumberOrPercentage::Percentage(a) => a.0,
+    }
+  }
+}
+
+/// A generic type that allows any kind of dimension and percentage to be
+/// used standalone or mixed within a `calc()` expression.
+///
+/// <https://drafts.csswg.org/css-values-4/#mixed-percentages>
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", content = "value", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum DimensionPercentage<D> {
+  /// An explicit dimension value.
+  Dimension(D),
+  /// A percentage.
+  Percentage(Percentage),
+  /// A `calc()` expression.
+  #[cfg_attr(feature = "visitor", skip_type)]
+  Calc(Box<Calc<DimensionPercentage<D>>>),
+}
+
+impl<
+    'i,
+    D: Parse<'i>
+      + std::ops::Mul<CSSNumber, Output = D>
+      + TryAdd<D>
+      + Clone
+      + TryOp
+      + TryMap
+      + Zero
+      + TrySign
+      + TryFrom<Angle>
+      + TryInto<Angle>
+      + PartialOrd<D>
+      + std::fmt::Debug,
+  > Parse<'i> for DimensionPercentage<D>
+{
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    match input.try_parse(Calc::parse) {
+      Ok(Calc::Value(v)) => return Ok(*v),
+      Ok(calc) => return Ok(DimensionPercentage::Calc(Box::new(calc))),
+      _ => {}
+    }
+
+    if let Ok(length) = input.try_parse(|input| D::parse(input)) {
+      return Ok(DimensionPercentage::Dimension(length));
+    }
+
+    if let Ok(percent) = input.try_parse(|input| Percentage::parse(input)) {
+      return Ok(DimensionPercentage::Percentage(percent));
+    }
+
+    Err(input.new_error_for_next_token())
+  }
+}
+
+impl<D: std::ops::Mul<CSSNumber, Output = D>> std::ops::Mul<CSSNumber> for DimensionPercentage<D> {
+  type Output = Self;
+
+  fn mul(self, other: CSSNumber) -> DimensionPercentage<D> {
+    match self {
+      DimensionPercentage::Dimension(l) => DimensionPercentage::Dimension(l * other),
+      DimensionPercentage::Percentage(p) => DimensionPercentage::Percentage(Percentage(p.0 * other)),
+      DimensionPercentage::Calc(c) => DimensionPercentage::Calc(Box::new(*c * other)),
+    }
+  }
+}
+
+impl<D: TryAdd<D> + Clone + Zero + TrySign + std::fmt::Debug> std::ops::Add<DimensionPercentage<D>>
+  for DimensionPercentage<D>
+{
+  type Output = DimensionPercentage<D>;
+
+  fn add(self, other: DimensionPercentage<D>) -> DimensionPercentage<D> {
+    // Unwrap calc(...) functions so we can add inside.
+    // Then wrap the result in a calc(...) again if necessary.
+    let a = unwrap_calc(self);
+    let b = unwrap_calc(other);
+    let res = AddInternal::add(a, b);
+    match res {
+      DimensionPercentage::Calc(c) => match *c {
+        Calc::Value(l) => *l,
+        Calc::Function(f) if !matches!(*f, MathFunction::Calc(_)) => {
+          DimensionPercentage::Calc(Box::new(Calc::Function(f)))
+        }
+        c => DimensionPercentage::Calc(Box::new(Calc::Function(Box::new(MathFunction::Calc(c))))),
+      },
+      _ => res,
+    }
+  }
+}
+
+fn unwrap_calc<D>(v: DimensionPercentage<D>) -> DimensionPercentage<D> {
+  match v {
+    DimensionPercentage::Calc(c) => match *c {
+      Calc::Function(f) => match *f {
+        MathFunction::Calc(c) => DimensionPercentage::Calc(Box::new(c)),
+        c => DimensionPercentage::Calc(Box::new(Calc::Function(Box::new(c)))),
+      },
+      _ => DimensionPercentage::Calc(c),
+    },
+    _ => v,
+  }
+}
+
+impl<D: TryAdd<D> + Clone + Zero + TrySign + std::fmt::Debug> AddInternal for DimensionPercentage<D> {
+  fn add(self, other: Self) -> Self {
+    match self.add_recursive(&other) {
+      Some(r) => r,
+      None => self.add(other),
+    }
+  }
+}
+
+impl<D: TryAdd<D> + Clone + Zero + TrySign + std::fmt::Debug> DimensionPercentage<D> {
+  fn add_recursive(&self, other: &DimensionPercentage<D>) -> Option<DimensionPercentage<D>> {
+    match (self, other) {
+      (DimensionPercentage::Dimension(a), DimensionPercentage::Dimension(b)) => {
+        if let Some(res) = a.try_add(b) {
+          Some(DimensionPercentage::Dimension(res))
+        } else {
+          None
+        }
+      }
+      (DimensionPercentage::Percentage(a), DimensionPercentage::Percentage(b)) => {
+        Some(DimensionPercentage::Percentage(Percentage(a.0 + b.0)))
+      }
+      (DimensionPercentage::Calc(a), other) => match &**a {
+        Calc::Value(v) => v.add_recursive(other),
+        Calc::Sum(a, b) => {
+          if let Some(res) = DimensionPercentage::Calc(Box::new(*a.clone())).add_recursive(other) {
+            return Some(res.add(DimensionPercentage::from(*b.clone())));
+          }
+
+          if let Some(res) = DimensionPercentage::Calc(Box::new(*b.clone())).add_recursive(other) {
+            return Some(DimensionPercentage::from(*a.clone()).add(res));
+          }
+
+          None
+        }
+        _ => None,
+      },
+      (other, DimensionPercentage::Calc(b)) => match &**b {
+        Calc::Value(v) => other.add_recursive(&*v),
+        Calc::Sum(a, b) => {
+          if let Some(res) = other.add_recursive(&DimensionPercentage::Calc(Box::new(*a.clone()))) {
+            return Some(res.add(DimensionPercentage::from(*b.clone())));
+          }
+
+          if let Some(res) = other.add_recursive(&DimensionPercentage::Calc(Box::new(*b.clone()))) {
+            return Some(DimensionPercentage::from(*a.clone()).add(res));
+          }
+
+          None
+        }
+        _ => None,
+      },
+      _ => None,
+    }
+  }
+
+  fn add(self, other: DimensionPercentage<D>) -> DimensionPercentage<D> {
+    let mut a = self;
+    let mut b = other;
+
+    if a.is_zero() {
+      return b;
+    }
+
+    if b.is_zero() {
+      return a;
+    }
+
+    if a.is_sign_negative() && b.is_sign_positive() {
+      std::mem::swap(&mut a, &mut b);
+    }
+
+    match (a, b) {
+      (DimensionPercentage::Calc(a), DimensionPercentage::Calc(b)) => {
+        DimensionPercentage::Calc(Box::new(a.add(*b).unwrap()))
+      }
+      (DimensionPercentage::Calc(calc), b) => {
+        if let Calc::Value(a) = *calc {
+          a.add(b)
+        } else {
+          DimensionPercentage::Calc(Box::new(Calc::Sum(Box::new((*calc).into()), Box::new(b.into()))))
+        }
+      }
+      (a, DimensionPercentage::Calc(calc)) => {
+        if let Calc::Value(b) = *calc {
+          a.add(*b)
+        } else {
+          DimensionPercentage::Calc(Box::new(Calc::Sum(Box::new(a.into()), Box::new((*calc).into()))))
+        }
+      }
+      (a, b) => DimensionPercentage::Calc(Box::new(Calc::Sum(Box::new(a.into()), Box::new(b.into())))),
+    }
+  }
+}
+
+impl<D> std::convert::Into<Calc<DimensionPercentage<D>>> for DimensionPercentage<D> {
+  fn into(self) -> Calc<DimensionPercentage<D>> {
+    match self {
+      DimensionPercentage::Calc(c) => *c,
+      b => Calc::Value(Box::new(b)),
+    }
+  }
+}
+
+impl<D> std::convert::From<Calc<DimensionPercentage<D>>> for DimensionPercentage<D> {
+  fn from(calc: Calc<DimensionPercentage<D>>) -> DimensionPercentage<D> {
+    DimensionPercentage::Calc(Box::new(calc))
+  }
+}
+
+impl<D: std::cmp::PartialOrd<D>> std::cmp::PartialOrd<DimensionPercentage<D>> for DimensionPercentage<D> {
+  fn partial_cmp(&self, other: &DimensionPercentage<D>) -> Option<std::cmp::Ordering> {
+    match (self, other) {
+      (DimensionPercentage::Dimension(a), DimensionPercentage::Dimension(b)) => a.partial_cmp(b),
+      (DimensionPercentage::Percentage(a), DimensionPercentage::Percentage(b)) => a.partial_cmp(b),
+      _ => None,
+    }
+  }
+}
+
+impl<D: TryOp> TryOp for DimensionPercentage<D> {
+  fn try_op<F: FnOnce(f32, f32) -> f32>(&self, rhs: &Self, op: F) -> Option<Self> {
+    match (self, rhs) {
+      (DimensionPercentage::Dimension(a), DimensionPercentage::Dimension(b)) => {
+        a.try_op(b, op).map(DimensionPercentage::Dimension)
+      }
+      (DimensionPercentage::Percentage(a), DimensionPercentage::Percentage(b)) => {
+        Some(DimensionPercentage::Percentage(Percentage(op(a.0, b.0))))
+      }
+      _ => None,
+    }
+  }
+
+  fn try_op_to<T, F: FnOnce(f32, f32) -> T>(&self, rhs: &Self, op: F) -> Option<T> {
+    match (self, rhs) {
+      (DimensionPercentage::Dimension(a), DimensionPercentage::Dimension(b)) => a.try_op_to(b, op),
+      (DimensionPercentage::Percentage(a), DimensionPercentage::Percentage(b)) => Some(op(a.0, b.0)),
+      _ => None,
+    }
+  }
+}
+
+impl<D: TryMap> TryMap for DimensionPercentage<D> {
+  fn try_map<F: FnOnce(f32) -> f32>(&self, op: F) -> Option<Self> {
+    match self {
+      DimensionPercentage::Dimension(v) => v.try_map(op).map(DimensionPercentage::Dimension),
+      _ => None,
+    }
+  }
+}
+
+impl<E, D: TryFrom<Angle, Error = E>> TryFrom<Angle> for DimensionPercentage<D> {
+  type Error = E;
+
+  fn try_from(value: Angle) -> Result<Self, Self::Error> {
+    Ok(DimensionPercentage::Dimension(D::try_from(value)?))
+  }
+}
+
+impl<E, D: TryInto<Angle, Error = E>> TryInto<Angle> for DimensionPercentage<D> {
+  type Error = ();
+
+  fn try_into(self) -> Result<Angle, Self::Error> {
+    match self {
+      DimensionPercentage::Dimension(d) => d.try_into().map_err(|_| ()),
+      _ => Err(()),
+    }
+  }
+}
+
+impl<D: Zero> Zero for DimensionPercentage<D> {
+  fn zero() -> Self {
+    DimensionPercentage::Dimension(D::zero())
+  }
+
+  fn is_zero(&self) -> bool {
+    match self {
+      DimensionPercentage::Dimension(d) => d.is_zero(),
+      DimensionPercentage::Percentage(p) => p.is_zero(),
+      _ => false,
+    }
+  }
+}
+
+impl<D: TrySign> TrySign for DimensionPercentage<D> {
+  fn try_sign(&self) -> Option<f32> {
+    match self {
+      DimensionPercentage::Dimension(d) => d.try_sign(),
+      DimensionPercentage::Percentage(p) => p.try_sign(),
+      DimensionPercentage::Calc(c) => c.try_sign(),
+    }
+  }
+}
+
+impl<D: ToCss + std::ops::Mul<CSSNumber, Output = D> + TrySign + Clone + std::fmt::Debug> ToCss
+  for DimensionPercentage<D>
+{
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    match self {
+      DimensionPercentage::Dimension(length) => length.to_css(dest),
+      DimensionPercentage::Percentage(percent) => percent.to_css(dest),
+      DimensionPercentage::Calc(calc) => calc.to_css(dest),
+    }
+  }
+}
diff --git a/src/values/position.rs b/src/values/position.rs
new file mode 100644
index 0000000..61373cb
--- /dev/null
+++ b/src/values/position.rs
@@ -0,0 +1,410 @@
+//! CSS position values.
+
+use super::length::LengthPercentage;
+use super::percentage::Percentage;
+use crate::error::{ParserError, PrinterError};
+use crate::macros::enum_property;
+use crate::printer::Printer;
+use crate::targets::Browsers;
+use crate::traits::{IsCompatible, Parse, ToCss, Zero};
+#[cfg(feature = "visitor")]
+use crate::visitor::Visit;
+use cssparser::*;
+
+#[cfg(feature = "serde")]
+use crate::serialization::ValueWrapper;
+
+/// A CSS [`<position>`](https://www.w3.org/TR/css3-values/#position) value,
+/// as used in the `background-position` property, gradients, masks, etc.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub struct Position {
+  /// The x-position.
+  pub x: HorizontalPosition,
+  /// The y-position.
+  pub y: VerticalPosition,
+}
+
+impl Position {
+  /// Returns a `Position` with both the x and y set to `center`.
+  pub fn center() -> Position {
+    Position {
+      x: HorizontalPosition::Center,
+      y: VerticalPosition::Center,
+    }
+  }
+
+  /// Returns whether both the x and y positions are centered.
+  pub fn is_center(&self) -> bool {
+    self.x.is_center() && self.y.is_center()
+  }
+
+  /// Returns whether both the x and y positions are zero.
+  pub fn is_zero(&self) -> bool {
+    self.x.is_zero() && self.y.is_zero()
+  }
+}
+
+impl Default for Position {
+  fn default() -> Position {
+    Position {
+      x: HorizontalPosition::Length(LengthPercentage::Percentage(Percentage(0.0))),
+      y: VerticalPosition::Length(LengthPercentage::Percentage(Percentage(0.0))),
+    }
+  }
+}
+
+impl<'i> Parse<'i> for Position {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    match input.try_parse(HorizontalPosition::parse) {
+      Ok(HorizontalPosition::Center) => {
+        // Try parsing a vertical position next.
+        if let Ok(y) = input.try_parse(VerticalPosition::parse) {
+          return Ok(Position {
+            x: HorizontalPosition::Center,
+            y,
+          });
+        }
+
+        // If it didn't work, assume the first actually represents a y position,
+        // and the next is an x position. e.g. `center left` rather than `left center`.
+        let x = input.try_parse(HorizontalPosition::parse).unwrap_or(HorizontalPosition::Center);
+        let y = VerticalPosition::Center;
+        return Ok(Position { x, y });
+      }
+      Ok(x @ HorizontalPosition::Length(_)) => {
+        // If we got a length as the first component, then the second must
+        // be a keyword or length (not a side offset).
+        if let Ok(y_keyword) = input.try_parse(VerticalPositionKeyword::parse) {
+          let y = VerticalPosition::Side {
+            side: y_keyword,
+            offset: None,
+          };
+          return Ok(Position { x, y });
+        }
+        if let Ok(y_lp) = input.try_parse(LengthPercentage::parse) {
+          let y = VerticalPosition::Length(y_lp);
+          return Ok(Position { x, y });
+        }
+        let y = VerticalPosition::Center;
+        let _ = input.try_parse(|i| i.expect_ident_matching("center"));
+        return Ok(Position { x, y });
+      }
+      Ok(HorizontalPosition::Side {
+        side: x_keyword,
+        offset: lp,
+      }) => {
+        // If we got a horizontal side keyword (and optional offset), expect another for the vertical side.
+        // e.g. `left center` or `left 20px center`
+        if input.try_parse(|i| i.expect_ident_matching("center")).is_ok() {
+          let x = HorizontalPosition::Side {
+            side: x_keyword,
+            offset: lp,
+          };
+          let y = VerticalPosition::Center;
+          return Ok(Position { x, y });
+        }
+
+        // e.g. `left top`, `left top 20px`, `left 20px top`, or `left 20px top 20px`
+        if let Ok(y_keyword) = input.try_parse(VerticalPositionKeyword::parse) {
+          let y_lp = input.try_parse(LengthPercentage::parse).ok();
+          let x = HorizontalPosition::Side {
+            side: x_keyword,
+            offset: lp,
+          };
+          let y = VerticalPosition::Side {
+            side: y_keyword,
+            offset: y_lp,
+          };
+          return Ok(Position { x, y });
+        }
+
+        // If we didn't get a vertical side keyword (e.g. `left 20px`), then apply the offset to the vertical side.
+        let x = HorizontalPosition::Side {
+          side: x_keyword,
+          offset: None,
+        };
+        let y = lp.map_or(VerticalPosition::Center, VerticalPosition::Length);
+        return Ok(Position { x, y });
+      }
+      _ => {}
+    }
+
+    // If the horizontal position didn't parse, then it must be out of order. Try vertical position keyword.
+    let y_keyword = VerticalPositionKeyword::parse(input)?;
+    let lp_and_x_pos: Result<_, ParseError<()>> = input.try_parse(|i| {
+      let y_lp = i.try_parse(LengthPercentage::parse).ok();
+      if let Ok(x_keyword) = i.try_parse(HorizontalPositionKeyword::parse) {
+        let x_lp = i.try_parse(LengthPercentage::parse).ok();
+        let x_pos = HorizontalPosition::Side {
+          side: x_keyword,
+          offset: x_lp,
+        };
+        return Ok((y_lp, x_pos));
+      }
+      i.expect_ident_matching("center")?;
+      let x_pos = HorizontalPosition::Center;
+      Ok((y_lp, x_pos))
+    });
+
+    if let Ok((y_lp, x)) = lp_and_x_pos {
+      let y = VerticalPosition::Side {
+        side: y_keyword,
+        offset: y_lp,
+      };
+      return Ok(Position { x, y });
+    }
+
+    let x = HorizontalPosition::Center;
+    let y = VerticalPosition::Side {
+      side: y_keyword,
+      offset: None,
+    };
+    Ok(Position { x, y })
+  }
+}
+
+impl ToCss for Position {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    match (&self.x, &self.y) {
+      (x_pos @ &HorizontalPosition::Side { side, offset: Some(_) }, &VerticalPosition::Length(ref y_lp))
+        if side != HorizontalPositionKeyword::Left =>
+      {
+        x_pos.to_css(dest)?;
+        dest.write_str(" top ")?;
+        y_lp.to_css(dest)
+      }
+      (x_pos @ &HorizontalPosition::Side { side, offset: Some(_) }, y)
+        if side != HorizontalPositionKeyword::Left && y.is_center() =>
+      {
+        // If there is a side keyword with an offset, "center" must be a keyword not a percentage.
+        x_pos.to_css(dest)?;
+        dest.write_str(" center")
+      }
+      (&HorizontalPosition::Length(ref x_lp), y_pos @ &VerticalPosition::Side { side, offset: Some(_) })
+        if side != VerticalPositionKeyword::Top =>
+      {
+        dest.write_str("left ")?;
+        x_lp.to_css(dest)?;
+        dest.write_str(" ")?;
+        y_pos.to_css(dest)
+      }
+      (x, y) if x.is_center() && y.is_center() => {
+        // `center center` => 50%
+        x.to_css(dest)
+      }
+      (&HorizontalPosition::Length(ref x_lp), y) if y.is_center() => {
+        // `center` is assumed if omitted.
+        x_lp.to_css(dest)
+      }
+      (&HorizontalPosition::Side { side, offset: None }, y) if y.is_center() => {
+        let p: LengthPercentage = side.into();
+        p.to_css(dest)
+      }
+      (x, y_pos @ &VerticalPosition::Side { offset: None, .. }) if x.is_center() => y_pos.to_css(dest),
+      (&HorizontalPosition::Side { side: x, offset: None }, &VerticalPosition::Side { side: y, offset: None }) => {
+        let x: LengthPercentage = x.into();
+        let y: LengthPercentage = y.into();
+        x.to_css(dest)?;
+        dest.write_str(" ")?;
+        y.to_css(dest)
+      }
+      (x_pos, y_pos) => {
+        let zero = LengthPercentage::zero();
+        let fifty = LengthPercentage::Percentage(Percentage(0.5));
+        let x_len = match &x_pos {
+          HorizontalPosition::Side {
+            side: HorizontalPositionKeyword::Left,
+            offset,
+          } => {
+            if let Some(len) = offset {
+              if len.is_zero() {
+                Some(&zero)
+              } else {
+                Some(len)
+              }
+            } else {
+              Some(&zero)
+            }
+          }
+          HorizontalPosition::Length(len) if len.is_zero() => Some(&zero),
+          HorizontalPosition::Length(len) => Some(len),
+          HorizontalPosition::Center => Some(&fifty),
+          _ => None,
+        };
+
+        let y_len = match &y_pos {
+          VerticalPosition::Side {
+            side: VerticalPositionKeyword::Top,
+            offset,
+          } => {
+            if let Some(len) = offset {
+              if len.is_zero() {
+                Some(&zero)
+              } else {
+                Some(len)
+              }
+            } else {
+              Some(&zero)
+            }
+          }
+          VerticalPosition::Length(len) if len.is_zero() => Some(&zero),
+          VerticalPosition::Length(len) => Some(len),
+          VerticalPosition::Center => Some(&fifty),
+          _ => None,
+        };
+
+        if let (Some(x), Some(y)) = (x_len, y_len) {
+          x.to_css(dest)?;
+          dest.write_str(" ")?;
+          y.to_css(dest)
+        } else {
+          x_pos.to_css(dest)?;
+          dest.write_str(" ")?;
+          y_pos.to_css(dest)
+        }
+      }
+    }
+  }
+}
+
+impl IsCompatible for Position {
+  fn is_compatible(&self, _browsers: Browsers) -> bool {
+    true
+  }
+}
+
+/// A component within a [Position](Position) value, representing a position
+/// along either the horizontal or vertical axis of a box.
+///
+/// This type is generic over side keywords.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum PositionComponent<S> {
+  /// The `center` keyword.
+  Center,
+  /// A length or percentage from the top-left corner of the box.
+  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<LengthPercentage>"))]
+  Length(LengthPercentage),
+  /// A side keyword with an optional offset.
+  Side {
+    /// A side keyword.
+    side: S,
+    /// Offset from the side.
+    offset: Option<LengthPercentage>,
+  },
+}
+
+impl<S> PositionComponent<S> {
+  fn is_center(&self) -> bool {
+    match self {
+      PositionComponent::Center => true,
+      PositionComponent::Length(LengthPercentage::Percentage(Percentage(p))) => *p == 0.5,
+      _ => false,
+    }
+  }
+
+  fn is_zero(&self) -> bool {
+    matches!(self, PositionComponent::Length(len) if len.is_zero())
+  }
+}
+
+impl<'i, S: Parse<'i>> Parse<'i> for PositionComponent<S> {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    if input.try_parse(|i| i.expect_ident_matching("center")).is_ok() {
+      return Ok(PositionComponent::Center);
+    }
+
+    if let Ok(lp) = input.try_parse(|input| LengthPercentage::parse(input)) {
+      return Ok(PositionComponent::Length(lp));
+    }
+
+    let side = S::parse(input)?;
+    let offset = input.try_parse(|input| LengthPercentage::parse(input)).ok();
+    Ok(PositionComponent::Side { side, offset })
+  }
+}
+
+impl<S: ToCss> ToCss for PositionComponent<S> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    use PositionComponent::*;
+    match &self {
+      Center => {
+        if dest.minify {
+          dest.write_str("50%")
+        } else {
+          dest.write_str("center")
+        }
+      }
+      Length(lp) => lp.to_css(dest),
+      Side { side, offset } => {
+        side.to_css(dest)?;
+        if let Some(lp) = offset {
+          dest.write_str(" ")?;
+          lp.to_css(dest)?;
+        }
+        Ok(())
+      }
+    }
+  }
+}
+
+enum_property! {
+  /// A horizontal position keyword.
+  pub enum HorizontalPositionKeyword {
+    /// The `left` keyword.
+    Left,
+    /// The `right` keyword.
+    Right,
+  }
+}
+
+impl Into<LengthPercentage> for HorizontalPositionKeyword {
+  fn into(self) -> LengthPercentage {
+    match self {
+      HorizontalPositionKeyword::Left => LengthPercentage::zero(),
+      HorizontalPositionKeyword::Right => LengthPercentage::Percentage(Percentage(1.0)),
+    }
+  }
+}
+
+enum_property! {
+  /// A vertical position keyword.
+  pub enum VerticalPositionKeyword {
+    /// The `top` keyword.
+    Top,
+    /// The `bottom` keyword.
+    Bottom,
+  }
+}
+
+impl Into<LengthPercentage> for VerticalPositionKeyword {
+  fn into(self) -> LengthPercentage {
+    match self {
+      VerticalPositionKeyword::Top => LengthPercentage::zero(),
+      VerticalPositionKeyword::Bottom => LengthPercentage::Percentage(Percentage(1.0)),
+    }
+  }
+}
+
+/// A horizontal position component.
+pub type HorizontalPosition = PositionComponent<HorizontalPositionKeyword>;
+
+/// A vertical position component.
+pub type VerticalPosition = PositionComponent<VerticalPositionKeyword>;
diff --git a/src/values/ratio.rs b/src/values/ratio.rs
new file mode 100644
index 0000000..4a80bb8
--- /dev/null
+++ b/src/values/ratio.rs
@@ -0,0 +1,64 @@
+//! CSS ratio values.
+
+use super::number::CSSNumber;
+use crate::error::{ParserError, PrinterError};
+use crate::printer::Printer;
+use crate::traits::{Parse, ToCss};
+#[cfg(feature = "visitor")]
+use crate::visitor::Visit;
+use cssparser::*;
+
+/// A CSS [`<ratio>`](https://www.w3.org/TR/css-values-4/#ratios) value,
+/// representing the ratio of two numeric values.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "visitor", visit(visit_ratio, RATIOS))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub struct Ratio(pub CSSNumber, pub CSSNumber);
+
+impl<'i> Parse<'i> for Ratio {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let first = CSSNumber::parse(input)?;
+    let second = if input.try_parse(|input| input.expect_delim('/')).is_ok() {
+      CSSNumber::parse(input)?
+    } else {
+      1.0
+    };
+
+    Ok(Ratio(first, second))
+  }
+}
+
+impl Ratio {
+  /// Parses a ratio where both operands are required.
+  pub fn parse_required<'i, 't>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let first = CSSNumber::parse(input)?;
+    input.expect_delim('/')?;
+    let second = CSSNumber::parse(input)?;
+    Ok(Ratio(first, second))
+  }
+}
+
+impl ToCss for Ratio {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    self.0.to_css(dest)?;
+    if self.1 != 1.0 {
+      dest.delim('/', true)?;
+      self.1.to_css(dest)?;
+    }
+    Ok(())
+  }
+}
+
+impl std::ops::Add<CSSNumber> for Ratio {
+  type Output = Self;
+
+  fn add(self, other: CSSNumber) -> Ratio {
+    Ratio(self.0 + other, self.1)
+  }
+}
diff --git a/src/values/rect.rs b/src/values/rect.rs
new file mode 100644
index 0000000..49b4e6c
--- /dev/null
+++ b/src/values/rect.rs
@@ -0,0 +1,130 @@
+//! Generic values for four sided properties.
+
+use crate::error::{ParserError, PrinterError};
+use crate::printer::Printer;
+use crate::traits::{IsCompatible, Parse, ToCss};
+#[cfg(feature = "visitor")]
+use crate::visitor::Visit;
+use cssparser::*;
+
+/// A generic value that represents a value for four sides of a box,
+/// e.g. border-width, margin, padding, etc.
+///
+/// When serialized, as few components as possible are written when
+/// there are duplicate values.
+#[derive(Clone, Debug, PartialEq, Eq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub struct Rect<T>(
+  /// The top component.
+  pub T,
+  /// The right component.
+  pub T,
+  /// The bottom component.
+  pub T,
+  /// The left component.
+  pub T,
+);
+
+impl<T> Rect<T> {
+  /// Returns a new `Rect<T>` value.
+  pub fn new(first: T, second: T, third: T, fourth: T) -> Self {
+    Rect(first, second, third, fourth)
+  }
+}
+
+impl<T: Default + Clone> Default for Rect<T> {
+  fn default() -> Rect<T> {
+    Rect::all(T::default())
+  }
+}
+
+impl<T> Rect<T>
+where
+  T: Clone,
+{
+  /// Returns a rect with all the values equal to `v`.
+  pub fn all(v: T) -> Self {
+    Rect::new(v.clone(), v.clone(), v.clone(), v)
+  }
+
+  /// Parses a new `Rect<T>` value with the given parse function.
+  pub fn parse_with<'i, 't, Parse>(
+    input: &mut Parser<'i, 't>,
+    parse: Parse,
+  ) -> Result<Self, ParseError<'i, ParserError<'i>>>
+  where
+    Parse: Fn(&mut Parser<'i, 't>) -> Result<T, ParseError<'i, ParserError<'i>>>,
+  {
+    let first = parse(input)?;
+    let second = if let Ok(second) = input.try_parse(|i| parse(i)) {
+      second
+    } else {
+      // <first>
+      return Ok(Self::new(first.clone(), first.clone(), first.clone(), first));
+    };
+    let third = if let Ok(third) = input.try_parse(|i| parse(i)) {
+      third
+    } else {
+      // <first> <second>
+      return Ok(Self::new(first.clone(), second.clone(), first, second));
+    };
+    let fourth = if let Ok(fourth) = input.try_parse(|i| parse(i)) {
+      fourth
+    } else {
+      // <first> <second> <third>
+      return Ok(Self::new(first, second.clone(), third, second));
+    };
+    // <first> <second> <third> <fourth>
+    Ok(Self::new(first, second, third, fourth))
+  }
+}
+
+impl<'i, T> Parse<'i> for Rect<T>
+where
+  T: Clone + PartialEq + Parse<'i>,
+{
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    Self::parse_with(input, T::parse)
+  }
+}
+
+impl<T> ToCss for Rect<T>
+where
+  T: PartialEq + ToCss,
+{
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    self.0.to_css(dest)?;
+    let same_vertical = self.0 == self.2;
+    let same_horizontal = self.1 == self.3;
+    if same_vertical && same_horizontal && self.0 == self.1 {
+      return Ok(());
+    }
+    dest.write_str(" ")?;
+    self.1.to_css(dest)?;
+    if same_vertical && same_horizontal {
+      return Ok(());
+    }
+    dest.write_str(" ")?;
+    self.2.to_css(dest)?;
+    if same_horizontal {
+      return Ok(());
+    }
+    dest.write_str(" ")?;
+    self.3.to_css(dest)
+  }
+}
+
+impl<T: IsCompatible> IsCompatible for Rect<T> {
+  fn is_compatible(&self, browsers: crate::targets::Browsers) -> bool {
+    self.0.is_compatible(browsers)
+      && self.1.is_compatible(browsers)
+      && self.2.is_compatible(browsers)
+      && self.3.is_compatible(browsers)
+  }
+}
diff --git a/src/values/resolution.rs b/src/values/resolution.rs
new file mode 100644
index 0000000..ddd78dc
--- /dev/null
+++ b/src/values/resolution.rs
@@ -0,0 +1,98 @@
+//! CSS resolution values.
+
+use super::length::serialize_dimension;
+use super::number::CSSNumber;
+use crate::compat::Feature;
+use crate::error::{ParserError, PrinterError};
+use crate::printer::Printer;
+use crate::traits::{Parse, ToCss};
+#[cfg(feature = "visitor")]
+use crate::visitor::Visit;
+use cssparser::*;
+
+/// A CSS [`<resolution>`](https://www.w3.org/TR/css-values-4/#resolution) value.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "visitor", visit(visit_resolution, RESOLUTIONS))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", content = "value", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum Resolution {
+  /// A resolution in dots per inch.
+  Dpi(CSSNumber),
+  /// A resolution in dots per centimeter.
+  Dpcm(CSSNumber),
+  /// A resolution in dots per px.
+  Dppx(CSSNumber),
+}
+
+impl<'i> Parse<'i> for Resolution {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    // TODO: calc?
+    let location = input.current_source_location();
+    match *input.next()? {
+      Token::Dimension { value, ref unit, .. } => {
+        match_ignore_ascii_case! { unit,
+          "dpi" => Ok(Resolution::Dpi(value)),
+          "dpcm" => Ok(Resolution::Dpcm(value)),
+          "dppx" | "x" => Ok(Resolution::Dppx(value)),
+          _ => Err(location.new_unexpected_token_error(Token::Ident(unit.clone())))
+        }
+      }
+      ref t => Err(location.new_unexpected_token_error(t.clone())),
+    }
+  }
+}
+
+impl<'i> TryFrom<&Token<'i>> for Resolution {
+  type Error = ();
+
+  fn try_from(token: &Token) -> Result<Self, Self::Error> {
+    match token {
+      Token::Dimension { value, ref unit, .. } => match_ignore_ascii_case! { unit,
+        "dpi" => Ok(Resolution::Dpi(*value)),
+        "dpcm" => Ok(Resolution::Dpcm(*value)),
+        "dppx" | "x" => Ok(Resolution::Dppx(*value)),
+        _ => Err(()),
+      },
+      _ => Err(()),
+    }
+  }
+}
+
+impl ToCss for Resolution {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    let (value, unit) = match self {
+      Resolution::Dpi(dpi) => (*dpi, "dpi"),
+      Resolution::Dpcm(dpcm) => (*dpcm, "dpcm"),
+      Resolution::Dppx(dppx) => {
+        if dest.targets.current.is_compatible(Feature::XResolutionUnit) {
+          (*dppx, "x")
+        } else {
+          (*dppx, "dppx")
+        }
+      }
+    };
+
+    serialize_dimension(value, unit, dest)
+  }
+}
+
+impl std::ops::Add<CSSNumber> for Resolution {
+  type Output = Self;
+
+  fn add(self, other: CSSNumber) -> Resolution {
+    match self {
+      Resolution::Dpi(dpi) => Resolution::Dpi(dpi + other),
+      Resolution::Dpcm(dpcm) => Resolution::Dpcm(dpcm + other),
+      Resolution::Dppx(dppx) => Resolution::Dppx(dppx + other),
+    }
+  }
+}
diff --git a/src/values/shape.rs b/src/values/shape.rs
new file mode 100644
index 0000000..9cb5f58
--- /dev/null
+++ b/src/values/shape.rs
@@ -0,0 +1,361 @@
+//! CSS shape values for masking and clipping.
+
+use super::length::LengthPercentage;
+use super::position::Position;
+use super::rect::Rect;
+use crate::error::{ParserError, PrinterError};
+use crate::macros::enum_property;
+use crate::printer::Printer;
+use crate::properties::border_radius::BorderRadius;
+use crate::traits::{Parse, ToCss};
+#[cfg(feature = "visitor")]
+use crate::visitor::Visit;
+use cssparser::*;
+
+/// A CSS [`<basic-shape>`](https://www.w3.org/TR/css-shapes-1/#basic-shape-functions) value.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", content = "value", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum BasicShape {
+  /// An inset rectangle.
+  Inset(InsetRect),
+  /// A circle.
+  Circle(Circle),
+  /// An ellipse.
+  Ellipse(Ellipse),
+  /// A polygon.
+  Polygon(Polygon),
+}
+
+/// An [`inset()`](https://www.w3.org/TR/css-shapes-1/#funcdef-inset) rectangle shape.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub struct InsetRect {
+  /// The rectangle.
+  pub rect: Rect<LengthPercentage>,
+  /// A corner radius for the rectangle.
+  pub radius: BorderRadius,
+}
+
+/// A [`circle()`](https://www.w3.org/TR/css-shapes-1/#funcdef-circle) shape.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub struct Circle {
+  /// The radius of the circle.
+  pub radius: ShapeRadius,
+  /// The position of the center of the circle.
+  pub position: Position,
+}
+
+/// A [`<shape-radius>`](https://www.w3.org/TR/css-shapes-1/#typedef-shape-radius) value
+/// that defines the radius of a `circle()` or `ellipse()` shape.
+#[derive(Debug, Clone, PartialEq, Parse, ToCss)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", content = "value", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub enum ShapeRadius {
+  /// An explicit length or percentage.
+  LengthPercentage(LengthPercentage),
+  /// The length from the center to the closest side of the box.
+  ClosestSide,
+  /// The length from the center to the farthest side of the box.
+  FarthestSide,
+}
+
+/// An [`ellipse()`](https://www.w3.org/TR/css-shapes-1/#funcdef-ellipse) shape.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(rename_all = "camelCase")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub struct Ellipse {
+  /// The x-radius of the ellipse.
+  pub radius_x: ShapeRadius,
+  /// The y-radius of the ellipse.
+  pub radius_y: ShapeRadius,
+  /// The position of the center of the ellipse.
+  pub position: Position,
+}
+
+/// A [`polygon()`](https://www.w3.org/TR/css-shapes-1/#funcdef-polygon) shape.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(rename_all = "camelCase")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub struct Polygon {
+  /// The fill rule used to determine the interior of the polygon.
+  pub fill_rule: FillRule,
+  /// The points of each vertex of the polygon.
+  pub points: Vec<Point>,
+}
+
+/// A point within a `polygon()` shape.
+///
+/// See [Polygon](Polygon).
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub struct Point {
+  /// The x position of the point.
+  x: LengthPercentage,
+  /// the y position of the point.
+  y: LengthPercentage,
+}
+
+enum_property! {
+  /// A [`<fill-rule>`](https://www.w3.org/TR/css-shapes-1/#typedef-fill-rule) used to
+  /// determine the interior of a `polygon()` shape.
+  ///
+  /// See [Polygon](Polygon).
+  pub enum FillRule {
+    /// The `nonzero` fill rule.
+    Nonzero,
+    /// The `evenodd` fill rule.
+    Evenodd,
+  }
+}
+
+impl Default for FillRule {
+  fn default() -> FillRule {
+    FillRule::Nonzero
+  }
+}
+
+impl<'i> Parse<'i> for BasicShape {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let location = input.current_source_location();
+    let f = input.expect_function()?;
+    match_ignore_ascii_case! { &f,
+      "inset" => Ok(BasicShape::Inset(input.parse_nested_block(InsetRect::parse)?)),
+      "circle" => Ok(BasicShape::Circle(input.parse_nested_block(Circle::parse)?)),
+      "ellipse" => Ok(BasicShape::Ellipse(input.parse_nested_block(Ellipse::parse)?)),
+      "polygon" => Ok(BasicShape::Polygon(input.parse_nested_block(Polygon::parse)?)),
+      _ => Err(location.new_unexpected_token_error(Token::Ident(f.clone()))),
+    }
+  }
+}
+
+impl<'i> Parse<'i> for InsetRect {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let rect = Rect::parse(input)?;
+    let radius = if input.try_parse(|input| input.expect_ident_matching("round")).is_ok() {
+      BorderRadius::parse(input)?
+    } else {
+      BorderRadius::default()
+    };
+    Ok(InsetRect { rect, radius })
+  }
+}
+
+impl<'i> Parse<'i> for Circle {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let radius = input.try_parse(ShapeRadius::parse).unwrap_or_default();
+    let position = if input.try_parse(|input| input.expect_ident_matching("at")).is_ok() {
+      Position::parse(input)?
+    } else {
+      Position::center()
+    };
+
+    Ok(Circle { radius, position })
+  }
+}
+
+impl Default for ShapeRadius {
+  fn default() -> ShapeRadius {
+    ShapeRadius::ClosestSide
+  }
+}
+
+impl<'i> Parse<'i> for Ellipse {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let (x, y) = input
+      .try_parse(|input| -> Result<_, ParseError<'i, ParserError<'i>>> {
+        Ok((ShapeRadius::parse(input)?, ShapeRadius::parse(input)?))
+      })
+      .unwrap_or_default();
+
+    let position = if input.try_parse(|input| input.expect_ident_matching("at")).is_ok() {
+      Position::parse(input)?
+    } else {
+      Position::center()
+    };
+
+    Ok(Ellipse {
+      radius_x: x,
+      radius_y: y,
+      position,
+    })
+  }
+}
+
+impl<'i> Parse<'i> for Polygon {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let fill_rule = input.try_parse(FillRule::parse);
+    if fill_rule.is_ok() {
+      input.expect_comma()?;
+    }
+
+    let points = input.parse_comma_separated(Point::parse)?;
+    Ok(Polygon {
+      fill_rule: fill_rule.unwrap_or_default(),
+      points,
+    })
+  }
+}
+
+impl<'i> Parse<'i> for Point {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let x = LengthPercentage::parse(input)?;
+    let y = LengthPercentage::parse(input)?;
+    Ok(Point { x, y })
+  }
+}
+
+impl ToCss for BasicShape {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    match self {
+      BasicShape::Inset(rect) => {
+        dest.write_str("inset(")?;
+        rect.to_css(dest)?;
+        dest.write_char(')')
+      }
+      BasicShape::Circle(circle) => {
+        dest.write_str("circle(")?;
+        circle.to_css(dest)?;
+        dest.write_char(')')
+      }
+      BasicShape::Ellipse(ellipse) => {
+        dest.write_str("ellipse(")?;
+        ellipse.to_css(dest)?;
+        dest.write_char(')')
+      }
+      BasicShape::Polygon(poly) => {
+        dest.write_str("polygon(")?;
+        poly.to_css(dest)?;
+        dest.write_char(')')
+      }
+    }
+  }
+}
+
+impl ToCss for InsetRect {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    self.rect.to_css(dest)?;
+    if self.radius != BorderRadius::default() {
+      dest.write_str(" round ")?;
+      self.radius.to_css(dest)?;
+    }
+    Ok(())
+  }
+}
+
+impl ToCss for Circle {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    let mut has_output = false;
+    if self.radius != ShapeRadius::default() {
+      self.radius.to_css(dest)?;
+      has_output = true;
+    }
+
+    if !self.position.is_center() {
+      if has_output {
+        dest.write_char(' ')?;
+      }
+      dest.write_str("at ")?;
+      self.position.to_css(dest)?;
+    }
+
+    Ok(())
+  }
+}
+
+impl ToCss for Ellipse {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    let mut has_output = false;
+    if self.radius_x != ShapeRadius::default() || self.radius_y != ShapeRadius::default() {
+      self.radius_x.to_css(dest)?;
+      dest.write_char(' ')?;
+      self.radius_y.to_css(dest)?;
+      has_output = true;
+    }
+
+    if !self.position.is_center() {
+      if has_output {
+        dest.write_char(' ')?;
+      }
+      dest.write_str("at ")?;
+      self.position.to_css(dest)?;
+    }
+
+    Ok(())
+  }
+}
+
+impl ToCss for Polygon {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    if self.fill_rule != FillRule::default() {
+      self.fill_rule.to_css(dest)?;
+      dest.delim(',', false)?;
+    }
+
+    let mut first = true;
+    for point in &self.points {
+      if first {
+        first = false;
+      } else {
+        dest.delim(',', false)?;
+      }
+      point.to_css(dest)?;
+    }
+
+    Ok(())
+  }
+}
+
+impl ToCss for Point {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    self.x.to_css(dest)?;
+    dest.write_char(' ')?;
+    self.y.to_css(dest)
+  }
+}
diff --git a/src/values/size.rs b/src/values/size.rs
new file mode 100644
index 0000000..79b96e3
--- /dev/null
+++ b/src/values/size.rs
@@ -0,0 +1,52 @@
+//! Generic values for two component properties.
+
+use crate::error::{ParserError, PrinterError};
+use crate::printer::Printer;
+use crate::traits::{IsCompatible, Parse, ToCss};
+#[cfg(feature = "visitor")]
+use crate::visitor::Visit;
+use cssparser::*;
+
+/// A generic value that represents a value with two components, e.g. a border radius.
+///
+/// When serialized, only a single component will be written if both are equal.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub struct Size2D<T>(pub T, pub T);
+
+impl<'i, T> Parse<'i> for Size2D<T>
+where
+  T: Parse<'i> + Clone,
+{
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let first = T::parse(input)?;
+    let second = input.try_parse(T::parse).unwrap_or_else(|_| first.clone());
+    Ok(Size2D(first, second))
+  }
+}
+
+impl<T> ToCss for Size2D<T>
+where
+  T: ToCss + PartialEq,
+{
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    self.0.to_css(dest)?;
+    if self.1 != self.0 {
+      dest.write_str(" ")?;
+      self.1.to_css(dest)?;
+    }
+    Ok(())
+  }
+}
+
+impl<T: IsCompatible> IsCompatible for Size2D<T> {
+  fn is_compatible(&self, browsers: crate::targets::Browsers) -> bool {
+    self.0.is_compatible(browsers) && self.1.is_compatible(browsers)
+  }
+}
diff --git a/src/values/string.rs b/src/values/string.rs
new file mode 100644
index 0000000..3829e29
--- /dev/null
+++ b/src/values/string.rs
@@ -0,0 +1,443 @@
+//! Types used to represent strings.
+
+use crate::traits::{Parse, ToCss};
+#[cfg(feature = "visitor")]
+use crate::visitor::{Visit, VisitTypes, Visitor};
+use cssparser::{serialize_string, CowRcStr};
+#[cfg(feature = "serde")]
+use serde::{Deserialize, Deserializer};
+#[cfg(any(feature = "serde", feature = "nodejs"))]
+use serde::{Serialize, Serializer};
+use std::borrow::{Borrow, Cow};
+use std::cmp;
+use std::fmt;
+use std::hash;
+use std::marker::PhantomData;
+use std::ops::Deref;
+use std::rc::Rc;
+use std::slice;
+use std::str;
+use std::sync::Arc;
+
+// We cannot store CowRcStr from cssparser directly because it is not threadsafe (due to Rc).
+// CowArcStr is exactly the same, but uses Arc instead of Rc. We could use Cow<str> instead,
+// but real-world benchmarks show a performance regression, likely due to the larger memory
+// footprint.
+//
+// In order to convert between CowRcStr and CowArcStr without cloning, we use some unsafe
+// tricks to access the internal fields. LocalCowRcStr must be exactly the same as CowRcStr
+// so we can transmutate between them.
+struct LocalCowRcStr<'a> {
+  ptr: &'static (),
+  borrowed_len_or_max: usize,
+  phantom: PhantomData<Result<&'a str, Rc<String>>>,
+}
+
+/// A string that is either shared (heap-allocated and atomically reference-counted)
+/// or borrowed from the input CSS source code.
+pub struct CowArcStr<'a> {
+  ptr: &'static (),
+  borrowed_len_or_max: usize,
+  phantom: PhantomData<Result<&'a str, Arc<String>>>,
+}
+
+impl<'a> From<CowRcStr<'a>> for CowArcStr<'a> {
+  #[inline]
+  fn from(s: CowRcStr<'a>) -> Self {
+    (&s).into()
+  }
+}
+
+impl<'a> From<&CowRcStr<'a>> for CowArcStr<'a> {
+  #[inline]
+  fn from(s: &CowRcStr<'a>) -> Self {
+    let local = unsafe { std::mem::transmute::<&CowRcStr<'a>, &LocalCowRcStr<'a>>(&s) };
+    if local.borrowed_len_or_max == usize::MAX {
+      // If the string is owned and not borrowed, we do need to clone.
+      // We could possibly use std::mem::take here, but that would mutate the
+      // original CowRcStr which we borrowed, so might be unexpected. Owned
+      // CowRcStr are very rare in practice though, since most strings are
+      // borrowed directly from the input.
+      let ptr = local.ptr as *const () as *mut String;
+      CowArcStr::from(unsafe { (*ptr).clone() })
+    } else {
+      let s = unsafe {
+        str::from_utf8_unchecked(slice::from_raw_parts(
+          local.ptr as *const () as *const u8,
+          local.borrowed_len_or_max,
+        ))
+      };
+
+      CowArcStr::from(s)
+    }
+  }
+}
+
+// The below implementation is copied and modified from cssparser.
+
+impl<'a> From<&'a str> for CowArcStr<'a> {
+  #[inline]
+  fn from(s: &'a str) -> Self {
+    let len = s.len();
+    assert!(len < usize::MAX);
+    CowArcStr {
+      ptr: unsafe { &*(s.as_ptr() as *const ()) },
+      borrowed_len_or_max: len,
+      phantom: PhantomData,
+    }
+  }
+}
+
+impl<'a> From<String> for CowArcStr<'a> {
+  #[inline]
+  fn from(s: String) -> Self {
+    CowArcStr::from_arc(Arc::new(s))
+  }
+}
+
+impl<'a> From<Cow<'a, str>> for CowArcStr<'a> {
+  #[inline]
+  fn from(s: Cow<'a, str>) -> Self {
+    match s {
+      Cow::Borrowed(s) => s.into(),
+      Cow::Owned(s) => s.into(),
+    }
+  }
+}
+
+impl<'a> CowArcStr<'a> {
+  #[inline]
+  fn from_arc(s: Arc<String>) -> Self {
+    let ptr = unsafe { &*(Arc::into_raw(s) as *const ()) };
+    CowArcStr {
+      ptr,
+      borrowed_len_or_max: usize::MAX,
+      phantom: PhantomData,
+    }
+  }
+
+  #[inline]
+  fn unpack(&self) -> Result<&'a str, *const String> {
+    if self.borrowed_len_or_max == usize::MAX {
+      Err(self.ptr as *const () as *const String)
+    } else {
+      unsafe {
+        Ok(str::from_utf8_unchecked(slice::from_raw_parts(
+          self.ptr as *const () as *const u8,
+          self.borrowed_len_or_max,
+        )))
+      }
+    }
+  }
+}
+
+#[cfg(feature = "into_owned")]
+impl<'any> static_self::IntoOwned<'any> for CowArcStr<'_> {
+  type Owned = CowArcStr<'any>;
+
+  /// Consumes the value and returns an owned clone.
+  fn into_owned(self) -> Self::Owned {
+    if self.borrowed_len_or_max != usize::MAX {
+      CowArcStr::from(self.as_ref().to_owned())
+    } else {
+      unsafe { std::mem::transmute(self) }
+    }
+  }
+}
+
+impl<'a> Clone for CowArcStr<'a> {
+  #[inline]
+  fn clone(&self) -> Self {
+    match self.unpack() {
+      Err(ptr) => {
+        let rc = unsafe { Arc::from_raw(ptr) };
+        let new_rc = rc.clone();
+        std::mem::forget(rc); // Don’t actually take ownership of this strong reference
+        CowArcStr::from_arc(new_rc)
+      }
+      Ok(_) => CowArcStr { ..*self },
+    }
+  }
+}
+
+impl<'a> Drop for CowArcStr<'a> {
+  #[inline]
+  fn drop(&mut self) {
+    if let Err(ptr) = self.unpack() {
+      std::mem::drop(unsafe { Arc::from_raw(ptr) })
+    }
+  }
+}
+
+impl<'a> Deref for CowArcStr<'a> {
+  type Target = str;
+
+  #[inline]
+  fn deref(&self) -> &str {
+    self.unpack().unwrap_or_else(|ptr| unsafe { &**ptr })
+  }
+}
+
+// Boilerplate / trivial impls below.
+
+impl<'a> AsRef<str> for CowArcStr<'a> {
+  #[inline]
+  fn as_ref(&self) -> &str {
+    self
+  }
+}
+
+impl<'a> Borrow<str> for CowArcStr<'a> {
+  #[inline]
+  fn borrow(&self) -> &str {
+    self
+  }
+}
+
+impl<'a> Default for CowArcStr<'a> {
+  #[inline]
+  fn default() -> Self {
+    Self::from("")
+  }
+}
+
+impl<'a> hash::Hash for CowArcStr<'a> {
+  #[inline]
+  fn hash<H: hash::Hasher>(&self, hasher: &mut H) {
+    str::hash(self, hasher)
+  }
+}
+
+impl<'a, T: AsRef<str>> PartialEq<T> for CowArcStr<'a> {
+  #[inline]
+  fn eq(&self, other: &T) -> bool {
+    str::eq(self, other.as_ref())
+  }
+}
+
+impl<'a, T: AsRef<str>> PartialOrd<T> for CowArcStr<'a> {
+  #[inline]
+  fn partial_cmp(&self, other: &T) -> Option<cmp::Ordering> {
+    str::partial_cmp(self, other.as_ref())
+  }
+}
+
+impl<'a> Eq for CowArcStr<'a> {}
+
+impl<'a> Ord for CowArcStr<'a> {
+  #[inline]
+  fn cmp(&self, other: &Self) -> cmp::Ordering {
+    str::cmp(self, other)
+  }
+}
+
+impl<'a> fmt::Display for CowArcStr<'a> {
+  #[inline]
+  fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+    str::fmt(self, formatter)
+  }
+}
+
+impl<'a> fmt::Debug for CowArcStr<'a> {
+  #[inline]
+  fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+    str::fmt(self, formatter)
+  }
+}
+
+#[cfg(any(feature = "nodejs", feature = "serde"))]
+impl<'a> Serialize for CowArcStr<'a> {
+  fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
+    self.as_ref().serialize(serializer)
+  }
+}
+
+#[cfg(feature = "serde")]
+#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
+impl<'a, 'de: 'a> Deserialize<'de> for CowArcStr<'a> {
+  fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+  where
+    D: Deserializer<'de>,
+  {
+    deserializer.deserialize_str(CowArcStrVisitor)
+  }
+}
+
+#[cfg(feature = "jsonschema")]
+#[cfg_attr(docsrs, doc(cfg(feature = "jsonschema")))]
+impl<'a> schemars::JsonSchema for CowArcStr<'a> {
+  fn is_referenceable() -> bool {
+    true
+  }
+
+  fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
+    String::json_schema(gen)
+  }
+
+  fn schema_name() -> String {
+    "String".into()
+  }
+}
+
+#[cfg(feature = "serde")]
+struct CowArcStrVisitor;
+
+#[cfg(feature = "serde")]
+impl<'de> serde::de::Visitor<'de> for CowArcStrVisitor {
+  type Value = CowArcStr<'de>;
+
+  fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+    formatter.write_str("a CowArcStr")
+  }
+
+  fn visit_borrowed_str<E>(self, v: &'de str) -> Result<Self::Value, E>
+  where
+    E: serde::de::Error,
+  {
+    Ok(v.into())
+  }
+
+  fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
+  where
+    E: serde::de::Error,
+  {
+    Ok(v.to_owned().into())
+  }
+
+  fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
+  where
+    E: serde::de::Error,
+  {
+    Ok(v.into())
+  }
+}
+
+#[cfg(feature = "visitor")]
+impl<'i, V: ?Sized + Visitor<'i, T>, T: Visit<'i, T, V>> Visit<'i, T, V> for CowArcStr<'i> {
+  const CHILD_TYPES: VisitTypes = VisitTypes::empty();
+  fn visit_children(&mut self, _: &mut V) -> Result<(), V::Error> {
+    Ok(())
+  }
+}
+
+/// A quoted CSS string.
+#[derive(Clone, Eq, Ord, Hash, Debug)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(transparent))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub struct CSSString<'i>(#[cfg_attr(feature = "serde", serde(borrow))] pub CowArcStr<'i>);
+
+impl<'i> Parse<'i> for CSSString<'i> {
+  fn parse<'t>(
+    input: &mut cssparser::Parser<'i, 't>,
+  ) -> Result<Self, cssparser::ParseError<'i, crate::error::ParserError<'i>>> {
+    let s = input.expect_string()?;
+    Ok(CSSString(s.into()))
+  }
+}
+
+impl<'i> ToCss for CSSString<'i> {
+  fn to_css<W>(&self, dest: &mut crate::printer::Printer<W>) -> Result<(), crate::error::PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    serialize_string(&self.0, dest)?;
+    Ok(())
+  }
+}
+
+impl<'i> cssparser::ToCss for CSSString<'i> {
+  fn to_css<W>(&self, dest: &mut W) -> fmt::Result
+  where
+    W: fmt::Write,
+  {
+    serialize_string(&self.0, dest)
+  }
+}
+
+macro_rules! impl_string_type {
+  ($t: ident) => {
+    impl<'i> From<CowRcStr<'i>> for $t<'i> {
+      fn from(s: CowRcStr<'i>) -> Self {
+        $t(s.into())
+      }
+    }
+
+    impl<'a> From<&CowRcStr<'a>> for $t<'a> {
+      fn from(s: &CowRcStr<'a>) -> Self {
+        $t(s.into())
+      }
+    }
+
+    impl<'i> From<String> for $t<'i> {
+      fn from(s: String) -> Self {
+        $t(s.into())
+      }
+    }
+
+    impl<'i> From<&'i str> for $t<'i> {
+      fn from(s: &'i str) -> Self {
+        $t(s.into())
+      }
+    }
+
+    impl<'a> From<std::borrow::Cow<'a, str>> for $t<'a> {
+      #[inline]
+      fn from(s: std::borrow::Cow<'a, str>) -> Self {
+        match s {
+          std::borrow::Cow::Borrowed(s) => s.into(),
+          std::borrow::Cow::Owned(s) => s.into(),
+        }
+      }
+    }
+
+    impl<'a> Deref for $t<'a> {
+      type Target = str;
+
+      #[inline]
+      fn deref(&self) -> &str {
+        self.0.deref()
+      }
+    }
+
+    impl<'a> AsRef<str> for $t<'a> {
+      #[inline]
+      fn as_ref(&self) -> &str {
+        self
+      }
+    }
+
+    impl<'a> Borrow<str> for $t<'a> {
+      #[inline]
+      fn borrow(&self) -> &str {
+        self
+      }
+    }
+
+    impl<'a> std::fmt::Display for $t<'a> {
+      #[inline]
+      fn fmt(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
+        str::fmt(self, formatter)
+      }
+    }
+
+    impl<'a, T: AsRef<str>> PartialEq<T> for $t<'a> {
+      #[inline]
+      fn eq(&self, other: &T) -> bool {
+        str::eq(self, other.as_ref())
+      }
+    }
+
+    impl<'a, T: AsRef<str>> PartialOrd<T> for $t<'a> {
+      #[inline]
+      fn partial_cmp(&self, other: &T) -> Option<std::cmp::Ordering> {
+        str::partial_cmp(self, other.as_ref())
+      }
+    }
+  };
+}
+
+impl_string_type!(CSSString);
+
+pub(crate) use impl_string_type;
diff --git a/src/values/syntax.rs b/src/values/syntax.rs
new file mode 100644
index 0000000..42dfe48
--- /dev/null
+++ b/src/values/syntax.rs
@@ -0,0 +1,653 @@
+//! CSS syntax strings
+
+use super::ident::Ident;
+use super::number::{CSSInteger, CSSNumber};
+use crate::error::{ParserError, PrinterError};
+use crate::printer::Printer;
+use crate::properties::custom::TokenList;
+use crate::stylesheet::ParserOptions;
+use crate::traits::{Parse, ToCss};
+use crate::values;
+#[cfg(feature = "visitor")]
+use crate::visitor::Visit;
+use cssparser::*;
+
+/// A CSS [syntax string](https://drafts.css-houdini.org/css-properties-values-api/#syntax-strings)
+/// used to define the grammar for a registered custom property.
+#[derive(Debug, PartialEq, Clone)]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", content = "value", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum SyntaxString {
+  /// A list of syntax components.
+  Components(Vec<SyntaxComponent>),
+  /// The universal syntax definition.
+  Universal,
+}
+
+/// A [syntax component](https://drafts.css-houdini.org/css-properties-values-api/#syntax-component)
+/// within a [SyntaxString](SyntaxString).
+///
+/// A syntax component consists of a component kind an a multiplier, which indicates how the component
+/// may repeat during parsing.
+#[derive(Debug, PartialEq, Clone)]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub struct SyntaxComponent {
+  /// The kind of component.
+  pub kind: SyntaxComponentKind,
+  /// A multiplier for the component.
+  pub multiplier: Multiplier,
+}
+
+/// A [syntax component component name](https://drafts.css-houdini.org/css-properties-values-api/#supported-names).
+#[derive(Debug, PartialEq, Clone)]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", content = "value", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum SyntaxComponentKind {
+  /// A `<length>` component.
+  Length,
+  /// A `<number>` component.
+  Number,
+  /// A `<percentage>` component.
+  Percentage,
+  /// A `<length-percentage>` component.
+  LengthPercentage,
+  /// A `<color>` component.
+  Color,
+  /// An `<image>` component.
+  Image,
+  /// A `<url>` component.
+  Url,
+  /// An `<integer>` component.
+  Integer,
+  /// An `<angle>` component.
+  Angle,
+  /// A `<time>` component.
+  Time,
+  /// A `<resolution>` component.
+  Resolution,
+  /// A `<transform-function>` component.
+  TransformFunction,
+  /// A `<transform-list>` component.
+  TransformList,
+  /// A `<custom-ident>` component.
+  CustomIdent,
+  /// A literal component.
+  Literal(String), // TODO: borrow??
+}
+
+/// A [multiplier](https://drafts.css-houdini.org/css-properties-values-api/#multipliers) for a
+/// [SyntaxComponent](SyntaxComponent). Indicates whether and how the component may be repeated.
+#[derive(Debug, PartialEq, Clone)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", content = "value", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum Multiplier {
+  /// The component may not be repeated.
+  None,
+  /// The component may repeat one or more times, separated by spaces.
+  Space,
+  /// The component may repeat one or more times, separated by commas.
+  Comma,
+}
+
+/// A parsed value for a [SyntaxComponent](SyntaxComponent).
+#[derive(Debug, PartialEq, Clone)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", content = "value", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub enum ParsedComponent<'i> {
+  /// A `<length>` value.
+  Length(values::length::Length),
+  /// A `<number>` value.
+  Number(CSSNumber),
+  /// A `<percentage>` value.
+  Percentage(values::percentage::Percentage),
+  /// A `<length-percentage>` value.
+  LengthPercentage(values::length::LengthPercentage),
+  /// A `<color>` value.
+  Color(values::color::CssColor),
+  /// An `<image>` value.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  Image(values::image::Image<'i>),
+  /// A `<url>` value.
+  Url(values::url::Url<'i>),
+  /// An `<integer>` value.
+  Integer(CSSInteger),
+  /// An `<angle>` value.
+  Angle(values::angle::Angle),
+  /// A `<time>` value.
+  Time(values::time::Time),
+  /// A `<resolution>` value.
+  Resolution(values::resolution::Resolution),
+  /// A `<transform-function>` value.
+  TransformFunction(crate::properties::transform::Transform),
+  /// A `<transform-list>` value.
+  TransformList(crate::properties::transform::TransformList),
+  /// A `<custom-ident>` value.
+  CustomIdent(values::ident::CustomIdent<'i>),
+  /// A literal value.
+  Literal(Ident<'i>),
+  /// A repeated component value.
+  Repeated {
+    /// The components to repeat.
+    #[cfg_attr(feature = "visitor", skip_type)]
+    components: Vec<ParsedComponent<'i>>,
+    /// A multiplier describing how the components repeat.
+    multiplier: Multiplier,
+  },
+  /// A raw token stream.
+  TokenList(crate::properties::custom::TokenList<'i>),
+}
+
+impl<'i> SyntaxString {
+  /// Parses a syntax string.
+  pub fn parse_string(input: &'i str) -> Result<SyntaxString, ()> {
+    // https://drafts.css-houdini.org/css-properties-values-api/#parsing-syntax
+    let mut input = input.trim_matches(SPACE_CHARACTERS);
+    if input.is_empty() {
+      return Err(());
+    }
+
+    if input == "*" {
+      return Ok(SyntaxString::Universal);
+    }
+
+    let mut components = Vec::new();
+    loop {
+      let component = SyntaxComponent::parse_string(&mut input)?;
+      components.push(component);
+
+      input = input.trim_start_matches(SPACE_CHARACTERS);
+      if input.is_empty() {
+        break;
+      }
+
+      if input.starts_with('|') {
+        input = &input[1..];
+        continue;
+      }
+
+      return Err(());
+    }
+
+    Ok(SyntaxString::Components(components))
+  }
+
+  /// Parses a value according to the syntax grammar.
+  pub fn parse_value<'t>(
+    &self,
+    input: &mut Parser<'i, 't>,
+  ) -> Result<ParsedComponent<'i>, ParseError<'i, ParserError<'i>>> {
+    match self {
+      SyntaxString::Universal => Ok(ParsedComponent::TokenList(TokenList::parse(
+        input,
+        &ParserOptions::default(),
+        0,
+      )?)),
+      SyntaxString::Components(components) => {
+        // Loop through each component, and return the first one that parses successfully.
+        for component in components {
+          let state = input.state();
+          let mut parsed = Vec::new();
+          loop {
+            let value: Result<ParsedComponent<'i>, ParseError<'i, ParserError<'i>>> = input.try_parse(|input| {
+              Ok(match &component.kind {
+                SyntaxComponentKind::Length => ParsedComponent::Length(values::length::Length::parse(input)?),
+                SyntaxComponentKind::Number => ParsedComponent::Number(CSSNumber::parse(input)?),
+                SyntaxComponentKind::Percentage => {
+                  ParsedComponent::Percentage(values::percentage::Percentage::parse(input)?)
+                }
+                SyntaxComponentKind::LengthPercentage => {
+                  ParsedComponent::LengthPercentage(values::length::LengthPercentage::parse(input)?)
+                }
+                SyntaxComponentKind::Color => ParsedComponent::Color(values::color::CssColor::parse(input)?),
+                SyntaxComponentKind::Image => ParsedComponent::Image(values::image::Image::parse(input)?),
+                SyntaxComponentKind::Url => ParsedComponent::Url(values::url::Url::parse(input)?),
+                SyntaxComponentKind::Integer => ParsedComponent::Integer(CSSInteger::parse(input)?),
+                SyntaxComponentKind::Angle => ParsedComponent::Angle(values::angle::Angle::parse(input)?),
+                SyntaxComponentKind::Time => ParsedComponent::Time(values::time::Time::parse(input)?),
+                SyntaxComponentKind::Resolution => {
+                  ParsedComponent::Resolution(values::resolution::Resolution::parse(input)?)
+                }
+                SyntaxComponentKind::TransformFunction => {
+                  ParsedComponent::TransformFunction(crate::properties::transform::Transform::parse(input)?)
+                }
+                SyntaxComponentKind::TransformList => {
+                  ParsedComponent::TransformList(crate::properties::transform::TransformList::parse(input)?)
+                }
+                SyntaxComponentKind::CustomIdent => {
+                  ParsedComponent::CustomIdent(values::ident::CustomIdent::parse(input)?)
+                }
+                SyntaxComponentKind::Literal(value) => {
+                  let location = input.current_source_location();
+                  let ident = input.expect_ident()?;
+                  if *ident != &value {
+                    return Err(location.new_unexpected_token_error(Token::Ident(ident.clone())));
+                  }
+                  ParsedComponent::Literal(ident.into())
+                }
+              })
+            });
+
+            if let Ok(value) = value {
+              match component.multiplier {
+                Multiplier::None => return Ok(value),
+                Multiplier::Space => {
+                  parsed.push(value);
+                  if input.is_exhausted() {
+                    return Ok(ParsedComponent::Repeated {
+                      components: parsed,
+                      multiplier: component.multiplier.clone(),
+                    });
+                  }
+                }
+                Multiplier::Comma => {
+                  parsed.push(value);
+                  match input.next() {
+                    Err(_) => {
+                      return Ok(ParsedComponent::Repeated {
+                        components: parsed,
+                        multiplier: component.multiplier.clone(),
+                      })
+                    }
+                    Ok(&Token::Comma) => continue,
+                    Ok(_) => break,
+                  }
+                }
+              }
+            } else {
+              break;
+            }
+          }
+
+          input.reset(&state);
+        }
+
+        Err(input.new_error_for_next_token())
+      }
+    }
+  }
+
+  /// Parses a value from a string according to the syntax grammar.
+  pub fn parse_value_from_string<'t>(
+    &self,
+    input: &'i str,
+  ) -> Result<ParsedComponent<'i>, ParseError<'i, ParserError<'i>>> {
+    let mut input = ParserInput::new(input);
+    let mut parser = Parser::new(&mut input);
+    self.parse_value(&mut parser)
+  }
+}
+
+impl SyntaxComponent {
+  fn parse_string(input: &mut &str) -> Result<SyntaxComponent, ()> {
+    let kind = SyntaxComponentKind::parse_string(input)?;
+
+    // Pre-multiplied types cannot have multipliers.
+    if kind == SyntaxComponentKind::TransformList {
+      return Ok(SyntaxComponent {
+        kind,
+        multiplier: Multiplier::None,
+      });
+    }
+
+    let multiplier = if input.starts_with('+') {
+      *input = &input[1..];
+      Multiplier::Space
+    } else if input.starts_with('#') {
+      *input = &input[1..];
+      Multiplier::Comma
+    } else {
+      Multiplier::None
+    };
+
+    Ok(SyntaxComponent { kind, multiplier })
+  }
+}
+
+// https://drafts.csswg.org/css-syntax-3/#whitespace
+static SPACE_CHARACTERS: &'static [char] = &['\u{0020}', '\u{0009}'];
+
+impl SyntaxComponentKind {
+  fn parse_string(input: &mut &str) -> Result<SyntaxComponentKind, ()> {
+    // https://drafts.css-houdini.org/css-properties-values-api/#consume-syntax-component
+    *input = input.trim_start_matches(SPACE_CHARACTERS);
+    if input.starts_with('<') {
+      // https://drafts.css-houdini.org/css-properties-values-api/#consume-data-type-name
+      let end_idx = input.find('>').ok_or(())?;
+      let name = &input[1..end_idx];
+      let component = match_ignore_ascii_case! {name,
+        "length" => SyntaxComponentKind::Length,
+        "number" => SyntaxComponentKind::Number,
+        "percentage" => SyntaxComponentKind::Percentage,
+        "length-percentage" => SyntaxComponentKind::LengthPercentage,
+        "color" => SyntaxComponentKind::Color,
+        "image" => SyntaxComponentKind::Image,
+        "url" => SyntaxComponentKind::Url,
+        "integer" => SyntaxComponentKind::Integer,
+        "angle" => SyntaxComponentKind::Angle,
+        "time" => SyntaxComponentKind::Time,
+        "resolution" => SyntaxComponentKind::Resolution,
+        "transform-function" => SyntaxComponentKind::TransformFunction,
+        "transform-list" => SyntaxComponentKind::TransformList,
+        "custom-ident" => SyntaxComponentKind::CustomIdent,
+        _ => return Err(())
+      };
+
+      *input = &input[end_idx + 1..];
+      Ok(component)
+    } else if input.starts_with(is_ident_start) {
+      // A literal.
+      let end_idx = input.find(|c| !is_name_code_point(c)).unwrap_or_else(|| input.len());
+      let name = input[0..end_idx].to_owned();
+      *input = &input[end_idx..];
+      Ok(SyntaxComponentKind::Literal(name))
+    } else {
+      return Err(());
+    }
+  }
+}
+
+#[inline]
+fn is_ident_start(c: char) -> bool {
+  // https://drafts.csswg.org/css-syntax-3/#ident-start-code-point
+  c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z' || c >= '\u{80}' || c == '_'
+}
+
+#[inline]
+fn is_name_code_point(c: char) -> bool {
+  // https://drafts.csswg.org/css-syntax-3/#ident-code-point
+  is_ident_start(c) || c >= '0' && c <= '9' || c == '-'
+}
+
+impl<'i> Parse<'i> for SyntaxString {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let string = input.expect_string_cloned()?;
+    SyntaxString::parse_string(string.as_ref()).map_err(|_| input.new_custom_error(ParserError::InvalidValue))
+  }
+}
+
+impl ToCss for SyntaxString {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    dest.write_char('"')?;
+    match self {
+      SyntaxString::Universal => dest.write_char('*')?,
+      SyntaxString::Components(components) => {
+        let mut first = true;
+        for component in components {
+          if first {
+            first = false;
+          } else {
+            dest.delim('|', true)?;
+          }
+
+          component.to_css(dest)?;
+        }
+      }
+    }
+
+    dest.write_char('"')
+  }
+}
+
+impl ToCss for SyntaxComponent {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    self.kind.to_css(dest)?;
+    match self.multiplier {
+      Multiplier::None => Ok(()),
+      Multiplier::Comma => dest.write_char('#'),
+      Multiplier::Space => dest.write_char('+'),
+    }
+  }
+}
+
+impl ToCss for SyntaxComponentKind {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    use SyntaxComponentKind::*;
+    let s = match self {
+      Length => "<length>",
+      Number => "<number>",
+      Percentage => "<percentage>",
+      LengthPercentage => "<length-percentage>",
+      Color => "<color>",
+      Image => "<image>",
+      Url => "<url>",
+      Integer => "<integer>",
+      Angle => "<angle>",
+      Time => "<time>",
+      Resolution => "<resolution>",
+      TransformFunction => "<transform-function>",
+      TransformList => "<transform-list>",
+      CustomIdent => "<custom-ident>",
+      Literal(l) => l,
+    };
+    dest.write_str(s)
+  }
+}
+
+impl<'i> ToCss for ParsedComponent<'i> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    use ParsedComponent::*;
+    match self {
+      Length(v) => v.to_css(dest),
+      Number(v) => v.to_css(dest),
+      Percentage(v) => v.to_css(dest),
+      LengthPercentage(v) => v.to_css(dest),
+      Color(v) => v.to_css(dest),
+      Image(v) => v.to_css(dest),
+      Url(v) => v.to_css(dest),
+      Integer(v) => v.to_css(dest),
+      Angle(v) => v.to_css(dest),
+      Time(v) => v.to_css(dest),
+      Resolution(v) => v.to_css(dest),
+      TransformFunction(v) => v.to_css(dest),
+      TransformList(v) => v.to_css(dest),
+      CustomIdent(v) => v.to_css(dest),
+      Literal(v) => v.to_css(dest),
+      Repeated { components, multiplier } => {
+        let mut first = true;
+        for component in components {
+          if first {
+            first = false;
+          } else {
+            match multiplier {
+              Multiplier::Comma => dest.delim(',', false)?,
+              Multiplier::Space => dest.write_char(' ')?,
+              Multiplier::None => unreachable!(),
+            }
+          }
+
+          component.to_css(dest)?;
+        }
+        Ok(())
+      }
+      TokenList(t) => t.to_css(dest, false),
+    }
+  }
+}
+
+#[cfg(test)]
+mod tests {
+  use crate::values::color::RGBA;
+
+  use super::*;
+
+  fn test(source: &str, test: &str, expected: ParsedComponent) {
+    let parsed = SyntaxString::parse_string(source).unwrap();
+
+    let mut input = ParserInput::new(test);
+    let mut parser = Parser::new(&mut input);
+    let value = parsed.parse_value(&mut parser).unwrap();
+    assert_eq!(value, expected);
+  }
+
+  fn parse_error_test(source: &str) {
+    let res = SyntaxString::parse_string(source);
+    match res {
+      Ok(_) => unreachable!(),
+      Err(_) => {}
+    }
+  }
+
+  fn error_test(source: &str, test: &str) {
+    let parsed = SyntaxString::parse_string(source).unwrap();
+    let mut input = ParserInput::new(test);
+    let mut parser = Parser::new(&mut input);
+    let res = parsed.parse_value(&mut parser);
+    match res {
+      Ok(_) => unreachable!(),
+      Err(_) => {}
+    }
+  }
+
+  #[test]
+  fn test_syntax() {
+    test(
+      "foo | <color>+ | <integer>",
+      "foo",
+      ParsedComponent::Literal("foo".into()),
+    );
+
+    test("foo|<color>+|<integer>", "foo", ParsedComponent::Literal("foo".into()));
+
+    test("foo | <color>+ | <integer>", "2", ParsedComponent::Integer(2));
+
+    test(
+      "foo | <color>+ | <integer>",
+      "red",
+      ParsedComponent::Repeated {
+        components: vec![ParsedComponent::Color(values::color::CssColor::RGBA(RGBA {
+          red: 255,
+          green: 0,
+          blue: 0,
+          alpha: 255,
+        }))],
+        multiplier: Multiplier::Space,
+      },
+    );
+
+    test(
+      "foo | <color>+ | <integer>",
+      "red blue",
+      ParsedComponent::Repeated {
+        components: vec![
+          ParsedComponent::Color(values::color::CssColor::RGBA(RGBA {
+            red: 255,
+            green: 0,
+            blue: 0,
+            alpha: 255,
+          })),
+          ParsedComponent::Color(values::color::CssColor::RGBA(RGBA {
+            red: 0,
+            green: 0,
+            blue: 255,
+            alpha: 255,
+          })),
+        ],
+        multiplier: Multiplier::Space,
+      },
+    );
+
+    error_test("foo | <color>+ | <integer>", "2.5");
+
+    error_test("foo | <color>+ | <integer>", "25px");
+
+    error_test("foo | <color>+ | <integer>", "red, green");
+
+    test(
+      "foo | <color># | <integer>",
+      "red, blue",
+      ParsedComponent::Repeated {
+        components: vec![
+          ParsedComponent::Color(values::color::CssColor::RGBA(RGBA {
+            red: 255,
+            green: 0,
+            blue: 0,
+            alpha: 255,
+          })),
+          ParsedComponent::Color(values::color::CssColor::RGBA(RGBA {
+            red: 0,
+            green: 0,
+            blue: 255,
+            alpha: 255,
+          })),
+        ],
+        multiplier: Multiplier::Comma,
+      },
+    );
+
+    error_test("foo | <color># | <integer>", "red green");
+
+    test(
+      "<length>",
+      "25px",
+      ParsedComponent::Length(values::length::Length::Value(values::length::LengthValue::Px(25.0))),
+    );
+
+    test(
+      "<length>",
+      "calc(25px + 25px)",
+      ParsedComponent::Length(values::length::Length::Value(values::length::LengthValue::Px(50.0))),
+    );
+
+    test(
+      "<length> | <percentage>",
+      "25px",
+      ParsedComponent::Length(values::length::Length::Value(values::length::LengthValue::Px(25.0))),
+    );
+
+    test(
+      "<length> | <percentage>",
+      "25%",
+      ParsedComponent::Percentage(values::percentage::Percentage(0.25)),
+    );
+
+    error_test("<length> | <percentage>", "calc(100% - 25px)");
+
+    test("foo | bar | baz", "bar", ParsedComponent::Literal("bar".into()));
+
+    test(
+      "<custom-ident>",
+      "hi",
+      ParsedComponent::CustomIdent(values::ident::CustomIdent("hi".into())),
+    );
+
+    parse_error_test("<transform-list>#");
+    parse_error_test("<color");
+    parse_error_test("color>");
+  }
+}
diff --git a/src/values/time.rs b/src/values/time.rs
new file mode 100644
index 0000000..11e67b3
--- /dev/null
+++ b/src/values/time.rs
@@ -0,0 +1,207 @@
+//! CSS time values.
+
+use super::angle::impl_try_from_angle;
+use super::calc::Calc;
+use super::number::CSSNumber;
+use crate::error::{ParserError, PrinterError};
+use crate::printer::Printer;
+use crate::traits::private::AddInternal;
+use crate::traits::{impl_op, Map, Op, Parse, Sign, ToCss, Zero};
+#[cfg(feature = "visitor")]
+use crate::visitor::Visit;
+use cssparser::*;
+
+/// A CSS [`<time>`](https://www.w3.org/TR/css-values-4/#time) value, in either
+/// seconds or milliseconds.
+///
+/// Time values may be explicit or computed by `calc()`, but are always stored and serialized
+/// as their computed value.
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "visitor", visit(visit_time, TIMES))]
+#[cfg_attr(
+  feature = "serde",
+  derive(serde::Serialize, serde::Deserialize),
+  serde(tag = "type", content = "value", rename_all = "kebab-case")
+)]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+pub enum Time {
+  /// A time in seconds.
+  Seconds(CSSNumber),
+  /// A time in milliseconds.
+  Milliseconds(CSSNumber),
+}
+
+impl Time {
+  /// Returns the time in milliseconds.
+  pub fn to_ms(&self) -> CSSNumber {
+    match self {
+      Time::Seconds(s) => s * 1000.0,
+      Time::Milliseconds(ms) => *ms,
+    }
+  }
+}
+
+impl Zero for Time {
+  fn zero() -> Self {
+    Time::Milliseconds(0.0)
+  }
+
+  fn is_zero(&self) -> bool {
+    match self {
+      Time::Seconds(s) => s.is_zero(),
+      Time::Milliseconds(s) => s.is_zero(),
+    }
+  }
+}
+
+impl<'i> Parse<'i> for Time {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    match input.try_parse(Calc::parse) {
+      Ok(Calc::Value(v)) => return Ok(*v),
+      // Time is always compatible, so they will always compute to a value.
+      Ok(_) => return Err(input.new_custom_error(ParserError::InvalidValue)),
+      _ => {}
+    }
+
+    let location = input.current_source_location();
+    match *input.next()? {
+      Token::Dimension { value, ref unit, .. } => {
+        match_ignore_ascii_case! { unit,
+          "s" => Ok(Time::Seconds(value)),
+          "ms" => Ok(Time::Milliseconds(value)),
+          _ => Err(location.new_unexpected_token_error(Token::Ident(unit.clone())))
+        }
+      }
+      ref t => Err(location.new_unexpected_token_error(t.clone())),
+    }
+  }
+}
+
+impl<'i> TryFrom<&Token<'i>> for Time {
+  type Error = ();
+
+  fn try_from(token: &Token) -> Result<Self, Self::Error> {
+    match token {
+      Token::Dimension { value, ref unit, .. } => match_ignore_ascii_case! { unit,
+        "s" => Ok(Time::Seconds(*value)),
+        "ms" => Ok(Time::Milliseconds(*value)),
+        _ => Err(()),
+      },
+      _ => Err(()),
+    }
+  }
+}
+
+impl ToCss for Time {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    // 0.1s is shorter than 100ms
+    // anything smaller is longer
+    match self {
+      Time::Seconds(s) => {
+        if *s > 0.0 && *s < 0.1 {
+          (*s * 1000.0).to_css(dest)?;
+          dest.write_str("ms")
+        } else {
+          s.to_css(dest)?;
+          dest.write_str("s")
+        }
+      }
+      Time::Milliseconds(ms) => {
+        if *ms == 0.0 || *ms >= 100.0 {
+          (*ms / 1000.0).to_css(dest)?;
+          dest.write_str("s")
+        } else {
+          ms.to_css(dest)?;
+          dest.write_str("ms")
+        }
+      }
+    }
+  }
+}
+
+impl std::convert::Into<Calc<Time>> for Time {
+  fn into(self) -> Calc<Time> {
+    Calc::Value(Box::new(self))
+  }
+}
+
+impl std::convert::TryFrom<Calc<Time>> for Time {
+  type Error = ();
+
+  fn try_from(calc: Calc<Time>) -> Result<Time, Self::Error> {
+    match calc {
+      Calc::Value(v) => Ok(*v),
+      _ => Err(()),
+    }
+  }
+}
+
+impl std::ops::Mul<f32> for Time {
+  type Output = Self;
+
+  fn mul(self, other: f32) -> Time {
+    match self {
+      Time::Seconds(t) => Time::Seconds(t * other),
+      Time::Milliseconds(t) => Time::Milliseconds(t * other),
+    }
+  }
+}
+
+impl AddInternal for Time {
+  fn add(self, other: Self) -> Self {
+    self + other
+  }
+}
+
+impl std::cmp::PartialOrd<Time> for Time {
+  fn partial_cmp(&self, other: &Time) -> Option<std::cmp::Ordering> {
+    self.to_ms().partial_cmp(&other.to_ms())
+  }
+}
+
+impl Op for Time {
+  fn op<F: FnOnce(f32, f32) -> f32>(&self, to: &Self, op: F) -> Self {
+    match (self, to) {
+      (Time::Seconds(a), Time::Seconds(b)) => Time::Seconds(op(*a, *b)),
+      (Time::Milliseconds(a), Time::Milliseconds(b)) => Time::Milliseconds(op(*a, *b)),
+      (Time::Seconds(a), Time::Milliseconds(b)) => Time::Seconds(op(*a, b / 1000.0)),
+      (Time::Milliseconds(a), Time::Seconds(b)) => Time::Milliseconds(op(*a, b * 1000.0)),
+    }
+  }
+
+  fn op_to<T, F: FnOnce(f32, f32) -> T>(&self, rhs: &Self, op: F) -> T {
+    match (self, rhs) {
+      (Time::Seconds(a), Time::Seconds(b)) => op(*a, *b),
+      (Time::Milliseconds(a), Time::Milliseconds(b)) => op(*a, *b),
+      (Time::Seconds(a), Time::Milliseconds(b)) => op(*a, b / 1000.0),
+      (Time::Milliseconds(a), Time::Seconds(b)) => op(*a, b * 1000.0),
+    }
+  }
+}
+
+impl Map for Time {
+  fn map<F: FnOnce(f32) -> f32>(&self, op: F) -> Self {
+    match self {
+      Time::Seconds(t) => Time::Seconds(op(*t)),
+      Time::Milliseconds(t) => Time::Milliseconds(op(*t)),
+    }
+  }
+}
+
+impl Sign for Time {
+  fn sign(&self) -> f32 {
+    match self {
+      Time::Seconds(v) | Time::Milliseconds(v) => v.sign(),
+    }
+  }
+}
+
+impl_op!(Time, std::ops::Rem, rem);
+impl_op!(Time, std::ops::Add, add);
+
+impl_try_from_angle!(Time);
diff --git a/src/values/url.rs b/src/values/url.rs
new file mode 100644
index 0000000..cebdeed
--- /dev/null
+++ b/src/values/url.rs
@@ -0,0 +1,134 @@
+//! CSS url() values.
+
+use crate::dependencies::{Dependency, Location, UrlDependency};
+use crate::error::{ParserError, PrinterError};
+use crate::printer::Printer;
+use crate::traits::{Parse, ToCss};
+use crate::values::string::CowArcStr;
+#[cfg(feature = "visitor")]
+use crate::visitor::Visit;
+use cssparser::*;
+
+/// A CSS [url()](https://www.w3.org/TR/css-values-4/#urls) value and its source location.
+#[derive(Debug, Clone)]
+#[cfg_attr(feature = "visitor", derive(Visit))]
+#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+#[cfg_attr(feature = "visitor", visit(visit_url, URLS))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
+pub struct Url<'i> {
+  /// The url string.
+  #[cfg_attr(feature = "serde", serde(borrow))]
+  pub url: CowArcStr<'i>,
+  /// The location where the `url()` was seen in the CSS source file.
+  pub loc: Location,
+}
+
+impl<'i> PartialEq for Url<'i> {
+  fn eq(&self, other: &Self) -> bool {
+    self.url == other.url
+  }
+}
+
+impl<'i> Parse<'i> for Url<'i> {
+  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
+    let loc = input.current_source_location();
+    let url = input.expect_url()?.into();
+    Ok(Url { url, loc: loc.into() })
+  }
+}
+
+impl<'i> ToCss for Url<'i> {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    let dep = if dest.dependencies.is_some() {
+      Some(UrlDependency::new(self, dest.filename()))
+    } else {
+      None
+    };
+
+    // If adding dependencies, always write url() with quotes so that the placeholder can
+    // be replaced without escaping more easily. Quotes may be removed later during minification.
+    if let Some(dep) = dep {
+      dest.write_str("url(")?;
+      serialize_string(&dep.placeholder, dest)?;
+      dest.write_char(')')?;
+
+      if let Some(dependencies) = &mut dest.dependencies {
+        dependencies.push(Dependency::Url(dep))
+      }
+
+      return Ok(());
+    }
+
+    use cssparser::ToCss;
+    if dest.minify {
+      let mut buf = String::new();
+      Token::UnquotedUrl(CowRcStr::from(self.url.as_ref())).to_css(&mut buf)?;
+
+      // If the unquoted url is longer than it would be quoted (e.g. `url("...")`)
+      // then serialize as a string and choose the shorter version.
+      if buf.len() > self.url.len() + 7 {
+        let mut buf2 = String::new();
+        serialize_string(&self.url, &mut buf2)?;
+        if buf2.len() + 5 < buf.len() {
+          dest.write_str("url(")?;
+          dest.write_str(&buf2)?;
+          return dest.write_char(')');
+        }
+      }
+
+      dest.write_str(&buf)?;
+    } else {
+      dest.write_str("url(")?;
+      serialize_string(&self.url, dest)?;
+      dest.write_char(')')?;
+    }
+
+    Ok(())
+  }
+}
+
+impl<'i> Url<'i> {
+  /// Returns whether the URL is absolute, and not relative.
+  pub fn is_absolute(&self) -> bool {
+    let url = self.url.as_ref();
+
+    // Quick checks. If the url starts with '.', it is relative.
+    if url.starts_with('.') {
+      return false;
+    }
+
+    // If the url starts with '/' it is absolute.
+    if url.starts_with('/') {
+      return true;
+    }
+
+    // If the url starts with '#' we have a fragment URL.
+    // These are resolved relative to the document rather than the CSS file.
+    // https://drafts.csswg.org/css-values-4/#local-urls
+    if url.starts_with('#') {
+      return true;
+    }
+
+    // Otherwise, we might have a scheme. These must start with an ascii alpha character.
+    // https://url.spec.whatwg.org/#scheme-start-state
+    if !url.starts_with(|c| matches!(c, 'a'..='z' | 'A'..='Z')) {
+      return false;
+    }
+
+    // https://url.spec.whatwg.org/#scheme-state
+    for b in url.as_bytes() {
+      let c = *b as char;
+      match c {
+        'a'..='z' | 'A'..='Z' | '0'..='9' | '+' | '-' | '.' => {}
+        ':' => return true,
+        _ => break,
+      }
+    }
+
+    false
+  }
+}
diff --git a/src/vendor_prefix.rs b/src/vendor_prefix.rs
new file mode 100644
index 0000000..e6353df
--- /dev/null
+++ b/src/vendor_prefix.rs
@@ -0,0 +1,186 @@
+//! Vendor prefixes.
+
+#![allow(non_upper_case_globals)]
+
+use crate::error::PrinterError;
+use crate::printer::Printer;
+use crate::traits::ToCss;
+#[cfg(feature = "visitor")]
+use crate::visitor::{Visit, VisitTypes, Visitor};
+use bitflags::bitflags;
+
+bitflags! {
+  /// Bit flags that represent one or more vendor prefixes, such as
+  /// `-webkit` or `-moz`.
+  ///
+  /// Multiple flags can be combined to represent
+  /// more than one prefix. During printing, the rule or property will
+  /// be duplicated for each prefix flag that is enabled. This enables
+  /// vendor prefixes to be added without increasing memory usage.
+  #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Clone, Copy)]
+  #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
+  pub struct VendorPrefix: u8 {
+    /// The `-webkit` vendor prefix.
+    const WebKit = 0b00000010;
+    /// The `-moz` vendor prefix.
+    const Moz    = 0b00000100;
+    /// The `-ms` vendor prefix.
+    const Ms     = 0b00001000;
+    /// The `-o` vendor prefix.
+    const O      = 0b00010000;
+    /// No vendor prefixes.
+    const None   = 0b00000001;
+  }
+}
+
+impl Default for VendorPrefix {
+  fn default() -> VendorPrefix {
+    VendorPrefix::None
+  }
+}
+
+impl VendorPrefix {
+  /// Returns a vendor prefix flag from a prefix string (without the leading `-`).
+  pub fn from_str(s: &str) -> VendorPrefix {
+    match s {
+      "webkit" => VendorPrefix::WebKit,
+      "moz" => VendorPrefix::Moz,
+      "ms" => VendorPrefix::Ms,
+      "o" => VendorPrefix::O,
+      _ => unreachable!(),
+    }
+  }
+
+  /// Returns VendorPrefix::None if empty.
+  #[inline]
+  pub fn or_none(self) -> Self {
+    self.or(VendorPrefix::None)
+  }
+
+  /// Returns `other` if `self` is empty
+  #[inline]
+  pub fn or(self, other: Self) -> Self {
+    if self.is_empty() {
+      other
+    } else {
+      self
+    }
+  }
+}
+
+impl ToCss for VendorPrefix {
+  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
+  where
+    W: std::fmt::Write,
+  {
+    cssparser::ToCss::to_css(self, dest)?;
+    Ok(())
+  }
+}
+
+impl cssparser::ToCss for VendorPrefix {
+  fn to_css<W>(&self, dest: &mut W) -> std::fmt::Result
+  where
+    W: std::fmt::Write,
+  {
+    match *self {
+      VendorPrefix::WebKit => dest.write_str("-webkit-"),
+      VendorPrefix::Moz => dest.write_str("-moz-"),
+      VendorPrefix::Ms => dest.write_str("-ms-"),
+      VendorPrefix::O => dest.write_str("-o-"),
+      _ => Ok(()),
+    }
+  }
+}
+
+#[cfg(feature = "serde")]
+#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
+impl serde::Serialize for VendorPrefix {
+  fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+  where
+    S: serde::Serializer,
+  {
+    let mut values = Vec::new();
+    if *self != VendorPrefix::None {
+      if self.contains(VendorPrefix::None) {
+        values.push("none");
+      }
+      if self.contains(VendorPrefix::WebKit) {
+        values.push("webkit");
+      }
+      if self.contains(VendorPrefix::Moz) {
+        values.push("moz");
+      }
+      if self.contains(VendorPrefix::Ms) {
+        values.push("ms");
+      }
+      if self.contains(VendorPrefix::O) {
+        values.push("o");
+      }
+    }
+    values.serialize(serializer)
+  }
+}
+
+#[cfg(feature = "serde")]
+#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
+impl<'de> serde::Deserialize<'de> for VendorPrefix {
+  fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+  where
+    D: serde::Deserializer<'de>,
+  {
+    use crate::values::string::CowArcStr;
+    let values = Vec::<CowArcStr<'de>>::deserialize(deserializer)?;
+    if values.is_empty() {
+      return Ok(VendorPrefix::None);
+    }
+    let mut res = VendorPrefix::empty();
+    for value in values {
+      res |= match value.as_ref() {
+        "none" => VendorPrefix::None,
+        "webkit" => VendorPrefix::WebKit,
+        "moz" => VendorPrefix::Moz,
+        "ms" => VendorPrefix::Ms,
+        "o" => VendorPrefix::O,
+        _ => continue,
+      };
+    }
+    Ok(res)
+  }
+}
+
+#[cfg(feature = "visitor")]
+#[cfg_attr(docsrs, doc(cfg(feature = "visitor")))]
+impl<'i, V: ?Sized + Visitor<'i, T>, T: Visit<'i, T, V>> Visit<'i, T, V> for VendorPrefix {
+  const CHILD_TYPES: VisitTypes = VisitTypes::empty();
+  fn visit_children(&mut self, _: &mut V) -> Result<(), V::Error> {
+    Ok(())
+  }
+}
+
+#[cfg(feature = "jsonschema")]
+#[cfg_attr(docsrs, doc(cfg(feature = "jsonschema")))]
+impl schemars::JsonSchema for VendorPrefix {
+  fn is_referenceable() -> bool {
+    true
+  }
+
+  fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
+    #[derive(schemars::JsonSchema)]
+    #[schemars(rename_all = "lowercase")]
+    #[allow(dead_code)]
+    enum Prefix {
+      None,
+      WebKit,
+      Moz,
+      Ms,
+      O,
+    }
+
+    Vec::<Prefix>::json_schema(gen)
+  }
+
+  fn schema_name() -> String {
+    "VendorPrefix".into()
+  }
+}
diff --git a/src/visitor.rs b/src/visitor.rs
new file mode 100644
index 0000000..beb04d2
--- /dev/null
+++ b/src/visitor.rs
@@ -0,0 +1,431 @@
+//! Visitors for traversing the values in a StyleSheet.
+//!
+//! The [Visitor](Visitor) trait includes methods for visiting and transforming rules, properties, and values within a StyleSheet.
+//! Each value implements the [Visit](Visit) trait, which knows how to visit the value itself, as well as its children.
+//! A Visitor is configured to only visit specific types of values using [VisitTypes](VisitTypes) flags. This enables
+//! entire branches to be skipped when a type does not contain any relevant values.
+//!
+//! # Example
+//!
+//! This example transforms a stylesheet, adding a prefix to all URLs, and converting pixels to rems.
+//!
+//! ```
+//! use std::convert::Infallible;
+//! use lightningcss::{
+//!   stylesheet::{StyleSheet, ParserOptions, PrinterOptions},
+//!   visitor::{Visitor, Visit, VisitTypes},
+//!   visit_types,
+//!   values::length::LengthValue,
+//!   values::url::Url
+//! };
+//!
+//! let mut stylesheet = StyleSheet::parse(
+//!   r#"
+//!     .foo {
+//!       background: url(bg.png);
+//!       width: 32px;
+//!     }
+//!   "#,
+//!   ParserOptions::default()
+//! ).unwrap();
+//!
+//! struct MyVisitor;
+//! impl<'i> Visitor<'i> for MyVisitor {
+//!   type Error = Infallible;
+//!
+//!   fn visit_types(&self) -> VisitTypes {
+//!     visit_types!(URLS | LENGTHS)
+//!   }
+//!
+//!   fn visit_url(&mut self, url: &mut Url<'i>) -> Result<(), Self::Error> {
+//!     url.url = format!("https://mywebsite.com/{}", url.url).into();
+//!     Ok(())
+//!   }
+//!
+//!   fn visit_length(&mut self, length: &mut LengthValue) -> Result<(), Self::Error> {
+//!     match length {
+//!       LengthValue::Px(px) => *length = LengthValue::Rem(*px / 16.0),
+//!       _ => {}
+//!     }
+//!
+//!     Ok(())
+//!   }
+//! }
+//!
+//! stylesheet.visit(&mut MyVisitor).unwrap();
+//!
+//! let res = stylesheet.to_css(PrinterOptions { minify: true, ..Default::default() }).unwrap();
+//! assert_eq!(res.code, ".foo{background:url(https://mywebsite.com/bg.png);width:2rem}");
+//! ```
+
+use crate::{
+  declaration::DeclarationBlock,
+  media_query::{MediaFeature, MediaFeatureValue, MediaList, MediaQuery},
+  parser::DefaultAtRule,
+  properties::{
+    custom::{EnvironmentVariable, Function, TokenList, TokenOrValue, Variable},
+    Property,
+  },
+  rules::{supports::SupportsCondition, CssRule, CssRuleList},
+  selector::{Selector, SelectorList},
+  stylesheet::StyleSheet,
+  values::{
+    angle::Angle,
+    color::CssColor,
+    ident::{CustomIdent, DashedIdent},
+    image::Image,
+    length::LengthValue,
+    ratio::Ratio,
+    resolution::Resolution,
+    time::Time,
+    url::Url,
+  },
+};
+use bitflags::bitflags;
+use indexmap::IndexMap;
+use smallvec::SmallVec;
+
+pub(crate) use lightningcss_derive::Visit;
+
+bitflags! {
+  /// Describes what a [Visitor](Visitor) will visit when traversing a StyleSheet.
+  ///
+  /// Flags may be combined to visit multiple types. The [visit_types](visit_types) macro allows
+  /// combining flags in a `const` expression.
+  #[derive(PartialEq, Eq, Clone, Copy)]
+  pub struct VisitTypes: u32 {
+    /// Visit rules.
+    const RULES = 1 << 0;
+    /// Visit properties;
+    const PROPERTIES = 1 << 1;
+    /// Visit urls.
+    const URLS = 1 << 2;
+    /// Visit colors.
+    const COLORS = 1 << 3;
+    /// Visit images.
+    const IMAGES = 1 << 4;
+    /// Visit lengths.
+    const LENGTHS = 1 << 5;
+    /// Visit angles.
+    const ANGLES = 1 << 6;
+    /// Visit ratios.
+    const RATIOS = 1 << 7;
+    /// Visit resolutions.
+    const RESOLUTIONS = 1 << 8;
+    /// Visit times.
+    const TIMES = 1 << 9;
+    /// Visit custom identifiers.
+    const CUSTOM_IDENTS = 1 << 10;
+    /// Visit dashed identifiers.
+    const DASHED_IDENTS = 1 << 11;
+    /// Visit variables.
+    const VARIABLES = 1 << 12;
+    /// Visit environment variables.
+    const ENVIRONMENT_VARIABLES = 1 << 13;
+    /// Visit media queries.
+    const MEDIA_QUERIES = 1 << 14;
+    /// Visit supports conditions.
+    const SUPPORTS_CONDITIONS = 1 << 15;
+    /// Visit selectors.
+    const SELECTORS = 1 << 16;
+    /// Visit custom functions.
+    const FUNCTIONS = 1 << 17;
+    /// Visit a token.
+    const TOKENS = 1 << 18;
+  }
+}
+
+/// Constructs a constant [VisitTypes](VisitTypes) from flags.
+#[macro_export]
+macro_rules! visit_types {
+  ($( $flag: ident )|+) => {
+    $crate::visitor::VisitTypes::from_bits_truncate(0 $(| $crate::visitor::VisitTypes::$flag.bits())+)
+  }
+}
+
+/// A trait for visiting or transforming rules, properties, and values in a StyleSheet.
+pub trait Visitor<'i, T: Visit<'i, T, Self> = DefaultAtRule> {
+  /// The `Err` value for `Result`s returned by `visit_*` methods.
+  type Error;
+
+  /// Returns the types of values that this visitor should visit. By default, it returns
+  /// `Self::TYPES`, but this can be overridden to change the value at runtime.
+  fn visit_types(&self) -> VisitTypes;
+
+  /// Visits a stylesheet.
+  #[inline]
+  fn visit_stylesheet<'o>(&mut self, stylesheet: &mut StyleSheet<'i, 'o, T>) -> Result<(), Self::Error> {
+    stylesheet.visit_children(self)
+  }
+
+  /// Visits a rule list.
+  #[inline]
+  fn visit_rule_list(&mut self, rules: &mut CssRuleList<'i, T>) -> Result<(), Self::Error> {
+    rules.visit_children(self)
+  }
+
+  /// Visits a rule.
+  #[inline]
+  fn visit_rule(&mut self, rule: &mut CssRule<'i, T>) -> Result<(), Self::Error> {
+    rule.visit_children(self)
+  }
+
+  /// Visits a declaration block.
+  #[inline]
+  fn visit_declaration_block(&mut self, decls: &mut DeclarationBlock<'i>) -> Result<(), Self::Error> {
+    decls.visit_children(self)
+  }
+
+  /// Visits a property.
+  #[inline]
+  fn visit_property(&mut self, property: &mut Property<'i>) -> Result<(), Self::Error> {
+    property.visit_children(self)
+  }
+
+  /// Visits a url.
+  fn visit_url(&mut self, _url: &mut Url<'i>) -> Result<(), Self::Error> {
+    Ok(())
+  }
+
+  /// Visits a color.
+  #[allow(unused_variables)]
+  fn visit_color(&mut self, color: &mut CssColor) -> Result<(), Self::Error> {
+    Ok(())
+  }
+
+  /// Visits an image.
+  #[inline]
+  fn visit_image(&mut self, image: &mut Image<'i>) -> Result<(), Self::Error> {
+    image.visit_children(self)
+  }
+
+  /// Visits a length.
+  #[allow(unused_variables)]
+  fn visit_length(&mut self, length: &mut LengthValue) -> Result<(), Self::Error> {
+    Ok(())
+  }
+
+  /// Visits an angle.
+  #[allow(unused_variables)]
+  fn visit_angle(&mut self, angle: &mut Angle) -> Result<(), Self::Error> {
+    Ok(())
+  }
+
+  /// Visits a ratio.
+  #[allow(unused_variables)]
+  fn visit_ratio(&mut self, ratio: &mut Ratio) -> Result<(), Self::Error> {
+    Ok(())
+  }
+
+  /// Visits a resolution.
+  #[allow(unused_variables)]
+  fn visit_resolution(&mut self, resolution: &mut Resolution) -> Result<(), Self::Error> {
+    Ok(())
+  }
+
+  /// Visits a time.
+  #[allow(unused_variables)]
+  fn visit_time(&mut self, time: &mut Time) -> Result<(), Self::Error> {
+    Ok(())
+  }
+
+  /// Visits a custom ident.
+  #[allow(unused_variables)]
+  fn visit_custom_ident(&mut self, ident: &mut CustomIdent) -> Result<(), Self::Error> {
+    Ok(())
+  }
+
+  /// Visits a dashed ident.
+  #[allow(unused_variables)]
+  fn visit_dashed_ident(&mut self, ident: &mut DashedIdent) -> Result<(), Self::Error> {
+    Ok(())
+  }
+
+  /// Visits a variable reference.
+  #[inline]
+  fn visit_variable(&mut self, var: &mut Variable<'i>) -> Result<(), Self::Error> {
+    var.visit_children(self)
+  }
+
+  /// Visits an environment variable reference.
+  #[inline]
+  fn visit_environment_variable(&mut self, env: &mut EnvironmentVariable<'i>) -> Result<(), Self::Error> {
+    env.visit_children(self)
+  }
+
+  /// Visits a media query list.
+  #[inline]
+  fn visit_media_list(&mut self, media: &mut MediaList<'i>) -> Result<(), Self::Error> {
+    media.visit_children(self)
+  }
+
+  /// Visits a media query.
+  #[inline]
+  fn visit_media_query(&mut self, query: &mut MediaQuery<'i>) -> Result<(), Self::Error> {
+    query.visit_children(self)
+  }
+
+  /// Visits a media feature.
+  #[inline]
+  fn visit_media_feature(&mut self, feature: &mut MediaFeature<'i>) -> Result<(), Self::Error> {
+    feature.visit_children(self)
+  }
+
+  /// Visits a media feature value.
+  #[inline]
+  fn visit_media_feature_value(&mut self, value: &mut MediaFeatureValue<'i>) -> Result<(), Self::Error> {
+    value.visit_children(self)
+  }
+
+  /// Visits a supports condition.
+  #[inline]
+  fn visit_supports_condition(&mut self, condition: &mut SupportsCondition<'i>) -> Result<(), Self::Error> {
+    condition.visit_children(self)
+  }
+
+  /// Visits a selector list.
+  #[inline]
+  fn visit_selector_list(&mut self, selectors: &mut SelectorList<'i>) -> Result<(), Self::Error> {
+    selectors.visit_children(self)
+  }
+
+  /// Visits a selector.
+  #[allow(unused_variables)]
+  fn visit_selector(&mut self, selector: &mut Selector<'i>) -> Result<(), Self::Error> {
+    Ok(())
+  }
+
+  /// Visits a custom function.
+  #[inline]
+  fn visit_function(&mut self, function: &mut Function<'i>) -> Result<(), Self::Error> {
+    function.visit_children(self)
+  }
+
+  /// Visits a token list.
+  #[inline]
+  fn visit_token_list(&mut self, tokens: &mut TokenList<'i>) -> Result<(), Self::Error> {
+    tokens.visit_children(self)
+  }
+
+  /// Visits a token or value in an unparsed property.
+  #[inline]
+  fn visit_token(&mut self, token: &mut TokenOrValue<'i>) -> Result<(), Self::Error> {
+    token.visit_children(self)
+  }
+}
+
+/// A trait for values that can be visited by a [Visitor](Visitor).
+pub trait Visit<'i, T: Visit<'i, T, V>, V: ?Sized + Visitor<'i, T>> {
+  /// The types of values contained within this value and its children.
+  /// This is used to skip branches that don't have any values requested
+  /// by the Visitor.
+  const CHILD_TYPES: VisitTypes;
+
+  /// Visits the value by calling an appropriate method on the Visitor.
+  /// If no corresponding visitor method exists, then the children are visited.
+  #[inline]
+  fn visit(&mut self, visitor: &mut V) -> Result<(), V::Error> {
+    self.visit_children(visitor)
+  }
+
+  /// Visit the children of this value.
+  fn visit_children(&mut self, visitor: &mut V) -> Result<(), V::Error>;
+}
+
+impl<'i, T: Visit<'i, T, V>, V: ?Sized + Visitor<'i, T>, U: Visit<'i, T, V>> Visit<'i, T, V> for Option<U> {
+  const CHILD_TYPES: VisitTypes = U::CHILD_TYPES;
+
+  fn visit(&mut self, visitor: &mut V) -> Result<(), V::Error> {
+    if let Some(v) = self {
+      v.visit(visitor)
+    } else {
+      Ok(())
+    }
+  }
+
+  fn visit_children(&mut self, visitor: &mut V) -> Result<(), V::Error> {
+    if let Some(v) = self {
+      v.visit_children(visitor)
+    } else {
+      Ok(())
+    }
+  }
+}
+
+impl<'i, T: Visit<'i, T, V>, V: ?Sized + Visitor<'i, T>, U: Visit<'i, T, V>> Visit<'i, T, V> for Box<U> {
+  const CHILD_TYPES: VisitTypes = U::CHILD_TYPES;
+
+  fn visit(&mut self, visitor: &mut V) -> Result<(), V::Error> {
+    self.as_mut().visit(visitor)
+  }
+
+  fn visit_children(&mut self, visitor: &mut V) -> Result<(), V::Error> {
+    self.as_mut().visit_children(visitor)
+  }
+}
+
+impl<'i, T: Visit<'i, T, V>, V: ?Sized + Visitor<'i, T>, U: Visit<'i, T, V>> Visit<'i, T, V> for Vec<U> {
+  const CHILD_TYPES: VisitTypes = U::CHILD_TYPES;
+
+  fn visit(&mut self, visitor: &mut V) -> Result<(), V::Error> {
+    self.iter_mut().try_for_each(|v| v.visit(visitor))
+  }
+
+  fn visit_children(&mut self, visitor: &mut V) -> Result<(), V::Error> {
+    self.iter_mut().try_for_each(|v| v.visit_children(visitor))
+  }
+}
+
+impl<'i, A: smallvec::Array<Item = U>, U: Visit<'i, T, V>, T: Visit<'i, T, V>, V: ?Sized + Visitor<'i, T>>
+  Visit<'i, T, V> for SmallVec<A>
+{
+  const CHILD_TYPES: VisitTypes = U::CHILD_TYPES;
+
+  fn visit(&mut self, visitor: &mut V) -> Result<(), V::Error> {
+    self.iter_mut().try_for_each(|v| v.visit(visitor))
+  }
+
+  fn visit_children(&mut self, visitor: &mut V) -> Result<(), V::Error> {
+    self.iter_mut().try_for_each(|v| v.visit_children(visitor))
+  }
+}
+
+impl<'i, T, V, U, W> Visit<'i, T, V> for IndexMap<U, W>
+where
+  T: Visit<'i, T, V>,
+  V: ?Sized + Visitor<'i, T>,
+  W: Visit<'i, T, V>,
+{
+  const CHILD_TYPES: VisitTypes = W::CHILD_TYPES;
+
+  fn visit(&mut self, visitor: &mut V) -> Result<(), V::Error> {
+    self.iter_mut().try_for_each(|(_k, v)| v.visit(visitor))
+  }
+
+  fn visit_children(&mut self, visitor: &mut V) -> Result<(), V::Error> {
+    self.iter_mut().try_for_each(|(_k, v)| v.visit_children(visitor))
+  }
+}
+
+macro_rules! impl_visit {
+  ($t: ty) => {
+    impl<'i, V: ?Sized + Visitor<'i, T>, T: Visit<'i, T, V>> Visit<'i, T, V> for $t {
+      const CHILD_TYPES: VisitTypes = VisitTypes::empty();
+
+      fn visit_children(&mut self, _: &mut V) -> Result<(), V::Error> {
+        Ok(())
+      }
+    }
+  };
+}
+
+impl_visit!(u8);
+impl_visit!(u16);
+impl_visit!(u32);
+impl_visit!(i32);
+impl_visit!(f32);
+impl_visit!(bool);
+impl_visit!(char);
+impl_visit!(str);
+impl_visit!(String);
+impl_visit!((f32, f32));
diff --git a/static-self-derive/Cargo.toml b/static-self-derive/Cargo.toml
new file mode 100644
index 0000000..7768251
--- /dev/null
+++ b/static-self-derive/Cargo.toml
@@ -0,0 +1,18 @@
+[package]
+name = "static-self-derive"
+version = "0.1.1"
+edition = "2021"
+authors = ["Devon Govett <devongovett@gmail.com>","Donny <kdy1997.dev@gmail.com>"]
+description = "Derive macros for static-self"
+license = "MPL-2.0"
+keywords = [  ]
+repository = "https://github.com/parcel-bundler/lightningcss"
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[lib]
+proc-macro = true
+
+[dependencies]
+syn = { version = "1.0", features = ["extra-traits"] }
+quote = "1.0"
+proc-macro2 = "1.0"
diff --git a/static-self-derive/src/into_owned.rs b/static-self-derive/src/into_owned.rs
new file mode 100644
index 0000000..2b69bd2
--- /dev/null
+++ b/static-self-derive/src/into_owned.rs
@@ -0,0 +1,194 @@
+use proc_macro::{self, TokenStream};
+use proc_macro2::Span;
+use quote::quote;
+use syn::{parse_macro_input, parse_quote, Data, DataEnum, DeriveInput, Field, Fields, Ident, Member};
+
+pub(crate) fn derive_into_owned(input: TokenStream) -> TokenStream {
+  let DeriveInput {
+    ident: self_name,
+    data,
+    mut generics,
+    ..
+  } = parse_macro_input!(input);
+
+  let res = match data {
+    Data::Struct(s) => {
+      let fields = s
+        .fields
+        .iter()
+        .enumerate()
+        .map(|(index, Field { ident, .. })| {
+          let name = ident
+            .as_ref()
+            .map_or_else(|| Member::Unnamed(index.into()), |ident| Member::Named(ident.clone()));
+
+          let value = into_owned(quote! { self.#name });
+          if let Some(ident) = ident {
+            quote! { #ident: #value }
+          } else {
+            value
+          }
+        })
+        .collect::<Vec<proc_macro2::TokenStream>>();
+
+      match s.fields {
+        Fields::Unnamed(_) => {
+          quote! {
+            #self_name(#(#fields),*)
+          }
+        }
+        Fields::Named(_) => {
+          quote! {
+            #self_name { #(#fields),* }
+          }
+        }
+        Fields::Unit => quote! {},
+      }
+    }
+    Data::Enum(DataEnum { variants, .. }) => {
+      let variants = variants
+        .iter()
+        .map(|variant| {
+          let name = &variant.ident;
+          let mut field_names = Vec::new();
+          let mut static_fields = Vec::new();
+          for (index, Field { ident, .. }) in variant.fields.iter().enumerate() {
+            let name = ident.as_ref().map_or_else(
+              || Ident::new(&format!("_{}", index), Span::call_site()),
+              |ident| ident.clone(),
+            );
+            field_names.push(name.clone());
+            let value = into_owned(quote! { #name });
+            static_fields.push(if let Some(ident) = ident {
+              quote! { #ident: #value }
+            } else {
+              value
+            })
+          }
+
+          match variant.fields {
+            Fields::Unnamed(_) => {
+              quote! {
+                #self_name::#name(#(#field_names),*) => {
+                  #self_name::#name(#(#static_fields),*)
+                }
+              }
+            }
+            Fields::Named(_) => {
+              quote! {
+                #self_name::#name { #(#field_names),* } => {
+                  #self_name::#name { #(#static_fields),* }
+                }
+              }
+            }
+            Fields::Unit => quote! {
+              #self_name::#name => #self_name::#name,
+            },
+          }
+        })
+        .collect::<proc_macro2::TokenStream>();
+
+      quote! {
+        match self {
+          #variants
+        }
+      }
+    }
+    _ => {
+      panic!("can only derive IntoOwned for enums and structs")
+    }
+  };
+
+  let orig_generics = generics.clone();
+
+  // Add generic bounds for all type parameters.
+  let mut type_param_names = vec![];
+
+  for ty in generics.type_params() {
+    type_param_names.push(ty.ident.clone());
+  }
+
+  for type_param in type_param_names {
+    generics.make_where_clause().predicates.push_value(parse_quote! {
+      #type_param: ::static_self::IntoOwned<'any>
+    })
+  }
+
+  let has_lifetime = generics
+    .params
+    .first()
+    .map_or(false, |v| matches!(v, syn::GenericParam::Lifetime(..)));
+  let has_generic = !generics.params.is_empty();
+
+  // Prepend `'any` to generics
+  let any = syn::GenericParam::Lifetime(syn::LifetimeDef {
+    attrs: Default::default(),
+    lifetime: syn::Lifetime {
+      apostrophe: Span::call_site(),
+      ident: Ident::new("any", Span::call_site()),
+    },
+    colon_token: None,
+    bounds: Default::default(),
+  });
+  generics.params.insert(0, any.clone());
+
+  let (impl_generics, _, where_clause) = generics.split_for_impl();
+  let (_, ty_generics, _) = orig_generics.split_for_impl();
+
+  let into_owned = if !has_generic {
+    quote! {
+      impl #impl_generics ::static_self::IntoOwned<'any> for #self_name #ty_generics #where_clause {
+        type Owned = Self;
+
+        #[inline]
+        fn into_owned(self) -> Self {
+          self
+        }
+      }
+    }
+  } else {
+    let mut generics_without_default = generics.clone();
+
+    let mut params = Vec::new();
+
+    for p in generics_without_default.params.iter_mut() {
+      if let syn::GenericParam::Type(ty) = p {
+        ty.default = None;
+
+        params.push(quote!(<#ty as static_self::IntoOwned<'any>>::Owned));
+      }
+    }
+
+    if has_lifetime {
+      quote! {
+        impl #impl_generics ::static_self::IntoOwned<'any> for #self_name #ty_generics #where_clause {
+          type Owned = #self_name<'any, #(#params),*>;
+          /// Consumes the value and returns an owned clone.
+          fn into_owned(self) -> Self::Owned {
+            use ::static_self::IntoOwned;
+
+            #res
+          }
+        }
+      }
+    } else {
+      quote! {
+        impl #impl_generics ::static_self::IntoOwned<'any> for #self_name #ty_generics #where_clause {
+          type Owned = #self_name<#(#params),*>;
+          /// Consumes the value and returns an owned clone.
+          fn into_owned(self) -> Self::Owned {
+            use ::static_self::IntoOwned;
+
+            #res
+          }
+        }
+      }
+    }
+  };
+
+  into_owned.into()
+}
+
+fn into_owned(name: proc_macro2::TokenStream) -> proc_macro2::TokenStream {
+  quote! { #name.into_owned() }
+}
diff --git a/static-self-derive/src/lib.rs b/static-self-derive/src/lib.rs
new file mode 100644
index 0000000..8e7f21e
--- /dev/null
+++ b/static-self-derive/src/lib.rs
@@ -0,0 +1,8 @@
+use proc_macro::TokenStream;
+
+mod into_owned;
+
+#[proc_macro_derive(IntoOwned)]
+pub fn derive_into_owned(input: TokenStream) -> TokenStream {
+  into_owned::derive_into_owned(input)
+}
diff --git a/static-self/Cargo.toml b/static-self/Cargo.toml
new file mode 100644
index 0000000..c2c62bb
--- /dev/null
+++ b/static-self/Cargo.toml
@@ -0,0 +1,16 @@
+[package]
+name = "static-self"
+version = "0.1.2"
+edition = "2021"
+authors = ["Devon Govett <devongovett@gmail.com>","Donny <kdy1997.dev@gmail.com>"]
+description = "A trait for values that can be cloned with a static lifetime"
+license = "MPL-2.0"
+keywords = [  ]
+repository = "https://github.com/parcel-bundler/lightningcss"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+smallvec = { version = "1.11.1", optional = true }
+static-self-derive = { version = "0.1.1", path = "../static-self-derive" }
+indexmap = { version = "2.2.6", optional = true }
diff --git a/static-self/src/lib.rs b/static-self/src/lib.rs
new file mode 100644
index 0000000..03fb606
--- /dev/null
+++ b/static-self/src/lib.rs
@@ -0,0 +1,153 @@
+pub use static_self_derive::IntoOwned;
+
+/// A trait for things that can be cloned with a new lifetime.
+///
+/// `'any` lifeitme means the output should have `'static` lifetime.
+pub trait IntoOwned<'any> {
+  /// A variant of `Self` with a new lifetime.
+  type Owned: 'any;
+
+  /// Make lifetime of `self` `'static`.
+  fn into_owned(self) -> Self::Owned;
+}
+
+macro_rules! impl_into_owned {
+  ($t: ty) => {
+    impl<'a> IntoOwned<'a> for $t {
+      type Owned = Self;
+
+      #[inline]
+      fn into_owned(self) -> Self {
+        self
+      }
+    }
+  };
+  ($($t:ty),*) => {
+    $(impl_into_owned!($t);)*
+  };
+}
+
+impl_into_owned!(bool, f32, f64, u8, u16, u32, u64, u128, i8, i16, i32, i64, i128, usize, isize, char, String);
+
+macro_rules! impl_tuple {
+  (
+    $($name:ident),*
+  ) =>{
+    #[allow(non_snake_case)]
+    impl<'any, $($name,)*> IntoOwned<'any> for ($($name,)*)
+    where
+        $($name: IntoOwned<'any>),*
+    {
+      type Owned = ($(<$name as IntoOwned<'any>>::Owned,)*);
+
+      #[inline]
+      fn into_owned(self) -> Self::Owned {
+        let ($($name,)*) = self;
+        ($($name.into_owned(),)*)
+      }
+    }
+  };
+}
+
+macro_rules! call_impl_tuple {
+  () => {};
+  ($first:ident) => {
+    impl_tuple!($first);
+  };
+  (
+    $first:ident,
+    $($name:ident),*
+  ) => {
+    call_impl_tuple!($($name),*);
+    impl_tuple!($first, $($name),*);
+  };
+}
+
+call_impl_tuple!(A, B, C, D, E, F, G, H, I, J, K, L);
+
+impl<'any, T> IntoOwned<'any> for Vec<T>
+where
+  T: IntoOwned<'any>,
+{
+  type Owned = Vec<<T as IntoOwned<'any>>::Owned>;
+
+  fn into_owned(self) -> Self::Owned {
+    self.into_iter().map(|v| v.into_owned()).collect()
+  }
+}
+impl<'any, T> IntoOwned<'any> for Option<T>
+where
+  T: IntoOwned<'any>,
+{
+  type Owned = Option<<T as IntoOwned<'any>>::Owned>;
+
+  fn into_owned(self) -> Self::Owned {
+    self.map(|v| v.into_owned())
+  }
+}
+
+impl<'any, T> IntoOwned<'any> for Box<T>
+where
+  T: IntoOwned<'any>,
+{
+  type Owned = Box<<T as IntoOwned<'any>>::Owned>;
+
+  fn into_owned(self) -> Self::Owned {
+    Box::new((*self).into_owned())
+  }
+}
+
+impl<'any, T> IntoOwned<'any> for Box<[T]>
+where
+  T: IntoOwned<'any>,
+{
+  type Owned = Box<[<T as IntoOwned<'any>>::Owned]>;
+
+  fn into_owned(self) -> Self::Owned {
+    self.into_vec().into_owned().into_boxed_slice()
+  }
+}
+
+#[cfg(feature = "smallvec")]
+impl<'any, T, const N: usize> IntoOwned<'any> for smallvec::SmallVec<[T; N]>
+where
+  T: IntoOwned<'any>,
+  [T; N]: smallvec::Array<Item = T>,
+  [<T as IntoOwned<'any>>::Owned; N]: smallvec::Array<Item = <T as IntoOwned<'any>>::Owned>,
+{
+  type Owned = smallvec::SmallVec<[<T as IntoOwned<'any>>::Owned; N]>;
+
+  fn into_owned(self) -> Self::Owned {
+    self.into_iter().map(|v| v.into_owned()).collect()
+  }
+}
+
+#[cfg(feature = "indexmap")]
+impl<'any, K, V> IntoOwned<'any> for indexmap::IndexMap<K, V>
+where
+  K: IntoOwned<'any>,
+  V: IntoOwned<'any>,
+  <K as IntoOwned<'any>>::Owned: Eq + std::hash::Hash,
+{
+  type Owned = indexmap::IndexMap<<K as IntoOwned<'any>>::Owned, <V as IntoOwned<'any>>::Owned>;
+
+  fn into_owned(self) -> Self::Owned {
+    self.into_iter().map(|(k, v)| (k.into_owned(), v.into_owned())).collect()
+  }
+}
+
+impl<'any, T, const N: usize> IntoOwned<'any> for [T; N]
+where
+  T: IntoOwned<'any>,
+{
+  type Owned = [<T as IntoOwned<'any>>::Owned; N];
+
+  fn into_owned(self) -> Self::Owned {
+    self
+      .into_iter()
+      .map(|v| v.into_owned())
+      .collect::<Vec<_>>()
+      .try_into()
+      .unwrap_or_else(|_| unreachable!("Vec<T> with N elements should be able to be converted to [T; N]"))
+  }
+}
diff --git a/test-integration.mjs b/test-integration.mjs
new file mode 100644
index 0000000..839a13d
--- /dev/null
+++ b/test-integration.mjs
@@ -0,0 +1,171 @@
+import puppeteer from 'puppeteer';
+import fetch from 'node-fetch';
+import assert from 'assert';
+import {diff} from 'jest-diff';
+import * as css from 'lightningcss';
+
+let urls = [
+  'https://getbootstrap.com/docs/5.1/examples/headers/',
+  'https://getbootstrap.com/docs/5.1/examples/heroes/',
+  'https://getbootstrap.com/docs/5.1/examples/features/',
+  'https://getbootstrap.com/docs/5.1/examples/sidebars/',
+  'https://getbootstrap.com/docs/5.1/examples/footers/',
+  'https://getbootstrap.com/docs/5.1/examples/dropdowns/',
+  'https://getbootstrap.com/docs/5.1/examples/list-groups/',
+  'https://getbootstrap.com/docs/5.1/examples/modals/',
+  'http://csszengarden.com',
+  'http://csszengarden.com/221/',
+  'http://csszengarden.com/219/',
+  'http://csszengarden.com/218/',
+  'http://csszengarden.com/217/',
+  'http://csszengarden.com/216/',
+  'http://csszengarden.com/215/'
+];
+
+let success = true;
+const browser = await puppeteer.launch();
+const page = await browser.newPage();
+
+for (let url of urls) {
+  await test(url);
+}
+
+async function test(url) {
+  console.log(`Testing ${url}...`);
+
+  await page.goto(url);
+  await page.waitForNetworkIdle();
+
+  // Snapshot the computed styles of all elements
+  let elements = await page.$$('body *');
+  let computed = [];
+  for (let element of elements) {
+    let style = await element.evaluate(node => {
+      let res = {};
+      let style = window.getComputedStyle(node);
+      for (let i = 0; i < style.length; i++) {
+        res[style.item(i)] = style.getPropertyValue(style.item(i));
+      }
+      return res;
+    });
+
+    for (let key in style) {
+      style[key] = normalize(key, style[key]);
+    }
+
+    computed.push(style);
+  }
+
+  // Find stylesheets, load, and minify.
+  let stylesheets = await page.evaluate(() => {
+    return [...document.styleSheets].map(styleSheet => styleSheet.href).filter(Boolean);
+  });
+
+  let texts = await Promise.all(stylesheets.map(async url => {
+    let res = await fetch(url);
+    return res.text();
+  }));
+
+  let minified = texts.map((code, i) => {
+    let minified = css.transform({
+      filename: 'test.css',
+      code: Buffer.from(code),
+      minify: true,
+      targets: {
+        chrome: 95 << 16
+      }
+    }).code.toString();
+    console.log(new URL(stylesheets[i]).pathname, '–', code.length + ' bytes', '=>', minified.length + ' bytes');
+    return minified;
+  });
+
+  await page.setCacheEnabled(false);
+
+  // Disable the original stylesheets and insert a <style> element containing the minified CSS for each.
+  await page.evaluate(minified => {
+    let i = 0;
+    for (let stylesheet of [...document.styleSheets]) {
+      if (stylesheet.href) {
+        stylesheet.disabled = true;
+
+        let style = document.createElement('style');
+        style.textContent = minified[i++].replace(/url\((.*?)\)/g, (orig, url) => {
+          if (/['"]?data:/.test(url)) {
+            return orig;
+          }
+
+          // Rewrite urls so they are relative to the original stylesheet.
+          return `url(${new URL(url, stylesheet.href)})`;
+        });
+
+        stylesheet.ownerNode.insertAdjacentElement('beforebegin', style);
+      }
+    }
+  }, minified);
+
+  await page.waitForNetworkIdle();
+
+  // Now get the computed style for each element again and compare.
+  let i = 0;
+  for (let element of elements) {
+    let style = await element.evaluate(node => {
+      let res = {};
+      let style = window.getComputedStyle(node);
+      for (let i = 0; i < style.length; i++) {
+        let name = style.item(i);
+        res[name] = style.getPropertyValue(name);
+      }
+      return res;
+    });
+
+    for (let key in style) {
+      style[key] = normalize(key, style[key]);
+
+      // Ignore prefixed properties that were removed during minification.
+      if (key.startsWith('-webkit-box-') && style[key] !== computed[i][key]) {
+        style[key] = computed[i][key];
+      }
+    }
+
+    try {
+      assert.deepEqual(style, computed[i]);
+    } catch (err) {
+      success = false;
+      console.log(diff(computed[i], style));
+      console.log(await element.evaluate(node => node.outerHTML))
+      console.log(minified[0]);
+    }
+
+    i++;
+  }
+
+  console.log('');
+}
+
+function normalize(key, value) {
+  if (key === 'background-position') {
+    value = value.replace(/(^|\s)0(%|px)/g, '$10');
+  }
+
+  if (key === 'background-image') {
+    // Center is implied.
+    value = value.replace('radial-gradient(at center center, ', 'radial-gradient(');
+  }
+
+  return value.split(' ').map(v => {
+    if (/^[\d\.]+px$/.test(v)) {
+      let val = parseFloat(v);
+      return Math.round(val) + 'px';
+    }
+
+    return v;
+  }).join(' ');
+}
+
+if (success) {
+  console.log('Pass!');
+  process.exit(0);
+} else {
+  console.log('Fail!');
+  process.exit(1);
+}
diff --git a/test.js b/test.js
new file mode 100644
index 0000000..cdb2d1c
--- /dev/null
+++ b/test.js
@@ -0,0 +1,78 @@
+const css = require('./');
+const fs = require('fs');
+
+if (process.argv[process.argv.length - 1] !== __filename) {
+  let opts = {
+    filename: process.argv[process.argv.length - 1],
+    code: fs.readFileSync(process.argv[process.argv.length - 1]),
+    minify: true,
+    sourceMap: true,
+    targets: {
+      chrome: 95 << 16
+    }
+  };
+
+  console.time('optimize');
+  let r = css.transform(opts);
+  console.timeEnd('optimize')
+  // console.log(r.toString());
+  console.log(r);
+  let code = r.code;
+  if (r.map) {
+    code = code.toString() + `\n/*# sourceMappingURL=out.css.map */\n`;
+  }
+  fs.writeFileSync('out.css', code);
+  if (r.map) {
+    fs.writeFileSync('out.css.map', r.map);
+  }
+  return;
+}
+
+let res = css.transform({
+  filename: __filename,
+  code: Buffer.from(`
+  @breakpoints {
+    .foo { color: yellow; }
+  }
+
+  .foo {
+    color: red;
+    @bar {
+      width: 25px;
+    }
+  }
+`),
+  drafts: {
+    nesting: true
+  },
+  targets: {
+    safari: 16 << 16
+  },
+  customAtRules: {
+    breakpoints: {
+      // Syntax string defining the at rule prelude.
+      // https://drafts.css-houdini.org/css-properties-values-api/#syntax-strings
+      prelude: null,
+      // Type of the at rule block.
+      // Can be declaration-list, rule-list, or style-block.
+      // https://www.w3.org/TR/css-syntax-3/#declaration-rule-list
+      body: 'rule-list'
+    },
+    bar: {
+      body: 'style-block'
+    }
+  },
+  visitor: {
+    Rule: {
+      custom(rule) {
+        console.log(rule.body);
+      }
+    },
+    Length(length) {
+      length.value *= 2;
+      return length;
+    }
+  }
+});
+
+console.log(res.code.toString());
diff --git a/tests/cli_integration_tests.rs b/tests/cli_integration_tests.rs
new file mode 100644
index 0000000..943beaa
--- /dev/null
+++ b/tests/cli_integration_tests.rs
@@ -0,0 +1,812 @@
+use assert_cmd::prelude::*;
+use assert_fs::fixture::FixtureError;
+use assert_fs::prelude::*;
+use indoc::indoc;
+use lightningcss::css_modules::CssModuleExport;
+use predicates::prelude::*;
+use std::collections::HashMap;
+use std::fs;
+use std::process::Command;
+
+fn test_file() -> Result<assert_fs::NamedTempFile, FixtureError> {
+  let file = assert_fs::NamedTempFile::new("test.css")?;
+  file.write_str(
+    r#"
+      .foo {
+        border: none;
+      }
+    "#,
+  )?;
+  Ok(file)
+}
+
+fn test_file2() -> Result<assert_fs::NamedTempFile, FixtureError> {
+  let file = assert_fs::NamedTempFile::new("test2.css")?;
+  file.write_str(
+    r#"
+      .foo {
+        color: yellow;
+      }
+    "#,
+  )?;
+  Ok(file)
+}
+
+fn css_module_test_vals() -> (String, String, String) {
+  let exports: HashMap<&str, CssModuleExport> = HashMap::from([
+    (
+      "fade",
+      CssModuleExport {
+        name: "EgL3uq_fade".into(),
+        composes: vec![],
+        is_referenced: false,
+      },
+    ),
+    (
+      "foo",
+      CssModuleExport {
+        name: "EgL3uq_foo".into(),
+        composes: vec![],
+        is_referenced: false,
+      },
+    ),
+    (
+      "circles",
+      CssModuleExport {
+        name: "EgL3uq_circles".into(),
+        composes: vec![],
+        is_referenced: true,
+      },
+    ),
+    (
+      "id",
+      CssModuleExport {
+        name: "EgL3uq_id".into(),
+        composes: vec![],
+        is_referenced: false,
+      },
+    ),
+    (
+      "test",
+      CssModuleExport {
+        name: "EgL3uq_test".into(),
+        composes: vec![],
+        is_referenced: true,
+      },
+    ),
+  ]);
+  (
+    r#"
+      .foo {
+        color: red;
+      }
+
+      #id {
+        animation: 2s test;
+      }
+
+      @keyframes test {
+        from { color: red }
+        to { color: yellow }
+      }
+
+      @counter-style circles {
+        symbols: Ⓐ Ⓑ Ⓒ;
+      }
+
+      ul {
+        list-style: circles;
+      }
+
+      @keyframes fade {
+        from { opacity: 0 }
+        to { opacity: 1 }
+      }
+    "#
+    .into(),
+    indoc! {r#"
+      .EgL3uq_foo {
+        color: red;
+      }
+
+      #EgL3uq_id {
+        animation: 2s EgL3uq_test;
+      }
+
+      @keyframes EgL3uq_test {
+        from {
+          color: red;
+        }
+
+        to {
+          color: #ff0;
+        }
+      }
+
+      @counter-style EgL3uq_circles {
+        symbols: Ⓐ Ⓑ Ⓒ;
+      }
+
+      ul {
+        list-style: EgL3uq_circles;
+      }
+
+      @keyframes EgL3uq_fade {
+        from {
+          opacity: 0;
+        }
+
+        to {
+          opacity: 1;
+        }
+      }
+    "#}
+    .into(),
+    serde_json::to_string(&exports).unwrap(),
+  )
+}
+
+#[test]
+fn valid_input_file() -> Result<(), Box<dyn std::error::Error>> {
+  let file = assert_fs::NamedTempFile::new("test.css")?;
+  file.write_str(
+    r#"
+      .foo {
+        border: none;
+      }
+    "#,
+  )?;
+
+  let mut cmd = Command::cargo_bin("lightningcss")?;
+  cmd.arg(file.path());
+  cmd.assert().success().stdout(predicate::str::contains(indoc! {r#"
+        .foo {
+          border: none;
+        }"#}));
+
+  Ok(())
+}
+
+#[test]
+fn empty_input_file() -> Result<(), Box<dyn std::error::Error>> {
+  let file = assert_fs::NamedTempFile::new("test.css")?;
+  file.write_str("")?;
+
+  let mut cmd = Command::cargo_bin("lightningcss")?;
+  cmd.arg(file.path());
+  cmd.assert().success();
+
+  Ok(())
+}
+
+#[test]
+fn output_file_option() -> Result<(), Box<dyn std::error::Error>> {
+  let infile = test_file()?;
+  let outfile = assert_fs::NamedTempFile::new("test.out")?;
+  let mut cmd = Command::cargo_bin("lightningcss")?;
+  cmd.arg(infile.path());
+  cmd.arg("--output-file").arg(outfile.path());
+  cmd.assert().success();
+  outfile.assert(predicate::str::contains(indoc! {r#"
+        .foo {
+          border: none;
+        }"#}));
+
+  Ok(())
+}
+
+#[test]
+fn output_file_option_create_missing_directories() -> Result<(), Box<dyn std::error::Error>> {
+  let infile = test_file()?;
+  let outdir = assert_fs::TempDir::new()?;
+  let outfile = outdir.child("out.css");
+  outdir.close()?;
+  let mut cmd = Command::cargo_bin("lightningcss")?;
+  cmd.arg(infile.path());
+  cmd.arg("--output-file").arg(outfile.path());
+  cmd.assert().success();
+  outfile.assert(predicate::str::contains(indoc! {
+    r#"
+      .foo {
+        border: none;
+      }
+    "#
+  }));
+  fs::remove_dir_all(outfile.parent().unwrap())?;
+
+  Ok(())
+}
+
+#[test]
+fn multiple_input_files() -> Result<(), Box<dyn std::error::Error>> {
+  let infile = test_file()?;
+  let infile2 = test_file2()?;
+  let outdir = assert_fs::TempDir::new()?;
+  let mut cmd = Command::cargo_bin("lightningcss")?;
+  cmd.arg(infile.path());
+  cmd.arg(infile2.path());
+  cmd.arg("--output-dir").arg(outdir.path());
+  cmd.assert().success();
+  outdir
+    .child(infile.file_name().unwrap())
+    .assert(predicate::str::contains(indoc! {r#"
+        .foo {
+          border: none;
+        }"#}));
+  outdir
+    .child(infile2.file_name().unwrap())
+    .assert(predicate::str::contains(indoc! {r#"
+        .foo {
+          color: #ff0;
+        }"#}));
+
+  Ok(())
+}
+
+#[test]
+fn multiple_input_files_out_file() -> Result<(), Box<dyn std::error::Error>> {
+  let infile = test_file()?;
+  let infile2 = test_file2()?;
+  let outdir = assert_fs::TempDir::new()?;
+  let mut cmd = Command::cargo_bin("lightningcss")?;
+  cmd.arg(infile.path());
+  cmd.arg(infile2.path());
+  cmd.arg("--output-file").arg(outdir.path());
+  cmd.assert().failure();
+
+  Ok(())
+}
+
+#[test]
+fn multiple_input_files_stdout() -> Result<(), Box<dyn std::error::Error>> {
+  let infile = test_file()?;
+  let infile2 = test_file2()?;
+  let mut cmd = Command::cargo_bin("lightningcss")?;
+  cmd.arg(infile.path());
+  cmd.arg(infile2.path());
+  cmd.assert().failure();
+
+  Ok(())
+}
+
+#[test]
+fn minify_option() -> Result<(), Box<dyn std::error::Error>> {
+  let infile = test_file()?;
+  let mut cmd = Command::cargo_bin("lightningcss")?;
+  cmd.arg(infile.path());
+  cmd.arg("--minify");
+  cmd
+    .assert()
+    .success()
+    .stdout(predicate::str::contains(indoc! {r#".foo{border:none}"#}));
+
+  Ok(())
+}
+
+#[test]
+fn nesting_option() -> Result<(), Box<dyn std::error::Error>> {
+  let infile = assert_fs::NamedTempFile::new("test.css")?;
+  infile.write_str(
+    r#"
+        .foo {
+          color: blue;
+          & > .bar { color: red; }
+        }
+      "#,
+  )?;
+
+  let mut cmd = Command::cargo_bin("lightningcss")?;
+  cmd.arg(infile.path());
+  cmd.arg("--targets=defaults");
+  cmd.arg("--nesting");
+  cmd.assert().success().stdout(predicate::str::contains(indoc! {r#"
+        .foo {
+          color: #00f;
+        }
+
+        .foo > .bar {
+          color: red;
+        }
+      "#}));
+
+  Ok(())
+}
+
+#[test]
+fn css_modules_infer_output_file() -> Result<(), Box<dyn std::error::Error>> {
+  let (input, _, exports) = css_module_test_vals();
+  let infile = assert_fs::NamedTempFile::new("test.css")?;
+  let outfile = assert_fs::NamedTempFile::new("out.css")?;
+  infile.write_str(&input)?;
+  let mut cmd = Command::cargo_bin("lightningcss")?;
+  cmd.current_dir(infile.path().parent().unwrap());
+  cmd.arg(infile.path());
+  cmd.arg("--css-modules");
+  cmd.arg("-o").arg(outfile.path());
+  cmd.assert().success();
+
+  let expected: serde_json::Value = serde_json::from_str(&exports)?;
+  let actual: serde_json::Value =
+    serde_json::from_str(&std::fs::read_to_string(outfile.path().with_extension("json"))?)?;
+  assert_eq!(expected, actual);
+
+  Ok(())
+}
+
+#[test]
+fn css_modules_output_target_option() -> Result<(), Box<dyn std::error::Error>> {
+  let (input, _, exports) = css_module_test_vals();
+  let infile = assert_fs::NamedTempFile::new("test.css")?;
+  let outfile = assert_fs::NamedTempFile::new("out.css")?;
+  let modules_file = assert_fs::NamedTempFile::new("module.json")?;
+  infile.write_str(&input)?;
+  let mut cmd = Command::cargo_bin("lightningcss")?;
+  cmd.current_dir(infile.path().parent().unwrap());
+  cmd.arg(infile.path());
+  cmd.arg("-o").arg(outfile.path());
+  cmd.arg("--css-modules").arg(modules_file.path());
+  cmd.assert().success();
+
+  let expected: serde_json::Value = serde_json::from_str(&exports)?;
+  let actual: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(modules_file.path())?)?;
+  assert_eq!(expected, actual);
+
+  Ok(())
+}
+
+#[test]
+fn css_modules_stdout() -> Result<(), Box<dyn std::error::Error>> {
+  let (input, out_code, exports) = css_module_test_vals();
+  let infile = assert_fs::NamedTempFile::new("test.css")?;
+  infile.write_str(&input)?;
+  let mut cmd = Command::cargo_bin("lightningcss")?;
+  cmd.current_dir(infile.path().parent().unwrap());
+  cmd.arg(infile.path());
+  cmd.arg("--css-modules");
+  let assert = cmd.assert().success();
+  let output = assert.get_output();
+
+  let expected: serde_json::Value = serde_json::from_str(&exports)?;
+  let actual: serde_json::Value = serde_json::from_slice(&output.stdout)?;
+  assert_eq!(out_code, actual.pointer("/code").unwrap().as_str().unwrap());
+  assert_eq!(&expected, actual.pointer("/exports").unwrap());
+
+  Ok(())
+}
+
+#[test]
+fn css_modules_pattern() -> Result<(), Box<dyn std::error::Error>> {
+  let (input, _, _) = css_module_test_vals();
+  let infile = assert_fs::NamedTempFile::new("test.css")?;
+  infile.write_str(&input)?;
+  let mut cmd = Command::cargo_bin("lightningcss")?;
+  cmd.current_dir(infile.path().parent().unwrap());
+  cmd.arg(infile.path());
+  cmd.arg("--css-modules");
+  cmd.arg("--css-modules-pattern").arg("[name]-[hash]-[local]");
+  cmd.assert().success().stdout(predicate::str::contains("test-EgL3uq-foo"));
+
+  Ok(())
+}
+
+#[test]
+fn css_modules_next_64299() -> Result<(), Box<dyn std::error::Error>> {
+  let file = assert_fs::NamedTempFile::new("test.css")?;
+  file.write_str(
+    "
+  .blue {
+    background: blue;
+
+    :global {
+      .red {
+        background: red;
+      }
+    }
+
+    &:global {
+      &.green {
+        background: green;
+      }
+    }
+  }
+  ",
+  )?;
+
+  let mut cmd = Command::cargo_bin("lightningcss")?;
+  cmd.arg(file.path());
+  cmd.arg("--css-modules");
+  cmd.assert().failure();
+
+  Ok(())
+}
+
+#[test]
+fn sourcemap() -> Result<(), Box<dyn std::error::Error>> {
+  let (input, _, _) = css_module_test_vals();
+  let infile = assert_fs::NamedTempFile::new("test.css")?;
+  let outdir = assert_fs::TempDir::new()?;
+  let outfile = outdir.child("out.css");
+  infile.write_str(&input)?;
+  let mut cmd = Command::cargo_bin("lightningcss")?;
+  cmd.current_dir(infile.path().parent().unwrap());
+  cmd.arg(infile.path());
+  cmd.arg("-o").arg(outfile.path());
+  cmd.arg("--sourcemap");
+  cmd.assert().success();
+
+  outfile.assert(predicate::str::contains(&format!(
+    "/*# sourceMappingURL={}.map */",
+    outfile.path().to_str().unwrap()
+  )));
+  let mapfile = outdir.child("out.css.map");
+  mapfile.assert(predicate::str::contains(r#""version":3"#));
+  mapfile.assert(predicate::str::contains(r#""sources":["test.css"]"#));
+  mapfile.assert(predicate::str::contains(
+    r#""mappings":"AACM;;;;AAIA;;;;AAIA;;;;;;;;;;AAKA;;;;AAIA;;;;AAIA""#,
+  ));
+
+  Ok(())
+}
+
+#[test]
+fn targets() -> Result<(), Box<dyn std::error::Error>> {
+  let file = assert_fs::NamedTempFile::new("test.css")?;
+  file.write_str(
+    r#"
+      @custom-media --foo print;
+      @media (--foo) {
+        .a { color: red }
+      }
+    "#,
+  )?;
+
+  let mut cmd = Command::cargo_bin("lightningcss")?;
+  cmd.arg(file.path());
+  cmd.arg("--custom-media");
+  cmd.arg("--targets").arg("last 1 Chrome version");
+  cmd.assert().success().stdout(predicate::str::contains(indoc! {r#"
+        @media print {
+          .a {
+            color: red;
+          }
+        }"#}));
+
+  Ok(())
+}
+
+#[test]
+fn preserve_custom_media() -> Result<(), Box<dyn std::error::Error>> {
+  let file = assert_fs::NamedTempFile::new("test.css")?;
+  file.write_str(
+    r#"
+      @custom-media --foo print;
+    "#,
+  )?;
+
+  let mut cmd = Command::cargo_bin("lightningcss")?;
+  cmd.arg(file.path());
+  cmd.arg("--custom-media");
+  cmd.assert().success().stdout(predicate::str::contains(indoc! {r#"
+    @custom-media --foo print;
+  "#}));
+
+  Ok(())
+}
+
+#[test]
+/// Test command line argument parsing failing when `--targets` is used at the same time as `--browserslist`.
+/// The two options are mutually exclusive.
+fn browserslist_targets_exclusive() -> Result<(), Box<dyn std::error::Error>> {
+  let mut cmd = Command::cargo_bin("lightningcss")?;
+  cmd.arg("--targets").arg("defaults");
+  cmd.arg("--browserslist");
+  cmd.assert().failure().stderr(predicate::str::contains(indoc! {r#"
+    error: The argument '--targets <TARGETS>' cannot be used with '--browserslist'
+  "#}));
+
+  Ok(())
+}
+
+#[test]
+/// Test browserslist defaults being applied when no configuration is provided or discovered.
+///
+/// Note: This test might fail in unhygienic environments and should ideally run inside a chroot.
+/// We have no control over the contents of our temp dir's parent dir (e.g. `/tmp`).
+/// If this parent dir or its ancestors contain a `browserslist`, `.browserslistrc` or `package.json`
+/// file, then configuration will be read from there, instead of applying defaults.
+fn browserslist_defaults() -> Result<(), Box<dyn std::error::Error>> {
+  let dir = assert_fs::TempDir::new()?;
+  let file = dir.child("test.css");
+  file.write_str(
+    r#"
+      * {
+        -webkit-border-radius: 1rem;
+        border-radius: 1rem;
+      }
+    "#,
+  )?;
+
+  let mut cmd = Command::cargo_bin("lightningcss")?;
+  cmd.current_dir(dir.path());
+  cmd.env_clear();
+  cmd.arg("--browserslist");
+  cmd.arg(file.path());
+  cmd.assert().success().stdout(predicate::str::contains(indoc! {r#"
+    * {
+      border-radius: 1rem;
+    }
+  "#}));
+
+  Ok(())
+}
+
+#[test]
+/// Test browserslist configuration being read from the `BROWSERSLIST` environment variable.
+fn browserslist_env_config() -> Result<(), Box<dyn std::error::Error>> {
+  let dir = assert_fs::TempDir::new()?;
+  let file = dir.child("test.css");
+  file.write_str(
+    r#"
+      * {
+        border-radius: 1rem;
+      }
+    "#,
+  )?;
+
+  let mut cmd = Command::cargo_bin("lightningcss")?;
+  cmd.current_dir(dir.path());
+  cmd.env_clear();
+  cmd.env("BROWSERSLIST", "safari 4");
+  cmd.arg("--browserslist");
+  cmd.arg(file.path());
+  cmd.assert().success().stdout(predicate::str::contains(indoc! {r#"
+    * {
+      -webkit-border-radius: 1rem;
+      border-radius: 1rem;
+    }
+  "#}));
+
+  Ok(())
+}
+
+#[test]
+/// Test browserslist configuration being read from the file configured
+/// by setting the `BROWSERSLIST_CONFIG` environment variable.
+fn browserslist_env_config_file() -> Result<(), Box<dyn std::error::Error>> {
+  let dir = assert_fs::TempDir::new()?;
+  let file = dir.child("test.css");
+  file.write_str(
+    r#"
+      * {
+        border-radius: 1rem;
+      }
+    "#,
+  )?;
+
+  let config = dir.child("config");
+  config.write_str(
+    r#"
+      safari 4
+    "#,
+  )?;
+
+  let mut cmd = Command::cargo_bin("lightningcss")?;
+  cmd.current_dir(dir.path());
+  cmd.env_clear();
+  cmd.env("BROWSERSLIST_CONFIG", config.path());
+  cmd.arg("--browserslist");
+  cmd.arg(file.path());
+  cmd.assert().success().stdout(predicate::str::contains(indoc! {r#"
+    * {
+      -webkit-border-radius: 1rem;
+      border-radius: 1rem;
+    }
+  "#}));
+
+  Ok(())
+}
+
+#[test]
+/// Test `browserslist` configuration file being read.
+fn browserslist_config_discovery() -> Result<(), Box<dyn std::error::Error>> {
+  let dir = assert_fs::TempDir::new()?;
+  let file = dir.child("test.css");
+  file.write_str(
+    r#"
+      * {
+        border-radius: 1rem;
+      }
+    "#,
+  )?;
+
+  let config = dir.child("browserslist");
+  config.write_str(
+    r#"
+      safari 4
+    "#,
+  )?;
+
+  let mut cmd = Command::cargo_bin("lightningcss")?;
+  cmd.current_dir(dir.path());
+  cmd.env_clear();
+  cmd.arg("--browserslist");
+  cmd.arg(file.path());
+  cmd.assert().success().stdout(predicate::str::contains(indoc! {r#"
+    * {
+      -webkit-border-radius: 1rem;
+      border-radius: 1rem;
+    }
+  "#}));
+
+  Ok(())
+}
+
+#[test]
+/// Test `.browserslistrc` configuration file being read.
+fn browserslist_rc_discovery() -> Result<(), Box<dyn std::error::Error>> {
+  let dir = assert_fs::TempDir::new()?;
+  let file = dir.child("test.css");
+  file.write_str(
+    r#"
+      * {
+        border-radius: 1rem;
+      }
+    "#,
+  )?;
+
+  let config = dir.child(".browserslistrc");
+  config.write_str(
+    r#"
+      safari 4
+    "#,
+  )?;
+
+  let mut cmd = Command::cargo_bin("lightningcss")?;
+  cmd.current_dir(dir.path());
+  cmd.env_clear();
+  cmd.arg("--browserslist");
+  cmd.arg(file.path());
+  cmd.assert().success().stdout(predicate::str::contains(indoc! {r#"
+    * {
+      -webkit-border-radius: 1rem;
+      border-radius: 1rem;
+    }
+  "#}));
+
+  Ok(())
+}
+
+#[test]
+/// Test `package.json` configuration section being read.
+fn browserslist_package_discovery() -> Result<(), Box<dyn std::error::Error>> {
+  let dir = assert_fs::TempDir::new()?;
+  let file = dir.child("test.css");
+  file.write_str(
+    r#"
+      * {
+        border-radius: 1rem;
+      }
+    "#,
+  )?;
+
+  let config = dir.child("package.json");
+  config.write_str(
+    r#"
+      {
+        "browserslist": "safari 4"
+      }
+    "#,
+  )?;
+
+  let mut cmd = Command::cargo_bin("lightningcss")?;
+  cmd.current_dir(dir.path());
+  cmd.env_clear();
+  cmd.arg("--browserslist");
+  cmd.arg(file.path());
+  cmd.assert().success().stdout(predicate::str::contains(indoc! {r#"
+    * {
+      -webkit-border-radius: 1rem;
+      border-radius: 1rem;
+    }
+  "#}));
+
+  Ok(())
+}
+
+#[test]
+/// Test environment targets being applied from the `NODE_ENV` environment variable.
+fn browserslist_environment_from_node_env() -> Result<(), Box<dyn std::error::Error>> {
+  let dir = assert_fs::TempDir::new()?;
+  let file = dir.child("test.css");
+  file.write_str(
+    r#"
+      * {
+        border-radius: 1rem;
+      }
+    "#,
+  )?;
+
+  let config = dir.child("browserslist");
+  config.write_str(
+    r#"
+      last 1 Chrome version
+
+      [legacy]
+      safari 4
+    "#,
+  )?;
+
+  let mut cmd = Command::cargo_bin("lightningcss")?;
+  cmd.current_dir(dir.path());
+  cmd.env_clear();
+  cmd.env("NODE_ENV", "legacy");
+  cmd.arg("--browserslist");
+  cmd.arg(file.path());
+  cmd.assert().success().stdout(predicate::str::contains(indoc! {r#"
+    * {
+      -webkit-border-radius: 1rem;
+      border-radius: 1rem;
+    }
+  "#}));
+
+  Ok(())
+}
+
+#[test]
+/// Test environment targets being applied from the `BROWSERSLIST_ENV` environment variable.
+fn browserslist_environment_from_browserslist_env() -> Result<(), Box<dyn std::error::Error>> {
+  let dir = assert_fs::TempDir::new()?;
+  let file = dir.child("test.css");
+  file.write_str(
+    r#"
+      * {
+        border-radius: 1rem;
+      }
+    "#,
+  )?;
+
+  let config = dir.child("browserslist");
+  config.write_str(
+    r#"
+      last 1 Chrome version
+
+      [legacy]
+      safari 4
+    "#,
+  )?;
+
+  let mut cmd = Command::cargo_bin("lightningcss")?;
+  cmd.current_dir(dir.path());
+  cmd.env_clear();
+  cmd.env("BROWSERSLIST_ENV", "legacy");
+  cmd.arg("--browserslist");
+  cmd.arg(file.path());
+  cmd.assert().success().stdout(predicate::str::contains(indoc! {r#"
+    * {
+      -webkit-border-radius: 1rem;
+      border-radius: 1rem;
+    }
+  "#}));
+
+  Ok(())
+}
+
+#[test]
+fn next_66191() -> Result<(), Box<dyn std::error::Error>> {
+  let infile = assert_fs::NamedTempFile::new("test.css")?;
+  infile.write_str(
+    r#"
+      .cb:is(input:checked) {
+        margin: 3rem;
+      }
+    "#,
+  )?;
+  let outfile = assert_fs::NamedTempFile::new("test.out")?;
+  let mut cmd = Command::cargo_bin("lightningcss")?;
+  cmd.arg(infile.path());
+  cmd.arg("--output-file").arg(outfile.path());
+  cmd.assert().success();
+  outfile.assert(predicate::str::contains(indoc! {r#".cb:is(input:checked)"#}));
+
+  Ok(())
+}
diff --git a/tests/test_cssom.rs b/tests/test_cssom.rs
new file mode 100644
index 0000000..4e13842
--- /dev/null
+++ b/tests/test_cssom.rs
@@ -0,0 +1,508 @@
+use lightningcss::{
+  declaration::DeclarationBlock,
+  properties::{Property, PropertyId},
+  stylesheet::{ParserOptions, PrinterOptions},
+  traits::ToCss,
+  vendor_prefix::VendorPrefix,
+};
+
+fn get_test(decls: &str, property_id: PropertyId, expected: Option<(&str, bool)>) {
+  let decls = DeclarationBlock::parse_string(decls, ParserOptions::default()).unwrap();
+  let v = decls.get(&property_id);
+  if let Some((expected, important)) = expected {
+    let (value, is_important) = v.unwrap();
+    assert_eq!(
+      *value,
+      Property::parse_string(property_id, expected, ParserOptions::default()).unwrap()
+    );
+    assert_eq!(is_important, important);
+  } else {
+    assert_eq!(v, None)
+  }
+}
+
+#[test]
+fn test_get() {
+  get_test("color: red", PropertyId::Color, Some(("red", false)));
+  get_test("color: red !important", PropertyId::Color, Some(("red", true)));
+  get_test("color: green; color: red", PropertyId::Color, Some(("red", false)));
+  get_test(
+    r#"
+    margin-top: 5px;
+    margin-bottom: 5px;
+    margin-left: 5px;
+    margin-right: 5px;
+    "#,
+    PropertyId::Margin,
+    Some(("5px", false)),
+  );
+  get_test(
+    r#"
+    margin-top: 5px;
+    margin-bottom: 5px;
+    margin-left: 6px;
+    margin-right: 6px;
+    "#,
+    PropertyId::Margin,
+    Some(("5px 6px", false)),
+  );
+  get_test(
+    r#"
+    margin-top: 5px;
+    margin-bottom: 5px;
+    margin-left: 6px;
+    margin-right: 6px;
+    "#,
+    PropertyId::Margin,
+    Some(("5px 6px", false)),
+  );
+  get_test(
+    r#"
+    margin-top: 5px;
+    margin-bottom: 5px;
+    "#,
+    PropertyId::Margin,
+    None,
+  );
+  get_test(
+    r#"
+    margin-top: 5px;
+    margin-bottom: 5px;
+    margin-left: 5px !important;
+    margin-right: 5px;
+    "#,
+    PropertyId::Margin,
+    None,
+  );
+  get_test(
+    r#"
+    margin-top: 5px !important;
+    margin-bottom: 5px !important;
+    margin-left: 5px !important;
+    margin-right: 5px !important;
+    "#,
+    PropertyId::Margin,
+    Some(("5px", true)),
+  );
+  get_test(
+    "margin: 5px 6px 7px 8px",
+    PropertyId::Margin,
+    Some(("5px 6px 7px 8px", false)),
+  );
+  get_test("margin: 5px 6px 7px 8px", PropertyId::MarginTop, Some(("5px", false)));
+  get_test(
+    r#"
+    border: 1px solid red;
+    border-color: green;
+    "#,
+    PropertyId::Border,
+    Some(("1px solid green", false)),
+  );
+  get_test(
+    r#"
+    border: 1px solid red;
+    border-left-color: green;
+    "#,
+    PropertyId::Border,
+    None,
+  );
+  get_test("background: red", PropertyId::Background, Some(("red", false)));
+  get_test("background: red", PropertyId::BackgroundColor, Some(("red", false)));
+  get_test(
+    "background: red url(foo.png)",
+    PropertyId::BackgroundColor,
+    Some(("red", false)),
+  );
+  get_test(
+    "background: url(foo.png), url(bar.png) red",
+    PropertyId::BackgroundColor,
+    Some(("red", false)),
+  );
+  get_test(
+    "background: url(foo.png) green, url(bar.png) red",
+    PropertyId::BackgroundColor,
+    Some(("red", false)),
+  );
+  get_test(
+    "background: linear-gradient(red, green)",
+    PropertyId::BackgroundImage,
+    Some(("linear-gradient(red, green)", false)),
+  );
+  get_test(
+    "background: linear-gradient(red, green), linear-gradient(#fff, #000)",
+    PropertyId::BackgroundImage,
+    Some(("linear-gradient(red, green), linear-gradient(#fff, #000)", false)),
+  );
+  get_test(
+    "background: linear-gradient(red, green) repeat-x, linear-gradient(#fff, #000) repeat-y",
+    PropertyId::BackgroundImage,
+    Some(("linear-gradient(red, green), linear-gradient(#fff, #000)", false)),
+  );
+  get_test(
+    "background: linear-gradient(red, green) repeat-x, linear-gradient(#fff, #000) repeat-y",
+    PropertyId::BackgroundRepeat,
+    Some(("repeat-x, repeat-y", false)),
+  );
+  get_test(
+    r#"
+    background: linear-gradient(red, green);
+    background-position-x: 20px;
+    background-position-y: 10px;
+    background-size: 50px 100px;
+    background-repeat: repeat no-repeat;
+    "#,
+    PropertyId::Background,
+    Some(("linear-gradient(red, green) 20px 10px / 50px 100px repeat-x", false)),
+  );
+  get_test(
+    r#"
+    background: linear-gradient(red, green);
+    background-position-x: 20px;
+    background-position-y: 10px !important;
+    background-size: 50px 100px;
+    background-repeat: repeat no-repeat;
+    "#,
+    PropertyId::Background,
+    None,
+  );
+  get_test(
+    r#"
+    background: linear-gradient(red, green), linear-gradient(#fff, #000) gray;
+    background-position-x: right 20px, 10px;
+    background-position-y: top 20px, 15px;
+    background-size: 50px 50px, auto;
+    background-repeat: repeat no-repeat, no-repeat;
+    "#,
+    PropertyId::Background,
+    Some(("linear-gradient(red, green) right 20px top 20px / 50px 50px repeat-x, gray linear-gradient(#fff, #000) 10px 15px no-repeat", false)),
+  );
+  get_test(
+    r#"
+    background: linear-gradient(red, green);
+    background-position-x: right 20px, 10px;
+    background-position-y: top 20px, 15px;
+    background-size: 50px 50px, auto;
+    background-repeat: repeat no-repeat, no-repeat;
+    "#,
+    PropertyId::Background,
+    None,
+  );
+  get_test(
+    r#"
+    background: linear-gradient(red, green);
+    background-position: 20px 10px;
+    background-size: 50px 100px;
+    background-repeat: repeat no-repeat;
+    "#,
+    PropertyId::Background,
+    Some(("linear-gradient(red, green) 20px 10px / 50px 100px repeat-x", false)),
+  );
+  get_test(
+    r#"
+    background-position-x: 20px;
+    background-position-y: 10px;
+    "#,
+    PropertyId::BackgroundPosition,
+    Some(("20px 10px", false)),
+  );
+  get_test(
+    r#"
+    background: linear-gradient(red, green) 20px 10px;
+    "#,
+    PropertyId::BackgroundPosition,
+    Some(("20px 10px", false)),
+  );
+  get_test(
+    r#"
+    background: linear-gradient(red, green) 20px 10px;
+    "#,
+    PropertyId::BackgroundPositionX,
+    Some(("20px", false)),
+  );
+  get_test(
+    r#"
+    background: linear-gradient(red, green) 20px 10px;
+    "#,
+    PropertyId::BackgroundPositionY,
+    Some(("10px", false)),
+  );
+  get_test(
+    "mask-border: linear-gradient(red, green) 25",
+    PropertyId::MaskBorderSource,
+    Some(("linear-gradient(red, green)", false)),
+  );
+  get_test("grid-area: a / b / c / d", PropertyId::GridRowStart, Some(("a", false)));
+  get_test("grid-area: a / b / c / d", PropertyId::GridRowEnd, Some(("c", false)));
+  get_test("grid-area: a / b / c / d", PropertyId::GridRow, Some(("a / c", false)));
+  get_test(
+    "grid-area: a / b / c / d",
+    PropertyId::GridColumn,
+    Some(("b / d", false)),
+  );
+  get_test(
+    r#"
+    grid-template-rows: auto 1fr;
+    grid-template-columns: auto 1fr auto;
+    grid-template-areas: none;
+    "#,
+    PropertyId::GridTemplate,
+    Some(("auto 1fr / auto 1fr auto", false)),
+  );
+  get_test(
+    r#"
+    grid-template-areas: ". a a ."
+        ". b b .";
+    grid-template-rows: auto 1fr;
+    grid-template-columns: 10px 1fr 1fr 10px;
+    "#,
+    PropertyId::GridTemplate,
+    Some((
+      r#"
+      ". a a ."
+      ". b b ." 1fr
+      / 10px 1fr 1fr 10px
+      "#,
+      false,
+    )),
+  );
+  get_test(
+    r#"
+    grid-template-areas: "a a a"
+                          "b b b";
+    grid-template-columns: repeat(3, 1fr);
+    grid-template-rows: auto 1fr;
+    "#,
+    PropertyId::GridTemplate,
+    None,
+  );
+  get_test(
+    r#"
+    grid-template-areas: "a a a"
+                         "b b b";
+    grid-template-rows: [header-top] auto [header-bottom main-top] 1fr [main-bottom];
+    grid-template-columns: auto 1fr auto;
+    grid-auto-flow: row;
+    grid-auto-rows: auto;
+    grid-auto-columns: auto;
+    "#,
+    PropertyId::Grid,
+    Some((
+      r#"
+      [header-top] "a a a" [header-bottom]
+      [main-top] "b b b" 1fr [main-bottom]
+      / auto 1fr auto
+      "#,
+      false,
+    )),
+  );
+  get_test(
+    r#"
+    grid-template-areas: "a a a"
+                         "b b b";
+    grid-template-rows: [header-top] auto [header-bottom main-top] 1fr [main-bottom];
+    grid-template-columns: auto 1fr auto;
+    grid-auto-flow: column;
+    grid-auto-rows: 1fr;
+    grid-auto-columns: 1fr;
+    "#,
+    PropertyId::Grid,
+    None,
+  );
+  get_test(
+    r#"
+    flex-direction: row;
+    flex-wrap: wrap;
+    "#,
+    PropertyId::FlexFlow(VendorPrefix::None),
+    Some(("row wrap", false)),
+  );
+  get_test(
+    r#"
+    -webkit-flex-direction: row;
+    -webkit-flex-wrap: wrap;
+    "#,
+    PropertyId::FlexFlow(VendorPrefix::WebKit),
+    Some(("row wrap", false)),
+  );
+  get_test(
+    r#"
+    flex-direction: row;
+    flex-wrap: wrap;
+    "#,
+    PropertyId::FlexFlow(VendorPrefix::WebKit),
+    None,
+  );
+  get_test(
+    r#"
+    -webkit-flex-direction: row;
+    flex-wrap: wrap;
+    "#,
+    PropertyId::FlexFlow(VendorPrefix::WebKit),
+    None,
+  );
+  get_test(
+    r#"
+    -webkit-flex-direction: row;
+    flex-wrap: wrap;
+    "#,
+    PropertyId::FlexFlow(VendorPrefix::None),
+    None,
+  );
+  get_test(
+    r#"
+    -webkit-flex-flow: row;
+    "#,
+    PropertyId::FlexDirection(VendorPrefix::WebKit),
+    Some(("row", false)),
+  );
+  get_test(
+    r#"
+    -webkit-flex-flow: row;
+    "#,
+    PropertyId::FlexDirection(VendorPrefix::None),
+    None,
+  );
+}
+
+fn set_test(orig: &str, property: &str, value: &str, important: bool, expected: &str) {
+  let mut decls = DeclarationBlock::parse_string(orig, ParserOptions::default()).unwrap();
+  decls.set(
+    Property::parse_string(property.into(), value, ParserOptions::default()).unwrap(),
+    important,
+  );
+  assert_eq!(decls.to_css_string(PrinterOptions::default()).unwrap(), expected);
+}
+
+#[test]
+fn test_set() {
+  set_test("color: red", "color", "green", false, "color: green");
+  set_test("color: red !important", "color", "green", false, "color: green");
+  set_test("color: red", "color", "green", true, "color: green !important");
+  set_test("margin: 5px", "margin", "10px", false, "margin: 10px");
+  set_test("margin: 5px", "margin-top", "8px", false, "margin: 8px 5px 5px");
+  set_test(
+    "margin: 5px",
+    "margin-inline-start",
+    "8px",
+    false,
+    "margin: 5px; margin-inline-start: 8px",
+  );
+  set_test(
+    "margin-inline-start: 5px; margin-top: 10px",
+    "margin-inline-start",
+    "8px",
+    false,
+    "margin-inline-start: 5px; margin-top: 10px; margin-inline-start: 8px",
+  );
+  set_test(
+    "margin: 5px; margin-inline-start: 8px",
+    "margin-left",
+    "10px",
+    false,
+    "margin: 5px; margin-inline-start: 8px; margin-left: 10px",
+  );
+  set_test(
+    "border: 1px solid red",
+    "border-right",
+    "1px solid green",
+    false,
+    "border: 1px solid red; border-right: 1px solid green",
+  );
+  set_test(
+    "border: 1px solid red",
+    "border-right-color",
+    "green",
+    false,
+    "border: 1px solid red; border-right-color: green",
+  );
+  set_test(
+    "animation: foo 2s",
+    "animation-name",
+    "foo, bar",
+    false,
+    "animation: 2s foo; animation-name: foo, bar",
+  );
+  set_test("animation: foo 2s", "animation-name", "bar", false, "animation: 2s bar");
+  set_test(
+    "background: linear-gradient(red, green)",
+    "background-position-x",
+    "20px",
+    false,
+    "background: linear-gradient(red, green) 20px 0",
+  );
+  set_test(
+    "background: linear-gradient(red, green)",
+    "background-position",
+    "20px 10px",
+    false,
+    "background: linear-gradient(red, green) 20px 10px",
+  );
+  set_test(
+    "flex-flow: row wrap",
+    "flex-direction",
+    "column",
+    false,
+    "flex-flow: column wrap",
+  );
+  set_test(
+    "-webkit-flex-flow: row wrap",
+    "-webkit-flex-direction",
+    "column",
+    false,
+    "-webkit-flex-flow: column wrap",
+  );
+  set_test(
+    "flex-flow: row wrap",
+    "-webkit-flex-direction",
+    "column",
+    false,
+    "flex-flow: wrap; -webkit-flex-direction: column",
+  );
+}
+
+fn remove_test(orig: &str, property_id: PropertyId, expected: &str) {
+  let mut decls = DeclarationBlock::parse_string(orig, ParserOptions::default()).unwrap();
+  decls.remove(&property_id);
+  assert_eq!(decls.to_css_string(PrinterOptions::default()).unwrap(), expected);
+}
+
+#[test]
+fn test_remove() {
+  remove_test("margin-top: 10px", PropertyId::MarginTop, "");
+  remove_test(
+    "margin-top: 10px; margin-left: 5px",
+    PropertyId::MarginTop,
+    "margin-left: 5px",
+  );
+  remove_test(
+    "margin-top: 10px !important; margin-left: 5px",
+    PropertyId::MarginTop,
+    "margin-left: 5px",
+  );
+  remove_test(
+    "margin: 10px",
+    PropertyId::MarginTop,
+    "margin-right: 10px; margin-bottom: 10px; margin-left: 10px",
+  );
+  remove_test("margin: 10px", PropertyId::Margin, "");
+  remove_test(
+    "margin-top: 10px; margin-right: 10px; margin-bottom: 10px; margin-left: 10px",
+    PropertyId::Margin,
+    "",
+  );
+  remove_test(
+    "flex-flow: column wrap",
+    PropertyId::FlexDirection(VendorPrefix::None),
+    "flex-wrap: wrap",
+  );
+  remove_test(
+    "flex-flow: column wrap",
+    PropertyId::FlexDirection(VendorPrefix::WebKit),
+    "flex-flow: column wrap",
+  );
+  remove_test(
+    "-webkit-flex-flow: column wrap",
+    PropertyId::FlexDirection(VendorPrefix::WebKit),
+    "-webkit-flex-wrap: wrap",
+  );
+}
diff --git a/tests/test_custom_parser.rs b/tests/test_custom_parser.rs
new file mode 100644
index 0000000..8e360ef
--- /dev/null
+++ b/tests/test_custom_parser.rs
@@ -0,0 +1,145 @@
+use cssparser::*;
+use lightningcss::{
+  declaration::DeclarationBlock,
+  error::{ParserError, PrinterError},
+  printer::Printer,
+  stylesheet::{ParserOptions, PrinterOptions, StyleSheet},
+  traits::{AtRuleParser, Parse, ToCss},
+  values::ident::Ident,
+};
+
+fn minify_test(source: &str, expected: &str) {
+  let mut stylesheet = StyleSheet::parse_with(&source, ParserOptions::default(), &mut TestAtRuleParser).unwrap();
+  stylesheet.minify(Default::default()).unwrap();
+  let res = stylesheet
+    .to_css(PrinterOptions {
+      minify: true,
+      ..PrinterOptions::default()
+    })
+    .unwrap();
+  assert_eq!(res.code, expected);
+}
+
+#[test]
+fn test_block() {
+  minify_test(
+    r#"
+    @block test {
+      color: yellow;
+    }
+  "#,
+    "@block test{color:#ff0}",
+  )
+}
+
+#[test]
+fn test_inline() {
+  minify_test(
+    r#"
+    @inline test;
+    .foo {
+      color: yellow;
+    }
+  "#,
+    "@inline test;.foo{color:#ff0}",
+  )
+}
+
+enum Prelude<'i> {
+  Block(Ident<'i>),
+  Inline(Ident<'i>),
+}
+
+#[derive(Debug, Clone)]
+enum AtRule<'i> {
+  Block(BlockRule<'i>),
+  Inline(InlineRule<'i>),
+}
+
+#[derive(Debug, Clone)]
+struct BlockRule<'i> {
+  name: Ident<'i>,
+  declarations: DeclarationBlock<'i>,
+}
+
+#[derive(Debug, Clone)]
+struct InlineRule<'i> {
+  name: Ident<'i>,
+}
+
+#[derive(Default)]
+struct TestAtRuleParser;
+impl<'i> AtRuleParser<'i> for TestAtRuleParser {
+  type Prelude = Prelude<'i>;
+  type Error = ParserError<'i>;
+  type AtRule = AtRule<'i>;
+
+  fn parse_prelude<'t>(
+    &mut self,
+    name: CowRcStr<'i>,
+    input: &mut Parser<'i, 't>,
+    _options: &ParserOptions<'_, 'i>,
+  ) -> Result<Self::Prelude, ParseError<'i, Self::Error>> {
+    let location = input.current_source_location();
+    match_ignore_ascii_case! {&*name,
+      "block" => {
+        let name = Ident::parse(input)?;
+        Ok(Prelude::Block(name))
+      },
+      "inline" => {
+        let name = Ident::parse(input)?;
+        Ok(Prelude::Inline(name))
+      },
+      _ => Err(location.new_unexpected_token_error(
+        cssparser::Token::Ident(name.clone())
+      ))
+    }
+  }
+
+  fn rule_without_block(
+    &mut self,
+    prelude: Self::Prelude,
+    _start: &ParserState,
+    _options: &ParserOptions<'_, 'i>,
+    _is_nested: bool,
+  ) -> Result<Self::AtRule, ()> {
+    match prelude {
+      Prelude::Inline(name) => Ok(AtRule::Inline(InlineRule { name })),
+      _ => unreachable!(),
+    }
+  }
+
+  fn parse_block<'t>(
+    &mut self,
+    prelude: Self::Prelude,
+    _start: &ParserState,
+    input: &mut Parser<'i, 't>,
+    _options: &ParserOptions<'_, 'i>,
+    _is_nested: bool,
+  ) -> Result<Self::AtRule, ParseError<'i, Self::Error>> {
+    match prelude {
+      Prelude::Block(name) => Ok(AtRule::Block(BlockRule {
+        name,
+        declarations: DeclarationBlock::parse(input, &ParserOptions::default())?,
+      })),
+      _ => unreachable!(),
+    }
+  }
+}
+
+impl<'i> ToCss for AtRule<'i> {
+  fn to_css<W: std::fmt::Write>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError> {
+    match self {
+      AtRule::Block(rule) => {
+        dest.write_str("@block ")?;
+        rule.name.to_css(dest)?;
+        rule.declarations.to_css_block(dest)
+      }
+      AtRule::Inline(rule) => {
+        dest.write_str("@inline ")?;
+        rule.name.to_css(dest)?;
+        dest.write_char(';')
+      }
+    }
+  }
+}
diff --git a/tests/test_serde.rs b/tests/test_serde.rs
new file mode 100644
index 0000000..a754edf
--- /dev/null
+++ b/tests/test_serde.rs
@@ -0,0 +1,20 @@
+#[cfg(feature = "serde")]
+use lightningcss::stylesheet::{ParserOptions, StyleSheet};
+
+#[cfg(feature = "serde")]
+#[test]
+fn test_serde() {
+  let code = r#"
+    .foo {
+      color: red;
+    }
+  "#;
+  let (json, stylesheet) = {
+    let stylesheet = StyleSheet::parse(code, ParserOptions::default()).unwrap();
+    let json = serde_json::to_string(&stylesheet).unwrap();
+    (json, stylesheet)
+  };
+
+  let deserialized: StyleSheet = serde_json::from_str(&json).unwrap();
+  assert_eq!(&deserialized.rules, &stylesheet.rules);
+}
diff --git a/tests/testdata/a.css b/tests/testdata/a.css
new file mode 100644
index 0000000..fcf2f61
--- /dev/null
+++ b/tests/testdata/a.css
@@ -0,0 +1,5 @@
+@import "b.css";
+
+.a {
+  width: 32px;
+}
diff --git a/tests/testdata/apply.css b/tests/testdata/apply.css
new file mode 100644
index 0000000..e2508b3
--- /dev/null
+++ b/tests/testdata/apply.css
@@ -0,0 +1,5 @@
+@import "./mixin.css";
+
+.foo {
+  @apply color;
+}
diff --git a/tests/testdata/b.css b/tests/testdata/b.css
new file mode 100644
index 0000000..18065d8
--- /dev/null
+++ b/tests/testdata/b.css
@@ -0,0 +1,3 @@
+.b {
+  height: calc(100vh - 64px);
+}
diff --git a/tests/testdata/baz.css b/tests/testdata/baz.css
new file mode 100644
index 0000000..095c8a7
--- /dev/null
+++ b/tests/testdata/baz.css
@@ -0,0 +1 @@
+.baz { color: blue; }
\ No newline at end of file
diff --git a/tests/testdata/foo.css b/tests/testdata/foo.css
new file mode 100644
index 0000000..9509961
--- /dev/null
+++ b/tests/testdata/foo.css
@@ -0,0 +1,3 @@
+@import 'root:hello/world.css';
+
+.foo { color: red; }
diff --git a/tests/testdata/hello/world.css b/tests/testdata/hello/world.css
new file mode 100644
index 0000000..bc1763b
--- /dev/null
+++ b/tests/testdata/hello/world.css
@@ -0,0 +1,3 @@
+@import 'root:baz.css';
+
+.bar { color: green; }
diff --git a/tests/testdata/mixin.css b/tests/testdata/mixin.css
new file mode 100644
index 0000000..a836131
--- /dev/null
+++ b/tests/testdata/mixin.css
@@ -0,0 +1,7 @@
+@mixin color {
+  color: red;
+
+  &.bar {
+    color: yellow;
+  }
+}
diff --git a/wasm/.gitignore b/wasm/.gitignore
new file mode 100644
index 0000000..717c519
--- /dev/null
+++ b/wasm/.gitignore
@@ -0,0 +1,8 @@
+*.d.ts
+*.wasm
+*.cjs
+package.json
+README.md
+browserslistToTargets.js
+flags.js
+composeVisitors.js
diff --git a/wasm/async.mjs b/wasm/async.mjs
new file mode 100644
index 0000000..f1f4df0
--- /dev/null
+++ b/wasm/async.mjs
@@ -0,0 +1,74 @@
+let cur_await_promise_sync;
+export function await_promise_sync(promise_addr, result_addr, error_addr) {
+  cur_await_promise_sync(promise_addr, result_addr, error_addr);
+}
+
+const State = {
+  None: 0,
+  Unwinding: 1,
+  Rewinding: 2
+};
+
+// This uses Binaryen's Asyncify transform to suspend native code execution while a promise is resolving.
+// That allows synchronous Rust code to call async JavaScript functions without multi-threading.
+// When Rust wants to await a promise, it calls await_promise_sync, which saves the stack state and unwinds.
+// That causes the bundle function to return early. If a promise has been queued, we can then await it
+// and "rewind" the function back to where it was before by calling it again. This time the result of
+// the promise can be returned, and the function can continue where it left off.
+// See the docs in https://github.com/WebAssembly/binaryen/blob/main/src/passes/Asyncify.cpp
+// The code here is also partially based on https://github.com/GoogleChromeLabs/asyncify
+export function createBundleAsync(env) {
+  let {instance, exports} = env;
+  let {asyncify_get_state, asyncify_start_unwind, asyncify_stop_unwind, asyncify_start_rewind, asyncify_stop_rewind} = instance.exports;
+
+  // allocate __asyncify_data
+  // Stack data goes right after the initial descriptor.
+  let DATA_ADDR = instance.exports.napi_wasm_malloc(8 + 4096);
+  let DATA_START = DATA_ADDR + 8;
+  let DATA_END = DATA_ADDR + 8 + 4096;
+  new Int32Array(env.memory.buffer, DATA_ADDR).set([DATA_START, DATA_END]);
+
+  function assertNoneState() {
+    if (asyncify_get_state() !== State.None) {
+      throw new Error(`Invalid async state ${asyncify_get_state()}, expected 0.`);
+    }
+  }
+
+  let promise, result, error;
+  cur_await_promise_sync = (promise_addr, result_addr, error_addr) => {
+    let state = asyncify_get_state();
+    if (state === State.Rewinding) {
+      asyncify_stop_rewind();
+      if (result != null) {
+        env.createValue(result, result_addr);
+      }
+      if (error != null) {
+        env.createValue(error, error_addr);
+      }
+      promise = result = error = null;
+      return;
+    }
+    assertNoneState();
+    promise = env.get(promise_addr);
+    asyncify_start_unwind(DATA_ADDR);
+  };
+
+  return async function bundleAsync(options) {
+    assertNoneState();
+    let res = exports.bundle(options);
+    while (asyncify_get_state() === State.Unwinding) {
+      asyncify_stop_unwind();
+      try {
+        result = await promise;
+      } catch (err) {
+        error = err;
+      }
+      assertNoneState();
+      asyncify_start_rewind(DATA_ADDR);
+      res = exports.bundle(options);
+    }
+
+    assertNoneState();
+    return res;
+  };
+}
diff --git a/wasm/import.meta.url-polyfill.js b/wasm/import.meta.url-polyfill.js
new file mode 100644
index 0000000..70c7abe
--- /dev/null
+++ b/wasm/import.meta.url-polyfill.js
@@ -0,0 +1,6 @@
+/**
+ * @see https://github.com/evanw/esbuild/issues/1633
+ */
+export const import_meta_url =
+  typeof document === 'undefined' ? new (require('url'.replace('', '')).URL)('file:' + __filename).href :
+    (document.currentScript && document.currentScript.src || new URL('main.js', document.baseURI).href)
diff --git a/wasm/index.mjs b/wasm/index.mjs
new file mode 100644
index 0000000..7120fd0
--- /dev/null
+++ b/wasm/index.mjs
@@ -0,0 +1,93 @@
+import { Environment, napi } from 'napi-wasm';
+import { await_promise_sync, createBundleAsync } from './async.mjs';
+
+let wasm, initPromise, bundleAsyncInternal;
+
+export default async function init(input) {
+  if (wasm) return;
+  if (initPromise) {
+    await initPromise;
+    return;
+  }
+
+  input = input ?? new URL('lightningcss_node.wasm', import.meta.url);
+  if (typeof input === 'string' || (typeof Request === 'function' && input instanceof Request) || (typeof URL === 'function' && input instanceof URL)) {
+    input = fetchOrReadFromFs(input);
+  }
+
+  let env;
+  initPromise = input
+    .then(input => load(input, {
+      env: {
+        ...napi,
+        await_promise_sync,
+        __getrandom_custom: (ptr, len) => {
+          let buf = env.memory.subarray(ptr, ptr + len);
+          crypto.getRandomValues(buf);
+        },
+      }
+    }))
+    .then(({instance}) => {
+      instance.exports.register_module();
+      env = new Environment(instance);
+      bundleAsyncInternal = createBundleAsync(env);
+      wasm = env.exports;
+    });
+
+  await initPromise;
+}
+
+export function transform(options) {
+  return wasm.transform(options);
+}
+
+export function transformStyleAttribute(options) {
+  return wasm.transformStyleAttribute(options);
+}
+
+export function bundle(options) {
+  return wasm.bundle(options);
+}
+
+export function bundleAsync(options) {
+  return bundleAsyncInternal(options);
+}
+
+export { browserslistToTargets } from './browserslistToTargets.js';
+export { Features } from './flags.js';
+export { composeVisitors } from './composeVisitors.js';
+
+async function load(module, imports) {
+  if (typeof Response === 'function' && module instanceof Response) {
+    if (typeof WebAssembly.instantiateStreaming === 'function') {
+      try {
+        return await WebAssembly.instantiateStreaming(module, imports);
+      } catch (e) {
+        if (module.headers.get('Content-Type') != 'application/wasm') {
+          console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e);
+        } else {
+          throw e;
+        }
+      }
+    }
+
+    const bytes = await module.arrayBuffer();
+    return await WebAssembly.instantiate(bytes, imports);
+  } else {
+    const instance = await WebAssembly.instantiate(module, imports);
+    if (instance instanceof WebAssembly.Instance) {
+      return { instance, module };
+    } else {
+      return instance;
+    }
+  }
+}
+
+async function fetchOrReadFromFs(inputPath) {
+  try {
+    const fs = await import('fs');
+    return fs.readFileSync(inputPath);
+  } catch {
+    return fetch(inputPath);
+  }
+};
diff --git a/wasm/wasm-node.mjs b/wasm/wasm-node.mjs
new file mode 100644
index 0000000..7e8c69f
--- /dev/null
+++ b/wasm/wasm-node.mjs
@@ -0,0 +1,57 @@
+import { Environment, napi } from 'napi-wasm';
+import { await_promise_sync, createBundleAsync } from './async.mjs';
+import fs from 'fs';
+import {webcrypto as crypto} from 'node:crypto';
+
+let wasmBytes = fs.readFileSync(new URL('lightningcss_node.wasm', import.meta.url));
+let wasmModule = new WebAssembly.Module(wasmBytes);
+let instance = new WebAssembly.Instance(wasmModule, {
+  env: {
+    ...napi,
+    await_promise_sync,
+    __getrandom_custom: (ptr, len) => {
+      let buf = env.memory.subarray(ptr, ptr + len);
+      crypto.getRandomValues(buf);
+    },
+  },
+});
+instance.exports.register_module();
+let env = new Environment(instance);
+let wasm = env.exports;
+let bundleAsyncInternal = createBundleAsync(env);
+
+export default async function init() {
+  // do nothing. for backward compatibility.
+}
+
+export function transform(options) {
+  return wasm.transform(options);
+}
+
+export function transformStyleAttribute(options) {
+  return wasm.transformStyleAttribute(options);
+}
+
+export function bundle(options) {
+  return wasm.bundle({
+    ...options,
+    resolver: {
+      read: (filePath) => fs.readFileSync(filePath, 'utf8')
+    }
+  });
+}
+
+export async function bundleAsync(options) {
+  if (!options.resolver?.read) {
+    options.resolver = {
+      ...options.resolver,
+      read: (filePath) => fs.readFileSync(filePath, 'utf8')
+    };
+  }
+
+  return bundleAsyncInternal(options);
+}
+
+export { browserslistToTargets } from './browserslistToTargets.js'
+export { Features } from './flags.js'
+export { composeVisitors } from './composeVisitors.js';
diff --git a/website/.posthtmlrc b/website/.posthtmlrc
new file mode 100644
index 0000000..def9e84
--- /dev/null
+++ b/website/.posthtmlrc
@@ -0,0 +1,28 @@
+{
+  "plugins": {
+    "posthtml-include": {},
+    "posthtml-markdownit": {
+      "markdownit": {
+        "html": true
+      },
+      "plugins": [
+        {
+          "plugin": "markdown-it-anchor"
+        },
+        {
+          "plugin": "markdown-it-table-of-contents",
+          "options": {
+            "containerHeaderHtml": "<h3>On this page</h3>",
+            "includeLevel": [
+              2,
+              3
+            ]
+          }
+        },
+        {
+          "plugin": "markdown-it-prism"
+        }
+      ]
+    }
+  }
+}
\ No newline at end of file
diff --git a/website/bundling.html b/website/bundling.html
new file mode 100644
index 0000000..67be390
--- /dev/null
+++ b/website/bundling.html
@@ -0,0 +1 @@
+<include src="website/include/layout.html" locals='{"title": "Bundling", "url": "bundling.html", "page": "website/pages/bundling.md"}' />
diff --git a/website/crush.svg b/website/crush.svg
new file mode 100644
index 0000000..818a18a
--- /dev/null
+++ b/website/crush.svg
@@ -0,0 +1,134 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 3989 3982">
+  <defs>
+    <style>
+      .cls-1 {
+        fill: #f9c8c0;
+      }
+
+      .cls-2 {
+        fill: #eba998;
+      }
+
+      .cls-5 {
+        fill: #fff;
+      }
+
+      .cls-6 {
+        fill: #9d9c9d;
+      }
+
+      .cls-7 {
+        fill: #930910;
+      }
+
+      .cls-8 {
+        fill: #ba1921;
+      }
+
+      .cls-9 {
+        fill: #ce926e;
+      }
+
+      .cls-10 {
+        fill: #d88b7c;
+      }
+
+      .cls-11 {
+        fill: #af0e0f;
+      }
+
+      .cls-13 {
+        fill: #e0a98a;
+      }
+
+      .cls-14 {
+        fill: #7b7f7f;
+      }
+    </style>
+  </defs>
+  <g id="Layer_1" data-name="Layer 1">
+    <g>
+      <path class="cls-13" d="M738.89,974.2s-252.29,76.5-235.73,353.98c0,0-23.28,77.61,108.06,137.99,0,0-106.49,70.61-139.49,150.05,0,0-42,149.93,91.5,289.43l-28.5,13.5s-191.99,53.33-142.49,232.91c0,0,42,137.57,218.99,200.57,0,0-246.72,43.15-148.49,222.93l124.49,194.05,260.34-188.05,232.67-172,305.33-220,32-93v-207l-110-140.04-273-289.96-19-58-134-105.33s-50-58.67-29.33-138.67l-68-52.67-45.33-130.69Z"/>
+      <g>
+        <path class="cls-2" d="M1302.89,1956.23c-5.77-52.24-43.33-105.67-43.33-105.67l-147.33-9.67c-138,64-57.33,180.67-57.33,180.67-.67,10,10.67,11.33,10.67,11.33l204.5,63.67c34.82-52.91,32.83-140.33,32.83-140.33Z"/>
+        <path class="cls-2" d="M1031.46,2162.56s-72.4,43.5-25.4,142l116.5,76.5,76-57.5,53-162s-128.7-21.5-220.1,1Z"/>
+      </g>
+      <path class="cls-10" d="M1202.23,1846.23s39.33,140,27.33,199.33l-182.67-49.33,18.67,36.67,204.5,63.67s37.5-72.5,37.5-119.5c0,0-5.5-93-48-126.5l-57.33-4.33Z"/>
+      <path class="cls-1" d="M1008.56,2183.56l214,60.5,24-81.5s-111.7-17-215.1,0l-22.9,21Z"/>
+      <path class="cls-2" d="M710.25,1408.24s-16.68,86.73,28.65,121.69l158,86.3s30.7-145.01,12.67-212.17c0,0-107.33-39.78-166.67-35.81,0,0-27.96,25.67-32.65,39.99Z"/>
+      <path class="cls-10" d="M705.56,1422.56l206,126.33s-10.04,63.09-14.67,67.33l-166-90.67s-36.67-78-25.33-103Z"/>
+      <path class="cls-8" d="M1758.56,1775.56s17.23,57.87,12.1,99l-326.98,430-332.98,490.55s-168.72,117.07-295.26,76.09l-83.59-37.19-148.29-59.79,82.67-64.67,330.67-240.67,306.02-219.78,203.31-149.55,105.33-113,147-211Z"/>
+      <path class="cls-7" d="M1701.13,1958.1l-357.57,365.46,88-34s-280.25,549.28-616.13,581.64c0,0,102.37,86.46,361.79,170.09,0,0,357.95,113.8,500.44,136.29l34.5-389.98,58.5-815.21,6-82.84-75.53,68.54Z"/>
+      <path class="cls-11" d="M1609.56,2111.14l-158.38,403.48c46.5,110.99-85.5,102-85.5,102,142.49,136.49-140.99,116.94-140.99,116.94,211.49,101.94,75,180.04,75,180.04-4.5,103.49-142.49,19.5-142.49,19.5l81,70.63c88.5,73.63-422.75-132.53-422.75-132.53l187.27,105.4c64.9,44.12,395.98,123.7,395.98,123.7-44.42-62.2,81-435.68,81-435.68l228.05-653.05,4.43-40.17-102.6,139.75Z"/>
+      <path class="cls-1" d="M738.89,1377.56s132,44,153.67,63l19,7.67-2-44.17s-164.67-57.83-170.67-26.5Z"/>
+      <g>
+        <path class="cls-9" d="M468.54,2477.56c-81.03,141.33,145.03,256,145.03,256l258-170-4.09-7.06c-276.22,115.38-398.94-78.94-398.94-78.94Z"/>
+        <path class="cls-9" d="M932.2,2233.85c31.5,101.71-140.47-28.99-140.47-28.99l-80.27-46.72,95.1-24.5-11.37-11.15,40.36-17.35c-54.65-37.5-218.34,22.5-218.34,22.5l140.22,104.92c21.73,52.42,92.27,108.07,92.27,108.07l-99-58.49c-36.5,72.46-209.99-139.5-209.99-139.5,12,55.5,64.27,139.49,64.27,139.49-100.4,1.13-183.41-102.69-183.41-102.69,22.75,99.94,173.14,171.68,173.14,171.68l248.66,18,207.31,60,32.86-24c-84.13-33-151.36-171.28-151.36-171.28Z"/>
+        <path class="cls-9" d="M1383.56,1850.56l-85-113-128.49-78h-58.51l112.02,179.65,38.65,9.02s102,116.67,9.33,253.33l-206-62s-85.03-45-38.02-159c0,0-41.98,15-55.98,106,0,0-85-14-70-62,0,0-129.46,47.33-156.69-44,0,0-8.31,112-80.31,20,0,0-12.5,31.86-29.25,29.93l7.25,27.07,225.87,74.51c-13.76,10.08-24.15,26.32-21.87,52.49,0,0,293,19,372.5,67.5l27.5,10.5-48,161,133-97v-74.75l1,.25,64-59.25,21-137.25-34-105Z"/>
+        <path class="cls-9" d="M526.3,1408.24c20.61,25.68,74.76,57.32,74.76,57.32l106.5,14c-22-81.5,38.5-111,38.5-111,46-5.5,163.5,35.5,163.5,35.5l3.5,102.5c0,52-16.5,108-16.5,108l-112-59c4,49,90,85,90,85l130-12,36.33-155.67-14.46-65.33-90.01-43c44.87,86-8.87,222-8.87,222,19.95-61.99,0-194,0-194-79-54-220.33-56.96-220.31-57.02,0,0-195.6-36.95-196.69-96.98,0,0-24.52,139.36,15.74,169.68Z"/>
+      </g>
+      <path class="cls-6" d="M580.64,2926.89s-22.08-40.05-7.75-98.67l7.75-54s139.59,45.33,232.25,102.67c0,0,270.67,132,332.67,148l376,119.33,82.67,26-32.67,54.67s-52.82,16.03-146.67,3.67c0,0-234-21-336-50.33,0,0-299.7-97.47-322-128,0,0-176.78-94.89-186.25-123.33Z"/>
+      <path class="cls-14" d="M1490.89,3204.23l30-10,21.33-33.33-37.33-23.33-238.67-74.67-220-69.33-268.78-125.15s-51.89,7.82-88.55-38.18l-66.67-20.67,14,30.67-54-15.33s-6,2,5.33,8.67c0,0,49.11,7.86,32,24,0,0,96.35,36.77,100.36,50.25l-68.36-19.58-17.6,4.73c-39.6-8.6-38.4,3.6-38.4,3.6-7,44,116.3,104.16,116.3,104.16l42.7,44.84,233,89.5c53,21,271.5,66,271.5,66,169.55,30.18,232.5,15,232.5,15,22.5-2.5,44,12.5,44,12.5h25c6-20-69.67-24.33-69.67-24.33Zm-282.57-98.36l180.93,51.94-173.02-66.91-47.64-24.31,35.79,11.08,283.18,68.56-55.33-6,7.33,13.33h-12l53.33,30.67-120.08-15.08c-65.5-14.9-119.23-42.76-152.49-63.28Z"/>
+      <polygon class="cls-8" points="1331.18 1565.17 1815.66 1590.67 1835.86 1673.32 1824.66 1757.16 1764.01 1883.32 1592.17 1800.56 1331.18 1565.17"/>
+      <path class="cls-11" d="M1790.16,1721.56l-30.6,40s-197.88-35.68-312.44-112.84l-412.99-219.76,20.43,35.6,476,522,39,44,187.56-247.42s27.44,21.42,13.44,77.42l19.6-32.1s17.6-83.9,0-106.9Z"/>
+      <path class="cls-7" d="M1034.13,1428.96l513.05,523.19s-82.5-143.99,181.49-157.49c0,0-286.29-67.5-227.89-146.99l-37.6-22.6s133.49,28.71,226.49-8.85c0,0,133.49-21.69,127.49,57.09l-27,48.24,3,112.1,24.19-76.5,43.01-140.6-54.81-42.48-414-37.52-188-38-120-65s-53.87-21.2-49.43-4.6Z"/>
+      <path class="cls-8" d="M853.56,1152.23l295.79-106.17,324.21-254.5,128-20.01,118,38.01,186.59,60-8.52-102.1-72.07-213.9-26-34-457.88-46.01-150.12-5.32h-206s-194.67,6-227.33,45.33l-26,84.67-12.67,50.67,12,298.67,45.33,142s19.33,42,76.67,62.67Z"/>
+      <g>
+        <path class="cls-11" d="M1833.66,522.89l-484.48-43.44c-170.99-13.28-271.47,0-271.47,0l339.85,68.77-433,19.5,32.05,162.04-168.05,227.8-34,74.72c140-93.28,283,37.28,283,37.28,55.58-6.33,247.12-174.85,247.12-174.85,84-143.99,256.49-116.99,256.49-116.99l296.98,84.44v-129.65l-64.5-209.62Z"/>
+        <path class="cls-11" d="M747.06,528.56l-29.5,121,23,306,56.8,90.5v-8.82l17.2-547.52c-15.09-7.75-67.5,38.83-67.5,38.83Z"/>
+      </g>
+      <path class="cls-7" d="M740.56,955.56l56.8,81.68s62.84-244.07,305.83-358.29c0,0-3.28-69.23-118.63-111.23l105-4.16,274.67,20s440.66,24.94,466.33-20c0,0,95,224,72,300l-263-84s-89-104-138-117c0,0-103-32-143,32,0,0-368,113.55-485,294.28,0,0,157-33.43,238,43.45l19,22.28-57,24s-89.43-32.09-117-51c-26.49-18.16-88-40.64-128,11l44,106-15,10-76-49s-45-131-41-150Z"/>
+      <path class="cls-7" d="M1464.89,2126.23l36.67-20.67,62.67-71.33-123.67-144.67s-16.33,186.33,24.33,236.67Z"/>
+      <path class="cls-14" d="M740.31,522.89s50.25-43.33,180.25-51.33c0,0,320-7,365,6l562,60.66,7.67-64.67s-20.33-37.65-72.67-49.65l-90.67-19.67-179.67-10.67-195.33-1.33-190-10s-310.67,2-378.67,55.33c0,0-39.84,45.33-7.92,85.33Z"/>
+      <path class="cls-6" d="M738.53,507.87s31.43-72.55,272.03-65.31c63.72,1.92,255-2.21,255-2.21l-93-19.79,314.96-18.9-24.62-9.43h-154l-251.33-10s-255.04,11.64-296,49.33c0,0-64.06,41.96-23.03,76.31Z"/>
+      <path class="cls-13" d="M3986.56,3986.56h-1313l-826.52-643.41-151.48-203.59,81.1-1153,21.32-461-674.42-77.33s-354.45-127.95-266-292.5l306-106.66,265.71-238.83s164.29-92.67,316.29,13.33c0,0,500,156,698,352,0,0,167.52,115.03,120.76,398.52,0,0,87.24,369.48,135.24,475.48,0,0,106,206,322,350l965,558.52v1028.48Z"/>
+      <path class="cls-1" d="M900.82,1135.27s40.24,58.3,121.74,57.3l185-69.5s-16.5-57.5-44-74l-262.74,86.2Z"/>
+      <g>
+        <path class="cls-9" d="M1972.18,1495.83l44.95-90.27-6.48-80.38,56.91-133.62-72,16.23v-116.23c-28.59,171.36-79.69,154.97-79.69,154.97-40.79,80.96-42.31-34.23-42.31-34.23-56.09,127.11-37.7,7.88-37.7,7.88-69.8,137.99-78.7-19.5-78.7-19.5-52.5,185.99-45-27-45-27-58.5,149.99-73.63-33.07-73.63-33.07l-13.36,11.61c6-25.96-35.61-230.67-35.61-230.67-13.11,313.34-99.38,178.64-99.38,178.64-15,232.49-124.49,52.03-124.49,52.03-15,173.53-61.5,46.96-61.5,46.96-51,190.49-59.72-46.96-59.72-46.96-22.22,158.53-100.77,55.56-100.77,55.56-46.43,104.72-179.99,24.4-179.99,24.4-27,72-119.99,0-119.99,0,6.07,102.91,223.21,195.59,223.21,195.59l229.77,65.76,520.47,32.02,155.02-29.73Z"/>
+        <path class="cls-9" d="M2178.23,3003.73s28.59-56.54,324.2,116.76l690.13,325.07s-359.89-248.81-740-460.71c0,0-112.15-104.08,175.88-186.74,0,0-306.03-154.67-498.05,97.36l-143.83,62.62,83.08,26.77-95.64,13.88c-5.83-61.16,65.13-118.83,65.13-118.83-118.35,62.44-10.75-70.34-10.75-70.34,228.02-115.91,360.04-337.13,360.04-337.13-93.62,81.81-332.69,174.02-332.69,174.02,46.78-18.95,102.01-79.95,152.99-149.22,48.41-38.03,106.26-90,160.84-155.67l262-330s-50.95,300.03-407.48,522.02c0,0,215.48-74.02,407.48-366.02,0,0-135.29,240.39-272,354,0,0,30.7,24.36-80.65,115.18l124.65-65.18s276-272,310-340v-128l-142-328-12-334s14.2-291.04-204.9-295.52c0,0,124.98,149.68-63.1,233.52,0,0,342.8-66.46-143.16,245.51l221.1,62.07-221.1,29.74s99.84,56.61,49.5,127.64h123.66s-124.92,182.72-382,211.04c0,0,210.6,51.25,331.25-18.56l-206.43,252.56s223.17-158,361.17-298c0,0-45.95,193.4-247.76,309.57l-160.66,26.61c-86.26,8.32-143.57-355.33-143.57-355.33-50.7,55.09-28.31,360.67,3.5,633.1l-3.5,4.06,4.73,6.39c19.4,164.42,41.96,315.34,53.54,389.82l-219.67,57.72-28.6,90.07-9,23-53-12-54,54s37,16.36-137,14.68c0,0,248.61,260.83,493.09,238.33l202.49,79.5,298.48,250.49,206.99,185h371.86s-453.05-332.01-516.05-467.03c0,0-405.74-252.03-193.87-264.03l448.89,489.05s-352.58-495.81-453.04-594.06l-85.85-60.94-74,114s-52.34,53.87-38.17-53.06l86.84-146.76Zm305.33-995.84l90-21.33-204,251.76s0,0,0,0l-53.79,22.61,167.79-253.03Z"/>
+        <polygon class="cls-9" points="2713.56 2259.56 2602.88 2366.49 3986.56 3243.51 3986.56 2997.48 2713.56 2259.56"/>
+      </g>
+      <g>
+        <path d="M1986.56,2933.1c58.41-63,261.08-145.49,261.08-145.49-220.49,61.5-261.08,145.49-261.08,145.49Z"/>
+        <polygon points="2203.56 3023.1 2147.14 3135.59 2310.63 2933.1 2203.56 3042.6 2325.63 2798.11 2247.64 2918.1 2247.64 2880.61 2203.56 3023.1"/>
+        <path d="M2452.56,2984.85l740,460.71c-108-140-740-460.71-740-460.71Z"/>
+        <path d="M2602.88,3172.29l740,460.71c-108-140-740-460.71-740-460.71Z"/>
+        <path d="M2185.79,2966.61l17.77-17.05c-5,3.42-11.05,9.35-17.77,17.05Z"/>
+        <path d="M1062.7,2045.15l214.49,66c99-200.99-15-269.99-15-269.99l-148.49-7.5c-169.24,78.31-51,211.49-51,211.49Zm197.86-190.59s91,86,7,238l-191-57s-102-120,40-188l144,7Z"/>
+        <path d="M774.71,1682.17s-113.99,96-70.5,254.99c0,0-27-128.99,70.5-254.99Z"/>
+        <path d="M741.71,1919.15s-4.3-103,79.5-205.49c0,0-117.12,102.34-79.5,205.49Z"/>
+        <path d="M1163.56,1824.56s-114-43.81-160,94.59c0,0,45-109.59,160-94.59Z"/>
+        <path d="M1259.56,1874.56c42,101,0,195,0,195,66-97,0-195,0-195Z"/>
+        <path d="M687.74,2071.56s-19.27,116.68-10.18,161l10.18-161Z"/>
+        <path d="M835.56,2183.56s-115.65,22-20.33,121c0,0-74.67-83,20.33-121Z"/>
+        <path d="M891.56,2158.14s-57,55.42-47,151.42c0,0,6-91.84,47-151.42Z"/>
+        <path d="M3090.49,2400.42c-44.62-27.29-130.04-88.33-169.38-122.78-38.51-33.73-109.67-108-140.99-148.49-19.86-25.67-58.63-78.43-72-108-27.52-60.88-75-256.49-75-256.49-22.5-63-43.5-365.98-43.5-365.98,12-136.49-127.49-239.99-127.49-239.99-87-70.5-301.49-188.99-301.49-188.99-71.08-47.01-156.15-86.09-234.87-116.6l-11.12-63.39c0-149.99-60-233.99-60-233.99v-19.5l4.9-54.66c-1-33-38-52-38-52-85-53-504-44-504-44-166-24-343-8-343-8-201,22-229,51-229,51-48.99,35-13.15,102.98-13.14,103l-.86,36-24,71,13.15,316.56h0c-36.48,15.52-175.05,83.24-213.15,230.44,0,0-32,140-20,176,0,0,0,65.46,90.16,125.29-22.82,11.06-93.46,50.05-113.49,117.37,0,0-38.67,134.67,7.33,203.33,0,0,27.81,49.32,69.75,96.66-37.51,5.47-133,28.31-157.75,120.34-11.79,43.85-29.6,114.04,15,181,32.34,48.56,75.04,107.42,173.92,146.05-42.13,10.24-144.87,47.78-147.25,170.52,0,0,5.04,91.72,42,126,21.44,19.89,76.5,88.5,76.5,88.5,0,0,16.72,20.23,32.63,32.4-5.25,18.08-10.8,42.42-10.8,64.54,0,0-16,68,10,98,0,0,166,168,558,264,0,0,149.77,25.93,277.29,37.93,26.31,30.8,210.04,233.22,520.31,265.08l149.99,54s67.5,60,111,94.5c0,0,95.99,93,182.99,147l236.43,192.5h62s-106-105-68-149l-66,16s-232-118-140-394c0,0-74.08,150.13-56,238-136.57-83-157.15-95.61-165.33-150.99-3.46-23.44-4.08-51.09-3.57-79.44,68.8,43.26,160.9,114.42,160.9,114.42-36-72-91.16-111.97-132-136-6.74-3.96-27.8-14.32-27.8-14.32,1.58-39.09,3.91-76.71,2.47-103.66-1.69-31.51,25.33-238.02,25.33-238.02l-74,114v68s-92,2-54-86l-32,36,24-76s73.89-128.17,118.23-178.95l-80.23,76.95,142-220s-170,160-198,300c0,0-36,160-78,62,0,0-8.79-193.1-13.21-319.53,243.83-50.55,611.23-423.85,738.66-560.06,175.16,97.92,1079.38,607.53,1289.55,809.58v-259.09s-681.65-324.88-896.07-456.04Zm-591.06-1131.11c96.01,225.02-99.01,393.04-99.01,393.04-5.66,4.1-11.24,8.39-16.73,12.83l-184.29-50.1,132.01-51-183.02,51,221.1,62.07c-22.33,19.58-42.85,41.12-60.54,61.54-70.54-15.13-285.66-59.67-330.99-50.33l318.49,65.1c-32.04,38.81-52.05,70.21-52.05,70.21l179.65-84.3c41.64,60.01-248.66,408.78-248.66,408.78l248.66-219.77c-60.37,273.03-371.67,448.76-371.67,448.76,22.46,6.28,45.91,6.27,69.89,1.28l-164.72,179.15,307.47-255.92c155.2-125.87,294.41-355.26,294.41-355.26-52.86,310.81-491.88,531.05-491.88,531.05,29.5,12.43,62.87,9.25,98.23-5.05-85.27,67.41-195.82,144.34-246.23,135.18,0,0-50-238-32-448,0,0-14-262-50-382,0,0-10-118.97,56-213.48l78.62-78.25-15.8-20.27,60.76-70-.45-14.27c66.05-67.45,165.01-170.48,182.72-199.99l-57.01,36-30-117.01v156.02l-96.04,114.51-1.18-37.34,38.39-49.35,14-102.57s-112,240-190,278c0,0-18-34-22-62,0,0-174,96-234-74,0,0,44-324-4-412,0,0,2,36-6,64l-22-64s41.69,338-40,422c0,0-94,92-206,68,0,0-180.74,110-452-118,0,0-56-70-22-140l26.74-7.86c16.71,17.47,57.94,52.87,122.26,58.86l204-77s-4.93-53.37-37.84-76.78c87.29-82.82,300.61-234.52,300.61-234.52,63.01-45,186.02,0,186.02,0,603.06,195.02,828.08,453.04,828.08,453.04Zm-889.87,841.84l110-111.58-320.88,1100.74c-75.81-26.35-173.22-63.23-232.1-79.8h0s-.02,0-.03,0c-5.46-1.54-10.59-2.9-15.33-4.06-100.39-28.26-266.1-105.51-347.65-151.87-64.43-36.62-175.5-80.66-206.65-92.78l894.65-652.22s68-57,87-86c0,0,169-178,180-258,0,0-108.77,143.09-188.77,241.86l-535.66-588.46c11.09,4.35,22.81,8.46,35.14,12.25l356.41,339.96-33-87,51,27.39v-66.39l12-22.5-124.49-67.5,131.99,46.5s94.5,24,166.49,3c0,0,156.65-40.27,217.08,19.09-19.79,74.23-46.66,162.78-66.79,182.65l-1.27,12.05-169.13,282.69Zm161.09-206.29l-26.68,253.29-76.25,1001.42s-.23,2.77-.54,7.67c0,0-153.56-30.98-203.62-45.67-14.49-4.25-31.8-9.91-50.84-16.42l336.84-1155.58,21.09-44.71Zm-1183.06,876.12c192.08,66.32,196.97,96.58,196.97,96.58l-22.03,7.08c-57.68-22.1-99.73-39.12-106.97-43.08,0,0,127,85,284,145,0,0-148.94-43.12-198-68-36.67-18.59-114-57.44-159.23-41.06-2.27-14.78-4.25-38.53,5.23-43.94,0,0-5.75-38.65,.03-52.58Zm538.66-413.04l-108.69-65.38s-53.13-77.37,18-136c0,0,143-13,198,0l-45.18,157.29-62.13,44.09Zm317.31-450.38l110.89,118.67c-23.8,29-43.86,52.39-55.89,64.33,0,0-10.19,6.55-25.89,16.86l-29.11-199.86Zm-527.9-777.54l249.9-73.41c10.59,12.48,26.98,34.18,29,50.95l-175,65s-79.79-3.51-103.9-42.54Zm-163.45-695.29c130.49-79.5,565.47-45,565.47-45,75-6,368.98,12,368.98,12-91.92-1.5-410.98,19.5-410.98,19.5l490.87,3.33c66,2,82,44,82,44l-2.12,40.74c-79.22-99.91-654.75-56.58-654.75-56.58l-226.13-6.17c-162.76,3.28-212.24,31.98-227.03,51.31-10.42-45.61,13.68-63.15,13.68-63.15Zm4.5,93c28.5-81,460.48-54,460.48-54l592.68,78.92c-93.79,45.29-720.31,.91-720.31,.91,564.46,59.33,708.48,36.03,743.4,24.46,30.28,62.25,47.19,209.19,47.19,209.19l3.79,40.22c-113.65-41.11-204.78-62.72-204.78-62.72-106.61-44.95-202.49-10.5-202.49-10.5-59.12,17.35-158.99,119.99-158.99,119.99-30,37.5-170.99,152.99-170.99,152.99l-285.13,105.36-110.85-181.86-19.5-310.48,25.5-112.49Zm80.93,591.55l-43.39-24.09-50.69-133.63,94.08,157.72Zm-319.02,260.4c-14.74-44.17-12.18-108.56,25.11-208.49,23.29-62.43,57.69-130.37,194.57-179.73l34.95,92.07-55.53-30.83,122.99,97.5s-43.5,66,27,148.49c0,0,43.56,66.64,149.11,113.07,4.63,16.01,31.94,121.14-26.12,194.41l-106.49,10.5-94.4-64.16,110.59,62.56c32.69-88.59,13.8-224.89,13.8-224.89-143.58-54.77-176.99-34.5-176.99-34.5-40.93,34.69-45.92,71.94-39.47,101.96-10.66-9.85-22.63-26.19-25.03-50.97,0,0-45,88.5-88.5-27,0,0-32.28,40.64-65.6,0Zm373.94,50.88c-22-27-141-66-141-66,0,0,55-3,153,34,0,0,14,99-10,201l-152-83s-63.22-91.42-7.23-140.79c85.17,12.96,142.23,58.79,142.23,58.79,11,11,10,134,10,134,15.05-63,5-138,5-138Zm-391,356c-27.68-67.97-55.73-194.95,78-283,0,0-19,45,9,65,0,0,144-10.98,225,46.51l51,26.49s123-12,159,5l250.9,93.08c34.62,22.71,63.75,48.7,81.1,77.92,0,0,119.28,182.79-27.13,312.46l-305.87-82.46s-83.47-21.73-173.73-48.56c2.27-28.71,12.76-100.36,60.88-192.59,0,0-73.52,94.62-63.61,191.78-94.41-28.13-195.39-61.66-215.54-81.62-32.52-32.23-107.57-77.37-129-130Zm-71,401c-33.63-64.1-84.83-194.41,112-267,0,0,10.4,42,191.7,93,0,0,9.27,2.97,23.75,9.16-15.52,17.24-50.06,58.08-55.46,85.84,0,0,36.56-63.22,67.14-80.64,9.75,4.52,20.91,10.14,32.7,16.91-32.94,15.64-116.8,69.92-99.84,211.88,0,0-4.36-142.65,101.87-210.71,5.99,3.49,12.13,7.27,18.32,11.36-11.83,21.86-30.49,57.69-33.19,71.2l-31.78,52,37.39-44.25s18.4-47.16,36.54-72.84c4.97,3.5,9.94,7.2,14.85,11.09,0,0,107.04-5.17,251,21l208,54s4.67,51.39,1.65,79.96l-94.99,67.41,47.34-149.29c-34.12-25.5-229.86,0-229.86,0-99.91,59.06-26.14,154.49-26.14,154.49l104.75,68.53-43.36,30.77-181.4-48.87s-39.41-12.96-94.72-22.91c-140.68-76.17-228.6-236.01-228.6-236.01,50.93,136.66,166,207.37,222.47,234.93-52.35-9.01-117.65-14.93-177.16-5.01,0,0-112.46-16.78-175-136Zm38.17,342.57s-28.43-147.17,104.99-164.99c0,0,80.14-20.42,187.49-6,0,0,145.25,31.34,238.72,88.44l-208.83,155.96c-40.98,9.1-275.86,55.17-322.37-73.41Zm665.83,641.43s-272-51-546-254c0,0-23-25-4-38,0,0,50.99-8.73,156,39,87.72,39.87,361,135,361,135l290,95-280-121,382.82,102.89,31.18,19.11-84-16,62,46-18.8,10.58c-103.45-14.75-414.56-129.04-414.56-129.04-93-33-373.48-145.49-373.48-145.49-172.49-70.5-64.5,18-64.5,18,81,70.5,202.49,119.99,202.49,119.99-66.61-42-4.5-61.5-4.5-61.5l119.99,48-49.5,18,25.5,21-61.5-13.5c37.5,28.5,200.99,76.5,200.99,76.5,317.16,76.56,406.25,67.79,431.25,57.42l13.62,1.05s33.82,5.94,42,16c0,0-198.82,9-418-45Zm827,206s-126-28-166-90c0,0,60,124,226,130l22,60,32-42s20.2,43.8,46.63,83.76l-42.63-19.76-146-54s-319.88-40.31-487.14-219.06c80.51,5.95,144.15,4.15,141.14-16.94l56-42,22.79,5.36c-1.94,44.13-2.92,139.4,21.21,184.64v-100s18,94,68,122c0,0-34-74-38-110,0,0,68,128,134,148,0,0-160-174-156-288,0,0,130,160,266,248Zm-4.12-548.38c-.37-11.48-.69-22.11-.94-31.62,0,0,261.75-146,322.4-226,0,0-275.35,168-349.35,116,0,0,233.41-159.91,320.78-266.06,177.01-136.48,366.1-444.94,366.1-444.94,30,216.2-402.04,548.87-402.04,548.87,219.02-36,456.05-486.05,456.05-486.05l-36,159.02,45.75-37.87c-62.8,148.05-320.63,351.03-320.63,351.03,52.81-8.02,113.66-48.9,171.28-99.78-185.79,193.99-454.16,352.12-573.4,417.4Z"/>
+        <path d="M1667.72,729.76c-497.34-210.82-846.51,302.52-846.51,302.52,402.78-449.48,774.3-331.56,846.51-302.52Z"/>
+        <path d="M1667.72,729.76c4.2,1.78,8.41,3.61,12.63,5.49,0,0-4.35-2.16-12.63-5.49Z"/>
+        <path d="M857.56,1013.56c148-30,254,18.72,254,18.72-98-85.28-254-18.72-254-18.72Z"/>
+        <polygon points="2064.39 1389.32 2331.41 1260.31 2178.23 1314.31 2229.4 1212.3 2064.39 1389.32"/>
+        <path d="M2229.4,1362.01c-39,47.7-201.02,131.53-201.02,131.53,141.01-42.79,201.02-131.53,201.02-131.53Z"/>
+        <path d="M2077.89,1525.56c-28.5,22.23-121.51,43.78-121.51,43.78,87.71,22.75,121.51-43.78,121.51-43.78Z"/>
+        <path d="M2388.42,1389.32c-105.01,159.02-324.03,213.02-324.03,213.02,246.02-27,324.03-213.02,324.03-213.02Z"/>
+        <path d="M2278.91,1937.15l-418.54-169.8c126.01,72.01,418.54,169.8,418.54,169.8Z"/>
+        <path d="M2095.56,1625.07c-90.09,10.38-235.19,96.49-235.19,96.49l235.19-96.49Z"/>
+        <path d="M1986.56,1767.36c19,13.8,344.85,122.2,344.85,122.2-102.15-46-344.85-122.2-344.85-122.2Z"/>
+        <path d="M2369.56,2011.56c-48,92-305.17,278-305.17,278,264.83-134,305.17-278,305.17-278Z"/>
+        <path d="M603.56,2575.56s-50-130,30-196c0,0-105.28,59.5-30,196Z"/>
+        <path d="M750.71,2397.63s-75,78-13.5,170.99c0,0-43.5-90,13.5-170.99Z"/>
+        <path d="M777.56,2390.13s-35.85,82.5-11.85,134.99c0,0-7.8-67.5,11.85-134.99Z"/>
+        <polygon points="1913.56 1389.32 1995.56 1240.44 2069.64 1101.56 2003.56 1183.56 1995.56 1091.56 1977.96 1233.56 1913.56 1389.32"/>
+        <path d="M1873.56,1362.01s80.68-147.25,113-308.45l-113,308.45Z"/>
+        <path d="M2289.41,3150.5c153.54,199.21,453.04,594.06,453.04,594.06-126.01-242-453.04-594.06-453.04-594.06Z"/>
+        <path d="M1939.56,2055.56c436,28,430-246,430-246-40,256-430,246-430,246Z"/>
+        <path d="M1913.56,2259.56s146.68-5.98,284.34-201.99c0,0-148.34,165.99-284.34,201.99Z"/>
+      </g>
+      <ellipse class="cls-1" cx="1158.06" cy="1868.57" rx="61" ry="14.75"/>
+      <g>
+        <path class="cls-5" d="M1463.18,1625.07c248.99,77.14,313.48,175.49,313.48,175.49,0-141-313.48-175.49-313.48-175.49Z"/>
+        <path class="cls-5" d="M900.82,536.22c114.11,39,247.37,0,247.37,0-122.99-30-247.37,0-247.37,0Z"/>
+      </g>
+    </g>
+  </g>
+</svg>
\ No newline at end of file
diff --git a/website/css-modules.html b/website/css-modules.html
new file mode 100644
index 0000000..b9b53a8
--- /dev/null
+++ b/website/css-modules.html
@@ -0,0 +1 @@
+<include src="website/include/layout.html" locals='{"title": "CSS Modules", "url": "css-modules.html", "page": "website/pages/css-modules.md"}' />
diff --git a/website/docs.css b/website/docs.css
new file mode 100644
index 0000000..c071162
--- /dev/null
+++ b/website/docs.css
@@ -0,0 +1,303 @@
+@import "synthwave.css";
+
+html {
+  color-scheme: dark;
+  background: #111;
+  font-family: system-ui;
+  --gold: lch(80% 82.34 80.104);
+  --gold-text: lch(85% 82.34 80.104);
+  --gold-shadow: lch(80% 82.34 80.104 / .7);
+}
+
+@font-face {
+  font-family:"din-1451-lt-pro-engschrift";
+  src:url("https://use.typekit.net/af/7fa6e1/00000000000000007735bbcd/30/l?primer=388f68b35a7cbf1ee3543172445c23e26935269fadd3b392a13ac7b2903677eb&fvd=n4&v=3") format("woff2"),url("https://use.typekit.net/af/7fa6e1/00000000000000007735bbcd/30/d?primer=388f68b35a7cbf1ee3543172445c23e26935269fadd3b392a13ac7b2903677eb&fvd=n4&v=3") format("woff"),url("https://use.typekit.net/af/7fa6e1/00000000000000007735bbcd/30/a?primer=388f68b35a7cbf1ee3543172445c23e26935269fadd3b392a13ac7b2903677eb&fvd=n4&v=3") format("opentype");
+  font-display:auto;font-style:normal;font-weight:400;font-stretch:normal;
+}
+
+@font-face {
+  font-family:"urbane-rounded";
+  src:url("https://use.typekit.net/af/916187/00000000000000007735bfa0/30/l?primer=81a69539b194230396845be9681d114557adfb35f4cccc679c164afb4aa47365&fvd=n6&v=3") format("woff2"),url("https://use.typekit.net/af/916187/00000000000000007735bfa0/30/d?primer=81a69539b194230396845be9681d114557adfb35f4cccc679c164afb4aa47365&fvd=n6&v=3") format("woff"),url("https://use.typekit.net/af/916187/00000000000000007735bfa0/30/a?primer=81a69539b194230396845be9681d114557adfb35f4cccc679c164afb4aa47365&fvd=n6&v=3") format("opentype");
+  font-display:auto;font-style:normal;font-weight:600;font-stretch:normal;
+}
+
+header {
+  max-width: 800px;
+  width: 100%;
+  margin: 0 auto;
+  padding: 50px 0;
+  font-size: 16px;
+  background: radial-gradient(closest-side, lch(80% 82.34 80.104 / .25), transparent);
+  display: grid;
+  column-gap: 30px;
+  grid-area: header;
+  grid-template-areas: "logo header"
+                        "logo subheader"
+                        ". links";
+}
+
+header svg {
+  filter: drop-shadow(0 0 5px var(--gold-shadow)) drop-shadow(0 0 15px var(--gold-shadow));
+  grid-area: logo;
+  place-self: center end;
+  width: 50px;
+}
+
+header svg .outer {
+  stroke-width: 30px;
+  stroke: var(--gold);
+}
+
+header svg .inner {
+  fill: lch(100% 82.34 80.104);
+}
+
+header .title {
+  font-family: urbane-rounded, ui-rounded;
+  font-size: 60px;
+  font-weight: 600;
+  -webkit-text-stroke: 2px var(--gold-text);
+  color: transparent;
+  filter: drop-shadow(0 0 3px var(--gold-shadow)) drop-shadow(0 0 10px var(--gold));
+  margin: 0;
+  letter-spacing: -0.02em;
+  text-decoration: none;
+}
+
+header .title::selection {
+  -webkit-text-stroke-color: #fffddd;
+  background-color: var(--gold-text);
+}
+
+h1, h2, h3 {
+  font-family: urbane-rounded, ui-rounded;
+  font-weight: 600;
+  color: lch(65% 85 35);
+  margin: 2em 0 .5em 0;
+  letter-spacing: -0.02em;
+}
+
+h1 {
+  margin-top: 0;
+}
+
+header p {
+  grid-area: links;
+  margin: 0;
+}
+
+header p a {
+  font-family: urbane-rounded, ui-rounded;
+  font-weight: 600;
+  font-size: 1em;
+  color: lch(90% 50.34 80.104);
+  filter: drop-shadow(0 0 8px lch(90% 50.34 80.104 / .7));
+  text-decoration-color: lch(90% 50.34 80.104 / 0);
+  text-decoration-style: wavy;
+  text-decoration-thickness: 2px;
+  text-underline-offset: 2px;
+  text-decoration-skip-ink: none;
+  transition: text-decoration-color 150ms;
+}
+
+header a:hover {
+  text-decoration-color: lch(90% 50.34 80.104);
+}
+
+@media (width < 500px) {
+  header {
+    grid-template-areas: "logo"
+                          "header"
+                          "subheader"
+                          "links";
+    place-items: center;
+    text-align: center;
+    gap: 8px;
+  }
+  header .title {
+    font-size: 38px;
+    -webkit-text-stroke-width: 1.5px;
+    padding: 0;
+  }
+
+  header h2 {
+    font-size: 14px;
+  }
+
+  header p a {
+    font-size: 13px;
+  }
+
+  header svg {
+    place-self: center;
+  }
+}
+
+body {
+  --body-padding: 20px;
+  padding: 0 var(--body-padding);
+  margin: 0 auto;
+  width: fit-content;
+  display: grid;
+  grid-template-columns: 180px 1fr;
+  gap: 40px;
+  grid-template-areas: "header header"
+                       "nav    main"
+                       "footer footer";
+}
+
+main {
+  max-width: 800px;
+  padding-right: 240px;
+  grid-area: main;
+  position: relative;
+}
+
+p, li {
+  line-height: 1.5em;
+}
+
+p:empty {
+  display: none;
+}
+
+a {
+  color: lch(85% 58 205);
+}
+
+nav {
+  grid-area: nav;
+  text-align: end;
+  padding-right: 20px;
+  border-right: 1px solid lch(90% 50.34 80.104 / .1);
+  height: fit-content;
+  position: sticky;
+  top: 40px;
+}
+
+nav h3,
+.table-of-contents h3 {
+  margin-top: 0;
+}
+
+main > aside {
+  position: sticky;
+  top: 40px;
+}
+
+.table-of-contents {
+  position: absolute;
+  left: 100%;
+  margin-left: 40px;
+  border-left: 1px solid lch(90% 50.34 80.104 / .1);
+  padding-left: 20px;
+  overflow: auto;
+  max-height: calc(100vh - 80px);
+}
+
+.table-of-contents ul,
+nav ul {
+  list-style: none;
+  padding-left: 2ch;
+}
+
+.table-of-contents > ul {
+  margin: 0;
+  padding: 0;
+  width: 180px;
+}
+
+nav > ul {
+  margin: 0;
+  padding: 0;
+}
+
+.table-of-contents li,
+nav li {
+  margin: 6px 0;
+  line-height: 1em;
+}
+
+.table-of-contents a,
+nav a {
+  color: lch(90% 50.34 80.104);
+  text-decoration: none;
+  font-family: urbane-rounded;
+  font-size: 14px;
+}
+
+.table-of-contents a:hover,
+.table-of-contents a[aria-current],
+nav a:hover,
+nav a[aria-current] {
+  color: var(--gold-text);
+}
+
+a[aria-current] {
+  text-decoration: underline;
+}
+
+.features {
+  column-count: 2;
+}
+
+@media (width < 1040px) {
+  .table-of-contents {
+    display: none;
+  }
+
+  main {
+    padding-right: 0;
+  }
+
+  .features {
+    column-count: 1;
+  }
+}
+
+@media (width < 600px) {
+  body {
+    display: block;
+    width: auto;
+  }
+
+  nav {
+    text-align: start;
+    border-right: none;
+    border-bottom: 1px solid lch(90% 50.34 80.104 / .1);
+    padding-bottom: 20px;
+    position: static;
+  }
+}
+
+.warning {
+  border: 4px solid lch(70% 82.34 80.104);
+  background: lch(80% 82.34 80.104 / .15);
+  padding: 20px;
+  border-radius: 8px;
+  margin: 20px 0;
+}
+
+.warning > :first-child {
+  margin-top: 0;
+}
+
+.warning > :last-child {
+  margin-bottom: 0;
+}
+
+.warning pre {
+  background: rgb(0 0 0 / .65);
+}
+
+.warning :is(h1, h2, h3) {
+  color: white;
+}
+
+footer {
+  font-size: 12px;
+  color: #666;
+  text-align: center;
+  padding-bottom: 20px;
+  grid-area: footer;
+}
diff --git a/website/docs.html b/website/docs.html
new file mode 100644
index 0000000..f6d60f4
--- /dev/null
+++ b/website/docs.html
@@ -0,0 +1 @@
+<include src="website/include/layout.html" locals='{"title": "Getting Started", "url": "docs.html", "page": "website/pages/docs.md"}' />
diff --git a/website/docs.js b/website/docs.js
new file mode 100644
index 0000000..802d17d
--- /dev/null
+++ b/website/docs.js
@@ -0,0 +1,38 @@
+// Mark the current section in the table of contents with aria-current when scrolled into view.
+let tocLinks = document.querySelectorAll('.table-of-contents a');
+let headers = new Map();
+for (let link of tocLinks) {
+  let headerId = link.hash.slice(1);
+  let header = document.getElementById(headerId);
+  headers.set(header, link);
+}
+
+let intersectingHeaders = new Set();
+let observer = new IntersectionObserver(entries => {
+  for (let entry of entries) {
+    if (entry.isIntersecting) {
+      intersectingHeaders.add(entry.target);
+    } else {
+      intersectingHeaders.delete(entry.target);
+    }
+  }
+
+  if (intersectingHeaders.size > 0) {
+    let current = document.querySelector('.table-of-contents a[aria-current]');
+    if (current) {
+      current.removeAttribute('aria-current');
+    }
+    let first;
+    for (let [header, link] of headers) {
+      if (intersectingHeaders.has(header)) {
+        first = link;
+        break;
+      }
+    }
+    first.setAttribute('aria-current', 'location');
+  }
+});
+
+for (let header of headers.keys()) {
+  observer.observe(header);
+}
diff --git a/website/favicon.svg b/website/favicon.svg
new file mode 100644
index 0000000..d58014d
--- /dev/null
+++ b/website/favicon.svg
@@ -0,0 +1,3 @@
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="495 168 360 654">
+  <path stroke-width="30" stroke="#f9bb03" d="M594.41,805c-.71,0-1.43-.15-2.11-.47-2.2-1.03-3.34-3.48-2.72-5.83l67.98-253.71h-140.45c-1.86,0-3.57-1.04-4.44-2.69-.86-1.65-.73-3.65,.34-5.18l26.85-38.35q25.56-36.51,104.91-149.83l106.31-151.82c1.39-1.99,4.01-2.69,6.21-1.66,2.2,1.03,3.34,3.48,2.72,5.83l-67.98,253.71h140.45c1.86,0,3.57,1.04,4.43,2.69,.86,1.65,.73,3.65-.34,5.18l-238.07,340c-.96,1.37-2.51,2.13-4.1,2.13Zm-67.69-270h137.37c1.55,0,3.02,.72,3.97,1.96,.95,1.23,1.27,2.84,.86,4.34l-62.33,232.61,216.29-308.9h-137.36c-1.55,0-3.02-.72-3.97-1.96-.95-1.23-1.27-2.84-.86-4.34l62.33-232.61-90.04,128.59q-79.35,113.32-104.91,149.83l-21.34,30.48Z"/>
+</svg>
diff --git a/website/include/layout.html b/website/include/layout.html
new file mode 100644
index 0000000..772a152
--- /dev/null
+++ b/website/include/layout.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
+    <title>{{ title }} – Lightning CSS</title>
+    <link rel="icon" href="favicon.svg">
+    <link rel="mask-icon" href="favicon.svg" color="#f9bb03">
+    <meta name="description" content="An extremely fast CSS parser, transformer, bundler, and minifier.">
+    <meta name="twitter:card" content="summary_large_image">
+    <meta name="twitter:image" content="og.jpeg">
+    <meta name="twitter:site" content="@lightningcss">
+    <meta name="twitter:creator" content="@lightningcss">
+    <meta property="og:type" content="website">
+    <meta property="og:locale" content="en_US">
+    <meta property="og:url" content="https://lightningcss.dev/{{ url }}">
+    <meta property="og:title" content="{{ title }} – Lightning CSS">
+    <meta property="og:description" content="An extremely fast CSS parser, transformer, bundler, and minifier.">
+    <meta property="og:image" content="og.jpeg">
+    <link rel="stylesheet" href="docs.css">
+  </head>
+  <body>
+    <header>
+      <svg viewBox="495 168 360 654">
+        <path class="outer" d="M594.41,805c-.71,0-1.43-.15-2.11-.47-2.2-1.03-3.34-3.48-2.72-5.83l67.98-253.71h-140.45c-1.86,0-3.57-1.04-4.44-2.69-.86-1.65-.73-3.65,.34-5.18l26.85-38.35q25.56-36.51,104.91-149.83l106.31-151.82c1.39-1.99,4.01-2.69,6.21-1.66,2.2,1.03,3.34,3.48,2.72,5.83l-67.98,253.71h140.45c1.86,0,3.57,1.04,4.43,2.69,.86,1.65,.73,3.65-.34,5.18l-238.07,340c-.96,1.37-2.51,2.13-4.1,2.13Zm-67.69-270h137.37c1.55,0,3.02,.72,3.97,1.96,.95,1.23,1.27,2.84,.86,4.34l-62.33,232.61,216.29-308.9h-137.36c-1.55,0-3.02-.72-3.97-1.96-.95-1.23-1.27-2.84-.86-4.34l62.33-232.61-90.04,128.59q-79.35,113.32-104.91,149.83l-21.34,30.48Z"/>
+        <path class="inner" d="M594.41,805c-.71,0-1.43-.15-2.11-.47-2.2-1.03-3.34-3.48-2.72-5.83l67.98-253.71h-140.45c-1.86,0-3.57-1.04-4.44-2.69-.86-1.65-.73-3.65,.34-5.18l26.85-38.35q25.56-36.51,104.91-149.83l106.31-151.82c1.39-1.99,4.01-2.69,6.21-1.66,2.2,1.03,3.34,3.48,2.72,5.83l-67.98,253.71h140.45c1.86,0,3.57,1.04,4.43,2.69,.86,1.65,.73,3.65-.34,5.18l-238.07,340c-.96,1.37-2.51,2.13-4.1,2.13Zm-67.69-270h137.37c1.55,0,3.02,.72,3.97,1.96,.95,1.23,1.27,2.84,.86,4.34l-62.33,232.61,216.29-308.9h-137.36c-1.55,0-3.02-.72-3.97-1.96-.95-1.23-1.27-2.84-.86-4.34l62.33-232.61-90.04,128.59q-79.35,113.32-104.91,149.83l-21.34,30.48Z"/>
+      </svg>
+      <a href="/" class="title">Lightning CSS</a>
+      <p><a href="./playground/index.html">Playground</a> • <a href="docs.html">Docs</a> • <a href="https://docs.rs/lightningcss" target="_blank">Rust docs</a> • <a href="https://npmjs.com/lightningcss" target="_blank">npm</a>  • <a href="https://github.com/parcel-bundler/lightningcss" target="_blank">GitHub</a></p>
+    </header>
+    <nav>
+      <h3>Docs</h3>
+      <ul>
+        <li><a href="docs.html">Getting started</a></li>
+        <li><a href="transpilation.html">Transpilation</a></li>
+        <li><a href="css-modules.html">CSS Modules</a></li>
+        <li><a href="bundling.html">Bundling</a></li>
+        <li><a href="minification.html">Minification</a></li>
+        <li><a href="transforms.html">Custom transforms</a></li>
+        <script>document.querySelector(`nav a[href="${location.pathname}"]`).setAttribute('aria-current', 'page')</script>
+      </ul>
+    </nav>
+    <main>
+      <markdown src="{{ page }}"></markdown>
+    </main>
+    <footer>
+      Copyright © 2024 Devon Govett and Parcel Contributors.
+    </footer>
+    <script async src="docs.js"></script>
+  </body>
+</html>
diff --git a/website/index.html b/website/index.html
new file mode 100644
index 0000000..1198b2c
--- /dev/null
+++ b/website/index.html
@@ -0,0 +1,935 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
+    <title>Lightning CSS</title>
+    <link rel="icon" href="favicon.svg">
+    <link rel="mask-icon" href="favicon.svg" color="#f9bb03">
+    <meta name="description" content="An extremely fast CSS parser, transformer, bundler, and minifier.">
+    <meta name="twitter:card" content="summary_large_image">
+    <meta name="twitter:image" content="og.jpeg">
+    <meta name="twitter:site" content="@lightningcss">
+    <meta name="twitter:creator" content="@lightningcss">
+    <meta property="og:type" content="website">
+    <meta property="og:locale" content="en_US">
+    <meta property="og:url" content="https://lightningcss.dev">
+    <meta property="og:title" content="Lightning CSS">
+    <meta property="og:description" content="An extremely fast CSS parser, transformer, bundler, and minifier.">
+    <meta property="og:image" content="og.jpeg">
+    <link rel="preload" as="font" type="font/woff2" crossorigin href="https://use.typekit.net/af/916187/00000000000000007735bfa0/30/l?primer=81a69539b194230396845be9681d114557adfb35f4cccc679c164afb4aa47365&fvd=n6&v=3">
+    <link rel="preload" as="image" href="lightspeed.svg">
+  </head>
+  <body>
+    <header>
+      <svg viewBox="495 168 360 654">
+        <path class="outer" d="M594.41,805c-.71,0-1.43-.15-2.11-.47-2.2-1.03-3.34-3.48-2.72-5.83l67.98-253.71h-140.45c-1.86,0-3.57-1.04-4.44-2.69-.86-1.65-.73-3.65,.34-5.18l26.85-38.35q25.56-36.51,104.91-149.83l106.31-151.82c1.39-1.99,4.01-2.69,6.21-1.66,2.2,1.03,3.34,3.48,2.72,5.83l-67.98,253.71h140.45c1.86,0,3.57,1.04,4.43,2.69,.86,1.65,.73,3.65-.34,5.18l-238.07,340c-.96,1.37-2.51,2.13-4.1,2.13Zm-67.69-270h137.37c1.55,0,3.02,.72,3.97,1.96,.95,1.23,1.27,2.84,.86,4.34l-62.33,232.61,216.29-308.9h-137.36c-1.55,0-3.02-.72-3.97-1.96-.95-1.23-1.27-2.84-.86-4.34l62.33-232.61-90.04,128.59q-79.35,113.32-104.91,149.83l-21.34,30.48Z"/>
+        <path class="inner" d="M594.41,805c-.71,0-1.43-.15-2.11-.47-2.2-1.03-3.34-3.48-2.72-5.83l67.98-253.71h-140.45c-1.86,0-3.57-1.04-4.44-2.69-.86-1.65-.73-3.65,.34-5.18l26.85-38.35q25.56-36.51,104.91-149.83l106.31-151.82c1.39-1.99,4.01-2.69,6.21-1.66,2.2,1.03,3.34,3.48,2.72,5.83l-67.98,253.71h140.45c1.86,0,3.57,1.04,4.43,2.69,.86,1.65,.73,3.65-.34,5.18l-238.07,340c-.96,1.37-2.51,2.13-4.1,2.13Zm-67.69-270h137.37c1.55,0,3.02,.72,3.97,1.96,.95,1.23,1.27,2.84,.86,4.34l-62.33,232.61,216.29-308.9h-137.36c-1.55,0-3.02-.72-3.97-1.96-.95-1.23-1.27-2.84-.86-4.34l62.33-232.61-90.04,128.59q-79.35,113.32-104.91,149.83l-21.34,30.48Z"/>
+      </svg>
+      <h1>Lightning CSS</h1>
+      <h2>An extremely fast CSS parser, transformer, bundler, and minifier.</h2>
+      <p><a href="./playground/index.html">Playground</a> • <a href="docs.html">Docs</a> • <a href="https://docs.rs/lightningcss" target="_blank">Rust docs</a> • <a href="https://npmjs.com/lightningcss" target="_blank">npm</a>  • <a href="https://github.com/parcel-bundler/lightningcss" target="_blank">GitHub</a></p>
+    </header>
+    <main>
+      <section class="warp">
+        <h3>Light speed</h3>
+        <p><strong>Lightning CSS is over 100x faster than comparable JavaScript-based tools.</strong> It can minify over 2.7 million lines of code per second on a single thread.</p>
+        <p style="font-size: 0.85em">Lightning CSS is written in Rust, a native systems programming language. It was built with performance in mind from the start, designed to make efficient use of memory, and limit AST passes.</p>
+        <p><a href="docs.html">Get started →</a></p>
+        <figure>
+          <div class="chart" role="img" aria-label="Build time chart. CSSNano – 544ms, ESBuild – 17.2ms, Lightning CSS – 4.16ms" style="max-width: 800px; margin-top: 40px; position: relative; padding-bottom: 110px; white-space: nowrap">
+            <div style="text-align: center; font-weight: bold; font-size: 15px; margin-left: 90px; margin-bottom: 12px">Build time</div>
+            <div style="position: absolute; left: 90px; right: 15px; height: 110px">
+              <div style="position: absolute; top: 0; left: 0; bottom: 20px; width: 1px; background: white"></div>
+              <div style="position: absolute; left: 0; bottom: 0; transform: translateX(-50%); font-size: 12px">0ms</div>
+              <div class="line" style="position: absolute; top: 0; left: 25%; bottom: 20px; width: 1px; background: white"></div>
+              <div class="line" style="position: absolute; left: 25%; bottom: 0; transform: translateX(-50%); font-size: 12px">150ms</div>
+              <div style="position: absolute; top: 0; left: 50%; bottom: 20px; width: 1px; background: white"></div>
+              <div style="position: absolute; left: 50%; bottom: 0; transform: translateX(-50%); font-size: 12px">300ms</div>
+              <div class="line" style="position: absolute; top: 0; left: 75%; bottom: 20px; width: 1px; background: white"></div>
+              <div class="line" style="position: absolute; left: 75%; bottom: 0; transform: translateX(-50%); font-size: 12px">450ms</div>
+              <div style="position: absolute; top: 0; left: 100%; bottom: 20px; width: 1px; background: white"></div>
+              <div style="position: absolute; left: 100%; bottom: 0; transform: translateX(-50%); font-size: 12px">600ms</div>
+              
+              <div class="bars" style="position: absolute; left: 1px; top: 0; right: 70px; --cssnano: 544.81; --esbuild: 17.2; --lightningcss: 4.16">
+                <div style="position: absolute; top: 5px; left: 0; width: 100%; height: 20px; background: var(--gold)"></div>
+                <div class="label" style="position: absolute; left: calc(100% + 5px); top: 5px; line-height: 20px; font-size: 12px">544.81ms</div>
+                <div style="position: absolute; top: 35px; left: 0; width: calc(1% * var(--esbuild) / var(--cssnano) * 100); height: 20px; background: var(--gold)"></div>
+                <div style="position: absolute; left: calc(1% * var(--esbuild) / var(--cssnano) * 100 + 5px); top: 35px; line-height: 20px; font-size: 12px">17.2ms</div>
+                <div style="position: absolute; top: 65px; left: 0; width: calc(1% * var(--lightningcss) / var(--cssnano) * 100); height: 20px; background: var(--gold)"></div>
+                <div style="position: absolute; left: calc(1% * var(--lightningcss) / var(--cssnano) * 100 + 5px); top: 65px; line-height: 20px; font-size: 12px">4.16ms</div>
+              </div>
+              <style>
+                @media (width < 700px) {
+                  .warp .bars {
+                    right: 5px !important;
+                  }
+
+                  .warp .bars .label {
+                    left: auto !important;
+                    right: 5px;
+                    color: black;
+                  }
+                }
+              </style>
+
+              <div style="position: absolute; right: calc(100% + 10px); top: 5px; line-height: 20px; font-size: 13px">CSSNano</div>
+              <div style="position: absolute; right: calc(100% + 10px); top: 35px; line-height: 20px; font-size: 13px">ESBuild</div>
+              <div style="position: absolute; left: -10px; transform: translateX(-100%); top: 65px; line-height: 20px; font-size: 13px">Lightning CSS</div> 
+            </div>
+          </div>
+          <figcaption>Time to minify Bootstrap 4 (~10,000 lines). See the <a href="https://github.com/parcel-bundler/lightningcss#benchmarks" target="_blank">readme</a> for more benchmarks.</figcaption>
+        </figure>
+      </section>
+      <section class="future">
+        <div class="inner">
+          <h3 class="title">Live in the future</h3>
+          <div class="description">
+            <p><strong>Lightning CSS lets you use modern CSS features and future syntax today.</strong> Features such as CSS nesting, custom media queries, high gamut color spaces, logical properties, and new selector features are automatically converted to more compatible syntax based on your browser targets.</p>
+            <p>Lightning CSS also automatically adds vendor prefixes for your browser targets, so you can keep your source code clean and repetition free.</p>
+            <p><a href="transpilation.html">Learn more →</a></p>
+          </div>
+          <div class="example">
+            <div class="targets"><h4>Target Browsers</h4><code>last 2 versions</code></div>
+            <div class="box input"><h4 class="title">Input</h4><pre>
+<code><span class="class">.foo</span> {
+  <span class="property">color</span>: <span class="keyword">oklab</span>(<span class="number">59.686% 0.1009 0.1192</span>);
+}</code></pre></div>
+            <div class="box"><h4 class="title">Output</h4><pre>
+<code><span class="class">.foo</span> {
+  <span class="property">color</span>: <span class="number">#c65d07</span>;
+  <span class="property">color</span>: <span class="keyword">color</span>(<span class="keyword">display-p3</span> <span class="number">.724144 .386777 .148795</span>);
+  <span class="property">color</span>: <span class="keyword">lab</span>(<span class="number">52.2319% 40.1449 59.9171</span>);
+}</code></pre>
+            </div>
+          </div>
+        </div>
+      </section>
+      <section class="crush">
+        <h3>Crush it!</h3>
+        <p style="font-weight: bold;">Lightning CSS is not only fast when it comes to build time. It produces smaller output, so your website loads faster too.</p>
+        <p style="font-size: 0.85em">The Lightning CSS minifier combines longhand properties into shorthands, removes unnecessary vendor prefixes, merges compatible adjacent rules, removes unnecessary default values, reduces <code>calc()</code> expressions, shortens colors, minifies gradients, and much more.</p>
+        <p><a href="minification.html">Details →</a></p>
+        <figure>
+          <div class="chart" role="img" aria-label="Output size chart. CSSNano – 155.89 KB, ESBuild – 156.57 KB, Lightning CSS – 139.74 KB" style="max-width: 800px; margin-top: 40px; position: relative; padding-bottom: 110px; white-space: nowrap">
+            <div style="text-align: center; font-weight: bold; font-size: 15px; margin-left: 90px; margin-bottom: 12px">Output size</div>
+            <div style="position: absolute; left: 90px; right: 15px; height: 110px">
+              <div style="position: absolute; top: 0; left: 0; bottom: 20px; width: 1px; background: black"></div>
+              <div style="position: absolute; left: 0; bottom: 0; transform: translateX(-50%); font-size: 12px">0 KB</div>
+              <div class="line" style="position: absolute; top: 0; left: 25%; bottom: 20px; width: 1px; background: black"></div>
+              <div class="line" style="position: absolute; left: 25%; bottom: 0; transform: translateX(-50%); font-size: 12px">40 KB</div>
+              <div style="position: absolute; top: 0; left: 50%; bottom: 20px; width: 1px; background: black"></div>
+              <div style="position: absolute; left: 50%; bottom: 0; transform: translateX(-50%); font-size: 12px">80 KB</div>
+              <div class="line" style="position: absolute; top: 0; left: 75%; bottom: 20px; width: 1px; background: black"></div>
+              <div class="line" style="position: absolute; left: 75%; bottom: 0; transform: translateX(-50%); font-size: 12px">120 KB</div>
+              <div style="position: absolute; top: 0; left: 100%; bottom: 20px; width: 1px; background: black"></div>
+              <div style="position: absolute; left: 100%; bottom: 0; transform: translateX(-50%); font-size: 12px">160 KB</div>
+              
+              <div style="position: absolute; left: 1px; top: 0; right: 5px; --cssnano: 155.89; --esbuild: 156.57; --lightningcss: 139.74">
+                <div style="position: absolute; top: 5px; left: 0; width: calc(1% * var(--cssnano) / var(--esbuild) * 100); height: 20px; background: rgb(186, 25, 33)"></div>
+                <div style="position: absolute; right: calc(100% + 5px - 1% * var(--cssnano) / var(--esbuild) * 100); top: 5px; line-height: 20px; font-size: 12px; color: white">155.89 KB</div>
+                <div style="position: absolute; top: 35px; left: 0; width: 100%; height: 20px; background: rgb(186, 25, 33)"></div>
+                <div style="position: absolute; right: 5px; top: 35px; line-height: 20px; font-size: 12px; color: white">156.57 KB</div>
+                <div style="position: absolute; top: 65px; left: 0; width: calc(1% * var(--lightningcss) / var(--esbuild) * 100); height: 20px; background: rgb(186, 25, 33)"></div>
+                <div style="position: absolute; right: calc(100% + 5px - 1% * var(--lightningcss) / var(--esbuild) * 100); top: 65px; line-height: 20px; font-size: 12px; color: white">139.74 KB</div>
+              </div>
+
+              <div style="position: absolute; right: calc(100% + 10px); top: 5px; line-height: 20px; font-size: 13px">CSSNano</div>
+              <div style="position: absolute; right: calc(100% + 10px); top: 35px; line-height: 20px; font-size: 13px">ESBuild</div>
+              <div style="position: absolute; left: -10px; transform: translateX(-100%); top: 65px; line-height: 20px; font-size: 13px">Lightning CSS</div> 
+            </div>
+          </div>
+          <figcaption>Output size after minifying Bootstrap 4 (~10,000 lines). See the <a href="https://github.com/parcel-bundler/lightningcss#benchmarks" target="_blank">readme</a> for more benchmarks.</figcaption>
+        </figure>
+      </section>
+      <section class="modules">
+        <div class="compartment main">
+          <h3>CSS modules</h3>
+          <p><strong>Lightning CSS supports CSS modules, which locally scope classes, ids, <code>@keyframes</code>, CSS variables, and more.</strong> This ensures that there are no unintended name clashes between different CSS files.</p>
+          <p style="font-size: 0.8em">Lightning CSS generates a mapping of the original names to scoped names, which can be used from your JavaScript. This also enables unused classes and variables to be tree-shaken.</p>
+          <p><a href="css-modules.html">Documentation →</a></p>
+        </div>
+        <pre class="compartment input"><code>.heading {
+  composes: typography from './typography.css';
+  color: gray;
+}</code></pre>
+        <pre class="compartment output"><code>.EgL3uq_heading {
+  color: gray;
+}</code></pre>
+        <pre class="compartment json"><code>{
+  "heading": {
+    "name": "EgL3uq_heading",
+    "composes": [{
+      "type": "dependency",
+      "name": "typography",
+      "specifier": "./typography.css"
+    }]
+  }
+}</code></pre>
+      </section>
+      <section class="parser">
+        <div>
+          <h3>Browser grade</h3>
+          <p><strong>Lightning CSS is written in Rust, using the <a href="https://github.com/servo/rust-cssparser" target="_blank">cssparser</a> and <a href="https://github.com/servo/stylo/tree/main/selectors" target="_blank">selectors</a> crates created by Mozilla and used by Firefox.</strong> These provide a solid CSS-parsing foundation on top of which Lightning CSS implements support for all specific CSS rules and properties.</p>
+          <p style="font-size: 0.85em">Lightning CSS fully parses every CSS rule, property, and value just as a browser would. This reduces duplicate work for transformers, leading to improved performance and minification.</p>
+          <p><a href="transforms.html">Custom transforms →</a></p>
+        </div>
+        <pre><code><span class="ident">Background</span>([<span class="ident">Background</span> {
+  image: <span class="ident">Url</span>(<span class="ident">Url</span> { url: <span class="string">"img.png"</span> }),
+  color: <span class="ident">CssColor</span>(<span class="ident">RGBA</span>(<span class="ident">RGBA</span> { red: <span class="number">0</span>, green: <span class="number">0</span>, blue: <span class="number">0</span>, alpha: <span class="number">0</span> })),
+  position: <span class="ident">Position</span> {
+    x: <span class="ident">Length</span>(<span class="ident">Dimension</span>(<span class="ident">Px</span>(<span class="number">20.0</span>))),
+    y: <span class="ident">Length</span>(<span class="ident">Dimension</span>(<span class="ident">Px</span>(<span class="number">10.0</span>))),
+  },
+  repeat: <span class="ident">BackgroundRepeat</span> {
+    x: <span class="ident">Repeat</span>,
+    y: <span class="ident">Repeat</span>,
+  },
+  size: <span class="ident">Explicit</span> {
+    width: <span class="ident">LengthPercentage</span>(<span class="ident">Dimension</span>(<span class="ident">Px</span>(<span class="number">50.0</span>))),
+    height: <span class="ident">LengthPercentage</span>(<span class="ident">Dimension</span>(<span class="ident">Px</span>(<span class="number">100.0</span>))),
+  },
+  attachment: <span class="ident">Scroll</span>,
+  origin: <span class="ident">PaddingBox</span>,
+  clip: <span class="ident">BorderBox</span>,
+}])</code></pre>
+      </section>
+    </main>
+    <footer>
+      Copyright © 2024 Devon Govett and Parcel Contributors.
+    </footer>
+    <style>
+      html {
+        color-scheme: dark;
+        background: #111;
+      }
+
+      body {
+        font-family: system-ui;
+        --gold: lch(80% 82.34 80.104);
+        --gold-text: lch(85% 82.34 80.104);
+        --gold-shadow: lch(80% 82.34 80.104 / .7);
+      }
+
+      @font-face {
+        font-family:"din-1451-lt-pro-engschrift";
+        src:url("https://use.typekit.net/af/7fa6e1/00000000000000007735bbcd/30/l?primer=388f68b35a7cbf1ee3543172445c23e26935269fadd3b392a13ac7b2903677eb&fvd=n4&v=3") format("woff2"),url("https://use.typekit.net/af/7fa6e1/00000000000000007735bbcd/30/d?primer=388f68b35a7cbf1ee3543172445c23e26935269fadd3b392a13ac7b2903677eb&fvd=n4&v=3") format("woff"),url("https://use.typekit.net/af/7fa6e1/00000000000000007735bbcd/30/a?primer=388f68b35a7cbf1ee3543172445c23e26935269fadd3b392a13ac7b2903677eb&fvd=n4&v=3") format("opentype");
+        font-display:auto;font-style:normal;font-weight:400;font-stretch:normal;
+      }
+
+      @font-face {
+        font-family:"urbane-rounded";
+        src:url("https://use.typekit.net/af/916187/00000000000000007735bfa0/30/l?primer=81a69539b194230396845be9681d114557adfb35f4cccc679c164afb4aa47365&fvd=n6&v=3") format("woff2"),url("https://use.typekit.net/af/916187/00000000000000007735bfa0/30/d?primer=81a69539b194230396845be9681d114557adfb35f4cccc679c164afb4aa47365&fvd=n6&v=3") format("woff"),url("https://use.typekit.net/af/916187/00000000000000007735bfa0/30/a?primer=81a69539b194230396845be9681d114557adfb35f4cccc679c164afb4aa47365&fvd=n6&v=3") format("opentype");
+        font-display:auto;font-style:normal;font-weight:600;font-stretch:normal;
+      }
+
+      header {
+        max-width: 1200px;
+        margin: 0 auto;
+        padding: 100px 0;
+        font-size: 16px;
+        background: radial-gradient(closest-side, lch(80% 82.34 80.104 / .3), transparent);
+        display: grid;
+        column-gap: 50px;
+        grid-template-areas: "logo header"
+                             "logo subheader"
+                             ". links";
+      }
+
+      header svg {
+        filter: drop-shadow(0 0 5px var(--gold-shadow)) drop-shadow(0 0 20px var(--gold-shadow));
+        grid-area: logo;
+        place-self: center end;
+        width: 100px;
+      }
+
+      header svg .outer {
+        stroke-width: 30px;
+        stroke: var(--gold);
+      }
+
+      header svg .inner {
+        fill: lch(100% 82.34 80.104);
+      }
+
+      header h1 {
+        font-family: urbane-rounded;
+        font-size: 100px;
+        -webkit-text-stroke: 3px var(--gold-text);
+        color: transparent;
+        filter: drop-shadow(0 0 3px var(--gold-shadow)) drop-shadow(0 0 15px var(--gold));
+        padding: 20px 0;
+        margin: 0;
+        letter-spacing: -0.02em;
+      }
+
+      header h1::selection { 
+        -webkit-text-stroke-color: #fffddd;
+        background-color: var(--gold-text);
+      }
+
+      header h2 {
+        font-family: urbane-rounded;
+        color: lch(65% 85 35);
+        text-shadow: 0 0 20px lch(65% 85 35);
+        margin: 0;
+        letter-spacing: -0.02em;
+      }
+
+      header p {
+        grid-area: links;
+      }
+
+      header a {
+        font-family: urbane-rounded;
+        font-size: 1.05em;
+        color: lch(90% 50.34 80.104);
+        filter: drop-shadow(0 0 8px lch(90% 50.34 80.104 / .7));
+        text-decoration-color: lch(90% 50.34 80.104 / 0);
+        text-decoration-style: wavy;
+        text-decoration-thickness: 2px;
+        text-underline-offset: 2px;
+        text-decoration-skip-ink: none;
+        transition: text-decoration-color 150ms;
+      }
+
+      header a:hover {
+        text-decoration-color: lch(90% 50.34 80.104);
+      }
+
+      @media (width < 950px) {
+        header {
+          column-gap: 30px;
+        }
+
+        header h1 {
+          font-size: 80px;
+        }
+
+        header h2 {
+          font-size: 20px;
+        }
+
+        header svg {
+          width: 80px;
+        }
+      }
+
+      @media (width < 800px) {
+        header {
+          column-gap: 15px;
+          padding: 30px 0;
+        }
+
+        header h1 {
+          font-size: 60px;
+        }
+
+        header h2 {
+          font-size: 16px;
+        }
+
+        header a {
+          font-size: 14px;
+        }
+
+        header svg {
+          width: 60px;
+        }
+      }
+
+      @media (width < 500px) {
+        header {
+          grid-template-areas: "logo"
+                               "header"
+                               "subheader"
+                               "links";
+          place-items: center;
+          text-align: center;
+          gap: 8px;
+        }
+        header h1 {
+          font-size: 38px;
+          -webkit-text-stroke-width: 1.5px;
+          padding: 0;
+        }
+
+        header h2 {
+          font-size: 14px;
+        }
+
+        header a {
+          font-size: 13px;
+        }
+
+        header svg {
+          place-self: center;
+        }
+      }
+
+      main {
+        max-width: 1400px;
+        margin: 0 auto;
+      }
+
+      main section {
+        --padding: 60px;
+        padding: var(--padding);
+        margin: 60px;
+        font-size: 22px;
+        --radius: 50px;
+        border-radius: var(--radius);
+      }
+
+      main section h3 {
+        margin-top: 0;
+        font-size: 1.8em;
+      }
+
+      main section p {
+        line-height: 1.4em;
+      }
+
+      main p a {
+        font-weight: bold;
+        font-size: 0.9em;
+        color: inherit;
+        text-decoration: none;
+      }
+
+      main p a:hover {
+        text-decoration: underline;
+      }
+
+      figure {
+        max-width: 800px;
+        margin: 0;
+      }
+
+      figcaption {
+        font-size: 10px;
+        text-align: center;
+        margin-top: 20px;
+      }
+
+      figcaption a {
+        color: inherit;
+      }
+
+      @media (width < 500px) {
+        main section {
+          margin: 20px 10px;
+          --padding: 20px;
+          --radius: 20px;
+          font-size: 16px;
+        }
+      }
+
+      .warp {
+        background: black;
+        color: white;
+        background-image: radial-gradient(rgb(10 3 34 / .2), rgb(10 3 34 / .7)), url(lightspeed.svg);
+        background-size: cover;
+        background-position: center center;
+        box-shadow: inset 0 0 0 1px rgb(255 255 255 / .15);
+      }
+
+      .warp h3 {
+        color: var(--gold-text);
+        text-shadow: 0 0 15px var(--gold-shadow);
+      }
+
+      .warp p {
+        max-width: 900px;
+      }
+
+      .warp figcaption {
+        color: #aaa;
+      }
+
+      .warp p a {
+        color: var(--gold-text);
+        text-shadow: 0 0 7px var(--gold-shadow);
+      }
+
+      .crush {
+        background-image: url(crush.svg), url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' viewBox='0 0 4 4'%3E%3Cpath fill='rgb(0 0 0 / .2)' d='M1 3h1v1H1V3zm2-2h1v1H3V1z'%3E%3C/path%3E%3C/svg%3E"), linear-gradient(to bottom right, lch(75% 82.34 80.104), lch(68% 82.34 80.104));
+        background-size: 600px auto, auto, auto;
+        background-repeat: no-repeat, repeat;
+        background-position: bottom right;
+        color: black;
+        padding-right: 550px;
+      }
+
+      .crush h3 {
+        font-size: 3em;
+        margin-bottom: 0;
+        color: lch(42.758% 73.588 34.159);
+        -webkit-text-stroke: 1px black;
+        text-shadow: 3px 3px 0 black;
+        text-transform: uppercase;
+        width: fit-content;
+        word-spacing: .1em;
+      }
+
+      .crush figcaption {
+        color: #222;
+      }
+
+      .crush p a {
+        color: lch(30% 73.588 34.159);
+      }
+
+      @media (width < 1200px) {
+        .crush {
+          padding-right: var(--padding);
+          padding-bottom: 500px;
+          background-position: bottom right;
+          background-size: auto 500px, auto, auto;
+        }
+      }
+
+      @media (width < 500px) {
+        .crush {
+          padding-bottom: 300px;
+          background-size: auto 300px, auto, auto;
+        }
+      }
+
+      .future {
+        background: #215178;
+        color: white;
+        background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100' viewBox='0 0 100 100'%3E%3Cg fill-rule='evenodd'%3E%3Cg fill='rgba(255,255,255,0.05)' fill-opacity='1'%3E%3Cpath opacity='.5' d='M96 95h4v1h-4v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h4v1h-4v9h4v1h-4v9h4v1h-4v9h4v1h-4v9h4v1h-4v9h4v1h-4v9h4v1h-4v9h4v1h-4v9zm-1 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-9-10h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm9-10v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-9-10h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm9-10v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-9-10h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm9-10v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-9-10h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9z'/%3E%3Cpath d='M6 5V0H5v5H0v1h5v94h1V6h94V5H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E"), radial-gradient(#1F4D97, #030B16);
+        box-shadow: inset 0 0 0 1px rgb(255 255 255 / .1);
+        position: relative;
+        --color: lch(85% 58 205);
+      }
+
+      .future .title {
+        font-family: din-1451-lt-pro-engschrift, sans-serif;
+        text-transform: uppercase;
+        position: absolute;
+        --height: 1.2em;
+        top: calc(var(--height) * -.5);
+        left: calc(var(--height) * 1.5);
+        margin-left: -5px;
+        background: var(--color);
+        filter: drop-shadow(0 0 8px var( --color));
+        width: fit-content;
+        color: #030B16;
+        margin-top: 0;
+      }
+
+      .future h3.title {
+        font-size: 1.6em;
+        line-height: var(--height);
+      }
+
+      .future h4.title {
+        font-size: 16px;
+        line-height: var(--height);
+      }
+
+      .future .title:before {
+        content: '';
+        display: inline-block;
+        position: absolute;
+        right: 100%;
+        width: 0;
+        height: 0;
+        border-top: var(--height) solid var( --color);
+        border-left: var(--height) solid transparent;
+      }
+
+      .future .title:after {
+        content: '';
+        display: inline-block;
+        position: absolute;
+        left: 100%;
+        width: 0;
+        height: 0;
+        border-bottom: var(--height) solid var( --color);
+        border-right: var(--height) solid transparent;
+      }
+
+      .future .inner {
+        border: 2px solid var(--color);
+        box-shadow: 0 0 15px var( --color), inset 0 0 15px var( --color);
+        border-radius: 20px;
+        padding: 20px;
+        display: flex;
+        flex-direction: row;
+        position: relative;
+      }
+
+      .future .description {
+        margin: 40px;
+      }
+
+      .future .description p:first-child {
+        margin-top: 0;
+      }
+
+      .future .description p:last-child {
+        margin-bottom: 0;
+      }
+
+      .future .example {
+        display: flex;
+        flex-direction: column;
+        gap: 30px;
+        flex-shrink: 0;
+      }
+
+      .future .box {
+        --color: lch(64% 103 0);
+        border: 2px solid var(--color);
+        background: lch(64% 103 0 / .15);
+        border-radius: 10px;
+        box-shadow: 0 0 20px var(--color), inset 0 0 10px var(--color);
+        padding: 15px;
+        text-shadow: 0 0 8px var(--color);
+        position: relative;
+        width: fit-content;
+        margin-left: auto;
+        max-width: 100%;
+        box-sizing: border-box;
+      }
+
+      .future .box code {
+        font-size: 14px;
+        display: block;
+        overflow: visible auto;
+      }
+
+      .future .box .title {
+        color: white
+      }
+
+      .future .box .property {
+        color: white;
+      }
+
+      .future .box .keyword {
+        color: var(--color);
+      }
+
+      .future .box .number {
+        color: lch(71% 103 52);
+      }
+
+      .future .box .class {
+        color: lch(87% 107 89);
+      }
+
+      .future .targets h4 {
+        font-family: din-1451-lt-pro-engschrift, sans-serif;
+        text-transform: uppercase;
+        display: inline;
+        vertical-align: middle;
+        margin: 0;
+        margin-right: 10px;
+        color: lch(85% 58 205 / .7);
+      }
+
+      .future .targets {
+        border: 2px solid lch(85% 58 205 / .5);
+        box-shadow: 0 0 10px lch(85% 58 205 / .5);
+        border-radius: 6px;
+        padding: 8px;
+        width: fit-content;
+        margin-left: auto;
+        font-size: 16px;
+      }
+
+      .future .targets code {
+        font-size: 15px;
+      }
+
+      .future a {
+        color: lch(85% 58 205);
+        text-shadow: 0 0 10px lch(85% 58 205 / .6);
+      }
+
+      @media (width < 1200px) {
+        .future .inner {
+          flex-direction: column;
+        }
+
+        .future .box, .future .targets {
+          margin-left: 0
+        }
+      }
+
+      @media (width < 500px) {
+        .future .description {
+          margin: 20px 4px;
+        }
+
+        .future .box {
+          padding: 10px;
+        }
+        
+        .future .targets h4 {
+          display: block;
+        }
+      }
+
+      .parser {
+        background-image: linear-gradient(rgb(0 0 0 / .1), rgb(0 0 0 / .1)), image-set("metal.jpeg?as=webp&width=1200" 1x, "metal.jpeg?as=webp&width=2400" 2x);
+        background-size: cover;
+        color: black;
+        text-shadow: inset 0 2px 5px black;
+        position: relative;
+        --inset: 35px;
+        padding: calc(var(--padding) + var(--inset) * .6);
+        display: flex;
+        column-gap: 40px;
+        row-gap: 20px;
+        box-shadow: inset 0 0 0 1px rgb(255 255 255 / .4);
+      }
+
+      .parser > * {
+        z-index: 2;
+      }
+
+      .parser:before {
+        content: '';
+        position: absolute;
+        inset: calc(var(--inset) / 2);
+        border-radius: calc(var(--radius) * .8);
+        box-shadow: inset 2px 3px 10px rgb(0 0 0 / .7), 1px 1px 0 rgb(255 255 255 / .4);
+        background: rgb(0 0 0 / .05);
+        pointer-events: none;
+        z-index: 0;
+      }
+
+      .parser:after {
+        content: '';
+        position: absolute;
+        inset: var(--inset);
+        border-radius: calc(var(--radius) * .4);
+        box-shadow: inset 1px 1px 0 rgb(255 255 255 / .4), 2px 3px 10px rgb(0 0 0 / .7);
+        background: rgb(255 255 255 / .25);
+        pointer-events: none;
+        z-index: 1;
+      }
+
+      .parser h3 {
+        font-family: Impact, Haettenschweiler, 'Arial Narrow Bold', sans-serif;
+        letter-spacing: 2px;
+        text-transform: uppercase;
+        text-shadow: 3px 3px 2px rgba(255,255,255,0.35);
+        background-color: rgba(0 0 0 / .65);
+        color: transparent;
+        -webkit-background-clip: text;
+        background-clip: text;
+      }
+
+      .parser p {
+        text-shadow: 0 1px 0 white;
+      }
+
+      .parser p:last-child {
+        margin-bottom: 0;
+      }
+
+      .parser pre {
+        padding: 20px;
+        width: fit-content;
+        margin: 0;
+        margin-right: -40px;
+        font-size: .6em;
+        display: flex;
+        align-items: end;
+        text-shadow: 0 1px 0 rgb(255 255 255 / .6);
+        font-weight: bold;
+        overflow: auto;
+        max-width: 100%;
+        flex-shrink: 0;
+        margin-top: auto;
+      }
+
+      .parser pre .ident {
+        color: lch(30% 80 300);
+      }
+
+      .parser pre .string {
+        color: lch(40% 108 144);
+      }
+
+      .parser pre .number {
+        color: lch(35% 117 348);
+      }
+
+      .parser a {
+        color: black;
+      }
+
+      .parser p:last-of-type a {
+        font-size: 0.8em;
+      }
+
+      @media (width < 1200px) {
+        .parser {
+          flex-direction: column;
+        }
+
+        .parser pre {
+          margin: 0;
+          padding: 0;
+        }
+      }
+
+      @media (width < 500px) {
+        .parser {
+          --inset: 25px;
+        }
+
+        .parser p, .parser pre {
+          text-shadow: 0 1px 0 rgb(255 255 255 / .6);
+        }
+      }
+
+      .modules {
+        background: black;
+        box-shadow: 0 0 0 1px rgb(255 255 255 / .2), 2px 2px 5px rgb(255 255 255 / .2);
+        padding: 0;
+        display: grid;
+        grid-template-areas: "main input  input"
+                             "main output json";
+      }
+
+      .modules .compartment {
+        padding: max(20px, calc(var(--padding) / 2));
+        border: 20px solid black;
+        border-radius: 40px;
+        --shadow-size: 20px;
+        --inner-shadow-size: 2px;
+        box-shadow: inset 0 0 0 1px rgb(255 255 255 / .15), inset -1px -1px var(--inner-shadow-size) rgb(255 255 255 / .3), inset 4px 8px var(--shadow-size) rgb(0 0 0 / .9);
+        margin: 0;
+        overflow: auto;
+        background: #ba1921;
+        background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='32' viewBox='0 0 16 32'%3E%3Cg fill='%23fff' fill-opacity='0.05'%3E%3Cpath fill-rule='evenodd' d='M0 24h4v2H0v-2zm0 4h6v2H0v-2zm0-8h2v2H0v-2zM0 0h4v2H0V0zm0 4h2v2H0V4zm16 20h-6v2h6v-2zm0 4H8v2h8v-2zm0-8h-4v2h4v-2zm0-20h-6v2h6V0zm0 4h-4v2h4V4zm-2 12h2v2h-2v-2zm0-8h2v2h-2V8zM2 8h10v2H2V8zm0 8h10v2H2v-2zm-2-4h14v2H0v-2zm4-8h6v2H4V4zm0 16h6v2H4v-2zM6 0h2v2H6V0zm0 24h2v2H6v-2z'/%3E%3C/g%3E%3C/svg%3E");
+      }
+
+      .modules .compartment.main {
+        grid-area: main;
+        border-top-left-radius: 50px;
+        border-bottom-left-radius: 50px;
+        padding: 60px 60px 50px 60px;
+        margin-right: -20px;
+        --shadow-size: 40px;
+        --inner-shadow-size: 4px;
+      }
+
+      .modules .compartment.main p:last-child {
+        margin-bottom: 0;
+      }
+
+      .modules .compartment:not(.main) {
+        display: flex;
+        align-items: center;
+        justify-content: center;
+      }
+
+      .modules h3 {
+        text-transform: uppercase;
+        font-size: 1.45em;
+        border-top: 4px solid white;
+        border-bottom: 4px solid white;
+        line-height: 1.5em;
+        width: fit-content;
+      }
+
+      .modules p {
+        font-family: Georgia, 'Times New Roman', Times, serif;
+        font-size: 0.87em;
+      }
+      
+      .modules pre {
+        font-size: 14px;
+      }
+
+      .modules .compartment.input {
+        grid-area: input;
+        border-top-right-radius: 50px;
+      }
+
+      .modules .compartment.output {
+        grid-area: output;
+        margin-top: -20px;
+      }
+
+      .modules .compartment.json {
+        grid-area: json;
+        margin-top: -20px;
+        margin-left: -20px;
+        border-bottom-right-radius: 50px;
+      }
+
+      @media (width < 1200px) {
+        .modules {
+          grid-template-areas: "main main"
+                               "input input"
+                               "output json";
+        }
+
+        .modules .compartment.main {
+          border-top-right-radius: 50px;
+          margin-right: 0;
+        }
+
+        .modules .compartment.input {
+          margin-top: -20px;
+        }
+
+        .modules .compartment.output {
+          border-bottom-left-radius: 50px;
+        }
+
+        .modules .compartment:not(.main) {
+          justify-content: start;
+        }
+      }
+
+      @media (width < 500px) {
+        .modules {
+          grid-template-areas: "main" "input" "output" "json";
+        }
+
+        .modules .compartment {
+          border-radius: calc(var(--radius) * 1.5) !important;
+          --shadow-size: 15px;
+        }
+
+        .modules .compartment.main {
+          padding: 30px;
+          --shadow-size: 25px;
+        }
+
+        .modules .compartment.json {
+          margin: 0;
+          margin-top: -20px;
+        }
+
+        .modules pre {
+          font-size: 12px;
+        }
+      }
+
+      footer {
+        font-size: 12px;
+        color: #666;
+        text-align: center;
+        padding-bottom: 20px;
+      }
+
+      @media (width < 600px) {
+        .chart .line {
+          display: none;
+        }
+      }
+    </style>
+  </body>
+</html>
diff --git a/website/lightspeed.svg b/website/lightspeed.svg
new file mode 100644
index 0000000..a4db4d1
--- /dev/null
+++ b/website/lightspeed.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 5000 3000"><defs><linearGradient id="a" x1="3652.8" x2="4102.26" y1="1499.88" y2="1499.88" gradientTransform="rotate(-146.96 3192.49 2255.31) scale(1.67)" gradientUnits="userSpaceOnUse"><stop offset="0"/><stop offset=".27" stop-color="#26004f"/><stop offset=".51" stop-color="#365fdb"/><stop offset=".72" stop-color="#63cbff"/><stop offset="1" stop-color="#defcff"/></linearGradient><linearGradient xlink:href="#a" id="c" x1="6726.43" x2="7579.02" y1="1498.93" y2="1498.93" gradientTransform="scale(.47 -.47) rotate(8.24 36069.1 23356.47)"/><linearGradient xlink:href="#a" id="d" x1="5176.37" x2="5592.71" y1="1499.06" y2="1499.06" gradientTransform="scale(-.53 .53) rotate(45.6 -3003.1 -5647.05)"/><linearGradient xlink:href="#a" id="e" x1="4110.36" x2="4477.25" y1="1500.13" y2="1500.13" gradientTransform="rotate(-30.59 2572.97 1241.34) scale(.95)"/><linearGradient xlink:href="#a" id="f" x1="3291.53" x2="3589.22" y1="1499.5" y2="1499.5" gradientTransform="scale(-.85 .85) rotate(-53.97 -285.75 6316.27)"/><linearGradient xlink:href="#a" id="g" x1="3965.48" x2="4393.27" y1="1499.46" y2="1499.46" gradientTransform="rotate(-60.56 2503.44 1225.04) scale(.9)"/><linearGradient xlink:href="#a" id="h" x1="4496.79" x2="5050.85" y1="1500.65" y2="1500.65" gradientTransform="scale(.46 -.46) rotate(9.48 32354.96 20769.25)"/><linearGradient xlink:href="#a" id="i" x1="5025.01" x2="5147.22" y1="1500.94" y2="1500.94" gradientTransform="rotate(167.36 1972.86 1258.49) scale(.6)"/><linearGradient xlink:href="#a" id="j" x1="3279.53" x2="3340.07" y1="1499.91" y2="1499.91" gradientTransform="rotate(-75.87 2400.73 688.22) scale(.66)"/><linearGradient xlink:href="#a" id="k" x1="4279.1" x2="4926.17" y1="1499.33" y2="1499.33" gradientTransform="scale(.58 -.58) rotate(.95 249846.09 147769.7)"/><linearGradient xlink:href="#a" id="l" x1="7270.96" x2="8506.07" y1="1498.74" y2="1498.74" gradientTransform="rotate(-110.61 1975.12 339.03) scale(.28)"/><linearGradient xlink:href="#a" id="m" x1="4386.93" x2="5288.82" y1="1500.56" y2="1500.56" gradientTransform="scale(-.66 .66) rotate(-36.31 212.58 10456.5)"/><linearGradient xlink:href="#a" id="n" x1="3942.9" x2="4917.9" y1="1500.26" y2="1500.26" gradientTransform="scale(-.69 .69) rotate(-83.92 -514.6 4885.25)"/><linearGradient xlink:href="#a" id="o" x1="4566.68" x2="5275.73" y1="1499.45" y2="1499.45" gradientTransform="rotate(113.27 1353.64 1548.14) scale(.34)"/><linearGradient xlink:href="#a" id="p" x1="3738.1" x2="4360.86" y1="1499.35" y2="1499.35" gradientTransform="matrix(-.43 -.00068 -.00068 .43 3290.13 859.4)"/><linearGradient xlink:href="#a" id="q" x1="4574.48" x2="5072.49" y1="1499.11" y2="1499.11" gradientTransform="rotate(-74.97 2375.13 406.3) scale(.54)"/><linearGradient xlink:href="#a" id="r" x1="3379.25" x2="3564.92" y1="1499.34" y2="1499.34" gradientTransform="scale(-.6 .6) rotate(66.95 -1908.86 -2548.07)"/><linearGradient xlink:href="#a" id="s" x1="3903.02" x2="4296.46" y1="1500.33" y2="1500.33" gradientTransform="rotate(126.79 2266.1 1482.3) scale(.86)"/><linearGradient xlink:href="#a" id="t" x1="3459.61" x2="3551.6" y1="1499.21" y2="1499.21" gradientTransform="rotate(-67.46 2456.3 588.64) scale(.65)"/><linearGradient xlink:href="#a" id="u" x1="4729.18" x2="6313.9" y1="1499.51" y2="1499.51" gradientTransform="rotate(-162.11 2439.42 1449.14) scale(.95)"/><linearGradient xlink:href="#a" id="v" x1="3021.18" x2="3474.4" y1="1500.55" y2="1500.55" gradientTransform="scale(-.99 .99) rotate(-84.75 -332.95 3902.1)"/><linearGradient xlink:href="#a" id="w" x1="2911.36" x2="3092.55" y1="1499.79" y2="1499.79" gradientTransform="scale(.43 -.43) rotate(-71.9 388.95 -3717.54)"/><linearGradient xlink:href="#a" id="x" x1="4424.45" x2="5137.38" y1="1501.13" y2="1501.13" gradientTransform="rotate(126.15 1868.69 1455.65) scale(.61)"/><linearGradient xlink:href="#a" id="y" x1="5396.72" x2="6509.51" y1="1499.55" y2="1499.55" gradientTransform="scale(.37 -.37) rotate(-74.26 632.03 -4515.33)"/><linearGradient xlink:href="#a" id="z" x1="4046.91" x2="4086.61" y1="1499.89" y2="1499.89" gradientTransform="rotate(-115.75 2326.76 1158.7) scale(.78)"/><linearGradient xlink:href="#a" id="A" x1="4469.49" x2="5334.71" y1="1501.22" y2="1501.22" gradientTransform="scale(-.51 .51) rotate(62.81 -2691.39 -3309.11)"/><linearGradient xlink:href="#a" id="B" x1="4289.09" x2="4751.29" y1="1498.71" y2="1498.71" gradientTransform="scale(-.54 .54) rotate(67.95 -2326.74 -2669.11)"/><linearGradient xlink:href="#a" id="C" x1="5321.18" x2="6399" y1="1499.79" y2="1499.79" gradientTransform="rotate(13.12 -2677.26 8241.75) scale(.33)"/><linearGradient xlink:href="#a" id="D" x1="5258.15" x2="6135.52" y1="1500.64" y2="1500.64" gradientTransform="rotate(-38.03 2897.3 -377.2) scale(.57)"/><linearGradient xlink:href="#a" id="E" x1="4880.6" x2="5827.14" y1="1500.64" y2="1500.64" gradientTransform="scale(-.64 .64) rotate(-23.04 1017.46 16067.42)"/><linearGradient xlink:href="#a" id="F" x1="4580.05" x2="4948.07" y1="1500.14" y2="1500.14" gradientTransform="rotate(23.19 1671.93 2403.4) scale(.83)"/><linearGradient xlink:href="#a" id="G" x1="2732.54" x2="2933.79" y1="1500.65" y2="1500.65" gradientTransform="scale(-1.01 1.01) rotate(48.56 -287.32 -3299.28)"/><linearGradient xlink:href="#a" id="H" x1="6572.29" x2="7025.84" y1="1500.17" y2="1500.17" gradientTransform="rotate(169.88 1578.02 1052.21) scale(.3)"/><linearGradient xlink:href="#a" id="I" x1="4579.63" x2="4773.56" y1="1500.48" y2="1500.48" gradientTransform="scale(.71 -.71) rotate(-8.96 -20383.75 -10995.89)"/><linearGradient xlink:href="#a" id="J" x1="4568.54" x2="4816.18" y1="1501.19" y2="1501.19" gradientTransform="rotate(146.79 2220.8 1428.9) scale(.81)"/><linearGradient xlink:href="#a" id="K" x1="3292.08" x2="3431.88" y1="1499.53" y2="1499.53" gradientTransform="rotate(-102.35 2424.66 1294.27) scale(.88)"/><linearGradient xlink:href="#a" id="L" x1="4059.55" x2="5017.32" y1="1498.97" y2="1498.97" gradientTransform="scale(.77 -.77) rotate(39 7422.75 1750.1)"/><linearGradient xlink:href="#a" id="M" x1="5813.27" x2="6124.92" y1="1500.9" y2="1500.9" gradientTransform="rotate(-153.81 1907.04 926.52) scale(.45)"/><linearGradient xlink:href="#a" id="N" x1="4046.28" x2="4458.07" y1="1499.69" y2="1499.69" gradientTransform="scale(-.61 .61) rotate(73.8 -1774.64 -1975.43)"/><linearGradient xlink:href="#a" id="O" x1="3974.34" x2="4492.81" y1="1498.89" y2="1498.89" gradientTransform="scale(.75 -.75) rotate(-61.42 -358.65 -1493.84)"/><linearGradient xlink:href="#a" id="P" x1="4384.29" x2="4740.8" y1="1500.05" y2="1500.05" gradientTransform="rotate(49.16 2580.66 1444.49) scale(1.03)"/><linearGradient xlink:href="#a" id="Q" x1="4164.33" x2="4485.64" y1="1501.03" y2="1501.03" gradientTransform="scale(-.51 .51) rotate(69.42 -2562.37 -2650.7)"/><linearGradient xlink:href="#a" id="R" x1="4097.75" x2="4713.1" y1="1500.68" y2="1500.68" gradientTransform="scale(.7 -.7) rotate(-66 -97.15 -1655.98)"/><linearGradient xlink:href="#a" id="S" x1="3499.63" x2="3788" y1="1499.79" y2="1499.79" gradientTransform="scale(.88 -.88) rotate(-73.78 207.86 -771.51)"/><linearGradient xlink:href="#a" id="T" x1="6081.46" x2="6710.17" y1="1500.25" y2="1500.25" gradientTransform="scale(-.45 .45) rotate(22.58 -6446.5 -16113.27)"/><linearGradient xlink:href="#a" id="U" x1="4478.15" x2="4977.52" y1="1500.43" y2="1500.43" gradientTransform="scale(-.62 .62) rotate(11.71 -5549.4 -26714.16)"/><linearGradient xlink:href="#a" id="V" x1="4423.29" x2="4610.68" y1="1500.5" y2="1500.5" gradientTransform="rotate(-11.9 4158.76 -2060.07) scale(.72)"/><linearGradient xlink:href="#a" id="W" x1="3607.66" x2="3939.02" y1="1500.42" y2="1500.42" gradientTransform="scale(-.78 .78) rotate(-70.29 -381.52 5310.23)"/><linearGradient xlink:href="#a" id="X" x1="4828.72" x2="5023" y1="1499.95" y2="1499.95" gradientTransform="rotate(.45 -109195.97 185968.46) scale(.42)"/><linearGradient xlink:href="#a" id="Y" x1="4046.49" x2="4350.05" y1="1499.71" y2="1499.71" gradientTransform="scale(-.69 .69) rotate(-6.89 4780.7 47148.04)"/><linearGradient xlink:href="#a" id="Z" x1="4788.15" x2="5306.19" y1="1501.06" y2="1501.06" gradientTransform="scale(.53 -.53) rotate(-14.48 -13727.36 -12032.29)"/><linearGradient xlink:href="#a" id="aa" x1="4114.59" x2="4523.39" y1="1499.92" y2="1499.92" gradientTransform="rotate(-119.25 2322.4 1174.61) scale(.78)"/><linearGradient xlink:href="#a" id="ab" x1="4614.49" x2="5071.22" y1="1500.46" y2="1500.46" gradientTransform="scale(.8 -.8) rotate(-21.01 -6600.96 -3668.68)"/><linearGradient xlink:href="#a" id="ac" x1="3174.23" x2="3340.08" y1="1500.24" y2="1500.24" gradientTransform="scale(-1.05 1.05) rotate(71.34 -217.76 -1478.91)"/><linearGradient xlink:href="#a" id="ad" x1="3434.9" x2="3619.3" y1="1500.79" y2="1500.79" gradientTransform="scale(-.61 .61) rotate(-27.72 829.83 13990.68)"/><linearGradient xlink:href="#a" id="ae" x1="3580.42" x2="4057.87" y1="1499.01" y2="1499.01" gradientTransform="rotate(123.35 2269.65 1489.22) scale(.86)"/><linearGradient xlink:href="#a" id="af" x1="2630.8" x2="2734.18" y1="1499.44" y2="1499.44" gradientTransform="scale(-.58 .58) rotate(-18.81 2036.87 20638.71)"/><linearGradient xlink:href="#a" id="ag" x1="3569.86" x2="4647.4" y1="1500.51" y2="1500.51" gradientTransform="rotate(144.7 2111.83 1408.32) scale(.74)"/><linearGradient xlink:href="#a" id="ah" x1="4191.7" x2="4795.1" y1="1500.56" y2="1500.56" gradientTransform="scale(-.7 .7) rotate(-38.19 64.36 9640.85)"/><linearGradient xlink:href="#a" id="ai" x1="5575.21" x2="5824.34" y1="1499.47" y2="1499.47" gradientTransform="scale(-.67 .67) rotate(14.94 -3741.78 -19404.89)"/><linearGradient xlink:href="#a" id="aj" x1="4710.63" x2="5226.11" y1="1499.69" y2="1499.69" gradientTransform="rotate(-154.69 2454.07 1456.1) scale(.96)"/><linearGradient xlink:href="#a" id="ak" x1="5484.81" x2="6321.16" y1="1500.52" y2="1500.52" gradientTransform="scale(-.46 .46) rotate(-9.94 8297.35 44272.02)"/><linearGradient xlink:href="#a" id="al" x1="2989.62" x2="3041.66" y1="1500.31" y2="1500.31" gradientTransform="rotate(-44.71 2845.91 -784.36) scale(.4)"/><linearGradient xlink:href="#a" id="am" x1="4436.25" x2="5392.62" y1="1499.18" y2="1499.18" gradientTransform="rotate(38.72 2027.58 1890.41) scale(.86)"/><linearGradient xlink:href="#a" id="an" x1="3931.93" x2="4099.27" y1="1499.02" y2="1499.02" gradientTransform="scale(-.51 .51) rotate(89.58 -2239.56 -1179.32)"/><linearGradient xlink:href="#a" id="ao" x1="2641.98" x2="2814.28" y1="1500.56" y2="1500.56" gradientTransform="scale(-.55 .55) rotate(11.4 -7446.78 -29942.42)"/><linearGradient xlink:href="#a" id="ap" x1="4578.54" x2="5468.18" y1="1500.05" y2="1500.05" gradientTransform="rotate(160.7 2215 1388.8) scale(.79)"/><linearGradient xlink:href="#a" id="aq" x1="3533.89" x2="3846.75" y1="1498.92" y2="1498.92" gradientTransform="rotate(43.05 503.68 3031.85) scale(.37)"/><linearGradient xlink:href="#a" id="ar" x1="3691.11" x2="4044.07" y1="1500.65" y2="1500.65" gradientTransform="scale(-.71 .71) rotate(65.89 -1309.37 -2333.87)"/><linearGradient xlink:href="#a" id="as" x1="5975.67" x2="6212.79" y1="1499.99" y2="1499.99" gradientTransform="rotate(11.54 -813.12 5940.4) scale(.62)"/><linearGradient xlink:href="#a" id="at" x1="3843.23" x2="4282.14" y1="1500.02" y2="1500.02" gradientTransform="scale(.5 -.5) rotate(44.23 8976.58 3123.64)"/><linearGradient xlink:href="#a" id="au" x1="4936.21" x2="5203.26" y1="1500.39" y2="1500.39" gradientTransform="rotate(-117.93 2075.17 701.09) scale(.47)"/><linearGradient xlink:href="#a" id="av" x1="4130.22" x2="4318.74" y1="1499.01" y2="1499.01" gradientTransform="scale(-.61 .61) rotate(28.45 -2993.46 -9760.66)"/><linearGradient xlink:href="#a" id="aw" x1="3507.92" x2="4181.66" y1="1499.99" y2="1499.99" gradientTransform="scale(-.68 .68) rotate(84.19 -1301.8 -1202.75)"/><linearGradient xlink:href="#a" id="ax" x1="6163.95" x2="7000.87" y1="1500.37" y2="1500.37" gradientTransform="rotate(17.02 558.3 3860.73) scale(.69)"/><linearGradient xlink:href="#a" id="ay" x1="3368.31" x2="3646.91" y1="1499.32" y2="1499.32" gradientTransform="scale(-.61 .61) rotate(-60.98 -309.41 7006.42)"/><linearGradient xlink:href="#a" id="az" x1="4784.1" x2="6179.88" y1="1501.13" y2="1501.13" gradientTransform="scale(-.67 .67) rotate(-25.62 668.84 14151.3)"/><linearGradient xlink:href="#a" id="aA" x1="4411.79" x2="4798.7" y1="1500.31" y2="1500.31" gradientTransform="rotate(-8.91 6271.95 -6065.85) scale(.55)"/><linearGradient xlink:href="#a" id="aB" x1="4130.44" x2="4762.97" y1="1500.18" y2="1500.18" gradientTransform="scale(-.84 .84) rotate(-47.38 -236.97 7121.64)"/><linearGradient xlink:href="#a" id="aC" x1="5948.6" x2="6615.05" y1="1499.47" y2="1499.47" gradientTransform="rotate(.88 -42711.86 75532.98) scale(.54)"/><linearGradient xlink:href="#a" id="aD" x1="4517.16" x2="4794.62" y1="1499.34" y2="1499.34" gradientTransform="rotate(148.61 2157.76 1406.2) scale(.77)"/><linearGradient xlink:href="#a" id="aE" x1="5596" x2="5868.31" y1="1501.02" y2="1501.02" gradientTransform="scale(.52 -.52) rotate(49.54 8080.89 2518.45)"/><linearGradient xlink:href="#a" id="aF" x1="3348.28" x2="4025.45" y1="1499.12" y2="1499.12" gradientTransform="rotate(49.75 1255.96 2343.72) scale(.57)"/><linearGradient xlink:href="#a" id="aG" x1="2871.51" x2="3007.61" y1="1499.55" y2="1499.55" gradientTransform="scale(.97 -.97) rotate(21.47 10238.24 1916.82)"/><linearGradient xlink:href="#a" id="aH" x1="4153.9" x2="4449.49" y1="1499.4" y2="1499.4" gradientTransform="rotate(-24.34 3548.5 -1578.3) scale(.53)"/><linearGradient xlink:href="#a" id="aI" x1="2934.52" x2="3119.67" y1="1500.29" y2="1500.29" gradientTransform="scale(-1.1 1.1) rotate(6.88 971.64 -32859.74)"/><linearGradient xlink:href="#a" id="aJ" x1="5349.31" x2="5588.01" y1="1500.33" y2="1500.33" gradientTransform="rotate(2.72 -3863.58 11559.61) scale(.81)"/><linearGradient xlink:href="#a" id="aK" x1="3309.55" x2="3566" y1="1499.27" y2="1499.27" gradientTransform="rotate(28.68 701.6 3278.07) scale(.57)"/><linearGradient xlink:href="#a" id="aL" x1="4285.73" x2="4657.41" y1="1500.23" y2="1500.23" gradientTransform="rotate(-16.2 1572.71 3699.6) scale(1.23)"/><linearGradient xlink:href="#a" id="aM" x1="2621.48" x2="3038.4" y1="1500.66" y2="1500.66" gradientTransform="scale(-.57 .57) rotate(-32.93 650.26 12583.26)"/><linearGradient xlink:href="#a" id="aN" x1="4102.3" x2="4944.78" y1="1499.8" y2="1499.8" gradientTransform="scale(1 -1) rotate(-36.4 -2385.19 -1010.54)"/><linearGradient xlink:href="#a" id="aO" x1="4072.21" x2="4442" y1="1500.56" y2="1500.56" gradientTransform="scale(-.88 .88) rotate(-5.58 1541.66 49737.6)"/><linearGradient xlink:href="#a" id="aP" x1="3208.28" x2="3433.07" y1="1500.68" y2="1500.68" gradientTransform="scale(-.6 .6) rotate(.68 -84670.18 -505192.42)"/><linearGradient xlink:href="#a" id="aQ" x1="3839.67" x2="4258.44" y1="1500.1" y2="1500.1" gradientTransform="rotate(33.74 1668.47 2253.3) scale(.78)"/><linearGradient xlink:href="#a" id="aR" x1="4324.83" x2="6110.8" y1="1500.57" y2="1500.57" gradientTransform="rotate(42.94 1341.68 2391.18) scale(.63)"/><linearGradient xlink:href="#a" id="aS" x1="5726.79" x2="6462.44" y1="1500.09" y2="1500.09" gradientTransform="scale(-.31 .31) rotate(1.31 -149782.33 -429407.85)"/><linearGradient xlink:href="#a" id="aT" x1="4750.05" x2="4909.28" y1="1499.47" y2="1499.47" gradientTransform="rotate(139.7 2154.63 1433.9) scale(.77)"/><linearGradient xlink:href="#a" id="aU" x1="4121.21" x2="4564.02" y1="1500.88" y2="1500.88" gradientTransform="scale(-.71 .71) rotate(59.31 -1386.03 -2900.35)"/><linearGradient xlink:href="#a" id="aV" x1="5624.61" x2="6127.91" y1="1499.78" y2="1499.78" gradientTransform="rotate(31.96 577.46 3296.75) scale(.5)"/><linearGradient xlink:href="#a" id="aW" x1="4643.35" x2="4901.39" y1="1498.51" y2="1498.51" gradientTransform="scale(-.35 .35) rotate(22.04 -9827.98 -20160.28)"/><linearGradient xlink:href="#a" id="aX" x1="3741.02" x2="4745.17" y1="1499.89" y2="1499.89" gradientTransform="scale(.91 -.91) rotate(-28.95 -3811.65 -1806.22)"/><linearGradient xlink:href="#a" id="aY" x1="4562.1" x2="4859.57" y1="1500.44" y2="1500.44" gradientTransform="rotate(-126.25 2408.75 1355.31) scale(.9)"/><linearGradient xlink:href="#a" id="aZ" x1="4158.36" x2="4616.41" y1="1499.77" y2="1499.77" gradientTransform="rotate(-162.55 2296.02 1330.46) scale(.82)"/><linearGradient xlink:href="#a" id="ba" x1="3133.39" x2="3218.66" y1="1500.44" y2="1500.44" gradientTransform="scale(.87 -.87) rotate(-21.92 -5980.52 -2743.42)"/><linearGradient xlink:href="#a" id="bb" x1="4179.97" x2="4746.48" y1="1500.11" y2="1500.11" gradientTransform="rotate(-113.74 2455.65 1408.82) scale(.94)"/><linearGradient xlink:href="#a" id="bc" x1="4379.93" x2="4801.03" y1="1500.79" y2="1500.79" gradientTransform="rotate(-114.72 2358.94 1216.63) scale(.82)"/><linearGradient xlink:href="#a" id="bd" x1="3796.11" x2="4195.38" y1="1500.44" y2="1500.44" gradientTransform="scale(.97 -.97) rotate(-39.19 -2072.85 -1043.56)"/><linearGradient xlink:href="#a" id="be" x1="3640.2" x2="3954.5" y1="1500.38" y2="1500.38" gradientTransform="rotate(-60.49 2506.28 991.09) scale(.82)"/><linearGradient xlink:href="#a" id="bf" x1="3968.6" x2="4215.09" y1="1499.48" y2="1499.48" gradientTransform="rotate(-45.14 2524.47 1340.99) scale(.96)"/><linearGradient xlink:href="#a" id="bg" x1="3991.89" x2="4428.3" y1="1499.43" y2="1499.43" gradientTransform="rotate(-60.33 2506.47 1065.5) scale(.85)"/><linearGradient xlink:href="#a" id="bh" x1="3879.79" x2="4553.13" y1="1500.43" y2="1500.43" gradientTransform="rotate(-64.63 2487.04 959.83) scale(.8)"/><linearGradient xlink:href="#a" id="bi" x1="3815.15" x2="4094.72" y1="1499.58" y2="1499.58" gradientTransform="rotate(-43.67 2600.9 872.21) scale(.84)"/><linearGradient xlink:href="#a" id="bj" x1="3581.94" x2="3809.41" y1="1499.9" y2="1499.9" gradientTransform="scale(-.95 .95) rotate(59.12 -465.11 -2405.26)"/><linearGradient xlink:href="#a" id="bk" x1="4733.11" x2="5338.45" y1="1500.17" y2="1500.17" gradientTransform="rotate(-43.36 2592.12 935.72) scale(.86)"/><linearGradient xlink:href="#a" id="bl" x1="5265.67" x2="5914.16" y1="1500.26" y2="1500.26" gradientTransform="rotate(-146.53 2450.75 1445.76) scale(.95)"/><linearGradient xlink:href="#a" id="bm" x1="4603.81" x2="4793.16" y1="1500.49" y2="1500.49" gradientTransform="rotate(-39.15 2571.25 1143.57) scale(.92)"/><linearGradient xlink:href="#a" id="bn" x1="3397.81" x2="4160.94" y1="1500.99" y2="1500.99" gradientTransform="scale(-.84 .84) rotate(61.02 -796.67 -2450.89)"/><linearGradient xlink:href="#a" id="bo" x1="2895.15" x2="3190.65" y1="1500.24" y2="1500.24" gradientTransform="scale(-.85 .85) rotate(88.85 -682.83 -808.34)"/><linearGradient xlink:href="#a" id="bp" x1="4143.41" x2="4383.75" y1="1500.02" y2="1500.02" gradientTransform="rotate(-30.25 2587.92 1190.88) scale(.94)"/><linearGradient xlink:href="#a" id="bq" x1="4661.52" x2="5539.16" y1="1499.52" y2="1499.52" gradientTransform="rotate(-140.72 2411.5 1392.54) scale(.91)"/><linearGradient xlink:href="#a" id="br" x1="3547.16" x2="3803.79" y1="1499.78" y2="1499.78" gradientTransform="rotate(-8.94 3446.64 -399.86) scale(.89)"/><linearGradient xlink:href="#a" id="bs" x1="3994.76" x2="4370.35" y1="1500.49" y2="1500.49" gradientTransform="rotate(-23.3 2610.03 1186.76) scale(.95)"/><linearGradient xlink:href="#a" id="bt" x1="2889.33" x2="3431.36" y1="1500.33" y2="1500.33" gradientTransform="scale(-.95 .95) rotate(81.55 -435.9 -1055.8)"/><linearGradient xlink:href="#a" id="bu" x1="4739.77" x2="5109.91" y1="1500.5" y2="1500.5" gradientTransform="rotate(-45.41 2581.83 931.97) scale(.85)"/><linearGradient xlink:href="#a" id="bv" x1="3989.83" x2="4326.32" y1="1499.9" y2="1499.9" gradientTransform="rotate(-21.8 3007.4 109.2) scale(.81)"/><linearGradient xlink:href="#a" id="bw" x1="3661.88" x2="4161.96" y1="1500.21" y2="1500.21" gradientTransform="scale(-.88 .88) rotate(31.8 -867.29 -6611.94)"/><linearGradient xlink:href="#a" id="bx" x1="4849.96" x2="5782.55" y1="1501.01" y2="1501.01" gradientTransform="rotate(8.21 1681.33 2674.12) scale(.93)"/><linearGradient xlink:href="#a" id="by" x1="2750.84" x2="2830.79" y1="1500" y2="1500" gradientTransform="scale(-.87 .87) rotate(66.32 -690.4 -1996.76)"/><linearGradient xlink:href="#a" id="bz" x1="3064.92" x2="3186.59" y1="1500.56" y2="1500.56" gradientTransform="scale(-.88 .88) rotate(25.9 -928.22 -8594.64)"/><linearGradient xlink:href="#a" id="bA" x1="3435.83" x2="3698.4" y1="1499.62" y2="1499.62" gradientTransform="scale(-.96 .96) rotate(49.38 -455.65 -3302.37)"/><linearGradient xlink:href="#a" id="bB" x1="3646.91" x2="3926.03" y1="1499.93" y2="1499.93" gradientTransform="rotate(-106.79 2486.09 1465.97) scale(.98)"/><linearGradient xlink:href="#a" id="bC" x1="2637.43" x2="2930.22" y1="1499.57" y2="1499.57" gradientTransform="scale(.95 -.95) rotate(-59.26 -467.43 -739.42)"/><linearGradient xlink:href="#a" id="bD" x1="2924.83" x2="3729.94" y1="1499.99" y2="1499.99" gradientTransform="scale(-.97 .97) rotate(36.18 -439.23 -5242.77)"/><linearGradient xlink:href="#a" id="bE" x1="4259.07" x2="4837.28" y1="1499.31" y2="1499.31" gradientTransform="scale(-.89 .89) rotate(36.53 -754.64 -5461.6)"/><linearGradient xlink:href="#a" id="bF" x1="4340.46" x2="4673.53" y1="1499.58" y2="1499.58" gradientTransform="rotate(11.68 1544.35 2774.29) scale(.89)"/><linearGradient xlink:href="#a" id="bG" x1="3239.55" x2="3355.03" y1="1499.99" y2="1499.99" gradientTransform="scale(-.97 .97) rotate(28.36 -475.77 -7211.31)"/><linearGradient xlink:href="#a" id="bH" x1="5070.42" x2="6442.79" y1="1500.64" y2="1500.64" gradientTransform="rotate(36.71 2272.82 1695.88) scale(.94)"/><linearGradient xlink:href="#a" id="bI" x1="3870.69" x2="4576.62" y1="1500.53" y2="1500.53" gradientTransform="rotate(-34.43 2546.24 1308.64) scale(.96)"/><linearGradient xlink:href="#a" id="bJ" x1="4796.56" x2="5130.45" y1="1499.91" y2="1499.91" gradientTransform="rotate(-16.86 2646.44 1148.4) scale(.96)"/><linearGradient xlink:href="#a" id="bK" x1="3513.66" x2="3806.14" y1="1499.29" y2="1499.29" gradientTransform="scale(-.97 .97) rotate(30.29 -470.14 -6632.67)"/><linearGradient xlink:href="#a" id="bL" x1="3407.59" x2="4073.69" y1="1500.78" y2="1500.78" gradientTransform="scale(-.93 .93) rotate(-24.89 -167.35 11821.68)"/><linearGradient xlink:href="#a" id="bM" x1="4805.43" x2="5543.05" y1="1500.21" y2="1500.21" gradientTransform="rotate(10.39 931 3644.21) scale(.83)"/><linearGradient xlink:href="#a" id="bN" x1="3267.95" x2="3399.02" y1="1500.21" y2="1500.21" gradientTransform="scale(-.84 .84) rotate(9.9 -2255.3 -26140.62)"/><linearGradient xlink:href="#a" id="bO" x1="4746.18" x2="5074.6" y1="1499.51" y2="1499.51" gradientTransform="scale(-.86 .86) rotate(25.91 -1070.3 -8697)"/><linearGradient xlink:href="#a" id="bP" x1="4623.35" x2="5478.47" y1="1500" y2="1500" gradientTransform="rotate(19.18 1509.76 2660.14) scale(.83)"/><linearGradient xlink:href="#a" id="bQ" x1="3847.37" x2="3917.71" y1="1500.32" y2="1500.32" gradientTransform="rotate(17.61 2212.62 1848.15) scale(.95)"/><linearGradient xlink:href="#a" id="bR" x1="4007.66" x2="4314.23" y1="1501.29" y2="1501.29" gradientTransform="scale(-.89 .89) rotate(-24.25 -55.37 12427.05)"/><linearGradient xlink:href="#a" id="bS" x1="3924.34" x2="4270.56" y1="1500.77" y2="1500.77" gradientTransform="rotate(64.62 2431.48 1534.8) scale(.97)"/><linearGradient xlink:href="#a" id="bT" x1="4060.52" x2="4367.61" y1="1500.18" y2="1500.18" gradientTransform="scale(-.93 .93) rotate(30.88 -643.8 -6635.1)"/><linearGradient xlink:href="#a" id="bU" x1="4303.52" x2="4694.09" y1="1500.3" y2="1500.3" gradientTransform="rotate(68.45 2082.23 1693.08) scale(.82)"/><linearGradient xlink:href="#a" id="bV" x1="4141.61" x2="5380.74" y1="1499.77" y2="1499.77" gradientTransform="scale(-.87 .87) rotate(-2 5812.94 136862.32)"/><linearGradient xlink:href="#a" id="bW" x1="3810.93" x2="4281.17" y1="1500.18" y2="1500.18" gradientTransform="rotate(33.43 2168 1802.86) scale(.91)"/><linearGradient xlink:href="#a" id="bX" x1="3662.9" x2="3809.13" y1="1500.49" y2="1500.49" gradientTransform="scale(-.88 .88) rotate(-22.27 19.92 13501.33)"/><linearGradient xlink:href="#a" id="bY" x1="3509.31" x2="4458.48" y1="1500.53" y2="1500.53" gradientTransform="scale(-.98 .98) rotate(-52.2 -319.87 5988)"/><linearGradient xlink:href="#a" id="bZ" x1="3490.23" x2="4255.87" y1="1499.98" y2="1499.98" gradientTransform="scale(-.98 .98) rotate(31.92 -423.24 -6155.24)"/><linearGradient xlink:href="#a" id="ca" x1="4713.2" x2="4999.63" y1="1499.66" y2="1499.66" gradientTransform="rotate(-4.8 4266.6 -1727.77) scale(.89)"/><linearGradient xlink:href="#a" id="cb" x1="2968.66" x2="3202.91" y1="1500.14" y2="1500.14" gradientTransform="scale(-.98 .98) rotate(-27.23 -280.09 10565.8)"/><linearGradient xlink:href="#a" id="cc" x1="4648.98" x2="5151.93" y1="1499.77" y2="1499.77" gradientTransform="scale(-.91 .91) rotate(-31.81 -193.93 9637.53)"/><linearGradient xlink:href="#a" id="cd" x1="4770.02" x2="5610.19" y1="1499.12" y2="1499.12" gradientTransform="rotate(14.95 1368.17 2921.7) scale(.84)"/><linearGradient xlink:href="#a" id="ce" x1="3345.97" x2="3446.64" y1="1499.94" y2="1499.94" gradientTransform="rotate(26.68 1901.97 2111.9) scale(.86)"/><linearGradient xlink:href="#a" id="cf" x1="3814.31" x2="4469.32" y1="1501.14" y2="1501.14" gradientTransform="scale(-.97 .97) rotate(-10.78 -126.9 24953.51)"/><linearGradient xlink:href="#a" id="cg" x1="4539.41" x2="4754.54" y1="1499.87" y2="1499.87" gradientTransform="rotate(16.37 1583.71 2625.47) scale(.86)"/><linearGradient xlink:href="#a" id="ch" x1="3913.42" x2="4933.76" y1="1500.56" y2="1500.56" gradientTransform="scale(-.83 .83) rotate(-10.95 1002.6 27006.4)"/><linearGradient xlink:href="#a" id="ci" x1="3848.9" x2="4450.91" y1="1500.19" y2="1500.19" gradientTransform="scale(-.94 .94) rotate(2.17 -2993.13 -117258.9)"/><linearGradient xlink:href="#a" id="cj" x1="4120.83" x2="4429.86" y1="1500.33" y2="1500.33" gradientTransform="rotate(-97.68 2365.96 1084.07) scale(.77)"/><linearGradient xlink:href="#a" id="ck" x1="3252.65" x2="3380.8" y1="1500.39" y2="1500.39" gradientTransform="rotate(-175.86 2444.59 1464.38) scale(.95)"/><linearGradient xlink:href="#a" id="cl" x1="3563.29" x2="4321.2" y1="1500.83" y2="1500.83" gradientTransform="scale(.95 -.95) rotate(-28.1 -3913.84 -1615.92)"/><linearGradient xlink:href="#a" id="cm" x1="3490.34" x2="3673.24" y1="1500.57" y2="1500.57" gradientTransform="rotate(-93.91 2484.72 1448.5) scale(.97)"/><linearGradient xlink:href="#a" id="cn" x1="4620.07" x2="4802.99" y1="1499.19" y2="1499.19" gradientTransform="rotate(-156.08 2412.89 1418.81) scale(.92)"/><linearGradient xlink:href="#a" id="co" x1="3191.37" x2="3308.15" y1="1499.72" y2="1499.72" gradientTransform="scale(-.97 .97) rotate(51.49 -405.57 -3065.21)"/><linearGradient xlink:href="#a" id="cp" x1="4795.09" x2="5602.46" y1="1500.15" y2="1500.15" gradientTransform="rotate(176.93 2475.16 1486.2) scale(.98)"/><linearGradient xlink:href="#a" id="cq" x1="5720.96" x2="6053.96" y1="1498.8" y2="1498.8" gradientTransform="rotate(-159.12 2290.32 1315.01) scale(.81)"/><linearGradient xlink:href="#a" id="cr" x1="2826.07" x2="3078.43" y1="1499.86" y2="1499.86" gradientTransform="scale(.91 -.91) rotate(-61.11 -370.65 -842.9)"/><linearGradient xlink:href="#a" id="cs" x1="3758.71" x2="4028.03" y1="1500.05" y2="1500.05" gradientTransform="rotate(-172.12 2497.3 1498.44)"/><linearGradient xlink:href="#a" id="ct" x1="3065.89" x2="3410.03" y1="1500.06" y2="1500.06" gradientTransform="scale(-.98 .98) rotate(61.77 -377.68 -2159.06)"/><linearGradient xlink:href="#a" id="cu" x1="5513.27" x2="5864.36" y1="1498.56" y2="1498.56" gradientTransform="rotate(-167.75 2252.7 1312.76) scale(.79)"/><linearGradient xlink:href="#a" id="cv" x1="3415.91" x2="3797.01" y1="1500.48" y2="1500.48" gradientTransform="scale(-.98 .98) rotate(32.94 -403.21 -5917.48)"/><linearGradient xlink:href="#a" id="cw" x1="3949.81" x2="4092.75" y1="1499.42" y2="1499.42" gradientTransform="rotate(-106.72 2329.81 1086.12) scale(.75)"/><linearGradient xlink:href="#a" id="cx" x1="2434.62" x2="2628.54" y1="1500.31" y2="1500.31" gradientTransform="scale(-.96 .96) rotate(60.16 -426.43 -2310.74)"/><linearGradient xlink:href="#a" id="cy" x1="3057.52" x2="3141.48" y1="1499.65" y2="1499.65" gradientTransform="rotate(-71.55 2499.02 1490.22)"/><linearGradient xlink:href="#a" id="cz" x1="3574.99" x2="3810.48" y1="1501.03" y2="1501.03" gradientTransform="rotate(-29.67 2756.06 611.76) scale(.84)"/><linearGradient xlink:href="#a" id="cA" x1="3046.31" x2="3410.86" y1="1499.85" y2="1499.85" gradientTransform="scale(-.97 .97) rotate(61.37 -406.67 -2201.4)"/><linearGradient xlink:href="#a" id="cB" x1="2628.56" x2="3457.72" y1="1499.43" y2="1499.43" gradientTransform="scale(-.76 .76) rotate(83.8 -992.43 -1122.63)"/><linearGradient xlink:href="#a" id="cC" x1="2418.82" x2="2550.31" y1="1500.18" y2="1500.18" gradientTransform="scale(.87 -.87) rotate(-23.07 -5538.85 -2641.06)"/><linearGradient xlink:href="#a" id="cD" x1="3872.83" x2="4100.39" y1="1500.23" y2="1500.23" gradientTransform="scale(.86 -.86) rotate(-53.58 -838.38 -1170.9)"/><linearGradient xlink:href="#a" id="cE" x1="3881.89" x2="4004.95" y1="1501.09" y2="1501.09" gradientTransform="rotate(-11.63 3960.67 -1619.35) scale(.76)"/><linearGradient xlink:href="#a" id="cF" x1="3214.28" x2="3490.13" y1="1500.2" y2="1500.2" gradientTransform="scale(-.93 .93) rotate(70.71 -511.97 -1634.69)"/><linearGradient xlink:href="#a" id="cG" x1="4136.98" x2="4571.01" y1="1499.71" y2="1499.71" gradientTransform="rotate(-23.97 2647.12 1075.28) scale(.94)"/><linearGradient xlink:href="#a" id="cH" x1="3129.39" x2="3273.72" y1="1500.13" y2="1500.13" gradientTransform="rotate(-73.53 2455.47 1064.33) scale(.82)"/><linearGradient xlink:href="#a" id="cI" x1="5702.69" x2="6843.32" y1="1501" y2="1501" gradientTransform="rotate(-30.45 2724.67 697.7) scale(.85)"/><linearGradient xlink:href="#a" id="cJ" x1="2988.08" x2="3067.99" y1="1499.61" y2="1499.61" gradientTransform="scale(.95 -.95) rotate(-38.03 -2226.8 -1189.58)"/><linearGradient xlink:href="#a" id="cK" x1="4798.47" x2="5152.66" y1="1498.94" y2="1498.94" gradientTransform="rotate(-141.34 2376.8 1351.07) scale(.87)"/><linearGradient xlink:href="#a" id="cL" x1="3475.94" x2="3790.55" y1="1499.4" y2="1499.4" gradientTransform="scale(-.98 .98) rotate(43.77 -396.64 -3952.85)"/><linearGradient xlink:href="#a" id="cM" x1="3255.92" x2="3545.83" y1="1500.07" y2="1500.07" gradientTransform="scale(-.92 .92) rotate(-5.72 841.23 47252.35)"/><linearGradient xlink:href="#a" id="cN" x1="2866.45" x2="2962.84" y1="1499.58" y2="1499.58" gradientTransform="scale(.91 -.91) rotate(-50.15 -1066.42 -1050.53)"/><linearGradient xlink:href="#a" id="cO" x1="4448.6" x2="5885.71" y1="1500.36" y2="1500.36" gradientTransform="scale(-.88 .88) rotate(40.17 -793.91 -4794.2)"/><linearGradient xlink:href="#a" id="cP" x1="3540.34" x2="3677.32" y1="1500.32" y2="1500.32" gradientTransform="rotate(-51.18 2516.73 1315.41) scale(.95)"/><linearGradient xlink:href="#a" id="cQ" x1="2767.28" x2="2852.86" y1="1500.31" y2="1500.31" gradientTransform="scale(.78 -.78) rotate(-74.12 257.8 -1119.8)"/><linearGradient xlink:href="#a" id="cR" x1="4013.6" x2="4799.7" y1="1500.43" y2="1500.43" gradientTransform="rotate(-41.9 2543.18 1254.18) scale(.94)"/><linearGradient xlink:href="#a" id="cS" x1="4329.44" x2="5112.94" y1="1500.26" y2="1500.26" gradientTransform="scale(-.99 .99) rotate(1.33 -1154.55 -186455.05)"/><linearGradient xlink:href="#a" id="cT" x1="4388.45" x2="4691.79" y1="1499.5" y2="1499.5" gradientTransform="rotate(-47.11 2540.76 1191.68) scale(.91)"/><linearGradient xlink:href="#a" id="cU" x1="2568.84" x2="2785.65" y1="1499.64" y2="1499.64" gradientTransform="matrix(-.79 .3877 .3877 .79 3378.17 -406.8)"/><linearGradient xlink:href="#a" id="cV" x1="4033.01" x2="4662.69" y1="1499.07" y2="1499.07" gradientTransform="rotate(8.98 657.66 4079.63) scale(.83)"/><linearGradient xlink:href="#a" id="cW" x1="3594.58" x2="4210.94" y1="1499.62" y2="1499.62" gradientTransform="scale(-.95 .95) rotate(55.98 -464.18 -2672.24)"/><linearGradient xlink:href="#a" id="cX" x1="3432.1" x2="3666.26" y1="1500.41" y2="1500.41" gradientTransform="scale(-.92 .92) rotate(-.59 12637.95 443953.7)"/><linearGradient xlink:href="#a" id="cY" x1="4701.9" x2="5415.82" y1="1501.09" y2="1501.09" gradientTransform="scale(-.87 .87) rotate(-31.32 -110.54 10013.6)"/><linearGradient xlink:href="#a" id="cZ" x1="3905.01" x2="4382.08" y1="1499.87" y2="1499.87" gradientTransform="rotate(-8.27 3765.9 -998.06) scale(.86)"/><linearGradient xlink:href="#a" id="da" x1="3481.97" x2="4049.05" y1="1500" y2="1500" gradientTransform="scale(-.97 .97) rotate(-31.84 -291.26 9285.09)"/><linearGradient xlink:href="#a" id="db" x1="4509.73" x2="5418.8" y1="1500.46" y2="1500.46" gradientTransform="scale(-.96 .96) rotate(-25.08 -249.3 11547.34)"/><linearGradient xlink:href="#a" id="dc" x1="4382.95" x2="5315.93" y1="1500.59" y2="1500.59" gradientTransform="scale(-.88 .88) rotate(14.68 -1297.62 -16573.6)"/><linearGradient xlink:href="#a" id="dd" x1="4717.94" x2="5114.83" y1="1499.81" y2="1499.81" gradientTransform="scale(-.79 .79) rotate(-2.68 8005 108587.84)"/><linearGradient xlink:href="#a" id="de" x1="3714.34" x2="3832.53" y1="1499.48" y2="1499.48" gradientTransform="rotate(38.89 2115.82 1816.43) scale(.89)"/><linearGradient xlink:href="#a" id="df" x1="3611.25" x2="3802.12" y1="1500.49" y2="1500.49" gradientTransform="rotate(19.23 2487.93 1516.34)"/><linearGradient xlink:href="#a" id="dg" x1="3084.72" x2="3318.9" y1="1499" y2="1499" gradientTransform="scale(-1 1) rotate(25.54 -334.37 -8078.04)"/><linearGradient xlink:href="#a" id="dh" x1="4416.2" x2="4632.73" y1="1500.19" y2="1500.19" gradientTransform="scale(-1 1) rotate(15.18 -330.15 -14796.55)"/><linearGradient xlink:href="#a" id="di" x1="3384.36" x2="3721.82" y1="1499.49" y2="1499.49" gradientTransform="rotate(-76.57 2484.83 1380.14) scale(.95)"/><linearGradient xlink:href="#a" id="dj" x1="2778.32" x2="3159.15" y1="1500.48" y2="1500.48" gradientTransform="scale(-.76 .76) rotate(17.29 -2311.45 -15114.16)"/><linearGradient xlink:href="#a" id="dk" x1="5566.39" x2="6274.41" y1="1500.14" y2="1500.14" gradientTransform="rotate(-32.62 2681.3 806.93) scale(.86)"/><linearGradient xlink:href="#a" id="dl" x1="4350.96" x2="4843.85" y1="1500.2" y2="1500.2" gradientTransform="rotate(-9.04 2667.09 1166.27) scale(.98)"/><linearGradient xlink:href="#a" id="dm" x1="4196.34" x2="4451.34" y1="1500.28" y2="1500.28" gradientTransform="rotate(7.19 1183.25 3420.3) scale(.9)"/><linearGradient xlink:href="#a" id="dn" x1="3654.19" x2="4035.21" y1="1499.45" y2="1499.45" gradientTransform="scale(-.82 .82) rotate(-41.71 -163.64 8065.9)"/><linearGradient xlink:href="#a" id="do" x1="4682.73" x2="5212.24" y1="1499.42" y2="1499.42" gradientTransform="rotate(42.74 2378.56 1593.32) scale(.96)"/><linearGradient xlink:href="#a" id="dp" x1="4122.07" x2="4306.22" y1="1500.58" y2="1500.58" gradientTransform="scale(-.96 .96) rotate(-2.07 1309.97 124663.33)"/><linearGradient xlink:href="#a" id="dq" x1="3613.52" x2="4111.08" y1="1500.39" y2="1500.39" gradientTransform="rotate(18.51 1982.54 2114.15) scale(.91)"/><linearGradient xlink:href="#a" id="dr" x1="4541.26" x2="5579.64" y1="1500.94" y2="1500.94" gradientTransform="scale(-.85 .85) rotate(-19.34 213.69 15692.48)"/><linearGradient xlink:href="#a" id="ds" x1="3544.62" x2="3675.04" y1="1500.54" y2="1500.54" gradientTransform="scale(-.87 .87) rotate(-31.32 -120.57 10037.47)"/><linearGradient xlink:href="#a" id="dt" x1="4186.86" x2="4646.95" y1="1500.46" y2="1500.46" gradientTransform="rotate(-16.48 3071.13 131.22) scale(.85)"/><linearGradient xlink:href="#a" id="du" x1="3056.11" x2="3188.54" y1="1499.61" y2="1499.61" gradientTransform="scale(-.82 .82) rotate(-28.9 27.16 11177.41)"/><linearGradient xlink:href="#a" id="dv" x1="2774.27" x2="2921.22" y1="1500.66" y2="1500.66" gradientTransform="scale(-.81 .81) rotate(-27.48 96.93 11756.91)"/><linearGradient xlink:href="#a" id="dw" x1="5079.98" x2="5565.29" y1="1499.98" y2="1499.98" gradientTransform="rotate(29.42 1616.48 2362.92) scale(.78)"/><linearGradient xlink:href="#a" id="dx" x1="5008.54" x2="5807.98" y1="1500.14" y2="1500.14" gradientTransform="scale(-.78 .78) rotate(-32.35 39.04 10438.78)"/><linearGradient xlink:href="#a" id="dy" x1="3210.77" x2="3689.3" y1="1499.51" y2="1499.51" gradientTransform="scale(-.86 .86) rotate(.56 -26356.94 -483482.16)"/><linearGradient xlink:href="#a" id="dz" x1="4187.48" x2="4518.47" y1="1500.19" y2="1500.19" gradientTransform="rotate(-11.25 2721.12 1036.01) scale(.97)"/><linearGradient xlink:href="#a" id="dA" x1="3856.79" x2="4475.58" y1="1499.35" y2="1499.35" gradientTransform="scale(-.95 .95) rotate(35 -527.1 -5552.57)"/><linearGradient xlink:href="#a" id="dB" x1="3786.14" x2="4152.19" y1="1500.45" y2="1500.45" gradientTransform="rotate(106.61 2248.78 1525.26) scale(.86)"/><linearGradient xlink:href="#a" id="dC" x1="3207.3" x2="3568.58" y1="1499.39" y2="1499.39" gradientTransform="scale(-.8 .8) rotate(-86.35 -440.11 4341.41)"/><linearGradient xlink:href="#a" id="dD" x1="4965.43" x2="9282.46" y1="1499.66" y2="1499.66" gradientTransform="rotate(32.14 2189.47 1789.54) scale(.92)"/><linearGradient xlink:href="#a" id="dE" x1="3752.46" x2="4099.3" y1="1499.88" y2="1499.88" gradientTransform="rotate(58.26 2377.08 1570.14) scale(.95)"/><linearGradient xlink:href="#a" id="dF" x1="4626.19" x2="4834.69" y1="1500.11" y2="1500.11" gradientTransform="rotate(-19.3 2934.53 388.87) scale(.86)"/><linearGradient xlink:href="#a" id="dG" x1="3259.39" x2="3372.12" y1="1500.8" y2="1500.8" gradientTransform="scale(.94 -.94) rotate(87.81 3864.78 375.7)"/><linearGradient xlink:href="#a" id="dH" x1="4397.88" x2="4611.25" y1="1498.17" y2="1498.17" gradientTransform="rotate(-154.38 1767.35 797.6) scale(.32)"/><linearGradient xlink:href="#a" id="dI" x1="4924.28" x2="5980.08" y1="1499.52" y2="1499.52" gradientTransform="rotate(-131.82 1858.43 581.62) scale(.3)"/><linearGradient xlink:href="#a" id="dJ" x1="5441.83" x2="6124.65" y1="1501.06" y2="1501.06" gradientTransform="rotate(162.79 1959.38 1277.77) scale(.6)"/><linearGradient xlink:href="#a" id="dK" x1="3829.96" x2="5127.29" y1="1500.16" y2="1500.16" gradientTransform="scale(.52 -.52) rotate(-22.09 -7899.62 -8297.9)"/><linearGradient xlink:href="#a" id="dL" x1="6848.71" x2="7631.24" y1="1498.14" y2="1498.14" gradientTransform="scale(.3 -.3) rotate(-64.77 -32.12 -6892.95)"/><linearGradient xlink:href="#a" id="dM" x1="4715.83" x2="6420.3" y1="1500.71" y2="1500.71" gradientTransform="scale(.38 -.38) rotate(-73.99 595.87 -4389.77)"/><linearGradient xlink:href="#a" id="dN" x1="8032.55" x2="8858.11" y1="1496.96" y2="1496.96" gradientTransform="scale(.38 -.38) rotate(-27.37 -6944.6 -11000.87)"/><linearGradient xlink:href="#a" id="dO" x1="6140.53" x2="6726.26" y1="1498.86" y2="1498.86" gradientTransform="rotate(-167.26 2147.08 1230.55) scale(.7)"/><linearGradient xlink:href="#a" id="dP" x1="2812.32" x2="3134.76" y1="1499.38" y2="1499.38" gradientTransform="scale(-.56 .56) rotate(70.33 -2159.03 -2384.47)"/><linearGradient xlink:href="#a" id="dQ" x1="3644.28" x2="3919.2" y1="1500.95" y2="1500.95" gradientTransform="rotate(-44.67 2765.33 -254.15) scale(.54)"/><linearGradient xlink:href="#a" id="dR" x1="3401.12" x2="3600.45" y1="1499" y2="1499" gradientTransform="rotate(-36.07 3252.6 -1776.76) scale(.29)"/><linearGradient xlink:href="#a" id="dS" x1="3477.99" x2="3770.44" y1="1500.88" y2="1500.88" gradientTransform="scale(-.91 .91) rotate(64.34 -573.13 -2073.98)"/><linearGradient xlink:href="#a" id="dT" x1="5119.3" x2="5693.25" y1="1498.8" y2="1498.8" gradientTransform="rotate(-168.71 2187.67 1267.55) scale(.73)"/><linearGradient xlink:href="#a" id="dU" x1="3492.89" x2="4215.45" y1="1500.69" y2="1500.69" gradientTransform="scale(-.73 .73) rotate(83.35 -1103.65 -1180.9)"/><linearGradient xlink:href="#a" id="dV" x1="4325.26" x2="4637.03" y1="1500.2" y2="1500.2" gradientTransform="rotate(-16.92 4640.59 -3668.44) scale(.44)"/><linearGradient xlink:href="#a" id="dW" x1="4201.97" x2="4327.2" y1="1499.6" y2="1499.6" gradientTransform="scale(-.69 .69) rotate(51.08 -1585.37 -3888)"/><linearGradient xlink:href="#a" id="dX" x1="5139.1" x2="5778.73" y1="1500.43" y2="1500.43" gradientTransform="scale(.53 -.53) rotate(-33.98 -3809.56 -5358.34)"/><linearGradient xlink:href="#a" id="dY" x1="5781.03" x2="7206.61" y1="1500.96" y2="1500.96" gradientTransform="rotate(-20 3297.07 -582.6) scale(.73)"/><linearGradient xlink:href="#a" id="dZ" x1="4536.56" x2="4937.46" y1="1500.49" y2="1500.49" gradientTransform="scale(-.82 .82) rotate(31.23 -1205.15 -7076.52)"/><linearGradient xlink:href="#a" id="ea" x1="3863.69" x2="4095.65" y1="1499.24" y2="1499.24" gradientTransform="rotate(-134.53 2172.41 1053.65) scale(.65)"/><linearGradient xlink:href="#a" id="eb" x1="4619.19" x2="5803.43" y1="1499.41" y2="1499.41" gradientTransform="scale(-.34 .34) rotate(9.54 -20175.8 -52185.01)"/><linearGradient xlink:href="#a" id="ec" x1="3576.42" x2="3730.59" y1="1499.64" y2="1499.64" gradientTransform="rotate(-48.16 2523.26 1307.81) scale(.95)"/><linearGradient xlink:href="#a" id="ed" x1="4561.35" x2="4977.58" y1="1501.35" y2="1501.35" gradientTransform="matrix(-.149 .4982 .4982 .149 2027.35 357.78)"/><linearGradient xlink:href="#a" id="ee" x1="4945.94" x2="7187.81" y1="1499.53" y2="1499.53" gradientTransform="rotate(-29.07 2638 1036.82) scale(.92)"/><linearGradient xlink:href="#a" id="ef" x1="11325.74" x2="12433.19" y1="1499.66" y2="1499.66" gradientTransform="rotate(-153.09 1761.45 776.33) scale(.31)"/><linearGradient xlink:href="#a" id="eg" x1="4197.73" x2="5472.55" y1="1500.01" y2="1500.01" gradientTransform="scale(-.46 .46) rotate(54.09 -3531.55 -4743.1)"/><linearGradient xlink:href="#a" id="eh" x1="5421.01" x2="5733.06" y1="1498.32" y2="1498.32" gradientTransform="scale(.26 -.26) rotate(-74.5 951.04 -7270.42)"/><linearGradient xlink:href="#a" id="ei" x1="5307.5" x2="6196.6" y1="1499.74" y2="1499.74" gradientTransform="rotate(-63.74 2481.83 350.36) scale(.58)"/><linearGradient xlink:href="#a" id="ej" x1="3172.3" x2="3411.5" y1="1499.05" y2="1499.05" gradientTransform="scale(.57 -.57) rotate(-38.53 -2789.75 -4216.97)"/><linearGradient xlink:href="#a" id="ek" x1="4164.14" x2="4455.27" y1="1501.02" y2="1501.02" gradientTransform="rotate(-.85 37392.52 -57695.95) scale(.65)"/><linearGradient xlink:href="#a" id="el" x1="4889.97" x2="6632.57" y1="1500.02" y2="1500.02" gradientTransform="scale(-.43 .43) rotate(37.88 -4886.4 -8660.2)"/><linearGradient xlink:href="#a" id="em" x1="5329.92" x2="5742.31" y1="1498.72" y2="1498.72" gradientTransform="rotate(-54.72 2598.3 -62.57) scale(.51)"/><linearGradient xlink:href="#a" id="en" x1="8628.72" x2="9215.7" y1="1502.44" y2="1502.44" gradientTransform="rotate(-8.54 8515.93 -10470.64) scale(.32)"/><linearGradient xlink:href="#a" id="eo" x1="3956.69" x2="4214.52" y1="1500" y2="1500" gradientTransform="scale(-.85 .85) rotate(16.27 -1443.9 -15129.28)"/><linearGradient xlink:href="#a" id="ep" x1="4881.32" x2="5330.56" y1="1498.59" y2="1498.59" gradientTransform="rotate(-133.54 1829.03 569.57) scale(.28)"/><linearGradient xlink:href="#a" id="eq" x1="4507.84" x2="5079.97" y1="1498.4" y2="1498.4" gradientTransform="scale(.51 -.51) rotate(-74.15 433.82 -2726.44)"/><linearGradient xlink:href="#a" id="er" x1="4400.09" x2="4696.53" y1="1499.8" y2="1499.8" gradientTransform="rotate(-114.04 2151.9 787.88) scale(.54)"/><linearGradient xlink:href="#a" id="es" x1="3723.25" x2="4168.59" y1="1499.22" y2="1499.22" gradientTransform="scale(-.52 .52) rotate(13.49 -7394.4 -25870.16)"/><linearGradient xlink:href="#a" id="et" x1="7664.35" x2="10722.72" y1="1497.58" y2="1497.58" gradientTransform="rotate(10.01 -2918.33 8960.79) scale(.45)"/><linearGradient xlink:href="#a" id="eu" x1="3454.05" x2="3701.91" y1="1498.79" y2="1498.79" gradientTransform="scale(-.78 .78) rotate(16.9 -2139.57 -15251.17)"/><linearGradient xlink:href="#a" id="ev" x1="5256.5" x2="6338.03" y1="1500.66" y2="1500.66" gradientTransform="rotate(-23.48 3624.46 -1724.28) scale(.52)"/><linearGradient xlink:href="#a" id="ew" x1="3668.11" x2="3984.94" y1="1500.42" y2="1500.42" gradientTransform="scale(-.33 .33) rotate(40.78 -6924.25 -9670.36)"/><linearGradient xlink:href="#a" id="ex" x1="4567.2" x2="4752.78" y1="1500.52" y2="1500.52" gradientTransform="scale(-.69 .69) rotate(44.46 -1725.82 -4843.71)"/><linearGradient xlink:href="#a" id="ey" x1="3888.3" x2="4283.3" y1="1500.9" y2="1500.9" gradientTransform="rotate(-37.22 2576.19 1149.06) scale(.92)"/><linearGradient xlink:href="#a" id="ez" x1="4634.67" x2="5838.34" y1="1499.49" y2="1499.49" gradientTransform="scale(-.74 .74) rotate(20.29 -2206.12 -12857.97)"/><linearGradient xlink:href="#a" id="eA" x1="4084.73" x2="4261.89" y1="1498.15" y2="1498.15" gradientTransform="rotate(-107.74 2009.94 339.29) scale(.3)"/><linearGradient xlink:href="#a" id="eB" x1="3616.81" x2="4308.95" y1="1500.72" y2="1500.72" gradientTransform="scale(-.6 .6) rotate(-6.65 7384.04 53798.05)"/><linearGradient xlink:href="#a" id="eC" x1="4953.81" x2="5586.06" y1="1499.91" y2="1499.91" gradientTransform="rotate(8.46 939.44 3712.83) scale(.86)"/><linearGradient xlink:href="#a" id="eD" x1="5711.28" x2="6170.93" y1="1500.14" y2="1500.14" gradientTransform="scale(-.45 .45) rotate(54.38 -3632.27 -4788.7)"/><linearGradient xlink:href="#a" id="eE" x1="3938.67" x2="4571.87" y1="1500.33" y2="1500.33" gradientTransform="rotate(-28.96 3285.12 -1154.65) scale(.53)"/><linearGradient xlink:href="#a" id="eF" x1="3835.4" x2="4240.57" y1="1500.56" y2="1500.56" gradientTransform="scale(.53 -.53) rotate(-78.41 625.57 -2427.88)"/><linearGradient xlink:href="#a" id="eG" x1="3842.2" x2="4588.02" y1="1499.21" y2="1499.21" gradientTransform="rotate(-73.15 2333.12 -201.21) scale(.3)"/><linearGradient xlink:href="#a" id="eH" x1="5650.53" x2="7095.03" y1="1500.73" y2="1500.73" gradientTransform="rotate(28.34 1174.84 2819.37) scale(.69)"/><linearGradient xlink:href="#a" id="eI" x1="4346.61" x2="5220.58" y1="1500.25" y2="1500.25" gradientTransform="scale(-.73 .73) rotate(-37.34 31.57 9568.62)"/><linearGradient xlink:href="#a" id="eJ" x1="4856.72" x2="5039.2" y1="1499.89" y2="1499.89" gradientTransform="rotate(-3.05 7646.6 -7607.56) scale(.81)"/><linearGradient xlink:href="#a" id="eK" x1="4865.74" x2="5123.12" y1="1499.19" y2="1499.19" gradientTransform="rotate(-35.97 3116.7 -1172.69) scale(.42)"/><linearGradient xlink:href="#a" id="eL" x1="3161.33" x2="3578.98" y1="1500.72" y2="1500.72" gradientTransform="scale(-.88 .88) rotate(77.62 -616.35 -1311.25)"/><linearGradient xlink:href="#a" id="eM" x1="5447.03" x2="5703.61" y1="1500.47" y2="1500.47" gradientTransform="rotate(-4.81 9231.58 -10867.85) scale(.59)"/><linearGradient xlink:href="#a" id="eN" x1="4675.85" x2="5215.53" y1="1501.45" y2="1501.45" gradientTransform="matrix(-.62 -.00065 -.00065 .62 3642.35 571.58)"/><linearGradient xlink:href="#a" id="eO" x1="2622.86" x2="2996.27" y1="1500.32" y2="1500.32" gradientTransform="scale(-.41 .41) rotate(-6.83 15861.35 69263.42)"/><linearGradient xlink:href="#a" id="eP" x1="4146.66" x2="5285.96" y1="1499.36" y2="1499.36" gradientTransform="rotate(33.68 2100.72 1862.2) scale(.89)"/><linearGradient xlink:href="#a" id="eQ" x1="2660.54" x2="2910.74" y1="1499.23" y2="1499.23" gradientTransform="scale(-.69 .69) rotate(39.02 -1839.1 -5879.78)"/><linearGradient xlink:href="#a" id="eR" x1="5296.54" x2="6477.39" y1="1501.47" y2="1501.47" gradientTransform="rotate(38.17 771.25 2949.03) scale(.49)"/><linearGradient xlink:href="#a" id="eS" x1="4208.64" x2="4633.19" y1="1500.18" y2="1500.18" gradientTransform="scale(-.55 .55) rotate(-9.28 6179.47 41510.35)"/><linearGradient xlink:href="#a" id="eT" x1="5003.62" x2="5174.38" y1="1500.84" y2="1500.84" gradientTransform="rotate(-59.2 2543.92 -351.74) scale(.37)"/><linearGradient xlink:href="#a" id="eU" x1="3329.29" x2="3987.37" y1="1501.21" y2="1501.21" gradientTransform="scale(-.26 .26) rotate(35.13 -10565.54 -14506.07)"/><linearGradient xlink:href="#a" id="eV" x1="4340.89" x2="5042.87" y1="1499.57" y2="1499.57" gradientTransform="rotate(20.89 1827.55 2260.17) scale(.87)"/><linearGradient xlink:href="#a" id="eW" x1="4407.8" x2="6039.06" y1="1500.25" y2="1500.25" gradientTransform="rotate(-31.12 3277.15 -1318.47) scale(.46)"/><linearGradient xlink:href="#a" id="eX" x1="6293.42" x2="8154.53" y1="1501.17" y2="1501.17" gradientTransform="scale(-.55 .55) rotate(-13.51 3821.33 29088.3)"/><linearGradient xlink:href="#a" id="eY" x1="3940.28" x2="4654.36" y1="1500.22" y2="1500.22" gradientTransform="rotate(58.41 1552.3 2043.27) scale(.63)"/><linearGradient xlink:href="#a" id="eZ" x1="5673.49" x2="6312.34" y1="1500.98" y2="1500.98" gradientTransform="scale(-.28 .28) rotate(-36.42 2309.64 19825.91)"/><linearGradient xlink:href="#a" id="fa" x1="4348.21" x2="5703.8" y1="1499.98" y2="1499.98" gradientTransform="rotate(56.86 2351.4 1587.57) scale(.94)"/><linearGradient xlink:href="#a" id="fb" x1="4420.06" x2="5080.95" y1="1499.54" y2="1499.54" gradientTransform="scale(-.71 .71) rotate(.54 -65373 -568068.8)"/><linearGradient xlink:href="#a" id="fc" x1="3743.95" x2="4159.98" y1="1500.45" y2="1500.45" gradientTransform="scale(-.96 .96) rotate(-4.29 446.44 60905.85)"/><linearGradient xlink:href="#a" id="fd" x1="3393.38" x2="3659.45" y1="1501.54" y2="1501.54" gradientTransform="rotate(-60.31 2530 -658.31) scale(.26)"/><linearGradient xlink:href="#a" id="fe" x1="3902.96" x2="5755.48" y1="1499.96" y2="1499.96" gradientTransform="scale(-.46 .46) rotate(-.76 131040.71 550967.33)"/><linearGradient xlink:href="#a" id="ff" x1="3055.54" x2="3142.85" y1="1500.76" y2="1500.76" gradientTransform="rotate(44.03 1203.2 2478.26) scale(.58)"/><linearGradient xlink:href="#a" id="fg" x1="4276.37" x2="4588.24" y1="1499.48" y2="1499.48" gradientTransform="rotate(39.32 1757.03 2109.81) scale(.78)"/><linearGradient xlink:href="#a" id="fh" x1="3731.32" x2="4152.54" y1="1499.49" y2="1499.49" gradientTransform="scale(-.32 .32) rotate(-38.43 1588.71 16945.33)"/><linearGradient xlink:href="#a" id="fi" x1="9810.09" x2="10775.77" y1="1502.79" y2="1502.79" gradientTransform="rotate(51.42 396.61 2883.33) scale(.25)"/><linearGradient xlink:href="#a" id="fj" x1="3521.97" x2="4776.37" y1="1500.29" y2="1500.29" gradientTransform="scale(-.76 .76) rotate(-15 1068.55 21242.9)"/><linearGradient xlink:href="#a" id="fk" x1="5668.32" x2="7630.78" y1="1500.54" y2="1500.54" gradientTransform="rotate(-6.76 8646.26 -10259.82) scale(.46)"/><linearGradient xlink:href="#a" id="fl" x1="3426.09" x2="4012.43" y1="1500.37" y2="1500.37" gradientTransform="scale(-.92 .92) rotate(-23.22 -105.73 12638.95)"/><linearGradient xlink:href="#a" id="fm" x1="5269.31" x2="6443.52" y1="1500.24" y2="1500.24" gradientTransform="rotate(87.92 1441.09 1785.23) scale(.48)"/><linearGradient xlink:href="#a" id="fn" x1="5994.82" x2="6619.61" y1="1498.54" y2="1498.54" gradientTransform="scale(-.42 .42) rotate(-31.64 1588.15 16329.63)"/><linearGradient xlink:href="#a" id="fo" x1="4169.49" x2="4510.48" y1="1498.31" y2="1498.31" gradientTransform="rotate(-55.72 2574.72 131.98) scale(.56)"/><linearGradient xlink:href="#a" id="fp" x1="3575.34" x2="4192.22" y1="1499.45" y2="1499.45" gradientTransform="rotate(36.49 2258.78 1707.65) scale(.93)"/><linearGradient xlink:href="#a" id="fq" x1="7154.55" x2="7972.02" y1="1500.7" y2="1500.7" gradientTransform="scale(-.46 .46) rotate(-35.36 957.82 13818.83)"/><linearGradient xlink:href="#a" id="fr" x1="4401.56" x2="4641.62" y1="1499.11" y2="1499.11" gradientTransform="rotate(36.94 845.28 2916.32) scale(.53)"/><linearGradient xlink:href="#a" id="fs" x1="5237.58" x2="5621.7" y1="1499.84" y2="1499.84" gradientTransform="scale(-.51 .51) rotate(-24.02 1845.1 18103.32)"/><linearGradient xlink:href="#a" id="ft" x1="6189.42" x2="7173.95" y1="1501.38" y2="1501.38" gradientTransform="rotate(-35.78 3267.91 -1809.04) scale(.28)"/><linearGradient xlink:href="#a" id="fu" x1="8820.5" x2="9414.96" y1="1499.75" y2="1499.75" gradientTransform="rotate(-14.1 5741.78 -5806.01) scale(.33)"/><linearGradient xlink:href="#a" id="fv" x1="3637.93" x2="4216.96" y1="1499.53" y2="1499.53" gradientTransform="rotate(-26.64 3253.47 -869.47) scale(.61)"/><linearGradient xlink:href="#a" id="fw" x1="3704.93" x2="4430.44" y1="1500.77" y2="1500.77" gradientTransform="scale(-.41 .41) rotate(-80.37 -849.54 7265.07)"/><linearGradient xlink:href="#a" id="fx" x1="6028.26" x2="6489.42" y1="1499.96" y2="1499.96" gradientTransform="rotate(.76 -41418.67 73638.07) scale(.62)"/><linearGradient xlink:href="#a" id="fy" x1="3282.47" x2="3592.87" y1="1499.62" y2="1499.62" gradientTransform="scale(-.82 .82) rotate(20.83 -1505.72 -11639.8)"/><linearGradient xlink:href="#a" id="fz" x1="6337.8" x2="6927.62" y1="1500.17" y2="1500.17" gradientTransform="rotate(48.33 539.72 2865.75) scale(.33)"/><linearGradient xlink:href="#a" id="fA" x1="4218.75" x2="4731.05" y1="1500.32" y2="1500.32" gradientTransform="rotate(101.26 1874.92 1592.54) scale(.66)"/><linearGradient xlink:href="#a" id="fB" x1="3546.91" x2="3729.53" y1="1500.6" y2="1500.6" gradientTransform="scale(.55 -.55) rotate(77.27 5830.56 1078.87)"/><linearGradient xlink:href="#a" id="fC" x1="3532.8" x2="4280.26" y1="1499.84" y2="1499.84" gradientTransform="scale(.76 -.76) rotate(65.51 5271.66 884.8)"/><linearGradient xlink:href="#a" id="fD" x1="5330.23" x2="6224.4" y1="1499.24" y2="1499.24" gradientTransform="rotate(100.7 1478.79 1655.96) scale(.45)"/><linearGradient xlink:href="#a" id="fE" x1="4832.81" x2="5104.52" y1="1500.33" y2="1500.33" gradientTransform="rotate(65.7 1419.72 2031.52) scale(.55)"/><linearGradient xlink:href="#a" id="fF" x1="4349.6" x2="4639.33" y1="1500.47" y2="1500.47" gradientTransform="scale(-.62 .62) rotate(-40.32 164.49 9941.65)"/><linearGradient xlink:href="#a" id="fG" x1="6336.44" x2="7034.3" y1="1501.23" y2="1501.23" gradientTransform="rotate(70.44 947.1 2185.56) scale(.33)"/><linearGradient xlink:href="#a" id="fH" x1="4771.06" x2="4974.57" y1="1500.29" y2="1500.29" gradientTransform="scale(.63 -.63) rotate(-13.98 -12908.73 -9122.72)"/><linearGradient xlink:href="#a" id="fI" x1="6085.89" x2="6415.72" y1="1501.98" y2="1501.98" gradientTransform="rotate(161.68 1865.57 1246.5) scale(.54)"/><linearGradient xlink:href="#a" id="fJ" x1="3603.69" x2="4139.79" y1="1500.31" y2="1500.31" gradientTransform="scale(-.55 .55) rotate(88.89 -1969.64 -1144.05)"/><linearGradient xlink:href="#a" id="fK" x1="4466.56" x2="5030.56" y1="1499.42" y2="1499.42" gradientTransform="rotate(-70.81 2427.3 565.6) scale(.63)"/><linearGradient xlink:href="#a" id="fL" x1="4005.65" x2="4339.36" y1="1499.98" y2="1499.98" gradientTransform="scale(.92 -.92) rotate(-6.85 -23859.16 -7384.17)"/><linearGradient xlink:href="#a" id="fM" x1="4753.79" x2="5675.58" y1="1499.08" y2="1499.08" gradientTransform="rotate(-51.23 2623.19 188.36) scale(.61)"/><linearGradient xlink:href="#a" id="fN" x1="4002.74" x2="4341.96" y1="1498.98" y2="1498.98" gradientTransform="rotate(173.8 2028.38 1250.44) scale(.63)"/><linearGradient xlink:href="#a" id="fO" x1="4256.93" x2="4795.56" y1="1500.3" y2="1500.3" gradientTransform="rotate(-111.58 2399.52 1281.78) scale(.86)"/><linearGradient xlink:href="#a" id="fP" x1="2634.45" x2="2741.88" y1="1500.26" y2="1500.26" gradientTransform="scale(.62 -.62) rotate(-43.78 -1939.32 -3182.74)"/><linearGradient xlink:href="#a" id="fQ" x1="3888.55" x2="4166.42" y1="1499.28" y2="1499.28" gradientTransform="rotate(-157.54 2186.94 1216) scale(.72)"/><linearGradient xlink:href="#a" id="fR" x1="4209.3" x2="4944.6" y1="1499.44" y2="1499.44" gradientTransform="rotate(-69.02 2443.27 576.32) scale(.64)"/><linearGradient xlink:href="#a" id="fS" x1="3690.72" x2="4209.65" y1="1499.78" y2="1499.78" gradientTransform="rotate(-30.77 2567.98 1258.7) scale(.95)"/><linearGradient xlink:href="#a" id="fT" x1="4454.03" x2="4948.73" y1="1499.8" y2="1499.8" gradientTransform="rotate(-47.61 2592.14 765.12) scale(.79)"/><linearGradient xlink:href="#a" id="fU" x1="5112.32" x2="5598.28" y1="1501" y2="1501" gradientTransform="scale(.56 -.56) rotate(-21.11 -8086.44 -7568.7)"/><linearGradient xlink:href="#a" id="fV" x1="5027.17" x2="5456.93" y1="1499.57" y2="1499.57" gradientTransform="rotate(-44.22 2743.63 -61.51) scale(.59)"/><linearGradient xlink:href="#a" id="fW" x1="2339.33" x2="2563.48" y1="1499.69" y2="1499.69" gradientTransform="scale(-.64 .64) rotate(44.84 -2056.22 -5045.87)"/><linearGradient xlink:href="#a" id="fX" x1="4264.31" x2="4455.02" y1="1498.73" y2="1498.73" gradientTransform="scale(.64 -.64) rotate(-71.85 223.34 -1847.52)"/><linearGradient xlink:href="#a" id="fY" x1="3827.79" x2="4302.84" y1="1500.15" y2="1500.15" gradientTransform="rotate(-150.37 2379.88 1376.7) scale(.89)"/><linearGradient xlink:href="#a" id="fZ" x1="3321.66" x2="3762.12" y1="1500.3" y2="1500.3" gradientTransform="scale(.86 -.86) rotate(-63.68 -238.4 -967.88)"/><linearGradient xlink:href="#a" id="ga" x1="4851.05" x2="5570.11" y1="1500.13" y2="1500.13" gradientTransform="scale(.73 -.73) rotate(-24.64 -5516.61 -3870)"/><linearGradient xlink:href="#a" id="gb" x1="4175.81" x2="4619.32" y1="1500.19" y2="1500.19" gradientTransform="rotate(-81.1 2408.28 957.63) scale(.75)"/><linearGradient xlink:href="#a" id="gc" x1="4193.81" x2="6202.77" y1="1499.78" y2="1499.78" gradientTransform="scale(-.6 .6) rotate(41.51 -2503.26 -5917.63)"/><linearGradient xlink:href="#a" id="gd" x1="3132.26" x2="3225.95" y1="1499.34" y2="1499.34" gradientTransform="rotate(-45.45 2649.04 475.26) scale(.73)"/><linearGradient xlink:href="#a" id="ge" x1="5520.73" x2="6125.31" y1="1501.4" y2="1501.4" gradientTransform="rotate(-22.08 3275.27 -638.1) scale(.7)"/><linearGradient xlink:href="#a" id="gf" x1="2596.59" x2="3114.58" y1="1500.56" y2="1500.56" gradientTransform="scale(-.88 .88) rotate(73.65 -627.38 -1527.57)"/><linearGradient xlink:href="#a" id="gg" x1="3411.79" x2="4153.42" y1="1500.62" y2="1500.62" gradientTransform="scale(-.94 .94) rotate(73.75 -477.62 -1451.96)"/><linearGradient xlink:href="#a" id="gh" x1="4540.27" x2="5009.19" y1="1501.27" y2="1501.27" gradientTransform="scale(-.6 .6) rotate(.6 -97574.1 -570695.49)"/><linearGradient xlink:href="#a" id="gi" x1="3714.44" x2="3934.67" y1="1499.34" y2="1499.34" gradientTransform="scale(-.72 .72) rotate(-9.27 2833.7 34509.07)"/><linearGradient xlink:href="#a" id="gj" x1="4313.02" x2="4447.6" y1="1499.3" y2="1499.3" gradientTransform="rotate(-121.23 2347.4 1230.95) scale(.82)"/><linearGradient xlink:href="#a" id="gk" x1="3246.21" x2="3887.98" y1="1500.36" y2="1500.36" gradientTransform="scale(-.94 .94) rotate(41.72 -546.57 -4352.63)"/><linearGradient xlink:href="#a" id="gl" x1="2858.23" x2="3076.3" y1="1500.37" y2="1500.37" gradientTransform="scale(.99 -.99) rotate(-84.12 513.32 -384.67)"/><linearGradient xlink:href="#a" id="gm" x1="5775.35" x2="5957.51" y1="1499.57" y2="1499.57" gradientTransform="rotate(-32.95 2772.42 447.38) scale(.79)"/><linearGradient xlink:href="#a" id="gn" x1="2792.11" x2="2883.38" y1="1499.81" y2="1499.81" gradientTransform="scale(-.83 .83) rotate(-11.96 875.21 24844.2)"/><linearGradient xlink:href="#a" id="go" x1="4258.08" x2="4532.79" y1="1499.63" y2="1499.63" gradientTransform="scale(.63 -.63) rotate(-51.78 -1094.65 -2616.74)"/><linearGradient xlink:href="#a" id="gp" x1="3123.62" x2="3486.13" y1="1500.71" y2="1500.71" gradientTransform="scale(-.78 .78) rotate(46.46 -1188.64 -4161.18)"/><linearGradient xlink:href="#a" id="gq" x1="4910.27" x2="5145.58" y1="1499.97" y2="1499.97" gradientTransform="rotate(-39.49 2540.34 1294.85) scale(.95)"/><linearGradient xlink:href="#a" id="gr" x1="2976.55" x2="3386.12" y1="1499.86" y2="1499.86" gradientTransform="scale(.95 -.95) rotate(-75.45 247.4 -547.85)"/><linearGradient xlink:href="#a" id="gs" x1="4910.77" x2="5523.69" y1="1500.56" y2="1500.56" gradientTransform="rotate(-43.38 2671.64 443.53) scale(.73)"/><linearGradient xlink:href="#a" id="gt" x1="3566.73" x2="3746.97" y1="1500.58" y2="1500.58" gradientTransform="scale(-.91 .91) rotate(47.88 -608.66 -3600.61)"/><linearGradient xlink:href="#a" id="gu" x1="2360.1" x2="2517.62" y1="1499.87" y2="1499.87" gradientTransform="scale(-.61 .61) rotate(26.41 -3198.9 -10660.43)"/><linearGradient xlink:href="#a" id="gv" x1="3016.43" x2="3193.79" y1="1499.61" y2="1499.61" gradientTransform="scale(.76 -.76) rotate(-81.66 556.34 -1083.15)"/><linearGradient xlink:href="#a" id="gw" x1="4004.86" x2="4495.3" y1="1500.14" y2="1500.14" gradientTransform="scale(-.76 .76) rotate(56.51 -1153.68 -3042.95)"/><linearGradient xlink:href="#a" id="gx" x1="5111.37" x2="5664.86" y1="1500.08" y2="1500.08" gradientTransform="rotate(30.45 1238.45 2711.2) scale(.68)"/><linearGradient xlink:href="#a" id="gy" x1="5619.36" x2="6367.22" y1="1499.03" y2="1499.03" gradientTransform="rotate(28.96 1227.4 2752.22) scale(.69)"/><linearGradient xlink:href="#a" id="gz" x1="4180.21" x2="4808.2" y1="1500.21" y2="1500.21" gradientTransform="rotate(-93.9 2392.66 1125.39) scale(.8)"/><linearGradient xlink:href="#a" id="gA" x1="6093.94" x2="7378.92" y1="1498.79" y2="1498.79" gradientTransform="scale(-.5 .5) rotate(24.64 -4991.96 -13434.61)"/><linearGradient xlink:href="#a" id="gB" x1="3718.08" x2="4051.83" y1="1499.07" y2="1499.07" gradientTransform="rotate(12.85 107.63 4627.28) scale(.7)"/><linearGradient xlink:href="#a" id="gC" x1="3573.06" x2="3667.05" y1="1500.59" y2="1500.59" gradientTransform="rotate(-9.32 5473.26 -4519.03) scale(.63)"/><linearGradient xlink:href="#a" id="gD" x1="3752.76" x2="4883.56" y1="1499.97" y2="1499.97" gradientTransform="scale(-.77 .77) rotate(89.7 -936.3 -833.35)"/><linearGradient xlink:href="#a" id="gE" x1="5932.33" x2="7252.78" y1="1499.66" y2="1499.66" gradientTransform="rotate(20.54 167.9 4160.64) scale(.57)"/><linearGradient xlink:href="#a" id="gF" x1="5109.76" x2="6108.44" y1="1500.02" y2="1500.02" gradientTransform="rotate(41.16 1463.82 2322.89) scale(.68)"/><linearGradient xlink:href="#a" id="gG" x1="3548.6" x2="4204.77" y1="1499.93" y2="1499.93" gradientTransform="scale(-.79 .79) rotate(4.1 -6341.22 -68123.7)"/><linearGradient xlink:href="#a" id="gH" x1="3915.07" x2="4448.24" y1="1500.52" y2="1500.52" gradientTransform="rotate(49.06 2400.73 1568.41) scale(.97)"/><linearGradient xlink:href="#a" id="gI" x1="3926.95" x2="4500.15" y1="1499.39" y2="1499.39" gradientTransform="rotate(-6.88 2857.26 824.56) scale(.97)"/><linearGradient xlink:href="#a" id="gJ" x1="3161.14" x2="3563.3" y1="1500.7" y2="1500.7" gradientTransform="scale(-.71 .71) rotate(-24.34 568.49 14267.4)"/><linearGradient xlink:href="#a" id="gK" x1="3523.42" x2="4149.86" y1="1499.76" y2="1499.76" gradientTransform="scale(-.51 .51) rotate(9.55 -10066.31 -38250.31)"/><linearGradient xlink:href="#a" id="gL" x1="4172.62" x2="5339.14" y1="1499.78" y2="1499.78" gradientTransform="rotate(22.77 1875.98 2182.44) scale(.87)"/><linearGradient xlink:href="#a" id="gM" x1="3169.21" x2="3959.44" y1="1500.06" y2="1500.06" gradientTransform="scale(-.91 .91) rotate(41.85 -647.16 -4428.27)"/><linearGradient xlink:href="#a" id="gN" x1="4076.27" x2="4499.96" y1="1499.5" y2="1499.5" gradientTransform="rotate(36.25 2029.87 1906.76) scale(.87)"/><linearGradient xlink:href="#a" id="gO" x1="3979.05" x2="4468.42" y1="1499.6" y2="1499.6" gradientTransform="rotate(36.8 2317.92 1656.35) scale(.95)"/><linearGradient xlink:href="#a" id="gP" x1="3702.83" x2="4282.92" y1="1500.63" y2="1500.63" gradientTransform="scale(-.66 .66) rotate(23.58 -2855.89 -11575.89)"/><linearGradient xlink:href="#a" id="gQ" x1="2773.85" x2="2902.76" y1="1501.07" y2="1501.07" gradientTransform="scale(-.52 .52) rotate(-36.75 606.44 12189.88)"/><linearGradient xlink:href="#a" id="gR" x1="2889.22" x2="3021.21" y1="1499.46" y2="1499.46" gradientTransform="scale(-.76 .76) rotate(-64.86 -348.46 5762.64)"/><linearGradient xlink:href="#a" id="gS" x1="3496.12" x2="4541.15" y1="1499.31" y2="1499.31" gradientTransform="rotate(42.7 975.66 2677.21) scale(.52)"/><linearGradient xlink:href="#a" id="gT" x1="5555.28" x2="6139.55" y1="1499.38" y2="1499.38" gradientTransform="rotate(35.54 1390.67 2473) scale(.69)"/><linearGradient xlink:href="#a" id="gU" x1="5252.01" x2="5737.67" y1="1502.21" y2="1502.21" gradientTransform="rotate(-12.94 4207.56 -2252.48) scale(.68)"/><linearGradient xlink:href="#a" id="gV" x1="4764.97" x2="5075.33" y1="1500.06" y2="1500.06" gradientTransform="rotate(-14.35 3187.48 -54.19) scale(.85)"/><linearGradient xlink:href="#a" id="gW" x1="3483.48" x2="3594.01" y1="1498.77" y2="1498.77" gradientTransform="scale(-.58 .58) rotate(-60.36 -299.6 7339.03)"/><linearGradient xlink:href="#a" id="gX" x1="5229.18" x2="6605.87" y1="1500.06" y2="1500.06" gradientTransform="rotate(-13.56 3373.32 -446.33) scale(.83)"/><linearGradient xlink:href="#a" id="gY" x1="3367.33" x2="3705.07" y1="1499.6" y2="1499.6" gradientTransform="scale(-.69 .69) rotate(-6.24 5305.58 51966.28)"/><linearGradient xlink:href="#a" id="gZ" x1="2654.18" x2="2797.43" y1="1500.54" y2="1500.54" gradientTransform="scale(-.52 .52) rotate(2.64 -31354.1 -142317.58)"/><linearGradient xlink:href="#a" id="ha" x1="4815.27" x2="6071.08" y1="1501.08" y2="1501.08" gradientTransform="rotate(62.83 1392.24 2079.6) scale(.55)"/><linearGradient xlink:href="#a" id="hb" x1="4263.47" x2="4508.33" y1="1499.16" y2="1499.16" gradientTransform="scale(-.98 .98) rotate(-27.94 -280.35 10326.52)"/><linearGradient xlink:href="#a" id="hc" x1="3512.11" x2="4084.17" y1="1499.98" y2="1499.98" gradientTransform="rotate(1.19 -15608.04 30988.47) scale(.75)"/><linearGradient xlink:href="#a" id="hd" x1="2718.91" x2="3357.96" y1="1499.62" y2="1499.62" gradientTransform="scale(-.59 .59) rotate(-52.05 -130.13 8250.74)"/><linearGradient xlink:href="#a" id="he" x1="3614.25" x2="3990.99" y1="1499.2" y2="1499.2" gradientTransform="scale(.64 -.64) rotate(83.64 5033.57 728.54)"/><linearGradient xlink:href="#a" id="hf" x1="3721.36" x2="4039.02" y1="1499.14" y2="1499.14" gradientTransform="rotate(107.83 2258.75 1521.42) scale(.87)"/><linearGradient xlink:href="#a" id="hg" x1="3410.06" x2="4076.63" y1="1500.01" y2="1500.01" gradientTransform="scale(-.72 .72) rotate(-70.38 -400.79 5563.17)"/><linearGradient xlink:href="#a" id="hh" x1="2678.65" x2="2923.7" y1="1500.52" y2="1500.52" gradientTransform="scale(-.61 .61) rotate(-83.1 -586.78 5340.85)"/><linearGradient xlink:href="#a" id="hi" x1="3403.97" x2="3618.53" y1="1501.46" y2="1501.46" gradientTransform="rotate(-153.45 1641.24 664.15) scale(.2)"/><linearGradient xlink:href="#a" id="hj" x1="2646.11" x2="2880.16" y1="1500.25" y2="1500.25" gradientTransform="scale(.34 -.34) rotate(-33.62 -5199.63 -10544.62)"/><linearGradient xlink:href="#a" id="hk" x1="4578.1" x2="4831.8" y1="1499.98" y2="1499.98" gradientTransform="rotate(-113.97 2283.67 1056.5) scale(.72)"/><linearGradient xlink:href="#a" id="hl" x1="3364.76" x2="4045.73" y1="1499.36" y2="1499.36" gradientTransform="scale(.59 -.59) rotate(-66.64 -32.99 -2353.04)"/><linearGradient xlink:href="#a" id="hm" x1="4173.9" x2="4689.22" y1="1496.48" y2="1496.48" gradientTransform="rotate(-71.88 2315.86 -620.14) scale(.14)"/><linearGradient xlink:href="#a" id="hn" x1="4024.91" x2="4515.53" y1="1500.89" y2="1500.89" gradientTransform="scale(.85 -.85) rotate(-21.6 -6160.52 -3018.55)"/><linearGradient xlink:href="#a" id="ho" x1="5552.11" x2="8107.97" y1="1499.55" y2="1499.55" gradientTransform="rotate(-157.03 2249.65 1270.94) scale(.77)"/><linearGradient xlink:href="#a" id="hp" x1="5731.84" x2="6566.06" y1="1500.08" y2="1500.08" gradientTransform="scale(-.4 .4) rotate(48.65 -4685.4 -6330.98)"/><linearGradient xlink:href="#a" id="hq" x1="4755.77" x2="5570.17" y1="1504.65" y2="1504.65" gradientTransform="rotate(-155.67 1527.78 589.5) scale(.11)"/><linearGradient xlink:href="#a" id="hr" x1="4543.22" x2="5781.38" y1="1498.35" y2="1498.35" gradientTransform="scale(-.34 .34) rotate(83.21 -4381.11 -2225.5)"/><linearGradient xlink:href="#a" id="hs" x1="4664.79" x2="5435.63" y1="1495.79" y2="1495.79" gradientTransform="rotate(174.76 1443.75 929.9) scale(.18)"/><linearGradient xlink:href="#a" id="ht" x1="3851.57" x2="4360.65" y1="1500.92" y2="1500.92" gradientTransform="scale(-.38 .38) rotate(44.54 -5348.55 -7561.95)"/><linearGradient xlink:href="#a" id="hu" x1="3469.24" x2="3635.42" y1="1499.26" y2="1499.26" gradientTransform="rotate(-148.25 2169.2 1147.25) scale(.68)"/><linearGradient xlink:href="#a" id="hv" x1="6822.11" x2="8349.07" y1="1494.58" y2="1494.58" gradientTransform="scale(.22 -.22) rotate(-64.95 65 -10091.87)"/><linearGradient xlink:href="#a" id="hw" x1="3626.31" x2="3780.37" y1="1499.93" y2="1499.93" gradientTransform="scale(.94 -.94) rotate(-54.95 -726.76 -823.15)"/><linearGradient xlink:href="#a" id="hx" x1="4706.31" x2="5031.89" y1="1501.19" y2="1501.19" gradientTransform="rotate(-159.82 1918.25 993.64) scale(.48)"/><linearGradient xlink:href="#a" id="hy" x1="6847.08" x2="7493.41" y1="1501.8" y2="1501.8" gradientTransform="scale(.34 -.34) rotate(-25.83 -8275.8 -13508.83)"/><linearGradient xlink:href="#a" id="hz" x1="4598.66" x2="5550.98" y1="1499.87" y2="1499.87" gradientTransform="rotate(-142.74 2105.79 1036.88) scale(.6)"/><linearGradient xlink:href="#a" id="hA" x1="3088.06" x2="3414.75" y1="1500.89" y2="1500.89" gradientTransform="scale(.46 -.46) rotate(-32.22 -4621.27 -7055.14)"/><linearGradient xlink:href="#a" id="hB" x1="2535.85" x2="2644.52" y1="1500.17" y2="1500.17" gradientTransform="scale(-.19 .19) rotate(53.49 -12006.05 -10186.23)"/><linearGradient xlink:href="#a" id="hC" x1="7863.76" x2="10959.17" y1="1499.28" y2="1499.28" gradientTransform="rotate(-150.83 1903.25 891.7) scale(.43)"/><linearGradient xlink:href="#a" id="hD" x1="2773.12" x2="3209.08" y1="1499.63" y2="1499.63" gradientTransform="scale(.95 -.95) rotate(-72.52 138.63 -567.94)"/><linearGradient xlink:href="#a" id="hE" x1="3137.88" x2="3261.22" y1="1500.05" y2="1500.05" gradientTransform="rotate(-51.8 2641.6 -99.98) scale(.52)"/><linearGradient xlink:href="#a" id="hF" x1="2713.01" x2="3278.51" y1="1498.99" y2="1498.99" gradientTransform="scale(-.44 .44) rotate(75.1 -3163.35 -2440.12)"/><linearGradient xlink:href="#a" id="hG" x1="3341.83" x2="3852.63" y1="1498.56" y2="1498.56" gradientTransform="rotate(-147.4 1969.3 925.21) scale(.49)"/><linearGradient xlink:href="#a" id="hH" x1="3345.77" x2="3695.05" y1="1501.34" y2="1501.34" gradientTransform="scale(-.52 .52) rotate(75.61 -2356.86 -2097.44)"/><linearGradient xlink:href="#a" id="hI" x1="4040.24" x2="4281.74" y1="1500.27" y2="1500.27" gradientTransform="rotate(-42.67 2772.16 -103.58) scale(.59)"/><linearGradient xlink:href="#a" id="hJ" x1="3554.01" x2="4566.1" y1="1500.83" y2="1500.83" gradientTransform="scale(.6 -.6) rotate(-34.32 -3465.67 -4277.03)"/><linearGradient xlink:href="#a" id="hK" x1="4848.57" x2="5459.94" y1="1499.76" y2="1499.76" gradientTransform="rotate(-73.19 2327.42 -251.5) scale(.28)"/><linearGradient xlink:href="#a" id="hL" x1="3596.84" x2="5942.65" y1="1499.24" y2="1499.24" gradientTransform="scale(.83 -.83) rotate(-58.69 -513.86 -1187.15)"/><linearGradient xlink:href="#a" id="hM" x1="3944.2" x2="4441.35" y1="1499.09" y2="1499.09" gradientTransform="rotate(-162.15 1848.72 955.55) scale(.42)"/><linearGradient xlink:href="#a" id="hN" x1="4290.86" x2="4649.67" y1="1499.77" y2="1499.77" gradientTransform="scale(.89 -.89) rotate(-37.05 -2430.45 -1522.28)"/><linearGradient xlink:href="#a" id="hO" x1="5334.82" x2="6002.83" y1="1500.89" y2="1500.89" gradientTransform="rotate(-24.64 3643.88 -1890.07) scale(.48)"/><linearGradient xlink:href="#a" id="hP" x1="3932.59" x2="4817.67" y1="1500" y2="1500" gradientTransform="rotate(-11.67 3382.29 -379.8) scale(.86)"/><linearGradient xlink:href="#a" id="hQ" x1="3323.48" x2="3598.74" y1="1499.72" y2="1499.72" gradientTransform="scale(-.2 .2) rotate(13.43 -30647.64 -56560.66)"/><linearGradient xlink:href="#a" id="hR" x1="3922.41" x2="4091.84" y1="1501.34" y2="1501.34" gradientTransform="rotate(-45.35 2801.17 -568.38) scale(.45)"/><linearGradient xlink:href="#a" id="hS" x1="4683.88" x2="5147.3" y1="1500.31" y2="1500.31" gradientTransform="scale(-.85 .85) rotate(21.56 -1232.71 -10940.88)"/><linearGradient xlink:href="#a" id="hT" x1="5436.1" x2="6764.33" y1="1499.47" y2="1499.47" gradientTransform="rotate(-140.97 1981.01 870.82) scale(.47)"/><linearGradient xlink:href="#a" id="hU" x1="4147.23" x2="4751.36" y1="1500.42" y2="1500.42" gradientTransform="scale(.23 -.23) rotate(-86.82 2114.5 -7304.44)"/><linearGradient xlink:href="#a" id="hV" x1="4982.08" x2="5738.99" y1="1498.97" y2="1498.97" gradientTransform="rotate(-98.7 2246.87 737.6) scale(.58)"/><linearGradient xlink:href="#a" id="hW" x1="5119.86" x2="6256.66" y1="1501.58" y2="1501.58" gradientTransform="scale(-.45 .45) rotate(79.84 -2956.92 -2002.95)"/><linearGradient xlink:href="#a" id="hX" x1="5500.61" x2="6781.26" y1="1500.71" y2="1500.71" gradientTransform="rotate(-44.96 2677.98 302.75) scale(.68)"/><linearGradient xlink:href="#a" id="hY" x1="3530.91" x2="3981.28" y1="1500.34" y2="1500.34" gradientTransform="scale(.99 -.99) rotate(-55.64 -671.19 -662.42)"/><linearGradient xlink:href="#a" id="hZ" x1="3931.78" x2="4975.55" y1="1501.54" y2="1501.54" gradientTransform="scale(-.39 .39) rotate(88.01 -3517.17 -1596.41)"/><linearGradient xlink:href="#a" id="ia" x1="4204.28" x2="4790.66" y1="1499.93" y2="1499.93" gradientTransform="rotate(-76.77 2342.8 291.39) scale(.48)"/><linearGradient xlink:href="#a" id="ib" x1="4476.82" x2="4840.54" y1="1500.79" y2="1500.79" gradientTransform="rotate(-30.32 3228.5 -1075.58) scale(.52)"/><linearGradient xlink:href="#a" id="ic" x1="4063.72" x2="4698.32" y1="1500.66" y2="1500.66" gradientTransform="scale(-.59 .59) rotate(63.54 -2031.78 -2890.16)"/><linearGradient xlink:href="#a" id="id" x1="3551" x2="3664.09" y1="1500.12" y2="1500.12" gradientTransform="rotate(-124.96 2461.32 1437.2) scale(.96)"/><linearGradient xlink:href="#a" id="ie" x1="4077.15" x2="4875.06" y1="1500.16" y2="1500.16" gradientTransform="rotate(-49.76 2696.49 -344.57) scale(.46)"/><linearGradient xlink:href="#a" id="if" x1="2585.8" x2="2878.69" y1="1499.81" y2="1499.81" gradientTransform="scale(-.63 .63) rotate(19.81 -3613.87 -14674.77)"/><linearGradient xlink:href="#a" id="ig" x1="4820.03" x2="5074.45" y1="1498.8" y2="1498.8" gradientTransform="rotate(5.01 -7396.24 16463.57) scale(.46)"/><linearGradient xlink:href="#a" id="ih" x1="3761.71" x2="4970.46" y1="1499.83" y2="1499.83" gradientTransform="rotate(-29.41 2881.5 191.17) scale(.76)"/><linearGradient xlink:href="#a" id="ii" x1="2931.67" x2="3180.01" y1="1500.34" y2="1500.34" gradientTransform="scale(-.78 .78) rotate(7.84 -3763.35 -35126.75)"/><linearGradient xlink:href="#a" id="ij" x1="5381.32" x2="7224.75" y1="1498.39" y2="1498.39" gradientTransform="rotate(.05 -956417.82 1597033.17) scale(.44)"/><linearGradient xlink:href="#a" id="ik" x1="4718.82" x2="4954.23" y1="1498.6" y2="1498.6" gradientTransform="scale(-.28 .28) rotate(-4.01 51206.64 157736.59)"/><linearGradient xlink:href="#a" id="il" x1="4969.66" x2="5873.33" y1="1499.95" y2="1499.95" gradientTransform="rotate(4.03 -1216.88 7230.9) scale(.84)"/><linearGradient xlink:href="#a" id="im" x1="3851.36" x2="4321.02" y1="1500.73" y2="1500.73" gradientTransform="rotate(4.84 -550.2 6138.32) scale(.84)"/><linearGradient xlink:href="#a" id="in" x1="3235.9" x2="3366.79" y1="1500" y2="1500" gradientTransform="scale(-.78 .78) rotate(3.39 -7879.6 -83518.18)"/><linearGradient xlink:href="#a" id="io" x1="4705.76" x2="5324.97" y1="1499.45" y2="1499.45" gradientTransform="rotate(-83.49 2246.72 168.9) scale(.38)"/><linearGradient xlink:href="#a" id="ip" x1="3319.18" x2="3762.94" y1="1501.55" y2="1501.55" gradientTransform="scale(.16 -.16) rotate(-67.69 621.52 -14166.56)"/><linearGradient xlink:href="#a" id="iq" x1="5275.1" x2="6133.88" y1="1500.77" y2="1500.77" gradientTransform="rotate(-16.2 4860.8 -4098.07) scale(.41)"/><linearGradient xlink:href="#a" id="ir" x1="4116.98" x2="4407.35" y1="1499.57" y2="1499.57" gradientTransform="rotate(-112.1 2408.44 1304.2) scale(.88)"/><linearGradient xlink:href="#a" id="is" x1="4542.1" x2="5122.76" y1="1500.27" y2="1500.27" gradientTransform="scale(-.77 .77) rotate(-11.82 1466.1 26306.58)"/><linearGradient xlink:href="#a" id="it" x1="4066.11" x2="4672.15" y1="1499.99" y2="1499.99" gradientTransform="scale(-.41 .41) rotate(84.22 -3326.19 -1812.69)"/><linearGradient xlink:href="#a" id="iu" x1="5044.01" x2="5558.35" y1="1498" y2="1498" gradientTransform="scale(-.22 .22) rotate(6.51 -51606.96 -111857.81)"/><linearGradient xlink:href="#a" id="iv" x1="4585.17" x2="5185.96" y1="1498.6" y2="1498.6" gradientTransform="rotate(-77.03 2253.2 -361.86) scale(.2)"/><linearGradient xlink:href="#a" id="iw" x1="2840.75" x2="2993.56" y1="1499.51" y2="1499.51" gradientTransform="scale(-.57 .57) rotate(12.38 -6491.82 -26646.96)"/><linearGradient xlink:href="#a" id="ix" x1="6059.71" x2="7737.66" y1="1505.75" y2="1505.75" gradientTransform="matrix(-.10998 .0021 .0021 .10998 2699.95 1330.45)"/><linearGradient xlink:href="#a" id="iy" x1="4256.53" x2="4739.64" y1="1498.81" y2="1498.81" gradientTransform="rotate(30.2 2451.56 1544.85) scale(.99)"/><linearGradient xlink:href="#a" id="iz" x1="5177.52" x2="6202.11" y1="1499.48" y2="1499.48" gradientTransform="scale(-.47 .47) rotate(15.74 -7836.23 -23587.15)"/><linearGradient xlink:href="#a" id="iA" x1="3596.27" x2="3752.33" y1="1501.01" y2="1501.01" gradientTransform="rotate(-14.48 4872.33 -3899.79) scale(.49)"/><linearGradient xlink:href="#a" id="iB" x1="5143.28" x2="5776.96" y1="1500.3" y2="1500.3" gradientTransform="scale(-.66 .66) rotate(-.57 76630.34 567943.16)"/><linearGradient xlink:href="#a" id="iC" x1="8406.62" x2="9257.7" y1="1500.89" y2="1500.89" gradientTransform="rotate(10.64 -3162.64 9218.04) scale(.39)"/><linearGradient xlink:href="#a" id="iD" x1="3405.66" x2="3619.89" y1="1500.36" y2="1500.36" gradientTransform="scale(-.35 .35) rotate(85.76 -4168.26 -1942.4)"/><linearGradient xlink:href="#a" id="iE" x1="3255.06" x2="3683.68" y1="1499.36" y2="1499.36" gradientTransform="rotate(-84.78 2315.8 588.61) scale(.57)"/><linearGradient xlink:href="#a" id="iF" x1="3834.44" x2="4632.84" y1="1500.36" y2="1500.36" gradientTransform="scale(-.21 .21) rotate(77.86 -8550.4 -4182.83)"/><linearGradient xlink:href="#a" id="iG" x1="4923.69" x2="5331.06" y1="1498.76" y2="1498.76" gradientTransform="scale(-.54 .54) rotate(-4.02 16987.7 94155.48)"/><linearGradient xlink:href="#a" id="iH" x1="4292.68" x2="4603.96" y1="1499.62" y2="1499.62" gradientTransform="rotate(-30.11 2991.8 -224.83) scale(.68)"/><linearGradient xlink:href="#a" id="iI" x1="2908.78" x2="3174.86" y1="1500.22" y2="1500.22" gradientTransform="scale(-.68 .68) rotate(48.19 -1719.18 -4311.73)"/><linearGradient xlink:href="#a" id="iJ" x1="2986.15" x2="3277.99" y1="1499.81" y2="1499.81" gradientTransform="scale(-.49 .49) rotate(-45.98 212.19 10460.23)"/><linearGradient xlink:href="#a" id="iK" x1="3446.07" x2="3730.66" y1="1500.56" y2="1500.56" gradientTransform="rotate(-39.92 2993.4 -1038.12) scale(.39)"/><linearGradient xlink:href="#a" id="iL" x1="5444.81" x2="5799.83" y1="1503.07" y2="1503.07" gradientTransform="scale(-.11 .11) rotate(25.42 -37262.74 -46960.02)"/><linearGradient xlink:href="#a" id="iM" x1="3528.67" x2="3843.08" y1="1499.35" y2="1499.35" gradientTransform="rotate(-12.47 5952.77 -5996.48) scale(.39)"/><linearGradient xlink:href="#a" id="iN" x1="5516.98" x2="6930.77" y1="1504.16" y2="1504.16" gradientTransform="scale(-.12 .12) rotate(35.14 -26760.62 -28873.66)"/><linearGradient xlink:href="#a" id="iO" x1="4752.32" x2="5078.98" y1="1499.25" y2="1499.25" gradientTransform="rotate(34.31 3.92 3737.98) scale(.32)"/><linearGradient xlink:href="#a" id="iP" x1="3465.5" x2="3809.43" y1="1497.23" y2="1497.23" gradientTransform="scale(-.17 .17) rotate(76.83 -11080.43 -5268.18)"/><linearGradient xlink:href="#a" id="iQ" x1="3644.98" x2="4175.43" y1="1502.4" y2="1502.4" gradientTransform="rotate(-26.89 4195.24 -3872.89) scale(.1)"/><linearGradient xlink:href="#a" id="iR" x1="3410.38" x2="4162.27" y1="1500.82" y2="1500.82" gradientTransform="scale(-.85 .85) rotate(46.77 -861.34 -3895.6)"/><linearGradient xlink:href="#a" id="iS" x1="3692.31" x2="4914.58" y1="1499.48" y2="1499.48" gradientTransform="rotate(50.34 2244.62 1670.74) scale(.91)"/><linearGradient xlink:href="#a" id="iT" x1="3116.07" x2="3876.67" y1="1499.9" y2="1499.9" gradientTransform="scale(-.66 .66) rotate(2.06 -22564.41 -154586.33)"/><linearGradient xlink:href="#a" id="iU" x1="3095.29" x2="3298.96" y1="1499.72" y2="1499.72" gradientTransform="rotate(45.83 941.4 2636.92) scale(.48)"/><linearGradient xlink:href="#a" id="iV" x1="5071.78" x2="5857.09" y1="1501.94" y2="1501.94" gradientTransform="scale(-.85 .85) rotate(-25.57 41.1 12157.48)"/><linearGradient xlink:href="#a" id="iW" x1="3726.07" x2="4477.18" y1="1499.54" y2="1499.54" gradientTransform="rotate(15.22 1880.9 2273.9) scale(.91)"/><linearGradient xlink:href="#a" id="iX" x1="3077.91" x2="3884.51" y1="1499.53" y2="1499.53" gradientTransform="scale(-.98 .98) rotate(-5.05 -47 51403.4)"/><linearGradient xlink:href="#a" id="iY" x1="4878.7" x2="6378.11" y1="1498.69" y2="1498.69" gradientTransform="rotate(-52.74 2699.16 -972.36) scale(.24)"/><linearGradient xlink:href="#a" id="iZ" x1="3600.03" x2="4143.2" y1="1500.34" y2="1500.34" gradientTransform="scale(-.83 .83) rotate(-41.06 -175.6 8140.8)"/><linearGradient xlink:href="#a" id="ja" x1="7419.01" x2="9172.52" y1="1498.92" y2="1498.92" gradientTransform="scale(-.3 .3) rotate(-63.99 -441.91 11372.59)"/><linearGradient xlink:href="#a" id="jb" x1="4134.51" x2="4386.14" y1="1499.66" y2="1499.66" gradientTransform="rotate(57.29 1591.38 2032.29) scale(.65)"/><linearGradient xlink:href="#a" id="jc" x1="2843.6" x2="3066.48" y1="1501.27" y2="1501.27" gradientTransform="scale(-.45 .45) rotate(-45.27 350.44 11259.72)"/><linearGradient xlink:href="#a" id="jd" x1="6992.1" x2="7246.26" y1="1500.86" y2="1500.86" gradientTransform="rotate(37.63 -374.35 3931) scale(.17)"/><linearGradient xlink:href="#a" id="je" x1="3871.1" x2="4288.4" y1="1499.52" y2="1499.52" gradientTransform="scale(-.96 .96) rotate(16.66 -598.28 -13653.78)"/><linearGradient xlink:href="#a" id="jf" x1="3902.84" x2="4165.78" y1="1500.15" y2="1500.15" gradientTransform="rotate(80.98 2332.04 1556.31) scale(.92)"/><linearGradient xlink:href="#a" id="jg" x1="5223.72" x2="7023.72" y1="1501.81" y2="1501.81" gradientTransform="scale(-.64 .64) rotate(-30.45 506.69 12508.87)"/><linearGradient xlink:href="#a" id="jh" x1="3390.73" x2="3654.04" y1="1498.62" y2="1498.62" gradientTransform="rotate(-21.44 3912.18 -2322.98) scale(.48)"/><linearGradient xlink:href="#a" id="ji" x1="3077.23" x2="3885.7" y1="1500.16" y2="1500.16" gradientTransform="scale(-.83 .83) rotate(-5.67 2442.4 50779.25)"/><linearGradient xlink:href="#a" id="jj" x1="3228.21" x2="3362.75" y1="1498.7" y2="1498.7" gradientTransform="rotate(4.2 -10073.7 20800.4) scale(.42)"/><linearGradient xlink:href="#a" id="jk" x1="2776.54" x2="3116.81" y1="1500.64" y2="1500.64" gradientTransform="scale(-.32 .32) rotate(-14.63 9375.85 40796.1)"/><linearGradient xlink:href="#a" id="jl" x1="4168.92" x2="4952.97" y1="1499.66" y2="1499.66" gradientTransform="rotate(46.4 936.93 2628.8) scale(.48)"/><linearGradient xlink:href="#a" id="jm" x1="4004.8" x2="4505.73" y1="1498.1" y2="1498.1" gradientTransform="rotate(18.05 48.26 4422.2) scale(.59)"/><linearGradient xlink:href="#a" id="jn" x1="5355.42" x2="5842.01" y1="1500.76" y2="1500.76" gradientTransform="rotate(1.5 -22991.47 42791.41) scale(.56)"/><linearGradient xlink:href="#a" id="jo" x1="3188.93" x2="3807.04" y1="1499.92" y2="1499.92" gradientTransform="scale(-.57 .57) rotate(1.55 -43054.97 -228257.67)"/><linearGradient xlink:href="#a" id="jp" x1="4071.37" x2="4415.55" y1="1498.97" y2="1498.97" gradientTransform="rotate(12.08 -4320.7 10548.28) scale(.18)"/><linearGradient xlink:href="#a" id="jq" x1="3369.32" x2="3773.19" y1="1499.87" y2="1499.87" gradientTransform="scale(-.49 .49) rotate(-37.1 701.26 12618.26)"/><linearGradient xlink:href="#a" id="jr" x1="3605.49" x2="4013.51" y1="1499.96" y2="1499.96" gradientTransform="rotate(87.46 938.54 1927.2) scale(.23)"/><linearGradient xlink:href="#a" id="js" x1="5761.54" x2="6218.55" y1="1498.87" y2="1498.87" gradientTransform="scale(-.5 .5) rotate(11.47 -8996.08 -31867.87)"/><linearGradient xlink:href="#a" id="jt" x1="4060.31" x2="4308.64" y1="1500.02" y2="1500.02" gradientTransform="rotate(-13.89 2538.88 1417.53)"/><linearGradient xlink:href="#a" id="ju" x1="7687.88" x2="8459.64" y1="1501.41" y2="1501.41" gradientTransform="scale(-.28 .28) rotate(-14.88 11168.52 44766.98)"/><linearGradient xlink:href="#a" id="jv" x1="3424.42" x2="3749.1" y1="1499.93" y2="1499.93" gradientTransform="scale(.94 -.94) rotate(88.49 3833.43 371.95)"/><linearGradient xlink:href="#a" id="jw" x1="4771.92" x2="5976.73" y1="1499.19" y2="1499.19" gradientTransform="rotate(30.97 1061.21 2868.54) scale(.64)"/><linearGradient xlink:href="#a" id="jx" x1="4657.32" x2="4917.45" y1="1500.6" y2="1500.6" gradientTransform="scale(-.79 .79) rotate(-10.16 1606.28 29831.12)"/><linearGradient xlink:href="#a" id="jy" x1="4553.8" x2="5260.2" y1="1500.69" y2="1500.69" gradientTransform="rotate(-10.99 6088.45 -6033.8) scale(.45)"/><linearGradient xlink:href="#a" id="jz" x1="4019.01" x2="4589.33" y1="1499.73" y2="1499.73" gradientTransform="scale(-.31 .31) rotate(3.42 -58616.44 -163276.88)"/><linearGradient xlink:href="#a" id="jA" x1="5815.58" x2="6702.72" y1="1501.44" y2="1501.44" gradientTransform="rotate(27.9 -508.11 4518.8) scale(.3)"/><linearGradient xlink:href="#a" id="jB" x1="5093.11" x2="5894.65" y1="1499.54" y2="1499.54" gradientTransform="scale(-.34 .34) rotate(-62.74 -365.45 10488.43)"/><linearGradient xlink:href="#a" id="jC" x1="4112.12" x2="5469.88" y1="1499.79" y2="1499.79" gradientTransform="scale(.18 -.18) rotate(-1.5 -368607.53 -462316.49)"/><linearGradient xlink:href="#a" id="jD" x1="5077.78" x2="5693.72" y1="1500.09" y2="1500.09" gradientTransform="rotate(-108.22 1935.84 179.82) scale(.2)"/><linearGradient xlink:href="#a" id="jE" x1="2820.76" x2="3427.61" y1="1499.43" y2="1499.43" gradientTransform="matrix(.54 .00028 .00028 -.54 1504.96 2308.3)"/><linearGradient xlink:href="#a" id="jF" x1="5412.24" x2="5873.1" y1="1499.42" y2="1499.42" gradientTransform="rotate(-153.71 1975.54 991.54) scale(.51)"/><linearGradient xlink:href="#a" id="jG" x1="2749.7" x2="3058.21" y1="1498.8" y2="1498.8" gradientTransform="scale(.68 -.68) rotate(-21.21 -7130.03 -5253.98)"/><linearGradient xlink:href="#a" id="jH" x1="3823.93" x2="4613.6" y1="1500.5" y2="1500.5" gradientTransform="rotate(-69.57 2401.89 33.1) scale(.42)"/><linearGradient xlink:href="#a" id="jI" x1="4587.11" x2="5810.42" y1="1499.73" y2="1499.73" gradientTransform="scale(.23 -.23) rotate(-52.25 -1833.63 -11667.1)"/><linearGradient xlink:href="#a" id="jJ" x1="4474.99" x2="5274.14" y1="1497.94" y2="1497.94" gradientTransform="rotate(-151.22 1667.27 656.87) scale(.21)"/><linearGradient xlink:href="#a" id="jK" x1="4542.24" x2="4971.11" y1="1497.41" y2="1497.41" gradientTransform="scale(.23 -.23) rotate(2.21 214344.6 231353)"/><linearGradient xlink:href="#a" id="jL" x1="3173.87" x2="3997.61" y1="1496.01" y2="1496.01" gradientTransform="rotate(-113.61 1846.38 149.65) scale(.14)"/><linearGradient xlink:href="#a" id="jM" x1="3860.79" x2="4485.29" y1="1499.79" y2="1499.79" gradientTransform="scale(.78 -.78) rotate(-30.44 -3773.49 -2698.59)"/><linearGradient xlink:href="#a" id="jN" x1="3116.28" x2="3592.88" y1="1500.64" y2="1500.64" gradientTransform="scale(.7 -.7) rotate(-50.63 -1143.46 -2148.13)"/><linearGradient xlink:href="#a" id="jO" x1="3790.15" x2="4141.42" y1="1500.59" y2="1500.59" gradientTransform="rotate(-171.38 1588.48 855.26) scale(.24)"/><linearGradient xlink:href="#a" id="jP" x1="5512.77" x2="6608.59" y1="1496.98" y2="1496.98" gradientTransform="rotate(-155.73 2283.54 1297.27) scale(.8)"/><linearGradient xlink:href="#a" id="jQ" x1="2798.44" x2="3008.35" y1="1499.27" y2="1499.27" gradientTransform="scale(-.45 .45) rotate(69.6 -3171.3 -2908.19)"/><linearGradient xlink:href="#a" id="jR" x1="3901.11" x2="4630.7" y1="1499.78" y2="1499.78" gradientTransform="rotate(-63.16 2491.96 743.83) scale(.73)"/><linearGradient xlink:href="#a" id="jS" x1="3674.4" x2="4261.32" y1="1499.13" y2="1499.13" gradientTransform="scale(-.64 .64) rotate(32.58 -2472.78 -7917.2)"/><linearGradient xlink:href="#a" id="jT" x1="2873.09" x2="3102.05" y1="1500.2" y2="1500.2" gradientTransform="rotate(-119.99 2386.76 1296.17) scale(.86)"/><linearGradient xlink:href="#a" id="jU" x1="2810.19" x2="3292.53" y1="1500.76" y2="1500.76" gradientTransform="scale(-.65 .65) rotate(50.45 -1844.36 -4140.68)"/><linearGradient xlink:href="#a" id="jV" x1="3378.67" x2="3668.21" y1="1499.79" y2="1499.79" gradientTransform="scale(-.51 .51) rotate(72.35 -2513.2 -2392.22)"/><linearGradient xlink:href="#a" id="jW" x1="3350.76" x2="3606.92" y1="1505.53" y2="1505.53" gradientTransform="rotate(-30.75 3836.25 -3291.6) scale(.1)"/><linearGradient xlink:href="#a" id="jX" x1="3140.64" x2="3242.18" y1="1500.78" y2="1500.78" gradientTransform="scale(-.41 .41) rotate(22.63 -7520.9 -17262.77)"/><linearGradient xlink:href="#a" id="jY" x1="4858.71" x2="5726.48" y1="1499.82" y2="1499.82" gradientTransform="rotate(-33.82 2623.47 1007.82) scale(.9)"/><linearGradient xlink:href="#a" id="jZ" x1="2865.36" x2="3015.52" y1="1500.29" y2="1500.29" gradientTransform="scale(.64 -.64) rotate(-85.01 777.4 -1548.07)"/><linearGradient xlink:href="#a" id="ka" x1="3553.73" x2="3841.74" y1="1499.17" y2="1499.17" gradientTransform="rotate(-139.19 2201.66 1126.23) scale(.69)"/><linearGradient xlink:href="#a" id="kb" x1="3249.24" x2="3467.43" y1="1499.76" y2="1499.76" gradientTransform="scale(.95 -.95) rotate(-25.36 -4591.64 -1815.17)"/><linearGradient xlink:href="#a" id="kc" x1="4186.99" x2="4621.64" y1="1501.01" y2="1501.01" gradientTransform="rotate(-25.32 2637.83 1079.27) scale(.93)"/><linearGradient xlink:href="#a" id="kd" x1="3308.51" x2="3614.87" y1="1498.91" y2="1498.91" gradientTransform="scale(.44 -.44) rotate(-61.89 -330.7 -4151.46)"/><linearGradient xlink:href="#a" id="ke" x1="7897.57" x2="9403.23" y1="1491.25" y2="1491.25" gradientTransform="scale(-.09 .09) rotate(40.83 -33270.55 -30760.63)"/><linearGradient xlink:href="#a" id="kf" x1="3589.15" x2="3818.25" y1="1500.08" y2="1500.08" gradientTransform="rotate(-3.19 8595.28 -9334.36) scale(.76)"/><linearGradient xlink:href="#a" id="kg" x1="3408.49" x2="3959.77" y1="1499.05" y2="1499.05" gradientTransform="scale(.22 -.22) rotate(-60.44 -530.44 -10860.6)"/><linearGradient xlink:href="#a" id="kh" x1="3800.03" x2="4579.05" y1="1501.99" y2="1501.99" gradientTransform="rotate(-129.01 1749.35 367.64) scale(.16)"/><linearGradient xlink:href="#a" id="ki" x1="6150.33" x2="7521.69" y1="1499.38" y2="1499.38" gradientTransform="matrix(-.46 -.00008 -.00008 .46 3354.76 805.04)"/><linearGradient xlink:href="#a" id="kj" x1="4211.25" x2="4813.92" y1="1500.5" y2="1500.5" gradientTransform="rotate(-136.74 1973.48 811.48) scale(.45)"/><linearGradient xlink:href="#a" id="kk" x1="6171.36" x2="7553.32" y1="1501.33" y2="1501.33" gradientTransform="scale(.28 -.28) rotate(-80.64 1345 -6117.52)"/><linearGradient xlink:href="#a" id="kl" x1="3707.84" x2="4214.64" y1="1499.73" y2="1499.73" gradientTransform="rotate(-91.79 2430.12 1237.86) scale(.87)"/><linearGradient xlink:href="#a" id="km" x1="5019.08" x2="6495.64" y1="1499.88" y2="1499.88" gradientTransform="scale(-.65 .65) rotate(-.67 67355.5 489383.33)"/><linearGradient xlink:href="#a" id="kn" x1="3102.86" x2="3463.46" y1="1499.85" y2="1499.85" gradientTransform="scale(.85 -.85) rotate(-41.59 -1901.66 -1588.77)"/><linearGradient xlink:href="#a" id="ko" x1="5019.36" x2="5926.77" y1="1500.11" y2="1500.11" gradientTransform="rotate(-127.15 1818.7 435) scale(.22)"/><linearGradient xlink:href="#a" id="kp" x1="3791.71" x2="4694.13" y1="1500.67" y2="1500.67" gradientTransform="scale(-.35 .35) rotate(58.94 -5105.62 -5061.05)"/><linearGradient xlink:href="#a" id="kq" x1="4054.83" x2="4432.54" y1="1500.2" y2="1500.2" gradientTransform="rotate(-25.59 4341 -4110.25) scale(.1)"/><linearGradient xlink:href="#a" id="kr" x1="3325.02" x2="4125.32" y1="1499.06" y2="1499.06" gradientTransform="scale(-.77 .77) rotate(38.03 -1358.6 -5657.56)"/><linearGradient xlink:href="#a" id="ks" x1="5873.95" x2="6713.66" y1="1499.78" y2="1499.78" gradientTransform="rotate(-126.95 1933.1 610.75) scale(.35)"/><linearGradient xlink:href="#a" id="kt" x1="3285.99" x2="3862.11" y1="1499.66" y2="1499.66" gradientTransform="scale(.77 -.77) rotate(-72.73 203.85 -1189.5)"/><linearGradient xlink:href="#a" id="ku" x1="4575.39" x2="5824.15" y1="1498.19" y2="1498.19" gradientTransform="rotate(-101.39 2051.57 248.91) scale(.29)"/><linearGradient xlink:href="#a" id="kv" x1="2716.3" x2="2835.68" y1="1499.97" y2="1499.97" gradientTransform="scale(-.61 .61) rotate(-9.02 5023.69 39547.9)"/><linearGradient xlink:href="#a" id="kw" x1="5465.64" x2="5906.89" y1="1498.25" y2="1498.25" gradientTransform="rotate(-65.71 2448.39 -75.25) scale(.41)"/><linearGradient xlink:href="#a" id="kx" x1="4486.43" x2="4878.54" y1="1500.44" y2="1500.44" gradientTransform="rotate(-93.83 2164.12 325.46) scale(.39)"/><linearGradient xlink:href="#a" id="ky" x1="2989.38" x2="3185.73" y1="1499.17" y2="1499.17" gradientTransform="scale(-.63 .63) rotate(33.53 -2518.43 -7710.6)"/><linearGradient xlink:href="#a" id="kz" x1="3648.97" x2="4315.24" y1="1500.35" y2="1500.35" gradientTransform="rotate(-67 2475.91 959.92) scale(.8)"/><linearGradient xlink:href="#a" id="kA" x1="4883.53" x2="6974.63" y1="1502.24" y2="1502.24" gradientTransform="scale(-.09 .09) rotate(75.52 -22663.28 -10059.6)"/><linearGradient xlink:href="#a" id="kB" x1="4232.19" x2="5224.37" y1="1498.77" y2="1498.77" gradientTransform="rotate(19.24 2391.4 1623.52) scale(.98)"/><linearGradient xlink:href="#a" id="kC" x1="3965.45" x2="5829.82" y1="1501.03" y2="1501.03" gradientTransform="rotate(-.74 81715.64 -132534.6) scale(.31)"/><linearGradient xlink:href="#a" id="kD" x1="7047.47" x2="9124.84" y1="1502.78" y2="1502.78" gradientTransform="scale(-.28 .28) rotate(78.65 -5885.8 -3147.26)"/><linearGradient xlink:href="#a" id="kE" x1="2357.37" x2="2479.68" y1="1500.78" y2="1500.78" gradientTransform="scale(-.61 .61) rotate(81.86 -1671.1 -1447.74)"/><linearGradient xlink:href="#a" id="kF" x1="4522.81" x2="4994.8" y1="1499.38" y2="1499.38" gradientTransform="rotate(-111.99 2223.48 907.9) scale(.63)"/><linearGradient xlink:href="#a" id="kG" x1="3816.85" x2="4104.6" y1="1501.78" y2="1501.78" gradientTransform="scale(-.19 .19) rotate(-33.78 4885.27 29369.08)"/><linearGradient xlink:href="#a" id="kH" x1="3090.01" x2="3230.59" y1="1500.83" y2="1500.83" gradientTransform="rotate(-41.03 2923.89 -799.27) scale(.44)"/><linearGradient xlink:href="#a" id="kI" x1="3158.82" x2="3588.04" y1="1500.7" y2="1500.7" gradientTransform="scale(-.37 .37) rotate(34.72 -6571.63 -10955.84)"/><linearGradient xlink:href="#a" id="kJ" x1="4828.29" x2="5184.72" y1="1499.56" y2="1499.56" gradientTransform="rotate(19.08 1670.98 2471.46) scale(.85)"/><linearGradient xlink:href="#a" id="kK" x1="4874.24" x2="5859.77" y1="1500.15" y2="1500.15" gradientTransform="scale(-.66 .66) rotate(-19.1 1340.73 18594.9)"/><linearGradient xlink:href="#a" id="kL" x1="3927.99" x2="4728.85" y1="1500.18" y2="1500.18" gradientTransform="rotate(-.6 121299.97 -198923.99) scale(.16)"/><linearGradient xlink:href="#a" id="kM" x1="3010.47" x2="3199.21" y1="1499.64" y2="1499.64" gradientTransform="scale(1 -1) rotate(-85.6 551.92 -362.24)"/><linearGradient xlink:href="#a" id="kN" x1="3586.31" x2="4095.84" y1="1500.68" y2="1500.68" gradientTransform="rotate(-13.99 5896.03 -6141.87) scale(.3)"/><linearGradient xlink:href="#a" id="kO" x1="3342.06" x2="3569.77" y1="1500.49" y2="1500.49" gradientTransform="scale(.55 -.55) rotate(-89.21 1050.29 -1994.64)"/><linearGradient xlink:href="#a" id="kP" x1="4213.2" x2="4717.79" y1="1498.08" y2="1498.08" gradientTransform="rotate(-66.41 2490.06 1245.2) scale(.9)"/><linearGradient xlink:href="#a" id="kQ" x1="3559.05" x2="3761.56" y1="1499.91" y2="1499.91" gradientTransform="rotate(26.4 1907.81 2108.96) scale(.87)"/><linearGradient xlink:href="#a" id="kR" x1="4965.2" x2="5947.96" y1="1500.53" y2="1500.53" gradientTransform="rotate(4.13 -3407.3 10592.77) scale(.73)"/><linearGradient xlink:href="#a" id="kS" x1="3072.22" x2="3431.26" y1="1500.78" y2="1500.78" gradientTransform="scale(-.44 .44) rotate(-17.56 4222.47 26876.96)"/><linearGradient xlink:href="#a" id="kT" x1="3252.69" x2="3715.09" y1="1499.93" y2="1499.93" gradientTransform="rotate(-47.5 2755.27 -515.18) scale(.44)"/><linearGradient xlink:href="#a" id="kU" x1="3858.57" x2="4590.08" y1="1500.74" y2="1500.74" gradientTransform="scale(-.68 .68) rotate(-46.28 -95.17 8322.27)"/><linearGradient xlink:href="#a" id="kV" x1="5512.8" x2="6928.62" y1="1499.28" y2="1499.28" gradientTransform="rotate(17.54 -17.82 4526.04) scale(.59)"/><linearGradient xlink:href="#a" id="kW" x1="3094.72" x2="3646.21" y1="1497.5" y2="1497.5" gradientTransform="scale(-.09 .09) rotate(-53.44 2069.8 38644.5)"/><linearGradient xlink:href="#a" id="kX" x1="5231.5" x2="6747.75" y1="1500.07" y2="1500.07" gradientTransform="rotate(-20.08 3058.55 40.04) scale(.81)"/><linearGradient xlink:href="#a" id="kY" x1="3817.58" x2="5447.46" y1="1504.51" y2="1504.51" gradientTransform="scale(-.15 .15) rotate(57.36 -15111.44 -11195.96)"/><linearGradient xlink:href="#a" id="kZ" x1="4970" x2="5806.2" y1="1499.96" y2="1499.96" gradientTransform="rotate(-39.83 2575.26 1115.72) scale(.91)"/><linearGradient xlink:href="#a" id="la" x1="3787.09" x2="4563.3" y1="1500.62" y2="1500.62" gradientTransform="scale(-.98 .98) rotate(43.76 -405.2 -3947.5)"/><linearGradient xlink:href="#a" id="lb" x1="5791.8" x2="6652.9" y1="1501.6" y2="1501.6" gradientTransform="scale(-.07 .07) rotate(48.44 -39138.41 -30258.55)"/><linearGradient xlink:href="#a" id="lc" x1="4883.6" x2="5637.19" y1="1500.01" y2="1500.01" gradientTransform="rotate(-22.2 4323 -3545.27) scale(.29)"/><linearGradient xlink:href="#a" id="ld" x1="4641.63" x2="5178.54" y1="1499.28" y2="1499.28" gradientTransform="scale(-.25 .25) rotate(-45.72 1252.97 17819.67)"/><linearGradient xlink:href="#a" id="le" x1="3300.85" x2="3397.9" y1="1500.23" y2="1500.23" gradientTransform="rotate(-21.26 3615.54 -1507.84) scale(.59)"/><linearGradient xlink:href="#a" id="lf" x1="4219.09" x2="4919.06" y1="1500.59" y2="1500.59" gradientTransform="scale(-.98 .98) rotate(-27.45 -294.93 10518.11)"/><linearGradient xlink:href="#a" id="lg" x1="4174.97" x2="4711.45" y1="1501.07" y2="1501.07" gradientTransform="rotate(-21.19 2823.64 624.31) scale(.88)"/><linearGradient xlink:href="#a" id="lh" x1="5553.05" x2="6916.41" y1="1498.3" y2="1498.3" gradientTransform="scale(-.63 .63) rotate(19.93 -3593.97 -14576.7)"/><linearGradient xlink:href="#a" id="li" x1="5222.84" x2="6233.55" y1="1500.73" y2="1500.73" gradientTransform="rotate(27.3 2346.7 1657.47) scale(.96)"/><linearGradient xlink:href="#a" id="lj" x1="4496.01" x2="6070.96" y1="1500.59" y2="1500.59" gradientTransform="rotate(-55.45 2531.78 931.69) scale(.82)"/><linearGradient xlink:href="#a" id="lk" x1="4315.42" x2="4618.31" y1="1498.76" y2="1498.76" gradientTransform="scale(-.49 .49) rotate(-66.79 -442.07 7529.2)"/><linearGradient xlink:href="#a" id="ll" x1="4054.76" x2="5259.66" y1="1499.73" y2="1499.73" gradientTransform="rotate(62.03 2283.63 1615.5) scale(.91)"/><linearGradient xlink:href="#a" id="lm" x1="4075.14" x2="4595.77" y1="1498.49" y2="1498.49" gradientTransform="scale(-.45 .45) rotate(-47.19 252.68 10856.75)"/><linearGradient xlink:href="#a" id="ln" x1="5492.14" x2="6906.4" y1="1498.99" y2="1498.99" gradientTransform="rotate(85.99 692.49 2019.62) scale(.12)"/><linearGradient xlink:href="#a" id="lo" x1="4917.87" x2="6729.1" y1="1501.07" y2="1501.07" gradientTransform="scale(-.19 .19) rotate(-33.69 4931.29 29398.57)"/><linearGradient xlink:href="#a" id="lp" x1="8842.23" x2="9932.84" y1="1502.03" y2="1502.03" gradientTransform="rotate(82.55 764.2 2056.08) scale(.18)"/><linearGradient xlink:href="#a" id="lq" x1="2876.15" x2="3139.58" y1="1498.95" y2="1498.95" gradientTransform="scale(-.64 .64) rotate(2.65 -19222.78 -122450)"/><linearGradient xlink:href="#a" id="lr" x1="4428.64" x2="5093.03" y1="1500.96" y2="1500.96" gradientTransform="rotate(92.03 849.56 1881.94) scale(.16)"/><linearGradient xlink:href="#a" id="ls" x1="4426.11" x2="5279.1" y1="1500.36" y2="1500.36" gradientTransform="scale(-.65 .65) rotate(47.39 -1917.43 -4579.88)"/><linearGradient xlink:href="#a" id="lt" x1="4381.67" x2="4754.17" y1="1497.9" y2="1497.9" gradientTransform="rotate(55.24 124.72 2950.58) scale(.11)"/><linearGradient xlink:href="#a" id="lu" x1="3757.89" x2="4376.06" y1="1499.96" y2="1499.96" gradientTransform="scale(-.37 .37) rotate(46.59 -5445.26 -7199.4)"/><linearGradient xlink:href="#a" id="lv" x1="4739.79" x2="5154.8" y1="1499.21" y2="1499.21" gradientTransform="rotate(9.53 2124.84 2027.16) scale(.96)"/><linearGradient xlink:href="#a" id="lw" x1="4626" x2="4978.25" y1="1498.78" y2="1498.78" gradientTransform="rotate(26.97 820.68 3210.12) scale(.62)"/><linearGradient xlink:href="#a" id="lx" x1="3121.66" x2="3292.08" y1="1499.26" y2="1499.26" gradientTransform="scale(-.91 .91) rotate(-70.36 -347 4840.95)"/><linearGradient xlink:href="#a" id="ly" x1="4800.82" x2="5665.19" y1="1499.77" y2="1499.77" gradientTransform="rotate(21.76 886.02 3303.97) scale(.69)"/><linearGradient xlink:href="#a" id="lz" x1="4490.94" x2="4739.12" y1="1501.98" y2="1501.98" gradientTransform="rotate(-22.98 4700.61 -4723.28) scale(.1)"/><linearGradient xlink:href="#a" id="lA" x1="3199.03" x2="3541.39" y1="1499.27" y2="1499.27" gradientTransform="scale(-.4 .4) rotate(-64.7 -427.42 9016.67)"/><linearGradient xlink:href="#a" id="lB" x1="2580.27" x2="2913.4" y1="1500.65" y2="1500.65" gradientTransform="scale(-.79 .79) rotate(14.53 -2240.2 -17924.04)"/><linearGradient xlink:href="#a" id="lC" x1="3988.81" x2="4212.41" y1="1500.69" y2="1500.69" gradientTransform="scale(-.28 .28) rotate(-22.76 5994.06 30270.6)"/><linearGradient xlink:href="#a" id="lD" x1="2767.67" x2="3022.76" y1="1500.07" y2="1500.07" gradientTransform="scale(-1.41 1.41) rotate(89.08 260.92 -555.76)"/><linearGradient xlink:href="#a" id="lE" x1="2768.98" x2="3007.95" y1="1500.5" y2="1500.5" gradientTransform="scale(1.39 -1.39) rotate(-1.15 -126476.92 2192.32)"/><linearGradient xlink:href="#a" id="lF" x1="2153.32" x2="2301.09" y1="1499.72" y2="1499.72" gradientTransform="scale(1.04 -1.04) rotate(-88.08 602.2 -265.34)"/><linearGradient xlink:href="#a" id="lG" x1="3679.85" x2="4271.94" y1="1500.07" y2="1500.07" gradientTransform="rotate(-39.16 2445.61 1768.52) scale(1.06)"/><linearGradient xlink:href="#a" id="lH" x1="3664.72" x2="3962.24" y1="1500.13" y2="1500.13" gradientTransform="rotate(-68.57 2526.08 1954.33) scale(1.18)"/><linearGradient xlink:href="#a" id="lI" x1="3389.18" x2="4614.49" y1="1499.71" y2="1499.71" gradientTransform="rotate(-76.65 2616.14 2398.3) scale(1.39)"/><linearGradient xlink:href="#a" id="lJ" x1="3142.4" x2="3510.7" y1="1500.17" y2="1500.17" gradientTransform="rotate(-91.52 2727.12 2360.45) scale(1.44)"/><linearGradient xlink:href="#a" id="lK" x1="2860.05" x2="3118.47" y1="1500.65" y2="1500.65" gradientTransform="scale(-1.3 1.3) rotate(48.25 345.06 -2877.35)"/><linearGradient xlink:href="#a" id="lL" x1="2799.15" x2="3640.9" y1="1499.99" y2="1499.99" gradientTransform="scale(1.35 -1.35) rotate(-89.35 527.16 189.56)"/><linearGradient xlink:href="#a" id="lM" x1="2615.63" x2="3102.32" y1="1500.14" y2="1500.14" gradientTransform="scale(1.2 -1.2) rotate(-46.02 -1273.03 -162.84)"/><linearGradient xlink:href="#a" id="lN" x1="3678.98" x2="4857.38" y1="1499.45" y2="1499.45" gradientTransform="rotate(-142.05 2622.6 1645.73) scale(1.12)"/><linearGradient xlink:href="#a" id="lO" x1="3006.74" x2="4552.92" y1="1499.17" y2="1499.17" gradientTransform="scale(-1.07 1.07) rotate(26.01 -39.86 -7593.03)"/><linearGradient xlink:href="#a" id="lP" x1="4215.07" x2="4699.13" y1="1499.67" y2="1499.67" gradientTransform="scale(-1.05 1.05) rotate(25.63 -112.64 -7820.97)"/><linearGradient xlink:href="#a" id="lQ" x1="2848.39" x2="3085.8" y1="1500.15" y2="1500.15" gradientTransform="scale(-1.31 1.31) rotate(88.56 144.23 -600.65)"/><linearGradient xlink:href="#a" id="lR" x1="2266.47" x2="2430.78" y1="1499.68" y2="1499.68" gradientTransform="scale(-1.31 1.31) rotate(15.37 1299.28 -12597.23)"/><linearGradient xlink:href="#a" id="lS" x1="2959.31" x2="3091.7" y1="1500.28" y2="1500.28" gradientTransform="rotate(-147.17 2779.69 1804.1) scale(1.27)"/><linearGradient xlink:href="#a" id="lT" x1="3840.41" x2="7400.41" y1="1499.74" y2="1499.74" gradientTransform="scale(-1.2 1.2) rotate(16.34 774.25 -12322.09)"/><linearGradient xlink:href="#a" id="lU" x1="3001.79" x2="4297.74" y1="1500.1" y2="1500.1" gradientTransform="rotate(-41.85 2270.51 2793.44) scale(1.32)"/><linearGradient xlink:href="#a" id="lV" x1="3171.19" x2="3299.61" y1="1499.4" y2="1499.4" gradientTransform="scale(1.1 -1.1) rotate(-79.72 343.53 -190.68)"/><linearGradient xlink:href="#a" id="lW" x1="2239.83" x2="2296" y1="1500.2" y2="1500.2" gradientTransform="scale(1.5 -1.5) rotate(-88.71 475.84 335.87)"/><linearGradient xlink:href="#a" id="lX" x1="2518.39" x2="2584.02" y1="1500.41" y2="1500.41" gradientTransform="scale(1.46 -1.46) rotate(-65.74 -178.55 342.95)"/><linearGradient xlink:href="#a" id="lY" x1="3572.23" x2="4014.26" y1="1499.65" y2="1499.65" gradientTransform="rotate(-120.58 2624.14 1720.94) scale(1.15)"/><linearGradient xlink:href="#a" id="lZ" x1="2827.8" x2="3047.27" y1="1500.43" y2="1500.43" gradientTransform="rotate(-46.87 2428.58 2035.64) scale(1.15)"/><linearGradient xlink:href="#a" id="ma" x1="3513.55" x2="5062.34" y1="1500.01" y2="1500.01" gradientTransform="scale(-1.27 1.27) rotate(.83 21994.28 -261863.58)"/><linearGradient xlink:href="#a" id="mb" x1="3911.65" x2="4415.45" y1="1500.38" y2="1500.38" gradientTransform="rotate(1.04 7132.9 -6039.8) scale(1.06)"/><linearGradient xlink:href="#a" id="mc" x1="3015.09" x2="3535.05" y1="1500.15" y2="1500.15" gradientTransform="scale(1.25 -1.25) rotate(-79.07 285.82 58.95)"/><linearGradient xlink:href="#a" id="md" x1="2873.54" x2="3317.86" y1="1499.66" y2="1499.66" gradientTransform="scale(-1.22 1.22) rotate(-9.55 -1736.54 24691.06)"/><linearGradient xlink:href="#a" id="me" x1="3332.51" x2="3623.34" y1="1500.03" y2="1500.03" gradientTransform="scale(-1.27 1.27) rotate(-.88 -20753.18 249424.41)"/><linearGradient xlink:href="#a" id="mf" x1="4701.03" x2="5717.36" y1="1500.68" y2="1500.68" gradientTransform="rotate(6.83 3250.02 413.6) scale(1.05)"/><linearGradient xlink:href="#a" id="mg" x1="4371.47" x2="4788.49" y1="1500.5" y2="1500.5" gradientTransform="rotate(-27.24 2478.63 1569.52) scale(1.01)"/><linearGradient xlink:href="#a" id="mh" x1="3698.81" x2="3859.79" y1="1500.2" y2="1500.2" gradientTransform="scale(-1.23 1.23) rotate(-5.85 -2846.34 39296.1)"/><linearGradient xlink:href="#a" id="mi" x1="3212.9" x2="3890.85" y1="1500.59" y2="1500.59" gradientTransform="scale(-1.03 1.03) rotate(-19.53 -410.51 13871.23)"/><linearGradient xlink:href="#a" id="mj" x1="4071.16" x2="4276.75" y1="1500.05" y2="1500.05" gradientTransform="rotate(25.44 4730.32 -835.83) scale(1.49)"/><linearGradient xlink:href="#a" id="mk" x1="3405.05" x2="3568.31" y1="1500.12" y2="1500.12" gradientTransform="matrix(-1.08 .00038 .00038 1.08 4485.17 -116.24)"/><linearGradient xlink:href="#a" id="ml" x1="2435.08" x2="2718.4" y1="1499.63" y2="1499.63" gradientTransform="scale(-1.43 1.43) rotate(52.83 509.5 -2344.87)"/><linearGradient xlink:href="#a" id="mm" x1="3419.72" x2="3696.45" y1="1500.37" y2="1500.37" gradientTransform="rotate(-39.65 2127.38 3389) scale(1.45)"/><linearGradient xlink:href="#a" id="mn" x1="2620.83" x2="2814.62" y1="1500.17" y2="1500.17" gradientTransform="scale(-1.3 1.3) rotate(-83.19 -234.58 3453.48)"/><linearGradient xlink:href="#a" id="mo" x1="3573.24" x2="4295.65" y1="1500.7" y2="1500.7" gradientTransform="rotate(74.68 2998.33 1301.96) scale(1.22)"/><linearGradient xlink:href="#a" id="mp" x1="3001.66" x2="3479.82" y1="1500.27" y2="1500.27" gradientTransform="scale(-1.45 1.45) rotate(-47.09 -473.72 5357.03)"/><linearGradient xlink:href="#a" id="mq" x1="4006.47" x2="4468.41" y1="1499.78" y2="1499.78" gradientTransform="rotate(-31 2346.61 2060.63) scale(1.11)"/><linearGradient xlink:href="#a" id="mr" x1="3408.86" x2="3979.92" y1="1500.51" y2="1500.51" gradientTransform="scale(-1.11 1.11) rotate(-23.56 -565.82 11254.86)"/><linearGradient xlink:href="#a" id="ms" x1="3738.51" x2="4360.95" y1="1499.97" y2="1499.97" gradientTransform="rotate(35.62 3122.88 953.85) scale(1.17)"/><linearGradient xlink:href="#a" id="mt" x1="3359.48" x2="3838.64" y1="1499.57" y2="1499.57" gradientTransform="scale(-1.48 1.48) rotate(3.11 9001.96 -63770.21)"/><linearGradient xlink:href="#a" id="mu" x1="3394.03" x2="3631.3" y1="1500.67" y2="1500.67" gradientTransform="rotate(-9.3 -379.78 7313.8) scale(1.36)"/><linearGradient xlink:href="#a" id="mv" x1="2903.83" x2="3524.02" y1="1500.05" y2="1500.05" gradientTransform="scale(-1.18 1.18) rotate(-14.94 -996.1 16473.6)"/><linearGradient xlink:href="#a" id="mw" x1="2447.06" x2="2732.83" y1="1500.36" y2="1500.36" gradientTransform="scale(1.48 -1.48) rotate(87.06 3090.38 162.08)"/><linearGradient xlink:href="#a" id="mx" x1="3137.93" x2="3450.36" y1="1500.57" y2="1500.57" gradientTransform="scale(-1.32 1.32) rotate(-25.24 -847.35 9681.65)"/><linearGradient xlink:href="#a" id="my" x1="6437.19" x2="7768.41" y1="1500.01" y2="1500.01" gradientTransform="rotate(79.43 700.24 2130.76) scale(.16)"/><linearGradient xlink:href="#a" id="mz" x1="2155.16" x2="2326.05" y1="1499.97" y2="1499.97" gradientTransform="scale(1.33 -1.33) rotate(-24.38 -4226.21 116.07)"/><linearGradient xlink:href="#a" id="mA" x1="5978.33" x2="6756.09" y1="1500.56" y2="1500.56" gradientTransform="rotate(53.09 393.87 2841.66) scale(.23)"/><linearGradient xlink:href="#a" id="mB" x1="3495.73" x2="3926.84" y1="1500.74" y2="1500.74" gradientTransform="scale(.36 -.36) rotate(68.4 8553.78 2422.23)"/><linearGradient xlink:href="#a" id="mC" x1="4756.83" x2="5653.7" y1="1499.47" y2="1499.47" gradientTransform="rotate(-.91 65285.25 -105048.73) scale(.33)"/><linearGradient xlink:href="#a" id="mD" x1="3668.98" x2="3824.98" y1="1502.26" y2="1502.26" gradientTransform="scale(-.18 .18) rotate(-59.36 -20.72 18675.01)"/><linearGradient xlink:href="#a" id="mE" x1="3623.48" x2="4373.8" y1="1500.69" y2="1500.69" gradientTransform="rotate(-112.19 2794.05 2127.8) scale(1.39)"/><linearGradient xlink:href="#a" id="mF" x1="2975.22" x2="3385.93" y1="1499.94" y2="1499.94" gradientTransform="scale(-.81 .81) rotate(10.99 -2463.86 -23929.3)"/><linearGradient xlink:href="#a" id="mG" x1="3739.36" x2="3934.03" y1="1500.2" y2="1500.2" gradientTransform="rotate(-92.05 2121.93 95.52) scale(.28)"/><linearGradient xlink:href="#a" id="mH" x1="3984.38" x2="4683.64" y1="1500.3" y2="1500.3" gradientTransform="scale(.29 -.29) rotate(-28.86 -7727.82 -15023.56)"/><linearGradient xlink:href="#a" id="mI" x1="4295.01" x2="4548.61" y1="1499.63" y2="1499.63" gradientTransform="rotate(-120.42 2151.68 877.5) scale(.58)"/><linearGradient xlink:href="#a" id="mJ" x1="3160.64" x2="5816.26" y1="1500.36" y2="1500.36" gradientTransform="scale(-.18 .18) rotate(-10.97 29525.05 86887.39)"/><linearGradient xlink:href="#a" id="mK" x1="4498.96" x2="5273.72" y1="1497.85" y2="1497.85" gradientTransform="rotate(176.82 1400.9 881.25) scale(.14)"/><linearGradient xlink:href="#a" id="mL" x1="3065.71" x2="3387.84" y1="1500.83" y2="1500.83" gradientTransform="scale(-.16 .16) rotate(-55.31 618.9 22123.97)"/><linearGradient xlink:href="#a" id="mM" x1="4203.51" x2="4770.08" y1="1499.91" y2="1499.91" gradientTransform="rotate(107.28 2181.82 1530.54) scale(.82)"/><linearGradient xlink:href="#a" id="mN" x1="2291.43" x2="2390.34" y1="1500.69" y2="1500.69" gradientTransform="scale(-.62 .62) rotate(8.88 -7036.02 -35848.22)"/><linearGradient xlink:href="#a" id="mO" x1="3703.26" x2="4208.9" y1="1496.64" y2="1496.64" gradientTransform="rotate(-119.6 1861 338.48) scale(.21)"/><linearGradient xlink:href="#a" id="mP" x1="2317.04" x2="3026.86" y1="1497.66" y2="1497.66" gradientTransform="scale(.25 -.25) rotate(-50.26 -2059.34 -10989.94)"/><linearGradient xlink:href="#a" id="mQ" x1="3377.49" x2="4220.25" y1="1499.44" y2="1499.44" gradientTransform="rotate(-108.24 2311.27 1057.57) scale(.73)"/><linearGradient xlink:href="#a" id="mR" x1="2627.82" x2="2909.61" y1="1500.53" y2="1500.53" gradientTransform="scale(-.74 .74) rotate(27.88 -1803.51 -8772.9)"/><linearGradient xlink:href="#a" id="mS" x1="4353.11" x2="5318.73" y1="1500.38" y2="1500.38" gradientTransform="rotate(-84.01 2375.7 864.46) scale(.7)"/><linearGradient xlink:href="#a" id="mT" x1="2515.77" x2="2935.42" y1="1500.26" y2="1500.26" gradientTransform="scale(1.33 -1.33) rotate(55.34 4359.72 223.65)"/><linearGradient xlink:href="#a" id="mU" x1="4059.24" x2="4664.14" y1="1499.31" y2="1499.31" gradientTransform="rotate(-85.04 2356.24 795.26) scale(.67)"/><linearGradient xlink:href="#a" id="mV" x1="2522.79" x2="2785.19" y1="1499.76" y2="1499.76" gradientTransform="scale(.95 -.95) rotate(48.35 5656.85 842.55)"/><linearGradient xlink:href="#a" id="mW" x1="4930.77" x2="6199.04" y1="1499.15" y2="1499.15" gradientTransform="rotate(13.57 -1693.24 6914.66) scale(.44)"/><linearGradient xlink:href="#a" id="mX" x1="2399.62" x2="2518.63" y1="1499.6" y2="1499.6" gradientTransform="scale(1.07 -1.07) rotate(-80.33 371.23 -236.64)"/><linearGradient xlink:href="#a" id="mY" x1="4183.96" x2="4458.94" y1="1499.31" y2="1499.31" gradientTransform="rotate(120.67 2088.97 1490.64) scale(.76)"/><linearGradient xlink:href="#a" id="mZ" x1="2447.03" x2="2917" y1="1499.04" y2="1499.04" gradientTransform="scale(.4 -.4) rotate(-39.51 -3259.76 -7261.03)"/><linearGradient xlink:href="#a" id="na" x1="3748.79" x2="4120.25" y1="1500.22" y2="1500.22" gradientTransform="rotate(76.82 1150.27 2007.92) scale(.39)"/><linearGradient xlink:href="#a" id="nb" x1="3113.27" x2="3438.24" y1="1501.12" y2="1501.12" gradientTransform="scale(-.47 .47) rotate(-22.75 2480.64 20120.67)"/><linearGradient xlink:href="#a" id="nc" x1="3691.31" x2="4206.94" y1="1499.9" y2="1499.9" gradientTransform="rotate(-3.68 15221.72 -21329.85) scale(.42)"/><linearGradient xlink:href="#a" id="nd" x1="3419.06" x2="3616.23" y1="1499.83" y2="1499.83" gradientTransform="scale(.57 -.57) rotate(3.96 62725.62 36377.16)"/><linearGradient xlink:href="#a" id="ne" x1="2990.71" x2="3455.53" y1="1501.16" y2="1501.16" gradientTransform="rotate(-54.26 2613.18 -196.44) scale(.47)"/><linearGradient xlink:href="#a" id="nf" x1="4372.22" x2="5305.84" y1="1500.84" y2="1500.84" gradientTransform="scale(-.61 .61) rotate(64.89 -1871.53 -2698.46)"/><linearGradient xlink:href="#a" id="ng" x1="4093.05" x2="4713.98" y1="1497.3" y2="1497.3" gradientTransform="rotate(-173.98 1635.75 917.3) scale(.29)"/><linearGradient xlink:href="#a" id="nh" x1="2610.39" x2="3098.24" y1="1502.14" y2="1502.14" gradientTransform="scale(-.31 .31) rotate(78.28 -5166.83 -2915.49)"/><linearGradient xlink:href="#a" id="ni" x1="3775.62" x2="3996.2" y1="1496.27" y2="1496.27" gradientTransform="rotate(100.31 1069.4 1723.16) scale(.24)"/><linearGradient xlink:href="#a" id="nj" x1="2209.53" x2="2484.34" y1="1499.89" y2="1499.89" gradientTransform="scale(1.1 -1.1) rotate(16.39 12020.91 1539.09)"/><linearGradient xlink:href="#a" id="nk" x1="3165.93" x2="4422.18" y1="1500.04" y2="1500.04" gradientTransform="scale(-1.49 1.49) rotate(-28.15 -907.65 8290.61)"/><linearGradient xlink:href="#a" id="nl" x1="6513.24" x2="7217.43" y1="1500.29" y2="1500.29" gradientTransform="rotate(155.37 1681.24 1223.63) scale(.42)"/><linearGradient xlink:href="#a" id="nm" x1="3668.09" x2="4886.9" y1="1493.67" y2="1493.67" gradientTransform="matrix(.08231 .0364 .0364 -.08231 2283.5 1558.64)"/><linearGradient xlink:href="#a" id="nn" x1="2996.17" x2="3418.01" y1="1500.67" y2="1500.67" gradientTransform="rotate(64.81 1326.48 2088.68) scale(.52)"/><linearGradient xlink:href="#a" id="no" x1="3123.51" x2="3965.1" y1="1500.39" y2="1500.39" gradientTransform="matrix(-.00204 1.06 1.06 .00204 917 -452.5)"/><linearGradient xlink:href="#a" id="np" x1="3753.88" x2="4166.7" y1="1497.26" y2="1497.26" gradientTransform="rotate(-173.01 1545.02 844.51) scale(.21)"/><linearGradient xlink:href="#a" id="nq" x1="2563.97" x2="2828.91" y1="1500.04" y2="1500.04" gradientTransform="scale(-.45 .45) rotate(12.28 -10312.07 -32033.64)"/><linearGradient xlink:href="#a" id="nr" x1="2955.67" x2="3178.59" y1="1500.15" y2="1500.15" gradientTransform="rotate(110.21 2832.07 1477.57) scale(1.19)"/><linearGradient xlink:href="#a" id="ns" x1="2602.37" x2="3485.17" y1="1497.96" y2="1497.96" gradientTransform="scale(-.33 .33) rotate(2.36 -76597.95 -225912.86)"/><linearGradient xlink:href="#a" id="nt" x1="3954.08" x2="4092.16" y1="1499.31" y2="1499.31" gradientTransform="rotate(55.75 1479.6 2116.9) scale(.62)"/><linearGradient xlink:href="#a" id="nu" x1="2589.22" x2="2948.23" y1="1501.35" y2="1501.35" gradientTransform="scale(-.56 .56) rotate(63.71 -2277.01 -2979.26)"/><linearGradient xlink:href="#a" id="nv" x1="3029.37" x2="3319.27" y1="1500.09" y2="1500.09" gradientTransform="rotate(-89.76 2413.45 1149.45) scale(.83)"/><linearGradient xlink:href="#a" id="nw" x1="2314.91" x2="2472.68" y1="1499.95" y2="1499.95" gradientTransform="scale(.72 -.72) rotate(-55.78 -728.8 -1822.92)"/><linearGradient xlink:href="#a" id="nx" x1="4139.35" x2="5113.94" y1="1500.85" y2="1500.85" gradientTransform="rotate(-15.94 4773.95 -3862.17) scale(.45)"/><linearGradient xlink:href="#a" id="ny" x1="3832.95" x2="4493.12" y1="1500.43" y2="1500.43" gradientTransform="scale(-.19 .19) rotate(-38.43 3501.59 26253.77)"/><linearGradient xlink:href="#a" id="nz" x1="4466.78" x2="4877.15" y1="1500.54" y2="1500.54" gradientTransform="rotate(170.23 2212.02 1359.38) scale(.78)"/><linearGradient xlink:href="#a" id="nA" x1="3498.44" x2="3823.11" y1="1499.04" y2="1499.04" gradientTransform="rotate(12.49 -4809.4 11124.93) scale(.1)"/><linearGradient xlink:href="#a" id="nB" x1="2366.6" x2="2741.04" y1="1500.14" y2="1500.14" gradientTransform="scale(1.09 -1.09) rotate(-78.82 318.12 -220.22)"/><linearGradient xlink:href="#a" id="nC" x1="3703.28" x2="4200.66" y1="1501.11" y2="1501.11" gradientTransform="rotate(-57.32 2566.83 -173.17) scale(.45)"/><linearGradient xlink:href="#a" id="nD" x1="3699.21" x2="4074.81" y1="1501.01" y2="1501.01" gradientTransform="rotate(-20.36 3296.95 -603.86) scale(.73)"/><linearGradient xlink:href="#a" id="nE" x1="2765.03" x2="3154.52" y1="1500.23" y2="1500.23" gradientTransform="scale(1.21 -1.21) rotate(-77.13 236.53 -10.18)"/><linearGradient xlink:href="#a" id="nF" x1="5010.72" x2="5404.03" y1="1503.03" y2="1503.03" gradientTransform="rotate(56.48 383.32 2761.97) scale(.2)"/><linearGradient xlink:href="#a" id="nG" x1="3071.13" x2="4015.85" y1="1499.84" y2="1499.84" gradientTransform="scale(-1.19 1.19) rotate(-82.44 -265.29 3631.48)"/><linearGradient xlink:href="#a" id="nH" x1="2473.38" x2="2923.59" y1="1499.67" y2="1499.67" gradientTransform="scale(-.87 .87) rotate(6.9 -2330.58 -37556.84)"/><linearGradient xlink:href="#a" id="nI" x1="4057.47" x2="6608.06" y1="1498.22" y2="1498.22" gradientTransform="rotate(61.28 1380.98 2104.2) scale(.56)"/><linearGradient xlink:href="#a" id="nJ" x1="2241.19" x2="2775.44" y1="1498.24" y2="1498.24" gradientTransform="scale(-.21 .21) rotate(72.81 -8864.2 -4998.78)"/><linearGradient xlink:href="#a" id="nK" x1="5033.4" x2="5461.75" y1="1502.48" y2="1502.48" gradientTransform="rotate(-6.84 11264.5 -15314.44) scale(.22)"/><linearGradient xlink:href="#a" id="nL" x1="2687.41" x2="3637.8" y1="1500.1" y2="1500.1" gradientTransform="scale(1.08 -1.08) rotate(-43.18 -1570 -542.54)"/><linearGradient xlink:href="#a" id="nM" x1="3363.73" x2="4042.31" y1="1500.59" y2="1500.59" gradientTransform="scale(.6 -.6) rotate(-32.28 -3918.87 -4481.67)"/><linearGradient xlink:href="#a" id="nN" x1="4378.96" x2="4564.39" y1="1500.05" y2="1500.05" gradientTransform="rotate(-38.75 2487.35 1561.25) scale(1.01)"/><linearGradient xlink:href="#a" id="nO" x1="2435.98" x2="2671.68" y1="1498.45" y2="1498.45" gradientTransform="scale(-.5 .5) rotate(-68.28 -470.08 7277.72)"/><linearGradient xlink:href="#a" id="nP" x1="4540.66" x2="5782.53" y1="1499.23" y2="1499.23" gradientTransform="rotate(-129.28 2104.4 905.84) scale(.56)"/><linearGradient xlink:href="#a" id="nQ" x1="3423.07" x2="4102.8" y1="1499.33" y2="1499.33" gradientTransform="scale(-1.13 1.13) rotate(5.53 1632.78 -40614.65)"/><linearGradient xlink:href="#a" id="nR" x1="3869.94" x2="4420.19" y1="1500.31" y2="1500.31" gradientTransform="rotate(-174.79 1887 1093.18) scale(.5)"/><linearGradient xlink:href="#a" id="nS" x1="2917.36" x2="3861.45" y1="1501.53" y2="1501.53" gradientTransform="scale(-.36 .36) rotate(-52.77 135.94 11692)"/><linearGradient xlink:href="#a" id="nT" x1="2380.05" x2="2594.95" y1="1498.69" y2="1498.69" gradientTransform="scale(.17 -.17) rotate(-83.98 2540.44 -10791.62)"/><linearGradient xlink:href="#a" id="nU" x1="3837.62" x2="4225.7" y1="1499.52" y2="1499.52" gradientTransform="rotate(9.12 -2695.75 8776.37) scale(.51)"/><linearGradient xlink:href="#a" id="nV" x1="3917.05" x2="5123.21" y1="1498.9" y2="1498.9" gradientTransform="scale(.54 -.54) rotate(-54.77 -896.07 -3308.99)"/><linearGradient xlink:href="#a" id="nW" x1="2776.05" x2="3172.4" y1="1501.59" y2="1501.59" gradientTransform="rotate(66.18 767.02 2343.64) scale(.28)"/><linearGradient xlink:href="#a" id="nX" x1="5171.47" x2="7500.78" y1="1501.04" y2="1501.04" gradientTransform="scale(.45 -.45) rotate(-70.34 268.77 -3545.33)"/><linearGradient xlink:href="#a" id="nY" x1="2726.18" x2="3701.04" y1="1499.89" y2="1499.89" gradientTransform="scale(.79 -.79) rotate(62.28 5325.37 889.9)"/><linearGradient xlink:href="#a" id="nZ" x1="6002.99" x2="6715.41" y1="1501.23" y2="1501.23" gradientTransform="rotate(146.92 1963.84 1362.31) scale(.64)"/><linearGradient xlink:href="#a" id="oa" x1="3925.64" x2="4247.58" y1="1499.71" y2="1499.71" gradientTransform="rotate(-118.57 2093.72 745.95) scale(.49)"/><linearGradient xlink:href="#a" id="ob" x1="2958.05" x2="7320.81" y1="1500.1" y2="1500.1" gradientTransform="scale(1.92 -1.92) rotate(-4.39 -28207.58 7451.88)"/><linearGradient xlink:href="#a" id="oc" x1="3378.7" x2="3638.18" y1="1499.99" y2="1499.99" gradientTransform="rotate(-163.14 3144.38 2029.48) scale(1.57)"/><linearGradient xlink:href="#a" id="od" x1="3047.17" x2="3839.71" y1="1500.04" y2="1500.04" gradientTransform="scale(1.12 -1.12) rotate(-57.08 -574.32 -267.15)"/><linearGradient xlink:href="#a" id="oe" x1="2473.09" x2="2744.27" y1="1500.2" y2="1500.2" gradientTransform="scale(-1.34 1.34) rotate(69.76 254.42 -1348.45)"/><linearGradient xlink:href="#a" id="of" x1="3603.54" x2="3949.16" y1="1499.28" y2="1499.28" gradientTransform="rotate(-62.53 2504.49 2250.83) scale(1.27)"/><linearGradient xlink:href="#a" id="og" x1="3172.13" x2="3486.72" y1="1500.33" y2="1500.33" gradientTransform="rotate(-163.95 2745.08 1698.88) scale(1.21)"/><linearGradient xlink:href="#a" id="oh" x1="3587.11" x2="4109.5" y1="1500.26" y2="1500.26" gradientTransform="scale(1.56 -1.56) rotate(-26.58 -3480.86 767.78)"/><linearGradient xlink:href="#a" id="oi" x1="3393.83" x2="3821.34" y1="1499.91" y2="1499.91" gradientTransform="rotate(13.12 1952.6 2216.78) scale(.93)"/><linearGradient xlink:href="#a" id="oj" x1="2249.22" x2="2409.21" y1="1500.26" y2="1500.26" gradientTransform="scale(-.83 .83) rotate(-39.52 -150.9 8394.78)"/><linearGradient xlink:href="#a" id="ok" x1="4072.42" x2="5415.61" y1="1499.41" y2="1499.41" gradientTransform="rotate(13.53 -2404.82 7839.39) scale(.35)"/><linearGradient xlink:href="#a" id="ol" x1="3355.61" x2="4190.98" y1="1500.27" y2="1500.27" gradientTransform="rotate(-147.03 2886.74 1921.21) scale(1.38)"/><linearGradient xlink:href="#a" id="om" x1="2250.08" x2="3120" y1="1499.37" y2="1499.37" gradientTransform="scale(-1.41 1.41) rotate(17 1507.7 -10830.62)"/><linearGradient xlink:href="#a" id="on" x1="3007.78" x2="3351.98" y1="1500" y2="1500" gradientTransform="rotate(174.02 4280.71 2445.63) scale(2.38)"/><linearGradient xlink:href="#a" id="oo" x1="2152.76" x2="2756.78" y1="1500.47" y2="1500.47" gradientTransform="scale(-.88 .88) rotate(63.71 -657.95 -2168.66)"/><linearGradient xlink:href="#a" id="op" x1="3210.03" x2="3392.29" y1="1500.36" y2="1500.36" gradientTransform="rotate(-13.95 1810.4 3051.1) scale(1.14)"/><linearGradient xlink:href="#a" id="oq" x1="2907.87" x2="3260.46" y1="1501.29" y2="1501.29" gradientTransform="scale(-.84 .84) rotate(61.57 -808.87 -2401.15)"/><linearGradient xlink:href="#a" id="or" x1="3204.91" x2="3754.99" y1="1500.32" y2="1500.32" gradientTransform="rotate(-134.59 2219.44 1118.5) scale(.7)"/><linearGradient xlink:href="#a" id="os" x1="3111.58" x2="3872.12" y1="1499.84" y2="1499.84" gradientTransform="scale(-1.43 1.43) rotate(42.95 610.87 -3284.79)"/><linearGradient xlink:href="#a" id="ot" x1="3729.19" x2="4222.52" y1="1500.5" y2="1500.5" gradientTransform="rotate(156.83 3013 1680.2) scale(1.37)"/><linearGradient xlink:href="#a" id="ou" x1="4564.31" x2="6508.97" y1="1499.39" y2="1499.39" gradientTransform="rotate(-50.02 2600.73 532.3) scale(.72)"/><linearGradient xlink:href="#a" id="ov" x1="2390.82" x2="2526.97" y1="1499.84" y2="1499.84" gradientTransform="matrix(2.38 .00623 .00623 -2.38 -1898.81 5059.84)"/><linearGradient xlink:href="#a" id="ow" x1="3668.91" x2="4323.06" y1="1500.03" y2="1500.03" gradientTransform="rotate(177.27 3555.97 2099.82) scale(1.83)"/><linearGradient xlink:href="#a" id="ox" x1="2767.73" x2="3082.57" y1="1499.86" y2="1499.86" gradientTransform="scale(1.4 -1.4) rotate(-68.44 -75.87 252.57)"/><linearGradient xlink:href="#a" id="oy" x1="2378.12" x2="2569.56" y1="1499.55" y2="1499.55" gradientTransform="scale(-.5 .5) rotate(79.12 -2479.82 -1893.16)"/><linearGradient xlink:href="#a" id="oz" x1="2959.32" x2="3226.88" y1="1500.05" y2="1500.05" gradientTransform="rotate(-124.62 2922.7 2193.47) scale(1.49)"/><linearGradient xlink:href="#a" id="oA" x1="2743.87" x2="3576.97" y1="1500.08" y2="1500.08" gradientTransform="scale(-.84 .84) rotate(4.23 -4397.16 -63663.33)"/><linearGradient xlink:href="#a" id="oB" x1="3303.95" x2="3587.53" y1="1499.61" y2="1499.61" gradientTransform="rotate(35.39 3218.64 866.32) scale(1.2)"/><linearGradient xlink:href="#a" id="oC" x1="3270.33" x2="4576.16" y1="1498.57" y2="1498.57" gradientTransform="scale(.8 -.8) rotate(-61.84 -331.5 -1265.13)"/><linearGradient xlink:href="#a" id="oD" x1="2891.08" x2="3418" y1="1499.88" y2="1499.88" gradientTransform="scale(-1.67 1.67) rotate(56.71 733.96 -1897.32)"/><linearGradient xlink:href="#a" id="oE" x1="3000.9" x2="3987.53" y1="1501.07" y2="1501.07" gradientTransform="rotate(-16.84 4054.04 -2246.26) scale(.59)"/><linearGradient xlink:href="#a" id="oF" x1="3292.23" x2="7318.95" y1="1500.56" y2="1500.56" gradientTransform="scale(1.04 -1.04) rotate(-70.56 44.91 -370.7)"/><linearGradient xlink:href="#a" id="oG" x1="2507.01" x2="3101.43" y1="1499.21" y2="1499.21" gradientTransform="scale(-1.47 1.47) rotate(36.35 809.75 -4142.48)"/><linearGradient xlink:href="#a" id="oH" x1="2413.71" x2="2782.25" y1="1499.56" y2="1499.56" gradientTransform="scale(2.45 -2.45) rotate(-72.77 -1.11 999.7)"/><linearGradient xlink:href="#a" id="oI" x1="3806.43" x2="4251.7" y1="1498.66" y2="1498.66" gradientTransform="rotate(-168.95 2783.8 1709.63) scale(1.24)"/><linearGradient xlink:href="#a" id="oJ" x1="2603.7" x2="2947.68" y1="1500.43" y2="1500.43" gradientTransform="scale(1.24 -1.24) rotate(88.71 3309.3 234.35)"/><linearGradient xlink:href="#a" id="oK" x1="2010.97" x2="2541.06" y1="1499.51" y2="1499.51" gradientTransform="scale(-1.44 1.44) rotate(26.07 1034.31 -6454.1)"/><linearGradient xlink:href="#a" id="oL" x1="3454.51" x2="4672.99" y1="1499.98" y2="1499.98" gradientTransform="rotate(-105.32 2751.45 2132.12) scale(1.37)"/><linearGradient xlink:href="#a" id="oM" x1="2999.92" x2="3481.24" y1="1500.59" y2="1500.59" gradientTransform="scale(-1.31 1.31) rotate(60.78 272.51 -1876.7)"/><linearGradient xlink:href="#a" id="oN" x1="3602.48" x2="4054.44" y1="1500.28" y2="1500.28" gradientTransform="rotate(-8.88 808.91 4885.78) scale(1.2)"/><linearGradient xlink:href="#a" id="oO" x1="2510.22" x2="2990.59" y1="1500.06" y2="1500.06" gradientTransform="scale(-1.2 1.2) rotate(43.57 198.82 -3541.07)"/><linearGradient xlink:href="#a" id="oP" x1="3211.23" x2="3380.39" y1="1499.44" y2="1499.44" gradientTransform="rotate(20.64 7677.56 -4397.95) scale(1.96)"/><linearGradient xlink:href="#a" id="oQ" x1="3197.54" x2="4121.38" y1="1499.99" y2="1499.99" gradientTransform="scale(-1.39 1.39) rotate(-31.08 -736.94 7844.62)"/><linearGradient xlink:href="#a" id="oR" x1="3927.54" x2="4790.1" y1="1500.07" y2="1500.07" gradientTransform="rotate(-148.74 2886.5 1908.78) scale(1.37)"/><linearGradient xlink:href="#a" id="oS" x1="3243.53" x2="3686.18" y1="1501.04" y2="1501.04" gradientTransform="scale(.79 -.79) rotate(83.25 4410.06 545.42)"/><linearGradient xlink:href="#a" id="oT" x1="3808.81" x2="7251.5" y1="1500.42" y2="1500.42" gradientTransform="rotate(-38.84 2462.04 1687.68) scale(1.04)"/><linearGradient xlink:href="#a" id="oU" x1="3144.6" x2="3368.88" y1="1499.4" y2="1499.4" gradientTransform="rotate(13.08 288.7 4382.59) scale(.72)"/><linearGradient xlink:href="#a" id="oV" x1="2419.86" x2="3268.42" y1="1500.12" y2="1500.12" gradientTransform="scale(-1.19 1.19) rotate(22.6 469.23 -8487.08)"/><linearGradient xlink:href="#a" id="oW" x1="3805.52" x2="4646.08" y1="1499.62" y2="1499.62" gradientTransform="rotate(35.76 5309.48 -956.18) scale(1.79)"/><linearGradient xlink:href="#a" id="oX" x1="3338.79" x2="3703.91" y1="1500.25" y2="1500.25" gradientTransform="scale(-.74 .74) rotate(4.8 -7031.64 -60552.23)"/><linearGradient xlink:href="#a" id="oY" x1="2863.94" x2="3075.66" y1="1499.98" y2="1499.98" gradientTransform="rotate(-1.51 -103461.37 183477.1) scale(2.9)"/><linearGradient xlink:href="#a" id="oZ" x1="2790.72" x2="3787.34" y1="1500.33" y2="1500.33" gradientTransform="scale(-1.69 1.69) rotate(-5.26 -6481.15 37353.72)"/><linearGradient xlink:href="#a" id="pa" x1="3512.78" x2="3915.42" y1="1500.03" y2="1500.03" gradientTransform="rotate(-.27 -103964.14 179916.98) scale(1.34)"/><linearGradient xlink:href="#a" id="pb" x1="2673.38" x2="2999.02" y1="1499.92" y2="1499.92" gradientTransform="scale(-1.27 1.27) rotate(-85.45 -235.1 3404.58)"/><linearGradient xlink:href="#a" id="pc" x1="3040.32" x2="3345.43" y1="1500.12" y2="1500.12" gradientTransform="rotate(35.54 7196.56 -2621.05) scale(2.31)"/><linearGradient xlink:href="#a" id="pd" x1="3905.41" x2="4613.26" y1="1499.59" y2="1499.59" gradientTransform="rotate(-81.01 2442.86 1157.72) scale(.85)"/><linearGradient xlink:href="#a" id="pe" x1="2864.67" x2="3117.23" y1="1500.34" y2="1500.34" gradientTransform="rotate(86.36 3258.84 1284.88) scale(1.37)"/><linearGradient xlink:href="#a" id="pf" x1="2793.3" x2="3365.63" y1="1499.87" y2="1499.87" gradientTransform="scale(-1.89 1.89) rotate(-34.94 -863.72 6181.89)"/><linearGradient xlink:href="#a" id="pg" x1="3230.2" x2="3968.56" y1="1500.19" y2="1500.19" gradientTransform="rotate(51.08 4089.33 448.3) scale(1.56)"/><linearGradient xlink:href="#a" id="ph" x1="2066.77" x2="2207.71" y1="1500.16" y2="1500.16" gradientTransform="scale(-1.33 1.33) rotate(46.12 414.83 -3057.14)"/><linearGradient xlink:href="#a" id="pi" x1="2903.05" x2="3727.37" y1="1500.55" y2="1500.55" gradientTransform="rotate(41.38 1745.07 2097.63) scale(.77)"/><linearGradient xlink:href="#a" id="pj" x1="2310.66" x2="2419.35" y1="1500.06" y2="1500.06" gradientTransform="scale(-3.28 3.28) rotate(-68.82 -220.4 2880.95)"/><linearGradient xlink:href="#a" id="pk" x1="2922.42" x2="4025.21" y1="1500.19" y2="1500.19" gradientTransform="rotate(-6.81 7510.42 -8102.5) scale(.56)"/><linearGradient xlink:href="#a" id="pl" x1="2813.41" x2="2975.68" y1="1499.81" y2="1499.81" gradientTransform="scale(-1.24 1.24) rotate(43.01 278.67 -3541.59)"/><linearGradient xlink:href="#a" id="pm" x1="2864.04" x2="3365.02" y1="1501.11" y2="1501.11" gradientTransform="rotate(-33.71 -756.17 14455.14) scale(3.66)"/><linearGradient xlink:href="#a" id="pn" x1="2109.39" x2="2343.42" y1="1499.9" y2="1499.9" gradientTransform="scale(-2.1 2.1) rotate(46.49 1244.93 -2427.08)"/><linearGradient xlink:href="#a" id="po" x1="3803.76" x2="4065.69" y1="1500.06" y2="1500.06" gradientTransform="rotate(-42.36 2618.92 810.44) scale(.83)"/><linearGradient xlink:href="#a" id="pp" x1="2654.76" x2="2969.91" y1="1500.3" y2="1500.3" gradientTransform="scale(1.27 -1.27) rotate(-2.22 -67273.65 -3093.87)"/><linearGradient xlink:href="#a" id="pq" x1="3732.35" x2="4898.89" y1="1502.2" y2="1502.2" gradientTransform="rotate(-14.72 5510.77 -5391.13) scale(.34)"/><linearGradient xlink:href="#a" id="pr" x1="4037.23" x2="4296.41" y1="1499.28" y2="1499.28" gradientTransform="scale(-.96 .96) rotate(17.15 -609.5 -13197.23)"/><linearGradient xlink:href="#a" id="ps" x1="3260.02" x2="3563.72" y1="1499.27" y2="1499.27" gradientTransform="rotate(-51.7 2422.19 2377.53) scale(1.26)"/><linearGradient xlink:href="#a" id="pt" x1="2138.79" x2="2508.22" y1="1500.02" y2="1500.02" gradientTransform="scale(-.72 .72) rotate(13.44 -3273.6 -20782.46)"/><linearGradient xlink:href="#a" id="pu" x1="2776.31" x2="3531.3" y1="1499.87" y2="1499.87" gradientTransform="rotate(-120.21 3426.92 3161.7) scale(2.13)"/><linearGradient xlink:href="#a" id="pv" x1="2885.78" x2="4530.93" y1="1498.6" y2="1498.6" gradientTransform="scale(-1.43 1.43) rotate(-77.1 -235.12 3530.23)"/><linearGradient xlink:href="#a" id="pw" x1="2145.24" x2="2482.1" y1="1500.34" y2="1500.34" gradientTransform="scale(1.22 -1.22) rotate(-86.41 493.49 29.79)"/><linearGradient xlink:href="#a" id="px" x1="3606.55" x2="4520.54" y1="1498.84" y2="1498.84" gradientTransform="rotate(131.97 2770.97 1532.79) scale(1.17)"/><linearGradient xlink:href="#a" id="py" x1="2544.59" x2="3070.49" y1="1500.26" y2="1500.26" gradientTransform="scale(-1.19 1.19) rotate(82.37 6.02 -873.52)"/><linearGradient xlink:href="#a" id="pz" x1="3982.99" x2="4372.81" y1="1499.06" y2="1499.06" gradientTransform="rotate(124.92 1736.19 1454.08) scale(.53)"/><linearGradient xlink:href="#a" id="pA" x1="4457.65" x2="5002.34" y1="1501.08" y2="1501.08" gradientTransform="scale(-.44 .44) rotate(34.24 -5045.46 -9740.8)"/><linearGradient xlink:href="#a" id="pB" x1="2795.23" x2="3236.85" y1="1499.67" y2="1499.67" gradientTransform="scale(-1.02 1.02) rotate(-82.14 -320.36 3954.18)"/><linearGradient xlink:href="#a" id="pC" x1="2971.65" x2="4142.73" y1="1500.25" y2="1500.25" gradientTransform="rotate(-71.35 2788.52 5002.06) scale(2.41)"/><linearGradient xlink:href="#a" id="pD" x1="2491.6" x2="2752.34" y1="1499.86" y2="1499.86" gradientTransform="scale(-1.64 1.64) rotate(-46.61 -521.97 5125.2)"/><linearGradient xlink:href="#a" id="pE" x1="2774.5" x2="3311.06" y1="1499.96" y2="1499.96" gradientTransform="rotate(83.13 2843.3 1392.02) scale(1.16)"/><linearGradient xlink:href="#a" id="pF" x1="2268.34" x2="2847.74" y1="1500.74" y2="1500.74" gradientTransform="scale(.36 -.36) rotate(54.26 9933.5 3639.03)"/><linearGradient xlink:href="#a" id="pG" x1="4798.43" x2="6889.9" y1="1495.78" y2="1495.78" gradientTransform="rotate(-153.46 2466.17 1467.4) scale(.97)"/><linearGradient xlink:href="#a" id="pH" x1="3504.49" x2="4874.49" y1="1501" y2="1501" gradientTransform="scale(-.85 .85) rotate(49.19 -839.8 -3593.4)"/><linearGradient xlink:href="#a" id="pI" x1="2811.15" x2="3213.26" y1="1499.92" y2="1499.92" gradientTransform="rotate(-69.84 2496.68 1456.94) scale(.98)"/><linearGradient xlink:href="#a" id="pJ" x1="2121.37" x2="2409.52" y1="1499.48" y2="1499.48" gradientTransform="scale(-.83 .83) rotate(12.94 -1933.63 -19761.52)"/><linearGradient xlink:href="#a" id="pK" x1="2644.2" x2="4970.17" y1="1499.41" y2="1499.41" gradientTransform="rotate(-116.35 3621.37 3681.53) scale(2.43)"/><linearGradient xlink:href="#a" id="pL" x1="2400.61" x2="2495.32" y1="1500.02" y2="1500.02" gradientTransform="scale(-2.45 2.45) rotate(84.99 895.94 -506.8)"/><linearGradient xlink:href="#a" id="pM" x1="3725.34" x2="5832.51" y1="1499.83" y2="1499.83" gradientTransform="rotate(-106.01 2393.02 1236.08) scale(.84)"/><linearGradient xlink:href="#a" id="pN" x1="2605.22" x2="3000.83" y1="1499.46" y2="1499.46" gradientTransform="scale(-.88 .88) rotate(-47.8 -266.88 6884.8)"/><linearGradient xlink:href="#a" id="pO" x1="3140.03" x2="4189.57" y1="1499.42" y2="1499.42" gradientTransform="rotate(-150 2664.67 1670) scale(1.16)"/><linearGradient xlink:href="#a" id="pP" x1="2249.64" x2="3815.97" y1="1500.1" y2="1500.1" gradientTransform="scale(.63 -.63) rotate(-74.54 356.9 -1844.38)"/><linearGradient xlink:href="#a" id="pQ" x1="2810.47" x2="3023.94" y1="1498.78" y2="1498.78" gradientTransform="rotate(-115.57 2119.7 747.1) scale(.51)"/><linearGradient xlink:href="#a" id="pR" x1="2834.81" x2="3204.61" y1="1499.46" y2="1499.46" gradientTransform="scale(-1.35 1.35) rotate(9.31 2354.07 -21355.69)"/><linearGradient xlink:href="#a" id="pS" x1="3511.99" x2="3864.62" y1="1499.87" y2="1499.87" gradientTransform="rotate(138.82 1985.25 1405.66) scale(.66)"/><linearGradient xlink:href="#a" id="pT" x1="2317.8" x2="3062.55" y1="1500.13" y2="1500.13" gradientTransform="scale(2.47 -2.47) rotate(29.39 5446.68 -1138.86)"/><linearGradient xlink:href="#a" id="pU" x1="2452.77" x2="2608.51" y1="1499.74" y2="1499.74" gradientTransform="scale(3.93 -3.93) rotate(11.06 10960.51 -5678.56)"/><linearGradient xlink:href="#a" id="pV" x1="2815.13" x2="2974.55" y1="1499.87" y2="1499.87" gradientTransform="rotate(-65.45 2583.47 4232.75) scale(2.01)"/><linearGradient xlink:href="#a" id="pW" x1="2441.35" x2="2659.41" y1="1500.37" y2="1500.37" gradientTransform="scale(-1.38 1.38) rotate(58.26 382.57 -1984.02)"/><linearGradient xlink:href="#a" id="pX" x1="2519.67" x2="2997.16" y1="1499.71" y2="1499.71" gradientTransform="scale(2.05 -2.05) rotate(-78.76 172.24 767.16)"/><linearGradient xlink:href="#a" id="pY" x1="2628.72" x2="3026.08" y1="1500.56" y2="1500.56" gradientTransform="rotate(-57.32 2573.49 -340.96) scale(.39)"/><linearGradient xlink:href="#a" id="pZ" x1="2181.37" x2="2557.06" y1="1499.87" y2="1499.87" gradientTransform="matrix(-1.27 .00465 .00465 1.27 4841.9 -419.66)"/><linearGradient xlink:href="#a" id="qa" x1="3154.59" x2="3841.81" y1="1499.71" y2="1499.71" gradientTransform="rotate(-171.26 3859.68 2463.7) scale(2.14)"/><linearGradient xlink:href="#a" id="qb" x1="2582.57" x2="3561.73" y1="1499.74" y2="1499.74" gradientTransform="scale(.78 -.78) rotate(5.57 37714.98 13780.81)"/><linearGradient xlink:href="#a" id="qc" x1="3076.19" x2="3468.74" y1="1499.67" y2="1499.67" gradientTransform="scale(.69 -.69) rotate(70.05 5347.74 933.65)"/><linearGradient xlink:href="#a" id="qd" x1="2811.58" x2="2897.52" y1="1500.27" y2="1500.27" gradientTransform="rotate(65.54 3699.7 908.45) scale(1.5)"/><linearGradient xlink:href="#a" id="qe" x1="3379.33" x2="5751.92" y1="1499.92" y2="1499.92" gradientTransform="scale(.75 -.75) rotate(62.83 5446.1 971.8)"/><linearGradient xlink:href="#a" id="qf" x1="2351.85" x2="2811.2" y1="1499.74" y2="1499.74" gradientTransform="scale(-1.32 1.32) rotate(-78.52 -247.96 3600.95)"/><linearGradient xlink:href="#a" id="qg" x1="2756.58" x2="2906.36" y1="1500.21" y2="1500.21" gradientTransform="rotate(45.9 1148.59 2485.37) scale(.55)"/><linearGradient xlink:href="#a" id="qh" x1="2316.72" x2="2906.63" y1="1500.2" y2="1500.2" gradientTransform="scale(-1.42 1.42) rotate(-8.64 -2917.13 25162.64)"/><linearGradient xlink:href="#a" id="qi" x1="2637.59" x2="2947.74" y1="1501.58" y2="1501.58" gradientTransform="rotate(151.28 1568.98 1222.7) scale(.35)"/><linearGradient xlink:href="#a" id="qj" x1="3287.46" x2="3516.41" y1="1500.37" y2="1500.37" gradientTransform="rotate(-179.13 3002.1 1806.56) scale(1.4)"/><linearGradient xlink:href="#a" id="qk" x1="2355.48" x2="2706.63" y1="1499.23" y2="1499.23" gradientTransform="scale(1.17 -1.17) rotate(-75.45 192.11 -85.29)"/><linearGradient xlink:href="#a" id="ql" x1="3026.01" x2="3780.95" y1="1499.36" y2="1499.36" gradientTransform="rotate(-15.69 5073.15 -4527.9) scale(.39)"/><linearGradient xlink:href="#a" id="qm" x1="2426.63" x2="2999.06" y1="1500.32" y2="1500.32" gradientTransform="scale(.71 -.71) rotate(-66.2 -90.13 -1581.95)"/><linearGradient xlink:href="#a" id="qn" x1="2902.69" x2="3448.63" y1="1502.02" y2="1502.02" gradientTransform="rotate(-43.84 2959.1 -1390.76) scale(.25)"/><linearGradient xlink:href="#a" id="qo" x1="2462.88" x2="2982.63" y1="1500.38" y2="1500.38" gradientTransform="scale(-1.22 1.22) rotate(-35.18 -524.35 7490.9)"/><linearGradient xlink:href="#a" id="qp" x1="3339.82" x2="6362.85" y1="1501.36" y2="1501.36" gradientTransform="rotate(148.43 1822.52 1316.04) scale(.54)"/><linearGradient xlink:href="#a" id="qq" x1="2355.38" x2="2972.82" y1="1499.85" y2="1499.85" gradientTransform="scale(-1.02 1.02) rotate(89.65 -294.12 -674.62)"/><radialGradient id="b" cx="2500" cy="1500" r="3012.22" fx="2500" fy="1500" gradientUnits="userSpaceOnUse"><stop offset="0"/><stop offset="0"/><stop offset=".5" stop-color="#0d0326"/><stop offset=".58" stop-color="#0c0b30"/><stop offset=".85" stop-color="#0a214c"/><stop offset="1" stop-color="#0a2a57"/></radialGradient></defs><g style="isolation:isolate"><path fill="url(#b)" d="M0 0h5000v3000H0z"/><path fill="url(#a)" d="m262.29 35.66 623.98 405.81-8.77 13.48L253.52 49.14c-8.97-5.83-.21-19.32 8.77-13.48Z" opacity=".77" style="mix-blend-mode:screen"/><path fill="url(#c)" d="m4756.25 1178 243.75-35.29v-8.59l-244.97 35.46 1.22 8.42z" opacity=".57" style="mix-blend-mode:screen"/><path fill="url(#d)" d="m1103.85 2929.1 153.72-156.97-3.39-3.32-153.72 156.97c-2.21 2.26 1.18 5.58 3.39 3.32Z" opacity=".77" style="mix-blend-mode:screen"/><path fill="url(#e)" d="m4118.14 548.39-297.28 175.73-3.8-6.42 297.28-175.73c4.27-2.53 8.07 3.9 3.8 6.42Z" opacity=".59" style="mix-blend-mode:screen"/><path fill="url(#f)" d="m1631.77 310.48 146.43 201.3 4.35-3.16-146.43-201.3c-2.1-2.89-6.46.27-4.35 3.16Z" style="mix-blend-mode:screen"/><path fill="url(#g)" d="m3343.39 13.78-188.24 333.51-7.21-4.07L3336.18 9.71c2.71-4.79 9.91-.73 7.21 4.07Z" opacity=".8" style="mix-blend-mode:screen"/><path fill="url(#h)" d="m3952.51 1254.61-248.8 41.55.9 5.38 248.8-41.55c3.58-.6 2.68-5.97-.9-5.38Z" opacity=".9" style="mix-blend-mode:screen"/><path fill="url(#i)" d="m938.45 1849.24 71.33-15.99.35 1.54-71.33 15.99c-1.03.23-1.37-1.31-.35-1.54Z" opacity=".59" style="mix-blend-mode:screen"/><path fill="url(#j)" d="m2634.68 966.37-9.58 38.06-.82-.21 9.58-38.06c.14-.55.96-.34.82.21Z" opacity=".94" style="mix-blend-mode:screen"/><path fill="url(#k)" d="m4289.82 1466.33-372.64 6.18.13 8.05 372.64-6.18c5.36-.09 5.23-8.14-.13-8.05Z" opacity=".49" style="mix-blend-mode:screen"/><path fill="url(#l)" d="M2031.33 242.94 1939.94 0H1932l92.38 245.55 6.95-2.61z" opacity=".89" style="mix-blend-mode:screen"/><path fill="url(#m)" d="m678.31 169.26 472.19 347.01 7.5-10.2-472.19-347.02c-6.79-4.99-14.29 5.21-7.5 10.2Z" opacity=".67" style="mix-blend-mode:screen"/><path fill="url(#n)" d="M2353.28 53.02 2347.63 0h-14.53l5.81 54.55 14.37-1.53z" opacity=".97" style="mix-blend-mode:screen"/><path fill="url(#o)" d="m2122.89 2370.6 94.99-220.83 4.77 2.05-94.99 220.83c-1.37 3.17-6.14 1.12-4.77-2.05Z" opacity=".67" style="mix-blend-mode:screen"/><path fill="url(#p)" d="m1425.04 1501.07 263.76.44v-5.7l-263.75-.44c-3.79 0-3.8 5.69 0 5.7Z" opacity=".67" style="mix-blend-mode:screen"/><path fill="url(#q)" d="m2862.68 160.17-69.07 257.24-5.56-1.49 69.07-257.24c.99-3.7 6.55-2.21 5.56 1.49Z" opacity=".59" style="mix-blend-mode:screen"/><path fill="url(#r)" d="m2095.44 2453.95 43.36-101.87-2.2-.94-43.36 101.87c-.62 1.46 1.58 2.4 2.2.94Z" opacity=".27" style="mix-blend-mode:screen"/><path fill="url(#s)" d="m1578.16 2726.66 199.57-266.86 5.77 4.31-199.57 266.86c-2.87 3.84-8.64-.47-5.77-4.31Z" opacity=".25" style="mix-blend-mode:screen"/><path fill="url(#t)" d="m2762.92 867.61-22.76 54.84-1.18-.49 22.76-54.84c.33-.79 1.51-.3 1.18.49Z" opacity=".42" style="mix-blend-mode:screen"/><path fill="url(#u)" d="M496.52 836.79 0 676.56v33.69l486.67 157.06 9.85-30.52z" opacity=".31" style="mix-blend-mode:screen"/><path fill="url(#v)" d="M2398 338.29 2366.9 0h-9.64l31.19 339.17 9.55-.88z" opacity=".93" style="mix-blend-mode:screen"/><path fill="url(#w)" d="m2668.52 2013.06-24.11-73.78-1.59.52 24.11 73.78c.35 1.06 1.94.54 1.59-.52Z" opacity=".44" style="mix-blend-mode:screen"/><path fill="url(#x)" d="m1545.32 2798.74 255-349.05 7.54 5.51-255 349.05c-3.67 5.02-11.21-.49-7.54-5.51Z" opacity=".99" style="mix-blend-mode:screen"/><path fill="url(#y)" d="m2853.43 2770.82 64.59 229.18h9.18l-65.27-231.58-8.5 2.4z" opacity=".71" style="mix-blend-mode:screen"/><path fill="url(#z)" d="m1964.23 388.46 13.27 27.51-.59.29-13.27-27.51c-.19-.4.4-.68.59-.29Z" opacity=".73" style="mix-blend-mode:screen"/><path fill="url(#A)" d="M1878.64 2699.39 1724.24 3000h10.69l152.17-296.26-8.46-4.35z" opacity=".9" style="mix-blend-mode:screen"/><path fill="url(#B)" d="m1909.58 2964.47 93.39-230.55-4.98-2.02-93.39 230.55c-1.34 3.31 3.64 5.33 4.98 2.02Z" opacity=".8" style="mix-blend-mode:screen"/><path fill="url(#C)" d="m3764.21 1798.66-346.87-80.85 1.75-7.5 346.87 80.85c4.99 1.16 3.24 8.66-1.75 7.5Z" opacity=".86" style="mix-blend-mode:screen"/><path fill="url(#D)" d="m4134.28 228.89-390.42 305.37-6.6-8.44 390.42-305.37c5.61-4.39 12.22 4.04 6.6 8.44Z" opacity=".33" style="mix-blend-mode:screen"/><path fill="url(#E)" d="m140.78 503.66 555.5 236.27 5.11-12-555.51-236.28c-7.98-3.4-13.1 8.6-5.11 12Z" opacity=".35" style="mix-blend-mode:screen"/><path fill="url(#F)" d="m4366.54 2303.15-278.16-119.18 2.58-6.01 278.16 119.18c4 1.71 1.43 7.73-2.58 6.01Z" opacity=".48" style="mix-blend-mode:screen"/><path fill="url(#G)" d="m1770.9 2329.48 133.67-151.45-3.27-2.89-133.67 151.45c-1.92 2.18 1.35 5.07 3.27 2.89Z" opacity=".82" style="mix-blend-mode:screen"/><path fill="url(#H)" d="m1167.42 1736.41 132.28-23.61.51 2.86-132.28 23.61c-1.9.34-2.41-2.52-.51-2.86Z" opacity=".38" style="mix-blend-mode:screen"/><path fill="url(#I)" d="m4555.7 1822.54-134.71-21.24-.46 2.91 134.71 21.24c1.94.31 2.4-2.61.46-2.91Z" opacity=".44" style="mix-blend-mode:screen"/><path fill="url(#J)" d="m929.42 2524.89 166.16-108.74 2.35 3.59-166.16 108.74c-2.39 1.56-4.74-2.03-2.35-3.59Z" opacity=".85" style="mix-blend-mode:screen"/><path fill="url(#K)" d="m2325.56 697.4 26.13 119.29-2.58.56-26.13-119.29c-.38-1.71 2.2-2.28 2.58-.56Z" opacity=".56" style="mix-blend-mode:screen"/><path fill="url(#L)" d="M3835.04 429.53 4365.57 0h-25.13l-515.35 417.24 9.95 12.29z" opacity=".9" style="mix-blend-mode:screen"/><path fill="url(#M)" d="m1041.58 780.99 124.19 61.09-1.32 2.68-124.19-61.09c-1.78-.88-.47-3.56 1.32-2.68Z" opacity=".9" style="mix-blend-mode:screen"/><path fill="url(#N)" d="M2124.19 2784.77 2061.65 3000h5.57l62.1-213.73-5.13-1.5z" opacity=".63" style="mix-blend-mode:screen"/><path fill="url(#O)" d="m3264.05 2912.24 47.8 87.76h9.51l-49.97-91.75-7.34 3.99z" opacity=".56" style="mix-blend-mode:screen"/><path fill="url(#P)" d="m3763.85 2968.21 27.49 31.79h10.35l-31.91-36.92-5.93 5.13z" opacity=".69" style="mix-blend-mode:screen"/><path fill="url(#Q)" d="m2027.61 2763.78 57.16-152.23-3.29-1.24-57.16 152.23c-.82 2.19 2.47 3.43 3.29 1.24Z" opacity=".61" style="mix-blend-mode:screen"/><path fill="url(#R)" d="m3136.18 2940.13 26.66 59.87h10.06l-28.33-63.61-8.39 3.74z" opacity=".4" style="mix-blend-mode:screen"/><path fill="url(#S)" d="m2903.42 2896.49 30.11 103.51h5.64l-30.55-105.02-5.2 1.51z" style="mix-blend-mode:screen"/><path fill="url(#T)" d="m481 2342.71 258.49-107.5-2.32-5.59-258.49 107.5c-3.72 1.55-1.4 7.13 2.32 5.59Z" opacity=".43" style="mix-blend-mode:screen"/><path fill="url(#U)" d="m596.11 1897.8 300.78-62.36-1.35-6.5-300.78 62.36c-4.32.9-2.98 7.4 1.35 6.5Z" opacity=".99" style="mix-blend-mode:screen"/><path fill="url(#V)" d="m3987.94 1188.15-130.76 27.54-.6-2.83 130.76-27.54c1.88-.4 2.48 2.43.6 2.83Z" opacity=".97" style="mix-blend-mode:screen"/><path fill="url(#W)" d="M2035.84 196.16 1965.55 0h-5.91l70.97 198.03 5.23-1.87z" opacity=".49" style="mix-blend-mode:screen"/><path fill="url(#X)" d="m3555.52 1509.11-80.51-.63v-1.74l80.53.63c1.16 0 1.14 1.75-.01 1.74Z" opacity=".39" style="mix-blend-mode:screen"/><path fill="url(#Y)" d="m795.42 1296.27 204.49 24.72.53-4.42-204.49-24.72c-2.94-.36-3.48 4.06-.53 4.42Z" opacity=".91" style="mix-blend-mode:screen"/><path fill="url(#Z)" d="m4261.18 1951.48-261.02-67.4-1.46 5.64 261.02 67.4c3.75.97 5.21-4.67 1.46-5.64Z" opacity=".4" style="mix-blend-mode:screen"/><path fill="url(#aa)" d="m1733.19 123.65 154.27 275.42-5.95 3.33-154.27-275.42c-2.22-3.96 3.73-7.3 5.95-3.33Z" opacity=".25" style="mix-blend-mode:screen"/><path fill="url(#ab)" d="m4896.27 2416.11-335.82-129.01-2.79 7.26 335.82 129.01c4.83 1.85 7.62-5.4 2.79-7.26Z" opacity=".8" style="mix-blend-mode:screen"/><path fill="url(#ac)" d="m1999.48 2988.54 55.16-163.33-3.53-1.19-55.16 163.33c-.79 2.35 2.74 3.54 3.53 1.19Z" opacity=".66" style="mix-blend-mode:screen"/><path fill="url(#ad)" d="m1547.68 1001.28 97.86 51.42 1.11-2.11-97.86-51.42c-1.41-.74-2.52 1.37-1.11 2.11Z" opacity=".85" style="mix-blend-mode:screen"/><path fill="url(#ae)" d="m1761.45 2614.89 223.57-339.72 7.34 4.83-223.57 339.72c-3.21 4.88-10.56.06-7.34-4.83Z" opacity=".67" style="mix-blend-mode:screen"/><path fill="url(#af)" d="m2010.36 1333.96 56.31 19.18.41-1.22-56.31-19.18c-.81-.28-1.22.94-.41 1.22Z" opacity=".49" style="mix-blend-mode:screen"/><path fill="url(#ag)" d="m1206.55 2405.14 643.15-455.3 9.84 13.9-643.15 455.3c-9.24 6.54-19.09-7.35-9.84-13.9Z" opacity=".77" style="mix-blend-mode:screen"/><path fill="url(#ah)" d="m877.75 229.9 328.27 258.18 5.58-7.09-328.27-258.18c-4.72-3.71-10.3 3.38-5.58 7.09Z" opacity=".26" style="mix-blend-mode:screen"/><path fill="url(#ai)" d="M75.83 2144.33 0 2164.57v3.7l76.75-20.48-.92-3.46z" opacity=".58" style="mix-blend-mode:screen"/><path fill="url(#aj)" d="m147.19 381.89 441.43 208.73-4.51 9.54-441.43-208.73c-6.34-3-1.84-12.54 4.51-9.54Z" opacity=".57" style="mix-blend-mode:screen"/><path fill="url(#ak)" d="m466.96 1147.96 376.32 65.94 1.42-8.13-376.32-65.94c-5.41-.95-6.84 7.18-1.42 8.13Z" opacity=".99" style="mix-blend-mode:screen"/><path fill="url(#al)" d="m2652.74 1349.08-14.53 14.38-.31-.31 14.53-14.38c.21-.21.52.11.31.31Z" opacity=".71" style="mix-blend-mode:screen"/><path fill="url(#am)" d="M3794.55 2548.83 4357.43 3000h28.13l-580.01-464.9-11 13.73z" opacity=".71" style="mix-blend-mode:screen"/><path fill="url(#an)" d="m2492.28 2660.19.64-85.22h-1.84l-.64 85.21c0 1.22 1.83 1.24 1.84.01Z" opacity=".36" style="mix-blend-mode:screen"/><path fill="url(#ao)" d="m1974.91 1606.8 92.44-18.63-.4-2-92.44 18.63c-1.33.27-.93 2.27.4 2Z" opacity=".5" style="mix-blend-mode:screen"/><path fill="url(#ap)" d="m284.32 2268.04 658.61-230.62 4.98 14.23-658.61 230.62c-9.47 3.31-14.46-10.91-4.98-14.23Z" opacity=".61" style="mix-blend-mode:screen"/><path fill="url(#aq)" d="m2858.9 1836.93-82.93-77.47 1.67-1.79 82.93 77.47c1.19 1.11-.48 2.91-1.67 1.79Z" opacity=".61" style="mix-blend-mode:screen"/><path fill="url(#ar)" d="m1865.3 2927.02 101.41-226.59-4.9-2.19-101.41 226.59c-1.46 3.26 3.44 5.45 4.9 2.19Z" opacity=".82" style="mix-blend-mode:screen"/><path fill="url(#as)" d="m4745.31 1959.82-142-29 .63-3.07 142 29c2.04.42 1.42 3.49-.63 3.07Z" opacity=".32" style="mix-blend-mode:screen"/><path fill="url(#at)" d="m3375.95 644.08-156.59 152.41 3.29 3.38 156.59-152.41c2.25-2.19-1.04-5.58-3.29-3.38Z" opacity=".44" style="mix-blend-mode:screen"/><path fill="url(#au)" d="m1909.4 383.05 57.91 109.24-2.36 1.25-57.91-109.24c-.83-1.57 1.53-2.82 2.36-1.25Z" opacity=".59" style="mix-blend-mode:screen"/><path fill="url(#av)" d="m1165.84 2223.74 100.68-54.55-1.18-2.18-100.68 54.55c-1.45.78-.27 2.96 1.18 2.18Z" opacity=".25" style="mix-blend-mode:screen"/><path fill="url(#aw)" d="M2380.23 2629.08 2342.52 3000h9.88l37.61-369.93-9.78-.99z" opacity=".95" style="mix-blend-mode:screen"/><path fill="url(#ax)" d="m4914.9 2245.56 85.1 26.06v-12.91l-81.49-24.95-3.61 11.8z" opacity=".49" style="mix-blend-mode:screen"/><path fill="url(#ay)" d="m1970.15 548.44 80.89 145.84 3.15-1.75-80.89-145.84c-1.16-2.1-4.32-.35-3.15 1.75Z" opacity=".48" style="mix-blend-mode:screen"/><path fill="url(#az)" d="M719.73 635.76 0 290.67v22.27l711.04 340.93 8.69-18.11z" opacity=".95" style="mix-blend-mode:screen"/><path fill="url(#aA)" d="m3747.14 1307.27-207.89 32.57-.7-4.49 207.89-32.57c2.99-.47 3.69 4.02.7 4.49Z" opacity=".6" style="mix-blend-mode:screen"/><path fill="url(#aB)" d="M1209.89 89.61 1127.43 0h-15.36l89.5 97.27 8.32-7.66z" opacity=".69" style="mix-blend-mode:screen"/><path fill="url(#aC)" d="m4731.29 1537.97-358.09-5.47.12-7.74 358.09 5.47c5.15.08 5.03 7.82-.12 7.74Z" opacity=".28" style="mix-blend-mode:screen"/><path fill="url(#aD)" d="m1001.67 2411.86 179.38-109.48 2.37 3.88-179.38 109.48c-2.58 1.57-4.95-2.3-2.37-3.88Z" opacity=".76" style="mix-blend-mode:screen"/><path fill="url(#aE)" d="M3769.03 14.05 3781.01 0h-3.98l-10.31 12.09 2.31 1.96z" opacity=".87" style="mix-blend-mode:screen"/><path fill="url(#aF)" d="m3052.74 2159.2-245.2-289.61 6.26-5.3L3059 2153.9c3.52 4.16-2.73 9.46-6.26 5.3Z" opacity=".61" style="mix-blend-mode:screen"/><path fill="url(#aG)" d="m3548.05 1086.46-121.41 47.75 1.03 2.62 121.41-47.75c1.75-.69.71-3.31-1.03-2.62Z" opacity=".48" style="mix-blend-mode:screen"/><path fill="url(#aH)" d="m3439.71 1076.73-141.12 63.85-1.38-3.05 141.12-63.85c2.03-.92 3.41 2.13 1.38 3.05Z" opacity=".56" style="mix-blend-mode:screen"/><path fill="url(#aI)" d="m1103.19 1670.65 200.8-24.22-.52-4.34-200.8 24.22c-2.89.35-2.37 4.69.52 4.34Z" opacity=".41" style="mix-blend-mode:screen"/><path fill="url(#aJ)" d="m4984.22 1619.86-190.15-9.02.19-4.11 190.15 9.02c2.73.13 2.54 4.24-.2 4.11Z" opacity=".68" style="mix-blend-mode:screen"/><path fill="url(#aK)" d="m3031.24 1792.33-126.96-69.44 1.5-2.74 126.96 69.44c1.82 1 .33 3.74-1.5 2.74Z" opacity=".35" style="mix-blend-mode:screen"/><path fill="url(#aL)" d="M4612.14 891.76 5000 779.07v-10.18l-390.59 113.48 2.73 9.39z" opacity=".58" style="mix-blend-mode:screen"/><path fill="url(#aM)" d="m1931.77 1135.08 196.3 127.11 2.75-4.24-196.3-127.11c-2.82-1.83-5.57 2.41-2.75 4.24Z" opacity=".75" style="mix-blend-mode:screen"/><path fill="url(#aN)" d="M4306.83 2842.75 4520.11 3000h30.25l-232.88-171.7-10.65 14.45z" opacity=".89" style="mix-blend-mode:screen"/><path fill="url(#aO)" d="m218.3 1280.87 321.64 31.41.68-6.95-321.64-31.41c-4.62-.45-5.31 6.5-.68 6.95Z" opacity=".35" style="mix-blend-mode:screen"/><path fill="url(#aP)" d="m1542.39 1512.93 134.15-1.61-.03-2.9-134.15 1.61c-1.93.02-1.9 2.92.03 2.9Z" style="mix-blend-mode:screen"/><path fill="url(#aQ)" d="m3630.43 2259.06-267.52-178.69 3.86-5.78 267.52 178.69c3.84 2.57-.01 8.35-3.86 5.78Z" opacity=".33" style="mix-blend-mode:screen"/><path fill="url(#aR)" d="M3337.01 2295.16 4094.48 3000h35.47l-776.48-722.52-16.46 17.68z" opacity=".25" style="mix-blend-mode:screen"/><path fill="url(#aS)" d="m1085.71 1534.56 223.29-5.09-.11-4.82-223.29 5.09c-3.21.07-3.1 4.9.11 4.82Z" opacity=".7" style="mix-blend-mode:screen"/><path fill="url(#aT)" d="m1078.82 2703.45 92.94-78.82 1.7 2.01-92.94 78.82c-1.34 1.13-3.04-.87-1.7-2.01Z" opacity=".72" style="mix-blend-mode:screen"/><path fill="url(#aU)" d="m1675.37 2883.53-69.1 116.47h7.78l67.08-113.05-5.76-3.42z" opacity=".77" style="mix-blend-mode:screen"/><path fill="url(#aV)" d="m4045.48 2467.05-212.56-132.61 2.87-4.59 212.56 132.61c3.06 1.91.19 6.5-2.87 4.59Z" opacity=".29" style="mix-blend-mode:screen"/><path fill="url(#aW)" d="m1513.65 1900.16 82.43-33.36-.72-1.78-82.43 33.36c-1.18.48-.46 2.26.72 1.78Z" opacity=".58" style="mix-blend-mode:screen"/><path fill="url(#aX)" d="m4817.58 2770.52-794.65-439.48-9.5 17.17 794.65 439.48c11.42 6.32 20.93-10.85 9.5-17.17Z" opacity=".73" style="mix-blend-mode:screen"/><path fill="url(#aY)" d="m1410.97 9.54-7-9.54h-7.05l9.46 12.91 4.59-3.37z" opacity=".51" style="mix-blend-mode:screen"/><path fill="url(#aZ)" d="m849.8 977.14 354.52 111.42-2.41 7.66L847.39 984.8c-5.1-1.6-2.69-9.26 2.41-7.66Z" opacity=".52" style="mix-blend-mode:screen"/><path fill="url(#ba)" d="m3614.5 1947.58-68.41-27.53-.59 1.48 68.41 27.53c.98.4 1.58-1.08.6-1.48Z" opacity=".71" style="mix-blend-mode:screen"/><path fill="url(#bb)" d="M1868.22 48.74 1846.78 0h-12.46l23.46 53.33 10.44-4.59z" opacity=".71" style="mix-blend-mode:screen"/><path fill="url(#bc)" d="M1861.02 102.59 1813.81 0h-8.1l48.63 105.67 6.68-3.08z" opacity=".45" style="mix-blend-mode:screen"/><path fill="url(#bd)" d="m4273.55 2940.49-297.93-242.88-5.25 6.44 297.93 242.88c4.28 3.49 9.53-2.94 5.25-6.44Z" opacity=".52" style="mix-blend-mode:screen"/><path fill="url(#be)" d="m3091.48 460.65-126.22 223.01-4.82-2.73 126.22-223.01c1.81-3.21 6.63-.48 4.82 2.73Z" opacity=".88" style="mix-blend-mode:screen"/><path fill="url(#bf)" d="m3657.96 339.62-164.69 165.54-3.58-3.56 164.69-165.54c2.37-2.38 5.95 1.18 3.58 3.56Z" opacity=".29" style="mix-blend-mode:screen"/><path fill="url(#bg)" d="m3312.34 81.54-181.66 318.9-6.89-3.93 181.66-318.9c2.61-4.58 9.5-.66 6.89 3.93Z" opacity=".89" style="mix-blend-mode:screen"/><path fill="url(#bh)" d="m3208 20.11-228.84 482.62-10.43-4.95 228.84-482.62c3.29-6.94 13.72-2 10.43 4.94Z" opacity=".71" style="mix-blend-mode:screen"/><path fill="url(#bi)" d="m3466.57 580.88-167.64 160.01-3.46-3.62 167.64-160.01c2.41-2.3 5.87 1.32 3.46 3.62Z" opacity=".87" style="mix-blend-mode:screen"/><path fill="url(#bj)" d="m1650.96 2915.62-50.45 84.38h5.38l49.04-82.01-3.97-2.37z" opacity=".85" style="mix-blend-mode:screen"/><path fill="url(#bk)" d="M3892.19 192.97 4096.54 0h-16.12l-195.83 184.93 7.6 8.04z" opacity=".66" style="mix-blend-mode:screen"/><path fill="url(#bl)" d="M308.33 43.38 242.71 0h-23.93l82.28 54.39 7.27-11.01z" opacity=".74" style="mix-blend-mode:screen"/><path fill="url(#bm)" d="M4129.53 176.26 3996.4 284.63l-2.34-2.88 133.13-108.37c1.91-1.56 4.26 1.32 2.34 2.88Z" opacity=".94" style="mix-blend-mode:screen"/><path fill="url(#bn)" d="M1858.3 2644.97 1661.65 3000h15.75l192.95-348.36-12.05-6.67z" opacity=".26" style="mix-blend-mode:screen"/><path fill="url(#bo)" d="m2479.86 2642.87 4.97-248.58-5.37-.11-4.97 248.58c-.07 3.57 5.3 3.68 5.37.11Z" opacity=".35" style="mix-blend-mode:screen"/><path fill="url(#bp)" d="m4032.6 608.95-193.57 112.9-2.44-4.18 193.57-112.9c2.78-1.62 5.22 2.56 2.44 4.18Z" opacity=".87" style="mix-blend-mode:screen"/><path fill="url(#bq)" d="M982.06 247.75 679.22 0h-26.97l319 260.97 10.81-13.22z" opacity=".26" style="mix-blend-mode:screen"/><path fill="url(#br)" d="M3640.4 1323.02 3418.02 1358l-.76-4.81 222.38-34.98c3.2-.5 3.95 4.3.76 4.81Z" opacity=".39" style="mix-blend-mode:screen"/><path fill="url(#bs)" d="M4136.44 799.84 3810.9 940.05l-3.03-7.03 325.54-140.21c4.68-2.02 7.71 5.02 3.03 7.03Z" opacity=".76" style="mix-blend-mode:screen"/><path fill="url(#bt)" d="m2284.29 2990.21 75.02-504.93-10.91-1.62-75.02 504.93c-1.08 7.26 9.83 8.88 10.91 1.62Z" opacity=".78" style="mix-blend-mode:screen"/><path fill="url(#bu)" d="M3836.36 149.28 3983.53 0h-9.42l-142.53 144.57 4.78 4.71z" opacity=".98" style="mix-blend-mode:screen"/><path fill="url(#bv)" d="m3869.5 955.63-249.81 99.9-2.16-5.4 249.81-99.9c3.59-1.44 5.75 3.96 2.16 5.4Z" opacity=".65" style="mix-blend-mode:screen"/><path fill="url(#bw)" d="m776.98 2574.07 368.97-228.84-4.94-7.97-368.97 228.84c-5.3 3.29-.36 11.26 4.94 7.97Z" opacity=".85" style="mix-blend-mode:screen"/><path fill="url(#bx)" d="m4660.41 1821.49 339.59 49.03v-18.73l-336.94-48.64-2.65 18.34z" opacity=".91" style="mix-blend-mode:screen"/><path fill="url(#by)" d="m2156.72 2284.45 27.59-62.9-1.36-.6-27.59 62.9c-.4.9.96 1.5 1.36.6Z" opacity=".43" style="mix-blend-mode:screen"/><path fill="url(#bz)" d="m1435.06 2018.15 95.6-46.42-1-2.07-95.6 46.42c-1.37.67-.37 2.73 1 2.07Z" opacity=".73" style="mix-blend-mode:screen"/><path fill="url(#bA)" d="m1347.52 2848.14 161.91-188.8-4.08-3.5-161.91 188.8c-2.33 2.71 1.75 6.21 4.08 3.5Z" opacity=".25" style="mix-blend-mode:screen"/><path fill="url(#bB)" d="m2100.26 164.39 78.15 259.02-5.6 1.69-78.15-259.02c-1.12-3.72 4.47-5.41 5.6-1.69Z" opacity=".63" style="mix-blend-mode:screen"/><path fill="url(#bC)" d="m3026.58 2380.41-140.19-235.79-5.1 3.03 140.19 235.79c2.01 3.39 7.11.36 5.1-3.03Z" opacity=".83" style="mix-blend-mode:screen"/><path fill="url(#bD)" d="m1036.02 2580.91 623.21-455.84-9.85-13.47-623.21 455.84c-8.96 6.55.88 20.02 9.85 13.47Z" opacity=".29" style="mix-blend-mode:screen"/><path fill="url(#bE)" d="M763.74 2779.14 465.61 3000h18.54l286.15-211.99-6.56-8.87z" opacity=".37" style="mix-blend-mode:screen"/><path fill="url(#bF)" d="m4387.96 1893.26-286.81-59.27 1.28-6.2 286.81 59.27c4.12.85 2.85 7.05-1.28 6.2Z" opacity=".32" style="mix-blend-mode:screen"/><path fill="url(#bG)" d="m1216.77 2194.15 97.09-52.42-1.13-2.1-97.09 52.42c-1.4.75-.26 2.85 1.13 2.1Z" opacity=".84" style="mix-blend-mode:screen"/><path fill="url(#bH)" d="m4418.38 2947.46 70.47 52.54h45.89l-99.96-74.53-16.4 21.99z" opacity=".63" style="mix-blend-mode:screen"/><path fill="url(#bI)" d="m4142.54 382.89-553.01 379.07-8.19-11.95 553.01-379.07c7.95-5.45 16.15 6.5 8.19 11.95Z" opacity=".53" style="mix-blend-mode:screen"/><path fill="url(#bJ)" d="m4918.28 770.68-304 92.13-1.99-6.57 304-92.13c4.37-1.32 6.36 5.24 1.99 6.57Z" opacity=".46" style="mix-blend-mode:screen"/><path fill="url(#bK)" d="m868.2 2455.95 241.25-140.88-3.04-5.21-241.25 140.88c-3.47 2.02-.43 7.24 3.04 5.21Z" opacity=".57" style="mix-blend-mode:screen"/><path fill="url(#bL)" d="m622.4 636.44 555.74 257.77 5.57-12.01-555.74-257.77c-7.99-3.71-13.56 8.3-5.57 12.01Z" opacity=".38" style="mix-blend-mode:screen"/><path fill="url(#bM)" d="m4990.46 1963.18-599.2-109.81 2.37-12.95 599.2 109.81c8.61 1.58 6.25 14.53-2.37 12.95Z" opacity=".35" style="mix-blend-mode:screen"/><path fill="url(#bN)" d="m1220.08 1724.49 106.84-18.65-.4-2.31-106.84 18.65c-1.54.27-1.13 2.58.4 2.31Z" opacity=".39" style="mix-blend-mode:screen"/><path fill="url(#bO)" d="m8.75 2713.69 250.9-121.91-2.63-5.42-250.9 121.9c-3.61 1.75-.98 7.18 2.63 5.42Z" opacity=".85" style="mix-blend-mode:screen"/><path fill="url(#bP)" d="m4813.86 2312.48-659.78-229.56 4.96-14.26 659.78 229.56c9.48 3.3 4.53 17.56-4.96 14.26Z" opacity=".72" style="mix-blend-mode:screen"/><path fill="url(#bQ)" d="m3786.41 1908.9-63.18-20.06.43-1.37 63.18 20.06c.91.29.48 1.65-.43 1.37Z" opacity=".36" style="mix-blend-mode:screen"/><path fill="url(#bR)" d="m494.03 600.19 246.4 111 2.4-5.32-246.4-111c-3.54-1.6-5.94 3.73-2.4 5.32Z" opacity=".56" style="mix-blend-mode:screen"/><path fill="url(#bS)" d="M3089.29 2751.79 3207.02 3000h7.96l-119.19-251.3-6.5 3.09z" opacity=".83" style="mix-blend-mode:screen"/><path fill="url(#bT)" d="m499.02 2699.98 241.35-144.32-3.12-5.22-241.35 144.32c-3.47 2.07-.35 7.29 3.12 5.22Z" style="mix-blend-mode:screen"/><path fill="url(#bU)" d="m3041.17 2880.2 47.31 119.8h7.38l-48.3-122.32-6.39 2.52z" opacity=".33" style="mix-blend-mode:screen"/><path fill="url(#bV)" d="M497.25 1418.37 0 1400.97v23.12l496.44 17.37.81-23.09z" opacity=".42" style="mix-blend-mode:screen"/><path fill="url(#bW)" d="m3849.01 2395.95-353.89-233.61 5.05-7.65 353.89 233.61c5.09 3.36.04 11.01-5.05 7.65Z" opacity=".47" style="mix-blend-mode:screen"/><path fill="url(#bX)" d="m900.03 846.55 117.81 48.23 1.04-2.55L901.07 844c-1.69-.69-2.74 1.85-1.04 2.55Z" opacity=".5" style="mix-blend-mode:screen"/><path fill="url(#bY)" d="M1512.3 210.74 1348.83 0h-25.05l172.89 222.87 15.63-12.13z" opacity=".98" style="mix-blend-mode:screen"/><path fill="url(#bZ)" d="m512.95 2746.91 627.16-390.74-8.44-13.55-627.16 390.74c-9.01 5.62-.58 19.17 8.44 13.55Z" opacity=".62" style="mix-blend-mode:screen"/><path fill="url(#ca)" d="m4725.04 1315.94-252.55 21.24-.46-5.46 252.55-21.24c3.63-.31 4.09 5.15.46 5.46Z" opacity=".64" style="mix-blend-mode:screen"/><path fill="url(#cb)" d="m1322.38 896.99 200.98 103.41 2.23-4.34-200.98-103.41c-2.89-1.49-5.13 2.86-2.23 4.34Z" opacity=".98" style="mix-blend-mode:screen"/><path fill="url(#cc)" d="M329.2 148.28 90.14 0H71.55l252.49 156.61 5.16-8.33z" opacity=".49" style="mix-blend-mode:screen"/><path fill="url(#cd)" d="M4335.47 1997.56 5000 2174.92v-15.57l-660.65-176.33-3.88 14.54z" opacity=".47" style="mix-blend-mode:screen"/><path fill="url(#ce)" d="m3229.57 1867.47-76.93-38.65.84-1.66 76.93 38.65c1.11.56.27 2.22-.84 1.66Z" opacity=".57" style="mix-blend-mode:screen"/><path fill="url(#cf)" d="m619.9 1149.38 2.54-13.35L4.73 1018.45a6.53 6.53 0 0 0-4.73.82v11.64c.65.36 1.32.72 2.19.88l617.7 117.58Z" opacity=".52" style="mix-blend-mode:screen"/><path fill="url(#cg)" d="m4353.78 2046.53-175.25-51.48 1.11-3.79 175.25 51.48c2.52.74 1.41 4.53-1.11 3.79Z" opacity=".79" style="mix-blend-mode:screen"/><path fill="url(#ch)" d="M812.16 1164.57 0 1007.49v18.47l808.72 156.42 3.44-17.81z" opacity=".52" style="mix-blend-mode:screen"/><path fill="url(#ci)" d="m60.14 1598.49 558.65-21.16-.46-12.07-558.65 21.16c-8.03.3-7.58 12.38.46 12.07Z" opacity=".92" style="mix-blend-mode:screen"/><path fill="url(#cj)" d="m2303.39 21.56 31.65 234.59-5.07.68-31.65-234.59c-.45-3.37 4.61-4.06 5.07-.68Z" opacity=".81" style="mix-blend-mode:screen"/><path fill="url(#ck)" d="m1662.13 1437.96 120.75 8.75-.19 2.61-120.75-8.75c-1.74-.13-1.55-2.74.19-2.61Z" opacity=".73" style="mix-blend-mode:screen"/><path fill="url(#cl)" d="m4573.42 2597.73-628.22-335.45-7.25 13.57 628.22 335.45c9.03 4.82 16.29-8.75 7.25-13.57Z" opacity=".25" style="mix-blend-mode:screen"/><path fill="url(#cm)" d="m2424.29 362.51 11.99 175.67-3.8.26-11.99-175.67c-.17-2.52 3.62-2.79 3.8-.26Z" opacity=".35" style="mix-blend-mode:screen"/><path fill="url(#cn)" d="m565.18 640.21 152.21 67.52-1.46 3.29-152.21-67.52c-2.19-.97-.73-4.26 1.46-3.29Z" opacity=".6" style="mix-blend-mode:screen"/><path fill="url(#co)" d="m1613.46 2615.93 70.06-88.03-1.9-1.51-70.06 88.03c-1.01 1.27.89 2.78 1.9 1.51Z" opacity=".88" style="mix-blend-mode:screen"/><path fill="url(#cp)" d="M251.32 1611.92 0 1625.38v16.96l252.23-13.52-.91-16.9z" opacity=".91" style="mix-blend-mode:screen"/><path fill="url(#cq)" d="M58.55 565.8 0 543.46v6.18l56.49 21.56 2.06-5.4z" opacity=".56" style="mix-blend-mode:screen"/><path fill="url(#cr)" d="m3042.78 2478.91-109.6-198.61-4.29 2.37 109.6 198.61c1.58 2.85 5.87.49 4.29-2.37Z" opacity=".47" style="mix-blend-mode:screen"/><path fill="url(#cs)" d="m992.06 1288.26 263.42 36.49-.79 5.69-263.42-36.49c-3.79-.52-3-6.22.79-5.69Z" opacity=".85" style="mix-blend-mode:screen"/><path fill="url(#ct)" d="m1778.61 2851.59 157.92-294.16-6.36-3.41-157.92 294.16c-2.27 4.23 4.08 7.64 6.36 3.41Z" opacity=".29" style="mix-blend-mode:screen"/><path fill="url(#cu)" d="M179.89 994.55 0 955.51v6.05l178.64 38.77 1.25-5.78z" opacity=".29" style="mix-blend-mode:screen"/><path fill="url(#cv)" d="m898.26 2542.95 310.22-201.04-4.34-6.7-310.22 201.04c-4.46 2.89-.12 9.6 4.34 6.7Z" opacity=".72" style="mix-blend-mode:screen"/><path fill="url(#cw)" d="m2156.29 351.45 30.67 102.07-2.21.66-30.67-102.07c-.44-1.47 1.76-2.13 2.21-.66Z" opacity=".27" style="mix-blend-mode:screen"/><path fill="url(#cx)" d="m2126.29 2155.75 91.97-160.34-3.46-1.99-91.97 160.34c-1.32 2.3 2.14 4.29 3.46 1.99Z" opacity=".77" style="mix-blend-mode:screen"/><path fill="url(#cy)" d="m2702.75 894.44-26.19 78.49-1.7-.57 26.19-78.49c.38-1.13 2.07-.56 1.7.57Z" opacity=".36" style="mix-blend-mode:screen"/><path fill="url(#cz)" d="m3453.48 959.59-169.56 96.6-2.09-3.66 169.56-96.6c2.44-1.39 4.53 2.27 2.09 3.66Z" opacity=".81" style="mix-blend-mode:screen"/><path fill="url(#cA)" d="m1776.65 2833.18 167.59-307.03-6.63-3.62-167.59 307.03c-2.41 4.41 4.22 8.04 6.63 3.62Z" opacity=".84" style="mix-blend-mode:screen"/><path fill="url(#cB)" d="m2375.23 2710.54 67.1-618.03-13.35-1.45-67.1 618.03c-.96 8.88 12.39 10.34 13.35 1.45Z" opacity=".61" style="mix-blend-mode:screen"/><path fill="url(#cC)" d="m3065.06 1739.21-104.05-44.3-.96 2.25 104.05 44.3c1.5.64 2.45-1.61.96-2.25Z" opacity=".49" style="mix-blend-mode:screen"/><path fill="url(#cD)" d="m3535.5 2907.36 68.35 92.64h5.21l-70.18-95.13-3.38 2.49z" opacity=".44" style="mix-blend-mode:screen"/><path fill="url(#cE)" d="m3620.79 1270.73-90.7 18.66-.4-1.96 90.7-18.66c1.3-.27 1.71 1.69.4 1.96Z" opacity=".69" style="mix-blend-mode:screen"/><path fill="url(#cF)" d="m2000.61 2935.73 83.41-238.3-5.15-1.8-83.41 238.3c-1.2 3.43 3.95 5.23 5.15 1.8Z" opacity=".49" style="mix-blend-mode:screen"/><path fill="url(#cG)" d="m4269.79 717.92-367.31 163.35-3.53-7.94 367.31-163.35c5.28-2.35 8.81 5.59 3.53 7.94Z" opacity=".47" style="mix-blend-mode:screen"/><path fill="url(#cH)" d="m2680.97 892.69-33.22 112.32-2.43-.72 33.22-112.32c.48-1.61 2.9-.9 2.43.72Z" opacity=".61" style="mix-blend-mode:screen"/><path fill="url(#cI)" d="M4852.31 129.63 5000 42.82V18.78l-158.19 92.98 10.5 17.87z" opacity=".97" style="mix-blend-mode:screen"/><path fill="url(#cJ)" d="m3414.51 2214.34-59.05-46.18-1 1.28 59.05 46.19c.85.66 1.85-.61 1-1.28Z" opacity=".54" style="mix-blend-mode:screen"/><path fill="url(#cK)" d="m692.27 50.34 239.41 191.49-4.14 5.17L688.13 55.52c-3.44-2.75.69-7.93 4.14-5.17Z" opacity=".78" style="mix-blend-mode:screen"/><path fill="url(#cL)" d="m1128.96 2817.85 219.94-210.68-4.55-4.75-219.94 210.68c-3.16 3.03 1.39 7.78 4.55 4.75Z" opacity=".61" style="mix-blend-mode:screen"/><path fill="url(#cM)" d="m941.36 1346.71 263.07 26.36.57-5.68-263.07-26.36c-3.78-.38-4.35 5.31-.57 5.68Z" opacity=".79" style="mix-blend-mode:screen"/><path fill="url(#cN)" d="m3150.57 2278.01-55.41-66.37-1.43 1.2 55.41 66.37c.8.95 2.23-.24 1.43-1.2Z" opacity=".77" style="mix-blend-mode:screen"/><path fill="url(#cO)" d="M748.99 2960.56 702.27 3000h41.69l22.37-18.88-17.34-20.56z" opacity=".31" style="mix-blend-mode:screen"/><path fill="url(#cP)" d="m3198.06 634.55-80.3 99.8-2.16-1.74 80.3-99.8c1.15-1.43 3.31.3 2.16 1.74Z" opacity=".68" style="mix-blend-mode:screen"/><path fill="url(#cQ)" d="m2715.26 2254.13-18.01-63.3-1.37.39 18.01 63.3c.26.91 1.63.52 1.37-.39Z" opacity=".92" style="mix-blend-mode:screen"/><path fill="url(#cR)" d="m4105.62 69.97-543.31 487.48-10.53-11.74 543.31-487.48c7.81-7.01 18.35 4.73 10.53 11.74Z" opacity=".74" style="mix-blend-mode:screen"/><path fill="url(#cS)" d="M45.52 1549.01 0 1550.07v16.55l45.9-1.07-.38-16.54z" opacity=".33" style="mix-blend-mode:screen"/><path fill="url(#cT)" d="m3864.2 35.34-186.81 201.14-4.35-4.04L3859.85 31.3c2.68-2.89 7.03 1.14 4.35 4.04Z" opacity=".73" style="mix-blend-mode:screen"/><path fill="url(#cU)" d="m1755.55 1867.52 170.09-83.48-1.8-3.68-170.09 83.48c-2.44 1.2-.64 4.88 1.8 3.68Z" opacity=".56" style="mix-blend-mode:screen"/><path fill="url(#cV)" d="m4266.35 1784.59-510.65-80.65 1.74-11.03 510.65 80.65c7.34 1.16 5.6 12.19-1.74 11.03Z" opacity=".71" style="mix-blend-mode:screen"/><path fill="url(#cW)" d="M1562.26 2878.34 1480.15 3000h15.13l77.38-114.65-10.4-7.01z" opacity=".37" style="mix-blend-mode:screen"/><path fill="url(#cX)" d="m830.5 1485.05 212.48 2.2.05-4.59-212.48-2.2c-3.05-.03-3.1 4.56-.05 4.59Z" opacity=".44" style="mix-blend-mode:screen"/><path fill="url(#cY)" d="M385.54 205.93 47.15 0H21.68l356.98 217.24 6.88-11.31z" opacity=".71" style="mix-blend-mode:screen"/><path fill="url(#cZ)" d="m4100.57 1271.97-402.36 58.49-1.26-8.69 402.36-58.49c5.78-.84 7.05 7.85 1.26 8.69Z" opacity=".4" style="mix-blend-mode:screen"/><path fill="url(#da)" d="m677.52 375.9 463.92 288.03 6.22-10.02-463.92-288.03c-6.67-4.14-12.9 5.88-6.22 10.02Z" opacity=".6" style="mix-blend-mode:screen"/><path fill="url(#db)" d="M176.89 402.46 0 319.67v20.68l168.95 79.07 7.94-16.96z" opacity=".95" style="mix-blend-mode:screen"/><path fill="url(#dc)" d="M337.76 2057.57 0 2146.06v18.13l342.21-89.66-4.45-16.96z" style="mix-blend-mode:screen"/><path fill="url(#dd)" d="M242.73 1390.82 0 1379.44v6.68l242.41 11.37.32-6.67z" opacity=".37" style="mix-blend-mode:screen"/><path fill="url(#de)" d="m3417.17 2241.17-80.66-65.05 1.41-1.74 80.66 65.05c1.16.94-.25 2.68-1.41 1.74Z" opacity=".7" style="mix-blend-mode:screen"/><path fill="url(#df)" d="m3724.18 1929.27-177.87-62.06 1.34-3.84 177.87 62.06c2.56.89 1.22 4.74-1.34 3.84Z" opacity=".73" style="mix-blend-mode:screen"/><path fill="url(#dg)" d="m1173.4 2136.34 208.79-99.78-2.16-4.51-208.79 99.78c-3 1.43-.85 5.95 2.16 4.51Z" opacity=".83" style="mix-blend-mode:screen"/><path fill="url(#dh)" d="M18.16 2170.74 0 2175.66v4.8l19.37-5.26-1.21-4.46z" style="mix-blend-mode:screen"/><path fill="url(#di)" d="m2771.86 376.58-73.57 308.09-6.66-1.59 73.57-308.09c1.06-4.43 7.72-2.84 6.66 1.59Z" opacity=".84" style="mix-blend-mode:screen"/><path fill="url(#dj)" d="m1554.87 1797.21 271.84-84.63-1.83-5.87-271.84 84.63c-3.91 1.22-2.08 7.09 1.83 5.87Z" opacity=".42" style="mix-blend-mode:screen"/><path fill="url(#dk)" d="M4730.34 80.73 4856.47 0h-24.2l-108.96 69.73 7.03 11z" opacity=".33" style="mix-blend-mode:screen"/><path fill="url(#dl)" d="m4762.91 1145.42-471.78 75.06-1.62-10.19 471.78-75.06c6.78-1.08 8.41 9.11 1.62 10.19Z" opacity=".68" style="mix-blend-mode:screen"/><path fill="url(#dm)" d="m4239.73 1722.05-225.23-28.44.61-4.87 225.23 28.44c3.24.41 2.63 5.28-.61 4.87Z" opacity=".7" style="mix-blend-mode:screen"/><path fill="url(#dn)" d="m1166.41 316.03 229.37 204.43 4.42-4.96-229.37-204.43c-3.3-2.94-7.72 2.02-4.42 4.96Z" style="mix-blend-mode:screen"/><path fill="url(#do)" d="m4038.82 2929.09 76.74 70.91h16.05l-85.4-78.91-7.39 8z" opacity=".51" style="mix-blend-mode:screen"/><path fill="url(#dp)" d="m137.73 1416.61 174.9 6.32.14-3.78-174.9-6.32c-2.51-.09-2.65 3.69-.14 3.78Z" opacity=".56" style="mix-blend-mode:screen"/><path fill="url(#dq)" d="m3886.2 1969.41-425.4-142.47 3.08-9.19 425.4 142.47c6.11 2.05 3.04 11.24-3.08 9.19Z" opacity=".77" style="mix-blend-mode:screen"/><path fill="url(#dr)" d="M332.06 729.26 0 612.76v20.06l325.79 114.31 6.27-17.87z" opacity=".51" style="mix-blend-mode:screen"/><path fill="url(#ds)" d="m1136.06 671.62 96.15 58.5 1.26-2.08-96.15-58.5c-1.38-.84-2.65 1.24-1.26 2.08Z" opacity=".82" style="mix-blend-mode:screen"/><path fill="url(#dt)" d="m4256.45 985.06-372.88 110.27-2.38-8.06L4254.07 977c5.36-1.58 7.75 6.47 2.38 8.06Z" opacity=".25" style="mix-blend-mode:screen"/><path fill="url(#du)" d="m1530.31 966.08 94.51 52.16 1.13-2.04-94.51-52.16c-1.36-.75-2.49 1.29-1.13 2.04Z" opacity=".27" style="mix-blend-mode:screen"/><path fill="url(#dv)" d="m1725.58 1099 104.49 54.34 1.17-2.26-104.49-54.34c-1.5-.78-2.68 1.48-1.17 2.26Z" opacity=".69" style="mix-blend-mode:screen"/><path fill="url(#dw)" d="m4590.34 2683.19-328.26-185.09 4-7.09 328.26 185.09c4.72 2.66.72 9.76-4 7.09Z" opacity=".95" style="mix-blend-mode:screen"/><path fill="url(#dx)" d="M404.48 165.5 143.2 0h-25.07L397.3 176.84l7.18-11.34z" opacity=".7" style="mix-blend-mode:screen"/><path fill="url(#dy)" d="m925.22 1519.61 405.06-3.92-.08-8.75-405.06 3.92c-5.82.06-5.74 8.81.08 8.75Z" opacity=".62" style="mix-blend-mode:screen"/><path fill="url(#dz)" d="m4408.67 1124.04-310.06 61.69-1.33-6.7 310.06-61.69c4.46-.89 5.79 5.81 1.33 6.7Z" opacity=".31" style="mix-blend-mode:screen"/><path fill="url(#dA)" d="m464.77 2932.4 475.37-332.84-7.19-10.27-475.37 332.84c-6.83 4.78.35 15.06 7.19 10.27Z" opacity=".67" style="mix-blend-mode:screen"/><path fill="url(#dB)" d="m2091.23 2858.45 89.12-298.78 6.46 1.93-89.12 298.78c-1.28 4.29-7.74 2.37-6.46-1.93Z" opacity=".36" style="mix-blend-mode:screen"/><path fill="url(#dC)" d="m2409.06 121.06 18.25 286.41 6.19-.39-18.26-286.41c-.26-4.12-6.45-3.73-6.19.39Z" opacity=".59" style="mix-blend-mode:screen"/><path fill="url(#dD)" d="m4396.58 2741.32 411.7 258.68h159.47l-526.03-330.52-45.14 71.84z" opacity=".88" style="mix-blend-mode:screen"/><path fill="url(#dE)" d="m3296.55 2794.02-171.99-277.97 6.01-3.72 171.99 277.97c2.47 4-3.53 7.72-6.01 3.72Z" opacity=".54" style="mix-blend-mode:screen"/><path fill="url(#dF)" d="m4399.93 836.9-167.97 58.83-1.27-3.63 167.97-58.83c2.41-.85 3.69 2.78 1.27 3.63Z" opacity=".57" style="mix-blend-mode:screen"/><path fill="url(#dG)" d="m2553.9 61.3-4.02 105.04 2.27.09 4.02-105.04c.06-1.51-2.21-1.6-2.27-.09Z" opacity=".91" style="mix-blend-mode:screen"/><path fill="url(#dH)" d="m1890.01 1206.89 61.11 29.29-.63 1.32-61.11-29.29c-.88-.42-.25-1.74.63-1.32Z" opacity=".87" style="mix-blend-mode:screen"/><path fill="url(#dI)" d="m1812.82 727.06 207.78 232.18-5.02 4.49-207.78-232.18c-2.99-3.34 2.03-7.83 5.02-4.49Z" opacity=".83" style="mix-blend-mode:screen"/><path fill="url(#dJ)" d="m413.04 2141.58 389.45-120.63 2.61 8.42L415.65 2150c-5.6 1.73-8.21-6.68-2.61-8.42Z" opacity=".28" style="mix-blend-mode:screen"/><path fill="url(#dK)" d="m4075.74 2131.57-617.24-250.51-5.41 13.34 617.24 250.51c8.87 3.6 14.29-9.73 5.41-13.34Z" style="mix-blend-mode:screen"/><path fill="url(#dL)" d="m3129.35 2842.45 74.23 157.55h5.48l-75.23-159.66-4.48 2.11z" opacity=".88" style="mix-blend-mode:screen"/><path fill="url(#dM)" d="m2791.24 2539.7 132.14 460.3h14.24l-133.22-464.08-13.16 3.78z" opacity=".35" style="mix-blend-mode:screen"/><path fill="url(#dN)" d="m4836.68 2706.57-272.28-141.02-3.05 5.88 272.28 141.02c3.91 2.03 6.96-3.85 3.05-5.88Z" opacity=".67" style="mix-blend-mode:screen"/><path fill="url(#dO)" d="M24.63 936.1 0 930.53v8.96l22.7 5.13 1.93-8.52z" opacity=".55" style="mix-blend-mode:screen"/><path fill="url(#dP)" d="m2260.44 2175.74 59.78-167.22-3.61-1.29-59.78 167.22c-.86 2.4 2.75 3.7 3.61 1.29Z" opacity=".46" style="mix-blend-mode:screen"/><path fill="url(#dQ)" d="m3042.9 965.66-103.99 102.77-2.22-2.25 103.99-102.77c1.49-1.48 3.72.77 2.22 2.25Z" opacity=".82" style="mix-blend-mode:screen"/><path fill="url(#dR)" d="m2754.36 1315.54-45.59 33.21-.72-.99 45.59-33.21c.66-.48 1.37.51.72.99Z" opacity=".5" style="mix-blend-mode:screen"/><path fill="url(#dS)" d="M1855.22 2836.56 1776.7 3000h6.3l77.34-160.98-5.12-2.46z" opacity=".33" style="mix-blend-mode:screen"/><path fill="url(#dT)" d="m206.39 1038.07 408.84 81.57-1.76 8.83-408.84-81.57c-5.88-1.17-4.12-10.01 1.76-8.83Z" opacity=".65" style="mix-blend-mode:screen"/><path fill="url(#dU)" d="M2355.31 2694.28 2319.7 3000h11.35l35.45-304.42-11.19-1.3z" opacity=".97" style="mix-blend-mode:screen"/><path fill="url(#dV)" d="m3390.51 1230.91-128.56 39.1-.84-2.78 128.56-39.1c1.85-.56 2.69 2.22.84 2.78Z" opacity=".94" style="mix-blend-mode:screen"/><path fill="url(#dW)" d="m1420.01 2839.17 53.93-66.79-1.44-1.17-53.93 66.79c-.78.96.67 2.13 1.44 1.17Z" opacity=".88" style="mix-blend-mode:screen"/><path fill="url(#dX)" d="m4231.17 2662.89-278.62-187.82-4.06 6.02 278.62 187.82c4 2.7 8.07-3.32 4.06-6.02Z" opacity=".78" style="mix-blend-mode:screen"/><path fill="url(#dY)" d="M4769.04 686.81 5000 602.76v-23.82l-238.62 86.83 7.66 21.04z" opacity=".76" style="mix-blend-mode:screen"/><path fill="url(#dZ)" d="m343.73 2811.66 276.99-167.99-3.63-5.99-276.99 167.99c-3.98 2.41-.35 8.4 3.63 5.99Z" opacity=".33" style="mix-blend-mode:screen"/><path fill="url(#ea)" d="m1775.1 760.94 104.58 106.29-2.3 2.26L1772.8 763.2c-1.5-1.53.79-3.79 2.3-2.26Z" opacity=".65" style="mix-blend-mode:screen"/><path fill="url(#eb)" d="m1174.07 1727.05 393.9-66.16-1.43-8.51-393.9 66.16c-5.66.95-4.24 9.46 1.43 8.51Z" opacity=".6" style="mix-blend-mode:screen"/><path fill="url(#ec)" d="m3276.7 634.82-96.24 107.49-2.32-2.08 96.24-107.49c1.38-1.54 3.71.53 2.32 2.08Z" opacity=".57" style="mix-blend-mode:screen"/><path fill="url(#ed)" d="M2093.32 2852.82 2049.29 3000h4.83l43.63-145.85-4.43-1.33z" opacity=".31" style="mix-blend-mode:screen"/><path fill="url(#ee)" d="M4470.45 429.73 5000 135.28V85l-550.9 306.32 21.35 38.41z" opacity=".71" style="mix-blend-mode:screen"/><path fill="url(#ef)" d="M62.44 259.03 0 227.34v8.23l59.12 30.01 3.32-6.55z" opacity=".88" style="mix-blend-mode:screen"/><path fill="url(#eg)" d="m1534.39 2844.19 338.64-467.66-10.11-7.32-338.64 467.66c-4.87 6.72 5.23 14.05 10.11 7.32Z" opacity=".8" style="mix-blend-mode:screen"/><path fill="url(#eh)" d="m2767.32 2461.4-21.18-76.38-1.65.46 21.18 76.38c.3 1.1 1.95.64 1.65-.46Z" opacity=".28" style="mix-blend-mode:screen"/><path fill="url(#ei)" d="M3229.89 32.78 3246.06 0h-12.37l-13.75 27.87 9.95 4.91z" opacity=".82" style="mix-blend-mode:screen"/><path fill="url(#ej)" d="m3195.99 2052.42-105.09-83.69-1.81 2.27 105.09 83.69c1.51 1.2 3.32-1.07 1.81-2.27Z" opacity=".6" style="mix-blend-mode:screen"/><path fill="url(#ek)" d="m3769.41 1483.16-187.27 2.78-.06-4.05 187.27-2.78c2.69-.04 2.75 4.01.06 4.05Z" opacity=".88" style="mix-blend-mode:screen"/><path fill="url(#el)" d="m887.55 2764.44 584.42-454.64-9.82-12.63-584.42 454.64c-8.4 6.53 1.42 19.17 9.82 12.63Z" opacity=".42" style="mix-blend-mode:screen"/><path fill="url(#em)" d="m3449.13 161.86-119.38 168.7-3.65-2.58 119.38-168.7c1.72-2.42 5.36.15 3.65 2.58Z" opacity=".95" style="mix-blend-mode:screen"/><path fill="url(#en)" d="m4595.26 1187.62-181.31 27.21-.59-3.92 181.31-27.21c2.61-.39 3.2 3.53.59 3.92Z" style="mix-blend-mode:screen"/><path fill="url(#eo)" d="m557.99 2068.98 209.16-61.05-1.32-4.52-209.16 61.05c-3.01.88-1.69 5.4 1.32 4.52Z" opacity=".71" style="mix-blend-mode:screen"/><path fill="url(#ep)" d="m1962.2 932.2 84.77 89.19-1.93 1.83-84.77-89.19c-1.22-1.28.71-3.11 1.93-1.83Z" opacity=".51" style="mix-blend-mode:screen"/><path fill="url(#eq)" d="m2871.09 2819.9 51.13 180.1h6.54l-51.62-181.82-6.05 1.72z" opacity=".92" style="mix-blend-mode:screen"/><path fill="url(#er)" d="m2015.29 408.74 65.01 145.73-3.15 1.4-65.01-145.73c-.93-2.09 2.21-3.5 3.15-1.4Z" opacity=".87" style="mix-blend-mode:screen"/><path fill="url(#es)" d="m1335.88 1781.84 221.08-53.05-1.15-4.78-221.08 53.05c-3.18.76-2.03 5.54 1.15 4.78Z" opacity=".66" style="mix-blend-mode:screen"/><path fill="url(#et)" d="m4776.98 1916.2 223.02 39.33v-29.75l-217.93-38.44-5.09 28.86z" opacity=".25" style="mix-blend-mode:screen"/><path fill="url(#eu)" d="m1123.58 1919.8 181.98-55.29-1.19-3.93-181.98 55.29c-2.62.79-1.42 4.73 1.19 3.93Z" opacity=".97" style="mix-blend-mode:screen"/><path fill="url(#ev)" d="M4338.49 707.96 3825 931.06l-4.82-11.1 513.49-223.1c7.38-3.21 12.21 7.89 4.82 11.1Z" opacity=".41" style="mix-blend-mode:screen"/><path fill="url(#ew)" d="m1959.65 1967.5 79.33-68.43-1.48-1.71-79.33 68.43c-1.14.98.34 2.7 1.48 1.71Z" opacity=".32" style="mix-blend-mode:screen"/><path fill="url(#ex)" d="m1075.46 2900.09 90.02-88.35-1.91-1.95-90.02 88.35c-1.29 1.27.61 3.22 1.91 1.95Z" opacity=".25" style="mix-blend-mode:screen"/><path fill="url(#ey)" d="m3808.2 511.48-286.73 217.81-4.71-6.2 286.73-217.81c4.12-3.13 8.83 3.06 4.71 6.2Z" opacity=".56" style="mix-blend-mode:screen"/><path fill="url(#ez)" d="M546.55 2211.55 0 2413.59v20.44l553.2-204.5-6.65-17.98z" opacity=".35" style="mix-blend-mode:screen"/><path fill="url(#eA)" d="m2338.7 993.86 16.13 50.4-1.09.35-16.13-50.4c-.23-.72.86-1.07 1.09-.35Z" opacity=".27" style="mix-blend-mode:screen"/><path fill="url(#eB)" d="m1027.59 1332.9 409.97 47.77 1.03-8.86-409.97-47.77c-5.89-.69-6.93 8.17-1.03 8.86Z" opacity=".42" style="mix-blend-mode:screen"/><path fill="url(#eC)" d="m4593.83 1816.92 406.17 60.39v-11.79l-404.46-60.14-1.71 11.54z" opacity=".66" style="mix-blend-mode:screen"/><path fill="url(#eD)" d="m1481.54 2917.95-58.78 82.05h5.46l56.93-79.46-3.61-2.59z" opacity=".39" style="mix-blend-mode:screen"/><path fill="url(#eE)" d="m3451.24 978.03-287.92 159.3-3.44-6.22 287.92-159.3c4.14-2.29 7.58 3.93 3.44 6.22Z" opacity=".73" style="mix-blend-mode:screen"/><path fill="url(#eF)" d="m2756.81 2740.69-42.66-207.92-4.49.92 42.66 207.92c.61 2.99 5.11 2.07 4.49-.92Z" opacity=".85" style="mix-blend-mode:screen"/><path fill="url(#eG)" d="m2683.6 901.67-64.39 212.66-4.6-1.39L2679 900.28c.93-3.06 5.52-1.67 4.6 1.39Z" opacity=".78" style="mix-blend-mode:screen"/><path fill="url(#eH)" d="M4396.32 2534.61 5000 2860.16v-24.06l-593.63-320.13-10.05 18.64z" opacity=".59" style="mix-blend-mode:screen"/><path fill="url(#eI)" d="m542.7 15.39 501.34 382.49 8.27-10.83L550.97 4.55c-7.21-5.5-15.48 5.33-8.27 10.83Z" opacity=".67" style="mix-blend-mode:screen"/><path fill="url(#eJ)" d="m4549.94 1392.3-145.85 7.78-.17-3.15 145.85-7.78c2.1-.11 2.27 3.04.17 3.15Z" opacity=".55" style="mix-blend-mode:screen"/><path fill="url(#eK)" d="m3389.01 856.21-86.34 62.67-1.35-1.87 86.34-62.67c1.24-.9 2.6.96 1.35 1.87Z" opacity=".71" style="mix-blend-mode:screen"/><path fill="url(#eL)" d="m2175.64 2996.32 78.32-356.92-7.71-1.69-78.32 356.92c-1.13 5.13 6.59 6.83 7.71 1.69Z" opacity=".75" style="mix-blend-mode:screen"/><path fill="url(#eM)" d="m4396.58 1341.99-150.4 12.66-.27-3.25 150.4-12.66c2.16-.18 2.44 3.07.27 3.25Z" opacity=".25" style="mix-blend-mode:screen"/><path fill="url(#eN)" d="m415.55 1501.74 330.61.29v-7.14l-330.6-.29c-4.75 0-4.76 7.14 0 7.14Z" opacity=".71" style="mix-blend-mode:screen"/><path fill="url(#eO)" d="m2028.7 1445.21 151.65 18.18.39-3.28-151.65-18.18c-2.18-.26-2.57 3.02-.39 3.28Z" opacity=".88" style="mix-blend-mode:screen"/><path fill="url(#eP)" d="m4555.6 2882.95-837.57-558.27 12.06-18.1 837.57 558.27c12.04 8.02-.01 26.13-12.06 18.1Z" opacity=".6" style="mix-blend-mode:screen"/><path fill="url(#eQ)" d="m1929.91 1964.27 132.83-107.62-2.33-2.87-132.83 107.62c-1.91 1.55.41 4.42 2.33 2.87Z" opacity=".83" style="mix-blend-mode:screen"/><path fill="url(#eR)" d="m4036.06 2715.72-453.72-356.7 7.71-9.8 453.72 356.7c6.52 5.13-1.18 14.94-7.71 9.8Z" opacity=".37" style="mix-blend-mode:screen"/><path fill="url(#eS)" d="m981.57 1254.71 228.83 37.37.81-4.94-228.83-37.37c-3.29-.54-4.1 4.41-.81 4.94Z" opacity=".27" style="mix-blend-mode:screen"/><path fill="url(#eT)" d="m3009.74 646.23-32.2 54.01-1.17-.7 32.2-54.01c.46-.78 1.63-.08 1.17.7Z" opacity=".47" style="mix-blend-mode:screen"/><path fill="url(#eU)" d="m2038.38 1827.03 141.02-99.23-2.14-3.05-141.02 99.23c-2.03 1.43.12 4.47 2.14 3.05Z" opacity=".42" style="mix-blend-mode:screen"/><path fill="url(#eV)" d="m4566.98 2295.65-566.92-216.31 4.67-12.25 566.92 216.31c8.15 3.11 3.48 15.36-4.67 12.25Z" opacity=".62" style="mix-blend-mode:screen"/><path fill="url(#eW)" d="m3897.03 666.25-638.26 385.42-8.33-13.79 638.26-385.42c9.17-5.54 17.51 8.25 8.33 13.79Z" opacity=".77" style="mix-blend-mode:screen"/><path fill="url(#eX)" d="M121.25 916.98 0 887.85v22.52l116.13 27.9 5.12-21.29z" opacity=".32" style="mix-blend-mode:screen"/><path fill="url(#eY)" d="m3209.22 2663.42-234.83-381.84 8.25-5.07 234.83 381.84c3.38 5.49-4.87 10.57-8.25 5.07Z" opacity=".74" style="mix-blend-mode:screen"/><path fill="url(#eZ)" d="m1486.33 754.75 143.41 105.8 2.29-3.1-143.41-105.8c-2.06-1.52-4.35 1.58-2.29 3.1Z" opacity=".37" style="mix-blend-mode:screen"/><path fill="url(#fa)" d="m3441.47 2967.45 21.26 32.55h32.65l-31.01-47.5-22.9 14.95z" opacity=".49" style="mix-blend-mode:screen"/><path fill="url(#fb)" d="m199.75 1526.52 465.66-4.36-.09-10.06-465.66 4.36c-6.69.06-6.6 10.12.09 10.06Z" opacity=".61" style="mix-blend-mode:screen"/><path fill="url(#fc)" d="m286.03 1338.31 394.06 29.54.64-8.52-394.06-29.54c-5.66-.42-6.31 8.09-.64 8.51Z" opacity=".61" style="mix-blend-mode:screen"/><path fill="url(#fd)" d="m2647.67 1242.55-33.42 58.59-1.27-.72 33.42-58.59c.48-.84 1.75-.12 1.27.72Z" opacity=".94" style="mix-blend-mode:screen"/><path fill="url(#fe)" d="m711.02 1485.34 842.33 11.18.24-18.2-842.33-11.18c-12.11-.16-12.36 18.04-.24 18.2Z" opacity=".37" style="mix-blend-mode:screen"/><path fill="url(#ff)" d="m2768.22 1760.02-36.16-34.95.76-.78 36.16 34.95c.52.5-.24 1.28-.76.78Z" opacity=".58" style="mix-blend-mode:screen"/><path fill="url(#fg)" d="m3754.03 2530.4-185.78-152.18 3.29-4.01 185.78 152.18c2.67 2.19-.62 6.2-3.29 4.01Z" opacity=".66" style="mix-blend-mode:screen"/><path fill="url(#fh)" d="m1921.21 1042.69 104.47 82.89 1.79-2.26-104.47-82.89c-1.5-1.19-3.29 1.07-1.79 2.26Z" opacity=".5" style="mix-blend-mode:screen"/><path fill="url(#fi)" d="m3640.19 2933.52 53.04 66.48h6.62l-55.61-69.71-4.05 3.23z" opacity=".97" style="mix-blend-mode:screen"/><path fill="url(#fj)" d="m351.39 934.72 912.27 244.47 5.28-19.71-912.27-244.47c-13.11-3.51-18.41 16.2-5.28 19.71Z" opacity=".77" style="mix-blend-mode:screen"/><path fill="url(#fk)" d="m4850.48 1231.4-892.89 105.89-2.29-19.29 892.89-105.89c12.83-1.52 15.13 17.77 2.29 19.29Z" opacity=".99" style="mix-blend-mode:screen"/><path fill="url(#fl)" d="m677.61 724.98 488.09 209.41 4.53-10.55-488.09-209.41c-7.02-3.01-11.55 7.53-4.53 10.55Z" opacity=".3" style="mix-blend-mode:screen"/><path fill="url(#fm)" d="m2541.94 2821.52 6.47 178.48h12l-6.49-178.92-11.98.44z" opacity=".55" style="mix-blend-mode:screen"/><path fill="url(#fn)" d="m778.11 442.35 223 137.44 2.97-4.82-223-137.44c-3.21-1.98-6.18 2.84-2.97 4.82Z" opacity=".62" style="mix-blend-mode:screen"/><path fill="url(#fo)" d="m3135.35 570.66-106.56 156.37-3.38-2.3 106.56-156.37c1.53-2.25 4.91.05 3.38 2.3Z" opacity=".45" style="mix-blend-mode:screen"/><path fill="url(#fp)" d="m3759.7 2439.1-457.23-338.17 7.31-9.88 457.23 338.17c6.57 4.86-.73 14.74-7.31 9.88Z" opacity=".52" style="mix-blend-mode:screen"/><path fill="url(#fq)" d="M496.86 74.08 392.46 0h-13.98l113.7 80.68 4.68-6.6z" opacity=".89" style="mix-blend-mode:screen"/><path fill="url(#fr)" d="m3399.94 2178.34-99.99-75.19 1.62-2.16 99.99 75.19c1.44 1.08-.19 3.24-1.62 2.16Z" opacity=".7" style="mix-blend-mode:screen"/><path fill="url(#fs)" d="m732.14 714.46 177.96 79.32 1.71-3.85-177.96-79.32c-2.56-1.14-4.27 2.7-1.71 3.85Z" opacity=".43" style="mix-blend-mode:screen"/><path fill="url(#ft)" d="m3576.77 727.89-224.52 161.77-3.5-4.85 224.52-161.77c3.23-2.33 6.73 2.52 3.5 4.85Z" opacity=".46" style="mix-blend-mode:screen"/><path fill="url(#fu)" d="m4691.86 951.72-186.55 46.89-1.01-4.03 186.55-46.89c2.68-.67 3.7 3.36 1.01 4.03Z" opacity=".97" style="mix-blend-mode:screen"/><path fill="url(#fv)" d="m3430.29 1037.48-310.89 155.93-3.37-6.72 310.89-155.93c4.47-2.24 7.84 4.47 3.37 6.72Z" opacity=".43" style="mix-blend-mode:screen"/><path fill="url(#fw)" d="M2321.55 467.39 2370.36 755l6.21-1.05-48.81-287.61c-.7-4.13-6.92-3.08-6.21 1.05Z" opacity=".36" style="mix-blend-mode:screen"/><path fill="url(#fx)" d="m4953.91 1535.75-280.95-3.75.08-6.07 280.95 3.75c4.04.05 3.96 6.12-.08 6.07Z" opacity=".76" style="mix-blend-mode:screen"/><path fill="url(#fy)" d="m1164.94 2010.83 235.05-89.41-1.93-5.08-235.05 89.41c-3.38 1.29-1.45 6.37 1.93 5.08Z" opacity=".96" style="mix-blend-mode:screen"/><path fill="url(#fz)" d="m3465.13 2587.35-127.63-143.36 3.1-2.76 127.63 143.36c1.83 2.06-1.26 4.82-3.1 2.76Z" opacity=".82" style="mix-blend-mode:screen"/><path fill="url(#fA)" d="m2207.43 2951.24 65.81-330.56 7.14 1.42-65.81 330.56c-.95 4.75-8.09 3.33-7.14-1.42Z" opacity=".25" style="mix-blend-mode:screen"/><path fill="url(#fB)" d="m2726.14 493.62-21.79 96.48 2.08.47 21.79-96.48c.31-1.39-1.77-1.86-2.08-.47Z" opacity=".53" style="mix-blend-mode:screen"/><path fill="url(#fC)" d="M3038.99 331.05 3189.8 0h-13.38l-148.51 326 11.08 5.05z" opacity=".7" style="mix-blend-mode:screen"/><path fill="url(#fD)" d="m2256.82 2763.47-44.7 236.53h8.84l44.39-234.91-8.53-1.62z" opacity=".53" style="mix-blend-mode:screen"/><path fill="url(#fE)" d="m3089.52 2810.77-61.07-135.3 2.92-1.32 61.07 135.3c.88 1.94-2.04 3.27-2.92 1.32Z" opacity=".36" style="mix-blend-mode:screen"/><path fill="url(#fF)" d="m1187.14 388.47 134.64 114.26 2.47-2.91-134.64-114.26c-1.94-1.64-4.41 1.27-2.47 2.91Z" opacity=".34" style="mix-blend-mode:screen"/><path fill="url(#fG)" d="m2995.31 2902.87-75.93-213.76 4.62-1.64 75.93 213.76c1.09 3.07-3.53 4.72-4.62 1.64Z" opacity=".62" style="mix-blend-mode:screen"/><path fill="url(#fH)" d="m4408.31 1973.36-122.8-30.57-.66 2.65 122.8 30.57c1.76.44 2.43-2.21.66-2.65Z" opacity=".31" style="mix-blend-mode:screen"/><path fill="url(#fI)" d="m503.25 2158.48 166.46-55.1 1.19 3.6-166.46 55.1c-2.39.79-3.59-2.8-1.19-3.6Z" opacity=".46" style="mix-blend-mode:screen"/><path fill="url(#fJ)" d="m2478.68 2764.6 5.67-292.83-6.33-.12-5.67 292.83c-.08 4.21 6.25 4.34 6.33.12Z" opacity=".87" style="mix-blend-mode:screen"/><path fill="url(#fK)" d="M3023.96 5.14 2908.85 335.8l-7.15-2.49L3016.82 2.65c1.65-4.75 8.8-2.27 7.15 2.49Z" opacity=".93" style="mix-blend-mode:screen"/><path fill="url(#fL)" d="m4774.78 1769.75-301.26-36.18-.78 6.51 301.26 36.18c4.33.52 5.12-5.99.78-6.51Z" opacity=".85" style="mix-blend-mode:screen"/><path fill="url(#fM)" d="M3364.65 432.92 3712.34 0h-15.4l-341.65 425.4 9.36 7.52z" opacity=".59" style="mix-blend-mode:screen"/><path fill="url(#fN)" d="m1339.96 1623.69 211.73-23 .5 4.58-211.73 23c-3.04.33-3.54-4.24-.5-4.58Z" opacity=".9" style="mix-blend-mode:screen"/><path fill="url(#fO)" d="M1947.22 87.34 1912.66 0h-10.69l36 91 9.25-3.66z" opacity=".6" style="mix-blend-mode:screen"/><path fill="url(#fP)" d="m2901.91 1884.24-47.57-45.6-.99 1.03 47.57 45.6c.68.66 1.67-.37.99-1.03Z" opacity=".33" style="mix-blend-mode:screen"/><path fill="url(#fQ)" d="m1400.85 1043.55 181.8 75.15-1.62 3.93-181.8-75.15c-2.61-1.08-.99-5.01 1.62-3.93Z" opacity=".52" style="mix-blend-mode:screen"/><path fill="url(#fR)" d="m3062.91 45.55-166.7 434.88-9.4-3.6 166.7-434.88c2.4-6.25 11.8-2.65 9.4 3.6Z" opacity=".65" style="mix-blend-mode:screen"/><path fill="url(#fS)" d="m3899.82 672.86-420.97 250.65-5.42-9.1 420.97-250.65c6.05-3.6 11.47 5.49 5.42 9.1Z" opacity=".25" style="mix-blend-mode:screen"/><path fill="url(#fT)" d="m3812.82 68.21-262.33 287.34-6.21-5.67 262.33-287.34c3.77-4.13 9.98 1.53 6.21 5.67Z" opacity=".44" style="mix-blend-mode:screen"/><path fill="url(#fU)" d="m4476.7 2259.83-253.37-97.85-2.11 5.47 253.37 97.85c3.64 1.41 5.76-4.07 2.11-5.48Z" opacity=".43" style="mix-blend-mode:screen"/><path fill="url(#fV)" d="m3754.06 283.35-180.35 175.52-3.79-3.9 180.35-175.52c2.59-2.52 6.39 1.37 3.79 3.9Z" opacity=".62" style="mix-blend-mode:screen"/><path fill="url(#fW)" d="m2176.05 1824.29 100.55-99.98-2.16-2.17-100.55 99.98c-1.45 1.44.71 3.61 2.16 2.17Z" opacity=".38" style="mix-blend-mode:screen"/><path fill="url(#fX)" d="m2980.25 2970.84 9.56 29.16h2.74l-9.82-29.97-2.48.81z" opacity=".62" style="mix-blend-mode:screen"/><path fill="url(#fY)" d="m1118.21 709.11 361.84 205.81-4.45 7.82-361.84-205.81c-5.2-2.96-.76-10.78 4.45-7.82Z" opacity=".86" style="mix-blend-mode:screen"/><path fill="url(#fZ)" d="m3236.39 2980.03-166.9-337.41-7.29 3.61 166.9 337.41c2.4 4.85 9.69 1.25 7.29-3.61Z" opacity=".46" style="mix-blend-mode:screen"/><path fill="url(#ga)" d="m4983.23 2632.46-474.56-217.7-4.7 10.25 474.56 217.7c6.82 3.13 11.53-7.12 4.7-10.25Z" opacity=".4" style="mix-blend-mode:screen"/><path fill="url(#gb)" d="M2699.25 251.47 2738.65 0h-7.24l-39.22 250.37 7.06 1.1z" opacity=".43" style="mix-blend-mode:screen"/><path fill="url(#gc)" d="M1443.7 2417.73 785.69 3000h38.59l636.37-563.12-16.95-19.15z" opacity=".34" style="mix-blend-mode:screen"/><path fill="url(#gd)" d="m2869.14 1126.03-47.18 47.93-1.04-1.02 47.18-47.93c.68-.69 1.71.33 1.04 1.02Z" opacity=".59" style="mix-blend-mode:screen"/><path fill="url(#ge)" d="m4852.97 551.11-388.6 157.67-3.41-8.4 388.6-157.67c5.59-2.27 9 6.13 3.41 8.4Z" opacity=".53" style="mix-blend-mode:screen"/><path fill="url(#gf)" d="m2190.09 2574.03 127.44-434.42-9.39-2.75-127.44 434.42c-1.83 6.24 7.55 9 9.39 2.75Z" opacity=".55" style="mix-blend-mode:screen"/><path fill="url(#gg)" d="m2082.06 2909.09-26.5 90.91h15.48l25.28-86.76-14.26-4.15z" style="mix-blend-mode:screen"/><path fill="url(#gh)" d="m617.27 1522.62 276.36-2.9-.06-5.97-276.36 2.9c-3.97.04-3.91 6.01.06 5.97Z" opacity=".39" style="mix-blend-mode:screen"/><path fill="url(#gi)" d="m1023.54 1260.61 153.98 25.15.54-3.33-153.98-25.15c-2.21-.36-2.76 2.97-.54 3.33Z" opacity=".39" style="mix-blend-mode:screen"/><path fill="url(#gj)" d="m1678.49 143.07 56.29 92.8-2.01 1.22-56.29-92.8c-.81-1.33 1.2-2.55 2.01-1.22Z" opacity=".41" style="mix-blend-mode:screen"/><path fill="url(#gk)" d="m1081.1 2773.77 443.42-395.38-8.54-9.58-443.42 395.38c-6.37 5.68 2.16 15.27 8.54 9.58Z" opacity=".96" style="mix-blend-mode:screen"/><path fill="url(#gl)" d="m2627.12 2711.64-21.9-212.51-4.59.47 21.9 212.51c.31 3.05 4.91 2.58 4.59-.47Z" opacity=".79" style="mix-blend-mode:screen"/><path fill="url(#gm)" d="m4787.41 19.11-119.25 77.3-1.67-2.58 119.25-77.3c1.71-1.11 3.39 1.46 1.67 2.58Z" opacity=".27" style="mix-blend-mode:screen"/><path fill="url(#gn)" d="m1655.6 1321.95 73.41 15.55.34-1.59-73.41-15.55c-1.06-.22-1.39 1.36-.34 1.59Z" opacity=".29" style="mix-blend-mode:screen"/><path fill="url(#go)" d="m3553.32 2834.72-106.42-135.16-2.92 2.3 106.42 135.16c1.53 1.94 4.45-.36 2.92-2.3Z" opacity=".6" style="mix-blend-mode:screen"/><path fill="url(#gp)" d="m1626.78 2423.34 191.58-201.6-4.36-4.14-191.58 201.6c-2.75 2.9 1.6 7.04 4.36 4.14Z" opacity=".58" style="mix-blend-mode:screen"/><path fill="url(#gq)" d="M4272.31 42.96 4324.46 0h-7.53l-47.66 39.27 3.04 3.69z" opacity=".37" style="mix-blend-mode:screen"/><path fill="url(#gr)" d="m2871.05 2913.87-96.76-372.74-8.05 2.09 96.76 372.74c1.39 5.36 9.45 3.27 8.05-2.09Z" opacity=".38" style="mix-blend-mode:screen"/><path fill="url(#gs)" d="M3780.75 296.61 4094.71 0h-13.91l-306.6 289.67 6.55 6.94z" opacity=".74" style="mix-blend-mode:screen"/><path fill="url(#gt)" d="m1336.5 2789.53 109.24-120.82-2.61-2.36-109.24 120.82c-1.57 1.74 1.04 4.1 2.61 2.36Z" opacity=".32" style="mix-blend-mode:screen"/><path fill="url(#gu)" d="m2135.34 1682.2 84.57-42-.91-1.83-84.57 42c-1.22.6-.31 2.43.91 1.83Z" opacity=".86" style="mix-blend-mode:screen"/><path fill="url(#gv)" d="m2649.1 2507.96-19.24-131.24-2.84.42 19.24 131.24c.28 1.89 3.11 1.47 2.84-.42Z" opacity=".89" style="mix-blend-mode:screen"/><path fill="url(#gw)" d="M1586.75 2873.29 1502.92 3000h9.59l80.91-122.29-6.67-4.42z" opacity=".65" style="mix-blend-mode:screen"/><path fill="url(#gx)" d="m4363.13 2599.69-323.29-190.08 4.11-6.99 323.29 190.08c4.65 2.73.54 9.72-4.11 6.99Z" opacity=".95" style="mix-blend-mode:screen"/><path fill="url(#gy)" d="m4839.47 2799.88-449.04-248.44 5.37-9.7 449.04 248.44c6.45 3.57 1.09 13.28-5.37 9.7Z" opacity=".44" style="mix-blend-mode:screen"/><path fill="url(#gz)" d="M2413.59 149.76 2403.38 0h-10.83l10.26 150.5 10.78-.74z" opacity=".43" style="mix-blend-mode:screen"/><path fill="url(#gA)" d="M555.84 2383.88 0 2638.84v15.19l561.6-257.6-5.76-12.55z" style="mix-blend-mode:screen"/><path fill="url(#gB)" d="m3553.03 1742.63-224.66-51.24 1.11-4.85 224.66 51.24c3.23.74 2.12 5.59-1.11 4.85Z" opacity=".91" style="mix-blend-mode:screen"/><path fill="url(#gC)" d="m3220.51 1382.38-57.43 9.43-.2-1.24 57.43-9.43c.83-.14 1.03 1.11.2 1.24Z" opacity=".38" style="mix-blend-mode:screen"/><path fill="url(#gD)" d="m2483.11 2962.07-.19 37.93h18.51l.19-37.83-18.51-.1z" opacity=".57" style="mix-blend-mode:screen"/><path fill="url(#gE)" d="M4320.93 2190.99 5000 2445.46v-17.1l-673.45-252.37-5.62 15z" opacity=".28" style="mix-blend-mode:screen"/><path fill="url(#gF)" d="M3832.91 2674.68 4205.06 3000h22.08l-384.67-336.26-9.56 10.94z" opacity=".46" style="mix-blend-mode:screen"/><path fill="url(#gG)" d="m653.87 1637.96 509.01-36.5-.79-11-509.01 36.5c-7.32.52-6.53 11.52.79 11Z" opacity=".79" style="mix-blend-mode:screen"/><path fill="url(#gH)" d="m3724.69 2920.12-333.73-384.7 8.31-7.21 333.73 384.7c4.8 5.53-3.51 12.75-8.31 7.21Z" opacity=".84" style="mix-blend-mode:screen"/><path fill="url(#gI)" d="m4418.51 1274.3-545.37 65.85-1.42-11.78 545.37-65.85c7.84-.95 9.27 10.84 1.42 11.78Z" opacity=".88" style="mix-blend-mode:screen"/><path fill="url(#gJ)" d="m1384.19 998.69 258.54 116.93 2.53-5.59-258.54-116.93c-3.72-1.68-6.25 3.9-2.53 5.59Z" opacity=".69" style="mix-blend-mode:screen"/><path fill="url(#gK)" d="m1334.03 1699.59 314.47-52.92-1.14-6.8-314.47 52.92c-4.52.76-3.38 7.56 1.14 6.8Z" opacity=".39" style="mix-blend-mode:screen"/><path fill="url(#gL)" d="m4774.97 2466.71-930.78-390.6 8.44-20.11 930.78 390.6c13.38 5.61 4.95 25.73-8.44 20.11Z" opacity=".59" style="mix-blend-mode:screen"/><path fill="url(#gM)" d="m1077.75 2784.24 529.59-474.38-10.25-11.44-529.59 474.38c-7.61 6.82 2.63 18.27 10.25 11.44Z" opacity=".28" style="mix-blend-mode:screen"/><path fill="url(#gN)" d="m3893.08 2526.4-293.18-214.98 4.65-6.34 293.18 214.98c4.21 3.09-.43 9.43-4.65 6.34Z" opacity=".83" style="mix-blend-mode:screen"/><path fill="url(#gO)" d="m3987.95 2618.69-367.57-274.96 5.94-7.94 367.57 274.96c5.28 3.95-.65 11.9-5.94 7.94Z" opacity=".49" style="mix-blend-mode:screen"/><path fill="url(#gP)" d="m1040.68 2141.29 344.57-150.38-3.25-7.45-344.57 150.38c-4.95 2.16-1.71 9.61 3.25 7.45Z" opacity=".52" style="mix-blend-mode:screen"/><path fill="url(#gQ)" d="m2060.71 1173 52.92 39.51.85-1.14-52.92-39.51c-.76-.57-1.61.58-.85 1.14Z" opacity=".98" style="mix-blend-mode:screen"/><path fill="url(#gR)" d="m2121.71 695.96 41.9 89.29 1.93-.91-41.9-89.29c-.6-1.28-2.53-.38-1.93.91Z" opacity=".71" style="mix-blend-mode:screen"/><path fill="url(#gS)" d="m3270.27 2218.68-394.34-363.91 7.86-8.52 394.34 363.91c5.67 5.23-2.19 13.76-7.86 8.52Z" opacity=".83" style="mix-blend-mode:screen"/><path fill="url(#gT)" d="m4540.5 2962.7-325.04-232.19 5.02-7.02 325.04 232.19c4.67 3.34-.34 10.36-5.02 7.02Z" opacity=".64" style="mix-blend-mode:screen"/><path fill="url(#gU)" d="m4647.08 1011-318.99 73.3-1.58-6.89 318.99-73.3c4.58-1.05 6.17 5.84 1.58 6.89Z" opacity=".47" style="mix-blend-mode:screen"/><path fill="url(#gV)" d="m4629.4 958.4-254.14 65.04-1.41-5.49 254.14-65.04c3.65-.93 5.06 4.56 1.41 5.49Z" opacity=".73" style="mix-blend-mode:screen"/><path fill="url(#gW)" d="m1996.77 616.62 31.43 55.24 1.19-.68-31.43-55.24c-.45-.79-1.65-.12-1.19.68Z" opacity=".28" style="mix-blend-mode:screen"/><path fill="url(#gX)" d="M4696.66 982.42 5000 909.27v-25.04l-309.05 74.53 5.71 23.66z" style="mix-blend-mode:screen"/><path fill="url(#gY)" d="m1226.13 1363.41 229.01 25.03.54-4.95-229.01-25.03c-3.29-.36-3.84 4.59-.54 4.95Z" opacity=".71" style="mix-blend-mode:screen"/><path fill="url(#gZ)" d="m2003.03 1523.74 73.97-3.42-.07-1.6-73.97 3.42c-1.06.05-.99 1.65.07 1.6Z" opacity=".48" style="mix-blend-mode:screen"/><path fill="url(#ha)" d="M3077.16 2641.79 3261.02 3000h16.68l-187.33-364.98-13.21 6.77z" opacity=".63" style="mix-blend-mode:screen"/><path fill="url(#hb)" d="m205.72 285.53 208.69 110.72 2.39-4.51-208.69-110.72c-3-1.59-5.39 2.92-2.39 4.51Z" opacity=".56" style="mix-blend-mode:screen"/><path fill="url(#hc)" d="m3689.53 1529.29-426.45-8.85.19-9.22 426.45 8.85c6.13.13 5.94 9.34-.19 9.21Z" opacity=".58" style="mix-blend-mode:screen"/><path fill="url(#hd)" d="m1948.83 799.74 229.7 294.55 6.36-4.96-229.7-294.55c-3.3-4.23-9.67.73-6.36 4.96Z" opacity=".6" style="mix-blend-mode:screen"/><path fill="url(#he)" d="m2650.48 127.65-26.62 238.57 5.16.58 26.62-238.57c.38-3.43-4.77-4.01-5.16-.58Z" opacity=".64" style="mix-blend-mode:screen"/><path fill="url(#hf)" d="m2089.92 2765.09 83.33-259.06 5.6 1.8-83.33 259.06c-1.2 3.72-6.8 1.93-5.6-1.8Z" opacity=".95" style="mix-blend-mode:screen"/><path fill="url(#hg)" d="M2125.85 434.9 1970.83 0h-10.91l156.25 438.35 9.68-3.45z" opacity=".54" style="mix-blend-mode:screen"/><path fill="url(#hh)" d="m2419.01 843.9 17.88 147.57 3.19-.39-17.88-147.57c-.26-2.12-3.45-1.74-3.19.39Z" opacity=".4" style="mix-blend-mode:screen"/><path fill="url(#hi)" d="m2300.04 1399.55 37.97 18.99-.41.82-37.97-18.99c-.55-.27-.14-1.09.41-.82Z" opacity=".93" style="mix-blend-mode:screen"/><path fill="url(#hj)" d="m2796.12 1695.68-66.11-43.94-.95 1.43 66.11 43.94c.95.63 1.9-.8.95-1.43Z" opacity=".87" style="mix-blend-mode:screen"/><path fill="url(#hk)" d="M1897.33 138.79 1835.62 0h-4.25l62.41 140.37 3.55-1.58z" opacity=".94" style="mix-blend-mode:screen"/><path fill="url(#hl)" d="M3014.83 2681.27 2858 2318.21l-7.85 3.39 156.83 363.06c2.25 5.22 10.1 1.83 7.85-3.39Z" opacity=".55" style="mix-blend-mode:screen"/><path fill="url(#hm)" d="m2597.69 1203.59-22.66 69.34-1.5-.49 22.66-69.34c.33-1 1.82-.51 1.5.49Z" opacity=".45" style="mix-blend-mode:screen"/><path fill="url(#hn)" d="m4606.54 2328.81-383.02-151.67-3.28 8.28 383.02 151.67c5.51 2.18 8.79-6.09 3.28-8.28Z" opacity=".44" style="mix-blend-mode:screen"/><path fill="url(#ho)" d="M338.95 561.59 0 417.93v45.81l322.5 136.68 16.45-38.83z" opacity=".74" style="mix-blend-mode:screen"/><path fill="url(#hp)" d="m1254.23 2920.99 218.64-248.41-5.37-4.72-218.64 248.41c-3.14 3.57 2.22 8.3 5.37 4.72Z" opacity=".63" style="mix-blend-mode:screen"/><path fill="url(#hq)" d="m2203.43 1364.96 78.23 35.38-.76 1.69-78.23-35.38c-1.12-.51-.36-2.2.76-1.69Z" opacity=".79" style="mix-blend-mode:screen"/><path fill="url(#hr)" d="m2345.44 2835.46 49.66-416.72-9-1.07-49.66 416.72c-.71 5.99 8.29 7.07 9 1.07Z" opacity=".73" style="mix-blend-mode:screen"/><path fill="url(#hs)" d="m1982.46 1546.09 134.83-12.39.27 2.91-134.83 12.39c-1.94.18-2.21-2.74-.27-2.91Z" opacity=".86" style="mix-blend-mode:screen"/><path fill="url(#ht)" d="m1819.38 2172.88 136.73-134.59-2.91-2.95-136.73 134.59c-1.97 1.93.94 4.89 2.91 2.95Z" opacity=".9" style="mix-blend-mode:screen"/><path fill="url(#hu)" d="m1843.83 1092.73 95.21 58.92-1.27 2.06-95.21-58.92c-1.37-.85-.1-2.9 1.27-2.06Z" opacity=".64" style="mix-blend-mode:screen"/><path fill="url(#hv)" d="m3119.51 2819.24-143.62-307.44-6.64 3.1 143.62 307.44c2.06 4.42 8.71 1.32 6.64-3.1Z" opacity=".59" style="mix-blend-mode:screen"/><path fill="url(#hw)" d="m3549.22 2993.57-82.56-117.71-2.54 1.78 82.56 117.71c1.19 1.69 3.73-.09 2.54-1.78Z" opacity=".38" style="mix-blend-mode:screen"/><path fill="url(#hx)" d="m1364.26 1080.98 144.78 53.22-1.15 3.13-144.78-53.22c-2.08-.76-.93-3.89 1.15-3.13Z" opacity=".83" style="mix-blend-mode:screen"/><path fill="url(#hy)" d="m4213.85 2326.71-194.08-93.91-2.03 4.19 194.08 93.92c2.79 1.35 4.82-2.84 2.03-4.19Z" opacity=".34" style="mix-blend-mode:screen"/><path fill="url(#hz)" d="m1040.04 381.76 453.46 344.93-7.45 9.8-453.46-344.93c-6.52-4.96.93-14.76 7.45-9.8Z" opacity=".38" style="mix-blend-mode:screen"/><path fill="url(#hA)" d="m3117.07 1886.95-127.08-80.09-1.73 2.75 127.08 80.09c1.83 1.15 3.56-1.59 1.73-2.75Z" opacity=".39" style="mix-blend-mode:screen"/><path fill="url(#hB)" d="m2410.15 1621.78 12.13-16.38-.35-.26-12.13 16.38c-.17.24.18.5.35.26Z" opacity=".37" style="mix-blend-mode:screen"/><path fill="url(#hC)" d="M473.61 353.08 0 88.74v32.9l459.61 256.53 14-25.09z" opacity=".56" style="mix-blend-mode:screen"/><path fill="url(#hD)" d="m2893.91 2736.45-123.59-392.46-8.48 2.67 123.59 392.46c1.78 5.64 10.26 2.98 8.48-2.67Z" opacity=".82" style="mix-blend-mode:screen"/><path fill="url(#hE)" d="m2744.32 1190.66-39.15 49.74-1.07-.85 39.15-49.74c.56-.71 1.64.13 1.07.85Z" opacity=".87" style="mix-blend-mode:screen"/><path fill="url(#hF)" d="m2341.07 2107.71 63.23-237.66-5.14-1.37-63.23 237.66c-.91 3.42 4.23 4.79 5.14 1.37Z" opacity=".6" style="mix-blend-mode:screen"/><path fill="url(#hG)" d="m1950.92 1145.87 206.5 132.03-2.85 4.46-206.5-132.03c-2.97-1.9-.12-6.36 2.85-4.46Z" opacity=".91" style="mix-blend-mode:screen"/><path fill="url(#hH)" d="m2261.13 2440.06 45.04-175.65-3.8-.97-45.04 175.65c-.65 2.52 3.15 3.5 3.8.97Z" opacity=".7" style="mix-blend-mode:screen"/><path fill="url(#hI)" d="m3277.83 784.99-104.34 96.19-2.08-2.25 104.34-96.19c1.5-1.38 3.58.87 2.08 2.25Z" opacity=".55" style="mix-blend-mode:screen"/><path fill="url(#hJ)" d="m3840.95 2407.25-493.93-337.15-7.29 10.67 493.93 337.15c7.1 4.85 14.39-5.82 7.29-10.67Z" opacity=".74" style="mix-blend-mode:screen"/><path fill="url(#hK)" d="m2740.8 708.6-48.99 162.22-3.51-1.06 48.99-162.22c.7-2.33 4.21-1.28 3.51 1.06Z" opacity=".26" style="mix-blend-mode:screen"/><path fill="url(#hL)" d="M3239.73 2757.02 3387.49 3000h48.83l-160.94-264.66-35.65 21.68z" opacity=".98" style="mix-blend-mode:screen"/><path fill="url(#hM)" d="m1717.92 1245.99 198.85 64.03-1.38 4.3-198.85-64.03c-2.86-.92-1.48-5.22 1.38-4.3Z" opacity=".31" style="mix-blend-mode:screen"/><path fill="url(#hN)" d="M4241.49 2819.02 4481.2 3000h11.36l-246.95-186.45-4.12 5.47z" opacity=".49" style="mix-blend-mode:screen"/><path fill="url(#hO)" d="m4016.18 808.88-286.24 131.31-2.84-6.19 286.24-131.31c4.11-1.89 6.96 4.3 2.84 6.19Z" opacity=".33" style="mix-blend-mode:screen"/><path fill="url(#hP)" d="m4434.13 1108.97-733.25 151.42-3.27-15.84 733.25-151.42c10.54-2.18 13.82 13.67 3.27 15.84Z" opacity=".98" style="mix-blend-mode:screen"/><path fill="url(#hQ)" d="m2151.11 1583.97 54.16-12.94-.28-1.17-54.16 12.94c-.78.19-.5 1.36.28 1.17Z" opacity=".26" style="mix-blend-mode:screen"/><path fill="url(#hR)" d="m2999.61 995.36-52.65 53.3-1.15-1.14 52.65-53.3c.76-.77 1.91.37 1.15 1.14Z" opacity=".62" style="mix-blend-mode:screen"/><path fill="url(#hS)" d="M248.68 2384.9 0 2483.17v9.07l251.78-99.49-3.1-7.85z" opacity=".76" style="mix-blend-mode:screen"/><path fill="url(#hT)" d="M943.47 229.45 1426 620.64l-8.45 10.43-482.53-391.19c-6.94-5.62 1.51-16.05 8.45-10.43Z" opacity=".76" style="mix-blend-mode:screen"/><path fill="url(#hU)" d="m2538.03 2157.45-7.54-135.43-2.93.16 7.54 135.43c.11 1.95 3.03 1.79 2.93-.16Z" opacity=".81" style="mix-blend-mode:screen"/><path fill="url(#hV)" d="M2286.22 71.37 2275.3 0h-9.53l11.14 72.79 9.31-1.42z" opacity=".65" style="mix-blend-mode:screen"/><path fill="url(#hW)" d="m2235.84 2946.16-9.64 53.84h11.07l9.3-51.92-10.73-1.92z" opacity=".36" style="mix-blend-mode:screen"/><path fill="url(#hX)" d="M3955.6 59.73 4015.42 0h-26.44l-46.58 46.51 13.2 13.22z" opacity=".27" style="mix-blend-mode:screen"/><path fill="url(#hY)" d="m3435.87 2877.06 84.06 122.94h11.51l-87.73-128.3-7.84 5.36z" opacity=".25" style="mix-blend-mode:screen"/><path fill="url(#hZ)" d="m2462.63 2702.94 13.79-397.87-8.6-.3-13.79 397.87c-.2 5.72 8.4 6.02 8.6.3Z" opacity=".91" style="mix-blend-mode:screen"/><path fill="url(#ia)" d="m2754.15 431.9-63.8 271.5-5.87-1.38 63.8-271.5c.92-3.9 6.78-2.53 5.87 1.38Z" opacity=".4" style="mix-blend-mode:screen"/><path fill="url(#ib)" d="m3550.25 888.4-161.5 94.45-2.04-3.49 161.5-94.45c2.32-1.36 4.36 2.13 2.04 3.49Z" opacity=".56" style="mix-blend-mode:screen"/><path fill="url(#ic)" d="M1911.19 2675.08 1749.5 3000h8.97l159.9-321.35-7.18-3.57z" opacity=".34" style="mix-blend-mode:screen"/><path fill="url(#id)" d="m1864.14 588.39 61.24 87.6-1.89 1.32-61.24-87.6c-.88-1.26 1.01-2.58 1.89-1.32Z" opacity=".58" style="mix-blend-mode:screen"/><path fill="url(#ie)" d="m3213.04 663.45-236.9 279.92-6.05-5.12 236.9-279.92c3.4-4.02 9.46 1.09 6.05 5.12Z" opacity=".89" style="mix-blend-mode:screen"/><path fill="url(#if)" d="m1892 1721.24 170.82-61.55-1.33-3.69-170.82 61.55c-2.46.88-1.13 4.58 1.33 3.69Z" opacity=".89" style="mix-blend-mode:screen"/><path fill="url(#ig)" d="m3683.86 1605-115.89-10.16.22-2.5 115.89 10.16c1.67.15 1.45 2.65-.22 2.5Z" opacity=".7" style="mix-blend-mode:screen"/><path fill="url(#ih)" d="m4137.8 588.08-794.53 447.84-9.68-17.17 794.53-447.84c11.42-6.44 21.11 10.73 9.68 17.17Z" opacity=".55" style="mix-blend-mode:screen"/><path fill="url(#ii)" d="m1469.29 1643.9 189.87-26.15-.57-4.1-189.87 26.15c-2.73.38-2.17 4.48.57 4.1Z" opacity=".75" style="mix-blend-mode:screen"/><path fill="url(#ij)" d="m4583.84 1510.45-807.75-.66v-17.45l807.76.66c11.61 0 11.61 17.46-.01 17.45Z" opacity=".48" style="mix-blend-mode:screen"/><path fill="url(#ik)" d="m1616.32 1438.86 66.19 4.64.1-1.43-66.19-4.64c-.95-.07-1.05 1.36-.1 1.43Z" opacity=".84" style="mix-blend-mode:screen"/><path fill="url(#il)" d="m4557.78 1653.07 442.22 31.16v-16.17l-441.09-31.09-1.13 16.1z" opacity=".69" style="mix-blend-mode:screen"/><path fill="url(#im)" d="m4017.95 1632.85-388.49-32.91.71-8.39 388.49 32.91c5.58.47 4.88 8.87-.71 8.39Z" opacity=".4" style="mix-blend-mode:screen"/><path fill="url(#in)" d="m1316.83 1571.32 100.63-5.97-.13-2.17-100.63 5.97c-1.45.09-1.32 2.26.13 2.17Z" opacity=".31" style="mix-blend-mode:screen"/><path fill="url(#io)" d="m2624.12 433.51-26.45 231.93-5.01-.57 26.45-231.93c.38-3.33 5.39-2.76 5.01.57Z" opacity=".95" style="mix-blend-mode:screen"/><path fill="url(#ip)" d="m2619.27 1788.67-27.22-66.34-1.43.59 27.22 66.34c.39.95 1.82.37 1.43-.59Z" opacity=".57" style="mix-blend-mode:screen"/><path fill="url(#iq)" d="m3938.12 1086.38-336.79 97.87-2.11-7.28 336.79-97.87c4.84-1.41 6.96 5.87 2.11 7.28Z" opacity=".64" style="mix-blend-mode:screen"/><path fill="url(#ir)" d="M1969.05 185.18 1893.87 0h-5.88l76.02 187.22 5.04-2.04z" opacity=".87" style="mix-blend-mode:screen"/><path fill="url(#is)" d="m33.54 989.21 432.74 90.55 1.96-9.35L35.5 979.86c-6.22-1.3-8.18 8.05-1.96 9.35Z" opacity=".39" style="mix-blend-mode:screen"/><path fill="url(#it)" d="m2386.43 2648.52 24.7-243.91-5.27-.53-24.7 243.91c-.35 3.51 4.92 4.04 5.27.53Z" opacity=".9" style="mix-blend-mode:screen"/><path fill="url(#iu)" d="m1693.36 1593.12 110.62-12.62-.27-2.39-110.62 12.62c-1.59.18-1.32 2.57.27 2.39Z" opacity=".63" style="mix-blend-mode:screen"/><path fill="url(#iv)" d="m2620.25 983.27-26.4 114.6-2.48-.57 26.4-114.6c.38-1.65 2.86-1.08 2.48.57Z" opacity=".3" style="mix-blend-mode:screen"/><path fill="url(#iw)" d="m1860.86 1641.16 84.13-18.48-.4-1.82-84.13 18.48c-1.21.27-.81 2.08.4 1.82Z" opacity=".76" style="mix-blend-mode:screen"/><path fill="url(#ix)" d="m1853.2 1514.44 182.77-3.52-.08-3.95-182.77 3.52c-2.63.05-2.55 4 .08 3.95Z" opacity=".81" style="mix-blend-mode:screen"/><path fill="url(#iy)" d="m4405.61 2613.91-408.19-237.52 5.13-8.82 408.19 237.52c5.87 3.41.74 12.24-5.13 8.82Z" opacity=".65" style="mix-blend-mode:screen"/><path fill="url(#iz)" d="m529.04 2060.68 460.01-129.6-2.8-9.94-460.01 129.6c-6.61 1.86-3.82 11.8 2.8 9.94Z" opacity=".26" style="mix-blend-mode:screen"/><path fill="url(#iA)" d="m3093.27 1347.83-73.25 18.91-.41-1.58 73.25-18.91c1.05-.27 1.46 1.31.41 1.58Z" opacity=".64" style="mix-blend-mode:screen"/><path fill="url(#iB)" d="M320.55 1473.91 0 1470.73v8.94l320.46 3.18.09-8.94z" opacity=".89" style="mix-blend-mode:screen"/><path fill="url(#iC)" d="m4769.27 1929.67 230.73 43.37v-7.24l-229.42-43.13-1.31 7z" opacity=".77" style="mix-blend-mode:screen"/><path fill="url(#iD)" d="m2455.42 2113.31 5.42-73.21-1.58-.12-5.42 73.21c-.08 1.05 1.5 1.17 1.58.12Z" opacity=".86" style="mix-blend-mode:screen"/><path fill="url(#iE)" d="m2563.83 830.31-22.01 240.78-5.2-.48 22.01-240.78c.32-3.46 5.52-2.99 5.2.48Z" opacity=".93" style="mix-blend-mode:screen"/><path fill="url(#iF)" d="m2380.82 2062.9 34.36-159.79-3.45-.74-34.36 159.79c-.49 2.3 2.96 3.04 3.45.74Z" opacity=".94" style="mix-blend-mode:screen"/><path fill="url(#iG)" d="m638.19 1371.31 215.41 15.17.33-4.65-215.41-15.17c-3.1-.22-3.43 4.44-.33 4.65Z" opacity=".83" style="mix-blend-mode:screen"/><path fill="url(#iH)" d="m3738.45 784.8-181.33 105.14-2.27-3.92 181.33-105.14c2.61-1.51 4.88 2.41 2.27 3.92Z" opacity=".99" style="mix-blend-mode:screen"/><path fill="url(#iI)" d="m1902.8 2170.62 118.68-132.7-2.87-2.56-118.68 132.7c-1.71 1.91 1.16 4.47 2.87 2.56Z" opacity=".98" style="mix-blend-mode:screen"/><path fill="url(#iJ)" d="m2012.31 997.58 98.14 101.55 2.19-2.12-98.14-101.55c-1.41-1.46-3.61.66-2.19 2.12Z" opacity=".96" style="mix-blend-mode:screen"/><path fill="url(#iK)" d="m2871.99 1190.34-85.17 71.26-1.54-1.84 85.17-71.26c1.22-1.02 2.77.82 1.54 1.84Z" opacity=".65" style="mix-blend-mode:screen"/><path fill="url(#iL)" d="m2100.09 1690.64 35.54-16.9-.37-.77-35.54 16.9c-.51.24-.15 1.01.37.77Z" opacity=".43" style="mix-blend-mode:screen"/><path fill="url(#iM)" d="m3004.77 1389.77-117.02 25.88-.56-2.53 117.02-25.88c1.68-.37 2.24 2.16.56 2.53Z" opacity=".7" style="mix-blend-mode:screen"/><path fill="url(#iN)" d="m1989.08 1862.01 141.26-99.46-2.15-3.05-141.26 99.46c-2.03 1.43.12 4.48 2.15 3.05Z" opacity=".67" style="mix-blend-mode:screen"/><path fill="url(#iO)" d="m3184.14 1968.32-85.9-58.62 1.27-1.86 85.9 58.62c1.23.84-.03 2.7-1.27 1.86Z" opacity=".35" style="mix-blend-mode:screen"/><path fill="url(#iP)" d="m2425.88 1819.35 12.98-55.39-1.2-.28-12.98 55.39c-.19.8 1.01 1.08 1.2.28Z" opacity=".34" style="mix-blend-mode:screen"/><path fill="url(#iQ)" d="m2651.82 1423.77-47.5 24.07-.52-1.03 47.5-24.07c.68-.35 1.2.68.52 1.03Z" opacity=".67" style="mix-blend-mode:screen"/><path fill="url(#iR)" d="m1162.96 2932.46 431.93-459.52-9.93-9.33-431.93 459.52c-6.21 6.6 3.72 15.94 9.93 9.33Z" opacity=".85" style="mix-blend-mode:screen"/><path fill="url(#iS)" d="M3183.7 2343.29 3728.2 3000h30.9l-557.08-671.89-18.32 15.18z" opacity=".86" style="mix-blend-mode:screen"/><path fill="url(#iT)" d="m1167.33 1553.36 495.13-17.85-.39-10.7-495.13 17.85c-7.12.26-6.74 10.96.39 10.7Z" opacity=".26" style="mix-blend-mode:screen"/><path fill="url(#iU)" d="m2768.11 1777.46-68.05-70.04 1.51-1.47 68.05 70.04c.98 1.01-.53 2.48-1.51 1.47Z" opacity=".55" style="mix-blend-mode:screen"/><path fill="url(#iV)" d="M36.08 314.53 0 297.27v15.76l29.95 14.32 6.13-12.82z" opacity=".89" style="mix-blend-mode:screen"/><path fill="url(#iW)" d="m4226.85 1976.85-652.44-177.44 3.83-14.1 652.44 177.44c9.38 2.55 5.55 16.65-3.83 14.1Z" opacity=".77" style="mix-blend-mode:screen"/><path fill="url(#iX)" d="m509.89 1332.55 781.16 69.05 1.49-16.88-781.16-69.05c-11.23-.99-12.73 15.89-1.49 16.88Z" opacity=".44" style="mix-blend-mode:screen"/><path fill="url(#iY)" d="m3073.5 752.28-219.2 288.27-6.23-4.74 219.2-288.27c3.15-4.14 9.38.59 6.23 4.74Z" opacity=".78" style="mix-blend-mode:screen"/><path fill="url(#iZ)" d="m1059.39 251.44 336.72 293.35 6.34-7.28-336.72-293.35c-4.84-4.22-11.18 3.06-6.34 7.28Z" opacity=".85" style="mix-blend-mode:screen"/><path fill="url(#ja)" d="M1779.79 9.79 1775.01 0h-12.38l7.16 14.67 10-4.88z" opacity=".61" style="mix-blend-mode:screen"/><path fill="url(#jb)" d="m3163.37 2536.58-87.94-136.91 2.96-1.9 87.94 136.91c1.26 1.97-1.69 3.87-2.96 1.9Z" opacity=".55" style="mix-blend-mode:screen"/><path fill="url(#jc)" d="m2116.9 1114.77 69.11 69.77 1.51-1.49-69.11-69.77c-.99-1-2.5.49-1.51 1.49Z" opacity=".76" style="mix-blend-mode:screen"/><path fill="url(#jd)" d="m3127.2 1984.1-33.27-25.65.55-.72 33.27 25.65c.48.37-.08 1.09-.55.72Z" opacity=".66" style="mix-blend-mode:screen"/><path fill="url(#je)" d="m257.74 2174.99 379.49-113.53-2.45-8.2-379.49 113.53c-5.45 1.63-3.01 9.83 2.45 8.2Z" opacity=".57" style="mix-blend-mode:screen"/><path fill="url(#jf)" d="m2699.82 2776.56 35.47 223.44h5.24l-35.6-224.25-5.11.81z" opacity=".96" style="mix-blend-mode:screen"/><path fill="url(#jg)" d="M630.19 387.23 0 16.82v28.73l617.64 363.04 12.55-21.36z" opacity=".53" style="mix-blend-mode:screen"/><path fill="url(#jh)" d="m3014.69 1299.3-116.38 45.73-.99-2.51 116.38-45.73c1.67-.66 2.66 1.86.99 2.51Z" opacity=".98" style="mix-blend-mode:screen"/><path fill="url(#ji)" d="m810.52 1339.4 664.11 65.96 1.43-14.35-664.11-65.96c-9.55-.95-10.98 13.4-1.43 14.35Z" opacity=".7" style="mix-blend-mode:screen"/><path fill="url(#jj)" d="m2861.55 1527.07-55.89-4.1.09-1.21 55.89 4.09c.8.06.72 1.27-.09 1.21Z" opacity=".38" style="mix-blend-mode:screen"/><path fill="url(#jk)" d="m2102.71 1397.56 105.36 27.51.59-2.28-105.36-27.51c-1.51-.4-2.11 1.88-.59 2.28Z" opacity=".74" style="mix-blend-mode:screen"/><path fill="url(#jl)" d="m3304.58 2350.82-256.21-269.08 5.81-5.54 256.21 269.08c3.68 3.87-2.13 9.41-5.81 5.54Z" opacity=".6" style="mix-blend-mode:screen"/><path fill="url(#jm)" d="M3620.82 1867.73 3343 1777.19l1.96-6 277.82 90.54c3.99 1.3 2.04 7.31-1.96 6Z" opacity=".28" style="mix-blend-mode:screen"/><path fill="url(#jn)" d="m4382.53 1552.42-271.56-7.14.15-5.87 271.56 7.14c3.9.1 3.75 5.97-.15 5.87Z" opacity=".9" style="mix-blend-mode:screen"/><path fill="url(#jo)" d="m1384.09 1533.92 348.74-9.41-.2-7.54-348.74 9.41c-5.01.14-4.81 7.67.2 7.54Z" opacity=".67" style="mix-blend-mode:screen"/><path fill="url(#jp)" d="m2839.9 1573.36-60.61-12.96.28-1.31 60.61 12.96c.87.19.59 1.5-.28 1.31Z" opacity=".97" style="mix-blend-mode:screen"/><path fill="url(#jq)" d="m1748.56 934.52 155.67 117.71 2.54-3.36-155.67-117.71c-2.24-1.69-4.78 1.67-2.54 3.36Z" opacity=".78" style="mix-blend-mode:screen"/><path fill="url(#jr)" d="m2514.55 1849.8-4.17-93.65 2.02-.09 4.17 93.65c.06 1.35-1.96 1.44-2.02.09Z" opacity=".84" style="mix-blend-mode:screen"/><path fill="url(#js)" d="m346.65 1938.66 222.81-45.22-.98-4.81-222.81 45.22c-3.2.65-2.23 5.47.98 4.81Z" opacity=".34" style="mix-blend-mode:screen"/><path fill="url(#jt)" d="m4240.85 1072.64-236.7 58.55-1.27-5.11 236.7-58.55c3.4-.84 4.67 4.27 1.27 5.11Z" opacity=".42" style="mix-blend-mode:screen"/><path fill="url(#ju)" d="m691.95 1022.01 208.9 55.52 1.2-4.51-208.9-55.52c-3-.8-4.2 3.72-1.2 4.51Z" opacity=".94" style="mix-blend-mode:screen"/><path fill="url(#jv)" d="m2542.13 21.04.56-21.04h-6.5l-.55 20.87 6.49.17z" opacity=".81" style="mix-blend-mode:screen"/><path fill="url(#jw)" d="m4386.1 2641.02-650.33-390.38 8.44-14.05 650.33 390.38c9.35 5.61.92 19.67-8.44 14.05Z" opacity=".68" style="mix-blend-mode:screen"/><path fill="url(#jx)" d="m120.14 1076.2 199.43 35.72.77-4.31-199.43-35.72c-2.87-.51-3.64 3.8-.77 4.31Z" opacity=".61" style="mix-blend-mode:screen"/><path fill="url(#jy)" d="m3722.54 1266.46-310.09 60.23-1.3-6.7 310.09-60.23c4.46-.87 5.76 5.83 1.3 6.7Z" opacity=".48" style="mix-blend-mode:screen"/><path fill="url(#jz)" d="m1639.63 1553.29 177.34-10.58-.23-3.83-177.34 10.58c-2.55.15-2.32 3.98.23 3.83Z" opacity=".79" style="mix-blend-mode:screen"/><path fill="url(#jA)" d="m3593.53 2082.37-229.05-121.33 2.62-4.95 229.05 121.33c3.29 1.74.67 6.69-2.62 4.95Z" opacity=".68" style="mix-blend-mode:screen"/><path fill="url(#jB)" d="m1870.48 284.44 122.99 238.69 5.16-2.66-122.99-238.69c-1.77-3.43-6.93-.78-5.16 2.66Z" opacity=".93" style="mix-blend-mode:screen"/><path fill="url(#jC)" d="m3160.14 1514.72-245.55-6.46-.14 5.31 245.55 6.46c3.53.09 3.67-5.21.14-5.31Z" opacity=".61" style="mix-blend-mode:screen"/><path fill="url(#jD)" d="m2300.16 887.67 38.53 116.99-2.53.83-38.53-116.99c-.55-1.68 1.97-2.52 2.53-.83Z" opacity=".7" style="mix-blend-mode:screen"/><path fill="url(#jE)" d="m3350.77 1497.04-323.85-.2v7l323.84.2c4.65 0 4.66-7 0-7Z" opacity=".39" style="mix-blend-mode:screen"/><path fill="url(#jF)" d="m954.66 733.97 209.32 103.41-2.23 4.52-209.32-103.41c-3.01-1.49-.78-6.01 2.23-4.52Z" opacity=".56" style="mix-blend-mode:screen"/><path fill="url(#jG)" d="m3267.56 1795.58-193.25-75.01-1.62 4.18 193.25 75.01c2.78 1.08 4.4-3.1 1.62-4.18Z" opacity=".84" style="mix-blend-mode:screen"/><path fill="url(#jH)" d="m2815.38 663.58-115.79 310.85-6.72-2.5 115.79-310.85c1.66-4.47 8.38-1.97 6.72 2.5Z" opacity=".84" style="mix-blend-mode:screen"/><path fill="url(#jI)" d="m3069.75 2230.78-173.58-224.12-4.84 3.75 173.58 224.12c2.49 3.22 7.34-.53 4.84-3.75Z" opacity=".59" style="mix-blend-mode:screen"/><path fill="url(#jJ)" d="m1985.09 1215.29 147.37 80.96-1.75 3.18-147.37-80.96c-2.12-1.16-.37-4.35 1.75-3.18Z" opacity=".65" style="mix-blend-mode:screen"/><path fill="url(#jK)" d="m3219.53 1471.25-97.76 3.76.08 2.11 97.76-3.76c1.41-.05 1.32-2.17-.08-2.11Z" opacity=".52" style="mix-blend-mode:screen"/><path fill="url(#jL)" d="m2418.37 1310.37 45.34 103.65-2.24.98-45.34-103.65c-.65-1.49 1.59-2.47 2.24-.98Z" opacity=".71" style="mix-blend-mode:screen"/><path fill="url(#jM)" d="m4282.45 2541.28-417.17-245.14-5.3 9.01 417.17 245.14c6 3.52 11.3-5.49 5.3-9.01Z" opacity=".49" style="mix-blend-mode:screen"/><path fill="url(#jN)" d="m3276.98 2441.24-209.31-255.09-5.51 4.52 209.31 255.09c3.01 3.67 8.52-.85 5.51-4.52Z" opacity=".57" style="mix-blend-mode:screen"/><path fill="url(#jO)" d="m2117.94 1441.2 81.15 12.3-.27 1.75-81.15-12.3c-1.17-.18-.9-1.93.27-1.75Z" opacity=".93" style="mix-blend-mode:screen"/><path fill="url(#jP)" d="M302.47 500.65 0 364.25v20.59l294.76 132.92 7.71-17.11z" opacity=".75" style="mix-blend-mode:screen"/><path fill="url(#jQ)" d="m2318.23 1991.49 32.65-87.76-1.9-.71-32.65 87.76c-.47 1.26 1.43 1.97 1.9.71Z" opacity=".31" style="mix-blend-mode:screen"/><path fill="url(#jR)" d="M3202.93 122.75 2965.6 591.84l-10.14-5.13 237.33-469.09c3.41-6.74 13.55-1.62 10.14 5.13Z" opacity=".58" style="mix-blend-mode:screen"/><path fill="url(#jS)" d="m1199.98 2335.46 313.44-200.33-4.33-6.77-313.44 200.33c-4.51 2.88-.18 9.65 4.33 6.77Z" opacity=".82" style="mix-blend-mode:screen"/><path fill="url(#jT)" d="m2243.42 1051.14 97.56 169.03-3.65 2.11-97.56-169.03c-1.4-2.43 2.25-4.54 3.65-2.11Z" opacity=".27" style="mix-blend-mode:screen"/><path fill="url(#jU)" d="m1901.54 2230.34 198.59-240.52-5.2-4.29-198.59 240.52c-2.85 3.46 2.34 7.75 5.2 4.29Z" opacity=".97" style="mix-blend-mode:screen"/><path fill="url(#jV)" d="m2219.65 2387.38 44.34-139.39-3.01-.96-44.34 139.39c-.64 2 2.37 2.96 3.01.96Z" opacity=".69" style="mix-blend-mode:screen"/><path fill="url(#jW)" d="m2590.09 1446.73-20.71 12.31-.27-.45 20.71-12.31c.3-.18.56.27.27.45Z" opacity=".84" style="mix-blend-mode:screen"/><path fill="url(#jX)" d="m1971.83 1720.7 37.99-15.84-.34-.82-37.99 15.84c-.55.23-.2 1.05.34.82Z" opacity=".86" style="mix-blend-mode:screen"/><path fill="url(#jY)" d="M4265.19 327.58 4754.19 0h-29.95l-468.33 313.73 9.28 13.85z" opacity=".72" style="mix-blend-mode:screen"/><path fill="url(#jZ)" d="m2566.16 2246.49-8.27-94.69-2.05.18 8.27 94.69c.12 1.36 2.17 1.18 2.05-.18Z" opacity=".87" style="mix-blend-mode:screen"/><path fill="url(#ka)" d="m1800.08 892.86 149.34 128.95-2.79 3.23-149.34-128.95c-2.15-1.85.64-5.08 2.79-3.23Z" opacity=".77" style="mix-blend-mode:screen"/><path fill="url(#kb)" d="m3885.46 2154.35-184.34-87.39-1.89 3.98 184.34 87.39c2.65 1.26 4.54-2.73 1.89-3.98Z" opacity=".98" style="mix-blend-mode:screen"/><path fill="url(#kc)" d="m4288.26 659.03-362.81 171.64-3.71-7.84 362.81-171.64c5.21-2.47 8.93 5.37 3.71 7.84Z" opacity=".67" style="mix-blend-mode:screen"/><path fill="url(#kd)" d="m2867.89 2186.31-62.91-117.82-2.55 1.36 62.91 117.82c.9 1.69 3.45.34 2.55-1.36Z" opacity=".28" style="mix-blend-mode:screen"/><path fill="url(#ke)" d="m1973.59 1956.39 104.16-89.92-1.94-2.25-104.16 89.92c-1.5 1.29.44 3.54 1.94 2.25Z" opacity=".49" style="mix-blend-mode:screen"/><path fill="url(#kf)" d="m3502.72 1445.99-172.62 9.63-.21-3.73 172.62-9.63c2.48-.14 2.69 3.59.21 3.73Z" opacity=".29" style="mix-blend-mode:screen"/><path fill="url(#kg)" d="m2726.8 1897.42-58.39-102.98-2.23 1.26 58.39 102.98c.84 1.48 3.07.22 2.23-1.26Z" opacity=".45" style="mix-blend-mode:screen"/><path fill="url(#kh)" d="m2294.72 1244.52 76.93 94.94-2.05 1.66-76.93-94.94c-1.11-1.36.94-3.03 2.05-1.66Z" opacity=".53" style="mix-blend-mode:screen"/><path fill="url(#ki)" d="M0 1492.69h503.32v13.59H0z" opacity=".56" style="mix-blend-mode:screen"/><path fill="url(#kj)" d="M1750.96 791.22 1945.1 973.9l-3.95 4.2-194.14-182.68c-2.79-2.63 1.15-6.82 3.95-4.2Z" opacity=".54" style="mix-blend-mode:screen"/><path fill="url(#kk)" d="m2691.85 2687.12 51.62 312.88h8.32l-51.85-314.22-8.09 1.34z" opacity=".59" style="mix-blend-mode:screen"/><path fill="url(#kl)" d="m2458.57 19.63 13.54 434.21-9.38.29-13.55-434.21c-.19-6.24 9.19-6.54 9.38-.29Z" opacity=".81" style="mix-blend-mode:screen"/><path fill="url(#km)" d="m424.28 1465.21-424.28-5v20.64l424.04 5 .24-20.64z" opacity=".76" style="mix-blend-mode:screen"/><path fill="url(#kn)" d="m3525.5 2405.6-225.96-200.54-4.33 4.88 225.96 200.54c3.25 2.88 7.58-2 4.33-4.88Z" opacity=".78" style="mix-blend-mode:screen"/><path fill="url(#ko)" d="m2041.28 891.13 121.03 159.7-3.45 2.62-121.03-159.7c-1.74-2.3 1.71-4.91 3.45-2.62Z" opacity=".27" style="mix-blend-mode:screen"/><path fill="url(#kp)" d="m1987.73 2357.17 161.84-268.74-5.81-3.5-161.84 268.74c-2.33 3.86 3.48 7.36 5.81 3.5Z" opacity=".45" style="mix-blend-mode:screen"/><path fill="url(#kq)" d="m2679.49 1414.49-34.72 16.63-.36-.75 34.72-16.63c.5-.24.86.51.36.75Z" opacity=".72" style="mix-blend-mode:screen"/><path fill="url(#kr)" d="m1128.14 2580.87 479.19-374.8-8.1-10.35-479.19 374.8c-6.89 5.39 1.21 15.75 8.1 10.35Z" opacity=".45" style="mix-blend-mode:screen"/><path fill="url(#ks)" d="m1610.46 312 176.19 234.28-5.06 3.81-176.19-234.28c-2.53-3.37 2.53-7.18 5.06-3.81Z" opacity=".81" style="mix-blend-mode:screen"/><path fill="url(#kt)" d="m2961.53 2969.94-129.52-416.53-9 2.8 129.52 416.53c1.86 5.99 10.86 3.19 9-2.8Z" opacity=".27" style="mix-blend-mode:screen"/><path fill="url(#ku)" d="m2311.05 543.1 71.94 356.79-7.71 1.55-71.94-356.79c-1.03-5.13 6.67-6.69 7.71-1.55Z" opacity=".54" style="mix-blend-mode:screen"/><path fill="url(#kv)" d="m1907.97 1406.93 70.58 11.21.24-1.53-70.58-11.21c-1.01-.16-1.26 1.36-.24 1.53Z" opacity=".67" style="mix-blend-mode:screen"/><path fill="url(#kw)" d="M3080.24 218.47 3006 383.01l-3.56-1.6 74.24-164.54c1.07-2.36 4.62-.76 3.56 1.6Z" opacity=".88" style="mix-blend-mode:screen"/><path fill="url(#kx)" d="m2440.14 580.65 10.04 150.16-3.24.22-10.04-150.16c-.14-2.16 3.1-2.38 3.24-.22Z" opacity=".93" style="mix-blend-mode:screen"/><path fill="url(#ky)" d="m1795.71 1967.7 102.2-67.71-1.46-2.21-102.2 67.71c-1.47.97 0 3.18 1.46 2.21Z" opacity=".72" style="mix-blend-mode:screen"/><path fill="url(#kz)" d="m3066.73 179.19-204.8 482.44-10.42-4.43 204.8-482.44c2.94-6.93 13.37-2.51 10.42 4.43Z" opacity=".29" style="mix-blend-mode:screen"/><path fill="url(#kA)" d="m2381.56 1967.35 48.82-189.09-4.09-1.06-48.82 189.09c-.7 2.72 3.38 3.78 4.09 1.06Z" opacity=".8" style="mix-blend-mode:screen"/><path fill="url(#kB)" d="m4101.7 2069.37 898.3 313.42v-22.04l-891.44-311.03-6.86 19.65z" opacity=".85" style="mix-blend-mode:screen"/><path fill="url(#kC)" d="m3527.41 1493.05-572.34 7.32-.16-12.37 572.34-7.32c8.23-.11 8.39 12.26.16 12.37Z" opacity=".26" style="mix-blend-mode:screen"/><path fill="url(#kD)" d="m2205.9 2935.12-13.02 64.88h12.75l12.52-62.42-12.25-2.46z" opacity=".86" style="mix-blend-mode:screen"/><path fill="url(#kE)" d="m2445.75 1885.53 10.5-73.44-1.59-.23-10.5 73.44c-.15 1.06 1.44 1.28 1.59.23Z" opacity=".59" style="mix-blend-mode:screen"/><path fill="url(#kF)" d="m1916.54 46.92 109.91 272.19-5.88 2.38L1910.66 49.3c-1.58-3.91 4.3-6.29 5.88-2.38Z" opacity=".31" style="mix-blend-mode:screen"/><path fill="url(#kG)" d="m2146.43 1264.21 44.55 29.8.64-.96-44.55-29.8c-.64-.43-1.28.53-.64.96Z" opacity=".39" style="mix-blend-mode:screen"/><path fill="url(#kH)" d="m2740.76 1291.42-45.92 39.96-.86-.99 45.92-39.96c.66-.57 1.52.42.86.99Z" opacity=".29" style="mix-blend-mode:screen"/><path fill="url(#kI)" d="m1976.38 1864.91 127.94-88.68-1.92-2.76-127.94 88.68c-1.84 1.27.08 4.04 1.92 2.76Z" opacity=".31" style="mix-blend-mode:screen"/><path fill="url(#kJ)" d="m4665.17 2252.18-284.89-98.56 2.13-6.16 284.89 98.56c4.09 1.42 1.97 7.57-2.13 6.16Z" opacity=".37" style="mix-blend-mode:screen"/><path fill="url(#kK)" d="m9.93 645.71 605.48 209.64 4.53-13.08L14.46 632.62c-8.7-3.01-13.24 10.07-4.53 13.08Z" opacity=".97" style="mix-blend-mode:screen"/><path fill="url(#kL)" d="m2863.13 1497.69-129.39 1.32-.03-2.8 129.39-1.32c1.86-.02 1.89 2.78.03 2.8Z" opacity=".84" style="mix-blend-mode:screen"/><path fill="url(#kM)" d="m2605.09 2842.8-14.24-185.33-4 .31 14.24 185.33c.2 2.66 4.21 2.36 4-.31Z" opacity=".39" style="mix-blend-mode:screen"/><path fill="url(#kN)" d="m2966.35 1385.72-147.5 36.73-.79-3.19 147.5-36.73c2.12-.53 2.92 2.66.79 3.19Z" opacity=".59" style="mix-blend-mode:screen"/><path fill="url(#kO)" d="m2514.29 2441.53-1.69-123.05-2.66.04 1.69 123.05c.02 1.77 2.68 1.73 2.66-.04Z" opacity=".97" style="mix-blend-mode:screen"/><path fill="url(#kP)" d="M3123.68 81.23 3159.16 0h-10.65l-33.77 77.32 8.94 3.91z" opacity=".59" style="mix-blend-mode:screen"/><path fill="url(#kQ)" d="m3476.95 1986.89-155.57-77.21 1.67-3.36 155.57 77.21c2.24 1.11.57 4.47-1.67 3.36Z" opacity=".85" style="mix-blend-mode:screen"/><path fill="url(#kR)" d="m4298.51 1637.46 701.49 50.66v-15.42l-700.38-50.58-1.11 15.34z" opacity=".94" style="mix-blend-mode:screen"/><path fill="url(#kS)" d="m1828.54 1289.39 150.4 47.59 1.03-3.25-150.4-47.59c-2.16-.68-3.19 2.57-1.03 3.25Z" opacity=".55" style="mix-blend-mode:screen"/><path fill="url(#kT)" d="m2860.18 1110.08-135.59 147.99-3.2-2.93 135.59-147.99c1.95-2.13 5.15.8 3.2 2.93Z" opacity=".45" style="mix-blend-mode:screen"/><path fill="url(#kU)" d="m1203.29 152.15 341.54 357.05 7.72-7.38-341.54-357.05c-4.91-5.13-12.63 2.24-7.72 7.38Z" opacity=".56" style="mix-blend-mode:screen"/><path fill="url(#kV)" d="m4973.86 2291.29-785.95-248.31 5.37-16.98 785.95 248.31c11.3 3.57 5.94 20.56-5.37 16.98Z" opacity=".51" style="mix-blend-mode:screen"/><path fill="url(#kW)" d="m2398.1 1363.46 30.82 41.58.9-.67-30.82-41.58c-.44-.6-1.34.07-.9.67Z" opacity=".56" style="mix-blend-mode:screen"/><path fill="url(#kX)" d="M4590.66 750.4 5000 600.79v-28.06l-418.39 152.92 9.05 24.75z" opacity=".54" style="mix-blend-mode:screen"/><path fill="url(#kY)" d="m2204.24 1967.14 134.15-209.47-4.53-2.9-134.15 209.47c-1.93 3.01 2.6 5.91 4.53 2.9Z" opacity=".28" style="mix-blend-mode:screen"/><path fill="url(#kZ)" d="M4228.71 68.61 4310.97 0h-25.36l-67.3 56.14 10.4 12.47z" opacity=".56" style="mix-blend-mode:screen"/><path fill="url(#la)" d="M1125.25 2805.56 922.24 3000h23.4l190.8-182.75-11.19-11.69z" opacity=".94" style="mix-blend-mode:screen"/><path fill="url(#lb)" d="m2282.78 1745.8 38.69-43.61-.94-.84-38.69 43.61c-.56.63.39 1.46.94.84Z" opacity=".93" style="mix-blend-mode:screen"/><path fill="url(#lc)" d="m3346.07 1157.44-201.3 82.16-1.78-4.35 201.3-82.16c2.89-1.18 4.67 3.17 1.78 4.35Z" opacity=".37" style="mix-blend-mode:screen"/><path fill="url(#ld)" d="m1913.49 900.47 93.43 95.83 2.07-2.02-93.43-95.83c-1.34-1.38-3.41.64-2.07 2.02Z" opacity=".57" style="mix-blend-mode:screen"/><path fill="url(#le)" d="m2996.34 1307.64-53.15 20.68-.45-1.15 53.15-20.68c.76-.3 1.21.85.45 1.15Z" opacity=".33" style="mix-blend-mode:screen"/><path fill="url(#lf)" d="M433.85 419.06 0 193.73v16.55L427.08 432.1l6.77-13.04z" opacity=".39" style="mix-blend-mode:screen"/><path fill="url(#lg)" d="m4316.7 801.71-436.61 169.24-3.66-9.43 436.61-169.24c6.28-2.43 9.94 7 3.66 9.43Z" opacity=".55" style="mix-blend-mode:screen"/><path fill="url(#lh)" d="M309.08 2283.85 0 2395.93v19.45l315.31-114.35-6.23-17.18z" opacity=".53" style="mix-blend-mode:screen"/><path fill="url(#li)" d="m4828.85 2714.27 171.15 88.36v-23.45l-161.59-83.43-9.56 18.52z" opacity=".99" style="mix-blend-mode:screen"/><path fill="url(#lj)" d="M3437.77 162.93 3549.98 0h-33.45l-101.45 147.31 22.69 15.62z" opacity=".37" style="mix-blend-mode:screen"/><path fill="url(#lk)" d="m1968.92 264.34 57.31 133.72 2.89-1.24-57.31-133.72c-.82-1.92-3.71-.69-2.89 1.24Z" opacity=".27" style="mix-blend-mode:screen"/><path fill="url(#ll)" d="M3155.81 2759.75 3283.37 3000H3310l-133.41-251.29-20.78 11.04z" opacity=".47" style="mix-blend-mode:screen"/><path fill="url(#lm)" d="m1667.29 604.39 155.84 168.29 3.64-3.37-155.84-168.29c-2.24-2.42-5.88.95-3.64 3.37Z" opacity=".25" style="mix-blend-mode:screen"/><path fill="url(#ln)" d="m2535.09 2026.58-11.77-167.76 3.62-.25 11.77 167.76c.17 2.41-3.46 2.67-3.63.25Z" opacity=".42" style="mix-blend-mode:screen"/><path fill="url(#lo)" d="m1743.13 999.88 277.94 185.26 4-6.01-277.94-185.26c-3.99-2.66-8 3.34-4 6.01Z" opacity=".39" style="mix-blend-mode:screen"/><path fill="url(#lp)" d="m2666.27 2788.41-24.49-187.31 4.05-.53 24.49 187.31c.35 2.69-3.7 3.22-4.05.53Z" opacity=".5" style="mix-blend-mode:screen"/><path fill="url(#lq)" d="m1671.66 1540.19 166.95-7.73-.17-3.61-166.95 7.73c-2.4.11-2.23 3.72.17 3.61Z" opacity=".94" style="mix-blend-mode:screen"/><path fill="url(#lr)" d="m2483.72 1924.41 3.84-107.73 2.33.08-3.84 107.73c-.06 1.55-2.38 1.47-2.33-.08Z" opacity=".53" style="mix-blend-mode:screen"/><path fill="url(#ls)" d="M1357.08 2734.06 1112.47 3000h16.14l237.21-257.9-8.74-8.04z" opacity=".83" style="mix-blend-mode:screen"/><path fill="url(#lt)" d="m2647.01 1712.66-24.14-34.79.75-.52 24.14 34.79c.35.5-.4 1.02-.75.52Z" opacity=".58" style="mix-blend-mode:screen"/><path fill="url(#lu)" d="m1865.54 2174.1 154.13-162.91-3.52-3.33-154.13 162.91c-2.22 2.34 1.3 5.67 3.52 3.33Z" opacity=".71" style="mix-blend-mode:screen"/><path fill="url(#lv)" d="m4627.92 1860.62 372.08 62.5v-8.67l-370.66-62.25-1.42 8.42z" opacity=".59" style="mix-blend-mode:screen"/><path fill="url(#lw)" d="m3858.5 2193.43-191.5-97.42 2.11-4.14 191.5 97.42c2.75 1.4.65 5.54-2.11 4.14Z" opacity=".54" style="mix-blend-mode:screen"/><path fill="url(#lx)" d="m2054.63 254.6 51.81 145.22 3.14-1.12-51.81-145.22c-.74-2.09-3.88-.97-3.14 1.12Z" opacity=".65" style="mix-blend-mode:screen"/><path fill="url(#ly)" d="m4511.15 2309.43-545.4-217.75 4.71-11.79 545.4 217.75c7.84 3.13 3.14 14.92-4.71 11.79Z" opacity=".29" style="mix-blend-mode:screen"/><path fill="url(#lz)" d="m2701.29 1414.96-22.12 9.38-.2-.48 22.12-9.38c.32-.13.52.34.2.48Z" opacity=".79" style="mix-blend-mode:screen"/><path fill="url(#lA)" d="m2208.76 887.05 57.99 122.65 2.65-1.25-57.99-122.65c-.83-1.76-3.48-.51-2.65 1.25Z" opacity=".91" style="mix-blend-mode:screen"/><path fill="url(#lB)" d="m1687.21 1713.67 251.36-65.17-1.41-5.43-251.36 65.17c-3.61.94-2.21 6.37 1.41 5.43Z" opacity=".64" style="mix-blend-mode:screen"/><path fill="url(#lC)" d="m1878.15 1240.17 58.05 24.35.53-1.25-58.05-24.35c-.83-.35-1.36.9-.53 1.25Z" opacity=".42" style="mix-blend-mode:screen"/><path fill="url(#lD)" d="m2475.31 2806.78-3.09 193.22h7.71l3.08-193.1-7.7-.12z" opacity=".63" style="mix-blend-mode:screen"/><path fill="url(#lE)" d="m4109.94 1528.8-327.6-6.59-.14 7.08 327.6 6.59c4.71.09 4.86-6.98.14-7.08Z" opacity=".99" style="mix-blend-mode:screen"/><path fill="url(#lF)" d="m2517.45 1972.8-5.07-151.57-3.28.11 5.07 151.57c.07 2.18 3.35 2.07 3.28-.11Z" opacity=".8" style="mix-blend-mode:screen"/><path fill="url(#lG)" d="m3959.93 319.83-482.86 393.22-8.5-10.43 482.86-393.22c6.94-5.65 15.44 4.78 8.5 10.43Z" opacity=".75" style="mix-blend-mode:screen"/><path fill="url(#lH)" d="M3004.17 226.3 3093.01 0h-8.04l-87.76 223.56 6.96 2.74z" opacity=".66" style="mix-blend-mode:screen"/><path fill="url(#lI)" d="M2802.38 305.46 2874.89 0h-37.3l-70.52 297.08 35.31 8.38z" opacity=".9" style="mix-blend-mode:screen"/><path fill="url(#lJ)" d="m2467.53 53.89 13.85 523.5-11.31.3-13.85-523.5c-.2-7.52 11.11-7.83 11.31-.3Z" opacity=".61" style="mix-blend-mode:screen"/><path fill="url(#lK)" d="m1402.35 2735.59 221.16-247.76-5.35-4.78L1397 2730.81c-3.18 3.56 2.17 8.34 5.35 4.78Z" opacity=".67" style="mix-blend-mode:screen"/><path fill="url(#lL)" d="m2502.35 2790.17 2.37 209.83h24.29l-2.37-210.1-24.29.27z" opacity=".77" style="mix-blend-mode:screen"/><path fill="url(#lM)" d="m3547.58 2576.55-400.52-415.04-8.97 8.65 400.52 415.04c5.76 5.97 14.73-2.68 8.97-8.65Z" opacity=".82" style="mix-blend-mode:screen"/><path fill="url(#lN)" d="M1463.43 674.29 598.78 0h-46.03l893.27 696.61 17.41-22.32z" opacity=".61" style="mix-blend-mode:screen"/><path fill="url(#lO)" d="M1374.85 2028.42 0 2699.4v39.29l1390.34-678.53-15.49-31.74z" opacity=".47" style="mix-blend-mode:screen"/><path fill="url(#lP)" d="M252.9 2571.75 0 2693.08v12.05l257.6-123.59-4.7-9.79z" opacity=".82" style="mix-blend-mode:screen"/><path fill="url(#lQ)" d="m2463.68 2811.59-4.75 188.41h6.63l4.75-188.24-6.63-.17z" opacity=".61" style="mix-blend-mode:screen"/><path fill="url(#lR)" d="m1759.63 1705.61 205.86-56.58-1.22-4.45-205.86 56.58c-2.96.81-1.74 5.26 1.22 4.45Z" opacity=".49" style="mix-blend-mode:screen"/><path fill="url(#lS)" d="m1870.31 1091.9 139.96 90.3-1.95 3.02-139.96-90.3c-2.01-1.3-.06-4.32 1.95-3.02Z" opacity=".75" style="mix-blend-mode:screen"/><path fill="url(#lT)" d="M179.47 2132.17 0 2184.79v95.52l205.26-60.19-25.79-87.95z" opacity=".39" style="mix-blend-mode:screen"/><path fill="url(#lU)" d="M3006.5 1070.97 4202.35 0h-54.89L2982.07 1043.69l24.43 27.28z" opacity=".66" style="mix-blend-mode:screen"/><path fill="url(#lV)" d="m2758.52 2935.75 11.65 64.25h3.07l-11.75-64.79-2.97.54z" opacity=".74" style="mix-blend-mode:screen"/><path fill="url(#lW)" d="m2516.21 2176.22-1.88-83.11-1.8.04 1.88 83.11c.03 1.19 1.82 1.15 1.8-.04Z" opacity=".36" style="mix-blend-mode:screen"/><path fill="url(#lX)" d="m2945.76 2486.21-39.04-86.62-1.87.84 39.04 86.61c.56 1.25 2.43.4 1.87-.84Z" opacity=".52" style="mix-blend-mode:screen"/><path fill="url(#lY)" d="m1610.97 5.97 256.31 433.79 9.37-5.54L1620.34.43c-.11-.18-.26-.26-.38-.43h-8.71a5.08 5.08 0 0 0-.28 5.97Z" opacity=".46" style="mix-blend-mode:screen"/><path fill="url(#lZ)" d="m2929.36 1045.61-170.31 181.79-3.93-3.68 170.31-181.79c2.45-2.61 6.38 1.06 3.93 3.68Z" opacity=".78" style="mix-blend-mode:screen"/><path fill="url(#ma)" d="M379.08 1509.78 0 1515.29v42.07l379.69-5.52-.61-42.06z" opacity=".49" style="mix-blend-mode:screen"/><path fill="url(#mb)" d="m4514.98 1542.21-525.79-9.54.21-11.36 525.79 9.54c7.56.14 7.36 11.5-.21 11.36Z" opacity=".44" style="mix-blend-mode:screen"/><path fill="url(#mc)" d="m2771.46 2942.77 11.06 57.23h14.18l-11.57-59.87-13.67 2.64z" opacity=".39" style="mix-blend-mode:screen"/><path fill="url(#md)" d="m727.97 1207.64 529.91 89.17 1.93-11.45-529.91-89.17c-7.62-1.28-9.55 10.17-1.93 11.45Z" opacity=".97" style="mix-blend-mode:screen"/><path fill="url(#me)" d="m245.63 1469.52 365.06 5.58.12-7.89-365.06-5.58c-5.25-.08-5.37 7.81-.12 7.89Z" opacity=".43" style="mix-blend-mode:screen"/><path fill="url(#mf)" d="m4801.86 1787.62 198.14 23.75v-23.06l-195.42-23.43-2.72 22.74z" opacity=".58" style="mix-blend-mode:screen"/><path fill="url(#mg)" d="m4556.68 446.8-371.11 191.07-4.13-8.02 371.11-191.07c5.33-2.75 9.47 5.27 4.13 8.02Z" opacity=".36" style="mix-blend-mode:screen"/><path fill="url(#mh)" d="m32.73 1249.5 194.99 19.98.43-4.21-194.99-19.98c-2.8-.29-3.24 3.93-.43 4.21Z" opacity=".5" style="mix-blend-mode:screen"/><path fill="url(#mi)" d="m522.54 806.52 649.39 230.39 4.98-14.03-649.39-230.39c-9.33-3.31-14.32 10.72-4.98 14.03Z" opacity=".27" style="mix-blend-mode:screen"/><path fill="url(#mj)" d="M4882.56 2636.67 4609.3 2506.7l2.81-5.9 273.26 129.97c3.93 1.87 1.12 7.77-2.81 5.9Z" opacity=".93" style="mix-blend-mode:screen"/><path fill="url(#mk)" d="m644.92 1502.55 173.95-.06v-3.76l-173.95.06c-2.5 0-2.5 3.76 0 3.76Z" opacity=".29" style="mix-blend-mode:screen"/><path fill="url(#ml)" d="m1747.86 2499.18 242.98-320.46-6.92-5.25-242.98 320.46c-3.49 4.61 3.43 9.86 6.92 5.25Z" opacity=".49" style="mix-blend-mode:screen"/><path fill="url(#mm)" d="m3833.16 400.95-305.2 252.91-5.46-6.59 305.2-252.91c4.39-3.64 9.86 2.96 5.46 6.59Z" opacity=".61" style="mix-blend-mode:screen"/><path fill="url(#mn)" d="m2347.47 246.33 29.64 248.15 5.36-.64-29.64-248.15c-.43-3.57-5.79-2.93-5.36.64Z" opacity=".31" style="mix-blend-mode:screen"/><path fill="url(#mo)" d="m2837.17 2768.19 63.49 231.81h19.59l-64.87-236.8-18.21 4.99z" opacity=".4" style="mix-blend-mode:screen"/><path fill="url(#mp)" d="M1363.22 266.17 1115.8 0h-20.21l256.79 276.25 10.84-10.08z" opacity=".48" style="mix-blend-mode:screen"/><path fill="url(#mq)" d="m4365.17 385.95-433.46 260.52-5.63-9.37 433.46-260.52c6.23-3.74 11.87 5.62 5.63 9.37Z" opacity=".73" style="mix-blend-mode:screen"/><path fill="url(#mr)" d="M324.72 559.58 900.7 810.75l5.43-12.45-575.98-251.17c-8.28-3.61-13.71 8.83-5.43 12.45Z" opacity=".71" style="mix-blend-mode:screen"/><path fill="url(#ms)" d="m4264.79 2773.91-587.61-420.96 9.1-12.7 587.61 420.96c8.45 6.05-.64 18.75-9.1 12.7Z" opacity=".38" style="mix-blend-mode:screen"/><path fill="url(#mt)" d="M261.37 1613.71 0 1627.92v15.16l262.19-14.24-.82-15.13z" opacity=".81" style="mix-blend-mode:screen"/><path fill="url(#mu)" d="m4016.19 1255.67-315.2 51.59-1.11-6.81 315.2-51.59c4.53-.74 5.65 6.07 1.11 6.81Z" opacity=".81" style="mix-blend-mode:screen"/><path fill="url(#mv)" d="m594.29 999.79 697.7 186.22 4.02-15.08-697.7-186.22c-10.03-2.68-14.06 12.4-4.02 15.08Z" opacity=".69" style="mix-blend-mode:screen"/><path fill="url(#mw)" d="m2562.78 188.83-21.47 418.12 9.04.46 21.47-418.12c.31-6.01-8.73-6.48-9.03-.46Z" opacity=".8" style="mix-blend-mode:screen"/><path fill="url(#mx)" d="m576.24 598.41 370.4 174.56 3.77-8-370.4-174.56c-5.32-2.51-9.1 5.49-3.77 8Z" opacity=".86" style="mix-blend-mode:screen"/><path fill="url(#my)" d="m2655.57 2347.87-39.67-212.43 4.59-.86 39.67 212.43c.57 3.05-4.02 3.91-4.59.86Z" opacity=".72" style="mix-blend-mode:screen"/><path fill="url(#mz)" d="m3084.55 1762.01-205.33-93.08-2.01 4.44 205.33 93.08c2.95 1.34 4.97-3.1 2.01-4.44Z" opacity=".38" style="mix-blend-mode:screen"/><path fill="url(#mA)" d="m3097.15 2298.63-108.4-144.37 3.12-2.34 108.4 144.37c1.56 2.07-1.56 4.42-3.12 2.34Z" opacity=".49" style="mix-blend-mode:screen"/><path fill="url(#mB)" d="m2772.45 807.07-56.26 142.07 3.07 1.22 56.26-142.07c.81-2.04-2.26-3.26-3.07-1.22Z" opacity=".52" style="mix-blend-mode:screen"/><path fill="url(#mC)" d="m3526.69 1486.81-289.61 4.6-.1-6.26 289.61-4.6c4.16-.07 4.27 6.19.1 6.26Z" opacity=".87" style="mix-blend-mode:screen"/><path fill="url(#mD)" d="m2322.01 1199.95 13.88 23.43.51-.3-13.88-23.43c-.2-.34-.71-.04-.51.3Z" opacity=".94" style="mix-blend-mode:screen"/><path fill="url(#mE)" d="M1919.28 43.81 1901.41 0h-24.16l21.31 52.26 20.72-8.45z" opacity=".49" style="mix-blend-mode:screen"/><path fill="url(#mF)" d="m1279.71 1740.41 322.5-62.67-1.35-6.97-322.5 62.67c-4.64.9-3.29 7.87 1.35 6.97Z" opacity=".75" style="mix-blend-mode:screen"/><path fill="url(#mG)" d="m2486.15 1095.92 1.94 54.29-1.17.04-1.94-54.29c-.03-.78 1.15-.82 1.17-.04Z" opacity=".71" style="mix-blend-mode:screen"/><path fill="url(#mH)" d="m3218.46 1893.5-175.11-96.48-2.08 3.78 175.11 96.48c2.52 1.39 4.6-2.4 2.08-3.78Z" opacity=".25" style="mix-blend-mode:screen"/><path fill="url(#mI)" d="m1905.23 483.81 73.11 124.49-2.69 1.58-73.11-124.49c-1.05-1.79 1.64-3.37 2.69-1.58Z" opacity=".67" style="mix-blend-mode:screen"/><path fill="url(#mJ)" d="m1799.11 1369.35 466.34 90.4 1.95-10.08-466.34-90.4c-6.7-1.3-8.66 8.78-1.95 10.08Z" opacity=".7" style="mix-blend-mode:screen"/><path fill="url(#mK)" d="m2126.4 1519.6 103.48-5.75.12 2.24-103.47 5.75c-1.49.08-1.61-2.15-.12-2.24Z" opacity=".76" style="mix-blend-mode:screen"/><path fill="url(#mL)" d="m2358.28 1296.12 29.23 42.22.91-.63-29.23-42.22c-.42-.61-1.33.02-.91.63Z" opacity=".29" style="mix-blend-mode:screen"/><path fill="url(#mM)" d="M2078.49 2838.97 2028.37 3000h10.45l49.2-158.06-9.53-2.97z" opacity=".27" style="mix-blend-mode:screen"/><path fill="url(#mN)" d="m2166.76 1552.74 59.79-9.35-.2-1.29-59.79 9.35c-.86.13-.66 1.43.2 1.29Z" opacity=".26" style="mix-blend-mode:screen"/><path fill="url(#mO)" d="m2321.12 1182.78 52.9 93.08-2.01 1.14-52.9-93.08c-.76-1.34 1.25-2.48 2.01-1.14Z" opacity=".28" style="mix-blend-mode:screen"/><path fill="url(#mP)" d="m2686.26 1721.22-110.38-132.81-2.87 2.39 110.38 132.81c1.59 1.91 4.46-.47 2.87-2.39Z" opacity=".28" style="mix-blend-mode:screen"/><path fill="url(#mQ)" d="m2113.91 307.08 191.3 580.21-12.54 4.13-191.3-580.21c-2.75-8.34 9.79-12.48 12.54-4.13Z" opacity=".29" style="mix-blend-mode:screen"/><path fill="url(#mR)" d="m1800.8 1872.43 183.51-97.08-2.1-3.97-183.51 97.08c-2.64 1.4-.54 5.36 2.1 3.97Z" opacity=".27" style="mix-blend-mode:screen"/><path fill="url(#mS)" d="M2642.95 205.34 2664.5 0h-14.59l-21.39 203.82 14.43 1.52z" opacity=".44" style="mix-blend-mode:screen"/><path fill="url(#mT)" d="m3314.51 311.13-313 452.65 9.78 6.76 313-452.65c4.5-6.51-5.28-13.28-9.78-6.76Z" opacity=".57" style="mix-blend-mode:screen"/><path fill="url(#mU)" d="m2628.18 68.41-34.45 397.31-8.59-.74 34.45-397.31c.5-5.71 9.08-4.97 8.59.74Z" opacity=".48" style="mix-blend-mode:screen"/><path fill="url(#mV)" d="m3088.32 834.55-163.29 183.55 3.97 3.53 163.29-183.55c2.35-2.64-1.62-6.17-3.97-3.53Z" opacity=".37" style="mix-blend-mode:screen"/><path fill="url(#mW)" d="m4092.45 1890.33-542.68-130.97 2.83-11.73 542.68 130.97c7.8 1.88 4.98 13.61-2.83 11.73Z" opacity=".27" style="mix-blend-mode:screen"/><path fill="url(#mX)" d="m2622.36 2212.62-21.23-124.6-2.69.46 21.23 124.6c.31 1.79 3 1.33 2.69-.46Z" opacity=".45" style="mix-blend-mode:screen"/><path fill="url(#mY)" d="m1744.65 2769.31 104.79-176.67 3.82 2.26-104.79 176.67c-1.51 2.54-5.33.28-3.82-2.26Z" opacity=".46" style="mix-blend-mode:screen"/><path fill="url(#mZ)" d="m2830.43 1769.85-143.38-118.26-2.56 3.1 143.38 118.26c2.06 1.7 4.62-1.4 2.56-3.1Z" opacity=".69" style="mix-blend-mode:screen"/><path fill="url(#na)" d="m2640.32 2107.18-32.3-137.92 2.98-.7 32.3 137.92c.46 1.98-2.52 2.68-2.98.7Z" opacity=".52" style="mix-blend-mode:screen"/><path fill="url(#nb)" d="m1813.21 1214.36 138.63 58.13 1.26-3-138.63-58.13c-1.99-.84-3.25 2.16-1.26 3Z" opacity=".88" style="mix-blend-mode:screen"/><path fill="url(#nc)" d="m3220.25 1456.12-215.99 13.89-.3-4.67 215.99-13.89c3.1-.2 3.41 4.47.3 4.67Z" opacity=".38" style="mix-blend-mode:screen"/><path fill="url(#nd)" d="m3500.94 1429.58-110.23 7.63.16 2.38 110.23-7.63c1.58-.11 1.42-2.49-.16-2.38Z" opacity=".3" style="mix-blend-mode:screen"/><path fill="url(#ne)" d="m2761.82 1140.27-125.74 174.69-3.77-2.72 125.74-174.69c1.81-2.51 5.58.2 3.77 2.72Z" opacity=".49" style="mix-blend-mode:screen"/><path fill="url(#nf)" d="m1837.6 2901.14-46.35 98.86h13.51l43.92-93.67-11.08-5.19z" opacity=".95" style="mix-blend-mode:screen"/><path fill="url(#ng)" d="m1872.15 1432.03 174.77 18.41-.4 3.78-174.77-18.41c-2.51-.26-2.12-4.04.4-3.78Z" opacity=".98" style="mix-blend-mode:screen"/><path fill="url(#nh)" d="m2423.21 1878.73 30.29-146.05-3.16-.65-30.29 146.05c-.44 2.1 2.72 2.76 3.16.65Z" opacity=".47" style="mix-blend-mode:screen"/><path fill="url(#ni)" d="m2436.17 1849.1 9.27-51 1.1.2-9.27 51c-.13.73-1.24.53-1.1-.2Z" opacity=".57" style="mix-blend-mode:screen"/><path fill="url(#nj)" d="M3174.64 1298.34 2886.75 1383l1.83 6.22 287.89-84.66c4.14-1.22 2.31-7.44-1.83-6.22Z" opacity=".82" style="mix-blend-mode:screen"/><path fill="url(#nk)" d="M766.2 550.17 0 140.26v45.53l747.26 399.78 18.94-35.4z" opacity=".52" style="mix-blend-mode:screen"/><path fill="url(#nl)" d="m696.49 2322.99 266.55-122.2 2.64 5.76-266.55 122.2c-3.83 1.76-6.48-4-2.64-5.76Z" opacity=".92" style="mix-blend-mode:screen"/><path fill="url(#nm)" d="m2762.15 1614.67-104.16-46.11-1 2.25 104.16 46.11c1.5.66 2.49-1.59 1-2.25Z" opacity=".58" style="mix-blend-mode:screen"/><path fill="url(#nn)" d="m2698.84 1928.18-91.86-195.32 4.22-1.98 91.86 195.32c1.32 2.81-2.9 4.79-4.22 1.98Z" opacity=".64" style="mix-blend-mode:screen"/><path fill="url(#no)" d="m2487.91 2853.6-.27 146.4h19.02l.28-146.37-19.03-.03z" opacity=".38" style="mix-blend-mode:screen"/><path fill="url(#np)" d="m2158.28 1457.29 83.93 10.29-.22 1.81-83.93-10.29c-1.21-.15-.99-1.96.22-1.81Z" opacity=".73" style="mix-blend-mode:screen"/><path fill="url(#nq)" d="m2064.35 1596.09 116.19-25.3-.55-2.51-116.19 25.3c-1.67.36-1.12 2.87.55 2.51Z" opacity=".87" style="mix-blend-mode:screen"/><path fill="url(#nr)" d="m2220.34 2252.26 90.47-245.68 5.31 1.95-90.47 245.68c-1.3 3.53-6.61 1.58-5.31-1.95Z" opacity=".62" style="mix-blend-mode:screen"/><path fill="url(#ns)" d="m1958.72 1525.39 289.79-11.92-.26-6.26-289.79 11.92c-4.17.17-3.91 6.43.26 6.26Z" opacity=".82" style="mix-blend-mode:screen"/><path fill="url(#nt)" d="m3052.3 2312.86-47.48-69.75 1.51-1.03 47.48 69.75c.68 1-.82 2.03-1.51 1.03Z" opacity=".26" style="mix-blend-mode:screen"/><path fill="url(#nu)" d="m2231.28 2049.36 87.32-176.78-3.82-1.89-87.32 176.78c-1.26 2.54 2.56 4.43 3.82 1.89Z" opacity=".89" style="mix-blend-mode:screen"/><path fill="url(#nv)" d="m2505.3 827.33-.97 236.62-5.11-.02.97-236.62c.01-3.4 5.13-3.38 5.11.02Z" opacity=".9" style="mix-blend-mode:screen"/><path fill="url(#nw)" d="m2755.6 1873.65-63.39-93.19-2.01 1.37 63.39 93.19c.91 1.34 2.93-.03 2.01-1.37Z" opacity=".41" style="mix-blend-mode:screen"/><path fill="url(#nx)" d="m3618.79 1185.74-413.64 118.08-2.55-8.94 413.64-118.08c5.95-1.7 8.5 7.24 2.55 8.94Z" opacity=".44" style="mix-blend-mode:screen"/><path fill="url(#ny)" d="m2101.19 1185.38 98.31 77.97 1.68-2.12-98.31-77.97c-1.41-1.12-3.1 1-1.68 2.12Z" opacity=".87" style="mix-blend-mode:screen"/><path fill="url(#nz)" d="m672.66 1810.96 312.5-53.78 1.16 6.75-312.5 53.78c-4.49.77-5.66-5.98-1.16-6.75Z" opacity=".46" style="mix-blend-mode:screen"/><path fill="url(#nA)" d="m2625.85 1528.17-30.74-6.8.15-.66 30.74 6.8c.44.1.3.76-.15.66Z" opacity=".85" style="mix-blend-mode:screen"/><path fill="url(#nB)" d="m2692.2 2450.72-77.96-394.37-8.52 1.68 77.96 394.37c1.12 5.67 9.64 3.99 8.52-1.68Z" opacity=".41" style="mix-blend-mode:screen"/><path fill="url(#nC)" d="m2913.44 860.3-119.34 186-4.02-2.58 119.34-186c1.72-2.67 5.74-.1 4.02 2.58Z" opacity=".56" style="mix-blend-mode:screen"/><path fill="url(#nD)" d="m3571.91 1105.42-253.32 94-2.03-5.47 253.32-94c3.64-1.35 5.68 4.12 2.03 5.47Z" opacity=".93" style="mix-blend-mode:screen"/><path fill="url(#nE)" d="M2743.07 2587.23 2837.4 3000h10.33l-94.85-415.01-9.81 2.24z" opacity=".68" style="mix-blend-mode:screen"/><path fill="url(#nF)" d="m2819.16 1983.71-42.95-64.87 1.4-.93 42.95 64.87c.62.93-.78 1.86-1.4.93Z" opacity=".87" style="mix-blend-mode:screen"/><path fill="url(#nG)" d="M2319.51 49.94 2312.88 0h-24.25l7.05 53.1 23.83-3.16z" opacity=".83" style="mix-blend-mode:screen"/><path fill="url(#nH)" d="m1568.34 1616.62 385.99-46.69-1.01-8.34-385.99 46.69c-5.55.67-4.54 9.01 1.01 8.34Z" opacity=".96" style="mix-blend-mode:screen"/><path fill="url(#nI)" d="M2902.56 2265.55 3305.08 3000h34.54l-410.5-749.01-26.56 14.56z" opacity=".96" style="mix-blend-mode:screen"/><path fill="url(#nJ)" d="m2443.68 1686.15 32.71-105.77-2.29-.71-32.71 105.77c-.47 1.52 1.81 2.23 2.29.71Z" opacity=".31" style="mix-blend-mode:screen"/><path fill="url(#nK)" d="m3158.54 1422.19-94.29 11.29-.24-2.04 94.29-11.29c1.36-.16 1.6 1.88.24 2.04Z" opacity=".52" style="mix-blend-mode:screen"/><path fill="url(#nL)" d="m3911.06 2808.99-739.77-694.19-15 15.99 739.77 694.19c10.63 9.98 25.64-6 15-15.99Z" opacity=".99" style="mix-blend-mode:screen"/><path fill="url(#nM)" d="m3622.84 2203.7-343.25-216.85-4.69 7.42 343.25 216.85c4.93 3.12 9.62-4.3 4.69-7.42Z" opacity=".89" style="mix-blend-mode:screen"/><path fill="url(#nN)" d="m4132.64 192.44-145.11 116.45-2.52-3.14 145.11-116.45c2.09-1.67 4.6 1.46 2.52 3.14Z" opacity=".91" style="mix-blend-mode:screen"/><path fill="url(#nO)" d="m2347.43 1119.88 42.79 107.41 2.32-.92-42.79-107.41c-.61-1.54-2.94-.62-2.32.92Z" opacity=".27" style="mix-blend-mode:screen"/><path fill="url(#nP)" d="m1351.77 84.36 433.74 530.25-11.46 9.37-433.74-530.25c-6.23-7.62 5.22-17 11.46-9.37Z" opacity=".74" style="mix-blend-mode:screen"/><path fill="url(#nQ)" d="M719.12 1663.63 0 1733.23v16.53l720.7-69.75-1.58-16.38z" opacity=".33" style="mix-blend-mode:screen"/><path fill="url(#nR)" d="m1555.37 1410.94 268.76 24.49-.53 5.81-268.76-24.49c-3.86-.35-3.34-6.16.53-5.81Z" opacity=".26" style="mix-blend-mode:screen"/><path fill="url(#nS)" d="m2059.96 926.93 203.44 267.68 5.78-4.4-203.44-267.68c-2.92-3.85-8.71.55-5.78 4.4Z" opacity=".77" style="mix-blend-mode:screen"/><path fill="url(#nT)" d="m2513.93 1628.23-3.83-36.29-.78.08 3.83 36.29c.06.52.84.44.78-.08Z" opacity=".48" style="mix-blend-mode:screen"/><path fill="url(#nU)" d="m3370.59 1641.73-194.24-31.17.67-4.2 194.24 31.17c2.79.45 2.12 4.65-.67 4.2Z" opacity=".91" style="mix-blend-mode:screen"/><path fill="url(#nV)" d="m3531.62 2948.7-374.69-530.55-11.46 8.1 374.69 530.55c5.39 7.63 16.85-.46 11.46-8.1Z" opacity=".59" style="mix-blend-mode:screen"/><path fill="url(#nW)" d="m2573.95 1670.53-44.03-99.76 2.16-.95 44.03 99.76c.63 1.43-1.52 2.39-2.16.95Z" opacity=".38" style="mix-blend-mode:screen"/><path fill="url(#nX)" d="m2994.74 2917.52 29.48 82.48h23.86l-32.19-90.04-21.15 7.56z" opacity=".81" style="mix-blend-mode:screen"/><path fill="url(#nY)" d="m3173.95 199.7-355.92 677.13 14.63 7.69 355.92-677.13c5.12-9.73-9.51-17.43-14.63-7.69Z" opacity=".74" style="mix-blend-mode:screen"/><path fill="url(#nZ)" d="m253.8 2956.65 375.68-244.69 5.29 8.12-375.68 244.69c-5.4 3.52-10.69-4.6-5.29-8.12Z" opacity=".53" style="mix-blend-mode:screen"/><path fill="url(#oa)" d="m2088.56 741.17 75.39 138.42-2.99 1.63-75.39-138.42c-1.08-1.99 1.91-3.62 2.99-1.63Z" opacity=".52" style="mix-blend-mode:screen"/><path fill="url(#ob)" d="m4629.88 1753.02 370.12 28.42v-179.83l-356.39-27.36-13.73 178.77z" opacity=".51" style="mix-blend-mode:screen"/><path fill="url(#oc)" d="m798.79 980.16 384.75 116.61-2.52 8.31-384.75-116.61c-5.53-1.68-3.02-9.99 2.52-8.31Z" opacity=".52" style="mix-blend-mode:screen"/><path fill="url(#od)" d="M3227.06 2641.12 3459.4 3000h22.69l-239.04-369.23-15.99 10.35z" opacity=".9" style="mix-blend-mode:screen"/><path fill="url(#oe)" d="m2089.07 2626.72 123.91-336.08-7.26-2.68-123.91 336.08c-1.78 4.83 5.48 7.51 7.26 2.68Z" opacity=".9" style="mix-blend-mode:screen"/><path fill="url(#of)" d="M3148.88 261.13 3284.63 0h-10.56l-133.5 256.82 8.31 4.31z" opacity=".54" style="mix-blend-mode:screen"/><path fill="url(#og)" d="m1353.67 1166.1 363.18 104.52-2.26 7.85-363.18-104.52c-5.22-1.5-2.97-9.35 2.26-7.85Z" opacity=".86" style="mix-blend-mode:screen"/><path fill="url(#oh)" d="m4922.38 2720.98 77.62 38.84v-19.43l-69.84-34.95-7.78 15.54z" opacity=".7" style="mix-blend-mode:screen"/><path fill="url(#oi)" d="m3691.67 1781.85-382.85-89.26 1.93-8.27 382.85 89.26c5.5 1.28 3.58 9.56-1.93 8.27Z" opacity=".83" style="mix-blend-mode:screen"/><path fill="url(#oj)" d="m2139.63 1204.7 100.83 83.18 1.8-2.18-100.83-83.18c-1.45-1.2-3.25.98-1.8 2.18Z" opacity=".42" style="mix-blend-mode:screen"/><path fill="url(#ok)" d="m3492.52 1743.71-455.13-109.5 2.37-9.83 455.13 109.5c6.54 1.57 4.18 11.41-2.37 9.83Z" opacity=".77" style="mix-blend-mode:screen"/><path fill="url(#ol)" d="m564.83 229.11 954.23 619.01-13.38 20.62-954.22-619.01c-13.72-8.9-.35-29.52 13.38-20.62Z" opacity=".33" style="mix-blend-mode:screen"/><path fill="url(#om)" d="m791.05 2035.94 1163.25-355.67-7.69-25.14L783.36 2010.8c-16.72 5.11-9.05 30.25 7.69 25.14Z" opacity=".33" style="mix-blend-mode:screen"/><path fill="url(#on)" d="m490.07 1701.53 806.47-84.49 1.83 17.43-806.47 84.49c-11.59 1.21-13.43-16.21-1.83-17.43Z" opacity=".94" style="mix-blend-mode:screen"/><path fill="url(#oo)" d="m2150.85 2219.96 233.45-472.63-10.21-5.04-233.45 472.63c-3.36 6.79 6.85 11.84 10.21 5.04Z" opacity=".5" style="mix-blend-mode:screen"/><path fill="url(#op)" d="m3486.77 1257.51-199.74 49.62-1.07-4.32 199.74-49.62c2.87-.71 3.95 3.6 1.07 4.32Z" opacity=".64" style="mix-blend-mode:screen"/><path fill="url(#oq)" d="m1939.42 2543.91 139.29-257.29-5.56-3.01-139.29 257.29c-2 3.7 3.56 6.71 5.56 3.01Z" opacity=".69" style="mix-blend-mode:screen"/><path fill="url(#or)" d="m1889.32 874.68 267.48 271.34-5.86 5.78-267.48-271.34c-3.84-3.9 2.02-9.68 5.86-5.78Z" opacity=".28" style="mix-blend-mode:screen"/><path fill="url(#os)" d="M1169.43 2722.9 871.78 3000h34.01l279.43-260.14-15.79-16.96z" opacity=".54" style="mix-blend-mode:screen"/><path fill="url(#ot)" d="m341.5 2415.28 612.62-262.25 5.67 13.24-612.62 262.25c-8.81 3.77-14.48-9.47-5.67-13.24Z" opacity=".91" style="mix-blend-mode:screen"/><path fill="url(#ou)" d="M3463.74 373.12 3776.6 0h-38.96l-296.77 353.94 22.87 19.18z" opacity=".68" style="mix-blend-mode:screen"/><path fill="url(#ov)" d="m4124.01 1500.76-320.72-.84-.02 6.93 320.72.84c4.61.01 4.63-6.92.02-6.93Z" opacity=".9" style="mix-blend-mode:screen"/><path fill="url(#ow)" d="M358.95 1588.97 0 1606.09v25.66l360.17-17.17-1.22-25.61z" opacity=".73" style="mix-blend-mode:screen"/><path fill="url(#ox)" d="M2969.88 2703.35 3087.11 3000h10.11l-118.6-300.11-8.74 3.46z" opacity=".36" style="mix-blend-mode:screen"/><path fill="url(#oy)" d="m2432.44 1857.44 17.93-93.35-2.02-.39-17.93 93.35c-.26 1.34 1.76 1.73 2.02.39Z" opacity=".28" style="mix-blend-mode:screen"/><path fill="url(#oz)" d="m1889.41 607.62 224.6 325.29-7.03 4.85-224.6-325.29c-3.23-4.68 3.8-9.53 7.03-4.85Z" opacity=".58" style="mix-blend-mode:screen"/><path fill="url(#oA)" d="m1053.78 1614.32 691.67-51.08-1.1-14.95-691.67 51.08c-9.94.73-8.85 15.68 1.1 14.95Z" opacity=".94" style="mix-blend-mode:screen"/><path fill="url(#oB)" d="m3558.05 2256.12-274.4-194.92 4.21-5.93 274.4 194.92c3.94 2.8-.26 8.73-4.21 5.93Z" opacity=".74" style="mix-blend-mode:screen"/><path fill="url(#oC)" d="m3026.43 2508.32 263.2 491.68h25.24l-268.81-502.19-19.63 10.51z" opacity=".88" style="mix-blend-mode:screen"/><path fill="url(#oD)" d="m1530.56 2958.76-27.08 41.24h22.54l20.29-30.9-15.75-10.34z" opacity=".88" style="mix-blend-mode:screen"/><path fill="url(#oE)" d="m3339.35 1252.92-553.55 167.49-3.62-11.96 553.55-167.49c7.96-2.41 11.58 9.55 3.62 11.96Z" opacity=".89" style="mix-blend-mode:screen"/><path fill="url(#oF)" d="m2959.71 2933.2 23.57 66.8h94.79l-34.07-96.55-84.29 29.75z" opacity=".76" style="mix-blend-mode:screen"/><path fill="url(#oG)" d="m1019.39 2600.35 697.99-513.74-11.1-15.08-697.99 513.74c-10.03 7.38 1.06 22.47 11.1 15.08Z" opacity=".7" style="mix-blend-mode:screen"/><path fill="url(#oH)" d="m2903.01 2834.9 51.21 165.1h20.18l-52.98-170.81-18.41 5.71z" opacity=".6" style="mix-blend-mode:screen"/><path fill="url(#oI)" d="m372.6 1079.89 536.6 104.75-2.26 11.6-536.6-104.75c-7.71-1.51-5.46-13.1 2.26-11.6Z" opacity=".6" style="mix-blend-mode:screen"/><path fill="url(#oJ)" d="m2525.94 140.78-9.45 420.28 9.08.2 9.45-420.28c.14-6.04-8.95-6.25-9.08-.2Z" opacity=".59" style="mix-blend-mode:screen"/><path fill="url(#oK)" d="m1610.66 1943.84 676.94-331.16-7.16-14.63-676.94 331.16c-9.73 4.76-2.58 19.39 7.16 14.63Z" opacity=".95" style="mix-blend-mode:screen"/><path fill="url(#oL)" d="M2171.52 234.06 2107.41 0h-37.02l66.69 243.49 34.44-9.43z" opacity=".97" style="mix-blend-mode:screen"/><path fill="url(#oM)" d="M1754.35 2820.29 1653.85 3000h15.46l96.82-173.13-11.78-6.58z" opacity=".75" style="mix-blend-mode:screen"/><path fill="url(#oN)" d="m4338.86 1218.69-530.48 82.86-1.79-11.46 530.48-82.86c7.62-1.19 9.42 10.27 1.79 11.46Z" opacity=".95" style="mix-blend-mode:screen"/><path fill="url(#oO)" d="m1509.33 2450.85 414.08-393.89-8.51-8.95-414.08 393.89c-5.95 5.66 2.55 14.61 8.51 8.95Z" opacity=".64" style="mix-blend-mode:screen"/><path fill="url(#oP)" d="m4114.12 2110.96-307.66-115.89 2.5-6.65 307.66 115.89c4.42 1.67 1.92 8.32-2.5 6.65Z" opacity=".48" style="mix-blend-mode:screen"/><path fill="url(#oQ)" d="M892.06 515.39 36.92 0H0v9.85l877.87 529.09 14.19-23.55z" opacity=".61" style="mix-blend-mode:screen"/><path fill="url(#oR)" d="M832.55 473.44 52.6 0H3.86l815.56 495.06 13.13-21.62z" opacity=".59" style="mix-blend-mode:screen"/><path fill="url(#oS)" d="m2665.81 62.48-40.49 342.33 7.4.87 40.49-342.33c.58-4.92-6.81-5.8-7.4-.88Z" opacity=".5" style="mix-blend-mode:screen"/><path fill="url(#oT)" d="M3587.85 673.8 4424.59 0h-122.45l-762.46 613.98 48.17 59.82z" opacity=".57" style="mix-blend-mode:screen"/><path fill="url(#oU)" d="m3104.35 1641.73-154.79-35.97.78-3.34 154.79 35.97c2.22.52 1.45 3.86-.78 3.34Z" opacity=".34" style="mix-blend-mode:screen"/><path fill="url(#oV)" d="m949.25 2157.3 921.85-383.84-8.29-19.92-921.85 383.84c-13.25 5.52-4.97 25.44 8.29 19.92Z" opacity=".63" style="mix-blend-mode:screen"/><path fill="url(#oW)" d="M4382.86 2875.53 4555.68 3000h54.92l-208.99-150.51-18.75 26.04z" opacity=".5" style="mix-blend-mode:screen"/><path fill="url(#oX)" d="m1130.02 1617.87 266.61-22.4-.48-5.76-266.61 22.4c-3.83.32-3.35 6.08.48 5.76Z" opacity=".41" style="mix-blend-mode:screen"/><path fill="url(#oY)" d="m4164.04 1462.95-607.92 15.99-.35-13.14 607.92-15.99c8.74-.23 9.09 12.91.35 13.14Z" opacity=".8" style="mix-blend-mode:screen"/><path fill="url(#oZ)" d="M908.09 1335.34 0 1251.75v36.14l904.79 83.29 3.3-35.84z" opacity=".52" style="mix-blend-mode:screen"/><path fill="url(#pa)" d="m4384.97 1496.93-532.1 2.51-.05-11.5 532.1-2.51c7.65-.04 7.71 11.46.05 11.5Z" opacity=".41" style="mix-blend-mode:screen"/><path fill="url(#pb)" d="m2379.82 42.37 32.47 407.86 8.81-.7-32.47-407.86c-.47-5.86-9.28-5.17-8.81.7Z" opacity=".3" style="mix-blend-mode:screen"/><path fill="url(#pc)" d="m4077.58 2635.75-566.94-404.95 8.75-12.25 566.94 404.95c8.15 5.82-.59 18.08-8.75 12.25Z" opacity=".3" style="mix-blend-mode:screen"/><path fill="url(#pd)" d="M2692.03 327.74 2743.86 0h-12.95l-51.52 325.74 12.64 2z" opacity=".77" style="mix-blend-mode:screen"/><path fill="url(#pe)" d="m2549.64 2340.48-21.71-341.67 7.38-.47 21.71 341.67c.31 4.91-7.07 5.38-7.38.47Z" opacity=".88" style="mix-blend-mode:screen"/><path fill="url(#pf)" d="M1033.72 461.21 373.52 0h-40.43l687.37 480.19 13.26-18.98z" opacity=".27" style="mix-blend-mode:screen"/><path fill="url(#pg)" d="M3207.2 2396.65 3694.33 3000h31.72l-499.65-618.86-19.2 15.51z" opacity=".87" style="mix-blend-mode:screen"/><path fill="url(#ph)" d="m2167.94 1848.14 128.34-133.44-2.88-2.77-128.34 133.44c-1.84 1.92 1.04 4.69 2.88 2.77Z" opacity=".34" style="mix-blend-mode:screen"/><path fill="url(#pi)" d="m3196.43 2122.5-469.07-413.24 8.93-10.14 469.07 413.24c6.74 5.94-2.18 16.08-8.93 10.14Z" opacity=".37" style="mix-blend-mode:screen"/><path fill="url(#pj)" d="m1950.54 70-27.12-70h-8.17l28.18 72.75 7.11-2.75z" opacity=".98" style="mix-blend-mode:screen"/><path fill="url(#pk)" d="m3340.67 1406.16-605.3 72.31-1.56-13.08 605.3-72.31c8.7-1.04 10.27 12.04 1.56 13.08Z" opacity=".48" style="mix-blend-mode:screen"/><path fill="url(#pl)" d="m1478.03 2456.18 145.39-135.62-2.93-3.14-145.39 135.62c-2.09 1.95.84 5.09 2.93 3.14Z" opacity=".49" style="mix-blend-mode:screen"/><path fill="url(#pm)" d="M3620.34 781.41 4791.65 0h-70.57L3598.61 748.83l21.73 32.58z" opacity=".56" style="mix-blend-mode:screen"/><path fill="url(#pn)" d="m1783.69 2262.15 335.27-353.12-7.63-7.24-335.27 353.12c-4.82 5.08 2.81 12.32 7.63 7.24Z" opacity=".35" style="mix-blend-mode:screen"/><path fill="url(#po)" d="M3456.38 631.09 3298.1 775.43l-3.12-3.42 158.28-144.34c2.28-2.07 5.4 1.34 3.12 3.42Z" opacity=".44" style="mix-blend-mode:screen"/><path fill="url(#pp)" d="m3924.97 1550.77-395.5-15.3-.33 8.55 395.5 15.3c5.68.22 6.02-8.33.33-8.55Z" opacity=".52" style="mix-blend-mode:screen"/><path fill="url(#pq)" d="m3284.05 1298.93-378.55 99.47-2.15-8.18 378.55-99.47c5.44-1.43 7.59 6.75 2.15 8.18Z" opacity=".55" style="mix-blend-mode:screen"/><path fill="url(#pr)" d="m261.64 2193.17 234.34-72.33-1.56-5.06-234.34 72.33c-3.37 1.04-1.81 6.1 1.56 5.06Z" opacity=".87" style="mix-blend-mode:screen"/><path fill="url(#ps)" d="m3333.24 450.91-235.28 297.95-6.44-5.08 235.28-297.95c3.38-4.28 9.82.8 6.44 5.08Z" opacity=".97" style="mix-blend-mode:screen"/><path fill="url(#pt)" d="m2036.91 1613.64 256.54-61.29-1.32-5.54-256.54 61.29c-3.69.88-2.37 6.43 1.32 5.54Z" opacity=".97" style="mix-blend-mode:screen"/><path fill="url(#pu)" d="M2218.42 982.52 1646.24 0h-39.82l582.27 999.83 29.73-17.31z" opacity=".83" style="mix-blend-mode:screen"/><path fill="url(#pv)" d="M2193.22 40.29 2184 0h-51.65l11.8 51.53 49.07-11.24z" opacity=".28" style="mix-blend-mode:screen"/><path fill="url(#pw)" d="m2553.05 2274.13-25.53-406.65-8.79.55 25.53 406.65c.37 5.84 9.15 5.3 8.79-.55Z" opacity=".62" style="mix-blend-mode:screen"/><path fill="url(#px)" d="M1625.15 2457.03 1136.85 3000h30.78l474.53-527.67-17.01-15.3z" opacity=".7" style="mix-blend-mode:screen"/><path fill="url(#py)" d="m2314.5 2939.79 82.11-612.95-13.24-1.77-82.11 612.95c-1.18 8.81 12.06 10.59 13.24 1.77Z" opacity=".4" style="mix-blend-mode:screen"/><path fill="url(#pz)" d="m1927.23 2317.36 117.96-169.01 3.65 2.55-117.96 169.01c-1.7 2.43-5.35-.12-3.65-2.55Z" opacity=".66" style="mix-blend-mode:screen"/><path fill="url(#pA)" d="m1364 2276.49 194.36-132.31-2.86-4.2-194.36 132.31c-2.79 1.9.06 6.1 2.86 4.2Z" opacity=".35" style="mix-blend-mode:screen"/><path fill="url(#pB)" d="m2301.65 94.16 61.08 442.59 9.56-1.32-61.08-442.59c-.88-6.36-10.44-5.05-9.56 1.32Z" opacity=".34" style="mix-blend-mode:screen"/><path fill="url(#pC)" d="M2891.83 434.45 3038.45 0h-63.56l-140.13 415.19 57.07 19.26z" opacity=".76" style="mix-blend-mode:screen"/><path fill="url(#pD)" d="m1473.09 420.14 291.46 308.36 6.66-6.3-291.46-308.36c-4.19-4.43-10.86 1.86-6.66 6.3Z" opacity=".76" style="mix-blend-mode:screen"/><path fill="url(#pE)" d="m2605.47 2430.73-73.85-613.27 13.25-1.6 73.85 613.27c1.06 8.81-12.19 10.42-13.25 1.6Z" opacity=".44" style="mix-blend-mode:screen"/><path fill="url(#pF)" d="m2708.46 1206.22-121.02 168.12 3.63 2.62 121.02-168.12c1.74-2.42-1.89-5.03-3.63-2.61Z" opacity=".59" style="mix-blend-mode:screen"/><path fill="url(#pG)" d="M515.03 489.59 0 232.36v48.41l495.68 247.57 19.35-38.75z" opacity=".95" style="mix-blend-mode:screen"/><path fill="url(#pH)" d="M1569.4 2559.37 1188.89 3000h32.86l366.47-424.38-18.82-16.25z" opacity=".87" style="mix-blend-mode:screen"/><path fill="url(#pI)" d="m2744.32 846.75-134.83 367.15-7.93-2.91 134.83-367.15c1.94-5.28 9.87-2.37 7.93 2.91Z" opacity=".84" style="mix-blend-mode:screen"/><path fill="url(#pJ)" d="m2044.89 1606.93 230.8-53.02-1.15-4.99-230.8 53.02c-3.32.76-2.17 5.75 1.15 4.99Z" opacity=".76" style="mix-blend-mode:screen"/><path fill="url(#pK)" d="M2397.31 1159.17 1823.25 0h-134.84l600.62 1212.79 108.28-53.62z" opacity=".87" style="mix-blend-mode:screen"/><path fill="url(#pL)" d="M2378.65 2859.28 2366.32 3000h4.98l12.29-140.29-4.94-.43z" opacity=".44" style="mix-blend-mode:screen"/><path fill="url(#pM)" d="M2232.56 500.79 2088.86 0h-39.55l146.71 511.28 36.54-10.49z" opacity=".4" style="mix-blend-mode:screen"/><path fill="url(#pN)" d="m1816.04 751.03 231.15 254.95 5.51-4.99-231.15-254.95c-3.32-3.66-8.83 1.33-5.51 4.99Z" opacity=".87" style="mix-blend-mode:screen"/><path fill="url(#pO)" d="m824.36 518.16 1040.42 600.57-12.98 22.48L811.38 540.64c-14.95-8.63-1.99-31.12 12.98-22.48Z" opacity=".33" style="mix-blend-mode:screen"/><path fill="url(#pP)" d="m2837.11 2678.94-259.02-936.92-20.25 5.6 259.02 936.92c3.72 13.47 23.97 7.88 20.25-5.6Z" opacity=".45" style="mix-blend-mode:screen"/><path fill="url(#pQ)" d="m2386.22 1259.48 46.53 97.25-2.1 1.01-46.53-97.25c-.67-1.4 1.43-2.4 2.1-1.01Z" opacity=".87" style="mix-blend-mode:screen"/><path fill="url(#pR)" d="m697.68 1800.81 486.05-79.71-1.72-10.5-486.05 79.71c-6.99 1.15-5.27 11.65 1.72 10.5Z" opacity=".52" style="mix-blend-mode:screen"/><path fill="url(#pS)" d="m1818.45 2092.71 174.33-152.5 3.3 3.77-174.33 152.5c-2.51 2.19-5.8-1.57-3.3-3.77Z" opacity=".47" style="mix-blend-mode:screen"/><path fill="url(#pT)" d="M3530.33 942.33 5000 114.38V69.23L3511.02 908.07l19.31 34.26z" opacity=".61" style="mix-blend-mode:screen"/><path fill="url(#pU)" d="m4852 1047.59 148-28.94v-13.34l-150.51 29.43 2.51 12.85z" opacity=".58" style="mix-blend-mode:screen"/><path fill="url(#pV)" d="m2898.7 634.72-132 288.93-6.24-2.85 132-288.93c1.9-4.15 8.14-1.3 6.24 2.85Z" opacity=".74" style="mix-blend-mode:screen"/><path fill="url(#pW)" d="m1912.94 2454.93 156.33-252.72-5.46-3.38-156.33 252.72c-2.25 3.63 3.21 7.01 5.46 3.38Z" opacity=".26" style="mix-blend-mode:screen"/><path fill="url(#pX)" d="m2760.37 2863.61 27.11 136.39h21.37l-27.92-140.47-20.56 4.08z" opacity=".99" style="mix-blend-mode:screen"/><path fill="url(#pY)" d="m2612.68 1327.27-83.64 130.35-2.82-1.81 83.64-130.35c1.2-1.87 4.02-.07 2.82 1.81Z" opacity=".5" style="mix-blend-mode:screen"/><path fill="url(#pZ)" d="m1596.12 1508.44 473.56-1.74-.04-10.23-473.56 1.74c-6.81.03-6.77 10.26.04 10.23Z" opacity=".9" style="mix-blend-mode:screen"/><path fill="url(#qa)" d="M1118.62 1272.14 0 1100.16v31.81l1113.84 171.24 4.78-31.07z" opacity=".54" style="mix-blend-mode:screen"/><path fill="url(#qb)" d="m3825.83 1362.9-752.3 73.36 1.59 16.26 752.3-73.36c10.81-1.05 9.24-17.31-1.59-16.26Z" style="mix-blend-mode:screen"/><path fill="url(#qc)" d="m2877.23 450.97-91.17 251.11 5.43 1.97 91.17-251.11c1.31-3.61-4.11-5.58-5.43-1.97Z" opacity=".35" style="mix-blend-mode:screen"/><path fill="url(#qd)" d="m2744.25 2041.12-52.68-115.84 2.5-1.14 52.68 115.84c.76 1.66-1.75 2.8-2.5 1.14Z" opacity=".34" style="mix-blend-mode:screen"/><path fill="url(#qe)" d="M3040.44 487.78 3290.79 0h-42.61l-241.46 470.48 33.72 17.3z" opacity=".79" style="mix-blend-mode:screen"/><path fill="url(#qf)" d="M2242.15 258.35 2361.32 845l12.68-2.58-119.17-586.65c-1.71-8.43-14.39-5.86-12.68 2.58Z" opacity=".76" style="mix-blend-mode:screen"/><path fill="url(#qg)" d="m2655.18 1661.44-56.99-58.82 1.27-1.23 56.99 58.82c.82.85-.45 2.08-1.27 1.23Z" opacity=".47" style="mix-blend-mode:screen"/><path fill="url(#qh)" d="m1012 1283.05 821.15 124.72 2.7-17.74-821.15-124.72c-11.8-1.79-14.51 15.95-2.69 17.74Z" opacity=".42" style="mix-blend-mode:screen"/><path fill="url(#qi)" d="m2361.07 1574.74 95.37-52.24 1.13 2.06-95.37 52.24c-1.37.75-2.5-1.31-1.13-2.06Z" opacity=".3" style="mix-blend-mode:screen"/><path fill="url(#qj)" d="m1077.15 1475.09 317.86 4.81-.1 6.87-317.86-4.81c-4.57-.07-4.47-6.94.1-6.87Z" opacity=".52" style="mix-blend-mode:screen"/><path fill="url(#qk)" d="m2754.73 2469.54-101.82-392.38-8.48 2.2 101.82 392.38c1.46 5.64 9.94 3.44 8.48-2.2Z" opacity=".52" style="mix-blend-mode:screen"/><path fill="url(#ql)" d="m2974.94 1369.88-277.92 78.04-1.69-6.01 277.92-78.04c3.99-1.12 5.68 4.88 1.69 6.01Z" opacity=".49" style="mix-blend-mode:screen"/><path fill="url(#qm)" d="m2835.03 2248.72-163.11-369.75-7.99 3.52 163.11 369.75c2.34 5.31 10.34 1.79 7.99-3.52Z" opacity=".35" style="mix-blend-mode:screen"/><path fill="url(#qn)" d="m2671.82 1337.14-97.62 93.69-2.02-2.11 97.62-93.69c1.4-1.35 3.43.76 2.02 2.11Z" opacity=".4" style="mix-blend-mode:screen"/><path fill="url(#qo)" d="m1369.7 711.7 510.87 360.14 7.78-11.04-510.87-360.14c-7.34-5.18-15.13 5.86-7.78 11.04Z" opacity=".44" style="mix-blend-mode:screen"/><path fill="url(#qp)" d="m739.17 2560.26 1367.33-840.19 18.16 29.55-1367.33 840.19c-19.65 12.08-37.83-17.46-18.16-29.55Z" opacity=".82" style="mix-blend-mode:screen"/><path fill="url(#qq)" d="m2499.65 2642.23 3.84-621.24-13.42-.08-3.84 621.24c-.06 8.93 13.37 9.02 13.42.08Z" opacity=".66" style="mix-blend-mode:screen"/></g></svg>
\ No newline at end of file
diff --git a/website/metal.jpeg b/website/metal.jpeg
new file mode 100644
index 0000000..f389e0f
--- /dev/null
+++ b/website/metal.jpeg
Binary files differ
diff --git a/website/minification.html b/website/minification.html
new file mode 100644
index 0000000..c9b5152
--- /dev/null
+++ b/website/minification.html
@@ -0,0 +1 @@
+<include src="website/include/layout.html" locals='{"title": "Minification", "url": "minification.html", "page": "website/pages/minification.md"}' />
diff --git a/website/og.jpeg b/website/og.jpeg
new file mode 100644
index 0000000..089924e
--- /dev/null
+++ b/website/og.jpeg
Binary files differ
diff --git a/website/pages/bundling.md b/website/pages/bundling.md
new file mode 100644
index 0000000..07e74e0
--- /dev/null
+++ b/website/pages/bundling.md
@@ -0,0 +1,177 @@
+<aside>
+
+[[toc]]
+
+</aside>
+
+# Bundling
+
+Lightning CSS supports bundling dependencies referenced by CSS `@import` rules into a single output file. When calling the Lightning CSS API, use the `bundle` or `bundleAsync` function instead of `transform`. When using the CLI, enable the `--bundle` flag.
+
+This API requires filesystem access, so it does not accept `code` directly via the API. Instead, the `filename` option is used to read the entry file directly.
+
+```js
+import { bundle } from 'lightningcss';
+
+let { code, map } = bundle({
+  filename: 'style.css',
+  minify: true
+});
+```
+
+## Dependencies
+
+CSS files can contain dependencies referenced by `@import` syntax, as well as references to classes in other files via [CSS modules](css-modules.html).
+
+### @import
+
+The [`@import`](https://developer.mozilla.org/en-US/docs/Web/CSS/@import) at-rule can be used to inline another CSS file into the same CSS bundle as the containing file. This means that at runtime a separate network request will not be needed to load the dependency. Referenced files should be relative to the containing CSS file.
+
+```css
+@import 'other.css';
+```
+
+`@import` rules must appear before all other rules in a stylesheet except `@charset` and `@layer` statement rules. Later import rules will cause an error to be emitted.
+
+### CSS modules
+
+Dependencies are also bundled when referencing another file via [CSS modules composition](css-modules.html#dependencies) or [external variables](css-modules.html#local-css-variables). See the linked CSS modules documentation for more details.
+
+## Conditional imports
+
+The `@import` rule can be conditional by appending a media query or `supports()` query. Lightning CSS will preserve this behavior by wrapping the inlined rules in `@media` and `@supports` rules as needed.
+
+```css
+/* a.css */
+@import "b.css" print;
+@import "c.css" supports(display: grid);
+
+.a { color: red }
+```
+
+```css
+/* b.css */
+.b { color: green }
+```
+
+```css
+/* c.css */
+.c { display: grid }
+```
+
+compiles to:
+
+```css
+@media print {
+  .b { color: green }
+}
+
+@supports (display: grid) {
+  .c { display: grid }
+}
+
+.a { color: red }
+```
+
+<div class="warning">
+
+**Note**: There are currently two cases where combining conditional rules is unsupported:
+
+1. Importing the same CSS file with only a media query, and again with only a supports query. This would require duplicating all rules in the file.
+2. Importing a file with a negated media type (e.g. `not print`) within another file with a negated media type.
+
+</div>
+
+## Cascade layers
+
+Imported CSS rules can also be placed into a CSS cascade layer, allowing you to control the order they apply. Nested imports will be placed into nested layers.
+
+```css
+/* a.css */
+@import "b.css" layer(foo);
+.a { color: red }
+```
+
+```css
+/* b.css */
+@import "c.css" layer(bar);
+.b { color: green }
+```
+
+```css
+/* c.css */
+.c { color: green }
+```
+
+compiles to:
+
+```css
+@layer foo.bar {
+  .c { color: green }
+}
+
+@layer foo {
+  .b { color: green }
+}
+
+.a { color: red }
+```
+
+<div class="warning">
+
+**Note**: There are two unsupported layer combinations that will currently emit a compiler error:
+
+1. Importing the same CSS file with different layer names. This would require duplicating all imported rules multiple times.
+2. Nested anonymous layers.
+
+</div>
+
+## Bundling order
+
+When `@import` rules are processed in browsers, if the same file appears more than once, the _last_ instance applies. This is the opposite from behavior in other languages like JavaScript. Lightning CSS follows this behavior when bundling so that the output behaves the same as if it were not bundled.
+
+```css
+/* index.css */
+@import "a.css";
+@import "b.css";
+@import "a.css";
+```
+
+```css
+/* a.css */
+body { background: green }
+```
+
+```css
+/* b.css */
+body { background: red }
+```
+
+compiles to:
+
+```css
+body { background: green }
+```
+
+## Custom resolvers
+
+The `bundleAsync` API is an asynchronous version of `bundle`, which also accepts a custom `resolver` object. This allows you to provide custom JavaScript functions for resolving `@import` specifiers to file paths, and reading files from the file system (or another source). The `read` and `resolve` functions are both optional, and may either return a string synchronously, or a Promise for asynchronous resolution.
+
+```js
+import { bundleAsync } from 'lightningcss';
+
+let { code, map } = await bundleAsync({
+  filename: 'style.css',
+  minify: true,
+  resolver: {
+    read(filePath) {
+      return fs.readFileSync(filePath, 'utf8');
+    },
+    resolve(specifier, from) {
+      return path.resolve(path.dirname(from), specifier);
+    }
+  }
+});
+```
+
+Note that using a custom resolver can slow down bundling significantly, especially when reading files asynchronously. Use `readFileSync` rather than `readFile` if possible for better performance, or omit either of the methods if you don't need to override the default behavior.
diff --git a/website/pages/css-modules.md b/website/pages/css-modules.md
new file mode 100644
index 0000000..a977e61
--- /dev/null
+++ b/website/pages/css-modules.md
@@ -0,0 +1,297 @@
+<aside>
+
+[[toc]]
+
+</aside>
+
+# CSS modules
+
+By default, CSS identifiers are global. If two files define the same class names, ids, custom properties, `@keyframes`, etc., they will potentially clash and overwrite each other. To solve this, Lightning CSS supports [CSS modules](https://github.com/css-modules/css-modules).
+
+CSS modules treat the classes defined in each file as unique. Each class name or identifier is renamed to include a unique hash, and a mapping is exported to JavaScript to allow referencing them.
+
+To enable CSS modules, provide the `cssModules` option when calling the Lightning CSS API. When using the CLI, enable the `--css-modules` flag.
+
+```js
+import {transform} from 'lightningcss';
+
+let {code, map, exports} = transform({
+  // ...
+  cssModules: true,
+  code: Buffer.from(`
+    .logo {
+      background: skyblue;
+    }
+  `),
+});
+```
+
+This returns an `exports` object in addition to the compiled code and source map. Each property in the `exports` object maps from the original name in the source CSS to the compiled (i.e. hashed) name. You can use this mapping in your JavaScript or template files to reference the compiled classes and identifiers.
+
+The exports object for the above example might look like this:
+
+```js
+{
+  logo: {
+    name: '8h19c6_logo',
+    isReferenced: false,
+    composes: []
+  }
+}
+```
+
+## Class composition
+
+Style rules in CSS modules can reference other classes with the `composes` property. This causes the referenced class to be applied whenever the composed class is used, effectively providing a form of style mixins.
+
+```css
+.bg-indigo {
+  background: indigo;
+}
+
+.indigo-white {
+  composes: bg-indigo;
+  color: white;
+}
+```
+
+In the above example, whenever the `indigo-white` class is applied, the `bg-indigo` class will be applied as well. This is indicated in the `exports` object returned by Lightning CSS as follows:
+
+```js
+{
+  'bg-indigo': {
+    name: '8h19c6_bg-indigo',
+    isReferenced: true,
+    composes: []
+  },
+  'indigo-white': {
+    name: '8h19c6_indigo-white',
+    isReferenced: false,
+    composes: [{
+      type: 'local',
+      name: '8h19c6_bg-indigo'
+    }]
+  }
+}
+```
+
+Multiple classes can be composed at once by separating them with spaces.
+
+```css
+.logo {
+  composes: bg-indigo padding-large;
+}
+```
+
+### Dependencies
+
+You can also reference class names defined in a different CSS file using the `from` keyword:
+
+```css
+.logo {
+  composes: bg-indigo from './colors.module.css';
+}
+```
+
+This outputs an exports object with the dependency information. It is the caller's responsibility to resolve this dependency and apply the target class name when using the `transform` API. When using the `bundle` API, this is handled automatically.
+
+```js
+{
+  logo: {
+    name: '8h19c6_logo',
+    isReferenced: false,
+    composes: [{
+      type: 'dependency',
+      name: 'bg-indigo',
+      specifier: './colors.module.css'
+    }]
+  }
+}
+```
+
+### Global composition
+
+Global (i.e. non-hashed) classes can also be composed using the `global` keyword:
+
+```css
+.search {
+  composes: search-widget from global;
+}
+```
+
+## Global exceptions
+
+Within a CSS module, all class and id selectors are local by default. You can also opt out of this behavior for a single selector using the `:global` pseudo class.
+
+```css
+.foo :global(.bar) {
+  color: red;
+}
+
+.foo .bar {
+  color: green;
+}
+```
+
+compiles to:
+
+```css
+.EgL3uq_foo .bar {
+  color: red;
+}
+
+.EgL3uq_foo .EgL3uq_bar {
+  color: #ff0;
+}
+```
+
+## Local CSS variables
+
+By default, class names, id selectors, and the names of `@keyframes`, `@counter-style`, and CSS grid lines and areas are scoped to the module they are defined in. Scoping for CSS variables and other [`<dashed-ident>`](https://www.w3.org/TR/css-values-4/#dashed-idents) names can also be enabled using the `dashedIdents` option when calling the Lightning CSS API. When using the CLI, enable the `--css-modules-dashed-idents` flag.
+
+```js
+let {code, map, exports} = transform({
+  // ...
+  cssModules: {
+    dashedIdents: true,
+  },
+});
+```
+
+When enabled, CSS variables will be renamed so they don't conflict with variable names defined in other files. Referencing a variable uses the standard `var()` syntax, which Lightning CSS will update to match the locally scoped variable name.
+
+```css
+:root {
+  --accent-color: hotpink;
+}
+
+.button {
+  background: var(--accent-color);
+}
+```
+
+becomes:
+
+```css
+:root {
+  --EgL3uq_accent-color: hotpink;
+}
+
+.EgL3uq_button {
+  background: var(--EgL3uq_accent-color);
+}
+```
+
+You can also reference variables defined in other files using the `from` keyword:
+
+```css
+.button {
+  background: var(--accent-color from './vars.module.css');
+}
+```
+
+Global variables may be referenced using the `from global` syntax.
+
+```css
+.button {
+  color: var(--color from global);
+}
+```
+
+The same syntax also applies to other CSS values that use the [`<dashed-ident>`](https://www.w3.org/TR/css-values-4/#dashed-idents) syntax. For example, the [@font-palette-values](https://drafts.csswg.org/css-fonts-4/#font-palette-values) rule and [font-palette](https://drafts.csswg.org/css-fonts-4/#propdef-font-palette) property use the `<dashed-ident>` syntax to define and refer to custom font color palettes, and will be scoped and referenced the same way as CSS variables.
+
+## Custom naming patterns
+
+By default, Lightning CSS prepends the hash of the filename to each class name and identifier in a CSS file. You can configure this naming pattern using the `pattern` when calling the Lightning CSS API. When using the CLI, provide the `--css-modules-pattern` option.
+
+A pattern is a string with placeholders that will be filled in by Lightning CSS. This allows you to add custom prefixes or adjust the naming convention for scoped classes.
+
+```js
+let {code, map, exports} = transform({
+  // ...
+  cssModules: {
+    pattern: 'my-company-[name]-[hash]-[local]',
+  },
+});
+```
+
+The following placeholders are currently supported:
+
+- `[name]` - The base name of the file, without the extension.
+- `[hash]` - A hash of the full file path.
+- `[content-hash]` - A hash of the file contents.
+- `[local]` - The original class name or identifier.
+
+<div class="warning">
+
+### CSS Grid
+
+**Note:** CSS grid line names can be ambiguous due to automatic postfixing done by the browser, which generates line names ending with `-start` and `-end` for each grid template area. When using CSS grid, your `"pattern"` configuration must end with the `[local]` placeholder so that these references work correctly.
+
+```js
+let { code, map, exports } = transform({
+  // ...
+  cssModules: {
+    // ❌ [local] must be at the end so that
+    // auto-generated grid line names work
+    pattern: '[local]-[hash]'
+    // ✅ do this instead
+    pattern: '[hash]-[local]'
+  }
+});
+```
+
+```css
+.grid {
+  grid-template-areas: 'nav main';
+}
+
+.nav {
+  grid-column-start: nav-start;
+}
+```
+
+</div>
+
+
+### Pure mode
+
+Just like the `pure` option of the `css-loader` for webpack, Lightning CSS also has a `pure` option that enforces usage of one or more id or class selectors for each rule. 
+
+
+```js
+let {code, map, exports} = transform({
+  // ...
+  cssModules: {
+    pure: true,
+  },
+});
+```
+
+If you enable this option, Lightning CSS will throw an error for CSS rules that don't have at least one id or class selector, like `div`.
+This is useful because selectors like `div` are not scoped and affects all elements on the page.
+
+
+
+## Turning off feature scoping
+
+Scoping of grid, animations, and custom identifiers can be turned off. By default all of these are scoped.
+
+```js
+let {code, map, exports} = transform({
+  // ...
+  cssModules: {
+    animation: true,
+    grid: true,
+    customIdents: true,
+  },
+});
+```
+
+## Unsupported features
+
+Lightning CSS does not currently implement all CSS modules features available in other implementations. Some of these may be added in the future.
+
+- Non-function syntax for the `:local` and `:global` pseudo classes.
+- The `@value` rule – superseded by standard CSS variables.
+- The `:import` and `:export` ICSS rules.
diff --git a/website/pages/docs.md b/website/pages/docs.md
new file mode 100644
index 0000000..3e818f6
--- /dev/null
+++ b/website/pages/docs.md
@@ -0,0 +1,180 @@
+<aside>
+
+[[toc]]
+
+</aside>
+
+# Getting Started
+
+Lightning CSS can be used as a library from JavaScript or Rust, or from a standalone CLI. It can also be wrapped as a plugin in other build tools, and it is built into [Parcel](https://parceljs.org) out of the box.
+
+## From Node
+
+First, install Lightning CSS using a package manager such as npm or Yarn.
+
+```shell
+npm install --save-dev lightningcss
+```
+
+Once installed, import the module and call one of the Lightning CSS APIs. The `transform` function compiles a CSS stylesheet from a [Node Buffer](https://nodejs.org/api/buffer.html). This example minifies the input CSS, and outputs the compiled code and a source map.
+
+```js
+import { transform } from 'lightningcss';
+
+let { code, map } = transform({
+  filename: 'style.css',
+  code: Buffer.from('.foo { color: red }'),
+  minify: true,
+  sourceMap: true
+});
+```
+
+See [Transpilation](transpilation.html) for details about syntax lowering and vendor prefixing CSS for your browser targets, and the draft syntax support in Lightning CSS. You can also use the `bundle` API to process `@import` rules and inline them – see [Bundling](bundling.html) for details.
+
+The [TypeScript definitions](https://github.com/parcel-bundler/lightningcss/blob/master/node/index.d.ts) also include documentation for all API options.
+
+## From Rust
+
+Lightning CSS can also be used as a Rust library to parse, transform, and minify CSS. See the Rust API docs on [docs.rs](https://docs.rs/lightningcss).
+
+## With Parcel
+
+[Parcel](https://parceljs.org) includes Lightning CSS as the default CSS transformer. You should also add a `browserslist` property to your `package.json`, which defines the target browsers that your CSS will be compiled for.
+
+While Lightning CSS handles the most commonly used PostCSS plugins like `autoprefixer`, `postcss-preset-env`, and CSS modules, you may still need PostCSS for more custom plugins like TailwindCSS. If that's the case, your PostCSS config will be picked up automatically. You can remove the plugins listed above from your PostCSS config, and they'll be handled by Lightning CSS.
+
+You can also configure Lightning CSS in the `package.json` in the root of your project. Currently, three options are supported: [drafts](transpilation.html#draft-syntax), which can be used to enable CSS nesting and custom media queries, [pseudoClasses](transpilation.html#pseudo-class-replacement), which allows replacing some pseudo classes like `:focus-visible` with normal classes that can be applied via JavaScript (e.g. polyfills), and [cssModules](css-modules.html), which enables CSS modules globally rather than only for files ending in `.module.css`, or accepts an options object.
+
+```json
+{
+  "@parcel/transformer-css": {
+    "cssModules": true,
+    "drafts": {
+      "nesting": true,
+      "customMedia": true
+    },
+    "pseudoClasses": {
+      "focusVisible": "focus-ring"
+    }
+  }
+}
+```
+
+See the [Parcel docs](https://parceljs.org/languages/css) for more details.
+
+## From Deno or in browser
+
+The `lightningcss-wasm` package can be used in Deno or directly in browsers. This uses a WebAssembly build of Lightning CSS. Use `TextEncoder` and `TextDecoder` convert code from a string to a typed array and back.
+
+```js
+import init, { transform } from 'https://esm.run/lightningcss-wasm';
+
+await init();
+
+let {code, map} = transform({
+  filename: 'style.css',
+  code: new TextEncoder().encode('.foo { color: red }'),
+  minify: true,
+});
+
+console.log(new TextDecoder().decode(code));
+```
+
+Note that the `bundle` and visitor APIs are not currently available in the WASM build.
+
+## With webpack
+
+[css-minimizer-webpack-plugin](https://webpack.js.org/plugins/css-minimizer-webpack-plugin/) has built in support for Lightning CSS. To use it, first install Lightning CSS in your project with a package manager like npm or Yarn:
+
+```shell
+npm install --save-dev lightningcss css-minimizer-webpack-plugin browserslist
+```
+
+Next, configure `css-minifier-webpack-plugin` to use Lightning CSS as the minifier. You can provide options using the `minimizerOptions` object. See [Transpilation](transpilation.html) for details.
+
+```js
+// webpack.config.js
+const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
+const lightningcss = require('lightningcss');
+const browserslist = require('browserslist');
+
+module.exports = {
+  optimization: {
+    minimize: true,
+    minimizer: [
+      new CssMinimizerPlugin({
+        minify: CssMinimizerPlugin.lightningCssMinify,
+        minimizerOptions: {
+          targets: lightningcss.browserslistToTargets(browserslist('>= 0.25%'))
+        },
+      }),
+    ],
+  },
+};
+```
+
+## With Vite
+
+Vite supports Lightning CSS out of the box. First, install Lightning CSS into your project:
+
+```shell
+npm install --save-dev lightningcss
+```
+
+Then, set `'lightningcss'` as CSS [transformer](https://vitejs.dev/config/shared-options.html#css-transformer) and [minifier](https://vitejs.dev/config/build-options.html#build-cssminify) in your Vite config. You can also configure Lightning CSS options such as targets and css modules via the [css.lightningcss](https://vitejs.dev/config/shared-options.html#css-lightningcss) option in your Vite config.
+
+```js
+// vite.config.ts
+import browserslist from 'browserslist';
+import {browserslistToTargets} from 'lightningcss';
+
+export default {
+  css: {
+    transformer: 'lightningcss',
+    lightningcss: {
+      targets: browserslistToTargets(browserslist('>= 0.25%'))
+    }
+  },
+  build: {
+    cssMinify: 'lightningcss'
+  }
+};
+```
+
+## From the CLI
+
+Lightning CSS includes a standalone CLI that can be used to compile, minify, and bundle CSS files. It can be used when you only need to compile CSS, and don't need more advanced functionality from a larger build tool such as code splitting and support for other languages.
+
+To use the CLI, install the `lightningcss-cli` package with an npm compatible package manager:
+
+```shell
+npm install --save-dev lightningcss-cli
+```
+
+Then, you can run the `lightningcss` command via `npx`, `yarn`, or by setting up a script in your package.json.
+
+```json
+{
+  "scripts": {
+    "build": "lightningcss --minify --bundle --targets \">= 0.25%\" input.css -o output.css"
+  }
+}
+```
+
+To see all of the available options, use the `--help` argument:
+
+```shell
+npx lightningcss-cli --help
+```
+
+## Error recovery
+
+By default, Lightning CSS is strict, and will error when parsing an invalid rule or declaration. However, sometimes you may encounter a third party library that you can't easily modify, which unintentionally contains invalid syntax, or IE-specific hacks. In these cases, you can enable the `errorRecovery` option (or `--error-recovery` CLI flag). This will skip over invalid rules and declarations, omitting them in the output, and producing a warning instead of an error. You should also open an issue or PR to fix the issue in the library if possible.
+
+## Source maps
+
+Lightning CSS supports generating source maps when compiling, minifying, and bundling your source code to make debugging easier. Use the `sourceMap` option to enable it when using the API, or the `--sourcemap` CLI flag.
+
+If the input CSS came from another compiler such as Sass or Less, you can also pass an input source map to Lightning CSS using the `inputSourceMap` API option. This will map compiled locations back to their location in the original source code.
+
+Finally, the `projectRoot` option can be used to make file paths in source maps relative to a root directory. This makes build stable between machines.
diff --git a/website/pages/minification.md b/website/pages/minification.md
new file mode 100644
index 0000000..ad5b7b2
--- /dev/null
+++ b/website/pages/minification.md
@@ -0,0 +1,284 @@
+<aside>
+
+[[toc]]
+
+</aside>
+
+# Minification
+
+Lightning CSS can optimize your CSS to make it smaller, which can help improve the loading performance of your website. When using the Lightning CSS API, enable the `minify` option, or when using the CLI, use the `--minify` flag.
+
+```js
+import { transform } from 'lightningcss';
+
+let { code, map } = transform({
+  // ...
+  minify: true
+});
+```
+
+## Optimizations
+
+The Lightning CSS minifier includes many optimizations to generate the smallest possible output for all rules, properties, and values in your stylesheet. Lightning CSS does not perform any optimizations that change the behavior of your CSS unless it can prove that it is safe to do so. For example, only adjacent style rules are merged to avoid changing the order and potentially breaking the styles.
+
+### Shorthands
+
+Lightning CSS will combine longhand properties into shorthands when all of the constituent longhand properties are defined. For example:
+
+```css
+.foo {
+  padding-top: 1px;
+  padding-left: 2px;
+  padding-bottom: 3px;
+  padding-right: 4px;
+}
+```
+
+minifies to:
+
+```css
+.foo{padding:1px 4px 3px 2px}
+```
+
+This is supported across most shorthand properties defined in the CSS spec.
+
+### Merge adjacent rules
+
+Lightning CSS will merge adjacent style rules with the same selectors or declarations.
+
+```css
+.a {
+  color: red;
+}
+
+.b {
+  color: red;
+}
+
+.c {
+  color: green;
+}
+
+.c {
+  padding: 10px;
+}
+```
+
+becomes:
+
+```css
+.a,.b{color:red}.c{color:green;padding:10px}
+```
+
+In addition to style rules, Lightning CSS will also merge adjacent `@media`, `@supports`, and `@container` rules with identical queries, and adjacent `@layer` rules with the same layer name.
+
+Lightning CSS will not merge rules that are not adjacent, e.g. if another rule is between rules with the same declarations or selectors. This is because changing the order of the rules could cause the behavior of the compiled CSS to differ from the input CSS.
+
+### Remove prefixes
+
+Lightning CSS will remove vendor prefixed properties that are not needed according to your configured browser targets. This is more likely to affect precompiled libraries that include unused prefixes rather than your own code.
+
+For example, when compiling for modern browsers, prefixed versions of the `transition` property will be removed, since the unprefixed version is supported by all browsers.
+
+```css
+.button {
+  -webkit-transition: background 200ms;
+  -moz-transition: background 200ms;
+  transition: background 200ms;
+}
+```
+
+becomes:
+
+```css
+.button{transition:background .2s}
+```
+
+See [Transpilation](transpilation.html) for more on how to configure browser targets.
+
+### Reduce calc
+
+Lightning CSS will reduce `calc()` and other math expressions to constant values where possible. When different units are used, the terms are reduced as much as possible.
+
+```css
+.foo {
+  width: calc(100px * 2);
+  height: calc(((75.37% - 63.5px) - 900px) + (2 * 100px));
+}
+```
+
+minifies to:
+
+```css
+.foo{width:200px;height:calc(75.37% - 763.5px)}
+```
+
+Note that `calc()` expressions with variables are currently left unmodified by Lightning CSS.
+
+### Minify colors
+
+Lightning CSS will minify colors to the smallest format possible without changing the color gamut. For example, named colors as well as `rgb()` and `hsl()` colors are converted to hex notation, using hex alpha notation when supported by your browser targets.
+
+```css
+.foo {
+  color: rgba(255, 255, 0, 0.8)
+}
+```
+
+minifies to:
+
+```css
+.foo{color:#ff0c}
+```
+
+Note that only colors in the RGB gamut (including HSL and HWB) are converted to hex. Colors in other color spaces such as LAB or P3 are preserved.
+
+In addition to static colors, Lightning CSS also supports many color functions such as `color-mix()` and relative colors. When all components are known, Lightning CSS precomputes the result of these functions and outputs a static color. This both reduces the size and makes the syntax compatible with more browser targets.
+
+```css
+.foo {
+  color: rgb(from rebeccapurple r calc(g * 2) b);
+  background: color-mix(in hsl, hsl(120deg 10% 20%), hsl(30deg 30% 40%));
+}
+```
+
+minifies to:
+
+```css
+.foo{color:#669;background:#545c3d}
+```
+
+Note that these conversions cannot be performed when any of the components include CSS variables.
+
+### Normalizing values
+
+Lightning CSS parses all properties and values according to the CSS specification, filling in defaults where appropriate. When minifying, it omits default values where possible since the browser will fill those in when parsing.
+
+```css
+.foo {
+  background: 0% 0% / auto repeat scroll padding-box border-box red;
+}
+```
+
+minifies to:
+
+```css
+.foo{background:red}
+```
+
+In addition to removing defaults, Lightning CSS also omits quotes, whitespace, and optional delimiters where possible. It also converts values to shorter equivalents where possible.
+
+```css
+.foo {
+  font-weight: bold;
+  background-position: center center;
+  background-image: url("logo.png");
+}
+```
+
+minifies to:
+
+```css
+.foo{background-image:url(logo.png);background-position:50%;font-weight:700}
+```
+
+### CSS grid templates
+
+Lightning CSS will minify the `grid-template-areas` property to remove unnecessary whitespace and placeholders in template strings.
+
+```css
+.foo {
+  grid-template-areas: "head head"
+                       "nav  main"
+                       "foot ....";
+}
+```
+
+minifies to:
+
+```css
+.foo{grid-template-areas:"head head""nav main""foot."}
+```
+
+### Reduce transforms
+
+Lightning CSS will reduce CSS transform functions to shorter equivalents where possible.
+
+```css
+.foo {
+  transform: translate(0, 50px);
+}
+```
+
+minifies to:
+
+```css
+.foo{transform:translateY(50px)}
+```
+
+In addition, the `matrix()` and `matrix3d()` functions are converted to their equivalent transforms when shorter:
+
+```css
+.foo {
+  transform: matrix3d(1, 0, 0, 0, 0, 0.707106, 0.707106, 0, 0, -0.707106, 0.707106, 0, 100, 100, 10, 1);
+}
+```
+
+minifies to:
+
+```css
+.foo{transform:translate3d(100px,100px,10px)rotateX(45deg)}
+```
+
+When a matrix would be shorter, individual transform functions are converted to a single matrix instead:
+
+```css
+.foo {
+  transform: translate(100px, 200px) rotate(45deg) skew(10deg) scale(2);
+}
+```
+
+minifies to:
+
+```css
+.foo{transform:matrix(1.41421,1.41421,-1.16485,1.66358,100,200)}
+```
+
+## Unused symbols
+
+If you know that certain class names, ids, `@keyframes` rules, CSS variables, or other CSS identifiers are unused (for example as part of a larger full project analysis), you can use the `unusedSymbols` option to remove them.
+
+```js
+let { code, map } = transform({
+  // ...
+  minify: true,
+  unusedSymbols: ['foo', 'fade-in', '--color']
+});
+```
+
+With this configuration, the following CSS:
+
+```css
+:root {
+  --color: red;
+}
+
+.foo {
+  color: var(--color);
+}
+
+@keyframes fade-in {
+  from { opacity: 0 }
+  to { opacity: 1 }
+}
+
+.bar {
+  color: green;
+}
+```
+
+minifies to:
+
+```css
+.bar{color:green}
+```
diff --git a/website/pages/transforms.md b/website/pages/transforms.md
new file mode 100644
index 0000000..7441cb5
--- /dev/null
+++ b/website/pages/transforms.md
@@ -0,0 +1,428 @@
+<aside>
+
+[[toc]]
+
+</aside>
+
+# Custom transforms
+
+The Lightning CSS visitor API can be used to implement custom transform plugins in JavaScript. It is designed to enable custom non-standard extensions to CSS, making your code easier to author while shipping standard CSS to the browser. You can implement extensions such as custom shorthand properties or additional at-rules (e.g. mixins), build time transforms (e.g. convert units, inline constants, etc.), CSS rule analysis, and much more.
+
+Custom transforms have a build time cost: it can be around 2x slower to compile with a JS visitor than without. This means visitors should generally be used to implement custom, non-standard CSS extensions. Common standard transforms such as compiling modern standard CSS features (and draft specs) for older browsers should be done in Rust as part of Lightning CSS itself. Please open an issue if there's a feature we don't handle yet.
+
+## Visitors
+
+Custom transforms are implemented by passing a `visitor` object to the Lightning CSS Node API. A visitor includes one or more functions which are called for specific value types such as `Rule`, `Property`, or `Length`. In general, you should try to be as specific as possible about the types of values you want to handle. This way, Lightning CSS needs to call into JS as infrequently as possible, with the smallest objects possible, which improves performance. See the [TypeScript definitions](https://github.com/parcel-bundler/lightningcss/blob/eb49015cf887ae720b80a2856ccbdf61bf940ef1/node/index.d.ts#L184-L214) for a full list of available visitor functions.
+
+Visitors can return a new value to update it. Each visitor accepts a different type of value, and usually expects the same type in return. This example multiplies all lengths by 2:
+
+```js
+import { transform } from 'lightningcss';
+
+let res = transform({
+  filename: 'test.css',
+  minify: true,
+  code: Buffer.from(`
+    .foo {
+      width: 12px;
+    }
+  `),
+  visitor: {
+    Length(length) {
+      return {
+        unit: length.unit,
+        value: length.value * 2
+      }
+    }
+  }
+});
+
+assert.equal(res.code.toString(), '.foo{width:24px}');
+```
+
+Some visitor functions accept an array as a return value, enabling you to replace one value with multiple, or remove a value by returning an empty array. You can also provide an object instead of a function to further reduce the number of times a visitor is called. For example, when providing a `Property` visitor, you can use an object with keys for specific property names. This improves performance by only calling your visitor function when needed.
+
+This example adds `-webkit-overflow-scrolling: touch` before any `overflow` properties.
+
+```js
+let res = transform({
+  filename: 'test.css',
+  minify: true,
+  code: Buffer.from(`
+    .foo {
+      overflow: auto;
+    }
+  `),
+  visitor: {
+    Property: {
+      overflow(property) {
+        return [{
+          property: 'custom',
+          value: {
+            name: '-webkit-overflow-scrolling',
+            value: [{
+              type: 'token',
+              value: {
+                type: 'ident',
+                value: 'touch'
+              }
+            }]
+          }
+        }, property];
+      },
+    }
+  }
+});
+
+assert.equal(res.code.toString(), '.foo{-webkit-overflow-scrolling:touch;overflow:auto}');
+```
+
+## Value types
+
+The Lightning CSS AST is very detailed – each CSS property has a specific value type with all parts fully normalized. For example, a shorthand property such as `background` includes values for all of its sub-properties such as `background-color`, `background-image`, `background-position`, etc. This makes it both easier and faster for custom transforms to correctly handle all value types without reimplementing parsing. See the [TypeScript definitions](https://github.com/parcel-bundler/lightningcss/blob/master/node/ast.d.ts) for full documentation of all values.
+
+Known property values can be either _parsed_ or _unparsed_. Parsed values are fully expanded following the CSS specification. Unparsed values could not be parsed according to the grammar, and are stored as raw CSS tokens. This may occur because the value is invalid, or because it included unknown values such as CSS variables. Each property visitor function will need to handle both types of values.
+
+```js
+transform({
+  code: Buffer.from(`
+    .foo { width: 12px }
+    .bar { width: var(--w) }
+  `),
+  visitor: {
+    Property: {
+      width(v) {
+        if (v.property === 'unparsed') {
+          // Handle unparsed value, e.g. `var(--w)`
+        } else {
+          // Handle parsed value, e.g. `12px`
+        }
+      }
+    }
+  }
+});
+```
+
+Unknown properties, including custom properties, have the property type "custom". These values are also stored as raw CSS tokens. To visit custom properties, use the `custom` visitor function, or an object to filter by name. For example, to handle a custom `size` property and expand it to `width` and `height`, the following transform might be used.
+
+```js
+let res = transform({
+  minify: true,
+  code: Buffer.from(`
+    .foo {
+      size: 12px;
+    }
+  `),
+  visitor: {
+    Property: {
+      custom: {
+        size(property) {
+          // Handle the size property when the value is a length.
+          if (property.value[0].type === 'length') {
+            let value = {
+              type: 'length-percentage',
+              value: { type: 'dimension', value: property.value[0].value }
+            };
+
+            return [
+              { property: 'width', value },
+              { property: 'height', value }
+            ];
+          }
+        }
+      }
+    }
+  }
+});
+
+assert.equal(res.code.toString(), '.foo{width:12px;height:12px}');
+```
+
+## Raw values
+
+The Lightning CSS AST is very detailed, which is really useful when you need to transform it. However, it can be tedious to construct a full AST from scratch when returning entirely new values from a visitor. That's when raw values come in handy. You can return a `raw` property containing a string of CSS syntax from visitors that return declarations (i.e. properties) and tokens, and Lightning CSS will parse it for you and put it into the AST.
+
+This example implements a custom `color` function, which returns a raw CSS color value as a string, rather than constructing the whole AST.
+
+```js
+let res = transform({
+  minify: true,
+  code: Buffer.from(`
+    .foo {
+      color: color('red');
+    }
+  `),
+  visitor: {
+    Function: {
+      color() {
+        return { raw: 'rgb(255, 0, 0)' };
+      }
+    }
+  }
+});
+
+assert.equal(res.code.toString(), '.foo{color:red}');
+```
+
+## Entry and exit visitors
+
+By default, visitors are called when traversing downward through the tree (a pre-order traversal). This means each node is visited before its children. Sometimes it is useful to process a node after its children instead (a post-order traversal). This can be done by using an `Exit` visitor function, such as `FunctionExit`.
+
+For example, if you had a function visitor to double a length argument, and a visitor to replace an environment variable with a value, you could use an exit visitor to process the function after its arguments.
+
+```js
+let res = transform({
+  filename: 'test.css',
+  minify: true,
+  code: Buffer.from(`
+    .foo {
+      padding: double(env(--branding-padding));
+    }
+  `),
+  visitor: {
+    FunctionExit: {
+      // This will run after the EnvironmentVariable visitor, below.
+      double(f) {
+        if (f.arguments[0].type === 'length') {
+          return {
+            type: 'length',
+            value: {
+              unit: f.arguments[0].value.unit,
+              value: f.arguments[0].value.value * 2
+            }
+          };
+        }
+      }
+    },
+    EnvironmentVariable: {
+      // This will run before the FunctionExit visitor, above.
+      '--branding-padding': () => ({
+        type: 'length',
+        value: {
+          unit: 'px',
+          value: 20
+        }
+      })
+    }
+  }
+});
+
+assert.equal(res.code.toString(), '.foo{padding:40px}');
+```
+
+## Composing visitors
+
+Multiple visitors can be combined into one using the `composeVisitors` function. This lets you reuse visitors between projects by publishing them as plugins. The AST is visited in a single pass, running the functions from each visitor object as if they were written together.
+
+```js
+import { transform, composeVisitors } from 'lightningcss';
+
+let environmentVisitor = {
+  EnvironmentVariable: {
+    '--branding-padding': () => ({
+      type: 'length',
+      value: {
+        unit: 'px',
+        value: 20
+      }
+    })
+  }
+};
+
+let doubleFunctionVisitor = {
+  FunctionExit: {
+    double(f) {
+      if (f.arguments[0].type === 'length') {
+        return {
+          type: 'length',
+          value: {
+            unit: f.arguments[0].value.unit,
+            value: f.arguments[0].value.value * 2
+          }
+        };
+      }
+    }
+  }
+};
+
+let res = transform({
+  filename: 'test.css',
+  minify: true,
+  code: Buffer.from(`
+    .foo {
+      padding: double(env(--branding-padding));
+    }
+  `),
+  visitor: composeVisitors([environmentVisitor, doubleFunctionVisitor])
+});
+
+assert.equal(res.code.toString(), '.foo{padding:40px}');
+```
+
+Each visitor object has the opportunity to visit every value once. If a visitor returns a new value, that value is visited by the other visitor objects but not again by the original visitor that created it. If other visitors subsequently modify the value, the previous visitors will not revisit the value. This is to avoid infinite loops.
+
+## Unknown at-rules
+
+By default, unknown at-rules are stored in the AST as raw tokens. This allows you to interpret them however you like by writing a custom visitor. The following example allows declaring static variables using named at-rules, and inlines them when an `at-keyword` token is seen:
+
+```js
+let declared = new Map();
+let res = transform({
+  filename: 'test.css',
+  minify: true,
+  code: Buffer.from(`
+    @blue #056ef0;
+
+    .menu_link {
+      background: @blue;
+    }
+  `),
+  visitor: {
+    Rule: {
+      unknown(rule) {
+        declared.set(rule.name, rule.prelude);
+        return [];
+      }
+    },
+    Token: {
+      'at-keyword'(token) {
+        return declared.get(token.value);
+      }
+    }
+  }
+});
+
+assert.equal(res.code.toString(), '.menu_link{background:#056ef0}');
+```
+
+## Custom at-rules
+
+Raw tokens as stored in unknown at-rules are fine for simple cases, but in more complex cases, you may wish to interpret a custom at-rule body as a standard CSS declaration list or rule list. However, by default, Lightning CSS does not know how unknown rules should be parsed. You can define their syntax using the `customAtRules` option.
+
+The syntax of the at-rule prelude can be defined with a [CSS syntax string](https://drafts.css-houdini.org/css-properties-values-api/#syntax-strings), which Lightning CSS will interpret and use to validate the input CSS. This uses the same syntax as the [@property](https://developer.mozilla.org/en-US/docs/Web/CSS/@property) rule. The body syntax is defined using one of the following options:
+
+* `"declaration-list"` – A list of CSS declarations (property value pairs), as in a style rule or other at-rules like `@font-face`.
+* `"rule-list"` – A list of nested CSS rules, including style rules and at rules. Directly nested declarations with CSS nesting are not allowed. This matches how rules like `@keyframes` are parsed.
+* `"style-block"` – A list of CSS declarations and/or nested rules. This matches the behavior of rules like `@media` and `@supports` which support directly nested declarations when inside a style rule. Note that the [nesting](transpilation.html#nesting) and [targets](transpilation.html#browser-targets) options must be defined for nesting to be compiled.
+
+This example defines two custom at-rules. `@mixin` defines a reusable style block, supporting both directly nested declarations and nested rules. A visitor function registers the mixin in a map and removes the custom rule. `@apply` looks up the requested mixin in the map and returns the nested rules, which are inlined into the parent.
+
+```js
+let mixins = new Map();
+let res = transform({
+  filename: 'test.css',
+  minify: true,
+  targets: { chrome: 100 << 16 },
+  code: Buffer.from(`
+    @mixin color {
+      color: red;
+
+      &.bar {
+        color: yellow;
+      }
+    }
+
+    .foo {
+      @apply color;
+    }
+  `),
+  customAtRules: {
+    mixin: {
+      prelude: '<custom-ident>',
+      body: 'style-block'
+    },
+    apply: {
+      prelude: '<custom-ident>'
+    }
+  },
+  visitor: {
+    Rule: {
+      custom: {
+        mixin(rule) {
+          mixins.set(rule.prelude.value, rule.body.value);
+          return [];
+        },
+        apply(rule) {
+          return mixins.get(rule.prelude.value);
+        }
+      }
+    }
+  }
+});
+
+assert.equal(res.code.toString(), '.foo{color:red}.foo.bar{color:#ff0}');
+```
+
+## Examples
+
+For examples of visitors that perform a variety of real world tasks, see the Lightning CSS [visitor tests](https://github.com/parcel-bundler/lightningcss/blob/master/node/test/visitor.test.mjs).
+
+## Publishing a plugin
+
+Visitor plugins can be published to npm in order to share them with others. Plugin packages simply consist of an exported visitor object, which users can compose with other plugins via the `composeVisitors` function as described above.
+
+```js
+// lightningcss-plugin-double-function
+export default {
+  FunctionExit: {
+    double(f) {
+      // ...
+    }
+  }
+};
+```
+
+Plugins can also export a function in order to accept options.
+
+```js
+// lightningcss-plugin-env
+export default (values) => ({
+  EnvironmentVariable(env) {
+    return values[env.name];
+  }
+});
+```
+
+Plugin package names should start with `lightningcss-plugin-` and be descriptive about what they do, e.g. `lightningcss-plugin-double-function`. In addition, they should include the `lightningcss-plugin` keyword in their package.json so people can find them on npm.
+
+```json
+{
+  "name": "lightningcss-plugin-double-function",
+  "keywords": ["lightningcss-plugin"],
+  "main": "plugin.mjs"
+}
+```
+
+## Using plugins
+
+To use a published visitor plugin, install the package from npm, import it, and use the `composeVisitors` function as described above.
+
+```js
+import { transform, composeVisitors } from 'lightningcss';
+import environmentVisitor from 'lightningcss-plugin-environment';
+import doubleFunctionVisitor from 'lightningcss-plugin-double-function';
+
+let res = transform({
+  filename: 'test.css',
+  minify: true,
+  code: Buffer.from(`
+    .foo {
+      padding: double(env(--branding-padding));
+    }
+  `),
+  visitor: composeVisitors([
+    environmentVisitor({
+      '--branding-padding': {
+        type: 'length',
+        value: {
+          unit: 'px',
+          value: 20
+        }
+      }
+    }),
+    doubleFunctionVisitor
+  ])
+});
+
+assert.equal(res.code.toString(), '.foo{padding:40px}');
+```
diff --git a/website/pages/transpilation.md b/website/pages/transpilation.md
new file mode 100644
index 0000000..7fd5cf3
--- /dev/null
+++ b/website/pages/transpilation.md
@@ -0,0 +1,659 @@
+<aside>
+
+[[toc]]
+
+</aside>
+
+# Transpilation
+
+Lightning CSS includes support for transpiling modern CSS syntax to support older browsers, including vendor prefixing and syntax lowering.
+
+## Browser targets
+
+By default Lightning CSS does not perform any transpilation of CSS syntax for older browsers. This means that if you write your code using modern syntax or without vendor prefixes, that’s what Lightning CSS will output. You can declare your app’s supported browsers using the `targets` option. When this is declared, Lightning CSS will transpile your code accordingly to ensure compatibility with your supported browsers.
+
+Targets are defined using an object that specifies the minimum version of each browser you want to support. The easiest way to build a targets object is to use [browserslist](https://browserslist.dev). This lets you use a query that automatically updates over time as new browser versions are released, market share changes, etc. The following example will return a targets object listing browsers with >= 0.25% market share.
+
+```js
+import browserslist from 'browserslist';
+import { transform, browserslistToTargets } from 'lightningcss';
+
+// Call this once per build.
+let targets = browserslistToTargets(browserslist('>= 0.25%'));
+
+// Use `targets` for each file you transform.
+let { code, map } = transform({
+  // ...
+  targets
+});
+```
+
+For the best performance, you should call browserslist once for your whole build process, and reuse the same `targets` object when calling `transform` for each file.
+
+Under the hood, `targets` are represented using an object that maps browser names to minimum versions. Version numbers are represented using a single 24-bit number, with one semver component (major, minor, patch) per byte. For example, this targets object would represent Safari 13.2.0.
+
+```js
+let targets = {
+  safari: (13 << 16) | (2 << 8)
+};
+```
+
+### CLI
+
+When using the CLI, targets can be provided by passing a [browserslist](https://browserslist.dev) query to the `--targets` option. Alternatively, if the `--browserslist` option is provided, then `lightningcss` finds browserslist configuration, selects queries by environment and loads the resulting queries as targets.
+
+Configuration discovery and targets resolution is modeled after the original `browserslist` Node package. The configuration is resolved in the following order:
+
+- If a `BROWSERSLIST` environment variable is present, then load targets from its value.
+- If a `BROWSERSLIST_CONFIG` environment variable is present, then load the browserslist configuration from the file at the provided path.
+- If none of the above apply, then find, parse and use targets from the first `browserslist`, `.browserslistrc`, or `package.json` configuration file in any parent directory.
+
+Browserslist configuration files may contain sections denoted by square brackets. Use these to specify different targets for different environments. Targets which are not placed in a section are added to `defaults` and used if no section matches the environment.
+
+```ini
+# Defaults, applied when no other section matches the provided environment.
+firefox ESR
+
+# Targets applied only to the staging environment.
+[staging]
+samsung >= 4
+```
+
+When using parsed configuration from `browserslist`, `.browserslistrc`, or `package.json` configuration files, the environment is determined by:
+
+- the `BROWSERSLIST_ENV` environment variable if present
+- the `NODE_ENV` environment variable if present
+- otherwise `"production"` is used.
+
+If no targets are found for the resulting environment, then the `defaults` configuration section is used.
+
+### Feature flags
+
+In most cases, setting the `targets` option and letting Lightning CSS automatically compile your CSS works great. However, in other cases you might need a little more control over exactly what features are compiled and which are not. That's where the `include` and `exclude` options come in.
+
+The `include` and `exclude` options allow you to explicitly turn on or off certain features. These override the defaults based on the provided browser targets. For example, you might want to only compile colors, and handle auto prefixing or other features with another tool. Or you may want to handle everything except vendor prefixing with Lightning CSS. These options make that possible.
+
+The `include` and `exclude` options are configured using the `Features` enum, which can be imported from `lightningcss`. You can bitwise OR multiple flags together to turn them on or off.
+
+```js
+import { transform, Features } from 'lightningcss';
+
+let { code, map } = transform({
+  // ...
+  targets,
+  // Always compile colors and CSS nesting, regardless of browser targets.
+  include: Features.Colors | Features.Nesting,
+  // Never add any vendor prefixes, regardless of targets.
+  exclude: Features.VendorPrefixes
+});
+```
+
+Here is a full list of available flags, described in the sections below:
+
+<div class="features">
+
+* `Nesting`
+* `NotSelectorList`
+* `DirSelector`
+* `LangSelectorList`
+* `IsSelector`
+* `TextDecorationThicknessPercent`
+* `MediaIntervalSyntax`
+* `MediaRangeSyntax`
+* `CustomMediaQueries`
+* `ClampFunction`
+* `ColorFunction`
+* `OklabColors`
+* `LabColors`
+* `P3Colors`
+* `HexAlphaColors`
+* `SpaceSeparatedColorNotation`
+* `LightDark`
+* `FontFamilySystemUi`
+* `DoublePositionGradients`
+* `VendorPrefixes`
+* `LogicalProperties`
+* `Selectors` – shorthand for `Nesting | NotSelectorList | DirSelector | LangSelectorList | IsSelector`
+* `MediaQueries` – shorthand for `MediaIntervalSyntax | MediaRangeSyntax | CustomMediaQueries`
+* `Colors` – shorthand for `ColorFunction | OklabColors | LabColors | P3Colors | HexAlphaColors | SpaceSeparatedColorNotation | LightDark`
+
+</div>
+
+## Vendor prefixing
+
+Based on your configured browser targets, Lightning CSS automatically adds vendor prefixed fallbacks for many CSS features. For example, when using the [`image-set()`](https://developer.mozilla.org/en-US/docs/Web/CSS/image/image-set()) function, Lightning CSS will output a fallback `-webkit-image-set()` value as well, since Chrome does not yet support the unprefixed value.
+
+```css
+.logo {
+  background: image-set(url(logo.png) 2x, url(logo.png) 1x);
+}
+```
+
+compiles to:
+
+```css
+.logo {
+  background: -webkit-image-set(url(logo.png) 2x, url(logo.png) 1x);
+  background: image-set("logo.png" 2x, "logo.png");
+}
+```
+
+In addition, if your CSS source code (or more likely a library) includes unnecessary vendor prefixes, Lightning CSS will automatically remove them to reduce bundle sizes. For example, when compiling for modern browsers, prefixed versions of the `transition` property will be removed, since the unprefixed version is supported by all browsers.
+
+```css
+.button {
+  -webkit-transition: background 200ms;
+  -moz-transition: background 200ms;
+  transition: background 200ms;
+}
+```
+
+becomes:
+
+```css
+.button {
+  transition: background .2s;
+}
+```
+
+## Syntax lowering
+
+Lightning CSS automatically compiles many modern CSS syntax features to more compatible output that is supported in your target browsers.
+
+### Nesting
+
+The [CSS Nesting](https://drafts.csswg.org/css-nesting/) spec enables style rules to be nested, with the selectors of the child rules extending the parent selector in some way. This is very commonly supported by CSS pre-processors like Sass, but with this spec, it will eventually be supported natively in browsers. Lightning CSS compiles this syntax to un-nested style rules that are supported in all browsers today.
+
+```css
+.foo {
+  color: blue;
+
+  .bar {
+    color: red;
+  }
+}
+```
+
+is equivalent to:
+
+```css
+.foo {
+  color: blue;
+}
+
+.foo .bar {
+  color: red;
+}
+```
+
+[Conditional rules](https://drafts.csswg.org/css-nesting/#conditionals) such as `@media` may also be nested within a style rule, without repeating the selector. For example:
+
+```css
+.foo {
+  display: grid;
+
+  @media (orientation: landscape) {
+    grid-auto-flow: column;
+  }
+}
+```
+
+is equivalent to:
+
+```css
+.foo {
+  display: grid;
+}
+
+@media (orientation: landscape) {
+  .foo {
+    grid-auto-flow: column;
+  }
+}
+```
+
+### Color mix
+
+The [`color-mix()`](https://drafts.csswg.org/css-color-5/#color-mix) function allows you to mix two colors by the specified amount in a certain color space. Lightning CSS will evaluate this function statically when all components are known (i.e. not variables).
+
+```css
+.foo {
+  color: color-mix(in hsl, hsl(120deg 10% 20%) 25%, hsl(30deg 30% 40%));
+}
+```
+
+compiles to:
+
+```css
+.foo {
+  color: #706a43;
+}
+```
+
+### Relative colors
+
+Relative colors allow you to modify the components of a color using math functions. In addition, you can convert colors between color spaces. Lightning CSS performs these calculations statically when all components are known (i.e. not variables).
+
+This example lightens `slateblue` by 10% in the LCH color space.
+
+```css
+.foo {
+  color: lch(from slateblue calc(l * 1.1) c h);
+}
+```
+
+compiles to:
+
+```css
+.foo {
+  color: lch(49.0282% 65.7776 296.794);
+}
+```
+
+### LAB colors
+
+Lightning CSS will convert [`lab()`](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/lab()), [`lch()`](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/lch()), [`oklab()`](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/oklab), and [`oklch()`](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/oklch) colors to fallback values for unsupported browsers when needed. These functions allow you to define colors in higher gamut color spaces, making it possible to use colors that cannot be represented by RGB.
+
+```css
+.foo {
+  color: lab(40% 56.6 39);
+}
+```
+
+compiles to:
+
+```css
+.foo {
+  color: #b32323;
+  color: color(display-p3 .643308 .192455 .167712);
+  color: lab(40% 56.6 39);
+}
+```
+
+As shown above, a `display-p3` fallback is included in addition to RGB when a target browser supports the P3 color space. This preserves high color gamut colors when possible.
+
+### Color function
+
+Lightning CSS converts the [`color()`](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/color()) function to RGB when needed for compatibility with older browsers. This allows you to use predefined color spaces such as `display-p3`, `xyz`, and `a98-rgb`.
+
+```css
+.foo {
+  color: color(a98-rgb 0.44091 0.49971 0.37408);
+}
+```
+
+compiles to:
+
+```css
+.foo {
+  color: #6a805d;
+  color: color(a98-rgb .44091 .49971 .37408);
+}
+```
+
+### HWB colors
+
+Lightning CSS converts [`hwb()`](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/hwb) colors to RGB.
+
+```css
+.foo {
+  color: hwb(194 0% 0%);
+}
+```
+
+compiles to:
+
+```css
+.foo {
+  color: #00c4ff;
+}
+```
+
+### Color notation
+
+Space separated color notation is converted to hex when needed. Hex colors with alpha are also converted to `rgba()` when unsupported by all targets.
+
+```css
+.foo {
+  color: #7bffff80;
+  background: rgb(123 255 255);
+}
+```
+
+compiles to:
+
+```css
+.foo {
+  color: rgba(123, 255, 255, .5);
+  background: #7bffff;
+}
+```
+
+### light-dark() color function
+
+The [`light-dark()`](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/light-dark) function allows you to specify a light mode and dark mode color in a single declaration, without needing to write a separate media query rule. In addition, it uses the [`color-scheme`](https://developer.mozilla.org/en-US/docs/Web/CSS/color-scheme) property to control which theme to use, which allows you to set it programmatically. The `color-scheme` property also inherits so themes can be nested and the nearest ancestor color scheme applies.
+
+Lightning CSS converts the `light-dark()` function to use CSS variable fallback when your browser targets don't support it natively. For this to work, you must set the `color-scheme` property on an ancestor element. The following example shows how you can support both operating system and programmatic overrides for the color scheme.
+
+```css
+html {
+  color-scheme: light dark;
+}
+
+html[data-theme=light] {
+  color-scheme: light;
+}
+
+html[data-theme=dark] {
+  color-scheme: dark;
+}
+
+button {
+  background: light-dark(#aaa, #444);
+}
+```
+
+compiles to:
+
+```css
+html {
+  --lightningcss-light: initial;
+  --lightningcss-dark: ;
+  color-scheme: light dark;
+}
+
+@media (prefers-color-scheme: dark) {
+  html {
+    --lightningcss-light: ;
+    --lightningcss-dark: initial;
+  }
+}
+
+html[data-theme="light"] {
+  --lightningcss-light: initial;
+  --lightningcss-dark: ;
+  color-scheme: light;
+}
+
+html[data-theme="dark"] {
+  --lightningcss-light: ;
+  --lightningcss-dark: initial;
+  color-scheme: dark;
+}
+
+button {
+  background: var(--lightningcss-light, #aaa) var(--lightningcss-dark, #444);
+}
+```
+
+### Logical properties
+
+CSS [logical properties](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Logical_Properties) allow you to define values in terms of writing direction, so that UIs mirror in right-to-left languages. Lightning CSS will compile these to use the `:dir()` selector when unsupported. If the `:dir()` selector is unsupported, it is compiled as described [below](#%3Adir()-selector).
+
+```css
+.foo {
+  border-start-start-radius: 20px
+}
+```
+
+compiles to:
+
+```css
+.foo:dir(ltr) {
+  border-top-left-radius: 20px;
+}
+
+.foo:dir(rtl) {
+  border-top-right-radius: 20px;
+}
+```
+
+
+### :dir() selector
+
+The [`:dir()`](https://developer.mozilla.org/en-US/docs/Web/CSS/:dir) selector matches elements based on the writing direction. Lightning CSS compiles this to use the `:lang()` selector when unsupported, which approximates this behavior as closely as possible.
+
+```css
+a:dir(rtl) {
+  color:red
+}
+```
+
+compiles to:
+
+```css
+a:lang(ae, ar, arc, bcc, bqi, ckb, dv, fa, glk, he, ku, mzn, nqo, pnb, ps, sd, ug, ur, yi) {
+  color: red;
+}
+```
+
+If multiple arguments to `:lang()` are unsupported, it is compiled as described [below](#%3Alang()-selector).
+
+### :lang() selector
+
+The [`:lang()`](https://developer.mozilla.org/en-US/docs/Web/CSS/:lang) selector matches elements based on their language. Some browsers do not support multiple arguments to this function, so Lightning CSS compiles them to use `:is()` when needed.
+
+```css
+a:lang(en, fr) {
+  color:red
+}
+```
+
+compiles to:
+
+```css
+a:is(:lang(en), :lang(fr)) {
+  color: red;
+}
+```
+
+When the `:is()` selector is unsupported, it is compiled as described [below](#%3Ais()-selector).
+
+### :is() selector
+
+The [`:is()`](https://developer.mozilla.org/en-US/docs/Web/CSS/:is) matches when one of its arguments matches. Lightning CSS falls back to the `:-webkit-any` and `:-moz-any` prefixed selectors.
+
+```css
+p:is(:first-child, .lead) {
+  margin-top: 0;
+}
+```
+
+compiles to:
+
+```css
+p:-webkit-any(:first-child, .lead) {
+  margin-top: 0;
+}
+
+p:-moz-any(:first-child, .lead) {
+  margin-top: 0;
+}
+
+p:is(:first-child, .lead) {
+  margin-top: 0;
+}
+```
+
+<div class="warning">
+
+**Note**: The prefixed versions of these selectors do not support complex selectors (e.g. selectors with combinators). Lightning CSS will only output prefixes if the arguments are simple selectors. Complex selectors in `:is()` are not currently compiled.
+
+</div>
+
+### :not() selector
+
+The [`:not()`](https://developer.mozilla.org/en-US/docs/Web/CSS/:not) selector can accept multiple arguments, and matches if none of the arguments match. Some older browsers only support a single argument, so Lightning CSS compiles this when needed. The `:is` selector is used to ensure the specificity remains the same, with fallback to `-webkit-any` and `-moz-any` as needed (described above).
+
+```css
+p:not(:first-child, .lead) {
+  margin-top: 1em;
+}
+```
+
+compiles to:
+
+```css
+p:not(:is(:first-child, .lead)) {
+  margin-top: 1em;
+}
+```
+
+### Math functions
+
+Lightning CSS simplifies [math functions](https://w3c.github.io/csswg-drafts/css-values/#math) including `clamp()`, `round()`, `rem()`, `mod()`, `abs()`, and `sign()`, [trigonometric functions](https://w3c.github.io/csswg-drafts/css-values/#trig-funcs) including `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, and `atan2()`, and [exponential functions](https://w3c.github.io/csswg-drafts/css-values/#exponent-funcs) including `pow()`, `log()`, `sqrt()`, `exp()`, and `hypot()` when all arguments are known (i.e. not variables). In addition, the numeric constants `e`, `pi`, `infinity`, `-infinity`, and `NaN` are supported in all calculations.
+
+```css
+.foo {
+  width: round(calc(100px * sin(pi / 4)), 5px);
+}
+```
+
+compiles to:
+
+```css
+.foo {
+  width: 70px;
+}
+```
+
+### Media query ranges
+
+[Media query range syntax](https://developer.mozilla.org/en-US/docs/Web/CSS/Media_Queries/Using_media_queries#syntax_improvements_in_level_4) allows defining media queries using comparison operators to create ranges and intervals. Lightning CSS compiles this to the corresponding `min` and `max` media features when needed.
+
+```css
+@media (480px <= width <= 768px) {
+  .foo { color: red }
+}
+```
+
+compiles to:
+
+```css
+@media (min-width: 480px) and (max-width: 768px) {
+  .foo { color: red }
+}
+```
+
+### Shorthands
+
+Lightning CSS compiles the following shorthands to corresponding longhands when the shorthand is not supported in all target browsers:
+
+* Alignment shorthands: [place-items](https://developer.mozilla.org/en-US/docs/Web/CSS/place-items), [place-content](https://developer.mozilla.org/en-US/docs/Web/CSS/place-content), [place-self](https://developer.mozilla.org/en-US/docs/Web/CSS/place-self)
+* [Overflow shorthand](https://developer.mozilla.org/en-US/docs/Web/CSS/overflow) with multiple values (e.g. `overflow: hidden auto`)
+* [text-decoration](https://developer.mozilla.org/en-US/docs/Web/CSS/text-decoration) with thickness, style, color, etc.
+* Two value [display](https://developer.mozilla.org/en-US/docs/Web/CSS/display) syntax (e.g. `display: inline flex`)
+
+### Double position gradients
+
+CSS gradients support using two positions in a color stop to repeat the color at two subsequent positions. When unsupported, Lightning CSS compiles it.
+
+```css
+.foo {
+  background: linear-gradient(green, red 30% 40%, pink);
+}
+```
+
+compiles to:
+
+```css
+.foo {
+  background: linear-gradient(green, red 30%, red 40%, pink);
+}
+```
+
+### system-ui font
+
+The `system-ui` font allows you to use the operating system default font. When unsupported, Lightning CSS compiles it to a font stack that works across major platforms.
+
+```css
+.foo {
+  font-family: system-ui;
+}
+```
+
+compiles to:
+
+```css
+.foo {
+  font-family: system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Noto Sans, Ubuntu, Cantarell, Helvetica Neue;
+}
+```
+
+## Draft syntax
+
+Lightning CSS can also be configured to compile several draft specs that are not yet available natively in any browser. Because these are drafts and the syntax can still change, they must be enabled manually in your project.
+
+### Custom media queries
+
+Support for [custom media queries](https://drafts.csswg.org/mediaqueries-5/#custom-mq) is included in the Media Queries Level 5 draft spec. This allows you to define media queries that are reused in multiple places within a CSS file. Lightning CSS will perform this substitution ahead of time when this feature is enabled.
+
+For example:
+
+```css
+@custom-media --modern (color), (hover);
+
+@media (--modern) and (width > 1024px) {
+  .a { color: green; }
+}
+```
+
+is equivalent to:
+
+```css
+@media ((color) or (hover)) and (width > 1024px) {
+  .a { color: green; }
+}
+```
+
+Because custom media queries are a draft, they are not enabled by default. To use them, enable the `customMedia` option under `drafts` when calling the Lightning CSS API. When using the CLI, enable the `--custom-media` flag.
+
+```js
+let { code, map } = transform({
+  // ...
+  drafts: {
+    customMedia: true
+  }
+});
+```
+
+## Pseudo class replacement
+
+Lightning CSS supports replacing CSS pseudo classes such as `:focus-visible` with normal CSS classes that can be applied using JavaScript. This makes it possible to polyfill these pseudo classes for older browsers.
+
+```js
+let { code, map } = transform({
+  // ...
+  pseudoClasses: {
+    focusVisible: 'focus-visible'
+  }
+});
+```
+
+The above configuration will result in the `:focus-visible` pseudo class in all selectors being replaced with the `.focus-visible` class. This enables you to use a JavaScript [polyfill](https://github.com/WICG/focus-visible), which will apply the `.focus-visible` class as appropriate.
+
+The following pseudo classes may be configured as shown above:
+
+* `hover` – corresponds to the `:hover` pseudo class
+* `active` – corresponds to the `:active` pseudo class
+* `focus` – corresponds to the `:focus` pseudo class
+* `focusVisible` – corresponds to the `:focus-visible` pseudo class
+* `focusWithin` – corresponds to the `:focus-within` pseudo class
+
+## Non-standard syntax
+
+For compatibility with other tools, Lightning CSS supports parsing some non-standard CSS syntax. This must be enabled by turning on a flag under the `nonStandard` option.
+
+```js
+let { code, map } = transform({
+  // ...
+  nonStandard: {
+    deepSelectorCombinator: true
+  }
+});
+
+```
+
+Currently the following features are supported:
+
+* `deepSelectorCombinator` – enables parsing the Vue/Angular `>>>` and `/deep/` selector combinators.
diff --git a/website/playground/index.html b/website/playground/index.html
new file mode 100644
index 0000000..a4ade5e
--- /dev/null
+++ b/website/playground/index.html
@@ -0,0 +1,212 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>⚡️ Lightning CSS Playground</title>
+    <style>
+      @font-face {
+        font-family:"urbane-rounded";
+        src:url("https://use.typekit.net/af/916187/00000000000000007735bfa0/30/l?primer=81a69539b194230396845be9681d114557adfb35f4cccc679c164afb4aa47365&fvd=n6&v=3") format("woff2"),url("https://use.typekit.net/af/916187/00000000000000007735bfa0/30/d?primer=81a69539b194230396845be9681d114557adfb35f4cccc679c164afb4aa47365&fvd=n6&v=3") format("woff"),url("https://use.typekit.net/af/916187/00000000000000007735bfa0/30/a?primer=81a69539b194230396845be9681d114557adfb35f4cccc679c164afb4aa47365&fvd=n6&v=3") format("opentype");
+        font-display:auto;font-style:normal;font-weight:600;font-stretch:normal;
+      }
+
+      html, body {
+        margin: 0;
+        height: 100%;
+        box-sizing: border-box;
+        font-family: -apple-system, system-ui;
+        color-scheme: dark;
+        background: #111;
+      }
+
+      body {
+        display: flex;
+        flex-direction: column;
+        gap: 5px;
+        padding: 10px;
+      }
+
+      header {
+        display: flex;
+        align-items: center;
+        margin-bottom: 5px;
+        --gold: lch(80% 82.34 80.104);
+      }
+
+      header a {
+        color: inherit;
+      }
+
+      header .github {
+        fill: currentColor;
+      }
+
+      header .logo {
+        grid-area: logo;
+        place-self: center end;
+        height: 60px;
+      }
+
+      header .logo .outer {
+        stroke-width: 30px;
+        stroke: var(--gold);
+      }
+
+      header h1 {
+        font-family: urbane-rounded, ui-rounded, system-ui;
+        font-size: 35px;
+        letter-spacing: -0.02em;
+        color: var(--gold);
+        padding: 20px 0;
+        margin: 0 20px;
+        flex: 1;
+      }
+
+      .targets {
+        display: table;
+        margin-top: 10px;
+      }
+
+      .targets label {
+        display: table-row;
+      }
+
+      .targets label span,
+      .targets label input {
+        display: table-cell;
+      }
+
+      main {
+        display: grid;
+        grid-template-areas: "sidebar source compiled         compiled"
+                             "sidebar visitor compiledModules compiledDependencies";
+        grid-template-columns: auto 2fr 1fr 1fr;
+        grid-template-rows: 1fr 1fr;
+        flex: 1;
+        gap: 10px;
+        overflow: hidden;
+      }
+
+      #sidebar {
+        grid-area: sidebar;
+        overflow: auto;
+      }
+
+      #visitor {
+        grid-area: visitor;
+        overflow: hidden;
+      }
+
+      #source {
+        grid-area: source;
+        overflow: hidden;
+      }
+
+      #source[data-expanded=true] {
+        grid-row: source-start / visitor-end;
+      }
+
+      #compiled {
+        grid-area: compiled;
+        overflow: hidden;
+      }
+
+      #compiled[data-expanded=true] {
+        grid-row: compiled-start / compiledDependencies-end;
+        grid-column: compiled-start / compiledDependencies-end;
+      }
+
+      #compiledModules {
+        grid-area: compiledModules;
+        overflow: hidden;
+      }
+
+      #compiledDependencies {
+        grid-area: compiledDependencies;
+        overflow: hidden;
+      }
+
+      #compiledModules[data-expanded=true],
+      #compiledDependencies[data-expanded=true] {
+        grid-row: compiledModules-start;
+        grid-column: compiledModules-start / compiledDependencies-end;
+      }
+
+      .cm-editor {
+        height: 100%;
+      }
+
+      label {
+        display: block;
+      }
+
+      h3 {
+        margin-bottom: 4px;
+      }
+
+      div > h3:first-child {
+        margin-top: 0;
+      }
+
+      summary {
+        font-weight: bold;
+      }
+    </style>
+  </head>
+  <body>
+    <header>
+      <svg class="logo" aria-hidden="true" viewBox="495 168 360 654">
+        <path class="outer" d="M594.41,805c-.71,0-1.43-.15-2.11-.47-2.2-1.03-3.34-3.48-2.72-5.83l67.98-253.71h-140.45c-1.86,0-3.57-1.04-4.44-2.69-.86-1.65-.73-3.65,.34-5.18l26.85-38.35q25.56-36.51,104.91-149.83l106.31-151.82c1.39-1.99,4.01-2.69,6.21-1.66,2.2,1.03,3.34,3.48,2.72,5.83l-67.98,253.71h140.45c1.86,0,3.57,1.04,4.43,2.69,.86,1.65,.73,3.65-.34,5.18l-238.07,340c-.96,1.37-2.51,2.13-4.1,2.13Zm-67.69-270h137.37c1.55,0,3.02,.72,3.97,1.96,.95,1.23,1.27,2.84,.86,4.34l-62.33,232.61,216.29-308.9h-137.36c-1.55,0-3.02-.72-3.97-1.96-.95-1.23-1.27-2.84-.86-4.34l62.33-232.61-90.04,128.59q-79.35,113.32-104.91,149.83l-21.34,30.48Z"/>
+      </svg>
+      <h1>Lightning CSS Playground</h1>
+      <a href="https://github.com/parcel-bundler/lightningcss" target="_blank" aria-label="GitHub">
+        <svg class="github" aria-hidden="true" width="30" height="30" viewBox="0 0 20 20"> <title>GitHub</title> <path d="M10 0a10 10 0 0 0-3.16 19.49c.5.1.68-.22.68-.48l-.01-1.7c-2.78.6-3.37-1.34-3.37-1.34-.46-1.16-1.11-1.47-1.11-1.47-.9-.62.07-.6.07-.6 1 .07 1.53 1.03 1.53 1.03.9 1.52 2.34 1.08 2.91.83.1-.65.35-1.09.63-1.34-2.22-.25-4.55-1.11-4.55-4.94 0-1.1.39-1.99 1.03-2.69a3.6 3.6 0 0 1 .1-2.64s.84-.27 2.75 1.02a9.58 9.58 0 0 1 5 0c1.91-1.3 2.75-1.02 2.75-1.02.55 1.37.2 2.4.1 2.64.64.7 1.03 1.6 1.03 2.69 0 3.84-2.34 4.68-4.57 4.93.36.31.68.92.68 1.85l-.01 2.75c0 .26.18.58.69.48A10 10 0 0 0 10 0"></path> </svg>
+      </a>
+    </header>
+    <main>
+      <form id="sidebar">
+        <h3>Options</h3>
+        <label><input id="minify" type="checkbox" checked> Minify</label>
+        <label><input id="cssModules" type="checkbox"> CSS modules</label>
+        <label><input id="analyzeDependencies" type="checkbox"> Analyze dependencies</label>
+        <label><input id="visitorEnabled" type="checkbox"> Visitor</label>
+        <h3>Draft syntax</h3>
+        <label><input id="customMedia" type="checkbox" checked> Custom media queries</label>
+        <h3>Targets</h3>
+        <div class="targets">
+          <label><span>Chrome: </span><input id="chrome" type="number" value="95"></label>
+          <label><span>Firefox: </span><input id="firefox" type="number"></label>
+          <label><span>Safari: </span><input id="safari" type="number"></label>
+          <label><span>Opera: </span><input id="opera" type="number"></label>
+          <label><span>Edge: </span><input id="edge" type="number"></label>
+          <label><span>IE: </span><input id="ie" type="number"></label>
+          <label><span>iOS: </span><input id="ios_saf" type="number"></label>
+          <label><span>Android: </span><input id="android" type="number"></label>
+          <label><span>Samsung: </span><input id="samsung" type="number"></label>
+        </div>
+        <h3>Features</h3>
+        <details>
+          <summary>Include</summary>
+          <div id="include" />
+        </details>
+        <details>
+          <summary>Exclude</summary>
+          <div id="exclude" />
+        </details>
+        <label>
+          <h3>Unused symbols</h3>
+          <textarea id="unusedSymbols" rows="4" placeholder="Separate items with newlines"></textarea>
+        </label>
+        <label>
+          <h3>Version</h3>
+          <select id="version"><option value="local">local</option></select>
+        </label>
+      </form>
+      <div id="source"></div>
+      <div id="visitor" data-expanded="true"></div>
+      <div id="compiled" data-expanded="true"></div>
+      <div id="compiledModules" hidden></div>
+      <div id="compiledDependencies" hidden></div>
+    </main>
+    <script type="module" src="playground.js"></script>
+  </body>
+</html>
diff --git a/website/playground/playground.js b/website/playground/playground.js
new file mode 100644
index 0000000..81ce4bb
--- /dev/null
+++ b/website/playground/playground.js
@@ -0,0 +1,423 @@
+import * as localWasm from '../../wasm';
+import { EditorView, basicSetup } from 'codemirror';
+import { javascript } from '@codemirror/lang-javascript';
+import { css } from '@codemirror/lang-css';
+import { oneDark } from '@codemirror/theme-one-dark';
+import { syntaxTree } from '@codemirror/language';
+import { linter, lintGutter } from '@codemirror/lint'
+import { Compartment } from '@codemirror/state'
+
+const linterCompartment = new Compartment;
+const visitorLinterCompartment = new Compartment;
+
+let wasm;
+let editor, visitorEditor, outputEditor, modulesEditor, depsEditor;
+let enc = new TextEncoder();
+let dec = new TextDecoder();
+let inputs = document.querySelectorAll('input[type=number]');
+
+async function loadVersions() {
+  const { versions } = await fetch('https://data.jsdelivr.com/v1/package/npm/lightningcss-wasm').then(r => r.json());
+  versions
+    .map(v => {
+      const option = document.createElement('option');
+      option.value = v;
+      option.textContent = v;
+      return option;
+    })
+    .forEach(o => {
+      version.appendChild(o);
+    })
+}
+
+async function loadWasm() {
+  if (version.value === 'local') {
+    wasm = localWasm;
+  } else {
+    wasm = await new Function('version', 'return import(`https://esm.sh/lightningcss-wasm@${version}?bundle`)')(version.value);
+  }
+  await wasm.default();
+}
+
+function loadPlaygroundState() {
+  const hash = window.location.hash.slice(1);
+  try {
+    return JSON.parse(decodeURIComponent(hash));
+  } catch {
+    return {
+      minify: minify.checked,
+      visitorEnabled: visitorEnabled.checked,
+      targets: getTargets(),
+      include: 0,
+      exclude: 0,
+      source: `@custom-media --modern (color), (hover);
+
+.foo {
+  background: yellow;
+
+  -webkit-border-radius: 2px;
+  -moz-border-radius: 2px;
+  border-radius: 2px;
+
+  -webkit-transition: background 200ms;
+  -moz-transition: background 200ms;
+  transition: background 200ms;
+
+  &.bar {
+    color: green;
+  }
+}
+
+@media (--modern) and (width > 1024px) {
+  .a {
+    color: green;
+  }
+}`,
+      version: version.value,
+      visitor: `{
+  Color(color) {
+    if (color.type === 'rgb') {
+      color.g = 0;
+      return color;
+    }
+  }
+}`
+    };
+  }
+}
+
+function reflectPlaygroundState(playgroundState) {
+  if (typeof playgroundState.minify !== 'undefined') {
+    minify.checked = playgroundState.minify;
+  }
+
+  if (typeof playgroundState.cssModules !== 'undefined') {
+    cssModules.checked = playgroundState.cssModules;
+    compiledModules.hidden = !playgroundState.cssModules;
+  }
+
+  if (typeof playgroundState.analyzeDependencies !== 'undefined') {
+    analyzeDependencies.checked = playgroundState.analyzeDependencies;
+    compiledDependencies.hidden = !playgroundState.analyzeDependencies;
+  }
+
+  if (typeof playgroundState.customMedia !== 'undefined') {
+    customMedia.checked = playgroundState.customMedia;
+  }
+
+  if (typeof playgroundState.visitorEnabled !== 'undefined') {
+    visitorEnabled.checked = playgroundState.visitorEnabled;
+  }
+
+  if (playgroundState.targets) {
+    const { targets } = playgroundState;
+    for (let input of inputs) {
+      let value = targets[input.id];
+      input.value = value == null ? '' : value >> 16;
+    }
+  }
+
+  updateFeatures(sidebar.elements.include, playgroundState.include);
+  updateFeatures(sidebar.elements.exclude, playgroundState.exclude);
+  if (playgroundState.include) {
+    include.parentElement.open = true;
+  }
+  if (playgroundState.exclude) {
+    exclude.parentElement.open = true;
+  }
+
+  if (playgroundState.unusedSymbols) {
+    unusedSymbols.value = playgroundState.unusedSymbols.join('\n');
+  }
+
+  if (playgroundState.version) {
+    version.value = playgroundState.version;
+  }
+}
+
+function savePlaygroundState() {
+  let data = new FormData(sidebar);
+  const playgroundState = {
+    minify: minify.checked,
+    customMedia: customMedia.checked,
+    cssModules: cssModules.checked,
+    analyzeDependencies: analyzeDependencies.checked,
+    targets: getTargets(),
+    include: getFeatures(data.getAll('include')),
+    exclude: getFeatures(data.getAll('exclude')),
+    source: editor.state.doc.toString(),
+    visitorEnabled: visitorEnabled.checked,
+    visitor: visitorEditor.state.doc.toString(),
+    unusedSymbols: unusedSymbols.value.split('\n').map(v => v.trim()).filter(Boolean),
+    version: version.value,
+  };
+
+  const hash = encodeURIComponent(JSON.stringify(playgroundState));
+
+  if (
+    typeof URL === 'function' &&
+    typeof history === 'object' &&
+    typeof history.replaceState === 'function'
+  ) {
+    const url = new URL(location.href);
+    url.hash = hash;
+    history.replaceState(null, null, url);
+  } else {
+    location.hash = hash;
+  }
+}
+
+function getTargets() {
+  let targets = {};
+  for (let input of inputs) {
+    if (input.value !== '') {
+      targets[input.id] = input.valueAsNumber << 16;
+    }
+  }
+
+  return targets;
+}
+
+function getFeatures(vals) {
+  let features = 0;
+  for (let name of vals) {
+    features |= wasm.Features[name];
+  }
+  return features;
+}
+
+function updateFeatures(elements, include) {
+  for (let checkbox of elements) {
+    let feature = wasm.Features[checkbox.value];
+    checkbox.checked = (include & feature) === feature;
+    checkbox.indeterminate = !checkbox.checked && (include & feature);
+  }
+}
+
+function update() {
+  const { transform } = wasm;
+
+  const targets = getTargets();
+  let data = new FormData(sidebar);
+  let include = getFeatures(data.getAll('include'));
+  let exclude = getFeatures(data.getAll('exclude'));
+  try {
+    let res = transform({
+      filename: 'test.css',
+      code: enc.encode(editor.state.doc.toString()),
+      minify: minify.checked,
+      targets: Object.keys(targets).length === 0 ? null : targets,
+      include,
+      exclude,
+      drafts: {
+        customMedia: customMedia.checked
+      },
+      cssModules: cssModules.checked,
+      analyzeDependencies: analyzeDependencies.checked,
+      unusedSymbols: unusedSymbols.value.split('\n').map(v => v.trim()).filter(Boolean),
+      visitor: visitorEnabled.checked ? (0, eval)('(' + visitorEditor.state.doc.toString() + ')') : undefined,
+    });
+
+    let update = outputEditor.state.update({ changes: { from: 0, to: outputEditor.state.doc.length, insert: dec.decode(res.code) } });
+    outputEditor.update([update]);
+
+    if (res.exports) {
+      let update = modulesEditor.state.update({ changes: { from: 0, to: modulesEditor.state.doc.length, insert: '// CSS module exports\n' + JSON.stringify(res.exports, false, 2) } });
+      modulesEditor.update([update]);
+    }
+
+    if (res.dependencies) {
+      let update = depsEditor.state.update({ changes: { from: 0, to: depsEditor.state.doc.length, insert: '// Dependencies\n' + JSON.stringify(res.dependencies, false, 2) } });
+      depsEditor.update([update]);
+    }
+
+    compiledModules.hidden = !cssModules.checked;
+    compiledDependencies.hidden = !analyzeDependencies.checked;
+    visitor.hidden = !visitorEnabled.checked;
+    source.dataset.expanded = visitor.hidden;
+    compiled.dataset.expanded = compiledModules.hidden && compiledDependencies.hidden;
+    compiledModules.dataset.expanded = compiledDependencies.hidden;
+    compiledDependencies.dataset.expanded = compiledModules.hidden;
+
+    editor.dispatch({
+      effects: linterCompartment.reconfigure(createCssLinter(null, res.warnings))
+    });
+
+    visitorEditor.dispatch({
+      effects: visitorLinterCompartment.reconfigure(createVisitorLinter(null))
+    });
+  } catch (e) {
+    let update = outputEditor.state.update({ changes: { from: 0, to: outputEditor.state.doc.length, insert: `/* ERROR: ${e.message} */` } });
+    outputEditor.update([update]);
+
+    editor.dispatch({
+      effects: linterCompartment.reconfigure(createCssLinter(e))
+    });
+
+    visitorEditor.dispatch({
+      effects: visitorLinterCompartment.reconfigure(createVisitorLinter(e))
+    });
+  }
+
+  savePlaygroundState();
+}
+
+function createCssLinter(lastError, warnings) {
+  return linter(view => {
+    let diagnostics = [];
+    if (lastError && lastError.loc) {
+      let l = view.state.doc.line(lastError.loc.line);
+      let loc = l.from + lastError.loc.column - 1;
+      let node = syntaxTree(view.state).resolveInner(loc, 1);
+      diagnostics.push(
+        {
+          from: node.from,
+          to: node.to,
+          message: lastError.message,
+          severity: 'error'
+        }
+      );
+    }
+    if (warnings) {
+      for (let warning of warnings) {
+        let l = view.state.doc.line(warning.loc.line);
+        let loc = l.from + warning.loc.column - 1;
+        let node = syntaxTree(view.state).resolveInner(loc, 1);
+        diagnostics.push({
+          from: node.from,
+          to: node.to,
+          message: warning.message,
+          severity: 'warning'
+        });
+      }
+    }
+    return diagnostics;
+  }, { delay: 0 });
+}
+
+function createVisitorLinter(lastError) {
+  return linter(view => {
+    if (lastError && !lastError.loc) {
+      // Firefox has lineNumber and columnNumber, Safari has line and column.
+      let line = lastError.lineNumber ?? lastError.line;
+      let column = lastError.columnNumber ?? lastError.column;
+      if (lastError.column != null) {
+        column--;
+      }
+
+      if (line == null) {
+        // Chrome.
+        let match = lastError.stack.match(/(?:(?:eval.*<anonymous>:)|(?:eval:))(?<line>\d+):(?<column>\d+)/);
+        if (match) {
+          line = Number(match.groups.line);
+          column = Number(match.groups.column);
+
+          // Chrome's column numbers are off by the amount of leading whitespace in the line.
+          let l = view.state.doc.line(line);
+          let m = l.text.match(/^\s*/);
+          if (m) {
+            column += m[0].length;
+          }
+        }
+      }
+
+      if (line != null) {
+        let l = view.state.doc.line(line);
+        let loc = l.from + column;
+        let node = syntaxTree(view.state).resolveInner(loc, -1);
+        return [
+          {
+            from: node.from,
+            to: node.to,
+            message: lastError.message,
+            severity: 'error'
+          }
+        ];
+      }
+    }
+    return [];
+  }, { delay: 0 });
+}
+
+function renderFeatures(parent, name) {
+  for (let feature in wasm.Features) {
+    let label = document.createElement('label');
+    let checkbox = document.createElement('input');
+    checkbox.type = 'checkbox';
+    checkbox.name = name;
+    checkbox.value = feature;
+    checkbox.oninput = () => {
+      let data = new FormData(sidebar);
+      let flags = getFeatures(data.getAll(name));
+      let f = wasm.Features[feature];
+      if (checkbox.checked) {
+        flags |= f;
+      } else {
+        flags &= ~f;
+      }
+      updateFeatures(sidebar.elements[name], flags);
+    };
+    label.appendChild(checkbox);
+    label.appendChild(document.createTextNode(' ' + feature))
+    parent.appendChild(label);
+  }
+}
+
+async function main() {
+  await loadWasm();
+  renderFeatures(include, 'include');
+  renderFeatures(exclude, 'exclude');
+
+  let state = loadPlaygroundState();
+  reflectPlaygroundState(state);
+
+  editor = new EditorView({
+    extensions: [lintGutter(), basicSetup, css(), oneDark, linterCompartment.of(createCssLinter())],
+    parent: source,
+    doc: state.source,
+    dispatch(tr) {
+      editor.update([tr]);
+      if (tr.docChanged) {
+        update();
+      }
+    }
+  });
+
+  visitorEditor = new EditorView({
+    extensions: [lintGutter(), basicSetup, javascript(), oneDark, visitorLinterCompartment.of(createVisitorLinter())],
+    parent: visitor,
+    doc: state.visitor,
+    dispatch(tr) {
+      visitorEditor.update([tr]);
+      if (tr.docChanged) {
+        update();
+      }
+    }
+  });
+
+  outputEditor = new EditorView({
+    extensions: [basicSetup, css(), oneDark, EditorView.editable.of(false), EditorView.lineWrapping],
+    parent: compiled,
+  });
+
+  modulesEditor = new EditorView({
+    extensions: [basicSetup, javascript(), oneDark, EditorView.editable.of(false), EditorView.lineWrapping],
+    parent: compiledModules,
+  });
+
+  depsEditor = new EditorView({
+    extensions: [basicSetup, javascript(), oneDark, EditorView.editable.of(false), EditorView.lineWrapping],
+    parent: compiledDependencies,
+  });
+
+  update();
+  sidebar.oninput = update;
+
+  await loadVersions();
+  version.onchange = async () => {
+    await loadWasm();
+    update();
+  };
+}
+
+main();
diff --git a/website/synthwave.css b/website/synthwave.css
new file mode 100644
index 0000000..9395b6a
--- /dev/null
+++ b/website/synthwave.css
@@ -0,0 +1,138 @@
+/*
+ * Based on Synthwave '84 Theme originally by Robb Owen [@Robb0wen] for Visual Studio Code
+ * Originally ported for PrismJS by Marc Backes [@themarcba]
+ */
+
+pre[class*="language-"] {
+  color: lab(64% 103 0);
+  text-shadow: 0 0 10px lab(64% 103 0 / .5);
+  background: rgb(255 255 255 / .05);
+  display: block;
+  padding: 20px;
+  border-radius: 8px;
+  font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
+  font-size: 13px;
+  text-align: left;
+  white-space: pre-wrap;
+  word-spacing: normal;
+  word-break: normal;
+  word-wrap: break-word;
+  line-height: 1.5;
+
+  -moz-tab-size: 4;
+  -o-tab-size: 4;
+  tab-size: 4;
+
+  -webkit-hyphens: none;
+  -moz-hyphens: none;
+  -ms-hyphens: none;
+  hyphens: none;
+}
+
+/* Code blocks */
+pre[class*="language-"] {
+  padding: 1em;
+  margin: .5em 0;
+  overflow: auto;
+}
+
+/* Inline code */
+:not(pre) > code[class*="language-"] {
+  padding: .1em;
+  border-radius: .3em;
+  white-space: normal;
+}
+
+.token.comment,
+.token.block-comment,
+.token.prolog,
+.token.doctype,
+.token.cdata {
+  color: #8e8e8e;
+  text-shadow: none;
+}
+
+.token.punctuation {
+  color: #ccc;
+}
+
+.token.tag,
+.token.attr-name,
+.token.namespace,
+.token.number,
+.token.unit,
+.token.hexcode,
+.token.deleted,
+.token.function {
+  color: lch(65% 85 35);
+  text-shadow: 0 0 10px lch(65% 85 35 / .5);
+}
+
+.token.property,
+.token.selector {
+  color: lch(85% 58 205);
+  text-shadow: 0 0 10px lch(85% 58 205 / .5);
+}
+
+.token.function-name {
+  color: #6196cc;
+}
+
+.token.boolean,
+.token.selector .token.id {
+  color: #fdfdfd;
+  text-shadow: 0 0 2px #001716, 0 0 3px #03edf975, 0 0 5px #03edf975, 0 0 8px #03edf975;
+}
+
+.token.class-name {
+  color: #fff5f6;
+  text-shadow: 0 0 2px #000, 0 0 10px #fc1f2c75, 0 0 5px #fc1f2c75, 0 0 25px #fc1f2c75;
+}
+
+.token.constant,
+.token.symbol {
+  color: lab(64% 103 0);
+  text-shadow: 0 0 2px #100c0f, 0 0 5px #dc078e33, 0 0 10px #fff3;
+}
+
+.token.important,
+.token.atrule,
+.token.keyword,
+.token.selector .token.class,
+.token.builtin {
+  color: #f4eee4;
+  text-shadow: 0 0 2px #393a33, 0 0 8px #f39f0575, 0 0 2px #f39f0575;
+}
+
+.token.string,
+.token.string-property,
+.token.char,
+.token.attr-value,
+.token.regex,
+.token.variable,
+.token.url {
+  color: lch(85% 82.34 80.104);
+  text-shadow: 0 0 10px lch(85% 82.34 80.104 / .5);
+}
+
+.token.operator,
+.token.entity {
+  color: #67cdcc;
+}
+
+.token.important,
+.token.bold {
+  font-weight: bold;
+}
+
+.token.italic {
+  font-style: italic;
+}
+
+.token.entity {
+  cursor: help;
+}
+
+.token.inserted {
+  color: green;
+}
diff --git a/website/transforms.html b/website/transforms.html
new file mode 100644
index 0000000..4208390
--- /dev/null
+++ b/website/transforms.html
@@ -0,0 +1 @@
+<include src="website/include/layout.html" locals='{"title": "Custom Transforms", "url": "transforms.html", "page": "website/pages/transforms.md"}' />
diff --git a/website/transpilation.html b/website/transpilation.html
new file mode 100644
index 0000000..64ec9c6
--- /dev/null
+++ b/website/transpilation.html
@@ -0,0 +1 @@
+<include src="website/include/layout.html" locals='{"title": "Transpilation", "url": "transpilation.html", "page": "website/pages/transpilation.md"}' />
diff --git a/yarn.lock b/yarn.lock
new file mode 100644
index 0000000..4c16f33
--- /dev/null
+++ b/yarn.lock
@@ -0,0 +1,4084 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.16.7", "@babel/code-frame@^7.21.4", "@babel/code-frame@^7.25.9":
+  version "7.26.2"
+  resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.26.2.tgz#4b5fab97d33338eff916235055f0ebc21e573a85"
+  integrity sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==
+  dependencies:
+    "@babel/helper-validator-identifier" "^7.25.9"
+    js-tokens "^4.0.0"
+    picocolors "^1.0.0"
+
+"@babel/generator@^7.21.4":
+  version "7.26.3"
+  resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.26.3.tgz#ab8d4360544a425c90c248df7059881f4b2ce019"
+  integrity sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ==
+  dependencies:
+    "@babel/parser" "^7.26.3"
+    "@babel/types" "^7.26.3"
+    "@jridgewell/gen-mapping" "^0.3.5"
+    "@jridgewell/trace-mapping" "^0.3.25"
+    jsesc "^3.0.2"
+
+"@babel/helper-environment-visitor@^7.18.9":
+  version "7.24.7"
+  resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz#4b31ba9551d1f90781ba83491dd59cf9b269f7d9"
+  integrity sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==
+  dependencies:
+    "@babel/types" "^7.24.7"
+
+"@babel/helper-function-name@^7.21.0":
+  version "7.24.7"
+  resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz#75f1e1725742f39ac6584ee0b16d94513da38dd2"
+  integrity sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==
+  dependencies:
+    "@babel/template" "^7.24.7"
+    "@babel/types" "^7.24.7"
+
+"@babel/helper-hoist-variables@^7.18.6":
+  version "7.24.7"
+  resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz#b4ede1cde2fd89436397f30dc9376ee06b0f25ee"
+  integrity sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==
+  dependencies:
+    "@babel/types" "^7.24.7"
+
+"@babel/helper-split-export-declaration@^7.18.6":
+  version "7.24.7"
+  resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz#83949436890e07fa3d6873c61a96e3bbf692d856"
+  integrity sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==
+  dependencies:
+    "@babel/types" "^7.24.7"
+
+"@babel/helper-string-parser@^7.25.9":
+  version "7.25.9"
+  resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz#1aabb72ee72ed35789b4bbcad3ca2862ce614e8c"
+  integrity sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==
+
+"@babel/helper-validator-identifier@^7.25.9":
+  version "7.25.9"
+  resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz#24b64e2c3ec7cd3b3c547729b8d16871f22cbdc7"
+  integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==
+
+"@babel/highlight@^7.16.7":
+  version "7.25.9"
+  resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.25.9.tgz#8141ce68fc73757946f983b343f1231f4691acc6"
+  integrity sha512-llL88JShoCsth8fF8R4SJnIn+WLvR6ccFxu1H3FlMhDontdcmZWf2HgIZ7AIqV3Xcck1idlohrN4EUBQz6klbw==
+  dependencies:
+    "@babel/helper-validator-identifier" "^7.25.9"
+    chalk "^2.4.2"
+    js-tokens "^4.0.0"
+    picocolors "^1.0.0"
+
+"@babel/parser@7.21.4":
+  version "7.21.4"
+  resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.21.4.tgz#94003fdfc520bbe2875d4ae557b43ddb6d880f17"
+  integrity sha512-alVJj7k7zIxqBZ7BTRhz0IqJFxW1VJbm6N8JbcYhQ186df9ZBPbZBmWSqAMXwHGsCJdYks7z/voa3ibiS5bCIw==
+
+"@babel/parser@^7.21.4", "@babel/parser@^7.25.9", "@babel/parser@^7.26.3":
+  version "7.26.3"
+  resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.26.3.tgz#8c51c5db6ddf08134af1ddbacf16aaab48bac234"
+  integrity sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==
+  dependencies:
+    "@babel/types" "^7.26.3"
+
+"@babel/template@^7.24.7":
+  version "7.25.9"
+  resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.25.9.tgz#ecb62d81a8a6f5dc5fe8abfc3901fc52ddf15016"
+  integrity sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==
+  dependencies:
+    "@babel/code-frame" "^7.25.9"
+    "@babel/parser" "^7.25.9"
+    "@babel/types" "^7.25.9"
+
+"@babel/traverse@7.21.4":
+  version "7.21.4"
+  resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.21.4.tgz#a836aca7b116634e97a6ed99976236b3282c9d36"
+  integrity sha512-eyKrRHKdyZxqDm+fV1iqL9UAHMoIg0nDaGqfIOd8rKH17m5snv7Gn4qgjBoFfLz9APvjFU/ICT00NVCv1Epp8Q==
+  dependencies:
+    "@babel/code-frame" "^7.21.4"
+    "@babel/generator" "^7.21.4"
+    "@babel/helper-environment-visitor" "^7.18.9"
+    "@babel/helper-function-name" "^7.21.0"
+    "@babel/helper-hoist-variables" "^7.18.6"
+    "@babel/helper-split-export-declaration" "^7.18.6"
+    "@babel/parser" "^7.21.4"
+    "@babel/types" "^7.21.4"
+    debug "^4.1.0"
+    globals "^11.1.0"
+
+"@babel/types@^7.21.4", "@babel/types@^7.24.7", "@babel/types@^7.25.9", "@babel/types@^7.26.3":
+  version "7.26.3"
+  resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.26.3.tgz#37e79830f04c2b5687acc77db97fbc75fb81f3c0"
+  integrity sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==
+  dependencies:
+    "@babel/helper-string-parser" "^7.25.9"
+    "@babel/helper-validator-identifier" "^7.25.9"
+
+"@bcherny/json-schema-ref-parser@10.0.5-fork":
+  version "10.0.5-fork"
+  resolved "https://registry.yarnpkg.com/@bcherny/json-schema-ref-parser/-/json-schema-ref-parser-10.0.5-fork.tgz#9b5e1e7e07964ea61840174098e634edbe8197bc"
+  integrity sha512-E/jKbPoca1tfUPj3iSbitDZTGnq6FUFjkH6L8U2oDwSuwK1WhnnVtCG7oFOTg/DDnyoXbQYUiUiGOibHqaGVnw==
+  dependencies:
+    "@jsdevtools/ono" "^7.1.3"
+    "@types/json-schema" "^7.0.6"
+    call-me-maybe "^1.0.1"
+    js-yaml "^4.1.0"
+
+"@codemirror/autocomplete@^6.0.0":
+  version "6.18.4"
+  resolved "https://registry.yarnpkg.com/@codemirror/autocomplete/-/autocomplete-6.18.4.tgz#4394f55d6771727179f2e28a871ef46bbbeb11b1"
+  integrity sha512-sFAphGQIqyQZfP2ZBsSHV7xQvo9Py0rV0dW7W3IMRdS+zDuNb2l3no78CvUaWKGfzFjI4FTrLdUSj86IGb2hRA==
+  dependencies:
+    "@codemirror/language" "^6.0.0"
+    "@codemirror/state" "^6.0.0"
+    "@codemirror/view" "^6.17.0"
+    "@lezer/common" "^1.0.0"
+
+"@codemirror/commands@^6.0.0":
+  version "6.7.1"
+  resolved "https://registry.yarnpkg.com/@codemirror/commands/-/commands-6.7.1.tgz#04561e95bc0779eaa49efd63e916c4efb3bbf6d6"
+  integrity sha512-llTrboQYw5H4THfhN4U3qCnSZ1SOJ60ohhz+SzU0ADGtwlc533DtklQP0vSFaQuCPDn3BPpOd1GbbnUtwNjsrw==
+  dependencies:
+    "@codemirror/language" "^6.0.0"
+    "@codemirror/state" "^6.4.0"
+    "@codemirror/view" "^6.27.0"
+    "@lezer/common" "^1.1.0"
+
+"@codemirror/lang-css@^6.0.1":
+  version "6.3.1"
+  resolved "https://registry.yarnpkg.com/@codemirror/lang-css/-/lang-css-6.3.1.tgz#763ca41aee81bb2431be55e3cfcc7cc8e91421a3"
+  integrity sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==
+  dependencies:
+    "@codemirror/autocomplete" "^6.0.0"
+    "@codemirror/language" "^6.0.0"
+    "@codemirror/state" "^6.0.0"
+    "@lezer/common" "^1.0.2"
+    "@lezer/css" "^1.1.7"
+
+"@codemirror/lang-javascript@^6.1.2":
+  version "6.2.2"
+  resolved "https://registry.yarnpkg.com/@codemirror/lang-javascript/-/lang-javascript-6.2.2.tgz#7141090b22994bef85bcc5608a3bc1257f2db2ad"
+  integrity sha512-VGQfY+FCc285AhWuwjYxQyUQcYurWlxdKYT4bqwr3Twnd5wP5WSeu52t4tvvuWmljT4EmgEgZCqSieokhtY8hg==
+  dependencies:
+    "@codemirror/autocomplete" "^6.0.0"
+    "@codemirror/language" "^6.6.0"
+    "@codemirror/lint" "^6.0.0"
+    "@codemirror/state" "^6.0.0"
+    "@codemirror/view" "^6.17.0"
+    "@lezer/common" "^1.0.0"
+    "@lezer/javascript" "^1.0.0"
+
+"@codemirror/language@^6.0.0", "@codemirror/language@^6.6.0":
+  version "6.10.7"
+  resolved "https://registry.yarnpkg.com/@codemirror/language/-/language-6.10.7.tgz#415ba3bb983416daa98084c010f4db59db45920e"
+  integrity sha512-aOswhVOLYhMNeqykt4P7+ukQSpGL0ynZYaEyFDVHE7fl2xgluU3yuE9MdgYNfw6EmaNidoFMIQ2iTh1ADrnT6A==
+  dependencies:
+    "@codemirror/state" "^6.0.0"
+    "@codemirror/view" "^6.23.0"
+    "@lezer/common" "^1.1.0"
+    "@lezer/highlight" "^1.0.0"
+    "@lezer/lr" "^1.0.0"
+    style-mod "^4.0.0"
+
+"@codemirror/lint@^6.0.0", "@codemirror/lint@^6.1.0":
+  version "6.8.4"
+  resolved "https://registry.yarnpkg.com/@codemirror/lint/-/lint-6.8.4.tgz#7d8aa5d1a6dec89ffcc23ad45ddca2e12e90982d"
+  integrity sha512-u4q7PnZlJUojeRe8FJa/njJcMctISGgPQ4PnWsd9268R4ZTtU+tfFYmwkBvgcrK2+QQ8tYFVALVb5fVJykKc5A==
+  dependencies:
+    "@codemirror/state" "^6.0.0"
+    "@codemirror/view" "^6.35.0"
+    crelt "^1.0.5"
+
+"@codemirror/search@^6.0.0":
+  version "6.5.8"
+  resolved "https://registry.yarnpkg.com/@codemirror/search/-/search-6.5.8.tgz#b59b3659b46184cc75d6108d7c050a4ca344c3a0"
+  integrity sha512-PoWtZvo7c1XFeZWmmyaOp2G0XVbOnm+fJzvghqGAktBW3cufwJUWvSCcNG0ppXiBEM05mZu6RhMtXPv2hpllig==
+  dependencies:
+    "@codemirror/state" "^6.0.0"
+    "@codemirror/view" "^6.0.0"
+    crelt "^1.0.5"
+
+"@codemirror/state@^6.0.0", "@codemirror/state@^6.4.0", "@codemirror/state@^6.5.0":
+  version "6.5.0"
+  resolved "https://registry.yarnpkg.com/@codemirror/state/-/state-6.5.0.tgz#e98dde85620618651543152fe1c2483300a0ccc9"
+  integrity sha512-MwBHVK60IiIHDcoMet78lxt6iw5gJOGSbNbOIVBHWVXIH4/Nq1+GQgLLGgI1KlnN86WDXsPudVaqYHKBIx7Eyw==
+  dependencies:
+    "@marijn/find-cluster-break" "^1.0.0"
+
+"@codemirror/theme-one-dark@^6.1.0":
+  version "6.1.2"
+  resolved "https://registry.yarnpkg.com/@codemirror/theme-one-dark/-/theme-one-dark-6.1.2.tgz#fcef9f9cfc17a07836cb7da17c9f6d7231064df8"
+  integrity sha512-F+sH0X16j/qFLMAfbciKTxVOwkdAS336b7AXTKOZhy8BR3eH/RelsnLgLFINrpST63mmN2OuwUt0W2ndUgYwUA==
+  dependencies:
+    "@codemirror/language" "^6.0.0"
+    "@codemirror/state" "^6.0.0"
+    "@codemirror/view" "^6.0.0"
+    "@lezer/highlight" "^1.0.0"
+
+"@codemirror/view@^6.0.0", "@codemirror/view@^6.17.0", "@codemirror/view@^6.23.0", "@codemirror/view@^6.27.0", "@codemirror/view@^6.35.0":
+  version "6.36.1"
+  resolved "https://registry.yarnpkg.com/@codemirror/view/-/view-6.36.1.tgz#3c543b8fd72c96b30c4b2b1464d1ebce7e0c5c4b"
+  integrity sha512-miD1nyT4m4uopZaDdO2uXU/LLHliKNYL9kB1C1wJHrunHLm/rpkb5QVSokqgw9hFqEZakrdlb/VGWX8aYZTslQ==
+  dependencies:
+    "@codemirror/state" "^6.5.0"
+    style-mod "^4.1.0"
+    w3c-keyname "^2.2.4"
+
+"@emnapi/runtime@^1.2.0":
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.3.1.tgz#0fcaa575afc31f455fd33534c19381cfce6c6f60"
+  integrity sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==
+  dependencies:
+    tslib "^2.4.0"
+
+"@esbuild/aix-ppc64@0.19.12":
+  version "0.19.12"
+  resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz#d1bc06aedb6936b3b6d313bf809a5a40387d2b7f"
+  integrity sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==
+
+"@esbuild/android-arm64@0.19.12":
+  version "0.19.12"
+  resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz#7ad65a36cfdb7e0d429c353e00f680d737c2aed4"
+  integrity sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==
+
+"@esbuild/android-arm@0.19.12":
+  version "0.19.12"
+  resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.19.12.tgz#b0c26536f37776162ca8bde25e42040c203f2824"
+  integrity sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==
+
+"@esbuild/android-x64@0.19.12":
+  version "0.19.12"
+  resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.19.12.tgz#cb13e2211282012194d89bf3bfe7721273473b3d"
+  integrity sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==
+
+"@esbuild/darwin-arm64@0.19.12":
+  version "0.19.12"
+  resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz#cbee41e988020d4b516e9d9e44dd29200996275e"
+  integrity sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==
+
+"@esbuild/darwin-x64@0.19.12":
+  version "0.19.12"
+  resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz#e37d9633246d52aecf491ee916ece709f9d5f4cd"
+  integrity sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==
+
+"@esbuild/freebsd-arm64@0.19.12":
+  version "0.19.12"
+  resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz#1ee4d8b682ed363b08af74d1ea2b2b4dbba76487"
+  integrity sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==
+
+"@esbuild/freebsd-x64@0.19.12":
+  version "0.19.12"
+  resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz#37a693553d42ff77cd7126764b535fb6cc28a11c"
+  integrity sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==
+
+"@esbuild/linux-arm64@0.19.12":
+  version "0.19.12"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz#be9b145985ec6c57470e0e051d887b09dddb2d4b"
+  integrity sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==
+
+"@esbuild/linux-arm@0.19.12":
+  version "0.19.12"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz#207ecd982a8db95f7b5279207d0ff2331acf5eef"
+  integrity sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==
+
+"@esbuild/linux-ia32@0.19.12":
+  version "0.19.12"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz#d0d86b5ca1562523dc284a6723293a52d5860601"
+  integrity sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==
+
+"@esbuild/linux-loong64@0.19.12":
+  version "0.19.12"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz#9a37f87fec4b8408e682b528391fa22afd952299"
+  integrity sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==
+
+"@esbuild/linux-mips64el@0.19.12":
+  version "0.19.12"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz#4ddebd4e6eeba20b509d8e74c8e30d8ace0b89ec"
+  integrity sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==
+
+"@esbuild/linux-ppc64@0.19.12":
+  version "0.19.12"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz#adb67dadb73656849f63cd522f5ecb351dd8dee8"
+  integrity sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==
+
+"@esbuild/linux-riscv64@0.19.12":
+  version "0.19.12"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz#11bc0698bf0a2abf8727f1c7ace2112612c15adf"
+  integrity sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==
+
+"@esbuild/linux-s390x@0.19.12":
+  version "0.19.12"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz#e86fb8ffba7c5c92ba91fc3b27ed5a70196c3cc8"
+  integrity sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==
+
+"@esbuild/linux-x64@0.19.12":
+  version "0.19.12"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz#5f37cfdc705aea687dfe5dfbec086a05acfe9c78"
+  integrity sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==
+
+"@esbuild/netbsd-x64@0.19.12":
+  version "0.19.12"
+  resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz#29da566a75324e0d0dd7e47519ba2f7ef168657b"
+  integrity sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==
+
+"@esbuild/openbsd-x64@0.19.12":
+  version "0.19.12"
+  resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz#306c0acbdb5a99c95be98bdd1d47c916e7dc3ff0"
+  integrity sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==
+
+"@esbuild/sunos-x64@0.19.12":
+  version "0.19.12"
+  resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz#0933eaab9af8b9b2c930236f62aae3fc593faf30"
+  integrity sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==
+
+"@esbuild/win32-arm64@0.19.12":
+  version "0.19.12"
+  resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz#773bdbaa1971b36db2f6560088639ccd1e6773ae"
+  integrity sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==
+
+"@esbuild/win32-ia32@0.19.12":
+  version "0.19.12"
+  resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz#000516cad06354cc84a73f0943a4aa690ef6fd67"
+  integrity sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==
+
+"@esbuild/win32-x64@0.19.12":
+  version "0.19.12"
+  resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz#c57c8afbb4054a3ab8317591a0b7320360b444ae"
+  integrity sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==
+
+"@img/sharp-darwin-arm64@0.33.5":
+  version "0.33.5"
+  resolved "https://registry.yarnpkg.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz#ef5b5a07862805f1e8145a377c8ba6e98813ca08"
+  integrity sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==
+  optionalDependencies:
+    "@img/sharp-libvips-darwin-arm64" "1.0.4"
+
+"@img/sharp-darwin-x64@0.33.5":
+  version "0.33.5"
+  resolved "https://registry.yarnpkg.com/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz#e03d3451cd9e664faa72948cc70a403ea4063d61"
+  integrity sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==
+  optionalDependencies:
+    "@img/sharp-libvips-darwin-x64" "1.0.4"
+
+"@img/sharp-libvips-darwin-arm64@1.0.4":
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz#447c5026700c01a993c7804eb8af5f6e9868c07f"
+  integrity sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==
+
+"@img/sharp-libvips-darwin-x64@1.0.4":
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz#e0456f8f7c623f9dbfbdc77383caa72281d86062"
+  integrity sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==
+
+"@img/sharp-libvips-linux-arm64@1.0.4":
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz#979b1c66c9a91f7ff2893556ef267f90ebe51704"
+  integrity sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==
+
+"@img/sharp-libvips-linux-arm@1.0.5":
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz#99f922d4e15216ec205dcb6891b721bfd2884197"
+  integrity sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==
+
+"@img/sharp-libvips-linux-s390x@1.0.4":
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz#f8a5eb1f374a082f72b3f45e2fb25b8118a8a5ce"
+  integrity sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==
+
+"@img/sharp-libvips-linux-x64@1.0.4":
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz#d4c4619cdd157774906e15770ee119931c7ef5e0"
+  integrity sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==
+
+"@img/sharp-libvips-linuxmusl-arm64@1.0.4":
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz#166778da0f48dd2bded1fa3033cee6b588f0d5d5"
+  integrity sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==
+
+"@img/sharp-libvips-linuxmusl-x64@1.0.4":
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz#93794e4d7720b077fcad3e02982f2f1c246751ff"
+  integrity sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==
+
+"@img/sharp-linux-arm64@0.33.5":
+  version "0.33.5"
+  resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz#edb0697e7a8279c9fc829a60fc35644c4839bb22"
+  integrity sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==
+  optionalDependencies:
+    "@img/sharp-libvips-linux-arm64" "1.0.4"
+
+"@img/sharp-linux-arm@0.33.5":
+  version "0.33.5"
+  resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz#422c1a352e7b5832842577dc51602bcd5b6f5eff"
+  integrity sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==
+  optionalDependencies:
+    "@img/sharp-libvips-linux-arm" "1.0.5"
+
+"@img/sharp-linux-s390x@0.33.5":
+  version "0.33.5"
+  resolved "https://registry.yarnpkg.com/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz#f5c077926b48e97e4a04d004dfaf175972059667"
+  integrity sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==
+  optionalDependencies:
+    "@img/sharp-libvips-linux-s390x" "1.0.4"
+
+"@img/sharp-linux-x64@0.33.5":
+  version "0.33.5"
+  resolved "https://registry.yarnpkg.com/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz#d806e0afd71ae6775cc87f0da8f2d03a7c2209cb"
+  integrity sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==
+  optionalDependencies:
+    "@img/sharp-libvips-linux-x64" "1.0.4"
+
+"@img/sharp-linuxmusl-arm64@0.33.5":
+  version "0.33.5"
+  resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz#252975b915894fb315af5deea174651e208d3d6b"
+  integrity sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==
+  optionalDependencies:
+    "@img/sharp-libvips-linuxmusl-arm64" "1.0.4"
+
+"@img/sharp-linuxmusl-x64@0.33.5":
+  version "0.33.5"
+  resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz#3f4609ac5d8ef8ec7dadee80b560961a60fd4f48"
+  integrity sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==
+  optionalDependencies:
+    "@img/sharp-libvips-linuxmusl-x64" "1.0.4"
+
+"@img/sharp-wasm32@0.33.5":
+  version "0.33.5"
+  resolved "https://registry.yarnpkg.com/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz#6f44f3283069d935bb5ca5813153572f3e6f61a1"
+  integrity sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==
+  dependencies:
+    "@emnapi/runtime" "^1.2.0"
+
+"@img/sharp-win32-ia32@0.33.5":
+  version "0.33.5"
+  resolved "https://registry.yarnpkg.com/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz#1a0c839a40c5351e9885628c85f2e5dfd02b52a9"
+  integrity sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==
+
+"@img/sharp-win32-x64@0.33.5":
+  version "0.33.5"
+  resolved "https://registry.yarnpkg.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz#56f00962ff0c4e0eb93d34a047d29fa995e3e342"
+  integrity sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==
+
+"@jridgewell/gen-mapping@^0.3.5":
+  version "0.3.8"
+  resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz#4f0e06362e01362f823d348f1872b08f666d8142"
+  integrity sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==
+  dependencies:
+    "@jridgewell/set-array" "^1.2.1"
+    "@jridgewell/sourcemap-codec" "^1.4.10"
+    "@jridgewell/trace-mapping" "^0.3.24"
+
+"@jridgewell/resolve-uri@^3.1.0":
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6"
+  integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==
+
+"@jridgewell/set-array@^1.2.1":
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280"
+  integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==
+
+"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14":
+  version "1.5.0"
+  resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a"
+  integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==
+
+"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25":
+  version "0.3.25"
+  resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0"
+  integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==
+  dependencies:
+    "@jridgewell/resolve-uri" "^3.1.0"
+    "@jridgewell/sourcemap-codec" "^1.4.14"
+
+"@jsdevtools/ono@^7.1.3":
+  version "7.1.3"
+  resolved "https://registry.yarnpkg.com/@jsdevtools/ono/-/ono-7.1.3.tgz#9df03bbd7c696a5c58885c34aa06da41c8543796"
+  integrity sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==
+
+"@lezer/common@^1.0.0", "@lezer/common@^1.0.2", "@lezer/common@^1.1.0", "@lezer/common@^1.2.0":
+  version "1.2.3"
+  resolved "https://registry.yarnpkg.com/@lezer/common/-/common-1.2.3.tgz#138fcddab157d83da557554851017c6c1e5667fd"
+  integrity sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==
+
+"@lezer/css@^1.1.7":
+  version "1.1.9"
+  resolved "https://registry.yarnpkg.com/@lezer/css/-/css-1.1.9.tgz#404563d361422c5a1fe917295f1527ee94845ed1"
+  integrity sha512-TYwgljcDv+YrV0MZFFvYFQHCfGgbPMR6nuqLabBdmZoFH3EP1gvw8t0vae326Ne3PszQkbXfVBjCnf3ZVCr0bA==
+  dependencies:
+    "@lezer/common" "^1.2.0"
+    "@lezer/highlight" "^1.0.0"
+    "@lezer/lr" "^1.0.0"
+
+"@lezer/highlight@^1.0.0", "@lezer/highlight@^1.1.3":
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/@lezer/highlight/-/highlight-1.2.1.tgz#596fa8f9aeb58a608be0a563e960c373cbf23f8b"
+  integrity sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==
+  dependencies:
+    "@lezer/common" "^1.0.0"
+
+"@lezer/javascript@^1.0.0":
+  version "1.4.21"
+  resolved "https://registry.yarnpkg.com/@lezer/javascript/-/javascript-1.4.21.tgz#8ebf7d1f891c70e3d00864f5a03ac42c75d19492"
+  integrity sha512-lL+1fcuxWYPURMM/oFZLEDm0XuLN128QPV+VuGtKpeaOGdcl9F2LYC3nh1S9LkPqx9M0mndZFdXCipNAZpzIkQ==
+  dependencies:
+    "@lezer/common" "^1.2.0"
+    "@lezer/highlight" "^1.1.3"
+    "@lezer/lr" "^1.3.0"
+
+"@lezer/lr@^1.0.0", "@lezer/lr@^1.3.0":
+  version "1.4.2"
+  resolved "https://registry.yarnpkg.com/@lezer/lr/-/lr-1.4.2.tgz#931ea3dea8e9de84e90781001dae30dea9ff1727"
+  integrity sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==
+  dependencies:
+    "@lezer/common" "^1.0.0"
+
+"@lmdb/lmdb-darwin-arm64@2.8.5":
+  version "2.8.5"
+  resolved "https://registry.yarnpkg.com/@lmdb/lmdb-darwin-arm64/-/lmdb-darwin-arm64-2.8.5.tgz#895d8cb16a9d709ce5fedd8b60022903b875e08e"
+  integrity sha512-KPDeVScZgA1oq0CiPBcOa3kHIqU+pTOwRFDIhxvmf8CTNvqdZQYp5cCKW0bUk69VygB2PuTiINFWbY78aR2pQw==
+
+"@lmdb/lmdb-darwin-x64@2.8.5":
+  version "2.8.5"
+  resolved "https://registry.yarnpkg.com/@lmdb/lmdb-darwin-x64/-/lmdb-darwin-x64-2.8.5.tgz#ca243534c8b37d5516c557e4624256d18dd63184"
+  integrity sha512-w/sLhN4T7MW1nB3R/U8WK5BgQLz904wh+/SmA2jD8NnF7BLLoUgflCNxOeSPOWp8geP6nP/+VjWzZVip7rZ1ug==
+
+"@lmdb/lmdb-linux-arm64@2.8.5":
+  version "2.8.5"
+  resolved "https://registry.yarnpkg.com/@lmdb/lmdb-linux-arm64/-/lmdb-linux-arm64-2.8.5.tgz#b44a8023057e21512eefb9f6120096843b531c1e"
+  integrity sha512-vtbZRHH5UDlL01TT5jB576Zox3+hdyogvpcbvVJlmU5PdL3c5V7cj1EODdh1CHPksRl+cws/58ugEHi8bcj4Ww==
+
+"@lmdb/lmdb-linux-arm@2.8.5":
+  version "2.8.5"
+  resolved "https://registry.yarnpkg.com/@lmdb/lmdb-linux-arm/-/lmdb-linux-arm-2.8.5.tgz#17bd54740779c3e4324e78e8f747c21416a84b3d"
+  integrity sha512-c0TGMbm2M55pwTDIfkDLB6BpIsgxV4PjYck2HiOX+cy/JWiBXz32lYbarPqejKs9Flm7YVAKSILUducU9g2RVg==
+
+"@lmdb/lmdb-linux-x64@2.8.5":
+  version "2.8.5"
+  resolved "https://registry.yarnpkg.com/@lmdb/lmdb-linux-x64/-/lmdb-linux-x64-2.8.5.tgz#6c61835b6cc58efdf79dbd5e8c72a38300a90302"
+  integrity sha512-Xkc8IUx9aEhP0zvgeKy7IQ3ReX2N8N1L0WPcQwnZweWmOuKfwpS3GRIYqLtK5za/w3E60zhFfNdS+3pBZPytqQ==
+
+"@lmdb/lmdb-win32-x64@2.8.5":
+  version "2.8.5"
+  resolved "https://registry.yarnpkg.com/@lmdb/lmdb-win32-x64/-/lmdb-win32-x64-2.8.5.tgz#8233e8762440b0f4632c47a09b1b6f23de8b934c"
+  integrity sha512-4wvrf5BgnR8RpogHhtpCPJMKBmvyZPhhUtEwMJbXh0ni2BucpfF07jlmyM11zRqQ2XIq6PbC2j7W7UCCcm1rRQ==
+
+"@marijn/find-cluster-break@^1.0.0":
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz#775374306116d51c0c500b8c4face0f9a04752d8"
+  integrity sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==
+
+"@mdn/browser-compat-data@~6.0.13":
+  version "6.0.13"
+  resolved "https://registry.yarnpkg.com/@mdn/browser-compat-data/-/browser-compat-data-6.0.13.tgz#1e2a45467a472d6e82d48ba544672b834ba26b61"
+  integrity sha512-RSYYaex/5lC4mim5pMTKHxfSpWf7Oc4r8Vk4exFVvt/KzIj7GacXAiYu5WfGnIWrBaa0KaSPda7oanQiFwjbeg==
+
+"@mischnic/json-sourcemap@^0.1.0":
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/@mischnic/json-sourcemap/-/json-sourcemap-0.1.1.tgz#0ef9b015a8f575dd9a8720d9a6b4dbc988425906"
+  integrity sha512-iA7+tyVqfrATAIsIRWQG+a7ZLLD0VaOCKV2Wd/v4mqIU3J9c4jx9p7S0nw1XH3gJCKNBOOwACOPYYSUu9pgT+w==
+  dependencies:
+    "@lezer/common" "^1.0.0"
+    "@lezer/lr" "^1.0.0"
+    json5 "^2.2.1"
+
+"@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3":
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz#9edec61b22c3082018a79f6d1c30289ddf3d9d11"
+  integrity sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==
+
+"@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3":
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz#33677a275204898ad8acbf62734fc4dc0b6a4855"
+  integrity sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==
+
+"@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3":
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz#19edf7cdc2e7063ee328403c1d895a86dd28f4bb"
+  integrity sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==
+
+"@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3":
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz#94fb0543ba2e28766c3fc439cabbe0440ae70159"
+  integrity sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==
+
+"@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3":
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz#4a0609ab5fe44d07c9c60a11e4484d3c38bbd6e3"
+  integrity sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==
+
+"@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3":
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz#0aa5502d547b57abfc4ac492de68e2006e417242"
+  integrity sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==
+
+"@napi-rs/cli@^2.14.0":
+  version "2.18.4"
+  resolved "https://registry.yarnpkg.com/@napi-rs/cli/-/cli-2.18.4.tgz#12bebfb7995902fa7ab43cc0b155a7f5a2caa873"
+  integrity sha512-SgJeA4df9DE2iAEpr3M2H0OKl/yjtg1BnRI5/JyowS71tUWhrfSu2LT0V3vlHET+g1hBVlrO60PmEXwUEKp8Mg==
+
+"@parcel/bundler-default@2.13.3":
+  version "2.13.3"
+  resolved "https://registry.yarnpkg.com/@parcel/bundler-default/-/bundler-default-2.13.3.tgz#3a7b88f473b46321532dc0f187667f8e34f0722d"
+  integrity sha512-mOuWeth0bZzRv1b9Lrvydis/hAzJyePy0gwa0tix3/zyYBvw0JY+xkXVR4qKyD/blc1Ra2qOlfI2uD3ucnsdXA==
+  dependencies:
+    "@parcel/diagnostic" "2.13.3"
+    "@parcel/graph" "3.3.3"
+    "@parcel/plugin" "2.13.3"
+    "@parcel/rust" "2.13.3"
+    "@parcel/utils" "2.13.3"
+    nullthrows "^1.1.1"
+
+"@parcel/cache@2.13.3":
+  version "2.13.3"
+  resolved "https://registry.yarnpkg.com/@parcel/cache/-/cache-2.13.3.tgz#ea23b8cc3d30ee7b7e735e4c58dc5294d5bdb437"
+  integrity sha512-Vz5+K5uCt9mcuQAMDo0JdbPYDmVdB8Nvu/A2vTEK2rqZPxvoOTczKeMBA4JqzKqGURHPRLaJCvuR8nDG+jhK9A==
+  dependencies:
+    "@parcel/fs" "2.13.3"
+    "@parcel/logger" "2.13.3"
+    "@parcel/utils" "2.13.3"
+    lmdb "2.8.5"
+
+"@parcel/codeframe@2.13.3":
+  version "2.13.3"
+  resolved "https://registry.yarnpkg.com/@parcel/codeframe/-/codeframe-2.13.3.tgz#1e3cc39f85948cc39e9f10584476ff13c0cd4f58"
+  integrity sha512-L/PQf+PT0xM8k9nc0B+PxxOYO2phQYnbuifu9o4pFRiqVmCtHztP+XMIvRJ2gOEXy3pgAImSPFVJ3xGxMFky4g==
+  dependencies:
+    chalk "^4.1.2"
+
+"@parcel/compressor-raw@2.13.3":
+  version "2.13.3"
+  resolved "https://registry.yarnpkg.com/@parcel/compressor-raw/-/compressor-raw-2.13.3.tgz#7b479b0b42108433b1c48daa0dab6c6387b7be79"
+  integrity sha512-C6vjDlgTLjYc358i7LA/dqcL0XDQZ1IHXFw6hBaHHOfxPKW2T4bzUI6RURyToEK9Q1X7+ggDKqgdLxwp4veCFg==
+  dependencies:
+    "@parcel/plugin" "2.13.3"
+
+"@parcel/config-default@2.13.3":
+  version "2.13.3"
+  resolved "https://registry.yarnpkg.com/@parcel/config-default/-/config-default-2.13.3.tgz#2d0498cf56cb162961e07b867d6f958f8aaaec64"
+  integrity sha512-WUsx83ic8DgLwwnL1Bua4lRgQqYjxiTT+DBxESGk1paNm1juWzyfPXEQDLXwiCTcWMQGiXQFQ8OuSISauVQ8dQ==
+  dependencies:
+    "@parcel/bundler-default" "2.13.3"
+    "@parcel/compressor-raw" "2.13.3"
+    "@parcel/namer-default" "2.13.3"
+    "@parcel/optimizer-css" "2.13.3"
+    "@parcel/optimizer-htmlnano" "2.13.3"
+    "@parcel/optimizer-image" "2.13.3"
+    "@parcel/optimizer-svgo" "2.13.3"
+    "@parcel/optimizer-swc" "2.13.3"
+    "@parcel/packager-css" "2.13.3"
+    "@parcel/packager-html" "2.13.3"
+    "@parcel/packager-js" "2.13.3"
+    "@parcel/packager-raw" "2.13.3"
+    "@parcel/packager-svg" "2.13.3"
+    "@parcel/packager-wasm" "2.13.3"
+    "@parcel/reporter-dev-server" "2.13.3"
+    "@parcel/resolver-default" "2.13.3"
+    "@parcel/runtime-browser-hmr" "2.13.3"
+    "@parcel/runtime-js" "2.13.3"
+    "@parcel/runtime-react-refresh" "2.13.3"
+    "@parcel/runtime-service-worker" "2.13.3"
+    "@parcel/transformer-babel" "2.13.3"
+    "@parcel/transformer-css" "2.13.3"
+    "@parcel/transformer-html" "2.13.3"
+    "@parcel/transformer-image" "2.13.3"
+    "@parcel/transformer-js" "2.13.3"
+    "@parcel/transformer-json" "2.13.3"
+    "@parcel/transformer-postcss" "2.13.3"
+    "@parcel/transformer-posthtml" "2.13.3"
+    "@parcel/transformer-raw" "2.13.3"
+    "@parcel/transformer-react-refresh-wrap" "2.13.3"
+    "@parcel/transformer-svg" "2.13.3"
+
+"@parcel/core@2.13.3":
+  version "2.13.3"
+  resolved "https://registry.yarnpkg.com/@parcel/core/-/core-2.13.3.tgz#d64ec42157a70df6a3674e98f52eb156a103985b"
+  integrity sha512-SRZFtqGiaKHlZ2YAvf+NHvBFWS3GnkBvJMfOJM7kxJRK3M1bhbwJa/GgSdzqro5UVf9Bfj6E+pkdrRQIOZ7jMQ==
+  dependencies:
+    "@mischnic/json-sourcemap" "^0.1.0"
+    "@parcel/cache" "2.13.3"
+    "@parcel/diagnostic" "2.13.3"
+    "@parcel/events" "2.13.3"
+    "@parcel/feature-flags" "2.13.3"
+    "@parcel/fs" "2.13.3"
+    "@parcel/graph" "3.3.3"
+    "@parcel/logger" "2.13.3"
+    "@parcel/package-manager" "2.13.3"
+    "@parcel/plugin" "2.13.3"
+    "@parcel/profiler" "2.13.3"
+    "@parcel/rust" "2.13.3"
+    "@parcel/source-map" "^2.1.1"
+    "@parcel/types" "2.13.3"
+    "@parcel/utils" "2.13.3"
+    "@parcel/workers" "2.13.3"
+    base-x "^3.0.8"
+    browserslist "^4.6.6"
+    clone "^2.1.1"
+    dotenv "^16.4.5"
+    dotenv-expand "^11.0.6"
+    json5 "^2.2.0"
+    msgpackr "^1.9.9"
+    nullthrows "^1.1.1"
+    semver "^7.5.2"
+
+"@parcel/diagnostic@2.13.3":
+  version "2.13.3"
+  resolved "https://registry.yarnpkg.com/@parcel/diagnostic/-/diagnostic-2.13.3.tgz#4bc00a915984f8e649a58641d639767d029f72d8"
+  integrity sha512-C70KXLBaXLJvr7XCEVu8m6TqNdw1gQLxqg5BQ8roR62R4vWWDnOq8PEksxDi4Y8Z/FF4i3Sapv6tRx9iBNxDEg==
+  dependencies:
+    "@mischnic/json-sourcemap" "^0.1.0"
+    nullthrows "^1.1.1"
+
+"@parcel/events@2.13.3":
+  version "2.13.3"
+  resolved "https://registry.yarnpkg.com/@parcel/events/-/events-2.13.3.tgz#068bdd9e1d40f88cb8110d06be2bd4d5fb23c2ad"
+  integrity sha512-ZkSHTTbD/E+53AjUzhAWTnMLnxLEU5yRw0H614CaruGh+GjgOIKyukGeToF5Gf/lvZ159VrJCGE0Z5EpgHVkuQ==
+
+"@parcel/feature-flags@2.13.3":
+  version "2.13.3"
+  resolved "https://registry.yarnpkg.com/@parcel/feature-flags/-/feature-flags-2.13.3.tgz#9664d46610a2744dd56677d26cf4fd45ab12928b"
+  integrity sha512-UZm14QpamDFoUut9YtCZSpG1HxPs07lUwUCpsAYL0PpxASD3oWJQxIJGfDZPa2272DarXDG9adTKrNXvkHZblw==
+
+"@parcel/fs@2.13.3":
+  version "2.13.3"
+  resolved "https://registry.yarnpkg.com/@parcel/fs/-/fs-2.13.3.tgz#166e7dcdd2afbab201aaf5839f69a8e853da66e0"
+  integrity sha512-+MPWAt0zr+TCDSlj1LvkORTjfB/BSffsE99A9AvScKytDSYYpY2s0t4vtV9unSh0FHMS2aBCZNJ4t7KL+DcPIg==
+  dependencies:
+    "@parcel/feature-flags" "2.13.3"
+    "@parcel/rust" "2.13.3"
+    "@parcel/types-internal" "2.13.3"
+    "@parcel/utils" "2.13.3"
+    "@parcel/watcher" "^2.0.7"
+    "@parcel/workers" "2.13.3"
+
+"@parcel/graph@3.3.3":
+  version "3.3.3"
+  resolved "https://registry.yarnpkg.com/@parcel/graph/-/graph-3.3.3.tgz#9a48d22f8d6c1e961f2723d4d7343f5388b689bb"
+  integrity sha512-pxs4GauEdvCN8nRd6wG3st6LvpHske3GfqGwUSR0P0X0pBPI1/NicvXz6xzp3rgb9gPWfbKXeI/2IOTfIxxVfg==
+  dependencies:
+    "@parcel/feature-flags" "2.13.3"
+    nullthrows "^1.1.1"
+
+"@parcel/logger@2.13.3":
+  version "2.13.3"
+  resolved "https://registry.yarnpkg.com/@parcel/logger/-/logger-2.13.3.tgz#0c91bb7fefa37b5dccd5cdfcd30cf52f5c56a1d9"
+  integrity sha512-8YF/ZhsQgd7ohQ2vEqcMD1Ag9JlJULROWRPGgGYLGD+twuxAiSdiFBpN3f+j4gQN4PYaLaIS/SwUFx11J243fQ==
+  dependencies:
+    "@parcel/diagnostic" "2.13.3"
+    "@parcel/events" "2.13.3"
+
+"@parcel/markdown-ansi@2.13.3":
+  version "2.13.3"
+  resolved "https://registry.yarnpkg.com/@parcel/markdown-ansi/-/markdown-ansi-2.13.3.tgz#05eec8407643d2c36f3511a37c38f08f7b236e24"
+  integrity sha512-B4rUdlNUulJs2xOQuDbN7Hq5a9roq8IZUcJ1vQ8PAv+zMGb7KCfqIIr/BSCDYGhayfAGBVWW8x55Kvrl1zrDYw==
+  dependencies:
+    chalk "^4.1.2"
+
+"@parcel/namer-default@2.13.3":
+  version "2.13.3"
+  resolved "https://registry.yarnpkg.com/@parcel/namer-default/-/namer-default-2.13.3.tgz#a77ce846de8203d2a4b1f93666520b0ac8a90865"
+  integrity sha512-A2a5A5fuyNcjSGOS0hPcdQmOE2kszZnLIXof7UMGNkNkeC62KAG8WcFZH5RNOY3LT5H773hq51zmc2Y2gE5Rnw==
+  dependencies:
+    "@parcel/diagnostic" "2.13.3"
+    "@parcel/plugin" "2.13.3"
+    nullthrows "^1.1.1"
+
+"@parcel/node-resolver-core@3.4.3":
+  version "3.4.3"
+  resolved "https://registry.yarnpkg.com/@parcel/node-resolver-core/-/node-resolver-core-3.4.3.tgz#aa254b2f0ac9fd5790bfd353430f19ae3b0ee778"
+  integrity sha512-IEnMks49egEic1ITBp59VQyHzkSQUXqpU9hOHwqN3KoSTdZ6rEgrXcS3pa6tdXay4NYGlcZ88kFCE8i/xYoVCg==
+  dependencies:
+    "@mischnic/json-sourcemap" "^0.1.0"
+    "@parcel/diagnostic" "2.13.3"
+    "@parcel/fs" "2.13.3"
+    "@parcel/rust" "2.13.3"
+    "@parcel/utils" "2.13.3"
+    nullthrows "^1.1.1"
+    semver "^7.5.2"
+
+"@parcel/optimizer-css@2.13.3":
+  version "2.13.3"
+  resolved "https://registry.yarnpkg.com/@parcel/optimizer-css/-/optimizer-css-2.13.3.tgz#504f75cdfde89f2463d06a8d18fbf861b2a352af"
+  integrity sha512-A8o9IVCv919vhv69SkLmyW2WjJR5WZgcMqV6L1uiGF8i8z18myrMhrp2JuSHx29PRT9uNyzNC4Xrd4StYjIhJg==
+  dependencies:
+    "@parcel/diagnostic" "2.13.3"
+    "@parcel/plugin" "2.13.3"
+    "@parcel/source-map" "^2.1.1"
+    "@parcel/utils" "2.13.3"
+    browserslist "^4.6.6"
+    lightningcss "^1.22.1"
+    nullthrows "^1.1.1"
+
+"@parcel/optimizer-htmlnano@2.13.3":
+  version "2.13.3"
+  resolved "https://registry.yarnpkg.com/@parcel/optimizer-htmlnano/-/optimizer-htmlnano-2.13.3.tgz#eaf0c011806d9856a64d4a96e9a30c970e3e003d"
+  integrity sha512-K4Uvg0Sy2pECP7pdvvbud++F0pfcbNkq+IxTrgqBX5HJnLEmRZwgdvZEKF43oMEolclMnURMQRGjRplRaPdbXg==
+  dependencies:
+    "@parcel/diagnostic" "2.13.3"
+    "@parcel/plugin" "2.13.3"
+    "@parcel/utils" "2.13.3"
+    htmlnano "^2.0.0"
+    nullthrows "^1.1.1"
+    posthtml "^0.16.5"
+
+"@parcel/optimizer-image@2.13.3":
+  version "2.13.3"
+  resolved "https://registry.yarnpkg.com/@parcel/optimizer-image/-/optimizer-image-2.13.3.tgz#7daac3ac2d13c769d84ee0d982132f86296fdde0"
+  integrity sha512-wlDUICA29J4UnqkKrWiyt68g1e85qfYhp4zJFcFJL0LX1qqh1QwsLUz3YJ+KlruoqPxJSFEC8ncBEKiVCsqhEQ==
+  dependencies:
+    "@parcel/diagnostic" "2.13.3"
+    "@parcel/plugin" "2.13.3"
+    "@parcel/rust" "2.13.3"
+    "@parcel/utils" "2.13.3"
+    "@parcel/workers" "2.13.3"
+
+"@parcel/optimizer-svgo@2.13.3":
+  version "2.13.3"
+  resolved "https://registry.yarnpkg.com/@parcel/optimizer-svgo/-/optimizer-svgo-2.13.3.tgz#8afd39b8903bee52dd98ae349aca7e27e9fcdaa1"
+  integrity sha512-piIKxQKzhZK54dJR6yqIcq+urZmpsfgUpLCZT3cnWlX4ux5+S2iN66qqZBs0zVn+a58LcWcoP4Z9ieiJmpiu2w==
+  dependencies:
+    "@parcel/diagnostic" "2.13.3"
+    "@parcel/plugin" "2.13.3"
+    "@parcel/utils" "2.13.3"
+
+"@parcel/optimizer-swc@2.13.3":
+  version "2.13.3"
+  resolved "https://registry.yarnpkg.com/@parcel/optimizer-swc/-/optimizer-swc-2.13.3.tgz#0ec2a4b8fc87c758fed8aba3a9145d78ac0449e9"
+  integrity sha512-zNSq6oWqLlW8ksPIDjM0VgrK6ZAJbPQCDvs1V+p0oX3CzEe85lT5VkRpnfrN1+/vvEJNGL8e60efHKpI+rXGTA==
+  dependencies:
+    "@parcel/diagnostic" "2.13.3"
+    "@parcel/plugin" "2.13.3"
+    "@parcel/source-map" "^2.1.1"
+    "@parcel/utils" "2.13.3"
+    "@swc/core" "^1.7.26"
+    nullthrows "^1.1.1"
+
+"@parcel/package-manager@2.13.3":
+  version "2.13.3"
+  resolved "https://registry.yarnpkg.com/@parcel/package-manager/-/package-manager-2.13.3.tgz#0106ca0f94f569c9fa00f538c5bba6e9ac6e9e37"
+  integrity sha512-FLNI5OrZxymGf/Yln0E/kjnGn5sdkQAxW7pQVdtuM+5VeN75yibJRjsSGv88PvJ+KvpD2ANgiIJo1RufmoPcww==
+  dependencies:
+    "@parcel/diagnostic" "2.13.3"
+    "@parcel/fs" "2.13.3"
+    "@parcel/logger" "2.13.3"
+    "@parcel/node-resolver-core" "3.4.3"
+    "@parcel/types" "2.13.3"
+    "@parcel/utils" "2.13.3"
+    "@parcel/workers" "2.13.3"
+    "@swc/core" "^1.7.26"
+    semver "^7.5.2"
+
+"@parcel/packager-css@2.13.3":
+  version "2.13.3"
+  resolved "https://registry.yarnpkg.com/@parcel/packager-css/-/packager-css-2.13.3.tgz#ee3c66884f1c7dc17489cefa63e03d5c57cf4bd7"
+  integrity sha512-ghDqRMtrUwaDERzFm9le0uz2PTeqqsjsW0ihQSZPSAptElRl9o5BR+XtMPv3r7Ui0evo+w35gD55oQCJ28vCig==
+  dependencies:
+    "@parcel/diagnostic" "2.13.3"
+    "@parcel/plugin" "2.13.3"
+    "@parcel/source-map" "^2.1.1"
+    "@parcel/utils" "2.13.3"
+    lightningcss "^1.22.1"
+    nullthrows "^1.1.1"
+
+"@parcel/packager-html@2.13.3":
+  version "2.13.3"
+  resolved "https://registry.yarnpkg.com/@parcel/packager-html/-/packager-html-2.13.3.tgz#00c080d87cd47d77730b9000224acef864d17abe"
+  integrity sha512-jDLnKSA/EzVEZ3/aegXO3QJ/Ij732AgBBkIQfeC8tUoxwVz5b3HiPBAjVjcUSfZs7mdBSHO+ELWC3UD+HbsIrQ==
+  dependencies:
+    "@parcel/plugin" "2.13.3"
+    "@parcel/types" "2.13.3"
+    "@parcel/utils" "2.13.3"
+    nullthrows "^1.1.1"
+    posthtml "^0.16.5"
+
+"@parcel/packager-js@2.13.3":
+  version "2.13.3"
+  resolved "https://registry.yarnpkg.com/@parcel/packager-js/-/packager-js-2.13.3.tgz#6e9fbb6a8cab064ab7021bb6b73f8934e4bc6576"
+  integrity sha512-0pMHHf2zOn7EOJe88QJw5h/wcV1bFfj6cXVcE55Wa8GX3V+SdCgolnlvNuBcRQ1Tlx0Xkpo+9hMFVIQbNQY6zw==
+  dependencies:
+    "@parcel/diagnostic" "2.13.3"
+    "@parcel/plugin" "2.13.3"
+    "@parcel/rust" "2.13.3"
+    "@parcel/source-map" "^2.1.1"
+    "@parcel/types" "2.13.3"
+    "@parcel/utils" "2.13.3"
+    globals "^13.2.0"
+    nullthrows "^1.1.1"
+
+"@parcel/packager-raw@2.13.3":
+  version "2.13.3"
+  resolved "https://registry.yarnpkg.com/@parcel/packager-raw/-/packager-raw-2.13.3.tgz#89c5bac28f59cbf9ddfb2a561575b3d19e6a021b"
+  integrity sha512-AWu4UB+akBdskzvT3KGVHIdacU9f7cI678DQQ1jKQuc9yZz5D0VFt3ocFBOmvDfEQDF0uH3jjtJR7fnuvX7Biw==
+  dependencies:
+    "@parcel/plugin" "2.13.3"
+
+"@parcel/packager-svg@2.13.3":
+  version "2.13.3"
+  resolved "https://registry.yarnpkg.com/@parcel/packager-svg/-/packager-svg-2.13.3.tgz#aa569e80de31f1869381cd30a7e091c26c31b7a8"
+  integrity sha512-tKGRiFq/4jh5u2xpTstNQ7gu+RuZWzlWqpw5NaFmcKe6VQe5CMcS499xTFoREAGnRvevSeIgC38X1a+VOo+/AA==
+  dependencies:
+    "@parcel/plugin" "2.13.3"
+    "@parcel/types" "2.13.3"
+    "@parcel/utils" "2.13.3"
+    posthtml "^0.16.4"
+
+"@parcel/packager-wasm@2.13.3":
+  version "2.13.3"
+  resolved "https://registry.yarnpkg.com/@parcel/packager-wasm/-/packager-wasm-2.13.3.tgz#fa179e5d47e5d96ccf2f9b9170288942afccc7f1"
+  integrity sha512-SZB56/b230vFrSehVXaUAWjJmWYc89gzb8OTLkBm7uvtFtov2J1R8Ig9TTJwinyXE3h84MCFP/YpQElSfoLkJw==
+  dependencies:
+    "@parcel/plugin" "2.13.3"
+
+"@parcel/plugin@2.13.3":
+  version "2.13.3"
+  resolved "https://registry.yarnpkg.com/@parcel/plugin/-/plugin-2.13.3.tgz#7542a161672821a1cb104ad09eb58695c53268c8"
+  integrity sha512-cterKHHcwg6q11Gpif/aqvHo056TR+yDVJ3fSdiG2xr5KD1VZ2B3hmofWERNNwjMcnR1h9Xq40B7jCKUhOyNFA==
+  dependencies:
+    "@parcel/types" "2.13.3"
+
+"@parcel/profiler@2.13.3":
+  version "2.13.3"
+  resolved "https://registry.yarnpkg.com/@parcel/profiler/-/profiler-2.13.3.tgz#4a375df8f8e1a0a0ab7e73e3562e4e28e9d7cdd7"
+  integrity sha512-ok6BwWSLvyHe5TuSXjSacYnDStFgP5Y30tA9mbtWSm0INDsYf+m5DqzpYPx8U54OaywWMK8w3MXUClosJX3aPA==
+  dependencies:
+    "@parcel/diagnostic" "2.13.3"
+    "@parcel/events" "2.13.3"
+    "@parcel/types-internal" "2.13.3"
+    chrome-trace-event "^1.0.2"
+
+"@parcel/reporter-cli@2.13.3":
+  version "2.13.3"
+  resolved "https://registry.yarnpkg.com/@parcel/reporter-cli/-/reporter-cli-2.13.3.tgz#46dcbefeaaf9281cc485fb4b0cc81e2c564abd6a"
+  integrity sha512-EA5tKt/6bXYNMEavSs35qHlFdx6cZmRazlZxPBgxPePQYoouNAPMNLUOEQozaPhz9f5fvNDN7EHOFaAWcdO2LA==
+  dependencies:
+    "@parcel/plugin" "2.13.3"
+    "@parcel/types" "2.13.3"
+    "@parcel/utils" "2.13.3"
+    chalk "^4.1.2"
+    term-size "^2.2.1"
+
+"@parcel/reporter-dev-server@2.13.3":
+  version "2.13.3"
+  resolved "https://registry.yarnpkg.com/@parcel/reporter-dev-server/-/reporter-dev-server-2.13.3.tgz#af5a9c5f8bf191e03ea95d4cdb59341c9851c83e"
+  integrity sha512-ZNeFp6AOIQFv7mZIv2P5O188dnZHNg0ymeDVcakfZomwhpSva2dFNS3AnvWo4eyWBlUxkmQO8BtaxeWTs7jAuA==
+  dependencies:
+    "@parcel/plugin" "2.13.3"
+    "@parcel/utils" "2.13.3"
+
+"@parcel/reporter-tracer@2.13.3":
+  version "2.13.3"
+  resolved "https://registry.yarnpkg.com/@parcel/reporter-tracer/-/reporter-tracer-2.13.3.tgz#4e60b56877d6bf7f0c468b7f75ff57d61ad11a1a"
+  integrity sha512-aBsVPI8jLZTDkFYrI69GxnsdvZKEYerkPsu935LcX9rfUYssOnmmUP+3oI+8fbg+qNjJuk9BgoQ4hCp9FOphMQ==
+  dependencies:
+    "@parcel/plugin" "2.13.3"
+    "@parcel/utils" "2.13.3"
+    chrome-trace-event "^1.0.3"
+    nullthrows "^1.1.1"
+
+"@parcel/resolver-default@2.13.3":
+  version "2.13.3"
+  resolved "https://registry.yarnpkg.com/@parcel/resolver-default/-/resolver-default-2.13.3.tgz#19987a465ad83a163b3c747e56447c6fd9a905f0"
+  integrity sha512-urBZuRALWT9pFMeWQ8JirchLmsQEyI9lrJptiwLbJWrwvmlwSUGkcstmPwoNRf/aAQjICB7ser/247Vny0pFxA==
+  dependencies:
+    "@parcel/node-resolver-core" "3.4.3"
+    "@parcel/plugin" "2.13.3"
+
+"@parcel/runtime-browser-hmr@2.13.3":
+  version "2.13.3"
+  resolved "https://registry.yarnpkg.com/@parcel/runtime-browser-hmr/-/runtime-browser-hmr-2.13.3.tgz#9d2ad14b995b6f357aa4a71e6248defa8d79be5d"
+  integrity sha512-EAcPojQFUNUGUrDk66cu3ySPO0NXRVS5CKPd4QrxPCVVbGzde4koKu8krC/TaGsoyUqhie8HMnS70qBP0GFfcQ==
+  dependencies:
+    "@parcel/plugin" "2.13.3"
+    "@parcel/utils" "2.13.3"
+
+"@parcel/runtime-js@2.13.3":
+  version "2.13.3"
+  resolved "https://registry.yarnpkg.com/@parcel/runtime-js/-/runtime-js-2.13.3.tgz#847623b17cb9f2e69db3e860ee1971f591175c27"
+  integrity sha512-62OucNAnxb2Q0uyTFWW/0Hvv2DJ4b5H6neh/YFu2/wmxaZ37xTpEuEcG2do7KW54xE5DeLP+RliHLwi4NvR3ww==
+  dependencies:
+    "@parcel/diagnostic" "2.13.3"
+    "@parcel/plugin" "2.13.3"
+    "@parcel/utils" "2.13.3"
+    nullthrows "^1.1.1"
+
+"@parcel/runtime-react-refresh@2.13.3":
+  version "2.13.3"
+  resolved "https://registry.yarnpkg.com/@parcel/runtime-react-refresh/-/runtime-react-refresh-2.13.3.tgz#7d80c130effffabe3977ded470ad7d97401012ea"
+  integrity sha512-PYZ1klpJVwqE3WuifILjtF1dugtesHEuJcXYZI85T6UoRSD5ctS1nAIpZzT14Ga1lRt/jd+eAmhWL1l3m/Vk1Q==
+  dependencies:
+    "@parcel/plugin" "2.13.3"
+    "@parcel/utils" "2.13.3"
+    react-error-overlay "6.0.9"
+    react-refresh ">=0.9 <=0.14"
+
+"@parcel/runtime-service-worker@2.13.3":
+  version "2.13.3"
+  resolved "https://registry.yarnpkg.com/@parcel/runtime-service-worker/-/runtime-service-worker-2.13.3.tgz#759c2fc71614187ea375dac509b7c44f3c4d919c"
+  integrity sha512-BjMhPuT7Us1+YIo31exPRwomPiL+jrZZS5UUAwlEW2XGHDceEotzRM94LwxeFliCScT4IOokGoxixm19qRuzWg==
+  dependencies:
+    "@parcel/plugin" "2.13.3"
+    "@parcel/utils" "2.13.3"
+    nullthrows "^1.1.1"
+
+"@parcel/rust@2.13.3":
+  version "2.13.3"
+  resolved "https://registry.yarnpkg.com/@parcel/rust/-/rust-2.13.3.tgz#924ef166e0a16923d01c83df8a65a7a726f77e3a"
+  integrity sha512-dLq85xDAtzr3P5200cvxk+8WXSWauYbxuev9LCPdwfhlaWo/JEj6cu9seVdWlkagjGwkoV1kXC+GGntgUXOLAQ==
+
+"@parcel/source-map@^2.1.1":
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/@parcel/source-map/-/source-map-2.1.1.tgz#fb193b82dba6dd62cc7a76b326f57bb35000a782"
+  integrity sha512-Ejx1P/mj+kMjQb8/y5XxDUn4reGdr+WyKYloBljpppUy8gs42T+BNoEOuRYqDVdgPc6NxduzIDoJS9pOFfV5Ew==
+  dependencies:
+    detect-libc "^1.0.3"
+
+"@parcel/transformer-babel@2.13.3":
+  version "2.13.3"
+  resolved "https://registry.yarnpkg.com/@parcel/transformer-babel/-/transformer-babel-2.13.3.tgz#a751ccaefd50836be3d01cc2afd5c0982708d5a7"
+  integrity sha512-ikzK9f5WTFrdQsPitQgjCPH6HmVU8AQPRemIJ2BndYhtodn5PQut5cnSvTrqax8RjYvheEKCQk/Zb/uR7qgS3g==
+  dependencies:
+    "@parcel/diagnostic" "2.13.3"
+    "@parcel/plugin" "2.13.3"
+    "@parcel/source-map" "^2.1.1"
+    "@parcel/utils" "2.13.3"
+    browserslist "^4.6.6"
+    json5 "^2.2.0"
+    nullthrows "^1.1.1"
+    semver "^7.5.2"
+
+"@parcel/transformer-css@2.13.3":
+  version "2.13.3"
+  resolved "https://registry.yarnpkg.com/@parcel/transformer-css/-/transformer-css-2.13.3.tgz#bb9bfd26798ac955febc7a4eba900a1593321433"
+  integrity sha512-zbrNURGph6JeVADbGydyZ7lcu/izj41kDxQ9xw4RPRW/3rofQiTU0OTREi+uBWiMENQySXVivEdzHA9cA+aLAA==
+  dependencies:
+    "@parcel/diagnostic" "2.13.3"
+    "@parcel/plugin" "2.13.3"
+    "@parcel/source-map" "^2.1.1"
+    "@parcel/utils" "2.13.3"
+    browserslist "^4.6.6"
+    lightningcss "^1.22.1"
+    nullthrows "^1.1.1"
+
+"@parcel/transformer-html@2.13.3":
+  version "2.13.3"
+  resolved "https://registry.yarnpkg.com/@parcel/transformer-html/-/transformer-html-2.13.3.tgz#969398bdce3f1a295462910976cf2f8d45a83c2d"
+  integrity sha512-Yf74FkL9RCCB4+hxQRVMNQThH9+fZ5w0NLiQPpWUOcgDEEyxTi4FWPQgEBsKl/XK2ehdydbQB9fBgPQLuQxwPg==
+  dependencies:
+    "@parcel/diagnostic" "2.13.3"
+    "@parcel/plugin" "2.13.3"
+    "@parcel/rust" "2.13.3"
+    nullthrows "^1.1.1"
+    posthtml "^0.16.5"
+    posthtml-parser "^0.12.1"
+    posthtml-render "^3.0.0"
+    semver "^7.5.2"
+    srcset "4"
+
+"@parcel/transformer-image@2.13.3":
+  version "2.13.3"
+  resolved "https://registry.yarnpkg.com/@parcel/transformer-image/-/transformer-image-2.13.3.tgz#e3ee409baa036e5f60036663ad87ff74ff499db3"
+  integrity sha512-wL1CXyeFAqbp2wcEq/JD3a/tbAyVIDMTC6laQxlIwnVV7dsENhK1qRuJZuoBdixESeUpFQSmmQvDIhcfT/cUUg==
+  dependencies:
+    "@parcel/plugin" "2.13.3"
+    "@parcel/utils" "2.13.3"
+    "@parcel/workers" "2.13.3"
+    nullthrows "^1.1.1"
+
+"@parcel/transformer-js@2.13.3":
+  version "2.13.3"
+  resolved "https://registry.yarnpkg.com/@parcel/transformer-js/-/transformer-js-2.13.3.tgz#e53be3b860fb2dd2430bbd7d1089365492255209"
+  integrity sha512-KqfNGn1IHzDoN2aPqt4nDksgb50Xzcny777C7A7hjlQ3cmkjyJrixYjzzsPaPSGJ+kJpknh3KE8unkQ9mhFvRQ==
+  dependencies:
+    "@parcel/diagnostic" "2.13.3"
+    "@parcel/plugin" "2.13.3"
+    "@parcel/rust" "2.13.3"
+    "@parcel/source-map" "^2.1.1"
+    "@parcel/utils" "2.13.3"
+    "@parcel/workers" "2.13.3"
+    "@swc/helpers" "^0.5.0"
+    browserslist "^4.6.6"
+    nullthrows "^1.1.1"
+    regenerator-runtime "^0.14.1"
+    semver "^7.5.2"
+
+"@parcel/transformer-json@2.13.3":
+  version "2.13.3"
+  resolved "https://registry.yarnpkg.com/@parcel/transformer-json/-/transformer-json-2.13.3.tgz#14ae4bcf572babe58a7aa204b7996ceb5a790698"
+  integrity sha512-rrq0ab6J0w9ePtsxi0kAvpCmrUYXXAx1Z5PATZakv89rSYbHBKEdXxyCoKFui/UPVCUEGVs5r0iOFepdHpIyeA==
+  dependencies:
+    "@parcel/plugin" "2.13.3"
+    json5 "^2.2.0"
+
+"@parcel/transformer-postcss@2.13.3":
+  version "2.13.3"
+  resolved "https://registry.yarnpkg.com/@parcel/transformer-postcss/-/transformer-postcss-2.13.3.tgz#26d67676ceb313f20097f599628b0da647ea497b"
+  integrity sha512-AIiWpU0QSFBrPcYIqAnhqB8RGE6yHFznnxztfg1t2zMSOnK3xoU6xqYKv8H/MduShGGrC3qVOeDfM8MUwzL3cw==
+  dependencies:
+    "@parcel/diagnostic" "2.13.3"
+    "@parcel/plugin" "2.13.3"
+    "@parcel/rust" "2.13.3"
+    "@parcel/utils" "2.13.3"
+    clone "^2.1.1"
+    nullthrows "^1.1.1"
+    postcss-value-parser "^4.2.0"
+    semver "^7.5.2"
+
+"@parcel/transformer-posthtml@2.13.3":
+  version "2.13.3"
+  resolved "https://registry.yarnpkg.com/@parcel/transformer-posthtml/-/transformer-posthtml-2.13.3.tgz#2599df5226aa41b9411bcd816bcbfd2a073b8d39"
+  integrity sha512-5GSLyccpHASwFAu3uJ83gDIBSvfsGdVmhJvy0Vxe+K1Fklk2ibhvvtUHMhB7mg6SPHC+R9jsNc3ZqY04ZLeGjw==
+  dependencies:
+    "@parcel/plugin" "2.13.3"
+    "@parcel/utils" "2.13.3"
+    nullthrows "^1.1.1"
+    posthtml "^0.16.5"
+    posthtml-parser "^0.12.1"
+    posthtml-render "^3.0.0"
+    semver "^7.5.2"
+
+"@parcel/transformer-raw@2.13.3":
+  version "2.13.3"
+  resolved "https://registry.yarnpkg.com/@parcel/transformer-raw/-/transformer-raw-2.13.3.tgz#6a2eb2201f5dd13c46e10d0aa1c1749d1165e6f3"
+  integrity sha512-BFsAbdQF0l8/Pdb7dSLJeYcd8jgwvAUbHgMink2MNXJuRUvDl19Gns8jVokU+uraFHulJMBj40+K/RTd33in4g==
+  dependencies:
+    "@parcel/plugin" "2.13.3"
+
+"@parcel/transformer-react-refresh-wrap@2.13.3":
+  version "2.13.3"
+  resolved "https://registry.yarnpkg.com/@parcel/transformer-react-refresh-wrap/-/transformer-react-refresh-wrap-2.13.3.tgz#45d69ad21940699cf74984bdc74dc8aceb725f65"
+  integrity sha512-mOof4cRyxsZRdg8kkWaFtaX98mHpxUhcGPU+nF9RQVa9q737ItxrorsPNR9hpZAyE2TtFNflNW7RoYsgvlLw8w==
+  dependencies:
+    "@parcel/plugin" "2.13.3"
+    "@parcel/utils" "2.13.3"
+    react-refresh ">=0.9 <=0.14"
+
+"@parcel/transformer-svg@2.13.3":
+  version "2.13.3"
+  resolved "https://registry.yarnpkg.com/@parcel/transformer-svg/-/transformer-svg-2.13.3.tgz#dabb0f9d23071d36d21e2e460111d5ed0fdb23e3"
+  integrity sha512-9jm7ZF4KHIrGLWlw/SFUz5KKJ20nxHvjFAmzde34R9Wu+F1BOjLZxae7w4ZRwvIc+UVOUcBBQFmhSVwVDZg6Dw==
+  dependencies:
+    "@parcel/diagnostic" "2.13.3"
+    "@parcel/plugin" "2.13.3"
+    "@parcel/rust" "2.13.3"
+    nullthrows "^1.1.1"
+    posthtml "^0.16.5"
+    posthtml-parser "^0.12.1"
+    posthtml-render "^3.0.0"
+    semver "^7.5.2"
+
+"@parcel/types-internal@2.13.3":
+  version "2.13.3"
+  resolved "https://registry.yarnpkg.com/@parcel/types-internal/-/types-internal-2.13.3.tgz#dbbfefeac3ce0e735dcf82bd171115e239d31692"
+  integrity sha512-Lhx0n+9RCp+Ipktf/I+CLm3zE9Iq9NtDd8b2Vr5lVWyoT8AbzBKIHIpTbhLS4kjZ80L3I6o93OYjqAaIjsqoZw==
+  dependencies:
+    "@parcel/diagnostic" "2.13.3"
+    "@parcel/feature-flags" "2.13.3"
+    "@parcel/source-map" "^2.1.1"
+    utility-types "^3.10.0"
+
+"@parcel/types@2.13.3":
+  version "2.13.3"
+  resolved "https://registry.yarnpkg.com/@parcel/types/-/types-2.13.3.tgz#cb59dd663a945f85eea3764364bb47066023d8a9"
+  integrity sha512-+RpFHxx8fy8/dpuehHUw/ja9PRExC3wJoIlIIF42E7SLu2SvlTHtKm6EfICZzxCXNEBzjoDbamCRcN0nmTPlhw==
+  dependencies:
+    "@parcel/types-internal" "2.13.3"
+    "@parcel/workers" "2.13.3"
+
+"@parcel/utils@2.13.3":
+  version "2.13.3"
+  resolved "https://registry.yarnpkg.com/@parcel/utils/-/utils-2.13.3.tgz#70199960d84a7c0c0bc813799dd6dab0571e2e59"
+  integrity sha512-yxY9xw2wOUlJaScOXYZmMGoZ4Ck4Kqj+p6Koe5kLkkWM1j98Q0Dj2tf/mNvZi4yrdnlm+dclCwNRnuE8Q9D+pw==
+  dependencies:
+    "@parcel/codeframe" "2.13.3"
+    "@parcel/diagnostic" "2.13.3"
+    "@parcel/logger" "2.13.3"
+    "@parcel/markdown-ansi" "2.13.3"
+    "@parcel/rust" "2.13.3"
+    "@parcel/source-map" "^2.1.1"
+    chalk "^4.1.2"
+    nullthrows "^1.1.1"
+
+"@parcel/watcher-android-arm64@2.5.0":
+  version "2.5.0"
+  resolved "https://registry.yarnpkg.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.0.tgz#e32d3dda6647791ee930556aee206fcd5ea0fb7a"
+  integrity sha512-qlX4eS28bUcQCdribHkg/herLe+0A9RyYC+mm2PXpncit8z5b3nSqGVzMNR3CmtAOgRutiZ02eIJJgP/b1iEFQ==
+
+"@parcel/watcher-darwin-arm64@2.5.0":
+  version "2.5.0"
+  resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.0.tgz#0d9e680b7e9ec1c8f54944f1b945aa8755afb12f"
+  integrity sha512-hyZ3TANnzGfLpRA2s/4U1kbw2ZI4qGxaRJbBH2DCSREFfubMswheh8TeiC1sGZ3z2jUf3s37P0BBlrD3sjVTUw==
+
+"@parcel/watcher-darwin-x64@2.5.0":
+  version "2.5.0"
+  resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.0.tgz#f9f1d5ce9d5878d344f14ef1856b7a830c59d1bb"
+  integrity sha512-9rhlwd78saKf18fT869/poydQK8YqlU26TMiNg7AIu7eBp9adqbJZqmdFOsbZ5cnLp5XvRo9wcFmNHgHdWaGYA==
+
+"@parcel/watcher-freebsd-x64@2.5.0":
+  version "2.5.0"
+  resolved "https://registry.yarnpkg.com/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.0.tgz#2b77f0c82d19e84ff4c21de6da7f7d096b1a7e82"
+  integrity sha512-syvfhZzyM8kErg3VF0xpV8dixJ+RzbUaaGaeb7uDuz0D3FK97/mZ5AJQ3XNnDsXX7KkFNtyQyFrXZzQIcN49Tw==
+
+"@parcel/watcher-linux-arm-glibc@2.5.0":
+  version "2.5.0"
+  resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.0.tgz#92ed322c56dbafa3d2545dcf2803334aee131e42"
+  integrity sha512-0VQY1K35DQET3dVYWpOaPFecqOT9dbuCfzjxoQyif1Wc574t3kOSkKevULddcR9znz1TcklCE7Ht6NIxjvTqLA==
+
+"@parcel/watcher-linux-arm-musl@2.5.0":
+  version "2.5.0"
+  resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.0.tgz#cd48e9bfde0cdbbd2ecd9accfc52967e22f849a4"
+  integrity sha512-6uHywSIzz8+vi2lAzFeltnYbdHsDm3iIB57d4g5oaB9vKwjb6N6dRIgZMujw4nm5r6v9/BQH0noq6DzHrqr2pA==
+
+"@parcel/watcher-linux-arm64-glibc@2.5.0":
+  version "2.5.0"
+  resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.0.tgz#7b81f6d5a442bb89fbabaf6c13573e94a46feb03"
+  integrity sha512-BfNjXwZKxBy4WibDb/LDCriWSKLz+jJRL3cM/DllnHH5QUyoiUNEp3GmL80ZqxeumoADfCCP19+qiYiC8gUBjA==
+
+"@parcel/watcher-linux-arm64-musl@2.5.0":
+  version "2.5.0"
+  resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.0.tgz#dcb8ff01077cdf59a18d9e0a4dff7a0cfe5fd732"
+  integrity sha512-S1qARKOphxfiBEkwLUbHjCY9BWPdWnW9j7f7Hb2jPplu8UZ3nes7zpPOW9bkLbHRvWM0WDTsjdOTUgW0xLBN1Q==
+
+"@parcel/watcher-linux-x64-glibc@2.5.0":
+  version "2.5.0"
+  resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.0.tgz#2e254600fda4e32d83942384d1106e1eed84494d"
+  integrity sha512-d9AOkusyXARkFD66S6zlGXyzx5RvY+chTP9Jp0ypSTC9d4lzyRs9ovGf/80VCxjKddcUvnsGwCHWuF2EoPgWjw==
+
+"@parcel/watcher-linux-x64-musl@2.5.0":
+  version "2.5.0"
+  resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.0.tgz#01fcea60fedbb3225af808d3f0a7b11229792eef"
+  integrity sha512-iqOC+GoTDoFyk/VYSFHwjHhYrk8bljW6zOhPuhi5t9ulqiYq1togGJB5e3PwYVFFfeVgc6pbz3JdQyDoBszVaA==
+
+"@parcel/watcher-win32-arm64@2.5.0":
+  version "2.5.0"
+  resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.0.tgz#87cdb16e0783e770197e52fb1dc027bb0c847154"
+  integrity sha512-twtft1d+JRNkM5YbmexfcH/N4znDtjgysFaV9zvZmmJezQsKpkfLYJ+JFV3uygugK6AtIM2oADPkB2AdhBrNig==
+
+"@parcel/watcher-win32-ia32@2.5.0":
+  version "2.5.0"
+  resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.0.tgz#778c39b56da33e045ba21c678c31a9f9d7c6b220"
+  integrity sha512-+rgpsNRKwo8A53elqbbHXdOMtY/tAtTzManTWShB5Kk54N8Q9mzNWV7tV+IbGueCbcj826MfWGU3mprWtuf1TA==
+
+"@parcel/watcher-win32-x64@2.5.0":
+  version "2.5.0"
+  resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.0.tgz#33873876d0bbc588aacce38e90d1d7480ce81cb7"
+  integrity sha512-lPrxve92zEHdgeff3aiu4gDOIt4u7sJYha6wbdEZDCDUhtjTsOMiaJzG5lMY4GkWH8p0fMmO2Ppq5G5XXG+DQw==
+
+"@parcel/watcher@^2.0.7":
+  version "2.5.0"
+  resolved "https://registry.yarnpkg.com/@parcel/watcher/-/watcher-2.5.0.tgz#5c88818b12b8de4307a9d3e6dc3e28eba0dfbd10"
+  integrity sha512-i0GV1yJnm2n3Yq1qw6QrUrd/LI9bE8WEBOTtOkpCXHHdyN3TAGgqAK/DAT05z4fq2x04cARXt2pDmjWjL92iTQ==
+  dependencies:
+    detect-libc "^1.0.3"
+    is-glob "^4.0.3"
+    micromatch "^4.0.5"
+    node-addon-api "^7.0.0"
+  optionalDependencies:
+    "@parcel/watcher-android-arm64" "2.5.0"
+    "@parcel/watcher-darwin-arm64" "2.5.0"
+    "@parcel/watcher-darwin-x64" "2.5.0"
+    "@parcel/watcher-freebsd-x64" "2.5.0"
+    "@parcel/watcher-linux-arm-glibc" "2.5.0"
+    "@parcel/watcher-linux-arm-musl" "2.5.0"
+    "@parcel/watcher-linux-arm64-glibc" "2.5.0"
+    "@parcel/watcher-linux-arm64-musl" "2.5.0"
+    "@parcel/watcher-linux-x64-glibc" "2.5.0"
+    "@parcel/watcher-linux-x64-musl" "2.5.0"
+    "@parcel/watcher-win32-arm64" "2.5.0"
+    "@parcel/watcher-win32-ia32" "2.5.0"
+    "@parcel/watcher-win32-x64" "2.5.0"
+
+"@parcel/workers@2.13.3":
+  version "2.13.3"
+  resolved "https://registry.yarnpkg.com/@parcel/workers/-/workers-2.13.3.tgz#781bd062efe9346b7ac9f883b91e8fc6e8f6bda1"
+  integrity sha512-oAHmdniWTRwwwsKbcF4t3VjOtKN+/W17Wj5laiYB+HLkfsjGTfIQPj3sdXmrlBAGpI4omIcvR70PHHXnfdTfwA==
+  dependencies:
+    "@parcel/diagnostic" "2.13.3"
+    "@parcel/logger" "2.13.3"
+    "@parcel/profiler" "2.13.3"
+    "@parcel/types-internal" "2.13.3"
+    "@parcel/utils" "2.13.3"
+    nullthrows "^1.1.1"
+
+"@swc/core-darwin-arm64@1.10.1":
+  version "1.10.1"
+  resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.10.1.tgz#faaaab19b4a039ae67ef661c0144a6f20fe8a78e"
+  integrity sha512-NyELPp8EsVZtxH/mEqvzSyWpfPJ1lugpTQcSlMduZLj1EASLO4sC8wt8hmL1aizRlsbjCX+r0PyL+l0xQ64/6Q==
+
+"@swc/core-darwin-x64@1.10.1":
+  version "1.10.1"
+  resolved "https://registry.yarnpkg.com/@swc/core-darwin-x64/-/core-darwin-x64-1.10.1.tgz#754600f453abd24471c202d48836f1161d798f49"
+  integrity sha512-L4BNt1fdQ5ZZhAk5qoDfUnXRabDOXKnXBxMDJ+PWLSxOGBbWE6aJTnu4zbGjJvtot0KM46m2LPAPY8ttknqaZA==
+
+"@swc/core-linux-arm-gnueabihf@1.10.1":
+  version "1.10.1"
+  resolved "https://registry.yarnpkg.com/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.10.1.tgz#b0f43c482d0d1819b382a4eb4a0733ce2e386257"
+  integrity sha512-Y1u9OqCHgvVp2tYQAJ7hcU9qO5brDMIrA5R31rwWQIAKDkJKtv3IlTHF0hrbWk1wPR0ZdngkQSJZple7G+Grvw==
+
+"@swc/core-linux-arm64-gnu@1.10.1":
+  version "1.10.1"
+  resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.10.1.tgz#e02a9e22c25ba85ef00335742e549e06284cf33a"
+  integrity sha512-tNQHO/UKdtnqjc7o04iRXng1wTUXPgVd8Y6LI4qIbHVoVPwksZydISjMcilKNLKIwOoUQAkxyJ16SlOAeADzhQ==
+
+"@swc/core-linux-arm64-musl@1.10.1":
+  version "1.10.1"
+  resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.10.1.tgz#3a0530af8f8bd3717f2f1bd8a2f5183fc58d4cf1"
+  integrity sha512-x0L2Pd9weQ6n8dI1z1Isq00VHFvpBClwQJvrt3NHzmR+1wCT/gcYl1tp9P5xHh3ldM8Cn4UjWCw+7PaUgg8FcQ==
+
+"@swc/core-linux-x64-gnu@1.10.1":
+  version "1.10.1"
+  resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.10.1.tgz#5eb4d282b047a22896ab1d4627403be4c3e4fa6a"
+  integrity sha512-yyYEwQcObV3AUsC79rSzN9z6kiWxKAVJ6Ntwq2N9YoZqSPYph+4/Am5fM1xEQYf/kb99csj0FgOelomJSobxQA==
+
+"@swc/core-linux-x64-musl@1.10.1":
+  version "1.10.1"
+  resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.10.1.tgz#890f2eda3e67ccc6817cdd04eff91e6ad9e761c4"
+  integrity sha512-tcaS43Ydd7Fk7sW5ROpaf2Kq1zR+sI5K0RM+0qYLYYurvsJruj3GhBCaiN3gkzd8m/8wkqNqtVklWaQYSDsyqA==
+
+"@swc/core-win32-arm64-msvc@1.10.1":
+  version "1.10.1"
+  resolved "https://registry.yarnpkg.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.10.1.tgz#4ea7b2a2fab47f801d31ea8b001a141efaa5e6bf"
+  integrity sha512-D3Qo1voA7AkbOzQ2UGuKNHfYGKL6eejN8VWOoQYtGHHQi1p5KK/Q7V1ku55oxXBsj79Ny5FRMqiRJpVGad7bjQ==
+
+"@swc/core-win32-ia32-msvc@1.10.1":
+  version "1.10.1"
+  resolved "https://registry.yarnpkg.com/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.10.1.tgz#729102669ccdb72e69884cce58e3686ac63d6f36"
+  integrity sha512-WalYdFoU3454Og+sDKHM1MrjvxUGwA2oralknXkXL8S0I/8RkWZOB++p3pLaGbTvOO++T+6znFbQdR8KRaa7DA==
+
+"@swc/core-win32-x64-msvc@1.10.1":
+  version "1.10.1"
+  resolved "https://registry.yarnpkg.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.10.1.tgz#7d665a7c69642861aed850ecb0cdf5d87197edda"
+  integrity sha512-JWobfQDbTnoqaIwPKQ3DVSywihVXlQMbDuwik/dDWlj33A8oEHcjPOGs4OqcA3RHv24i+lfCQpM3Mn4FAMfacA==
+
+"@swc/core@^1.7.26":
+  version "1.10.1"
+  resolved "https://registry.yarnpkg.com/@swc/core/-/core-1.10.1.tgz#16b3b8284bafb0ecabb253925796883971e5a761"
+  integrity sha512-rQ4dS6GAdmtzKiCRt3LFVxl37FaY1cgL9kSUTnhQ2xc3fmHOd7jdJK/V4pSZMG1ruGTd0bsi34O2R0Olg9Zo/w==
+  dependencies:
+    "@swc/counter" "^0.1.3"
+    "@swc/types" "^0.1.17"
+  optionalDependencies:
+    "@swc/core-darwin-arm64" "1.10.1"
+    "@swc/core-darwin-x64" "1.10.1"
+    "@swc/core-linux-arm-gnueabihf" "1.10.1"
+    "@swc/core-linux-arm64-gnu" "1.10.1"
+    "@swc/core-linux-arm64-musl" "1.10.1"
+    "@swc/core-linux-x64-gnu" "1.10.1"
+    "@swc/core-linux-x64-musl" "1.10.1"
+    "@swc/core-win32-arm64-msvc" "1.10.1"
+    "@swc/core-win32-ia32-msvc" "1.10.1"
+    "@swc/core-win32-x64-msvc" "1.10.1"
+
+"@swc/counter@^0.1.3":
+  version "0.1.3"
+  resolved "https://registry.yarnpkg.com/@swc/counter/-/counter-0.1.3.tgz#cc7463bd02949611c6329596fccd2b0ec782b0e9"
+  integrity sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==
+
+"@swc/helpers@^0.5.0":
+  version "0.5.15"
+  resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.15.tgz#79efab344c5819ecf83a43f3f9f811fc84b516d7"
+  integrity sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==
+  dependencies:
+    tslib "^2.8.0"
+
+"@swc/types@^0.1.17":
+  version "0.1.17"
+  resolved "https://registry.yarnpkg.com/@swc/types/-/types-0.1.17.tgz#bd1d94e73497f27341bf141abdf4c85230d41e7c"
+  integrity sha512-V5gRru+aD8YVyCOMAjMpWR1Ui577DD5KSJsHP8RAxopAH22jFz6GZd/qxqjO6MJHQhcsjvjOFXyDhyLQUnMveQ==
+  dependencies:
+    "@swc/counter" "^0.1.3"
+
+"@trysound/sax@0.2.0":
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad"
+  integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==
+
+"@types/glob@^7.1.3":
+  version "7.2.0"
+  resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.2.0.tgz#bc1b5bf3aa92f25bd5dd39f35c57361bdce5b2eb"
+  integrity sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==
+  dependencies:
+    "@types/minimatch" "*"
+    "@types/node" "*"
+
+"@types/json-schema@^7.0.11", "@types/json-schema@^7.0.6":
+  version "7.0.15"
+  resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841"
+  integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==
+
+"@types/lodash@^4.14.182":
+  version "4.17.13"
+  resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.13.tgz#786e2d67cfd95e32862143abe7463a7f90c300eb"
+  integrity sha512-lfx+dftrEZcdBPczf9d0Qv0x+j/rfNCMuC6OcfXmO8gkfeNAY88PgKUbvG56whcN23gc27yenwF6oJZXGFpYxg==
+
+"@types/minimatch@*":
+  version "5.1.2"
+  resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-5.1.2.tgz#07508b45797cb81ec3f273011b054cd0755eddca"
+  integrity sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==
+
+"@types/node@*":
+  version "22.10.2"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-22.10.2.tgz#a485426e6d1fdafc7b0d4c7b24e2c78182ddabb9"
+  integrity sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==
+  dependencies:
+    undici-types "~6.20.0"
+
+"@types/prettier@^2.6.1":
+  version "2.7.3"
+  resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.7.3.tgz#3e51a17e291d01d17d3fc61422015a933af7a08f"
+  integrity sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==
+
+"@types/yauzl@^2.9.1":
+  version "2.10.3"
+  resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.10.3.tgz#e9b2808b4f109504a03cda958259876f61017999"
+  integrity sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==
+  dependencies:
+    "@types/node" "*"
+
+"@yarnpkg/lockfile@^1.1.0":
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31"
+  integrity sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==
+
+agent-base@6:
+  version "6.0.2"
+  resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"
+  integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==
+  dependencies:
+    debug "4"
+
+ansi-regex@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
+  integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
+
+ansi-styles@^3.2.1:
+  version "3.2.1"
+  resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
+  integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==
+  dependencies:
+    color-convert "^1.9.0"
+
+ansi-styles@^4.1.0:
+  version "4.3.0"
+  resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
+  integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
+  dependencies:
+    color-convert "^2.0.1"
+
+ansi-styles@^5.0.0:
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b"
+  integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==
+
+any-promise@^1.0.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f"
+  integrity sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==
+
+argparse@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
+  integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
+
+assert@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/assert/-/assert-2.1.0.tgz#6d92a238d05dc02e7427c881fb8be81c8448b2dd"
+  integrity sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==
+  dependencies:
+    call-bind "^1.0.2"
+    is-nan "^1.3.2"
+    object-is "^1.1.5"
+    object.assign "^4.1.4"
+    util "^0.12.5"
+
+ast-types@0.15.2:
+  version "0.15.2"
+  resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.15.2.tgz#39ae4809393c4b16df751ee563411423e85fb49d"
+  integrity sha512-c27loCv9QkZinsa5ProX751khO9DJl/AcB5c2KNtA6NRvHKS0PgLfcftz72KVq504vB0Gku5s2kUZzDBvQWvHg==
+  dependencies:
+    tslib "^2.0.1"
+
+at-least-node@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2"
+  integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==
+
+autoprefixer@^10.4.21:
+  version "10.4.21"
+  resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.21.tgz#77189468e7a8ad1d9a37fbc08efc9f480cf0a95d"
+  integrity sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==
+  dependencies:
+    browserslist "^4.24.4"
+    caniuse-lite "^1.0.30001702"
+    fraction.js "^4.3.7"
+    normalize-range "^0.1.2"
+    picocolors "^1.1.1"
+    postcss-value-parser "^4.2.0"
+
+available-typed-arrays@^1.0.7:
+  version "1.0.7"
+  resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846"
+  integrity sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==
+  dependencies:
+    possible-typed-array-names "^1.0.0"
+
+balanced-match@^1.0.0:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
+  integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
+
+base-x@^3.0.8:
+  version "3.0.10"
+  resolved "https://registry.yarnpkg.com/base-x/-/base-x-3.0.10.tgz#62de58653f8762b5d6f8d9fe30fa75f7b2585a75"
+  integrity sha512-7d0s06rR9rYaIWHkpfLIFICM/tkSVdoPC9qYAQRpxn9DdKNWNsKC0uk++akckyLq16Tx2WIinnZ6WRriAt6njQ==
+  dependencies:
+    safe-buffer "^5.0.1"
+
+base64-js@^1.3.1:
+  version "1.5.1"
+  resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
+  integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
+
+bl@^4.0.3:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a"
+  integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==
+  dependencies:
+    buffer "^5.5.0"
+    inherits "^2.0.4"
+    readable-stream "^3.4.0"
+
+boolbase@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
+  integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==
+
+brace-expansion@^1.1.7:
+  version "1.1.11"
+  resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
+  integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
+  dependencies:
+    balanced-match "^1.0.0"
+    concat-map "0.0.1"
+
+braces@^3.0.3:
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789"
+  integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
+  dependencies:
+    fill-range "^7.1.1"
+
+browserslist@^4.0.0, browserslist@^4.23.3, browserslist@^4.6.6:
+  version "4.24.3"
+  resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.3.tgz#5fc2725ca8fb3c1432e13dac278c7cc103e026d2"
+  integrity sha512-1CPmv8iobE2fyRMV97dAcMVegvvWKxmq94hkLiAkUGwKVTyDLw33K+ZxiFrREKmmps4rIw6grcCFCnTMSZ/YiA==
+  dependencies:
+    caniuse-lite "^1.0.30001688"
+    electron-to-chromium "^1.5.73"
+    node-releases "^2.0.19"
+    update-browserslist-db "^1.1.1"
+
+browserslist@^4.24.4:
+  version "4.24.4"
+  resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.4.tgz#c6b2865a3f08bcb860a0e827389003b9fe686e4b"
+  integrity sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==
+  dependencies:
+    caniuse-lite "^1.0.30001688"
+    electron-to-chromium "^1.5.73"
+    node-releases "^2.0.19"
+    update-browserslist-db "^1.1.1"
+
+buffer-crc32@~0.2.3:
+  version "0.2.13"
+  resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242"
+  integrity sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==
+
+buffer@^5.2.1, buffer@^5.5.0:
+  version "5.7.1"
+  resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0"
+  integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==
+  dependencies:
+    base64-js "^1.3.1"
+    ieee754 "^1.1.13"
+
+call-bind-apply-helpers@^1.0.0, call-bind-apply-helpers@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz#32e5892e6361b29b0b545ba6f7763378daca2840"
+  integrity sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==
+  dependencies:
+    es-errors "^1.3.0"
+    function-bind "^1.1.2"
+
+call-bind@^1.0.0, call-bind@^1.0.2, call-bind@^1.0.7, call-bind@^1.0.8:
+  version "1.0.8"
+  resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.8.tgz#0736a9660f537e3388826f440d5ec45f744eaa4c"
+  integrity sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==
+  dependencies:
+    call-bind-apply-helpers "^1.0.0"
+    es-define-property "^1.0.0"
+    get-intrinsic "^1.2.4"
+    set-function-length "^1.2.2"
+
+call-bound@^1.0.2, call-bound@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.3.tgz#41cfd032b593e39176a71533ab4f384aa04fd681"
+  integrity sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==
+  dependencies:
+    call-bind-apply-helpers "^1.0.1"
+    get-intrinsic "^1.2.6"
+
+call-me-maybe@^1.0.1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.2.tgz#03f964f19522ba643b1b0693acb9152fe2074baa"
+  integrity sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==
+
+callsites@^3.0.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
+  integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
+
+caniuse-api@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-3.0.0.tgz#5e4d90e2274961d46291997df599e3ed008ee4c0"
+  integrity sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==
+  dependencies:
+    browserslist "^4.0.0"
+    caniuse-lite "^1.0.0"
+    lodash.memoize "^4.1.2"
+    lodash.uniq "^4.5.0"
+
+caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001688:
+  version "1.0.30001690"
+  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001690.tgz#f2d15e3aaf8e18f76b2b8c1481abde063b8104c8"
+  integrity sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w==
+
+caniuse-lite@^1.0.30001702:
+  version "1.0.30001704"
+  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001704.tgz#6644fe909d924ac3a7125e8a0ab6af95b1f32990"
+  integrity sha512-+L2IgBbV6gXB4ETf0keSvLr7JUrRVbIaB/lrQ1+z8mRcQiisG5k+lG6O4n6Y5q6f5EuNfaYXKgymucphlEXQew==
+
+caniuse-lite@^1.0.30001717:
+  version "1.0.30001717"
+  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001717.tgz#5d9fec5ce09796a1893013825510678928aca129"
+  integrity sha512-auPpttCq6BDEG8ZAuHJIplGw6GODhjw+/11e7IjpnYCxZcW/ONgPs0KVBJ0d1bY3e2+7PRe5RCLyP+PfwVgkYw==
+
+chalk@^2.4.2:
+  version "2.4.2"
+  resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
+  integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
+  dependencies:
+    ansi-styles "^3.2.1"
+    escape-string-regexp "^1.0.5"
+    supports-color "^5.3.0"
+
+chalk@^4.0.0, chalk@^4.1.2:
+  version "4.1.2"
+  resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
+  integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
+  dependencies:
+    ansi-styles "^4.1.0"
+    supports-color "^7.1.0"
+
+chownr@^1.1.1:
+  version "1.1.4"
+  resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b"
+  integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==
+
+chrome-trace-event@^1.0.2, chrome-trace-event@^1.0.3:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz#05bffd7ff928465093314708c93bdfa9bd1f0f5b"
+  integrity sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==
+
+ci-info@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46"
+  integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==
+
+cli-color@^2.0.2:
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/cli-color/-/cli-color-2.0.4.tgz#d658080290968816b322248b7306fad2346fb2c8"
+  integrity sha512-zlnpg0jNcibNrO7GG9IeHH7maWFeCz+Ja1wx/7tZNU5ASSSSZ+/qZciM0/LHCYxSdqv5h2sdbQ/PXYdOuetXvA==
+  dependencies:
+    d "^1.0.1"
+    es5-ext "^0.10.64"
+    es6-iterator "^2.0.3"
+    memoizee "^0.4.15"
+    timers-ext "^0.1.7"
+
+clone@^2.1.1:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f"
+  integrity sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==
+
+codemirror@^6.0.1:
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-6.0.1.tgz#62b91142d45904547ee3e0e0e4c1a79158035a29"
+  integrity sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==
+  dependencies:
+    "@codemirror/autocomplete" "^6.0.0"
+    "@codemirror/commands" "^6.0.0"
+    "@codemirror/language" "^6.0.0"
+    "@codemirror/lint" "^6.0.0"
+    "@codemirror/search" "^6.0.0"
+    "@codemirror/state" "^6.0.0"
+    "@codemirror/view" "^6.0.0"
+
+color-convert@^1.9.0:
+  version "1.9.3"
+  resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
+  integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
+  dependencies:
+    color-name "1.1.3"
+
+color-convert@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
+  integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
+  dependencies:
+    color-name "~1.1.4"
+
+color-name@1.1.3:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
+  integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==
+
+color-name@^1.0.0, color-name@~1.1.4:
+  version "1.1.4"
+  resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
+  integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
+
+color-string@^1.9.0:
+  version "1.9.1"
+  resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.1.tgz#4467f9146f036f855b764dfb5bf8582bf342c7a4"
+  integrity sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==
+  dependencies:
+    color-name "^1.0.0"
+    simple-swizzle "^0.2.2"
+
+color@^4.2.3:
+  version "4.2.3"
+  resolved "https://registry.yarnpkg.com/color/-/color-4.2.3.tgz#d781ecb5e57224ee43ea9627560107c0e0c6463a"
+  integrity sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==
+  dependencies:
+    color-convert "^2.0.1"
+    color-string "^1.9.0"
+
+colord@^2.9.3:
+  version "2.9.3"
+  resolved "https://registry.yarnpkg.com/colord/-/colord-2.9.3.tgz#4f8ce919de456f1d5c1c368c307fe20f3e59fb43"
+  integrity sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==
+
+commander@^12.1.0:
+  version "12.1.0"
+  resolved "https://registry.yarnpkg.com/commander/-/commander-12.1.0.tgz#01423b36f501259fdaac4d0e4d60c96c991585d3"
+  integrity sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==
+
+commander@^6.1.0:
+  version "6.2.1"
+  resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c"
+  integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==
+
+commander@^7.2.0:
+  version "7.2.0"
+  resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7"
+  integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==
+
+concat-map@0.0.1:
+  version "0.0.1"
+  resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
+  integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
+
+cosmiconfig@^9.0.0:
+  version "9.0.0"
+  resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-9.0.0.tgz#34c3fc58287b915f3ae905ab6dc3de258b55ad9d"
+  integrity sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==
+  dependencies:
+    env-paths "^2.2.1"
+    import-fresh "^3.3.0"
+    js-yaml "^4.1.0"
+    parse-json "^5.2.0"
+
+crelt@^1.0.5:
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/crelt/-/crelt-1.0.6.tgz#7cc898ea74e190fb6ef9dae57f8f81cf7302df72"
+  integrity sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==
+
+cross-spawn@^6.0.5:
+  version "6.0.6"
+  resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.6.tgz#30d0efa0712ddb7eb5a76e1e8721bffafa6b5d57"
+  integrity sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==
+  dependencies:
+    nice-try "^1.0.4"
+    path-key "^2.0.1"
+    semver "^5.5.0"
+    shebang-command "^1.2.0"
+    which "^1.2.9"
+
+css-declaration-sorter@^7.2.0:
+  version "7.2.0"
+  resolved "https://registry.yarnpkg.com/css-declaration-sorter/-/css-declaration-sorter-7.2.0.tgz#6dec1c9523bc4a643e088aab8f09e67a54961024"
+  integrity sha512-h70rUM+3PNFuaBDTLe8wF/cdWu+dOZmb7pJt8Z2sedYbAcQVQV/tEchueg3GWxwqS0cxtbxmaHEdkNACqcvsow==
+
+css-select@^5.1.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/css-select/-/css-select-5.1.0.tgz#b8ebd6554c3637ccc76688804ad3f6a6fdaea8a6"
+  integrity sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==
+  dependencies:
+    boolbase "^1.0.0"
+    css-what "^6.1.0"
+    domhandler "^5.0.2"
+    domutils "^3.0.1"
+    nth-check "^2.0.1"
+
+css-tree@^2.3.1:
+  version "2.3.1"
+  resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-2.3.1.tgz#10264ce1e5442e8572fc82fbe490644ff54b5c20"
+  integrity sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==
+  dependencies:
+    mdn-data "2.0.30"
+    source-map-js "^1.0.1"
+
+css-tree@~2.2.0:
+  version "2.2.1"
+  resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-2.2.1.tgz#36115d382d60afd271e377f9c5f67d02bd48c032"
+  integrity sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==
+  dependencies:
+    mdn-data "2.0.28"
+    source-map-js "^1.0.1"
+
+css-what@^6.1.0:
+  version "6.1.0"
+  resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4"
+  integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==
+
+cssesc@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
+  integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
+
+cssnano-preset-default@^7.0.6:
+  version "7.0.6"
+  resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-7.0.6.tgz#0220fa7507478369aa2a226bac03e1204cd024c1"
+  integrity sha512-ZzrgYupYxEvdGGuqL+JKOY70s7+saoNlHSCK/OGn1vB2pQK8KSET8jvenzItcY+kA7NoWvfbb/YhlzuzNKjOhQ==
+  dependencies:
+    browserslist "^4.23.3"
+    css-declaration-sorter "^7.2.0"
+    cssnano-utils "^5.0.0"
+    postcss-calc "^10.0.2"
+    postcss-colormin "^7.0.2"
+    postcss-convert-values "^7.0.4"
+    postcss-discard-comments "^7.0.3"
+    postcss-discard-duplicates "^7.0.1"
+    postcss-discard-empty "^7.0.0"
+    postcss-discard-overridden "^7.0.0"
+    postcss-merge-longhand "^7.0.4"
+    postcss-merge-rules "^7.0.4"
+    postcss-minify-font-values "^7.0.0"
+    postcss-minify-gradients "^7.0.0"
+    postcss-minify-params "^7.0.2"
+    postcss-minify-selectors "^7.0.4"
+    postcss-normalize-charset "^7.0.0"
+    postcss-normalize-display-values "^7.0.0"
+    postcss-normalize-positions "^7.0.0"
+    postcss-normalize-repeat-style "^7.0.0"
+    postcss-normalize-string "^7.0.0"
+    postcss-normalize-timing-functions "^7.0.0"
+    postcss-normalize-unicode "^7.0.2"
+    postcss-normalize-url "^7.0.0"
+    postcss-normalize-whitespace "^7.0.0"
+    postcss-ordered-values "^7.0.1"
+    postcss-reduce-initial "^7.0.2"
+    postcss-reduce-transforms "^7.0.0"
+    postcss-svgo "^7.0.1"
+    postcss-unique-selectors "^7.0.3"
+
+cssnano-utils@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/cssnano-utils/-/cssnano-utils-5.0.0.tgz#b53a0343dd5d21012911882db6ae7d2eae0e3687"
+  integrity sha512-Uij0Xdxc24L6SirFr25MlwC2rCFX6scyUmuKpzI+JQ7cyqDEwD42fJ0xfB3yLfOnRDU5LKGgjQ9FA6LYh76GWQ==
+
+cssnano@^7.0.6:
+  version "7.0.6"
+  resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-7.0.6.tgz#63d54fd42bc017f6aaed69e47d9aaef85b7850ec"
+  integrity sha512-54woqx8SCbp8HwvNZYn68ZFAepuouZW4lTwiMVnBErM3VkO7/Sd4oTOt3Zz3bPx3kxQ36aISppyXj2Md4lg8bw==
+  dependencies:
+    cssnano-preset-default "^7.0.6"
+    lilconfig "^3.1.2"
+
+csso@^5.0.5:
+  version "5.0.5"
+  resolved "https://registry.yarnpkg.com/csso/-/csso-5.0.5.tgz#f9b7fe6cc6ac0b7d90781bb16d5e9874303e2ca6"
+  integrity sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==
+  dependencies:
+    css-tree "~2.2.0"
+
+d@1, d@^1.0.1, d@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/d/-/d-1.0.2.tgz#2aefd554b81981e7dccf72d6842ae725cb17e5de"
+  integrity sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==
+  dependencies:
+    es5-ext "^0.10.64"
+    type "^2.7.2"
+
+data-uri-to-buffer@^4.0.0:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz#d8feb2b2881e6a4f58c2e08acfd0e2834e26222e"
+  integrity sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==
+
+debug@4, debug@^4.1.0, debug@^4.1.1:
+  version "4.4.0"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a"
+  integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==
+  dependencies:
+    ms "^2.1.3"
+
+debug@4.3.2:
+  version "4.3.2"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b"
+  integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==
+  dependencies:
+    ms "2.1.2"
+
+define-data-property@^1.0.1, define-data-property@^1.1.4:
+  version "1.1.4"
+  resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e"
+  integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==
+  dependencies:
+    es-define-property "^1.0.0"
+    es-errors "^1.3.0"
+    gopd "^1.0.1"
+
+define-properties@^1.1.3, define-properties@^1.2.1:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c"
+  integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==
+  dependencies:
+    define-data-property "^1.0.1"
+    has-property-descriptors "^1.0.0"
+    object-keys "^1.1.1"
+
+dequal@^2.0.0:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be"
+  integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==
+
+detect-libc@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
+  integrity sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==
+
+detect-libc@^2.0.1, detect-libc@^2.0.3:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.3.tgz#f0cd503b40f9939b894697d19ad50895e30cf700"
+  integrity sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==
+
+devtools-protocol@0.0.937139:
+  version "0.0.937139"
+  resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.937139.tgz#bdee3751fdfdb81cb701fd3afa94b1065dafafcf"
+  integrity sha512-daj+rzR3QSxsPRy5vjjthn58axO8c11j58uY0lG5vvlJk/EiOdCWOptGdkXDjtuRHr78emKq0udHCXM4trhoDQ==
+
+diff-sequences@^27.5.1:
+  version "27.5.1"
+  resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.5.1.tgz#eaecc0d327fd68c8d9672a1e64ab8dccb2ef5327"
+  integrity sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==
+
+diff@^5.0.0:
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/diff/-/diff-5.2.0.tgz#26ded047cd1179b78b9537d5ef725503ce1ae531"
+  integrity sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==
+
+dom-serializer@^1.0.1:
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.4.1.tgz#de5d41b1aea290215dc45a6dae8adcf1d32e2d30"
+  integrity sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==
+  dependencies:
+    domelementtype "^2.0.1"
+    domhandler "^4.2.0"
+    entities "^2.0.0"
+
+dom-serializer@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53"
+  integrity sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==
+  dependencies:
+    domelementtype "^2.3.0"
+    domhandler "^5.0.2"
+    entities "^4.2.0"
+
+domelementtype@^2.0.1, domelementtype@^2.2.0, domelementtype@^2.3.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d"
+  integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==
+
+domhandler@^3.3.0:
+  version "3.3.0"
+  resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-3.3.0.tgz#6db7ea46e4617eb15cf875df68b2b8524ce0037a"
+  integrity sha512-J1C5rIANUbuYK+FuFL98650rihynUOEzRLxW+90bKZRWB6A1X1Tf82GxR1qAWLyfNPRvjqfip3Q5tdYlmAa9lA==
+  dependencies:
+    domelementtype "^2.0.1"
+
+domhandler@^4.0.0, domhandler@^4.2.0, domhandler@^4.2.2:
+  version "4.3.1"
+  resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.3.1.tgz#8d792033416f59d68bc03a5aa7b018c1ca89279c"
+  integrity sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==
+  dependencies:
+    domelementtype "^2.2.0"
+
+domhandler@^5.0.2, domhandler@^5.0.3:
+  version "5.0.3"
+  resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31"
+  integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==
+  dependencies:
+    domelementtype "^2.3.0"
+
+domutils@^2.4.2, domutils@^2.5.2, domutils@^2.8.0:
+  version "2.8.0"
+  resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135"
+  integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==
+  dependencies:
+    dom-serializer "^1.0.1"
+    domelementtype "^2.2.0"
+    domhandler "^4.2.0"
+
+domutils@^3.0.1, domutils@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.1.0.tgz#c47f551278d3dc4b0b1ab8cbb42d751a6f0d824e"
+  integrity sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==
+  dependencies:
+    dom-serializer "^2.0.0"
+    domelementtype "^2.3.0"
+    domhandler "^5.0.3"
+
+dotenv-expand@^11.0.6:
+  version "11.0.7"
+  resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-11.0.7.tgz#af695aea007d6fdc84c86cd8d0ad7beb40a0bd08"
+  integrity sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==
+  dependencies:
+    dotenv "^16.4.5"
+
+dotenv@^16.4.5:
+  version "16.4.7"
+  resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.7.tgz#0e20c5b82950140aa99be360a8a5f52335f53c26"
+  integrity sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==
+
+dunder-proto@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a"
+  integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==
+  dependencies:
+    call-bind-apply-helpers "^1.0.1"
+    es-errors "^1.3.0"
+    gopd "^1.2.0"
+
+electron-to-chromium@^1.5.73:
+  version "1.5.75"
+  resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.75.tgz#bba96eabf0e8ca36324679caa38b982800acc87d"
+  integrity sha512-Lf3++DumRE/QmweGjU+ZcKqQ+3bKkU/qjaKYhIJKEOhgIO9Xs6IiAQFkfFoj+RhgDk4LUeNsLo6plExHqSyu6Q==
+
+end-of-stream@^1.1.0, end-of-stream@^1.4.1:
+  version "1.4.4"
+  resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
+  integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==
+  dependencies:
+    once "^1.4.0"
+
+entities@^2.0.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55"
+  integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==
+
+entities@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/entities/-/entities-3.0.1.tgz#2b887ca62585e96db3903482d336c1006c3001d4"
+  integrity sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==
+
+entities@^4.2.0, entities@^4.5.0:
+  version "4.5.0"
+  resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48"
+  integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==
+
+entities@~2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5"
+  integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==
+
+env-paths@^2.2.1:
+  version "2.2.1"
+  resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2"
+  integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==
+
+error-ex@^1.3.1:
+  version "1.3.2"
+  resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
+  integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==
+  dependencies:
+    is-arrayish "^0.2.1"
+
+es-define-property@^1.0.0, es-define-property@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa"
+  integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==
+
+es-errors@^1.3.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f"
+  integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==
+
+es-object-atoms@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.0.0.tgz#ddb55cd47ac2e240701260bc2a8e31ecb643d941"
+  integrity sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==
+  dependencies:
+    es-errors "^1.3.0"
+
+es5-ext@^0.10.35, es5-ext@^0.10.46, es5-ext@^0.10.62, es5-ext@^0.10.64, es5-ext@~0.10.14, es5-ext@~0.10.2:
+  version "0.10.64"
+  resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.64.tgz#12e4ffb48f1ba2ea777f1fcdd1918ef73ea21714"
+  integrity sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==
+  dependencies:
+    es6-iterator "^2.0.3"
+    es6-symbol "^3.1.3"
+    esniff "^2.0.1"
+    next-tick "^1.1.0"
+
+es6-iterator@^2.0.3:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7"
+  integrity sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==
+  dependencies:
+    d "1"
+    es5-ext "^0.10.35"
+    es6-symbol "^3.1.1"
+
+es6-symbol@^3.1.1, es6-symbol@^3.1.3:
+  version "3.1.4"
+  resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.4.tgz#f4e7d28013770b4208ecbf3e0bf14d3bcb557b8c"
+  integrity sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==
+  dependencies:
+    d "^1.0.2"
+    ext "^1.7.0"
+
+es6-weak-map@^2.0.3:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/es6-weak-map/-/es6-weak-map-2.0.3.tgz#b6da1f16cc2cc0d9be43e6bdbfc5e7dfcdf31d53"
+  integrity sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==
+  dependencies:
+    d "1"
+    es5-ext "^0.10.46"
+    es6-iterator "^2.0.3"
+    es6-symbol "^3.1.1"
+
+esbuild@^0.19.8:
+  version "0.19.12"
+  resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.19.12.tgz#dc82ee5dc79e82f5a5c3b4323a2a641827db3e04"
+  integrity sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==
+  optionalDependencies:
+    "@esbuild/aix-ppc64" "0.19.12"
+    "@esbuild/android-arm" "0.19.12"
+    "@esbuild/android-arm64" "0.19.12"
+    "@esbuild/android-x64" "0.19.12"
+    "@esbuild/darwin-arm64" "0.19.12"
+    "@esbuild/darwin-x64" "0.19.12"
+    "@esbuild/freebsd-arm64" "0.19.12"
+    "@esbuild/freebsd-x64" "0.19.12"
+    "@esbuild/linux-arm" "0.19.12"
+    "@esbuild/linux-arm64" "0.19.12"
+    "@esbuild/linux-ia32" "0.19.12"
+    "@esbuild/linux-loong64" "0.19.12"
+    "@esbuild/linux-mips64el" "0.19.12"
+    "@esbuild/linux-ppc64" "0.19.12"
+    "@esbuild/linux-riscv64" "0.19.12"
+    "@esbuild/linux-s390x" "0.19.12"
+    "@esbuild/linux-x64" "0.19.12"
+    "@esbuild/netbsd-x64" "0.19.12"
+    "@esbuild/openbsd-x64" "0.19.12"
+    "@esbuild/sunos-x64" "0.19.12"
+    "@esbuild/win32-arm64" "0.19.12"
+    "@esbuild/win32-ia32" "0.19.12"
+    "@esbuild/win32-x64" "0.19.12"
+
+escalade@^3.2.0:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5"
+  integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==
+
+escape-string-regexp@^1.0.5:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
+  integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==
+
+esniff@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/esniff/-/esniff-2.0.1.tgz#a4d4b43a5c71c7ec51c51098c1d8a29081f9b308"
+  integrity sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==
+  dependencies:
+    d "^1.0.1"
+    es5-ext "^0.10.62"
+    event-emitter "^0.3.5"
+    type "^2.7.2"
+
+esprima@~4.0.0:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
+  integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
+
+event-emitter@^0.3.5:
+  version "0.3.5"
+  resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39"
+  integrity sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==
+  dependencies:
+    d "1"
+    es5-ext "~0.10.14"
+
+ext@^1.7.0:
+  version "1.7.0"
+  resolved "https://registry.yarnpkg.com/ext/-/ext-1.7.0.tgz#0ea4383c0103d60e70be99e9a7f11027a33c4f5f"
+  integrity sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==
+  dependencies:
+    type "^2.7.2"
+
+extract-zip@2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a"
+  integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==
+  dependencies:
+    debug "^4.1.1"
+    get-stream "^5.1.0"
+    yauzl "^2.10.0"
+  optionalDependencies:
+    "@types/yauzl" "^2.9.1"
+
+fclone@^1.0.11:
+  version "1.0.11"
+  resolved "https://registry.yarnpkg.com/fclone/-/fclone-1.0.11.tgz#10e85da38bfea7fc599341c296ee1d77266ee640"
+  integrity sha512-GDqVQezKzRABdeqflsgMr7ktzgF9CyS+p2oe0jJqUY6izSSbhPIQJDpoU4PtGcD7VPM9xh/dVrTu6z1nwgmEGw==
+
+fd-slicer@~1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e"
+  integrity sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==
+  dependencies:
+    pend "~1.2.0"
+
+fetch-blob@^3.1.2, fetch-blob@^3.1.4:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/fetch-blob/-/fetch-blob-3.2.0.tgz#f09b8d4bbd45adc6f0c20b7e787e793e309dcce9"
+  integrity sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==
+  dependencies:
+    node-domexception "^1.0.0"
+    web-streams-polyfill "^3.0.3"
+
+fill-range@^7.1.1:
+  version "7.1.1"
+  resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292"
+  integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==
+  dependencies:
+    to-regex-range "^5.0.1"
+
+find-up@^4.0.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19"
+  integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==
+  dependencies:
+    locate-path "^5.0.0"
+    path-exists "^4.0.0"
+
+find-yarn-workspace-root@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz#f47fb8d239c900eb78179aa81b66673eac88f7bd"
+  integrity sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==
+  dependencies:
+    micromatch "^4.0.2"
+
+flowgen@^1.21.0:
+  version "1.21.0"
+  resolved "https://registry.yarnpkg.com/flowgen/-/flowgen-1.21.0.tgz#f7ecb693892c4bd069492dbf77db561bbb451aa9"
+  integrity sha512-pFNFFyMLRmW6njhOIm5TrbGUDTv64aujmys2KrkRE2NYD8sXwJUyicQRwU5SPRBRJnFSD/FNlnHo2NnHI5eJSw==
+  dependencies:
+    "@babel/code-frame" "^7.16.7"
+    "@babel/highlight" "^7.16.7"
+    commander "^6.1.0"
+    lodash "^4.17.20"
+    prettier "^2.5.1"
+    shelljs "^0.8.4"
+    typescript "~4.4.4"
+    typescript-compiler "^1.4.1-2"
+
+for-each@^0.3.3:
+  version "0.3.3"
+  resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e"
+  integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==
+  dependencies:
+    is-callable "^1.1.3"
+
+formdata-polyfill@^4.0.10:
+  version "4.0.10"
+  resolved "https://registry.yarnpkg.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz#24807c31c9d402e002ab3d8c720144ceb8848423"
+  integrity sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==
+  dependencies:
+    fetch-blob "^3.1.2"
+
+fraction.js@^4.3.7:
+  version "4.3.7"
+  resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7"
+  integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==
+
+fs-constants@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad"
+  integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==
+
+fs-extra@^9.0.0:
+  version "9.1.0"
+  resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d"
+  integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==
+  dependencies:
+    at-least-node "^1.0.0"
+    graceful-fs "^4.2.0"
+    jsonfile "^6.0.1"
+    universalify "^2.0.0"
+
+fs.realpath@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
+  integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
+
+function-bind@^1.1.2:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c"
+  integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
+
+get-intrinsic@^1.2.4, get-intrinsic@^1.2.6:
+  version "1.2.6"
+  resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.6.tgz#43dd3dd0e7b49b82b2dfcad10dc824bf7fc265d5"
+  integrity sha512-qxsEs+9A+u85HhllWJJFicJfPDhRmjzoYdl64aMWW9yRIJmSyxdn8IEkuIM530/7T+lv0TIHd8L6Q/ra0tEoeA==
+  dependencies:
+    call-bind-apply-helpers "^1.0.1"
+    dunder-proto "^1.0.0"
+    es-define-property "^1.0.1"
+    es-errors "^1.3.0"
+    es-object-atoms "^1.0.0"
+    function-bind "^1.1.2"
+    gopd "^1.2.0"
+    has-symbols "^1.1.0"
+    hasown "^2.0.2"
+    math-intrinsics "^1.0.0"
+
+get-port@^4.2.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/get-port/-/get-port-4.2.0.tgz#e37368b1e863b7629c43c5a323625f95cf24b119"
+  integrity sha512-/b3jarXkH8KJoOMQc3uVGHASwGLPq3gSFJ7tgJm2diza+bydJPTGOibin2steecKeOylE8oY2JERlVWkAJO6yw==
+
+get-stdin@^8.0.0:
+  version "8.0.0"
+  resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-8.0.0.tgz#cbad6a73feb75f6eeb22ba9e01f89aa28aa97a53"
+  integrity sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==
+
+get-stream@^5.1.0:
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3"
+  integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==
+  dependencies:
+    pump "^3.0.0"
+
+glob-promise@^4.2.2:
+  version "4.2.2"
+  resolved "https://registry.yarnpkg.com/glob-promise/-/glob-promise-4.2.2.tgz#15f44bcba0e14219cd93af36da6bb905ff007877"
+  integrity sha512-xcUzJ8NWN5bktoTIX7eOclO1Npxd/dyVqUJxlLIDasT4C7KZyqlPIwkdJ0Ypiy3p2ZKahTjK4M9uC3sNSfNMzw==
+  dependencies:
+    "@types/glob" "^7.1.3"
+
+glob@^7.0.0, glob@^7.1.3, glob@^7.1.6:
+  version "7.2.3"
+  resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
+  integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==
+  dependencies:
+    fs.realpath "^1.0.0"
+    inflight "^1.0.4"
+    inherits "2"
+    minimatch "^3.1.1"
+    once "^1.3.0"
+    path-is-absolute "^1.0.0"
+
+globals@^11.1.0:
+  version "11.12.0"
+  resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
+  integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==
+
+globals@^13.2.0:
+  version "13.24.0"
+  resolved "https://registry.yarnpkg.com/globals/-/globals-13.24.0.tgz#8432a19d78ce0c1e833949c36adb345400bb1171"
+  integrity sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==
+  dependencies:
+    type-fest "^0.20.2"
+
+gopd@^1.0.1, gopd@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1"
+  integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==
+
+graceful-fs@^4.1.11, graceful-fs@^4.1.6, graceful-fs@^4.2.0:
+  version "4.2.11"
+  resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
+  integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
+
+has-flag@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
+  integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==
+
+has-flag@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
+  integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
+
+has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854"
+  integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==
+  dependencies:
+    es-define-property "^1.0.0"
+
+has-symbols@^1.0.3, has-symbols@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338"
+  integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==
+
+has-tostringtag@^1.0.0, has-tostringtag@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc"
+  integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==
+  dependencies:
+    has-symbols "^1.0.3"
+
+hasown@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003"
+  integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==
+  dependencies:
+    function-bind "^1.1.2"
+
+htmlnano@^2.0.0:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/htmlnano/-/htmlnano-2.1.1.tgz#9ba84e145cd8b7cd4c783d9ab8ff46a80e79b59b"
+  integrity sha512-kAERyg/LuNZYmdqgCdYvugyLWNFAm8MWXpQMz1pLpetmCbFwoMxvkSoaAMlFrOC4OKTWI4KlZGT/RsNxg4ghOw==
+  dependencies:
+    cosmiconfig "^9.0.0"
+    posthtml "^0.16.5"
+    timsort "^0.3.0"
+
+htmlparser2@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-5.0.1.tgz#7daa6fc3e35d6107ac95a4fc08781f091664f6e7"
+  integrity sha512-vKZZra6CSe9qsJzh0BjBGXo8dvzNsq/oGvsjfRdOrrryfeD9UOBEEQdeoqCRmKZchF5h2zOBMQ6YuQ0uRUmdbQ==
+  dependencies:
+    domelementtype "^2.0.1"
+    domhandler "^3.3.0"
+    domutils "^2.4.2"
+    entities "^2.0.0"
+
+htmlparser2@^6.0.0:
+  version "6.1.0"
+  resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.1.0.tgz#c4d762b6c3371a05dbe65e94ae43a9f845fb8fb7"
+  integrity sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==
+  dependencies:
+    domelementtype "^2.0.1"
+    domhandler "^4.0.0"
+    domutils "^2.5.2"
+    entities "^2.0.0"
+
+htmlparser2@^7.1.1:
+  version "7.2.0"
+  resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-7.2.0.tgz#8817cdea38bbc324392a90b1990908e81a65f5a5"
+  integrity sha512-H7MImA4MS6cw7nbyURtLPO1Tms7C5H602LRETv95z1MxO/7CP7rDVROehUYeYBUYEON94NXXDEPmZuq+hX4sog==
+  dependencies:
+    domelementtype "^2.0.1"
+    domhandler "^4.2.2"
+    domutils "^2.8.0"
+    entities "^3.0.1"
+
+htmlparser2@^9.0.0:
+  version "9.1.0"
+  resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-9.1.0.tgz#cdb498d8a75a51f739b61d3f718136c369bc8c23"
+  integrity sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==
+  dependencies:
+    domelementtype "^2.3.0"
+    domhandler "^5.0.3"
+    domutils "^3.1.0"
+    entities "^4.5.0"
+
+https-proxy-agent@5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2"
+  integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==
+  dependencies:
+    agent-base "6"
+    debug "4"
+
+ieee754@^1.1.13:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
+  integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
+
+import-fresh@^3.3.0:
+  version "3.3.0"
+  resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"
+  integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==
+  dependencies:
+    parent-module "^1.0.0"
+    resolve-from "^4.0.0"
+
+inflight@^1.0.4:
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
+  integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==
+  dependencies:
+    once "^1.3.0"
+    wrappy "1"
+
+inherits@2, inherits@^2.0.3, inherits@^2.0.4:
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
+  integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
+
+interpret@^1.0.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e"
+  integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==
+
+is-arguments@^1.0.4:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.2.0.tgz#ad58c6aecf563b78ef2bf04df540da8f5d7d8e1b"
+  integrity sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==
+  dependencies:
+    call-bound "^1.0.2"
+    has-tostringtag "^1.0.2"
+
+is-arrayish@^0.2.1:
+  version "0.2.1"
+  resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
+  integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==
+
+is-arrayish@^0.3.1:
+  version "0.3.2"
+  resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03"
+  integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==
+
+is-callable@^1.1.3:
+  version "1.2.7"
+  resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055"
+  integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==
+
+is-ci@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c"
+  integrity sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==
+  dependencies:
+    ci-info "^2.0.0"
+
+is-core-module@^2.16.0:
+  version "2.16.0"
+  resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.16.0.tgz#6c01ffdd5e33c49c1d2abfa93334a85cb56bd81c"
+  integrity sha512-urTSINYfAYgcbLb0yDQ6egFm6h3Mo1DcF9EkyXSRjjzdHbsulg01qhwWuXdOoUBuTkbQ80KDboXa0vFJ+BDH+g==
+  dependencies:
+    hasown "^2.0.2"
+
+is-docker@^2.0.0:
+  version "2.2.1"
+  resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa"
+  integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==
+
+is-extglob@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
+  integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==
+
+is-generator-function@^1.0.7:
+  version "1.0.10"
+  resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.10.tgz#f1558baf1ac17e0deea7c0415c438351ff2b3c72"
+  integrity sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==
+  dependencies:
+    has-tostringtag "^1.0.0"
+
+is-glob@^4.0.3:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
+  integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
+  dependencies:
+    is-extglob "^2.1.1"
+
+is-json@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/is-json/-/is-json-2.0.1.tgz#6be166d144828a131d686891b983df62c39491ff"
+  integrity sha512-6BEnpVn1rcf3ngfmViLM6vjUjGErbdrL4rwlv+u1NO1XO8kqT4YGL8+19Q+Z/bas8tY90BTWMk2+fW1g6hQjbA==
+
+is-nan@^1.3.2:
+  version "1.3.2"
+  resolved "https://registry.yarnpkg.com/is-nan/-/is-nan-1.3.2.tgz#043a54adea31748b55b6cd4e09aadafa69bd9e1d"
+  integrity sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==
+  dependencies:
+    call-bind "^1.0.0"
+    define-properties "^1.1.3"
+
+is-number@^7.0.0:
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
+  integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
+
+is-promise@^2.2.2:
+  version "2.2.2"
+  resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.2.2.tgz#39ab959ccbf9a774cf079f7b40c7a26f763135f1"
+  integrity sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==
+
+is-typed-array@^1.1.3:
+  version "1.1.15"
+  resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.15.tgz#4bfb4a45b61cee83a5a46fba778e4e8d59c0ce0b"
+  integrity sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==
+  dependencies:
+    which-typed-array "^1.1.16"
+
+is-wsl@^2.1.1:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271"
+  integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==
+  dependencies:
+    is-docker "^2.0.0"
+
+isexe@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
+  integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
+
+jest-diff@^27.4.2:
+  version "27.5.1"
+  resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-27.5.1.tgz#a07f5011ac9e6643cf8a95a462b7b1ecf6680def"
+  integrity sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw==
+  dependencies:
+    chalk "^4.0.0"
+    diff-sequences "^27.5.1"
+    jest-get-type "^27.5.1"
+    pretty-format "^27.5.1"
+
+jest-get-type@^27.5.1:
+  version "27.5.1"
+  resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-27.5.1.tgz#3cd613c507b0f7ace013df407a1c1cd578bcb4f1"
+  integrity sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==
+
+js-tokens@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
+  integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
+
+js-yaml@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602"
+  integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==
+  dependencies:
+    argparse "^2.0.1"
+
+jsesc@^3.0.2:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d"
+  integrity sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==
+
+json-parse-even-better-errors@^2.3.0:
+  version "2.3.1"
+  resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d"
+  integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==
+
+json-schema-to-typescript@^11.0.2:
+  version "11.0.5"
+  resolved "https://registry.yarnpkg.com/json-schema-to-typescript/-/json-schema-to-typescript-11.0.5.tgz#04020422b7970e1c3b2ee8b601548e8751e1cd03"
+  integrity sha512-ZNlvngzlPzjYYECbR+uJ9aUWo25Gw/VuwUytvcuKiwc6NaiZhMyf7qBsxZE2eixmj8AoQEQJhSRG7btln0sUDw==
+  dependencies:
+    "@bcherny/json-schema-ref-parser" "10.0.5-fork"
+    "@types/json-schema" "^7.0.11"
+    "@types/lodash" "^4.14.182"
+    "@types/prettier" "^2.6.1"
+    cli-color "^2.0.2"
+    get-stdin "^8.0.0"
+    glob "^7.1.6"
+    glob-promise "^4.2.2"
+    is-glob "^4.0.3"
+    lodash "^4.17.21"
+    minimist "^1.2.6"
+    mkdirp "^1.0.4"
+    mz "^2.7.0"
+    prettier "^2.6.2"
+
+json5@^2.2.0, json5@^2.2.1:
+  version "2.2.3"
+  resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283"
+  integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==
+
+jsonfile@^6.0.1:
+  version "6.1.0"
+  resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae"
+  integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==
+  dependencies:
+    universalify "^2.0.0"
+  optionalDependencies:
+    graceful-fs "^4.1.6"
+
+klaw-sync@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/klaw-sync/-/klaw-sync-6.0.0.tgz#1fd2cfd56ebb6250181114f0a581167099c2b28c"
+  integrity sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==
+  dependencies:
+    graceful-fs "^4.1.11"
+
+kleur@^4.0.3:
+  version "4.1.5"
+  resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.5.tgz#95106101795f7050c6c650f350c683febddb1780"
+  integrity sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==
+
+lightningcss@^1.22.1:
+  version "0.0.0"
+  uid ""
+
+"lightningcss@link:.":
+  version "0.0.0"
+  uid ""
+
+lilconfig@^3.1.2:
+  version "3.1.3"
+  resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-3.1.3.tgz#a1bcfd6257f9585bf5ae14ceeebb7b559025e4c4"
+  integrity sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==
+
+lines-and-columns@^1.1.6:
+  version "1.2.4"
+  resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632"
+  integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==
+
+linkify-it@^3.0.1:
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-3.0.3.tgz#a98baf44ce45a550efb4d49c769d07524cc2fa2e"
+  integrity sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==
+  dependencies:
+    uc.micro "^1.0.1"
+
+lmdb@2.8.5:
+  version "2.8.5"
+  resolved "https://registry.yarnpkg.com/lmdb/-/lmdb-2.8.5.tgz#ce191110c755c0951caa062722e300c703973837"
+  integrity sha512-9bMdFfc80S+vSldBmG3HOuLVHnxRdNTlpzR6QDnzqCQtCzGUEAGTzBKYMeIM+I/sU4oZfgbcbS7X7F65/z/oxQ==
+  dependencies:
+    msgpackr "^1.9.5"
+    node-addon-api "^6.1.0"
+    node-gyp-build-optional-packages "5.1.1"
+    ordered-binary "^1.4.1"
+    weak-lru-cache "^1.2.2"
+  optionalDependencies:
+    "@lmdb/lmdb-darwin-arm64" "2.8.5"
+    "@lmdb/lmdb-darwin-x64" "2.8.5"
+    "@lmdb/lmdb-linux-arm" "2.8.5"
+    "@lmdb/lmdb-linux-arm64" "2.8.5"
+    "@lmdb/lmdb-linux-x64" "2.8.5"
+    "@lmdb/lmdb-win32-x64" "2.8.5"
+
+locate-path@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0"
+  integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==
+  dependencies:
+    p-locate "^4.1.0"
+
+lodash.memoize@^4.1.2:
+  version "4.1.2"
+  resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
+  integrity sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==
+
+lodash.uniq@^4.5.0:
+  version "4.5.0"
+  resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
+  integrity sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==
+
+lodash@^4.17.20, lodash@^4.17.21:
+  version "4.17.21"
+  resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
+  integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
+
+lru-queue@^0.1.0:
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/lru-queue/-/lru-queue-0.1.0.tgz#2738bd9f0d3cf4f84490c5736c48699ac632cda3"
+  integrity sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ==
+  dependencies:
+    es5-ext "~0.10.2"
+
+markdown-it-anchor@^8.6.6:
+  version "8.6.7"
+  resolved "https://registry.yarnpkg.com/markdown-it-anchor/-/markdown-it-anchor-8.6.7.tgz#ee6926daf3ad1ed5e4e3968b1740eef1c6399634"
+  integrity sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA==
+
+markdown-it-prism@^2.3.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/markdown-it-prism/-/markdown-it-prism-2.3.0.tgz#8b3ca2105e665ca20f62b2dc2b81f9393660a5af"
+  integrity sha512-ePtHY80gZyeje4bn3R3SL0jpd1C9HFaYffJW2Ma0YD+tspqa2v9TuVwUyFwboFu4jnFNcO8oPQROgbcYJbmBvw==
+  dependencies:
+    prismjs "1.29.0"
+
+markdown-it-table-of-contents@^0.6.0:
+  version "0.6.0"
+  resolved "https://registry.yarnpkg.com/markdown-it-table-of-contents/-/markdown-it-table-of-contents-0.6.0.tgz#7c9e8d619fb12f88c9fb05a7b32b2fe932b9c541"
+  integrity sha512-jHvEjZVEibyW97zEYg19mZCIXO16lHbvRaPDkEuOfMPBmzlI7cYczMZLMfUvwkhdOVQpIxu3gx6mgaw46KsNsQ==
+
+markdown-it@^12.0.2:
+  version "12.3.2"
+  resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-12.3.2.tgz#bf92ac92283fe983fe4de8ff8abfb5ad72cd0c90"
+  integrity sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==
+  dependencies:
+    argparse "^2.0.1"
+    entities "~2.1.0"
+    linkify-it "^3.0.1"
+    mdurl "^1.0.1"
+    uc.micro "^1.0.5"
+
+math-intrinsics@^1.0.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9"
+  integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==
+
+mdn-data@2.0.28:
+  version "2.0.28"
+  resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.28.tgz#5ec48e7bef120654539069e1ae4ddc81ca490eba"
+  integrity sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==
+
+mdn-data@2.0.30:
+  version "2.0.30"
+  resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.30.tgz#ce4df6f80af6cfbe218ecd5c552ba13c4dfa08cc"
+  integrity sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==
+
+mdurl@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e"
+  integrity sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==
+
+memoizee@^0.4.15:
+  version "0.4.17"
+  resolved "https://registry.yarnpkg.com/memoizee/-/memoizee-0.4.17.tgz#942a5f8acee281fa6fb9c620bddc57e3b7382949"
+  integrity sha512-DGqD7Hjpi/1or4F/aYAspXKNm5Yili0QDAFAY4QYvpqpgiY6+1jOfqpmByzjxbWd/T9mChbCArXAbDAsTm5oXA==
+  dependencies:
+    d "^1.0.2"
+    es5-ext "^0.10.64"
+    es6-weak-map "^2.0.3"
+    event-emitter "^0.3.5"
+    is-promise "^2.2.2"
+    lru-queue "^0.1.0"
+    next-tick "^1.1.0"
+    timers-ext "^0.1.7"
+
+micromatch@^4.0.2, micromatch@^4.0.5:
+  version "4.0.8"
+  resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202"
+  integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==
+  dependencies:
+    braces "^3.0.3"
+    picomatch "^2.3.1"
+
+min-indent@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869"
+  integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==
+
+minimatch@^3.1.1:
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
+  integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
+  dependencies:
+    brace-expansion "^1.1.7"
+
+minimist@^1.2.6:
+  version "1.2.8"
+  resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
+  integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
+
+mkdirp-classic@^0.5.2:
+  version "0.5.3"
+  resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113"
+  integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==
+
+mkdirp@^1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
+  integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
+
+mri@^1.1.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b"
+  integrity sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==
+
+ms@2.1.2:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
+  integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
+
+ms@^2.1.3:
+  version "2.1.3"
+  resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
+  integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
+
+msgpackr-extract@^3.0.2:
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz#e9d87023de39ce714872f9e9504e3c1996d61012"
+  integrity sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==
+  dependencies:
+    node-gyp-build-optional-packages "5.2.2"
+  optionalDependencies:
+    "@msgpackr-extract/msgpackr-extract-darwin-arm64" "3.0.3"
+    "@msgpackr-extract/msgpackr-extract-darwin-x64" "3.0.3"
+    "@msgpackr-extract/msgpackr-extract-linux-arm" "3.0.3"
+    "@msgpackr-extract/msgpackr-extract-linux-arm64" "3.0.3"
+    "@msgpackr-extract/msgpackr-extract-linux-x64" "3.0.3"
+    "@msgpackr-extract/msgpackr-extract-win32-x64" "3.0.3"
+
+msgpackr@^1.9.5, msgpackr@^1.9.9:
+  version "1.11.2"
+  resolved "https://registry.yarnpkg.com/msgpackr/-/msgpackr-1.11.2.tgz#4463b7f7d68f2e24865c395664973562ad24473d"
+  integrity sha512-F9UngXRlPyWCDEASDpTf6c9uNhGPTqnTeLVt7bN+bU1eajoR/8V9ys2BRaV5C/e5ihE6sJ9uPIKaYt6bFuO32g==
+  optionalDependencies:
+    msgpackr-extract "^3.0.2"
+
+mz@^2.7.0:
+  version "2.7.0"
+  resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32"
+  integrity sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==
+  dependencies:
+    any-promise "^1.0.0"
+    object-assign "^4.0.1"
+    thenify-all "^1.0.0"
+
+nanoid@^3.3.7:
+  version "3.3.8"
+  resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf"
+  integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==
+
+napi-wasm@^1.0.1:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/napi-wasm/-/napi-wasm-1.1.3.tgz#7bb95c88e6561f84880bb67195437b1cfbe99224"
+  integrity sha512-h/4nMGsHjZDCYmQVNODIrYACVJ+I9KItbG+0si6W/jSjdA9JbWDoU4LLeMXVcEQGHjttI2tuXqDrbGF7qkUHHg==
+
+next-tick@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb"
+  integrity sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==
+
+nice-try@^1.0.4:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
+  integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
+
+node-addon-api@^6.1.0:
+  version "6.1.0"
+  resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-6.1.0.tgz#ac8470034e58e67d0c6f1204a18ae6995d9c0d76"
+  integrity sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==
+
+node-addon-api@^7.0.0:
+  version "7.1.1"
+  resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-7.1.1.tgz#1aba6693b0f255258a049d621329329322aad558"
+  integrity sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==
+
+node-domexception@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5"
+  integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==
+
+node-fetch@2.6.5:
+  version "2.6.5"
+  resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.5.tgz#42735537d7f080a7e5f78b6c549b7146be1742fd"
+  integrity sha512-mmlIVHJEu5rnIxgEgez6b9GgWXbkZj5YZ7fx+2r94a2E+Uirsp6HsPTPlomfdHtpt/B0cdKviwkoaM6pyvUOpQ==
+  dependencies:
+    whatwg-url "^5.0.0"
+
+node-fetch@^3.1.0:
+  version "3.3.2"
+  resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.3.2.tgz#d1e889bacdf733b4ff3b2b243eb7a12866a0b78b"
+  integrity sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==
+  dependencies:
+    data-uri-to-buffer "^4.0.0"
+    fetch-blob "^3.1.4"
+    formdata-polyfill "^4.0.10"
+
+node-gyp-build-optional-packages@5.1.1:
+  version "5.1.1"
+  resolved "https://registry.yarnpkg.com/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.1.1.tgz#52b143b9dd77b7669073cbfe39e3f4118bfc603c"
+  integrity sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw==
+  dependencies:
+    detect-libc "^2.0.1"
+
+node-gyp-build-optional-packages@5.2.2:
+  version "5.2.2"
+  resolved "https://registry.yarnpkg.com/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz#522f50c2d53134d7f3a76cd7255de4ab6c96a3a4"
+  integrity sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==
+  dependencies:
+    detect-libc "^2.0.1"
+
+node-releases@^2.0.19:
+  version "2.0.19"
+  resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.19.tgz#9e445a52950951ec4d177d843af370b411caf314"
+  integrity sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==
+
+normalize-range@^0.1.2:
+  version "0.1.2"
+  resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942"
+  integrity sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==
+
+nth-check@^2.0.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d"
+  integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==
+  dependencies:
+    boolbase "^1.0.0"
+
+nullthrows@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/nullthrows/-/nullthrows-1.1.1.tgz#7818258843856ae971eae4208ad7d7eb19a431b1"
+  integrity sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==
+
+object-assign@^4.0.1:
+  version "4.1.1"
+  resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
+  integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==
+
+object-is@^1.1.5:
+  version "1.1.6"
+  resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.6.tgz#1a6a53aed2dd8f7e6775ff870bea58545956ab07"
+  integrity sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==
+  dependencies:
+    call-bind "^1.0.7"
+    define-properties "^1.2.1"
+
+object-keys@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
+  integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
+
+object.assign@^4.1.4:
+  version "4.1.7"
+  resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.7.tgz#8c14ca1a424c6a561b0bb2a22f66f5049a945d3d"
+  integrity sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==
+  dependencies:
+    call-bind "^1.0.8"
+    call-bound "^1.0.3"
+    define-properties "^1.2.1"
+    es-object-atoms "^1.0.0"
+    has-symbols "^1.1.0"
+    object-keys "^1.1.1"
+
+once@^1.3.0, once@^1.3.1, once@^1.4.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
+  integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==
+  dependencies:
+    wrappy "1"
+
+open@^7.4.2:
+  version "7.4.2"
+  resolved "https://registry.yarnpkg.com/open/-/open-7.4.2.tgz#b8147e26dcf3e426316c730089fd71edd29c2321"
+  integrity sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==
+  dependencies:
+    is-docker "^2.0.0"
+    is-wsl "^2.1.1"
+
+ordered-binary@^1.4.1:
+  version "1.5.3"
+  resolved "https://registry.yarnpkg.com/ordered-binary/-/ordered-binary-1.5.3.tgz#8bee2aa7a82c3439caeb1e80c272fd4cf51170fb"
+  integrity sha512-oGFr3T+pYdTGJ+YFEILMpS3es+GiIbs9h/XQrclBXUtd44ey7XwfsMzM31f64I1SQOawDoDr/D823kNCADI8TA==
+
+os-tmpdir@~1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
+  integrity sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==
+
+p-limit@^2.2.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1"
+  integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==
+  dependencies:
+    p-try "^2.0.0"
+
+p-locate@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07"
+  integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==
+  dependencies:
+    p-limit "^2.2.0"
+
+p-try@^2.0.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
+  integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
+
+parcel@^2.8.2:
+  version "2.13.3"
+  resolved "https://registry.yarnpkg.com/parcel/-/parcel-2.13.3.tgz#d82c31ecf50169215e31a716b0f8ee5a20bdd865"
+  integrity sha512-8GrC8C7J8mwRpAlk7EJ7lwdFTbCN+dcXH2gy5AsEs9pLfzo9wvxOTx6W0fzSlvCOvZOita+8GdfYlGfEt0tRgA==
+  dependencies:
+    "@parcel/config-default" "2.13.3"
+    "@parcel/core" "2.13.3"
+    "@parcel/diagnostic" "2.13.3"
+    "@parcel/events" "2.13.3"
+    "@parcel/feature-flags" "2.13.3"
+    "@parcel/fs" "2.13.3"
+    "@parcel/logger" "2.13.3"
+    "@parcel/package-manager" "2.13.3"
+    "@parcel/reporter-cli" "2.13.3"
+    "@parcel/reporter-dev-server" "2.13.3"
+    "@parcel/reporter-tracer" "2.13.3"
+    "@parcel/utils" "2.13.3"
+    chalk "^4.1.2"
+    commander "^12.1.0"
+    get-port "^4.2.0"
+
+parent-module@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2"
+  integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==
+  dependencies:
+    callsites "^3.0.0"
+
+parse-json@^5.2.0:
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd"
+  integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==
+  dependencies:
+    "@babel/code-frame" "^7.0.0"
+    error-ex "^1.3.1"
+    json-parse-even-better-errors "^2.3.0"
+    lines-and-columns "^1.1.6"
+
+patch-package@^6.5.0:
+  version "6.5.1"
+  resolved "https://registry.yarnpkg.com/patch-package/-/patch-package-6.5.1.tgz#3e5d00c16997e6160291fee06a521c42ac99b621"
+  integrity sha512-I/4Zsalfhc6bphmJTlrLoOcAF87jcxko4q0qsv4bGcurbr8IskEOtdnt9iCmsQVGL1B+iUhSQqweyTLJfCF9rA==
+  dependencies:
+    "@yarnpkg/lockfile" "^1.1.0"
+    chalk "^4.1.2"
+    cross-spawn "^6.0.5"
+    find-yarn-workspace-root "^2.0.0"
+    fs-extra "^9.0.0"
+    is-ci "^2.0.0"
+    klaw-sync "^6.0.0"
+    minimist "^1.2.6"
+    open "^7.4.2"
+    rimraf "^2.6.3"
+    semver "^5.6.0"
+    slash "^2.0.0"
+    tmp "^0.0.33"
+    yaml "^1.10.2"
+
+path-browserify@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd"
+  integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==
+
+path-exists@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
+  integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==
+
+path-is-absolute@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
+  integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==
+
+path-key@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
+  integrity sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==
+
+path-parse@^1.0.7:
+  version "1.0.7"
+  resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
+  integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
+
+pend@~1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"
+  integrity sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==
+
+picocolors@^1.0.0, picocolors@^1.1.0, picocolors@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
+  integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
+
+picomatch@^2.3.1:
+  version "2.3.1"
+  resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
+  integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
+
+pkg-dir@4.2.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3"
+  integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==
+  dependencies:
+    find-up "^4.0.0"
+
+possible-typed-array-names@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz#89bb63c6fada2c3e90adc4a647beeeb39cc7bf8f"
+  integrity sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==
+
+postcss-calc@^10.0.2:
+  version "10.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-10.0.2.tgz#15f01635a27b9d38913a98c4ef2877f5b715b439"
+  integrity sha512-DT/Wwm6fCKgpYVI7ZEWuPJ4az8hiEHtCUeYjZXqU7Ou4QqYh1Df2yCQ7Ca6N7xqKPFkxN3fhf+u9KSoOCJNAjg==
+  dependencies:
+    postcss-selector-parser "^6.1.2"
+    postcss-value-parser "^4.2.0"
+
+postcss-colormin@^7.0.2:
+  version "7.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-7.0.2.tgz#6f3c53c13158168669f45adc3926f35cb240ef8e"
+  integrity sha512-YntRXNngcvEvDbEjTdRWGU606eZvB5prmHG4BF0yLmVpamXbpsRJzevyy6MZVyuecgzI2AWAlvFi8DAeCqwpvA==
+  dependencies:
+    browserslist "^4.23.3"
+    caniuse-api "^3.0.0"
+    colord "^2.9.3"
+    postcss-value-parser "^4.2.0"
+
+postcss-convert-values@^7.0.4:
+  version "7.0.4"
+  resolved "https://registry.yarnpkg.com/postcss-convert-values/-/postcss-convert-values-7.0.4.tgz#fc13ecedded6365f3c794b502dbcf77d298da12c"
+  integrity sha512-e2LSXPqEHVW6aoGbjV9RsSSNDO3A0rZLCBxN24zvxF25WknMPpX8Dm9UxxThyEbaytzggRuZxaGXqaOhxQ514Q==
+  dependencies:
+    browserslist "^4.23.3"
+    postcss-value-parser "^4.2.0"
+
+postcss-discard-comments@^7.0.3:
+  version "7.0.3"
+  resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-7.0.3.tgz#9c414e8ee99d3514ad06a3465ccc20ec1dbce780"
+  integrity sha512-q6fjd4WU4afNhWOA2WltHgCbkRhZPgQe7cXF74fuVB/ge4QbM9HEaOIzGSiMvM+g/cOsNAUGdf2JDzqA2F8iLA==
+  dependencies:
+    postcss-selector-parser "^6.1.2"
+
+postcss-discard-duplicates@^7.0.1:
+  version "7.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-discard-duplicates/-/postcss-discard-duplicates-7.0.1.tgz#f87f2fe47d8f01afb1e98361c1db3ce1e8afd1a3"
+  integrity sha512-oZA+v8Jkpu1ct/xbbrntHRsfLGuzoP+cpt0nJe5ED2FQF8n8bJtn7Bo28jSmBYwqgqnqkuSXJfSUEE7if4nClQ==
+
+postcss-discard-empty@^7.0.0:
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/postcss-discard-empty/-/postcss-discard-empty-7.0.0.tgz#218829d1ef0a5d5142dd62f0aa60e00e599d2033"
+  integrity sha512-e+QzoReTZ8IAwhnSdp/++7gBZ/F+nBq9y6PomfwORfP7q9nBpK5AMP64kOt0bA+lShBFbBDcgpJ3X4etHg4lzA==
+
+postcss-discard-overridden@^7.0.0:
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/postcss-discard-overridden/-/postcss-discard-overridden-7.0.0.tgz#b123ea51e3d4e1d0a254cf71eaff1201926d319c"
+  integrity sha512-GmNAzx88u3k2+sBTZrJSDauR0ccpE24omTQCVmaTTZFz1du6AasspjaUPMJ2ud4RslZpoFKyf+6MSPETLojc6w==
+
+postcss-merge-longhand@^7.0.4:
+  version "7.0.4"
+  resolved "https://registry.yarnpkg.com/postcss-merge-longhand/-/postcss-merge-longhand-7.0.4.tgz#a52d0662b4b29420f3b64a8d5b0ac5133d8db776"
+  integrity sha512-zer1KoZA54Q8RVHKOY5vMke0cCdNxMP3KBfDerjH/BYHh4nCIh+1Yy0t1pAEQF18ac/4z3OFclO+ZVH8azjR4A==
+  dependencies:
+    postcss-value-parser "^4.2.0"
+    stylehacks "^7.0.4"
+
+postcss-merge-rules@^7.0.4:
+  version "7.0.4"
+  resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-7.0.4.tgz#648cc864d3121e6ec72c2a4f08df1cc801e60ce8"
+  integrity sha512-ZsaamiMVu7uBYsIdGtKJ64PkcQt6Pcpep/uO90EpLS3dxJi6OXamIobTYcImyXGoW0Wpugh7DSD3XzxZS9JCPg==
+  dependencies:
+    browserslist "^4.23.3"
+    caniuse-api "^3.0.0"
+    cssnano-utils "^5.0.0"
+    postcss-selector-parser "^6.1.2"
+
+postcss-minify-font-values@^7.0.0:
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/postcss-minify-font-values/-/postcss-minify-font-values-7.0.0.tgz#d16a75a2548e000779566b3568fc874ee5d0aa17"
+  integrity sha512-2ckkZtgT0zG8SMc5aoNwtm5234eUx1GGFJKf2b1bSp8UflqaeFzR50lid4PfqVI9NtGqJ2J4Y7fwvnP/u1cQog==
+  dependencies:
+    postcss-value-parser "^4.2.0"
+
+postcss-minify-gradients@^7.0.0:
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/postcss-minify-gradients/-/postcss-minify-gradients-7.0.0.tgz#f6d84456e6d49164a55d0e45bb1b1809c6cf0959"
+  integrity sha512-pdUIIdj/C93ryCHew0UgBnL2DtUS3hfFa5XtERrs4x+hmpMYGhbzo6l/Ir5de41O0GaKVpK1ZbDNXSY6GkXvtg==
+  dependencies:
+    colord "^2.9.3"
+    cssnano-utils "^5.0.0"
+    postcss-value-parser "^4.2.0"
+
+postcss-minify-params@^7.0.2:
+  version "7.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-minify-params/-/postcss-minify-params-7.0.2.tgz#264a76e25f202d8b5ca5290569c0e8c3ac599dfe"
+  integrity sha512-nyqVLu4MFl9df32zTsdcLqCFfE/z2+f8GE1KHPxWOAmegSo6lpV2GNy5XQvrzwbLmiU7d+fYay4cwto1oNdAaQ==
+  dependencies:
+    browserslist "^4.23.3"
+    cssnano-utils "^5.0.0"
+    postcss-value-parser "^4.2.0"
+
+postcss-minify-selectors@^7.0.4:
+  version "7.0.4"
+  resolved "https://registry.yarnpkg.com/postcss-minify-selectors/-/postcss-minify-selectors-7.0.4.tgz#2b69c99ec48a1c223fce4840609d9c53340a11f5"
+  integrity sha512-JG55VADcNb4xFCf75hXkzc1rNeURhlo7ugf6JjiiKRfMsKlDzN9CXHZDyiG6x/zGchpjQS+UAgb1d4nqXqOpmA==
+  dependencies:
+    cssesc "^3.0.0"
+    postcss-selector-parser "^6.1.2"
+
+postcss-normalize-charset@^7.0.0:
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-7.0.0.tgz#92244ae73c31bf8f8885d5f16ff69e857ac6c001"
+  integrity sha512-ABisNUXMeZeDNzCQxPxBCkXexvBrUHV+p7/BXOY+ulxkcjUZO0cp8ekGBwvIh2LbCwnWbyMPNJVtBSdyhM2zYQ==
+
+postcss-normalize-display-values@^7.0.0:
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-display-values/-/postcss-normalize-display-values-7.0.0.tgz#01fb50e5e97ef8935363629bea5a6d3b3aac1342"
+  integrity sha512-lnFZzNPeDf5uGMPYgGOw7v0BfB45+irSRz9gHQStdkkhiM0gTfvWkWB5BMxpn0OqgOQuZG/mRlZyJxp0EImr2Q==
+  dependencies:
+    postcss-value-parser "^4.2.0"
+
+postcss-normalize-positions@^7.0.0:
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-positions/-/postcss-normalize-positions-7.0.0.tgz#4eebd7c9d3dde40c97b8047cad38124fc844c463"
+  integrity sha512-I0yt8wX529UKIGs2y/9Ybs2CelSvItfmvg/DBIjTnoUSrPxSV7Z0yZ8ShSVtKNaV/wAY+m7bgtyVQLhB00A1NQ==
+  dependencies:
+    postcss-value-parser "^4.2.0"
+
+postcss-normalize-repeat-style@^7.0.0:
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-7.0.0.tgz#0cb784655d5714d29bd3bda6dee2fb628aa7227b"
+  integrity sha512-o3uSGYH+2q30ieM3ppu9GTjSXIzOrRdCUn8UOMGNw7Af61bmurHTWI87hRybrP6xDHvOe5WlAj3XzN6vEO8jLw==
+  dependencies:
+    postcss-value-parser "^4.2.0"
+
+postcss-normalize-string@^7.0.0:
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-string/-/postcss-normalize-string-7.0.0.tgz#a119d3e63a9614570d8413d572fb9fc8c6a64e8c"
+  integrity sha512-w/qzL212DFVOpMy3UGyxrND+Kb0fvCiBBujiaONIihq7VvtC7bswjWgKQU/w4VcRyDD8gpfqUiBQ4DUOwEJ6Qg==
+  dependencies:
+    postcss-value-parser "^4.2.0"
+
+postcss-normalize-timing-functions@^7.0.0:
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-7.0.0.tgz#99d0ee8c4b23b7f4355fafb91385833b9b07108b"
+  integrity sha512-tNgw3YV0LYoRwg43N3lTe3AEWZ66W7Dh7lVEpJbHoKOuHc1sLrzMLMFjP8SNULHaykzsonUEDbKedv8C+7ej6g==
+  dependencies:
+    postcss-value-parser "^4.2.0"
+
+postcss-normalize-unicode@^7.0.2:
+  version "7.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-unicode/-/postcss-normalize-unicode-7.0.2.tgz#095f8d36ea29adfdf494069c1de101112992a713"
+  integrity sha512-ztisabK5C/+ZWBdYC+Y9JCkp3M9qBv/XFvDtSw0d/XwfT3UaKeW/YTm/MD/QrPNxuecia46vkfEhewjwcYFjkg==
+  dependencies:
+    browserslist "^4.23.3"
+    postcss-value-parser "^4.2.0"
+
+postcss-normalize-url@^7.0.0:
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-url/-/postcss-normalize-url-7.0.0.tgz#c88cb7cf8952d3ff631e4eba924e7b060ca802f6"
+  integrity sha512-+d7+PpE+jyPX1hDQZYG+NaFD+Nd2ris6r8fPTBAjE8z/U41n/bib3vze8x7rKs5H1uEw5ppe9IojewouHk0klQ==
+  dependencies:
+    postcss-value-parser "^4.2.0"
+
+postcss-normalize-whitespace@^7.0.0:
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-whitespace/-/postcss-normalize-whitespace-7.0.0.tgz#46b025f0bea72139ddee63015619b0c21cebd845"
+  integrity sha512-37/toN4wwZErqohedXYqWgvcHUGlT8O/m2jVkAfAe9Bd4MzRqlBmXrJRePH0e9Wgnz2X7KymTgTOaaFizQe3AQ==
+  dependencies:
+    postcss-value-parser "^4.2.0"
+
+postcss-ordered-values@^7.0.1:
+  version "7.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-7.0.1.tgz#8b4b5b8070ca7756bd49f07d5edf274b8f6782e0"
+  integrity sha512-irWScWRL6nRzYmBOXReIKch75RRhNS86UPUAxXdmW/l0FcAsg0lvAXQCby/1lymxn/o0gVa6Rv/0f03eJOwHxw==
+  dependencies:
+    cssnano-utils "^5.0.0"
+    postcss-value-parser "^4.2.0"
+
+postcss-reduce-initial@^7.0.2:
+  version "7.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-7.0.2.tgz#3dc085347a5943e18547d4b0aa5bd4ff5a93b2c5"
+  integrity sha512-pOnu9zqQww7dEKf62Nuju6JgsW2V0KRNBHxeKohU+JkHd/GAH5uvoObqFLqkeB2n20mr6yrlWDvo5UBU5GnkfA==
+  dependencies:
+    browserslist "^4.23.3"
+    caniuse-api "^3.0.0"
+
+postcss-reduce-transforms@^7.0.0:
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/postcss-reduce-transforms/-/postcss-reduce-transforms-7.0.0.tgz#0386080a14e5faad9f8eda33375b79fe7c4f9677"
+  integrity sha512-pnt1HKKZ07/idH8cpATX/ujMbtOGhUfE+m8gbqwJE05aTaNw8gbo34a2e3if0xc0dlu75sUOiqvwCGY3fzOHew==
+  dependencies:
+    postcss-value-parser "^4.2.0"
+
+postcss-selector-parser@^6.1.2:
+  version "6.1.2"
+  resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz#27ecb41fb0e3b6ba7a1ec84fff347f734c7929de"
+  integrity sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==
+  dependencies:
+    cssesc "^3.0.0"
+    util-deprecate "^1.0.2"
+
+postcss-svgo@^7.0.1:
+  version "7.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-7.0.1.tgz#2b63571d8e9568384df334bac9917baff4d23f58"
+  integrity sha512-0WBUlSL4lhD9rA5k1e5D8EN5wCEyZD6HJk0jIvRxl+FDVOMlJ7DePHYWGGVc5QRqrJ3/06FTXM0bxjmJpmTPSA==
+  dependencies:
+    postcss-value-parser "^4.2.0"
+    svgo "^3.3.2"
+
+postcss-unique-selectors@^7.0.3:
+  version "7.0.3"
+  resolved "https://registry.yarnpkg.com/postcss-unique-selectors/-/postcss-unique-selectors-7.0.3.tgz#483fc11215b23d517d5d9bbe5833d9915619ca33"
+  integrity sha512-J+58u5Ic5T1QjP/LDV9g3Cx4CNOgB5vz+kM6+OxHHhFACdcDeKhBXjQmB7fnIZM12YSTvsL0Opwco83DmacW2g==
+  dependencies:
+    postcss-selector-parser "^6.1.2"
+
+postcss-value-parser@^4.2.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514"
+  integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
+
+postcss@^8.3.11:
+  version "8.4.49"
+  resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.49.tgz#4ea479048ab059ab3ae61d082190fabfd994fe19"
+  integrity sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==
+  dependencies:
+    nanoid "^3.3.7"
+    picocolors "^1.1.1"
+    source-map-js "^1.2.1"
+
+posthtml-expressions@^1.7.1:
+  version "1.11.4"
+  resolved "https://registry.yarnpkg.com/posthtml-expressions/-/posthtml-expressions-1.11.4.tgz#eb86666de10940268a74fe0f3fb62d3f7607b5de"
+  integrity sha512-tJI6KhKLcePRO0/i4d01MNXfcaBa2jIu4MuVLixvGwCRzxdY2D7LLm17ijNyQNQu3xOhCffBLtUMju0K64smmQ==
+  dependencies:
+    fclone "^1.0.11"
+    posthtml "^0.16.5"
+    posthtml-match-helper "^1.0.1"
+    posthtml-parser "^0.10.0"
+    posthtml-render "^3.0.0"
+
+posthtml-include@^1.7.4:
+  version "1.7.4"
+  resolved "https://registry.yarnpkg.com/posthtml-include/-/posthtml-include-1.7.4.tgz#45e7abd18395ce5a6e8af9dc5c5390f85cf6171c"
+  integrity sha512-GO5QzHiM6/fXq8DxLoLN+jEW4sH/6nuGF9z+NJmP1qi1A3J2zCC7WwXrEwaPL3T8LrH+FL4IedK+mIJHbn5ZEA==
+  dependencies:
+    posthtml "^0.16.6"
+    posthtml-expressions "^1.7.1"
+    posthtml-parser "^0.11.0"
+
+posthtml-markdownit@^1.3.1:
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/posthtml-markdownit/-/posthtml-markdownit-1.3.1.tgz#7ba3d5aa92ebbe2e06002909b55faaf7c889f4d5"
+  integrity sha512-ohva3TR6zD+k+yRirsvC5tYvSNs6FyZ4rD5WdS2rXA7ZP8z5XXispGJPuGKFt4L7pS4eBmB5oROoggG/pwatuA==
+  dependencies:
+    markdown-it "^12.0.2"
+    min-indent "^1.0.0"
+    posthtml "^0.15.0"
+    posthtml-parser "^0.6.0"
+    posthtml-render "^1.4.0"
+
+posthtml-match-helper@^1.0.1:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/posthtml-match-helper/-/posthtml-match-helper-1.0.4.tgz#b8f384179732cb5d5e060b9dd1945a2352eb6a44"
+  integrity sha512-Tj9orTIBxHdnraCxoEGjoizsFsTGvukzwcuhOjYQGmDG6gTlaRbMrGgi1J+FwKTN8hsCQENHYY0Deqs9a89BVg==
+
+posthtml-parser@^0.10.0:
+  version "0.10.2"
+  resolved "https://registry.yarnpkg.com/posthtml-parser/-/posthtml-parser-0.10.2.tgz#df364d7b179f2a6bf0466b56be7b98fd4e97c573"
+  integrity sha512-PId6zZ/2lyJi9LiKfe+i2xv57oEjJgWbsHGGANwos5AvdQp98i6AtamAl8gzSVFGfQ43Glb5D614cvZf012VKg==
+  dependencies:
+    htmlparser2 "^7.1.1"
+
+posthtml-parser@^0.11.0:
+  version "0.11.0"
+  resolved "https://registry.yarnpkg.com/posthtml-parser/-/posthtml-parser-0.11.0.tgz#25d1c7bf811ea83559bc4c21c189a29747a24b7a"
+  integrity sha512-QecJtfLekJbWVo/dMAA+OSwY79wpRmbqS5TeXvXSX+f0c6pW4/SE6inzZ2qkU7oAMCPqIDkZDvd/bQsSFUnKyw==
+  dependencies:
+    htmlparser2 "^7.1.1"
+
+posthtml-parser@^0.12.1:
+  version "0.12.1"
+  resolved "https://registry.yarnpkg.com/posthtml-parser/-/posthtml-parser-0.12.1.tgz#f29cc2eec3e6dd0bb99ac169f49963515adbff21"
+  integrity sha512-rYFmsDLfYm+4Ts2Oh4DCDSZPtdC1BLnRXAobypVzX9alj28KGl65dIFtgDY9zB57D0TC4Qxqrawuq/2et1P0GA==
+  dependencies:
+    htmlparser2 "^9.0.0"
+
+posthtml-parser@^0.6.0:
+  version "0.6.0"
+  resolved "https://registry.yarnpkg.com/posthtml-parser/-/posthtml-parser-0.6.0.tgz#52488cdb4fa591c3102de73197c471859ee0be63"
+  integrity sha512-5ffwKQNgtVHdhZniWxu+1ryvaZv5l25HPLUV6W5xy5nYVWMXtvjtwRnbSpfbKFvbyl7XI+d4AqkjmonkREqnXA==
+  dependencies:
+    htmlparser2 "^5.0.1"
+
+posthtml-parser@^0.7.2:
+  version "0.7.2"
+  resolved "https://registry.yarnpkg.com/posthtml-parser/-/posthtml-parser-0.7.2.tgz#3fba3375544d824bb1c8504f0d69f6e0b95774db"
+  integrity sha512-LjEEG/3fNcWZtBfsOE3Gbyg1Li4CmsZRkH1UmbMR7nKdMXVMYI3B4/ZMiCpaq8aI1Aym4FRMMW9SAOLSwOnNsQ==
+  dependencies:
+    htmlparser2 "^6.0.0"
+
+posthtml-prism@^1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/posthtml-prism/-/posthtml-prism-1.0.4.tgz#303e82c0adae11642de9046b519760ca212e781b"
+  integrity sha512-f/hk5b72NeAtipHduosv+xgr1OvHv+Oj3VFQ3d9uf8nIqxpapDb2HkGP3Q67QvKIyZbztTWLr1W7fHxNb0qpQA==
+  dependencies:
+    prismjs "^1.19.0"
+
+posthtml-render@^1.3.1, posthtml-render@^1.4.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/posthtml-render/-/posthtml-render-1.4.0.tgz#40114070c45881cacb93347dae3eff53afbcff13"
+  integrity sha512-W1779iVHGfq0Fvh2PROhCe2QhB8mEErgqzo1wpIt36tCgChafP+hbXIhLDOM8ePJrZcFs0vkNEtdibEWVqChqw==
+
+posthtml-render@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/posthtml-render/-/posthtml-render-3.0.0.tgz#97be44931496f495b4f07b99e903cc70ad6a3205"
+  integrity sha512-z+16RoxK3fUPgwaIgH9NGnK1HKY9XIDpydky5eQGgAFVXTCSezalv9U2jQuNV+Z9qV1fDWNzldcw4eK0SSbqKA==
+  dependencies:
+    is-json "^2.0.1"
+
+posthtml@^0.15.0:
+  version "0.15.2"
+  resolved "https://registry.yarnpkg.com/posthtml/-/posthtml-0.15.2.tgz#739cf0d3ffec70868b87121dc7393478e1898c9c"
+  integrity sha512-YugEJ5ze/0DLRIVBjCpDwANWL4pPj1kHJ/2llY8xuInr0nbkon3qTiMPe5LQa+cCwNjxS7nAZZTp+1M+6mT4Zg==
+  dependencies:
+    posthtml-parser "^0.7.2"
+    posthtml-render "^1.3.1"
+
+posthtml@^0.16.4, posthtml@^0.16.5, posthtml@^0.16.6:
+  version "0.16.6"
+  resolved "https://registry.yarnpkg.com/posthtml/-/posthtml-0.16.6.tgz#e2fc407f67a64d2fa3567afe770409ffdadafe59"
+  integrity sha512-JcEmHlyLK/o0uGAlj65vgg+7LIms0xKXe60lcDOTU7oVX/3LuEuLwrQpW3VJ7de5TaFKiW4kWkaIpJL42FEgxQ==
+  dependencies:
+    posthtml-parser "^0.11.0"
+    posthtml-render "^3.0.0"
+
+prettier@^2.5.1, prettier@^2.6.2:
+  version "2.8.8"
+  resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da"
+  integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==
+
+pretty-format@^27.5.1:
+  version "27.5.1"
+  resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e"
+  integrity sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==
+  dependencies:
+    ansi-regex "^5.0.1"
+    ansi-styles "^5.0.0"
+    react-is "^17.0.1"
+
+prismjs@1.29.0, prismjs@^1.19.0:
+  version "1.29.0"
+  resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.29.0.tgz#f113555a8fa9b57c35e637bba27509dcf802dd12"
+  integrity sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==
+
+process@^0.11.10:
+  version "0.11.10"
+  resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
+  integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==
+
+progress@2.0.3:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
+  integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
+
+proxy-from-env@1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
+  integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
+
+pump@^3.0.0:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.2.tgz#836f3edd6bc2ee599256c924ffe0d88573ddcbf8"
+  integrity sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==
+  dependencies:
+    end-of-stream "^1.1.0"
+    once "^1.3.1"
+
+puppeteer@^12.0.1:
+  version "12.0.1"
+  resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-12.0.1.tgz#ae79d0e174a07563e0bf2e05c94ccafce3e70033"
+  integrity sha512-YQ3GRiyZW0ddxTW+iiQcv2/8TT5c3+FcRUCg7F8q2gHqxd5akZN400VRXr9cHQKLWGukmJLDiE72MrcLK9tFHQ==
+  dependencies:
+    debug "4.3.2"
+    devtools-protocol "0.0.937139"
+    extract-zip "2.0.1"
+    https-proxy-agent "5.0.0"
+    node-fetch "2.6.5"
+    pkg-dir "4.2.0"
+    progress "2.0.3"
+    proxy-from-env "1.1.0"
+    rimraf "3.0.2"
+    tar-fs "2.1.1"
+    unbzip2-stream "1.4.3"
+    ws "8.2.3"
+
+react-error-overlay@6.0.9:
+  version "6.0.9"
+  resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.9.tgz#3c743010c9359608c375ecd6bc76f35d93995b0a"
+  integrity sha512-nQTTcUu+ATDbrSD1BZHr5kgSD4oF8OFjxun8uAaL8RwPBacGBNPf/yAuVVdx17N8XNzRDMrZ9XcKZHCjPW+9ew==
+
+react-is@^17.0.1:
+  version "17.0.2"
+  resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
+  integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
+
+"react-refresh@>=0.9 <=0.14":
+  version "0.14.2"
+  resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.14.2.tgz#3833da01ce32da470f1f936b9d477da5c7028bf9"
+  integrity sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==
+
+readable-stream@^3.1.1, readable-stream@^3.4.0:
+  version "3.6.2"
+  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967"
+  integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==
+  dependencies:
+    inherits "^2.0.3"
+    string_decoder "^1.1.1"
+    util-deprecate "^1.0.1"
+
+recast@^0.22.0:
+  version "0.22.0"
+  resolved "https://registry.yarnpkg.com/recast/-/recast-0.22.0.tgz#1dd3bf1b86e5eb810b044221a1a734234ed3e9c0"
+  integrity sha512-5AAx+mujtXijsEavc5lWXBPQqrM4+Dl5qNH96N2aNeuJFUzpiiToKPsxQD/zAIJHspz7zz0maX0PCtCTFVlixQ==
+  dependencies:
+    assert "^2.0.0"
+    ast-types "0.15.2"
+    esprima "~4.0.0"
+    source-map "~0.6.1"
+    tslib "^2.0.1"
+
+rechoir@^0.6.2:
+  version "0.6.2"
+  resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384"
+  integrity sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==
+  dependencies:
+    resolve "^1.1.6"
+
+regenerator-runtime@^0.14.1:
+  version "0.14.1"
+  resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f"
+  integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==
+
+resolve-from@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
+  integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==
+
+resolve@^1.1.6:
+  version "1.22.10"
+  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.10.tgz#b663e83ffb09bbf2386944736baae803029b8b39"
+  integrity sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==
+  dependencies:
+    is-core-module "^2.16.0"
+    path-parse "^1.0.7"
+    supports-preserve-symlinks-flag "^1.0.0"
+
+rimraf@3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
+  integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
+  dependencies:
+    glob "^7.1.3"
+
+rimraf@^2.6.3:
+  version "2.7.1"
+  resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
+  integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==
+  dependencies:
+    glob "^7.1.3"
+
+sade@^1.7.3:
+  version "1.8.1"
+  resolved "https://registry.yarnpkg.com/sade/-/sade-1.8.1.tgz#0a78e81d658d394887be57d2a409bf703a3b2701"
+  integrity sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==
+  dependencies:
+    mri "^1.1.0"
+
+safe-buffer@^5.0.1, safe-buffer@~5.2.0:
+  version "5.2.1"
+  resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
+  integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
+
+semver@^5.5.0, semver@^5.6.0:
+  version "5.7.2"
+  resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8"
+  integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==
+
+semver@^7.5.2, semver@^7.6.3:
+  version "7.6.3"
+  resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143"
+  integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==
+
+set-function-length@^1.2.2:
+  version "1.2.2"
+  resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449"
+  integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==
+  dependencies:
+    define-data-property "^1.1.4"
+    es-errors "^1.3.0"
+    function-bind "^1.1.2"
+    get-intrinsic "^1.2.4"
+    gopd "^1.0.1"
+    has-property-descriptors "^1.0.2"
+
+sharp@^0.33.5:
+  version "0.33.5"
+  resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.33.5.tgz#13e0e4130cc309d6a9497596715240b2ec0c594e"
+  integrity sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==
+  dependencies:
+    color "^4.2.3"
+    detect-libc "^2.0.3"
+    semver "^7.6.3"
+  optionalDependencies:
+    "@img/sharp-darwin-arm64" "0.33.5"
+    "@img/sharp-darwin-x64" "0.33.5"
+    "@img/sharp-libvips-darwin-arm64" "1.0.4"
+    "@img/sharp-libvips-darwin-x64" "1.0.4"
+    "@img/sharp-libvips-linux-arm" "1.0.5"
+    "@img/sharp-libvips-linux-arm64" "1.0.4"
+    "@img/sharp-libvips-linux-s390x" "1.0.4"
+    "@img/sharp-libvips-linux-x64" "1.0.4"
+    "@img/sharp-libvips-linuxmusl-arm64" "1.0.4"
+    "@img/sharp-libvips-linuxmusl-x64" "1.0.4"
+    "@img/sharp-linux-arm" "0.33.5"
+    "@img/sharp-linux-arm64" "0.33.5"
+    "@img/sharp-linux-s390x" "0.33.5"
+    "@img/sharp-linux-x64" "0.33.5"
+    "@img/sharp-linuxmusl-arm64" "0.33.5"
+    "@img/sharp-linuxmusl-x64" "0.33.5"
+    "@img/sharp-wasm32" "0.33.5"
+    "@img/sharp-win32-ia32" "0.33.5"
+    "@img/sharp-win32-x64" "0.33.5"
+
+shebang-command@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
+  integrity sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==
+  dependencies:
+    shebang-regex "^1.0.0"
+
+shebang-regex@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
+  integrity sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==
+
+shelljs@^0.8.4:
+  version "0.8.5"
+  resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.5.tgz#de055408d8361bed66c669d2f000538ced8ee20c"
+  integrity sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==
+  dependencies:
+    glob "^7.0.0"
+    interpret "^1.0.0"
+    rechoir "^0.6.2"
+
+simple-swizzle@^0.2.2:
+  version "0.2.2"
+  resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a"
+  integrity sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==
+  dependencies:
+    is-arrayish "^0.3.1"
+
+slash@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44"
+  integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==
+
+source-map-js@^1.0.1, source-map-js@^1.2.1:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
+  integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
+
+source-map@~0.6.1:
+  version "0.6.1"
+  resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
+  integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
+
+srcset@4:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/srcset/-/srcset-4.0.0.tgz#336816b665b14cd013ba545b6fe62357f86e65f4"
+  integrity sha512-wvLeHgcVHKO8Sc/H/5lkGreJQVeYMm9rlmt8PuR1xE31rIuXhuzznUUqAt8MqLhB3MqJdFzlNAfpcWnxiFUcPw==
+
+string_decoder@^1.1.1:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
+  integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
+  dependencies:
+    safe-buffer "~5.2.0"
+
+style-mod@^4.0.0, style-mod@^4.1.0:
+  version "4.1.2"
+  resolved "https://registry.yarnpkg.com/style-mod/-/style-mod-4.1.2.tgz#ca238a1ad4786520f7515a8539d5a63691d7bf67"
+  integrity sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==
+
+stylehacks@^7.0.4:
+  version "7.0.4"
+  resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-7.0.4.tgz#9c21f7374f4bccc0082412b859b3c89d77d3277c"
+  integrity sha512-i4zfNrGMt9SB4xRK9L83rlsFCgdGANfeDAYacO1pkqcE7cRHPdWHwnKZVz7WY17Veq/FvyYsRAU++Ga+qDFIww==
+  dependencies:
+    browserslist "^4.23.3"
+    postcss-selector-parser "^6.1.2"
+
+supports-color@^5.3.0:
+  version "5.5.0"
+  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
+  integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==
+  dependencies:
+    has-flag "^3.0.0"
+
+supports-color@^7.1.0:
+  version "7.2.0"
+  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
+  integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==
+  dependencies:
+    has-flag "^4.0.0"
+
+supports-preserve-symlinks-flag@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
+  integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
+
+svgo@^3.3.2:
+  version "3.3.2"
+  resolved "https://registry.yarnpkg.com/svgo/-/svgo-3.3.2.tgz#ad58002652dffbb5986fc9716afe52d869ecbda8"
+  integrity sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==
+  dependencies:
+    "@trysound/sax" "0.2.0"
+    commander "^7.2.0"
+    css-select "^5.1.0"
+    css-tree "^2.3.1"
+    css-what "^6.1.0"
+    csso "^5.0.5"
+    picocolors "^1.0.0"
+
+tar-fs@2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784"
+  integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==
+  dependencies:
+    chownr "^1.1.1"
+    mkdirp-classic "^0.5.2"
+    pump "^3.0.0"
+    tar-stream "^2.1.4"
+
+tar-stream@^2.1.4:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287"
+  integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==
+  dependencies:
+    bl "^4.0.3"
+    end-of-stream "^1.4.1"
+    fs-constants "^1.0.0"
+    inherits "^2.0.3"
+    readable-stream "^3.1.1"
+
+term-size@^2.2.1:
+  version "2.2.1"
+  resolved "https://registry.yarnpkg.com/term-size/-/term-size-2.2.1.tgz#2a6a54840432c2fb6320fea0f415531e90189f54"
+  integrity sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==
+
+thenify-all@^1.0.0:
+  version "1.6.0"
+  resolved "https://registry.yarnpkg.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726"
+  integrity sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==
+  dependencies:
+    thenify ">= 3.1.0 < 4"
+
+"thenify@>= 3.1.0 < 4":
+  version "3.3.1"
+  resolved "https://registry.yarnpkg.com/thenify/-/thenify-3.3.1.tgz#8932e686a4066038a016dd9e2ca46add9838a95f"
+  integrity sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==
+  dependencies:
+    any-promise "^1.0.0"
+
+through@^2.3.8:
+  version "2.3.8"
+  resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
+  integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==
+
+timers-ext@^0.1.7:
+  version "0.1.8"
+  resolved "https://registry.yarnpkg.com/timers-ext/-/timers-ext-0.1.8.tgz#b4e442f10b7624a29dd2aa42c295e257150cf16c"
+  integrity sha512-wFH7+SEAcKfJpfLPkrgMPvvwnEtj8W4IurvEyrKsDleXnKLCDw71w8jltvfLa8Rm4qQxxT4jmDBYbJG/z7qoww==
+  dependencies:
+    es5-ext "^0.10.64"
+    next-tick "^1.1.0"
+
+timsort@^0.3.0:
+  version "0.3.0"
+  resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
+  integrity sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A==
+
+tmp@^0.0.33:
+  version "0.0.33"
+  resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
+  integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==
+  dependencies:
+    os-tmpdir "~1.0.2"
+
+to-regex-range@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
+  integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
+  dependencies:
+    is-number "^7.0.0"
+
+tr46@~0.0.3:
+  version "0.0.3"
+  resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
+  integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==
+
+tslib@^2.0.1, tslib@^2.4.0, tslib@^2.8.0:
+  version "2.8.1"
+  resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
+  integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
+
+type-fest@^0.20.2:
+  version "0.20.2"
+  resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4"
+  integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==
+
+type@^2.7.2:
+  version "2.7.3"
+  resolved "https://registry.yarnpkg.com/type/-/type-2.7.3.tgz#436981652129285cc3ba94f392886c2637ea0486"
+  integrity sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==
+
+typescript-compiler@^1.4.1-2:
+  version "1.4.1-2"
+  resolved "https://registry.yarnpkg.com/typescript-compiler/-/typescript-compiler-1.4.1-2.tgz#ba4f7db22d91534a1929d90009dce161eb72fd3f"
+  integrity sha512-EMopKmoAEJqA4XXRFGOb7eSBhmQMbBahW6P1Koayeatp0b4AW2q/bBqYWkpG7QVQc9HGQUiS4trx2ZHcnAaZUg==
+
+typescript@^5.7.2:
+  version "5.7.2"
+  resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.7.2.tgz#3169cf8c4c8a828cde53ba9ecb3d2b1d5dd67be6"
+  integrity sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==
+
+typescript@~4.4.4:
+  version "4.4.4"
+  resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.4.4.tgz#2cd01a1a1f160704d3101fd5a58ff0f9fcb8030c"
+  integrity sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==
+
+uc.micro@^1.0.1, uc.micro@^1.0.5:
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac"
+  integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==
+
+unbzip2-stream@1.4.3:
+  version "1.4.3"
+  resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz#b0da04c4371311df771cdc215e87f2130991ace7"
+  integrity sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==
+  dependencies:
+    buffer "^5.2.1"
+    through "^2.3.8"
+
+undici-types@~6.20.0:
+  version "6.20.0"
+  resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.20.0.tgz#8171bf22c1f588d1554d55bf204bc624af388433"
+  integrity sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==
+
+universalify@^2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d"
+  integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==
+
+update-browserslist-db@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz#80846fba1d79e82547fb661f8d141e0945755fe5"
+  integrity sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==
+  dependencies:
+    escalade "^3.2.0"
+    picocolors "^1.1.0"
+
+util-deprecate@^1.0.1, util-deprecate@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
+  integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
+
+util@^0.12.4, util@^0.12.5:
+  version "0.12.5"
+  resolved "https://registry.yarnpkg.com/util/-/util-0.12.5.tgz#5f17a6059b73db61a875668781a1c2b136bd6fbc"
+  integrity sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==
+  dependencies:
+    inherits "^2.0.3"
+    is-arguments "^1.0.4"
+    is-generator-function "^1.0.7"
+    is-typed-array "^1.1.3"
+    which-typed-array "^1.1.2"
+
+utility-types@^3.10.0:
+  version "3.11.0"
+  resolved "https://registry.yarnpkg.com/utility-types/-/utility-types-3.11.0.tgz#607c40edb4f258915e901ea7995607fdf319424c"
+  integrity sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==
+
+uvu@^0.5.6:
+  version "0.5.6"
+  resolved "https://registry.yarnpkg.com/uvu/-/uvu-0.5.6.tgz#2754ca20bcb0bb59b64e9985e84d2e81058502df"
+  integrity sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==
+  dependencies:
+    dequal "^2.0.0"
+    diff "^5.0.0"
+    kleur "^4.0.3"
+    sade "^1.7.3"
+
+w3c-keyname@^2.2.4:
+  version "2.2.8"
+  resolved "https://registry.yarnpkg.com/w3c-keyname/-/w3c-keyname-2.2.8.tgz#7b17c8c6883d4e8b86ac8aba79d39e880f8869c5"
+  integrity sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==
+
+weak-lru-cache@^1.2.2:
+  version "1.2.2"
+  resolved "https://registry.yarnpkg.com/weak-lru-cache/-/weak-lru-cache-1.2.2.tgz#fdbb6741f36bae9540d12f480ce8254060dccd19"
+  integrity sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw==
+
+web-streams-polyfill@^3.0.3:
+  version "3.3.3"
+  resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz#2073b91a2fdb1fbfbd401e7de0ac9f8214cecb4b"
+  integrity sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==
+
+webidl-conversions@^3.0.0:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
+  integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==
+
+whatwg-url@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"
+  integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==
+  dependencies:
+    tr46 "~0.0.3"
+    webidl-conversions "^3.0.0"
+
+which-typed-array@^1.1.16, which-typed-array@^1.1.2:
+  version "1.1.18"
+  resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.18.tgz#df2389ebf3fbb246a71390e90730a9edb6ce17ad"
+  integrity sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA==
+  dependencies:
+    available-typed-arrays "^1.0.7"
+    call-bind "^1.0.8"
+    call-bound "^1.0.3"
+    for-each "^0.3.3"
+    gopd "^1.2.0"
+    has-tostringtag "^1.0.2"
+
+which@^1.2.9:
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
+  integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
+  dependencies:
+    isexe "^2.0.0"
+
+wrappy@1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
+  integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
+
+ws@8.2.3:
+  version "8.2.3"
+  resolved "https://registry.yarnpkg.com/ws/-/ws-8.2.3.tgz#63a56456db1b04367d0b721a0b80cae6d8becbba"
+  integrity sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==
+
+yaml@^1.10.2:
+  version "1.10.2"
+  resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
+  integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
+
+yauzl@^2.10.0:
+  version "2.10.0"
+  resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9"
+  integrity sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==
+  dependencies:
+    buffer-crc32 "~0.2.3"
+    fd-slicer "~1.1.0"