Repository: coreos/dex
Pull request #711 (2016-11-30 22:35:28)

*: add theme based frontend configuration
This PR reworks the web layout so static files can be provided and
a "themes" directory to allow a certain degree of control over logos,
styles, etc.

This PR does NOT add general support for frontend customization,
only enough to allow us to start exploring theming internally.
The dex binary also must now be run from the root directory since
templates are no longer "compiled into" the binary.

The docker image has been updated with frontend assets.

ericchiang: cc @rithujohn191 @amrutac 

Tested the docker image with:

```
make docker-image
sudo docker run -p 5556:5556 -d \
    -v $PWD/examples/config-dev.yaml:/examples/config-dev.yaml |
    quay.io/coreos/dex:$( ./scripts/git-version ) |
    serve /examples/config-dev.yaml
./bin/example-app
```
ericchiang: per irl conversation with @amrutac we're going to punt on the fav icon. @rithujohn191 want to take a quick look?
amrutac: 👍 

Patch:
diff --git a/Dockerfile b/Dockerfile
index c5361667..4f0d43a4 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -13,6 +13,11 @@ RUN apk add --update ca-certificates openssl
 
 COPY _output/bin/dex /usr/local/bin/dex
 
+# Import frontend assets and set the correct CWD directory so the assets
+# are in the default path.
+COPY web /web
+WORKDIR /
+
 ENTRYPOINT ["dex"]
 
 CMD ["version"]
diff --git a/Makefile b/Makefile
index 62ab5895..5aa8144e 100644
--- a/Makefile
+++ b/Makefile
@@ -25,7 +25,7 @@ LD_FLAGS="-w -X $(REPO_PATH)/version.Version=$(VERSION)"
 
 build: bin/dex bin/example-app
 
-bin/dex: FORCE generated
+bin/dex: FORCE
 	@go install -v -ldflags $(LD_FLAGS) $(REPO_PATH)/cmd/dex
 
 bin/example-app: FORCE
@@ -35,9 +35,6 @@ bin/example-app: FORCE
 release-binary:
 	@go build -o _output/bin/dex -v -ldflags $(LD_FLAGS) $(REPO_PATH)/cmd/dex
 
-.PHONY: generated
-generated: server/templates_default.go
-
 test:
 	@go test -v -i $(shell go list ./... | grep -v '/vendor/')
 	@go test -v $(shell go list ./... | grep -v '/vendor/')
@@ -57,9 +54,6 @@ lint:
       golint -set_exit_status $$package $$i || exit 1; \
 	done
 
-server/templates_default.go: $(wildcard web/templates/**)
-	@go run server/templates_default_gen.go
-
 _output/bin/dex:
 	# Using rkt to build the dex binary.
 	@./scripts/rkt-build
diff --git a/cmd/dex/config.go b/cmd/dex/config.go
index 2ee9e58a..dc3715b8 100644
--- a/cmd/dex/config.go
+++ b/cmd/dex/config.go
@@ -30,7 +30,7 @@ type Config struct {
 	GRPC       GRPC        `json:"grpc"`
 	Expiry     Expiry      `json:"expiry"`
 
-	Templates server.TemplateConfig `json:"templates"`
+	Frontend server.WebConfig `json:"frontend"`
 
 	// StaticClients cause the server to use this list of clients rather than
 	// querying the storage. Write operations, like creating a client, will fail.
diff --git a/cmd/dex/serve.go b/cmd/dex/serve.go
index e218e473..c9baa9d3 100644
--- a/cmd/dex/serve.go
+++ b/cmd/dex/serve.go
@@ -151,7 +151,7 @@ func serve(cmd *cobra.Command, args []string) error {
 		Issuer:                 c.Issuer,
 		Connectors:             connectors,
 		Storage:                s,
-		TemplateConfig:         c.Templates,
+		Web:                    c.Frontend,
 		EnablePasswordDB:       c.EnablePasswordDB,
 	}
 	if c.Expiry.SigningKeys != "" {
diff --git a/examples/config-dev.yaml b/examples/config-dev.yaml
index 5f94e202..1b5a48d3 100644
--- a/examples/config-dev.yaml
+++ b/examples/config-dev.yaml
@@ -14,7 +14,7 @@ storage:
 
 # Configuration for the HTTP endpoints.
 web:
-  http: 127.0.0.1:5556
+  http: 0.0.0.0:5556
   # Uncomment for HTTPS options.
   # https: 127.0.0.1:5554
   # tlsCert: /etc/dex/tls.crt
diff --git a/server/server.go b/server/server.go
index b8d2c8d3..91d119e9 100644
--- a/server/server.go
+++ b/server/server.go
@@ -56,7 +56,32 @@ type Config struct {
 
 	EnablePasswordDB bool
 
-	TemplateConfig TemplateConfig
+	Web WebConfig
+}
+
+// WebConfig holds the server's frontend templates and asset configuration.
+//
+// These are currently very custom to CoreOS and it's not recommended that
+// outside users attempt to customize these.
+type WebConfig struct {
+	// A filepath to web static.
+	//
+	// It is expected to contain the following directories:
+	//
+	//   * static - Static static served at "( issuer URL )/static".
+	//   * templates - HTML templates controlled by dex.
+	//   * themes/(theme) - Static static served at "( issuer URL )/theme".
+	//
+	Dir string
+
+	// Defaults to "( issuer URL )/theme/logo.png"
+	LogoURL string
+
+	// Defaults to "dex"
+	Issuer string
+
+	// Defaults to "coreos"
+	Theme string
 }
 
 func value(val, defaultValue time.Duration) time.Duration {
@@ -130,9 +155,17 @@ func newServer(ctx context.Context, c Config, rotationStrategy rotationStrategy)
 		supported[respType] = true
 	}
 
-	tmpls, err := loadTemplates(c.TemplateConfig)
+	web := webConfig{
+		dir:       c.Web.Dir,
+		logoURL:   c.Web.LogoURL,
+		issuerURL: c.Issuer,
+		issuer:    c.Web.Issuer,
+		theme:     c.Web.Theme,
+	}
+
+	static, theme, tmpls, err := loadWebConfig(web)
 	if err != nil {
-		return nil, fmt.Errorf("server: failed to load templates: %v", err)
+		return nil, fmt.Errorf("server: failed to load web static: %v", err)
 	}
 
 	now := c.Now
@@ -159,6 +192,10 @@ func newServer(ctx context.Context, c Config, rotationStrategy rotationStrategy)
 	handleFunc := func(p string, h http.HandlerFunc) {
 		r.HandleFunc(path.Join(issuerURL.Path, p), h)
 	}
+	handlePrefix := func(p string, h http.Handler) {
+		prefix := path.Join(issuerURL.Path, p)
+		r.PathPrefix(prefix).Handler(http.StripPrefix(prefix, h))
+	}
 	r.NotFoundHandler = http.HandlerFunc(s.notFound)
 
 	discoveryHandler, err := s.discoveryHandler()
@@ -175,6 +212,8 @@ func newServer(ctx context.Context, c Config, rotationStrategy rotationStrategy)
 	handleFunc("/callback", s.handleConnectorCallback)
 	handleFunc("/approval", s.handleApproval)
 	handleFunc("/healthz", s.handleHealth)
+	handlePrefix("/static", static)
+	handlePrefix("/theme", theme)
 	s.mux = r
 
 	startKeyRotation(ctx, c.Storage, rotationStrategy, now)
diff --git a/server/templates.go b/server/templates.go
index e8285fe3..649d5b08 100644
--- a/server/templates.go
+++ b/server/templates.go
@@ -6,8 +6,10 @@ import (
 	"io/ioutil"
 	"log"
 	"net/http"
+	"os"
 	"path/filepath"
 	"sort"
+	"strings"
 	"text/template"
 )
 
@@ -18,8 +20,6 @@ const (
 	tmplOOB      = "oob.html"
 )
 
-const coreOSLogoURL = "https://coreos.com/assets/images/brand/coreos-wordmark-135x40px.png"
-
 var requiredTmpls = []string{
 	tmplApproval,
 	tmplLogin,
@@ -27,65 +27,122 @@ var requiredTmpls = []string{
 	tmplOOB,
 }
 
-// TemplateConfig describes.
-type TemplateConfig struct {
-	// TODO(ericchiang): Asking for a directory with a set of templates doesn't indicate
-	// what the templates should look like and doesn't allow consumers of this package to
-	// provide their own templates in memory. In the future clean this up.
-
-	// Directory of the templates. If empty, these will be loaded from memory.
-	Dir string `yaml:"dir"`
+type templates struct {
+	loginTmpl    *template.Template
+	approvalTmpl *template.Template
+	passwordTmpl *template.Template
+	oobTmpl      *template.Template
+}
 
-	// Defaults to the CoreOS logo and "dex".
-	LogoURL string `yaml:"logoURL"`
-	Issuer  string `yaml:"issuerName"`
+type webConfig struct {
+	dir       string
+	logoURL   string
+	issuer    string
+	theme     string
+	issuerURL string
 }
 
-type globalData struct {
-	LogoURL string
-	Issuer  string
+func join(base, path string) string {
+	b := strings.HasSuffix(base, "/")
+	p := strings.HasPrefix(path, "/")
+	switch {
+	case b && p:
+		return base + path[1:]
+	case b || p:
+		return base + path
+	default:
+		return base + "/" + path
+	}
 }
 
-func loadTemplates(config TemplateConfig) (*templates, error) {
-	var tmpls *template.Template
-	if config.Dir != "" {
-		files, err := ioutil.ReadDir(config.Dir)
-		if err != nil {
-			return nil, fmt.Errorf("read dir: %v", err)
+func dirExists(dir string) error {
+	stat, err := os.Stat(dir)
+	if err != nil {
+		if os.IsNotExist(err) {
+			return fmt.Errorf("directory %q does not exist", dir)
 		}
-		filenames := []string{}
-		for _, file := range files {
-			if file.IsDir() {
-				continue
-			}
-			filenames = append(filenames, filepath.Join(config.Dir, file.Name()))
-		}
-		if len(filenames) == 0 {
-			return nil, fmt.Errorf("no files in template dir %s", config.Dir)
-		}
-		if tmpls, err = template.ParseFiles(filenames...); err != nil {
-			return nil, fmt.Errorf("parse files: %v", err)
+		return fmt.Errorf("stat directory %q: %v", dir, err)
+	}
+	if !stat.IsDir() {
+		return fmt.Errorf("path %q is a file not a directory", dir)
+	}
+	return nil
+}
+
+// loadWebConfig returns static assets, theme assets, and templates used by the frontend by
+// reading the directory specified in the webConfig.
+//
+// The directory layout is expected to be:
+//
+//    ( web directory )
+//    |- static
+//    |- themes
+//    |  |- (theme name)
+//    |- templates
+//
+func loadWebConfig(c webConfig) (static, theme http.Handler, templates *templates, err error) {
+	if c.theme == "" {
+		c.theme = "coreos"
+	}
+	if c.issuer == "" {
+		c.issuer = "dex"
+	}
+	if c.dir == "" {
+		c.dir = "./web"
+	}
+	if c.logoURL == "" {
+		c.logoURL = join(c.issuerURL, "theme/logo.png")
+	}
+
+	if err := dirExists(c.dir); err != nil {
+		return nil, nil, nil, fmt.Errorf("load web dir: %v", err)
+	}
+
+	staticDir := filepath.Join(c.dir, "static")
+	templatesDir := filepath.Join(c.dir, "templates")
+	themeDir := filepath.Join(c.dir, "themes", c.theme)
+
+	for _, dir := range []string{staticDir, templatesDir, themeDir} {
+		if err := dirExists(dir); err != nil {
+			return nil, nil, nil, fmt.Errorf("load dir: %v", err)
 		}
-	} else {
-		// Load templates from memory. This code is largely copied from the standard library's
-		// ParseFiles source code.
-		// See: https://goo.gl/6Wm4mN
-		for name, data := range defaultTemplates {
-			var t *template.Template
-			if tmpls == nil {
-				tmpls = template.New(name)
-			}
-			if name == tmpls.Name() {
-				t = tmpls
-			} else {
-				t = tmpls.New(name)
-			}
-			if _, err := t.Parse(data); err != nil {
-				return nil, fmt.Errorf("parsing %s: %v", name, err)
-			}
+	}
+
+	static = http.FileServer(http.Dir(staticDir))
+	theme = http.FileServer(http.Dir(themeDir))
+
+	templates, err = loadTemplates(c, templatesDir)
+	return
+}
+
+// loadTemplates parses the expected templates from the provided directory.
+func loadTemplates(c webConfig, templatesDir string) (*templates, error) {
+	files, err := ioutil.ReadDir(templatesDir)
+	if err != nil {
+		return nil, fmt.Errorf("read dir: %v", err)
+	}
+
+	filenames := []string{}
+	for _, file := range files {
+		if file.IsDir() {
+			continue
 		}
+		filenames = append(filenames, filepath.Join(templatesDir, file.Name()))
+	}
+	if len(filenames) == 0 {
+		return nil, fmt.Errorf("no files in template dir %q", templatesDir)
 	}
 
+	funcs := map[string]interface{}{
+		"issuer": func() string { return c.issuer },
+		"logo":   func() string { return c.logoURL },
+		"url":    func(s string) string { return join(c.issuerURL, s) },
+	}
+
+	tmpls, err := template.New("").Funcs(funcs).ParseFiles(filenames...)
+	if err != nil {
+		return nil, fmt.Errorf("parse files: %v", err)
+	}
 	missingTmpls := []string{}
 	for _, tmplName := range requiredTmpls {
 		if tmpls.Lookup(tmplName) == nil {
@@ -95,16 +152,7 @@ func loadTemplates(config TemplateConfig) (*templates, error) {
 	if len(missingTmpls) > 0 {
 		return nil, fmt.Errorf("missing template(s): %s", missingTmpls)
 	}
-
-	if config.LogoURL == "" {
-		config.LogoURL = coreOSLogoURL
-	}
-	if config.Issuer == "" {
-		config.Issuer = "dex"
-	}
-
 	return &templates{
-		globalData:   config,
 		loginTmpl:    tmpls.Lookup(tmplLogin),
 		approvalTmpl: tmpls.Lookup(tmplApproval),
 		passwordTmpl: tmpls.Lookup(tmplPassword),
@@ -118,14 +166,6 @@ var scopeDescriptions = map[string]string{
 	"email":          "View your email",
 }
 
-type templates struct {
-	globalData   TemplateConfig
-	loginTmpl    *template.Template
-	approvalTmpl *template.Template
-	passwordTmpl *template.Template
-	oobTmpl      *template.Template
-}
-
 type connectorInfo struct {
 	ID   string
 	Name string
@@ -142,21 +182,19 @@ func (t *templates) login(w http.ResponseWriter, connectors []connectorInfo, aut
 	sort.Sort(byName(connectors))
 
 	data := struct {
-		TemplateConfig
 		Connectors []connectorInfo
 		AuthReqID  string
-	}{t.globalData, connectors, authReqID}
+	}{connectors, authReqID}
 	renderTemplate(w, t.loginTmpl, data)
 }
 
 func (t *templates) password(w http.ResponseWriter, authReqID, callback, lastUsername string, lastWasInvalid bool) {
 	data := struct {
-		TemplateConfig
 		AuthReqID string
 		PostURL   string
 		Username  string
 		Invalid   bool
-	}{t.globalData, authReqID, callback, lastUsername, lastWasInvalid}
+	}{authReqID, string(callback), lastUsername, lastWasInvalid}
 	renderTemplate(w, t.passwordTmpl, data)
 }
 
@@ -170,20 +208,18 @@ func (t *templates) approval(w http.ResponseWriter, authReqID, username, clientN
 	}
 	sort.Strings(accesses)
 	data := struct {
-		TemplateConfig
 		User      string
 		Client    string
 		AuthReqID string
 		Scopes    []string
-	}{t.globalData, username, clientName, authReqID, accesses}
+	}{username, clientName, authReqID, accesses}
 	renderTemplate(w, t.approvalTmpl, data)
 }
 
 func (t *templates) oob(w http.ResponseWriter, code string) {
 	data := struct {
-		TemplateConfig
 		Code string
-	}{t.globalData, code}
+	}{code}
 	renderTemplate(w, t.oobTmpl, data)
 }
 
diff --git a/server/templates_default.go b/server/templates_default.go
deleted file mode 100644
index 651c7411..00000000
--- a/server/templates_default.go
+++ /dev/null
@@ -1,362 +0,0 @@
-// This file was generated by the makefile. Do not edit.
-
-package server
-
-// defaultTemplates is a key for file name to file data of the files in web/templates.
-var defaultTemplates = map[string]string{
-	"approval.html": `{{ template "header.html" . }}
-
-<div class="panel">
-  <h2 class="heading">Grant Access</h2>
-
-  <hr>
-  <div class="list-with-title">
-    <div class="subtle-text">{{ .Client }} would like to:</div>
-      {{ range $scope := .Scopes }}
-      <li class="bullet-point">
-        <div class="subtle-text">
-          {{ $scope }}
-        </div>
-      </li>
-      {{ end }}
-  </div>
-  <hr>
-
-  <div>
-    <div class="form-row">
-      <form method="post">
-        <input type="hidden" name="req" value="{{ .AuthReqID }}"/>
-        <input type="hidden" name="approval" value="approve">
-        <button type="submit" class="btn btn-success">
-            <span class="btn-text">Grant Access</span>
-        </button>
-      </form>
-    </div>
-    <div class="form-row">
-      <form method="post">
-        <input type="hidden" name="req" value="{{ .AuthReqID }}"/>
-        <input type="hidden" name="approval" value="rejected">
-        <button type="submit" class="btn btn-provider">
-            <span class="btn-text">Cancel</span>
-        </button>
-      </form>
-    </div>
-  </div>
-
-</div>
-
-{{ template "footer.html" . }}
-`,
-	"footer.html": `    </div>
-  </body>
-</html>
-`,
-	"header.html": `<!DOCTYPE html>
-<html>
-  <head>
-    <meta charset="utf-8">
-    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
-    <title>{{ .Issuer }}</title>
-    <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <style>
-      * {
-        -webkit-box-sizing: border-box;
-           -moz-box-sizing: border-box;
-                box-sizing: border-box;
-      }
-
-      html,
-      body {
-        margin: 0;
-        background-color: #efefef;
-        font-family: 'Source Sans Pro', Helvetica, sans-serif;
-        color: #333;
-      }
-      a {
-        color: #428BCA;
-        text-decoration: none;
-      }
-      a:active, a:hover, a:visited {
-        color: #2A6596;
-        text-decoration: underline;
-      }
-      #navbar {
-        background-color: #fff;
-        color: #333;
-        height: 46px;
-        box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2);
-        font-size: 13px;
-        font-weight: 100;
-        overflow: hidden;
-        padding: 0 10px;
-      }
-      #navbar-logo-wrap {
-        width: 300px;
-        height: 100%;
-        display: inline-block;
-        overflow: hidden;
-        padding: 10px 15px;
-      }
-      #navbar-logo {
-        height: 100%;
-        max-height: 25px;
-      }
-      #container {
-        margin: 45px auto;
-        text-align: center;
-        max-width: 500px;
-        min-width: 320px;
-      }
-      .heading {
-        font-size: 20px;
-        font-weight: 500;
-        margin-top: 0;
-        margin-bottom: 10px;
-      }
-      .footer {
-        margin: 30px;
-      }
-      .input-label-right {
-        position: absolute;
-        right: 0;
-        bottom: 0;
-      }
-      .input-desc {
-        width: 250px;
-        margin: 4px auto;
-        text-align: left;
-        position: relative;
-      }
-      .subtle-text {
-        color: #999;
-        font-size: 12px;
-      }
-      .panel {
-        background-color: #fff;
-        padding: 30px;
-        box-shadow: 0px 5px 15px rgba(0, 0, 0, 0.5);
-      }
-      .explain {
-        font-size: 13px;
-        color: #666;
-      }
-
-      .btn {
-        box-shadow: inset 0 1px 0px rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.25), 0 0px 1px rgba(0, 0, 0, 0.25);
-        padding: 0;
-        font-size: 14px;
-        border-radius: 4px;
-        border: none;
-        cursor: pointer;
-        font-size: 16px;
-      }
-      .btn:focus {
-        outline: none;
-      }
-      .btn:active {
-        outline: none;
-        box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
-      }
-      .btn-primary {
-        color: #fff;
-        background-color: #333;
-        padding: 6px 12px;
-        min-width: 200px;
-        border: none;
-      }
-      .btn-primary:hover {
-        background-color: #666;
-        color: #fff;
-      }
-      .btn-provider {
-        background-color: #fff;
-        color: #333;
-        width: 250px;
-      }
-      .btn-provider:hover {
-        color: #999;
-      }
-      .btn-success {
-        background-color: #2FC98E;
-        color: #fff;
-        width: 250px;
-      }
-      .btn-success:hover {
-        background-color: #49E3A8;
-      }
-      .btn-icon {
-        width: 36px;
-        height: 36px;
-        float: left;
-        margin-right: 5px;
-        background-repeat: no-repeat;
-        background-position: center;
-        background-size: 24px;
-      }
-      .btn-icon-google {
-        background-color: #DB4437;
-        background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+Cjxzdmcgd2lkdGg9IjM2cHgiIGhlaWdodD0iMzdweCIgdmlld0JveD0iMCAwIDM2IDM3IiB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHhtbG5zOnNrZXRjaD0iaHR0cDovL3d3dy5ib2hlbWlhbmNvZGluZy5jb20vc2tldGNoL25zIj4KICAgIDwhLS0gR2VuZXJhdG9yOiBTa2V0Y2ggMy4zLjIgKDEyMDQzKSAtIGh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaCAtLT4KICAgIDx0aXRsZT5TaGFwZSArIGcrPC90aXRsZT4KICAgIDxkZXNjPkNyZWF0ZWQgd2l0aCBTa2V0Y2guPC9kZXNjPgogICAgPGRlZnM+CiAgICAgICAgPGxpbmVhckdyYWRpZW50IHgxPSIzLjg0OTMxNTA3JSIgeTE9IjM0LjQ3MzI2MiUiIHgyPSI5Mi4yODU0Nzk1JSIgeTI9IjcwLjIyMzI2MiUiIGlkPSJsaW5lYXJHcmFkaWVudC0xIj4KICAgICAgICAgICAgPHN0b3Agc3RvcC1jb2xvcj0iIzNFMjcyMyIgc3RvcC1vcGFjaXR5PSIwLjIiIG9mZnNldD0iMCUiPjwvc3RvcD4KICAgICAgICAgICAgPHN0b3Agc3RvcC1jb2xvcj0iIzNFMjcyMyIgc3RvcC1vcGFjaXR5PSIwLjAyIiBvZmZzZXQ9IjEwMCUiPjwvc3RvcD4KICAgICAgICA8L2xpbmVhckdyYWRpZW50PgogICAgPC9kZWZzPgogICAgPGcgaWQ9IlBhZ2UtMSIgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9IjEiIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCIgc2tldGNoOnR5cGU9Ik1TUGFnZSI+CiAgICAgICAgPGcgaWQ9IkEuMS1WZXJpZnktRW1haWwtU2NyZWVuX2xvZ2luLSIgc2tldGNoOnR5cGU9Ik1TQXJ0Ym9hcmRHcm91cCIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTQwNy4wMDAwMDAsIC0yNzIuMDAwMDAwKSI+CiAgICAgICAgICAgIDxnIGlkPSJTaGFwZS0rLWcrIiBza2V0Y2g6dHlwZT0iTVNMYXllckdyb3VwIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSg0MDcuMDAwMDAwLCAyNzIuMDAwMDAwKSI+CiAgICAgICAgICAgICAgICA8cGF0aCBkPSJNMzUuOTYzOTg4MSwxNS4zMjM2OTA1IEwxOC43OTIzMjE0LDAuNzUyMDIzODEgTDEwLjc1Nzk3NjIsMC43NTIwMjM4MSBDNS4xNDQ5NDA0OCwwLjc1MjAyMzgxIDIuMzkzNDUyMzgsNC4xNjM4NjkwNSAyLjM5MzQ1MjM4LDguMDE1OTUyMzggQzIuMzkzNDUyMzgsMTAuOTg3NTU5NSA0LjgxNDc2MTksMTQuMjg5MzQ1MiA4Ljg4Njk2NDI5LDE0LjI4OTM0NTIgTDkuODc3NSwxNC4yODkzNDUyIEM5LjY1NzM4MDk1LDE0LjYxOTUyMzggOS41NDczMjE0MywxNS4yNzk4ODEgOS41NDczMjE0MywxNS43MjAxMTkgQzkuNTQ3MzIxNDMsMTYuODIwNzE0MyA5Ljk4NzU1OTUyLDE3LjM3MTAxMTkgMTAuNjQ3OTE2NywxOC4wMzEzNjkgQzguODg2OTY0MjksMTguMTQxNDI4NiA1LjU4NTE3ODU3LDE4LjQ3MTYwNzEgMy4xNjM4NjkwNSwyMC4wMTI0NDA1IEMwLjg1MjYxOTA0OCwyMS4zMzMxNTQ4IDAuMTkyMjYxOTA1LDIzLjMxNDIyNjIgMC4xOTIyNjE5MDUsMjQuNzQ1IEMwLjE5MjI2MTkwNSwyNS45NTU2NTQ4IDAuNjMyNSwyNy4wNTYyNSAxLjYyMzAzNTcxLDI4LjA0Njc4NTcgTDE1LjQ5MDUzNTcsMzYuOTE0Mjg1NyBMMzUuOTYzOTg4MSwzNi45MTQyODU3IEwzNS45NjM5ODgxLDE1LjMyMzY5MDUgWiIgaWQ9IlNoYXBlIiBmaWxsPSJ1cmwoI2xpbmVhckdyYWRpZW50LTEpIiBza2V0Y2g6dHlwZT0iTVNTaGFwZUdyb3VwIj48L3BhdGg+CiAgICAgICAgICAgICAgICA8ZyBpZD0iZysiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAuMTkyMjYyLCAxLjE5MjI2MikiIHNrZXRjaDp0eXBlPSJNU1NoYXBlR3JvdXAiPgogICAgICAgICAgICAgICAgICAgIDxwYXRoIGQ9Ik0xOC42MDAwNTk1LC0wLjExMDA1OTUyNCBMMTguNjAwMDU5NSwtMC40NDAyMzgwOTUgTDEwLjU2NTcxNDMsLTAuMTEwMDU5NTI0IEM0Ljk1MjY3ODU3LC0wLjExMDA1OTUyNCAyLjIwMTE5MDQ4LDMuMzAxNzg1NzEgMi4yMDExOTA0OCw3LjE1Mzg2OTA1IEMyLjIwMTE5MDQ4LDEwLjEyNTQ3NjIgNC42MjI1LDEzLjQyNzI2MTkgOC42OTQ3MDIzOCwxMy40MjcyNjE5IEw5LjY4NTIzODEsMTMuNDI3MjYxOSBDOS40NjUxMTkwNSwxMy43NTc0NDA1IDkuMzU1MDU5NTIsMTQuNDE3Nzk3NiA5LjM1NTA1OTUyLDE0Ljk2ODA5NTIgQzkuMzU1MDU5NTIsMTYuMDY4NjkwNSA5Ljc5NTI5NzYyLDE2LjYxODk4ODEgMTAuNDU1NjU0OCwxNy4yNzkzNDUyIEM4LjY5NDcwMjM4LDE3LjM4OTQwNDggNS4zOTI5MTY2NywxNy43MTk1ODMzIDIuOTcxNjA3MTQsMTkuMjYwNDE2NyBDMC42NjAzNTcxNDMsMjAuNTgxMTMxIDAsMjIuNTYyMjAyNCAwLDIzLjk5Mjk3NjIgQzAsMjYuODU0NTIzOCAyLjc1MTQ4ODEsMjkuNDk1OTUyNCA4LjM2NDUyMzgxLDI5LjQ5NTk1MjQgQzE1LjA3ODE1NDgsMjkuNDk1OTUyNCAxOC42MDAwNTk1LDI1Ljg2Mzk4ODEgMTguNjAwMDU5NSwyMi4yMzIwMjM4IEMxOC42MDAwNTk1LDE5LjQ4MDUzNTcgMTcuMDU5MjI2MiwxOC4xNTk4MjE0IDE1LjI5ODI3MzgsMTYuNzI5MDQ3NiBMMTMuODY3NSwxNS42Mjg0NTI0IEMxMy40MjcyNjE5LDE1LjI5ODI3MzggMTIuOTg3MDIzOCwxNC44NTgwMzU3IDEyLjk4NzAyMzgsMTMuOTc3NTU5NSBDMTIuOTg3MDIzOCwxMy4wOTcwODMzIDEzLjUzNzMyMTQsMTIuNDM2NzI2MiAxNC4wODc2MTksMTIuMTA2NTQ3NiBDMTUuNzM4NTExOSwxMC43ODU4MzMzIDE3LjM4OTQwNDgsOS40NjUxMTkwNSAxNy4zODk0MDQ4LDYuNDkzNTExOSBDMTcuMzg5NDA0OCwzLjc0MjAyMzgxIDE1LjczODUxMTksMi4zMTEyNSAxNC43NDc5NzYyLDEuNTQwODMzMzMgTDE2Ljk0OTE2NjcsMS41NDA4MzMzMyBMMTguNjAwMDU5NSwtMC4xMTAwNTk1MjQgTDE4LjYwMDA1OTUsLTAuMTEwMDU5NTI0IFogTTE2LjA2ODY5MDUsMjMuNjYyNzk3NiBDMTYuMDY4NjkwNSwyNS44NjM5ODgxIDE0LjA4NzYxOSwyNy44NDUwNTk1IDEwLjM0NTU5NTIsMjcuODQ1MDU5NSBDNi4xNjMzMzMzMywyNy44NDUwNTk1IDMuNTIxOTA0NzYsMjUuNzUzOTI4NiAzLjUyMTkwNDc2LDIzLjExMjUgQzMuNTIxOTA0NzYsMjAuMzYxMDExOSA2LjA1MzI3MzgxLDE5LjM3MDQ3NjIgNi44MjM2OTA0OCwxOS4wNDAyOTc2IEM4LjQ3NDU4MzMzLDE4LjQ5IDEwLjU2NTcxNDMsMTguMzc5OTQwNSAxMC44OTU4OTI5LDE4LjM3OTk0MDUgTDExLjc3NjM2OSwxOC4zNzk5NDA1IEMxNC43NDc5NzYyLDIwLjU4MTEzMSAxNi4wNjg2OTA1LDIxLjY4MTcyNjIgMTYuMDY4NjkwNSwyMy42NjI3OTc2IEwxNi4wNjg2OTA1LDIzLjY2Mjc5NzYgWiBNMTAuNTY1NzE0MywxMi4xMDY1NDc2IEM3LjI2MzkyODU3LDEyLjEwNjU0NzYgNS41MDI5NzYxOSw4LjI1NDQ2NDI5IDUuNTAyOTc2MTksNS4yODI4NTcxNCBDNS41MDI5NzYxOSwyLjUzMTM2OTA1IDcuMjYzOTI4NTcsMS4yMTA2NTQ3NiA5LjEzNDk0MDQ4LDEuMjEwNjU0NzYgQzEyLjY1Njg0NTIsMS4yMTA2NTQ3NiAxNC40MTc3OTc2LDUuNjEzMDM1NzEgMTQuNDE3Nzk3Niw4LjI1NDQ2NDI5IEMxNC4zMDc3MzgxLDExLjQ0NjE5MDUgMTEuNTU2MjUsMTIuMTA2NTQ3NiAxMC41NjU3MTQzLDEyLjEwNjU0NzYgTDEwLjU2NTcxNDMsMTIuMTA2NTQ3NiBaIE0yNi40MTQyODU3LDEyLjk4NzAyMzggTDI2LjQxNDI4NTcsOC4wMzQzNDUyNCBMMjQuNzYzMzkyOSw4LjAzNDM0NTI0IEwyNC43NjMzOTI5LDEyLjk4NzAyMzggTDE5LjgxMDcxNDMsMTIuOTg3MDIzOCBMMTkuODEwNzE0MywxNC42Mzc5MTY3IEwyNC43NjMzOTI5LDE0LjYzNzkxNjcgTDI0Ljc2MzM5MjksMTkuNTkwNTk1MiBMMjYuNDE0Mjg1NywxOS41OTA1OTUyIEwyNi40MTQyODU3LDE0LjYzNzkxNjcgTDMxLjM2Njk2NDMsMTQuNjM3OTE2NyBMMzEuMzY2OTY0MywxMi45ODcwMjM4IEwyNi40MTQyODU3LDEyLjk4NzAyMzggTDI2LjQxNDI4NTcsMTIuOTg3MDIzOCBaIiBpZD0iU2hhcGUiIG9wYWNpdHk9IjAuMTYiIGZpbGw9IiMzRTI3MjMiPjwvcGF0aD4KICAgICAgICAgICAgICAgICAgICA8cGF0aCBkPSJNMTguNjAwMDU5NSwtMC40NDAyMzgwOTUgTDEwLjU2NTcxNDMsLTAuNDQwMjM4MDk1IEM0Ljk1MjY3ODU3LC0wLjQ0MDIzODA5NSAyLjIwMTE5MDQ4LDIuOTcxNjA3MTQgMi4yMDExOTA0OCw2LjgyMzY5MDQ4IEMyLjIwMTE5MDQ4LDkuNzk1Mjk3NjIgNC42MjI1LDEzLjA5NzA4MzMgOC42OTQ3MDIzOCwxMy4wOTcwODMzIEw5LjY4NTIzODEsMTMuMDk3MDgzMyBDOS40NjUxMTkwNSwxMy40MjcyNjE5IDkuMzU1MDU5NTIsMTQuMDg3NjE5IDkuMzU1MDU5NTIsMTQuNTI3ODU3MSBDOS4zNTUwNTk1MiwxNS42Mjg0NTI0IDkuNzk1Mjk3NjIsMTYuMTc4NzUgMTAuNDU1NjU0OCwxNi44MzkxMDcxIEM4LjY5NDcwMjM4LDE2Ljk0OTE2NjcgNS4zOTI5MTY2NywxNy4yNzkzNDUyIDIuOTcxNjA3MTQsMTguODIwMTc4NiBDMC42NjAzNTcxNDMsMjAuMTQwODkyOSAwLDIyLjEyMTk2NDMgMCwyMy41NTI3MzgxIEMwLDI2LjQxNDI4NTcgMi43NTE0ODgxLDI5LjA1NTcxNDMgOC4zNjQ1MjM4MSwyOS4wNTU3MTQzIEMxNS4wNzgxNTQ4LDI5LjA1NTcxNDMgMTguNjAwMDU5NSwyNS40MjM3NSAxOC42MDAwNTk1LDIxLjc5MTc4NTcgQzE4LjYwMDA1OTUsMTkuMDQwMjk3NiAxNy4wNTkyMjYyLDE3LjcxOTU4MzMgMTUuMjk4MjczOCwxNi4yODg4MDk1IEwxMy44Njc1LDE1LjE4ODIxNDMgQzEzLjQyNzI2MTksMTQuODU4MDM1NyAxMi45ODcwMjM4LDE0LjQxNzc5NzYgMTIuOTg3MDIzOCwxMy41MzczMjE0IEMxMi45ODcwMjM4LDEyLjY1Njg0NTIgMTMuNTM3MzIxNCwxMS45OTY0ODgxIDE0LjA4NzYxOSwxMS42NjYzMDk1IEMxNS43Mzg1MTE5LDEwLjM0NTU5NTIgMTcuMzg5NDA0OCw5LjAyNDg4MDk1IDE3LjM4OTQwNDgsNi4wNTMyNzM4MSBDMTcuMzg5NDA0OCwzLjMwMTc4NTcxIDE1LjczODUxMTksMS44NzEwMTE5IDE0Ljc0Nzk3NjIsMS4xMDA1OTUyNCBMMTYuOTQ5MTY2NywxLjEwMDU5NTI0IEwxOC42MDAwNTk1LC0wLjQ0MDIzODA5NSBMMTguNjAwMDU5NSwtMC40NDAyMzgwOTUgWiBNMTYuMDY4NjkwNSwyMy4zMzI2MTkgQzE2LjA2ODY5MDUsMjUuNTMzODA5NSAxNC4wODc2MTksMjcuNTE0ODgxIDEwLjM0NTU5NTIsMjcuNTE0ODgxIEM2LjE2MzMzMzMzLDI3LjUxNDg4MSAzLjUyMTkwNDc2LDI1LjQyMzc1IDMuNTIxOTA0NzYsMjIuNzgyMzIxNCBDMy41MjE5MDQ3NiwyMC4wMzA4MzMzIDYuMDUzMjczODEsMTkuMDQwMjk3NiA2LjgyMzY5MDQ4LDE4LjcxMDExOSBDOC40NzQ1ODMzMywxOC4xNTk4MjE0IDEwLjU2NTcxNDMsMTguMDQ5NzYxOSAxMC44OTU4OTI5LDE4LjA0OTc2MTkgTDExLjc3NjM2OSwxOC4wNDk3NjE5IEMxNC43NDc5NzYyLDIwLjI1MDk1MjQgMTYuMDY4NjkwNSwyMS4zNTE1NDc2IDE2LjA2ODY5MDUsMjMuMzMyNjE5IEwxNi4wNjg2OTA1LDIzLjMzMjYxOSBaIE0xMC41NjU3MTQzLDExLjg4NjQyODYgQzcuMjYzOTI4NTcsMTEuODg2NDI4NiA1LjUwMjk3NjE5LDguMDM0MzQ1MjQgNS41MDI5NzYxOSw1LjA2MjczODEgQzUuNTAyOTc2MTksMi4zMTEyNSA3LjI2MzkyODU3LDAuOTkwNTM1NzE0IDkuMTM0OTQwNDgsMC45OTA1MzU3MTQgQzEyLjY1Njg0NTIsMC45OTA1MzU3MTQgMTQuNDE3Nzk3Niw1LjM5MjkxNjY3IDE0LjQxNzc5NzYsOC4wMzQzNDUyNCBDMTQuMzA3NzM4MSwxMS4yMjYwNzE0IDExLjU1NjI1LDExLjg4NjQyODYgMTAuNTY1NzE0MywxMS44ODY0Mjg2IEwxMC41NjU3MTQzLDExLjg4NjQyODYgWiBNMjYuNDE0Mjg1NywxMi42NTY4NDUyIEwyNi40MTQyODU3LDcuNzA0MTY2NjcgTDI0Ljc2MzM5MjksNy43MDQxNjY2NyBMMjQuNzYzMzkyOSwxMi42NTY4NDUyIEwxOS44MTA3MTQzLDEyLjY1Njg0NTIgTDE5LjgxMDcxNDMsMTQuMzA3NzM4MSBMMjQuNzYzMzkyOSwxNC4zMDc3MzgxIEwyNC43NjMzOTI5LDE5LjI2MDQxNjcgTDI2LjQxNDI4NTcsMTkuMjYwNDE2NyBMMjYuNDE0Mjg1NywxNC4zMDc3MzgxIEwzMS4zNjY5NjQzLDE0LjMwNzczODEgTDMxLjM2Njk2NDMsMTIuNjU2ODQ1MiBMMjYuNDE0Mjg1NywxMi42NTY4NDUyIEwyNi40MTQyODU3LDEyLjY1Njg0NTIgWiIgaWQ9IlNoYXBlIiBmaWxsPSIjRjFGMUYxIj48L3BhdGg+CiAgICAgICAgICAgICAgICAgICAgPHBhdGggZD0iTTkuNzk1Mjk3NjIsMTMuMzE3MjAyNCBMOS43OTUyOTc2MiwxMy4wOTcwODMzIEM5LjU3NTE3ODU3LDEzLjQyNzI2MTkgOS40NjUxMTkwNSwxNC4wODc2MTkgOS40NjUxMTkwNSwxNC41Mjc4NTcxIEw5LjQ2NTExOTA1LDE0LjYzNzkxNjcgQzkuNDY1MTE5MDUsMTQuMTk3Njc4NiA5LjU3NTE3ODU3LDEzLjY0NzM4MSA5Ljc5NTI5NzYyLDEzLjMxNzIwMjQgTDkuNzk1Mjk3NjIsMTMuMzE3MjAyNCBaIE0xMC40NTU2NTQ4LDE2Ljk0OTE2NjcgQzguNjk0NzAyMzgsMTcuMDU5MjI2MiA1LjM5MjkxNjY3LDE3LjM4OTQwNDggMi45NzE2MDcxNCwxOC45MzAyMzgxIEMwLjY2MDM1NzE0MywyMC4yNTA5NTI0IDAsMjIuMjMyMDIzOCAwLDIzLjY2Mjc5NzYgTDAsMjMuNzcyODU3MSBDMC4xMTAwNTk1MjQsMjIuMzQyMDgzMyAwLjc3MDQxNjY2NywyMC40NzEwNzE0IDIuOTcxNjA3MTQsMTkuMTUwMzU3MSBDNS4zOTI5MTY2NywxNy43MTk1ODMzIDguNjk0NzAyMzgsMTcuMjc5MzQ1MiAxMC40NTU2NTQ4LDE3LjE2OTI4NTcgTDEwLjQ1NTY1NDgsMTYuOTQ5MTY2NyBMMTAuNDU1NjU0OCwxNi45NDkxNjY3IFogTTEwLjM0NTU5NTIsMjcuNTE0ODgxIEM2LjI3MzM5Mjg2LDI3LjUxNDg4MSAzLjYzMTk2NDI5LDI1LjUzMzgwOTUgMy41MjE5MDQ3NiwyMi44OTIzODEgTDMuNTIxOTA0NzYsMjMuMDAyNDQwNSBDMy41MjE5MDQ3NiwyNS42NDM4NjkgNi4xNjMzMzMzMywyNy43MzUgMTAuMzQ1NTk1MiwyNy43MzUgQzE0LjA4NzYxOSwyNy43MzUgMTYuMDY4NjkwNSwyNS43NTM5Mjg2IDE2LjA2ODY5MDUsMjMuNTUyNzM4MSBMMTYuMDY4NjkwNSwyMy40NDI2Nzg2IEMxNS45NTg2MzEsMjUuNjQzODY5IDEzLjk3NzU1OTUsMjcuNTE0ODgxIDEwLjM0NTU5NTIsMjcuNTE0ODgxIEwxMC4zNDU1OTUyLDI3LjUxNDg4MSBaIE0xNC4zMDc3MzgxLDguMjU0NDY0MjkgTDE0LjMwNzczODEsOC4xNDQ0MDQ3NiBDMTQuMTk3Njc4NiwxMS4zMzYxMzEgMTEuNTU2MjUsMTEuODg2NDI4NiAxMC40NTU2NTQ4LDExLjg4NjQyODYgQzcuMjYzOTI4NTcsMTEuODg2NDI4NiA1LjM5MjkxNjY3LDguMTQ0NDA0NzYgNS4zOTI5MTY2Nyw1LjE3Mjc5NzYyIEw1LjM5MjkxNjY3LDUuMjgyODU3MTQgQzUuMzkyOTE2NjcsOC4yNTQ0NjQyOSA3LjE1Mzg2OTA1LDEyLjEwNjU0NzYgMTAuNDU1NjU0OCwxMi4xMDY1NDc2IEMxMS41NTYyNSwxMi4xMDY1NDc2IDE0LjMwNzczODEsMTEuNDQ2MTkwNSAxNC4zMDc3MzgxLDguMjU0NDY0MjkgTDE0LjMwNzczODEsOC4yNTQ0NjQyOSBaIE0xNS40MDgzMzMzLDE2LjI4ODgwOTUgTDEzLjk3NzU1OTUsMTUuMTg4MjE0MyBDMTMuNTM3MzIxNCwxNC44NTgwMzU3IDEzLjIwNzE0MjksMTQuNDE3Nzk3NiAxMy4wOTcwODMzLDEzLjY0NzM4MSBMMTMuMDk3MDgzMywxMy43NTc0NDA1IEMxMy4wOTcwODMzLDE0LjYzNzkxNjcgMTMuNTM3MzIxNCwxNS4wNzgxNTQ4IDEzLjk3NzU1OTUsMTUuNDA4MzMzMyBMMTUuNDA4MzMzMywxNi41MDg5Mjg2IEMxNy4wNTkyMjYyLDE3LjkzOTcwMjQgMTguNjAwMDU5NSwxOS4yNjA0MTY3IDE4LjcxMDExOSwyMS43OTE3ODU3IEwxOC43MTAxMTksMjEuNjgxNzI2MiBDMTguNjAwMDU5NSwxOS4wNDAyOTc2IDE3LjA1OTIyNjIsMTcuODI5NjQyOSAxNS40MDgzMzMzLDE2LjI4ODgwOTUgTDE1LjQwODMzMzMsMTYuMjg4ODA5NSBaIE0yNi40MTQyODU3LDEyLjY1Njg0NTIgTDI2LjQxNDI4NTcsMTIuODc2OTY0MyBMMzEuMzY2OTY0MywxMi44NzY5NjQzIEwzMS4zNjY5NjQzLDEyLjY1Njg0NTIgTDI2LjQxNDI4NTcsMTIuNjU2ODQ1MiBMMjYuNDE0Mjg1NywxMi42NTY4NDUyIFogTTE3LjQ5OTQ2NDMsNi4yNzMzOTI4NiBMMTcuNDk5NDY0Myw2LjE2MzMzMzMzIEMxNy40OTk0NjQzLDMuNDExODQ1MjQgMTUuODQ4NTcxNCwxLjk4MTA3MTQzIDE0Ljg1ODAzNTcsMS4yMTA2NTQ3NiBMMTQuODU4MDM1NywxLjU0MDgzMzMzIEMxNS44NDg1NzE0LDIuMjAxMTkwNDggMTcuNDk5NDY0MywzLjYzMTk2NDI5IDE3LjQ5OTQ2NDMsNi4yNzMzOTI4NiBMMTcuNDk5NDY0Myw2LjI3MzM5Mjg2IFogTTI2LjQxNDI4NTcsNy43MDQxNjY2NyBMMjQuNzYzMzkyOSw3LjcwNDE2NjY3IEwyNC43NjMzOTI5LDcuOTI0Mjg1NzEgTDI2LjQxNDI4NTcsNy45MjQyODU3MSBMMjYuNDE0Mjg1Nyw3LjcwNDE2NjY3IEwyNi40MTQyODU3LDcuNzA0MTY2NjcgWiBNMi4zMTEyNSw2LjgyMzY5MDQ4IEwyLjMxMTI1LDYuOTMzNzUgQzIuNDIxMzA5NTIsMy4wODE2NjY2NyA1LjA2MjczODEsLTAuMTEwMDU5NTI0IDEwLjU2NTcxNDMsLTAuMTEwMDU5NTI0IEwxOC4zNzk5NDA1LC0wLjExMDA1OTUyNCBMMTguNzEwMTE5LC0wLjQ0MDIzODA5NSBMMTAuNjc1NzczOCwtMC40NDAyMzgwOTUgQzQuOTUyNjc4NTcsLTAuNDQwMjM4MDk1IDIuMzExMjUsMi45NzE2MDcxNCAyLjMxMTI1LDYuODIzNjkwNDggTDIuMzExMjUsNi44MjM2OTA0OCBaIE0xOS44MTA3MTQzLDEyLjk4NzAyMzggTDI0Ljc2MzM5MjksMTIuOTg3MDIzOCBMMjQuNzYzMzkyOSwxMi43NjY5MDQ4IEwxOS44MTA3MTQzLDEyLjc2NjkwNDggTDE5LjgxMDcxNDMsMTIuOTg3MDIzOCBMMTkuODEwNzE0MywxMi45ODcwMjM4IFoiIGlkPSJTaGFwZSIgZmlsbC1vcGFjaXR5PSIwLjY0IiBmaWxsPSIjRkZGRkZGIj48L3BhdGg+CiAgICAgICAgICAgICAgICA8L2c+CiAgICAgICAgICAgIDwvZz4KICAgICAgICA8L2c+CiAgICA8L2c+Cjwvc3ZnPg==);
-      }
-      .btn-icon-local {
-        background-color: #84B6EF;
-        background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+Cjxzdmcgd2lkdGg9IjI0cHgiIGhlaWdodD0iMjBweCIgdmlld0JveD0iMCAwIDI0IDIwIiB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHhtbG5zOnNrZXRjaD0iaHR0cDovL3d3dy5ib2hlbWlhbmNvZGluZy5jb20vc2tldGNoL25zIj4KICAgIDwhLS0gR2VuZXJhdG9yOiBTa2V0Y2ggMy4zLjIgKDEyMDQzKSAtIGh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaCAtLT4KICAgIDx0aXRsZT5SZWN0YW5nbGUgMjkxICsgUGF0aCAyMzI8L3RpdGxlPgogICAgPGRlc2M+Q3JlYXRlZCB3aXRoIFNrZXRjaC48L2Rlc2M+CiAgICA8ZGVmcz48L2RlZnM+CiAgICA8ZyBpZD0iUGFnZS0xIiBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiBza2V0Y2g6dHlwZT0iTVNQYWdlIj4KICAgICAgICA8ZyBpZD0iQS4xLVZlcmlmeS1FbWFpbC1TY3JlZW5fbG9naW4tIiBza2V0Y2g6dHlwZT0iTVNBcnRib2FyZEdyb3VwIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtNDA5LjAwMDAwMCwgLTIwOS4wMDAwMDApIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZT0iI0ZGRkZGRiI+CiAgICAgICAgICAgIDxnIGlkPSJSZWN0YW5nbGUtMzktQ29weS02LSstRW1haWwtQ29weS0rLVJlY3RhbmdsZS0yOTAtKy1SZWN0YW5nbGUtMjkxLSstUGF0aC0yMzIiIHNrZXRjaDp0eXBlPSJNU0xheWVyR3JvdXAiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDQwMC4wMDAwMDAsIDE5Ny4wMDAwMDApIj4KICAgICAgICAgICAgICAgIDxnIGlkPSJSZWN0YW5nbGUtMjkwLSstUmVjdGFuZ2xlLTI5MS0rLVBhdGgtMjMyIiBza2V0Y2g6dHlwZT0iTVNTaGFwZUdyb3VwIj4KICAgICAgICAgICAgICAgICAgICA8ZyBpZD0iUmVjdGFuZ2xlLTI5MS0rLVBhdGgtMjMyIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSg5LjAwMDAwMCwgMTIuMDAwMDAwKSI+CiAgICAgICAgICAgICAgICAgICAgICAgIDxnPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgPHJlY3QgaWQ9IlJlY3RhbmdsZS0yOTEiIHg9IjAiIHk9IjAiIHdpZHRoPSIyNCIgaGVpZ2h0PSIxOS4zNSI+PC9yZWN0PgogICAgICAgICAgICAgICAgICAgICAgICAgICAgPHBhdGggZD0iTTAsMS45MjcyNzEzOSBMMTEuNjExMzAxOSwxMi45IEwyNCwxLjE5MjYyODgxIiBpZD0iUGF0aC0yMzIiPjwvcGF0aD4KICAgICAgICAgICAgICAgICAgICAgICAgPC9nPgogICAgICAgICAgICAgICAgICAgIDwvZz4KICAgICAgICAgICAgICAgIDwvZz4KICAgICAgICAgICAgPC9nPgogICAgICAgIDwvZz4KICAgIDwvZz4KPC9zdmc+);
-      }
-      .btn-icon-coreos {
-        /* B&W CoreOS SVG logo */
-        background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DQo8c3ZnIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBmaWxsPSIjNjY2IiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmlld0JveD0iMCAwIDIxNSAyMTUiIHhtbDpzcGFjZT0icHJlc2VydmUiPg0KICA8Zz4NCiAgICA8Zz4NCiAgICAgIDxwYXRoIGQ9Ik0xMDcuNDc5LDEuMDc1Yy01OC42NzcsMC0xMDYuNDA0LDQ3LjczLTEwNi40MDQsMTA2LjM5OGMwLDU4LjY3Miw0Ny43MjcsMTA2LjM5OSwxMDYuNDA0LDEwNi4zOTkNCiAgICAgICAgICAgICAgYzU4LjY1OSwwLDEwNi4zOS00Ny43MjcsMTA2LjM5LTEwNi4zOTlDMjEzLjg2OSw0OC44MDUsMTY2LjEzOCwxLjA3NSwxMDcuNDc5LDEuMDc1eiBNMTQ3LjQ0OSwxMzQuNjI3DQogICAgICAgICAgICAgIGMtMC44OCwwLjEyOC0xLjc0OSwwLjI1MS0yLjYzMiwwLjM2NGMtOC4wMywxLjAzOC0xNi42MDIsMS43NDMtMjUuNTYxLDIuMDc4Yy0zLjg1NiwwLjE0NC03Ljc5MywwLjIzMS0xMS43NzYsMC4yMzENCiAgICAgICAgICAgICAgYy0zLjk5NSwwLTcuOTItMC4wODYtMTEuNzg4LTAuMjMxYy04Ljk0OC0wLjMzNS0xNy41MjYtMS4wNC0yNS41NDktMi4wNzhjLTAuNzE2LTUuOTg3LTEuMjAxLTEyLjIxNi0xLjQ0My0xOC42MjMNCiAgICAgICAgICAgICAgYy0wLjExNi0yLjkyNi0wLjE3My01Ljg5Ni0wLjE3My04Ljg5NWMwLTMuMDAyLDAuMDU3LTUuOTY2LDAuMTczLTguODk4YzAuMjQzLTYuNDA4LDAuNzI4LTEyLjYzMywxLjQ0My0xOC42Mg0KICAgICAgICAgICAgICBjMC4xNDQtMS4yNDYsMC4zMDYtMi40ODUsMC40NzMtMy43MDljNS4yNDEtMzguMDQsMTkuNzUyLTY1LjQwOCwzNi44NjMtNjUuNDA4YzUzLjM2NCwwLDk2LjYzMiw0My4yNjIsOTYuNjMzLDk2LjYzNQ0KICAgICAgICAgICAgICBDMjA0LjExMiwxMTkuNTQ3LDE4MC44NjYsMTI5LjkzNCwxNDcuNDQ5LDEzNC42Mjd6Ii8+DQogICAgICA8cGF0aCBkPSJNMTQ3LjQ0OCw4MC4zMTZjLTAuOTY5LTEuNDE0LTIuMDA5LTIuNzY4LTMuMTE3LTQuMDY5Yy04Ljg2Ni0xMC40NTEtMjIuMDc0LTE3LjA5Mi0zNi44NTItMTcuMDkyDQogICAgICAgICAgICAgIGMtNC43OTEsMC05LjA1Nyw3LjMzMy0xMS43ODgsMTguNzJjLTEuMDg1LDQuNTQtMS45MjgsOS43MjEtMi40NywxNS4zNDNjLTAuNDI4LDQuNTA1LTAuNjU4LDkuMjk3LTAuNjU4LDE0LjI1NQ0KICAgICAgICAgICAgICBzMC4yMzEsOS43NTEsMC42NTgsMTQuMjUyYzQuNTA4LDAuNDI4LDkuMjkzLDAuNjU3LDE0LjI1OCwwLjY1N2M0Ljk1OSwwLDkuNzQ0LTAuMjMsMTQuMjUyLTAuNjU3DQogICAgICAgICAgICAgIGM5LjkxMS0wLjk0LDE4LjQ2Ni0yLjg0NiwyNC41MjctNS4zNTdjNS45ODYtMi40NzYsOS41MjgtNS41NTksOS41MjgtOC44OTVDMTU1Ljc4Niw5Ny40MDcsMTUyLjcxMiw4OC4wNTcsMTQ3LjQ0OCw4MC4zMTZ6Ii8+DQogICAgPC9nPg0KICA8L2c+DQo8L3N2Zz4NCg==);
-      }
-      .btn-icon-github {
-        background-color: #F5F5F5;
-        background-image: url(data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjE2IiB3aWR0aD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggZD0iTTggMEMzLjU4IDAgMCAzLjU4IDAgOGMwIDMuNTQgMi4yOSA2LjUzIDUuNDcgNy41OSAwLjQgMC4wNyAwLjU1LTAuMTcgMC41NS0wLjM4IDAtMC4xOS0wLjAxLTAuODItMC4wMS0xLjQ5LTIuMDEgMC4zNy0yLjUzLTAuNDktMi42OS0wLjk0LTAuMDktMC4yMy0wLjQ4LTAuOTQtMC44Mi0xLjEzLTAuMjgtMC4xNS0wLjY4LTAuNTItMC4wMS0wLjUzIDAuNjMtMC4wMSAxLjA4IDAuNTggMS4yMyAwLjgyIDAuNzIgMS4yMSAxLjg3IDAuODcgMi4zMyAwLjY2IDAuMDctMC41MiAwLjI4LTAuODcgMC41MS0xLjA3LTEuNzgtMC4yLTMuNjQtMC44OS0zLjY0LTMuOTUgMC0wLjg3IDAuMzEtMS41OSAwLjgyLTIuMTUtMC4wOC0wLjItMC4zNi0xLjAyIDAuMDgtMi4xMiAwIDAgMC42Ny0wLjIxIDIuMiAwLjgyIDAuNjQtMC4xOCAxLjMyLTAuMjcgMi0wLjI3IDAuNjggMCAxLjM2IDAuMDkgMiAwLjI3IDEuNTMtMS4wNCAyLjItMC44MiAyLjItMC44MiAwLjQ0IDEuMSAwLjE2IDEuOTIgMC4wOCAyLjEyIDAuNTEgMC41NiAwLjgyIDEuMjcgMC44MiAyLjE1IDAgMy4wNy0xLjg3IDMuNzUtMy42NSAzLjk1IDAuMjkgMC4yNSAwLjU0IDAuNzMgMC41NCAxLjQ4IDAgMS4wNy0wLjAxIDEuOTMtMC4wMSAyLjIgMCAwLjIxIDAuMTUgMC40NiAwLjU1IDAuMzhDMTMuNzEgMTQuNTMgMTYgMTEuNTMgMTYgOCAxNiAzLjU4IDEyLjQyIDAgOCAweiIgLz4KPC9zdmc+Cg==);
-      }
-      .btn-icon-bitbucket {
-        background-color: #205081;
-        background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMTAwMCIgd2lkdGg9Ijc4NS43MTQiPjxwYXRoIGQ9Ik00NTQuNzcgNDc5LjM4NnE0LjQ2NCAzNS4xNTQgLTI4LjE3OSA1Ni4zNTh0LTYyLjIxNyAzLjM0OHEtMjEuNzYyIC05LjQ4NiAtMjkuODUzIC0zMi4zNjR0LS4yNzkgLTQ1Ljc1NiAyOS4wMTYgLTMyLjM2NHEyMC4wODggLTEwLjA0NCA0MC40NTUgLTYuNjk2dDM1LjcxMiAxOS44MDkgMTUuMzQ1IDM3LjY2NXptNjEuOTM4IC0xMS43MThxLTcuODEyIC01OS43MDYgLTYzLjA1NCAtOTEuNTEydC0xMDkuOTI2IC03LjI1NHEtMzUuMTU0IDE1LjYyNCAtNTYuMDc5IDQ5LjM4M3QtMTkuMjUxIDcyLjI2MXEyLjIzMiA1MC43NzggNDMuMjQ1IDg2LjQ5dDkyLjM0OSAzMS4yNDhxNTAuNzc4IC00LjQ2NCA4NC44MTYgLTQ2Ljg3MnQyNy45IC05My43NDR6bTEzMy4zNjIgLTMwMi40MzZxLTExLjE2IC0xNS4wNjYgLTMxLjI0OCAtMjQuODMxdC0zMi4zNjQgLTEyLjI3NiAtMzkuNjE4IC02Ljk3NXEtMTYyLjM3OCAtMjYuMjI2IC0zMTUuODI4IDEuMTE2IC0yMy45OTQgMy45MDYgLTM2LjgyOCA2LjY5NnQtMzAuNjkgMTIuMjc2IC0yNy45IDIzLjk5NHExNi43NCAxNS42MjQgNDIuNDA4IDI1LjM4OXQ0MS4wMTMgMTIuMjc2IDQ4LjgyNSA2LjQxN3ExMjcuMjI0IDE2LjE4MiAyNDkuOTg0IC41NTggMzUuMTU0IC00LjQ2NCA0OS45NDEgLTYuNjk2dDQwLjQ1NSAtMTEuOTk3IDQxLjg1IC0yNS45NDd6bTMxLjgwNiA1NzcuNTNxLTQuNDY0IDE0LjUwOCAtOC42NDkgNDIuNjg3dC03LjgxMiA0Ni44NzIgLTE1LjkwMyAzOS4wNiAtMzIuMzY0IDMxLjUyN3EtNDcuOTg4IDI2Ljc4NCAtMTA1Ljc0MSAzOS44OTd0LTExMi43MTYgMTIuMjc2IC0xMTIuNDM3IC0xMC4zMjNxLTI1LjY2OCAtNC40NjQgLTQ1LjQ3NyAtMTAuMDQ0dC00Mi42ODcgLTE1LjA2NiAtNDAuNzM0IC0yNC4yNzMgLTI5LjAxNiAtMzQuMzE3cS0xMy45NSAtNTMuNTY4IC0zMS44MDYgLTE2Mi45MzZsMy4zNDggLTguOTI4IDEwLjA0NCAtNS4wMjJxMTI0LjQzNCA4Mi41ODQgMjgyLjYyNyA4Mi41ODR0MjgzLjE4NSAtODIuNTg0cTExLjcxOCAzLjM0OCAxMy4zOTIgMTIuODM0dC0yLjc5IDI1LjExIC00LjQ2NCAyMC42NDZ6bTEwMC45OTggLTUzNi4yMzhxLTE0LjUwOCA5My4xODYgLTYxLjkzOCAzNjUuNDkgLTIuNzkgMTYuNzQgLTE1LjA2NiAzMS4yNDh0LTI0LjI3MyAyMi4zMiAtMzAuNDExIDE3LjI5OHEtMTQwLjYxNiA3MC4zMDggLTM0MC4zOCA0OS4xMDQgLTEzOC4zODQgLTE1LjA2NiAtMjE5Ljg1MiAtNzcuNTYyIC04LjM3IC02LjY5NiAtMTQuMjI5IC0xNC43ODd0LTkuNDg2IC0xOS41MyAtNS4wMjIgLTE4Ljk3MiAtMy4zNDggLTIyLjA0MSAtMy4wNjkgLTE5LjUzcS01LjAyMiAtMjcuOSAtMTQuNzg3IC04My43dC0xNS42MjQgLTkwLjExNyAtMTMuMTEzIC04Mi4zMDUgLTEyLjI3NiAtODguMTY0cTEuNjc0IC0xNC41MDggOS43NjUgLTI3LjA2M3QxNy41NzcgLTIwLjkyNSAyNS4xMSAtMTYuNzQgMjUuNjY4IC0xMi41NTUgMjYuNzg0IC0xMC4zMjNxNjkuNzUgLTI1LjY2OCAxNzQuNjU0IC0zNS43MTIgMjExLjQ4MiAtMjAuNjQ2IDM3Ny4yMDggMjcuOSA4Ni40OSAyNS42NjggMTE5Ljk3IDY4LjA3NiA4LjkyOCAxMS4xNiA5LjIwNyAyOC40NTh0LTMuMDY5IDMwLjEzMnoiIGZpbGw9IiNGRkZGRkYiLz48L3N2Zz4K);
-      }
-      .btn-text {
-        line-height: 36px;
-        padding: 6px 12px;
-        text-align: center;
-        font-weight: 600;
-      }
-      .form-row {
-        display: block;
-        margin: 20px auto;
-      }
-      label {
-        font-size: 13px;
-        font-weight: 600;
-      }
-      .input-box {
-        display: block;
-        height: 36px;
-        padding: 6px 12px;
-        font-size: 14px;
-        line-height: 1.42857143;
-        color: #666;
-        border: 1px solid #CCC;
-        border-radius: 4px;
-        box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
-        width: 250px;
-        margin: auto;
-      }
-      .input-box:focus,
-      .input-box:active {
-        outline: none;
-        border-color: #66AFE9;
-      }
-      .error-box-field,
-      .error-box {
-        background-color: #DD1327;
-        max-width: 320px;
-        color: #fff;
-        font-size: 14px;
-        font-weight: normal;
-        padding: 4px 0;
-      }
-      .error-box {
-        margin: 20px auto;
-      }
-      .error-box-field {
-        margin: 0 auto;
-        width: 250px;
-      }
-      .instruction-block {
-        font-size: 14px;
-      }
-      .detail-block {
-        color: #777;
-        font-size: 12px;
-        margin-top: 20px;
-      }
-      .bullet-point {
-        list-style: square;
-      }
-      .list-with-title {
-        text-align: left;
-        margin: 0 25%;
-      }
-      .hr {
-        color: #999;
-      }
-    </style>
-  </head>
-
-  <body>
-    <div id="navbar">
-      <div id="navbar-logo-wrap">
-        <img id="navbar-logo" src="{{ .LogoURL }}">
-      </div>
-    </div>
-
-    <div id="container">
-
-`,
-	"login.html": `{{ template "header.html" . }}
-
-<div class="panel">
-  <h2 class="heading">Log in to {{ .Issuer }} </h2>
-
-  <div>
-    {{ range $c := .Connectors }}
-      <div class="form-row">
-        <a href="{{ $c.URL }}?req={{ $.AuthReqID }}" target="_self">
-          <button class="btn btn-provider">
-            <span class="btn-icon btn-icon-{{ $c.ID }}"></span>
-            <span class="btn-text">Log in with {{ $c.Name }}</span>
-          </button>
-        </a>
-      </div>
-    {{ end }}
-  </div>
-
-</div>
-
-
-{{ template "footer.html" . }}
-`,
-	"oob.html": `{{ template "header.html" . }}
-
-<div class="panel">
-  <h2 class="heading">Login Successful</h2>
-
-  Please copy this code, switch to your application and paste it there:
-  <br/>
-  <input type="text" value="{{ .Code }}" />
-</div>
-
-{{ template "footer.html" . }}
-`,
-	"password.html": `{{ template "header.html" . }}
-
-<div class="panel">
-  <h2 class="heading">Log in to Your Account</h2>
-  <form method="post" action="{{ .PostURL }}">
-    <div class="form-row">
-      <div class="input-desc">
-        <label for="userid">Username</label>
-      </div>
-	  <input tabindex="1" required id="login" name="login" type="text" class="input-box" placeholder="username" {{ if .Username }}value="{{ .Username }}" {{ else }} autofocus {{ end }}/>
-    </div>
-    <div class="form-row">
-      <div class="input-desc">
-        <label for="password">Password</label>
-      </div>
-	  <input tabindex="2" required id="password" name="password" type="password" class="input-box" placeholder="password" {{ if .Invalid }} autofocus {{ end }}/>
-    </div>
-    <input type="hidden" name="req" value="{{ .AuthReqID }}"/>
-
-    {{ if .Invalid }}
-      <div class="error-box">
-        Invalid username and password.
-      </div>
-    {{ end }}
-
-    <button tabindex="3" type="submit" class="btn btn-primary">Login</button>
-
-  </form>
-</div>
-
-{{ template "footer.html" . }}
-`,
-}
diff --git a/server/templates_default_gen.go b/server/templates_default_gen.go
deleted file mode 100644
index 0e46df78..00000000
--- a/server/templates_default_gen.go
+++ /dev/null
@@ -1,85 +0,0 @@
-// +build ignore
-
-package main
-
-import (
-	"bytes"
-	"fmt"
-	"io/ioutil"
-	"log"
-	"os/exec"
-	"path/filepath"
-)
-
-// ignoreFile uses "git check-ignore" to determine if we should ignore a file.
-func ignoreFile(p string) (ok bool, err error) {
-	err = exec.Command("git", "check-ignore", p).Run()
-	if err == nil {
-		return true, nil
-	}
-	exitErr, ok := err.(*exec.ExitError)
-	if ok {
-		if sys := exitErr.Sys(); sys != nil {
-			e, ok := sys.(interface {
-				// Is the returned value something that returns an exit status?
-				ExitStatus() int
-			})
-			if ok && e.ExitStatus() == 1 {
-				return false, nil
-			}
-		}
-	}
-	return false, err
-}
-
-// Maps aren't deterministic, use a struct instead.
-
-type fileData struct {
-	name string
-	data string
-}
-
-func main() {
-	// ReadDir guarentees result in sorted order.
-	dir, err := ioutil.ReadDir("web/templates")
-	if err != nil {
-		log.Fatal(err)
-	}
-	files := []fileData{}
-	for _, file := range dir {
-		p := filepath.Join("web/templates", file.Name())
-		ignore, err := ignoreFile(p)
-		if err != nil {
-			log.Fatal(err)
-		}
-		if ignore {
-			continue
-		}
-
-		data, err := ioutil.ReadFile(p)
-		if err != nil {
-			log.Fatal(err)
-		}
-		if bytes.Contains(data, []byte{'`'}) {
-			log.Fatalf("file %s contains escape character '`' and cannot be compiled into go source", p)
-		}
-		files = append(files, fileData{file.Name(), string(data)})
-	}
-
-	f := new(bytes.Buffer)
-
-	fmt.Fprintln(f, "// This file was generated by the makefile. Do not edit.")
-	fmt.Fprintln(f)
-	fmt.Fprintln(f, "package server")
-	fmt.Fprintln(f)
-	fmt.Fprintln(f, "// defaultTemplates is a key for file name to file data of the files in web/templates.")
-	fmt.Fprintln(f, "var defaultTemplates = map[string]string{")
-	for _, file := range files {
-		fmt.Fprintf(f, "\t%q: `%s`,\n", file.name, file.data)
-	}
-	fmt.Fprintln(f, "}")
-
-	if err := ioutil.WriteFile("server/templates_default.go", f.Bytes(), 0644); err != nil {
-		log.Fatal(err)
-	}
-}
diff --git a/web/static/main.css b/web/static/main.css
new file mode 100644
index 00000000..e69de29b
diff --git a/web/templates/header.html b/web/templates/header.html
index cadb078d..79438ec4 100644
--- a/web/templates/header.html
+++ b/web/templates/header.html
@@ -3,8 +3,10 @@
   <head>
     <meta charset="utf-8">
     <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
-    <title>{{ .Issuer }}</title>
+    <title>{{ issuer }}</title>
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <link href="{{ url "static/main.css" }}" rel="stylesheet">
+    <link href="{{ url "theme/style.css" }}" rel="stylesheet">
     <style>
       * {
         -webkit-box-sizing: border-box;
@@ -232,7 +234,7 @@
   <body>
     <div id="navbar">
       <div id="navbar-logo-wrap">
-        <img id="navbar-logo" src="{{ .LogoURL }}">
+        <img id="navbar-logo" src="{{ logo }}">
       </div>
     </div>
 
diff --git a/web/templates/login.html b/web/templates/login.html
index ea43903a..e52f775a 100644
--- a/web/templates/login.html
+++ b/web/templates/login.html
@@ -1,7 +1,7 @@
 {{ template "header.html" . }}
 
 <div class="panel">
-  <h2 class="heading">Log in to {{ .Issuer }} </h2>
+  <h2 class="heading">Log in to {{ issuer }} </h2>
 
   <div>
     {{ range $c := .Connectors }}


Test patch:
diff --git a/server/server_test.go b/server/server_test.go
index a5865dfa..3ab41940 100644
--- a/server/server_test.go
+++ b/server/server_test.go
@@ -11,6 +11,8 @@ import (
 	"net/http/httptest"
 	"net/http/httputil"
 	"net/url"
+	"os"
+	"path/filepath"
 	"reflect"
 	"sort"
 	"strings"
@@ -85,6 +87,9 @@ func newTestServer(ctx context.Context, t *testing.T, updateConfig func(c *Confi
 				Connector:   mock.NewCallbackConnector(),
 			},
 		},
+		Web: WebConfig{
+			Dir: filepath.Join(os.Getenv("GOPATH"), "src/github.com/coreos/dex/web"),
+		},
 	}
 	if updateConfig != nil {
 		updateConfig(&config)
diff --git a/server/templates_test.go b/server/templates_test.go
index efbb29ed..abb4e431 100644
--- a/server/templates_test.go
+++ b/server/templates_test.go
@@ -1,16 +1,1 @@
 package server
-
-import "testing"
-
-func TestNewTemplates(t *testing.T) {
-	var config TemplateConfig
-	if _, err := loadTemplates(config); err != nil {
-		t.Fatal(err)
-	}
-}
-
-func TestLoadTemplates(t *testing.T) {
-	var config TemplateConfig
-
-	config.Dir = "../web/templates"
-}