├── .gitignore ├── Dockerfile ├── README.md ├── docker-compose.yml ├── kube ├── deployment │ ├── backend.yaml │ ├── frontend.yaml │ └── queue.yaml └── hpa.yaml ├── monitoring ├── Makefile ├── custom-metrics-api │ ├── cm-adapter-serving-certs.yaml │ ├── custom-metrics-apiserver-auth-delegator-cluster-role-binding.yaml │ ├── custom-metrics-apiserver-auth-reader-role-binding.yaml │ ├── custom-metrics-apiserver-deployment.yaml │ ├── custom-metrics-apiserver-resource-reader-cluster-role-binding.yaml │ ├── custom-metrics-apiserver-service-account.yaml │ ├── custom-metrics-apiserver-service.yaml │ ├── custom-metrics-apiservice.yaml │ ├── custom-metrics-cluster-role.yaml │ ├── custom-metrics-resource-reader-cluster-role.yaml │ └── hpa-custom-metrics-cluster-role-binding.yaml ├── metrics-server │ ├── aggregated-metrics-reader.yaml │ ├── auth-delegator.yaml │ ├── auth-reader.yaml │ ├── metrics-apiservice.yaml │ ├── metrics-server-deployment.yaml │ ├── metrics-server-service.yaml │ └── resource-reader.yaml ├── namespaces.yaml └── prometheus │ ├── prometheus-cfg.yaml │ ├── prometheus-dep.yaml │ ├── prometheus-rbac.yaml │ └── prometheus-svc.yaml ├── pom.xml └── src ├── main ├── java │ └── com │ │ └── learnk8s │ │ └── app │ │ ├── SpringBootApplication.java │ │ ├── controller │ │ └── HelloController.java │ │ ├── model │ │ └── Ticket.java │ │ └── queue │ │ ├── QueueConfig.java │ │ └── QueueService.java └── resources │ ├── application.yaml │ ├── static │ └── tachyons.min.css │ └── templates │ ├── home.html │ ├── layout.html │ └── success.html └── test └── java └── com └── learnk8s └── app ├── SpringBootApplicationTests.java └── service └── QueueServiceServiceImplTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | ### Eclipse template 2 | *.pydevproject 3 | .metadata 4 | .gradle 5 | bin/ 6 | tmp/ 7 | *.tmp 8 | *.bak 9 | *.swp 10 | *~.nib 11 | local.properties 12 | .settings/ 13 | .loadpath 14 | 15 | # Eclipse Core 16 | .project 17 | 18 | # External tool builders 19 | .externalToolBuilders/ 20 | 21 | # Locally stored "Eclipse launch configurations" 22 | *.launch 23 | 24 | # CDT-specific 25 | .cproject 26 | 27 | # JDT-specific (Eclipse Java Development Tools) 28 | .classpath 29 | 30 | # Java annotation processor (APT) 31 | .factorypath 32 | 33 | # PDT-specific 34 | .buildpath 35 | 36 | # sbteclipse plugin 37 | .target 38 | 39 | # TeXlipse plugin 40 | .texlipse 41 | 42 | 43 | ### JetBrains template 44 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion 45 | 46 | *.iml 47 | 48 | ## Directory-based project format: 49 | .idea/ 50 | # if you remove the above rule, at least ignore the following: 51 | 52 | # User-specific stuff: 53 | # .idea/workspace.xml 54 | # .idea/tasks.xml 55 | # .idea/dictionaries 56 | 57 | # Sensitive or high-churn files: 58 | # .idea/dataSources.ids 59 | # .idea/dataSources.xml 60 | # .idea/sqlDataSources.xml 61 | # .idea/dynamic.xml 62 | # .idea/uiDesigner.xml 63 | 64 | # Gradle: 65 | # .idea/gradle.xml 66 | # .idea/libraries 67 | 68 | # Mongo Explorer plugin: 69 | # .idea/mongoSettings.xml 70 | 71 | ## File-based project format: 72 | *.ipr 73 | *.iws 74 | 75 | ## Plugin-specific files: 76 | 77 | # IntelliJ 78 | /out/ 79 | 80 | # mpeltonen/sbt-idea plugin 81 | .idea_modules/ 82 | 83 | # JIRA plugin 84 | atlassian-ide-plugin.xml 85 | 86 | # Crashlytics plugin (for Android Studio and IntelliJ) 87 | com_crashlytics_export_strings.xml 88 | crashlytics.properties 89 | crashlytics-build.properties 90 | 91 | 92 | ### Java template 93 | *.class 94 | 95 | # Mobile Tools for Java (J2ME) 96 | .mtj.tmp/ 97 | 98 | # Package Files # 99 | *.jar 100 | *.war 101 | *.ear 102 | 103 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 104 | hs_err_pid* 105 | 106 | 107 | /project/project/ 108 | /project/target/ 109 | /project/activator-* 110 | /logs/ 111 | /RUNNING_PID 112 | .DS_Store 113 | /target/ 114 | /workspace/ 115 | /ode_modules/ 116 | /.apt_generated/ 117 | activemq-data/ -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM maven:3.5.3-jdk-10-slim as build 2 | 3 | WORKDIR /app 4 | COPY pom.xml . 5 | COPY src src 6 | RUN mvn package -q -Dmaven.test.skip=true 7 | 8 | FROM openjdk:10.0.1-10-jre-slim 9 | 10 | WORKDIR /app 11 | EXPOSE 8080 12 | ENV STORE_ENABLED=true 13 | ENV WORKER_ENABLED=true 14 | COPY --from=build /app/target/spring-boot-k8s-hpa-0.0.1-SNAPSHOT.jar /app 15 | 16 | CMD ["java", "-jar", "spring-boot-k8s-hpa-0.0.1-SNAPSHOT.jar"] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Autoscaling Spring Boot with the Horizontal Pod Autoscaler and custom metrics on Kubernetes 2 | 3 | ## Prerequisites 4 | 5 | You should have minikube installed. 6 | 7 | You should start minikube with at least 4GB of RAM: 8 | 9 | ```bash 10 | minikube start \ 11 | --memory 4096 \ 12 | --extra-config=controller-manager.horizontal-pod-autoscaler-upscale-delay=1m \ 13 | --extra-config=controller-manager.horizontal-pod-autoscaler-downscale-delay=2m \ 14 | --extra-config=controller-manager.horizontal-pod-autoscaler-sync-period=10s 15 | ``` 16 | 17 | > If you're using a pre-existing minikube instance, you can resize the VM by destroying it an recreating it. Just adding the `--memory 4096` won't have any effect. 18 | 19 | You should install `jq` — a lightweight and flexible command-line JSON processor. 20 | 21 | You can find more [info about `jq` on the official website](https://github.com/stedolan/jq). 22 | 23 | ## Installing Custom Metrics Api 24 | 25 | Deploy the Metrics Server in the `kube-system` namespace: 26 | 27 | ```bash 28 | kubectl create -f monitoring/metrics-server 29 | ``` 30 | 31 | After one minute the metric-server starts reporting CPU and memory usage for nodes and pods. 32 | 33 | View nodes metrics: 34 | 35 | ```bash 36 | kubectl get --raw "/apis/metrics.k8s.io/v1beta1/nodes" | jq . 37 | ``` 38 | 39 | View pods metrics: 40 | 41 | ```bash 42 | kubectl get --raw "/apis/metrics.k8s.io/v1beta1/pods" | jq . 43 | ``` 44 | 45 | Create the monitoring namespace: 46 | 47 | ```bash 48 | kubectl create -f monitoring/namespaces.yaml 49 | ``` 50 | 51 | Deploy Prometheus v2 in the monitoring namespace: 52 | 53 | ```bash 54 | kubectl create -f monitoring/prometheus 55 | ``` 56 | 57 | Deploy the Prometheus custom metrics API adapter: 58 | 59 | ```bash 60 | kubectl create -f monitoring/custom-metrics-api 61 | ``` 62 | 63 | List the custom metrics provided by Prometheus: 64 | 65 | ```bash 66 | kubectl get --raw "/apis/custom.metrics.k8s.io/v1beta1" | jq . 67 | ``` 68 | 69 | ## Package the application 70 | 71 | You package the application as a container with: 72 | 73 | ```bash 74 | eval $(minikube docker-env) 75 | docker build -t spring-boot-hpa . 76 | ``` 77 | 78 | ## Deploying the application 79 | 80 | Deploy the application in Kubernetes with: 81 | 82 | ```bash 83 | kubectl create -f kube/deployment 84 | ``` 85 | 86 | You can visit the application at http://minkube_ip:32000 87 | 88 | > (Find the minikube ip address via `minikube ip`) 89 | 90 | You can post messages to the queue by via http://minkube_ip:32000/submit?quantity=2 91 | 92 | You should be able to see the number of pending messages from http://minkube_ip:32000/metrics and from the custom metrics endpoint: 93 | 94 | ```bash 95 | kubectl get --raw "/apis/custom.metrics.k8s.io/v1beta1/namespaces/default/pods/*/messages" | jq . 96 | ``` 97 | 98 | ## Autoscaling workers 99 | 100 | You can scale the application in proportion to the number of messages in the queue with the Horizontal Pod Autoscaler. You can deploy the HPA with: 101 | 102 | ```bash 103 | kubectl create -f kube/hpa.yaml 104 | ``` 105 | 106 | You can send more traffic to the application with: 107 | 108 | ```bash 109 | while true; do curl -d "quantity=1" -X POST http://minkube_ip:32000/submit ; sleep 4; done 110 | ``` 111 | 112 | When the application can't cope with the number of incoming messages, the autoscaler increases the number of pods in 3 minute intervals. 113 | 114 | You may need to wait three minutes before you can see more pods joining the deployment with: 115 | 116 | ```bash 117 | kubectl get pods 118 | ``` 119 | 120 | The autoscaler will remove pods from the deployment every 5 minutes. 121 | 122 | You can inspect the event and triggers in the HPA with: 123 | 124 | ```bash 125 | kubectl get hpa spring-boot-hpa 126 | ``` 127 | 128 | ## Notes 129 | 130 | The configuration for metrics and metrics server is configured to run on minikube only. 131 | 132 | **You won't be able to run the same YAML files for metrics and custom metrics server on your cluster or EKS, GKE, AKS, etc.** 133 | 134 | Also, there are secrets checked in the repository to deploy the Prometheus adapter. 135 | 136 | **In production, you should generate your own secrets and (possibly) not check them into version control.** 137 | 138 | If you wish to run metrics and custom metrics server in production, you should check out the following resources: 139 | 140 | - [Metrics server](https://github.com/kubernetes-sigs/metrics-server) 141 | - [How to install Prometheus and the Promtheus Adapter](https://github.com/DirectXMan12/k8s-prometheus-adapter/blob/master/docs/walkthrough.md) -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | web: 5 | build: . 6 | environment: 7 | ACTIVEMQ_BROKER_URL: tcp://activemq:61616 8 | depends_on: 9 | - activemq 10 | ports: 11 | - "8080:8080" 12 | 13 | activemq: 14 | image: webcenter/activemq:5.14.3 15 | -------------------------------------------------------------------------------- /kube/deployment/backend.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: backend 6 | spec: 7 | replicas: 1 8 | selector: 9 | matchLabels: 10 | app: backend 11 | template: 12 | metadata: 13 | labels: 14 | app: backend 15 | annotations: 16 | prometheus.io/scrape: 'true' 17 | spec: 18 | containers: 19 | - name: backend 20 | image: spring-boot-hpa 21 | imagePullPolicy: IfNotPresent 22 | env: 23 | - name: ACTIVEMQ_BROKER_URL 24 | value: "tcp://queue:61616" 25 | - name: STORE_ENABLED 26 | value: "false" 27 | - name: WORKER_ENABLED 28 | value: "true" 29 | ports: 30 | - containerPort: 8080 31 | readinessProbe: 32 | initialDelaySeconds: 5 33 | periodSeconds: 5 34 | httpGet: 35 | path: /health 36 | port: 8080 37 | resources: 38 | limits: 39 | memory: 256Mi 40 | --- 41 | apiVersion: v1 42 | kind: Service 43 | metadata: 44 | name: backend 45 | spec: 46 | ports: 47 | - nodePort: 31000 48 | port: 80 49 | targetPort: 8080 50 | selector: 51 | app: backend 52 | type: NodePort -------------------------------------------------------------------------------- /kube/deployment/frontend.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: frontend 6 | spec: 7 | replicas: 1 8 | selector: 9 | matchLabels: 10 | app: frontend 11 | template: 12 | metadata: 13 | labels: 14 | app: frontend 15 | spec: 16 | containers: 17 | - name: frontend 18 | image: spring-boot-hpa 19 | imagePullPolicy: IfNotPresent 20 | env: 21 | - name: ACTIVEMQ_BROKER_URL 22 | value: "tcp://queue:61616" 23 | - name: STORE_ENABLED 24 | value: "true" 25 | - name: WORKER_ENABLED 26 | value: "false" 27 | ports: 28 | - containerPort: 8080 29 | readinessProbe: 30 | initialDelaySeconds: 5 31 | periodSeconds: 5 32 | httpGet: 33 | path: /health 34 | port: 8080 35 | resources: 36 | limits: 37 | memory: 512Mi 38 | --- 39 | apiVersion: v1 40 | kind: Service 41 | metadata: 42 | name: frontend 43 | spec: 44 | ports: 45 | - nodePort: 32000 46 | port: 80 47 | targetPort: 8080 48 | selector: 49 | app: frontend 50 | type: NodePort -------------------------------------------------------------------------------- /kube/deployment/queue.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: queue 6 | spec: 7 | replicas: 1 8 | selector: 9 | matchLabels: 10 | app: queue 11 | template: 12 | metadata: 13 | labels: 14 | app: queue 15 | spec: 16 | containers: 17 | - name: web 18 | image: webcenter/activemq:5.14.3 19 | imagePullPolicy: IfNotPresent 20 | ports: 21 | - containerPort: 61616 22 | resources: 23 | limits: 24 | memory: 512Mi 25 | --- 26 | apiVersion: v1 27 | kind: Service 28 | metadata: 29 | name: queue 30 | spec: 31 | ports: 32 | - port: 61616 33 | targetPort: 61616 34 | selector: 35 | app: queue -------------------------------------------------------------------------------- /kube/hpa.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: autoscaling/v2beta1 3 | kind: HorizontalPodAutoscaler 4 | metadata: 5 | name: spring-boot-hpa 6 | spec: 7 | scaleTargetRef: 8 | apiVersion: apps/v1 9 | kind: Deployment 10 | name: backend 11 | minReplicas: 1 12 | maxReplicas: 10 13 | metrics: 14 | - type: Pods 15 | pods: 16 | metricName: messages 17 | targetAverageValue: 10 -------------------------------------------------------------------------------- /monitoring/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for generating TLS certs for the Prometheus custom metrics API adapter 2 | 3 | SHELL=bash 4 | UNAME := $(shell uname) 5 | PURPOSE:=metrics 6 | SERVICE_NAME:=custom-metrics-apiserver 7 | ALT_NAMES:="custom-metrics-apiserver.monitoring","custom-metrics-apiserver.monitoring.svc" 8 | SECRET_FILE:=custom-metrics-api/cm-adapter-serving-certs.yaml 9 | 10 | certs: gensecret rmcerts 11 | 12 | .PHONY: gencerts 13 | gencerts: 14 | @echo Generating TLS certs 15 | @openssl req -x509 -sha256 -new -nodes -days 365 -newkey rsa:2048 -keyout $(PURPOSE)-ca.key -out $(PURPOSE)-ca.crt -subj "/CN=ca" 16 | @echo '{"signing":{"default":{"expiry":"43800h","usages":["signing","key encipherment","'$(PURPOSE)'"]}}}' > "$(PURPOSE)-ca-config.json" 17 | @echo '{"CN":"'$(SERVICE_NAME)'","hosts":[$(ALT_NAMES)],"key":{"algo":"rsa","size":2048}}' | cfssl gencert -ca=metrics-ca.crt -ca-key=metrics-ca.key -config=metrics-ca-config.json - | cfssljson -bare apiserver 18 | 19 | .PHONY: gensecret 20 | gensecret: gencerts 21 | @echo Generating $(SECRET_FILE) 22 | @echo "apiVersion: v1" > $(SECRET_FILE) 23 | @echo "kind: Secret" >> $(SECRET_FILE) 24 | @echo "metadata:" >> $(SECRET_FILE) 25 | @echo " name: cm-adapter-serving-certs" >> $(SECRET_FILE) 26 | @echo " namespace: monitoring" >> $(SECRET_FILE) 27 | @echo "data:" >> $(SECRET_FILE) 28 | ifeq ($(UNAME), Darwin) 29 | @echo " serving.crt: $$(cat apiserver.pem | base64)" >> $(SECRET_FILE) 30 | @echo " serving.key: $$(cat apiserver-key.pem | base64)" >> $(SECRET_FILE) 31 | endif 32 | ifeq ($(UNAME), Linux) 33 | @echo " serving.crt: $$(cat apiserver.pem | base64 -w 0)" >> $(SECRET_FILE) 34 | @echo " serving.key: $$(cat apiserver-key.pem | base64 -w 0)" >> $(SECRET_FILE) 35 | endif 36 | 37 | .PHONY: rmcerts 38 | rmcerts: 39 | @rm -f apiserver-key.pem apiserver.csr apiserver.pem 40 | @rm -f metrics-ca-config.json metrics-ca.crt metrics-ca.key 41 | 42 | .PHONY: deploy 43 | deploy: 44 | kubectl create -f ./namespaces.yaml 45 | kubectl create -f ./metrics-server 46 | kubectl create -f ./prometheus 47 | kubectl create -f ./custom-metrics-api 48 | -------------------------------------------------------------------------------- /monitoring/custom-metrics-api/cm-adapter-serving-certs.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: cm-adapter-serving-certs 5 | namespace: monitoring 6 | data: 7 | serving.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURlVENDQW1HZ0F3SUJBZ0lVVHEyVnd2azd5bmg1YlhDd3JjaDdGZEpHdjdNd0RRWUpLb1pJaHZjTkFRRUwKQlFBd0RURUxNQWtHQTFVRUF4TUNZMkV3SGhjTk1UZ3dOVEl3TVRFd05UQXdXaGNOTWpNd05URTVNVEV3TlRBdwpXakFqTVNFd0h3WURWUVFERXhoamRYTjBiMjB0YldWMGNtbGpjeTFoY0dselpYSjJaWEl3Z2dFaU1BMEdDU3FHClNJYjNEUUVCQVFVQUE0SUJEd0F3Z2dFS0FvSUJBUUMvQlFIRzVLNGl5QkxoVHk5YUcwa2ZSMFRGOEJaeDhRcS8KTmhMYTNJaVlPZnBBUWVwanBvVER5dXJyd2hQbmVJL2x6RVVEcnl2NytGTzJZeFlCbzdHQVU1YkJobHBiSGo0TgpCeUxpVW9WRnhSNGx1SGJCVmtwNU5ldzJxZndmZ3JTa3lxNS8wRmdRcUVGaTRpKzhIUWlWNzQ4aENYdVVUK0swClFRVnN2QzZRTzBWS0xScGlSYlh5ZTNCdlppNFZKeS9tZ0VTdG9UUGhjSGF5eHI4MTZuZGE4OVNtbFd0UFlnbmYKSGl4MnNROGhrOHAzY0FMeFZaVHp1bUxYR0xBV2w2YXl0bVhLUWZyTzM1aHQ3a20zY2RraFdOUWFSNFpnVDB1SQpkTml0dFNDN1ArZSt1NzlVaHZ2QjZHNjdtK2Jyc2ZOSWw1ZkpXWmg3RTNBNnVJYmhvb1BQQWdNQkFBR2pnYm93CmdiY3dEZ1lEVlIwUEFRSC9CQVFEQWdXZ01Bd0dBMVVkRXdFQi93UUNNQUF3SFFZRFZSME9CQllFRkVyN0xzZG0KN0FiM3RwRDkwQ1diUjVsZmt3aktNQjhHQTFVZEl3UVlNQmFBRkNDckRDNW1wd25LVHNzaU1yS3FCczZDam9lNQpNRmNHQTFVZEVRUlFNRTZDSTJOMWMzUnZiUzF0WlhSeWFXTnpMV0Z3YVhObGNuWmxjaTV0YjI1cGRHOXlhVzVuCmdpZGpkWE4wYjIwdGJXVjBjbWxqY3kxaGNHbHpaWEoyWlhJdWJXOXVhWFJ2Y21sdVp5NXpkbU13RFFZSktvWkkKaHZjTkFRRUxCUUFEZ2dFQkFEQ2w1dlJpQ1A2OHp6ZW03NXJCd09rQ3NzOXBmU2N6Z1JZMzVKd2l0OEdUbnFpcQplMndqbk5TUFQwMUU3V0krQjdaK2lKNTFvUm1LcWFhejl3SFpuOHdXVVNPdGRGaWliZCtxemVCS1BYdTBmM1hoCldYT21hZjUzcmdRTFV4TWRDUVQ3TnNxbGh6a1hCMGZGOGVEcUdXTTFET2lxODVuSGRpZ0NnekZBSEVlQSt0TGEKSU1vN2M5RjN1bG40eWhVakdSWm5zUmtrV0VjYThRMUpDcm1zRXhjOHltNEVrcmdUL29XSTNneXFIallnTERPbApDVG5PL2pCUWVEcmNqSEpaT1hnYmZ3Q1FDOGpqUDE2Ly9XNy91dHNrWUVBY0E5eXI5TjFrSkFvMVJnTnZDQkpCCjhDNmtCS3pvTVNBSFpuSDJJNVMzcnZSNnJEMnZpQWVseXQ1OWlFQT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo= 8 | serving.key: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcFFJQkFBS0NBUUVBdndVQnh1U3VJc2dTNFU4dldodEpIMGRFeGZBV2NmRUt2ellTMnR5SW1EbjZRRUhxClk2YUV3OHJxNjhJVDUzaVA1Y3hGQTY4cisvaFR0bU1XQWFPeGdGT1d3WVphV3g0K0RRY2k0bEtGUmNVZUpiaDIKd1ZaS2VUWHNOcW44SDRLMHBNcXVmOUJZRUtoQll1SXZ2QjBJbGUrUElRbDdsRS9pdEVFRmJMd3VrRHRGU2kwYQpZa1cxOG50d2IyWXVGU2N2NW9CRXJhRXo0WEIyc3NhL05lcDNXdlBVcHBWclQySUozeDRzZHJFUElaUEtkM0FDCjhWV1U4N3BpMXhpd0ZwZW1zclpseWtINnp0K1liZTVKdDNIWklWalVHa2VHWUU5TGlIVFlyYlVndXovbnZydS8KVkliN3dlaHV1NXZtNjdIelNKZVh5Vm1ZZXhOd09yaUc0YUtEendJREFRQUJBb0lCQVFDWHJob2pIU05KYXlYUwowMVgrYnZ3NVloWXlOdHNienhPQmtpYVdzOWVqU1NaMkwrRTNEOXNNdmV5ZEdKZEZqbTUxUXlicUxnYjNKZEVUCldzcjJzazBZWVVlc2xFcmJYOEJXQi95alNuT1pXMVQ5RHVUOTBNK0NRRmM1Y1BGSzlEY05SQVFjYWd3RDdoYzkKdGUzQnFpeFBJdEs4b1JNSVNnYnlxMDdyY09Kb2s4YW9uMWpDZTl4Z0ZTSFd0WFBvdVg0cWRENytlY2F3TFhLTQpqU2lndXNMTmFCM2tVOHJsQWdtMityV2xOKzBnYmMyMU1qeGxoVXhTZHd6OU1oTzBrOGRvby8yZktWTEJvN3FuCkNBSlFuS1pwd28wVkJONldpbHRhcC8vQ1N1WmYvZ1Y3RUlSN3ZLbEcwMlJBUm5EcU5tVW44L1dXdHFTZ29POFAKZk1BNnQ0OEJBb0dCQVBjNENCR1duSVQ1RTJ4c3JyaWl1SUVNbTdNR1NlOWZvVVRLb295ckQycW92czV3VG51cgpyK0xtZWtZV2V0K1J5U3NMVmlKZnNDT21MNWVPZDgwKzR3OUxqOGU1THM3WXRDUUEzcjB4dHJHQi9jN1FwQ3FFCkxNbGt4SmJCQjlxT2VEUVpZMFhGUldkN01RakcrTTh6UFB1WmEyRkkwNGJtd09kQU9KT2g5OStSQW9HQkFNWE4KOUJJZEQvTjd2WkFEdERnR0drWDk0K1NQVkxrNDhiSCt6NWwyNHVjd3k3SWhHSjZ2QVhVZnZKZDRnVmRPelpWZgpSWmVSWE1rUWNwZ0dBWk5HVlVGdWFpaDEzZFpQM2FGUTFLRDd6SmZ5bGRKZGdLRnI3bEN2aVk5dytSdE54dGt2ClFNa25DazBSSytsTmFFQ0hIQ1hhNDlBZzFSMTN6WEN4MzA2aDBUMWZBb0dCQUpBRjBJMk9sTGpTOG9Iai94WUoKbGp0NFFPelc2UlBQUG9CNUhLR2V0bWlJemtBdUpVcmZCcVRGYU02VUlobEZNVkVtVlpQSTh6Q1JqSUJhdTJpVApEbXpHdElqQ1ZZYWpzSzZZNHlxTlg4V2gyVjgzSmF5dk8vaElBRTlqUFZRU1Urem1MWE5rTGI3UGdnMSsvVXlZCnFScGFrcmdtV1RUcUVjK25GazdMdVVCQkFvR0FGNTdhY0hkTCtsVmljNW9kb1RzSlZDWlhXbW5MUWpzTWthYk4KLzVOMDBRckFxRXJQZDlPS0p3UmRMdkZTOEt3V1hLbjkwM1dtVU5SSCt1TnN1THVEWHNiaW9tWkEwV0RFSklsVQplc3pXeDZaT1FjcXNtNW9aNFVTMDlvNFZFUytlZVR6dFRaZjk0T29XQmt3YUVPcVJiK3ZObklMdk1BK29WcmJHCjVjcnBBNTBDZ1lFQTFiNHZrVy9CNEtoY2Z1S3gvZ1FhRzdyemI0MjZmNVRyVFllb0VYN1FxVldCdTR6V0lqUUsKUVBJZ04xSk5kYnFsVm1Pa2g3QzNyQ29xUGg4STNEUkphQXNCMXpWQ0hCcysyVGtFM1pOajhBekZKeDROY0U2VwowTlNSYmZYdmxyS1dHb2RvcVUyNWZhVk1zUWJDSG0wcm9Ya1EwWHlBaytZb284R3BvSnRPNlVNPQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo= 9 | -------------------------------------------------------------------------------- /monitoring/custom-metrics-api/custom-metrics-apiserver-auth-delegator-cluster-role-binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1beta1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: custom-metrics:system:auth-delegator 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: system:auth-delegator 9 | subjects: 10 | - kind: ServiceAccount 11 | name: custom-metrics-apiserver 12 | namespace: monitoring 13 | -------------------------------------------------------------------------------- /monitoring/custom-metrics-api/custom-metrics-apiserver-auth-reader-role-binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1beta1 2 | kind: RoleBinding 3 | metadata: 4 | name: custom-metrics-auth-reader 5 | namespace: kube-system 6 | roleRef: 7 | apiGroup: rbac.authorization.k8s.io 8 | kind: Role 9 | name: extension-apiserver-authentication-reader 10 | subjects: 11 | - kind: ServiceAccount 12 | name: custom-metrics-apiserver 13 | namespace: monitoring 14 | -------------------------------------------------------------------------------- /monitoring/custom-metrics-api/custom-metrics-apiserver-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app: custom-metrics-apiserver 6 | name: custom-metrics-apiserver 7 | namespace: monitoring 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | app: custom-metrics-apiserver 13 | selector: 14 | matchLabels: 15 | app: custom-metrics-apiserver 16 | template: 17 | metadata: 18 | labels: 19 | app: custom-metrics-apiserver 20 | name: custom-metrics-apiserver 21 | spec: 22 | serviceAccountName: custom-metrics-apiserver 23 | containers: 24 | - name: custom-metrics-apiserver 25 | image: quay.io/coreos/k8s-prometheus-adapter-amd64:v0.2.0 26 | args: 27 | - /adapter 28 | - --secure-port=6443 29 | - --tls-cert-file=/var/run/serving-cert/serving.crt 30 | - --tls-private-key-file=/var/run/serving-cert/serving.key 31 | - --logtostderr=true 32 | - --prometheus-url=http://prometheus.monitoring.svc:9090/ 33 | - --metrics-relist-interval=30s 34 | - --rate-interval=5m 35 | - --v=10 36 | ports: 37 | - containerPort: 6443 38 | volumeMounts: 39 | - mountPath: /var/run/serving-cert 40 | name: volume-serving-cert 41 | readOnly: true 42 | volumes: 43 | - name: volume-serving-cert 44 | secret: 45 | secretName: cm-adapter-serving-certs 46 | -------------------------------------------------------------------------------- /monitoring/custom-metrics-api/custom-metrics-apiserver-resource-reader-cluster-role-binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1beta1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: custom-metrics-resource-reader 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: custom-metrics-resource-reader 9 | subjects: 10 | - kind: ServiceAccount 11 | name: custom-metrics-apiserver 12 | namespace: monitoring 13 | -------------------------------------------------------------------------------- /monitoring/custom-metrics-api/custom-metrics-apiserver-service-account.yaml: -------------------------------------------------------------------------------- 1 | kind: ServiceAccount 2 | apiVersion: v1 3 | metadata: 4 | name: custom-metrics-apiserver 5 | namespace: monitoring 6 | -------------------------------------------------------------------------------- /monitoring/custom-metrics-api/custom-metrics-apiserver-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: custom-metrics-apiserver 5 | namespace: monitoring 6 | spec: 7 | ports: 8 | - port: 443 9 | targetPort: 6443 10 | selector: 11 | app: custom-metrics-apiserver 12 | -------------------------------------------------------------------------------- /monitoring/custom-metrics-api/custom-metrics-apiservice.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiregistration.k8s.io/v1beta1 2 | kind: APIService 3 | metadata: 4 | name: v1beta1.custom.metrics.k8s.io 5 | spec: 6 | service: 7 | name: custom-metrics-apiserver 8 | namespace: monitoring 9 | group: custom.metrics.k8s.io 10 | version: v1beta1 11 | insecureSkipTLSVerify: true 12 | groupPriorityMinimum: 100 13 | versionPriority: 100 14 | -------------------------------------------------------------------------------- /monitoring/custom-metrics-api/custom-metrics-cluster-role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1beta1 2 | kind: ClusterRole 3 | metadata: 4 | name: custom-metrics-server-resources 5 | rules: 6 | - apiGroups: 7 | - custom.metrics.k8s.io 8 | resources: ["*"] 9 | verbs: ["*"] 10 | -------------------------------------------------------------------------------- /monitoring/custom-metrics-api/custom-metrics-resource-reader-cluster-role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1beta1 2 | kind: ClusterRole 3 | metadata: 4 | name: custom-metrics-resource-reader 5 | rules: 6 | - apiGroups: 7 | - "" 8 | resources: 9 | - namespaces 10 | - pods 11 | - services 12 | verbs: 13 | - get 14 | - list 15 | -------------------------------------------------------------------------------- /monitoring/custom-metrics-api/hpa-custom-metrics-cluster-role-binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1beta1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: hpa-controller-custom-metrics 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: custom-metrics-server-resources 9 | subjects: 10 | - kind: ServiceAccount 11 | name: horizontal-pod-autoscaler 12 | namespace: kube-system 13 | -------------------------------------------------------------------------------- /monitoring/metrics-server/aggregated-metrics-reader.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: system:aggregated-metrics-reader 6 | labels: 7 | rbac.authorization.k8s.io/aggregate-to-view: "true" 8 | rbac.authorization.k8s.io/aggregate-to-edit: "true" 9 | rbac.authorization.k8s.io/aggregate-to-admin: "true" 10 | rules: 11 | - apiGroups: ["metrics.k8s.io"] 12 | resources: ["pods", "nodes"] 13 | verbs: ["get", "list", "watch"] 14 | -------------------------------------------------------------------------------- /monitoring/metrics-server/auth-delegator.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRoleBinding 4 | metadata: 5 | name: metrics-server:system:auth-delegator 6 | roleRef: 7 | apiGroup: rbac.authorization.k8s.io 8 | kind: ClusterRole 9 | name: system:auth-delegator 10 | subjects: 11 | - kind: ServiceAccount 12 | name: metrics-server 13 | namespace: kube-system 14 | -------------------------------------------------------------------------------- /monitoring/metrics-server/auth-reader.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: RoleBinding 4 | metadata: 5 | name: metrics-server-auth-reader 6 | namespace: kube-system 7 | roleRef: 8 | apiGroup: rbac.authorization.k8s.io 9 | kind: Role 10 | name: extension-apiserver-authentication-reader 11 | subjects: 12 | - kind: ServiceAccount 13 | name: metrics-server 14 | namespace: kube-system 15 | -------------------------------------------------------------------------------- /monitoring/metrics-server/metrics-apiservice.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiregistration.k8s.io/v1beta1 3 | kind: APIService 4 | metadata: 5 | name: v1beta1.metrics.k8s.io 6 | spec: 7 | service: 8 | name: metrics-server 9 | namespace: kube-system 10 | group: metrics.k8s.io 11 | version: v1beta1 12 | insecureSkipTLSVerify: true 13 | groupPriorityMinimum: 100 14 | versionPriority: 100 15 | -------------------------------------------------------------------------------- /monitoring/metrics-server/metrics-server-deployment.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: metrics-server 6 | namespace: kube-system 7 | --- 8 | apiVersion: apps/v1 9 | kind: Deployment 10 | metadata: 11 | name: metrics-server 12 | namespace: kube-system 13 | labels: 14 | k8s-app: metrics-server 15 | spec: 16 | selector: 17 | matchLabels: 18 | k8s-app: metrics-server 19 | template: 20 | metadata: 21 | name: metrics-server 22 | labels: 23 | k8s-app: metrics-server 24 | spec: 25 | serviceAccountName: metrics-server 26 | volumes: 27 | # mount in tmp so we can safely use from-scratch images and/or read-only containers 28 | - name: tmp-dir 29 | emptyDir: {} 30 | containers: 31 | - name: metrics-server 32 | image: k8s.gcr.io/metrics-server-amd64:v0.3.6 33 | args: 34 | - --cert-dir=/tmp 35 | - --secure-port=4443 36 | - --kubelet-insecure-tls 37 | - --kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname 38 | ports: 39 | - name: main-port 40 | containerPort: 4443 41 | protocol: TCP 42 | securityContext: 43 | readOnlyRootFilesystem: true 44 | runAsNonRoot: true 45 | runAsUser: 1000 46 | imagePullPolicy: Always 47 | volumeMounts: 48 | - name: tmp-dir 49 | mountPath: /tmp 50 | 51 | -------------------------------------------------------------------------------- /monitoring/metrics-server/metrics-server-service.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: metrics-server 6 | namespace: kube-system 7 | labels: 8 | kubernetes.io/name: "Metrics-server" 9 | kubernetes.io/cluster-service: "true" 10 | spec: 11 | selector: 12 | k8s-app: metrics-server 13 | ports: 14 | - port: 443 15 | protocol: TCP 16 | targetPort: main-port 17 | -------------------------------------------------------------------------------- /monitoring/metrics-server/resource-reader.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: system:metrics-server 6 | rules: 7 | - apiGroups: 8 | - "" 9 | resources: 10 | - pods 11 | - nodes 12 | - nodes/stats 13 | - namespaces 14 | verbs: 15 | - get 16 | - list 17 | - watch 18 | --- 19 | apiVersion: rbac.authorization.k8s.io/v1 20 | kind: ClusterRoleBinding 21 | metadata: 22 | name: system:metrics-server 23 | roleRef: 24 | apiGroup: rbac.authorization.k8s.io 25 | kind: ClusterRole 26 | name: system:metrics-server 27 | subjects: 28 | - kind: ServiceAccount 29 | name: metrics-server 30 | namespace: kube-system 31 | -------------------------------------------------------------------------------- /monitoring/namespaces.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Namespace 4 | metadata: 5 | name: monitoring 6 | 7 | 8 | -------------------------------------------------------------------------------- /monitoring/prometheus/prometheus-cfg.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | kind: ConfigMap 3 | apiVersion: v1 4 | metadata: 5 | labels: 6 | app: prometheus 7 | name: prometheus-config 8 | namespace: monitoring 9 | data: 10 | prometheus.yml: | 11 | # A scrape configuration for running Prometheus on a Kubernetes cluster. 12 | # This uses separate scrape configs for cluster components (i.e. API server, node) 13 | # and services to allow each to use different authentication configs. 14 | # 15 | # Kubernetes labels will be added as Prometheus labels on metrics via the 16 | # `labelmap` relabeling action. 17 | # 18 | # If you are using Kubernetes 1.7.2 or earlier, please take note of the comments 19 | # for the kubernetes-cadvisor job; you will need to edit or remove this job. 20 | 21 | # Scrape config for API servers. 22 | # 23 | # Kubernetes exposes API servers as endpoints to the default/kubernetes 24 | # service so this uses `endpoints` role and uses relabelling to only keep 25 | # the endpoints associated with the default/kubernetes service using the 26 | # default named port `https`. This works for single API server deployments as 27 | # well as HA API server deployments. 28 | global: 29 | scrape_interval: 15s 30 | scrape_timeout: 10s 31 | evaluation_interval: 1m 32 | 33 | scrape_configs: 34 | - job_name: 'kubernetes-apiservers' 35 | 36 | kubernetes_sd_configs: 37 | - role: endpoints 38 | 39 | # Default to scraping over https. If required, just disable this or change to 40 | # `http`. 41 | scheme: https 42 | 43 | # This TLS & bearer token file config is used to connect to the actual scrape 44 | # endpoints for cluster components. This is separate to discovery auth 45 | # configuration because discovery & scraping are two separate concerns in 46 | # Prometheus. The discovery auth config is automatic if Prometheus runs inside 47 | # the cluster. Otherwise, more config options have to be provided within the 48 | # . 49 | tls_config: 50 | ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt 51 | # If your node certificates are self-signed or use a different CA to the 52 | # master CA, then disable certificate verification below. Note that 53 | # certificate verification is an integral part of a secure infrastructure 54 | # so this should only be disabled in a controlled environment. You can 55 | # disable certificate verification by uncommenting the line below. 56 | # 57 | # insecure_skip_verify: true 58 | bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token 59 | 60 | # Keep only the default/kubernetes service endpoints for the https port. This 61 | # will add targets for each API server which Kubernetes adds an endpoint to 62 | # the default/kubernetes service. 63 | relabel_configs: 64 | - source_labels: [__meta_kubernetes_namespace, __meta_kubernetes_service_name, __meta_kubernetes_endpoint_port_name] 65 | action: keep 66 | regex: default;kubernetes;https 67 | 68 | # Scrape config for nodes (kubelet). 69 | # 70 | # Rather than connecting directly to the node, the scrape is proxied though the 71 | # Kubernetes apiserver. This means it will work if Prometheus is running out of 72 | # cluster, or can't connect to nodes for some other reason (e.g. because of 73 | # firewalling). 74 | - job_name: 'kubernetes-nodes' 75 | 76 | # Default to scraping over https. If required, just disable this or change to 77 | # `http`. 78 | scheme: https 79 | 80 | # This TLS & bearer token file config is used to connect to the actual scrape 81 | # endpoints for cluster components. This is separate to discovery auth 82 | # configuration because discovery & scraping are two separate concerns in 83 | # Prometheus. The discovery auth config is automatic if Prometheus runs inside 84 | # the cluster. Otherwise, more config options have to be provided within the 85 | # . 86 | tls_config: 87 | ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt 88 | bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token 89 | 90 | kubernetes_sd_configs: 91 | - role: node 92 | 93 | relabel_configs: 94 | - action: labelmap 95 | regex: __meta_kubernetes_node_label_(.+) 96 | - target_label: __address__ 97 | replacement: kubernetes.default.svc:443 98 | - source_labels: [__meta_kubernetes_node_name] 99 | regex: (.+) 100 | target_label: __metrics_path__ 101 | replacement: /api/v1/nodes/${1}/proxy/metrics 102 | 103 | # Scrape config for Kubelet cAdvisor. 104 | # 105 | # This is required for Kubernetes 1.7.3 and later, where cAdvisor metrics 106 | # (those whose names begin with 'container_') have been removed from the 107 | # Kubelet metrics endpoint. This job scrapes the cAdvisor endpoint to 108 | # retrieve those metrics. 109 | # 110 | # In Kubernetes 1.7.0-1.7.2, these metrics are only exposed on the cAdvisor 111 | # HTTP endpoint; use "replacement: /api/v1/nodes/${1}:4194/proxy/metrics" 112 | # in that case (and ensure cAdvisor's HTTP server hasn't been disabled with 113 | # the --cadvisor-port=0 Kubelet flag). 114 | # 115 | # This job is not necessary and should be removed in Kubernetes 1.6 and 116 | # earlier versions, or it will cause the metrics to be scraped twice. 117 | - job_name: 'kubernetes-cadvisor' 118 | 119 | # Default to scraping over https. If required, just disable this or change to 120 | # `http`. 121 | scheme: https 122 | 123 | # This TLS & bearer token file config is used to connect to the actual scrape 124 | # endpoints for cluster components. This is separate to discovery auth 125 | # configuration because discovery & scraping are two separate concerns in 126 | # Prometheus. The discovery auth config is automatic if Prometheus runs inside 127 | # the cluster. Otherwise, more config options have to be provided within the 128 | # . 129 | tls_config: 130 | ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt 131 | bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token 132 | 133 | kubernetes_sd_configs: 134 | - role: node 135 | 136 | relabel_configs: 137 | - action: labelmap 138 | regex: __meta_kubernetes_node_label_(.+) 139 | - target_label: __address__ 140 | replacement: kubernetes.default.svc:443 141 | - source_labels: [__meta_kubernetes_node_name] 142 | regex: (.+) 143 | target_label: __metrics_path__ 144 | replacement: /api/v1/nodes/${1}/proxy/metrics/cadvisor 145 | 146 | # Scrape config for service endpoints. 147 | # 148 | # The relabeling allows the actual service scrape endpoint to be configured 149 | # via the following annotations: 150 | # 151 | # * `prometheus.io/scrape`: Only scrape services that have a value of `true` 152 | # * `prometheus.io/scheme`: If the metrics endpoint is secured then you will need 153 | # to set this to `https` & most likely set the `tls_config` of the scrape config. 154 | # * `prometheus.io/path`: If the metrics path is not `/metrics` override this. 155 | # * `prometheus.io/port`: If the metrics are exposed on a different port to the 156 | # service then set this appropriately. 157 | - job_name: 'kubernetes-service-endpoints' 158 | 159 | kubernetes_sd_configs: 160 | - role: endpoints 161 | 162 | relabel_configs: 163 | - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_scrape] 164 | action: keep 165 | regex: true 166 | - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_scheme] 167 | action: replace 168 | target_label: __scheme__ 169 | regex: (https?) 170 | - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_path] 171 | action: replace 172 | target_label: __metrics_path__ 173 | regex: (.+) 174 | - source_labels: [__address__, __meta_kubernetes_service_annotation_prometheus_io_port] 175 | action: replace 176 | target_label: __address__ 177 | regex: ([^:]+)(?::\d+)?;(\d+) 178 | replacement: $1:$2 179 | - action: labelmap 180 | regex: __meta_kubernetes_service_label_(.+) 181 | - source_labels: [__meta_kubernetes_namespace] 182 | action: replace 183 | target_label: kubernetes_namespace 184 | - source_labels: [__meta_kubernetes_service_name] 185 | action: replace 186 | target_label: kubernetes_name 187 | 188 | # Example scrape config for pods 189 | # 190 | # The relabeling allows the actual pod scrape endpoint to be configured via the 191 | # following annotations: 192 | # 193 | # * `prometheus.io/scrape`: Only scrape pods that have a value of `true` 194 | # * `prometheus.io/path`: If the metrics path is not `/metrics` override this. 195 | # * `prometheus.io/port`: Scrape the pod on the indicated port instead of the 196 | # pod's declared ports (default is a port-free target if none are declared). 197 | - job_name: 'kubernetes-pods' 198 | # if you want to use metrics on jobs, set the below field to 199 | # true to prevent Prometheus from setting the `job` label 200 | # automatically. 201 | honor_labels: false 202 | kubernetes_sd_configs: 203 | - role: pod 204 | # skip verification so you can do HTTPS to pods 205 | tls_config: 206 | insecure_skip_verify: true 207 | # make sure your labels are in order 208 | relabel_configs: 209 | # these labels tell Prometheus to automatically attach source 210 | # pod and namespace information to each collected sample, so 211 | # that they'll be exposed in the custom metrics API automatically. 212 | - source_labels: [__meta_kubernetes_namespace] 213 | action: replace 214 | target_label: namespace 215 | - source_labels: [__meta_kubernetes_pod_name] 216 | action: replace 217 | target_label: pod 218 | # these labels tell Prometheus to look for 219 | # prometheus.io/{scrape,path,port} annotations to configure 220 | # how to scrape 221 | - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape] 222 | action: keep 223 | regex: true 224 | - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path] 225 | action: replace 226 | target_label: __metrics_path__ 227 | regex: (.+) 228 | - source_labels: [__address__, __meta_kubernetes_pod_annotation_prometheus_io_port] 229 | action: replace 230 | regex: ([^:]+)(?::\d+)?;(\d+) 231 | replacement: $1:$2 232 | target_label: __address__ 233 | - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scheme] 234 | action: replace 235 | target_label: __scheme__ 236 | regex: (.+) 237 | -------------------------------------------------------------------------------- /monitoring/prometheus/prometheus-dep.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: prometheus 6 | namespace: monitoring 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: prometheus 12 | template: 13 | metadata: 14 | labels: 15 | app: prometheus 16 | annotations: 17 | prometheus.io/scrape: 'false' 18 | spec: 19 | serviceAccountName: prometheus 20 | containers: 21 | - name: prometheus 22 | image: prom/prometheus:v2.1.0 23 | imagePullPolicy: Always 24 | command: 25 | - prometheus 26 | - --config.file=/etc/prometheus/prometheus.yml 27 | - --storage.tsdb.retention=1h 28 | ports: 29 | - containerPort: 9090 30 | protocol: TCP 31 | resources: 32 | limits: 33 | memory: 2Gi 34 | volumeMounts: 35 | - mountPath: /etc/prometheus/prometheus.yml 36 | name: prometheus-config 37 | subPath: prometheus.yml 38 | volumes: 39 | - name: prometheus-config 40 | configMap: 41 | name: prometheus-config 42 | items: 43 | - key: prometheus.yml 44 | path: prometheus.yml 45 | mode: 0644 46 | -------------------------------------------------------------------------------- /monitoring/prometheus/prometheus-rbac.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1beta1 3 | kind: ClusterRole 4 | metadata: 5 | name: prometheus 6 | rules: 7 | - apiGroups: [""] 8 | resources: 9 | - nodes 10 | - nodes/proxy 11 | - services 12 | - endpoints 13 | - pods 14 | verbs: ["get", "list", "watch"] 15 | - apiGroups: 16 | - extensions 17 | - apps 18 | resources: 19 | - ingresses 20 | verbs: ["get", "list", "watch"] 21 | - nonResourceURLs: ["/metrics"] 22 | verbs: ["get"] 23 | --- 24 | apiVersion: v1 25 | kind: ServiceAccount 26 | metadata: 27 | name: prometheus 28 | namespace: monitoring 29 | --- 30 | apiVersion: rbac.authorization.k8s.io/v1beta1 31 | kind: ClusterRoleBinding 32 | metadata: 33 | name: prometheus 34 | roleRef: 35 | apiGroup: rbac.authorization.k8s.io 36 | kind: ClusterRole 37 | name: prometheus 38 | subjects: 39 | - kind: ServiceAccount 40 | name: prometheus 41 | namespace: monitoring 42 | -------------------------------------------------------------------------------- /monitoring/prometheus/prometheus-svc.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: prometheus 6 | namespace: monitoring 7 | labels: 8 | app: prometheus 9 | spec: 10 | type: NodePort 11 | ports: 12 | - port: 9090 13 | targetPort: 9090 14 | nodePort: 31190 15 | protocol: TCP 16 | selector: 17 | app: prometheus 18 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.learnk8s 7 | spring-boot-k8s-hpa 8 | 0.0.1-SNAPSHOT 9 | jar 10 | 11 | spring-boot-k8s-hpa 12 | Demo project for Spring Boot and Kubernetes 13 | 14 | 15 | org.springframework.boot 16 | spring-boot-starter-parent 17 | 2.0.0.RELEASE 18 | 19 | 20 | 21 | 22 | UTF-8 23 | UTF-8 24 | 10 25 | 2.21.0 26 | 27 | 28 | 29 | 30 | org.springframework.boot 31 | spring-boot-starter-web 32 | 2.0.0.RELEASE 33 | 34 | 35 | org.springframework.boot 36 | spring-boot-starter-test 37 | test 38 | 2.0.0.RELEASE 39 | 40 | 41 | 42 | org.springframework.boot 43 | spring-boot-configuration-processor 44 | true 45 | 2.0.0.RELEASE 46 | 47 | 48 | 49 | org.springframework.boot 50 | spring-boot-starter-thymeleaf 51 | 2.0.0.RELEASE 52 | 53 | 54 | 55 | nz.net.ultraq.thymeleaf 56 | thymeleaf-layout-dialect 57 | 2.3.0 58 | 59 | 60 | 61 | org.springframework.boot 62 | spring-boot-starter-activemq 63 | 2.0.0.RELEASE 64 | 65 | 66 | 67 | org.apache.activemq.tooling 68 | activemq-junit 69 | ${activemq.version} 70 | test 71 | 72 | 73 | 74 | 75 | 76 | 77 | org.springframework.boot 78 | spring-boot-maven-plugin 79 | 80 | 81 | maven-compiler-plugin 82 | 3.7.0 83 | 84 | 85 | --add-modules 86 | java.xml.bind 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /src/main/java/com/learnk8s/app/SpringBootApplication.java: -------------------------------------------------------------------------------- 1 | package com.learnk8s.app; 2 | 3 | import com.learnk8s.app.queue.QueueService; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.beans.factory.annotation.Value; 6 | import org.springframework.boot.SpringApplication; 7 | import org.springframework.jms.annotation.EnableJms; 8 | import org.springframework.jms.annotation.JmsListenerConfigurer; 9 | import org.springframework.jms.config.JmsListenerEndpointRegistrar; 10 | import org.springframework.jms.config.SimpleJmsListenerEndpoint; 11 | 12 | @org.springframework.boot.autoconfigure.SpringBootApplication 13 | @EnableJms 14 | public class SpringBootApplication implements JmsListenerConfigurer { 15 | @Value("${queue.name}") 16 | private String queueName; 17 | 18 | @Value("${worker.name}") 19 | private String workerName; 20 | 21 | @Value("${worker.enabled}") 22 | private boolean workerEnabled; 23 | 24 | @Autowired 25 | private QueueService queueService; 26 | 27 | public static void main(String[] args) { 28 | SpringApplication.run(SpringBootApplication.class, args); 29 | } 30 | 31 | @Override 32 | public void configureJmsListeners(JmsListenerEndpointRegistrar registrar) { 33 | if (workerEnabled) { 34 | SimpleJmsListenerEndpoint endpoint = new SimpleJmsListenerEndpoint(); 35 | endpoint.setId(workerName); 36 | endpoint.setDestination(queueName); 37 | endpoint.setMessageListener(queueService); 38 | registrar.registerEndpoint(endpoint); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/learnk8s/app/controller/HelloController.java: -------------------------------------------------------------------------------- 1 | package com.learnk8s.app.controller; 2 | 3 | import com.learnk8s.app.model.Ticket; 4 | import com.learnk8s.app.queue.QueueService; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.beans.factory.annotation.Value; 7 | import org.springframework.http.HttpStatus; 8 | import org.springframework.http.ResponseEntity; 9 | import org.springframework.stereotype.Controller; 10 | import org.springframework.ui.Model; 11 | import org.springframework.web.bind.annotation.*; 12 | 13 | import java.util.UUID; 14 | 15 | @Controller 16 | public class HelloController { 17 | 18 | @Autowired 19 | private QueueService queueService; 20 | 21 | @Value("${queue.name}") 22 | private String queueName; 23 | 24 | @Value("${worker.name}") 25 | private String workerName; 26 | 27 | @Value("${store.enabled}") 28 | private boolean storeEnabled; 29 | 30 | @Value("${worker.enabled}") 31 | private boolean workerEnabled; 32 | 33 | @GetMapping("/") 34 | public String home(Model model) { 35 | int pendingMessages = queueService.pendingJobs(queueName); 36 | model.addAttribute("ticket", new Ticket()); 37 | model.addAttribute("pendingJobs", pendingMessages); 38 | model.addAttribute("completedJobs", queueService.completedJobs()); 39 | model.addAttribute("isConnected", queueService.isUp() ? "yes" : "no"); 40 | model.addAttribute("queueName", this.queueName); 41 | model.addAttribute("workerName", this.workerName); 42 | model.addAttribute("isStoreEnabled", this.storeEnabled); 43 | model.addAttribute("isWorkerEnabled", this.workerEnabled); 44 | return "home"; 45 | } 46 | 47 | @PostMapping("/submit") 48 | public String submit(@ModelAttribute Ticket ticket) { 49 | for (long i = 0; i < ticket.getQuantity(); i++) { 50 | String id = UUID.randomUUID().toString(); 51 | queueService.send(queueName, id); 52 | } 53 | return "success"; 54 | } 55 | 56 | @ResponseBody 57 | @RequestMapping(value="/metrics", produces="text/plain") 58 | public String metrics() { 59 | int totalMessages = queueService.pendingJobs(queueName); 60 | return "# HELP messages Number of messages in the queueService\n" 61 | + "# TYPE messages gauge\n" 62 | + "messages " + totalMessages; 63 | } 64 | 65 | @RequestMapping(value="/health") 66 | public ResponseEntity health() { 67 | HttpStatus status; 68 | if (queueService.isUp()) { 69 | status = HttpStatus.OK; 70 | } else { 71 | status = HttpStatus.BAD_REQUEST; 72 | } 73 | return new ResponseEntity<>(status); 74 | } 75 | 76 | 77 | } -------------------------------------------------------------------------------- /src/main/java/com/learnk8s/app/model/Ticket.java: -------------------------------------------------------------------------------- 1 | package com.learnk8s.app.model; 2 | 3 | public class Ticket { 4 | private long quantity; 5 | 6 | public long getQuantity() { 7 | return quantity; 8 | } 9 | 10 | public void setQuantity(long quantity) { 11 | this.quantity = quantity; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/learnk8s/app/queue/QueueConfig.java: -------------------------------------------------------------------------------- 1 | package com.learnk8s.app.queue; 2 | 3 | import org.apache.activemq.ActiveMQConnectionFactory; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | import org.springframework.beans.factory.annotation.Value; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.jms.config.DefaultJmsListenerContainerFactory; 10 | import org.springframework.jms.connection.CachingConnectionFactory; 11 | import org.springframework.jms.core.JmsTemplate; 12 | 13 | @Configuration 14 | public class QueueConfig { 15 | @Value("${activemq.brokerUrl}") 16 | private String brokerUrl; 17 | 18 | @Bean 19 | public ActiveMQConnectionFactory activeMQConnectionFactory() { 20 | ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(); 21 | activeMQConnectionFactory.setBrokerURL(brokerUrl); 22 | 23 | return activeMQConnectionFactory; 24 | } 25 | 26 | @Bean 27 | public DefaultJmsListenerContainerFactory jmsListenerContainerFactory() { 28 | DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory(); 29 | factory.setConnectionFactory(activeMQConnectionFactory()); 30 | factory.setConcurrency("1"); 31 | return factory; 32 | } 33 | 34 | @Bean 35 | public CachingConnectionFactory cachingConnectionFactory() { 36 | return new CachingConnectionFactory(activeMQConnectionFactory()); 37 | } 38 | 39 | @Bean 40 | public JmsTemplate jmsTemplate() { 41 | return new JmsTemplate(cachingConnectionFactory()); 42 | } 43 | 44 | @Bean 45 | public QueueService queue() { 46 | return new QueueService(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/learnk8s/app/queue/QueueService.java: -------------------------------------------------------------------------------- 1 | package com.learnk8s.app.queue; 2 | 3 | import org.apache.activemq.command.ActiveMQTextMessage; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.jms.core.JmsTemplate; 8 | import org.springframework.stereotype.Component; 9 | 10 | import javax.jms.JMSException; 11 | import javax.jms.Message; 12 | import javax.jms.MessageListener; 13 | import java.util.Collections; 14 | 15 | @Component 16 | public class QueueService implements MessageListener { 17 | private static final Logger LOGGER = LoggerFactory.getLogger(QueueService.class); 18 | 19 | @Autowired 20 | private JmsTemplate jmsTemplate; 21 | 22 | private int counter = 0; 23 | 24 | public int completedJobs() { 25 | return counter; 26 | } 27 | 28 | public void send(String destination, String message) { 29 | LOGGER.info("sending message='{}' to destination='{}'", message, destination); 30 | jmsTemplate.convertAndSend(destination, message); 31 | } 32 | 33 | public int pendingJobs(String queueName) { 34 | return jmsTemplate.browse(queueName, (s, qb) -> Collections.list(qb.getEnumeration()).size()); 35 | } 36 | 37 | public boolean isUp() { 38 | var connection = jmsTemplate.getConnectionFactory(); 39 | try { 40 | connection.createConnection().close(); 41 | return true; 42 | } catch (JMSException e) { 43 | e.printStackTrace(); 44 | } 45 | return false; 46 | } 47 | 48 | @Override 49 | public void onMessage(Message message) { 50 | if (message instanceof ActiveMQTextMessage) { 51 | ActiveMQTextMessage textMessage = (ActiveMQTextMessage) message; 52 | try { 53 | LOGGER.info("Processing task " + textMessage.getText()); 54 | Thread.sleep(5000); 55 | LOGGER.info("Completed task " + textMessage.getText()); 56 | } catch (InterruptedException e) { 57 | e.printStackTrace(); 58 | } catch (JMSException e) { 59 | e.printStackTrace(); 60 | } 61 | counter++; 62 | } else { 63 | LOGGER.error("Message is not a text message " + message.toString()); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/resources/application.yaml: -------------------------------------------------------------------------------- 1 | server: 2 | address: 0.0.0.0 3 | activemq: 4 | brokerUrl: ${ACTIVEMQ_BROKER_URL:vm://localhost:61616} 5 | queue: 6 | name: ${QUEUE_NAME:mainQueue} 7 | worker: 8 | name: ${HOSTNAME:worker1} 9 | enabled: ${WORKER_ENABLED:true} 10 | store: 11 | enabled: ${STORE_ENABLED:true} -------------------------------------------------------------------------------- /src/main/resources/static/tachyons.min.css: -------------------------------------------------------------------------------- 1 | /*! TACHYONS v4.9.0 | http://tachyons.io */ 2 | /*! normalize.css v7.0.0 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,footer,header,nav,section{display:block}h1{font-size:2em;margin:.67em 0}figcaption,figure,main{display:block}figure{margin:1em 40px}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent;-webkit-text-decoration-skip:objects}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:inherit;font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}dfn{font-style:italic}mark{background-color:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}audio,video{display:inline-block}audio:not([controls]){display:none;height:0}img{border-style:none}svg:not(:root){overflow:hidden}button,input,optgroup,select,textarea{font-family:sans-serif;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}/* 1 */ [type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{display:inline-block;vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}/* 1 */ menu,details{display:block}summary{display:list-item}canvas{display:inline-block}[hidden],template{display:none}.border-box,a,article,aside,blockquote,body,code,dd,div,dl,dt,fieldset,figcaption,figure,footer,form,h1,h2,h3,h4,h5,h6,header,html,input[type=email],input[type=number],input[type=password],input[type=tel],input[type=text],input[type=url],legend,li,main,nav,ol,p,pre,section,table,td,textarea,th,tr,ul{box-sizing:border-box}.aspect-ratio{height:0;position:relative}.aspect-ratio--16x9{padding-bottom:56.25%}.aspect-ratio--9x16{padding-bottom:177.77%}.aspect-ratio--4x3{padding-bottom:75%}.aspect-ratio--3x4{padding-bottom:133.33%}.aspect-ratio--6x4{padding-bottom:66.6%}.aspect-ratio--4x6{padding-bottom:150%}.aspect-ratio--8x5{padding-bottom:62.5%}.aspect-ratio--5x8{padding-bottom:160%}.aspect-ratio--7x5{padding-bottom:71.42%}.aspect-ratio--5x7{padding-bottom:140%}.aspect-ratio--1x1{padding-bottom:100%}.aspect-ratio--object{position:absolute;top:0;right:0;bottom:0;left:0;width:100%;height:100%;z-index:100}img{max-width:100%}.cover{background-size:cover!important}.contain{background-size:contain!important}.bg-center{background-position:50%}.bg-center,.bg-top{background-repeat:no-repeat}.bg-top{background-position:top}.bg-right{background-position:100%}.bg-bottom,.bg-right{background-repeat:no-repeat}.bg-bottom{background-position:bottom}.bg-left{background-repeat:no-repeat;background-position:0}.outline{outline:1px solid}.outline-transparent{outline:1px solid transparent}.outline-0{outline:0}.ba{border-style:solid;border-width:1px}.bt{border-top-style:solid;border-top-width:1px}.br{border-right-style:solid;border-right-width:1px}.bb{border-bottom-style:solid;border-bottom-width:1px}.bl{border-left-style:solid;border-left-width:1px}.bn{border-style:none;border-width:0}.b--black{border-color:#000}.b--near-black{border-color:#111}.b--dark-gray{border-color:#333}.b--mid-gray{border-color:#555}.b--gray{border-color:#777}.b--silver{border-color:#999}.b--light-silver{border-color:#aaa}.b--moon-gray{border-color:#ccc}.b--light-gray{border-color:#eee}.b--near-white{border-color:#f4f4f4}.b--white{border-color:#fff}.b--white-90{border-color:hsla(0,0%,100%,.9)}.b--white-80{border-color:hsla(0,0%,100%,.8)}.b--white-70{border-color:hsla(0,0%,100%,.7)}.b--white-60{border-color:hsla(0,0%,100%,.6)}.b--white-50{border-color:hsla(0,0%,100%,.5)}.b--white-40{border-color:hsla(0,0%,100%,.4)}.b--white-30{border-color:hsla(0,0%,100%,.3)}.b--white-20{border-color:hsla(0,0%,100%,.2)}.b--white-10{border-color:hsla(0,0%,100%,.1)}.b--white-05{border-color:hsla(0,0%,100%,.05)}.b--white-025{border-color:hsla(0,0%,100%,.025)}.b--white-0125{border-color:hsla(0,0%,100%,.0125)}.b--black-90{border-color:rgba(0,0,0,.9)}.b--black-80{border-color:rgba(0,0,0,.8)}.b--black-70{border-color:rgba(0,0,0,.7)}.b--black-60{border-color:rgba(0,0,0,.6)}.b--black-50{border-color:rgba(0,0,0,.5)}.b--black-40{border-color:rgba(0,0,0,.4)}.b--black-30{border-color:rgba(0,0,0,.3)}.b--black-20{border-color:rgba(0,0,0,.2)}.b--black-10{border-color:rgba(0,0,0,.1)}.b--black-05{border-color:rgba(0,0,0,.05)}.b--black-025{border-color:rgba(0,0,0,.025)}.b--black-0125{border-color:rgba(0,0,0,.0125)}.b--dark-red{border-color:#e7040f}.b--red{border-color:#ff4136}.b--light-red{border-color:#ff725c}.b--orange{border-color:#ff6300}.b--gold{border-color:#ffb700}.b--yellow{border-color:gold}.b--light-yellow{border-color:#fbf1a9}.b--purple{border-color:#5e2ca5}.b--light-purple{border-color:#a463f2}.b--dark-pink{border-color:#d5008f}.b--hot-pink{border-color:#ff41b4}.b--pink{border-color:#ff80cc}.b--light-pink{border-color:#ffa3d7}.b--dark-green{border-color:#137752}.b--green{border-color:#19a974}.b--light-green{border-color:#9eebcf}.b--navy{border-color:#001b44}.b--dark-blue{border-color:#00449e}.b--blue{border-color:#357edd}.b--light-blue{border-color:#96ccff}.b--lightest-blue{border-color:#cdecff}.b--washed-blue{border-color:#f6fffe}.b--washed-green{border-color:#e8fdf5}.b--washed-yellow{border-color:#fffceb}.b--washed-red{border-color:#ffdfdf}.b--transparent{border-color:transparent}.b--inherit{border-color:inherit}.br0{border-radius:0}.br1{border-radius:.125rem}.br2{border-radius:.25rem}.br3{border-radius:.5rem}.br4{border-radius:1rem}.br-100{border-radius:100%}.br-pill{border-radius:9999px}.br--bottom{border-top-left-radius:0;border-top-right-radius:0}.br--top{border-bottom-right-radius:0}.br--right,.br--top{border-bottom-left-radius:0}.br--right{border-top-left-radius:0}.br--left{border-top-right-radius:0;border-bottom-right-radius:0}.b--dotted{border-style:dotted}.b--dashed{border-style:dashed}.b--solid{border-style:solid}.b--none{border-style:none}.bw0{border-width:0}.bw1{border-width:.125rem}.bw2{border-width:.25rem}.bw3{border-width:.5rem}.bw4{border-width:1rem}.bw5{border-width:2rem}.bt-0{border-top-width:0}.br-0{border-right-width:0}.bb-0{border-bottom-width:0}.bl-0{border-left-width:0}.shadow-1{box-shadow:0 0 4px 2px rgba(0,0,0,.2)}.shadow-2{box-shadow:0 0 8px 2px rgba(0,0,0,.2)}.shadow-3{box-shadow:2px 2px 4px 2px rgba(0,0,0,.2)}.shadow-4{box-shadow:2px 2px 8px 0 rgba(0,0,0,.2)}.shadow-5{box-shadow:4px 4px 8px 0 rgba(0,0,0,.2)}.pre{overflow-x:auto;overflow-y:hidden;overflow:scroll}.top-0{top:0}.right-0{right:0}.bottom-0{bottom:0}.left-0{left:0}.top-1{top:1rem}.right-1{right:1rem}.bottom-1{bottom:1rem}.left-1{left:1rem}.top-2{top:2rem}.right-2{right:2rem}.bottom-2{bottom:2rem}.left-2{left:2rem}.top--1{top:-1rem}.right--1{right:-1rem}.bottom--1{bottom:-1rem}.left--1{left:-1rem}.top--2{top:-2rem}.right--2{right:-2rem}.bottom--2{bottom:-2rem}.left--2{left:-2rem}.absolute--fill{top:0;right:0;bottom:0;left:0}.cf:after,.cf:before{content:" ";display:table}.cf:after{clear:both}.cf{*zoom:1}.cl{clear:left}.cr{clear:right}.cb{clear:both}.cn{clear:none}.dn{display:none}.di{display:inline}.db{display:block}.dib{display:inline-block}.dit{display:inline-table}.dt{display:table}.dtc{display:table-cell}.dt-row{display:table-row}.dt-row-group{display:table-row-group}.dt-column{display:table-column}.dt-column-group{display:table-column-group}.dt--fixed{table-layout:fixed;width:100%}.flex{display:-webkit-box;display:-ms-flexbox;display:flex}.inline-flex{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex}.flex-auto{-webkit-box-flex:1;-ms-flex:1 1 auto;flex:1 1 auto;min-width:0;min-height:0}.flex-none{-webkit-box-flex:0;-ms-flex:none;flex:none}.flex-column{-webkit-box-orient:vertical;-ms-flex-direction:column;flex-direction:column}.flex-column,.flex-row{-webkit-box-direction:normal}.flex-row{-webkit-box-orient:horizontal;-ms-flex-direction:row;flex-direction:row}.flex-wrap{-ms-flex-wrap:wrap;flex-wrap:wrap}.flex-nowrap{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.flex-wrap-reverse{-ms-flex-wrap:wrap-reverse;flex-wrap:wrap-reverse}.flex-column-reverse{-webkit-box-orient:vertical;-webkit-box-direction:reverse;-ms-flex-direction:column-reverse;flex-direction:column-reverse}.flex-row-reverse{-webkit-box-orient:horizontal;-webkit-box-direction:reverse;-ms-flex-direction:row-reverse;flex-direction:row-reverse}.items-start{-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start}.items-end{-webkit-box-align:end;-ms-flex-align:end;align-items:flex-end}.items-center{-webkit-box-align:center;-ms-flex-align:center;align-items:center}.items-baseline{-webkit-box-align:baseline;-ms-flex-align:baseline;align-items:baseline}.items-stretch{-webkit-box-align:stretch;-ms-flex-align:stretch;align-items:stretch}.self-start{-ms-flex-item-align:start;align-self:flex-start}.self-end{-ms-flex-item-align:end;align-self:flex-end}.self-center{-ms-flex-item-align:center;-ms-grid-row-align:center;align-self:center}.self-baseline{-ms-flex-item-align:baseline;align-self:baseline}.self-stretch{-ms-flex-item-align:stretch;-ms-grid-row-align:stretch;align-self:stretch}.justify-start{-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.justify-end{-webkit-box-pack:end;-ms-flex-pack:end;justify-content:flex-end}.justify-center{-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.justify-between{-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.justify-around{-ms-flex-pack:distribute;justify-content:space-around}.content-start{-ms-flex-line-pack:start;align-content:flex-start}.content-end{-ms-flex-line-pack:end;align-content:flex-end}.content-center{-ms-flex-line-pack:center;align-content:center}.content-between{-ms-flex-line-pack:justify;align-content:space-between}.content-around{-ms-flex-line-pack:distribute;align-content:space-around}.content-stretch{-ms-flex-line-pack:stretch;align-content:stretch}.order-0{-webkit-box-ordinal-group:1;-ms-flex-order:0;order:0}.order-1{-webkit-box-ordinal-group:2;-ms-flex-order:1;order:1}.order-2{-webkit-box-ordinal-group:3;-ms-flex-order:2;order:2}.order-3{-webkit-box-ordinal-group:4;-ms-flex-order:3;order:3}.order-4{-webkit-box-ordinal-group:5;-ms-flex-order:4;order:4}.order-5{-webkit-box-ordinal-group:6;-ms-flex-order:5;order:5}.order-6{-webkit-box-ordinal-group:7;-ms-flex-order:6;order:6}.order-7{-webkit-box-ordinal-group:8;-ms-flex-order:7;order:7}.order-8{-webkit-box-ordinal-group:9;-ms-flex-order:8;order:8}.order-last{-webkit-box-ordinal-group:100000;-ms-flex-order:99999;order:99999}.flex-grow-0{-webkit-box-flex:0;-ms-flex-positive:0;flex-grow:0}.flex-grow-1{-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1}.flex-shrink-0{-ms-flex-negative:0;flex-shrink:0}.flex-shrink-1{-ms-flex-negative:1;flex-shrink:1}.fl{float:left}.fl,.fr{_display:inline}.fr{float:right}.fn{float:none}.sans-serif{font-family:-apple-system,BlinkMacSystemFont,avenir next,avenir,helvetica neue,helvetica,ubuntu,roboto,noto,segoe ui,arial,sans-serif}.serif{font-family:georgia,times,serif}.system-sans-serif{font-family:sans-serif}.system-serif{font-family:serif}.code,code{font-family:Consolas,monaco,monospace}.courier{font-family:Courier Next,courier,monospace}.helvetica{font-family:helvetica neue,helvetica,sans-serif}.avenir{font-family:avenir next,avenir,sans-serif}.athelas{font-family:athelas,georgia,serif}.georgia{font-family:georgia,serif}.times{font-family:times,serif}.bodoni{font-family:Bodoni MT,serif}.calisto{font-family:Calisto MT,serif}.garamond{font-family:garamond,serif}.baskerville{font-family:baskerville,serif}.i{font-style:italic}.fs-normal{font-style:normal}.normal{font-weight:400}.b{font-weight:700}.fw1{font-weight:100}.fw2{font-weight:200}.fw3{font-weight:300}.fw4{font-weight:400}.fw5{font-weight:500}.fw6{font-weight:600}.fw7{font-weight:700}.fw8{font-weight:800}.fw9{font-weight:900}.input-reset{-webkit-appearance:none;-moz-appearance:none}.button-reset::-moz-focus-inner,.input-reset::-moz-focus-inner{border:0;padding:0}.h1{height:1rem}.h2{height:2rem}.h3{height:4rem}.h4{height:8rem}.h5{height:16rem}.h-25{height:25%}.h-50{height:50%}.h-75{height:75%}.h-100{height:100%}.min-h-100{min-height:100%}.vh-25{height:25vh}.vh-50{height:50vh}.vh-75{height:75vh}.vh-100{height:100vh}.min-vh-100{min-height:100vh}.h-auto{height:auto}.h-inherit{height:inherit}.tracked{letter-spacing:.1em}.tracked-tight{letter-spacing:-.05em}.tracked-mega{letter-spacing:.25em}.lh-solid{line-height:1}.lh-title{line-height:1.25}.lh-copy{line-height:1.5}.link{text-decoration:none}.link,.link:active,.link:focus,.link:hover,.link:link,.link:visited{transition:color .15s ease-in}.link:focus{outline:1px dotted currentColor}.list{list-style-type:none}.mw-100{max-width:100%}.mw1{max-width:1rem}.mw2{max-width:2rem}.mw3{max-width:4rem}.mw4{max-width:8rem}.mw5{max-width:16rem}.mw6{max-width:32rem}.mw7{max-width:48rem}.mw8{max-width:64rem}.mw9{max-width:96rem}.mw-none{max-width:none}.w1{width:1rem}.w2{width:2rem}.w3{width:4rem}.w4{width:8rem}.w5{width:16rem}.w-10{width:10%}.w-20{width:20%}.w-25{width:25%}.w-30{width:30%}.w-33{width:33%}.w-34{width:34%}.w-40{width:40%}.w-50{width:50%}.w-60{width:60%}.w-70{width:70%}.w-75{width:75%}.w-80{width:80%}.w-90{width:90%}.w-100{width:100%}.w-third{width:33.33333%}.w-two-thirds{width:66.66667%}.w-auto{width:auto}.overflow-visible{overflow:visible}.overflow-hidden{overflow:hidden}.overflow-scroll{overflow:scroll}.overflow-auto{overflow:auto}.overflow-x-visible{overflow-x:visible}.overflow-x-hidden{overflow-x:hidden}.overflow-x-scroll{overflow-x:scroll}.overflow-x-auto{overflow-x:auto}.overflow-y-visible{overflow-y:visible}.overflow-y-hidden{overflow-y:hidden}.overflow-y-scroll{overflow-y:scroll}.overflow-y-auto{overflow-y:auto}.static{position:static}.relative{position:relative}.absolute{position:absolute}.fixed{position:fixed}.o-100{opacity:1}.o-90{opacity:.9}.o-80{opacity:.8}.o-70{opacity:.7}.o-60{opacity:.6}.o-50{opacity:.5}.o-40{opacity:.4}.o-30{opacity:.3}.o-20{opacity:.2}.o-10{opacity:.1}.o-05{opacity:.05}.o-025{opacity:.025}.o-0{opacity:0}.rotate-45{-webkit-transform:rotate(45deg);transform:rotate(45deg)}.rotate-90{-webkit-transform:rotate(90deg);transform:rotate(90deg)}.rotate-135{-webkit-transform:rotate(135deg);transform:rotate(135deg)}.rotate-180{-webkit-transform:rotate(180deg);transform:rotate(180deg)}.rotate-225{-webkit-transform:rotate(225deg);transform:rotate(225deg)}.rotate-270{-webkit-transform:rotate(270deg);transform:rotate(270deg)}.rotate-315{-webkit-transform:rotate(315deg);transform:rotate(315deg)}.black-90{color:rgba(0,0,0,.9)}.black-80{color:rgba(0,0,0,.8)}.black-70{color:rgba(0,0,0,.7)}.black-60{color:rgba(0,0,0,.6)}.black-50{color:rgba(0,0,0,.5)}.black-40{color:rgba(0,0,0,.4)}.black-30{color:rgba(0,0,0,.3)}.black-20{color:rgba(0,0,0,.2)}.black-10{color:rgba(0,0,0,.1)}.black-05{color:rgba(0,0,0,.05)}.white-90{color:hsla(0,0%,100%,.9)}.white-80{color:hsla(0,0%,100%,.8)}.white-70{color:hsla(0,0%,100%,.7)}.white-60{color:hsla(0,0%,100%,.6)}.white-50{color:hsla(0,0%,100%,.5)}.white-40{color:hsla(0,0%,100%,.4)}.white-30{color:hsla(0,0%,100%,.3)}.white-20{color:hsla(0,0%,100%,.2)}.white-10{color:hsla(0,0%,100%,.1)}.black{color:#000}.near-black{color:#111}.dark-gray{color:#333}.mid-gray{color:#555}.gray{color:#777}.silver{color:#999}.light-silver{color:#aaa}.moon-gray{color:#ccc}.light-gray{color:#eee}.near-white{color:#f4f4f4}.white{color:#fff}.dark-red{color:#e7040f}.red{color:#ff4136}.light-red{color:#ff725c}.orange{color:#ff6300}.gold{color:#ffb700}.yellow{color:gold}.light-yellow{color:#fbf1a9}.purple{color:#5e2ca5}.light-purple{color:#a463f2}.dark-pink{color:#d5008f}.hot-pink{color:#ff41b4}.pink{color:#ff80cc}.light-pink{color:#ffa3d7}.dark-green{color:#137752}.green{color:#19a974}.light-green{color:#9eebcf}.navy{color:#001b44}.dark-blue{color:#00449e}.blue{color:#357edd}.light-blue{color:#96ccff}.lightest-blue{color:#cdecff}.washed-blue{color:#f6fffe}.washed-green{color:#e8fdf5}.washed-yellow{color:#fffceb}.washed-red{color:#ffdfdf}.color-inherit{color:inherit}.bg-black-90{background-color:rgba(0,0,0,.9)}.bg-black-80{background-color:rgba(0,0,0,.8)}.bg-black-70{background-color:rgba(0,0,0,.7)}.bg-black-60{background-color:rgba(0,0,0,.6)}.bg-black-50{background-color:rgba(0,0,0,.5)}.bg-black-40{background-color:rgba(0,0,0,.4)}.bg-black-30{background-color:rgba(0,0,0,.3)}.bg-black-20{background-color:rgba(0,0,0,.2)}.bg-black-10{background-color:rgba(0,0,0,.1)}.bg-black-05{background-color:rgba(0,0,0,.05)}.bg-white-90{background-color:hsla(0,0%,100%,.9)}.bg-white-80{background-color:hsla(0,0%,100%,.8)}.bg-white-70{background-color:hsla(0,0%,100%,.7)}.bg-white-60{background-color:hsla(0,0%,100%,.6)}.bg-white-50{background-color:hsla(0,0%,100%,.5)}.bg-white-40{background-color:hsla(0,0%,100%,.4)}.bg-white-30{background-color:hsla(0,0%,100%,.3)}.bg-white-20{background-color:hsla(0,0%,100%,.2)}.bg-white-10{background-color:hsla(0,0%,100%,.1)}.bg-black{background-color:#000}.bg-near-black{background-color:#111}.bg-dark-gray{background-color:#333}.bg-mid-gray{background-color:#555}.bg-gray{background-color:#777}.bg-silver{background-color:#999}.bg-light-silver{background-color:#aaa}.bg-moon-gray{background-color:#ccc}.bg-light-gray{background-color:#eee}.bg-near-white{background-color:#f4f4f4}.bg-white{background-color:#fff}.bg-transparent{background-color:transparent}.bg-dark-red{background-color:#e7040f}.bg-red{background-color:#ff4136}.bg-light-red{background-color:#ff725c}.bg-orange{background-color:#ff6300}.bg-gold{background-color:#ffb700}.bg-yellow{background-color:gold}.bg-light-yellow{background-color:#fbf1a9}.bg-purple{background-color:#5e2ca5}.bg-light-purple{background-color:#a463f2}.bg-dark-pink{background-color:#d5008f}.bg-hot-pink{background-color:#ff41b4}.bg-pink{background-color:#ff80cc}.bg-light-pink{background-color:#ffa3d7}.bg-dark-green{background-color:#137752}.bg-green{background-color:#19a974}.bg-light-green{background-color:#9eebcf}.bg-navy{background-color:#001b44}.bg-dark-blue{background-color:#00449e}.bg-blue{background-color:#357edd}.bg-light-blue{background-color:#96ccff}.bg-lightest-blue{background-color:#cdecff}.bg-washed-blue{background-color:#f6fffe}.bg-washed-green{background-color:#e8fdf5}.bg-washed-yellow{background-color:#fffceb}.bg-washed-red{background-color:#ffdfdf}.bg-inherit{background-color:inherit}.hover-black:focus,.hover-black:hover{color:#000}.hover-near-black:focus,.hover-near-black:hover{color:#111}.hover-dark-gray:focus,.hover-dark-gray:hover{color:#333}.hover-mid-gray:focus,.hover-mid-gray:hover{color:#555}.hover-gray:focus,.hover-gray:hover{color:#777}.hover-silver:focus,.hover-silver:hover{color:#999}.hover-light-silver:focus,.hover-light-silver:hover{color:#aaa}.hover-moon-gray:focus,.hover-moon-gray:hover{color:#ccc}.hover-light-gray:focus,.hover-light-gray:hover{color:#eee}.hover-near-white:focus,.hover-near-white:hover{color:#f4f4f4}.hover-white:focus,.hover-white:hover{color:#fff}.hover-black-90:focus,.hover-black-90:hover{color:rgba(0,0,0,.9)}.hover-black-80:focus,.hover-black-80:hover{color:rgba(0,0,0,.8)}.hover-black-70:focus,.hover-black-70:hover{color:rgba(0,0,0,.7)}.hover-black-60:focus,.hover-black-60:hover{color:rgba(0,0,0,.6)}.hover-black-50:focus,.hover-black-50:hover{color:rgba(0,0,0,.5)}.hover-black-40:focus,.hover-black-40:hover{color:rgba(0,0,0,.4)}.hover-black-30:focus,.hover-black-30:hover{color:rgba(0,0,0,.3)}.hover-black-20:focus,.hover-black-20:hover{color:rgba(0,0,0,.2)}.hover-black-10:focus,.hover-black-10:hover{color:rgba(0,0,0,.1)}.hover-white-90:focus,.hover-white-90:hover{color:hsla(0,0%,100%,.9)}.hover-white-80:focus,.hover-white-80:hover{color:hsla(0,0%,100%,.8)}.hover-white-70:focus,.hover-white-70:hover{color:hsla(0,0%,100%,.7)}.hover-white-60:focus,.hover-white-60:hover{color:hsla(0,0%,100%,.6)}.hover-white-50:focus,.hover-white-50:hover{color:hsla(0,0%,100%,.5)}.hover-white-40:focus,.hover-white-40:hover{color:hsla(0,0%,100%,.4)}.hover-white-30:focus,.hover-white-30:hover{color:hsla(0,0%,100%,.3)}.hover-white-20:focus,.hover-white-20:hover{color:hsla(0,0%,100%,.2)}.hover-white-10:focus,.hover-white-10:hover{color:hsla(0,0%,100%,.1)}.hover-inherit:focus,.hover-inherit:hover{color:inherit}.hover-bg-black:focus,.hover-bg-black:hover{background-color:#000}.hover-bg-near-black:focus,.hover-bg-near-black:hover{background-color:#111}.hover-bg-dark-gray:focus,.hover-bg-dark-gray:hover{background-color:#333}.hover-bg-mid-gray:focus,.hover-bg-mid-gray:hover{background-color:#555}.hover-bg-gray:focus,.hover-bg-gray:hover{background-color:#777}.hover-bg-silver:focus,.hover-bg-silver:hover{background-color:#999}.hover-bg-light-silver:focus,.hover-bg-light-silver:hover{background-color:#aaa}.hover-bg-moon-gray:focus,.hover-bg-moon-gray:hover{background-color:#ccc}.hover-bg-light-gray:focus,.hover-bg-light-gray:hover{background-color:#eee}.hover-bg-near-white:focus,.hover-bg-near-white:hover{background-color:#f4f4f4}.hover-bg-white:focus,.hover-bg-white:hover{background-color:#fff}.hover-bg-transparent:focus,.hover-bg-transparent:hover{background-color:transparent}.hover-bg-black-90:focus,.hover-bg-black-90:hover{background-color:rgba(0,0,0,.9)}.hover-bg-black-80:focus,.hover-bg-black-80:hover{background-color:rgba(0,0,0,.8)}.hover-bg-black-70:focus,.hover-bg-black-70:hover{background-color:rgba(0,0,0,.7)}.hover-bg-black-60:focus,.hover-bg-black-60:hover{background-color:rgba(0,0,0,.6)}.hover-bg-black-50:focus,.hover-bg-black-50:hover{background-color:rgba(0,0,0,.5)}.hover-bg-black-40:focus,.hover-bg-black-40:hover{background-color:rgba(0,0,0,.4)}.hover-bg-black-30:focus,.hover-bg-black-30:hover{background-color:rgba(0,0,0,.3)}.hover-bg-black-20:focus,.hover-bg-black-20:hover{background-color:rgba(0,0,0,.2)}.hover-bg-black-10:focus,.hover-bg-black-10:hover{background-color:rgba(0,0,0,.1)}.hover-bg-white-90:focus,.hover-bg-white-90:hover{background-color:hsla(0,0%,100%,.9)}.hover-bg-white-80:focus,.hover-bg-white-80:hover{background-color:hsla(0,0%,100%,.8)}.hover-bg-white-70:focus,.hover-bg-white-70:hover{background-color:hsla(0,0%,100%,.7)}.hover-bg-white-60:focus,.hover-bg-white-60:hover{background-color:hsla(0,0%,100%,.6)}.hover-bg-white-50:focus,.hover-bg-white-50:hover{background-color:hsla(0,0%,100%,.5)}.hover-bg-white-40:focus,.hover-bg-white-40:hover{background-color:hsla(0,0%,100%,.4)}.hover-bg-white-30:focus,.hover-bg-white-30:hover{background-color:hsla(0,0%,100%,.3)}.hover-bg-white-20:focus,.hover-bg-white-20:hover{background-color:hsla(0,0%,100%,.2)}.hover-bg-white-10:focus,.hover-bg-white-10:hover{background-color:hsla(0,0%,100%,.1)}.hover-dark-red:focus,.hover-dark-red:hover{color:#e7040f}.hover-red:focus,.hover-red:hover{color:#ff4136}.hover-light-red:focus,.hover-light-red:hover{color:#ff725c}.hover-orange:focus,.hover-orange:hover{color:#ff6300}.hover-gold:focus,.hover-gold:hover{color:#ffb700}.hover-yellow:focus,.hover-yellow:hover{color:gold}.hover-light-yellow:focus,.hover-light-yellow:hover{color:#fbf1a9}.hover-purple:focus,.hover-purple:hover{color:#5e2ca5}.hover-light-purple:focus,.hover-light-purple:hover{color:#a463f2}.hover-dark-pink:focus,.hover-dark-pink:hover{color:#d5008f}.hover-hot-pink:focus,.hover-hot-pink:hover{color:#ff41b4}.hover-pink:focus,.hover-pink:hover{color:#ff80cc}.hover-light-pink:focus,.hover-light-pink:hover{color:#ffa3d7}.hover-dark-green:focus,.hover-dark-green:hover{color:#137752}.hover-green:focus,.hover-green:hover{color:#19a974}.hover-light-green:focus,.hover-light-green:hover{color:#9eebcf}.hover-navy:focus,.hover-navy:hover{color:#001b44}.hover-dark-blue:focus,.hover-dark-blue:hover{color:#00449e}.hover-blue:focus,.hover-blue:hover{color:#357edd}.hover-light-blue:focus,.hover-light-blue:hover{color:#96ccff}.hover-lightest-blue:focus,.hover-lightest-blue:hover{color:#cdecff}.hover-washed-blue:focus,.hover-washed-blue:hover{color:#f6fffe}.hover-washed-green:focus,.hover-washed-green:hover{color:#e8fdf5}.hover-washed-yellow:focus,.hover-washed-yellow:hover{color:#fffceb}.hover-washed-red:focus,.hover-washed-red:hover{color:#ffdfdf}.hover-bg-dark-red:focus,.hover-bg-dark-red:hover{background-color:#e7040f}.hover-bg-red:focus,.hover-bg-red:hover{background-color:#ff4136}.hover-bg-light-red:focus,.hover-bg-light-red:hover{background-color:#ff725c}.hover-bg-orange:focus,.hover-bg-orange:hover{background-color:#ff6300}.hover-bg-gold:focus,.hover-bg-gold:hover{background-color:#ffb700}.hover-bg-yellow:focus,.hover-bg-yellow:hover{background-color:gold}.hover-bg-light-yellow:focus,.hover-bg-light-yellow:hover{background-color:#fbf1a9}.hover-bg-purple:focus,.hover-bg-purple:hover{background-color:#5e2ca5}.hover-bg-light-purple:focus,.hover-bg-light-purple:hover{background-color:#a463f2}.hover-bg-dark-pink:focus,.hover-bg-dark-pink:hover{background-color:#d5008f}.hover-bg-hot-pink:focus,.hover-bg-hot-pink:hover{background-color:#ff41b4}.hover-bg-pink:focus,.hover-bg-pink:hover{background-color:#ff80cc}.hover-bg-light-pink:focus,.hover-bg-light-pink:hover{background-color:#ffa3d7}.hover-bg-dark-green:focus,.hover-bg-dark-green:hover{background-color:#137752}.hover-bg-green:focus,.hover-bg-green:hover{background-color:#19a974}.hover-bg-light-green:focus,.hover-bg-light-green:hover{background-color:#9eebcf}.hover-bg-navy:focus,.hover-bg-navy:hover{background-color:#001b44}.hover-bg-dark-blue:focus,.hover-bg-dark-blue:hover{background-color:#00449e}.hover-bg-blue:focus,.hover-bg-blue:hover{background-color:#357edd}.hover-bg-light-blue:focus,.hover-bg-light-blue:hover{background-color:#96ccff}.hover-bg-lightest-blue:focus,.hover-bg-lightest-blue:hover{background-color:#cdecff}.hover-bg-washed-blue:focus,.hover-bg-washed-blue:hover{background-color:#f6fffe}.hover-bg-washed-green:focus,.hover-bg-washed-green:hover{background-color:#e8fdf5}.hover-bg-washed-yellow:focus,.hover-bg-washed-yellow:hover{background-color:#fffceb}.hover-bg-washed-red:focus,.hover-bg-washed-red:hover{background-color:#ffdfdf}.hover-bg-inherit:focus,.hover-bg-inherit:hover{background-color:inherit}.pa0{padding:0}.pa1{padding:.25rem}.pa2{padding:.5rem}.pa3{padding:1rem}.pa4{padding:2rem}.pa5{padding:4rem}.pa6{padding:8rem}.pa7{padding:16rem}.pl0{padding-left:0}.pl1{padding-left:.25rem}.pl2{padding-left:.5rem}.pl3{padding-left:1rem}.pl4{padding-left:2rem}.pl5{padding-left:4rem}.pl6{padding-left:8rem}.pl7{padding-left:16rem}.pr0{padding-right:0}.pr1{padding-right:.25rem}.pr2{padding-right:.5rem}.pr3{padding-right:1rem}.pr4{padding-right:2rem}.pr5{padding-right:4rem}.pr6{padding-right:8rem}.pr7{padding-right:16rem}.pb0{padding-bottom:0}.pb1{padding-bottom:.25rem}.pb2{padding-bottom:.5rem}.pb3{padding-bottom:1rem}.pb4{padding-bottom:2rem}.pb5{padding-bottom:4rem}.pb6{padding-bottom:8rem}.pb7{padding-bottom:16rem}.pt0{padding-top:0}.pt1{padding-top:.25rem}.pt2{padding-top:.5rem}.pt3{padding-top:1rem}.pt4{padding-top:2rem}.pt5{padding-top:4rem}.pt6{padding-top:8rem}.pt7{padding-top:16rem}.pv0{padding-top:0;padding-bottom:0}.pv1{padding-top:.25rem;padding-bottom:.25rem}.pv2{padding-top:.5rem;padding-bottom:.5rem}.pv3{padding-top:1rem;padding-bottom:1rem}.pv4{padding-top:2rem;padding-bottom:2rem}.pv5{padding-top:4rem;padding-bottom:4rem}.pv6{padding-top:8rem;padding-bottom:8rem}.pv7{padding-top:16rem;padding-bottom:16rem}.ph0{padding-left:0;padding-right:0}.ph1{padding-left:.25rem;padding-right:.25rem}.ph2{padding-left:.5rem;padding-right:.5rem}.ph3{padding-left:1rem;padding-right:1rem}.ph4{padding-left:2rem;padding-right:2rem}.ph5{padding-left:4rem;padding-right:4rem}.ph6{padding-left:8rem;padding-right:8rem}.ph7{padding-left:16rem;padding-right:16rem}.ma0{margin:0}.ma1{margin:.25rem}.ma2{margin:.5rem}.ma3{margin:1rem}.ma4{margin:2rem}.ma5{margin:4rem}.ma6{margin:8rem}.ma7{margin:16rem}.ml0{margin-left:0}.ml1{margin-left:.25rem}.ml2{margin-left:.5rem}.ml3{margin-left:1rem}.ml4{margin-left:2rem}.ml5{margin-left:4rem}.ml6{margin-left:8rem}.ml7{margin-left:16rem}.mr0{margin-right:0}.mr1{margin-right:.25rem}.mr2{margin-right:.5rem}.mr3{margin-right:1rem}.mr4{margin-right:2rem}.mr5{margin-right:4rem}.mr6{margin-right:8rem}.mr7{margin-right:16rem}.mb0{margin-bottom:0}.mb1{margin-bottom:.25rem}.mb2{margin-bottom:.5rem}.mb3{margin-bottom:1rem}.mb4{margin-bottom:2rem}.mb5{margin-bottom:4rem}.mb6{margin-bottom:8rem}.mb7{margin-bottom:16rem}.mt0{margin-top:0}.mt1{margin-top:.25rem}.mt2{margin-top:.5rem}.mt3{margin-top:1rem}.mt4{margin-top:2rem}.mt5{margin-top:4rem}.mt6{margin-top:8rem}.mt7{margin-top:16rem}.mv0{margin-top:0;margin-bottom:0}.mv1{margin-top:.25rem;margin-bottom:.25rem}.mv2{margin-top:.5rem;margin-bottom:.5rem}.mv3{margin-top:1rem;margin-bottom:1rem}.mv4{margin-top:2rem;margin-bottom:2rem}.mv5{margin-top:4rem;margin-bottom:4rem}.mv6{margin-top:8rem;margin-bottom:8rem}.mv7{margin-top:16rem;margin-bottom:16rem}.mh0{margin-left:0;margin-right:0}.mh1{margin-left:.25rem;margin-right:.25rem}.mh2{margin-left:.5rem;margin-right:.5rem}.mh3{margin-left:1rem;margin-right:1rem}.mh4{margin-left:2rem;margin-right:2rem}.mh5{margin-left:4rem;margin-right:4rem}.mh6{margin-left:8rem;margin-right:8rem}.mh7{margin-left:16rem;margin-right:16rem}.na1{margin:-.25rem}.na2{margin:-.5rem}.na3{margin:-1rem}.na4{margin:-2rem}.na5{margin:-4rem}.na6{margin:-8rem}.na7{margin:-16rem}.nl1{margin-left:-.25rem}.nl2{margin-left:-.5rem}.nl3{margin-left:-1rem}.nl4{margin-left:-2rem}.nl5{margin-left:-4rem}.nl6{margin-left:-8rem}.nl7{margin-left:-16rem}.nr1{margin-right:-.25rem}.nr2{margin-right:-.5rem}.nr3{margin-right:-1rem}.nr4{margin-right:-2rem}.nr5{margin-right:-4rem}.nr6{margin-right:-8rem}.nr7{margin-right:-16rem}.nb1{margin-bottom:-.25rem}.nb2{margin-bottom:-.5rem}.nb3{margin-bottom:-1rem}.nb4{margin-bottom:-2rem}.nb5{margin-bottom:-4rem}.nb6{margin-bottom:-8rem}.nb7{margin-bottom:-16rem}.nt1{margin-top:-.25rem}.nt2{margin-top:-.5rem}.nt3{margin-top:-1rem}.nt4{margin-top:-2rem}.nt5{margin-top:-4rem}.nt6{margin-top:-8rem}.nt7{margin-top:-16rem}.collapse{border-collapse:collapse;border-spacing:0}.striped--light-silver:nth-child(odd){background-color:#aaa}.striped--moon-gray:nth-child(odd){background-color:#ccc}.striped--light-gray:nth-child(odd){background-color:#eee}.striped--near-white:nth-child(odd){background-color:#f4f4f4}.stripe-light:nth-child(odd){background-color:hsla(0,0%,100%,.1)}.stripe-dark:nth-child(odd){background-color:rgba(0,0,0,.1)}.strike{text-decoration:line-through}.underline{text-decoration:underline}.no-underline{text-decoration:none}.tl{text-align:left}.tr{text-align:right}.tc{text-align:center}.tj{text-align:justify}.ttc{text-transform:capitalize}.ttl{text-transform:lowercase}.ttu{text-transform:uppercase}.ttn{text-transform:none}.f-6,.f-headline{font-size:6rem}.f-5,.f-subheadline{font-size:5rem}.f1{font-size:3rem}.f2{font-size:2.25rem}.f3{font-size:1.5rem}.f4{font-size:1.25rem}.f5{font-size:1rem}.f6{font-size:.875rem}.f7{font-size:.75rem}.measure{max-width:30em}.measure-wide{max-width:34em}.measure-narrow{max-width:20em}.indent{text-indent:1em;margin-top:0;margin-bottom:0}.small-caps{font-variant:small-caps}.truncate{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.overflow-container{overflow-y:scroll}.center{margin-left:auto}.center,.mr-auto{margin-right:auto}.ml-auto{margin-left:auto}.clip{position:fixed!important;_position:absolute!important;clip:rect(1px 1px 1px 1px);clip:rect(1px,1px,1px,1px)}.ws-normal{white-space:normal}.nowrap{white-space:nowrap}.pre{white-space:pre}.v-base{vertical-align:baseline}.v-mid{vertical-align:middle}.v-top{vertical-align:top}.v-btm{vertical-align:bottom}.dim{opacity:1}.dim,.dim:focus,.dim:hover{transition:opacity .15s ease-in}.dim:focus,.dim:hover{opacity:.5}.dim:active{opacity:.8;transition:opacity .15s ease-out}.glow,.glow:focus,.glow:hover{transition:opacity .15s ease-in}.glow:focus,.glow:hover{opacity:1}.hide-child .child{opacity:0;transition:opacity .15s ease-in}.hide-child:active .child,.hide-child:focus .child,.hide-child:hover .child{opacity:1;transition:opacity .15s ease-in}.underline-hover:focus,.underline-hover:hover{text-decoration:underline}.grow{-moz-osx-font-smoothing:grayscale;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-transform:translateZ(0);transform:translateZ(0);transition:-webkit-transform .25s ease-out;transition:transform .25s ease-out;transition:transform .25s ease-out,-webkit-transform .25s ease-out}.grow:focus,.grow:hover{-webkit-transform:scale(1.05);transform:scale(1.05)}.grow:active{-webkit-transform:scale(.9);transform:scale(.9)}.grow-large{-moz-osx-font-smoothing:grayscale;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-transform:translateZ(0);transform:translateZ(0);transition:-webkit-transform .25s ease-in-out;transition:transform .25s ease-in-out;transition:transform .25s ease-in-out,-webkit-transform .25s ease-in-out}.grow-large:focus,.grow-large:hover{-webkit-transform:scale(1.2);transform:scale(1.2)}.grow-large:active{-webkit-transform:scale(.95);transform:scale(.95)}.pointer:hover,.shadow-hover{cursor:pointer}.shadow-hover{position:relative;transition:all .5s cubic-bezier(.165,.84,.44,1)}.shadow-hover:after{content:"";box-shadow:0 0 16px 2px rgba(0,0,0,.2);border-radius:inherit;opacity:0;position:absolute;top:0;left:0;width:100%;height:100%;z-index:-1;transition:opacity .5s cubic-bezier(.165,.84,.44,1)}.shadow-hover:focus:after,.shadow-hover:hover:after{opacity:1}.bg-animate,.bg-animate:focus,.bg-animate:hover{transition:background-color .15s ease-in-out}.z-0{z-index:0}.z-1{z-index:1}.z-2{z-index:2}.z-3{z-index:3}.z-4{z-index:4}.z-5{z-index:5}.z-999{z-index:999}.z-9999{z-index:9999}.z-max{z-index:2147483647}.z-inherit{z-index:inherit}.z-initial{z-index:auto}.z-unset{z-index:unset}.nested-copy-line-height ol,.nested-copy-line-height p,.nested-copy-line-height ul{line-height:1.5}.nested-headline-line-height h1,.nested-headline-line-height h2,.nested-headline-line-height h3,.nested-headline-line-height h4,.nested-headline-line-height h5,.nested-headline-line-height h6{line-height:1.25}.nested-list-reset ol,.nested-list-reset ul{padding-left:0;margin-left:0;list-style-type:none}.nested-copy-indent p+p{text-indent:1em;margin-top:0;margin-bottom:0}.nested-copy-separator p+p{margin-top:1.5em}.nested-img img{width:100%;max-width:100%;display:block}.nested-links a{color:#357edd;transition:color .15s ease-in}.nested-links a:focus,.nested-links a:hover{color:#96ccff;transition:color .15s ease-in}.debug *{outline:1px solid gold}.debug-white *{outline:1px solid #fff}.debug-black *{outline:1px solid #000}.debug-grid{background:transparent url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAAFElEQVR4AWPAC97/9x0eCsAEPgwAVLshdpENIxcAAAAASUVORK5CYII=) repeat 0 0}.debug-grid-16{background:transparent url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAMklEQVR4AWOgCLz/b0epAa6UGuBOqQHOQHLUgFEDnAbcBZ4UGwDOkiCnkIhdgNgNxAYAiYlD+8sEuo8AAAAASUVORK5CYII=) repeat 0 0}.debug-grid-8-solid{background:#fff url(data:image/gif;base64,R0lGODdhCAAIAPEAAADw/wDx/////wAAACwAAAAACAAIAAACDZQvgaeb/lxbAIKA8y0AOw==) repeat 0 0}.debug-grid-16-solid{background:#fff url(data:image/gif;base64,R0lGODdhEAAQAPEAAADw/wDx/xXy/////ywAAAAAEAAQAAACIZyPKckYDQFsb6ZqD85jZ2+BkwiRFKehhqQCQgDHcgwEBQA7) repeat 0 0}@media screen and (min-width:30em){.aspect-ratio-ns{height:0;position:relative}.aspect-ratio--16x9-ns{padding-bottom:56.25%}.aspect-ratio--9x16-ns{padding-bottom:177.77%}.aspect-ratio--4x3-ns{padding-bottom:75%}.aspect-ratio--3x4-ns{padding-bottom:133.33%}.aspect-ratio--6x4-ns{padding-bottom:66.6%}.aspect-ratio--4x6-ns{padding-bottom:150%}.aspect-ratio--8x5-ns{padding-bottom:62.5%}.aspect-ratio--5x8-ns{padding-bottom:160%}.aspect-ratio--7x5-ns{padding-bottom:71.42%}.aspect-ratio--5x7-ns{padding-bottom:140%}.aspect-ratio--1x1-ns{padding-bottom:100%}.aspect-ratio--object-ns{position:absolute;top:0;right:0;bottom:0;left:0;width:100%;height:100%;z-index:100}.cover-ns{background-size:cover!important}.contain-ns{background-size:contain!important}.bg-center-ns{background-position:50%}.bg-center-ns,.bg-top-ns{background-repeat:no-repeat}.bg-top-ns{background-position:top}.bg-right-ns{background-position:100%}.bg-bottom-ns,.bg-right-ns{background-repeat:no-repeat}.bg-bottom-ns{background-position:bottom}.bg-left-ns{background-repeat:no-repeat;background-position:0}.outline-ns{outline:1px solid}.outline-transparent-ns{outline:1px solid transparent}.outline-0-ns{outline:0}.ba-ns{border-style:solid;border-width:1px}.bt-ns{border-top-style:solid;border-top-width:1px}.br-ns{border-right-style:solid;border-right-width:1px}.bb-ns{border-bottom-style:solid;border-bottom-width:1px}.bl-ns{border-left-style:solid;border-left-width:1px}.bn-ns{border-style:none;border-width:0}.br0-ns{border-radius:0}.br1-ns{border-radius:.125rem}.br2-ns{border-radius:.25rem}.br3-ns{border-radius:.5rem}.br4-ns{border-radius:1rem}.br-100-ns{border-radius:100%}.br-pill-ns{border-radius:9999px}.br--bottom-ns{border-top-left-radius:0;border-top-right-radius:0}.br--top-ns{border-bottom-right-radius:0}.br--right-ns,.br--top-ns{border-bottom-left-radius:0}.br--right-ns{border-top-left-radius:0}.br--left-ns{border-top-right-radius:0;border-bottom-right-radius:0}.b--dotted-ns{border-style:dotted}.b--dashed-ns{border-style:dashed}.b--solid-ns{border-style:solid}.b--none-ns{border-style:none}.bw0-ns{border-width:0}.bw1-ns{border-width:.125rem}.bw2-ns{border-width:.25rem}.bw3-ns{border-width:.5rem}.bw4-ns{border-width:1rem}.bw5-ns{border-width:2rem}.bt-0-ns{border-top-width:0}.br-0-ns{border-right-width:0}.bb-0-ns{border-bottom-width:0}.bl-0-ns{border-left-width:0}.shadow-1-ns{box-shadow:0 0 4px 2px rgba(0,0,0,.2)}.shadow-2-ns{box-shadow:0 0 8px 2px rgba(0,0,0,.2)}.shadow-3-ns{box-shadow:2px 2px 4px 2px rgba(0,0,0,.2)}.shadow-4-ns{box-shadow:2px 2px 8px 0 rgba(0,0,0,.2)}.shadow-5-ns{box-shadow:4px 4px 8px 0 rgba(0,0,0,.2)}.top-0-ns{top:0}.left-0-ns{left:0}.right-0-ns{right:0}.bottom-0-ns{bottom:0}.top-1-ns{top:1rem}.left-1-ns{left:1rem}.right-1-ns{right:1rem}.bottom-1-ns{bottom:1rem}.top-2-ns{top:2rem}.left-2-ns{left:2rem}.right-2-ns{right:2rem}.bottom-2-ns{bottom:2rem}.top--1-ns{top:-1rem}.right--1-ns{right:-1rem}.bottom--1-ns{bottom:-1rem}.left--1-ns{left:-1rem}.top--2-ns{top:-2rem}.right--2-ns{right:-2rem}.bottom--2-ns{bottom:-2rem}.left--2-ns{left:-2rem}.absolute--fill-ns{top:0;right:0;bottom:0;left:0}.cl-ns{clear:left}.cr-ns{clear:right}.cb-ns{clear:both}.cn-ns{clear:none}.dn-ns{display:none}.di-ns{display:inline}.db-ns{display:block}.dib-ns{display:inline-block}.dit-ns{display:inline-table}.dt-ns{display:table}.dtc-ns{display:table-cell}.dt-row-ns{display:table-row}.dt-row-group-ns{display:table-row-group}.dt-column-ns{display:table-column}.dt-column-group-ns{display:table-column-group}.dt--fixed-ns{table-layout:fixed;width:100%}.flex-ns{display:-webkit-box;display:-ms-flexbox;display:flex}.inline-flex-ns{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex}.flex-auto-ns{-webkit-box-flex:1;-ms-flex:1 1 auto;flex:1 1 auto;min-width:0;min-height:0}.flex-none-ns{-webkit-box-flex:0;-ms-flex:none;flex:none}.flex-column-ns{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.flex-row-ns{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.flex-wrap-ns{-ms-flex-wrap:wrap;flex-wrap:wrap}.flex-nowrap-ns{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.flex-wrap-reverse-ns{-ms-flex-wrap:wrap-reverse;flex-wrap:wrap-reverse}.flex-column-reverse-ns{-webkit-box-orient:vertical;-webkit-box-direction:reverse;-ms-flex-direction:column-reverse;flex-direction:column-reverse}.flex-row-reverse-ns{-webkit-box-orient:horizontal;-webkit-box-direction:reverse;-ms-flex-direction:row-reverse;flex-direction:row-reverse}.items-start-ns{-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start}.items-end-ns{-webkit-box-align:end;-ms-flex-align:end;align-items:flex-end}.items-center-ns{-webkit-box-align:center;-ms-flex-align:center;align-items:center}.items-baseline-ns{-webkit-box-align:baseline;-ms-flex-align:baseline;align-items:baseline}.items-stretch-ns{-webkit-box-align:stretch;-ms-flex-align:stretch;align-items:stretch}.self-start-ns{-ms-flex-item-align:start;align-self:flex-start}.self-end-ns{-ms-flex-item-align:end;align-self:flex-end}.self-center-ns{-ms-flex-item-align:center;-ms-grid-row-align:center;align-self:center}.self-baseline-ns{-ms-flex-item-align:baseline;align-self:baseline}.self-stretch-ns{-ms-flex-item-align:stretch;-ms-grid-row-align:stretch;align-self:stretch}.justify-start-ns{-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.justify-end-ns{-webkit-box-pack:end;-ms-flex-pack:end;justify-content:flex-end}.justify-center-ns{-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.justify-between-ns{-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.justify-around-ns{-ms-flex-pack:distribute;justify-content:space-around}.content-start-ns{-ms-flex-line-pack:start;align-content:flex-start}.content-end-ns{-ms-flex-line-pack:end;align-content:flex-end}.content-center-ns{-ms-flex-line-pack:center;align-content:center}.content-between-ns{-ms-flex-line-pack:justify;align-content:space-between}.content-around-ns{-ms-flex-line-pack:distribute;align-content:space-around}.content-stretch-ns{-ms-flex-line-pack:stretch;align-content:stretch}.order-0-ns{-webkit-box-ordinal-group:1;-ms-flex-order:0;order:0}.order-1-ns{-webkit-box-ordinal-group:2;-ms-flex-order:1;order:1}.order-2-ns{-webkit-box-ordinal-group:3;-ms-flex-order:2;order:2}.order-3-ns{-webkit-box-ordinal-group:4;-ms-flex-order:3;order:3}.order-4-ns{-webkit-box-ordinal-group:5;-ms-flex-order:4;order:4}.order-5-ns{-webkit-box-ordinal-group:6;-ms-flex-order:5;order:5}.order-6-ns{-webkit-box-ordinal-group:7;-ms-flex-order:6;order:6}.order-7-ns{-webkit-box-ordinal-group:8;-ms-flex-order:7;order:7}.order-8-ns{-webkit-box-ordinal-group:9;-ms-flex-order:8;order:8}.order-last-ns{-webkit-box-ordinal-group:100000;-ms-flex-order:99999;order:99999}.flex-grow-0-ns{-webkit-box-flex:0;-ms-flex-positive:0;flex-grow:0}.flex-grow-1-ns{-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1}.flex-shrink-0-ns{-ms-flex-negative:0;flex-shrink:0}.flex-shrink-1-ns{-ms-flex-negative:1;flex-shrink:1}.fl-ns{float:left}.fl-ns,.fr-ns{display:inline}.fr-ns{float:right}.fn-ns{float:none}.i-ns{font-style:italic}.fs-normal-ns{font-style:normal}.normal-ns{font-weight:400}.b-ns{font-weight:700}.fw1-ns{font-weight:100}.fw2-ns{font-weight:200}.fw3-ns{font-weight:300}.fw4-ns{font-weight:400}.fw5-ns{font-weight:500}.fw6-ns{font-weight:600}.fw7-ns{font-weight:700}.fw8-ns{font-weight:800}.fw9-ns{font-weight:900}.h1-ns{height:1rem}.h2-ns{height:2rem}.h3-ns{height:4rem}.h4-ns{height:8rem}.h5-ns{height:16rem}.h-25-ns{height:25%}.h-50-ns{height:50%}.h-75-ns{height:75%}.h-100-ns{height:100%}.min-h-100-ns{min-height:100%}.vh-25-ns{height:25vh}.vh-50-ns{height:50vh}.vh-75-ns{height:75vh}.vh-100-ns{height:100vh}.min-vh-100-ns{min-height:100vh}.h-auto-ns{height:auto}.h-inherit-ns{height:inherit}.tracked-ns{letter-spacing:.1em}.tracked-tight-ns{letter-spacing:-.05em}.tracked-mega-ns{letter-spacing:.25em}.lh-solid-ns{line-height:1}.lh-title-ns{line-height:1.25}.lh-copy-ns{line-height:1.5}.mw-100-ns{max-width:100%}.mw1-ns{max-width:1rem}.mw2-ns{max-width:2rem}.mw3-ns{max-width:4rem}.mw4-ns{max-width:8rem}.mw5-ns{max-width:16rem}.mw6-ns{max-width:32rem}.mw7-ns{max-width:48rem}.mw8-ns{max-width:64rem}.mw9-ns{max-width:96rem}.mw-none-ns{max-width:none}.w1-ns{width:1rem}.w2-ns{width:2rem}.w3-ns{width:4rem}.w4-ns{width:8rem}.w5-ns{width:16rem}.w-10-ns{width:10%}.w-20-ns{width:20%}.w-25-ns{width:25%}.w-30-ns{width:30%}.w-33-ns{width:33%}.w-34-ns{width:34%}.w-40-ns{width:40%}.w-50-ns{width:50%}.w-60-ns{width:60%}.w-70-ns{width:70%}.w-75-ns{width:75%}.w-80-ns{width:80%}.w-90-ns{width:90%}.w-100-ns{width:100%}.w-third-ns{width:33.33333%}.w-two-thirds-ns{width:66.66667%}.w-auto-ns{width:auto}.overflow-visible-ns{overflow:visible}.overflow-hidden-ns{overflow:hidden}.overflow-scroll-ns{overflow:scroll}.overflow-auto-ns{overflow:auto}.overflow-x-visible-ns{overflow-x:visible}.overflow-x-hidden-ns{overflow-x:hidden}.overflow-x-scroll-ns{overflow-x:scroll}.overflow-x-auto-ns{overflow-x:auto}.overflow-y-visible-ns{overflow-y:visible}.overflow-y-hidden-ns{overflow-y:hidden}.overflow-y-scroll-ns{overflow-y:scroll}.overflow-y-auto-ns{overflow-y:auto}.static-ns{position:static}.relative-ns{position:relative}.absolute-ns{position:absolute}.fixed-ns{position:fixed}.rotate-45-ns{-webkit-transform:rotate(45deg);transform:rotate(45deg)}.rotate-90-ns{-webkit-transform:rotate(90deg);transform:rotate(90deg)}.rotate-135-ns{-webkit-transform:rotate(135deg);transform:rotate(135deg)}.rotate-180-ns{-webkit-transform:rotate(180deg);transform:rotate(180deg)}.rotate-225-ns{-webkit-transform:rotate(225deg);transform:rotate(225deg)}.rotate-270-ns{-webkit-transform:rotate(270deg);transform:rotate(270deg)}.rotate-315-ns{-webkit-transform:rotate(315deg);transform:rotate(315deg)}.pa0-ns{padding:0}.pa1-ns{padding:.25rem}.pa2-ns{padding:.5rem}.pa3-ns{padding:1rem}.pa4-ns{padding:2rem}.pa5-ns{padding:4rem}.pa6-ns{padding:8rem}.pa7-ns{padding:16rem}.pl0-ns{padding-left:0}.pl1-ns{padding-left:.25rem}.pl2-ns{padding-left:.5rem}.pl3-ns{padding-left:1rem}.pl4-ns{padding-left:2rem}.pl5-ns{padding-left:4rem}.pl6-ns{padding-left:8rem}.pl7-ns{padding-left:16rem}.pr0-ns{padding-right:0}.pr1-ns{padding-right:.25rem}.pr2-ns{padding-right:.5rem}.pr3-ns{padding-right:1rem}.pr4-ns{padding-right:2rem}.pr5-ns{padding-right:4rem}.pr6-ns{padding-right:8rem}.pr7-ns{padding-right:16rem}.pb0-ns{padding-bottom:0}.pb1-ns{padding-bottom:.25rem}.pb2-ns{padding-bottom:.5rem}.pb3-ns{padding-bottom:1rem}.pb4-ns{padding-bottom:2rem}.pb5-ns{padding-bottom:4rem}.pb6-ns{padding-bottom:8rem}.pb7-ns{padding-bottom:16rem}.pt0-ns{padding-top:0}.pt1-ns{padding-top:.25rem}.pt2-ns{padding-top:.5rem}.pt3-ns{padding-top:1rem}.pt4-ns{padding-top:2rem}.pt5-ns{padding-top:4rem}.pt6-ns{padding-top:8rem}.pt7-ns{padding-top:16rem}.pv0-ns{padding-top:0;padding-bottom:0}.pv1-ns{padding-top:.25rem;padding-bottom:.25rem}.pv2-ns{padding-top:.5rem;padding-bottom:.5rem}.pv3-ns{padding-top:1rem;padding-bottom:1rem}.pv4-ns{padding-top:2rem;padding-bottom:2rem}.pv5-ns{padding-top:4rem;padding-bottom:4rem}.pv6-ns{padding-top:8rem;padding-bottom:8rem}.pv7-ns{padding-top:16rem;padding-bottom:16rem}.ph0-ns{padding-left:0;padding-right:0}.ph1-ns{padding-left:.25rem;padding-right:.25rem}.ph2-ns{padding-left:.5rem;padding-right:.5rem}.ph3-ns{padding-left:1rem;padding-right:1rem}.ph4-ns{padding-left:2rem;padding-right:2rem}.ph5-ns{padding-left:4rem;padding-right:4rem}.ph6-ns{padding-left:8rem;padding-right:8rem}.ph7-ns{padding-left:16rem;padding-right:16rem}.ma0-ns{margin:0}.ma1-ns{margin:.25rem}.ma2-ns{margin:.5rem}.ma3-ns{margin:1rem}.ma4-ns{margin:2rem}.ma5-ns{margin:4rem}.ma6-ns{margin:8rem}.ma7-ns{margin:16rem}.ml0-ns{margin-left:0}.ml1-ns{margin-left:.25rem}.ml2-ns{margin-left:.5rem}.ml3-ns{margin-left:1rem}.ml4-ns{margin-left:2rem}.ml5-ns{margin-left:4rem}.ml6-ns{margin-left:8rem}.ml7-ns{margin-left:16rem}.mr0-ns{margin-right:0}.mr1-ns{margin-right:.25rem}.mr2-ns{margin-right:.5rem}.mr3-ns{margin-right:1rem}.mr4-ns{margin-right:2rem}.mr5-ns{margin-right:4rem}.mr6-ns{margin-right:8rem}.mr7-ns{margin-right:16rem}.mb0-ns{margin-bottom:0}.mb1-ns{margin-bottom:.25rem}.mb2-ns{margin-bottom:.5rem}.mb3-ns{margin-bottom:1rem}.mb4-ns{margin-bottom:2rem}.mb5-ns{margin-bottom:4rem}.mb6-ns{margin-bottom:8rem}.mb7-ns{margin-bottom:16rem}.mt0-ns{margin-top:0}.mt1-ns{margin-top:.25rem}.mt2-ns{margin-top:.5rem}.mt3-ns{margin-top:1rem}.mt4-ns{margin-top:2rem}.mt5-ns{margin-top:4rem}.mt6-ns{margin-top:8rem}.mt7-ns{margin-top:16rem}.mv0-ns{margin-top:0;margin-bottom:0}.mv1-ns{margin-top:.25rem;margin-bottom:.25rem}.mv2-ns{margin-top:.5rem;margin-bottom:.5rem}.mv3-ns{margin-top:1rem;margin-bottom:1rem}.mv4-ns{margin-top:2rem;margin-bottom:2rem}.mv5-ns{margin-top:4rem;margin-bottom:4rem}.mv6-ns{margin-top:8rem;margin-bottom:8rem}.mv7-ns{margin-top:16rem;margin-bottom:16rem}.mh0-ns{margin-left:0;margin-right:0}.mh1-ns{margin-left:.25rem;margin-right:.25rem}.mh2-ns{margin-left:.5rem;margin-right:.5rem}.mh3-ns{margin-left:1rem;margin-right:1rem}.mh4-ns{margin-left:2rem;margin-right:2rem}.mh5-ns{margin-left:4rem;margin-right:4rem}.mh6-ns{margin-left:8rem;margin-right:8rem}.mh7-ns{margin-left:16rem;margin-right:16rem}.na1-ns{margin:-.25rem}.na2-ns{margin:-.5rem}.na3-ns{margin:-1rem}.na4-ns{margin:-2rem}.na5-ns{margin:-4rem}.na6-ns{margin:-8rem}.na7-ns{margin:-16rem}.nl1-ns{margin-left:-.25rem}.nl2-ns{margin-left:-.5rem}.nl3-ns{margin-left:-1rem}.nl4-ns{margin-left:-2rem}.nl5-ns{margin-left:-4rem}.nl6-ns{margin-left:-8rem}.nl7-ns{margin-left:-16rem}.nr1-ns{margin-right:-.25rem}.nr2-ns{margin-right:-.5rem}.nr3-ns{margin-right:-1rem}.nr4-ns{margin-right:-2rem}.nr5-ns{margin-right:-4rem}.nr6-ns{margin-right:-8rem}.nr7-ns{margin-right:-16rem}.nb1-ns{margin-bottom:-.25rem}.nb2-ns{margin-bottom:-.5rem}.nb3-ns{margin-bottom:-1rem}.nb4-ns{margin-bottom:-2rem}.nb5-ns{margin-bottom:-4rem}.nb6-ns{margin-bottom:-8rem}.nb7-ns{margin-bottom:-16rem}.nt1-ns{margin-top:-.25rem}.nt2-ns{margin-top:-.5rem}.nt3-ns{margin-top:-1rem}.nt4-ns{margin-top:-2rem}.nt5-ns{margin-top:-4rem}.nt6-ns{margin-top:-8rem}.nt7-ns{margin-top:-16rem}.strike-ns{text-decoration:line-through}.underline-ns{text-decoration:underline}.no-underline-ns{text-decoration:none}.tl-ns{text-align:left}.tr-ns{text-align:right}.tc-ns{text-align:center}.tj-ns{text-align:justify}.ttc-ns{text-transform:capitalize}.ttl-ns{text-transform:lowercase}.ttu-ns{text-transform:uppercase}.ttn-ns{text-transform:none}.f-6-ns,.f-headline-ns{font-size:6rem}.f-5-ns,.f-subheadline-ns{font-size:5rem}.f1-ns{font-size:3rem}.f2-ns{font-size:2.25rem}.f3-ns{font-size:1.5rem}.f4-ns{font-size:1.25rem}.f5-ns{font-size:1rem}.f6-ns{font-size:.875rem}.f7-ns{font-size:.75rem}.measure-ns{max-width:30em}.measure-wide-ns{max-width:34em}.measure-narrow-ns{max-width:20em}.indent-ns{text-indent:1em;margin-top:0;margin-bottom:0}.small-caps-ns{font-variant:small-caps}.truncate-ns{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.center-ns{margin-left:auto}.center-ns,.mr-auto-ns{margin-right:auto}.ml-auto-ns{margin-left:auto}.clip-ns{position:fixed!important;position:absolute!important;clip:rect(1px 1px 1px 1px);clip:rect(1px,1px,1px,1px)}.ws-normal-ns{white-space:normal}.nowrap-ns{white-space:nowrap}.pre-ns{white-space:pre}.v-base-ns{vertical-align:baseline}.v-mid-ns{vertical-align:middle}.v-top-ns{vertical-align:top}.v-btm-ns{vertical-align:bottom}}@media screen and (min-width:30em) and (max-width:60em){.aspect-ratio-m{height:0;position:relative}.aspect-ratio--16x9-m{padding-bottom:56.25%}.aspect-ratio--9x16-m{padding-bottom:177.77%}.aspect-ratio--4x3-m{padding-bottom:75%}.aspect-ratio--3x4-m{padding-bottom:133.33%}.aspect-ratio--6x4-m{padding-bottom:66.6%}.aspect-ratio--4x6-m{padding-bottom:150%}.aspect-ratio--8x5-m{padding-bottom:62.5%}.aspect-ratio--5x8-m{padding-bottom:160%}.aspect-ratio--7x5-m{padding-bottom:71.42%}.aspect-ratio--5x7-m{padding-bottom:140%}.aspect-ratio--1x1-m{padding-bottom:100%}.aspect-ratio--object-m{position:absolute;top:0;right:0;bottom:0;left:0;width:100%;height:100%;z-index:100}.cover-m{background-size:cover!important}.contain-m{background-size:contain!important}.bg-center-m{background-position:50%}.bg-center-m,.bg-top-m{background-repeat:no-repeat}.bg-top-m{background-position:top}.bg-right-m{background-position:100%}.bg-bottom-m,.bg-right-m{background-repeat:no-repeat}.bg-bottom-m{background-position:bottom}.bg-left-m{background-repeat:no-repeat;background-position:0}.outline-m{outline:1px solid}.outline-transparent-m{outline:1px solid transparent}.outline-0-m{outline:0}.ba-m{border-style:solid;border-width:1px}.bt-m{border-top-style:solid;border-top-width:1px}.br-m{border-right-style:solid;border-right-width:1px}.bb-m{border-bottom-style:solid;border-bottom-width:1px}.bl-m{border-left-style:solid;border-left-width:1px}.bn-m{border-style:none;border-width:0}.br0-m{border-radius:0}.br1-m{border-radius:.125rem}.br2-m{border-radius:.25rem}.br3-m{border-radius:.5rem}.br4-m{border-radius:1rem}.br-100-m{border-radius:100%}.br-pill-m{border-radius:9999px}.br--bottom-m{border-top-left-radius:0;border-top-right-radius:0}.br--top-m{border-bottom-right-radius:0}.br--right-m,.br--top-m{border-bottom-left-radius:0}.br--right-m{border-top-left-radius:0}.br--left-m{border-top-right-radius:0;border-bottom-right-radius:0}.b--dotted-m{border-style:dotted}.b--dashed-m{border-style:dashed}.b--solid-m{border-style:solid}.b--none-m{border-style:none}.bw0-m{border-width:0}.bw1-m{border-width:.125rem}.bw2-m{border-width:.25rem}.bw3-m{border-width:.5rem}.bw4-m{border-width:1rem}.bw5-m{border-width:2rem}.bt-0-m{border-top-width:0}.br-0-m{border-right-width:0}.bb-0-m{border-bottom-width:0}.bl-0-m{border-left-width:0}.shadow-1-m{box-shadow:0 0 4px 2px rgba(0,0,0,.2)}.shadow-2-m{box-shadow:0 0 8px 2px rgba(0,0,0,.2)}.shadow-3-m{box-shadow:2px 2px 4px 2px rgba(0,0,0,.2)}.shadow-4-m{box-shadow:2px 2px 8px 0 rgba(0,0,0,.2)}.shadow-5-m{box-shadow:4px 4px 8px 0 rgba(0,0,0,.2)}.top-0-m{top:0}.left-0-m{left:0}.right-0-m{right:0}.bottom-0-m{bottom:0}.top-1-m{top:1rem}.left-1-m{left:1rem}.right-1-m{right:1rem}.bottom-1-m{bottom:1rem}.top-2-m{top:2rem}.left-2-m{left:2rem}.right-2-m{right:2rem}.bottom-2-m{bottom:2rem}.top--1-m{top:-1rem}.right--1-m{right:-1rem}.bottom--1-m{bottom:-1rem}.left--1-m{left:-1rem}.top--2-m{top:-2rem}.right--2-m{right:-2rem}.bottom--2-m{bottom:-2rem}.left--2-m{left:-2rem}.absolute--fill-m{top:0;right:0;bottom:0;left:0}.cl-m{clear:left}.cr-m{clear:right}.cb-m{clear:both}.cn-m{clear:none}.dn-m{display:none}.di-m{display:inline}.db-m{display:block}.dib-m{display:inline-block}.dit-m{display:inline-table}.dt-m{display:table}.dtc-m{display:table-cell}.dt-row-m{display:table-row}.dt-row-group-m{display:table-row-group}.dt-column-m{display:table-column}.dt-column-group-m{display:table-column-group}.dt--fixed-m{table-layout:fixed;width:100%}.flex-m{display:-webkit-box;display:-ms-flexbox;display:flex}.inline-flex-m{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex}.flex-auto-m{-webkit-box-flex:1;-ms-flex:1 1 auto;flex:1 1 auto;min-width:0;min-height:0}.flex-none-m{-webkit-box-flex:0;-ms-flex:none;flex:none}.flex-column-m{-webkit-box-orient:vertical;-ms-flex-direction:column;flex-direction:column}.flex-column-m,.flex-row-m{-webkit-box-direction:normal}.flex-row-m{-webkit-box-orient:horizontal;-ms-flex-direction:row;flex-direction:row}.flex-wrap-m{-ms-flex-wrap:wrap;flex-wrap:wrap}.flex-nowrap-m{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.flex-wrap-reverse-m{-ms-flex-wrap:wrap-reverse;flex-wrap:wrap-reverse}.flex-column-reverse-m{-webkit-box-orient:vertical;-webkit-box-direction:reverse;-ms-flex-direction:column-reverse;flex-direction:column-reverse}.flex-row-reverse-m{-webkit-box-orient:horizontal;-webkit-box-direction:reverse;-ms-flex-direction:row-reverse;flex-direction:row-reverse}.items-start-m{-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start}.items-end-m{-webkit-box-align:end;-ms-flex-align:end;align-items:flex-end}.items-center-m{-webkit-box-align:center;-ms-flex-align:center;align-items:center}.items-baseline-m{-webkit-box-align:baseline;-ms-flex-align:baseline;align-items:baseline}.items-stretch-m{-webkit-box-align:stretch;-ms-flex-align:stretch;align-items:stretch}.self-start-m{-ms-flex-item-align:start;align-self:flex-start}.self-end-m{-ms-flex-item-align:end;align-self:flex-end}.self-center-m{-ms-flex-item-align:center;-ms-grid-row-align:center;align-self:center}.self-baseline-m{-ms-flex-item-align:baseline;align-self:baseline}.self-stretch-m{-ms-flex-item-align:stretch;-ms-grid-row-align:stretch;align-self:stretch}.justify-start-m{-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.justify-end-m{-webkit-box-pack:end;-ms-flex-pack:end;justify-content:flex-end}.justify-center-m{-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.justify-between-m{-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.justify-around-m{-ms-flex-pack:distribute;justify-content:space-around}.content-start-m{-ms-flex-line-pack:start;align-content:flex-start}.content-end-m{-ms-flex-line-pack:end;align-content:flex-end}.content-center-m{-ms-flex-line-pack:center;align-content:center}.content-between-m{-ms-flex-line-pack:justify;align-content:space-between}.content-around-m{-ms-flex-line-pack:distribute;align-content:space-around}.content-stretch-m{-ms-flex-line-pack:stretch;align-content:stretch}.order-0-m{-webkit-box-ordinal-group:1;-ms-flex-order:0;order:0}.order-1-m{-webkit-box-ordinal-group:2;-ms-flex-order:1;order:1}.order-2-m{-webkit-box-ordinal-group:3;-ms-flex-order:2;order:2}.order-3-m{-webkit-box-ordinal-group:4;-ms-flex-order:3;order:3}.order-4-m{-webkit-box-ordinal-group:5;-ms-flex-order:4;order:4}.order-5-m{-webkit-box-ordinal-group:6;-ms-flex-order:5;order:5}.order-6-m{-webkit-box-ordinal-group:7;-ms-flex-order:6;order:6}.order-7-m{-webkit-box-ordinal-group:8;-ms-flex-order:7;order:7}.order-8-m{-webkit-box-ordinal-group:9;-ms-flex-order:8;order:8}.order-last-m{-webkit-box-ordinal-group:100000;-ms-flex-order:99999;order:99999}.flex-grow-0-m{-webkit-box-flex:0;-ms-flex-positive:0;flex-grow:0}.flex-grow-1-m{-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1}.flex-shrink-0-m{-ms-flex-negative:0;flex-shrink:0}.flex-shrink-1-m{-ms-flex-negative:1;flex-shrink:1}.fl-m{float:left}.fl-m,.fr-m{display:inline}.fr-m{float:right}.fn-m{float:none}.i-m{font-style:italic}.fs-normal-m{font-style:normal}.normal-m{font-weight:400}.b-m{font-weight:700}.fw1-m{font-weight:100}.fw2-m{font-weight:200}.fw3-m{font-weight:300}.fw4-m{font-weight:400}.fw5-m{font-weight:500}.fw6-m{font-weight:600}.fw7-m{font-weight:700}.fw8-m{font-weight:800}.fw9-m{font-weight:900}.h1-m{height:1rem}.h2-m{height:2rem}.h3-m{height:4rem}.h4-m{height:8rem}.h5-m{height:16rem}.h-25-m{height:25%}.h-50-m{height:50%}.h-75-m{height:75%}.h-100-m{height:100%}.min-h-100-m{min-height:100%}.vh-25-m{height:25vh}.vh-50-m{height:50vh}.vh-75-m{height:75vh}.vh-100-m{height:100vh}.min-vh-100-m{min-height:100vh}.h-auto-m{height:auto}.h-inherit-m{height:inherit}.tracked-m{letter-spacing:.1em}.tracked-tight-m{letter-spacing:-.05em}.tracked-mega-m{letter-spacing:.25em}.lh-solid-m{line-height:1}.lh-title-m{line-height:1.25}.lh-copy-m{line-height:1.5}.mw-100-m{max-width:100%}.mw1-m{max-width:1rem}.mw2-m{max-width:2rem}.mw3-m{max-width:4rem}.mw4-m{max-width:8rem}.mw5-m{max-width:16rem}.mw6-m{max-width:32rem}.mw7-m{max-width:48rem}.mw8-m{max-width:64rem}.mw9-m{max-width:96rem}.mw-none-m{max-width:none}.w1-m{width:1rem}.w2-m{width:2rem}.w3-m{width:4rem}.w4-m{width:8rem}.w5-m{width:16rem}.w-10-m{width:10%}.w-20-m{width:20%}.w-25-m{width:25%}.w-30-m{width:30%}.w-33-m{width:33%}.w-34-m{width:34%}.w-40-m{width:40%}.w-50-m{width:50%}.w-60-m{width:60%}.w-70-m{width:70%}.w-75-m{width:75%}.w-80-m{width:80%}.w-90-m{width:90%}.w-100-m{width:100%}.w-third-m{width:33.33333%}.w-two-thirds-m{width:66.66667%}.w-auto-m{width:auto}.overflow-visible-m{overflow:visible}.overflow-hidden-m{overflow:hidden}.overflow-scroll-m{overflow:scroll}.overflow-auto-m{overflow:auto}.overflow-x-visible-m{overflow-x:visible}.overflow-x-hidden-m{overflow-x:hidden}.overflow-x-scroll-m{overflow-x:scroll}.overflow-x-auto-m{overflow-x:auto}.overflow-y-visible-m{overflow-y:visible}.overflow-y-hidden-m{overflow-y:hidden}.overflow-y-scroll-m{overflow-y:scroll}.overflow-y-auto-m{overflow-y:auto}.static-m{position:static}.relative-m{position:relative}.absolute-m{position:absolute}.fixed-m{position:fixed}.rotate-45-m{-webkit-transform:rotate(45deg);transform:rotate(45deg)}.rotate-90-m{-webkit-transform:rotate(90deg);transform:rotate(90deg)}.rotate-135-m{-webkit-transform:rotate(135deg);transform:rotate(135deg)}.rotate-180-m{-webkit-transform:rotate(180deg);transform:rotate(180deg)}.rotate-225-m{-webkit-transform:rotate(225deg);transform:rotate(225deg)}.rotate-270-m{-webkit-transform:rotate(270deg);transform:rotate(270deg)}.rotate-315-m{-webkit-transform:rotate(315deg);transform:rotate(315deg)}.pa0-m{padding:0}.pa1-m{padding:.25rem}.pa2-m{padding:.5rem}.pa3-m{padding:1rem}.pa4-m{padding:2rem}.pa5-m{padding:4rem}.pa6-m{padding:8rem}.pa7-m{padding:16rem}.pl0-m{padding-left:0}.pl1-m{padding-left:.25rem}.pl2-m{padding-left:.5rem}.pl3-m{padding-left:1rem}.pl4-m{padding-left:2rem}.pl5-m{padding-left:4rem}.pl6-m{padding-left:8rem}.pl7-m{padding-left:16rem}.pr0-m{padding-right:0}.pr1-m{padding-right:.25rem}.pr2-m{padding-right:.5rem}.pr3-m{padding-right:1rem}.pr4-m{padding-right:2rem}.pr5-m{padding-right:4rem}.pr6-m{padding-right:8rem}.pr7-m{padding-right:16rem}.pb0-m{padding-bottom:0}.pb1-m{padding-bottom:.25rem}.pb2-m{padding-bottom:.5rem}.pb3-m{padding-bottom:1rem}.pb4-m{padding-bottom:2rem}.pb5-m{padding-bottom:4rem}.pb6-m{padding-bottom:8rem}.pb7-m{padding-bottom:16rem}.pt0-m{padding-top:0}.pt1-m{padding-top:.25rem}.pt2-m{padding-top:.5rem}.pt3-m{padding-top:1rem}.pt4-m{padding-top:2rem}.pt5-m{padding-top:4rem}.pt6-m{padding-top:8rem}.pt7-m{padding-top:16rem}.pv0-m{padding-top:0;padding-bottom:0}.pv1-m{padding-top:.25rem;padding-bottom:.25rem}.pv2-m{padding-top:.5rem;padding-bottom:.5rem}.pv3-m{padding-top:1rem;padding-bottom:1rem}.pv4-m{padding-top:2rem;padding-bottom:2rem}.pv5-m{padding-top:4rem;padding-bottom:4rem}.pv6-m{padding-top:8rem;padding-bottom:8rem}.pv7-m{padding-top:16rem;padding-bottom:16rem}.ph0-m{padding-left:0;padding-right:0}.ph1-m{padding-left:.25rem;padding-right:.25rem}.ph2-m{padding-left:.5rem;padding-right:.5rem}.ph3-m{padding-left:1rem;padding-right:1rem}.ph4-m{padding-left:2rem;padding-right:2rem}.ph5-m{padding-left:4rem;padding-right:4rem}.ph6-m{padding-left:8rem;padding-right:8rem}.ph7-m{padding-left:16rem;padding-right:16rem}.ma0-m{margin:0}.ma1-m{margin:.25rem}.ma2-m{margin:.5rem}.ma3-m{margin:1rem}.ma4-m{margin:2rem}.ma5-m{margin:4rem}.ma6-m{margin:8rem}.ma7-m{margin:16rem}.ml0-m{margin-left:0}.ml1-m{margin-left:.25rem}.ml2-m{margin-left:.5rem}.ml3-m{margin-left:1rem}.ml4-m{margin-left:2rem}.ml5-m{margin-left:4rem}.ml6-m{margin-left:8rem}.ml7-m{margin-left:16rem}.mr0-m{margin-right:0}.mr1-m{margin-right:.25rem}.mr2-m{margin-right:.5rem}.mr3-m{margin-right:1rem}.mr4-m{margin-right:2rem}.mr5-m{margin-right:4rem}.mr6-m{margin-right:8rem}.mr7-m{margin-right:16rem}.mb0-m{margin-bottom:0}.mb1-m{margin-bottom:.25rem}.mb2-m{margin-bottom:.5rem}.mb3-m{margin-bottom:1rem}.mb4-m{margin-bottom:2rem}.mb5-m{margin-bottom:4rem}.mb6-m{margin-bottom:8rem}.mb7-m{margin-bottom:16rem}.mt0-m{margin-top:0}.mt1-m{margin-top:.25rem}.mt2-m{margin-top:.5rem}.mt3-m{margin-top:1rem}.mt4-m{margin-top:2rem}.mt5-m{margin-top:4rem}.mt6-m{margin-top:8rem}.mt7-m{margin-top:16rem}.mv0-m{margin-top:0;margin-bottom:0}.mv1-m{margin-top:.25rem;margin-bottom:.25rem}.mv2-m{margin-top:.5rem;margin-bottom:.5rem}.mv3-m{margin-top:1rem;margin-bottom:1rem}.mv4-m{margin-top:2rem;margin-bottom:2rem}.mv5-m{margin-top:4rem;margin-bottom:4rem}.mv6-m{margin-top:8rem;margin-bottom:8rem}.mv7-m{margin-top:16rem;margin-bottom:16rem}.mh0-m{margin-left:0;margin-right:0}.mh1-m{margin-left:.25rem;margin-right:.25rem}.mh2-m{margin-left:.5rem;margin-right:.5rem}.mh3-m{margin-left:1rem;margin-right:1rem}.mh4-m{margin-left:2rem;margin-right:2rem}.mh5-m{margin-left:4rem;margin-right:4rem}.mh6-m{margin-left:8rem;margin-right:8rem}.mh7-m{margin-left:16rem;margin-right:16rem}.na1-m{margin:-.25rem}.na2-m{margin:-.5rem}.na3-m{margin:-1rem}.na4-m{margin:-2rem}.na5-m{margin:-4rem}.na6-m{margin:-8rem}.na7-m{margin:-16rem}.nl1-m{margin-left:-.25rem}.nl2-m{margin-left:-.5rem}.nl3-m{margin-left:-1rem}.nl4-m{margin-left:-2rem}.nl5-m{margin-left:-4rem}.nl6-m{margin-left:-8rem}.nl7-m{margin-left:-16rem}.nr1-m{margin-right:-.25rem}.nr2-m{margin-right:-.5rem}.nr3-m{margin-right:-1rem}.nr4-m{margin-right:-2rem}.nr5-m{margin-right:-4rem}.nr6-m{margin-right:-8rem}.nr7-m{margin-right:-16rem}.nb1-m{margin-bottom:-.25rem}.nb2-m{margin-bottom:-.5rem}.nb3-m{margin-bottom:-1rem}.nb4-m{margin-bottom:-2rem}.nb5-m{margin-bottom:-4rem}.nb6-m{margin-bottom:-8rem}.nb7-m{margin-bottom:-16rem}.nt1-m{margin-top:-.25rem}.nt2-m{margin-top:-.5rem}.nt3-m{margin-top:-1rem}.nt4-m{margin-top:-2rem}.nt5-m{margin-top:-4rem}.nt6-m{margin-top:-8rem}.nt7-m{margin-top:-16rem}.strike-m{text-decoration:line-through}.underline-m{text-decoration:underline}.no-underline-m{text-decoration:none}.tl-m{text-align:left}.tr-m{text-align:right}.tc-m{text-align:center}.tj-m{text-align:justify}.ttc-m{text-transform:capitalize}.ttl-m{text-transform:lowercase}.ttu-m{text-transform:uppercase}.ttn-m{text-transform:none}.f-6-m,.f-headline-m{font-size:6rem}.f-5-m,.f-subheadline-m{font-size:5rem}.f1-m{font-size:3rem}.f2-m{font-size:2.25rem}.f3-m{font-size:1.5rem}.f4-m{font-size:1.25rem}.f5-m{font-size:1rem}.f6-m{font-size:.875rem}.f7-m{font-size:.75rem}.measure-m{max-width:30em}.measure-wide-m{max-width:34em}.measure-narrow-m{max-width:20em}.indent-m{text-indent:1em;margin-top:0;margin-bottom:0}.small-caps-m{font-variant:small-caps}.truncate-m{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.center-m{margin-left:auto}.center-m,.mr-auto-m{margin-right:auto}.ml-auto-m{margin-left:auto}.clip-m{position:fixed!important;position:absolute!important;clip:rect(1px 1px 1px 1px);clip:rect(1px,1px,1px,1px)}.ws-normal-m{white-space:normal}.nowrap-m{white-space:nowrap}.pre-m{white-space:pre}.v-base-m{vertical-align:baseline}.v-mid-m{vertical-align:middle}.v-top-m{vertical-align:top}.v-btm-m{vertical-align:bottom}}@media screen and (min-width:60em){.aspect-ratio-l{height:0;position:relative}.aspect-ratio--16x9-l{padding-bottom:56.25%}.aspect-ratio--9x16-l{padding-bottom:177.77%}.aspect-ratio--4x3-l{padding-bottom:75%}.aspect-ratio--3x4-l{padding-bottom:133.33%}.aspect-ratio--6x4-l{padding-bottom:66.6%}.aspect-ratio--4x6-l{padding-bottom:150%}.aspect-ratio--8x5-l{padding-bottom:62.5%}.aspect-ratio--5x8-l{padding-bottom:160%}.aspect-ratio--7x5-l{padding-bottom:71.42%}.aspect-ratio--5x7-l{padding-bottom:140%}.aspect-ratio--1x1-l{padding-bottom:100%}.aspect-ratio--object-l{position:absolute;top:0;right:0;bottom:0;left:0;width:100%;height:100%;z-index:100}.cover-l{background-size:cover!important}.contain-l{background-size:contain!important}.bg-center-l{background-position:50%}.bg-center-l,.bg-top-l{background-repeat:no-repeat}.bg-top-l{background-position:top}.bg-right-l{background-position:100%}.bg-bottom-l,.bg-right-l{background-repeat:no-repeat}.bg-bottom-l{background-position:bottom}.bg-left-l{background-repeat:no-repeat;background-position:0}.outline-l{outline:1px solid}.outline-transparent-l{outline:1px solid transparent}.outline-0-l{outline:0}.ba-l{border-style:solid;border-width:1px}.bt-l{border-top-style:solid;border-top-width:1px}.br-l{border-right-style:solid;border-right-width:1px}.bb-l{border-bottom-style:solid;border-bottom-width:1px}.bl-l{border-left-style:solid;border-left-width:1px}.bn-l{border-style:none;border-width:0}.br0-l{border-radius:0}.br1-l{border-radius:.125rem}.br2-l{border-radius:.25rem}.br3-l{border-radius:.5rem}.br4-l{border-radius:1rem}.br-100-l{border-radius:100%}.br-pill-l{border-radius:9999px}.br--bottom-l{border-top-left-radius:0;border-top-right-radius:0}.br--top-l{border-bottom-right-radius:0}.br--right-l,.br--top-l{border-bottom-left-radius:0}.br--right-l{border-top-left-radius:0}.br--left-l{border-top-right-radius:0;border-bottom-right-radius:0}.b--dotted-l{border-style:dotted}.b--dashed-l{border-style:dashed}.b--solid-l{border-style:solid}.b--none-l{border-style:none}.bw0-l{border-width:0}.bw1-l{border-width:.125rem}.bw2-l{border-width:.25rem}.bw3-l{border-width:.5rem}.bw4-l{border-width:1rem}.bw5-l{border-width:2rem}.bt-0-l{border-top-width:0}.br-0-l{border-right-width:0}.bb-0-l{border-bottom-width:0}.bl-0-l{border-left-width:0}.shadow-1-l{box-shadow:0 0 4px 2px rgba(0,0,0,.2)}.shadow-2-l{box-shadow:0 0 8px 2px rgba(0,0,0,.2)}.shadow-3-l{box-shadow:2px 2px 4px 2px rgba(0,0,0,.2)}.shadow-4-l{box-shadow:2px 2px 8px 0 rgba(0,0,0,.2)}.shadow-5-l{box-shadow:4px 4px 8px 0 rgba(0,0,0,.2)}.top-0-l{top:0}.left-0-l{left:0}.right-0-l{right:0}.bottom-0-l{bottom:0}.top-1-l{top:1rem}.left-1-l{left:1rem}.right-1-l{right:1rem}.bottom-1-l{bottom:1rem}.top-2-l{top:2rem}.left-2-l{left:2rem}.right-2-l{right:2rem}.bottom-2-l{bottom:2rem}.top--1-l{top:-1rem}.right--1-l{right:-1rem}.bottom--1-l{bottom:-1rem}.left--1-l{left:-1rem}.top--2-l{top:-2rem}.right--2-l{right:-2rem}.bottom--2-l{bottom:-2rem}.left--2-l{left:-2rem}.absolute--fill-l{top:0;right:0;bottom:0;left:0}.cl-l{clear:left}.cr-l{clear:right}.cb-l{clear:both}.cn-l{clear:none}.dn-l{display:none}.di-l{display:inline}.db-l{display:block}.dib-l{display:inline-block}.dit-l{display:inline-table}.dt-l{display:table}.dtc-l{display:table-cell}.dt-row-l{display:table-row}.dt-row-group-l{display:table-row-group}.dt-column-l{display:table-column}.dt-column-group-l{display:table-column-group}.dt--fixed-l{table-layout:fixed;width:100%}.flex-l{display:-webkit-box;display:-ms-flexbox;display:flex}.inline-flex-l{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex}.flex-auto-l{-webkit-box-flex:1;-ms-flex:1 1 auto;flex:1 1 auto;min-width:0;min-height:0}.flex-none-l{-webkit-box-flex:0;-ms-flex:none;flex:none}.flex-column-l{-webkit-box-orient:vertical;-ms-flex-direction:column;flex-direction:column}.flex-column-l,.flex-row-l{-webkit-box-direction:normal}.flex-row-l{-webkit-box-orient:horizontal;-ms-flex-direction:row;flex-direction:row}.flex-wrap-l{-ms-flex-wrap:wrap;flex-wrap:wrap}.flex-nowrap-l{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.flex-wrap-reverse-l{-ms-flex-wrap:wrap-reverse;flex-wrap:wrap-reverse}.flex-column-reverse-l{-webkit-box-orient:vertical;-webkit-box-direction:reverse;-ms-flex-direction:column-reverse;flex-direction:column-reverse}.flex-row-reverse-l{-webkit-box-orient:horizontal;-webkit-box-direction:reverse;-ms-flex-direction:row-reverse;flex-direction:row-reverse}.items-start-l{-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start}.items-end-l{-webkit-box-align:end;-ms-flex-align:end;align-items:flex-end}.items-center-l{-webkit-box-align:center;-ms-flex-align:center;align-items:center}.items-baseline-l{-webkit-box-align:baseline;-ms-flex-align:baseline;align-items:baseline}.items-stretch-l{-webkit-box-align:stretch;-ms-flex-align:stretch;align-items:stretch}.self-start-l{-ms-flex-item-align:start;align-self:flex-start}.self-end-l{-ms-flex-item-align:end;align-self:flex-end}.self-center-l{-ms-flex-item-align:center;-ms-grid-row-align:center;align-self:center}.self-baseline-l{-ms-flex-item-align:baseline;align-self:baseline}.self-stretch-l{-ms-flex-item-align:stretch;-ms-grid-row-align:stretch;align-self:stretch}.justify-start-l{-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.justify-end-l{-webkit-box-pack:end;-ms-flex-pack:end;justify-content:flex-end}.justify-center-l{-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.justify-between-l{-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.justify-around-l{-ms-flex-pack:distribute;justify-content:space-around}.content-start-l{-ms-flex-line-pack:start;align-content:flex-start}.content-end-l{-ms-flex-line-pack:end;align-content:flex-end}.content-center-l{-ms-flex-line-pack:center;align-content:center}.content-between-l{-ms-flex-line-pack:justify;align-content:space-between}.content-around-l{-ms-flex-line-pack:distribute;align-content:space-around}.content-stretch-l{-ms-flex-line-pack:stretch;align-content:stretch}.order-0-l{-webkit-box-ordinal-group:1;-ms-flex-order:0;order:0}.order-1-l{-webkit-box-ordinal-group:2;-ms-flex-order:1;order:1}.order-2-l{-webkit-box-ordinal-group:3;-ms-flex-order:2;order:2}.order-3-l{-webkit-box-ordinal-group:4;-ms-flex-order:3;order:3}.order-4-l{-webkit-box-ordinal-group:5;-ms-flex-order:4;order:4}.order-5-l{-webkit-box-ordinal-group:6;-ms-flex-order:5;order:5}.order-6-l{-webkit-box-ordinal-group:7;-ms-flex-order:6;order:6}.order-7-l{-webkit-box-ordinal-group:8;-ms-flex-order:7;order:7}.order-8-l{-webkit-box-ordinal-group:9;-ms-flex-order:8;order:8}.order-last-l{-webkit-box-ordinal-group:100000;-ms-flex-order:99999;order:99999}.flex-grow-0-l{-webkit-box-flex:0;-ms-flex-positive:0;flex-grow:0}.flex-grow-1-l{-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1}.flex-shrink-0-l{-ms-flex-negative:0;flex-shrink:0}.flex-shrink-1-l{-ms-flex-negative:1;flex-shrink:1}.fl-l{float:left}.fl-l,.fr-l{display:inline}.fr-l{float:right}.fn-l{float:none}.i-l{font-style:italic}.fs-normal-l{font-style:normal}.normal-l{font-weight:400}.b-l{font-weight:700}.fw1-l{font-weight:100}.fw2-l{font-weight:200}.fw3-l{font-weight:300}.fw4-l{font-weight:400}.fw5-l{font-weight:500}.fw6-l{font-weight:600}.fw7-l{font-weight:700}.fw8-l{font-weight:800}.fw9-l{font-weight:900}.h1-l{height:1rem}.h2-l{height:2rem}.h3-l{height:4rem}.h4-l{height:8rem}.h5-l{height:16rem}.h-25-l{height:25%}.h-50-l{height:50%}.h-75-l{height:75%}.h-100-l{height:100%}.min-h-100-l{min-height:100%}.vh-25-l{height:25vh}.vh-50-l{height:50vh}.vh-75-l{height:75vh}.vh-100-l{height:100vh}.min-vh-100-l{min-height:100vh}.h-auto-l{height:auto}.h-inherit-l{height:inherit}.tracked-l{letter-spacing:.1em}.tracked-tight-l{letter-spacing:-.05em}.tracked-mega-l{letter-spacing:.25em}.lh-solid-l{line-height:1}.lh-title-l{line-height:1.25}.lh-copy-l{line-height:1.5}.mw-100-l{max-width:100%}.mw1-l{max-width:1rem}.mw2-l{max-width:2rem}.mw3-l{max-width:4rem}.mw4-l{max-width:8rem}.mw5-l{max-width:16rem}.mw6-l{max-width:32rem}.mw7-l{max-width:48rem}.mw8-l{max-width:64rem}.mw9-l{max-width:96rem}.mw-none-l{max-width:none}.w1-l{width:1rem}.w2-l{width:2rem}.w3-l{width:4rem}.w4-l{width:8rem}.w5-l{width:16rem}.w-10-l{width:10%}.w-20-l{width:20%}.w-25-l{width:25%}.w-30-l{width:30%}.w-33-l{width:33%}.w-34-l{width:34%}.w-40-l{width:40%}.w-50-l{width:50%}.w-60-l{width:60%}.w-70-l{width:70%}.w-75-l{width:75%}.w-80-l{width:80%}.w-90-l{width:90%}.w-100-l{width:100%}.w-third-l{width:33.33333%}.w-two-thirds-l{width:66.66667%}.w-auto-l{width:auto}.overflow-visible-l{overflow:visible}.overflow-hidden-l{overflow:hidden}.overflow-scroll-l{overflow:scroll}.overflow-auto-l{overflow:auto}.overflow-x-visible-l{overflow-x:visible}.overflow-x-hidden-l{overflow-x:hidden}.overflow-x-scroll-l{overflow-x:scroll}.overflow-x-auto-l{overflow-x:auto}.overflow-y-visible-l{overflow-y:visible}.overflow-y-hidden-l{overflow-y:hidden}.overflow-y-scroll-l{overflow-y:scroll}.overflow-y-auto-l{overflow-y:auto}.static-l{position:static}.relative-l{position:relative}.absolute-l{position:absolute}.fixed-l{position:fixed}.rotate-45-l{-webkit-transform:rotate(45deg);transform:rotate(45deg)}.rotate-90-l{-webkit-transform:rotate(90deg);transform:rotate(90deg)}.rotate-135-l{-webkit-transform:rotate(135deg);transform:rotate(135deg)}.rotate-180-l{-webkit-transform:rotate(180deg);transform:rotate(180deg)}.rotate-225-l{-webkit-transform:rotate(225deg);transform:rotate(225deg)}.rotate-270-l{-webkit-transform:rotate(270deg);transform:rotate(270deg)}.rotate-315-l{-webkit-transform:rotate(315deg);transform:rotate(315deg)}.pa0-l{padding:0}.pa1-l{padding:.25rem}.pa2-l{padding:.5rem}.pa3-l{padding:1rem}.pa4-l{padding:2rem}.pa5-l{padding:4rem}.pa6-l{padding:8rem}.pa7-l{padding:16rem}.pl0-l{padding-left:0}.pl1-l{padding-left:.25rem}.pl2-l{padding-left:.5rem}.pl3-l{padding-left:1rem}.pl4-l{padding-left:2rem}.pl5-l{padding-left:4rem}.pl6-l{padding-left:8rem}.pl7-l{padding-left:16rem}.pr0-l{padding-right:0}.pr1-l{padding-right:.25rem}.pr2-l{padding-right:.5rem}.pr3-l{padding-right:1rem}.pr4-l{padding-right:2rem}.pr5-l{padding-right:4rem}.pr6-l{padding-right:8rem}.pr7-l{padding-right:16rem}.pb0-l{padding-bottom:0}.pb1-l{padding-bottom:.25rem}.pb2-l{padding-bottom:.5rem}.pb3-l{padding-bottom:1rem}.pb4-l{padding-bottom:2rem}.pb5-l{padding-bottom:4rem}.pb6-l{padding-bottom:8rem}.pb7-l{padding-bottom:16rem}.pt0-l{padding-top:0}.pt1-l{padding-top:.25rem}.pt2-l{padding-top:.5rem}.pt3-l{padding-top:1rem}.pt4-l{padding-top:2rem}.pt5-l{padding-top:4rem}.pt6-l{padding-top:8rem}.pt7-l{padding-top:16rem}.pv0-l{padding-top:0;padding-bottom:0}.pv1-l{padding-top:.25rem;padding-bottom:.25rem}.pv2-l{padding-top:.5rem;padding-bottom:.5rem}.pv3-l{padding-top:1rem;padding-bottom:1rem}.pv4-l{padding-top:2rem;padding-bottom:2rem}.pv5-l{padding-top:4rem;padding-bottom:4rem}.pv6-l{padding-top:8rem;padding-bottom:8rem}.pv7-l{padding-top:16rem;padding-bottom:16rem}.ph0-l{padding-left:0;padding-right:0}.ph1-l{padding-left:.25rem;padding-right:.25rem}.ph2-l{padding-left:.5rem;padding-right:.5rem}.ph3-l{padding-left:1rem;padding-right:1rem}.ph4-l{padding-left:2rem;padding-right:2rem}.ph5-l{padding-left:4rem;padding-right:4rem}.ph6-l{padding-left:8rem;padding-right:8rem}.ph7-l{padding-left:16rem;padding-right:16rem}.ma0-l{margin:0}.ma1-l{margin:.25rem}.ma2-l{margin:.5rem}.ma3-l{margin:1rem}.ma4-l{margin:2rem}.ma5-l{margin:4rem}.ma6-l{margin:8rem}.ma7-l{margin:16rem}.ml0-l{margin-left:0}.ml1-l{margin-left:.25rem}.ml2-l{margin-left:.5rem}.ml3-l{margin-left:1rem}.ml4-l{margin-left:2rem}.ml5-l{margin-left:4rem}.ml6-l{margin-left:8rem}.ml7-l{margin-left:16rem}.mr0-l{margin-right:0}.mr1-l{margin-right:.25rem}.mr2-l{margin-right:.5rem}.mr3-l{margin-right:1rem}.mr4-l{margin-right:2rem}.mr5-l{margin-right:4rem}.mr6-l{margin-right:8rem}.mr7-l{margin-right:16rem}.mb0-l{margin-bottom:0}.mb1-l{margin-bottom:.25rem}.mb2-l{margin-bottom:.5rem}.mb3-l{margin-bottom:1rem}.mb4-l{margin-bottom:2rem}.mb5-l{margin-bottom:4rem}.mb6-l{margin-bottom:8rem}.mb7-l{margin-bottom:16rem}.mt0-l{margin-top:0}.mt1-l{margin-top:.25rem}.mt2-l{margin-top:.5rem}.mt3-l{margin-top:1rem}.mt4-l{margin-top:2rem}.mt5-l{margin-top:4rem}.mt6-l{margin-top:8rem}.mt7-l{margin-top:16rem}.mv0-l{margin-top:0;margin-bottom:0}.mv1-l{margin-top:.25rem;margin-bottom:.25rem}.mv2-l{margin-top:.5rem;margin-bottom:.5rem}.mv3-l{margin-top:1rem;margin-bottom:1rem}.mv4-l{margin-top:2rem;margin-bottom:2rem}.mv5-l{margin-top:4rem;margin-bottom:4rem}.mv6-l{margin-top:8rem;margin-bottom:8rem}.mv7-l{margin-top:16rem;margin-bottom:16rem}.mh0-l{margin-left:0;margin-right:0}.mh1-l{margin-left:.25rem;margin-right:.25rem}.mh2-l{margin-left:.5rem;margin-right:.5rem}.mh3-l{margin-left:1rem;margin-right:1rem}.mh4-l{margin-left:2rem;margin-right:2rem}.mh5-l{margin-left:4rem;margin-right:4rem}.mh6-l{margin-left:8rem;margin-right:8rem}.mh7-l{margin-left:16rem;margin-right:16rem}.na1-l{margin:-.25rem}.na2-l{margin:-.5rem}.na3-l{margin:-1rem}.na4-l{margin:-2rem}.na5-l{margin:-4rem}.na6-l{margin:-8rem}.na7-l{margin:-16rem}.nl1-l{margin-left:-.25rem}.nl2-l{margin-left:-.5rem}.nl3-l{margin-left:-1rem}.nl4-l{margin-left:-2rem}.nl5-l{margin-left:-4rem}.nl6-l{margin-left:-8rem}.nl7-l{margin-left:-16rem}.nr1-l{margin-right:-.25rem}.nr2-l{margin-right:-.5rem}.nr3-l{margin-right:-1rem}.nr4-l{margin-right:-2rem}.nr5-l{margin-right:-4rem}.nr6-l{margin-right:-8rem}.nr7-l{margin-right:-16rem}.nb1-l{margin-bottom:-.25rem}.nb2-l{margin-bottom:-.5rem}.nb3-l{margin-bottom:-1rem}.nb4-l{margin-bottom:-2rem}.nb5-l{margin-bottom:-4rem}.nb6-l{margin-bottom:-8rem}.nb7-l{margin-bottom:-16rem}.nt1-l{margin-top:-.25rem}.nt2-l{margin-top:-.5rem}.nt3-l{margin-top:-1rem}.nt4-l{margin-top:-2rem}.nt5-l{margin-top:-4rem}.nt6-l{margin-top:-8rem}.nt7-l{margin-top:-16rem}.strike-l{text-decoration:line-through}.underline-l{text-decoration:underline}.no-underline-l{text-decoration:none}.tl-l{text-align:left}.tr-l{text-align:right}.tc-l{text-align:center}.tj-l{text-align:justify}.ttc-l{text-transform:capitalize}.ttl-l{text-transform:lowercase}.ttu-l{text-transform:uppercase}.ttn-l{text-transform:none}.f-6-l,.f-headline-l{font-size:6rem}.f-5-l,.f-subheadline-l{font-size:5rem}.f1-l{font-size:3rem}.f2-l{font-size:2.25rem}.f3-l{font-size:1.5rem}.f4-l{font-size:1.25rem}.f5-l{font-size:1rem}.f6-l{font-size:.875rem}.f7-l{font-size:.75rem}.measure-l{max-width:30em}.measure-wide-l{max-width:34em}.measure-narrow-l{max-width:20em}.indent-l{text-indent:1em;margin-top:0;margin-bottom:0}.small-caps-l{font-variant:small-caps}.truncate-l{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.center-l{margin-left:auto}.center-l,.mr-auto-l{margin-right:auto}.ml-auto-l{margin-left:auto}.clip-l{position:fixed!important;position:absolute!important;clip:rect(1px 1px 1px 1px);clip:rect(1px,1px,1px,1px)}.ws-normal-l{white-space:normal}.nowrap-l{white-space:nowrap}.pre-l{white-space:pre}.v-base-l{vertical-align:baseline}.v-mid-l{vertical-align:middle}.v-top-l{vertical-align:top}.v-btm-l{vertical-align:bottom}} 3 | 4 | -------------------------------------------------------------------------------- /src/main/resources/templates/home.html: -------------------------------------------------------------------------------- 1 | 4 |
5 |
6 |

Store

7 |
8 |

You can buy items.

9 |
10 |
11 |
12 | 13 | 14 |
15 |
16 | 17 | 18 |
19 |
20 | 21 | 22 |
23 |
24 |
25 |
26 |

Admin

27 |
28 |

Store backend system.

29 |
30 |
    31 |
  • Connected:
  • 32 |
  • Consuming from queue:
  • 33 |
  • Worker name:
  • 34 |
35 |
36 |
37 |
38 |

Pending jobs

39 |

(from queue)

40 |
41 |
42 |
43 |

Completed jobs

44 |

 

45 |
46 |
47 |
48 |
49 | -------------------------------------------------------------------------------- /src/main/resources/templates/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | Store 6 | 7 | 8 | 9 |
10 | 11 | -------------------------------------------------------------------------------- /src/main/resources/templates/success.html: -------------------------------------------------------------------------------- 1 | 4 |
5 |

Congratulations!

6 |
7 |

You bought items.

8 |
9 | Buy more items 10 |
11 | -------------------------------------------------------------------------------- /src/test/java/com/learnk8s/app/SpringBootApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.learnk8s.app; 2 | 3 | import com.learnk8s.app.queue.QueueService; 4 | import org.apache.activemq.command.ActiveMQTextMessage; 5 | import org.apache.activemq.junit.EmbeddedActiveMQBroker; 6 | import org.junit.Before; 7 | import org.junit.ClassRule; 8 | import org.junit.Rule; 9 | import org.junit.Test; 10 | import org.junit.runner.RunWith; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.boot.test.context.SpringBootTest; 13 | import org.springframework.test.context.junit4.SpringRunner; 14 | 15 | import static org.assertj.core.api.Java6Assertions.assertThat; 16 | 17 | @RunWith(SpringRunner.class) 18 | @SpringBootTest 19 | public class SpringBootApplicationTests { 20 | 21 | private static final String QUEUE_NAME = "testQueue"; 22 | 23 | @Rule 24 | public EmbeddedActiveMQBroker broker = new EmbeddedActiveMQBroker() { 25 | @Override 26 | protected void configure() { 27 | try { 28 | this.getBrokerService().addConnector("tcp://localhost:61616"); 29 | 30 | } catch (Exception e) { 31 | // noop test should fail 32 | } 33 | } 34 | }; 35 | 36 | @Autowired 37 | private QueueService queueService; 38 | 39 | @Before 40 | public void setup() { 41 | } 42 | 43 | @Test 44 | public void testSend() throws Exception { 45 | queueService.send(QUEUE_NAME, "test"); 46 | assertThat(queueService.pendingJobs(QUEUE_NAME)).isEqualTo(1); 47 | } 48 | 49 | @Test 50 | public void testReceive() throws Exception { 51 | var message = new ActiveMQTextMessage(); 52 | message.setText("test"); 53 | queueService.onMessage(message); 54 | assertThat(queueService.completedJobs()).isEqualTo(1); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/test/java/com/learnk8s/app/service/QueueServiceServiceImplTest.java: -------------------------------------------------------------------------------- 1 | package com.learnk8s.app.service; 2 | 3 | import com.learnk8s.app.queue.QueueService; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | 6 | import com.learnk8s.app.SpringBootApplicationTests; 7 | 8 | public class QueueServiceServiceImplTest extends SpringBootApplicationTests { 9 | 10 | @Autowired 11 | private QueueService queueService; 12 | } 13 | --------------------------------------------------------------------------------