diff --git a/Dockerfile b/Dockerfile
index b2e91bc..04dac9e 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -17,9 +17,6 @@ RUN pnpm run build:css && pnpm run build:js
FROM golang:1.26-alpine AS go-builder
WORKDIR /app
-# Install git for potential private modules (though not strictly needed here)
-RUN apk add --no-cache git
-
# Download Go modules
COPY go.mod go.sum ./
RUN go mod download
@@ -41,9 +38,6 @@ RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /app/server ./cmd/schr
FROM alpine:latest
WORKDIR /app
-# Install root certificates and timezone data
-RUN apk --no-cache add ca-certificates tzdata
-
# Copy the compiled binary from the builder stage
COPY --from=go-builder /app/server .
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..cac2dd7
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,7 @@
+.PHONY: run
+run: docker
+ air
+
+.PHONY: docker
+docker:
+ docker compose up -d
diff --git a/cmd/schreifuchs-ch/main.go b/cmd/schreifuchs-ch/main.go
index 2e0724c..f569ab8 100644
--- a/cmd/schreifuchs-ch/main.go
+++ b/cmd/schreifuchs-ch/main.go
@@ -30,11 +30,7 @@ func main() {
slog.Error("could not read configuaration", "err", err)
}
- slog.SetDefault(
- slog.New(
- slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: cfg.LogLevel}),
- ),
- )
+ setupLogger(&cfg)
err = server.Start(ctx, cfg)
if err != nil {
@@ -42,3 +38,22 @@ func main() {
return
}
}
+
+func setupLogger(cfg *config.Cfg) {
+ opts := slog.HandlerOptions{
+ Level: cfg.LogLevel,
+ }
+ var handler slog.Handler
+
+ if cfg.LogJSON {
+ handler = slog.NewJSONHandler(os.Stdout, &opts)
+ } else {
+ handler = slog.NewTextHandler(os.Stdout, &opts)
+ }
+
+ slog.SetDefault(
+ slog.New(
+ handler,
+ ),
+ )
+}
diff --git a/docker-compose.yml b/docker-compose.yml
index e78c91a..ab36b3e 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,7 +1,6 @@
services:
valkey:
image: valkey/valkey:8
- restart: always
ports:
- "6379:6379"
command: valkey-server --requirepass "valkey"
diff --git a/go.mod b/go.mod
index 8effb4f..252bfa3 100644
--- a/go.mod
+++ b/go.mod
@@ -18,6 +18,7 @@ require (
)
require (
+ github.com/InfinityTools/go-binpack2d v1.0.0 // indirect
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e // indirect
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
diff --git a/go.sum b/go.sum
index ad4cfde..1472531 100644
--- a/go.sum
+++ b/go.sum
@@ -1,3 +1,5 @@
+github.com/InfinityTools/go-binpack2d v1.0.0 h1:l15wC3zDlR0FoffIcPze0qbszshmGCpMncDiAxpWnE8=
+github.com/InfinityTools/go-binpack2d v1.0.0/go.mod h1:3hwEmVO6ffChGwW55BVr9k4rzSYGow0OH2B8bjEdO6o=
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e h1:HjVbSQHy+dnlS6C3XajZ69NYAb5jbGNfHanvm1+iYlo=
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e/go.mod h1:3mnrkvGpurZ4ZrTDbYU84xhwXW2TjTKShSwjRi2ihfQ=
github.com/a-h/templ v0.3.977 h1:kiKAPXTZE2Iaf8JbtM21r54A8bCNsncrfnokZZSrSDg=
diff --git a/internal/components/gallery/controller.go b/internal/components/gallery/controller.go
new file mode 100644
index 0000000..90adfe8
--- /dev/null
+++ b/internal/components/gallery/controller.go
@@ -0,0 +1,86 @@
+package gallery
+
+import (
+ "context"
+ "math"
+
+ "github.com/InfinityTools/go-binpack2d"
+)
+
+type Gallery struct {
+ GridWith int
+ GridHeight int
+ Images []Image
+}
+type Set struct {
+ SM Gallery
+ LG Gallery
+ XLG Gallery
+}
+
+func (s *Service) GetGallerySet(ctx context.Context) (gallerySet Set, err error) {
+ gallerySet.SM, err = s.GetGallery(ctx, 50)
+ if err != nil {
+ return
+ }
+ gallerySet.LG, err = s.GetGallery(ctx, 80)
+ if err != nil {
+ return
+ }
+
+ gallerySet.XLG, err = s.GetGallery(ctx, 120)
+ if err != nil {
+ return
+ }
+
+ return
+}
+
+func (s *Service) GetGallery(ctx context.Context, width int) (gallery Gallery, err error) {
+ imgs, err := s.GetImages(ctx)
+ if err != nil {
+ return
+ }
+
+ packer := binpack2d.Create(width, 999999)
+
+ for len(imgs) > 0 {
+ unused := make([]Image, 0, len(imgs))
+ for _, img := range imgs {
+ adjustImageSize(&img, 200)
+ rect, ok := packer.Insert(img.Width, img.Height, binpack2d.RULE_BEST_SHORT_SIDE_FIT)
+ if !ok {
+ unused = append(unused, img)
+ continue
+ }
+
+ img.X = rect.X
+ img.Y = rect.Y
+
+ gallery.Images = append(gallery.Images, img)
+ }
+ imgs = unused
+ }
+
+ packer.ShrinkBin(false)
+
+ gallery.GridHeight = packer.GetHeight()
+ gallery.GridWith = packer.GetWidth()
+
+ return
+}
+
+// adjustImageSize adjusts the image size so that the aspect ratio stays the same
+// but the image area is the given parameter.
+func adjustImageSize(img *Image, area int) {
+ // Calculate the original aspect ratio (W / H)
+ aspectRatio := float64(img.Width) / float64(img.Height)
+
+ // Calculate new height and width using the derived formulas
+ newHeight := math.Sqrt(float64(area) / aspectRatio)
+ newWidth := newHeight * aspectRatio
+
+ // Round to the nearest integer to minimize precision loss
+ img.Width = int(math.Round(newWidth))
+ img.Height = int(math.Round(newHeight))
+}
diff --git a/internal/components/gallery/dir.go b/internal/components/gallery/dir.go
new file mode 100644
index 0000000..0207d70
--- /dev/null
+++ b/internal/components/gallery/dir.go
@@ -0,0 +1,40 @@
+package gallery
+
+import (
+ "os"
+ "path"
+)
+
+type fileWithPath struct {
+ os.FileInfo
+ path string
+}
+
+func (f fileWithPath) Name() string {
+ return path.Join(f.path, f.FileInfo.Name())
+}
+
+func (s *Service) readDirRecursive(path string) (files []os.FileInfo, err error) {
+ return s.readDirRecursiveTo([]os.FileInfo{}, path)
+}
+
+func (s *Service) readDirRecursiveTo(files []os.FileInfo, p ...string) ([]os.FileInfo, error) {
+ localFiles, err := s.fs.ReadDir(path.Join(p...))
+ if err != nil {
+ return files, err
+ }
+
+ for _, file := range localFiles {
+ if file.IsDir() {
+ newP := append(p, file.Name())
+ files, err = s.readDirRecursiveTo(files, newP...)
+ if err != nil {
+ return files, err
+ }
+ } else {
+ files = append(files, fileWithPath{file, path.Join(p[1:]...)})
+ }
+ }
+
+ return files, nil
+}
diff --git a/internal/components/gallery/image.go b/internal/components/gallery/image.go
new file mode 100644
index 0000000..5a48548
--- /dev/null
+++ b/internal/components/gallery/image.go
@@ -0,0 +1,113 @@
+package gallery
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "hash/fnv"
+ "log/slog"
+ "os"
+ "path"
+ "strings"
+ "time"
+
+ "github.com/valkey-io/valkey-go"
+)
+
+type Image struct {
+ UID string
+ X int
+ Y int
+ Width int
+ Height int
+}
+
+const (
+ imagesKey = "gallery:images:list"
+ hashKey = "gallery:images:hash"
+)
+
+func (s *Service) autoRefresh() {
+ t := time.NewTicker(time.Minute)
+ for {
+ ctx, cancle := context.WithTimeout(context.Background(), time.Minute)
+
+ if err := s.assembleImageList(ctx); err != nil {
+ slog.Error("error while refreshing gallery", "err", err)
+ }
+ cancle()
+ <-t.C
+ }
+}
+
+func (s *Service) assembleImageList(ctx context.Context) error {
+ files, err := s.readDirRecursive(s.cfg.Path)
+ if err != nil {
+ return fmt.Errorf("could not read contents of %s: %w", s.cfg.Path, err)
+ }
+ slog.Debug("loaded files", "files", files)
+
+ savedHash, _ := s.cache.Do(ctx, s.cache.B().Get().Key(hashKey).Build()).AsBytes()
+
+ newHash := hashImageList(files)
+
+ if bytes.Equal(savedHash, newHash) {
+ slog.Debug("images up to date no refreshing of gallery", "savedHash", savedHash, "newHash", newHash)
+ return nil
+ }
+
+ slog.Info("starting gallery refresh")
+ defer slog.Info("gallery refresh completed")
+
+ imgs := make([]Image, 0, len(files))
+ for _, file := range files {
+ if strings.HasSuffix(file.Name(), ".md") {
+ continue
+ }
+
+ imagePath := path.Join(s.cfg.Path, file.Name())
+ img, err := s.img.GetImage(imagePath)
+ if err != nil || img.Image == nil {
+ slog.Warn("could not get image for gallery", "err", err, "img", img)
+ continue
+ }
+
+ uid, err := img.UID()
+ if err != nil {
+ continue
+ }
+
+ imgs = append(imgs, Image{
+ UID: uid,
+ Width: img.Bounds().Dx(),
+ Height: img.Bounds().Dy(),
+ })
+ }
+
+ if err = s.cache.Do(ctx, s.cache.B().Set().Key(hashKey).Value(valkey.BinaryString(newHash)).Build()).Error(); err != nil {
+ return err
+ }
+
+ if err = s.cache.Do(ctx, s.cache.B().Set().Key(imagesKey).Value(valkey.JSON(imgs)).Build()).Error(); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func hashImageList(files []os.FileInfo) []byte {
+ h := fnv.New128()
+
+ for _, file := range files {
+ fmt.Fprint(h, file.ModTime())
+ fmt.Fprint(h, file.Size())
+ fmt.Fprint(h, file.Name())
+ }
+
+ return h.Sum([]byte{})
+}
+
+func (s *Service) GetImages(ctx context.Context) (images []Image, err error) {
+ err = s.cache.Do(ctx, s.cache.B().Get().Key(imagesKey).Build()).DecodeJSON(&images)
+ return
+}
diff --git a/internal/components/gallery/resource.go b/internal/components/gallery/resource.go
new file mode 100644
index 0000000..e90f18b
--- /dev/null
+++ b/internal/components/gallery/resource.go
@@ -0,0 +1,35 @@
+package gallery
+
+import (
+ "time"
+
+ "git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/components/images"
+ "git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/pkg/config"
+ "git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/pkg/filesystem"
+ "github.com/valkey-io/valkey-go"
+)
+
+type getImager interface {
+ GetImage(path string) (img images.Image, err error)
+}
+
+type Service struct {
+ fs filesystem.FS
+ cfg *config.Gallery
+ img getImager
+ cache valkey.Client
+
+ refreshIntervall time.Duration
+}
+
+func New(fs filesystem.FS, cfg *config.Gallery, img getImager, cache valkey.Client) *Service {
+ s := &Service{
+ fs: fs,
+ cfg: cfg,
+ img: img,
+ cache: cache,
+ }
+ go s.autoRefresh()
+
+ return s
+}
diff --git a/internal/components/images/controller.go b/internal/components/images/controller.go
index 70c68ce..5679f37 100644
--- a/internal/components/images/controller.go
+++ b/internal/components/images/controller.go
@@ -21,7 +21,7 @@ import (
var ErrNotAnImage = errors.New("not an image")
-func (s *Service) getMime(path string) (mimeType string, err error) {
+func (s *Service) GetMime(path string) (mimeType string, err error) {
info, err := s.fs.Stat(path)
if err != nil {
err = fmt.Errorf("image file info could not be fetched: %w", err)
@@ -35,13 +35,12 @@ func (s *Service) getMime(path string) (mimeType string, err error) {
return
}
-func (s *Service) getImage(uid string) (file []byte, mimeType string, err error) {
- path, err := PathFromUID(uid)
+func (s *Service) getImage(path string) (file []byte, mimeType string, err error) {
if err != nil {
err = fmt.Errorf("could not get path: %w", err)
return
}
- mimeType, err = s.getMime(path)
+ mimeType, err = s.GetMime(path)
if err != nil {
return
}
@@ -59,7 +58,19 @@ func (s *Service) getImage(uid string) (file []byte, mimeType string, err error)
return
}
-func (s *Service) GetImage(ctx context.Context, uid string, options Options) (img io.Reader, mimeType string, err error) {
+func (s *Service) GetImage(path string) (img Image, err error) {
+ rawImage, _, err := s.getImage(path)
+ if err != nil {
+ return
+ }
+
+ img.Path = path
+ img.Image, _, err = image.Decode(bytes.NewBuffer(rawImage))
+
+ return
+}
+
+func (s *Service) GetScaledImage(ctx context.Context, uid string, options Options) (img io.Reader, mimeType string, err error) {
if err = s.seph.Acquire(ctx, 1); err != nil {
err = fmt.Errorf("could not Acquire semaphore: %w", err)
return
@@ -67,7 +78,12 @@ func (s *Service) GetImage(ctx context.Context, uid string, options Options) (im
}
defer s.seph.Release(1)
- rawImage, mimeType, err := s.getImage(uid)
+ path, err := PathFromUID(uid)
+ if err != nil {
+ return
+ }
+
+ rawImage, mimeType, err := s.getImage(path)
if err != nil {
return
}
diff --git a/internal/components/images/controller_test.go b/internal/components/images/controller_test.go
index 43d8795..5504247 100644
--- a/internal/components/images/controller_test.go
+++ b/internal/components/images/controller_test.go
@@ -15,7 +15,7 @@ import (
func Setup(ctx context.Context, ctrl *gomock.Controller) (*images.Service, *mock.Client) {
valkey := mock.NewClient(ctrl)
- return images.New(testdata.FS(), &config.ImageConfig{Concurency: 1, Quality: 80}, valkey), valkey
+ return images.New(testdata.FS(), &config.Image{Concurency: 1, Quality: 80}, valkey), valkey
}
func BenchmarkService_GetImage(b *testing.B) {
@@ -29,7 +29,7 @@ func BenchmarkService_GetImage(b *testing.B) {
for b.Loop() {
valkeyClient.EXPECT().Do(gomock.Any(), gomock.Any()).Return(mock.ErrorResult(errors.New("adf")))
valkeyClient.EXPECT().Do(gomock.Any(), gomock.Any()).Return(mock.Result(valkey.ValkeyMessage{}))
- _, _, err = srv.GetImage(b.Context(), uid, images.Options{Width: 500, Quality: 80})
+ _, _, err = srv.GetScaledImage(b.Context(), uid, images.Options{Width: 500, Quality: 80})
if err != nil {
b.Error(err)
}
diff --git a/internal/components/images/image.go b/internal/components/images/image.go
new file mode 100644
index 0000000..72bf3f5
--- /dev/null
+++ b/internal/components/images/image.go
@@ -0,0 +1,12 @@
+package images
+
+import "image"
+
+type Image struct {
+ image.Image
+ Path string `json:"path"`
+}
+
+func (i Image) UID() (string, error) {
+ return UIDFromPath(i.Path)
+}
diff --git a/internal/components/images/resorce.go b/internal/components/images/resorce.go
index 3d4b860..fbb25f5 100644
--- a/internal/components/images/resorce.go
+++ b/internal/components/images/resorce.go
@@ -18,10 +18,10 @@ type Service struct {
fs fileSystem
cache valkey.Client
seph *semaphore.Weighted
- cfg *config.ImageConfig
+ cfg *config.Image
}
-func New(fs fileSystem, cfg *config.ImageConfig, cache valkey.Client) *Service {
+func New(fs fileSystem, cfg *config.Image, cache valkey.Client) *Service {
return &Service{
fs: fs,
cache: cache,
diff --git a/internal/components/page/page.go b/internal/components/page/page.go
index 4b64089..507d097 100644
--- a/internal/components/page/page.go
+++ b/internal/components/page/page.go
@@ -56,9 +56,14 @@ func (s *Service) getPageHeaders(ctx context.Context, uid string) (page PageHead
}
}
- page.md, err = s.fs.Read(contentMD.Path())
- if err != nil {
- return
+ if contentMD != nil {
+
+ page.md, err = s.fs.Read(contentMD.Path())
+ if err != nil {
+ return
+ }
+ } else {
+ page.md = []byte("# ...")
}
title := titleRGX.FindStringSubmatch(string(page.md))
diff --git a/internal/components/translate/context.go b/internal/components/translate/context.go
index 3489d2c..d1f8e78 100644
--- a/internal/components/translate/context.go
+++ b/internal/components/translate/context.go
@@ -25,6 +25,7 @@ func SupportedLanguages(ctx context.Context) []string {
s, ok := ctx.Value(serviceKey).(*Service)
if !ok {
slog.Error("could not extract translation service from context for getting supported languages")
+ return []string{}
}
return s.SupportedLanguages()
}
diff --git a/internal/handlers/restgallery/controller.go b/internal/handlers/restgallery/controller.go
new file mode 100644
index 0000000..cf8c466
--- /dev/null
+++ b/internal/handlers/restgallery/controller.go
@@ -0,0 +1,18 @@
+package restgallery
+
+import (
+ "log/slog"
+ "net/http"
+
+ "git.schreifuchs.ch/schreifuchs/schreifuchs.ch/web/pages"
+)
+
+func (h *Handler) gallery(w http.ResponseWriter, r *http.Request) {
+ imgs, err := h.srv.GetGallerySet(r.Context())
+ if err != nil {
+ slog.Error("could not get images", "err", err)
+ w.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+ h.r.Render(r.Context(), w, r, pages.GallerySet(imgs))
+}
diff --git a/internal/handlers/restgallery/resource.go b/internal/handlers/restgallery/resource.go
new file mode 100644
index 0000000..a8c08ad
--- /dev/null
+++ b/internal/handlers/restgallery/resource.go
@@ -0,0 +1,40 @@
+package restgallery
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+
+ "git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/components/gallery"
+ "git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/pkg/config"
+ "github.com/a-h/templ"
+)
+
+type Handler struct {
+ http.Handler
+ srv galleryService
+ r renderer
+ cfg *config.Gallery
+}
+
+func New(renderer renderer, srv galleryService, cfg *config.Gallery) *Handler {
+ mux := http.NewServeMux()
+ h := &Handler{
+ Handler: mux,
+ srv: srv,
+ r: renderer,
+ cfg: cfg,
+ }
+
+ mux.HandleFunc(fmt.Sprintf("GET /%s", url.PathEscape(cfg.Path)), h.gallery)
+ return h
+}
+
+type galleryService interface {
+ GetGallerySet(ctx context.Context) (gallerySet gallery.Set, err error)
+}
+type renderer interface {
+ Render(ctx context.Context, w io.Writer, r *http.Request, component templ.Component)
+}
diff --git a/internal/handlers/resthome/controller.go b/internal/handlers/resthome/controller.go
index 7e781e9..9a5afa2 100644
--- a/internal/handlers/resthome/controller.go
+++ b/internal/handlers/resthome/controller.go
@@ -1,6 +1,8 @@
package resthome
import (
+ "context"
+ "errors"
"fmt"
"net/http"
@@ -24,6 +26,10 @@ func (h *Handler) page(w http.ResponseWriter, r *http.Request) {
page, err := h.src.GetPage(r.Context(), uid)
if err != nil {
+ if errors.Is(err, context.Canceled) {
+ w.WriteHeader(http.StatusRequestTimeout)
+ }
+
w.WriteHeader(http.StatusInternalServerError)
return
}
diff --git a/internal/handlers/restimage/conroller.go b/internal/handlers/restimage/controller.go
similarity index 92%
rename from internal/handlers/restimage/conroller.go
rename to internal/handlers/restimage/controller.go
index b25f095..270955c 100644
--- a/internal/handlers/restimage/conroller.go
+++ b/internal/handlers/restimage/controller.go
@@ -31,7 +31,7 @@ func (h *Handler) getImage(w http.ResponseWriter, r *http.Request) {
return
}
- img, mime, err := h.img.GetImage(r.Context(), uid, options)
+ img, mime, err := h.img.GetScaledImage(r.Context(), uid, options)
if err != nil {
slog.Error("error wile serving image", "err", err, "uid", uid)
w.WriteHeader(http.StatusInternalServerError)
diff --git a/internal/handlers/restimage/resource.go b/internal/handlers/restimage/resource.go
index d83dd3e..da2e738 100644
--- a/internal/handlers/restimage/resource.go
+++ b/internal/handlers/restimage/resource.go
@@ -25,5 +25,5 @@ func New(srv imageService) *Handler {
}
type imageService interface {
- GetImage(ctx context.Context, uid string, options images.Options) (img io.Reader, mimeType string, err error)
+ GetScaledImage(ctx context.Context, uid string, options images.Options) (img io.Reader, mimeType string, err error)
}
diff --git a/internal/pkg/config/default.go b/internal/pkg/config/default.go
index 84fc754..a535fec 100644
--- a/internal/pkg/config/default.go
+++ b/internal/pkg/config/default.go
@@ -4,14 +4,22 @@ import "time"
func Default() Cfg {
return Cfg{
- Image: &ImageConfig{
+ Image: &Image{
Concurency: 10,
Quality: 80,
CacheLifetime: time.Hour * 24 * 31,
},
- Tracking: &UmamiConfig{
+ Tracking: &Umami{
ScriptSrc: "https://umami.schreifuchs.ch/script.js",
WebsiteID: "54d8a379-77d5-4c20-b46d-5c9a6c9e39bd",
},
+ Gallery: &Gallery{
+ Path: "photos",
+ },
+ Cache: &Valkey{
+ RefreshTime: time.Minute * 10,
+ LifeTime: time.Hour * 48,
+ },
+ LogJSON: true,
}
}
diff --git a/internal/pkg/config/resource.go b/internal/pkg/config/resource.go
index 362e35e..ebe27e9 100644
--- a/internal/pkg/config/resource.go
+++ b/internal/pkg/config/resource.go
@@ -10,11 +10,13 @@ import (
type Cfg struct {
FileSystem *WebdavConfig `env:", prefix=FILESYSTEM_"`
- Cache *ValkeyConfig `env:", prefix=CACHE_"`
- Image *ImageConfig `env:", prefix=IMAGE_"`
- Tracking *UmamiConfig `env:", prefix=TRACKING_"`
+ Cache *Valkey `env:", prefix=CACHE_"`
+ Image *Image `env:", prefix=IMAGE_"`
+ Tracking *Umami `env:", prefix=TRACKING_"`
+ Gallery *Gallery `env:", prefix=GALLERY_"`
LogLevel slog.Level `env:"LOG_LEVEL, default=DEBUG"`
+ LogJSON bool `env:"LOG_JSON"`
}
type WebdavConfig struct {
@@ -23,11 +25,14 @@ type WebdavConfig struct {
Password string `env:"PASSWORD"`
}
-type ValkeyConfig struct {
+type Valkey struct {
ClientName string `env:"CLIENTNAME"`
Username string `env:"USERNAME"`
Password string `env:"PASSWORD"`
+ RefreshTime time.Duration `env:"REFRESH_TIME"`
+ LifeTime time.Duration `env:"LIFE_TIME"`
+
// InitAddress point to valkey nodes.
// Valkey will connect to them one by one and issue a CLUSTER SLOT command to initialize the cluster client until success.
// If len(InitAddress) == 1 and the address is not running in cluster mode, valkey will fall back to the single client mode.
@@ -36,13 +41,17 @@ type ValkeyConfig struct {
InitAddress []string `env:"ADDRESS"`
}
-type ImageConfig struct {
+type Image struct {
Concurency int64 `env:"CONCURENCY"`
Quality int `env:"QUALITY"`
CacheLifetime time.Duration `env:"CACHE_LIFETIME"`
}
-type UmamiConfig struct {
+type Gallery struct {
+ Path string `env:"PATH"`
+}
+
+type Umami struct {
ScriptSrc string `env:"SCRIPT_SRC"`
WebsiteID string `env:"WEBSITE_ID"`
}
@@ -64,5 +73,7 @@ func Read(ctx context.Context) (cfg Cfg, err error) {
return
}
+ slog.Info("rft", "rft", cfg.Cache.RefreshTime)
+
return
}
diff --git a/internal/pkg/filesystem/cache.go b/internal/pkg/filesystem/cache.go
index 022e346..65a6683 100644
--- a/internal/pkg/filesystem/cache.go
+++ b/internal/pkg/filesystem/cache.go
@@ -23,11 +23,11 @@ type CachedClient struct {
}
// NewCachedClient creates a new CachedClient.
-func NewCachedClient(impl FS, client valkey.Client, refreshtime time.Duration) *CachedClient {
+func NewCachedClient(impl FS, client valkey.Client, refreshtime time.Duration, lifetime time.Duration) *CachedClient {
c := &CachedClient{
impl: impl,
cache: client,
- ttl: refreshtime * 5,
+ ttl: lifetime,
}
go func() {
@@ -42,6 +42,7 @@ func NewCachedClient(impl FS, client valkey.Client, refreshtime time.Duration) *
}
func (c *CachedClient) revalidate(ctx context.Context) {
+ slog.Info("starting revalidation")
go c.revalidateReadDir(ctx)
go c.revalidateRead(ctx)
go c.revalidateStat(ctx)
diff --git a/internal/pkg/filesystem/read.go b/internal/pkg/filesystem/read.go
index ac4b728..5912a82 100644
--- a/internal/pkg/filesystem/read.go
+++ b/internal/pkg/filesystem/read.go
@@ -26,7 +26,6 @@ func (c *CachedClient) Read(path string) ([]byte, error) {
if err == nil && len(cached) > 0 {
c.readHits.Add(1)
c.hits.Add(1)
- slog.Debug("cache hit", "key", key)
var file readFile
err := json.Unmarshal(cached, &file)
diff --git a/internal/pkg/filesystem/readdir.go b/internal/pkg/filesystem/readdir.go
index a9d0fc2..e7412fa 100644
--- a/internal/pkg/filesystem/readdir.go
+++ b/internal/pkg/filesystem/readdir.go
@@ -22,7 +22,6 @@ func (c *CachedClient) ReadDir(path string) ([]os.FileInfo, error) {
if err := json.Unmarshal([]byte(val), &cached); err == nil {
c.readDirHits.Add(1)
c.hits.Add(1)
- slog.Debug("cache hit", "key", key)
infos := make([]os.FileInfo, len(cached))
for i, f := range cached {
infos[i] = f
diff --git a/internal/pkg/filesystem/stat.go b/internal/pkg/filesystem/stat.go
index 05c3eba..d4272a2 100644
--- a/internal/pkg/filesystem/stat.go
+++ b/internal/pkg/filesystem/stat.go
@@ -19,7 +19,6 @@ func (c *CachedClient) Stat(path string) (info os.FileInfo, err error) {
var f File
if err := json.Unmarshal([]byte(val), &f); err == nil {
c.hits.Add(1)
- slog.Debug("cache hit", "key", key)
return f, nil
}
diff --git a/internal/server/factory.go b/internal/server/factory.go
index a299a90..1abf757 100644
--- a/internal/server/factory.go
+++ b/internal/server/factory.go
@@ -3,12 +3,15 @@ package server
import (
"fmt"
"net/http"
+ "path"
"time"
+ "git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/components/gallery"
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/components/images"
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/components/page"
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/components/translate"
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/handlers/rest"
+ "git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/handlers/restgallery"
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/handlers/resthome"
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/handlers/restimage"
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/pkg/filesystem"
@@ -59,11 +62,12 @@ func (s *Server) dynamicRoutes() (h http.Handler, err error) {
}
if err != nil {
} else {
- fs = filesystem.NewCachedClient(webdavClient, valkeyClient, 5*time.Hour)
+ fs = filesystem.NewCachedClient(webdavClient, valkeyClient, s.cfg.Cache.RefreshTime, s.cfg.Cache.LifeTime)
}
+ imageSrv := images.New(fs, s.cfg.Image, valkeyClient)
mux.Handle("/images/", rest.Use(
- restimage.New(images.New(fs, s.cfg.Image, valkeyClient)),
+ restimage.New(imageSrv),
middleware.Cache(time.Hour*24*5),
))
@@ -71,7 +75,14 @@ func (s *Server) dynamicRoutes() (h http.Handler, err error) {
renderer := layouts.New(s.cfg)
mux.Handle("/", rest.Use(
- resthome.New(renderer, page.New(fs)),
+ func() http.Handler {
+ mux := http.NewServeMux()
+
+ mux.Handle(path.Join("/"+s.cfg.Gallery.Path), restgallery.New(renderer, gallery.New(fs, s.cfg.Gallery, imageSrv, valkeyClient), s.cfg.Gallery))
+ mux.Handle("/", resthome.New(renderer, page.New(fs)))
+
+ return mux
+ }(),
translator.Middleware,
middleware.Cache(time.Minute),
))
diff --git a/web/pages/gallery.templ b/web/pages/gallery.templ
new file mode 100644
index 0000000..36abadb
--- /dev/null
+++ b/web/pages/gallery.templ
@@ -0,0 +1,51 @@
+package pages
+
+import (
+ "fmt"
+ "git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/components/gallery"
+)
+
+templ GallerySet(g gallery.Set) {
+
+ for _,img := range g.SM.Images {
+

+ }
+
+
+ @Gallery(g.SM)
+
+
+ @Gallery(g.LG)
+
+
+ @Gallery(g.XLG)
+
+}
+
+templ Gallery(g gallery.Gallery) {
+
+ for _, img := range g.Images {
+
+

+
+ }
+
+}
diff --git a/web/static/css/output.css b/web/static/css/output.css
index 470f7c7..8b20dbe 100644
--- a/web/static/css/output.css
+++ b/web/static/css/output.css
@@ -1,2 +1,2 @@
/*! tailwindcss v4.1.18 | MIT License | https://tailwindcss.com */
-@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-font-weight:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-text-shadow-color:initial;--tw-text-shadow-alpha:100%}}}@layer theme{:root,:host{--font-sans:"Outfit","outfit",sans-serif;--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-6xl:72rem;--text-2xl:1.5rem;--text-2xl--line-height:calc(2/1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25/1.875);--font-weight-bold:700;--text-shadow-lg:0px 0px 20px #000;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components{.prose img{max-height:90vh;margin-left:auto;margin-right:auto;display:block}}@layer utilities{.pointer-events-none{pointer-events:none}.absolute{position:absolute}.fixed{position:fixed}.static{position:static}.z-50{z-index:50}.col-start-1{grid-column-start:1}.row-start-1{grid-row-start:1}.prose{color:var(--tw-prose-body);max-width:65ch}.prose :where(p):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em}.prose :where([class~=lead]):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-lead);margin-top:1.2em;margin-bottom:1.2em;font-size:1.25em;line-height:1.6}.prose :where(a):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-links);font-weight:500;text-decoration:underline}.prose :where(strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-bold);font-weight:600}.prose :where(a strong):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(blockquote strong):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(thead th strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(ol):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em;padding-inline-start:1.625em;list-style-type:decimal}.prose :where(ol[type=A]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-alpha}.prose :where(ol[type=a]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-alpha}.prose :where(ol[type=A s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-alpha}.prose :where(ol[type=a s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-alpha}.prose :where(ol[type=I]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-roman}.prose :where(ol[type=i]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-roman}.prose :where(ol[type=I s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-roman}.prose :where(ol[type=i s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-roman}.prose :where(ol[type="1"]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:decimal}.prose :where(ul):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em;padding-inline-start:1.625em;list-style-type:disc}.prose :where(ol>li):not(:where([class~=not-prose],[class~=not-prose] *))::marker{color:var(--tw-prose-counters);font-weight:400}.prose :where(ul>li):not(:where([class~=not-prose],[class~=not-prose] *))::marker{color:var(--tw-prose-bullets)}.prose :where(dt):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);margin-top:1.25em;font-weight:600}.prose :where(hr):not(:where([class~=not-prose],[class~=not-prose] *)){border-color:var(--tw-prose-hr);border-top-width:1px;margin-top:3em;margin-bottom:3em}.prose :where(blockquote):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-quotes);border-inline-start-width:.25rem;border-inline-start-color:var(--tw-prose-quote-borders);quotes:"“""”""‘""’";margin-top:1.6em;margin-bottom:1.6em;padding-inline-start:1em;font-style:italic;font-weight:500}.prose :where(blockquote p:first-of-type):not(:where([class~=not-prose],[class~=not-prose] *)):before{content:open-quote}.prose :where(blockquote p:last-of-type):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:close-quote}.prose :where(h1):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);margin-top:0;margin-bottom:.888889em;font-size:2.25em;font-weight:800;line-height:1.11111}.prose :where(h1 strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-weight:900}.prose :where(h2):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);margin-top:2em;margin-bottom:1em;font-size:1.5em;font-weight:700;line-height:1.33333}.prose :where(h2 strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-weight:800}.prose :where(h3):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);margin-top:1.6em;margin-bottom:.6em;font-size:1.25em;font-weight:600;line-height:1.6}.prose :where(h3 strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-weight:700}.prose :where(h4):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);margin-top:1.5em;margin-bottom:.5em;font-weight:600;line-height:1.5}.prose :where(h4 strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-weight:700}.prose :where(img):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose :where(picture):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em;display:block}.prose :where(video):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose :where(kbd):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-kbd);box-shadow:0 0 0 1px var(--tw-prose-kbd-shadows),0 3px 0 var(--tw-prose-kbd-shadows);padding-top:.1875em;padding-inline-end:.375em;padding-bottom:.1875em;border-radius:.3125rem;padding-inline-start:.375em;font-family:inherit;font-size:.875em;font-weight:500}.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-code);font-size:.875em;font-weight:600}.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)):before,.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:"`"}.prose :where(a code):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(h1 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(h2 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-size:.875em}.prose :where(h3 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-size:.9em}.prose :where(h4 code):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(blockquote code):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(thead th code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(pre):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-pre-code);background-color:var(--tw-prose-pre-bg);padding-top:.857143em;padding-inline-end:1.14286em;padding-bottom:.857143em;border-radius:.375rem;margin-top:1.71429em;margin-bottom:1.71429em;padding-inline-start:1.14286em;font-size:.875em;font-weight:400;line-height:1.71429;overflow-x:auto}.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)){font-weight:inherit;color:inherit;font-size:inherit;font-family:inherit;line-height:inherit;background-color:#0000;border-width:0;border-radius:0;padding:0}.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)):before,.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:none}.prose :where(table):not(:where([class~=not-prose],[class~=not-prose] *)){table-layout:auto;width:100%;margin-top:2em;margin-bottom:2em;font-size:.875em;line-height:1.71429}.prose :where(thead):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-width:1px;border-bottom-color:var(--tw-prose-th-borders)}.prose :where(thead th):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);vertical-align:bottom;padding-inline-end:.571429em;padding-bottom:.571429em;padding-inline-start:.571429em;font-weight:600}.prose :where(tbody tr):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-width:1px;border-bottom-color:var(--tw-prose-td-borders)}.prose :where(tbody tr:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-width:0}.prose :where(tbody td):not(:where([class~=not-prose],[class~=not-prose] *)){vertical-align:baseline}.prose :where(tfoot):not(:where([class~=not-prose],[class~=not-prose] *)){border-top-width:1px;border-top-color:var(--tw-prose-th-borders)}.prose :where(tfoot td):not(:where([class~=not-prose],[class~=not-prose] *)){vertical-align:top}.prose :where(th,td):not(:where([class~=not-prose],[class~=not-prose] *)){text-align:start}.prose :where(figure>*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0;margin-bottom:0}.prose :where(figcaption):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-captions);margin-top:.857143em;font-size:.875em;line-height:1.42857}.prose{--tw-prose-body:oklch(37.3% .034 259.733);--tw-prose-headings:oklch(21% .034 264.665);--tw-prose-lead:oklch(44.6% .03 256.802);--tw-prose-links:oklch(21% .034 264.665);--tw-prose-bold:oklch(21% .034 264.665);--tw-prose-counters:oklch(55.1% .027 264.364);--tw-prose-bullets:oklch(87.2% .01 258.338);--tw-prose-hr:oklch(92.8% .006 264.531);--tw-prose-quotes:oklch(21% .034 264.665);--tw-prose-quote-borders:oklch(92.8% .006 264.531);--tw-prose-captions:oklch(55.1% .027 264.364);--tw-prose-kbd:oklch(21% .034 264.665);--tw-prose-kbd-shadows:oklab(21% -.00316127 -.0338527/.1);--tw-prose-code:oklch(21% .034 264.665);--tw-prose-pre-code:oklch(92.8% .006 264.531);--tw-prose-pre-bg:oklch(27.8% .033 256.848);--tw-prose-th-borders:oklch(87.2% .01 258.338);--tw-prose-td-borders:oklch(92.8% .006 264.531);--tw-prose-invert-body:oklch(87.2% .01 258.338);--tw-prose-invert-headings:#fff;--tw-prose-invert-lead:oklch(70.7% .022 261.325);--tw-prose-invert-links:#fff;--tw-prose-invert-bold:#fff;--tw-prose-invert-counters:oklch(70.7% .022 261.325);--tw-prose-invert-bullets:oklch(44.6% .03 256.802);--tw-prose-invert-hr:oklch(37.3% .034 259.733);--tw-prose-invert-quotes:oklch(96.7% .003 264.542);--tw-prose-invert-quote-borders:oklch(37.3% .034 259.733);--tw-prose-invert-captions:oklch(70.7% .022 261.325);--tw-prose-invert-kbd:#fff;--tw-prose-invert-kbd-shadows:#ffffff1a;--tw-prose-invert-code:#fff;--tw-prose-invert-pre-code:oklch(87.2% .01 258.338);--tw-prose-invert-pre-bg:#00000080;--tw-prose-invert-th-borders:oklch(44.6% .03 256.802);--tw-prose-invert-td-borders:oklch(37.3% .034 259.733);font-size:1rem;line-height:1.75}.prose :where(picture>img):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0;margin-bottom:0}.prose :where(li):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.5em;margin-bottom:.5em}.prose :where(ol>li):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(ul>li):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:.375em}.prose :where(.prose>ul>li p):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.75em;margin-bottom:.75em}.prose :where(.prose>ul>li>p:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em}.prose :where(.prose>ul>li>p:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.25em}.prose :where(.prose>ol>li>p:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em}.prose :where(.prose>ol>li>p:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.25em}.prose :where(ul ul,ul ol,ol ul,ol ol):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.75em;margin-bottom:.75em}.prose :where(dl):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em}.prose :where(dd):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.5em;padding-inline-start:1.625em}.prose :where(hr+*):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(h2+*):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(h3+*):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(h4+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(thead th:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:0}.prose :where(thead th:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-end:0}.prose :where(tbody td,tfoot td):not(:where([class~=not-prose],[class~=not-prose] *)){padding-top:.571429em;padding-inline-end:.571429em;padding-bottom:.571429em;padding-inline-start:.571429em}.prose :where(tbody td:first-child,tfoot td:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:0}.prose :where(tbody td:last-child,tfoot td:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-end:0}.prose :where(figure):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose :where(.prose>:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(.prose>:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:0}.flex{display:flex}.grid{display:grid}.hidden{display:none}.h-96{height:calc(var(--spacing)*96)}.h-full{height:100%}.min-h-48{min-height:calc(var(--spacing)*48)}.min-h-screen{min-height:100vh}.w-full{width:100%}.max-w-6xl{max-width:var(--container-6xl)}.resize{resize:both}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-rows-1{grid-template-rows:repeat(1,minmax(0,1fr))}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-2{gap:calc(var(--spacing)*2)}.bg-black{background-color:var(--color-black)}.object-cover{object-fit:cover}.p-1{padding:calc(var(--spacing)*1)}.p-2{padding:calc(var(--spacing)*2)}.px-12{padding-inline:calc(var(--spacing)*12)}.pt-8{padding-top:calc(var(--spacing)*8)}.text-justify{text-align:justify}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.text-white{color:var(--color-white)}.uppercase{text-transform:uppercase}.mix-blend-difference{mix-blend-mode:difference}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.prose-invert{--tw-prose-body:var(--tw-prose-invert-body);--tw-prose-headings:var(--tw-prose-invert-headings);--tw-prose-lead:var(--tw-prose-invert-lead);--tw-prose-links:var(--tw-prose-invert-links);--tw-prose-bold:var(--tw-prose-invert-bold);--tw-prose-counters:var(--tw-prose-invert-counters);--tw-prose-bullets:var(--tw-prose-invert-bullets);--tw-prose-hr:var(--tw-prose-invert-hr);--tw-prose-quotes:var(--tw-prose-invert-quotes);--tw-prose-quote-borders:var(--tw-prose-invert-quote-borders);--tw-prose-captions:var(--tw-prose-invert-captions);--tw-prose-kbd:var(--tw-prose-invert-kbd);--tw-prose-kbd-shadows:var(--tw-prose-invert-kbd-shadows);--tw-prose-code:var(--tw-prose-invert-code);--tw-prose-pre-code:var(--tw-prose-invert-pre-code);--tw-prose-pre-bg:var(--tw-prose-invert-pre-bg);--tw-prose-th-borders:var(--tw-prose-invert-th-borders);--tw-prose-td-borders:var(--tw-prose-invert-td-borders)}.text-shadow-lg{text-shadow:var(--text-shadow-lg)}@media (hover:hover){.hover\:underline:hover{text-decoration-line:underline}}}@font-face{font-family:Outfit;font-style:normal;font-weight:100 900;font-display:swap;src:url(../fonts/Outfit-LatinExt-Variable.woff2)format("woff2");unicode-range:U+100-2BA,U+2BD-2C5,U+2C7-2CC,U+2CE-2D7,U+2DD-2FF,U+304,U+308,U+329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Outfit;font-style:normal;font-weight:100 900;font-display:swap;src:url(../fonts/Outfit-Latin-Variable.woff2)format("woff2");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-text-shadow-color{syntax:"*";inherits:false}@property --tw-text-shadow-alpha{syntax:"";inherits:false;initial-value:100%}
\ No newline at end of file
+@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-font-weight:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-text-shadow-color:initial;--tw-text-shadow-alpha:100%}}}@layer theme{:root,:host{--font-sans:"Outfit","outfit",sans-serif;--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-6xl:72rem;--text-2xl:1.5rem;--text-2xl--line-height:calc(2/1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25/1.875);--font-weight-bold:700;--text-shadow-lg:0px 0px 20px #000;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components{.prose img{max-height:90vh;margin-left:auto;margin-right:auto;display:block}}@layer utilities{.pointer-events-none{pointer-events:none}.absolute{position:absolute}.fixed{position:fixed}.static{position:static}.z-50{z-index:50}.col-start-1{grid-column-start:1}.row-start-1{grid-row-start:1}.m-5{margin:calc(var(--spacing)*5)}.prose{color:var(--tw-prose-body);max-width:65ch}.prose :where(p):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em}.prose :where([class~=lead]):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-lead);margin-top:1.2em;margin-bottom:1.2em;font-size:1.25em;line-height:1.6}.prose :where(a):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-links);font-weight:500;text-decoration:underline}.prose :where(strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-bold);font-weight:600}.prose :where(a strong):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(blockquote strong):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(thead th strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(ol):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em;padding-inline-start:1.625em;list-style-type:decimal}.prose :where(ol[type=A]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-alpha}.prose :where(ol[type=a]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-alpha}.prose :where(ol[type=A s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-alpha}.prose :where(ol[type=a s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-alpha}.prose :where(ol[type=I]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-roman}.prose :where(ol[type=i]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-roman}.prose :where(ol[type=I s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-roman}.prose :where(ol[type=i s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-roman}.prose :where(ol[type="1"]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:decimal}.prose :where(ul):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em;padding-inline-start:1.625em;list-style-type:disc}.prose :where(ol>li):not(:where([class~=not-prose],[class~=not-prose] *))::marker{color:var(--tw-prose-counters);font-weight:400}.prose :where(ul>li):not(:where([class~=not-prose],[class~=not-prose] *))::marker{color:var(--tw-prose-bullets)}.prose :where(dt):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);margin-top:1.25em;font-weight:600}.prose :where(hr):not(:where([class~=not-prose],[class~=not-prose] *)){border-color:var(--tw-prose-hr);border-top-width:1px;margin-top:3em;margin-bottom:3em}.prose :where(blockquote):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-quotes);border-inline-start-width:.25rem;border-inline-start-color:var(--tw-prose-quote-borders);quotes:"“""”""‘""’";margin-top:1.6em;margin-bottom:1.6em;padding-inline-start:1em;font-style:italic;font-weight:500}.prose :where(blockquote p:first-of-type):not(:where([class~=not-prose],[class~=not-prose] *)):before{content:open-quote}.prose :where(blockquote p:last-of-type):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:close-quote}.prose :where(h1):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);margin-top:0;margin-bottom:.888889em;font-size:2.25em;font-weight:800;line-height:1.11111}.prose :where(h1 strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-weight:900}.prose :where(h2):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);margin-top:2em;margin-bottom:1em;font-size:1.5em;font-weight:700;line-height:1.33333}.prose :where(h2 strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-weight:800}.prose :where(h3):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);margin-top:1.6em;margin-bottom:.6em;font-size:1.25em;font-weight:600;line-height:1.6}.prose :where(h3 strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-weight:700}.prose :where(h4):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);margin-top:1.5em;margin-bottom:.5em;font-weight:600;line-height:1.5}.prose :where(h4 strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-weight:700}.prose :where(img):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose :where(picture):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em;display:block}.prose :where(video):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose :where(kbd):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-kbd);box-shadow:0 0 0 1px var(--tw-prose-kbd-shadows),0 3px 0 var(--tw-prose-kbd-shadows);padding-top:.1875em;padding-inline-end:.375em;padding-bottom:.1875em;border-radius:.3125rem;padding-inline-start:.375em;font-family:inherit;font-size:.875em;font-weight:500}.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-code);font-size:.875em;font-weight:600}.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)):before,.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:"`"}.prose :where(a code):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(h1 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(h2 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-size:.875em}.prose :where(h3 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-size:.9em}.prose :where(h4 code):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(blockquote code):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(thead th code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(pre):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-pre-code);background-color:var(--tw-prose-pre-bg);padding-top:.857143em;padding-inline-end:1.14286em;padding-bottom:.857143em;border-radius:.375rem;margin-top:1.71429em;margin-bottom:1.71429em;padding-inline-start:1.14286em;font-size:.875em;font-weight:400;line-height:1.71429;overflow-x:auto}.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)){font-weight:inherit;color:inherit;font-size:inherit;font-family:inherit;line-height:inherit;background-color:#0000;border-width:0;border-radius:0;padding:0}.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)):before,.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:none}.prose :where(table):not(:where([class~=not-prose],[class~=not-prose] *)){table-layout:auto;width:100%;margin-top:2em;margin-bottom:2em;font-size:.875em;line-height:1.71429}.prose :where(thead):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-width:1px;border-bottom-color:var(--tw-prose-th-borders)}.prose :where(thead th):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);vertical-align:bottom;padding-inline-end:.571429em;padding-bottom:.571429em;padding-inline-start:.571429em;font-weight:600}.prose :where(tbody tr):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-width:1px;border-bottom-color:var(--tw-prose-td-borders)}.prose :where(tbody tr:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-width:0}.prose :where(tbody td):not(:where([class~=not-prose],[class~=not-prose] *)){vertical-align:baseline}.prose :where(tfoot):not(:where([class~=not-prose],[class~=not-prose] *)){border-top-width:1px;border-top-color:var(--tw-prose-th-borders)}.prose :where(tfoot td):not(:where([class~=not-prose],[class~=not-prose] *)){vertical-align:top}.prose :where(th,td):not(:where([class~=not-prose],[class~=not-prose] *)){text-align:start}.prose :where(figure>*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0;margin-bottom:0}.prose :where(figcaption):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-captions);margin-top:.857143em;font-size:.875em;line-height:1.42857}.prose{--tw-prose-body:oklch(37.3% .034 259.733);--tw-prose-headings:oklch(21% .034 264.665);--tw-prose-lead:oklch(44.6% .03 256.802);--tw-prose-links:oklch(21% .034 264.665);--tw-prose-bold:oklch(21% .034 264.665);--tw-prose-counters:oklch(55.1% .027 264.364);--tw-prose-bullets:oklch(87.2% .01 258.338);--tw-prose-hr:oklch(92.8% .006 264.531);--tw-prose-quotes:oklch(21% .034 264.665);--tw-prose-quote-borders:oklch(92.8% .006 264.531);--tw-prose-captions:oklch(55.1% .027 264.364);--tw-prose-kbd:oklch(21% .034 264.665);--tw-prose-kbd-shadows:oklab(21% -.00316127 -.0338527/.1);--tw-prose-code:oklch(21% .034 264.665);--tw-prose-pre-code:oklch(92.8% .006 264.531);--tw-prose-pre-bg:oklch(27.8% .033 256.848);--tw-prose-th-borders:oklch(87.2% .01 258.338);--tw-prose-td-borders:oklch(92.8% .006 264.531);--tw-prose-invert-body:oklch(87.2% .01 258.338);--tw-prose-invert-headings:#fff;--tw-prose-invert-lead:oklch(70.7% .022 261.325);--tw-prose-invert-links:#fff;--tw-prose-invert-bold:#fff;--tw-prose-invert-counters:oklch(70.7% .022 261.325);--tw-prose-invert-bullets:oklch(44.6% .03 256.802);--tw-prose-invert-hr:oklch(37.3% .034 259.733);--tw-prose-invert-quotes:oklch(96.7% .003 264.542);--tw-prose-invert-quote-borders:oklch(37.3% .034 259.733);--tw-prose-invert-captions:oklch(70.7% .022 261.325);--tw-prose-invert-kbd:#fff;--tw-prose-invert-kbd-shadows:#ffffff1a;--tw-prose-invert-code:#fff;--tw-prose-invert-pre-code:oklch(87.2% .01 258.338);--tw-prose-invert-pre-bg:#00000080;--tw-prose-invert-th-borders:oklch(44.6% .03 256.802);--tw-prose-invert-td-borders:oklch(37.3% .034 259.733);font-size:1rem;line-height:1.75}.prose :where(picture>img):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0;margin-bottom:0}.prose :where(li):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.5em;margin-bottom:.5em}.prose :where(ol>li):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(ul>li):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:.375em}.prose :where(.prose>ul>li p):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.75em;margin-bottom:.75em}.prose :where(.prose>ul>li>p:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em}.prose :where(.prose>ul>li>p:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.25em}.prose :where(.prose>ol>li>p:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em}.prose :where(.prose>ol>li>p:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.25em}.prose :where(ul ul,ul ol,ol ul,ol ol):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.75em;margin-bottom:.75em}.prose :where(dl):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em}.prose :where(dd):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.5em;padding-inline-start:1.625em}.prose :where(hr+*):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(h2+*):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(h3+*):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(h4+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(thead th:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:0}.prose :where(thead th:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-end:0}.prose :where(tbody td,tfoot td):not(:where([class~=not-prose],[class~=not-prose] *)){padding-top:.571429em;padding-inline-end:.571429em;padding-bottom:.571429em;padding-inline-start:.571429em}.prose :where(tbody td:first-child,tfoot td:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:0}.prose :where(tbody td:last-child,tfoot td:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-end:0}.prose :where(figure):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose :where(.prose>:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(.prose>:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:0}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.h-96{height:calc(var(--spacing)*96)}.h-full{height:100%}.min-h-48{min-height:calc(var(--spacing)*48)}.min-h-screen{min-height:100vh}.w-full{width:100%}.max-w-6xl{max-width:var(--container-6xl)}.resize{resize:both}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-rows-1{grid-template-rows:repeat(1,minmax(0,1fr))}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-2{gap:calc(var(--spacing)*2)}.gap-5{gap:calc(var(--spacing)*5)}.overflow-hidden{overflow:hidden}.bg-black{background-color:var(--color-black)}.object-cover{object-fit:cover}.p-1{padding:calc(var(--spacing)*1)}.p-2{padding:calc(var(--spacing)*2)}.px-12{padding-inline:calc(var(--spacing)*12)}.pt-8{padding-top:calc(var(--spacing)*8)}.text-justify{text-align:justify}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.text-white{color:var(--color-white)}.uppercase{text-transform:uppercase}.mix-blend-difference{mix-blend-mode:difference}.shadow,.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.prose-invert{--tw-prose-body:var(--tw-prose-invert-body);--tw-prose-headings:var(--tw-prose-invert-headings);--tw-prose-lead:var(--tw-prose-invert-lead);--tw-prose-links:var(--tw-prose-invert-links);--tw-prose-bold:var(--tw-prose-invert-bold);--tw-prose-counters:var(--tw-prose-invert-counters);--tw-prose-bullets:var(--tw-prose-invert-bullets);--tw-prose-hr:var(--tw-prose-invert-hr);--tw-prose-quotes:var(--tw-prose-invert-quotes);--tw-prose-quote-borders:var(--tw-prose-invert-quote-borders);--tw-prose-captions:var(--tw-prose-invert-captions);--tw-prose-kbd:var(--tw-prose-invert-kbd);--tw-prose-kbd-shadows:var(--tw-prose-invert-kbd-shadows);--tw-prose-code:var(--tw-prose-invert-code);--tw-prose-pre-code:var(--tw-prose-invert-pre-code);--tw-prose-pre-bg:var(--tw-prose-invert-pre-bg);--tw-prose-th-borders:var(--tw-prose-invert-th-borders);--tw-prose-td-borders:var(--tw-prose-invert-td-borders)}.text-shadow-lg{text-shadow:var(--text-shadow-lg)}@media (hover:hover){.hover\:underline:hover{text-decoration-line:underline}}@media (min-width:64rem){.lg\:block{display:block}.lg\:hidden{display:none}}}@font-face{font-family:Outfit;font-style:normal;font-weight:100 900;font-display:swap;src:url(../fonts/Outfit-LatinExt-Variable.woff2)format("woff2");unicode-range:U+100-2BA,U+2BD-2C5,U+2C7-2CC,U+2CE-2D7,U+2DD-2FF,U+304,U+308,U+329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Outfit;font-style:normal;font-weight:100 900;font-display:swap;src:url(../fonts/Outfit-Latin-Variable.woff2)format("woff2");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-text-shadow-color{syntax:"*";inherits:false}@property --tw-text-shadow-alpha{syntax:"";inherits:false;initial-value:100%}
\ No newline at end of file