From 84714a20617118100c29fa25df0067eb38fa9b8a Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Wed, 13 Sep 2023 11:07:46 -0700 Subject: [PATCH] multiple IDPs and identity transformations docs --- site/content/docs/background/architecture.md | 2 +- .../docs/howto/configure-auth-for-webapps.md | 5 + site/content/docs/howto/login.md | 5 + ...figure-supervisor-federationdomain-idps.md | 411 ++++++++++++++++++ ...nfigure-supervisor-with-activedirectory.md | 4 +- .../configure-supervisor-with-auth0.md | 4 +- .../configure-supervisor-with-dex.md | 4 +- .../configure-supervisor-with-gitlab.md | 4 +- ...configure-supervisor-with-jumpcloudldap.md | 4 +- .../configure-supervisor-with-okta.md | 4 +- .../configure-supervisor-with-openldap.md | 4 +- ...re-supervisor-with-workspace_one_access.md | 4 +- .../howto/supervisor/configure-supervisor.md | 55 ++- ...s_8967e03afb92eb0cac064520bf021ba2.content | 2 +- 14 files changed, 489 insertions(+), 23 deletions(-) create mode 100644 site/content/docs/howto/supervisor/configure-supervisor-federationdomain-idps.md diff --git a/site/content/docs/background/architecture.md b/site/content/docs/background/architecture.md index 0e62e886af..633fd8288c 100644 --- a/site/content/docs/background/architecture.md +++ b/site/content/docs/background/architecture.md @@ -17,7 +17,7 @@ environments with zero configuration. Pinniped is composed of two parts. 1. The Pinniped Supervisor is an OIDC server which allows users to authenticate -with an external identity provider (IDP), and then issues its own federation ID tokens +with external identity providers (IDP), and then issues its own federation ID tokens to be passed on to clusters based on the user information from the IDP. 1. The Pinniped Concierge is a credential exchange API which takes as input a credential from an identity source (e.g., Pinniped Supervisor, proprietary IDP), diff --git a/site/content/docs/howto/configure-auth-for-webapps.md b/site/content/docs/howto/configure-auth-for-webapps.md index e42a7a06ae..c7c68fc454 100644 --- a/site/content/docs/howto/configure-auth-for-webapps.md +++ b/site/content/docs/howto/configure-auth-for-webapps.md @@ -46,6 +46,11 @@ framework (e.g. Spring, Rails, Django, etc.) to implement authentication. The Su - Clients must use `query` as the [response_mode](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest) at the authorization endpoint, or not specify the `response_mode` param, which defaults to `query`. +- If the Supervisor's FederationDomain was configured with explicit `identityProviders` in its spec, then the + client must send an extra parameter on the initial authorization request to indicate which identity provider + the user would like to use when authenticating. This parameter is called `pinniped_idp_name` and the value + of the parameter should be set to the `displayName` of the identity provider as it was configured on the + FederationDomain. Most web application frameworks offer all these capabilities in their OAuth2/OIDC libraries. diff --git a/site/content/docs/howto/login.md b/site/content/docs/howto/login.md index 6103459022..886f78c304 100644 --- a/site/content/docs/howto/login.md +++ b/site/content/docs/howto/login.md @@ -76,6 +76,11 @@ One flag of note is `--upstream-identity-provider-flow browser_authcode` to choo (the default for OIDCIdentityProviders), and `--upstream-identity-provider-flow cli_password` to choose end-user `kubectl` login via CLI username/password prompts (the default for LDAPIdentityProviders and ActiveDirectoryIdentityProviders). +If the cluster is using a Pinniped Supervisor's FederationDomain to provide authentication services, +and if that FederationDomain allows multiple identity providers, then you will need to specify which identity provider +you would like to use in the resulting kubeconfig with the `--upstream-identity-provider-name` and/or `--upstream-identity-provider-type` flags. +You may call `pinniped get kubeconfig` multiple times to generate multiple kubeconfigs for the cluster. + ## Use the generated kubeconfig with `kubectl` to access the cluster A cluster user will typically be given a Pinniped-compatible kubeconfig by their cluster admin. They can use this kubeconfig diff --git a/site/content/docs/howto/supervisor/configure-supervisor-federationdomain-idps.md b/site/content/docs/howto/supervisor/configure-supervisor-federationdomain-idps.md new file mode 100644 index 0000000000..7242626815 --- /dev/null +++ b/site/content/docs/howto/supervisor/configure-supervisor-federationdomain-idps.md @@ -0,0 +1,411 @@ +--- +title: Configure Identity Providers (IDPs) on a FederationDomain +description: Learn how to use one or more identity providers, and identity transformations and policies, on a FederationDomain. +cascade: + layout: docs +menu: + docs: + name: IDPs on FederationDomains + weight: 20 + parent: howto-configure-supervisor +--- + +This guide explains how to associate one or more external identity providers (IDPs) with a FederationDomain. +It also details how to configure identity transformations and identity policies for those identity +providers. + +## Prerequisites + +This how-to guide assumes that you have already [installed the Pinniped Supervisor]({{< ref "install-supervisor" >}}) +and have already read the guide about how to [configure the Supervisor as an OIDC issuer]({{< ref "configure-supervisor" >}}). + +This guide focuses on the use of the `spec.identityProviders` setting on the +[FederationDomain](https://github.com/vmware-tanzu/pinniped/blob/main/generated/{{< latestcodegenversion >}}/README.adoc#federationdomain) +CRD. + +## Summary + +External identity providers may be configured in the Supervisor by creating OIDCIdentityProvider, +ActiveDirectoryIdentityProvider, or LDAPIdentityProvider resources in the same namespace as the Supervisor. + +There are two ways to configure which of these external identity providers shall be used by a FederationDomain. + +1. When there is no `spec.identityProviders` configured on a FederationDomain, then the FederationDomain will use + the one and only identity provider that is configured in the same namespace. This provides backwards compatibility + with older configurations of Supervisors from before the `spec.identityProviders` setting was added to the + FederationDomain CRD. There must be exactly one OIDCIdentityProvider, + ActiveDirectoryIdentityProvider, or LDAPIdentityProvider resource in the same namespace as the Supervisor. + If there are no identity provider resources, or if there are more than one, then the FederationDomain will + not allow any users to authenticate, and a error message will be shown in its `status`. + +2. When `spec.identityProviders` is explicitly configured on a FederationDomain, then the FederationDomain will + allow clients to use any of those identity providers to authenticate. In this case, you may optionally also configure + identity transformations and policies that the FederationDomain should apply to each of these identity providers + (see below for details). When using the `pinniped get kubeconfig` CLI command, you will need to choose for + which identity provider you would like to generate a kubeconfig. A cluster may have multiple kubeconfigs, + e.g. one for each identity provider. + +The remainder of this guide focuses on the second case, and describes the settings that may be used to explicitly +configure which identity providers are used, along with optional identity transformations and policies. + +## Configuring a FederationDomain's identity providers + +A user may authenticate to a FederationDomain using any of the IDPs configured in the FederationDomain's +`spec.identityProviders`. To add IDPs to this list, simply configure each as a reference to the type and name +of the resource. The identity provider resources must be in the same namespace where the Supervisor was +[installed]({{< ref "docs/howto/install-supervisor.md" >}}). + +Here is an example FederationDomain with two IDPs configured. + +```yaml +apiVersion: config.supervisor.pinniped.dev/v1alpha1 +kind: FederationDomain +metadata: + name: my-provider + # Must be in the same namespace where the Supervisor is installed. + namespace: pinniped-supervisor +spec: + issuer: https://my-issuer.example.com/any/path + tls: + secretName: my-tls-cert-secret + # Available identity providers are selected here... + identityProviders: + - displayName: ActiveDirectory for Admins + objectRef: + apiGroup: idp.supervisor.pinniped.dev + kind: ActiveDirectoryIdentityProvider + name: ad-for-admins + - displayName: Okta for Developers + objectRef: + apiGroup: idp.supervisor.pinniped.dev + kind: OIDCIdentityProvider + name: okta-for-developers +``` + +Now users may use either of the above identity providers to authenticate. You can create +kubeconfigs for both IDPs for each cluster by using `pinniped get kubeconfig` twice for +each cluster. + +## Important consideration when using multiple identity providers: conflicting usernames and group names + +When multiple identity providers are configured onto a FederationDomain, then a user may use any of those +providers to authenticate to that FederationDomain. Since an identity in Kubernetes is simply a username string +and a list of group name strings, it is very important to consider what will happen if two users each authenticate +to a FederationDomain using different identity providers. + +1. If two users are assigned the same username string as a result of authenticating, then those two users will be + considered the *same user* by Kubernetes, regardless of which IDP they came from. +2. If two users are assigned the same group name string as a result of authenticating, then those two users will be + considered to *both belong to same group* by Kubernetes, regardless of which IDP they came from. + +This may or may not be desirable. If the two identity providers are intended to represent distinct, non-overlapping +sets of users, then it is not desirable for their usernames and group names to conflict by being identical. +On the other hand, if two identity providers contain the same sets of users, and when those users authenticate +then your intention is that they are the same identity, then it may be desirable. + +Let's consider several example use cases. +- Imagine two IDPs in which usernames and group names are assigned to users by their administrators + separately, with no coordination between them. In this case, a user named "ryan" from one IDP is probably not + the same human as the user named "ryan" from the other IDP. The group named "admins" from one IDP may + have a totally different intended meaning then the group named "admins" from the other IDP. + In this case, it is not desirable for usernames and group names from one IDP to conflict with the usernames + and group names from the other IDP. +- Imagine two IDPs which both use corporate email addresses as usernames. These email addresses are + assigned by IT and are not adjustable by the individual users. In this case, it may be desirable for a human + user to be able to authenticate using either IDP and end up being assigned the same username in + Kubernetes clusters either way. However, it may or may not be the case that the user's group names from one IDP + are meant to represent the same Kubernetes groups as the same group names from the other IDP. + In this case, it is not a concern for usernames from one IDP to conflict with the usernames from the other IDP, + however it still might be a concern for group names from one IDP to conflict with the group names from the other IDP. + +You can easily add configuration to the FederationDomain to handle username and group name conflicts. +1. When it is not desirable for usernames to conflict, then a simple solution is to use identity transformations + to change all usernames to have a prefix which is unique to each IDP within that FederationDomain. For example, + usernames `ldap:ryan` and `gitlab:ryan` will be considered two different users by Kubernetes. +2. Similarly, when it is not desirable for group names to conflict, then a simple solution is to use identity transformations + to change all group names to have a prefix which is unique to each IDP within that FederationDomain. + +Refer to the next section to learn about how to configure identity transformations to add prefixes to usernames +and group names. + +## Identity transformations and policies + +When a user authenticates, the configuration of the OIDCIdentityProvider, ActiveDirectoryIdentityProvider, or +LDAPIdentityProvider resource determines how the user's username and group names will be extracted from the external +identity provider in a protocol-specific way (e.g. via OIDC ID token claims or LDAP record attributes). + +Then, operating on the username and group names extracted from the external IDP: +- Identity **transformations** can change either the user's username or group names. +- Identity **policies** can reject the user's authentication based on their username and/or groups. + +Identity transformations and policies are configured on the FederationDomain, so they are specific to that specific +FederationDomain's usage of the external identity provider. + +Transformations and policies are configured using the Common Expression Language (CEL) programming language. +They are configured as a list, and they will be executed in the order specified. The output of each transformation +or policy expression may impact the input values for the next expression from the list. + +Pinniped's implementation of CEL expressions includes the +[standard language features](https://github.com/google/cel-spec/blob/master/doc/langdef.md) +as well as [the string extensions](https://github.com/google/cel-go/tree/master/ext#strings). + +### Pipelines of identity transformation and policy `expressions` + +There are three types of transformation expressions: +- `username/v1` are expressions which may change the user's username. These expressions must return a string, + and the value of the string will be the user's username. Returning an empty string or a string that contains + only whitespace characters will cause an authentication error. Returning the value of the `username` variable + unmodified will leave the username unchanged. +- `groups/v1` are expressions which may change the group names of the groups to which the user belongs. + These expressions must return a list of strings. The returned list may be empty. The returned list will + be the names of the groups to which the user belongs. Returning the value of the `groups` variable + unmodified will leave the groups unchanged. +- `policy/v1` are expressions which may reject the user's authentication based on their username and/or groups. + These expressions must return a boolean. Returning true has no impact on the user's authentication + and will therefore allow the user's authentication to continue. Returning true also has no impact + on the username or group names. Returning false will the reject user's authentication + and the user will see the error message configured for that policy expression (or a default error message). + Rejecting a user's authentication prevents the user from authenticating into every cluster + which uses this FederationDomain for identity services. This happens before + Kubernetes RBAC policies are considered by the individual clusters. Therefore, this is a authentication-level + rejection, not an authorization check. + +All three transformation expression types are written using CEL expressions. They are declared as a list of transformations and policies. +Each time a user attempts to authenticate, and each time a user's session is automatically refreshed periodically, +the list is evaluated in the order that it was declared. +`username/v1` expressions may change the username that is passed to the next expressions. +`groups/v1` expressions may change the group names that are passed to the next expressions. +`policy/v1` expressions may halt the processing of further expressions when they reject the authentication. +Because each expression in the list can pass information to the following expressions via its return values, +the list of expressions acts like a "pipeline". +Any unexpected runtime evaluation errors (e.g. division by zero) cause the authentication to fail. + +The following variables are available to each expression, regardless of type: +- `username` is a string. The value will be the username of the user who is attempting to authenticate. + Its value will never be the empty string. The value of `username` may have been modified by the + previous `username/v1` transformations in the pipeline. +- `groups` is a list of strings. The value will be the list of group names to which + the user attempting to authenticate belongs. The list may be empty, meaning that the user does not belong to any groups. + The value of `groups` may have been modified by the previous `groups/v1` transformations in the pipeline. +- `strConst` contains the string constants declared for those transformations, and each string + constant can be referenced using its name e.g. a string constant called `x` can be referenced as `strConst.x` +- `strListConst` contains the list constants declared for those transformations, and each list + constant can be referenced using its name e.g. a list constant called `x` can be referenced as `strListConst.x` + +Each identity provider selected for use in a FederationDomain may declare its own list of expressions. +The expressions will only be applied when that FederationDomain uses that identity provider. + +### Transformation pipelines `constants` + +Rather than repeating the same special strings across multiple expressions, you may optionally configure +string constants and string list constants for your transformation pipeline. + +For example, if there is a special username or group name which will be used for comparisons in your expressions, +then you might like to declare it as a string constant. If there is a special list of usernames or group names +which will be used for comparisons then you might like to declare the list as a constant. + +Constants are available in every expression of the pipeline. + +### Transformation pipelines `examples` + +Because the pipelines of expressions may behave differently based on their inputs, you may also optionally configure +`examples` to demonstrate how a pipeline is expected to behave for various possible input scenarios. These examples +act as living documentation for your fellow administrators, and also act as unit tests for your CEL expression code. + +Each example declares inputs for the whole pipeline of expressions, and also declares the expected results of the +entire pipeline running on those inputs. The inputs are examples of the username and list of group names that might +be determined by the related OIDCIdentityProvider, ActiveDirectoryIdentityProvider, or LDAPIdentityProvider resource. +The expected outputs are the username and list of group names, or the authentication rejection, for which your pipeline +should result upon the given inputs. + +If any example does not behave as expected, Pinniped will mark the whole FederationDomain with an error in +its `status` and users will not be allowed to use the FederationDomain to authenticate until the error is corrected. + +### Putting it all together: an example of a transformation pipeline configuration + +The following example is contrived to demonstrate every feature of the `transforms` configuration +(constants, expressions, and examples). It is likely more complex than a typical configuration. + +Documentation for each of the fields shown below can be found in the API docs for the +[FederationDomain](https://github.com/vmware-tanzu/pinniped/blob/main/generated/{{< latestcodegenversion >}}/README.adoc#federationdomain) +CRD. + +```yaml +kind: FederationDomain +apiVersion: config.supervisor.pinniped.dev/v1alpha1 +metadata: + name: demo-federation-domain + namespace: pinniped-supervisor +spec: + issuer: https://issuer.example.com/demo-issuer + tls: + secretName: my-federation-domain-tls + identityProviders: + - displayName: ActiveDirectory for Admins + objectRef: + apiGroup: idp.supervisor.pinniped.dev + kind: ActiveDirectoryIdentityProvider + name: ad-for-admins + transforms: + constants: + - name: prefix + type: string + stringValue: "ad:" + - name: onlyIncludeGroupsWithThisPrefix + type: string + stringValue: "kube/" + - name: mustBelongToOneOfThese + type: stringList + stringListValue: + - "kube/admins" + - "kube/developers" + - "kube/auditors" + - name: additionalAdmins + type: stringList + stringListValue: + - "ryan@example.com" + - "ben@example.com" + - "josh@example.com" + expressions: + # This expression runs first, so it operates on the unmodified usernames + # and groups as extracted from AD by the ActiveDirectoryIdentityProvider. + # It rejects auth for any user who does not belong to certain groups. + # When it returns true, the pipeline continues. When it returns false, + # the pipeline stops and the auth is rejected. + - type: policy/v1 + expression: 'groups.exists(g, g in strListConst.mustBelongToOneOfThese)' + message: "Only users in kube groups are allowed to authenticate" + # This expression runs second, and the previous expression was a policy + # (which cannot change username or groups), so this expression also + # operates on the unmodified usernames and groups as extracted from the + # IDP. For certain users, this adds a new group to their list of groups. + - type: groups/v1 + expression: 'username in strListConst.additionalAdmins ? groups + ["kube/admins"] : groups' + # This expression runs next. Due to the expression above, this expression + # operates on the original username, and on a potentially changed list of + # groups. This drops all groups which do not start with a certain prefix. + - type: groups/v1 + expression: 'groups.filter(group, group.startsWith(strConst.onlyIncludeGroupsWithThisPrefix))' + # Due to the expressions above, this expression operates on the original + # username, and on a potentially changed list of groups. This + # unconditionally prefixes the username. + - type: username/v1 + expression: 'strConst.prefix + username' + # The expressions above have already changed the username and might have + # changed the groups before this expression runs. This unconditionally + # prefixes all group names. + - type: groups/v1 + expression: 'groups.map(group, strConst.prefix + group)' + examples: + - username: "ryan@example.com" + groups: [ "kube/developers", "kube/auditors", "non-kube-group" ] + expects: + username: "ad:ryan@example.com" + groups: [ "ad:kube/developers", "ad:kube/auditors", "ad:kube/admins" ] + - username: "someone_else@example.com" + groups: [ "kube/developers", "kube/other", "non-kube-group" ] + expects: + username: "ad:someone_else@example.com" + groups: [ "ad:kube/developers", "ad:kube/other" ] + - username: "paul@example.com" + groups: [ "kube/other", "non-kube-group" ] + expects: + rejected: true + message: "Only users in kube groups are allowed to authenticate" +``` + +### Some useful features of CEL + +Pinniped uses the cel-go library to implement CEL expressions. +It includes the CEL [standard language features](https://github.com/google/cel-spec/blob/master/doc/langdef.md) +as well as the [string extensions](https://github.com/google/cel-go/tree/master/ext#strings). +This section will attempt to highlight some of the useful features of CEL, but is not intended to +be a comprehensive overview of everything that you can use in CEL expressions. + +- CEL has several [built-in functions](https://github.com/google/cel-spec/blob/master/doc/langdef.md) which may be called on strings: + `contains`, `startsWith`, `endsWith`, and `matches` (for regex matching), e.g. `x.contains("some-substring")` for a string `x` +- The [string extensions](https://github.com/google/cel-go/tree/master/ext#strings) + include several additional functions which may be called on strings: + `charAt`, `indexOf`, `join`, `lastIndexOf`, `lowerAscii`, `quote`, `replace`, `split`, `substring`, + `trim`, `upperAscii`, and `reverse` +- CEL has [several useful functions which can be called on lists](https://github.com/google/cel-spec/blob/master/doc/langdef.md#macros): + - `map` and `filter` can be used to return a modified copy of a list + - `exists`, `exists_one`, and `all` can be used to perform boolean checks on the contents of a list +- Equality of strings and lists can be compared with the `==` and `!=` operators +- Lexicographic ordering of strings can be compared with `<`, `<=`, `>`, and `>=` operators +- Concatenation of two strings or two lists can be performed with the `+` operator +- List membership may be tested using the `in` operator, e.g. `"foo" in x` for a list `x` +- CEL does not have an `if` statement, but it does include a ternary operator to achieve the same result: + `boolean_expression ? when_true_expression : when_false_expression`. These may be nested, + e.g. the `when_true_expression` may itself be another ternary expression. +- Boolean operators include `&&` (and), `||` (or), `!` (not), `in` (inclusion in a list), and the ternary `?:` +- String literals can be quoted using single quotes `''` or double quotes `""` and [may contain quoted special characters](https://github.com/google/cel-spec/blob/master/doc/langdef.md#string-and-bytes-values) +- List literals can be written as a comma-seperated list of elements within enclosing `[]` +- `[]` may be used to index into a list, e.g. `x[4]` for a list `x` +- `size(x)` returns the length of a string `x` or the length of a list `x` + +### Example expressions + +Below are some examples of using expressions for identity transformations and policies. + +Note that any of the string literals in these examples could be replaced by string +constants, i.e. `"prefix"` could instead refer to a constant like `strConst.prefix`. +Any literal list of strings could be replaced by a string list constant, e.g. +`["allowed1", "allowed2"]` could instead refer to a constant like `strListConst.allowedGroups`. + +#### Example `username/v1` expressions + +- Prefix the username: + - `"prefix" + username` +- Suffix the username: + - `username + "suffix"` +- Down-case the username (be careful that this will cause "Ryan" and "ryan" to become the same user in Kubernetes): + - `username.lowerAscii()` + +#### Example `groups/v1` expressions + +- Prefix all group names: + - `groups.map(g, "prefix" + g)` +- Suffix all group names: + - `groups.map(g, g + "suffix")` +- Filter groups to remove any group names that start with `system:` which is a prefix that has a special meaning to Kubernetes: + - `groups.filter(group, !group.startsWith("system:"))` +- Filter groups to keep only groups with a certain prefix: + - `groups.filter(group, group.startsWith("kube/"))` +- Down-case all group names (be careful that this will cause "Admins" and "admins" to become the same group in Kubernetes): + - `groups.map(g, g.lowerAscii())` +- Filter groups based on an allow list, dropping any group names except those included in the allow list: + - `groups.filter(g, g in ["allowed1", "allowed2"])` +- Filter groups based on a disallow list, dropping any group names included in the disallow list: + - `groups.filter(g, !(g in ["dropped1", "dropped2"]))` +- Filter groups based on a list of disallowed prefixes, dropping any groups which have one of the disallowed prefixes: + - `groups.filter(group, !(["disallowed-prefix1:", "disallowed-prefix2:"].exists(prefix, group.startsWith(prefix))))` +- Unconditionally add a group: + - `groups + ["new-group"]` +- Add a group, but only if the user already belongs to another specific group: + - `"other" in groups ? groups + ["new-group"] : groups` +- Rename a particular group if the user belongs to that group: + - `groups.map(g, g == "other" ? "other-renamed" : g)` +- Unconditionally drop all groups: + - `[]` + +#### Example `policy/v1` expressions + +- User must belong to a particular group: + - `"required-group" in groups` +- User must belong to at least one of the groups in a list: + - `groups.exists(g, g in ["foobar", "foobaz", "foobat"])` +- User must belong to all the groups in a list: + - `["foobar", "foobaz", "foobat"].all(g, g in groups)` +- User must not belong to any of the groups in a list: + - `!groups.exists(g, g in ["foobar", "foobaz"])` +- Certain users are allowed to authenticate and everyone else is rejected: + - `username in ["foobar", "foobaz"]` +- Certain users are not allowed to authenticate: + - `!(username in ["foobar", "foobaz"])` + +## Next steps + +Next, +[configure the Concierge to use the Supervisor for authentication]({{< ref "configure-concierge-supervisor-jwt" >}}) +on each cluster. diff --git a/site/content/docs/howto/supervisor/configure-supervisor-with-activedirectory.md b/site/content/docs/howto/supervisor/configure-supervisor-with-activedirectory.md index 149df7fb03..ca48fc1b59 100644 --- a/site/content/docs/howto/supervisor/configure-supervisor-with-activedirectory.md +++ b/site/content/docs/howto/supervisor/configure-supervisor-with-activedirectory.md @@ -11,8 +11,8 @@ menu: aliases: - /docs/howto/configure-supervisor-with-activedirectory/ --- -The Supervisor is an [OpenID Connect (OIDC)](https://openid.net/connect/) issuer that supports connecting a single -"upstream" identity provider to many "downstream" cluster clients. +The Supervisor is an [OpenID Connect (OIDC)](https://openid.net/connect/) issuer that supports connecting +"upstream" identity providers to many "downstream" cluster clients. This guide shows you how to configure the Supervisor so that users can authenticate to their Kubernetes cluster using their identity from Active Directory. diff --git a/site/content/docs/howto/supervisor/configure-supervisor-with-auth0.md b/site/content/docs/howto/supervisor/configure-supervisor-with-auth0.md index 094012cdd2..52effc3b5d 100644 --- a/site/content/docs/howto/supervisor/configure-supervisor-with-auth0.md +++ b/site/content/docs/howto/supervisor/configure-supervisor-with-auth0.md @@ -11,8 +11,8 @@ menu: aliases: - /docs/howto/configure-supervisor-with-auth0/ --- -The Supervisor is an [OpenID Connect (OIDC)](https://openid.net/connect/) issuer that supports connecting a single -"upstream" identity provider to many "downstream" cluster clients. +The Supervisor is an [OpenID Connect (OIDC)](https://openid.net/connect/) issuer that supports connecting +"upstream" identity providers to many "downstream" cluster clients. This guide shows you how to configure the Supervisor so that users can authenticate to their Kubernetes cluster using their Auth0 credentials. diff --git a/site/content/docs/howto/supervisor/configure-supervisor-with-dex.md b/site/content/docs/howto/supervisor/configure-supervisor-with-dex.md index d1e4f22965..afb81a34d7 100644 --- a/site/content/docs/howto/supervisor/configure-supervisor-with-dex.md +++ b/site/content/docs/howto/supervisor/configure-supervisor-with-dex.md @@ -12,8 +12,8 @@ aliases: - /docs/howto/configure-supervisor-with-dex/ --- -The Supervisor is an [OpenID Connect (OIDC)](https://openid.net/connect/) issuer that supports connecting a single -"upstream" identity provider to many "downstream" cluster clients. +The Supervisor is an [OpenID Connect (OIDC)](https://openid.net/connect/) issuer that supports connecting +"upstream" identity providers to many "downstream" cluster clients. This guide shows you how to configure the Supervisor so that users can authenticate to their Kubernetes cluster using Dex and Github. diff --git a/site/content/docs/howto/supervisor/configure-supervisor-with-gitlab.md b/site/content/docs/howto/supervisor/configure-supervisor-with-gitlab.md index 92afc9c086..e1e5ad0817 100644 --- a/site/content/docs/howto/supervisor/configure-supervisor-with-gitlab.md +++ b/site/content/docs/howto/supervisor/configure-supervisor-with-gitlab.md @@ -11,8 +11,8 @@ menu: aliases: - /docs/howto/configure-supervisor-with-gitlab/ --- -The Supervisor is an [OpenID Connect (OIDC)](https://openid.net/connect/) issuer that supports connecting a single -"upstream" identity provider to many "downstream" cluster clients. +The Supervisor is an [OpenID Connect (OIDC)](https://openid.net/connect/) issuer that supports connecting +"upstream" identity providers to many "downstream" cluster clients. This guide shows you how to configure the Supervisor so that users can authenticate to their Kubernetes cluster using their GitLab credentials. diff --git a/site/content/docs/howto/supervisor/configure-supervisor-with-jumpcloudldap.md b/site/content/docs/howto/supervisor/configure-supervisor-with-jumpcloudldap.md index b24af28d6b..313824bf63 100644 --- a/site/content/docs/howto/supervisor/configure-supervisor-with-jumpcloudldap.md +++ b/site/content/docs/howto/supervisor/configure-supervisor-with-jumpcloudldap.md @@ -11,8 +11,8 @@ menu: aliases: - /docs/howto/configure-supervisor-with-jumpcloudldap/ --- -The Supervisor is an [OpenID Connect (OIDC)](https://openid.net/connect/) issuer that supports connecting a single -"upstream" identity provider to many "downstream" cluster clients. +The Supervisor is an [OpenID Connect (OIDC)](https://openid.net/connect/) issuer that supports connecting +"upstream" identity providers to many "downstream" cluster clients. [JumpCloud](https://jumpcloud.com) is a cloud-based service which bills itself as "a comprehensive and flexible cloud directory platform". It includes the capability to act as an LDAP identity provider. diff --git a/site/content/docs/howto/supervisor/configure-supervisor-with-okta.md b/site/content/docs/howto/supervisor/configure-supervisor-with-okta.md index 4c4e16c915..53f6cf18db 100644 --- a/site/content/docs/howto/supervisor/configure-supervisor-with-okta.md +++ b/site/content/docs/howto/supervisor/configure-supervisor-with-okta.md @@ -11,8 +11,8 @@ menu: aliases: - /docs/howto/configure-supervisor-with-okta/ --- -The Supervisor is an [OpenID Connect (OIDC)](https://openid.net/connect/) issuer that supports connecting a single -"upstream" identity provider to many "downstream" cluster clients. +The Supervisor is an [OpenID Connect (OIDC)](https://openid.net/connect/) issuer that supports connecting +"upstream" identity providers to many "downstream" cluster clients. This guide shows you how to configure the Supervisor so that users can authenticate to their Kubernetes cluster using their Okta credentials. diff --git a/site/content/docs/howto/supervisor/configure-supervisor-with-openldap.md b/site/content/docs/howto/supervisor/configure-supervisor-with-openldap.md index 010fccb4c1..d6e70c45c6 100644 --- a/site/content/docs/howto/supervisor/configure-supervisor-with-openldap.md +++ b/site/content/docs/howto/supervisor/configure-supervisor-with-openldap.md @@ -11,8 +11,8 @@ menu: aliases: - /docs/howto/configure-supervisor-with-openldap/ --- -The Supervisor is an [OpenID Connect (OIDC)](https://openid.net/connect/) issuer that supports connecting a single -"upstream" identity provider to many "downstream" cluster clients. +The Supervisor is an [OpenID Connect (OIDC)](https://openid.net/connect/) issuer that supports connecting +"upstream" identity providers to many "downstream" cluster clients. [OpenLDAP](https://www.openldap.org) is a popular open source LDAP server for Linux/UNIX. diff --git a/site/content/docs/howto/supervisor/configure-supervisor-with-workspace_one_access.md b/site/content/docs/howto/supervisor/configure-supervisor-with-workspace_one_access.md index d87af48be6..7b7455295b 100644 --- a/site/content/docs/howto/supervisor/configure-supervisor-with-workspace_one_access.md +++ b/site/content/docs/howto/supervisor/configure-supervisor-with-workspace_one_access.md @@ -11,8 +11,8 @@ menu: aliases: - /docs/howto/configure-supervisor-with-workspace_one_access/ --- -The Supervisor is an [OpenID Connect (OIDC)](https://openid.net/connect/) issuer that supports connecting a single -"upstream" identity provider to many "downstream" cluster clients. +The Supervisor is an [OpenID Connect (OIDC)](https://openid.net/connect/) issuer that supports connecting +"upstream" identity providers to many "downstream" cluster clients. This guide shows you how to configure the Supervisor so that users can authenticate to their Kubernetes cluster using their [Workspace ONE Access](https://www.vmware.com/products/workspace-one/access.html) credentials. diff --git a/site/content/docs/howto/supervisor/configure-supervisor.md b/site/content/docs/howto/supervisor/configure-supervisor.md index 00b7e15cde..ef7d778306 100644 --- a/site/content/docs/howto/supervisor/configure-supervisor.md +++ b/site/content/docs/howto/supervisor/configure-supervisor.md @@ -12,8 +12,8 @@ aliases: - /docs/howto/configure-supervisor/ --- -The Supervisor is an [OpenID Connect (OIDC)](https://openid.net/connect/) issuer that supports connecting a single -"upstream" identity provider to many "downstream" cluster clients. When a user authenticates, the Supervisor can issue +The Supervisor is an [OpenID Connect (OIDC)](https://openid.net/connect/) issuer that supports connecting +"upstream" identity providers to many "downstream" cluster clients. When a user authenticates, the Supervisor can issue [JSON Web Tokens (JWTs)](https://tools.ietf.org/html/rfc7519) that can be [validated by the Pinniped Concierge]({{< ref "configure-concierge-jwt" >}}). This guide explains how to expose the Supervisor's REST endpoints to clients. @@ -246,10 +246,54 @@ spec: # for the HTTPS endpoints served by this OIDC Provider. tls: secretName: my-tls-cert-secret + # Configure which identity providers (OIDCIdentityProvider, + # ActiveDirectoryIdentityProvider, or LDAPIdentityProvider) + # shall be used by this FederationDomain. + identityProviders: + # See the "IDPs on FederationDomains" Supervisor configuration guide + # for details on how to configure this section of the FederationDomain's + # spec. ``` -You can create multiple FederationDomains as long as each has a unique issuer string. +### How FederationDomains Work + Each FederationDomain can be used to provide access to a set of Kubernetes clusters for a set of user identities. +A FederationDomain will allow the same set of users to authenticate into all Kubernetes clusters +which choose to trust that FederationDomain to provide authentication services. Authenticating to a FederationDomain +will only provide access to those clusters which choose to trust that FederationDomain, and will not provide access +to any clusters that are trusting other FederationDomains. Therefore, FederationDomains are a means of providing +authentication isolation. + +You can create multiple FederationDomains in a single Pinniped Supervisor, as long as each has a unique issuer string. + +When a user authenticates into any cluster that chooses to trust the FederationDomain for authentication, +then the user has started a single sign-on session with that FederationDomain which will last approximately 9 hours. +The user will not need to manually authenticate again into any cluster any cluster that chooses to trust the +FederationDomain for authentication until the session expires. Behind the scenes, the user's client (e.g. `kubectl`) +will only be given short-lived access to each cluster (approximately 5 minutes), but will automatically refresh those +credentials without the need for user interaction. During each refresh, the Supervisor will perform checks against +the external identity provider to determine if the user's session should be allowed to continue. +A user may have active sessions with multiple FederationDomains at the same time, but each will require authenticating +separately to start the single sign-on session with that FederationDomain. + +Technically, each FederationDomain is a separate OIDC issuer which serves multiple REST endpoints to clients, such +as the Pinniped CLI. The tokens issued by each FederationDomain are signed by that FederationDomain and cannot +be used by any other FederationDomain, which provides the isolation properties of the FederationDomain concept. + +Any Kubernetes cluster can choose to trust a FederationDomain to provide user authentication +for that cluster by using the FederationDomain its OIDC issuer for JSON Web Token (JWT) formatted ID tokens. +This can be done by installing the Pinniped Concierge on each cluster and +[configuring the Concierge to use the Supervisor for authentication]({{< ref "configure-concierge-supervisor-jwt" >}}) +on each cluster. Alternatively, it can be done by [configuring the Kubernetes API server on each cluster to use +the FederationDomain as its OIDC issuer]({{< ref "../../tutorials/supervisor-without-concierge-demo" >}}). + +When two FederationDomains use the same hostname in the `spec.issuer`, then: +1. They must use different paths in their `spec.issuer` URLs to differentiate them from each other. +2. They must use the same `tls.secretName` if they both configure a `tls.secretName` (see below for details of TLS configuration). + +The Supervisor uses the hostname on the each incoming request to determine which TLS certificate to serve for that +request, and then it uses the path to determine which FederationDomain should serve the request if there are multiple +FederationDomains with the same hostname. ### Configuring TLS for the Supervisor OIDC endpoints @@ -295,6 +339,7 @@ should be signed by a certificate authority that is trusted by their browsers. ## Next steps Next, configure an OIDCIdentityProvider, ActiveDirectoryIdentityProvider, or an LDAPIdentityProvider for the Supervisor -(several examples are available in these guides). Then +(several examples are available in these guides). Then learn [how to configure a FederationDomain to use one or more +identity providers]({{< ref "configure-supervisor-federationdomain-idps" >}}). And finally, [configure the Concierge to use the Supervisor for authentication]({{< ref "configure-concierge-supervisor-jwt" >}}) -on each cluster! +on each cluster. diff --git a/site/resources/_gen/assets/scss/scss/site.scss_8967e03afb92eb0cac064520bf021ba2.content b/site/resources/_gen/assets/scss/scss/site.scss_8967e03afb92eb0cac064520bf021ba2.content index 05f412690d..ea2c0185b1 100644 --- a/site/resources/_gen/assets/scss/scss/site.scss_8967e03afb92eb0cac064520bf021ba2.content +++ b/site/resources/_gen/assets/scss/scss/site.scss_8967e03afb92eb0cac064520bf021ba2.content @@ -1,3 +1,3 @@ -body{font-family:"Metropolis-Light",Helvetica,sans-serif;margin:0px;line-height:1.25}.wrapper{max-width:980px;margin:0px auto;padding:20px}@media only screen and (max-width: 767px){.wrapper{max-width:100%}}.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;content:"";line-height:0}.clearfix:after{clear:both}h1,h2,h3,h4,h5,h6{font-weight:300}h1{font-size:36px}h2{font-size:28px}h3{font-size:22px}h4{font-size:18px}li{list-style-type:none;display:inline;padding-right:25px;font-size:14px;line-height:1.6em}li:last-of-type{padding-right:0px}p{line-height:1.7em;font-weight:300;font-size:16px;color:#333}a{font-size:16px;text-decoration:none;color:#0095D3;font-family:"Metropolis-Medium",Helvetica,sans-serif}button{background-color:unset;border:none}.button{color:#0095D3;font-size:12px;font-weight:600;background-color:#fff;border-radius:3px;padding:14px 10px;min-width:200px;text-transform:uppercase;border:1px solid #fff}.button.secondary{background-color:#0091DA;color:#fff}.button.tertiary{border:1px solid #0095D3}.button img.button-icon{margin-left:5px;margin-right:5px;margin-bottom:-8px;width:24px;height:24px}.buttons{margin-top:40px}.buttons .button:first-of-type{margin-right:30px}@media only screen and (max-width: 767px){.buttons .button:first-of-type{margin:0px 0px 20px 0px}}.strong{font-family:"Metropolis-Medium",Helvetica,sans-serif}.bg-grey{background-color:#F2F2F2}.grid.three{display:grid;grid-template-columns:1fr 1fr 1fr;row-gap:20px;column-gap:20px}@media only screen and (max-width: 767px){.grid.three{grid-template-columns:1fr}}.grid.two{display:grid;grid-template-columns:1fr 1fr}@media only screen and (max-width: 767px){.grid.two{grid-template-columns:1fr}}@font-face{font-family:"Metropolis-Bold";src:url("/fonts/Metropolis-Bold.eot");src:url("/fonts/Metropolis-Bold.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-Bold.woff2") format("woff2"),url("/fonts/Metropolis-Bold.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-BoldItalic";src:url("/fonts/Metropolis-BoldItalic.eot");src:url("/fonts/Metropolis-BoldItalic.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-BoldItalic.woff2") format("woff2"),url("/fonts/Metropolis-BoldItalic.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-Light";src:url("/fonts/Metropolis-Light.eot");src:url("/fonts/Metropolis-Light.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-Light.woff2") format("woff2"),url("/fonts/Metropolis-Light.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-LightItalic";src:url("/fonts/Metropolis-LightItalic.eot");src:url("/fonts/Metropolis-LightItalic.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-LightItalic.woff2") format("woff2"),url("/fonts/Metropolis-LightItalic.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-Regular";src:url("/fonts/Metropolis-Regular.eot");src:url("/fonts/Metropolis-Regular.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-Regular.woff2") format("woff2"),url("/fonts/Metropolis-Regular.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-RegularItalic";src:url("/fonts/Metropolis-RegularItalic.eot");src:url("/fonts/Metropolis-RegularItalic.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-RegularItalic.woff2") format("woff2"),url("/fonts/Metropolis-RegularItalic.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-Medium";src:url("/fonts/Metropolis-Medium.eot");src:url("/fonts/Metropolis-Medium.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-Medium.woff2") format("woff2"),url("/fonts/Metropolis-Medium.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-MediumItalic";src:url("/fonts/Metropolis-MediumItalic.eot");src:url("/fonts/Metropolis-MediumItalic.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-MediumItalic.woff2") format("woff2"),url("/fonts/Metropolis-MediumItalic.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-SemiBold";src:url("/fonts/Metropolis-SemiBold.eot");src:url("/fonts/Metropolis-SemiBold.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-SemiBold.woff2") format("woff2"),url("/fonts/Metropolis-SemiBold.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-SemiBoldItalic";src:url("/fonts/Metropolis-SemiBoldItalic.eot");src:url("/fonts/Metropolis-SemiBoldItalic.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-SemiBoldItalic.woff2") format("woff2"),url("/fonts/Metropolis-SemiBoldItalic.woff") format("woff");font-weight:normal;font-style:normal}header .wrapper{padding:10px 20px}header .desktop-links{float:right;margin:15px 0px 0px 0px;padding-left:0px}header a{color:#333;font-family:"Metropolis-Light",Helvetica,sans-serif}header a.active{font-family:"Metropolis-Medium",Helvetica,sans-serif}header li img{vertical-align:bottom;margin-right:10px}header .mobile{display:none}@media only screen and (min-width: 768px) and (max-width: 1279px){header .desktop-links li{padding-right:10px}}@media only screen and (max-width: 767px){header{position:relative}header .expanded-icon{display:none;padding:11px 3px 0px 0px}header .collapsed-icon{padding-top:12px}header .mobile-menu-visible .mobile{display:block}header .mobile-menu-visible .mobile .collapsed-icon{display:none}header .mobile-menu-visible .mobile .expanded-icon{display:block}header .desktop-links{display:none}header .mobile{display:block}header button{float:right}header button:focus{outline:none}header ul{padding-left:0px}header ul li{display:block;margin:20px 0px}header .mobile-menu{position:absolute;background-color:#fff;width:100%;top:70px;left:0px;padding-bottom:20px;display:none}header .mobile-menu .header-links{margin:0px 20px}header .mobile-menu .social{margin:0px 20px;padding-top:20px}header .mobile-menu .social img{vertical-align:middle;padding-right:10px}header .mobile-menu .social a{font-size:14px;padding-right:35px}header .mobile-menu .social a:last-of-type{padding-right:0px}}body{font-family:"Metropolis-Light",Helvetica,sans-serif;margin:0px;line-height:1.25}.wrapper{max-width:980px;margin:0px auto;padding:20px}@media only screen and (max-width: 767px){.wrapper{max-width:100%}}.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;content:"";line-height:0}.clearfix:after{clear:both}h1,h2,h3,h4,h5,h6{font-weight:300}h1{font-size:36px}h2{font-size:28px}h3{font-size:22px}h4{font-size:18px}li{list-style-type:none;display:inline;padding-right:25px;font-size:14px;line-height:1.6em}li:last-of-type{padding-right:0px}p{line-height:1.7em;font-weight:300;font-size:16px;color:#333}a{font-size:16px;text-decoration:none;color:#0095D3;font-family:"Metropolis-Medium",Helvetica,sans-serif}button{background-color:unset;border:none}.button{color:#0095D3;font-size:12px;font-weight:600;background-color:#fff;border-radius:3px;padding:14px 10px;min-width:200px;text-transform:uppercase;border:1px solid #fff}.button.secondary{background-color:#0091DA;color:#fff}.button.tertiary{border:1px solid #0095D3}.button img.button-icon{margin-left:5px;margin-right:5px;margin-bottom:-8px;width:24px;height:24px}.buttons{margin-top:40px}.buttons .button:first-of-type{margin-right:30px}@media only screen and (max-width: 767px){.buttons .button:first-of-type{margin:0px 0px 20px 0px}}.strong{font-family:"Metropolis-Medium",Helvetica,sans-serif}.bg-grey{background-color:#F2F2F2}.grid.three{display:grid;grid-template-columns:1fr 1fr 1fr;row-gap:20px;column-gap:20px}@media only screen and (max-width: 767px){.grid.three{grid-template-columns:1fr}}.grid.two{display:grid;grid-template-columns:1fr 1fr}@media only screen and (max-width: 767px){.grid.two{grid-template-columns:1fr}}@font-face{font-family:"Metropolis-Bold";src:url("/fonts/Metropolis-Bold.eot");src:url("/fonts/Metropolis-Bold.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-Bold.woff2") format("woff2"),url("/fonts/Metropolis-Bold.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-BoldItalic";src:url("/fonts/Metropolis-BoldItalic.eot");src:url("/fonts/Metropolis-BoldItalic.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-BoldItalic.woff2") format("woff2"),url("/fonts/Metropolis-BoldItalic.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-Light";src:url("/fonts/Metropolis-Light.eot");src:url("/fonts/Metropolis-Light.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-Light.woff2") format("woff2"),url("/fonts/Metropolis-Light.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-LightItalic";src:url("/fonts/Metropolis-LightItalic.eot");src:url("/fonts/Metropolis-LightItalic.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-LightItalic.woff2") format("woff2"),url("/fonts/Metropolis-LightItalic.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-Regular";src:url("/fonts/Metropolis-Regular.eot");src:url("/fonts/Metropolis-Regular.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-Regular.woff2") format("woff2"),url("/fonts/Metropolis-Regular.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-RegularItalic";src:url("/fonts/Metropolis-RegularItalic.eot");src:url("/fonts/Metropolis-RegularItalic.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-RegularItalic.woff2") format("woff2"),url("/fonts/Metropolis-RegularItalic.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-Medium";src:url("/fonts/Metropolis-Medium.eot");src:url("/fonts/Metropolis-Medium.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-Medium.woff2") format("woff2"),url("/fonts/Metropolis-Medium.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-MediumItalic";src:url("/fonts/Metropolis-MediumItalic.eot");src:url("/fonts/Metropolis-MediumItalic.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-MediumItalic.woff2") format("woff2"),url("/fonts/Metropolis-MediumItalic.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-SemiBold";src:url("/fonts/Metropolis-SemiBold.eot");src:url("/fonts/Metropolis-SemiBold.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-SemiBold.woff2") format("woff2"),url("/fonts/Metropolis-SemiBold.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-SemiBoldItalic";src:url("/fonts/Metropolis-SemiBoldItalic.eot");src:url("/fonts/Metropolis-SemiBoldItalic.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-SemiBoldItalic.woff2") format("woff2"),url("/fonts/Metropolis-SemiBoldItalic.woff") format("woff");font-weight:normal;font-style:normal}footer .wrapper{padding:20px}footer li{list-style-type:none;display:inline;padding-right:25px;font-size:12px}footer li:last-of-type{padding-right:0px}footer .top-links{min-height:52px;display:flex;align-items:center;justify-content:space-between}footer .left-links{padding:0px}footer .left-links li img{vertical-align:bottom;margin-right:10px}footer .left-links li a{color:#333;font-weight:300;font-size:12px;font-family:"Metropolis-Light",Helvetica,sans-serif}footer .left-links .mobile{display:none}footer .right-links p{margin:0px}footer .right-links .copywrite{font-size:12px;padding-right:10px}footer .right-links .copywrite a{font-size:12px;color:#333;font-family:"Metropolis-Light",Helvetica,sans-serif}footer .right-links a{vertical-align:middle}footer .bottom-links{margin:10px 0px 30px 0px}footer .bottom-links p{font-size:12px;display:flex;justify-content:space-between;flex-wrap:wrap}footer .bottom-links p .ot-sdk-show-settings{cursor:pointer}footer .bottom-links a{font-size:12px;font-family:"Metropolis-Light",Helvetica,sans-serif}footer .bottom-links img{max-width:75px;vertical-align:middle;margin-left:30px}@media only screen and (max-width: 767px){footer .footer-links{display:block}footer .footer-links .right-links{display:none}footer .footer-links .left-links{float:none;margin:10px 0px}footer .footer-links .left-links .desktop{display:none}footer .footer-links .left-links .mobile{display:inline}footer .footer-links .left-links .copywrite{display:block;margin-top:20px}footer .bottom-links{margin:10px 0px 20px 0px;float:none}footer .bottom-links img{margin-left:0px;display:block;margin-top:10px}}body{font-family:"Metropolis-Light",Helvetica,sans-serif;margin:0px;line-height:1.25}.wrapper{max-width:980px;margin:0px auto;padding:20px}@media only screen and (max-width: 767px){.wrapper{max-width:100%}}.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;content:"";line-height:0}.clearfix:after{clear:both}h1,h2,h3,h4,h5,h6{font-weight:300}h1{font-size:36px}h2{font-size:28px}h3{font-size:22px}h4{font-size:18px}li{list-style-type:none;display:inline;padding-right:25px;font-size:14px;line-height:1.6em}li:last-of-type{padding-right:0px}p{line-height:1.7em;font-weight:300;font-size:16px;color:#333}a{font-size:16px;text-decoration:none;color:#0095D3;font-family:"Metropolis-Medium",Helvetica,sans-serif}button{background-color:unset;border:none}.button{color:#0095D3;font-size:12px;font-weight:600;background-color:#fff;border-radius:3px;padding:14px 10px;min-width:200px;text-transform:uppercase;border:1px solid #fff}.button.secondary{background-color:#0091DA;color:#fff}.button.tertiary{border:1px solid #0095D3}.button img.button-icon{margin-left:5px;margin-right:5px;margin-bottom:-8px;width:24px;height:24px}.buttons{margin-top:40px}.buttons .button:first-of-type{margin-right:30px}@media only screen and (max-width: 767px){.buttons .button:first-of-type{margin:0px 0px 20px 0px}}.strong{font-family:"Metropolis-Medium",Helvetica,sans-serif}.bg-grey{background-color:#F2F2F2}.grid.three{display:grid;grid-template-columns:1fr 1fr 1fr;row-gap:20px;column-gap:20px}@media only screen and (max-width: 767px){.grid.three{grid-template-columns:1fr}}.grid.two{display:grid;grid-template-columns:1fr 1fr}@media only screen and (max-width: 767px){.grid.two{grid-template-columns:1fr}}@font-face{font-family:"Metropolis-Bold";src:url("/fonts/Metropolis-Bold.eot");src:url("/fonts/Metropolis-Bold.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-Bold.woff2") format("woff2"),url("/fonts/Metropolis-Bold.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-BoldItalic";src:url("/fonts/Metropolis-BoldItalic.eot");src:url("/fonts/Metropolis-BoldItalic.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-BoldItalic.woff2") format("woff2"),url("/fonts/Metropolis-BoldItalic.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-Light";src:url("/fonts/Metropolis-Light.eot");src:url("/fonts/Metropolis-Light.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-Light.woff2") format("woff2"),url("/fonts/Metropolis-Light.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-LightItalic";src:url("/fonts/Metropolis-LightItalic.eot");src:url("/fonts/Metropolis-LightItalic.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-LightItalic.woff2") format("woff2"),url("/fonts/Metropolis-LightItalic.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-Regular";src:url("/fonts/Metropolis-Regular.eot");src:url("/fonts/Metropolis-Regular.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-Regular.woff2") format("woff2"),url("/fonts/Metropolis-Regular.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-RegularItalic";src:url("/fonts/Metropolis-RegularItalic.eot");src:url("/fonts/Metropolis-RegularItalic.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-RegularItalic.woff2") format("woff2"),url("/fonts/Metropolis-RegularItalic.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-Medium";src:url("/fonts/Metropolis-Medium.eot");src:url("/fonts/Metropolis-Medium.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-Medium.woff2") format("woff2"),url("/fonts/Metropolis-Medium.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-MediumItalic";src:url("/fonts/Metropolis-MediumItalic.eot");src:url("/fonts/Metropolis-MediumItalic.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-MediumItalic.woff2") format("woff2"),url("/fonts/Metropolis-MediumItalic.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-SemiBold";src:url("/fonts/Metropolis-SemiBold.eot");src:url("/fonts/Metropolis-SemiBold.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-SemiBold.woff2") format("woff2"),url("/fonts/Metropolis-SemiBold.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-SemiBoldItalic";src:url("/fonts/Metropolis-SemiBoldItalic.eot");src:url("/fonts/Metropolis-SemiBoldItalic.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-SemiBoldItalic.woff2") format("woff2"),url("/fonts/Metropolis-SemiBoldItalic.woff") format("woff");font-weight:normal;font-style:normal}code{background:#efefef;padding:2px 4px;font-size:85%}pre code{background:none;font-size:14px;line-height:1.6em}.highlight pre codesite/sidebar/reorganize{font-size:100%}.hero{background-color:#0091DA;color:#fff}.hero .text-block{max-width:550px;padding:0px 0px 10px 0px}.hero .text-block p{margin-bottom:20px;font-size:18px;color:#fff}.hero h2{font-size:36px}.hero.homepage{background-image:url(/img/hero-image.png);background-position:center center;background-repeat:no-repeat;background-size:cover;padding-bottom:80px}@media only screen and (max-width: 767px){.hero .text-block{max-width:unset;margin-right:0px}.hero .button{display:block;text-align:center}.hero.homepage{background-image:none}}.grid-container{margin-top:-80px}.grid-container .grid.three{padding-bottom:20px}.grid-container .grid.three .card{position:relative;padding:30px 20px;background-color:#fff;text-align:center;box-shadow:0px 2px 10px rgba(0,0,0,0.2)}.grid-container .grid.three .card h3{color:#333;font-size:22px}.grid-container .grid.three .card p{color:#333}.introduction .grid.two{column-gap:140px;padding:35px 20px}.introduction .grid.two p{margin:0px;font-size:16px}.introduction .grid.two p.strong{color:#333}@media only screen and (max-width: 767px){.introduction{padding:0px 20px}.introduction .col:first-of-type{padding-bottom:50px}}.use-cases .grid{grid-template-columns:220px 1fr;margin-bottom:30px;grid-template-areas:"image text"}.use-cases .grid .image{background-color:#0091DA;text-align:center;display:flex;align-items:center;justify-content:center;grid-area:image}.use-cases .grid .image img{justify-self:center}.use-cases .grid .text{border:1px solid #F2F2F2;padding:30px;grid-area:text}.use-cases .grid .text a.button{display:block;max-width:138px;text-align:center;padding:5px 10px;min-width:unset}.use-cases .grid.image-right{grid-template-columns:1fr 220px;grid-template-areas:"text image"}@media only screen and (max-width: 767px){.use-cases .grid.image-right{grid-template-columns:1fr;grid-template-areas:"image" "text"}}@media only screen and (max-width: 767px){.use-cases .grid{grid-template-columns:1fr;grid-template-rows:minmax(160px, 1fr);grid-template-areas:"image" "text"}}.use-cases h2{color:#111}.use-cases p.strong{color:#1B3951;font-size:16px}.team{background-color:#1D428A}.team h2,.team h3,.team p{color:#fff}.team p{font-size:16px}.team a{color:#fff;font-weight:300;text-decoration:underline}.team .grid.three{row-gap:40px;margin:40px 0px}.team .bio{display:grid;grid-template-columns:120px 1fr;column-gap:20px}.team .bio .info{align-self:center}.team .bio .info p{margin:0px}.team .bio .info p.name{font-size:16px;font-family:"Metropolis-Medium",Helvetica,sans-serif}.team .bio .info p.position{font-size:14px}.hero.subpage{background-image:url(/img/blog-hero-image.png);background-position:center center;background-repeat:no-repeat;background-size:cover;padding-bottom:140px}.experimental .grid.three .col{padding:0px}.experimental .icon{background-color:#0091DA;padding:25px;min-height:95px;display:flex;align-items:center;justify-content:center}.experimental .content{padding:25px}.experimental .content .example{background-color:#F2F2F2}.blog{padding-bottom:50px}.blog .col{border:1px solid #F2F2F2}.blog .col img{width:100%}.blog .col .content{padding:0px 20px}.blog.landing{background-color:#fff;margin-top:-90px}.blog.landing h3 a{font-size:16px}.blog.landing .pagination{margin:30px auto 50px auto}.blog.landing .pagination ul{padding:0px;text-align:center}.blog.landing .pagination ul li{padding:0px}.blog.landing .pagination ul li a{padding:5px 10px}.blog.landing .pagination ul li a.active{background-color:#F2F2F2;border-radius:50%}.blog.landing .pagination ul li.left-arrow{margin-right:15px}.blog.landing .pagination ul li.right-arrow{margin-left:15px}.blog .blog-post{background-color:#fff;margin:-110px 0px 0px -30px;padding:30px 90px 30px 30px}.blog .blog-post .author{color:#0095D3;margin:0px}.blog .blog-post .date{color:#111;margin:0px;font-weight:600}.blog .blog-post .header,.blog .blog-post h4{color:#111;font-weight:600}.blog .blog-post a{font-size:16px}.blog .blog-post ul{list-style-type:disc;padding-left:20px}.blog .blog-post ul li:first-child{margin-top:10px}.blog .blog-post ul li{list-style-type:unset;display:list-item;margin-bottom:10px;font-size:16px;color:#333;list-style-image:url(/img/arrow.svg)}.blog .blog-post ol li:first-child{margin-top:10px}.blog .blog-post ol li{list-style-type:decimal;display:list-item;margin-bottom:10px;font-size:16px;color:#333}.blog .blog-post code .c1{color:#0095D3;font-style:italic}.blog .blog-post code .se{color:#ff0000}.blog .blog-post pre{white-space:pre-wrap;white-space:-moz-pre-wrap;white-space:-pre-wrap;white-space:-o-pre-wrap;word-wrap:break-word}.blog .blog-post img{max-width:100%}.blog .blog-post strong{font-family:"Metropolis-Medium",Helvetica,sans-serif}.blog .blog-post-card p.no-margin{margin-block-start:0;margin-block-end:0}.getting-started{background-color:#F2F2F2;color:#111}.getting-started p{color:#111;font-size:16px}.getting-started .left-side{width:50%;float:left}.getting-started .right-side{width:25%;float:right}.getting-started h2{font-size:30px;margin-bottom:0px}.getting-started a{display:block;max-width:138px;text-align:center;padding:10px;min-width:unset}.getting-started .button{margin-top:50px;border:1px solid #0095D3}@media only screen and (max-width: 767px){.getting-started .wrapper{padding-bottom:40px}.getting-started .left-side{width:100%;float:none}.getting-started .right-side{width:100%;float:none}.getting-started .button{display:block;text-align:center;max-width:unset;margin-top:20px}}.community{background-color:#fff;margin-top:-90px;padding:30px 30px 50px 30px}.community .grid .col{border:1px solid #F2F2F2}.community .grid .col .icon{display:flex;align-items:center;justify-content:center;min-height:140px}.community .grid .col .content{padding:0px 20px 20px 20px;text-align:center}.community .grid .col .content h3{margin-top:0px}.resources{background-color:#fff;margin-top:-90px;padding:30px 30px 50px 30px}.resources .embed-responsive{position:relative}.resources .embed-responsive:before{padding-top:56.25%;display:block;content:""}.resources .embed-responsive .embed-responsive-item{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.resources .grid .col{border:1px solid #F2F2F2}.resources .grid .col .icon{display:flex;align-items:center;justify-content:center;min-height:140px}.resources .grid .col .icon img{max-width:100%;height:auto}.resources .grid .col .content{padding:0px 20px 20px 20px;text-align:center}.resources .grid .col .content h3{margin-top:0px}.docs{background-color:#fff;margin-top:-90px;padding:30px 30px 50px 30px}.docs .side-nav{width:25%;float:left;position:relative}@media only screen and (max-width: 1279px){.docs .side-nav{width:100%;float:none}}.docs .side-nav a.active{background:#F2F2F2;padding:5px 7px;margin-left:-7px}.docs .side-nav h3{font-size:18px;font-family:"Metropolis-Medium",Helvetica,sans-serif;margin-bottom:10px}.docs .side-nav h3 a{font-weight:300;line-height:1.25;color:#000}.docs .side-nav ul{padding-left:0px;margin-top:0;margin-bottom:35px;list-style-type:none}.docs .side-nav ul li{padding-right:0px;display:list-item}.docs .side-nav ul li a{font-size:14px;font-weight:300}.docs .side-nav ul li.heading{color:#111;font-size:14px}.docs .docs-content{width:75%;float:right}@media only screen and (max-width: 1279px){.docs .docs-content{width:100%;float:none}}.docs .docs-content a{font-size:16px}.docs .docs-content ul{list-style-type:disc;padding-left:20px}.docs .docs-content ul li:first-child{margin-top:10px}.docs .docs-content ul li{list-style-type:unset;display:list-item;margin-bottom:10px;font-size:16px;color:#333;line-height:1.6em;list-style-image:url(/img/arrow.svg)}.docs .docs-content ol li:first-child{margin-top:10px}.docs .docs-content ol li{list-style-type:decimal;display:list-item;margin-bottom:10px;font-size:16px;color:#333}.docs .docs-content code .c1{color:#0095D3;font-style:italic}.docs .docs-content code .se{color:#ff0000}.docs .docs-content pre{white-space:pre-wrap;white-space:-moz-pre-wrap;white-space:-pre-wrap;white-space:-o-pre-wrap;word-wrap:break-word}.docs .docs-content img{max-width:100%}.docs .docs-content strong{font-family:"Metropolis-Medium",Helvetica,sans-serif}.docs .danger{padding:10px;font-family:"Metropolis-LightItalic",Helvetica,sans-serif}.docs .danger .danger-icon{float:left;padding:40px;width:24px;height:24px}.docs .button{white-space:nowrap}.docs .button a{font-size:14px}.docs table td{padding:10px 30px}.terms ul li{display:block}.chroma{color:#272822;background-color:#fafafa}.chroma .err{color:#960050;background-color:#1e0010}.chroma .lntd{vertical-align:top;padding:0;margin:0;border:0}.chroma .lntable{border-spacing:0;padding:0;margin:0;border:0;width:auto;overflow:auto;display:block}.chroma .hl{display:block;width:100%;background-color:#ffffcc}.chroma .lnt{margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f}.chroma .ln{margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f}.chroma .k{color:#00a8c8}.chroma .kc{color:#00a8c8}.chroma .kd{color:#00a8c8}.chroma .kn{color:#f92672}.chroma .kp{color:#00a8c8}.chroma .kr{color:#00a8c8}.chroma .kt{color:#00a8c8}.chroma .n{color:#111111}.chroma .na{color:#75af00}.chroma .nb{color:#111111}.chroma .bp{color:#111111}.chroma .nc{color:#75af00}.chroma .no{color:#00a8c8}.chroma .nd{color:#75af00}.chroma .ni{color:#111111}.chroma .ne{color:#75af00}.chroma .nf{color:#75af00}.chroma .fm{color:#111111}.chroma .nl{color:#111111}.chroma .nn{color:#111111}.chroma .nx{color:#75af00}.chroma .py{color:#111111}.chroma .nt{color:#f92672}.chroma .nv{color:#111111}.chroma .vc{color:#111111}.chroma .vg{color:#111111}.chroma .vi{color:#111111}.chroma .vm{color:#111111}.chroma .l{color:#ae81ff}.chroma .ld{color:#d88200}.chroma .s{color:#d88200}.chroma .sa{color:#d88200}.chroma .sb{color:#d88200}.chroma .sc{color:#d88200}.chroma .dl{color:#d88200}.chroma .sd{color:#d88200}.chroma .s2{color:#d88200}.chroma .se{color:#8045ff}.chroma .sh{color:#d88200}.chroma .si{color:#d88200}.chroma .sx{color:#d88200}.chroma .sr{color:#d88200}.chroma .s1{color:#d88200}.chroma .ss{color:#d88200}.chroma .m{color:#ae81ff}.chroma .mb{color:#ae81ff}.chroma .mf{color:#ae81ff}.chroma .mh{color:#ae81ff}.chroma .mi{color:#ae81ff}.chroma .il{color:#ae81ff}.chroma .mo{color:#ae81ff}.chroma .o{color:#f92672}.chroma .ow{color:#f92672}.chroma .p{color:#111111}.chroma .c{color:#75715e}.chroma .ch{color:#75715e}.chroma .cm{color:#75715e}.chroma .c1{color:#75715e}.chroma .cs{color:#75715e}.chroma .cp{color:#75715e}.chroma .cpf{color:#75715e}.chroma .ge{font-style:italic}.chroma .gs{font-weight:bold} +body{font-family:"Metropolis-Light",Helvetica,sans-serif;margin:0px;line-height:1.25}.wrapper{max-width:980px;margin:0px auto;padding:20px}@media only screen and (max-width: 767px){.wrapper{max-width:100%}}.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;content:"";line-height:0}.clearfix:after{clear:both}h1,h2,h3,h4,h5,h6{font-weight:300}h1{font-size:36px}h2{font-size:28px}h3{font-size:22px}h4{font-size:18px}li{list-style-type:none;display:inline;padding-right:25px;font-size:14px;line-height:1.6em}li:last-of-type{padding-right:0px}p{line-height:1.7em;font-weight:300;font-size:16px;color:#333}a{font-size:16px;text-decoration:none;color:#0095D3;font-family:"Metropolis-Medium",Helvetica,sans-serif}button{background-color:unset;border:none}.button{color:#0095D3;font-size:12px;font-weight:600;background-color:#fff;border-radius:3px;padding:14px 10px;min-width:200px;text-transform:uppercase;border:1px solid #fff}.button.secondary{background-color:#0091DA;color:#fff}.button.tertiary{border:1px solid #0095D3}.button img.button-icon{margin-left:5px;margin-right:5px;margin-bottom:-8px;width:24px;height:24px}.buttons{margin-top:40px}.buttons .button:first-of-type{margin-right:30px}@media only screen and (max-width: 767px){.buttons .button:first-of-type{margin:0px 0px 20px 0px}}.strong{font-family:"Metropolis-Medium",Helvetica,sans-serif}.bg-grey{background-color:#F2F2F2}.grid.three{display:grid;grid-template-columns:1fr 1fr 1fr;row-gap:20px;column-gap:20px}@media only screen and (max-width: 767px){.grid.three{grid-template-columns:1fr}}.grid.two{display:grid;grid-template-columns:1fr 1fr}@media only screen and (max-width: 767px){.grid.two{grid-template-columns:1fr}}@font-face{font-family:"Metropolis-Bold";src:url("/fonts/Metropolis-Bold.eot");src:url("/fonts/Metropolis-Bold.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-Bold.woff2") format("woff2"),url("/fonts/Metropolis-Bold.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-BoldItalic";src:url("/fonts/Metropolis-BoldItalic.eot");src:url("/fonts/Metropolis-BoldItalic.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-BoldItalic.woff2") format("woff2"),url("/fonts/Metropolis-BoldItalic.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-Light";src:url("/fonts/Metropolis-Light.eot");src:url("/fonts/Metropolis-Light.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-Light.woff2") format("woff2"),url("/fonts/Metropolis-Light.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-LightItalic";src:url("/fonts/Metropolis-LightItalic.eot");src:url("/fonts/Metropolis-LightItalic.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-LightItalic.woff2") format("woff2"),url("/fonts/Metropolis-LightItalic.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-Regular";src:url("/fonts/Metropolis-Regular.eot");src:url("/fonts/Metropolis-Regular.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-Regular.woff2") format("woff2"),url("/fonts/Metropolis-Regular.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-RegularItalic";src:url("/fonts/Metropolis-RegularItalic.eot");src:url("/fonts/Metropolis-RegularItalic.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-RegularItalic.woff2") format("woff2"),url("/fonts/Metropolis-RegularItalic.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-Medium";src:url("/fonts/Metropolis-Medium.eot");src:url("/fonts/Metropolis-Medium.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-Medium.woff2") format("woff2"),url("/fonts/Metropolis-Medium.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-MediumItalic";src:url("/fonts/Metropolis-MediumItalic.eot");src:url("/fonts/Metropolis-MediumItalic.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-MediumItalic.woff2") format("woff2"),url("/fonts/Metropolis-MediumItalic.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-SemiBold";src:url("/fonts/Metropolis-SemiBold.eot");src:url("/fonts/Metropolis-SemiBold.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-SemiBold.woff2") format("woff2"),url("/fonts/Metropolis-SemiBold.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-SemiBoldItalic";src:url("/fonts/Metropolis-SemiBoldItalic.eot");src:url("/fonts/Metropolis-SemiBoldItalic.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-SemiBoldItalic.woff2") format("woff2"),url("/fonts/Metropolis-SemiBoldItalic.woff") format("woff");font-weight:normal;font-style:normal}header .wrapper{padding:10px 20px}header .desktop-links{float:right;margin:15px 0px 0px 0px;padding-left:0px}header a{color:#333;font-family:"Metropolis-Light",Helvetica,sans-serif}header a.active{font-family:"Metropolis-Medium",Helvetica,sans-serif}header li img{vertical-align:bottom;margin-right:10px}header .mobile{display:none}@media only screen and (min-width: 768px) and (max-width: 1279px){header .desktop-links li{padding-right:10px}}@media only screen and (max-width: 767px){header{position:relative}header .expanded-icon{display:none;padding:11px 3px 0px 0px}header .collapsed-icon{padding-top:12px}header .mobile-menu-visible .mobile{display:block}header .mobile-menu-visible .mobile .collapsed-icon{display:none}header .mobile-menu-visible .mobile .expanded-icon{display:block}header .desktop-links{display:none}header .mobile{display:block}header button{float:right}header button:focus{outline:none}header ul{padding-left:0px}header ul li{display:block;margin:20px 0px}header .mobile-menu{position:absolute;background-color:#fff;width:100%;top:70px;left:0px;padding-bottom:20px;display:none}header .mobile-menu .header-links{margin:0px 20px}header .mobile-menu .social{margin:0px 20px;padding-top:20px}header .mobile-menu .social img{vertical-align:middle;padding-right:10px}header .mobile-menu .social a{font-size:14px;padding-right:35px}header .mobile-menu .social a:last-of-type{padding-right:0px}}body{font-family:"Metropolis-Light",Helvetica,sans-serif;margin:0px;line-height:1.25}.wrapper{max-width:980px;margin:0px auto;padding:20px}@media only screen and (max-width: 767px){.wrapper{max-width:100%}}.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;content:"";line-height:0}.clearfix:after{clear:both}h1,h2,h3,h4,h5,h6{font-weight:300}h1{font-size:36px}h2{font-size:28px}h3{font-size:22px}h4{font-size:18px}li{list-style-type:none;display:inline;padding-right:25px;font-size:14px;line-height:1.6em}li:last-of-type{padding-right:0px}p{line-height:1.7em;font-weight:300;font-size:16px;color:#333}a{font-size:16px;text-decoration:none;color:#0095D3;font-family:"Metropolis-Medium",Helvetica,sans-serif}button{background-color:unset;border:none}.button{color:#0095D3;font-size:12px;font-weight:600;background-color:#fff;border-radius:3px;padding:14px 10px;min-width:200px;text-transform:uppercase;border:1px solid #fff}.button.secondary{background-color:#0091DA;color:#fff}.button.tertiary{border:1px solid #0095D3}.button img.button-icon{margin-left:5px;margin-right:5px;margin-bottom:-8px;width:24px;height:24px}.buttons{margin-top:40px}.buttons .button:first-of-type{margin-right:30px}@media only screen and (max-width: 767px){.buttons .button:first-of-type{margin:0px 0px 20px 0px}}.strong{font-family:"Metropolis-Medium",Helvetica,sans-serif}.bg-grey{background-color:#F2F2F2}.grid.three{display:grid;grid-template-columns:1fr 1fr 1fr;row-gap:20px;column-gap:20px}@media only screen and (max-width: 767px){.grid.three{grid-template-columns:1fr}}.grid.two{display:grid;grid-template-columns:1fr 1fr}@media only screen and (max-width: 767px){.grid.two{grid-template-columns:1fr}}@font-face{font-family:"Metropolis-Bold";src:url("/fonts/Metropolis-Bold.eot");src:url("/fonts/Metropolis-Bold.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-Bold.woff2") format("woff2"),url("/fonts/Metropolis-Bold.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-BoldItalic";src:url("/fonts/Metropolis-BoldItalic.eot");src:url("/fonts/Metropolis-BoldItalic.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-BoldItalic.woff2") format("woff2"),url("/fonts/Metropolis-BoldItalic.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-Light";src:url("/fonts/Metropolis-Light.eot");src:url("/fonts/Metropolis-Light.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-Light.woff2") format("woff2"),url("/fonts/Metropolis-Light.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-LightItalic";src:url("/fonts/Metropolis-LightItalic.eot");src:url("/fonts/Metropolis-LightItalic.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-LightItalic.woff2") format("woff2"),url("/fonts/Metropolis-LightItalic.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-Regular";src:url("/fonts/Metropolis-Regular.eot");src:url("/fonts/Metropolis-Regular.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-Regular.woff2") format("woff2"),url("/fonts/Metropolis-Regular.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-RegularItalic";src:url("/fonts/Metropolis-RegularItalic.eot");src:url("/fonts/Metropolis-RegularItalic.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-RegularItalic.woff2") format("woff2"),url("/fonts/Metropolis-RegularItalic.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-Medium";src:url("/fonts/Metropolis-Medium.eot");src:url("/fonts/Metropolis-Medium.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-Medium.woff2") format("woff2"),url("/fonts/Metropolis-Medium.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-MediumItalic";src:url("/fonts/Metropolis-MediumItalic.eot");src:url("/fonts/Metropolis-MediumItalic.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-MediumItalic.woff2") format("woff2"),url("/fonts/Metropolis-MediumItalic.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-SemiBold";src:url("/fonts/Metropolis-SemiBold.eot");src:url("/fonts/Metropolis-SemiBold.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-SemiBold.woff2") format("woff2"),url("/fonts/Metropolis-SemiBold.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-SemiBoldItalic";src:url("/fonts/Metropolis-SemiBoldItalic.eot");src:url("/fonts/Metropolis-SemiBoldItalic.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-SemiBoldItalic.woff2") format("woff2"),url("/fonts/Metropolis-SemiBoldItalic.woff") format("woff");font-weight:normal;font-style:normal}footer .wrapper{padding:20px}footer li{list-style-type:none;display:inline;padding-right:25px;font-size:12px}footer li:last-of-type{padding-right:0px}footer .top-links{min-height:52px;display:flex;align-items:center;justify-content:space-between}footer .left-links{padding:0px}footer .left-links li img{vertical-align:bottom;margin-right:10px}footer .left-links li a{color:#333;font-weight:300;font-size:12px;font-family:"Metropolis-Light",Helvetica,sans-serif}footer .left-links .mobile{display:none}footer .right-links p{margin:0px}footer .right-links .copywrite{font-size:12px;padding-right:10px}footer .right-links .copywrite a{font-size:12px;color:#333;font-family:"Metropolis-Light",Helvetica,sans-serif}footer .right-links a{vertical-align:middle}footer .bottom-links{margin:10px 0px 30px 0px}footer .bottom-links p{font-size:12px;display:flex;justify-content:space-between;flex-wrap:wrap}footer .bottom-links p .ot-sdk-show-settings{cursor:pointer}footer .bottom-links a{font-size:12px;font-family:"Metropolis-Light",Helvetica,sans-serif}footer .bottom-links img{max-width:75px;vertical-align:middle;margin-left:30px}@media only screen and (max-width: 767px){footer .footer-links{display:block}footer .footer-links .right-links{display:none}footer .footer-links .left-links{float:none;margin:10px 0px}footer .footer-links .left-links .desktop{display:none}footer .footer-links .left-links .mobile{display:inline}footer .footer-links .left-links .copywrite{display:block;margin-top:20px}footer .bottom-links{margin:10px 0px 20px 0px;float:none}footer .bottom-links img{margin-left:0px;display:block;margin-top:10px}}body{font-family:"Metropolis-Light",Helvetica,sans-serif;margin:0px;line-height:1.25}.wrapper{max-width:980px;margin:0px auto;padding:20px}@media only screen and (max-width: 767px){.wrapper{max-width:100%}}.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;content:"";line-height:0}.clearfix:after{clear:both}h1,h2,h3,h4,h5,h6{font-weight:300}h1{font-size:36px}h2{font-size:28px}h3{font-size:22px}h4{font-size:18px}li{list-style-type:none;display:inline;padding-right:25px;font-size:14px;line-height:1.6em}li:last-of-type{padding-right:0px}p{line-height:1.7em;font-weight:300;font-size:16px;color:#333}a{font-size:16px;text-decoration:none;color:#0095D3;font-family:"Metropolis-Medium",Helvetica,sans-serif}button{background-color:unset;border:none}.button{color:#0095D3;font-size:12px;font-weight:600;background-color:#fff;border-radius:3px;padding:14px 10px;min-width:200px;text-transform:uppercase;border:1px solid #fff}.button.secondary{background-color:#0091DA;color:#fff}.button.tertiary{border:1px solid #0095D3}.button img.button-icon{margin-left:5px;margin-right:5px;margin-bottom:-8px;width:24px;height:24px}.buttons{margin-top:40px}.buttons .button:first-of-type{margin-right:30px}@media only screen and (max-width: 767px){.buttons .button:first-of-type{margin:0px 0px 20px 0px}}.strong{font-family:"Metropolis-Medium",Helvetica,sans-serif}.bg-grey{background-color:#F2F2F2}.grid.three{display:grid;grid-template-columns:1fr 1fr 1fr;row-gap:20px;column-gap:20px}@media only screen and (max-width: 767px){.grid.three{grid-template-columns:1fr}}.grid.two{display:grid;grid-template-columns:1fr 1fr}@media only screen and (max-width: 767px){.grid.two{grid-template-columns:1fr}}@font-face{font-family:"Metropolis-Bold";src:url("/fonts/Metropolis-Bold.eot");src:url("/fonts/Metropolis-Bold.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-Bold.woff2") format("woff2"),url("/fonts/Metropolis-Bold.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-BoldItalic";src:url("/fonts/Metropolis-BoldItalic.eot");src:url("/fonts/Metropolis-BoldItalic.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-BoldItalic.woff2") format("woff2"),url("/fonts/Metropolis-BoldItalic.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-Light";src:url("/fonts/Metropolis-Light.eot");src:url("/fonts/Metropolis-Light.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-Light.woff2") format("woff2"),url("/fonts/Metropolis-Light.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-LightItalic";src:url("/fonts/Metropolis-LightItalic.eot");src:url("/fonts/Metropolis-LightItalic.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-LightItalic.woff2") format("woff2"),url("/fonts/Metropolis-LightItalic.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-Regular";src:url("/fonts/Metropolis-Regular.eot");src:url("/fonts/Metropolis-Regular.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-Regular.woff2") format("woff2"),url("/fonts/Metropolis-Regular.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-RegularItalic";src:url("/fonts/Metropolis-RegularItalic.eot");src:url("/fonts/Metropolis-RegularItalic.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-RegularItalic.woff2") format("woff2"),url("/fonts/Metropolis-RegularItalic.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-Medium";src:url("/fonts/Metropolis-Medium.eot");src:url("/fonts/Metropolis-Medium.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-Medium.woff2") format("woff2"),url("/fonts/Metropolis-Medium.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-MediumItalic";src:url("/fonts/Metropolis-MediumItalic.eot");src:url("/fonts/Metropolis-MediumItalic.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-MediumItalic.woff2") format("woff2"),url("/fonts/Metropolis-MediumItalic.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-SemiBold";src:url("/fonts/Metropolis-SemiBold.eot");src:url("/fonts/Metropolis-SemiBold.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-SemiBold.woff2") format("woff2"),url("/fonts/Metropolis-SemiBold.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-SemiBoldItalic";src:url("/fonts/Metropolis-SemiBoldItalic.eot");src:url("/fonts/Metropolis-SemiBoldItalic.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-SemiBoldItalic.woff2") format("woff2"),url("/fonts/Metropolis-SemiBoldItalic.woff") format("woff");font-weight:normal;font-style:normal}code{background:#efefef;padding:2px 4px;font-size:85%}pre code{background:none;font-size:14px;line-height:1.6em}pre.chroma{padding:10px}pre.chroma code{padding-left:0px}.highlight pre codesite/sidebar/reorganize{font-size:100%}.hero{background-color:#0091DA;color:#fff}.hero .text-block{max-width:550px;padding:0px 0px 10px 0px}.hero .text-block p{margin-bottom:20px;font-size:18px;color:#fff}.hero h2{font-size:36px}.hero.homepage{background-image:url(/img/hero-image.png);background-position:center center;background-repeat:no-repeat;background-size:cover;padding-bottom:80px}@media only screen and (max-width: 767px){.hero .text-block{max-width:unset;margin-right:0px}.hero .button{display:block;text-align:center}.hero.homepage{background-image:none}}.grid-container{margin-top:-80px}.grid-container .grid.three{padding-bottom:20px}.grid-container .grid.three .card{position:relative;padding:30px 20px;background-color:#fff;text-align:center;box-shadow:0px 2px 10px rgba(0,0,0,0.2)}.grid-container .grid.three .card h3{color:#333;font-size:22px}.grid-container .grid.three .card p{color:#333}.introduction .grid.two{column-gap:140px;padding:35px 20px}.introduction .grid.two p{margin:0px;font-size:16px}.introduction .grid.two p.strong{color:#333}@media only screen and (max-width: 767px){.introduction{padding:0px 20px}.introduction .col:first-of-type{padding-bottom:50px}}.use-cases .grid{grid-template-columns:220px 1fr;margin-bottom:30px;grid-template-areas:"image text"}.use-cases .grid .image{background-color:#0091DA;text-align:center;display:flex;align-items:center;justify-content:center;grid-area:image}.use-cases .grid .image img{justify-self:center}.use-cases .grid .text{border:1px solid #F2F2F2;padding:30px;grid-area:text}.use-cases .grid .text a.button{display:block;max-width:138px;text-align:center;padding:5px 10px;min-width:unset}.use-cases .grid.image-right{grid-template-columns:1fr 220px;grid-template-areas:"text image"}@media only screen and (max-width: 767px){.use-cases .grid.image-right{grid-template-columns:1fr;grid-template-areas:"image" "text"}}@media only screen and (max-width: 767px){.use-cases .grid{grid-template-columns:1fr;grid-template-rows:minmax(160px, 1fr);grid-template-areas:"image" "text"}}.use-cases h2{color:#111}.use-cases p.strong{color:#1B3951;font-size:16px}.team{background-color:#1D428A}.team h2,.team h3,.team p{color:#fff}.team p{font-size:16px}.team a{color:#fff;font-weight:300;text-decoration:underline}.team .grid.three{row-gap:40px;margin:40px 0px}.team .bio{display:grid;grid-template-columns:120px 1fr;column-gap:20px}.team .bio .info{align-self:center}.team .bio .info p{margin:0px}.team .bio .info p.name{font-size:16px;font-family:"Metropolis-Medium",Helvetica,sans-serif}.team .bio .info p.position{font-size:14px}.hero.subpage{background-image:url(/img/blog-hero-image.png);background-position:center center;background-repeat:no-repeat;background-size:cover;padding-bottom:140px}.experimental .grid.three .col{padding:0px}.experimental .icon{background-color:#0091DA;padding:25px;min-height:95px;display:flex;align-items:center;justify-content:center}.experimental .content{padding:25px}.experimental .content .example{background-color:#F2F2F2}.blog{padding-bottom:50px}.blog .col{border:1px solid #F2F2F2}.blog .col img{width:100%}.blog .col .content{padding:0px 20px}.blog.landing{background-color:#fff;margin-top:-90px}.blog.landing h3 a{font-size:16px}.blog.landing .pagination{margin:30px auto 50px auto}.blog.landing .pagination ul{padding:0px;text-align:center}.blog.landing .pagination ul li{padding:0px}.blog.landing .pagination ul li a{padding:5px 10px}.blog.landing .pagination ul li a.active{background-color:#F2F2F2;border-radius:50%}.blog.landing .pagination ul li.left-arrow{margin-right:15px}.blog.landing .pagination ul li.right-arrow{margin-left:15px}.blog .blog-post{background-color:#fff;margin:-110px 0px 0px -30px;padding:30px 90px 30px 30px}.blog .blog-post .author{color:#0095D3;margin:0px}.blog .blog-post .date{color:#111;margin:0px;font-weight:600}.blog .blog-post .header,.blog .blog-post h4{color:#111;font-weight:600}.blog .blog-post a{font-size:16px}.blog .blog-post ul{list-style-type:disc;padding-left:20px}.blog .blog-post ul li:first-child{margin-top:10px}.blog .blog-post ul li{list-style-type:unset;display:list-item;margin-bottom:10px;font-size:16px;color:#333;list-style-image:url(/img/arrow.svg)}.blog .blog-post ol li:first-child{margin-top:10px}.blog .blog-post ol li{list-style-type:decimal;display:list-item;margin-bottom:10px;font-size:16px;color:#333}.blog .blog-post code .c1{color:#0095D3;font-style:italic}.blog .blog-post code .se{color:#ff0000}.blog .blog-post pre{white-space:pre-wrap;white-space:-moz-pre-wrap;white-space:-pre-wrap;white-space:-o-pre-wrap;word-wrap:break-word}.blog .blog-post img{max-width:100%}.blog .blog-post strong{font-family:"Metropolis-Medium",Helvetica,sans-serif}.blog .blog-post-card p.no-margin{margin-block-start:0;margin-block-end:0}.getting-started{background-color:#F2F2F2;color:#111}.getting-started p{color:#111;font-size:16px}.getting-started .left-side{width:50%;float:left}.getting-started .right-side{width:25%;float:right}.getting-started h2{font-size:30px;margin-bottom:0px}.getting-started a{display:block;max-width:138px;text-align:center;padding:10px;min-width:unset}.getting-started .button{margin-top:50px;border:1px solid #0095D3}@media only screen and (max-width: 767px){.getting-started .wrapper{padding-bottom:40px}.getting-started .left-side{width:100%;float:none}.getting-started .right-side{width:100%;float:none}.getting-started .button{display:block;text-align:center;max-width:unset;margin-top:20px}}.community{background-color:#fff;margin-top:-90px;padding:30px 30px 50px 30px}.community .grid .col{border:1px solid #F2F2F2}.community .grid .col .icon{display:flex;align-items:center;justify-content:center;min-height:140px}.community .grid .col .content{padding:0px 20px 20px 20px;text-align:center}.community .grid .col .content h3{margin-top:0px}.resources{background-color:#fff;margin-top:-90px;padding:30px 30px 50px 30px}.resources .embed-responsive{position:relative}.resources .embed-responsive:before{padding-top:56.25%;display:block;content:""}.resources .embed-responsive .embed-responsive-item{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.resources .grid .col{border:1px solid #F2F2F2}.resources .grid .col .icon{display:flex;align-items:center;justify-content:center;min-height:140px}.resources .grid .col .icon img{max-width:100%;height:auto}.resources .grid .col .content{padding:0px 20px 20px 20px;text-align:center}.resources .grid .col .content h3{margin-top:0px}.docs{background-color:#fff;margin-top:-90px;padding:30px 30px 50px 30px}.docs .side-nav{width:25%;float:left;position:relative}@media only screen and (max-width: 1279px){.docs .side-nav{width:100%;float:none}}.docs .side-nav a.active{background:#F2F2F2;padding:5px 7px;margin-left:-7px}.docs .side-nav h3{font-size:18px;font-family:"Metropolis-Medium",Helvetica,sans-serif;margin-bottom:10px}.docs .side-nav h3 a{font-weight:300;line-height:1.25;color:#000}.docs .side-nav ul{padding-left:0px;margin-top:0;margin-bottom:35px;list-style-type:none}.docs .side-nav ul li{padding-right:0px;display:list-item}.docs .side-nav ul li a{font-size:14px;font-weight:300}.docs .side-nav ul li.heading{color:#111;font-size:14px}.docs .docs-content{width:75%;float:right}@media only screen and (max-width: 1279px){.docs .docs-content{width:100%;float:none}}.docs .docs-content a{font-size:16px}.docs .docs-content ul{list-style-type:disc;padding-left:20px}.docs .docs-content ul li:first-child{margin-top:10px}.docs .docs-content ul li{list-style-type:unset;display:list-item;margin-bottom:10px;font-size:16px;color:#333;line-height:1.6em;list-style-image:url(/img/arrow.svg)}.docs .docs-content ol li:first-child{margin-top:10px}.docs .docs-content ol li{list-style-type:decimal;display:list-item;margin-bottom:10px;font-size:16px;color:#333}.docs .docs-content code .c1{color:#0095D3;font-style:italic}.docs .docs-content code .se{color:#ff0000}.docs .docs-content pre{white-space:pre-wrap;white-space:-moz-pre-wrap;white-space:-pre-wrap;white-space:-o-pre-wrap;word-wrap:break-word}.docs .docs-content img{max-width:100%}.docs .docs-content strong{font-family:"Metropolis-Medium",Helvetica,sans-serif}.docs .danger{padding:10px;font-family:"Metropolis-LightItalic",Helvetica,sans-serif}.docs .danger .danger-icon{float:left;padding:40px;width:24px;height:24px}.docs .button{white-space:nowrap}.docs .button a{font-size:14px}.docs table td{padding:10px 30px}.terms ul li{display:block}.chroma{color:#272822;background-color:#fafafa}.chroma .err{color:#960050;background-color:#1e0010}.chroma .lntd{vertical-align:top;padding:0;margin:0;border:0}.chroma .lntable{border-spacing:0;padding:0;margin:0;border:0;width:auto;overflow:auto;display:block}.chroma .hl{display:block;width:100%;background-color:#ffffcc}.chroma .lnt{margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f}.chroma .ln{margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f}.chroma .k{color:#00a8c8}.chroma .kc{color:#00a8c8}.chroma .kd{color:#00a8c8}.chroma .kn{color:#f92672}.chroma .kp{color:#00a8c8}.chroma .kr{color:#00a8c8}.chroma .kt{color:#00a8c8}.chroma .n{color:#111111}.chroma .na{color:#75af00}.chroma .nb{color:#111111}.chroma .bp{color:#111111}.chroma .nc{color:#75af00}.chroma .no{color:#00a8c8}.chroma .nd{color:#75af00}.chroma .ni{color:#111111}.chroma .ne{color:#75af00}.chroma .nf{color:#75af00}.chroma .fm{color:#111111}.chroma .nl{color:#111111}.chroma .nn{color:#111111}.chroma .nx{color:#75af00}.chroma .py{color:#111111}.chroma .nt{color:#f92672}.chroma .nv{color:#111111}.chroma .vc{color:#111111}.chroma .vg{color:#111111}.chroma .vi{color:#111111}.chroma .vm{color:#111111}.chroma .l{color:#ae81ff}.chroma .ld{color:#d88200}.chroma .s{color:#d88200}.chroma .sa{color:#d88200}.chroma .sb{color:#d88200}.chroma .sc{color:#d88200}.chroma .dl{color:#d88200}.chroma .sd{color:#d88200}.chroma .s2{color:#d88200}.chroma .se{color:#8045ff}.chroma .sh{color:#d88200}.chroma .si{color:#d88200}.chroma .sx{color:#d88200}.chroma .sr{color:#d88200}.chroma .s1{color:#d88200}.chroma .ss{color:#d88200}.chroma .m{color:#ae81ff}.chroma .mb{color:#ae81ff}.chroma .mf{color:#ae81ff}.chroma .mh{color:#ae81ff}.chroma .mi{color:#ae81ff}.chroma .il{color:#ae81ff}.chroma .mo{color:#ae81ff}.chroma .o{color:#f92672}.chroma .ow{color:#f92672}.chroma .p{color:#111111}.chroma .c{color:#75715e}.chroma .ch{color:#75715e}.chroma .cm{color:#75715e}.chroma .c1{color:#75715e}.chroma .cs{color:#75715e}.chroma .cp{color:#75715e}.chroma .cpf{color:#75715e}.chroma .ge{font-style:italic}.chroma .gs{font-weight:bold} /*# sourceMappingURL=style.css.map */ \ No newline at end of file