diff --git a/.env b/.env index 9dcb631..d44f236 100644 --- a/.env +++ b/.env @@ -6,7 +6,7 @@ ASSET_MANIFEST_PATH=web/dist/manifest.json # Public shop URL and upstream proxy target PRESTASHOP_BASE_URL=http://localhost PRESTASHOP_PROXY_TARGET=http://localhost -PRESTASHOP_VERSION=1.7.3 +PRESTASHOP_VERSION=1.7.2 # Cookie settings # Optional explicit override. If omitted, the app derives the cookie name from diff --git a/cmd/proxy/main.go b/cmd/proxy/main.go index fd06ed6..9ce2071 100644 --- a/cmd/proxy/main.go +++ b/cmd/proxy/main.go @@ -71,7 +71,7 @@ func run() error { customerService := pscustomer.NewService(prestaDB, cfg.PrestaShopTablePrefix) cartService := pscart.NewService(prestaDB, cfg.PrestaShopTablePrefix) routeService := psroutes.NewService(prestaDB, cfg.PrestaShopTablePrefix) - sessionService := pssession.NewService(prestaDB, cfg.PrestaShopTablePrefix) + sessionService := pssession.NewService(prestaDB, cfg.PrestaShopTablePrefix, cfg.PrestaShopVersion) productRoute, err := routeService.LoadProductRoute(context.Background()) if err != nil { return fmt.Errorf("load product route rule: %w", err) diff --git a/internal/http/handlers/navigation_test.go b/internal/http/handlers/navigation_test.go new file mode 100644 index 0000000..4a69ed2 --- /dev/null +++ b/internal/http/handlers/navigation_test.go @@ -0,0 +1,23 @@ +package handlers + +import ( + "net/http/httptest" + "testing" + + pscatalog "git.ma-al.com/goc_marek/ps_shop/internal/prestashop/catalog" +) + +func TestAssignMarketSwitchLinksUsesCountryCurrencyID(t *testing.T) { + req := httptest.NewRequest("GET", "https://shop.example.com/pl/product/test", nil) + locale := pscatalog.HeaderLocaleData{ + Countries: []pscatalog.LocaleOption{ + {ID: 36, Code: "PL", CurrencyID: 6, Label: "Polska PLN"}, + }, + } + + assignMarketSwitchLinks(req, &locale) + + if got := locale.Countries[0].URL; got != "/pl/product/test?market=36%3APL%3A6" { + t.Fatalf("market url = %q, want %q", got, "/pl/product/test?market=36%3APL%3A6") + } +} diff --git a/internal/http/middleware/market_test.go b/internal/http/middleware/market_test.go new file mode 100644 index 0000000..16d5c37 --- /dev/null +++ b/internal/http/middleware/market_test.go @@ -0,0 +1,55 @@ +package middleware + +import ( + "testing" + + pscookie "git.ma-al.com/goc_marek/ps_shop/internal/prestashop/cookie" +) + +func TestApplyRequestMarketUsesSelectedCountryCurrency(t *testing.T) { + session := &pscookie.SessionContext{ + Values: map[string]string{ + "date_add": "2026-05-12 10:28:57", + "id_guest": "10", + "id_connections": "11", + "id_lang": "2", + "id_language": "2", + "id_currency": "1", + "id_shop": "1", + "id_cart": "55", + "checksum": "old", + }, + OrderedKeys: []string{"date_add", "id_lang", "id_language", "id_currency", "id_guest", "id_connections", "id_shop", "id_cart", "checksum"}, + } + + applyRequestMarket(session, marketSelection{ + CountryID: 36, + CountryISO: "PL", + CurrencyID: 6, + }) + + if got := session.Values["iso_code_country"]; got != "PL" { + t.Fatalf("iso_code_country = %q, want %q", got, "PL") + } + if _, ok := session.Values["id_country"]; ok { + t.Fatalf("id_country should not be persisted in anonymous market cookie") + } + if got := session.Values["id_currency"]; got != "6" { + t.Fatalf("id_currency = %q, want %q", got, "6") + } + if session.CurrencyID == nil || *session.CurrencyID != 6 { + t.Fatalf("CurrencyID = %v, want 6", session.CurrencyID) + } + if _, ok := session.Values["id_shop"]; ok { + t.Fatalf("id_shop should not be persisted in anonymous market cookie") + } + if _, ok := session.Values["id_cart"]; ok { + t.Fatalf("id_cart should not be persisted in anonymous market cookie") + } + wantOrder := []string{"date_add", "id_lang", "id_language", "iso_code_country", "id_currency", "id_guest", "id_connections", "checksum"} + for i, key := range wantOrder { + if i >= len(session.OrderedKeys) || session.OrderedKeys[i] != key { + t.Fatalf("OrderedKeys[%d] = %q, want %q; full=%v", i, session.OrderedKeys[i], key, session.OrderedKeys) + } + } +} diff --git a/internal/http/middleware/session.go b/internal/http/middleware/session.go index 95c5901..d128cab 100644 --- a/internal/http/middleware/session.go +++ b/internal/http/middleware/session.go @@ -4,10 +4,7 @@ import ( "context" "fmt" "hash/crc32" - "net" "net/http" - "net/url" - "path" "strconv" "strings" @@ -21,6 +18,14 @@ type AnonymousSessionInitializer interface { NewAnonymous(ctx context.Context, req *http.Request, cookieName string) (*pscookie.SessionContext, error) } +type SessionExpiryRefresher interface { + RefreshExpiry(ctx context.Context, session *pscookie.SessionContext) error +} + +type SessionCookieNameResolver interface { + ResolveCookieName(ctx context.Context, req *http.Request) (string, error) +} + type LanguageResolver interface { ResolveLanguageID(ctx context.Context, req *http.Request, fallback int64) int64 } @@ -31,11 +36,22 @@ type ProductRouteMatcher interface { func Session(cfg psconfig.Config, codec pscookie.Codec, initializer AnonymousSessionInitializer, languageResolver LanguageResolver, matcher ProductRouteMatcher) echo.MiddlewareFunc { ownership := cfg.ParseRouteOwnership() + expiryRefresher, _ := initializer.(SessionExpiryRefresher) + cookieNameResolver, _ := initializer.(SessionCookieNameResolver) return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { ownedRoute := ownsProductRoute(ownership.ProductPrefixes, c.Request().URL.Path, matcher) configuredCookieName := cfg.DeriveCookieName(requestCookieHost(c.Request())) + if cookieNameResolver != nil { + resolvedCookieName, err := cookieNameResolver.ResolveCookieName(c.Request().Context(), c.Request()) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("prestashop cookie name resolution failed: %v", err)) + } + if strings.TrimSpace(resolvedCookieName) != "" { + configuredCookieName = resolvedCookieName + } + } cookieName, rawCookie := findPrestaShopCookie(c.Request(), configuredCookieName) if cookieName == "" { cookieName = configuredCookieName @@ -66,12 +82,17 @@ func Session(cfg psconfig.Config, codec pscookie.Codec, initializer AnonymousSes applyRequestMarket(session, requestMarketSelection(c.Request())) } if ownedRoute && shouldSetSessionCookie(rawCookie, session) { + if expiryRefresher != nil { + if err := expiryRefresher.RefreshExpiry(c.Request().Context(), session); err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("prestashop session expiry refresh failed: %v", err)) + } + } encoded, err := codec.Encode(session) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "prestashop cookie encode failed") } session.RawCookie = encoded - setPrestaShopCookie(c.Request(), c.Response(), ownership.ProductPrefixes, cookieName, encoded) + setPrestaShopCookie(c.Request(), c.Response(), session, cookieName, encoded) if redirectURL, ok := clearMarketSelectionURL(c.Request()); ok { return c.Redirect(http.StatusSeeOther, redirectURL) } @@ -213,19 +234,18 @@ func applyRequestMarket(session *pscookie.SessionContext, selection marketSelect session.CurrencyID = int64Ptr(selection.CurrencyID) session.Values["iso_code_country"] = selection.CountryISO - if selection.CountryID > 0 { - session.Values["id_country"] = strconv.FormatInt(selection.CountryID, 10) - session.OrderedKeys = ensureOrderedKey(session.OrderedKeys, "id_country", 5) - } session.Values["id_currency"] = strconv.FormatInt(selection.CurrencyID, 10) + delete(session.Values, "id_country") session.OrderedKeys = ensureOrderedKey(session.OrderedKeys, "iso_code_country", 4) - session.OrderedKeys = ensureOrderedKey(session.OrderedKeys, "id_currency", 6) + session.OrderedKeys = removeOrderedKey(session.OrderedKeys, "id_country") + session.OrderedKeys = ensureOrderedKey(session.OrderedKeys, "id_currency", 5) if !session.IsLoggedIn { - if checksum := anonymousSessionChecksum(session, sessionLanguageID(session)); checksum != "" { - session.Values["checksum"] = checksum - session.OrderedKeys = ensureOrderedKey(session.OrderedKeys, "checksum", len(session.OrderedKeys)) - } + trimAnonymousCookieValues(session) + session.OrderedKeys = ensureOrderedKey(session.OrderedKeys, "id_guest", 6) + session.OrderedKeys = ensureOrderedKey(session.OrderedKeys, "id_connections", 7) + session.OrderedKeys = removeOrderedKey(session.OrderedKeys, "checksum") + session.OrderedKeys = ensureOrderedKey(session.OrderedKeys, "checksum", len(session.OrderedKeys)) } session.Plaintext = "" @@ -284,6 +304,46 @@ func ensureOrderedKey(keys []string, key string, index int) []string { return keys } +func removeOrderedKey(keys []string, key string) []string { + for i, existing := range keys { + if existing == key { + return append(keys[:i], keys[i+1:]...) + } + } + return keys +} + +func trimAnonymousCookieValues(session *pscookie.SessionContext) { + if session == nil || session.Values == nil { + return + } + + allowed := map[string]struct{}{ + "date_add": {}, + "id_lang": {}, + "id_language": {}, + "iso_code_country": {}, + "id_currency": {}, + "id_guest": {}, + "id_connections": {}, + "checksum": {}, + } + + for key := range session.Values { + if _, ok := allowed[key]; !ok { + delete(session.Values, key) + } + } + + filtered := make([]string, 0, len(session.OrderedKeys)) + for _, key := range session.OrderedKeys { + if _, ok := allowed[key]; ok { + filtered = append(filtered, key) + } + } + session.OrderedKeys = filtered +} + func int64Ptr(value int64) *int64 { if value == 0 { return nil @@ -353,53 +413,17 @@ func clearMarketSelectionURL(req *http.Request) (string, bool) { return cleanPath, true } -func setPrestaShopCookie(req *http.Request, res *echo.Response, ownedPrefixes []string, name, value string) { - http.SetCookie(res.Writer, &http.Cookie{ - Name: name, - Value: value, - Path: requestCookiePath(req.URL.Path, ownedPrefixes), - Domain: requestCookieDomain(req), - Secure: requestCookieSecure(req), - HttpOnly: true, - SameSite: http.SameSiteLaxMode, - }) -} - -func requestCookiePath(requestPath string, ownedPrefixes []string) string { - for _, prefix := range ownedPrefixes { - if prefix == "" || !strings.HasPrefix(requestPath, prefix) { - continue - } - base := path.Clean(strings.TrimSuffix(prefix, "/")) - if base == "." || base == "/" { - return "/" - } - parent := path.Dir(base) - if parent == "." { - return "/" - } - if !strings.HasSuffix(parent, "/") { - parent += "/" - } - return parent +func setPrestaShopCookie(req *http.Request, res *echo.Response, session *pscookie.SessionContext, name, value string) { + maxAge := 1 + if session != nil && session.ExpiresAt != nil { + maxAge = int(session.ExpiresAt.UTC().Unix()) } - return "/" -} - -func requestCookieDomain(req *http.Request) string { - host := requestCookieHost(req) - if host == "" { - return "" + header := fmt.Sprintf("%s=%s; path=/; max-age=%d; HttpOnly; SameSite=Lax", name, value, maxAge) + if requestCookieSecure(req) { + header += "; Secure" } - if parsed, err := url.Parse("http://" + host); err == nil { - host = parsed.Hostname() - } - host = strings.TrimSpace(strings.TrimPrefix(host, ".")) - if host == "" || strings.EqualFold(host, "localhost") || net.ParseIP(host) != nil { - return "" - } - return host + res.Header().Add(echo.HeaderSetCookie, header) } func requestCookieHost(req *http.Request) string { diff --git a/internal/http/middleware/session_test.go b/internal/http/middleware/session_test.go new file mode 100644 index 0000000..bd3a9bf --- /dev/null +++ b/internal/http/middleware/session_test.go @@ -0,0 +1,36 @@ +package middleware + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/labstack/echo/v4" + + pscookie "git.ma-al.com/goc_marek/ps_shop/internal/prestashop/cookie" +) + +func TestSetPrestaShopCookiePersistsExpiry(t *testing.T) { + e := echo.New() + req := httptest.NewRequest(http.MethodGet, "https://shop.example.com/product/test", nil) + rec := httptest.NewRecorder() + res := e.NewContext(req, rec).Response() + expiresAt := time.Now().UTC().Add(4 * time.Hour).Truncate(time.Second) + + setPrestaShopCookie(req, res, &pscookie.SessionContext{ + ExpiresAt: &expiresAt, + }, "PrestaShop-test", "value") + + setCookie := rec.Header().Get("Set-Cookie") + if !strings.Contains(setCookie, "max-age=") { + t.Fatalf("Set-Cookie missing max-age: %q", setCookie) + } + if strings.Contains(setCookie, "Expires=") { + t.Fatalf("Set-Cookie should not include Expires: %q", setCookie) + } + if !strings.Contains(setCookie, "path=/") { + t.Fatalf("Set-Cookie missing path=/: %q", setCookie) + } +} diff --git a/internal/prestashop/catalog/service.go b/internal/prestashop/catalog/service.go index bfc4271..0be55ce 100644 --- a/internal/prestashop/catalog/service.go +++ b/internal/prestashop/catalog/service.go @@ -391,6 +391,9 @@ ORDER BY cl.name ASC if err := s.db.WithContext(ctx).Raw(strings.TrimSpace(countryQuery), countryArgs...).Scan(&locale.Countries).Error; err != nil { return HeaderLocaleData{}, err } + for i := range locale.Countries { + locale.Countries[i] = formatCountryLocaleOption(locale.Countries[i]) + } locale.CurrentLanguage = pickLocaleOptionByID(locale.Languages, languageID) locale.CurrentCountry = pickLocaleOptionByCode(locale.Countries, countryISO) @@ -499,3 +502,17 @@ func pickLocaleOptionByCode(options []LocaleOption, code string) LocaleOption { } return LocaleOption{Code: code, Label: code, Meta: code} } + +func formatCountryLocaleOption(option LocaleOption) LocaleOption { + label := strings.TrimSpace(option.Label) + currencyCode := strings.TrimSpace(option.Meta) + if idx := strings.IndexByte(currencyCode, ' '); idx >= 0 { + currencyCode = currencyCode[:idx] + } + currencyCode = strings.TrimSpace(currencyCode) + if label == "" || currencyCode == "" { + return option + } + option.Label = label + " " + currencyCode + return option +} diff --git a/internal/prestashop/catalog/service_test.go b/internal/prestashop/catalog/service_test.go new file mode 100644 index 0000000..5d54eff --- /dev/null +++ b/internal/prestashop/catalog/service_test.go @@ -0,0 +1,26 @@ +package catalog + +import "testing" + +func TestFormatCountryLocaleOptionAddsCurrencyCodeToLabel(t *testing.T) { + option := LocaleOption{ + Label: "Polska", + Meta: "PLN zl", + } + + got := formatCountryLocaleOption(option) + if got.Label != "Polska PLN" { + t.Fatalf("formatCountryLocaleOption().Label = %q, want %q", got.Label, "Polska PLN") + } +} + +func TestFormatCountryLocaleOptionFallsBackWithoutMeta(t *testing.T) { + option := LocaleOption{ + Label: "Polska", + } + + got := formatCountryLocaleOption(option) + if got.Label != "Polska" { + t.Fatalf("formatCountryLocaleOption().Label = %q, want %q", got.Label, "Polska") + } +} diff --git a/internal/prestashop/config/config.go b/internal/prestashop/config/config.go index d542a0b..9c90d55 100644 --- a/internal/prestashop/config/config.go +++ b/internal/prestashop/config/config.go @@ -140,15 +140,15 @@ func (c Config) DeriveCookieName(host string) string { return c.PrestaShopCookieName } - domain := normalizedCookieDomain(host) + domain := fallbackCookieHashDomain(host) if domain == "" { - domain = normalizedCookieDomain(c.PrestaShopBaseURL) + domain = fallbackCookieHashDomain(c.PrestaShopBaseURL) } if domain == "" { - domain = normalizedCookieDomain(c.PrestaShopProxyTarget) + domain = fallbackCookieHashDomain(c.PrestaShopProxyTarget) } - sum := md5.Sum([]byte(c.PrestaShopVersion + "PrestaShop" + domain)) + sum := md5.Sum([]byte(c.PrestaShopVersion + "ps-s1" + domain)) return fmt.Sprintf("PrestaShop-%x", sum) } @@ -256,6 +256,17 @@ func normalizedCookieDomain(input string) string { return value } +func fallbackCookieHashDomain(input string) string { + value := normalizedCookieDomain(input) + if value == "" { + return "" + } + if net.ParseIP(value) != nil || !strings.Contains(value, ".") { + return "" + } + return value +} + func dbDSNFromEnv() string { return mysqlDSN( firstNonEmpty(os.Getenv("PRESTASHOP_DB_HOST"), os.Getenv("DB_HOST")), diff --git a/internal/prestashop/config/config_test.go b/internal/prestashop/config/config_test.go new file mode 100644 index 0000000..a53a689 --- /dev/null +++ b/internal/prestashop/config/config_test.go @@ -0,0 +1,19 @@ +package config + +import ( + "crypto/md5" + "fmt" + "testing" +) + +func TestDeriveCookieNameMatchesFallbackPrestashopRule(t *testing.T) { + cfg := Config{PrestaShopVersion: "1.7.3"} + + got := cfg.DeriveCookieName("localhost") + sum := md5.Sum([]byte("1.7.3" + "ps-s1")) + want := fmt.Sprintf("PrestaShop-%x", sum) + + if got != want { + t.Fatalf("DeriveCookieName() = %q, want %q", got, want) + } +} diff --git a/internal/prestashop/cookie/codec.go b/internal/prestashop/cookie/codec.go index 08593ef..b32f025 100644 --- a/internal/prestashop/cookie/codec.go +++ b/internal/prestashop/cookie/codec.go @@ -10,6 +10,7 @@ import ( "errors" "fmt" "hash" + "hash/crc32" "sort" "strings" ) @@ -99,7 +100,7 @@ func (c *nativeCodec) Encode(session *SessionContext) (string, error) { plaintext := session.Plaintext if plaintext == "" { - plaintext = serializeValues(session.Values, session.OrderedKeys) + plaintext = serializeCookieValues(session.Values, session.OrderedKeys, c.cfg.CookieIV) } return c.encryptInternal(plaintext) } @@ -321,6 +322,67 @@ func serializeValues(values map[string]string, orderedKeys []string) string { return strings.Join(pairs, fieldSeparator) } +func serializeCookieValues(values map[string]string, orderedKeys []string, cookieIV string) string { + if len(values) == 0 { + return "" + } + + keys := orderedValueKeys(values, orderedKeys, "checksum") + if len(keys) == 0 { + return "" + } + + var builder strings.Builder + for _, key := range keys { + builder.WriteString(key) + builder.WriteString(pairSeparator) + builder.WriteString(values[key]) + builder.WriteString(fieldSeparator) + } + + checksum := crc32.ChecksumIEEE([]byte(cookieIV + builder.String())) + builder.WriteString("checksum") + builder.WriteString(pairSeparator) + builder.WriteString(fmt.Sprintf("%d", checksum)) + return builder.String() +} + +func orderedValueKeys(values map[string]string, orderedKeys []string, excluded ...string) []string { + if len(values) == 0 { + return nil + } + + excludeSet := make(map[string]struct{}, len(excluded)) + for _, key := range excluded { + excludeSet[key] = struct{}{} + } + + keys := make([]string, 0, len(values)) + seen := map[string]struct{}{} + for _, key := range orderedKeys { + if _, skip := excludeSet[key]; skip { + continue + } + if _, ok := values[key]; ok { + keys = append(keys, key) + seen[key] = struct{}{} + } + } + + extra := make([]string, 0) + for key := range values { + if _, skip := excludeSet[key]; skip { + continue + } + if _, ok := seen[key]; !ok { + extra = append(extra, key) + } + } + sort.Strings(extra) + keys = append(keys, extra...) + return keys +} + func int64Ptr(value string) *int64 { if value == "" { return nil diff --git a/internal/prestashop/cookie/codec_test.go b/internal/prestashop/cookie/codec_test.go index 29a10fd..f58ab75 100644 --- a/internal/prestashop/cookie/codec_test.go +++ b/internal/prestashop/cookie/codec_test.go @@ -1,6 +1,11 @@ package cookie -import "testing" +import ( + "fmt" + "hash/crc32" + "strings" + "testing" +) const ( testCookieKey = "def000008bf3d70e7012b7493c382d561e193218d0c74ab162fb0ea8029ce20e926531b4bcf0aaec9381152e6c161f198e06918b2d1aad67cc7cf40819a51ee328c63830" @@ -66,3 +71,44 @@ func TestNativeCodecRoundTrip(t *testing.T) { t.Fatalf("plaintext mismatch after roundtrip\n got: %s\nwant: %s", redecoded.Plaintext, decoded.Plaintext) } } + +func TestNativeCodecEncodeRecomputesPrestashopChecksum(t *testing.T) { + codec, err := NewCodec(Config{ + CookieName: "PrestaShop-test", + CookieKey: testCookieKey, + CookieIV: "vfRFMV42", + }) + if err != nil { + t.Fatalf("NewCodec() error = %v", err) + } + + decoded, err := codec.Decode(testCookie) + if err != nil { + t.Fatalf("Decode() error = %v", err) + } + + decoded.Values["iso_code_country"] = "PL" + decoded.Values["id_currency"] = "6" + decoded.Values["checksum"] = "stale" + decoded.Plaintext = "" + + encoded, err := codec.Encode(decoded) + if err != nil { + t.Fatalf("Encode() error = %v", err) + } + + redecoded, err := codec.Decode(encoded) + if err != nil { + t.Fatalf("Decode(encoded) error = %v", err) + } + + pairs := strings.Split(redecoded.Plaintext, fieldSeparator) + if len(pairs) < 2 { + t.Fatalf("plaintext too short: %q", redecoded.Plaintext) + } + body := strings.Join(pairs[:len(pairs)-1], fieldSeparator) + fieldSeparator + wantChecksum := fmt.Sprintf("%d", crc32.ChecksumIEEE([]byte("vfRFMV42"+body))) + if got := redecoded.Values["checksum"]; got != wantChecksum { + t.Fatalf("checksum = %q, want %q", got, wantChecksum) + } +} diff --git a/internal/prestashop/session/service.go b/internal/prestashop/session/service.go index 8b14402..71e5cab 100644 --- a/internal/prestashop/session/service.go +++ b/internal/prestashop/session/service.go @@ -2,10 +2,13 @@ package session import ( "context" + "crypto/md5" "fmt" "hash/crc32" "net" "net/http" + "net/url" + "regexp" "strconv" "strings" "time" @@ -16,8 +19,9 @@ import ( ) type Service struct { - db *gorm.DB - prefix string + db *gorm.DB + prefix string + version string } type defaults struct { @@ -26,10 +30,11 @@ type defaults struct { ShopID int64 ShopGroupID int64 CountryISO string + CookieHours int64 } -func NewService(db *gorm.DB, prefix string) *Service { - return &Service{db: db, prefix: prefix} +func NewService(db *gorm.DB, prefix, version string) *Service { + return &Service{db: db, prefix: prefix, version: version} } func (s *Service) NewAnonymous(ctx context.Context, req *http.Request, cookieName string) (*pscookie.SessionContext, error) { @@ -88,12 +93,52 @@ func (s *Service) NewAnonymous(ctx context.Context, req *http.Request, cookieNam ShopID: int64Ptr(def.ShopID), GuestID: int64Ptr(guestID), IsLoggedIn: false, + ExpiresAt: cookieExpiry(now, def.CookieHours), Values: values, OrderedKeys: orderedKeys, ParseStatus: pscookie.ParseStatusAnonymous, }, nil } +func (s *Service) RefreshExpiry(ctx context.Context, session *pscookie.SessionContext) error { + if s == nil || session == nil { + return nil + } + + def, err := s.loadDefaults(ctx) + if err != nil { + return err + } + + session.ExpiresAt = cookieExpiry(time.Now().UTC(), def.CookieHours) + return nil +} + +func (s *Service) ResolveCookieName(ctx context.Context, req *http.Request) (string, error) { + if s == nil || s.db == nil { + return "", fmt.Errorf("prestashop session service is not initialized") + } + + host := requestHost(req) + shop, err := s.loadCookieShopContext(ctx, req) + if err != nil { + return "", err + } + + baseName := "ps-s" + strconv.FormatInt(shop.ShopID, 10) + sharedDomains := []string(nil) + if shop.ShareOrder { + baseName = "ps-sg" + strconv.FormatInt(shop.ShopGroupID, 10) + sharedDomains, err = s.loadSharedCartDomains(ctx, shop.ShopGroupID) + if err != nil { + return "", err + } + } + + sum := md5.Sum([]byte(s.version + baseName + prestashopCookieDomain(host, sharedDomains))) + return fmt.Sprintf("PrestaShop-%x", sum), nil +} + func (s *Service) loadDefaults(ctx context.Context) (*defaults, error) { def := &defaults{ LanguageID: 1, @@ -101,6 +146,7 @@ func (s *Service) loadDefaults(ctx context.Context) (*defaults, error) { ShopID: 1, ShopGroupID: 1, CountryISO: "US", + CookieHours: 480, } configTable := s.prefix + "configuration" @@ -111,7 +157,7 @@ func (s *Service) loadDefaults(ctx context.Context) (*defaults, error) { Name string Value string } - configQuery := fmt.Sprintf("SELECT name, value FROM %s WHERE name IN ('PS_LANG_DEFAULT', 'PS_CURRENCY_DEFAULT', 'PS_COUNTRY_DEFAULT')", configTable) + configQuery := fmt.Sprintf("SELECT name, value FROM %s WHERE name IN ('PS_LANG_DEFAULT', 'PS_CURRENCY_DEFAULT', 'PS_COUNTRY_DEFAULT', 'PS_COOKIE_LIFETIME_FO')", configTable) if err := s.db.WithContext(ctx).Raw(configQuery).Scan(&configs).Error; err != nil { return nil, err } @@ -131,6 +177,10 @@ func (s *Service) loadDefaults(ctx context.Context) (*defaults, error) { if parsed, err := strconv.ParseInt(cfg.Value, 10, 64); err == nil && parsed > 0 { countryID = parsed } + case "PS_COOKIE_LIFETIME_FO": + if parsed, err := strconv.ParseInt(cfg.Value, 10, 64); err == nil && parsed > 0 { + def.CookieHours = parsed + } } } @@ -272,6 +322,195 @@ func (s *Service) connectionInsert(ctx context.Context, def *defaults, guestID i return columns, values, nil } +func cookieExpiry(now time.Time, lifetimeHours int64) *time.Time { + if lifetimeHours <= 0 { + return nil + } + expiresAt := now.Add(time.Duration(lifetimeHours) * time.Hour) + return &expiresAt +} + +type cookieShopContext struct { + ShopID int64 `gorm:"column:id_shop"` + ShopGroupID int64 `gorm:"column:id_shop_group"` + ShareOrder bool `gorm:"column:share_order"` + URI string `gorm:"column:uri"` + Main bool `gorm:"column:main"` +} + +func (s *Service) loadCookieShopContext(ctx context.Context, req *http.Request) (*cookieShopContext, error) { + normalizedHost := requestHost(req) + requestURI := requestPath(req) + + shopURLTable := s.prefix + "shop_url" + shopTable := s.prefix + "shop" + shopGroupTable := s.prefix + "shop_group" + + if normalizedHost != "" { + query := fmt.Sprintf(` +SELECT s.id_shop, s.id_shop_group, sg.share_order, CONCAT(su.physical_uri, su.virtual_uri) AS uri, su.main +FROM %s s +JOIN %s sg ON sg.id_shop_group = s.id_shop_group +JOIN %s su ON su.id_shop = s.id_shop +WHERE su.active = 1 + AND s.active = 1 + AND s.deleted = 0 + AND (LOWER(su.domain) = ? OR LOWER(su.domain_ssl) = ?) +ORDER BY LENGTH(CONCAT(su.physical_uri, su.virtual_uri)) DESC, su.main DESC, s.id_shop ASC +`, shopTable, shopGroupTable, shopURLTable) + var shops []cookieShopContext + if err := s.db.WithContext(ctx).Raw(query, normalizedHost, normalizedHost).Scan(&shops).Error; err != nil { + return nil, err + } + for _, shop := range shops { + if uriMatchesRequest(shop.URI, requestURI) { + return &shop, nil + } + } + } + + fallbackQuery := fmt.Sprintf(` +SELECT s.id_shop, s.id_shop_group, sg.share_order, '' AS uri, 1 AS main +FROM %s s +JOIN %s sg ON sg.id_shop_group = s.id_shop_group +WHERE s.active = 1 + AND s.deleted = 0 +ORDER BY s.id_shop ASC +LIMIT 1 +`, shopTable, shopGroupTable) + var shop cookieShopContext + if err := s.db.WithContext(ctx).Raw(fallbackQuery).Scan(&shop).Error; err != nil { + return nil, err + } + if shop.ShopID == 0 { + return nil, fmt.Errorf("prestashop shop context not found") + } + return &shop, nil +} + +func (s *Service) loadSharedCartDomains(ctx context.Context, shopGroupID int64) ([]string, error) { + if shopGroupID == 0 { + return nil, nil + } + + type row struct { + Domain string `gorm:"column:domain"` + } + + shopURLTable := s.prefix + "shop_url" + shopTable := s.prefix + "shop" + query := fmt.Sprintf(` +SELECT su.domain +FROM %s su +JOIN %s s ON s.id_shop = su.id_shop +WHERE su.main = 1 + AND su.active = 1 + AND s.id_shop_group = ? +`, shopURLTable, shopTable) + + var rows []row + if err := s.db.WithContext(ctx).Raw(query, shopGroupID).Scan(&rows).Error; err != nil { + return nil, err + } + + domains := make([]string, 0, len(rows)) + for _, row := range rows { + if host := normalizeRequestHost(row.Domain); host != "" { + domains = append(domains, host) + } + } + + return domains, nil +} + +func normalizeRequestHost(input string) string { + value := strings.TrimSpace(input) + if value == "" { + return "" + } + if strings.Contains(value, "://") { + if parsed, err := url.Parse(value); err == nil { + value = parsed.Hostname() + } + } + if host, _, err := net.SplitHostPort(value); err == nil { + value = host + } + return strings.ToLower(strings.TrimSpace(value)) +} + +func requestHost(req *http.Request) string { + if req == nil { + return "" + } + + host := req.Header.Get("X-Forwarded-Host") + if host == "" { + host = req.Host + } + if strings.Contains(host, ",") { + host = strings.TrimSpace(strings.Split(host, ",")[0]) + } + + return normalizeRequestHost(host) +} + +func requestPath(req *http.Request) string { + if req == nil || req.URL == nil { + return "/" + } + path := req.URL.EscapedPath() + if path == "" { + path = req.URL.Path + } + if path == "" { + return "/" + } + decoded, err := url.PathUnescape(path) + if err == nil && decoded != "" { + path = decoded + } + if !strings.HasPrefix(path, "/") { + path = "/" + path + } + return path +} + +func uriMatchesRequest(uri, requestURI string) bool { + if uri == "" { + uri = "/" + } + if requestURI == "" { + requestURI = "/" + } + return strings.HasPrefix(strings.ToLower(requestURI), strings.ToLower(uri)) +} + +var sharedDomainPattern = regexp.MustCompile(`^(?:.*\.)?([^.]*(?:.{2,4})?\..{2,3})$`) + +func prestashopCookieDomain(host string, sharedURLs []string) string { + normalizedHost := normalizeRequestHost(host) + if normalizedHost == "" { + return "" + } + if net.ParseIP(normalizedHost) != nil || !strings.Contains(normalizedHost, ".") { + return "" + } + + for _, sharedURL := range sharedURLs { + if normalizeRequestHost(sharedURL) != normalizedHost { + continue + } + matches := sharedDomainPattern.FindStringSubmatch(normalizedHost) + if len(matches) == 2 { + return "." + matches[1] + } + break + } + + return normalizedHost +} + func (s *Service) tableColumns(ctx context.Context, tableName string) (map[string]bool, error) { type columnRow struct { ColumnName string `gorm:"column:COLUMN_NAME"` diff --git a/internal/prestashop/session/service_test.go b/internal/prestashop/session/service_test.go new file mode 100644 index 0000000..d40f2f9 --- /dev/null +++ b/internal/prestashop/session/service_test.go @@ -0,0 +1,27 @@ +package session + +import "testing" + +func TestPrestashopCookieDomain(t *testing.T) { + if got := prestashopCookieDomain("localhost", nil); got != "" { + t.Fatalf("prestashopCookieDomain(localhost) = %q, want empty", got) + } + + if got := prestashopCookieDomain("shop.example.com", []string{"shop.example.com"}); got != ".example.com" { + t.Fatalf("prestashopCookieDomain(shared) = %q, want %q", got, ".example.com") + } + + if got := prestashopCookieDomain("shop.example.com", nil); got != "shop.example.com" { + t.Fatalf("prestashopCookieDomain(single) = %q, want %q", got, "shop.example.com") + } +} + +func TestURIMatchesRequest(t *testing.T) { + if !uriMatchesRequest("/shop/fr/", "/shop/fr/product/test") { + t.Fatalf("expected nested shop URI to match request path") + } + + if uriMatchesRequest("/shop/fr/", "/shop/en/product/test") { + t.Fatalf("unexpected match for different shop URI") + } +} diff --git a/scripts/prestashop_cookie_bridge.php b/scripts/prestashop_cookie_bridge.php deleted file mode 100644 index 0a87448..0000000 --- a/scripts/prestashop_cookie_bridge.php +++ /dev/null @@ -1,79 +0,0 @@ - \n"); - exit(1); -} - -$mode = $argv[1]; -$bootstrap = $argv[2]; -$input = json_decode(stream_get_contents(STDIN), true); - -if (!is_array($input)) { - fwrite(STDERR, "invalid input\n"); - exit(1); -} - -if (!is_file($bootstrap)) { - fwrite(STDERR, "bootstrap not found\n"); - exit(1); -} - -$cookieName = $input['cookie_name'] ?? null; -if (!$cookieName) { - fwrite(STDERR, "cookie name missing\n"); - exit(1); -} - -$_SERVER['HTTP_HOST'] = $_SERVER['HTTP_HOST'] ?? 'localhost'; -$_SERVER['REQUEST_METHOD'] = $_SERVER['REQUEST_METHOD'] ?? 'GET'; -$_SERVER['REMOTE_ADDR'] = $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1'; -$_COOKIE[$cookieName] = $input['raw_cookie'] ?? ''; - -require_once $bootstrap; - -if (!class_exists('Cookie')) { - fwrite(STDERR, "prestashop cookie class unavailable\n"); - exit(1); -} - -$cookie = new Cookie($cookieName); - -if ($mode !== 'decode') { - fwrite(STDERR, "unsupported mode\n"); - exit(1); -} - -$reflection = new ReflectionClass($cookie); -$content = []; -if ($reflection->hasProperty('_content')) { - $property = $reflection->getProperty('_content'); - $property->setAccessible(true); - $content = $property->getValue($cookie); -} - -$response = [ - 'customer_id' => isset($content['id_customer']) ? (int) $content['id_customer'] : null, - 'cart_id' => isset($content['id_cart']) ? (int) $content['id_cart'] : null, - 'language_id' => isset($content['id_lang']) ? (int) $content['id_lang'] : null, - 'currency_id' => isset($content['id_currency']) ? (int) $content['id_currency'] : null, - 'shop_id' => isset($content['id_shop']) ? (int) $content['id_shop'] : null, - 'guest_id' => isset($content['id_guest']) ? (int) $content['id_guest'] : null, - 'is_logged_in' => !empty($content['logged']), - 'expires_at' => null, - 'values' => array_map(static function ($value): string { - if (is_bool($value)) { - return $value ? '1' : '0'; - } - if (is_scalar($value) || $value === null) { - return (string) $value; - } - return json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: ''; - }, $content), - 'raw_cookie' => $input['raw_cookie'] ?? '', -]; - -header('Content-Type: application/json'); -echo json_encode($response, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); diff --git a/templates/category.templ b/templates/category.templ index 8373ec8..dceca81 100644 --- a/templates/category.templ +++ b/templates/category.templ @@ -43,6 +43,27 @@ templ CategoryPage(data viewmodel.CategoryPageData, cssPath string, jsPath strin } + + if data.Session != nil { +
+

Go Cookie Debug

+
+
+

Raw Cookie

+
{ data.Session.RawCookie }
+
+
+

Decoded Values

+
+									for _, line := range sessionCookieLines(data.Session) {
+										{ line }
+										{"\n"}
+									}
+								
+
+
+
+ } } diff --git a/templates/category_templ.go b/templates/category_templ.go index 2e49b6f..7e91a66 100644 --- a/templates/category_templ.go +++ b/templates/category_templ.go @@ -181,7 +181,58 @@ func CategoryPage(data viewmodel.CategoryPageData, cssPath string, jsPath string return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.Session != nil { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "

Go Cookie Debug

Raw Cookie

")
+				if templ_7745c5c3_Err != nil {
+					return templ_7745c5c3_Err
+				}
+				var templ_7745c5c3_Var10 string
+				templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(data.Session.RawCookie)
+				if templ_7745c5c3_Err != nil {
+					return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/category.templ`, Line: 53, Col: 127}
+				}
+				_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
+				if templ_7745c5c3_Err != nil {
+					return templ_7745c5c3_Err
+				}
+				templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "

Decoded Values

")
+				if templ_7745c5c3_Err != nil {
+					return templ_7745c5c3_Err
+				}
+				for _, line := range sessionCookieLines(data.Session) {
+					var templ_7745c5c3_Var11 string
+					templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(line)
+					if templ_7745c5c3_Err != nil {
+						return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/category.templ`, Line: 59, Col: 16}
+					}
+					_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
+					if templ_7745c5c3_Err != nil {
+						return templ_7745c5c3_Err
+					}
+					templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, " ")
+					if templ_7745c5c3_Err != nil {
+						return templ_7745c5c3_Err
+					}
+					var templ_7745c5c3_Var12 string
+					templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs("\n")
+					if templ_7745c5c3_Err != nil {
+						return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/category.templ`, Line: 60, Col: 15}
+					}
+					_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
+					if templ_7745c5c3_Err != nil {
+						return templ_7745c5c3_Err
+					}
+				}
+				templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/templates/layout.templ b/templates/layout.templ index d9c7f37..cb84489 100644 --- a/templates/layout.templ +++ b/templates/layout.templ @@ -28,8 +28,8 @@ templ Layout(title string, cssPath string, jsPath string, menu []pscatalog.MenuI if hasHeaderLocale(locale) {
- @LocalePicker("Market", locale.CurrentCountry, locale.Countries, true) - @LocalePicker("Language", locale.CurrentLanguage, locale.Languages, true) + @LocalePicker("Market", locale.CurrentCountry, locale.Countries, true, false) + @LocalePicker("Language", locale.CurrentLanguage, locale.Languages, true, true)
} @@ -67,7 +67,7 @@ templ Layout(title string, cssPath string, jsPath string, menu []pscatalog.MenuI } -templ LocalePicker(title string, current pscatalog.LocaleOption, options []pscatalog.LocaleOption, navigable bool) { +templ LocalePicker(title string, current pscatalog.LocaleOption, options []pscatalog.LocaleOption, navigable bool, showMeta bool) {
@@ -77,7 +77,7 @@ templ LocalePicker(title string, current pscatalog.LocaleOption, options []pscat { title } } - if current.Code != "" { + if showMeta && current.Code != "" { { current.Code } } @@ -91,16 +91,16 @@ templ LocalePicker(title string, current pscatalog.LocaleOption, options []pscat if navigable && option.URL != "" { { option.Label } - if option.Meta != "" { + if showMeta && option.Meta != "" { { option.Meta } - } else if option.Code != "" { + } else if showMeta && option.Code != "" { { option.Code } } } else { { option.Label } - if option.Meta != "" { + if showMeta && option.Meta != "" { { option.Meta } } diff --git a/templates/layout_helpers.go b/templates/layout_helpers.go index 6f8d7c1..814ce33 100644 --- a/templates/layout_helpers.go +++ b/templates/layout_helpers.go @@ -1,10 +1,12 @@ package templates import ( + "sort" "strconv" "strings" pscatalog "git.ma-al.com/goc_marek/ps_shop/internal/prestashop/catalog" + pscookie "git.ma-al.com/goc_marek/ps_shop/internal/prestashop/cookie" ) func menuListClass(depth int) string { @@ -76,3 +78,21 @@ func menuPanelID(id int64) string { } return "mega-menu-panel-" + strconv.FormatInt(id, 10) } + +func sessionCookieLines(session *pscookie.SessionContext) []string { + if session == nil || len(session.Values) == 0 { + return nil + } + + keys := make([]string, 0, len(session.Values)) + for key := range session.Values { + keys = append(keys, key) + } + sort.Strings(keys) + + lines := make([]string, 0, len(keys)) + for _, key := range keys { + lines = append(lines, key+"="+session.Values[key]) + } + return lines +} diff --git a/templates/layout_templ.go b/templates/layout_templ.go index 912e81a..71d1596 100644 --- a/templates/layout_templ.go +++ b/templates/layout_templ.go @@ -92,11 +92,11 @@ func Layout(title string, cssPath string, jsPath string, menu []pscatalog.MenuIt if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = LocalePicker("Market", locale.CurrentCountry, locale.Countries, true).Render(ctx, templ_7745c5c3_Buffer) + templ_7745c5c3_Err = LocalePicker("Market", locale.CurrentCountry, locale.Countries, true, false).Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = LocalePicker("Language", locale.CurrentLanguage, locale.Languages, true).Render(ctx, templ_7745c5c3_Buffer) + templ_7745c5c3_Err = LocalePicker("Language", locale.CurrentLanguage, locale.Languages, true, true).Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -147,7 +147,7 @@ func Layout(title string, cssPath string, jsPath string, menu []pscatalog.MenuIt }) } -func LocalePicker(title string, current pscatalog.LocaleOption, options []pscatalog.LocaleOption, navigable bool) templ.Component { +func LocalePicker(title string, current pscatalog.LocaleOption, options []pscatalog.LocaleOption, navigable bool, showMeta bool) templ.Component { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { @@ -197,7 +197,7 @@ func LocalePicker(title string, current pscatalog.LocaleOption, options []pscata if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - if current.Code != "" { + if showMeta && current.Code != "" { templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err @@ -292,7 +292,7 @@ func LocalePicker(title string, current pscatalog.LocaleOption, options []pscata if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - if option.Meta != "" { + if showMeta && option.Meta != "" { templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err @@ -310,7 +310,7 @@ func LocalePicker(title string, current pscatalog.LocaleOption, options []pscata if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - } else if option.Code != "" { + } else if showMeta && option.Code != "" { templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err @@ -369,7 +369,7 @@ func LocalePicker(title string, current pscatalog.LocaleOption, options []pscata if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - if option.Meta != "" { + if showMeta && option.Meta != "" { templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err diff --git a/templates/product.templ b/templates/product.templ index 52780a6..a380aff 100644 --- a/templates/product.templ +++ b/templates/product.templ @@ -52,6 +52,27 @@ templ ProductPage(data viewmodel.ProductPageData, cssPath string, jsPath string) + + if data.Session != nil { +
+

Go Cookie Debug

+
+
+

Raw Cookie

+
{ data.Session.RawCookie }
+
+
+

Decoded Values

+
+									for _, line := range sessionCookieLines(data.Session) {
+										{ line }
+										{"\n"}
+									}
+								
+
+
+
+ } } diff --git a/templates/product_templ.go b/templates/product_templ.go index b74b74c..4d21ef6 100644 --- a/templates/product_templ.go +++ b/templates/product_templ.go @@ -229,7 +229,58 @@ func ProductPage(data viewmodel.ProductPageData, cssPath string, jsPath string) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\">Account and login remain on PrestaShop") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\">Account and login remain on PrestaShop") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.Session != nil { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "

Go Cookie Debug

Raw Cookie

")
+				if templ_7745c5c3_Err != nil {
+					return templ_7745c5c3_Err
+				}
+				var templ_7745c5c3_Var14 string
+				templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(data.Session.RawCookie)
+				if templ_7745c5c3_Err != nil {
+					return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/product.templ`, Line: 62, Col: 127}
+				}
+				_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
+				if templ_7745c5c3_Err != nil {
+					return templ_7745c5c3_Err
+				}
+				templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "

Decoded Values

")
+				if templ_7745c5c3_Err != nil {
+					return templ_7745c5c3_Err
+				}
+				for _, line := range sessionCookieLines(data.Session) {
+					var templ_7745c5c3_Var15 string
+					templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(line)
+					if templ_7745c5c3_Err != nil {
+						return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/product.templ`, Line: 68, Col: 16}
+					}
+					_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
+					if templ_7745c5c3_Err != nil {
+						return templ_7745c5c3_Err
+					}
+					templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, " ")
+					if templ_7745c5c3_Err != nil {
+						return templ_7745c5c3_Err
+					}
+					var templ_7745c5c3_Var16 string
+					templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs("\n")
+					if templ_7745c5c3_Err != nil {
+						return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/product.templ`, Line: 69, Col: 15}
+					}
+					_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
+					if templ_7745c5c3_Err != nil {
+						return templ_7745c5c3_Err
+					}
+				}
+				templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/web/dist/app.css b/web/dist/app.css index 2b5f5e5..d94df41 100644 --- a/web/dist/app.css +++ b/web/dist/app.css @@ -1 +1 @@ -*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }/*! tailwindcss v3.4.19 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}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;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}:root{--cta-glow:0 0 0 0 transparent}body{font-family:IBM Plex Sans,Avenir Next,Segoe UI,sans-serif;background:radial-gradient(circle at top left,hsla(39,82%,69%,.24),transparent 28%),radial-gradient(circle at top right,rgba(157,217,210,.16),transparent 34%),linear-gradient(180deg,#fbfaf6,#f5efe3 55%,#f7f3ea);--tw-text-opacity:1;color:rgb(28 25 23/var(--tw-text-opacity,1))}h1,h2,h3{font-family:Cormorant Garamond,IBM Plex Sans,serif}.site-container{margin-left:auto;margin-right:auto;width:100%;max-width:80rem;padding-left:1rem;padding-right:1rem}@media (min-width:640px){.site-container{padding-left:1.25rem;padding-right:1.25rem}}@media (min-width:1024px){.site-container{padding-left:2rem;padding-right:2rem}}.site-header{position:sticky;top:0;z-index:40;--tw-backdrop-blur:blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.utility-bar{background:linear-gradient(90deg,rgba(20,33,61,.98),rgba(37,58,89,.94));border-bottom-width:1px;border-color:hsla(0,0%,100%,.1)}.header-locale,.utility-bar{--tw-text-opacity:1;color:rgb(245 245 244/var(--tw-text-opacity,1))}.header-locale{display:flex;width:100%;flex-wrap:wrap;align-items:center;justify-content:flex-start;gap:.5rem;font-size:.875rem;line-height:1.25rem}@media (min-width:640px){.header-locale{width:auto;justify-content:flex-end}}.locale-picker{position:relative;width:100%}@media (min-width:640px){.locale-picker{width:auto}}.locale-picker__summary{display:flex;width:100%;cursor:pointer;list-style-type:none;align-items:center;justify-content:space-between;gap:.5rem;border-radius:9999px;border-width:1px;border-color:hsla(0,0%,100%,0);padding:.375rem .75rem;--tw-text-opacity:1;color:rgb(245 245 244/var(--tw-text-opacity,1));transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.locale-picker__summary:hover{border-color:hsla(0,0%,100%,.2);background-color:hsla(0,0%,100%,.1);--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}@media (min-width:640px){.locale-picker__summary{width:auto;justify-content:flex-start}}.locale-picker[open] .locale-picker__summary{border-color:hsla(0,0%,100%,.2);background-color:hsla(0,0%,100%,.1);--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.locale-picker__summary::-webkit-details-marker{display:none}.locale-picker__value{font-weight:500}.locale-picker__chevron,.locale-picker__code,.locale-picker__item-meta{font-size:.74rem;text-transform:uppercase;letter-spacing:.16em;color:hsla(24,6%,83%,.8)}.locale-picker__panel{left:0;right:0;z-index:50;margin-top:.75rem;min-width:13rem;border-radius:1.5rem;border-width:1px;border-color:hsla(0,0%,100%,.7);background-color:hsla(0,0%,100%,.95);padding:.75rem;--tw-text-opacity:1;color:rgb(28 25 23/var(--tw-text-opacity,1));--tw-shadow:0 22px 48px rgba(20,33,61,.18);--tw-shadow-colored:0 22px 48px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);--tw-backdrop-blur:blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}@media (min-width:640px){.locale-picker__panel{position:absolute;left:auto;right:0}}.locale-picker__title{margin-bottom:.5rem;padding-left:.5rem;padding-right:.5rem;font-size:.68rem;font-weight:600;text-transform:uppercase;letter-spacing:.2em;--tw-text-opacity:1;color:rgb(168 162 158/var(--tw-text-opacity,1))}.main-nav{background:rgba(255,251,245,.82);border-bottom-width:1px;border-color:hsla(0,0%,100%,.6);--tw-shadow:0 10px 30px rgba(20,33,61,.08);--tw-shadow-colored:0 10px 30px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.header-bar{display:flex;flex-wrap:wrap;align-items:center;gap:1rem;padding-top:1rem;padding-bottom:1rem}@media (min-width:640px){.header-bar{gap:1.25rem;padding-top:1.25rem;padding-bottom:1.25rem}}@media (min-width:1024px){.header-bar{flex-wrap:nowrap}}.brand-mark{display:inline-flex;align-items:flex-end;gap:.125rem;font-size:2.25rem;line-height:2.5rem;font-weight:900;line-height:1;letter-spacing:-.025em;--tw-text-opacity:1;color:rgb(0 0 0/var(--tw-text-opacity,1));transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.3s}.brand-mark:hover{--tw-translate-y:-0.125rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@media (min-width:640px){.brand-mark{font-size:3rem;line-height:1}}.brand-mark__accent{--tw-text-opacity:1;color:rgb(245 158 11/var(--tw-text-opacity,1))}.menu-toggle{display:inline-flex;height:2.75rem;align-items:center;justify-content:center;border-radius:9999px;border-width:1px;border-color:hsla(24,6%,83%,.7);background-color:hsla(0,0%,100%,.8);padding-left:1.25rem;padding-right:1.25rem;font-size:.75rem;line-height:1rem;font-weight:600;text-transform:uppercase;letter-spacing:.28em;--tw-text-opacity:1;color:rgb(28 25 23/var(--tw-text-opacity,1));transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.menu-toggle:hover{--tw-border-opacity:1;border-color:rgb(245 158 11/var(--tw-border-opacity,1));--tw-text-opacity:1;color:rgb(217 119 6/var(--tw-text-opacity,1))}.menu-panel{order:9999;width:100%;border-radius:1.5rem;border-width:1px;border-color:hsla(0,0%,100%,.7);background-color:hsla(0,0%,100%,.95);padding:1rem;--tw-shadow:0 16px 34px rgba(20,33,61,.12);--tw-shadow-colored:0 16px 34px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);--tw-backdrop-blur:blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}@media (min-width:1024px){.menu-panel{order:0;border-radius:0;border-width:0;background-color:transparent;padding:0;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}}.desktop-nav{display:flex;align-items:center;justify-content:center;gap:2rem}@media (min-width:1280px){.desktop-nav{gap:3rem}}.desktop-nav__toggle{display:inline-flex;height:2rem;width:1.25rem;align-items:center;justify-content:center;font-size:.875rem;line-height:1.25rem;--tw-text-opacity:1;color:rgb(168 162 158/var(--tw-text-opacity,1));transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.desktop-nav__entry--open .desktop-nav__link,.desktop-nav__entry--open .desktop-nav__toggle,.desktop-nav__toggle:hover{--tw-text-opacity:1;color:rgb(217 119 6/var(--tw-text-opacity,1))}.mega-menu{background:linear-gradient(180deg,rgba(255,252,247,.98),hsla(40,67%,96%,.98));position:absolute;left:0;right:0;top:100%;z-index:50;border-top-width:1px;border-color:hsla(0,0%,100%,.7);--tw-shadow:0 28px 60px rgba(20,33,61,.16);--tw-shadow-colored:0 28px 60px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);--tw-backdrop-blur:blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.mega-menu__grid{margin-left:auto;margin-right:auto;width:100%;max-width:80rem;padding-left:1rem;padding-right:1rem}@media (min-width:640px){.mega-menu__grid{padding-left:1.25rem;padding-right:1.25rem}}@media (min-width:1024px){.mega-menu__grid{padding-left:2rem;padding-right:2rem}}.mega-menu__grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));-moz-column-gap:3rem;column-gap:3rem;row-gap:2.5rem;padding-top:2.5rem;padding-bottom:2.5rem}@media (min-width:1024px){.mega-menu__grid{grid-template-columns:repeat(4,minmax(0,1fr))}}@media (min-width:1280px){.mega-menu__grid{grid-template-columns:repeat(5,minmax(0,1fr))}}.mega-menu__heading{margin-bottom:1rem;display:inline-flex;font-size:.875rem;line-height:1.25rem;font-weight:600;text-transform:uppercase;letter-spacing:.12em;--tw-text-opacity:1;color:rgb(28 25 23/var(--tw-text-opacity,1));transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.mega-menu__heading:hover{--tw-text-opacity:1;color:rgb(217 119 6/var(--tw-text-opacity,1))}.mega-menu__link{display:inline-flex;font-size:1.02rem;line-height:1.75rem;--tw-text-opacity:1;color:rgb(68 64 60/var(--tw-text-opacity,1));transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.mega-menu__link:hover{--tw-translate-x:0.25rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));--tw-text-opacity:1;color:rgb(217 119 6/var(--tw-text-opacity,1))}.mega-menu__all{grid-column:1/-1;display:flex;align-items:flex-start;justify-content:flex-end;border-top-width:1px;--tw-border-opacity:1;border-color:rgb(231 229 228/var(--tw-border-opacity,1));padding-top:1.25rem}.mega-menu__all-link{display:inline-flex;font-size:.875rem;line-height:1.25rem;font-weight:600;text-transform:uppercase;letter-spacing:.18em;--tw-text-opacity:1;color:rgb(217 119 6/var(--tw-text-opacity,1));transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.mega-menu__all-link:hover{--tw-text-opacity:1;color:rgb(180 83 9/var(--tw-text-opacity,1))}.header-actions{margin-left:0;display:flex;align-items:center;gap:1rem;font-size:1.5rem;line-height:2rem;--tw-text-opacity:1;color:rgb(28 25 23/var(--tw-text-opacity,1))}@media (min-width:1024px){.header-actions{margin-left:auto}}.nav-icon{display:flex;height:2.75rem;width:2.75rem;align-items:center;justify-content:center;border-radius:9999px;background-color:hsla(0,0%,100%,.7);font-size:1.35rem;--tw-shadow:0 10px 24px rgba(20,33,61,.08);--tw-shadow-colored:0 10px 24px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.nav-icon:hover{--tw-translate-y:-0.125rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));--tw-text-opacity:1;color:rgb(217 119 6/var(--tw-text-opacity,1))}@media (max-width:1023px){.desktop-nav,.mega-menu{display:none}}button[type=submit]{box-shadow:var(--cta-glow)}.mx-auto{margin-right:auto}.ml-auto,.mx-auto{margin-left:auto}.mt-10{margin-top:2.5rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.mt-8{margin-top:2rem}.flex{display:flex}.inline-flex{display:inline-flex}.grid{display:grid}.hidden{display:none}.min-h-screen{min-height:100vh}.w-full{width:100%}.max-w-2xl{max-width:42rem}.max-w-6xl{max-width:72rem}.max-w-7xl{max-width:80rem}.max-w-none{max-width:none}.shrink-0{flex-shrink:0}.basis-full{flex-basis:100%}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.justify-between{justify-content:space-between}.gap-10{gap:2.5rem}.gap-12{gap:3rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-5{gap:1.25rem}.gap-6{gap:1.5rem}.gap-8{gap:2rem}.gap-x-6{-moz-column-gap:1.5rem;column-gap:1.5rem}.gap-y-2{row-gap:.5rem}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.25rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem*var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.75rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem*var(--tw-space-y-reverse))}.rounded-3xl{border-radius:1.5rem}.rounded-\[1\.75rem\]{border-radius:1.75rem}.rounded-\[2rem\]{border-radius:2rem}.rounded-full{border-radius:9999px}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-amber-500\/30{border-color:rgba(245,158,11,.3)}.border-emerald-400\/40{border-color:rgba(52,211,153,.4)}.border-emerald-500\/20{border-color:rgba(16,185,129,.2)}.border-stone-800{--tw-border-opacity:1;border-color:rgb(41 37 36/var(--tw-border-opacity,1))}.border-white\/10{border-color:hsla(0,0%,100%,.1)}.bg-amber-300{--tw-bg-opacity:1;background-color:rgb(252 211 77/var(--tw-bg-opacity,1))}.bg-amber-400\/10{background-color:rgba(251,191,36,.1)}.bg-slate-900\/70{background-color:rgba(15,23,42,.7)}.bg-stone-900\/70{background-color:rgba(28,25,23,.7)}.bg-white\/5{background-color:hsla(0,0%,100%,.05)}.bg-\[radial-gradient\(circle_at_top\2c _rgba\(245\2c 158\2c 11\2c 0\.28\)\2c _transparent_40\%\)\2c linear-gradient\(180deg\2c \#0c0a09\2c \#1c1917\)\]{background-image:radial-gradient(circle at top,rgba(245,158,11,.28),transparent 40%),linear-gradient(180deg,#0c0a09,#1c1917)}.bg-\[radial-gradient\(circle_at_top_left\2c _rgba\(34\2c 197\2c 94\2c 0\.18\)\2c _transparent_35\%\)\2c linear-gradient\(180deg\2c \#0b1020\2c \#111827\)\]{background-image:radial-gradient(circle at top left,rgba(34,197,94,.18),transparent 35%),linear-gradient(180deg,#0b1020,#111827)}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-10{padding-top:2.5rem;padding-bottom:2.5rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.pb-6{padding-bottom:1.5rem}.text-right{text-align:right}.font-serif{font-family:ui-serif,Georgia,Cambria,Times New Roman,Times,serif}.text-2xl{font-size:1.5rem;line-height:2rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-5xl{font-size:3rem;line-height:1}.text-\[0\.65rem\]{font-size:.65rem}.text-\[0\.92rem\]{font-size:.92rem}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xs{font-size:.75rem;line-height:1rem}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.leading-7{line-height:1.75rem}.leading-8{line-height:2rem}.tracking-\[0\.22em\]{letter-spacing:.22em}.tracking-\[0\.24em\]{letter-spacing:.24em}.tracking-\[0\.28em\]{letter-spacing:.28em}.tracking-\[0\.2em\]{letter-spacing:.2em}.tracking-\[0\.32em\]{letter-spacing:.32em}.text-amber-200{--tw-text-opacity:1;color:rgb(253 230 138/var(--tw-text-opacity,1))}.text-amber-300{--tw-text-opacity:1;color:rgb(252 211 77/var(--tw-text-opacity,1))}.text-amber-300\/80{color:rgba(252,211,77,.8)}.text-emerald-200{--tw-text-opacity:1;color:rgb(167 243 208/var(--tw-text-opacity,1))}.text-emerald-300{--tw-text-opacity:1;color:rgb(110 231 183/var(--tw-text-opacity,1))}.text-slate-300{--tw-text-opacity:1;color:rgb(203 213 225/var(--tw-text-opacity,1))}.text-stone-300{--tw-text-opacity:1;color:rgb(214 211 209/var(--tw-text-opacity,1))}.text-stone-400{--tw-text-opacity:1;color:rgb(168 162 158/var(--tw-text-opacity,1))}.text-stone-50{--tw-text-opacity:1;color:rgb(250 250 249/var(--tw-text-opacity,1))}.text-stone-500{--tw-text-opacity:1;color:rgb(120 113 108/var(--tw-text-opacity,1))}.text-stone-700{--tw-text-opacity:1;color:rgb(68 64 60/var(--tw-text-opacity,1))}.text-stone-900{--tw-text-opacity:1;color:rgb(28 25 23/var(--tw-text-opacity,1))}.text-stone-950{--tw-text-opacity:1;color:rgb(12 10 9/var(--tw-text-opacity,1))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.underline{text-decoration-line:underline}.underline-offset-4{text-underline-offset:4px}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.shadow-2xl{--tw-shadow:0 25px 50px -12px rgba(0,0,0,.25);--tw-shadow-colored:0 25px 50px -12px var(--tw-shadow-color)}.shadow-2xl,.shadow-\[0_24px_80px_rgba\(0\2c 0\2c 0\2c 0\.35\)\]{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-\[0_24px_80px_rgba\(0\2c 0\2c 0\2c 0\.35\)\]{--tw-shadow:0 24px 80px rgba(0,0,0,.35);--tw-shadow-colored:0 24px 80px var(--tw-shadow-color)}.shadow-amber-950\/20{--tw-shadow-color:rgba(69,26,3,.2);--tw-shadow:var(--tw-shadow-colored)}.backdrop-blur{--tw-backdrop-blur:blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.hover\:-translate-y-1:hover{--tw-translate-y:-0.25rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:border-emerald-400\/40:hover{border-color:rgba(52,211,153,.4)}.hover\:bg-amber-200:hover{--tw-bg-opacity:1;background-color:rgb(253 230 138/var(--tw-bg-opacity,1))}.hover\:bg-emerald-300:hover{--tw-bg-opacity:1;background-color:rgb(110 231 183/var(--tw-bg-opacity,1))}.hover\:text-amber-600:hover{--tw-text-opacity:1;color:rgb(217 119 6/var(--tw-text-opacity,1))}.hover\:text-slate-950:hover{--tw-text-opacity:1;color:rgb(2 6 23/var(--tw-text-opacity,1))}@media (min-width:768px){.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width:1024px){.lg\:ml-10{margin-left:2.5rem}.lg\:block{display:block}.lg\:flex{display:flex}.lg\:hidden{display:none}.lg\:min-w-0{min-width:0}.lg\:flex-1{flex:1 1 0%}.lg\:basis-auto{flex-basis:auto}.lg\:grid-cols-\[1\.1fr_0\.9fr\]{grid-template-columns:1.1fr .9fr}.lg\:items-center{align-items:center}.lg\:justify-center{justify-content:center}.lg\:px-8{padding-left:2rem;padding-right:2rem}}@media (min-width:1280px){.xl\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}} \ No newline at end of file +*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }/*! tailwindcss v3.4.19 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}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;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}:root{--cta-glow:0 0 0 0 transparent}body{font-family:IBM Plex Sans,Avenir Next,Segoe UI,sans-serif;background:radial-gradient(circle at top left,hsla(39,82%,69%,.24),transparent 28%),radial-gradient(circle at top right,rgba(157,217,210,.16),transparent 34%),linear-gradient(180deg,#fbfaf6,#f5efe3 55%,#f7f3ea);--tw-text-opacity:1;color:rgb(28 25 23/var(--tw-text-opacity,1))}h1,h2,h3{font-family:Cormorant Garamond,IBM Plex Sans,serif}.site-container{margin-left:auto;margin-right:auto;width:100%;max-width:80rem;padding-left:1rem;padding-right:1rem}@media (min-width:640px){.site-container{padding-left:1.25rem;padding-right:1.25rem}}@media (min-width:1024px){.site-container{padding-left:2rem;padding-right:2rem}}.site-header{position:sticky;top:0;z-index:40;--tw-backdrop-blur:blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.utility-bar{background:linear-gradient(90deg,rgba(20,33,61,.98),rgba(37,58,89,.94));border-bottom-width:1px;border-color:hsla(0,0%,100%,.1)}.header-locale,.utility-bar{--tw-text-opacity:1;color:rgb(245 245 244/var(--tw-text-opacity,1))}.header-locale{display:flex;width:100%;flex-wrap:wrap;align-items:center;justify-content:flex-start;gap:.5rem;font-size:.875rem;line-height:1.25rem}@media (min-width:640px){.header-locale{width:auto;justify-content:flex-end}}.locale-picker{position:relative;width:100%}@media (min-width:640px){.locale-picker{width:auto}}.locale-picker__summary{display:flex;width:100%;cursor:pointer;list-style-type:none;align-items:center;justify-content:space-between;gap:.5rem;border-radius:9999px;border-width:1px;border-color:hsla(0,0%,100%,0);padding:.375rem .75rem;--tw-text-opacity:1;color:rgb(245 245 244/var(--tw-text-opacity,1));transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.locale-picker__summary:hover{border-color:hsla(0,0%,100%,.2);background-color:hsla(0,0%,100%,.1);--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}@media (min-width:640px){.locale-picker__summary{width:auto;justify-content:flex-start}}.locale-picker[open] .locale-picker__summary{border-color:hsla(0,0%,100%,.2);background-color:hsla(0,0%,100%,.1);--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.locale-picker__summary::-webkit-details-marker{display:none}.locale-picker__value{font-weight:500}.locale-picker__chevron,.locale-picker__code,.locale-picker__item-meta{font-size:.74rem;text-transform:uppercase;letter-spacing:.16em;color:hsla(24,6%,83%,.8)}.locale-picker__panel{left:0;right:0;z-index:50;margin-top:.75rem;min-width:13rem;border-radius:1.5rem;border-width:1px;border-color:hsla(0,0%,100%,.7);background-color:hsla(0,0%,100%,.95);padding:.75rem;--tw-text-opacity:1;color:rgb(28 25 23/var(--tw-text-opacity,1));--tw-shadow:0 22px 48px rgba(20,33,61,.18);--tw-shadow-colored:0 22px 48px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);--tw-backdrop-blur:blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}@media (min-width:640px){.locale-picker__panel{position:absolute;left:auto;right:0}}.locale-picker__title{margin-bottom:.5rem;padding-left:.5rem;padding-right:.5rem;font-size:.68rem;font-weight:600;text-transform:uppercase;letter-spacing:.2em;--tw-text-opacity:1;color:rgb(168 162 158/var(--tw-text-opacity,1))}.main-nav{background:rgba(255,251,245,.82);border-bottom-width:1px;border-color:hsla(0,0%,100%,.6);--tw-shadow:0 10px 30px rgba(20,33,61,.08);--tw-shadow-colored:0 10px 30px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.header-bar{display:flex;flex-wrap:wrap;align-items:center;gap:1rem;padding-top:1rem;padding-bottom:1rem}@media (min-width:640px){.header-bar{gap:1.25rem;padding-top:1.25rem;padding-bottom:1.25rem}}@media (min-width:1024px){.header-bar{flex-wrap:nowrap}}.brand-mark{display:inline-flex;align-items:flex-end;gap:.125rem;font-size:2.25rem;line-height:2.5rem;font-weight:900;line-height:1;letter-spacing:-.025em;--tw-text-opacity:1;color:rgb(0 0 0/var(--tw-text-opacity,1));transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.3s}.brand-mark:hover{--tw-translate-y:-0.125rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@media (min-width:640px){.brand-mark{font-size:3rem;line-height:1}}.brand-mark__accent{--tw-text-opacity:1;color:rgb(245 158 11/var(--tw-text-opacity,1))}.menu-toggle{display:inline-flex;height:2.75rem;align-items:center;justify-content:center;border-radius:9999px;border-width:1px;border-color:hsla(24,6%,83%,.7);background-color:hsla(0,0%,100%,.8);padding-left:1.25rem;padding-right:1.25rem;font-size:.75rem;line-height:1rem;font-weight:600;text-transform:uppercase;letter-spacing:.28em;--tw-text-opacity:1;color:rgb(28 25 23/var(--tw-text-opacity,1));transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.menu-toggle:hover{--tw-border-opacity:1;border-color:rgb(245 158 11/var(--tw-border-opacity,1));--tw-text-opacity:1;color:rgb(217 119 6/var(--tw-text-opacity,1))}.menu-panel{order:9999;width:100%;border-radius:1.5rem;border-width:1px;border-color:hsla(0,0%,100%,.7);background-color:hsla(0,0%,100%,.95);padding:1rem;--tw-shadow:0 16px 34px rgba(20,33,61,.12);--tw-shadow-colored:0 16px 34px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);--tw-backdrop-blur:blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}@media (min-width:1024px){.menu-panel{order:0;border-radius:0;border-width:0;background-color:transparent;padding:0;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}}.desktop-nav{display:flex;align-items:center;justify-content:center;gap:2rem}@media (min-width:1280px){.desktop-nav{gap:3rem}}.desktop-nav__toggle{display:inline-flex;height:2rem;width:1.25rem;align-items:center;justify-content:center;font-size:.875rem;line-height:1.25rem;--tw-text-opacity:1;color:rgb(168 162 158/var(--tw-text-opacity,1));transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.desktop-nav__entry--open .desktop-nav__link,.desktop-nav__entry--open .desktop-nav__toggle,.desktop-nav__toggle:hover{--tw-text-opacity:1;color:rgb(217 119 6/var(--tw-text-opacity,1))}.mega-menu{background:linear-gradient(180deg,rgba(255,252,247,.98),hsla(40,67%,96%,.98));position:absolute;left:0;right:0;top:100%;z-index:50;border-top-width:1px;border-color:hsla(0,0%,100%,.7);--tw-shadow:0 28px 60px rgba(20,33,61,.16);--tw-shadow-colored:0 28px 60px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);--tw-backdrop-blur:blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.mega-menu__grid{margin-left:auto;margin-right:auto;width:100%;max-width:80rem;padding-left:1rem;padding-right:1rem}@media (min-width:640px){.mega-menu__grid{padding-left:1.25rem;padding-right:1.25rem}}@media (min-width:1024px){.mega-menu__grid{padding-left:2rem;padding-right:2rem}}.mega-menu__grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));-moz-column-gap:3rem;column-gap:3rem;row-gap:2.5rem;padding-top:2.5rem;padding-bottom:2.5rem}@media (min-width:1024px){.mega-menu__grid{grid-template-columns:repeat(4,minmax(0,1fr))}}@media (min-width:1280px){.mega-menu__grid{grid-template-columns:repeat(5,minmax(0,1fr))}}.mega-menu__heading{margin-bottom:1rem;display:inline-flex;font-size:.875rem;line-height:1.25rem;font-weight:600;text-transform:uppercase;letter-spacing:.12em;--tw-text-opacity:1;color:rgb(28 25 23/var(--tw-text-opacity,1));transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.mega-menu__heading:hover{--tw-text-opacity:1;color:rgb(217 119 6/var(--tw-text-opacity,1))}.mega-menu__link{display:inline-flex;font-size:1.02rem;line-height:1.75rem;--tw-text-opacity:1;color:rgb(68 64 60/var(--tw-text-opacity,1));transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.mega-menu__link:hover{--tw-translate-x:0.25rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));--tw-text-opacity:1;color:rgb(217 119 6/var(--tw-text-opacity,1))}.mega-menu__all{grid-column:1/-1;display:flex;align-items:flex-start;justify-content:flex-end;border-top-width:1px;--tw-border-opacity:1;border-color:rgb(231 229 228/var(--tw-border-opacity,1));padding-top:1.25rem}.mega-menu__all-link{display:inline-flex;font-size:.875rem;line-height:1.25rem;font-weight:600;text-transform:uppercase;letter-spacing:.18em;--tw-text-opacity:1;color:rgb(217 119 6/var(--tw-text-opacity,1));transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.mega-menu__all-link:hover{--tw-text-opacity:1;color:rgb(180 83 9/var(--tw-text-opacity,1))}.header-actions{margin-left:0;display:flex;align-items:center;gap:1rem;font-size:1.5rem;line-height:2rem;--tw-text-opacity:1;color:rgb(28 25 23/var(--tw-text-opacity,1))}@media (min-width:1024px){.header-actions{margin-left:auto}}.nav-icon{display:flex;height:2.75rem;width:2.75rem;align-items:center;justify-content:center;border-radius:9999px;background-color:hsla(0,0%,100%,.7);font-size:1.35rem;--tw-shadow:0 10px 24px rgba(20,33,61,.08);--tw-shadow-colored:0 10px 24px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.nav-icon:hover{--tw-translate-y:-0.125rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));--tw-text-opacity:1;color:rgb(217 119 6/var(--tw-text-opacity,1))}@media (max-width:1023px){.desktop-nav,.mega-menu{display:none}}button[type=submit]{box-shadow:var(--cta-glow)}.mx-auto{margin-right:auto}.ml-auto,.mx-auto{margin-left:auto}.mt-10{margin-top:2.5rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.mt-8{margin-top:2rem}.flex{display:flex}.inline-flex{display:inline-flex}.grid{display:grid}.hidden{display:none}.min-h-screen{min-height:100vh}.w-full{width:100%}.max-w-2xl{max-width:42rem}.max-w-6xl{max-width:72rem}.max-w-7xl{max-width:80rem}.max-w-none{max-width:none}.shrink-0{flex-shrink:0}.basis-full{flex-basis:100%}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.justify-between{justify-content:space-between}.gap-10{gap:2.5rem}.gap-12{gap:3rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-5{gap:1.25rem}.gap-6{gap:1.5rem}.gap-8{gap:2rem}.gap-x-6{-moz-column-gap:1.5rem;column-gap:1.5rem}.gap-y-2{row-gap:.5rem}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.25rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem*var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.75rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem*var(--tw-space-y-reverse))}.overflow-x-auto{overflow-x:auto}.rounded-2xl{border-radius:1rem}.rounded-3xl{border-radius:1.5rem}.rounded-\[1\.75rem\]{border-radius:1.75rem}.rounded-\[2rem\]{border-radius:2rem}.rounded-full{border-radius:9999px}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-amber-500\/30{border-color:rgba(245,158,11,.3)}.border-emerald-400\/40{border-color:rgba(52,211,153,.4)}.border-emerald-500\/20{border-color:rgba(16,185,129,.2)}.border-stone-800{--tw-border-opacity:1;border-color:rgb(41 37 36/var(--tw-border-opacity,1))}.border-white\/10{border-color:hsla(0,0%,100%,.1)}.bg-amber-300{--tw-bg-opacity:1;background-color:rgb(252 211 77/var(--tw-bg-opacity,1))}.bg-amber-400\/10{background-color:rgba(251,191,36,.1)}.bg-black\/30{background-color:rgba(0,0,0,.3)}.bg-slate-900\/70{background-color:rgba(15,23,42,.7)}.bg-slate-950\/70{background-color:rgba(2,6,23,.7)}.bg-stone-900\/70{background-color:rgba(28,25,23,.7)}.bg-stone-950\/80{background-color:rgba(12,10,9,.8)}.bg-white\/5{background-color:hsla(0,0%,100%,.05)}.bg-\[radial-gradient\(circle_at_top\2c _rgba\(245\2c 158\2c 11\2c 0\.28\)\2c _transparent_40\%\)\2c linear-gradient\(180deg\2c \#0c0a09\2c \#1c1917\)\]{background-image:radial-gradient(circle at top,rgba(245,158,11,.28),transparent 40%),linear-gradient(180deg,#0c0a09,#1c1917)}.bg-\[radial-gradient\(circle_at_top_left\2c _rgba\(34\2c 197\2c 94\2c 0\.18\)\2c _transparent_35\%\)\2c linear-gradient\(180deg\2c \#0b1020\2c \#111827\)\]{background-image:radial-gradient(circle at top left,rgba(34,197,94,.18),transparent 35%),linear-gradient(180deg,#0b1020,#111827)}.p-4{padding:1rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-10{padding-top:2.5rem;padding-bottom:2.5rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.pb-6{padding-bottom:1.5rem}.text-right{text-align:right}.font-serif{font-family:ui-serif,Georgia,Cambria,Times New Roman,Times,serif}.text-2xl{font-size:1.5rem;line-height:2rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-5xl{font-size:3rem;line-height:1}.text-\[0\.65rem\]{font-size:.65rem}.text-\[0\.92rem\]{font-size:.92rem}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xs{font-size:.75rem;line-height:1rem}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.leading-6{line-height:1.5rem}.leading-7{line-height:1.75rem}.leading-8{line-height:2rem}.tracking-\[0\.22em\]{letter-spacing:.22em}.tracking-\[0\.24em\]{letter-spacing:.24em}.tracking-\[0\.28em\]{letter-spacing:.28em}.tracking-\[0\.2em\]{letter-spacing:.2em}.tracking-\[0\.32em\]{letter-spacing:.32em}.text-amber-200{--tw-text-opacity:1;color:rgb(253 230 138/var(--tw-text-opacity,1))}.text-amber-300{--tw-text-opacity:1;color:rgb(252 211 77/var(--tw-text-opacity,1))}.text-amber-300\/80{color:rgba(252,211,77,.8)}.text-emerald-200{--tw-text-opacity:1;color:rgb(167 243 208/var(--tw-text-opacity,1))}.text-emerald-300{--tw-text-opacity:1;color:rgb(110 231 183/var(--tw-text-opacity,1))}.text-slate-300{--tw-text-opacity:1;color:rgb(203 213 225/var(--tw-text-opacity,1))}.text-stone-200{--tw-text-opacity:1;color:rgb(231 229 228/var(--tw-text-opacity,1))}.text-stone-300{--tw-text-opacity:1;color:rgb(214 211 209/var(--tw-text-opacity,1))}.text-stone-400{--tw-text-opacity:1;color:rgb(168 162 158/var(--tw-text-opacity,1))}.text-stone-50{--tw-text-opacity:1;color:rgb(250 250 249/var(--tw-text-opacity,1))}.text-stone-500{--tw-text-opacity:1;color:rgb(120 113 108/var(--tw-text-opacity,1))}.text-stone-700{--tw-text-opacity:1;color:rgb(68 64 60/var(--tw-text-opacity,1))}.text-stone-900{--tw-text-opacity:1;color:rgb(28 25 23/var(--tw-text-opacity,1))}.text-stone-950{--tw-text-opacity:1;color:rgb(12 10 9/var(--tw-text-opacity,1))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.underline{text-decoration-line:underline}.underline-offset-4{text-underline-offset:4px}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.shadow-2xl{--tw-shadow:0 25px 50px -12px rgba(0,0,0,.25);--tw-shadow-colored:0 25px 50px -12px var(--tw-shadow-color)}.shadow-2xl,.shadow-\[0_24px_80px_rgba\(0\2c 0\2c 0\2c 0\.35\)\]{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-\[0_24px_80px_rgba\(0\2c 0\2c 0\2c 0\.35\)\]{--tw-shadow:0 24px 80px rgba(0,0,0,.35);--tw-shadow-colored:0 24px 80px var(--tw-shadow-color)}.shadow-amber-950\/20{--tw-shadow-color:rgba(69,26,3,.2);--tw-shadow:var(--tw-shadow-colored)}.backdrop-blur{--tw-backdrop-blur:blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.hover\:-translate-y-1:hover{--tw-translate-y:-0.25rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:border-emerald-400\/40:hover{border-color:rgba(52,211,153,.4)}.hover\:bg-amber-200:hover{--tw-bg-opacity:1;background-color:rgb(253 230 138/var(--tw-bg-opacity,1))}.hover\:bg-emerald-300:hover{--tw-bg-opacity:1;background-color:rgb(110 231 183/var(--tw-bg-opacity,1))}.hover\:text-amber-600:hover{--tw-text-opacity:1;color:rgb(217 119 6/var(--tw-text-opacity,1))}.hover\:text-slate-950:hover{--tw-text-opacity:1;color:rgb(2 6 23/var(--tw-text-opacity,1))}@media (min-width:768px){.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width:1024px){.lg\:ml-10{margin-left:2.5rem}.lg\:block{display:block}.lg\:flex{display:flex}.lg\:hidden{display:none}.lg\:min-w-0{min-width:0}.lg\:flex-1{flex:1 1 0%}.lg\:basis-auto{flex-basis:auto}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-\[1\.1fr_0\.9fr\]{grid-template-columns:1.1fr .9fr}.lg\:items-center{align-items:center}.lg\:justify-center{justify-content:center}.lg\:px-8{padding-left:2rem;padding-right:2rem}}@media (min-width:1280px){.xl\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}} \ No newline at end of file