├── .gitignore ├── Makefile ├── static ├── robots.txt ├── styles.css ├── main.js └── pure-min.css ├── templates ├── error.tpl ├── empty.tpl ├── create.tpl ├── preshow.tpl ├── layout.tpl ├── new.tpl ├── show.tpl └── 404.html ├── go.mod ├── recaptcha.go ├── templates.go ├── go.sum ├── store.go ├── README.md └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | crypt 2 | crypt.db 3 | config.json 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build 2 | 3 | build: 4 | go build . 5 | -------------------------------------------------------------------------------- /static/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Allow: /$ 3 | Disallow: / 4 | -------------------------------------------------------------------------------- /templates/error.tpl: -------------------------------------------------------------------------------- 1 |
Something went wrong
3 | Create something 4 |There is nothing, and there was nothing all the time.
3 | Create something 4 |Sorry, but the page you were trying to view does not exist.
58 | 59 | 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Crypt 2 | 3 | This is the simplest possible service for leaving one-time notes. 4 |
5 |
6 | ## Features
7 |
8 | 1. Generate link that will be available only for one view (and QR code for it)
9 | 2. Links protected with reCAPTCHA, so brute-force is not possible (reCAPTCHA js file is included on a separate pre-show page and never on the same page where you enter or reveal secret)
10 | 3. Optional E2E encryption. Secret encrypted in the browser and is sent to the server encrypted.
11 | 4. Simplistic uncluttered responsive UI with full mobile support
12 |
13 | Crypt can be compiled into a single binary, all batteries included:
14 |
15 | 1. Embedded DB (boltdb)
16 | 2. All static files are compiled to binary
17 |
18 | Trying to keep dependencies to bare minimum, only use:
19 |
20 | 1. google uuid library
21 | 2. boltdb for storing secrets
22 | 3. QR code library for qr code generation
23 |
24 | Frontend follows the same ideology:
25 |
26 | 1. No JS frameworks. The only JS file is responsible for E2E encryption and autoresizing textarea
27 | 2. Everything is rendered server-side (with native Go module). Works without JS perfectly via forms (long forgotten technology), except for reCAPTCHA.
28 | 3. Pure CSS is only 3-rd party dependency for grid and styling forms, inputs, etc.
29 |
30 | ## TODO
31 |
32 | Would be nice to have those features:
33 |
34 | 1. Customizable expiration for links
35 | 2. Customizable amount of possible views for links
36 |
37 | ## Installation
38 |
39 | You need to have go and make tools installed:
40 |
41 | 1. Clone this repository
42 | 2. Run `make`
43 |
44 | ## Configuration
45 |
46 | - `-config` — path to configuration file
47 |
48 | Possible contents of a configuration file is listed below:
49 |
50 | ```
51 | {
52 | "port": "which port should be used to start server, default: 8080",
53 | "domain": "domain that this server is binded to, need this to generate correct link, required",
54 | "db_location": "location of the db file on disk, default: crypt.db",
55 | "client_key": "ReCAPTCHA site key, required",
56 | "secret_key": "ReCAPTCHA secret key, required"
57 | }
58 | ```
59 |
60 | - `-prefix` — prefix for env variables name, default: crypt
61 |
62 | You can override any option with env variable, even those that are required. So, you can totally skip config file and use only env vars.
63 | To do that you have to set env varibale with name `{prefix}\_{option}`.
64 |
65 | Option name should be spelled uppercase without any symbols.
66 | For example, to override secret key you have to set `{prefix}\_SECRETKEY` variable.
67 |
--------------------------------------------------------------------------------
/static/main.js:
--------------------------------------------------------------------------------
1 | function str2ab(str) {
2 | var buf = new ArrayBuffer(str.length * 2); // 2 bytes for each char
3 | var bufView = new Uint16Array(buf);
4 | for (var i = 0, strLen = str.length; i < strLen; i++) {
5 | bufView[i] = str.charCodeAt(i);
6 | }
7 | return buf;
8 | }
9 |
10 | function ab2str(buf) {
11 | var bufView = new Uint16Array(buf);
12 | return bufView.toString();
13 | }
14 |
15 | function toRealString(buf) {
16 | return String.fromCharCode.apply(null, new Uint16Array(buf));
17 | }
18 |
19 | function init() {
20 | var text = document.querySelector("._content_area");
21 | function resize() {
22 | text.style.height = "auto";
23 | text.style.height = text.scrollHeight + 5 + "px";
24 | }
25 | /* 0-timeout to get the already changed text */
26 | function delayedResize() {
27 | window.setTimeout(resize, 0);
28 | }
29 | text.addEventListener("input", resize, false);
30 | text.focus();
31 | text.select();
32 | resize();
33 | }
34 |
35 | function deab2str(str) {
36 | var arr = str.split(",").map(function (i) {
37 | return parseInt(i);
38 | });
39 | var buf = new ArrayBuffer(arr.length * 2);
40 | var bufView = new Uint16Array(buf);
41 | arr.forEach(function (v, i) {
42 | bufView[i] = v;
43 | });
44 | return buf;
45 | }
46 |
47 | function initalizeEncrypt(form) {
48 | form.addEventListener("submit", encryptSecret);
49 | }
50 |
51 | function initializeDecryptor(decryptor) {
52 | decryptor.addEventListener("click", decryptSecret);
53 | }
54 |
55 | var cipherType = {
56 | name: "AES-CTR",
57 | length: 256,
58 | };
59 |
60 | function keyFromPass(pass, salt) {
61 | var bufPass = str2ab(pass);
62 | return crypto.subtle
63 | .importKey("raw", bufPass, { name: "PBKDF2" }, false, ["deriveKey"])
64 | .then(function (key) {
65 | return crypto.subtle.deriveKey(
66 | {
67 | name: "PBKDF2",
68 | salt: salt,
69 | iterations: 1000,
70 | hash: { name: "SHA-1" },
71 | },
72 | key,
73 | cipherType,
74 | false,
75 | ["encrypt", "decrypt"]
76 | );
77 | });
78 | }
79 |
80 | function encrypt(secret, key) {
81 | var bufSecret = str2ab(secret);
82 | return crypto.subtle
83 | .encrypt(
84 | {
85 | name: "AES-CTR",
86 | counter: new Uint8Array(16),
87 | length: 128,
88 | },
89 | key,
90 | bufSecret
91 | )
92 | .then(function (cipher) {
93 | return ab2str(cipher);
94 | });
95 | }
96 |
97 | function decrypt(cipher, key) {
98 | var bufCipher = deab2str(cipher);
99 | return crypto.subtle
100 | .decrypt(
101 | {
102 | name: "AES-CTR",
103 | counter: new Uint8Array(16),
104 | length: 128,
105 | },
106 | key,
107 | bufCipher
108 | )
109 | .then(function (bufPlain) {
110 | return toRealString(bufPlain);
111 | });
112 | }
113 |
114 | var cipherText = "";
115 |
116 | function decryptSecret(ev) {
117 | var secret = document.querySelector("._secret_show");
118 | var pass = document.querySelector("._decrypt_pass");
119 | var saltEl = document.querySelector("._salt");
120 | pass.nextElementSibling.setAttribute("hidden", "");
121 |
122 | var originalCipher = cipherText || secret.value;
123 | cipherText = originalCipher;
124 | var salt = saltEl.value.split(",").map(function (i) {
125 | return parseInt(i);
126 | });
127 | salt = new Uint8Array(salt);
128 | keyFromPass(pass.value, salt)
129 | .then(function (key) {
130 | return decrypt(originalCipher, key);
131 | })
132 | .then(function (plaintext) {
133 | secret.value = plaintext;
134 | secret.style.height = "auto";
135 | secret.style.height = secret.scrollHeight + "px";
136 | })
137 | .catch(function (err) {
138 | pass.nextElementSibling.removeAttribute("hidden");
139 | });
140 | }
141 |
142 | function encryptSecret(ev) {
143 | var pass = document.querySelector("._encrypt_pass");
144 | var secret = document.querySelector("._create_secret");
145 | var saltEl = document.querySelector("._salt");
146 | var salt = window.crypto.getRandomValues(new Uint8Array(16));
147 | pass.nextElementSibling.setAttribute("hidden", "");
148 | saltEl.value = salt.toString();
149 | if (pass.value) {
150 | keyFromPass(pass.value, salt)
151 | .then(function (key) {
152 | return encrypt(secret.value, key);
153 | })
154 | .then(function (cipher) {
155 | secret.value = cipher;
156 | ev.target.removeEventListener("submit", encryptSecret);
157 | ev.target.submit();
158 | })
159 | .catch(function (error) {
160 | pass.nextElementSibling.removeAttribute("hidden");
161 | pass.value = "";
162 | saltEl.value = "";
163 | });
164 | ev.preventDefault();
165 | }
166 | }
167 |
168 | (function () {
169 | document.addEventListener("DOMContentLoaded", function () {
170 | var form = document.querySelector("._submit_new");
171 | var decryptor = document.querySelector("._decryptor");
172 | if (form) {
173 | initalizeEncrypt(form);
174 | }
175 |
176 | if (decryptor) {
177 | initializeDecryptor(decryptor);
178 | }
179 | });
180 | })();
181 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "embed"
6 | "encoding/json"
7 | "flag"
8 | "fmt"
9 | "html/template"
10 | "net/http"
11 | "os"
12 | "reflect"
13 | "strings"
14 |
15 | "github.com/boltdb/bolt"
16 | "github.com/skip2/go-qrcode"
17 | )
18 |
19 | //go:embed static/*
20 | var static embed.FS
21 |
22 | type Config struct {
23 | Domain string `required:"true" json:"domain"`
24 | SecretKey string `required:"true" json:"secret_key"`
25 | ClientKey string `required:"true" json:"client_key"`
26 | DbLocation string `default:"crypt.db" json:"db_location"`
27 | Port string `default:"8080" json:"port"`
28 | }
29 |
30 | func handler(w http.ResponseWriter, r *http.Request, db *bolt.DB, config Config) {
31 | var action = r.URL.Path[1:]
32 | switch action {
33 | case "":
34 | var tplRes bytes.Buffer
35 | loadTemplate("new.tpl").Execute(&tplRes, nil)
36 | loadTemplate("layout.tpl").Execute(w, template.HTML(tplRes.String()))
37 | case "create":
38 | var tplRes bytes.Buffer
39 | err := r.ParseForm()
40 | if err != nil {
41 | loadTemplate("error.tpl").Execute(&tplRes, err)
42 | } else {
43 | body := r.FormValue("secret")
44 | salt := r.FormValue("salt")
45 | link, storeErr := storeAndLink(db, body, salt)
46 | link = config.Domain + "/show?uid=" + link
47 | if storeErr != nil {
48 | loadTemplate("error.tpl").Execute(&tplRes, nil)
49 | } else {
50 | loadTemplate("create.tpl").Execute(&tplRes, link)
51 | }
52 | }
53 | loadTemplate("layout.tpl").Execute(w, template.HTML(tplRes.String()))
54 |
55 | case "show":
56 | var tplRes bytes.Buffer
57 | err := r.ParseForm()
58 | if err != nil {
59 | loadTemplate("error.tpl").Execute(&tplRes, err)
60 | } else {
61 | uid := r.FormValue("uid")
62 | recaptcha := r.FormValue("g-recaptcha-response")
63 |
64 | if recaptcha != "" && checkRecaptcha(config.SecretKey, recaptcha) {
65 | secret, salt, readErr := readAndDelete(db, uid)
66 | if readErr != nil {
67 | loadTemplate("error.tpl").Execute(&tplRes, nil)
68 | } else if secret == "" {
69 | loadTemplate("empty.tpl").Execute(&tplRes, nil)
70 | } else {
71 | loadTemplate("show.tpl").Execute(&tplRes, map[string]string{
72 | "secret": secret,
73 | "salt": salt,
74 | })
75 | }
76 | } else {
77 | loadTemplate("preshow.tpl").Execute(&tplRes, map[string]string{
78 | "uid": uid,
79 | "clientKey": config.ClientKey,
80 | })
81 | }
82 | }
83 | loadTemplate("layout.tpl").Execute(w, template.HTML(tplRes.String()))
84 | case "qr.png":
85 | uid := r.URL.Query().Get("uid")
86 | data, err := qrcode.Encode(fmt.Sprintf("%s/show?uid=%s", config.Domain, uid), qrcode.Medium, 256)
87 | if err != nil {
88 | w.WriteHeader(http.StatusInternalServerError)
89 | w.Header().Set("Content-Type", "image/png")
90 | w.Write([]byte(""))
91 | } else {
92 | w.WriteHeader(http.StatusOK)
93 | w.Write(data)
94 | }
95 | default:
96 | file, err := static.ReadFile("static/" + action)
97 | if err != nil {
98 | w.WriteHeader(http.StatusNotFound)
99 | loadTemplate("404.html").Execute(w, "")
100 | } else {
101 | if strings.HasSuffix(action, ".css") {
102 | w.Header().Set("Content-Type", "text/css")
103 | }
104 | w.WriteHeader(http.StatusOK)
105 | w.Write(file)
106 | }
107 | }
108 | }
109 |
110 | func getEnv(prefix, name string) (string, bool) {
111 | envName := strings.ToUpper(fmt.Sprintf("%s_%s", prefix, name))
112 | return os.LookupEnv(envName)
113 | }
114 |
115 | func overrideConfig(prefix string, config any) error {
116 | type_ := reflect.TypeOf(config)
117 | if type_.Kind() != reflect.Ptr {
118 | return fmt.Errorf("config struct must be pointer to a struct")
119 | }
120 | type_ = type_.Elem()
121 | if type_.Kind() != reflect.Struct {
122 | return fmt.Errorf("config struct must be pointer to a struct")
123 | }
124 | value_ := reflect.ValueOf(config).Elem()
125 | fields := reflect.VisibleFields(type_)
126 | for _, field := range fields {
127 | if field.Type.Kind() != reflect.String {
128 | return fmt.Errorf("only string config parameters are supported, for %s given %s", field.Name, field.Type.String())
129 | }
130 | v, ok := getEnv(prefix, field.Name)
131 | fieldValue := value_.FieldByIndex(field.Index)
132 | if ok && fieldValue.CanSet() {
133 | fieldValue.SetString(v)
134 | }
135 | required, ok := field.Tag.Lookup("required")
136 | if ok && required == "true" && fieldValue.String() == "" {
137 | return fmt.Errorf("field %s is required but not set", field.Name)
138 | }
139 | def, ok := field.Tag.Lookup("default")
140 | if ok && fieldValue.String() == "" && fieldValue.CanSet() {
141 | fieldValue.SetString(def)
142 | }
143 | }
144 | return nil
145 | }
146 |
147 | func main() {
148 | config := Config{}
149 | var configLoc = flag.String("config", "", "config file for crypt in JSON format")
150 | var prefix = flag.String("prefix", "crypt", "env prefix for overrides")
151 | flag.Parse()
152 | if configLoc != nil && *configLoc != "" {
153 | file, err := os.ReadFile(*configLoc)
154 | if err != nil {
155 | panic(fmt.Sprintf("can't read config file %s", *configLoc))
156 | }
157 | json.Unmarshal(file, &config)
158 | }
159 | err := overrideConfig(*prefix, &config)
160 | if err != nil {
161 | panic(fmt.Sprintf("error while parsing config: %s", err.Error()))
162 | }
163 | db, err := bolt.Open(config.DbLocation, 0600, nil)
164 | if err != nil {
165 | panic(err)
166 | }
167 | defer db.Close()
168 |
169 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
170 | handler(w, r, db, config)
171 | })
172 | fmt.Printf("Listening on port %s\n", config.Port)
173 | http.ListenAndServe(":"+config.Port, nil)
174 | }
175 |
--------------------------------------------------------------------------------
/static/pure-min.css:
--------------------------------------------------------------------------------
1 | /*!
2 | Pure v3.0.0
3 | Copyright 2013 Yahoo!
4 | Licensed under the BSD License.
5 | https://github.com/pure-css/pure/blob/master/LICENSE
6 | */
7 | /*!
8 | normalize.css v | MIT License | https://necolas.github.io/normalize.css/
9 | Copyright (c) Nicolas Gallagher and Jonathan Neal
10 | */
11 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=button],[type=reset],[type=submit],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{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-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none}html{font-family:sans-serif}.hidden,[hidden]{display:none!important}.pure-img{max-width:100%;height:auto;display:block}.pure-g{display:flex;flex-flow:row wrap;align-content:flex-start}.pure-u{display:inline-block;vertical-align:top}.pure-u-1,.pure-u-1-1,.pure-u-1-12,.pure-u-1-2,.pure-u-1-24,.pure-u-1-3,.pure-u-1-4,.pure-u-1-5,.pure-u-1-6,.pure-u-1-8,.pure-u-10-24,.pure-u-11-12,.pure-u-11-24,.pure-u-12-24,.pure-u-13-24,.pure-u-14-24,.pure-u-15-24,.pure-u-16-24,.pure-u-17-24,.pure-u-18-24,.pure-u-19-24,.pure-u-2-24,.pure-u-2-3,.pure-u-2-5,.pure-u-20-24,.pure-u-21-24,.pure-u-22-24,.pure-u-23-24,.pure-u-24-24,.pure-u-3-24,.pure-u-3-4,.pure-u-3-5,.pure-u-3-8,.pure-u-4-24,.pure-u-4-5,.pure-u-5-12,.pure-u-5-24,.pure-u-5-5,.pure-u-5-6,.pure-u-5-8,.pure-u-6-24,.pure-u-7-12,.pure-u-7-24,.pure-u-7-8,.pure-u-8-24,.pure-u-9-24{display:inline-block;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-1-24{width:4.1667%}.pure-u-1-12,.pure-u-2-24{width:8.3333%}.pure-u-1-8,.pure-u-3-24{width:12.5%}.pure-u-1-6,.pure-u-4-24{width:16.6667%}.pure-u-1-5{width:20%}.pure-u-5-24{width:20.8333%}.pure-u-1-4,.pure-u-6-24{width:25%}.pure-u-7-24{width:29.1667%}.pure-u-1-3,.pure-u-8-24{width:33.3333%}.pure-u-3-8,.pure-u-9-24{width:37.5%}.pure-u-2-5{width:40%}.pure-u-10-24,.pure-u-5-12{width:41.6667%}.pure-u-11-24{width:45.8333%}.pure-u-1-2,.pure-u-12-24{width:50%}.pure-u-13-24{width:54.1667%}.pure-u-14-24,.pure-u-7-12{width:58.3333%}.pure-u-3-5{width:60%}.pure-u-15-24,.pure-u-5-8{width:62.5%}.pure-u-16-24,.pure-u-2-3{width:66.6667%}.pure-u-17-24{width:70.8333%}.pure-u-18-24,.pure-u-3-4{width:75%}.pure-u-19-24{width:79.1667%}.pure-u-4-5{width:80%}.pure-u-20-24,.pure-u-5-6{width:83.3333%}.pure-u-21-24,.pure-u-7-8{width:87.5%}.pure-u-11-12,.pure-u-22-24{width:91.6667%}.pure-u-23-24{width:95.8333%}.pure-u-1,.pure-u-1-1,.pure-u-24-24,.pure-u-5-5{width:100%}.pure-button{display:inline-block;line-height:normal;white-space:nowrap;vertical-align:middle;text-align:center;cursor:pointer;-webkit-user-drag:none;-webkit-user-select:none;user-select:none;box-sizing:border-box}.pure-button::-moz-focus-inner{padding:0;border:0}.pure-button-group{letter-spacing:-.31em;text-rendering:optimizespeed}.opera-only :-o-prefocus,.pure-button-group{word-spacing:-0.43em}.pure-button-group .pure-button{letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-button{font-family:inherit;font-size:100%;padding:.5em 1em;color:rgba(0,0,0,.8);border:none transparent;background-color:#e6e6e6;text-decoration:none;border-radius:2px}.pure-button-hover,.pure-button:focus,.pure-button:hover{background-image:linear-gradient(transparent,rgba(0,0,0,.05) 40%,rgba(0,0,0,.1))}.pure-button:focus{outline:0}.pure-button-active,.pure-button:active{box-shadow:0 0 0 1px rgba(0,0,0,.15) inset,0 0 6px rgba(0,0,0,.2) inset;border-color:#000}.pure-button-disabled,.pure-button-disabled:active,.pure-button-disabled:focus,.pure-button-disabled:hover,.pure-button[disabled]{border:none;background-image:none;opacity:.4;cursor:not-allowed;box-shadow:none;pointer-events:none}.pure-button-hidden{display:none}.pure-button-primary,.pure-button-selected,a.pure-button-primary,a.pure-button-selected{background-color:#0078e7;color:#fff}.pure-button-group .pure-button{margin:0;border-radius:0;border-right:1px solid rgba(0,0,0,.2)}.pure-button-group .pure-button:first-child{border-top-left-radius:2px;border-bottom-left-radius:2px}.pure-button-group .pure-button:last-child{border-top-right-radius:2px;border-bottom-right-radius:2px;border-right:none}.pure-form input[type=color],.pure-form input[type=date],.pure-form input[type=datetime-local],.pure-form input[type=datetime],.pure-form input[type=email],.pure-form input[type=month],.pure-form input[type=number],.pure-form input[type=password],.pure-form input[type=search],.pure-form input[type=tel],.pure-form input[type=text],.pure-form input[type=time],.pure-form input[type=url],.pure-form input[type=week],.pure-form select,.pure-form textarea{padding:.5em .6em;display:inline-block;border:1px solid #ccc;box-shadow:inset 0 1px 3px #ddd;border-radius:4px;vertical-align:middle;box-sizing:border-box}.pure-form input:not([type]){padding:.5em .6em;display:inline-block;border:1px solid #ccc;box-shadow:inset 0 1px 3px #ddd;border-radius:4px;box-sizing:border-box}.pure-form input[type=color]{padding:.2em .5em}.pure-form input[type=color]:focus,.pure-form input[type=date]:focus,.pure-form input[type=datetime-local]:focus,.pure-form input[type=datetime]:focus,.pure-form input[type=email]:focus,.pure-form input[type=month]:focus,.pure-form input[type=number]:focus,.pure-form input[type=password]:focus,.pure-form input[type=search]:focus,.pure-form input[type=tel]:focus,.pure-form input[type=text]:focus,.pure-form input[type=time]:focus,.pure-form input[type=url]:focus,.pure-form input[type=week]:focus,.pure-form select:focus,.pure-form textarea:focus{outline:0;border-color:#129fea}.pure-form input:not([type]):focus{outline:0;border-color:#129fea}.pure-form input[type=checkbox]:focus,.pure-form input[type=file]:focus,.pure-form input[type=radio]:focus{outline:thin solid #129FEA;outline:1px auto #129FEA}.pure-form .pure-checkbox,.pure-form .pure-radio{margin:.5em 0;display:block}.pure-form input[type=color][disabled],.pure-form input[type=date][disabled],.pure-form input[type=datetime-local][disabled],.pure-form input[type=datetime][disabled],.pure-form input[type=email][disabled],.pure-form input[type=month][disabled],.pure-form input[type=number][disabled],.pure-form input[type=password][disabled],.pure-form input[type=search][disabled],.pure-form input[type=tel][disabled],.pure-form input[type=text][disabled],.pure-form input[type=time][disabled],.pure-form input[type=url][disabled],.pure-form input[type=week][disabled],.pure-form select[disabled],.pure-form textarea[disabled]{cursor:not-allowed;background-color:#eaeded;color:#cad2d3}.pure-form input:not([type])[disabled]{cursor:not-allowed;background-color:#eaeded;color:#cad2d3}.pure-form input[readonly],.pure-form select[readonly],.pure-form textarea[readonly]{background-color:#eee;color:#777;border-color:#ccc}.pure-form input:focus:invalid,.pure-form select:focus:invalid,.pure-form textarea:focus:invalid{color:#b94a48;border-color:#e9322d}.pure-form input[type=checkbox]:focus:invalid:focus,.pure-form input[type=file]:focus:invalid:focus,.pure-form input[type=radio]:focus:invalid:focus{outline-color:#e9322d}.pure-form select{height:2.25em;border:1px solid #ccc;background-color:#fff}.pure-form select[multiple]{height:auto}.pure-form label{margin:.5em 0 .2em}.pure-form fieldset{margin:0;padding:.35em 0 .75em;border:0}.pure-form legend{display:block;width:100%;padding:.3em 0;margin-bottom:.3em;color:#333;border-bottom:1px solid #e5e5e5}.pure-form-stacked input[type=color],.pure-form-stacked input[type=date],.pure-form-stacked input[type=datetime-local],.pure-form-stacked input[type=datetime],.pure-form-stacked input[type=email],.pure-form-stacked input[type=file],.pure-form-stacked input[type=month],.pure-form-stacked input[type=number],.pure-form-stacked input[type=password],.pure-form-stacked input[type=search],.pure-form-stacked input[type=tel],.pure-form-stacked input[type=text],.pure-form-stacked input[type=time],.pure-form-stacked input[type=url],.pure-form-stacked input[type=week],.pure-form-stacked label,.pure-form-stacked select,.pure-form-stacked textarea{display:block;margin:.25em 0}.pure-form-stacked input:not([type]){display:block;margin:.25em 0}.pure-form-aligned input,.pure-form-aligned select,.pure-form-aligned textarea,.pure-form-message-inline{display:inline-block;vertical-align:middle}.pure-form-aligned textarea{vertical-align:top}.pure-form-aligned .pure-control-group{margin-bottom:.5em}.pure-form-aligned .pure-control-group label{text-align:right;display:inline-block;vertical-align:middle;width:10em;margin:0 1em 0 0}.pure-form-aligned .pure-controls{margin:1.5em 0 0 11em}.pure-form .pure-input-rounded,.pure-form input.pure-input-rounded{border-radius:2em;padding:.5em 1em}.pure-form .pure-group fieldset{margin-bottom:10px}.pure-form .pure-group input,.pure-form .pure-group textarea{display:block;padding:10px;margin:0 0 -1px;border-radius:0;position:relative;top:-1px}.pure-form .pure-group input:focus,.pure-form .pure-group textarea:focus{z-index:3}.pure-form .pure-group input:first-child,.pure-form .pure-group textarea:first-child{top:1px;border-radius:4px 4px 0 0;margin:0}.pure-form .pure-group input:first-child:last-child,.pure-form .pure-group textarea:first-child:last-child{top:1px;border-radius:4px;margin:0}.pure-form .pure-group input:last-child,.pure-form .pure-group textarea:last-child{top:-2px;border-radius:0 0 4px 4px;margin:0}.pure-form .pure-group button{margin:.35em 0}.pure-form .pure-input-1{width:100%}.pure-form .pure-input-3-4{width:75%}.pure-form .pure-input-2-3{width:66%}.pure-form .pure-input-1-2{width:50%}.pure-form .pure-input-1-3{width:33%}.pure-form .pure-input-1-4{width:25%}.pure-form-message-inline{display:inline-block;padding-left:.3em;color:#666;vertical-align:middle;font-size:.875em}.pure-form-message{display:block;color:#666;font-size:.875em}@media only screen and (max-width :480px){.pure-form button[type=submit]{margin:.7em 0 0}.pure-form input:not([type]),.pure-form input[type=color],.pure-form input[type=date],.pure-form input[type=datetime-local],.pure-form input[type=datetime],.pure-form input[type=email],.pure-form input[type=month],.pure-form input[type=number],.pure-form input[type=password],.pure-form input[type=search],.pure-form input[type=tel],.pure-form input[type=text],.pure-form input[type=time],.pure-form input[type=url],.pure-form input[type=week],.pure-form label{margin-bottom:.3em;display:block}.pure-group input:not([type]),.pure-group input[type=color],.pure-group input[type=date],.pure-group input[type=datetime-local],.pure-group input[type=datetime],.pure-group input[type=email],.pure-group input[type=month],.pure-group input[type=number],.pure-group input[type=password],.pure-group input[type=search],.pure-group input[type=tel],.pure-group input[type=text],.pure-group input[type=time],.pure-group input[type=url],.pure-group input[type=week]{margin-bottom:0}.pure-form-aligned .pure-control-group label{margin-bottom:.3em;text-align:left;display:block;width:100%}.pure-form-aligned .pure-controls{margin:1.5em 0 0 0}.pure-form-message,.pure-form-message-inline{display:block;font-size:.75em;padding:.2em 0 .8em}}.pure-menu{box-sizing:border-box}.pure-menu-fixed{position:fixed;left:0;top:0;z-index:3}.pure-menu-item,.pure-menu-list{position:relative}.pure-menu-list{list-style:none;margin:0;padding:0}.pure-menu-item{padding:0;margin:0;height:100%}.pure-menu-heading,.pure-menu-link{display:block;text-decoration:none;white-space:nowrap}.pure-menu-horizontal{width:100%;white-space:nowrap}.pure-menu-horizontal .pure-menu-list{display:inline-block}.pure-menu-horizontal .pure-menu-heading,.pure-menu-horizontal .pure-menu-item,.pure-menu-horizontal .pure-menu-separator{display:inline-block;vertical-align:middle}.pure-menu-item .pure-menu-item{display:block}.pure-menu-children{display:none;position:absolute;left:100%;top:0;margin:0;padding:0;z-index:3}.pure-menu-horizontal .pure-menu-children{left:0;top:auto;width:inherit}.pure-menu-active>.pure-menu-children,.pure-menu-allow-hover:hover>.pure-menu-children{display:block;position:absolute}.pure-menu-has-children>.pure-menu-link:after{padding-left:.5em;content:"\25B8";font-size:small}.pure-menu-horizontal .pure-menu-has-children>.pure-menu-link:after{content:"\25BE"}.pure-menu-scrollable{overflow-y:scroll;overflow-x:hidden}.pure-menu-scrollable .pure-menu-list{display:block}.pure-menu-horizontal.pure-menu-scrollable .pure-menu-list{display:inline-block}.pure-menu-horizontal.pure-menu-scrollable{white-space:nowrap;overflow-y:hidden;overflow-x:auto;padding:.5em 0}.pure-menu-horizontal .pure-menu-children .pure-menu-separator,.pure-menu-separator{background-color:#ccc;height:1px;margin:.3em 0}.pure-menu-horizontal .pure-menu-separator{width:1px;height:1.3em;margin:0 .3em}.pure-menu-horizontal .pure-menu-children .pure-menu-separator{display:block;width:auto}.pure-menu-heading{text-transform:uppercase;color:#565d64}.pure-menu-link{color:#777}.pure-menu-children{background-color:#fff}.pure-menu-heading,.pure-menu-link{padding:.5em 1em}.pure-menu-disabled{opacity:.5}.pure-menu-disabled .pure-menu-link:hover{background-color:transparent;cursor:default}.pure-menu-active>.pure-menu-link,.pure-menu-link:focus,.pure-menu-link:hover{background-color:#eee}.pure-menu-selected>.pure-menu-link,.pure-menu-selected>.pure-menu-link:visited{color:#000}.pure-table{border-collapse:collapse;border-spacing:0;empty-cells:show;border:1px solid #cbcbcb}.pure-table caption{color:#000;font:italic 85%/1 arial,sans-serif;padding:1em 0;text-align:center}.pure-table td,.pure-table th{border-left:1px solid #cbcbcb;border-width:0 0 0 1px;font-size:inherit;margin:0;overflow:visible;padding:.5em 1em}.pure-table thead{background-color:#e0e0e0;color:#000;text-align:left;vertical-align:bottom}.pure-table td{background-color:transparent}.pure-table-odd td{background-color:#f2f2f2}.pure-table-striped tr:nth-child(2n-1) td{background-color:#f2f2f2}.pure-table-bordered td{border-bottom:1px solid #cbcbcb}.pure-table-bordered tbody>tr:last-child>td{border-bottom-width:0}.pure-table-horizontal td,.pure-table-horizontal th{border-width:0 0 1px 0;border-bottom:1px solid #cbcbcb}.pure-table-horizontal tbody>tr:last-child>td{border-bottom-width:0}
--------------------------------------------------------------------------------