diff --git a/server/.idea/dataSources.xml b/server/.idea/dataSources.xml index 9f38ede..776c738 100644 --- a/server/.idea/dataSources.xml +++ b/server/.idea/dataSources.xml @@ -1,7 +1,7 @@ - + sqlite.xerial true org.sqlite.JDBC diff --git a/server/.idea/sqldialects.xml b/server/.idea/sqldialects.xml index a5d08e4..8a4b714 100644 --- a/server/.idea/sqldialects.xml +++ b/server/.idea/sqldialects.xml @@ -1,7 +1,11 @@ - + + + + + \ No newline at end of file diff --git a/server/api/apierr/enums.go b/server/api/apierr/enums.go index 9180476..8401c66 100644 --- a/server/api/apierr/enums.go +++ b/server/api/apierr/enums.go @@ -5,11 +5,15 @@ type APIError int const ( NO_ERROR APIError = 0000 - MISSING_UID APIError = 1101 - MISSING_TOK APIError = 1102 - MISSING_TITLE APIError = 1103 - INVALID_PRIO APIError = 1104 - REQ_METHOD APIError = 1105 + MISSING_UID APIError = 1101 + MISSING_TOK APIError = 1102 + MISSING_TITLE APIError = 1103 + INVALID_PRIO APIError = 1104 + REQ_METHOD APIError = 1105 + INVALID_CLIENTTYPE APIError = 1106 + MISSING_QUERY_PARAM APIError = 1151 + MISSING_BODY_PARAM APIError = 1152 + MISSING_URI_PARAM APIError = 1153 NO_TITLE APIError = 1201 TITLE_TOO_LONG APIError = 1202 @@ -24,6 +28,12 @@ const ( QUOTA_REACHED APIError = 2101 + FAILED_VERIFY_PRO_TOKEN APIError = 3001 + INVALID_PRO_TOKEN APIError = 3002 + + COMMIT_FAILED = 9001 + DATABASE_ERROR = 9002 + FIREBASE_COM_FAILED APIError = 9901 FIREBASE_COM_ERRORED APIError = 9902 INTERNAL_EXCEPTION APIError = 9903 diff --git a/server/api/handler/api.go b/server/api/handler/api.go new file mode 100644 index 0000000..12f03cc --- /dev/null +++ b/server/api/handler/api.go @@ -0,0 +1,167 @@ +package handler + +import ( + "blackforestbytes.com/simplecloudnotifier/api/apierr" + "blackforestbytes.com/simplecloudnotifier/api/models" + "blackforestbytes.com/simplecloudnotifier/common/ginresp" + "blackforestbytes.com/simplecloudnotifier/logic" + "github.com/gin-gonic/gin" + "net/http" +) + +type APIHandler struct { + app *logic.Application +} + +// CreateUser swaggerdoc +// +// @Summary Create a new user +// @ID api-user-create +// +// @Param post_body body handler.CreateUser.body false " " +// +// @Success 200 {object} models.UserJSON +// @Failure 400 {object} ginresp.apiError +// @Failure 500 {object} ginresp.apiError +// +// @Router /api-v2/user/ [POST] +func (h APIHandler) CreateUser(g *gin.Context) ginresp.HTTPResponse { + type body struct { + FCMToken string `form:"fcm_token"` + ProToken *string `form:"pro_token"` + Username *string `form:"username"` + AgentModel string `form:"agent_model"` + AgentVersion string `form:"agent_version"` + ClientType string `form:"client_type"` + } + + ctx := h.app.StartRequest(g) + defer ctx.Cancel() + + var b body + if err := g.ShouldBindJSON(&b); err != nil { + return ginresp.InternAPIError(apierr.MISSING_BODY_PARAM, "Failed to read body", err) + } + + var clientType models.ClientType + if b.ClientType == string(models.ClientTypeAndroid) { + clientType = models.ClientTypeAndroid + } else if b.ClientType == string(models.ClientTypeIOS) { + clientType = models.ClientTypeIOS + } else { + return ginresp.InternAPIError(apierr.INVALID_CLIENTTYPE, "Invalid ClientType", nil) + } + + if b.ProToken != nil { + ptok, err := h.app.VerifyProToken(*b.ProToken) + if err != nil { + return ginresp.InternAPIError(apierr.FAILED_VERIFY_PRO_TOKEN, "Failed to query purchase status", err) + } + + if !ptok { + return ginresp.InternAPIError(apierr.INVALID_PRO_TOKEN, "Purchase token could not be verified", nil) + } + } + + readKey := h.app.GenerateRandomAuthKey() + sendKey := h.app.GenerateRandomAuthKey() + adminKey := h.app.GenerateRandomAuthKey() + + err := h.app.Database.ClearFCMTokens(ctx, b.FCMToken) + if err != nil { + return ginresp.InternAPIError(apierr.DATABASE_ERROR, "Failed to clear existing fcm tokens", err) + } + + if b.ProToken != nil { + err := h.app.Database.ClearProTokens(ctx, b.FCMToken) + if err != nil { + return ginresp.InternAPIError(apierr.DATABASE_ERROR, "Failed to clear existing fcm tokens", err) + } + } + + userobj, err := h.app.Database.CreateUser(ctx, readKey, sendKey, adminKey, b.ProToken, b.Username) + if err != nil { + return ginresp.InternAPIError(apierr.DATABASE_ERROR, "Failed to create user in db", err) + } + + _, err = h.app.Database.CreateClient(ctx, userobj.UserID, clientType, b.FCMToken, b.AgentModel, b.AgentVersion) + if err != nil { + return ginresp.InternAPIError(apierr.DATABASE_ERROR, "Failed to create user in db", err) + } + + return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, userobj.JSON())) +} + +func (h APIHandler) GetUser(g *gin.Context) ginresp.HTTPResponse { + return ginresp.NotImplemented() +} + +func (h APIHandler) UpdateUser(g *gin.Context) ginresp.HTTPResponse { + return ginresp.NotImplemented() +} + +func (h APIHandler) ListClients(g *gin.Context) ginresp.HTTPResponse { + return ginresp.NotImplemented() +} + +func (h APIHandler) GetClient(g *gin.Context) ginresp.HTTPResponse { + return ginresp.NotImplemented() +} + +func (h APIHandler) AddClient(g *gin.Context) ginresp.HTTPResponse { + return ginresp.NotImplemented() +} + +func (h APIHandler) DeleteClient(g *gin.Context) ginresp.HTTPResponse { + return ginresp.NotImplemented() +} + +func (h APIHandler) ListChannels(g *gin.Context) ginresp.HTTPResponse { + return ginresp.NotImplemented() +} + +func (h APIHandler) GetChannel(g *gin.Context) ginresp.HTTPResponse { + return ginresp.NotImplemented() +} + +func (h APIHandler) GetChannelMessages(g *gin.Context) ginresp.HTTPResponse { + return ginresp.NotImplemented() +} + +func (h APIHandler) ListUserSubscriptions(g *gin.Context) ginresp.HTTPResponse { + return ginresp.NotImplemented() +} + +func (h APIHandler) ListChannelSubscriptions(g *gin.Context) ginresp.HTTPResponse { + return ginresp.NotImplemented() +} + +func (h APIHandler) GetSubscription(g *gin.Context) ginresp.HTTPResponse { + return ginresp.NotImplemented() +} + +func (h APIHandler) CancelSubscription(g *gin.Context) ginresp.HTTPResponse { + return ginresp.NotImplemented() +} + +func (h APIHandler) CreateSubscription(g *gin.Context) ginresp.HTTPResponse { + return ginresp.NotImplemented() +} + +func (h APIHandler) ListMessages(g *gin.Context) ginresp.HTTPResponse { + return ginresp.NotImplemented() +} + +func (h APIHandler) GetMessage(g *gin.Context) ginresp.HTTPResponse { + return ginresp.NotImplemented() +} + +func (h APIHandler) DeleteMessage(g *gin.Context) ginresp.HTTPResponse { + return ginresp.NotImplemented() +} + +func NewAPIHandler(app *logic.Application) APIHandler { + return APIHandler{ + app: app, + } +} diff --git a/server/api/handler/common.go b/server/api/handler/common.go index 9160c69..7e68130 100644 --- a/server/api/handler/common.go +++ b/server/api/handler/common.go @@ -34,7 +34,7 @@ type pingResponseInfo struct { // Ping swaggerdoc // // @Success 200 {object} pingResponse -// @Failure 500 {object} ginresp.errBody +// @Failure 500 {object} ginresp.apiError // @Router /ping [get] // @Router /ping [post] // @Router /ping [put] @@ -60,7 +60,7 @@ func (h CommonHandler) Ping(g *gin.Context) ginresp.HTTPResponse { // DatabaseTest swaggerdoc // // @Success 200 {object} handler.DatabaseTest.response -// @Failure 500 {object} ginresp.errBody +// @Failure 500 {object} ginresp.apiError // @Router /db-test [get] func (h CommonHandler) DatabaseTest(g *gin.Context) ginresp.HTTPResponse { type response struct { @@ -88,7 +88,7 @@ func (h CommonHandler) DatabaseTest(g *gin.Context) ginresp.HTTPResponse { // Health swaggerdoc // // @Success 200 {object} handler.Health.response -// @Failure 500 {object} ginresp.errBody +// @Failure 500 {object} ginresp.apiError // @Router /health [get] func (h CommonHandler) Health(*gin.Context) ginresp.HTTPResponse { type response struct { @@ -96,3 +96,14 @@ func (h CommonHandler) Health(*gin.Context) ginresp.HTTPResponse { } return ginresp.JSON(http.StatusOK, response{Status: "ok"}) } + +func (h CommonHandler) NoRoute(g *gin.Context) ginresp.HTTPResponse { + return ginresp.JSON(http.StatusNotFound, gin.H{ + "FullPath": g.FullPath(), + "Method": g.Request.Method, + "URL": g.Request.URL.String(), + "RequestURI": g.Request.RequestURI, + "Proto": g.Request.Proto, + "Header": g.Request.Header, + }) +} diff --git a/server/api/handler/compat.go b/server/api/handler/compat.go index 9bee99a..026db23 100644 --- a/server/api/handler/compat.go +++ b/server/api/handler/compat.go @@ -1,17 +1,10 @@ package handler import ( - "blackforestbytes.com/simplecloudnotifier/api/apierr" "blackforestbytes.com/simplecloudnotifier/api/models" "blackforestbytes.com/simplecloudnotifier/common/ginresp" "blackforestbytes.com/simplecloudnotifier/logic" - "context" - "database/sql" - "fmt" "github.com/gin-gonic/gin" - "net/http" - "strconv" - "time" ) type CompatHandler struct { @@ -32,8 +25,8 @@ func NewCompatHandler(app *logic.Application) CompatHandler { // @Param pro query string true "if the user is a paid account" Enums(true, false) // @Param pro_token query string true "the (android) IAP token" // @Success 200 {object} handler.Register.response -// @Failure 500 {object} ginresp.internAPIError -// @Router /register.php [get] +// @Failure 500 {object} ginresp.apiError +// @Router /api/register.php [get] func (h CompatHandler) Register(g *gin.Context) ginresp.HTTPResponse { type query struct { FCMToken *string `form:"fcm_token"` @@ -50,81 +43,9 @@ func (h CompatHandler) Register(g *gin.Context) ginresp.HTTPResponse { IsPro int `json:"is_pro"` } - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() + //TODO - var q query - if err := g.ShouldBindQuery(&q); err != nil { - return ginresp.InternAPIError(0, "Failed to read arguments") - } - - if q.FCMToken == nil { - return ginresp.InternAPIError(0, "Missing parameter [[fcm_token]]") - } - if q.Pro == nil { - return ginresp.InternAPIError(0, "Missing parameter [[pro]]") - } - if q.ProToken == nil { - return ginresp.InternAPIError(0, "Missing parameter [[pro_token]]") - } - - isProInt := 0 - isProBool := false - if *q.Pro == "true" { - isProInt = 1 - isProBool = true - } else { - q.ProToken = nil - } - - if isProBool { - ptok, err := h.app.VerifyProToken(*q.ProToken) - if err != nil { - return ginresp.InternAPIError(0, fmt.Sprintf("Failed to query purchaste status: %v", err)) - } - - if !ptok { - return ginresp.InternAPIError(0, "Purchase token could not be verified") - } - } - - userKey := h.app.GenerateRandomAuthKey() - - return h.app.RunTransaction(ctx, nil, func(tx *sql.Tx) (ginresp.HTTPResponse, bool) { - - res, err := tx.ExecContext(ctx, "INSERT INTO users (user_key, fcm_token, is_pro, pro_token, timestamp_accessed) VALUES (?, ?, ?, ?, NOW())", userKey, *q.FCMToken, isProInt, q.ProToken) - if err != nil { - return ginresp.InternAPIError(0, fmt.Sprintf("Failed to create user: %v", err)), false - } - - userId, err := res.LastInsertId() - if err != nil { - return ginresp.InternAPIError(0, fmt.Sprintf("Failed to get user_id: %v", err)), false - } - - _, err = tx.ExecContext(ctx, "UPDATE users SET fcm_token=NULL WHERE user_id <> ? AND fcm_token=?", userId, q.FCMToken) - if err != nil { - return ginresp.InternAPIError(0, fmt.Sprintf("Failed to update fcm: %v", err)), false - } - - if isProInt == 1 { - _, err := tx.ExecContext(ctx, "UPDATE users SET is_pro=0, pro_token=NULL WHERE user_id <> ? AND pro_token = ?", userId, q.ProToken) - if err != nil { - return ginresp.InternAPIError(0, fmt.Sprintf("Failed to update ispro: %v", err)), false - } - } - - return ginresp.JSON(http.StatusOK, response{ - Success: true, - Message: "New user registered", - UserID: strconv.FormatInt(userId, 10), - UserKey: userKey, - QuotaUsed: 0, - QuotaMax: h.app.QuotaMax(isProBool), - IsPro: isProInt, - }), true - - }) + return ginresp.NotImplemented() } // Info swaggerdoc @@ -134,8 +55,8 @@ func (h CompatHandler) Register(g *gin.Context) ginresp.HTTPResponse { // @Param user_id query string true "the user_id" // @Param user_key query string true "the user_key" // @Success 200 {object} handler.Info.response -// @Failure 500 {object} ginresp.internAPIError -// @Router /info.php [get] +// @Failure 500 {object} ginresp.apiError +// @Router /api/info.php [get] func (h CompatHandler) Info(g *gin.Context) ginresp.HTTPResponse { type query struct { UserID string `form:"user_id"` @@ -155,7 +76,7 @@ func (h CompatHandler) Info(g *gin.Context) ginresp.HTTPResponse { //TODO - return ginresp.InternAPIError(0, "NotImplemented") + return ginresp.NotImplemented() } // Ack swaggerdoc @@ -166,8 +87,8 @@ func (h CompatHandler) Info(g *gin.Context) ginresp.HTTPResponse { // @Param user_key query string true "the user_key" // @Param scn_msg_id query string true "the message id" // @Success 200 {object} handler.Ack.response -// @Failure 500 {object} ginresp.internAPIError -// @Router /ack.php [get] +// @Failure 500 {object} ginresp.apiError +// @Router /api/ack.php [get] func (h CompatHandler) Ack(g *gin.Context) ginresp.HTTPResponse { type query struct { UserID string `form:"user_id"` @@ -183,7 +104,7 @@ func (h CompatHandler) Ack(g *gin.Context) ginresp.HTTPResponse { //TODO - return ginresp.InternAPIError(0, "NotImplemented") + return ginresp.NotImplemented() } // Requery swaggerdoc @@ -193,8 +114,8 @@ func (h CompatHandler) Ack(g *gin.Context) ginresp.HTTPResponse { // @Param user_id query string true "the user_id" // @Param user_key query string true "the user_key" // @Success 200 {object} handler.Requery.response -// @Failure 500 {object} ginresp.internAPIError -// @Router /requery.php [get] +// @Failure 500 {object} ginresp.apiError +// @Router /api/requery.php [get] func (h CompatHandler) Requery(g *gin.Context) ginresp.HTTPResponse { type query struct { UserID string `form:"user_id"` @@ -209,7 +130,7 @@ func (h CompatHandler) Requery(g *gin.Context) ginresp.HTTPResponse { //TODO - return ginresp.InternAPIError(0, "NotImplemented") + return ginresp.NotImplemented() } // Update swaggerdoc @@ -220,8 +141,8 @@ func (h CompatHandler) Requery(g *gin.Context) ginresp.HTTPResponse { // @Param user_key query string true "the user_key" // @Param fcm_token query string true "the (android) fcm token" // @Success 200 {object} handler.Update.response -// @Failure 500 {object} ginresp.internAPIError -// @Router /update.php [get] +// @Failure 500 {object} ginresp.apiError +// @Router /api/update.php [get] func (h CompatHandler) Update(g *gin.Context) ginresp.HTTPResponse { type query struct { UserID string `form:"user_id"` @@ -240,7 +161,7 @@ func (h CompatHandler) Update(g *gin.Context) ginresp.HTTPResponse { //TODO - return ginresp.InternAPIError(0, "NotImplemented") + return ginresp.NotImplemented() } // Expand swaggerdoc @@ -248,8 +169,8 @@ func (h CompatHandler) Update(g *gin.Context) ginresp.HTTPResponse { // @Summary Get a whole (potentially truncated) message // @ID compat-expand // @Success 200 {object} handler.Expand.response -// @Failure 500 {object} ginresp.internAPIError -// @Router /expand.php [get] +// @Failure 500 {object} ginresp.apiError +// @Router /api/expand.php [get] func (h CompatHandler) Expand(g *gin.Context) ginresp.HTTPResponse { type query struct { UserID string `form:"user_id"` @@ -264,7 +185,7 @@ func (h CompatHandler) Expand(g *gin.Context) ginresp.HTTPResponse { //TODO - return ginresp.InternAPIError(0, "NotImplemented") + return ginresp.NotImplemented() } // Upgrade swaggerdoc @@ -276,8 +197,8 @@ func (h CompatHandler) Expand(g *gin.Context) ginresp.HTTPResponse { // @Param pro query string true "if the user is a paid account" Enums(true, false) // @Param pro_token query string true "the (android) IAP token" // @Success 200 {object} handler.Upgrade.response -// @Failure 500 {object} ginresp.internAPIError -// @Router /upgrade.php [get] +// @Failure 500 {object} ginresp.apiError +// @Router /api/upgrade.php [get] func (h CompatHandler) Upgrade(g *gin.Context) ginresp.HTTPResponse { type query struct { UserID string `form:"user_id"` @@ -293,47 +214,5 @@ func (h CompatHandler) Upgrade(g *gin.Context) ginresp.HTTPResponse { //TODO - return ginresp.InternAPIError(0, "NotImplemented") -} - -// Send swaggerdoc -// -// @Summary Send a message -// @Description (all arguments can either be supplied in the query or in the json body) -// @ID compat-send -// @Accept json -// @Produce json -// @Param _ query handler.Send.query false " " -// @Param post_body body handler.Send.body false " " -// @Success 200 {object} handler.Send.response -// @Failure 500 {object} ginresp.sendAPIError -// @Router /send.php [post] -func (h CompatHandler) Send(g *gin.Context) ginresp.HTTPResponse { - type query struct { - UserID string `form:"user_id" required:"true"` - UserKey string `form:"user_key" required:"true"` - Title string `form:"title" required:"true"` - Content *string `form:"content"` - Priority *string `form:"priority"` - MessageID *string `form:"msg_id"` - Timestamp *string `form:"timestamp"` - } - type body struct { - UserID string `json:"user_id" required:"true"` - UserKey string `json:"user_key" required:"true"` - Title string `json:"title" required:"true"` - Content *string `json:"content"` - Priority *string `json:"priority"` - MessageID *string `json:"msg_id"` - Timestamp *string `json:"timestamp"` - } - type response struct { - Success string `json:"success"` - Message string `json:"message"` - //TODO - } - - //TODO - - return ginresp.SendAPIError(apierr.INTERNAL_EXCEPTION, -1, "NotImplemented") + return ginresp.NotImplemented() } diff --git a/server/api/handler/message.go b/server/api/handler/message.go new file mode 100644 index 0000000..0c64fdc --- /dev/null +++ b/server/api/handler/message.go @@ -0,0 +1,21 @@ +package handler + +import ( + "blackforestbytes.com/simplecloudnotifier/common/ginresp" + "blackforestbytes.com/simplecloudnotifier/logic" + "github.com/gin-gonic/gin" +) + +type MessageHandler struct { + app *logic.Application +} + +func (h MessageHandler) SendMessage(g *gin.Context) ginresp.HTTPResponse { + return ginresp.NotImplemented() +} + +func NewMessageHandler(app *logic.Application) MessageHandler { + return MessageHandler{ + app: app, + } +} diff --git a/server/api/handler/website.go b/server/api/handler/website.go new file mode 100644 index 0000000..b93d539 --- /dev/null +++ b/server/api/handler/website.go @@ -0,0 +1,98 @@ +package handler + +import ( + "blackforestbytes.com/simplecloudnotifier/common/ginresp" + "blackforestbytes.com/simplecloudnotifier/logic" + "blackforestbytes.com/simplecloudnotifier/website" + "github.com/gin-gonic/gin" + "net/http" + "strings" +) + +type WebsiteHandler struct { + app *logic.Application +} + +func NewWebsiteHandler(app *logic.Application) WebsiteHandler { + return WebsiteHandler{ + app: app, + } +} + +func (h WebsiteHandler) Index(g *gin.Context) ginresp.HTTPResponse { + return h.serveAsset(g, "index.html") +} + +func (h WebsiteHandler) APIDocs(g *gin.Context) ginresp.HTTPResponse { + return h.serveAsset(g, "api.html") +} + +func (h WebsiteHandler) APIDocsMore(g *gin.Context) ginresp.HTTPResponse { + return h.serveAsset(g, "api_more.html") +} + +func (h WebsiteHandler) MessageSent(g *gin.Context) ginresp.HTTPResponse { + return h.serveAsset(g, "message_sent.html") +} + +func (h WebsiteHandler) FaviconIco(g *gin.Context) ginresp.HTTPResponse { + return h.serveAsset(g, "favicon.ico") +} + +func (h WebsiteHandler) FaviconPNG(g *gin.Context) ginresp.HTTPResponse { + return h.serveAsset(g, "favicon.png") +} + +func (h WebsiteHandler) Javascript(g *gin.Context) ginresp.HTTPResponse { + type uri struct { + Filename string `uri:"fn"` + } + + var u uri + if err := g.ShouldBindUri(&u); err != nil { + return ginresp.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + } + + return h.serveAsset(g, "js/"+u.Filename) +} + +func (h WebsiteHandler) CSS(g *gin.Context) ginresp.HTTPResponse { + type uri struct { + Filename string `uri:"fn"` + } + + var u uri + if err := g.ShouldBindUri(&u); err != nil { + return ginresp.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + } + + return h.serveAsset(g, "css/"+u.Filename) +} + +func (h WebsiteHandler) serveAsset(g *gin.Context, fn string) ginresp.HTTPResponse { + data, err := website.Assets.ReadFile(fn) + if err != nil { + return ginresp.Status(http.StatusNotFound) + } + + mime := "text/plain" + + lowerFN := strings.ToLower(fn) + if strings.HasSuffix(lowerFN, ".html") || strings.HasSuffix(lowerFN, ".htm") { + mime = "text/html" + } else if strings.HasSuffix(lowerFN, ".css") { + mime = "text/css" + } else if strings.HasSuffix(lowerFN, ".js") { + mime = "text/javascript" + } else if strings.HasSuffix(lowerFN, ".json") { + mime = "application/json" + } else if strings.HasSuffix(lowerFN, ".jpeg") || strings.HasSuffix(lowerFN, ".jpg") { + mime = "image/jpeg" + } else if strings.HasSuffix(lowerFN, ".png") { + mime = "image/png" + } else if strings.HasSuffix(lowerFN, ".svg") { + mime = "image/svg+xml" + } + + return ginresp.Data(http.StatusOK, mime, data) +} diff --git a/server/api/models/client.go b/server/api/models/client.go new file mode 100644 index 0000000..544a745 --- /dev/null +++ b/server/api/models/client.go @@ -0,0 +1,23 @@ +package models + +import "time" + +type ClientType string + +const ( + ClientTypeAndroid ClientType = "ANDROID" + ClientTypeIOS ClientType = "IOS" +) + +type Client struct { + ClientID int64 + UserID int64 + Type ClientType + FCMToken *string + TimestampCreated time.Time + AgentModel string + AgentVersion string +} + +type ClientJSON struct { +} diff --git a/server/api/models/user.go b/server/api/models/user.go new file mode 100644 index 0000000..cdb8919 --- /dev/null +++ b/server/api/models/user.go @@ -0,0 +1,51 @@ +package models + +import "time" + +type User struct { + UserID int64 + Username *string + ReadKey string + SendKey string + AdminKey string + TimestampCreated time.Time + TimestampLastRead *time.Time + TimestampLastSent *time.Time + MessagesSent int + QuotaToday int + QuotaDay *string + IsPro bool + ProToken *string +} + +func (u User) JSON() UserJSON { + return UserJSON{ + UserID: u.UserID, + Username: u.Username, + ReadKey: u.ReadKey, + SendKey: u.SendKey, + AdminKey: u.AdminKey, + TimestampCreated: u.TimestampCreated.Format(time.RFC3339Nano), + TimestampLastRead: timeOptFmt(u.TimestampLastRead, time.RFC3339Nano), + TimestampLastSent: timeOptFmt(u.TimestampLastSent, time.RFC3339Nano), + MessagesSent: u.MessagesSent, + QuotaToday: u.QuotaToday, + QuotaDay: u.QuotaDay, + IsPro: u.IsPro, + } +} + +type UserJSON struct { + UserID int64 `json:"user_id"` + Username *string `json:"username"` + ReadKey string `json:"read_key"` + SendKey string `json:"send_key"` + AdminKey string `json:"admin_key"` + TimestampCreated string `json:"timestamp_created"` + TimestampLastRead *string `json:"timestamp_last_read"` + TimestampLastSent *string `json:"timestamp_last_sent"` + MessagesSent int `json:"messages_sent"` + QuotaToday int `json:"quota_today"` + QuotaDay *string `json:"quota_day"` + IsPro bool `json:"is_pro"` +} diff --git a/server/api/models/utils.go b/server/api/models/utils.go new file mode 100644 index 0000000..6e2d0f4 --- /dev/null +++ b/server/api/models/utils.go @@ -0,0 +1,14 @@ +package models + +import ( + "gogs.mikescher.com/BlackForestBytes/goext/langext" + "time" +) + +func timeOptFmt(t *time.Time, fmt string) *string { + if t == nil { + return nil + } else { + return langext.Ptr(t.Format(fmt)) + } +} diff --git a/server/api/router.go b/server/api/router.go index 2985e6f..a0e79d9 100644 --- a/server/api/router.go +++ b/server/api/router.go @@ -12,16 +12,22 @@ import ( type Router struct { app *logic.Application - commonHandler handler.CommonHandler - compatHandler handler.CompatHandler + commonHandler handler.CommonHandler + compatHandler handler.CompatHandler + websiteHandler handler.WebsiteHandler + apiHandler handler.APIHandler + messageHandler handler.MessageHandler } func NewRouter(app *logic.Application) *Router { return &Router{ app: app, - commonHandler: handler.NewCommonHandler(app), - compatHandler: handler.NewCompatHandler(app), + commonHandler: handler.NewCommonHandler(app), + compatHandler: handler.NewCompatHandler(app), + websiteHandler: handler.NewWebsiteHandler(app), + apiHandler: handler.NewAPIHandler(app), + messageHandler: handler.NewMessageHandler(app), } } @@ -30,23 +36,101 @@ func NewRouter(app *logic.Application) *Router { // @version 2.0 // @description API for SCN // @host scn.blackforestbytes.com -// @BasePath /api/ +// @BasePath / func (r *Router) Init(e *gin.Engine) { - e.Any("/ping", ginresp.Wrap(r.commonHandler.Ping)) - e.POST("/db-test", ginresp.Wrap(r.commonHandler.DatabaseTest)) - e.GET("/health", ginresp.Wrap(r.commonHandler.Health)) + // ================ General ================ - e.GET("documentation/swagger", ginext.RedirectTemporary("/documentation/swagger/")) - e.GET("documentation/swagger/", ginresp.Wrap(swagger.Handle)) - e.GET("documentation/swagger/:fn", ginresp.Wrap(swagger.Handle)) + e.Any("/api/common/ping", ginresp.Wrap(r.commonHandler.Ping)) + e.POST("/api/common/db-test", ginresp.Wrap(r.commonHandler.DatabaseTest)) + e.GET("/api/common/health", ginresp.Wrap(r.commonHandler.Health)) + + // ================ Swagger ================ + + e.GET("/documentation/swagger", ginext.RedirectTemporary("/documentation/swagger/")) + e.GET("/documentation/swagger/", ginresp.Wrap(swagger.Handle)) + e.GET("/documentation/swagger/:fn", ginresp.Wrap(swagger.Handle)) + + // ================ Website ================ + + e.GET("/", ginresp.Wrap(r.websiteHandler.Index)) + e.GET("/index.php", ginresp.Wrap(r.websiteHandler.Index)) + e.GET("/index.html", ginresp.Wrap(r.websiteHandler.Index)) + e.GET("/index", ginresp.Wrap(r.websiteHandler.Index)) + + e.GET("/api", ginresp.Wrap(r.websiteHandler.APIDocs)) + e.GET("/api.php", ginresp.Wrap(r.websiteHandler.APIDocs)) + e.GET("/api.html", ginresp.Wrap(r.websiteHandler.APIDocs)) + + e.GET("/api_more", ginresp.Wrap(r.websiteHandler.APIDocsMore)) + e.GET("/api_more.php", ginresp.Wrap(r.websiteHandler.APIDocsMore)) + e.GET("/api_more.html", ginresp.Wrap(r.websiteHandler.APIDocsMore)) + + e.GET("/message_sent", ginresp.Wrap(r.websiteHandler.MessageSent)) + e.GET("/message_sent.php", ginresp.Wrap(r.websiteHandler.MessageSent)) + e.GET("/message_sent.html", ginresp.Wrap(r.websiteHandler.MessageSent)) + + e.GET("/favicon.ico", ginresp.Wrap(r.websiteHandler.FaviconIco)) + e.GET("/favicon.png", ginresp.Wrap(r.websiteHandler.FaviconPNG)) + + e.GET("/js/:fn", ginresp.Wrap(r.websiteHandler.Javascript)) + e.GET("/css/:fn", ginresp.Wrap(r.websiteHandler.CSS)) + + // ================ Compat (v1) ================ + + compat := e.Group("/api/") + { + compat.GET("/register.php", ginresp.Wrap(r.compatHandler.Register)) + compat.GET("/info.php", ginresp.Wrap(r.compatHandler.Info)) + compat.GET("/ack.php", ginresp.Wrap(r.compatHandler.Ack)) + compat.GET("/requery.php", ginresp.Wrap(r.compatHandler.Requery)) + compat.GET("/update.php", ginresp.Wrap(r.compatHandler.Update)) + compat.GET("/expand.php", ginresp.Wrap(r.compatHandler.Expand)) + compat.GET("/upgrade.php", ginresp.Wrap(r.compatHandler.Upgrade)) + } + + // ================ Manage API ================ + + apiv2 := e.Group("/api-v2/") + { + + apiv2.POST("/user/", ginresp.Wrap(r.apiHandler.CreateUser)) + apiv2.GET("/user/:uid", ginresp.Wrap(r.apiHandler.GetUser)) + apiv2.PATCH("/user/:uid", ginresp.Wrap(r.apiHandler.UpdateUser)) + + apiv2.GET("/user/:uid/clients", ginresp.Wrap(r.apiHandler.ListClients)) + apiv2.GET("/user/:uid/clients/:cid", ginresp.Wrap(r.apiHandler.GetClient)) + apiv2.POST("/user/:uid/clients", ginresp.Wrap(r.apiHandler.AddClient)) + apiv2.DELETE("/user/:uid/clients", ginresp.Wrap(r.apiHandler.DeleteClient)) + + apiv2.GET("/user/:uid/channels", ginresp.Wrap(r.apiHandler.ListChannels)) + apiv2.GET("/user/:uid/channels/:cid", ginresp.Wrap(r.apiHandler.GetChannel)) + apiv2.GET("/user/:uid/channels/:cid/messages", ginresp.Wrap(r.apiHandler.GetChannelMessages)) + apiv2.GET("/user/:uid/channels/:cid/subscriptions", ginresp.Wrap(r.apiHandler.ListChannelSubscriptions)) + + apiv2.GET("/user/:uid/subscriptions", ginresp.Wrap(r.apiHandler.ListUserSubscriptions)) + apiv2.GET("/user/:uid/subscriptions/:sid", ginresp.Wrap(r.apiHandler.GetSubscription)) + apiv2.DELETE("/user/:uid/subscriptions/:sid", ginresp.Wrap(r.apiHandler.CancelSubscription)) + apiv2.POST("/user/:uid/subscriptions", ginresp.Wrap(r.apiHandler.CreateSubscription)) + + apiv2.GET("/messages", ginresp.Wrap(r.apiHandler.ListMessages)) + apiv2.GET("/messages/:mid", ginresp.Wrap(r.apiHandler.GetMessage)) + apiv2.DELETE("/messages/:mid", ginresp.Wrap(r.apiHandler.DeleteMessage)) + + apiv2.POST("/messages", ginresp.Wrap(r.messageHandler.SendMessage)) + } + + // ================ Send API ================ + + sendAPI := e.Group("") + { + sendAPI.POST("/", ginresp.Wrap(r.messageHandler.SendMessage)) + sendAPI.POST("/send", ginresp.Wrap(r.messageHandler.SendMessage)) + sendAPI.POST("/send.php") + } + + if r.app.Config.ReturnRawErrors { + e.NoRoute(ginresp.Wrap(r.commonHandler.NoRoute)) + } - e.POST("/send.php", ginresp.Wrap(r.compatHandler.Send)) - e.GET("/register.php", ginresp.Wrap(r.compatHandler.Register)) - e.GET("/info.php", ginresp.Wrap(r.compatHandler.Info)) - e.GET("/ack.php", ginresp.Wrap(r.compatHandler.Ack)) - e.GET("/requery.php", ginresp.Wrap(r.compatHandler.Requery)) - e.GET("/update.php", ginresp.Wrap(r.compatHandler.Update)) - e.GET("/expand.php", ginresp.Wrap(r.compatHandler.Expand)) - e.GET("/upgrade.php", ginresp.Wrap(r.compatHandler.Upgrade)) } diff --git a/server/cmd/scnserver/main.go b/server/cmd/scnserver/main.go index 5c1287b..87d8ae7 100644 --- a/server/cmd/scnserver/main.go +++ b/server/cmd/scnserver/main.go @@ -7,7 +7,6 @@ import ( "blackforestbytes.com/simplecloudnotifier/common/ginext" "blackforestbytes.com/simplecloudnotifier/db" "blackforestbytes.com/simplecloudnotifier/logic" - "context" "fmt" "github.com/rs/zerolog/log" ) @@ -19,13 +18,17 @@ func main() { log.Info().Msg(fmt.Sprintf("Starting with config-namespace <%s>", conf.Namespace)) - sqlite, err := db.NewDatabase(context.Background(), conf) + sqlite, err := db.NewDatabase(conf) if err != nil { panic(err) } app := logic.NewApp(sqlite) + if err := app.Migrate(); err != nil { + panic(err) + } + ginengine := ginext.NewEngine(conf) router := api.NewRouter(app) diff --git a/server/common/ginresp/apiError.go b/server/common/ginresp/apiError.go index 638506b..358240d 100644 --- a/server/common/ginresp/apiError.go +++ b/server/common/ginresp/apiError.go @@ -1,14 +1,9 @@ package ginresp -type sendAPIError struct { +type apiError struct { Success bool `json:"success"` Error int `json:"error"` ErrorHighlight int `json:"errhighlight"` Message string `json:"message"` -} - -type internAPIError struct { - Success bool `json:"success"` - ErrorID int `json:"errid,omitempty"` - Message string `json:"message"` + RawError error `json:"errorObject,omitempty"` } diff --git a/server/common/ginresp/error.go b/server/common/ginresp/error.go deleted file mode 100644 index 9b2d4ff..0000000 --- a/server/common/ginresp/error.go +++ /dev/null @@ -1,6 +0,0 @@ -package ginresp - -type errBody struct { - Success bool `json:"success"` - Message string `json:"message"` -} diff --git a/server/common/ginresp/resp.go b/server/common/ginresp/resp.go index a0c368f..f1284fb 100644 --- a/server/common/ginresp/resp.go +++ b/server/common/ginresp/resp.go @@ -1,13 +1,14 @@ package ginresp import ( + scn "blackforestbytes.com/simplecloudnotifier" "blackforestbytes.com/simplecloudnotifier/api/apierr" "github.com/gin-gonic/gin" "net/http" ) type HTTPResponse interface { - Write(context *gin.Context) + Write(g *gin.Context) } type jsonHTTPResponse struct { @@ -73,13 +74,21 @@ func Text(sc int, data string) HTTPResponse { } func InternalError(e error) HTTPResponse { - return &errHTTPResponse{statusCode: http.StatusInternalServerError, data: errBody{Success: false, Message: e.Error()}} + return &errHTTPResponse{statusCode: http.StatusInternalServerError, data: apiError{Success: false, Error: int(apierr.INTERNAL_EXCEPTION), Message: e.Error()}} } -func InternAPIError(errid int, msg string) HTTPResponse { - return &errHTTPResponse{statusCode: http.StatusInternalServerError, data: internAPIError{Success: false, ErrorID: errid, Message: msg}} +func InternAPIError(errorid apierr.APIError, msg string, e error) HTTPResponse { + if scn.Conf.ReturnRawErrors { + return &errHTTPResponse{statusCode: http.StatusInternalServerError, data: apiError{Success: false, Error: int(errorid), Message: msg, RawError: e}} + } else { + return &errHTTPResponse{statusCode: http.StatusInternalServerError, data: apiError{Success: false, Error: int(errorid), Message: msg}} + } } func SendAPIError(errorid apierr.APIError, highlight int, msg string) HTTPResponse { - return &errHTTPResponse{statusCode: http.StatusInternalServerError, data: sendAPIError{Success: false, Error: int(errorid), ErrorHighlight: highlight, Message: msg}} + return &errHTTPResponse{statusCode: http.StatusInternalServerError, data: apiError{Success: false, Error: int(errorid), ErrorHighlight: highlight, Message: msg}} +} + +func NotImplemented() HTTPResponse { + return &errHTTPResponse{statusCode: http.StatusInternalServerError, data: apiError{Success: false, Error: -1, ErrorHighlight: 0, Message: "Not Implemented"}} } diff --git a/server/common/ginresp/wrapper.go b/server/common/ginresp/wrapper.go index 53838ce..32978f8 100644 --- a/server/common/ginresp/wrapper.go +++ b/server/common/ginresp/wrapper.go @@ -6,15 +6,15 @@ type WHandlerFunc func(*gin.Context) HTTPResponse func Wrap(fn WHandlerFunc) gin.HandlerFunc { - return func(context *gin.Context) { + return func(g *gin.Context) { - wrap := fn(context) + wrap := fn(g) - if context.Writer.Written() { + if g.Writer.Written() { panic("Writing in WrapperFunc is not supported") } - wrap.Write(context) + wrap.Write(g) } diff --git a/server/config.go b/server/config.go index 3e9d686..6a2ce78 100644 --- a/server/config.go +++ b/server/config.go @@ -3,48 +3,59 @@ package server import ( "github.com/rs/zerolog/log" "os" + "time" ) type Config struct { - Namespace string - GinDebug bool - ServerIP string - ServerPort string - DBFile string + Namespace string + GinDebug bool + ServerIP string + ServerPort string + DBFile string + RequestTimeout time.Duration + ReturnRawErrors bool } var Conf Config var configLoc = Config{ - Namespace: "local", - GinDebug: true, - ServerIP: "0.0.0.0", - ServerPort: "8080", - DBFile: ".run-data/db.sqlite3", + Namespace: "local", + GinDebug: true, + ServerIP: "0.0.0.0", + ServerPort: "8080", + DBFile: ".run-data/db.sqlite3", + RequestTimeout: 16 * time.Second, + ReturnRawErrors: true, } var configDev = Config{ - Namespace: "develop", - GinDebug: true, - ServerIP: "0.0.0.0", - ServerPort: "80", - DBFile: "/data/scn.sqlite3", + Namespace: "develop", + GinDebug: true, + ServerIP: "0.0.0.0", + ServerPort: "80", + DBFile: "/data/scn.sqlite3", + RequestTimeout: 16 * time.Second, + ReturnRawErrors: true, } var configStag = Config{ - Namespace: "staging", - GinDebug: true, - ServerIP: "0.0.0.0", - ServerPort: "80", - DBFile: "/data/scn.sqlite3", + Namespace: "staging", + GinDebug: true, + ServerIP: "0.0.0.0", + ServerPort: "80", + DBFile: "/data/scn.sqlite3", + RequestTimeout: 16 * time.Second, + ReturnRawErrors: true, } var configProd = Config{ - Namespace: "production", - GinDebug: false, - ServerIP: "0.0.0.0", - ServerPort: "80", - DBFile: "/data/scn.sqlite3", + Namespace: "production", + GinDebug: false, + ServerIP: "0.0.0.0", + ServerPort: "80", + DBFile: "/data/scn.sqlite3", + RequestTimeout: 16 * time.Second, + ReturnRawErrors: false, } var allConfig = []Config{ diff --git a/server/db/context.go b/server/db/context.go new file mode 100644 index 0000000..7e60b6f --- /dev/null +++ b/server/db/context.go @@ -0,0 +1,15 @@ +package db + +import ( + "database/sql" + "time" +) + +type TxContext interface { + Deadline() (deadline time.Time, ok bool) + Done() <-chan struct{} + Err() error + Value(key any) any + + GetOrCreateTransaction(db *Database) (*sql.Tx, error) +} diff --git a/server/db/database.go b/server/db/database.go index 532ae20..c3ce453 100644 --- a/server/db/database.go +++ b/server/db/database.go @@ -8,43 +8,60 @@ import ( "errors" "fmt" _ "github.com/mattn/go-sqlite3" + "time" ) -//go:embed schema_1.0.sql +//go:embed schema_1.0.ddl var schema_1_0 string -//go:embed schema_2.0.sql +//go:embed schema_2.0.ddl var schema_2_0 string -func NewDatabase(ctx context.Context, conf scn.Config) (*sql.DB, error) { +//go:embed schema_3.0.ddl +var schema_3_0 string + +type Database struct { + db *sql.DB +} + +func NewDatabase(conf scn.Config) (*Database, error) { db, err := sql.Open("sqlite3", conf.DBFile) if err != nil { return nil, err } - schema, err := getSchemaFromDB(ctx, db) + return &Database{db}, nil +} + +func (db *Database) Migrate(ctx context.Context) error { + ctx, cancel := context.WithTimeout(context.Background(), 24*time.Second) + defer cancel() + + schema, err := db.ReadSchema(ctx) if schema == 0 { - _, err = db.ExecContext(ctx, schema_1_0) + _, err = db.db.ExecContext(ctx, schema_3_0) if err != nil { - return nil, err + return err } - return db, nil + return nil } else if schema == 1 { - return nil, errors.New("cannot autom. upgrade schema 1") + return errors.New("cannot autom. upgrade schema 1") } else if schema == 2 { - return db, nil + return errors.New("cannot autom. upgrade schema 2") //TODO + } else if schema == 3 { + return nil // current } else { - return nil, errors.New(fmt.Sprintf("Unknown DB schema: %d", schema)) + return errors.New(fmt.Sprintf("Unknown DB schema: %d", schema)) } } -func getSchemaFromDB(ctx context.Context, db *sql.DB) (int, error) { +func (db *Database) ReadSchema(ctx context.Context) (int, error) { - r1, err := db.QueryContext(ctx, "SELECT name FROM sqlite_master WHERE type='table' AND name='meta'") + r1, err := db.db.QueryContext(ctx, "SELECT name FROM sqlite_master WHERE type='table' AND name='meta'") if err != nil { return 0, err } @@ -53,7 +70,7 @@ func getSchemaFromDB(ctx context.Context, db *sql.DB) (int, error) { return 0, nil } - r2, err := db.QueryContext(ctx, "SELECT value_int FROM meta WHERE key='schema'") + r2, err := db.db.QueryContext(ctx, "SELECT value_int FROM meta WHERE meta_key='schema'") if err != nil { return 0, err } @@ -69,3 +86,11 @@ func getSchemaFromDB(ctx context.Context, db *sql.DB) (int, error) { return schema, nil } + +func (db *Database) Ping() error { + return db.db.Ping() +} + +func (db *Database) BeginTx(ctx context.Context) (*sql.Tx, error) { + return db.db.BeginTx(ctx, nil) +} diff --git a/server/db/methods.go b/server/db/methods.go new file mode 100644 index 0000000..baf1a82 --- /dev/null +++ b/server/db/methods.go @@ -0,0 +1,112 @@ +package db + +import ( + "blackforestbytes.com/simplecloudnotifier/api/models" + "gogs.mikescher.com/BlackForestBytes/goext/langext" + "time" +) + +func (db *Database) CreateUser(ctx TxContext, readKey string, sendKey string, adminKey string, protoken *string, username *string) (models.User, error) { + tx, err := ctx.GetOrCreateTransaction(db) + if err != nil { + return models.User{}, err + } + + now := time.Now().UTC() + + res, err := tx.ExecContext(ctx, "INSERT INTO users (username, read_key, send_key, admin_key, is_pro, pro_token, timestamp_created) VALUES (?, ?, ?, ?, ?, ?, ?)", + username, + readKey, + sendKey, + adminKey, + bool2DB(protoken != nil), + protoken, + time2DB(now)) + if err != nil { + return models.User{}, err + } + + liid, err := res.LastInsertId() + if err != nil { + return models.User{}, err + } + + return models.User{ + UserID: liid, + Username: username, + ReadKey: readKey, + SendKey: sendKey, + AdminKey: adminKey, + TimestampCreated: now, + TimestampLastRead: nil, + TimestampLastSent: nil, + MessagesSent: 0, + QuotaToday: 0, + QuotaDay: nil, + IsPro: protoken != nil, + ProToken: protoken, + }, nil +} + +func (db *Database) CreateClient(ctx TxContext, userid int64, ctype models.ClientType, fcmToken string, agentModel string, agentVersion string) (models.Client, error) { + tx, err := ctx.GetOrCreateTransaction(db) + if err != nil { + return models.Client{}, err + } + + now := time.Now().UTC() + + res, err := tx.ExecContext(ctx, "INSERT INTO clients (user_id, type, fcm_token, timestamp_created, agent_model, agent_version) VALUES (?, ?, ?, ?, ?, ?)", + userid, + string(ctype), + fcmToken, + time2DB(now), + agentModel, + agentVersion) + if err != nil { + return models.Client{}, err + } + + liid, err := res.LastInsertId() + if err != nil { + return models.Client{}, err + } + + return models.Client{ + ClientID: liid, + UserID: userid, + Type: ctype, + FCMToken: langext.Ptr(fcmToken), + TimestampCreated: now, + AgentModel: agentModel, + AgentVersion: agentVersion, + }, nil +} + +func (db *Database) ClearFCMTokens(ctx TxContext, fcmtoken string) error { + tx, err := ctx.GetOrCreateTransaction(db) + if err != nil { + return err + } + + _, err = tx.ExecContext(ctx, "DELETE FROM clients WHERE fcm_token = ?", fcmtoken) + if err != nil { + return err + } + + return nil +} + +func (db *Database) ClearProTokens(ctx TxContext, protoken string) error { + tx, err := ctx.GetOrCreateTransaction(db) + if err != nil { + return err + } + + _, err = tx.ExecContext(ctx, "UPDATE users SET is_pro=0, pro_token=NULL WHERE pro_token = ?", protoken) + if err != nil { + return err + } + + return nil +} diff --git a/server/db/schema_1.0.sql b/server/db/schema_1.0.ddl similarity index 100% rename from server/db/schema_1.0.sql rename to server/db/schema_1.0.ddl diff --git a/server/db/schema_2.0.sql b/server/db/schema_2.0.ddl similarity index 100% rename from server/db/schema_2.0.sql rename to server/db/schema_2.0.ddl diff --git a/server/db/schema_3.0.sql b/server/db/schema_3.0.ddl similarity index 69% rename from server/db/schema_3.0.sql rename to server/db/schema_3.0.ddl index 18de924..01b7d60 100644 --- a/server/db/schema_3.0.sql +++ b/server/db/schema_3.0.ddl @@ -8,9 +8,9 @@ CREATE TABLE users send_key TEXT NOT NULL, admin_key TEXT NOT NULL, - timestamp_created TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - timestamp_lastread TEXT NULL DEFAULT NULL, - timestamp_lastsent TEXT NULL DEFAULT NULL, + timestamp_created INTEGER NOT NULL, + timestamp_lastread INTEGER NULL DEFAULT NULL, + timestamp_lastsent INTEGER NULL DEFAULT NULL, messages_sent INTEGER NOT NULL DEFAULT '0', @@ -18,24 +18,23 @@ CREATE TABLE users quota_day TEXT NULL DEFAULT NULL, is_pro INTEGER CHECK(is_pro IN (0, 1)) NOT NULL DEFAULT 0, - pro_token TEXT NULL DEFAULT NULL, - - PRIMARY KEY (user_id) + pro_token TEXT NULL DEFAULT NULL ); -CREATE UNIQUE INDEX "idx_users_protoken" ON users (pro_token); +CREATE UNIQUE INDEX "idx_users_protoken" ON users (pro_token) WHERE pro_token IS NOT NULL; CREATE TABLE clients ( - client_id INTEGER PRIMARY KEY AUTOINCREMENT, + client_id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id INTEGER NOT NULL, + user_id INTEGER NOT NULL, + type TEXT CHECK(type IN ('ANDROID', 'IOS')) NOT NULL, + fcm_token TEXT NULL, - type TEXT NOT NULL, + timestamp_created INTEGER NOT NULL, - fcm_token TEXT NULL, - - PRIMARY KEY (client_id) + agent_model TEXT NOT NULL, + agent_version TEXT NOT NULL ); CREATE INDEX "idx_clients_userid" ON clients (user_id); CREATE UNIQUE INDEX "idx_clients_fcmtoken" ON clients (fcm_token); @@ -54,11 +53,9 @@ CREATE TABLE channels messages_sent INTEGER NOT NULL DEFAULT '0', - timestamp_created TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - timestamp_lastread TEXT NULL DEFAULT NULL, - timestamp_lastsent TEXT NULL DEFAULT NULL, - - PRIMARY KEY (channel_id) + timestamp_created INTEGER NOT NULL, + timestamp_lastread INTEGER NULL DEFAULT NULL, + timestamp_lastsent INTEGER NULL DEFAULT NULL ); CREATE UNIQUE INDEX "idx_channels_identity" ON channels (owner_user_id, name); @@ -68,9 +65,7 @@ CREATE TABLE subscriptions subscriber_user_id INTEGER NOT NULL, channel_owner_user_id INTEGER NOT NULL, - channel_name TEXT NOT NULL, - - PRIMARY KEY (subscription_id) + channel_name TEXT NOT NULL ); CREATE UNIQUE INDEX "idx_subscriptions_ref" ON subscriptions (subscriber_user_id, channel_owner_user_id, channel_name); @@ -83,15 +78,13 @@ CREATE TABLE messages channel_id INTEGER NOT NULL, - timestamp_real TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - timestamp_client TEXT NULL, + timestamp_real INTEGER NOT NULL, + timestamp_client INTEGER NULL, title TEXT NOT NULL, content TEXT NULL, priority INTEGER CHECK(priority IN (0, 1, 2)) NOT NULL, - usr_message_id TEXT NULL, - - PRIMARY KEY (scn_message_id) + usr_message_id TEXT NULL ); CREATE INDEX "idx_messages_channel" ON messages (sender_user_id, channel_name); CREATE INDEX "idx_messages_idempotency" ON messages (sender_user_id, usr_message_id); @@ -105,16 +98,15 @@ CREATE TABLE deliveries receiver_user_id INTEGER NOT NULL, receiver_client_id INTEGER NOT NULL, - timestamp_created TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - timestamp_finalized TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + timestamp_created INTEGER NOT NULL, + timestamp_finalized INTEGER NOT NULL, status TEXT CHECK(status IN ('RETRY','SUCCESS','FAILED')) NOT NULL, retry_count INTEGER NOT NULL DEFAULT 0, + next_delivery INTEGER NULL DEFAULT NULL, - fcm_message_id TEXT NULL, - - PRIMARY KEY (delivery_id) + fcm_message_id TEXT NULL ); CREATE INDEX "idx_deliveries_receiver" ON deliveries (scn_message_id, receiver_client_id); diff --git a/server/db/schema_sqlite.ddl b/server/db/schema_sqlite.ddl new file mode 100644 index 0000000..5194e91 --- /dev/null +++ b/server/db/schema_sqlite.ddl @@ -0,0 +1,7 @@ +CREATE TABLE sqlite_master ( + type text, + name text, + tbl_name text, + rootpage integer, + sql text +); \ No newline at end of file diff --git a/server/db/utils.go b/server/db/utils.go new file mode 100644 index 0000000..3258f51 --- /dev/null +++ b/server/db/utils.go @@ -0,0 +1,15 @@ +package db + +import "time" + +func bool2DB(b bool) int { + if b { + return 1 + } else { + return 0 + } +} + +func time2DB(t time.Time) int64 { + return t.UnixMilli() +} diff --git a/server/logic/application.go b/server/logic/application.go index 91adbe5..d99f780 100644 --- a/server/logic/application.go +++ b/server/logic/application.go @@ -2,10 +2,8 @@ package logic import ( scn "blackforestbytes.com/simplecloudnotifier" - "blackforestbytes.com/simplecloudnotifier/common/ginresp" + "blackforestbytes.com/simplecloudnotifier/db" "context" - "database/sql" - "fmt" "github.com/gin-gonic/gin" "github.com/rs/zerolog/log" "math/rand" @@ -20,10 +18,10 @@ import ( type Application struct { Config scn.Config Gin *gin.Engine - Database *sql.DB + Database *db.Database } -func NewApp(db *sql.DB) *Application { +func NewApp(db *db.Database) *Application { return &Application{Database: db} } @@ -74,30 +72,6 @@ func (app *Application) GenerateRandomAuthKey() string { return k } -func (app *Application) RunTransaction(ctx context.Context, opt *sql.TxOptions, fn func(tx *sql.Tx) (ginresp.HTTPResponse, bool)) ginresp.HTTPResponse { - - tx, err := app.Database.BeginTx(ctx, opt) - if err != nil { - return ginresp.InternAPIError(0, fmt.Sprintf("Failed to create transaction: %v", err)) - } - - res, commit := fn(tx) - - if commit { - err = tx.Commit() - if err != nil { - return ginresp.InternAPIError(0, fmt.Sprintf("Failed to commit transaction: %v", err)) - } - } else { - err = tx.Rollback() - if err != nil { - return ginresp.InternAPIError(0, fmt.Sprintf("Failed to rollback transaction: %v", err)) - } - } - - return res -} - func (app *Application) QuotaMax(ispro bool) int { if ispro { return 1000 @@ -109,3 +83,16 @@ func (app *Application) QuotaMax(ispro bool) int { func (app *Application) VerifyProToken(token string) (bool, error) { return false, nil //TODO implement pro verification } + +func (app *Application) Migrate() error { + ctx, cancel := context.WithTimeout(context.Background(), 24*time.Second) + defer cancel() + + return app.Database.Migrate(ctx) +} + +func (app *Application) StartRequest(g *gin.Context) *AppContext { + ctx, cancel := context.WithTimeout(context.Background(), app.Config.RequestTimeout) + + return &AppContext{inner: ctx, cancelFunc: cancel} +} diff --git a/server/logic/context.go b/server/logic/context.go new file mode 100644 index 0000000..3adfea2 --- /dev/null +++ b/server/logic/context.go @@ -0,0 +1,75 @@ +package logic + +import ( + "blackforestbytes.com/simplecloudnotifier/api/apierr" + "blackforestbytes.com/simplecloudnotifier/common/ginresp" + "blackforestbytes.com/simplecloudnotifier/db" + "context" + "database/sql" + "errors" + "time" +) + +type AppContext struct { + inner context.Context + cancelFunc context.CancelFunc + cancelled bool + transaction *sql.Tx +} + +func (ac *AppContext) Deadline() (deadline time.Time, ok bool) { + return ac.inner.Deadline() +} + +func (ac *AppContext) Done() <-chan struct{} { + return ac.inner.Done() +} + +func (ac *AppContext) Err() error { + return ac.inner.Err() +} + +func (ac *AppContext) Value(key any) any { + return ac.inner.Value(key) +} + +func (ac *AppContext) Cancel() { + ac.cancelled = true + if ac.transaction != nil { + err := ac.transaction.Rollback() + if err != nil { + panic("failed to rollback transaction: " + err.Error()) + } + ac.transaction = nil + } + ac.cancelFunc() +} + +func (ac *AppContext) FinishSuccess(res ginresp.HTTPResponse) ginresp.HTTPResponse { + if ac.cancelled { + panic("Cannot finish a cancelled request") + } + if ac.transaction != nil { + err := ac.transaction.Commit() + if err != nil { + return ginresp.InternAPIError(apierr.COMMIT_FAILED, "Failed to comit changes to DB", err) + } + ac.transaction = nil + } + return res +} + +func (ac *AppContext) GetOrCreateTransaction(db *db.Database) (*sql.Tx, error) { + if ac.cancelled { + return nil, errors.New("context cancelled") + } + if ac.transaction != nil { + return ac.transaction, nil + } + tx, err := db.BeginTx(ac) + if err != nil { + return nil, err + } + ac.transaction = tx + return tx, nil +} diff --git a/server/swagger/swagger.json b/server/swagger/swagger.json index 75d4068..df83e3a 100644 --- a/server/swagger/swagger.json +++ b/server/swagger/swagger.json @@ -7,9 +7,45 @@ "version": "2.0" }, "host": "scn.blackforestbytes.com", - "basePath": "/api/", + "basePath": "/", "paths": { - "/ack.php": { + "/api-v2/user/": { + "post": { + "summary": "Create a new user", + "operationId": "api-user-create", + "parameters": [ + { + "description": " ", + "name": "post_body", + "in": "body", + "schema": { + "$ref": "#/definitions/handler.CreateUser.body" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.UserJSON" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/ginresp.apiError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/ginresp.apiError" + } + } + } + } + }, + "/api/ack.php": { "get": { "summary": "Acknowledge that a message was received", "operationId": "compat-ack", @@ -46,31 +82,13 @@ "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/ginresp.internAPIError" + "$ref": "#/definitions/ginresp.apiError" } } } } }, - "/db-test": { - "get": { - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/handler.DatabaseTest.response" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/ginresp.errBody" - } - } - } - } - }, - "/expand.php": { + "/api/expand.php": { "get": { "summary": "Get a whole (potentially truncated) message", "operationId": "compat-expand", @@ -84,31 +102,13 @@ "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/ginresp.internAPIError" + "$ref": "#/definitions/ginresp.apiError" } } } } }, - "/health": { - "get": { - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/handler.Health.response" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/ginresp.errBody" - } - } - } - } - }, - "/info.php": { + "/api/info.php": { "get": { "summary": "Get information about the current user", "operationId": "compat-info", @@ -138,95 +138,13 @@ "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/ginresp.internAPIError" + "$ref": "#/definitions/ginresp.apiError" } } } } }, - "/ping": { - "get": { - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/handler.pingResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/ginresp.errBody" - } - } - } - }, - "put": { - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/handler.pingResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/ginresp.errBody" - } - } - } - }, - "post": { - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/handler.pingResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/ginresp.errBody" - } - } - } - }, - "delete": { - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/handler.pingResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/ginresp.errBody" - } - } - } - }, - "patch": { - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/handler.pingResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/ginresp.errBody" - } - } - } - } - }, - "/register.php": { + "/api/register.php": { "get": { "summary": "Register a new account", "operationId": "compat-register", @@ -267,13 +185,13 @@ "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/ginresp.internAPIError" + "$ref": "#/definitions/ginresp.apiError" } } } } }, - "/requery.php": { + "/api/requery.php": { "get": { "summary": "Return all not-acknowledged messages", "operationId": "compat-requery", @@ -303,85 +221,13 @@ "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/ginresp.internAPIError" + "$ref": "#/definitions/ginresp.apiError" } } } } }, - "/send.php": { - "post": { - "description": "(all arguments can either be supplied in the query or in the json body)", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "summary": "Send a message", - "operationId": "compat-send", - "parameters": [ - { - "type": "string", - "name": "content", - "in": "query" - }, - { - "type": "string", - "name": "messageID", - "in": "query" - }, - { - "type": "string", - "name": "priority", - "in": "query" - }, - { - "type": "string", - "name": "timestamp", - "in": "query" - }, - { - "type": "string", - "name": "title", - "in": "query" - }, - { - "type": "string", - "name": "userID", - "in": "query" - }, - { - "type": "string", - "name": "userKey", - "in": "query" - }, - { - "description": " ", - "name": "post_body", - "in": "body", - "schema": { - "$ref": "#/definitions/handler.Send.body" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/handler.Send.response" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/ginresp.sendAPIError" - } - } - } - } - }, - "/update.php": { + "/api/update.php": { "get": { "summary": "Set the fcm-token (android)", "operationId": "compat-update", @@ -418,13 +264,13 @@ "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/ginresp.internAPIError" + "$ref": "#/definitions/ginresp.apiError" } } } } }, - "/upgrade.php": { + "/api/upgrade.php": { "get": { "summary": "Upgrade a free account to a paid account", "operationId": "compat-upgrade", @@ -472,7 +318,125 @@ "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/ginresp.internAPIError" + "$ref": "#/definitions/ginresp.apiError" + } + } + } + } + }, + "/db-test": { + "get": { + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.DatabaseTest.response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/ginresp.apiError" + } + } + } + } + }, + "/health": { + "get": { + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.Health.response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/ginresp.apiError" + } + } + } + } + }, + "/ping": { + "get": { + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.pingResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/ginresp.apiError" + } + } + } + }, + "put": { + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.pingResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/ginresp.apiError" + } + } + } + }, + "post": { + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.pingResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/ginresp.apiError" + } + } + } + }, + "delete": { + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.pingResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/ginresp.apiError" + } + } + } + }, + "patch": { + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.pingResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/ginresp.apiError" } } } @@ -480,32 +444,7 @@ } }, "definitions": { - "ginresp.errBody": { - "type": "object", - "properties": { - "message": { - "type": "string" - }, - "success": { - "type": "boolean" - } - } - }, - "ginresp.internAPIError": { - "type": "object", - "properties": { - "errid": { - "type": "integer" - }, - "message": { - "type": "string" - }, - "success": { - "type": "boolean" - } - } - }, - "ginresp.sendAPIError": { + "ginresp.apiError": { "type": "object", "properties": { "errhighlight": { @@ -514,6 +453,7 @@ "error": { "type": "integer" }, + "errorObject": {}, "message": { "type": "string" }, @@ -539,6 +479,29 @@ } } }, + "handler.CreateUser.body": { + "type": "object", + "properties": { + "agentModel": { + "type": "string" + }, + "agentVersion": { + "type": "string" + }, + "clientType": { + "type": "string" + }, + "fcmtoken": { + "type": "string" + }, + "proToken": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, "handler.DatabaseTest.response": { "type": "object", "properties": { @@ -656,43 +619,6 @@ } } }, - "handler.Send.body": { - "type": "object", - "properties": { - "content": { - "type": "string" - }, - "msg_id": { - "type": "string" - }, - "priority": { - "type": "string" - }, - "timestamp": { - "type": "string" - }, - "title": { - "type": "string" - }, - "user_id": { - "type": "string" - }, - "user_key": { - "type": "string" - } - } - }, - "handler.Send.response": { - "type": "object", - "properties": { - "message": { - "type": "string" - }, - "success": { - "type": "string" - } - } - }, "handler.Update.response": { "type": "object", "properties": { @@ -818,6 +744,47 @@ "type": "string" } } + }, + "models.UserJSON": { + "type": "object", + "properties": { + "admin_key": { + "type": "string" + }, + "is_pro": { + "type": "boolean" + }, + "messages_sent": { + "type": "integer" + }, + "quota_day": { + "type": "string" + }, + "quota_today": { + "type": "integer" + }, + "read_key": { + "type": "string" + }, + "send_key": { + "type": "string" + }, + "timestamp_created": { + "type": "string" + }, + "timestamp_last_read": { + "type": "string" + }, + "timestamp_last_sent": { + "type": "string" + }, + "user_id": { + "type": "integer" + }, + "username": { + "type": "string" + } + } } } } \ No newline at end of file diff --git a/server/swagger/swagger.yaml b/server/swagger/swagger.yaml index 7f9a6ce..bd20c7b 100644 --- a/server/swagger/swagger.yaml +++ b/server/swagger/swagger.yaml @@ -1,27 +1,12 @@ -basePath: /api/ +basePath: / definitions: - ginresp.errBody: - properties: - message: - type: string - success: - type: boolean - type: object - ginresp.internAPIError: - properties: - errid: - type: integer - message: - type: string - success: - type: boolean - type: object - ginresp.sendAPIError: + ginresp.apiError: properties: errhighlight: type: integer error: type: integer + errorObject: {} message: type: string success: @@ -38,6 +23,21 @@ definitions: success: type: string type: object + handler.CreateUser.body: + properties: + agentModel: + type: string + agentVersion: + type: string + clientType: + type: string + fcmtoken: + type: string + proToken: + type: string + username: + type: string + type: object handler.DatabaseTest.response: properties: libVersion: @@ -114,30 +114,6 @@ definitions: success: type: string type: object - handler.Send.body: - properties: - content: - type: string - msg_id: - type: string - priority: - type: string - timestamp: - type: string - title: - type: string - user_id: - type: string - user_key: - type: string - type: object - handler.Send.response: - properties: - message: - type: string - success: - type: string - type: object handler.Update.response: properties: is_pro: @@ -220,6 +196,33 @@ definitions: usr_msg_id: type: string type: object + models.UserJSON: + properties: + admin_key: + type: string + is_pro: + type: boolean + messages_sent: + type: integer + quota_day: + type: string + quota_today: + type: integer + read_key: + type: string + send_key: + type: string + timestamp_created: + type: string + timestamp_last_read: + type: string + timestamp_last_sent: + type: string + user_id: + type: integer + username: + type: string + type: object host: scn.blackforestbytes.com info: contact: {} @@ -227,7 +230,30 @@ info: title: SimpleCloudNotifier API version: "2.0" paths: - /ack.php: + /api-v2/user/: + post: + operationId: api-user-create + parameters: + - description: ' ' + in: body + name: post_body + schema: + $ref: '#/definitions/handler.CreateUser.body' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.UserJSON' + "400": + description: Bad Request + schema: + $ref: '#/definitions/ginresp.apiError' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/ginresp.apiError' + summary: Create a new user + /api/ack.php: get: operationId: compat-ack parameters: @@ -254,20 +280,9 @@ paths: "500": description: Internal Server Error schema: - $ref: '#/definitions/ginresp.internAPIError' + $ref: '#/definitions/ginresp.apiError' summary: Acknowledge that a message was received - /db-test: - get: - responses: - "200": - description: OK - schema: - $ref: '#/definitions/handler.DatabaseTest.response' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/ginresp.errBody' - /expand.php: + /api/expand.php: get: operationId: compat-expand responses: @@ -278,20 +293,9 @@ paths: "500": description: Internal Server Error schema: - $ref: '#/definitions/ginresp.internAPIError' + $ref: '#/definitions/ginresp.apiError' summary: Get a whole (potentially truncated) message - /health: - get: - responses: - "200": - description: OK - schema: - $ref: '#/definitions/handler.Health.response' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/ginresp.errBody' - /info.php: + /api/info.php: get: operationId: compat-info parameters: @@ -313,60 +317,9 @@ paths: "500": description: Internal Server Error schema: - $ref: '#/definitions/ginresp.internAPIError' + $ref: '#/definitions/ginresp.apiError' summary: Get information about the current user - /ping: - delete: - responses: - "200": - description: OK - schema: - $ref: '#/definitions/handler.pingResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/ginresp.errBody' - get: - responses: - "200": - description: OK - schema: - $ref: '#/definitions/handler.pingResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/ginresp.errBody' - patch: - responses: - "200": - description: OK - schema: - $ref: '#/definitions/handler.pingResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/ginresp.errBody' - post: - responses: - "200": - description: OK - schema: - $ref: '#/definitions/handler.pingResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/ginresp.errBody' - put: - responses: - "200": - description: OK - schema: - $ref: '#/definitions/handler.pingResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/ginresp.errBody' - /register.php: + /api/register.php: get: operationId: compat-register parameters: @@ -396,9 +349,9 @@ paths: "500": description: Internal Server Error schema: - $ref: '#/definitions/ginresp.internAPIError' + $ref: '#/definitions/ginresp.apiError' summary: Register a new account - /requery.php: + /api/requery.php: get: operationId: compat-requery parameters: @@ -420,55 +373,9 @@ paths: "500": description: Internal Server Error schema: - $ref: '#/definitions/ginresp.internAPIError' + $ref: '#/definitions/ginresp.apiError' summary: Return all not-acknowledged messages - /send.php: - post: - consumes: - - application/json - description: (all arguments can either be supplied in the query or in the json - body) - operationId: compat-send - parameters: - - in: query - name: content - type: string - - in: query - name: messageID - type: string - - in: query - name: priority - type: string - - in: query - name: timestamp - type: string - - in: query - name: title - type: string - - in: query - name: userID - type: string - - in: query - name: userKey - type: string - - description: ' ' - in: body - name: post_body - schema: - $ref: '#/definitions/handler.Send.body' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/handler.Send.response' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/ginresp.sendAPIError' - summary: Send a message - /update.php: + /api/update.php: get: operationId: compat-update parameters: @@ -495,9 +402,9 @@ paths: "500": description: Internal Server Error schema: - $ref: '#/definitions/ginresp.internAPIError' + $ref: '#/definitions/ginresp.apiError' summary: Set the fcm-token (android) - /upgrade.php: + /api/upgrade.php: get: operationId: compat-upgrade parameters: @@ -532,6 +439,79 @@ paths: "500": description: Internal Server Error schema: - $ref: '#/definitions/ginresp.internAPIError' + $ref: '#/definitions/ginresp.apiError' summary: Upgrade a free account to a paid account + /db-test: + get: + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.DatabaseTest.response' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/ginresp.apiError' + /health: + get: + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.Health.response' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/ginresp.apiError' + /ping: + delete: + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.pingResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/ginresp.apiError' + get: + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.pingResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/ginresp.apiError' + patch: + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.pingResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/ginresp.apiError' + post: + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.pingResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/ginresp.apiError' + put: + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.pingResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/ginresp.apiError' swagger: "2.0" diff --git a/server/website/api.html b/server/website/api.html new file mode 100644 index 0000000..8e3f4b8 --- /dev/null +++ b/server/website/api.html @@ -0,0 +1,47 @@ + + + + + + Simple Cloud Notifications - API + + + + + + + + + + + +
+ + Send + +

Simple Cloud Notifier

+ +

Get your user-id and user-key from the app and send notifications to your phone by performing a POST request against https://simplecloudnotifier.blackforestbytes.com/

+
curl                                          \
+    --data "user_id={userid}"                 \
+    --data "user_key={userkey}"               \
+    --data "title={message_title}"            \
+    --data "content={message_body}"           \
+    --data "priority={0|1|2}"                 \
+    --data "msg_id={unique_message_id}"       \
+    https://scn.blackforestbytes.com/
+

The content, priority and msg_id parameters are optional, you can also send message with only a title and the default priority

+
curl                                          \
+    --data "user_id={userid}"                 \
+    --data "user_key={userkey}"               \
+    --data "title={message_title}"            \
+    https://scn.blackforestbytes.com/
+ + More + +
+ + \ No newline at end of file diff --git a/server/website/api_more.html b/server/website/api_more.html new file mode 100644 index 0000000..f86c54a --- /dev/null +++ b/server/website/api_more.html @@ -0,0 +1,299 @@ + + + + + + Simple Cloud Notifications - API + + + + + + + + + + + +
+ + Send + +

Simple Cloud Notifier

+ +

Introduction

+
+

+ With this API you can send push notifications to your phone. +

+

+ To recieve them you will need to install the SimpleCloudNotifier app from the play store. + When you open the app you can click on the account tab to see you unique user_id and user_key. + These two values are used to identify and authenticate your device so that send messages can be routed to your phone. +

+

+ You can at any time generate a new user_key in the app and invalidate the old one. +

+

+ There is also a web interface for this API to manually send notifications to your phone or to test your setup. +

+
+ +

Quota

+
+

+ By default you can send up to 100 messages per day per device. + If you need more you can upgrade your account in the app to get 1000 messages per day, this has the additional benefit of removing ads and supporting the development of the app (and making sure I can pay the server costs). +

+
+ +

API Requests

+
+

+ To send a new notification you send a POST request to the URL https://scn.blackforestbytes.com/. + All Parameters can either directly be submitted as URL parameters or they can be put into the POST body. +

+

+ You need to supply a valid user_id - user_key pair and a title for your message, all other parameter are optional. +

+
+ +

API Response

+
+

+ If the operation was successful the API will respond with an HTTP statuscode 200 and an JSON payload indicating the send message and your remaining quota +

+
{
+    "success":true,
+    "message":"Message sent",
+    "response":
+    {
+        "multicast_id":8000000000000000006,
+        "success":1,
+        "failure":0,
+        "canonical_ids":0,
+        "results": [{"message_id":"0:10000000000000000000000000000000d"}]
+    },
+    "quota":17,
+    "quota_max":100
+}
+

+ If the operation is not successful the API will respond with an 4xx HTTP statuscode. +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
StatuscodeExplanation
200 (OK)Message sent
400 (Bad Request)The request is invalid (missing parameters or wrong values)
401 (Unauthorized)The user_id was not found or the user_key is wrong
403 (Forbidden)The user has exceeded its daily quota - wait 24 hours or upgrade your account
412 (Precondition Failed)There is no device connected with this account - open the app and press the refresh button in the account tab
500 (Internal Server Error)There was an internal error while sending your data - try again later
+

+ There is also always a JSON payload with additional information. + The success field is always there and in the error state you the message field to get a descritpion of the problem. +

+
{
+    "success":false,
+    "error":2101,
+    "errhighlight":-1,
+    "message":"Daily quota reached (100)"
+}
+
+ +

Message Content

+
+

+ Every message must have a title set. + But you also (optionally) add more content, while the title has a max length of 120 characters, the conntent can be up to 10.000 characters. + You can see the whole message with title and content in the app or when clicking on the notification. +

+

+ If needed the content can be supplied in the content parameter. +

+
curl                                          \
+    --data "user_id={userid}"                 \
+    --data "user_key={userkey}"               \
+    --data "title={message_title}"            \
+    --data "content={message_content}"        \
+    https://scn.blackforestbytes.com/
+
+ +

Message Priority

+
+

+ Currently you can send a message with three different priorities: 0 (low), 1 (normal) and 2 (high). + In the app you can then configure a different behaviour for different priorities, e.g. only playing a sound if the notification is high priority. +

+

+ Priorites are either 0, 1 or 2 and are supplied in the priority parameter. + If no priority is supplied the message will get the default priority of 1. +

+
curl                                          \
+    --data "user_id={userid}"                 \
+    --data "user_key={userkey}"               \
+    --data "title={message_title}"            \
+    --data "priority={0|1|2}"                 \
+    https://scn.blackforestbytes.com/s
+
+ +

Message Uniqueness

+
+

+ Sometimes your script can run in an environment with an unstable connection and you want to implement an automatic re-try mechanism to send a message again if the last try failed due to bad connectivity. +

+

+ To ensure that a message is only send once you can generate a unique id for your message (I would recommend a simple uuidgen). + If you send a message with an UUID that was already used in the near past the API still returns OK, but no new message is sent. +

+

+ The message_id is optional - but if you want to use it you need to supply it via the msg_id parameter. +

+
curl                                          \
+    --data "user_id={userid}"                 \
+    --data "user_key={userkey}"               \
+    --data "title={message_title}"            \
+    --data "msg_id={message_id}"              \
+    https://scn.blackforestbytes.com/
+

+ Be aware that the server only saves send messages for a short amount of time. Because of that you can only use this to prevent duplicates in a short time-frame, older messages with the same ID are probably already deleted and the message will be send again. +

+
+ +

Custom Time

+
+

+ You can modify the displayed timestamp of a message by sending the timestamp parameter. The format must be a valid UNIX timestamp (elapsed seconds since 1970-01-01 GMT) +

+

+ The custom timestamp must be within 48 hours of the current time. This parameter is only intended to supply a more precise value in case the message sending was delayed. +

+
curl                                          \
+    --data "user_id={userid}"                 \
+    --data "user_key={userkey}"               \
+    --data "title={message_title}"            \
+    --data "timestamp={unix_timestamp}"       \
+    https://scn.blackforestbytes.com/
+
+ +

Bash script example

+
+

+ Depending on your use case it can be useful to create a bash script that handles things like resending messages if you have connection problems or waiting if there is no quota left.
+ Here is an example how such a scrippt could look like, you can put it into /usr/local/sbin and call it with scn_send "title" "content" +

+
#!/usr/bin/env bash
+
+#
+# Call with   `scn_send title`
+#        or   `scn_send title content`
+#        or   `scn_send title content priority`
+#
+#
+
+if [ "$#" -lt 1 ]; then
+    echo "no title supplied via parameter"
+    exit 1
+fi
+
+################################################################################
+# INSERT YOUR DATA HERE                                                        #
+################################################################################
+user_id=999
+user_key="????????????????????????????????????????????????????????????????"
+################################################################################
+
+title=$1
+content=""
+sendtime=$(date +%s)
+
+if [ "$#" -gt 1 ]; then
+    content=$2
+fi
+
+priority=1
+
+if [ "$#" -gt 2 ]; then
+    priority=$3
+fi
+
+usr_msg_id=$(uuidgen)
+
+while true ; do
+
+    curlresp=$(curl -s -o /dev/null -w "%{http_code}" \
+                    -d "user_id=$user_id" -d "user_key=$user_key" -d "title=$title" -d "timestamp=$sendtime" \
+                    -d "content=$content" -d "priority=$priority" -d "msg_id=$usr_msg_id" \
+                    https://scn.blackforestbytes.com/)
+
+    if [ "$curlresp" == 200 ] ; then
+        echo "Successfully send"
+        exit 0
+    fi
+
+    if [ "$curlresp" == 400 ] ; then
+        echo "Bad request - something went wrong"
+        exit 1
+    fi
+
+    if [ "$curlresp" == 401 ] ; then
+        echo "Unauthorized - wrong userid/userkey"
+        exit 1
+    fi
+
+    if [ "$curlresp" == 403 ] ; then
+        echo "Quota exceeded - wait one hour before re-try"
+        sleep 3600
+    fi
+
+    if [ "$curlresp" == 412 ] ; then
+        echo "Precondition Failed - No device linked"
+        exit 1
+    fi
+
+    if [ "$curlresp" == 500 ] ; then
+        echo "Internal server error - waiting for better times"
+        sleep 60
+    fi
+
+    # if none of the above matched we probably hav no network ...
+    echo "Send failed (response code $curlresp) ... try again in 5s"
+    sleep 5
+done
+
+

+ Be aware that the server only saves send messages for a short amount of time. Because of that you can only use this to prevent duplicates in a short time-frame, older messages with the same ID are probably already deleted and the message will be send again. +

+
+
+ + + \ No newline at end of file diff --git a/server/website/css/mini-dark.min.css b/server/website/css/mini-dark.min.css new file mode 100644 index 0000000..3877560 --- /dev/null +++ b/server/website/css/mini-dark.min.css @@ -0,0 +1 @@ +:root{--fore-color:#fdfdfd;--secondary-fore-color:#f0f0f0;--back-color:#111;--secondary-back-color:#222;--blockquote-color:#f57c00;--pre-color:#1565c0;--border-color:#ddd;--secondary-border-color:#aaa;--heading-ratio:1.19;--universal-margin:.5rem;--universal-padding:.5rem;--universal-border-radius:.125rem;--a-link-color:#0277bd;--a-visited-color:#01579b}html{font-size:16px}a,b,del,em,i,ins,q,span,strong,u{font-size:1em}html,*{font-family:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, "Helvetica Neue", Helvetica, sans-serif;line-height:1.5;-webkit-text-size-adjust:100%}*{font-size:1rem}body{margin:0;color:var(--fore-color);background:var(--back-color)}details{display:block}summary{display:list-item}abbr[title]{border-bottom:none;text-decoration:underline dotted}input{overflow:visible}img{max-width:100%;height:auto}h1,h2,h3,h4,h5,h6{line-height:1.2;margin:calc(1.5 * var(--universal-margin)) var(--universal-margin);font-weight:500}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small{color:var(--secondary-fore-color);display:block;margin-top:-.25rem}h1{font-size:calc(1rem * var(--heading-ratio) * var(--heading-ratio) * var(--heading-ratio) * var(--heading-ratio))}h2{font-size:calc(1rem * var(--heading-ratio) * var(--heading-ratio) * var(--heading-ratio))}h3{font-size:calc(1rem * var(--heading-ratio) * var(--heading-ratio))}h4{font-size:calc(1rem * var(--heading-ratio))}h5{font-size:1rem}h6{font-size:calc(1rem / var(--heading-ratio))}p{margin:var(--universal-margin)}ol,ul{margin:var(--universal-margin);padding-left:calc(2 * var(--universal-margin))}b,strong{font-weight:700}hr{box-sizing:content-box;border:0;line-height:1.25em;margin:var(--universal-margin);height:.0625rem;background:linear-gradient(to right, transparent, var(--border-color) 20%, var(--border-color) 80%, transparent)}blockquote{display:block;position:relative;font-style:italic;color:var(--secondary-fore-color);margin:var(--universal-margin);padding:calc(3 * var(--universal-padding));border:.0625rem solid var(--secondary-border-color);border-left:.375rem solid var(--blockquote-color);border-radius:0 var(--universal-border-radius) var(--universal-border-radius) 0}blockquote:before{position:absolute;top:calc(0rem - var(--universal-padding));left:0;font-family:sans-serif;font-size:3rem;font-weight:700;content:"\201c";color:var(--blockquote-color)}blockquote[cite]:after{font-style:normal;font-size:.75em;font-weight:700;content:"\a— " attr(cite);white-space:pre}code,kbd,pre,samp{font-family:Menlo, Consolas, monospace;font-size:.85em}code{background:var(--secondary-back-color);border-radius:var(--universal-border-radius);padding:calc(var(--universal-padding) / 4) calc(var(--universal-padding) / 2)}kbd{background:var(--fore-color);color:var(--back-color);border-radius:var(--universal-border-radius);padding:calc(var(--universal-padding) / 4) calc(var(--universal-padding) / 2)}pre{overflow:auto;background:var(--secondary-back-color);padding:calc(1.5 * var(--universal-padding));margin:var(--universal-margin);border:.0625rem solid var(--secondary-border-color);border-left:.25rem solid var(--pre-color);border-radius:0 var(--universal-border-radius) var(--universal-border-radius) 0}sup,sub,code,kbd{line-height:0;position:relative;vertical-align:baseline}small,sup,sub,figcaption{font-size:.75em}sup{top:-.5em}sub{bottom:-.25em}figure{margin:var(--universal-margin)}figcaption{color:var(--secondary-fore-color)}a{text-decoration:none}a:link{color:var(--a-link-color)}a:visited{color:var(--a-visited-color)}a:hover,a:focus{text-decoration:underline}.container{margin:0 auto;padding:0 calc(1.5 * var(--universal-padding))}.row{box-sizing:border-box;display:flex;flex:0 1 auto;flex-flow:row wrap}.col-sm,[class^='col-sm-'],[class^='col-sm-offset-'],.row[class*='cols-sm-']>*{box-sizing:border-box;flex:0 0 auto;padding:0 calc(var(--universal-padding) / 2)}.col-sm,.row.cols-sm>*{max-width:100%;flex-grow:1;flex-basis:0}.col-sm-1,.row.cols-sm-1>*{max-width:8.33333%;flex-basis:8.33333%}.col-sm-offset-0{margin-left:0}.col-sm-2,.row.cols-sm-2>*{max-width:16.66667%;flex-basis:16.66667%}.col-sm-offset-1{margin-left:8.33333%}.col-sm-3,.row.cols-sm-3>*{max-width:25%;flex-basis:25%}.col-sm-offset-2{margin-left:16.66667%}.col-sm-4,.row.cols-sm-4>*{max-width:33.33333%;flex-basis:33.33333%}.col-sm-offset-3{margin-left:25%}.col-sm-5,.row.cols-sm-5>*{max-width:41.66667%;flex-basis:41.66667%}.col-sm-offset-4{margin-left:33.33333%}.col-sm-6,.row.cols-sm-6>*{max-width:50%;flex-basis:50%}.col-sm-offset-5{margin-left:41.66667%}.col-sm-7,.row.cols-sm-7>*{max-width:58.33333%;flex-basis:58.33333%}.col-sm-offset-6{margin-left:50%}.col-sm-8,.row.cols-sm-8>*{max-width:66.66667%;flex-basis:66.66667%}.col-sm-offset-7{margin-left:58.33333%}.col-sm-9,.row.cols-sm-9>*{max-width:75%;flex-basis:75%}.col-sm-offset-8{margin-left:66.66667%}.col-sm-10,.row.cols-sm-10>*{max-width:83.33333%;flex-basis:83.33333%}.col-sm-offset-9{margin-left:75%}.col-sm-11,.row.cols-sm-11>*{max-width:91.66667%;flex-basis:91.66667%}.col-sm-offset-10{margin-left:83.33333%}.col-sm-12,.row.cols-sm-12>*{max-width:100%;flex-basis:100%}.col-sm-offset-11{margin-left:91.66667%}.col-sm-normal{order:initial}.col-sm-first{order:-999}.col-sm-last{order:999}@media screen and (min-width: 768px){.col-md,[class^='col-md-'],[class^='col-md-offset-'],.row[class*='cols-md-']>*{box-sizing:border-box;flex:0 0 auto;padding:0 calc(var(--universal-padding) / 2)}.col-md,.row.cols-md>*{max-width:100%;flex-grow:1;flex-basis:0}.col-md-1,.row.cols-md-1>*{max-width:8.33333%;flex-basis:8.33333%}.col-md-offset-0{margin-left:0}.col-md-2,.row.cols-md-2>*{max-width:16.66667%;flex-basis:16.66667%}.col-md-offset-1{margin-left:8.33333%}.col-md-3,.row.cols-md-3>*{max-width:25%;flex-basis:25%}.col-md-offset-2{margin-left:16.66667%}.col-md-4,.row.cols-md-4>*{max-width:33.33333%;flex-basis:33.33333%}.col-md-offset-3{margin-left:25%}.col-md-5,.row.cols-md-5>*{max-width:41.66667%;flex-basis:41.66667%}.col-md-offset-4{margin-left:33.33333%}.col-md-6,.row.cols-md-6>*{max-width:50%;flex-basis:50%}.col-md-offset-5{margin-left:41.66667%}.col-md-7,.row.cols-md-7>*{max-width:58.33333%;flex-basis:58.33333%}.col-md-offset-6{margin-left:50%}.col-md-8,.row.cols-md-8>*{max-width:66.66667%;flex-basis:66.66667%}.col-md-offset-7{margin-left:58.33333%}.col-md-9,.row.cols-md-9>*{max-width:75%;flex-basis:75%}.col-md-offset-8{margin-left:66.66667%}.col-md-10,.row.cols-md-10>*{max-width:83.33333%;flex-basis:83.33333%}.col-md-offset-9{margin-left:75%}.col-md-11,.row.cols-md-11>*{max-width:91.66667%;flex-basis:91.66667%}.col-md-offset-10{margin-left:83.33333%}.col-md-12,.row.cols-md-12>*{max-width:100%;flex-basis:100%}.col-md-offset-11{margin-left:91.66667%}.col-md-normal{order:initial}.col-md-first{order:-999}.col-md-last{order:999}}@media screen and (min-width: 1280px){.col-lg,[class^='col-lg-'],[class^='col-lg-offset-'],.row[class*='cols-lg-']>*{box-sizing:border-box;flex:0 0 auto;padding:0 calc(var(--universal-padding) / 2)}.col-lg,.row.cols-lg>*{max-width:100%;flex-grow:1;flex-basis:0}.col-lg-1,.row.cols-lg-1>*{max-width:8.33333%;flex-basis:8.33333%}.col-lg-offset-0{margin-left:0}.col-lg-2,.row.cols-lg-2>*{max-width:16.66667%;flex-basis:16.66667%}.col-lg-offset-1{margin-left:8.33333%}.col-lg-3,.row.cols-lg-3>*{max-width:25%;flex-basis:25%}.col-lg-offset-2{margin-left:16.66667%}.col-lg-4,.row.cols-lg-4>*{max-width:33.33333%;flex-basis:33.33333%}.col-lg-offset-3{margin-left:25%}.col-lg-5,.row.cols-lg-5>*{max-width:41.66667%;flex-basis:41.66667%}.col-lg-offset-4{margin-left:33.33333%}.col-lg-6,.row.cols-lg-6>*{max-width:50%;flex-basis:50%}.col-lg-offset-5{margin-left:41.66667%}.col-lg-7,.row.cols-lg-7>*{max-width:58.33333%;flex-basis:58.33333%}.col-lg-offset-6{margin-left:50%}.col-lg-8,.row.cols-lg-8>*{max-width:66.66667%;flex-basis:66.66667%}.col-lg-offset-7{margin-left:58.33333%}.col-lg-9,.row.cols-lg-9>*{max-width:75%;flex-basis:75%}.col-lg-offset-8{margin-left:66.66667%}.col-lg-10,.row.cols-lg-10>*{max-width:83.33333%;flex-basis:83.33333%}.col-lg-offset-9{margin-left:75%}.col-lg-11,.row.cols-lg-11>*{max-width:91.66667%;flex-basis:91.66667%}.col-lg-offset-10{margin-left:83.33333%}.col-lg-12,.row.cols-lg-12>*{max-width:100%;flex-basis:100%}.col-lg-offset-11{margin-left:91.66667%}.col-lg-normal{order:initial}.col-lg-first{order:-999}.col-lg-last{order:999}}:root{--card-back-color:#111;--card-fore-color:#fdfdfd;--card-border-color:#aaa}.card{display:flex;flex-direction:column;justify-content:space-between;align-self:center;position:relative;width:100%;background:var(--card-back-color);color:var(--card-fore-color);border:.0625rem solid var(--card-border-color);border-radius:var(--universal-border-radius);margin:var(--universal-margin);overflow:hidden}@media screen and (min-width: 320px){.card{max-width:320px}}.card>.section{background:var(--card-back-color);color:var(--card-fore-color);box-sizing:border-box;margin:0;border:0;border-radius:0;border-bottom:.0625rem solid var(--card-border-color);padding:var(--universal-padding);width:100%}.card>.section.media{height:200px;padding:0;-o-object-fit:cover;object-fit:cover}.card>.section:last-child{border-bottom:0}@media screen and (min-width: 240px){.card.small{max-width:240px}}@media screen and (min-width: 480px){.card.large{max-width:480px}}.card.fluid{max-width:100%;width:auto}.card.warning{--card-back-color:#ffca28;--card-fore-color:#111;--card-border-color:#e8b825}.card.error{--card-back-color:#b71c1c;--card-fore-color:#f8f8f8;--card-border-color:#a71a1a}.card>.section.dark{--card-back-color:#e0e0e0;--card-fore-color:#111}.card>.section.double-padded{padding:calc(1.5 * var(--universal-padding))}:root{--form-back-color:#222;--form-fore-color:#fdfdfd;--form-border-color:#aaa;--input-back-color:#111;--input-fore-color:#fdfdfd;--input-border-color:#aaa;--input-focus-color:#0288d1;--input-invalid-color:#d32f2f;--button-back-color:#212121;--button-hover-back-color:#444;--button-fore-color:#e2e2e2;--button-border-color:transparent;--button-hover-border-color:transparent;--button-group-border-color:rgba(124,124,124,0.54)}form{background:var(--form-back-color);color:var(--form-fore-color);border:.0625rem solid var(--form-border-color);border-radius:var(--universal-border-radius);margin:var(--universal-margin);padding:calc(2 * var(--universal-padding)) var(--universal-padding)}fieldset{border:.0625rem solid var(--form-border-color);border-radius:var(--universal-border-radius);margin:calc(var(--universal-margin) / 4);padding:var(--universal-padding)}legend{box-sizing:border-box;display:table;max-width:100%;white-space:normal;font-weight:700;padding:calc(var(--universal-padding) / 2)}label{padding:calc(var(--universal-padding) / 2) var(--universal-padding)}.input-group{display:inline-block}.input-group.fluid{display:flex;align-items:center;justify-content:center}.input-group.fluid>input{max-width:100%;flex-grow:1;flex-basis:0px}@media screen and (max-width: 767px){.input-group.fluid{align-items:stretch;flex-direction:column}}.input-group.vertical{display:flex;align-items:stretch;flex-direction:column}.input-group.vertical>input{max-width:100%;flex-grow:1;flex-basis:0px}[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}input:not([type]),[type="text"],[type="email"],[type="number"],[type="search"],[type="password"],[type="url"],[type="tel"],[type="checkbox"],[type="radio"],textarea,select{box-sizing:border-box;background:var(--input-back-color);color:var(--input-fore-color);border:.0625rem solid var(--input-border-color);border-radius:var(--universal-border-radius);margin:calc(var(--universal-margin) / 2);padding:var(--universal-padding) calc(1.5 * var(--universal-padding))}input:not([type="button"]):not([type="submit"]):not([type="reset"]):hover,input:not([type="button"]):not([type="submit"]):not([type="reset"]):focus,textarea:hover,textarea:focus,select:hover,select:focus{border-color:var(--input-focus-color);box-shadow:none}input:not([type="button"]):not([type="submit"]):not([type="reset"]):invalid,input:not([type="button"]):not([type="submit"]):not([type="reset"]):focus:invalid,textarea:invalid,textarea:focus:invalid,select:invalid,select:focus:invalid{border-color:var(--input-invalid-color);box-shadow:none}input:not([type="button"]):not([type="submit"]):not([type="reset"])[readonly],textarea[readonly],select[readonly]{background:var(--secondary-back-color)}select{max-width:100%}option{overflow:hidden;text-overflow:ellipsis}[type="checkbox"],[type="radio"]{-webkit-appearance:none;-moz-appearance:none;appearance:none;position:relative;height:calc(1rem + var(--universal-padding) / 2);width:calc(1rem + var(--universal-padding) / 2);vertical-align:text-bottom;padding:0;flex-basis:calc(1rem + var(--universal-padding) / 2) !important;flex-grow:0 !important}[type="checkbox"]:checked:before,[type="radio"]:checked:before{position:absolute}[type="checkbox"]:checked:before{content:'\2713';font-family:sans-serif;font-size:calc(1rem + var(--universal-padding) / 2);top:calc(0rem - var(--universal-padding));left:calc(var(--universal-padding) / 4)}[type="radio"]{border-radius:100%}[type="radio"]:checked:before{border-radius:100%;content:'';top:calc(.0625rem + var(--universal-padding) / 2);left:calc(.0625rem + var(--universal-padding) / 2);background:var(--input-fore-color);width:0.5rem;height:0.5rem}:placeholder-shown{color:var(--input-fore-color)}::-ms-placeholder{color:var(--input-fore-color);opacity:0.54}button::-moz-focus-inner,[type="button"]::-moz-focus-inner,[type="reset"]::-moz-focus-inner,[type="submit"]::-moz-focus-inner{border-style:none;padding:0}button,html [type="button"],[type="reset"],[type="submit"]{-webkit-appearance:button}button{overflow:visible;text-transform:none}button,[type="button"],[type="submit"],[type="reset"],a.button,label.button,.button,a[role="button"],label[role="button"],[role="button"]{display:inline-block;background:var(--button-back-color);color:var(--button-fore-color);border:.0625rem solid var(--button-border-color);border-radius:var(--universal-border-radius);padding:var(--universal-padding) calc(1.5 * var(--universal-padding));margin:var(--universal-margin);text-decoration:none;cursor:pointer;transition:background 0.3s}button:hover,button:focus,[type="button"]:hover,[type="button"]:focus,[type="submit"]:hover,[type="submit"]:focus,[type="reset"]:hover,[type="reset"]:focus,a.button:hover,a.button:focus,label.button:hover,label.button:focus,.button:hover,.button:focus,a[role="button"]:hover,a[role="button"]:focus,label[role="button"]:hover,label[role="button"]:focus,[role="button"]:hover,[role="button"]:focus{background:var(--button-hover-back-color);border-color:var(--button-hover-border-color)}input:disabled,input[disabled],textarea:disabled,textarea[disabled],select:disabled,select[disabled],button:disabled,button[disabled],.button:disabled,.button[disabled],[role="button"]:disabled,[role="button"][disabled]{cursor:not-allowed;opacity:.75}.button-group{display:flex;border:.0625rem solid var(--button-group-border-color);border-radius:var(--universal-border-radius);margin:var(--universal-margin)}.button-group>button,.button-group [type="button"],.button-group>[type="submit"],.button-group>[type="reset"],.button-group>.button,.button-group>[role="button"]{margin:0;max-width:100%;flex:1 1 auto;text-align:center;border:0;border-radius:0;box-shadow:none}.button-group>:not(:first-child){border-left:.0625rem solid var(--button-group-border-color)}@media screen and (max-width: 767px){.button-group{flex-direction:column}.button-group>:not(:first-child){border:0;border-top:.0625rem solid var(--button-group-border-color)}}button.primary,[type="button"].primary,[type="submit"].primary,[type="reset"].primary,.button.primary,[role="button"].primary{--button-back-color:#1976d2;--button-fore-color:#f8f8f8}button.primary:hover,button.primary:focus,[type="button"].primary:hover,[type="button"].primary:focus,[type="submit"].primary:hover,[type="submit"].primary:focus,[type="reset"].primary:hover,[type="reset"].primary:focus,.button.primary:hover,.button.primary:focus,[role="button"].primary:hover,[role="button"].primary:focus{--button-hover-back-color:#1565c0}button.secondary,[type="button"].secondary,[type="submit"].secondary,[type="reset"].secondary,.button.secondary,[role="button"].secondary{--button-back-color:#d32f2f;--button-fore-color:#f8f8f8}button.secondary:hover,button.secondary:focus,[type="button"].secondary:hover,[type="button"].secondary:focus,[type="submit"].secondary:hover,[type="submit"].secondary:focus,[type="reset"].secondary:hover,[type="reset"].secondary:focus,.button.secondary:hover,.button.secondary:focus,[role="button"].secondary:hover,[role="button"].secondary:focus{--button-hover-back-color:#c62828}button.tertiary,[type="button"].tertiary,[type="submit"].tertiary,[type="reset"].tertiary,.button.tertiary,[role="button"].tertiary{--button-back-color:#308732;--button-fore-color:#f8f8f8}button.tertiary:hover,button.tertiary:focus,[type="button"].tertiary:hover,[type="button"].tertiary:focus,[type="submit"].tertiary:hover,[type="submit"].tertiary:focus,[type="reset"].tertiary:hover,[type="reset"].tertiary:focus,.button.tertiary:hover,.button.tertiary:focus,[role="button"].tertiary:hover,[role="button"].tertiary:focus{--button-hover-back-color:#277529}button.inverse,[type="button"].inverse,[type="submit"].inverse,[type="reset"].inverse,.button.inverse,[role="button"].inverse{--button-back-color:#f8f8f8;--button-fore-color:#212121}button.inverse:hover,button.inverse:focus,[type="button"].inverse:hover,[type="button"].inverse:focus,[type="submit"].inverse:hover,[type="submit"].inverse:focus,[type="reset"].inverse:hover,[type="reset"].inverse:focus,.button.inverse:hover,.button.inverse:focus,[role="button"].inverse:hover,[role="button"].inverse:focus{--button-hover-back-color:#f0f0f0}button.small,[type="button"].small,[type="submit"].small,[type="reset"].small,.button.small,[role="button"].small{padding:calc(0.5 * var(--universal-padding)) calc(0.75 * var(--universal-padding));margin:var(--universal-margin)}button.large,[type="button"].large,[type="submit"].large,[type="reset"].large,.button.large,[role="button"].large{padding:calc(1.5 * var(--universal-padding)) calc(2 * var(--universal-padding));margin:var(--universal-margin)}:root{--header-back-color:#111;--header-hover-back-color:#222;--header-fore-color:#f0f0f0;--header-border-color:#aaa;--nav-back-color:#111;--nav-hover-back-color:#222;--nav-fore-color:#f0f0f0;--nav-border-color:#aaa;--nav-link-color:#0277bd;--footer-fore-color:#f0f0f0;--footer-back-color:#111;--footer-border-color:#aaa;--footer-link-color:#0277bd;--drawer-back-color:#111;--drawer-hover-back-color:#222;--drawer-border-color:#aaa;--drawer-close-color:#f0f0f0}header{height:3.1875rem;background:var(--header-back-color);color:var(--header-fore-color);border-bottom:.0625rem solid var(--header-border-color);padding:calc(var(--universal-padding) / 4) 0;white-space:nowrap;overflow-x:auto;overflow-y:hidden}header.row{box-sizing:content-box}header .logo{color:var(--header-fore-color);font-size:1.75rem;padding:var(--universal-padding) calc(2 * var(--universal-padding));text-decoration:none}header button,header [type="button"],header .button,header [role="button"]{box-sizing:border-box;position:relative;top:calc(0rem - var(--universal-padding) / 4);height:calc(3.1875rem + var(--universal-padding) / 2);background:var(--header-back-color);line-height:calc(3.1875rem - var(--universal-padding) * 1.5);text-align:center;color:var(--header-fore-color);border:0;border-radius:0;margin:0;text-transform:uppercase}header button:hover,header button:focus,header [type="button"]:hover,header [type="button"]:focus,header .button:hover,header .button:focus,header [role="button"]:hover,header [role="button"]:focus{background:var(--header-hover-back-color)}nav{background:var(--nav-back-color);color:var(--nav-fore-color);border:.0625rem solid var(--nav-border-color);border-radius:var(--universal-border-radius);margin:var(--universal-margin)}nav *{padding:var(--universal-padding) calc(1.5 * var(--universal-padding))}nav a,nav a:visited{display:block;color:var(--nav-link-color);border-radius:var(--universal-border-radius);transition:background 0.3s}nav a:hover,nav a:focus,nav a:visited:hover,nav a:visited:focus{text-decoration:none;background:var(--nav-hover-back-color)}nav .sublink-1{position:relative;margin-left:calc(2 * var(--universal-padding))}nav .sublink-1:before{position:absolute;left:calc(var(--universal-padding) - 1 * var(--universal-padding));top:-.0625rem;content:'';height:100%;border:.0625rem solid var(--nav-border-color);border-left:0}nav .sublink-2{position:relative;margin-left:calc(4 * var(--universal-padding))}nav .sublink-2:before{position:absolute;left:calc(var(--universal-padding) - 3 * var(--universal-padding));top:-.0625rem;content:'';height:100%;border:.0625rem solid var(--nav-border-color);border-left:0}footer{background:var(--footer-back-color);color:var(--footer-fore-color);border-top:.0625rem solid var(--footer-border-color);padding:calc(2 * var(--universal-padding)) var(--universal-padding);font-size:.875rem}footer a,footer a:visited{color:var(--footer-link-color)}header.sticky{position:-webkit-sticky;position:sticky;z-index:1101;top:0}footer.sticky{position:-webkit-sticky;position:sticky;z-index:1101;bottom:0}.drawer-toggle:before{display:inline-block;position:relative;vertical-align:bottom;content:'\00a0\2261\00a0';font-family:sans-serif;font-size:1.5em}@media screen and (min-width: 768px){.drawer-toggle:not(.persistent){display:none}}[type="checkbox"].drawer{height:1px;width:1px;margin:-1px;overflow:hidden;position:absolute;clip:rect(0 0 0 0);-webkit-clip-path:inset(100%);clip-path:inset(100%)}[type="checkbox"].drawer+*{display:block;box-sizing:border-box;position:fixed;top:0;width:320px;height:100vh;overflow-y:auto;background:var(--drawer-back-color);border:.0625rem solid var(--drawer-border-color);border-radius:0;margin:0;z-index:1110;right:-320px;transition:right 0.3s}[type="checkbox"].drawer+* .drawer-close{position:absolute;top:var(--universal-margin);right:var(--universal-margin);z-index:1111;width:2rem;height:2rem;border-radius:var(--universal-border-radius);padding:var(--universal-padding);margin:0;cursor:pointer;transition:background 0.3s}[type="checkbox"].drawer+* .drawer-close:before{display:block;content:'\00D7';color:var(--drawer-close-color);position:relative;font-family:sans-serif;font-size:2rem;line-height:1;text-align:center}[type="checkbox"].drawer+* .drawer-close:hover,[type="checkbox"].drawer+* .drawer-close:focus{background:var(--drawer-hover-back-color)}@media screen and (max-width: 320px){[type="checkbox"].drawer+*{width:100%}}[type="checkbox"].drawer:checked+*{right:0}@media screen and (min-width: 768px){[type="checkbox"].drawer:not(.persistent)+*{position:static;height:100%;z-index:1100}[type="checkbox"].drawer:not(.persistent)+* .drawer-close{display:none}}:root{--table-border-color:#ddd;--table-border-separator-color:#666;--table-head-back-color:#212121;--table-head-fore-color:#fdfdfd;--table-body-back-color:#111;--table-body-fore-color:#fdfdfd;--table-body-alt-back-color:#444}table{border-collapse:separate;border-spacing:0;margin:0;display:flex;flex:0 1 auto;flex-flow:row wrap;padding:var(--universal-padding);padding-top:0}table caption{font-size:1.5rem;margin:calc(2 * var(--universal-margin)) 0;max-width:100%;flex:0 0 100%}table thead,table tbody{display:flex;flex-flow:row wrap;border:.0625rem solid var(--table-border-color)}table thead{z-index:999;border-radius:var(--universal-border-radius) var(--universal-border-radius) 0 0;border-bottom:.0625rem solid var(--table-border-separator-color)}table tbody{border-top:0;margin-top:calc(0 - var(--universal-margin));border-radius:0 0 var(--universal-border-radius) var(--universal-border-radius)}table tr{display:flex;padding:0}table th,table td{padding:calc(2 * var(--universal-padding))}table th{text-align:left;background:var(--table-head-back-color);color:var(--table-head-fore-color)}table td{background:var(--table-body-back-color);color:var(--table-body-fore-color);border-top:.0625rem solid var(--table-border-color)}table:not(.horizontal){overflow:auto;max-height:400px}table:not(.horizontal) thead,table:not(.horizontal) tbody{max-width:100%;flex:0 0 100%}table:not(.horizontal) tr{flex-flow:row wrap;flex:0 0 100%}table:not(.horizontal) th,table:not(.horizontal) td{flex:1 0 0%;overflow:hidden;text-overflow:ellipsis}table:not(.horizontal) thead{position:sticky;top:0}table:not(.horizontal) tbody tr:first-child td{border-top:0}table.horizontal{border:0}table.horizontal thead,table.horizontal tbody{border:0;flex-flow:row nowrap}table.horizontal tbody{overflow:auto;justify-content:space-between;flex:1 0 0;margin-left:calc( 4 * var(--universal-margin));padding-bottom:calc(var(--universal-padding) / 4)}table.horizontal tr{flex-direction:column;flex:1 0 auto}table.horizontal th,table.horizontal td{width:100%;border:0;border-bottom:.0625rem solid var(--table-border-color)}table.horizontal th:not(:first-child),table.horizontal td:not(:first-child){border-top:0}table.horizontal th{text-align:right;border-left:.0625rem solid var(--table-border-color);border-right:.0625rem solid var(--table-border-separator-color)}table.horizontal thead tr:first-child{padding-left:0}table.horizontal th:first-child,table.horizontal td:first-child{border-top:.0625rem solid var(--table-border-color)}table.horizontal tbody tr:last-child td{border-right:.0625rem solid var(--table-border-color)}table.horizontal tbody tr:last-child td:first-child{border-top-right-radius:0.25rem}table.horizontal tbody tr:last-child td:last-child{border-bottom-right-radius:0.25rem}table.horizontal thead tr:first-child th:first-child{border-top-left-radius:0.25rem}table.horizontal thead tr:first-child th:last-child{border-bottom-left-radius:0.25rem}@media screen and (max-width: 767px){table,table.horizontal{border-collapse:collapse;border:0;width:100%;display:table}table thead,table th,table.horizontal thead,table.horizontal th{border:0;height:1px;width:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;clip:rect(0 0 0 0);-webkit-clip-path:inset(100%);clip-path:inset(100%)}table tbody,table.horizontal tbody{border:0;display:table-row-group}table tr,table.horizontal tr{display:block;border:.0625rem solid var(--table-border-color);border-radius:var(--universal-border-radius);background:#fafafa;padding:var(--universal-padding);margin:var(--universal-margin);margin-bottom:calc(2 * var(--universal-margin))}table th,table td,table.horizontal th,table.horizontal td{width:auto}table td,table.horizontal td{display:block;border:0;text-align:right}table td:before,table.horizontal td:before{content:attr(data-label);float:left;font-weight:600}table th:first-child,table td:first-child,table.horizontal th:first-child,table.horizontal td:first-child{border-top:0}table tbody tr:last-child td,table.horizontal tbody tr:last-child td{border-right:0}}:root{--table-body-alt-back-color:#444}table.striped tr:nth-of-type(2n)>td{background:var(--table-body-alt-back-color)}@media screen and (max-width: 768px){table.striped tr:nth-of-type(2n){background:var(--table-body-alt-back-color)}}:root{--table-body-hover-back-color:#5c819f}table.hoverable tr:hover,table.hoverable tr:hover>td,table.hoverable tr:focus,table.hoverable tr:focus>td{background:var(--table-body-hover-back-color)}@media screen and (max-width: 768px){table.hoverable tr:hover,table.hoverable tr:hover>td,table.hoverable tr:focus,table.hoverable tr:focus>td{background:var(--table-body-hover-back-color)}}:root{--mark-back-color:#0277bd;--mark-fore-color:#fafafa}mark{background:var(--mark-back-color);color:var(--mark-fore-color);font-size:.95em;line-height:1em;border-radius:var(--universal-border-radius);padding:calc(var(--universal-padding) / 4) calc(var(--universal-padding) / 2)}mark.inline-block{display:inline-block;font-size:1em;line-height:1.5;padding:calc(var(--universal-padding) / 2) var(--universal-padding)}:root{--toast-back-color:#424242;--toast-fore-color:#fafafa}.toast{position:fixed;bottom:calc(var(--universal-margin) * 3);left:50%;transform:translate(-50%, -50%);z-index:1111;color:var(--toast-fore-color);background:var(--toast-back-color);border-radius:calc(var(--universal-border-radius) * 16);padding:var(--universal-padding) calc(var(--universal-padding) * 3)}:root{--tooltip-back-color:#fafafa;--tooltip-fore-color:#212121}.tooltip{position:relative;display:inline-block}.tooltip:before,.tooltip:after{position:absolute;opacity:0;clip:rect(0 0 0 0);-webkit-clip-path:inset(100%);clip-path:inset(100%);transition:all 0.3s;z-index:1010;left:50%}.tooltip:not(.bottom):before,.tooltip:not(.bottom):after{bottom:75%}.tooltip.bottom:before,.tooltip.bottom:after{top:75%}.tooltip:hover:before,.tooltip:hover:after,.tooltip:focus:before,.tooltip:focus:after{opacity:1;clip:auto;-webkit-clip-path:inset(0%);clip-path:inset(0%)}.tooltip:before{content:'';background:transparent;border:var(--universal-margin) solid transparent;left:calc(50% - var(--universal-margin))}.tooltip:not(.bottom):before{border-top-color:#fafafa}.tooltip.bottom:before{border-bottom-color:#fafafa}.tooltip:after{content:attr(aria-label);color:var(--tooltip-fore-color);background:var(--tooltip-back-color);border-radius:var(--universal-border-radius);padding:var(--universal-padding);white-space:nowrap;transform:translateX(-50%)}.tooltip:not(.bottom):after{margin-bottom:calc(2 * var(--universal-margin))}.tooltip.bottom:after{margin-top:calc(2 * var(--universal-margin))}:root{--modal-overlay-color:rgba(0,0,0,0.45);--modal-close-color:#f0f0f0;--modal-close-hover-color:#222}[type="checkbox"].modal{height:1px;width:1px;margin:-1px;overflow:hidden;position:absolute;clip:rect(0 0 0 0);-webkit-clip-path:inset(100%);clip-path:inset(100%)}[type="checkbox"].modal+div{position:fixed;top:0;left:0;display:none;width:100vw;height:100vh;background:var(--modal-overlay-color)}[type="checkbox"].modal+div .card{margin:0 auto;max-height:50vh;overflow:auto}[type="checkbox"].modal+div .card .modal-close{position:absolute;top:0;right:0;width:1.75rem;height:1.75rem;border-radius:var(--universal-border-radius);padding:var(--universal-padding);margin:0;cursor:pointer;transition:background 0.3s}[type="checkbox"].modal+div .card .modal-close:before{display:block;content:'\00D7';color:var(--modal-close-color);position:relative;font-family:sans-serif;font-size:1.75rem;line-height:1;text-align:center}[type="checkbox"].modal+div .card .modal-close:hover,[type="checkbox"].modal+div .card .modal-close:focus{background:var(--modal-close-hover-color)}[type="checkbox"].modal:checked+div{display:flex;flex:0 1 auto;z-index:1200}[type="checkbox"].modal:checked+div .card .modal-close{z-index:1211}:root{--collapse-label-back-color:#111;--collapse-label-fore-color:#fafafa;--collapse-label-hover-back-color:#222;--collapse-selected-label-back-color:#444;--collapse-border-color:#aaa;--collapse-content-back-color:#212121;--collapse-selected-label-border-color:#0277bd}.collapse{width:calc(100% - 2 * var(--universal-margin));opacity:1;display:flex;flex-direction:column;margin:var(--universal-margin);border-radius:var(--universal-border-radius)}.collapse>[type="radio"],.collapse>[type="checkbox"]{height:1px;width:1px;margin:-1px;overflow:hidden;position:absolute;clip:rect(0 0 0 0);-webkit-clip-path:inset(100%);clip-path:inset(100%)}.collapse>label{flex-grow:1;display:inline-block;height:1.5rem;cursor:pointer;transition:background 0.3s;color:var(--collapse-label-fore-color);background:var(--collapse-label-back-color);border:.0625rem solid var(--collapse-border-color);padding:calc(1.5 * var(--universal-padding))}.collapse>label:hover,.collapse>label:focus{background:var(--collapse-label-hover-back-color)}.collapse>label+div{flex-basis:auto;height:1px;width:1px;margin:-1px;overflow:hidden;position:absolute;clip:rect(0 0 0 0);-webkit-clip-path:inset(100%);clip-path:inset(100%);transition:max-height 0.3s;max-height:1px}.collapse>:checked+label{background:var(--collapse-selected-label-back-color);border-bottom-color:var(--collapse-selected-label-border-color)}.collapse>:checked+label+div{box-sizing:border-box;position:relative;width:100%;height:auto;overflow:auto;margin:0;background:var(--collapse-content-back-color);border:.0625rem solid var(--collapse-border-color);border-top:0;padding:var(--universal-padding);clip:auto;-webkit-clip-path:inset(0%);clip-path:inset(0%);max-height:400px}.collapse>label:not(:first-of-type){border-top:0}.collapse>label:first-of-type{border-radius:var(--universal-border-radius) var(--universal-border-radius) 0 0}.collapse>label:last-of-type:not(:first-of-type){border-radius:0 0 var(--universal-border-radius) var(--universal-border-radius)}.collapse>label:last-of-type:first-of-type{border-radius:var(--universal-border-radius)}.collapse>:checked:last-of-type:not(:first-of-type)+label{border-radius:0}.collapse>:checked:last-of-type+label+div{border-radius:0 0 var(--universal-border-radius) var(--universal-border-radius)}mark.secondary{--mark-back-color:#d32f2f}mark.tertiary{--mark-back-color:#308732}mark.tag{padding:calc(var(--universal-padding)/2) var(--universal-padding);border-radius:1em}:root{--progress-back-color:#aaa;--progress-fore-color:#555}progress{display:block;vertical-align:baseline;-webkit-appearance:none;-moz-appearance:none;appearance:none;height:.75rem;width:calc(100% - 2 * var(--universal-margin));margin:var(--universal-margin);border:0;border-radius:calc(2 * var(--universal-border-radius));background:var(--progress-back-color);color:var(--progress-fore-color)}progress::-webkit-progress-value{background:var(--progress-fore-color);border-top-left-radius:calc(2 * var(--universal-border-radius));border-bottom-left-radius:calc(2 * var(--universal-border-radius))}progress::-webkit-progress-bar{background:var(#aaa)}progress::-moz-progress-bar{background:var(--progress-fore-color);border-top-left-radius:calc(2 * var(--universal-border-radius));border-bottom-left-radius:calc(2 * var(--universal-border-radius))}progress[value="1000"]::-webkit-progress-value{border-radius:calc(2 * var(--universal-border-radius))}progress[value="1000"]::-moz-progress-bar{border-radius:calc(2 * var(--universal-border-radius))}progress.inline{display:inline-block;vertical-align:middle;width:60%}:root{--spinner-back-color:#ddd;--spinner-fore-color:#555}@keyframes spinner-donut-anim{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}.spinner{display:inline-block;margin:var(--universal-margin);border:.25rem solid var(--spinner-back-color);border-left:.25rem solid var(--spinner-fore-color);border-radius:50%;width:1.25rem;height:1.25rem;animation:spinner-donut-anim 1.2s linear infinite}progress.primary{--progress-fore-color:#1976d2}progress.secondary{--progress-fore-color:#d32f2f}progress.tertiary{--progress-fore-color:#308732}.spinner.primary{--spinner-fore-color:#1976d2}.spinner.secondary{--spinner-fore-color:#d32f2f}.spinner.tertiary{--spinner-fore-color:#308732}span[class^='icon-']{display:inline-block;height:1em;width:1em;vertical-align:-0.125em;background-size:contain;margin:0 calc(var(--universal-margin) / 4)}span[class^='icon-'].secondary{-webkit-filter:invert(25%);filter:invert(25%)}span[class^='icon-'].inverse{-webkit-filter:invert(100%);filter:invert(100%)}span.icon-alert{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23fdfdfd' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12' y2='16'%3E%3C/line%3E%3C/svg%3E")}span.icon-bookmark{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23fdfdfd' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z'%3E%3C/path%3E%3C/svg%3E")}span.icon-calendar{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23fdfdfd' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2' ry='2'%3E%3C/rect%3E%3Cline x1='16' y1='2' x2='16' y2='6'%3E%3C/line%3E%3Cline x1='8' y1='2' x2='8' y2='6'%3E%3C/line%3E%3Cline x1='3' y1='10' x2='21' y2='10'%3E%3C/line%3E%3C/svg%3E")}span.icon-credit{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23fdfdfd' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='1' y='4' width='22' height='16' rx='2' ry='2'%3E%3C/rect%3E%3Cline x1='1' y1='10' x2='23' y2='10'%3E%3C/line%3E%3C/svg%3E")}span.icon-edit{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23fdfdfd' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 14.66V20a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h5.34'%3E%3C/path%3E%3Cpolygon points='18 2 22 6 12 16 8 16 8 12 18 2'%3E%3C/polygon%3E%3C/svg%3E")}span.icon-link{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23fdfdfd' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6'%3E%3C/path%3E%3Cpolyline points='15 3 21 3 21 9'%3E%3C/polyline%3E%3Cline x1='10' y1='14' x2='21' y2='3'%3E%3C/line%3E%3C/svg%3E")}span.icon-help{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23fdfdfd' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3'%3E%3C/path%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='17' x2='12' y2='17'%3E%3C/line%3E%3C/svg%3E")}span.icon-home{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23fdfdfd' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z'%3E%3C/path%3E%3Cpolyline points='9 22 9 12 15 12 15 22'%3E%3C/polyline%3E%3C/svg%3E")}span.icon-info{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23fdfdfd' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='16' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='8' x2='12' y2='8'%3E%3C/line%3E%3C/svg%3E")}span.icon-lock{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23fdfdfd' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='11' width='18' height='11' rx='2' ry='2'%3E%3C/rect%3E%3Cpath d='M7 11V7a5 5 0 0 1 10 0v4'%3E%3C/path%3E%3C/svg%3E")}span.icon-mail{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23fdfdfd' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z'%3E%3C/path%3E%3Cpolyline points='22,6 12,13 2,6'%3E%3C/polyline%3E%3C/svg%3E")}span.icon-location{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23fdfdfd' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z'%3E%3C/path%3E%3Ccircle cx='12' cy='10' r='3'%3E%3C/circle%3E%3C/svg%3E")}span.icon-phone{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23fdfdfd' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z'%3E%3C/path%3E%3C/svg%3E")}span.icon-rss{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23fdfdfd' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 11a9 9 0 0 1 9 9'%3E%3C/path%3E%3Cpath d='M4 4a16 16 0 0 1 16 16'%3E%3C/path%3E%3Ccircle cx='5' cy='19' r='1'%3E%3C/circle%3E%3C/svg%3E")}span.icon-search{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23fdfdfd' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'%3E%3C/circle%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'%3E%3C/line%3E%3C/svg%3E")}span.icon-settings{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23fdfdfd' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='3'%3E%3C/circle%3E%3Cpath d='M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z'%3E%3C/path%3E%3C/svg%3E")}span.icon-share{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23fdfdfd' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='18' cy='5' r='3'%3E%3C/circle%3E%3Ccircle cx='6' cy='12' r='3'%3E%3C/circle%3E%3Ccircle cx='18' cy='19' r='3'%3E%3C/circle%3E%3Cline x1='8.59' y1='13.51' x2='15.42' y2='17.49'%3E%3C/line%3E%3Cline x1='15.41' y1='6.51' x2='8.59' y2='10.49'%3E%3C/line%3E%3C/svg%3E")}span.icon-cart{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23fdfdfd' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='9' cy='21' r='1'%3E%3C/circle%3E%3Ccircle cx='20' cy='21' r='1'%3E%3C/circle%3E%3Cpath d='M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6'%3E%3C/path%3E%3C/svg%3E")}span.icon-upload{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23fdfdfd' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4'%3E%3C/path%3E%3Cpolyline points='17 8 12 3 7 8'%3E%3C/polyline%3E%3Cline x1='12' y1='3' x2='12' y2='15'%3E%3C/line%3E%3C/svg%3E")}span.icon-user{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23fdfdfd' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2'%3E%3C/path%3E%3Ccircle cx='12' cy='7' r='4'%3E%3C/circle%3E%3C/svg%3E")}:root{--generic-border-color:rgba(0,0,0,0.3);--generic-box-shadow:0 .25rem .25rem 0 rgba(0,0,0,0.125),0 .125rem .125rem -.125rem rgba(0,0,0,0.125)}.hidden{display:none !important}.visually-hidden{position:absolute !important;width:1px !important;height:1px !important;margin:-1px !important;border:0 !important;padding:0 !important;clip:rect(0 0 0 0) !important;-webkit-clip-path:inset(100%) !important;clip-path:inset(100%) !important;overflow:hidden !important}.bordered{border:.0625rem solid var(--generic-border-color) !important}.rounded{border-radius:var(--universal-border-radius) !important}.circular{border-radius:50% !important}.shadowed{box-shadow:var(--generic-box-shadow) !important}.responsive-margin{margin:calc(var(--universal-margin) / 4) !important}@media screen and (min-width: 768px){.responsive-margin{margin:calc(var(--universal-margin) / 2) !important}}@media screen and (min-width: 1280px){.responsive-margin{margin:var(--universal-margin) !important}}.responsive-padding{padding:calc(var(--universal-padding) / 4) !important}@media screen and (min-width: 768px){.responsive-padding{padding:calc(var(--universal-padding) / 2) !important}}@media screen and (min-width: 1280px){.responsive-padding{padding:var(--universal-padding) !important}}@media screen and (max-width: 767px){.hidden-sm{display:none !important}}@media screen and (min-width: 768px) and (max-width: 1279px){.hidden-md{display:none !important}}@media screen and (min-width: 1280px){.hidden-lg{display:none !important}}@media screen and (max-width: 767px){.visually-hidden-sm{position:absolute !important;width:1px !important;height:1px !important;margin:-1px !important;border:0 !important;padding:0 !important;clip:rect(0 0 0 0) !important;-webkit-clip-path:inset(100%) !important;clip-path:inset(100%) !important;overflow:hidden !important}}@media screen and (min-width: 768px) and (max-width: 1279px){.visually-hidden-md{position:absolute !important;width:1px !important;height:1px !important;margin:-1px !important;border:0 !important;padding:0 !important;clip:rect(0 0 0 0) !important;-webkit-clip-path:inset(100%) !important;clip-path:inset(100%) !important;overflow:hidden !important}}@media screen and (min-width: 1280px){.visually-hidden-lg{position:absolute !important;width:1px !important;height:1px !important;margin:-1px !important;border:0 !important;padding:0 !important;clip:rect(0 0 0 0) !important;-webkit-clip-path:inset(100%) !important;clip-path:inset(100%) !important;overflow:hidden !important}} diff --git a/server/website/css/mini-default.min.css b/server/website/css/mini-default.min.css new file mode 100644 index 0000000..bc8c5bc --- /dev/null +++ b/server/website/css/mini-default.min.css @@ -0,0 +1 @@ +:root{--fore-color:#111;--secondary-fore-color:#444;--back-color:#f8f8f8;--secondary-back-color:#f0f0f0;--blockquote-color:#f57c00;--pre-color:#1565c0;--border-color:#aaa;--secondary-border-color:#ddd;--heading-ratio:1.19;--universal-margin:.5rem;--universal-padding:.5rem;--universal-border-radius:.125rem;--a-link-color:#0277bd;--a-visited-color:#01579b}html{font-size:16px}a,b,del,em,i,ins,q,span,strong,u{font-size:1em}html,*{font-family:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, "Helvetica Neue", Helvetica, sans-serif;line-height:1.5;-webkit-text-size-adjust:100%}*{font-size:1rem}body{margin:0;color:var(--fore-color);background:var(--back-color)}details{display:block}summary{display:list-item}abbr[title]{border-bottom:none;text-decoration:underline dotted}input{overflow:visible}img{max-width:100%;height:auto}h1,h2,h3,h4,h5,h6{line-height:1.2;margin:calc(1.5 * var(--universal-margin)) var(--universal-margin);font-weight:500}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small{color:var(--secondary-fore-color);display:block;margin-top:-.25rem}h1{font-size:calc(1rem * var(--heading-ratio) * var(--heading-ratio) * var(--heading-ratio) * var(--heading-ratio))}h2{font-size:calc(1rem * var(--heading-ratio) * var(--heading-ratio) * var(--heading-ratio))}h3{font-size:calc(1rem * var(--heading-ratio) * var(--heading-ratio))}h4{font-size:calc(1rem * var(--heading-ratio))}h5{font-size:1rem}h6{font-size:calc(1rem / var(--heading-ratio))}p{margin:var(--universal-margin)}ol,ul{margin:var(--universal-margin);padding-left:calc(2 * var(--universal-margin))}b,strong{font-weight:700}hr{box-sizing:content-box;border:0;line-height:1.25em;margin:var(--universal-margin);height:.0625rem;background:linear-gradient(to right, transparent, var(--border-color) 20%, var(--border-color) 80%, transparent)}blockquote{display:block;position:relative;font-style:italic;color:var(--secondary-fore-color);margin:var(--universal-margin);padding:calc(3 * var(--universal-padding));border:.0625rem solid var(--secondary-border-color);border-left:.375rem solid var(--blockquote-color);border-radius:0 var(--universal-border-radius) var(--universal-border-radius) 0}blockquote:before{position:absolute;top:calc(0rem - var(--universal-padding));left:0;font-family:sans-serif;font-size:3rem;font-weight:700;content:"\201c";color:var(--blockquote-color)}blockquote[cite]:after{font-style:normal;font-size:.75em;font-weight:700;content:"\a— " attr(cite);white-space:pre}code,kbd,pre,samp{font-family:Menlo, Consolas, monospace;font-size:.85em}code{background:var(--secondary-back-color);border-radius:var(--universal-border-radius);padding:calc(var(--universal-padding) / 4) calc(var(--universal-padding) / 2)}kbd{background:var(--fore-color);color:var(--back-color);border-radius:var(--universal-border-radius);padding:calc(var(--universal-padding) / 4) calc(var(--universal-padding) / 2)}pre{overflow:auto;background:var(--secondary-back-color);padding:calc(1.5 * var(--universal-padding));margin:var(--universal-margin);border:.0625rem solid var(--secondary-border-color);border-left:.25rem solid var(--pre-color);border-radius:0 var(--universal-border-radius) var(--universal-border-radius) 0}sup,sub,code,kbd{line-height:0;position:relative;vertical-align:baseline}small,sup,sub,figcaption{font-size:.75em}sup{top:-.5em}sub{bottom:-.25em}figure{margin:var(--universal-margin)}figcaption{color:var(--secondary-fore-color)}a{text-decoration:none}a:link{color:var(--a-link-color)}a:visited{color:var(--a-visited-color)}a:hover,a:focus{text-decoration:underline}.container{margin:0 auto;padding:0 calc(1.5 * var(--universal-padding))}.row{box-sizing:border-box;display:flex;flex:0 1 auto;flex-flow:row wrap}.col-sm,[class^='col-sm-'],[class^='col-sm-offset-'],.row[class*='cols-sm-']>*{box-sizing:border-box;flex:0 0 auto;padding:0 calc(var(--universal-padding) / 2)}.col-sm,.row.cols-sm>*{max-width:100%;flex-grow:1;flex-basis:0}.col-sm-1,.row.cols-sm-1>*{max-width:8.33333%;flex-basis:8.33333%}.col-sm-offset-0{margin-left:0}.col-sm-2,.row.cols-sm-2>*{max-width:16.66667%;flex-basis:16.66667%}.col-sm-offset-1{margin-left:8.33333%}.col-sm-3,.row.cols-sm-3>*{max-width:25%;flex-basis:25%}.col-sm-offset-2{margin-left:16.66667%}.col-sm-4,.row.cols-sm-4>*{max-width:33.33333%;flex-basis:33.33333%}.col-sm-offset-3{margin-left:25%}.col-sm-5,.row.cols-sm-5>*{max-width:41.66667%;flex-basis:41.66667%}.col-sm-offset-4{margin-left:33.33333%}.col-sm-6,.row.cols-sm-6>*{max-width:50%;flex-basis:50%}.col-sm-offset-5{margin-left:41.66667%}.col-sm-7,.row.cols-sm-7>*{max-width:58.33333%;flex-basis:58.33333%}.col-sm-offset-6{margin-left:50%}.col-sm-8,.row.cols-sm-8>*{max-width:66.66667%;flex-basis:66.66667%}.col-sm-offset-7{margin-left:58.33333%}.col-sm-9,.row.cols-sm-9>*{max-width:75%;flex-basis:75%}.col-sm-offset-8{margin-left:66.66667%}.col-sm-10,.row.cols-sm-10>*{max-width:83.33333%;flex-basis:83.33333%}.col-sm-offset-9{margin-left:75%}.col-sm-11,.row.cols-sm-11>*{max-width:91.66667%;flex-basis:91.66667%}.col-sm-offset-10{margin-left:83.33333%}.col-sm-12,.row.cols-sm-12>*{max-width:100%;flex-basis:100%}.col-sm-offset-11{margin-left:91.66667%}.col-sm-normal{order:initial}.col-sm-first{order:-999}.col-sm-last{order:999}@media screen and (min-width: 768px){.col-md,[class^='col-md-'],[class^='col-md-offset-'],.row[class*='cols-md-']>*{box-sizing:border-box;flex:0 0 auto;padding:0 calc(var(--universal-padding) / 2)}.col-md,.row.cols-md>*{max-width:100%;flex-grow:1;flex-basis:0}.col-md-1,.row.cols-md-1>*{max-width:8.33333%;flex-basis:8.33333%}.col-md-offset-0{margin-left:0}.col-md-2,.row.cols-md-2>*{max-width:16.66667%;flex-basis:16.66667%}.col-md-offset-1{margin-left:8.33333%}.col-md-3,.row.cols-md-3>*{max-width:25%;flex-basis:25%}.col-md-offset-2{margin-left:16.66667%}.col-md-4,.row.cols-md-4>*{max-width:33.33333%;flex-basis:33.33333%}.col-md-offset-3{margin-left:25%}.col-md-5,.row.cols-md-5>*{max-width:41.66667%;flex-basis:41.66667%}.col-md-offset-4{margin-left:33.33333%}.col-md-6,.row.cols-md-6>*{max-width:50%;flex-basis:50%}.col-md-offset-5{margin-left:41.66667%}.col-md-7,.row.cols-md-7>*{max-width:58.33333%;flex-basis:58.33333%}.col-md-offset-6{margin-left:50%}.col-md-8,.row.cols-md-8>*{max-width:66.66667%;flex-basis:66.66667%}.col-md-offset-7{margin-left:58.33333%}.col-md-9,.row.cols-md-9>*{max-width:75%;flex-basis:75%}.col-md-offset-8{margin-left:66.66667%}.col-md-10,.row.cols-md-10>*{max-width:83.33333%;flex-basis:83.33333%}.col-md-offset-9{margin-left:75%}.col-md-11,.row.cols-md-11>*{max-width:91.66667%;flex-basis:91.66667%}.col-md-offset-10{margin-left:83.33333%}.col-md-12,.row.cols-md-12>*{max-width:100%;flex-basis:100%}.col-md-offset-11{margin-left:91.66667%}.col-md-normal{order:initial}.col-md-first{order:-999}.col-md-last{order:999}}@media screen and (min-width: 1280px){.col-lg,[class^='col-lg-'],[class^='col-lg-offset-'],.row[class*='cols-lg-']>*{box-sizing:border-box;flex:0 0 auto;padding:0 calc(var(--universal-padding) / 2)}.col-lg,.row.cols-lg>*{max-width:100%;flex-grow:1;flex-basis:0}.col-lg-1,.row.cols-lg-1>*{max-width:8.33333%;flex-basis:8.33333%}.col-lg-offset-0{margin-left:0}.col-lg-2,.row.cols-lg-2>*{max-width:16.66667%;flex-basis:16.66667%}.col-lg-offset-1{margin-left:8.33333%}.col-lg-3,.row.cols-lg-3>*{max-width:25%;flex-basis:25%}.col-lg-offset-2{margin-left:16.66667%}.col-lg-4,.row.cols-lg-4>*{max-width:33.33333%;flex-basis:33.33333%}.col-lg-offset-3{margin-left:25%}.col-lg-5,.row.cols-lg-5>*{max-width:41.66667%;flex-basis:41.66667%}.col-lg-offset-4{margin-left:33.33333%}.col-lg-6,.row.cols-lg-6>*{max-width:50%;flex-basis:50%}.col-lg-offset-5{margin-left:41.66667%}.col-lg-7,.row.cols-lg-7>*{max-width:58.33333%;flex-basis:58.33333%}.col-lg-offset-6{margin-left:50%}.col-lg-8,.row.cols-lg-8>*{max-width:66.66667%;flex-basis:66.66667%}.col-lg-offset-7{margin-left:58.33333%}.col-lg-9,.row.cols-lg-9>*{max-width:75%;flex-basis:75%}.col-lg-offset-8{margin-left:66.66667%}.col-lg-10,.row.cols-lg-10>*{max-width:83.33333%;flex-basis:83.33333%}.col-lg-offset-9{margin-left:75%}.col-lg-11,.row.cols-lg-11>*{max-width:91.66667%;flex-basis:91.66667%}.col-lg-offset-10{margin-left:83.33333%}.col-lg-12,.row.cols-lg-12>*{max-width:100%;flex-basis:100%}.col-lg-offset-11{margin-left:91.66667%}.col-lg-normal{order:initial}.col-lg-first{order:-999}.col-lg-last{order:999}}:root{--card-back-color:#f8f8f8;--card-fore-color:#111;--card-border-color:#ddd}.card{display:flex;flex-direction:column;justify-content:space-between;align-self:center;position:relative;width:100%;background:var(--card-back-color);color:var(--card-fore-color);border:.0625rem solid var(--card-border-color);border-radius:var(--universal-border-radius);margin:var(--universal-margin);overflow:hidden}@media screen and (min-width: 320px){.card{max-width:320px}}.card>.section{background:var(--card-back-color);color:var(--card-fore-color);box-sizing:border-box;margin:0;border:0;border-radius:0;border-bottom:.0625rem solid var(--card-border-color);padding:var(--universal-padding);width:100%}.card>.section.media{height:200px;padding:0;-o-object-fit:cover;object-fit:cover}.card>.section:last-child{border-bottom:0}@media screen and (min-width: 240px){.card.small{max-width:240px}}@media screen and (min-width: 480px){.card.large{max-width:480px}}.card.fluid{max-width:100%;width:auto}.card.warning{--card-back-color:#ffca28;--card-border-color:#e8b825}.card.error{--card-back-color:#b71c1c;--card-fore-color:#f8f8f8;--card-border-color:#a71a1a}.card>.section.dark{--card-back-color:#e0e0e0}.card>.section.double-padded{padding:calc(1.5 * var(--universal-padding))}:root{--form-back-color:#f0f0f0;--form-fore-color:#111;--form-border-color:#ddd;--input-back-color:#f8f8f8;--input-fore-color:#111;--input-border-color:#ddd;--input-focus-color:#0288d1;--input-invalid-color:#d32f2f;--button-back-color:#e2e2e2;--button-hover-back-color:#dcdcdc;--button-fore-color:#212121;--button-border-color:transparent;--button-hover-border-color:transparent;--button-group-border-color:rgba(124,124,124,0.54)}form{background:var(--form-back-color);color:var(--form-fore-color);border:.0625rem solid var(--form-border-color);border-radius:var(--universal-border-radius);margin:var(--universal-margin);padding:calc(2 * var(--universal-padding)) var(--universal-padding)}fieldset{border:.0625rem solid var(--form-border-color);border-radius:var(--universal-border-radius);margin:calc(var(--universal-margin) / 4);padding:var(--universal-padding)}legend{box-sizing:border-box;display:table;max-width:100%;white-space:normal;font-weight:700;padding:calc(var(--universal-padding) / 2)}label{padding:calc(var(--universal-padding) / 2) var(--universal-padding)}.input-group{display:inline-block}.input-group.fluid{display:flex;align-items:center;justify-content:center}.input-group.fluid>input{max-width:100%;flex-grow:1;flex-basis:0px}@media screen and (max-width: 767px){.input-group.fluid{align-items:stretch;flex-direction:column}}.input-group.vertical{display:flex;align-items:stretch;flex-direction:column}.input-group.vertical>input{max-width:100%;flex-grow:1;flex-basis:0px}[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}input:not([type]),[type="text"],[type="email"],[type="number"],[type="search"],[type="password"],[type="url"],[type="tel"],[type="checkbox"],[type="radio"],textarea,select{box-sizing:border-box;background:var(--input-back-color);color:var(--input-fore-color);border:.0625rem solid var(--input-border-color);border-radius:var(--universal-border-radius);margin:calc(var(--universal-margin) / 2);padding:var(--universal-padding) calc(1.5 * var(--universal-padding))}input:not([type="button"]):not([type="submit"]):not([type="reset"]):hover,input:not([type="button"]):not([type="submit"]):not([type="reset"]):focus,textarea:hover,textarea:focus,select:hover,select:focus{border-color:var(--input-focus-color);box-shadow:none}input:not([type="button"]):not([type="submit"]):not([type="reset"]):invalid,input:not([type="button"]):not([type="submit"]):not([type="reset"]):focus:invalid,textarea:invalid,textarea:focus:invalid,select:invalid,select:focus:invalid{border-color:var(--input-invalid-color);box-shadow:none}input:not([type="button"]):not([type="submit"]):not([type="reset"])[readonly],textarea[readonly],select[readonly]{background:var(--secondary-back-color)}select{max-width:100%}option{overflow:hidden;text-overflow:ellipsis}[type="checkbox"],[type="radio"]{-webkit-appearance:none;-moz-appearance:none;appearance:none;position:relative;height:calc(1rem + var(--universal-padding) / 2);width:calc(1rem + var(--universal-padding) / 2);vertical-align:text-bottom;padding:0;flex-basis:calc(1rem + var(--universal-padding) / 2) !important;flex-grow:0 !important}[type="checkbox"]:checked:before,[type="radio"]:checked:before{position:absolute}[type="checkbox"]:checked:before{content:'\2713';font-family:sans-serif;font-size:calc(1rem + var(--universal-padding) / 2);top:calc(0rem - var(--universal-padding));left:calc(var(--universal-padding) / 4)}[type="radio"]{border-radius:100%}[type="radio"]:checked:before{border-radius:100%;content:'';top:calc(.0625rem + var(--universal-padding) / 2);left:calc(.0625rem + var(--universal-padding) / 2);background:var(--input-fore-color);width:0.5rem;height:0.5rem}:placeholder-shown{color:var(--input-fore-color)}::-ms-placeholder{color:var(--input-fore-color);opacity:0.54}button::-moz-focus-inner,[type="button"]::-moz-focus-inner,[type="reset"]::-moz-focus-inner,[type="submit"]::-moz-focus-inner{border-style:none;padding:0}button,html [type="button"],[type="reset"],[type="submit"]{-webkit-appearance:button}button{overflow:visible;text-transform:none}button,[type="button"],[type="submit"],[type="reset"],a.button,label.button,.button,a[role="button"],label[role="button"],[role="button"]{display:inline-block;background:var(--button-back-color);color:var(--button-fore-color);border:.0625rem solid var(--button-border-color);border-radius:var(--universal-border-radius);padding:var(--universal-padding) calc(1.5 * var(--universal-padding));margin:var(--universal-margin);text-decoration:none;cursor:pointer;transition:background 0.3s}button:hover,button:focus,[type="button"]:hover,[type="button"]:focus,[type="submit"]:hover,[type="submit"]:focus,[type="reset"]:hover,[type="reset"]:focus,a.button:hover,a.button:focus,label.button:hover,label.button:focus,.button:hover,.button:focus,a[role="button"]:hover,a[role="button"]:focus,label[role="button"]:hover,label[role="button"]:focus,[role="button"]:hover,[role="button"]:focus{background:var(--button-hover-back-color);border-color:var(--button-hover-border-color)}input:disabled,input[disabled],textarea:disabled,textarea[disabled],select:disabled,select[disabled],button:disabled,button[disabled],.button:disabled,.button[disabled],[role="button"]:disabled,[role="button"][disabled]{cursor:not-allowed;opacity:.75}.button-group{display:flex;border:.0625rem solid var(--button-group-border-color);border-radius:var(--universal-border-radius);margin:var(--universal-margin)}.button-group>button,.button-group [type="button"],.button-group>[type="submit"],.button-group>[type="reset"],.button-group>.button,.button-group>[role="button"]{margin:0;max-width:100%;flex:1 1 auto;text-align:center;border:0;border-radius:0;box-shadow:none}.button-group>:not(:first-child){border-left:.0625rem solid var(--button-group-border-color)}@media screen and (max-width: 767px){.button-group{flex-direction:column}.button-group>:not(:first-child){border:0;border-top:.0625rem solid var(--button-group-border-color)}}button.primary,[type="button"].primary,[type="submit"].primary,[type="reset"].primary,.button.primary,[role="button"].primary{--button-back-color:#1976d2;--button-fore-color:#f8f8f8}button.primary:hover,button.primary:focus,[type="button"].primary:hover,[type="button"].primary:focus,[type="submit"].primary:hover,[type="submit"].primary:focus,[type="reset"].primary:hover,[type="reset"].primary:focus,.button.primary:hover,.button.primary:focus,[role="button"].primary:hover,[role="button"].primary:focus{--button-hover-back-color:#1565c0}button.secondary,[type="button"].secondary,[type="submit"].secondary,[type="reset"].secondary,.button.secondary,[role="button"].secondary{--button-back-color:#d32f2f;--button-fore-color:#f8f8f8}button.secondary:hover,button.secondary:focus,[type="button"].secondary:hover,[type="button"].secondary:focus,[type="submit"].secondary:hover,[type="submit"].secondary:focus,[type="reset"].secondary:hover,[type="reset"].secondary:focus,.button.secondary:hover,.button.secondary:focus,[role="button"].secondary:hover,[role="button"].secondary:focus{--button-hover-back-color:#c62828}button.tertiary,[type="button"].tertiary,[type="submit"].tertiary,[type="reset"].tertiary,.button.tertiary,[role="button"].tertiary{--button-back-color:#308732;--button-fore-color:#f8f8f8}button.tertiary:hover,button.tertiary:focus,[type="button"].tertiary:hover,[type="button"].tertiary:focus,[type="submit"].tertiary:hover,[type="submit"].tertiary:focus,[type="reset"].tertiary:hover,[type="reset"].tertiary:focus,.button.tertiary:hover,.button.tertiary:focus,[role="button"].tertiary:hover,[role="button"].tertiary:focus{--button-hover-back-color:#277529}button.inverse,[type="button"].inverse,[type="submit"].inverse,[type="reset"].inverse,.button.inverse,[role="button"].inverse{--button-back-color:#212121;--button-fore-color:#f8f8f8}button.inverse:hover,button.inverse:focus,[type="button"].inverse:hover,[type="button"].inverse:focus,[type="submit"].inverse:hover,[type="submit"].inverse:focus,[type="reset"].inverse:hover,[type="reset"].inverse:focus,.button.inverse:hover,.button.inverse:focus,[role="button"].inverse:hover,[role="button"].inverse:focus{--button-hover-back-color:#111}button.small,[type="button"].small,[type="submit"].small,[type="reset"].small,.button.small,[role="button"].small{padding:calc(0.5 * var(--universal-padding)) calc(0.75 * var(--universal-padding));margin:var(--universal-margin)}button.large,[type="button"].large,[type="submit"].large,[type="reset"].large,.button.large,[role="button"].large{padding:calc(1.5 * var(--universal-padding)) calc(2 * var(--universal-padding));margin:var(--universal-margin)}:root{--header-back-color:#f8f8f8;--header-hover-back-color:#f0f0f0;--header-fore-color:#444;--header-border-color:#ddd;--nav-back-color:#f8f8f8;--nav-hover-back-color:#f0f0f0;--nav-fore-color:#444;--nav-border-color:#ddd;--nav-link-color:#0277bd;--footer-fore-color:#444;--footer-back-color:#f8f8f8;--footer-border-color:#ddd;--footer-link-color:#0277bd;--drawer-back-color:#f8f8f8;--drawer-hover-back-color:#f0f0f0;--drawer-border-color:#ddd;--drawer-close-color:#444}header{height:3.1875rem;background:var(--header-back-color);color:var(--header-fore-color);border-bottom:.0625rem solid var(--header-border-color);padding:calc(var(--universal-padding) / 4) 0;white-space:nowrap;overflow-x:auto;overflow-y:hidden}header.row{box-sizing:content-box}header .logo{color:var(--header-fore-color);font-size:1.75rem;padding:var(--universal-padding) calc(2 * var(--universal-padding));text-decoration:none}header button,header [type="button"],header .button,header [role="button"]{box-sizing:border-box;position:relative;top:calc(0rem - var(--universal-padding) / 4);height:calc(3.1875rem + var(--universal-padding) / 2);background:var(--header-back-color);line-height:calc(3.1875rem - var(--universal-padding) * 1.5);text-align:center;color:var(--header-fore-color);border:0;border-radius:0;margin:0;text-transform:uppercase}header button:hover,header button:focus,header [type="button"]:hover,header [type="button"]:focus,header .button:hover,header .button:focus,header [role="button"]:hover,header [role="button"]:focus{background:var(--header-hover-back-color)}nav{background:var(--nav-back-color);color:var(--nav-fore-color);border:.0625rem solid var(--nav-border-color);border-radius:var(--universal-border-radius);margin:var(--universal-margin)}nav *{padding:var(--universal-padding) calc(1.5 * var(--universal-padding))}nav a,nav a:visited{display:block;color:var(--nav-link-color);border-radius:var(--universal-border-radius);transition:background 0.3s}nav a:hover,nav a:focus,nav a:visited:hover,nav a:visited:focus{text-decoration:none;background:var(--nav-hover-back-color)}nav .sublink-1{position:relative;margin-left:calc(2 * var(--universal-padding))}nav .sublink-1:before{position:absolute;left:calc(var(--universal-padding) - 1 * var(--universal-padding));top:-.0625rem;content:'';height:100%;border:.0625rem solid var(--nav-border-color);border-left:0}nav .sublink-2{position:relative;margin-left:calc(4 * var(--universal-padding))}nav .sublink-2:before{position:absolute;left:calc(var(--universal-padding) - 3 * var(--universal-padding));top:-.0625rem;content:'';height:100%;border:.0625rem solid var(--nav-border-color);border-left:0}footer{background:var(--footer-back-color);color:var(--footer-fore-color);border-top:.0625rem solid var(--footer-border-color);padding:calc(2 * var(--universal-padding)) var(--universal-padding);font-size:.875rem}footer a,footer a:visited{color:var(--footer-link-color)}header.sticky{position:-webkit-sticky;position:sticky;z-index:1101;top:0}footer.sticky{position:-webkit-sticky;position:sticky;z-index:1101;bottom:0}.drawer-toggle:before{display:inline-block;position:relative;vertical-align:bottom;content:'\00a0\2261\00a0';font-family:sans-serif;font-size:1.5em}@media screen and (min-width: 768px){.drawer-toggle:not(.persistent){display:none}}[type="checkbox"].drawer{height:1px;width:1px;margin:-1px;overflow:hidden;position:absolute;clip:rect(0 0 0 0);-webkit-clip-path:inset(100%);clip-path:inset(100%)}[type="checkbox"].drawer+*{display:block;box-sizing:border-box;position:fixed;top:0;width:320px;height:100vh;overflow-y:auto;background:var(--drawer-back-color);border:.0625rem solid var(--drawer-border-color);border-radius:0;margin:0;z-index:1110;right:-320px;transition:right 0.3s}[type="checkbox"].drawer+* .drawer-close{position:absolute;top:var(--universal-margin);right:var(--universal-margin);z-index:1111;width:2rem;height:2rem;border-radius:var(--universal-border-radius);padding:var(--universal-padding);margin:0;cursor:pointer;transition:background 0.3s}[type="checkbox"].drawer+* .drawer-close:before{display:block;content:'\00D7';color:var(--drawer-close-color);position:relative;font-family:sans-serif;font-size:2rem;line-height:1;text-align:center}[type="checkbox"].drawer+* .drawer-close:hover,[type="checkbox"].drawer+* .drawer-close:focus{background:var(--drawer-hover-back-color)}@media screen and (max-width: 320px){[type="checkbox"].drawer+*{width:100%}}[type="checkbox"].drawer:checked+*{right:0}@media screen and (min-width: 768px){[type="checkbox"].drawer:not(.persistent)+*{position:static;height:100%;z-index:1100}[type="checkbox"].drawer:not(.persistent)+* .drawer-close{display:none}}:root{--table-border-color:#aaa;--table-border-separator-color:#666;--table-head-back-color:#e6e6e6;--table-head-fore-color:#111;--table-body-back-color:#f8f8f8;--table-body-fore-color:#111;--table-body-alt-back-color:#eee}table{border-collapse:separate;border-spacing:0;margin:0;display:flex;flex:0 1 auto;flex-flow:row wrap;padding:var(--universal-padding);padding-top:0}table caption{font-size:1.5rem;margin:calc(2 * var(--universal-margin)) 0;max-width:100%;flex:0 0 100%}table thead,table tbody{display:flex;flex-flow:row wrap;border:.0625rem solid var(--table-border-color)}table thead{z-index:999;border-radius:var(--universal-border-radius) var(--universal-border-radius) 0 0;border-bottom:.0625rem solid var(--table-border-separator-color)}table tbody{border-top:0;margin-top:calc(0 - var(--universal-margin));border-radius:0 0 var(--universal-border-radius) var(--universal-border-radius)}table tr{display:flex;padding:0}table th,table td{padding:calc(2 * var(--universal-padding))}table th{text-align:left;background:var(--table-head-back-color);color:var(--table-head-fore-color)}table td{background:var(--table-body-back-color);color:var(--table-body-fore-color);border-top:.0625rem solid var(--table-border-color)}table:not(.horizontal){overflow:auto;max-height:400px}table:not(.horizontal) thead,table:not(.horizontal) tbody{max-width:100%;flex:0 0 100%}table:not(.horizontal) tr{flex-flow:row wrap;flex:0 0 100%}table:not(.horizontal) th,table:not(.horizontal) td{flex:1 0 0%;overflow:hidden;text-overflow:ellipsis}table:not(.horizontal) thead{position:sticky;top:0}table:not(.horizontal) tbody tr:first-child td{border-top:0}table.horizontal{border:0}table.horizontal thead,table.horizontal tbody{border:0;flex-flow:row nowrap}table.horizontal tbody{overflow:auto;justify-content:space-between;flex:1 0 0;margin-left:calc( 4 * var(--universal-margin));padding-bottom:calc(var(--universal-padding) / 4)}table.horizontal tr{flex-direction:column;flex:1 0 auto}table.horizontal th,table.horizontal td{width:100%;border:0;border-bottom:.0625rem solid var(--table-border-color)}table.horizontal th:not(:first-child),table.horizontal td:not(:first-child){border-top:0}table.horizontal th{text-align:right;border-left:.0625rem solid var(--table-border-color);border-right:.0625rem solid var(--table-border-separator-color)}table.horizontal thead tr:first-child{padding-left:0}table.horizontal th:first-child,table.horizontal td:first-child{border-top:.0625rem solid var(--table-border-color)}table.horizontal tbody tr:last-child td{border-right:.0625rem solid var(--table-border-color)}table.horizontal tbody tr:last-child td:first-child{border-top-right-radius:0.25rem}table.horizontal tbody tr:last-child td:last-child{border-bottom-right-radius:0.25rem}table.horizontal thead tr:first-child th:first-child{border-top-left-radius:0.25rem}table.horizontal thead tr:first-child th:last-child{border-bottom-left-radius:0.25rem}@media screen and (max-width: 767px){table,table.horizontal{border-collapse:collapse;border:0;width:100%;display:table}table thead,table th,table.horizontal thead,table.horizontal th{border:0;height:1px;width:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;clip:rect(0 0 0 0);-webkit-clip-path:inset(100%);clip-path:inset(100%)}table tbody,table.horizontal tbody{border:0;display:table-row-group}table tr,table.horizontal tr{display:block;border:.0625rem solid var(--table-border-color);border-radius:var(--universal-border-radius);background:#fafafa;padding:var(--universal-padding);margin:var(--universal-margin);margin-bottom:calc(2 * var(--universal-margin))}table th,table td,table.horizontal th,table.horizontal td{width:auto}table td,table.horizontal td{display:block;border:0;text-align:right}table td:before,table.horizontal td:before{content:attr(data-label);float:left;font-weight:600}table th:first-child,table td:first-child,table.horizontal th:first-child,table.horizontal td:first-child{border-top:0}table tbody tr:last-child td,table.horizontal tbody tr:last-child td{border-right:0}}:root{--table-body-alt-back-color:#eee}table.striped tr:nth-of-type(2n)>td{background:var(--table-body-alt-back-color)}@media screen and (max-width: 768px){table.striped tr:nth-of-type(2n){background:var(--table-body-alt-back-color)}}:root{--table-body-hover-back-color:#90caf9}table.hoverable tr:hover,table.hoverable tr:hover>td,table.hoverable tr:focus,table.hoverable tr:focus>td{background:var(--table-body-hover-back-color)}@media screen and (max-width: 768px){table.hoverable tr:hover,table.hoverable tr:hover>td,table.hoverable tr:focus,table.hoverable tr:focus>td{background:var(--table-body-hover-back-color)}}:root{--mark-back-color:#0277bd;--mark-fore-color:#fafafa}mark{background:var(--mark-back-color);color:var(--mark-fore-color);font-size:.95em;line-height:1em;border-radius:var(--universal-border-radius);padding:calc(var(--universal-padding) / 4) calc(var(--universal-padding) / 2)}mark.inline-block{display:inline-block;font-size:1em;line-height:1.5;padding:calc(var(--universal-padding) / 2) var(--universal-padding)}:root{--toast-back-color:#424242;--toast-fore-color:#fafafa}.toast{position:fixed;bottom:calc(var(--universal-margin) * 3);left:50%;transform:translate(-50%, -50%);z-index:1111;color:var(--toast-fore-color);background:var(--toast-back-color);border-radius:calc(var(--universal-border-radius) * 16);padding:var(--universal-padding) calc(var(--universal-padding) * 3)}:root{--tooltip-back-color:#212121;--tooltip-fore-color:#fafafa}.tooltip{position:relative;display:inline-block}.tooltip:before,.tooltip:after{position:absolute;opacity:0;clip:rect(0 0 0 0);-webkit-clip-path:inset(100%);clip-path:inset(100%);transition:all 0.3s;z-index:1010;left:50%}.tooltip:not(.bottom):before,.tooltip:not(.bottom):after{bottom:75%}.tooltip.bottom:before,.tooltip.bottom:after{top:75%}.tooltip:hover:before,.tooltip:hover:after,.tooltip:focus:before,.tooltip:focus:after{opacity:1;clip:auto;-webkit-clip-path:inset(0%);clip-path:inset(0%)}.tooltip:before{content:'';background:transparent;border:var(--universal-margin) solid transparent;left:calc(50% - var(--universal-margin))}.tooltip:not(.bottom):before{border-top-color:#212121}.tooltip.bottom:before{border-bottom-color:#212121}.tooltip:after{content:attr(aria-label);color:var(--tooltip-fore-color);background:var(--tooltip-back-color);border-radius:var(--universal-border-radius);padding:var(--universal-padding);white-space:nowrap;transform:translateX(-50%)}.tooltip:not(.bottom):after{margin-bottom:calc(2 * var(--universal-margin))}.tooltip.bottom:after{margin-top:calc(2 * var(--universal-margin))}:root{--modal-overlay-color:rgba(0,0,0,0.45);--modal-close-color:#444;--modal-close-hover-color:#f0f0f0}[type="checkbox"].modal{height:1px;width:1px;margin:-1px;overflow:hidden;position:absolute;clip:rect(0 0 0 0);-webkit-clip-path:inset(100%);clip-path:inset(100%)}[type="checkbox"].modal+div{position:fixed;top:0;left:0;display:none;width:100vw;height:100vh;background:var(--modal-overlay-color)}[type="checkbox"].modal+div .card{margin:0 auto;max-height:50vh;overflow:auto}[type="checkbox"].modal+div .card .modal-close{position:absolute;top:0;right:0;width:1.75rem;height:1.75rem;border-radius:var(--universal-border-radius);padding:var(--universal-padding);margin:0;cursor:pointer;transition:background 0.3s}[type="checkbox"].modal+div .card .modal-close:before{display:block;content:'\00D7';color:var(--modal-close-color);position:relative;font-family:sans-serif;font-size:1.75rem;line-height:1;text-align:center}[type="checkbox"].modal+div .card .modal-close:hover,[type="checkbox"].modal+div .card .modal-close:focus{background:var(--modal-close-hover-color)}[type="checkbox"].modal:checked+div{display:flex;flex:0 1 auto;z-index:1200}[type="checkbox"].modal:checked+div .card .modal-close{z-index:1211}:root{--collapse-label-back-color:#e8e8e8;--collapse-label-fore-color:#212121;--collapse-label-hover-back-color:#f0f0f0;--collapse-selected-label-back-color:#ececec;--collapse-border-color:#ddd;--collapse-content-back-color:#fafafa;--collapse-selected-label-border-color:#0277bd}.collapse{width:calc(100% - 2 * var(--universal-margin));opacity:1;display:flex;flex-direction:column;margin:var(--universal-margin);border-radius:var(--universal-border-radius)}.collapse>[type="radio"],.collapse>[type="checkbox"]{height:1px;width:1px;margin:-1px;overflow:hidden;position:absolute;clip:rect(0 0 0 0);-webkit-clip-path:inset(100%);clip-path:inset(100%)}.collapse>label{flex-grow:1;display:inline-block;height:1.5rem;cursor:pointer;transition:background 0.3s;color:var(--collapse-label-fore-color);background:var(--collapse-label-back-color);border:.0625rem solid var(--collapse-border-color);padding:calc(1.5 * var(--universal-padding))}.collapse>label:hover,.collapse>label:focus{background:var(--collapse-label-hover-back-color)}.collapse>label+div{flex-basis:auto;height:1px;width:1px;margin:-1px;overflow:hidden;position:absolute;clip:rect(0 0 0 0);-webkit-clip-path:inset(100%);clip-path:inset(100%);transition:max-height 0.3s;max-height:1px}.collapse>:checked+label{background:var(--collapse-selected-label-back-color);border-bottom-color:var(--collapse-selected-label-border-color)}.collapse>:checked+label+div{box-sizing:border-box;position:relative;width:100%;height:auto;overflow:auto;margin:0;background:var(--collapse-content-back-color);border:.0625rem solid var(--collapse-border-color);border-top:0;padding:var(--universal-padding);clip:auto;-webkit-clip-path:inset(0%);clip-path:inset(0%);max-height:400px}.collapse>label:not(:first-of-type){border-top:0}.collapse>label:first-of-type{border-radius:var(--universal-border-radius) var(--universal-border-radius) 0 0}.collapse>label:last-of-type:not(:first-of-type){border-radius:0 0 var(--universal-border-radius) var(--universal-border-radius)}.collapse>label:last-of-type:first-of-type{border-radius:var(--universal-border-radius)}.collapse>:checked:last-of-type:not(:first-of-type)+label{border-radius:0}.collapse>:checked:last-of-type+label+div{border-radius:0 0 var(--universal-border-radius) var(--universal-border-radius)}mark.secondary{--mark-back-color:#d32f2f}mark.tertiary{--mark-back-color:#308732}mark.tag{padding:calc(var(--universal-padding)/2) var(--universal-padding);border-radius:1em}:root{--progress-back-color:#ddd;--progress-fore-color:#555}progress{display:block;vertical-align:baseline;-webkit-appearance:none;-moz-appearance:none;appearance:none;height:.75rem;width:calc(100% - 2 * var(--universal-margin));margin:var(--universal-margin);border:0;border-radius:calc(2 * var(--universal-border-radius));background:var(--progress-back-color);color:var(--progress-fore-color)}progress::-webkit-progress-value{background:var(--progress-fore-color);border-top-left-radius:calc(2 * var(--universal-border-radius));border-bottom-left-radius:calc(2 * var(--universal-border-radius))}progress::-webkit-progress-bar{background:var(#ddd)}progress::-moz-progress-bar{background:var(--progress-fore-color);border-top-left-radius:calc(2 * var(--universal-border-radius));border-bottom-left-radius:calc(2 * var(--universal-border-radius))}progress[value="1000"]::-webkit-progress-value{border-radius:calc(2 * var(--universal-border-radius))}progress[value="1000"]::-moz-progress-bar{border-radius:calc(2 * var(--universal-border-radius))}progress.inline{display:inline-block;vertical-align:middle;width:60%}:root{--spinner-back-color:#ddd;--spinner-fore-color:#555}@keyframes spinner-donut-anim{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}.spinner{display:inline-block;margin:var(--universal-margin);border:.25rem solid var(--spinner-back-color);border-left:.25rem solid var(--spinner-fore-color);border-radius:50%;width:1.25rem;height:1.25rem;animation:spinner-donut-anim 1.2s linear infinite}progress.primary{--progress-fore-color:#1976d2}progress.secondary{--progress-fore-color:#d32f2f}progress.tertiary{--progress-fore-color:#308732}.spinner.primary{--spinner-fore-color:#1976d2}.spinner.secondary{--spinner-fore-color:#d32f2f}.spinner.tertiary{--spinner-fore-color:#308732}span[class^='icon-']{display:inline-block;height:1em;width:1em;vertical-align:-0.125em;background-size:contain;margin:0 calc(var(--universal-margin) / 4)}span[class^='icon-'].secondary{-webkit-filter:invert(25%);filter:invert(25%)}span[class^='icon-'].inverse{-webkit-filter:invert(100%);filter:invert(100%)}span.icon-alert{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23111' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12' y2='16'%3E%3C/line%3E%3C/svg%3E")}span.icon-bookmark{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23111' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z'%3E%3C/path%3E%3C/svg%3E")}span.icon-calendar{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23111' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2' ry='2'%3E%3C/rect%3E%3Cline x1='16' y1='2' x2='16' y2='6'%3E%3C/line%3E%3Cline x1='8' y1='2' x2='8' y2='6'%3E%3C/line%3E%3Cline x1='3' y1='10' x2='21' y2='10'%3E%3C/line%3E%3C/svg%3E")}span.icon-credit{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23111' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='1' y='4' width='22' height='16' rx='2' ry='2'%3E%3C/rect%3E%3Cline x1='1' y1='10' x2='23' y2='10'%3E%3C/line%3E%3C/svg%3E")}span.icon-edit{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23111' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 14.66V20a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h5.34'%3E%3C/path%3E%3Cpolygon points='18 2 22 6 12 16 8 16 8 12 18 2'%3E%3C/polygon%3E%3C/svg%3E")}span.icon-link{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23111' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6'%3E%3C/path%3E%3Cpolyline points='15 3 21 3 21 9'%3E%3C/polyline%3E%3Cline x1='10' y1='14' x2='21' y2='3'%3E%3C/line%3E%3C/svg%3E")}span.icon-help{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23111' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3'%3E%3C/path%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='17' x2='12' y2='17'%3E%3C/line%3E%3C/svg%3E")}span.icon-home{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23111' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z'%3E%3C/path%3E%3Cpolyline points='9 22 9 12 15 12 15 22'%3E%3C/polyline%3E%3C/svg%3E")}span.icon-info{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23111' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='16' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='8' x2='12' y2='8'%3E%3C/line%3E%3C/svg%3E")}span.icon-lock{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23111' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='11' width='18' height='11' rx='2' ry='2'%3E%3C/rect%3E%3Cpath d='M7 11V7a5 5 0 0 1 10 0v4'%3E%3C/path%3E%3C/svg%3E")}span.icon-mail{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23111' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z'%3E%3C/path%3E%3Cpolyline points='22,6 12,13 2,6'%3E%3C/polyline%3E%3C/svg%3E")}span.icon-location{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23111' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z'%3E%3C/path%3E%3Ccircle cx='12' cy='10' r='3'%3E%3C/circle%3E%3C/svg%3E")}span.icon-phone{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23111' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z'%3E%3C/path%3E%3C/svg%3E")}span.icon-rss{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23111' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 11a9 9 0 0 1 9 9'%3E%3C/path%3E%3Cpath d='M4 4a16 16 0 0 1 16 16'%3E%3C/path%3E%3Ccircle cx='5' cy='19' r='1'%3E%3C/circle%3E%3C/svg%3E")}span.icon-search{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23111' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'%3E%3C/circle%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'%3E%3C/line%3E%3C/svg%3E")}span.icon-settings{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23111' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='3'%3E%3C/circle%3E%3Cpath d='M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z'%3E%3C/path%3E%3C/svg%3E")}span.icon-share{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23111' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='18' cy='5' r='3'%3E%3C/circle%3E%3Ccircle cx='6' cy='12' r='3'%3E%3C/circle%3E%3Ccircle cx='18' cy='19' r='3'%3E%3C/circle%3E%3Cline x1='8.59' y1='13.51' x2='15.42' y2='17.49'%3E%3C/line%3E%3Cline x1='15.41' y1='6.51' x2='8.59' y2='10.49'%3E%3C/line%3E%3C/svg%3E")}span.icon-cart{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23111' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='9' cy='21' r='1'%3E%3C/circle%3E%3Ccircle cx='20' cy='21' r='1'%3E%3C/circle%3E%3Cpath d='M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6'%3E%3C/path%3E%3C/svg%3E")}span.icon-upload{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23111' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4'%3E%3C/path%3E%3Cpolyline points='17 8 12 3 7 8'%3E%3C/polyline%3E%3Cline x1='12' y1='3' x2='12' y2='15'%3E%3C/line%3E%3C/svg%3E")}span.icon-user{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23111' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2'%3E%3C/path%3E%3Ccircle cx='12' cy='7' r='4'%3E%3C/circle%3E%3C/svg%3E")}:root{--generic-border-color:rgba(0,0,0,0.3);--generic-box-shadow:0 .25rem .25rem 0 rgba(0,0,0,0.125),0 .125rem .125rem -.125rem rgba(0,0,0,0.25)}.hidden{display:none !important}.visually-hidden{position:absolute !important;width:1px !important;height:1px !important;margin:-1px !important;border:0 !important;padding:0 !important;clip:rect(0 0 0 0) !important;-webkit-clip-path:inset(100%) !important;clip-path:inset(100%) !important;overflow:hidden !important}.bordered{border:.0625rem solid var(--generic-border-color) !important}.rounded{border-radius:var(--universal-border-radius) !important}.circular{border-radius:50% !important}.shadowed{box-shadow:var(--generic-box-shadow) !important}.responsive-margin{margin:calc(var(--universal-margin) / 4) !important}@media screen and (min-width: 768px){.responsive-margin{margin:calc(var(--universal-margin) / 2) !important}}@media screen and (min-width: 1280px){.responsive-margin{margin:var(--universal-margin) !important}}.responsive-padding{padding:calc(var(--universal-padding) / 4) !important}@media screen and (min-width: 768px){.responsive-padding{padding:calc(var(--universal-padding) / 2) !important}}@media screen and (min-width: 1280px){.responsive-padding{padding:var(--universal-padding) !important}}@media screen and (max-width: 767px){.hidden-sm{display:none !important}}@media screen and (min-width: 768px) and (max-width: 1279px){.hidden-md{display:none !important}}@media screen and (min-width: 1280px){.hidden-lg{display:none !important}}@media screen and (max-width: 767px){.visually-hidden-sm{position:absolute !important;width:1px !important;height:1px !important;margin:-1px !important;border:0 !important;padding:0 !important;clip:rect(0 0 0 0) !important;-webkit-clip-path:inset(100%) !important;clip-path:inset(100%) !important;overflow:hidden !important}}@media screen and (min-width: 768px) and (max-width: 1279px){.visually-hidden-md{position:absolute !important;width:1px !important;height:1px !important;margin:-1px !important;border:0 !important;padding:0 !important;clip:rect(0 0 0 0) !important;-webkit-clip-path:inset(100%) !important;clip-path:inset(100%) !important;overflow:hidden !important}}@media screen and (min-width: 1280px){.visually-hidden-lg{position:absolute !important;width:1px !important;height:1px !important;margin:-1px !important;border:0 !important;padding:0 !important;clip:rect(0 0 0 0) !important;-webkit-clip-path:inset(100%) !important;clip-path:inset(100%) !important;overflow:hidden !important}} diff --git a/server/website/css/mini-nord.min.css b/server/website/css/mini-nord.min.css new file mode 100644 index 0000000..af09abb --- /dev/null +++ b/server/website/css/mini-nord.min.css @@ -0,0 +1 @@ +:root{--fore-color:#2e3440;--secondary-fore-color:#3b4252;--back-color:#eceff4;--secondary-back-color:#e5e9f0;--blockquote-color:#d08770;--pre-color:#b48ead;--border-color:#d8dee9;--secondary-border-color:#e5e9f0;--heading-ratio:1.19;--universal-margin:.5rem;--universal-padding:.5rem;--universal-border-radius:.125rem;--a-link-color:#88c0d0;--a-visited-color:#5e81ac}html{font-size:16px}a,b,del,em,i,ins,q,span,strong,u{font-size:1em}html,*{font-family:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, "Helvetica Neue", Helvetica, sans-serif;line-height:1.5;-webkit-text-size-adjust:100%}*{font-size:1rem}body{margin:0;color:var(--fore-color);background:var(--back-color)}details{display:block}summary{display:list-item}abbr[title]{border-bottom:none;text-decoration:underline dotted}input{overflow:visible}img{max-width:100%;height:auto}h1,h2,h3,h4,h5,h6{line-height:1.2;margin:calc(1.5 * var(--universal-margin)) var(--universal-margin);font-weight:500}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small{color:var(--secondary-fore-color);display:block;margin-top:-.25rem}h1{font-size:calc(1rem * var(--heading-ratio) * var(--heading-ratio) * var(--heading-ratio) * var(--heading-ratio))}h2{font-size:calc(1rem * var(--heading-ratio) * var(--heading-ratio) * var(--heading-ratio))}h3{font-size:calc(1rem * var(--heading-ratio) * var(--heading-ratio))}h4{font-size:calc(1rem * var(--heading-ratio))}h5{font-size:1rem}h6{font-size:calc(1rem / var(--heading-ratio))}p{margin:var(--universal-margin)}ol,ul{margin:var(--universal-margin);padding-left:calc(2 * var(--universal-margin))}b,strong{font-weight:700}hr{box-sizing:content-box;border:0;line-height:1.25em;margin:var(--universal-margin);height:.0625rem;background:linear-gradient(to right, transparent, var(--border-color) 20%, var(--border-color) 80%, transparent)}blockquote{display:block;position:relative;font-style:italic;color:var(--secondary-fore-color);margin:var(--universal-margin);padding:calc(3 * var(--universal-padding));border:.0625rem solid var(--secondary-border-color);border-left:.375rem solid var(--blockquote-color);border-radius:0 var(--universal-border-radius) var(--universal-border-radius) 0}blockquote:before{position:absolute;top:calc(0rem - var(--universal-padding));left:0;font-family:sans-serif;font-size:3rem;font-weight:700;content:"\201c";color:var(--blockquote-color)}blockquote[cite]:after{font-style:normal;font-size:.75em;font-weight:700;content:"\a— " attr(cite);white-space:pre}code,kbd,pre,samp{font-family:Menlo, Consolas, monospace;font-size:.85em}code{background:var(--secondary-back-color);border-radius:var(--universal-border-radius);padding:calc(var(--universal-padding) / 4) calc(var(--universal-padding) / 2)}kbd{background:var(--fore-color);color:var(--back-color);border-radius:var(--universal-border-radius);padding:calc(var(--universal-padding) / 4) calc(var(--universal-padding) / 2)}pre{overflow:auto;background:var(--secondary-back-color);padding:calc(1.5 * var(--universal-padding));margin:var(--universal-margin);border:.0625rem solid var(--secondary-border-color);border-left:.25rem solid var(--pre-color);border-radius:0 var(--universal-border-radius) var(--universal-border-radius) 0}sup,sub,code,kbd{line-height:0;position:relative;vertical-align:baseline}small,sup,sub,figcaption{font-size:.75em}sup{top:-.5em}sub{bottom:-.25em}figure{margin:var(--universal-margin)}figcaption{color:var(--secondary-fore-color)}a{text-decoration:none}a:link{color:var(--a-link-color)}a:visited{color:var(--a-visited-color)}a:hover,a:focus{text-decoration:underline}.container{margin:0 auto;padding:0 calc(1.5 * var(--universal-padding))}.row{box-sizing:border-box;display:flex;flex:0 1 auto;flex-flow:row wrap}.col-sm,[class^='col-sm-'],[class^='col-sm-offset-'],.row[class*='cols-sm-']>*{box-sizing:border-box;flex:0 0 auto;padding:0 calc(var(--universal-padding) / 2)}.col-sm,.row.cols-sm>*{max-width:100%;flex-grow:1;flex-basis:0}.col-sm-1,.row.cols-sm-1>*{max-width:8.33333%;flex-basis:8.33333%}.col-sm-offset-0{margin-left:0}.col-sm-2,.row.cols-sm-2>*{max-width:16.66667%;flex-basis:16.66667%}.col-sm-offset-1{margin-left:8.33333%}.col-sm-3,.row.cols-sm-3>*{max-width:25%;flex-basis:25%}.col-sm-offset-2{margin-left:16.66667%}.col-sm-4,.row.cols-sm-4>*{max-width:33.33333%;flex-basis:33.33333%}.col-sm-offset-3{margin-left:25%}.col-sm-5,.row.cols-sm-5>*{max-width:41.66667%;flex-basis:41.66667%}.col-sm-offset-4{margin-left:33.33333%}.col-sm-6,.row.cols-sm-6>*{max-width:50%;flex-basis:50%}.col-sm-offset-5{margin-left:41.66667%}.col-sm-7,.row.cols-sm-7>*{max-width:58.33333%;flex-basis:58.33333%}.col-sm-offset-6{margin-left:50%}.col-sm-8,.row.cols-sm-8>*{max-width:66.66667%;flex-basis:66.66667%}.col-sm-offset-7{margin-left:58.33333%}.col-sm-9,.row.cols-sm-9>*{max-width:75%;flex-basis:75%}.col-sm-offset-8{margin-left:66.66667%}.col-sm-10,.row.cols-sm-10>*{max-width:83.33333%;flex-basis:83.33333%}.col-sm-offset-9{margin-left:75%}.col-sm-11,.row.cols-sm-11>*{max-width:91.66667%;flex-basis:91.66667%}.col-sm-offset-10{margin-left:83.33333%}.col-sm-12,.row.cols-sm-12>*{max-width:100%;flex-basis:100%}.col-sm-offset-11{margin-left:91.66667%}.col-sm-normal{order:initial}.col-sm-first{order:-999}.col-sm-last{order:999}@media screen and (min-width: 768px){.col-md,[class^='col-md-'],[class^='col-md-offset-'],.row[class*='cols-md-']>*{box-sizing:border-box;flex:0 0 auto;padding:0 calc(var(--universal-padding) / 2)}.col-md,.row.cols-md>*{max-width:100%;flex-grow:1;flex-basis:0}.col-md-1,.row.cols-md-1>*{max-width:8.33333%;flex-basis:8.33333%}.col-md-offset-0{margin-left:0}.col-md-2,.row.cols-md-2>*{max-width:16.66667%;flex-basis:16.66667%}.col-md-offset-1{margin-left:8.33333%}.col-md-3,.row.cols-md-3>*{max-width:25%;flex-basis:25%}.col-md-offset-2{margin-left:16.66667%}.col-md-4,.row.cols-md-4>*{max-width:33.33333%;flex-basis:33.33333%}.col-md-offset-3{margin-left:25%}.col-md-5,.row.cols-md-5>*{max-width:41.66667%;flex-basis:41.66667%}.col-md-offset-4{margin-left:33.33333%}.col-md-6,.row.cols-md-6>*{max-width:50%;flex-basis:50%}.col-md-offset-5{margin-left:41.66667%}.col-md-7,.row.cols-md-7>*{max-width:58.33333%;flex-basis:58.33333%}.col-md-offset-6{margin-left:50%}.col-md-8,.row.cols-md-8>*{max-width:66.66667%;flex-basis:66.66667%}.col-md-offset-7{margin-left:58.33333%}.col-md-9,.row.cols-md-9>*{max-width:75%;flex-basis:75%}.col-md-offset-8{margin-left:66.66667%}.col-md-10,.row.cols-md-10>*{max-width:83.33333%;flex-basis:83.33333%}.col-md-offset-9{margin-left:75%}.col-md-11,.row.cols-md-11>*{max-width:91.66667%;flex-basis:91.66667%}.col-md-offset-10{margin-left:83.33333%}.col-md-12,.row.cols-md-12>*{max-width:100%;flex-basis:100%}.col-md-offset-11{margin-left:91.66667%}.col-md-normal{order:initial}.col-md-first{order:-999}.col-md-last{order:999}}@media screen and (min-width: 1280px){.col-lg,[class^='col-lg-'],[class^='col-lg-offset-'],.row[class*='cols-lg-']>*{box-sizing:border-box;flex:0 0 auto;padding:0 calc(var(--universal-padding) / 2)}.col-lg,.row.cols-lg>*{max-width:100%;flex-grow:1;flex-basis:0}.col-lg-1,.row.cols-lg-1>*{max-width:8.33333%;flex-basis:8.33333%}.col-lg-offset-0{margin-left:0}.col-lg-2,.row.cols-lg-2>*{max-width:16.66667%;flex-basis:16.66667%}.col-lg-offset-1{margin-left:8.33333%}.col-lg-3,.row.cols-lg-3>*{max-width:25%;flex-basis:25%}.col-lg-offset-2{margin-left:16.66667%}.col-lg-4,.row.cols-lg-4>*{max-width:33.33333%;flex-basis:33.33333%}.col-lg-offset-3{margin-left:25%}.col-lg-5,.row.cols-lg-5>*{max-width:41.66667%;flex-basis:41.66667%}.col-lg-offset-4{margin-left:33.33333%}.col-lg-6,.row.cols-lg-6>*{max-width:50%;flex-basis:50%}.col-lg-offset-5{margin-left:41.66667%}.col-lg-7,.row.cols-lg-7>*{max-width:58.33333%;flex-basis:58.33333%}.col-lg-offset-6{margin-left:50%}.col-lg-8,.row.cols-lg-8>*{max-width:66.66667%;flex-basis:66.66667%}.col-lg-offset-7{margin-left:58.33333%}.col-lg-9,.row.cols-lg-9>*{max-width:75%;flex-basis:75%}.col-lg-offset-8{margin-left:66.66667%}.col-lg-10,.row.cols-lg-10>*{max-width:83.33333%;flex-basis:83.33333%}.col-lg-offset-9{margin-left:75%}.col-lg-11,.row.cols-lg-11>*{max-width:91.66667%;flex-basis:91.66667%}.col-lg-offset-10{margin-left:83.33333%}.col-lg-12,.row.cols-lg-12>*{max-width:100%;flex-basis:100%}.col-lg-offset-11{margin-left:91.66667%}.col-lg-normal{order:initial}.col-lg-first{order:-999}.col-lg-last{order:999}}:root{--card-back-color:#eceff4;--card-fore-color:#2e3440;--card-border-color:#e5e9f0}.card{display:flex;flex-direction:column;justify-content:space-between;align-self:center;position:relative;width:100%;background:var(--card-back-color);color:var(--card-fore-color);border:.0625rem solid var(--card-border-color);border-radius:var(--universal-border-radius);margin:var(--universal-margin);overflow:hidden}@media screen and (min-width: 320px){.card{max-width:320px}}.card>.section{background:var(--card-back-color);color:var(--card-fore-color);box-sizing:border-box;margin:0;border:0;border-radius:0;border-bottom:.0625rem solid var(--card-border-color);padding:var(--universal-padding);width:100%}.card>.section.media{height:200px;padding:0;-o-object-fit:cover;object-fit:cover}.card>.section:last-child{border-bottom:0}@media screen and (min-width: 240px){.card.small{max-width:240px}}@media screen and (min-width: 480px){.card.large{max-width:480px}}.card.fluid{max-width:100%;width:auto}.card.warning{--card-back-color:#ebcb8b;--card-border-color:#d08770}.card.error{--card-back-color:#bf616a;--card-border-color:#434c5e}.card>.section.dark{--card-back-color:#d8dee9}.card>.section.double-padded{padding:calc(1.5 * var(--universal-padding))}:root{--form-back-color:#e5e9f0;--form-fore-color:#2e3440;--form-border-color:#e5e9f0;--input-back-color:#eceff4;--input-fore-color:#2e3440;--input-border-color:#e5e9f0;--input-focus-color:#88c0d0;--input-invalid-color:#bf616a;--button-back-color:#e5e9f0;--button-hover-back-color:#d8dee9;--button-fore-color:#2e3440;--button-border-color:transparent;--button-hover-border-color:transparent;--button-group-border-color:rgba(124,124,124,0.54)}form{background:var(--form-back-color);color:var(--form-fore-color);border:.0625rem solid var(--form-border-color);border-radius:var(--universal-border-radius);margin:var(--universal-margin);padding:calc(2 * var(--universal-padding)) var(--universal-padding)}fieldset{border:.0625rem solid var(--form-border-color);border-radius:var(--universal-border-radius);margin:calc(var(--universal-margin) / 4);padding:var(--universal-padding)}legend{box-sizing:border-box;display:table;max-width:100%;white-space:normal;font-weight:700;padding:calc(var(--universal-padding) / 2)}label{padding:calc(var(--universal-padding) / 2) var(--universal-padding)}.input-group{display:inline-block}.input-group.fluid{display:flex;align-items:center;justify-content:center}.input-group.fluid>input{max-width:100%;flex-grow:1;flex-basis:0px}@media screen and (max-width: 767px){.input-group.fluid{align-items:stretch;flex-direction:column}}.input-group.vertical{display:flex;align-items:stretch;flex-direction:column}.input-group.vertical>input{max-width:100%;flex-grow:1;flex-basis:0px}[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}input:not([type]),[type="text"],[type="email"],[type="number"],[type="search"],[type="password"],[type="url"],[type="tel"],[type="checkbox"],[type="radio"],textarea,select{box-sizing:border-box;background:var(--input-back-color);color:var(--input-fore-color);border:.0625rem solid var(--input-border-color);border-radius:var(--universal-border-radius);margin:calc(var(--universal-margin) / 2);padding:var(--universal-padding) calc(1.5 * var(--universal-padding))}input:not([type="button"]):not([type="submit"]):not([type="reset"]):hover,input:not([type="button"]):not([type="submit"]):not([type="reset"]):focus,textarea:hover,textarea:focus,select:hover,select:focus{border-color:var(--input-focus-color);box-shadow:none}input:not([type="button"]):not([type="submit"]):not([type="reset"]):invalid,input:not([type="button"]):not([type="submit"]):not([type="reset"]):focus:invalid,textarea:invalid,textarea:focus:invalid,select:invalid,select:focus:invalid{border-color:var(--input-invalid-color);box-shadow:none}input:not([type="button"]):not([type="submit"]):not([type="reset"])[readonly],textarea[readonly],select[readonly]{background:var(--secondary-back-color)}select{max-width:100%}option{overflow:hidden;text-overflow:ellipsis}[type="checkbox"],[type="radio"]{-webkit-appearance:none;-moz-appearance:none;appearance:none;position:relative;height:calc(1rem + var(--universal-padding) / 2);width:calc(1rem + var(--universal-padding) / 2);vertical-align:text-bottom;padding:0;flex-basis:calc(1rem + var(--universal-padding) / 2) !important;flex-grow:0 !important}[type="checkbox"]:checked:before,[type="radio"]:checked:before{position:absolute}[type="checkbox"]:checked:before{content:'\2713';font-family:sans-serif;font-size:calc(1rem + var(--universal-padding) / 2);top:calc(0rem - var(--universal-padding));left:calc(var(--universal-padding) / 4)}[type="radio"]{border-radius:100%}[type="radio"]:checked:before{border-radius:100%;content:'';top:calc(.0625rem + var(--universal-padding) / 2);left:calc(.0625rem + var(--universal-padding) / 2);background:var(--input-fore-color);width:0.5rem;height:0.5rem}:placeholder-shown{color:var(--input-fore-color)}::-ms-placeholder{color:var(--input-fore-color);opacity:0.54}button::-moz-focus-inner,[type="button"]::-moz-focus-inner,[type="reset"]::-moz-focus-inner,[type="submit"]::-moz-focus-inner{border-style:none;padding:0}button,html [type="button"],[type="reset"],[type="submit"]{-webkit-appearance:button}button{overflow:visible;text-transform:none}button,[type="button"],[type="submit"],[type="reset"],a.button,label.button,.button,a[role="button"],label[role="button"],[role="button"]{display:inline-block;background:var(--button-back-color);color:var(--button-fore-color);border:.0625rem solid var(--button-border-color);border-radius:var(--universal-border-radius);padding:var(--universal-padding) calc(1.5 * var(--universal-padding));margin:var(--universal-margin);text-decoration:none;cursor:pointer;transition:background 0.3s}button:hover,button:focus,[type="button"]:hover,[type="button"]:focus,[type="submit"]:hover,[type="submit"]:focus,[type="reset"]:hover,[type="reset"]:focus,a.button:hover,a.button:focus,label.button:hover,label.button:focus,.button:hover,.button:focus,a[role="button"]:hover,a[role="button"]:focus,label[role="button"]:hover,label[role="button"]:focus,[role="button"]:hover,[role="button"]:focus{background:var(--button-hover-back-color);border-color:var(--button-hover-border-color)}input:disabled,input[disabled],textarea:disabled,textarea[disabled],select:disabled,select[disabled],button:disabled,button[disabled],.button:disabled,.button[disabled],[role="button"]:disabled,[role="button"][disabled]{cursor:not-allowed;opacity:.75}.button-group{display:flex;border:.0625rem solid var(--button-group-border-color);border-radius:var(--universal-border-radius);margin:var(--universal-margin)}.button-group>button,.button-group [type="button"],.button-group>[type="submit"],.button-group>[type="reset"],.button-group>.button,.button-group>[role="button"]{margin:0;max-width:100%;flex:1 1 auto;text-align:center;border:0;border-radius:0;box-shadow:none}.button-group>:not(:first-child){border-left:.0625rem solid var(--button-group-border-color)}@media screen and (max-width: 767px){.button-group{flex-direction:column}.button-group>:not(:first-child){border:0;border-top:.0625rem solid var(--button-group-border-color)}}button.primary,[type="button"].primary,[type="submit"].primary,[type="reset"].primary,.button.primary,[role="button"].primary{--button-back-color:#5e81ac;--button-fore-color:#eceff4}button.primary:hover,button.primary:focus,[type="button"].primary:hover,[type="button"].primary:focus,[type="submit"].primary:hover,[type="submit"].primary:focus,[type="reset"].primary:hover,[type="reset"].primary:focus,.button.primary:hover,.button.primary:focus,[role="button"].primary:hover,[role="button"].primary:focus{--button-hover-back-color:#5e81ac}button.secondary,[type="button"].secondary,[type="submit"].secondary,[type="reset"].secondary,.button.secondary,[role="button"].secondary{--button-back-color:#bf616a;--button-fore-color:#eceff4}button.secondary:hover,button.secondary:focus,[type="button"].secondary:hover,[type="button"].secondary:focus,[type="submit"].secondary:hover,[type="submit"].secondary:focus,[type="reset"].secondary:hover,[type="reset"].secondary:focus,.button.secondary:hover,.button.secondary:focus,[role="button"].secondary:hover,[role="button"].secondary:focus{--button-hover-back-color:#bf616a}button.tertiary,[type="button"].tertiary,[type="submit"].tertiary,[type="reset"].tertiary,.button.tertiary,[role="button"].tertiary{--button-back-color:#a3be8c;--button-fore-color:#434c5e}button.tertiary:hover,button.tertiary:focus,[type="button"].tertiary:hover,[type="button"].tertiary:focus,[type="submit"].tertiary:hover,[type="submit"].tertiary:focus,[type="reset"].tertiary:hover,[type="reset"].tertiary:focus,.button.tertiary:hover,.button.tertiary:focus,[role="button"].tertiary:hover,[role="button"].tertiary:focus{--button-hover-back-color:#a3be8c}button.inverse,[type="button"].inverse,[type="submit"].inverse,[type="reset"].inverse,.button.inverse,[role="button"].inverse{--button-back-color:#3b4252;--button-fore-color:#eceff4}button.inverse:hover,button.inverse:focus,[type="button"].inverse:hover,[type="button"].inverse:focus,[type="submit"].inverse:hover,[type="submit"].inverse:focus,[type="reset"].inverse:hover,[type="reset"].inverse:focus,.button.inverse:hover,.button.inverse:focus,[role="button"].inverse:hover,[role="button"].inverse:focus{--button-hover-back-color:#2e3440}button.small,[type="button"].small,[type="submit"].small,[type="reset"].small,.button.small,[role="button"].small{padding:calc(0.5 * var(--universal-padding)) calc(0.75 * var(--universal-padding));margin:var(--universal-margin)}button.large,[type="button"].large,[type="submit"].large,[type="reset"].large,.button.large,[role="button"].large{padding:calc(1.5 * var(--universal-padding)) calc(2 * var(--universal-padding));margin:var(--universal-margin)}:root{--header-back-color:#eceff4;--header-hover-back-color:#e5e9f0;--header-fore-color:#3b4252;--header-border-color:#e5e9f0;--nav-back-color:#eceff4;--nav-hover-back-color:#e5e9f0;--nav-fore-color:#3b4252;--nav-border-color:#e5e9f0;--nav-link-color:#88c0d0;--footer-fore-color:#3b4252;--footer-back-color:#eceff4;--footer-border-color:#e5e9f0;--footer-link-color:#88c0d0;--drawer-back-color:#eceff4;--drawer-hover-back-color:#e5e9f0;--drawer-border-color:#e5e9f0;--drawer-close-color:#3b4252}header{height:3.1875rem;background:var(--header-back-color);color:var(--header-fore-color);border-bottom:.0625rem solid var(--header-border-color);padding:calc(var(--universal-padding) / 4) 0;white-space:nowrap;overflow-x:auto;overflow-y:hidden}header.row{box-sizing:content-box}header .logo{color:var(--header-fore-color);font-size:1.75rem;padding:var(--universal-padding) calc(2 * var(--universal-padding));text-decoration:none}header button,header [type="button"],header .button,header [role="button"]{box-sizing:border-box;position:relative;top:calc(0rem - var(--universal-padding) / 4);height:calc(3.1875rem + var(--universal-padding) / 2);background:var(--header-back-color);line-height:calc(3.1875rem - var(--universal-padding) * 1.5);text-align:center;color:var(--header-fore-color);border:0;border-radius:0;margin:0;text-transform:uppercase}header button:hover,header button:focus,header [type="button"]:hover,header [type="button"]:focus,header .button:hover,header .button:focus,header [role="button"]:hover,header [role="button"]:focus{background:var(--header-hover-back-color)}nav{background:var(--nav-back-color);color:var(--nav-fore-color);border:.0625rem solid var(--nav-border-color);border-radius:var(--universal-border-radius);margin:var(--universal-margin)}nav *{padding:var(--universal-padding) calc(1.5 * var(--universal-padding))}nav a,nav a:visited{display:block;color:var(--nav-link-color);border-radius:var(--universal-border-radius);transition:background 0.3s}nav a:hover,nav a:focus,nav a:visited:hover,nav a:visited:focus{text-decoration:none;background:var(--nav-hover-back-color)}nav .sublink-1{position:relative;margin-left:calc(2 * var(--universal-padding))}nav .sublink-1:before{position:absolute;left:calc(var(--universal-padding) - 1 * var(--universal-padding));top:-.0625rem;content:'';height:100%;border:.0625rem solid var(--nav-border-color);border-left:0}nav .sublink-2{position:relative;margin-left:calc(4 * var(--universal-padding))}nav .sublink-2:before{position:absolute;left:calc(var(--universal-padding) - 3 * var(--universal-padding));top:-.0625rem;content:'';height:100%;border:.0625rem solid var(--nav-border-color);border-left:0}footer{background:var(--footer-back-color);color:var(--footer-fore-color);border-top:.0625rem solid var(--footer-border-color);padding:calc(2 * var(--universal-padding)) var(--universal-padding);font-size:.875rem}footer a,footer a:visited{color:var(--footer-link-color)}header.sticky{position:-webkit-sticky;position:sticky;z-index:1101;top:0}footer.sticky{position:-webkit-sticky;position:sticky;z-index:1101;bottom:0}.drawer-toggle:before{display:inline-block;position:relative;vertical-align:bottom;content:'\00a0\2261\00a0';font-family:sans-serif;font-size:1.5em}@media screen and (min-width: 768px){.drawer-toggle:not(.persistent){display:none}}[type="checkbox"].drawer{height:1px;width:1px;margin:-1px;overflow:hidden;position:absolute;clip:rect(0 0 0 0);-webkit-clip-path:inset(100%);clip-path:inset(100%)}[type="checkbox"].drawer+*{display:block;box-sizing:border-box;position:fixed;top:0;width:320px;height:100vh;overflow-y:auto;background:var(--drawer-back-color);border:.0625rem solid var(--drawer-border-color);border-radius:0;margin:0;z-index:1110;right:-320px;transition:right 0.3s}[type="checkbox"].drawer+* .drawer-close{position:absolute;top:var(--universal-margin);right:var(--universal-margin);z-index:1111;width:2rem;height:2rem;border-radius:var(--universal-border-radius);padding:var(--universal-padding);margin:0;cursor:pointer;transition:background 0.3s}[type="checkbox"].drawer+* .drawer-close:before{display:block;content:'\00D7';color:var(--drawer-close-color);position:relative;font-family:sans-serif;font-size:2rem;line-height:1;text-align:center}[type="checkbox"].drawer+* .drawer-close:hover,[type="checkbox"].drawer+* .drawer-close:focus{background:var(--drawer-hover-back-color)}@media screen and (max-width: 320px){[type="checkbox"].drawer+*{width:100%}}[type="checkbox"].drawer:checked+*{right:0}@media screen and (min-width: 768px){[type="checkbox"].drawer:not(.persistent)+*{position:static;height:100%;z-index:1100}[type="checkbox"].drawer:not(.persistent)+* .drawer-close{display:none}}:root{--table-border-color:#d8dee9;--table-border-separator-color:#434c5e;--table-head-back-color:#e5e9f0;--table-head-fore-color:#2e3440;--table-body-back-color:#eceff4;--table-body-fore-color:#2e3440;--table-body-alt-back-color:#e5e9f0}table{border-collapse:separate;border-spacing:0;margin:0;display:flex;flex:0 1 auto;flex-flow:row wrap;padding:var(--universal-padding);padding-top:0}table caption{font-size:1.5rem;margin:calc(2 * var(--universal-margin)) 0;max-width:100%;flex:0 0 100%}table thead,table tbody{display:flex;flex-flow:row wrap;border:.0625rem solid var(--table-border-color)}table thead{z-index:999;border-radius:var(--universal-border-radius) var(--universal-border-radius) 0 0;border-bottom:.0625rem solid var(--table-border-separator-color)}table tbody{border-top:0;margin-top:calc(0 - var(--universal-margin));border-radius:0 0 var(--universal-border-radius) var(--universal-border-radius)}table tr{display:flex;padding:0}table th,table td{padding:calc(2 * var(--universal-padding))}table th{text-align:left;background:var(--table-head-back-color);color:var(--table-head-fore-color)}table td{background:var(--table-body-back-color);color:var(--table-body-fore-color);border-top:.0625rem solid var(--table-border-color)}table:not(.horizontal){overflow:auto;max-height:400px}table:not(.horizontal) thead,table:not(.horizontal) tbody{max-width:100%;flex:0 0 100%}table:not(.horizontal) tr{flex-flow:row wrap;flex:0 0 100%}table:not(.horizontal) th,table:not(.horizontal) td{flex:1 0 0%;overflow:hidden;text-overflow:ellipsis}table:not(.horizontal) thead{position:sticky;top:0}table:not(.horizontal) tbody tr:first-child td{border-top:0}table.horizontal{border:0}table.horizontal thead,table.horizontal tbody{border:0;flex-flow:row nowrap}table.horizontal tbody{overflow:auto;justify-content:space-between;flex:1 0 0;margin-left:calc( 4 * var(--universal-margin));padding-bottom:calc(var(--universal-padding) / 4)}table.horizontal tr{flex-direction:column;flex:1 0 auto}table.horizontal th,table.horizontal td{width:100%;border:0;border-bottom:.0625rem solid var(--table-border-color)}table.horizontal th:not(:first-child),table.horizontal td:not(:first-child){border-top:0}table.horizontal th{text-align:right;border-left:.0625rem solid var(--table-border-color);border-right:.0625rem solid var(--table-border-separator-color)}table.horizontal thead tr:first-child{padding-left:0}table.horizontal th:first-child,table.horizontal td:first-child{border-top:.0625rem solid var(--table-border-color)}table.horizontal tbody tr:last-child td{border-right:.0625rem solid var(--table-border-color)}table.horizontal tbody tr:last-child td:first-child{border-top-right-radius:0.25rem}table.horizontal tbody tr:last-child td:last-child{border-bottom-right-radius:0.25rem}table.horizontal thead tr:first-child th:first-child{border-top-left-radius:0.25rem}table.horizontal thead tr:first-child th:last-child{border-bottom-left-radius:0.25rem}@media screen and (max-width: 767px){table,table.horizontal{border-collapse:collapse;border:0;width:100%;display:table}table thead,table th,table.horizontal thead,table.horizontal th{border:0;height:1px;width:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;clip:rect(0 0 0 0);-webkit-clip-path:inset(100%);clip-path:inset(100%)}table tbody,table.horizontal tbody{border:0;display:table-row-group}table tr,table.horizontal tr{display:block;border:.0625rem solid var(--table-border-color);border-radius:var(--universal-border-radius);background:#fafafa;padding:var(--universal-padding);margin:var(--universal-margin);margin-bottom:calc(2 * var(--universal-margin))}table th,table td,table.horizontal th,table.horizontal td{width:auto}table td,table.horizontal td{display:block;border:0;text-align:right}table td:before,table.horizontal td:before{content:attr(data-label);float:left;font-weight:600}table th:first-child,table td:first-child,table.horizontal th:first-child,table.horizontal td:first-child{border-top:0}table tbody tr:last-child td,table.horizontal tbody tr:last-child td{border-right:0}}:root{--table-body-alt-back-color:#e5e9f0}table.striped tr:nth-of-type(2n)>td{background:var(--table-body-alt-back-color)}@media screen and (max-width: 768px){table.striped tr:nth-of-type(2n){background:var(--table-body-alt-back-color)}}:root{--table-body-hover-back-color:#88c0d0}table.hoverable tr:hover,table.hoverable tr:hover>td,table.hoverable tr:focus,table.hoverable tr:focus>td{background:var(--table-body-hover-back-color)}@media screen and (max-width: 768px){table.hoverable tr:hover,table.hoverable tr:hover>td,table.hoverable tr:focus,table.hoverable tr:focus>td{background:var(--table-body-hover-back-color)}}:root{--mark-back-color:#5e81ac;--mark-fore-color:#fafafa}mark{background:var(--mark-back-color);color:var(--mark-fore-color);font-size:.95em;line-height:1em;border-radius:var(--universal-border-radius);padding:calc(var(--universal-padding) / 4) calc(var(--universal-padding) / 2)}mark.inline-block{display:inline-block;font-size:1em;line-height:1.5;padding:calc(var(--universal-padding) / 2) var(--universal-padding)}:root{--toast-back-color:#2e3440;--toast-fore-color:#eceff4}.toast{position:fixed;bottom:calc(var(--universal-margin) * 3);left:50%;transform:translate(-50%, -50%);z-index:1111;color:var(--toast-fore-color);background:var(--toast-back-color);border-radius:calc(var(--universal-border-radius) * 16);padding:var(--universal-padding) calc(var(--universal-padding) * 3)}:root{--tooltip-back-color:#2e3440;--tooltip-fore-color:#eceff4}.tooltip{position:relative;display:inline-block}.tooltip:before,.tooltip:after{position:absolute;opacity:0;clip:rect(0 0 0 0);-webkit-clip-path:inset(100%);clip-path:inset(100%);transition:all 0.3s;z-index:1010;left:50%}.tooltip:not(.bottom):before,.tooltip:not(.bottom):after{bottom:75%}.tooltip.bottom:before,.tooltip.bottom:after{top:75%}.tooltip:hover:before,.tooltip:hover:after,.tooltip:focus:before,.tooltip:focus:after{opacity:1;clip:auto;-webkit-clip-path:inset(0%);clip-path:inset(0%)}.tooltip:before{content:'';background:transparent;border:var(--universal-margin) solid transparent;left:calc(50% - var(--universal-margin))}.tooltip:not(.bottom):before{border-top-color:#2e3440}.tooltip.bottom:before{border-bottom-color:#2e3440}.tooltip:after{content:attr(aria-label);color:var(--tooltip-fore-color);background:var(--tooltip-back-color);border-radius:var(--universal-border-radius);padding:var(--universal-padding);white-space:nowrap;transform:translateX(-50%)}.tooltip:not(.bottom):after{margin-bottom:calc(2 * var(--universal-margin))}.tooltip.bottom:after{margin-top:calc(2 * var(--universal-margin))}:root{--modal-overlay-color:rgba(0,0,0,0.45);--modal-close-color:#3b4252;--modal-close-hover-color:#e5e9f0}[type="checkbox"].modal{height:1px;width:1px;margin:-1px;overflow:hidden;position:absolute;clip:rect(0 0 0 0);-webkit-clip-path:inset(100%);clip-path:inset(100%)}[type="checkbox"].modal+div{position:fixed;top:0;left:0;display:none;width:100vw;height:100vh;background:var(--modal-overlay-color)}[type="checkbox"].modal+div .card{margin:0 auto;max-height:50vh;overflow:auto}[type="checkbox"].modal+div .card .modal-close{position:absolute;top:0;right:0;width:1.75rem;height:1.75rem;border-radius:var(--universal-border-radius);padding:var(--universal-padding);margin:0;cursor:pointer;transition:background 0.3s}[type="checkbox"].modal+div .card .modal-close:before{display:block;content:'\00D7';color:var(--modal-close-color);position:relative;font-family:sans-serif;font-size:1.75rem;line-height:1;text-align:center}[type="checkbox"].modal+div .card .modal-close:hover,[type="checkbox"].modal+div .card .modal-close:focus{background:var(--modal-close-hover-color)}[type="checkbox"].modal:checked+div{display:flex;flex:0 1 auto;z-index:1200}[type="checkbox"].modal:checked+div .card .modal-close{z-index:1211}:root{--collapse-label-back-color:#e5e9f0;--collapse-label-fore-color:#2e3440;--collapse-label-hover-back-color:#e5e9f0;--collapse-selected-label-back-color:#e5e9f0;--collapse-border-color:#e5e9f0;--collapse-content-back-color:#fafafa;--collapse-selected-label-border-color:#88c0d0}.collapse{width:calc(100% - 2 * var(--universal-margin));opacity:1;display:flex;flex-direction:column;margin:var(--universal-margin);border-radius:var(--universal-border-radius)}.collapse>[type="radio"],.collapse>[type="checkbox"]{height:1px;width:1px;margin:-1px;overflow:hidden;position:absolute;clip:rect(0 0 0 0);-webkit-clip-path:inset(100%);clip-path:inset(100%)}.collapse>label{flex-grow:1;display:inline-block;height:1.5rem;cursor:pointer;transition:background 0.3s;color:var(--collapse-label-fore-color);background:var(--collapse-label-back-color);border:.0625rem solid var(--collapse-border-color);padding:calc(1.5 * var(--universal-padding))}.collapse>label:hover,.collapse>label:focus{background:var(--collapse-label-hover-back-color)}.collapse>label+div{flex-basis:auto;height:1px;width:1px;margin:-1px;overflow:hidden;position:absolute;clip:rect(0 0 0 0);-webkit-clip-path:inset(100%);clip-path:inset(100%);transition:max-height 0.3s;max-height:1px}.collapse>:checked+label{background:var(--collapse-selected-label-back-color);border-bottom-color:var(--collapse-selected-label-border-color)}.collapse>:checked+label+div{box-sizing:border-box;position:relative;width:100%;height:auto;overflow:auto;margin:0;background:var(--collapse-content-back-color);border:.0625rem solid var(--collapse-border-color);border-top:0;padding:var(--universal-padding);clip:auto;-webkit-clip-path:inset(0%);clip-path:inset(0%);max-height:400px}.collapse>label:not(:first-of-type){border-top:0}.collapse>label:first-of-type{border-radius:var(--universal-border-radius) var(--universal-border-radius) 0 0}.collapse>label:last-of-type:not(:first-of-type){border-radius:0 0 var(--universal-border-radius) var(--universal-border-radius)}.collapse>label:last-of-type:first-of-type{border-radius:var(--universal-border-radius)}.collapse>:checked:last-of-type:not(:first-of-type)+label{border-radius:0}.collapse>:checked:last-of-type+label+div{border-radius:0 0 var(--universal-border-radius) var(--universal-border-radius)}mark.secondary{--mark-back-color:#bf616a}mark.tertiary{--mark-back-color:#a3be8c}mark.tag{padding:calc(var(--universal-padding)/2) var(--universal-padding);border-radius:1em}:root{--progress-back-color:#e5e9f0;--progress-fore-color:#434c5e}progress{display:block;vertical-align:baseline;-webkit-appearance:none;-moz-appearance:none;appearance:none;height:.75rem;width:calc(100% - 2 * var(--universal-margin));margin:var(--universal-margin);border:0;border-radius:calc(2 * var(--universal-border-radius));background:var(--progress-back-color);color:var(--progress-fore-color)}progress::-webkit-progress-value{background:var(--progress-fore-color);border-top-left-radius:calc(2 * var(--universal-border-radius));border-bottom-left-radius:calc(2 * var(--universal-border-radius))}progress::-webkit-progress-bar{background:var(#e5e9f0)}progress::-moz-progress-bar{background:var(--progress-fore-color);border-top-left-radius:calc(2 * var(--universal-border-radius));border-bottom-left-radius:calc(2 * var(--universal-border-radius))}progress[value="1000"]::-webkit-progress-value{border-radius:calc(2 * var(--universal-border-radius))}progress[value="1000"]::-moz-progress-bar{border-radius:calc(2 * var(--universal-border-radius))}progress.inline{display:inline-block;vertical-align:middle;width:60%}:root{--spinner-back-color:#d8dee9;--spinner-fore-color:#434c5e}@keyframes spinner-donut-anim{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}.spinner{display:inline-block;margin:var(--universal-margin);border:.25rem solid var(--spinner-back-color);border-left:.25rem solid var(--spinner-fore-color);border-radius:50%;width:1.25rem;height:1.25rem;animation:spinner-donut-anim 1.2s linear infinite}progress.primary{--progress-fore-color:#5e81ac}progress.secondary{--progress-fore-color:#bf616a}progress.tertiary{--progress-fore-color:#a3be8c}.spinner.primary{--spinner-fore-color:#5e81ac}.spinner.secondary{--spinner-fore-color:#bf616a}.spinner.tertiary{--spinner-fore-color:#a3be8c}span[class^='icon-']{display:inline-block;height:1em;width:1em;vertical-align:-0.125em;background-size:contain;margin:0 calc(var(--universal-margin) / 4)}span[class^='icon-'].secondary{-webkit-filter:invert(25%);filter:invert(25%)}span[class^='icon-'].inverse{-webkit-filter:invert(100%);filter:invert(100%)}span.icon-alert{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%232e3440' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12' y2='16'%3E%3C/line%3E%3C/svg%3E")}span.icon-bookmark{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%232e3440' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z'%3E%3C/path%3E%3C/svg%3E")}span.icon-calendar{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%232e3440' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2' ry='2'%3E%3C/rect%3E%3Cline x1='16' y1='2' x2='16' y2='6'%3E%3C/line%3E%3Cline x1='8' y1='2' x2='8' y2='6'%3E%3C/line%3E%3Cline x1='3' y1='10' x2='21' y2='10'%3E%3C/line%3E%3C/svg%3E")}span.icon-credit{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%232e3440' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='1' y='4' width='22' height='16' rx='2' ry='2'%3E%3C/rect%3E%3Cline x1='1' y1='10' x2='23' y2='10'%3E%3C/line%3E%3C/svg%3E")}span.icon-edit{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%232e3440' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 14.66V20a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h5.34'%3E%3C/path%3E%3Cpolygon points='18 2 22 6 12 16 8 16 8 12 18 2'%3E%3C/polygon%3E%3C/svg%3E")}span.icon-link{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%232e3440' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6'%3E%3C/path%3E%3Cpolyline points='15 3 21 3 21 9'%3E%3C/polyline%3E%3Cline x1='10' y1='14' x2='21' y2='3'%3E%3C/line%3E%3C/svg%3E")}span.icon-help{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%232e3440' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3'%3E%3C/path%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='17' x2='12' y2='17'%3E%3C/line%3E%3C/svg%3E")}span.icon-home{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%232e3440' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z'%3E%3C/path%3E%3Cpolyline points='9 22 9 12 15 12 15 22'%3E%3C/polyline%3E%3C/svg%3E")}span.icon-info{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%232e3440' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='16' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='8' x2='12' y2='8'%3E%3C/line%3E%3C/svg%3E")}span.icon-lock{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%232e3440' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='11' width='18' height='11' rx='2' ry='2'%3E%3C/rect%3E%3Cpath d='M7 11V7a5 5 0 0 1 10 0v4'%3E%3C/path%3E%3C/svg%3E")}span.icon-mail{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%232e3440' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z'%3E%3C/path%3E%3Cpolyline points='22,6 12,13 2,6'%3E%3C/polyline%3E%3C/svg%3E")}span.icon-location{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%232e3440' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z'%3E%3C/path%3E%3Ccircle cx='12' cy='10' r='3'%3E%3C/circle%3E%3C/svg%3E")}span.icon-phone{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%232e3440' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z'%3E%3C/path%3E%3C/svg%3E")}span.icon-rss{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%232e3440' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 11a9 9 0 0 1 9 9'%3E%3C/path%3E%3Cpath d='M4 4a16 16 0 0 1 16 16'%3E%3C/path%3E%3Ccircle cx='5' cy='19' r='1'%3E%3C/circle%3E%3C/svg%3E")}span.icon-search{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%232e3440' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'%3E%3C/circle%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'%3E%3C/line%3E%3C/svg%3E")}span.icon-settings{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%232e3440' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='3'%3E%3C/circle%3E%3Cpath d='M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z'%3E%3C/path%3E%3C/svg%3E")}span.icon-share{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%232e3440' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='18' cy='5' r='3'%3E%3C/circle%3E%3Ccircle cx='6' cy='12' r='3'%3E%3C/circle%3E%3Ccircle cx='18' cy='19' r='3'%3E%3C/circle%3E%3Cline x1='8.59' y1='13.51' x2='15.42' y2='17.49'%3E%3C/line%3E%3Cline x1='15.41' y1='6.51' x2='8.59' y2='10.49'%3E%3C/line%3E%3C/svg%3E")}span.icon-cart{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%232e3440' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='9' cy='21' r='1'%3E%3C/circle%3E%3Ccircle cx='20' cy='21' r='1'%3E%3C/circle%3E%3Cpath d='M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6'%3E%3C/path%3E%3C/svg%3E")}span.icon-upload{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%232e3440' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4'%3E%3C/path%3E%3Cpolyline points='17 8 12 3 7 8'%3E%3C/polyline%3E%3Cline x1='12' y1='3' x2='12' y2='15'%3E%3C/line%3E%3C/svg%3E")}span.icon-user{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%232e3440' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2'%3E%3C/path%3E%3Ccircle cx='12' cy='7' r='4'%3E%3C/circle%3E%3C/svg%3E")}:root{--generic-border-color:rgba(0,0,0,0.3);--generic-box-shadow:0 .25rem .25rem 0 rgba(0,0,0,0.125),0 .125rem .125rem -.125rem rgba(0,0,0,0.125)}.hidden{display:none !important}.visually-hidden{position:absolute !important;width:1px !important;height:1px !important;margin:-1px !important;border:0 !important;padding:0 !important;clip:rect(0 0 0 0) !important;-webkit-clip-path:inset(100%) !important;clip-path:inset(100%) !important;overflow:hidden !important}.bordered{border:.0625rem solid var(--generic-border-color) !important}.rounded{border-radius:var(--universal-border-radius) !important}.circular{border-radius:50% !important}.shadowed{box-shadow:var(--generic-box-shadow) !important}.responsive-margin{margin:calc(var(--universal-margin) / 4) !important}@media screen and (min-width: 768px){.responsive-margin{margin:calc(var(--universal-margin) / 2) !important}}@media screen and (min-width: 1280px){.responsive-margin{margin:var(--universal-margin) !important}}.responsive-padding{padding:calc(var(--universal-padding) / 4) !important}@media screen and (min-width: 768px){.responsive-padding{padding:calc(var(--universal-padding) / 2) !important}}@media screen and (min-width: 1280px){.responsive-padding{padding:var(--universal-padding) !important}}@media screen and (max-width: 767px){.hidden-sm{display:none !important}}@media screen and (min-width: 768px) and (max-width: 1279px){.hidden-md{display:none !important}}@media screen and (min-width: 1280px){.hidden-lg{display:none !important}}@media screen and (max-width: 767px){.visually-hidden-sm{position:absolute !important;width:1px !important;height:1px !important;margin:-1px !important;border:0 !important;padding:0 !important;clip:rect(0 0 0 0) !important;-webkit-clip-path:inset(100%) !important;clip-path:inset(100%) !important;overflow:hidden !important}}@media screen and (min-width: 768px) and (max-width: 1279px){.visually-hidden-md{position:absolute !important;width:1px !important;height:1px !important;margin:-1px !important;border:0 !important;padding:0 !important;clip:rect(0 0 0 0) !important;-webkit-clip-path:inset(100%) !important;clip-path:inset(100%) !important;overflow:hidden !important}}@media screen and (min-width: 1280px){.visually-hidden-lg{position:absolute !important;width:1px !important;height:1px !important;margin:-1px !important;border:0 !important;padding:0 !important;clip:rect(0 0 0 0) !important;-webkit-clip-path:inset(100%) !important;clip-path:inset(100%) !important;overflow:hidden !important}} diff --git a/server/website/css/style.css b/server/website/css/style.css new file mode 100644 index 0000000..bec9693 --- /dev/null +++ b/server/website/css/style.css @@ -0,0 +1,250 @@ + +html +{ + height: 100%; +} + +body +{ + display: flex; + justify-content: center; + align-items: center; + min-height: 100%; + +} + +@keyframes blink-shadow { + 0% { box-shadow: 0 0 32px #DDD; } + 50% { box-shadow: none; } + 100% { box-shadow: 0 0 32px #DDD; } +} + +#mainpnl +{ + box-shadow: 0 0 32px #DDD; + //animation:blink-shadow ease-in-out 4s infinite; + width: 87%; + min-width: 300px; + max-width: 900px; + position: relative; + min-height: 570px; + + background: var(--form-back-color); + color: var(--form-fore-color); + border: .0625rem solid var(--form-border-color); + border-radius: var(--universal-border-radius); + margin: 32px .5rem; + padding: calc(2 * var(--universal-padding)) var(--universal-padding); +} + +.red-code +{ + border-left: .25rem solid #E53935; +} + +.yellow-code +{ + border-left: .25rem solid #FFCB05; +} + +#mainpnl input, +#mainpnl textarea +{ + width: 100%; +} + +.responsive-label { + align-items:center; +} +@media (min-width: 768px) { + .responsive-label .col-md-3 { + text-align:right + } +} + +#mainpnl h1 +{ + text-align: center; + margin-top: 0; + margin-bottom: 24px; + font-weight: bold; + color: #FFF; + text-shadow: #000 0 0 2px, #888 0 0 8px; +} +@media (max-width: 600px) { + #mainpnl h1 { + font-size: calc(0.85rem * var(--heading-ratio) * var(--heading-ratio) * var(--heading-ratio) * var(--heading-ratio)); + margin-top: 40px; + } +} + +#mainpnl button +{ + width: 100%; + margin-left: 4px; + margin-right: 4px; +} + +#copyinfo +{ + margin: 4px; + position: fixed; + bottom: 0; + right: 0; + //z-index: -999; + display: flex; + flex-direction: column; + text-align: right; +} + +#copyinfo a, +#copyinfo a:visited, +#copyinfo a:active +{ + font-family: "Courier New", monospace; + color: #AAA; + text-decoration: none; + display: block; + line-height: 1em; +} + +#copyinfo a:hover +{ + font-family: "Courier New", monospace; + color: #0288D1; +} + +#tr_link +{ + position: absolute; + top: 0; + right: 0; + margin: -1px -1px 0 0; + border-top-left-radius: 0; + border-bottom-right-radius: 0; + min-width: 40px; + text-align: center; +} + +#tl_link +{ + position: absolute; + top: 0; + left: 0; + margin: -1px 0 0 -1px; + border-top-right-radius: 0; + border-bottom-left-radius: 0; + padding: 4px 4px 0 4px; +} + +.icn-google-play { + display: inline-block; + width: 32px; + height: 32px; + background: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHg9IjBweCIgeT0iMHB4IgogICAgIHdpZHRoPSI1MCIgaGVpZ2h0PSI1MCIKICAgICB2aWV3Qm94PSIwIDAgNDggNDgiCiAgICAgc3R5bGU9ImZpbGw6IzAwMDAwMDsiPjxnIGlkPSJzdXJmYWNlMSI+PHBhdGggc3R5bGU9IiBmaWxsOiM0REI2QUM7IiBkPSJNIDcuNzAzMTI1IDQuMDQyOTY5IEMgNy4yOTI5NjkgNC4xNDg0MzggNyA0LjUwNzgxMyA3IDUuMTIxMDk0IEMgNyA2LjkyMTg3NSA3IDIzLjkxNDA2MyA3IDIzLjkxNDA2MyBDIDcgMjMuOTE0MDYzIDcgNDIuMjgxMjUgNyA0My4wODk4NDQgQyA3IDQzLjUzNTE1NiA3LjE5NTMxMyA0My44MzU5MzggNy41IDQzLjk0NTMxMyBMIDI3LjY3OTY4OCAyMy44ODI4MTMgWiAiPjwvcGF0aD48cGF0aCBzdHlsZT0iIGZpbGw6I0RDRTc3NTsiIGQ9Ik0gMzMuMjM4MjgxIDE4LjM1OTM3NSBMIDI0LjkyOTY4OCAxMy41NjI1IEMgMjQuOTI5Njg4IDEzLjU2MjUgOS42ODM1OTQgNC43NjE3MTkgOC43ODkwNjMgNC4yNDIxODggQyA4LjQwMjM0NCA0LjAxOTUzMSA4LjAxOTUzMSAzLjk2MDkzOCA3LjcwMzEyNSA0LjA0Mjk2OSBMIDI3LjY4MzU5NCAyMy44ODI4MTMgWiAiPjwvcGF0aD48cGF0aCBzdHlsZT0iIGZpbGw6I0QzMkYyRjsiIGQ9Ik0gOC40MTc5NjkgNDMuODAwNzgxIEMgOC45NDkyMTkgNDMuNDkyMTg4IDIzLjY5OTIxOSAzNC45NzY1NjMgMzMuMjgxMjUgMjkuNDQ1MzEzIEwgMjcuNjc5Njg4IDIzLjg4MjgxMyBMIDcuNSA0My45NDUzMTMgQyA3Ljc0NjA5NCA0NC4wMzkwNjMgOC4wNjY0MDYgNDQuMDAzOTA2IDguNDE3OTY5IDQzLjgwMDc4MSBaICI+PC9wYXRoPjxwYXRoIHN0eWxlPSIgZmlsbDojRkJDMDJEOyIgZD0iTSA0MS4zOTg0MzggMjMuMDcwMzEzIEMgNDAuNjAxNTYzIDIyLjY0MDYyNSAzMy4yOTY4NzUgMTguMzk0NTMxIDMzLjI5Njg3NSAxOC4zOTQ1MzEgTCAzMy4yMzgyODEgMTguMzU5Mzc1IEwgMjcuNjc5Njg4IDIzLjg4MjgxMyBMIDMzLjI4MTI1IDI5LjQ0NTMxMyBDIDM3LjcxNDg0NCAyNi44ODY3MTkgNDEuMDQyOTY5IDI0Ljk2NDg0NCA0MS4zMzk4NDQgMjQuNzkyOTY5IEMgNDIuMjg1MTU2IDI0LjI0NjA5NCA0Mi4xOTUzMTMgMjMuNSA0MS4zOTg0MzggMjMuMDcwMzEzIFogIj48L3BhdGg+PC9nPjwvc3ZnPg==') 50% 50% no-repeat; + background-size: 100%; +} + +#btnSend +{ + height: 42px; +} + +#btnSend .spinnerbox .spinner +{ + margin: 0; + padding: 0; + height: 16px; + width: 16px; +} + +#btnSend .spinnerbox +{ + margin: -8px; + display: flex; + justify-content: center; + align-items: center; + align-content: center; +} + +input[type='number'] { + -moz-appearance:textfield; +} + +input::-webkit-outer-spin-button, +input::-webkit-inner-spin-button { + -webkit-appearance: none; +} + +.input-invalid, +.input-invalid:hover, +.input-invalid:active +{ + border-color: var(--input-invalid-color) !important; + box-shadow: none !important; +} + +.card.success { + --card-back-color: rgb(48, 135, 50); + --card-border-color: rgba(0, 0, 0, 0.3);; +} + +.fullcenterflex +{ + width: 100%; + height: 100%; + position: absolute; + top: 0; + left: 0; + display: flex; + justify-content: center; + align-items: center; + align-content: center; + pointer-events: none; +} + +.fullcenterflex .card +{ + pointer-events: auto; +} + +a.card, +a.card:active, +a.card:visited, +a.card:hover +{ + color: #000; + text-decoration: none; +} + +a.card:hover +{ + box-shadow: 0 0 16px #AAA; +} + +table.scode_table { + max-height: none; +} + +table.scode_table td:nth-child(2) { + flex-grow: 3; +} + +table.scode_table th:nth-child(2) { + flex-grow: 3; +} + +#mainpnl h2 { + margin-top: 1.75rem; +} + +.linkcaption:hover, +.linkcaption:focus { + text-decoration: none; +} + +pre, pre span +{ + font-family: Menlo, Consolas, monospace; + background: #F9F9F9;; +} \ No newline at end of file diff --git a/server/website/css/toastify.min.css b/server/website/css/toastify.min.css new file mode 100644 index 0000000..765a518 --- /dev/null +++ b/server/website/css/toastify.min.css @@ -0,0 +1,15 @@ +/** + * Minified by jsDelivr using clean-css v4.2.0. + * Original file: /npm/toastify.js@1.3.0/src/toastify.css + * + * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files + */ +/*! + * Toastify js 1.2.2 + * https://github.com/apvarun/toastify-js + * @license MIT licensed + * + * Copyright (C) 2018 Varun A P + */ +.toastify{padding:12px 20px;color:#fff;display:inline-block;box-shadow:0 3px 6px -1px rgba(0,0,0,.12),0 10px 36px -4px rgba(77,96,232,.3);background:-webkit-linear-gradient(315deg,#73a5ff,#5477f5);background:linear-gradient(135deg,#73a5ff,#5477f5);position:fixed;opacity:0;transition:all .4s cubic-bezier(.215,.61,.355,1);border-radius:2px;cursor:pointer;text-decoration:none;max-width:calc(50% - 20px)}.toastify.on{opacity:1}.toast-close{opacity:.4;padding:0 5px}.right{right:15px}.left{left:15px}.top{top:-150px}.bottom{bottom:-150px}.rounded{border-radius:25px}.avatar{width:1.5em;height:1.5em;margin:0 5px;border-radius:2px}@media only screen and (max-width:360px){.left,.right{margin-left:auto;margin-right:auto;left:0;right:0;max-width:fit-content}} +/*# sourceMappingURL=/sm/734ed69e2fe87a4469526acc0a10708fa8e0211c7d4359f9e034ceb89bb5d540.map */ \ No newline at end of file diff --git a/server/website/favicon.ico b/server/website/favicon.ico new file mode 100644 index 0000000..3729f2e Binary files /dev/null and b/server/website/favicon.ico differ diff --git a/server/website/favicon.png b/server/website/favicon.png new file mode 100644 index 0000000..a45baf3 Binary files /dev/null and b/server/website/favicon.png differ diff --git a/server/website/index.html b/server/website/index.html new file mode 100644 index 0000000..cd3c81b --- /dev/null +++ b/server/website/index.html @@ -0,0 +1,69 @@ + + + + + Simple Cloud Notifications + + + + + + + + + + + + + +
+ + + API + +

Simple Cloud Notifier

+ +
+
+
type="number">
+
+ +
+
+
type="text" maxlength="64">
+
+ +
+
+
+ +
+
+ +
+
+
type="text" maxlength="80">
+
+ +
+
+
+
+ +
+
+
+
+
+ + + + + \ No newline at end of file diff --git a/server/website/js/logic.js b/server/website/js/logic.js new file mode 100644 index 0000000..6171928 --- /dev/null +++ b/server/website/js/logic.js @@ -0,0 +1,90 @@ + +function send() +{ + let me = document.getElementById("btnSend"); + if (me.classList.contains("btn-disabled")) return; + + me.innerHTML = "
"; + + me.classList.add("btn-disabled"); + + let uid = document.getElementById("uid"); + let key = document.getElementById("ukey"); + let msg = document.getElementById("msg"); + let txt = document.getElementById("txt"); + let pio = document.getElementById("prio"); + + uid.classList.remove('input-invalid'); + key.classList.remove('input-invalid'); + msg.classList.remove('input-invalid'); + txt.classList.remove('input-invalid'); + pio.classList.remove('input-invalid'); + + let data = new FormData(); + data.append('user_id', uid.value); + data.append('user_key', key.value); + data.append('title', msg.value); + data.append('content', txt.value); + data.append('priority', pio.value); + + let xhr = new XMLHttpRequest(); + xhr.open('POST', '/', true); + xhr.onreadystatechange = function () + { + if (xhr.readyState !== 4) return; + + console.log('Status: ' + xhr.status); + if (xhr.status === 200 || xhr.status === 401 || xhr.status === 403 || xhr.status === 412) + { + let resp = JSON.parse(xhr.responseText); + if (!resp.success || xhr.status !== 200) + { + if (resp.errhighlight === 101) uid.classList.add('input-invalid'); + if (resp.errhighlight === 102) key.classList.add('input-invalid'); + if (resp.errhighlight === 103) msg.classList.add('input-invalid'); + if (resp.errhighlight === 104) txt.classList.add('input-invalid'); + if (resp.errhighlight === 105) pio.classList.add('input-invalid'); + + Toastify({ + text: resp.message, + gravity: "top", + positionLeft: false, + backgroundColor: "#D32F2F", + }).showToast(); + } + else + { + window.location.href = + '/message_sent' + + '?ok=' + 1 + + '&message_count=' + resp.messagecount + + '"a=' + resp.quota + + '"a_remain=' + (resp.quota_max-resp.quota) + + '"a_max=' + resp.quota_max + + '&preset_user_id=' + uid.value + + '&preset_user_key=' + key.value; + } + } + else + { + Toastify({ + text: 'Request failed: Statuscode=' + xhr.status, + gravity: "top", + positionLeft: false, + backgroundColor: "#D32F2F", + }).showToast(); + } + + me.classList.remove("btn-disabled"); + me.innerHTML = "Send"; + }; + xhr.send(data); +} + +window.addEventListener("load",function () +{ + let btnSend = document.getElementById("btnSend"); + + if (btnSend !== undefined) btnSend.onclick = function () { send(); return false; }; + +},false); \ No newline at end of file diff --git a/server/website/js/toastify.js b/server/website/js/toastify.js new file mode 100644 index 0000000..bbd01cd --- /dev/null +++ b/server/website/js/toastify.js @@ -0,0 +1,8 @@ +/** + * Minified by jsDelivr using UglifyJS v3.4.3. + * Original file: /npm/toastify-js@1.3.0/src/toastify.js + * + * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files + */ +!function(t,o){"object"==typeof module&&module.exports?(require("./toastify.css"),module.exports=o()):t.Toastify=o()}(this,function(t){var i=function(t){return new i.lib.init(t)};function r(t,o){return!(!t||"string"!=typeof o)&&!!(t.className&&-1 + + + + Simple Cloud Notifications + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/server/website/website.go b/server/website/website.go new file mode 100644 index 0000000..b1661ab --- /dev/null +++ b/server/website/website.go @@ -0,0 +1,8 @@ +package website + +import "embed" + +//go:embed * +//go:embed css/* +//go:embed js/* +var Assets embed.FS