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 item + } +
+ + + +} + +templ Gallery(g gallery.Gallery) { +
+ for _, img := range g.Images { +
+ Gallery item +
+ } +
+} 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