Bug 1462233 - Implement the env() function with hardcoded zeros for safe-area-inset. r=heycam,firefox-style-system-reviewers
authorEmilio Cobos Álvarez <emilio@crisal.io>
Mon, 05 Nov 2018 10:39:46 +0000
changeset 444397 15da260eaa3cb3e9e3951114de8f4c82a8435275
parent 444396 538a16d495142178a73e0bdc30f100b43d2fd62b
child 444398 7945ede7e1b84939c21c78b615810160c8ffd46b
push id109582
push usercsabou@mozilla.com
push dateMon, 05 Nov 2018 16:21:20 +0000
treeherdermozilla-inbound@4115e3cf7ad0 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersheycam, firefox-style-system-reviewers
bugs1462233
milestone65.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1462233 - Implement the env() function with hardcoded zeros for safe-area-inset. r=heycam,firefox-style-system-reviewers Intent to Implement and Ship: https://groups.google.com/d/msg/mozilla.dev.platform/EVKyR1B87T0/_l-_qK8SAAAJ Differential Revision: https://phabricator.services.mozilla.com/D9609
Cargo.lock
servo/components/malloc_size_of/Cargo.toml
servo/components/selectors/Cargo.toml
servo/components/style/Cargo.toml
servo/components/style/custom_properties.rs
servo/components/style/gecko/media_queries.rs
servo/components/style/properties/cascade.rs
servo/components/style/properties/declaration_block.rs
servo/components/style/properties/helpers/animated_properties.mako.rs
servo/components/style/properties/properties.mako.rs
servo/components/style_traits/Cargo.toml
servo/ports/geckolib/Cargo.toml
servo/ports/geckolib/tests/Cargo.toml
servo/tests/unit/style/Cargo.toml
testing/web-platform/meta/css/css-env/at-supports.tentative.html.ini
testing/web-platform/meta/css/css-env/env-in-custom-properties.tentative.html.ini
testing/web-platform/meta/css/css-env/fallback-nested-var.tentative.html.ini
testing/web-platform/meta/css/css-env/seralization-round-tripping.tentative.html.ini
testing/web-platform/meta/css/css-env/supports-script.tentative.html.ini
testing/web-platform/meta/css/css-env/syntax.tentative.html.ini
testing/web-platform/meta/css/css-env/unknown-env-names-override-previous.tentative.html.ini
third_party/rust/cssparser/.cargo-checksum.json
third_party/rust/cssparser/Cargo.toml
third_party/rust/cssparser/build.rs
third_party/rust/cssparser/src/parser.rs
third_party/rust/cssparser/src/tests.rs
third_party/rust/cssparser/src/tokenizer.rs
xpcom/ds/StaticAtoms.py
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -576,17 +576,17 @@ name = "crossbeam-utils"
 version = "0.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "cfg-if 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "cssparser"
-version = "0.24.1"
+version = "0.25.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "cssparser-macros 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
  "dtoa-short 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "itoa 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "matches 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
  "phf 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)",
  "proc-macro2 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -979,17 +979,17 @@ dependencies = [
  "zip 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "geckoservo"
 version = "0.0.1"
 dependencies = [
  "atomic_refcell 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "cssparser 0.24.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "cssparser 0.25.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "cstr 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
  "libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)",
  "log 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)",
  "malloc_size_of 0.0.1",
  "nsstring 0.1.0",
  "num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)",
  "parking_lot 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)",
  "selectors 0.20.0",
@@ -1407,17 +1407,17 @@ dependencies = [
  "cfg-if 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "malloc_size_of"
 version = "0.0.1"
 dependencies = [
  "app_units 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "cssparser 0.24.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "cssparser 0.25.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "euclid 0.19.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "hashglobe 0.1.0",
  "selectors 0.20.0",
  "servo_arc 0.1.1",
  "smallbitvec 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "smallvec 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)",
  "thin-slice 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -2179,17 +2179,17 @@ dependencies = [
  "syn 0.15.7 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "selectors"
 version = "0.20.0"
 dependencies = [
  "bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "cssparser 0.24.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "cssparser 0.25.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "fxhash 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "log 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)",
  "matches 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
  "phf 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)",
  "phf_codegen 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)",
  "precomputed-hash 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "servo_arc 0.1.1",
  "smallvec 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -2357,17 +2357,17 @@ version = "0.0.1"
 dependencies = [
  "app_units 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "arrayvec 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
  "atomic_refcell 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "bindgen 0.43.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
  "byteorder 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "cfg-if 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "cssparser 0.24.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "cssparser 0.25.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "euclid 0.19.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "fallible 0.0.1",
  "fxhash 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "hashglobe 0.1.0",
  "itertools 0.7.6 (registry+https://github.com/rust-lang/crates.io-index)",
  "itoa 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "lazy_static 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "log 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -2413,30 +2413,30 @@ dependencies = [
 ]
 
 [[package]]
 name = "style_traits"
 version = "0.0.1"
 dependencies = [
  "app_units 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "cssparser 0.24.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "cssparser 0.25.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "euclid 0.19.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "malloc_size_of 0.0.1",
  "malloc_size_of_derive 0.0.1",
  "selectors 0.20.0",
  "servo_arc 0.1.1",
 ]
 
 [[package]]
 name = "stylo_tests"
 version = "0.0.1"
 dependencies = [
  "atomic_refcell 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "cssparser 0.24.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "cssparser 0.25.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "cstr 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
  "env_logger 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)",
  "geckoservo 0.0.1",
  "libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)",
  "log 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)",
  "malloc_size_of 0.0.1",
  "num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)",
  "regex 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -3170,17 +3170,17 @@ dependencies = [
 "checksum cranelift-wasm 0.20.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2a9d3454bf60ee6c3d1f54d6cf9ed82cfc1a2e7efb9ec1b16666bf2987c88bfa"
 "checksum crc 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bd5d02c0aac6bd68393ed69e00bbc2457f3e89075c6349db7189618dc4ddc1d7"
 "checksum crossbeam-deque 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f739f8c5363aca78cfb059edf753d8f0d36908c348f3d8d1503f03d8b75d9cf3"
 "checksum crossbeam-deque 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "fe8153ef04a7594ded05b427ffad46ddeaf22e63fd48d42b3e1e3bb4db07cae7"
 "checksum crossbeam-epoch 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "927121f5407de9956180ff5e936fe3cf4324279280001cd56b669d28ee7e9150"
 "checksum crossbeam-epoch 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2af0e75710d6181e234c8ecc79f14a97907850a541b13b0be1dd10992f2e4620"
 "checksum crossbeam-utils 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "2760899e32a1d58d5abb31129f8fae5de75220bc2176e77ff7c627ae45c918d9"
 "checksum crossbeam-utils 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d636a8b3bcc1b409d7ffd3facef8f21dcb4009626adbd0c5e6c4305c07253c7b"
-"checksum cssparser 0.24.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b200a7193703a615c8d2751fed1ede39b9c4b3905e09d1ec7064a24688c190fc"
+"checksum cssparser 0.25.0 (registry+https://github.com/rust-lang/crates.io-index)" = "730363a45c4e248d4f21d3e5c1156d1a9cdec0855056c0d9539e814bc59865c3"
 "checksum cssparser-macros 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "f3a5383ae18dbfdeb569ed62019f5bddb2a95cd2d3833313c475a0d014777805"
 "checksum cstr 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "b6557bdb1dc9647eae1cf7f5601b14cd45fc3c7ccf2df618387416fe542da6ea"
 "checksum cstr-macros 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "0472c17c83d3ec1af32fb6ee2b3ad56ae0b6e69355d63d1d30602055c34324a8"
 "checksum cubeb 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "8a3502aafa1bf95c524f65d2ba46d8741700c6a8a9543ea52c6da3d8b69a2896"
 "checksum cubeb-backend 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fdcac95519416d9ec814db2dc40e6293e7da25b906023d93f48b87f0587ab138"
 "checksum cubeb-core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "37f7b20f757a4e4b6aa28863236551bff77682dc6db192eba15af615492b5445"
 "checksum cubeb-sys 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "653b9e245d35dbe2a2da7c4586275cee75ff656ddeb02d4a73b4afdfa6d67502"
 "checksum darling 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2a78af487e4eb8f4421a1770687b328af6bb4494ca93435210678c6eea875c11"
--- a/servo/components/malloc_size_of/Cargo.toml
+++ b/servo/components/malloc_size_of/Cargo.toml
@@ -21,17 +21,17 @@ servo = [
     "time",
     "url",
     "webrender_api",
     "xml5ever",
 ]
 
 [dependencies]
 app_units = "0.7"
-cssparser = "0.24.0"
+cssparser = "0.25"
 euclid = "0.19"
 hashglobe = { path = "../hashglobe" }
 hyper = { version = "0.10", optional = true }
 hyper_serde = { version = "0.8", optional = true }
 keyboard-types = {version = "0.4.2-servo", features = ["serde"], optional = true}
 mozjs = { version = "0.9.0", optional = true }
 selectors = { path = "../selectors" }
 serde = { version = "1.0.27", optional = true }
--- a/servo/components/selectors/Cargo.toml
+++ b/servo/components/selectors/Cargo.toml
@@ -17,17 +17,17 @@ name = "selectors"
 path = "lib.rs"
 
 [features]
 bench = []
 
 [dependencies]
 bitflags = "1.0"
 matches = "0.1"
-cssparser = "0.24.0"
+cssparser = "0.25"
 log = "0.4"
 fxhash = "0.2"
 phf = "0.7.18"
 precomputed-hash = "0.1"
 servo_arc = { version = "0.1", path = "../servo_arc" }
 smallvec = "0.6"
 thin-slice = "0.1.0"
 
--- a/servo/components/style/Cargo.toml
+++ b/servo/components/style/Cargo.toml
@@ -26,17 +26,17 @@ gecko_debug = []
 
 [dependencies]
 app_units = "0.7"
 arrayvec = "0.4.6"
 atomic_refcell = "0.1"
 bitflags = "1.0"
 byteorder = "1.0"
 cfg-if = "0.1.0"
-cssparser = "0.24.0"
+cssparser = "0.25"
 new_debug_unreachable = "1.0"
 encoding_rs = {version = "0.7", optional = true}
 euclid = "0.19"
 fallible = { path = "../fallible" }
 fxhash = "0.2"
 hashglobe = { path = "../hashglobe" }
 html5ever = {version = "0.22", optional = true}
 itertools = "0.7.6"
--- a/servo/components/style/custom_properties.rs
+++ b/servo/components/style/custom_properties.rs
@@ -16,16 +16,69 @@ use selectors::parser::SelectorParseErro
 use servo_arc::Arc;
 use smallvec::SmallVec;
 use std::borrow::{Borrow, Cow};
 use std::cmp;
 use std::fmt::{self, Write};
 use std::hash::Hash;
 use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss};
 
+/// The environment from which to get `env` function values.
+///
+/// TODO(emilio): If this becomes a bit more complex we should probably move it
+/// to the `media_queries` module, or something.
+pub struct CssEnvironment;
+
+struct EnvironmentVariable {
+    name: Atom,
+    value: VariableValue,
+}
+
+macro_rules! make_variable {
+    ($name:expr, $value:expr) => {{
+        EnvironmentVariable {
+            name: $name,
+            value: {
+                // TODO(emilio): We could make this be more efficient (though a
+                // bit less convenient).
+                let mut input = ParserInput::new($value);
+                let mut input = Parser::new(&mut input);
+
+                let (first_token_type, css, last_token_type) =
+                    parse_self_contained_declaration_value(&mut input, None).unwrap();
+
+                VariableValue {
+                    css: css.into_owned(),
+                    first_token_type,
+                    last_token_type,
+                    references: Default::default(),
+                    references_environment: false,
+                }
+            },
+        }
+    }};
+}
+
+lazy_static! {
+    static ref ENVIRONMENT_VARIABLES: [EnvironmentVariable; 4] = [
+        make_variable!(atom!("safe-area-inset-top"), "0px"),
+        make_variable!(atom!("safe-area-inset-bottom"), "0px"),
+        make_variable!(atom!("safe-area-inset-left"), "0px"),
+        make_variable!(atom!("safe-area-inset-right"), "0px"),
+    ];
+}
+
+impl CssEnvironment {
+    #[inline]
+    fn get(&self, name: &Atom) -> Option<&VariableValue> {
+        let var = ENVIRONMENT_VARIABLES.iter().find(|var| var.name == *name)?;
+        Some(&var.value)
+    }
+}
+
 /// A custom property name is just an `Atom`.
 ///
 /// Note that this does not include the `--` prefix
 pub type Name = Atom;
 
 /// Parse a custom property name.
 ///
 /// <https://drafts.csswg.org/css-variables/#typedef-custom-property-name>
@@ -43,16 +96,22 @@ pub fn parse_name(s: &str) -> Result<&st
 /// references to other custom property names.
 #[derive(Clone, Debug, MallocSizeOf, PartialEq)]
 pub struct VariableValue {
     css: String,
 
     first_token_type: TokenSerializationType,
     last_token_type: TokenSerializationType,
 
+    /// Whether a variable value has a reference to an environment variable.
+    ///
+    /// If this is the case, we need to perform variable substitution on the
+    /// value.
+    references_environment: bool,
+
     /// Custom property names in var() functions.
     references: PrecomputedHashSet<Name>,
 }
 
 impl ToCss for SpecifiedValue {
     fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
     where
         W: Write,
@@ -211,23 +270,32 @@ where
 
         self.pos += 1;
         let value = &self.inner.values[key];
 
         Some((key, value))
     }
 }
 
+/// A struct holding information about the external references to that a custom
+/// property value may have.
+#[derive(Default)]
+struct VarOrEnvReferences {
+    custom_property_references: PrecomputedHashSet<Name>,
+    references_environment: bool,
+}
+
 impl VariableValue {
     fn empty() -> Self {
         Self {
             css: String::new(),
             last_token_type: TokenSerializationType::nothing(),
             first_token_type: TokenSerializationType::nothing(),
             references: PrecomputedHashSet::default(),
+            references_environment: false,
         }
     }
 
     fn push(
         &mut self,
         css: &str,
         css_first_token_type: TokenSerializationType,
         css_last_token_type: TokenSerializationType,
@@ -268,41 +336,42 @@ impl VariableValue {
             &variable.css,
             variable.first_token_type,
             variable.last_token_type,
         )
     }
 
     /// Parse a custom property value.
     pub fn parse<'i, 't>(input: &mut Parser<'i, 't>) -> Result<Arc<Self>, ParseError<'i>> {
-        let mut references = PrecomputedHashSet::default();
+        let mut references = VarOrEnvReferences::default();
 
         let (first_token_type, css, last_token_type) =
             parse_self_contained_declaration_value(input, Some(&mut references))?;
 
         Ok(Arc::new(VariableValue {
             css: css.into_owned(),
             first_token_type,
             last_token_type,
-            references,
+            references: references.custom_property_references,
+            references_environment: references.references_environment,
         }))
     }
 }
 
 /// Parse the value of a non-custom property that contains `var()` references.
 pub fn parse_non_custom_with_var<'i, 't>(
     input: &mut Parser<'i, 't>,
 ) -> Result<(TokenSerializationType, Cow<'i, str>), ParseError<'i>> {
     let (first_token_type, css, _) = parse_self_contained_declaration_value(input, None)?;
     Ok((first_token_type, css))
 }
 
 fn parse_self_contained_declaration_value<'i, 't>(
     input: &mut Parser<'i, 't>,
-    references: Option<&mut PrecomputedHashSet<Name>>,
+    references: Option<&mut VarOrEnvReferences>,
 ) -> Result<(TokenSerializationType, Cow<'i, str>, TokenSerializationType), ParseError<'i>> {
     let start_position = input.position();
     let mut missing_closing_characters = String::new();
     let (first, last) =
         parse_declaration_value(input, references, &mut missing_closing_characters)?;
     let mut css: Cow<str> = input.slice_from(start_position).into();
     if !missing_closing_characters.is_empty() {
         // Unescaped backslash at EOF in a quoted string is ignored.
@@ -312,34 +381,34 @@ fn parse_self_contained_declaration_valu
         css.to_mut().push_str(&missing_closing_characters);
     }
     Ok((first, css, last))
 }
 
 /// <https://drafts.csswg.org/css-syntax-3/#typedef-declaration-value>
 fn parse_declaration_value<'i, 't>(
     input: &mut Parser<'i, 't>,
-    references: Option<&mut PrecomputedHashSet<Name>>,
+    references: Option<&mut VarOrEnvReferences>,
     missing_closing_characters: &mut String,
 ) -> Result<(TokenSerializationType, TokenSerializationType), ParseError<'i>> {
     input.parse_until_before(Delimiter::Bang | Delimiter::Semicolon, |input| {
         // Need at least one token
         let start = input.state();
         input.next_including_whitespace()?;
         input.reset(&start);
 
         parse_declaration_value_block(input, references, missing_closing_characters)
     })
 }
 
 /// Like parse_declaration_value, but accept `!` and `;` since they are only
 /// invalid at the top level
 fn parse_declaration_value_block<'i, 't>(
     input: &mut Parser<'i, 't>,
-    mut references: Option<&mut PrecomputedHashSet<Name>>,
+    mut references: Option<&mut VarOrEnvReferences>,
     missing_closing_characters: &mut String,
 ) -> Result<(TokenSerializationType, TokenSerializationType), ParseError<'i>> {
     let mut token_start = input.position();
     let mut token = match input.next_including_whitespace_and_comments() {
         // FIXME: remove clone() when borrows are non-lexical
         Ok(token) => token.clone(),
         Err(_) => {
             return Ok((
@@ -402,16 +471,22 @@ fn parse_declaration_value_block<'i, 't>
             },
             Token::Function(ref name) => {
                 if name.eq_ignore_ascii_case("var") {
                     let args_start = input.state();
                     input.parse_nested_block(|input| {
                         parse_var_function(input, references.as_mut().map(|r| &mut **r))
                     })?;
                     input.reset(&args_start);
+                } else if name.eq_ignore_ascii_case("env") {
+                    let args_start = input.state();
+                    input.parse_nested_block(|input| {
+                        parse_env_function(input, references.as_mut().map(|r| &mut **r))
+                    })?;
+                    input.reset(&args_start);
                 }
                 nested!();
                 check_closed!(")");
                 Token::CloseParenthesis.serialization_type()
             },
             Token::ParenthesisBlock => {
                 nested!();
                 check_closed!(")");
@@ -463,69 +538,89 @@ fn parse_declaration_value_block<'i, 't>
         token = match input.next_including_whitespace_and_comments() {
             // FIXME: remove clone() when borrows are non-lexical
             Ok(token) => token.clone(),
             Err(..) => return Ok((first_token_type, last_token_type)),
         };
     }
 }
 
+fn parse_fallback<'i, 't>(input: &mut Parser<'i, 't>) -> Result<(), ParseError<'i>> {
+    // Exclude `!` and `;` at the top level
+    // https://drafts.csswg.org/css-syntax/#typedef-declaration-value
+    input.parse_until_before(Delimiter::Bang | Delimiter::Semicolon, |input| {
+        // At least one non-comment token.
+        input.next_including_whitespace()?;
+        // Skip until the end.
+        while let Ok(_) = input.next_including_whitespace_and_comments() {}
+        Ok(())
+    })
+}
+
 // If the var function is valid, return Ok((custom_property_name, fallback))
 fn parse_var_function<'i, 't>(
     input: &mut Parser<'i, 't>,
-    references: Option<&mut PrecomputedHashSet<Name>>,
+    references: Option<&mut VarOrEnvReferences>,
 ) -> Result<(), ParseError<'i>> {
     let name = input.expect_ident_cloned()?;
-    let name: Result<_, ParseError> = parse_name(&name).map_err(|()| {
+    let name = parse_name(&name).map_err(|()| {
         input.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name.clone()))
-    });
-    let name = name?;
+    })?;
     if input.try(|input| input.expect_comma()).is_ok() {
-        // Exclude `!` and `;` at the top level
-        // https://drafts.csswg.org/css-syntax/#typedef-declaration-value
-        input.parse_until_before(Delimiter::Bang | Delimiter::Semicolon, |input| {
-            // At least one non-comment token.
-            input.next_including_whitespace()?;
-            // Skip until the end.
-            while let Ok(_) = input.next_including_whitespace_and_comments() {}
-            Ok(())
-        })?;
+        parse_fallback(input)?;
     }
     if let Some(refs) = references {
-        refs.insert(Atom::from(name));
+        refs.custom_property_references.insert(Atom::from(name));
+    }
+    Ok(())
+}
+
+fn parse_env_function<'i, 't>(
+    input: &mut Parser<'i, 't>,
+    references: Option<&mut VarOrEnvReferences>,
+) -> Result<(), ParseError<'i>> {
+    // TODO(emilio): This should be <custom-ident> per spec, but no other
+    // browser does that, see https://github.com/w3c/csswg-drafts/issues/3262.
+    input.expect_ident()?;
+    if input.try(|input| input.expect_comma()).is_ok() {
+        parse_fallback(input)?;
+    }
+    if let Some(references) = references {
+        references.references_environment = true;
     }
     Ok(())
 }
 
 /// A struct that takes care of encapsulating the cascade process for custom
 /// properties.
 pub struct CustomPropertiesBuilder<'a> {
     seen: PrecomputedHashSet<&'a Name>,
     may_have_cycles: bool,
     custom_properties: Option<CustomPropertiesMap>,
     inherited: Option<&'a Arc<CustomPropertiesMap>>,
+    environment: &'a CssEnvironment,
 }
 
 impl<'a> CustomPropertiesBuilder<'a> {
     /// Create a new builder, inheriting from a given custom properties map.
-    pub fn new(inherited: Option<&'a Arc<CustomPropertiesMap>>) -> Self {
+    pub fn new(
+        inherited: Option<&'a Arc<CustomPropertiesMap>>,
+        environment: &'a CssEnvironment,
+    ) -> Self {
         Self {
             seen: PrecomputedHashSet::default(),
             may_have_cycles: false,
             custom_properties: None,
             inherited,
+            environment,
         }
     }
 
     /// Cascade a given custom property declaration.
-    pub fn cascade(
-        &mut self,
-        name: &'a Name,
-        specified_value: &CustomDeclarationValue,
-    ) {
+    pub fn cascade(&mut self, name: &'a Name, specified_value: &CustomDeclarationValue) {
         let was_already_present = !self.seen.insert(name);
         if was_already_present {
             return;
         }
 
         if !self.value_may_affect_style(name, specified_value) {
             return;
         }
@@ -535,34 +630,53 @@ impl<'a> CustomPropertiesBuilder<'a> {
                 Some(inherited) => (**inherited).clone(),
                 None => CustomPropertiesMap::new(),
             });
         }
 
         let map = self.custom_properties.as_mut().unwrap();
         match *specified_value {
             CustomDeclarationValue::Value(ref unparsed_value) => {
-                self.may_have_cycles |= !unparsed_value.references.is_empty();
-                map.insert(name.clone(), (*unparsed_value).clone());
+                let has_references = !unparsed_value.references.is_empty();
+                self.may_have_cycles |= has_references;
+
+                // If the variable value has no references and it has an
+                // environment variable here, perform substitution here instead
+                // of forcing a full traversal in `substitute_all` afterwards.
+                let value = if !has_references && unparsed_value.references_environment {
+                    let invalid = Default::default(); // Irrelevant, since there are no references.
+                    let result = substitute_references_in_value(
+                        unparsed_value,
+                        &map,
+                        &invalid,
+                        &self.environment,
+                    );
+                    match result {
+                        Ok(new_value) => Arc::new(new_value),
+                        Err(..) => {
+                            map.remove(name);
+                            return;
+                        },
+                    }
+                } else {
+                    (*unparsed_value).clone()
+                };
+                map.insert(name.clone(), value);
             },
             CustomDeclarationValue::CSSWideKeyword(keyword) => match keyword {
                 CSSWideKeyword::Initial => {
                     map.remove(name);
                 },
                 // handled in value_may_affect_style
                 CSSWideKeyword::Unset | CSSWideKeyword::Inherit => unreachable!(),
             },
         }
     }
 
-    fn value_may_affect_style(
-        &self,
-        name: &Name,
-        value: &CustomDeclarationValue,
-    ) -> bool {
+    fn value_may_affect_style(&self, name: &Name, value: &CustomDeclarationValue) -> bool {
         match *value {
             CustomDeclarationValue::CSSWideKeyword(CSSWideKeyword::Unset) |
             CustomDeclarationValue::CSSWideKeyword(CSSWideKeyword::Inherit) => {
                 // Custom properties are inherited by default. So
                 // explicit 'inherit' or 'unset' means we can just use
                 // any existing value in the inherited CustomPropertiesMap.
                 return false;
             },
@@ -591,37 +705,36 @@ impl<'a> CustomPropertiesBuilder<'a> {
             _ => {},
         }
 
         true
     }
 
     /// Returns the final map of applicable custom properties.
     ///
-    /// If there was any specified property, we've created a new map and now we need
-    /// to remove any potential cycles, and wrap it in an arc.
+    /// If there was any specified property, we've created a new map and now we
+    /// need to remove any potential cycles, and wrap it in an arc.
     ///
     /// Otherwise, just use the inherited custom properties map.
     pub fn build(mut self) -> Option<Arc<CustomPropertiesMap>> {
         let mut map = match self.custom_properties.take() {
             Some(m) => m,
             None => return self.inherited.cloned(),
         };
-
         if self.may_have_cycles {
-            substitute_all(&mut map);
+            substitute_all(&mut map, self.environment);
         }
         Some(Arc::new(map))
     }
 }
 
 /// Resolve all custom properties to either substituted or invalid.
 ///
 /// It does cycle dependencies removal at the same time as substitution.
-fn substitute_all(custom_properties_map: &mut CustomPropertiesMap) {
+fn substitute_all(custom_properties_map: &mut CustomPropertiesMap, environment: &CssEnvironment) {
     // The cycle dependencies removal in this function is a variant
     // of Tarjan's algorithm. It is mostly based on the pseudo-code
     // listed in
     // https://en.wikipedia.org/w/index.php?
     // title=Tarjan%27s_strongly_connected_components_algorithm&oldid=801728495
     //
     // FIXME This function currently does at least one addref to names
     // for each variable regardless whether it has reference. Each
@@ -659,16 +772,18 @@ fn substitute_all(custom_properties_map:
         /// Information of each variable indexed by the order index.
         var_info: SmallVec<[VarInfo; 5]>,
         /// The stack of order index of visited variables. It contains
         /// all unfinished strong connected components.
         stack: SmallVec<[usize; 5]>,
         map: &'a mut CustomPropertiesMap,
         /// The set of invalid custom properties.
         invalid: &'a mut PrecomputedHashSet<Name>,
+        /// The environment to substitute `env()` variables.
+        environment: &'a CssEnvironment,
     }
 
     /// This function combines the traversal for cycle removal and value
     /// substitution. It returns either a signal None if this variable
     /// has been fully resolved (to either having no reference or being
     /// marked invalid), or the order index for the given name.
     ///
     /// When it returns, the variable corresponds to the name would be
@@ -681,38 +796,48 @@ fn substitute_all(custom_properties_map:
     ///   of dependency circle, it would put all variables in the same
     ///   strong connected component to the set together.
     /// * It doesn't have any reference, because either this variable
     ///   doesn't have reference at all in specified value, or it has
     ///   been completely resolved.
     /// * There is no such variable at all.
     fn traverse<'a>(name: Name, context: &mut Context<'a>) -> Option<usize> {
         // Some shortcut checks.
-        let (name, value) = if let Some(value) = context.map.get(&name) {
-            // This variable has been resolved. Return the signal value.
-            if value.references.is_empty() || context.invalid.contains(&name) {
+        let (name, value) = {
+            let value = context.map.get(&name)?;
+
+            // Nothing to resolve.
+            if value.references.is_empty() {
+                debug_assert!(
+                    !value.references_environment,
+                    "Should've been handled earlier"
+                );
                 return None;
             }
+
+            // This variable has already been resolved.
+            if context.invalid.contains(&name) {
+                return None;
+            }
+
             // Whether this variable has been visited in this traversal.
             let key;
             match context.index_map.entry(name) {
                 Entry::Occupied(entry) => {
                     return Some(*entry.get());
                 },
                 Entry::Vacant(entry) => {
                     key = entry.key().clone();
                     entry.insert(context.count);
                 },
             }
+
             // Hold a strong reference to the value so that we don't
             // need to keep reference to context.map.
             (key, value.clone())
-        } else {
-            // The variable doesn't exist at all.
-            return None;
         };
 
         // Add new entry to the information table.
         let index = context.count;
         context.count += 1;
         debug_assert_eq!(index, context.var_info.len());
         context.var_info.push(VarInfo {
             name: Some(name),
@@ -788,39 +913,30 @@ fn substitute_all(custom_properties_map:
             // This variable is in loop. Resolve to invalid.
             context.invalid.insert(name);
             return None;
         }
 
         // Now we have shown that this variable is not in a loop, and
         // all of its dependencies should have been resolved. We can
         // start substitution now.
-        let mut computed_value = ComputedValue::empty();
-        let mut input = ParserInput::new(&value.css);
-        let mut input = Parser::new(&mut input);
-        let mut position = (input.position(), value.first_token_type);
-        let result = substitute_block(
-            &mut input,
-            &mut position,
-            &mut computed_value,
-            &mut |name, partial_computed_value| {
-                if let Some(value) = context.map.get(name) {
-                    if !context.invalid.contains(name) {
-                        partial_computed_value.push_variable(value);
-                        return Ok(value.last_token_type);
-                    }
-                }
-                Err(())
+        let result = substitute_references_in_value(
+            &value,
+            &context.map,
+            &context.invalid,
+            &context.environment,
+        );
+
+        match result {
+            Ok(computed_value) => {
+                context.map.insert(name, Arc::new(computed_value));
             },
-        );
-        if let Ok(last_token_type) = result {
-            computed_value.push_from(position, &input, last_token_type);
-            context.map.insert(name, Arc::new(computed_value));
-        } else {
-            context.invalid.insert(name);
+            Err(..) => {
+                context.invalid.insert(name);
+            },
         }
 
         // All resolved, so return the signal value.
         None
     }
 
     // We have to clone the names so that we can mutably borrow the map
     // in the context we create for traversal.
@@ -829,42 +945,69 @@ fn substitute_all(custom_properties_map:
     for name in names.into_iter() {
         let mut context = Context {
             count: 0,
             index_map: PrecomputedHashMap::default(),
             stack: SmallVec::new(),
             var_info: SmallVec::new(),
             map: custom_properties_map,
             invalid: &mut invalid,
+            environment,
         };
         traverse(name, &mut context);
     }
 
     custom_properties_map.remove_set(&invalid);
 }
 
+/// Replace `var()` and `env()` functions in a pre-existing variable value.
+fn substitute_references_in_value<'i>(
+    value: &'i VariableValue,
+    custom_properties: &CustomPropertiesMap,
+    invalid_custom_properties: &PrecomputedHashSet<Name>,
+    environment: &CssEnvironment,
+) -> Result<ComputedValue, ParseError<'i>> {
+    debug_assert!(!value.references.is_empty() || value.references_environment);
+
+    let mut input = ParserInput::new(&value.css);
+    let mut input = Parser::new(&mut input);
+    let mut position = (input.position(), value.first_token_type);
+    let mut computed_value = ComputedValue::empty();
+
+    let last_token_type = substitute_block(
+        &mut input,
+        &mut position,
+        &mut computed_value,
+        custom_properties,
+        invalid_custom_properties,
+        environment,
+    )?;
+
+    computed_value.push_from(position, &input, last_token_type);
+    Ok(computed_value)
+}
+
 /// Replace `var()` functions in an arbitrary bit of input.
 ///
-/// The `substitute_one` callback is called for each `var()` function in `input`.
-/// If the variable has its initial value,
-/// the callback should return `Err(())` and leave `partial_computed_value` unchanged.
+/// If the variable has its initial value, the callback should return `Err(())`
+/// and leave `partial_computed_value` unchanged.
+///
 /// Otherwise, it should push the value of the variable (with its own `var()` functions replaced)
 /// to `partial_computed_value` and return `Ok(last_token_type of what was pushed)`
 ///
 /// Return `Err(())` if `input` is invalid at computed-value time.
 /// or `Ok(last_token_type that was pushed to partial_computed_value)` otherwise.
-fn substitute_block<'i, 't, F>(
+fn substitute_block<'i, 't>(
     input: &mut Parser<'i, 't>,
     position: &mut (SourcePosition, TokenSerializationType),
     partial_computed_value: &mut ComputedValue,
-    substitute_one: &mut F,
-) -> Result<TokenSerializationType, ParseError<'i>>
-where
-    F: FnMut(&Name, &mut ComputedValue) -> Result<TokenSerializationType, ()>,
-{
+    custom_properties: &CustomPropertiesMap,
+    invalid_custom_properties: &PrecomputedHashSet<Name>,
+    env: &CssEnvironment,
+) -> Result<TokenSerializationType, ParseError<'i>> {
     let mut last_token_type = TokenSerializationType::nothing();
     let mut set_position_at_next_iteration = false;
     loop {
         let before_this_token = input.position();
         // FIXME: remove clone() when borrows are non-lexical
         let next = input
             .next_including_whitespace_and_comments()
             .map(|t| t.clone());
@@ -878,97 +1021,130 @@ where
             );
             set_position_at_next_iteration = false;
         }
         let token = match next {
             Ok(token) => token,
             Err(..) => break,
         };
         match token {
-            Token::Function(ref name) if name.eq_ignore_ascii_case("var") => {
+            Token::Function(ref name)
+                if name.eq_ignore_ascii_case("var") || name.eq_ignore_ascii_case("env") =>
+            {
+                let is_env = name.eq_ignore_ascii_case("env");
+
                 partial_computed_value.push(
                     input.slice(position.0..before_this_token),
                     position.1,
                     last_token_type,
                 );
                 input.parse_nested_block(|input| {
-                    // parse_var_function() ensures neither .unwrap() will fail.
-                    let name = input.expect_ident_cloned().unwrap();
-                    let name = Atom::from(parse_name(&name).unwrap());
+                    // parse_var_function() / parse_env_function() ensure neither .unwrap() will fail.
+                    let name = {
+                        let name = input.expect_ident().unwrap();
+                        if is_env {
+                            Atom::from(&**name)
+                        } else {
+                            Atom::from(parse_name(&name).unwrap())
+                        }
+                    };
 
-                    if let Ok(last) = substitute_one(&name, partial_computed_value) {
-                        last_token_type = last;
+                    let value = if is_env {
+                        env.get(&name)
+                    } else {
+                        if invalid_custom_properties.contains(&name) {
+                            None
+                        } else {
+                            custom_properties.get(&name).map(|v| &**v)
+                        }
+                    };
+
+                    if let Some(v) = value {
+                        last_token_type = v.last_token_type;
+                        partial_computed_value.push_variable(v);
                         // Skip over the fallback, as `parse_nested_block` would return `Err`
-                        // if we don’t consume all of `input`.
+                        // if we don't consume all of `input`.
                         // FIXME: Add a specialized method to cssparser to do this with less work.
-                        while let Ok(_) = input.next() {}
+                        while input.next().is_ok() {}
                     } else {
                         input.expect_comma()?;
                         let after_comma = input.state();
-                        let first_token_type = input.next_including_whitespace_and_comments()
+                        let first_token_type = input
+                            .next_including_whitespace_and_comments()
                             // parse_var_function() ensures that .unwrap() will not fail.
                             .unwrap()
                             .serialization_type();
                         input.reset(&after_comma);
                         let mut position = (after_comma.position(), first_token_type);
                         last_token_type = substitute_block(
                             input,
                             &mut position,
                             partial_computed_value,
-                            substitute_one,
+                            custom_properties,
+                            invalid_custom_properties,
+                            env,
                         )?;
                         partial_computed_value.push_from(position, input, last_token_type);
                     }
                     Ok(())
                 })?;
                 set_position_at_next_iteration = true
-            },
-
+            }
             Token::Function(_) |
             Token::ParenthesisBlock |
             Token::CurlyBracketBlock |
             Token::SquareBracketBlock => {
                 input.parse_nested_block(|input| {
-                    substitute_block(input, position, partial_computed_value, substitute_one)
+                    substitute_block(
+                        input,
+                        position,
+                        partial_computed_value,
+                        custom_properties,
+                        invalid_custom_properties,
+                        env,
+                    )
                 })?;
-                // It’s the same type for CloseCurlyBracket and CloseSquareBracket.
+                // It's the same type for CloseCurlyBracket and CloseSquareBracket.
                 last_token_type = Token::CloseParenthesis.serialization_type();
             },
 
             _ => last_token_type = token.serialization_type(),
         }
     }
     // FIXME: deal with things being implicitly closed at the end of the input. E.g.
     // ```html
     // <div style="--color: rgb(0,0,0">
     // <p style="background: var(--color) var(--image) top left; --image: url('a.png"></p>
     // </div>
     // ```
     Ok(last_token_type)
 }
 
-/// Replace `var()` functions for a non-custom property.
+/// Replace `var()` and `env()` functions for a non-custom property.
+///
 /// Return `Err(())` for invalid at computed time.
 pub fn substitute<'i>(
     input: &'i str,
     first_token_type: TokenSerializationType,
     computed_values_map: Option<&Arc<CustomPropertiesMap>>,
+    env: &CssEnvironment,
 ) -> Result<String, ParseError<'i>> {
     let mut substituted = ComputedValue::empty();
     let mut input = ParserInput::new(input);
     let mut input = Parser::new(&mut input);
     let mut position = (input.position(), first_token_type);
+    let invalid = PrecomputedHashSet::default();
+    let empty_map = CustomPropertiesMap::new();
+    let custom_properties = match computed_values_map {
+        Some(m) => &**m,
+        None => &empty_map,
+    };
     let last_token_type = substitute_block(
         &mut input,
         &mut position,
         &mut substituted,
-        &mut |name, substituted| {
-            if let Some(value) = computed_values_map.and_then(|map| map.get(name)) {
-                substituted.push_variable(value);
-                Ok(value.last_token_type)
-            } else {
-                Err(())
-            }
-        },
+        &custom_properties,
+        &invalid,
+        env,
     )?;
     substituted.push_from(position, &input, last_token_type);
     Ok(substituted.css)
 }
--- a/servo/components/style/gecko/media_queries.rs
+++ b/servo/components/style/gecko/media_queries.rs
@@ -2,16 +2,17 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 //! Gecko's media-query device and expression representation.
 
 use app_units::AU_PER_PX;
 use app_units::Au;
 use cssparser::RGBA;
+use custom_properties::CssEnvironment;
 use euclid::Size2D;
 use euclid::TypedScale;
 use gecko::values::{convert_nscolor_to_rgba, convert_rgba_to_nscolor};
 use gecko_bindings::bindings;
 use gecko_bindings::structs;
 use gecko_bindings::structs::{nsPresContext, RawGeckoPresContextBorrowed};
 use media_queries::MediaType;
 use properties::ComputedValues;
@@ -47,16 +48,19 @@ pub struct Device {
     /// <https://quirks.spec.whatwg.org/#the-tables-inherit-color-from-body-quirk>
     body_text_color: AtomicUsize,
     /// Whether any styles computed in the document relied on the root font-size
     /// by using rem units.
     used_root_font_size: AtomicBool,
     /// Whether any styles computed in the document relied on the viewport size
     /// by using vw/vh/vmin/vmax units.
     used_viewport_size: AtomicBool,
+    /// The CssEnvironment object responsible of getting CSS environment
+    /// variables.
+    environment: CssEnvironment,
 }
 
 impl fmt::Debug for Device {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         use nsstring::nsCString;
 
         let mut doc_uri = nsCString::new();
         unsafe {
@@ -82,19 +86,26 @@ impl Device {
         Device {
             pres_context,
             default_values: ComputedValues::default_values(unsafe { &*pres_context }),
             // FIXME(bz): Seems dubious?
             root_font_size: AtomicIsize::new(FontSize::medium().size().0 as isize),
             body_text_color: AtomicUsize::new(unsafe { &*pres_context }.mDefaultColor as usize),
             used_root_font_size: AtomicBool::new(false),
             used_viewport_size: AtomicBool::new(false),
+            environment: CssEnvironment,
         }
     }
 
+    /// Get the relevant environment to resolve `env()` functions.
+    #[inline]
+    pub fn environment(&self) -> &CssEnvironment {
+        &self.environment
+    }
+
     /// Tells the device that a new viewport rule has been found, and stores the
     /// relevant viewport constraints.
     pub fn account_for_viewport_rule(&mut self, _constraints: &ViewportConstraints) {
         unreachable!("Gecko doesn't support @viewport");
     }
 
     /// Whether any animation name may be referenced from the style of any
     /// element.
--- a/servo/components/style/properties/cascade.rs
+++ b/servo/components/style/properties/cascade.rs
@@ -238,17 +238,20 @@ where
             ) ||
             parent_style.unwrap().pseudo() == Some(PseudoElement::FirstLine)
     );
 
     let inherited_style = parent_style.unwrap_or(device.default_computed_values());
 
     let mut declarations = SmallVec::<[(&_, CascadeLevel); 32]>::new();
     let custom_properties = {
-        let mut builder = CustomPropertiesBuilder::new(inherited_style.custom_properties());
+        let mut builder = CustomPropertiesBuilder::new(
+            inherited_style.custom_properties(),
+            device.environment(),
+        );
 
         for (declaration, cascade_level) in iter_declarations() {
             declarations.push((declaration, cascade_level));
             if let PropertyDeclaration::Custom(ref declaration) = *declaration {
                 builder.cascade(&declaration.name, &declaration.value);
             }
         }
 
@@ -415,16 +418,17 @@ impl<'a, 'b: 'a> Cascade<'a, 'b> {
                 .borrow_mut()
                 .set_uncacheable();
         }
 
         Cow::Owned(declaration.value.substitute_variables(
             declaration.id,
             self.context.builder.custom_properties.as_ref(),
             self.context.quirks_mode,
+            self.context.device().environment(),
         ))
     }
 
     fn apply_declaration<Phase: CascadePhase>(
         &mut self,
         longhand_id: LonghandId,
         declaration: &PropertyDeclaration,
     ) {
--- a/servo/components/style/properties/declaration_block.rs
+++ b/servo/components/style/properties/declaration_block.rs
@@ -4,17 +4,17 @@
 
 //! A property declaration block.
 
 #![deny(missing_docs)]
 
 use context::QuirksMode;
 use cssparser::{DeclarationListParser, parse_important, ParserInput, CowRcStr};
 use cssparser::{Parser, AtRuleParser, DeclarationParser, Delimiter, ParseErrorKind};
-use custom_properties::CustomPropertiesBuilder;
+use custom_properties::{CustomPropertiesBuilder, CssEnvironment};
 use error_reporting::{ParseErrorReporter, ContextualParseError};
 use itertools::Itertools;
 use parser::ParserContext;
 use properties::animated_properties::{AnimationValue, AnimationValueMap};
 use shared_lock::Locked;
 use smallbitvec::{self, SmallBitVec};
 use smallvec::SmallVec;
 use std::fmt::{self, Write};
@@ -755,23 +755,29 @@ impl PropertyDeclarationBlock {
 
         // FIXME(emilio): Should this assert, or assert that the declaration is
         // the property we expect?
         let declaration = match self.declarations.get(0) {
             Some(d) => d,
             None => return Err(fmt::Error),
         };
 
+        // TODO(emilio): When we implement any environment variable without
+        // hard-coding the values we're going to need to get something
+        // meaningful out of here... All this code path is so terribly hacky
+        // ;_;.
+        let env = CssEnvironment;
+
         let custom_properties = if let Some(cv) = computed_values {
             // If there are extra custom properties for this declaration block,
             // factor them in too.
             if let Some(block) = custom_properties_block {
                 // FIXME(emilio): This is not super-efficient here, and all this
                 // feels like a hack anyway...
-                block.cascade_custom_properties(cv.custom_properties())
+                block.cascade_custom_properties(cv.custom_properties(), &env)
             } else {
                 cv.custom_properties().cloned()
             }
         } else {
             None
         };
 
         match (declaration, computed_values) {
@@ -785,16 +791,17 @@ impl PropertyDeclarationBlock {
             (
                 &PropertyDeclaration::WithVariables(ref declaration),
                 Some(ref _computed_values),
             ) => {
                 declaration.value.substitute_variables(
                     declaration.id,
                     custom_properties.as_ref(),
                     QuirksMode::NoQuirks,
+                    &env,
                 ).to_css(dest)
             },
             (ref d, _) => d.to_css(dest),
         }
     }
 
     /// Convert AnimationValueMap to PropertyDeclarationBlock.
     pub fn from_animation_value_map(animation_value_map: &AnimationValueMap) -> Self {
@@ -830,27 +837,34 @@ impl PropertyDeclarationBlock {
 
     /// Returns a custom properties map which is the result of cascading custom
     /// properties in this declaration block along with context's custom
     /// properties.
     pub fn cascade_custom_properties_with_context(
         &self,
         context: &Context,
     ) -> Option<Arc<::custom_properties::CustomPropertiesMap>> {
-        self.cascade_custom_properties(context.style().custom_properties())
+        self.cascade_custom_properties(
+            context.style().custom_properties(),
+            context.device().environment(),
+        )
     }
 
     /// Returns a custom properties map which is the result of cascading custom
     /// properties in this declaration block along with the given custom
     /// properties.
-    pub fn cascade_custom_properties(
+    fn cascade_custom_properties(
         &self,
         inherited_custom_properties: Option<&Arc<::custom_properties::CustomPropertiesMap>>,
+        environment: &CssEnvironment,
     ) -> Option<Arc<::custom_properties::CustomPropertiesMap>> {
-        let mut builder = CustomPropertiesBuilder::new(inherited_custom_properties);
+        let mut builder = CustomPropertiesBuilder::new(
+            inherited_custom_properties,
+            environment,
+        );
 
         for declaration in self.normal_declaration_iter() {
             if let PropertyDeclaration::Custom(ref declaration) = *declaration {
                 builder.cascade(&declaration.name, &declaration.value);
             }
         }
 
         builder.build()
--- a/servo/components/style/properties/helpers/animated_properties.mako.rs
+++ b/servo/components/style/properties/helpers/animated_properties.mako.rs
@@ -514,16 +514,17 @@ impl AnimationValue {
                 let substituted = {
                     let custom_properties =
                         extra_custom_properties.or_else(|| context.style().custom_properties());
 
                     declaration.value.substitute_variables(
                         declaration.id,
                         custom_properties,
                         context.quirks_mode,
+                        context.device().environment(),
                     )
                 };
                 return AnimationValue::from_declaration(
                     &substituted,
                     context,
                     extra_custom_properties,
                     initial,
                 )
--- a/servo/components/style/properties/properties.mako.rs
+++ b/servo/components/style/properties/properties.mako.rs
@@ -1531,20 +1531,24 @@ impl ToCss for UnparsedValue {
 }
 
 impl UnparsedValue {
     fn substitute_variables(
         &self,
         longhand_id: LonghandId,
         custom_properties: Option<<&Arc<::custom_properties::CustomPropertiesMap>>,
         quirks_mode: QuirksMode,
+        environment: &::custom_properties::CssEnvironment,
     ) -> PropertyDeclaration {
-        ::custom_properties::substitute(&self.css, self.first_token_type, custom_properties)
-        .ok()
-        .and_then(|css| {
+        ::custom_properties::substitute(
+            &self.css,
+            self.first_token_type,
+            custom_properties,
+            environment,
+        ).ok().and_then(|css| {
             // As of this writing, only the base URL is used for property
             // values.
             //
             // NOTE(emilio): we intentionally pase `None` as the rule type here.
             // If something starts depending on it, it's probably a bug, since
             // it'd change how values are parsed depending on whether we're in a
             // @keyframes rule or not, for example... So think twice about
             // whether you want to do this!
@@ -2209,44 +2213,43 @@ impl PropertyDeclaration {
             PropertyId::LonghandAlias(id, _) |
             PropertyId::Longhand(id) => {
                 input.skip_whitespace();  // Unnecessary for correctness, but may help try() rewind less.
                 input.try(CSSWideKeyword::parse).map(|keyword| {
                     PropertyDeclaration::CSSWideKeyword(
                         WideKeywordDeclaration { id, keyword },
                     )
                 }).or_else(|()| {
-                    input.look_for_var_functions();
+                    input.look_for_var_or_env_functions();
                     input.parse_entirely(|input| id.parse_value(context, input))
                     .or_else(|err| {
                         while let Ok(_) = input.next() {}  // Look for var() after the error.
-                        if input.seen_var_functions() {
-                            input.reset(&start);
-                            let (first_token_type, css) =
-                                ::custom_properties::parse_non_custom_with_var(input).map_err(|e| {
-                                    StyleParseErrorKind::new_invalid(
-                                        non_custom_id.unwrap().name(),
-                                        e,
-                                    )
-                                })?;
-                            Ok(PropertyDeclaration::WithVariables(VariableDeclaration {
-                                id,
-                                value: Arc::new(UnparsedValue {
+                        if !input.seen_var_or_env_functions() {
+                            return Err(StyleParseErrorKind::new_invalid(
+                                non_custom_id.unwrap().name(),
+                                err,
+                            ));
+                        }
+                        input.reset(&start);
+                        let (first_token_type, css) =
+                            ::custom_properties::parse_non_custom_with_var(input).map_err(|e| {
+                                StyleParseErrorKind::new_invalid(
+                                    non_custom_id.unwrap().name(),
+                                    e,
+                                )
+                            })?;
+                        Ok(PropertyDeclaration::WithVariables(VariableDeclaration {
+                            id,
+                            value: Arc::new(UnparsedValue {
                                 css: css.into_owned(),
                                 first_token_type: first_token_type,
                                 url_data: context.url_data.clone(),
                                 from_shorthand: None,
-                                }),
-                            }))
-                        } else {
-                            Err(StyleParseErrorKind::new_invalid(
-                                non_custom_id.unwrap().name(),
-                                err,
-                            ))
-                        }
+                            }),
+                        }))
                     })
                 }).map(|declaration| {
                     declarations.push(declaration)
                 })?;
             }
             PropertyId::ShorthandAlias(id, _) |
             PropertyId::Shorthand(id) => {
                 input.skip_whitespace();  // Unnecessary for correctness, but may help try() rewind less.
@@ -2259,22 +2262,23 @@ impl PropertyDeclaration {
                                 WideKeywordDeclaration {
                                     id: longhand,
                                     keyword,
                                 },
                             ))
                         }
                     }
                 } else {
-                    input.look_for_var_functions();
-                    // Not using parse_entirely here: each ${shorthand.ident}::parse_into function
-                    // needs to do so *before* pushing to `declarations`.
+                    input.look_for_var_or_env_functions();
+                    // Not using parse_entirely here: each
+                    // ${shorthand.ident}::parse_into function needs to do so
+                    // *before* pushing to `declarations`.
                     id.parse_into(declarations, context, input).or_else(|err| {
                         while let Ok(_) = input.next() {}  // Look for var() after the error.
-                        if !input.seen_var_functions() {
+                        if !input.seen_var_or_env_functions() {
                             return Err(StyleParseErrorKind::new_invalid(
                                 non_custom_id.unwrap().name(),
                                 err,
                             ));
                         }
 
                         input.reset(&start);
                         let (first_token_type, css) =
--- a/servo/components/style_traits/Cargo.toml
+++ b/servo/components/style_traits/Cargo.toml
@@ -10,17 +10,17 @@ name = "style_traits"
 path = "lib.rs"
 
 [features]
 servo = ["serde", "servo_atoms", "cssparser/serde", "webrender_api", "servo_url"]
 gecko = []
 
 [dependencies]
 app_units = "0.7"
-cssparser = "0.24.0"
+cssparser = "0.25"
 bitflags = "1.0"
 euclid = "0.19"
 malloc_size_of = { path = "../malloc_size_of" }
 malloc_size_of_derive = { path = "../malloc_size_of_derive" }
 selectors = { path = "../selectors" }
 serde = {version = "1.0", optional = true}
 webrender_api = {git = "https://github.com/servo/webrender", optional = true}
 servo_atoms = {path = "../atoms", optional = true}
--- a/servo/ports/geckolib/Cargo.toml
+++ b/servo/ports/geckolib/Cargo.toml
@@ -9,17 +9,17 @@ name = "geckoservo"
 path = "lib.rs"
 
 [features]
 bindgen = ["style/use_bindgen"]
 gecko_debug = ["style/gecko_debug", "nsstring/gecko_debug"]
 
 [dependencies]
 atomic_refcell = "0.1"
-cssparser = "0.24.0"
+cssparser = "0.25"
 cstr = "0.1.2"
 libc = "0.2"
 log = {version = "0.4", features = ["release_max_level_info"]}
 malloc_size_of = {path = "../../components/malloc_size_of"}
 nsstring = {path = "../../support/gecko/nsstring"}
 num-traits = "0.2"
 parking_lot = "0.6"
 selectors = {path = "../../components/selectors"}
--- a/servo/ports/geckolib/tests/Cargo.toml
+++ b/servo/ports/geckolib/tests/Cargo.toml
@@ -8,17 +8,17 @@ build = "build.rs"
 
 [lib]
 name = "stylo_tests"
 path = "lib.rs"
 doctest = false
 
 [dependencies]
 atomic_refcell = "0.1"
-cssparser = "0.24.0"
+cssparser = "0.25"
 cstr = "0.1.2"
 env_logger = { version = "0.5", default-features = false }
 geckoservo = {path = "../../../ports/geckolib"}
 libc = "0.2"
 log = {version = "0.4", features = ["release_max_level_info"]}
 malloc_size_of = {path = "../../../components/malloc_size_of"}
 num-traits = "0.2"
 selectors = {path = "../../../components/selectors"}
--- a/servo/tests/unit/style/Cargo.toml
+++ b/servo/tests/unit/style/Cargo.toml
@@ -7,17 +7,17 @@ license = "MPL-2.0"
 [lib]
 name = "style_tests"
 path = "lib.rs"
 doctest = false
 
 [dependencies]
 byteorder = "1.0"
 app_units = "0.7"
-cssparser = "0.24.0"
+cssparser = "0.25"
 euclid = "0.19"
 html5ever = "0.22"
 parking_lot = "0.6"
 rayon = "1"
 serde_json = "1.0"
 selectors = {path = "../../../components/selectors"}
 servo_arc = {path = "../../../components/servo_arc"}
 servo_atoms = {path = "../../../components/atoms"}
deleted file mode 100644
--- a/testing/web-platform/meta/css/css-env/at-supports.tentative.html.ini
+++ /dev/null
@@ -1,4 +0,0 @@
-[at-supports.tentative.html]
-  [Test that CSS env vars work with @support]
-    expected: FAIL
-
deleted file mode 100644
--- a/testing/web-platform/meta/css/css-env/env-in-custom-properties.tentative.html.ini
+++ /dev/null
@@ -1,4 +0,0 @@
-[env-in-custom-properties.tentative.html]
-  [Test env() will work in custom properties]
-    expected: FAIL
-
deleted file mode 100644
--- a/testing/web-platform/meta/css/css-env/fallback-nested-var.tentative.html.ini
+++ /dev/null
@@ -1,4 +0,0 @@
-[fallback-nested-var.tentative.html]
-  [Test that nested var() fallback values work with CSS env vars]
-    expected: FAIL
-
deleted file mode 100644
--- a/testing/web-platform/meta/css/css-env/seralization-round-tripping.tentative.html.ini
+++ /dev/null
@@ -1,4 +0,0 @@
-[seralization-round-tripping.tentative.html]
-  [Test style seralization round tripping with CSS env vars]
-    expected: FAIL
-
deleted file mode 100644
--- a/testing/web-platform/meta/css/css-env/supports-script.tentative.html.ini
+++ /dev/null
@@ -1,4 +0,0 @@
-[supports-script.tentative.html]
-  [Test that CSS env vars work with CSS.supports]
-    expected: FAIL
-
deleted file mode 100644
--- a/testing/web-platform/meta/css/css-env/syntax.tentative.html.ini
+++ /dev/null
@@ -1,43 +0,0 @@
-[syntax.tentative.html]
-  [background-color: env(test) rgba(0, 0, 0, 0)]
-    expected: FAIL
-
-  [background-color: ENV(test) rgba(0, 0, 0, 0)]
-    expected: FAIL
-
-  [background-color: env(test) !important rgba(0, 0, 0, 0)]
-    expected: FAIL
-
-  [background-color: env(test, 10px) rgba(0, 0, 0, 0)]
-    expected: FAIL
-
-  [background-color: env(test, blue) rgb(0, 0, 255)]
-    expected: FAIL
-
-  [background-color: env(test, env(another)) rgba(0, 0, 0, 0)]
-    expected: FAIL
-
-  [background-color: env(test, env(another, blue)) rgb(0, 0, 255)]
-    expected: FAIL
-
-  [background-color: env(-test) rgba(0, 0, 0, 0)]
-    expected: FAIL
-
-  [background-color: env(--test) rgba(0, 0, 0, 0)]
-    expected: FAIL
-
-  [background-color: env( test) rgba(0, 0, 0, 0)]
-    expected: FAIL
-
-  [background-color: env(test ) rgba(0, 0, 0, 0)]
-    expected: FAIL
-
-  [background-color: env( test ) rgba(0, 0, 0, 0)]
-    expected: FAIL
-
-  [background-color: env(test /**/, blue) rgb(0, 0, 255)]
-    expected: FAIL
-
-  [background-color: env(test, {}) rgba(0, 0, 0, 0)]
-    expected: FAIL
-
deleted file mode 100644
--- a/testing/web-platform/meta/css/css-env/unknown-env-names-override-previous.tentative.html.ini
+++ /dev/null
@@ -1,4 +0,0 @@
-[unknown-env-names-override-previous.tentative.html]
-  [Test unknown env() names will override previous values]
-    expected: FAIL
-
--- a/third_party/rust/cssparser/.cargo-checksum.json
+++ b/third_party/rust/cssparser/.cargo-checksum.json
@@ -1,1 +1,1 @@
-{"files":{"Cargo.toml":"8329b9e0771dc3e34acca27dbb047f397e3bb30e6d0a69444ce9a2ad0b0fc2cd","LICENSE":"fab3dd6bdab226f1c08630b1dd917e11fcb4ec5e1e020e2c16f83a0a13863e85","README.md":"c5781e673335f37ed3d7acb119f8ed33efdf6eb75a7094b7da2abe0c3230adb8","build.rs":"ce686e87cccb6aa85a8cd34688d809398c5a624f179fd9a172d1049892da3f4c","build/match_byte.rs":"31905ae3dba69fa82c1f13069df4cd056bb340d59ee5d177679425f105f203cf","docs/404.html":"025861f76f8d1f6d67c20ab624c6e418f4f824385e2dd8ad8732c4ea563c6a2e","docs/index.html":"025861f76f8d1f6d67c20ab624c6e418f4f824385e2dd8ad8732c4ea563c6a2e","src/color.rs":"c60f1b0ab7a2a6213e434604ee33f78e7ef74347f325d86d0b9192d8225ae1cc","src/cow_rc_str.rs":"541216f8ef74ee3cc5cbbc1347e5f32ed66588c401851c9a7d68b867aede1de0","src/from_bytes.rs":"331fe63af2123ae3675b61928a69461b5ac77799fff3ce9978c55cf2c558f4ff","src/lib.rs":"a474ee88ef8f73fcb7b7272d426e5eafb4ad10d104797a5a188d1676c8180972","src/macros.rs":"adb9773c157890381556ea83d7942dcc676f99eea71abbb6afeffee1e3f28960","src/nth.rs":"5c70fb542d1376cddab69922eeb4c05e4fcf8f413f27563a2af50f72a47c8f8c","src/parser.rs":"9ed4aec998221eb2d2ba99db2f9f82a02399fb0c3b8500627f68f5aab872adde","src/rules_and_declarations.rs":"622ce07c117a511d40ce595602d4f4730659a59273388f28553d1a2b0fac92ce","src/serializer.rs":"3e2dfc60613f885cb6f99abfc854fde2a1e00de507431bd2e51178b61abfd69b","src/size_of_tests.rs":"e5f63c8c18721cc3ff7a5407e84f9889ffa10e66da96e8510a696c3e00ad72d5","src/tests.rs":"4a9223b9d2dc982144499aee497515553fc3d9ec86ca7b2e62b6caa5d4a11570","src/tokenizer.rs":"429b2cba419cf8b923fbcc32d3bd34c0b39284ebfcb9fc29b8eb8643d8d5f312","src/unicode_range.rs":"ae159d2ebe4123a6666e18dc0362f89b475240a6b7ed5fb6fe21b9e7a4139da8"},"package":"b200a7193703a615c8d2751fed1ede39b9c4b3905e09d1ec7064a24688c190fc"}
\ No newline at end of file
+{"files":{"Cargo.toml":"47497bde56f31c8a24665d840fbe5b03f14324dd06a68f907e419f8e7a855186","LICENSE":"fab3dd6bdab226f1c08630b1dd917e11fcb4ec5e1e020e2c16f83a0a13863e85","README.md":"c5781e673335f37ed3d7acb119f8ed33efdf6eb75a7094b7da2abe0c3230adb8","build.rs":"310d6d7b1931ff783a8aa1a4c6baee87b4c9130c858e4694ef69cc96df5e38dc","build/match_byte.rs":"31905ae3dba69fa82c1f13069df4cd056bb340d59ee5d177679425f105f203cf","docs/404.html":"025861f76f8d1f6d67c20ab624c6e418f4f824385e2dd8ad8732c4ea563c6a2e","docs/index.html":"025861f76f8d1f6d67c20ab624c6e418f4f824385e2dd8ad8732c4ea563c6a2e","src/color.rs":"c60f1b0ab7a2a6213e434604ee33f78e7ef74347f325d86d0b9192d8225ae1cc","src/cow_rc_str.rs":"541216f8ef74ee3cc5cbbc1347e5f32ed66588c401851c9a7d68b867aede1de0","src/from_bytes.rs":"331fe63af2123ae3675b61928a69461b5ac77799fff3ce9978c55cf2c558f4ff","src/lib.rs":"a474ee88ef8f73fcb7b7272d426e5eafb4ad10d104797a5a188d1676c8180972","src/macros.rs":"adb9773c157890381556ea83d7942dcc676f99eea71abbb6afeffee1e3f28960","src/nth.rs":"5c70fb542d1376cddab69922eeb4c05e4fcf8f413f27563a2af50f72a47c8f8c","src/parser.rs":"a4ec0bd1b5eab6632cf1985701a7ea641fe7f7bbcc0a2bd33f924ae6228591a5","src/rules_and_declarations.rs":"622ce07c117a511d40ce595602d4f4730659a59273388f28553d1a2b0fac92ce","src/serializer.rs":"3e2dfc60613f885cb6f99abfc854fde2a1e00de507431bd2e51178b61abfd69b","src/size_of_tests.rs":"e5f63c8c18721cc3ff7a5407e84f9889ffa10e66da96e8510a696c3e00ad72d5","src/tests.rs":"9d08b3943d453664e01d58e307f79345e240f9f9ce6f8d36a842eff37155563e","src/tokenizer.rs":"adcf5811955e8df57a519e3d1e44fe3afeb5afeb1076daeb8d36fed1abcf1327","src/unicode_range.rs":"ae159d2ebe4123a6666e18dc0362f89b475240a6b7ed5fb6fe21b9e7a4139da8"},"package":"730363a45c4e248d4f21d3e5c1156d1a9cdec0855056c0d9539e814bc59865c3"}
\ No newline at end of file
--- a/third_party/rust/cssparser/Cargo.toml
+++ b/third_party/rust/cssparser/Cargo.toml
@@ -7,17 +7,17 @@
 #
 # If you believe there's an error in this file please file an
 # issue against the rust-lang/cargo repository. If you're
 # editing this file be aware that the upstream Cargo.toml
 # will likely look very different (and much more reasonable)
 
 [package]
 name = "cssparser"
-version = "0.24.1"
+version = "0.25.0"
 authors = ["Simon Sapin <simon.sapin@exyr.org>"]
 build = "build.rs"
 exclude = ["src/css-parsing-tests/**", "src/big-data-url.css"]
 description = "Rust implementation of CSS Syntax Level 3"
 documentation = "https://docs.rs/cssparser/"
 readme = "README.md"
 keywords = ["css", "syntax", "parser"]
 license = "MPL-2.0"
--- a/third_party/rust/cssparser/build.rs
+++ b/third_party/rust/cssparser/build.rs
@@ -3,20 +3,16 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #[macro_use]
 extern crate quote;
 #[macro_use]
 extern crate syn;
 extern crate proc_macro2;
 
-use std::env;
-use std::path::Path;
-
-
 #[cfg(feature = "dummy_match_byte")]
 mod codegen {
     use std::path::Path;
     pub fn main() {}
 }
 
 #[cfg(not(feature = "dummy_match_byte"))]
 #[path = "build/match_byte.rs"]
--- a/third_party/rust/cssparser/src/parser.rs
+++ b/third_party/rust/cssparser/src/parser.rs
@@ -465,27 +465,29 @@ impl<'i: 't, 't> Parser<'i, 't> {
     ///
     /// Should only be used with `SourcePosition` values from the same `Parser` instance.
     #[inline]
     pub fn reset(&mut self, state: &ParserState) {
         self.input.tokenizer.reset(state);
         self.at_start_of = state.at_start_of;
     }
 
-    /// Start looking for `var()` functions. (See the `.seen_var_functions()` method.)
+    /// Start looking for `var()` / `env()` functions. (See the
+    /// `.seen_var_or_env_functions()` method.)
     #[inline]
-    pub fn look_for_var_functions(&mut self) {
-        self.input.tokenizer.look_for_var_functions()
+    pub fn look_for_var_or_env_functions(&mut self) {
+        self.input.tokenizer.look_for_var_or_env_functions()
     }
 
-    /// Return whether a `var()` function has been seen by the tokenizer since
-    /// either `look_for_var_functions` was called, and stop looking.
+    /// Return whether a `var()` or `env()` function has been seen by the
+    /// tokenizer since either `look_for_var_or_env_functions` was called, and
+    /// stop looking.
     #[inline]
-    pub fn seen_var_functions(&mut self) -> bool {
-        self.input.tokenizer.seen_var_functions()
+    pub fn seen_var_or_env_functions(&mut self) -> bool {
+        self.input.tokenizer.seen_var_or_env_functions()
     }
 
     /// Execute the given closure, passing it the parser.
     /// If the result (returned unchanged) is `Err`,
     /// the internal state of the parser  (including position within the input)
     /// is restored to what it was before the call.
     #[inline]
     pub fn try<F, T, E>(&mut self, thing: F) -> Result<T, E>
--- a/third_party/rust/cssparser/src/tests.rs
+++ b/third_party/rust/cssparser/src/tests.rs
@@ -680,24 +680,24 @@ impl ToJson for Color {
 const BACKGROUND_IMAGE: &'static str = include_str!("big-data-url.css");
 
 #[cfg(feature = "bench")]
 #[bench]
 fn unquoted_url(b: &mut Bencher) {
     b.iter(|| {
         let mut input = ParserInput::new(BACKGROUND_IMAGE);
         let mut input = Parser::new(&mut input);
-        input.look_for_var_functions();
+        input.look_for_var_or_env_functions();
 
         let result = input.try(|input| input.expect_url());
 
         assert!(result.is_ok());
 
-        input.seen_var_functions();
-        (result.is_ok(), input.seen_var_functions())
+        input.seen_var_or_env_functions();
+        (result.is_ok(), input.seen_var_or_env_functions())
     })
 }
 
 
 #[cfg(feature = "bench")]
 #[bench]
 fn numeric(b: &mut Bencher) {
     b.iter(|| {
--- a/third_party/rust/cssparser/src/tokenizer.rs
+++ b/third_party/rust/cssparser/src/tokenizer.rs
@@ -203,17 +203,17 @@ pub struct Tokenizer<'a> {
     input: &'a str,
     /// Counted in bytes, not code points. From 0.
     position: usize,
     /// The position at the start of the current line; but adjusted to
     /// ensure that computing the column will give the result in units
     /// of UTF-16 characters.
     current_line_start_position: usize,
     current_line_number: u32,
-    var_functions: SeenStatus,
+    var_or_env_functions: SeenStatus,
     source_map_url: Option<&'a str>,
     source_url: Option<&'a str>,
 }
 
 #[derive(Copy, Clone, PartialEq, Eq)]
 enum SeenStatus {
     DontCare,
     LookingForThem,
@@ -229,39 +229,41 @@ impl<'a> Tokenizer<'a> {
 
     #[inline]
     pub fn with_first_line_number(input: &str, first_line_number: u32) -> Tokenizer {
         Tokenizer {
             input: input,
             position: 0,
             current_line_start_position: 0,
             current_line_number: first_line_number,
-            var_functions: SeenStatus::DontCare,
+            var_or_env_functions: SeenStatus::DontCare,
             source_map_url: None,
             source_url: None,
         }
     }
 
     #[inline]
-    pub fn look_for_var_functions(&mut self) {
-        self.var_functions = SeenStatus::LookingForThem;
+    pub fn look_for_var_or_env_functions(&mut self) {
+        self.var_or_env_functions = SeenStatus::LookingForThem;
     }
 
     #[inline]
-    pub fn seen_var_functions(&mut self) -> bool {
-        let seen = self.var_functions == SeenStatus::SeenAtLeastOne;
-        self.var_functions = SeenStatus::DontCare;
+    pub fn seen_var_or_env_functions(&mut self) -> bool {
+        let seen = self.var_or_env_functions == SeenStatus::SeenAtLeastOne;
+        self.var_or_env_functions = SeenStatus::DontCare;
         seen
     }
 
     #[inline]
     pub fn see_function(&mut self, name: &str) {
-        if self.var_functions == SeenStatus::LookingForThem {
-            if name.eq_ignore_ascii_case("var") {
-                self.var_functions = SeenStatus::SeenAtLeastOne;
+        if self.var_or_env_functions == SeenStatus::LookingForThem {
+            if name.eq_ignore_ascii_case("var") ||
+                name.eq_ignore_ascii_case("env")
+            {
+                self.var_or_env_functions = SeenStatus::SeenAtLeastOne;
             }
         }
     }
 
     #[inline]
     pub fn next(&mut self) -> Result<Token<'a>, ()> {
         next_token(self)
     }
--- a/xpcom/ds/StaticAtoms.py
+++ b/xpcom/ds/StaticAtoms.py
@@ -991,16 +991,20 @@ STATIC_ATOMS = [
     Atom("rtl", "rtl"),
     Atom("ruby", "ruby"),
     Atom("rubyBase", "ruby-base"),
     Atom("rubyBaseContainer", "ruby-base-container"),
     Atom("rubyText", "ruby-text"),
     Atom("rubyTextContainer", "ruby-text-container"),
     Atom("rules", "rules"),
     Atom("s", "s"),
+    Atom("safe_area_inset_top", "safe-area-inset-top"),
+    Atom("safe_area_inset_bottom", "safe-area-inset-bottom"),
+    Atom("safe_area_inset_left", "safe-area-inset-left"),
+    Atom("safe_area_inset_right", "safe-area-inset-right"),
     Atom("samp", "samp"),
     Atom("sandbox", "sandbox"),
     Atom("sbattr", "sbattr"),
     Atom("scale", "scale"),
     Atom("scan", "scan"),
     Atom("scheme", "scheme"),
     Atom("scope", "scope"),
     Atom("scoped", "scoped"),