generated from moebiusband/go-http-skeleton
	Initial commit
This commit is contained in:
		
							
								
								
									
										23
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| # ---> Go | ||||
| # If you prefer the allow list template instead of the deny list, see community template: | ||||
| # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore | ||||
| # | ||||
| # Binaries for programs and plugins | ||||
| *.exe | ||||
| *.exe~ | ||||
| *.dll | ||||
| *.so | ||||
| *.dylib | ||||
|  | ||||
| # Test binary, built with `go test -c` | ||||
| *.test | ||||
|  | ||||
| # Output of the go coverage tool, specifically when used with LiteIDE | ||||
| *.out | ||||
|  | ||||
| # Dependency directories (remove the comment below to include it) | ||||
| # vendor/ | ||||
|  | ||||
| # Go workspace file | ||||
| go.work | ||||
|  | ||||
							
								
								
									
										9
									
								
								LICENSE
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								LICENSE
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| MIT License | ||||
|  | ||||
| Copyright (c) 2024 moebiusband | ||||
|  | ||||
| Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: | ||||
|  | ||||
| The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. | ||||
|  | ||||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | ||||
							
								
								
									
										18
									
								
								Makefile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								Makefile
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| TARGET = ./site | ||||
|  | ||||
| .PHONY: clean distclean test components $(TARGET) | ||||
|  | ||||
| .NOTPARALLEL: | ||||
|  | ||||
| $(TARGET): components | ||||
| 	$(info ===>  BUILD site) | ||||
| 	@go build ./cmd/site | ||||
|  | ||||
| components: | ||||
| 	$(info ===>  BUILD templates) | ||||
| 	cd web/components && templ generate | ||||
|  | ||||
| clean: | ||||
| 	$(info ===>  CLEAN) | ||||
| 	@go clean | ||||
| 	@rm -f $(TARGET) | ||||
							
								
								
									
										3
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| # go-http-skeleton | ||||
|  | ||||
| A basic go http skeleton as baseline for website projects | ||||
							
								
								
									
										41
									
								
								cmd/site/main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								cmd/site/main.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | ||||
| package main | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"log/slog" | ||||
| 	"os" | ||||
| 	"os/signal" | ||||
|  | ||||
| 	"git.clustercockpit.org/moebiusband/go-http-skeleton/internal/util" | ||||
| 	"git.clustercockpit.org/moebiusband/go-http-skeleton/web" | ||||
| 	"github.com/joho/godotenv" | ||||
| ) | ||||
|  | ||||
| func main() { | ||||
| 	godotenv.Load() | ||||
|  | ||||
| 	logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) | ||||
| 	logger.Info("Starting http server") | ||||
| 	defer logger.Info("Stopping http server") | ||||
|  | ||||
| 	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) | ||||
| 	defer stop() | ||||
|  | ||||
| 	if err := run(ctx); err != nil { | ||||
| 		logger.Error("Error running http server", slog.Any("err", err)) | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func run(ctx context.Context) error { | ||||
| 	eg := util.NewErrGroupSharedCtx( | ||||
| 		ctx, | ||||
| 		web.RunBlocking(8080), | ||||
| 	) | ||||
| 	if err := eg.Wait(); err != nil { | ||||
| 		return fmt.Errorf("error running http server: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
							
								
								
									
										33
									
								
								go.mod
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								go.mod
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | ||||
| module git.clustercockpit.org/moebiusband/go-http-skeleton | ||||
|  | ||||
| go 1.22 | ||||
|  | ||||
| require ( | ||||
| 	github.com/a-h/templ v0.2.747 // indirect | ||||
| 	github.com/benbjohnson/hashfs v0.2.2 // indirect | ||||
| 	github.com/delaneyj/datastar v0.16.2 // indirect | ||||
| 	github.com/delaneyj/gostar v0.7.3 // indirect | ||||
| 	github.com/dustin/go-humanize v1.0.1 // indirect | ||||
| 	github.com/go-sanitize/sanitize v1.1.0 // indirect | ||||
| 	github.com/goccy/go-json v0.10.3 // indirect | ||||
| 	github.com/google/uuid v1.6.0 // indirect | ||||
| 	github.com/gorilla/securecookie v1.1.2 // indirect | ||||
| 	github.com/gorilla/sessions v1.3.0 // indirect | ||||
| 	github.com/igrmk/treemap/v2 v2.0.1 // indirect | ||||
| 	github.com/joho/godotenv v1.5.1 // indirect | ||||
| 	github.com/mattn/go-isatty v0.0.20 // indirect | ||||
| 	github.com/ncruces/go-strftime v0.1.9 // indirect | ||||
| 	github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect | ||||
| 	github.com/samber/lo v1.44.0 // indirect | ||||
| 	github.com/valyala/bytebufferpool v1.0.0 // indirect | ||||
| 	golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect | ||||
| 	golang.org/x/sync v0.7.0 // indirect | ||||
| 	golang.org/x/sys v0.22.0 // indirect | ||||
| 	golang.org/x/text v0.16.0 // indirect | ||||
| 	google.golang.org/protobuf v1.34.2 // indirect | ||||
| 	modernc.org/libc v1.54.2 // indirect | ||||
| 	modernc.org/mathutil v1.6.0 // indirect | ||||
| 	modernc.org/memory v1.8.0 // indirect | ||||
| 	modernc.org/sqlite v1.30.1 // indirect | ||||
| 	zombiezen.com/go/sqlite v1.3.0 // indirect | ||||
| ) | ||||
							
								
								
									
										68
									
								
								go.sum
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								go.sum
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,68 @@ | ||||
| github.com/a-h/templ v0.2.747 h1:D0dQ2lxC3W7Dxl6fxQ/1zZHBQslSkTSvl5FxP/CfdKg= | ||||
| github.com/a-h/templ v0.2.747/go.mod h1:69ObQIbrcuwPCU32ohNaWce3Cb7qM5GMiqN1K+2yop4= | ||||
| github.com/benbjohnson/hashfs v0.2.2 h1:vFZtksphM5LcnMRFctj49jCUkCc7wp3NP6INyfjkse4= | ||||
| github.com/benbjohnson/hashfs v0.2.2/go.mod h1:7OMXaMVo1YkfiIPxKrl7OXkUTUgWjmsAKyR+E6xDIRM= | ||||
| github.com/delaneyj/datastar v0.16.2 h1:gFvuVZE1Ze95YcfyrxLb+wAlaMYDCx/k24hxObRYU+s= | ||||
| github.com/delaneyj/datastar v0.16.2/go.mod h1:+h1DvxZr7kOxWhjOxMgYTz17pfVR+fQ2me//3pqNm9I= | ||||
| github.com/delaneyj/gostar v0.7.3 h1:+7vEN7T9Y6HESGMHwlXzsTC/apgVwQy1bR4NQ7IKYfo= | ||||
| github.com/delaneyj/gostar v0.7.3/go.mod h1:mlxRWAVbntRR2VWlpXAzt7y9HY+bQtEm/lsyFnGLx/w= | ||||
| github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= | ||||
| github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= | ||||
| github.com/go-sanitize/sanitize v1.1.0 h1:wq9tl5+VfkyCacCZIVQf6ksegRpfWl3N2vAyyYD0F1I= | ||||
| github.com/go-sanitize/sanitize v1.1.0/go.mod h1:r+anm3xp/Y1+pTNvPSgHMznwb0VVZgszoMQs3naOf0A= | ||||
| github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= | ||||
| github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= | ||||
| github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= | ||||
| github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= | ||||
| github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= | ||||
| github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= | ||||
| github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= | ||||
| github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= | ||||
| github.com/gorilla/sessions v1.3.0 h1:XYlkq7KcpOB2ZhHBPv5WpjMIxrQosiZanfoy1HLZFzg= | ||||
| github.com/gorilla/sessions v1.3.0/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ= | ||||
| github.com/igrmk/treemap/v2 v2.0.1 h1:Jhy4z3yhATvYZMWCmxsnHO5NnNZBdueSzvxh6353l+0= | ||||
| github.com/igrmk/treemap/v2 v2.0.1/go.mod h1:PkTPvx+8OHS8/41jnnyVY+oVsfkaOUZGcr+sfonosd4= | ||||
| github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= | ||||
| github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= | ||||
| github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= | ||||
| github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= | ||||
| github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= | ||||
| github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= | ||||
| github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= | ||||
| github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= | ||||
| github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= | ||||
| github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= | ||||
| github.com/samber/lo v1.44.0 h1:5il56KxRE+GHsm1IR+sZ/6J42NODigFiqCWpSc2dybA= | ||||
| github.com/samber/lo v1.44.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU= | ||||
| github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= | ||||
| github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= | ||||
| golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY= | ||||
| golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= | ||||
| golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= | ||||
| golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= | ||||
| golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= | ||||
| golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= | ||||
| golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= | ||||
| golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= | ||||
| golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= | ||||
| golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= | ||||
| google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= | ||||
| google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= | ||||
| modernc.org/libc v1.41.0 h1:g9YAc6BkKlgORsUWj+JwqoB1wU3o4DE3bM3yvA3k+Gk= | ||||
| modernc.org/libc v1.41.0/go.mod h1:w0eszPsiXoOnoMJgrXjglgLuDy/bt5RR4y3QzUUeodY= | ||||
| modernc.org/libc v1.54.2 h1:9ymAodb+3v85YfBIZqn62BGgO4L9zF2Hx4LNb6dSU/Q= | ||||
| modernc.org/libc v1.54.2/go.mod h1:B0D6klDmSmnq26T1iocn9kzyX6NtbzjuI3+oX/xfvng= | ||||
| modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= | ||||
| modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= | ||||
| modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E= | ||||
| modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E= | ||||
| modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= | ||||
| modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU= | ||||
| modernc.org/sqlite v1.29.1 h1:19GY2qvWB4VPw0HppFlZCPAbmxFU41r+qjKZQdQ1ryA= | ||||
| modernc.org/sqlite v1.29.1/go.mod h1:hG41jCYxOAOoO6BRK66AdRlmOcDzXf7qnwlwjUIOqa0= | ||||
| modernc.org/sqlite v1.30.1 h1:YFhPVfu2iIgUf9kuA1CR7iiHdcEEsI2i+yjRYHscyxk= | ||||
| modernc.org/sqlite v1.30.1/go.mod h1:DUmsiWQDaAvU4abhc/N+djlom/L2o8f7gZ95RCvyoLU= | ||||
| zombiezen.com/go/sqlite v1.3.0 h1:98g1gnCm+CNz6AuQHu0gqyw7gR2WU3O3PJufDOStpUs= | ||||
| zombiezen.com/go/sqlite v1.3.0/go.mod h1:yRl27//s/9aXU3RWs8uFQwjkTG9gYNGEls6+6SvrclY= | ||||
							
								
								
									
										231
									
								
								internal/repository/dbConn.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										231
									
								
								internal/repository/dbConn.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,231 @@ | ||||
| package repository | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"runtime" | ||||
| 	"time" | ||||
|  | ||||
| 	"google.golang.org/protobuf/types/known/timestamppb" | ||||
| 	"zombiezen.com/go/sqlite" | ||||
| 	"zombiezen.com/go/sqlite/sqlitemigration" | ||||
| 	"zombiezen.com/go/sqlite/sqlitex" | ||||
| ) | ||||
|  | ||||
| type Database struct { | ||||
| 	filename   string | ||||
| 	migrations []string | ||||
| 	writePool  *sqlitex.Pool | ||||
| 	readPool   *sqlitex.Pool | ||||
| } | ||||
|  | ||||
| type TxFn func(tx *sqlite.Conn) error | ||||
|  | ||||
| func NewDatabase(ctx context.Context, dbFilename string, migrations []string) (*Database, error) { | ||||
| 	if dbFilename == "" { | ||||
| 		return nil, fmt.Errorf("database filename is required") | ||||
| 	} | ||||
|  | ||||
| 	db := &Database{ | ||||
| 		filename:   dbFilename, | ||||
| 		migrations: migrations, | ||||
| 	} | ||||
|  | ||||
| 	if err := db.Reset(ctx, false); err != nil { | ||||
| 		return nil, fmt.Errorf("failed to reset database: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	return db, nil | ||||
| } | ||||
|  | ||||
| func (db *Database) WriteWithoutTx(ctx context.Context, fn TxFn) error { | ||||
| 	conn, err := db.writePool.Take(ctx) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("failed to take write connection: %w", err) | ||||
| 	} | ||||
| 	if conn == nil { | ||||
| 		return fmt.Errorf("could not get write connection from pool") | ||||
| 	} | ||||
| 	defer db.writePool.Put(conn) | ||||
|  | ||||
| 	if err := fn(conn); err != nil { | ||||
| 		return fmt.Errorf("could not execute write transaction: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (db *Database) Reset(ctx context.Context, shouldClear bool) (err error) { | ||||
| 	if err := db.Close(); err != nil { | ||||
| 		return fmt.Errorf("could not close database: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	if shouldClear { | ||||
| 		if err := os.RemoveAll(db.filename + "*"); err != nil { | ||||
| 			return fmt.Errorf("could not remove database file: %w", err) | ||||
| 		} | ||||
| 	} | ||||
| 	if err := os.MkdirAll(filepath.Dir(db.filename), 0755); err != nil { | ||||
| 		return fmt.Errorf("could not create database directory: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	uri := fmt.Sprintf("file:%s?_journal_mode=WAL&_synchronous=NORMAL", db.filename) | ||||
|  | ||||
| 	db.writePool, err = sqlitex.NewPool(uri, sqlitex.PoolOptions{ | ||||
| 		PoolSize: 1, | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("could not open write pool: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	db.readPool, err = sqlitex.NewPool(uri, sqlitex.PoolOptions{ | ||||
| 		PoolSize: runtime.NumCPU(), | ||||
| 	}) | ||||
|  | ||||
| 	if err := db.WriteTX(ctx, func(tx *sqlite.Conn) error { | ||||
| 		foreignKeysStmt := tx.Prep("PRAGMA foreign_keys = ON") | ||||
| 		defer foreignKeysStmt.Finalize() | ||||
| 		if _, err := foreignKeysStmt.Step(); err != nil { | ||||
| 			return fmt.Errorf("failed to enable foreign keys: %w", err) | ||||
| 		} | ||||
|  | ||||
| 		return nil | ||||
| 	}); err != nil { | ||||
| 		return fmt.Errorf("failed to initialize database: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	schema := sqlitemigration.Schema{Migrations: db.migrations} | ||||
| 	conn, err := db.writePool.Take(ctx) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("failed to take write connection: %w", err) | ||||
| 	} | ||||
| 	defer db.writePool.Put(conn) | ||||
|  | ||||
| 	if err := sqlitemigration.Migrate(ctx, conn, schema); err != nil { | ||||
| 		db.writePool.Put(conn) | ||||
| 		return fmt.Errorf("failed to migrate database: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (db *Database) Close() error { | ||||
| 	errs := []error{} | ||||
| 	if db.writePool != nil { | ||||
| 		errs = append(errs, db.writePool.Close()) | ||||
| 	} | ||||
|  | ||||
| 	if db.readPool != nil { | ||||
| 		errs = append(errs, db.readPool.Close()) | ||||
| 	} | ||||
|  | ||||
| 	return errors.Join(errs...) | ||||
| } | ||||
|  | ||||
| func (db *Database) WriteTX(ctx context.Context, fn TxFn) (err error) { | ||||
| 	conn, err := db.writePool.Take(ctx) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("failed to take write connection: %w", err) | ||||
| 	} | ||||
| 	if conn == nil { | ||||
| 		return fmt.Errorf("could not get write connection from pool") | ||||
| 	} | ||||
| 	defer db.writePool.Put(conn) | ||||
|  | ||||
| 	endFn, err := sqlitex.ImmediateTransaction(conn) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("could not start transaction: %w", err) | ||||
| 	} | ||||
| 	defer endFn(&err) | ||||
|  | ||||
| 	if err := fn(conn); err != nil { | ||||
| 		return fmt.Errorf("could not execute write transaction: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (db *Database) ReadTX(ctx context.Context, fn TxFn) (err error) { | ||||
| 	conn, err := db.readPool.Take(ctx) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("failed to take read connection: %w", err) | ||||
| 	} | ||||
| 	if conn == nil { | ||||
| 		return fmt.Errorf("could not get read connection from pool") | ||||
| 	} | ||||
| 	defer db.readPool.Put(conn) | ||||
|  | ||||
| 	endFn := sqlitex.Transaction(conn) | ||||
| 	defer endFn(&err) | ||||
|  | ||||
| 	if err := fn(conn); err != nil { | ||||
| 		return fmt.Errorf("could not execute read transaction: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| const ( | ||||
| 	secondsInADay      = 86400 | ||||
| 	UnixEpochJulianDay = 2440587.5 | ||||
| ) | ||||
|  | ||||
| var JulianZeroTime = JulianDayToTime(0) | ||||
|  | ||||
| // TimeToJulianDay converts a time.Time into a Julian day. | ||||
| func TimeToJulianDay(t time.Time) float64 { | ||||
| 	return float64(t.UTC().Unix())/secondsInADay + UnixEpochJulianDay | ||||
| } | ||||
|  | ||||
| // JulianDayToTime converts a Julian day into a time.Time. | ||||
| func JulianDayToTime(d float64) time.Time { | ||||
| 	return time.Unix(int64((d-UnixEpochJulianDay)*secondsInADay), 0).UTC() | ||||
| } | ||||
|  | ||||
| func JulianNow() float64 { | ||||
| 	return TimeToJulianDay(time.Now()) | ||||
| } | ||||
|  | ||||
| func TimestampJulian(ts *timestamppb.Timestamp) float64 { | ||||
| 	return TimeToJulianDay(ts.AsTime()) | ||||
| } | ||||
|  | ||||
| func JulianDayToTimestamp(f float64) *timestamppb.Timestamp { | ||||
| 	t := JulianDayToTime(f) | ||||
| 	return timestamppb.New(t) | ||||
| } | ||||
|  | ||||
| func StmtJulianToTimestamp(stmt *sqlite.Stmt, colName string) *timestamppb.Timestamp { | ||||
| 	julianDays := stmt.GetFloat(colName) | ||||
| 	return JulianDayToTimestamp(julianDays) | ||||
| } | ||||
|  | ||||
| func StmtJulianToTime(stmt *sqlite.Stmt, colName string) time.Time { | ||||
| 	julianDays := stmt.GetFloat(colName) | ||||
| 	return JulianDayToTime(julianDays) | ||||
| } | ||||
|  | ||||
| func DurationToMilliseconds(d time.Duration) int64 { | ||||
| 	return int64(d / time.Millisecond) | ||||
| } | ||||
|  | ||||
| func MillisecondsToDuration(ms int64) time.Duration { | ||||
| 	return time.Duration(ms) * time.Millisecond | ||||
| } | ||||
|  | ||||
| func StmtBytes(stmt *sqlite.Stmt, colName string) []byte { | ||||
| 	bl := stmt.GetLen(colName) | ||||
| 	if bl == 0 { | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	buf := make([]byte, bl) | ||||
| 	if writtent := stmt.GetBytes(colName, buf); writtent != bl { | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	return buf | ||||
| } | ||||
							
								
								
									
										67
									
								
								internal/util/egctx.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								internal/util/egctx.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,67 @@ | ||||
| package util | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
|  | ||||
| 	"golang.org/x/sync/errgroup" | ||||
| ) | ||||
|  | ||||
| type ErrGroupSharedCtx struct { | ||||
| 	eg  *errgroup.Group | ||||
| 	ctx context.Context | ||||
| } | ||||
|  | ||||
| type CtxErrFunc func(ctx context.Context) error | ||||
|  | ||||
| func NewErrGroupSharedCtx(ctx context.Context, funcs ...CtxErrFunc) *ErrGroupSharedCtx { | ||||
| 	eg, ctx := errgroup.WithContext(ctx) | ||||
|  | ||||
| 	egCtx := &ErrGroupSharedCtx{ | ||||
| 		eg:  eg, | ||||
| 		ctx: ctx, | ||||
| 	} | ||||
|  | ||||
| 	egCtx.Go(funcs...) | ||||
|  | ||||
| 	return egCtx | ||||
| } | ||||
|  | ||||
| func (egc *ErrGroupSharedCtx) Go(funcs ...CtxErrFunc) { | ||||
| 	for _, f := range funcs { | ||||
| 		fn := f | ||||
| 		egc.eg.Go(func() error { | ||||
| 			return fn(egc.ctx) | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (egc *ErrGroupSharedCtx) Wait() error { | ||||
| 	return egc.eg.Wait() | ||||
| } | ||||
|  | ||||
| type ErrGroupSeparateCtx struct { | ||||
| 	eg *errgroup.Group | ||||
| } | ||||
|  | ||||
| func NewErrGroupSeparateCtx() *ErrGroupSeparateCtx { | ||||
| 	eg := &errgroup.Group{} | ||||
|  | ||||
| 	egCtx := &ErrGroupSeparateCtx{ | ||||
| 		eg: eg, | ||||
| 	} | ||||
|  | ||||
| 	return egCtx | ||||
| } | ||||
|  | ||||
| func (egc *ErrGroupSeparateCtx) Go(ctx context.Context, funcs ...CtxErrFunc) { | ||||
| 	for _, f := range funcs { | ||||
| 		fn := f | ||||
| 		egc.eg.Go(func() error { | ||||
| 			return fn(ctx) | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (egc *ErrGroupSeparateCtx) Wait() error { | ||||
| 	return egc.eg.Wait() | ||||
| } | ||||
							
								
								
									
										34
									
								
								web/components/base.templ
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								web/components/base.templ
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| package components | ||||
|  | ||||
| templ Page(content templ.Component) { | ||||
| 	<!DOCTYPE html> | ||||
| 	<html lang="en"> | ||||
| 		<head> | ||||
| 			<meta charset="utf-8"/> | ||||
| 			<meta name="viewport" content="width=device-width,initial-scale=1"/> | ||||
| 			<meta name="description" content="A sane golang website template."/> | ||||
| 			<link rel="preload" as="style" href="static/css/fonts.css" media="screen,print"/> | ||||
| 			<link rel="preload" as="style" href="static/css/site.screen.css" media="screen"/> | ||||
| 			<link rel="preload" as="font" href="static/css/fonts/poppins-western.woff2" media="screen,print" crossorigin/> | ||||
| 			<link rel="preload" as="font" href="static/css/fonts/poppins-western-bold.woff2" media="screen,print" crossorigin/> | ||||
| 			<link rel="preload" as="font" href="static/css/fonts/interface-Regular.woff2" media="screen,print" crossorigin/> | ||||
| 			<link rel="stylesheet" href="static/css/fonts.css" media="screen,print"/> | ||||
| 			<link rel="stylesheet" href="static/css/site.screen.css" media="screen"/> | ||||
| 			<link rel="stylesheet" href="static/css/site.print.css" media="print"/> | ||||
| 			<link rel="apple-touch-icon" href="static/apple-touch-icon.png"/> | ||||
| 			<link rel="icon" href="static/favicon.ico"/> | ||||
| 			<link rel="icon" href="static/favicon.svg" type="image/svg+xml"/> | ||||
| 			<title>Awesome content</title> | ||||
| 		</head> | ||||
| 		<body> | ||||
| 			<div id="fauxBody"> | ||||
| 				@Header() | ||||
| 				<main> | ||||
| 					@content | ||||
| 				</main> | ||||
| 				<a href="#top" id="backToTop"><span>Back To Top</span></a> | ||||
| 				@Footer() | ||||
| 			</div> | ||||
| 		</body> | ||||
| 	</html> | ||||
| } | ||||
							
								
								
									
										5
									
								
								web/components/errors.templ
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								web/components/errors.templ
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| package components | ||||
|  | ||||
| templ Page404() { | ||||
| 	<div>Page not found</div> | ||||
| } | ||||
							
								
								
									
										37
									
								
								web/components/footer.templ
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								web/components/footer.templ
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,37 @@ | ||||
| package components | ||||
|  | ||||
| templ Footer() { | ||||
| 	<footer> | ||||
| 		<section> | ||||
| 			<h2 class="optional">Legal Disclaimer</h2> | ||||
| 			<p> | ||||
| 				© Jason M. Knight, <span>All Rights Reserved.</span> | ||||
| 			</p> | ||||
| 		</section> | ||||
| 		<section id="socialMedia"> | ||||
| 			<h2 class="optional">Visit Me On Social Media</h2> | ||||
| 			<ul class="socialMenu"> | ||||
| 				<li> | ||||
| 					<a href="#" class="icon_facebook"> | ||||
| 						<span>Facebook</span> | ||||
| 					</a> | ||||
| 				</li> | ||||
| 				<li> | ||||
| 					<a href="#" class="icon_linkedIn"> | ||||
| 						<span>LinkedIn</span> | ||||
| 					</a> | ||||
| 				</li> | ||||
| 				<li> | ||||
| 					<a href="#" class="icon_x"> | ||||
| 						<span>The Artist Formerly Known As Twitter</span> | ||||
| 					</a> | ||||
| 				</li> | ||||
| 				<li> | ||||
| 					<a href="#" class="icon_medium"> | ||||
| 						<span>Medium</span> | ||||
| 					</a> | ||||
| 				</li> | ||||
| 			</ul> | ||||
| 		</section> | ||||
| 	</footer> | ||||
| } | ||||
							
								
								
									
										29
									
								
								web/components/header.templ
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								web/components/header.templ
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | ||||
| package components | ||||
|  | ||||
| templ Header() { | ||||
| 	<header id="top"> | ||||
| 		<a href="./"> | ||||
| 			<h1>Website template</h1> | ||||
| 			<p>A Golang Templ based skeleton</p> | ||||
| 		</a> | ||||
| 		<a href="#mainMenu" class="mainMenuOpen" hidden> | ||||
| 			<span>Open Main Menu</span> | ||||
| 		</a> | ||||
| 		<nav id="mainMenu"> | ||||
| 			<a href="#" class="modalClose" hidden tabindex="-1"> | ||||
| 				<span>Close Main Menu</span> | ||||
| 			</a> | ||||
| 			<div data-modalTitle="Main Menu"> | ||||
| 				<a href="#" class="modalClose icon_close" hidden> | ||||
| 					<span>Close Main Menu</span> | ||||
| 				</a> | ||||
| 				<ul> | ||||
| 					<li><a href="./">Home</a></li> | ||||
| 					<li><a href="#whatIDo">What I Do</a></li> | ||||
| 					<li><a href="#testimonials">Testimonials</a></li> | ||||
| 					<li><a href="#contact">Contact</a></li> | ||||
| 				</ul> | ||||
| 			</div> | ||||
| 		</nav> | ||||
| 	</header> | ||||
| } | ||||
							
								
								
									
										16
									
								
								web/components/index.templ
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								web/components/index.templ
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| package components | ||||
|  | ||||
| templ IndexPage() { | ||||
| 	<section class="hero" id="semanticEasy"> | ||||
| 		<div> | ||||
| 			<header> | ||||
| 				<h2>Semantic Accessible <span>Markup Is Easy</span></h2> | ||||
| 				<p>Stop Making It Hard</p> | ||||
| 			</header> | ||||
| 			<p> | ||||
| 				I have spent the past decade and a half helping site owners out of deep legal troubles for accessibility woes, improve workplace conditions, gain essential leadership skills, by way of promoting good practices. I can help you learn to leverage caching models, write accessibible HTML, keep your style within guidelines such as the WCAG, and aid in lawsuits involving the US ADA, UQ EQA, and a plethora of other accessibility laws from around the globe. | ||||
| 			</p> | ||||
| 			<a class="action" href="#">Learn More</a> | ||||
| 		</div> | ||||
| 	</section> | ||||
| } | ||||
							
								
								
									
										22
									
								
								web/routes.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								web/routes.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| package web | ||||
|  | ||||
| import ( | ||||
| 	"net/http" | ||||
| 	"time" | ||||
|  | ||||
| 	"git.clustercockpit.org/moebiusband/go-http-skeleton/web/components" | ||||
| 	"github.com/a-h/templ" | ||||
| 	"github.com/benbjohnson/hashfs" | ||||
| 	"github.com/gorilla/sessions" | ||||
| ) | ||||
|  | ||||
| func setupRoutes(r *http.ServeMux) error { | ||||
| 	r.Handle("GET /static/", hashfs.FileServer(staticSys)) | ||||
| 	r.Handle("GET /{$}", templ.Handler(components.Page(components.IndexPage()))) | ||||
| 	r.Handle("GET /404", templ.Handler(components.Page(components.Page404()), templ.WithStatus(http.StatusNotFound))) | ||||
|  | ||||
| 	sessionStore := sessions.NewCookieStore([]byte("datastar-session-secret")) | ||||
| 	sessionStore.MaxAge(int(24 * time.Hour / time.Second)) | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
							
								
								
									
										36
									
								
								web/server.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								web/server.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | ||||
| package web | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"embed" | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
|  | ||||
| 	"git.clustercockpit.org/moebiusband/go-http-skeleton/internal/util" | ||||
| 	"github.com/benbjohnson/hashfs" | ||||
| ) | ||||
|  | ||||
| //go:embed static/* | ||||
| var staticFS embed.FS | ||||
|  | ||||
| var staticSys = hashfs.NewFS(staticFS) | ||||
|  | ||||
| func RunBlocking(port int) util.CtxErrFunc { | ||||
| 	return func(ctx context.Context) error { | ||||
| 		r := http.NewServeMux() | ||||
|  | ||||
| 		setupRoutes(r) | ||||
|  | ||||
| 		srv := &http.Server{ | ||||
| 			Addr:    fmt.Sprintf(":%d", port), | ||||
| 			Handler: r, | ||||
| 		} | ||||
|  | ||||
| 		go func() { | ||||
| 			<-ctx.Done() | ||||
| 			srv.Shutdown(context.Background()) | ||||
| 		}() | ||||
|  | ||||
| 		return srv.ListenAndServe() | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										
											BIN
										
									
								
								web/static/apple-touch-icon.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								web/static/apple-touch-icon.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 12 KiB | 
							
								
								
									
										51
									
								
								web/static/css/fonts.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								web/static/css/fonts.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,51 @@ | ||||
| @font-face { | ||||
| 	font-display:swap; | ||||
| 	font-family:"flowtext"; | ||||
| 	font-style:normal; | ||||
| 	font-weight:normal; | ||||
| 	src: | ||||
| 		url("fonts/poppins-western.woff2") format("woff2"), | ||||
| 		url("fonts/poppins-western.woff") format("woff"); | ||||
| } | ||||
|  | ||||
| @font-face { | ||||
| 	font-display:swap; | ||||
| 	font-family:"flowtext"; | ||||
| 	font-style:normal; | ||||
| 	font-weight:bold; | ||||
| 	src: | ||||
| 		url("fonts/poppins-western-bold.woff2") format("woff2"), | ||||
| 		url("fonts/poppins-western-bold.woff") format("woff"); | ||||
| } | ||||
|  | ||||
| @font-face { | ||||
| 	font-display:swap; | ||||
| 	font-family:interface; | ||||
| 	font-style:normal; | ||||
| 	font-weight:normal; | ||||
| 	src: | ||||
| 		url("fonts/interface-Regular.woff2") format("woff2"), | ||||
| 		url("fonts/interface-Regular.woff") format("woff"); | ||||
| } | ||||
|  | ||||
| [class*="icon_"]:not(.iconAfter):before, | ||||
| [class*="icon_"].iconAfter:after { | ||||
| 	content:var(--iconChar); | ||||
| 	display:inline-block; | ||||
| 	vertical-align:middle; | ||||
| 	position:relative; | ||||
| 	font-family:interface; | ||||
| 	font-weight:normal; | ||||
| 	line-height:1; | ||||
| } | ||||
|  | ||||
| .icon_x             { --iconChar:"\E61B"; } | ||||
| .icon_close         { --iconChar:"\F00D"; } | ||||
| .icon_linkedIn      { --iconChar:"\F08C"; } | ||||
| .icon_facebook      { --iconChar:"\F09A"; } | ||||
| .icon_medium        { --iconChar:"\F23A"; } | ||||
| .icon_accessibility { --iconChar:"\F368"; } | ||||
| .icon_web           { --iconChar:"\F57D"; } | ||||
| .icon_efficiency    { --iconChar:"\F625"; } | ||||
| .icon_workplace     { --iconChar:"\F7D9"; } | ||||
| .icon_caratUp       { --iconChar:"\F102"; } | ||||
							
								
								
									
										
											BIN
										
									
								
								web/static/css/fonts/interface-Regular.woff
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								web/static/css/fonts/interface-Regular.woff
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								web/static/css/fonts/interface-Regular.woff2
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								web/static/css/fonts/interface-Regular.woff2
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								web/static/css/fonts/poppins-western-bold.woff
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								web/static/css/fonts/poppins-western-bold.woff
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								web/static/css/fonts/poppins-western-bold.woff2
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								web/static/css/fonts/poppins-western-bold.woff2
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								web/static/css/fonts/poppins-western.woff
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								web/static/css/fonts/poppins-western.woff
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								web/static/css/fonts/poppins-western.woff2
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								web/static/css/fonts/poppins-western.woff2
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										138
									
								
								web/static/css/site.print.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										138
									
								
								web/static/css/site.print.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,138 @@ | ||||
| .action, | ||||
| #backToTop, | ||||
| #mainMenu, | ||||
| .modal, | ||||
| .optional, | ||||
| #socialMedia { | ||||
| 	display:none; | ||||
| } | ||||
|  | ||||
| figure, blockquote { | ||||
| 	padding:0; | ||||
| 	margin:0; | ||||
| } | ||||
|  | ||||
| body, button, input, table, textarea, select { | ||||
| 	font-size:12pt; | ||||
| 	line-height:1.75; | ||||
| 	font-family:flowtext,sans-serif; | ||||
| } | ||||
|  | ||||
| body { | ||||
| 	max-width:7.5in; | ||||
| 	margin:0 auto; | ||||
| } | ||||
|  | ||||
| a { | ||||
| 	color:#000; | ||||
| 	text-decoration:none; | ||||
| } | ||||
|  | ||||
| header { | ||||
| 	margin-bottom:1rem; | ||||
| } | ||||
|  | ||||
| p { | ||||
| 	margin:0 0 1rem; | ||||
| } | ||||
|  | ||||
| li { | ||||
|   margin-bottom:0.5rem; | ||||
| } | ||||
|  | ||||
| h2 { | ||||
| 	font-size:24pt; | ||||
| 	font-weight:normal; | ||||
| 	margin:0 0 0.5rem; | ||||
| } | ||||
|  | ||||
| h1, | ||||
| .hero h2 { | ||||
| 	margin:0; | ||||
| 	line-height:1.2; | ||||
| } | ||||
|  | ||||
| h3 { | ||||
| 	font-size:16pt; | ||||
| 	font-weight:normal; | ||||
| 	margin:0; | ||||
| } | ||||
|  | ||||
| main > section { | ||||
| 	padding-bottom:1.75rem; | ||||
| } | ||||
|  | ||||
| main > section header p { | ||||
|   font-size:14pt; | ||||
| } | ||||
|  | ||||
| .hero header p { | ||||
| 	margin-top:0; | ||||
| } | ||||
|  | ||||
| .hasPlate, | ||||
| .cards { | ||||
| 	display:flex; | ||||
| 	flex-wrap:wrap; | ||||
| 	justify-content:center; | ||||
| 	gap:1rem 2rem; | ||||
| } | ||||
|  | ||||
| .hasPlate > figure { | ||||
| 	flex-grow:0; | ||||
| 	width:min(12rem, 100%); | ||||
| 	text-align:center; | ||||
| } | ||||
|  | ||||
| .hasPlate > figure img { | ||||
| 	width:100%; | ||||
| 	height:auto; | ||||
| 	border-radius:1rem; | ||||
| } | ||||
|  | ||||
| .hasPlate > div { | ||||
| 	width:1%; | ||||
| 	flex-grow:1; | ||||
| } | ||||
|  | ||||
| .cards > * { | ||||
| 	width:calc(50% - 1rem); | ||||
| 	break-inside:avoid; | ||||
| } | ||||
|  | ||||
| .cards > * > *:last-child { | ||||
|   margin-bottom:0; | ||||
|   padding-bottom:0; | ||||
| } | ||||
|  | ||||
| #whatIDo h3[class*="icon_"]:before { | ||||
| 	font-size:36pt; | ||||
| 	margin-right:0.5rem; | ||||
| } | ||||
|  | ||||
| #testimonials figcaption { | ||||
|   font-size:14pt; | ||||
|   margin-bottom:0.5rem; | ||||
| } | ||||
|  | ||||
| #testimonials figcaption img { | ||||
|   display:inline-block; | ||||
|   vertical-align:middle; | ||||
|   width:3rem; | ||||
|   height:3rem; | ||||
|   margin-right:0.25rem; | ||||
|   border-radius:50%; | ||||
| } | ||||
|  | ||||
| #whatIDo ul, | ||||
| #testimonials blockquote { | ||||
|   font-size:10pt; | ||||
| } | ||||
|  | ||||
| .note { | ||||
|   text-align:center; | ||||
| } | ||||
|  | ||||
| #fauxBody > footer { | ||||
|   text-align:center; | ||||
| } | ||||
							
								
								
									
										763
									
								
								web/static/css/site.screen.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										763
									
								
								web/static/css/site.screen.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,763 @@ | ||||
| :root { | ||||
|  | ||||
| 	--flowColor:#024; | ||||
| 	--flowBg:#FFF; | ||||
| 	--flowAltBg:#CDF; | ||||
| 	--flowAltBorder:0.0625rem solid #04F; | ||||
| 	--flowAnchorNormal:#04F; | ||||
| 	--flowAnchorVisited:#000; | ||||
| 	--flowAnchorHover:#069; | ||||
| 	--flowMarker:#080; | ||||
| 	 | ||||
| 	--flyoutColor:#36C; | ||||
| 	--flyoutBgColor:#ACE; | ||||
| 	 | ||||
| 	--chromeColor:#000; | ||||
| 	--chromeBg:var(--flowBg); | ||||
| 	--chromeAnchorNormal:var(--chromeColor); | ||||
| 	--chromeAnchorVisited:var(--chromeColor); | ||||
| 	--chromeAnchorHover:#369; | ||||
| 	 | ||||
| 	--mainHeadingColor:#356; | ||||
| 	 | ||||
| 	--modalOuterBg:#ACEC; | ||||
| 	--modalOuterShadow:inset 0 0 16em #68A; | ||||
| 	--modalInnerBg:#DEF; | ||||
| 	--modalInputBg:#FFF; | ||||
| 	--modalInputBorder:0.125rem solid #048; | ||||
| 	--modalHeadingBg:#ACE; | ||||
| 	--modalButtonColor:#FFF; | ||||
| 	--modalButtonBg:#048; | ||||
| 	--modalShadow:0.5rem 0.5rem 2rem #0247; | ||||
| 	 | ||||
| 	--actionColor:#039; | ||||
| 	--actionBg:#FFF; | ||||
| 	--actionBorder:0.125rem solid var(--actionColor); | ||||
| 	--actionHoverColor:#FFF; | ||||
| 	--actionHoverBg:#05A; | ||||
| 	 | ||||
| 	--heroColor:#FFF; | ||||
| 	--heroBgColor:#05A; | ||||
| 	--heroBg:linear-gradient(20deg, #05A, #012); | ||||
| 	--heroTextShadow: | ||||
| 		0 0 2rem #000, | ||||
| 		0.25rem 0.25rem 1rem #000; | ||||
| 	 | ||||
| 	--heroActionColor:#FFF; | ||||
| 	--heroActionBg:transparent; | ||||
| 	--heroActionHoverColor:#036; | ||||
| 	--heroActionHoverBg:#8CF; | ||||
|  | ||||
| 	--borderColor:#048; | ||||
| 	--standardBorder:0.125rem solid var(--borderColor); | ||||
| 	 | ||||
| 	 | ||||
| 	--horzPad:clamp(0.5rem, 4vw, 2rem); | ||||
| 	--vertPad:clamp(1rem, min(8vw, 10vh), 4rem); | ||||
| 	--standardPad:var(--vertPad) var(--horzPad); | ||||
| 	--chromePad:1rem var(--horzPad); | ||||
| 	--heroPad:clamp(1rem, 3vh, 6rem) var(--horzPad); | ||||
| 	 | ||||
| } | ||||
|  | ||||
| html,body,div,p,h1,h2,h3,h4,h5,h6,figure, | ||||
| ul,ol,li,dl,dt,dd,form,fieldset,caption,legend, | ||||
| table,tr,td,th,address,blockquote,img { | ||||
| 	margin:0; | ||||
| 	padding:0; | ||||
| } | ||||
|  | ||||
| img, fieldset { | ||||
| 	border:none; | ||||
| } | ||||
|  | ||||
| blockquote, q { | ||||
| 	quotes:none; | ||||
| } | ||||
|  | ||||
| body *, *:after, *:before { | ||||
| 	box-sizing:border-box; | ||||
| } | ||||
|  | ||||
| legend { | ||||
| 	-webkit-appearance: none; | ||||
| 	-moz-appearance: none; | ||||
| 	appearance: none; | ||||
| 	display:block; | ||||
| } | ||||
|  | ||||
| button, label, summary { | ||||
| 	cursor:pointer; | ||||
| } | ||||
|  | ||||
| html, body { | ||||
| 	height:100%; | ||||
| } | ||||
|  | ||||
| body, button, input, table, textarea, select { | ||||
| 	font-size:1rem; | ||||
| 	line-height:1.75; | ||||
| 	font-family:flowtext,sans-serif; | ||||
| 	color:var(--flowColor); | ||||
| 	background:var(--flowBg); | ||||
| } | ||||
|  | ||||
| a { | ||||
| 	color:var(--flowAnchorNormal); | ||||
| 	transition:color 0.3s, background 0.3s, scale 0.3s; | ||||
| } | ||||
|  | ||||
| a:visited { | ||||
| 	color:var(--flowAnchorVisited); | ||||
| } | ||||
|  | ||||
| a:active, | ||||
| a:focus, | ||||
| a:hover { | ||||
| 	color:var(--flowAnchorHover); | ||||
| } | ||||
|  | ||||
| input, | ||||
| select, | ||||
| textarea { | ||||
| 	padding:0.25rem 0.5rem; | ||||
| } | ||||
|  | ||||
| #fauxBody,  | ||||
| .modal, | ||||
| .modal > .modalClose { | ||||
| 	position:fixed; | ||||
| 	top:0; | ||||
| 	left:0; | ||||
| 	width:100%; | ||||
| 	height:100%; | ||||
| 	overflow:auto; | ||||
| 	scroll-behavior:smooth; | ||||
| } | ||||
|  | ||||
| .modal, | ||||
| #top, | ||||
| .cards,  | ||||
| .hasPlate, | ||||
| #fauxBody > footer { | ||||
| 	position:relative; | ||||
| 	display:flex; | ||||
| 	flex-wrap:wrap; | ||||
| 	align-items:center; | ||||
| 	justify-content:center; | ||||
| } | ||||
|  | ||||
| .modalClose span, | ||||
| .mainMenuOpen span, | ||||
| #fauxBody > footer h2.optional { | ||||
| 	position:fixed; | ||||
| 	right:999em; | ||||
| 	top:0; | ||||
| 	opacity:0; | ||||
| } | ||||
|  | ||||
| .modal { | ||||
| 	left:-200vw; | ||||
| 	background:var(--modalOuterBg); | ||||
| 	box-shadow:var(--modalOuterShadow); | ||||
| 	opacity:0; | ||||
| 	transition:left 0s 0.5s, opacity 0.5s; | ||||
| } | ||||
|  | ||||
| .modal:target { | ||||
| 	left:0; | ||||
| 	opacity:1; | ||||
| 	transition:opacity 0.5s; | ||||
| } | ||||
|  | ||||
| .modal > div { | ||||
| 	/* | ||||
| 		depth sort over outer .modalClose | ||||
| 	*/ | ||||
| 	position:relative; | ||||
| 	/* | ||||
| 		if you don't auto margin, scrolling at small screen heights is messed. | ||||
| 	*/ | ||||
| 	margin:auto 1rem; | ||||
| 	overflow:hidden; | ||||
| 	background:var(--modalInnerBg); | ||||
| 	color:var(--flowColor); | ||||
| 	box-shadow:var(--modalShadow); | ||||
| 	border:var(--standardBorder); | ||||
| 	border-radius:1rem; | ||||
| 	scale:0; | ||||
| 	transition:scale 0.5s; | ||||
| } | ||||
| 	 | ||||
| .modal:target > div { | ||||
| 	scale:1; | ||||
| } | ||||
|  | ||||
| .modal .modalClose, | ||||
| #mainMenu .modalClose { | ||||
| 	position:absolute; | ||||
| 	text-decoration:none; | ||||
| } | ||||
|  | ||||
| .modal:target .modalClose { | ||||
| 	display:block; | ||||
| } | ||||
|  | ||||
| .modal > * > .modalClose, | ||||
| #mainMenu > * > .modalClose { | ||||
| 	top:0.5rem; | ||||
| 	right:1rem; | ||||
| 	font-size:2rem; | ||||
| 	line-height:1; | ||||
| 	color:var(--flowColor); | ||||
| } | ||||
|  | ||||
| .modal > * > .modalClose:focus, | ||||
| .modal > * > .modalClose:hover { | ||||
| 	scale:1.2; | ||||
| } | ||||
|  | ||||
| .modal h2 { | ||||
| 	font-size:1.25rem; | ||||
| 	padding:0.5rem 4rem 0.5rem 1rem; | ||||
| 	margin-bottom:1rem; | ||||
| 	background:var(--modalHeadingBg); | ||||
| 	border-bottom:var(--standardBorder); | ||||
| } | ||||
|  | ||||
| .modal h2 ~ * { | ||||
| 	margin:1rem; | ||||
| } | ||||
|  | ||||
| #top, | ||||
| #fauxBody > footer { | ||||
| 	max-width:72rem; | ||||
| 	margin:0 auto; | ||||
| 	padding:var(--chromePad); | ||||
| 	background:var(--chromeBgColor); | ||||
| 	color:var(--chromeColor); | ||||
| } | ||||
|  | ||||
| #fauxBody > footer { | ||||
| 	z-index:800; | ||||
| } | ||||
|  | ||||
| #fauxBody > footer p span { | ||||
| 	display:inline-block; | ||||
| } | ||||
|  | ||||
| #top { | ||||
| 	justify-content:space-between; | ||||
| 	gap:2rem; | ||||
| 	z-index:900; | ||||
| } | ||||
|  | ||||
| #top a, | ||||
| #fauxBody > footer a { | ||||
| 	color:var(--chromeAnchorNormal); | ||||
| 	text-decoration:none; | ||||
| } | ||||
|  | ||||
| #top > a:first-child { | ||||
| 	display:block; | ||||
| 	overflow:hidden; | ||||
| 	flex-grow:0; | ||||
| 	font-size:0.875rem; | ||||
| 	line-height:1.1; | ||||
| } | ||||
|  | ||||
| h1 { | ||||
| 	font-size:2rem; | ||||
| 	text-transform:uppercase; | ||||
| 	transform-origin:center left; | ||||
| 	letter-spacing:0.125rem; | ||||
| 	scale:1.15 1; | ||||
| 	color:var(--mainHeadingColor); | ||||
| } | ||||
|  | ||||
| #mainMenu { | ||||
| 	flex-grow:1; | ||||
| 	text-align:right; | ||||
| } | ||||
|  | ||||
| #mainMenu li { | ||||
| 	list-style:none; | ||||
| 	display:inline; | ||||
| 	margin-right:1.5rem; | ||||
| } | ||||
|  | ||||
| #mainMenu li a { | ||||
| 	display:inline-block; | ||||
| 	font-size:1.125rem; | ||||
| } | ||||
|  | ||||
| #mainMenu li a:focus, | ||||
| #mainMenu li a:hover, | ||||
| .socialMenu a:focus, | ||||
| .socialMenu a:hover, | ||||
| .mainMenuOpen:focus, | ||||
| .mainMenuOpen:hover { | ||||
| 	color:var(--chromeAnchorHover); | ||||
| 	scale:1.2; | ||||
| } | ||||
|  | ||||
| #fauxBody > footer > section:first-child { | ||||
| 	width:1%; | ||||
| 	flex-grow:1; | ||||
| } | ||||
|  | ||||
| .socialMenu { | ||||
| 	list-style:none; | ||||
| 	display:flex; | ||||
| 	gap:1.5rem; | ||||
| } | ||||
|  | ||||
| .socialMenu a { | ||||
| 	position:relative; | ||||
| 	display:block; | ||||
| } | ||||
|  | ||||
| .socialMenu a:before { | ||||
| 	font-size:2.5rem; | ||||
| 	transition:color 0.3s, scale 0.3s; | ||||
| } | ||||
|  | ||||
| .socialMenu a:focus:before, | ||||
| .socialMenu a:hover:before { | ||||
| 	scale:1.375; | ||||
| } | ||||
|  | ||||
| .socialMenu a span, | ||||
| #backToTop span { | ||||
| 	position:absolute; | ||||
| 	font-size:0.875rem; | ||||
| 	color:var(--flyoutColor); | ||||
| 	background:var(--flyoutBgColor); | ||||
| 	border:0.125rem solid var(--flyoutColor); | ||||
| } | ||||
|  | ||||
| .socialMenu a span { | ||||
| 	bottom:3.5rem; | ||||
| 	right:999em; | ||||
| 	padding:0.25rem 0.75rem; | ||||
| 	white-space:nowrap; | ||||
| 	border-radius:1rem; | ||||
| 	transition:right 0s 0.3s, opacity 0.3s; | ||||
| 	opacity:0; | ||||
| } | ||||
|  | ||||
| .socialMenu a span:after { | ||||
| 	content:""; | ||||
| 	position:absolute; | ||||
| 	bottom:-0.5625rem; | ||||
| 	right:1rem; | ||||
| 	width:1rem; | ||||
| 	height:1rem; | ||||
| 	background:var(--flyoutBgColor); | ||||
| 	border:solid var(--flyoutColor); | ||||
| 	border-width:0 0.125rem 0.125rem 0; | ||||
| 	rotate:45deg; | ||||
| } | ||||
|  | ||||
| .socialMenu a:focus span, | ||||
| .socialMenu a:hover span { | ||||
| 	right:-0.5rem; | ||||
| 	opacity:1; | ||||
| 	transition:opacity 0.3s; | ||||
| } | ||||
|  | ||||
| main > section:not(.hero), | ||||
| .hero > div, | ||||
| .hasCards > header, | ||||
| .hasPlate { | ||||
| 	max-width:56rem; | ||||
| 	margin:0 auto; | ||||
| } | ||||
|  | ||||
| main > section:not(.hero), | ||||
| .hero > div, | ||||
| .hasPlate { | ||||
| 	padding:var(--standardPad); | ||||
| } | ||||
|  | ||||
| .hasPlate { | ||||
| 	padding-top:0; | ||||
| } | ||||
|  | ||||
| main > section:not(.hero) { | ||||
| 	border-bottom:var(--flowAltBorder); | ||||
| } | ||||
|  | ||||
| main > section > header:not(.hasPlate) { | ||||
| 	text-align:center; | ||||
| } | ||||
|  | ||||
| main > section:not(.hero):nth-child(odd) { | ||||
| 	background:var(--flowAltBg); | ||||
| } | ||||
|  | ||||
| main h2 { | ||||
| 	font-size:clamp(1.75rem, 5vw, 2.5rem); | ||||
| 	font-weight:normal; | ||||
| } | ||||
|  | ||||
| main h2 span { | ||||
| 	display:inline-block; /* soft keep-together */ | ||||
| } | ||||
|  | ||||
| main h3 { | ||||
| 	font-size:clamp(1.25rem, 3vw, 2rem); | ||||
| 	font-weight:normal; | ||||
| } | ||||
|  | ||||
| main p { | ||||
| 	max-width:40em; | ||||
| 	margin:1em 0; | ||||
| } | ||||
|  | ||||
| main section > header p { | ||||
| 	font-size:1.125rem; | ||||
| } | ||||
|  | ||||
| main > section > header:not(.hasPlate) p { | ||||
| 	margin:1em auto; | ||||
| } | ||||
|  | ||||
| main > section.hasCards { | ||||
| 	max-width:100%; | ||||
| } | ||||
|  | ||||
| .cards,  | ||||
| .hasPlate { | ||||
| 	gap:2rem; | ||||
| } | ||||
|  | ||||
| .cards { | ||||
| 	margin:2rem auto 0; | ||||
| 	align-items:stretch; | ||||
| 	gap:2rem 4rem; | ||||
| } | ||||
|  | ||||
| #whatIDo .cards { | ||||
| 	max-width:68em; | ||||
| } | ||||
|  | ||||
| .cards > * { | ||||
| 	flex-grow:1; | ||||
| 	width:1%; | ||||
| 	min-width:min(20rem, 100%); | ||||
| 	max-width:24rem; | ||||
| } | ||||
|  | ||||
| #whatIDo .cards > * { | ||||
| 	min-width:min(24rem, 100%); | ||||
| 	max-width:40rem; | ||||
| } | ||||
|  | ||||
| .cards h3[class*="icon_"]:before { | ||||
| 	font-size:4rem; | ||||
| 	margin-right:1rem; | ||||
| } | ||||
|  | ||||
| .cards li { | ||||
| 	list-style:none; | ||||
| 	position:Relative; | ||||
| 	margin-bottom:0.5rem; | ||||
| 	padding-left:2rem; | ||||
| } | ||||
|  | ||||
| .cards li:before { | ||||
| 	content:"\F058"; | ||||
| 	position:absolute; | ||||
| 	top:0; | ||||
| 	left:0.5rem; | ||||
| 	color:var(--flowMarker); | ||||
| 	font-family:interface; | ||||
| } | ||||
|  | ||||
| .hasPlate > div { | ||||
| 	flex-grow:1; | ||||
| 	width:1%; | ||||
| 	min-width:min(20rem, 100%); | ||||
| } | ||||
|  | ||||
| .hasPlate.trailing > figure, | ||||
| .hasPlate.trailing > picture { | ||||
| 	position:relative; | ||||
| 	flex-grow:0; | ||||
| 	width:30%; | ||||
| 	max-width:16rem; | ||||
| 	text-align:center; | ||||
| } | ||||
|  | ||||
| figure img, | ||||
| figure picture { | ||||
| 	width:100%; | ||||
| 	height:auto; | ||||
| 	border-radius:1rem; | ||||
| } | ||||
|  | ||||
| .note { | ||||
| 	text-align:center; | ||||
| } | ||||
|  | ||||
| .action { | ||||
| 	display:inline-block; | ||||
| 	position:relative; | ||||
| 	overflow:hidden; | ||||
| 	margin-top:1rem; | ||||
| 	z-index:1; | ||||
| 	text-decoration:none; | ||||
| 	padding:0.75rem 1.5rem; | ||||
| 	color:var(--actionColor); | ||||
| 	background:var(--actionBg); | ||||
| 	border:var(--actionBorder); | ||||
| 	border-radius:0.5em; | ||||
| 	transition:color 0.3s, text-shadow 0.3s; | ||||
| } | ||||
|  | ||||
| .action:focus, | ||||
| .action:hover { | ||||
| 	color:var(--actionHoverColor); | ||||
| 	text-shadow:none; | ||||
| } | ||||
|  | ||||
| .action:after { | ||||
| 	content:""; | ||||
| 	position:absolute; | ||||
| 	z-index:-1; | ||||
| 	top:50%; | ||||
| 	left:50%; | ||||
| 	width:100%; | ||||
| 	height:100%; | ||||
| 	border-radius:0.5rem; | ||||
| 	translate:-50% -50%; | ||||
| 	scale:0; | ||||
| 	opacity:0; | ||||
| 	background:var(--actionHoverBg); | ||||
| 	transition:scale 0.3s, opacity 0.3s; | ||||
| } | ||||
|  | ||||
| .action:focus:after, | ||||
| .action:hover:after { | ||||
| 	scale:1.1; | ||||
| 	opacity:1; | ||||
| } | ||||
|  | ||||
| .hero { | ||||
| 	padding:var(--heroPad); | ||||
| 	background-color:var(--heroBgColor); | ||||
| 	color:var(--heroColor); | ||||
| 	text-shadow:var(--heroTextShadow); | ||||
| } | ||||
|  | ||||
| .hero .action { | ||||
| 	color:var(--heroActionColor); | ||||
| 	background:var(--heroActionBg); | ||||
| 	border-color:var(--heroActionColor); | ||||
| } | ||||
|  | ||||
| .hero .action:focus, | ||||
| .hero .action:hover { | ||||
| 	color:var(--heroActionHoverColor); | ||||
| } | ||||
|  | ||||
| .hero .action:after { | ||||
| 	background:var(--heroActionHoverBg); | ||||
| } | ||||
|  | ||||
| .hero header { | ||||
| 	margin-bottom:1.5rem; | ||||
| 	line-height:1.2; | ||||
| } | ||||
|  | ||||
| .hero header p { | ||||
| 	margin:0.25rem 0 1.5rem; | ||||
| 	font-size:1.5rem; | ||||
| } | ||||
|  | ||||
| #semanticEasy { | ||||
| 	background-image:url(images/heroBg.avif), var(--heroBg); | ||||
| 	background-position:center; | ||||
| 	background-size:cover; | ||||
| } | ||||
|  | ||||
| section:not(.hero) h2, | ||||
| section:not(.hero) h3 { | ||||
| 	color:var(--mainHeadingColor); | ||||
| } | ||||
|  | ||||
| #testimonials > header { | ||||
| 	margin-bottom:var(--vertPad); | ||||
| } | ||||
|  | ||||
| #testimonials > .cards { | ||||
| 	max-width:76rem; | ||||
| 	margin:0 auto; | ||||
| } | ||||
|  | ||||
| #testimonials figcaption { | ||||
| 	font-size:1.25rem; | ||||
| 	font-style:italic; | ||||
| } | ||||
|  | ||||
| #testimonials figcaption img { | ||||
| 	display:inline-block; | ||||
| 	vertical-align:middle; | ||||
| 	width:4rem; | ||||
| 	height:4rem; | ||||
| 	margin-right:0.5rem; | ||||
| 	border-radius:50%; | ||||
| } | ||||
|  | ||||
| #backToTop { | ||||
| 	display:block; | ||||
| 	position:sticky; | ||||
| 	z-index:600; | ||||
| 	bottom:0; | ||||
| 	width:2.5rem; | ||||
| 	height:3.5rem; | ||||
| 	padding-bottom:1rem; | ||||
| 	margin:-3.5rem 1rem 0 auto; | ||||
| 	opacity:0.6; | ||||
| 	transition:opacity 0.3s; | ||||
| } | ||||
|  | ||||
| #backToTop.hide { | ||||
| 	opacity:0; | ||||
| } | ||||
|  | ||||
| #backToTop:not(.hide):focus, | ||||
| #backToTop:not(.hide):hover { | ||||
| 	opacity:1; | ||||
| } | ||||
|  | ||||
| #backToTop span { | ||||
| 	right:1.5rem; | ||||
| 	top:0.25rem; | ||||
| 	padding:0.125rem 2rem 0.125rem 1rem; | ||||
| 	white-space:nowrap; | ||||
| 	border-radius:1.5rem 0 0 1.5rem; | ||||
| 	transform-origin:center right; | ||||
| 	transition:scale 0.3s; | ||||
| 	scale:0 1; | ||||
| } | ||||
|  | ||||
| #backToTop:focus span, | ||||
| #backToTop:hover span { | ||||
| 	scale:1; | ||||
| } | ||||
|  | ||||
| #backToTop:after { | ||||
| 	content:"\F102"; | ||||
| 	position:relative; | ||||
| 	display:block; | ||||
| 	width:2.5rem; | ||||
| 	height:2.5rem; | ||||
| 	text-align:center; | ||||
| 	font-family:interface; | ||||
| 	font-size:1.5rem; | ||||
| 	line-height:2rem; | ||||
| 	border:0.25rem solid var(--flyoutColor); | ||||
| 	border-radius:50%; | ||||
| 	background:var(--heroBgColor); | ||||
| 	color:var(--heroColor); | ||||
| } | ||||
|  | ||||
| @media (max-width:56rem) { | ||||
|  | ||||
| 	/* | ||||
| 		It sucks to have to repeat all this, | ||||
| 		one of the few cases where native mixins | ||||
| 		would be a godsend. | ||||
| 	*/ | ||||
|  | ||||
| 	#mainMenu, | ||||
| 	#mainMenu > .modalClose { | ||||
| 		top:0; | ||||
| 		left:0; | ||||
| 		width:100%; | ||||
| 		height:100%; | ||||
| 		overflow:auto; | ||||
| 		scroll-behavior:smooth; | ||||
| 	} | ||||
|  | ||||
| 	#mainMenu { | ||||
| 		position:fixed; | ||||
| 		display:flex; | ||||
| 		flex-wrap:wrap; | ||||
| 		align-items:center; | ||||
| 		justify-content:center; | ||||
| 		left:-200vw; | ||||
| 		background:var(--modalOuterBg); | ||||
| 		box-shadow:var(--modalOuterShadow); | ||||
| 		opacity:0; | ||||
| 		transition:left 0s 0.5s, opacity 0.5s; | ||||
| 	} | ||||
|  | ||||
| 	#mainMenu:target { | ||||
| 		left:0; | ||||
| 		opacity:1; | ||||
| 		transition:opacity 0.5s; | ||||
| 	} | ||||
|  | ||||
| 	#mainMenu > div { | ||||
| 		position:relative; | ||||
| 		margin:auto 1rem; | ||||
| 		overflow:hidden; | ||||
| 		background:var(--modalInnerBg); | ||||
| 		color:var(--flowColor); | ||||
| 		box-shadow:var(--modalShadow); | ||||
| 		border:var(--standardBorder); | ||||
| 		border-radius:1rem; | ||||
| 		scale:0; | ||||
| 		transition:scale 0.5s; | ||||
| 	} | ||||
| 		 | ||||
| 	#mainMenu:target > div { | ||||
| 		scale:1; | ||||
| 	} | ||||
|  | ||||
| 	#mainMenu:target .modalClose { | ||||
| 		display:block; | ||||
| 	} | ||||
|  | ||||
| 	#mainMenu > * > .modalClose:focus, | ||||
| 	#mainMenu > * > .modalClose:hover { | ||||
| 		scale:1.2; | ||||
| 	} | ||||
|  | ||||
| 	#mainMenu > div:before { | ||||
| 		content:attr(data-modalTitle); | ||||
| 		display:block; | ||||
| 		font-size:1.25rem; | ||||
| 		padding:0.5rem 4rem 0.5rem 1rem; | ||||
| 		margin-bottom:1rem; | ||||
| 		background:var(--modalHeadingBg); | ||||
| 		border-bottom:var(--standardBorder); | ||||
| 	} | ||||
|  | ||||
| 	#mainMenu ul { | ||||
| 		margin:1rem; | ||||
| 	} | ||||
| 	 | ||||
| 	#mainMenu li { | ||||
| 		display:block; | ||||
| 		text-align:center; | ||||
| 		margin:0.5rem 0; | ||||
| 	} | ||||
| 	 | ||||
| 	.mainMenuOpen { | ||||
| 		display:block; | ||||
| 		width:2.5rem; | ||||
| 		height:2rem; | ||||
| 		background:linear-gradient( | ||||
| 			#000 20%, | ||||
| 			transparent 20% 40%, | ||||
| 			#000 40% 60%, | ||||
| 			transparent 60% 80%, | ||||
| 			#000 80% | ||||
| 		); | ||||
| 	} | ||||
| 	 | ||||
| } /* max-width:56rem */ | ||||
							
								
								
									
										
											BIN
										
									
								
								web/static/favicon.ico
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								web/static/favicon.ico
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 37 KiB | 
							
								
								
									
										1
									
								
								web/static/favicon.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								web/static/favicon.svg
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| <svg width="16em" height="16em" viewBox="0 0 203.2 203.2" xmlns="http://www.w3.org/2000/svg"><g transform="matrix(.20387 .2137 -.20387 .2137 128.46 -10.68)"><path d="m271.76 703.73-211.3-314.83 276.27-58.724-64.977 373.55" fill="#d5b360"/><path d="m271.76 703.73-211.3-314.83 276.27-58.724z" fill-opacity="0" stroke="#3e3632" stroke-linecap="round" stroke-linejoin="round" stroke-width="11"/><path d="m215.67 405.24 65.519 63.54m-76.56 6.867 63.54-65.519m-45.433 88.23 63.54-65.519m-84.739 15.245 65.519 63.541" fill-opacity="0" stroke="#3e3632" stroke-linecap="round" stroke-linejoin="round" stroke-width="8"/><path d="m328.56 236.75c3.897-10.97 5.02-23.007 2.554-34.554-4.783-22.409-21.817-36.657-41.676-37.89 1.346-7.609 1.314-15.557-0.369-23.415-5.828-27.213-28.979-44.343-53.839-40.513-6.532-24.271-27.356-39.494-49.689-38.994-3.032-7.637-17.489-39.799-49.025-49.658-22.371-6.975-82.684 7.057-82.687 7.05-3e-3 -0.012 25.332 8.591 40.719 33.116 4.867 7.753 7.006 17.012 7.502 26.468-1.321 0.266-2.643 0.532-3.964 0.797-25.644 5.213-42.087 30.802-37.683 58.993-24.66 6.33-39.817 31.39-34.022 58.86 1.657 7.863 4.834 15.002 9.158 21.406-17.641 9.202-27.421 29.078-22.676 51.496 2.444 11.551 8.358 22.059 16.38 30.495-14.956 8.922-22.819 26.303-18.529 46.499 5.914 27.873 37.743 48.009 60.763 52.573 22.953 4.707 75.488 4.761 133.59-7.818 58.266-11.895 111.3-34.354 125.22-47.197 14.132-12.498 40.091-44.813 34.124-72.753-4.295-20.195-18.551-32.893-35.844-34.961" fill="#fffffe"/><path d="m328.56 236.75c3.897-10.97 5.02-23.007 2.554-34.554-4.783-22.409-21.817-36.657-41.676-37.89 1.346-7.609 1.314-15.557-0.369-23.415-5.828-27.213-28.979-44.343-53.839-40.513-6.532-24.271-27.356-39.494-49.689-38.994-3.032-7.637-17.489-39.799-49.025-49.658-22.371-6.975-82.684 7.057-82.687 7.05-3e-3 -0.012 25.332 8.591 40.719 33.116 4.867 7.753 7.006 17.012 7.502 26.468-1.321 0.266-2.643 0.532-3.964 0.797-25.644 5.213-42.087 30.802-37.683 58.993-24.66 6.33-39.817 31.39-34.022 58.86 1.657 7.863 4.834 15.002 9.158 21.406-17.641 9.202-27.421 29.078-22.676 51.496 2.444 11.551 8.358 22.059 16.38 30.495-14.956 8.922-22.819 26.303-18.529 46.499 5.914 27.873 37.743 48.009 60.763 52.573 22.953 4.707 75.488 4.761 133.59-7.818 58.266-11.895 111.3-34.354 125.22-47.197 14.132-12.498 40.091-44.813 34.124-72.753-4.295-20.195-18.551-32.893-35.844-34.961z" fill-opacity="0" stroke="#3e3632" stroke-linecap="round" stroke-linejoin="round" stroke-width="11"/><path d="m184.49 72.517c-0.108-0.604-13.1 42.635-107.01 66.574m149.56-23.297c-0.017 0.146-5.601 15.147-29.231 36.635-23.687 21.079-65.42 48.646-139.02 64.457m222.03-37.656c-0.109 0.174-8.437 23.849-40.838 53.951-32.5 29.173-89.075 64.773-187.98 67.361m265.38-48.424c-0.048-0.057-11.438 19.506-34.151 44.665-22.74 24.947-56.804 55.49-102.83 72.474m-84.823-340.9c0 3e-3 26.33 1.97 34.616 40.955" fill-opacity="0" stroke="#3e3632" stroke-linecap="round" stroke-linejoin="round" stroke-width="9"/></g></svg> | ||||
| After Width: | Height: | Size: 2.9 KiB | 
		Reference in New Issue
	
	Block a user