@cimplify/cli 0.6.10 → 0.6.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -861,7 +861,7 @@ export const brand: Brand = {
861
861
  businessId: "bus_currents_electronics",
862
862
  },
863
863
  };
864
- ` }, { "path": "lib/cimplify-loader.ts", "kind": "text", "content": 'import type { ImageLoader } from "next/image";\n\nimport { assetUrl, isCimplifyAsset } from "@cimplify/sdk";\n\nconst cdnBase = process.env.NEXT_PUBLIC_CIMPLIFY_CDN_URL?.trim() || undefined;\n\nconst cimplifyImageLoader: ImageLoader = ({ src, width, quality }) => {\n if (isCimplifyAsset(src, cdnBase)) {\n return assetUrl(src, {\n base: cdnBase,\n w: width,\n quality,\n format: "auto",\n });\n }\n return src;\n};\n\nexport default cimplifyImageLoader;\n' }, { "path": "lib/site.config.ts", "kind": "text", "content": "/**\n * Canonical absolute URL. The platform overwrites this at build time with the\n * storefront's primary domain; the default only applies to local builds. It's\n * per-deploy config, not per-request, so it stays a static constant \u2014 keeping\n * the root layout prerenderable under `cacheComponents`.\n */\nexport const SITE_URL = \"https://example.com\";\n" }, { "path": "lib/cart.ts", "kind": "text", "content": '"use client";\n\nimport { useCart } from "@cimplify/sdk/react";\n\n/**\n * Reactive cart count for the header pill. Subscribes to the SDK cart\n * state \u2014 updates immediately on add / remove / quantity change.\n */\nexport function useCartCount(): { count: number } {\n const { itemCount } = useCart();\n return { count: itemCount };\n}\n' }, { "path": "lib/site-url.ts", "kind": "text", "content": 'import { SITE_URL } from "./site.config";\n\n/** Canonical absolute URL for this storefront. */\nexport async function getSiteUrl(): Promise<string> {\n return SITE_URL;\n}\n' }, { "path": "metadata.json", "kind": "text", "content": '{\n "id": "retail",\n "name": "Retail",\n "tagline": "Variant-aware retail storefront with full product detail pages and instalment support.",\n "industry": "retail",\n "tags": ["retail", "electronics", "variants"],\n "stability": "stable",\n "schemaType": "Store",\n "mock": {\n "seedName": "retail",\n "seedBusinessId": "bus_currents_electronics"\n }\n}\n' }, { "path": "tsconfig.json", "kind": "text", "content": '{\n "compilerOptions": {\n "target": "ES2022",\n "lib": ["dom", "dom.iterable", "esnext"],\n "allowJs": false,\n "skipLibCheck": true,\n "strict": true,\n "noEmit": true,\n "esModuleInterop": true,\n "module": "esnext",\n "moduleResolution": "bundler",\n "resolveJsonModule": true,\n "isolatedModules": true,\n "jsx": "preserve",\n "incremental": true,\n "plugins": [{ "name": "next" }],\n "paths": {\n "@/*": ["./*"]\n }\n },\n "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts"],\n "exclude": ["node_modules"]\n}\n' }, { "path": "CLAUDE.md", "kind": "text", "content": "# CLAUDE.md\n\nThis project was scaffolded from a Cimplify storefront template (`cimplify init`).\n\n## Read these first\n\n- **`AGENTS.md`** at the project root \u2014 the file \u2194 `brand.X` field map for *this template's* industry, plus the architectural rules.\n- **`.claude/skills/cimplify-storefront/SKILL.md`** \u2014 the playbook for common tasks (rebrand, add a section, wire a Server Action, deploy, eject a component). Invoke it whenever you're asked to change content, palette, copy, or routing.\n\n## The two-line summary\n\n1. **Edit `lib/brand.ts`** for any visible string.\n2. **Edit `app/globals.css` `@theme` block** for any palette / radius / font change.\n\nThat covers ~95% of what merchants ask for. For anything else, follow the `cimplify-storefront` skill.\n\n## Don't do\n\n- Hardcode strings in pages or components.\n- Disable `cacheComponents: true` in `next.config.ts`.\n- Use `unstable_cache` \u2014 Next 16's canonical primitive is `'use cache'`.\n- Run `bun test` (Bun's `vi` shim is incomplete) \u2014 use `bun run test:run`.\n" }, { "path": "README.md", "kind": "text", "content": "# __STOREFRONT_NAME__\n\nA Cimplify storefront scaffolded from the **retail** template \u2014 Next.js 16 (App Router), React 19, Tailwind v4. Cool navy + electric blue palette, Inter + JetBrains Mono typography, modern electronics-store aesthetic.\n\n## Run\n\n```bash\nbun install\nbun dev\n```\n\nTwo things start in parallel:\n\n- `cimplify-mock --seed retail` \u2014 the Cimplify mock API on `http://127.0.0.1:8787`, seeded with Currents Electronics.\n- `next dev` \u2014 this storefront on `http://localhost:3000`.\n\nOpen the storefront in your browser. Edit `app/page.tsx` to start customising.\n\n## Structure\n\n```\napp/\n layout.tsx # root layout, fonts, providers, header/footer/modal\n page.tsx # home\n shop/page.tsx # full catalogue (SDK <CataloguePage/>)\n collections/[slug]/ # collection landing\n categories/[slug]/ # category landing\n cart/page.tsx # SDK <CartPage/>\n checkout/page.tsx # SDK <CheckoutPage/>\n orders/[id]/page.tsx # post-checkout thank-you\n about, faq, terms, privacy\n globals.css # Tailwind import + theme tokens\ncomponents/\n providers.tsx # CimplifyProvider client wrapper\n header.tsx, footer.tsx, hero.tsx\n store-product-card.tsx # SDK <ProductCard/> wired to URL-driven modal\n product-modal.tsx # ?product=<slug> deep-linkable modal\n collection-strip.tsx # horizontal product strip\n category-grid.tsx # SDK <CategoryGrid/> with router navigation\nlib/\n cart.ts # useCartCount() for the header pill\n```\n\n## Switch the seed\n\nThis template is wired to the `retail` seed. To preview a different industry without re-scaffolding:\n\n```bash\ncimplify-mock --seed restaurant # Mama's Kitchen\ncimplify-mock --seed services # Serene Spa\ncimplify-mock --seed grocery # FreshMart\n```\n\nFor a fresh scaffold with another design altogether:\n\n```bash\ncimplify init my-store --template bakery # warm food/pastry\ncimplify init my-store --template restaurant # coming soon\ncimplify init my-store --template services # coming soon\ncimplify init my-store --template grocery # coming soon\n```\n\n## Go live\n\n```diff\n# .env.local\n- NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev\n+ NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=<your tenant key>\n```\n\nDeploy with `cimplify deploy --prod` after linking the project. See [`cimplify` CLI docs](https://www.cimplify.dev/docs/cli). `next.config.ts` already whitelists the SDK image hosts under `images.remotePatterns`.\n" }, { "path": ".env.example", "kind": "text", "content": "# Your tenant public key. Get it from the desk's Developers tab.\n# `cpk_live_*` / `cpk_test_*` keys auto-route same-origin requests\n# to hosted Cimplify in both dev and prod. Anything else (`mock-dev`,\n# empty) falls back to the local `cimplify dev` mock.\nNEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev\n\n# Optional. Forces a fixed canonical URL for sitemap.xml, robots.txt,\n# OpenGraph, and llms.txt. Leave unset and the storefront derives the\n# canonical from the request `Host` header automatically.\n# NEXT_PUBLIC_SITE_URL=\n" }], "storefront-fashion": [{ "path": "vitest.config.ts", "kind": "text", "content": 'import { defineConfig } from "vitest/config";\n\nexport default defineConfig({\n test: {\n environment: "node",\n include: ["__tests__/**/*.test.ts"],\n globals: false,\n },\n});\n' }, { "path": "e2e/visual.spec.ts", "kind": "text", "content": 'import { test, expect } from "@playwright/test";\n\n/**\n * Visual regression on the storefront\'s key surfaces. Catches CSS / layout\n * drift that the contract harness can\'t see.\n *\n * Anti-flake notes:\n * - We disable animations (CSS transitions can leave half-frame snapshots).\n * - We mask images (Unsplash / CDN URLs occasionally render slight pixel\n * differences). The container layout is what we\'re regressing on, not\n * image bytes.\n * - Fonts are served by `next/font` and locked at build time, but if you\n * see flake on a fresh CI runner, `await page.evaluate(() => document.fonts.ready)`\n * before the snapshot.\n */\ntest.describe("visual regression", () => {\n test.beforeEach(async ({ page }) => {\n await page.addStyleTag({\n content: `*, *::before, *::after { animation: none !important; transition: none !important; }`,\n });\n });\n\n test("homepage", async ({ page }) => {\n await page.goto("/");\n await page.evaluate(() => document.fonts.ready);\n await expect(page).toHaveScreenshot("homepage.png", {\n fullPage: true,\n mask: [page.locator("img")],\n });\n });\n\n test("shop / catalogue", async ({ page }) => {\n await page.goto("/shop");\n await page.evaluate(() => document.fonts.ready);\n await expect(page).toHaveScreenshot("shop.png", {\n fullPage: true,\n mask: [page.locator("img")],\n });\n });\n\n test("cart drawer after adding a product", async ({ page }) => {\n await page.goto("/products/studio-tee-natural");\n await page.evaluate(() => document.fonts.ready);\n // Click whatever the storefront\'s primary Add-to-Cart button is.\n await page.getByRole("button", { name: /add to cart/i }).first().click();\n // Drawer auto-opens via CartDrawerProvider\'s openOnAdd watcher.\n await expect(page.getByRole("dialog", { name: /cart/i })).toBeVisible();\n await expect(page).toHaveScreenshot("cart-drawer.png", {\n mask: [page.locator("img")],\n });\n });\n});\n' }, { "path": "__tests__/cart-flow.test.ts", "kind": "text", "content": 'import { createCartFlowSuite } from "@cimplify/sdk/testing/suite";\nimport { expect } from "vitest";\nimport { brand } from "../lib/brand";\n\n/**\n * Cart flow suite \u2014 base + per-merchant assertions.\n *\n * The base suite (from `@cimplify/sdk/testing/suite`) exercises the\n * universal contract: empty cart, add, dedupe, remove, businessId\n * round-trip. The `extend` hook below adds Studio FRX-specific\n * invariants \u2014 copy this pattern when a merchant has unique business\n * rules that need their own coverage.\n *\n * \u2022 `getHandle()` returns the suite\'s current `TestClientHandle`\n * (per `it`) so the seed and session are already wired.\n * \u2022 `it` is the vitest API; cases land inside the suite\'s `describe`\n * so its `beforeEach`/`afterEach` hooks apply automatically.\n */\ncreateCartFlowSuite({\n seed: brand.mock.seed,\n businessId: brand.mock.businessId,\n extend: ({ getHandle, it }) => {\n it("Studio FRX prices everything in GHS", async () => {\n // Currency is brand-level (not per-product) \u2014 Product carries the price\n // string only. Catalog completeness here is just a sanity check.\n expect(brand.currency).toBe("GHS");\n const h = getHandle();\n const list = await h.client.catalogue.getProducts();\n if (!list.ok) throw list.error;\n const items = (list.value as unknown as { items?: unknown[] }).items ?? [];\n expect(items.length).toBeGreaterThan(0);\n });\n\n it("every apparel product covers the studio\'s core size run (S/M/L)", async () => {\n const h = getHandle();\n const list = await h.client.catalogue.getProducts();\n if (!list.ok) throw list.error;\n const items = (list.value as unknown as { items?: { id: string; name: string }[] }).items ?? [];\n\n // Apparel only \u2014 caps/socks/totes intentionally lack size variants.\n const apparel = items.filter((p) => /tee|hoodie|jacket|trouser|pant/i.test(p.name));\n expect(apparel.length).toBeGreaterThan(0);\n\n const required = new Set(["S", "M", "L"]);\n for (const product of apparel) {\n const variants = await h.client.catalogue.getVariants(product.id);\n if (!variants.ok) throw variants.error;\n const sizes = new Set(variants.value.map((v) => v.name).filter(Boolean));\n for (const size of required) {\n expect(sizes, `${product.name} missing size ${size}`).toContain(size);\n }\n }\n });\n },\n});\n' }, { "path": "__tests__/brand.test.ts", "kind": "text", "content": 'import { createBrandSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateBrandSuite({ brand });\n' }, { "path": "__tests__/contract.test.ts", "kind": "text", "content": 'import { createContractSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateContractSuite({ seed: brand.mock.seed });\n' }, { "path": "AGENTS.md", "kind": "text", "content": '# AGENTS.md \u2014 Fashion / streetwear storefront template\n\nIf you are an AI agent (Claude, Cursor, Aider, devin, \u2026) working on this storefront, **start here.**\n\n## TL;DR for rebranding\n\n1. **Edit `lib/brand.ts`** \u2014 every visible string lives here.\n2. **Edit `app/globals.css`** \u2014 `@theme { \u2026 }` for palette + radius + font references.\n3. **Edit `.env.local`** \u2014 `NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY` (optional: `NEXT_PUBLIC_SITE_URL`).\n\n## Aesthetic\n\nStreetwear / lookbook-led brands (Nike, Aritzia, Highsnobiety, drop culture):\n\n- **Anton display + Inter** \u2014 uppercase condensed display, clean body sans.\n- **High-contrast palette** \u2014 near-black foreground on near-white background, electric primary (default coral, `oklch(0.7 0.24 30)`).\n- **Sharp corners**: `0.125rem` \u2014 minimal rounding.\n- **Editorial full-bleed hero** with image gradient overlay.\n- **Block-letter header** \u2014 no logo mark, just the shortName in display type.\n- Schema.org `@type` is `Store`.\n\n## Page surface\n\n```\napp/\n page.tsx Multi-section home \u2014 full-bleed editorial hero,\n trust bar, category tiles, promo banner,\n "Just dropped" grid, brand strip, collections,\n studio-collective CTA, best sellers, newsletter\n shop/page.tsx SDK <CataloguePage/> with custom hero\n search/page.tsx Search\n collections/[slug]/page.tsx Collection landing (drops)\n categories/[slug]/page.tsx Category landing\n products/[slug]/page.tsx Full product detail page (Product JSON-LD)\n\n size-guide/page.tsx \u2B50 Fashion-specific: chest/length/shoulder + waist/hip/inseam\n tables, "how to measure" callout\n lookbook/page.tsx \u2B50 Fashion-specific: editorial multi-drop lookbook \u2014\n hero + 3-up tile galleries per drop, "Shop Drop X" CTAs\n\n cart/page.tsx, checkout/page.tsx, orders/[id]/page.tsx\n\n account/page.tsx <CimplifyAccount /> (iframe)\n account/orders/page.tsx <CimplifyAccount section="orders" />\n account/addresses/page.tsx <CimplifyAccount section="addresses" />\n account/settings/page.tsx <CimplifyAccount section="settings" />\n login/page.tsx, signup/page.tsx redirects \u2192 /account\n\n contact/page.tsx, track-order/page.tsx\n\n about/page.tsx, faq/page.tsx\n shipping/page.tsx (DHL Express worldwide), returns/page.tsx (30 days),\n accessibility/page.tsx, terms/page.tsx, privacy/page.tsx\n\n sitemap-page/page.tsx, sitemap.ts, robots.ts, llms.txt/route.ts, opensearch.xml/route.ts\n error.tsx, not-found.tsx\n```\n\n## File \u2194 brand-field map\n\n| File | Reads from `brand` |\n|---|---|\n| `app/layout.tsx` | identity, contact, socials, Store JSON-LD |\n| `app/page.tsx` | `brand.hero`, `brand.trustItems`, `brand.brandStrip`, `brand.promo`, `brand.tradeIn` (= studio-collective copy), `brand.newsletter` |\n| `app/about/page.tsx` | `brand.about` |\n| `app/faq/page.tsx` | `brand.faq` (sizing, drops, returns, customs) |\n| `app/shipping/page.tsx` | `brand.shipping` (worldwide DHL, customs) |\n| `app/returns/page.tsx` | `brand.returns` (30 days, free US/UK/EU) |\n| `app/accessibility/page.tsx` | `brand.accessibility` |\n| `app/terms/page.tsx`, `app/privacy/page.tsx` | `brand.terms`, `brand.privacy` |\n| `app/contact/page.tsx` | `brand.contactPage`, `brand.contact` |\n| `app/track-order/page.tsx` | `brand.trackOrder` |\n| `app/account/*/page.tsx` | `brand.account` (iframe owns UI) |\n| `app/products/[slug]/page.tsx` | `brand.name`, `brand.currency` (Product JSON-LD) |\n| `app/size-guide/page.tsx` | currently has its size charts inlined \u2014 hoist to `brand.sizeGuide` if you want agents to edit them |\n| `app/lookbook/page.tsx` | currently has the lookbook entries inlined (image URLs + drop names) \u2014 hoist to `brand.lookbook` for agent editing |\n| `app/llms.txt/route.ts` | `brand.llms`, contact, currency |\n| `components/header.tsx`, `footer.tsx` | `brand.header`, `brand.footer`, `brand.contact`, `brand.socials` |\n| `components/promo-banner.tsx`, `trade-in-cta.tsx`, `brand-marquee.tsx`, `trust-bar.tsx`, `newsletter.tsx` | corresponding optional sections in `brand` |\n\n## Fashion-specific notes\n\n- **Hero is full-bleed editorial.** Replace `HERO_FALLBACK_IMAGE` in `app/page.tsx` with the merchant\'s drop campaign shot for maximum impact.\n- **Drop badges and "limited run" copy** drive urgency. Keep `brand.hero.badge` and `brand.promo.badge` short and time-bounded.\n- **`brand.tradeIn`** is repurposed as the "Studio collective" / membership programme \u2014 three-step access flow.\n- **Product detail uses the page-driven model** (`/products/[slug]`) \u2014 fashion is a consideration purchase (sizing, fit, fabric details), not impulse.\n- **`brandStrip.headline` is `"Stocked at"`** with editorial publication wordmarks (Vogue, Highsnobiety, etc.) \u2014 typography-led, no logo licensing required.\n\n## Known TODOs\n\n- `app/size-guide/page.tsx` has its size charts inlined as constants. Move them to `brand.sizeGuide` (typed) so agents can edit per-merchant size charts in one file.\n- `app/lookbook/page.tsx` has its drop entries inlined. Move to `brand.lookbook` for agent editing \u2014 each drop becomes `{ drop, title, date, byline, hero, tiles[] }`.\n- Contact form + newsletter fake submits.\n\n## Mock seed\n\nWired to `--seed fashion` (Studio FRX apparel catalogue \u2014 14 products with size variants XS\u20132XL, Drop 04 + Best Sellers collections, full-bleed product imagery served from `static-tmp.cimplify.io`).\n\n## Customizing SDK components\n\nFor anything beyond `lib/brand.ts` + `app/globals.css`, you\'ll likely want to lean on the SDK\'s prebuilt components rather than reinvent. Particularly for **product customization** (variants, add-ons, bundles, composites, services with scheduling), the SDK already gets the price math, variant axis matching, and cart payload contracts right. Default to ejecting and restyling:\n\n```bash\ncimplify add variant-selector # copies into ./components/\ncimplify add cart-drawer\ncimplify add product-page\n```\n\nThen edit the local copy. **Don\'t change the cart payload shape** unless you\'re also touching the SDK\'s mock and the backend lens.\n\nIf you\'re considering a from-scratch rebuild of the customizer, read these SDK source files first to understand the contract you must reproduce:\n- `src/react/product-customizer.tsx` \u2014 top-level state machine + final `onAddToCart` payload assembly\n- `src/react/variant-selector.tsx` \u2014 multi-axis matching via `display_attributes`; default-variant init runs in a `useEffect` and is easy to break\n- `src/react/{add-on,bundle,composite}-selector.tsx` \u2014 group constraints, exclusivity rules, per-component variants\n- `src/mock/domain/cart/{index,pricing}.ts` \u2014 the contract: `computeUnitPrice`, `computeBundlePrice`, `computeCompositePrice` (4 modes), `computeLineKey`\n\nFull ejection + customization rules are in the SDK-level [`AGENTS.md`](../../AGENTS.md) under "Don\'t reinvent product customization".\n\n## Quick start\n\n```bash\nbun install\nbun dev\n```\n\nOpen <http://localhost:3000>.\n' }, { "path": "postcss.config.mjs", "kind": "text", "content": 'const config = {\n plugins: {\n "@tailwindcss/postcss": {},\n },\n};\n\nexport default config;\n' }, { "path": "components/trade-in-cta.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport { brand } from "@/lib/brand";\n\nexport function TradeInCta() {\n const t = brand.tradeIn;\n if (!t) return null;\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-14 sm:py-20">\n <div className="grid grid-cols-1 lg:grid-cols-2 gap-8 items-center bg-foreground text-background rounded-3xl p-8 sm:p-12 lg:p-14 overflow-hidden relative">\n <div className="absolute top-0 right-0 w-1/2 h-full opacity-[0.04] [background-image:linear-gradient(135deg,white_1px,transparent_1px)] [background-size:24px_24px] pointer-events-none" />\n <div>\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary-foreground/70 mb-3">\n {t.eyebrow}\n </p>\n <h2 className="text-[clamp(1.75rem,3.5vw,2.5rem)] font-bold m-0 mb-4 -tracking-[0.025em] leading-[1.1]">\n {t.title}\n </h2>\n <p className="text-background/75 leading-relaxed mb-6">{t.body}</p>\n <div className="flex flex-wrap items-center gap-3">\n <Link\n href={t.primaryCtaHref}\n className="inline-flex items-center gap-2 px-5 py-2.5 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors"\n >\n {t.primaryCtaLabel}\n <svg viewBox="0 0 12 12" className="w-3 h-3" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden>\n <path d="M3 6h7m0 0L7 3m3 3L7 9" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n </Link>\n <Link\n href={t.secondaryCtaHref}\n className="inline-flex items-center gap-2 px-5 py-2.5 rounded-md border border-background/25 hover:bg-background/10 transition-colors text-sm font-medium"\n >\n {t.secondaryCtaLabel}\n </Link>\n </div>\n </div>\n <div className="grid grid-cols-3 gap-3">\n {t.steps.map((s) => (\n <div\n key={s.step}\n className="rounded-2xl p-4 sm:p-5 bg-background/5 border border-background/10"\n >\n <p className="text-[10px] font-mono text-primary tabular-nums mb-3">{s.step}</p>\n <p className="text-[13px] sm:text-sm font-semibold mb-1.5 -tracking-[0.015em]">\n {s.title}\n </p>\n <p className="text-[11px] sm:text-xs text-background/65 leading-snug">{s.body}</p>\n </div>\n ))}\n </div>\n </div>\n </section>\n );\n}\n' }, { "path": "components/cart-pill.tsx", "kind": "text", "content": '"use client";\n\nimport { useCartDrawer } from "@cimplify/sdk/react";\nimport { useCartCount } from "@/lib/cart";\n\n/**\n * Cart pill \u2014 dynamic island. Reads the live cart count via the SDK and\n * opens the side cart drawer on click (instead of navigating to /cart).\n * Wrap in `<Suspense fallback={<CartPillSkeleton/>}>` so the cached\n * header chrome streams without blocking on the cart fetch.\n */\nexport function CartPill() {\n const { count } = useCartCount();\n const { open } = useCartDrawer();\n return (\n <button\n type="button"\n onClick={open}\n aria-label={`Open cart, ${count} ${count === 1 ? "item" : "items"}`}\n className="inline-flex items-center gap-1.5 px-4 py-2.5 sm:py-2 rounded-full bg-foreground text-background text-xs font-semibold tracking-wide transition-transform hover:scale-105 cursor-pointer"\n >\n Cart \xB7 {count}\n </button>\n );\n}\n\nexport function CartPillSkeleton() {\n return (\n <span\n aria-hidden\n className="inline-flex items-center gap-1.5 px-4 py-2.5 sm:py-2 rounded-full bg-foreground/80 text-background text-xs font-semibold tracking-wide"\n >\n Cart \xB7 \u2026\n </span>\n );\n}\n' }, { "path": "components/category-tiles.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport Image from "next/image";\nimport type { Category } from "@cimplify/sdk";\n\ninterface CategoryTilesProps {\n categories: Category[];\n}\n\nconst ICONS: Record<string, React.ReactNode> = {\n phones: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-7 h-7" aria-hidden>\n <rect x="6" y="2" width="12" height="20" rx="2.5" />\n <line x1="12" y1="18" x2="12" y2="18" strokeWidth="2.5" strokeLinecap="round" />\n </svg>\n ),\n laptops: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-7 h-7" aria-hidden>\n <rect x="3" y="4" width="18" height="12" rx="1.5" />\n <line x1="2" y1="20" x2="22" y2="20" strokeLinecap="round" />\n </svg>\n ),\n audio: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-7 h-7" aria-hidden>\n <path d="M3 14v-2a9 9 0 0 1 18 0v2" />\n <rect x="2" y="14" width="5" height="6" rx="1.5" />\n <rect x="17" y="14" width="5" height="6" rx="1.5" />\n </svg>\n ),\n accessories: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-7 h-7" aria-hidden>\n <rect x="2" y="6" width="14" height="10" rx="2" />\n <path d="M16 12h4l2 2v0l-2 2h-4" />\n </svg>\n ),\n gaming: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-7 h-7" aria-hidden>\n <path d="M5 7h14a3 3 0 0 1 3 3v4a3 3 0 0 1-3 3h-1l-2-3H8l-2 3H5a3 3 0 0 1-3-3v-4a3 3 0 0 1 3-3z" />\n <line x1="9" y1="11" x2="9" y2="13" strokeLinecap="round" />\n <line x1="8" y1="12" x2="10" y2="12" strokeLinecap="round" />\n <circle cx="15" cy="11" r="0.75" fill="currentColor" />\n <circle cx="17" cy="13" r="0.75" fill="currentColor" />\n </svg>\n ),\n};\n\nexport function CategoryTiles({ categories }: CategoryTilesProps) {\n if (categories.length === 0) return null;\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-14 sm:py-20">\n <div className="flex items-end justify-between gap-6 mb-8">\n <div>\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2">\n Shop the catalogue\n </p>\n <h2 className="text-[clamp(1.75rem,3vw,2.25rem)] font-bold -tracking-[0.025em]">\n Pick your category.\n </h2>\n </div>\n <Link\n href="/shop"\n className="text-sm font-semibold text-primary hover:underline whitespace-nowrap hidden sm:inline-flex items-center gap-1"\n >\n See everything\n <svg viewBox="0 0 12 12" className="w-3 h-3" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden>\n <path d="M3 6h7m0 0L7 3m3 3L7 9" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n </Link>\n </div>\n <div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3 sm:gap-4">\n {categories.map((c, i) => (\n <Link\n key={c.id}\n href={`/categories/${c.slug}`}\n className="group relative overflow-hidden rounded-2xl bg-card border border-border p-5 hover:border-primary hover:-translate-y-0.5 transition-all"\n >\n <div className="flex items-start justify-between mb-12">\n <div className="grid place-items-center w-10 h-10 rounded-lg bg-primary/10 text-primary">\n {ICONS[c.slug] ?? (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-7 h-7" aria-hidden>\n <rect x="3" y="3" width="18" height="18" rx="2" />\n </svg>\n )}\n </div>\n <span className="text-[10px] font-mono text-muted-foreground tabular-nums">\n {String(i + 1).padStart(2, "0")}\n </span>\n </div>\n <p className="text-base font-semibold mb-1 -tracking-[0.015em]">{c.name}</p>\n {c.product_count != null && (\n <p className="text-xs text-muted-foreground">\n {c.product_count} {c.product_count === 1 ? "product" : "products"}\n </p>\n )}\n <span className="absolute right-4 bottom-4 grid place-items-center w-7 h-7 rounded-full bg-foreground text-background opacity-0 group-hover:opacity-100 transition-opacity">\n <svg viewBox="0 0 12 12" className="w-3 h-3" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden>\n <path d="M3 6h7m0 0L7 3m3 3L7 9" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n </span>\n </Link>\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "components/promo-banner.tsx", "kind": "text", "content": `import Link from "next/link";
864
+ ` }, { "path": "lib/cimplify-loader.ts", "kind": "text", "content": 'import type { ImageLoader } from "next/image";\n\nimport { assetUrl, isCimplifyAsset } from "@cimplify/sdk";\n\nconst cdnBase = process.env.NEXT_PUBLIC_CIMPLIFY_CDN_URL?.trim() || undefined;\n\nconst cimplifyImageLoader: ImageLoader = ({ src, width, quality }) => {\n if (isCimplifyAsset(src, cdnBase)) {\n return assetUrl(src, {\n base: cdnBase,\n w: width,\n quality,\n format: "auto",\n });\n }\n return src;\n};\n\nexport default cimplifyImageLoader;\n' }, { "path": "lib/site.config.ts", "kind": "text", "content": "/**\n * Canonical absolute URL. The platform overwrites this at build time with the\n * storefront's primary domain; the default only applies to local builds. It's\n * per-deploy config, not per-request, so it stays a static constant \u2014 keeping\n * the root layout prerenderable under `cacheComponents`.\n */\nexport const SITE_URL = \"https://example.com\";\n" }, { "path": "lib/cart.ts", "kind": "text", "content": '"use client";\n\nimport { useCart } from "@cimplify/sdk/react";\n\n/**\n * Reactive cart count for the header pill. Subscribes to the SDK cart\n * state \u2014 updates immediately on add / remove / quantity change.\n */\nexport function useCartCount(): { count: number } {\n const { itemCount } = useCart();\n return { count: itemCount };\n}\n' }, { "path": "lib/site-url.ts", "kind": "text", "content": 'import { SITE_URL } from "./site.config";\n\n/** Canonical absolute URL for this storefront. */\nexport async function getSiteUrl(): Promise<string> {\n return SITE_URL;\n}\n' }, { "path": "metadata.json", "kind": "text", "content": '{\n "id": "retail",\n "name": "Retail",\n "tagline": "Variant-aware retail storefront with full product detail pages and instalment support.",\n "industry": "retail",\n "tags": ["retail", "electronics", "variants"],\n "stability": "stable",\n "schemaType": "Store",\n "mock": {\n "seedName": "retail",\n "seedBusinessId": "bus_currents_electronics"\n }\n}\n' }, { "path": "tsconfig.json", "kind": "text", "content": '{\n "compilerOptions": {\n "target": "ES2022",\n "lib": ["dom", "dom.iterable", "esnext"],\n "allowJs": false,\n "skipLibCheck": true,\n "strict": true,\n "noEmit": true,\n "esModuleInterop": true,\n "module": "esnext",\n "moduleResolution": "bundler",\n "resolveJsonModule": true,\n "isolatedModules": true,\n "jsx": "preserve",\n "incremental": true,\n "plugins": [{ "name": "next" }],\n "paths": {\n "@/*": ["./*"]\n }\n },\n "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts"],\n "exclude": ["node_modules"]\n}\n' }, { "path": "CLAUDE.md", "kind": "text", "content": "# CLAUDE.md\n\nThis project was scaffolded from a Cimplify storefront template (`cimplify init`).\n\n## Read these first\n\n- **`AGENTS.md`** at the project root \u2014 the file \u2194 `brand.X` field map for *this template's* industry, plus the architectural rules.\n- **`.claude/skills/cimplify-storefront/SKILL.md`** \u2014 the playbook for common tasks (rebrand, add a section, wire a Server Action, deploy, eject a component). Invoke it whenever you're asked to change content, palette, copy, or routing.\n\n## The two-line summary\n\n1. **Edit `lib/brand.ts`** for any visible string.\n2. **Edit `app/globals.css` `@theme` block** for any palette / radius / font change.\n\nThat covers ~95% of what merchants ask for. For anything else, follow the `cimplify-storefront` skill.\n\n## Don't do\n\n- Hardcode strings in pages or components.\n- Disable `cacheComponents: true` in `next.config.ts`.\n- Use `unstable_cache` \u2014 Next 16's canonical primitive is `'use cache'`.\n- Run `bun test` (Bun's `vi` shim is incomplete) \u2014 use `bun run test:run`.\n" }, { "path": "README.md", "kind": "text", "content": "# __STOREFRONT_NAME__\n\nA Cimplify storefront scaffolded from the **retail** template \u2014 Next.js 16 (App Router), React 19, Tailwind v4. Cool navy + electric blue palette, Inter + JetBrains Mono typography, modern electronics-store aesthetic.\n\n## Run\n\n```bash\nbun install\nbun dev\n```\n\nTwo things start in parallel:\n\n- `cimplify-mock --seed retail` \u2014 the Cimplify mock API on `http://127.0.0.1:8787`, seeded with Currents Electronics.\n- `next dev` \u2014 this storefront on `http://localhost:3000`.\n\nOpen the storefront in your browser. Edit `app/page.tsx` to start customising.\n\n## Structure\n\n```\napp/\n layout.tsx # root layout, fonts, providers, header/footer/modal\n page.tsx # home\n shop/page.tsx # full catalogue (SDK <CataloguePage/>)\n collections/[slug]/ # collection landing\n categories/[slug]/ # category landing\n cart/page.tsx # SDK <CartPage/>\n checkout/page.tsx # SDK <CheckoutPage/>\n orders/[id]/page.tsx # post-checkout thank-you\n about, faq, terms, privacy\n globals.css # Tailwind import + theme tokens\ncomponents/\n providers.tsx # CimplifyProvider client wrapper\n header.tsx, footer.tsx, hero.tsx\n store-product-card.tsx # SDK <ProductCard/> wired to URL-driven modal\n product-modal.tsx # ?product=<slug> deep-linkable modal\n collection-strip.tsx # horizontal product strip\n category-grid.tsx # SDK <CategoryGrid/> with router navigation\nlib/\n cart.ts # useCartCount() for the header pill\n```\n\n## Switch the seed\n\nThis template is wired to the `retail` seed. To preview a different industry without re-scaffolding:\n\n```bash\ncimplify-mock --seed restaurant # Mama's Kitchen\ncimplify-mock --seed services # Serene Spa\ncimplify-mock --seed grocery # FreshMart\n```\n\nFor a fresh scaffold with another design altogether:\n\n```bash\ncimplify init my-store --template bakery # warm food/pastry\ncimplify init my-store --template restaurant # coming soon\ncimplify init my-store --template services # coming soon\ncimplify init my-store --template grocery # coming soon\n```\n\n## Go live\n\n```diff\n# .env.local\n- NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev\n+ NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=<your tenant key>\n```\n\nDeploy with `cimplify deploy --prod` after linking the project. See [`cimplify` CLI docs](https://www.cimplify.dev/docs/cli). `next.config.ts` already whitelists the SDK image hosts under `images.remotePatterns`.\n" }, { "path": "bun.lock", "kind": "binary", "content": "ewogICJsb2NrZmlsZVZlcnNpb24iOiAxLAogICJjb25maWdWZXJzaW9uIjogMSwKICAid29ya3NwYWNlcyI6IHsKICAgICIiOiB7CiAgICAgICJuYW1lIjogIl9fU1RPUkVGUk9OVF9OQU1FX18iLAogICAgICAiZGVwZW5kZW5jaWVzIjogewogICAgICAgICJAY2ltcGxpZnkvc2RrIjogIl4wLjQ4LjIiLAogICAgICAgICJuZXh0IjogIl4xNi4yLjQiLAogICAgICAgICJyZWFjdCI6ICJeMTkuMC4wIiwKICAgICAgICAicmVhY3QtZG9tIjogIl4xOS4wLjAiLAogICAgICB9LAogICAgICAiZGV2RGVwZW5kZW5jaWVzIjogewogICAgICAgICJAdGFpbHdpbmRjc3MvcG9zdGNzcyI6ICJeNC4yLjQiLAogICAgICAgICJAdHlwZXMvbm9kZSI6ICJeMjIuMTAuMCIsCiAgICAgICAgIkB0eXBlcy9yZWFjdCI6ICJeMTkuMC4wIiwKICAgICAgICAiQHR5cGVzL3JlYWN0LWRvbSI6ICJeMTkuMC4wIiwKICAgICAgICAiY29uY3VycmVudGx5IjogIl45LjAuMCIsCiAgICAgICAgInRhaWx3aW5kY3NzIjogIl40LjIuNCIsCiAgICAgICAgInR5cGVzY3JpcHQiOiAiXjUuOS4zIiwKICAgICAgICAidml0ZXN0IjogIl40LjEuNSIsCiAgICAgIH0sCiAgICB9LAogIH0sCiAgInBhY2thZ2VzIjogewogICAgIkBhbGxvYy9xdWljay1scnUiOiBbIkBhbGxvYy9xdWljay1scnVANS4yLjAiLCAiIiwge30sICJzaGE1MTItVXJjQUJCKzRiVXJGQUJ3Ymx1VElCRXJYd3Zic1UvVjdUWldmbWJnSmZia3dpQnV6aVM5Z3hkT0RVeXVpZWNmZEdRODVqZ2xNVzZqdVMzK3o1VHNLTHc9PSJdLAoKICAgICJAYmFiZWwvcnVudGltZSI6IFsiQGJhYmVsL3J1bnRpbWVANy4yOS4yIiwgIiIsIHt9LCAic2hhNTEyLUppRFNoSDQ1ektIV3lHZTRaTlZSckNqQno4Tmg5VE1tWkcxa2g0UVRLOGhDQlRXQmk4RGEraTdzMWZKdzcvbFlwTTRjY2VwU05mcXpaL1F2QUJCaTVnPT0iXSwKCiAgICAiQGJhc2UtdWkvcmVhY3QiOiBbIkBiYXNlLXVpL3JlYWN0QDEuNS4wIiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIkBiYWJlbC9ydW50aW1lIjogIl43LjI5LjIiLCAiQGJhc2UtdWkvdXRpbHMiOiAiMC4yLjkiLCAiQGZsb2F0aW5nLXVpL3JlYWN0LWRvbSI6ICJeMi4xLjgiLCAiQGZsb2F0aW5nLXVpL3V0aWxzIjogIl4wLjIuMTEiLCAidXNlLXN5bmMtZXh0ZXJuYWwtc3RvcmUiOiAiXjEuNi4wIiB9LCAicGVlckRlcGVuZGVuY2llcyI6IHsgIkBkYXRlLWZucy90eiI6ICJeMS4yLjAiLCAiQHR5cGVzL3JlYWN0IjogIl4xNyB8fCBeMTggfHwgXjE5IiwgImRhdGUtZm5zIjogIl40LjAuMCIsICJyZWFjdCI6ICJeMTcgfHwgXjE4IHx8IF4xOSIsICJyZWFjdC1kb20iOiAiXjE3IHx8IF4xOCB8fCBeMTkiIH0sICJvcHRpb25hbFBlZXJzIjogWyJAZGF0ZS1mbnMvdHoiLCAiQHR5cGVzL3JlYWN0IiwgImRhdGUtZm5zIl0gfSwgInNoYTUxMi16MWdTQWxjZWQxeVkraU0rbUhERXRJa0Q4VUkzRWJzNTJNdUJQeHZWNmY1aFJ1dGsreHZDSC93dUI3aERxRHpLOUpHNUZvTXo1bmhycXRTczF3anQxQT09Il0sCgogICAgIkBiYXNlLXVpL3V0aWxzIjogWyJAYmFzZS11aS91dGlsc0AwLjIuOSIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAYmFiZWwvcnVudGltZSI6ICJeNy4yOS4yIiwgIkBmbG9hdGluZy11aS91dGlscyI6ICJeMC4yLjExIiwgInJlc2VsZWN0IjogIl41LjEuMSIsICJ1c2Utc3luYy1leHRlcm5hbC1zdG9yZSI6ICJeMS42LjAiIH0sICJwZWVyRGVwZW5kZW5jaWVzIjogeyAiQHR5cGVzL3JlYWN0IjogIl4xNyB8fCBeMTggfHwgXjE5IiwgInJlYWN0IjogIl4xNyB8fCBeMTggfHwgXjE5IiwgInJlYWN0LWRvbSI6ICJeMTcgfHwgXjE4IHx8IF4xOSIgfSwgIm9wdGlvbmFsUGVlcnMiOiBbIkB0eXBlcy9yZWFjdCJdIH0sICJzaGE1MTIteC9QRERDWXpvcVBwanJkeWIzVmN5eWxUSTJJalVYRXRZREdpNWZvaDdLc25tTkpJSWFWd0EyR0xnREgxZHBzMUdnWGlKYkE2MGhNK0F5dVRmUXpJdnc9PSJdLAoKICAgICJAY2ltcGxpZnkvc2RrIjogWyJAY2ltcGxpZnkvc2RrQDAuNDguMiIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAYmFzZS11aS9yZWFjdCI6ICJeMS40LjEiLCAiY2xzeCI6ICJeMi4xLjEiLCAiZGF0ZS1mbnMiOiAiXjQuMS4wIiwgImxpYnBob25lbnVtYmVyLWpzIjogIjEuMTIuNDEiLCAicmVhY3QtZGF5LXBpY2tlciI6ICJeOS4xNC4wIiwgInRhaWx3aW5kLW1lcmdlIjogIl4zLjUuMCIsICJ6b2QiOiAiXjQuNC4zIiB9LCAicGVlckRlcGVuZGVuY2llcyI6IHsgIkBwYXlzdGFjay9pbmxpbmUtanMiOiAiXjIuMjIuOCIsICJtc3ciOiAiPj0yLjAuMCIsICJyZWFjdCI6ICI+PTE3LjAuMCIsICJ2aXRlc3QiOiAiPj0yLjAuMCIgfSwgIm9wdGlvbmFsUGVlcnMiOiBbIkBwYXlzdGFjay9pbmxpbmUtanMiLCAibXN3IiwgInJlYWN0IiwgInZpdGVzdCJdLCAiYmluIjogeyAiY2ltcGxpZnktbW9jayI6ICJkaXN0L21vY2svY2xpLm1qcyIgfSB9LCAic2hhNTEyLTZJaDhNNkxZVkhDdU84c2JnZWc3VmdQTUJIWHZNK2NodFkzLzJWZkdWRWZFeU5UY0d4UzZlcDFOMDdNS21tTG5xQWNMZTZteFQwbVFiL2h0cldQZ1hBPT0iXSwKCiAgICAiQGRhdGUtZm5zL3R6IjogWyJAZGF0ZS1mbnMvdHpAMS40LjEiLCAiIiwge30sICJzaGE1MTItUDVMVU5odGJqNllmSTNpSmp3NUVMOWVVQUc2T2l0RDBXM2ZXUWNwUWpEUmMvUUlzTDB0Uk51TzFQY0R2UGNjV0wxZlNUWFhkRTFkcytsOTVEVi9PRkE9PSJdLAoKICAgICJAZW1uYXBpL2NvcmUiOiBbIkBlbW5hcGkvY29yZUAxLjEwLjAiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiQGVtbmFwaS93YXNpLXRocmVhZHMiOiAiMS4yLjEiLCAidHNsaWIiOiAiXjIuNC4wIiB9IH0sICJzaGE1MTIteXE2T2tKNHA4MkNBZlBsMHU5bVFlYlFIS1BKa1k3V3JJdWsyMDVjVFluWWUrazJaOFlCaDExRnJiUkcvSDZpaGlycWNhY09nbDJCSU84b3lNUUxlWHc9PSJdLAoKICAgICJAZW1uYXBpL3J1bnRpbWUiOiBbIkBlbW5hcGkvcnVudGltZUAxLjEwLjAiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAidHNsaWIiOiAiXjIuNC4wIiB9IH0sICJzaGE1MTItZXd2WWxrODZ4VW9HSTB6UVJOcS9tQysxNlIxUWVEbEtReTIxS2kzb1NZWE5nTGI0NUdWMVA2QTBNKy9zNm55Q3VORHFlNVZwYVk4NEJ6WEd3VmJ3RkE9PSJdLAoKICAgICJAZW1uYXBpL3dhc2ktdGhyZWFkcyI6IFsiQGVtbmFwaS93YXNpLXRocmVhZHNAMS4yLjEiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAidHNsaWIiOiAiXjIuNC4wIiB9IH0sICJzaGE1MTItdVRJSTdPWUYrL01lcy9NcmNJT1lwNXlPdFNNTEJXU0lvTFBwY2d3aXBvaUtibGk2azMyMnRjb0ZzeG9JSXhQRHFXMDFTUUdBZ2tvNEV6WmkyQk52Mnc9PSJdLAoKICAgICJAZmxvYXRpbmctdWkvY29yZSI6IFsiQGZsb2F0aW5nLXVpL2NvcmVAMS43LjUiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiQGZsb2F0aW5nLXVpL3V0aWxzIjogIl4wLjIuMTEiIH0gfSwgInNoYTUxMi0xSWg0V1RXeXcwK2xLeUZNY0JIR2JiNVU1RnR1SEp1dWpveXlyNXpUYVdTNUVZTWVUNkpiMkF1RGVmdHNDc0V1Y2hPK21NMmlqNStxOWNyaHlkekxoUT09Il0sCgogICAgIkBmbG9hdGluZy11aS9kb20iOiBbIkBmbG9hdGluZy11aS9kb21AMS43LjYiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiQGZsb2F0aW5nLXVpL2NvcmUiOiAiXjEuNy41IiwgIkBmbG9hdGluZy11aS91dGlscyI6ICJeMC4yLjExIiB9IH0sICJzaGE1MTItOWdaU0FJNVhNMzY4ODBQUE1tLy85ZGZpRW5nWW9DNkFtMml6RVMxRkY0MDZZRnNqdnlCTW1lSjJnNFNBanUzeFd3dHV5bk5SRkwyczloZ3hwTEk1U1E9PSJdLAoKICAgICJAZmxvYXRpbmctdWkvcmVhY3QtZG9tIjogWyJAZmxvYXRpbmctdWkvcmVhY3QtZG9tQDIuMS44IiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIkBmbG9hdGluZy11aS9kb20iOiAiXjEuNy42IiB9LCAicGVlckRlcGVuZGVuY2llcyI6IHsgInJlYWN0IjogIj49MTYuOC4wIiwgInJlYWN0LWRvbSI6ICI+PTE2LjguMCIgfSB9LCAic2hhNTEyLWNDNTJiSHdNL24vQ3hTODdGSDB5V2RuZ0VacmpkdExXL3FWcnVvNjhxZytwcks3WlE0WUdkdXQyR3lEVnBvR2VBWWUvaDg5OXJWZU9WbTZPaTQwazJBPT0iXSwKCiAgICAiQGZsb2F0aW5nLXVpL3V0aWxzIjogWyJAZmxvYXRpbmctdWkvdXRpbHNAMC4yLjExIiwgIiIsIHt9LCAic2hhNTEyLVJpQi95SWg3OHBjSXhsNmxMTUcwQ2dCWEFaMlkwZVZIcU1QWXVndSs5VTBBZVQ2WUJlaUpwZjdsYmRKTkl1Z0ZQNVNJandOUmdvNERoUjFReGkyNkdnPT0iXSwKCiAgICAiQGltZy9jb2xvdXIiOiBbIkBpbWcvY29sb3VyQDEuMS4wIiwgIiIsIHt9LCAic2hhNTEyLVRkNzZxN2o1N28vdExWZGdTNzQ2Y1lBUmZTeXhrOGlFZlJ4ZXdMOWg0T016WWhiVzRUQWNwcGwwbVQ0ZXlxWGRkaDZML2p3b003NW1vN2l4YS9wQ2VRPT0iXSwKCiAgICAiQGltZy9zaGFycC1kYXJ3aW4tYXJtNjQiOiBbIkBpbWcvc2hhcnAtZGFyd2luLWFybTY0QDAuMzQuNSIsICIiLCB7ICJvcHRpb25hbERlcGVuZGVuY2llcyI6IHsgIkBpbWcvc2hhcnAtbGlidmlwcy1kYXJ3aW4tYXJtNjQiOiAiMS4yLjQiIH0sICJvcyI6ICJkYXJ3aW4iLCAiY3B1IjogImFybTY0IiB9LCAic2hhNTEyLWltdFEzV01KWGJNWTRmeGIvTmRwNkhCVE5WdFdDVUkwV2RvYnloZUdmNSthZDZ4WDhWSURPOHUyeEU0cWMvZnIwOENLRy83ZERzZUZ0bjZNNmcvcjN3PT0iXSwKCiAgICAiQGltZy9zaGFycC1kYXJ3aW4teDY0IjogWyJAaW1nL3NoYXJwLWRhcndpbi14NjRAMC4zNC41IiwgIiIsIHsgIm9wdGlvbmFsRGVwZW5kZW5jaWVzIjogeyAiQGltZy9zaGFycC1saWJ2aXBzLWRhcndpbi14NjQiOiAiMS4yLjQiIH0sICJvcyI6ICJkYXJ3aW4iLCAiY3B1IjogIng2NCIgfSwgInNoYTUxMi1ZTkVGQUYvNEtRL1BlVzBOK3IrYVZWc29JWTAvcXh4aWtGMlNXZHArTlJrbU1CN3k5TEJaQVZxUTR5aEdDbS9IM0gyNzBPU3lrcW1RTUtMQmhCSkRFdz09Il0sCgogICAgIkBpbWcvc2hhcnAtbGlidmlwcy1kYXJ3aW4tYXJtNjQiOiBbIkBpbWcvc2hhcnAtbGlidmlwcy1kYXJ3aW4tYXJtNjRAMS4yLjQiLCAiIiwgeyAib3MiOiAiZGFyd2luIiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi16cWpqbzdSYXRGZkZvUDBNa1E1MWpmdUZaQm5WRTJwUmlheWRLSjFHL3JIWnZuc3JIQU9jUUFMSWk5c0E1Y281eGVuUWRUdWdDdnRiMWN1Zjc4VmY0Zz09Il0sCgogICAgIkBpbWcvc2hhcnAtbGlidmlwcy1kYXJ3aW4teDY0IjogWyJAaW1nL3NoYXJwLWxpYnZpcHMtZGFyd2luLXg2NEAxLjIuNCIsICIiLCB7ICJvcyI6ICJkYXJ3aW4iLCAiY3B1IjogIng2NCIgfSwgInNoYTUxMi0xSU9kNXhmVmhsR3dYK3pYdjJOOTNrMHlNT052VWxBTnlsYkp3MWVUYWg4Sy9KdHBpMTVLQytXU2lhWC9uQm1ibTJIeFJNMWdaMG5TZGpTc3JaYkdLZz09Il0sCgogICAgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC1hcm0iOiBbIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC1hcm1AMS4yLjQiLCAiIiwgeyAib3MiOiAibGludXgiLCAiY3B1IjogImFybSIgfSwgInNoYTUxMi1iRkk3eGNLRkVMZGlOQ1ZvdjhlNDRJYTR1MmJ5QStsM1h0c0FqK1E4dGZDd082QlE4aURvallkdm9QTXFzS0RrdW9PbytYNkhaQTBzMHExMUFOTVE4QT09Il0sCgogICAgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC1hcm02NCI6IFsiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4LWFybTY0QDEuMi40IiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi1leGNqWDhEZnNJY0oxMHgxS3pyNFJjV2UxZWRDOVBxdURSUlB4M1lWQ3ZRditVNXA3WWluMnMzMmZ0emlrWG9qYjFQSUZjLzlNdDI4L3kraVJrbGtydz09Il0sCgogICAgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC1wcGM2NCI6IFsiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4LXBwYzY0QDEuMi40IiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJwcGM2NCIgfSwgInNoYTUxMi1GTXV2R2lqTERZRzZsVytiL1V2eWlsVVd1NUF5dSszcjJkMVM4bm90aUdDSXlZVS83NmVpZzFVZk1ta1o3dndnT3J6S3psUWJGU3VRZmdtN0dZVVBwQT09Il0sCgogICAgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC1yaXNjdjY0IjogWyJAaW1nL3NoYXJwLWxpYnZpcHMtbGludXgtcmlzY3Y2NEAxLjIuNCIsICIiLCB7ICJvcyI6ICJsaW51eCIsICJjcHUiOiAibm9uZSIgfSwgInNoYTUxMi1vVkRiY1I0elVDMGNlODJ0ZXViU20reDZFVGl4dEtaQmgvcWJSRUlPY0kzY1VMekR5YjE4U3IvV2N5eDdOUlFlUXpPaUhUTmJaRkYxVXdQUzJzY3lHQT09Il0sCgogICAgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC1zMzkweCI6IFsiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4LXMzOTB4QDEuMi40IiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJzMzkweCIgfSwgInNoYTUxMi1xbXA5VnJ6Z1BnTW9HWnlQdnJRSHFrMDJ1eWpBMC9RclRPMjZUcWs2bDRaVjBNUFdJVzZMVGtxT0lvditKMXlFdTdNYkZRYURwd2R3SktoYkp2dVJ4UT09Il0sCgogICAgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC14NjQiOiBbIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC14NjRAMS4yLjQiLCAiIiwgeyAib3MiOiAibGludXgiLCAiY3B1IjogIng2NCIgfSwgInNoYTUxMi10SnhpaUxzbUhjOUF4MWJ6M29hT1lCVVJUWEdJUkRPREJxaHZlVkhvbnJISjkvK2s4OXFiTGwwYmNKbnMrZTR0NHJ2YU5CeGFFWnNGdFNmQWRxdVBydz09Il0sCgogICAgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eG11c2wtYXJtNjQiOiBbIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eG11c2wtYXJtNjRAMS4yLjQiLCAiIiwgeyAib3MiOiAibGludXgiLCAiY3B1IjogImFybTY0IiB9LCAic2hhNTEyLUZWUUh1d3gxSUl1Tm93OVFBYllVekorRW44S2NWbTlMazUrdUdVUUpIYVptTUVDWm1PbGl4OUhuSDduMVRSa1hNUzBwR3hJSm9rSVZCOVN1cVpHR1h3PT0iXSwKCiAgICAiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4bXVzbC14NjQiOiBbIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eG11c2wteDY0QDEuMi40IiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJ4NjQiIH0sICJzaGE1MTItK0xweUJrN0w0NFpJWHd6L1ZZZmdsYVgvb2t4ZXpFU2M2VXhEU295bzJLczZKeGM0WTdzR2pwZ1U5czRQTWdxZ2pqMWdaQ3lsVGllTmFtcUExTUY3RGc9PSJdLAoKICAgICJAaW1nL3NoYXJwLWxpbnV4LWFybSI6IFsiQGltZy9zaGFycC1saW51eC1hcm1AMC4zNC41IiwgIiIsIHsgIm9wdGlvbmFsRGVwZW5kZW5jaWVzIjogeyAiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4LWFybSI6ICIxLjIuNCIgfSwgIm9zIjogImxpbnV4IiwgImNwdSI6ICJhcm0iIH0sICJzaGE1MTItOWRMcXN2d3RnMXV1WEJHWktzeGVtOTU5NSt1anYwc0o2Vmk4d2NUQU5TRnB3Vi9HT05hdDVlQ2t6UW8vMU82elJJa2gwbS84KzVCanJScjdqRFVTWnc9PSJdLAoKICAgICJAaW1nL3NoYXJwLWxpbnV4LWFybTY0IjogWyJAaW1nL3NoYXJwLWxpbnV4LWFybTY0QDAuMzQuNSIsICIiLCB7ICJvcHRpb25hbERlcGVuZGVuY2llcyI6IHsgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC1hcm02NCI6ICIxLjIuNCIgfSwgIm9zIjogImxpbnV4IiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi1iS1F6YUpSWS9ia1BPWHlLeDVFVnVwN3FrYW9qRUNHNk5MWXN3Z2t0T1pqYVhlY1NBZUNXaVp3d2lGZjMvWStPMUhyYXVpRTNGVnNHeEZnOGMyNHJaZz09Il0sCgogICAgIkBpbWcvc2hhcnAtbGludXgtcHBjNjQiOiBbIkBpbWcvc2hhcnAtbGludXgtcHBjNjRAMC4zNC41IiwgIiIsIHsgIm9wdGlvbmFsRGVwZW5kZW5jaWVzIjogeyAiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4LXBwYzY0IjogIjEuMi40IiB9LCAib3MiOiAibGludXgiLCAiY3B1IjogInBwYzY0IiB9LCAic2hhNTEyLTd6em53TmFxVzZZdHNmckdHREE2QlJrSVNLQUFFMUpvMFFkcE5ZWE5NSHUyKzBkVHJQZmxUTE5rcGM4bDdNVVA1TTE2WkpjVXZ5c1ZXV3JNZWZacXVBPT0iXSwKCiAgICAiQGltZy9zaGFycC1saW51eC1yaXNjdjY0IjogWyJAaW1nL3NoYXJwLWxpbnV4LXJpc2N2NjRAMC4zNC41IiwgIiIsIHsgIm9wdGlvbmFsRGVwZW5kZW5jaWVzIjogeyAiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4LXJpc2N2NjQiOiAiMS4yLjQiIH0sICJvcyI6ICJsaW51eCIsICJjcHUiOiAibm9uZSIgfSwgInNoYTUxMi01MWdKdUxQVEthN3BpWVBhVnM4R21CeW83L1U3LzdUWk9xK2NuWEpJSFpLYXZJUkhBUDc3ZTNOMkhFbDNkZ2lxZEQvdzB5VWZpSm5JSTc3UHVEREZkdz09Il0sCgogICAgIkBpbWcvc2hhcnAtbGludXgtczM5MHgiOiBbIkBpbWcvc2hhcnAtbGludXgtczM5MHhAMC4zNC41IiwgIiIsIHsgIm9wdGlvbmFsRGVwZW5kZW5jaWVzIjogeyAiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4LXMzOTB4IjogIjEuMi40IiB9LCAib3MiOiAibGludXgiLCAiY3B1IjogInMzOTB4IiB9LCAic2hhNTEyLW5RdENrMFBkS2ZobzNlQzVNcmJRb2lnSjJnZDFDZ2RkVU1rYWJVaityQmV2czh0WjJjVUxPeDQ2RTdveVgrMDRXR2ZBQmdJd21NQzBWcWllVGlSNGpnPT0iXSwKCiAgICAiQGltZy9zaGFycC1saW51eC14NjQiOiBbIkBpbWcvc2hhcnAtbGludXgteDY0QDAuMzQuNSIsICIiLCB7ICJvcHRpb25hbERlcGVuZGVuY2llcyI6IHsgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC14NjQiOiAiMS4yLjQiIH0sICJvcyI6ICJsaW51eCIsICJjcHUiOiAieDY0IiB9LCAic2hhNTEyLU1FemQ4SFBLeFZ4VmVud0FhK0pSUHdFQzdRRmpvUFd1UzVOWm5CdDZCM3B1N0VHMkdlMGlkMW9MSFpwUEpkbjNPUUsrQlFEaXc5elN0aUhCVEpRUVFRPT0iXSwKCiAgICAiQGltZy9zaGFycC1saW51eG11c2wtYXJtNjQiOiBbIkBpbWcvc2hhcnAtbGludXhtdXNsLWFybTY0QDAuMzQuNSIsICIiLCB7ICJvcHRpb25hbERlcGVuZGVuY2llcyI6IHsgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eG11c2wtYXJtNjQiOiAiMS4yLjQiIH0sICJvcyI6ICJsaW51eCIsICJjcHUiOiAiYXJtNjQiIH0sICJzaGE1MTItZnBySlI2R3RSc010Nkt5ZnE0NElzQ2hWWmVHTjk3Z1REMzMxd2VSMWV4MWMxcnlwREVBQk42VG0yeGExd0U2bFliNURkRW5rMDNOWlBxQTdJZDIxeWc9PSJdLAoKICAgICJAaW1nL3NoYXJwLWxpbnV4bXVzbC14NjQiOiBbIkBpbWcvc2hhcnAtbGludXhtdXNsLXg2NEAwLjM0LjUiLCAiIiwgeyAib3B0aW9uYWxEZXBlbmRlbmNpZXMiOiB7ICJAaW1nL3NoYXJwLWxpYnZpcHMtbGludXhtdXNsLXg2NCI6ICIxLjIuNCIgfSwgIm9zIjogImxpbnV4IiwgImNwdSI6ICJ4NjQiIH0sICJzaGE1MTItSmc4d05UMU1Vekl2aEJGeFZpcXJFaFdER3pxeW1vM3NWN3o3WnNhV2JaTkRMWFJKWm9SR3JqdWxwNjBZWXRWNHdmWThWSUtjV2lkam9qbExjV3JkOFE9PSJdLAoKICAgICJAaW1nL3NoYXJwLXdhc20zMiI6IFsiQGltZy9zaGFycC13YXNtMzJAMC4zNC41IiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIkBlbW5hcGkvcnVudGltZSI6ICJeMS43LjAiIH0sICJjcHUiOiAibm9uZSIgfSwgInNoYTUxMi1PZFdURWlWa1kyUEh3cWtiQkk4ZnJGeFFRRmVrSGFTU2tVSUprd3pjbFdaZTY0TzFYNFVsVWpxcXFMYVBiVXBNT1FrNkZCdS9IdGxHWE5ibElzMGh1dz09Il0sCgogICAgIkBpbWcvc2hhcnAtd2luMzItYXJtNjQiOiBbIkBpbWcvc2hhcnAtd2luMzItYXJtNjRAMC4zNC41IiwgIiIsIHsgIm9zIjogIndpbjMyIiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi1XUTNBZ1dDV1lTYjJ5dCtJRzhtbkM2SmRrOVdoczdPMGd4cGhibHNMdmRoU3BTVHRtdTY5WkcxR2tiNk51dnhzTkFDd2lQVjZjTlNaTnp0MEtQc3c3Zz09Il0sCgogICAgIkBpbWcvc2hhcnAtd2luMzItaWEzMiI6IFsiQGltZy9zaGFycC13aW4zMi1pYTMyQDAuMzQuNSIsICIiLCB7ICJvcyI6ICJ3aW4zMiIsICJjcHUiOiAiaWEzMiIgfSwgInNoYTUxMi1GVjltLzdObWVDbVNIREQ1ajQrNHBOSThDcDNhVytKdkxvWGNUVW8wSXF5alNmQVpKOGRJVW1pangxcWFKc0lpVStIb3N3NnhNNUtpakFXUkpDU2dOZz09Il0sCgogICAgIkBpbWcvc2hhcnAtd2luMzIteDY0IjogWyJAaW1nL3NoYXJwLXdpbjMyLXg2NEAwLjM0LjUiLCAiIiwgeyAib3MiOiAid2luMzIiLCAiY3B1IjogIng2NCIgfSwgInNoYTUxMi0rMjlZTXNxWTIvOWVGRWlXOTNlcVdudUxjV2N1Zm93WGV3d1NOSVQ2VXdaZFVVQ3JNM29Gak1XSC9aNi9UTW1iNGhsRmVubWZBVmJwV2V1cDJqcnlDdz09Il0sCgogICAgIkBqcmlkZ2V3ZWxsL2dlbi1tYXBwaW5nIjogWyJAanJpZGdld2VsbC9nZW4tbWFwcGluZ0AwLjMuMTMiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiQGpyaWRnZXdlbGwvc291cmNlbWFwLWNvZGVjIjogIl4xLjUuMCIsICJAanJpZGdld2VsbC90cmFjZS1tYXBwaW5nIjogIl4wLjMuMjQiIH0gfSwgInNoYTUxMi0ya2t0LzduaUo2TWdFUHhGMGJZZFE2ZXRaYUErZlF2RGNMS2NraHkxeUlRT3phb0tqQkJqU2o2My9hTFZqWUUzcWhSdDVkdk0rdVV5ZkNnNlVLQ0JiQT09Il0sCgogICAgIkBqcmlkZ2V3ZWxsL3JlbWFwcGluZyI6IFsiQGpyaWRnZXdlbGwvcmVtYXBwaW5nQDIuMy41IiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIkBqcmlkZ2V3ZWxsL2dlbi1tYXBwaW5nIjogIl4wLjMuNSIsICJAanJpZGdld2VsbC90cmFjZS1tYXBwaW5nIjogIl4wLjMuMjQiIH0gfSwgInNoYTUxMi1MSTl1LytsYVlHNERzMVRES1NKVzJZUHJJbGNWWU93aTJmVUM2eEI0M2x1ZUNqZ3hWNGxmZk9DWkN0WUZpSDZUTk9YK3RRS1h4OTdUNElLSGJoeUhFUT09Il0sCgogICAgIkBqcmlkZ2V3ZWxsL3Jlc29sdmUtdXJpIjogWyJAanJpZGdld2VsbC9yZXNvbHZlLXVyaUAzLjEuMiIsICIiLCB7fSwgInNoYTUxMi1iUklTZ0NJalAyMC90YldTUFdNRWk1NFFWUFJaRXhrdUQ5bEpMK1VJeFVLdHdWSkE4d1cxVHJiMWpNczFSRlhvMUNCVE5aLzVocEM5UXZtS1dkb3BLdz09Il0sCgogICAgIkBqcmlkZ2V3ZWxsL3NvdXJjZW1hcC1jb2RlYyI6IFsiQGpyaWRnZXdlbGwvc291cmNlbWFwLWNvZGVjQDEuNS41IiwgIiIsIHt9LCAic2hhNTEyLWNZUTkzMTBncnF4dWVXYmwrV3VJVUlhaVVhRGNqN1dPcTVmVmhFbGpOVmdSZk9VaFk5ZnkyelR2Zm9xV3NuZWJoOFNsNzBWU2NGYklDdkpuTEtCME9nPT0iXSwKCiAgICAiQGpyaWRnZXdlbGwvdHJhY2UtbWFwcGluZyI6IFsiQGpyaWRnZXdlbGwvdHJhY2UtbWFwcGluZ0AwLjMuMzEiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiQGpyaWRnZXdlbGwvcmVzb2x2ZS11cmkiOiAiXjMuMS4wIiwgIkBqcmlkZ2V3ZWxsL3NvdXJjZW1hcC1jb2RlYyI6ICJeMS40LjE0IiB9IH0sICJzaGE1MTItenpOUitTZFFTREp6Yzhqb2FlUDhRUW9DUXI4TnVZeDJkSUl5dGwxUWVCRVpISjl1VzZoZWJzcllnYno4aEp3VVFhbzNUV0NNdG1mVjhOdTF0d09MQXc9PSJdLAoKICAgICJAbmFwaS1ycy93YXNtLXJ1bnRpbWUiOiBbIkBuYXBpLXJzL3dhc20tcnVudGltZUAxLjEuNCIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAdHlieXMvd2FzbS11dGlsIjogIl4wLjEwLjEiIH0sICJwZWVyRGVwZW5kZW5jaWVzIjogeyAiQGVtbmFwaS9jb3JlIjogIl4xLjcuMSIsICJAZW1uYXBpL3J1bnRpbWUiOiAiXjEuNy4xIiB9IH0sICJzaGE1MTItM05RTk5nQTFZU2xKYi9rTUgxaWxkQVNQOUhXNy83a1luUkkyc3pXSmFvZmFTMWhXbWJHSTRIK2QzKzIyYUd6WFhOOUlKK24rR2lGVmNHaXBKUDE4b3c9PSJdLAoKICAgICJAbmV4dC9lbnYiOiBbIkBuZXh0L2VudkAxNi4yLjYiLCAiIiwge30sICJzaGE1MTItZ2Q4SG9ITjR1Zmo3M1dtUjNKbVZvbHJwSlI0N0lMSzZMb3VQNXhFbFBnbGFWeGlyNmUxYTdWenZUdkRXa09vUFhUOXJra1R6eUN4QnU0eWVaZlp3Y3c9PSJdLAoKICAgICJAbmV4dC9zd2MtZGFyd2luLWFybTY0IjogWyJAbmV4dC9zd2MtZGFyd2luLWFybTY0QDE2LjIuNiIsICIiLCB7ICJvcyI6ICJkYXJ3aW4iLCAiY3B1IjogImFybTY0IiB9LCAic2hhNTEyLVpKR2trY05mWWdyck1rcU9kWjd6b0xhMVRPeTBxcGNNZmsvejRNaC9GS1V6NDBnVk8rSE5RV3FtTHhmNjdaNVdCNjREUnAwZGhFYnlIZmVsKzZzSlVnPT0iXSwKCiAgICAiQG5leHQvc3djLWRhcndpbi14NjQiOiBbIkBuZXh0L3N3Yy1kYXJ3aW4teDY0QDE2LjIuNiIsICIiLCB7ICJvcyI6ICJkYXJ3aW4iLCAiY3B1IjogIng2NCIgfSwgInNoYTUxMi12L1lMQkhJWTEzMkNlZDNwdUJKN1lKS3cxbHFzQ3JnY05vMmFSSmxDRXlRcnJDZVJKbHZHbG5teGhQeE5RSTNLRTNOMURONXI5VFBOUHZrYTNucTVSUT09Il0sCgogICAgIkBuZXh0L3N3Yy1saW51eC1hcm02NC1nbnUiOiBbIkBuZXh0L3N3Yy1saW51eC1hcm02NC1nbnVAMTYuMi42IiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi1SUE92cWxZQmJjUWprejlWUVFEWjJUMmJBUklqWFpWMUtGbHQrVjJNcjZTVy9lNEk5ZmNLc2FBMGhkeWYyRkhvVGxzVjJ4bkJkNVk5MTJyUC8xQ2U2dz09Il0sCgogICAgIkBuZXh0L3N3Yy1saW51eC1hcm02NC1tdXNsIjogWyJAbmV4dC9zd2MtbGludXgtYXJtNjQtbXVzbEAxNi4yLjYiLCAiIiwgeyAib3MiOiAibGludXgiLCAiY3B1IjogImFybTY0IiB9LCAic2hhNTEyLVVSVVR1MStkTWt4SnNQRmdtK09lRXZxOXdmNXN1ancwRXZnWXk4MFRER0hUU0xUbklIZXFiMEV1OEEzc0M5NUlSZ2plalFMK2tDNG13KzR5UHhpQVhBPT0iXSwKCiAgICAiQG5leHQvc3djLWxpbnV4LXg2NC1nbnUiOiBbIkBuZXh0L3N3Yy1saW51eC14NjQtZ251QDE2LjIuNiIsICIiLCB7ICJvcyI6ICJsaW51eCIsICJjcHUiOiAieDY0IiB9LCAic2hhNTEyLURPajE4Mm1QVjhHM1VrcmF5TG9SRU01WUVZSStEazV3djdPeDl4bDFmRmliQUVMRXNGRDBsRFBmSEllSUxsdXRNTWZkeWhsellQRUxHM3BldUthdXJ3PT0iXSwKCiAgICAiQG5leHQvc3djLWxpbnV4LXg2NC1tdXNsIjogWyJAbmV4dC9zd2MtbGludXgteDY0LW11c2xAMTYuMi42IiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJ4NjQiIH0sICJzaGE1MTItSEtRNVNQL1YvdWI3M1V2RjduL3plSmx4azJrTG10TDdXenJnNFdmbWtqbU5vczVvbkoydEt1N3laT1BkTDE4QTZTdmZuM21heDI5eW0rcnk3TmtLNGc9PSJdLAoKICAgICJAbmV4dC9zd2Mtd2luMzItYXJtNjQtbXN2YyI6IFsiQG5leHQvc3djLXdpbjMyLWFybTY0LW1zdmNAMTYuMi42IiwgIiIsIHsgIm9zIjogIndpbjMyIiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi1MWlhwVGxQeVM1djdIaFNtbnZzTEdQM2lJWWdZT0JuYzhyOEFybFQ1NXNHSFY4OWJSMkhsRGRCaldRK1BZNlNKTW1rOFR1VkdGdXhhbG5QM2svMER3Zz09Il0sCgogICAgIkBuZXh0L3N3Yy13aW4zMi14NjQtbXN2YyI6IFsiQG5leHQvc3djLXdpbjMyLXg2NC1tc3ZjQDE2LjIuNiIsICIiLCB7ICJvcyI6ICJ3aW4zMiIsICJjcHUiOiAieDY0IiB9LCAic2hhNTEyLUYwKzRpMGg5SjZDNGVFM0VBUFdzb0NrN1VXL2Riek9qeXp4WTBxbkRVT1lGdTZGRm1kWjZsOTcvWGRWMy9OejNWWXlPN1VXanlFSlVYa0dxY29YZk1BPT0iXSwKCiAgICAiQG94Yy1wcm9qZWN0L3R5cGVzIjogWyJAb3hjLXByb2plY3QvdHlwZXNAMC4xMzAuMCIsICIiLCB7fSwgInNoYTUxMi1pYkQydXN4OUpSdTdmNXB1MnRNS01JNGNwQTROZ1hKUW9ZUlA0cFE3UHhtbjFsNmsvNTNxV3RRV1pheWhZeTNYNFFaa3Q5ME90K21KRWFlWG91aW82UT09Il0sCgogICAgIkByb2xsZG93bi9iaW5kaW5nLWFuZHJvaWQtYXJtNjQiOiBbIkByb2xsZG93bi9iaW5kaW5nLWFuZHJvaWQtYXJtNjRAMS4wLjEiLCAiIiwgeyAib3MiOiAiYW5kcm9pZCIsICJjcHUiOiAiYXJtNjQiIH0sICJzaGE1MTItZkpJM0kwcjNDM09qL3pkQkNwYUNtQlJaWWYwN3hwYXE0eUNmRERvU0ZtK2JlV056YklsMjZwdVc4UnJhVWR1Z29Kdy85NXplck5PbjZqYXNBaHpTbWc9PSJdLAoKICAgICJAcm9sbGRvd24vYmluZGluZy1kYXJ3aW4tYXJtNjQiOiBbIkByb2xsZG93bi9iaW5kaW5nLWRhcndpbi1hcm02NEAxLjAuMSIsICIiLCB7ICJvcyI6ICJkYXJ3aW4iLCAiY3B1IjogImFybTY0IiB9LCAic2hhNTEyLWNLbkFoV0VzVjdUUGNBLzVFQXRlRHA2S2NKWkJRMkcrQnFFN3pheU1NaTdrTXZ3UnNidjdXVDlhT25uMFdObDRTS0VJZjQzdmpTMzFpVVB1ODBuelhnPT0iXSwKCiAgICAiQHJvbGxkb3duL2JpbmRpbmctZGFyd2luLXg2NCI6IFsiQHJvbGxkb3duL2JpbmRpbmctZGFyd2luLXg2NEAxLjAuMSIsICIiLCB7ICJvcyI6ICJkYXJ3aW4iLCAiY3B1IjogIng2NCIgfSwgInNoYTUxMi1ZS3JWd1FqSVJCUG8rNUcvdTAzd0dqYmR5NHE3cHl6Q2U5M0RLOVZKN3prVm1lZzhMSjdHYmdzaUhXZFI0eFNvZTRDQVhSRDdCY2pnYnRyNjRia1hOZz09Il0sCgogICAgIkByb2xsZG93bi9iaW5kaW5nLWZyZWVic2QteDY0IjogWyJAcm9sbGRvd24vYmluZGluZy1mcmVlYnNkLXg2NEAxLjAuMSIsICIiLCB7ICJvcyI6ICJmcmVlYnNkIiwgImNwdSI6ICJ4NjQiIH0sICJzaGE1MTItei9vQnNSRW80NlNzRnFCd1l0RmUwa3BKZUJpakFUNDhPL1dYTEk0c3VpQ0xCa3IwM1JUdFRKTUN6U2REZDJ6bmxoOFZKaXpMMDlYVmtRZ2s4SVpvbnc9PSJdLAoKICAgICJAcm9sbGRvd24vYmluZGluZy1saW51eC1hcm0tZ251ZWFiaWhmIjogWyJAcm9sbGRvd24vYmluZGluZy1saW51eC1hcm0tZ251ZWFiaWhmQDEuMC4xIiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJhcm0iIH0sICJzaGE1MTItaWs4cTdHTTExenh2WXhGYzJQZURjVDZUQnZoQ1FNYVV4ZnBoL001bDlzS3VUcy9TamczTCtCeXcwRjd3MFpWTEJabXgzMFArZ0cwRUN6ek4rTUZjbVE9PSJdLAoKICAgICJAcm9sbGRvd24vYmluZGluZy1saW51eC1hcm02NC1nbnUiOiBbIkByb2xsZG93bi9iaW5kaW5nLWxpbnV4LWFybTY0LWdudUAxLjAuMSIsICIiLCB7ICJvcyI6ICJsaW51eCIsICJjcHUiOiAiYXJtNjQiIH0sICJzaGE1MTItUW9TeDJFa3lycmRaNmtjeUU4c3RxWjYydDBZcmE4RnM1aWE5bE94SnJoNlRNUUpLN2dRS21zY2RUSGY3cE9YS1JFS3JWd090SmNRRzNxVlNmYzg2NkE9PSJdLAoKICAgICJAcm9sbGRvd24vYmluZGluZy1saW51eC1hcm02NC1tdXNsIjogWyJAcm9sbGRvd24vYmluZGluZy1saW51eC1hcm02NC1tdXNsQDEuMC4xIiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi11d053RnB3S2VOaVphd2ZBV0JnZzBWSXp0UFRWM2loaGgxdlYzMzRoOWl2bk5Mb3J4blFNVTZGejh3RzFaYjRRaDlMQzEvTWtjeVQzWWxEWEczUnNnZz09Il0sCgogICAgIkByb2xsZG93bi9iaW5kaW5nLWxpbnV4LXBwYzY0LWdudSI6IFsiQHJvbGxkb3duL2JpbmRpbmctbGludXgtcHBjNjQtZ251QDEuMC4xIiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJwcGM2NCIgfSwgInNoYTUxMi16WTFidWw3T1dyN0RGQmlKKyt3b2ZYdm5yOEI0NWNlM1FzUVVoS3JJaFhzeWdBaDdiVGt3eWVNMWJpMWEyZzVDL3lDL044VFp5R0RFb01mbS9sOW1wZz09Il0sCgogICAgIkByb2xsZG93bi9iaW5kaW5nLWxpbnV4LXMzOTB4LWdudSI6IFsiQHJvbGxkb3duL2JpbmRpbmctbGludXgtczM5MHgtZ251QDEuMC4xIiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJzMzkweCIgfSwgInNoYTUxMi0wZnJsc1QvZjRGdDZJN1NNRVNUS25GM2Nac2RpY1FuMWRDTWtGL2pUOXdETEUrZ0dvaVFmdjFubVQ5ZStzN3MvZmVrdnZ5NnRaTTJqSHZJMnRrYkpEUT09Il0sCgogICAgIkByb2xsZG93bi9iaW5kaW5nLWxpbnV4LXg2NC1nbnUiOiBbIkByb2xsZG93bi9iaW5kaW5nLWxpbnV4LXg2NC1nbnVAMS4wLjEiLCAiIiwgeyAib3MiOiAibGludXgiLCAiY3B1IjogIng2NCIgfSwgInNoYTUxMi1YQUJWbUdwOVRnMFdzcFRWdndkdVRjNGZwcXk2Sm5BVXJTUWU2T3V5cUQvMDNuSTdyME85T1dVa01Jd0ZyaktBSXFvbHZxb0E0WnJKcHBnd0UwR3htdz09Il0sCgogICAgIkByb2xsZG93bi9iaW5kaW5nLWxpbnV4LXg2NC1tdXNsIjogWyJAcm9sbGRvd24vYmluZGluZy1saW51eC14NjQtbXVzbEAxLjAuMSIsICIiLCB7ICJvcyI6ICJsaW51eCIsICJjcHUiOiAieDY0IiB9LCAic2hhNTEyLWJWNGZ6c3d1elZjS0Q5MG8vVk02UXFLeG54bERxMGcyQklTRExOVm14cm5ocHYxRERieVBoQ0lqWWZ2ellMVitNdmtLS25RdDJRNkFPODZTRUJVTFVRPT0iXSwKCiAgICAiQHJvbGxkb3duL2JpbmRpbmctb3Blbmhhcm1vbnktYXJtNjQiOiBbIkByb2xsZG93bi9iaW5kaW5nLW9wZW5oYXJtb255LWFybTY0QDEuMC4xIiwgIiIsIHsgIm9zIjogIm5vbmUiLCAiY3B1IjogImFybTY0IiB9LCAic2hhNTEyLS9NaDBaaHEzT1A3ZlZzMGtjUUhaUDZsWkV0aE1HVGFTZjhVQlFZU0ZFWkRXR1hYbEVDK25KNkVxZW5hSzJ0NExCWE1lM0ErSy9HMkJWWFhkdE9yNFBRPT0iXSwKCiAgICAiQHJvbGxkb3duL2JpbmRpbmctd2FzbTMyLXdhc2kiOiBbIkByb2xsZG93bi9iaW5kaW5nLXdhc20zMi13YXNpQDEuMC4xIiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIkBlbW5hcGkvY29yZSI6ICIxLjEwLjAiLCAiQGVtbmFwaS9ydW50aW1lIjogIjEuMTAuMCIsICJAbmFwaS1ycy93YXNtLXJ1bnRpbWUiOiAiXjEuMS40IiB9LCAiY3B1IjogIm5vbmUiIH0sICJzaGE1MTItKzF4YzlYNDVsOHVmc0JBbTZHanZ4MnFEUklZOWxUVnQwY2dXTmNKKzFnZGhYdmtieGVQQTYweVJUd1NUdVhMMDlDTWh5Sm1qcFY3RTNOb3l4YnFGUVE9PSJdLAoKICAgICJAcm9sbGRvd24vYmluZGluZy13aW4zMi1hcm02NC1tc3ZjIjogWyJAcm9sbGRvd24vYmluZGluZy13aW4zMi1hcm02NC1tc3ZjQDEuMC4xIiwgIiIsIHsgIm9zIjogIndpbjMyIiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi0xRCtVcVpkZm51UitKeTFHZ01Kd2k4NWJENDBIMjF1Tm1PUFJXUWh3NG9SU3VvbFovQjVyaXhaNDVESzJLWE9UQ3ZtVkNlY2F1V2dFaGJ3OGJJN3RPdz09Il0sCgogICAgIkByb2xsZG93bi9iaW5kaW5nLXdpbjMyLXg2NC1tc3ZjIjogWyJAcm9sbGRvd24vYmluZGluZy13aW4zMi14NjQtbXN2Y0AxLjAuMSIsICIiLCB7ICJvcyI6ICJ3aW4zMiIsICJjcHUiOiAieDY0IiB9LCAic2hhNTEyLUlOQXljYVd1aGxPSzN3azRtUkhHc2Rnd1lXbWQ5Y0NoZFBkRTlid1dteTZybjlWcVZOWU5GR2hPZFhyb2ZYVXh3SEluY1NpUE5iOHRObThrbkRWSWVRPT0iXSwKCiAgICAiQHJvbGxkb3duL3BsdWdpbnV0aWxzIjogWyJAcm9sbGRvd24vcGx1Z2ludXRpbHNAMS4wLjEiLCAiIiwge30sICJzaGE1MTItMmo5Ykd0NUpoOGhqK3ZQdGd6UHRsNzJqMHlSeEhBeXVtb282VE5mQWpzTEIwNFV0cFN2UGJQY0RjQk14ejduKzlDWUIwYzFHeFFGeFlSZzJqaW1xR3c9PSJdLAoKICAgICJAc3RhbmRhcmQtc2NoZW1hL3NwZWMiOiBbIkBzdGFuZGFyZC1zY2hlbWEvc3BlY0AxLjEuMCIsICIiLCB7fSwgInNoYTUxMi1sMmFGeTVqQUxobmlHNUhncXJENmpYTGkvclVXckt2cU4vcUp4NnlvSnNnS2hibFZkK2lxcVU0UkNYYXZtL2pQaXR5RG81VEN2S01ucGpLbk9yaXkwdz09Il0sCgogICAgIkBzd2MvaGVscGVycyI6IFsiQHN3Yy9oZWxwZXJzQDAuNS4xNSIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJ0c2xpYiI6ICJeMi44LjAiIH0gfSwgInNoYTUxMi1KUTVUdU1pNDVPd2k0L0JJTUFKQm9TUW9PSnUxMm9Pay9nQURxbGNVTDlKRWRIQjh2eWpVU3N4cWVOWG5tWEhqWUtNaTJXY1l0ZXpHRUVocVVJL0UyZz09Il0sCgogICAgIkB0YWJieV9haS9oaWpyaS1jb252ZXJ0ZXIiOiBbIkB0YWJieV9haS9oaWpyaS1jb252ZXJ0ZXJAMS4wLjUiLCAiIiwge30sICJzaGE1MTItcjViQ2xLcmNJdXNEb28wNDlkU0w4Q2F3bkhSNm1SZER3aGxRdUlnWlJOdHk2OHEweDhrM0xmMUJ0UEFNeFJmL0dnbkhCbklPNHVqZDMrR1FkTFd6eFE9PSJdLAoKICAgICJAdGFpbHdpbmRjc3Mvbm9kZSI6IFsiQHRhaWx3aW5kY3NzL25vZGVANC4zLjAiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiQGpyaWRnZXdlbGwvcmVtYXBwaW5nIjogIl4yLjMuNSIsICJlbmhhbmNlZC1yZXNvbHZlIjogIl41LjIxLjAiLCAiaml0aSI6ICJeMi42LjEiLCAibGlnaHRuaW5nY3NzIjogIjEuMzIuMCIsICJtYWdpYy1zdHJpbmciOiAiXjAuMzAuMjEiLCAic291cmNlLW1hcC1qcyI6ICJeMS4yLjEiLCAidGFpbHdpbmRjc3MiOiAiNC4zLjAiIH0gfSwgInNoYTUxMi1hRmI0Z1VoRk9nZGg5QVhvNEl6QkVPekJra0F4bTlWaWd3REpuTUlZdjNsY2ZYQ0pWZXNOZmJFYUJsNEJOZ1ZSeWlkOTJBbWR2aXF3QlVCUktTZVkzZz09Il0sCgogICAgIkB0YWlsd2luZGNzcy9veGlkZSI6IFsiQHRhaWx3aW5kY3NzL294aWRlQDQuMy4wIiwgIiIsIHsgIm9wdGlvbmFsRGVwZW5kZW5jaWVzIjogeyAiQHRhaWx3aW5kY3NzL294aWRlLWFuZHJvaWQtYXJtNjQiOiAiNC4zLjAiLCAiQHRhaWx3aW5kY3NzL294aWRlLWRhcndpbi1hcm02NCI6ICI0LjMuMCIsICJAdGFpbHdpbmRjc3Mvb3hpZGUtZGFyd2luLXg2NCI6ICI0LjMuMCIsICJAdGFpbHdpbmRjc3Mvb3hpZGUtZnJlZWJzZC14NjQiOiAiNC4zLjAiLCAiQHRhaWx3aW5kY3NzL294aWRlLWxpbnV4LWFybS1nbnVlYWJpaGYiOiAiNC4zLjAiLCAiQHRhaWx3aW5kY3NzL294aWRlLWxpbnV4LWFybTY0LWdudSI6ICI0LjMuMCIsICJAdGFpbHdpbmRjc3Mvb3hpZGUtbGludXgtYXJtNjQtbXVzbCI6ICI0LjMuMCIsICJAdGFpbHdpbmRjc3Mvb3hpZGUtbGludXgteDY0LWdudSI6ICI0LjMuMCIsICJAdGFpbHdpbmRjc3Mvb3hpZGUtbGludXgteDY0LW11c2wiOiAiNC4zLjAiLCAiQHRhaWx3aW5kY3NzL294aWRlLXdhc20zMi13YXNpIjogIjQuMy4wIiwgIkB0YWlsd2luZGNzcy9veGlkZS13aW4zMi1hcm02NC1tc3ZjIjogIjQuMy4wIiwgIkB0YWlsd2luZGNzcy9veGlkZS13aW4zMi14NjQtbXN2YyI6ICI0LjMuMCIgfSB9LCAic2hhNTEyLUY3SFpHQmVOOUkwL0F1dUpTNVB3Y0Q4eGF5eDVyaTVHaGpZVURCRVZZVWtleHlBL2dpd2JETmpSVnJ4U2V6RTNUMjUwT1UySy93cC9sdFd4M1VPZWZnPT0iXSwKCiAgICAiQHRhaWx3aW5kY3NzL294aWRlLWFuZHJvaWQtYXJtNjQiOiBbIkB0YWlsd2luZGNzcy9veGlkZS1hbmRyb2lkLWFybTY0QDQuMy4wIiwgIiIsIHsgIm9zIjogImFuZHJvaWQiLCAiY3B1IjogImFybTY0IiB9LCAic2hhNTEyLVRKUGlxNjd0S2xMdU9iUDZSa3d2VkdEb3hDTUJWdERnS2tMZmEvdXlqNy9GeXh2UXdIUytVT25WclhYZ2JFc2ZVYU1naVZ2QzRLYkpuUnIyNmhvNE5nPT0iXSwKCiAgICAiQHRhaWx3aW5kY3NzL294aWRlLWRhcndpbi1hcm02NCI6IFsiQHRhaWx3aW5kY3NzL294aWRlLWRhcndpbi1hcm02NEA0LjMuMCIsICIiLCB7ICJvcyI6ICJkYXJ3aW4iLCAiY3B1IjogImFybTY0IiB9LCAic2hhNTEyLW9NTi9XWlJiK1NPMzdCbVVFbEVnZUVXdVU4RS9IWFJraU9EeEp4TGUxVVRIVlhMcmRWU2dmYUpWN3BTbGhSR01TT2lYTHV4VElqZnNGM3dZdno4Y2dRPT0iXSwKCiAgICAiQHRhaWx3aW5kY3NzL294aWRlLWRhcndpbi14NjQiOiBbIkB0YWlsd2luZGNzcy9veGlkZS1kYXJ3aW4teDY0QDQuMy4wIiwgIiIsIHsgIm9zIjogImRhcndpbiIsICJjcHUiOiAieDY0IiB9LCAic2hhNTEyLU42Q1VtdTRhNmJLVkFEZnc3N3AraXc2WWQ5UTNPQmhlMHZlYURYK1FhemZ1VllsUXNIZkRneEJyc2pRL0lXK3p5d0w4bVRyTmQwU2RKVC96Z3R2TWRBPT0iXSwKCiAgICAiQHRhaWx3aW5kY3NzL294aWRlLWZyZWVic2QteDY0IjogWyJAdGFpbHdpbmRjc3Mvb3hpZGUtZnJlZWJzZC14NjRANC4zLjAiLCAiIiwgeyAib3MiOiAiZnJlZWJzZCIsICJjcHUiOiAieDY0IiB9LCAic2hhNTEyLXpETDVoQmtRZEg1QzZNcHFiSzNnUUFnUDgwdHNNd1NJMjZ2ak96akp0TkNNVW8wbEZnT0l0ekhLQkl1cE9aTlF4dDNvdVBIN1JQaHZOaGlUZkNlNUNRPT0iXSwKCiAgICAiQHRhaWx3aW5kY3NzL294aWRlLWxpbnV4LWFybS1nbnVlYWJpaGYiOiBbIkB0YWlsd2luZGNzcy9veGlkZS1saW51eC1hcm0tZ251ZWFiaWhmQDQuMy4wIiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJhcm0iIH0sICJzaGE1MTItUjA2SGROaTdBN09Fb01zZjZkNHRqWjcxUkNXblpRUEhqMm1ub3RTRlVSak5MZEJDK2NJZ1hRN2w4MUNxZW9pUWZ0amY2T09ibHhYTUluTWdOMlZ6TUE9PSJdLAoKICAgICJAdGFpbHdpbmRjc3Mvb3hpZGUtbGludXgtYXJtNjQtZ251IjogWyJAdGFpbHdpbmRjc3Mvb3hpZGUtbGludXgtYXJtNjQtZ251QDQuMy4wIiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi1xVEpIRUxYOGpldGpoUlFIQ0xpbGtWTG15YnB6TlFBdGFJL2dhb1ZvaWRuL3VmYk5EYkFvOEtsSzJKK3lQb2M4d1F4dkR4Q21oLzVscjhuQzErbFRiZz09Il0sCgogICAgIkB0YWlsd2luZGNzcy9veGlkZS1saW51eC1hcm02NC1tdXNsIjogWyJAdGFpbHdpbmRjc3Mvb3hpZGUtbGludXgtYXJtNjQtbXVzbEA0LjMuMCIsICIiLCB7ICJvcyI6ICJsaW51eCIsICJjcHUiOiAiYXJtNjQiIH0sICJzaGE1MTItWjZzdWtpUXNuZ25XTytsMzlYNHBQYmlXVDgxSUMrUExLRitQSHhJbHlaYkdOYjlNT0RmWWxYRVZsRnZlajVCT1pJbldYMDFrVnl6ZUx2SHNYaGZjelE9PSJdLAoKICAgICJAdGFpbHdpbmRjc3Mvb3hpZGUtbGludXgteDY0LWdudSI6IFsiQHRhaWx3aW5kY3NzL294aWRlLWxpbnV4LXg2NC1nbnVANC4zLjAiLCAiIiwgeyAib3MiOiAibGludXgiLCAiY3B1IjogIng2NCIgfSwgInNoYTUxMi1EUk5kUVJwU0d6UkdmQVJWdVZreHZNOFExMm5oMTlsNEJGL0c3ekdBMW9lKzl3Y0M2c2FGQkhUSVNycEljS3poaVh0U3JsU3JsdUNmdk11bGVkb0NUUT09Il0sCgogICAgIkB0YWlsd2luZGNzcy9veGlkZS1saW51eC14NjQtbXVzbCI6IFsiQHRhaWx3aW5kY3NzL294aWRlLWxpbnV4LXg2NC1tdXNsQDQuMy4wIiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJ4NjQiIH0sICJzaGE1MTItWjBJQURiRG84Ymg2STdoMklRTXg2MDFBZFhCTGZGcEVkVW90ZnQ4NmV2ZC84WlBmbFplOUNPUE84UTF2dytwZkxXSVVvOXpOL0pHWnZ3dUFKcWR1cWc9PSJdLAoKICAgICJAdGFpbHdpbmRjc3Mvb3hpZGUtd2FzbTMyLXdhc2kiOiBbIkB0YWlsd2luZGNzcy9veGlkZS13YXNtMzItd2FzaUA0LjMuMCIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAZW1uYXBpL2NvcmUiOiAiXjEuMTAuMCIsICJAZW1uYXBpL3J1bnRpbWUiOiAiXjEuMTAuMCIsICJAZW1uYXBpL3dhc2ktdGhyZWFkcyI6ICJeMS4yLjEiLCAiQG5hcGktcnMvd2FzbS1ydW50aW1lIjogIl4xLjEuNCIsICJAdHlieXMvd2FzbS11dGlsIjogIl4wLjEwLjEiLCAidHNsaWIiOiAiXjIuOC4xIiB9LCAiY3B1IjogIm5vbmUiIH0sICJzaGE1MTItSE5aR09VeEVtRWxrc1lSN1M2c0M1alRlTkdwb2JBc3k5dTdHdTBBc2tKOC8yMEZSOUdxZWJVeUIrSEJjVS9heDZCSHVpdUppK09kYTRCK1lYNkgxeUE9PSJdLAoKICAgICJAdGFpbHdpbmRjc3Mvb3hpZGUtd2luMzItYXJtNjQtbXN2YyI6IFsiQHRhaWx3aW5kY3NzL294aWRlLXdpbjMyLWFybTY0LW1zdmNANC4zLjAiLCAiIiwgeyAib3MiOiAid2luMzIiLCAiY3B1IjogImFybTY0IiB9LCAic2hhNTEyLVBlK1JQVlRpMVQrcXltdXVScGNkdndTVlpqbmxsL2Y3bjhnQnhNTWgzeExUY3RNREtxcGRmR2ltYk15aW9xdExoVVlaeGRKOXdHTmhWN01LSHZnWnNRPT0iXSwKCiAgICAiQHRhaWx3aW5kY3NzL294aWRlLXdpbjMyLXg2NC1tc3ZjIjogWyJAdGFpbHdpbmRjc3Mvb3hpZGUtd2luMzIteDY0LW1zdmNANC4zLjAiLCAiIiwgeyAib3MiOiAid2luMzIiLCAiY3B1IjogIng2NCIgfSwgInNoYTUxMi1NdnJmMmtYVy95ZVcvT1RlelpsQ0dPaXJYUmNVdUxJQngvNVkxMkJhUE03d0pvcnlHNmRmUy9OSkw4YUJQcXRURXgvVm00VDR2S3pGVWNLRFQrVEtVQT09Il0sCgogICAgIkB0YWlsd2luZGNzcy9wb3N0Y3NzIjogWyJAdGFpbHdpbmRjc3MvcG9zdGNzc0A0LjMuMCIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAYWxsb2MvcXVpY2stbHJ1IjogIl41LjIuMCIsICJAdGFpbHdpbmRjc3Mvbm9kZSI6ICI0LjMuMCIsICJAdGFpbHdpbmRjc3Mvb3hpZGUiOiAiNC4zLjAiLCAicG9zdGNzcyI6ICJeOC41LjEwIiwgInRhaWx3aW5kY3NzIjogIjQuMy4wIiB9IH0sICJzaGE1MTItSm0wNVRqeCs5eUNMR3Y1cXcxYys4NFBzZHM4TW55ckVRWUNCK0ZGazJsZ0dpVWpsUnFkeGtlNG1WVHVZcmoyeG5WWnFLaW0yQXByNXlTdVFSWUF3L3c9PSJdLAoKICAgICJAdHlieXMvd2FzbS11dGlsIjogWyJAdHlieXMvd2FzbS11dGlsQDAuMTAuMiIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJ0c2xpYiI6ICJeMi40LjAiIH0gfSwgInNoYTUxMi1Sb0J2SjJYMHd1S2xXRklqcndmZkd3MUlxWkhLUXF6SWNoS2FhZFpaZm5OcHNBWXAybU0waDM2SnRQQ2pOREFIR2dZZXovMTV1TUJwZkd3Y2hoaU1nZz09Il0sCgogICAgIkB0eXBlcy9jaGFpIjogWyJAdHlwZXMvY2hhaUA1LjIuMyIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAdHlwZXMvZGVlcC1lcWwiOiAiKiIsICJhc3NlcnRpb24tZXJyb3IiOiAiXjIuMC4xIiB9IH0sICJzaGE1MTItTXc1NThvZUE5ZkZidjY1L3k0bUh0WERzOWJQbkZNWkFML2p4ZFBGVXBPSEhJWFg5MW1jZ0VIYlM1TGFocitwd1pGUjhBN0dRbGVSV2VJNmNHRkMyVUE9PSJdLAoKICAgICJAdHlwZXMvZGVlcC1lcWwiOiBbIkB0eXBlcy9kZWVwLWVxbEA0LjAuMiIsICIiLCB7fSwgInNoYTUxMi1jOWg5ZFZWTWlnTVBjNGJ3VHZDNWR4cXRxSlp3UVBlUHNXalBscFNPbm9qYm9yNnBHcWRrNTQxbGZBN0FxRlFyNXBCMUJSZHEwanVZOWRiODFCd3lGdz09Il0sCgogICAgIkB0eXBlcy9lc3RyZWUiOiBbIkB0eXBlcy9lc3RyZWVAMS4wLjkiLCAiIiwge30sICJzaGE1MTItR2hkUGd5MWVsNC9JbVAwNVgwNVV3NGN3Mi9NOTNCQ1VtbkV2V1pOU3RsQ3pFS01FNEZraytZcG9BNU9pSE5RbW9TN0NhZmI4WGEzUHlhOG0xUXJ6ZWc9PSJdLAoKICAgICJAdHlwZXMvbm9kZSI6IFsiQHR5cGVzL25vZGVAMjIuMTkuMTkiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAidW5kaWNpLXR5cGVzIjogIn42LjIxLjAiIH0gfSwgInNoYTUxMi1keWgveE8yRmg1YllyZldhYXFHclJRUUdrTmRtWXc2QW1hQVV2WWVVTU5UV1F0dmI3OTZpa0xkbVRjaFJtT2xPaUlKMVREWGZXZ1Z4MVFrVWxRNkhldz09Il0sCgogICAgIkB0eXBlcy9yZWFjdCI6IFsiQHR5cGVzL3JlYWN0QDE5LjIuMTUiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiY3NzdHlwZSI6ICJeMy4yLjIiIH0gfSwgInNoYTUxMi1lUndjR05IdmUrRThxdEVRU1NSbDZ1cmgrckZvcDR2OGdtNk84ckd2MjVDb2RidkZkTGpBMXZWUTFLa2lGRTB3MFVQT25iOHREaUZLTDVscDBydFk1UT09Il0sCgogICAgIkB0eXBlcy9yZWFjdC1kb20iOiBbIkB0eXBlcy9yZWFjdC1kb21AMTkuMi4zIiwgIiIsIHsgInBlZXJEZXBlbmRlbmNpZXMiOiB7ICJAdHlwZXMvcmVhY3QiOiAiXjE5LjIuMCIgfSB9LCAic2hhNTEyLWpwMkwvZVk2Zm4rS2dWVlFBT3FZSXRiRjBWWS9ZQXBlNU16MkYwYXlrU084Z3gzMWJZQ1p5dlNlWXhDSEt2ekhHNWVaamMrenlhUzVCckJXeWEyK2tRPT0iXSwKCiAgICAiQHZpdGVzdC9leHBlY3QiOiBbIkB2aXRlc3QvZXhwZWN0QDQuMS42IiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIkBzdGFuZGFyZC1zY2hlbWEvc3BlYyI6ICJeMS4xLjAiLCAiQHR5cGVzL2NoYWkiOiAiXjUuMi4yIiwgIkB2aXRlc3Qvc3B5IjogIjQuMS42IiwgIkB2aXRlc3QvdXRpbHMiOiAiNC4xLjYiLCAiY2hhaSI6ICJeNi4yLjIiLCAidGlueXJhaW5ib3ciOiAiXjMuMS4wIiB9IH0sICJzaGE1MTItN0VIRHF1UHRoQUxTVjBqaGhqZ0VXOEZYYXZpTXg3clNxdThXNm9xQ29BdU9oS292ODE0UDk5UURWMXB4TUEzUVB2MjFZdWR2Sm5nSWhqck5JNG9wTGc9PSJdLAoKICAgICJAdml0ZXN0L21vY2tlciI6IFsiQHZpdGVzdC9tb2NrZXJANC4xLjYiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiQHZpdGVzdC9zcHkiOiAiNC4xLjYiLCAiZXN0cmVlLXdhbGtlciI6ICJeMy4wLjMiLCAibWFnaWMtc3RyaW5nIjogIl4wLjMwLjIxIiB9LCAicGVlckRlcGVuZGVuY2llcyI6IHsgIm1zdyI6ICJeMi40LjkiLCAidml0ZSI6ICJeNi4wLjAgfHwgXjcuMC4wIHx8IF44LjAuMCIgfSwgIm9wdGlvbmFsUGVlcnMiOiBbIm1zdyIsICJ2aXRlIl0gfSwgInNoYTUxMi1NQ0ZjNjNjek1qRUluT2xjWTJjcFFDdkNOK0tnYkFuKzYweHU5Y01nUDRzS2FMQzVKTkFLdzdKSDhRZEFub0FDODhoVzFJaVNOWitHZ1ZYbE4xVWNNUT09Il0sCgogICAgIkB2aXRlc3QvcHJldHR5LWZvcm1hdCI6IFsiQHZpdGVzdC9wcmV0dHktZm9ybWF0QDQuMS42IiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgInRpbnlyYWluYm93IjogIl4zLjEuMCIgfSB9LCAic2hhNTEyLWg1U3hEL0l6TmhaWW5yU1pSc1VaUUlDK3ZEMEdZOGNVdnEwaXdzbWtGS2l4UkNLTExXcUNYYS9GSVE0UzFSK3NJK1BHb29qa0hzZE5yYlppTTlRcGd3PT0iXSwKCiAgICAiQHZpdGVzdC9ydW5uZXIiOiBbIkB2aXRlc3QvcnVubmVyQDQuMS42IiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIkB2aXRlc3QvdXRpbHMiOiAiNC4xLjYiLCAicGF0aGUiOiAiXjIuMC4zIiB9IH0sICJzaGE1MTItbk9QQ21uMit5RDBaTm1LZHNYR3YvVXhNTVdiTXVLZUQ2R3lZbmNOd2RrWUR4cFF2clBTS1lqMnJXdURqQzJZNGI2dzZoamlwNWRCS0Z6RVV1WmUzdkE9PSJdLAoKICAgICJAdml0ZXN0L3NuYXBzaG90IjogWyJAdml0ZXN0L3NuYXBzaG90QDQuMS42IiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIkB2aXRlc3QvcHJldHR5LWZvcm1hdCI6ICI0LjEuNiIsICJAdml0ZXN0L3V0aWxzIjogIjQuMS42IiwgIm1hZ2ljLXN0cmluZyI6ICJeMC4zMC4yMSIsICJwYXRoZSI6ICJeMi4wLjMiIH0gfSwgInNoYTUxMi1ZaHNkRTZ4QVZmVERtemp4TDJaRFV2amorWnNneU9LZStUZFF6cWtENzJ3SU9tSGthOE51R1E2TnBUTlp2OUQyWjYzZmJ3V0tKUGVWcEV3NEVRZ1l4dz09Il0sCgogICAgIkB2aXRlc3Qvc3B5IjogWyJAdml0ZXN0L3NweUA0LjEuNiIsICIiLCB7fSwgInNoYTUxMi1KRkt4TXg2dWRod0toL0xkbzI3MGUxN1FYNzEwdmd1bk1rdVBBdlhqSFN2QzZvcUxXQUhoVmhqZy9JNzFxMHUwQ0JTRXJJT0RWMUtqdjBGUU5TV2pkZz09Il0sCgogICAgIkB2aXRlc3QvdXRpbHMiOiBbIkB2aXRlc3QvdXRpbHNANC4xLjYiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiQHZpdGVzdC9wcmV0dHktZm9ybWF0IjogIjQuMS42IiwgImNvbnZlcnQtc291cmNlLW1hcCI6ICJeMi4wLjAiLCAidGlueXJhaW5ib3ciOiAiXjMuMS4wIiB9IH0sICJzaGE1MTItRnhJWStVODFSM0xHS0N4YUhIRlJRNStnNi9pUmdHTG1lSFdkcDJBbWo0bGpRUnJFSVdIbVp5RGZEWUJSWmxweXFBN3FLeHRTOUREMWRoazhSblJJVlE9PSJdLAoKICAgICJhbnNpLXJlZ2V4IjogWyJhbnNpLXJlZ2V4QDUuMC4xIiwgIiIsIHt9LCAic2hhNTEyLXF1SlFYbFRTVUdMMkxIOVNVWG84VndzWTRzb2FuaGdvNkxOU204NEUxTEJjRThzM08wd3BkaVJ6eVI5ei9aWkpNbE1XdjM3cU9PYjlwZEpsTVVFS0ZRPT0iXSwKCiAgICAiYW5zaS1zdHlsZXMiOiBbImFuc2ktc3R5bGVzQDQuMy4wIiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgImNvbG9yLWNvbnZlcnQiOiAiXjIuMC4xIiB9IH0sICJzaGE1MTItemJCOXJDSkFUMXJiamlWRGIyaHFLRkhOWUx4Z3RrOE5VUnhaM0lad0QzRjZOdHhiWFpRQ25uU2kxTGt4K0lEb2hkUGxGcDIyMndWQUxJaGVaSlFTRWc9PSJdLAoKICAgICJhc3NlcnRpb24tZXJyb3IiOiBbImFzc2VydGlvbi1lcnJvckAyLjAuMSIsICIiLCB7fSwgInNoYTUxMi1Jemk4UlFjZmZxQ2VOVmdGaWdLbGkxc3NrbElicEhuQ1ljNkFrblhHWW9CNmdySnF5ZWJ5N2p2MTJKVVFnbVRBbklEbmJjazF1eGtzVDRkek4zUFdCQT09Il0sCgogICAgImJhc2VsaW5lLWJyb3dzZXItbWFwcGluZyI6IFsiYmFzZWxpbmUtYnJvd3Nlci1tYXBwaW5nQDIuMTAuMzEiLCAiIiwgeyAiYmluIjogeyAiYmFzZWxpbmUtYnJvd3Nlci1tYXBwaW5nIjogImRpc3QvY2xpLmNqcyIgfSB9LCAic2hhNTEyLU11allPM2VQNzJ1dm1TRTBpNHdsdHNvZFJmSXBaQVRQM2p2elJOUkdHeGd6SWQ3YVZvY1ZKSlYzbmYwMXFuenpLRkd4UVZDOWJwV3hsNWNqeFRyLzdRPT0iXSwKCiAgICAiY2FuaXVzZS1saXRlIjogWyJjYW5pdXNlLWxpdGVAMS4wLjMwMDAxNzkzIiwgIiIsIHt9LCAic2hhNTEyLWl3U3NZV2FDT29oMjZjVjhOd05SVmlIbHJmVXZZc0hEZlJWY2J0bXcwS2c2UEpJWlpYd01rajE0NDJGWUxCR2tlVWYxanVBc1UzRFRmeFc1NzltclBBPT0iXSwKCiAgICAiY2hhaSI6IFsiY2hhaUA2LjIuMiIsICIiLCB7fSwgInNoYTUxMi1OVVBSbHVPZk9pVEtCS3ZXUHRTRDRQaEZ2V0NxT2kwQkdTdE5XczU3WDlqczdYR1RwclNtRm96NUYwdFdoUjRXUGpOZVI5alhxZEM3L1VwU0pUbmxSZz09Il0sCgogICAgImNoYWxrIjogWyJjaGFsa0A0LjEuMiIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJhbnNpLXN0eWxlcyI6ICJeNC4xLjAiLCAic3VwcG9ydHMtY29sb3IiOiAiXjcuMS4wIiB9IH0sICJzaGE1MTItb0tuYmhGeVJJWHBVdWV6OGlCTW15RWE0bmJqNElPUXl1aGMvd3k5a1k3L1dWUGN3SU85VkE2NjhQdThSa083KzBHNzZTTFJPZXl3OUNwUTA2MWk0bUE9PSJdLAoKICAgICJjbGllbnQtb25seSI6IFsiY2xpZW50LW9ubHlAMC4wLjEiLCAiIiwge30sICJzaGE1MTItSVYzT3UwalNNelpyZDNwWjQ4bkxrVDlEQTdBZzFwblB6YWlRaHBXN2MzUmJjcXF6dnp6VnUrTDhnZnFNcC84SU0yTVF0U2lxYUN4cnJjZnU4SThyTUE9PSJdLAoKICAgICJjbGl1aSI6IFsiY2xpdWlAOC4wLjEiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAic3RyaW5nLXdpZHRoIjogIl40LjIuMCIsICJzdHJpcC1hbnNpIjogIl42LjAuMSIsICJ3cmFwLWFuc2kiOiAiXjcuMC4wIiB9IH0sICJzaGE1MTItQlNlTm55dXM3NUM0Ly9OUTlnUXQxL2NzVFh5by84U2IrYWZMQWt6QXB0RnVNc29kOUhGb2tHTnVkWnBpL29RVjczaG5WSytzUis1UFZSTWQrRHI3WVE9PSJdLAoKICAgICJjbHN4IjogWyJjbHN4QDIuMS4xIiwgIiIsIHt9LCAic2hhNTEyLWVZbTBRV0J0VXJCV1pXRzBkMzg2T0dBdzE2Wjk5NVBpT1ZvMkI3YmpXU2JIZWRHbDVlMFpXYXE2NWtPR2dVU05lc0VJRGtCOUlTYlRnL0pLOWRoQ1pBPT0iXSwKCiAgICAiY29sb3ItY29udmVydCI6IFsiY29sb3ItY29udmVydEAyLjAuMSIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJjb2xvci1uYW1lIjogIn4xLjEuNCIgfSB9LCAic2hhNTEyLVJSRUNQc2o3aXUveGI1b0tZY3NGSFNwcEZObnNqLzUyT1ZUUktiNHpQNW9uWHdWRjN6Vm1tVG9OY09mR0MrQ1JEcGZLL1U1ODRmTWczOFpIQ2FFbEtRPT0iXSwKCiAgICAiY29sb3ItbmFtZSI6IFsiY29sb3ItbmFtZUAxLjEuNCIsICIiLCB7fSwgInNoYTUxMi1kT3krM0F1VzNhMndOYlpISXVNWnBUY2dqR3VMVS91QkwvdWJjWkY5T1hiRG84ZmY0Tzh5VnA1QmYwZWZTOHVFb1lvNXE0Rng3ZFk5T2dRR1hnQXNRQT09Il0sCgogICAgImNvbmN1cnJlbnRseSI6IFsiY29uY3VycmVudGx5QDkuMi4xIiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgImNoYWxrIjogIjQuMS4yIiwgInJ4anMiOiAiNy44LjIiLCAic2hlbGwtcXVvdGUiOiAiMS44LjMiLCAic3VwcG9ydHMtY29sb3IiOiAiOC4xLjEiLCAidHJlZS1raWxsIjogIjEuMi4yIiwgInlhcmdzIjogIjE3LjcuMiIgfSwgImJpbiI6IHsgImNvbmMiOiAiZGlzdC9iaW4vY29uY3VycmVudGx5LmpzIiwgImNvbmN1cnJlbnRseSI6ICJkaXN0L2Jpbi9jb25jdXJyZW50bHkuanMiIH0gfSwgInNoYTUxMi1mc2ZyTzBNeFY2NFpub3k4L2wxdlZJampIYTI5U1p5eXFQZ1FCd2hpRGNhVzh3SmMyVzNYV1ZPR3g0TTNvSkJudi96ZFVaSUlwMWdEZVM5OEd6UDhOZz09Il0sCgogICAgImNvbnZlcnQtc291cmNlLW1hcCI6IFsiY29udmVydC1zb3VyY2UtbWFwQDIuMC4wIiwgIiIsIHt9LCAic2hhNTEyLUt2cDQ1OUhyVjJGRUoxQ0FzaTFLdStNWTNrYXNIMTlURnlrVHoyeFdtTWVxNmJrMk5VM1hYdmZKK1E2MW0weGt0V3d0KzFIU1lmM0pac1RtczNhUkpnPT0iXSwKCiAgICAiY3NzdHlwZSI6IFsiY3NzdHlwZUAzLjIuMyIsICIiLCB7fSwgInNoYTUxMi16MUhHS2NZeTJ4QThBR1Fmd3JuMFBBeStQQjdYL0dTajNVVkpXOXFLeW40M3hXYStnbDVuWG1VNHFxTE1SeldWTEZDOEt1c1VYOFQvMGtDaU9ZcEFJUT09Il0sCgogICAgImRhdGUtZm5zIjogWyJkYXRlLWZuc0A0LjIuMSIsICIiLCB7fSwgInNoYTUxMi0zN1JoU2R4YUcxc3VlbjZWREN6YTZyTnJRZm9veVFoNTdIRlZQd1FHRXEyUVdsaVZMelBRWjhPYTAxN3dlT3UrSFpDbnpJN04zUGYvd3lvQktmRXFyQT09Il0sCgogICAgImRhdGUtZm5zLWphbGFsaSI6IFsiZGF0ZS1mbnMtamFsYWxpQDQuMS4wLTAiLCAiIiwge30sICJzaGE1MTItaFRJUC96K3QrcUt3QkRjbW1zbm1qV1RkdXhDZys1S2ZkcVdRdmIyWC84Qzkra25ZWTZlcE4vcGZ4ZER1eVZsU1ZlRnowc001ZUVmd0lVUTcwVTRja2c9PSJdLAoKICAgICJkZXRlY3QtbGliYyI6IFsiZGV0ZWN0LWxpYmNAMi4xLjIiLCAiIiwge30sICJzaGE1MTItQnRqMkJPT084M28zV3lINTllOE1nWHN4RVFWY2Fya1VPcEVZcnViQjB1cnduTjEweVEzNjRyc2lCeVUxMW5abHFXWVptMDVpL29mN2lvNG16aWhCdFE9PSJdLAoKICAgICJlbW9qaS1yZWdleCI6IFsiZW1vamktcmVnZXhAOC4wLjAiLCAiIiwge30sICJzaGE1MTItTVNqWXpjV05PQTBld0FIcHowTXhwWUZ2d2c2eWp5MU5HM3h0ZW9xejY0NFZDby9SUGducjEvR0d0K2ljM2lKVHpROEV1M1RkTTE0U2F3blZVbUdFNkE9PSJdLAoKICAgICJlbmhhbmNlZC1yZXNvbHZlIjogWyJlbmhhbmNlZC1yZXNvbHZlQDUuMjEuNSIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJncmFjZWZ1bC1mcyI6ICJeNC4yLjQiLCAidGFwYWJsZSI6ICJeMi4zLjMiIH0gfSwgInNoYTUxMi1tTENOYnJRbGkxMUsxeVNVbXVOdDRaVUIzT3BHSURxNHEydlRCVGY1Y0wybHBzUmpJOVFLcVNEMG5kalc4Rnl2Y1cvSmo0NmdNZTlzeXlIQXN2TWEvQT09Il0sCgogICAgImVzLW1vZHVsZS1sZXhlciI6IFsiZXMtbW9kdWxlLWxleGVyQDIuMS4wIiwgIiIsIHt9LCAic2hhNTEyLW4yN3pUWU1qWXUxYWo0TWpDV3pTUDdHOXI3NXV0c2FvYzhtNjF3ZUsrVzhKTUJHR1F5YmQ0M0dzdENYWjNXTm1TRnRHVDl3aTU5cVFUVzZtaFRSNUxRPT0iXSwKCiAgICAiZXNjYWxhZGUiOiBbImVzY2FsYWRlQDMuMi4wIiwgIiIsIHt9LCAic2hhNTEyLVdVajJxbHhhUXRPNGc2UHE1YzI5R1RjV0dEeWQ4aXRMOHpUbGlwZ0VDejNKZXNBaWlPS290ZDhKVTZvdEIzUEFDZ0c2eGtKVXlWaGJvTVMrYmplL2pBPT0iXSwKCiAgICAiZXN0cmVlLXdhbGtlciI6IFsiZXN0cmVlLXdhbGtlckAzLjAuMyIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAdHlwZXMvZXN0cmVlIjogIl4xLjAuMCIgfSB9LCAic2hhNTEyLTdSVUtmWGdTTU1renQ2WnVYbXFhcE91ckxHUFBmZ2o2bDl1Ulo3bFJHb2x2azB5MnlvY2MzNUxkY3hLQzVQUVpkbjJETXFpb0FRMk5vV2NyVEttbTZnPT0iXSwKCiAgICAiZXhwZWN0LXR5cGUiOiBbImV4cGVjdC10eXBlQDEuMy4wIiwgIiIsIHt9LCAic2hhNTEyLWtudnllYXVZaHFqT1l2UTY2TXpuU01zODN3bUhyQ3ljTkVONkFvKzJBZVlFZnhVSWt1aVZ4ZEVhMXFsR0VQSytXZTNuMFRIaURjaVlTc0NjZ1cvRG9BPT0iXSwKCiAgICAiZmRpciI6IFsiZmRpckA2LjUuMCIsICIiLCB7ICJwZWVyRGVwZW5kZW5jaWVzIjogeyAicGljb21hdGNoIjogIl4zIHx8IF40IiB9LCAib3B0aW9uYWxQZWVycyI6IFsicGljb21hdGNoIl0gfSwgInNoYTUxMi10SWJZdFpidWNPczBCUkdxUEprc2hKVVlkTCtTREg3ZFZNOGdqeStFUnAzV0FVakxFRkpFKzAya2FueUh0d2pXT253cktZQml3QW1NMHA0a0xKQW5YZz09Il0sCgogICAgImZzZXZlbnRzIjogWyJmc2V2ZW50c0AyLjMuMyIsICIiLCB7ICJvcyI6ICJkYXJ3aW4iIH0sICJzaGE1MTItNXhvRGZYK2ZMN2ZhQVRuYWdtV1BwYkZ0d2gvUjc3V21NTXFxSEdTNjVDM3Z2QjBZSHJnRitCMVltWjM0NDF0TWo1bjYzazAyMTJYTm9Kd3psaGZmUXc9PSJdLAoKICAgICJnZXQtY2FsbGVyLWZpbGUiOiBbImdldC1jYWxsZXItZmlsZUAyLjAuNSIsICIiLCB7fSwgInNoYTUxMi1EeUZQM0JNLzNZSFRRT0NVTC93ME9aSFIwbHBLZUdyeG90Y0hXY3FORWRubHRxRndYVmZoRUJROTRlSW8zNEFmUXBvMHJHa2k0Y3lJaWZ0WTA2aDJGZz09Il0sCgogICAgImdyYWNlZnVsLWZzIjogWyJncmFjZWZ1bC1mc0A0LjIuMTEiLCAiIiwge30sICJzaGE1MTItUmJKNS9qbUZjTk5DY0RWNW85ZVRuQkxKL0hzeldWMFA3M2JjK0ZmNG5TL3JKaitZYVM2SUd5aU9MMFZvQllYK2wxV3JsM2s2M2gvS3JIK25oSjBYdlE9PSJdLAoKICAgICJoYXMtZmxhZyI6IFsiaGFzLWZsYWdANC4wLjAiLCAiIiwge30sICJzaGE1MTItRXlrSlQvUTFLalRXY3RwcGdJQWdmU08wdEtWdVpVamhnTXIxN2txVHVtTWw2QWZ2M0VJU2xlVTdxWlV6b1hERlRBSFREQzROT29HL1p4VTNFdmxNUFE9PSJdLAoKICAgICJpcy1mdWxsd2lkdGgtY29kZS1wb2ludCI6IFsiaXMtZnVsbHdpZHRoLWNvZGUtcG9pbnRAMy4wLjAiLCAiIiwge30sICJzaGE1MTItenltbTUrdStzQ3NTV3lEOXFOYWVqVjNERnZoQ0tjbEtkaXpZYUpVdUhBODNSTGpiN25TdUduZGRDSEd2MGhrK0tZN0JNQWxzV2VLNFVlZzZFVjZYUWc9PSJdLAoKICAgICJqaXRpIjogWyJqaXRpQDIuNy4wIiwgIiIsIHsgImJpbiI6IHsgImppdGkiOiAibGliL2ppdGktY2xpLm1qcyIgfSB9LCAic2hhNTEyLUFDLzdKb2ZKdlpHcnJuZVdOYUVuSmVPTFV4K0psR3Q3dE5hMHdaaVJQVDRNWTF3bWZLanQyKzZPMnAydXoyK3NrbGw4T1pabUpNTnFla2U3a0tiTmdRPT0iXSwKCiAgICAibGlicGhvbmVudW1iZXItanMiOiBbImxpYnBob25lbnVtYmVyLWpzQDEuMTIuNDEiLCAiIiwge30sICJzaGE1MTItbHNtTW1HWEJ4WElLL1ZNTEVqMGtMNk10VXMxa0JHajFuVEN6aTZ6Z1FvRzFERXdxd3QyRFF5SHhjTHlrY2VJeEFuZkUzaHlhN051SWg2UHBDNlMzZkE9PSJdLAoKICAgICJsaWdodG5pbmdjc3MiOiBbImxpZ2h0bmluZ2Nzc0AxLjMyLjAiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiZGV0ZWN0LWxpYmMiOiAiXjIuMC4zIiB9LCAib3B0aW9uYWxEZXBlbmRlbmNpZXMiOiB7ICJsaWdodG5pbmdjc3MtYW5kcm9pZC1hcm02NCI6ICIxLjMyLjAiLCAibGlnaHRuaW5nY3NzLWRhcndpbi1hcm02NCI6ICIxLjMyLjAiLCAibGlnaHRuaW5nY3NzLWRhcndpbi14NjQiOiAiMS4zMi4wIiwgImxpZ2h0bmluZ2Nzcy1mcmVlYnNkLXg2NCI6ICIxLjMyLjAiLCAibGlnaHRuaW5nY3NzLWxpbnV4LWFybS1nbnVlYWJpaGYiOiAiMS4zMi4wIiwgImxpZ2h0bmluZ2Nzcy1saW51eC1hcm02NC1nbnUiOiAiMS4zMi4wIiwgImxpZ2h0bmluZ2Nzcy1saW51eC1hcm02NC1tdXNsIjogIjEuMzIuMCIsICJsaWdodG5pbmdjc3MtbGludXgteDY0LWdudSI6ICIxLjMyLjAiLCAibGlnaHRuaW5nY3NzLWxpbnV4LXg2NC1tdXNsIjogIjEuMzIuMCIsICJsaWdodG5pbmdjc3Mtd2luMzItYXJtNjQtbXN2YyI6ICIxLjMyLjAiLCAibGlnaHRuaW5nY3NzLXdpbjMyLXg2NC1tc3ZjIjogIjEuMzIuMCIgfSB9LCAic2hhNTEyLU5YWUJ6aW5OcmJsZnJhUEd5cmJQb0QxOUMxaDlsZkkvMW16Z1dZdlhVVGU0MTRHei9YMUZEMlhCWlNaTTdyUlRyTUE4SkwzT3RBYUdpZnJJS2hRNXlRPT0iXSwKCiAgICAibGlnaHRuaW5nY3NzLWFuZHJvaWQtYXJtNjQiOiBbImxpZ2h0bmluZ2Nzcy1hbmRyb2lkLWFybTY0QDEuMzIuMCIsICIiLCB7ICJvcyI6ICJhbmRyb2lkIiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi1ZSzcvQ2xUdDRrQUswdm82dzNYK1BubTBEMmNmMnZQSGJoT1hkb050aTFHYTBhbDFQNFRCWmh3akFUdmpOd0xFQkNuS3ZqSmMyalFnSFhIME5Fd2xBZz09Il0sCgogICAgImxpZ2h0bmluZ2Nzcy1kYXJ3aW4tYXJtNjQiOiBbImxpZ2h0bmluZ2Nzcy1kYXJ3aW4tYXJtNjRAMS4zMi4wIiwgIiIsIHsgIm9zIjogImRhcndpbiIsICJjcHUiOiAiYXJtNjQiIH0sICJzaGE1MTItUnplRzlKdTViYWcyQnYxL2x3bFZKdkJFM3E2VHRYc2tkWkxMQ3lmZzVwdCtITHo5QnFsSUNPN0xaTTdWSE5UVG4vNVBSaEhGQlNqazVsYzRjbXNjUFE9PSJdLAoKICAgICJsaWdodG5pbmdjc3MtZGFyd2luLXg2NCI6IFsibGlnaHRuaW5nY3NzLWRhcndpbi14NjRAMS4zMi4wIiwgIiIsIHsgIm9zIjogImRhcndpbiIsICJjcHUiOiAieDY0IiB9LCAic2hhNTEyLVUrUXNCcDJtL3Myd3FwVVlULzZ3bmxhZ2RaYnRaZG5kU211dC9OSnFsQ2NNTFRXcDVtdUNySUQrSzVVSjZqcUQyQkZzaGVqQ1lYbmlQRGJOaDczVjh3PT0iXSwKCiAgICAibGlnaHRuaW5nY3NzLWZyZWVic2QteDY0IjogWyJsaWdodG5pbmdjc3MtZnJlZWJzZC14NjRAMS4zMi4wIiwgIiIsIHsgIm9zIjogImZyZWVic2QiLCAiY3B1IjogIng2NCIgfSwgInNoYTUxMi1KQ1RpZ2VkRWtzWmszdEhUVHRobk1kVmZHZjYxRmt5OEppMkU0WWpVVEVRWDE0eGl5L2xUelhudTF2d2laZTNiWWUwcStTcHNTSC9DVGVEWEs2V0hpZz09Il0sCgogICAgImxpZ2h0bmluZ2Nzcy1saW51eC1hcm0tZ251ZWFiaWhmIjogWyJsaWdodG5pbmdjc3MtbGludXgtYXJtLWdudWVhYmloZkAxLjMyLjAiLCAiIiwgeyAib3MiOiAibGludXgiLCAiY3B1IjogImFybSIgfSwgInNoYTUxMi14NnJubnBSYTJHTDB6UU9rdDZydHMzWURQemR1THBXdndBRjZFTWhYRlZaWEQ0dFByQmtFRnF6R293ekNzSVdzUGpxU0srdHlORU9EVUJYZWVWSFNrdz09Il0sCgogICAgImxpZ2h0bmluZ2Nzcy1saW51eC1hcm02NC1nbnUiOiBbImxpZ2h0bmluZ2Nzcy1saW51eC1hcm02NC1nbnVAMS4zMi4wIiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi0wbm5NeW95T0xSSlhmYk1PaWxhU1JjTEgzSnc1ejlIRE5HZlQvZ3dDUGdhRGpueDBpOHc3dkJ6RkxGUjFmNkNNTEtGOGdWYmVibWtVTjNmYS9rUUpwUT09Il0sCgogICAgImxpZ2h0bmluZ2Nzcy1saW51eC1hcm02NC1tdXNsIjogWyJsaWdodG5pbmdjc3MtbGludXgtYXJtNjQtbXVzbEAxLjMyLjAiLCAiIiwgeyAib3MiOiAibGludXgiLCAiY3B1IjogImFybTY0IiB9LCAic2hhNTEyLVVwUWtvZW5yNFVKRXpnVklZcEk4MGxERnZSbVBWZzZvcWJvTkhmb0g0Q1FJZk5BK0hPclo3TW83S1pQMDJkQzZMamdoUFFKZUJzdlhoSm9kL3duSUJnPT0iXSwKCiAgICAibGlnaHRuaW5nY3NzLWxpbnV4LXg2NC1nbnUiOiBbImxpZ2h0bmluZ2Nzcy1saW51eC14NjQtZ251QDEuMzIuMCIsICIiLCB7ICJvcyI6ICJsaW51eCIsICJjcHUiOiAieDY0IiB9LCAic2hhNTEyLVY3UXI1MkloWm1kS1BWcitWdHc4bytXTHNRSllDVGQ4bG9JZnBEYU1SV0dVWmZCT1lFSmV5SklrcUdJRE1aUHdQeDI0cFVNZndTeHhJOHBoci9NYk9BPT0iXSwKCiAgICAibGlnaHRuaW5nY3NzLWxpbnV4LXg2NC1tdXNsIjogWyJsaWdodG5pbmdjc3MtbGludXgteDY0LW11c2xAMS4zMi4wIiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJ4NjQiIH0sICJzaGE1MTItYlljTHArVmIwYXdzaVhnLzgwdUNSZXpDWUhOZzEvbDNtdDBnekhuV1Y5WFAxVzVzS2E1L1RDZEdXYVIvekJNMlBlRi9IYnNRdi9qMlVSTk9pVnV4V2c9PSJdLAoKICAgICJsaWdodG5pbmdjc3Mtd2luMzItYXJtNjQtbXN2YyI6IFsibGlnaHRuaW5nY3NzLXdpbjMyLWFybTY0LW1zdmNAMS4zMi4wIiwgIiIsIHsgIm9zIjogIndpbjMyIiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi04U2JDOEJSNDBwUzZiYUNNOHNidFlEU3dFVlFkNEpsRlRPbGFEM2dXR0hmVGhUY0FCbk5EQmRhNmVUWmVxYm9mYWxJSmhGeDBxS3pnSEptY1BUbkdkdz09Il0sCgogICAgImxpZ2h0bmluZ2Nzcy13aW4zMi14NjQtbXN2YyI6IFsibGlnaHRuaW5nY3NzLXdpbjMyLXg2NC1tc3ZjQDEuMzIuMCIsICIiLCB7ICJvcyI6ICJ3aW4zMiIsICJjcHUiOiAieDY0IiB9LCAic2hhNTEyLUFtcTlCL1NvWllkRGkxa0Zyb2pub3FQTHhZaFE0V281WGlMOEVWSnJWc0I4QVJvQzFQV1c2Vkd0VDBXS0NlbWp5OGFDK2xvdUpualM3VTE4eDNiMDZRPT0iXSwKCiAgICAibWFnaWMtc3RyaW5nIjogWyJtYWdpYy1zdHJpbmdAMC4zMC4yMSIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAanJpZGdld2VsbC9zb3VyY2VtYXAtY29kZWMiOiAiXjEuNS41IiB9IH0sICJzaGE1MTItdmQyRjRZVXlFWEtHY0xIb3ErVEV5Q2p4dWVTZUhuRnh5eWpOcDgweWcwWFY0dlVobkRlci9sdnZscU0vYXJCNWJYUU41SzIvM29pbnlDUnl4OFQyQ1E9PSJdLAoKICAgICJuYW5vaWQiOiBbIm5hbm9pZEAzLjMuMTIiLCAiIiwgeyAiYmluIjogeyAibmFub2lkIjogImJpbi9uYW5vaWQuY2pzIiB9IH0sICJzaGE1MTItWkI5UkgvMzlxcHE1VnU2WStObVVhRmhRUjZwcCtNMlh0NzZYQm5Fd0RhR2NWQXFobHZ4cmwzQjJiS1M1RDNOSDNRUjc2djNhU3JLYUYvS2l5N2xFdFE9PSJdLAoKICAgICJuZXh0IjogWyJuZXh0QDE2LjIuNiIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAbmV4dC9lbnYiOiAiMTYuMi42IiwgIkBzd2MvaGVscGVycyI6ICIwLjUuMTUiLCAiYmFzZWxpbmUtYnJvd3Nlci1tYXBwaW5nIjogIl4yLjkuMTkiLCAiY2FuaXVzZS1saXRlIjogIl4xLjAuMzAwMDE1NzkiLCAicG9zdGNzcyI6ICI4LjQuMzEiLCAic3R5bGVkLWpzeCI6ICI1LjEuNiIgfSwgIm9wdGlvbmFsRGVwZW5kZW5jaWVzIjogeyAiQG5leHQvc3djLWRhcndpbi1hcm02NCI6ICIxNi4yLjYiLCAiQG5leHQvc3djLWRhcndpbi14NjQiOiAiMTYuMi42IiwgIkBuZXh0L3N3Yy1saW51eC1hcm02NC1nbnUiOiAiMTYuMi42IiwgIkBuZXh0L3N3Yy1saW51eC1hcm02NC1tdXNsIjogIjE2LjIuNiIsICJAbmV4dC9zd2MtbGludXgteDY0LWdudSI6ICIxNi4yLjYiLCAiQG5leHQvc3djLWxpbnV4LXg2NC1tdXNsIjogIjE2LjIuNiIsICJAbmV4dC9zd2Mtd2luMzItYXJtNjQtbXN2YyI6ICIxNi4yLjYiLCAiQG5leHQvc3djLXdpbjMyLXg2NC1tc3ZjIjogIjE2LjIuNiIsICJzaGFycCI6ICJeMC4zNC41IiB9LCAicGVlckRlcGVuZGVuY2llcyI6IHsgIkBvcGVudGVsZW1ldHJ5L2FwaSI6ICJeMS4xLjAiLCAiQHBsYXl3cmlnaHQvdGVzdCI6ICJeMS41MS4xIiwgImJhYmVsLXBsdWdpbi1yZWFjdC1jb21waWxlciI6ICIqIiwgInJlYWN0IjogIl4xOC4yLjAgfHwgMTkuMC4wLXJjLWRlNjhkMmY0LTIwMjQxMjA0IHx8IF4xOS4wLjAiLCAicmVhY3QtZG9tIjogIl4xOC4yLjAgfHwgMTkuMC4wLXJjLWRlNjhkMmY0LTIwMjQxMjA0IHx8IF4xOS4wLjAiLCAic2FzcyI6ICJeMS4zLjAiIH0sICJvcHRpb25hbFBlZXJzIjogWyJAb3BlbnRlbGVtZXRyeS9hcGkiLCAiQHBsYXl3cmlnaHQvdGVzdCIsICJiYWJlbC1wbHVnaW4tcmVhY3QtY29tcGlsZXIiLCAic2FzcyJdLCAiYmluIjogeyAibmV4dCI6ICJkaXN0L2Jpbi9uZXh0IiB9IH0sICJzaGE1MTItcU9WZ0tKZzErQXQxNU5wZVVQK2VKZ0NIdlRDZ1hzb2d3ZXE4N1JpL0l4N1BrcVFIZzRzZGFYbVNGcUtsZ2FJWEU0a1cwZzI1TEU2OFc4N1VBTmxIdHc9PSJdLAoKICAgICJvYnVnIjogWyJvYnVnQDIuMS4xIiwgIiIsIHt9LCAic2hhNTEyLXVUcUY5TXVQcmFBUStJc25QZjM2NlJHNGNQOVJ0VWk3TUxPMU4zS0VjK3diMGE2eUtwZUwwbG1rMklCMWpZNUtIUEFsVGM2VC9KUmRDL1lxeEhOd2tRPT0iXSwKCiAgICAicGF0aGUiOiBbInBhdGhlQDIuMC4zIiwgIiIsIHt9LCAic2hhNTEyLVdVakdjQXFQMWdRYWNvUWUrT0JKc0ZBN0xkNER5WHVVSWpaNWNjNzVjTEh2SjdkdE5zVHVncGh4SUFEd3NwUytBcmFBVWVQQ0tyU1Z0UExGai9GODh3PT0iXSwKCiAgICAicGljb2NvbG9ycyI6IFsicGljb2NvbG9yc0AxLjEuMSIsICIiLCB7fSwgInNoYTUxMi14Y2VIMnNuaHRiNU05bGlxRHNtRXc1NmxlMzc2bVRaa0VYL2pFYi9SeE5GeWVnTnVsN2VOc2xDWFA5RkRqL0xjdTBYOEtFeU1jZVAybnRwYUhyREVWQT09Il0sCgogICAgInBpY29tYXRjaCI6IFsicGljb21hdGNoQDQuMC40IiwgIiIsIHt9LCAic2hhNTEyLVFQODhCQUt2TWFtLzNOeEg2dmoybzIxUjZNanhaVUFkNm5sd0FTL3BuR3ZOOUlWTG9jTEh4R1lJekZoZzZmVVErNXRoNlA0ZHY0ZVc5algzRFNJajdBPT0iXSwKCiAgICAicG9zdGNzcyI6IFsicG9zdGNzc0A4LjUuMTUiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAibmFub2lkIjogIl4zLjMuMTIiLCAicGljb2NvbG9ycyI6ICJeMS4xLjEiLCAic291cmNlLW1hcC1qcyI6ICJeMS4yLjEiIH0gfSwgInNoYTUxMi1GZlI4c2pkNGVtMlQ2ZmIzSTJNd0FKVTdIV1ZNcjl6YmErZW5tUWVlV0ZmQ2JtK1VPQy8wWDREUzhYdHBVVE13V01HYmpLWVA3eGpmTmVrenlHbUIzQT09Il0sCgogICAgInJlYWN0IjogWyJyZWFjdEAxOS4yLjYiLCAiIiwge30sICJzaGE1MTItc2ZXR0dmYXZpMHhyOFBnMHNWc3lITUFPemlWWUtnUExOclM3aWcraXZNTmIzd2JDQnczS3h0ZmxzR0JBd0QzZ1lRbEUvQUVac1RMZ1RvUnJTQ2piMFE9PSJdLAoKICAgICJyZWFjdC1kYXktcGlja2VyIjogWyJyZWFjdC1kYXktcGlja2VyQDkuMTQuMCIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAZGF0ZS1mbnMvdHoiOiAiXjEuNC4xIiwgIkB0YWJieV9haS9oaWpyaS1jb252ZXJ0ZXIiOiAiMS4wLjUiLCAiZGF0ZS1mbnMiOiAiXjQuMS4wIiwgImRhdGUtZm5zLWphbGFsaSI6ICI0LjEuMC0wIiB9LCAicGVlckRlcGVuZGVuY2llcyI6IHsgInJlYWN0IjogIj49MTYuOC4wIiB9IH0sICJzaGE1MTItdEJhb0RXalB3ZTBNNXBHcnVtNEgwU1I2THlrK0JPOW9IbnA5SmJLcEdLVzJtbHJhTlBnUDlCTWZzZzVwV3B3cnNzQVJtZXFrN1lCbDJvWHV0WlRhSEE9PSJdLAoKICAgICJyZWFjdC1kb20iOiBbInJlYWN0LWRvbUAxOS4yLjYiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAic2NoZWR1bGVyIjogIl4wLjI3LjAiIH0sICJwZWVyRGVwZW5kZW5jaWVzIjogeyAicmVhY3QiOiAiXjE5LjIuNiIgfSB9LCAic2hhNTEyLTBwck1JK2h2QmJQanNXbnhETHhsQ0d5TThQTjZVdVdqRVVDWW1aaE82N3hJVjlYYXNhL3IvdkRucStYeXE0TG8yN2c4UVNiTzVZekFSdTBEMVNwczNnPT0iXSwKCiAgICAicmVxdWlyZS1kaXJlY3RvcnkiOiBbInJlcXVpcmUtZGlyZWN0b3J5QDIuMS4xIiwgIiIsIHt9LCAic2hhNTEyLWZHeEVJNyt3c0c5eHJ2ZGpzcmxtTDIyT01UVGlIUndBTXJvaUVlTWdxOGd6b0xDL1BRcjdSc1JEU1RMVWcvYlpBWnRGK1RWSWtIYzYvNFJJS3J1aStRPT0iXSwKCiAgICAicmVzZWxlY3QiOiBbInJlc2VsZWN0QDUuMi4wIiwgIiIsIHt9LCAic2hhNTEyLUFnWjNVT1ptM1luZGZySjRPWWpnclQ3Ym1DbS8xaXFranZFZkgvb1lqemg2UEQycXc0UXVUM2pqblhJcnBkdDRNVHBNWGNsTVQzbFhibVJZK1hSYWt3PT0iXSwKCiAgICAicm9sbGRvd24iOiBbInJvbGxkb3duQDEuMC4xIiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIkBveGMtcHJvamVjdC90eXBlcyI6ICI9MC4xMzAuMCIsICJAcm9sbGRvd24vcGx1Z2ludXRpbHMiOiAiXjEuMC4wIiB9LCAib3B0aW9uYWxEZXBlbmRlbmNpZXMiOiB7ICJAcm9sbGRvd24vYmluZGluZy1hbmRyb2lkLWFybTY0IjogIjEuMC4xIiwgIkByb2xsZG93bi9iaW5kaW5nLWRhcndpbi1hcm02NCI6ICIxLjAuMSIsICJAcm9sbGRvd24vYmluZGluZy1kYXJ3aW4teDY0IjogIjEuMC4xIiwgIkByb2xsZG93bi9iaW5kaW5nLWZyZWVic2QteDY0IjogIjEuMC4xIiwgIkByb2xsZG93bi9iaW5kaW5nLWxpbnV4LWFybS1nbnVlYWJpaGYiOiAiMS4wLjEiLCAiQHJvbGxkb3duL2JpbmRpbmctbGludXgtYXJtNjQtZ251IjogIjEuMC4xIiwgIkByb2xsZG93bi9iaW5kaW5nLWxpbnV4LWFybTY0LW11c2wiOiAiMS4wLjEiLCAiQHJvbGxkb3duL2JpbmRpbmctbGludXgtcHBjNjQtZ251IjogIjEuMC4xIiwgIkByb2xsZG93bi9iaW5kaW5nLWxpbnV4LXMzOTB4LWdudSI6ICIxLjAuMSIsICJAcm9sbGRvd24vYmluZGluZy1saW51eC14NjQtZ251IjogIjEuMC4xIiwgIkByb2xsZG93bi9iaW5kaW5nLWxpbnV4LXg2NC1tdXNsIjogIjEuMC4xIiwgIkByb2xsZG93bi9iaW5kaW5nLW9wZW5oYXJtb255LWFybTY0IjogIjEuMC4xIiwgIkByb2xsZG93bi9iaW5kaW5nLXdhc20zMi13YXNpIjogIjEuMC4xIiwgIkByb2xsZG93bi9iaW5kaW5nLXdpbjMyLWFybTY0LW1zdmMiOiAiMS4wLjEiLCAiQHJvbGxkb3duL2JpbmRpbmctd2luMzIteDY0LW1zdmMiOiAiMS4wLjEiIH0sICJiaW4iOiB7ICJyb2xsZG93biI6ICJiaW4vY2xpLm1qcyIgfSB9LCAic2hhNTEyLVgwS1FIbGpObkVrV05xcWl6OXpKckd1bmgxQjBIZ094TFh2bkZwQ09jYWR6Y3k1cW9oWjN0cU1FVWcwMHZuY29Sb3ZYdUszWnFDVDlLbm5Lem9JbkZRPT0iXSwKCiAgICAicnhqcyI6IFsicnhqc0A3LjguMiIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJ0c2xpYiI6ICJeMi4xLjAiIH0gfSwgInNoYTUxMi1kaEtmOTAzVS9QUVpZNmJvTk50QUdkV2JHODVXQWJqVC8xeFlvWklDN0ZBWTB5V2FwT0JRVnNWckRsNThXODYvL2UxVnBNTkJ0UlY0TWFYZmRNeVNGQT09Il0sCgogICAgInNjaGVkdWxlciI6IFsic2NoZWR1bGVyQDAuMjcuMCIsICIiLCB7fSwgInNoYTUxMi1lTnYrV3JWYkt1MWYzdmJZSlQveHRpRjVzeUE1SFBJTXRmOUlnWS9uS2cwc1dxekFVRXZxWS94bTdPY1pjL3FhZkx4L2lPOUZnT21lU0FwNHY1dGkvUT09Il0sCgogICAgInNlbXZlciI6IFsic2VtdmVyQDcuOC4wIiwgIiIsIHsgImJpbiI6IHsgInNlbXZlciI6ICJiaW4vc2VtdmVyLmpzIiB9IH0sICJzaGE1MTItQWNNN2RWLzV1bDRFZWtvUTI5QWdtNXZyaThKTnFSeWozOW8wcXBYNnZERjJHWnJ0dXRabDVSd2dEMVhuWmppVEFmbmNzSmhNSTQ4UVFIM3NOODdZTkE9PSJdLAoKICAgICJzaGFycCI6IFsic2hhcnBAMC4zNC41IiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIkBpbWcvY29sb3VyIjogIl4xLjAuMCIsICJkZXRlY3QtbGliYyI6ICJeMi4xLjIiLCAic2VtdmVyIjogIl43LjcuMyIgfSwgIm9wdGlvbmFsRGVwZW5kZW5jaWVzIjogeyAiQGltZy9zaGFycC1kYXJ3aW4tYXJtNjQiOiAiMC4zNC41IiwgIkBpbWcvc2hhcnAtZGFyd2luLXg2NCI6ICIwLjM0LjUiLCAiQGltZy9zaGFycC1saWJ2aXBzLWRhcndpbi1hcm02NCI6ICIxLjIuNCIsICJAaW1nL3NoYXJwLWxpYnZpcHMtZGFyd2luLXg2NCI6ICIxLjIuNCIsICJAaW1nL3NoYXJwLWxpYnZpcHMtbGludXgtYXJtIjogIjEuMi40IiwgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC1hcm02NCI6ICIxLjIuNCIsICJAaW1nL3NoYXJwLWxpYnZpcHMtbGludXgtcHBjNjQiOiAiMS4yLjQiLCAiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4LXJpc2N2NjQiOiAiMS4yLjQiLCAiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4LXMzOTB4IjogIjEuMi40IiwgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC14NjQiOiAiMS4yLjQiLCAiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4bXVzbC1hcm02NCI6ICIxLjIuNCIsICJAaW1nL3NoYXJwLWxpYnZpcHMtbGludXhtdXNsLXg2NCI6ICIxLjIuNCIsICJAaW1nL3NoYXJwLWxpbnV4LWFybSI6ICIwLjM0LjUiLCAiQGltZy9zaGFycC1saW51eC1hcm02NCI6ICIwLjM0LjUiLCAiQGltZy9zaGFycC1saW51eC1wcGM2NCI6ICIwLjM0LjUiLCAiQGltZy9zaGFycC1saW51eC1yaXNjdjY0IjogIjAuMzQuNSIsICJAaW1nL3NoYXJwLWxpbnV4LXMzOTB4IjogIjAuMzQuNSIsICJAaW1nL3NoYXJwLWxpbnV4LXg2NCI6ICIwLjM0LjUiLCAiQGltZy9zaGFycC1saW51eG11c2wtYXJtNjQiOiAiMC4zNC41IiwgIkBpbWcvc2hhcnAtbGludXhtdXNsLXg2NCI6ICIwLjM0LjUiLCAiQGltZy9zaGFycC13YXNtMzIiOiAiMC4zNC41IiwgIkBpbWcvc2hhcnAtd2luMzItYXJtNjQiOiAiMC4zNC41IiwgIkBpbWcvc2hhcnAtd2luMzItaWEzMiI6ICIwLjM0LjUiLCAiQGltZy9zaGFycC13aW4zMi14NjQiOiAiMC4zNC41IiB9IH0sICJzaGE1MTItT3U5STVGdDlXTmNDYlhyVTljTWdQQmNDSzhMaXdMcWNieXdXM3Q0b0RWMzduMXB6cHVOTHNZaUFWOGVPRG5qYnRRbFNEd1oyY1VFZVF6NEU1NEhsdGc9PSJdLAoKICAgICJzaGVsbC1xdW90ZSI6IFsic2hlbGwtcXVvdGVAMS44LjMiLCAiIiwge30sICJzaGE1MTItT2JtbklGNGhYTmcxQnFobkhtZ2JERVRGOGRMUENnZ1pXQmprUWZoWnBic3pabll1cjVEVWxqVGNDSGlpNUxDM0o1RTB5ZU8vMUxJTXlIK1V2SFFneXc9PSJdLAoKICAgICJzaWdpbmZvIjogWyJzaWdpbmZvQDIuMC4wIiwgIiIsIHt9LCAic2hhNTEyLXlieDBXTzEvOGJTQkxFV1hadkVkN2dNVzNTbjNKRmxXM1R2WDFuUkViRExSTlFOYWVOTjhXSzBtZUJ3UGRBYU9JN1R0UlJSSm4vRXMxemhyckNIdTdnPT0iXSwKCiAgICAic291cmNlLW1hcC1qcyI6IFsic291cmNlLW1hcC1qc0AxLjIuMSIsICIiLCB7fSwgInNoYTUxMi1VWFdNS2hMT3dWS2I3MjhJVXRRUFh4ZllVK3VzZHlidFVySy84dUdFOENRTXZyaE9wd3Z6REJ3ajBRaFNMN01RYzd2SXNJU0JHOFZROCtJRFF4cGZRQT09Il0sCgogICAgInN0YWNrYmFjayI6IFsic3RhY2tiYWNrQDAuMC4yIiwgIiIsIHt9LCAic2hhNTEyLTFYTUpFNWZRbzFqR0g2WS83ZWJud1BPQkVrSUVuVDRRRjMyZDVSMStWWGRYdmVNMElCTUp0OHpmYXhYMVAzUWhWd3JZZSs1NzYramtBTnRTUzJtQmJ3PT0iXSwKCiAgICAic3RkLWVudiI6IFsic3RkLWVudkA0LjEuMCIsICIiLCB7fSwgInNoYTUxMi1ScTd5YmNYMlJ1QzU1cjlvYVBWRVc3L3h1M3RqOHU0R2VCWUhCV0N5Y2hGdHpNSXI4NkE3ZTNQUEVCUFQzN3NIU3RLWDMrVGlYL0ZyL0FDbUpMVmxMUT09Il0sCgogICAgInN0cmluZy13aWR0aCI6IFsic3RyaW5nLXdpZHRoQDQuMi4zIiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgImVtb2ppLXJlZ2V4IjogIl44LjAuMCIsICJpcy1mdWxsd2lkdGgtY29kZS1wb2ludCI6ICJeMy4wLjAiLCAic3RyaXAtYW5zaSI6ICJeNi4wLjEiIH0gfSwgInNoYTUxMi13S3lRUlFwakowc0lwNjJFclNaZEdzak1KV3NhcDVvUk5paEhodTZHN0pWTy85aklCNlV5ZXZMK3RYdU9xcm5nOGovY3hLVFd5V1V3dlNUcmlpWnovZz09Il0sCgogICAgInN0cmlwLWFuc2kiOiBbInN0cmlwLWFuc2lANi4wLjEiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiYW5zaS1yZWdleCI6ICJeNS4wLjEiIH0gfSwgInNoYTUxMi1ZMzhWUFNIY3FrRnJDcEZuUTl2dVNYbXF1dXY1b1hPS3BHZVQ2YUdycjNvM0djOUFsVmE2SkJmVVNPQ25ieEdHWkYrLzBvb0k3S3JQdVVTenRVZFU1QT09Il0sCgogICAgInN0eWxlZC1qc3giOiBbInN0eWxlZC1qc3hANS4xLjYiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiY2xpZW50LW9ubHkiOiAiMC4wLjEiIH0sICJwZWVyRGVwZW5kZW5jaWVzIjogeyAicmVhY3QiOiAiPj0gMTYuOC4wIHx8IDE3LngueCB8fCBeMTguMC4wLTAgfHwgXjE5LjAuMC0wIiB9IH0sICJzaGE1MTItcVNWeURUZU1vdGR2UVlvSFdMTkd3UkZKSEMraStadmRCUllvc09GZ0MrV2cxdng0ZnJOMi9SRy9OQTdTWXFxdktOTGYzOVAyTFNSQTJwdTZuMFhZWkE9PSJdLAoKICAgICJzdXBwb3J0cy1jb2xvciI6IFsic3VwcG9ydHMtY29sb3JAOC4xLjEiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiaGFzLWZsYWciOiAiXjQuMC4wIiB9IH0sICJzaGE1MTItTXBVRU4yT29kdFV6eHZLUWw3MmNVRjdSUTVFaUhzR3ZTc1ZHMGlhOWM1UmJXR0wyQ0k0QzdFcFBTOFVUQklwbG5selppTnVWNTZ3K0Z1Tnh5M3R5MlE9PSJdLAoKICAgICJ0YWlsd2luZC1tZXJnZSI6IFsidGFpbHdpbmQtbWVyZ2VAMy42LjAiLCAiIiwge30sICJzaGE1MTItdXhMN3FBVlFyaXFSUVBBeUszcGo2NlZxc2tXcW9aMzdQVzk0andPVHdOZnEvejlveXUxVitlcXJacXRSMitmQ2lYZFlPWmUvTW9kdDhHdHZxTnp1K3c9PSJdLAoKICAgICJ0YWlsd2luZGNzcyI6IFsidGFpbHdpbmRjc3NANC4zLjAiLCAiIiwge30sICJzaGE1MTIteTZueE1HQjFuTVc5UjZrOTZlNWdkSUZ6Y2ZML2dUSlJOYXFHZXMxWXZrTG5QVlh6V2dicUZGMnlMQzBUOEc3NzRuMjRjeDNQZThYcktvbmlDT0FIK1E9PSJdLAoKICAgICJ0YXBhYmxlIjogWyJ0YXBhYmxlQDIuMy4zIiwgIiIsIHt9LCAic2hhNTEyLXV4Yy96cHFGZzZ4N0M4dk9FN2xoNkxiZGE4ZUVMOXptVm0vUExlVFBCUmhoMXhDZ2RXYVErSjFDVWllR3BJZm0ySGR0c1VwUnYrSHNoaWFzQk1jYzZBPT0iXSwKCiAgICAidGlueWJlbmNoIjogWyJ0aW55YmVuY2hAMi45LjAiLCAiIiwge30sICJzaGE1MTItMCtEVXZxV01WYWxMbWhhNmxyNGtEOGlBTUsxSHpWMC9hS25DdFdiOXY5NjQxVG5QL01GYjdQYzJieG94UWpUWEFFcnJ5WFZnVU9mdjJZcU5sbHFHZWc9PSJdLAoKICAgICJ0aW55ZXhlYyI6IFsidGlueWV4ZWNAMS4xLjIiLCAiIiwge30sICJzaGE1MTItZEFxU3FFL1JhYnBCS0k4K2gyNkdmTHE2VmIzSlZYczMwWFlRamRNamFqL2MydFM4SVlZTWJJelA1OTlLdFJqN2M1Ny93WUFwYjNRamdSZ1htckN1a0E9PSJdLAoKICAgICJ0aW55Z2xvYmJ5IjogWyJ0aW55Z2xvYmJ5QDAuMi4xNiIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJmZGlyIjogIl42LjUuMCIsICJwaWNvbWF0Y2giOiAiXjQuMC40IiB9IH0sICJzaGE1MTItcG45OVZob0FDWVI4bkZIaHhxaXgrdXZzYlhpbmVBYXNXbTVvalhvTjh4RXdLNUtkMy9UcmhObjF3Qnl1RDUyVXhXUkx5OHB1K2tSTW5pRWk2RXE5Wmc9PSJdLAoKICAgICJ0aW55cmFpbmJvdyI6IFsidGlueXJhaW5ib3dAMy4xLjAiLCAiIiwge30sICJzaGE1MTItQmYrSUxtQmdyZXRVcmRKeHpYTTBTZ1hMWjNYZmlhVXVPai9JS1FIdVRYaXArMDVYbit1eUVZZFZnMGtZRGlwVEJjTHJDVnlVekFQejdRbUFyYjBtbXc9PSJdLAoKICAgICJ0cmVlLWtpbGwiOiBbInRyZWUta2lsbEAxLjIuMiIsICIiLCB7ICJiaW4iOiB7ICJ0cmVlLWtpbGwiOiAiY2xpLmpzIiB9IH0sICJzaGE1MTItTDBPcnBpOHFHcFJHLy9OZCtIOTB2RkIrM2lIbnVlMXpTU0dtTk9PQ2gxR0xKN3JVS1Z3VjJIdmlqcGhHUVMyVW1oVVpld1M5Vmd2eFlJZGdyK2ZHMUE9PSJdLAoKICAgICJ0c2xpYiI6IFsidHNsaWJAMi44LjEiLCAiIiwge30sICJzaGE1MTItb0pGdTk0SFFiK0tWZHVTVVFMN3ducG1xbmZtTHNPQS9uQWg2YjZFSDB3Q0VvSzAvbVBlWFU2YzN3S0RWODNNa091SFBSSHRTWEtLVTk5SUJhelMvMnc9PSJdLAoKICAgICJ0eXBlc2NyaXB0IjogWyJ0eXBlc2NyaXB0QDUuOS4zIiwgIiIsIHsgImJpbiI6IHsgInRzYyI6ICJiaW4vdHNjIiwgInRzc2VydmVyIjogImJpbi90c3NlcnZlciIgfSB9LCAic2hhNTEyLWpsMXZaelBEaW5McjllVXQzSi90N1Y2RmdORXc5UWp2QlBkeXN6OUtmUURENDFmUXJDMlk0dktRZGlhVXBGVDRiWGxiMVJIaExwcDh3dG02TTVUZ1N3PT0iXSwKCiAgICAidW5kaWNpLXR5cGVzIjogWyJ1bmRpY2ktdHlwZXNANi4yMS4wIiwgIiIsIHt9LCAic2hhNTEyLWl3RFpxZzBRQUdyZzlSYXY1SDRuME02NGMzbWtSNTljSjZ3UXArN0M0bkkwZ3NtRXhhZWRhWUxOTzQ0ZVQ0QXRCQndqYlRpR1BNbHQyTWQwVDlIOUpRPT0iXSwKCiAgICAidXNlLXN5bmMtZXh0ZXJuYWwtc3RvcmUiOiBbInVzZS1zeW5jLWV4dGVybmFsLXN0b3JlQDEuNi4wIiwgIiIsIHsgInBlZXJEZXBlbmRlbmNpZXMiOiB7ICJyZWFjdCI6ICJeMTYuOC4wIHx8IF4xNy4wLjAgfHwgXjE4LjAuMCB8fCBeMTkuMC4wIiB9IH0sICJzaGE1MTItUHA2R1N3R1AvTnJQSXJ4VkZBSWtPUWV5dzhsRmVuT0hpalFXa1VUckR2ckY0QUxxeWxQMkMvS0NrZVM5ZHBVTTNLdllSUWhuYTV2dDdJTDk1K1pROXc9PSJdLAoKICAgICJ2aXRlIjogWyJ2aXRlQDguMC4xMyIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJsaWdodG5pbmdjc3MiOiAiXjEuMzIuMCIsICJwaWNvbWF0Y2giOiAiXjQuMC40IiwgInBvc3Rjc3MiOiAiXjguNS4xNCIsICJyb2xsZG93biI6ICIxLjAuMSIsICJ0aW55Z2xvYmJ5IjogIl4wLjIuMTYiIH0sICJvcHRpb25hbERlcGVuZGVuY2llcyI6IHsgImZzZXZlbnRzIjogIn4yLjMuMyIgfSwgInBlZXJEZXBlbmRlbmNpZXMiOiB7ICJAdHlwZXMvbm9kZSI6ICJeMjAuMTkuMCB8fCA+PTIyLjEyLjAiLCAiQHZpdGVqcy9kZXZ0b29scyI6ICJeMC4xLjE4IiwgImVzYnVpbGQiOiAiXjAuMjcuMCB8fCBeMC4yOC4wIiwgImppdGkiOiAiPj0xLjIxLjAiLCAibGVzcyI6ICJeNC4wLjAiLCAic2FzcyI6ICJeMS43MC4wIiwgInNhc3MtZW1iZWRkZWQiOiAiXjEuNzAuMCIsICJzdHlsdXMiOiAiPj0wLjU0LjgiLCAic3VnYXJzcyI6ICJeNS4wLjAiLCAidGVyc2VyIjogIl41LjE2LjAiLCAidHN4IjogIl40LjguMSIsICJ5YW1sIjogIl4yLjQuMiIgfSwgIm9wdGlvbmFsUGVlcnMiOiBbIkB0eXBlcy9ub2RlIiwgIkB2aXRlanMvZGV2dG9vbHMiLCAiZXNidWlsZCIsICJqaXRpIiwgImxlc3MiLCAic2FzcyIsICJzYXNzLWVtYmVkZGVkIiwgInN0eWx1cyIsICJzdWdhcnNzIiwgInRlcnNlciIsICJ0c3giLCAieWFtbCJdLCAiYmluIjogeyAidml0ZSI6ICJiaW4vdml0ZS5qcyIgfSB9LCAic2hhNTEyLU1GdGpCWWd6bVN4bWdBNFJBZmpJeVhXcEdlMW9BTG5qZ1VUenpWN1FMeC9US3hDemp0TUg2RmQ5L2VWSys1RmcxcU5vejVWQXdzbU1zL05vZnJtSnZ3PT0iXSwKCiAgICAidml0ZXN0IjogWyJ2aXRlc3RANC4xLjYiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiQHZpdGVzdC9leHBlY3QiOiAiNC4xLjYiLCAiQHZpdGVzdC9tb2NrZXIiOiAiNC4xLjYiLCAiQHZpdGVzdC9wcmV0dHktZm9ybWF0IjogIjQuMS42IiwgIkB2aXRlc3QvcnVubmVyIjogIjQuMS42IiwgIkB2aXRlc3Qvc25hcHNob3QiOiAiNC4xLjYiLCAiQHZpdGVzdC9zcHkiOiAiNC4xLjYiLCAiQHZpdGVzdC91dGlscyI6ICI0LjEuNiIsICJlcy1tb2R1bGUtbGV4ZXIiOiAiXjIuMC4wIiwgImV4cGVjdC10eXBlIjogIl4xLjMuMCIsICJtYWdpYy1zdHJpbmciOiAiXjAuMzAuMjEiLCAib2J1ZyI6ICJeMi4xLjEiLCAicGF0aGUiOiAiXjIuMC4zIiwgInBpY29tYXRjaCI6ICJeNC4wLjMiLCAic3RkLWVudiI6ICJeNC4wLjAtcmMuMSIsICJ0aW55YmVuY2giOiAiXjIuOS4wIiwgInRpbnlleGVjIjogIl4xLjAuMiIsICJ0aW55Z2xvYmJ5IjogIl4wLjIuMTUiLCAidGlueXJhaW5ib3ciOiAiXjMuMS4wIiwgInZpdGUiOiAiXjYuMC4wIHx8IF43LjAuMCB8fCBeOC4wLjAiLCAid2h5LWlzLW5vZGUtcnVubmluZyI6ICJeMi4zLjAiIH0sICJwZWVyRGVwZW5kZW5jaWVzIjogeyAiQGVkZ2UtcnVudGltZS92bSI6ICIqIiwgIkBvcGVudGVsZW1ldHJ5L2FwaSI6ICJeMS45LjAiLCAiQHR5cGVzL25vZGUiOiAiXjIwLjAuMCB8fCBeMjIuMC4wIHx8ID49MjQuMC4wIiwgIkB2aXRlc3QvYnJvd3Nlci1wbGF5d3JpZ2h0IjogIjQuMS42IiwgIkB2aXRlc3QvYnJvd3Nlci1wcmV2aWV3IjogIjQuMS42IiwgIkB2aXRlc3QvYnJvd3Nlci13ZWJkcml2ZXJpbyI6ICI0LjEuNiIsICJAdml0ZXN0L2NvdmVyYWdlLWlzdGFuYnVsIjogIjQuMS42IiwgIkB2aXRlc3QvY292ZXJhZ2UtdjgiOiAiNC4xLjYiLCAiQHZpdGVzdC91aSI6ICI0LjEuNiIsICJoYXBweS1kb20iOiAiKiIsICJqc2RvbSI6ICIqIiB9LCAib3B0aW9uYWxQZWVycyI6IFsiQGVkZ2UtcnVudGltZS92bSIsICJAb3BlbnRlbGVtZXRyeS9hcGkiLCAiQHR5cGVzL25vZGUiLCAiQHZpdGVzdC9icm93c2VyLXBsYXl3cmlnaHQiLCAiQHZpdGVzdC9icm93c2VyLXByZXZpZXciLCAiQHZpdGVzdC9icm93c2VyLXdlYmRyaXZlcmlvIiwgIkB2aXRlc3QvY292ZXJhZ2UtaXN0YW5idWwiLCAiQHZpdGVzdC9jb3ZlcmFnZS12OCIsICJAdml0ZXN0L3VpIiwgImhhcHB5LWRvbSIsICJqc2RvbSJdLCAiYmluIjogeyAidml0ZXN0IjogInZpdGVzdC5tanMiIH0gfSwgInNoYTUxMi02bHZqYlMzcDliNENyZENtZ3V6YmgyLzR1b1hoR0UycTcxUjRPWDVzcUY5UjFibzlYZDZmR3JNQWZ2cDV3bkN6bEJuRlZkQ09wNm9udVRRVmJvOGlVUT09Il0sCgogICAgIndoeS1pcy1ub2RlLXJ1bm5pbmciOiBbIndoeS1pcy1ub2RlLXJ1bm5pbmdAMi4zLjAiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAic2lnaW5mbyI6ICJeMi4wLjAiLCAic3RhY2tiYWNrIjogIjAuMC4yIiB9LCAiYmluIjogeyAid2h5LWlzLW5vZGUtcnVubmluZyI6ICJjbGkuanMiIH0gfSwgInNoYTUxMi1oVXJtYVdCZFZEY3h2WXFueWgwOXp1bkt6Uk9XamJaVGlOeThkQkVqa1M3ZWhFRFFpYlhKN1h2bG10Ynd1VGNsVWlJeU4rQ3lYUUQ0Vm1rbzhmTm04dz09Il0sCgogICAgIndyYXAtYW5zaSI6IFsid3JhcC1hbnNpQDcuMC4wIiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgImFuc2ktc3R5bGVzIjogIl40LjAuMCIsICJzdHJpbmctd2lkdGgiOiAiXjQuMS4wIiwgInN0cmlwLWFuc2kiOiAiXjYuMC4wIiB9IH0sICJzaGE1MTItWVZHSWoya2FtTFNUeHc2TnNaam9CeGZTd3NuMHljZGVzbWM0cCtRMjFjNXpQdVoxcGwrTmZ4VmR4UHRkSHZtTlZPUTZYU1lHNEFVdHl0L0ZpN0QxNlE9PSJdLAoKICAgICJ5MThuIjogWyJ5MThuQDUuMC44IiwgIiIsIHt9LCAic2hhNTEyLTBwZkZ6ZWdlRFdKSEpJQW1UTFJQMkR3SGpkRjVzN2pvOXR1enRkUXhBaElOQ2R2UyszbkdJTnFQZDAwQXBocUpSLzBMaEFOVVM2Lys3U0NiOThZT2ZBPT0iXSwKCiAgICAieWFyZ3MiOiBbInlhcmdzQDE3LjcuMiIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJjbGl1aSI6ICJeOC4wLjEiLCAiZXNjYWxhZGUiOiAiXjMuMS4xIiwgImdldC1jYWxsZXItZmlsZSI6ICJeMi4wLjUiLCAicmVxdWlyZS1kaXJlY3RvcnkiOiAiXjIuMS4xIiwgInN0cmluZy13aWR0aCI6ICJeNC4yLjMiLCAieTE4biI6ICJeNS4wLjUiLCAieWFyZ3MtcGFyc2VyIjogIl4yMS4xLjEiIH0gfSwgInNoYTUxMi03ZFN6elJRKytDS25OSS9rcktuWVJWN0pLS1BVWE1FaDYxc29hSEtnOW1yV0VoekZXaEZueFB4R2wrNjljRDFPdTYzQzEzTlVQQ25tSWNydnFDdU02dz09Il0sCgogICAgInlhcmdzLXBhcnNlciI6IFsieWFyZ3MtcGFyc2VyQDIxLjEuMSIsICIiLCB7fSwgInNoYTUxMi10VnBzSlc3RGRqZWNBaUZwYklCMWUzcXhJUXNFNk5vUGM1L2VUZHJiYklDNGgwTFZzV2hub2EzZyttMkhjbEJJdWpIenN4WjRWSlZBK0dVdWMyL0xCdz09Il0sCgogICAgInpvZCI6IFsiem9kQDQuNC4zIiwgIiIsIHt9LCAic2hhNTEyLXl0RU5GaklKRmwyVXdZZ2xkZTJqY2hXMkh3bTRHSkZMRGlTWFdkVHJKUUJJTjlGY3lwN240RGh4SkVpV05BSk1WMS9CcVdmVy9ra2c3MVVEY0hKeVRRPT0iXSwKCiAgICAiQHRhaWx3aW5kY3NzL294aWRlLXdhc20zMi13YXNpL0BlbW5hcGkvY29yZSI6IFsiQGVtbmFwaS9jb3JlQDEuMTAuMCIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAZW1uYXBpL3dhc2ktdGhyZWFkcyI6ICIxLjIuMSIsICJ0c2xpYiI6ICJeMi40LjAiIH0sICJidW5kbGVkIjogdHJ1ZSB9LCAic2hhNTEyLXlxNk9rSjRwODJDQWZQbDB1OW1RZWJRSEtQSmtZN1dySXVrMjA1Y1RZblllK2syWjhZQmgxMUZyYlJHL0g2aWhpcnFjYWNPZ2wyQklPOG95TVFMZVh3PT0iXSwKCiAgICAiQHRhaWx3aW5kY3NzL294aWRlLXdhc20zMi13YXNpL0BlbW5hcGkvcnVudGltZSI6IFsiQGVtbmFwaS9ydW50aW1lQDEuMTAuMCIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJ0c2xpYiI6ICJeMi40LjAiIH0sICJidW5kbGVkIjogdHJ1ZSB9LCAic2hhNTEyLWV3dllsazg2eFVvR0kwelFSTnEvbUMrMTZSMVFlRGxLUXkyMUtpM29TWVhOZ0xiNDVHVjFQNkEwTSsvczZueUN1TkRxZTVWcGFZODRCelhHd1Zid0ZBPT0iXSwKCiAgICAiQHRhaWx3aW5kY3NzL294aWRlLXdhc20zMi13YXNpL0BlbW5hcGkvd2FzaS10aHJlYWRzIjogWyJAZW1uYXBpL3dhc2ktdGhyZWFkc0AxLjIuMSIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJ0c2xpYiI6ICJeMi40LjAiIH0sICJidW5kbGVkIjogdHJ1ZSB9LCAic2hhNTEyLXVUSUk3T1lGKy9NZXMvTXJjSU9ZcDV5T3RTTUxCV1NJb0xQcGNnd2lwb2lLYmxpNmszMjJ0Y29Gc3hvSUl4UERxVzAxU1FHQWdrbzRFelppMkJOdjJ3PT0iXSwKCiAgICAiQHRhaWx3aW5kY3NzL294aWRlLXdhc20zMi13YXNpL0BuYXBpLXJzL3dhc20tcnVudGltZSI6IFsiQG5hcGktcnMvd2FzbS1ydW50aW1lQDEuMS40IiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIkB0eWJ5cy93YXNtLXV0aWwiOiAiXjAuMTAuMSIgfSwgInBlZXJEZXBlbmRlbmNpZXMiOiB7ICJAZW1uYXBpL2NvcmUiOiAiXjEuNy4xIiwgIkBlbW5hcGkvcnVudGltZSI6ICJeMS43LjEiIH0sICJidW5kbGVkIjogdHJ1ZSB9LCAic2hhNTEyLTNOUU5OZ0ExWVNsSmIva01IMWlsZEFTUDlIVzcvN2tZblJJMnN6V0phb2ZhUzFoV21iR0k0SCtkMysyMmFHelhYTjlJSituK0dpRlZjR2lwSlAxOG93PT0iXSwKCiAgICAiQHRhaWx3aW5kY3NzL294aWRlLXdhc20zMi13YXNpL0B0eWJ5cy93YXNtLXV0aWwiOiBbIkB0eWJ5cy93YXNtLXV0aWxAMC4xMC4yIiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgInRzbGliIjogIl4yLjQuMCIgfSwgImJ1bmRsZWQiOiB0cnVlIH0sICJzaGE1MTItUm9CdkoyWDB3dUtsV0ZJanJ3ZmZHdzFJcVpIS1FxekljaEthYWRaWmZuTnBzQVlwMm1NMGgzNkp0UENqTkRBSEdnWWV6LzE1dU1CcGZHd2NoaGlNZ2c9PSJdLAoKICAgICJAdGFpbHdpbmRjc3Mvb3hpZGUtd2FzbTMyLXdhc2kvdHNsaWIiOiBbInRzbGliQDIuOC4xIiwgIiIsIHsgImJ1bmRsZWQiOiB0cnVlIH0sICJzaGE1MTItb0pGdTk0SFFiK0tWZHVTVVFMN3ducG1xbmZtTHNPQS9uQWg2YjZFSDB3Q0VvSzAvbVBlWFU2YzN3S0RWODNNa091SFBSSHRTWEtLVTk5SUJhelMvMnc9PSJdLAoKICAgICJjaGFsay9zdXBwb3J0cy1jb2xvciI6IFsic3VwcG9ydHMtY29sb3JANy4yLjAiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiaGFzLWZsYWciOiAiXjQuMC4wIiB9IH0sICJzaGE1MTItcXBDQXZSbDlzdHVPSHZlS3NuN0huY0pSdnY1MDFxSWFjS3pRbE8vK0x3eGM5KzBxMndMeXY0RGZ2dDgwL0RQbjJwcU9Cc0pkRGlvZ1hHUjkrT3Z3Unc9PSJdLAoKICAgICJuZXh0L3Bvc3Rjc3MiOiBbInBvc3Rjc3NAOC40LjMxIiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIm5hbm9pZCI6ICJeMy4zLjYiLCAicGljb2NvbG9ycyI6ICJeMS4wLjAiLCAic291cmNlLW1hcC1qcyI6ICJeMS4wLjIiIH0gfSwgInNoYTUxMi1QUzA4SWJvaWE5bXRzLzJ5Z1YzZUxwWTVnaG5VY2ZMVi9FWFRPVzFFMnFZeEpLR0dCVXROak43NkZZSG5NczM2Um1BUm40MWJDMEFabW4rclIwT1ZwUT09Il0sCiAgfQp9Cg==" }, { "path": ".env.example", "kind": "text", "content": "# Your tenant public key. Get it from the desk's Developers tab.\n# `cpk_live_*` / `cpk_test_*` keys auto-route same-origin requests\n# to hosted Cimplify in both dev and prod. Anything else (`mock-dev`,\n# empty) falls back to the local `cimplify dev` mock.\nNEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev\n\n# Optional. Forces a fixed canonical URL for sitemap.xml, robots.txt,\n# OpenGraph, and llms.txt. Leave unset and the storefront derives the\n# canonical from the request `Host` header automatically.\n# NEXT_PUBLIC_SITE_URL=\n" }], "storefront-fashion": [{ "path": "vitest.config.ts", "kind": "text", "content": 'import { defineConfig } from "vitest/config";\n\nexport default defineConfig({\n test: {\n environment: "node",\n include: ["__tests__/**/*.test.ts"],\n globals: false,\n },\n});\n' }, { "path": "e2e/visual.spec.ts", "kind": "text", "content": 'import { test, expect } from "@playwright/test";\n\n/**\n * Visual regression on the storefront\'s key surfaces. Catches CSS / layout\n * drift that the contract harness can\'t see.\n *\n * Anti-flake notes:\n * - We disable animations (CSS transitions can leave half-frame snapshots).\n * - We mask images (Unsplash / CDN URLs occasionally render slight pixel\n * differences). The container layout is what we\'re regressing on, not\n * image bytes.\n * - Fonts are served by `next/font` and locked at build time, but if you\n * see flake on a fresh CI runner, `await page.evaluate(() => document.fonts.ready)`\n * before the snapshot.\n */\ntest.describe("visual regression", () => {\n test.beforeEach(async ({ page }) => {\n await page.addStyleTag({\n content: `*, *::before, *::after { animation: none !important; transition: none !important; }`,\n });\n });\n\n test("homepage", async ({ page }) => {\n await page.goto("/");\n await page.evaluate(() => document.fonts.ready);\n await expect(page).toHaveScreenshot("homepage.png", {\n fullPage: true,\n mask: [page.locator("img")],\n });\n });\n\n test("shop / catalogue", async ({ page }) => {\n await page.goto("/shop");\n await page.evaluate(() => document.fonts.ready);\n await expect(page).toHaveScreenshot("shop.png", {\n fullPage: true,\n mask: [page.locator("img")],\n });\n });\n\n test("cart drawer after adding a product", async ({ page }) => {\n await page.goto("/products/studio-tee-natural");\n await page.evaluate(() => document.fonts.ready);\n // Click whatever the storefront\'s primary Add-to-Cart button is.\n await page.getByRole("button", { name: /add to cart/i }).first().click();\n // Drawer auto-opens via CartDrawerProvider\'s openOnAdd watcher.\n await expect(page.getByRole("dialog", { name: /cart/i })).toBeVisible();\n await expect(page).toHaveScreenshot("cart-drawer.png", {\n mask: [page.locator("img")],\n });\n });\n});\n' }, { "path": "__tests__/cart-flow.test.ts", "kind": "text", "content": 'import { createCartFlowSuite } from "@cimplify/sdk/testing/suite";\nimport { expect } from "vitest";\nimport { brand } from "../lib/brand";\n\n/**\n * Cart flow suite \u2014 base + per-merchant assertions.\n *\n * The base suite (from `@cimplify/sdk/testing/suite`) exercises the\n * universal contract: empty cart, add, dedupe, remove, businessId\n * round-trip. The `extend` hook below adds Studio FRX-specific\n * invariants \u2014 copy this pattern when a merchant has unique business\n * rules that need their own coverage.\n *\n * \u2022 `getHandle()` returns the suite\'s current `TestClientHandle`\n * (per `it`) so the seed and session are already wired.\n * \u2022 `it` is the vitest API; cases land inside the suite\'s `describe`\n * so its `beforeEach`/`afterEach` hooks apply automatically.\n */\ncreateCartFlowSuite({\n seed: brand.mock.seed,\n businessId: brand.mock.businessId,\n extend: ({ getHandle, it }) => {\n it("Studio FRX prices everything in GHS", async () => {\n // Currency is brand-level (not per-product) \u2014 Product carries the price\n // string only. Catalog completeness here is just a sanity check.\n expect(brand.currency).toBe("GHS");\n const h = getHandle();\n const list = await h.client.catalogue.getProducts();\n if (!list.ok) throw list.error;\n const items = (list.value as unknown as { items?: unknown[] }).items ?? [];\n expect(items.length).toBeGreaterThan(0);\n });\n\n it("every apparel product covers the studio\'s core size run (S/M/L)", async () => {\n const h = getHandle();\n const list = await h.client.catalogue.getProducts();\n if (!list.ok) throw list.error;\n const items = (list.value as unknown as { items?: { id: string; name: string }[] }).items ?? [];\n\n // Apparel only \u2014 caps/socks/totes intentionally lack size variants.\n const apparel = items.filter((p) => /tee|hoodie|jacket|trouser|pant/i.test(p.name));\n expect(apparel.length).toBeGreaterThan(0);\n\n const required = new Set(["S", "M", "L"]);\n for (const product of apparel) {\n const variants = await h.client.catalogue.getVariants(product.id);\n if (!variants.ok) throw variants.error;\n const sizes = new Set(variants.value.map((v) => v.name).filter(Boolean));\n for (const size of required) {\n expect(sizes, `${product.name} missing size ${size}`).toContain(size);\n }\n }\n });\n },\n});\n' }, { "path": "__tests__/brand.test.ts", "kind": "text", "content": 'import { createBrandSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateBrandSuite({ brand });\n' }, { "path": "__tests__/contract.test.ts", "kind": "text", "content": 'import { createContractSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateContractSuite({ seed: brand.mock.seed });\n' }, { "path": "AGENTS.md", "kind": "text", "content": '# AGENTS.md \u2014 Fashion / streetwear storefront template\n\nIf you are an AI agent (Claude, Cursor, Aider, devin, \u2026) working on this storefront, **start here.**\n\n## TL;DR for rebranding\n\n1. **Edit `lib/brand.ts`** \u2014 every visible string lives here.\n2. **Edit `app/globals.css`** \u2014 `@theme { \u2026 }` for palette + radius + font references.\n3. **Edit `.env.local`** \u2014 `NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY` (optional: `NEXT_PUBLIC_SITE_URL`).\n\n## Aesthetic\n\nStreetwear / lookbook-led brands (Nike, Aritzia, Highsnobiety, drop culture):\n\n- **Anton display + Inter** \u2014 uppercase condensed display, clean body sans.\n- **High-contrast palette** \u2014 near-black foreground on near-white background, electric primary (default coral, `oklch(0.7 0.24 30)`).\n- **Sharp corners**: `0.125rem` \u2014 minimal rounding.\n- **Editorial full-bleed hero** with image gradient overlay.\n- **Block-letter header** \u2014 no logo mark, just the shortName in display type.\n- Schema.org `@type` is `Store`.\n\n## Page surface\n\n```\napp/\n page.tsx Multi-section home \u2014 full-bleed editorial hero,\n trust bar, category tiles, promo banner,\n "Just dropped" grid, brand strip, collections,\n studio-collective CTA, best sellers, newsletter\n shop/page.tsx SDK <CataloguePage/> with custom hero\n search/page.tsx Search\n collections/[slug]/page.tsx Collection landing (drops)\n categories/[slug]/page.tsx Category landing\n products/[slug]/page.tsx Full product detail page (Product JSON-LD)\n\n size-guide/page.tsx \u2B50 Fashion-specific: chest/length/shoulder + waist/hip/inseam\n tables, "how to measure" callout\n lookbook/page.tsx \u2B50 Fashion-specific: editorial multi-drop lookbook \u2014\n hero + 3-up tile galleries per drop, "Shop Drop X" CTAs\n\n cart/page.tsx, checkout/page.tsx, orders/[id]/page.tsx\n\n account/page.tsx <CimplifyAccount /> (iframe)\n account/orders/page.tsx <CimplifyAccount section="orders" />\n account/addresses/page.tsx <CimplifyAccount section="addresses" />\n account/settings/page.tsx <CimplifyAccount section="settings" />\n login/page.tsx, signup/page.tsx redirects \u2192 /account\n\n contact/page.tsx, track-order/page.tsx\n\n about/page.tsx, faq/page.tsx\n shipping/page.tsx (DHL Express worldwide), returns/page.tsx (30 days),\n accessibility/page.tsx, terms/page.tsx, privacy/page.tsx\n\n sitemap-page/page.tsx, sitemap.ts, robots.ts, llms.txt/route.ts, opensearch.xml/route.ts\n error.tsx, not-found.tsx\n```\n\n## File \u2194 brand-field map\n\n| File | Reads from `brand` |\n|---|---|\n| `app/layout.tsx` | identity, contact, socials, Store JSON-LD |\n| `app/page.tsx` | `brand.hero`, `brand.trustItems`, `brand.brandStrip`, `brand.promo`, `brand.tradeIn` (= studio-collective copy), `brand.newsletter` |\n| `app/about/page.tsx` | `brand.about` |\n| `app/faq/page.tsx` | `brand.faq` (sizing, drops, returns, customs) |\n| `app/shipping/page.tsx` | `brand.shipping` (worldwide DHL, customs) |\n| `app/returns/page.tsx` | `brand.returns` (30 days, free US/UK/EU) |\n| `app/accessibility/page.tsx` | `brand.accessibility` |\n| `app/terms/page.tsx`, `app/privacy/page.tsx` | `brand.terms`, `brand.privacy` |\n| `app/contact/page.tsx` | `brand.contactPage`, `brand.contact` |\n| `app/track-order/page.tsx` | `brand.trackOrder` |\n| `app/account/*/page.tsx` | `brand.account` (iframe owns UI) |\n| `app/products/[slug]/page.tsx` | `brand.name`, `brand.currency` (Product JSON-LD) |\n| `app/size-guide/page.tsx` | currently has its size charts inlined \u2014 hoist to `brand.sizeGuide` if you want agents to edit them |\n| `app/lookbook/page.tsx` | currently has the lookbook entries inlined (image URLs + drop names) \u2014 hoist to `brand.lookbook` for agent editing |\n| `app/llms.txt/route.ts` | `brand.llms`, contact, currency |\n| `components/header.tsx`, `footer.tsx` | `brand.header`, `brand.footer`, `brand.contact`, `brand.socials` |\n| `components/promo-banner.tsx`, `trade-in-cta.tsx`, `brand-marquee.tsx`, `trust-bar.tsx`, `newsletter.tsx` | corresponding optional sections in `brand` |\n\n## Fashion-specific notes\n\n- **Hero is full-bleed editorial.** Replace `HERO_FALLBACK_IMAGE` in `app/page.tsx` with the merchant\'s drop campaign shot for maximum impact.\n- **Drop badges and "limited run" copy** drive urgency. Keep `brand.hero.badge` and `brand.promo.badge` short and time-bounded.\n- **`brand.tradeIn`** is repurposed as the "Studio collective" / membership programme \u2014 three-step access flow.\n- **Product detail uses the page-driven model** (`/products/[slug]`) \u2014 fashion is a consideration purchase (sizing, fit, fabric details), not impulse.\n- **`brandStrip.headline` is `"Stocked at"`** with editorial publication wordmarks (Vogue, Highsnobiety, etc.) \u2014 typography-led, no logo licensing required.\n\n## Known TODOs\n\n- `app/size-guide/page.tsx` has its size charts inlined as constants. Move them to `brand.sizeGuide` (typed) so agents can edit per-merchant size charts in one file.\n- `app/lookbook/page.tsx` has its drop entries inlined. Move to `brand.lookbook` for agent editing \u2014 each drop becomes `{ drop, title, date, byline, hero, tiles[] }`.\n- Contact form + newsletter fake submits.\n\n## Mock seed\n\nWired to `--seed fashion` (Studio FRX apparel catalogue \u2014 14 products with size variants XS\u20132XL, Drop 04 + Best Sellers collections, full-bleed product imagery served from `static-tmp.cimplify.io`).\n\n## Customizing SDK components\n\nFor anything beyond `lib/brand.ts` + `app/globals.css`, you\'ll likely want to lean on the SDK\'s prebuilt components rather than reinvent. Particularly for **product customization** (variants, add-ons, bundles, composites, services with scheduling), the SDK already gets the price math, variant axis matching, and cart payload contracts right. Default to ejecting and restyling:\n\n```bash\ncimplify add variant-selector # copies into ./components/\ncimplify add cart-drawer\ncimplify add product-page\n```\n\nThen edit the local copy. **Don\'t change the cart payload shape** unless you\'re also touching the SDK\'s mock and the backend lens.\n\nIf you\'re considering a from-scratch rebuild of the customizer, read these SDK source files first to understand the contract you must reproduce:\n- `src/react/product-customizer.tsx` \u2014 top-level state machine + final `onAddToCart` payload assembly\n- `src/react/variant-selector.tsx` \u2014 multi-axis matching via `display_attributes`; default-variant init runs in a `useEffect` and is easy to break\n- `src/react/{add-on,bundle,composite}-selector.tsx` \u2014 group constraints, exclusivity rules, per-component variants\n- `src/mock/domain/cart/{index,pricing}.ts` \u2014 the contract: `computeUnitPrice`, `computeBundlePrice`, `computeCompositePrice` (4 modes), `computeLineKey`\n\nFull ejection + customization rules are in the SDK-level [`AGENTS.md`](../../AGENTS.md) under "Don\'t reinvent product customization".\n\n## Quick start\n\n```bash\nbun install\nbun dev\n```\n\nOpen <http://localhost:3000>.\n' }, { "path": "postcss.config.mjs", "kind": "text", "content": 'const config = {\n plugins: {\n "@tailwindcss/postcss": {},\n },\n};\n\nexport default config;\n' }, { "path": "components/trade-in-cta.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport { brand } from "@/lib/brand";\n\nexport function TradeInCta() {\n const t = brand.tradeIn;\n if (!t) return null;\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-14 sm:py-20">\n <div className="grid grid-cols-1 lg:grid-cols-2 gap-8 items-center bg-foreground text-background rounded-3xl p-8 sm:p-12 lg:p-14 overflow-hidden relative">\n <div className="absolute top-0 right-0 w-1/2 h-full opacity-[0.04] [background-image:linear-gradient(135deg,white_1px,transparent_1px)] [background-size:24px_24px] pointer-events-none" />\n <div>\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary-foreground/70 mb-3">\n {t.eyebrow}\n </p>\n <h2 className="text-[clamp(1.75rem,3.5vw,2.5rem)] font-bold m-0 mb-4 -tracking-[0.025em] leading-[1.1]">\n {t.title}\n </h2>\n <p className="text-background/75 leading-relaxed mb-6">{t.body}</p>\n <div className="flex flex-wrap items-center gap-3">\n <Link\n href={t.primaryCtaHref}\n className="inline-flex items-center gap-2 px-5 py-2.5 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors"\n >\n {t.primaryCtaLabel}\n <svg viewBox="0 0 12 12" className="w-3 h-3" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden>\n <path d="M3 6h7m0 0L7 3m3 3L7 9" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n </Link>\n <Link\n href={t.secondaryCtaHref}\n className="inline-flex items-center gap-2 px-5 py-2.5 rounded-md border border-background/25 hover:bg-background/10 transition-colors text-sm font-medium"\n >\n {t.secondaryCtaLabel}\n </Link>\n </div>\n </div>\n <div className="grid grid-cols-3 gap-3">\n {t.steps.map((s) => (\n <div\n key={s.step}\n className="rounded-2xl p-4 sm:p-5 bg-background/5 border border-background/10"\n >\n <p className="text-[10px] font-mono text-primary tabular-nums mb-3">{s.step}</p>\n <p className="text-[13px] sm:text-sm font-semibold mb-1.5 -tracking-[0.015em]">\n {s.title}\n </p>\n <p className="text-[11px] sm:text-xs text-background/65 leading-snug">{s.body}</p>\n </div>\n ))}\n </div>\n </div>\n </section>\n );\n}\n' }, { "path": "components/cart-pill.tsx", "kind": "text", "content": '"use client";\n\nimport { useCartDrawer } from "@cimplify/sdk/react";\nimport { useCartCount } from "@/lib/cart";\n\n/**\n * Cart pill \u2014 dynamic island. Reads the live cart count via the SDK and\n * opens the side cart drawer on click (instead of navigating to /cart).\n * Wrap in `<Suspense fallback={<CartPillSkeleton/>}>` so the cached\n * header chrome streams without blocking on the cart fetch.\n */\nexport function CartPill() {\n const { count } = useCartCount();\n const { open } = useCartDrawer();\n return (\n <button\n type="button"\n onClick={open}\n aria-label={`Open cart, ${count} ${count === 1 ? "item" : "items"}`}\n className="inline-flex items-center gap-1.5 px-4 py-2.5 sm:py-2 rounded-full bg-foreground text-background text-xs font-semibold tracking-wide transition-transform hover:scale-105 cursor-pointer"\n >\n Cart \xB7 {count}\n </button>\n );\n}\n\nexport function CartPillSkeleton() {\n return (\n <span\n aria-hidden\n className="inline-flex items-center gap-1.5 px-4 py-2.5 sm:py-2 rounded-full bg-foreground/80 text-background text-xs font-semibold tracking-wide"\n >\n Cart \xB7 \u2026\n </span>\n );\n}\n' }, { "path": "components/category-tiles.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport Image from "next/image";\nimport type { Category } from "@cimplify/sdk";\n\ninterface CategoryTilesProps {\n categories: Category[];\n}\n\nconst ICONS: Record<string, React.ReactNode> = {\n phones: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-7 h-7" aria-hidden>\n <rect x="6" y="2" width="12" height="20" rx="2.5" />\n <line x1="12" y1="18" x2="12" y2="18" strokeWidth="2.5" strokeLinecap="round" />\n </svg>\n ),\n laptops: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-7 h-7" aria-hidden>\n <rect x="3" y="4" width="18" height="12" rx="1.5" />\n <line x1="2" y1="20" x2="22" y2="20" strokeLinecap="round" />\n </svg>\n ),\n audio: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-7 h-7" aria-hidden>\n <path d="M3 14v-2a9 9 0 0 1 18 0v2" />\n <rect x="2" y="14" width="5" height="6" rx="1.5" />\n <rect x="17" y="14" width="5" height="6" rx="1.5" />\n </svg>\n ),\n accessories: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-7 h-7" aria-hidden>\n <rect x="2" y="6" width="14" height="10" rx="2" />\n <path d="M16 12h4l2 2v0l-2 2h-4" />\n </svg>\n ),\n gaming: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-7 h-7" aria-hidden>\n <path d="M5 7h14a3 3 0 0 1 3 3v4a3 3 0 0 1-3 3h-1l-2-3H8l-2 3H5a3 3 0 0 1-3-3v-4a3 3 0 0 1 3-3z" />\n <line x1="9" y1="11" x2="9" y2="13" strokeLinecap="round" />\n <line x1="8" y1="12" x2="10" y2="12" strokeLinecap="round" />\n <circle cx="15" cy="11" r="0.75" fill="currentColor" />\n <circle cx="17" cy="13" r="0.75" fill="currentColor" />\n </svg>\n ),\n};\n\nexport function CategoryTiles({ categories }: CategoryTilesProps) {\n if (categories.length === 0) return null;\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-14 sm:py-20">\n <div className="flex items-end justify-between gap-6 mb-8">\n <div>\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2">\n Shop the catalogue\n </p>\n <h2 className="text-[clamp(1.75rem,3vw,2.25rem)] font-bold -tracking-[0.025em]">\n Pick your category.\n </h2>\n </div>\n <Link\n href="/shop"\n className="text-sm font-semibold text-primary hover:underline whitespace-nowrap hidden sm:inline-flex items-center gap-1"\n >\n See everything\n <svg viewBox="0 0 12 12" className="w-3 h-3" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden>\n <path d="M3 6h7m0 0L7 3m3 3L7 9" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n </Link>\n </div>\n <div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3 sm:gap-4">\n {categories.map((c, i) => (\n <Link\n key={c.id}\n href={`/categories/${c.slug}`}\n className="group relative overflow-hidden rounded-2xl bg-card border border-border p-5 hover:border-primary hover:-translate-y-0.5 transition-all"\n >\n <div className="flex items-start justify-between mb-12">\n <div className="grid place-items-center w-10 h-10 rounded-lg bg-primary/10 text-primary">\n {ICONS[c.slug] ?? (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-7 h-7" aria-hidden>\n <rect x="3" y="3" width="18" height="18" rx="2" />\n </svg>\n )}\n </div>\n <span className="text-[10px] font-mono text-muted-foreground tabular-nums">\n {String(i + 1).padStart(2, "0")}\n </span>\n </div>\n <p className="text-base font-semibold mb-1 -tracking-[0.015em]">{c.name}</p>\n {c.product_count != null && (\n <p className="text-xs text-muted-foreground">\n {c.product_count} {c.product_count === 1 ? "product" : "products"}\n </p>\n )}\n <span className="absolute right-4 bottom-4 grid place-items-center w-7 h-7 rounded-full bg-foreground text-background opacity-0 group-hover:opacity-100 transition-opacity">\n <svg viewBox="0 0 12 12" className="w-3 h-3" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden>\n <path d="M3 6h7m0 0L7 3m3 3L7 9" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n </span>\n </Link>\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "components/promo-banner.tsx", "kind": "text", "content": `import Link from "next/link";
865
865
  import { brand } from "@/lib/brand";
866
866
 
867
867
  /**
@@ -1529,7 +1529,7 @@ export const brand: Brand = {
1529
1529
  businessId: "bus_studio_frx",
1530
1530
  },
1531
1531
  };
1532
- ` }, { "path": "lib/cimplify-loader.ts", "kind": "text", "content": 'import type { ImageLoader } from "next/image";\n\nimport { assetUrl, isCimplifyAsset } from "@cimplify/sdk";\n\nconst cdnBase = process.env.NEXT_PUBLIC_CIMPLIFY_CDN_URL?.trim() || undefined;\n\nconst cimplifyImageLoader: ImageLoader = ({ src, width, quality }) => {\n if (isCimplifyAsset(src, cdnBase)) {\n return assetUrl(src, {\n base: cdnBase,\n w: width,\n quality,\n format: "auto",\n });\n }\n return src;\n};\n\nexport default cimplifyImageLoader;\n' }, { "path": "lib/site.config.ts", "kind": "text", "content": "/**\n * Canonical absolute URL. The platform overwrites this at build time with the\n * storefront's primary domain; the default only applies to local builds. It's\n * per-deploy config, not per-request, so it stays a static constant \u2014 keeping\n * the root layout prerenderable under `cacheComponents`.\n */\nexport const SITE_URL = \"https://example.com\";\n" }, { "path": "lib/cart.ts", "kind": "text", "content": '"use client";\n\nimport { useCart } from "@cimplify/sdk/react";\n\n/**\n * Reactive cart count for the header pill. Subscribes to the SDK cart\n * state \u2014 updates immediately on add / remove / quantity change.\n */\nexport function useCartCount(): { count: number } {\n const { itemCount } = useCart();\n return { count: itemCount };\n}\n' }, { "path": "lib/site-url.ts", "kind": "text", "content": 'import { SITE_URL } from "./site.config";\n\n/** Canonical absolute URL for this storefront. */\nexport async function getSiteUrl(): Promise<string> {\n return SITE_URL;\n}\n' }, { "path": "metadata.json", "kind": "text", "content": '{\n "id": "fashion",\n "name": "Fashion",\n "tagline": "Editorial multi-drop fashion storefront with lookbooks and size guides.",\n "industry": "fashion",\n "tags": ["fashion", "apparel", "lookbook"],\n "stability": "stable",\n "schemaType": "Store",\n "mock": {\n "seedName": "fashion",\n "seedBusinessId": "bus_studio_frx"\n }\n}\n' }, { "path": "playwright.config.ts", "kind": "text", "content": 'import { defineConfig, devices } from "@playwright/test";\n\n/**\n * Visual regression config \u2014 boots `bun dev` (mock + next), runs against\n * a stable Chromium, snapshots key pages.\n *\n * Run:\n * bun run check:visual # compare against baseline\n * bun run check:visual --update-snapshots # accept new baseline\n *\n * Baselines live in `e2e/__snapshots__/`. Commit them.\n */\nexport default defineConfig({\n testDir: "./e2e",\n fullyParallel: false,\n forbidOnly: Boolean(process.env.CI),\n retries: process.env.CI ? 1 : 0,\n workers: 1,\n reporter: process.env.CI ? "github" : "list",\n\n use: {\n baseURL: "http://localhost:3000",\n trace: "retain-on-failure",\n // Viewport snapshots are noisy across OS / browser builds. Lock here.\n viewport: { width: 1280, height: 800 },\n },\n\n projects: [\n {\n name: "chromium",\n use: { ...devices["Desktop Chrome"], viewport: { width: 1280, height: 800 } },\n },\n ],\n\n expect: {\n // Anti-flake: allow tiny rendering differences (font hinting across runners).\n toHaveScreenshot: { maxDiffPixelRatio: 0.01 },\n },\n\n webServer: {\n command: "bun dev",\n url: "http://localhost:3000",\n reuseExistingServer: !process.env.CI,\n timeout: 120_000,\n stdout: "ignore",\n stderr: "pipe",\n },\n});\n' }, { "path": "tsconfig.json", "kind": "text", "content": '{\n "compilerOptions": {\n "target": "ES2022",\n "lib": ["dom", "dom.iterable", "esnext"],\n "allowJs": false,\n "skipLibCheck": true,\n "strict": true,\n "noEmit": true,\n "esModuleInterop": true,\n "module": "esnext",\n "moduleResolution": "bundler",\n "resolveJsonModule": true,\n "isolatedModules": true,\n "jsx": "preserve",\n "incremental": true,\n "plugins": [{ "name": "next" }],\n "paths": {\n "@/*": ["./*"]\n }\n },\n "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts"],\n "exclude": ["node_modules"]\n}\n' }, { "path": "CLAUDE.md", "kind": "text", "content": "# CLAUDE.md\n\nThis project was scaffolded from a Cimplify storefront template (`cimplify init`).\n\n## Read these first\n\n- **`AGENTS.md`** at the project root \u2014 the file \u2194 `brand.X` field map for *this template's* industry, plus the architectural rules.\n- **`.claude/skills/cimplify-storefront/SKILL.md`** \u2014 the playbook for common tasks (rebrand, add a section, wire a Server Action, deploy, eject a component). Invoke it whenever you're asked to change content, palette, copy, or routing.\n\n## The two-line summary\n\n1. **Edit `lib/brand.ts`** for any visible string.\n2. **Edit `app/globals.css` `@theme` block** for any palette / radius / font change.\n\nThat covers ~95% of what merchants ask for. For anything else, follow the `cimplify-storefront` skill.\n\n## Don't do\n\n- Hardcode strings in pages or components.\n- Disable `cacheComponents: true` in `next.config.ts`.\n- Use `unstable_cache` \u2014 Next 16's canonical primitive is `'use cache'`.\n- Run `bun test` (Bun's `vi` shim is incomplete) \u2014 use `bun run test:run`.\n" }, { "path": "README.md", "kind": "text", "content": "# __STOREFRONT_NAME__\n\nA Cimplify storefront scaffolded from the **retail** template \u2014 Next.js 16 (App Router), React 19, Tailwind v4. Cool navy + electric blue palette, Inter + JetBrains Mono typography, modern electronics-store aesthetic.\n\n## Run\n\n```bash\nbun install\nbun dev\n```\n\nTwo things start in parallel:\n\n- `cimplify-mock --seed retail` \u2014 the Cimplify mock API on `http://127.0.0.1:8787`, seeded with Currents Electronics.\n- `next dev` \u2014 this storefront on `http://localhost:3000`.\n\nOpen the storefront in your browser. Edit `app/page.tsx` to start customising.\n\n## Structure\n\n```\napp/\n layout.tsx # root layout, fonts, providers, header/footer/modal\n page.tsx # home\n shop/page.tsx # full catalogue (SDK <CataloguePage/>)\n collections/[slug]/ # collection landing\n categories/[slug]/ # category landing\n cart/page.tsx # SDK <CartPage/>\n checkout/page.tsx # SDK <CheckoutPage/>\n orders/[id]/page.tsx # post-checkout thank-you\n about, faq, terms, privacy\n globals.css # Tailwind import + theme tokens\ncomponents/\n providers.tsx # CimplifyProvider client wrapper\n header.tsx, footer.tsx, hero.tsx\n store-product-card.tsx # SDK <ProductCard/> wired to URL-driven modal\n product-modal.tsx # ?product=<slug> deep-linkable modal\n collection-strip.tsx # horizontal product strip\n category-grid.tsx # SDK <CategoryGrid/> with router navigation\nlib/\n cart.ts # useCartCount() for the header pill\n```\n\n## Switch the seed\n\nThis template is wired to the `retail` seed. To preview a different industry without re-scaffolding:\n\n```bash\ncimplify-mock --seed restaurant # Mama's Kitchen\ncimplify-mock --seed services # Serene Spa\ncimplify-mock --seed grocery # FreshMart\n```\n\nFor a fresh scaffold with another design altogether:\n\n```bash\ncimplify init my-store --template bakery # warm food/pastry\ncimplify init my-store --template restaurant # coming soon\ncimplify init my-store --template services # coming soon\ncimplify init my-store --template grocery # coming soon\n```\n\n## Go live\n\n```diff\n# .env.local\n- NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev\n+ NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=<your tenant key>\n```\n\nDeploy with `cimplify deploy --prod` after linking the project. See [`cimplify` CLI docs](https://www.cimplify.dev/docs/cli). `next.config.ts` already whitelists the SDK image hosts under `images.remotePatterns`.\n" }, { "path": ".env.example", "kind": "text", "content": "# Your tenant public key. Get it from the desk's Developers tab.\n# `cpk_live_*` / `cpk_test_*` keys auto-route same-origin requests\n# to hosted Cimplify in both dev and prod. Anything else (`mock-dev`,\n# empty) falls back to the local `cimplify dev` mock.\nNEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev\n\n# Optional. Forces a fixed canonical URL for sitemap.xml, robots.txt,\n# OpenGraph, and llms.txt. Leave unset and the storefront derives the\n# canonical from the request `Host` header automatically.\n# NEXT_PUBLIC_SITE_URL=\n" }], "storefront-grocery": [{ "path": "vitest.config.ts", "kind": "text", "content": 'import { defineConfig } from "vitest/config";\n\nexport default defineConfig({\n test: {\n environment: "node",\n include: ["__tests__/**/*.test.ts"],\n globals: false,\n },\n});\n' }, { "path": "__tests__/cart-flow.test.ts", "kind": "text", "content": 'import { createCartFlowSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateCartFlowSuite({ seed: brand.mock.seed, businessId: brand.mock.businessId });\n' }, { "path": "__tests__/brand.test.ts", "kind": "text", "content": 'import { createBrandSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateBrandSuite({ brand });\n' }, { "path": "__tests__/contract.test.ts", "kind": "text", "content": 'import { createContractSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateContractSuite({ seed: brand.mock.seed });\n' }, { "path": "AGENTS.md", "kind": "text", "content": '# AGENTS.md \u2014 Grocery storefront template\n\nIf you are an AI agent (Claude, Cursor, Aider, devin, \u2026) working on this storefront, **start here.**\n\n## TL;DR for rebranding\n\n1. **Edit `lib/brand.ts`** \u2014 every visible string lives here.\n2. **Edit `app/globals.css`** \u2014 `@theme { \u2026 }` for palette + radius.\n3. **Edit `.env.local`** \u2014 set `NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY` (the only required var).\n## Aesthetic\n\n- **Inter sans only** (no serif) \u2014 friendly, readable for dense product grids.\n- **Fresh green + cream** \u2014 vibrant green primary, warm cream background.\n- **Generous radius**: `0.875rem` \u2014 softer than retail.\n- **Bold, slightly heavy** headings.\n- Schema.org `@type` is `GroceryStore`.\n\n## Page surface\n\n```\napp/\n page.tsx Home \u2014 hero ("Free delivery over GH\u20B5150"),\n collection strips, category grid, newsletter\n shop/page.tsx Aisle (SDK <CataloguePage/>)\n search/page.tsx Search\n collections/[slug]/page.tsx Collection landing (e.g. weekly box, deals)\n categories/[slug]/page.tsx Category landing\n\n cart/page.tsx, checkout/page.tsx, orders/[id]/page.tsx\n\n account/page.tsx <CimplifyAccount /> (iframe)\n account/orders/page.tsx <CimplifyAccount section="orders" />\n account/addresses/page.tsx <CimplifyAccount section="addresses" />\n account/settings/page.tsx <CimplifyAccount section="settings" />\n login/page.tsx, signup/page.tsx redirects \u2192 /account\n\n contact/page.tsx, track-order/page.tsx\n\n about/page.tsx, faq/page.tsx\n shipping/page.tsx (delivery + slots), returns/page.tsx (quality guarantee),\n accessibility/page.tsx, terms/page.tsx, privacy/page.tsx\n\n sitemap-page/page.tsx, sitemap.ts, robots.ts, llms.txt/route.ts, opensearch.xml/route.ts\n error.tsx, not-found.tsx\n```\n\n## File \u2194 brand-field map\n\n| File | Reads from `brand` |\n|---|---|\n| `app/layout.tsx` | identity, contact, socials, GroceryStore JSON-LD |\n| `app/page.tsx` | `brand.hero` (free-delivery badge) |\n| `app/about/page.tsx` | `brand.about` |\n| `app/faq/page.tsx` | `brand.faq` (delivery slots, sourcing, subscription boxes) |\n| `app/shipping/page.tsx` | `brand.shipping` (delivery slots + free-over threshold) |\n| `app/returns/page.tsx` | `brand.returns` (quality guarantee) |\n| `app/accessibility/page.tsx` | `brand.accessibility` |\n| `app/terms/page.tsx`, `app/privacy/page.tsx` | `brand.terms`, `brand.privacy` |\n| `app/contact/page.tsx` | `brand.contactPage`, `brand.contact` |\n| `app/track-order/page.tsx` | `brand.trackOrder` |\n| `app/account/*/page.tsx` | `brand.account` (iframe owns UI) |\n| `app/llms.txt/route.ts` | `brand.llms`, contact, currency |\n| `components/header.tsx`, `footer.tsx` | `brand.header`, `brand.footer`, `brand.contact`, `brand.socials` |\n\n## Grocery-specific notes\n\n- **Subscription boxes** are modelled as products with `billing_plans` (renders the SubscriptionCard variant). The basket-box landing lives at `/categories/baskets`.\n- **Weight-based pricing**: SDK supports `quantity_pricing` tiers per product, but most grocery products are sold by unit/kg. Real weight-at-pack pricing happens on the backend pricing rule.\n- Mock seed: `--seed grocery` (FreshMart).\n\n## Known TODOs\n\n- Contact + newsletter fake submits.\n- Hero / category / product imagery is Unsplash placeholder \u2014 replace with merchant photography.\n\n## Customizing SDK components\n\nFor anything beyond `lib/brand.ts` + `app/globals.css`, lean on the SDK\'s prebuilt components rather than reinvent. **Especially for product customization** (variants, add-ons, bundles, composites) \u2014 the SDK already gets price math, axis matching, and cart payload contracts right. Default to ejecting and restyling:\n\n```bash\ncimplify add cart-drawer\ncimplify add add-on-selector\ncimplify add product-page\n```\n\nThen edit the local copy. **Don\'t change the cart payload shape** unless you\'re also touching the SDK mock + backend lens. Full ejection rules and the customizer contract are in the SDK-level [`AGENTS.md`](../../AGENTS.md) \u2192 "Don\'t reinvent product customization".\n\n## Quick start\n\n```bash\nbun install\nbun dev\n```\n\nOpen <http://localhost:3000>.\n' }, { "path": "postcss.config.mjs", "kind": "text", "content": 'const config = {\n plugins: {\n "@tailwindcss/postcss": {},\n },\n};\n\nexport default config;\n' }, { "path": "components/cart-pill.tsx", "kind": "text", "content": '"use client";\n\nimport { useCartDrawer } from "@cimplify/sdk/react";\nimport { useCartCount } from "@/lib/cart";\n\n/**\n * Cart pill \u2014 dynamic island. Reads the live cart count via the SDK and\n * opens the side cart drawer on click (instead of navigating to /cart).\n * Wrap in `<Suspense fallback={<CartPillSkeleton/>}>` so the cached\n * header chrome streams without blocking on the cart fetch.\n */\nexport function CartPill() {\n const { count } = useCartCount();\n const { open } = useCartDrawer();\n return (\n <button\n type="button"\n onClick={open}\n aria-label={`Open cart, ${count} ${count === 1 ? "item" : "items"}`}\n className="inline-flex items-center gap-1.5 px-4 py-2.5 sm:py-2 rounded-full bg-foreground text-background text-xs font-semibold tracking-wide transition-transform hover:scale-105 cursor-pointer"\n >\n Cart \xB7 {count}\n </button>\n );\n}\n\nexport function CartPillSkeleton() {\n return (\n <span\n aria-hidden\n className="inline-flex items-center gap-1.5 px-4 py-2.5 sm:py-2 rounded-full bg-foreground/80 text-background text-xs font-semibold tracking-wide"\n >\n Cart \xB7 \u2026\n </span>\n );\n}\n' }, { "path": "components/collection-strip.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport type { Collection, Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "./store-product-card";\n\ninterface CollectionStripProps {\n collection: Collection;\n products: Product[];\n collectionHref?: string;\n}\n\n/**\n * Horizontal strip of products under a collection title. Cards come from the\n * SDK so every variant (Food, Bundle, Composite, Service, \u2026) renders\n * correctly; clicks open the shared URL-driven product modal.\n */\nexport function CollectionStrip({ collection, products, collectionHref }: CollectionStripProps) {\n if (products.length === 0) return null;\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 pt-12">\n <header className="grid grid-cols-[1fr_auto] items-end gap-4 mb-5">\n <h2 className="font-serif text-[28px] font-semibold m-0">{collection.name}</h2>\n {collectionHref && (\n <Link\n href={collectionHref}\n className="text-[13px] font-semibold text-primary hover:underline"\n >\n See all \u2192\n </Link>\n )}\n {collection.description && (\n <p className="col-span-full m-0 mt-1 text-sm text-muted-foreground">\n {collection.description}\n </p>\n )}\n </header>\n <div className="grid grid-flow-col auto-cols-[minmax(220px,1fr)] gap-4 overflow-x-auto snap-x snap-mandatory pb-2">\n {products.slice(0, 8).map((p) => (\n <div key={p.id} className="snap-start">\n <StoreProductCard product={p} />\n </div>\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "components/hero.tsx", "kind": "text", "content": 'interface HeroProps {\n badge?: string;\n title: string;\n subtitle?: string;\n}\n\nexport function Hero({ badge, title, subtitle }: HeroProps) {\n return (\n <section className="px-6 sm:px-8 py-16 text-center bg-gradient-to-b from-accent to-background">\n {badge && (\n <span className="inline-block mb-4 px-3.5 py-1.5 rounded-full bg-card border border-accent text-accent-foreground text-[11px] font-semibold uppercase tracking-[0.12em]">\n {badge}\n </span>\n )}\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold m-0 -tracking-[0.02em]">\n {title}\n </h1>\n {subtitle && (\n <p className="mx-auto mt-2 max-w-xl text-base text-muted-foreground">\n {subtitle}\n </p>\n )}\n </section>\n );\n}\n' }, { "path": "components/account-iframe.tsx", "kind": "text", "content": '"use client";\n\nimport { CimplifyAccount } from "@cimplify/sdk/react";\n\n/**\n * Cimplify Account portal \u2014 iframe-mounted UI hosted by Cimplify Link.\n * Handles sign-in, sign-up, OTP, addresses, payment methods, sessions,\n * and order history. The iframe owns auth state; we just choose which\n * `section` to land on.\n */\nexport function AccountIframe({ section }: { section?: string }) {\n return <CimplifyAccount section={section} />;\n}\n' }, { "path": "components/policy-page.tsx", "kind": "text", "content": 'import type { BrandPolicySection } from "@/lib/brand";\n\ninterface PolicyShape {\n eyebrow: string;\n title: string;\n lastUpdated?: string;\n sections: BrandPolicySection[];\n}\n\n/**\n * Shared layout for shipping / returns / accessibility / terms / privacy.\n * Reads a `{ eyebrow, title, lastUpdated, sections[] }` block from brand.\n */\nexport function PolicyPage({ policy }: { policy: PolicyShape }) {\n return (\n <article className="max-w-3xl mx-auto px-6 sm:px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {policy.eyebrow}\n </p>\n <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-2 -tracking-[0.02em]">\n {policy.title}\n </h1>\n {policy.lastUpdated && (\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {policy.lastUpdated}\n </p>\n )}\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {policy.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="text-2xl font-semibold mt-0">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "components/header.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport { Suspense } from "react";\nimport { NavLink } from "./nav-link";\nimport { CartPill, CartPillSkeleton } from "./cart-pill";\nimport { MobileNav } from "./mobile-nav";\nimport { brand } from "@/lib/brand";\n\n/**\n * Server-rendered header chrome. Brand mark + nav layout streams from the\n * cache; the active-link styling and live cart count are dynamic islands\n * mounted in their own Suspense boundaries so the chrome never blocks.\n */\nexport function Header() {\n return (\n <header className="sticky top-0 z-30 flex items-center justify-between px-6 sm:px-8 py-4 border-b border-border bg-background/90 backdrop-blur-md">\n <Link href="/" className="flex items-baseline gap-2">\n <span className="font-serif text-[22px] font-semibold -tracking-[0.02em]">\n {brand.shortName}\n </span>\n <span className="hidden sm:inline text-[11px] font-medium uppercase tracking-[0.12em] text-muted-foreground">\n {brand.microTag}\n </span>\n </Link>\n <div className="flex items-center gap-3 sm:gap-6">\n <nav className="hidden sm:flex items-center gap-6">\n {brand.header.nav.map((link) => (\n <Suspense key={link.href} fallback={<NavLinkFallback>{link.label}</NavLinkFallback>}>\n <NavLink href={link.href}>{link.label}</NavLink>\n </Suspense>\n ))}\n </nav>\n <Suspense fallback={<CartPillSkeleton />}>\n <CartPill />\n </Suspense>\n <div className="sm:hidden">\n <MobileNav />\n </div>\n </div>\n </header>\n );\n}\n\nfunction NavLinkFallback({ children }: { children: React.ReactNode }) {\n return (\n <span className="text-[13px] font-medium tracking-wide text-muted-foreground">\n {children}\n </span>\n );\n}\n' }, { "path": "components/product-modal.tsx", "kind": "text", "content": '"use client";\n\nimport { useEffect } from "react";\nimport Image from "next/image";\nimport { useRouter, useSearchParams, usePathname } from "next/navigation";\nimport { ProductSheet, useProduct, useCart } from "@cimplify/sdk/react";\n\n/**\n * URL-driven product modal. Reads `?product=<slug>` and renders the SDK\'s\n * `<ProductSheet/>` \u2014 vertical layout with image-on-top, then header, then\n * the variant/add-on/composite/bundle customizer. Closing the modal clears\n * the search param. Deep-linkable and survives reloads.\n */\nexport function ProductModal() {\n const router = useRouter();\n const pathname = usePathname();\n const searchParams = useSearchParams();\n const slug = searchParams?.get("product") ?? null;\n\n const { product } = useProduct(slug ?? "", { enabled: Boolean(slug) });\n const { addItem } = useCart();\n\n useEffect(() => {\n if (!slug) return;\n const original = document.body.style.overflow;\n document.body.style.overflow = "hidden";\n return () => {\n document.body.style.overflow = original;\n };\n }, [slug]);\n\n useEffect(() => {\n if (!slug) return;\n const onKey = (e: KeyboardEvent) => {\n if (e.key === "Escape") close();\n };\n window.addEventListener("keydown", onKey);\n return () => window.removeEventListener("keydown", onKey);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [slug]);\n\n if (!slug) return null;\n\n function close() {\n const next = new URLSearchParams(searchParams?.toString() ?? "");\n next.delete("product");\n const qs = next.toString();\n router.replace(qs ? `${pathname}?${qs}` : pathname, { scroll: false });\n }\n\n return (\n <div\n role="dialog"\n aria-modal="true"\n onClick={close}\n className="fixed inset-0 z-[100] flex items-end sm:items-center justify-center sm:p-6 bg-foreground/50 backdrop-blur-sm"\n >\n <div\n onClick={(e) => e.stopPropagation()}\n className="relative w-full sm:max-w-lg max-h-[92vh] overflow-auto bg-card sm:rounded-3xl rounded-t-3xl shadow-2xl"\n >\n <button\n onClick={close}\n aria-label="Close product details"\n className="absolute top-4 right-4 z-10 w-9 h-9 rounded-full bg-card border border-border text-sm cursor-pointer transition-colors hover:bg-muted"\n >\n \u2715\n </button>\n {product ? (\n <ProductSheet\n product={product}\n onClose={close}\n onAddToCart={async (p, qty, options) => {\n await addItem(p, qty, options);\n close();\n }}\n renderImage={({ src, alt, className }) => (\n <Image\n src={src}\n alt={alt}\n width={1200}\n height={900}\n className={className}\n style={{ width: "100%", height: "auto", objectFit: "cover" }}\n priority\n />\n )}\n classNames={{\n root: "p-6 sm:p-8 gap-4",\n image: "rounded-2xl overflow-hidden -mx-6 sm:-mx-8 -mt-6 sm:-mt-8 mb-2",\n header: "flex items-baseline justify-between gap-4",\n name: "font-serif text-2xl font-semibold m-0",\n price: "text-lg font-semibold text-primary",\n description: "text-sm text-muted-foreground leading-relaxed",\n customizer: "pt-2",\n }}\n />\n ) : (\n <div className="p-8 text-center text-muted-foreground">Loading\u2026</div>\n )}\n </div>\n </div>\n );\n}\n' }, { "path": "components/mobile-nav.tsx", "kind": "text", "content": '"use client";\n\nimport { useEffect, useState } from "react";\nimport Link from "next/link";\nimport { brand } from "@/lib/brand";\n\n/**\n * Hamburger button + slide-in drawer for narrow viewports. Header hides\n * its inline nav links below `sm` and renders this in their place; the\n * cart pill stays in the header chrome.\n */\nexport function MobileNav() {\n const [open, setOpen] = useState(false);\n\n useEffect(() => {\n if (!open) return;\n const onKey = (event: KeyboardEvent) => {\n if (event.key === "Escape") setOpen(false);\n };\n document.addEventListener("keydown", onKey);\n const previousOverflow = document.body.style.overflow;\n document.body.style.overflow = "hidden";\n return () => {\n document.removeEventListener("keydown", onKey);\n document.body.style.overflow = previousOverflow;\n };\n }, [open]);\n\n return (\n <>\n <button\n type="button"\n onClick={() => setOpen(true)}\n aria-label="Open menu"\n aria-expanded={open}\n aria-controls="mobile-nav-drawer"\n className="grid place-items-center w-11 h-11 -mr-2 rounded-md text-foreground hover:bg-muted transition-colors"\n >\n <svg\n width="20"\n height="20"\n viewBox="0 0 24 24"\n fill="none"\n stroke="currentColor"\n strokeWidth="2"\n strokeLinecap="round"\n strokeLinejoin="round"\n aria-hidden="true"\n >\n <line x1="3" y1="6" x2="21" y2="6" />\n <line x1="3" y1="12" x2="21" y2="12" />\n <line x1="3" y1="18" x2="21" y2="18" />\n </svg>\n </button>\n\n {open ? (\n <div className="fixed inset-0 z-50 sm:hidden">\n <button\n type="button"\n onClick={() => setOpen(false)}\n aria-label="Close menu"\n className="absolute inset-0 bg-background/80 backdrop-blur-sm"\n />\n <nav\n id="mobile-nav-drawer"\n aria-label="Mobile navigation"\n className="absolute inset-y-0 right-0 w-[85%] max-w-sm flex flex-col bg-background border-l border-border shadow-2xl"\n >\n <div className="flex items-center justify-between px-6 py-4 border-b border-border">\n <span className="text-xs font-semibold uppercase tracking-[0.16em] text-muted-foreground">\n Menu\n </span>\n <button\n type="button"\n onClick={() => setOpen(false)}\n aria-label="Close menu"\n className="grid place-items-center w-11 h-11 -mr-2 rounded-md text-foreground hover:bg-muted transition-colors"\n >\n <svg\n width="20"\n height="20"\n viewBox="0 0 24 24"\n fill="none"\n stroke="currentColor"\n strokeWidth="2"\n strokeLinecap="round"\n strokeLinejoin="round"\n aria-hidden="true"\n >\n <line x1="18" y1="6" x2="6" y2="18" />\n <line x1="6" y1="6" x2="18" y2="18" />\n </svg>\n </button>\n </div>\n <ul className="flex flex-col gap-1 px-3 py-4">\n {brand.header.nav.map((link) => (\n <li key={link.href}>\n <Link\n href={link.href}\n onClick={() => setOpen(false)}\n className="block px-3 py-3 rounded-md text-base font-medium text-foreground hover:bg-muted transition-colors"\n >\n {link.label}\n </Link>\n </li>\n ))}\n </ul>\n </nav>\n </div>\n ) : null}\n </>\n );\n}\n' }, { "path": "components/providers.tsx", "kind": "text", "content": '"use client";\n\nimport { useMemo, type ReactNode } from "react";\nimport { createCimplifyClient } from "@cimplify/sdk";\nimport { CimplifyProvider, CartDrawerProvider } from "@cimplify/sdk/react";\n\n// Same-origin client \u2014 every request goes through the Next.js rewrite in\n// next.config.ts, so no CORS preflight ever hits the browser.\nexport function Providers({ children }: { children: ReactNode }) {\n const client = useMemo(() => {\n const baseUrl =\n typeof window !== "undefined" ? window.location.origin : "http://127.0.0.1:8787";\n const publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY ?? "mock-dev";\n return createCimplifyClient({\n baseUrl,\n publicKey,\n suppressPublicKeyWarning: true,\n });\n }, []);\n\n return (\n <CimplifyProvider client={client}>\n <CartDrawerProvider>{children}</CartDrawerProvider>\n </CimplifyProvider>\n );\n}\n' }, { "path": "components/footer.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport { brand } from "@/lib/brand";\n\nconst ICONS: Record<string, React.ReactNode> = {\n instagram: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" aria-hidden className="w-5 h-5">\n <rect x="3" y="3" width="18" height="18" rx="5" />\n <circle cx="12" cy="12" r="4" />\n <circle cx="17.5" cy="6.5" r="0.75" fill="currentColor" />\n </svg>\n ),\n x: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M18 2h3l-7.5 8.6L22 22h-6.6l-5-6.5L4 22H1l8-9.2L1.4 2H8l4.6 6 5.4-6z" />\n </svg>\n ),\n tiktok: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M16 3v3.5a4.5 4.5 0 0 0 4.5 4.5V14a7.5 7.5 0 0 1-4.5-1.5V16a5 5 0 1 1-5-5v3a2 2 0 1 0 2 2V3z" />\n </svg>\n ),\n facebook: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M14 9V7a1 1 0 0 1 1-1h2V3h-3a4 4 0 0 0-4 4v2H8v3h2v9h3v-9h2.5l.5-3H13z" />\n </svg>\n ),\n youtube: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M22.5 6.5a2.6 2.6 0 0 0-1.8-1.8C19 4.2 12 4.2 12 4.2s-7 0-8.7.5A2.6 2.6 0 0 0 1.5 6.5C1 8.2 1 12 1 12s0 3.8.5 5.5a2.6 2.6 0 0 0 1.8 1.8C5 19.8 12 19.8 12 19.8s7 0 8.7-.5a2.6 2.6 0 0 0 1.8-1.8c.5-1.7.5-5.5.5-5.5s0-3.8-.5-5.5zM10 15.5v-7l6 3.5-6 3.5z" />\n </svg>\n ),\n linkedin: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M4 4h4v16H4zM6 2.5a2 2 0 1 0 0 4 2 2 0 0 0 0-4zM10 8h4v2.5h.1c.6-1.1 2-2.5 4-2.5 4 0 4.9 2.6 4.9 6V20h-4v-5c0-1.5-.5-3-2.3-3-1.7 0-2.5 1.3-2.5 3v5h-4z" />\n </svg>\n ),\n whatsapp: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M12 2a10 10 0 0 0-8.6 15l-1.4 5 5.2-1.4A10 10 0 1 0 12 2zm5 14.2c-.2.6-1.2 1.2-1.7 1.2-.5.1-1.1.1-1.7-.1-.4-.1-.9-.3-1.5-.6a8.4 8.4 0 0 1-3.7-3.4c-.7-1-1-1.8-1-2.5 0-.7.4-1.1.6-1.3.2-.2.4-.2.5-.2h.4c.1 0 .3 0 .4.3l.6 1.4c.1.2 0 .3 0 .4l-.3.4-.3.3c-.1.1-.2.2-.1.4.2.4.7 1.1 1.4 1.8.9.8 1.7 1.1 1.9 1.2.2.1.3.1.5-.1l.6-.7c.2-.2.3-.2.5-.1l1.4.7c.2.1.3.2.4.3.1.2.1.6 0 1z" />\n </svg>\n ),\n};\n\nconst FALLBACK_ICON = (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" aria-hidden className="w-5 h-5">\n <circle cx="12" cy="12" r="9" />\n </svg>\n);\n\nexport async function Footer() {\n "use cache";\n const year = new Date().getFullYear();\n return (\n <footer className="mt-12 px-6 sm:px-8 py-10 text-xs text-muted-foreground border-t border-border bg-card">\n <div className="max-w-7xl mx-auto">\n <div className="grid gap-10 md:grid-cols-[1.4fr_repeat(4,1fr)]">\n <div>\n <p className="font-serif text-2xl text-foreground m-0 mb-2">{brand.name}</p>\n <p className="leading-relaxed mb-4 max-w-sm">{brand.footer.blurb}</p>\n <address className="not-italic space-y-1">\n <p className="m-0">{brand.contact.address}</p>\n <p className="m-0">\n <a\n href={`tel:${brand.contact.phoneTel}`}\n className="hover:text-foreground transition-colors"\n >\n {brand.contact.phone}\n </a>\n </p>\n <p className="m-0">\n <a\n href={`mailto:${brand.contact.email}`}\n className="hover:text-foreground transition-colors"\n >\n {brand.contact.email}\n </a>\n </p>\n <p className="m-0">{brand.contact.hours}</p>\n </address>\n <div className="flex items-center gap-3 mt-5">\n {brand.socials.map((s) => (\n <a\n key={s.label}\n href={s.href}\n aria-label={s.label}\n target="_blank"\n rel="noopener noreferrer"\n className="inline-flex items-center justify-center w-9 h-9 rounded-full border border-border text-muted-foreground hover:text-foreground hover:border-foreground transition-colors"\n >\n {(s.icon && ICONS[s.icon]) ?? FALLBACK_ICON}\n </a>\n ))}\n </div>\n </div>\n {brand.footer.sitemap.map((section) => (\n <nav key={section.title} aria-labelledby={`footer-${section.title}`}>\n <p\n id={`footer-${section.title}`}\n className="font-semibold text-foreground mb-3 text-[13px] uppercase tracking-[0.08em]"\n >\n {section.title}\n </p>\n <ul className="space-y-2 m-0 p-0 list-none">\n {section.links.map((link) => (\n <li key={link.label}>\n <Link\n href={link.href}\n className="hover:text-foreground transition-colors"\n >\n {link.label}\n </Link>\n </li>\n ))}\n </ul>\n </nav>\n ))}\n </div>\n <div className="mt-12 pt-6 border-t border-border flex flex-col sm:flex-row items-center justify-between gap-3">\n <p className="m-0">\xA9 {year} {brand.name}. All rights reserved.</p>\n {brand.footer.poweredBy && (\n <p className="m-0 inline-flex items-center gap-1.5">\n <span className="text-muted-foreground/80">Powered by</span>\n <a\n href={brand.footer.poweredBy.href}\n target="_blank"\n rel="noopener noreferrer"\n aria-label={brand.footer.poweredBy.label}\n className="inline-flex items-center gap-1 font-serif text-foreground hover:text-primary transition-colors"\n >\n <span className="font-semibold tracking-tight">{brand.footer.poweredBy.label}</span>\n <svg\n viewBox="0 0 12 12"\n aria-hidden\n className="w-3 h-3 opacity-70"\n fill="none"\n stroke="currentColor"\n strokeWidth="1.5"\n >\n <path d="M3 9L9 3M9 3H4M9 3v5" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n </a>\n </p>\n )}\n </div>\n </div>\n </footer>\n );\n}\n' }, { "path": "components/store-product-card.tsx", "kind": "text", "content": '"use client";\n\nimport Link from "next/link";\nimport {\n CardVariant,\n FoodProductCard,\n RetailProductCard,\n WholesaleProductCard,\n DigitalProductCard,\n BundleProductCard,\n CompositeProductCard,\n StandardServiceCard,\n CompactServiceCard,\n ScheduleServiceCard,\n RentalServiceCard,\n AccommodationCard,\n LeaseServiceCard,\n SubscriptionCard,\n} from "@cimplify/sdk/react";\nimport type { CardLayoutProps } from "@cimplify/sdk/react";\nimport type { Product } from "@cimplify/sdk";\nimport { PRODUCT_TYPE, RENDER_HINT, DURATION_UNIT } from "@cimplify/sdk";\n\nconst RENTAL_UNITS = new Set<string>([DURATION_UNIT.Days, DURATION_UNIT.Hours]);\nconst LEASE_UNITS = new Set<string>([DURATION_UNIT.Weeks, DURATION_UNIT.Months, DURATION_UNIT.Years]);\n\nconst VARIANT_CARDS: Record<CardVariant, React.ComponentType<CardLayoutProps>> = {\n [CardVariant.Food]: FoodProductCard,\n [CardVariant.Retail]: RetailProductCard,\n [CardVariant.Wholesale]: WholesaleProductCard,\n [CardVariant.Digital]: DigitalProductCard,\n [CardVariant.Bundle]: BundleProductCard,\n [CardVariant.Composite]: CompositeProductCard,\n [CardVariant.Standard]: StandardServiceCard,\n [CardVariant.Compact]: CompactServiceCard,\n [CardVariant.Schedule]: ScheduleServiceCard,\n [CardVariant.Rental]: RentalServiceCard,\n [CardVariant.Accommodation]: AccommodationCard,\n [CardVariant.Lease]: LeaseServiceCard,\n [CardVariant.Subscription]: SubscriptionCard,\n};\n\nfunction resolveVariant(p: Product): CardVariant {\n if (p.type === PRODUCT_TYPE.Bundle) return CardVariant.Bundle;\n if (p.type === PRODUCT_TYPE.Composite) return CardVariant.Composite;\n if (p.quantity_pricing && p.quantity_pricing.length > 1) return CardVariant.Wholesale;\n if (p.type === PRODUCT_TYPE.Digital) return CardVariant.Digital;\n if (p.type === PRODUCT_TYPE.Service) {\n if (p.duration_unit && RENTAL_UNITS.has(p.duration_unit)) return CardVariant.Rental;\n if (p.duration_unit === DURATION_UNIT.Nights) return CardVariant.Accommodation;\n if (p.duration_unit && LEASE_UNITS.has(p.duration_unit)) return CardVariant.Lease;\n if (p.billing_plans && p.billing_plans.length > 0 && !p.duration_minutes) return CardVariant.Subscription;\n return CardVariant.Standard;\n }\n if (p.render_hint === RENDER_HINT.Food) return CardVariant.Food;\n return CardVariant.Retail;\n}\n\ninterface Props {\n product: Product;\n /** Override the auto-detected card variant. */\n variant?: CardVariant;\n}\n\n/**\n * Variant-aware product card that opens the URL-driven product modal on\n * click. Uses `next/link` with `?product=<slug>` so the link is statically\n * pre-renderable \u2014 no `useSearchParams` dependency means cards don\'t\n * become dynamic islands. The modal (which already reads `useSearchParams`\n * inside a Suspense boundary in the root layout) handles the rest.\n */\nexport function StoreProductCard({ product, variant }: Props) {\n const slug = product.slug || product.id;\n const Card = VARIANT_CARDS[variant ?? resolveVariant(product)];\n const href = `?product=${encodeURIComponent(slug)}`;\n\n return (\n <Card\n product={product}\n renderLink={({ className, children }) => (\n <Link href={href} scroll={false} prefetch={false} className={className}>\n {children}\n </Link>\n )}\n />\n );\n}\n' }, { "path": "components/json-ld.tsx", "kind": "text", "content": 'import { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\n\n/**\n * Streams the organization JSON-LD script tag in async so it doesn\'t block\n * the rest of the layout shell. `getSiteUrl` reads request headers, and\n * any await on dynamic data inside `RootLayout` makes Next 16 mark the\n * whole route as blocking.\n */\nexport async function OrganizationJsonLd(): Promise<React.ReactElement> {\n const siteUrl = await getSiteUrl();\n const ld = {\n "@context": "https://schema.org",\n "@type": brand.schemaType,\n name: brand.name,\n url: siteUrl,\n description: brand.description,\n email: brand.contact.email,\n telephone: brand.contact.phoneTel,\n address: {\n "@type": "PostalAddress",\n streetAddress: brand.contact.streetAddress,\n addressLocality: brand.contact.city,\n addressCountry: brand.contact.countryCode,\n },\n sameAs: brand.socials.map((s) => s.href),\n };\n return (\n <script\n type="application/ld+json"\n dangerouslySetInnerHTML={{ __html: JSON.stringify(ld) }}\n />\n );\n}\n' }, { "path": "components/nav-link.tsx", "kind": "text", "content": '"use client";\n\nimport Link from "next/link";\nimport { usePathname } from "next/navigation";\n\nexport function NavLink({ href, children }: { href: string; children: React.ReactNode }) {\n const pathname = usePathname();\n const active = pathname === href;\n return (\n <Link\n href={href}\n className={[\n "text-[13px] font-medium tracking-wide transition-colors",\n active ? "text-primary" : "text-muted-foreground hover:text-foreground",\n ].join(" ")}\n >\n {children}\n </Link>\n );\n}\n' }, { "path": "components/category-grid.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CategoryGrid as SdkCategoryGrid } from "@cimplify/sdk/react";\nimport type { Category } from "@cimplify/sdk";\n\n/**\n * Homepage category tiles \u2014 defers to the SDK\'s <CategoryGrid/> for layout\n * and accessibility, and routes selections to /categories/:slug.\n */\nexport function CategoryGrid({ categories }: { categories?: Category[] }) {\n const router = useRouter();\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 pt-14">\n <h2 className="font-serif text-[28px] font-semibold m-0 mb-5">Browse the menu</h2>\n <SdkCategoryGrid\n categories={categories}\n onSelect={(c) => router.push(`/categories/${c.slug}`)}\n classNames={{\n item: "flex flex-col gap-1 p-6 bg-card border border-border rounded-2xl cursor-pointer text-left transition-all hover:border-primary hover:-translate-y-0.5",\n name: "font-serif text-lg font-semibold text-foreground",\n description: "text-xs text-muted-foreground",\n count: "text-xs text-muted-foreground mt-1",\n }}\n />\n </section>\n );\n}\n' }, { "path": "components/cart-drawer.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CartDrawer as SdkCartDrawer } from "@cimplify/sdk/react";\n\n/**\n * Side-drawer cart. Auto-opens when an item is added (via\n * `<CartDrawerProvider>` in `providers.tsx`). The header\'s cart pill\n * also calls `useCartDrawer().open()` to reveal it on click.\n */\nexport function CartDrawer() {\n const router = useRouter();\n return <SdkCartDrawer onCheckout={() => router.push("/checkout")} />;\n}\n' }, { "path": "next.config.ts", "kind": "text", "content": 'import type { NextConfig } from "next";\n\n// Same-origin proxy target for the storefront API. The public key\n// decides: a real `cpk_live_\u2026` / `cpk_test_\u2026` only resolves against\n// hosted Cimplify, so browser cart writes go to the same place the\n// server catalogue reads come from. Anything else (`mock-dev`, empty)\n// falls back to the local `cimplify dev` mock in dev.\nconst publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY?.trim() ?? "";\nconst keyTargetsHostedCimplify =\n publicKey.startsWith("cpk_live_") || publicKey.startsWith("cpk_test_");\nconst STOREFRONT_URL =\n process.env.NODE_ENV === "production" || keyTargetsHostedCimplify\n ? "https://storefronts.cimplify.io"\n : "http://127.0.0.1:8787";\n\nif (STOREFRONT_URL === "http://127.0.0.1:8787") {\n console.warn(\n "[cimplify] next.config resolved the local storefront API URL; production deploys must run with NODE_ENV=production.",\n );\n}\n\nconst nextConfig: NextConfig = {\n // Enable Next 16\'s `cacheComponents` mode so we can use `\'use cache\'` +\n // `cacheTag` / `cacheLife` for SSR caching. Cached chrome streams first,\n // dynamic Suspense boundaries fill in when ready.\n cacheComponents: true,\n async redirects() {\n // Config-level so cacheComponents needn\'t prerender a redirect()-only page.\n return [\n { source: "/login", destination: "/account", permanent: false },\n { source: "/signup", destination: "/account", permanent: false },\n ];\n },\n async rewrites() {\n return [\n { source: "/api/v1/:path*", destination: `${STOREFRONT_URL}/api/v1/:path*` },\n { source: "/img/:path*", destination: `${STOREFRONT_URL}/img/:path*` },\n { source: "/elements/:path*", destination: `${STOREFRONT_URL}/elements/:path*` },\n { source: "/_mock/:path*", destination: `${STOREFRONT_URL}/_mock/:path*` },\n ];\n },\n images: {\n loader: "custom",\n loaderFile: "./lib/cimplify-loader.ts",\n remotePatterns: [\n { protocol: "http", hostname: "127.0.0.1", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "3000", pathname: "/img/**" },\n { protocol: "https", hostname: "loremflickr.com", pathname: "/**" },\n { protocol: "https", hostname: "images.unsplash.com", pathname: "/**" },\n { protocol: "https", hostname: "static-tmp.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "storefrontassetscdn.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "cdn.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "res.cloudinary.com", pathname: "/cimplify/**" },\n ],\n },\n};\n\nexport default nextConfig;\n' }, { "path": ".claude/skills/cimplify-storefront/SKILL.md", "kind": "text", "content": "---\nname: cimplify-storefront\ndescription: Build, customize, rebrand, or deploy a Cimplify-scaffolded storefront. Triggers when the user asks to create a storefront, rebrand a Cimplify template, change the palette, add a page, deploy to Cimplify, or works in a project containing `lib/brand.ts` and `next.config.ts` with `cacheComponents`.\n---\n\n# Cimplify Storefront skill\n\nYou're working on a project scaffolded from `cimplify init`. The architecture is opinionated and the rebrand surface is intentionally small. Read `AGENTS.md` at the project root for the file \u2194 brand-field map for *this template's* industry; this skill gives you the playbook that's the same across all six.\n\n## The contract \u2014 never break\n\n1. **`lib/brand.ts` is the only place for content edits.** Every visible string reads from this file. If a string isn't in `brand`, *add a field* to the `Brand` interface \u2014 don't hardcode it in a page or component.\n2. **`app/globals.css` `@theme { \u2026 }`** holds palette + radius + font references. To re-skin the entire site, edit only this block.\n3. **Server Components are cached** via `'use cache'` + `cacheTag(tags.X())` + `cacheLife(\"hours\")`. Don't downgrade them to `\"use client\"` to avoid an issue \u2014 fix the issue.\n4. **Client islands** (anything reading `useSearchParams`, `usePathname`, `useRouter`, `useState`) live behind `<Suspense>`.\n5. **`cacheComponents: true`** in `next.config.ts` is non-negotiable. Don't turn it off.\n6. **`bun run test:run` (vitest)** is the canonical test runner. `bun test` will show false failures because Bun's `vi` shim is incomplete.\n7. **Cart, checkout, orders stay client.** They're session-bound. Don't try to SSR them.\n\n## The brand-field schema (in `lib/brand.ts`)\n\n```\nidentity: name, shortName, microTag, description, schemaType, currency, locale\ncontact: address, streetAddress, city, countryCode, phone, phoneTel, email, privacyEmail, hours\nsocials: [{ label, href, icon }] // icon \u2208 instagram|x|tiktok|facebook|youtube|linkedin|whatsapp\nheader: { nav: [{ label, href }] }\nhero: { badge, title, subtitle, primaryCtaLabel, secondaryCtaLabel?, secondaryCtaHref? }\noptional: trustItems[]?, brandStrip?, promo?, tradeIn? // render conditionally\nnewsletter: { eyebrow, title, body, placeholder, submitLabel, successLabel }\nabout: { eyebrow, title, paragraphs[], sections[{ heading, body }] }\nfaq: { eyebrow, title, sections[{ title, items[{ q, a }] }], contactPrompt, contactEmail }\nterms,\nprivacy,\nshipping,\nreturns,\naccessibility: { eyebrow, title, lastUpdated, sections[{ heading, body | { intro, bullets[] } }] }\naccount: eyebrows + titles for /account, /login, /signup\ncontactPage: { eyebrow, title, body, reasons[], directLines[{ label, value, href }] }\ntrackOrder: { eyebrow, title, body }\nfooter: { blurb, sitemap[{ title, links[] }], poweredBy? }\nllms: { summary } // opens /llms.txt\nmock: { seed, businessId }\n```\n\n## Playbook \u2014 common tasks\n\n### Rebrand a storefront for a new merchant\n\n1. Edit `lib/brand.ts`. Replace every field with the merchant's content. Use the brief / context the user gave you.\n2. Edit `app/globals.css` `@theme { \u2026 }`. Change `--color-primary`, `--color-background`, `--color-foreground`, optionally `--radius`. Use OKLCH; if the brand only gave hex, convert.\n3. (Optional) Swap fonts in `app/layout.tsx` \u2014 `next/font/google` import + the variable wired into the `<html>` className.\n4. Set `.env.local`: `NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY` (optional: `NEXT_PUBLIC_SITE_URL`).\n\nDon't touch any other file. If the rebrand needs content not in the schema, add the field to `Brand` first, populate it, then read it from the page.\n\n### Add a new section / component\n\n1. Build it as a Server Component in `components/` (or a client island in `*-client.tsx` if interactive).\n2. Read merchant copy from `brand`. Add new fields to the `Brand` interface if needed.\n3. Wrap interactive bits in `<Suspense fallback={\u2026}>` so the cached chrome streams.\n4. Compose into the page.\n\n### Wire a Server Action that mutates data\n\n```ts\n\"use server\";\nimport { getServerClient, revalidateProducts } from \"@cimplify/sdk/server\";\n\nexport async function createProduct(input: ProductInput) {\n await getServerClient().catalogue.createProduct(input);\n revalidateProducts();\n}\n```\n\nAfter every mutation, call the matching `revalidate*` helper from `@cimplify/sdk/server`: `revalidateProducts`, `revalidateProduct(id)`, `revalidateCategories`, `revalidateCategory(id)`, `revalidateCollections`, `revalidateCollection(id)`, `revalidateBusiness`.\n\n### Add a Server Component data fetch\n\n```ts\nimport { cacheTag, cacheLife } from \"next/cache\";\nimport { getServerClient, tags } from \"@cimplify/sdk/server\";\n\nasync function getX() {\n \"use cache\";\n cacheTag(tags.products());\n cacheLife(\"hours\");\n const r = await getServerClient().catalogue.getProducts({ limit: 24 });\n if (!r.ok) throw new Error(r.error.message);\n return r.value.items;\n}\n```\n\n### Eject an SDK component for deeper customization\n\nTry `classNames`, `renderImage`, `renderLink`, slot props first. If those run out:\n\n```bash\ncimplify list\ncimplify add product-card --dir src/components/cimplify\n```\n\nOnce ejected, the file is yours. Hooks/types still come from `@cimplify/sdk`.\n\n### Deploy\n\n```bash\ncimplify login\ncimplify projects create my-store\ncimplify link <project-id>\ncimplify env push\ncimplify deploy --prod\ncimplify logs --follow\ncimplify domains add my-store.com\n```\n\n## Pitfalls \u2014 explicit \u274C list\n\n- \u274C Hardcoding any visible string in a page or component. Always `brand.X`.\n- \u274C Disabling `cacheComponents: true` to silence a warning. Fix the warning by wrapping the dynamic call in `<Suspense>`.\n- \u274C Using `unstable_cache` \u2014 Next 16's canonical primitive is `'use cache'` + `cacheTag` + `cacheLife`.\n- \u274C Bypassing `getServerClient()` and calling `createCimplifyClient` directly in a Server Component \u2014 loses per-request memoization.\n- \u274C Mutating data without calling the matching `revalidate*` helper.\n- \u274C Adding an `app/error.tsx` handler that calls `reset()` without logging \u2014 silently swallowed errors hide bugs.\n- \u274C Running `bun test` and reporting failures. Use `bun run test:run` (vitest).\n- \u274C Editing the per-template `AGENTS.md` to remove notes about TODOs (contact form, newsletter fake submits) \u2014 they're real.\n\n## Where things live\n\n| Need | Look here |\n|---|---|\n| Which page reads which `brand.X` field | `AGENTS.md` at project root |\n| Architectural rules | `AGENTS.md` at project root + this skill |\n| Running locally | `bun dev` (boots mock + Next together) |\n| Switching mock seed | edit `dev:mock` in `package.json` |\n| Full SDK reference | `docs/sdk/storefronts.md` in the Cimplify repo |\n\n## What to do when the user asks something out-of-scope\n\nIf the user asks for something the template doesn't support out of the box:\n\n1. **Check first** \u2014 is there an SDK component or hook that does it? `@cimplify/sdk/react` has 50+ components, 30+ hooks. Search `node_modules/@cimplify/sdk/dist/react.d.mts` if needed.\n2. **Compose from existing parts** before authoring new ones.\n3. **If genuinely missing** \u2014 build the new component, hoist its strings into `brand.ts`, document in `AGENTS.md`.\n\nDon't introduce competing patterns (a new caching strategy, a new auth flow, a new theming system). The template's opinions are intentional.\n" }, { "path": ".cursor/rules/cimplify-storefront.mdc", "kind": "binary", "content": "LS0tCmRlc2NyaXB0aW9uOiBDaW1wbGlmeSBzdG9yZWZyb250IOKAlCBhcmNoaXRlY3R1cmFsIHJ1bGVzIGFuZCByZWJyYW5kIGNvbnRyYWN0IGZvciBhIHByb2plY3Qgc2NhZmZvbGRlZCBmcm9tIGBjaW1wbGlmeSBpbml0YC4gVHJpZ2dlcnMgd2hlbiBmaWxlcyBsaWtlIGxpYi9icmFuZC50cywgYXBwL2dsb2JhbHMuY3NzLCBjb21wb25lbnRzL2hlYWRlci50c3gsIG9yIC5jaW1wbGlmeS9wcm9qZWN0Lmpzb24gYXJlIGluIHRoZSB3b3Jrc3BhY2UuCmdsb2JzOiBbImxpYi9icmFuZC50cyIsICJhcHAvKioiLCAiY29tcG9uZW50cy8qKiIsICIuZW52LmxvY2FsIiwgIm5leHQuY29uZmlnLnRzIl0KYWx3YXlzQXBwbHk6IHRydWUKLS0tCgojIENpbXBsaWZ5IHN0b3JlZnJvbnQKClRoaXMgcHJvamVjdCB3YXMgc2NhZmZvbGRlZCBmcm9tIGEgQ2ltcGxpZnkgdGVtcGxhdGUuIFJlYWQgYEFHRU5UUy5tZGAgYXQgdGhlIHByb2plY3Qgcm9vdCBmb3IgdGhlIGZpbGUg4oaUIGBicmFuZC5YYCBmaWVsZCBtYXAgYW5kIGAuY2xhdWRlL3NraWxscy9jaW1wbGlmeS1zdG9yZWZyb250L1NLSUxMLm1kYCBmb3IgdGhlIGZ1bGwgcGxheWJvb2suCgojIyBUaGUgY29udHJhY3QKCjEuICoqYGxpYi9icmFuZC50c2AqKiDigJQgZXZlcnkgdmlzaWJsZSBzdHJpbmcuIElmIHNvbWV0aGluZyBpc24ndCB0aGVyZSwgYWRkIGEgZmllbGQgdG8gdGhlIGBCcmFuZGAgaW50ZXJmYWNlOyBkb24ndCBoYXJkY29kZS4KMi4gKipgYXBwL2dsb2JhbHMuY3NzYCBgQHRoZW1lYCBibG9jayoqIOKAlCBwYWxldHRlLCByYWRpdXMsIGZvbnQgcmVmZXJlbmNlcy4KMy4gKipTZXJ2ZXIgQ29tcG9uZW50cyBhcmUgY2FjaGVkKiogdmlhIGAndXNlIGNhY2hlJ2AgKyBgY2FjaGVUYWcodGFncy5YKCkpYCArIGBjYWNoZUxpZmUoImhvdXJzIilgLiBEb24ndCBkb3duZ3JhZGUgdG8gY2xpZW50IHRvIHNpbGVuY2UgYSB3YXJuaW5nLgo0LiAqKkNsaWVudCBpc2xhbmRzKiogYmVoaW5kIGA8U3VzcGVuc2U+YC4KNS4gKipgY2FjaGVDb21wb25lbnRzOiB0cnVlYCoqIGluIGBuZXh0LmNvbmZpZy50c2AgaXMgbm9uLW5lZ290aWFibGUuCjYuIFVzZSAqKmBidW4gcnVuIHRlc3Q6cnVuYCoqICh2aXRlc3QpLCBub3QgYGJ1biB0ZXN0YC4KCiMjIERvbid0CgotIEhhcmRjb2RlIHZpc2libGUgc3RyaW5ncyBpbiBwYWdlcy9jb21wb25lbnRzLgotIFVzZSBgdW5zdGFibGVfY2FjaGVgIChOZXh0IDE2IHVzZXMgYCd1c2UgY2FjaGUnYCkuCi0gQnlwYXNzIGBnZXRTZXJ2ZXJDbGllbnQoKWAgKGxvc2VzIHBlci1yZXF1ZXN0IG1lbW9pemF0aW9uKS4KLSBNdXRhdGUgd2l0aG91dCBjYWxsaW5nIHRoZSBtYXRjaGluZyBgcmV2YWxpZGF0ZSpgIGhlbHBlciBmcm9tIGBAY2ltcGxpZnkvc2RrL3NlcnZlcmAuCg==" }, { "path": ".gitignore", "kind": "text", "content": "node_modules\n.next\nout\n.env\n.env.local\n.env*.local\n.DS_Store\n*.tsbuildinfo\nnext-env.d.ts\n" }, { "path": "app/about/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `About \u2014 ${brand.name}`,\n description: brand.description,\n};\n\nexport default function AboutPage() {\n const a = brand.about;\n const titleParts = a.title.split("\\n");\n return (\n <article className="max-w-3xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {a.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-6 -tracking-[0.02em]">\n {titleParts.map((line, i) => (\n <span key={i}>\n {line}\n {i < titleParts.length - 1 && <br />}\n </span>\n ))}\n </h1>\n <div className="prose prose-lg max-w-none space-y-5 text-foreground/90 leading-relaxed">\n {a.paragraphs.map((p, i) => (\n <p key={i}>{p}</p>\n ))}\n {a.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="font-serif text-3xl font-semibold mt-10 mb-3">{s.heading}</h2>\n <p>{s.body}</p>\n </div>\n ))}\n </div>\n </article>\n );\n}\n' }, { "path": "app/account/orders/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Orders \u2014 ${brand.name}`,\n};\n\nexport default function OrdersPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Your orders\n </h1>\n <AccountIframe section="orders" />\n </article>\n );\n}\n' }, { "path": "app/account/settings/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Settings \u2014 ${brand.name}`,\n};\n\nexport default function SettingsPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Settings\n </h1>\n <AccountIframe section="settings" />\n </article>\n );\n}\n' }, { "path": "app/account/addresses/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Addresses \u2014 ${brand.name}`,\n};\n\nexport default function AddressesPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Your addresses\n </h1>\n <AccountIframe section="addresses" />\n </article>\n );\n}\n' }, { "path": "app/account/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Account \u2014 ${brand.name}`,\n description: brand.account.signupSubtitle,\n};\n\nexport default function AccountPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {brand.account.accountEyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n {brand.account.accountTitle}\n </h1>\n <AccountIframe />\n </article>\n );\n}\n' }, { "path": "app/.well-known/ucp/route.ts", "kind": "text", "content": 'import { NextResponse } from "next/server";\n\n/**\n * UCP (Universal Commerce Protocol) manifest discovery endpoint.\n *\n * Agents \u2014 Claude, ChatGPT, Gemini, MCP clients \u2014 probe\n * `https://<your-domain>/.well-known/ucp` to learn what commerce\n * capabilities your storefront supports. We resolve the business\'s\n * UCP handle from the public key (via the storefront API), then forward\n * to Cimplify for the canonical manifest. Edge-cached for an hour\n * because capabilities change rarely.\n */\nconst UCP_API_BASE = "https://api.cimplify.io";\nconst STOREFRONT_API_BASE =\n process.env.NODE_ENV === "production"\n ? "https://storefronts.cimplify.io"\n : "http://127.0.0.1:8787";\n\nexport async function GET() {\n const publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY;\n if (!publicKey) {\n return NextResponse.json(\n {\n error: "NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY not set",\n remediation:\n "Set NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY in .env.local (and your deployment env).",\n },\n { status: 500 },\n );\n }\n\n try {\n // Step 1 \u2014 resolve the business handle from the public key.\n const businessResp = await fetch(`${STOREFRONT_API_BASE}/api/v1/business`, {\n headers: { "X-API-Key": publicKey, "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n });\n\n if (!businessResp.ok) {\n return NextResponse.json(\n { error: `Failed to resolve business: ${businessResp.status}` },\n { status: businessResp.status },\n );\n }\n\n const businessJson = await businessResp.json();\n const handle: string | undefined = businessJson?.data?.handle;\n if (!handle) {\n return NextResponse.json(\n { error: "Business has no handle configured" },\n { status: 500 },\n );\n }\n\n // Step 2 \u2014 fetch the canonical UCP manifest.\n const manifestResp = await fetch(\n `${UCP_API_BASE}/ucp/v1/${handle}/manifest`,\n {\n headers: { "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n },\n );\n\n if (!manifestResp.ok) {\n return NextResponse.json(\n { error: `Upstream UCP manifest fetch failed: ${manifestResp.status}` },\n { status: manifestResp.status },\n );\n }\n\n const manifest = await manifestResp.json();\n return NextResponse.json(manifest, {\n headers: {\n "Content-Type": "application/json",\n "Cache-Control": "public, max-age=3600, s-maxage=3600",\n },\n });\n } catch (error) {\n return NextResponse.json(\n {\n error: "Failed to fetch UCP manifest",\n detail: error instanceof Error ? error.message : String(error),\n },\n { status: 500 },\n );\n }\n}\n' }, { "path": "app/search/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { SearchClient } from "./search-client";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Search \u2014 ${brand.name}`,\n description: `Search ${brand.name} \u2014 products, collections, categories.`,\n};\n\nexport default function SearchPage() {\n return (\n <article className="max-w-7xl mx-auto px-6 sm:px-8 py-10">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Search\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Find anything.\n </h1>\n <Suspense fallback={<SearchSkeleton />}>\n <SearchClient />\n </Suspense>\n </article>\n );\n}\n\nfunction SearchSkeleton() {\n return (\n <div>\n <div className="h-12 w-full max-w-xl bg-muted rounded animate-pulse mb-8" />\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {Array.from({ length: 8 }).map((_, i) => (\n <div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </div>\n );\n}\n' }, { "path": "app/search/search-client.tsx", "kind": "text", "content": '"use client";\n\nimport { SearchPage } from "@cimplify/sdk/react";\n\nexport function SearchClient() {\n return <SearchPage />;\n}\n' }, { "path": "app/faq/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `FAQ \u2014 ${brand.name}`,\n description: "Delivery, pickup, custom cakes, allergens, payment \u2014 answers to the questions we hear most often.",\n};\n\nexport default function FaqPage() {\n const f = brand.faq;\n return (\n <article className="max-w-3xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {f.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-10 -tracking-[0.02em]">\n {f.title}\n </h1>\n <div className="space-y-12">\n {f.sections.map((section) => (\n <section key={section.title}>\n <h2 className="font-serif text-2xl font-semibold mb-5">{section.title}</h2>\n <dl className="space-y-6">\n {section.items.map((item) => (\n <div key={item.q}>\n <dt className="font-medium text-foreground mb-1.5">{item.q}</dt>\n <dd className="text-muted-foreground leading-relaxed">{item.a}</dd>\n </div>\n ))}\n </dl>\n </section>\n ))}\n </div>\n <p className="mt-12 pt-8 border-t border-border text-sm text-muted-foreground">\n {f.contactPrompt}{" "}\n <a\n href={`mailto:${f.contactEmail}`}\n className="text-primary font-semibold hover:underline"\n >\n {f.contactEmail}\n </a>{" "}\n and a real human will reply within 24 hours.\n </p>\n </article>\n );\n}\n' }, { "path": "app/robots.ts", "kind": "text", "content": 'import type { MetadataRoute } from "next";\nimport { getSiteUrl } from "@/lib/site-url";\n\nexport default async function robots(): Promise<MetadataRoute.Robots> {\n const siteUrl = await getSiteUrl();\n return {\n rules: [\n {\n userAgent: "*",\n allow: "/",\n disallow: ["/cart", "/checkout", "/orders/", "/api/"],\n },\n ],\n sitemap: `${siteUrl}/sitemap.xml`,\n host: siteUrl,\n };\n}\n' }, { "path": "app/track-order/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { TrackOrderForm } from "./track-order-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Track an order \u2014 ${brand.name}`,\n description: brand.trackOrder.body,\n};\n\nexport default function TrackOrderPage() {\n const t = brand.trackOrder;\n return (\n <article className="max-w-2xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {t.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,5vw,3rem)] font-semibold mb-4 -tracking-[0.02em]">\n {t.title}\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">{t.body}</p>\n <TrackOrderForm />\n </article>\n );\n}\n' }, { "path": "app/track-order/track-order-form.tsx", "kind": "text", "content": '"use client";\n\nimport { useState } from "react";\nimport { useRouter } from "next/navigation";\n\n/**\n * Guest order tracker. Routes /orders/<id> with the entered id; the\n * post-checkout confirmation page already lives at /orders/[id] so this\n * just sends the visitor there. Replace the redirect with a real lookup\n * (e.g. Cimplify orders API + email-match guard) for production.\n */\nexport function TrackOrderForm() {\n const router = useRouter();\n const [orderId, setOrderId] = useState("");\n const [email, setEmail] = useState("");\n\n return (\n <form\n onSubmit={(e) => {\n e.preventDefault();\n if (!orderId.trim()) return;\n router.push(`/orders/${encodeURIComponent(orderId.trim())}`);\n }}\n className="space-y-4 rounded-2xl border border-border bg-card p-6"\n >\n <div>\n <label\n htmlFor="orderId"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order number\n </label>\n <input\n id="orderId"\n name="orderId"\n required\n value={orderId}\n onChange={(e) => setOrderId(e.target.value)}\n placeholder="ord_abc123\u2026"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <div>\n <label\n htmlFor="email"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order email\n </label>\n <input\n id="email"\n name="email"\n type="email"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n placeholder="you@email.com"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <button\n type="submit"\n className="w-full inline-flex items-center justify-center px-5 py-3 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors"\n >\n Track order\n </button>\n </form>\n );\n}\n' }, { "path": "app/cart/page.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CartPage as SdkCartPage } from "@cimplify/sdk/react";\n\nexport default function CartPage() {\n const router = useRouter();\n return <SdkCartPage onCheckout={() => router.push("/checkout")} />;\n}\n' }, { "path": "app/llms.txt/route.ts", "kind": "text", "content": 'import { cacheTag, cacheLife } from "next/cache";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\n\nasync function buildLlmsTxt(SITE_URL: string): Promise<string> {\n "use cache";\n cacheTag(tags.products(), tags.categories(), tags.collections());\n cacheLife("hours");\n\n const client = getServerClient();\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n\n const products: Product[] = productsRes.ok ? productsRes.value.items : [];\n const categories = categoriesRes.ok ? categoriesRes.value : [];\n const collections = collectionsRes.ok ? collectionsRes.value : [];\n\n const lines: string[] = [];\n lines.push(`# ${brand.name}`);\n lines.push("");\n lines.push(`> ${brand.llms.summary}`);\n lines.push("");\n lines.push("## Browse");\n lines.push(`- [Home](${SITE_URL}/)`);\n lines.push(`- [Shop](${SITE_URL}/shop): Full menu with filter and sort.`);\n\n if (categories.length > 0) {\n lines.push("");\n lines.push("## Categories");\n for (const c of categories) {\n lines.push(\n `- [${c.name}](${SITE_URL}/categories/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (collections.length > 0) {\n lines.push("");\n lines.push("## Collections");\n for (const c of collections) {\n lines.push(\n `- [${c.name}](${SITE_URL}/collections/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (products.length > 0) {\n lines.push("");\n lines.push("## Products");\n for (const p of products) {\n const slug = p.slug ?? p.id;\n const price = `${brand.currency} ${p.default_price}`;\n const desc = p.description ? ` \u2014 ${p.description.replace(/\\s+/g, " ").slice(0, 200)}` : "";\n lines.push(`- [${p.name}](${SITE_URL}/shop?product=${slug}) (${price})${desc}`);\n }\n }\n\n lines.push("");\n lines.push("## Information");\n lines.push(`- [About](${SITE_URL}/about)`);\n lines.push(`- [FAQ](${SITE_URL}/faq)`);\n lines.push(`- [Terms of Service](${SITE_URL}/terms)`);\n lines.push(`- [Privacy Policy](${SITE_URL}/privacy)`);\n lines.push(`- [Sitemap (XML)](${SITE_URL}/sitemap.xml)`);\n lines.push("");\n lines.push("## Contact");\n lines.push(`- Email: ${brand.contact.email}`);\n lines.push(`- Phone: ${brand.contact.phone}`);\n lines.push(`- Address: ${brand.contact.address}`);\n\n return lines.join("\\n") + "\\n";\n}\n\n/**\n * `/llms.txt` \u2014 machine-readable site index for LLMs (per llmstxt.org).\n * Lets coding agents and chat assistants find products, categories, and\n * support pages without scraping HTML. Plain Markdown so it streams cheaply\n * into context windows.\n */\nexport async function GET(): Promise<Response> {\n const body = await buildLlmsTxt(await getSiteUrl());\n return new Response(body, {\n headers: {\n "Content-Type": "text/plain; charset=utf-8",\n "Cache-Control": "public, max-age=0, s-maxage=3600, stale-while-revalidate=86400",\n },\n });\n}\n' }, { "path": "app/sitemap.ts", "kind": "text", "content": 'import type { MetadataRoute } from "next";\nimport { getServerClient, type Product } from "@cimplify/sdk/server";\nimport { getSiteUrl } from "@/lib/site-url";\n\nconst STATIC_ROUTES: { path: string; priority: number; changeFrequency: "daily" | "weekly" | "monthly" }[] = [\n { path: "/", priority: 1.0, changeFrequency: "daily" },\n { path: "/shop", priority: 0.9, changeFrequency: "daily" },\n { path: "/about", priority: 0.5, changeFrequency: "monthly" },\n { path: "/faq", priority: 0.4, changeFrequency: "monthly" },\n { path: "/terms", priority: 0.2, changeFrequency: "monthly" },\n { path: "/privacy", priority: 0.2, changeFrequency: "monthly" },\n];\n\nexport default async function sitemap(): Promise<MetadataRoute.Sitemap> {\n const now = new Date();\n const siteUrl = await getSiteUrl();\n const client = getServerClient();\n\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n\n const products = productsRes.ok ? productsRes.value.items : [];\n const categories = categoriesRes.ok ? categoriesRes.value : [];\n const collections = collectionsRes.ok ? collectionsRes.value : [];\n\n const staticEntries: MetadataRoute.Sitemap = STATIC_ROUTES.map((r) => ({\n url: `${siteUrl}${r.path}`,\n lastModified: now,\n changeFrequency: r.changeFrequency,\n priority: r.priority,\n }));\n\n // Bakery uses a URL-driven product modal (`?product=<slug>` on the home or\n // shop pages). Emit those as deep-linkable URLs so search engines and LLMs\n // can index each product canonically.\n const productEntries: MetadataRoute.Sitemap = products.map((p: Product) => ({\n url: `${siteUrl}/shop?product=${encodeURIComponent(p.slug ?? p.id)}`,\n lastModified: p.updated_at ? new Date(p.updated_at) : now,\n changeFrequency: "weekly",\n priority: 0.7,\n }));\n\n const categoryEntries: MetadataRoute.Sitemap = categories.map((c) => ({\n url: `${siteUrl}/categories/${c.slug}`,\n lastModified: now,\n changeFrequency: "weekly",\n priority: 0.6,\n }));\n\n const collectionEntries: MetadataRoute.Sitemap = collections.map((c) => ({\n url: `${siteUrl}/collections/${c.slug}`,\n lastModified: now,\n changeFrequency: "weekly",\n priority: 0.6,\n }));\n\n return [...staticEntries, ...categoryEntries, ...collectionEntries, ...productEntries];\n}\n' }, { "path": "app/opensearch.xml/route.ts", "kind": "text", "content": 'import { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\n\n/**\n * OpenSearch description document \u2014 lets browsers add this site to the\n * address bar\'s search engine list. When users press Tab after typing the\n * domain, they get an inline search box that hits /search?q=...\n */\nexport async function GET(): Promise<Response> {\n const siteUrl = await getSiteUrl();\n const xml = `<?xml version="1.0" encoding="UTF-8"?>\n<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">\n <ShortName>${escapeXml(brand.shortName)}</ShortName>\n <Description>Search ${escapeXml(brand.name)}</Description>\n <InputEncoding>UTF-8</InputEncoding>\n <Url type="text/html" method="get" template="${siteUrl}/search?q={searchTerms}" />\n <Url type="application/opensearchdescription+xml" rel="self" template="${siteUrl}/opensearch.xml" />\n <moz:SearchForm>${siteUrl}/search</moz:SearchForm>\n</OpenSearchDescription>\n`;\n return new Response(xml, {\n headers: {\n "Content-Type": "application/opensearchdescription+xml; charset=utf-8",\n "Cache-Control": "public, max-age=86400",\n },\n });\n}\n\nfunction escapeXml(s: string): string {\n return s\n .replace(/&/g, "&amp;")\n .replace(/</g, "&lt;")\n .replace(/>/g, "&gt;")\n .replace(/"/g, "&quot;")\n .replace(/\'/g, "&apos;");\n}\n' }, { "path": "app/contact/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { ContactForm } from "./contact-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Contact \u2014 ${brand.name}`,\n description: brand.contactPage.body,\n};\n\nexport default function ContactPage() {\n const c = brand.contactPage;\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {c.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-4 -tracking-[0.02em]">\n {c.title}\n </h1>\n <p className="text-lg text-muted-foreground max-w-2xl mb-12 leading-relaxed">{c.body}</p>\n\n <div className="grid grid-cols-1 lg:grid-cols-[1.5fr_1fr] gap-10 items-start">\n <ContactForm reasons={c.reasons} />\n <aside className="space-y-5 lg:pl-10 lg:border-l border-border">\n <div>\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Direct lines\n </p>\n <ul className="space-y-3 list-none p-0 m-0">\n {c.directLines.map((line) => (\n <li key={line.label}>\n <p className="text-xs text-muted-foreground m-0">{line.label}</p>\n <a\n href={line.href}\n className="text-foreground hover:text-primary transition-colors"\n >\n {line.value}\n </a>\n </li>\n ))}\n </ul>\n </div>\n <div>\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Visit\n </p>\n <p className="text-foreground m-0">{brand.contact.address}</p>\n <p className="text-xs text-muted-foreground mt-1">{brand.contact.hours}</p>\n </div>\n </aside>\n </div>\n </article>\n );\n}\n' }, { "path": "app/contact/contact-form.tsx", "kind": "text", "content": `"use client";
1532
+ ` }, { "path": "lib/cimplify-loader.ts", "kind": "text", "content": 'import type { ImageLoader } from "next/image";\n\nimport { assetUrl, isCimplifyAsset } from "@cimplify/sdk";\n\nconst cdnBase = process.env.NEXT_PUBLIC_CIMPLIFY_CDN_URL?.trim() || undefined;\n\nconst cimplifyImageLoader: ImageLoader = ({ src, width, quality }) => {\n if (isCimplifyAsset(src, cdnBase)) {\n return assetUrl(src, {\n base: cdnBase,\n w: width,\n quality,\n format: "auto",\n });\n }\n return src;\n};\n\nexport default cimplifyImageLoader;\n' }, { "path": "lib/site.config.ts", "kind": "text", "content": "/**\n * Canonical absolute URL. The platform overwrites this at build time with the\n * storefront's primary domain; the default only applies to local builds. It's\n * per-deploy config, not per-request, so it stays a static constant \u2014 keeping\n * the root layout prerenderable under `cacheComponents`.\n */\nexport const SITE_URL = \"https://example.com\";\n" }, { "path": "lib/cart.ts", "kind": "text", "content": '"use client";\n\nimport { useCart } from "@cimplify/sdk/react";\n\n/**\n * Reactive cart count for the header pill. Subscribes to the SDK cart\n * state \u2014 updates immediately on add / remove / quantity change.\n */\nexport function useCartCount(): { count: number } {\n const { itemCount } = useCart();\n return { count: itemCount };\n}\n' }, { "path": "lib/site-url.ts", "kind": "text", "content": 'import { SITE_URL } from "./site.config";\n\n/** Canonical absolute URL for this storefront. */\nexport async function getSiteUrl(): Promise<string> {\n return SITE_URL;\n}\n' }, { "path": "metadata.json", "kind": "text", "content": '{\n "id": "fashion",\n "name": "Fashion",\n "tagline": "Editorial multi-drop fashion storefront with lookbooks and size guides.",\n "industry": "fashion",\n "tags": ["fashion", "apparel", "lookbook"],\n "stability": "stable",\n "schemaType": "Store",\n "mock": {\n "seedName": "fashion",\n "seedBusinessId": "bus_studio_frx"\n }\n}\n' }, { "path": "playwright.config.ts", "kind": "text", "content": 'import { defineConfig, devices } from "@playwright/test";\n\n/**\n * Visual regression config \u2014 boots `bun dev` (mock + next), runs against\n * a stable Chromium, snapshots key pages.\n *\n * Run:\n * bun run check:visual # compare against baseline\n * bun run check:visual --update-snapshots # accept new baseline\n *\n * Baselines live in `e2e/__snapshots__/`. Commit them.\n */\nexport default defineConfig({\n testDir: "./e2e",\n fullyParallel: false,\n forbidOnly: Boolean(process.env.CI),\n retries: process.env.CI ? 1 : 0,\n workers: 1,\n reporter: process.env.CI ? "github" : "list",\n\n use: {\n baseURL: "http://localhost:3000",\n trace: "retain-on-failure",\n // Viewport snapshots are noisy across OS / browser builds. Lock here.\n viewport: { width: 1280, height: 800 },\n },\n\n projects: [\n {\n name: "chromium",\n use: { ...devices["Desktop Chrome"], viewport: { width: 1280, height: 800 } },\n },\n ],\n\n expect: {\n // Anti-flake: allow tiny rendering differences (font hinting across runners).\n toHaveScreenshot: { maxDiffPixelRatio: 0.01 },\n },\n\n webServer: {\n command: "bun dev",\n url: "http://localhost:3000",\n reuseExistingServer: !process.env.CI,\n timeout: 120_000,\n stdout: "ignore",\n stderr: "pipe",\n },\n});\n' }, { "path": "tsconfig.json", "kind": "text", "content": '{\n "compilerOptions": {\n "target": "ES2022",\n "lib": ["dom", "dom.iterable", "esnext"],\n "allowJs": false,\n "skipLibCheck": true,\n "strict": true,\n "noEmit": true,\n "esModuleInterop": true,\n "module": "esnext",\n "moduleResolution": "bundler",\n "resolveJsonModule": true,\n "isolatedModules": true,\n "jsx": "preserve",\n "incremental": true,\n "plugins": [{ "name": "next" }],\n "paths": {\n "@/*": ["./*"]\n }\n },\n "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts"],\n "exclude": ["node_modules"]\n}\n' }, { "path": "CLAUDE.md", "kind": "text", "content": "# CLAUDE.md\n\nThis project was scaffolded from a Cimplify storefront template (`cimplify init`).\n\n## Read these first\n\n- **`AGENTS.md`** at the project root \u2014 the file \u2194 `brand.X` field map for *this template's* industry, plus the architectural rules.\n- **`.claude/skills/cimplify-storefront/SKILL.md`** \u2014 the playbook for common tasks (rebrand, add a section, wire a Server Action, deploy, eject a component). Invoke it whenever you're asked to change content, palette, copy, or routing.\n\n## The two-line summary\n\n1. **Edit `lib/brand.ts`** for any visible string.\n2. **Edit `app/globals.css` `@theme` block** for any palette / radius / font change.\n\nThat covers ~95% of what merchants ask for. For anything else, follow the `cimplify-storefront` skill.\n\n## Don't do\n\n- Hardcode strings in pages or components.\n- Disable `cacheComponents: true` in `next.config.ts`.\n- Use `unstable_cache` \u2014 Next 16's canonical primitive is `'use cache'`.\n- Run `bun test` (Bun's `vi` shim is incomplete) \u2014 use `bun run test:run`.\n" }, { "path": "README.md", "kind": "text", "content": "# __STOREFRONT_NAME__\n\nA Cimplify storefront scaffolded from the **retail** template \u2014 Next.js 16 (App Router), React 19, Tailwind v4. Cool navy + electric blue palette, Inter + JetBrains Mono typography, modern electronics-store aesthetic.\n\n## Run\n\n```bash\nbun install\nbun dev\n```\n\nTwo things start in parallel:\n\n- `cimplify-mock --seed retail` \u2014 the Cimplify mock API on `http://127.0.0.1:8787`, seeded with Currents Electronics.\n- `next dev` \u2014 this storefront on `http://localhost:3000`.\n\nOpen the storefront in your browser. Edit `app/page.tsx` to start customising.\n\n## Structure\n\n```\napp/\n layout.tsx # root layout, fonts, providers, header/footer/modal\n page.tsx # home\n shop/page.tsx # full catalogue (SDK <CataloguePage/>)\n collections/[slug]/ # collection landing\n categories/[slug]/ # category landing\n cart/page.tsx # SDK <CartPage/>\n checkout/page.tsx # SDK <CheckoutPage/>\n orders/[id]/page.tsx # post-checkout thank-you\n about, faq, terms, privacy\n globals.css # Tailwind import + theme tokens\ncomponents/\n providers.tsx # CimplifyProvider client wrapper\n header.tsx, footer.tsx, hero.tsx\n store-product-card.tsx # SDK <ProductCard/> wired to URL-driven modal\n product-modal.tsx # ?product=<slug> deep-linkable modal\n collection-strip.tsx # horizontal product strip\n category-grid.tsx # SDK <CategoryGrid/> with router navigation\nlib/\n cart.ts # useCartCount() for the header pill\n```\n\n## Switch the seed\n\nThis template is wired to the `retail` seed. To preview a different industry without re-scaffolding:\n\n```bash\ncimplify-mock --seed restaurant # Mama's Kitchen\ncimplify-mock --seed services # Serene Spa\ncimplify-mock --seed grocery # FreshMart\n```\n\nFor a fresh scaffold with another design altogether:\n\n```bash\ncimplify init my-store --template bakery # warm food/pastry\ncimplify init my-store --template restaurant # coming soon\ncimplify init my-store --template services # coming soon\ncimplify init my-store --template grocery # coming soon\n```\n\n## Go live\n\n```diff\n# .env.local\n- NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev\n+ NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=<your tenant key>\n```\n\nDeploy with `cimplify deploy --prod` after linking the project. See [`cimplify` CLI docs](https://www.cimplify.dev/docs/cli). `next.config.ts` already whitelists the SDK image hosts under `images.remotePatterns`.\n" }, { "path": "bun.lock", "kind": "binary", "content": "ewogICJsb2NrZmlsZVZlcnNpb24iOiAxLAogICJjb25maWdWZXJzaW9uIjogMSwKICAid29ya3NwYWNlcyI6IHsKICAgICIiOiB7CiAgICAgICJuYW1lIjogIl9fU1RPUkVGUk9OVF9OQU1FX18iLAogICAgICAiZGVwZW5kZW5jaWVzIjogewogICAgICAgICJAY2ltcGxpZnkvc2RrIjogIl4wLjQ4LjIiLAogICAgICAgICJuZXh0IjogIl4xNi4yLjQiLAogICAgICAgICJyZWFjdCI6ICJeMTkuMC4wIiwKICAgICAgICAicmVhY3QtZG9tIjogIl4xOS4wLjAiLAogICAgICB9LAogICAgICAiZGV2RGVwZW5kZW5jaWVzIjogewogICAgICAgICJAcGxheXdyaWdodC90ZXN0IjogIl4xLjUwLjAiLAogICAgICAgICJAdGFpbHdpbmRjc3MvcG9zdGNzcyI6ICJeNC4yLjQiLAogICAgICAgICJAdHlwZXMvbm9kZSI6ICJeMjIuMTAuMCIsCiAgICAgICAgIkB0eXBlcy9yZWFjdCI6ICJeMTkuMC4wIiwKICAgICAgICAiQHR5cGVzL3JlYWN0LWRvbSI6ICJeMTkuMC4wIiwKICAgICAgICAiY29uY3VycmVudGx5IjogIl45LjAuMCIsCiAgICAgICAgInRhaWx3aW5kY3NzIjogIl40LjIuNCIsCiAgICAgICAgInR5cGVzY3JpcHQiOiAiXjUuOS4zIiwKICAgICAgICAidml0ZXN0IjogIl40LjEuNSIsCiAgICAgIH0sCiAgICB9LAogIH0sCiAgInBhY2thZ2VzIjogewogICAgIkBhbGxvYy9xdWljay1scnUiOiBbIkBhbGxvYy9xdWljay1scnVANS4yLjAiLCAiIiwge30sICJzaGE1MTItVXJjQUJCKzRiVXJGQUJ3Ymx1VElCRXJYd3Zic1UvVjdUWldmbWJnSmZia3dpQnV6aVM5Z3hkT0RVeXVpZWNmZEdRODVqZ2xNVzZqdVMzK3o1VHNLTHc9PSJdLAoKICAgICJAYmFiZWwvcnVudGltZSI6IFsiQGJhYmVsL3J1bnRpbWVANy4yOS4yIiwgIiIsIHt9LCAic2hhNTEyLUppRFNoSDQ1ektIV3lHZTRaTlZSckNqQno4Tmg5VE1tWkcxa2g0UVRLOGhDQlRXQmk4RGEraTdzMWZKdzcvbFlwTTRjY2VwU05mcXpaL1F2QUJCaTVnPT0iXSwKCiAgICAiQGJhc2UtdWkvcmVhY3QiOiBbIkBiYXNlLXVpL3JlYWN0QDEuNS4wIiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIkBiYWJlbC9ydW50aW1lIjogIl43LjI5LjIiLCAiQGJhc2UtdWkvdXRpbHMiOiAiMC4yLjkiLCAiQGZsb2F0aW5nLXVpL3JlYWN0LWRvbSI6ICJeMi4xLjgiLCAiQGZsb2F0aW5nLXVpL3V0aWxzIjogIl4wLjIuMTEiLCAidXNlLXN5bmMtZXh0ZXJuYWwtc3RvcmUiOiAiXjEuNi4wIiB9LCAicGVlckRlcGVuZGVuY2llcyI6IHsgIkBkYXRlLWZucy90eiI6ICJeMS4yLjAiLCAiQHR5cGVzL3JlYWN0IjogIl4xNyB8fCBeMTggfHwgXjE5IiwgImRhdGUtZm5zIjogIl40LjAuMCIsICJyZWFjdCI6ICJeMTcgfHwgXjE4IHx8IF4xOSIsICJyZWFjdC1kb20iOiAiXjE3IHx8IF4xOCB8fCBeMTkiIH0sICJvcHRpb25hbFBlZXJzIjogWyJAZGF0ZS1mbnMvdHoiLCAiQHR5cGVzL3JlYWN0IiwgImRhdGUtZm5zIl0gfSwgInNoYTUxMi16MWdTQWxjZWQxeVkraU0rbUhERXRJa0Q4VUkzRWJzNTJNdUJQeHZWNmY1aFJ1dGsreHZDSC93dUI3aERxRHpLOUpHNUZvTXo1bmhycXRTczF3anQxQT09Il0sCgogICAgIkBiYXNlLXVpL3V0aWxzIjogWyJAYmFzZS11aS91dGlsc0AwLjIuOSIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAYmFiZWwvcnVudGltZSI6ICJeNy4yOS4yIiwgIkBmbG9hdGluZy11aS91dGlscyI6ICJeMC4yLjExIiwgInJlc2VsZWN0IjogIl41LjEuMSIsICJ1c2Utc3luYy1leHRlcm5hbC1zdG9yZSI6ICJeMS42LjAiIH0sICJwZWVyRGVwZW5kZW5jaWVzIjogeyAiQHR5cGVzL3JlYWN0IjogIl4xNyB8fCBeMTggfHwgXjE5IiwgInJlYWN0IjogIl4xNyB8fCBeMTggfHwgXjE5IiwgInJlYWN0LWRvbSI6ICJeMTcgfHwgXjE4IHx8IF4xOSIgfSwgIm9wdGlvbmFsUGVlcnMiOiBbIkB0eXBlcy9yZWFjdCJdIH0sICJzaGE1MTIteC9QRERDWXpvcVBwanJkeWIzVmN5eWxUSTJJalVYRXRZREdpNWZvaDdLc25tTkpJSWFWd0EyR0xnREgxZHBzMUdnWGlKYkE2MGhNK0F5dVRmUXpJdnc9PSJdLAoKICAgICJAY2ltcGxpZnkvc2RrIjogWyJAY2ltcGxpZnkvc2RrQDAuNDguMiIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAYmFzZS11aS9yZWFjdCI6ICJeMS40LjEiLCAiY2xzeCI6ICJeMi4xLjEiLCAiZGF0ZS1mbnMiOiAiXjQuMS4wIiwgImxpYnBob25lbnVtYmVyLWpzIjogIjEuMTIuNDEiLCAicmVhY3QtZGF5LXBpY2tlciI6ICJeOS4xNC4wIiwgInRhaWx3aW5kLW1lcmdlIjogIl4zLjUuMCIsICJ6b2QiOiAiXjQuNC4zIiB9LCAicGVlckRlcGVuZGVuY2llcyI6IHsgIkBwYXlzdGFjay9pbmxpbmUtanMiOiAiXjIuMjIuOCIsICJtc3ciOiAiPj0yLjAuMCIsICJyZWFjdCI6ICI+PTE3LjAuMCIsICJ2aXRlc3QiOiAiPj0yLjAuMCIgfSwgIm9wdGlvbmFsUGVlcnMiOiBbIkBwYXlzdGFjay9pbmxpbmUtanMiLCAibXN3IiwgInJlYWN0IiwgInZpdGVzdCJdLCAiYmluIjogeyAiY2ltcGxpZnktbW9jayI6ICJkaXN0L21vY2svY2xpLm1qcyIgfSB9LCAic2hhNTEyLTZJaDhNNkxZVkhDdU84c2JnZWc3VmdQTUJIWHZNK2NodFkzLzJWZkdWRWZFeU5UY0d4UzZlcDFOMDdNS21tTG5xQWNMZTZteFQwbVFiL2h0cldQZ1hBPT0iXSwKCiAgICAiQGRhdGUtZm5zL3R6IjogWyJAZGF0ZS1mbnMvdHpAMS40LjEiLCAiIiwge30sICJzaGE1MTItUDVMVU5odGJqNllmSTNpSmp3NUVMOWVVQUc2T2l0RDBXM2ZXUWNwUWpEUmMvUUlzTDB0Uk51TzFQY0R2UGNjV0wxZlNUWFhkRTFkcytsOTVEVi9PRkE9PSJdLAoKICAgICJAZW1uYXBpL2NvcmUiOiBbIkBlbW5hcGkvY29yZUAxLjEwLjAiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiQGVtbmFwaS93YXNpLXRocmVhZHMiOiAiMS4yLjEiLCAidHNsaWIiOiAiXjIuNC4wIiB9IH0sICJzaGE1MTIteXE2T2tKNHA4MkNBZlBsMHU5bVFlYlFIS1BKa1k3V3JJdWsyMDVjVFluWWUrazJaOFlCaDExRnJiUkcvSDZpaGlycWNhY09nbDJCSU84b3lNUUxlWHc9PSJdLAoKICAgICJAZW1uYXBpL3J1bnRpbWUiOiBbIkBlbW5hcGkvcnVudGltZUAxLjEwLjAiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAidHNsaWIiOiAiXjIuNC4wIiB9IH0sICJzaGE1MTItZXd2WWxrODZ4VW9HSTB6UVJOcS9tQysxNlIxUWVEbEtReTIxS2kzb1NZWE5nTGI0NUdWMVA2QTBNKy9zNm55Q3VORHFlNVZwYVk4NEJ6WEd3VmJ3RkE9PSJdLAoKICAgICJAZW1uYXBpL3dhc2ktdGhyZWFkcyI6IFsiQGVtbmFwaS93YXNpLXRocmVhZHNAMS4yLjEiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAidHNsaWIiOiAiXjIuNC4wIiB9IH0sICJzaGE1MTItdVRJSTdPWUYrL01lcy9NcmNJT1lwNXlPdFNNTEJXU0lvTFBwY2d3aXBvaUtibGk2azMyMnRjb0ZzeG9JSXhQRHFXMDFTUUdBZ2tvNEV6WmkyQk52Mnc9PSJdLAoKICAgICJAZmxvYXRpbmctdWkvY29yZSI6IFsiQGZsb2F0aW5nLXVpL2NvcmVAMS43LjUiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiQGZsb2F0aW5nLXVpL3V0aWxzIjogIl4wLjIuMTEiIH0gfSwgInNoYTUxMi0xSWg0V1RXeXcwK2xLeUZNY0JIR2JiNVU1RnR1SEp1dWpveXlyNXpUYVdTNUVZTWVUNkpiMkF1RGVmdHNDc0V1Y2hPK21NMmlqNStxOWNyaHlkekxoUT09Il0sCgogICAgIkBmbG9hdGluZy11aS9kb20iOiBbIkBmbG9hdGluZy11aS9kb21AMS43LjYiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiQGZsb2F0aW5nLXVpL2NvcmUiOiAiXjEuNy41IiwgIkBmbG9hdGluZy11aS91dGlscyI6ICJeMC4yLjExIiB9IH0sICJzaGE1MTItOWdaU0FJNVhNMzY4ODBQUE1tLy85ZGZpRW5nWW9DNkFtMml6RVMxRkY0MDZZRnNqdnlCTW1lSjJnNFNBanUzeFd3dHV5bk5SRkwyczloZ3hwTEk1U1E9PSJdLAoKICAgICJAZmxvYXRpbmctdWkvcmVhY3QtZG9tIjogWyJAZmxvYXRpbmctdWkvcmVhY3QtZG9tQDIuMS44IiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIkBmbG9hdGluZy11aS9kb20iOiAiXjEuNy42IiB9LCAicGVlckRlcGVuZGVuY2llcyI6IHsgInJlYWN0IjogIj49MTYuOC4wIiwgInJlYWN0LWRvbSI6ICI+PTE2LjguMCIgfSB9LCAic2hhNTEyLWNDNTJiSHdNL24vQ3hTODdGSDB5V2RuZ0VacmpkdExXL3FWcnVvNjhxZytwcks3WlE0WUdkdXQyR3lEVnBvR2VBWWUvaDg5OXJWZU9WbTZPaTQwazJBPT0iXSwKCiAgICAiQGZsb2F0aW5nLXVpL3V0aWxzIjogWyJAZmxvYXRpbmctdWkvdXRpbHNAMC4yLjExIiwgIiIsIHt9LCAic2hhNTEyLVJpQi95SWg3OHBjSXhsNmxMTUcwQ2dCWEFaMlkwZVZIcU1QWXVndSs5VTBBZVQ2WUJlaUpwZjdsYmRKTkl1Z0ZQNVNJandOUmdvNERoUjFReGkyNkdnPT0iXSwKCiAgICAiQGltZy9jb2xvdXIiOiBbIkBpbWcvY29sb3VyQDEuMS4wIiwgIiIsIHt9LCAic2hhNTEyLVRkNzZxN2o1N28vdExWZGdTNzQ2Y1lBUmZTeXhrOGlFZlJ4ZXdMOWg0T016WWhiVzRUQWNwcGwwbVQ0ZXlxWGRkaDZML2p3b003NW1vN2l4YS9wQ2VRPT0iXSwKCiAgICAiQGltZy9zaGFycC1kYXJ3aW4tYXJtNjQiOiBbIkBpbWcvc2hhcnAtZGFyd2luLWFybTY0QDAuMzQuNSIsICIiLCB7ICJvcHRpb25hbERlcGVuZGVuY2llcyI6IHsgIkBpbWcvc2hhcnAtbGlidmlwcy1kYXJ3aW4tYXJtNjQiOiAiMS4yLjQiIH0sICJvcyI6ICJkYXJ3aW4iLCAiY3B1IjogImFybTY0IiB9LCAic2hhNTEyLWltdFEzV01KWGJNWTRmeGIvTmRwNkhCVE5WdFdDVUkwV2RvYnloZUdmNSthZDZ4WDhWSURPOHUyeEU0cWMvZnIwOENLRy83ZERzZUZ0bjZNNmcvcjN3PT0iXSwKCiAgICAiQGltZy9zaGFycC1kYXJ3aW4teDY0IjogWyJAaW1nL3NoYXJwLWRhcndpbi14NjRAMC4zNC41IiwgIiIsIHsgIm9wdGlvbmFsRGVwZW5kZW5jaWVzIjogeyAiQGltZy9zaGFycC1saWJ2aXBzLWRhcndpbi14NjQiOiAiMS4yLjQiIH0sICJvcyI6ICJkYXJ3aW4iLCAiY3B1IjogIng2NCIgfSwgInNoYTUxMi1ZTkVGQUYvNEtRL1BlVzBOK3IrYVZWc29JWTAvcXh4aWtGMlNXZHArTlJrbU1CN3k5TEJaQVZxUTR5aEdDbS9IM0gyNzBPU3lrcW1RTUtMQmhCSkRFdz09Il0sCgogICAgIkBpbWcvc2hhcnAtbGlidmlwcy1kYXJ3aW4tYXJtNjQiOiBbIkBpbWcvc2hhcnAtbGlidmlwcy1kYXJ3aW4tYXJtNjRAMS4yLjQiLCAiIiwgeyAib3MiOiAiZGFyd2luIiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi16cWpqbzdSYXRGZkZvUDBNa1E1MWpmdUZaQm5WRTJwUmlheWRLSjFHL3JIWnZuc3JIQU9jUUFMSWk5c0E1Y281eGVuUWRUdWdDdnRiMWN1Zjc4VmY0Zz09Il0sCgogICAgIkBpbWcvc2hhcnAtbGlidmlwcy1kYXJ3aW4teDY0IjogWyJAaW1nL3NoYXJwLWxpYnZpcHMtZGFyd2luLXg2NEAxLjIuNCIsICIiLCB7ICJvcyI6ICJkYXJ3aW4iLCAiY3B1IjogIng2NCIgfSwgInNoYTUxMi0xSU9kNXhmVmhsR3dYK3pYdjJOOTNrMHlNT052VWxBTnlsYkp3MWVUYWg4Sy9KdHBpMTVLQytXU2lhWC9uQm1ibTJIeFJNMWdaMG5TZGpTc3JaYkdLZz09Il0sCgogICAgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC1hcm0iOiBbIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC1hcm1AMS4yLjQiLCAiIiwgeyAib3MiOiAibGludXgiLCAiY3B1IjogImFybSIgfSwgInNoYTUxMi1iRkk3eGNLRkVMZGlOQ1ZvdjhlNDRJYTR1MmJ5QStsM1h0c0FqK1E4dGZDd082QlE4aURvallkdm9QTXFzS0RrdW9PbytYNkhaQTBzMHExMUFOTVE4QT09Il0sCgogICAgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC1hcm02NCI6IFsiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4LWFybTY0QDEuMi40IiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi1leGNqWDhEZnNJY0oxMHgxS3pyNFJjV2UxZWRDOVBxdURSUlB4M1lWQ3ZRditVNXA3WWluMnMzMmZ0emlrWG9qYjFQSUZjLzlNdDI4L3kraVJrbGtydz09Il0sCgogICAgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC1wcGM2NCI6IFsiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4LXBwYzY0QDEuMi40IiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJwcGM2NCIgfSwgInNoYTUxMi1GTXV2R2lqTERZRzZsVytiL1V2eWlsVVd1NUF5dSszcjJkMVM4bm90aUdDSXlZVS83NmVpZzFVZk1ta1o3dndnT3J6S3psUWJGU3VRZmdtN0dZVVBwQT09Il0sCgogICAgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC1yaXNjdjY0IjogWyJAaW1nL3NoYXJwLWxpYnZpcHMtbGludXgtcmlzY3Y2NEAxLjIuNCIsICIiLCB7ICJvcyI6ICJsaW51eCIsICJjcHUiOiAibm9uZSIgfSwgInNoYTUxMi1vVkRiY1I0elVDMGNlODJ0ZXViU20reDZFVGl4dEtaQmgvcWJSRUlPY0kzY1VMekR5YjE4U3IvV2N5eDdOUlFlUXpPaUhUTmJaRkYxVXdQUzJzY3lHQT09Il0sCgogICAgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC1zMzkweCI6IFsiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4LXMzOTB4QDEuMi40IiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJzMzkweCIgfSwgInNoYTUxMi1xbXA5VnJ6Z1BnTW9HWnlQdnJRSHFrMDJ1eWpBMC9RclRPMjZUcWs2bDRaVjBNUFdJVzZMVGtxT0lvditKMXlFdTdNYkZRYURwd2R3SktoYkp2dVJ4UT09Il0sCgogICAgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC14NjQiOiBbIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC14NjRAMS4yLjQiLCAiIiwgeyAib3MiOiAibGludXgiLCAiY3B1IjogIng2NCIgfSwgInNoYTUxMi10SnhpaUxzbUhjOUF4MWJ6M29hT1lCVVJUWEdJUkRPREJxaHZlVkhvbnJISjkvK2s4OXFiTGwwYmNKbnMrZTR0NHJ2YU5CeGFFWnNGdFNmQWRxdVBydz09Il0sCgogICAgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eG11c2wtYXJtNjQiOiBbIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eG11c2wtYXJtNjRAMS4yLjQiLCAiIiwgeyAib3MiOiAibGludXgiLCAiY3B1IjogImFybTY0IiB9LCAic2hhNTEyLUZWUUh1d3gxSUl1Tm93OVFBYllVekorRW44S2NWbTlMazUrdUdVUUpIYVptTUVDWm1PbGl4OUhuSDduMVRSa1hNUzBwR3hJSm9rSVZCOVN1cVpHR1h3PT0iXSwKCiAgICAiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4bXVzbC14NjQiOiBbIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eG11c2wteDY0QDEuMi40IiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJ4NjQiIH0sICJzaGE1MTItK0xweUJrN0w0NFpJWHd6L1ZZZmdsYVgvb2t4ZXpFU2M2VXhEU295bzJLczZKeGM0WTdzR2pwZ1U5czRQTWdxZ2pqMWdaQ3lsVGllTmFtcUExTUY3RGc9PSJdLAoKICAgICJAaW1nL3NoYXJwLWxpbnV4LWFybSI6IFsiQGltZy9zaGFycC1saW51eC1hcm1AMC4zNC41IiwgIiIsIHsgIm9wdGlvbmFsRGVwZW5kZW5jaWVzIjogeyAiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4LWFybSI6ICIxLjIuNCIgfSwgIm9zIjogImxpbnV4IiwgImNwdSI6ICJhcm0iIH0sICJzaGE1MTItOWRMcXN2d3RnMXV1WEJHWktzeGVtOTU5NSt1anYwc0o2Vmk4d2NUQU5TRnB3Vi9HT05hdDVlQ2t6UW8vMU82elJJa2gwbS84KzVCanJScjdqRFVTWnc9PSJdLAoKICAgICJAaW1nL3NoYXJwLWxpbnV4LWFybTY0IjogWyJAaW1nL3NoYXJwLWxpbnV4LWFybTY0QDAuMzQuNSIsICIiLCB7ICJvcHRpb25hbERlcGVuZGVuY2llcyI6IHsgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC1hcm02NCI6ICIxLjIuNCIgfSwgIm9zIjogImxpbnV4IiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi1iS1F6YUpSWS9ia1BPWHlLeDVFVnVwN3FrYW9qRUNHNk5MWXN3Z2t0T1pqYVhlY1NBZUNXaVp3d2lGZjMvWStPMUhyYXVpRTNGVnNHeEZnOGMyNHJaZz09Il0sCgogICAgIkBpbWcvc2hhcnAtbGludXgtcHBjNjQiOiBbIkBpbWcvc2hhcnAtbGludXgtcHBjNjRAMC4zNC41IiwgIiIsIHsgIm9wdGlvbmFsRGVwZW5kZW5jaWVzIjogeyAiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4LXBwYzY0IjogIjEuMi40IiB9LCAib3MiOiAibGludXgiLCAiY3B1IjogInBwYzY0IiB9LCAic2hhNTEyLTd6em53TmFxVzZZdHNmckdHREE2QlJrSVNLQUFFMUpvMFFkcE5ZWE5NSHUyKzBkVHJQZmxUTE5rcGM4bDdNVVA1TTE2WkpjVXZ5c1ZXV3JNZWZacXVBPT0iXSwKCiAgICAiQGltZy9zaGFycC1saW51eC1yaXNjdjY0IjogWyJAaW1nL3NoYXJwLWxpbnV4LXJpc2N2NjRAMC4zNC41IiwgIiIsIHsgIm9wdGlvbmFsRGVwZW5kZW5jaWVzIjogeyAiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4LXJpc2N2NjQiOiAiMS4yLjQiIH0sICJvcyI6ICJsaW51eCIsICJjcHUiOiAibm9uZSIgfSwgInNoYTUxMi01MWdKdUxQVEthN3BpWVBhVnM4R21CeW83L1U3LzdUWk9xK2NuWEpJSFpLYXZJUkhBUDc3ZTNOMkhFbDNkZ2lxZEQvdzB5VWZpSm5JSTc3UHVEREZkdz09Il0sCgogICAgIkBpbWcvc2hhcnAtbGludXgtczM5MHgiOiBbIkBpbWcvc2hhcnAtbGludXgtczM5MHhAMC4zNC41IiwgIiIsIHsgIm9wdGlvbmFsRGVwZW5kZW5jaWVzIjogeyAiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4LXMzOTB4IjogIjEuMi40IiB9LCAib3MiOiAibGludXgiLCAiY3B1IjogInMzOTB4IiB9LCAic2hhNTEyLW5RdENrMFBkS2ZobzNlQzVNcmJRb2lnSjJnZDFDZ2RkVU1rYWJVaityQmV2czh0WjJjVUxPeDQ2RTdveVgrMDRXR2ZBQmdJd21NQzBWcWllVGlSNGpnPT0iXSwKCiAgICAiQGltZy9zaGFycC1saW51eC14NjQiOiBbIkBpbWcvc2hhcnAtbGludXgteDY0QDAuMzQuNSIsICIiLCB7ICJvcHRpb25hbERlcGVuZGVuY2llcyI6IHsgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC14NjQiOiAiMS4yLjQiIH0sICJvcyI6ICJsaW51eCIsICJjcHUiOiAieDY0IiB9LCAic2hhNTEyLU1FemQ4SFBLeFZ4VmVud0FhK0pSUHdFQzdRRmpvUFd1UzVOWm5CdDZCM3B1N0VHMkdlMGlkMW9MSFpwUEpkbjNPUUsrQlFEaXc5elN0aUhCVEpRUVFRPT0iXSwKCiAgICAiQGltZy9zaGFycC1saW51eG11c2wtYXJtNjQiOiBbIkBpbWcvc2hhcnAtbGludXhtdXNsLWFybTY0QDAuMzQuNSIsICIiLCB7ICJvcHRpb25hbERlcGVuZGVuY2llcyI6IHsgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eG11c2wtYXJtNjQiOiAiMS4yLjQiIH0sICJvcyI6ICJsaW51eCIsICJjcHUiOiAiYXJtNjQiIH0sICJzaGE1MTItZnBySlI2R3RSc010Nkt5ZnE0NElzQ2hWWmVHTjk3Z1REMzMxd2VSMWV4MWMxcnlwREVBQk42VG0yeGExd0U2bFliNURkRW5rMDNOWlBxQTdJZDIxeWc9PSJdLAoKICAgICJAaW1nL3NoYXJwLWxpbnV4bXVzbC14NjQiOiBbIkBpbWcvc2hhcnAtbGludXhtdXNsLXg2NEAwLjM0LjUiLCAiIiwgeyAib3B0aW9uYWxEZXBlbmRlbmNpZXMiOiB7ICJAaW1nL3NoYXJwLWxpYnZpcHMtbGludXhtdXNsLXg2NCI6ICIxLjIuNCIgfSwgIm9zIjogImxpbnV4IiwgImNwdSI6ICJ4NjQiIH0sICJzaGE1MTItSmc4d05UMU1Vekl2aEJGeFZpcXJFaFdER3pxeW1vM3NWN3o3WnNhV2JaTkRMWFJKWm9SR3JqdWxwNjBZWXRWNHdmWThWSUtjV2lkam9qbExjV3JkOFE9PSJdLAoKICAgICJAaW1nL3NoYXJwLXdhc20zMiI6IFsiQGltZy9zaGFycC13YXNtMzJAMC4zNC41IiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIkBlbW5hcGkvcnVudGltZSI6ICJeMS43LjAiIH0sICJjcHUiOiAibm9uZSIgfSwgInNoYTUxMi1PZFdURWlWa1kyUEh3cWtiQkk4ZnJGeFFRRmVrSGFTU2tVSUprd3pjbFdaZTY0TzFYNFVsVWpxcXFMYVBiVXBNT1FrNkZCdS9IdGxHWE5ibElzMGh1dz09Il0sCgogICAgIkBpbWcvc2hhcnAtd2luMzItYXJtNjQiOiBbIkBpbWcvc2hhcnAtd2luMzItYXJtNjRAMC4zNC41IiwgIiIsIHsgIm9zIjogIndpbjMyIiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi1XUTNBZ1dDV1lTYjJ5dCtJRzhtbkM2SmRrOVdoczdPMGd4cGhibHNMdmRoU3BTVHRtdTY5WkcxR2tiNk51dnhzTkFDd2lQVjZjTlNaTnp0MEtQc3c3Zz09Il0sCgogICAgIkBpbWcvc2hhcnAtd2luMzItaWEzMiI6IFsiQGltZy9zaGFycC13aW4zMi1pYTMyQDAuMzQuNSIsICIiLCB7ICJvcyI6ICJ3aW4zMiIsICJjcHUiOiAiaWEzMiIgfSwgInNoYTUxMi1GVjltLzdObWVDbVNIREQ1ajQrNHBOSThDcDNhVytKdkxvWGNUVW8wSXF5alNmQVpKOGRJVW1pangxcWFKc0lpVStIb3N3NnhNNUtpakFXUkpDU2dOZz09Il0sCgogICAgIkBpbWcvc2hhcnAtd2luMzIteDY0IjogWyJAaW1nL3NoYXJwLXdpbjMyLXg2NEAwLjM0LjUiLCAiIiwgeyAib3MiOiAid2luMzIiLCAiY3B1IjogIng2NCIgfSwgInNoYTUxMi0rMjlZTXNxWTIvOWVGRWlXOTNlcVdudUxjV2N1Zm93WGV3d1NOSVQ2VXdaZFVVQ3JNM29Gak1XSC9aNi9UTW1iNGhsRmVubWZBVmJwV2V1cDJqcnlDdz09Il0sCgogICAgIkBqcmlkZ2V3ZWxsL2dlbi1tYXBwaW5nIjogWyJAanJpZGdld2VsbC9nZW4tbWFwcGluZ0AwLjMuMTMiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiQGpyaWRnZXdlbGwvc291cmNlbWFwLWNvZGVjIjogIl4xLjUuMCIsICJAanJpZGdld2VsbC90cmFjZS1tYXBwaW5nIjogIl4wLjMuMjQiIH0gfSwgInNoYTUxMi0ya2t0LzduaUo2TWdFUHhGMGJZZFE2ZXRaYUErZlF2RGNMS2NraHkxeUlRT3phb0tqQkJqU2o2My9hTFZqWUUzcWhSdDVkdk0rdVV5ZkNnNlVLQ0JiQT09Il0sCgogICAgIkBqcmlkZ2V3ZWxsL3JlbWFwcGluZyI6IFsiQGpyaWRnZXdlbGwvcmVtYXBwaW5nQDIuMy41IiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIkBqcmlkZ2V3ZWxsL2dlbi1tYXBwaW5nIjogIl4wLjMuNSIsICJAanJpZGdld2VsbC90cmFjZS1tYXBwaW5nIjogIl4wLjMuMjQiIH0gfSwgInNoYTUxMi1MSTl1LytsYVlHNERzMVRES1NKVzJZUHJJbGNWWU93aTJmVUM2eEI0M2x1ZUNqZ3hWNGxmZk9DWkN0WUZpSDZUTk9YK3RRS1h4OTdUNElLSGJoeUhFUT09Il0sCgogICAgIkBqcmlkZ2V3ZWxsL3Jlc29sdmUtdXJpIjogWyJAanJpZGdld2VsbC9yZXNvbHZlLXVyaUAzLjEuMiIsICIiLCB7fSwgInNoYTUxMi1iUklTZ0NJalAyMC90YldTUFdNRWk1NFFWUFJaRXhrdUQ5bEpMK1VJeFVLdHdWSkE4d1cxVHJiMWpNczFSRlhvMUNCVE5aLzVocEM5UXZtS1dkb3BLdz09Il0sCgogICAgIkBqcmlkZ2V3ZWxsL3NvdXJjZW1hcC1jb2RlYyI6IFsiQGpyaWRnZXdlbGwvc291cmNlbWFwLWNvZGVjQDEuNS41IiwgIiIsIHt9LCAic2hhNTEyLWNZUTkzMTBncnF4dWVXYmwrV3VJVUlhaVVhRGNqN1dPcTVmVmhFbGpOVmdSZk9VaFk5ZnkyelR2Zm9xV3NuZWJoOFNsNzBWU2NGYklDdkpuTEtCME9nPT0iXSwKCiAgICAiQGpyaWRnZXdlbGwvdHJhY2UtbWFwcGluZyI6IFsiQGpyaWRnZXdlbGwvdHJhY2UtbWFwcGluZ0AwLjMuMzEiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiQGpyaWRnZXdlbGwvcmVzb2x2ZS11cmkiOiAiXjMuMS4wIiwgIkBqcmlkZ2V3ZWxsL3NvdXJjZW1hcC1jb2RlYyI6ICJeMS40LjE0IiB9IH0sICJzaGE1MTItenpOUitTZFFTREp6Yzhqb2FlUDhRUW9DUXI4TnVZeDJkSUl5dGwxUWVCRVpISjl1VzZoZWJzcllnYno4aEp3VVFhbzNUV0NNdG1mVjhOdTF0d09MQXc9PSJdLAoKICAgICJAbmFwaS1ycy93YXNtLXJ1bnRpbWUiOiBbIkBuYXBpLXJzL3dhc20tcnVudGltZUAxLjEuNCIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAdHlieXMvd2FzbS11dGlsIjogIl4wLjEwLjEiIH0sICJwZWVyRGVwZW5kZW5jaWVzIjogeyAiQGVtbmFwaS9jb3JlIjogIl4xLjcuMSIsICJAZW1uYXBpL3J1bnRpbWUiOiAiXjEuNy4xIiB9IH0sICJzaGE1MTItM05RTk5nQTFZU2xKYi9rTUgxaWxkQVNQOUhXNy83a1luUkkyc3pXSmFvZmFTMWhXbWJHSTRIK2QzKzIyYUd6WFhOOUlKK24rR2lGVmNHaXBKUDE4b3c9PSJdLAoKICAgICJAbmV4dC9lbnYiOiBbIkBuZXh0L2VudkAxNi4yLjYiLCAiIiwge30sICJzaGE1MTItZ2Q4SG9ITjR1Zmo3M1dtUjNKbVZvbHJwSlI0N0lMSzZMb3VQNXhFbFBnbGFWeGlyNmUxYTdWenZUdkRXa09vUFhUOXJra1R6eUN4QnU0eWVaZlp3Y3c9PSJdLAoKICAgICJAbmV4dC9zd2MtZGFyd2luLWFybTY0IjogWyJAbmV4dC9zd2MtZGFyd2luLWFybTY0QDE2LjIuNiIsICIiLCB7ICJvcyI6ICJkYXJ3aW4iLCAiY3B1IjogImFybTY0IiB9LCAic2hhNTEyLVpKR2trY05mWWdyck1rcU9kWjd6b0xhMVRPeTBxcGNNZmsvejRNaC9GS1V6NDBnVk8rSE5RV3FtTHhmNjdaNVdCNjREUnAwZGhFYnlIZmVsKzZzSlVnPT0iXSwKCiAgICAiQG5leHQvc3djLWRhcndpbi14NjQiOiBbIkBuZXh0L3N3Yy1kYXJ3aW4teDY0QDE2LjIuNiIsICIiLCB7ICJvcyI6ICJkYXJ3aW4iLCAiY3B1IjogIng2NCIgfSwgInNoYTUxMi12L1lMQkhJWTEzMkNlZDNwdUJKN1lKS3cxbHFzQ3JnY05vMmFSSmxDRXlRcnJDZVJKbHZHbG5teGhQeE5RSTNLRTNOMURONXI5VFBOUHZrYTNucTVSUT09Il0sCgogICAgIkBuZXh0L3N3Yy1saW51eC1hcm02NC1nbnUiOiBbIkBuZXh0L3N3Yy1saW51eC1hcm02NC1nbnVAMTYuMi42IiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi1SUE92cWxZQmJjUWprejlWUVFEWjJUMmJBUklqWFpWMUtGbHQrVjJNcjZTVy9lNEk5ZmNLc2FBMGhkeWYyRkhvVGxzVjJ4bkJkNVk5MTJyUC8xQ2U2dz09Il0sCgogICAgIkBuZXh0L3N3Yy1saW51eC1hcm02NC1tdXNsIjogWyJAbmV4dC9zd2MtbGludXgtYXJtNjQtbXVzbEAxNi4yLjYiLCAiIiwgeyAib3MiOiAibGludXgiLCAiY3B1IjogImFybTY0IiB9LCAic2hhNTEyLVVSVVR1MStkTWt4SnNQRmdtK09lRXZxOXdmNXN1ancwRXZnWXk4MFRER0hUU0xUbklIZXFiMEV1OEEzc0M5NUlSZ2plalFMK2tDNG13KzR5UHhpQVhBPT0iXSwKCiAgICAiQG5leHQvc3djLWxpbnV4LXg2NC1nbnUiOiBbIkBuZXh0L3N3Yy1saW51eC14NjQtZ251QDE2LjIuNiIsICIiLCB7ICJvcyI6ICJsaW51eCIsICJjcHUiOiAieDY0IiB9LCAic2hhNTEyLURPajE4Mm1QVjhHM1VrcmF5TG9SRU01WUVZSStEazV3djdPeDl4bDFmRmliQUVMRXNGRDBsRFBmSEllSUxsdXRNTWZkeWhsellQRUxHM3BldUthdXJ3PT0iXSwKCiAgICAiQG5leHQvc3djLWxpbnV4LXg2NC1tdXNsIjogWyJAbmV4dC9zd2MtbGludXgteDY0LW11c2xAMTYuMi42IiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJ4NjQiIH0sICJzaGE1MTItSEtRNVNQL1YvdWI3M1V2RjduL3plSmx4azJrTG10TDdXenJnNFdmbWtqbU5vczVvbkoydEt1N3laT1BkTDE4QTZTdmZuM21heDI5eW0rcnk3TmtLNGc9PSJdLAoKICAgICJAbmV4dC9zd2Mtd2luMzItYXJtNjQtbXN2YyI6IFsiQG5leHQvc3djLXdpbjMyLWFybTY0LW1zdmNAMTYuMi42IiwgIiIsIHsgIm9zIjogIndpbjMyIiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi1MWlhwVGxQeVM1djdIaFNtbnZzTEdQM2lJWWdZT0JuYzhyOEFybFQ1NXNHSFY4OWJSMkhsRGRCaldRK1BZNlNKTW1rOFR1VkdGdXhhbG5QM2svMER3Zz09Il0sCgogICAgIkBuZXh0L3N3Yy13aW4zMi14NjQtbXN2YyI6IFsiQG5leHQvc3djLXdpbjMyLXg2NC1tc3ZjQDE2LjIuNiIsICIiLCB7ICJvcyI6ICJ3aW4zMiIsICJjcHUiOiAieDY0IiB9LCAic2hhNTEyLUYwKzRpMGg5SjZDNGVFM0VBUFdzb0NrN1VXL2Riek9qeXp4WTBxbkRVT1lGdTZGRm1kWjZsOTcvWGRWMy9OejNWWXlPN1VXanlFSlVYa0dxY29YZk1BPT0iXSwKCiAgICAiQG94Yy1wcm9qZWN0L3R5cGVzIjogWyJAb3hjLXByb2plY3QvdHlwZXNAMC4xMzAuMCIsICIiLCB7fSwgInNoYTUxMi1pYkQydXN4OUpSdTdmNXB1MnRNS01JNGNwQTROZ1hKUW9ZUlA0cFE3UHhtbjFsNmsvNTNxV3RRV1pheWhZeTNYNFFaa3Q5ME90K21KRWFlWG91aW82UT09Il0sCgogICAgIkBwbGF5d3JpZ2h0L3Rlc3QiOiBbIkBwbGF5d3JpZ2h0L3Rlc3RAMS42MC4wIiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgInBsYXl3cmlnaHQiOiAiMS42MC4wIiB9LCAiYmluIjogeyAicGxheXdyaWdodCI6ICJjbGkuanMiIH0gfSwgInNoYTUxMi1PNzF5WkliQWgvUHhETU5HbnMzN0dIQklmclZrRVZ5bitBWHlJYTVkT1RmYjQveE52UldWK1Z2L05NYk5DdE9EQi9wTzd2TGxGMk9UbU1WTGhtcjdBZz09Il0sCgogICAgIkByb2xsZG93bi9iaW5kaW5nLWFuZHJvaWQtYXJtNjQiOiBbIkByb2xsZG93bi9iaW5kaW5nLWFuZHJvaWQtYXJtNjRAMS4wLjEiLCAiIiwgeyAib3MiOiAiYW5kcm9pZCIsICJjcHUiOiAiYXJtNjQiIH0sICJzaGE1MTItZkpJM0kwcjNDM09qL3pkQkNwYUNtQlJaWWYwN3hwYXE0eUNmRERvU0ZtK2JlV056YklsMjZwdVc4UnJhVWR1Z29Kdy85NXplck5PbjZqYXNBaHpTbWc9PSJdLAoKICAgICJAcm9sbGRvd24vYmluZGluZy1kYXJ3aW4tYXJtNjQiOiBbIkByb2xsZG93bi9iaW5kaW5nLWRhcndpbi1hcm02NEAxLjAuMSIsICIiLCB7ICJvcyI6ICJkYXJ3aW4iLCAiY3B1IjogImFybTY0IiB9LCAic2hhNTEyLWNLbkFoV0VzVjdUUGNBLzVFQXRlRHA2S2NKWkJRMkcrQnFFN3pheU1NaTdrTXZ3UnNidjdXVDlhT25uMFdObDRTS0VJZjQzdmpTMzFpVVB1ODBuelhnPT0iXSwKCiAgICAiQHJvbGxkb3duL2JpbmRpbmctZGFyd2luLXg2NCI6IFsiQHJvbGxkb3duL2JpbmRpbmctZGFyd2luLXg2NEAxLjAuMSIsICIiLCB7ICJvcyI6ICJkYXJ3aW4iLCAiY3B1IjogIng2NCIgfSwgInNoYTUxMi1ZS3JWd1FqSVJCUG8rNUcvdTAzd0dqYmR5NHE3cHl6Q2U5M0RLOVZKN3prVm1lZzhMSjdHYmdzaUhXZFI0eFNvZTRDQVhSRDdCY2pnYnRyNjRia1hOZz09Il0sCgogICAgIkByb2xsZG93bi9iaW5kaW5nLWZyZWVic2QteDY0IjogWyJAcm9sbGRvd24vYmluZGluZy1mcmVlYnNkLXg2NEAxLjAuMSIsICIiLCB7ICJvcyI6ICJmcmVlYnNkIiwgImNwdSI6ICJ4NjQiIH0sICJzaGE1MTItei9vQnNSRW80NlNzRnFCd1l0RmUwa3BKZUJpakFUNDhPL1dYTEk0c3VpQ0xCa3IwM1JUdFRKTUN6U2REZDJ6bmxoOFZKaXpMMDlYVmtRZ2s4SVpvbnc9PSJdLAoKICAgICJAcm9sbGRvd24vYmluZGluZy1saW51eC1hcm0tZ251ZWFiaWhmIjogWyJAcm9sbGRvd24vYmluZGluZy1saW51eC1hcm0tZ251ZWFiaWhmQDEuMC4xIiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJhcm0iIH0sICJzaGE1MTItaWs4cTdHTTExenh2WXhGYzJQZURjVDZUQnZoQ1FNYVV4ZnBoL001bDlzS3VUcy9TamczTCtCeXcwRjd3MFpWTEJabXgzMFArZ0cwRUN6ek4rTUZjbVE9PSJdLAoKICAgICJAcm9sbGRvd24vYmluZGluZy1saW51eC1hcm02NC1nbnUiOiBbIkByb2xsZG93bi9iaW5kaW5nLWxpbnV4LWFybTY0LWdudUAxLjAuMSIsICIiLCB7ICJvcyI6ICJsaW51eCIsICJjcHUiOiAiYXJtNjQiIH0sICJzaGE1MTItUW9TeDJFa3lycmRaNmtjeUU4c3RxWjYydDBZcmE4RnM1aWE5bE94SnJoNlRNUUpLN2dRS21zY2RUSGY3cE9YS1JFS3JWd090SmNRRzNxVlNmYzg2NkE9PSJdLAoKICAgICJAcm9sbGRvd24vYmluZGluZy1saW51eC1hcm02NC1tdXNsIjogWyJAcm9sbGRvd24vYmluZGluZy1saW51eC1hcm02NC1tdXNsQDEuMC4xIiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi11d053RnB3S2VOaVphd2ZBV0JnZzBWSXp0UFRWM2loaGgxdlYzMzRoOWl2bk5Mb3J4blFNVTZGejh3RzFaYjRRaDlMQzEvTWtjeVQzWWxEWEczUnNnZz09Il0sCgogICAgIkByb2xsZG93bi9iaW5kaW5nLWxpbnV4LXBwYzY0LWdudSI6IFsiQHJvbGxkb3duL2JpbmRpbmctbGludXgtcHBjNjQtZ251QDEuMC4xIiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJwcGM2NCIgfSwgInNoYTUxMi16WTFidWw3T1dyN0RGQmlKKyt3b2ZYdm5yOEI0NWNlM1FzUVVoS3JJaFhzeWdBaDdiVGt3eWVNMWJpMWEyZzVDL3lDL044VFp5R0RFb01mbS9sOW1wZz09Il0sCgogICAgIkByb2xsZG93bi9iaW5kaW5nLWxpbnV4LXMzOTB4LWdudSI6IFsiQHJvbGxkb3duL2JpbmRpbmctbGludXgtczM5MHgtZ251QDEuMC4xIiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJzMzkweCIgfSwgInNoYTUxMi0wZnJsc1QvZjRGdDZJN1NNRVNUS25GM2Nac2RpY1FuMWRDTWtGL2pUOXdETEUrZ0dvaVFmdjFubVQ5ZStzN3MvZmVrdnZ5NnRaTTJqSHZJMnRrYkpEUT09Il0sCgogICAgIkByb2xsZG93bi9iaW5kaW5nLWxpbnV4LXg2NC1nbnUiOiBbIkByb2xsZG93bi9iaW5kaW5nLWxpbnV4LXg2NC1nbnVAMS4wLjEiLCAiIiwgeyAib3MiOiAibGludXgiLCAiY3B1IjogIng2NCIgfSwgInNoYTUxMi1YQUJWbUdwOVRnMFdzcFRWdndkdVRjNGZwcXk2Sm5BVXJTUWU2T3V5cUQvMDNuSTdyME85T1dVa01Jd0ZyaktBSXFvbHZxb0E0WnJKcHBnd0UwR3htdz09Il0sCgogICAgIkByb2xsZG93bi9iaW5kaW5nLWxpbnV4LXg2NC1tdXNsIjogWyJAcm9sbGRvd24vYmluZGluZy1saW51eC14NjQtbXVzbEAxLjAuMSIsICIiLCB7ICJvcyI6ICJsaW51eCIsICJjcHUiOiAieDY0IiB9LCAic2hhNTEyLWJWNGZ6c3d1elZjS0Q5MG8vVk02UXFLeG54bERxMGcyQklTRExOVm14cm5ocHYxRERieVBoQ0lqWWZ2ellMVitNdmtLS25RdDJRNkFPODZTRUJVTFVRPT0iXSwKCiAgICAiQHJvbGxkb3duL2JpbmRpbmctb3Blbmhhcm1vbnktYXJtNjQiOiBbIkByb2xsZG93bi9iaW5kaW5nLW9wZW5oYXJtb255LWFybTY0QDEuMC4xIiwgIiIsIHsgIm9zIjogIm5vbmUiLCAiY3B1IjogImFybTY0IiB9LCAic2hhNTEyLS9NaDBaaHEzT1A3ZlZzMGtjUUhaUDZsWkV0aE1HVGFTZjhVQlFZU0ZFWkRXR1hYbEVDK25KNkVxZW5hSzJ0NExCWE1lM0ErSy9HMkJWWFhkdE9yNFBRPT0iXSwKCiAgICAiQHJvbGxkb3duL2JpbmRpbmctd2FzbTMyLXdhc2kiOiBbIkByb2xsZG93bi9iaW5kaW5nLXdhc20zMi13YXNpQDEuMC4xIiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIkBlbW5hcGkvY29yZSI6ICIxLjEwLjAiLCAiQGVtbmFwaS9ydW50aW1lIjogIjEuMTAuMCIsICJAbmFwaS1ycy93YXNtLXJ1bnRpbWUiOiAiXjEuMS40IiB9LCAiY3B1IjogIm5vbmUiIH0sICJzaGE1MTItKzF4YzlYNDVsOHVmc0JBbTZHanZ4MnFEUklZOWxUVnQwY2dXTmNKKzFnZGhYdmtieGVQQTYweVJUd1NUdVhMMDlDTWh5Sm1qcFY3RTNOb3l4YnFGUVE9PSJdLAoKICAgICJAcm9sbGRvd24vYmluZGluZy13aW4zMi1hcm02NC1tc3ZjIjogWyJAcm9sbGRvd24vYmluZGluZy13aW4zMi1hcm02NC1tc3ZjQDEuMC4xIiwgIiIsIHsgIm9zIjogIndpbjMyIiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi0xRCtVcVpkZm51UitKeTFHZ01Kd2k4NWJENDBIMjF1Tm1PUFJXUWh3NG9SU3VvbFovQjVyaXhaNDVESzJLWE9UQ3ZtVkNlY2F1V2dFaGJ3OGJJN3RPdz09Il0sCgogICAgIkByb2xsZG93bi9iaW5kaW5nLXdpbjMyLXg2NC1tc3ZjIjogWyJAcm9sbGRvd24vYmluZGluZy13aW4zMi14NjQtbXN2Y0AxLjAuMSIsICIiLCB7ICJvcyI6ICJ3aW4zMiIsICJjcHUiOiAieDY0IiB9LCAic2hhNTEyLUlOQXljYVd1aGxPSzN3azRtUkhHc2Rnd1lXbWQ5Y0NoZFBkRTlid1dteTZybjlWcVZOWU5GR2hPZFhyb2ZYVXh3SEluY1NpUE5iOHRObThrbkRWSWVRPT0iXSwKCiAgICAiQHJvbGxkb3duL3BsdWdpbnV0aWxzIjogWyJAcm9sbGRvd24vcGx1Z2ludXRpbHNAMS4wLjEiLCAiIiwge30sICJzaGE1MTItMmo5Ykd0NUpoOGhqK3ZQdGd6UHRsNzJqMHlSeEhBeXVtb282VE5mQWpzTEIwNFV0cFN2UGJQY0RjQk14ejduKzlDWUIwYzFHeFFGeFlSZzJqaW1xR3c9PSJdLAoKICAgICJAc3RhbmRhcmQtc2NoZW1hL3NwZWMiOiBbIkBzdGFuZGFyZC1zY2hlbWEvc3BlY0AxLjEuMCIsICIiLCB7fSwgInNoYTUxMi1sMmFGeTVqQUxobmlHNUhncXJENmpYTGkvclVXckt2cU4vcUp4NnlvSnNnS2hibFZkK2lxcVU0UkNYYXZtL2pQaXR5RG81VEN2S01ucGpLbk9yaXkwdz09Il0sCgogICAgIkBzd2MvaGVscGVycyI6IFsiQHN3Yy9oZWxwZXJzQDAuNS4xNSIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJ0c2xpYiI6ICJeMi44LjAiIH0gfSwgInNoYTUxMi1KUTVUdU1pNDVPd2k0L0JJTUFKQm9TUW9PSnUxMm9Pay9nQURxbGNVTDlKRWRIQjh2eWpVU3N4cWVOWG5tWEhqWUtNaTJXY1l0ZXpHRUVocVVJL0UyZz09Il0sCgogICAgIkB0YWJieV9haS9oaWpyaS1jb252ZXJ0ZXIiOiBbIkB0YWJieV9haS9oaWpyaS1jb252ZXJ0ZXJAMS4wLjUiLCAiIiwge30sICJzaGE1MTItcjViQ2xLcmNJdXNEb28wNDlkU0w4Q2F3bkhSNm1SZER3aGxRdUlnWlJOdHk2OHEweDhrM0xmMUJ0UEFNeFJmL0dnbkhCbklPNHVqZDMrR1FkTFd6eFE9PSJdLAoKICAgICJAdGFpbHdpbmRjc3Mvbm9kZSI6IFsiQHRhaWx3aW5kY3NzL25vZGVANC4zLjAiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiQGpyaWRnZXdlbGwvcmVtYXBwaW5nIjogIl4yLjMuNSIsICJlbmhhbmNlZC1yZXNvbHZlIjogIl41LjIxLjAiLCAiaml0aSI6ICJeMi42LjEiLCAibGlnaHRuaW5nY3NzIjogIjEuMzIuMCIsICJtYWdpYy1zdHJpbmciOiAiXjAuMzAuMjEiLCAic291cmNlLW1hcC1qcyI6ICJeMS4yLjEiLCAidGFpbHdpbmRjc3MiOiAiNC4zLjAiIH0gfSwgInNoYTUxMi1hRmI0Z1VoRk9nZGg5QVhvNEl6QkVPekJra0F4bTlWaWd3REpuTUlZdjNsY2ZYQ0pWZXNOZmJFYUJsNEJOZ1ZSeWlkOTJBbWR2aXF3QlVCUktTZVkzZz09Il0sCgogICAgIkB0YWlsd2luZGNzcy9veGlkZSI6IFsiQHRhaWx3aW5kY3NzL294aWRlQDQuMy4wIiwgIiIsIHsgIm9wdGlvbmFsRGVwZW5kZW5jaWVzIjogeyAiQHRhaWx3aW5kY3NzL294aWRlLWFuZHJvaWQtYXJtNjQiOiAiNC4zLjAiLCAiQHRhaWx3aW5kY3NzL294aWRlLWRhcndpbi1hcm02NCI6ICI0LjMuMCIsICJAdGFpbHdpbmRjc3Mvb3hpZGUtZGFyd2luLXg2NCI6ICI0LjMuMCIsICJAdGFpbHdpbmRjc3Mvb3hpZGUtZnJlZWJzZC14NjQiOiAiNC4zLjAiLCAiQHRhaWx3aW5kY3NzL294aWRlLWxpbnV4LWFybS1nbnVlYWJpaGYiOiAiNC4zLjAiLCAiQHRhaWx3aW5kY3NzL294aWRlLWxpbnV4LWFybTY0LWdudSI6ICI0LjMuMCIsICJAdGFpbHdpbmRjc3Mvb3hpZGUtbGludXgtYXJtNjQtbXVzbCI6ICI0LjMuMCIsICJAdGFpbHdpbmRjc3Mvb3hpZGUtbGludXgteDY0LWdudSI6ICI0LjMuMCIsICJAdGFpbHdpbmRjc3Mvb3hpZGUtbGludXgteDY0LW11c2wiOiAiNC4zLjAiLCAiQHRhaWx3aW5kY3NzL294aWRlLXdhc20zMi13YXNpIjogIjQuMy4wIiwgIkB0YWlsd2luZGNzcy9veGlkZS13aW4zMi1hcm02NC1tc3ZjIjogIjQuMy4wIiwgIkB0YWlsd2luZGNzcy9veGlkZS13aW4zMi14NjQtbXN2YyI6ICI0LjMuMCIgfSB9LCAic2hhNTEyLUY3SFpHQmVOOUkwL0F1dUpTNVB3Y0Q4eGF5eDVyaTVHaGpZVURCRVZZVWtleHlBL2dpd2JETmpSVnJ4U2V6RTNUMjUwT1UySy93cC9sdFd4M1VPZWZnPT0iXSwKCiAgICAiQHRhaWx3aW5kY3NzL294aWRlLWFuZHJvaWQtYXJtNjQiOiBbIkB0YWlsd2luZGNzcy9veGlkZS1hbmRyb2lkLWFybTY0QDQuMy4wIiwgIiIsIHsgIm9zIjogImFuZHJvaWQiLCAiY3B1IjogImFybTY0IiB9LCAic2hhNTEyLVRKUGlxNjd0S2xMdU9iUDZSa3d2VkdEb3hDTUJWdERnS2tMZmEvdXlqNy9GeXh2UXdIUytVT25WclhYZ2JFc2ZVYU1naVZ2QzRLYkpuUnIyNmhvNE5nPT0iXSwKCiAgICAiQHRhaWx3aW5kY3NzL294aWRlLWRhcndpbi1hcm02NCI6IFsiQHRhaWx3aW5kY3NzL294aWRlLWRhcndpbi1hcm02NEA0LjMuMCIsICIiLCB7ICJvcyI6ICJkYXJ3aW4iLCAiY3B1IjogImFybTY0IiB9LCAic2hhNTEyLW9NTi9XWlJiK1NPMzdCbVVFbEVnZUVXdVU4RS9IWFJraU9EeEp4TGUxVVRIVlhMcmRWU2dmYUpWN3BTbGhSR01TT2lYTHV4VElqZnNGM3dZdno4Y2dRPT0iXSwKCiAgICAiQHRhaWx3aW5kY3NzL294aWRlLWRhcndpbi14NjQiOiBbIkB0YWlsd2luZGNzcy9veGlkZS1kYXJ3aW4teDY0QDQuMy4wIiwgIiIsIHsgIm9zIjogImRhcndpbiIsICJjcHUiOiAieDY0IiB9LCAic2hhNTEyLU42Q1VtdTRhNmJLVkFEZnc3N3AraXc2WWQ5UTNPQmhlMHZlYURYK1FhemZ1VllsUXNIZkRneEJyc2pRL0lXK3p5d0w4bVRyTmQwU2RKVC96Z3R2TWRBPT0iXSwKCiAgICAiQHRhaWx3aW5kY3NzL294aWRlLWZyZWVic2QteDY0IjogWyJAdGFpbHdpbmRjc3Mvb3hpZGUtZnJlZWJzZC14NjRANC4zLjAiLCAiIiwgeyAib3MiOiAiZnJlZWJzZCIsICJjcHUiOiAieDY0IiB9LCAic2hhNTEyLXpETDVoQmtRZEg1QzZNcHFiSzNnUUFnUDgwdHNNd1NJMjZ2ak96akp0TkNNVW8wbEZnT0l0ekhLQkl1cE9aTlF4dDNvdVBIN1JQaHZOaGlUZkNlNUNRPT0iXSwKCiAgICAiQHRhaWx3aW5kY3NzL294aWRlLWxpbnV4LWFybS1nbnVlYWJpaGYiOiBbIkB0YWlsd2luZGNzcy9veGlkZS1saW51eC1hcm0tZ251ZWFiaWhmQDQuMy4wIiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJhcm0iIH0sICJzaGE1MTItUjA2SGROaTdBN09Fb01zZjZkNHRqWjcxUkNXblpRUEhqMm1ub3RTRlVSak5MZEJDK2NJZ1hRN2w4MUNxZW9pUWZ0amY2T09ibHhYTUluTWdOMlZ6TUE9PSJdLAoKICAgICJAdGFpbHdpbmRjc3Mvb3hpZGUtbGludXgtYXJtNjQtZ251IjogWyJAdGFpbHdpbmRjc3Mvb3hpZGUtbGludXgtYXJtNjQtZ251QDQuMy4wIiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi1xVEpIRUxYOGpldGpoUlFIQ0xpbGtWTG15YnB6TlFBdGFJL2dhb1ZvaWRuL3VmYk5EYkFvOEtsSzJKK3lQb2M4d1F4dkR4Q21oLzVscjhuQzErbFRiZz09Il0sCgogICAgIkB0YWlsd2luZGNzcy9veGlkZS1saW51eC1hcm02NC1tdXNsIjogWyJAdGFpbHdpbmRjc3Mvb3hpZGUtbGludXgtYXJtNjQtbXVzbEA0LjMuMCIsICIiLCB7ICJvcyI6ICJsaW51eCIsICJjcHUiOiAiYXJtNjQiIH0sICJzaGE1MTItWjZzdWtpUXNuZ25XTytsMzlYNHBQYmlXVDgxSUMrUExLRitQSHhJbHlaYkdOYjlNT0RmWWxYRVZsRnZlajVCT1pJbldYMDFrVnl6ZUx2SHNYaGZjelE9PSJdLAoKICAgICJAdGFpbHdpbmRjc3Mvb3hpZGUtbGludXgteDY0LWdudSI6IFsiQHRhaWx3aW5kY3NzL294aWRlLWxpbnV4LXg2NC1nbnVANC4zLjAiLCAiIiwgeyAib3MiOiAibGludXgiLCAiY3B1IjogIng2NCIgfSwgInNoYTUxMi1EUk5kUVJwU0d6UkdmQVJWdVZreHZNOFExMm5oMTlsNEJGL0c3ekdBMW9lKzl3Y0M2c2FGQkhUSVNycEljS3poaVh0U3JsU3JsdUNmdk11bGVkb0NUUT09Il0sCgogICAgIkB0YWlsd2luZGNzcy9veGlkZS1saW51eC14NjQtbXVzbCI6IFsiQHRhaWx3aW5kY3NzL294aWRlLWxpbnV4LXg2NC1tdXNsQDQuMy4wIiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJ4NjQiIH0sICJzaGE1MTItWjBJQURiRG84Ymg2STdoMklRTXg2MDFBZFhCTGZGcEVkVW90ZnQ4NmV2ZC84WlBmbFplOUNPUE84UTF2dytwZkxXSVVvOXpOL0pHWnZ3dUFKcWR1cWc9PSJdLAoKICAgICJAdGFpbHdpbmRjc3Mvb3hpZGUtd2FzbTMyLXdhc2kiOiBbIkB0YWlsd2luZGNzcy9veGlkZS13YXNtMzItd2FzaUA0LjMuMCIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAZW1uYXBpL2NvcmUiOiAiXjEuMTAuMCIsICJAZW1uYXBpL3J1bnRpbWUiOiAiXjEuMTAuMCIsICJAZW1uYXBpL3dhc2ktdGhyZWFkcyI6ICJeMS4yLjEiLCAiQG5hcGktcnMvd2FzbS1ydW50aW1lIjogIl4xLjEuNCIsICJAdHlieXMvd2FzbS11dGlsIjogIl4wLjEwLjEiLCAidHNsaWIiOiAiXjIuOC4xIiB9LCAiY3B1IjogIm5vbmUiIH0sICJzaGE1MTItSE5aR09VeEVtRWxrc1lSN1M2c0M1alRlTkdwb2JBc3k5dTdHdTBBc2tKOC8yMEZSOUdxZWJVeUIrSEJjVS9heDZCSHVpdUppK09kYTRCK1lYNkgxeUE9PSJdLAoKICAgICJAdGFpbHdpbmRjc3Mvb3hpZGUtd2luMzItYXJtNjQtbXN2YyI6IFsiQHRhaWx3aW5kY3NzL294aWRlLXdpbjMyLWFybTY0LW1zdmNANC4zLjAiLCAiIiwgeyAib3MiOiAid2luMzIiLCAiY3B1IjogImFybTY0IiB9LCAic2hhNTEyLVBlK1JQVlRpMVQrcXltdXVScGNkdndTVlpqbmxsL2Y3bjhnQnhNTWgzeExUY3RNREtxcGRmR2ltYk15aW9xdExoVVlaeGRKOXdHTmhWN01LSHZnWnNRPT0iXSwKCiAgICAiQHRhaWx3aW5kY3NzL294aWRlLXdpbjMyLXg2NC1tc3ZjIjogWyJAdGFpbHdpbmRjc3Mvb3hpZGUtd2luMzIteDY0LW1zdmNANC4zLjAiLCAiIiwgeyAib3MiOiAid2luMzIiLCAiY3B1IjogIng2NCIgfSwgInNoYTUxMi1NdnJmMmtYVy95ZVcvT1RlelpsQ0dPaXJYUmNVdUxJQngvNVkxMkJhUE03d0pvcnlHNmRmUy9OSkw4YUJQcXRURXgvVm00VDR2S3pGVWNLRFQrVEtVQT09Il0sCgogICAgIkB0YWlsd2luZGNzcy9wb3N0Y3NzIjogWyJAdGFpbHdpbmRjc3MvcG9zdGNzc0A0LjMuMCIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAYWxsb2MvcXVpY2stbHJ1IjogIl41LjIuMCIsICJAdGFpbHdpbmRjc3Mvbm9kZSI6ICI0LjMuMCIsICJAdGFpbHdpbmRjc3Mvb3hpZGUiOiAiNC4zLjAiLCAicG9zdGNzcyI6ICJeOC41LjEwIiwgInRhaWx3aW5kY3NzIjogIjQuMy4wIiB9IH0sICJzaGE1MTItSm0wNVRqeCs5eUNMR3Y1cXcxYys4NFBzZHM4TW55ckVRWUNCK0ZGazJsZ0dpVWpsUnFkeGtlNG1WVHVZcmoyeG5WWnFLaW0yQXByNXlTdVFSWUF3L3c9PSJdLAoKICAgICJAdHlieXMvd2FzbS11dGlsIjogWyJAdHlieXMvd2FzbS11dGlsQDAuMTAuMiIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJ0c2xpYiI6ICJeMi40LjAiIH0gfSwgInNoYTUxMi1Sb0J2SjJYMHd1S2xXRklqcndmZkd3MUlxWkhLUXF6SWNoS2FhZFpaZm5OcHNBWXAybU0waDM2SnRQQ2pOREFIR2dZZXovMTV1TUJwZkd3Y2hoaU1nZz09Il0sCgogICAgIkB0eXBlcy9jaGFpIjogWyJAdHlwZXMvY2hhaUA1LjIuMyIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAdHlwZXMvZGVlcC1lcWwiOiAiKiIsICJhc3NlcnRpb24tZXJyb3IiOiAiXjIuMC4xIiB9IH0sICJzaGE1MTItTXc1NThvZUE5ZkZidjY1L3k0bUh0WERzOWJQbkZNWkFML2p4ZFBGVXBPSEhJWFg5MW1jZ0VIYlM1TGFocitwd1pGUjhBN0dRbGVSV2VJNmNHRkMyVUE9PSJdLAoKICAgICJAdHlwZXMvZGVlcC1lcWwiOiBbIkB0eXBlcy9kZWVwLWVxbEA0LjAuMiIsICIiLCB7fSwgInNoYTUxMi1jOWg5ZFZWTWlnTVBjNGJ3VHZDNWR4cXRxSlp3UVBlUHNXalBscFNPbm9qYm9yNnBHcWRrNTQxbGZBN0FxRlFyNXBCMUJSZHEwanVZOWRiODFCd3lGdz09Il0sCgogICAgIkB0eXBlcy9lc3RyZWUiOiBbIkB0eXBlcy9lc3RyZWVAMS4wLjkiLCAiIiwge30sICJzaGE1MTItR2hkUGd5MWVsNC9JbVAwNVgwNVV3NGN3Mi9NOTNCQ1VtbkV2V1pOU3RsQ3pFS01FNEZraytZcG9BNU9pSE5RbW9TN0NhZmI4WGEzUHlhOG0xUXJ6ZWc9PSJdLAoKICAgICJAdHlwZXMvbm9kZSI6IFsiQHR5cGVzL25vZGVAMjIuMTkuMTkiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAidW5kaWNpLXR5cGVzIjogIn42LjIxLjAiIH0gfSwgInNoYTUxMi1keWgveE8yRmg1YllyZldhYXFHclJRUUdrTmRtWXc2QW1hQVV2WWVVTU5UV1F0dmI3OTZpa0xkbVRjaFJtT2xPaUlKMVREWGZXZ1Z4MVFrVWxRNkhldz09Il0sCgogICAgIkB0eXBlcy9yZWFjdCI6IFsiQHR5cGVzL3JlYWN0QDE5LjIuMTUiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiY3NzdHlwZSI6ICJeMy4yLjIiIH0gfSwgInNoYTUxMi1lUndjR05IdmUrRThxdEVRU1NSbDZ1cmgrckZvcDR2OGdtNk84ckd2MjVDb2RidkZkTGpBMXZWUTFLa2lGRTB3MFVQT25iOHREaUZLTDVscDBydFk1UT09Il0sCgogICAgIkB0eXBlcy9yZWFjdC1kb20iOiBbIkB0eXBlcy9yZWFjdC1kb21AMTkuMi4zIiwgIiIsIHsgInBlZXJEZXBlbmRlbmNpZXMiOiB7ICJAdHlwZXMvcmVhY3QiOiAiXjE5LjIuMCIgfSB9LCAic2hhNTEyLWpwMkwvZVk2Zm4rS2dWVlFBT3FZSXRiRjBWWS9ZQXBlNU16MkYwYXlrU084Z3gzMWJZQ1p5dlNlWXhDSEt2ekhHNWVaamMrenlhUzVCckJXeWEyK2tRPT0iXSwKCiAgICAiQHZpdGVzdC9leHBlY3QiOiBbIkB2aXRlc3QvZXhwZWN0QDQuMS42IiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIkBzdGFuZGFyZC1zY2hlbWEvc3BlYyI6ICJeMS4xLjAiLCAiQHR5cGVzL2NoYWkiOiAiXjUuMi4yIiwgIkB2aXRlc3Qvc3B5IjogIjQuMS42IiwgIkB2aXRlc3QvdXRpbHMiOiAiNC4xLjYiLCAiY2hhaSI6ICJeNi4yLjIiLCAidGlueXJhaW5ib3ciOiAiXjMuMS4wIiB9IH0sICJzaGE1MTItN0VIRHF1UHRoQUxTVjBqaGhqZ0VXOEZYYXZpTXg3clNxdThXNm9xQ29BdU9oS292ODE0UDk5UURWMXB4TUEzUVB2MjFZdWR2Sm5nSWhqck5JNG9wTGc9PSJdLAoKICAgICJAdml0ZXN0L21vY2tlciI6IFsiQHZpdGVzdC9tb2NrZXJANC4xLjYiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiQHZpdGVzdC9zcHkiOiAiNC4xLjYiLCAiZXN0cmVlLXdhbGtlciI6ICJeMy4wLjMiLCAibWFnaWMtc3RyaW5nIjogIl4wLjMwLjIxIiB9LCAicGVlckRlcGVuZGVuY2llcyI6IHsgIm1zdyI6ICJeMi40LjkiLCAidml0ZSI6ICJeNi4wLjAgfHwgXjcuMC4wIHx8IF44LjAuMCIgfSwgIm9wdGlvbmFsUGVlcnMiOiBbIm1zdyIsICJ2aXRlIl0gfSwgInNoYTUxMi1NQ0ZjNjNjek1qRUluT2xjWTJjcFFDdkNOK0tnYkFuKzYweHU5Y01nUDRzS2FMQzVKTkFLdzdKSDhRZEFub0FDODhoVzFJaVNOWitHZ1ZYbE4xVWNNUT09Il0sCgogICAgIkB2aXRlc3QvcHJldHR5LWZvcm1hdCI6IFsiQHZpdGVzdC9wcmV0dHktZm9ybWF0QDQuMS42IiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgInRpbnlyYWluYm93IjogIl4zLjEuMCIgfSB9LCAic2hhNTEyLWg1U3hEL0l6TmhaWW5yU1pSc1VaUUlDK3ZEMEdZOGNVdnEwaXdzbWtGS2l4UkNLTExXcUNYYS9GSVE0UzFSK3NJK1BHb29qa0hzZE5yYlppTTlRcGd3PT0iXSwKCiAgICAiQHZpdGVzdC9ydW5uZXIiOiBbIkB2aXRlc3QvcnVubmVyQDQuMS42IiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIkB2aXRlc3QvdXRpbHMiOiAiNC4xLjYiLCAicGF0aGUiOiAiXjIuMC4zIiB9IH0sICJzaGE1MTItbk9QQ21uMit5RDBaTm1LZHNYR3YvVXhNTVdiTXVLZUQ2R3lZbmNOd2RrWUR4cFF2clBTS1lqMnJXdURqQzJZNGI2dzZoamlwNWRCS0Z6RVV1WmUzdkE9PSJdLAoKICAgICJAdml0ZXN0L3NuYXBzaG90IjogWyJAdml0ZXN0L3NuYXBzaG90QDQuMS42IiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIkB2aXRlc3QvcHJldHR5LWZvcm1hdCI6ICI0LjEuNiIsICJAdml0ZXN0L3V0aWxzIjogIjQuMS42IiwgIm1hZ2ljLXN0cmluZyI6ICJeMC4zMC4yMSIsICJwYXRoZSI6ICJeMi4wLjMiIH0gfSwgInNoYTUxMi1ZaHNkRTZ4QVZmVERtemp4TDJaRFV2amorWnNneU9LZStUZFF6cWtENzJ3SU9tSGthOE51R1E2TnBUTlp2OUQyWjYzZmJ3V0tKUGVWcEV3NEVRZ1l4dz09Il0sCgogICAgIkB2aXRlc3Qvc3B5IjogWyJAdml0ZXN0L3NweUA0LjEuNiIsICIiLCB7fSwgInNoYTUxMi1KRkt4TXg2dWRod0toL0xkbzI3MGUxN1FYNzEwdmd1bk1rdVBBdlhqSFN2QzZvcUxXQUhoVmhqZy9JNzFxMHUwQ0JTRXJJT0RWMUtqdjBGUU5TV2pkZz09Il0sCgogICAgIkB2aXRlc3QvdXRpbHMiOiBbIkB2aXRlc3QvdXRpbHNANC4xLjYiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiQHZpdGVzdC9wcmV0dHktZm9ybWF0IjogIjQuMS42IiwgImNvbnZlcnQtc291cmNlLW1hcCI6ICJeMi4wLjAiLCAidGlueXJhaW5ib3ciOiAiXjMuMS4wIiB9IH0sICJzaGE1MTItRnhJWStVODFSM0xHS0N4YUhIRlJRNStnNi9pUmdHTG1lSFdkcDJBbWo0bGpRUnJFSVdIbVp5RGZEWUJSWmxweXFBN3FLeHRTOUREMWRoazhSblJJVlE9PSJdLAoKICAgICJhbnNpLXJlZ2V4IjogWyJhbnNpLXJlZ2V4QDUuMC4xIiwgIiIsIHt9LCAic2hhNTEyLXF1SlFYbFRTVUdMMkxIOVNVWG84VndzWTRzb2FuaGdvNkxOU204NEUxTEJjRThzM08wd3BkaVJ6eVI5ei9aWkpNbE1XdjM3cU9PYjlwZEpsTVVFS0ZRPT0iXSwKCiAgICAiYW5zaS1zdHlsZXMiOiBbImFuc2ktc3R5bGVzQDQuMy4wIiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgImNvbG9yLWNvbnZlcnQiOiAiXjIuMC4xIiB9IH0sICJzaGE1MTItemJCOXJDSkFUMXJiamlWRGIyaHFLRkhOWUx4Z3RrOE5VUnhaM0lad0QzRjZOdHhiWFpRQ25uU2kxTGt4K0lEb2hkUGxGcDIyMndWQUxJaGVaSlFTRWc9PSJdLAoKICAgICJhc3NlcnRpb24tZXJyb3IiOiBbImFzc2VydGlvbi1lcnJvckAyLjAuMSIsICIiLCB7fSwgInNoYTUxMi1Jemk4UlFjZmZxQ2VOVmdGaWdLbGkxc3NrbElicEhuQ1ljNkFrblhHWW9CNmdySnF5ZWJ5N2p2MTJKVVFnbVRBbklEbmJjazF1eGtzVDRkek4zUFdCQT09Il0sCgogICAgImJhc2VsaW5lLWJyb3dzZXItbWFwcGluZyI6IFsiYmFzZWxpbmUtYnJvd3Nlci1tYXBwaW5nQDIuMTAuMzEiLCAiIiwgeyAiYmluIjogeyAiYmFzZWxpbmUtYnJvd3Nlci1tYXBwaW5nIjogImRpc3QvY2xpLmNqcyIgfSB9LCAic2hhNTEyLU11allPM2VQNzJ1dm1TRTBpNHdsdHNvZFJmSXBaQVRQM2p2elJOUkdHeGd6SWQ3YVZvY1ZKSlYzbmYwMXFuenpLRkd4UVZDOWJwV3hsNWNqeFRyLzdRPT0iXSwKCiAgICAiY2FuaXVzZS1saXRlIjogWyJjYW5pdXNlLWxpdGVAMS4wLjMwMDAxNzkzIiwgIiIsIHt9LCAic2hhNTEyLWl3U3NZV2FDT29oMjZjVjhOd05SVmlIbHJmVXZZc0hEZlJWY2J0bXcwS2c2UEpJWlpYd01rajE0NDJGWUxCR2tlVWYxanVBc1UzRFRmeFc1NzltclBBPT0iXSwKCiAgICAiY2hhaSI6IFsiY2hhaUA2LjIuMiIsICIiLCB7fSwgInNoYTUxMi1OVVBSbHVPZk9pVEtCS3ZXUHRTRDRQaEZ2V0NxT2kwQkdTdE5XczU3WDlqczdYR1RwclNtRm96NUYwdFdoUjRXUGpOZVI5alhxZEM3L1VwU0pUbmxSZz09Il0sCgogICAgImNoYWxrIjogWyJjaGFsa0A0LjEuMiIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJhbnNpLXN0eWxlcyI6ICJeNC4xLjAiLCAic3VwcG9ydHMtY29sb3IiOiAiXjcuMS4wIiB9IH0sICJzaGE1MTItb0tuYmhGeVJJWHBVdWV6OGlCTW15RWE0bmJqNElPUXl1aGMvd3k5a1k3L1dWUGN3SU85VkE2NjhQdThSa083KzBHNzZTTFJPZXl3OUNwUTA2MWk0bUE9PSJdLAoKICAgICJjbGllbnQtb25seSI6IFsiY2xpZW50LW9ubHlAMC4wLjEiLCAiIiwge30sICJzaGE1MTItSVYzT3UwalNNelpyZDNwWjQ4bkxrVDlEQTdBZzFwblB6YWlRaHBXN2MzUmJjcXF6dnp6VnUrTDhnZnFNcC84SU0yTVF0U2lxYUN4cnJjZnU4SThyTUE9PSJdLAoKICAgICJjbGl1aSI6IFsiY2xpdWlAOC4wLjEiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAic3RyaW5nLXdpZHRoIjogIl40LjIuMCIsICJzdHJpcC1hbnNpIjogIl42LjAuMSIsICJ3cmFwLWFuc2kiOiAiXjcuMC4wIiB9IH0sICJzaGE1MTItQlNlTm55dXM3NUM0Ly9OUTlnUXQxL2NzVFh5by84U2IrYWZMQWt6QXB0RnVNc29kOUhGb2tHTnVkWnBpL29RVjczaG5WSytzUis1UFZSTWQrRHI3WVE9PSJdLAoKICAgICJjbHN4IjogWyJjbHN4QDIuMS4xIiwgIiIsIHt9LCAic2hhNTEyLWVZbTBRV0J0VXJCV1pXRzBkMzg2T0dBdzE2Wjk5NVBpT1ZvMkI3YmpXU2JIZWRHbDVlMFpXYXE2NWtPR2dVU05lc0VJRGtCOUlTYlRnL0pLOWRoQ1pBPT0iXSwKCiAgICAiY29sb3ItY29udmVydCI6IFsiY29sb3ItY29udmVydEAyLjAuMSIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJjb2xvci1uYW1lIjogIn4xLjEuNCIgfSB9LCAic2hhNTEyLVJSRUNQc2o3aXUveGI1b0tZY3NGSFNwcEZObnNqLzUyT1ZUUktiNHpQNW9uWHdWRjN6Vm1tVG9OY09mR0MrQ1JEcGZLL1U1ODRmTWczOFpIQ2FFbEtRPT0iXSwKCiAgICAiY29sb3ItbmFtZSI6IFsiY29sb3ItbmFtZUAxLjEuNCIsICIiLCB7fSwgInNoYTUxMi1kT3krM0F1VzNhMndOYlpISXVNWnBUY2dqR3VMVS91QkwvdWJjWkY5T1hiRG84ZmY0Tzh5VnA1QmYwZWZTOHVFb1lvNXE0Rng3ZFk5T2dRR1hnQXNRQT09Il0sCgogICAgImNvbmN1cnJlbnRseSI6IFsiY29uY3VycmVudGx5QDkuMi4xIiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgImNoYWxrIjogIjQuMS4yIiwgInJ4anMiOiAiNy44LjIiLCAic2hlbGwtcXVvdGUiOiAiMS44LjMiLCAic3VwcG9ydHMtY29sb3IiOiAiOC4xLjEiLCAidHJlZS1raWxsIjogIjEuMi4yIiwgInlhcmdzIjogIjE3LjcuMiIgfSwgImJpbiI6IHsgImNvbmMiOiAiZGlzdC9iaW4vY29uY3VycmVudGx5LmpzIiwgImNvbmN1cnJlbnRseSI6ICJkaXN0L2Jpbi9jb25jdXJyZW50bHkuanMiIH0gfSwgInNoYTUxMi1mc2ZyTzBNeFY2NFpub3k4L2wxdlZJampIYTI5U1p5eXFQZ1FCd2hpRGNhVzh3SmMyVzNYV1ZPR3g0TTNvSkJudi96ZFVaSUlwMWdEZVM5OEd6UDhOZz09Il0sCgogICAgImNvbnZlcnQtc291cmNlLW1hcCI6IFsiY29udmVydC1zb3VyY2UtbWFwQDIuMC4wIiwgIiIsIHt9LCAic2hhNTEyLUt2cDQ1OUhyVjJGRUoxQ0FzaTFLdStNWTNrYXNIMTlURnlrVHoyeFdtTWVxNmJrMk5VM1hYdmZKK1E2MW0weGt0V3d0KzFIU1lmM0pac1RtczNhUkpnPT0iXSwKCiAgICAiY3NzdHlwZSI6IFsiY3NzdHlwZUAzLjIuMyIsICIiLCB7fSwgInNoYTUxMi16MUhHS2NZeTJ4QThBR1Fmd3JuMFBBeStQQjdYL0dTajNVVkpXOXFLeW40M3hXYStnbDVuWG1VNHFxTE1SeldWTEZDOEt1c1VYOFQvMGtDaU9ZcEFJUT09Il0sCgogICAgImRhdGUtZm5zIjogWyJkYXRlLWZuc0A0LjIuMSIsICIiLCB7fSwgInNoYTUxMi0zN1JoU2R4YUcxc3VlbjZWREN6YTZyTnJRZm9veVFoNTdIRlZQd1FHRXEyUVdsaVZMelBRWjhPYTAxN3dlT3UrSFpDbnpJN04zUGYvd3lvQktmRXFyQT09Il0sCgogICAgImRhdGUtZm5zLWphbGFsaSI6IFsiZGF0ZS1mbnMtamFsYWxpQDQuMS4wLTAiLCAiIiwge30sICJzaGE1MTItaFRJUC96K3QrcUt3QkRjbW1zbm1qV1RkdXhDZys1S2ZkcVdRdmIyWC84Qzkra25ZWTZlcE4vcGZ4ZER1eVZsU1ZlRnowc001ZUVmd0lVUTcwVTRja2c9PSJdLAoKICAgICJkZXRlY3QtbGliYyI6IFsiZGV0ZWN0LWxpYmNAMi4xLjIiLCAiIiwge30sICJzaGE1MTItQnRqMkJPT084M28zV3lINTllOE1nWHN4RVFWY2Fya1VPcEVZcnViQjB1cnduTjEweVEzNjRyc2lCeVUxMW5abHFXWVptMDVpL29mN2lvNG16aWhCdFE9PSJdLAoKICAgICJlbW9qaS1yZWdleCI6IFsiZW1vamktcmVnZXhAOC4wLjAiLCAiIiwge30sICJzaGE1MTItTVNqWXpjV05PQTBld0FIcHowTXhwWUZ2d2c2eWp5MU5HM3h0ZW9xejY0NFZDby9SUGducjEvR0d0K2ljM2lKVHpROEV1M1RkTTE0U2F3blZVbUdFNkE9PSJdLAoKICAgICJlbmhhbmNlZC1yZXNvbHZlIjogWyJlbmhhbmNlZC1yZXNvbHZlQDUuMjEuNSIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJncmFjZWZ1bC1mcyI6ICJeNC4yLjQiLCAidGFwYWJsZSI6ICJeMi4zLjMiIH0gfSwgInNoYTUxMi1tTENOYnJRbGkxMUsxeVNVbXVOdDRaVUIzT3BHSURxNHEydlRCVGY1Y0wybHBzUmpJOVFLcVNEMG5kalc4Rnl2Y1cvSmo0NmdNZTlzeXlIQXN2TWEvQT09Il0sCgogICAgImVzLW1vZHVsZS1sZXhlciI6IFsiZXMtbW9kdWxlLWxleGVyQDIuMS4wIiwgIiIsIHt9LCAic2hhNTEyLW4yN3pUWU1qWXUxYWo0TWpDV3pTUDdHOXI3NXV0c2FvYzhtNjF3ZUsrVzhKTUJHR1F5YmQ0M0dzdENYWjNXTm1TRnRHVDl3aTU5cVFUVzZtaFRSNUxRPT0iXSwKCiAgICAiZXNjYWxhZGUiOiBbImVzY2FsYWRlQDMuMi4wIiwgIiIsIHt9LCAic2hhNTEyLVdVajJxbHhhUXRPNGc2UHE1YzI5R1RjV0dEeWQ4aXRMOHpUbGlwZ0VDejNKZXNBaWlPS290ZDhKVTZvdEIzUEFDZ0c2eGtKVXlWaGJvTVMrYmplL2pBPT0iXSwKCiAgICAiZXN0cmVlLXdhbGtlciI6IFsiZXN0cmVlLXdhbGtlckAzLjAuMyIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAdHlwZXMvZXN0cmVlIjogIl4xLjAuMCIgfSB9LCAic2hhNTEyLTdSVUtmWGdTTU1renQ2WnVYbXFhcE91ckxHUFBmZ2o2bDl1Ulo3bFJHb2x2azB5MnlvY2MzNUxkY3hLQzVQUVpkbjJETXFpb0FRMk5vV2NyVEttbTZnPT0iXSwKCiAgICAiZXhwZWN0LXR5cGUiOiBbImV4cGVjdC10eXBlQDEuMy4wIiwgIiIsIHt9LCAic2hhNTEyLWtudnllYXVZaHFqT1l2UTY2TXpuU01zODN3bUhyQ3ljTkVONkFvKzJBZVlFZnhVSWt1aVZ4ZEVhMXFsR0VQSytXZTNuMFRIaURjaVlTc0NjZ1cvRG9BPT0iXSwKCiAgICAiZmRpciI6IFsiZmRpckA2LjUuMCIsICIiLCB7ICJwZWVyRGVwZW5kZW5jaWVzIjogeyAicGljb21hdGNoIjogIl4zIHx8IF40IiB9LCAib3B0aW9uYWxQZWVycyI6IFsicGljb21hdGNoIl0gfSwgInNoYTUxMi10SWJZdFpidWNPczBCUkdxUEprc2hKVVlkTCtTREg3ZFZNOGdqeStFUnAzV0FVakxFRkpFKzAya2FueUh0d2pXT253cktZQml3QW1NMHA0a0xKQW5YZz09Il0sCgogICAgImZzZXZlbnRzIjogWyJmc2V2ZW50c0AyLjMuMiIsICIiLCB7ICJvcyI6ICJkYXJ3aW4iIH0sICJzaGE1MTIteGlxTVFSNHhBZUhUdUI5dVdtK2ZGUmNJT2dLQk1pT0JQK2VYaXlUN2pzZ1ZDcTFia1Z5Z3QwMG9BU293QjdFZHRwT0hhYVBnS3Q4MTJQOWFiK0RES0E9PSJdLAoKICAgICJnZXQtY2FsbGVyLWZpbGUiOiBbImdldC1jYWxsZXItZmlsZUAyLjAuNSIsICIiLCB7fSwgInNoYTUxMi1EeUZQM0JNLzNZSFRRT0NVTC93ME9aSFIwbHBLZUdyeG90Y0hXY3FORWRubHRxRndYVmZoRUJROTRlSW8zNEFmUXBvMHJHa2k0Y3lJaWZ0WTA2aDJGZz09Il0sCgogICAgImdyYWNlZnVsLWZzIjogWyJncmFjZWZ1bC1mc0A0LjIuMTEiLCAiIiwge30sICJzaGE1MTItUmJKNS9qbUZjTk5DY0RWNW85ZVRuQkxKL0hzeldWMFA3M2JjK0ZmNG5TL3JKaitZYVM2SUd5aU9MMFZvQllYK2wxV3JsM2s2M2gvS3JIK25oSjBYdlE9PSJdLAoKICAgICJoYXMtZmxhZyI6IFsiaGFzLWZsYWdANC4wLjAiLCAiIiwge30sICJzaGE1MTItRXlrSlQvUTFLalRXY3RwcGdJQWdmU08wdEtWdVpVamhnTXIxN2txVHVtTWw2QWZ2M0VJU2xlVTdxWlV6b1hERlRBSFREQzROT29HL1p4VTNFdmxNUFE9PSJdLAoKICAgICJpcy1mdWxsd2lkdGgtY29kZS1wb2ludCI6IFsiaXMtZnVsbHdpZHRoLWNvZGUtcG9pbnRAMy4wLjAiLCAiIiwge30sICJzaGE1MTItenltbTUrdStzQ3NTV3lEOXFOYWVqVjNERnZoQ0tjbEtkaXpZYUpVdUhBODNSTGpiN25TdUduZGRDSEd2MGhrK0tZN0JNQWxzV2VLNFVlZzZFVjZYUWc9PSJdLAoKICAgICJqaXRpIjogWyJqaXRpQDIuNy4wIiwgIiIsIHsgImJpbiI6IHsgImppdGkiOiAibGliL2ppdGktY2xpLm1qcyIgfSB9LCAic2hhNTEyLUFDLzdKb2ZKdlpHcnJuZVdOYUVuSmVPTFV4K0psR3Q3dE5hMHdaaVJQVDRNWTF3bWZLanQyKzZPMnAydXoyK3NrbGw4T1pabUpNTnFla2U3a0tiTmdRPT0iXSwKCiAgICAibGlicGhvbmVudW1iZXItanMiOiBbImxpYnBob25lbnVtYmVyLWpzQDEuMTIuNDEiLCAiIiwge30sICJzaGE1MTItbHNtTW1HWEJ4WElLL1ZNTEVqMGtMNk10VXMxa0JHajFuVEN6aTZ6Z1FvRzFERXdxd3QyRFF5SHhjTHlrY2VJeEFuZkUzaHlhN051SWg2UHBDNlMzZkE9PSJdLAoKICAgICJsaWdodG5pbmdjc3MiOiBbImxpZ2h0bmluZ2Nzc0AxLjMyLjAiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiZGV0ZWN0LWxpYmMiOiAiXjIuMC4zIiB9LCAib3B0aW9uYWxEZXBlbmRlbmNpZXMiOiB7ICJsaWdodG5pbmdjc3MtYW5kcm9pZC1hcm02NCI6ICIxLjMyLjAiLCAibGlnaHRuaW5nY3NzLWRhcndpbi1hcm02NCI6ICIxLjMyLjAiLCAibGlnaHRuaW5nY3NzLWRhcndpbi14NjQiOiAiMS4zMi4wIiwgImxpZ2h0bmluZ2Nzcy1mcmVlYnNkLXg2NCI6ICIxLjMyLjAiLCAibGlnaHRuaW5nY3NzLWxpbnV4LWFybS1nbnVlYWJpaGYiOiAiMS4zMi4wIiwgImxpZ2h0bmluZ2Nzcy1saW51eC1hcm02NC1nbnUiOiAiMS4zMi4wIiwgImxpZ2h0bmluZ2Nzcy1saW51eC1hcm02NC1tdXNsIjogIjEuMzIuMCIsICJsaWdodG5pbmdjc3MtbGludXgteDY0LWdudSI6ICIxLjMyLjAiLCAibGlnaHRuaW5nY3NzLWxpbnV4LXg2NC1tdXNsIjogIjEuMzIuMCIsICJsaWdodG5pbmdjc3Mtd2luMzItYXJtNjQtbXN2YyI6ICIxLjMyLjAiLCAibGlnaHRuaW5nY3NzLXdpbjMyLXg2NC1tc3ZjIjogIjEuMzIuMCIgfSB9LCAic2hhNTEyLU5YWUJ6aW5OcmJsZnJhUEd5cmJQb0QxOUMxaDlsZkkvMW16Z1dZdlhVVGU0MTRHei9YMUZEMlhCWlNaTTdyUlRyTUE4SkwzT3RBYUdpZnJJS2hRNXlRPT0iXSwKCiAgICAibGlnaHRuaW5nY3NzLWFuZHJvaWQtYXJtNjQiOiBbImxpZ2h0bmluZ2Nzcy1hbmRyb2lkLWFybTY0QDEuMzIuMCIsICIiLCB7ICJvcyI6ICJhbmRyb2lkIiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi1ZSzcvQ2xUdDRrQUswdm82dzNYK1BubTBEMmNmMnZQSGJoT1hkb050aTFHYTBhbDFQNFRCWmh3akFUdmpOd0xFQkNuS3ZqSmMyalFnSFhIME5Fd2xBZz09Il0sCgogICAgImxpZ2h0bmluZ2Nzcy1kYXJ3aW4tYXJtNjQiOiBbImxpZ2h0bmluZ2Nzcy1kYXJ3aW4tYXJtNjRAMS4zMi4wIiwgIiIsIHsgIm9zIjogImRhcndpbiIsICJjcHUiOiAiYXJtNjQiIH0sICJzaGE1MTItUnplRzlKdTViYWcyQnYxL2x3bFZKdkJFM3E2VHRYc2tkWkxMQ3lmZzVwdCtITHo5QnFsSUNPN0xaTTdWSE5UVG4vNVBSaEhGQlNqazVsYzRjbXNjUFE9PSJdLAoKICAgICJsaWdodG5pbmdjc3MtZGFyd2luLXg2NCI6IFsibGlnaHRuaW5nY3NzLWRhcndpbi14NjRAMS4zMi4wIiwgIiIsIHsgIm9zIjogImRhcndpbiIsICJjcHUiOiAieDY0IiB9LCAic2hhNTEyLVUrUXNCcDJtL3Myd3FwVVlULzZ3bmxhZ2RaYnRaZG5kU211dC9OSnFsQ2NNTFRXcDVtdUNySUQrSzVVSjZqcUQyQkZzaGVqQ1lYbmlQRGJOaDczVjh3PT0iXSwKCiAgICAibGlnaHRuaW5nY3NzLWZyZWVic2QteDY0IjogWyJsaWdodG5pbmdjc3MtZnJlZWJzZC14NjRAMS4zMi4wIiwgIiIsIHsgIm9zIjogImZyZWVic2QiLCAiY3B1IjogIng2NCIgfSwgInNoYTUxMi1KQ1RpZ2VkRWtzWmszdEhUVHRobk1kVmZHZjYxRmt5OEppMkU0WWpVVEVRWDE0eGl5L2xUelhudTF2d2laZTNiWWUwcStTcHNTSC9DVGVEWEs2V0hpZz09Il0sCgogICAgImxpZ2h0bmluZ2Nzcy1saW51eC1hcm0tZ251ZWFiaWhmIjogWyJsaWdodG5pbmdjc3MtbGludXgtYXJtLWdudWVhYmloZkAxLjMyLjAiLCAiIiwgeyAib3MiOiAibGludXgiLCAiY3B1IjogImFybSIgfSwgInNoYTUxMi14NnJubnBSYTJHTDB6UU9rdDZydHMzWURQemR1THBXdndBRjZFTWhYRlZaWEQ0dFByQmtFRnF6R293ekNzSVdzUGpxU0srdHlORU9EVUJYZWVWSFNrdz09Il0sCgogICAgImxpZ2h0bmluZ2Nzcy1saW51eC1hcm02NC1nbnUiOiBbImxpZ2h0bmluZ2Nzcy1saW51eC1hcm02NC1nbnVAMS4zMi4wIiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi0wbm5NeW95T0xSSlhmYk1PaWxhU1JjTEgzSnc1ejlIRE5HZlQvZ3dDUGdhRGpueDBpOHc3dkJ6RkxGUjFmNkNNTEtGOGdWYmVibWtVTjNmYS9rUUpwUT09Il0sCgogICAgImxpZ2h0bmluZ2Nzcy1saW51eC1hcm02NC1tdXNsIjogWyJsaWdodG5pbmdjc3MtbGludXgtYXJtNjQtbXVzbEAxLjMyLjAiLCAiIiwgeyAib3MiOiAibGludXgiLCAiY3B1IjogImFybTY0IiB9LCAic2hhNTEyLVVwUWtvZW5yNFVKRXpnVklZcEk4MGxERnZSbVBWZzZvcWJvTkhmb0g0Q1FJZk5BK0hPclo3TW83S1pQMDJkQzZMamdoUFFKZUJzdlhoSm9kL3duSUJnPT0iXSwKCiAgICAibGlnaHRuaW5nY3NzLWxpbnV4LXg2NC1nbnUiOiBbImxpZ2h0bmluZ2Nzcy1saW51eC14NjQtZ251QDEuMzIuMCIsICIiLCB7ICJvcyI6ICJsaW51eCIsICJjcHUiOiAieDY0IiB9LCAic2hhNTEyLVY3UXI1MkloWm1kS1BWcitWdHc4bytXTHNRSllDVGQ4bG9JZnBEYU1SV0dVWmZCT1lFSmV5SklrcUdJRE1aUHdQeDI0cFVNZndTeHhJOHBoci9NYk9BPT0iXSwKCiAgICAibGlnaHRuaW5nY3NzLWxpbnV4LXg2NC1tdXNsIjogWyJsaWdodG5pbmdjc3MtbGludXgteDY0LW11c2xAMS4zMi4wIiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJ4NjQiIH0sICJzaGE1MTItYlljTHArVmIwYXdzaVhnLzgwdUNSZXpDWUhOZzEvbDNtdDBnekhuV1Y5WFAxVzVzS2E1L1RDZEdXYVIvekJNMlBlRi9IYnNRdi9qMlVSTk9pVnV4V2c9PSJdLAoKICAgICJsaWdodG5pbmdjc3Mtd2luMzItYXJtNjQtbXN2YyI6IFsibGlnaHRuaW5nY3NzLXdpbjMyLWFybTY0LW1zdmNAMS4zMi4wIiwgIiIsIHsgIm9zIjogIndpbjMyIiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi04U2JDOEJSNDBwUzZiYUNNOHNidFlEU3dFVlFkNEpsRlRPbGFEM2dXR0hmVGhUY0FCbk5EQmRhNmVUWmVxYm9mYWxJSmhGeDBxS3pnSEptY1BUbkdkdz09Il0sCgogICAgImxpZ2h0bmluZ2Nzcy13aW4zMi14NjQtbXN2YyI6IFsibGlnaHRuaW5nY3NzLXdpbjMyLXg2NC1tc3ZjQDEuMzIuMCIsICIiLCB7ICJvcyI6ICJ3aW4zMiIsICJjcHUiOiAieDY0IiB9LCAic2hhNTEyLUFtcTlCL1NvWllkRGkxa0Zyb2pub3FQTHhZaFE0V281WGlMOEVWSnJWc0I4QVJvQzFQV1c2Vkd0VDBXS0NlbWp5OGFDK2xvdUpualM3VTE4eDNiMDZRPT0iXSwKCiAgICAibWFnaWMtc3RyaW5nIjogWyJtYWdpYy1zdHJpbmdAMC4zMC4yMSIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAanJpZGdld2VsbC9zb3VyY2VtYXAtY29kZWMiOiAiXjEuNS41IiB9IH0sICJzaGE1MTItdmQyRjRZVXlFWEtHY0xIb3ErVEV5Q2p4dWVTZUhuRnh5eWpOcDgweWcwWFY0dlVobkRlci9sdnZscU0vYXJCNWJYUU41SzIvM29pbnlDUnl4OFQyQ1E9PSJdLAoKICAgICJuYW5vaWQiOiBbIm5hbm9pZEAzLjMuMTIiLCAiIiwgeyAiYmluIjogeyAibmFub2lkIjogImJpbi9uYW5vaWQuY2pzIiB9IH0sICJzaGE1MTItWkI5UkgvMzlxcHE1VnU2WStObVVhRmhRUjZwcCtNMlh0NzZYQm5Fd0RhR2NWQXFobHZ4cmwzQjJiS1M1RDNOSDNRUjc2djNhU3JLYUYvS2l5N2xFdFE9PSJdLAoKICAgICJuZXh0IjogWyJuZXh0QDE2LjIuNiIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAbmV4dC9lbnYiOiAiMTYuMi42IiwgIkBzd2MvaGVscGVycyI6ICIwLjUuMTUiLCAiYmFzZWxpbmUtYnJvd3Nlci1tYXBwaW5nIjogIl4yLjkuMTkiLCAiY2FuaXVzZS1saXRlIjogIl4xLjAuMzAwMDE1NzkiLCAicG9zdGNzcyI6ICI4LjQuMzEiLCAic3R5bGVkLWpzeCI6ICI1LjEuNiIgfSwgIm9wdGlvbmFsRGVwZW5kZW5jaWVzIjogeyAiQG5leHQvc3djLWRhcndpbi1hcm02NCI6ICIxNi4yLjYiLCAiQG5leHQvc3djLWRhcndpbi14NjQiOiAiMTYuMi42IiwgIkBuZXh0L3N3Yy1saW51eC1hcm02NC1nbnUiOiAiMTYuMi42IiwgIkBuZXh0L3N3Yy1saW51eC1hcm02NC1tdXNsIjogIjE2LjIuNiIsICJAbmV4dC9zd2MtbGludXgteDY0LWdudSI6ICIxNi4yLjYiLCAiQG5leHQvc3djLWxpbnV4LXg2NC1tdXNsIjogIjE2LjIuNiIsICJAbmV4dC9zd2Mtd2luMzItYXJtNjQtbXN2YyI6ICIxNi4yLjYiLCAiQG5leHQvc3djLXdpbjMyLXg2NC1tc3ZjIjogIjE2LjIuNiIsICJzaGFycCI6ICJeMC4zNC41IiB9LCAicGVlckRlcGVuZGVuY2llcyI6IHsgIkBvcGVudGVsZW1ldHJ5L2FwaSI6ICJeMS4xLjAiLCAiQHBsYXl3cmlnaHQvdGVzdCI6ICJeMS41MS4xIiwgImJhYmVsLXBsdWdpbi1yZWFjdC1jb21waWxlciI6ICIqIiwgInJlYWN0IjogIl4xOC4yLjAgfHwgMTkuMC4wLXJjLWRlNjhkMmY0LTIwMjQxMjA0IHx8IF4xOS4wLjAiLCAicmVhY3QtZG9tIjogIl4xOC4yLjAgfHwgMTkuMC4wLXJjLWRlNjhkMmY0LTIwMjQxMjA0IHx8IF4xOS4wLjAiLCAic2FzcyI6ICJeMS4zLjAiIH0sICJvcHRpb25hbFBlZXJzIjogWyJAb3BlbnRlbGVtZXRyeS9hcGkiLCAiQHBsYXl3cmlnaHQvdGVzdCIsICJiYWJlbC1wbHVnaW4tcmVhY3QtY29tcGlsZXIiLCAic2FzcyJdLCAiYmluIjogeyAibmV4dCI6ICJkaXN0L2Jpbi9uZXh0IiB9IH0sICJzaGE1MTItcU9WZ0tKZzErQXQxNU5wZVVQK2VKZ0NIdlRDZ1hzb2d3ZXE4N1JpL0l4N1BrcVFIZzRzZGFYbVNGcUtsZ2FJWEU0a1cwZzI1TEU2OFc4N1VBTmxIdHc9PSJdLAoKICAgICJvYnVnIjogWyJvYnVnQDIuMS4xIiwgIiIsIHt9LCAic2hhNTEyLXVUcUY5TXVQcmFBUStJc25QZjM2NlJHNGNQOVJ0VWk3TUxPMU4zS0VjK3diMGE2eUtwZUwwbG1rMklCMWpZNUtIUEFsVGM2VC9KUmRDL1lxeEhOd2tRPT0iXSwKCiAgICAicGF0aGUiOiBbInBhdGhlQDIuMC4zIiwgIiIsIHt9LCAic2hhNTEyLVdVakdjQXFQMWdRYWNvUWUrT0JKc0ZBN0xkNER5WHVVSWpaNWNjNzVjTEh2SjdkdE5zVHVncGh4SUFEd3NwUytBcmFBVWVQQ0tyU1Z0UExGai9GODh3PT0iXSwKCiAgICAicGljb2NvbG9ycyI6IFsicGljb2NvbG9yc0AxLjEuMSIsICIiLCB7fSwgInNoYTUxMi14Y2VIMnNuaHRiNU05bGlxRHNtRXc1NmxlMzc2bVRaa0VYL2pFYi9SeE5GeWVnTnVsN2VOc2xDWFA5RkRqL0xjdTBYOEtFeU1jZVAybnRwYUhyREVWQT09Il0sCgogICAgInBpY29tYXRjaCI6IFsicGljb21hdGNoQDQuMC40IiwgIiIsIHt9LCAic2hhNTEyLVFQODhCQUt2TWFtLzNOeEg2dmoybzIxUjZNanhaVUFkNm5sd0FTL3BuR3ZOOUlWTG9jTEh4R1lJekZoZzZmVVErNXRoNlA0ZHY0ZVc5algzRFNJajdBPT0iXSwKCiAgICAicGxheXdyaWdodCI6IFsicGxheXdyaWdodEAxLjYwLjAiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAicGxheXdyaWdodC1jb3JlIjogIjEuNjAuMCIgfSwgIm9wdGlvbmFsRGVwZW5kZW5jaWVzIjogeyAiZnNldmVudHMiOiAiMi4zLjIiIH0sICJiaW4iOiB7ICJwbGF5d3JpZ2h0IjogImNsaS5qcyIgfSB9LCAic2hhNTEyLWhoZUhkb2tNOGNkcUNiMGxjRTNzK3pUNHQ0Vyt2dmpwR3hzWmxEbmlrYXJ6eDh0U3pNZWJoM1VpRnRncXdGd25UbmpZUWNzeU1GOGVpMm1DTy90cGVBPT0iXSwKCiAgICAicGxheXdyaWdodC1jb3JlIjogWyJwbGF5d3JpZ2h0LWNvcmVAMS42MC4wIiwgIiIsIHsgImJpbiI6IHsgInBsYXl3cmlnaHQtY29yZSI6ICJjbGkuanMiIH0gfSwgInNoYTUxMi05Ylc2enZYL20wbEViZ1RLSjZZcHBPS3g4SDNWT1BCTU9DRmgyaXJYRk9UNEJiSGdyeDVoUGp3SllMVDQwTHUrNHF0RDM2cUtjL0huNTZTdFVXNTdJQT09Il0sCgogICAgInBvc3Rjc3MiOiBbInBvc3Rjc3NAOC41LjE1IiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIm5hbm9pZCI6ICJeMy4zLjEyIiwgInBpY29jb2xvcnMiOiAiXjEuMS4xIiwgInNvdXJjZS1tYXAtanMiOiAiXjEuMi4xIiB9IH0sICJzaGE1MTItRmZSOHNqZDRlbTJUNmZiM0kyTXdBSlU3SFdWTXI5emJhK2VubVFlZVdGZkNibStVT0MvMFg0RFM4WHRwVVRNd1dNR2JqS1lQN3hqZk5la3p5R21CM0E9PSJdLAoKICAgICJyZWFjdCI6IFsicmVhY3RAMTkuMi42IiwgIiIsIHt9LCAic2hhNTEyLXNmV0dHZmF2aTB4cjhQZzBzVnN5SE1BT3ppVllLZ1BMTnJTN2lnK2l2TU5iM3diQ0J3M0t4dGZsc0dCQXdEM2dZUWxFL0FFWnNUTGdUb1JyU0NqYjBRPT0iXSwKCiAgICAicmVhY3QtZGF5LXBpY2tlciI6IFsicmVhY3QtZGF5LXBpY2tlckA5LjE0LjAiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiQGRhdGUtZm5zL3R6IjogIl4xLjQuMSIsICJAdGFiYnlfYWkvaGlqcmktY29udmVydGVyIjogIjEuMC41IiwgImRhdGUtZm5zIjogIl40LjEuMCIsICJkYXRlLWZucy1qYWxhbGkiOiAiNC4xLjAtMCIgfSwgInBlZXJEZXBlbmRlbmNpZXMiOiB7ICJyZWFjdCI6ICI+PTE2LjguMCIgfSB9LCAic2hhNTEyLXRCYW9EV2pQd2UwTTVwR3J1bTRIMFNSNkx5aytCTzlvSG5wOUpiS3BHS1cybWxyYU5QZ1A5Qk1mc2c1cFdwd3Jzc0FSbWVxazdZQmwyb1h1dFpUYUhBPT0iXSwKCiAgICAicmVhY3QtZG9tIjogWyJyZWFjdC1kb21AMTkuMi42IiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgInNjaGVkdWxlciI6ICJeMC4yNy4wIiB9LCAicGVlckRlcGVuZGVuY2llcyI6IHsgInJlYWN0IjogIl4xOS4yLjYiIH0gfSwgInNoYTUxMi0wcHJNSStodkJiUGpzV254REx4bENHeU04UE42VXVXakVVQ1ltWmhPNjd4SVY5WGFzYS9yL3ZEbnErWHlxNExvMjdnOFFTYk81WXpBUnUwRDFTcHMzZz09Il0sCgogICAgInJlcXVpcmUtZGlyZWN0b3J5IjogWyJyZXF1aXJlLWRpcmVjdG9yeUAyLjEuMSIsICIiLCB7fSwgInNoYTUxMi1mR3hFSTcrd3NHOXhydmRqc3JsbUwyMk9NVFRpSFJ3QU1yb2lFZU1ncThnem9MQy9QUXI3UnNSRFNUTFVnL2JaQVp0RitUVklrSGM2LzRSSUtydWkrUT09Il0sCgogICAgInJlc2VsZWN0IjogWyJyZXNlbGVjdEA1LjIuMCIsICIiLCB7fSwgInNoYTUxMi1BZ1ozVU9abTNZbmRmcko0T1lqZ3JUN2JtQ20vMWlxa2p2RWZIL29ZanpoNlBEMnF3NFF1VDNqam5YSXJwZHQ0TVRwTVhjbE1UM2xYYm1SWStYUmFrdz09Il0sCgogICAgInJvbGxkb3duIjogWyJyb2xsZG93bkAxLjAuMSIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAb3hjLXByb2plY3QvdHlwZXMiOiAiPTAuMTMwLjAiLCAiQHJvbGxkb3duL3BsdWdpbnV0aWxzIjogIl4xLjAuMCIgfSwgIm9wdGlvbmFsRGVwZW5kZW5jaWVzIjogeyAiQHJvbGxkb3duL2JpbmRpbmctYW5kcm9pZC1hcm02NCI6ICIxLjAuMSIsICJAcm9sbGRvd24vYmluZGluZy1kYXJ3aW4tYXJtNjQiOiAiMS4wLjEiLCAiQHJvbGxkb3duL2JpbmRpbmctZGFyd2luLXg2NCI6ICIxLjAuMSIsICJAcm9sbGRvd24vYmluZGluZy1mcmVlYnNkLXg2NCI6ICIxLjAuMSIsICJAcm9sbGRvd24vYmluZGluZy1saW51eC1hcm0tZ251ZWFiaWhmIjogIjEuMC4xIiwgIkByb2xsZG93bi9iaW5kaW5nLWxpbnV4LWFybTY0LWdudSI6ICIxLjAuMSIsICJAcm9sbGRvd24vYmluZGluZy1saW51eC1hcm02NC1tdXNsIjogIjEuMC4xIiwgIkByb2xsZG93bi9iaW5kaW5nLWxpbnV4LXBwYzY0LWdudSI6ICIxLjAuMSIsICJAcm9sbGRvd24vYmluZGluZy1saW51eC1zMzkweC1nbnUiOiAiMS4wLjEiLCAiQHJvbGxkb3duL2JpbmRpbmctbGludXgteDY0LWdudSI6ICIxLjAuMSIsICJAcm9sbGRvd24vYmluZGluZy1saW51eC14NjQtbXVzbCI6ICIxLjAuMSIsICJAcm9sbGRvd24vYmluZGluZy1vcGVuaGFybW9ueS1hcm02NCI6ICIxLjAuMSIsICJAcm9sbGRvd24vYmluZGluZy13YXNtMzItd2FzaSI6ICIxLjAuMSIsICJAcm9sbGRvd24vYmluZGluZy13aW4zMi1hcm02NC1tc3ZjIjogIjEuMC4xIiwgIkByb2xsZG93bi9iaW5kaW5nLXdpbjMyLXg2NC1tc3ZjIjogIjEuMC4xIiB9LCAiYmluIjogeyAicm9sbGRvd24iOiAiYmluL2NsaS5tanMiIH0gfSwgInNoYTUxMi1YMEtRSGxqTm5Fa1dOcXFpejl6SnJHdW5oMUIwSGdPeExYdm5GcENPY2FkemN5NXFvaFozdHFNRVVnMDB2bmNvUm92WHVLM1pxQ1Q5S25uS3pvSW5GUT09Il0sCgogICAgInJ4anMiOiBbInJ4anNANy44LjIiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAidHNsaWIiOiAiXjIuMS4wIiB9IH0sICJzaGE1MTItZGhLZjkwM1UvUFFaWTZib05OdEFHZFdiRzg1V0FialQvMXhZb1pJQzdGQVkweVdhcE9CUVZzVnJEbDU4Vzg2Ly9lMVZwTU5CdFJWNE1hWGZkTXlTRkE9PSJdLAoKICAgICJzY2hlZHVsZXIiOiBbInNjaGVkdWxlckAwLjI3LjAiLCAiIiwge30sICJzaGE1MTItZU52K1dyVmJLdTFmM3ZiWUpUL3h0aUY1c3lBNUhQSU10ZjlJZ1kvbktnMHNXcXpBVUV2cVkveG03T2NaYy9xYWZMeC9pTzlGZ09tZVNBcDR2NXRpL1E9PSJdLAoKICAgICJzZW12ZXIiOiBbInNlbXZlckA3LjguMCIsICIiLCB7ICJiaW4iOiB7ICJzZW12ZXIiOiAiYmluL3NlbXZlci5qcyIgfSB9LCAic2hhNTEyLUFjTTdkVi81dWw0RWVrb1EyOUFnbTV2cmk4Sk5xUnlqMzlvMHFwWDZ2REYyR1pydHV0Wmw1UndnRDFYblpqaVRBZm5jc0poTUk0OFFRSDNzTjg3WU5BPT0iXSwKCiAgICAic2hhcnAiOiBbInNoYXJwQDAuMzQuNSIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAaW1nL2NvbG91ciI6ICJeMS4wLjAiLCAiZGV0ZWN0LWxpYmMiOiAiXjIuMS4yIiwgInNlbXZlciI6ICJeNy43LjMiIH0sICJvcHRpb25hbERlcGVuZGVuY2llcyI6IHsgIkBpbWcvc2hhcnAtZGFyd2luLWFybTY0IjogIjAuMzQuNSIsICJAaW1nL3NoYXJwLWRhcndpbi14NjQiOiAiMC4zNC41IiwgIkBpbWcvc2hhcnAtbGlidmlwcy1kYXJ3aW4tYXJtNjQiOiAiMS4yLjQiLCAiQGltZy9zaGFycC1saWJ2aXBzLWRhcndpbi14NjQiOiAiMS4yLjQiLCAiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4LWFybSI6ICIxLjIuNCIsICJAaW1nL3NoYXJwLWxpYnZpcHMtbGludXgtYXJtNjQiOiAiMS4yLjQiLCAiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4LXBwYzY0IjogIjEuMi40IiwgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC1yaXNjdjY0IjogIjEuMi40IiwgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC1zMzkweCI6ICIxLjIuNCIsICJAaW1nL3NoYXJwLWxpYnZpcHMtbGludXgteDY0IjogIjEuMi40IiwgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eG11c2wtYXJtNjQiOiAiMS4yLjQiLCAiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4bXVzbC14NjQiOiAiMS4yLjQiLCAiQGltZy9zaGFycC1saW51eC1hcm0iOiAiMC4zNC41IiwgIkBpbWcvc2hhcnAtbGludXgtYXJtNjQiOiAiMC4zNC41IiwgIkBpbWcvc2hhcnAtbGludXgtcHBjNjQiOiAiMC4zNC41IiwgIkBpbWcvc2hhcnAtbGludXgtcmlzY3Y2NCI6ICIwLjM0LjUiLCAiQGltZy9zaGFycC1saW51eC1zMzkweCI6ICIwLjM0LjUiLCAiQGltZy9zaGFycC1saW51eC14NjQiOiAiMC4zNC41IiwgIkBpbWcvc2hhcnAtbGludXhtdXNsLWFybTY0IjogIjAuMzQuNSIsICJAaW1nL3NoYXJwLWxpbnV4bXVzbC14NjQiOiAiMC4zNC41IiwgIkBpbWcvc2hhcnAtd2FzbTMyIjogIjAuMzQuNSIsICJAaW1nL3NoYXJwLXdpbjMyLWFybTY0IjogIjAuMzQuNSIsICJAaW1nL3NoYXJwLXdpbjMyLWlhMzIiOiAiMC4zNC41IiwgIkBpbWcvc2hhcnAtd2luMzIteDY0IjogIjAuMzQuNSIgfSB9LCAic2hhNTEyLU91OUk1RnQ5V05jQ2JYclU5Y01nUEJjQ0s4TGl3THFjYnl3VzN0NG9EVjM3bjFwenB1TkxzWWlBVjhlT0RuamJ0UWxTRHdaMmNVRWVRejRFNTRIbHRnPT0iXSwKCiAgICAic2hlbGwtcXVvdGUiOiBbInNoZWxsLXF1b3RlQDEuOC4zIiwgIiIsIHt9LCAic2hhNTEyLU9ibW5JRjRoWE5nMUJxaG5IbWdiREVURjhkTFBDZ2daV0Jqa1FmaFpwYnN6Wm5ZdXI1RFVsalRjQ0hpaTVMQzNKNUUweWVPLzFMSU15SCtVdkhRZ3l3PT0iXSwKCiAgICAic2lnaW5mbyI6IFsic2lnaW5mb0AyLjAuMCIsICIiLCB7fSwgInNoYTUxMi15YngwV08xLzhiU0JMRVdYWnZFZDdnTVczU24zSkZsVzNUdlgxblJFYkRMUk5RTmFlTk44V0swbWVCd1BkQWFPSTdUdFJSUkpuL0VzMXpocnJDSHU3Zz09Il0sCgogICAgInNvdXJjZS1tYXAtanMiOiBbInNvdXJjZS1tYXAtanNAMS4yLjEiLCAiIiwge30sICJzaGE1MTItVVhXTUtoTE93VktiNzI4SVV0UVBYeGZZVSt1c2R5YnRVcksvOHVHRThDUU12cmhPcHd2ekRCd2owUWhTTDdNUWM3dklzSVNCRzhWUTgrSURReHBmUUE9PSJdLAoKICAgICJzdGFja2JhY2siOiBbInN0YWNrYmFja0AwLjAuMiIsICIiLCB7fSwgInNoYTUxMi0xWE1KRTVmUW8xakdINlkvN2VibndQT0JFa0lFblQ0UUYzMmQ1UjErVlhkWHZlTTBJQk1KdDh6ZmF4WDFQM1FoVndyWWUrNTc2K2prQU50U1MybUJidz09Il0sCgogICAgInN0ZC1lbnYiOiBbInN0ZC1lbnZANC4xLjAiLCAiIiwge30sICJzaGE1MTItUnE3eWJjWDJSdUM1NXI5b2FQVkVXNy94dTN0ajh1NEdlQllIQldDeWNoRnR6TUlyODZBN2UzUFBFQlBUMzdzSFN0S1gzK1RpWC9Gci9BQ21KTFZsTFE9PSJdLAoKICAgICJzdHJpbmctd2lkdGgiOiBbInN0cmluZy13aWR0aEA0LjIuMyIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJlbW9qaS1yZWdleCI6ICJeOC4wLjAiLCAiaXMtZnVsbHdpZHRoLWNvZGUtcG9pbnQiOiAiXjMuMC4wIiwgInN0cmlwLWFuc2kiOiAiXjYuMC4xIiB9IH0sICJzaGE1MTItd0t5UVJRcGpKMHNJcDYyRXJTWmRHc2pNSldzYXA1b1JOaWhIaHU2RzdKVk8vOWpJQjZVeWV2TCt0WHVPcXJuZzhqL2N4S1RXeVdVd3ZTVHJpaVp6L2c9PSJdLAoKICAgICJzdHJpcC1hbnNpIjogWyJzdHJpcC1hbnNpQDYuMC4xIiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgImFuc2ktcmVnZXgiOiAiXjUuMC4xIiB9IH0sICJzaGE1MTItWTM4VlBTSGNxa0ZyQ3BGblE5dnVTWG1xdXV2NW9YT0twR2VUNmFHcnIzbzNHYzlBbFZhNkpCZlVTT0NuYnhHR1pGKy8wb29JN0tyUHVVU3p0VWRVNUE9PSJdLAoKICAgICJzdHlsZWQtanN4IjogWyJzdHlsZWQtanN4QDUuMS42IiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgImNsaWVudC1vbmx5IjogIjAuMC4xIiB9LCAicGVlckRlcGVuZGVuY2llcyI6IHsgInJlYWN0IjogIj49IDE2LjguMCB8fCAxNy54LnggfHwgXjE4LjAuMC0wIHx8IF4xOS4wLjAtMCIgfSB9LCAic2hhNTEyLXFTVnlEVGVNb3RkdlFZb0hXTE5Hd1JGSkhDK2krWnZkQlJZb3NPRmdDK1dnMXZ4NGZyTjIvUkcvTkE3U1lxcXZLTkxmMzlQMkxTUkEycHU2bjBYWVpBPT0iXSwKCiAgICAic3VwcG9ydHMtY29sb3IiOiBbInN1cHBvcnRzLWNvbG9yQDguMS4xIiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgImhhcy1mbGFnIjogIl40LjAuMCIgfSB9LCAic2hhNTEyLU1wVUVOMk9vZHRVenh2S1FsNzJjVUY3UlE1RWlIc0d2U3NWRzBpYTljNVJiV0dMMkNJNEM3RXBQUzhVVEJJcGxubHpaaU51VjU2dytGdU54eTN0eTJRPT0iXSwKCiAgICAidGFpbHdpbmQtbWVyZ2UiOiBbInRhaWx3aW5kLW1lcmdlQDMuNi4wIiwgIiIsIHt9LCAic2hhNTEyLXV4TDdxQVZRcmlxUlFQQXlLM3BqNjZWcXNrV3FvWjM3UFc5NGp3T1R3TmZxL3o5b3l1MVYrZXFyWnF0UjIrZkNpWGRZT1plL01vZHQ4R3R2cU56dSt3PT0iXSwKCiAgICAidGFpbHdpbmRjc3MiOiBbInRhaWx3aW5kY3NzQDQuMy4wIiwgIiIsIHt9LCAic2hhNTEyLXk2bnhNR0Ixbk1XOVI2azk2ZTVnZElGemNmTC9nVEpSTmFxR2VzMVl2a0xuUFZYeldnYnFGRjJ5TEMwVDhHNzc0bjI0Y3gzUGU4WHJLb25pQ09BSCtRPT0iXSwKCiAgICAidGFwYWJsZSI6IFsidGFwYWJsZUAyLjMuMyIsICIiLCB7fSwgInNoYTUxMi11eGMvenBxRmc2eDdDOHZPRTdsaDZMYmRhOGVFTDl6bVZtL1BMZVRQQlJoaDF4Q2dkV2FRK0oxQ1VpZUdwSWZtMkhkdHNVcFJ2K0hzaGlhc0JNY2M2QT09Il0sCgogICAgInRpbnliZW5jaCI6IFsidGlueWJlbmNoQDIuOS4wIiwgIiIsIHt9LCAic2hhNTEyLTArRFV2cVdNVmFsTG1oYTZscjRrRDhpQU1LMUh6VjAvYUtuQ3RXYjl2OTY0MVRuUC9NRmI3UGMyYnhveFFqVFhBRXJyeVhWZ1VPZnYyWXFObGxxR2VnPT0iXSwKCiAgICAidGlueWV4ZWMiOiBbInRpbnlleGVjQDEuMS4yIiwgIiIsIHt9LCAic2hhNTEyLWRBcVNxRS9SYWJwQktJOCtoMjZHZkxxNlZiM0pWWHMzMFhZUWpkTWphai9jMnRTOElZWU1iSXpQNTk5S3RSajdjNTcvd1lBcGIzUWpnUmdYbXJDdWtBPT0iXSwKCiAgICAidGlueWdsb2JieSI6IFsidGlueWdsb2JieUAwLjIuMTYiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiZmRpciI6ICJeNi41LjAiLCAicGljb21hdGNoIjogIl40LjAuNCIgfSB9LCAic2hhNTEyLXBuOTlWaG9BQ1lSOG5GSGh4cWl4K3V2c2JYaW5lQWFzV201b2pYb044eEV3SzVLZDMvVHJoTm4xd0J5dUQ1MlV4V1JMeThwdStrUk1uaUVpNkVxOVpnPT0iXSwKCiAgICAidGlueXJhaW5ib3ciOiBbInRpbnlyYWluYm93QDMuMS4wIiwgIiIsIHt9LCAic2hhNTEyLUJmK0lMbUJncmV0VXJkSnh6WE0wU2dYTFozWGZpYVV1T2ovSUtRSHVUWGlwKzA1WG4rdXlFWWRWZzBrWURpcFRCY0xyQ1Z5VXpBUHo3UW1BcmIwbW13PT0iXSwKCiAgICAidHJlZS1raWxsIjogWyJ0cmVlLWtpbGxAMS4yLjIiLCAiIiwgeyAiYmluIjogeyAidHJlZS1raWxsIjogImNsaS5qcyIgfSB9LCAic2hhNTEyLUwwT3JwaThxR3BSRy8vTmQrSDkwdkZCKzNpSG51ZTF6U1NHbU5PT0NoMUdMSjdyVUtWd1YySHZpanBoR1FTMlVtaFVaZXdTOVZndnhZSWRncitmRzFBPT0iXSwKCiAgICAidHNsaWIiOiBbInRzbGliQDIuOC4xIiwgIiIsIHt9LCAic2hhNTEyLW9KRnU5NEhRYitLVmR1U1VRTDd3bnBtcW5mbUxzT0EvbkFoNmI2RUgwd0NFb0swL21QZVhVNmMzd0tEVjgzTWtPdUhQUkh0U1hLS1U5OUlCYXpTLzJ3PT0iXSwKCiAgICAidHlwZXNjcmlwdCI6IFsidHlwZXNjcmlwdEA1LjkuMyIsICIiLCB7ICJiaW4iOiB7ICJ0c2MiOiAiYmluL3RzYyIsICJ0c3NlcnZlciI6ICJiaW4vdHNzZXJ2ZXIiIH0gfSwgInNoYTUxMi1qbDF2WnpQRGluTHI5ZVV0M0ovdDdWNkZnTkV3OVFqdkJQZHlzejlLZlFERDQxZlFyQzJZNHZLUWRpYVVwRlQ0YlhsYjFSSGhMcHA4d3RtNk01VGdTdz09Il0sCgogICAgInVuZGljaS10eXBlcyI6IFsidW5kaWNpLXR5cGVzQDYuMjEuMCIsICIiLCB7fSwgInNoYTUxMi1pd0RacWcwUUFHcmc5UmF2NUg0bjBNNjRjM21rUjU5Y0o2d1FwKzdDNG5JMGdzbUV4YWVkYVlMTk80NGVUNEF0QkJ3amJUaUdQTWx0Mk1kMFQ5SDlKUT09Il0sCgogICAgInVzZS1zeW5jLWV4dGVybmFsLXN0b3JlIjogWyJ1c2Utc3luYy1leHRlcm5hbC1zdG9yZUAxLjYuMCIsICIiLCB7ICJwZWVyRGVwZW5kZW5jaWVzIjogeyAicmVhY3QiOiAiXjE2LjguMCB8fCBeMTcuMC4wIHx8IF4xOC4wLjAgfHwgXjE5LjAuMCIgfSB9LCAic2hhNTEyLVBwNkdTd0dQL05yUElyeFZGQUlrT1FleXc4bEZlbk9IaWpRV2tVVHJEdnJGNEFMcXlsUDJDL0tDa2VTOWRwVU0zS3ZZUlFobmE1dnQ3SUw5NStaUTl3PT0iXSwKCiAgICAidml0ZSI6IFsidml0ZUA4LjAuMTMiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAibGlnaHRuaW5nY3NzIjogIl4xLjMyLjAiLCAicGljb21hdGNoIjogIl40LjAuNCIsICJwb3N0Y3NzIjogIl44LjUuMTQiLCAicm9sbGRvd24iOiAiMS4wLjEiLCAidGlueWdsb2JieSI6ICJeMC4yLjE2IiB9LCAib3B0aW9uYWxEZXBlbmRlbmNpZXMiOiB7ICJmc2V2ZW50cyI6ICJ+Mi4zLjMiIH0sICJwZWVyRGVwZW5kZW5jaWVzIjogeyAiQHR5cGVzL25vZGUiOiAiXjIwLjE5LjAgfHwgPj0yMi4xMi4wIiwgIkB2aXRlanMvZGV2dG9vbHMiOiAiXjAuMS4xOCIsICJlc2J1aWxkIjogIl4wLjI3LjAgfHwgXjAuMjguMCIsICJqaXRpIjogIj49MS4yMS4wIiwgImxlc3MiOiAiXjQuMC4wIiwgInNhc3MiOiAiXjEuNzAuMCIsICJzYXNzLWVtYmVkZGVkIjogIl4xLjcwLjAiLCAic3R5bHVzIjogIj49MC41NC44IiwgInN1Z2Fyc3MiOiAiXjUuMC4wIiwgInRlcnNlciI6ICJeNS4xNi4wIiwgInRzeCI6ICJeNC44LjEiLCAieWFtbCI6ICJeMi40LjIiIH0sICJvcHRpb25hbFBlZXJzIjogWyJAdHlwZXMvbm9kZSIsICJAdml0ZWpzL2RldnRvb2xzIiwgImVzYnVpbGQiLCAiaml0aSIsICJsZXNzIiwgInNhc3MiLCAic2Fzcy1lbWJlZGRlZCIsICJzdHlsdXMiLCAic3VnYXJzcyIsICJ0ZXJzZXIiLCAidHN4IiwgInlhbWwiXSwgImJpbiI6IHsgInZpdGUiOiAiYmluL3ZpdGUuanMiIH0gfSwgInNoYTUxMi1NRnRqQllnem1TeG1nQTRSQWZqSXlYV3BHZTFvQUxuamdVVHp6VjdRTHgvVEt4Q3pqdE1INkZkOS9lVksrNUZnMXFOb3o1VkF3c21Ncy9Ob2ZybUp2dz09Il0sCgogICAgInZpdGVzdCI6IFsidml0ZXN0QDQuMS42IiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIkB2aXRlc3QvZXhwZWN0IjogIjQuMS42IiwgIkB2aXRlc3QvbW9ja2VyIjogIjQuMS42IiwgIkB2aXRlc3QvcHJldHR5LWZvcm1hdCI6ICI0LjEuNiIsICJAdml0ZXN0L3J1bm5lciI6ICI0LjEuNiIsICJAdml0ZXN0L3NuYXBzaG90IjogIjQuMS42IiwgIkB2aXRlc3Qvc3B5IjogIjQuMS42IiwgIkB2aXRlc3QvdXRpbHMiOiAiNC4xLjYiLCAiZXMtbW9kdWxlLWxleGVyIjogIl4yLjAuMCIsICJleHBlY3QtdHlwZSI6ICJeMS4zLjAiLCAibWFnaWMtc3RyaW5nIjogIl4wLjMwLjIxIiwgIm9idWciOiAiXjIuMS4xIiwgInBhdGhlIjogIl4yLjAuMyIsICJwaWNvbWF0Y2giOiAiXjQuMC4zIiwgInN0ZC1lbnYiOiAiXjQuMC4wLXJjLjEiLCAidGlueWJlbmNoIjogIl4yLjkuMCIsICJ0aW55ZXhlYyI6ICJeMS4wLjIiLCAidGlueWdsb2JieSI6ICJeMC4yLjE1IiwgInRpbnlyYWluYm93IjogIl4zLjEuMCIsICJ2aXRlIjogIl42LjAuMCB8fCBeNy4wLjAgfHwgXjguMC4wIiwgIndoeS1pcy1ub2RlLXJ1bm5pbmciOiAiXjIuMy4wIiB9LCAicGVlckRlcGVuZGVuY2llcyI6IHsgIkBlZGdlLXJ1bnRpbWUvdm0iOiAiKiIsICJAb3BlbnRlbGVtZXRyeS9hcGkiOiAiXjEuOS4wIiwgIkB0eXBlcy9ub2RlIjogIl4yMC4wLjAgfHwgXjIyLjAuMCB8fCA+PTI0LjAuMCIsICJAdml0ZXN0L2Jyb3dzZXItcGxheXdyaWdodCI6ICI0LjEuNiIsICJAdml0ZXN0L2Jyb3dzZXItcHJldmlldyI6ICI0LjEuNiIsICJAdml0ZXN0L2Jyb3dzZXItd2ViZHJpdmVyaW8iOiAiNC4xLjYiLCAiQHZpdGVzdC9jb3ZlcmFnZS1pc3RhbmJ1bCI6ICI0LjEuNiIsICJAdml0ZXN0L2NvdmVyYWdlLXY4IjogIjQuMS42IiwgIkB2aXRlc3QvdWkiOiAiNC4xLjYiLCAiaGFwcHktZG9tIjogIioiLCAianNkb20iOiAiKiIgfSwgIm9wdGlvbmFsUGVlcnMiOiBbIkBlZGdlLXJ1bnRpbWUvdm0iLCAiQG9wZW50ZWxlbWV0cnkvYXBpIiwgIkB0eXBlcy9ub2RlIiwgIkB2aXRlc3QvYnJvd3Nlci1wbGF5d3JpZ2h0IiwgIkB2aXRlc3QvYnJvd3Nlci1wcmV2aWV3IiwgIkB2aXRlc3QvYnJvd3Nlci13ZWJkcml2ZXJpbyIsICJAdml0ZXN0L2NvdmVyYWdlLWlzdGFuYnVsIiwgIkB2aXRlc3QvY292ZXJhZ2UtdjgiLCAiQHZpdGVzdC91aSIsICJoYXBweS1kb20iLCAianNkb20iXSwgImJpbiI6IHsgInZpdGVzdCI6ICJ2aXRlc3QubWpzIiB9IH0sICJzaGE1MTItNmx2amJTM3A5YjRDcmRDbWd1emJoMi80dW9YaEdFMnE3MVI0T1g1c3FGOVIxYm85WGQ2ZkdyTUFmdnA1d25DemxCbkZWZENPcDZvbnVUUVZibzhpVVE9PSJdLAoKICAgICJ3aHktaXMtbm9kZS1ydW5uaW5nIjogWyJ3aHktaXMtbm9kZS1ydW5uaW5nQDIuMy4wIiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgInNpZ2luZm8iOiAiXjIuMC4wIiwgInN0YWNrYmFjayI6ICIwLjAuMiIgfSwgImJpbiI6IHsgIndoeS1pcy1ub2RlLXJ1bm5pbmciOiAiY2xpLmpzIiB9IH0sICJzaGE1MTItaFVybWFXQmRWRGN4dllxbnloMDl6dW5LelJPV2piWlRpTnk4ZEJFamtTN2VoRURRaWJYSjdYdmxtdGJ3dVRjbFVpSXlOK0N5WFFENFZta284Zk5tOHc9PSJdLAoKICAgICJ3cmFwLWFuc2kiOiBbIndyYXAtYW5zaUA3LjAuMCIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJhbnNpLXN0eWxlcyI6ICJeNC4wLjAiLCAic3RyaW5nLXdpZHRoIjogIl40LjEuMCIsICJzdHJpcC1hbnNpIjogIl42LjAuMCIgfSB9LCAic2hhNTEyLVlWR0lqMmthbUxTVHh3Nk5zWmpvQnhmU3dzbjB5Y2Rlc21jNHArUTIxYzV6UHVaMXBsK05meFZkeFB0ZEh2bU5WT1E2WFNZRzRBVXR5dC9GaTdEMTZRPT0iXSwKCiAgICAieTE4biI6IFsieTE4bkA1LjAuOCIsICIiLCB7fSwgInNoYTUxMi0wcGZGemVnZURXSkhKSUFtVExSUDJEd0hqZEY1czdqbzl0dXp0ZFF4QWhJTkNkdlMrM25HSU5xUGQwMEFwaHFKUi8wTGhBTlVTNi8rN1NDYjk4WU9mQT09Il0sCgogICAgInlhcmdzIjogWyJ5YXJnc0AxNy43LjIiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiY2xpdWkiOiAiXjguMC4xIiwgImVzY2FsYWRlIjogIl4zLjEuMSIsICJnZXQtY2FsbGVyLWZpbGUiOiAiXjIuMC41IiwgInJlcXVpcmUtZGlyZWN0b3J5IjogIl4yLjEuMSIsICJzdHJpbmctd2lkdGgiOiAiXjQuMi4zIiwgInkxOG4iOiAiXjUuMC41IiwgInlhcmdzLXBhcnNlciI6ICJeMjEuMS4xIiB9IH0sICJzaGE1MTItN2RTenpSUSsrQ0tuTkkva3JLbllSVjdKS0tQVVhNRWg2MXNvYUhLZzltcldFaHpGV2hGbnhQeEdsKzY5Y0QxT3U2M0MxM05VUENubUljcnZxQ3VNNnc9PSJdLAoKICAgICJ5YXJncy1wYXJzZXIiOiBbInlhcmdzLXBhcnNlckAyMS4xLjEiLCAiIiwge30sICJzaGE1MTItdFZwc0pXN0RkamVjQWlGcGJJQjFlM3F4SVFzRTZOb1BjNS9lVGRyYmJJQzRoMExWc1dobm9hM2crbTJIY2xCSXVqSHpzeFo0VkpWQStHVXVjMi9MQnc9PSJdLAoKICAgICJ6b2QiOiBbInpvZEA0LjQuMyIsICIiLCB7fSwgInNoYTUxMi15dEVORmpJSkZsMlV3WWdsZGUyamNoVzJId200R0pGTERpU1hXZFRySlFCSU45RmN5cDduNERoeEpFaVdOQUpNVjEvQnFXZlcva2tnNzFVRGNISnlUUT09Il0sCgogICAgIkB0YWlsd2luZGNzcy9veGlkZS13YXNtMzItd2FzaS9AZW1uYXBpL2NvcmUiOiBbIkBlbW5hcGkvY29yZUAxLjEwLjAiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiQGVtbmFwaS93YXNpLXRocmVhZHMiOiAiMS4yLjEiLCAidHNsaWIiOiAiXjIuNC4wIiB9LCAiYnVuZGxlZCI6IHRydWUgfSwgInNoYTUxMi15cTZPa0o0cDgyQ0FmUGwwdTltUWViUUhLUEprWTdXckl1azIwNWNUWW5ZZStrMlo4WUJoMTFGcmJSRy9INmloaXJxY2FjT2dsMkJJTzhveU1RTGVYdz09Il0sCgogICAgIkB0YWlsd2luZGNzcy9veGlkZS13YXNtMzItd2FzaS9AZW1uYXBpL3J1bnRpbWUiOiBbIkBlbW5hcGkvcnVudGltZUAxLjEwLjAiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAidHNsaWIiOiAiXjIuNC4wIiB9LCAiYnVuZGxlZCI6IHRydWUgfSwgInNoYTUxMi1ld3ZZbGs4NnhVb0dJMHpRUk5xL21DKzE2UjFRZURsS1F5MjFLaTNvU1lYTmdMYjQ1R1YxUDZBME0rL3M2bnlDdU5EcWU1VnBhWTg0QnpYR3dWYndGQT09Il0sCgogICAgIkB0YWlsd2luZGNzcy9veGlkZS13YXNtMzItd2FzaS9AZW1uYXBpL3dhc2ktdGhyZWFkcyI6IFsiQGVtbmFwaS93YXNpLXRocmVhZHNAMS4yLjEiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAidHNsaWIiOiAiXjIuNC4wIiB9LCAiYnVuZGxlZCI6IHRydWUgfSwgInNoYTUxMi11VElJN09ZRisvTWVzL01yY0lPWXA1eU90U01MQldTSW9MUHBjZ3dpcG9pS2JsaTZrMzIydGNvRnN4b0lJeFBEcVcwMVNRR0Fna280RXpaaTJCTnYydz09Il0sCgogICAgIkB0YWlsd2luZGNzcy9veGlkZS13YXNtMzItd2FzaS9AbmFwaS1ycy93YXNtLXJ1bnRpbWUiOiBbIkBuYXBpLXJzL3dhc20tcnVudGltZUAxLjEuNCIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAdHlieXMvd2FzbS11dGlsIjogIl4wLjEwLjEiIH0sICJwZWVyRGVwZW5kZW5jaWVzIjogeyAiQGVtbmFwaS9jb3JlIjogIl4xLjcuMSIsICJAZW1uYXBpL3J1bnRpbWUiOiAiXjEuNy4xIiB9LCAiYnVuZGxlZCI6IHRydWUgfSwgInNoYTUxMi0zTlFOTmdBMVlTbEpiL2tNSDFpbGRBU1A5SFc3LzdrWW5SSTJzeldKYW9mYVMxaFdtYkdJNEgrZDMrMjJhR3pYWE45SUorbitHaUZWY0dpcEpQMThvdz09Il0sCgogICAgIkB0YWlsd2luZGNzcy9veGlkZS13YXNtMzItd2FzaS9AdHlieXMvd2FzbS11dGlsIjogWyJAdHlieXMvd2FzbS11dGlsQDAuMTAuMiIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJ0c2xpYiI6ICJeMi40LjAiIH0sICJidW5kbGVkIjogdHJ1ZSB9LCAic2hhNTEyLVJvQnZKMlgwd3VLbFdGSWpyd2ZmR3cxSXFaSEtRcXpJY2hLYWFkWlpmbk5wc0FZcDJtTTBoMzZKdFBDak5EQUhHZ1llei8xNXVNQnBmR3djaGhpTWdnPT0iXSwKCiAgICAiQHRhaWx3aW5kY3NzL294aWRlLXdhc20zMi13YXNpL3RzbGliIjogWyJ0c2xpYkAyLjguMSIsICIiLCB7ICJidW5kbGVkIjogdHJ1ZSB9LCAic2hhNTEyLW9KRnU5NEhRYitLVmR1U1VRTDd3bnBtcW5mbUxzT0EvbkFoNmI2RUgwd0NFb0swL21QZVhVNmMzd0tEVjgzTWtPdUhQUkh0U1hLS1U5OUlCYXpTLzJ3PT0iXSwKCiAgICAiY2hhbGsvc3VwcG9ydHMtY29sb3IiOiBbInN1cHBvcnRzLWNvbG9yQDcuMi4wIiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgImhhcy1mbGFnIjogIl40LjAuMCIgfSB9LCAic2hhNTEyLXFwQ0F2Umw5c3R1T0h2ZUtzbjdIbmNKUnZ2NTAxcUlhY0t6UWxPLytMd3hjOSswcTJ3THl2NERmdnQ4MC9EUG4ycHFPQnNKZERpb2dYR1I5K092d1J3PT0iXSwKCiAgICAibmV4dC9wb3N0Y3NzIjogWyJwb3N0Y3NzQDguNC4zMSIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJuYW5vaWQiOiAiXjMuMy42IiwgInBpY29jb2xvcnMiOiAiXjEuMC4wIiwgInNvdXJjZS1tYXAtanMiOiAiXjEuMC4yIiB9IH0sICJzaGE1MTItUFMwOElib2lhOW10cy8yeWdWM2VMcFk1Z2huVWNmTFYvRVhUT1cxRTJxWXhKS0dHQlV0TmpONzZGWUhuTXMzNlJtQVJuNDFiQzBBWm1uK3JSME9WcFE9PSJdLAoKICAgICJ2aXRlL2ZzZXZlbnRzIjogWyJmc2V2ZW50c0AyLjMuMyIsICIiLCB7ICJvcyI6ICJkYXJ3aW4iIH0sICJzaGE1MTItNXhvRGZYK2ZMN2ZhQVRuYWdtV1BwYkZ0d2gvUjc3V21NTXFxSEdTNjVDM3Z2QjBZSHJnRitCMVltWjM0NDF0TWo1bjYzazAyMTJYTm9Kd3psaGZmUXc9PSJdLAogIH0KfQo=" }, { "path": ".env.example", "kind": "text", "content": "# Your tenant public key. Get it from the desk's Developers tab.\n# `cpk_live_*` / `cpk_test_*` keys auto-route same-origin requests\n# to hosted Cimplify in both dev and prod. Anything else (`mock-dev`,\n# empty) falls back to the local `cimplify dev` mock.\nNEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev\n\n# Optional. Forces a fixed canonical URL for sitemap.xml, robots.txt,\n# OpenGraph, and llms.txt. Leave unset and the storefront derives the\n# canonical from the request `Host` header automatically.\n# NEXT_PUBLIC_SITE_URL=\n" }], "storefront-grocery": [{ "path": "vitest.config.ts", "kind": "text", "content": 'import { defineConfig } from "vitest/config";\n\nexport default defineConfig({\n test: {\n environment: "node",\n include: ["__tests__/**/*.test.ts"],\n globals: false,\n },\n});\n' }, { "path": "__tests__/cart-flow.test.ts", "kind": "text", "content": 'import { createCartFlowSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateCartFlowSuite({ seed: brand.mock.seed, businessId: brand.mock.businessId });\n' }, { "path": "__tests__/brand.test.ts", "kind": "text", "content": 'import { createBrandSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateBrandSuite({ brand });\n' }, { "path": "__tests__/contract.test.ts", "kind": "text", "content": 'import { createContractSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateContractSuite({ seed: brand.mock.seed });\n' }, { "path": "AGENTS.md", "kind": "text", "content": '# AGENTS.md \u2014 Grocery storefront template\n\nIf you are an AI agent (Claude, Cursor, Aider, devin, \u2026) working on this storefront, **start here.**\n\n## TL;DR for rebranding\n\n1. **Edit `lib/brand.ts`** \u2014 every visible string lives here.\n2. **Edit `app/globals.css`** \u2014 `@theme { \u2026 }` for palette + radius.\n3. **Edit `.env.local`** \u2014 set `NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY` (the only required var).\n## Aesthetic\n\n- **Inter sans only** (no serif) \u2014 friendly, readable for dense product grids.\n- **Fresh green + cream** \u2014 vibrant green primary, warm cream background.\n- **Generous radius**: `0.875rem` \u2014 softer than retail.\n- **Bold, slightly heavy** headings.\n- Schema.org `@type` is `GroceryStore`.\n\n## Page surface\n\n```\napp/\n page.tsx Home \u2014 hero ("Free delivery over GH\u20B5150"),\n collection strips, category grid, newsletter\n shop/page.tsx Aisle (SDK <CataloguePage/>)\n search/page.tsx Search\n collections/[slug]/page.tsx Collection landing (e.g. weekly box, deals)\n categories/[slug]/page.tsx Category landing\n\n cart/page.tsx, checkout/page.tsx, orders/[id]/page.tsx\n\n account/page.tsx <CimplifyAccount /> (iframe)\n account/orders/page.tsx <CimplifyAccount section="orders" />\n account/addresses/page.tsx <CimplifyAccount section="addresses" />\n account/settings/page.tsx <CimplifyAccount section="settings" />\n login/page.tsx, signup/page.tsx redirects \u2192 /account\n\n contact/page.tsx, track-order/page.tsx\n\n about/page.tsx, faq/page.tsx\n shipping/page.tsx (delivery + slots), returns/page.tsx (quality guarantee),\n accessibility/page.tsx, terms/page.tsx, privacy/page.tsx\n\n sitemap-page/page.tsx, sitemap.ts, robots.ts, llms.txt/route.ts, opensearch.xml/route.ts\n error.tsx, not-found.tsx\n```\n\n## File \u2194 brand-field map\n\n| File | Reads from `brand` |\n|---|---|\n| `app/layout.tsx` | identity, contact, socials, GroceryStore JSON-LD |\n| `app/page.tsx` | `brand.hero` (free-delivery badge) |\n| `app/about/page.tsx` | `brand.about` |\n| `app/faq/page.tsx` | `brand.faq` (delivery slots, sourcing, subscription boxes) |\n| `app/shipping/page.tsx` | `brand.shipping` (delivery slots + free-over threshold) |\n| `app/returns/page.tsx` | `brand.returns` (quality guarantee) |\n| `app/accessibility/page.tsx` | `brand.accessibility` |\n| `app/terms/page.tsx`, `app/privacy/page.tsx` | `brand.terms`, `brand.privacy` |\n| `app/contact/page.tsx` | `brand.contactPage`, `brand.contact` |\n| `app/track-order/page.tsx` | `brand.trackOrder` |\n| `app/account/*/page.tsx` | `brand.account` (iframe owns UI) |\n| `app/llms.txt/route.ts` | `brand.llms`, contact, currency |\n| `components/header.tsx`, `footer.tsx` | `brand.header`, `brand.footer`, `brand.contact`, `brand.socials` |\n\n## Grocery-specific notes\n\n- **Subscription boxes** are modelled as products with `billing_plans` (renders the SubscriptionCard variant). The basket-box landing lives at `/categories/baskets`.\n- **Weight-based pricing**: SDK supports `quantity_pricing` tiers per product, but most grocery products are sold by unit/kg. Real weight-at-pack pricing happens on the backend pricing rule.\n- Mock seed: `--seed grocery` (FreshMart).\n\n## Known TODOs\n\n- Contact + newsletter fake submits.\n- Hero / category / product imagery is Unsplash placeholder \u2014 replace with merchant photography.\n\n## Customizing SDK components\n\nFor anything beyond `lib/brand.ts` + `app/globals.css`, lean on the SDK\'s prebuilt components rather than reinvent. **Especially for product customization** (variants, add-ons, bundles, composites) \u2014 the SDK already gets price math, axis matching, and cart payload contracts right. Default to ejecting and restyling:\n\n```bash\ncimplify add cart-drawer\ncimplify add add-on-selector\ncimplify add product-page\n```\n\nThen edit the local copy. **Don\'t change the cart payload shape** unless you\'re also touching the SDK mock + backend lens. Full ejection rules and the customizer contract are in the SDK-level [`AGENTS.md`](../../AGENTS.md) \u2192 "Don\'t reinvent product customization".\n\n## Quick start\n\n```bash\nbun install\nbun dev\n```\n\nOpen <http://localhost:3000>.\n' }, { "path": "postcss.config.mjs", "kind": "text", "content": 'const config = {\n plugins: {\n "@tailwindcss/postcss": {},\n },\n};\n\nexport default config;\n' }, { "path": "components/cart-pill.tsx", "kind": "text", "content": '"use client";\n\nimport { useCartDrawer } from "@cimplify/sdk/react";\nimport { useCartCount } from "@/lib/cart";\n\n/**\n * Cart pill \u2014 dynamic island. Reads the live cart count via the SDK and\n * opens the side cart drawer on click (instead of navigating to /cart).\n * Wrap in `<Suspense fallback={<CartPillSkeleton/>}>` so the cached\n * header chrome streams without blocking on the cart fetch.\n */\nexport function CartPill() {\n const { count } = useCartCount();\n const { open } = useCartDrawer();\n return (\n <button\n type="button"\n onClick={open}\n aria-label={`Open cart, ${count} ${count === 1 ? "item" : "items"}`}\n className="inline-flex items-center gap-1.5 px-4 py-2.5 sm:py-2 rounded-full bg-foreground text-background text-xs font-semibold tracking-wide transition-transform hover:scale-105 cursor-pointer"\n >\n Cart \xB7 {count}\n </button>\n );\n}\n\nexport function CartPillSkeleton() {\n return (\n <span\n aria-hidden\n className="inline-flex items-center gap-1.5 px-4 py-2.5 sm:py-2 rounded-full bg-foreground/80 text-background text-xs font-semibold tracking-wide"\n >\n Cart \xB7 \u2026\n </span>\n );\n}\n' }, { "path": "components/collection-strip.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport type { Collection, Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "./store-product-card";\n\ninterface CollectionStripProps {\n collection: Collection;\n products: Product[];\n collectionHref?: string;\n}\n\n/**\n * Horizontal strip of products under a collection title. Cards come from the\n * SDK so every variant (Food, Bundle, Composite, Service, \u2026) renders\n * correctly; clicks open the shared URL-driven product modal.\n */\nexport function CollectionStrip({ collection, products, collectionHref }: CollectionStripProps) {\n if (products.length === 0) return null;\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 pt-12">\n <header className="grid grid-cols-[1fr_auto] items-end gap-4 mb-5">\n <h2 className="font-serif text-[28px] font-semibold m-0">{collection.name}</h2>\n {collectionHref && (\n <Link\n href={collectionHref}\n className="text-[13px] font-semibold text-primary hover:underline"\n >\n See all \u2192\n </Link>\n )}\n {collection.description && (\n <p className="col-span-full m-0 mt-1 text-sm text-muted-foreground">\n {collection.description}\n </p>\n )}\n </header>\n <div className="grid grid-flow-col auto-cols-[minmax(220px,1fr)] gap-4 overflow-x-auto snap-x snap-mandatory pb-2">\n {products.slice(0, 8).map((p) => (\n <div key={p.id} className="snap-start">\n <StoreProductCard product={p} />\n </div>\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "components/hero.tsx", "kind": "text", "content": 'interface HeroProps {\n badge?: string;\n title: string;\n subtitle?: string;\n}\n\nexport function Hero({ badge, title, subtitle }: HeroProps) {\n return (\n <section className="px-6 sm:px-8 py-16 text-center bg-gradient-to-b from-accent to-background">\n {badge && (\n <span className="inline-block mb-4 px-3.5 py-1.5 rounded-full bg-card border border-accent text-accent-foreground text-[11px] font-semibold uppercase tracking-[0.12em]">\n {badge}\n </span>\n )}\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold m-0 -tracking-[0.02em]">\n {title}\n </h1>\n {subtitle && (\n <p className="mx-auto mt-2 max-w-xl text-base text-muted-foreground">\n {subtitle}\n </p>\n )}\n </section>\n );\n}\n' }, { "path": "components/account-iframe.tsx", "kind": "text", "content": '"use client";\n\nimport { CimplifyAccount } from "@cimplify/sdk/react";\n\n/**\n * Cimplify Account portal \u2014 iframe-mounted UI hosted by Cimplify Link.\n * Handles sign-in, sign-up, OTP, addresses, payment methods, sessions,\n * and order history. The iframe owns auth state; we just choose which\n * `section` to land on.\n */\nexport function AccountIframe({ section }: { section?: string }) {\n return <CimplifyAccount section={section} />;\n}\n' }, { "path": "components/policy-page.tsx", "kind": "text", "content": 'import type { BrandPolicySection } from "@/lib/brand";\n\ninterface PolicyShape {\n eyebrow: string;\n title: string;\n lastUpdated?: string;\n sections: BrandPolicySection[];\n}\n\n/**\n * Shared layout for shipping / returns / accessibility / terms / privacy.\n * Reads a `{ eyebrow, title, lastUpdated, sections[] }` block from brand.\n */\nexport function PolicyPage({ policy }: { policy: PolicyShape }) {\n return (\n <article className="max-w-3xl mx-auto px-6 sm:px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {policy.eyebrow}\n </p>\n <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-2 -tracking-[0.02em]">\n {policy.title}\n </h1>\n {policy.lastUpdated && (\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {policy.lastUpdated}\n </p>\n )}\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {policy.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="text-2xl font-semibold mt-0">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "components/header.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport { Suspense } from "react";\nimport { NavLink } from "./nav-link";\nimport { CartPill, CartPillSkeleton } from "./cart-pill";\nimport { MobileNav } from "./mobile-nav";\nimport { brand } from "@/lib/brand";\n\n/**\n * Server-rendered header chrome. Brand mark + nav layout streams from the\n * cache; the active-link styling and live cart count are dynamic islands\n * mounted in their own Suspense boundaries so the chrome never blocks.\n */\nexport function Header() {\n return (\n <header className="sticky top-0 z-30 flex items-center justify-between px-6 sm:px-8 py-4 border-b border-border bg-background/90 backdrop-blur-md">\n <Link href="/" className="flex items-baseline gap-2">\n <span className="font-serif text-[22px] font-semibold -tracking-[0.02em]">\n {brand.shortName}\n </span>\n <span className="hidden sm:inline text-[11px] font-medium uppercase tracking-[0.12em] text-muted-foreground">\n {brand.microTag}\n </span>\n </Link>\n <div className="flex items-center gap-3 sm:gap-6">\n <nav className="hidden sm:flex items-center gap-6">\n {brand.header.nav.map((link) => (\n <Suspense key={link.href} fallback={<NavLinkFallback>{link.label}</NavLinkFallback>}>\n <NavLink href={link.href}>{link.label}</NavLink>\n </Suspense>\n ))}\n </nav>\n <Suspense fallback={<CartPillSkeleton />}>\n <CartPill />\n </Suspense>\n <div className="sm:hidden">\n <MobileNav />\n </div>\n </div>\n </header>\n );\n}\n\nfunction NavLinkFallback({ children }: { children: React.ReactNode }) {\n return (\n <span className="text-[13px] font-medium tracking-wide text-muted-foreground">\n {children}\n </span>\n );\n}\n' }, { "path": "components/product-modal.tsx", "kind": "text", "content": '"use client";\n\nimport { useEffect } from "react";\nimport Image from "next/image";\nimport { useRouter, useSearchParams, usePathname } from "next/navigation";\nimport { ProductSheet, useProduct, useCart } from "@cimplify/sdk/react";\n\n/**\n * URL-driven product modal. Reads `?product=<slug>` and renders the SDK\'s\n * `<ProductSheet/>` \u2014 vertical layout with image-on-top, then header, then\n * the variant/add-on/composite/bundle customizer. Closing the modal clears\n * the search param. Deep-linkable and survives reloads.\n */\nexport function ProductModal() {\n const router = useRouter();\n const pathname = usePathname();\n const searchParams = useSearchParams();\n const slug = searchParams?.get("product") ?? null;\n\n const { product } = useProduct(slug ?? "", { enabled: Boolean(slug) });\n const { addItem } = useCart();\n\n useEffect(() => {\n if (!slug) return;\n const original = document.body.style.overflow;\n document.body.style.overflow = "hidden";\n return () => {\n document.body.style.overflow = original;\n };\n }, [slug]);\n\n useEffect(() => {\n if (!slug) return;\n const onKey = (e: KeyboardEvent) => {\n if (e.key === "Escape") close();\n };\n window.addEventListener("keydown", onKey);\n return () => window.removeEventListener("keydown", onKey);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [slug]);\n\n if (!slug) return null;\n\n function close() {\n const next = new URLSearchParams(searchParams?.toString() ?? "");\n next.delete("product");\n const qs = next.toString();\n router.replace(qs ? `${pathname}?${qs}` : pathname, { scroll: false });\n }\n\n return (\n <div\n role="dialog"\n aria-modal="true"\n onClick={close}\n className="fixed inset-0 z-[100] flex items-end sm:items-center justify-center sm:p-6 bg-foreground/50 backdrop-blur-sm"\n >\n <div\n onClick={(e) => e.stopPropagation()}\n className="relative w-full sm:max-w-lg max-h-[92vh] overflow-auto bg-card sm:rounded-3xl rounded-t-3xl shadow-2xl"\n >\n <button\n onClick={close}\n aria-label="Close product details"\n className="absolute top-4 right-4 z-10 w-9 h-9 rounded-full bg-card border border-border text-sm cursor-pointer transition-colors hover:bg-muted"\n >\n \u2715\n </button>\n {product ? (\n <ProductSheet\n product={product}\n onClose={close}\n onAddToCart={async (p, qty, options) => {\n await addItem(p, qty, options);\n close();\n }}\n renderImage={({ src, alt, className }) => (\n <Image\n src={src}\n alt={alt}\n width={1200}\n height={900}\n className={className}\n style={{ width: "100%", height: "auto", objectFit: "cover" }}\n priority\n />\n )}\n classNames={{\n root: "p-6 sm:p-8 gap-4",\n image: "rounded-2xl overflow-hidden -mx-6 sm:-mx-8 -mt-6 sm:-mt-8 mb-2",\n header: "flex items-baseline justify-between gap-4",\n name: "font-serif text-2xl font-semibold m-0",\n price: "text-lg font-semibold text-primary",\n description: "text-sm text-muted-foreground leading-relaxed",\n customizer: "pt-2",\n }}\n />\n ) : (\n <div className="p-8 text-center text-muted-foreground">Loading\u2026</div>\n )}\n </div>\n </div>\n );\n}\n' }, { "path": "components/mobile-nav.tsx", "kind": "text", "content": '"use client";\n\nimport { useEffect, useState } from "react";\nimport Link from "next/link";\nimport { brand } from "@/lib/brand";\n\n/**\n * Hamburger button + slide-in drawer for narrow viewports. Header hides\n * its inline nav links below `sm` and renders this in their place; the\n * cart pill stays in the header chrome.\n */\nexport function MobileNav() {\n const [open, setOpen] = useState(false);\n\n useEffect(() => {\n if (!open) return;\n const onKey = (event: KeyboardEvent) => {\n if (event.key === "Escape") setOpen(false);\n };\n document.addEventListener("keydown", onKey);\n const previousOverflow = document.body.style.overflow;\n document.body.style.overflow = "hidden";\n return () => {\n document.removeEventListener("keydown", onKey);\n document.body.style.overflow = previousOverflow;\n };\n }, [open]);\n\n return (\n <>\n <button\n type="button"\n onClick={() => setOpen(true)}\n aria-label="Open menu"\n aria-expanded={open}\n aria-controls="mobile-nav-drawer"\n className="grid place-items-center w-11 h-11 -mr-2 rounded-md text-foreground hover:bg-muted transition-colors"\n >\n <svg\n width="20"\n height="20"\n viewBox="0 0 24 24"\n fill="none"\n stroke="currentColor"\n strokeWidth="2"\n strokeLinecap="round"\n strokeLinejoin="round"\n aria-hidden="true"\n >\n <line x1="3" y1="6" x2="21" y2="6" />\n <line x1="3" y1="12" x2="21" y2="12" />\n <line x1="3" y1="18" x2="21" y2="18" />\n </svg>\n </button>\n\n {open ? (\n <div className="fixed inset-0 z-50 sm:hidden">\n <button\n type="button"\n onClick={() => setOpen(false)}\n aria-label="Close menu"\n className="absolute inset-0 bg-background/80 backdrop-blur-sm"\n />\n <nav\n id="mobile-nav-drawer"\n aria-label="Mobile navigation"\n className="absolute inset-y-0 right-0 w-[85%] max-w-sm flex flex-col bg-background border-l border-border shadow-2xl"\n >\n <div className="flex items-center justify-between px-6 py-4 border-b border-border">\n <span className="text-xs font-semibold uppercase tracking-[0.16em] text-muted-foreground">\n Menu\n </span>\n <button\n type="button"\n onClick={() => setOpen(false)}\n aria-label="Close menu"\n className="grid place-items-center w-11 h-11 -mr-2 rounded-md text-foreground hover:bg-muted transition-colors"\n >\n <svg\n width="20"\n height="20"\n viewBox="0 0 24 24"\n fill="none"\n stroke="currentColor"\n strokeWidth="2"\n strokeLinecap="round"\n strokeLinejoin="round"\n aria-hidden="true"\n >\n <line x1="18" y1="6" x2="6" y2="18" />\n <line x1="6" y1="6" x2="18" y2="18" />\n </svg>\n </button>\n </div>\n <ul className="flex flex-col gap-1 px-3 py-4">\n {brand.header.nav.map((link) => (\n <li key={link.href}>\n <Link\n href={link.href}\n onClick={() => setOpen(false)}\n className="block px-3 py-3 rounded-md text-base font-medium text-foreground hover:bg-muted transition-colors"\n >\n {link.label}\n </Link>\n </li>\n ))}\n </ul>\n </nav>\n </div>\n ) : null}\n </>\n );\n}\n' }, { "path": "components/providers.tsx", "kind": "text", "content": '"use client";\n\nimport { useMemo, type ReactNode } from "react";\nimport { createCimplifyClient } from "@cimplify/sdk";\nimport { CimplifyProvider, CartDrawerProvider } from "@cimplify/sdk/react";\n\n// Same-origin client \u2014 every request goes through the Next.js rewrite in\n// next.config.ts, so no CORS preflight ever hits the browser.\nexport function Providers({ children }: { children: ReactNode }) {\n const client = useMemo(() => {\n const baseUrl =\n typeof window !== "undefined" ? window.location.origin : "http://127.0.0.1:8787";\n const publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY ?? "mock-dev";\n return createCimplifyClient({\n baseUrl,\n publicKey,\n suppressPublicKeyWarning: true,\n });\n }, []);\n\n return (\n <CimplifyProvider client={client}>\n <CartDrawerProvider>{children}</CartDrawerProvider>\n </CimplifyProvider>\n );\n}\n' }, { "path": "components/footer.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport { brand } from "@/lib/brand";\n\nconst ICONS: Record<string, React.ReactNode> = {\n instagram: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" aria-hidden className="w-5 h-5">\n <rect x="3" y="3" width="18" height="18" rx="5" />\n <circle cx="12" cy="12" r="4" />\n <circle cx="17.5" cy="6.5" r="0.75" fill="currentColor" />\n </svg>\n ),\n x: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M18 2h3l-7.5 8.6L22 22h-6.6l-5-6.5L4 22H1l8-9.2L1.4 2H8l4.6 6 5.4-6z" />\n </svg>\n ),\n tiktok: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M16 3v3.5a4.5 4.5 0 0 0 4.5 4.5V14a7.5 7.5 0 0 1-4.5-1.5V16a5 5 0 1 1-5-5v3a2 2 0 1 0 2 2V3z" />\n </svg>\n ),\n facebook: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M14 9V7a1 1 0 0 1 1-1h2V3h-3a4 4 0 0 0-4 4v2H8v3h2v9h3v-9h2.5l.5-3H13z" />\n </svg>\n ),\n youtube: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M22.5 6.5a2.6 2.6 0 0 0-1.8-1.8C19 4.2 12 4.2 12 4.2s-7 0-8.7.5A2.6 2.6 0 0 0 1.5 6.5C1 8.2 1 12 1 12s0 3.8.5 5.5a2.6 2.6 0 0 0 1.8 1.8C5 19.8 12 19.8 12 19.8s7 0 8.7-.5a2.6 2.6 0 0 0 1.8-1.8c.5-1.7.5-5.5.5-5.5s0-3.8-.5-5.5zM10 15.5v-7l6 3.5-6 3.5z" />\n </svg>\n ),\n linkedin: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M4 4h4v16H4zM6 2.5a2 2 0 1 0 0 4 2 2 0 0 0 0-4zM10 8h4v2.5h.1c.6-1.1 2-2.5 4-2.5 4 0 4.9 2.6 4.9 6V20h-4v-5c0-1.5-.5-3-2.3-3-1.7 0-2.5 1.3-2.5 3v5h-4z" />\n </svg>\n ),\n whatsapp: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M12 2a10 10 0 0 0-8.6 15l-1.4 5 5.2-1.4A10 10 0 1 0 12 2zm5 14.2c-.2.6-1.2 1.2-1.7 1.2-.5.1-1.1.1-1.7-.1-.4-.1-.9-.3-1.5-.6a8.4 8.4 0 0 1-3.7-3.4c-.7-1-1-1.8-1-2.5 0-.7.4-1.1.6-1.3.2-.2.4-.2.5-.2h.4c.1 0 .3 0 .4.3l.6 1.4c.1.2 0 .3 0 .4l-.3.4-.3.3c-.1.1-.2.2-.1.4.2.4.7 1.1 1.4 1.8.9.8 1.7 1.1 1.9 1.2.2.1.3.1.5-.1l.6-.7c.2-.2.3-.2.5-.1l1.4.7c.2.1.3.2.4.3.1.2.1.6 0 1z" />\n </svg>\n ),\n};\n\nconst FALLBACK_ICON = (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" aria-hidden className="w-5 h-5">\n <circle cx="12" cy="12" r="9" />\n </svg>\n);\n\nexport async function Footer() {\n "use cache";\n const year = new Date().getFullYear();\n return (\n <footer className="mt-12 px-6 sm:px-8 py-10 text-xs text-muted-foreground border-t border-border bg-card">\n <div className="max-w-7xl mx-auto">\n <div className="grid gap-10 md:grid-cols-[1.4fr_repeat(4,1fr)]">\n <div>\n <p className="font-serif text-2xl text-foreground m-0 mb-2">{brand.name}</p>\n <p className="leading-relaxed mb-4 max-w-sm">{brand.footer.blurb}</p>\n <address className="not-italic space-y-1">\n <p className="m-0">{brand.contact.address}</p>\n <p className="m-0">\n <a\n href={`tel:${brand.contact.phoneTel}`}\n className="hover:text-foreground transition-colors"\n >\n {brand.contact.phone}\n </a>\n </p>\n <p className="m-0">\n <a\n href={`mailto:${brand.contact.email}`}\n className="hover:text-foreground transition-colors"\n >\n {brand.contact.email}\n </a>\n </p>\n <p className="m-0">{brand.contact.hours}</p>\n </address>\n <div className="flex items-center gap-3 mt-5">\n {brand.socials.map((s) => (\n <a\n key={s.label}\n href={s.href}\n aria-label={s.label}\n target="_blank"\n rel="noopener noreferrer"\n className="inline-flex items-center justify-center w-9 h-9 rounded-full border border-border text-muted-foreground hover:text-foreground hover:border-foreground transition-colors"\n >\n {(s.icon && ICONS[s.icon]) ?? FALLBACK_ICON}\n </a>\n ))}\n </div>\n </div>\n {brand.footer.sitemap.map((section) => (\n <nav key={section.title} aria-labelledby={`footer-${section.title}`}>\n <p\n id={`footer-${section.title}`}\n className="font-semibold text-foreground mb-3 text-[13px] uppercase tracking-[0.08em]"\n >\n {section.title}\n </p>\n <ul className="space-y-2 m-0 p-0 list-none">\n {section.links.map((link) => (\n <li key={link.label}>\n <Link\n href={link.href}\n className="hover:text-foreground transition-colors"\n >\n {link.label}\n </Link>\n </li>\n ))}\n </ul>\n </nav>\n ))}\n </div>\n <div className="mt-12 pt-6 border-t border-border flex flex-col sm:flex-row items-center justify-between gap-3">\n <p className="m-0">\xA9 {year} {brand.name}. All rights reserved.</p>\n {brand.footer.poweredBy && (\n <p className="m-0 inline-flex items-center gap-1.5">\n <span className="text-muted-foreground/80">Powered by</span>\n <a\n href={brand.footer.poweredBy.href}\n target="_blank"\n rel="noopener noreferrer"\n aria-label={brand.footer.poweredBy.label}\n className="inline-flex items-center gap-1 font-serif text-foreground hover:text-primary transition-colors"\n >\n <span className="font-semibold tracking-tight">{brand.footer.poweredBy.label}</span>\n <svg\n viewBox="0 0 12 12"\n aria-hidden\n className="w-3 h-3 opacity-70"\n fill="none"\n stroke="currentColor"\n strokeWidth="1.5"\n >\n <path d="M3 9L9 3M9 3H4M9 3v5" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n </a>\n </p>\n )}\n </div>\n </div>\n </footer>\n );\n}\n' }, { "path": "components/store-product-card.tsx", "kind": "text", "content": '"use client";\n\nimport Link from "next/link";\nimport {\n CardVariant,\n FoodProductCard,\n RetailProductCard,\n WholesaleProductCard,\n DigitalProductCard,\n BundleProductCard,\n CompositeProductCard,\n StandardServiceCard,\n CompactServiceCard,\n ScheduleServiceCard,\n RentalServiceCard,\n AccommodationCard,\n LeaseServiceCard,\n SubscriptionCard,\n} from "@cimplify/sdk/react";\nimport type { CardLayoutProps } from "@cimplify/sdk/react";\nimport type { Product } from "@cimplify/sdk";\nimport { PRODUCT_TYPE, RENDER_HINT, DURATION_UNIT } from "@cimplify/sdk";\n\nconst RENTAL_UNITS = new Set<string>([DURATION_UNIT.Days, DURATION_UNIT.Hours]);\nconst LEASE_UNITS = new Set<string>([DURATION_UNIT.Weeks, DURATION_UNIT.Months, DURATION_UNIT.Years]);\n\nconst VARIANT_CARDS: Record<CardVariant, React.ComponentType<CardLayoutProps>> = {\n [CardVariant.Food]: FoodProductCard,\n [CardVariant.Retail]: RetailProductCard,\n [CardVariant.Wholesale]: WholesaleProductCard,\n [CardVariant.Digital]: DigitalProductCard,\n [CardVariant.Bundle]: BundleProductCard,\n [CardVariant.Composite]: CompositeProductCard,\n [CardVariant.Standard]: StandardServiceCard,\n [CardVariant.Compact]: CompactServiceCard,\n [CardVariant.Schedule]: ScheduleServiceCard,\n [CardVariant.Rental]: RentalServiceCard,\n [CardVariant.Accommodation]: AccommodationCard,\n [CardVariant.Lease]: LeaseServiceCard,\n [CardVariant.Subscription]: SubscriptionCard,\n};\n\nfunction resolveVariant(p: Product): CardVariant {\n if (p.type === PRODUCT_TYPE.Bundle) return CardVariant.Bundle;\n if (p.type === PRODUCT_TYPE.Composite) return CardVariant.Composite;\n if (p.quantity_pricing && p.quantity_pricing.length > 1) return CardVariant.Wholesale;\n if (p.type === PRODUCT_TYPE.Digital) return CardVariant.Digital;\n if (p.type === PRODUCT_TYPE.Service) {\n if (p.duration_unit && RENTAL_UNITS.has(p.duration_unit)) return CardVariant.Rental;\n if (p.duration_unit === DURATION_UNIT.Nights) return CardVariant.Accommodation;\n if (p.duration_unit && LEASE_UNITS.has(p.duration_unit)) return CardVariant.Lease;\n if (p.billing_plans && p.billing_plans.length > 0 && !p.duration_minutes) return CardVariant.Subscription;\n return CardVariant.Standard;\n }\n if (p.render_hint === RENDER_HINT.Food) return CardVariant.Food;\n return CardVariant.Retail;\n}\n\ninterface Props {\n product: Product;\n /** Override the auto-detected card variant. */\n variant?: CardVariant;\n}\n\n/**\n * Variant-aware product card that opens the URL-driven product modal on\n * click. Uses `next/link` with `?product=<slug>` so the link is statically\n * pre-renderable \u2014 no `useSearchParams` dependency means cards don\'t\n * become dynamic islands. The modal (which already reads `useSearchParams`\n * inside a Suspense boundary in the root layout) handles the rest.\n */\nexport function StoreProductCard({ product, variant }: Props) {\n const slug = product.slug || product.id;\n const Card = VARIANT_CARDS[variant ?? resolveVariant(product)];\n const href = `?product=${encodeURIComponent(slug)}`;\n\n return (\n <Card\n product={product}\n renderLink={({ className, children }) => (\n <Link href={href} scroll={false} prefetch={false} className={className}>\n {children}\n </Link>\n )}\n />\n );\n}\n' }, { "path": "components/json-ld.tsx", "kind": "text", "content": 'import { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\n\n/**\n * Streams the organization JSON-LD script tag in async so it doesn\'t block\n * the rest of the layout shell. `getSiteUrl` reads request headers, and\n * any await on dynamic data inside `RootLayout` makes Next 16 mark the\n * whole route as blocking.\n */\nexport async function OrganizationJsonLd(): Promise<React.ReactElement> {\n const siteUrl = await getSiteUrl();\n const ld = {\n "@context": "https://schema.org",\n "@type": brand.schemaType,\n name: brand.name,\n url: siteUrl,\n description: brand.description,\n email: brand.contact.email,\n telephone: brand.contact.phoneTel,\n address: {\n "@type": "PostalAddress",\n streetAddress: brand.contact.streetAddress,\n addressLocality: brand.contact.city,\n addressCountry: brand.contact.countryCode,\n },\n sameAs: brand.socials.map((s) => s.href),\n };\n return (\n <script\n type="application/ld+json"\n dangerouslySetInnerHTML={{ __html: JSON.stringify(ld) }}\n />\n );\n}\n' }, { "path": "components/nav-link.tsx", "kind": "text", "content": '"use client";\n\nimport Link from "next/link";\nimport { usePathname } from "next/navigation";\n\nexport function NavLink({ href, children }: { href: string; children: React.ReactNode }) {\n const pathname = usePathname();\n const active = pathname === href;\n return (\n <Link\n href={href}\n className={[\n "text-[13px] font-medium tracking-wide transition-colors",\n active ? "text-primary" : "text-muted-foreground hover:text-foreground",\n ].join(" ")}\n >\n {children}\n </Link>\n );\n}\n' }, { "path": "components/category-grid.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CategoryGrid as SdkCategoryGrid } from "@cimplify/sdk/react";\nimport type { Category } from "@cimplify/sdk";\n\n/**\n * Homepage category tiles \u2014 defers to the SDK\'s <CategoryGrid/> for layout\n * and accessibility, and routes selections to /categories/:slug.\n */\nexport function CategoryGrid({ categories }: { categories?: Category[] }) {\n const router = useRouter();\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 pt-14">\n <h2 className="font-serif text-[28px] font-semibold m-0 mb-5">Browse the menu</h2>\n <SdkCategoryGrid\n categories={categories}\n onSelect={(c) => router.push(`/categories/${c.slug}`)}\n classNames={{\n item: "flex flex-col gap-1 p-6 bg-card border border-border rounded-2xl cursor-pointer text-left transition-all hover:border-primary hover:-translate-y-0.5",\n name: "font-serif text-lg font-semibold text-foreground",\n description: "text-xs text-muted-foreground",\n count: "text-xs text-muted-foreground mt-1",\n }}\n />\n </section>\n );\n}\n' }, { "path": "components/cart-drawer.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CartDrawer as SdkCartDrawer } from "@cimplify/sdk/react";\n\n/**\n * Side-drawer cart. Auto-opens when an item is added (via\n * `<CartDrawerProvider>` in `providers.tsx`). The header\'s cart pill\n * also calls `useCartDrawer().open()` to reveal it on click.\n */\nexport function CartDrawer() {\n const router = useRouter();\n return <SdkCartDrawer onCheckout={() => router.push("/checkout")} />;\n}\n' }, { "path": "next.config.ts", "kind": "text", "content": 'import type { NextConfig } from "next";\n\n// Same-origin proxy target for the storefront API. The public key\n// decides: a real `cpk_live_\u2026` / `cpk_test_\u2026` only resolves against\n// hosted Cimplify, so browser cart writes go to the same place the\n// server catalogue reads come from. Anything else (`mock-dev`, empty)\n// falls back to the local `cimplify dev` mock in dev.\nconst publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY?.trim() ?? "";\nconst keyTargetsHostedCimplify =\n publicKey.startsWith("cpk_live_") || publicKey.startsWith("cpk_test_");\nconst STOREFRONT_URL =\n process.env.NODE_ENV === "production" || keyTargetsHostedCimplify\n ? "https://storefronts.cimplify.io"\n : "http://127.0.0.1:8787";\n\nif (STOREFRONT_URL === "http://127.0.0.1:8787") {\n console.warn(\n "[cimplify] next.config resolved the local storefront API URL; production deploys must run with NODE_ENV=production.",\n );\n}\n\nconst nextConfig: NextConfig = {\n // Enable Next 16\'s `cacheComponents` mode so we can use `\'use cache\'` +\n // `cacheTag` / `cacheLife` for SSR caching. Cached chrome streams first,\n // dynamic Suspense boundaries fill in when ready.\n cacheComponents: true,\n async redirects() {\n // Config-level so cacheComponents needn\'t prerender a redirect()-only page.\n return [\n { source: "/login", destination: "/account", permanent: false },\n { source: "/signup", destination: "/account", permanent: false },\n ];\n },\n async rewrites() {\n return [\n { source: "/api/v1/:path*", destination: `${STOREFRONT_URL}/api/v1/:path*` },\n { source: "/img/:path*", destination: `${STOREFRONT_URL}/img/:path*` },\n { source: "/elements/:path*", destination: `${STOREFRONT_URL}/elements/:path*` },\n { source: "/_mock/:path*", destination: `${STOREFRONT_URL}/_mock/:path*` },\n ];\n },\n images: {\n loader: "custom",\n loaderFile: "./lib/cimplify-loader.ts",\n remotePatterns: [\n { protocol: "http", hostname: "127.0.0.1", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "3000", pathname: "/img/**" },\n { protocol: "https", hostname: "loremflickr.com", pathname: "/**" },\n { protocol: "https", hostname: "images.unsplash.com", pathname: "/**" },\n { protocol: "https", hostname: "static-tmp.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "storefrontassetscdn.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "cdn.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "res.cloudinary.com", pathname: "/cimplify/**" },\n ],\n },\n};\n\nexport default nextConfig;\n' }, { "path": ".claude/skills/cimplify-storefront/SKILL.md", "kind": "text", "content": "---\nname: cimplify-storefront\ndescription: Build, customize, rebrand, or deploy a Cimplify-scaffolded storefront. Triggers when the user asks to create a storefront, rebrand a Cimplify template, change the palette, add a page, deploy to Cimplify, or works in a project containing `lib/brand.ts` and `next.config.ts` with `cacheComponents`.\n---\n\n# Cimplify Storefront skill\n\nYou're working on a project scaffolded from `cimplify init`. The architecture is opinionated and the rebrand surface is intentionally small. Read `AGENTS.md` at the project root for the file \u2194 brand-field map for *this template's* industry; this skill gives you the playbook that's the same across all six.\n\n## The contract \u2014 never break\n\n1. **`lib/brand.ts` is the only place for content edits.** Every visible string reads from this file. If a string isn't in `brand`, *add a field* to the `Brand` interface \u2014 don't hardcode it in a page or component.\n2. **`app/globals.css` `@theme { \u2026 }`** holds palette + radius + font references. To re-skin the entire site, edit only this block.\n3. **Server Components are cached** via `'use cache'` + `cacheTag(tags.X())` + `cacheLife(\"hours\")`. Don't downgrade them to `\"use client\"` to avoid an issue \u2014 fix the issue.\n4. **Client islands** (anything reading `useSearchParams`, `usePathname`, `useRouter`, `useState`) live behind `<Suspense>`.\n5. **`cacheComponents: true`** in `next.config.ts` is non-negotiable. Don't turn it off.\n6. **`bun run test:run` (vitest)** is the canonical test runner. `bun test` will show false failures because Bun's `vi` shim is incomplete.\n7. **Cart, checkout, orders stay client.** They're session-bound. Don't try to SSR them.\n\n## The brand-field schema (in `lib/brand.ts`)\n\n```\nidentity: name, shortName, microTag, description, schemaType, currency, locale\ncontact: address, streetAddress, city, countryCode, phone, phoneTel, email, privacyEmail, hours\nsocials: [{ label, href, icon }] // icon \u2208 instagram|x|tiktok|facebook|youtube|linkedin|whatsapp\nheader: { nav: [{ label, href }] }\nhero: { badge, title, subtitle, primaryCtaLabel, secondaryCtaLabel?, secondaryCtaHref? }\noptional: trustItems[]?, brandStrip?, promo?, tradeIn? // render conditionally\nnewsletter: { eyebrow, title, body, placeholder, submitLabel, successLabel }\nabout: { eyebrow, title, paragraphs[], sections[{ heading, body }] }\nfaq: { eyebrow, title, sections[{ title, items[{ q, a }] }], contactPrompt, contactEmail }\nterms,\nprivacy,\nshipping,\nreturns,\naccessibility: { eyebrow, title, lastUpdated, sections[{ heading, body | { intro, bullets[] } }] }\naccount: eyebrows + titles for /account, /login, /signup\ncontactPage: { eyebrow, title, body, reasons[], directLines[{ label, value, href }] }\ntrackOrder: { eyebrow, title, body }\nfooter: { blurb, sitemap[{ title, links[] }], poweredBy? }\nllms: { summary } // opens /llms.txt\nmock: { seed, businessId }\n```\n\n## Playbook \u2014 common tasks\n\n### Rebrand a storefront for a new merchant\n\n1. Edit `lib/brand.ts`. Replace every field with the merchant's content. Use the brief / context the user gave you.\n2. Edit `app/globals.css` `@theme { \u2026 }`. Change `--color-primary`, `--color-background`, `--color-foreground`, optionally `--radius`. Use OKLCH; if the brand only gave hex, convert.\n3. (Optional) Swap fonts in `app/layout.tsx` \u2014 `next/font/google` import + the variable wired into the `<html>` className.\n4. Set `.env.local`: `NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY` (optional: `NEXT_PUBLIC_SITE_URL`).\n\nDon't touch any other file. If the rebrand needs content not in the schema, add the field to `Brand` first, populate it, then read it from the page.\n\n### Add a new section / component\n\n1. Build it as a Server Component in `components/` (or a client island in `*-client.tsx` if interactive).\n2. Read merchant copy from `brand`. Add new fields to the `Brand` interface if needed.\n3. Wrap interactive bits in `<Suspense fallback={\u2026}>` so the cached chrome streams.\n4. Compose into the page.\n\n### Wire a Server Action that mutates data\n\n```ts\n\"use server\";\nimport { getServerClient, revalidateProducts } from \"@cimplify/sdk/server\";\n\nexport async function createProduct(input: ProductInput) {\n await getServerClient().catalogue.createProduct(input);\n revalidateProducts();\n}\n```\n\nAfter every mutation, call the matching `revalidate*` helper from `@cimplify/sdk/server`: `revalidateProducts`, `revalidateProduct(id)`, `revalidateCategories`, `revalidateCategory(id)`, `revalidateCollections`, `revalidateCollection(id)`, `revalidateBusiness`.\n\n### Add a Server Component data fetch\n\n```ts\nimport { cacheTag, cacheLife } from \"next/cache\";\nimport { getServerClient, tags } from \"@cimplify/sdk/server\";\n\nasync function getX() {\n \"use cache\";\n cacheTag(tags.products());\n cacheLife(\"hours\");\n const r = await getServerClient().catalogue.getProducts({ limit: 24 });\n if (!r.ok) throw new Error(r.error.message);\n return r.value.items;\n}\n```\n\n### Eject an SDK component for deeper customization\n\nTry `classNames`, `renderImage`, `renderLink`, slot props first. If those run out:\n\n```bash\ncimplify list\ncimplify add product-card --dir src/components/cimplify\n```\n\nOnce ejected, the file is yours. Hooks/types still come from `@cimplify/sdk`.\n\n### Deploy\n\n```bash\ncimplify login\ncimplify projects create my-store\ncimplify link <project-id>\ncimplify env push\ncimplify deploy --prod\ncimplify logs --follow\ncimplify domains add my-store.com\n```\n\n## Pitfalls \u2014 explicit \u274C list\n\n- \u274C Hardcoding any visible string in a page or component. Always `brand.X`.\n- \u274C Disabling `cacheComponents: true` to silence a warning. Fix the warning by wrapping the dynamic call in `<Suspense>`.\n- \u274C Using `unstable_cache` \u2014 Next 16's canonical primitive is `'use cache'` + `cacheTag` + `cacheLife`.\n- \u274C Bypassing `getServerClient()` and calling `createCimplifyClient` directly in a Server Component \u2014 loses per-request memoization.\n- \u274C Mutating data without calling the matching `revalidate*` helper.\n- \u274C Adding an `app/error.tsx` handler that calls `reset()` without logging \u2014 silently swallowed errors hide bugs.\n- \u274C Running `bun test` and reporting failures. Use `bun run test:run` (vitest).\n- \u274C Editing the per-template `AGENTS.md` to remove notes about TODOs (contact form, newsletter fake submits) \u2014 they're real.\n\n## Where things live\n\n| Need | Look here |\n|---|---|\n| Which page reads which `brand.X` field | `AGENTS.md` at project root |\n| Architectural rules | `AGENTS.md` at project root + this skill |\n| Running locally | `bun dev` (boots mock + Next together) |\n| Switching mock seed | edit `dev:mock` in `package.json` |\n| Full SDK reference | `docs/sdk/storefronts.md` in the Cimplify repo |\n\n## What to do when the user asks something out-of-scope\n\nIf the user asks for something the template doesn't support out of the box:\n\n1. **Check first** \u2014 is there an SDK component or hook that does it? `@cimplify/sdk/react` has 50+ components, 30+ hooks. Search `node_modules/@cimplify/sdk/dist/react.d.mts` if needed.\n2. **Compose from existing parts** before authoring new ones.\n3. **If genuinely missing** \u2014 build the new component, hoist its strings into `brand.ts`, document in `AGENTS.md`.\n\nDon't introduce competing patterns (a new caching strategy, a new auth flow, a new theming system). The template's opinions are intentional.\n" }, { "path": ".cursor/rules/cimplify-storefront.mdc", "kind": "binary", "content": "LS0tCmRlc2NyaXB0aW9uOiBDaW1wbGlmeSBzdG9yZWZyb250IOKAlCBhcmNoaXRlY3R1cmFsIHJ1bGVzIGFuZCByZWJyYW5kIGNvbnRyYWN0IGZvciBhIHByb2plY3Qgc2NhZmZvbGRlZCBmcm9tIGBjaW1wbGlmeSBpbml0YC4gVHJpZ2dlcnMgd2hlbiBmaWxlcyBsaWtlIGxpYi9icmFuZC50cywgYXBwL2dsb2JhbHMuY3NzLCBjb21wb25lbnRzL2hlYWRlci50c3gsIG9yIC5jaW1wbGlmeS9wcm9qZWN0Lmpzb24gYXJlIGluIHRoZSB3b3Jrc3BhY2UuCmdsb2JzOiBbImxpYi9icmFuZC50cyIsICJhcHAvKioiLCAiY29tcG9uZW50cy8qKiIsICIuZW52LmxvY2FsIiwgIm5leHQuY29uZmlnLnRzIl0KYWx3YXlzQXBwbHk6IHRydWUKLS0tCgojIENpbXBsaWZ5IHN0b3JlZnJvbnQKClRoaXMgcHJvamVjdCB3YXMgc2NhZmZvbGRlZCBmcm9tIGEgQ2ltcGxpZnkgdGVtcGxhdGUuIFJlYWQgYEFHRU5UUy5tZGAgYXQgdGhlIHByb2plY3Qgcm9vdCBmb3IgdGhlIGZpbGUg4oaUIGBicmFuZC5YYCBmaWVsZCBtYXAgYW5kIGAuY2xhdWRlL3NraWxscy9jaW1wbGlmeS1zdG9yZWZyb250L1NLSUxMLm1kYCBmb3IgdGhlIGZ1bGwgcGxheWJvb2suCgojIyBUaGUgY29udHJhY3QKCjEuICoqYGxpYi9icmFuZC50c2AqKiDigJQgZXZlcnkgdmlzaWJsZSBzdHJpbmcuIElmIHNvbWV0aGluZyBpc24ndCB0aGVyZSwgYWRkIGEgZmllbGQgdG8gdGhlIGBCcmFuZGAgaW50ZXJmYWNlOyBkb24ndCBoYXJkY29kZS4KMi4gKipgYXBwL2dsb2JhbHMuY3NzYCBgQHRoZW1lYCBibG9jayoqIOKAlCBwYWxldHRlLCByYWRpdXMsIGZvbnQgcmVmZXJlbmNlcy4KMy4gKipTZXJ2ZXIgQ29tcG9uZW50cyBhcmUgY2FjaGVkKiogdmlhIGAndXNlIGNhY2hlJ2AgKyBgY2FjaGVUYWcodGFncy5YKCkpYCArIGBjYWNoZUxpZmUoImhvdXJzIilgLiBEb24ndCBkb3duZ3JhZGUgdG8gY2xpZW50IHRvIHNpbGVuY2UgYSB3YXJuaW5nLgo0LiAqKkNsaWVudCBpc2xhbmRzKiogYmVoaW5kIGA8U3VzcGVuc2U+YC4KNS4gKipgY2FjaGVDb21wb25lbnRzOiB0cnVlYCoqIGluIGBuZXh0LmNvbmZpZy50c2AgaXMgbm9uLW5lZ290aWFibGUuCjYuIFVzZSAqKmBidW4gcnVuIHRlc3Q6cnVuYCoqICh2aXRlc3QpLCBub3QgYGJ1biB0ZXN0YC4KCiMjIERvbid0CgotIEhhcmRjb2RlIHZpc2libGUgc3RyaW5ncyBpbiBwYWdlcy9jb21wb25lbnRzLgotIFVzZSBgdW5zdGFibGVfY2FjaGVgIChOZXh0IDE2IHVzZXMgYCd1c2UgY2FjaGUnYCkuCi0gQnlwYXNzIGBnZXRTZXJ2ZXJDbGllbnQoKWAgKGxvc2VzIHBlci1yZXF1ZXN0IG1lbW9pemF0aW9uKS4KLSBNdXRhdGUgd2l0aG91dCBjYWxsaW5nIHRoZSBtYXRjaGluZyBgcmV2YWxpZGF0ZSpgIGhlbHBlciBmcm9tIGBAY2ltcGxpZnkvc2RrL3NlcnZlcmAuCg==" }, { "path": ".gitignore", "kind": "text", "content": "node_modules\n.next\nout\n.env\n.env.local\n.env*.local\n.DS_Store\n*.tsbuildinfo\nnext-env.d.ts\n" }, { "path": "app/about/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `About \u2014 ${brand.name}`,\n description: brand.description,\n};\n\nexport default function AboutPage() {\n const a = brand.about;\n const titleParts = a.title.split("\\n");\n return (\n <article className="max-w-3xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {a.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-6 -tracking-[0.02em]">\n {titleParts.map((line, i) => (\n <span key={i}>\n {line}\n {i < titleParts.length - 1 && <br />}\n </span>\n ))}\n </h1>\n <div className="prose prose-lg max-w-none space-y-5 text-foreground/90 leading-relaxed">\n {a.paragraphs.map((p, i) => (\n <p key={i}>{p}</p>\n ))}\n {a.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="font-serif text-3xl font-semibold mt-10 mb-3">{s.heading}</h2>\n <p>{s.body}</p>\n </div>\n ))}\n </div>\n </article>\n );\n}\n' }, { "path": "app/account/orders/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Orders \u2014 ${brand.name}`,\n};\n\nexport default function OrdersPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Your orders\n </h1>\n <AccountIframe section="orders" />\n </article>\n );\n}\n' }, { "path": "app/account/settings/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Settings \u2014 ${brand.name}`,\n};\n\nexport default function SettingsPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Settings\n </h1>\n <AccountIframe section="settings" />\n </article>\n );\n}\n' }, { "path": "app/account/addresses/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Addresses \u2014 ${brand.name}`,\n};\n\nexport default function AddressesPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Your addresses\n </h1>\n <AccountIframe section="addresses" />\n </article>\n );\n}\n' }, { "path": "app/account/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Account \u2014 ${brand.name}`,\n description: brand.account.signupSubtitle,\n};\n\nexport default function AccountPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {brand.account.accountEyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n {brand.account.accountTitle}\n </h1>\n <AccountIframe />\n </article>\n );\n}\n' }, { "path": "app/.well-known/ucp/route.ts", "kind": "text", "content": 'import { NextResponse } from "next/server";\n\n/**\n * UCP (Universal Commerce Protocol) manifest discovery endpoint.\n *\n * Agents \u2014 Claude, ChatGPT, Gemini, MCP clients \u2014 probe\n * `https://<your-domain>/.well-known/ucp` to learn what commerce\n * capabilities your storefront supports. We resolve the business\'s\n * UCP handle from the public key (via the storefront API), then forward\n * to Cimplify for the canonical manifest. Edge-cached for an hour\n * because capabilities change rarely.\n */\nconst UCP_API_BASE = "https://api.cimplify.io";\nconst STOREFRONT_API_BASE =\n process.env.NODE_ENV === "production"\n ? "https://storefronts.cimplify.io"\n : "http://127.0.0.1:8787";\n\nexport async function GET() {\n const publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY;\n if (!publicKey) {\n return NextResponse.json(\n {\n error: "NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY not set",\n remediation:\n "Set NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY in .env.local (and your deployment env).",\n },\n { status: 500 },\n );\n }\n\n try {\n // Step 1 \u2014 resolve the business handle from the public key.\n const businessResp = await fetch(`${STOREFRONT_API_BASE}/api/v1/business`, {\n headers: { "X-API-Key": publicKey, "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n });\n\n if (!businessResp.ok) {\n return NextResponse.json(\n { error: `Failed to resolve business: ${businessResp.status}` },\n { status: businessResp.status },\n );\n }\n\n const businessJson = await businessResp.json();\n const handle: string | undefined = businessJson?.data?.handle;\n if (!handle) {\n return NextResponse.json(\n { error: "Business has no handle configured" },\n { status: 500 },\n );\n }\n\n // Step 2 \u2014 fetch the canonical UCP manifest.\n const manifestResp = await fetch(\n `${UCP_API_BASE}/ucp/v1/${handle}/manifest`,\n {\n headers: { "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n },\n );\n\n if (!manifestResp.ok) {\n return NextResponse.json(\n { error: `Upstream UCP manifest fetch failed: ${manifestResp.status}` },\n { status: manifestResp.status },\n );\n }\n\n const manifest = await manifestResp.json();\n return NextResponse.json(manifest, {\n headers: {\n "Content-Type": "application/json",\n "Cache-Control": "public, max-age=3600, s-maxage=3600",\n },\n });\n } catch (error) {\n return NextResponse.json(\n {\n error: "Failed to fetch UCP manifest",\n detail: error instanceof Error ? error.message : String(error),\n },\n { status: 500 },\n );\n }\n}\n' }, { "path": "app/search/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { SearchClient } from "./search-client";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Search \u2014 ${brand.name}`,\n description: `Search ${brand.name} \u2014 products, collections, categories.`,\n};\n\nexport default function SearchPage() {\n return (\n <article className="max-w-7xl mx-auto px-6 sm:px-8 py-10">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Search\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Find anything.\n </h1>\n <Suspense fallback={<SearchSkeleton />}>\n <SearchClient />\n </Suspense>\n </article>\n );\n}\n\nfunction SearchSkeleton() {\n return (\n <div>\n <div className="h-12 w-full max-w-xl bg-muted rounded animate-pulse mb-8" />\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {Array.from({ length: 8 }).map((_, i) => (\n <div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </div>\n );\n}\n' }, { "path": "app/search/search-client.tsx", "kind": "text", "content": '"use client";\n\nimport { SearchPage } from "@cimplify/sdk/react";\n\nexport function SearchClient() {\n return <SearchPage />;\n}\n' }, { "path": "app/faq/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `FAQ \u2014 ${brand.name}`,\n description: "Delivery, pickup, custom cakes, allergens, payment \u2014 answers to the questions we hear most often.",\n};\n\nexport default function FaqPage() {\n const f = brand.faq;\n return (\n <article className="max-w-3xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {f.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-10 -tracking-[0.02em]">\n {f.title}\n </h1>\n <div className="space-y-12">\n {f.sections.map((section) => (\n <section key={section.title}>\n <h2 className="font-serif text-2xl font-semibold mb-5">{section.title}</h2>\n <dl className="space-y-6">\n {section.items.map((item) => (\n <div key={item.q}>\n <dt className="font-medium text-foreground mb-1.5">{item.q}</dt>\n <dd className="text-muted-foreground leading-relaxed">{item.a}</dd>\n </div>\n ))}\n </dl>\n </section>\n ))}\n </div>\n <p className="mt-12 pt-8 border-t border-border text-sm text-muted-foreground">\n {f.contactPrompt}{" "}\n <a\n href={`mailto:${f.contactEmail}`}\n className="text-primary font-semibold hover:underline"\n >\n {f.contactEmail}\n </a>{" "}\n and a real human will reply within 24 hours.\n </p>\n </article>\n );\n}\n' }, { "path": "app/robots.ts", "kind": "text", "content": 'import type { MetadataRoute } from "next";\nimport { getSiteUrl } from "@/lib/site-url";\n\nexport default async function robots(): Promise<MetadataRoute.Robots> {\n const siteUrl = await getSiteUrl();\n return {\n rules: [\n {\n userAgent: "*",\n allow: "/",\n disallow: ["/cart", "/checkout", "/orders/", "/api/"],\n },\n ],\n sitemap: `${siteUrl}/sitemap.xml`,\n host: siteUrl,\n };\n}\n' }, { "path": "app/track-order/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { TrackOrderForm } from "./track-order-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Track an order \u2014 ${brand.name}`,\n description: brand.trackOrder.body,\n};\n\nexport default function TrackOrderPage() {\n const t = brand.trackOrder;\n return (\n <article className="max-w-2xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {t.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,5vw,3rem)] font-semibold mb-4 -tracking-[0.02em]">\n {t.title}\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">{t.body}</p>\n <TrackOrderForm />\n </article>\n );\n}\n' }, { "path": "app/track-order/track-order-form.tsx", "kind": "text", "content": '"use client";\n\nimport { useState } from "react";\nimport { useRouter } from "next/navigation";\n\n/**\n * Guest order tracker. Routes /orders/<id> with the entered id; the\n * post-checkout confirmation page already lives at /orders/[id] so this\n * just sends the visitor there. Replace the redirect with a real lookup\n * (e.g. Cimplify orders API + email-match guard) for production.\n */\nexport function TrackOrderForm() {\n const router = useRouter();\n const [orderId, setOrderId] = useState("");\n const [email, setEmail] = useState("");\n\n return (\n <form\n onSubmit={(e) => {\n e.preventDefault();\n if (!orderId.trim()) return;\n router.push(`/orders/${encodeURIComponent(orderId.trim())}`);\n }}\n className="space-y-4 rounded-2xl border border-border bg-card p-6"\n >\n <div>\n <label\n htmlFor="orderId"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order number\n </label>\n <input\n id="orderId"\n name="orderId"\n required\n value={orderId}\n onChange={(e) => setOrderId(e.target.value)}\n placeholder="ord_abc123\u2026"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <div>\n <label\n htmlFor="email"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order email\n </label>\n <input\n id="email"\n name="email"\n type="email"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n placeholder="you@email.com"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <button\n type="submit"\n className="w-full inline-flex items-center justify-center px-5 py-3 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors"\n >\n Track order\n </button>\n </form>\n );\n}\n' }, { "path": "app/cart/page.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CartPage as SdkCartPage } from "@cimplify/sdk/react";\n\nexport default function CartPage() {\n const router = useRouter();\n return <SdkCartPage onCheckout={() => router.push("/checkout")} />;\n}\n' }, { "path": "app/llms.txt/route.ts", "kind": "text", "content": 'import { cacheTag, cacheLife } from "next/cache";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\n\nasync function buildLlmsTxt(SITE_URL: string): Promise<string> {\n "use cache";\n cacheTag(tags.products(), tags.categories(), tags.collections());\n cacheLife("hours");\n\n const client = getServerClient();\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n\n const products: Product[] = productsRes.ok ? productsRes.value.items : [];\n const categories = categoriesRes.ok ? categoriesRes.value : [];\n const collections = collectionsRes.ok ? collectionsRes.value : [];\n\n const lines: string[] = [];\n lines.push(`# ${brand.name}`);\n lines.push("");\n lines.push(`> ${brand.llms.summary}`);\n lines.push("");\n lines.push("## Browse");\n lines.push(`- [Home](${SITE_URL}/)`);\n lines.push(`- [Shop](${SITE_URL}/shop): Full menu with filter and sort.`);\n\n if (categories.length > 0) {\n lines.push("");\n lines.push("## Categories");\n for (const c of categories) {\n lines.push(\n `- [${c.name}](${SITE_URL}/categories/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (collections.length > 0) {\n lines.push("");\n lines.push("## Collections");\n for (const c of collections) {\n lines.push(\n `- [${c.name}](${SITE_URL}/collections/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (products.length > 0) {\n lines.push("");\n lines.push("## Products");\n for (const p of products) {\n const slug = p.slug ?? p.id;\n const price = `${brand.currency} ${p.default_price}`;\n const desc = p.description ? ` \u2014 ${p.description.replace(/\\s+/g, " ").slice(0, 200)}` : "";\n lines.push(`- [${p.name}](${SITE_URL}/shop?product=${slug}) (${price})${desc}`);\n }\n }\n\n lines.push("");\n lines.push("## Information");\n lines.push(`- [About](${SITE_URL}/about)`);\n lines.push(`- [FAQ](${SITE_URL}/faq)`);\n lines.push(`- [Terms of Service](${SITE_URL}/terms)`);\n lines.push(`- [Privacy Policy](${SITE_URL}/privacy)`);\n lines.push(`- [Sitemap (XML)](${SITE_URL}/sitemap.xml)`);\n lines.push("");\n lines.push("## Contact");\n lines.push(`- Email: ${brand.contact.email}`);\n lines.push(`- Phone: ${brand.contact.phone}`);\n lines.push(`- Address: ${brand.contact.address}`);\n\n return lines.join("\\n") + "\\n";\n}\n\n/**\n * `/llms.txt` \u2014 machine-readable site index for LLMs (per llmstxt.org).\n * Lets coding agents and chat assistants find products, categories, and\n * support pages without scraping HTML. Plain Markdown so it streams cheaply\n * into context windows.\n */\nexport async function GET(): Promise<Response> {\n const body = await buildLlmsTxt(await getSiteUrl());\n return new Response(body, {\n headers: {\n "Content-Type": "text/plain; charset=utf-8",\n "Cache-Control": "public, max-age=0, s-maxage=3600, stale-while-revalidate=86400",\n },\n });\n}\n' }, { "path": "app/sitemap.ts", "kind": "text", "content": 'import type { MetadataRoute } from "next";\nimport { getServerClient, type Product } from "@cimplify/sdk/server";\nimport { getSiteUrl } from "@/lib/site-url";\n\nconst STATIC_ROUTES: { path: string; priority: number; changeFrequency: "daily" | "weekly" | "monthly" }[] = [\n { path: "/", priority: 1.0, changeFrequency: "daily" },\n { path: "/shop", priority: 0.9, changeFrequency: "daily" },\n { path: "/about", priority: 0.5, changeFrequency: "monthly" },\n { path: "/faq", priority: 0.4, changeFrequency: "monthly" },\n { path: "/terms", priority: 0.2, changeFrequency: "monthly" },\n { path: "/privacy", priority: 0.2, changeFrequency: "monthly" },\n];\n\nexport default async function sitemap(): Promise<MetadataRoute.Sitemap> {\n const now = new Date();\n const siteUrl = await getSiteUrl();\n const client = getServerClient();\n\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n\n const products = productsRes.ok ? productsRes.value.items : [];\n const categories = categoriesRes.ok ? categoriesRes.value : [];\n const collections = collectionsRes.ok ? collectionsRes.value : [];\n\n const staticEntries: MetadataRoute.Sitemap = STATIC_ROUTES.map((r) => ({\n url: `${siteUrl}${r.path}`,\n lastModified: now,\n changeFrequency: r.changeFrequency,\n priority: r.priority,\n }));\n\n // Bakery uses a URL-driven product modal (`?product=<slug>` on the home or\n // shop pages). Emit those as deep-linkable URLs so search engines and LLMs\n // can index each product canonically.\n const productEntries: MetadataRoute.Sitemap = products.map((p: Product) => ({\n url: `${siteUrl}/shop?product=${encodeURIComponent(p.slug ?? p.id)}`,\n lastModified: p.updated_at ? new Date(p.updated_at) : now,\n changeFrequency: "weekly",\n priority: 0.7,\n }));\n\n const categoryEntries: MetadataRoute.Sitemap = categories.map((c) => ({\n url: `${siteUrl}/categories/${c.slug}`,\n lastModified: now,\n changeFrequency: "weekly",\n priority: 0.6,\n }));\n\n const collectionEntries: MetadataRoute.Sitemap = collections.map((c) => ({\n url: `${siteUrl}/collections/${c.slug}`,\n lastModified: now,\n changeFrequency: "weekly",\n priority: 0.6,\n }));\n\n return [...staticEntries, ...categoryEntries, ...collectionEntries, ...productEntries];\n}\n' }, { "path": "app/opensearch.xml/route.ts", "kind": "text", "content": 'import { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\n\n/**\n * OpenSearch description document \u2014 lets browsers add this site to the\n * address bar\'s search engine list. When users press Tab after typing the\n * domain, they get an inline search box that hits /search?q=...\n */\nexport async function GET(): Promise<Response> {\n const siteUrl = await getSiteUrl();\n const xml = `<?xml version="1.0" encoding="UTF-8"?>\n<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">\n <ShortName>${escapeXml(brand.shortName)}</ShortName>\n <Description>Search ${escapeXml(brand.name)}</Description>\n <InputEncoding>UTF-8</InputEncoding>\n <Url type="text/html" method="get" template="${siteUrl}/search?q={searchTerms}" />\n <Url type="application/opensearchdescription+xml" rel="self" template="${siteUrl}/opensearch.xml" />\n <moz:SearchForm>${siteUrl}/search</moz:SearchForm>\n</OpenSearchDescription>\n`;\n return new Response(xml, {\n headers: {\n "Content-Type": "application/opensearchdescription+xml; charset=utf-8",\n "Cache-Control": "public, max-age=86400",\n },\n });\n}\n\nfunction escapeXml(s: string): string {\n return s\n .replace(/&/g, "&amp;")\n .replace(/</g, "&lt;")\n .replace(/>/g, "&gt;")\n .replace(/"/g, "&quot;")\n .replace(/\'/g, "&apos;");\n}\n' }, { "path": "app/contact/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { ContactForm } from "./contact-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Contact \u2014 ${brand.name}`,\n description: brand.contactPage.body,\n};\n\nexport default function ContactPage() {\n const c = brand.contactPage;\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {c.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-4 -tracking-[0.02em]">\n {c.title}\n </h1>\n <p className="text-lg text-muted-foreground max-w-2xl mb-12 leading-relaxed">{c.body}</p>\n\n <div className="grid grid-cols-1 lg:grid-cols-[1.5fr_1fr] gap-10 items-start">\n <ContactForm reasons={c.reasons} />\n <aside className="space-y-5 lg:pl-10 lg:border-l border-border">\n <div>\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Direct lines\n </p>\n <ul className="space-y-3 list-none p-0 m-0">\n {c.directLines.map((line) => (\n <li key={line.label}>\n <p className="text-xs text-muted-foreground m-0">{line.label}</p>\n <a\n href={line.href}\n className="text-foreground hover:text-primary transition-colors"\n >\n {line.value}\n </a>\n </li>\n ))}\n </ul>\n </div>\n <div>\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Visit\n </p>\n <p className="text-foreground m-0">{brand.contact.address}</p>\n <p className="text-xs text-muted-foreground mt-1">{brand.contact.hours}</p>\n </div>\n </aside>\n </div>\n </article>\n );\n}\n' }, { "path": "app/contact/contact-form.tsx", "kind": "text", "content": `"use client";
1533
1533
 
1534
1534
  import { useState } from "react";
1535
1535
 
@@ -2058,7 +2058,7 @@ export const brand: Brand = {
2058
2058
  businessId: "bus_freshmart",
2059
2059
  },
2060
2060
  };
2061
- ` }, { "path": "lib/cimplify-loader.ts", "kind": "text", "content": 'import type { ImageLoader } from "next/image";\n\nimport { assetUrl, isCimplifyAsset } from "@cimplify/sdk";\n\nconst cdnBase = process.env.NEXT_PUBLIC_CIMPLIFY_CDN_URL?.trim() || undefined;\n\nconst cimplifyImageLoader: ImageLoader = ({ src, width, quality }) => {\n if (isCimplifyAsset(src, cdnBase)) {\n return assetUrl(src, {\n base: cdnBase,\n w: width,\n quality,\n format: "auto",\n });\n }\n return src;\n};\n\nexport default cimplifyImageLoader;\n' }, { "path": "lib/site.config.ts", "kind": "text", "content": "/**\n * Canonical absolute URL. The platform overwrites this at build time with the\n * storefront's primary domain; the default only applies to local builds. It's\n * per-deploy config, not per-request, so it stays a static constant \u2014 keeping\n * the root layout prerenderable under `cacheComponents`.\n */\nexport const SITE_URL = \"https://example.com\";\n" }, { "path": "lib/cart.ts", "kind": "text", "content": '"use client";\n\nimport { useCart } from "@cimplify/sdk/react";\n\n/**\n * Reactive cart count for the header pill. Subscribes to the SDK cart\n * state \u2014 updates immediately on add / remove / quantity change.\n */\nexport function useCartCount(): { count: number } {\n const { itemCount } = useCart();\n return { count: itemCount };\n}\n' }, { "path": "lib/site-url.ts", "kind": "text", "content": 'import { SITE_URL } from "./site.config";\n\n/** Canonical absolute URL for this storefront. */\nexport async function getSiteUrl(): Promise<string> {\n return SITE_URL;\n}\n' }, { "path": "metadata.json", "kind": "text", "content": '{\n "id": "grocery",\n "name": "Grocery",\n "tagline": "High-SKU grocery storefront with quick-add cart UX and delivery windows.",\n "industry": "retail",\n "tags": ["food", "grocery", "delivery"],\n "stability": "stable",\n "schemaType": "GroceryStore",\n "mock": {\n "seedName": "grocery",\n "seedBusinessId": "bus_freshmart"\n }\n}\n' }, { "path": "tsconfig.json", "kind": "text", "content": '{\n "compilerOptions": {\n "target": "ES2022",\n "lib": ["dom", "dom.iterable", "esnext"],\n "allowJs": false,\n "skipLibCheck": true,\n "strict": true,\n "noEmit": true,\n "esModuleInterop": true,\n "module": "esnext",\n "moduleResolution": "bundler",\n "resolveJsonModule": true,\n "isolatedModules": true,\n "jsx": "preserve",\n "incremental": true,\n "plugins": [{ "name": "next" }],\n "paths": {\n "@/*": ["./*"]\n }\n },\n "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts"],\n "exclude": ["node_modules"]\n}\n' }, { "path": "CLAUDE.md", "kind": "text", "content": "# CLAUDE.md\n\nThis project was scaffolded from a Cimplify storefront template (`cimplify init`).\n\n## Read these first\n\n- **`AGENTS.md`** at the project root \u2014 the file \u2194 `brand.X` field map for *this template's* industry, plus the architectural rules.\n- **`.claude/skills/cimplify-storefront/SKILL.md`** \u2014 the playbook for common tasks (rebrand, add a section, wire a Server Action, deploy, eject a component). Invoke it whenever you're asked to change content, palette, copy, or routing.\n\n## The two-line summary\n\n1. **Edit `lib/brand.ts`** for any visible string.\n2. **Edit `app/globals.css` `@theme` block** for any palette / radius / font change.\n\nThat covers ~95% of what merchants ask for. For anything else, follow the `cimplify-storefront` skill.\n\n## Don't do\n\n- Hardcode strings in pages or components.\n- Disable `cacheComponents: true` in `next.config.ts`.\n- Use `unstable_cache` \u2014 Next 16's canonical primitive is `'use cache'`.\n- Run `bun test` (Bun's `vi` shim is incomplete) \u2014 use `bun run test:run`.\n" }, { "path": "README.md", "kind": "text", "content": "# __STOREFRONT_NAME__\n\nA Cimplify storefront scaffolded from the **bakery** template \u2014 Next.js 16 (App Router), React 19, Tailwind v4. Warm food-styled palette, Playfair Display + Inter typography, designed for bakeries, p\xE2tisseries, and food businesses.\n\nFor a different industry, scaffold with `--template`:\n\n```bash\ncimplify init my-store --template retail # electronics / consumer goods\ncimplify init my-store --template bakery # default \u2014 food / p\xE2tisserie\n```\n\n## Run\n\n```bash\nbun install\nbun dev\n```\n\nTwo things start in parallel:\n\n- `cimplify-mock` \u2014 the Cimplify mock API on `http://127.0.0.1:8787` (Akua's Bakery seeded by default).\n- `next dev` \u2014 this storefront on `http://localhost:3000`.\n\nOpen the storefront in your browser. Edit `app/page.tsx` to start customising. The SDK ships full pages (`<CataloguePage>`, `<ProductPage>`, `<CartPage>`, `<CheckoutPage>`) and layouts (`<DefaultProductLayout>`, `<FoodProductLayout>`, etc.) that you can swap in.\n\n## Structure\n\n```\napp/\n layout.tsx # root layout, fonts, providers, header/footer/modal\n page.tsx # home\n shop/page.tsx # full catalogue (SDK <CataloguePage/>)\n collections/[slug]/ # collection landing\n categories/[slug]/ # category landing\n cart/page.tsx # SDK <CartPage/>\n checkout/page.tsx # SDK <CheckoutPage/>\n orders/[id]/page.tsx # post-checkout thank-you\n globals.css # Tailwind import + theme tokens\ncomponents/\n providers.tsx # CimplifyProvider client wrapper\n header.tsx, footer.tsx, hero.tsx\n store-product-card.tsx # SDK <ProductCard/> wired to URL-driven modal\n product-modal.tsx # ?product=<slug> deep-linkable modal\n collection-strip.tsx # horizontal product strip\n category-grid.tsx # SDK <CategoryGrid/> with router navigation\nlib/\n cart.ts # useCartCount() for the header pill\n```\n\n## Switch seed\n\n```bash\ncimplify-mock --seed restaurant # Mama's Kitchen\ncimplify-mock --seed retail # Currents Electronics\ncimplify-mock --seed services # Serene Spa\ncimplify-mock --seed grocery # FreshMart\n```\n\n## Go live\n\n```diff\n# .env.local\n- NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev\n+ NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=<your tenant key>\n```\n\nDeploy with `cimplify deploy --prod` after linking the project. See [`cimplify` CLI docs](https://www.cimplify.dev/docs/cli). `next.config.ts` already whitelists the SDK image hosts under `images.remotePatterns`.\n" }, { "path": ".env.example", "kind": "text", "content": "# Your tenant public key. Get it from the desk's Developers tab.\n# `cpk_live_*` / `cpk_test_*` keys auto-route same-origin requests\n# to hosted Cimplify in both dev and prod. Anything else (`mock-dev`,\n# empty) falls back to the local `cimplify dev` mock.\nNEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev\n\n# Optional. Forces a fixed canonical URL for sitemap.xml, robots.txt,\n# OpenGraph, and llms.txt. Leave unset and the storefront derives the\n# canonical from the request `Host` header automatically.\n# NEXT_PUBLIC_SITE_URL=\n" }], "storefront-auto": [{ "path": "vitest.config.ts", "kind": "text", "content": 'import { defineConfig } from "vitest/config";\n\nexport default defineConfig({\n test: {\n environment: "node",\n include: ["__tests__/**/*.test.ts"],\n globals: false,\n },\n});\n' }, { "path": "__tests__/cart-flow.test.ts", "kind": "text", "content": 'import { createCartFlowSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateCartFlowSuite({ seed: brand.mock.seed, businessId: brand.mock.businessId });\n' }, { "path": "__tests__/brand.test.ts", "kind": "text", "content": 'import { createBrandSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateBrandSuite({ brand });\n' }, { "path": "__tests__/contract.test.ts", "kind": "text", "content": 'import { createContractSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateContractSuite({ seed: brand.mock.seed });\n' }, { "path": "AGENTS.md", "kind": "text", "content": '# AGENTS.md \u2014 Auto-parts storefront template\n\nIf you are an AI agent working on this storefront, **start here.**\n\n## TL;DR for rebranding\n\n1. **Edit `lib/brand.ts`** \u2014 every visible string, plus the `fitments` make/model/year tree.\n2. **Edit `app/globals.css`** \u2014 `@theme { \u2026 }` block for palette + radius + font references.\n3. **Edit `.env.local`** \u2014 `NEXT_PUBLIC_CIMPLIFY_*` env vars.\n\n## Aesthetic\n\n- **Inter + JetBrains Mono** \u2014 clean modern sans, mono-spaced labels for spec-feel.\n- **Carbon-black foreground + sport-red accent.**\n- **Sharp corners**: `0.5rem` \u2014 workshop-modern.\n- **`AutoPartsStore`** is the Schema.org `@type` (set in `brand.schemaType`).\n\n## Page surface\n\n```\napp/\n page.tsx AutoHero + FitmentFinder + TrustBar + Most ordered\n grid + ServiceBrief + Newsletter\n shop/page.tsx SDK <CataloguePage/>. Reads `?fits=<tag>` to\n filter by fitment tag.\n search/page.tsx Search\n collections/[slug]/page.tsx Collection landing\n categories/[slug]/page.tsx Category landing\n products/[slug]/page.tsx Full product detail page (Product JSON-LD)\n cart/page.tsx, checkout/page.tsx, orders/[id]/page.tsx SDK\n account/* + login/signup\n contact/, track-order/, about, faq, terms, privacy, shipping, returns,\n accessibility/, sitemap-page/ Brand-driven content pages\n```\n\n## File \u2194 brand-field map\n\n| File | Reads from `brand` |\n|---|---|\n| `app/layout.tsx` | identity, contact, socials, schemaType (AutoPartsStore JSON-LD) |\n| `app/page.tsx` | `brand.hero`, `brand.fitments`, `brand.serviceBrief`, `brand.newsletter` |\n| `app/about/page.tsx` | `brand.about` |\n| `app/faq/page.tsx` | `brand.faq` |\n| `app/terms/page.tsx` | `brand.terms` |\n| `app/privacy/page.tsx` | `brand.privacy` |\n| `app/shipping/page.tsx` | `brand.shipping` |\n| `app/returns/page.tsx` | `brand.returns` |\n| `app/accessibility/page.tsx` | `brand.accessibility` |\n| `app/contact/page.tsx` | `brand.contactPage`, `brand.contact` |\n| `app/track-order/page.tsx` | `brand.trackOrder` |\n| `app/products/[slug]/page.tsx` | `brand.name`, `brand.currency` |\n| `app/llms.txt/route.ts` | `brand.llms`, contact, currency |\n| `components/auto-hero.tsx` | `brand.hero`, hard-coded trust-chip labels |\n| `components/fitment-finder.tsx` | `brand.fitments` (makes/models/years tree) |\n| `components/service-brief.tsx` | `brand.serviceBrief` (3 cards) |\n| `components/header.tsx`, `components/footer.tsx` | `brand.shortName`, `brand.header.nav`, `brand.footer`, `brand.contact`, `brand.socials` |\n| `components/trust-bar.tsx` | `brand.trustItems` |\n| `components/newsletter.tsx` | `brand.newsletter` |\n\n## Auto-specific notes\n\n- **Fitment is tag-based, not a custom_attribute.** Each product carries `tags: ["fits:toyota", "fits:toyota:corolla"]` etc. or `["fits:universal"]`. The shop page reads `?fits=<tag>` and filters by tag prefix match. Universal-fit products bypass the filter.\n- **Adding a vehicle**: edit `brand.fitments.makes` in `lib/brand.ts`. Add `{ slug, name, models: [...] }`. Slug becomes the URL fragment.\n- **Adding a part with new fitment**: tag the product `fits:<make>` and (optionally) `fits:<make>:<model>`. No schema change needed.\n- **Partner-workshop fitting**: the mock doesn\'t model this; the storefront copy describes it. To wire it, eject the cart drawer and add a "Add fitting (GH\u20B580)" toggle that adds a service line.\n\n## Known TODOs\n\n- Contact form + newsletter signup currently fake their submit. Wire to a real provider.\n- The fitment finder uses `useRouter().push` to a query-string URL; the catalogue page-side filter is left as an exercise for the merchant (read the `fits` param and pass it as a SDK `getProducts({ tags: [...] })` filter).\n\n## Mock seed\n\nWired to `--seed auto` (Driveline Auto Parts). Edit `dev:mock` in `package.json` to preview another seed.\n\n## Customizing SDK components\n\nFor anything beyond `lib/brand.ts` + `app/globals.css`, lean on the SDK\'s prebuilt components rather than reinvent. **Especially for product customization** (variants, add-ons, fitment-tag filtering, services with scheduling) \u2014 the SDK already gets price math, axis matching, tag filtering, and cart payload contracts right. Default to ejecting and restyling:\n\n```bash\ncimplify add cart-drawer\ncimplify add variant-selector\ncimplify add product-page\n```\n\nThen edit the local copy. **Don\'t change the cart payload shape** unless you\'re also touching the SDK mock + backend lens. Full ejection rules and the customizer contract are in the SDK-level [`AGENTS.md`](../../AGENTS.md) \u2192 "Don\'t reinvent product customization".\n\n## Quick start\n\n```bash\nbun install\nbun dev\n```\n\nOpen <http://localhost:3000>.\n' }, { "path": "postcss.config.mjs", "kind": "text", "content": 'const config = {\n plugins: {\n "@tailwindcss/postcss": {},\n },\n};\n\nexport default config;\n' }, { "path": "components/trade-in-cta.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport { brand } from "@/lib/brand";\n\nexport function TradeInCta() {\n const t = brand.tradeIn;\n if (!t) return null;\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-14 sm:py-20">\n <div className="grid grid-cols-1 lg:grid-cols-2 gap-8 items-center bg-foreground text-background rounded-3xl p-8 sm:p-12 lg:p-14 overflow-hidden relative">\n <div className="absolute top-0 right-0 w-1/2 h-full opacity-[0.04] [background-image:linear-gradient(135deg,white_1px,transparent_1px)] [background-size:24px_24px] pointer-events-none" />\n <div>\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary-foreground/70 mb-3">\n {t.eyebrow}\n </p>\n <h2 className="text-[clamp(1.75rem,3.5vw,2.5rem)] font-bold m-0 mb-4 -tracking-[0.025em] leading-[1.1]">\n {t.title}\n </h2>\n <p className="text-background/75 leading-relaxed mb-6">{t.body}</p>\n <div className="flex flex-wrap items-center gap-3">\n <Link\n href={t.primaryCtaHref}\n className="inline-flex items-center gap-2 px-5 py-2.5 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors"\n >\n {t.primaryCtaLabel}\n <svg viewBox="0 0 12 12" className="w-3 h-3" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden>\n <path d="M3 6h7m0 0L7 3m3 3L7 9" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n </Link>\n <Link\n href={t.secondaryCtaHref}\n className="inline-flex items-center gap-2 px-5 py-2.5 rounded-md border border-background/25 hover:bg-background/10 transition-colors text-sm font-medium"\n >\n {t.secondaryCtaLabel}\n </Link>\n </div>\n </div>\n <div className="grid grid-cols-3 gap-3">\n {t.steps.map((s) => (\n <div\n key={s.step}\n className="rounded-2xl p-4 sm:p-5 bg-background/5 border border-background/10"\n >\n <p className="text-[10px] font-mono text-primary tabular-nums mb-3">{s.step}</p>\n <p className="text-[13px] sm:text-sm font-semibold mb-1.5 -tracking-[0.015em]">\n {s.title}\n </p>\n <p className="text-[11px] sm:text-xs text-background/65 leading-snug">{s.body}</p>\n </div>\n ))}\n </div>\n </div>\n </section>\n );\n}\n' }, { "path": "components/cart-pill.tsx", "kind": "text", "content": '"use client";\n\nimport { useCartDrawer } from "@cimplify/sdk/react";\nimport { useCartCount } from "@/lib/cart";\n\n/**\n * Cart pill \u2014 dynamic island. Reads the live cart count via the SDK and\n * opens the side cart drawer on click (instead of navigating to /cart).\n * Wrap in `<Suspense fallback={<CartPillSkeleton/>}>` so the cached\n * header chrome streams without blocking on the cart fetch.\n */\nexport function CartPill() {\n const { count } = useCartCount();\n const { open } = useCartDrawer();\n return (\n <button\n type="button"\n onClick={open}\n aria-label={`Open cart, ${count} ${count === 1 ? "item" : "items"}`}\n className="inline-flex items-center gap-1.5 px-4 py-2.5 sm:py-2 rounded-full bg-foreground text-background text-xs font-semibold tracking-wide transition-transform hover:scale-105 cursor-pointer"\n >\n Cart \xB7 {count}\n </button>\n );\n}\n\nexport function CartPillSkeleton() {\n return (\n <span\n aria-hidden\n className="inline-flex items-center gap-1.5 px-4 py-2.5 sm:py-2 rounded-full bg-foreground/80 text-background text-xs font-semibold tracking-wide"\n >\n Cart \xB7 \u2026\n </span>\n );\n}\n' }, { "path": "components/category-tiles.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport Image from "next/image";\nimport type { Category } from "@cimplify/sdk";\n\ninterface CategoryTilesProps {\n categories: Category[];\n}\n\nconst ICONS: Record<string, React.ReactNode> = {\n phones: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-7 h-7" aria-hidden>\n <rect x="6" y="2" width="12" height="20" rx="2.5" />\n <line x1="12" y1="18" x2="12" y2="18" strokeWidth="2.5" strokeLinecap="round" />\n </svg>\n ),\n laptops: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-7 h-7" aria-hidden>\n <rect x="3" y="4" width="18" height="12" rx="1.5" />\n <line x1="2" y1="20" x2="22" y2="20" strokeLinecap="round" />\n </svg>\n ),\n audio: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-7 h-7" aria-hidden>\n <path d="M3 14v-2a9 9 0 0 1 18 0v2" />\n <rect x="2" y="14" width="5" height="6" rx="1.5" />\n <rect x="17" y="14" width="5" height="6" rx="1.5" />\n </svg>\n ),\n accessories: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-7 h-7" aria-hidden>\n <rect x="2" y="6" width="14" height="10" rx="2" />\n <path d="M16 12h4l2 2v0l-2 2h-4" />\n </svg>\n ),\n gaming: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-7 h-7" aria-hidden>\n <path d="M5 7h14a3 3 0 0 1 3 3v4a3 3 0 0 1-3 3h-1l-2-3H8l-2 3H5a3 3 0 0 1-3-3v-4a3 3 0 0 1 3-3z" />\n <line x1="9" y1="11" x2="9" y2="13" strokeLinecap="round" />\n <line x1="8" y1="12" x2="10" y2="12" strokeLinecap="round" />\n <circle cx="15" cy="11" r="0.75" fill="currentColor" />\n <circle cx="17" cy="13" r="0.75" fill="currentColor" />\n </svg>\n ),\n};\n\nexport function CategoryTiles({ categories }: CategoryTilesProps) {\n if (categories.length === 0) return null;\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-14 sm:py-20">\n <div className="flex items-end justify-between gap-6 mb-8">\n <div>\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2">\n Shop the catalogue\n </p>\n <h2 className="text-[clamp(1.75rem,3vw,2.25rem)] font-bold -tracking-[0.025em]">\n Pick your category.\n </h2>\n </div>\n <Link\n href="/shop"\n className="text-sm font-semibold text-primary hover:underline whitespace-nowrap hidden sm:inline-flex items-center gap-1"\n >\n See everything\n <svg viewBox="0 0 12 12" className="w-3 h-3" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden>\n <path d="M3 6h7m0 0L7 3m3 3L7 9" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n </Link>\n </div>\n <div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3 sm:gap-4">\n {categories.map((c, i) => (\n <Link\n key={c.id}\n href={`/categories/${c.slug}`}\n className="group relative overflow-hidden rounded-2xl bg-card border border-border p-5 hover:border-primary hover:-translate-y-0.5 transition-all"\n >\n <div className="flex items-start justify-between mb-12">\n <div className="grid place-items-center w-10 h-10 rounded-lg bg-primary/10 text-primary">\n {ICONS[c.slug] ?? (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-7 h-7" aria-hidden>\n <rect x="3" y="3" width="18" height="18" rx="2" />\n </svg>\n )}\n </div>\n <span className="text-[10px] font-mono text-muted-foreground tabular-nums">\n {String(i + 1).padStart(2, "0")}\n </span>\n </div>\n <p className="text-base font-semibold mb-1 -tracking-[0.015em]">{c.name}</p>\n {c.product_count != null && (\n <p className="text-xs text-muted-foreground">\n {c.product_count} {c.product_count === 1 ? "product" : "products"}\n </p>\n )}\n <span className="absolute right-4 bottom-4 grid place-items-center w-7 h-7 rounded-full bg-foreground text-background opacity-0 group-hover:opacity-100 transition-opacity">\n <svg viewBox="0 0 12 12" className="w-3 h-3" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden>\n <path d="M3 6h7m0 0L7 3m3 3L7 9" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n </span>\n </Link>\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "components/promo-banner.tsx", "kind": "text", "content": `import Link from "next/link";
2061
+ ` }, { "path": "lib/cimplify-loader.ts", "kind": "text", "content": 'import type { ImageLoader } from "next/image";\n\nimport { assetUrl, isCimplifyAsset } from "@cimplify/sdk";\n\nconst cdnBase = process.env.NEXT_PUBLIC_CIMPLIFY_CDN_URL?.trim() || undefined;\n\nconst cimplifyImageLoader: ImageLoader = ({ src, width, quality }) => {\n if (isCimplifyAsset(src, cdnBase)) {\n return assetUrl(src, {\n base: cdnBase,\n w: width,\n quality,\n format: "auto",\n });\n }\n return src;\n};\n\nexport default cimplifyImageLoader;\n' }, { "path": "lib/site.config.ts", "kind": "text", "content": "/**\n * Canonical absolute URL. The platform overwrites this at build time with the\n * storefront's primary domain; the default only applies to local builds. It's\n * per-deploy config, not per-request, so it stays a static constant \u2014 keeping\n * the root layout prerenderable under `cacheComponents`.\n */\nexport const SITE_URL = \"https://example.com\";\n" }, { "path": "lib/cart.ts", "kind": "text", "content": '"use client";\n\nimport { useCart } from "@cimplify/sdk/react";\n\n/**\n * Reactive cart count for the header pill. Subscribes to the SDK cart\n * state \u2014 updates immediately on add / remove / quantity change.\n */\nexport function useCartCount(): { count: number } {\n const { itemCount } = useCart();\n return { count: itemCount };\n}\n' }, { "path": "lib/site-url.ts", "kind": "text", "content": 'import { SITE_URL } from "./site.config";\n\n/** Canonical absolute URL for this storefront. */\nexport async function getSiteUrl(): Promise<string> {\n return SITE_URL;\n}\n' }, { "path": "metadata.json", "kind": "text", "content": '{\n "id": "grocery",\n "name": "Grocery",\n "tagline": "High-SKU grocery storefront with quick-add cart UX and delivery windows.",\n "industry": "retail",\n "tags": ["food", "grocery", "delivery"],\n "stability": "stable",\n "schemaType": "GroceryStore",\n "mock": {\n "seedName": "grocery",\n "seedBusinessId": "bus_freshmart"\n }\n}\n' }, { "path": "tsconfig.json", "kind": "text", "content": '{\n "compilerOptions": {\n "target": "ES2022",\n "lib": ["dom", "dom.iterable", "esnext"],\n "allowJs": false,\n "skipLibCheck": true,\n "strict": true,\n "noEmit": true,\n "esModuleInterop": true,\n "module": "esnext",\n "moduleResolution": "bundler",\n "resolveJsonModule": true,\n "isolatedModules": true,\n "jsx": "preserve",\n "incremental": true,\n "plugins": [{ "name": "next" }],\n "paths": {\n "@/*": ["./*"]\n }\n },\n "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts"],\n "exclude": ["node_modules"]\n}\n' }, { "path": "CLAUDE.md", "kind": "text", "content": "# CLAUDE.md\n\nThis project was scaffolded from a Cimplify storefront template (`cimplify init`).\n\n## Read these first\n\n- **`AGENTS.md`** at the project root \u2014 the file \u2194 `brand.X` field map for *this template's* industry, plus the architectural rules.\n- **`.claude/skills/cimplify-storefront/SKILL.md`** \u2014 the playbook for common tasks (rebrand, add a section, wire a Server Action, deploy, eject a component). Invoke it whenever you're asked to change content, palette, copy, or routing.\n\n## The two-line summary\n\n1. **Edit `lib/brand.ts`** for any visible string.\n2. **Edit `app/globals.css` `@theme` block** for any palette / radius / font change.\n\nThat covers ~95% of what merchants ask for. For anything else, follow the `cimplify-storefront` skill.\n\n## Don't do\n\n- Hardcode strings in pages or components.\n- Disable `cacheComponents: true` in `next.config.ts`.\n- Use `unstable_cache` \u2014 Next 16's canonical primitive is `'use cache'`.\n- Run `bun test` (Bun's `vi` shim is incomplete) \u2014 use `bun run test:run`.\n" }, { "path": "README.md", "kind": "text", "content": "# __STOREFRONT_NAME__\n\nA Cimplify storefront scaffolded from the **bakery** template \u2014 Next.js 16 (App Router), React 19, Tailwind v4. Warm food-styled palette, Playfair Display + Inter typography, designed for bakeries, p\xE2tisseries, and food businesses.\n\nFor a different industry, scaffold with `--template`:\n\n```bash\ncimplify init my-store --template retail # electronics / consumer goods\ncimplify init my-store --template bakery # default \u2014 food / p\xE2tisserie\n```\n\n## Run\n\n```bash\nbun install\nbun dev\n```\n\nTwo things start in parallel:\n\n- `cimplify-mock` \u2014 the Cimplify mock API on `http://127.0.0.1:8787` (Akua's Bakery seeded by default).\n- `next dev` \u2014 this storefront on `http://localhost:3000`.\n\nOpen the storefront in your browser. Edit `app/page.tsx` to start customising. The SDK ships full pages (`<CataloguePage>`, `<ProductPage>`, `<CartPage>`, `<CheckoutPage>`) and layouts (`<DefaultProductLayout>`, `<FoodProductLayout>`, etc.) that you can swap in.\n\n## Structure\n\n```\napp/\n layout.tsx # root layout, fonts, providers, header/footer/modal\n page.tsx # home\n shop/page.tsx # full catalogue (SDK <CataloguePage/>)\n collections/[slug]/ # collection landing\n categories/[slug]/ # category landing\n cart/page.tsx # SDK <CartPage/>\n checkout/page.tsx # SDK <CheckoutPage/>\n orders/[id]/page.tsx # post-checkout thank-you\n globals.css # Tailwind import + theme tokens\ncomponents/\n providers.tsx # CimplifyProvider client wrapper\n header.tsx, footer.tsx, hero.tsx\n store-product-card.tsx # SDK <ProductCard/> wired to URL-driven modal\n product-modal.tsx # ?product=<slug> deep-linkable modal\n collection-strip.tsx # horizontal product strip\n category-grid.tsx # SDK <CategoryGrid/> with router navigation\nlib/\n cart.ts # useCartCount() for the header pill\n```\n\n## Switch seed\n\n```bash\ncimplify-mock --seed restaurant # Mama's Kitchen\ncimplify-mock --seed retail # Currents Electronics\ncimplify-mock --seed services # Serene Spa\ncimplify-mock --seed grocery # FreshMart\n```\n\n## Go live\n\n```diff\n# .env.local\n- NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev\n+ NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=<your tenant key>\n```\n\nDeploy with `cimplify deploy --prod` after linking the project. See [`cimplify` CLI docs](https://www.cimplify.dev/docs/cli). `next.config.ts` already whitelists the SDK image hosts under `images.remotePatterns`.\n" }, { "path": "bun.lock", "kind": "binary", "content": "ewogICJsb2NrZmlsZVZlcnNpb24iOiAxLAogICJjb25maWdWZXJzaW9uIjogMSwKICAid29ya3NwYWNlcyI6IHsKICAgICIiOiB7CiAgICAgICJuYW1lIjogIl9fU1RPUkVGUk9OVF9OQU1FX18iLAogICAgICAiZGVwZW5kZW5jaWVzIjogewogICAgICAgICJAY2ltcGxpZnkvc2RrIjogIl4wLjQ4LjIiLAogICAgICAgICJuZXh0IjogIl4xNi4yLjQiLAogICAgICAgICJyZWFjdCI6ICJeMTkuMC4wIiwKICAgICAgICAicmVhY3QtZG9tIjogIl4xOS4wLjAiLAogICAgICB9LAogICAgICAiZGV2RGVwZW5kZW5jaWVzIjogewogICAgICAgICJAdGFpbHdpbmRjc3MvcG9zdGNzcyI6ICJeNC4yLjQiLAogICAgICAgICJAdHlwZXMvbm9kZSI6ICJeMjIuMTAuMCIsCiAgICAgICAgIkB0eXBlcy9yZWFjdCI6ICJeMTkuMC4wIiwKICAgICAgICAiQHR5cGVzL3JlYWN0LWRvbSI6ICJeMTkuMC4wIiwKICAgICAgICAiY29uY3VycmVudGx5IjogIl45LjAuMCIsCiAgICAgICAgInRhaWx3aW5kY3NzIjogIl40LjIuNCIsCiAgICAgICAgInR5cGVzY3JpcHQiOiAiXjUuOS4zIiwKICAgICAgICAidml0ZXN0IjogIl40LjEuNSIsCiAgICAgIH0sCiAgICB9LAogIH0sCiAgInBhY2thZ2VzIjogewogICAgIkBhbGxvYy9xdWljay1scnUiOiBbIkBhbGxvYy9xdWljay1scnVANS4yLjAiLCAiIiwge30sICJzaGE1MTItVXJjQUJCKzRiVXJGQUJ3Ymx1VElCRXJYd3Zic1UvVjdUWldmbWJnSmZia3dpQnV6aVM5Z3hkT0RVeXVpZWNmZEdRODVqZ2xNVzZqdVMzK3o1VHNLTHc9PSJdLAoKICAgICJAYmFiZWwvcnVudGltZSI6IFsiQGJhYmVsL3J1bnRpbWVANy4yOS4yIiwgIiIsIHt9LCAic2hhNTEyLUppRFNoSDQ1ektIV3lHZTRaTlZSckNqQno4Tmg5VE1tWkcxa2g0UVRLOGhDQlRXQmk4RGEraTdzMWZKdzcvbFlwTTRjY2VwU05mcXpaL1F2QUJCaTVnPT0iXSwKCiAgICAiQGJhc2UtdWkvcmVhY3QiOiBbIkBiYXNlLXVpL3JlYWN0QDEuNS4wIiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIkBiYWJlbC9ydW50aW1lIjogIl43LjI5LjIiLCAiQGJhc2UtdWkvdXRpbHMiOiAiMC4yLjkiLCAiQGZsb2F0aW5nLXVpL3JlYWN0LWRvbSI6ICJeMi4xLjgiLCAiQGZsb2F0aW5nLXVpL3V0aWxzIjogIl4wLjIuMTEiLCAidXNlLXN5bmMtZXh0ZXJuYWwtc3RvcmUiOiAiXjEuNi4wIiB9LCAicGVlckRlcGVuZGVuY2llcyI6IHsgIkBkYXRlLWZucy90eiI6ICJeMS4yLjAiLCAiQHR5cGVzL3JlYWN0IjogIl4xNyB8fCBeMTggfHwgXjE5IiwgImRhdGUtZm5zIjogIl40LjAuMCIsICJyZWFjdCI6ICJeMTcgfHwgXjE4IHx8IF4xOSIsICJyZWFjdC1kb20iOiAiXjE3IHx8IF4xOCB8fCBeMTkiIH0sICJvcHRpb25hbFBlZXJzIjogWyJAZGF0ZS1mbnMvdHoiLCAiQHR5cGVzL3JlYWN0IiwgImRhdGUtZm5zIl0gfSwgInNoYTUxMi16MWdTQWxjZWQxeVkraU0rbUhERXRJa0Q4VUkzRWJzNTJNdUJQeHZWNmY1aFJ1dGsreHZDSC93dUI3aERxRHpLOUpHNUZvTXo1bmhycXRTczF3anQxQT09Il0sCgogICAgIkBiYXNlLXVpL3V0aWxzIjogWyJAYmFzZS11aS91dGlsc0AwLjIuOSIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAYmFiZWwvcnVudGltZSI6ICJeNy4yOS4yIiwgIkBmbG9hdGluZy11aS91dGlscyI6ICJeMC4yLjExIiwgInJlc2VsZWN0IjogIl41LjEuMSIsICJ1c2Utc3luYy1leHRlcm5hbC1zdG9yZSI6ICJeMS42LjAiIH0sICJwZWVyRGVwZW5kZW5jaWVzIjogeyAiQHR5cGVzL3JlYWN0IjogIl4xNyB8fCBeMTggfHwgXjE5IiwgInJlYWN0IjogIl4xNyB8fCBeMTggfHwgXjE5IiwgInJlYWN0LWRvbSI6ICJeMTcgfHwgXjE4IHx8IF4xOSIgfSwgIm9wdGlvbmFsUGVlcnMiOiBbIkB0eXBlcy9yZWFjdCJdIH0sICJzaGE1MTIteC9QRERDWXpvcVBwanJkeWIzVmN5eWxUSTJJalVYRXRZREdpNWZvaDdLc25tTkpJSWFWd0EyR0xnREgxZHBzMUdnWGlKYkE2MGhNK0F5dVRmUXpJdnc9PSJdLAoKICAgICJAY2ltcGxpZnkvc2RrIjogWyJAY2ltcGxpZnkvc2RrQDAuNDguMiIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAYmFzZS11aS9yZWFjdCI6ICJeMS40LjEiLCAiY2xzeCI6ICJeMi4xLjEiLCAiZGF0ZS1mbnMiOiAiXjQuMS4wIiwgImxpYnBob25lbnVtYmVyLWpzIjogIjEuMTIuNDEiLCAicmVhY3QtZGF5LXBpY2tlciI6ICJeOS4xNC4wIiwgInRhaWx3aW5kLW1lcmdlIjogIl4zLjUuMCIsICJ6b2QiOiAiXjQuNC4zIiB9LCAicGVlckRlcGVuZGVuY2llcyI6IHsgIkBwYXlzdGFjay9pbmxpbmUtanMiOiAiXjIuMjIuOCIsICJtc3ciOiAiPj0yLjAuMCIsICJyZWFjdCI6ICI+PTE3LjAuMCIsICJ2aXRlc3QiOiAiPj0yLjAuMCIgfSwgIm9wdGlvbmFsUGVlcnMiOiBbIkBwYXlzdGFjay9pbmxpbmUtanMiLCAibXN3IiwgInJlYWN0IiwgInZpdGVzdCJdLCAiYmluIjogeyAiY2ltcGxpZnktbW9jayI6ICJkaXN0L21vY2svY2xpLm1qcyIgfSB9LCAic2hhNTEyLTZJaDhNNkxZVkhDdU84c2JnZWc3VmdQTUJIWHZNK2NodFkzLzJWZkdWRWZFeU5UY0d4UzZlcDFOMDdNS21tTG5xQWNMZTZteFQwbVFiL2h0cldQZ1hBPT0iXSwKCiAgICAiQGRhdGUtZm5zL3R6IjogWyJAZGF0ZS1mbnMvdHpAMS40LjEiLCAiIiwge30sICJzaGE1MTItUDVMVU5odGJqNllmSTNpSmp3NUVMOWVVQUc2T2l0RDBXM2ZXUWNwUWpEUmMvUUlzTDB0Uk51TzFQY0R2UGNjV0wxZlNUWFhkRTFkcytsOTVEVi9PRkE9PSJdLAoKICAgICJAZW1uYXBpL2NvcmUiOiBbIkBlbW5hcGkvY29yZUAxLjEwLjAiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiQGVtbmFwaS93YXNpLXRocmVhZHMiOiAiMS4yLjEiLCAidHNsaWIiOiAiXjIuNC4wIiB9IH0sICJzaGE1MTIteXE2T2tKNHA4MkNBZlBsMHU5bVFlYlFIS1BKa1k3V3JJdWsyMDVjVFluWWUrazJaOFlCaDExRnJiUkcvSDZpaGlycWNhY09nbDJCSU84b3lNUUxlWHc9PSJdLAoKICAgICJAZW1uYXBpL3J1bnRpbWUiOiBbIkBlbW5hcGkvcnVudGltZUAxLjEwLjAiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAidHNsaWIiOiAiXjIuNC4wIiB9IH0sICJzaGE1MTItZXd2WWxrODZ4VW9HSTB6UVJOcS9tQysxNlIxUWVEbEtReTIxS2kzb1NZWE5nTGI0NUdWMVA2QTBNKy9zNm55Q3VORHFlNVZwYVk4NEJ6WEd3VmJ3RkE9PSJdLAoKICAgICJAZW1uYXBpL3dhc2ktdGhyZWFkcyI6IFsiQGVtbmFwaS93YXNpLXRocmVhZHNAMS4yLjEiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAidHNsaWIiOiAiXjIuNC4wIiB9IH0sICJzaGE1MTItdVRJSTdPWUYrL01lcy9NcmNJT1lwNXlPdFNNTEJXU0lvTFBwY2d3aXBvaUtibGk2azMyMnRjb0ZzeG9JSXhQRHFXMDFTUUdBZ2tvNEV6WmkyQk52Mnc9PSJdLAoKICAgICJAZmxvYXRpbmctdWkvY29yZSI6IFsiQGZsb2F0aW5nLXVpL2NvcmVAMS43LjUiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiQGZsb2F0aW5nLXVpL3V0aWxzIjogIl4wLjIuMTEiIH0gfSwgInNoYTUxMi0xSWg0V1RXeXcwK2xLeUZNY0JIR2JiNVU1RnR1SEp1dWpveXlyNXpUYVdTNUVZTWVUNkpiMkF1RGVmdHNDc0V1Y2hPK21NMmlqNStxOWNyaHlkekxoUT09Il0sCgogICAgIkBmbG9hdGluZy11aS9kb20iOiBbIkBmbG9hdGluZy11aS9kb21AMS43LjYiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiQGZsb2F0aW5nLXVpL2NvcmUiOiAiXjEuNy41IiwgIkBmbG9hdGluZy11aS91dGlscyI6ICJeMC4yLjExIiB9IH0sICJzaGE1MTItOWdaU0FJNVhNMzY4ODBQUE1tLy85ZGZpRW5nWW9DNkFtMml6RVMxRkY0MDZZRnNqdnlCTW1lSjJnNFNBanUzeFd3dHV5bk5SRkwyczloZ3hwTEk1U1E9PSJdLAoKICAgICJAZmxvYXRpbmctdWkvcmVhY3QtZG9tIjogWyJAZmxvYXRpbmctdWkvcmVhY3QtZG9tQDIuMS44IiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIkBmbG9hdGluZy11aS9kb20iOiAiXjEuNy42IiB9LCAicGVlckRlcGVuZGVuY2llcyI6IHsgInJlYWN0IjogIj49MTYuOC4wIiwgInJlYWN0LWRvbSI6ICI+PTE2LjguMCIgfSB9LCAic2hhNTEyLWNDNTJiSHdNL24vQ3hTODdGSDB5V2RuZ0VacmpkdExXL3FWcnVvNjhxZytwcks3WlE0WUdkdXQyR3lEVnBvR2VBWWUvaDg5OXJWZU9WbTZPaTQwazJBPT0iXSwKCiAgICAiQGZsb2F0aW5nLXVpL3V0aWxzIjogWyJAZmxvYXRpbmctdWkvdXRpbHNAMC4yLjExIiwgIiIsIHt9LCAic2hhNTEyLVJpQi95SWg3OHBjSXhsNmxMTUcwQ2dCWEFaMlkwZVZIcU1QWXVndSs5VTBBZVQ2WUJlaUpwZjdsYmRKTkl1Z0ZQNVNJandOUmdvNERoUjFReGkyNkdnPT0iXSwKCiAgICAiQGltZy9jb2xvdXIiOiBbIkBpbWcvY29sb3VyQDEuMS4wIiwgIiIsIHt9LCAic2hhNTEyLVRkNzZxN2o1N28vdExWZGdTNzQ2Y1lBUmZTeXhrOGlFZlJ4ZXdMOWg0T016WWhiVzRUQWNwcGwwbVQ0ZXlxWGRkaDZML2p3b003NW1vN2l4YS9wQ2VRPT0iXSwKCiAgICAiQGltZy9zaGFycC1kYXJ3aW4tYXJtNjQiOiBbIkBpbWcvc2hhcnAtZGFyd2luLWFybTY0QDAuMzQuNSIsICIiLCB7ICJvcHRpb25hbERlcGVuZGVuY2llcyI6IHsgIkBpbWcvc2hhcnAtbGlidmlwcy1kYXJ3aW4tYXJtNjQiOiAiMS4yLjQiIH0sICJvcyI6ICJkYXJ3aW4iLCAiY3B1IjogImFybTY0IiB9LCAic2hhNTEyLWltdFEzV01KWGJNWTRmeGIvTmRwNkhCVE5WdFdDVUkwV2RvYnloZUdmNSthZDZ4WDhWSURPOHUyeEU0cWMvZnIwOENLRy83ZERzZUZ0bjZNNmcvcjN3PT0iXSwKCiAgICAiQGltZy9zaGFycC1kYXJ3aW4teDY0IjogWyJAaW1nL3NoYXJwLWRhcndpbi14NjRAMC4zNC41IiwgIiIsIHsgIm9wdGlvbmFsRGVwZW5kZW5jaWVzIjogeyAiQGltZy9zaGFycC1saWJ2aXBzLWRhcndpbi14NjQiOiAiMS4yLjQiIH0sICJvcyI6ICJkYXJ3aW4iLCAiY3B1IjogIng2NCIgfSwgInNoYTUxMi1ZTkVGQUYvNEtRL1BlVzBOK3IrYVZWc29JWTAvcXh4aWtGMlNXZHArTlJrbU1CN3k5TEJaQVZxUTR5aEdDbS9IM0gyNzBPU3lrcW1RTUtMQmhCSkRFdz09Il0sCgogICAgIkBpbWcvc2hhcnAtbGlidmlwcy1kYXJ3aW4tYXJtNjQiOiBbIkBpbWcvc2hhcnAtbGlidmlwcy1kYXJ3aW4tYXJtNjRAMS4yLjQiLCAiIiwgeyAib3MiOiAiZGFyd2luIiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi16cWpqbzdSYXRGZkZvUDBNa1E1MWpmdUZaQm5WRTJwUmlheWRLSjFHL3JIWnZuc3JIQU9jUUFMSWk5c0E1Y281eGVuUWRUdWdDdnRiMWN1Zjc4VmY0Zz09Il0sCgogICAgIkBpbWcvc2hhcnAtbGlidmlwcy1kYXJ3aW4teDY0IjogWyJAaW1nL3NoYXJwLWxpYnZpcHMtZGFyd2luLXg2NEAxLjIuNCIsICIiLCB7ICJvcyI6ICJkYXJ3aW4iLCAiY3B1IjogIng2NCIgfSwgInNoYTUxMi0xSU9kNXhmVmhsR3dYK3pYdjJOOTNrMHlNT052VWxBTnlsYkp3MWVUYWg4Sy9KdHBpMTVLQytXU2lhWC9uQm1ibTJIeFJNMWdaMG5TZGpTc3JaYkdLZz09Il0sCgogICAgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC1hcm0iOiBbIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC1hcm1AMS4yLjQiLCAiIiwgeyAib3MiOiAibGludXgiLCAiY3B1IjogImFybSIgfSwgInNoYTUxMi1iRkk3eGNLRkVMZGlOQ1ZvdjhlNDRJYTR1MmJ5QStsM1h0c0FqK1E4dGZDd082QlE4aURvallkdm9QTXFzS0RrdW9PbytYNkhaQTBzMHExMUFOTVE4QT09Il0sCgogICAgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC1hcm02NCI6IFsiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4LWFybTY0QDEuMi40IiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi1leGNqWDhEZnNJY0oxMHgxS3pyNFJjV2UxZWRDOVBxdURSUlB4M1lWQ3ZRditVNXA3WWluMnMzMmZ0emlrWG9qYjFQSUZjLzlNdDI4L3kraVJrbGtydz09Il0sCgogICAgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC1wcGM2NCI6IFsiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4LXBwYzY0QDEuMi40IiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJwcGM2NCIgfSwgInNoYTUxMi1GTXV2R2lqTERZRzZsVytiL1V2eWlsVVd1NUF5dSszcjJkMVM4bm90aUdDSXlZVS83NmVpZzFVZk1ta1o3dndnT3J6S3psUWJGU3VRZmdtN0dZVVBwQT09Il0sCgogICAgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC1yaXNjdjY0IjogWyJAaW1nL3NoYXJwLWxpYnZpcHMtbGludXgtcmlzY3Y2NEAxLjIuNCIsICIiLCB7ICJvcyI6ICJsaW51eCIsICJjcHUiOiAibm9uZSIgfSwgInNoYTUxMi1vVkRiY1I0elVDMGNlODJ0ZXViU20reDZFVGl4dEtaQmgvcWJSRUlPY0kzY1VMekR5YjE4U3IvV2N5eDdOUlFlUXpPaUhUTmJaRkYxVXdQUzJzY3lHQT09Il0sCgogICAgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC1zMzkweCI6IFsiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4LXMzOTB4QDEuMi40IiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJzMzkweCIgfSwgInNoYTUxMi1xbXA5VnJ6Z1BnTW9HWnlQdnJRSHFrMDJ1eWpBMC9RclRPMjZUcWs2bDRaVjBNUFdJVzZMVGtxT0lvditKMXlFdTdNYkZRYURwd2R3SktoYkp2dVJ4UT09Il0sCgogICAgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC14NjQiOiBbIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC14NjRAMS4yLjQiLCAiIiwgeyAib3MiOiAibGludXgiLCAiY3B1IjogIng2NCIgfSwgInNoYTUxMi10SnhpaUxzbUhjOUF4MWJ6M29hT1lCVVJUWEdJUkRPREJxaHZlVkhvbnJISjkvK2s4OXFiTGwwYmNKbnMrZTR0NHJ2YU5CeGFFWnNGdFNmQWRxdVBydz09Il0sCgogICAgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eG11c2wtYXJtNjQiOiBbIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eG11c2wtYXJtNjRAMS4yLjQiLCAiIiwgeyAib3MiOiAibGludXgiLCAiY3B1IjogImFybTY0IiB9LCAic2hhNTEyLUZWUUh1d3gxSUl1Tm93OVFBYllVekorRW44S2NWbTlMazUrdUdVUUpIYVptTUVDWm1PbGl4OUhuSDduMVRSa1hNUzBwR3hJSm9rSVZCOVN1cVpHR1h3PT0iXSwKCiAgICAiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4bXVzbC14NjQiOiBbIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eG11c2wteDY0QDEuMi40IiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJ4NjQiIH0sICJzaGE1MTItK0xweUJrN0w0NFpJWHd6L1ZZZmdsYVgvb2t4ZXpFU2M2VXhEU295bzJLczZKeGM0WTdzR2pwZ1U5czRQTWdxZ2pqMWdaQ3lsVGllTmFtcUExTUY3RGc9PSJdLAoKICAgICJAaW1nL3NoYXJwLWxpbnV4LWFybSI6IFsiQGltZy9zaGFycC1saW51eC1hcm1AMC4zNC41IiwgIiIsIHsgIm9wdGlvbmFsRGVwZW5kZW5jaWVzIjogeyAiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4LWFybSI6ICIxLjIuNCIgfSwgIm9zIjogImxpbnV4IiwgImNwdSI6ICJhcm0iIH0sICJzaGE1MTItOWRMcXN2d3RnMXV1WEJHWktzeGVtOTU5NSt1anYwc0o2Vmk4d2NUQU5TRnB3Vi9HT05hdDVlQ2t6UW8vMU82elJJa2gwbS84KzVCanJScjdqRFVTWnc9PSJdLAoKICAgICJAaW1nL3NoYXJwLWxpbnV4LWFybTY0IjogWyJAaW1nL3NoYXJwLWxpbnV4LWFybTY0QDAuMzQuNSIsICIiLCB7ICJvcHRpb25hbERlcGVuZGVuY2llcyI6IHsgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC1hcm02NCI6ICIxLjIuNCIgfSwgIm9zIjogImxpbnV4IiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi1iS1F6YUpSWS9ia1BPWHlLeDVFVnVwN3FrYW9qRUNHNk5MWXN3Z2t0T1pqYVhlY1NBZUNXaVp3d2lGZjMvWStPMUhyYXVpRTNGVnNHeEZnOGMyNHJaZz09Il0sCgogICAgIkBpbWcvc2hhcnAtbGludXgtcHBjNjQiOiBbIkBpbWcvc2hhcnAtbGludXgtcHBjNjRAMC4zNC41IiwgIiIsIHsgIm9wdGlvbmFsRGVwZW5kZW5jaWVzIjogeyAiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4LXBwYzY0IjogIjEuMi40IiB9LCAib3MiOiAibGludXgiLCAiY3B1IjogInBwYzY0IiB9LCAic2hhNTEyLTd6em53TmFxVzZZdHNmckdHREE2QlJrSVNLQUFFMUpvMFFkcE5ZWE5NSHUyKzBkVHJQZmxUTE5rcGM4bDdNVVA1TTE2WkpjVXZ5c1ZXV3JNZWZacXVBPT0iXSwKCiAgICAiQGltZy9zaGFycC1saW51eC1yaXNjdjY0IjogWyJAaW1nL3NoYXJwLWxpbnV4LXJpc2N2NjRAMC4zNC41IiwgIiIsIHsgIm9wdGlvbmFsRGVwZW5kZW5jaWVzIjogeyAiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4LXJpc2N2NjQiOiAiMS4yLjQiIH0sICJvcyI6ICJsaW51eCIsICJjcHUiOiAibm9uZSIgfSwgInNoYTUxMi01MWdKdUxQVEthN3BpWVBhVnM4R21CeW83L1U3LzdUWk9xK2NuWEpJSFpLYXZJUkhBUDc3ZTNOMkhFbDNkZ2lxZEQvdzB5VWZpSm5JSTc3UHVEREZkdz09Il0sCgogICAgIkBpbWcvc2hhcnAtbGludXgtczM5MHgiOiBbIkBpbWcvc2hhcnAtbGludXgtczM5MHhAMC4zNC41IiwgIiIsIHsgIm9wdGlvbmFsRGVwZW5kZW5jaWVzIjogeyAiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4LXMzOTB4IjogIjEuMi40IiB9LCAib3MiOiAibGludXgiLCAiY3B1IjogInMzOTB4IiB9LCAic2hhNTEyLW5RdENrMFBkS2ZobzNlQzVNcmJRb2lnSjJnZDFDZ2RkVU1rYWJVaityQmV2czh0WjJjVUxPeDQ2RTdveVgrMDRXR2ZBQmdJd21NQzBWcWllVGlSNGpnPT0iXSwKCiAgICAiQGltZy9zaGFycC1saW51eC14NjQiOiBbIkBpbWcvc2hhcnAtbGludXgteDY0QDAuMzQuNSIsICIiLCB7ICJvcHRpb25hbERlcGVuZGVuY2llcyI6IHsgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC14NjQiOiAiMS4yLjQiIH0sICJvcyI6ICJsaW51eCIsICJjcHUiOiAieDY0IiB9LCAic2hhNTEyLU1FemQ4SFBLeFZ4VmVud0FhK0pSUHdFQzdRRmpvUFd1UzVOWm5CdDZCM3B1N0VHMkdlMGlkMW9MSFpwUEpkbjNPUUsrQlFEaXc5elN0aUhCVEpRUVFRPT0iXSwKCiAgICAiQGltZy9zaGFycC1saW51eG11c2wtYXJtNjQiOiBbIkBpbWcvc2hhcnAtbGludXhtdXNsLWFybTY0QDAuMzQuNSIsICIiLCB7ICJvcHRpb25hbERlcGVuZGVuY2llcyI6IHsgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eG11c2wtYXJtNjQiOiAiMS4yLjQiIH0sICJvcyI6ICJsaW51eCIsICJjcHUiOiAiYXJtNjQiIH0sICJzaGE1MTItZnBySlI2R3RSc010Nkt5ZnE0NElzQ2hWWmVHTjk3Z1REMzMxd2VSMWV4MWMxcnlwREVBQk42VG0yeGExd0U2bFliNURkRW5rMDNOWlBxQTdJZDIxeWc9PSJdLAoKICAgICJAaW1nL3NoYXJwLWxpbnV4bXVzbC14NjQiOiBbIkBpbWcvc2hhcnAtbGludXhtdXNsLXg2NEAwLjM0LjUiLCAiIiwgeyAib3B0aW9uYWxEZXBlbmRlbmNpZXMiOiB7ICJAaW1nL3NoYXJwLWxpYnZpcHMtbGludXhtdXNsLXg2NCI6ICIxLjIuNCIgfSwgIm9zIjogImxpbnV4IiwgImNwdSI6ICJ4NjQiIH0sICJzaGE1MTItSmc4d05UMU1Vekl2aEJGeFZpcXJFaFdER3pxeW1vM3NWN3o3WnNhV2JaTkRMWFJKWm9SR3JqdWxwNjBZWXRWNHdmWThWSUtjV2lkam9qbExjV3JkOFE9PSJdLAoKICAgICJAaW1nL3NoYXJwLXdhc20zMiI6IFsiQGltZy9zaGFycC13YXNtMzJAMC4zNC41IiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIkBlbW5hcGkvcnVudGltZSI6ICJeMS43LjAiIH0sICJjcHUiOiAibm9uZSIgfSwgInNoYTUxMi1PZFdURWlWa1kyUEh3cWtiQkk4ZnJGeFFRRmVrSGFTU2tVSUprd3pjbFdaZTY0TzFYNFVsVWpxcXFMYVBiVXBNT1FrNkZCdS9IdGxHWE5ibElzMGh1dz09Il0sCgogICAgIkBpbWcvc2hhcnAtd2luMzItYXJtNjQiOiBbIkBpbWcvc2hhcnAtd2luMzItYXJtNjRAMC4zNC41IiwgIiIsIHsgIm9zIjogIndpbjMyIiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi1XUTNBZ1dDV1lTYjJ5dCtJRzhtbkM2SmRrOVdoczdPMGd4cGhibHNMdmRoU3BTVHRtdTY5WkcxR2tiNk51dnhzTkFDd2lQVjZjTlNaTnp0MEtQc3c3Zz09Il0sCgogICAgIkBpbWcvc2hhcnAtd2luMzItaWEzMiI6IFsiQGltZy9zaGFycC13aW4zMi1pYTMyQDAuMzQuNSIsICIiLCB7ICJvcyI6ICJ3aW4zMiIsICJjcHUiOiAiaWEzMiIgfSwgInNoYTUxMi1GVjltLzdObWVDbVNIREQ1ajQrNHBOSThDcDNhVytKdkxvWGNUVW8wSXF5alNmQVpKOGRJVW1pangxcWFKc0lpVStIb3N3NnhNNUtpakFXUkpDU2dOZz09Il0sCgogICAgIkBpbWcvc2hhcnAtd2luMzIteDY0IjogWyJAaW1nL3NoYXJwLXdpbjMyLXg2NEAwLjM0LjUiLCAiIiwgeyAib3MiOiAid2luMzIiLCAiY3B1IjogIng2NCIgfSwgInNoYTUxMi0rMjlZTXNxWTIvOWVGRWlXOTNlcVdudUxjV2N1Zm93WGV3d1NOSVQ2VXdaZFVVQ3JNM29Gak1XSC9aNi9UTW1iNGhsRmVubWZBVmJwV2V1cDJqcnlDdz09Il0sCgogICAgIkBqcmlkZ2V3ZWxsL2dlbi1tYXBwaW5nIjogWyJAanJpZGdld2VsbC9nZW4tbWFwcGluZ0AwLjMuMTMiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiQGpyaWRnZXdlbGwvc291cmNlbWFwLWNvZGVjIjogIl4xLjUuMCIsICJAanJpZGdld2VsbC90cmFjZS1tYXBwaW5nIjogIl4wLjMuMjQiIH0gfSwgInNoYTUxMi0ya2t0LzduaUo2TWdFUHhGMGJZZFE2ZXRaYUErZlF2RGNMS2NraHkxeUlRT3phb0tqQkJqU2o2My9hTFZqWUUzcWhSdDVkdk0rdVV5ZkNnNlVLQ0JiQT09Il0sCgogICAgIkBqcmlkZ2V3ZWxsL3JlbWFwcGluZyI6IFsiQGpyaWRnZXdlbGwvcmVtYXBwaW5nQDIuMy41IiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIkBqcmlkZ2V3ZWxsL2dlbi1tYXBwaW5nIjogIl4wLjMuNSIsICJAanJpZGdld2VsbC90cmFjZS1tYXBwaW5nIjogIl4wLjMuMjQiIH0gfSwgInNoYTUxMi1MSTl1LytsYVlHNERzMVRES1NKVzJZUHJJbGNWWU93aTJmVUM2eEI0M2x1ZUNqZ3hWNGxmZk9DWkN0WUZpSDZUTk9YK3RRS1h4OTdUNElLSGJoeUhFUT09Il0sCgogICAgIkBqcmlkZ2V3ZWxsL3Jlc29sdmUtdXJpIjogWyJAanJpZGdld2VsbC9yZXNvbHZlLXVyaUAzLjEuMiIsICIiLCB7fSwgInNoYTUxMi1iUklTZ0NJalAyMC90YldTUFdNRWk1NFFWUFJaRXhrdUQ5bEpMK1VJeFVLdHdWSkE4d1cxVHJiMWpNczFSRlhvMUNCVE5aLzVocEM5UXZtS1dkb3BLdz09Il0sCgogICAgIkBqcmlkZ2V3ZWxsL3NvdXJjZW1hcC1jb2RlYyI6IFsiQGpyaWRnZXdlbGwvc291cmNlbWFwLWNvZGVjQDEuNS41IiwgIiIsIHt9LCAic2hhNTEyLWNZUTkzMTBncnF4dWVXYmwrV3VJVUlhaVVhRGNqN1dPcTVmVmhFbGpOVmdSZk9VaFk5ZnkyelR2Zm9xV3NuZWJoOFNsNzBWU2NGYklDdkpuTEtCME9nPT0iXSwKCiAgICAiQGpyaWRnZXdlbGwvdHJhY2UtbWFwcGluZyI6IFsiQGpyaWRnZXdlbGwvdHJhY2UtbWFwcGluZ0AwLjMuMzEiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiQGpyaWRnZXdlbGwvcmVzb2x2ZS11cmkiOiAiXjMuMS4wIiwgIkBqcmlkZ2V3ZWxsL3NvdXJjZW1hcC1jb2RlYyI6ICJeMS40LjE0IiB9IH0sICJzaGE1MTItenpOUitTZFFTREp6Yzhqb2FlUDhRUW9DUXI4TnVZeDJkSUl5dGwxUWVCRVpISjl1VzZoZWJzcllnYno4aEp3VVFhbzNUV0NNdG1mVjhOdTF0d09MQXc9PSJdLAoKICAgICJAbmFwaS1ycy93YXNtLXJ1bnRpbWUiOiBbIkBuYXBpLXJzL3dhc20tcnVudGltZUAxLjEuNCIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAdHlieXMvd2FzbS11dGlsIjogIl4wLjEwLjEiIH0sICJwZWVyRGVwZW5kZW5jaWVzIjogeyAiQGVtbmFwaS9jb3JlIjogIl4xLjcuMSIsICJAZW1uYXBpL3J1bnRpbWUiOiAiXjEuNy4xIiB9IH0sICJzaGE1MTItM05RTk5nQTFZU2xKYi9rTUgxaWxkQVNQOUhXNy83a1luUkkyc3pXSmFvZmFTMWhXbWJHSTRIK2QzKzIyYUd6WFhOOUlKK24rR2lGVmNHaXBKUDE4b3c9PSJdLAoKICAgICJAbmV4dC9lbnYiOiBbIkBuZXh0L2VudkAxNi4yLjYiLCAiIiwge30sICJzaGE1MTItZ2Q4SG9ITjR1Zmo3M1dtUjNKbVZvbHJwSlI0N0lMSzZMb3VQNXhFbFBnbGFWeGlyNmUxYTdWenZUdkRXa09vUFhUOXJra1R6eUN4QnU0eWVaZlp3Y3c9PSJdLAoKICAgICJAbmV4dC9zd2MtZGFyd2luLWFybTY0IjogWyJAbmV4dC9zd2MtZGFyd2luLWFybTY0QDE2LjIuNiIsICIiLCB7ICJvcyI6ICJkYXJ3aW4iLCAiY3B1IjogImFybTY0IiB9LCAic2hhNTEyLVpKR2trY05mWWdyck1rcU9kWjd6b0xhMVRPeTBxcGNNZmsvejRNaC9GS1V6NDBnVk8rSE5RV3FtTHhmNjdaNVdCNjREUnAwZGhFYnlIZmVsKzZzSlVnPT0iXSwKCiAgICAiQG5leHQvc3djLWRhcndpbi14NjQiOiBbIkBuZXh0L3N3Yy1kYXJ3aW4teDY0QDE2LjIuNiIsICIiLCB7ICJvcyI6ICJkYXJ3aW4iLCAiY3B1IjogIng2NCIgfSwgInNoYTUxMi12L1lMQkhJWTEzMkNlZDNwdUJKN1lKS3cxbHFzQ3JnY05vMmFSSmxDRXlRcnJDZVJKbHZHbG5teGhQeE5RSTNLRTNOMURONXI5VFBOUHZrYTNucTVSUT09Il0sCgogICAgIkBuZXh0L3N3Yy1saW51eC1hcm02NC1nbnUiOiBbIkBuZXh0L3N3Yy1saW51eC1hcm02NC1nbnVAMTYuMi42IiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi1SUE92cWxZQmJjUWprejlWUVFEWjJUMmJBUklqWFpWMUtGbHQrVjJNcjZTVy9lNEk5ZmNLc2FBMGhkeWYyRkhvVGxzVjJ4bkJkNVk5MTJyUC8xQ2U2dz09Il0sCgogICAgIkBuZXh0L3N3Yy1saW51eC1hcm02NC1tdXNsIjogWyJAbmV4dC9zd2MtbGludXgtYXJtNjQtbXVzbEAxNi4yLjYiLCAiIiwgeyAib3MiOiAibGludXgiLCAiY3B1IjogImFybTY0IiB9LCAic2hhNTEyLVVSVVR1MStkTWt4SnNQRmdtK09lRXZxOXdmNXN1ancwRXZnWXk4MFRER0hUU0xUbklIZXFiMEV1OEEzc0M5NUlSZ2plalFMK2tDNG13KzR5UHhpQVhBPT0iXSwKCiAgICAiQG5leHQvc3djLWxpbnV4LXg2NC1nbnUiOiBbIkBuZXh0L3N3Yy1saW51eC14NjQtZ251QDE2LjIuNiIsICIiLCB7ICJvcyI6ICJsaW51eCIsICJjcHUiOiAieDY0IiB9LCAic2hhNTEyLURPajE4Mm1QVjhHM1VrcmF5TG9SRU01WUVZSStEazV3djdPeDl4bDFmRmliQUVMRXNGRDBsRFBmSEllSUxsdXRNTWZkeWhsellQRUxHM3BldUthdXJ3PT0iXSwKCiAgICAiQG5leHQvc3djLWxpbnV4LXg2NC1tdXNsIjogWyJAbmV4dC9zd2MtbGludXgteDY0LW11c2xAMTYuMi42IiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJ4NjQiIH0sICJzaGE1MTItSEtRNVNQL1YvdWI3M1V2RjduL3plSmx4azJrTG10TDdXenJnNFdmbWtqbU5vczVvbkoydEt1N3laT1BkTDE4QTZTdmZuM21heDI5eW0rcnk3TmtLNGc9PSJdLAoKICAgICJAbmV4dC9zd2Mtd2luMzItYXJtNjQtbXN2YyI6IFsiQG5leHQvc3djLXdpbjMyLWFybTY0LW1zdmNAMTYuMi42IiwgIiIsIHsgIm9zIjogIndpbjMyIiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi1MWlhwVGxQeVM1djdIaFNtbnZzTEdQM2lJWWdZT0JuYzhyOEFybFQ1NXNHSFY4OWJSMkhsRGRCaldRK1BZNlNKTW1rOFR1VkdGdXhhbG5QM2svMER3Zz09Il0sCgogICAgIkBuZXh0L3N3Yy13aW4zMi14NjQtbXN2YyI6IFsiQG5leHQvc3djLXdpbjMyLXg2NC1tc3ZjQDE2LjIuNiIsICIiLCB7ICJvcyI6ICJ3aW4zMiIsICJjcHUiOiAieDY0IiB9LCAic2hhNTEyLUYwKzRpMGg5SjZDNGVFM0VBUFdzb0NrN1VXL2Riek9qeXp4WTBxbkRVT1lGdTZGRm1kWjZsOTcvWGRWMy9OejNWWXlPN1VXanlFSlVYa0dxY29YZk1BPT0iXSwKCiAgICAiQG94Yy1wcm9qZWN0L3R5cGVzIjogWyJAb3hjLXByb2plY3QvdHlwZXNAMC4xMzAuMCIsICIiLCB7fSwgInNoYTUxMi1pYkQydXN4OUpSdTdmNXB1MnRNS01JNGNwQTROZ1hKUW9ZUlA0cFE3UHhtbjFsNmsvNTNxV3RRV1pheWhZeTNYNFFaa3Q5ME90K21KRWFlWG91aW82UT09Il0sCgogICAgIkByb2xsZG93bi9iaW5kaW5nLWFuZHJvaWQtYXJtNjQiOiBbIkByb2xsZG93bi9iaW5kaW5nLWFuZHJvaWQtYXJtNjRAMS4wLjEiLCAiIiwgeyAib3MiOiAiYW5kcm9pZCIsICJjcHUiOiAiYXJtNjQiIH0sICJzaGE1MTItZkpJM0kwcjNDM09qL3pkQkNwYUNtQlJaWWYwN3hwYXE0eUNmRERvU0ZtK2JlV056YklsMjZwdVc4UnJhVWR1Z29Kdy85NXplck5PbjZqYXNBaHpTbWc9PSJdLAoKICAgICJAcm9sbGRvd24vYmluZGluZy1kYXJ3aW4tYXJtNjQiOiBbIkByb2xsZG93bi9iaW5kaW5nLWRhcndpbi1hcm02NEAxLjAuMSIsICIiLCB7ICJvcyI6ICJkYXJ3aW4iLCAiY3B1IjogImFybTY0IiB9LCAic2hhNTEyLWNLbkFoV0VzVjdUUGNBLzVFQXRlRHA2S2NKWkJRMkcrQnFFN3pheU1NaTdrTXZ3UnNidjdXVDlhT25uMFdObDRTS0VJZjQzdmpTMzFpVVB1ODBuelhnPT0iXSwKCiAgICAiQHJvbGxkb3duL2JpbmRpbmctZGFyd2luLXg2NCI6IFsiQHJvbGxkb3duL2JpbmRpbmctZGFyd2luLXg2NEAxLjAuMSIsICIiLCB7ICJvcyI6ICJkYXJ3aW4iLCAiY3B1IjogIng2NCIgfSwgInNoYTUxMi1ZS3JWd1FqSVJCUG8rNUcvdTAzd0dqYmR5NHE3cHl6Q2U5M0RLOVZKN3prVm1lZzhMSjdHYmdzaUhXZFI0eFNvZTRDQVhSRDdCY2pnYnRyNjRia1hOZz09Il0sCgogICAgIkByb2xsZG93bi9iaW5kaW5nLWZyZWVic2QteDY0IjogWyJAcm9sbGRvd24vYmluZGluZy1mcmVlYnNkLXg2NEAxLjAuMSIsICIiLCB7ICJvcyI6ICJmcmVlYnNkIiwgImNwdSI6ICJ4NjQiIH0sICJzaGE1MTItei9vQnNSRW80NlNzRnFCd1l0RmUwa3BKZUJpakFUNDhPL1dYTEk0c3VpQ0xCa3IwM1JUdFRKTUN6U2REZDJ6bmxoOFZKaXpMMDlYVmtRZ2s4SVpvbnc9PSJdLAoKICAgICJAcm9sbGRvd24vYmluZGluZy1saW51eC1hcm0tZ251ZWFiaWhmIjogWyJAcm9sbGRvd24vYmluZGluZy1saW51eC1hcm0tZ251ZWFiaWhmQDEuMC4xIiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJhcm0iIH0sICJzaGE1MTItaWs4cTdHTTExenh2WXhGYzJQZURjVDZUQnZoQ1FNYVV4ZnBoL001bDlzS3VUcy9TamczTCtCeXcwRjd3MFpWTEJabXgzMFArZ0cwRUN6ek4rTUZjbVE9PSJdLAoKICAgICJAcm9sbGRvd24vYmluZGluZy1saW51eC1hcm02NC1nbnUiOiBbIkByb2xsZG93bi9iaW5kaW5nLWxpbnV4LWFybTY0LWdudUAxLjAuMSIsICIiLCB7ICJvcyI6ICJsaW51eCIsICJjcHUiOiAiYXJtNjQiIH0sICJzaGE1MTItUW9TeDJFa3lycmRaNmtjeUU4c3RxWjYydDBZcmE4RnM1aWE5bE94SnJoNlRNUUpLN2dRS21zY2RUSGY3cE9YS1JFS3JWd090SmNRRzNxVlNmYzg2NkE9PSJdLAoKICAgICJAcm9sbGRvd24vYmluZGluZy1saW51eC1hcm02NC1tdXNsIjogWyJAcm9sbGRvd24vYmluZGluZy1saW51eC1hcm02NC1tdXNsQDEuMC4xIiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi11d053RnB3S2VOaVphd2ZBV0JnZzBWSXp0UFRWM2loaGgxdlYzMzRoOWl2bk5Mb3J4blFNVTZGejh3RzFaYjRRaDlMQzEvTWtjeVQzWWxEWEczUnNnZz09Il0sCgogICAgIkByb2xsZG93bi9iaW5kaW5nLWxpbnV4LXBwYzY0LWdudSI6IFsiQHJvbGxkb3duL2JpbmRpbmctbGludXgtcHBjNjQtZ251QDEuMC4xIiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJwcGM2NCIgfSwgInNoYTUxMi16WTFidWw3T1dyN0RGQmlKKyt3b2ZYdm5yOEI0NWNlM1FzUVVoS3JJaFhzeWdBaDdiVGt3eWVNMWJpMWEyZzVDL3lDL044VFp5R0RFb01mbS9sOW1wZz09Il0sCgogICAgIkByb2xsZG93bi9iaW5kaW5nLWxpbnV4LXMzOTB4LWdudSI6IFsiQHJvbGxkb3duL2JpbmRpbmctbGludXgtczM5MHgtZ251QDEuMC4xIiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJzMzkweCIgfSwgInNoYTUxMi0wZnJsc1QvZjRGdDZJN1NNRVNUS25GM2Nac2RpY1FuMWRDTWtGL2pUOXdETEUrZ0dvaVFmdjFubVQ5ZStzN3MvZmVrdnZ5NnRaTTJqSHZJMnRrYkpEUT09Il0sCgogICAgIkByb2xsZG93bi9iaW5kaW5nLWxpbnV4LXg2NC1nbnUiOiBbIkByb2xsZG93bi9iaW5kaW5nLWxpbnV4LXg2NC1nbnVAMS4wLjEiLCAiIiwgeyAib3MiOiAibGludXgiLCAiY3B1IjogIng2NCIgfSwgInNoYTUxMi1YQUJWbUdwOVRnMFdzcFRWdndkdVRjNGZwcXk2Sm5BVXJTUWU2T3V5cUQvMDNuSTdyME85T1dVa01Jd0ZyaktBSXFvbHZxb0E0WnJKcHBnd0UwR3htdz09Il0sCgogICAgIkByb2xsZG93bi9iaW5kaW5nLWxpbnV4LXg2NC1tdXNsIjogWyJAcm9sbGRvd24vYmluZGluZy1saW51eC14NjQtbXVzbEAxLjAuMSIsICIiLCB7ICJvcyI6ICJsaW51eCIsICJjcHUiOiAieDY0IiB9LCAic2hhNTEyLWJWNGZ6c3d1elZjS0Q5MG8vVk02UXFLeG54bERxMGcyQklTRExOVm14cm5ocHYxRERieVBoQ0lqWWZ2ellMVitNdmtLS25RdDJRNkFPODZTRUJVTFVRPT0iXSwKCiAgICAiQHJvbGxkb3duL2JpbmRpbmctb3Blbmhhcm1vbnktYXJtNjQiOiBbIkByb2xsZG93bi9iaW5kaW5nLW9wZW5oYXJtb255LWFybTY0QDEuMC4xIiwgIiIsIHsgIm9zIjogIm5vbmUiLCAiY3B1IjogImFybTY0IiB9LCAic2hhNTEyLS9NaDBaaHEzT1A3ZlZzMGtjUUhaUDZsWkV0aE1HVGFTZjhVQlFZU0ZFWkRXR1hYbEVDK25KNkVxZW5hSzJ0NExCWE1lM0ErSy9HMkJWWFhkdE9yNFBRPT0iXSwKCiAgICAiQHJvbGxkb3duL2JpbmRpbmctd2FzbTMyLXdhc2kiOiBbIkByb2xsZG93bi9iaW5kaW5nLXdhc20zMi13YXNpQDEuMC4xIiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIkBlbW5hcGkvY29yZSI6ICIxLjEwLjAiLCAiQGVtbmFwaS9ydW50aW1lIjogIjEuMTAuMCIsICJAbmFwaS1ycy93YXNtLXJ1bnRpbWUiOiAiXjEuMS40IiB9LCAiY3B1IjogIm5vbmUiIH0sICJzaGE1MTItKzF4YzlYNDVsOHVmc0JBbTZHanZ4MnFEUklZOWxUVnQwY2dXTmNKKzFnZGhYdmtieGVQQTYweVJUd1NUdVhMMDlDTWh5Sm1qcFY3RTNOb3l4YnFGUVE9PSJdLAoKICAgICJAcm9sbGRvd24vYmluZGluZy13aW4zMi1hcm02NC1tc3ZjIjogWyJAcm9sbGRvd24vYmluZGluZy13aW4zMi1hcm02NC1tc3ZjQDEuMC4xIiwgIiIsIHsgIm9zIjogIndpbjMyIiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi0xRCtVcVpkZm51UitKeTFHZ01Kd2k4NWJENDBIMjF1Tm1PUFJXUWh3NG9SU3VvbFovQjVyaXhaNDVESzJLWE9UQ3ZtVkNlY2F1V2dFaGJ3OGJJN3RPdz09Il0sCgogICAgIkByb2xsZG93bi9iaW5kaW5nLXdpbjMyLXg2NC1tc3ZjIjogWyJAcm9sbGRvd24vYmluZGluZy13aW4zMi14NjQtbXN2Y0AxLjAuMSIsICIiLCB7ICJvcyI6ICJ3aW4zMiIsICJjcHUiOiAieDY0IiB9LCAic2hhNTEyLUlOQXljYVd1aGxPSzN3azRtUkhHc2Rnd1lXbWQ5Y0NoZFBkRTlid1dteTZybjlWcVZOWU5GR2hPZFhyb2ZYVXh3SEluY1NpUE5iOHRObThrbkRWSWVRPT0iXSwKCiAgICAiQHJvbGxkb3duL3BsdWdpbnV0aWxzIjogWyJAcm9sbGRvd24vcGx1Z2ludXRpbHNAMS4wLjEiLCAiIiwge30sICJzaGE1MTItMmo5Ykd0NUpoOGhqK3ZQdGd6UHRsNzJqMHlSeEhBeXVtb282VE5mQWpzTEIwNFV0cFN2UGJQY0RjQk14ejduKzlDWUIwYzFHeFFGeFlSZzJqaW1xR3c9PSJdLAoKICAgICJAc3RhbmRhcmQtc2NoZW1hL3NwZWMiOiBbIkBzdGFuZGFyZC1zY2hlbWEvc3BlY0AxLjEuMCIsICIiLCB7fSwgInNoYTUxMi1sMmFGeTVqQUxobmlHNUhncXJENmpYTGkvclVXckt2cU4vcUp4NnlvSnNnS2hibFZkK2lxcVU0UkNYYXZtL2pQaXR5RG81VEN2S01ucGpLbk9yaXkwdz09Il0sCgogICAgIkBzd2MvaGVscGVycyI6IFsiQHN3Yy9oZWxwZXJzQDAuNS4xNSIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJ0c2xpYiI6ICJeMi44LjAiIH0gfSwgInNoYTUxMi1KUTVUdU1pNDVPd2k0L0JJTUFKQm9TUW9PSnUxMm9Pay9nQURxbGNVTDlKRWRIQjh2eWpVU3N4cWVOWG5tWEhqWUtNaTJXY1l0ZXpHRUVocVVJL0UyZz09Il0sCgogICAgIkB0YWJieV9haS9oaWpyaS1jb252ZXJ0ZXIiOiBbIkB0YWJieV9haS9oaWpyaS1jb252ZXJ0ZXJAMS4wLjUiLCAiIiwge30sICJzaGE1MTItcjViQ2xLcmNJdXNEb28wNDlkU0w4Q2F3bkhSNm1SZER3aGxRdUlnWlJOdHk2OHEweDhrM0xmMUJ0UEFNeFJmL0dnbkhCbklPNHVqZDMrR1FkTFd6eFE9PSJdLAoKICAgICJAdGFpbHdpbmRjc3Mvbm9kZSI6IFsiQHRhaWx3aW5kY3NzL25vZGVANC4zLjAiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiQGpyaWRnZXdlbGwvcmVtYXBwaW5nIjogIl4yLjMuNSIsICJlbmhhbmNlZC1yZXNvbHZlIjogIl41LjIxLjAiLCAiaml0aSI6ICJeMi42LjEiLCAibGlnaHRuaW5nY3NzIjogIjEuMzIuMCIsICJtYWdpYy1zdHJpbmciOiAiXjAuMzAuMjEiLCAic291cmNlLW1hcC1qcyI6ICJeMS4yLjEiLCAidGFpbHdpbmRjc3MiOiAiNC4zLjAiIH0gfSwgInNoYTUxMi1hRmI0Z1VoRk9nZGg5QVhvNEl6QkVPekJra0F4bTlWaWd3REpuTUlZdjNsY2ZYQ0pWZXNOZmJFYUJsNEJOZ1ZSeWlkOTJBbWR2aXF3QlVCUktTZVkzZz09Il0sCgogICAgIkB0YWlsd2luZGNzcy9veGlkZSI6IFsiQHRhaWx3aW5kY3NzL294aWRlQDQuMy4wIiwgIiIsIHsgIm9wdGlvbmFsRGVwZW5kZW5jaWVzIjogeyAiQHRhaWx3aW5kY3NzL294aWRlLWFuZHJvaWQtYXJtNjQiOiAiNC4zLjAiLCAiQHRhaWx3aW5kY3NzL294aWRlLWRhcndpbi1hcm02NCI6ICI0LjMuMCIsICJAdGFpbHdpbmRjc3Mvb3hpZGUtZGFyd2luLXg2NCI6ICI0LjMuMCIsICJAdGFpbHdpbmRjc3Mvb3hpZGUtZnJlZWJzZC14NjQiOiAiNC4zLjAiLCAiQHRhaWx3aW5kY3NzL294aWRlLWxpbnV4LWFybS1nbnVlYWJpaGYiOiAiNC4zLjAiLCAiQHRhaWx3aW5kY3NzL294aWRlLWxpbnV4LWFybTY0LWdudSI6ICI0LjMuMCIsICJAdGFpbHdpbmRjc3Mvb3hpZGUtbGludXgtYXJtNjQtbXVzbCI6ICI0LjMuMCIsICJAdGFpbHdpbmRjc3Mvb3hpZGUtbGludXgteDY0LWdudSI6ICI0LjMuMCIsICJAdGFpbHdpbmRjc3Mvb3hpZGUtbGludXgteDY0LW11c2wiOiAiNC4zLjAiLCAiQHRhaWx3aW5kY3NzL294aWRlLXdhc20zMi13YXNpIjogIjQuMy4wIiwgIkB0YWlsd2luZGNzcy9veGlkZS13aW4zMi1hcm02NC1tc3ZjIjogIjQuMy4wIiwgIkB0YWlsd2luZGNzcy9veGlkZS13aW4zMi14NjQtbXN2YyI6ICI0LjMuMCIgfSB9LCAic2hhNTEyLUY3SFpHQmVOOUkwL0F1dUpTNVB3Y0Q4eGF5eDVyaTVHaGpZVURCRVZZVWtleHlBL2dpd2JETmpSVnJ4U2V6RTNUMjUwT1UySy93cC9sdFd4M1VPZWZnPT0iXSwKCiAgICAiQHRhaWx3aW5kY3NzL294aWRlLWFuZHJvaWQtYXJtNjQiOiBbIkB0YWlsd2luZGNzcy9veGlkZS1hbmRyb2lkLWFybTY0QDQuMy4wIiwgIiIsIHsgIm9zIjogImFuZHJvaWQiLCAiY3B1IjogImFybTY0IiB9LCAic2hhNTEyLVRKUGlxNjd0S2xMdU9iUDZSa3d2VkdEb3hDTUJWdERnS2tMZmEvdXlqNy9GeXh2UXdIUytVT25WclhYZ2JFc2ZVYU1naVZ2QzRLYkpuUnIyNmhvNE5nPT0iXSwKCiAgICAiQHRhaWx3aW5kY3NzL294aWRlLWRhcndpbi1hcm02NCI6IFsiQHRhaWx3aW5kY3NzL294aWRlLWRhcndpbi1hcm02NEA0LjMuMCIsICIiLCB7ICJvcyI6ICJkYXJ3aW4iLCAiY3B1IjogImFybTY0IiB9LCAic2hhNTEyLW9NTi9XWlJiK1NPMzdCbVVFbEVnZUVXdVU4RS9IWFJraU9EeEp4TGUxVVRIVlhMcmRWU2dmYUpWN3BTbGhSR01TT2lYTHV4VElqZnNGM3dZdno4Y2dRPT0iXSwKCiAgICAiQHRhaWx3aW5kY3NzL294aWRlLWRhcndpbi14NjQiOiBbIkB0YWlsd2luZGNzcy9veGlkZS1kYXJ3aW4teDY0QDQuMy4wIiwgIiIsIHsgIm9zIjogImRhcndpbiIsICJjcHUiOiAieDY0IiB9LCAic2hhNTEyLU42Q1VtdTRhNmJLVkFEZnc3N3AraXc2WWQ5UTNPQmhlMHZlYURYK1FhemZ1VllsUXNIZkRneEJyc2pRL0lXK3p5d0w4bVRyTmQwU2RKVC96Z3R2TWRBPT0iXSwKCiAgICAiQHRhaWx3aW5kY3NzL294aWRlLWZyZWVic2QteDY0IjogWyJAdGFpbHdpbmRjc3Mvb3hpZGUtZnJlZWJzZC14NjRANC4zLjAiLCAiIiwgeyAib3MiOiAiZnJlZWJzZCIsICJjcHUiOiAieDY0IiB9LCAic2hhNTEyLXpETDVoQmtRZEg1QzZNcHFiSzNnUUFnUDgwdHNNd1NJMjZ2ak96akp0TkNNVW8wbEZnT0l0ekhLQkl1cE9aTlF4dDNvdVBIN1JQaHZOaGlUZkNlNUNRPT0iXSwKCiAgICAiQHRhaWx3aW5kY3NzL294aWRlLWxpbnV4LWFybS1nbnVlYWJpaGYiOiBbIkB0YWlsd2luZGNzcy9veGlkZS1saW51eC1hcm0tZ251ZWFiaWhmQDQuMy4wIiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJhcm0iIH0sICJzaGE1MTItUjA2SGROaTdBN09Fb01zZjZkNHRqWjcxUkNXblpRUEhqMm1ub3RTRlVSak5MZEJDK2NJZ1hRN2w4MUNxZW9pUWZ0amY2T09ibHhYTUluTWdOMlZ6TUE9PSJdLAoKICAgICJAdGFpbHdpbmRjc3Mvb3hpZGUtbGludXgtYXJtNjQtZ251IjogWyJAdGFpbHdpbmRjc3Mvb3hpZGUtbGludXgtYXJtNjQtZ251QDQuMy4wIiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi1xVEpIRUxYOGpldGpoUlFIQ0xpbGtWTG15YnB6TlFBdGFJL2dhb1ZvaWRuL3VmYk5EYkFvOEtsSzJKK3lQb2M4d1F4dkR4Q21oLzVscjhuQzErbFRiZz09Il0sCgogICAgIkB0YWlsd2luZGNzcy9veGlkZS1saW51eC1hcm02NC1tdXNsIjogWyJAdGFpbHdpbmRjc3Mvb3hpZGUtbGludXgtYXJtNjQtbXVzbEA0LjMuMCIsICIiLCB7ICJvcyI6ICJsaW51eCIsICJjcHUiOiAiYXJtNjQiIH0sICJzaGE1MTItWjZzdWtpUXNuZ25XTytsMzlYNHBQYmlXVDgxSUMrUExLRitQSHhJbHlaYkdOYjlNT0RmWWxYRVZsRnZlajVCT1pJbldYMDFrVnl6ZUx2SHNYaGZjelE9PSJdLAoKICAgICJAdGFpbHdpbmRjc3Mvb3hpZGUtbGludXgteDY0LWdudSI6IFsiQHRhaWx3aW5kY3NzL294aWRlLWxpbnV4LXg2NC1nbnVANC4zLjAiLCAiIiwgeyAib3MiOiAibGludXgiLCAiY3B1IjogIng2NCIgfSwgInNoYTUxMi1EUk5kUVJwU0d6UkdmQVJWdVZreHZNOFExMm5oMTlsNEJGL0c3ekdBMW9lKzl3Y0M2c2FGQkhUSVNycEljS3poaVh0U3JsU3JsdUNmdk11bGVkb0NUUT09Il0sCgogICAgIkB0YWlsd2luZGNzcy9veGlkZS1saW51eC14NjQtbXVzbCI6IFsiQHRhaWx3aW5kY3NzL294aWRlLWxpbnV4LXg2NC1tdXNsQDQuMy4wIiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJ4NjQiIH0sICJzaGE1MTItWjBJQURiRG84Ymg2STdoMklRTXg2MDFBZFhCTGZGcEVkVW90ZnQ4NmV2ZC84WlBmbFplOUNPUE84UTF2dytwZkxXSVVvOXpOL0pHWnZ3dUFKcWR1cWc9PSJdLAoKICAgICJAdGFpbHdpbmRjc3Mvb3hpZGUtd2FzbTMyLXdhc2kiOiBbIkB0YWlsd2luZGNzcy9veGlkZS13YXNtMzItd2FzaUA0LjMuMCIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAZW1uYXBpL2NvcmUiOiAiXjEuMTAuMCIsICJAZW1uYXBpL3J1bnRpbWUiOiAiXjEuMTAuMCIsICJAZW1uYXBpL3dhc2ktdGhyZWFkcyI6ICJeMS4yLjEiLCAiQG5hcGktcnMvd2FzbS1ydW50aW1lIjogIl4xLjEuNCIsICJAdHlieXMvd2FzbS11dGlsIjogIl4wLjEwLjEiLCAidHNsaWIiOiAiXjIuOC4xIiB9LCAiY3B1IjogIm5vbmUiIH0sICJzaGE1MTItSE5aR09VeEVtRWxrc1lSN1M2c0M1alRlTkdwb2JBc3k5dTdHdTBBc2tKOC8yMEZSOUdxZWJVeUIrSEJjVS9heDZCSHVpdUppK09kYTRCK1lYNkgxeUE9PSJdLAoKICAgICJAdGFpbHdpbmRjc3Mvb3hpZGUtd2luMzItYXJtNjQtbXN2YyI6IFsiQHRhaWx3aW5kY3NzL294aWRlLXdpbjMyLWFybTY0LW1zdmNANC4zLjAiLCAiIiwgeyAib3MiOiAid2luMzIiLCAiY3B1IjogImFybTY0IiB9LCAic2hhNTEyLVBlK1JQVlRpMVQrcXltdXVScGNkdndTVlpqbmxsL2Y3bjhnQnhNTWgzeExUY3RNREtxcGRmR2ltYk15aW9xdExoVVlaeGRKOXdHTmhWN01LSHZnWnNRPT0iXSwKCiAgICAiQHRhaWx3aW5kY3NzL294aWRlLXdpbjMyLXg2NC1tc3ZjIjogWyJAdGFpbHdpbmRjc3Mvb3hpZGUtd2luMzIteDY0LW1zdmNANC4zLjAiLCAiIiwgeyAib3MiOiAid2luMzIiLCAiY3B1IjogIng2NCIgfSwgInNoYTUxMi1NdnJmMmtYVy95ZVcvT1RlelpsQ0dPaXJYUmNVdUxJQngvNVkxMkJhUE03d0pvcnlHNmRmUy9OSkw4YUJQcXRURXgvVm00VDR2S3pGVWNLRFQrVEtVQT09Il0sCgogICAgIkB0YWlsd2luZGNzcy9wb3N0Y3NzIjogWyJAdGFpbHdpbmRjc3MvcG9zdGNzc0A0LjMuMCIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAYWxsb2MvcXVpY2stbHJ1IjogIl41LjIuMCIsICJAdGFpbHdpbmRjc3Mvbm9kZSI6ICI0LjMuMCIsICJAdGFpbHdpbmRjc3Mvb3hpZGUiOiAiNC4zLjAiLCAicG9zdGNzcyI6ICJeOC41LjEwIiwgInRhaWx3aW5kY3NzIjogIjQuMy4wIiB9IH0sICJzaGE1MTItSm0wNVRqeCs5eUNMR3Y1cXcxYys4NFBzZHM4TW55ckVRWUNCK0ZGazJsZ0dpVWpsUnFkeGtlNG1WVHVZcmoyeG5WWnFLaW0yQXByNXlTdVFSWUF3L3c9PSJdLAoKICAgICJAdHlieXMvd2FzbS11dGlsIjogWyJAdHlieXMvd2FzbS11dGlsQDAuMTAuMiIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJ0c2xpYiI6ICJeMi40LjAiIH0gfSwgInNoYTUxMi1Sb0J2SjJYMHd1S2xXRklqcndmZkd3MUlxWkhLUXF6SWNoS2FhZFpaZm5OcHNBWXAybU0waDM2SnRQQ2pOREFIR2dZZXovMTV1TUJwZkd3Y2hoaU1nZz09Il0sCgogICAgIkB0eXBlcy9jaGFpIjogWyJAdHlwZXMvY2hhaUA1LjIuMyIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAdHlwZXMvZGVlcC1lcWwiOiAiKiIsICJhc3NlcnRpb24tZXJyb3IiOiAiXjIuMC4xIiB9IH0sICJzaGE1MTItTXc1NThvZUE5ZkZidjY1L3k0bUh0WERzOWJQbkZNWkFML2p4ZFBGVXBPSEhJWFg5MW1jZ0VIYlM1TGFocitwd1pGUjhBN0dRbGVSV2VJNmNHRkMyVUE9PSJdLAoKICAgICJAdHlwZXMvZGVlcC1lcWwiOiBbIkB0eXBlcy9kZWVwLWVxbEA0LjAuMiIsICIiLCB7fSwgInNoYTUxMi1jOWg5ZFZWTWlnTVBjNGJ3VHZDNWR4cXRxSlp3UVBlUHNXalBscFNPbm9qYm9yNnBHcWRrNTQxbGZBN0FxRlFyNXBCMUJSZHEwanVZOWRiODFCd3lGdz09Il0sCgogICAgIkB0eXBlcy9lc3RyZWUiOiBbIkB0eXBlcy9lc3RyZWVAMS4wLjkiLCAiIiwge30sICJzaGE1MTItR2hkUGd5MWVsNC9JbVAwNVgwNVV3NGN3Mi9NOTNCQ1VtbkV2V1pOU3RsQ3pFS01FNEZraytZcG9BNU9pSE5RbW9TN0NhZmI4WGEzUHlhOG0xUXJ6ZWc9PSJdLAoKICAgICJAdHlwZXMvbm9kZSI6IFsiQHR5cGVzL25vZGVAMjIuMTkuMTkiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAidW5kaWNpLXR5cGVzIjogIn42LjIxLjAiIH0gfSwgInNoYTUxMi1keWgveE8yRmg1YllyZldhYXFHclJRUUdrTmRtWXc2QW1hQVV2WWVVTU5UV1F0dmI3OTZpa0xkbVRjaFJtT2xPaUlKMVREWGZXZ1Z4MVFrVWxRNkhldz09Il0sCgogICAgIkB0eXBlcy9yZWFjdCI6IFsiQHR5cGVzL3JlYWN0QDE5LjIuMTUiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiY3NzdHlwZSI6ICJeMy4yLjIiIH0gfSwgInNoYTUxMi1lUndjR05IdmUrRThxdEVRU1NSbDZ1cmgrckZvcDR2OGdtNk84ckd2MjVDb2RidkZkTGpBMXZWUTFLa2lGRTB3MFVQT25iOHREaUZLTDVscDBydFk1UT09Il0sCgogICAgIkB0eXBlcy9yZWFjdC1kb20iOiBbIkB0eXBlcy9yZWFjdC1kb21AMTkuMi4zIiwgIiIsIHsgInBlZXJEZXBlbmRlbmNpZXMiOiB7ICJAdHlwZXMvcmVhY3QiOiAiXjE5LjIuMCIgfSB9LCAic2hhNTEyLWpwMkwvZVk2Zm4rS2dWVlFBT3FZSXRiRjBWWS9ZQXBlNU16MkYwYXlrU084Z3gzMWJZQ1p5dlNlWXhDSEt2ekhHNWVaamMrenlhUzVCckJXeWEyK2tRPT0iXSwKCiAgICAiQHZpdGVzdC9leHBlY3QiOiBbIkB2aXRlc3QvZXhwZWN0QDQuMS42IiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIkBzdGFuZGFyZC1zY2hlbWEvc3BlYyI6ICJeMS4xLjAiLCAiQHR5cGVzL2NoYWkiOiAiXjUuMi4yIiwgIkB2aXRlc3Qvc3B5IjogIjQuMS42IiwgIkB2aXRlc3QvdXRpbHMiOiAiNC4xLjYiLCAiY2hhaSI6ICJeNi4yLjIiLCAidGlueXJhaW5ib3ciOiAiXjMuMS4wIiB9IH0sICJzaGE1MTItN0VIRHF1UHRoQUxTVjBqaGhqZ0VXOEZYYXZpTXg3clNxdThXNm9xQ29BdU9oS292ODE0UDk5UURWMXB4TUEzUVB2MjFZdWR2Sm5nSWhqck5JNG9wTGc9PSJdLAoKICAgICJAdml0ZXN0L21vY2tlciI6IFsiQHZpdGVzdC9tb2NrZXJANC4xLjYiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiQHZpdGVzdC9zcHkiOiAiNC4xLjYiLCAiZXN0cmVlLXdhbGtlciI6ICJeMy4wLjMiLCAibWFnaWMtc3RyaW5nIjogIl4wLjMwLjIxIiB9LCAicGVlckRlcGVuZGVuY2llcyI6IHsgIm1zdyI6ICJeMi40LjkiLCAidml0ZSI6ICJeNi4wLjAgfHwgXjcuMC4wIHx8IF44LjAuMCIgfSwgIm9wdGlvbmFsUGVlcnMiOiBbIm1zdyIsICJ2aXRlIl0gfSwgInNoYTUxMi1NQ0ZjNjNjek1qRUluT2xjWTJjcFFDdkNOK0tnYkFuKzYweHU5Y01nUDRzS2FMQzVKTkFLdzdKSDhRZEFub0FDODhoVzFJaVNOWitHZ1ZYbE4xVWNNUT09Il0sCgogICAgIkB2aXRlc3QvcHJldHR5LWZvcm1hdCI6IFsiQHZpdGVzdC9wcmV0dHktZm9ybWF0QDQuMS42IiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgInRpbnlyYWluYm93IjogIl4zLjEuMCIgfSB9LCAic2hhNTEyLWg1U3hEL0l6TmhaWW5yU1pSc1VaUUlDK3ZEMEdZOGNVdnEwaXdzbWtGS2l4UkNLTExXcUNYYS9GSVE0UzFSK3NJK1BHb29qa0hzZE5yYlppTTlRcGd3PT0iXSwKCiAgICAiQHZpdGVzdC9ydW5uZXIiOiBbIkB2aXRlc3QvcnVubmVyQDQuMS42IiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIkB2aXRlc3QvdXRpbHMiOiAiNC4xLjYiLCAicGF0aGUiOiAiXjIuMC4zIiB9IH0sICJzaGE1MTItbk9QQ21uMit5RDBaTm1LZHNYR3YvVXhNTVdiTXVLZUQ2R3lZbmNOd2RrWUR4cFF2clBTS1lqMnJXdURqQzJZNGI2dzZoamlwNWRCS0Z6RVV1WmUzdkE9PSJdLAoKICAgICJAdml0ZXN0L3NuYXBzaG90IjogWyJAdml0ZXN0L3NuYXBzaG90QDQuMS42IiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIkB2aXRlc3QvcHJldHR5LWZvcm1hdCI6ICI0LjEuNiIsICJAdml0ZXN0L3V0aWxzIjogIjQuMS42IiwgIm1hZ2ljLXN0cmluZyI6ICJeMC4zMC4yMSIsICJwYXRoZSI6ICJeMi4wLjMiIH0gfSwgInNoYTUxMi1ZaHNkRTZ4QVZmVERtemp4TDJaRFV2amorWnNneU9LZStUZFF6cWtENzJ3SU9tSGthOE51R1E2TnBUTlp2OUQyWjYzZmJ3V0tKUGVWcEV3NEVRZ1l4dz09Il0sCgogICAgIkB2aXRlc3Qvc3B5IjogWyJAdml0ZXN0L3NweUA0LjEuNiIsICIiLCB7fSwgInNoYTUxMi1KRkt4TXg2dWRod0toL0xkbzI3MGUxN1FYNzEwdmd1bk1rdVBBdlhqSFN2QzZvcUxXQUhoVmhqZy9JNzFxMHUwQ0JTRXJJT0RWMUtqdjBGUU5TV2pkZz09Il0sCgogICAgIkB2aXRlc3QvdXRpbHMiOiBbIkB2aXRlc3QvdXRpbHNANC4xLjYiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiQHZpdGVzdC9wcmV0dHktZm9ybWF0IjogIjQuMS42IiwgImNvbnZlcnQtc291cmNlLW1hcCI6ICJeMi4wLjAiLCAidGlueXJhaW5ib3ciOiAiXjMuMS4wIiB9IH0sICJzaGE1MTItRnhJWStVODFSM0xHS0N4YUhIRlJRNStnNi9pUmdHTG1lSFdkcDJBbWo0bGpRUnJFSVdIbVp5RGZEWUJSWmxweXFBN3FLeHRTOUREMWRoazhSblJJVlE9PSJdLAoKICAgICJhbnNpLXJlZ2V4IjogWyJhbnNpLXJlZ2V4QDUuMC4xIiwgIiIsIHt9LCAic2hhNTEyLXF1SlFYbFRTVUdMMkxIOVNVWG84VndzWTRzb2FuaGdvNkxOU204NEUxTEJjRThzM08wd3BkaVJ6eVI5ei9aWkpNbE1XdjM3cU9PYjlwZEpsTVVFS0ZRPT0iXSwKCiAgICAiYW5zaS1zdHlsZXMiOiBbImFuc2ktc3R5bGVzQDQuMy4wIiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgImNvbG9yLWNvbnZlcnQiOiAiXjIuMC4xIiB9IH0sICJzaGE1MTItemJCOXJDSkFUMXJiamlWRGIyaHFLRkhOWUx4Z3RrOE5VUnhaM0lad0QzRjZOdHhiWFpRQ25uU2kxTGt4K0lEb2hkUGxGcDIyMndWQUxJaGVaSlFTRWc9PSJdLAoKICAgICJhc3NlcnRpb24tZXJyb3IiOiBbImFzc2VydGlvbi1lcnJvckAyLjAuMSIsICIiLCB7fSwgInNoYTUxMi1Jemk4UlFjZmZxQ2VOVmdGaWdLbGkxc3NrbElicEhuQ1ljNkFrblhHWW9CNmdySnF5ZWJ5N2p2MTJKVVFnbVRBbklEbmJjazF1eGtzVDRkek4zUFdCQT09Il0sCgogICAgImJhc2VsaW5lLWJyb3dzZXItbWFwcGluZyI6IFsiYmFzZWxpbmUtYnJvd3Nlci1tYXBwaW5nQDIuMTAuMzEiLCAiIiwgeyAiYmluIjogeyAiYmFzZWxpbmUtYnJvd3Nlci1tYXBwaW5nIjogImRpc3QvY2xpLmNqcyIgfSB9LCAic2hhNTEyLU11allPM2VQNzJ1dm1TRTBpNHdsdHNvZFJmSXBaQVRQM2p2elJOUkdHeGd6SWQ3YVZvY1ZKSlYzbmYwMXFuenpLRkd4UVZDOWJwV3hsNWNqeFRyLzdRPT0iXSwKCiAgICAiY2FuaXVzZS1saXRlIjogWyJjYW5pdXNlLWxpdGVAMS4wLjMwMDAxNzkzIiwgIiIsIHt9LCAic2hhNTEyLWl3U3NZV2FDT29oMjZjVjhOd05SVmlIbHJmVXZZc0hEZlJWY2J0bXcwS2c2UEpJWlpYd01rajE0NDJGWUxCR2tlVWYxanVBc1UzRFRmeFc1NzltclBBPT0iXSwKCiAgICAiY2hhaSI6IFsiY2hhaUA2LjIuMiIsICIiLCB7fSwgInNoYTUxMi1OVVBSbHVPZk9pVEtCS3ZXUHRTRDRQaEZ2V0NxT2kwQkdTdE5XczU3WDlqczdYR1RwclNtRm96NUYwdFdoUjRXUGpOZVI5alhxZEM3L1VwU0pUbmxSZz09Il0sCgogICAgImNoYWxrIjogWyJjaGFsa0A0LjEuMiIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJhbnNpLXN0eWxlcyI6ICJeNC4xLjAiLCAic3VwcG9ydHMtY29sb3IiOiAiXjcuMS4wIiB9IH0sICJzaGE1MTItb0tuYmhGeVJJWHBVdWV6OGlCTW15RWE0bmJqNElPUXl1aGMvd3k5a1k3L1dWUGN3SU85VkE2NjhQdThSa083KzBHNzZTTFJPZXl3OUNwUTA2MWk0bUE9PSJdLAoKICAgICJjbGllbnQtb25seSI6IFsiY2xpZW50LW9ubHlAMC4wLjEiLCAiIiwge30sICJzaGE1MTItSVYzT3UwalNNelpyZDNwWjQ4bkxrVDlEQTdBZzFwblB6YWlRaHBXN2MzUmJjcXF6dnp6VnUrTDhnZnFNcC84SU0yTVF0U2lxYUN4cnJjZnU4SThyTUE9PSJdLAoKICAgICJjbGl1aSI6IFsiY2xpdWlAOC4wLjEiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAic3RyaW5nLXdpZHRoIjogIl40LjIuMCIsICJzdHJpcC1hbnNpIjogIl42LjAuMSIsICJ3cmFwLWFuc2kiOiAiXjcuMC4wIiB9IH0sICJzaGE1MTItQlNlTm55dXM3NUM0Ly9OUTlnUXQxL2NzVFh5by84U2IrYWZMQWt6QXB0RnVNc29kOUhGb2tHTnVkWnBpL29RVjczaG5WSytzUis1UFZSTWQrRHI3WVE9PSJdLAoKICAgICJjbHN4IjogWyJjbHN4QDIuMS4xIiwgIiIsIHt9LCAic2hhNTEyLWVZbTBRV0J0VXJCV1pXRzBkMzg2T0dBdzE2Wjk5NVBpT1ZvMkI3YmpXU2JIZWRHbDVlMFpXYXE2NWtPR2dVU05lc0VJRGtCOUlTYlRnL0pLOWRoQ1pBPT0iXSwKCiAgICAiY29sb3ItY29udmVydCI6IFsiY29sb3ItY29udmVydEAyLjAuMSIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJjb2xvci1uYW1lIjogIn4xLjEuNCIgfSB9LCAic2hhNTEyLVJSRUNQc2o3aXUveGI1b0tZY3NGSFNwcEZObnNqLzUyT1ZUUktiNHpQNW9uWHdWRjN6Vm1tVG9OY09mR0MrQ1JEcGZLL1U1ODRmTWczOFpIQ2FFbEtRPT0iXSwKCiAgICAiY29sb3ItbmFtZSI6IFsiY29sb3ItbmFtZUAxLjEuNCIsICIiLCB7fSwgInNoYTUxMi1kT3krM0F1VzNhMndOYlpISXVNWnBUY2dqR3VMVS91QkwvdWJjWkY5T1hiRG84ZmY0Tzh5VnA1QmYwZWZTOHVFb1lvNXE0Rng3ZFk5T2dRR1hnQXNRQT09Il0sCgogICAgImNvbmN1cnJlbnRseSI6IFsiY29uY3VycmVudGx5QDkuMi4xIiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgImNoYWxrIjogIjQuMS4yIiwgInJ4anMiOiAiNy44LjIiLCAic2hlbGwtcXVvdGUiOiAiMS44LjMiLCAic3VwcG9ydHMtY29sb3IiOiAiOC4xLjEiLCAidHJlZS1raWxsIjogIjEuMi4yIiwgInlhcmdzIjogIjE3LjcuMiIgfSwgImJpbiI6IHsgImNvbmMiOiAiZGlzdC9iaW4vY29uY3VycmVudGx5LmpzIiwgImNvbmN1cnJlbnRseSI6ICJkaXN0L2Jpbi9jb25jdXJyZW50bHkuanMiIH0gfSwgInNoYTUxMi1mc2ZyTzBNeFY2NFpub3k4L2wxdlZJampIYTI5U1p5eXFQZ1FCd2hpRGNhVzh3SmMyVzNYV1ZPR3g0TTNvSkJudi96ZFVaSUlwMWdEZVM5OEd6UDhOZz09Il0sCgogICAgImNvbnZlcnQtc291cmNlLW1hcCI6IFsiY29udmVydC1zb3VyY2UtbWFwQDIuMC4wIiwgIiIsIHt9LCAic2hhNTEyLUt2cDQ1OUhyVjJGRUoxQ0FzaTFLdStNWTNrYXNIMTlURnlrVHoyeFdtTWVxNmJrMk5VM1hYdmZKK1E2MW0weGt0V3d0KzFIU1lmM0pac1RtczNhUkpnPT0iXSwKCiAgICAiY3NzdHlwZSI6IFsiY3NzdHlwZUAzLjIuMyIsICIiLCB7fSwgInNoYTUxMi16MUhHS2NZeTJ4QThBR1Fmd3JuMFBBeStQQjdYL0dTajNVVkpXOXFLeW40M3hXYStnbDVuWG1VNHFxTE1SeldWTEZDOEt1c1VYOFQvMGtDaU9ZcEFJUT09Il0sCgogICAgImRhdGUtZm5zIjogWyJkYXRlLWZuc0A0LjIuMSIsICIiLCB7fSwgInNoYTUxMi0zN1JoU2R4YUcxc3VlbjZWREN6YTZyTnJRZm9veVFoNTdIRlZQd1FHRXEyUVdsaVZMelBRWjhPYTAxN3dlT3UrSFpDbnpJN04zUGYvd3lvQktmRXFyQT09Il0sCgogICAgImRhdGUtZm5zLWphbGFsaSI6IFsiZGF0ZS1mbnMtamFsYWxpQDQuMS4wLTAiLCAiIiwge30sICJzaGE1MTItaFRJUC96K3QrcUt3QkRjbW1zbm1qV1RkdXhDZys1S2ZkcVdRdmIyWC84Qzkra25ZWTZlcE4vcGZ4ZER1eVZsU1ZlRnowc001ZUVmd0lVUTcwVTRja2c9PSJdLAoKICAgICJkZXRlY3QtbGliYyI6IFsiZGV0ZWN0LWxpYmNAMi4xLjIiLCAiIiwge30sICJzaGE1MTItQnRqMkJPT084M28zV3lINTllOE1nWHN4RVFWY2Fya1VPcEVZcnViQjB1cnduTjEweVEzNjRyc2lCeVUxMW5abHFXWVptMDVpL29mN2lvNG16aWhCdFE9PSJdLAoKICAgICJlbW9qaS1yZWdleCI6IFsiZW1vamktcmVnZXhAOC4wLjAiLCAiIiwge30sICJzaGE1MTItTVNqWXpjV05PQTBld0FIcHowTXhwWUZ2d2c2eWp5MU5HM3h0ZW9xejY0NFZDby9SUGducjEvR0d0K2ljM2lKVHpROEV1M1RkTTE0U2F3blZVbUdFNkE9PSJdLAoKICAgICJlbmhhbmNlZC1yZXNvbHZlIjogWyJlbmhhbmNlZC1yZXNvbHZlQDUuMjEuNSIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJncmFjZWZ1bC1mcyI6ICJeNC4yLjQiLCAidGFwYWJsZSI6ICJeMi4zLjMiIH0gfSwgInNoYTUxMi1tTENOYnJRbGkxMUsxeVNVbXVOdDRaVUIzT3BHSURxNHEydlRCVGY1Y0wybHBzUmpJOVFLcVNEMG5kalc4Rnl2Y1cvSmo0NmdNZTlzeXlIQXN2TWEvQT09Il0sCgogICAgImVzLW1vZHVsZS1sZXhlciI6IFsiZXMtbW9kdWxlLWxleGVyQDIuMS4wIiwgIiIsIHt9LCAic2hhNTEyLW4yN3pUWU1qWXUxYWo0TWpDV3pTUDdHOXI3NXV0c2FvYzhtNjF3ZUsrVzhKTUJHR1F5YmQ0M0dzdENYWjNXTm1TRnRHVDl3aTU5cVFUVzZtaFRSNUxRPT0iXSwKCiAgICAiZXNjYWxhZGUiOiBbImVzY2FsYWRlQDMuMi4wIiwgIiIsIHt9LCAic2hhNTEyLVdVajJxbHhhUXRPNGc2UHE1YzI5R1RjV0dEeWQ4aXRMOHpUbGlwZ0VDejNKZXNBaWlPS290ZDhKVTZvdEIzUEFDZ0c2eGtKVXlWaGJvTVMrYmplL2pBPT0iXSwKCiAgICAiZXN0cmVlLXdhbGtlciI6IFsiZXN0cmVlLXdhbGtlckAzLjAuMyIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAdHlwZXMvZXN0cmVlIjogIl4xLjAuMCIgfSB9LCAic2hhNTEyLTdSVUtmWGdTTU1renQ2WnVYbXFhcE91ckxHUFBmZ2o2bDl1Ulo3bFJHb2x2azB5MnlvY2MzNUxkY3hLQzVQUVpkbjJETXFpb0FRMk5vV2NyVEttbTZnPT0iXSwKCiAgICAiZXhwZWN0LXR5cGUiOiBbImV4cGVjdC10eXBlQDEuMy4wIiwgIiIsIHt9LCAic2hhNTEyLWtudnllYXVZaHFqT1l2UTY2TXpuU01zODN3bUhyQ3ljTkVONkFvKzJBZVlFZnhVSWt1aVZ4ZEVhMXFsR0VQSytXZTNuMFRIaURjaVlTc0NjZ1cvRG9BPT0iXSwKCiAgICAiZmRpciI6IFsiZmRpckA2LjUuMCIsICIiLCB7ICJwZWVyRGVwZW5kZW5jaWVzIjogeyAicGljb21hdGNoIjogIl4zIHx8IF40IiB9LCAib3B0aW9uYWxQZWVycyI6IFsicGljb21hdGNoIl0gfSwgInNoYTUxMi10SWJZdFpidWNPczBCUkdxUEprc2hKVVlkTCtTREg3ZFZNOGdqeStFUnAzV0FVakxFRkpFKzAya2FueUh0d2pXT253cktZQml3QW1NMHA0a0xKQW5YZz09Il0sCgogICAgImZzZXZlbnRzIjogWyJmc2V2ZW50c0AyLjMuMyIsICIiLCB7ICJvcyI6ICJkYXJ3aW4iIH0sICJzaGE1MTItNXhvRGZYK2ZMN2ZhQVRuYWdtV1BwYkZ0d2gvUjc3V21NTXFxSEdTNjVDM3Z2QjBZSHJnRitCMVltWjM0NDF0TWo1bjYzazAyMTJYTm9Kd3psaGZmUXc9PSJdLAoKICAgICJnZXQtY2FsbGVyLWZpbGUiOiBbImdldC1jYWxsZXItZmlsZUAyLjAuNSIsICIiLCB7fSwgInNoYTUxMi1EeUZQM0JNLzNZSFRRT0NVTC93ME9aSFIwbHBLZUdyeG90Y0hXY3FORWRubHRxRndYVmZoRUJROTRlSW8zNEFmUXBvMHJHa2k0Y3lJaWZ0WTA2aDJGZz09Il0sCgogICAgImdyYWNlZnVsLWZzIjogWyJncmFjZWZ1bC1mc0A0LjIuMTEiLCAiIiwge30sICJzaGE1MTItUmJKNS9qbUZjTk5DY0RWNW85ZVRuQkxKL0hzeldWMFA3M2JjK0ZmNG5TL3JKaitZYVM2SUd5aU9MMFZvQllYK2wxV3JsM2s2M2gvS3JIK25oSjBYdlE9PSJdLAoKICAgICJoYXMtZmxhZyI6IFsiaGFzLWZsYWdANC4wLjAiLCAiIiwge30sICJzaGE1MTItRXlrSlQvUTFLalRXY3RwcGdJQWdmU08wdEtWdVpVamhnTXIxN2txVHVtTWw2QWZ2M0VJU2xlVTdxWlV6b1hERlRBSFREQzROT29HL1p4VTNFdmxNUFE9PSJdLAoKICAgICJpcy1mdWxsd2lkdGgtY29kZS1wb2ludCI6IFsiaXMtZnVsbHdpZHRoLWNvZGUtcG9pbnRAMy4wLjAiLCAiIiwge30sICJzaGE1MTItenltbTUrdStzQ3NTV3lEOXFOYWVqVjNERnZoQ0tjbEtkaXpZYUpVdUhBODNSTGpiN25TdUduZGRDSEd2MGhrK0tZN0JNQWxzV2VLNFVlZzZFVjZYUWc9PSJdLAoKICAgICJqaXRpIjogWyJqaXRpQDIuNy4wIiwgIiIsIHsgImJpbiI6IHsgImppdGkiOiAibGliL2ppdGktY2xpLm1qcyIgfSB9LCAic2hhNTEyLUFDLzdKb2ZKdlpHcnJuZVdOYUVuSmVPTFV4K0psR3Q3dE5hMHdaaVJQVDRNWTF3bWZLanQyKzZPMnAydXoyK3NrbGw4T1pabUpNTnFla2U3a0tiTmdRPT0iXSwKCiAgICAibGlicGhvbmVudW1iZXItanMiOiBbImxpYnBob25lbnVtYmVyLWpzQDEuMTIuNDEiLCAiIiwge30sICJzaGE1MTItbHNtTW1HWEJ4WElLL1ZNTEVqMGtMNk10VXMxa0JHajFuVEN6aTZ6Z1FvRzFERXdxd3QyRFF5SHhjTHlrY2VJeEFuZkUzaHlhN051SWg2UHBDNlMzZkE9PSJdLAoKICAgICJsaWdodG5pbmdjc3MiOiBbImxpZ2h0bmluZ2Nzc0AxLjMyLjAiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiZGV0ZWN0LWxpYmMiOiAiXjIuMC4zIiB9LCAib3B0aW9uYWxEZXBlbmRlbmNpZXMiOiB7ICJsaWdodG5pbmdjc3MtYW5kcm9pZC1hcm02NCI6ICIxLjMyLjAiLCAibGlnaHRuaW5nY3NzLWRhcndpbi1hcm02NCI6ICIxLjMyLjAiLCAibGlnaHRuaW5nY3NzLWRhcndpbi14NjQiOiAiMS4zMi4wIiwgImxpZ2h0bmluZ2Nzcy1mcmVlYnNkLXg2NCI6ICIxLjMyLjAiLCAibGlnaHRuaW5nY3NzLWxpbnV4LWFybS1nbnVlYWJpaGYiOiAiMS4zMi4wIiwgImxpZ2h0bmluZ2Nzcy1saW51eC1hcm02NC1nbnUiOiAiMS4zMi4wIiwgImxpZ2h0bmluZ2Nzcy1saW51eC1hcm02NC1tdXNsIjogIjEuMzIuMCIsICJsaWdodG5pbmdjc3MtbGludXgteDY0LWdudSI6ICIxLjMyLjAiLCAibGlnaHRuaW5nY3NzLWxpbnV4LXg2NC1tdXNsIjogIjEuMzIuMCIsICJsaWdodG5pbmdjc3Mtd2luMzItYXJtNjQtbXN2YyI6ICIxLjMyLjAiLCAibGlnaHRuaW5nY3NzLXdpbjMyLXg2NC1tc3ZjIjogIjEuMzIuMCIgfSB9LCAic2hhNTEyLU5YWUJ6aW5OcmJsZnJhUEd5cmJQb0QxOUMxaDlsZkkvMW16Z1dZdlhVVGU0MTRHei9YMUZEMlhCWlNaTTdyUlRyTUE4SkwzT3RBYUdpZnJJS2hRNXlRPT0iXSwKCiAgICAibGlnaHRuaW5nY3NzLWFuZHJvaWQtYXJtNjQiOiBbImxpZ2h0bmluZ2Nzcy1hbmRyb2lkLWFybTY0QDEuMzIuMCIsICIiLCB7ICJvcyI6ICJhbmRyb2lkIiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi1ZSzcvQ2xUdDRrQUswdm82dzNYK1BubTBEMmNmMnZQSGJoT1hkb050aTFHYTBhbDFQNFRCWmh3akFUdmpOd0xFQkNuS3ZqSmMyalFnSFhIME5Fd2xBZz09Il0sCgogICAgImxpZ2h0bmluZ2Nzcy1kYXJ3aW4tYXJtNjQiOiBbImxpZ2h0bmluZ2Nzcy1kYXJ3aW4tYXJtNjRAMS4zMi4wIiwgIiIsIHsgIm9zIjogImRhcndpbiIsICJjcHUiOiAiYXJtNjQiIH0sICJzaGE1MTItUnplRzlKdTViYWcyQnYxL2x3bFZKdkJFM3E2VHRYc2tkWkxMQ3lmZzVwdCtITHo5QnFsSUNPN0xaTTdWSE5UVG4vNVBSaEhGQlNqazVsYzRjbXNjUFE9PSJdLAoKICAgICJsaWdodG5pbmdjc3MtZGFyd2luLXg2NCI6IFsibGlnaHRuaW5nY3NzLWRhcndpbi14NjRAMS4zMi4wIiwgIiIsIHsgIm9zIjogImRhcndpbiIsICJjcHUiOiAieDY0IiB9LCAic2hhNTEyLVUrUXNCcDJtL3Myd3FwVVlULzZ3bmxhZ2RaYnRaZG5kU211dC9OSnFsQ2NNTFRXcDVtdUNySUQrSzVVSjZqcUQyQkZzaGVqQ1lYbmlQRGJOaDczVjh3PT0iXSwKCiAgICAibGlnaHRuaW5nY3NzLWZyZWVic2QteDY0IjogWyJsaWdodG5pbmdjc3MtZnJlZWJzZC14NjRAMS4zMi4wIiwgIiIsIHsgIm9zIjogImZyZWVic2QiLCAiY3B1IjogIng2NCIgfSwgInNoYTUxMi1KQ1RpZ2VkRWtzWmszdEhUVHRobk1kVmZHZjYxRmt5OEppMkU0WWpVVEVRWDE0eGl5L2xUelhudTF2d2laZTNiWWUwcStTcHNTSC9DVGVEWEs2V0hpZz09Il0sCgogICAgImxpZ2h0bmluZ2Nzcy1saW51eC1hcm0tZ251ZWFiaWhmIjogWyJsaWdodG5pbmdjc3MtbGludXgtYXJtLWdudWVhYmloZkAxLjMyLjAiLCAiIiwgeyAib3MiOiAibGludXgiLCAiY3B1IjogImFybSIgfSwgInNoYTUxMi14NnJubnBSYTJHTDB6UU9rdDZydHMzWURQemR1THBXdndBRjZFTWhYRlZaWEQ0dFByQmtFRnF6R293ekNzSVdzUGpxU0srdHlORU9EVUJYZWVWSFNrdz09Il0sCgogICAgImxpZ2h0bmluZ2Nzcy1saW51eC1hcm02NC1nbnUiOiBbImxpZ2h0bmluZ2Nzcy1saW51eC1hcm02NC1nbnVAMS4zMi4wIiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi0wbm5NeW95T0xSSlhmYk1PaWxhU1JjTEgzSnc1ejlIRE5HZlQvZ3dDUGdhRGpueDBpOHc3dkJ6RkxGUjFmNkNNTEtGOGdWYmVibWtVTjNmYS9rUUpwUT09Il0sCgogICAgImxpZ2h0bmluZ2Nzcy1saW51eC1hcm02NC1tdXNsIjogWyJsaWdodG5pbmdjc3MtbGludXgtYXJtNjQtbXVzbEAxLjMyLjAiLCAiIiwgeyAib3MiOiAibGludXgiLCAiY3B1IjogImFybTY0IiB9LCAic2hhNTEyLVVwUWtvZW5yNFVKRXpnVklZcEk4MGxERnZSbVBWZzZvcWJvTkhmb0g0Q1FJZk5BK0hPclo3TW83S1pQMDJkQzZMamdoUFFKZUJzdlhoSm9kL3duSUJnPT0iXSwKCiAgICAibGlnaHRuaW5nY3NzLWxpbnV4LXg2NC1nbnUiOiBbImxpZ2h0bmluZ2Nzcy1saW51eC14NjQtZ251QDEuMzIuMCIsICIiLCB7ICJvcyI6ICJsaW51eCIsICJjcHUiOiAieDY0IiB9LCAic2hhNTEyLVY3UXI1MkloWm1kS1BWcitWdHc4bytXTHNRSllDVGQ4bG9JZnBEYU1SV0dVWmZCT1lFSmV5SklrcUdJRE1aUHdQeDI0cFVNZndTeHhJOHBoci9NYk9BPT0iXSwKCiAgICAibGlnaHRuaW5nY3NzLWxpbnV4LXg2NC1tdXNsIjogWyJsaWdodG5pbmdjc3MtbGludXgteDY0LW11c2xAMS4zMi4wIiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJ4NjQiIH0sICJzaGE1MTItYlljTHArVmIwYXdzaVhnLzgwdUNSZXpDWUhOZzEvbDNtdDBnekhuV1Y5WFAxVzVzS2E1L1RDZEdXYVIvekJNMlBlRi9IYnNRdi9qMlVSTk9pVnV4V2c9PSJdLAoKICAgICJsaWdodG5pbmdjc3Mtd2luMzItYXJtNjQtbXN2YyI6IFsibGlnaHRuaW5nY3NzLXdpbjMyLWFybTY0LW1zdmNAMS4zMi4wIiwgIiIsIHsgIm9zIjogIndpbjMyIiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi04U2JDOEJSNDBwUzZiYUNNOHNidFlEU3dFVlFkNEpsRlRPbGFEM2dXR0hmVGhUY0FCbk5EQmRhNmVUWmVxYm9mYWxJSmhGeDBxS3pnSEptY1BUbkdkdz09Il0sCgogICAgImxpZ2h0bmluZ2Nzcy13aW4zMi14NjQtbXN2YyI6IFsibGlnaHRuaW5nY3NzLXdpbjMyLXg2NC1tc3ZjQDEuMzIuMCIsICIiLCB7ICJvcyI6ICJ3aW4zMiIsICJjcHUiOiAieDY0IiB9LCAic2hhNTEyLUFtcTlCL1NvWllkRGkxa0Zyb2pub3FQTHhZaFE0V281WGlMOEVWSnJWc0I4QVJvQzFQV1c2Vkd0VDBXS0NlbWp5OGFDK2xvdUpualM3VTE4eDNiMDZRPT0iXSwKCiAgICAibWFnaWMtc3RyaW5nIjogWyJtYWdpYy1zdHJpbmdAMC4zMC4yMSIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAanJpZGdld2VsbC9zb3VyY2VtYXAtY29kZWMiOiAiXjEuNS41IiB9IH0sICJzaGE1MTItdmQyRjRZVXlFWEtHY0xIb3ErVEV5Q2p4dWVTZUhuRnh5eWpOcDgweWcwWFY0dlVobkRlci9sdnZscU0vYXJCNWJYUU41SzIvM29pbnlDUnl4OFQyQ1E9PSJdLAoKICAgICJuYW5vaWQiOiBbIm5hbm9pZEAzLjMuMTIiLCAiIiwgeyAiYmluIjogeyAibmFub2lkIjogImJpbi9uYW5vaWQuY2pzIiB9IH0sICJzaGE1MTItWkI5UkgvMzlxcHE1VnU2WStObVVhRmhRUjZwcCtNMlh0NzZYQm5Fd0RhR2NWQXFobHZ4cmwzQjJiS1M1RDNOSDNRUjc2djNhU3JLYUYvS2l5N2xFdFE9PSJdLAoKICAgICJuZXh0IjogWyJuZXh0QDE2LjIuNiIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAbmV4dC9lbnYiOiAiMTYuMi42IiwgIkBzd2MvaGVscGVycyI6ICIwLjUuMTUiLCAiYmFzZWxpbmUtYnJvd3Nlci1tYXBwaW5nIjogIl4yLjkuMTkiLCAiY2FuaXVzZS1saXRlIjogIl4xLjAuMzAwMDE1NzkiLCAicG9zdGNzcyI6ICI4LjQuMzEiLCAic3R5bGVkLWpzeCI6ICI1LjEuNiIgfSwgIm9wdGlvbmFsRGVwZW5kZW5jaWVzIjogeyAiQG5leHQvc3djLWRhcndpbi1hcm02NCI6ICIxNi4yLjYiLCAiQG5leHQvc3djLWRhcndpbi14NjQiOiAiMTYuMi42IiwgIkBuZXh0L3N3Yy1saW51eC1hcm02NC1nbnUiOiAiMTYuMi42IiwgIkBuZXh0L3N3Yy1saW51eC1hcm02NC1tdXNsIjogIjE2LjIuNiIsICJAbmV4dC9zd2MtbGludXgteDY0LWdudSI6ICIxNi4yLjYiLCAiQG5leHQvc3djLWxpbnV4LXg2NC1tdXNsIjogIjE2LjIuNiIsICJAbmV4dC9zd2Mtd2luMzItYXJtNjQtbXN2YyI6ICIxNi4yLjYiLCAiQG5leHQvc3djLXdpbjMyLXg2NC1tc3ZjIjogIjE2LjIuNiIsICJzaGFycCI6ICJeMC4zNC41IiB9LCAicGVlckRlcGVuZGVuY2llcyI6IHsgIkBvcGVudGVsZW1ldHJ5L2FwaSI6ICJeMS4xLjAiLCAiQHBsYXl3cmlnaHQvdGVzdCI6ICJeMS41MS4xIiwgImJhYmVsLXBsdWdpbi1yZWFjdC1jb21waWxlciI6ICIqIiwgInJlYWN0IjogIl4xOC4yLjAgfHwgMTkuMC4wLXJjLWRlNjhkMmY0LTIwMjQxMjA0IHx8IF4xOS4wLjAiLCAicmVhY3QtZG9tIjogIl4xOC4yLjAgfHwgMTkuMC4wLXJjLWRlNjhkMmY0LTIwMjQxMjA0IHx8IF4xOS4wLjAiLCAic2FzcyI6ICJeMS4zLjAiIH0sICJvcHRpb25hbFBlZXJzIjogWyJAb3BlbnRlbGVtZXRyeS9hcGkiLCAiQHBsYXl3cmlnaHQvdGVzdCIsICJiYWJlbC1wbHVnaW4tcmVhY3QtY29tcGlsZXIiLCAic2FzcyJdLCAiYmluIjogeyAibmV4dCI6ICJkaXN0L2Jpbi9uZXh0IiB9IH0sICJzaGE1MTItcU9WZ0tKZzErQXQxNU5wZVVQK2VKZ0NIdlRDZ1hzb2d3ZXE4N1JpL0l4N1BrcVFIZzRzZGFYbVNGcUtsZ2FJWEU0a1cwZzI1TEU2OFc4N1VBTmxIdHc9PSJdLAoKICAgICJvYnVnIjogWyJvYnVnQDIuMS4xIiwgIiIsIHt9LCAic2hhNTEyLXVUcUY5TXVQcmFBUStJc25QZjM2NlJHNGNQOVJ0VWk3TUxPMU4zS0VjK3diMGE2eUtwZUwwbG1rMklCMWpZNUtIUEFsVGM2VC9KUmRDL1lxeEhOd2tRPT0iXSwKCiAgICAicGF0aGUiOiBbInBhdGhlQDIuMC4zIiwgIiIsIHt9LCAic2hhNTEyLVdVakdjQXFQMWdRYWNvUWUrT0JKc0ZBN0xkNER5WHVVSWpaNWNjNzVjTEh2SjdkdE5zVHVncGh4SUFEd3NwUytBcmFBVWVQQ0tyU1Z0UExGai9GODh3PT0iXSwKCiAgICAicGljb2NvbG9ycyI6IFsicGljb2NvbG9yc0AxLjEuMSIsICIiLCB7fSwgInNoYTUxMi14Y2VIMnNuaHRiNU05bGlxRHNtRXc1NmxlMzc2bVRaa0VYL2pFYi9SeE5GeWVnTnVsN2VOc2xDWFA5RkRqL0xjdTBYOEtFeU1jZVAybnRwYUhyREVWQT09Il0sCgogICAgInBpY29tYXRjaCI6IFsicGljb21hdGNoQDQuMC40IiwgIiIsIHt9LCAic2hhNTEyLVFQODhCQUt2TWFtLzNOeEg2dmoybzIxUjZNanhaVUFkNm5sd0FTL3BuR3ZOOUlWTG9jTEh4R1lJekZoZzZmVVErNXRoNlA0ZHY0ZVc5algzRFNJajdBPT0iXSwKCiAgICAicG9zdGNzcyI6IFsicG9zdGNzc0A4LjUuMTUiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAibmFub2lkIjogIl4zLjMuMTIiLCAicGljb2NvbG9ycyI6ICJeMS4xLjEiLCAic291cmNlLW1hcC1qcyI6ICJeMS4yLjEiIH0gfSwgInNoYTUxMi1GZlI4c2pkNGVtMlQ2ZmIzSTJNd0FKVTdIV1ZNcjl6YmErZW5tUWVlV0ZmQ2JtK1VPQy8wWDREUzhYdHBVVE13V01HYmpLWVA3eGpmTmVrenlHbUIzQT09Il0sCgogICAgInJlYWN0IjogWyJyZWFjdEAxOS4yLjYiLCAiIiwge30sICJzaGE1MTItc2ZXR0dmYXZpMHhyOFBnMHNWc3lITUFPemlWWUtnUExOclM3aWcraXZNTmIzd2JDQnczS3h0ZmxzR0JBd0QzZ1lRbEUvQUVac1RMZ1RvUnJTQ2piMFE9PSJdLAoKICAgICJyZWFjdC1kYXktcGlja2VyIjogWyJyZWFjdC1kYXktcGlja2VyQDkuMTQuMCIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAZGF0ZS1mbnMvdHoiOiAiXjEuNC4xIiwgIkB0YWJieV9haS9oaWpyaS1jb252ZXJ0ZXIiOiAiMS4wLjUiLCAiZGF0ZS1mbnMiOiAiXjQuMS4wIiwgImRhdGUtZm5zLWphbGFsaSI6ICI0LjEuMC0wIiB9LCAicGVlckRlcGVuZGVuY2llcyI6IHsgInJlYWN0IjogIj49MTYuOC4wIiB9IH0sICJzaGE1MTItdEJhb0RXalB3ZTBNNXBHcnVtNEgwU1I2THlrK0JPOW9IbnA5SmJLcEdLVzJtbHJhTlBnUDlCTWZzZzVwV3B3cnNzQVJtZXFrN1lCbDJvWHV0WlRhSEE9PSJdLAoKICAgICJyZWFjdC1kb20iOiBbInJlYWN0LWRvbUAxOS4yLjYiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAic2NoZWR1bGVyIjogIl4wLjI3LjAiIH0sICJwZWVyRGVwZW5kZW5jaWVzIjogeyAicmVhY3QiOiAiXjE5LjIuNiIgfSB9LCAic2hhNTEyLTBwck1JK2h2QmJQanNXbnhETHhsQ0d5TThQTjZVdVdqRVVDWW1aaE82N3hJVjlYYXNhL3IvdkRucStYeXE0TG8yN2c4UVNiTzVZekFSdTBEMVNwczNnPT0iXSwKCiAgICAicmVxdWlyZS1kaXJlY3RvcnkiOiBbInJlcXVpcmUtZGlyZWN0b3J5QDIuMS4xIiwgIiIsIHt9LCAic2hhNTEyLWZHeEVJNyt3c0c5eHJ2ZGpzcmxtTDIyT01UVGlIUndBTXJvaUVlTWdxOGd6b0xDL1BRcjdSc1JEU1RMVWcvYlpBWnRGK1RWSWtIYzYvNFJJS3J1aStRPT0iXSwKCiAgICAicmVzZWxlY3QiOiBbInJlc2VsZWN0QDUuMi4wIiwgIiIsIHt9LCAic2hhNTEyLUFnWjNVT1ptM1luZGZySjRPWWpnclQ3Ym1DbS8xaXFranZFZkgvb1lqemg2UEQycXc0UXVUM2pqblhJcnBkdDRNVHBNWGNsTVQzbFhibVJZK1hSYWt3PT0iXSwKCiAgICAicm9sbGRvd24iOiBbInJvbGxkb3duQDEuMC4xIiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIkBveGMtcHJvamVjdC90eXBlcyI6ICI9MC4xMzAuMCIsICJAcm9sbGRvd24vcGx1Z2ludXRpbHMiOiAiXjEuMC4wIiB9LCAib3B0aW9uYWxEZXBlbmRlbmNpZXMiOiB7ICJAcm9sbGRvd24vYmluZGluZy1hbmRyb2lkLWFybTY0IjogIjEuMC4xIiwgIkByb2xsZG93bi9iaW5kaW5nLWRhcndpbi1hcm02NCI6ICIxLjAuMSIsICJAcm9sbGRvd24vYmluZGluZy1kYXJ3aW4teDY0IjogIjEuMC4xIiwgIkByb2xsZG93bi9iaW5kaW5nLWZyZWVic2QteDY0IjogIjEuMC4xIiwgIkByb2xsZG93bi9iaW5kaW5nLWxpbnV4LWFybS1nbnVlYWJpaGYiOiAiMS4wLjEiLCAiQHJvbGxkb3duL2JpbmRpbmctbGludXgtYXJtNjQtZ251IjogIjEuMC4xIiwgIkByb2xsZG93bi9iaW5kaW5nLWxpbnV4LWFybTY0LW11c2wiOiAiMS4wLjEiLCAiQHJvbGxkb3duL2JpbmRpbmctbGludXgtcHBjNjQtZ251IjogIjEuMC4xIiwgIkByb2xsZG93bi9iaW5kaW5nLWxpbnV4LXMzOTB4LWdudSI6ICIxLjAuMSIsICJAcm9sbGRvd24vYmluZGluZy1saW51eC14NjQtZ251IjogIjEuMC4xIiwgIkByb2xsZG93bi9iaW5kaW5nLWxpbnV4LXg2NC1tdXNsIjogIjEuMC4xIiwgIkByb2xsZG93bi9iaW5kaW5nLW9wZW5oYXJtb255LWFybTY0IjogIjEuMC4xIiwgIkByb2xsZG93bi9iaW5kaW5nLXdhc20zMi13YXNpIjogIjEuMC4xIiwgIkByb2xsZG93bi9iaW5kaW5nLXdpbjMyLWFybTY0LW1zdmMiOiAiMS4wLjEiLCAiQHJvbGxkb3duL2JpbmRpbmctd2luMzIteDY0LW1zdmMiOiAiMS4wLjEiIH0sICJiaW4iOiB7ICJyb2xsZG93biI6ICJiaW4vY2xpLm1qcyIgfSB9LCAic2hhNTEyLVgwS1FIbGpObkVrV05xcWl6OXpKckd1bmgxQjBIZ094TFh2bkZwQ09jYWR6Y3k1cW9oWjN0cU1FVWcwMHZuY29Sb3ZYdUszWnFDVDlLbm5Lem9JbkZRPT0iXSwKCiAgICAicnhqcyI6IFsicnhqc0A3LjguMiIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJ0c2xpYiI6ICJeMi4xLjAiIH0gfSwgInNoYTUxMi1kaEtmOTAzVS9QUVpZNmJvTk50QUdkV2JHODVXQWJqVC8xeFlvWklDN0ZBWTB5V2FwT0JRVnNWckRsNThXODYvL2UxVnBNTkJ0UlY0TWFYZmRNeVNGQT09Il0sCgogICAgInNjaGVkdWxlciI6IFsic2NoZWR1bGVyQDAuMjcuMCIsICIiLCB7fSwgInNoYTUxMi1lTnYrV3JWYkt1MWYzdmJZSlQveHRpRjVzeUE1SFBJTXRmOUlnWS9uS2cwc1dxekFVRXZxWS94bTdPY1pjL3FhZkx4L2lPOUZnT21lU0FwNHY1dGkvUT09Il0sCgogICAgInNlbXZlciI6IFsic2VtdmVyQDcuOC4wIiwgIiIsIHsgImJpbiI6IHsgInNlbXZlciI6ICJiaW4vc2VtdmVyLmpzIiB9IH0sICJzaGE1MTItQWNNN2RWLzV1bDRFZWtvUTI5QWdtNXZyaThKTnFSeWozOW8wcXBYNnZERjJHWnJ0dXRabDVSd2dEMVhuWmppVEFmbmNzSmhNSTQ4UVFIM3NOODdZTkE9PSJdLAoKICAgICJzaGFycCI6IFsic2hhcnBAMC4zNC41IiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIkBpbWcvY29sb3VyIjogIl4xLjAuMCIsICJkZXRlY3QtbGliYyI6ICJeMi4xLjIiLCAic2VtdmVyIjogIl43LjcuMyIgfSwgIm9wdGlvbmFsRGVwZW5kZW5jaWVzIjogeyAiQGltZy9zaGFycC1kYXJ3aW4tYXJtNjQiOiAiMC4zNC41IiwgIkBpbWcvc2hhcnAtZGFyd2luLXg2NCI6ICIwLjM0LjUiLCAiQGltZy9zaGFycC1saWJ2aXBzLWRhcndpbi1hcm02NCI6ICIxLjIuNCIsICJAaW1nL3NoYXJwLWxpYnZpcHMtZGFyd2luLXg2NCI6ICIxLjIuNCIsICJAaW1nL3NoYXJwLWxpYnZpcHMtbGludXgtYXJtIjogIjEuMi40IiwgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC1hcm02NCI6ICIxLjIuNCIsICJAaW1nL3NoYXJwLWxpYnZpcHMtbGludXgtcHBjNjQiOiAiMS4yLjQiLCAiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4LXJpc2N2NjQiOiAiMS4yLjQiLCAiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4LXMzOTB4IjogIjEuMi40IiwgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC14NjQiOiAiMS4yLjQiLCAiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4bXVzbC1hcm02NCI6ICIxLjIuNCIsICJAaW1nL3NoYXJwLWxpYnZpcHMtbGludXhtdXNsLXg2NCI6ICIxLjIuNCIsICJAaW1nL3NoYXJwLWxpbnV4LWFybSI6ICIwLjM0LjUiLCAiQGltZy9zaGFycC1saW51eC1hcm02NCI6ICIwLjM0LjUiLCAiQGltZy9zaGFycC1saW51eC1wcGM2NCI6ICIwLjM0LjUiLCAiQGltZy9zaGFycC1saW51eC1yaXNjdjY0IjogIjAuMzQuNSIsICJAaW1nL3NoYXJwLWxpbnV4LXMzOTB4IjogIjAuMzQuNSIsICJAaW1nL3NoYXJwLWxpbnV4LXg2NCI6ICIwLjM0LjUiLCAiQGltZy9zaGFycC1saW51eG11c2wtYXJtNjQiOiAiMC4zNC41IiwgIkBpbWcvc2hhcnAtbGludXhtdXNsLXg2NCI6ICIwLjM0LjUiLCAiQGltZy9zaGFycC13YXNtMzIiOiAiMC4zNC41IiwgIkBpbWcvc2hhcnAtd2luMzItYXJtNjQiOiAiMC4zNC41IiwgIkBpbWcvc2hhcnAtd2luMzItaWEzMiI6ICIwLjM0LjUiLCAiQGltZy9zaGFycC13aW4zMi14NjQiOiAiMC4zNC41IiB9IH0sICJzaGE1MTItT3U5STVGdDlXTmNDYlhyVTljTWdQQmNDSzhMaXdMcWNieXdXM3Q0b0RWMzduMXB6cHVOTHNZaUFWOGVPRG5qYnRRbFNEd1oyY1VFZVF6NEU1NEhsdGc9PSJdLAoKICAgICJzaGVsbC1xdW90ZSI6IFsic2hlbGwtcXVvdGVAMS44LjMiLCAiIiwge30sICJzaGE1MTItT2JtbklGNGhYTmcxQnFobkhtZ2JERVRGOGRMUENnZ1pXQmprUWZoWnBic3pabll1cjVEVWxqVGNDSGlpNUxDM0o1RTB5ZU8vMUxJTXlIK1V2SFFneXc9PSJdLAoKICAgICJzaWdpbmZvIjogWyJzaWdpbmZvQDIuMC4wIiwgIiIsIHt9LCAic2hhNTEyLXlieDBXTzEvOGJTQkxFV1hadkVkN2dNVzNTbjNKRmxXM1R2WDFuUkViRExSTlFOYWVOTjhXSzBtZUJ3UGRBYU9JN1R0UlJSSm4vRXMxemhyckNIdTdnPT0iXSwKCiAgICAic291cmNlLW1hcC1qcyI6IFsic291cmNlLW1hcC1qc0AxLjIuMSIsICIiLCB7fSwgInNoYTUxMi1VWFdNS2hMT3dWS2I3MjhJVXRRUFh4ZllVK3VzZHlidFVySy84dUdFOENRTXZyaE9wd3Z6REJ3ajBRaFNMN01RYzd2SXNJU0JHOFZROCtJRFF4cGZRQT09Il0sCgogICAgInN0YWNrYmFjayI6IFsic3RhY2tiYWNrQDAuMC4yIiwgIiIsIHt9LCAic2hhNTEyLTFYTUpFNWZRbzFqR0g2WS83ZWJud1BPQkVrSUVuVDRRRjMyZDVSMStWWGRYdmVNMElCTUp0OHpmYXhYMVAzUWhWd3JZZSs1NzYramtBTnRTUzJtQmJ3PT0iXSwKCiAgICAic3RkLWVudiI6IFsic3RkLWVudkA0LjEuMCIsICIiLCB7fSwgInNoYTUxMi1ScTd5YmNYMlJ1QzU1cjlvYVBWRVc3L3h1M3RqOHU0R2VCWUhCV0N5Y2hGdHpNSXI4NkE3ZTNQUEVCUFQzN3NIU3RLWDMrVGlYL0ZyL0FDbUpMVmxMUT09Il0sCgogICAgInN0cmluZy13aWR0aCI6IFsic3RyaW5nLXdpZHRoQDQuMi4zIiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgImVtb2ppLXJlZ2V4IjogIl44LjAuMCIsICJpcy1mdWxsd2lkdGgtY29kZS1wb2ludCI6ICJeMy4wLjAiLCAic3RyaXAtYW5zaSI6ICJeNi4wLjEiIH0gfSwgInNoYTUxMi13S3lRUlFwakowc0lwNjJFclNaZEdzak1KV3NhcDVvUk5paEhodTZHN0pWTy85aklCNlV5ZXZMK3RYdU9xcm5nOGovY3hLVFd5V1V3dlNUcmlpWnovZz09Il0sCgogICAgInN0cmlwLWFuc2kiOiBbInN0cmlwLWFuc2lANi4wLjEiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiYW5zaS1yZWdleCI6ICJeNS4wLjEiIH0gfSwgInNoYTUxMi1ZMzhWUFNIY3FrRnJDcEZuUTl2dVNYbXF1dXY1b1hPS3BHZVQ2YUdycjNvM0djOUFsVmE2SkJmVVNPQ25ieEdHWkYrLzBvb0k3S3JQdVVTenRVZFU1QT09Il0sCgogICAgInN0eWxlZC1qc3giOiBbInN0eWxlZC1qc3hANS4xLjYiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiY2xpZW50LW9ubHkiOiAiMC4wLjEiIH0sICJwZWVyRGVwZW5kZW5jaWVzIjogeyAicmVhY3QiOiAiPj0gMTYuOC4wIHx8IDE3LngueCB8fCBeMTguMC4wLTAgfHwgXjE5LjAuMC0wIiB9IH0sICJzaGE1MTItcVNWeURUZU1vdGR2UVlvSFdMTkd3UkZKSEMraStadmRCUllvc09GZ0MrV2cxdng0ZnJOMi9SRy9OQTdTWXFxdktOTGYzOVAyTFNSQTJwdTZuMFhZWkE9PSJdLAoKICAgICJzdXBwb3J0cy1jb2xvciI6IFsic3VwcG9ydHMtY29sb3JAOC4xLjEiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiaGFzLWZsYWciOiAiXjQuMC4wIiB9IH0sICJzaGE1MTItTXBVRU4yT29kdFV6eHZLUWw3MmNVRjdSUTVFaUhzR3ZTc1ZHMGlhOWM1UmJXR0wyQ0k0QzdFcFBTOFVUQklwbG5selppTnVWNTZ3K0Z1Tnh5M3R5MlE9PSJdLAoKICAgICJ0YWlsd2luZC1tZXJnZSI6IFsidGFpbHdpbmQtbWVyZ2VAMy42LjAiLCAiIiwge30sICJzaGE1MTItdXhMN3FBVlFyaXFSUVBBeUszcGo2NlZxc2tXcW9aMzdQVzk0andPVHdOZnEvejlveXUxVitlcXJacXRSMitmQ2lYZFlPWmUvTW9kdDhHdHZxTnp1K3c9PSJdLAoKICAgICJ0YWlsd2luZGNzcyI6IFsidGFpbHdpbmRjc3NANC4zLjAiLCAiIiwge30sICJzaGE1MTIteTZueE1HQjFuTVc5UjZrOTZlNWdkSUZ6Y2ZML2dUSlJOYXFHZXMxWXZrTG5QVlh6V2dicUZGMnlMQzBUOEc3NzRuMjRjeDNQZThYcktvbmlDT0FIK1E9PSJdLAoKICAgICJ0YXBhYmxlIjogWyJ0YXBhYmxlQDIuMy4zIiwgIiIsIHt9LCAic2hhNTEyLXV4Yy96cHFGZzZ4N0M4dk9FN2xoNkxiZGE4ZUVMOXptVm0vUExlVFBCUmhoMXhDZ2RXYVErSjFDVWllR3BJZm0ySGR0c1VwUnYrSHNoaWFzQk1jYzZBPT0iXSwKCiAgICAidGlueWJlbmNoIjogWyJ0aW55YmVuY2hAMi45LjAiLCAiIiwge30sICJzaGE1MTItMCtEVXZxV01WYWxMbWhhNmxyNGtEOGlBTUsxSHpWMC9hS25DdFdiOXY5NjQxVG5QL01GYjdQYzJieG94UWpUWEFFcnJ5WFZnVU9mdjJZcU5sbHFHZWc9PSJdLAoKICAgICJ0aW55ZXhlYyI6IFsidGlueWV4ZWNAMS4xLjIiLCAiIiwge30sICJzaGE1MTItZEFxU3FFL1JhYnBCS0k4K2gyNkdmTHE2VmIzSlZYczMwWFlRamRNamFqL2MydFM4SVlZTWJJelA1OTlLdFJqN2M1Ny93WUFwYjNRamdSZ1htckN1a0E9PSJdLAoKICAgICJ0aW55Z2xvYmJ5IjogWyJ0aW55Z2xvYmJ5QDAuMi4xNiIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJmZGlyIjogIl42LjUuMCIsICJwaWNvbWF0Y2giOiAiXjQuMC40IiB9IH0sICJzaGE1MTItcG45OVZob0FDWVI4bkZIaHhxaXgrdXZzYlhpbmVBYXNXbTVvalhvTjh4RXdLNUtkMy9UcmhObjF3Qnl1RDUyVXhXUkx5OHB1K2tSTW5pRWk2RXE5Wmc9PSJdLAoKICAgICJ0aW55cmFpbmJvdyI6IFsidGlueXJhaW5ib3dAMy4xLjAiLCAiIiwge30sICJzaGE1MTItQmYrSUxtQmdyZXRVcmRKeHpYTTBTZ1hMWjNYZmlhVXVPai9JS1FIdVRYaXArMDVYbit1eUVZZFZnMGtZRGlwVEJjTHJDVnlVekFQejdRbUFyYjBtbXc9PSJdLAoKICAgICJ0cmVlLWtpbGwiOiBbInRyZWUta2lsbEAxLjIuMiIsICIiLCB7ICJiaW4iOiB7ICJ0cmVlLWtpbGwiOiAiY2xpLmpzIiB9IH0sICJzaGE1MTItTDBPcnBpOHFHcFJHLy9OZCtIOTB2RkIrM2lIbnVlMXpTU0dtTk9PQ2gxR0xKN3JVS1Z3VjJIdmlqcGhHUVMyVW1oVVpld1M5Vmd2eFlJZGdyK2ZHMUE9PSJdLAoKICAgICJ0c2xpYiI6IFsidHNsaWJAMi44LjEiLCAiIiwge30sICJzaGE1MTItb0pGdTk0SFFiK0tWZHVTVVFMN3ducG1xbmZtTHNPQS9uQWg2YjZFSDB3Q0VvSzAvbVBlWFU2YzN3S0RWODNNa091SFBSSHRTWEtLVTk5SUJhelMvMnc9PSJdLAoKICAgICJ0eXBlc2NyaXB0IjogWyJ0eXBlc2NyaXB0QDUuOS4zIiwgIiIsIHsgImJpbiI6IHsgInRzYyI6ICJiaW4vdHNjIiwgInRzc2VydmVyIjogImJpbi90c3NlcnZlciIgfSB9LCAic2hhNTEyLWpsMXZaelBEaW5McjllVXQzSi90N1Y2RmdORXc5UWp2QlBkeXN6OUtmUURENDFmUXJDMlk0dktRZGlhVXBGVDRiWGxiMVJIaExwcDh3dG02TTVUZ1N3PT0iXSwKCiAgICAidW5kaWNpLXR5cGVzIjogWyJ1bmRpY2ktdHlwZXNANi4yMS4wIiwgIiIsIHt9LCAic2hhNTEyLWl3RFpxZzBRQUdyZzlSYXY1SDRuME02NGMzbWtSNTljSjZ3UXArN0M0bkkwZ3NtRXhhZWRhWUxOTzQ0ZVQ0QXRCQndqYlRpR1BNbHQyTWQwVDlIOUpRPT0iXSwKCiAgICAidXNlLXN5bmMtZXh0ZXJuYWwtc3RvcmUiOiBbInVzZS1zeW5jLWV4dGVybmFsLXN0b3JlQDEuNi4wIiwgIiIsIHsgInBlZXJEZXBlbmRlbmNpZXMiOiB7ICJyZWFjdCI6ICJeMTYuOC4wIHx8IF4xNy4wLjAgfHwgXjE4LjAuMCB8fCBeMTkuMC4wIiB9IH0sICJzaGE1MTItUHA2R1N3R1AvTnJQSXJ4VkZBSWtPUWV5dzhsRmVuT0hpalFXa1VUckR2ckY0QUxxeWxQMkMvS0NrZVM5ZHBVTTNLdllSUWhuYTV2dDdJTDk1K1pROXc9PSJdLAoKICAgICJ2aXRlIjogWyJ2aXRlQDguMC4xMyIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJsaWdodG5pbmdjc3MiOiAiXjEuMzIuMCIsICJwaWNvbWF0Y2giOiAiXjQuMC40IiwgInBvc3Rjc3MiOiAiXjguNS4xNCIsICJyb2xsZG93biI6ICIxLjAuMSIsICJ0aW55Z2xvYmJ5IjogIl4wLjIuMTYiIH0sICJvcHRpb25hbERlcGVuZGVuY2llcyI6IHsgImZzZXZlbnRzIjogIn4yLjMuMyIgfSwgInBlZXJEZXBlbmRlbmNpZXMiOiB7ICJAdHlwZXMvbm9kZSI6ICJeMjAuMTkuMCB8fCA+PTIyLjEyLjAiLCAiQHZpdGVqcy9kZXZ0b29scyI6ICJeMC4xLjE4IiwgImVzYnVpbGQiOiAiXjAuMjcuMCB8fCBeMC4yOC4wIiwgImppdGkiOiAiPj0xLjIxLjAiLCAibGVzcyI6ICJeNC4wLjAiLCAic2FzcyI6ICJeMS43MC4wIiwgInNhc3MtZW1iZWRkZWQiOiAiXjEuNzAuMCIsICJzdHlsdXMiOiAiPj0wLjU0LjgiLCAic3VnYXJzcyI6ICJeNS4wLjAiLCAidGVyc2VyIjogIl41LjE2LjAiLCAidHN4IjogIl40LjguMSIsICJ5YW1sIjogIl4yLjQuMiIgfSwgIm9wdGlvbmFsUGVlcnMiOiBbIkB0eXBlcy9ub2RlIiwgIkB2aXRlanMvZGV2dG9vbHMiLCAiZXNidWlsZCIsICJqaXRpIiwgImxlc3MiLCAic2FzcyIsICJzYXNzLWVtYmVkZGVkIiwgInN0eWx1cyIsICJzdWdhcnNzIiwgInRlcnNlciIsICJ0c3giLCAieWFtbCJdLCAiYmluIjogeyAidml0ZSI6ICJiaW4vdml0ZS5qcyIgfSB9LCAic2hhNTEyLU1GdGpCWWd6bVN4bWdBNFJBZmpJeVhXcEdlMW9BTG5qZ1VUenpWN1FMeC9US3hDemp0TUg2RmQ5L2VWSys1RmcxcU5vejVWQXdzbU1zL05vZnJtSnZ3PT0iXSwKCiAgICAidml0ZXN0IjogWyJ2aXRlc3RANC4xLjYiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiQHZpdGVzdC9leHBlY3QiOiAiNC4xLjYiLCAiQHZpdGVzdC9tb2NrZXIiOiAiNC4xLjYiLCAiQHZpdGVzdC9wcmV0dHktZm9ybWF0IjogIjQuMS42IiwgIkB2aXRlc3QvcnVubmVyIjogIjQuMS42IiwgIkB2aXRlc3Qvc25hcHNob3QiOiAiNC4xLjYiLCAiQHZpdGVzdC9zcHkiOiAiNC4xLjYiLCAiQHZpdGVzdC91dGlscyI6ICI0LjEuNiIsICJlcy1tb2R1bGUtbGV4ZXIiOiAiXjIuMC4wIiwgImV4cGVjdC10eXBlIjogIl4xLjMuMCIsICJtYWdpYy1zdHJpbmciOiAiXjAuMzAuMjEiLCAib2J1ZyI6ICJeMi4xLjEiLCAicGF0aGUiOiAiXjIuMC4zIiwgInBpY29tYXRjaCI6ICJeNC4wLjMiLCAic3RkLWVudiI6ICJeNC4wLjAtcmMuMSIsICJ0aW55YmVuY2giOiAiXjIuOS4wIiwgInRpbnlleGVjIjogIl4xLjAuMiIsICJ0aW55Z2xvYmJ5IjogIl4wLjIuMTUiLCAidGlueXJhaW5ib3ciOiAiXjMuMS4wIiwgInZpdGUiOiAiXjYuMC4wIHx8IF43LjAuMCB8fCBeOC4wLjAiLCAid2h5LWlzLW5vZGUtcnVubmluZyI6ICJeMi4zLjAiIH0sICJwZWVyRGVwZW5kZW5jaWVzIjogeyAiQGVkZ2UtcnVudGltZS92bSI6ICIqIiwgIkBvcGVudGVsZW1ldHJ5L2FwaSI6ICJeMS45LjAiLCAiQHR5cGVzL25vZGUiOiAiXjIwLjAuMCB8fCBeMjIuMC4wIHx8ID49MjQuMC4wIiwgIkB2aXRlc3QvYnJvd3Nlci1wbGF5d3JpZ2h0IjogIjQuMS42IiwgIkB2aXRlc3QvYnJvd3Nlci1wcmV2aWV3IjogIjQuMS42IiwgIkB2aXRlc3QvYnJvd3Nlci13ZWJkcml2ZXJpbyI6ICI0LjEuNiIsICJAdml0ZXN0L2NvdmVyYWdlLWlzdGFuYnVsIjogIjQuMS42IiwgIkB2aXRlc3QvY292ZXJhZ2UtdjgiOiAiNC4xLjYiLCAiQHZpdGVzdC91aSI6ICI0LjEuNiIsICJoYXBweS1kb20iOiAiKiIsICJqc2RvbSI6ICIqIiB9LCAib3B0aW9uYWxQZWVycyI6IFsiQGVkZ2UtcnVudGltZS92bSIsICJAb3BlbnRlbGVtZXRyeS9hcGkiLCAiQHR5cGVzL25vZGUiLCAiQHZpdGVzdC9icm93c2VyLXBsYXl3cmlnaHQiLCAiQHZpdGVzdC9icm93c2VyLXByZXZpZXciLCAiQHZpdGVzdC9icm93c2VyLXdlYmRyaXZlcmlvIiwgIkB2aXRlc3QvY292ZXJhZ2UtaXN0YW5idWwiLCAiQHZpdGVzdC9jb3ZlcmFnZS12OCIsICJAdml0ZXN0L3VpIiwgImhhcHB5LWRvbSIsICJqc2RvbSJdLCAiYmluIjogeyAidml0ZXN0IjogInZpdGVzdC5tanMiIH0gfSwgInNoYTUxMi02bHZqYlMzcDliNENyZENtZ3V6YmgyLzR1b1hoR0UycTcxUjRPWDVzcUY5UjFibzlYZDZmR3JNQWZ2cDV3bkN6bEJuRlZkQ09wNm9udVRRVmJvOGlVUT09Il0sCgogICAgIndoeS1pcy1ub2RlLXJ1bm5pbmciOiBbIndoeS1pcy1ub2RlLXJ1bm5pbmdAMi4zLjAiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAic2lnaW5mbyI6ICJeMi4wLjAiLCAic3RhY2tiYWNrIjogIjAuMC4yIiB9LCAiYmluIjogeyAid2h5LWlzLW5vZGUtcnVubmluZyI6ICJjbGkuanMiIH0gfSwgInNoYTUxMi1oVXJtYVdCZFZEY3h2WXFueWgwOXp1bkt6Uk9XamJaVGlOeThkQkVqa1M3ZWhFRFFpYlhKN1h2bG10Ynd1VGNsVWlJeU4rQ3lYUUQ0Vm1rbzhmTm04dz09Il0sCgogICAgIndyYXAtYW5zaSI6IFsid3JhcC1hbnNpQDcuMC4wIiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgImFuc2ktc3R5bGVzIjogIl40LjAuMCIsICJzdHJpbmctd2lkdGgiOiAiXjQuMS4wIiwgInN0cmlwLWFuc2kiOiAiXjYuMC4wIiB9IH0sICJzaGE1MTItWVZHSWoya2FtTFNUeHc2TnNaam9CeGZTd3NuMHljZGVzbWM0cCtRMjFjNXpQdVoxcGwrTmZ4VmR4UHRkSHZtTlZPUTZYU1lHNEFVdHl0L0ZpN0QxNlE9PSJdLAoKICAgICJ5MThuIjogWyJ5MThuQDUuMC44IiwgIiIsIHt9LCAic2hhNTEyLTBwZkZ6ZWdlRFdKSEpJQW1UTFJQMkR3SGpkRjVzN2pvOXR1enRkUXhBaElOQ2R2UyszbkdJTnFQZDAwQXBocUpSLzBMaEFOVVM2Lys3U0NiOThZT2ZBPT0iXSwKCiAgICAieWFyZ3MiOiBbInlhcmdzQDE3LjcuMiIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJjbGl1aSI6ICJeOC4wLjEiLCAiZXNjYWxhZGUiOiAiXjMuMS4xIiwgImdldC1jYWxsZXItZmlsZSI6ICJeMi4wLjUiLCAicmVxdWlyZS1kaXJlY3RvcnkiOiAiXjIuMS4xIiwgInN0cmluZy13aWR0aCI6ICJeNC4yLjMiLCAieTE4biI6ICJeNS4wLjUiLCAieWFyZ3MtcGFyc2VyIjogIl4yMS4xLjEiIH0gfSwgInNoYTUxMi03ZFN6elJRKytDS25OSS9rcktuWVJWN0pLS1BVWE1FaDYxc29hSEtnOW1yV0VoekZXaEZueFB4R2wrNjljRDFPdTYzQzEzTlVQQ25tSWNydnFDdU02dz09Il0sCgogICAgInlhcmdzLXBhcnNlciI6IFsieWFyZ3MtcGFyc2VyQDIxLjEuMSIsICIiLCB7fSwgInNoYTUxMi10VnBzSlc3RGRqZWNBaUZwYklCMWUzcXhJUXNFNk5vUGM1L2VUZHJiYklDNGgwTFZzV2hub2EzZyttMkhjbEJJdWpIenN4WjRWSlZBK0dVdWMyL0xCdz09Il0sCgogICAgInpvZCI6IFsiem9kQDQuNC4zIiwgIiIsIHt9LCAic2hhNTEyLXl0RU5GaklKRmwyVXdZZ2xkZTJqY2hXMkh3bTRHSkZMRGlTWFdkVHJKUUJJTjlGY3lwN240RGh4SkVpV05BSk1WMS9CcVdmVy9ra2c3MVVEY0hKeVRRPT0iXSwKCiAgICAiQHRhaWx3aW5kY3NzL294aWRlLXdhc20zMi13YXNpL0BlbW5hcGkvY29yZSI6IFsiQGVtbmFwaS9jb3JlQDEuMTAuMCIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAZW1uYXBpL3dhc2ktdGhyZWFkcyI6ICIxLjIuMSIsICJ0c2xpYiI6ICJeMi40LjAiIH0sICJidW5kbGVkIjogdHJ1ZSB9LCAic2hhNTEyLXlxNk9rSjRwODJDQWZQbDB1OW1RZWJRSEtQSmtZN1dySXVrMjA1Y1RZblllK2syWjhZQmgxMUZyYlJHL0g2aWhpcnFjYWNPZ2wyQklPOG95TVFMZVh3PT0iXSwKCiAgICAiQHRhaWx3aW5kY3NzL294aWRlLXdhc20zMi13YXNpL0BlbW5hcGkvcnVudGltZSI6IFsiQGVtbmFwaS9ydW50aW1lQDEuMTAuMCIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJ0c2xpYiI6ICJeMi40LjAiIH0sICJidW5kbGVkIjogdHJ1ZSB9LCAic2hhNTEyLWV3dllsazg2eFVvR0kwelFSTnEvbUMrMTZSMVFlRGxLUXkyMUtpM29TWVhOZ0xiNDVHVjFQNkEwTSsvczZueUN1TkRxZTVWcGFZODRCelhHd1Zid0ZBPT0iXSwKCiAgICAiQHRhaWx3aW5kY3NzL294aWRlLXdhc20zMi13YXNpL0BlbW5hcGkvd2FzaS10aHJlYWRzIjogWyJAZW1uYXBpL3dhc2ktdGhyZWFkc0AxLjIuMSIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJ0c2xpYiI6ICJeMi40LjAiIH0sICJidW5kbGVkIjogdHJ1ZSB9LCAic2hhNTEyLXVUSUk3T1lGKy9NZXMvTXJjSU9ZcDV5T3RTTUxCV1NJb0xQcGNnd2lwb2lLYmxpNmszMjJ0Y29Gc3hvSUl4UERxVzAxU1FHQWdrbzRFelppMkJOdjJ3PT0iXSwKCiAgICAiQHRhaWx3aW5kY3NzL294aWRlLXdhc20zMi13YXNpL0BuYXBpLXJzL3dhc20tcnVudGltZSI6IFsiQG5hcGktcnMvd2FzbS1ydW50aW1lQDEuMS40IiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIkB0eWJ5cy93YXNtLXV0aWwiOiAiXjAuMTAuMSIgfSwgInBlZXJEZXBlbmRlbmNpZXMiOiB7ICJAZW1uYXBpL2NvcmUiOiAiXjEuNy4xIiwgIkBlbW5hcGkvcnVudGltZSI6ICJeMS43LjEiIH0sICJidW5kbGVkIjogdHJ1ZSB9LCAic2hhNTEyLTNOUU5OZ0ExWVNsSmIva01IMWlsZEFTUDlIVzcvN2tZblJJMnN6V0phb2ZhUzFoV21iR0k0SCtkMysyMmFHelhYTjlJSituK0dpRlZjR2lwSlAxOG93PT0iXSwKCiAgICAiQHRhaWx3aW5kY3NzL294aWRlLXdhc20zMi13YXNpL0B0eWJ5cy93YXNtLXV0aWwiOiBbIkB0eWJ5cy93YXNtLXV0aWxAMC4xMC4yIiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgInRzbGliIjogIl4yLjQuMCIgfSwgImJ1bmRsZWQiOiB0cnVlIH0sICJzaGE1MTItUm9CdkoyWDB3dUtsV0ZJanJ3ZmZHdzFJcVpIS1FxekljaEthYWRaWmZuTnBzQVlwMm1NMGgzNkp0UENqTkRBSEdnWWV6LzE1dU1CcGZHd2NoaGlNZ2c9PSJdLAoKICAgICJAdGFpbHdpbmRjc3Mvb3hpZGUtd2FzbTMyLXdhc2kvdHNsaWIiOiBbInRzbGliQDIuOC4xIiwgIiIsIHsgImJ1bmRsZWQiOiB0cnVlIH0sICJzaGE1MTItb0pGdTk0SFFiK0tWZHVTVVFMN3ducG1xbmZtTHNPQS9uQWg2YjZFSDB3Q0VvSzAvbVBlWFU2YzN3S0RWODNNa091SFBSSHRTWEtLVTk5SUJhelMvMnc9PSJdLAoKICAgICJjaGFsay9zdXBwb3J0cy1jb2xvciI6IFsic3VwcG9ydHMtY29sb3JANy4yLjAiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiaGFzLWZsYWciOiAiXjQuMC4wIiB9IH0sICJzaGE1MTItcXBDQXZSbDlzdHVPSHZlS3NuN0huY0pSdnY1MDFxSWFjS3pRbE8vK0x3eGM5KzBxMndMeXY0RGZ2dDgwL0RQbjJwcU9Cc0pkRGlvZ1hHUjkrT3Z3Unc9PSJdLAoKICAgICJuZXh0L3Bvc3Rjc3MiOiBbInBvc3Rjc3NAOC40LjMxIiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIm5hbm9pZCI6ICJeMy4zLjYiLCAicGljb2NvbG9ycyI6ICJeMS4wLjAiLCAic291cmNlLW1hcC1qcyI6ICJeMS4wLjIiIH0gfSwgInNoYTUxMi1QUzA4SWJvaWE5bXRzLzJ5Z1YzZUxwWTVnaG5VY2ZMVi9FWFRPVzFFMnFZeEpLR0dCVXROak43NkZZSG5NczM2Um1BUm40MWJDMEFabW4rclIwT1ZwUT09Il0sCiAgfQp9Cg==" }, { "path": ".env.example", "kind": "text", "content": "# Your tenant public key. Get it from the desk's Developers tab.\n# `cpk_live_*` / `cpk_test_*` keys auto-route same-origin requests\n# to hosted Cimplify in both dev and prod. Anything else (`mock-dev`,\n# empty) falls back to the local `cimplify dev` mock.\nNEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev\n\n# Optional. Forces a fixed canonical URL for sitemap.xml, robots.txt,\n# OpenGraph, and llms.txt. Leave unset and the storefront derives the\n# canonical from the request `Host` header automatically.\n# NEXT_PUBLIC_SITE_URL=\n" }], "storefront-auto": [{ "path": "vitest.config.ts", "kind": "text", "content": 'import { defineConfig } from "vitest/config";\n\nexport default defineConfig({\n test: {\n environment: "node",\n include: ["__tests__/**/*.test.ts"],\n globals: false,\n },\n});\n' }, { "path": "__tests__/cart-flow.test.ts", "kind": "text", "content": 'import { createCartFlowSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateCartFlowSuite({ seed: brand.mock.seed, businessId: brand.mock.businessId });\n' }, { "path": "__tests__/brand.test.ts", "kind": "text", "content": 'import { createBrandSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateBrandSuite({ brand });\n' }, { "path": "__tests__/contract.test.ts", "kind": "text", "content": 'import { createContractSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateContractSuite({ seed: brand.mock.seed });\n' }, { "path": "AGENTS.md", "kind": "text", "content": '# AGENTS.md \u2014 Auto-parts storefront template\n\nIf you are an AI agent working on this storefront, **start here.**\n\n## TL;DR for rebranding\n\n1. **Edit `lib/brand.ts`** \u2014 every visible string, plus the `fitments` make/model/year tree.\n2. **Edit `app/globals.css`** \u2014 `@theme { \u2026 }` block for palette + radius + font references.\n3. **Edit `.env.local`** \u2014 `NEXT_PUBLIC_CIMPLIFY_*` env vars.\n\n## Aesthetic\n\n- **Inter + JetBrains Mono** \u2014 clean modern sans, mono-spaced labels for spec-feel.\n- **Carbon-black foreground + sport-red accent.**\n- **Sharp corners**: `0.5rem` \u2014 workshop-modern.\n- **`AutoPartsStore`** is the Schema.org `@type` (set in `brand.schemaType`).\n\n## Page surface\n\n```\napp/\n page.tsx AutoHero + FitmentFinder + TrustBar + Most ordered\n grid + ServiceBrief + Newsletter\n shop/page.tsx SDK <CataloguePage/>. Reads `?fits=<tag>` to\n filter by fitment tag.\n search/page.tsx Search\n collections/[slug]/page.tsx Collection landing\n categories/[slug]/page.tsx Category landing\n products/[slug]/page.tsx Full product detail page (Product JSON-LD)\n cart/page.tsx, checkout/page.tsx, orders/[id]/page.tsx SDK\n account/* + login/signup\n contact/, track-order/, about, faq, terms, privacy, shipping, returns,\n accessibility/, sitemap-page/ Brand-driven content pages\n```\n\n## File \u2194 brand-field map\n\n| File | Reads from `brand` |\n|---|---|\n| `app/layout.tsx` | identity, contact, socials, schemaType (AutoPartsStore JSON-LD) |\n| `app/page.tsx` | `brand.hero`, `brand.fitments`, `brand.serviceBrief`, `brand.newsletter` |\n| `app/about/page.tsx` | `brand.about` |\n| `app/faq/page.tsx` | `brand.faq` |\n| `app/terms/page.tsx` | `brand.terms` |\n| `app/privacy/page.tsx` | `brand.privacy` |\n| `app/shipping/page.tsx` | `brand.shipping` |\n| `app/returns/page.tsx` | `brand.returns` |\n| `app/accessibility/page.tsx` | `brand.accessibility` |\n| `app/contact/page.tsx` | `brand.contactPage`, `brand.contact` |\n| `app/track-order/page.tsx` | `brand.trackOrder` |\n| `app/products/[slug]/page.tsx` | `brand.name`, `brand.currency` |\n| `app/llms.txt/route.ts` | `brand.llms`, contact, currency |\n| `components/auto-hero.tsx` | `brand.hero`, hard-coded trust-chip labels |\n| `components/fitment-finder.tsx` | `brand.fitments` (makes/models/years tree) |\n| `components/service-brief.tsx` | `brand.serviceBrief` (3 cards) |\n| `components/header.tsx`, `components/footer.tsx` | `brand.shortName`, `brand.header.nav`, `brand.footer`, `brand.contact`, `brand.socials` |\n| `components/trust-bar.tsx` | `brand.trustItems` |\n| `components/newsletter.tsx` | `brand.newsletter` |\n\n## Auto-specific notes\n\n- **Fitment is tag-based, not a custom_attribute.** Each product carries `tags: ["fits:toyota", "fits:toyota:corolla"]` etc. or `["fits:universal"]`. The shop page reads `?fits=<tag>` and filters by tag prefix match. Universal-fit products bypass the filter.\n- **Adding a vehicle**: edit `brand.fitments.makes` in `lib/brand.ts`. Add `{ slug, name, models: [...] }`. Slug becomes the URL fragment.\n- **Adding a part with new fitment**: tag the product `fits:<make>` and (optionally) `fits:<make>:<model>`. No schema change needed.\n- **Partner-workshop fitting**: the mock doesn\'t model this; the storefront copy describes it. To wire it, eject the cart drawer and add a "Add fitting (GH\u20B580)" toggle that adds a service line.\n\n## Known TODOs\n\n- Contact form + newsletter signup currently fake their submit. Wire to a real provider.\n- The fitment finder uses `useRouter().push` to a query-string URL; the catalogue page-side filter is left as an exercise for the merchant (read the `fits` param and pass it as a SDK `getProducts({ tags: [...] })` filter).\n\n## Mock seed\n\nWired to `--seed auto` (Driveline Auto Parts). Edit `dev:mock` in `package.json` to preview another seed.\n\n## Customizing SDK components\n\nFor anything beyond `lib/brand.ts` + `app/globals.css`, lean on the SDK\'s prebuilt components rather than reinvent. **Especially for product customization** (variants, add-ons, fitment-tag filtering, services with scheduling) \u2014 the SDK already gets price math, axis matching, tag filtering, and cart payload contracts right. Default to ejecting and restyling:\n\n```bash\ncimplify add cart-drawer\ncimplify add variant-selector\ncimplify add product-page\n```\n\nThen edit the local copy. **Don\'t change the cart payload shape** unless you\'re also touching the SDK mock + backend lens. Full ejection rules and the customizer contract are in the SDK-level [`AGENTS.md`](../../AGENTS.md) \u2192 "Don\'t reinvent product customization".\n\n## Quick start\n\n```bash\nbun install\nbun dev\n```\n\nOpen <http://localhost:3000>.\n' }, { "path": "postcss.config.mjs", "kind": "text", "content": 'const config = {\n plugins: {\n "@tailwindcss/postcss": {},\n },\n};\n\nexport default config;\n' }, { "path": "components/trade-in-cta.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport { brand } from "@/lib/brand";\n\nexport function TradeInCta() {\n const t = brand.tradeIn;\n if (!t) return null;\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-14 sm:py-20">\n <div className="grid grid-cols-1 lg:grid-cols-2 gap-8 items-center bg-foreground text-background rounded-3xl p-8 sm:p-12 lg:p-14 overflow-hidden relative">\n <div className="absolute top-0 right-0 w-1/2 h-full opacity-[0.04] [background-image:linear-gradient(135deg,white_1px,transparent_1px)] [background-size:24px_24px] pointer-events-none" />\n <div>\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary-foreground/70 mb-3">\n {t.eyebrow}\n </p>\n <h2 className="text-[clamp(1.75rem,3.5vw,2.5rem)] font-bold m-0 mb-4 -tracking-[0.025em] leading-[1.1]">\n {t.title}\n </h2>\n <p className="text-background/75 leading-relaxed mb-6">{t.body}</p>\n <div className="flex flex-wrap items-center gap-3">\n <Link\n href={t.primaryCtaHref}\n className="inline-flex items-center gap-2 px-5 py-2.5 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors"\n >\n {t.primaryCtaLabel}\n <svg viewBox="0 0 12 12" className="w-3 h-3" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden>\n <path d="M3 6h7m0 0L7 3m3 3L7 9" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n </Link>\n <Link\n href={t.secondaryCtaHref}\n className="inline-flex items-center gap-2 px-5 py-2.5 rounded-md border border-background/25 hover:bg-background/10 transition-colors text-sm font-medium"\n >\n {t.secondaryCtaLabel}\n </Link>\n </div>\n </div>\n <div className="grid grid-cols-3 gap-3">\n {t.steps.map((s) => (\n <div\n key={s.step}\n className="rounded-2xl p-4 sm:p-5 bg-background/5 border border-background/10"\n >\n <p className="text-[10px] font-mono text-primary tabular-nums mb-3">{s.step}</p>\n <p className="text-[13px] sm:text-sm font-semibold mb-1.5 -tracking-[0.015em]">\n {s.title}\n </p>\n <p className="text-[11px] sm:text-xs text-background/65 leading-snug">{s.body}</p>\n </div>\n ))}\n </div>\n </div>\n </section>\n );\n}\n' }, { "path": "components/cart-pill.tsx", "kind": "text", "content": '"use client";\n\nimport { useCartDrawer } from "@cimplify/sdk/react";\nimport { useCartCount } from "@/lib/cart";\n\n/**\n * Cart pill \u2014 dynamic island. Reads the live cart count via the SDK and\n * opens the side cart drawer on click (instead of navigating to /cart).\n * Wrap in `<Suspense fallback={<CartPillSkeleton/>}>` so the cached\n * header chrome streams without blocking on the cart fetch.\n */\nexport function CartPill() {\n const { count } = useCartCount();\n const { open } = useCartDrawer();\n return (\n <button\n type="button"\n onClick={open}\n aria-label={`Open cart, ${count} ${count === 1 ? "item" : "items"}`}\n className="inline-flex items-center gap-1.5 px-4 py-2.5 sm:py-2 rounded-full bg-foreground text-background text-xs font-semibold tracking-wide transition-transform hover:scale-105 cursor-pointer"\n >\n Cart \xB7 {count}\n </button>\n );\n}\n\nexport function CartPillSkeleton() {\n return (\n <span\n aria-hidden\n className="inline-flex items-center gap-1.5 px-4 py-2.5 sm:py-2 rounded-full bg-foreground/80 text-background text-xs font-semibold tracking-wide"\n >\n Cart \xB7 \u2026\n </span>\n );\n}\n' }, { "path": "components/category-tiles.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport Image from "next/image";\nimport type { Category } from "@cimplify/sdk";\n\ninterface CategoryTilesProps {\n categories: Category[];\n}\n\nconst ICONS: Record<string, React.ReactNode> = {\n phones: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-7 h-7" aria-hidden>\n <rect x="6" y="2" width="12" height="20" rx="2.5" />\n <line x1="12" y1="18" x2="12" y2="18" strokeWidth="2.5" strokeLinecap="round" />\n </svg>\n ),\n laptops: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-7 h-7" aria-hidden>\n <rect x="3" y="4" width="18" height="12" rx="1.5" />\n <line x1="2" y1="20" x2="22" y2="20" strokeLinecap="round" />\n </svg>\n ),\n audio: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-7 h-7" aria-hidden>\n <path d="M3 14v-2a9 9 0 0 1 18 0v2" />\n <rect x="2" y="14" width="5" height="6" rx="1.5" />\n <rect x="17" y="14" width="5" height="6" rx="1.5" />\n </svg>\n ),\n accessories: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-7 h-7" aria-hidden>\n <rect x="2" y="6" width="14" height="10" rx="2" />\n <path d="M16 12h4l2 2v0l-2 2h-4" />\n </svg>\n ),\n gaming: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-7 h-7" aria-hidden>\n <path d="M5 7h14a3 3 0 0 1 3 3v4a3 3 0 0 1-3 3h-1l-2-3H8l-2 3H5a3 3 0 0 1-3-3v-4a3 3 0 0 1 3-3z" />\n <line x1="9" y1="11" x2="9" y2="13" strokeLinecap="round" />\n <line x1="8" y1="12" x2="10" y2="12" strokeLinecap="round" />\n <circle cx="15" cy="11" r="0.75" fill="currentColor" />\n <circle cx="17" cy="13" r="0.75" fill="currentColor" />\n </svg>\n ),\n};\n\nexport function CategoryTiles({ categories }: CategoryTilesProps) {\n if (categories.length === 0) return null;\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-14 sm:py-20">\n <div className="flex items-end justify-between gap-6 mb-8">\n <div>\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2">\n Shop the catalogue\n </p>\n <h2 className="text-[clamp(1.75rem,3vw,2.25rem)] font-bold -tracking-[0.025em]">\n Pick your category.\n </h2>\n </div>\n <Link\n href="/shop"\n className="text-sm font-semibold text-primary hover:underline whitespace-nowrap hidden sm:inline-flex items-center gap-1"\n >\n See everything\n <svg viewBox="0 0 12 12" className="w-3 h-3" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden>\n <path d="M3 6h7m0 0L7 3m3 3L7 9" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n </Link>\n </div>\n <div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3 sm:gap-4">\n {categories.map((c, i) => (\n <Link\n key={c.id}\n href={`/categories/${c.slug}`}\n className="group relative overflow-hidden rounded-2xl bg-card border border-border p-5 hover:border-primary hover:-translate-y-0.5 transition-all"\n >\n <div className="flex items-start justify-between mb-12">\n <div className="grid place-items-center w-10 h-10 rounded-lg bg-primary/10 text-primary">\n {ICONS[c.slug] ?? (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-7 h-7" aria-hidden>\n <rect x="3" y="3" width="18" height="18" rx="2" />\n </svg>\n )}\n </div>\n <span className="text-[10px] font-mono text-muted-foreground tabular-nums">\n {String(i + 1).padStart(2, "0")}\n </span>\n </div>\n <p className="text-base font-semibold mb-1 -tracking-[0.015em]">{c.name}</p>\n {c.product_count != null && (\n <p className="text-xs text-muted-foreground">\n {c.product_count} {c.product_count === 1 ? "product" : "products"}\n </p>\n )}\n <span className="absolute right-4 bottom-4 grid place-items-center w-7 h-7 rounded-full bg-foreground text-background opacity-0 group-hover:opacity-100 transition-opacity">\n <svg viewBox="0 0 12 12" className="w-3 h-3" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden>\n <path d="M3 6h7m0 0L7 3m3 3L7 9" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n </span>\n </Link>\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "components/promo-banner.tsx", "kind": "text", "content": `import Link from "next/link";
2062
2062
  import { brand } from "@/lib/brand";
2063
2063
 
2064
2064
  /**
@@ -2999,7 +2999,7 @@ export const brand: Brand = {
2999
2999
  businessId: "bus_driveline_auto",
3000
3000
  },
3001
3001
  };
3002
- ` }, { "path": "lib/cimplify-loader.ts", "kind": "text", "content": 'import type { ImageLoader } from "next/image";\n\nimport { assetUrl, isCimplifyAsset } from "@cimplify/sdk";\n\nconst cdnBase = process.env.NEXT_PUBLIC_CIMPLIFY_CDN_URL?.trim() || undefined;\n\nconst cimplifyImageLoader: ImageLoader = ({ src, width, quality }) => {\n if (isCimplifyAsset(src, cdnBase)) {\n return assetUrl(src, {\n base: cdnBase,\n w: width,\n quality,\n format: "auto",\n });\n }\n return src;\n};\n\nexport default cimplifyImageLoader;\n' }, { "path": "lib/site.config.ts", "kind": "text", "content": "/**\n * Canonical absolute URL. The platform overwrites this at build time with the\n * storefront's primary domain; the default only applies to local builds. It's\n * per-deploy config, not per-request, so it stays a static constant \u2014 keeping\n * the root layout prerenderable under `cacheComponents`.\n */\nexport const SITE_URL = \"https://example.com\";\n" }, { "path": "lib/cart.ts", "kind": "text", "content": '"use client";\n\nimport { useCart } from "@cimplify/sdk/react";\n\n/**\n * Reactive cart count for the header pill. Subscribes to the SDK cart\n * state \u2014 updates immediately on add / remove / quantity change.\n */\nexport function useCartCount(): { count: number } {\n const { itemCount } = useCart();\n return { count: itemCount };\n}\n' }, { "path": "lib/site-url.ts", "kind": "text", "content": 'import { SITE_URL } from "./site.config";\n\n/** Canonical absolute URL for this storefront. */\nexport async function getSiteUrl(): Promise<string> {\n return SITE_URL;\n}\n' }, { "path": "metadata.json", "kind": "text", "content": '{\n "id": "auto",\n "name": "Auto Parts",\n "tagline": "Auto-parts storefront with vehicle fitment finder and installation booking.",\n "industry": "automotive",\n "tags": ["automotive", "parts", "fitment"],\n "stability": "stable",\n "schemaType": "AutoPartsStore",\n "mock": {\n "seedName": "auto",\n "seedBusinessId": "bus_driveline_auto"\n }\n}\n' }, { "path": "tsconfig.json", "kind": "text", "content": '{\n "compilerOptions": {\n "target": "ES2022",\n "lib": ["dom", "dom.iterable", "esnext"],\n "allowJs": false,\n "skipLibCheck": true,\n "strict": true,\n "noEmit": true,\n "esModuleInterop": true,\n "module": "esnext",\n "moduleResolution": "bundler",\n "resolveJsonModule": true,\n "isolatedModules": true,\n "jsx": "preserve",\n "incremental": true,\n "plugins": [{ "name": "next" }],\n "paths": {\n "@/*": ["./*"]\n }\n },\n "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts"],\n "exclude": ["node_modules"]\n}\n' }, { "path": "CLAUDE.md", "kind": "text", "content": "# CLAUDE.md\n\nThis project was scaffolded from a Cimplify storefront template (`cimplify init`).\n\n## Read these first\n\n- **`AGENTS.md`** at the project root \u2014 the file \u2194 `brand.X` field map for *this template's* industry, plus the architectural rules.\n- **`.claude/skills/cimplify-storefront/SKILL.md`** \u2014 the playbook for common tasks (rebrand, add a section, wire a Server Action, deploy, eject a component). Invoke it whenever you're asked to change content, palette, copy, or routing.\n\n## The two-line summary\n\n1. **Edit `lib/brand.ts`** for any visible string.\n2. **Edit `app/globals.css` `@theme` block** for any palette / radius / font change.\n\nThat covers ~95% of what merchants ask for. For anything else, follow the `cimplify-storefront` skill.\n\n## Don't do\n\n- Hardcode strings in pages or components.\n- Disable `cacheComponents: true` in `next.config.ts`.\n- Use `unstable_cache` \u2014 Next 16's canonical primitive is `'use cache'`.\n- Run `bun test` (Bun's `vi` shim is incomplete) \u2014 use `bun run test:run`.\n" }, { "path": "README.md", "kind": "text", "content": "# __STOREFRONT_NAME__\n\nA Cimplify storefront scaffolded from the **auto** template \u2014 Next.js 16 (App Router), React 19, Tailwind v4. Carbon-black + sport-red palette, Inter + JetBrains Mono typography, workshop-modern aesthetic.\n\n## Run\n\n```bash\nbun install\nbun dev\n```\n\nTwo things start in parallel:\n\n- `cimplify-mock --seed auto` \u2014 the Cimplify mock API on `http://127.0.0.1:8787`, seeded with Driveline Auto Parts.\n- `next dev` \u2014 this storefront on `http://localhost:3000`.\n\nOpen the storefront in your browser. Edit `app/page.tsx` to start customising.\n\n## Auto-specific behaviour\n\n- **Fitment finder** (`components/fitment-finder.tsx`): Make \u2192 Model \u2192 Year selector on the home page. Submitting pushes `?fits=fits:<make>:<model>` onto `/shop`; the shop page filters the catalogue by matching `tags` on `MockProduct`. Products tagged `fits:universal` always appear.\n- Vehicle data lives in `brand.fitments` (lib/brand.ts) \u2014 add a make/model/year by editing that array.\n- `Bosch`, `Mobil 1`, `Castrol`, `NGK`, `Brembo`, `Bridgestone` brand strip \u2014 swap to your own authorised brands.\n- `AutoPartsStore` schema.org `@type` is set in `brand.schemaType` so JSON-LD on every page identifies the business correctly.\n\n## Switch the seed\n\n```bash\ncimplify-mock --seed pharmacy # Wellspring Pharmacy\ncimplify-mock --seed restaurant # Mama's Kitchen\ncimplify-mock --seed retail # Currents Electronics\n```\n\n## Go live\n\n```diff\n# .env.local\n- NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev\n+ NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=<your tenant key>\n```\n\nDeploy with `cimplify deploy --prod` after linking the project. See [`cimplify` CLI docs](https://www.cimplify.dev/docs/cli). `next.config.ts` already whitelists the SDK image hosts under `images.remotePatterns`.\n" }, { "path": ".env.example", "kind": "text", "content": "# Your tenant public key. Get it from the desk's Developers tab.\n# `cpk_live_*` / `cpk_test_*` keys auto-route same-origin requests\n# to hosted Cimplify in both dev and prod. Anything else (`mock-dev`,\n# empty) falls back to the local `cimplify dev` mock.\nNEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev\n\n# Optional. Forces a fixed canonical URL for sitemap.xml, robots.txt,\n# OpenGraph, and llms.txt. Leave unset and the storefront derives the\n# canonical from the request `Host` header automatically.\n# NEXT_PUBLIC_SITE_URL=\n" }], "storefront-restaurant": [{ "path": "vitest.config.ts", "kind": "text", "content": 'import { defineConfig } from "vitest/config";\n\nexport default defineConfig({\n test: {\n environment: "node",\n include: ["__tests__/**/*.test.ts"],\n globals: false,\n },\n});\n' }, { "path": "__tests__/cart-flow.test.ts", "kind": "text", "content": 'import { createCartFlowSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateCartFlowSuite({ seed: brand.mock.seed, businessId: brand.mock.businessId });\n' }, { "path": "__tests__/brand.test.ts", "kind": "text", "content": 'import { createBrandSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateBrandSuite({ brand });\n' }, { "path": "__tests__/contract.test.ts", "kind": "text", "content": 'import { createContractSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateContractSuite({ seed: brand.mock.seed });\n' }, { "path": "AGENTS.md", "kind": "text", "content": '# AGENTS.md \u2014 Restaurant storefront template\n\nIf you are an AI agent (Claude, Cursor, Aider, devin, \u2026) working on this storefront, **start here.**\n\n## TL;DR for rebranding\n\n1. **Edit `lib/brand.ts`** \u2014 every visible string lives here.\n2. **Edit `app/globals.css`** \u2014 `@theme { \u2026 }` for palette + radius.\n3. **Edit `.env.local`** \u2014 set `NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY` (the only required var).\n## Aesthetic\n\n- **Lora serif + Inter** \u2014 warm, food-friendly headings + clean body.\n- **Forest green + cream** \u2014 deep sage primary, warm cream background.\n- **Mid radius**: `0.625rem`.\n- Schema.org `@type` is `Restaurant`.\n\n## Page surface\n\n```\napp/\n page.tsx Home \u2014 hero with daily-special badge,\n collection strips, category grid\n shop/page.tsx Full menu (SDK <CataloguePage/>)\n search/page.tsx Search\n collections/[slug]/page.tsx Collection landing (e.g. brunch, mains)\n categories/[slug]/page.tsx Category landing\n\n reservations/page.tsx \u2B50 Restaurant-specific: reservation widget\n (date + lunch/dinner slot + party size)\n backed by service-type "seating" products\n\n cart/page.tsx, checkout/page.tsx, orders/[id]/page.tsx\n\n account/page.tsx <CimplifyAccount /> (iframe)\n account/orders/page.tsx <CimplifyAccount section="orders" />\n account/addresses/page.tsx <CimplifyAccount section="addresses" />\n account/settings/page.tsx <CimplifyAccount section="settings" />\n login/page.tsx, signup/page.tsx redirects \u2192 /account\n\n contact/page.tsx, track-order/page.tsx\n\n about/page.tsx, faq/page.tsx\n shipping/page.tsx (delivery), returns/page.tsx (refunds), accessibility/page.tsx\n terms/page.tsx, privacy/page.tsx\n\n sitemap-page/page.tsx, sitemap.ts, robots.ts, llms.txt/route.ts, opensearch.xml/route.ts\n error.tsx, not-found.tsx\n```\n\n## File \u2194 brand-field map\n\n| File | Reads from `brand` |\n|---|---|\n| `app/layout.tsx` | identity, contact, socials, Restaurant JSON-LD |\n| `app/page.tsx` | `brand.hero` (daily-special badge) |\n| `app/about/page.tsx` | `brand.about` |\n| `app/faq/page.tsx` | `brand.faq` (reservations, dietary, delivery) |\n| `app/shipping/page.tsx` | `brand.shipping` (delivery + pickup) |\n| `app/returns/page.tsx` | `brand.returns` (refund + cancellation) |\n| `app/accessibility/page.tsx` | `brand.accessibility` |\n| `app/terms/page.tsx`, `app/privacy/page.tsx` | `brand.terms`, `brand.privacy` |\n| `app/contact/page.tsx` | `brand.contactPage`, `brand.contact` |\n| `app/track-order/page.tsx` | `brand.trackOrder` |\n| `app/account/*/page.tsx` | `brand.account` (iframe owns the UI) |\n| `app/reservations/*` | derives seating from service-type products in the catalogue |\n| `app/llms.txt/route.ts` | `brand.llms`, contact, currency |\n| `components/header.tsx`, `footer.tsx` | `brand.header`, `brand.footer`, `brand.contact`, `brand.socials` |\n\n## Restaurant-specific notes\n\n- **Reservations are service-type products.** Add seating options like "Two-top", "Four-top", "Long table for 12" to the catalogue with `type: "service"`. The `/reservations` page filters them in.\n- The reservation widget adds the chosen seating to cart with the date, time, party size, and notes as cart-item notes; checkout finalises the booking via Cimplify\'s order pipeline.\n- Schema.org `@type` is `Restaurant`. `servesCuisine` etc. can be added to the JSON-LD object in `app/layout.tsx` if needed.\n- Mock seed: `--seed restaurant` (Mama\'s Kitchen).\n\n## Known TODOs\n\n- `/reservations` widget assumes every slot is bookable. For production, wire `useAvailableSlots({ serviceId, date })` from `@cimplify/sdk/react` to filter to actually-free windows.\n- Contact form + newsletter fake their submits.\n- Hero / dish photography is Unsplash \u2014 replace with merchant photos.\n\n## Customizing SDK components\n\nFor anything beyond `lib/brand.ts` + `app/globals.css`, lean on the SDK\'s prebuilt components rather than reinvent. **Especially for product customization** (variants, add-ons, bundles, composites, services with scheduling) \u2014 the SDK already gets price math, axis matching, and cart payload contracts right. Default to ejecting and restyling:\n\n```bash\ncimplify add cart-drawer\ncimplify add add-on-selector\ncimplify add product-page\n```\n\nThen edit the local copy. **Don\'t change the cart payload shape** unless you\'re also touching the SDK mock + backend lens. Full ejection rules and the customizer contract are in the SDK-level [`AGENTS.md`](../../AGENTS.md) \u2192 "Don\'t reinvent product customization".\n\n## Quick start\n\n```bash\nbun install\nbun dev\n```\n\nOpen <http://localhost:3000>.\n' }, { "path": "postcss.config.mjs", "kind": "text", "content": 'const config = {\n plugins: {\n "@tailwindcss/postcss": {},\n },\n};\n\nexport default config;\n' }, { "path": "components/cart-pill.tsx", "kind": "text", "content": '"use client";\n\nimport { useCartDrawer } from "@cimplify/sdk/react";\nimport { useCartCount } from "@/lib/cart";\n\n/**\n * Cart pill \u2014 dynamic island. Reads the live cart count via the SDK and\n * opens the side cart drawer on click (instead of navigating to /cart).\n * Wrap in `<Suspense fallback={<CartPillSkeleton/>}>` so the cached\n * header chrome streams without blocking on the cart fetch.\n */\nexport function CartPill() {\n const { count } = useCartCount();\n const { open } = useCartDrawer();\n return (\n <button\n type="button"\n onClick={open}\n aria-label={`Open cart, ${count} ${count === 1 ? "item" : "items"}`}\n className="inline-flex items-center gap-1.5 px-4 py-2.5 sm:py-2 rounded-full bg-foreground text-background text-xs font-semibold tracking-wide transition-transform hover:scale-105 cursor-pointer"\n >\n Cart \xB7 {count}\n </button>\n );\n}\n\nexport function CartPillSkeleton() {\n return (\n <span\n aria-hidden\n className="inline-flex items-center gap-1.5 px-4 py-2.5 sm:py-2 rounded-full bg-foreground/80 text-background text-xs font-semibold tracking-wide"\n >\n Cart \xB7 \u2026\n </span>\n );\n}\n' }, { "path": "components/collection-strip.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport type { Collection, Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "./store-product-card";\n\ninterface CollectionStripProps {\n collection: Collection;\n products: Product[];\n collectionHref?: string;\n}\n\n/**\n * Horizontal strip of products under a collection title. Cards come from the\n * SDK so every variant (Food, Bundle, Composite, Service, \u2026) renders\n * correctly; clicks open the shared URL-driven product modal.\n */\nexport function CollectionStrip({ collection, products, collectionHref }: CollectionStripProps) {\n if (products.length === 0) return null;\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 pt-12">\n <header className="grid grid-cols-[1fr_auto] items-end gap-4 mb-5">\n <h2 className="font-serif text-[28px] font-semibold m-0">{collection.name}</h2>\n {collectionHref && (\n <Link\n href={collectionHref}\n className="text-[13px] font-semibold text-primary hover:underline"\n >\n See all \u2192\n </Link>\n )}\n {collection.description && (\n <p className="col-span-full m-0 mt-1 text-sm text-muted-foreground">\n {collection.description}\n </p>\n )}\n </header>\n <div className="grid grid-flow-col auto-cols-[minmax(220px,1fr)] gap-4 overflow-x-auto snap-x snap-mandatory pb-2">\n {products.slice(0, 8).map((p) => (\n <div key={p.id} className="snap-start">\n <StoreProductCard product={p} />\n </div>\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "components/hero.tsx", "kind": "text", "content": 'interface HeroProps {\n badge?: string;\n title: string;\n subtitle?: string;\n}\n\nexport function Hero({ badge, title, subtitle }: HeroProps) {\n return (\n <section className="px-6 sm:px-8 py-16 text-center bg-gradient-to-b from-accent to-background">\n {badge && (\n <span className="inline-block mb-4 px-3.5 py-1.5 rounded-full bg-card border border-accent text-accent-foreground text-[11px] font-semibold uppercase tracking-[0.12em]">\n {badge}\n </span>\n )}\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold m-0 -tracking-[0.02em]">\n {title}\n </h1>\n {subtitle && (\n <p className="mx-auto mt-2 max-w-xl text-base text-muted-foreground">\n {subtitle}\n </p>\n )}\n </section>\n );\n}\n' }, { "path": "components/account-iframe.tsx", "kind": "text", "content": '"use client";\n\nimport { CimplifyAccount } from "@cimplify/sdk/react";\n\n/**\n * Cimplify Account portal \u2014 iframe-mounted UI hosted by Cimplify Link.\n * Handles sign-in, sign-up, OTP, addresses, payment methods, sessions,\n * and order history. The iframe owns auth state; we just choose which\n * `section` to land on.\n */\nexport function AccountIframe({ section }: { section?: string }) {\n return <CimplifyAccount section={section} />;\n}\n' }, { "path": "components/policy-page.tsx", "kind": "text", "content": 'import type { BrandPolicySection } from "@/lib/brand";\n\ninterface PolicyShape {\n eyebrow: string;\n title: string;\n lastUpdated?: string;\n sections: BrandPolicySection[];\n}\n\n/**\n * Shared layout for shipping / returns / accessibility / terms / privacy.\n * Reads a `{ eyebrow, title, lastUpdated, sections[] }` block from brand.\n */\nexport function PolicyPage({ policy }: { policy: PolicyShape }) {\n return (\n <article className="max-w-3xl mx-auto px-6 sm:px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {policy.eyebrow}\n </p>\n <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-2 -tracking-[0.02em]">\n {policy.title}\n </h1>\n {policy.lastUpdated && (\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {policy.lastUpdated}\n </p>\n )}\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {policy.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="text-2xl font-semibold mt-0">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "components/header.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport { Suspense } from "react";\nimport { NavLink } from "./nav-link";\nimport { CartPill, CartPillSkeleton } from "./cart-pill";\nimport { MobileNav } from "./mobile-nav";\nimport { brand } from "@/lib/brand";\n\n/**\n * Server-rendered header chrome. Brand mark + nav layout streams from the\n * cache; the active-link styling and live cart count are dynamic islands\n * mounted in their own Suspense boundaries so the chrome never blocks.\n */\nexport function Header() {\n return (\n <header className="sticky top-0 z-30 flex items-center justify-between px-6 sm:px-8 py-4 border-b border-border bg-background/90 backdrop-blur-md">\n <Link href="/" className="flex items-baseline gap-2">\n <span className="font-serif text-[22px] font-semibold -tracking-[0.02em]">\n {brand.shortName}\n </span>\n <span className="hidden sm:inline text-[11px] font-medium uppercase tracking-[0.12em] text-muted-foreground">\n {brand.microTag}\n </span>\n </Link>\n <div className="flex items-center gap-3 sm:gap-6">\n <nav className="hidden sm:flex items-center gap-6">\n {brand.header.nav.map((link) => (\n <Suspense key={link.href} fallback={<NavLinkFallback>{link.label}</NavLinkFallback>}>\n <NavLink href={link.href}>{link.label}</NavLink>\n </Suspense>\n ))}\n </nav>\n <Suspense fallback={<CartPillSkeleton />}>\n <CartPill />\n </Suspense>\n <div className="sm:hidden">\n <MobileNav />\n </div>\n </div>\n </header>\n );\n}\n\nfunction NavLinkFallback({ children }: { children: React.ReactNode }) {\n return (\n <span className="text-[13px] font-medium tracking-wide text-muted-foreground">\n {children}\n </span>\n );\n}\n' }, { "path": "components/product-modal.tsx", "kind": "text", "content": '"use client";\n\nimport { useEffect } from "react";\nimport Image from "next/image";\nimport { useRouter, useSearchParams, usePathname } from "next/navigation";\nimport { ProductSheet, useProduct, useCart } from "@cimplify/sdk/react";\n\n/**\n * URL-driven product modal. Reads `?product=<slug>` and renders the SDK\'s\n * `<ProductSheet/>` \u2014 vertical layout with image-on-top, then header, then\n * the variant/add-on/composite/bundle customizer. Closing the modal clears\n * the search param. Deep-linkable and survives reloads.\n */\nexport function ProductModal() {\n const router = useRouter();\n const pathname = usePathname();\n const searchParams = useSearchParams();\n const slug = searchParams?.get("product") ?? null;\n\n const { product } = useProduct(slug ?? "", { enabled: Boolean(slug) });\n const { addItem } = useCart();\n\n useEffect(() => {\n if (!slug) return;\n const original = document.body.style.overflow;\n document.body.style.overflow = "hidden";\n return () => {\n document.body.style.overflow = original;\n };\n }, [slug]);\n\n useEffect(() => {\n if (!slug) return;\n const onKey = (e: KeyboardEvent) => {\n if (e.key === "Escape") close();\n };\n window.addEventListener("keydown", onKey);\n return () => window.removeEventListener("keydown", onKey);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [slug]);\n\n if (!slug) return null;\n\n function close() {\n const next = new URLSearchParams(searchParams?.toString() ?? "");\n next.delete("product");\n const qs = next.toString();\n router.replace(qs ? `${pathname}?${qs}` : pathname, { scroll: false });\n }\n\n return (\n <div\n role="dialog"\n aria-modal="true"\n onClick={close}\n className="fixed inset-0 z-[100] flex items-end sm:items-center justify-center sm:p-6 bg-foreground/50 backdrop-blur-sm"\n >\n <div\n onClick={(e) => e.stopPropagation()}\n className="relative w-full sm:max-w-lg max-h-[92vh] overflow-auto bg-card sm:rounded-3xl rounded-t-3xl shadow-2xl"\n >\n <button\n onClick={close}\n aria-label="Close product details"\n className="absolute top-4 right-4 z-10 w-9 h-9 rounded-full bg-card border border-border text-sm cursor-pointer transition-colors hover:bg-muted"\n >\n \u2715\n </button>\n {product ? (\n <ProductSheet\n product={product}\n onClose={close}\n onAddToCart={async (p, qty, options) => {\n await addItem(p, qty, options);\n close();\n }}\n renderImage={({ src, alt, className }) => (\n <Image\n src={src}\n alt={alt}\n width={1200}\n height={900}\n className={className}\n style={{ width: "100%", height: "auto", objectFit: "cover" }}\n priority\n />\n )}\n classNames={{\n root: "p-6 sm:p-8 gap-4",\n image: "rounded-2xl overflow-hidden -mx-6 sm:-mx-8 -mt-6 sm:-mt-8 mb-2",\n header: "flex items-baseline justify-between gap-4",\n name: "font-serif text-2xl font-semibold m-0",\n price: "text-lg font-semibold text-primary",\n description: "text-sm text-muted-foreground leading-relaxed",\n customizer: "pt-2",\n }}\n />\n ) : (\n <div className="p-8 text-center text-muted-foreground">Loading\u2026</div>\n )}\n </div>\n </div>\n );\n}\n' }, { "path": "components/mobile-nav.tsx", "kind": "text", "content": '"use client";\n\nimport { useEffect, useState } from "react";\nimport Link from "next/link";\nimport { brand } from "@/lib/brand";\n\n/**\n * Hamburger button + slide-in drawer for narrow viewports. Header hides\n * its inline nav links below `sm` and renders this in their place; the\n * cart pill stays in the header chrome.\n */\nexport function MobileNav() {\n const [open, setOpen] = useState(false);\n\n useEffect(() => {\n if (!open) return;\n const onKey = (event: KeyboardEvent) => {\n if (event.key === "Escape") setOpen(false);\n };\n document.addEventListener("keydown", onKey);\n const previousOverflow = document.body.style.overflow;\n document.body.style.overflow = "hidden";\n return () => {\n document.removeEventListener("keydown", onKey);\n document.body.style.overflow = previousOverflow;\n };\n }, [open]);\n\n return (\n <>\n <button\n type="button"\n onClick={() => setOpen(true)}\n aria-label="Open menu"\n aria-expanded={open}\n aria-controls="mobile-nav-drawer"\n className="grid place-items-center w-11 h-11 -mr-2 rounded-md text-foreground hover:bg-muted transition-colors"\n >\n <svg\n width="20"\n height="20"\n viewBox="0 0 24 24"\n fill="none"\n stroke="currentColor"\n strokeWidth="2"\n strokeLinecap="round"\n strokeLinejoin="round"\n aria-hidden="true"\n >\n <line x1="3" y1="6" x2="21" y2="6" />\n <line x1="3" y1="12" x2="21" y2="12" />\n <line x1="3" y1="18" x2="21" y2="18" />\n </svg>\n </button>\n\n {open ? (\n <div className="fixed inset-0 z-50 sm:hidden">\n <button\n type="button"\n onClick={() => setOpen(false)}\n aria-label="Close menu"\n className="absolute inset-0 bg-background/80 backdrop-blur-sm"\n />\n <nav\n id="mobile-nav-drawer"\n aria-label="Mobile navigation"\n className="absolute inset-y-0 right-0 w-[85%] max-w-sm flex flex-col bg-background border-l border-border shadow-2xl"\n >\n <div className="flex items-center justify-between px-6 py-4 border-b border-border">\n <span className="text-xs font-semibold uppercase tracking-[0.16em] text-muted-foreground">\n Menu\n </span>\n <button\n type="button"\n onClick={() => setOpen(false)}\n aria-label="Close menu"\n className="grid place-items-center w-11 h-11 -mr-2 rounded-md text-foreground hover:bg-muted transition-colors"\n >\n <svg\n width="20"\n height="20"\n viewBox="0 0 24 24"\n fill="none"\n stroke="currentColor"\n strokeWidth="2"\n strokeLinecap="round"\n strokeLinejoin="round"\n aria-hidden="true"\n >\n <line x1="18" y1="6" x2="6" y2="18" />\n <line x1="6" y1="6" x2="18" y2="18" />\n </svg>\n </button>\n </div>\n <ul className="flex flex-col gap-1 px-3 py-4">\n {brand.header.nav.map((link) => (\n <li key={link.href}>\n <Link\n href={link.href}\n onClick={() => setOpen(false)}\n className="block px-3 py-3 rounded-md text-base font-medium text-foreground hover:bg-muted transition-colors"\n >\n {link.label}\n </Link>\n </li>\n ))}\n </ul>\n </nav>\n </div>\n ) : null}\n </>\n );\n}\n' }, { "path": "components/providers.tsx", "kind": "text", "content": '"use client";\n\nimport { useMemo, type ReactNode } from "react";\nimport { createCimplifyClient } from "@cimplify/sdk";\nimport { CimplifyProvider, CartDrawerProvider } from "@cimplify/sdk/react";\n\n// Same-origin client \u2014 every request goes through the Next.js rewrite in\n// next.config.ts, so no CORS preflight ever hits the browser.\nexport function Providers({ children }: { children: ReactNode }) {\n const client = useMemo(() => {\n const baseUrl =\n typeof window !== "undefined" ? window.location.origin : "http://127.0.0.1:8787";\n const publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY ?? "mock-dev";\n return createCimplifyClient({\n baseUrl,\n publicKey,\n suppressPublicKeyWarning: true,\n });\n }, []);\n\n return (\n <CimplifyProvider client={client}>\n <CartDrawerProvider>{children}</CartDrawerProvider>\n </CimplifyProvider>\n );\n}\n' }, { "path": "components/footer.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport { brand } from "@/lib/brand";\n\nconst ICONS: Record<string, React.ReactNode> = {\n instagram: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" aria-hidden className="w-5 h-5">\n <rect x="3" y="3" width="18" height="18" rx="5" />\n <circle cx="12" cy="12" r="4" />\n <circle cx="17.5" cy="6.5" r="0.75" fill="currentColor" />\n </svg>\n ),\n x: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M18 2h3l-7.5 8.6L22 22h-6.6l-5-6.5L4 22H1l8-9.2L1.4 2H8l4.6 6 5.4-6z" />\n </svg>\n ),\n tiktok: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M16 3v3.5a4.5 4.5 0 0 0 4.5 4.5V14a7.5 7.5 0 0 1-4.5-1.5V16a5 5 0 1 1-5-5v3a2 2 0 1 0 2 2V3z" />\n </svg>\n ),\n facebook: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M14 9V7a1 1 0 0 1 1-1h2V3h-3a4 4 0 0 0-4 4v2H8v3h2v9h3v-9h2.5l.5-3H13z" />\n </svg>\n ),\n youtube: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M22.5 6.5a2.6 2.6 0 0 0-1.8-1.8C19 4.2 12 4.2 12 4.2s-7 0-8.7.5A2.6 2.6 0 0 0 1.5 6.5C1 8.2 1 12 1 12s0 3.8.5 5.5a2.6 2.6 0 0 0 1.8 1.8C5 19.8 12 19.8 12 19.8s7 0 8.7-.5a2.6 2.6 0 0 0 1.8-1.8c.5-1.7.5-5.5.5-5.5s0-3.8-.5-5.5zM10 15.5v-7l6 3.5-6 3.5z" />\n </svg>\n ),\n linkedin: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M4 4h4v16H4zM6 2.5a2 2 0 1 0 0 4 2 2 0 0 0 0-4zM10 8h4v2.5h.1c.6-1.1 2-2.5 4-2.5 4 0 4.9 2.6 4.9 6V20h-4v-5c0-1.5-.5-3-2.3-3-1.7 0-2.5 1.3-2.5 3v5h-4z" />\n </svg>\n ),\n whatsapp: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M12 2a10 10 0 0 0-8.6 15l-1.4 5 5.2-1.4A10 10 0 1 0 12 2zm5 14.2c-.2.6-1.2 1.2-1.7 1.2-.5.1-1.1.1-1.7-.1-.4-.1-.9-.3-1.5-.6a8.4 8.4 0 0 1-3.7-3.4c-.7-1-1-1.8-1-2.5 0-.7.4-1.1.6-1.3.2-.2.4-.2.5-.2h.4c.1 0 .3 0 .4.3l.6 1.4c.1.2 0 .3 0 .4l-.3.4-.3.3c-.1.1-.2.2-.1.4.2.4.7 1.1 1.4 1.8.9.8 1.7 1.1 1.9 1.2.2.1.3.1.5-.1l.6-.7c.2-.2.3-.2.5-.1l1.4.7c.2.1.3.2.4.3.1.2.1.6 0 1z" />\n </svg>\n ),\n};\n\nconst FALLBACK_ICON = (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" aria-hidden className="w-5 h-5">\n <circle cx="12" cy="12" r="9" />\n </svg>\n);\n\nexport async function Footer() {\n "use cache";\n const year = new Date().getFullYear();\n return (\n <footer className="mt-12 px-6 sm:px-8 py-10 text-xs text-muted-foreground border-t border-border bg-card">\n <div className="max-w-7xl mx-auto">\n <div className="grid gap-10 md:grid-cols-[1.4fr_repeat(4,1fr)]">\n <div>\n <p className="font-serif text-2xl text-foreground m-0 mb-2">{brand.name}</p>\n <p className="leading-relaxed mb-4 max-w-sm">{brand.footer.blurb}</p>\n <address className="not-italic space-y-1">\n <p className="m-0">{brand.contact.address}</p>\n <p className="m-0">\n <a\n href={`tel:${brand.contact.phoneTel}`}\n className="hover:text-foreground transition-colors"\n >\n {brand.contact.phone}\n </a>\n </p>\n <p className="m-0">\n <a\n href={`mailto:${brand.contact.email}`}\n className="hover:text-foreground transition-colors"\n >\n {brand.contact.email}\n </a>\n </p>\n <p className="m-0">{brand.contact.hours}</p>\n </address>\n <div className="flex items-center gap-3 mt-5">\n {brand.socials.map((s) => (\n <a\n key={s.label}\n href={s.href}\n aria-label={s.label}\n target="_blank"\n rel="noopener noreferrer"\n className="inline-flex items-center justify-center w-9 h-9 rounded-full border border-border text-muted-foreground hover:text-foreground hover:border-foreground transition-colors"\n >\n {(s.icon && ICONS[s.icon]) ?? FALLBACK_ICON}\n </a>\n ))}\n </div>\n </div>\n {brand.footer.sitemap.map((section) => (\n <nav key={section.title} aria-labelledby={`footer-${section.title}`}>\n <p\n id={`footer-${section.title}`}\n className="font-semibold text-foreground mb-3 text-[13px] uppercase tracking-[0.08em]"\n >\n {section.title}\n </p>\n <ul className="space-y-2 m-0 p-0 list-none">\n {section.links.map((link) => (\n <li key={link.label}>\n <Link\n href={link.href}\n className="hover:text-foreground transition-colors"\n >\n {link.label}\n </Link>\n </li>\n ))}\n </ul>\n </nav>\n ))}\n </div>\n <div className="mt-12 pt-6 border-t border-border flex flex-col sm:flex-row items-center justify-between gap-3">\n <p className="m-0">\xA9 {year} {brand.name}. All rights reserved.</p>\n {brand.footer.poweredBy && (\n <p className="m-0 inline-flex items-center gap-1.5">\n <span className="text-muted-foreground/80">Powered by</span>\n <a\n href={brand.footer.poweredBy.href}\n target="_blank"\n rel="noopener noreferrer"\n aria-label={brand.footer.poweredBy.label}\n className="inline-flex items-center gap-1 font-serif text-foreground hover:text-primary transition-colors"\n >\n <span className="font-semibold tracking-tight">{brand.footer.poweredBy.label}</span>\n <svg\n viewBox="0 0 12 12"\n aria-hidden\n className="w-3 h-3 opacity-70"\n fill="none"\n stroke="currentColor"\n strokeWidth="1.5"\n >\n <path d="M3 9L9 3M9 3H4M9 3v5" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n </a>\n </p>\n )}\n </div>\n </div>\n </footer>\n );\n}\n' }, { "path": "components/store-product-card.tsx", "kind": "text", "content": '"use client";\n\nimport Link from "next/link";\nimport {\n CardVariant,\n FoodProductCard,\n RetailProductCard,\n WholesaleProductCard,\n DigitalProductCard,\n BundleProductCard,\n CompositeProductCard,\n StandardServiceCard,\n CompactServiceCard,\n ScheduleServiceCard,\n RentalServiceCard,\n AccommodationCard,\n LeaseServiceCard,\n SubscriptionCard,\n} from "@cimplify/sdk/react";\nimport type { CardLayoutProps } from "@cimplify/sdk/react";\nimport type { Product } from "@cimplify/sdk";\nimport { PRODUCT_TYPE, RENDER_HINT, DURATION_UNIT } from "@cimplify/sdk";\n\nconst RENTAL_UNITS = new Set<string>([DURATION_UNIT.Days, DURATION_UNIT.Hours]);\nconst LEASE_UNITS = new Set<string>([DURATION_UNIT.Weeks, DURATION_UNIT.Months, DURATION_UNIT.Years]);\n\nconst VARIANT_CARDS: Record<CardVariant, React.ComponentType<CardLayoutProps>> = {\n [CardVariant.Food]: FoodProductCard,\n [CardVariant.Retail]: RetailProductCard,\n [CardVariant.Wholesale]: WholesaleProductCard,\n [CardVariant.Digital]: DigitalProductCard,\n [CardVariant.Bundle]: BundleProductCard,\n [CardVariant.Composite]: CompositeProductCard,\n [CardVariant.Standard]: StandardServiceCard,\n [CardVariant.Compact]: CompactServiceCard,\n [CardVariant.Schedule]: ScheduleServiceCard,\n [CardVariant.Rental]: RentalServiceCard,\n [CardVariant.Accommodation]: AccommodationCard,\n [CardVariant.Lease]: LeaseServiceCard,\n [CardVariant.Subscription]: SubscriptionCard,\n};\n\nfunction resolveVariant(p: Product): CardVariant {\n if (p.type === PRODUCT_TYPE.Bundle) return CardVariant.Bundle;\n if (p.type === PRODUCT_TYPE.Composite) return CardVariant.Composite;\n if (p.quantity_pricing && p.quantity_pricing.length > 1) return CardVariant.Wholesale;\n if (p.type === PRODUCT_TYPE.Digital) return CardVariant.Digital;\n if (p.type === PRODUCT_TYPE.Service) {\n if (p.duration_unit && RENTAL_UNITS.has(p.duration_unit)) return CardVariant.Rental;\n if (p.duration_unit === DURATION_UNIT.Nights) return CardVariant.Accommodation;\n if (p.duration_unit && LEASE_UNITS.has(p.duration_unit)) return CardVariant.Lease;\n if (p.billing_plans && p.billing_plans.length > 0 && !p.duration_minutes) return CardVariant.Subscription;\n return CardVariant.Standard;\n }\n if (p.render_hint === RENDER_HINT.Food) return CardVariant.Food;\n return CardVariant.Retail;\n}\n\ninterface Props {\n product: Product;\n /** Override the auto-detected card variant. */\n variant?: CardVariant;\n}\n\n/**\n * Variant-aware product card that opens the URL-driven product modal on\n * click. Uses `next/link` with `?product=<slug>` so the link is statically\n * pre-renderable \u2014 no `useSearchParams` dependency means cards don\'t\n * become dynamic islands. The modal (which already reads `useSearchParams`\n * inside a Suspense boundary in the root layout) handles the rest.\n */\nexport function StoreProductCard({ product, variant }: Props) {\n const slug = product.slug || product.id;\n const Card = VARIANT_CARDS[variant ?? resolveVariant(product)];\n const href = `?product=${encodeURIComponent(slug)}`;\n\n return (\n <Card\n product={product}\n renderLink={({ className, children }) => (\n <Link href={href} scroll={false} prefetch={false} className={className}>\n {children}\n </Link>\n )}\n />\n );\n}\n' }, { "path": "components/json-ld.tsx", "kind": "text", "content": 'import { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\n\n/**\n * Streams the organization JSON-LD script tag in async so it doesn\'t block\n * the rest of the layout shell. `getSiteUrl` reads request headers, and\n * any await on dynamic data inside `RootLayout` makes Next 16 mark the\n * whole route as blocking.\n */\nexport async function OrganizationJsonLd(): Promise<React.ReactElement> {\n const siteUrl = await getSiteUrl();\n const ld = {\n "@context": "https://schema.org",\n "@type": brand.schemaType,\n name: brand.name,\n url: siteUrl,\n description: brand.description,\n email: brand.contact.email,\n telephone: brand.contact.phoneTel,\n address: {\n "@type": "PostalAddress",\n streetAddress: brand.contact.streetAddress,\n addressLocality: brand.contact.city,\n addressCountry: brand.contact.countryCode,\n },\n sameAs: brand.socials.map((s) => s.href),\n };\n return (\n <script\n type="application/ld+json"\n dangerouslySetInnerHTML={{ __html: JSON.stringify(ld) }}\n />\n );\n}\n' }, { "path": "components/nav-link.tsx", "kind": "text", "content": '"use client";\n\nimport Link from "next/link";\nimport { usePathname } from "next/navigation";\n\nexport function NavLink({ href, children }: { href: string; children: React.ReactNode }) {\n const pathname = usePathname();\n const active = pathname === href;\n return (\n <Link\n href={href}\n className={[\n "text-[13px] font-medium tracking-wide transition-colors",\n active ? "text-primary" : "text-muted-foreground hover:text-foreground",\n ].join(" ")}\n >\n {children}\n </Link>\n );\n}\n' }, { "path": "components/category-grid.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CategoryGrid as SdkCategoryGrid } from "@cimplify/sdk/react";\nimport type { Category } from "@cimplify/sdk";\n\n/**\n * Homepage category tiles \u2014 defers to the SDK\'s <CategoryGrid/> for layout\n * and accessibility, and routes selections to /categories/:slug.\n */\nexport function CategoryGrid({ categories }: { categories?: Category[] }) {\n const router = useRouter();\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 pt-14">\n <h2 className="font-serif text-[28px] font-semibold m-0 mb-5">Browse the menu</h2>\n <SdkCategoryGrid\n categories={categories}\n onSelect={(c) => router.push(`/categories/${c.slug}`)}\n classNames={{\n item: "flex flex-col gap-1 p-6 bg-card border border-border rounded-2xl cursor-pointer text-left transition-all hover:border-primary hover:-translate-y-0.5",\n name: "font-serif text-lg font-semibold text-foreground",\n description: "text-xs text-muted-foreground",\n count: "text-xs text-muted-foreground mt-1",\n }}\n />\n </section>\n );\n}\n' }, { "path": "components/cart-drawer.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CartDrawer as SdkCartDrawer } from "@cimplify/sdk/react";\n\n/**\n * Side-drawer cart. Auto-opens when an item is added (via\n * `<CartDrawerProvider>` in `providers.tsx`). The header\'s cart pill\n * also calls `useCartDrawer().open()` to reveal it on click.\n */\nexport function CartDrawer() {\n const router = useRouter();\n return <SdkCartDrawer onCheckout={() => router.push("/checkout")} />;\n}\n' }, { "path": "next.config.ts", "kind": "text", "content": 'import type { NextConfig } from "next";\n\n// Same-origin proxy target for the storefront API. The public key\n// decides: a real `cpk_live_\u2026` / `cpk_test_\u2026` only resolves against\n// hosted Cimplify, so browser cart writes go to the same place the\n// server catalogue reads come from. Anything else (`mock-dev`, empty)\n// falls back to the local `cimplify dev` mock in dev.\nconst publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY?.trim() ?? "";\nconst keyTargetsHostedCimplify =\n publicKey.startsWith("cpk_live_") || publicKey.startsWith("cpk_test_");\nconst STOREFRONT_URL =\n process.env.NODE_ENV === "production" || keyTargetsHostedCimplify\n ? "https://storefronts.cimplify.io"\n : "http://127.0.0.1:8787";\n\nif (STOREFRONT_URL === "http://127.0.0.1:8787") {\n console.warn(\n "[cimplify] next.config resolved the local storefront API URL; production deploys must run with NODE_ENV=production.",\n );\n}\n\nconst nextConfig: NextConfig = {\n // Enable Next 16\'s `cacheComponents` mode so we can use `\'use cache\'` +\n // `cacheTag` / `cacheLife` for SSR caching. Cached chrome streams first,\n // dynamic Suspense boundaries fill in when ready.\n cacheComponents: true,\n async redirects() {\n // Config-level so cacheComponents needn\'t prerender a redirect()-only page.\n return [\n { source: "/login", destination: "/account", permanent: false },\n { source: "/signup", destination: "/account", permanent: false },\n ];\n },\n async rewrites() {\n return [\n { source: "/api/v1/:path*", destination: `${STOREFRONT_URL}/api/v1/:path*` },\n { source: "/img/:path*", destination: `${STOREFRONT_URL}/img/:path*` },\n { source: "/elements/:path*", destination: `${STOREFRONT_URL}/elements/:path*` },\n { source: "/_mock/:path*", destination: `${STOREFRONT_URL}/_mock/:path*` },\n ];\n },\n images: {\n loader: "custom",\n loaderFile: "./lib/cimplify-loader.ts",\n remotePatterns: [\n { protocol: "http", hostname: "127.0.0.1", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "3000", pathname: "/img/**" },\n { protocol: "https", hostname: "loremflickr.com", pathname: "/**" },\n { protocol: "https", hostname: "images.unsplash.com", pathname: "/**" },\n { protocol: "https", hostname: "static-tmp.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "storefrontassetscdn.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "cdn.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "res.cloudinary.com", pathname: "/cimplify/**" },\n ],\n },\n};\n\nexport default nextConfig;\n' }, { "path": ".claude/skills/cimplify-storefront/SKILL.md", "kind": "text", "content": "---\nname: cimplify-storefront\ndescription: Build, customize, rebrand, or deploy a Cimplify-scaffolded storefront. Triggers when the user asks to create a storefront, rebrand a Cimplify template, change the palette, add a page, deploy to Cimplify, or works in a project containing `lib/brand.ts` and `next.config.ts` with `cacheComponents`.\n---\n\n# Cimplify Storefront skill\n\nYou're working on a project scaffolded from `cimplify init`. The architecture is opinionated and the rebrand surface is intentionally small. Read `AGENTS.md` at the project root for the file \u2194 brand-field map for *this template's* industry; this skill gives you the playbook that's the same across all six.\n\n## The contract \u2014 never break\n\n1. **`lib/brand.ts` is the only place for content edits.** Every visible string reads from this file. If a string isn't in `brand`, *add a field* to the `Brand` interface \u2014 don't hardcode it in a page or component.\n2. **`app/globals.css` `@theme { \u2026 }`** holds palette + radius + font references. To re-skin the entire site, edit only this block.\n3. **Server Components are cached** via `'use cache'` + `cacheTag(tags.X())` + `cacheLife(\"hours\")`. Don't downgrade them to `\"use client\"` to avoid an issue \u2014 fix the issue.\n4. **Client islands** (anything reading `useSearchParams`, `usePathname`, `useRouter`, `useState`) live behind `<Suspense>`.\n5. **`cacheComponents: true`** in `next.config.ts` is non-negotiable. Don't turn it off.\n6. **`bun run test:run` (vitest)** is the canonical test runner. `bun test` will show false failures because Bun's `vi` shim is incomplete.\n7. **Cart, checkout, orders stay client.** They're session-bound. Don't try to SSR them.\n\n## The brand-field schema (in `lib/brand.ts`)\n\n```\nidentity: name, shortName, microTag, description, schemaType, currency, locale\ncontact: address, streetAddress, city, countryCode, phone, phoneTel, email, privacyEmail, hours\nsocials: [{ label, href, icon }] // icon \u2208 instagram|x|tiktok|facebook|youtube|linkedin|whatsapp\nheader: { nav: [{ label, href }] }\nhero: { badge, title, subtitle, primaryCtaLabel, secondaryCtaLabel?, secondaryCtaHref? }\noptional: trustItems[]?, brandStrip?, promo?, tradeIn? // render conditionally\nnewsletter: { eyebrow, title, body, placeholder, submitLabel, successLabel }\nabout: { eyebrow, title, paragraphs[], sections[{ heading, body }] }\nfaq: { eyebrow, title, sections[{ title, items[{ q, a }] }], contactPrompt, contactEmail }\nterms,\nprivacy,\nshipping,\nreturns,\naccessibility: { eyebrow, title, lastUpdated, sections[{ heading, body | { intro, bullets[] } }] }\naccount: eyebrows + titles for /account, /login, /signup\ncontactPage: { eyebrow, title, body, reasons[], directLines[{ label, value, href }] }\ntrackOrder: { eyebrow, title, body }\nfooter: { blurb, sitemap[{ title, links[] }], poweredBy? }\nllms: { summary } // opens /llms.txt\nmock: { seed, businessId }\n```\n\n## Playbook \u2014 common tasks\n\n### Rebrand a storefront for a new merchant\n\n1. Edit `lib/brand.ts`. Replace every field with the merchant's content. Use the brief / context the user gave you.\n2. Edit `app/globals.css` `@theme { \u2026 }`. Change `--color-primary`, `--color-background`, `--color-foreground`, optionally `--radius`. Use OKLCH; if the brand only gave hex, convert.\n3. (Optional) Swap fonts in `app/layout.tsx` \u2014 `next/font/google` import + the variable wired into the `<html>` className.\n4. Set `.env.local`: `NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY` (optional: `NEXT_PUBLIC_SITE_URL`).\n\nDon't touch any other file. If the rebrand needs content not in the schema, add the field to `Brand` first, populate it, then read it from the page.\n\n### Add a new section / component\n\n1. Build it as a Server Component in `components/` (or a client island in `*-client.tsx` if interactive).\n2. Read merchant copy from `brand`. Add new fields to the `Brand` interface if needed.\n3. Wrap interactive bits in `<Suspense fallback={\u2026}>` so the cached chrome streams.\n4. Compose into the page.\n\n### Wire a Server Action that mutates data\n\n```ts\n\"use server\";\nimport { getServerClient, revalidateProducts } from \"@cimplify/sdk/server\";\n\nexport async function createProduct(input: ProductInput) {\n await getServerClient().catalogue.createProduct(input);\n revalidateProducts();\n}\n```\n\nAfter every mutation, call the matching `revalidate*` helper from `@cimplify/sdk/server`: `revalidateProducts`, `revalidateProduct(id)`, `revalidateCategories`, `revalidateCategory(id)`, `revalidateCollections`, `revalidateCollection(id)`, `revalidateBusiness`.\n\n### Add a Server Component data fetch\n\n```ts\nimport { cacheTag, cacheLife } from \"next/cache\";\nimport { getServerClient, tags } from \"@cimplify/sdk/server\";\n\nasync function getX() {\n \"use cache\";\n cacheTag(tags.products());\n cacheLife(\"hours\");\n const r = await getServerClient().catalogue.getProducts({ limit: 24 });\n if (!r.ok) throw new Error(r.error.message);\n return r.value.items;\n}\n```\n\n### Eject an SDK component for deeper customization\n\nTry `classNames`, `renderImage`, `renderLink`, slot props first. If those run out:\n\n```bash\ncimplify list\ncimplify add product-card --dir src/components/cimplify\n```\n\nOnce ejected, the file is yours. Hooks/types still come from `@cimplify/sdk`.\n\n### Deploy\n\n```bash\ncimplify login\ncimplify projects create my-store\ncimplify link <project-id>\ncimplify env push\ncimplify deploy --prod\ncimplify logs --follow\ncimplify domains add my-store.com\n```\n\n## Pitfalls \u2014 explicit \u274C list\n\n- \u274C Hardcoding any visible string in a page or component. Always `brand.X`.\n- \u274C Disabling `cacheComponents: true` to silence a warning. Fix the warning by wrapping the dynamic call in `<Suspense>`.\n- \u274C Using `unstable_cache` \u2014 Next 16's canonical primitive is `'use cache'` + `cacheTag` + `cacheLife`.\n- \u274C Bypassing `getServerClient()` and calling `createCimplifyClient` directly in a Server Component \u2014 loses per-request memoization.\n- \u274C Mutating data without calling the matching `revalidate*` helper.\n- \u274C Adding an `app/error.tsx` handler that calls `reset()` without logging \u2014 silently swallowed errors hide bugs.\n- \u274C Running `bun test` and reporting failures. Use `bun run test:run` (vitest).\n- \u274C Editing the per-template `AGENTS.md` to remove notes about TODOs (contact form, newsletter fake submits) \u2014 they're real.\n\n## Where things live\n\n| Need | Look here |\n|---|---|\n| Which page reads which `brand.X` field | `AGENTS.md` at project root |\n| Architectural rules | `AGENTS.md` at project root + this skill |\n| Running locally | `bun dev` (boots mock + Next together) |\n| Switching mock seed | edit `dev:mock` in `package.json` |\n| Full SDK reference | `docs/sdk/storefronts.md` in the Cimplify repo |\n\n## What to do when the user asks something out-of-scope\n\nIf the user asks for something the template doesn't support out of the box:\n\n1. **Check first** \u2014 is there an SDK component or hook that does it? `@cimplify/sdk/react` has 50+ components, 30+ hooks. Search `node_modules/@cimplify/sdk/dist/react.d.mts` if needed.\n2. **Compose from existing parts** before authoring new ones.\n3. **If genuinely missing** \u2014 build the new component, hoist its strings into `brand.ts`, document in `AGENTS.md`.\n\nDon't introduce competing patterns (a new caching strategy, a new auth flow, a new theming system). The template's opinions are intentional.\n" }, { "path": ".cursor/rules/cimplify-storefront.mdc", "kind": "binary", "content": "LS0tCmRlc2NyaXB0aW9uOiBDaW1wbGlmeSBzdG9yZWZyb250IOKAlCBhcmNoaXRlY3R1cmFsIHJ1bGVzIGFuZCByZWJyYW5kIGNvbnRyYWN0IGZvciBhIHByb2plY3Qgc2NhZmZvbGRlZCBmcm9tIGBjaW1wbGlmeSBpbml0YC4gVHJpZ2dlcnMgd2hlbiBmaWxlcyBsaWtlIGxpYi9icmFuZC50cywgYXBwL2dsb2JhbHMuY3NzLCBjb21wb25lbnRzL2hlYWRlci50c3gsIG9yIC5jaW1wbGlmeS9wcm9qZWN0Lmpzb24gYXJlIGluIHRoZSB3b3Jrc3BhY2UuCmdsb2JzOiBbImxpYi9icmFuZC50cyIsICJhcHAvKioiLCAiY29tcG9uZW50cy8qKiIsICIuZW52LmxvY2FsIiwgIm5leHQuY29uZmlnLnRzIl0KYWx3YXlzQXBwbHk6IHRydWUKLS0tCgojIENpbXBsaWZ5IHN0b3JlZnJvbnQKClRoaXMgcHJvamVjdCB3YXMgc2NhZmZvbGRlZCBmcm9tIGEgQ2ltcGxpZnkgdGVtcGxhdGUuIFJlYWQgYEFHRU5UUy5tZGAgYXQgdGhlIHByb2plY3Qgcm9vdCBmb3IgdGhlIGZpbGUg4oaUIGBicmFuZC5YYCBmaWVsZCBtYXAgYW5kIGAuY2xhdWRlL3NraWxscy9jaW1wbGlmeS1zdG9yZWZyb250L1NLSUxMLm1kYCBmb3IgdGhlIGZ1bGwgcGxheWJvb2suCgojIyBUaGUgY29udHJhY3QKCjEuICoqYGxpYi9icmFuZC50c2AqKiDigJQgZXZlcnkgdmlzaWJsZSBzdHJpbmcuIElmIHNvbWV0aGluZyBpc24ndCB0aGVyZSwgYWRkIGEgZmllbGQgdG8gdGhlIGBCcmFuZGAgaW50ZXJmYWNlOyBkb24ndCBoYXJkY29kZS4KMi4gKipgYXBwL2dsb2JhbHMuY3NzYCBgQHRoZW1lYCBibG9jayoqIOKAlCBwYWxldHRlLCByYWRpdXMsIGZvbnQgcmVmZXJlbmNlcy4KMy4gKipTZXJ2ZXIgQ29tcG9uZW50cyBhcmUgY2FjaGVkKiogdmlhIGAndXNlIGNhY2hlJ2AgKyBgY2FjaGVUYWcodGFncy5YKCkpYCArIGBjYWNoZUxpZmUoImhvdXJzIilgLiBEb24ndCBkb3duZ3JhZGUgdG8gY2xpZW50IHRvIHNpbGVuY2UgYSB3YXJuaW5nLgo0LiAqKkNsaWVudCBpc2xhbmRzKiogYmVoaW5kIGA8U3VzcGVuc2U+YC4KNS4gKipgY2FjaGVDb21wb25lbnRzOiB0cnVlYCoqIGluIGBuZXh0LmNvbmZpZy50c2AgaXMgbm9uLW5lZ290aWFibGUuCjYuIFVzZSAqKmBidW4gcnVuIHRlc3Q6cnVuYCoqICh2aXRlc3QpLCBub3QgYGJ1biB0ZXN0YC4KCiMjIERvbid0CgotIEhhcmRjb2RlIHZpc2libGUgc3RyaW5ncyBpbiBwYWdlcy9jb21wb25lbnRzLgotIFVzZSBgdW5zdGFibGVfY2FjaGVgIChOZXh0IDE2IHVzZXMgYCd1c2UgY2FjaGUnYCkuCi0gQnlwYXNzIGBnZXRTZXJ2ZXJDbGllbnQoKWAgKGxvc2VzIHBlci1yZXF1ZXN0IG1lbW9pemF0aW9uKS4KLSBNdXRhdGUgd2l0aG91dCBjYWxsaW5nIHRoZSBtYXRjaGluZyBgcmV2YWxpZGF0ZSpgIGhlbHBlciBmcm9tIGBAY2ltcGxpZnkvc2RrL3NlcnZlcmAuCg==" }, { "path": ".gitignore", "kind": "text", "content": "node_modules\n.next\nout\n.env\n.env.local\n.env*.local\n.DS_Store\n*.tsbuildinfo\nnext-env.d.ts\n" }, { "path": "app/about/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `About \u2014 ${brand.name}`,\n description: brand.description,\n};\n\nexport default function AboutPage() {\n const a = brand.about;\n const titleParts = a.title.split("\\n");\n return (\n <article className="max-w-3xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {a.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-6 -tracking-[0.02em]">\n {titleParts.map((line, i) => (\n <span key={i}>\n {line}\n {i < titleParts.length - 1 && <br />}\n </span>\n ))}\n </h1>\n <div className="prose prose-lg max-w-none space-y-5 text-foreground/90 leading-relaxed">\n {a.paragraphs.map((p, i) => (\n <p key={i}>{p}</p>\n ))}\n {a.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="font-serif text-3xl font-semibold mt-10 mb-3">{s.heading}</h2>\n <p>{s.body}</p>\n </div>\n ))}\n </div>\n </article>\n );\n}\n' }, { "path": "app/account/orders/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Orders \u2014 ${brand.name}`,\n};\n\nexport default function OrdersPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Your orders\n </h1>\n <AccountIframe section="orders" />\n </article>\n );\n}\n' }, { "path": "app/account/settings/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Settings \u2014 ${brand.name}`,\n};\n\nexport default function SettingsPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Settings\n </h1>\n <AccountIframe section="settings" />\n </article>\n );\n}\n' }, { "path": "app/account/addresses/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Addresses \u2014 ${brand.name}`,\n};\n\nexport default function AddressesPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Your addresses\n </h1>\n <AccountIframe section="addresses" />\n </article>\n );\n}\n' }, { "path": "app/account/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Account \u2014 ${brand.name}`,\n description: brand.account.signupSubtitle,\n};\n\nexport default function AccountPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {brand.account.accountEyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n {brand.account.accountTitle}\n </h1>\n <AccountIframe />\n </article>\n );\n}\n' }, { "path": "app/.well-known/ucp/route.ts", "kind": "text", "content": 'import { NextResponse } from "next/server";\n\n/**\n * UCP (Universal Commerce Protocol) manifest discovery endpoint.\n *\n * Agents \u2014 Claude, ChatGPT, Gemini, MCP clients \u2014 probe\n * `https://<your-domain>/.well-known/ucp` to learn what commerce\n * capabilities your storefront supports. We resolve the business\'s\n * UCP handle from the public key (via the storefront API), then forward\n * to Cimplify for the canonical manifest. Edge-cached for an hour\n * because capabilities change rarely.\n */\nconst UCP_API_BASE = "https://api.cimplify.io";\nconst STOREFRONT_API_BASE =\n process.env.NODE_ENV === "production"\n ? "https://storefronts.cimplify.io"\n : "http://127.0.0.1:8787";\n\nexport async function GET() {\n const publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY;\n if (!publicKey) {\n return NextResponse.json(\n {\n error: "NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY not set",\n remediation:\n "Set NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY in .env.local (and your deployment env).",\n },\n { status: 500 },\n );\n }\n\n try {\n // Step 1 \u2014 resolve the business handle from the public key.\n const businessResp = await fetch(`${STOREFRONT_API_BASE}/api/v1/business`, {\n headers: { "X-API-Key": publicKey, "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n });\n\n if (!businessResp.ok) {\n return NextResponse.json(\n { error: `Failed to resolve business: ${businessResp.status}` },\n { status: businessResp.status },\n );\n }\n\n const businessJson = await businessResp.json();\n const handle: string | undefined = businessJson?.data?.handle;\n if (!handle) {\n return NextResponse.json(\n { error: "Business has no handle configured" },\n { status: 500 },\n );\n }\n\n // Step 2 \u2014 fetch the canonical UCP manifest.\n const manifestResp = await fetch(\n `${UCP_API_BASE}/ucp/v1/${handle}/manifest`,\n {\n headers: { "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n },\n );\n\n if (!manifestResp.ok) {\n return NextResponse.json(\n { error: `Upstream UCP manifest fetch failed: ${manifestResp.status}` },\n { status: manifestResp.status },\n );\n }\n\n const manifest = await manifestResp.json();\n return NextResponse.json(manifest, {\n headers: {\n "Content-Type": "application/json",\n "Cache-Control": "public, max-age=3600, s-maxage=3600",\n },\n });\n } catch (error) {\n return NextResponse.json(\n {\n error: "Failed to fetch UCP manifest",\n detail: error instanceof Error ? error.message : String(error),\n },\n { status: 500 },\n );\n }\n}\n' }, { "path": "app/search/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { SearchClient } from "./search-client";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Search \u2014 ${brand.name}`,\n description: `Search ${brand.name} \u2014 products, collections, categories.`,\n};\n\nexport default function SearchPage() {\n return (\n <article className="max-w-7xl mx-auto px-6 sm:px-8 py-10">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Search\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Find anything.\n </h1>\n <Suspense fallback={<SearchSkeleton />}>\n <SearchClient />\n </Suspense>\n </article>\n );\n}\n\nfunction SearchSkeleton() {\n return (\n <div>\n <div className="h-12 w-full max-w-xl bg-muted rounded animate-pulse mb-8" />\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {Array.from({ length: 8 }).map((_, i) => (\n <div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </div>\n );\n}\n' }, { "path": "app/search/search-client.tsx", "kind": "text", "content": '"use client";\n\nimport { SearchPage } from "@cimplify/sdk/react";\n\nexport function SearchClient() {\n return <SearchPage />;\n}\n' }, { "path": "app/faq/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `FAQ \u2014 ${brand.name}`,\n description: "Delivery, pickup, custom cakes, allergens, payment \u2014 answers to the questions we hear most often.",\n};\n\nexport default function FaqPage() {\n const f = brand.faq;\n return (\n <article className="max-w-3xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {f.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-10 -tracking-[0.02em]">\n {f.title}\n </h1>\n <div className="space-y-12">\n {f.sections.map((section) => (\n <section key={section.title}>\n <h2 className="font-serif text-2xl font-semibold mb-5">{section.title}</h2>\n <dl className="space-y-6">\n {section.items.map((item) => (\n <div key={item.q}>\n <dt className="font-medium text-foreground mb-1.5">{item.q}</dt>\n <dd className="text-muted-foreground leading-relaxed">{item.a}</dd>\n </div>\n ))}\n </dl>\n </section>\n ))}\n </div>\n <p className="mt-12 pt-8 border-t border-border text-sm text-muted-foreground">\n {f.contactPrompt}{" "}\n <a\n href={`mailto:${f.contactEmail}`}\n className="text-primary font-semibold hover:underline"\n >\n {f.contactEmail}\n </a>{" "}\n and a real human will reply within 24 hours.\n </p>\n </article>\n );\n}\n' }, { "path": "app/robots.ts", "kind": "text", "content": 'import type { MetadataRoute } from "next";\nimport { getSiteUrl } from "@/lib/site-url";\n\nexport default async function robots(): Promise<MetadataRoute.Robots> {\n const siteUrl = await getSiteUrl();\n return {\n rules: [\n {\n userAgent: "*",\n allow: "/",\n disallow: ["/cart", "/checkout", "/orders/", "/api/"],\n },\n ],\n sitemap: `${siteUrl}/sitemap.xml`,\n host: siteUrl,\n };\n}\n' }, { "path": "app/track-order/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { TrackOrderForm } from "./track-order-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Track an order \u2014 ${brand.name}`,\n description: brand.trackOrder.body,\n};\n\nexport default function TrackOrderPage() {\n const t = brand.trackOrder;\n return (\n <article className="max-w-2xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {t.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,5vw,3rem)] font-semibold mb-4 -tracking-[0.02em]">\n {t.title}\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">{t.body}</p>\n <TrackOrderForm />\n </article>\n );\n}\n' }, { "path": "app/track-order/track-order-form.tsx", "kind": "text", "content": '"use client";\n\nimport { useState } from "react";\nimport { useRouter } from "next/navigation";\n\n/**\n * Guest order tracker. Routes /orders/<id> with the entered id; the\n * post-checkout confirmation page already lives at /orders/[id] so this\n * just sends the visitor there. Replace the redirect with a real lookup\n * (e.g. Cimplify orders API + email-match guard) for production.\n */\nexport function TrackOrderForm() {\n const router = useRouter();\n const [orderId, setOrderId] = useState("");\n const [email, setEmail] = useState("");\n\n return (\n <form\n onSubmit={(e) => {\n e.preventDefault();\n if (!orderId.trim()) return;\n router.push(`/orders/${encodeURIComponent(orderId.trim())}`);\n }}\n className="space-y-4 rounded-2xl border border-border bg-card p-6"\n >\n <div>\n <label\n htmlFor="orderId"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order number\n </label>\n <input\n id="orderId"\n name="orderId"\n required\n value={orderId}\n onChange={(e) => setOrderId(e.target.value)}\n placeholder="ord_abc123\u2026"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <div>\n <label\n htmlFor="email"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order email\n </label>\n <input\n id="email"\n name="email"\n type="email"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n placeholder="you@email.com"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <button\n type="submit"\n className="w-full inline-flex items-center justify-center px-5 py-3 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors"\n >\n Track order\n </button>\n </form>\n );\n}\n' }, { "path": "app/cart/page.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CartPage as SdkCartPage } from "@cimplify/sdk/react";\n\nexport default function CartPage() {\n const router = useRouter();\n return <SdkCartPage onCheckout={() => router.push("/checkout")} />;\n}\n' }, { "path": "app/llms.txt/route.ts", "kind": "text", "content": 'import { cacheTag, cacheLife } from "next/cache";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\n\nasync function buildLlmsTxt(SITE_URL: string): Promise<string> {\n "use cache";\n cacheTag(tags.products(), tags.categories(), tags.collections());\n cacheLife("hours");\n\n const client = getServerClient();\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n\n const products: Product[] = productsRes.ok ? productsRes.value.items : [];\n const categories = categoriesRes.ok ? categoriesRes.value : [];\n const collections = collectionsRes.ok ? collectionsRes.value : [];\n\n const lines: string[] = [];\n lines.push(`# ${brand.name}`);\n lines.push("");\n lines.push(`> ${brand.llms.summary}`);\n lines.push("");\n lines.push("## Browse");\n lines.push(`- [Home](${SITE_URL}/)`);\n lines.push(`- [Shop](${SITE_URL}/shop): Full menu with filter and sort.`);\n\n if (categories.length > 0) {\n lines.push("");\n lines.push("## Categories");\n for (const c of categories) {\n lines.push(\n `- [${c.name}](${SITE_URL}/categories/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (collections.length > 0) {\n lines.push("");\n lines.push("## Collections");\n for (const c of collections) {\n lines.push(\n `- [${c.name}](${SITE_URL}/collections/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (products.length > 0) {\n lines.push("");\n lines.push("## Products");\n for (const p of products) {\n const slug = p.slug ?? p.id;\n const price = `${brand.currency} ${p.default_price}`;\n const desc = p.description ? ` \u2014 ${p.description.replace(/\\s+/g, " ").slice(0, 200)}` : "";\n lines.push(`- [${p.name}](${SITE_URL}/shop?product=${slug}) (${price})${desc}`);\n }\n }\n\n lines.push("");\n lines.push("## Information");\n lines.push(`- [About](${SITE_URL}/about)`);\n lines.push(`- [FAQ](${SITE_URL}/faq)`);\n lines.push(`- [Terms of Service](${SITE_URL}/terms)`);\n lines.push(`- [Privacy Policy](${SITE_URL}/privacy)`);\n lines.push(`- [Sitemap (XML)](${SITE_URL}/sitemap.xml)`);\n lines.push("");\n lines.push("## Contact");\n lines.push(`- Email: ${brand.contact.email}`);\n lines.push(`- Phone: ${brand.contact.phone}`);\n lines.push(`- Address: ${brand.contact.address}`);\n\n return lines.join("\\n") + "\\n";\n}\n\n/**\n * `/llms.txt` \u2014 machine-readable site index for LLMs (per llmstxt.org).\n * Lets coding agents and chat assistants find products, categories, and\n * support pages without scraping HTML. Plain Markdown so it streams cheaply\n * into context windows.\n */\nexport async function GET(): Promise<Response> {\n const body = await buildLlmsTxt(await getSiteUrl());\n return new Response(body, {\n headers: {\n "Content-Type": "text/plain; charset=utf-8",\n "Cache-Control": "public, max-age=0, s-maxage=3600, stale-while-revalidate=86400",\n },\n });\n}\n' }, { "path": "app/sitemap.ts", "kind": "text", "content": 'import type { MetadataRoute } from "next";\nimport { getServerClient, type Product } from "@cimplify/sdk/server";\nimport { getSiteUrl } from "@/lib/site-url";\n\nconst STATIC_ROUTES: { path: string; priority: number; changeFrequency: "daily" | "weekly" | "monthly" }[] = [\n { path: "/", priority: 1.0, changeFrequency: "daily" },\n { path: "/shop", priority: 0.9, changeFrequency: "daily" },\n { path: "/about", priority: 0.5, changeFrequency: "monthly" },\n { path: "/faq", priority: 0.4, changeFrequency: "monthly" },\n { path: "/terms", priority: 0.2, changeFrequency: "monthly" },\n { path: "/privacy", priority: 0.2, changeFrequency: "monthly" },\n];\n\nexport default async function sitemap(): Promise<MetadataRoute.Sitemap> {\n const now = new Date();\n const siteUrl = await getSiteUrl();\n const client = getServerClient();\n\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n\n const products = productsRes.ok ? productsRes.value.items : [];\n const categories = categoriesRes.ok ? categoriesRes.value : [];\n const collections = collectionsRes.ok ? collectionsRes.value : [];\n\n const staticEntries: MetadataRoute.Sitemap = STATIC_ROUTES.map((r) => ({\n url: `${siteUrl}${r.path}`,\n lastModified: now,\n changeFrequency: r.changeFrequency,\n priority: r.priority,\n }));\n\n // Bakery uses a URL-driven product modal (`?product=<slug>` on the home or\n // shop pages). Emit those as deep-linkable URLs so search engines and LLMs\n // can index each product canonically.\n const productEntries: MetadataRoute.Sitemap = products.map((p: Product) => ({\n url: `${siteUrl}/shop?product=${encodeURIComponent(p.slug ?? p.id)}`,\n lastModified: p.updated_at ? new Date(p.updated_at) : now,\n changeFrequency: "weekly",\n priority: 0.7,\n }));\n\n const categoryEntries: MetadataRoute.Sitemap = categories.map((c) => ({\n url: `${siteUrl}/categories/${c.slug}`,\n lastModified: now,\n changeFrequency: "weekly",\n priority: 0.6,\n }));\n\n const collectionEntries: MetadataRoute.Sitemap = collections.map((c) => ({\n url: `${siteUrl}/collections/${c.slug}`,\n lastModified: now,\n changeFrequency: "weekly",\n priority: 0.6,\n }));\n\n return [...staticEntries, ...categoryEntries, ...collectionEntries, ...productEntries];\n}\n' }, { "path": "app/opensearch.xml/route.ts", "kind": "text", "content": 'import { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\n\n/**\n * OpenSearch description document \u2014 lets browsers add this site to the\n * address bar\'s search engine list. When users press Tab after typing the\n * domain, they get an inline search box that hits /search?q=...\n */\nexport async function GET(): Promise<Response> {\n const siteUrl = await getSiteUrl();\n const xml = `<?xml version="1.0" encoding="UTF-8"?>\n<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">\n <ShortName>${escapeXml(brand.shortName)}</ShortName>\n <Description>Search ${escapeXml(brand.name)}</Description>\n <InputEncoding>UTF-8</InputEncoding>\n <Url type="text/html" method="get" template="${siteUrl}/search?q={searchTerms}" />\n <Url type="application/opensearchdescription+xml" rel="self" template="${siteUrl}/opensearch.xml" />\n <moz:SearchForm>${siteUrl}/search</moz:SearchForm>\n</OpenSearchDescription>\n`;\n return new Response(xml, {\n headers: {\n "Content-Type": "application/opensearchdescription+xml; charset=utf-8",\n "Cache-Control": "public, max-age=86400",\n },\n });\n}\n\nfunction escapeXml(s: string): string {\n return s\n .replace(/&/g, "&amp;")\n .replace(/</g, "&lt;")\n .replace(/>/g, "&gt;")\n .replace(/"/g, "&quot;")\n .replace(/\'/g, "&apos;");\n}\n' }, { "path": "app/contact/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { ContactForm } from "./contact-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Contact \u2014 ${brand.name}`,\n description: brand.contactPage.body,\n};\n\nexport default function ContactPage() {\n const c = brand.contactPage;\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {c.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-4 -tracking-[0.02em]">\n {c.title}\n </h1>\n <p className="text-lg text-muted-foreground max-w-2xl mb-12 leading-relaxed">{c.body}</p>\n\n <div className="grid grid-cols-1 lg:grid-cols-[1.5fr_1fr] gap-10 items-start">\n <ContactForm reasons={c.reasons} />\n <aside className="space-y-5 lg:pl-10 lg:border-l border-border">\n <div>\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Direct lines\n </p>\n <ul className="space-y-3 list-none p-0 m-0">\n {c.directLines.map((line) => (\n <li key={line.label}>\n <p className="text-xs text-muted-foreground m-0">{line.label}</p>\n <a\n href={line.href}\n className="text-foreground hover:text-primary transition-colors"\n >\n {line.value}\n </a>\n </li>\n ))}\n </ul>\n </div>\n <div>\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Visit\n </p>\n <p className="text-foreground m-0">{brand.contact.address}</p>\n <p className="text-xs text-muted-foreground mt-1">{brand.contact.hours}</p>\n </div>\n </aside>\n </div>\n </article>\n );\n}\n' }, { "path": "app/contact/contact-form.tsx", "kind": "text", "content": `"use client";
3002
+ ` }, { "path": "lib/cimplify-loader.ts", "kind": "text", "content": 'import type { ImageLoader } from "next/image";\n\nimport { assetUrl, isCimplifyAsset } from "@cimplify/sdk";\n\nconst cdnBase = process.env.NEXT_PUBLIC_CIMPLIFY_CDN_URL?.trim() || undefined;\n\nconst cimplifyImageLoader: ImageLoader = ({ src, width, quality }) => {\n if (isCimplifyAsset(src, cdnBase)) {\n return assetUrl(src, {\n base: cdnBase,\n w: width,\n quality,\n format: "auto",\n });\n }\n return src;\n};\n\nexport default cimplifyImageLoader;\n' }, { "path": "lib/site.config.ts", "kind": "text", "content": "/**\n * Canonical absolute URL. The platform overwrites this at build time with the\n * storefront's primary domain; the default only applies to local builds. It's\n * per-deploy config, not per-request, so it stays a static constant \u2014 keeping\n * the root layout prerenderable under `cacheComponents`.\n */\nexport const SITE_URL = \"https://example.com\";\n" }, { "path": "lib/cart.ts", "kind": "text", "content": '"use client";\n\nimport { useCart } from "@cimplify/sdk/react";\n\n/**\n * Reactive cart count for the header pill. Subscribes to the SDK cart\n * state \u2014 updates immediately on add / remove / quantity change.\n */\nexport function useCartCount(): { count: number } {\n const { itemCount } = useCart();\n return { count: itemCount };\n}\n' }, { "path": "lib/site-url.ts", "kind": "text", "content": 'import { SITE_URL } from "./site.config";\n\n/** Canonical absolute URL for this storefront. */\nexport async function getSiteUrl(): Promise<string> {\n return SITE_URL;\n}\n' }, { "path": "metadata.json", "kind": "text", "content": '{\n "id": "auto",\n "name": "Auto Parts",\n "tagline": "Auto-parts storefront with vehicle fitment finder and installation booking.",\n "industry": "automotive",\n "tags": ["automotive", "parts", "fitment"],\n "stability": "stable",\n "schemaType": "AutoPartsStore",\n "mock": {\n "seedName": "auto",\n "seedBusinessId": "bus_driveline_auto"\n }\n}\n' }, { "path": "tsconfig.json", "kind": "text", "content": '{\n "compilerOptions": {\n "target": "ES2022",\n "lib": ["dom", "dom.iterable", "esnext"],\n "allowJs": false,\n "skipLibCheck": true,\n "strict": true,\n "noEmit": true,\n "esModuleInterop": true,\n "module": "esnext",\n "moduleResolution": "bundler",\n "resolveJsonModule": true,\n "isolatedModules": true,\n "jsx": "preserve",\n "incremental": true,\n "plugins": [{ "name": "next" }],\n "paths": {\n "@/*": ["./*"]\n }\n },\n "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts"],\n "exclude": ["node_modules"]\n}\n' }, { "path": "CLAUDE.md", "kind": "text", "content": "# CLAUDE.md\n\nThis project was scaffolded from a Cimplify storefront template (`cimplify init`).\n\n## Read these first\n\n- **`AGENTS.md`** at the project root \u2014 the file \u2194 `brand.X` field map for *this template's* industry, plus the architectural rules.\n- **`.claude/skills/cimplify-storefront/SKILL.md`** \u2014 the playbook for common tasks (rebrand, add a section, wire a Server Action, deploy, eject a component). Invoke it whenever you're asked to change content, palette, copy, or routing.\n\n## The two-line summary\n\n1. **Edit `lib/brand.ts`** for any visible string.\n2. **Edit `app/globals.css` `@theme` block** for any palette / radius / font change.\n\nThat covers ~95% of what merchants ask for. For anything else, follow the `cimplify-storefront` skill.\n\n## Don't do\n\n- Hardcode strings in pages or components.\n- Disable `cacheComponents: true` in `next.config.ts`.\n- Use `unstable_cache` \u2014 Next 16's canonical primitive is `'use cache'`.\n- Run `bun test` (Bun's `vi` shim is incomplete) \u2014 use `bun run test:run`.\n" }, { "path": "README.md", "kind": "text", "content": "# __STOREFRONT_NAME__\n\nA Cimplify storefront scaffolded from the **auto** template \u2014 Next.js 16 (App Router), React 19, Tailwind v4. Carbon-black + sport-red palette, Inter + JetBrains Mono typography, workshop-modern aesthetic.\n\n## Run\n\n```bash\nbun install\nbun dev\n```\n\nTwo things start in parallel:\n\n- `cimplify-mock --seed auto` \u2014 the Cimplify mock API on `http://127.0.0.1:8787`, seeded with Driveline Auto Parts.\n- `next dev` \u2014 this storefront on `http://localhost:3000`.\n\nOpen the storefront in your browser. Edit `app/page.tsx` to start customising.\n\n## Auto-specific behaviour\n\n- **Fitment finder** (`components/fitment-finder.tsx`): Make \u2192 Model \u2192 Year selector on the home page. Submitting pushes `?fits=fits:<make>:<model>` onto `/shop`; the shop page filters the catalogue by matching `tags` on `MockProduct`. Products tagged `fits:universal` always appear.\n- Vehicle data lives in `brand.fitments` (lib/brand.ts) \u2014 add a make/model/year by editing that array.\n- `Bosch`, `Mobil 1`, `Castrol`, `NGK`, `Brembo`, `Bridgestone` brand strip \u2014 swap to your own authorised brands.\n- `AutoPartsStore` schema.org `@type` is set in `brand.schemaType` so JSON-LD on every page identifies the business correctly.\n\n## Switch the seed\n\n```bash\ncimplify-mock --seed pharmacy # Wellspring Pharmacy\ncimplify-mock --seed restaurant # Mama's Kitchen\ncimplify-mock --seed retail # Currents Electronics\n```\n\n## Go live\n\n```diff\n# .env.local\n- NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev\n+ NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=<your tenant key>\n```\n\nDeploy with `cimplify deploy --prod` after linking the project. See [`cimplify` CLI docs](https://www.cimplify.dev/docs/cli). `next.config.ts` already whitelists the SDK image hosts under `images.remotePatterns`.\n" }, { "path": "bun.lock", "kind": "binary", "content": "ewogICJsb2NrZmlsZVZlcnNpb24iOiAxLAogICJjb25maWdWZXJzaW9uIjogMSwKICAid29ya3NwYWNlcyI6IHsKICAgICIiOiB7CiAgICAgICJuYW1lIjogIl9fU1RPUkVGUk9OVF9OQU1FX18iLAogICAgICAiZGVwZW5kZW5jaWVzIjogewogICAgICAgICJAY2ltcGxpZnkvc2RrIjogIl4wLjQ4LjIiLAogICAgICAgICJuZXh0IjogIl4xNi4yLjQiLAogICAgICAgICJyZWFjdCI6ICJeMTkuMC4wIiwKICAgICAgICAicmVhY3QtZG9tIjogIl4xOS4wLjAiLAogICAgICB9LAogICAgICAiZGV2RGVwZW5kZW5jaWVzIjogewogICAgICAgICJAdGFpbHdpbmRjc3MvcG9zdGNzcyI6ICJeNC4yLjQiLAogICAgICAgICJAdHlwZXMvbm9kZSI6ICJeMjIuMTAuMCIsCiAgICAgICAgIkB0eXBlcy9yZWFjdCI6ICJeMTkuMC4wIiwKICAgICAgICAiQHR5cGVzL3JlYWN0LWRvbSI6ICJeMTkuMC4wIiwKICAgICAgICAiY29uY3VycmVudGx5IjogIl45LjAuMCIsCiAgICAgICAgInRhaWx3aW5kY3NzIjogIl40LjIuNCIsCiAgICAgICAgInR5cGVzY3JpcHQiOiAiXjUuOS4zIiwKICAgICAgICAidml0ZXN0IjogIl40LjEuNSIsCiAgICAgIH0sCiAgICB9LAogIH0sCiAgInBhY2thZ2VzIjogewogICAgIkBhbGxvYy9xdWljay1scnUiOiBbIkBhbGxvYy9xdWljay1scnVANS4yLjAiLCAiIiwge30sICJzaGE1MTItVXJjQUJCKzRiVXJGQUJ3Ymx1VElCRXJYd3Zic1UvVjdUWldmbWJnSmZia3dpQnV6aVM5Z3hkT0RVeXVpZWNmZEdRODVqZ2xNVzZqdVMzK3o1VHNLTHc9PSJdLAoKICAgICJAYmFiZWwvcnVudGltZSI6IFsiQGJhYmVsL3J1bnRpbWVANy4yOS4yIiwgIiIsIHt9LCAic2hhNTEyLUppRFNoSDQ1ektIV3lHZTRaTlZSckNqQno4Tmg5VE1tWkcxa2g0UVRLOGhDQlRXQmk4RGEraTdzMWZKdzcvbFlwTTRjY2VwU05mcXpaL1F2QUJCaTVnPT0iXSwKCiAgICAiQGJhc2UtdWkvcmVhY3QiOiBbIkBiYXNlLXVpL3JlYWN0QDEuNS4wIiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIkBiYWJlbC9ydW50aW1lIjogIl43LjI5LjIiLCAiQGJhc2UtdWkvdXRpbHMiOiAiMC4yLjkiLCAiQGZsb2F0aW5nLXVpL3JlYWN0LWRvbSI6ICJeMi4xLjgiLCAiQGZsb2F0aW5nLXVpL3V0aWxzIjogIl4wLjIuMTEiLCAidXNlLXN5bmMtZXh0ZXJuYWwtc3RvcmUiOiAiXjEuNi4wIiB9LCAicGVlckRlcGVuZGVuY2llcyI6IHsgIkBkYXRlLWZucy90eiI6ICJeMS4yLjAiLCAiQHR5cGVzL3JlYWN0IjogIl4xNyB8fCBeMTggfHwgXjE5IiwgImRhdGUtZm5zIjogIl40LjAuMCIsICJyZWFjdCI6ICJeMTcgfHwgXjE4IHx8IF4xOSIsICJyZWFjdC1kb20iOiAiXjE3IHx8IF4xOCB8fCBeMTkiIH0sICJvcHRpb25hbFBlZXJzIjogWyJAZGF0ZS1mbnMvdHoiLCAiQHR5cGVzL3JlYWN0IiwgImRhdGUtZm5zIl0gfSwgInNoYTUxMi16MWdTQWxjZWQxeVkraU0rbUhERXRJa0Q4VUkzRWJzNTJNdUJQeHZWNmY1aFJ1dGsreHZDSC93dUI3aERxRHpLOUpHNUZvTXo1bmhycXRTczF3anQxQT09Il0sCgogICAgIkBiYXNlLXVpL3V0aWxzIjogWyJAYmFzZS11aS91dGlsc0AwLjIuOSIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAYmFiZWwvcnVudGltZSI6ICJeNy4yOS4yIiwgIkBmbG9hdGluZy11aS91dGlscyI6ICJeMC4yLjExIiwgInJlc2VsZWN0IjogIl41LjEuMSIsICJ1c2Utc3luYy1leHRlcm5hbC1zdG9yZSI6ICJeMS42LjAiIH0sICJwZWVyRGVwZW5kZW5jaWVzIjogeyAiQHR5cGVzL3JlYWN0IjogIl4xNyB8fCBeMTggfHwgXjE5IiwgInJlYWN0IjogIl4xNyB8fCBeMTggfHwgXjE5IiwgInJlYWN0LWRvbSI6ICJeMTcgfHwgXjE4IHx8IF4xOSIgfSwgIm9wdGlvbmFsUGVlcnMiOiBbIkB0eXBlcy9yZWFjdCJdIH0sICJzaGE1MTIteC9QRERDWXpvcVBwanJkeWIzVmN5eWxUSTJJalVYRXRZREdpNWZvaDdLc25tTkpJSWFWd0EyR0xnREgxZHBzMUdnWGlKYkE2MGhNK0F5dVRmUXpJdnc9PSJdLAoKICAgICJAY2ltcGxpZnkvc2RrIjogWyJAY2ltcGxpZnkvc2RrQDAuNDguMiIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAYmFzZS11aS9yZWFjdCI6ICJeMS40LjEiLCAiY2xzeCI6ICJeMi4xLjEiLCAiZGF0ZS1mbnMiOiAiXjQuMS4wIiwgImxpYnBob25lbnVtYmVyLWpzIjogIjEuMTIuNDEiLCAicmVhY3QtZGF5LXBpY2tlciI6ICJeOS4xNC4wIiwgInRhaWx3aW5kLW1lcmdlIjogIl4zLjUuMCIsICJ6b2QiOiAiXjQuNC4zIiB9LCAicGVlckRlcGVuZGVuY2llcyI6IHsgIkBwYXlzdGFjay9pbmxpbmUtanMiOiAiXjIuMjIuOCIsICJtc3ciOiAiPj0yLjAuMCIsICJyZWFjdCI6ICI+PTE3LjAuMCIsICJ2aXRlc3QiOiAiPj0yLjAuMCIgfSwgIm9wdGlvbmFsUGVlcnMiOiBbIkBwYXlzdGFjay9pbmxpbmUtanMiLCAibXN3IiwgInJlYWN0IiwgInZpdGVzdCJdLCAiYmluIjogeyAiY2ltcGxpZnktbW9jayI6ICJkaXN0L21vY2svY2xpLm1qcyIgfSB9LCAic2hhNTEyLTZJaDhNNkxZVkhDdU84c2JnZWc3VmdQTUJIWHZNK2NodFkzLzJWZkdWRWZFeU5UY0d4UzZlcDFOMDdNS21tTG5xQWNMZTZteFQwbVFiL2h0cldQZ1hBPT0iXSwKCiAgICAiQGRhdGUtZm5zL3R6IjogWyJAZGF0ZS1mbnMvdHpAMS40LjEiLCAiIiwge30sICJzaGE1MTItUDVMVU5odGJqNllmSTNpSmp3NUVMOWVVQUc2T2l0RDBXM2ZXUWNwUWpEUmMvUUlzTDB0Uk51TzFQY0R2UGNjV0wxZlNUWFhkRTFkcytsOTVEVi9PRkE9PSJdLAoKICAgICJAZW1uYXBpL2NvcmUiOiBbIkBlbW5hcGkvY29yZUAxLjEwLjAiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiQGVtbmFwaS93YXNpLXRocmVhZHMiOiAiMS4yLjEiLCAidHNsaWIiOiAiXjIuNC4wIiB9IH0sICJzaGE1MTIteXE2T2tKNHA4MkNBZlBsMHU5bVFlYlFIS1BKa1k3V3JJdWsyMDVjVFluWWUrazJaOFlCaDExRnJiUkcvSDZpaGlycWNhY09nbDJCSU84b3lNUUxlWHc9PSJdLAoKICAgICJAZW1uYXBpL3J1bnRpbWUiOiBbIkBlbW5hcGkvcnVudGltZUAxLjEwLjAiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAidHNsaWIiOiAiXjIuNC4wIiB9IH0sICJzaGE1MTItZXd2WWxrODZ4VW9HSTB6UVJOcS9tQysxNlIxUWVEbEtReTIxS2kzb1NZWE5nTGI0NUdWMVA2QTBNKy9zNm55Q3VORHFlNVZwYVk4NEJ6WEd3VmJ3RkE9PSJdLAoKICAgICJAZW1uYXBpL3dhc2ktdGhyZWFkcyI6IFsiQGVtbmFwaS93YXNpLXRocmVhZHNAMS4yLjEiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAidHNsaWIiOiAiXjIuNC4wIiB9IH0sICJzaGE1MTItdVRJSTdPWUYrL01lcy9NcmNJT1lwNXlPdFNNTEJXU0lvTFBwY2d3aXBvaUtibGk2azMyMnRjb0ZzeG9JSXhQRHFXMDFTUUdBZ2tvNEV6WmkyQk52Mnc9PSJdLAoKICAgICJAZmxvYXRpbmctdWkvY29yZSI6IFsiQGZsb2F0aW5nLXVpL2NvcmVAMS43LjUiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiQGZsb2F0aW5nLXVpL3V0aWxzIjogIl4wLjIuMTEiIH0gfSwgInNoYTUxMi0xSWg0V1RXeXcwK2xLeUZNY0JIR2JiNVU1RnR1SEp1dWpveXlyNXpUYVdTNUVZTWVUNkpiMkF1RGVmdHNDc0V1Y2hPK21NMmlqNStxOWNyaHlkekxoUT09Il0sCgogICAgIkBmbG9hdGluZy11aS9kb20iOiBbIkBmbG9hdGluZy11aS9kb21AMS43LjYiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiQGZsb2F0aW5nLXVpL2NvcmUiOiAiXjEuNy41IiwgIkBmbG9hdGluZy11aS91dGlscyI6ICJeMC4yLjExIiB9IH0sICJzaGE1MTItOWdaU0FJNVhNMzY4ODBQUE1tLy85ZGZpRW5nWW9DNkFtMml6RVMxRkY0MDZZRnNqdnlCTW1lSjJnNFNBanUzeFd3dHV5bk5SRkwyczloZ3hwTEk1U1E9PSJdLAoKICAgICJAZmxvYXRpbmctdWkvcmVhY3QtZG9tIjogWyJAZmxvYXRpbmctdWkvcmVhY3QtZG9tQDIuMS44IiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIkBmbG9hdGluZy11aS9kb20iOiAiXjEuNy42IiB9LCAicGVlckRlcGVuZGVuY2llcyI6IHsgInJlYWN0IjogIj49MTYuOC4wIiwgInJlYWN0LWRvbSI6ICI+PTE2LjguMCIgfSB9LCAic2hhNTEyLWNDNTJiSHdNL24vQ3hTODdGSDB5V2RuZ0VacmpkdExXL3FWcnVvNjhxZytwcks3WlE0WUdkdXQyR3lEVnBvR2VBWWUvaDg5OXJWZU9WbTZPaTQwazJBPT0iXSwKCiAgICAiQGZsb2F0aW5nLXVpL3V0aWxzIjogWyJAZmxvYXRpbmctdWkvdXRpbHNAMC4yLjExIiwgIiIsIHt9LCAic2hhNTEyLVJpQi95SWg3OHBjSXhsNmxMTUcwQ2dCWEFaMlkwZVZIcU1QWXVndSs5VTBBZVQ2WUJlaUpwZjdsYmRKTkl1Z0ZQNVNJandOUmdvNERoUjFReGkyNkdnPT0iXSwKCiAgICAiQGltZy9jb2xvdXIiOiBbIkBpbWcvY29sb3VyQDEuMS4wIiwgIiIsIHt9LCAic2hhNTEyLVRkNzZxN2o1N28vdExWZGdTNzQ2Y1lBUmZTeXhrOGlFZlJ4ZXdMOWg0T016WWhiVzRUQWNwcGwwbVQ0ZXlxWGRkaDZML2p3b003NW1vN2l4YS9wQ2VRPT0iXSwKCiAgICAiQGltZy9zaGFycC1kYXJ3aW4tYXJtNjQiOiBbIkBpbWcvc2hhcnAtZGFyd2luLWFybTY0QDAuMzQuNSIsICIiLCB7ICJvcHRpb25hbERlcGVuZGVuY2llcyI6IHsgIkBpbWcvc2hhcnAtbGlidmlwcy1kYXJ3aW4tYXJtNjQiOiAiMS4yLjQiIH0sICJvcyI6ICJkYXJ3aW4iLCAiY3B1IjogImFybTY0IiB9LCAic2hhNTEyLWltdFEzV01KWGJNWTRmeGIvTmRwNkhCVE5WdFdDVUkwV2RvYnloZUdmNSthZDZ4WDhWSURPOHUyeEU0cWMvZnIwOENLRy83ZERzZUZ0bjZNNmcvcjN3PT0iXSwKCiAgICAiQGltZy9zaGFycC1kYXJ3aW4teDY0IjogWyJAaW1nL3NoYXJwLWRhcndpbi14NjRAMC4zNC41IiwgIiIsIHsgIm9wdGlvbmFsRGVwZW5kZW5jaWVzIjogeyAiQGltZy9zaGFycC1saWJ2aXBzLWRhcndpbi14NjQiOiAiMS4yLjQiIH0sICJvcyI6ICJkYXJ3aW4iLCAiY3B1IjogIng2NCIgfSwgInNoYTUxMi1ZTkVGQUYvNEtRL1BlVzBOK3IrYVZWc29JWTAvcXh4aWtGMlNXZHArTlJrbU1CN3k5TEJaQVZxUTR5aEdDbS9IM0gyNzBPU3lrcW1RTUtMQmhCSkRFdz09Il0sCgogICAgIkBpbWcvc2hhcnAtbGlidmlwcy1kYXJ3aW4tYXJtNjQiOiBbIkBpbWcvc2hhcnAtbGlidmlwcy1kYXJ3aW4tYXJtNjRAMS4yLjQiLCAiIiwgeyAib3MiOiAiZGFyd2luIiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi16cWpqbzdSYXRGZkZvUDBNa1E1MWpmdUZaQm5WRTJwUmlheWRLSjFHL3JIWnZuc3JIQU9jUUFMSWk5c0E1Y281eGVuUWRUdWdDdnRiMWN1Zjc4VmY0Zz09Il0sCgogICAgIkBpbWcvc2hhcnAtbGlidmlwcy1kYXJ3aW4teDY0IjogWyJAaW1nL3NoYXJwLWxpYnZpcHMtZGFyd2luLXg2NEAxLjIuNCIsICIiLCB7ICJvcyI6ICJkYXJ3aW4iLCAiY3B1IjogIng2NCIgfSwgInNoYTUxMi0xSU9kNXhmVmhsR3dYK3pYdjJOOTNrMHlNT052VWxBTnlsYkp3MWVUYWg4Sy9KdHBpMTVLQytXU2lhWC9uQm1ibTJIeFJNMWdaMG5TZGpTc3JaYkdLZz09Il0sCgogICAgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC1hcm0iOiBbIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC1hcm1AMS4yLjQiLCAiIiwgeyAib3MiOiAibGludXgiLCAiY3B1IjogImFybSIgfSwgInNoYTUxMi1iRkk3eGNLRkVMZGlOQ1ZvdjhlNDRJYTR1MmJ5QStsM1h0c0FqK1E4dGZDd082QlE4aURvallkdm9QTXFzS0RrdW9PbytYNkhaQTBzMHExMUFOTVE4QT09Il0sCgogICAgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC1hcm02NCI6IFsiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4LWFybTY0QDEuMi40IiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi1leGNqWDhEZnNJY0oxMHgxS3pyNFJjV2UxZWRDOVBxdURSUlB4M1lWQ3ZRditVNXA3WWluMnMzMmZ0emlrWG9qYjFQSUZjLzlNdDI4L3kraVJrbGtydz09Il0sCgogICAgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC1wcGM2NCI6IFsiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4LXBwYzY0QDEuMi40IiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJwcGM2NCIgfSwgInNoYTUxMi1GTXV2R2lqTERZRzZsVytiL1V2eWlsVVd1NUF5dSszcjJkMVM4bm90aUdDSXlZVS83NmVpZzFVZk1ta1o3dndnT3J6S3psUWJGU3VRZmdtN0dZVVBwQT09Il0sCgogICAgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC1yaXNjdjY0IjogWyJAaW1nL3NoYXJwLWxpYnZpcHMtbGludXgtcmlzY3Y2NEAxLjIuNCIsICIiLCB7ICJvcyI6ICJsaW51eCIsICJjcHUiOiAibm9uZSIgfSwgInNoYTUxMi1vVkRiY1I0elVDMGNlODJ0ZXViU20reDZFVGl4dEtaQmgvcWJSRUlPY0kzY1VMekR5YjE4U3IvV2N5eDdOUlFlUXpPaUhUTmJaRkYxVXdQUzJzY3lHQT09Il0sCgogICAgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC1zMzkweCI6IFsiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4LXMzOTB4QDEuMi40IiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJzMzkweCIgfSwgInNoYTUxMi1xbXA5VnJ6Z1BnTW9HWnlQdnJRSHFrMDJ1eWpBMC9RclRPMjZUcWs2bDRaVjBNUFdJVzZMVGtxT0lvditKMXlFdTdNYkZRYURwd2R3SktoYkp2dVJ4UT09Il0sCgogICAgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC14NjQiOiBbIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC14NjRAMS4yLjQiLCAiIiwgeyAib3MiOiAibGludXgiLCAiY3B1IjogIng2NCIgfSwgInNoYTUxMi10SnhpaUxzbUhjOUF4MWJ6M29hT1lCVVJUWEdJUkRPREJxaHZlVkhvbnJISjkvK2s4OXFiTGwwYmNKbnMrZTR0NHJ2YU5CeGFFWnNGdFNmQWRxdVBydz09Il0sCgogICAgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eG11c2wtYXJtNjQiOiBbIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eG11c2wtYXJtNjRAMS4yLjQiLCAiIiwgeyAib3MiOiAibGludXgiLCAiY3B1IjogImFybTY0IiB9LCAic2hhNTEyLUZWUUh1d3gxSUl1Tm93OVFBYllVekorRW44S2NWbTlMazUrdUdVUUpIYVptTUVDWm1PbGl4OUhuSDduMVRSa1hNUzBwR3hJSm9rSVZCOVN1cVpHR1h3PT0iXSwKCiAgICAiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4bXVzbC14NjQiOiBbIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eG11c2wteDY0QDEuMi40IiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJ4NjQiIH0sICJzaGE1MTItK0xweUJrN0w0NFpJWHd6L1ZZZmdsYVgvb2t4ZXpFU2M2VXhEU295bzJLczZKeGM0WTdzR2pwZ1U5czRQTWdxZ2pqMWdaQ3lsVGllTmFtcUExTUY3RGc9PSJdLAoKICAgICJAaW1nL3NoYXJwLWxpbnV4LWFybSI6IFsiQGltZy9zaGFycC1saW51eC1hcm1AMC4zNC41IiwgIiIsIHsgIm9wdGlvbmFsRGVwZW5kZW5jaWVzIjogeyAiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4LWFybSI6ICIxLjIuNCIgfSwgIm9zIjogImxpbnV4IiwgImNwdSI6ICJhcm0iIH0sICJzaGE1MTItOWRMcXN2d3RnMXV1WEJHWktzeGVtOTU5NSt1anYwc0o2Vmk4d2NUQU5TRnB3Vi9HT05hdDVlQ2t6UW8vMU82elJJa2gwbS84KzVCanJScjdqRFVTWnc9PSJdLAoKICAgICJAaW1nL3NoYXJwLWxpbnV4LWFybTY0IjogWyJAaW1nL3NoYXJwLWxpbnV4LWFybTY0QDAuMzQuNSIsICIiLCB7ICJvcHRpb25hbERlcGVuZGVuY2llcyI6IHsgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC1hcm02NCI6ICIxLjIuNCIgfSwgIm9zIjogImxpbnV4IiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi1iS1F6YUpSWS9ia1BPWHlLeDVFVnVwN3FrYW9qRUNHNk5MWXN3Z2t0T1pqYVhlY1NBZUNXaVp3d2lGZjMvWStPMUhyYXVpRTNGVnNHeEZnOGMyNHJaZz09Il0sCgogICAgIkBpbWcvc2hhcnAtbGludXgtcHBjNjQiOiBbIkBpbWcvc2hhcnAtbGludXgtcHBjNjRAMC4zNC41IiwgIiIsIHsgIm9wdGlvbmFsRGVwZW5kZW5jaWVzIjogeyAiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4LXBwYzY0IjogIjEuMi40IiB9LCAib3MiOiAibGludXgiLCAiY3B1IjogInBwYzY0IiB9LCAic2hhNTEyLTd6em53TmFxVzZZdHNmckdHREE2QlJrSVNLQUFFMUpvMFFkcE5ZWE5NSHUyKzBkVHJQZmxUTE5rcGM4bDdNVVA1TTE2WkpjVXZ5c1ZXV3JNZWZacXVBPT0iXSwKCiAgICAiQGltZy9zaGFycC1saW51eC1yaXNjdjY0IjogWyJAaW1nL3NoYXJwLWxpbnV4LXJpc2N2NjRAMC4zNC41IiwgIiIsIHsgIm9wdGlvbmFsRGVwZW5kZW5jaWVzIjogeyAiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4LXJpc2N2NjQiOiAiMS4yLjQiIH0sICJvcyI6ICJsaW51eCIsICJjcHUiOiAibm9uZSIgfSwgInNoYTUxMi01MWdKdUxQVEthN3BpWVBhVnM4R21CeW83L1U3LzdUWk9xK2NuWEpJSFpLYXZJUkhBUDc3ZTNOMkhFbDNkZ2lxZEQvdzB5VWZpSm5JSTc3UHVEREZkdz09Il0sCgogICAgIkBpbWcvc2hhcnAtbGludXgtczM5MHgiOiBbIkBpbWcvc2hhcnAtbGludXgtczM5MHhAMC4zNC41IiwgIiIsIHsgIm9wdGlvbmFsRGVwZW5kZW5jaWVzIjogeyAiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4LXMzOTB4IjogIjEuMi40IiB9LCAib3MiOiAibGludXgiLCAiY3B1IjogInMzOTB4IiB9LCAic2hhNTEyLW5RdENrMFBkS2ZobzNlQzVNcmJRb2lnSjJnZDFDZ2RkVU1rYWJVaityQmV2czh0WjJjVUxPeDQ2RTdveVgrMDRXR2ZBQmdJd21NQzBWcWllVGlSNGpnPT0iXSwKCiAgICAiQGltZy9zaGFycC1saW51eC14NjQiOiBbIkBpbWcvc2hhcnAtbGludXgteDY0QDAuMzQuNSIsICIiLCB7ICJvcHRpb25hbERlcGVuZGVuY2llcyI6IHsgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC14NjQiOiAiMS4yLjQiIH0sICJvcyI6ICJsaW51eCIsICJjcHUiOiAieDY0IiB9LCAic2hhNTEyLU1FemQ4SFBLeFZ4VmVud0FhK0pSUHdFQzdRRmpvUFd1UzVOWm5CdDZCM3B1N0VHMkdlMGlkMW9MSFpwUEpkbjNPUUsrQlFEaXc5elN0aUhCVEpRUVFRPT0iXSwKCiAgICAiQGltZy9zaGFycC1saW51eG11c2wtYXJtNjQiOiBbIkBpbWcvc2hhcnAtbGludXhtdXNsLWFybTY0QDAuMzQuNSIsICIiLCB7ICJvcHRpb25hbERlcGVuZGVuY2llcyI6IHsgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eG11c2wtYXJtNjQiOiAiMS4yLjQiIH0sICJvcyI6ICJsaW51eCIsICJjcHUiOiAiYXJtNjQiIH0sICJzaGE1MTItZnBySlI2R3RSc010Nkt5ZnE0NElzQ2hWWmVHTjk3Z1REMzMxd2VSMWV4MWMxcnlwREVBQk42VG0yeGExd0U2bFliNURkRW5rMDNOWlBxQTdJZDIxeWc9PSJdLAoKICAgICJAaW1nL3NoYXJwLWxpbnV4bXVzbC14NjQiOiBbIkBpbWcvc2hhcnAtbGludXhtdXNsLXg2NEAwLjM0LjUiLCAiIiwgeyAib3B0aW9uYWxEZXBlbmRlbmNpZXMiOiB7ICJAaW1nL3NoYXJwLWxpYnZpcHMtbGludXhtdXNsLXg2NCI6ICIxLjIuNCIgfSwgIm9zIjogImxpbnV4IiwgImNwdSI6ICJ4NjQiIH0sICJzaGE1MTItSmc4d05UMU1Vekl2aEJGeFZpcXJFaFdER3pxeW1vM3NWN3o3WnNhV2JaTkRMWFJKWm9SR3JqdWxwNjBZWXRWNHdmWThWSUtjV2lkam9qbExjV3JkOFE9PSJdLAoKICAgICJAaW1nL3NoYXJwLXdhc20zMiI6IFsiQGltZy9zaGFycC13YXNtMzJAMC4zNC41IiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIkBlbW5hcGkvcnVudGltZSI6ICJeMS43LjAiIH0sICJjcHUiOiAibm9uZSIgfSwgInNoYTUxMi1PZFdURWlWa1kyUEh3cWtiQkk4ZnJGeFFRRmVrSGFTU2tVSUprd3pjbFdaZTY0TzFYNFVsVWpxcXFMYVBiVXBNT1FrNkZCdS9IdGxHWE5ibElzMGh1dz09Il0sCgogICAgIkBpbWcvc2hhcnAtd2luMzItYXJtNjQiOiBbIkBpbWcvc2hhcnAtd2luMzItYXJtNjRAMC4zNC41IiwgIiIsIHsgIm9zIjogIndpbjMyIiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi1XUTNBZ1dDV1lTYjJ5dCtJRzhtbkM2SmRrOVdoczdPMGd4cGhibHNMdmRoU3BTVHRtdTY5WkcxR2tiNk51dnhzTkFDd2lQVjZjTlNaTnp0MEtQc3c3Zz09Il0sCgogICAgIkBpbWcvc2hhcnAtd2luMzItaWEzMiI6IFsiQGltZy9zaGFycC13aW4zMi1pYTMyQDAuMzQuNSIsICIiLCB7ICJvcyI6ICJ3aW4zMiIsICJjcHUiOiAiaWEzMiIgfSwgInNoYTUxMi1GVjltLzdObWVDbVNIREQ1ajQrNHBOSThDcDNhVytKdkxvWGNUVW8wSXF5alNmQVpKOGRJVW1pangxcWFKc0lpVStIb3N3NnhNNUtpakFXUkpDU2dOZz09Il0sCgogICAgIkBpbWcvc2hhcnAtd2luMzIteDY0IjogWyJAaW1nL3NoYXJwLXdpbjMyLXg2NEAwLjM0LjUiLCAiIiwgeyAib3MiOiAid2luMzIiLCAiY3B1IjogIng2NCIgfSwgInNoYTUxMi0rMjlZTXNxWTIvOWVGRWlXOTNlcVdudUxjV2N1Zm93WGV3d1NOSVQ2VXdaZFVVQ3JNM29Gak1XSC9aNi9UTW1iNGhsRmVubWZBVmJwV2V1cDJqcnlDdz09Il0sCgogICAgIkBqcmlkZ2V3ZWxsL2dlbi1tYXBwaW5nIjogWyJAanJpZGdld2VsbC9nZW4tbWFwcGluZ0AwLjMuMTMiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiQGpyaWRnZXdlbGwvc291cmNlbWFwLWNvZGVjIjogIl4xLjUuMCIsICJAanJpZGdld2VsbC90cmFjZS1tYXBwaW5nIjogIl4wLjMuMjQiIH0gfSwgInNoYTUxMi0ya2t0LzduaUo2TWdFUHhGMGJZZFE2ZXRaYUErZlF2RGNMS2NraHkxeUlRT3phb0tqQkJqU2o2My9hTFZqWUUzcWhSdDVkdk0rdVV5ZkNnNlVLQ0JiQT09Il0sCgogICAgIkBqcmlkZ2V3ZWxsL3JlbWFwcGluZyI6IFsiQGpyaWRnZXdlbGwvcmVtYXBwaW5nQDIuMy41IiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIkBqcmlkZ2V3ZWxsL2dlbi1tYXBwaW5nIjogIl4wLjMuNSIsICJAanJpZGdld2VsbC90cmFjZS1tYXBwaW5nIjogIl4wLjMuMjQiIH0gfSwgInNoYTUxMi1MSTl1LytsYVlHNERzMVRES1NKVzJZUHJJbGNWWU93aTJmVUM2eEI0M2x1ZUNqZ3hWNGxmZk9DWkN0WUZpSDZUTk9YK3RRS1h4OTdUNElLSGJoeUhFUT09Il0sCgogICAgIkBqcmlkZ2V3ZWxsL3Jlc29sdmUtdXJpIjogWyJAanJpZGdld2VsbC9yZXNvbHZlLXVyaUAzLjEuMiIsICIiLCB7fSwgInNoYTUxMi1iUklTZ0NJalAyMC90YldTUFdNRWk1NFFWUFJaRXhrdUQ5bEpMK1VJeFVLdHdWSkE4d1cxVHJiMWpNczFSRlhvMUNCVE5aLzVocEM5UXZtS1dkb3BLdz09Il0sCgogICAgIkBqcmlkZ2V3ZWxsL3NvdXJjZW1hcC1jb2RlYyI6IFsiQGpyaWRnZXdlbGwvc291cmNlbWFwLWNvZGVjQDEuNS41IiwgIiIsIHt9LCAic2hhNTEyLWNZUTkzMTBncnF4dWVXYmwrV3VJVUlhaVVhRGNqN1dPcTVmVmhFbGpOVmdSZk9VaFk5ZnkyelR2Zm9xV3NuZWJoOFNsNzBWU2NGYklDdkpuTEtCME9nPT0iXSwKCiAgICAiQGpyaWRnZXdlbGwvdHJhY2UtbWFwcGluZyI6IFsiQGpyaWRnZXdlbGwvdHJhY2UtbWFwcGluZ0AwLjMuMzEiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiQGpyaWRnZXdlbGwvcmVzb2x2ZS11cmkiOiAiXjMuMS4wIiwgIkBqcmlkZ2V3ZWxsL3NvdXJjZW1hcC1jb2RlYyI6ICJeMS40LjE0IiB9IH0sICJzaGE1MTItenpOUitTZFFTREp6Yzhqb2FlUDhRUW9DUXI4TnVZeDJkSUl5dGwxUWVCRVpISjl1VzZoZWJzcllnYno4aEp3VVFhbzNUV0NNdG1mVjhOdTF0d09MQXc9PSJdLAoKICAgICJAbmFwaS1ycy93YXNtLXJ1bnRpbWUiOiBbIkBuYXBpLXJzL3dhc20tcnVudGltZUAxLjEuNCIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAdHlieXMvd2FzbS11dGlsIjogIl4wLjEwLjEiIH0sICJwZWVyRGVwZW5kZW5jaWVzIjogeyAiQGVtbmFwaS9jb3JlIjogIl4xLjcuMSIsICJAZW1uYXBpL3J1bnRpbWUiOiAiXjEuNy4xIiB9IH0sICJzaGE1MTItM05RTk5nQTFZU2xKYi9rTUgxaWxkQVNQOUhXNy83a1luUkkyc3pXSmFvZmFTMWhXbWJHSTRIK2QzKzIyYUd6WFhOOUlKK24rR2lGVmNHaXBKUDE4b3c9PSJdLAoKICAgICJAbmV4dC9lbnYiOiBbIkBuZXh0L2VudkAxNi4yLjYiLCAiIiwge30sICJzaGE1MTItZ2Q4SG9ITjR1Zmo3M1dtUjNKbVZvbHJwSlI0N0lMSzZMb3VQNXhFbFBnbGFWeGlyNmUxYTdWenZUdkRXa09vUFhUOXJra1R6eUN4QnU0eWVaZlp3Y3c9PSJdLAoKICAgICJAbmV4dC9zd2MtZGFyd2luLWFybTY0IjogWyJAbmV4dC9zd2MtZGFyd2luLWFybTY0QDE2LjIuNiIsICIiLCB7ICJvcyI6ICJkYXJ3aW4iLCAiY3B1IjogImFybTY0IiB9LCAic2hhNTEyLVpKR2trY05mWWdyck1rcU9kWjd6b0xhMVRPeTBxcGNNZmsvejRNaC9GS1V6NDBnVk8rSE5RV3FtTHhmNjdaNVdCNjREUnAwZGhFYnlIZmVsKzZzSlVnPT0iXSwKCiAgICAiQG5leHQvc3djLWRhcndpbi14NjQiOiBbIkBuZXh0L3N3Yy1kYXJ3aW4teDY0QDE2LjIuNiIsICIiLCB7ICJvcyI6ICJkYXJ3aW4iLCAiY3B1IjogIng2NCIgfSwgInNoYTUxMi12L1lMQkhJWTEzMkNlZDNwdUJKN1lKS3cxbHFzQ3JnY05vMmFSSmxDRXlRcnJDZVJKbHZHbG5teGhQeE5RSTNLRTNOMURONXI5VFBOUHZrYTNucTVSUT09Il0sCgogICAgIkBuZXh0L3N3Yy1saW51eC1hcm02NC1nbnUiOiBbIkBuZXh0L3N3Yy1saW51eC1hcm02NC1nbnVAMTYuMi42IiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi1SUE92cWxZQmJjUWprejlWUVFEWjJUMmJBUklqWFpWMUtGbHQrVjJNcjZTVy9lNEk5ZmNLc2FBMGhkeWYyRkhvVGxzVjJ4bkJkNVk5MTJyUC8xQ2U2dz09Il0sCgogICAgIkBuZXh0L3N3Yy1saW51eC1hcm02NC1tdXNsIjogWyJAbmV4dC9zd2MtbGludXgtYXJtNjQtbXVzbEAxNi4yLjYiLCAiIiwgeyAib3MiOiAibGludXgiLCAiY3B1IjogImFybTY0IiB9LCAic2hhNTEyLVVSVVR1MStkTWt4SnNQRmdtK09lRXZxOXdmNXN1ancwRXZnWXk4MFRER0hUU0xUbklIZXFiMEV1OEEzc0M5NUlSZ2plalFMK2tDNG13KzR5UHhpQVhBPT0iXSwKCiAgICAiQG5leHQvc3djLWxpbnV4LXg2NC1nbnUiOiBbIkBuZXh0L3N3Yy1saW51eC14NjQtZ251QDE2LjIuNiIsICIiLCB7ICJvcyI6ICJsaW51eCIsICJjcHUiOiAieDY0IiB9LCAic2hhNTEyLURPajE4Mm1QVjhHM1VrcmF5TG9SRU01WUVZSStEazV3djdPeDl4bDFmRmliQUVMRXNGRDBsRFBmSEllSUxsdXRNTWZkeWhsellQRUxHM3BldUthdXJ3PT0iXSwKCiAgICAiQG5leHQvc3djLWxpbnV4LXg2NC1tdXNsIjogWyJAbmV4dC9zd2MtbGludXgteDY0LW11c2xAMTYuMi42IiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJ4NjQiIH0sICJzaGE1MTItSEtRNVNQL1YvdWI3M1V2RjduL3plSmx4azJrTG10TDdXenJnNFdmbWtqbU5vczVvbkoydEt1N3laT1BkTDE4QTZTdmZuM21heDI5eW0rcnk3TmtLNGc9PSJdLAoKICAgICJAbmV4dC9zd2Mtd2luMzItYXJtNjQtbXN2YyI6IFsiQG5leHQvc3djLXdpbjMyLWFybTY0LW1zdmNAMTYuMi42IiwgIiIsIHsgIm9zIjogIndpbjMyIiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi1MWlhwVGxQeVM1djdIaFNtbnZzTEdQM2lJWWdZT0JuYzhyOEFybFQ1NXNHSFY4OWJSMkhsRGRCaldRK1BZNlNKTW1rOFR1VkdGdXhhbG5QM2svMER3Zz09Il0sCgogICAgIkBuZXh0L3N3Yy13aW4zMi14NjQtbXN2YyI6IFsiQG5leHQvc3djLXdpbjMyLXg2NC1tc3ZjQDE2LjIuNiIsICIiLCB7ICJvcyI6ICJ3aW4zMiIsICJjcHUiOiAieDY0IiB9LCAic2hhNTEyLUYwKzRpMGg5SjZDNGVFM0VBUFdzb0NrN1VXL2Riek9qeXp4WTBxbkRVT1lGdTZGRm1kWjZsOTcvWGRWMy9OejNWWXlPN1VXanlFSlVYa0dxY29YZk1BPT0iXSwKCiAgICAiQG94Yy1wcm9qZWN0L3R5cGVzIjogWyJAb3hjLXByb2plY3QvdHlwZXNAMC4xMzAuMCIsICIiLCB7fSwgInNoYTUxMi1pYkQydXN4OUpSdTdmNXB1MnRNS01JNGNwQTROZ1hKUW9ZUlA0cFE3UHhtbjFsNmsvNTNxV3RRV1pheWhZeTNYNFFaa3Q5ME90K21KRWFlWG91aW82UT09Il0sCgogICAgIkByb2xsZG93bi9iaW5kaW5nLWFuZHJvaWQtYXJtNjQiOiBbIkByb2xsZG93bi9iaW5kaW5nLWFuZHJvaWQtYXJtNjRAMS4wLjEiLCAiIiwgeyAib3MiOiAiYW5kcm9pZCIsICJjcHUiOiAiYXJtNjQiIH0sICJzaGE1MTItZkpJM0kwcjNDM09qL3pkQkNwYUNtQlJaWWYwN3hwYXE0eUNmRERvU0ZtK2JlV056YklsMjZwdVc4UnJhVWR1Z29Kdy85NXplck5PbjZqYXNBaHpTbWc9PSJdLAoKICAgICJAcm9sbGRvd24vYmluZGluZy1kYXJ3aW4tYXJtNjQiOiBbIkByb2xsZG93bi9iaW5kaW5nLWRhcndpbi1hcm02NEAxLjAuMSIsICIiLCB7ICJvcyI6ICJkYXJ3aW4iLCAiY3B1IjogImFybTY0IiB9LCAic2hhNTEyLWNLbkFoV0VzVjdUUGNBLzVFQXRlRHA2S2NKWkJRMkcrQnFFN3pheU1NaTdrTXZ3UnNidjdXVDlhT25uMFdObDRTS0VJZjQzdmpTMzFpVVB1ODBuelhnPT0iXSwKCiAgICAiQHJvbGxkb3duL2JpbmRpbmctZGFyd2luLXg2NCI6IFsiQHJvbGxkb3duL2JpbmRpbmctZGFyd2luLXg2NEAxLjAuMSIsICIiLCB7ICJvcyI6ICJkYXJ3aW4iLCAiY3B1IjogIng2NCIgfSwgInNoYTUxMi1ZS3JWd1FqSVJCUG8rNUcvdTAzd0dqYmR5NHE3cHl6Q2U5M0RLOVZKN3prVm1lZzhMSjdHYmdzaUhXZFI0eFNvZTRDQVhSRDdCY2pnYnRyNjRia1hOZz09Il0sCgogICAgIkByb2xsZG93bi9iaW5kaW5nLWZyZWVic2QteDY0IjogWyJAcm9sbGRvd24vYmluZGluZy1mcmVlYnNkLXg2NEAxLjAuMSIsICIiLCB7ICJvcyI6ICJmcmVlYnNkIiwgImNwdSI6ICJ4NjQiIH0sICJzaGE1MTItei9vQnNSRW80NlNzRnFCd1l0RmUwa3BKZUJpakFUNDhPL1dYTEk0c3VpQ0xCa3IwM1JUdFRKTUN6U2REZDJ6bmxoOFZKaXpMMDlYVmtRZ2s4SVpvbnc9PSJdLAoKICAgICJAcm9sbGRvd24vYmluZGluZy1saW51eC1hcm0tZ251ZWFiaWhmIjogWyJAcm9sbGRvd24vYmluZGluZy1saW51eC1hcm0tZ251ZWFiaWhmQDEuMC4xIiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJhcm0iIH0sICJzaGE1MTItaWs4cTdHTTExenh2WXhGYzJQZURjVDZUQnZoQ1FNYVV4ZnBoL001bDlzS3VUcy9TamczTCtCeXcwRjd3MFpWTEJabXgzMFArZ0cwRUN6ek4rTUZjbVE9PSJdLAoKICAgICJAcm9sbGRvd24vYmluZGluZy1saW51eC1hcm02NC1nbnUiOiBbIkByb2xsZG93bi9iaW5kaW5nLWxpbnV4LWFybTY0LWdudUAxLjAuMSIsICIiLCB7ICJvcyI6ICJsaW51eCIsICJjcHUiOiAiYXJtNjQiIH0sICJzaGE1MTItUW9TeDJFa3lycmRaNmtjeUU4c3RxWjYydDBZcmE4RnM1aWE5bE94SnJoNlRNUUpLN2dRS21zY2RUSGY3cE9YS1JFS3JWd090SmNRRzNxVlNmYzg2NkE9PSJdLAoKICAgICJAcm9sbGRvd24vYmluZGluZy1saW51eC1hcm02NC1tdXNsIjogWyJAcm9sbGRvd24vYmluZGluZy1saW51eC1hcm02NC1tdXNsQDEuMC4xIiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi11d053RnB3S2VOaVphd2ZBV0JnZzBWSXp0UFRWM2loaGgxdlYzMzRoOWl2bk5Mb3J4blFNVTZGejh3RzFaYjRRaDlMQzEvTWtjeVQzWWxEWEczUnNnZz09Il0sCgogICAgIkByb2xsZG93bi9iaW5kaW5nLWxpbnV4LXBwYzY0LWdudSI6IFsiQHJvbGxkb3duL2JpbmRpbmctbGludXgtcHBjNjQtZ251QDEuMC4xIiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJwcGM2NCIgfSwgInNoYTUxMi16WTFidWw3T1dyN0RGQmlKKyt3b2ZYdm5yOEI0NWNlM1FzUVVoS3JJaFhzeWdBaDdiVGt3eWVNMWJpMWEyZzVDL3lDL044VFp5R0RFb01mbS9sOW1wZz09Il0sCgogICAgIkByb2xsZG93bi9iaW5kaW5nLWxpbnV4LXMzOTB4LWdudSI6IFsiQHJvbGxkb3duL2JpbmRpbmctbGludXgtczM5MHgtZ251QDEuMC4xIiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJzMzkweCIgfSwgInNoYTUxMi0wZnJsc1QvZjRGdDZJN1NNRVNUS25GM2Nac2RpY1FuMWRDTWtGL2pUOXdETEUrZ0dvaVFmdjFubVQ5ZStzN3MvZmVrdnZ5NnRaTTJqSHZJMnRrYkpEUT09Il0sCgogICAgIkByb2xsZG93bi9iaW5kaW5nLWxpbnV4LXg2NC1nbnUiOiBbIkByb2xsZG93bi9iaW5kaW5nLWxpbnV4LXg2NC1nbnVAMS4wLjEiLCAiIiwgeyAib3MiOiAibGludXgiLCAiY3B1IjogIng2NCIgfSwgInNoYTUxMi1YQUJWbUdwOVRnMFdzcFRWdndkdVRjNGZwcXk2Sm5BVXJTUWU2T3V5cUQvMDNuSTdyME85T1dVa01Jd0ZyaktBSXFvbHZxb0E0WnJKcHBnd0UwR3htdz09Il0sCgogICAgIkByb2xsZG93bi9iaW5kaW5nLWxpbnV4LXg2NC1tdXNsIjogWyJAcm9sbGRvd24vYmluZGluZy1saW51eC14NjQtbXVzbEAxLjAuMSIsICIiLCB7ICJvcyI6ICJsaW51eCIsICJjcHUiOiAieDY0IiB9LCAic2hhNTEyLWJWNGZ6c3d1elZjS0Q5MG8vVk02UXFLeG54bERxMGcyQklTRExOVm14cm5ocHYxRERieVBoQ0lqWWZ2ellMVitNdmtLS25RdDJRNkFPODZTRUJVTFVRPT0iXSwKCiAgICAiQHJvbGxkb3duL2JpbmRpbmctb3Blbmhhcm1vbnktYXJtNjQiOiBbIkByb2xsZG93bi9iaW5kaW5nLW9wZW5oYXJtb255LWFybTY0QDEuMC4xIiwgIiIsIHsgIm9zIjogIm5vbmUiLCAiY3B1IjogImFybTY0IiB9LCAic2hhNTEyLS9NaDBaaHEzT1A3ZlZzMGtjUUhaUDZsWkV0aE1HVGFTZjhVQlFZU0ZFWkRXR1hYbEVDK25KNkVxZW5hSzJ0NExCWE1lM0ErSy9HMkJWWFhkdE9yNFBRPT0iXSwKCiAgICAiQHJvbGxkb3duL2JpbmRpbmctd2FzbTMyLXdhc2kiOiBbIkByb2xsZG93bi9iaW5kaW5nLXdhc20zMi13YXNpQDEuMC4xIiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIkBlbW5hcGkvY29yZSI6ICIxLjEwLjAiLCAiQGVtbmFwaS9ydW50aW1lIjogIjEuMTAuMCIsICJAbmFwaS1ycy93YXNtLXJ1bnRpbWUiOiAiXjEuMS40IiB9LCAiY3B1IjogIm5vbmUiIH0sICJzaGE1MTItKzF4YzlYNDVsOHVmc0JBbTZHanZ4MnFEUklZOWxUVnQwY2dXTmNKKzFnZGhYdmtieGVQQTYweVJUd1NUdVhMMDlDTWh5Sm1qcFY3RTNOb3l4YnFGUVE9PSJdLAoKICAgICJAcm9sbGRvd24vYmluZGluZy13aW4zMi1hcm02NC1tc3ZjIjogWyJAcm9sbGRvd24vYmluZGluZy13aW4zMi1hcm02NC1tc3ZjQDEuMC4xIiwgIiIsIHsgIm9zIjogIndpbjMyIiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi0xRCtVcVpkZm51UitKeTFHZ01Kd2k4NWJENDBIMjF1Tm1PUFJXUWh3NG9SU3VvbFovQjVyaXhaNDVESzJLWE9UQ3ZtVkNlY2F1V2dFaGJ3OGJJN3RPdz09Il0sCgogICAgIkByb2xsZG93bi9iaW5kaW5nLXdpbjMyLXg2NC1tc3ZjIjogWyJAcm9sbGRvd24vYmluZGluZy13aW4zMi14NjQtbXN2Y0AxLjAuMSIsICIiLCB7ICJvcyI6ICJ3aW4zMiIsICJjcHUiOiAieDY0IiB9LCAic2hhNTEyLUlOQXljYVd1aGxPSzN3azRtUkhHc2Rnd1lXbWQ5Y0NoZFBkRTlid1dteTZybjlWcVZOWU5GR2hPZFhyb2ZYVXh3SEluY1NpUE5iOHRObThrbkRWSWVRPT0iXSwKCiAgICAiQHJvbGxkb3duL3BsdWdpbnV0aWxzIjogWyJAcm9sbGRvd24vcGx1Z2ludXRpbHNAMS4wLjEiLCAiIiwge30sICJzaGE1MTItMmo5Ykd0NUpoOGhqK3ZQdGd6UHRsNzJqMHlSeEhBeXVtb282VE5mQWpzTEIwNFV0cFN2UGJQY0RjQk14ejduKzlDWUIwYzFHeFFGeFlSZzJqaW1xR3c9PSJdLAoKICAgICJAc3RhbmRhcmQtc2NoZW1hL3NwZWMiOiBbIkBzdGFuZGFyZC1zY2hlbWEvc3BlY0AxLjEuMCIsICIiLCB7fSwgInNoYTUxMi1sMmFGeTVqQUxobmlHNUhncXJENmpYTGkvclVXckt2cU4vcUp4NnlvSnNnS2hibFZkK2lxcVU0UkNYYXZtL2pQaXR5RG81VEN2S01ucGpLbk9yaXkwdz09Il0sCgogICAgIkBzd2MvaGVscGVycyI6IFsiQHN3Yy9oZWxwZXJzQDAuNS4xNSIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJ0c2xpYiI6ICJeMi44LjAiIH0gfSwgInNoYTUxMi1KUTVUdU1pNDVPd2k0L0JJTUFKQm9TUW9PSnUxMm9Pay9nQURxbGNVTDlKRWRIQjh2eWpVU3N4cWVOWG5tWEhqWUtNaTJXY1l0ZXpHRUVocVVJL0UyZz09Il0sCgogICAgIkB0YWJieV9haS9oaWpyaS1jb252ZXJ0ZXIiOiBbIkB0YWJieV9haS9oaWpyaS1jb252ZXJ0ZXJAMS4wLjUiLCAiIiwge30sICJzaGE1MTItcjViQ2xLcmNJdXNEb28wNDlkU0w4Q2F3bkhSNm1SZER3aGxRdUlnWlJOdHk2OHEweDhrM0xmMUJ0UEFNeFJmL0dnbkhCbklPNHVqZDMrR1FkTFd6eFE9PSJdLAoKICAgICJAdGFpbHdpbmRjc3Mvbm9kZSI6IFsiQHRhaWx3aW5kY3NzL25vZGVANC4zLjAiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiQGpyaWRnZXdlbGwvcmVtYXBwaW5nIjogIl4yLjMuNSIsICJlbmhhbmNlZC1yZXNvbHZlIjogIl41LjIxLjAiLCAiaml0aSI6ICJeMi42LjEiLCAibGlnaHRuaW5nY3NzIjogIjEuMzIuMCIsICJtYWdpYy1zdHJpbmciOiAiXjAuMzAuMjEiLCAic291cmNlLW1hcC1qcyI6ICJeMS4yLjEiLCAidGFpbHdpbmRjc3MiOiAiNC4zLjAiIH0gfSwgInNoYTUxMi1hRmI0Z1VoRk9nZGg5QVhvNEl6QkVPekJra0F4bTlWaWd3REpuTUlZdjNsY2ZYQ0pWZXNOZmJFYUJsNEJOZ1ZSeWlkOTJBbWR2aXF3QlVCUktTZVkzZz09Il0sCgogICAgIkB0YWlsd2luZGNzcy9veGlkZSI6IFsiQHRhaWx3aW5kY3NzL294aWRlQDQuMy4wIiwgIiIsIHsgIm9wdGlvbmFsRGVwZW5kZW5jaWVzIjogeyAiQHRhaWx3aW5kY3NzL294aWRlLWFuZHJvaWQtYXJtNjQiOiAiNC4zLjAiLCAiQHRhaWx3aW5kY3NzL294aWRlLWRhcndpbi1hcm02NCI6ICI0LjMuMCIsICJAdGFpbHdpbmRjc3Mvb3hpZGUtZGFyd2luLXg2NCI6ICI0LjMuMCIsICJAdGFpbHdpbmRjc3Mvb3hpZGUtZnJlZWJzZC14NjQiOiAiNC4zLjAiLCAiQHRhaWx3aW5kY3NzL294aWRlLWxpbnV4LWFybS1nbnVlYWJpaGYiOiAiNC4zLjAiLCAiQHRhaWx3aW5kY3NzL294aWRlLWxpbnV4LWFybTY0LWdudSI6ICI0LjMuMCIsICJAdGFpbHdpbmRjc3Mvb3hpZGUtbGludXgtYXJtNjQtbXVzbCI6ICI0LjMuMCIsICJAdGFpbHdpbmRjc3Mvb3hpZGUtbGludXgteDY0LWdudSI6ICI0LjMuMCIsICJAdGFpbHdpbmRjc3Mvb3hpZGUtbGludXgteDY0LW11c2wiOiAiNC4zLjAiLCAiQHRhaWx3aW5kY3NzL294aWRlLXdhc20zMi13YXNpIjogIjQuMy4wIiwgIkB0YWlsd2luZGNzcy9veGlkZS13aW4zMi1hcm02NC1tc3ZjIjogIjQuMy4wIiwgIkB0YWlsd2luZGNzcy9veGlkZS13aW4zMi14NjQtbXN2YyI6ICI0LjMuMCIgfSB9LCAic2hhNTEyLUY3SFpHQmVOOUkwL0F1dUpTNVB3Y0Q4eGF5eDVyaTVHaGpZVURCRVZZVWtleHlBL2dpd2JETmpSVnJ4U2V6RTNUMjUwT1UySy93cC9sdFd4M1VPZWZnPT0iXSwKCiAgICAiQHRhaWx3aW5kY3NzL294aWRlLWFuZHJvaWQtYXJtNjQiOiBbIkB0YWlsd2luZGNzcy9veGlkZS1hbmRyb2lkLWFybTY0QDQuMy4wIiwgIiIsIHsgIm9zIjogImFuZHJvaWQiLCAiY3B1IjogImFybTY0IiB9LCAic2hhNTEyLVRKUGlxNjd0S2xMdU9iUDZSa3d2VkdEb3hDTUJWdERnS2tMZmEvdXlqNy9GeXh2UXdIUytVT25WclhYZ2JFc2ZVYU1naVZ2QzRLYkpuUnIyNmhvNE5nPT0iXSwKCiAgICAiQHRhaWx3aW5kY3NzL294aWRlLWRhcndpbi1hcm02NCI6IFsiQHRhaWx3aW5kY3NzL294aWRlLWRhcndpbi1hcm02NEA0LjMuMCIsICIiLCB7ICJvcyI6ICJkYXJ3aW4iLCAiY3B1IjogImFybTY0IiB9LCAic2hhNTEyLW9NTi9XWlJiK1NPMzdCbVVFbEVnZUVXdVU4RS9IWFJraU9EeEp4TGUxVVRIVlhMcmRWU2dmYUpWN3BTbGhSR01TT2lYTHV4VElqZnNGM3dZdno4Y2dRPT0iXSwKCiAgICAiQHRhaWx3aW5kY3NzL294aWRlLWRhcndpbi14NjQiOiBbIkB0YWlsd2luZGNzcy9veGlkZS1kYXJ3aW4teDY0QDQuMy4wIiwgIiIsIHsgIm9zIjogImRhcndpbiIsICJjcHUiOiAieDY0IiB9LCAic2hhNTEyLU42Q1VtdTRhNmJLVkFEZnc3N3AraXc2WWQ5UTNPQmhlMHZlYURYK1FhemZ1VllsUXNIZkRneEJyc2pRL0lXK3p5d0w4bVRyTmQwU2RKVC96Z3R2TWRBPT0iXSwKCiAgICAiQHRhaWx3aW5kY3NzL294aWRlLWZyZWVic2QteDY0IjogWyJAdGFpbHdpbmRjc3Mvb3hpZGUtZnJlZWJzZC14NjRANC4zLjAiLCAiIiwgeyAib3MiOiAiZnJlZWJzZCIsICJjcHUiOiAieDY0IiB9LCAic2hhNTEyLXpETDVoQmtRZEg1QzZNcHFiSzNnUUFnUDgwdHNNd1NJMjZ2ak96akp0TkNNVW8wbEZnT0l0ekhLQkl1cE9aTlF4dDNvdVBIN1JQaHZOaGlUZkNlNUNRPT0iXSwKCiAgICAiQHRhaWx3aW5kY3NzL294aWRlLWxpbnV4LWFybS1nbnVlYWJpaGYiOiBbIkB0YWlsd2luZGNzcy9veGlkZS1saW51eC1hcm0tZ251ZWFiaWhmQDQuMy4wIiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJhcm0iIH0sICJzaGE1MTItUjA2SGROaTdBN09Fb01zZjZkNHRqWjcxUkNXblpRUEhqMm1ub3RTRlVSak5MZEJDK2NJZ1hRN2w4MUNxZW9pUWZ0amY2T09ibHhYTUluTWdOMlZ6TUE9PSJdLAoKICAgICJAdGFpbHdpbmRjc3Mvb3hpZGUtbGludXgtYXJtNjQtZ251IjogWyJAdGFpbHdpbmRjc3Mvb3hpZGUtbGludXgtYXJtNjQtZ251QDQuMy4wIiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi1xVEpIRUxYOGpldGpoUlFIQ0xpbGtWTG15YnB6TlFBdGFJL2dhb1ZvaWRuL3VmYk5EYkFvOEtsSzJKK3lQb2M4d1F4dkR4Q21oLzVscjhuQzErbFRiZz09Il0sCgogICAgIkB0YWlsd2luZGNzcy9veGlkZS1saW51eC1hcm02NC1tdXNsIjogWyJAdGFpbHdpbmRjc3Mvb3hpZGUtbGludXgtYXJtNjQtbXVzbEA0LjMuMCIsICIiLCB7ICJvcyI6ICJsaW51eCIsICJjcHUiOiAiYXJtNjQiIH0sICJzaGE1MTItWjZzdWtpUXNuZ25XTytsMzlYNHBQYmlXVDgxSUMrUExLRitQSHhJbHlaYkdOYjlNT0RmWWxYRVZsRnZlajVCT1pJbldYMDFrVnl6ZUx2SHNYaGZjelE9PSJdLAoKICAgICJAdGFpbHdpbmRjc3Mvb3hpZGUtbGludXgteDY0LWdudSI6IFsiQHRhaWx3aW5kY3NzL294aWRlLWxpbnV4LXg2NC1nbnVANC4zLjAiLCAiIiwgeyAib3MiOiAibGludXgiLCAiY3B1IjogIng2NCIgfSwgInNoYTUxMi1EUk5kUVJwU0d6UkdmQVJWdVZreHZNOFExMm5oMTlsNEJGL0c3ekdBMW9lKzl3Y0M2c2FGQkhUSVNycEljS3poaVh0U3JsU3JsdUNmdk11bGVkb0NUUT09Il0sCgogICAgIkB0YWlsd2luZGNzcy9veGlkZS1saW51eC14NjQtbXVzbCI6IFsiQHRhaWx3aW5kY3NzL294aWRlLWxpbnV4LXg2NC1tdXNsQDQuMy4wIiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJ4NjQiIH0sICJzaGE1MTItWjBJQURiRG84Ymg2STdoMklRTXg2MDFBZFhCTGZGcEVkVW90ZnQ4NmV2ZC84WlBmbFplOUNPUE84UTF2dytwZkxXSVVvOXpOL0pHWnZ3dUFKcWR1cWc9PSJdLAoKICAgICJAdGFpbHdpbmRjc3Mvb3hpZGUtd2FzbTMyLXdhc2kiOiBbIkB0YWlsd2luZGNzcy9veGlkZS13YXNtMzItd2FzaUA0LjMuMCIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAZW1uYXBpL2NvcmUiOiAiXjEuMTAuMCIsICJAZW1uYXBpL3J1bnRpbWUiOiAiXjEuMTAuMCIsICJAZW1uYXBpL3dhc2ktdGhyZWFkcyI6ICJeMS4yLjEiLCAiQG5hcGktcnMvd2FzbS1ydW50aW1lIjogIl4xLjEuNCIsICJAdHlieXMvd2FzbS11dGlsIjogIl4wLjEwLjEiLCAidHNsaWIiOiAiXjIuOC4xIiB9LCAiY3B1IjogIm5vbmUiIH0sICJzaGE1MTItSE5aR09VeEVtRWxrc1lSN1M2c0M1alRlTkdwb2JBc3k5dTdHdTBBc2tKOC8yMEZSOUdxZWJVeUIrSEJjVS9heDZCSHVpdUppK09kYTRCK1lYNkgxeUE9PSJdLAoKICAgICJAdGFpbHdpbmRjc3Mvb3hpZGUtd2luMzItYXJtNjQtbXN2YyI6IFsiQHRhaWx3aW5kY3NzL294aWRlLXdpbjMyLWFybTY0LW1zdmNANC4zLjAiLCAiIiwgeyAib3MiOiAid2luMzIiLCAiY3B1IjogImFybTY0IiB9LCAic2hhNTEyLVBlK1JQVlRpMVQrcXltdXVScGNkdndTVlpqbmxsL2Y3bjhnQnhNTWgzeExUY3RNREtxcGRmR2ltYk15aW9xdExoVVlaeGRKOXdHTmhWN01LSHZnWnNRPT0iXSwKCiAgICAiQHRhaWx3aW5kY3NzL294aWRlLXdpbjMyLXg2NC1tc3ZjIjogWyJAdGFpbHdpbmRjc3Mvb3hpZGUtd2luMzIteDY0LW1zdmNANC4zLjAiLCAiIiwgeyAib3MiOiAid2luMzIiLCAiY3B1IjogIng2NCIgfSwgInNoYTUxMi1NdnJmMmtYVy95ZVcvT1RlelpsQ0dPaXJYUmNVdUxJQngvNVkxMkJhUE03d0pvcnlHNmRmUy9OSkw4YUJQcXRURXgvVm00VDR2S3pGVWNLRFQrVEtVQT09Il0sCgogICAgIkB0YWlsd2luZGNzcy9wb3N0Y3NzIjogWyJAdGFpbHdpbmRjc3MvcG9zdGNzc0A0LjMuMCIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAYWxsb2MvcXVpY2stbHJ1IjogIl41LjIuMCIsICJAdGFpbHdpbmRjc3Mvbm9kZSI6ICI0LjMuMCIsICJAdGFpbHdpbmRjc3Mvb3hpZGUiOiAiNC4zLjAiLCAicG9zdGNzcyI6ICJeOC41LjEwIiwgInRhaWx3aW5kY3NzIjogIjQuMy4wIiB9IH0sICJzaGE1MTItSm0wNVRqeCs5eUNMR3Y1cXcxYys4NFBzZHM4TW55ckVRWUNCK0ZGazJsZ0dpVWpsUnFkeGtlNG1WVHVZcmoyeG5WWnFLaW0yQXByNXlTdVFSWUF3L3c9PSJdLAoKICAgICJAdHlieXMvd2FzbS11dGlsIjogWyJAdHlieXMvd2FzbS11dGlsQDAuMTAuMiIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJ0c2xpYiI6ICJeMi40LjAiIH0gfSwgInNoYTUxMi1Sb0J2SjJYMHd1S2xXRklqcndmZkd3MUlxWkhLUXF6SWNoS2FhZFpaZm5OcHNBWXAybU0waDM2SnRQQ2pOREFIR2dZZXovMTV1TUJwZkd3Y2hoaU1nZz09Il0sCgogICAgIkB0eXBlcy9jaGFpIjogWyJAdHlwZXMvY2hhaUA1LjIuMyIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAdHlwZXMvZGVlcC1lcWwiOiAiKiIsICJhc3NlcnRpb24tZXJyb3IiOiAiXjIuMC4xIiB9IH0sICJzaGE1MTItTXc1NThvZUE5ZkZidjY1L3k0bUh0WERzOWJQbkZNWkFML2p4ZFBGVXBPSEhJWFg5MW1jZ0VIYlM1TGFocitwd1pGUjhBN0dRbGVSV2VJNmNHRkMyVUE9PSJdLAoKICAgICJAdHlwZXMvZGVlcC1lcWwiOiBbIkB0eXBlcy9kZWVwLWVxbEA0LjAuMiIsICIiLCB7fSwgInNoYTUxMi1jOWg5ZFZWTWlnTVBjNGJ3VHZDNWR4cXRxSlp3UVBlUHNXalBscFNPbm9qYm9yNnBHcWRrNTQxbGZBN0FxRlFyNXBCMUJSZHEwanVZOWRiODFCd3lGdz09Il0sCgogICAgIkB0eXBlcy9lc3RyZWUiOiBbIkB0eXBlcy9lc3RyZWVAMS4wLjkiLCAiIiwge30sICJzaGE1MTItR2hkUGd5MWVsNC9JbVAwNVgwNVV3NGN3Mi9NOTNCQ1VtbkV2V1pOU3RsQ3pFS01FNEZraytZcG9BNU9pSE5RbW9TN0NhZmI4WGEzUHlhOG0xUXJ6ZWc9PSJdLAoKICAgICJAdHlwZXMvbm9kZSI6IFsiQHR5cGVzL25vZGVAMjIuMTkuMTkiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAidW5kaWNpLXR5cGVzIjogIn42LjIxLjAiIH0gfSwgInNoYTUxMi1keWgveE8yRmg1YllyZldhYXFHclJRUUdrTmRtWXc2QW1hQVV2WWVVTU5UV1F0dmI3OTZpa0xkbVRjaFJtT2xPaUlKMVREWGZXZ1Z4MVFrVWxRNkhldz09Il0sCgogICAgIkB0eXBlcy9yZWFjdCI6IFsiQHR5cGVzL3JlYWN0QDE5LjIuMTUiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiY3NzdHlwZSI6ICJeMy4yLjIiIH0gfSwgInNoYTUxMi1lUndjR05IdmUrRThxdEVRU1NSbDZ1cmgrckZvcDR2OGdtNk84ckd2MjVDb2RidkZkTGpBMXZWUTFLa2lGRTB3MFVQT25iOHREaUZLTDVscDBydFk1UT09Il0sCgogICAgIkB0eXBlcy9yZWFjdC1kb20iOiBbIkB0eXBlcy9yZWFjdC1kb21AMTkuMi4zIiwgIiIsIHsgInBlZXJEZXBlbmRlbmNpZXMiOiB7ICJAdHlwZXMvcmVhY3QiOiAiXjE5LjIuMCIgfSB9LCAic2hhNTEyLWpwMkwvZVk2Zm4rS2dWVlFBT3FZSXRiRjBWWS9ZQXBlNU16MkYwYXlrU084Z3gzMWJZQ1p5dlNlWXhDSEt2ekhHNWVaamMrenlhUzVCckJXeWEyK2tRPT0iXSwKCiAgICAiQHZpdGVzdC9leHBlY3QiOiBbIkB2aXRlc3QvZXhwZWN0QDQuMS42IiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIkBzdGFuZGFyZC1zY2hlbWEvc3BlYyI6ICJeMS4xLjAiLCAiQHR5cGVzL2NoYWkiOiAiXjUuMi4yIiwgIkB2aXRlc3Qvc3B5IjogIjQuMS42IiwgIkB2aXRlc3QvdXRpbHMiOiAiNC4xLjYiLCAiY2hhaSI6ICJeNi4yLjIiLCAidGlueXJhaW5ib3ciOiAiXjMuMS4wIiB9IH0sICJzaGE1MTItN0VIRHF1UHRoQUxTVjBqaGhqZ0VXOEZYYXZpTXg3clNxdThXNm9xQ29BdU9oS292ODE0UDk5UURWMXB4TUEzUVB2MjFZdWR2Sm5nSWhqck5JNG9wTGc9PSJdLAoKICAgICJAdml0ZXN0L21vY2tlciI6IFsiQHZpdGVzdC9tb2NrZXJANC4xLjYiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiQHZpdGVzdC9zcHkiOiAiNC4xLjYiLCAiZXN0cmVlLXdhbGtlciI6ICJeMy4wLjMiLCAibWFnaWMtc3RyaW5nIjogIl4wLjMwLjIxIiB9LCAicGVlckRlcGVuZGVuY2llcyI6IHsgIm1zdyI6ICJeMi40LjkiLCAidml0ZSI6ICJeNi4wLjAgfHwgXjcuMC4wIHx8IF44LjAuMCIgfSwgIm9wdGlvbmFsUGVlcnMiOiBbIm1zdyIsICJ2aXRlIl0gfSwgInNoYTUxMi1NQ0ZjNjNjek1qRUluT2xjWTJjcFFDdkNOK0tnYkFuKzYweHU5Y01nUDRzS2FMQzVKTkFLdzdKSDhRZEFub0FDODhoVzFJaVNOWitHZ1ZYbE4xVWNNUT09Il0sCgogICAgIkB2aXRlc3QvcHJldHR5LWZvcm1hdCI6IFsiQHZpdGVzdC9wcmV0dHktZm9ybWF0QDQuMS42IiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgInRpbnlyYWluYm93IjogIl4zLjEuMCIgfSB9LCAic2hhNTEyLWg1U3hEL0l6TmhaWW5yU1pSc1VaUUlDK3ZEMEdZOGNVdnEwaXdzbWtGS2l4UkNLTExXcUNYYS9GSVE0UzFSK3NJK1BHb29qa0hzZE5yYlppTTlRcGd3PT0iXSwKCiAgICAiQHZpdGVzdC9ydW5uZXIiOiBbIkB2aXRlc3QvcnVubmVyQDQuMS42IiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIkB2aXRlc3QvdXRpbHMiOiAiNC4xLjYiLCAicGF0aGUiOiAiXjIuMC4zIiB9IH0sICJzaGE1MTItbk9QQ21uMit5RDBaTm1LZHNYR3YvVXhNTVdiTXVLZUQ2R3lZbmNOd2RrWUR4cFF2clBTS1lqMnJXdURqQzJZNGI2dzZoamlwNWRCS0Z6RVV1WmUzdkE9PSJdLAoKICAgICJAdml0ZXN0L3NuYXBzaG90IjogWyJAdml0ZXN0L3NuYXBzaG90QDQuMS42IiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIkB2aXRlc3QvcHJldHR5LWZvcm1hdCI6ICI0LjEuNiIsICJAdml0ZXN0L3V0aWxzIjogIjQuMS42IiwgIm1hZ2ljLXN0cmluZyI6ICJeMC4zMC4yMSIsICJwYXRoZSI6ICJeMi4wLjMiIH0gfSwgInNoYTUxMi1ZaHNkRTZ4QVZmVERtemp4TDJaRFV2amorWnNneU9LZStUZFF6cWtENzJ3SU9tSGthOE51R1E2TnBUTlp2OUQyWjYzZmJ3V0tKUGVWcEV3NEVRZ1l4dz09Il0sCgogICAgIkB2aXRlc3Qvc3B5IjogWyJAdml0ZXN0L3NweUA0LjEuNiIsICIiLCB7fSwgInNoYTUxMi1KRkt4TXg2dWRod0toL0xkbzI3MGUxN1FYNzEwdmd1bk1rdVBBdlhqSFN2QzZvcUxXQUhoVmhqZy9JNzFxMHUwQ0JTRXJJT0RWMUtqdjBGUU5TV2pkZz09Il0sCgogICAgIkB2aXRlc3QvdXRpbHMiOiBbIkB2aXRlc3QvdXRpbHNANC4xLjYiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiQHZpdGVzdC9wcmV0dHktZm9ybWF0IjogIjQuMS42IiwgImNvbnZlcnQtc291cmNlLW1hcCI6ICJeMi4wLjAiLCAidGlueXJhaW5ib3ciOiAiXjMuMS4wIiB9IH0sICJzaGE1MTItRnhJWStVODFSM0xHS0N4YUhIRlJRNStnNi9pUmdHTG1lSFdkcDJBbWo0bGpRUnJFSVdIbVp5RGZEWUJSWmxweXFBN3FLeHRTOUREMWRoazhSblJJVlE9PSJdLAoKICAgICJhbnNpLXJlZ2V4IjogWyJhbnNpLXJlZ2V4QDUuMC4xIiwgIiIsIHt9LCAic2hhNTEyLXF1SlFYbFRTVUdMMkxIOVNVWG84VndzWTRzb2FuaGdvNkxOU204NEUxTEJjRThzM08wd3BkaVJ6eVI5ei9aWkpNbE1XdjM3cU9PYjlwZEpsTVVFS0ZRPT0iXSwKCiAgICAiYW5zaS1zdHlsZXMiOiBbImFuc2ktc3R5bGVzQDQuMy4wIiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgImNvbG9yLWNvbnZlcnQiOiAiXjIuMC4xIiB9IH0sICJzaGE1MTItemJCOXJDSkFUMXJiamlWRGIyaHFLRkhOWUx4Z3RrOE5VUnhaM0lad0QzRjZOdHhiWFpRQ25uU2kxTGt4K0lEb2hkUGxGcDIyMndWQUxJaGVaSlFTRWc9PSJdLAoKICAgICJhc3NlcnRpb24tZXJyb3IiOiBbImFzc2VydGlvbi1lcnJvckAyLjAuMSIsICIiLCB7fSwgInNoYTUxMi1Jemk4UlFjZmZxQ2VOVmdGaWdLbGkxc3NrbElicEhuQ1ljNkFrblhHWW9CNmdySnF5ZWJ5N2p2MTJKVVFnbVRBbklEbmJjazF1eGtzVDRkek4zUFdCQT09Il0sCgogICAgImJhc2VsaW5lLWJyb3dzZXItbWFwcGluZyI6IFsiYmFzZWxpbmUtYnJvd3Nlci1tYXBwaW5nQDIuMTAuMzEiLCAiIiwgeyAiYmluIjogeyAiYmFzZWxpbmUtYnJvd3Nlci1tYXBwaW5nIjogImRpc3QvY2xpLmNqcyIgfSB9LCAic2hhNTEyLU11allPM2VQNzJ1dm1TRTBpNHdsdHNvZFJmSXBaQVRQM2p2elJOUkdHeGd6SWQ3YVZvY1ZKSlYzbmYwMXFuenpLRkd4UVZDOWJwV3hsNWNqeFRyLzdRPT0iXSwKCiAgICAiY2FuaXVzZS1saXRlIjogWyJjYW5pdXNlLWxpdGVAMS4wLjMwMDAxNzkzIiwgIiIsIHt9LCAic2hhNTEyLWl3U3NZV2FDT29oMjZjVjhOd05SVmlIbHJmVXZZc0hEZlJWY2J0bXcwS2c2UEpJWlpYd01rajE0NDJGWUxCR2tlVWYxanVBc1UzRFRmeFc1NzltclBBPT0iXSwKCiAgICAiY2hhaSI6IFsiY2hhaUA2LjIuMiIsICIiLCB7fSwgInNoYTUxMi1OVVBSbHVPZk9pVEtCS3ZXUHRTRDRQaEZ2V0NxT2kwQkdTdE5XczU3WDlqczdYR1RwclNtRm96NUYwdFdoUjRXUGpOZVI5alhxZEM3L1VwU0pUbmxSZz09Il0sCgogICAgImNoYWxrIjogWyJjaGFsa0A0LjEuMiIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJhbnNpLXN0eWxlcyI6ICJeNC4xLjAiLCAic3VwcG9ydHMtY29sb3IiOiAiXjcuMS4wIiB9IH0sICJzaGE1MTItb0tuYmhGeVJJWHBVdWV6OGlCTW15RWE0bmJqNElPUXl1aGMvd3k5a1k3L1dWUGN3SU85VkE2NjhQdThSa083KzBHNzZTTFJPZXl3OUNwUTA2MWk0bUE9PSJdLAoKICAgICJjbGllbnQtb25seSI6IFsiY2xpZW50LW9ubHlAMC4wLjEiLCAiIiwge30sICJzaGE1MTItSVYzT3UwalNNelpyZDNwWjQ4bkxrVDlEQTdBZzFwblB6YWlRaHBXN2MzUmJjcXF6dnp6VnUrTDhnZnFNcC84SU0yTVF0U2lxYUN4cnJjZnU4SThyTUE9PSJdLAoKICAgICJjbGl1aSI6IFsiY2xpdWlAOC4wLjEiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAic3RyaW5nLXdpZHRoIjogIl40LjIuMCIsICJzdHJpcC1hbnNpIjogIl42LjAuMSIsICJ3cmFwLWFuc2kiOiAiXjcuMC4wIiB9IH0sICJzaGE1MTItQlNlTm55dXM3NUM0Ly9OUTlnUXQxL2NzVFh5by84U2IrYWZMQWt6QXB0RnVNc29kOUhGb2tHTnVkWnBpL29RVjczaG5WSytzUis1UFZSTWQrRHI3WVE9PSJdLAoKICAgICJjbHN4IjogWyJjbHN4QDIuMS4xIiwgIiIsIHt9LCAic2hhNTEyLWVZbTBRV0J0VXJCV1pXRzBkMzg2T0dBdzE2Wjk5NVBpT1ZvMkI3YmpXU2JIZWRHbDVlMFpXYXE2NWtPR2dVU05lc0VJRGtCOUlTYlRnL0pLOWRoQ1pBPT0iXSwKCiAgICAiY29sb3ItY29udmVydCI6IFsiY29sb3ItY29udmVydEAyLjAuMSIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJjb2xvci1uYW1lIjogIn4xLjEuNCIgfSB9LCAic2hhNTEyLVJSRUNQc2o3aXUveGI1b0tZY3NGSFNwcEZObnNqLzUyT1ZUUktiNHpQNW9uWHdWRjN6Vm1tVG9OY09mR0MrQ1JEcGZLL1U1ODRmTWczOFpIQ2FFbEtRPT0iXSwKCiAgICAiY29sb3ItbmFtZSI6IFsiY29sb3ItbmFtZUAxLjEuNCIsICIiLCB7fSwgInNoYTUxMi1kT3krM0F1VzNhMndOYlpISXVNWnBUY2dqR3VMVS91QkwvdWJjWkY5T1hiRG84ZmY0Tzh5VnA1QmYwZWZTOHVFb1lvNXE0Rng3ZFk5T2dRR1hnQXNRQT09Il0sCgogICAgImNvbmN1cnJlbnRseSI6IFsiY29uY3VycmVudGx5QDkuMi4xIiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgImNoYWxrIjogIjQuMS4yIiwgInJ4anMiOiAiNy44LjIiLCAic2hlbGwtcXVvdGUiOiAiMS44LjMiLCAic3VwcG9ydHMtY29sb3IiOiAiOC4xLjEiLCAidHJlZS1raWxsIjogIjEuMi4yIiwgInlhcmdzIjogIjE3LjcuMiIgfSwgImJpbiI6IHsgImNvbmMiOiAiZGlzdC9iaW4vY29uY3VycmVudGx5LmpzIiwgImNvbmN1cnJlbnRseSI6ICJkaXN0L2Jpbi9jb25jdXJyZW50bHkuanMiIH0gfSwgInNoYTUxMi1mc2ZyTzBNeFY2NFpub3k4L2wxdlZJampIYTI5U1p5eXFQZ1FCd2hpRGNhVzh3SmMyVzNYV1ZPR3g0TTNvSkJudi96ZFVaSUlwMWdEZVM5OEd6UDhOZz09Il0sCgogICAgImNvbnZlcnQtc291cmNlLW1hcCI6IFsiY29udmVydC1zb3VyY2UtbWFwQDIuMC4wIiwgIiIsIHt9LCAic2hhNTEyLUt2cDQ1OUhyVjJGRUoxQ0FzaTFLdStNWTNrYXNIMTlURnlrVHoyeFdtTWVxNmJrMk5VM1hYdmZKK1E2MW0weGt0V3d0KzFIU1lmM0pac1RtczNhUkpnPT0iXSwKCiAgICAiY3NzdHlwZSI6IFsiY3NzdHlwZUAzLjIuMyIsICIiLCB7fSwgInNoYTUxMi16MUhHS2NZeTJ4QThBR1Fmd3JuMFBBeStQQjdYL0dTajNVVkpXOXFLeW40M3hXYStnbDVuWG1VNHFxTE1SeldWTEZDOEt1c1VYOFQvMGtDaU9ZcEFJUT09Il0sCgogICAgImRhdGUtZm5zIjogWyJkYXRlLWZuc0A0LjIuMSIsICIiLCB7fSwgInNoYTUxMi0zN1JoU2R4YUcxc3VlbjZWREN6YTZyTnJRZm9veVFoNTdIRlZQd1FHRXEyUVdsaVZMelBRWjhPYTAxN3dlT3UrSFpDbnpJN04zUGYvd3lvQktmRXFyQT09Il0sCgogICAgImRhdGUtZm5zLWphbGFsaSI6IFsiZGF0ZS1mbnMtamFsYWxpQDQuMS4wLTAiLCAiIiwge30sICJzaGE1MTItaFRJUC96K3QrcUt3QkRjbW1zbm1qV1RkdXhDZys1S2ZkcVdRdmIyWC84Qzkra25ZWTZlcE4vcGZ4ZER1eVZsU1ZlRnowc001ZUVmd0lVUTcwVTRja2c9PSJdLAoKICAgICJkZXRlY3QtbGliYyI6IFsiZGV0ZWN0LWxpYmNAMi4xLjIiLCAiIiwge30sICJzaGE1MTItQnRqMkJPT084M28zV3lINTllOE1nWHN4RVFWY2Fya1VPcEVZcnViQjB1cnduTjEweVEzNjRyc2lCeVUxMW5abHFXWVptMDVpL29mN2lvNG16aWhCdFE9PSJdLAoKICAgICJlbW9qaS1yZWdleCI6IFsiZW1vamktcmVnZXhAOC4wLjAiLCAiIiwge30sICJzaGE1MTItTVNqWXpjV05PQTBld0FIcHowTXhwWUZ2d2c2eWp5MU5HM3h0ZW9xejY0NFZDby9SUGducjEvR0d0K2ljM2lKVHpROEV1M1RkTTE0U2F3blZVbUdFNkE9PSJdLAoKICAgICJlbmhhbmNlZC1yZXNvbHZlIjogWyJlbmhhbmNlZC1yZXNvbHZlQDUuMjEuNSIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJncmFjZWZ1bC1mcyI6ICJeNC4yLjQiLCAidGFwYWJsZSI6ICJeMi4zLjMiIH0gfSwgInNoYTUxMi1tTENOYnJRbGkxMUsxeVNVbXVOdDRaVUIzT3BHSURxNHEydlRCVGY1Y0wybHBzUmpJOVFLcVNEMG5kalc4Rnl2Y1cvSmo0NmdNZTlzeXlIQXN2TWEvQT09Il0sCgogICAgImVzLW1vZHVsZS1sZXhlciI6IFsiZXMtbW9kdWxlLWxleGVyQDIuMS4wIiwgIiIsIHt9LCAic2hhNTEyLW4yN3pUWU1qWXUxYWo0TWpDV3pTUDdHOXI3NXV0c2FvYzhtNjF3ZUsrVzhKTUJHR1F5YmQ0M0dzdENYWjNXTm1TRnRHVDl3aTU5cVFUVzZtaFRSNUxRPT0iXSwKCiAgICAiZXNjYWxhZGUiOiBbImVzY2FsYWRlQDMuMi4wIiwgIiIsIHt9LCAic2hhNTEyLVdVajJxbHhhUXRPNGc2UHE1YzI5R1RjV0dEeWQ4aXRMOHpUbGlwZ0VDejNKZXNBaWlPS290ZDhKVTZvdEIzUEFDZ0c2eGtKVXlWaGJvTVMrYmplL2pBPT0iXSwKCiAgICAiZXN0cmVlLXdhbGtlciI6IFsiZXN0cmVlLXdhbGtlckAzLjAuMyIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAdHlwZXMvZXN0cmVlIjogIl4xLjAuMCIgfSB9LCAic2hhNTEyLTdSVUtmWGdTTU1renQ2WnVYbXFhcE91ckxHUFBmZ2o2bDl1Ulo3bFJHb2x2azB5MnlvY2MzNUxkY3hLQzVQUVpkbjJETXFpb0FRMk5vV2NyVEttbTZnPT0iXSwKCiAgICAiZXhwZWN0LXR5cGUiOiBbImV4cGVjdC10eXBlQDEuMy4wIiwgIiIsIHt9LCAic2hhNTEyLWtudnllYXVZaHFqT1l2UTY2TXpuU01zODN3bUhyQ3ljTkVONkFvKzJBZVlFZnhVSWt1aVZ4ZEVhMXFsR0VQSytXZTNuMFRIaURjaVlTc0NjZ1cvRG9BPT0iXSwKCiAgICAiZmRpciI6IFsiZmRpckA2LjUuMCIsICIiLCB7ICJwZWVyRGVwZW5kZW5jaWVzIjogeyAicGljb21hdGNoIjogIl4zIHx8IF40IiB9LCAib3B0aW9uYWxQZWVycyI6IFsicGljb21hdGNoIl0gfSwgInNoYTUxMi10SWJZdFpidWNPczBCUkdxUEprc2hKVVlkTCtTREg3ZFZNOGdqeStFUnAzV0FVakxFRkpFKzAya2FueUh0d2pXT253cktZQml3QW1NMHA0a0xKQW5YZz09Il0sCgogICAgImZzZXZlbnRzIjogWyJmc2V2ZW50c0AyLjMuMyIsICIiLCB7ICJvcyI6ICJkYXJ3aW4iIH0sICJzaGE1MTItNXhvRGZYK2ZMN2ZhQVRuYWdtV1BwYkZ0d2gvUjc3V21NTXFxSEdTNjVDM3Z2QjBZSHJnRitCMVltWjM0NDF0TWo1bjYzazAyMTJYTm9Kd3psaGZmUXc9PSJdLAoKICAgICJnZXQtY2FsbGVyLWZpbGUiOiBbImdldC1jYWxsZXItZmlsZUAyLjAuNSIsICIiLCB7fSwgInNoYTUxMi1EeUZQM0JNLzNZSFRRT0NVTC93ME9aSFIwbHBLZUdyeG90Y0hXY3FORWRubHRxRndYVmZoRUJROTRlSW8zNEFmUXBvMHJHa2k0Y3lJaWZ0WTA2aDJGZz09Il0sCgogICAgImdyYWNlZnVsLWZzIjogWyJncmFjZWZ1bC1mc0A0LjIuMTEiLCAiIiwge30sICJzaGE1MTItUmJKNS9qbUZjTk5DY0RWNW85ZVRuQkxKL0hzeldWMFA3M2JjK0ZmNG5TL3JKaitZYVM2SUd5aU9MMFZvQllYK2wxV3JsM2s2M2gvS3JIK25oSjBYdlE9PSJdLAoKICAgICJoYXMtZmxhZyI6IFsiaGFzLWZsYWdANC4wLjAiLCAiIiwge30sICJzaGE1MTItRXlrSlQvUTFLalRXY3RwcGdJQWdmU08wdEtWdVpVamhnTXIxN2txVHVtTWw2QWZ2M0VJU2xlVTdxWlV6b1hERlRBSFREQzROT29HL1p4VTNFdmxNUFE9PSJdLAoKICAgICJpcy1mdWxsd2lkdGgtY29kZS1wb2ludCI6IFsiaXMtZnVsbHdpZHRoLWNvZGUtcG9pbnRAMy4wLjAiLCAiIiwge30sICJzaGE1MTItenltbTUrdStzQ3NTV3lEOXFOYWVqVjNERnZoQ0tjbEtkaXpZYUpVdUhBODNSTGpiN25TdUduZGRDSEd2MGhrK0tZN0JNQWxzV2VLNFVlZzZFVjZYUWc9PSJdLAoKICAgICJqaXRpIjogWyJqaXRpQDIuNy4wIiwgIiIsIHsgImJpbiI6IHsgImppdGkiOiAibGliL2ppdGktY2xpLm1qcyIgfSB9LCAic2hhNTEyLUFDLzdKb2ZKdlpHcnJuZVdOYUVuSmVPTFV4K0psR3Q3dE5hMHdaaVJQVDRNWTF3bWZLanQyKzZPMnAydXoyK3NrbGw4T1pabUpNTnFla2U3a0tiTmdRPT0iXSwKCiAgICAibGlicGhvbmVudW1iZXItanMiOiBbImxpYnBob25lbnVtYmVyLWpzQDEuMTIuNDEiLCAiIiwge30sICJzaGE1MTItbHNtTW1HWEJ4WElLL1ZNTEVqMGtMNk10VXMxa0JHajFuVEN6aTZ6Z1FvRzFERXdxd3QyRFF5SHhjTHlrY2VJeEFuZkUzaHlhN051SWg2UHBDNlMzZkE9PSJdLAoKICAgICJsaWdodG5pbmdjc3MiOiBbImxpZ2h0bmluZ2Nzc0AxLjMyLjAiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiZGV0ZWN0LWxpYmMiOiAiXjIuMC4zIiB9LCAib3B0aW9uYWxEZXBlbmRlbmNpZXMiOiB7ICJsaWdodG5pbmdjc3MtYW5kcm9pZC1hcm02NCI6ICIxLjMyLjAiLCAibGlnaHRuaW5nY3NzLWRhcndpbi1hcm02NCI6ICIxLjMyLjAiLCAibGlnaHRuaW5nY3NzLWRhcndpbi14NjQiOiAiMS4zMi4wIiwgImxpZ2h0bmluZ2Nzcy1mcmVlYnNkLXg2NCI6ICIxLjMyLjAiLCAibGlnaHRuaW5nY3NzLWxpbnV4LWFybS1nbnVlYWJpaGYiOiAiMS4zMi4wIiwgImxpZ2h0bmluZ2Nzcy1saW51eC1hcm02NC1nbnUiOiAiMS4zMi4wIiwgImxpZ2h0bmluZ2Nzcy1saW51eC1hcm02NC1tdXNsIjogIjEuMzIuMCIsICJsaWdodG5pbmdjc3MtbGludXgteDY0LWdudSI6ICIxLjMyLjAiLCAibGlnaHRuaW5nY3NzLWxpbnV4LXg2NC1tdXNsIjogIjEuMzIuMCIsICJsaWdodG5pbmdjc3Mtd2luMzItYXJtNjQtbXN2YyI6ICIxLjMyLjAiLCAibGlnaHRuaW5nY3NzLXdpbjMyLXg2NC1tc3ZjIjogIjEuMzIuMCIgfSB9LCAic2hhNTEyLU5YWUJ6aW5OcmJsZnJhUEd5cmJQb0QxOUMxaDlsZkkvMW16Z1dZdlhVVGU0MTRHei9YMUZEMlhCWlNaTTdyUlRyTUE4SkwzT3RBYUdpZnJJS2hRNXlRPT0iXSwKCiAgICAibGlnaHRuaW5nY3NzLWFuZHJvaWQtYXJtNjQiOiBbImxpZ2h0bmluZ2Nzcy1hbmRyb2lkLWFybTY0QDEuMzIuMCIsICIiLCB7ICJvcyI6ICJhbmRyb2lkIiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi1ZSzcvQ2xUdDRrQUswdm82dzNYK1BubTBEMmNmMnZQSGJoT1hkb050aTFHYTBhbDFQNFRCWmh3akFUdmpOd0xFQkNuS3ZqSmMyalFnSFhIME5Fd2xBZz09Il0sCgogICAgImxpZ2h0bmluZ2Nzcy1kYXJ3aW4tYXJtNjQiOiBbImxpZ2h0bmluZ2Nzcy1kYXJ3aW4tYXJtNjRAMS4zMi4wIiwgIiIsIHsgIm9zIjogImRhcndpbiIsICJjcHUiOiAiYXJtNjQiIH0sICJzaGE1MTItUnplRzlKdTViYWcyQnYxL2x3bFZKdkJFM3E2VHRYc2tkWkxMQ3lmZzVwdCtITHo5QnFsSUNPN0xaTTdWSE5UVG4vNVBSaEhGQlNqazVsYzRjbXNjUFE9PSJdLAoKICAgICJsaWdodG5pbmdjc3MtZGFyd2luLXg2NCI6IFsibGlnaHRuaW5nY3NzLWRhcndpbi14NjRAMS4zMi4wIiwgIiIsIHsgIm9zIjogImRhcndpbiIsICJjcHUiOiAieDY0IiB9LCAic2hhNTEyLVUrUXNCcDJtL3Myd3FwVVlULzZ3bmxhZ2RaYnRaZG5kU211dC9OSnFsQ2NNTFRXcDVtdUNySUQrSzVVSjZqcUQyQkZzaGVqQ1lYbmlQRGJOaDczVjh3PT0iXSwKCiAgICAibGlnaHRuaW5nY3NzLWZyZWVic2QteDY0IjogWyJsaWdodG5pbmdjc3MtZnJlZWJzZC14NjRAMS4zMi4wIiwgIiIsIHsgIm9zIjogImZyZWVic2QiLCAiY3B1IjogIng2NCIgfSwgInNoYTUxMi1KQ1RpZ2VkRWtzWmszdEhUVHRobk1kVmZHZjYxRmt5OEppMkU0WWpVVEVRWDE0eGl5L2xUelhudTF2d2laZTNiWWUwcStTcHNTSC9DVGVEWEs2V0hpZz09Il0sCgogICAgImxpZ2h0bmluZ2Nzcy1saW51eC1hcm0tZ251ZWFiaWhmIjogWyJsaWdodG5pbmdjc3MtbGludXgtYXJtLWdudWVhYmloZkAxLjMyLjAiLCAiIiwgeyAib3MiOiAibGludXgiLCAiY3B1IjogImFybSIgfSwgInNoYTUxMi14NnJubnBSYTJHTDB6UU9rdDZydHMzWURQemR1THBXdndBRjZFTWhYRlZaWEQ0dFByQmtFRnF6R293ekNzSVdzUGpxU0srdHlORU9EVUJYZWVWSFNrdz09Il0sCgogICAgImxpZ2h0bmluZ2Nzcy1saW51eC1hcm02NC1nbnUiOiBbImxpZ2h0bmluZ2Nzcy1saW51eC1hcm02NC1nbnVAMS4zMi4wIiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi0wbm5NeW95T0xSSlhmYk1PaWxhU1JjTEgzSnc1ejlIRE5HZlQvZ3dDUGdhRGpueDBpOHc3dkJ6RkxGUjFmNkNNTEtGOGdWYmVibWtVTjNmYS9rUUpwUT09Il0sCgogICAgImxpZ2h0bmluZ2Nzcy1saW51eC1hcm02NC1tdXNsIjogWyJsaWdodG5pbmdjc3MtbGludXgtYXJtNjQtbXVzbEAxLjMyLjAiLCAiIiwgeyAib3MiOiAibGludXgiLCAiY3B1IjogImFybTY0IiB9LCAic2hhNTEyLVVwUWtvZW5yNFVKRXpnVklZcEk4MGxERnZSbVBWZzZvcWJvTkhmb0g0Q1FJZk5BK0hPclo3TW83S1pQMDJkQzZMamdoUFFKZUJzdlhoSm9kL3duSUJnPT0iXSwKCiAgICAibGlnaHRuaW5nY3NzLWxpbnV4LXg2NC1nbnUiOiBbImxpZ2h0bmluZ2Nzcy1saW51eC14NjQtZ251QDEuMzIuMCIsICIiLCB7ICJvcyI6ICJsaW51eCIsICJjcHUiOiAieDY0IiB9LCAic2hhNTEyLVY3UXI1MkloWm1kS1BWcitWdHc4bytXTHNRSllDVGQ4bG9JZnBEYU1SV0dVWmZCT1lFSmV5SklrcUdJRE1aUHdQeDI0cFVNZndTeHhJOHBoci9NYk9BPT0iXSwKCiAgICAibGlnaHRuaW5nY3NzLWxpbnV4LXg2NC1tdXNsIjogWyJsaWdodG5pbmdjc3MtbGludXgteDY0LW11c2xAMS4zMi4wIiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJ4NjQiIH0sICJzaGE1MTItYlljTHArVmIwYXdzaVhnLzgwdUNSZXpDWUhOZzEvbDNtdDBnekhuV1Y5WFAxVzVzS2E1L1RDZEdXYVIvekJNMlBlRi9IYnNRdi9qMlVSTk9pVnV4V2c9PSJdLAoKICAgICJsaWdodG5pbmdjc3Mtd2luMzItYXJtNjQtbXN2YyI6IFsibGlnaHRuaW5nY3NzLXdpbjMyLWFybTY0LW1zdmNAMS4zMi4wIiwgIiIsIHsgIm9zIjogIndpbjMyIiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi04U2JDOEJSNDBwUzZiYUNNOHNidFlEU3dFVlFkNEpsRlRPbGFEM2dXR0hmVGhUY0FCbk5EQmRhNmVUWmVxYm9mYWxJSmhGeDBxS3pnSEptY1BUbkdkdz09Il0sCgogICAgImxpZ2h0bmluZ2Nzcy13aW4zMi14NjQtbXN2YyI6IFsibGlnaHRuaW5nY3NzLXdpbjMyLXg2NC1tc3ZjQDEuMzIuMCIsICIiLCB7ICJvcyI6ICJ3aW4zMiIsICJjcHUiOiAieDY0IiB9LCAic2hhNTEyLUFtcTlCL1NvWllkRGkxa0Zyb2pub3FQTHhZaFE0V281WGlMOEVWSnJWc0I4QVJvQzFQV1c2Vkd0VDBXS0NlbWp5OGFDK2xvdUpualM3VTE4eDNiMDZRPT0iXSwKCiAgICAibWFnaWMtc3RyaW5nIjogWyJtYWdpYy1zdHJpbmdAMC4zMC4yMSIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAanJpZGdld2VsbC9zb3VyY2VtYXAtY29kZWMiOiAiXjEuNS41IiB9IH0sICJzaGE1MTItdmQyRjRZVXlFWEtHY0xIb3ErVEV5Q2p4dWVTZUhuRnh5eWpOcDgweWcwWFY0dlVobkRlci9sdnZscU0vYXJCNWJYUU41SzIvM29pbnlDUnl4OFQyQ1E9PSJdLAoKICAgICJuYW5vaWQiOiBbIm5hbm9pZEAzLjMuMTIiLCAiIiwgeyAiYmluIjogeyAibmFub2lkIjogImJpbi9uYW5vaWQuY2pzIiB9IH0sICJzaGE1MTItWkI5UkgvMzlxcHE1VnU2WStObVVhRmhRUjZwcCtNMlh0NzZYQm5Fd0RhR2NWQXFobHZ4cmwzQjJiS1M1RDNOSDNRUjc2djNhU3JLYUYvS2l5N2xFdFE9PSJdLAoKICAgICJuZXh0IjogWyJuZXh0QDE2LjIuNiIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAbmV4dC9lbnYiOiAiMTYuMi42IiwgIkBzd2MvaGVscGVycyI6ICIwLjUuMTUiLCAiYmFzZWxpbmUtYnJvd3Nlci1tYXBwaW5nIjogIl4yLjkuMTkiLCAiY2FuaXVzZS1saXRlIjogIl4xLjAuMzAwMDE1NzkiLCAicG9zdGNzcyI6ICI4LjQuMzEiLCAic3R5bGVkLWpzeCI6ICI1LjEuNiIgfSwgIm9wdGlvbmFsRGVwZW5kZW5jaWVzIjogeyAiQG5leHQvc3djLWRhcndpbi1hcm02NCI6ICIxNi4yLjYiLCAiQG5leHQvc3djLWRhcndpbi14NjQiOiAiMTYuMi42IiwgIkBuZXh0L3N3Yy1saW51eC1hcm02NC1nbnUiOiAiMTYuMi42IiwgIkBuZXh0L3N3Yy1saW51eC1hcm02NC1tdXNsIjogIjE2LjIuNiIsICJAbmV4dC9zd2MtbGludXgteDY0LWdudSI6ICIxNi4yLjYiLCAiQG5leHQvc3djLWxpbnV4LXg2NC1tdXNsIjogIjE2LjIuNiIsICJAbmV4dC9zd2Mtd2luMzItYXJtNjQtbXN2YyI6ICIxNi4yLjYiLCAiQG5leHQvc3djLXdpbjMyLXg2NC1tc3ZjIjogIjE2LjIuNiIsICJzaGFycCI6ICJeMC4zNC41IiB9LCAicGVlckRlcGVuZGVuY2llcyI6IHsgIkBvcGVudGVsZW1ldHJ5L2FwaSI6ICJeMS4xLjAiLCAiQHBsYXl3cmlnaHQvdGVzdCI6ICJeMS41MS4xIiwgImJhYmVsLXBsdWdpbi1yZWFjdC1jb21waWxlciI6ICIqIiwgInJlYWN0IjogIl4xOC4yLjAgfHwgMTkuMC4wLXJjLWRlNjhkMmY0LTIwMjQxMjA0IHx8IF4xOS4wLjAiLCAicmVhY3QtZG9tIjogIl4xOC4yLjAgfHwgMTkuMC4wLXJjLWRlNjhkMmY0LTIwMjQxMjA0IHx8IF4xOS4wLjAiLCAic2FzcyI6ICJeMS4zLjAiIH0sICJvcHRpb25hbFBlZXJzIjogWyJAb3BlbnRlbGVtZXRyeS9hcGkiLCAiQHBsYXl3cmlnaHQvdGVzdCIsICJiYWJlbC1wbHVnaW4tcmVhY3QtY29tcGlsZXIiLCAic2FzcyJdLCAiYmluIjogeyAibmV4dCI6ICJkaXN0L2Jpbi9uZXh0IiB9IH0sICJzaGE1MTItcU9WZ0tKZzErQXQxNU5wZVVQK2VKZ0NIdlRDZ1hzb2d3ZXE4N1JpL0l4N1BrcVFIZzRzZGFYbVNGcUtsZ2FJWEU0a1cwZzI1TEU2OFc4N1VBTmxIdHc9PSJdLAoKICAgICJvYnVnIjogWyJvYnVnQDIuMS4xIiwgIiIsIHt9LCAic2hhNTEyLXVUcUY5TXVQcmFBUStJc25QZjM2NlJHNGNQOVJ0VWk3TUxPMU4zS0VjK3diMGE2eUtwZUwwbG1rMklCMWpZNUtIUEFsVGM2VC9KUmRDL1lxeEhOd2tRPT0iXSwKCiAgICAicGF0aGUiOiBbInBhdGhlQDIuMC4zIiwgIiIsIHt9LCAic2hhNTEyLVdVakdjQXFQMWdRYWNvUWUrT0JKc0ZBN0xkNER5WHVVSWpaNWNjNzVjTEh2SjdkdE5zVHVncGh4SUFEd3NwUytBcmFBVWVQQ0tyU1Z0UExGai9GODh3PT0iXSwKCiAgICAicGljb2NvbG9ycyI6IFsicGljb2NvbG9yc0AxLjEuMSIsICIiLCB7fSwgInNoYTUxMi14Y2VIMnNuaHRiNU05bGlxRHNtRXc1NmxlMzc2bVRaa0VYL2pFYi9SeE5GeWVnTnVsN2VOc2xDWFA5RkRqL0xjdTBYOEtFeU1jZVAybnRwYUhyREVWQT09Il0sCgogICAgInBpY29tYXRjaCI6IFsicGljb21hdGNoQDQuMC40IiwgIiIsIHt9LCAic2hhNTEyLVFQODhCQUt2TWFtLzNOeEg2dmoybzIxUjZNanhaVUFkNm5sd0FTL3BuR3ZOOUlWTG9jTEh4R1lJekZoZzZmVVErNXRoNlA0ZHY0ZVc5algzRFNJajdBPT0iXSwKCiAgICAicG9zdGNzcyI6IFsicG9zdGNzc0A4LjUuMTUiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAibmFub2lkIjogIl4zLjMuMTIiLCAicGljb2NvbG9ycyI6ICJeMS4xLjEiLCAic291cmNlLW1hcC1qcyI6ICJeMS4yLjEiIH0gfSwgInNoYTUxMi1GZlI4c2pkNGVtMlQ2ZmIzSTJNd0FKVTdIV1ZNcjl6YmErZW5tUWVlV0ZmQ2JtK1VPQy8wWDREUzhYdHBVVE13V01HYmpLWVA3eGpmTmVrenlHbUIzQT09Il0sCgogICAgInJlYWN0IjogWyJyZWFjdEAxOS4yLjYiLCAiIiwge30sICJzaGE1MTItc2ZXR0dmYXZpMHhyOFBnMHNWc3lITUFPemlWWUtnUExOclM3aWcraXZNTmIzd2JDQnczS3h0ZmxzR0JBd0QzZ1lRbEUvQUVac1RMZ1RvUnJTQ2piMFE9PSJdLAoKICAgICJyZWFjdC1kYXktcGlja2VyIjogWyJyZWFjdC1kYXktcGlja2VyQDkuMTQuMCIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAZGF0ZS1mbnMvdHoiOiAiXjEuNC4xIiwgIkB0YWJieV9haS9oaWpyaS1jb252ZXJ0ZXIiOiAiMS4wLjUiLCAiZGF0ZS1mbnMiOiAiXjQuMS4wIiwgImRhdGUtZm5zLWphbGFsaSI6ICI0LjEuMC0wIiB9LCAicGVlckRlcGVuZGVuY2llcyI6IHsgInJlYWN0IjogIj49MTYuOC4wIiB9IH0sICJzaGE1MTItdEJhb0RXalB3ZTBNNXBHcnVtNEgwU1I2THlrK0JPOW9IbnA5SmJLcEdLVzJtbHJhTlBnUDlCTWZzZzVwV3B3cnNzQVJtZXFrN1lCbDJvWHV0WlRhSEE9PSJdLAoKICAgICJyZWFjdC1kb20iOiBbInJlYWN0LWRvbUAxOS4yLjYiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAic2NoZWR1bGVyIjogIl4wLjI3LjAiIH0sICJwZWVyRGVwZW5kZW5jaWVzIjogeyAicmVhY3QiOiAiXjE5LjIuNiIgfSB9LCAic2hhNTEyLTBwck1JK2h2QmJQanNXbnhETHhsQ0d5TThQTjZVdVdqRVVDWW1aaE82N3hJVjlYYXNhL3IvdkRucStYeXE0TG8yN2c4UVNiTzVZekFSdTBEMVNwczNnPT0iXSwKCiAgICAicmVxdWlyZS1kaXJlY3RvcnkiOiBbInJlcXVpcmUtZGlyZWN0b3J5QDIuMS4xIiwgIiIsIHt9LCAic2hhNTEyLWZHeEVJNyt3c0c5eHJ2ZGpzcmxtTDIyT01UVGlIUndBTXJvaUVlTWdxOGd6b0xDL1BRcjdSc1JEU1RMVWcvYlpBWnRGK1RWSWtIYzYvNFJJS3J1aStRPT0iXSwKCiAgICAicmVzZWxlY3QiOiBbInJlc2VsZWN0QDUuMi4wIiwgIiIsIHt9LCAic2hhNTEyLUFnWjNVT1ptM1luZGZySjRPWWpnclQ3Ym1DbS8xaXFranZFZkgvb1lqemg2UEQycXc0UXVUM2pqblhJcnBkdDRNVHBNWGNsTVQzbFhibVJZK1hSYWt3PT0iXSwKCiAgICAicm9sbGRvd24iOiBbInJvbGxkb3duQDEuMC4xIiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIkBveGMtcHJvamVjdC90eXBlcyI6ICI9MC4xMzAuMCIsICJAcm9sbGRvd24vcGx1Z2ludXRpbHMiOiAiXjEuMC4wIiB9LCAib3B0aW9uYWxEZXBlbmRlbmNpZXMiOiB7ICJAcm9sbGRvd24vYmluZGluZy1hbmRyb2lkLWFybTY0IjogIjEuMC4xIiwgIkByb2xsZG93bi9iaW5kaW5nLWRhcndpbi1hcm02NCI6ICIxLjAuMSIsICJAcm9sbGRvd24vYmluZGluZy1kYXJ3aW4teDY0IjogIjEuMC4xIiwgIkByb2xsZG93bi9iaW5kaW5nLWZyZWVic2QteDY0IjogIjEuMC4xIiwgIkByb2xsZG93bi9iaW5kaW5nLWxpbnV4LWFybS1nbnVlYWJpaGYiOiAiMS4wLjEiLCAiQHJvbGxkb3duL2JpbmRpbmctbGludXgtYXJtNjQtZ251IjogIjEuMC4xIiwgIkByb2xsZG93bi9iaW5kaW5nLWxpbnV4LWFybTY0LW11c2wiOiAiMS4wLjEiLCAiQHJvbGxkb3duL2JpbmRpbmctbGludXgtcHBjNjQtZ251IjogIjEuMC4xIiwgIkByb2xsZG93bi9iaW5kaW5nLWxpbnV4LXMzOTB4LWdudSI6ICIxLjAuMSIsICJAcm9sbGRvd24vYmluZGluZy1saW51eC14NjQtZ251IjogIjEuMC4xIiwgIkByb2xsZG93bi9iaW5kaW5nLWxpbnV4LXg2NC1tdXNsIjogIjEuMC4xIiwgIkByb2xsZG93bi9iaW5kaW5nLW9wZW5oYXJtb255LWFybTY0IjogIjEuMC4xIiwgIkByb2xsZG93bi9iaW5kaW5nLXdhc20zMi13YXNpIjogIjEuMC4xIiwgIkByb2xsZG93bi9iaW5kaW5nLXdpbjMyLWFybTY0LW1zdmMiOiAiMS4wLjEiLCAiQHJvbGxkb3duL2JpbmRpbmctd2luMzIteDY0LW1zdmMiOiAiMS4wLjEiIH0sICJiaW4iOiB7ICJyb2xsZG93biI6ICJiaW4vY2xpLm1qcyIgfSB9LCAic2hhNTEyLVgwS1FIbGpObkVrV05xcWl6OXpKckd1bmgxQjBIZ094TFh2bkZwQ09jYWR6Y3k1cW9oWjN0cU1FVWcwMHZuY29Sb3ZYdUszWnFDVDlLbm5Lem9JbkZRPT0iXSwKCiAgICAicnhqcyI6IFsicnhqc0A3LjguMiIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJ0c2xpYiI6ICJeMi4xLjAiIH0gfSwgInNoYTUxMi1kaEtmOTAzVS9QUVpZNmJvTk50QUdkV2JHODVXQWJqVC8xeFlvWklDN0ZBWTB5V2FwT0JRVnNWckRsNThXODYvL2UxVnBNTkJ0UlY0TWFYZmRNeVNGQT09Il0sCgogICAgInNjaGVkdWxlciI6IFsic2NoZWR1bGVyQDAuMjcuMCIsICIiLCB7fSwgInNoYTUxMi1lTnYrV3JWYkt1MWYzdmJZSlQveHRpRjVzeUE1SFBJTXRmOUlnWS9uS2cwc1dxekFVRXZxWS94bTdPY1pjL3FhZkx4L2lPOUZnT21lU0FwNHY1dGkvUT09Il0sCgogICAgInNlbXZlciI6IFsic2VtdmVyQDcuOC4wIiwgIiIsIHsgImJpbiI6IHsgInNlbXZlciI6ICJiaW4vc2VtdmVyLmpzIiB9IH0sICJzaGE1MTItQWNNN2RWLzV1bDRFZWtvUTI5QWdtNXZyaThKTnFSeWozOW8wcXBYNnZERjJHWnJ0dXRabDVSd2dEMVhuWmppVEFmbmNzSmhNSTQ4UVFIM3NOODdZTkE9PSJdLAoKICAgICJzaGFycCI6IFsic2hhcnBAMC4zNC41IiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIkBpbWcvY29sb3VyIjogIl4xLjAuMCIsICJkZXRlY3QtbGliYyI6ICJeMi4xLjIiLCAic2VtdmVyIjogIl43LjcuMyIgfSwgIm9wdGlvbmFsRGVwZW5kZW5jaWVzIjogeyAiQGltZy9zaGFycC1kYXJ3aW4tYXJtNjQiOiAiMC4zNC41IiwgIkBpbWcvc2hhcnAtZGFyd2luLXg2NCI6ICIwLjM0LjUiLCAiQGltZy9zaGFycC1saWJ2aXBzLWRhcndpbi1hcm02NCI6ICIxLjIuNCIsICJAaW1nL3NoYXJwLWxpYnZpcHMtZGFyd2luLXg2NCI6ICIxLjIuNCIsICJAaW1nL3NoYXJwLWxpYnZpcHMtbGludXgtYXJtIjogIjEuMi40IiwgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC1hcm02NCI6ICIxLjIuNCIsICJAaW1nL3NoYXJwLWxpYnZpcHMtbGludXgtcHBjNjQiOiAiMS4yLjQiLCAiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4LXJpc2N2NjQiOiAiMS4yLjQiLCAiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4LXMzOTB4IjogIjEuMi40IiwgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC14NjQiOiAiMS4yLjQiLCAiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4bXVzbC1hcm02NCI6ICIxLjIuNCIsICJAaW1nL3NoYXJwLWxpYnZpcHMtbGludXhtdXNsLXg2NCI6ICIxLjIuNCIsICJAaW1nL3NoYXJwLWxpbnV4LWFybSI6ICIwLjM0LjUiLCAiQGltZy9zaGFycC1saW51eC1hcm02NCI6ICIwLjM0LjUiLCAiQGltZy9zaGFycC1saW51eC1wcGM2NCI6ICIwLjM0LjUiLCAiQGltZy9zaGFycC1saW51eC1yaXNjdjY0IjogIjAuMzQuNSIsICJAaW1nL3NoYXJwLWxpbnV4LXMzOTB4IjogIjAuMzQuNSIsICJAaW1nL3NoYXJwLWxpbnV4LXg2NCI6ICIwLjM0LjUiLCAiQGltZy9zaGFycC1saW51eG11c2wtYXJtNjQiOiAiMC4zNC41IiwgIkBpbWcvc2hhcnAtbGludXhtdXNsLXg2NCI6ICIwLjM0LjUiLCAiQGltZy9zaGFycC13YXNtMzIiOiAiMC4zNC41IiwgIkBpbWcvc2hhcnAtd2luMzItYXJtNjQiOiAiMC4zNC41IiwgIkBpbWcvc2hhcnAtd2luMzItaWEzMiI6ICIwLjM0LjUiLCAiQGltZy9zaGFycC13aW4zMi14NjQiOiAiMC4zNC41IiB9IH0sICJzaGE1MTItT3U5STVGdDlXTmNDYlhyVTljTWdQQmNDSzhMaXdMcWNieXdXM3Q0b0RWMzduMXB6cHVOTHNZaUFWOGVPRG5qYnRRbFNEd1oyY1VFZVF6NEU1NEhsdGc9PSJdLAoKICAgICJzaGVsbC1xdW90ZSI6IFsic2hlbGwtcXVvdGVAMS44LjMiLCAiIiwge30sICJzaGE1MTItT2JtbklGNGhYTmcxQnFobkhtZ2JERVRGOGRMUENnZ1pXQmprUWZoWnBic3pabll1cjVEVWxqVGNDSGlpNUxDM0o1RTB5ZU8vMUxJTXlIK1V2SFFneXc9PSJdLAoKICAgICJzaWdpbmZvIjogWyJzaWdpbmZvQDIuMC4wIiwgIiIsIHt9LCAic2hhNTEyLXlieDBXTzEvOGJTQkxFV1hadkVkN2dNVzNTbjNKRmxXM1R2WDFuUkViRExSTlFOYWVOTjhXSzBtZUJ3UGRBYU9JN1R0UlJSSm4vRXMxemhyckNIdTdnPT0iXSwKCiAgICAic291cmNlLW1hcC1qcyI6IFsic291cmNlLW1hcC1qc0AxLjIuMSIsICIiLCB7fSwgInNoYTUxMi1VWFdNS2hMT3dWS2I3MjhJVXRRUFh4ZllVK3VzZHlidFVySy84dUdFOENRTXZyaE9wd3Z6REJ3ajBRaFNMN01RYzd2SXNJU0JHOFZROCtJRFF4cGZRQT09Il0sCgogICAgInN0YWNrYmFjayI6IFsic3RhY2tiYWNrQDAuMC4yIiwgIiIsIHt9LCAic2hhNTEyLTFYTUpFNWZRbzFqR0g2WS83ZWJud1BPQkVrSUVuVDRRRjMyZDVSMStWWGRYdmVNMElCTUp0OHpmYXhYMVAzUWhWd3JZZSs1NzYramtBTnRTUzJtQmJ3PT0iXSwKCiAgICAic3RkLWVudiI6IFsic3RkLWVudkA0LjEuMCIsICIiLCB7fSwgInNoYTUxMi1ScTd5YmNYMlJ1QzU1cjlvYVBWRVc3L3h1M3RqOHU0R2VCWUhCV0N5Y2hGdHpNSXI4NkE3ZTNQUEVCUFQzN3NIU3RLWDMrVGlYL0ZyL0FDbUpMVmxMUT09Il0sCgogICAgInN0cmluZy13aWR0aCI6IFsic3RyaW5nLXdpZHRoQDQuMi4zIiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgImVtb2ppLXJlZ2V4IjogIl44LjAuMCIsICJpcy1mdWxsd2lkdGgtY29kZS1wb2ludCI6ICJeMy4wLjAiLCAic3RyaXAtYW5zaSI6ICJeNi4wLjEiIH0gfSwgInNoYTUxMi13S3lRUlFwakowc0lwNjJFclNaZEdzak1KV3NhcDVvUk5paEhodTZHN0pWTy85aklCNlV5ZXZMK3RYdU9xcm5nOGovY3hLVFd5V1V3dlNUcmlpWnovZz09Il0sCgogICAgInN0cmlwLWFuc2kiOiBbInN0cmlwLWFuc2lANi4wLjEiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiYW5zaS1yZWdleCI6ICJeNS4wLjEiIH0gfSwgInNoYTUxMi1ZMzhWUFNIY3FrRnJDcEZuUTl2dVNYbXF1dXY1b1hPS3BHZVQ2YUdycjNvM0djOUFsVmE2SkJmVVNPQ25ieEdHWkYrLzBvb0k3S3JQdVVTenRVZFU1QT09Il0sCgogICAgInN0eWxlZC1qc3giOiBbInN0eWxlZC1qc3hANS4xLjYiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiY2xpZW50LW9ubHkiOiAiMC4wLjEiIH0sICJwZWVyRGVwZW5kZW5jaWVzIjogeyAicmVhY3QiOiAiPj0gMTYuOC4wIHx8IDE3LngueCB8fCBeMTguMC4wLTAgfHwgXjE5LjAuMC0wIiB9IH0sICJzaGE1MTItcVNWeURUZU1vdGR2UVlvSFdMTkd3UkZKSEMraStadmRCUllvc09GZ0MrV2cxdng0ZnJOMi9SRy9OQTdTWXFxdktOTGYzOVAyTFNSQTJwdTZuMFhZWkE9PSJdLAoKICAgICJzdXBwb3J0cy1jb2xvciI6IFsic3VwcG9ydHMtY29sb3JAOC4xLjEiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiaGFzLWZsYWciOiAiXjQuMC4wIiB9IH0sICJzaGE1MTItTXBVRU4yT29kdFV6eHZLUWw3MmNVRjdSUTVFaUhzR3ZTc1ZHMGlhOWM1UmJXR0wyQ0k0QzdFcFBTOFVUQklwbG5selppTnVWNTZ3K0Z1Tnh5M3R5MlE9PSJdLAoKICAgICJ0YWlsd2luZC1tZXJnZSI6IFsidGFpbHdpbmQtbWVyZ2VAMy42LjAiLCAiIiwge30sICJzaGE1MTItdXhMN3FBVlFyaXFSUVBBeUszcGo2NlZxc2tXcW9aMzdQVzk0andPVHdOZnEvejlveXUxVitlcXJacXRSMitmQ2lYZFlPWmUvTW9kdDhHdHZxTnp1K3c9PSJdLAoKICAgICJ0YWlsd2luZGNzcyI6IFsidGFpbHdpbmRjc3NANC4zLjAiLCAiIiwge30sICJzaGE1MTIteTZueE1HQjFuTVc5UjZrOTZlNWdkSUZ6Y2ZML2dUSlJOYXFHZXMxWXZrTG5QVlh6V2dicUZGMnlMQzBUOEc3NzRuMjRjeDNQZThYcktvbmlDT0FIK1E9PSJdLAoKICAgICJ0YXBhYmxlIjogWyJ0YXBhYmxlQDIuMy4zIiwgIiIsIHt9LCAic2hhNTEyLXV4Yy96cHFGZzZ4N0M4dk9FN2xoNkxiZGE4ZUVMOXptVm0vUExlVFBCUmhoMXhDZ2RXYVErSjFDVWllR3BJZm0ySGR0c1VwUnYrSHNoaWFzQk1jYzZBPT0iXSwKCiAgICAidGlueWJlbmNoIjogWyJ0aW55YmVuY2hAMi45LjAiLCAiIiwge30sICJzaGE1MTItMCtEVXZxV01WYWxMbWhhNmxyNGtEOGlBTUsxSHpWMC9hS25DdFdiOXY5NjQxVG5QL01GYjdQYzJieG94UWpUWEFFcnJ5WFZnVU9mdjJZcU5sbHFHZWc9PSJdLAoKICAgICJ0aW55ZXhlYyI6IFsidGlueWV4ZWNAMS4xLjIiLCAiIiwge30sICJzaGE1MTItZEFxU3FFL1JhYnBCS0k4K2gyNkdmTHE2VmIzSlZYczMwWFlRamRNamFqL2MydFM4SVlZTWJJelA1OTlLdFJqN2M1Ny93WUFwYjNRamdSZ1htckN1a0E9PSJdLAoKICAgICJ0aW55Z2xvYmJ5IjogWyJ0aW55Z2xvYmJ5QDAuMi4xNiIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJmZGlyIjogIl42LjUuMCIsICJwaWNvbWF0Y2giOiAiXjQuMC40IiB9IH0sICJzaGE1MTItcG45OVZob0FDWVI4bkZIaHhxaXgrdXZzYlhpbmVBYXNXbTVvalhvTjh4RXdLNUtkMy9UcmhObjF3Qnl1RDUyVXhXUkx5OHB1K2tSTW5pRWk2RXE5Wmc9PSJdLAoKICAgICJ0aW55cmFpbmJvdyI6IFsidGlueXJhaW5ib3dAMy4xLjAiLCAiIiwge30sICJzaGE1MTItQmYrSUxtQmdyZXRVcmRKeHpYTTBTZ1hMWjNYZmlhVXVPai9JS1FIdVRYaXArMDVYbit1eUVZZFZnMGtZRGlwVEJjTHJDVnlVekFQejdRbUFyYjBtbXc9PSJdLAoKICAgICJ0cmVlLWtpbGwiOiBbInRyZWUta2lsbEAxLjIuMiIsICIiLCB7ICJiaW4iOiB7ICJ0cmVlLWtpbGwiOiAiY2xpLmpzIiB9IH0sICJzaGE1MTItTDBPcnBpOHFHcFJHLy9OZCtIOTB2RkIrM2lIbnVlMXpTU0dtTk9PQ2gxR0xKN3JVS1Z3VjJIdmlqcGhHUVMyVW1oVVpld1M5Vmd2eFlJZGdyK2ZHMUE9PSJdLAoKICAgICJ0c2xpYiI6IFsidHNsaWJAMi44LjEiLCAiIiwge30sICJzaGE1MTItb0pGdTk0SFFiK0tWZHVTVVFMN3ducG1xbmZtTHNPQS9uQWg2YjZFSDB3Q0VvSzAvbVBlWFU2YzN3S0RWODNNa091SFBSSHRTWEtLVTk5SUJhelMvMnc9PSJdLAoKICAgICJ0eXBlc2NyaXB0IjogWyJ0eXBlc2NyaXB0QDUuOS4zIiwgIiIsIHsgImJpbiI6IHsgInRzYyI6ICJiaW4vdHNjIiwgInRzc2VydmVyIjogImJpbi90c3NlcnZlciIgfSB9LCAic2hhNTEyLWpsMXZaelBEaW5McjllVXQzSi90N1Y2RmdORXc5UWp2QlBkeXN6OUtmUURENDFmUXJDMlk0dktRZGlhVXBGVDRiWGxiMVJIaExwcDh3dG02TTVUZ1N3PT0iXSwKCiAgICAidW5kaWNpLXR5cGVzIjogWyJ1bmRpY2ktdHlwZXNANi4yMS4wIiwgIiIsIHt9LCAic2hhNTEyLWl3RFpxZzBRQUdyZzlSYXY1SDRuME02NGMzbWtSNTljSjZ3UXArN0M0bkkwZ3NtRXhhZWRhWUxOTzQ0ZVQ0QXRCQndqYlRpR1BNbHQyTWQwVDlIOUpRPT0iXSwKCiAgICAidXNlLXN5bmMtZXh0ZXJuYWwtc3RvcmUiOiBbInVzZS1zeW5jLWV4dGVybmFsLXN0b3JlQDEuNi4wIiwgIiIsIHsgInBlZXJEZXBlbmRlbmNpZXMiOiB7ICJyZWFjdCI6ICJeMTYuOC4wIHx8IF4xNy4wLjAgfHwgXjE4LjAuMCB8fCBeMTkuMC4wIiB9IH0sICJzaGE1MTItUHA2R1N3R1AvTnJQSXJ4VkZBSWtPUWV5dzhsRmVuT0hpalFXa1VUckR2ckY0QUxxeWxQMkMvS0NrZVM5ZHBVTTNLdllSUWhuYTV2dDdJTDk1K1pROXc9PSJdLAoKICAgICJ2aXRlIjogWyJ2aXRlQDguMC4xMyIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJsaWdodG5pbmdjc3MiOiAiXjEuMzIuMCIsICJwaWNvbWF0Y2giOiAiXjQuMC40IiwgInBvc3Rjc3MiOiAiXjguNS4xNCIsICJyb2xsZG93biI6ICIxLjAuMSIsICJ0aW55Z2xvYmJ5IjogIl4wLjIuMTYiIH0sICJvcHRpb25hbERlcGVuZGVuY2llcyI6IHsgImZzZXZlbnRzIjogIn4yLjMuMyIgfSwgInBlZXJEZXBlbmRlbmNpZXMiOiB7ICJAdHlwZXMvbm9kZSI6ICJeMjAuMTkuMCB8fCA+PTIyLjEyLjAiLCAiQHZpdGVqcy9kZXZ0b29scyI6ICJeMC4xLjE4IiwgImVzYnVpbGQiOiAiXjAuMjcuMCB8fCBeMC4yOC4wIiwgImppdGkiOiAiPj0xLjIxLjAiLCAibGVzcyI6ICJeNC4wLjAiLCAic2FzcyI6ICJeMS43MC4wIiwgInNhc3MtZW1iZWRkZWQiOiAiXjEuNzAuMCIsICJzdHlsdXMiOiAiPj0wLjU0LjgiLCAic3VnYXJzcyI6ICJeNS4wLjAiLCAidGVyc2VyIjogIl41LjE2LjAiLCAidHN4IjogIl40LjguMSIsICJ5YW1sIjogIl4yLjQuMiIgfSwgIm9wdGlvbmFsUGVlcnMiOiBbIkB0eXBlcy9ub2RlIiwgIkB2aXRlanMvZGV2dG9vbHMiLCAiZXNidWlsZCIsICJqaXRpIiwgImxlc3MiLCAic2FzcyIsICJzYXNzLWVtYmVkZGVkIiwgInN0eWx1cyIsICJzdWdhcnNzIiwgInRlcnNlciIsICJ0c3giLCAieWFtbCJdLCAiYmluIjogeyAidml0ZSI6ICJiaW4vdml0ZS5qcyIgfSB9LCAic2hhNTEyLU1GdGpCWWd6bVN4bWdBNFJBZmpJeVhXcEdlMW9BTG5qZ1VUenpWN1FMeC9US3hDemp0TUg2RmQ5L2VWSys1RmcxcU5vejVWQXdzbU1zL05vZnJtSnZ3PT0iXSwKCiAgICAidml0ZXN0IjogWyJ2aXRlc3RANC4xLjYiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiQHZpdGVzdC9leHBlY3QiOiAiNC4xLjYiLCAiQHZpdGVzdC9tb2NrZXIiOiAiNC4xLjYiLCAiQHZpdGVzdC9wcmV0dHktZm9ybWF0IjogIjQuMS42IiwgIkB2aXRlc3QvcnVubmVyIjogIjQuMS42IiwgIkB2aXRlc3Qvc25hcHNob3QiOiAiNC4xLjYiLCAiQHZpdGVzdC9zcHkiOiAiNC4xLjYiLCAiQHZpdGVzdC91dGlscyI6ICI0LjEuNiIsICJlcy1tb2R1bGUtbGV4ZXIiOiAiXjIuMC4wIiwgImV4cGVjdC10eXBlIjogIl4xLjMuMCIsICJtYWdpYy1zdHJpbmciOiAiXjAuMzAuMjEiLCAib2J1ZyI6ICJeMi4xLjEiLCAicGF0aGUiOiAiXjIuMC4zIiwgInBpY29tYXRjaCI6ICJeNC4wLjMiLCAic3RkLWVudiI6ICJeNC4wLjAtcmMuMSIsICJ0aW55YmVuY2giOiAiXjIuOS4wIiwgInRpbnlleGVjIjogIl4xLjAuMiIsICJ0aW55Z2xvYmJ5IjogIl4wLjIuMTUiLCAidGlueXJhaW5ib3ciOiAiXjMuMS4wIiwgInZpdGUiOiAiXjYuMC4wIHx8IF43LjAuMCB8fCBeOC4wLjAiLCAid2h5LWlzLW5vZGUtcnVubmluZyI6ICJeMi4zLjAiIH0sICJwZWVyRGVwZW5kZW5jaWVzIjogeyAiQGVkZ2UtcnVudGltZS92bSI6ICIqIiwgIkBvcGVudGVsZW1ldHJ5L2FwaSI6ICJeMS45LjAiLCAiQHR5cGVzL25vZGUiOiAiXjIwLjAuMCB8fCBeMjIuMC4wIHx8ID49MjQuMC4wIiwgIkB2aXRlc3QvYnJvd3Nlci1wbGF5d3JpZ2h0IjogIjQuMS42IiwgIkB2aXRlc3QvYnJvd3Nlci1wcmV2aWV3IjogIjQuMS42IiwgIkB2aXRlc3QvYnJvd3Nlci13ZWJkcml2ZXJpbyI6ICI0LjEuNiIsICJAdml0ZXN0L2NvdmVyYWdlLWlzdGFuYnVsIjogIjQuMS42IiwgIkB2aXRlc3QvY292ZXJhZ2UtdjgiOiAiNC4xLjYiLCAiQHZpdGVzdC91aSI6ICI0LjEuNiIsICJoYXBweS1kb20iOiAiKiIsICJqc2RvbSI6ICIqIiB9LCAib3B0aW9uYWxQZWVycyI6IFsiQGVkZ2UtcnVudGltZS92bSIsICJAb3BlbnRlbGVtZXRyeS9hcGkiLCAiQHR5cGVzL25vZGUiLCAiQHZpdGVzdC9icm93c2VyLXBsYXl3cmlnaHQiLCAiQHZpdGVzdC9icm93c2VyLXByZXZpZXciLCAiQHZpdGVzdC9icm93c2VyLXdlYmRyaXZlcmlvIiwgIkB2aXRlc3QvY292ZXJhZ2UtaXN0YW5idWwiLCAiQHZpdGVzdC9jb3ZlcmFnZS12OCIsICJAdml0ZXN0L3VpIiwgImhhcHB5LWRvbSIsICJqc2RvbSJdLCAiYmluIjogeyAidml0ZXN0IjogInZpdGVzdC5tanMiIH0gfSwgInNoYTUxMi02bHZqYlMzcDliNENyZENtZ3V6YmgyLzR1b1hoR0UycTcxUjRPWDVzcUY5UjFibzlYZDZmR3JNQWZ2cDV3bkN6bEJuRlZkQ09wNm9udVRRVmJvOGlVUT09Il0sCgogICAgIndoeS1pcy1ub2RlLXJ1bm5pbmciOiBbIndoeS1pcy1ub2RlLXJ1bm5pbmdAMi4zLjAiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAic2lnaW5mbyI6ICJeMi4wLjAiLCAic3RhY2tiYWNrIjogIjAuMC4yIiB9LCAiYmluIjogeyAid2h5LWlzLW5vZGUtcnVubmluZyI6ICJjbGkuanMiIH0gfSwgInNoYTUxMi1oVXJtYVdCZFZEY3h2WXFueWgwOXp1bkt6Uk9XamJaVGlOeThkQkVqa1M3ZWhFRFFpYlhKN1h2bG10Ynd1VGNsVWlJeU4rQ3lYUUQ0Vm1rbzhmTm04dz09Il0sCgogICAgIndyYXAtYW5zaSI6IFsid3JhcC1hbnNpQDcuMC4wIiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgImFuc2ktc3R5bGVzIjogIl40LjAuMCIsICJzdHJpbmctd2lkdGgiOiAiXjQuMS4wIiwgInN0cmlwLWFuc2kiOiAiXjYuMC4wIiB9IH0sICJzaGE1MTItWVZHSWoya2FtTFNUeHc2TnNaam9CeGZTd3NuMHljZGVzbWM0cCtRMjFjNXpQdVoxcGwrTmZ4VmR4UHRkSHZtTlZPUTZYU1lHNEFVdHl0L0ZpN0QxNlE9PSJdLAoKICAgICJ5MThuIjogWyJ5MThuQDUuMC44IiwgIiIsIHt9LCAic2hhNTEyLTBwZkZ6ZWdlRFdKSEpJQW1UTFJQMkR3SGpkRjVzN2pvOXR1enRkUXhBaElOQ2R2UyszbkdJTnFQZDAwQXBocUpSLzBMaEFOVVM2Lys3U0NiOThZT2ZBPT0iXSwKCiAgICAieWFyZ3MiOiBbInlhcmdzQDE3LjcuMiIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJjbGl1aSI6ICJeOC4wLjEiLCAiZXNjYWxhZGUiOiAiXjMuMS4xIiwgImdldC1jYWxsZXItZmlsZSI6ICJeMi4wLjUiLCAicmVxdWlyZS1kaXJlY3RvcnkiOiAiXjIuMS4xIiwgInN0cmluZy13aWR0aCI6ICJeNC4yLjMiLCAieTE4biI6ICJeNS4wLjUiLCAieWFyZ3MtcGFyc2VyIjogIl4yMS4xLjEiIH0gfSwgInNoYTUxMi03ZFN6elJRKytDS25OSS9rcktuWVJWN0pLS1BVWE1FaDYxc29hSEtnOW1yV0VoekZXaEZueFB4R2wrNjljRDFPdTYzQzEzTlVQQ25tSWNydnFDdU02dz09Il0sCgogICAgInlhcmdzLXBhcnNlciI6IFsieWFyZ3MtcGFyc2VyQDIxLjEuMSIsICIiLCB7fSwgInNoYTUxMi10VnBzSlc3RGRqZWNBaUZwYklCMWUzcXhJUXNFNk5vUGM1L2VUZHJiYklDNGgwTFZzV2hub2EzZyttMkhjbEJJdWpIenN4WjRWSlZBK0dVdWMyL0xCdz09Il0sCgogICAgInpvZCI6IFsiem9kQDQuNC4zIiwgIiIsIHt9LCAic2hhNTEyLXl0RU5GaklKRmwyVXdZZ2xkZTJqY2hXMkh3bTRHSkZMRGlTWFdkVHJKUUJJTjlGY3lwN240RGh4SkVpV05BSk1WMS9CcVdmVy9ra2c3MVVEY0hKeVRRPT0iXSwKCiAgICAiQHRhaWx3aW5kY3NzL294aWRlLXdhc20zMi13YXNpL0BlbW5hcGkvY29yZSI6IFsiQGVtbmFwaS9jb3JlQDEuMTAuMCIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAZW1uYXBpL3dhc2ktdGhyZWFkcyI6ICIxLjIuMSIsICJ0c2xpYiI6ICJeMi40LjAiIH0sICJidW5kbGVkIjogdHJ1ZSB9LCAic2hhNTEyLXlxNk9rSjRwODJDQWZQbDB1OW1RZWJRSEtQSmtZN1dySXVrMjA1Y1RZblllK2syWjhZQmgxMUZyYlJHL0g2aWhpcnFjYWNPZ2wyQklPOG95TVFMZVh3PT0iXSwKCiAgICAiQHRhaWx3aW5kY3NzL294aWRlLXdhc20zMi13YXNpL0BlbW5hcGkvcnVudGltZSI6IFsiQGVtbmFwaS9ydW50aW1lQDEuMTAuMCIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJ0c2xpYiI6ICJeMi40LjAiIH0sICJidW5kbGVkIjogdHJ1ZSB9LCAic2hhNTEyLWV3dllsazg2eFVvR0kwelFSTnEvbUMrMTZSMVFlRGxLUXkyMUtpM29TWVhOZ0xiNDVHVjFQNkEwTSsvczZueUN1TkRxZTVWcGFZODRCelhHd1Zid0ZBPT0iXSwKCiAgICAiQHRhaWx3aW5kY3NzL294aWRlLXdhc20zMi13YXNpL0BlbW5hcGkvd2FzaS10aHJlYWRzIjogWyJAZW1uYXBpL3dhc2ktdGhyZWFkc0AxLjIuMSIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJ0c2xpYiI6ICJeMi40LjAiIH0sICJidW5kbGVkIjogdHJ1ZSB9LCAic2hhNTEyLXVUSUk3T1lGKy9NZXMvTXJjSU9ZcDV5T3RTTUxCV1NJb0xQcGNnd2lwb2lLYmxpNmszMjJ0Y29Gc3hvSUl4UERxVzAxU1FHQWdrbzRFelppMkJOdjJ3PT0iXSwKCiAgICAiQHRhaWx3aW5kY3NzL294aWRlLXdhc20zMi13YXNpL0BuYXBpLXJzL3dhc20tcnVudGltZSI6IFsiQG5hcGktcnMvd2FzbS1ydW50aW1lQDEuMS40IiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIkB0eWJ5cy93YXNtLXV0aWwiOiAiXjAuMTAuMSIgfSwgInBlZXJEZXBlbmRlbmNpZXMiOiB7ICJAZW1uYXBpL2NvcmUiOiAiXjEuNy4xIiwgIkBlbW5hcGkvcnVudGltZSI6ICJeMS43LjEiIH0sICJidW5kbGVkIjogdHJ1ZSB9LCAic2hhNTEyLTNOUU5OZ0ExWVNsSmIva01IMWlsZEFTUDlIVzcvN2tZblJJMnN6V0phb2ZhUzFoV21iR0k0SCtkMysyMmFHelhYTjlJSituK0dpRlZjR2lwSlAxOG93PT0iXSwKCiAgICAiQHRhaWx3aW5kY3NzL294aWRlLXdhc20zMi13YXNpL0B0eWJ5cy93YXNtLXV0aWwiOiBbIkB0eWJ5cy93YXNtLXV0aWxAMC4xMC4yIiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgInRzbGliIjogIl4yLjQuMCIgfSwgImJ1bmRsZWQiOiB0cnVlIH0sICJzaGE1MTItUm9CdkoyWDB3dUtsV0ZJanJ3ZmZHdzFJcVpIS1FxekljaEthYWRaWmZuTnBzQVlwMm1NMGgzNkp0UENqTkRBSEdnWWV6LzE1dU1CcGZHd2NoaGlNZ2c9PSJdLAoKICAgICJAdGFpbHdpbmRjc3Mvb3hpZGUtd2FzbTMyLXdhc2kvdHNsaWIiOiBbInRzbGliQDIuOC4xIiwgIiIsIHsgImJ1bmRsZWQiOiB0cnVlIH0sICJzaGE1MTItb0pGdTk0SFFiK0tWZHVTVVFMN3ducG1xbmZtTHNPQS9uQWg2YjZFSDB3Q0VvSzAvbVBlWFU2YzN3S0RWODNNa091SFBSSHRTWEtLVTk5SUJhelMvMnc9PSJdLAoKICAgICJjaGFsay9zdXBwb3J0cy1jb2xvciI6IFsic3VwcG9ydHMtY29sb3JANy4yLjAiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiaGFzLWZsYWciOiAiXjQuMC4wIiB9IH0sICJzaGE1MTItcXBDQXZSbDlzdHVPSHZlS3NuN0huY0pSdnY1MDFxSWFjS3pRbE8vK0x3eGM5KzBxMndMeXY0RGZ2dDgwL0RQbjJwcU9Cc0pkRGlvZ1hHUjkrT3Z3Unc9PSJdLAoKICAgICJuZXh0L3Bvc3Rjc3MiOiBbInBvc3Rjc3NAOC40LjMxIiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIm5hbm9pZCI6ICJeMy4zLjYiLCAicGljb2NvbG9ycyI6ICJeMS4wLjAiLCAic291cmNlLW1hcC1qcyI6ICJeMS4wLjIiIH0gfSwgInNoYTUxMi1QUzA4SWJvaWE5bXRzLzJ5Z1YzZUxwWTVnaG5VY2ZMVi9FWFRPVzFFMnFZeEpLR0dCVXROak43NkZZSG5NczM2Um1BUm40MWJDMEFabW4rclIwT1ZwUT09Il0sCiAgfQp9Cg==" }, { "path": ".env.example", "kind": "text", "content": "# Your tenant public key. Get it from the desk's Developers tab.\n# `cpk_live_*` / `cpk_test_*` keys auto-route same-origin requests\n# to hosted Cimplify in both dev and prod. Anything else (`mock-dev`,\n# empty) falls back to the local `cimplify dev` mock.\nNEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev\n\n# Optional. Forces a fixed canonical URL for sitemap.xml, robots.txt,\n# OpenGraph, and llms.txt. Leave unset and the storefront derives the\n# canonical from the request `Host` header automatically.\n# NEXT_PUBLIC_SITE_URL=\n" }], "storefront-restaurant": [{ "path": "vitest.config.ts", "kind": "text", "content": 'import { defineConfig } from "vitest/config";\n\nexport default defineConfig({\n test: {\n environment: "node",\n include: ["__tests__/**/*.test.ts"],\n globals: false,\n },\n});\n' }, { "path": "__tests__/cart-flow.test.ts", "kind": "text", "content": 'import { createCartFlowSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateCartFlowSuite({ seed: brand.mock.seed, businessId: brand.mock.businessId });\n' }, { "path": "__tests__/brand.test.ts", "kind": "text", "content": 'import { createBrandSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateBrandSuite({ brand });\n' }, { "path": "__tests__/contract.test.ts", "kind": "text", "content": 'import { createContractSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateContractSuite({ seed: brand.mock.seed });\n' }, { "path": "AGENTS.md", "kind": "text", "content": '# AGENTS.md \u2014 Restaurant storefront template\n\nIf you are an AI agent (Claude, Cursor, Aider, devin, \u2026) working on this storefront, **start here.**\n\n## TL;DR for rebranding\n\n1. **Edit `lib/brand.ts`** \u2014 every visible string lives here.\n2. **Edit `app/globals.css`** \u2014 `@theme { \u2026 }` for palette + radius.\n3. **Edit `.env.local`** \u2014 set `NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY` (the only required var).\n## Aesthetic\n\n- **Lora serif + Inter** \u2014 warm, food-friendly headings + clean body.\n- **Forest green + cream** \u2014 deep sage primary, warm cream background.\n- **Mid radius**: `0.625rem`.\n- Schema.org `@type` is `Restaurant`.\n\n## Page surface\n\n```\napp/\n page.tsx Home \u2014 hero with daily-special badge,\n collection strips, category grid\n shop/page.tsx Full menu (SDK <CataloguePage/>)\n search/page.tsx Search\n collections/[slug]/page.tsx Collection landing (e.g. brunch, mains)\n categories/[slug]/page.tsx Category landing\n\n reservations/page.tsx \u2B50 Restaurant-specific: reservation widget\n (date + lunch/dinner slot + party size)\n backed by service-type "seating" products\n\n cart/page.tsx, checkout/page.tsx, orders/[id]/page.tsx\n\n account/page.tsx <CimplifyAccount /> (iframe)\n account/orders/page.tsx <CimplifyAccount section="orders" />\n account/addresses/page.tsx <CimplifyAccount section="addresses" />\n account/settings/page.tsx <CimplifyAccount section="settings" />\n login/page.tsx, signup/page.tsx redirects \u2192 /account\n\n contact/page.tsx, track-order/page.tsx\n\n about/page.tsx, faq/page.tsx\n shipping/page.tsx (delivery), returns/page.tsx (refunds), accessibility/page.tsx\n terms/page.tsx, privacy/page.tsx\n\n sitemap-page/page.tsx, sitemap.ts, robots.ts, llms.txt/route.ts, opensearch.xml/route.ts\n error.tsx, not-found.tsx\n```\n\n## File \u2194 brand-field map\n\n| File | Reads from `brand` |\n|---|---|\n| `app/layout.tsx` | identity, contact, socials, Restaurant JSON-LD |\n| `app/page.tsx` | `brand.hero` (daily-special badge) |\n| `app/about/page.tsx` | `brand.about` |\n| `app/faq/page.tsx` | `brand.faq` (reservations, dietary, delivery) |\n| `app/shipping/page.tsx` | `brand.shipping` (delivery + pickup) |\n| `app/returns/page.tsx` | `brand.returns` (refund + cancellation) |\n| `app/accessibility/page.tsx` | `brand.accessibility` |\n| `app/terms/page.tsx`, `app/privacy/page.tsx` | `brand.terms`, `brand.privacy` |\n| `app/contact/page.tsx` | `brand.contactPage`, `brand.contact` |\n| `app/track-order/page.tsx` | `brand.trackOrder` |\n| `app/account/*/page.tsx` | `brand.account` (iframe owns the UI) |\n| `app/reservations/*` | derives seating from service-type products in the catalogue |\n| `app/llms.txt/route.ts` | `brand.llms`, contact, currency |\n| `components/header.tsx`, `footer.tsx` | `brand.header`, `brand.footer`, `brand.contact`, `brand.socials` |\n\n## Restaurant-specific notes\n\n- **Reservations are service-type products.** Add seating options like "Two-top", "Four-top", "Long table for 12" to the catalogue with `type: "service"`. The `/reservations` page filters them in.\n- The reservation widget adds the chosen seating to cart with the date, time, party size, and notes as cart-item notes; checkout finalises the booking via Cimplify\'s order pipeline.\n- Schema.org `@type` is `Restaurant`. `servesCuisine` etc. can be added to the JSON-LD object in `app/layout.tsx` if needed.\n- Mock seed: `--seed restaurant` (Mama\'s Kitchen).\n\n## Known TODOs\n\n- `/reservations` widget assumes every slot is bookable. For production, wire `useAvailableSlots({ serviceId, date })` from `@cimplify/sdk/react` to filter to actually-free windows.\n- Contact form + newsletter fake their submits.\n- Hero / dish photography is Unsplash \u2014 replace with merchant photos.\n\n## Customizing SDK components\n\nFor anything beyond `lib/brand.ts` + `app/globals.css`, lean on the SDK\'s prebuilt components rather than reinvent. **Especially for product customization** (variants, add-ons, bundles, composites, services with scheduling) \u2014 the SDK already gets price math, axis matching, and cart payload contracts right. Default to ejecting and restyling:\n\n```bash\ncimplify add cart-drawer\ncimplify add add-on-selector\ncimplify add product-page\n```\n\nThen edit the local copy. **Don\'t change the cart payload shape** unless you\'re also touching the SDK mock + backend lens. Full ejection rules and the customizer contract are in the SDK-level [`AGENTS.md`](../../AGENTS.md) \u2192 "Don\'t reinvent product customization".\n\n## Quick start\n\n```bash\nbun install\nbun dev\n```\n\nOpen <http://localhost:3000>.\n' }, { "path": "postcss.config.mjs", "kind": "text", "content": 'const config = {\n plugins: {\n "@tailwindcss/postcss": {},\n },\n};\n\nexport default config;\n' }, { "path": "components/cart-pill.tsx", "kind": "text", "content": '"use client";\n\nimport { useCartDrawer } from "@cimplify/sdk/react";\nimport { useCartCount } from "@/lib/cart";\n\n/**\n * Cart pill \u2014 dynamic island. Reads the live cart count via the SDK and\n * opens the side cart drawer on click (instead of navigating to /cart).\n * Wrap in `<Suspense fallback={<CartPillSkeleton/>}>` so the cached\n * header chrome streams without blocking on the cart fetch.\n */\nexport function CartPill() {\n const { count } = useCartCount();\n const { open } = useCartDrawer();\n return (\n <button\n type="button"\n onClick={open}\n aria-label={`Open cart, ${count} ${count === 1 ? "item" : "items"}`}\n className="inline-flex items-center gap-1.5 px-4 py-2.5 sm:py-2 rounded-full bg-foreground text-background text-xs font-semibold tracking-wide transition-transform hover:scale-105 cursor-pointer"\n >\n Cart \xB7 {count}\n </button>\n );\n}\n\nexport function CartPillSkeleton() {\n return (\n <span\n aria-hidden\n className="inline-flex items-center gap-1.5 px-4 py-2.5 sm:py-2 rounded-full bg-foreground/80 text-background text-xs font-semibold tracking-wide"\n >\n Cart \xB7 \u2026\n </span>\n );\n}\n' }, { "path": "components/collection-strip.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport type { Collection, Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "./store-product-card";\n\ninterface CollectionStripProps {\n collection: Collection;\n products: Product[];\n collectionHref?: string;\n}\n\n/**\n * Horizontal strip of products under a collection title. Cards come from the\n * SDK so every variant (Food, Bundle, Composite, Service, \u2026) renders\n * correctly; clicks open the shared URL-driven product modal.\n */\nexport function CollectionStrip({ collection, products, collectionHref }: CollectionStripProps) {\n if (products.length === 0) return null;\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 pt-12">\n <header className="grid grid-cols-[1fr_auto] items-end gap-4 mb-5">\n <h2 className="font-serif text-[28px] font-semibold m-0">{collection.name}</h2>\n {collectionHref && (\n <Link\n href={collectionHref}\n className="text-[13px] font-semibold text-primary hover:underline"\n >\n See all \u2192\n </Link>\n )}\n {collection.description && (\n <p className="col-span-full m-0 mt-1 text-sm text-muted-foreground">\n {collection.description}\n </p>\n )}\n </header>\n <div className="grid grid-flow-col auto-cols-[minmax(220px,1fr)] gap-4 overflow-x-auto snap-x snap-mandatory pb-2">\n {products.slice(0, 8).map((p) => (\n <div key={p.id} className="snap-start">\n <StoreProductCard product={p} />\n </div>\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "components/hero.tsx", "kind": "text", "content": 'interface HeroProps {\n badge?: string;\n title: string;\n subtitle?: string;\n}\n\nexport function Hero({ badge, title, subtitle }: HeroProps) {\n return (\n <section className="px-6 sm:px-8 py-16 text-center bg-gradient-to-b from-accent to-background">\n {badge && (\n <span className="inline-block mb-4 px-3.5 py-1.5 rounded-full bg-card border border-accent text-accent-foreground text-[11px] font-semibold uppercase tracking-[0.12em]">\n {badge}\n </span>\n )}\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold m-0 -tracking-[0.02em]">\n {title}\n </h1>\n {subtitle && (\n <p className="mx-auto mt-2 max-w-xl text-base text-muted-foreground">\n {subtitle}\n </p>\n )}\n </section>\n );\n}\n' }, { "path": "components/account-iframe.tsx", "kind": "text", "content": '"use client";\n\nimport { CimplifyAccount } from "@cimplify/sdk/react";\n\n/**\n * Cimplify Account portal \u2014 iframe-mounted UI hosted by Cimplify Link.\n * Handles sign-in, sign-up, OTP, addresses, payment methods, sessions,\n * and order history. The iframe owns auth state; we just choose which\n * `section` to land on.\n */\nexport function AccountIframe({ section }: { section?: string }) {\n return <CimplifyAccount section={section} />;\n}\n' }, { "path": "components/policy-page.tsx", "kind": "text", "content": 'import type { BrandPolicySection } from "@/lib/brand";\n\ninterface PolicyShape {\n eyebrow: string;\n title: string;\n lastUpdated?: string;\n sections: BrandPolicySection[];\n}\n\n/**\n * Shared layout for shipping / returns / accessibility / terms / privacy.\n * Reads a `{ eyebrow, title, lastUpdated, sections[] }` block from brand.\n */\nexport function PolicyPage({ policy }: { policy: PolicyShape }) {\n return (\n <article className="max-w-3xl mx-auto px-6 sm:px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {policy.eyebrow}\n </p>\n <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-2 -tracking-[0.02em]">\n {policy.title}\n </h1>\n {policy.lastUpdated && (\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {policy.lastUpdated}\n </p>\n )}\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {policy.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="text-2xl font-semibold mt-0">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "components/header.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport { Suspense } from "react";\nimport { NavLink } from "./nav-link";\nimport { CartPill, CartPillSkeleton } from "./cart-pill";\nimport { MobileNav } from "./mobile-nav";\nimport { brand } from "@/lib/brand";\n\n/**\n * Server-rendered header chrome. Brand mark + nav layout streams from the\n * cache; the active-link styling and live cart count are dynamic islands\n * mounted in their own Suspense boundaries so the chrome never blocks.\n */\nexport function Header() {\n return (\n <header className="sticky top-0 z-30 flex items-center justify-between px-6 sm:px-8 py-4 border-b border-border bg-background/90 backdrop-blur-md">\n <Link href="/" className="flex items-baseline gap-2">\n <span className="font-serif text-[22px] font-semibold -tracking-[0.02em]">\n {brand.shortName}\n </span>\n <span className="hidden sm:inline text-[11px] font-medium uppercase tracking-[0.12em] text-muted-foreground">\n {brand.microTag}\n </span>\n </Link>\n <div className="flex items-center gap-3 sm:gap-6">\n <nav className="hidden sm:flex items-center gap-6">\n {brand.header.nav.map((link) => (\n <Suspense key={link.href} fallback={<NavLinkFallback>{link.label}</NavLinkFallback>}>\n <NavLink href={link.href}>{link.label}</NavLink>\n </Suspense>\n ))}\n </nav>\n <Suspense fallback={<CartPillSkeleton />}>\n <CartPill />\n </Suspense>\n <div className="sm:hidden">\n <MobileNav />\n </div>\n </div>\n </header>\n );\n}\n\nfunction NavLinkFallback({ children }: { children: React.ReactNode }) {\n return (\n <span className="text-[13px] font-medium tracking-wide text-muted-foreground">\n {children}\n </span>\n );\n}\n' }, { "path": "components/product-modal.tsx", "kind": "text", "content": '"use client";\n\nimport { useEffect } from "react";\nimport Image from "next/image";\nimport { useRouter, useSearchParams, usePathname } from "next/navigation";\nimport { ProductSheet, useProduct, useCart } from "@cimplify/sdk/react";\n\n/**\n * URL-driven product modal. Reads `?product=<slug>` and renders the SDK\'s\n * `<ProductSheet/>` \u2014 vertical layout with image-on-top, then header, then\n * the variant/add-on/composite/bundle customizer. Closing the modal clears\n * the search param. Deep-linkable and survives reloads.\n */\nexport function ProductModal() {\n const router = useRouter();\n const pathname = usePathname();\n const searchParams = useSearchParams();\n const slug = searchParams?.get("product") ?? null;\n\n const { product } = useProduct(slug ?? "", { enabled: Boolean(slug) });\n const { addItem } = useCart();\n\n useEffect(() => {\n if (!slug) return;\n const original = document.body.style.overflow;\n document.body.style.overflow = "hidden";\n return () => {\n document.body.style.overflow = original;\n };\n }, [slug]);\n\n useEffect(() => {\n if (!slug) return;\n const onKey = (e: KeyboardEvent) => {\n if (e.key === "Escape") close();\n };\n window.addEventListener("keydown", onKey);\n return () => window.removeEventListener("keydown", onKey);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [slug]);\n\n if (!slug) return null;\n\n function close() {\n const next = new URLSearchParams(searchParams?.toString() ?? "");\n next.delete("product");\n const qs = next.toString();\n router.replace(qs ? `${pathname}?${qs}` : pathname, { scroll: false });\n }\n\n return (\n <div\n role="dialog"\n aria-modal="true"\n onClick={close}\n className="fixed inset-0 z-[100] flex items-end sm:items-center justify-center sm:p-6 bg-foreground/50 backdrop-blur-sm"\n >\n <div\n onClick={(e) => e.stopPropagation()}\n className="relative w-full sm:max-w-lg max-h-[92vh] overflow-auto bg-card sm:rounded-3xl rounded-t-3xl shadow-2xl"\n >\n <button\n onClick={close}\n aria-label="Close product details"\n className="absolute top-4 right-4 z-10 w-9 h-9 rounded-full bg-card border border-border text-sm cursor-pointer transition-colors hover:bg-muted"\n >\n \u2715\n </button>\n {product ? (\n <ProductSheet\n product={product}\n onClose={close}\n onAddToCart={async (p, qty, options) => {\n await addItem(p, qty, options);\n close();\n }}\n renderImage={({ src, alt, className }) => (\n <Image\n src={src}\n alt={alt}\n width={1200}\n height={900}\n className={className}\n style={{ width: "100%", height: "auto", objectFit: "cover" }}\n priority\n />\n )}\n classNames={{\n root: "p-6 sm:p-8 gap-4",\n image: "rounded-2xl overflow-hidden -mx-6 sm:-mx-8 -mt-6 sm:-mt-8 mb-2",\n header: "flex items-baseline justify-between gap-4",\n name: "font-serif text-2xl font-semibold m-0",\n price: "text-lg font-semibold text-primary",\n description: "text-sm text-muted-foreground leading-relaxed",\n customizer: "pt-2",\n }}\n />\n ) : (\n <div className="p-8 text-center text-muted-foreground">Loading\u2026</div>\n )}\n </div>\n </div>\n );\n}\n' }, { "path": "components/mobile-nav.tsx", "kind": "text", "content": '"use client";\n\nimport { useEffect, useState } from "react";\nimport Link from "next/link";\nimport { brand } from "@/lib/brand";\n\n/**\n * Hamburger button + slide-in drawer for narrow viewports. Header hides\n * its inline nav links below `sm` and renders this in their place; the\n * cart pill stays in the header chrome.\n */\nexport function MobileNav() {\n const [open, setOpen] = useState(false);\n\n useEffect(() => {\n if (!open) return;\n const onKey = (event: KeyboardEvent) => {\n if (event.key === "Escape") setOpen(false);\n };\n document.addEventListener("keydown", onKey);\n const previousOverflow = document.body.style.overflow;\n document.body.style.overflow = "hidden";\n return () => {\n document.removeEventListener("keydown", onKey);\n document.body.style.overflow = previousOverflow;\n };\n }, [open]);\n\n return (\n <>\n <button\n type="button"\n onClick={() => setOpen(true)}\n aria-label="Open menu"\n aria-expanded={open}\n aria-controls="mobile-nav-drawer"\n className="grid place-items-center w-11 h-11 -mr-2 rounded-md text-foreground hover:bg-muted transition-colors"\n >\n <svg\n width="20"\n height="20"\n viewBox="0 0 24 24"\n fill="none"\n stroke="currentColor"\n strokeWidth="2"\n strokeLinecap="round"\n strokeLinejoin="round"\n aria-hidden="true"\n >\n <line x1="3" y1="6" x2="21" y2="6" />\n <line x1="3" y1="12" x2="21" y2="12" />\n <line x1="3" y1="18" x2="21" y2="18" />\n </svg>\n </button>\n\n {open ? (\n <div className="fixed inset-0 z-50 sm:hidden">\n <button\n type="button"\n onClick={() => setOpen(false)}\n aria-label="Close menu"\n className="absolute inset-0 bg-background/80 backdrop-blur-sm"\n />\n <nav\n id="mobile-nav-drawer"\n aria-label="Mobile navigation"\n className="absolute inset-y-0 right-0 w-[85%] max-w-sm flex flex-col bg-background border-l border-border shadow-2xl"\n >\n <div className="flex items-center justify-between px-6 py-4 border-b border-border">\n <span className="text-xs font-semibold uppercase tracking-[0.16em] text-muted-foreground">\n Menu\n </span>\n <button\n type="button"\n onClick={() => setOpen(false)}\n aria-label="Close menu"\n className="grid place-items-center w-11 h-11 -mr-2 rounded-md text-foreground hover:bg-muted transition-colors"\n >\n <svg\n width="20"\n height="20"\n viewBox="0 0 24 24"\n fill="none"\n stroke="currentColor"\n strokeWidth="2"\n strokeLinecap="round"\n strokeLinejoin="round"\n aria-hidden="true"\n >\n <line x1="18" y1="6" x2="6" y2="18" />\n <line x1="6" y1="6" x2="18" y2="18" />\n </svg>\n </button>\n </div>\n <ul className="flex flex-col gap-1 px-3 py-4">\n {brand.header.nav.map((link) => (\n <li key={link.href}>\n <Link\n href={link.href}\n onClick={() => setOpen(false)}\n className="block px-3 py-3 rounded-md text-base font-medium text-foreground hover:bg-muted transition-colors"\n >\n {link.label}\n </Link>\n </li>\n ))}\n </ul>\n </nav>\n </div>\n ) : null}\n </>\n );\n}\n' }, { "path": "components/providers.tsx", "kind": "text", "content": '"use client";\n\nimport { useMemo, type ReactNode } from "react";\nimport { createCimplifyClient } from "@cimplify/sdk";\nimport { CimplifyProvider, CartDrawerProvider } from "@cimplify/sdk/react";\n\n// Same-origin client \u2014 every request goes through the Next.js rewrite in\n// next.config.ts, so no CORS preflight ever hits the browser.\nexport function Providers({ children }: { children: ReactNode }) {\n const client = useMemo(() => {\n const baseUrl =\n typeof window !== "undefined" ? window.location.origin : "http://127.0.0.1:8787";\n const publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY ?? "mock-dev";\n return createCimplifyClient({\n baseUrl,\n publicKey,\n suppressPublicKeyWarning: true,\n });\n }, []);\n\n return (\n <CimplifyProvider client={client}>\n <CartDrawerProvider>{children}</CartDrawerProvider>\n </CimplifyProvider>\n );\n}\n' }, { "path": "components/footer.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport { brand } from "@/lib/brand";\n\nconst ICONS: Record<string, React.ReactNode> = {\n instagram: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" aria-hidden className="w-5 h-5">\n <rect x="3" y="3" width="18" height="18" rx="5" />\n <circle cx="12" cy="12" r="4" />\n <circle cx="17.5" cy="6.5" r="0.75" fill="currentColor" />\n </svg>\n ),\n x: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M18 2h3l-7.5 8.6L22 22h-6.6l-5-6.5L4 22H1l8-9.2L1.4 2H8l4.6 6 5.4-6z" />\n </svg>\n ),\n tiktok: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M16 3v3.5a4.5 4.5 0 0 0 4.5 4.5V14a7.5 7.5 0 0 1-4.5-1.5V16a5 5 0 1 1-5-5v3a2 2 0 1 0 2 2V3z" />\n </svg>\n ),\n facebook: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M14 9V7a1 1 0 0 1 1-1h2V3h-3a4 4 0 0 0-4 4v2H8v3h2v9h3v-9h2.5l.5-3H13z" />\n </svg>\n ),\n youtube: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M22.5 6.5a2.6 2.6 0 0 0-1.8-1.8C19 4.2 12 4.2 12 4.2s-7 0-8.7.5A2.6 2.6 0 0 0 1.5 6.5C1 8.2 1 12 1 12s0 3.8.5 5.5a2.6 2.6 0 0 0 1.8 1.8C5 19.8 12 19.8 12 19.8s7 0 8.7-.5a2.6 2.6 0 0 0 1.8-1.8c.5-1.7.5-5.5.5-5.5s0-3.8-.5-5.5zM10 15.5v-7l6 3.5-6 3.5z" />\n </svg>\n ),\n linkedin: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M4 4h4v16H4zM6 2.5a2 2 0 1 0 0 4 2 2 0 0 0 0-4zM10 8h4v2.5h.1c.6-1.1 2-2.5 4-2.5 4 0 4.9 2.6 4.9 6V20h-4v-5c0-1.5-.5-3-2.3-3-1.7 0-2.5 1.3-2.5 3v5h-4z" />\n </svg>\n ),\n whatsapp: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M12 2a10 10 0 0 0-8.6 15l-1.4 5 5.2-1.4A10 10 0 1 0 12 2zm5 14.2c-.2.6-1.2 1.2-1.7 1.2-.5.1-1.1.1-1.7-.1-.4-.1-.9-.3-1.5-.6a8.4 8.4 0 0 1-3.7-3.4c-.7-1-1-1.8-1-2.5 0-.7.4-1.1.6-1.3.2-.2.4-.2.5-.2h.4c.1 0 .3 0 .4.3l.6 1.4c.1.2 0 .3 0 .4l-.3.4-.3.3c-.1.1-.2.2-.1.4.2.4.7 1.1 1.4 1.8.9.8 1.7 1.1 1.9 1.2.2.1.3.1.5-.1l.6-.7c.2-.2.3-.2.5-.1l1.4.7c.2.1.3.2.4.3.1.2.1.6 0 1z" />\n </svg>\n ),\n};\n\nconst FALLBACK_ICON = (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" aria-hidden className="w-5 h-5">\n <circle cx="12" cy="12" r="9" />\n </svg>\n);\n\nexport async function Footer() {\n "use cache";\n const year = new Date().getFullYear();\n return (\n <footer className="mt-12 px-6 sm:px-8 py-10 text-xs text-muted-foreground border-t border-border bg-card">\n <div className="max-w-7xl mx-auto">\n <div className="grid gap-10 md:grid-cols-[1.4fr_repeat(4,1fr)]">\n <div>\n <p className="font-serif text-2xl text-foreground m-0 mb-2">{brand.name}</p>\n <p className="leading-relaxed mb-4 max-w-sm">{brand.footer.blurb}</p>\n <address className="not-italic space-y-1">\n <p className="m-0">{brand.contact.address}</p>\n <p className="m-0">\n <a\n href={`tel:${brand.contact.phoneTel}`}\n className="hover:text-foreground transition-colors"\n >\n {brand.contact.phone}\n </a>\n </p>\n <p className="m-0">\n <a\n href={`mailto:${brand.contact.email}`}\n className="hover:text-foreground transition-colors"\n >\n {brand.contact.email}\n </a>\n </p>\n <p className="m-0">{brand.contact.hours}</p>\n </address>\n <div className="flex items-center gap-3 mt-5">\n {brand.socials.map((s) => (\n <a\n key={s.label}\n href={s.href}\n aria-label={s.label}\n target="_blank"\n rel="noopener noreferrer"\n className="inline-flex items-center justify-center w-9 h-9 rounded-full border border-border text-muted-foreground hover:text-foreground hover:border-foreground transition-colors"\n >\n {(s.icon && ICONS[s.icon]) ?? FALLBACK_ICON}\n </a>\n ))}\n </div>\n </div>\n {brand.footer.sitemap.map((section) => (\n <nav key={section.title} aria-labelledby={`footer-${section.title}`}>\n <p\n id={`footer-${section.title}`}\n className="font-semibold text-foreground mb-3 text-[13px] uppercase tracking-[0.08em]"\n >\n {section.title}\n </p>\n <ul className="space-y-2 m-0 p-0 list-none">\n {section.links.map((link) => (\n <li key={link.label}>\n <Link\n href={link.href}\n className="hover:text-foreground transition-colors"\n >\n {link.label}\n </Link>\n </li>\n ))}\n </ul>\n </nav>\n ))}\n </div>\n <div className="mt-12 pt-6 border-t border-border flex flex-col sm:flex-row items-center justify-between gap-3">\n <p className="m-0">\xA9 {year} {brand.name}. All rights reserved.</p>\n {brand.footer.poweredBy && (\n <p className="m-0 inline-flex items-center gap-1.5">\n <span className="text-muted-foreground/80">Powered by</span>\n <a\n href={brand.footer.poweredBy.href}\n target="_blank"\n rel="noopener noreferrer"\n aria-label={brand.footer.poweredBy.label}\n className="inline-flex items-center gap-1 font-serif text-foreground hover:text-primary transition-colors"\n >\n <span className="font-semibold tracking-tight">{brand.footer.poweredBy.label}</span>\n <svg\n viewBox="0 0 12 12"\n aria-hidden\n className="w-3 h-3 opacity-70"\n fill="none"\n stroke="currentColor"\n strokeWidth="1.5"\n >\n <path d="M3 9L9 3M9 3H4M9 3v5" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n </a>\n </p>\n )}\n </div>\n </div>\n </footer>\n );\n}\n' }, { "path": "components/store-product-card.tsx", "kind": "text", "content": '"use client";\n\nimport Link from "next/link";\nimport {\n CardVariant,\n FoodProductCard,\n RetailProductCard,\n WholesaleProductCard,\n DigitalProductCard,\n BundleProductCard,\n CompositeProductCard,\n StandardServiceCard,\n CompactServiceCard,\n ScheduleServiceCard,\n RentalServiceCard,\n AccommodationCard,\n LeaseServiceCard,\n SubscriptionCard,\n} from "@cimplify/sdk/react";\nimport type { CardLayoutProps } from "@cimplify/sdk/react";\nimport type { Product } from "@cimplify/sdk";\nimport { PRODUCT_TYPE, RENDER_HINT, DURATION_UNIT } from "@cimplify/sdk";\n\nconst RENTAL_UNITS = new Set<string>([DURATION_UNIT.Days, DURATION_UNIT.Hours]);\nconst LEASE_UNITS = new Set<string>([DURATION_UNIT.Weeks, DURATION_UNIT.Months, DURATION_UNIT.Years]);\n\nconst VARIANT_CARDS: Record<CardVariant, React.ComponentType<CardLayoutProps>> = {\n [CardVariant.Food]: FoodProductCard,\n [CardVariant.Retail]: RetailProductCard,\n [CardVariant.Wholesale]: WholesaleProductCard,\n [CardVariant.Digital]: DigitalProductCard,\n [CardVariant.Bundle]: BundleProductCard,\n [CardVariant.Composite]: CompositeProductCard,\n [CardVariant.Standard]: StandardServiceCard,\n [CardVariant.Compact]: CompactServiceCard,\n [CardVariant.Schedule]: ScheduleServiceCard,\n [CardVariant.Rental]: RentalServiceCard,\n [CardVariant.Accommodation]: AccommodationCard,\n [CardVariant.Lease]: LeaseServiceCard,\n [CardVariant.Subscription]: SubscriptionCard,\n};\n\nfunction resolveVariant(p: Product): CardVariant {\n if (p.type === PRODUCT_TYPE.Bundle) return CardVariant.Bundle;\n if (p.type === PRODUCT_TYPE.Composite) return CardVariant.Composite;\n if (p.quantity_pricing && p.quantity_pricing.length > 1) return CardVariant.Wholesale;\n if (p.type === PRODUCT_TYPE.Digital) return CardVariant.Digital;\n if (p.type === PRODUCT_TYPE.Service) {\n if (p.duration_unit && RENTAL_UNITS.has(p.duration_unit)) return CardVariant.Rental;\n if (p.duration_unit === DURATION_UNIT.Nights) return CardVariant.Accommodation;\n if (p.duration_unit && LEASE_UNITS.has(p.duration_unit)) return CardVariant.Lease;\n if (p.billing_plans && p.billing_plans.length > 0 && !p.duration_minutes) return CardVariant.Subscription;\n return CardVariant.Standard;\n }\n if (p.render_hint === RENDER_HINT.Food) return CardVariant.Food;\n return CardVariant.Retail;\n}\n\ninterface Props {\n product: Product;\n /** Override the auto-detected card variant. */\n variant?: CardVariant;\n}\n\n/**\n * Variant-aware product card that opens the URL-driven product modal on\n * click. Uses `next/link` with `?product=<slug>` so the link is statically\n * pre-renderable \u2014 no `useSearchParams` dependency means cards don\'t\n * become dynamic islands. The modal (which already reads `useSearchParams`\n * inside a Suspense boundary in the root layout) handles the rest.\n */\nexport function StoreProductCard({ product, variant }: Props) {\n const slug = product.slug || product.id;\n const Card = VARIANT_CARDS[variant ?? resolveVariant(product)];\n const href = `?product=${encodeURIComponent(slug)}`;\n\n return (\n <Card\n product={product}\n renderLink={({ className, children }) => (\n <Link href={href} scroll={false} prefetch={false} className={className}>\n {children}\n </Link>\n )}\n />\n );\n}\n' }, { "path": "components/json-ld.tsx", "kind": "text", "content": 'import { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\n\n/**\n * Streams the organization JSON-LD script tag in async so it doesn\'t block\n * the rest of the layout shell. `getSiteUrl` reads request headers, and\n * any await on dynamic data inside `RootLayout` makes Next 16 mark the\n * whole route as blocking.\n */\nexport async function OrganizationJsonLd(): Promise<React.ReactElement> {\n const siteUrl = await getSiteUrl();\n const ld = {\n "@context": "https://schema.org",\n "@type": brand.schemaType,\n name: brand.name,\n url: siteUrl,\n description: brand.description,\n email: brand.contact.email,\n telephone: brand.contact.phoneTel,\n address: {\n "@type": "PostalAddress",\n streetAddress: brand.contact.streetAddress,\n addressLocality: brand.contact.city,\n addressCountry: brand.contact.countryCode,\n },\n sameAs: brand.socials.map((s) => s.href),\n };\n return (\n <script\n type="application/ld+json"\n dangerouslySetInnerHTML={{ __html: JSON.stringify(ld) }}\n />\n );\n}\n' }, { "path": "components/nav-link.tsx", "kind": "text", "content": '"use client";\n\nimport Link from "next/link";\nimport { usePathname } from "next/navigation";\n\nexport function NavLink({ href, children }: { href: string; children: React.ReactNode }) {\n const pathname = usePathname();\n const active = pathname === href;\n return (\n <Link\n href={href}\n className={[\n "text-[13px] font-medium tracking-wide transition-colors",\n active ? "text-primary" : "text-muted-foreground hover:text-foreground",\n ].join(" ")}\n >\n {children}\n </Link>\n );\n}\n' }, { "path": "components/category-grid.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CategoryGrid as SdkCategoryGrid } from "@cimplify/sdk/react";\nimport type { Category } from "@cimplify/sdk";\n\n/**\n * Homepage category tiles \u2014 defers to the SDK\'s <CategoryGrid/> for layout\n * and accessibility, and routes selections to /categories/:slug.\n */\nexport function CategoryGrid({ categories }: { categories?: Category[] }) {\n const router = useRouter();\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 pt-14">\n <h2 className="font-serif text-[28px] font-semibold m-0 mb-5">Browse the menu</h2>\n <SdkCategoryGrid\n categories={categories}\n onSelect={(c) => router.push(`/categories/${c.slug}`)}\n classNames={{\n item: "flex flex-col gap-1 p-6 bg-card border border-border rounded-2xl cursor-pointer text-left transition-all hover:border-primary hover:-translate-y-0.5",\n name: "font-serif text-lg font-semibold text-foreground",\n description: "text-xs text-muted-foreground",\n count: "text-xs text-muted-foreground mt-1",\n }}\n />\n </section>\n );\n}\n' }, { "path": "components/cart-drawer.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CartDrawer as SdkCartDrawer } from "@cimplify/sdk/react";\n\n/**\n * Side-drawer cart. Auto-opens when an item is added (via\n * `<CartDrawerProvider>` in `providers.tsx`). The header\'s cart pill\n * also calls `useCartDrawer().open()` to reveal it on click.\n */\nexport function CartDrawer() {\n const router = useRouter();\n return <SdkCartDrawer onCheckout={() => router.push("/checkout")} />;\n}\n' }, { "path": "next.config.ts", "kind": "text", "content": 'import type { NextConfig } from "next";\n\n// Same-origin proxy target for the storefront API. The public key\n// decides: a real `cpk_live_\u2026` / `cpk_test_\u2026` only resolves against\n// hosted Cimplify, so browser cart writes go to the same place the\n// server catalogue reads come from. Anything else (`mock-dev`, empty)\n// falls back to the local `cimplify dev` mock in dev.\nconst publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY?.trim() ?? "";\nconst keyTargetsHostedCimplify =\n publicKey.startsWith("cpk_live_") || publicKey.startsWith("cpk_test_");\nconst STOREFRONT_URL =\n process.env.NODE_ENV === "production" || keyTargetsHostedCimplify\n ? "https://storefronts.cimplify.io"\n : "http://127.0.0.1:8787";\n\nif (STOREFRONT_URL === "http://127.0.0.1:8787") {\n console.warn(\n "[cimplify] next.config resolved the local storefront API URL; production deploys must run with NODE_ENV=production.",\n );\n}\n\nconst nextConfig: NextConfig = {\n // Enable Next 16\'s `cacheComponents` mode so we can use `\'use cache\'` +\n // `cacheTag` / `cacheLife` for SSR caching. Cached chrome streams first,\n // dynamic Suspense boundaries fill in when ready.\n cacheComponents: true,\n async redirects() {\n // Config-level so cacheComponents needn\'t prerender a redirect()-only page.\n return [\n { source: "/login", destination: "/account", permanent: false },\n { source: "/signup", destination: "/account", permanent: false },\n ];\n },\n async rewrites() {\n return [\n { source: "/api/v1/:path*", destination: `${STOREFRONT_URL}/api/v1/:path*` },\n { source: "/img/:path*", destination: `${STOREFRONT_URL}/img/:path*` },\n { source: "/elements/:path*", destination: `${STOREFRONT_URL}/elements/:path*` },\n { source: "/_mock/:path*", destination: `${STOREFRONT_URL}/_mock/:path*` },\n ];\n },\n images: {\n loader: "custom",\n loaderFile: "./lib/cimplify-loader.ts",\n remotePatterns: [\n { protocol: "http", hostname: "127.0.0.1", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "3000", pathname: "/img/**" },\n { protocol: "https", hostname: "loremflickr.com", pathname: "/**" },\n { protocol: "https", hostname: "images.unsplash.com", pathname: "/**" },\n { protocol: "https", hostname: "static-tmp.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "storefrontassetscdn.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "cdn.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "res.cloudinary.com", pathname: "/cimplify/**" },\n ],\n },\n};\n\nexport default nextConfig;\n' }, { "path": ".claude/skills/cimplify-storefront/SKILL.md", "kind": "text", "content": "---\nname: cimplify-storefront\ndescription: Build, customize, rebrand, or deploy a Cimplify-scaffolded storefront. Triggers when the user asks to create a storefront, rebrand a Cimplify template, change the palette, add a page, deploy to Cimplify, or works in a project containing `lib/brand.ts` and `next.config.ts` with `cacheComponents`.\n---\n\n# Cimplify Storefront skill\n\nYou're working on a project scaffolded from `cimplify init`. The architecture is opinionated and the rebrand surface is intentionally small. Read `AGENTS.md` at the project root for the file \u2194 brand-field map for *this template's* industry; this skill gives you the playbook that's the same across all six.\n\n## The contract \u2014 never break\n\n1. **`lib/brand.ts` is the only place for content edits.** Every visible string reads from this file. If a string isn't in `brand`, *add a field* to the `Brand` interface \u2014 don't hardcode it in a page or component.\n2. **`app/globals.css` `@theme { \u2026 }`** holds palette + radius + font references. To re-skin the entire site, edit only this block.\n3. **Server Components are cached** via `'use cache'` + `cacheTag(tags.X())` + `cacheLife(\"hours\")`. Don't downgrade them to `\"use client\"` to avoid an issue \u2014 fix the issue.\n4. **Client islands** (anything reading `useSearchParams`, `usePathname`, `useRouter`, `useState`) live behind `<Suspense>`.\n5. **`cacheComponents: true`** in `next.config.ts` is non-negotiable. Don't turn it off.\n6. **`bun run test:run` (vitest)** is the canonical test runner. `bun test` will show false failures because Bun's `vi` shim is incomplete.\n7. **Cart, checkout, orders stay client.** They're session-bound. Don't try to SSR them.\n\n## The brand-field schema (in `lib/brand.ts`)\n\n```\nidentity: name, shortName, microTag, description, schemaType, currency, locale\ncontact: address, streetAddress, city, countryCode, phone, phoneTel, email, privacyEmail, hours\nsocials: [{ label, href, icon }] // icon \u2208 instagram|x|tiktok|facebook|youtube|linkedin|whatsapp\nheader: { nav: [{ label, href }] }\nhero: { badge, title, subtitle, primaryCtaLabel, secondaryCtaLabel?, secondaryCtaHref? }\noptional: trustItems[]?, brandStrip?, promo?, tradeIn? // render conditionally\nnewsletter: { eyebrow, title, body, placeholder, submitLabel, successLabel }\nabout: { eyebrow, title, paragraphs[], sections[{ heading, body }] }\nfaq: { eyebrow, title, sections[{ title, items[{ q, a }] }], contactPrompt, contactEmail }\nterms,\nprivacy,\nshipping,\nreturns,\naccessibility: { eyebrow, title, lastUpdated, sections[{ heading, body | { intro, bullets[] } }] }\naccount: eyebrows + titles for /account, /login, /signup\ncontactPage: { eyebrow, title, body, reasons[], directLines[{ label, value, href }] }\ntrackOrder: { eyebrow, title, body }\nfooter: { blurb, sitemap[{ title, links[] }], poweredBy? }\nllms: { summary } // opens /llms.txt\nmock: { seed, businessId }\n```\n\n## Playbook \u2014 common tasks\n\n### Rebrand a storefront for a new merchant\n\n1. Edit `lib/brand.ts`. Replace every field with the merchant's content. Use the brief / context the user gave you.\n2. Edit `app/globals.css` `@theme { \u2026 }`. Change `--color-primary`, `--color-background`, `--color-foreground`, optionally `--radius`. Use OKLCH; if the brand only gave hex, convert.\n3. (Optional) Swap fonts in `app/layout.tsx` \u2014 `next/font/google` import + the variable wired into the `<html>` className.\n4. Set `.env.local`: `NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY` (optional: `NEXT_PUBLIC_SITE_URL`).\n\nDon't touch any other file. If the rebrand needs content not in the schema, add the field to `Brand` first, populate it, then read it from the page.\n\n### Add a new section / component\n\n1. Build it as a Server Component in `components/` (or a client island in `*-client.tsx` if interactive).\n2. Read merchant copy from `brand`. Add new fields to the `Brand` interface if needed.\n3. Wrap interactive bits in `<Suspense fallback={\u2026}>` so the cached chrome streams.\n4. Compose into the page.\n\n### Wire a Server Action that mutates data\n\n```ts\n\"use server\";\nimport { getServerClient, revalidateProducts } from \"@cimplify/sdk/server\";\n\nexport async function createProduct(input: ProductInput) {\n await getServerClient().catalogue.createProduct(input);\n revalidateProducts();\n}\n```\n\nAfter every mutation, call the matching `revalidate*` helper from `@cimplify/sdk/server`: `revalidateProducts`, `revalidateProduct(id)`, `revalidateCategories`, `revalidateCategory(id)`, `revalidateCollections`, `revalidateCollection(id)`, `revalidateBusiness`.\n\n### Add a Server Component data fetch\n\n```ts\nimport { cacheTag, cacheLife } from \"next/cache\";\nimport { getServerClient, tags } from \"@cimplify/sdk/server\";\n\nasync function getX() {\n \"use cache\";\n cacheTag(tags.products());\n cacheLife(\"hours\");\n const r = await getServerClient().catalogue.getProducts({ limit: 24 });\n if (!r.ok) throw new Error(r.error.message);\n return r.value.items;\n}\n```\n\n### Eject an SDK component for deeper customization\n\nTry `classNames`, `renderImage`, `renderLink`, slot props first. If those run out:\n\n```bash\ncimplify list\ncimplify add product-card --dir src/components/cimplify\n```\n\nOnce ejected, the file is yours. Hooks/types still come from `@cimplify/sdk`.\n\n### Deploy\n\n```bash\ncimplify login\ncimplify projects create my-store\ncimplify link <project-id>\ncimplify env push\ncimplify deploy --prod\ncimplify logs --follow\ncimplify domains add my-store.com\n```\n\n## Pitfalls \u2014 explicit \u274C list\n\n- \u274C Hardcoding any visible string in a page or component. Always `brand.X`.\n- \u274C Disabling `cacheComponents: true` to silence a warning. Fix the warning by wrapping the dynamic call in `<Suspense>`.\n- \u274C Using `unstable_cache` \u2014 Next 16's canonical primitive is `'use cache'` + `cacheTag` + `cacheLife`.\n- \u274C Bypassing `getServerClient()` and calling `createCimplifyClient` directly in a Server Component \u2014 loses per-request memoization.\n- \u274C Mutating data without calling the matching `revalidate*` helper.\n- \u274C Adding an `app/error.tsx` handler that calls `reset()` without logging \u2014 silently swallowed errors hide bugs.\n- \u274C Running `bun test` and reporting failures. Use `bun run test:run` (vitest).\n- \u274C Editing the per-template `AGENTS.md` to remove notes about TODOs (contact form, newsletter fake submits) \u2014 they're real.\n\n## Where things live\n\n| Need | Look here |\n|---|---|\n| Which page reads which `brand.X` field | `AGENTS.md` at project root |\n| Architectural rules | `AGENTS.md` at project root + this skill |\n| Running locally | `bun dev` (boots mock + Next together) |\n| Switching mock seed | edit `dev:mock` in `package.json` |\n| Full SDK reference | `docs/sdk/storefronts.md` in the Cimplify repo |\n\n## What to do when the user asks something out-of-scope\n\nIf the user asks for something the template doesn't support out of the box:\n\n1. **Check first** \u2014 is there an SDK component or hook that does it? `@cimplify/sdk/react` has 50+ components, 30+ hooks. Search `node_modules/@cimplify/sdk/dist/react.d.mts` if needed.\n2. **Compose from existing parts** before authoring new ones.\n3. **If genuinely missing** \u2014 build the new component, hoist its strings into `brand.ts`, document in `AGENTS.md`.\n\nDon't introduce competing patterns (a new caching strategy, a new auth flow, a new theming system). The template's opinions are intentional.\n" }, { "path": ".cursor/rules/cimplify-storefront.mdc", "kind": "binary", "content": "LS0tCmRlc2NyaXB0aW9uOiBDaW1wbGlmeSBzdG9yZWZyb250IOKAlCBhcmNoaXRlY3R1cmFsIHJ1bGVzIGFuZCByZWJyYW5kIGNvbnRyYWN0IGZvciBhIHByb2plY3Qgc2NhZmZvbGRlZCBmcm9tIGBjaW1wbGlmeSBpbml0YC4gVHJpZ2dlcnMgd2hlbiBmaWxlcyBsaWtlIGxpYi9icmFuZC50cywgYXBwL2dsb2JhbHMuY3NzLCBjb21wb25lbnRzL2hlYWRlci50c3gsIG9yIC5jaW1wbGlmeS9wcm9qZWN0Lmpzb24gYXJlIGluIHRoZSB3b3Jrc3BhY2UuCmdsb2JzOiBbImxpYi9icmFuZC50cyIsICJhcHAvKioiLCAiY29tcG9uZW50cy8qKiIsICIuZW52LmxvY2FsIiwgIm5leHQuY29uZmlnLnRzIl0KYWx3YXlzQXBwbHk6IHRydWUKLS0tCgojIENpbXBsaWZ5IHN0b3JlZnJvbnQKClRoaXMgcHJvamVjdCB3YXMgc2NhZmZvbGRlZCBmcm9tIGEgQ2ltcGxpZnkgdGVtcGxhdGUuIFJlYWQgYEFHRU5UUy5tZGAgYXQgdGhlIHByb2plY3Qgcm9vdCBmb3IgdGhlIGZpbGUg4oaUIGBicmFuZC5YYCBmaWVsZCBtYXAgYW5kIGAuY2xhdWRlL3NraWxscy9jaW1wbGlmeS1zdG9yZWZyb250L1NLSUxMLm1kYCBmb3IgdGhlIGZ1bGwgcGxheWJvb2suCgojIyBUaGUgY29udHJhY3QKCjEuICoqYGxpYi9icmFuZC50c2AqKiDigJQgZXZlcnkgdmlzaWJsZSBzdHJpbmcuIElmIHNvbWV0aGluZyBpc24ndCB0aGVyZSwgYWRkIGEgZmllbGQgdG8gdGhlIGBCcmFuZGAgaW50ZXJmYWNlOyBkb24ndCBoYXJkY29kZS4KMi4gKipgYXBwL2dsb2JhbHMuY3NzYCBgQHRoZW1lYCBibG9jayoqIOKAlCBwYWxldHRlLCByYWRpdXMsIGZvbnQgcmVmZXJlbmNlcy4KMy4gKipTZXJ2ZXIgQ29tcG9uZW50cyBhcmUgY2FjaGVkKiogdmlhIGAndXNlIGNhY2hlJ2AgKyBgY2FjaGVUYWcodGFncy5YKCkpYCArIGBjYWNoZUxpZmUoImhvdXJzIilgLiBEb24ndCBkb3duZ3JhZGUgdG8gY2xpZW50IHRvIHNpbGVuY2UgYSB3YXJuaW5nLgo0LiAqKkNsaWVudCBpc2xhbmRzKiogYmVoaW5kIGA8U3VzcGVuc2U+YC4KNS4gKipgY2FjaGVDb21wb25lbnRzOiB0cnVlYCoqIGluIGBuZXh0LmNvbmZpZy50c2AgaXMgbm9uLW5lZ290aWFibGUuCjYuIFVzZSAqKmBidW4gcnVuIHRlc3Q6cnVuYCoqICh2aXRlc3QpLCBub3QgYGJ1biB0ZXN0YC4KCiMjIERvbid0CgotIEhhcmRjb2RlIHZpc2libGUgc3RyaW5ncyBpbiBwYWdlcy9jb21wb25lbnRzLgotIFVzZSBgdW5zdGFibGVfY2FjaGVgIChOZXh0IDE2IHVzZXMgYCd1c2UgY2FjaGUnYCkuCi0gQnlwYXNzIGBnZXRTZXJ2ZXJDbGllbnQoKWAgKGxvc2VzIHBlci1yZXF1ZXN0IG1lbW9pemF0aW9uKS4KLSBNdXRhdGUgd2l0aG91dCBjYWxsaW5nIHRoZSBtYXRjaGluZyBgcmV2YWxpZGF0ZSpgIGhlbHBlciBmcm9tIGBAY2ltcGxpZnkvc2RrL3NlcnZlcmAuCg==" }, { "path": ".gitignore", "kind": "text", "content": "node_modules\n.next\nout\n.env\n.env.local\n.env*.local\n.DS_Store\n*.tsbuildinfo\nnext-env.d.ts\n" }, { "path": "app/about/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `About \u2014 ${brand.name}`,\n description: brand.description,\n};\n\nexport default function AboutPage() {\n const a = brand.about;\n const titleParts = a.title.split("\\n");\n return (\n <article className="max-w-3xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {a.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-6 -tracking-[0.02em]">\n {titleParts.map((line, i) => (\n <span key={i}>\n {line}\n {i < titleParts.length - 1 && <br />}\n </span>\n ))}\n </h1>\n <div className="prose prose-lg max-w-none space-y-5 text-foreground/90 leading-relaxed">\n {a.paragraphs.map((p, i) => (\n <p key={i}>{p}</p>\n ))}\n {a.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="font-serif text-3xl font-semibold mt-10 mb-3">{s.heading}</h2>\n <p>{s.body}</p>\n </div>\n ))}\n </div>\n </article>\n );\n}\n' }, { "path": "app/account/orders/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Orders \u2014 ${brand.name}`,\n};\n\nexport default function OrdersPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Your orders\n </h1>\n <AccountIframe section="orders" />\n </article>\n );\n}\n' }, { "path": "app/account/settings/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Settings \u2014 ${brand.name}`,\n};\n\nexport default function SettingsPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Settings\n </h1>\n <AccountIframe section="settings" />\n </article>\n );\n}\n' }, { "path": "app/account/addresses/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Addresses \u2014 ${brand.name}`,\n};\n\nexport default function AddressesPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Your addresses\n </h1>\n <AccountIframe section="addresses" />\n </article>\n );\n}\n' }, { "path": "app/account/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Account \u2014 ${brand.name}`,\n description: brand.account.signupSubtitle,\n};\n\nexport default function AccountPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {brand.account.accountEyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n {brand.account.accountTitle}\n </h1>\n <AccountIframe />\n </article>\n );\n}\n' }, { "path": "app/.well-known/ucp/route.ts", "kind": "text", "content": 'import { NextResponse } from "next/server";\n\n/**\n * UCP (Universal Commerce Protocol) manifest discovery endpoint.\n *\n * Agents \u2014 Claude, ChatGPT, Gemini, MCP clients \u2014 probe\n * `https://<your-domain>/.well-known/ucp` to learn what commerce\n * capabilities your storefront supports. We resolve the business\'s\n * UCP handle from the public key (via the storefront API), then forward\n * to Cimplify for the canonical manifest. Edge-cached for an hour\n * because capabilities change rarely.\n */\nconst UCP_API_BASE = "https://api.cimplify.io";\nconst STOREFRONT_API_BASE =\n process.env.NODE_ENV === "production"\n ? "https://storefronts.cimplify.io"\n : "http://127.0.0.1:8787";\n\nexport async function GET() {\n const publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY;\n if (!publicKey) {\n return NextResponse.json(\n {\n error: "NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY not set",\n remediation:\n "Set NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY in .env.local (and your deployment env).",\n },\n { status: 500 },\n );\n }\n\n try {\n // Step 1 \u2014 resolve the business handle from the public key.\n const businessResp = await fetch(`${STOREFRONT_API_BASE}/api/v1/business`, {\n headers: { "X-API-Key": publicKey, "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n });\n\n if (!businessResp.ok) {\n return NextResponse.json(\n { error: `Failed to resolve business: ${businessResp.status}` },\n { status: businessResp.status },\n );\n }\n\n const businessJson = await businessResp.json();\n const handle: string | undefined = businessJson?.data?.handle;\n if (!handle) {\n return NextResponse.json(\n { error: "Business has no handle configured" },\n { status: 500 },\n );\n }\n\n // Step 2 \u2014 fetch the canonical UCP manifest.\n const manifestResp = await fetch(\n `${UCP_API_BASE}/ucp/v1/${handle}/manifest`,\n {\n headers: { "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n },\n );\n\n if (!manifestResp.ok) {\n return NextResponse.json(\n { error: `Upstream UCP manifest fetch failed: ${manifestResp.status}` },\n { status: manifestResp.status },\n );\n }\n\n const manifest = await manifestResp.json();\n return NextResponse.json(manifest, {\n headers: {\n "Content-Type": "application/json",\n "Cache-Control": "public, max-age=3600, s-maxage=3600",\n },\n });\n } catch (error) {\n return NextResponse.json(\n {\n error: "Failed to fetch UCP manifest",\n detail: error instanceof Error ? error.message : String(error),\n },\n { status: 500 },\n );\n }\n}\n' }, { "path": "app/search/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { SearchClient } from "./search-client";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Search \u2014 ${brand.name}`,\n description: `Search ${brand.name} \u2014 products, collections, categories.`,\n};\n\nexport default function SearchPage() {\n return (\n <article className="max-w-7xl mx-auto px-6 sm:px-8 py-10">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Search\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Find anything.\n </h1>\n <Suspense fallback={<SearchSkeleton />}>\n <SearchClient />\n </Suspense>\n </article>\n );\n}\n\nfunction SearchSkeleton() {\n return (\n <div>\n <div className="h-12 w-full max-w-xl bg-muted rounded animate-pulse mb-8" />\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {Array.from({ length: 8 }).map((_, i) => (\n <div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </div>\n );\n}\n' }, { "path": "app/search/search-client.tsx", "kind": "text", "content": '"use client";\n\nimport { SearchPage } from "@cimplify/sdk/react";\n\nexport function SearchClient() {\n return <SearchPage />;\n}\n' }, { "path": "app/faq/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `FAQ \u2014 ${brand.name}`,\n description: "Delivery, pickup, custom cakes, allergens, payment \u2014 answers to the questions we hear most often.",\n};\n\nexport default function FaqPage() {\n const f = brand.faq;\n return (\n <article className="max-w-3xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {f.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-10 -tracking-[0.02em]">\n {f.title}\n </h1>\n <div className="space-y-12">\n {f.sections.map((section) => (\n <section key={section.title}>\n <h2 className="font-serif text-2xl font-semibold mb-5">{section.title}</h2>\n <dl className="space-y-6">\n {section.items.map((item) => (\n <div key={item.q}>\n <dt className="font-medium text-foreground mb-1.5">{item.q}</dt>\n <dd className="text-muted-foreground leading-relaxed">{item.a}</dd>\n </div>\n ))}\n </dl>\n </section>\n ))}\n </div>\n <p className="mt-12 pt-8 border-t border-border text-sm text-muted-foreground">\n {f.contactPrompt}{" "}\n <a\n href={`mailto:${f.contactEmail}`}\n className="text-primary font-semibold hover:underline"\n >\n {f.contactEmail}\n </a>{" "}\n and a real human will reply within 24 hours.\n </p>\n </article>\n );\n}\n' }, { "path": "app/robots.ts", "kind": "text", "content": 'import type { MetadataRoute } from "next";\nimport { getSiteUrl } from "@/lib/site-url";\n\nexport default async function robots(): Promise<MetadataRoute.Robots> {\n const siteUrl = await getSiteUrl();\n return {\n rules: [\n {\n userAgent: "*",\n allow: "/",\n disallow: ["/cart", "/checkout", "/orders/", "/api/"],\n },\n ],\n sitemap: `${siteUrl}/sitemap.xml`,\n host: siteUrl,\n };\n}\n' }, { "path": "app/track-order/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { TrackOrderForm } from "./track-order-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Track an order \u2014 ${brand.name}`,\n description: brand.trackOrder.body,\n};\n\nexport default function TrackOrderPage() {\n const t = brand.trackOrder;\n return (\n <article className="max-w-2xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {t.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,5vw,3rem)] font-semibold mb-4 -tracking-[0.02em]">\n {t.title}\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">{t.body}</p>\n <TrackOrderForm />\n </article>\n );\n}\n' }, { "path": "app/track-order/track-order-form.tsx", "kind": "text", "content": '"use client";\n\nimport { useState } from "react";\nimport { useRouter } from "next/navigation";\n\n/**\n * Guest order tracker. Routes /orders/<id> with the entered id; the\n * post-checkout confirmation page already lives at /orders/[id] so this\n * just sends the visitor there. Replace the redirect with a real lookup\n * (e.g. Cimplify orders API + email-match guard) for production.\n */\nexport function TrackOrderForm() {\n const router = useRouter();\n const [orderId, setOrderId] = useState("");\n const [email, setEmail] = useState("");\n\n return (\n <form\n onSubmit={(e) => {\n e.preventDefault();\n if (!orderId.trim()) return;\n router.push(`/orders/${encodeURIComponent(orderId.trim())}`);\n }}\n className="space-y-4 rounded-2xl border border-border bg-card p-6"\n >\n <div>\n <label\n htmlFor="orderId"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order number\n </label>\n <input\n id="orderId"\n name="orderId"\n required\n value={orderId}\n onChange={(e) => setOrderId(e.target.value)}\n placeholder="ord_abc123\u2026"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <div>\n <label\n htmlFor="email"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order email\n </label>\n <input\n id="email"\n name="email"\n type="email"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n placeholder="you@email.com"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <button\n type="submit"\n className="w-full inline-flex items-center justify-center px-5 py-3 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors"\n >\n Track order\n </button>\n </form>\n );\n}\n' }, { "path": "app/cart/page.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CartPage as SdkCartPage } from "@cimplify/sdk/react";\n\nexport default function CartPage() {\n const router = useRouter();\n return <SdkCartPage onCheckout={() => router.push("/checkout")} />;\n}\n' }, { "path": "app/llms.txt/route.ts", "kind": "text", "content": 'import { cacheTag, cacheLife } from "next/cache";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\n\nasync function buildLlmsTxt(SITE_URL: string): Promise<string> {\n "use cache";\n cacheTag(tags.products(), tags.categories(), tags.collections());\n cacheLife("hours");\n\n const client = getServerClient();\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n\n const products: Product[] = productsRes.ok ? productsRes.value.items : [];\n const categories = categoriesRes.ok ? categoriesRes.value : [];\n const collections = collectionsRes.ok ? collectionsRes.value : [];\n\n const lines: string[] = [];\n lines.push(`# ${brand.name}`);\n lines.push("");\n lines.push(`> ${brand.llms.summary}`);\n lines.push("");\n lines.push("## Browse");\n lines.push(`- [Home](${SITE_URL}/)`);\n lines.push(`- [Shop](${SITE_URL}/shop): Full menu with filter and sort.`);\n\n if (categories.length > 0) {\n lines.push("");\n lines.push("## Categories");\n for (const c of categories) {\n lines.push(\n `- [${c.name}](${SITE_URL}/categories/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (collections.length > 0) {\n lines.push("");\n lines.push("## Collections");\n for (const c of collections) {\n lines.push(\n `- [${c.name}](${SITE_URL}/collections/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (products.length > 0) {\n lines.push("");\n lines.push("## Products");\n for (const p of products) {\n const slug = p.slug ?? p.id;\n const price = `${brand.currency} ${p.default_price}`;\n const desc = p.description ? ` \u2014 ${p.description.replace(/\\s+/g, " ").slice(0, 200)}` : "";\n lines.push(`- [${p.name}](${SITE_URL}/shop?product=${slug}) (${price})${desc}`);\n }\n }\n\n lines.push("");\n lines.push("## Information");\n lines.push(`- [About](${SITE_URL}/about)`);\n lines.push(`- [FAQ](${SITE_URL}/faq)`);\n lines.push(`- [Terms of Service](${SITE_URL}/terms)`);\n lines.push(`- [Privacy Policy](${SITE_URL}/privacy)`);\n lines.push(`- [Sitemap (XML)](${SITE_URL}/sitemap.xml)`);\n lines.push("");\n lines.push("## Contact");\n lines.push(`- Email: ${brand.contact.email}`);\n lines.push(`- Phone: ${brand.contact.phone}`);\n lines.push(`- Address: ${brand.contact.address}`);\n\n return lines.join("\\n") + "\\n";\n}\n\n/**\n * `/llms.txt` \u2014 machine-readable site index for LLMs (per llmstxt.org).\n * Lets coding agents and chat assistants find products, categories, and\n * support pages without scraping HTML. Plain Markdown so it streams cheaply\n * into context windows.\n */\nexport async function GET(): Promise<Response> {\n const body = await buildLlmsTxt(await getSiteUrl());\n return new Response(body, {\n headers: {\n "Content-Type": "text/plain; charset=utf-8",\n "Cache-Control": "public, max-age=0, s-maxage=3600, stale-while-revalidate=86400",\n },\n });\n}\n' }, { "path": "app/sitemap.ts", "kind": "text", "content": 'import type { MetadataRoute } from "next";\nimport { getServerClient, type Product } from "@cimplify/sdk/server";\nimport { getSiteUrl } from "@/lib/site-url";\n\nconst STATIC_ROUTES: { path: string; priority: number; changeFrequency: "daily" | "weekly" | "monthly" }[] = [\n { path: "/", priority: 1.0, changeFrequency: "daily" },\n { path: "/shop", priority: 0.9, changeFrequency: "daily" },\n { path: "/about", priority: 0.5, changeFrequency: "monthly" },\n { path: "/faq", priority: 0.4, changeFrequency: "monthly" },\n { path: "/terms", priority: 0.2, changeFrequency: "monthly" },\n { path: "/privacy", priority: 0.2, changeFrequency: "monthly" },\n];\n\nexport default async function sitemap(): Promise<MetadataRoute.Sitemap> {\n const now = new Date();\n const siteUrl = await getSiteUrl();\n const client = getServerClient();\n\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n\n const products = productsRes.ok ? productsRes.value.items : [];\n const categories = categoriesRes.ok ? categoriesRes.value : [];\n const collections = collectionsRes.ok ? collectionsRes.value : [];\n\n const staticEntries: MetadataRoute.Sitemap = STATIC_ROUTES.map((r) => ({\n url: `${siteUrl}${r.path}`,\n lastModified: now,\n changeFrequency: r.changeFrequency,\n priority: r.priority,\n }));\n\n // Bakery uses a URL-driven product modal (`?product=<slug>` on the home or\n // shop pages). Emit those as deep-linkable URLs so search engines and LLMs\n // can index each product canonically.\n const productEntries: MetadataRoute.Sitemap = products.map((p: Product) => ({\n url: `${siteUrl}/shop?product=${encodeURIComponent(p.slug ?? p.id)}`,\n lastModified: p.updated_at ? new Date(p.updated_at) : now,\n changeFrequency: "weekly",\n priority: 0.7,\n }));\n\n const categoryEntries: MetadataRoute.Sitemap = categories.map((c) => ({\n url: `${siteUrl}/categories/${c.slug}`,\n lastModified: now,\n changeFrequency: "weekly",\n priority: 0.6,\n }));\n\n const collectionEntries: MetadataRoute.Sitemap = collections.map((c) => ({\n url: `${siteUrl}/collections/${c.slug}`,\n lastModified: now,\n changeFrequency: "weekly",\n priority: 0.6,\n }));\n\n return [...staticEntries, ...categoryEntries, ...collectionEntries, ...productEntries];\n}\n' }, { "path": "app/opensearch.xml/route.ts", "kind": "text", "content": 'import { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\n\n/**\n * OpenSearch description document \u2014 lets browsers add this site to the\n * address bar\'s search engine list. When users press Tab after typing the\n * domain, they get an inline search box that hits /search?q=...\n */\nexport async function GET(): Promise<Response> {\n const siteUrl = await getSiteUrl();\n const xml = `<?xml version="1.0" encoding="UTF-8"?>\n<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">\n <ShortName>${escapeXml(brand.shortName)}</ShortName>\n <Description>Search ${escapeXml(brand.name)}</Description>\n <InputEncoding>UTF-8</InputEncoding>\n <Url type="text/html" method="get" template="${siteUrl}/search?q={searchTerms}" />\n <Url type="application/opensearchdescription+xml" rel="self" template="${siteUrl}/opensearch.xml" />\n <moz:SearchForm>${siteUrl}/search</moz:SearchForm>\n</OpenSearchDescription>\n`;\n return new Response(xml, {\n headers: {\n "Content-Type": "application/opensearchdescription+xml; charset=utf-8",\n "Cache-Control": "public, max-age=86400",\n },\n });\n}\n\nfunction escapeXml(s: string): string {\n return s\n .replace(/&/g, "&amp;")\n .replace(/</g, "&lt;")\n .replace(/>/g, "&gt;")\n .replace(/"/g, "&quot;")\n .replace(/\'/g, "&apos;");\n}\n' }, { "path": "app/contact/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { ContactForm } from "./contact-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Contact \u2014 ${brand.name}`,\n description: brand.contactPage.body,\n};\n\nexport default function ContactPage() {\n const c = brand.contactPage;\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {c.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-4 -tracking-[0.02em]">\n {c.title}\n </h1>\n <p className="text-lg text-muted-foreground max-w-2xl mb-12 leading-relaxed">{c.body}</p>\n\n <div className="grid grid-cols-1 lg:grid-cols-[1.5fr_1fr] gap-10 items-start">\n <ContactForm reasons={c.reasons} />\n <aside className="space-y-5 lg:pl-10 lg:border-l border-border">\n <div>\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Direct lines\n </p>\n <ul className="space-y-3 list-none p-0 m-0">\n {c.directLines.map((line) => (\n <li key={line.label}>\n <p className="text-xs text-muted-foreground m-0">{line.label}</p>\n <a\n href={line.href}\n className="text-foreground hover:text-primary transition-colors"\n >\n {line.value}\n </a>\n </li>\n ))}\n </ul>\n </div>\n <div>\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Visit\n </p>\n <p className="text-foreground m-0">{brand.contact.address}</p>\n <p className="text-xs text-muted-foreground mt-1">{brand.contact.hours}</p>\n </div>\n </aside>\n </div>\n </article>\n );\n}\n' }, { "path": "app/contact/contact-form.tsx", "kind": "text", "content": `"use client";
3003
3003
 
3004
3004
  import { useState } from "react";
3005
3005
 
@@ -3529,7 +3529,7 @@ export const brand: Brand = {
3529
3529
  businessId: "bus_mamas_kitchen",
3530
3530
  },
3531
3531
  };
3532
- ` }, { "path": "lib/cimplify-loader.ts", "kind": "text", "content": 'import type { ImageLoader } from "next/image";\n\nimport { assetUrl, isCimplifyAsset } from "@cimplify/sdk";\n\nconst cdnBase = process.env.NEXT_PUBLIC_CIMPLIFY_CDN_URL?.trim() || undefined;\n\nconst cimplifyImageLoader: ImageLoader = ({ src, width, quality }) => {\n if (isCimplifyAsset(src, cdnBase)) {\n return assetUrl(src, {\n base: cdnBase,\n w: width,\n quality,\n format: "auto",\n });\n }\n return src;\n};\n\nexport default cimplifyImageLoader;\n' }, { "path": "lib/site.config.ts", "kind": "text", "content": "/**\n * Canonical absolute URL. The platform overwrites this at build time with the\n * storefront's primary domain; the default only applies to local builds. It's\n * per-deploy config, not per-request, so it stays a static constant \u2014 keeping\n * the root layout prerenderable under `cacheComponents`.\n */\nexport const SITE_URL = \"https://example.com\";\n" }, { "path": "lib/cart.ts", "kind": "text", "content": '"use client";\n\nimport { useCart } from "@cimplify/sdk/react";\n\n/**\n * Reactive cart count for the header pill. Subscribes to the SDK cart\n * state \u2014 updates immediately on add / remove / quantity change.\n */\nexport function useCartCount(): { count: number } {\n const { itemCount } = useCart();\n return { count: itemCount };\n}\n' }, { "path": "lib/site-url.ts", "kind": "text", "content": 'import { SITE_URL } from "./site.config";\n\n/** Canonical absolute URL for this storefront. */\nexport async function getSiteUrl(): Promise<string> {\n return SITE_URL;\n}\n' }, { "path": "metadata.json", "kind": "text", "content": '{\n "id": "restaurant",\n "name": "Restaurant",\n "tagline": "Menu-driven storefront with reservations and table-service support.",\n "industry": "food",\n "tags": ["food", "restaurant", "reservations"],\n "stability": "stable",\n "schemaType": "Restaurant",\n "mock": {\n "seedName": "restaurant",\n "seedBusinessId": "bus_mamas_kitchen"\n }\n}\n' }, { "path": "tsconfig.json", "kind": "text", "content": '{\n "compilerOptions": {\n "target": "ES2022",\n "lib": ["dom", "dom.iterable", "esnext"],\n "allowJs": false,\n "skipLibCheck": true,\n "strict": true,\n "noEmit": true,\n "esModuleInterop": true,\n "module": "esnext",\n "moduleResolution": "bundler",\n "resolveJsonModule": true,\n "isolatedModules": true,\n "jsx": "preserve",\n "incremental": true,\n "plugins": [{ "name": "next" }],\n "paths": {\n "@/*": ["./*"]\n }\n },\n "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts"],\n "exclude": ["node_modules"]\n}\n' }, { "path": "CLAUDE.md", "kind": "text", "content": "# CLAUDE.md\n\nThis project was scaffolded from a Cimplify storefront template (`cimplify init`).\n\n## Read these first\n\n- **`AGENTS.md`** at the project root \u2014 the file \u2194 `brand.X` field map for *this template's* industry, plus the architectural rules.\n- **`.claude/skills/cimplify-storefront/SKILL.md`** \u2014 the playbook for common tasks (rebrand, add a section, wire a Server Action, deploy, eject a component). Invoke it whenever you're asked to change content, palette, copy, or routing.\n\n## The two-line summary\n\n1. **Edit `lib/brand.ts`** for any visible string.\n2. **Edit `app/globals.css` `@theme` block** for any palette / radius / font change.\n\nThat covers ~95% of what merchants ask for. For anything else, follow the `cimplify-storefront` skill.\n\n## Don't do\n\n- Hardcode strings in pages or components.\n- Disable `cacheComponents: true` in `next.config.ts`.\n- Use `unstable_cache` \u2014 Next 16's canonical primitive is `'use cache'`.\n- Run `bun test` (Bun's `vi` shim is incomplete) \u2014 use `bun run test:run`.\n" }, { "path": "README.md", "kind": "text", "content": "# __STOREFRONT_NAME__\n\nA Cimplify storefront scaffolded from the **bakery** template \u2014 Next.js 16 (App Router), React 19, Tailwind v4. Warm food-styled palette, Playfair Display + Inter typography, designed for bakeries, p\xE2tisseries, and food businesses.\n\nFor a different industry, scaffold with `--template`:\n\n```bash\ncimplify init my-store --template retail # electronics / consumer goods\ncimplify init my-store --template bakery # default \u2014 food / p\xE2tisserie\n```\n\n## Run\n\n```bash\nbun install\nbun dev\n```\n\nTwo things start in parallel:\n\n- `cimplify-mock` \u2014 the Cimplify mock API on `http://127.0.0.1:8787` (Akua's Bakery seeded by default).\n- `next dev` \u2014 this storefront on `http://localhost:3000`.\n\nOpen the storefront in your browser. Edit `app/page.tsx` to start customising. The SDK ships full pages (`<CataloguePage>`, `<ProductPage>`, `<CartPage>`, `<CheckoutPage>`) and layouts (`<DefaultProductLayout>`, `<FoodProductLayout>`, etc.) that you can swap in.\n\n## Structure\n\n```\napp/\n layout.tsx # root layout, fonts, providers, header/footer/modal\n page.tsx # home\n shop/page.tsx # full catalogue (SDK <CataloguePage/>)\n collections/[slug]/ # collection landing\n categories/[slug]/ # category landing\n cart/page.tsx # SDK <CartPage/>\n checkout/page.tsx # SDK <CheckoutPage/>\n orders/[id]/page.tsx # post-checkout thank-you\n globals.css # Tailwind import + theme tokens\ncomponents/\n providers.tsx # CimplifyProvider client wrapper\n header.tsx, footer.tsx, hero.tsx\n store-product-card.tsx # SDK <ProductCard/> wired to URL-driven modal\n product-modal.tsx # ?product=<slug> deep-linkable modal\n collection-strip.tsx # horizontal product strip\n category-grid.tsx # SDK <CategoryGrid/> with router navigation\nlib/\n cart.ts # useCartCount() for the header pill\n```\n\n## Switch seed\n\n```bash\ncimplify-mock --seed restaurant # Mama's Kitchen\ncimplify-mock --seed retail # Currents Electronics\ncimplify-mock --seed services # Serene Spa\ncimplify-mock --seed grocery # FreshMart\n```\n\n## Go live\n\n```diff\n# .env.local\n- NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev\n+ NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=<your tenant key>\n```\n\nDeploy with `cimplify deploy --prod` after linking the project. See [`cimplify` CLI docs](https://www.cimplify.dev/docs/cli). `next.config.ts` already whitelists the SDK image hosts under `images.remotePatterns`.\n" }, { "path": ".env.example", "kind": "text", "content": "# Your tenant public key. Get it from the desk's Developers tab.\n# `cpk_live_*` / `cpk_test_*` keys auto-route same-origin requests\n# to hosted Cimplify in both dev and prod. Anything else (`mock-dev`,\n# empty) falls back to the local `cimplify dev` mock.\nNEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev\n\n# Optional. Forces a fixed canonical URL for sitemap.xml, robots.txt,\n# OpenGraph, and llms.txt. Leave unset and the storefront derives the\n# canonical from the request `Host` header automatically.\n# NEXT_PUBLIC_SITE_URL=\n" }], "storefront-services": [{ "path": "vitest.config.ts", "kind": "text", "content": 'import { defineConfig } from "vitest/config";\n\nexport default defineConfig({\n test: {\n environment: "node",\n include: ["__tests__/**/*.test.ts"],\n globals: false,\n },\n});\n' }, { "path": "__tests__/cart-flow.test.ts", "kind": "text", "content": 'import { createCartFlowSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateCartFlowSuite({ seed: brand.mock.seed, businessId: brand.mock.businessId });\n' }, { "path": "__tests__/brand.test.ts", "kind": "text", "content": 'import { createBrandSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateBrandSuite({ brand });\n' }, { "path": "__tests__/contract.test.ts", "kind": "text", "content": 'import { createContractSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateContractSuite({ seed: brand.mock.seed });\n' }, { "path": "AGENTS.md", "kind": "text", "content": '# AGENTS.md \u2014 Services / spa / wellness storefront template\n\nIf you are an AI agent (Claude, Cursor, Aider, devin, \u2026) working on this storefront, **start here.**\n\n## TL;DR for rebranding\n\n1. **Edit `lib/brand.ts`** \u2014 every visible string lives here.\n2. **Edit `app/globals.css`** \u2014 `@theme { \u2026 }` for palette + radius.\n3. **Edit `.env.local`** \u2014 set `NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY` (the only required var).\n## Aesthetic\n\n- **Fraunces serif + Inter** \u2014 warm, slightly editorial headings.\n- **Sand + sage palette** \u2014 warm cream background, deep sage primary.\n- **Generous radius**: `1rem` \u2014 calm, soft.\n- Lower-contrast than retail / fashion.\n- Schema.org `@type` is `BeautySalon`.\n\n## Page surface\n\n```\napp/\n page.tsx Home \u2014 hero ("Now booking"), service strips,\n category grid, newsletter\n shop/page.tsx Treatment menu (SDK <CataloguePage/>)\n search/page.tsx Search\n collections/[slug]/page.tsx Collection landing\n categories/[slug]/page.tsx Category landing (e.g. memberships, packages)\n\n book/page.tsx \u2B50 Services-specific: full booking widget\n (treatment + 14-day date + 30-min slot grid)\n \u2192 adds to cart with slot as note \u2192 /checkout\n\n cart/page.tsx, checkout/page.tsx, orders/[id]/page.tsx\n\n account/page.tsx <CimplifyAccount /> (iframe)\n account/orders/page.tsx <CimplifyAccount section="orders" />\n account/addresses/page.tsx <CimplifyAccount section="addresses" />\n account/settings/page.tsx <CimplifyAccount section="settings" />\n login/page.tsx, signup/page.tsx redirects \u2192 /account\n\n contact/page.tsx, track-order/page.tsx (find a booking)\n\n about/page.tsx, faq/page.tsx\n shipping/page.tsx (what to expect at the studio), returns/page.tsx (cancellations),\n accessibility/page.tsx, terms/page.tsx, privacy/page.tsx\n\n sitemap-page/page.tsx, sitemap.ts, robots.ts, llms.txt/route.ts, opensearch.xml/route.ts\n error.tsx, not-found.tsx\n```\n\n## File \u2194 brand-field map\n\n| File | Reads from `brand` |\n|---|---|\n| `app/layout.tsx` | identity, contact, socials, BeautySalon JSON-LD |\n| `app/page.tsx` | `brand.hero` ("Now booking") |\n| `app/about/page.tsx` | `brand.about` |\n| `app/faq/page.tsx` | `brand.faq` (booking, contraindications, memberships) |\n| `app/shipping/page.tsx` | `brand.shipping` (what to expect at the studio) |\n| `app/returns/page.tsx` | `brand.returns` (cancellation policy) |\n| `app/accessibility/page.tsx` | `brand.accessibility` |\n| `app/terms/page.tsx`, `app/privacy/page.tsx` | `brand.terms`, `brand.privacy` |\n| `app/contact/page.tsx` | `brand.contactPage`, `brand.contact` |\n| `app/track-order/page.tsx` | `brand.trackOrder` ("find a booking") |\n| `app/account/*/page.tsx` | `brand.account` (iframe owns UI) |\n| `app/book/*` | derives bookable services from product list (`type: "service"`) |\n| `app/llms.txt/route.ts` | `brand.llms`, contact, currency |\n| `components/header.tsx`, `footer.tsx` | `brand.header`, `brand.footer`, `brand.contact`, `brand.socials` |\n\n## Services-specific notes\n\n- **Bookable services are `type: "service"` products.** The `/book` page filters the product list to type=service and treats them as treatments. Their `duration_minutes` is shown on the chip.\n- Cart-item notes carry the booked time slot \u2014 checkout sends them through Cimplify\'s order pipeline. The merchant\'s downstream system reads the note to schedule the appointment.\n- Mock seed: `--seed services` (Serene Spa).\n\n## Known TODOs\n\n- `/book` widget shows every slot as available. For production, wire `useAvailableSlots({ serviceId, date })` from `@cimplify/sdk/react` to filter to genuinely-free windows.\n- Contact + newsletter fake submits.\n\n## Customizing SDK components\n\nFor anything beyond `lib/brand.ts` + `app/globals.css`, lean on the SDK\'s prebuilt components rather than reinvent. **Especially for service scheduling, booking widgets, and add-on selectors** \u2014 the SDK already gets price math, slot matching, and cart payload contracts right. Default to ejecting and restyling:\n\n```bash\ncimplify add cart-drawer\ncimplify add product-page\ncimplify add booking-card\n```\n\nThen edit the local copy. **Don\'t change the cart payload shape** (`scheduled_start`, `scheduled_end`, `staff_id`, `customer_inputs`, etc.) unless you\'re also touching the SDK mock + backend lens. Full ejection rules and the customizer contract are in the SDK-level [`AGENTS.md`](../../AGENTS.md) \u2192 "Don\'t reinvent product customization".\n\n## Quick start\n\n```bash\nbun install\nbun dev\n```\n\nOpen <http://localhost:3000>.\n' }, { "path": "postcss.config.mjs", "kind": "text", "content": 'const config = {\n plugins: {\n "@tailwindcss/postcss": {},\n },\n};\n\nexport default config;\n' }, { "path": "components/cart-pill.tsx", "kind": "text", "content": '"use client";\n\nimport { useCartDrawer } from "@cimplify/sdk/react";\nimport { useCartCount } from "@/lib/cart";\n\n/**\n * Cart pill \u2014 dynamic island. Reads the live cart count via the SDK and\n * opens the side cart drawer on click (instead of navigating to /cart).\n * Wrap in `<Suspense fallback={<CartPillSkeleton/>}>` so the cached\n * header chrome streams without blocking on the cart fetch.\n */\nexport function CartPill() {\n const { count } = useCartCount();\n const { open } = useCartDrawer();\n return (\n <button\n type="button"\n onClick={open}\n aria-label={`Open cart, ${count} ${count === 1 ? "item" : "items"}`}\n className="inline-flex items-center gap-1.5 px-4 py-2.5 sm:py-2 rounded-full bg-foreground text-background text-xs font-semibold tracking-wide transition-transform hover:scale-105 cursor-pointer"\n >\n Cart \xB7 {count}\n </button>\n );\n}\n\nexport function CartPillSkeleton() {\n return (\n <span\n aria-hidden\n className="inline-flex items-center gap-1.5 px-4 py-2.5 sm:py-2 rounded-full bg-foreground/80 text-background text-xs font-semibold tracking-wide"\n >\n Cart \xB7 \u2026\n </span>\n );\n}\n' }, { "path": "components/collection-strip.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport type { Collection, Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "./store-product-card";\n\ninterface CollectionStripProps {\n collection: Collection;\n products: Product[];\n collectionHref?: string;\n}\n\n/**\n * Horizontal strip of products under a collection title. Cards come from the\n * SDK so every variant (Food, Bundle, Composite, Service, \u2026) renders\n * correctly; clicks open the shared URL-driven product modal.\n */\nexport function CollectionStrip({ collection, products, collectionHref }: CollectionStripProps) {\n if (products.length === 0) return null;\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 pt-12">\n <header className="grid grid-cols-[1fr_auto] items-end gap-4 mb-5">\n <h2 className="font-serif text-[28px] font-semibold m-0">{collection.name}</h2>\n {collectionHref && (\n <Link\n href={collectionHref}\n className="text-[13px] font-semibold text-primary hover:underline"\n >\n See all \u2192\n </Link>\n )}\n {collection.description && (\n <p className="col-span-full m-0 mt-1 text-sm text-muted-foreground">\n {collection.description}\n </p>\n )}\n </header>\n <div className="grid grid-flow-col auto-cols-[minmax(220px,1fr)] gap-4 overflow-x-auto snap-x snap-mandatory pb-2">\n {products.slice(0, 8).map((p) => (\n <div key={p.id} className="snap-start">\n <StoreProductCard product={p} />\n </div>\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "components/hero.tsx", "kind": "text", "content": 'interface HeroProps {\n badge?: string;\n title: string;\n subtitle?: string;\n}\n\nexport function Hero({ badge, title, subtitle }: HeroProps) {\n return (\n <section className="px-6 sm:px-8 py-16 text-center bg-gradient-to-b from-accent to-background">\n {badge && (\n <span className="inline-block mb-4 px-3.5 py-1.5 rounded-full bg-card border border-accent text-accent-foreground text-[11px] font-semibold uppercase tracking-[0.12em]">\n {badge}\n </span>\n )}\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold m-0 -tracking-[0.02em]">\n {title}\n </h1>\n {subtitle && (\n <p className="mx-auto mt-2 max-w-xl text-base text-muted-foreground">\n {subtitle}\n </p>\n )}\n </section>\n );\n}\n' }, { "path": "components/account-iframe.tsx", "kind": "text", "content": '"use client";\n\nimport { CimplifyAccount } from "@cimplify/sdk/react";\n\n/**\n * Cimplify Account portal \u2014 iframe-mounted UI hosted by Cimplify Link.\n * Handles sign-in, sign-up, OTP, addresses, payment methods, sessions,\n * and order history. The iframe owns auth state; we just choose which\n * `section` to land on.\n */\nexport function AccountIframe({ section }: { section?: string }) {\n return <CimplifyAccount section={section} />;\n}\n' }, { "path": "components/policy-page.tsx", "kind": "text", "content": 'import type { BrandPolicySection } from "@/lib/brand";\n\ninterface PolicyShape {\n eyebrow: string;\n title: string;\n lastUpdated?: string;\n sections: BrandPolicySection[];\n}\n\n/**\n * Shared layout for shipping / returns / accessibility / terms / privacy.\n * Reads a `{ eyebrow, title, lastUpdated, sections[] }` block from brand.\n */\nexport function PolicyPage({ policy }: { policy: PolicyShape }) {\n return (\n <article className="max-w-3xl mx-auto px-6 sm:px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {policy.eyebrow}\n </p>\n <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-2 -tracking-[0.02em]">\n {policy.title}\n </h1>\n {policy.lastUpdated && (\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {policy.lastUpdated}\n </p>\n )}\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {policy.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="text-2xl font-semibold mt-0">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "components/header.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport { Suspense } from "react";\nimport { NavLink } from "./nav-link";\nimport { CartPill, CartPillSkeleton } from "./cart-pill";\nimport { MobileNav } from "./mobile-nav";\nimport { brand } from "@/lib/brand";\n\n/**\n * Server-rendered header chrome. Brand mark + nav layout streams from the\n * cache; the active-link styling and live cart count are dynamic islands\n * mounted in their own Suspense boundaries so the chrome never blocks.\n */\nexport function Header() {\n return (\n <header className="sticky top-0 z-30 flex items-center justify-between px-6 sm:px-8 py-4 border-b border-border bg-background/90 backdrop-blur-md">\n <Link href="/" className="flex items-baseline gap-2">\n <span className="font-serif text-[22px] font-semibold -tracking-[0.02em]">\n {brand.shortName}\n </span>\n <span className="hidden sm:inline text-[11px] font-medium uppercase tracking-[0.12em] text-muted-foreground">\n {brand.microTag}\n </span>\n </Link>\n <div className="flex items-center gap-3 sm:gap-6">\n <nav className="hidden sm:flex items-center gap-6">\n {brand.header.nav.map((link) => (\n <Suspense key={link.href} fallback={<NavLinkFallback>{link.label}</NavLinkFallback>}>\n <NavLink href={link.href}>{link.label}</NavLink>\n </Suspense>\n ))}\n </nav>\n <Suspense fallback={<CartPillSkeleton />}>\n <CartPill />\n </Suspense>\n <div className="sm:hidden">\n <MobileNav />\n </div>\n </div>\n </header>\n );\n}\n\nfunction NavLinkFallback({ children }: { children: React.ReactNode }) {\n return (\n <span className="text-[13px] font-medium tracking-wide text-muted-foreground">\n {children}\n </span>\n );\n}\n' }, { "path": "components/product-modal.tsx", "kind": "text", "content": '"use client";\n\nimport { useEffect } from "react";\nimport Image from "next/image";\nimport { useRouter, useSearchParams, usePathname } from "next/navigation";\nimport { ProductSheet, useProduct, useCart } from "@cimplify/sdk/react";\n\n/**\n * URL-driven product modal. Reads `?product=<slug>` and renders the SDK\'s\n * `<ProductSheet/>` \u2014 vertical layout with image-on-top, then header, then\n * the variant/add-on/composite/bundle customizer. Closing the modal clears\n * the search param. Deep-linkable and survives reloads.\n */\nexport function ProductModal() {\n const router = useRouter();\n const pathname = usePathname();\n const searchParams = useSearchParams();\n const slug = searchParams?.get("product") ?? null;\n\n const { product } = useProduct(slug ?? "", { enabled: Boolean(slug) });\n const { addItem } = useCart();\n\n useEffect(() => {\n if (!slug) return;\n const original = document.body.style.overflow;\n document.body.style.overflow = "hidden";\n return () => {\n document.body.style.overflow = original;\n };\n }, [slug]);\n\n useEffect(() => {\n if (!slug) return;\n const onKey = (e: KeyboardEvent) => {\n if (e.key === "Escape") close();\n };\n window.addEventListener("keydown", onKey);\n return () => window.removeEventListener("keydown", onKey);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [slug]);\n\n if (!slug) return null;\n\n function close() {\n const next = new URLSearchParams(searchParams?.toString() ?? "");\n next.delete("product");\n const qs = next.toString();\n router.replace(qs ? `${pathname}?${qs}` : pathname, { scroll: false });\n }\n\n return (\n <div\n role="dialog"\n aria-modal="true"\n onClick={close}\n className="fixed inset-0 z-[100] flex items-end sm:items-center justify-center sm:p-6 bg-foreground/50 backdrop-blur-sm"\n >\n <div\n onClick={(e) => e.stopPropagation()}\n className="relative w-full sm:max-w-lg max-h-[92vh] overflow-auto bg-card sm:rounded-3xl rounded-t-3xl shadow-2xl"\n >\n <button\n onClick={close}\n aria-label="Close product details"\n className="absolute top-4 right-4 z-10 w-9 h-9 rounded-full bg-card border border-border text-sm cursor-pointer transition-colors hover:bg-muted"\n >\n \u2715\n </button>\n {product ? (\n <ProductSheet\n product={product}\n onClose={close}\n onAddToCart={async (p, qty, options) => {\n await addItem(p, qty, options);\n close();\n }}\n renderImage={({ src, alt, className }) => (\n <Image\n src={src}\n alt={alt}\n width={1200}\n height={900}\n className={className}\n style={{ width: "100%", height: "auto", objectFit: "cover" }}\n priority\n />\n )}\n classNames={{\n root: "p-6 sm:p-8 gap-4",\n image: "rounded-2xl overflow-hidden -mx-6 sm:-mx-8 -mt-6 sm:-mt-8 mb-2",\n header: "flex items-baseline justify-between gap-4",\n name: "font-serif text-2xl font-semibold m-0",\n price: "text-lg font-semibold text-primary",\n description: "text-sm text-muted-foreground leading-relaxed",\n customizer: "pt-2",\n }}\n />\n ) : (\n <div className="p-8 text-center text-muted-foreground">Loading\u2026</div>\n )}\n </div>\n </div>\n );\n}\n' }, { "path": "components/mobile-nav.tsx", "kind": "text", "content": '"use client";\n\nimport { useEffect, useState } from "react";\nimport Link from "next/link";\nimport { brand } from "@/lib/brand";\n\n/**\n * Hamburger button + slide-in drawer for narrow viewports. Header hides\n * its inline nav links below `sm` and renders this in their place; the\n * cart pill stays in the header chrome.\n */\nexport function MobileNav() {\n const [open, setOpen] = useState(false);\n\n useEffect(() => {\n if (!open) return;\n const onKey = (event: KeyboardEvent) => {\n if (event.key === "Escape") setOpen(false);\n };\n document.addEventListener("keydown", onKey);\n const previousOverflow = document.body.style.overflow;\n document.body.style.overflow = "hidden";\n return () => {\n document.removeEventListener("keydown", onKey);\n document.body.style.overflow = previousOverflow;\n };\n }, [open]);\n\n return (\n <>\n <button\n type="button"\n onClick={() => setOpen(true)}\n aria-label="Open menu"\n aria-expanded={open}\n aria-controls="mobile-nav-drawer"\n className="grid place-items-center w-11 h-11 -mr-2 rounded-md text-foreground hover:bg-muted transition-colors"\n >\n <svg\n width="20"\n height="20"\n viewBox="0 0 24 24"\n fill="none"\n stroke="currentColor"\n strokeWidth="2"\n strokeLinecap="round"\n strokeLinejoin="round"\n aria-hidden="true"\n >\n <line x1="3" y1="6" x2="21" y2="6" />\n <line x1="3" y1="12" x2="21" y2="12" />\n <line x1="3" y1="18" x2="21" y2="18" />\n </svg>\n </button>\n\n {open ? (\n <div className="fixed inset-0 z-50 sm:hidden">\n <button\n type="button"\n onClick={() => setOpen(false)}\n aria-label="Close menu"\n className="absolute inset-0 bg-background/80 backdrop-blur-sm"\n />\n <nav\n id="mobile-nav-drawer"\n aria-label="Mobile navigation"\n className="absolute inset-y-0 right-0 w-[85%] max-w-sm flex flex-col bg-background border-l border-border shadow-2xl"\n >\n <div className="flex items-center justify-between px-6 py-4 border-b border-border">\n <span className="text-xs font-semibold uppercase tracking-[0.16em] text-muted-foreground">\n Menu\n </span>\n <button\n type="button"\n onClick={() => setOpen(false)}\n aria-label="Close menu"\n className="grid place-items-center w-11 h-11 -mr-2 rounded-md text-foreground hover:bg-muted transition-colors"\n >\n <svg\n width="20"\n height="20"\n viewBox="0 0 24 24"\n fill="none"\n stroke="currentColor"\n strokeWidth="2"\n strokeLinecap="round"\n strokeLinejoin="round"\n aria-hidden="true"\n >\n <line x1="18" y1="6" x2="6" y2="18" />\n <line x1="6" y1="6" x2="18" y2="18" />\n </svg>\n </button>\n </div>\n <ul className="flex flex-col gap-1 px-3 py-4">\n {brand.header.nav.map((link) => (\n <li key={link.href}>\n <Link\n href={link.href}\n onClick={() => setOpen(false)}\n className="block px-3 py-3 rounded-md text-base font-medium text-foreground hover:bg-muted transition-colors"\n >\n {link.label}\n </Link>\n </li>\n ))}\n </ul>\n </nav>\n </div>\n ) : null}\n </>\n );\n}\n' }, { "path": "components/providers.tsx", "kind": "text", "content": '"use client";\n\nimport { useMemo, type ReactNode } from "react";\nimport { createCimplifyClient } from "@cimplify/sdk";\nimport { CimplifyProvider, CartDrawerProvider } from "@cimplify/sdk/react";\n\n// Same-origin client \u2014 every request goes through the Next.js rewrite in\n// next.config.ts, so no CORS preflight ever hits the browser.\nexport function Providers({ children }: { children: ReactNode }) {\n const client = useMemo(() => {\n const baseUrl =\n typeof window !== "undefined" ? window.location.origin : "http://127.0.0.1:8787";\n const publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY ?? "mock-dev";\n return createCimplifyClient({\n baseUrl,\n publicKey,\n suppressPublicKeyWarning: true,\n });\n }, []);\n\n return (\n <CimplifyProvider client={client}>\n <CartDrawerProvider>{children}</CartDrawerProvider>\n </CimplifyProvider>\n );\n}\n' }, { "path": "components/footer.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport { brand } from "@/lib/brand";\n\nconst ICONS: Record<string, React.ReactNode> = {\n instagram: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" aria-hidden className="w-5 h-5">\n <rect x="3" y="3" width="18" height="18" rx="5" />\n <circle cx="12" cy="12" r="4" />\n <circle cx="17.5" cy="6.5" r="0.75" fill="currentColor" />\n </svg>\n ),\n x: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M18 2h3l-7.5 8.6L22 22h-6.6l-5-6.5L4 22H1l8-9.2L1.4 2H8l4.6 6 5.4-6z" />\n </svg>\n ),\n tiktok: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M16 3v3.5a4.5 4.5 0 0 0 4.5 4.5V14a7.5 7.5 0 0 1-4.5-1.5V16a5 5 0 1 1-5-5v3a2 2 0 1 0 2 2V3z" />\n </svg>\n ),\n facebook: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M14 9V7a1 1 0 0 1 1-1h2V3h-3a4 4 0 0 0-4 4v2H8v3h2v9h3v-9h2.5l.5-3H13z" />\n </svg>\n ),\n youtube: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M22.5 6.5a2.6 2.6 0 0 0-1.8-1.8C19 4.2 12 4.2 12 4.2s-7 0-8.7.5A2.6 2.6 0 0 0 1.5 6.5C1 8.2 1 12 1 12s0 3.8.5 5.5a2.6 2.6 0 0 0 1.8 1.8C5 19.8 12 19.8 12 19.8s7 0 8.7-.5a2.6 2.6 0 0 0 1.8-1.8c.5-1.7.5-5.5.5-5.5s0-3.8-.5-5.5zM10 15.5v-7l6 3.5-6 3.5z" />\n </svg>\n ),\n linkedin: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M4 4h4v16H4zM6 2.5a2 2 0 1 0 0 4 2 2 0 0 0 0-4zM10 8h4v2.5h.1c.6-1.1 2-2.5 4-2.5 4 0 4.9 2.6 4.9 6V20h-4v-5c0-1.5-.5-3-2.3-3-1.7 0-2.5 1.3-2.5 3v5h-4z" />\n </svg>\n ),\n whatsapp: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M12 2a10 10 0 0 0-8.6 15l-1.4 5 5.2-1.4A10 10 0 1 0 12 2zm5 14.2c-.2.6-1.2 1.2-1.7 1.2-.5.1-1.1.1-1.7-.1-.4-.1-.9-.3-1.5-.6a8.4 8.4 0 0 1-3.7-3.4c-.7-1-1-1.8-1-2.5 0-.7.4-1.1.6-1.3.2-.2.4-.2.5-.2h.4c.1 0 .3 0 .4.3l.6 1.4c.1.2 0 .3 0 .4l-.3.4-.3.3c-.1.1-.2.2-.1.4.2.4.7 1.1 1.4 1.8.9.8 1.7 1.1 1.9 1.2.2.1.3.1.5-.1l.6-.7c.2-.2.3-.2.5-.1l1.4.7c.2.1.3.2.4.3.1.2.1.6 0 1z" />\n </svg>\n ),\n};\n\nconst FALLBACK_ICON = (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" aria-hidden className="w-5 h-5">\n <circle cx="12" cy="12" r="9" />\n </svg>\n);\n\nexport async function Footer() {\n "use cache";\n const year = new Date().getFullYear();\n return (\n <footer className="mt-12 px-6 sm:px-8 py-10 text-xs text-muted-foreground border-t border-border bg-card">\n <div className="max-w-7xl mx-auto">\n <div className="grid gap-10 md:grid-cols-[1.4fr_repeat(4,1fr)]">\n <div>\n <p className="font-serif text-2xl text-foreground m-0 mb-2">{brand.name}</p>\n <p className="leading-relaxed mb-4 max-w-sm">{brand.footer.blurb}</p>\n <address className="not-italic space-y-1">\n <p className="m-0">{brand.contact.address}</p>\n <p className="m-0">\n <a\n href={`tel:${brand.contact.phoneTel}`}\n className="hover:text-foreground transition-colors"\n >\n {brand.contact.phone}\n </a>\n </p>\n <p className="m-0">\n <a\n href={`mailto:${brand.contact.email}`}\n className="hover:text-foreground transition-colors"\n >\n {brand.contact.email}\n </a>\n </p>\n <p className="m-0">{brand.contact.hours}</p>\n </address>\n <div className="flex items-center gap-3 mt-5">\n {brand.socials.map((s) => (\n <a\n key={s.label}\n href={s.href}\n aria-label={s.label}\n target="_blank"\n rel="noopener noreferrer"\n className="inline-flex items-center justify-center w-9 h-9 rounded-full border border-border text-muted-foreground hover:text-foreground hover:border-foreground transition-colors"\n >\n {(s.icon && ICONS[s.icon]) ?? FALLBACK_ICON}\n </a>\n ))}\n </div>\n </div>\n {brand.footer.sitemap.map((section) => (\n <nav key={section.title} aria-labelledby={`footer-${section.title}`}>\n <p\n id={`footer-${section.title}`}\n className="font-semibold text-foreground mb-3 text-[13px] uppercase tracking-[0.08em]"\n >\n {section.title}\n </p>\n <ul className="space-y-2 m-0 p-0 list-none">\n {section.links.map((link) => (\n <li key={link.label}>\n <Link\n href={link.href}\n className="hover:text-foreground transition-colors"\n >\n {link.label}\n </Link>\n </li>\n ))}\n </ul>\n </nav>\n ))}\n </div>\n <div className="mt-12 pt-6 border-t border-border flex flex-col sm:flex-row items-center justify-between gap-3">\n <p className="m-0">\xA9 {year} {brand.name}. All rights reserved.</p>\n {brand.footer.poweredBy && (\n <p className="m-0 inline-flex items-center gap-1.5">\n <span className="text-muted-foreground/80">Powered by</span>\n <a\n href={brand.footer.poweredBy.href}\n target="_blank"\n rel="noopener noreferrer"\n aria-label={brand.footer.poweredBy.label}\n className="inline-flex items-center gap-1 font-serif text-foreground hover:text-primary transition-colors"\n >\n <span className="font-semibold tracking-tight">{brand.footer.poweredBy.label}</span>\n <svg\n viewBox="0 0 12 12"\n aria-hidden\n className="w-3 h-3 opacity-70"\n fill="none"\n stroke="currentColor"\n strokeWidth="1.5"\n >\n <path d="M3 9L9 3M9 3H4M9 3v5" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n </a>\n </p>\n )}\n </div>\n </div>\n </footer>\n );\n}\n' }, { "path": "components/store-product-card.tsx", "kind": "text", "content": '"use client";\n\nimport Link from "next/link";\nimport {\n CardVariant,\n FoodProductCard,\n RetailProductCard,\n WholesaleProductCard,\n DigitalProductCard,\n BundleProductCard,\n CompositeProductCard,\n StandardServiceCard,\n CompactServiceCard,\n ScheduleServiceCard,\n RentalServiceCard,\n AccommodationCard,\n LeaseServiceCard,\n SubscriptionCard,\n} from "@cimplify/sdk/react";\nimport type { CardLayoutProps } from "@cimplify/sdk/react";\nimport type { Product } from "@cimplify/sdk";\nimport { PRODUCT_TYPE, RENDER_HINT, DURATION_UNIT } from "@cimplify/sdk";\n\nconst RENTAL_UNITS = new Set<string>([DURATION_UNIT.Days, DURATION_UNIT.Hours]);\nconst LEASE_UNITS = new Set<string>([DURATION_UNIT.Weeks, DURATION_UNIT.Months, DURATION_UNIT.Years]);\n\nconst VARIANT_CARDS: Record<CardVariant, React.ComponentType<CardLayoutProps>> = {\n [CardVariant.Food]: FoodProductCard,\n [CardVariant.Retail]: RetailProductCard,\n [CardVariant.Wholesale]: WholesaleProductCard,\n [CardVariant.Digital]: DigitalProductCard,\n [CardVariant.Bundle]: BundleProductCard,\n [CardVariant.Composite]: CompositeProductCard,\n [CardVariant.Standard]: StandardServiceCard,\n [CardVariant.Compact]: CompactServiceCard,\n [CardVariant.Schedule]: ScheduleServiceCard,\n [CardVariant.Rental]: RentalServiceCard,\n [CardVariant.Accommodation]: AccommodationCard,\n [CardVariant.Lease]: LeaseServiceCard,\n [CardVariant.Subscription]: SubscriptionCard,\n};\n\nfunction resolveVariant(p: Product): CardVariant {\n if (p.type === PRODUCT_TYPE.Bundle) return CardVariant.Bundle;\n if (p.type === PRODUCT_TYPE.Composite) return CardVariant.Composite;\n if (p.quantity_pricing && p.quantity_pricing.length > 1) return CardVariant.Wholesale;\n if (p.type === PRODUCT_TYPE.Digital) return CardVariant.Digital;\n if (p.type === PRODUCT_TYPE.Service) {\n if (p.duration_unit && RENTAL_UNITS.has(p.duration_unit)) return CardVariant.Rental;\n if (p.duration_unit === DURATION_UNIT.Nights) return CardVariant.Accommodation;\n if (p.duration_unit && LEASE_UNITS.has(p.duration_unit)) return CardVariant.Lease;\n if (p.billing_plans && p.billing_plans.length > 0 && !p.duration_minutes) return CardVariant.Subscription;\n return CardVariant.Standard;\n }\n if (p.render_hint === RENDER_HINT.Food) return CardVariant.Food;\n return CardVariant.Retail;\n}\n\ninterface Props {\n product: Product;\n /** Override the auto-detected card variant. */\n variant?: CardVariant;\n}\n\n/**\n * Variant-aware product card that opens the URL-driven product modal on\n * click. Uses `next/link` with `?product=<slug>` so the link is statically\n * pre-renderable \u2014 no `useSearchParams` dependency means cards don\'t\n * become dynamic islands. The modal (which already reads `useSearchParams`\n * inside a Suspense boundary in the root layout) handles the rest.\n */\nexport function StoreProductCard({ product, variant }: Props) {\n const slug = product.slug || product.id;\n const Card = VARIANT_CARDS[variant ?? resolveVariant(product)];\n const href = `?product=${encodeURIComponent(slug)}`;\n\n return (\n <Card\n product={product}\n renderLink={({ className, children }) => (\n <Link href={href} scroll={false} prefetch={false} className={className}>\n {children}\n </Link>\n )}\n />\n );\n}\n' }, { "path": "components/json-ld.tsx", "kind": "text", "content": 'import { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\n\n/**\n * Streams the organization JSON-LD script tag in async so it doesn\'t block\n * the rest of the layout shell. `getSiteUrl` reads request headers, and\n * any await on dynamic data inside `RootLayout` makes Next 16 mark the\n * whole route as blocking.\n */\nexport async function OrganizationJsonLd(): Promise<React.ReactElement> {\n const siteUrl = await getSiteUrl();\n const ld = {\n "@context": "https://schema.org",\n "@type": brand.schemaType,\n name: brand.name,\n url: siteUrl,\n description: brand.description,\n email: brand.contact.email,\n telephone: brand.contact.phoneTel,\n address: {\n "@type": "PostalAddress",\n streetAddress: brand.contact.streetAddress,\n addressLocality: brand.contact.city,\n addressCountry: brand.contact.countryCode,\n },\n sameAs: brand.socials.map((s) => s.href),\n };\n return (\n <script\n type="application/ld+json"\n dangerouslySetInnerHTML={{ __html: JSON.stringify(ld) }}\n />\n );\n}\n' }, { "path": "components/nav-link.tsx", "kind": "text", "content": '"use client";\n\nimport Link from "next/link";\nimport { usePathname } from "next/navigation";\n\nexport function NavLink({ href, children }: { href: string; children: React.ReactNode }) {\n const pathname = usePathname();\n const active = pathname === href;\n return (\n <Link\n href={href}\n className={[\n "text-[13px] font-medium tracking-wide transition-colors",\n active ? "text-primary" : "text-muted-foreground hover:text-foreground",\n ].join(" ")}\n >\n {children}\n </Link>\n );\n}\n' }, { "path": "components/category-grid.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CategoryGrid as SdkCategoryGrid } from "@cimplify/sdk/react";\nimport type { Category } from "@cimplify/sdk";\n\n/**\n * Homepage category tiles \u2014 defers to the SDK\'s <CategoryGrid/> for layout\n * and accessibility, and routes selections to /categories/:slug.\n */\nexport function CategoryGrid({ categories }: { categories?: Category[] }) {\n const router = useRouter();\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 pt-14">\n <h2 className="font-serif text-[28px] font-semibold m-0 mb-5">Browse the menu</h2>\n <SdkCategoryGrid\n categories={categories}\n onSelect={(c) => router.push(`/categories/${c.slug}`)}\n classNames={{\n item: "flex flex-col gap-1 p-6 bg-card border border-border rounded-2xl cursor-pointer text-left transition-all hover:border-primary hover:-translate-y-0.5",\n name: "font-serif text-lg font-semibold text-foreground",\n description: "text-xs text-muted-foreground",\n count: "text-xs text-muted-foreground mt-1",\n }}\n />\n </section>\n );\n}\n' }, { "path": "components/cart-drawer.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CartDrawer as SdkCartDrawer } from "@cimplify/sdk/react";\n\n/**\n * Side-drawer cart. Auto-opens when an item is added (via\n * `<CartDrawerProvider>` in `providers.tsx`). The header\'s cart pill\n * also calls `useCartDrawer().open()` to reveal it on click.\n */\nexport function CartDrawer() {\n const router = useRouter();\n return <SdkCartDrawer onCheckout={() => router.push("/checkout")} />;\n}\n' }, { "path": "next.config.ts", "kind": "text", "content": 'import type { NextConfig } from "next";\n\n// Same-origin proxy target for the storefront API. The public key\n// decides: a real `cpk_live_\u2026` / `cpk_test_\u2026` only resolves against\n// hosted Cimplify, so browser cart writes go to the same place the\n// server catalogue reads come from. Anything else (`mock-dev`, empty)\n// falls back to the local `cimplify dev` mock in dev.\nconst publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY?.trim() ?? "";\nconst keyTargetsHostedCimplify =\n publicKey.startsWith("cpk_live_") || publicKey.startsWith("cpk_test_");\nconst STOREFRONT_URL =\n process.env.NODE_ENV === "production" || keyTargetsHostedCimplify\n ? "https://storefronts.cimplify.io"\n : "http://127.0.0.1:8787";\n\nif (STOREFRONT_URL === "http://127.0.0.1:8787") {\n console.warn(\n "[cimplify] next.config resolved the local storefront API URL; production deploys must run with NODE_ENV=production.",\n );\n}\n\nconst nextConfig: NextConfig = {\n // Enable Next 16\'s `cacheComponents` mode so we can use `\'use cache\'` +\n // `cacheTag` / `cacheLife` for SSR caching. Cached chrome streams first,\n // dynamic Suspense boundaries fill in when ready.\n cacheComponents: true,\n async redirects() {\n // Config-level so cacheComponents needn\'t prerender a redirect()-only page.\n return [\n { source: "/login", destination: "/account", permanent: false },\n { source: "/signup", destination: "/account", permanent: false },\n ];\n },\n async rewrites() {\n return [\n { source: "/api/v1/:path*", destination: `${STOREFRONT_URL}/api/v1/:path*` },\n { source: "/img/:path*", destination: `${STOREFRONT_URL}/img/:path*` },\n { source: "/elements/:path*", destination: `${STOREFRONT_URL}/elements/:path*` },\n { source: "/_mock/:path*", destination: `${STOREFRONT_URL}/_mock/:path*` },\n ];\n },\n images: {\n loader: "custom",\n loaderFile: "./lib/cimplify-loader.ts",\n remotePatterns: [\n { protocol: "http", hostname: "127.0.0.1", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "3000", pathname: "/img/**" },\n { protocol: "https", hostname: "loremflickr.com", pathname: "/**" },\n { protocol: "https", hostname: "images.unsplash.com", pathname: "/**" },\n { protocol: "https", hostname: "static-tmp.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "storefrontassetscdn.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "cdn.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "res.cloudinary.com", pathname: "/cimplify/**" },\n ],\n },\n};\n\nexport default nextConfig;\n' }, { "path": ".claude/skills/cimplify-storefront/SKILL.md", "kind": "text", "content": "---\nname: cimplify-storefront\ndescription: Build, customize, rebrand, or deploy a Cimplify-scaffolded storefront. Triggers when the user asks to create a storefront, rebrand a Cimplify template, change the palette, add a page, deploy to Cimplify, or works in a project containing `lib/brand.ts` and `next.config.ts` with `cacheComponents`.\n---\n\n# Cimplify Storefront skill\n\nYou're working on a project scaffolded from `cimplify init`. The architecture is opinionated and the rebrand surface is intentionally small. Read `AGENTS.md` at the project root for the file \u2194 brand-field map for *this template's* industry; this skill gives you the playbook that's the same across all six.\n\n## The contract \u2014 never break\n\n1. **`lib/brand.ts` is the only place for content edits.** Every visible string reads from this file. If a string isn't in `brand`, *add a field* to the `Brand` interface \u2014 don't hardcode it in a page or component.\n2. **`app/globals.css` `@theme { \u2026 }`** holds palette + radius + font references. To re-skin the entire site, edit only this block.\n3. **Server Components are cached** via `'use cache'` + `cacheTag(tags.X())` + `cacheLife(\"hours\")`. Don't downgrade them to `\"use client\"` to avoid an issue \u2014 fix the issue.\n4. **Client islands** (anything reading `useSearchParams`, `usePathname`, `useRouter`, `useState`) live behind `<Suspense>`.\n5. **`cacheComponents: true`** in `next.config.ts` is non-negotiable. Don't turn it off.\n6. **`bun run test:run` (vitest)** is the canonical test runner. `bun test` will show false failures because Bun's `vi` shim is incomplete.\n7. **Cart, checkout, orders stay client.** They're session-bound. Don't try to SSR them.\n\n## The brand-field schema (in `lib/brand.ts`)\n\n```\nidentity: name, shortName, microTag, description, schemaType, currency, locale\ncontact: address, streetAddress, city, countryCode, phone, phoneTel, email, privacyEmail, hours\nsocials: [{ label, href, icon }] // icon \u2208 instagram|x|tiktok|facebook|youtube|linkedin|whatsapp\nheader: { nav: [{ label, href }] }\nhero: { badge, title, subtitle, primaryCtaLabel, secondaryCtaLabel?, secondaryCtaHref? }\noptional: trustItems[]?, brandStrip?, promo?, tradeIn? // render conditionally\nnewsletter: { eyebrow, title, body, placeholder, submitLabel, successLabel }\nabout: { eyebrow, title, paragraphs[], sections[{ heading, body }] }\nfaq: { eyebrow, title, sections[{ title, items[{ q, a }] }], contactPrompt, contactEmail }\nterms,\nprivacy,\nshipping,\nreturns,\naccessibility: { eyebrow, title, lastUpdated, sections[{ heading, body | { intro, bullets[] } }] }\naccount: eyebrows + titles for /account, /login, /signup\ncontactPage: { eyebrow, title, body, reasons[], directLines[{ label, value, href }] }\ntrackOrder: { eyebrow, title, body }\nfooter: { blurb, sitemap[{ title, links[] }], poweredBy? }\nllms: { summary } // opens /llms.txt\nmock: { seed, businessId }\n```\n\n## Playbook \u2014 common tasks\n\n### Rebrand a storefront for a new merchant\n\n1. Edit `lib/brand.ts`. Replace every field with the merchant's content. Use the brief / context the user gave you.\n2. Edit `app/globals.css` `@theme { \u2026 }`. Change `--color-primary`, `--color-background`, `--color-foreground`, optionally `--radius`. Use OKLCH; if the brand only gave hex, convert.\n3. (Optional) Swap fonts in `app/layout.tsx` \u2014 `next/font/google` import + the variable wired into the `<html>` className.\n4. Set `.env.local`: `NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY` (optional: `NEXT_PUBLIC_SITE_URL`).\n\nDon't touch any other file. If the rebrand needs content not in the schema, add the field to `Brand` first, populate it, then read it from the page.\n\n### Add a new section / component\n\n1. Build it as a Server Component in `components/` (or a client island in `*-client.tsx` if interactive).\n2. Read merchant copy from `brand`. Add new fields to the `Brand` interface if needed.\n3. Wrap interactive bits in `<Suspense fallback={\u2026}>` so the cached chrome streams.\n4. Compose into the page.\n\n### Wire a Server Action that mutates data\n\n```ts\n\"use server\";\nimport { getServerClient, revalidateProducts } from \"@cimplify/sdk/server\";\n\nexport async function createProduct(input: ProductInput) {\n await getServerClient().catalogue.createProduct(input);\n revalidateProducts();\n}\n```\n\nAfter every mutation, call the matching `revalidate*` helper from `@cimplify/sdk/server`: `revalidateProducts`, `revalidateProduct(id)`, `revalidateCategories`, `revalidateCategory(id)`, `revalidateCollections`, `revalidateCollection(id)`, `revalidateBusiness`.\n\n### Add a Server Component data fetch\n\n```ts\nimport { cacheTag, cacheLife } from \"next/cache\";\nimport { getServerClient, tags } from \"@cimplify/sdk/server\";\n\nasync function getX() {\n \"use cache\";\n cacheTag(tags.products());\n cacheLife(\"hours\");\n const r = await getServerClient().catalogue.getProducts({ limit: 24 });\n if (!r.ok) throw new Error(r.error.message);\n return r.value.items;\n}\n```\n\n### Eject an SDK component for deeper customization\n\nTry `classNames`, `renderImage`, `renderLink`, slot props first. If those run out:\n\n```bash\ncimplify list\ncimplify add product-card --dir src/components/cimplify\n```\n\nOnce ejected, the file is yours. Hooks/types still come from `@cimplify/sdk`.\n\n### Deploy\n\n```bash\ncimplify login\ncimplify projects create my-store\ncimplify link <project-id>\ncimplify env push\ncimplify deploy --prod\ncimplify logs --follow\ncimplify domains add my-store.com\n```\n\n## Pitfalls \u2014 explicit \u274C list\n\n- \u274C Hardcoding any visible string in a page or component. Always `brand.X`.\n- \u274C Disabling `cacheComponents: true` to silence a warning. Fix the warning by wrapping the dynamic call in `<Suspense>`.\n- \u274C Using `unstable_cache` \u2014 Next 16's canonical primitive is `'use cache'` + `cacheTag` + `cacheLife`.\n- \u274C Bypassing `getServerClient()` and calling `createCimplifyClient` directly in a Server Component \u2014 loses per-request memoization.\n- \u274C Mutating data without calling the matching `revalidate*` helper.\n- \u274C Adding an `app/error.tsx` handler that calls `reset()` without logging \u2014 silently swallowed errors hide bugs.\n- \u274C Running `bun test` and reporting failures. Use `bun run test:run` (vitest).\n- \u274C Editing the per-template `AGENTS.md` to remove notes about TODOs (contact form, newsletter fake submits) \u2014 they're real.\n\n## Where things live\n\n| Need | Look here |\n|---|---|\n| Which page reads which `brand.X` field | `AGENTS.md` at project root |\n| Architectural rules | `AGENTS.md` at project root + this skill |\n| Running locally | `bun dev` (boots mock + Next together) |\n| Switching mock seed | edit `dev:mock` in `package.json` |\n| Full SDK reference | `docs/sdk/storefronts.md` in the Cimplify repo |\n\n## What to do when the user asks something out-of-scope\n\nIf the user asks for something the template doesn't support out of the box:\n\n1. **Check first** \u2014 is there an SDK component or hook that does it? `@cimplify/sdk/react` has 50+ components, 30+ hooks. Search `node_modules/@cimplify/sdk/dist/react.d.mts` if needed.\n2. **Compose from existing parts** before authoring new ones.\n3. **If genuinely missing** \u2014 build the new component, hoist its strings into `brand.ts`, document in `AGENTS.md`.\n\nDon't introduce competing patterns (a new caching strategy, a new auth flow, a new theming system). The template's opinions are intentional.\n" }, { "path": ".cursor/rules/cimplify-storefront.mdc", "kind": "binary", "content": "LS0tCmRlc2NyaXB0aW9uOiBDaW1wbGlmeSBzdG9yZWZyb250IOKAlCBhcmNoaXRlY3R1cmFsIHJ1bGVzIGFuZCByZWJyYW5kIGNvbnRyYWN0IGZvciBhIHByb2plY3Qgc2NhZmZvbGRlZCBmcm9tIGBjaW1wbGlmeSBpbml0YC4gVHJpZ2dlcnMgd2hlbiBmaWxlcyBsaWtlIGxpYi9icmFuZC50cywgYXBwL2dsb2JhbHMuY3NzLCBjb21wb25lbnRzL2hlYWRlci50c3gsIG9yIC5jaW1wbGlmeS9wcm9qZWN0Lmpzb24gYXJlIGluIHRoZSB3b3Jrc3BhY2UuCmdsb2JzOiBbImxpYi9icmFuZC50cyIsICJhcHAvKioiLCAiY29tcG9uZW50cy8qKiIsICIuZW52LmxvY2FsIiwgIm5leHQuY29uZmlnLnRzIl0KYWx3YXlzQXBwbHk6IHRydWUKLS0tCgojIENpbXBsaWZ5IHN0b3JlZnJvbnQKClRoaXMgcHJvamVjdCB3YXMgc2NhZmZvbGRlZCBmcm9tIGEgQ2ltcGxpZnkgdGVtcGxhdGUuIFJlYWQgYEFHRU5UUy5tZGAgYXQgdGhlIHByb2plY3Qgcm9vdCBmb3IgdGhlIGZpbGUg4oaUIGBicmFuZC5YYCBmaWVsZCBtYXAgYW5kIGAuY2xhdWRlL3NraWxscy9jaW1wbGlmeS1zdG9yZWZyb250L1NLSUxMLm1kYCBmb3IgdGhlIGZ1bGwgcGxheWJvb2suCgojIyBUaGUgY29udHJhY3QKCjEuICoqYGxpYi9icmFuZC50c2AqKiDigJQgZXZlcnkgdmlzaWJsZSBzdHJpbmcuIElmIHNvbWV0aGluZyBpc24ndCB0aGVyZSwgYWRkIGEgZmllbGQgdG8gdGhlIGBCcmFuZGAgaW50ZXJmYWNlOyBkb24ndCBoYXJkY29kZS4KMi4gKipgYXBwL2dsb2JhbHMuY3NzYCBgQHRoZW1lYCBibG9jayoqIOKAlCBwYWxldHRlLCByYWRpdXMsIGZvbnQgcmVmZXJlbmNlcy4KMy4gKipTZXJ2ZXIgQ29tcG9uZW50cyBhcmUgY2FjaGVkKiogdmlhIGAndXNlIGNhY2hlJ2AgKyBgY2FjaGVUYWcodGFncy5YKCkpYCArIGBjYWNoZUxpZmUoImhvdXJzIilgLiBEb24ndCBkb3duZ3JhZGUgdG8gY2xpZW50IHRvIHNpbGVuY2UgYSB3YXJuaW5nLgo0LiAqKkNsaWVudCBpc2xhbmRzKiogYmVoaW5kIGA8U3VzcGVuc2U+YC4KNS4gKipgY2FjaGVDb21wb25lbnRzOiB0cnVlYCoqIGluIGBuZXh0LmNvbmZpZy50c2AgaXMgbm9uLW5lZ290aWFibGUuCjYuIFVzZSAqKmBidW4gcnVuIHRlc3Q6cnVuYCoqICh2aXRlc3QpLCBub3QgYGJ1biB0ZXN0YC4KCiMjIERvbid0CgotIEhhcmRjb2RlIHZpc2libGUgc3RyaW5ncyBpbiBwYWdlcy9jb21wb25lbnRzLgotIFVzZSBgdW5zdGFibGVfY2FjaGVgIChOZXh0IDE2IHVzZXMgYCd1c2UgY2FjaGUnYCkuCi0gQnlwYXNzIGBnZXRTZXJ2ZXJDbGllbnQoKWAgKGxvc2VzIHBlci1yZXF1ZXN0IG1lbW9pemF0aW9uKS4KLSBNdXRhdGUgd2l0aG91dCBjYWxsaW5nIHRoZSBtYXRjaGluZyBgcmV2YWxpZGF0ZSpgIGhlbHBlciBmcm9tIGBAY2ltcGxpZnkvc2RrL3NlcnZlcmAuCg==" }, { "path": ".gitignore", "kind": "text", "content": "node_modules\n.next\nout\n.env\n.env.local\n.env*.local\n.DS_Store\n*.tsbuildinfo\nnext-env.d.ts\n" }, { "path": "app/about/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `About \u2014 ${brand.name}`,\n description: brand.description,\n};\n\nexport default function AboutPage() {\n const a = brand.about;\n const titleParts = a.title.split("\\n");\n return (\n <article className="max-w-3xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {a.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-6 -tracking-[0.02em]">\n {titleParts.map((line, i) => (\n <span key={i}>\n {line}\n {i < titleParts.length - 1 && <br />}\n </span>\n ))}\n </h1>\n <div className="prose prose-lg max-w-none space-y-5 text-foreground/90 leading-relaxed">\n {a.paragraphs.map((p, i) => (\n <p key={i}>{p}</p>\n ))}\n {a.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="font-serif text-3xl font-semibold mt-10 mb-3">{s.heading}</h2>\n <p>{s.body}</p>\n </div>\n ))}\n </div>\n </article>\n );\n}\n' }, { "path": "app/account/orders/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Orders \u2014 ${brand.name}`,\n};\n\nexport default function OrdersPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Your orders\n </h1>\n <AccountIframe section="orders" />\n </article>\n );\n}\n' }, { "path": "app/account/settings/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Settings \u2014 ${brand.name}`,\n};\n\nexport default function SettingsPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Settings\n </h1>\n <AccountIframe section="settings" />\n </article>\n );\n}\n' }, { "path": "app/account/addresses/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Addresses \u2014 ${brand.name}`,\n};\n\nexport default function AddressesPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Your addresses\n </h1>\n <AccountIframe section="addresses" />\n </article>\n );\n}\n' }, { "path": "app/account/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Account \u2014 ${brand.name}`,\n description: brand.account.signupSubtitle,\n};\n\nexport default function AccountPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {brand.account.accountEyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n {brand.account.accountTitle}\n </h1>\n <AccountIframe />\n </article>\n );\n}\n' }, { "path": "app/.well-known/ucp/route.ts", "kind": "text", "content": 'import { NextResponse } from "next/server";\n\n/**\n * UCP (Universal Commerce Protocol) manifest discovery endpoint.\n *\n * Agents \u2014 Claude, ChatGPT, Gemini, MCP clients \u2014 probe\n * `https://<your-domain>/.well-known/ucp` to learn what commerce\n * capabilities your storefront supports. We resolve the business\'s\n * UCP handle from the public key (via the storefront API), then forward\n * to Cimplify for the canonical manifest. Edge-cached for an hour\n * because capabilities change rarely.\n */\nconst UCP_API_BASE = "https://api.cimplify.io";\nconst STOREFRONT_API_BASE =\n process.env.NODE_ENV === "production"\n ? "https://storefronts.cimplify.io"\n : "http://127.0.0.1:8787";\n\nexport async function GET() {\n const publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY;\n if (!publicKey) {\n return NextResponse.json(\n {\n error: "NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY not set",\n remediation:\n "Set NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY in .env.local (and your deployment env).",\n },\n { status: 500 },\n );\n }\n\n try {\n // Step 1 \u2014 resolve the business handle from the public key.\n const businessResp = await fetch(`${STOREFRONT_API_BASE}/api/v1/business`, {\n headers: { "X-API-Key": publicKey, "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n });\n\n if (!businessResp.ok) {\n return NextResponse.json(\n { error: `Failed to resolve business: ${businessResp.status}` },\n { status: businessResp.status },\n );\n }\n\n const businessJson = await businessResp.json();\n const handle: string | undefined = businessJson?.data?.handle;\n if (!handle) {\n return NextResponse.json(\n { error: "Business has no handle configured" },\n { status: 500 },\n );\n }\n\n // Step 2 \u2014 fetch the canonical UCP manifest.\n const manifestResp = await fetch(\n `${UCP_API_BASE}/ucp/v1/${handle}/manifest`,\n {\n headers: { "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n },\n );\n\n if (!manifestResp.ok) {\n return NextResponse.json(\n { error: `Upstream UCP manifest fetch failed: ${manifestResp.status}` },\n { status: manifestResp.status },\n );\n }\n\n const manifest = await manifestResp.json();\n return NextResponse.json(manifest, {\n headers: {\n "Content-Type": "application/json",\n "Cache-Control": "public, max-age=3600, s-maxage=3600",\n },\n });\n } catch (error) {\n return NextResponse.json(\n {\n error: "Failed to fetch UCP manifest",\n detail: error instanceof Error ? error.message : String(error),\n },\n { status: 500 },\n );\n }\n}\n' }, { "path": "app/search/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { SearchClient } from "./search-client";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Search \u2014 ${brand.name}`,\n description: `Search ${brand.name} \u2014 products, collections, categories.`,\n};\n\nexport default function SearchPage() {\n return (\n <article className="max-w-7xl mx-auto px-6 sm:px-8 py-10">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Search\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Find anything.\n </h1>\n <Suspense fallback={<SearchSkeleton />}>\n <SearchClient />\n </Suspense>\n </article>\n );\n}\n\nfunction SearchSkeleton() {\n return (\n <div>\n <div className="h-12 w-full max-w-xl bg-muted rounded animate-pulse mb-8" />\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {Array.from({ length: 8 }).map((_, i) => (\n <div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </div>\n );\n}\n' }, { "path": "app/search/search-client.tsx", "kind": "text", "content": '"use client";\n\nimport { SearchPage } from "@cimplify/sdk/react";\n\nexport function SearchClient() {\n return <SearchPage />;\n}\n' }, { "path": "app/faq/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `FAQ \u2014 ${brand.name}`,\n description: "Delivery, pickup, custom cakes, allergens, payment \u2014 answers to the questions we hear most often.",\n};\n\nexport default function FaqPage() {\n const f = brand.faq;\n return (\n <article className="max-w-3xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {f.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-10 -tracking-[0.02em]">\n {f.title}\n </h1>\n <div className="space-y-12">\n {f.sections.map((section) => (\n <section key={section.title}>\n <h2 className="font-serif text-2xl font-semibold mb-5">{section.title}</h2>\n <dl className="space-y-6">\n {section.items.map((item) => (\n <div key={item.q}>\n <dt className="font-medium text-foreground mb-1.5">{item.q}</dt>\n <dd className="text-muted-foreground leading-relaxed">{item.a}</dd>\n </div>\n ))}\n </dl>\n </section>\n ))}\n </div>\n <p className="mt-12 pt-8 border-t border-border text-sm text-muted-foreground">\n {f.contactPrompt}{" "}\n <a\n href={`mailto:${f.contactEmail}`}\n className="text-primary font-semibold hover:underline"\n >\n {f.contactEmail}\n </a>{" "}\n and a real human will reply within 24 hours.\n </p>\n </article>\n );\n}\n' }, { "path": "app/robots.ts", "kind": "text", "content": 'import type { MetadataRoute } from "next";\nimport { getSiteUrl } from "@/lib/site-url";\n\nexport default async function robots(): Promise<MetadataRoute.Robots> {\n const siteUrl = await getSiteUrl();\n return {\n rules: [\n {\n userAgent: "*",\n allow: "/",\n disallow: ["/cart", "/checkout", "/orders/", "/api/"],\n },\n ],\n sitemap: `${siteUrl}/sitemap.xml`,\n host: siteUrl,\n };\n}\n' }, { "path": "app/track-order/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { TrackOrderForm } from "./track-order-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Track an order \u2014 ${brand.name}`,\n description: brand.trackOrder.body,\n};\n\nexport default function TrackOrderPage() {\n const t = brand.trackOrder;\n return (\n <article className="max-w-2xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {t.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,5vw,3rem)] font-semibold mb-4 -tracking-[0.02em]">\n {t.title}\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">{t.body}</p>\n <TrackOrderForm />\n </article>\n );\n}\n' }, { "path": "app/track-order/track-order-form.tsx", "kind": "text", "content": '"use client";\n\nimport { useState } from "react";\nimport { useRouter } from "next/navigation";\n\n/**\n * Guest order tracker. Routes /orders/<id> with the entered id; the\n * post-checkout confirmation page already lives at /orders/[id] so this\n * just sends the visitor there. Replace the redirect with a real lookup\n * (e.g. Cimplify orders API + email-match guard) for production.\n */\nexport function TrackOrderForm() {\n const router = useRouter();\n const [orderId, setOrderId] = useState("");\n const [email, setEmail] = useState("");\n\n return (\n <form\n onSubmit={(e) => {\n e.preventDefault();\n if (!orderId.trim()) return;\n router.push(`/orders/${encodeURIComponent(orderId.trim())}`);\n }}\n className="space-y-4 rounded-2xl border border-border bg-card p-6"\n >\n <div>\n <label\n htmlFor="orderId"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order number\n </label>\n <input\n id="orderId"\n name="orderId"\n required\n value={orderId}\n onChange={(e) => setOrderId(e.target.value)}\n placeholder="ord_abc123\u2026"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <div>\n <label\n htmlFor="email"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order email\n </label>\n <input\n id="email"\n name="email"\n type="email"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n placeholder="you@email.com"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <button\n type="submit"\n className="w-full inline-flex items-center justify-center px-5 py-3 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors"\n >\n Track order\n </button>\n </form>\n );\n}\n' }, { "path": "app/cart/page.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CartPage as SdkCartPage } from "@cimplify/sdk/react";\n\nexport default function CartPage() {\n const router = useRouter();\n return <SdkCartPage onCheckout={() => router.push("/checkout")} />;\n}\n' }, { "path": "app/llms.txt/route.ts", "kind": "text", "content": 'import { cacheTag, cacheLife } from "next/cache";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\n\nasync function buildLlmsTxt(SITE_URL: string): Promise<string> {\n "use cache";\n cacheTag(tags.products(), tags.categories(), tags.collections());\n cacheLife("hours");\n\n const client = getServerClient();\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n\n const products: Product[] = productsRes.ok ? productsRes.value.items : [];\n const categories = categoriesRes.ok ? categoriesRes.value : [];\n const collections = collectionsRes.ok ? collectionsRes.value : [];\n\n const lines: string[] = [];\n lines.push(`# ${brand.name}`);\n lines.push("");\n lines.push(`> ${brand.llms.summary}`);\n lines.push("");\n lines.push("## Browse");\n lines.push(`- [Home](${SITE_URL}/)`);\n lines.push(`- [Shop](${SITE_URL}/shop): Full menu with filter and sort.`);\n\n if (categories.length > 0) {\n lines.push("");\n lines.push("## Categories");\n for (const c of categories) {\n lines.push(\n `- [${c.name}](${SITE_URL}/categories/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (collections.length > 0) {\n lines.push("");\n lines.push("## Collections");\n for (const c of collections) {\n lines.push(\n `- [${c.name}](${SITE_URL}/collections/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (products.length > 0) {\n lines.push("");\n lines.push("## Products");\n for (const p of products) {\n const slug = p.slug ?? p.id;\n const price = `${brand.currency} ${p.default_price}`;\n const desc = p.description ? ` \u2014 ${p.description.replace(/\\s+/g, " ").slice(0, 200)}` : "";\n lines.push(`- [${p.name}](${SITE_URL}/shop?product=${slug}) (${price})${desc}`);\n }\n }\n\n lines.push("");\n lines.push("## Information");\n lines.push(`- [About](${SITE_URL}/about)`);\n lines.push(`- [FAQ](${SITE_URL}/faq)`);\n lines.push(`- [Terms of Service](${SITE_URL}/terms)`);\n lines.push(`- [Privacy Policy](${SITE_URL}/privacy)`);\n lines.push(`- [Sitemap (XML)](${SITE_URL}/sitemap.xml)`);\n lines.push("");\n lines.push("## Contact");\n lines.push(`- Email: ${brand.contact.email}`);\n lines.push(`- Phone: ${brand.contact.phone}`);\n lines.push(`- Address: ${brand.contact.address}`);\n\n return lines.join("\\n") + "\\n";\n}\n\n/**\n * `/llms.txt` \u2014 machine-readable site index for LLMs (per llmstxt.org).\n * Lets coding agents and chat assistants find products, categories, and\n * support pages without scraping HTML. Plain Markdown so it streams cheaply\n * into context windows.\n */\nexport async function GET(): Promise<Response> {\n const body = await buildLlmsTxt(await getSiteUrl());\n return new Response(body, {\n headers: {\n "Content-Type": "text/plain; charset=utf-8",\n "Cache-Control": "public, max-age=0, s-maxage=3600, stale-while-revalidate=86400",\n },\n });\n}\n' }, { "path": "app/sitemap.ts", "kind": "text", "content": 'import type { MetadataRoute } from "next";\nimport { getServerClient, type Product } from "@cimplify/sdk/server";\nimport { getSiteUrl } from "@/lib/site-url";\n\nconst STATIC_ROUTES: { path: string; priority: number; changeFrequency: "daily" | "weekly" | "monthly" }[] = [\n { path: "/", priority: 1.0, changeFrequency: "daily" },\n { path: "/shop", priority: 0.9, changeFrequency: "daily" },\n { path: "/about", priority: 0.5, changeFrequency: "monthly" },\n { path: "/faq", priority: 0.4, changeFrequency: "monthly" },\n { path: "/terms", priority: 0.2, changeFrequency: "monthly" },\n { path: "/privacy", priority: 0.2, changeFrequency: "monthly" },\n];\n\nexport default async function sitemap(): Promise<MetadataRoute.Sitemap> {\n const now = new Date();\n const siteUrl = await getSiteUrl();\n const client = getServerClient();\n\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n\n const products = productsRes.ok ? productsRes.value.items : [];\n const categories = categoriesRes.ok ? categoriesRes.value : [];\n const collections = collectionsRes.ok ? collectionsRes.value : [];\n\n const staticEntries: MetadataRoute.Sitemap = STATIC_ROUTES.map((r) => ({\n url: `${siteUrl}${r.path}`,\n lastModified: now,\n changeFrequency: r.changeFrequency,\n priority: r.priority,\n }));\n\n // Bakery uses a URL-driven product modal (`?product=<slug>` on the home or\n // shop pages). Emit those as deep-linkable URLs so search engines and LLMs\n // can index each product canonically.\n const productEntries: MetadataRoute.Sitemap = products.map((p: Product) => ({\n url: `${siteUrl}/shop?product=${encodeURIComponent(p.slug ?? p.id)}`,\n lastModified: p.updated_at ? new Date(p.updated_at) : now,\n changeFrequency: "weekly",\n priority: 0.7,\n }));\n\n const categoryEntries: MetadataRoute.Sitemap = categories.map((c) => ({\n url: `${siteUrl}/categories/${c.slug}`,\n lastModified: now,\n changeFrequency: "weekly",\n priority: 0.6,\n }));\n\n const collectionEntries: MetadataRoute.Sitemap = collections.map((c) => ({\n url: `${siteUrl}/collections/${c.slug}`,\n lastModified: now,\n changeFrequency: "weekly",\n priority: 0.6,\n }));\n\n return [...staticEntries, ...categoryEntries, ...collectionEntries, ...productEntries];\n}\n' }, { "path": "app/opensearch.xml/route.ts", "kind": "text", "content": 'import { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\n\n/**\n * OpenSearch description document \u2014 lets browsers add this site to the\n * address bar\'s search engine list. When users press Tab after typing the\n * domain, they get an inline search box that hits /search?q=...\n */\nexport async function GET(): Promise<Response> {\n const siteUrl = await getSiteUrl();\n const xml = `<?xml version="1.0" encoding="UTF-8"?>\n<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">\n <ShortName>${escapeXml(brand.shortName)}</ShortName>\n <Description>Search ${escapeXml(brand.name)}</Description>\n <InputEncoding>UTF-8</InputEncoding>\n <Url type="text/html" method="get" template="${siteUrl}/search?q={searchTerms}" />\n <Url type="application/opensearchdescription+xml" rel="self" template="${siteUrl}/opensearch.xml" />\n <moz:SearchForm>${siteUrl}/search</moz:SearchForm>\n</OpenSearchDescription>\n`;\n return new Response(xml, {\n headers: {\n "Content-Type": "application/opensearchdescription+xml; charset=utf-8",\n "Cache-Control": "public, max-age=86400",\n },\n });\n}\n\nfunction escapeXml(s: string): string {\n return s\n .replace(/&/g, "&amp;")\n .replace(/</g, "&lt;")\n .replace(/>/g, "&gt;")\n .replace(/"/g, "&quot;")\n .replace(/\'/g, "&apos;");\n}\n' }, { "path": "app/contact/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { ContactForm } from "./contact-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Contact \u2014 ${brand.name}`,\n description: brand.contactPage.body,\n};\n\nexport default function ContactPage() {\n const c = brand.contactPage;\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {c.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-4 -tracking-[0.02em]">\n {c.title}\n </h1>\n <p className="text-lg text-muted-foreground max-w-2xl mb-12 leading-relaxed">{c.body}</p>\n\n <div className="grid grid-cols-1 lg:grid-cols-[1.5fr_1fr] gap-10 items-start">\n <ContactForm reasons={c.reasons} />\n <aside className="space-y-5 lg:pl-10 lg:border-l border-border">\n <div>\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Direct lines\n </p>\n <ul className="space-y-3 list-none p-0 m-0">\n {c.directLines.map((line) => (\n <li key={line.label}>\n <p className="text-xs text-muted-foreground m-0">{line.label}</p>\n <a\n href={line.href}\n className="text-foreground hover:text-primary transition-colors"\n >\n {line.value}\n </a>\n </li>\n ))}\n </ul>\n </div>\n <div>\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Visit\n </p>\n <p className="text-foreground m-0">{brand.contact.address}</p>\n <p className="text-xs text-muted-foreground mt-1">{brand.contact.hours}</p>\n </div>\n </aside>\n </div>\n </article>\n );\n}\n' }, { "path": "app/contact/contact-form.tsx", "kind": "text", "content": `"use client";
3532
+ ` }, { "path": "lib/cimplify-loader.ts", "kind": "text", "content": 'import type { ImageLoader } from "next/image";\n\nimport { assetUrl, isCimplifyAsset } from "@cimplify/sdk";\n\nconst cdnBase = process.env.NEXT_PUBLIC_CIMPLIFY_CDN_URL?.trim() || undefined;\n\nconst cimplifyImageLoader: ImageLoader = ({ src, width, quality }) => {\n if (isCimplifyAsset(src, cdnBase)) {\n return assetUrl(src, {\n base: cdnBase,\n w: width,\n quality,\n format: "auto",\n });\n }\n return src;\n};\n\nexport default cimplifyImageLoader;\n' }, { "path": "lib/site.config.ts", "kind": "text", "content": "/**\n * Canonical absolute URL. The platform overwrites this at build time with the\n * storefront's primary domain; the default only applies to local builds. It's\n * per-deploy config, not per-request, so it stays a static constant \u2014 keeping\n * the root layout prerenderable under `cacheComponents`.\n */\nexport const SITE_URL = \"https://example.com\";\n" }, { "path": "lib/cart.ts", "kind": "text", "content": '"use client";\n\nimport { useCart } from "@cimplify/sdk/react";\n\n/**\n * Reactive cart count for the header pill. Subscribes to the SDK cart\n * state \u2014 updates immediately on add / remove / quantity change.\n */\nexport function useCartCount(): { count: number } {\n const { itemCount } = useCart();\n return { count: itemCount };\n}\n' }, { "path": "lib/site-url.ts", "kind": "text", "content": 'import { SITE_URL } from "./site.config";\n\n/** Canonical absolute URL for this storefront. */\nexport async function getSiteUrl(): Promise<string> {\n return SITE_URL;\n}\n' }, { "path": "metadata.json", "kind": "text", "content": '{\n "id": "restaurant",\n "name": "Restaurant",\n "tagline": "Menu-driven storefront with reservations and table-service support.",\n "industry": "food",\n "tags": ["food", "restaurant", "reservations"],\n "stability": "stable",\n "schemaType": "Restaurant",\n "mock": {\n "seedName": "restaurant",\n "seedBusinessId": "bus_mamas_kitchen"\n }\n}\n' }, { "path": "tsconfig.json", "kind": "text", "content": '{\n "compilerOptions": {\n "target": "ES2022",\n "lib": ["dom", "dom.iterable", "esnext"],\n "allowJs": false,\n "skipLibCheck": true,\n "strict": true,\n "noEmit": true,\n "esModuleInterop": true,\n "module": "esnext",\n "moduleResolution": "bundler",\n "resolveJsonModule": true,\n "isolatedModules": true,\n "jsx": "preserve",\n "incremental": true,\n "plugins": [{ "name": "next" }],\n "paths": {\n "@/*": ["./*"]\n }\n },\n "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts"],\n "exclude": ["node_modules"]\n}\n' }, { "path": "CLAUDE.md", "kind": "text", "content": "# CLAUDE.md\n\nThis project was scaffolded from a Cimplify storefront template (`cimplify init`).\n\n## Read these first\n\n- **`AGENTS.md`** at the project root \u2014 the file \u2194 `brand.X` field map for *this template's* industry, plus the architectural rules.\n- **`.claude/skills/cimplify-storefront/SKILL.md`** \u2014 the playbook for common tasks (rebrand, add a section, wire a Server Action, deploy, eject a component). Invoke it whenever you're asked to change content, palette, copy, or routing.\n\n## The two-line summary\n\n1. **Edit `lib/brand.ts`** for any visible string.\n2. **Edit `app/globals.css` `@theme` block** for any palette / radius / font change.\n\nThat covers ~95% of what merchants ask for. For anything else, follow the `cimplify-storefront` skill.\n\n## Don't do\n\n- Hardcode strings in pages or components.\n- Disable `cacheComponents: true` in `next.config.ts`.\n- Use `unstable_cache` \u2014 Next 16's canonical primitive is `'use cache'`.\n- Run `bun test` (Bun's `vi` shim is incomplete) \u2014 use `bun run test:run`.\n" }, { "path": "README.md", "kind": "text", "content": "# __STOREFRONT_NAME__\n\nA Cimplify storefront scaffolded from the **bakery** template \u2014 Next.js 16 (App Router), React 19, Tailwind v4. Warm food-styled palette, Playfair Display + Inter typography, designed for bakeries, p\xE2tisseries, and food businesses.\n\nFor a different industry, scaffold with `--template`:\n\n```bash\ncimplify init my-store --template retail # electronics / consumer goods\ncimplify init my-store --template bakery # default \u2014 food / p\xE2tisserie\n```\n\n## Run\n\n```bash\nbun install\nbun dev\n```\n\nTwo things start in parallel:\n\n- `cimplify-mock` \u2014 the Cimplify mock API on `http://127.0.0.1:8787` (Akua's Bakery seeded by default).\n- `next dev` \u2014 this storefront on `http://localhost:3000`.\n\nOpen the storefront in your browser. Edit `app/page.tsx` to start customising. The SDK ships full pages (`<CataloguePage>`, `<ProductPage>`, `<CartPage>`, `<CheckoutPage>`) and layouts (`<DefaultProductLayout>`, `<FoodProductLayout>`, etc.) that you can swap in.\n\n## Structure\n\n```\napp/\n layout.tsx # root layout, fonts, providers, header/footer/modal\n page.tsx # home\n shop/page.tsx # full catalogue (SDK <CataloguePage/>)\n collections/[slug]/ # collection landing\n categories/[slug]/ # category landing\n cart/page.tsx # SDK <CartPage/>\n checkout/page.tsx # SDK <CheckoutPage/>\n orders/[id]/page.tsx # post-checkout thank-you\n globals.css # Tailwind import + theme tokens\ncomponents/\n providers.tsx # CimplifyProvider client wrapper\n header.tsx, footer.tsx, hero.tsx\n store-product-card.tsx # SDK <ProductCard/> wired to URL-driven modal\n product-modal.tsx # ?product=<slug> deep-linkable modal\n collection-strip.tsx # horizontal product strip\n category-grid.tsx # SDK <CategoryGrid/> with router navigation\nlib/\n cart.ts # useCartCount() for the header pill\n```\n\n## Switch seed\n\n```bash\ncimplify-mock --seed restaurant # Mama's Kitchen\ncimplify-mock --seed retail # Currents Electronics\ncimplify-mock --seed services # Serene Spa\ncimplify-mock --seed grocery # FreshMart\n```\n\n## Go live\n\n```diff\n# .env.local\n- NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev\n+ NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=<your tenant key>\n```\n\nDeploy with `cimplify deploy --prod` after linking the project. See [`cimplify` CLI docs](https://www.cimplify.dev/docs/cli). `next.config.ts` already whitelists the SDK image hosts under `images.remotePatterns`.\n" }, { "path": "bun.lock", "kind": "binary", "content": "ewogICJsb2NrZmlsZVZlcnNpb24iOiAxLAogICJjb25maWdWZXJzaW9uIjogMSwKICAid29ya3NwYWNlcyI6IHsKICAgICIiOiB7CiAgICAgICJuYW1lIjogIl9fU1RPUkVGUk9OVF9OQU1FX18iLAogICAgICAiZGVwZW5kZW5jaWVzIjogewogICAgICAgICJAY2ltcGxpZnkvc2RrIjogIl4wLjQ4LjIiLAogICAgICAgICJuZXh0IjogIl4xNi4yLjQiLAogICAgICAgICJyZWFjdCI6ICJeMTkuMC4wIiwKICAgICAgICAicmVhY3QtZG9tIjogIl4xOS4wLjAiLAogICAgICB9LAogICAgICAiZGV2RGVwZW5kZW5jaWVzIjogewogICAgICAgICJAdGFpbHdpbmRjc3MvcG9zdGNzcyI6ICJeNC4yLjQiLAogICAgICAgICJAdHlwZXMvbm9kZSI6ICJeMjIuMTAuMCIsCiAgICAgICAgIkB0eXBlcy9yZWFjdCI6ICJeMTkuMC4wIiwKICAgICAgICAiQHR5cGVzL3JlYWN0LWRvbSI6ICJeMTkuMC4wIiwKICAgICAgICAiY29uY3VycmVudGx5IjogIl45LjAuMCIsCiAgICAgICAgInRhaWx3aW5kY3NzIjogIl40LjIuNCIsCiAgICAgICAgInR5cGVzY3JpcHQiOiAiXjUuOS4zIiwKICAgICAgICAidml0ZXN0IjogIl40LjEuNSIsCiAgICAgIH0sCiAgICB9LAogIH0sCiAgInBhY2thZ2VzIjogewogICAgIkBhbGxvYy9xdWljay1scnUiOiBbIkBhbGxvYy9xdWljay1scnVANS4yLjAiLCAiIiwge30sICJzaGE1MTItVXJjQUJCKzRiVXJGQUJ3Ymx1VElCRXJYd3Zic1UvVjdUWldmbWJnSmZia3dpQnV6aVM5Z3hkT0RVeXVpZWNmZEdRODVqZ2xNVzZqdVMzK3o1VHNLTHc9PSJdLAoKICAgICJAYmFiZWwvcnVudGltZSI6IFsiQGJhYmVsL3J1bnRpbWVANy4yOS4yIiwgIiIsIHt9LCAic2hhNTEyLUppRFNoSDQ1ektIV3lHZTRaTlZSckNqQno4Tmg5VE1tWkcxa2g0UVRLOGhDQlRXQmk4RGEraTdzMWZKdzcvbFlwTTRjY2VwU05mcXpaL1F2QUJCaTVnPT0iXSwKCiAgICAiQGJhc2UtdWkvcmVhY3QiOiBbIkBiYXNlLXVpL3JlYWN0QDEuNS4wIiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIkBiYWJlbC9ydW50aW1lIjogIl43LjI5LjIiLCAiQGJhc2UtdWkvdXRpbHMiOiAiMC4yLjkiLCAiQGZsb2F0aW5nLXVpL3JlYWN0LWRvbSI6ICJeMi4xLjgiLCAiQGZsb2F0aW5nLXVpL3V0aWxzIjogIl4wLjIuMTEiLCAidXNlLXN5bmMtZXh0ZXJuYWwtc3RvcmUiOiAiXjEuNi4wIiB9LCAicGVlckRlcGVuZGVuY2llcyI6IHsgIkBkYXRlLWZucy90eiI6ICJeMS4yLjAiLCAiQHR5cGVzL3JlYWN0IjogIl4xNyB8fCBeMTggfHwgXjE5IiwgImRhdGUtZm5zIjogIl40LjAuMCIsICJyZWFjdCI6ICJeMTcgfHwgXjE4IHx8IF4xOSIsICJyZWFjdC1kb20iOiAiXjE3IHx8IF4xOCB8fCBeMTkiIH0sICJvcHRpb25hbFBlZXJzIjogWyJAZGF0ZS1mbnMvdHoiLCAiQHR5cGVzL3JlYWN0IiwgImRhdGUtZm5zIl0gfSwgInNoYTUxMi16MWdTQWxjZWQxeVkraU0rbUhERXRJa0Q4VUkzRWJzNTJNdUJQeHZWNmY1aFJ1dGsreHZDSC93dUI3aERxRHpLOUpHNUZvTXo1bmhycXRTczF3anQxQT09Il0sCgogICAgIkBiYXNlLXVpL3V0aWxzIjogWyJAYmFzZS11aS91dGlsc0AwLjIuOSIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAYmFiZWwvcnVudGltZSI6ICJeNy4yOS4yIiwgIkBmbG9hdGluZy11aS91dGlscyI6ICJeMC4yLjExIiwgInJlc2VsZWN0IjogIl41LjEuMSIsICJ1c2Utc3luYy1leHRlcm5hbC1zdG9yZSI6ICJeMS42LjAiIH0sICJwZWVyRGVwZW5kZW5jaWVzIjogeyAiQHR5cGVzL3JlYWN0IjogIl4xNyB8fCBeMTggfHwgXjE5IiwgInJlYWN0IjogIl4xNyB8fCBeMTggfHwgXjE5IiwgInJlYWN0LWRvbSI6ICJeMTcgfHwgXjE4IHx8IF4xOSIgfSwgIm9wdGlvbmFsUGVlcnMiOiBbIkB0eXBlcy9yZWFjdCJdIH0sICJzaGE1MTIteC9QRERDWXpvcVBwanJkeWIzVmN5eWxUSTJJalVYRXRZREdpNWZvaDdLc25tTkpJSWFWd0EyR0xnREgxZHBzMUdnWGlKYkE2MGhNK0F5dVRmUXpJdnc9PSJdLAoKICAgICJAY2ltcGxpZnkvc2RrIjogWyJAY2ltcGxpZnkvc2RrQDAuNDguMiIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAYmFzZS11aS9yZWFjdCI6ICJeMS40LjEiLCAiY2xzeCI6ICJeMi4xLjEiLCAiZGF0ZS1mbnMiOiAiXjQuMS4wIiwgImxpYnBob25lbnVtYmVyLWpzIjogIjEuMTIuNDEiLCAicmVhY3QtZGF5LXBpY2tlciI6ICJeOS4xNC4wIiwgInRhaWx3aW5kLW1lcmdlIjogIl4zLjUuMCIsICJ6b2QiOiAiXjQuNC4zIiB9LCAicGVlckRlcGVuZGVuY2llcyI6IHsgIkBwYXlzdGFjay9pbmxpbmUtanMiOiAiXjIuMjIuOCIsICJtc3ciOiAiPj0yLjAuMCIsICJyZWFjdCI6ICI+PTE3LjAuMCIsICJ2aXRlc3QiOiAiPj0yLjAuMCIgfSwgIm9wdGlvbmFsUGVlcnMiOiBbIkBwYXlzdGFjay9pbmxpbmUtanMiLCAibXN3IiwgInJlYWN0IiwgInZpdGVzdCJdLCAiYmluIjogeyAiY2ltcGxpZnktbW9jayI6ICJkaXN0L21vY2svY2xpLm1qcyIgfSB9LCAic2hhNTEyLTZJaDhNNkxZVkhDdU84c2JnZWc3VmdQTUJIWHZNK2NodFkzLzJWZkdWRWZFeU5UY0d4UzZlcDFOMDdNS21tTG5xQWNMZTZteFQwbVFiL2h0cldQZ1hBPT0iXSwKCiAgICAiQGRhdGUtZm5zL3R6IjogWyJAZGF0ZS1mbnMvdHpAMS40LjEiLCAiIiwge30sICJzaGE1MTItUDVMVU5odGJqNllmSTNpSmp3NUVMOWVVQUc2T2l0RDBXM2ZXUWNwUWpEUmMvUUlzTDB0Uk51TzFQY0R2UGNjV0wxZlNUWFhkRTFkcytsOTVEVi9PRkE9PSJdLAoKICAgICJAZW1uYXBpL2NvcmUiOiBbIkBlbW5hcGkvY29yZUAxLjEwLjAiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiQGVtbmFwaS93YXNpLXRocmVhZHMiOiAiMS4yLjEiLCAidHNsaWIiOiAiXjIuNC4wIiB9IH0sICJzaGE1MTIteXE2T2tKNHA4MkNBZlBsMHU5bVFlYlFIS1BKa1k3V3JJdWsyMDVjVFluWWUrazJaOFlCaDExRnJiUkcvSDZpaGlycWNhY09nbDJCSU84b3lNUUxlWHc9PSJdLAoKICAgICJAZW1uYXBpL3J1bnRpbWUiOiBbIkBlbW5hcGkvcnVudGltZUAxLjEwLjAiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAidHNsaWIiOiAiXjIuNC4wIiB9IH0sICJzaGE1MTItZXd2WWxrODZ4VW9HSTB6UVJOcS9tQysxNlIxUWVEbEtReTIxS2kzb1NZWE5nTGI0NUdWMVA2QTBNKy9zNm55Q3VORHFlNVZwYVk4NEJ6WEd3VmJ3RkE9PSJdLAoKICAgICJAZW1uYXBpL3dhc2ktdGhyZWFkcyI6IFsiQGVtbmFwaS93YXNpLXRocmVhZHNAMS4yLjEiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAidHNsaWIiOiAiXjIuNC4wIiB9IH0sICJzaGE1MTItdVRJSTdPWUYrL01lcy9NcmNJT1lwNXlPdFNNTEJXU0lvTFBwY2d3aXBvaUtibGk2azMyMnRjb0ZzeG9JSXhQRHFXMDFTUUdBZ2tvNEV6WmkyQk52Mnc9PSJdLAoKICAgICJAZmxvYXRpbmctdWkvY29yZSI6IFsiQGZsb2F0aW5nLXVpL2NvcmVAMS43LjUiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiQGZsb2F0aW5nLXVpL3V0aWxzIjogIl4wLjIuMTEiIH0gfSwgInNoYTUxMi0xSWg0V1RXeXcwK2xLeUZNY0JIR2JiNVU1RnR1SEp1dWpveXlyNXpUYVdTNUVZTWVUNkpiMkF1RGVmdHNDc0V1Y2hPK21NMmlqNStxOWNyaHlkekxoUT09Il0sCgogICAgIkBmbG9hdGluZy11aS9kb20iOiBbIkBmbG9hdGluZy11aS9kb21AMS43LjYiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiQGZsb2F0aW5nLXVpL2NvcmUiOiAiXjEuNy41IiwgIkBmbG9hdGluZy11aS91dGlscyI6ICJeMC4yLjExIiB9IH0sICJzaGE1MTItOWdaU0FJNVhNMzY4ODBQUE1tLy85ZGZpRW5nWW9DNkFtMml6RVMxRkY0MDZZRnNqdnlCTW1lSjJnNFNBanUzeFd3dHV5bk5SRkwyczloZ3hwTEk1U1E9PSJdLAoKICAgICJAZmxvYXRpbmctdWkvcmVhY3QtZG9tIjogWyJAZmxvYXRpbmctdWkvcmVhY3QtZG9tQDIuMS44IiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIkBmbG9hdGluZy11aS9kb20iOiAiXjEuNy42IiB9LCAicGVlckRlcGVuZGVuY2llcyI6IHsgInJlYWN0IjogIj49MTYuOC4wIiwgInJlYWN0LWRvbSI6ICI+PTE2LjguMCIgfSB9LCAic2hhNTEyLWNDNTJiSHdNL24vQ3hTODdGSDB5V2RuZ0VacmpkdExXL3FWcnVvNjhxZytwcks3WlE0WUdkdXQyR3lEVnBvR2VBWWUvaDg5OXJWZU9WbTZPaTQwazJBPT0iXSwKCiAgICAiQGZsb2F0aW5nLXVpL3V0aWxzIjogWyJAZmxvYXRpbmctdWkvdXRpbHNAMC4yLjExIiwgIiIsIHt9LCAic2hhNTEyLVJpQi95SWg3OHBjSXhsNmxMTUcwQ2dCWEFaMlkwZVZIcU1QWXVndSs5VTBBZVQ2WUJlaUpwZjdsYmRKTkl1Z0ZQNVNJandOUmdvNERoUjFReGkyNkdnPT0iXSwKCiAgICAiQGltZy9jb2xvdXIiOiBbIkBpbWcvY29sb3VyQDEuMS4wIiwgIiIsIHt9LCAic2hhNTEyLVRkNzZxN2o1N28vdExWZGdTNzQ2Y1lBUmZTeXhrOGlFZlJ4ZXdMOWg0T016WWhiVzRUQWNwcGwwbVQ0ZXlxWGRkaDZML2p3b003NW1vN2l4YS9wQ2VRPT0iXSwKCiAgICAiQGltZy9zaGFycC1kYXJ3aW4tYXJtNjQiOiBbIkBpbWcvc2hhcnAtZGFyd2luLWFybTY0QDAuMzQuNSIsICIiLCB7ICJvcHRpb25hbERlcGVuZGVuY2llcyI6IHsgIkBpbWcvc2hhcnAtbGlidmlwcy1kYXJ3aW4tYXJtNjQiOiAiMS4yLjQiIH0sICJvcyI6ICJkYXJ3aW4iLCAiY3B1IjogImFybTY0IiB9LCAic2hhNTEyLWltdFEzV01KWGJNWTRmeGIvTmRwNkhCVE5WdFdDVUkwV2RvYnloZUdmNSthZDZ4WDhWSURPOHUyeEU0cWMvZnIwOENLRy83ZERzZUZ0bjZNNmcvcjN3PT0iXSwKCiAgICAiQGltZy9zaGFycC1kYXJ3aW4teDY0IjogWyJAaW1nL3NoYXJwLWRhcndpbi14NjRAMC4zNC41IiwgIiIsIHsgIm9wdGlvbmFsRGVwZW5kZW5jaWVzIjogeyAiQGltZy9zaGFycC1saWJ2aXBzLWRhcndpbi14NjQiOiAiMS4yLjQiIH0sICJvcyI6ICJkYXJ3aW4iLCAiY3B1IjogIng2NCIgfSwgInNoYTUxMi1ZTkVGQUYvNEtRL1BlVzBOK3IrYVZWc29JWTAvcXh4aWtGMlNXZHArTlJrbU1CN3k5TEJaQVZxUTR5aEdDbS9IM0gyNzBPU3lrcW1RTUtMQmhCSkRFdz09Il0sCgogICAgIkBpbWcvc2hhcnAtbGlidmlwcy1kYXJ3aW4tYXJtNjQiOiBbIkBpbWcvc2hhcnAtbGlidmlwcy1kYXJ3aW4tYXJtNjRAMS4yLjQiLCAiIiwgeyAib3MiOiAiZGFyd2luIiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi16cWpqbzdSYXRGZkZvUDBNa1E1MWpmdUZaQm5WRTJwUmlheWRLSjFHL3JIWnZuc3JIQU9jUUFMSWk5c0E1Y281eGVuUWRUdWdDdnRiMWN1Zjc4VmY0Zz09Il0sCgogICAgIkBpbWcvc2hhcnAtbGlidmlwcy1kYXJ3aW4teDY0IjogWyJAaW1nL3NoYXJwLWxpYnZpcHMtZGFyd2luLXg2NEAxLjIuNCIsICIiLCB7ICJvcyI6ICJkYXJ3aW4iLCAiY3B1IjogIng2NCIgfSwgInNoYTUxMi0xSU9kNXhmVmhsR3dYK3pYdjJOOTNrMHlNT052VWxBTnlsYkp3MWVUYWg4Sy9KdHBpMTVLQytXU2lhWC9uQm1ibTJIeFJNMWdaMG5TZGpTc3JaYkdLZz09Il0sCgogICAgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC1hcm0iOiBbIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC1hcm1AMS4yLjQiLCAiIiwgeyAib3MiOiAibGludXgiLCAiY3B1IjogImFybSIgfSwgInNoYTUxMi1iRkk3eGNLRkVMZGlOQ1ZvdjhlNDRJYTR1MmJ5QStsM1h0c0FqK1E4dGZDd082QlE4aURvallkdm9QTXFzS0RrdW9PbytYNkhaQTBzMHExMUFOTVE4QT09Il0sCgogICAgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC1hcm02NCI6IFsiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4LWFybTY0QDEuMi40IiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi1leGNqWDhEZnNJY0oxMHgxS3pyNFJjV2UxZWRDOVBxdURSUlB4M1lWQ3ZRditVNXA3WWluMnMzMmZ0emlrWG9qYjFQSUZjLzlNdDI4L3kraVJrbGtydz09Il0sCgogICAgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC1wcGM2NCI6IFsiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4LXBwYzY0QDEuMi40IiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJwcGM2NCIgfSwgInNoYTUxMi1GTXV2R2lqTERZRzZsVytiL1V2eWlsVVd1NUF5dSszcjJkMVM4bm90aUdDSXlZVS83NmVpZzFVZk1ta1o3dndnT3J6S3psUWJGU3VRZmdtN0dZVVBwQT09Il0sCgogICAgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC1yaXNjdjY0IjogWyJAaW1nL3NoYXJwLWxpYnZpcHMtbGludXgtcmlzY3Y2NEAxLjIuNCIsICIiLCB7ICJvcyI6ICJsaW51eCIsICJjcHUiOiAibm9uZSIgfSwgInNoYTUxMi1vVkRiY1I0elVDMGNlODJ0ZXViU20reDZFVGl4dEtaQmgvcWJSRUlPY0kzY1VMekR5YjE4U3IvV2N5eDdOUlFlUXpPaUhUTmJaRkYxVXdQUzJzY3lHQT09Il0sCgogICAgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC1zMzkweCI6IFsiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4LXMzOTB4QDEuMi40IiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJzMzkweCIgfSwgInNoYTUxMi1xbXA5VnJ6Z1BnTW9HWnlQdnJRSHFrMDJ1eWpBMC9RclRPMjZUcWs2bDRaVjBNUFdJVzZMVGtxT0lvditKMXlFdTdNYkZRYURwd2R3SktoYkp2dVJ4UT09Il0sCgogICAgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC14NjQiOiBbIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC14NjRAMS4yLjQiLCAiIiwgeyAib3MiOiAibGludXgiLCAiY3B1IjogIng2NCIgfSwgInNoYTUxMi10SnhpaUxzbUhjOUF4MWJ6M29hT1lCVVJUWEdJUkRPREJxaHZlVkhvbnJISjkvK2s4OXFiTGwwYmNKbnMrZTR0NHJ2YU5CeGFFWnNGdFNmQWRxdVBydz09Il0sCgogICAgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eG11c2wtYXJtNjQiOiBbIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eG11c2wtYXJtNjRAMS4yLjQiLCAiIiwgeyAib3MiOiAibGludXgiLCAiY3B1IjogImFybTY0IiB9LCAic2hhNTEyLUZWUUh1d3gxSUl1Tm93OVFBYllVekorRW44S2NWbTlMazUrdUdVUUpIYVptTUVDWm1PbGl4OUhuSDduMVRSa1hNUzBwR3hJSm9rSVZCOVN1cVpHR1h3PT0iXSwKCiAgICAiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4bXVzbC14NjQiOiBbIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eG11c2wteDY0QDEuMi40IiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJ4NjQiIH0sICJzaGE1MTItK0xweUJrN0w0NFpJWHd6L1ZZZmdsYVgvb2t4ZXpFU2M2VXhEU295bzJLczZKeGM0WTdzR2pwZ1U5czRQTWdxZ2pqMWdaQ3lsVGllTmFtcUExTUY3RGc9PSJdLAoKICAgICJAaW1nL3NoYXJwLWxpbnV4LWFybSI6IFsiQGltZy9zaGFycC1saW51eC1hcm1AMC4zNC41IiwgIiIsIHsgIm9wdGlvbmFsRGVwZW5kZW5jaWVzIjogeyAiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4LWFybSI6ICIxLjIuNCIgfSwgIm9zIjogImxpbnV4IiwgImNwdSI6ICJhcm0iIH0sICJzaGE1MTItOWRMcXN2d3RnMXV1WEJHWktzeGVtOTU5NSt1anYwc0o2Vmk4d2NUQU5TRnB3Vi9HT05hdDVlQ2t6UW8vMU82elJJa2gwbS84KzVCanJScjdqRFVTWnc9PSJdLAoKICAgICJAaW1nL3NoYXJwLWxpbnV4LWFybTY0IjogWyJAaW1nL3NoYXJwLWxpbnV4LWFybTY0QDAuMzQuNSIsICIiLCB7ICJvcHRpb25hbERlcGVuZGVuY2llcyI6IHsgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC1hcm02NCI6ICIxLjIuNCIgfSwgIm9zIjogImxpbnV4IiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi1iS1F6YUpSWS9ia1BPWHlLeDVFVnVwN3FrYW9qRUNHNk5MWXN3Z2t0T1pqYVhlY1NBZUNXaVp3d2lGZjMvWStPMUhyYXVpRTNGVnNHeEZnOGMyNHJaZz09Il0sCgogICAgIkBpbWcvc2hhcnAtbGludXgtcHBjNjQiOiBbIkBpbWcvc2hhcnAtbGludXgtcHBjNjRAMC4zNC41IiwgIiIsIHsgIm9wdGlvbmFsRGVwZW5kZW5jaWVzIjogeyAiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4LXBwYzY0IjogIjEuMi40IiB9LCAib3MiOiAibGludXgiLCAiY3B1IjogInBwYzY0IiB9LCAic2hhNTEyLTd6em53TmFxVzZZdHNmckdHREE2QlJrSVNLQUFFMUpvMFFkcE5ZWE5NSHUyKzBkVHJQZmxUTE5rcGM4bDdNVVA1TTE2WkpjVXZ5c1ZXV3JNZWZacXVBPT0iXSwKCiAgICAiQGltZy9zaGFycC1saW51eC1yaXNjdjY0IjogWyJAaW1nL3NoYXJwLWxpbnV4LXJpc2N2NjRAMC4zNC41IiwgIiIsIHsgIm9wdGlvbmFsRGVwZW5kZW5jaWVzIjogeyAiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4LXJpc2N2NjQiOiAiMS4yLjQiIH0sICJvcyI6ICJsaW51eCIsICJjcHUiOiAibm9uZSIgfSwgInNoYTUxMi01MWdKdUxQVEthN3BpWVBhVnM4R21CeW83L1U3LzdUWk9xK2NuWEpJSFpLYXZJUkhBUDc3ZTNOMkhFbDNkZ2lxZEQvdzB5VWZpSm5JSTc3UHVEREZkdz09Il0sCgogICAgIkBpbWcvc2hhcnAtbGludXgtczM5MHgiOiBbIkBpbWcvc2hhcnAtbGludXgtczM5MHhAMC4zNC41IiwgIiIsIHsgIm9wdGlvbmFsRGVwZW5kZW5jaWVzIjogeyAiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4LXMzOTB4IjogIjEuMi40IiB9LCAib3MiOiAibGludXgiLCAiY3B1IjogInMzOTB4IiB9LCAic2hhNTEyLW5RdENrMFBkS2ZobzNlQzVNcmJRb2lnSjJnZDFDZ2RkVU1rYWJVaityQmV2czh0WjJjVUxPeDQ2RTdveVgrMDRXR2ZBQmdJd21NQzBWcWllVGlSNGpnPT0iXSwKCiAgICAiQGltZy9zaGFycC1saW51eC14NjQiOiBbIkBpbWcvc2hhcnAtbGludXgteDY0QDAuMzQuNSIsICIiLCB7ICJvcHRpb25hbERlcGVuZGVuY2llcyI6IHsgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC14NjQiOiAiMS4yLjQiIH0sICJvcyI6ICJsaW51eCIsICJjcHUiOiAieDY0IiB9LCAic2hhNTEyLU1FemQ4SFBLeFZ4VmVud0FhK0pSUHdFQzdRRmpvUFd1UzVOWm5CdDZCM3B1N0VHMkdlMGlkMW9MSFpwUEpkbjNPUUsrQlFEaXc5elN0aUhCVEpRUVFRPT0iXSwKCiAgICAiQGltZy9zaGFycC1saW51eG11c2wtYXJtNjQiOiBbIkBpbWcvc2hhcnAtbGludXhtdXNsLWFybTY0QDAuMzQuNSIsICIiLCB7ICJvcHRpb25hbERlcGVuZGVuY2llcyI6IHsgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eG11c2wtYXJtNjQiOiAiMS4yLjQiIH0sICJvcyI6ICJsaW51eCIsICJjcHUiOiAiYXJtNjQiIH0sICJzaGE1MTItZnBySlI2R3RSc010Nkt5ZnE0NElzQ2hWWmVHTjk3Z1REMzMxd2VSMWV4MWMxcnlwREVBQk42VG0yeGExd0U2bFliNURkRW5rMDNOWlBxQTdJZDIxeWc9PSJdLAoKICAgICJAaW1nL3NoYXJwLWxpbnV4bXVzbC14NjQiOiBbIkBpbWcvc2hhcnAtbGludXhtdXNsLXg2NEAwLjM0LjUiLCAiIiwgeyAib3B0aW9uYWxEZXBlbmRlbmNpZXMiOiB7ICJAaW1nL3NoYXJwLWxpYnZpcHMtbGludXhtdXNsLXg2NCI6ICIxLjIuNCIgfSwgIm9zIjogImxpbnV4IiwgImNwdSI6ICJ4NjQiIH0sICJzaGE1MTItSmc4d05UMU1Vekl2aEJGeFZpcXJFaFdER3pxeW1vM3NWN3o3WnNhV2JaTkRMWFJKWm9SR3JqdWxwNjBZWXRWNHdmWThWSUtjV2lkam9qbExjV3JkOFE9PSJdLAoKICAgICJAaW1nL3NoYXJwLXdhc20zMiI6IFsiQGltZy9zaGFycC13YXNtMzJAMC4zNC41IiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIkBlbW5hcGkvcnVudGltZSI6ICJeMS43LjAiIH0sICJjcHUiOiAibm9uZSIgfSwgInNoYTUxMi1PZFdURWlWa1kyUEh3cWtiQkk4ZnJGeFFRRmVrSGFTU2tVSUprd3pjbFdaZTY0TzFYNFVsVWpxcXFMYVBiVXBNT1FrNkZCdS9IdGxHWE5ibElzMGh1dz09Il0sCgogICAgIkBpbWcvc2hhcnAtd2luMzItYXJtNjQiOiBbIkBpbWcvc2hhcnAtd2luMzItYXJtNjRAMC4zNC41IiwgIiIsIHsgIm9zIjogIndpbjMyIiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi1XUTNBZ1dDV1lTYjJ5dCtJRzhtbkM2SmRrOVdoczdPMGd4cGhibHNMdmRoU3BTVHRtdTY5WkcxR2tiNk51dnhzTkFDd2lQVjZjTlNaTnp0MEtQc3c3Zz09Il0sCgogICAgIkBpbWcvc2hhcnAtd2luMzItaWEzMiI6IFsiQGltZy9zaGFycC13aW4zMi1pYTMyQDAuMzQuNSIsICIiLCB7ICJvcyI6ICJ3aW4zMiIsICJjcHUiOiAiaWEzMiIgfSwgInNoYTUxMi1GVjltLzdObWVDbVNIREQ1ajQrNHBOSThDcDNhVytKdkxvWGNUVW8wSXF5alNmQVpKOGRJVW1pangxcWFKc0lpVStIb3N3NnhNNUtpakFXUkpDU2dOZz09Il0sCgogICAgIkBpbWcvc2hhcnAtd2luMzIteDY0IjogWyJAaW1nL3NoYXJwLXdpbjMyLXg2NEAwLjM0LjUiLCAiIiwgeyAib3MiOiAid2luMzIiLCAiY3B1IjogIng2NCIgfSwgInNoYTUxMi0rMjlZTXNxWTIvOWVGRWlXOTNlcVdudUxjV2N1Zm93WGV3d1NOSVQ2VXdaZFVVQ3JNM29Gak1XSC9aNi9UTW1iNGhsRmVubWZBVmJwV2V1cDJqcnlDdz09Il0sCgogICAgIkBqcmlkZ2V3ZWxsL2dlbi1tYXBwaW5nIjogWyJAanJpZGdld2VsbC9nZW4tbWFwcGluZ0AwLjMuMTMiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiQGpyaWRnZXdlbGwvc291cmNlbWFwLWNvZGVjIjogIl4xLjUuMCIsICJAanJpZGdld2VsbC90cmFjZS1tYXBwaW5nIjogIl4wLjMuMjQiIH0gfSwgInNoYTUxMi0ya2t0LzduaUo2TWdFUHhGMGJZZFE2ZXRaYUErZlF2RGNMS2NraHkxeUlRT3phb0tqQkJqU2o2My9hTFZqWUUzcWhSdDVkdk0rdVV5ZkNnNlVLQ0JiQT09Il0sCgogICAgIkBqcmlkZ2V3ZWxsL3JlbWFwcGluZyI6IFsiQGpyaWRnZXdlbGwvcmVtYXBwaW5nQDIuMy41IiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIkBqcmlkZ2V3ZWxsL2dlbi1tYXBwaW5nIjogIl4wLjMuNSIsICJAanJpZGdld2VsbC90cmFjZS1tYXBwaW5nIjogIl4wLjMuMjQiIH0gfSwgInNoYTUxMi1MSTl1LytsYVlHNERzMVRES1NKVzJZUHJJbGNWWU93aTJmVUM2eEI0M2x1ZUNqZ3hWNGxmZk9DWkN0WUZpSDZUTk9YK3RRS1h4OTdUNElLSGJoeUhFUT09Il0sCgogICAgIkBqcmlkZ2V3ZWxsL3Jlc29sdmUtdXJpIjogWyJAanJpZGdld2VsbC9yZXNvbHZlLXVyaUAzLjEuMiIsICIiLCB7fSwgInNoYTUxMi1iUklTZ0NJalAyMC90YldTUFdNRWk1NFFWUFJaRXhrdUQ5bEpMK1VJeFVLdHdWSkE4d1cxVHJiMWpNczFSRlhvMUNCVE5aLzVocEM5UXZtS1dkb3BLdz09Il0sCgogICAgIkBqcmlkZ2V3ZWxsL3NvdXJjZW1hcC1jb2RlYyI6IFsiQGpyaWRnZXdlbGwvc291cmNlbWFwLWNvZGVjQDEuNS41IiwgIiIsIHt9LCAic2hhNTEyLWNZUTkzMTBncnF4dWVXYmwrV3VJVUlhaVVhRGNqN1dPcTVmVmhFbGpOVmdSZk9VaFk5ZnkyelR2Zm9xV3NuZWJoOFNsNzBWU2NGYklDdkpuTEtCME9nPT0iXSwKCiAgICAiQGpyaWRnZXdlbGwvdHJhY2UtbWFwcGluZyI6IFsiQGpyaWRnZXdlbGwvdHJhY2UtbWFwcGluZ0AwLjMuMzEiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiQGpyaWRnZXdlbGwvcmVzb2x2ZS11cmkiOiAiXjMuMS4wIiwgIkBqcmlkZ2V3ZWxsL3NvdXJjZW1hcC1jb2RlYyI6ICJeMS40LjE0IiB9IH0sICJzaGE1MTItenpOUitTZFFTREp6Yzhqb2FlUDhRUW9DUXI4TnVZeDJkSUl5dGwxUWVCRVpISjl1VzZoZWJzcllnYno4aEp3VVFhbzNUV0NNdG1mVjhOdTF0d09MQXc9PSJdLAoKICAgICJAbmFwaS1ycy93YXNtLXJ1bnRpbWUiOiBbIkBuYXBpLXJzL3dhc20tcnVudGltZUAxLjEuNCIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAdHlieXMvd2FzbS11dGlsIjogIl4wLjEwLjEiIH0sICJwZWVyRGVwZW5kZW5jaWVzIjogeyAiQGVtbmFwaS9jb3JlIjogIl4xLjcuMSIsICJAZW1uYXBpL3J1bnRpbWUiOiAiXjEuNy4xIiB9IH0sICJzaGE1MTItM05RTk5nQTFZU2xKYi9rTUgxaWxkQVNQOUhXNy83a1luUkkyc3pXSmFvZmFTMWhXbWJHSTRIK2QzKzIyYUd6WFhOOUlKK24rR2lGVmNHaXBKUDE4b3c9PSJdLAoKICAgICJAbmV4dC9lbnYiOiBbIkBuZXh0L2VudkAxNi4yLjYiLCAiIiwge30sICJzaGE1MTItZ2Q4SG9ITjR1Zmo3M1dtUjNKbVZvbHJwSlI0N0lMSzZMb3VQNXhFbFBnbGFWeGlyNmUxYTdWenZUdkRXa09vUFhUOXJra1R6eUN4QnU0eWVaZlp3Y3c9PSJdLAoKICAgICJAbmV4dC9zd2MtZGFyd2luLWFybTY0IjogWyJAbmV4dC9zd2MtZGFyd2luLWFybTY0QDE2LjIuNiIsICIiLCB7ICJvcyI6ICJkYXJ3aW4iLCAiY3B1IjogImFybTY0IiB9LCAic2hhNTEyLVpKR2trY05mWWdyck1rcU9kWjd6b0xhMVRPeTBxcGNNZmsvejRNaC9GS1V6NDBnVk8rSE5RV3FtTHhmNjdaNVdCNjREUnAwZGhFYnlIZmVsKzZzSlVnPT0iXSwKCiAgICAiQG5leHQvc3djLWRhcndpbi14NjQiOiBbIkBuZXh0L3N3Yy1kYXJ3aW4teDY0QDE2LjIuNiIsICIiLCB7ICJvcyI6ICJkYXJ3aW4iLCAiY3B1IjogIng2NCIgfSwgInNoYTUxMi12L1lMQkhJWTEzMkNlZDNwdUJKN1lKS3cxbHFzQ3JnY05vMmFSSmxDRXlRcnJDZVJKbHZHbG5teGhQeE5RSTNLRTNOMURONXI5VFBOUHZrYTNucTVSUT09Il0sCgogICAgIkBuZXh0L3N3Yy1saW51eC1hcm02NC1nbnUiOiBbIkBuZXh0L3N3Yy1saW51eC1hcm02NC1nbnVAMTYuMi42IiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi1SUE92cWxZQmJjUWprejlWUVFEWjJUMmJBUklqWFpWMUtGbHQrVjJNcjZTVy9lNEk5ZmNLc2FBMGhkeWYyRkhvVGxzVjJ4bkJkNVk5MTJyUC8xQ2U2dz09Il0sCgogICAgIkBuZXh0L3N3Yy1saW51eC1hcm02NC1tdXNsIjogWyJAbmV4dC9zd2MtbGludXgtYXJtNjQtbXVzbEAxNi4yLjYiLCAiIiwgeyAib3MiOiAibGludXgiLCAiY3B1IjogImFybTY0IiB9LCAic2hhNTEyLVVSVVR1MStkTWt4SnNQRmdtK09lRXZxOXdmNXN1ancwRXZnWXk4MFRER0hUU0xUbklIZXFiMEV1OEEzc0M5NUlSZ2plalFMK2tDNG13KzR5UHhpQVhBPT0iXSwKCiAgICAiQG5leHQvc3djLWxpbnV4LXg2NC1nbnUiOiBbIkBuZXh0L3N3Yy1saW51eC14NjQtZ251QDE2LjIuNiIsICIiLCB7ICJvcyI6ICJsaW51eCIsICJjcHUiOiAieDY0IiB9LCAic2hhNTEyLURPajE4Mm1QVjhHM1VrcmF5TG9SRU01WUVZSStEazV3djdPeDl4bDFmRmliQUVMRXNGRDBsRFBmSEllSUxsdXRNTWZkeWhsellQRUxHM3BldUthdXJ3PT0iXSwKCiAgICAiQG5leHQvc3djLWxpbnV4LXg2NC1tdXNsIjogWyJAbmV4dC9zd2MtbGludXgteDY0LW11c2xAMTYuMi42IiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJ4NjQiIH0sICJzaGE1MTItSEtRNVNQL1YvdWI3M1V2RjduL3plSmx4azJrTG10TDdXenJnNFdmbWtqbU5vczVvbkoydEt1N3laT1BkTDE4QTZTdmZuM21heDI5eW0rcnk3TmtLNGc9PSJdLAoKICAgICJAbmV4dC9zd2Mtd2luMzItYXJtNjQtbXN2YyI6IFsiQG5leHQvc3djLXdpbjMyLWFybTY0LW1zdmNAMTYuMi42IiwgIiIsIHsgIm9zIjogIndpbjMyIiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi1MWlhwVGxQeVM1djdIaFNtbnZzTEdQM2lJWWdZT0JuYzhyOEFybFQ1NXNHSFY4OWJSMkhsRGRCaldRK1BZNlNKTW1rOFR1VkdGdXhhbG5QM2svMER3Zz09Il0sCgogICAgIkBuZXh0L3N3Yy13aW4zMi14NjQtbXN2YyI6IFsiQG5leHQvc3djLXdpbjMyLXg2NC1tc3ZjQDE2LjIuNiIsICIiLCB7ICJvcyI6ICJ3aW4zMiIsICJjcHUiOiAieDY0IiB9LCAic2hhNTEyLUYwKzRpMGg5SjZDNGVFM0VBUFdzb0NrN1VXL2Riek9qeXp4WTBxbkRVT1lGdTZGRm1kWjZsOTcvWGRWMy9OejNWWXlPN1VXanlFSlVYa0dxY29YZk1BPT0iXSwKCiAgICAiQG94Yy1wcm9qZWN0L3R5cGVzIjogWyJAb3hjLXByb2plY3QvdHlwZXNAMC4xMzAuMCIsICIiLCB7fSwgInNoYTUxMi1pYkQydXN4OUpSdTdmNXB1MnRNS01JNGNwQTROZ1hKUW9ZUlA0cFE3UHhtbjFsNmsvNTNxV3RRV1pheWhZeTNYNFFaa3Q5ME90K21KRWFlWG91aW82UT09Il0sCgogICAgIkByb2xsZG93bi9iaW5kaW5nLWFuZHJvaWQtYXJtNjQiOiBbIkByb2xsZG93bi9iaW5kaW5nLWFuZHJvaWQtYXJtNjRAMS4wLjEiLCAiIiwgeyAib3MiOiAiYW5kcm9pZCIsICJjcHUiOiAiYXJtNjQiIH0sICJzaGE1MTItZkpJM0kwcjNDM09qL3pkQkNwYUNtQlJaWWYwN3hwYXE0eUNmRERvU0ZtK2JlV056YklsMjZwdVc4UnJhVWR1Z29Kdy85NXplck5PbjZqYXNBaHpTbWc9PSJdLAoKICAgICJAcm9sbGRvd24vYmluZGluZy1kYXJ3aW4tYXJtNjQiOiBbIkByb2xsZG93bi9iaW5kaW5nLWRhcndpbi1hcm02NEAxLjAuMSIsICIiLCB7ICJvcyI6ICJkYXJ3aW4iLCAiY3B1IjogImFybTY0IiB9LCAic2hhNTEyLWNLbkFoV0VzVjdUUGNBLzVFQXRlRHA2S2NKWkJRMkcrQnFFN3pheU1NaTdrTXZ3UnNidjdXVDlhT25uMFdObDRTS0VJZjQzdmpTMzFpVVB1ODBuelhnPT0iXSwKCiAgICAiQHJvbGxkb3duL2JpbmRpbmctZGFyd2luLXg2NCI6IFsiQHJvbGxkb3duL2JpbmRpbmctZGFyd2luLXg2NEAxLjAuMSIsICIiLCB7ICJvcyI6ICJkYXJ3aW4iLCAiY3B1IjogIng2NCIgfSwgInNoYTUxMi1ZS3JWd1FqSVJCUG8rNUcvdTAzd0dqYmR5NHE3cHl6Q2U5M0RLOVZKN3prVm1lZzhMSjdHYmdzaUhXZFI0eFNvZTRDQVhSRDdCY2pnYnRyNjRia1hOZz09Il0sCgogICAgIkByb2xsZG93bi9iaW5kaW5nLWZyZWVic2QteDY0IjogWyJAcm9sbGRvd24vYmluZGluZy1mcmVlYnNkLXg2NEAxLjAuMSIsICIiLCB7ICJvcyI6ICJmcmVlYnNkIiwgImNwdSI6ICJ4NjQiIH0sICJzaGE1MTItei9vQnNSRW80NlNzRnFCd1l0RmUwa3BKZUJpakFUNDhPL1dYTEk0c3VpQ0xCa3IwM1JUdFRKTUN6U2REZDJ6bmxoOFZKaXpMMDlYVmtRZ2s4SVpvbnc9PSJdLAoKICAgICJAcm9sbGRvd24vYmluZGluZy1saW51eC1hcm0tZ251ZWFiaWhmIjogWyJAcm9sbGRvd24vYmluZGluZy1saW51eC1hcm0tZ251ZWFiaWhmQDEuMC4xIiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJhcm0iIH0sICJzaGE1MTItaWs4cTdHTTExenh2WXhGYzJQZURjVDZUQnZoQ1FNYVV4ZnBoL001bDlzS3VUcy9TamczTCtCeXcwRjd3MFpWTEJabXgzMFArZ0cwRUN6ek4rTUZjbVE9PSJdLAoKICAgICJAcm9sbGRvd24vYmluZGluZy1saW51eC1hcm02NC1nbnUiOiBbIkByb2xsZG93bi9iaW5kaW5nLWxpbnV4LWFybTY0LWdudUAxLjAuMSIsICIiLCB7ICJvcyI6ICJsaW51eCIsICJjcHUiOiAiYXJtNjQiIH0sICJzaGE1MTItUW9TeDJFa3lycmRaNmtjeUU4c3RxWjYydDBZcmE4RnM1aWE5bE94SnJoNlRNUUpLN2dRS21zY2RUSGY3cE9YS1JFS3JWd090SmNRRzNxVlNmYzg2NkE9PSJdLAoKICAgICJAcm9sbGRvd24vYmluZGluZy1saW51eC1hcm02NC1tdXNsIjogWyJAcm9sbGRvd24vYmluZGluZy1saW51eC1hcm02NC1tdXNsQDEuMC4xIiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi11d053RnB3S2VOaVphd2ZBV0JnZzBWSXp0UFRWM2loaGgxdlYzMzRoOWl2bk5Mb3J4blFNVTZGejh3RzFaYjRRaDlMQzEvTWtjeVQzWWxEWEczUnNnZz09Il0sCgogICAgIkByb2xsZG93bi9iaW5kaW5nLWxpbnV4LXBwYzY0LWdudSI6IFsiQHJvbGxkb3duL2JpbmRpbmctbGludXgtcHBjNjQtZ251QDEuMC4xIiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJwcGM2NCIgfSwgInNoYTUxMi16WTFidWw3T1dyN0RGQmlKKyt3b2ZYdm5yOEI0NWNlM1FzUVVoS3JJaFhzeWdBaDdiVGt3eWVNMWJpMWEyZzVDL3lDL044VFp5R0RFb01mbS9sOW1wZz09Il0sCgogICAgIkByb2xsZG93bi9iaW5kaW5nLWxpbnV4LXMzOTB4LWdudSI6IFsiQHJvbGxkb3duL2JpbmRpbmctbGludXgtczM5MHgtZ251QDEuMC4xIiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJzMzkweCIgfSwgInNoYTUxMi0wZnJsc1QvZjRGdDZJN1NNRVNUS25GM2Nac2RpY1FuMWRDTWtGL2pUOXdETEUrZ0dvaVFmdjFubVQ5ZStzN3MvZmVrdnZ5NnRaTTJqSHZJMnRrYkpEUT09Il0sCgogICAgIkByb2xsZG93bi9iaW5kaW5nLWxpbnV4LXg2NC1nbnUiOiBbIkByb2xsZG93bi9iaW5kaW5nLWxpbnV4LXg2NC1nbnVAMS4wLjEiLCAiIiwgeyAib3MiOiAibGludXgiLCAiY3B1IjogIng2NCIgfSwgInNoYTUxMi1YQUJWbUdwOVRnMFdzcFRWdndkdVRjNGZwcXk2Sm5BVXJTUWU2T3V5cUQvMDNuSTdyME85T1dVa01Jd0ZyaktBSXFvbHZxb0E0WnJKcHBnd0UwR3htdz09Il0sCgogICAgIkByb2xsZG93bi9iaW5kaW5nLWxpbnV4LXg2NC1tdXNsIjogWyJAcm9sbGRvd24vYmluZGluZy1saW51eC14NjQtbXVzbEAxLjAuMSIsICIiLCB7ICJvcyI6ICJsaW51eCIsICJjcHUiOiAieDY0IiB9LCAic2hhNTEyLWJWNGZ6c3d1elZjS0Q5MG8vVk02UXFLeG54bERxMGcyQklTRExOVm14cm5ocHYxRERieVBoQ0lqWWZ2ellMVitNdmtLS25RdDJRNkFPODZTRUJVTFVRPT0iXSwKCiAgICAiQHJvbGxkb3duL2JpbmRpbmctb3Blbmhhcm1vbnktYXJtNjQiOiBbIkByb2xsZG93bi9iaW5kaW5nLW9wZW5oYXJtb255LWFybTY0QDEuMC4xIiwgIiIsIHsgIm9zIjogIm5vbmUiLCAiY3B1IjogImFybTY0IiB9LCAic2hhNTEyLS9NaDBaaHEzT1A3ZlZzMGtjUUhaUDZsWkV0aE1HVGFTZjhVQlFZU0ZFWkRXR1hYbEVDK25KNkVxZW5hSzJ0NExCWE1lM0ErSy9HMkJWWFhkdE9yNFBRPT0iXSwKCiAgICAiQHJvbGxkb3duL2JpbmRpbmctd2FzbTMyLXdhc2kiOiBbIkByb2xsZG93bi9iaW5kaW5nLXdhc20zMi13YXNpQDEuMC4xIiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIkBlbW5hcGkvY29yZSI6ICIxLjEwLjAiLCAiQGVtbmFwaS9ydW50aW1lIjogIjEuMTAuMCIsICJAbmFwaS1ycy93YXNtLXJ1bnRpbWUiOiAiXjEuMS40IiB9LCAiY3B1IjogIm5vbmUiIH0sICJzaGE1MTItKzF4YzlYNDVsOHVmc0JBbTZHanZ4MnFEUklZOWxUVnQwY2dXTmNKKzFnZGhYdmtieGVQQTYweVJUd1NUdVhMMDlDTWh5Sm1qcFY3RTNOb3l4YnFGUVE9PSJdLAoKICAgICJAcm9sbGRvd24vYmluZGluZy13aW4zMi1hcm02NC1tc3ZjIjogWyJAcm9sbGRvd24vYmluZGluZy13aW4zMi1hcm02NC1tc3ZjQDEuMC4xIiwgIiIsIHsgIm9zIjogIndpbjMyIiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi0xRCtVcVpkZm51UitKeTFHZ01Kd2k4NWJENDBIMjF1Tm1PUFJXUWh3NG9SU3VvbFovQjVyaXhaNDVESzJLWE9UQ3ZtVkNlY2F1V2dFaGJ3OGJJN3RPdz09Il0sCgogICAgIkByb2xsZG93bi9iaW5kaW5nLXdpbjMyLXg2NC1tc3ZjIjogWyJAcm9sbGRvd24vYmluZGluZy13aW4zMi14NjQtbXN2Y0AxLjAuMSIsICIiLCB7ICJvcyI6ICJ3aW4zMiIsICJjcHUiOiAieDY0IiB9LCAic2hhNTEyLUlOQXljYVd1aGxPSzN3azRtUkhHc2Rnd1lXbWQ5Y0NoZFBkRTlid1dteTZybjlWcVZOWU5GR2hPZFhyb2ZYVXh3SEluY1NpUE5iOHRObThrbkRWSWVRPT0iXSwKCiAgICAiQHJvbGxkb3duL3BsdWdpbnV0aWxzIjogWyJAcm9sbGRvd24vcGx1Z2ludXRpbHNAMS4wLjEiLCAiIiwge30sICJzaGE1MTItMmo5Ykd0NUpoOGhqK3ZQdGd6UHRsNzJqMHlSeEhBeXVtb282VE5mQWpzTEIwNFV0cFN2UGJQY0RjQk14ejduKzlDWUIwYzFHeFFGeFlSZzJqaW1xR3c9PSJdLAoKICAgICJAc3RhbmRhcmQtc2NoZW1hL3NwZWMiOiBbIkBzdGFuZGFyZC1zY2hlbWEvc3BlY0AxLjEuMCIsICIiLCB7fSwgInNoYTUxMi1sMmFGeTVqQUxobmlHNUhncXJENmpYTGkvclVXckt2cU4vcUp4NnlvSnNnS2hibFZkK2lxcVU0UkNYYXZtL2pQaXR5RG81VEN2S01ucGpLbk9yaXkwdz09Il0sCgogICAgIkBzd2MvaGVscGVycyI6IFsiQHN3Yy9oZWxwZXJzQDAuNS4xNSIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJ0c2xpYiI6ICJeMi44LjAiIH0gfSwgInNoYTUxMi1KUTVUdU1pNDVPd2k0L0JJTUFKQm9TUW9PSnUxMm9Pay9nQURxbGNVTDlKRWRIQjh2eWpVU3N4cWVOWG5tWEhqWUtNaTJXY1l0ZXpHRUVocVVJL0UyZz09Il0sCgogICAgIkB0YWJieV9haS9oaWpyaS1jb252ZXJ0ZXIiOiBbIkB0YWJieV9haS9oaWpyaS1jb252ZXJ0ZXJAMS4wLjUiLCAiIiwge30sICJzaGE1MTItcjViQ2xLcmNJdXNEb28wNDlkU0w4Q2F3bkhSNm1SZER3aGxRdUlnWlJOdHk2OHEweDhrM0xmMUJ0UEFNeFJmL0dnbkhCbklPNHVqZDMrR1FkTFd6eFE9PSJdLAoKICAgICJAdGFpbHdpbmRjc3Mvbm9kZSI6IFsiQHRhaWx3aW5kY3NzL25vZGVANC4zLjAiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiQGpyaWRnZXdlbGwvcmVtYXBwaW5nIjogIl4yLjMuNSIsICJlbmhhbmNlZC1yZXNvbHZlIjogIl41LjIxLjAiLCAiaml0aSI6ICJeMi42LjEiLCAibGlnaHRuaW5nY3NzIjogIjEuMzIuMCIsICJtYWdpYy1zdHJpbmciOiAiXjAuMzAuMjEiLCAic291cmNlLW1hcC1qcyI6ICJeMS4yLjEiLCAidGFpbHdpbmRjc3MiOiAiNC4zLjAiIH0gfSwgInNoYTUxMi1hRmI0Z1VoRk9nZGg5QVhvNEl6QkVPekJra0F4bTlWaWd3REpuTUlZdjNsY2ZYQ0pWZXNOZmJFYUJsNEJOZ1ZSeWlkOTJBbWR2aXF3QlVCUktTZVkzZz09Il0sCgogICAgIkB0YWlsd2luZGNzcy9veGlkZSI6IFsiQHRhaWx3aW5kY3NzL294aWRlQDQuMy4wIiwgIiIsIHsgIm9wdGlvbmFsRGVwZW5kZW5jaWVzIjogeyAiQHRhaWx3aW5kY3NzL294aWRlLWFuZHJvaWQtYXJtNjQiOiAiNC4zLjAiLCAiQHRhaWx3aW5kY3NzL294aWRlLWRhcndpbi1hcm02NCI6ICI0LjMuMCIsICJAdGFpbHdpbmRjc3Mvb3hpZGUtZGFyd2luLXg2NCI6ICI0LjMuMCIsICJAdGFpbHdpbmRjc3Mvb3hpZGUtZnJlZWJzZC14NjQiOiAiNC4zLjAiLCAiQHRhaWx3aW5kY3NzL294aWRlLWxpbnV4LWFybS1nbnVlYWJpaGYiOiAiNC4zLjAiLCAiQHRhaWx3aW5kY3NzL294aWRlLWxpbnV4LWFybTY0LWdudSI6ICI0LjMuMCIsICJAdGFpbHdpbmRjc3Mvb3hpZGUtbGludXgtYXJtNjQtbXVzbCI6ICI0LjMuMCIsICJAdGFpbHdpbmRjc3Mvb3hpZGUtbGludXgteDY0LWdudSI6ICI0LjMuMCIsICJAdGFpbHdpbmRjc3Mvb3hpZGUtbGludXgteDY0LW11c2wiOiAiNC4zLjAiLCAiQHRhaWx3aW5kY3NzL294aWRlLXdhc20zMi13YXNpIjogIjQuMy4wIiwgIkB0YWlsd2luZGNzcy9veGlkZS13aW4zMi1hcm02NC1tc3ZjIjogIjQuMy4wIiwgIkB0YWlsd2luZGNzcy9veGlkZS13aW4zMi14NjQtbXN2YyI6ICI0LjMuMCIgfSB9LCAic2hhNTEyLUY3SFpHQmVOOUkwL0F1dUpTNVB3Y0Q4eGF5eDVyaTVHaGpZVURCRVZZVWtleHlBL2dpd2JETmpSVnJ4U2V6RTNUMjUwT1UySy93cC9sdFd4M1VPZWZnPT0iXSwKCiAgICAiQHRhaWx3aW5kY3NzL294aWRlLWFuZHJvaWQtYXJtNjQiOiBbIkB0YWlsd2luZGNzcy9veGlkZS1hbmRyb2lkLWFybTY0QDQuMy4wIiwgIiIsIHsgIm9zIjogImFuZHJvaWQiLCAiY3B1IjogImFybTY0IiB9LCAic2hhNTEyLVRKUGlxNjd0S2xMdU9iUDZSa3d2VkdEb3hDTUJWdERnS2tMZmEvdXlqNy9GeXh2UXdIUytVT25WclhYZ2JFc2ZVYU1naVZ2QzRLYkpuUnIyNmhvNE5nPT0iXSwKCiAgICAiQHRhaWx3aW5kY3NzL294aWRlLWRhcndpbi1hcm02NCI6IFsiQHRhaWx3aW5kY3NzL294aWRlLWRhcndpbi1hcm02NEA0LjMuMCIsICIiLCB7ICJvcyI6ICJkYXJ3aW4iLCAiY3B1IjogImFybTY0IiB9LCAic2hhNTEyLW9NTi9XWlJiK1NPMzdCbVVFbEVnZUVXdVU4RS9IWFJraU9EeEp4TGUxVVRIVlhMcmRWU2dmYUpWN3BTbGhSR01TT2lYTHV4VElqZnNGM3dZdno4Y2dRPT0iXSwKCiAgICAiQHRhaWx3aW5kY3NzL294aWRlLWRhcndpbi14NjQiOiBbIkB0YWlsd2luZGNzcy9veGlkZS1kYXJ3aW4teDY0QDQuMy4wIiwgIiIsIHsgIm9zIjogImRhcndpbiIsICJjcHUiOiAieDY0IiB9LCAic2hhNTEyLU42Q1VtdTRhNmJLVkFEZnc3N3AraXc2WWQ5UTNPQmhlMHZlYURYK1FhemZ1VllsUXNIZkRneEJyc2pRL0lXK3p5d0w4bVRyTmQwU2RKVC96Z3R2TWRBPT0iXSwKCiAgICAiQHRhaWx3aW5kY3NzL294aWRlLWZyZWVic2QteDY0IjogWyJAdGFpbHdpbmRjc3Mvb3hpZGUtZnJlZWJzZC14NjRANC4zLjAiLCAiIiwgeyAib3MiOiAiZnJlZWJzZCIsICJjcHUiOiAieDY0IiB9LCAic2hhNTEyLXpETDVoQmtRZEg1QzZNcHFiSzNnUUFnUDgwdHNNd1NJMjZ2ak96akp0TkNNVW8wbEZnT0l0ekhLQkl1cE9aTlF4dDNvdVBIN1JQaHZOaGlUZkNlNUNRPT0iXSwKCiAgICAiQHRhaWx3aW5kY3NzL294aWRlLWxpbnV4LWFybS1nbnVlYWJpaGYiOiBbIkB0YWlsd2luZGNzcy9veGlkZS1saW51eC1hcm0tZ251ZWFiaWhmQDQuMy4wIiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJhcm0iIH0sICJzaGE1MTItUjA2SGROaTdBN09Fb01zZjZkNHRqWjcxUkNXblpRUEhqMm1ub3RTRlVSak5MZEJDK2NJZ1hRN2w4MUNxZW9pUWZ0amY2T09ibHhYTUluTWdOMlZ6TUE9PSJdLAoKICAgICJAdGFpbHdpbmRjc3Mvb3hpZGUtbGludXgtYXJtNjQtZ251IjogWyJAdGFpbHdpbmRjc3Mvb3hpZGUtbGludXgtYXJtNjQtZ251QDQuMy4wIiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi1xVEpIRUxYOGpldGpoUlFIQ0xpbGtWTG15YnB6TlFBdGFJL2dhb1ZvaWRuL3VmYk5EYkFvOEtsSzJKK3lQb2M4d1F4dkR4Q21oLzVscjhuQzErbFRiZz09Il0sCgogICAgIkB0YWlsd2luZGNzcy9veGlkZS1saW51eC1hcm02NC1tdXNsIjogWyJAdGFpbHdpbmRjc3Mvb3hpZGUtbGludXgtYXJtNjQtbXVzbEA0LjMuMCIsICIiLCB7ICJvcyI6ICJsaW51eCIsICJjcHUiOiAiYXJtNjQiIH0sICJzaGE1MTItWjZzdWtpUXNuZ25XTytsMzlYNHBQYmlXVDgxSUMrUExLRitQSHhJbHlaYkdOYjlNT0RmWWxYRVZsRnZlajVCT1pJbldYMDFrVnl6ZUx2SHNYaGZjelE9PSJdLAoKICAgICJAdGFpbHdpbmRjc3Mvb3hpZGUtbGludXgteDY0LWdudSI6IFsiQHRhaWx3aW5kY3NzL294aWRlLWxpbnV4LXg2NC1nbnVANC4zLjAiLCAiIiwgeyAib3MiOiAibGludXgiLCAiY3B1IjogIng2NCIgfSwgInNoYTUxMi1EUk5kUVJwU0d6UkdmQVJWdVZreHZNOFExMm5oMTlsNEJGL0c3ekdBMW9lKzl3Y0M2c2FGQkhUSVNycEljS3poaVh0U3JsU3JsdUNmdk11bGVkb0NUUT09Il0sCgogICAgIkB0YWlsd2luZGNzcy9veGlkZS1saW51eC14NjQtbXVzbCI6IFsiQHRhaWx3aW5kY3NzL294aWRlLWxpbnV4LXg2NC1tdXNsQDQuMy4wIiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJ4NjQiIH0sICJzaGE1MTItWjBJQURiRG84Ymg2STdoMklRTXg2MDFBZFhCTGZGcEVkVW90ZnQ4NmV2ZC84WlBmbFplOUNPUE84UTF2dytwZkxXSVVvOXpOL0pHWnZ3dUFKcWR1cWc9PSJdLAoKICAgICJAdGFpbHdpbmRjc3Mvb3hpZGUtd2FzbTMyLXdhc2kiOiBbIkB0YWlsd2luZGNzcy9veGlkZS13YXNtMzItd2FzaUA0LjMuMCIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAZW1uYXBpL2NvcmUiOiAiXjEuMTAuMCIsICJAZW1uYXBpL3J1bnRpbWUiOiAiXjEuMTAuMCIsICJAZW1uYXBpL3dhc2ktdGhyZWFkcyI6ICJeMS4yLjEiLCAiQG5hcGktcnMvd2FzbS1ydW50aW1lIjogIl4xLjEuNCIsICJAdHlieXMvd2FzbS11dGlsIjogIl4wLjEwLjEiLCAidHNsaWIiOiAiXjIuOC4xIiB9LCAiY3B1IjogIm5vbmUiIH0sICJzaGE1MTItSE5aR09VeEVtRWxrc1lSN1M2c0M1alRlTkdwb2JBc3k5dTdHdTBBc2tKOC8yMEZSOUdxZWJVeUIrSEJjVS9heDZCSHVpdUppK09kYTRCK1lYNkgxeUE9PSJdLAoKICAgICJAdGFpbHdpbmRjc3Mvb3hpZGUtd2luMzItYXJtNjQtbXN2YyI6IFsiQHRhaWx3aW5kY3NzL294aWRlLXdpbjMyLWFybTY0LW1zdmNANC4zLjAiLCAiIiwgeyAib3MiOiAid2luMzIiLCAiY3B1IjogImFybTY0IiB9LCAic2hhNTEyLVBlK1JQVlRpMVQrcXltdXVScGNkdndTVlpqbmxsL2Y3bjhnQnhNTWgzeExUY3RNREtxcGRmR2ltYk15aW9xdExoVVlaeGRKOXdHTmhWN01LSHZnWnNRPT0iXSwKCiAgICAiQHRhaWx3aW5kY3NzL294aWRlLXdpbjMyLXg2NC1tc3ZjIjogWyJAdGFpbHdpbmRjc3Mvb3hpZGUtd2luMzIteDY0LW1zdmNANC4zLjAiLCAiIiwgeyAib3MiOiAid2luMzIiLCAiY3B1IjogIng2NCIgfSwgInNoYTUxMi1NdnJmMmtYVy95ZVcvT1RlelpsQ0dPaXJYUmNVdUxJQngvNVkxMkJhUE03d0pvcnlHNmRmUy9OSkw4YUJQcXRURXgvVm00VDR2S3pGVWNLRFQrVEtVQT09Il0sCgogICAgIkB0YWlsd2luZGNzcy9wb3N0Y3NzIjogWyJAdGFpbHdpbmRjc3MvcG9zdGNzc0A0LjMuMCIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAYWxsb2MvcXVpY2stbHJ1IjogIl41LjIuMCIsICJAdGFpbHdpbmRjc3Mvbm9kZSI6ICI0LjMuMCIsICJAdGFpbHdpbmRjc3Mvb3hpZGUiOiAiNC4zLjAiLCAicG9zdGNzcyI6ICJeOC41LjEwIiwgInRhaWx3aW5kY3NzIjogIjQuMy4wIiB9IH0sICJzaGE1MTItSm0wNVRqeCs5eUNMR3Y1cXcxYys4NFBzZHM4TW55ckVRWUNCK0ZGazJsZ0dpVWpsUnFkeGtlNG1WVHVZcmoyeG5WWnFLaW0yQXByNXlTdVFSWUF3L3c9PSJdLAoKICAgICJAdHlieXMvd2FzbS11dGlsIjogWyJAdHlieXMvd2FzbS11dGlsQDAuMTAuMiIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJ0c2xpYiI6ICJeMi40LjAiIH0gfSwgInNoYTUxMi1Sb0J2SjJYMHd1S2xXRklqcndmZkd3MUlxWkhLUXF6SWNoS2FhZFpaZm5OcHNBWXAybU0waDM2SnRQQ2pOREFIR2dZZXovMTV1TUJwZkd3Y2hoaU1nZz09Il0sCgogICAgIkB0eXBlcy9jaGFpIjogWyJAdHlwZXMvY2hhaUA1LjIuMyIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAdHlwZXMvZGVlcC1lcWwiOiAiKiIsICJhc3NlcnRpb24tZXJyb3IiOiAiXjIuMC4xIiB9IH0sICJzaGE1MTItTXc1NThvZUE5ZkZidjY1L3k0bUh0WERzOWJQbkZNWkFML2p4ZFBGVXBPSEhJWFg5MW1jZ0VIYlM1TGFocitwd1pGUjhBN0dRbGVSV2VJNmNHRkMyVUE9PSJdLAoKICAgICJAdHlwZXMvZGVlcC1lcWwiOiBbIkB0eXBlcy9kZWVwLWVxbEA0LjAuMiIsICIiLCB7fSwgInNoYTUxMi1jOWg5ZFZWTWlnTVBjNGJ3VHZDNWR4cXRxSlp3UVBlUHNXalBscFNPbm9qYm9yNnBHcWRrNTQxbGZBN0FxRlFyNXBCMUJSZHEwanVZOWRiODFCd3lGdz09Il0sCgogICAgIkB0eXBlcy9lc3RyZWUiOiBbIkB0eXBlcy9lc3RyZWVAMS4wLjkiLCAiIiwge30sICJzaGE1MTItR2hkUGd5MWVsNC9JbVAwNVgwNVV3NGN3Mi9NOTNCQ1VtbkV2V1pOU3RsQ3pFS01FNEZraytZcG9BNU9pSE5RbW9TN0NhZmI4WGEzUHlhOG0xUXJ6ZWc9PSJdLAoKICAgICJAdHlwZXMvbm9kZSI6IFsiQHR5cGVzL25vZGVAMjIuMTkuMTkiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAidW5kaWNpLXR5cGVzIjogIn42LjIxLjAiIH0gfSwgInNoYTUxMi1keWgveE8yRmg1YllyZldhYXFHclJRUUdrTmRtWXc2QW1hQVV2WWVVTU5UV1F0dmI3OTZpa0xkbVRjaFJtT2xPaUlKMVREWGZXZ1Z4MVFrVWxRNkhldz09Il0sCgogICAgIkB0eXBlcy9yZWFjdCI6IFsiQHR5cGVzL3JlYWN0QDE5LjIuMTUiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiY3NzdHlwZSI6ICJeMy4yLjIiIH0gfSwgInNoYTUxMi1lUndjR05IdmUrRThxdEVRU1NSbDZ1cmgrckZvcDR2OGdtNk84ckd2MjVDb2RidkZkTGpBMXZWUTFLa2lGRTB3MFVQT25iOHREaUZLTDVscDBydFk1UT09Il0sCgogICAgIkB0eXBlcy9yZWFjdC1kb20iOiBbIkB0eXBlcy9yZWFjdC1kb21AMTkuMi4zIiwgIiIsIHsgInBlZXJEZXBlbmRlbmNpZXMiOiB7ICJAdHlwZXMvcmVhY3QiOiAiXjE5LjIuMCIgfSB9LCAic2hhNTEyLWpwMkwvZVk2Zm4rS2dWVlFBT3FZSXRiRjBWWS9ZQXBlNU16MkYwYXlrU084Z3gzMWJZQ1p5dlNlWXhDSEt2ekhHNWVaamMrenlhUzVCckJXeWEyK2tRPT0iXSwKCiAgICAiQHZpdGVzdC9leHBlY3QiOiBbIkB2aXRlc3QvZXhwZWN0QDQuMS42IiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIkBzdGFuZGFyZC1zY2hlbWEvc3BlYyI6ICJeMS4xLjAiLCAiQHR5cGVzL2NoYWkiOiAiXjUuMi4yIiwgIkB2aXRlc3Qvc3B5IjogIjQuMS42IiwgIkB2aXRlc3QvdXRpbHMiOiAiNC4xLjYiLCAiY2hhaSI6ICJeNi4yLjIiLCAidGlueXJhaW5ib3ciOiAiXjMuMS4wIiB9IH0sICJzaGE1MTItN0VIRHF1UHRoQUxTVjBqaGhqZ0VXOEZYYXZpTXg3clNxdThXNm9xQ29BdU9oS292ODE0UDk5UURWMXB4TUEzUVB2MjFZdWR2Sm5nSWhqck5JNG9wTGc9PSJdLAoKICAgICJAdml0ZXN0L21vY2tlciI6IFsiQHZpdGVzdC9tb2NrZXJANC4xLjYiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiQHZpdGVzdC9zcHkiOiAiNC4xLjYiLCAiZXN0cmVlLXdhbGtlciI6ICJeMy4wLjMiLCAibWFnaWMtc3RyaW5nIjogIl4wLjMwLjIxIiB9LCAicGVlckRlcGVuZGVuY2llcyI6IHsgIm1zdyI6ICJeMi40LjkiLCAidml0ZSI6ICJeNi4wLjAgfHwgXjcuMC4wIHx8IF44LjAuMCIgfSwgIm9wdGlvbmFsUGVlcnMiOiBbIm1zdyIsICJ2aXRlIl0gfSwgInNoYTUxMi1NQ0ZjNjNjek1qRUluT2xjWTJjcFFDdkNOK0tnYkFuKzYweHU5Y01nUDRzS2FMQzVKTkFLdzdKSDhRZEFub0FDODhoVzFJaVNOWitHZ1ZYbE4xVWNNUT09Il0sCgogICAgIkB2aXRlc3QvcHJldHR5LWZvcm1hdCI6IFsiQHZpdGVzdC9wcmV0dHktZm9ybWF0QDQuMS42IiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgInRpbnlyYWluYm93IjogIl4zLjEuMCIgfSB9LCAic2hhNTEyLWg1U3hEL0l6TmhaWW5yU1pSc1VaUUlDK3ZEMEdZOGNVdnEwaXdzbWtGS2l4UkNLTExXcUNYYS9GSVE0UzFSK3NJK1BHb29qa0hzZE5yYlppTTlRcGd3PT0iXSwKCiAgICAiQHZpdGVzdC9ydW5uZXIiOiBbIkB2aXRlc3QvcnVubmVyQDQuMS42IiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIkB2aXRlc3QvdXRpbHMiOiAiNC4xLjYiLCAicGF0aGUiOiAiXjIuMC4zIiB9IH0sICJzaGE1MTItbk9QQ21uMit5RDBaTm1LZHNYR3YvVXhNTVdiTXVLZUQ2R3lZbmNOd2RrWUR4cFF2clBTS1lqMnJXdURqQzJZNGI2dzZoamlwNWRCS0Z6RVV1WmUzdkE9PSJdLAoKICAgICJAdml0ZXN0L3NuYXBzaG90IjogWyJAdml0ZXN0L3NuYXBzaG90QDQuMS42IiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIkB2aXRlc3QvcHJldHR5LWZvcm1hdCI6ICI0LjEuNiIsICJAdml0ZXN0L3V0aWxzIjogIjQuMS42IiwgIm1hZ2ljLXN0cmluZyI6ICJeMC4zMC4yMSIsICJwYXRoZSI6ICJeMi4wLjMiIH0gfSwgInNoYTUxMi1ZaHNkRTZ4QVZmVERtemp4TDJaRFV2amorWnNneU9LZStUZFF6cWtENzJ3SU9tSGthOE51R1E2TnBUTlp2OUQyWjYzZmJ3V0tKUGVWcEV3NEVRZ1l4dz09Il0sCgogICAgIkB2aXRlc3Qvc3B5IjogWyJAdml0ZXN0L3NweUA0LjEuNiIsICIiLCB7fSwgInNoYTUxMi1KRkt4TXg2dWRod0toL0xkbzI3MGUxN1FYNzEwdmd1bk1rdVBBdlhqSFN2QzZvcUxXQUhoVmhqZy9JNzFxMHUwQ0JTRXJJT0RWMUtqdjBGUU5TV2pkZz09Il0sCgogICAgIkB2aXRlc3QvdXRpbHMiOiBbIkB2aXRlc3QvdXRpbHNANC4xLjYiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiQHZpdGVzdC9wcmV0dHktZm9ybWF0IjogIjQuMS42IiwgImNvbnZlcnQtc291cmNlLW1hcCI6ICJeMi4wLjAiLCAidGlueXJhaW5ib3ciOiAiXjMuMS4wIiB9IH0sICJzaGE1MTItRnhJWStVODFSM0xHS0N4YUhIRlJRNStnNi9pUmdHTG1lSFdkcDJBbWo0bGpRUnJFSVdIbVp5RGZEWUJSWmxweXFBN3FLeHRTOUREMWRoazhSblJJVlE9PSJdLAoKICAgICJhbnNpLXJlZ2V4IjogWyJhbnNpLXJlZ2V4QDUuMC4xIiwgIiIsIHt9LCAic2hhNTEyLXF1SlFYbFRTVUdMMkxIOVNVWG84VndzWTRzb2FuaGdvNkxOU204NEUxTEJjRThzM08wd3BkaVJ6eVI5ei9aWkpNbE1XdjM3cU9PYjlwZEpsTVVFS0ZRPT0iXSwKCiAgICAiYW5zaS1zdHlsZXMiOiBbImFuc2ktc3R5bGVzQDQuMy4wIiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgImNvbG9yLWNvbnZlcnQiOiAiXjIuMC4xIiB9IH0sICJzaGE1MTItemJCOXJDSkFUMXJiamlWRGIyaHFLRkhOWUx4Z3RrOE5VUnhaM0lad0QzRjZOdHhiWFpRQ25uU2kxTGt4K0lEb2hkUGxGcDIyMndWQUxJaGVaSlFTRWc9PSJdLAoKICAgICJhc3NlcnRpb24tZXJyb3IiOiBbImFzc2VydGlvbi1lcnJvckAyLjAuMSIsICIiLCB7fSwgInNoYTUxMi1Jemk4UlFjZmZxQ2VOVmdGaWdLbGkxc3NrbElicEhuQ1ljNkFrblhHWW9CNmdySnF5ZWJ5N2p2MTJKVVFnbVRBbklEbmJjazF1eGtzVDRkek4zUFdCQT09Il0sCgogICAgImJhc2VsaW5lLWJyb3dzZXItbWFwcGluZyI6IFsiYmFzZWxpbmUtYnJvd3Nlci1tYXBwaW5nQDIuMTAuMzEiLCAiIiwgeyAiYmluIjogeyAiYmFzZWxpbmUtYnJvd3Nlci1tYXBwaW5nIjogImRpc3QvY2xpLmNqcyIgfSB9LCAic2hhNTEyLU11allPM2VQNzJ1dm1TRTBpNHdsdHNvZFJmSXBaQVRQM2p2elJOUkdHeGd6SWQ3YVZvY1ZKSlYzbmYwMXFuenpLRkd4UVZDOWJwV3hsNWNqeFRyLzdRPT0iXSwKCiAgICAiY2FuaXVzZS1saXRlIjogWyJjYW5pdXNlLWxpdGVAMS4wLjMwMDAxNzkzIiwgIiIsIHt9LCAic2hhNTEyLWl3U3NZV2FDT29oMjZjVjhOd05SVmlIbHJmVXZZc0hEZlJWY2J0bXcwS2c2UEpJWlpYd01rajE0NDJGWUxCR2tlVWYxanVBc1UzRFRmeFc1NzltclBBPT0iXSwKCiAgICAiY2hhaSI6IFsiY2hhaUA2LjIuMiIsICIiLCB7fSwgInNoYTUxMi1OVVBSbHVPZk9pVEtCS3ZXUHRTRDRQaEZ2V0NxT2kwQkdTdE5XczU3WDlqczdYR1RwclNtRm96NUYwdFdoUjRXUGpOZVI5alhxZEM3L1VwU0pUbmxSZz09Il0sCgogICAgImNoYWxrIjogWyJjaGFsa0A0LjEuMiIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJhbnNpLXN0eWxlcyI6ICJeNC4xLjAiLCAic3VwcG9ydHMtY29sb3IiOiAiXjcuMS4wIiB9IH0sICJzaGE1MTItb0tuYmhGeVJJWHBVdWV6OGlCTW15RWE0bmJqNElPUXl1aGMvd3k5a1k3L1dWUGN3SU85VkE2NjhQdThSa083KzBHNzZTTFJPZXl3OUNwUTA2MWk0bUE9PSJdLAoKICAgICJjbGllbnQtb25seSI6IFsiY2xpZW50LW9ubHlAMC4wLjEiLCAiIiwge30sICJzaGE1MTItSVYzT3UwalNNelpyZDNwWjQ4bkxrVDlEQTdBZzFwblB6YWlRaHBXN2MzUmJjcXF6dnp6VnUrTDhnZnFNcC84SU0yTVF0U2lxYUN4cnJjZnU4SThyTUE9PSJdLAoKICAgICJjbGl1aSI6IFsiY2xpdWlAOC4wLjEiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAic3RyaW5nLXdpZHRoIjogIl40LjIuMCIsICJzdHJpcC1hbnNpIjogIl42LjAuMSIsICJ3cmFwLWFuc2kiOiAiXjcuMC4wIiB9IH0sICJzaGE1MTItQlNlTm55dXM3NUM0Ly9OUTlnUXQxL2NzVFh5by84U2IrYWZMQWt6QXB0RnVNc29kOUhGb2tHTnVkWnBpL29RVjczaG5WSytzUis1UFZSTWQrRHI3WVE9PSJdLAoKICAgICJjbHN4IjogWyJjbHN4QDIuMS4xIiwgIiIsIHt9LCAic2hhNTEyLWVZbTBRV0J0VXJCV1pXRzBkMzg2T0dBdzE2Wjk5NVBpT1ZvMkI3YmpXU2JIZWRHbDVlMFpXYXE2NWtPR2dVU05lc0VJRGtCOUlTYlRnL0pLOWRoQ1pBPT0iXSwKCiAgICAiY29sb3ItY29udmVydCI6IFsiY29sb3ItY29udmVydEAyLjAuMSIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJjb2xvci1uYW1lIjogIn4xLjEuNCIgfSB9LCAic2hhNTEyLVJSRUNQc2o3aXUveGI1b0tZY3NGSFNwcEZObnNqLzUyT1ZUUktiNHpQNW9uWHdWRjN6Vm1tVG9OY09mR0MrQ1JEcGZLL1U1ODRmTWczOFpIQ2FFbEtRPT0iXSwKCiAgICAiY29sb3ItbmFtZSI6IFsiY29sb3ItbmFtZUAxLjEuNCIsICIiLCB7fSwgInNoYTUxMi1kT3krM0F1VzNhMndOYlpISXVNWnBUY2dqR3VMVS91QkwvdWJjWkY5T1hiRG84ZmY0Tzh5VnA1QmYwZWZTOHVFb1lvNXE0Rng3ZFk5T2dRR1hnQXNRQT09Il0sCgogICAgImNvbmN1cnJlbnRseSI6IFsiY29uY3VycmVudGx5QDkuMi4xIiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgImNoYWxrIjogIjQuMS4yIiwgInJ4anMiOiAiNy44LjIiLCAic2hlbGwtcXVvdGUiOiAiMS44LjMiLCAic3VwcG9ydHMtY29sb3IiOiAiOC4xLjEiLCAidHJlZS1raWxsIjogIjEuMi4yIiwgInlhcmdzIjogIjE3LjcuMiIgfSwgImJpbiI6IHsgImNvbmMiOiAiZGlzdC9iaW4vY29uY3VycmVudGx5LmpzIiwgImNvbmN1cnJlbnRseSI6ICJkaXN0L2Jpbi9jb25jdXJyZW50bHkuanMiIH0gfSwgInNoYTUxMi1mc2ZyTzBNeFY2NFpub3k4L2wxdlZJampIYTI5U1p5eXFQZ1FCd2hpRGNhVzh3SmMyVzNYV1ZPR3g0TTNvSkJudi96ZFVaSUlwMWdEZVM5OEd6UDhOZz09Il0sCgogICAgImNvbnZlcnQtc291cmNlLW1hcCI6IFsiY29udmVydC1zb3VyY2UtbWFwQDIuMC4wIiwgIiIsIHt9LCAic2hhNTEyLUt2cDQ1OUhyVjJGRUoxQ0FzaTFLdStNWTNrYXNIMTlURnlrVHoyeFdtTWVxNmJrMk5VM1hYdmZKK1E2MW0weGt0V3d0KzFIU1lmM0pac1RtczNhUkpnPT0iXSwKCiAgICAiY3NzdHlwZSI6IFsiY3NzdHlwZUAzLjIuMyIsICIiLCB7fSwgInNoYTUxMi16MUhHS2NZeTJ4QThBR1Fmd3JuMFBBeStQQjdYL0dTajNVVkpXOXFLeW40M3hXYStnbDVuWG1VNHFxTE1SeldWTEZDOEt1c1VYOFQvMGtDaU9ZcEFJUT09Il0sCgogICAgImRhdGUtZm5zIjogWyJkYXRlLWZuc0A0LjIuMSIsICIiLCB7fSwgInNoYTUxMi0zN1JoU2R4YUcxc3VlbjZWREN6YTZyTnJRZm9veVFoNTdIRlZQd1FHRXEyUVdsaVZMelBRWjhPYTAxN3dlT3UrSFpDbnpJN04zUGYvd3lvQktmRXFyQT09Il0sCgogICAgImRhdGUtZm5zLWphbGFsaSI6IFsiZGF0ZS1mbnMtamFsYWxpQDQuMS4wLTAiLCAiIiwge30sICJzaGE1MTItaFRJUC96K3QrcUt3QkRjbW1zbm1qV1RkdXhDZys1S2ZkcVdRdmIyWC84Qzkra25ZWTZlcE4vcGZ4ZER1eVZsU1ZlRnowc001ZUVmd0lVUTcwVTRja2c9PSJdLAoKICAgICJkZXRlY3QtbGliYyI6IFsiZGV0ZWN0LWxpYmNAMi4xLjIiLCAiIiwge30sICJzaGE1MTItQnRqMkJPT084M28zV3lINTllOE1nWHN4RVFWY2Fya1VPcEVZcnViQjB1cnduTjEweVEzNjRyc2lCeVUxMW5abHFXWVptMDVpL29mN2lvNG16aWhCdFE9PSJdLAoKICAgICJlbW9qaS1yZWdleCI6IFsiZW1vamktcmVnZXhAOC4wLjAiLCAiIiwge30sICJzaGE1MTItTVNqWXpjV05PQTBld0FIcHowTXhwWUZ2d2c2eWp5MU5HM3h0ZW9xejY0NFZDby9SUGducjEvR0d0K2ljM2lKVHpROEV1M1RkTTE0U2F3blZVbUdFNkE9PSJdLAoKICAgICJlbmhhbmNlZC1yZXNvbHZlIjogWyJlbmhhbmNlZC1yZXNvbHZlQDUuMjEuNSIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJncmFjZWZ1bC1mcyI6ICJeNC4yLjQiLCAidGFwYWJsZSI6ICJeMi4zLjMiIH0gfSwgInNoYTUxMi1tTENOYnJRbGkxMUsxeVNVbXVOdDRaVUIzT3BHSURxNHEydlRCVGY1Y0wybHBzUmpJOVFLcVNEMG5kalc4Rnl2Y1cvSmo0NmdNZTlzeXlIQXN2TWEvQT09Il0sCgogICAgImVzLW1vZHVsZS1sZXhlciI6IFsiZXMtbW9kdWxlLWxleGVyQDIuMS4wIiwgIiIsIHt9LCAic2hhNTEyLW4yN3pUWU1qWXUxYWo0TWpDV3pTUDdHOXI3NXV0c2FvYzhtNjF3ZUsrVzhKTUJHR1F5YmQ0M0dzdENYWjNXTm1TRnRHVDl3aTU5cVFUVzZtaFRSNUxRPT0iXSwKCiAgICAiZXNjYWxhZGUiOiBbImVzY2FsYWRlQDMuMi4wIiwgIiIsIHt9LCAic2hhNTEyLVdVajJxbHhhUXRPNGc2UHE1YzI5R1RjV0dEeWQ4aXRMOHpUbGlwZ0VDejNKZXNBaWlPS290ZDhKVTZvdEIzUEFDZ0c2eGtKVXlWaGJvTVMrYmplL2pBPT0iXSwKCiAgICAiZXN0cmVlLXdhbGtlciI6IFsiZXN0cmVlLXdhbGtlckAzLjAuMyIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAdHlwZXMvZXN0cmVlIjogIl4xLjAuMCIgfSB9LCAic2hhNTEyLTdSVUtmWGdTTU1renQ2WnVYbXFhcE91ckxHUFBmZ2o2bDl1Ulo3bFJHb2x2azB5MnlvY2MzNUxkY3hLQzVQUVpkbjJETXFpb0FRMk5vV2NyVEttbTZnPT0iXSwKCiAgICAiZXhwZWN0LXR5cGUiOiBbImV4cGVjdC10eXBlQDEuMy4wIiwgIiIsIHt9LCAic2hhNTEyLWtudnllYXVZaHFqT1l2UTY2TXpuU01zODN3bUhyQ3ljTkVONkFvKzJBZVlFZnhVSWt1aVZ4ZEVhMXFsR0VQSytXZTNuMFRIaURjaVlTc0NjZ1cvRG9BPT0iXSwKCiAgICAiZmRpciI6IFsiZmRpckA2LjUuMCIsICIiLCB7ICJwZWVyRGVwZW5kZW5jaWVzIjogeyAicGljb21hdGNoIjogIl4zIHx8IF40IiB9LCAib3B0aW9uYWxQZWVycyI6IFsicGljb21hdGNoIl0gfSwgInNoYTUxMi10SWJZdFpidWNPczBCUkdxUEprc2hKVVlkTCtTREg3ZFZNOGdqeStFUnAzV0FVakxFRkpFKzAya2FueUh0d2pXT253cktZQml3QW1NMHA0a0xKQW5YZz09Il0sCgogICAgImZzZXZlbnRzIjogWyJmc2V2ZW50c0AyLjMuMyIsICIiLCB7ICJvcyI6ICJkYXJ3aW4iIH0sICJzaGE1MTItNXhvRGZYK2ZMN2ZhQVRuYWdtV1BwYkZ0d2gvUjc3V21NTXFxSEdTNjVDM3Z2QjBZSHJnRitCMVltWjM0NDF0TWo1bjYzazAyMTJYTm9Kd3psaGZmUXc9PSJdLAoKICAgICJnZXQtY2FsbGVyLWZpbGUiOiBbImdldC1jYWxsZXItZmlsZUAyLjAuNSIsICIiLCB7fSwgInNoYTUxMi1EeUZQM0JNLzNZSFRRT0NVTC93ME9aSFIwbHBLZUdyeG90Y0hXY3FORWRubHRxRndYVmZoRUJROTRlSW8zNEFmUXBvMHJHa2k0Y3lJaWZ0WTA2aDJGZz09Il0sCgogICAgImdyYWNlZnVsLWZzIjogWyJncmFjZWZ1bC1mc0A0LjIuMTEiLCAiIiwge30sICJzaGE1MTItUmJKNS9qbUZjTk5DY0RWNW85ZVRuQkxKL0hzeldWMFA3M2JjK0ZmNG5TL3JKaitZYVM2SUd5aU9MMFZvQllYK2wxV3JsM2s2M2gvS3JIK25oSjBYdlE9PSJdLAoKICAgICJoYXMtZmxhZyI6IFsiaGFzLWZsYWdANC4wLjAiLCAiIiwge30sICJzaGE1MTItRXlrSlQvUTFLalRXY3RwcGdJQWdmU08wdEtWdVpVamhnTXIxN2txVHVtTWw2QWZ2M0VJU2xlVTdxWlV6b1hERlRBSFREQzROT29HL1p4VTNFdmxNUFE9PSJdLAoKICAgICJpcy1mdWxsd2lkdGgtY29kZS1wb2ludCI6IFsiaXMtZnVsbHdpZHRoLWNvZGUtcG9pbnRAMy4wLjAiLCAiIiwge30sICJzaGE1MTItenltbTUrdStzQ3NTV3lEOXFOYWVqVjNERnZoQ0tjbEtkaXpZYUpVdUhBODNSTGpiN25TdUduZGRDSEd2MGhrK0tZN0JNQWxzV2VLNFVlZzZFVjZYUWc9PSJdLAoKICAgICJqaXRpIjogWyJqaXRpQDIuNy4wIiwgIiIsIHsgImJpbiI6IHsgImppdGkiOiAibGliL2ppdGktY2xpLm1qcyIgfSB9LCAic2hhNTEyLUFDLzdKb2ZKdlpHcnJuZVdOYUVuSmVPTFV4K0psR3Q3dE5hMHdaaVJQVDRNWTF3bWZLanQyKzZPMnAydXoyK3NrbGw4T1pabUpNTnFla2U3a0tiTmdRPT0iXSwKCiAgICAibGlicGhvbmVudW1iZXItanMiOiBbImxpYnBob25lbnVtYmVyLWpzQDEuMTIuNDEiLCAiIiwge30sICJzaGE1MTItbHNtTW1HWEJ4WElLL1ZNTEVqMGtMNk10VXMxa0JHajFuVEN6aTZ6Z1FvRzFERXdxd3QyRFF5SHhjTHlrY2VJeEFuZkUzaHlhN051SWg2UHBDNlMzZkE9PSJdLAoKICAgICJsaWdodG5pbmdjc3MiOiBbImxpZ2h0bmluZ2Nzc0AxLjMyLjAiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiZGV0ZWN0LWxpYmMiOiAiXjIuMC4zIiB9LCAib3B0aW9uYWxEZXBlbmRlbmNpZXMiOiB7ICJsaWdodG5pbmdjc3MtYW5kcm9pZC1hcm02NCI6ICIxLjMyLjAiLCAibGlnaHRuaW5nY3NzLWRhcndpbi1hcm02NCI6ICIxLjMyLjAiLCAibGlnaHRuaW5nY3NzLWRhcndpbi14NjQiOiAiMS4zMi4wIiwgImxpZ2h0bmluZ2Nzcy1mcmVlYnNkLXg2NCI6ICIxLjMyLjAiLCAibGlnaHRuaW5nY3NzLWxpbnV4LWFybS1nbnVlYWJpaGYiOiAiMS4zMi4wIiwgImxpZ2h0bmluZ2Nzcy1saW51eC1hcm02NC1nbnUiOiAiMS4zMi4wIiwgImxpZ2h0bmluZ2Nzcy1saW51eC1hcm02NC1tdXNsIjogIjEuMzIuMCIsICJsaWdodG5pbmdjc3MtbGludXgteDY0LWdudSI6ICIxLjMyLjAiLCAibGlnaHRuaW5nY3NzLWxpbnV4LXg2NC1tdXNsIjogIjEuMzIuMCIsICJsaWdodG5pbmdjc3Mtd2luMzItYXJtNjQtbXN2YyI6ICIxLjMyLjAiLCAibGlnaHRuaW5nY3NzLXdpbjMyLXg2NC1tc3ZjIjogIjEuMzIuMCIgfSB9LCAic2hhNTEyLU5YWUJ6aW5OcmJsZnJhUEd5cmJQb0QxOUMxaDlsZkkvMW16Z1dZdlhVVGU0MTRHei9YMUZEMlhCWlNaTTdyUlRyTUE4SkwzT3RBYUdpZnJJS2hRNXlRPT0iXSwKCiAgICAibGlnaHRuaW5nY3NzLWFuZHJvaWQtYXJtNjQiOiBbImxpZ2h0bmluZ2Nzcy1hbmRyb2lkLWFybTY0QDEuMzIuMCIsICIiLCB7ICJvcyI6ICJhbmRyb2lkIiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi1ZSzcvQ2xUdDRrQUswdm82dzNYK1BubTBEMmNmMnZQSGJoT1hkb050aTFHYTBhbDFQNFRCWmh3akFUdmpOd0xFQkNuS3ZqSmMyalFnSFhIME5Fd2xBZz09Il0sCgogICAgImxpZ2h0bmluZ2Nzcy1kYXJ3aW4tYXJtNjQiOiBbImxpZ2h0bmluZ2Nzcy1kYXJ3aW4tYXJtNjRAMS4zMi4wIiwgIiIsIHsgIm9zIjogImRhcndpbiIsICJjcHUiOiAiYXJtNjQiIH0sICJzaGE1MTItUnplRzlKdTViYWcyQnYxL2x3bFZKdkJFM3E2VHRYc2tkWkxMQ3lmZzVwdCtITHo5QnFsSUNPN0xaTTdWSE5UVG4vNVBSaEhGQlNqazVsYzRjbXNjUFE9PSJdLAoKICAgICJsaWdodG5pbmdjc3MtZGFyd2luLXg2NCI6IFsibGlnaHRuaW5nY3NzLWRhcndpbi14NjRAMS4zMi4wIiwgIiIsIHsgIm9zIjogImRhcndpbiIsICJjcHUiOiAieDY0IiB9LCAic2hhNTEyLVUrUXNCcDJtL3Myd3FwVVlULzZ3bmxhZ2RaYnRaZG5kU211dC9OSnFsQ2NNTFRXcDVtdUNySUQrSzVVSjZqcUQyQkZzaGVqQ1lYbmlQRGJOaDczVjh3PT0iXSwKCiAgICAibGlnaHRuaW5nY3NzLWZyZWVic2QteDY0IjogWyJsaWdodG5pbmdjc3MtZnJlZWJzZC14NjRAMS4zMi4wIiwgIiIsIHsgIm9zIjogImZyZWVic2QiLCAiY3B1IjogIng2NCIgfSwgInNoYTUxMi1KQ1RpZ2VkRWtzWmszdEhUVHRobk1kVmZHZjYxRmt5OEppMkU0WWpVVEVRWDE0eGl5L2xUelhudTF2d2laZTNiWWUwcStTcHNTSC9DVGVEWEs2V0hpZz09Il0sCgogICAgImxpZ2h0bmluZ2Nzcy1saW51eC1hcm0tZ251ZWFiaWhmIjogWyJsaWdodG5pbmdjc3MtbGludXgtYXJtLWdudWVhYmloZkAxLjMyLjAiLCAiIiwgeyAib3MiOiAibGludXgiLCAiY3B1IjogImFybSIgfSwgInNoYTUxMi14NnJubnBSYTJHTDB6UU9rdDZydHMzWURQemR1THBXdndBRjZFTWhYRlZaWEQ0dFByQmtFRnF6R293ekNzSVdzUGpxU0srdHlORU9EVUJYZWVWSFNrdz09Il0sCgogICAgImxpZ2h0bmluZ2Nzcy1saW51eC1hcm02NC1nbnUiOiBbImxpZ2h0bmluZ2Nzcy1saW51eC1hcm02NC1nbnVAMS4zMi4wIiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi0wbm5NeW95T0xSSlhmYk1PaWxhU1JjTEgzSnc1ejlIRE5HZlQvZ3dDUGdhRGpueDBpOHc3dkJ6RkxGUjFmNkNNTEtGOGdWYmVibWtVTjNmYS9rUUpwUT09Il0sCgogICAgImxpZ2h0bmluZ2Nzcy1saW51eC1hcm02NC1tdXNsIjogWyJsaWdodG5pbmdjc3MtbGludXgtYXJtNjQtbXVzbEAxLjMyLjAiLCAiIiwgeyAib3MiOiAibGludXgiLCAiY3B1IjogImFybTY0IiB9LCAic2hhNTEyLVVwUWtvZW5yNFVKRXpnVklZcEk4MGxERnZSbVBWZzZvcWJvTkhmb0g0Q1FJZk5BK0hPclo3TW83S1pQMDJkQzZMamdoUFFKZUJzdlhoSm9kL3duSUJnPT0iXSwKCiAgICAibGlnaHRuaW5nY3NzLWxpbnV4LXg2NC1nbnUiOiBbImxpZ2h0bmluZ2Nzcy1saW51eC14NjQtZ251QDEuMzIuMCIsICIiLCB7ICJvcyI6ICJsaW51eCIsICJjcHUiOiAieDY0IiB9LCAic2hhNTEyLVY3UXI1MkloWm1kS1BWcitWdHc4bytXTHNRSllDVGQ4bG9JZnBEYU1SV0dVWmZCT1lFSmV5SklrcUdJRE1aUHdQeDI0cFVNZndTeHhJOHBoci9NYk9BPT0iXSwKCiAgICAibGlnaHRuaW5nY3NzLWxpbnV4LXg2NC1tdXNsIjogWyJsaWdodG5pbmdjc3MtbGludXgteDY0LW11c2xAMS4zMi4wIiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJ4NjQiIH0sICJzaGE1MTItYlljTHArVmIwYXdzaVhnLzgwdUNSZXpDWUhOZzEvbDNtdDBnekhuV1Y5WFAxVzVzS2E1L1RDZEdXYVIvekJNMlBlRi9IYnNRdi9qMlVSTk9pVnV4V2c9PSJdLAoKICAgICJsaWdodG5pbmdjc3Mtd2luMzItYXJtNjQtbXN2YyI6IFsibGlnaHRuaW5nY3NzLXdpbjMyLWFybTY0LW1zdmNAMS4zMi4wIiwgIiIsIHsgIm9zIjogIndpbjMyIiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi04U2JDOEJSNDBwUzZiYUNNOHNidFlEU3dFVlFkNEpsRlRPbGFEM2dXR0hmVGhUY0FCbk5EQmRhNmVUWmVxYm9mYWxJSmhGeDBxS3pnSEptY1BUbkdkdz09Il0sCgogICAgImxpZ2h0bmluZ2Nzcy13aW4zMi14NjQtbXN2YyI6IFsibGlnaHRuaW5nY3NzLXdpbjMyLXg2NC1tc3ZjQDEuMzIuMCIsICIiLCB7ICJvcyI6ICJ3aW4zMiIsICJjcHUiOiAieDY0IiB9LCAic2hhNTEyLUFtcTlCL1NvWllkRGkxa0Zyb2pub3FQTHhZaFE0V281WGlMOEVWSnJWc0I4QVJvQzFQV1c2Vkd0VDBXS0NlbWp5OGFDK2xvdUpualM3VTE4eDNiMDZRPT0iXSwKCiAgICAibWFnaWMtc3RyaW5nIjogWyJtYWdpYy1zdHJpbmdAMC4zMC4yMSIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAanJpZGdld2VsbC9zb3VyY2VtYXAtY29kZWMiOiAiXjEuNS41IiB9IH0sICJzaGE1MTItdmQyRjRZVXlFWEtHY0xIb3ErVEV5Q2p4dWVTZUhuRnh5eWpOcDgweWcwWFY0dlVobkRlci9sdnZscU0vYXJCNWJYUU41SzIvM29pbnlDUnl4OFQyQ1E9PSJdLAoKICAgICJuYW5vaWQiOiBbIm5hbm9pZEAzLjMuMTIiLCAiIiwgeyAiYmluIjogeyAibmFub2lkIjogImJpbi9uYW5vaWQuY2pzIiB9IH0sICJzaGE1MTItWkI5UkgvMzlxcHE1VnU2WStObVVhRmhRUjZwcCtNMlh0NzZYQm5Fd0RhR2NWQXFobHZ4cmwzQjJiS1M1RDNOSDNRUjc2djNhU3JLYUYvS2l5N2xFdFE9PSJdLAoKICAgICJuZXh0IjogWyJuZXh0QDE2LjIuNiIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAbmV4dC9lbnYiOiAiMTYuMi42IiwgIkBzd2MvaGVscGVycyI6ICIwLjUuMTUiLCAiYmFzZWxpbmUtYnJvd3Nlci1tYXBwaW5nIjogIl4yLjkuMTkiLCAiY2FuaXVzZS1saXRlIjogIl4xLjAuMzAwMDE1NzkiLCAicG9zdGNzcyI6ICI4LjQuMzEiLCAic3R5bGVkLWpzeCI6ICI1LjEuNiIgfSwgIm9wdGlvbmFsRGVwZW5kZW5jaWVzIjogeyAiQG5leHQvc3djLWRhcndpbi1hcm02NCI6ICIxNi4yLjYiLCAiQG5leHQvc3djLWRhcndpbi14NjQiOiAiMTYuMi42IiwgIkBuZXh0L3N3Yy1saW51eC1hcm02NC1nbnUiOiAiMTYuMi42IiwgIkBuZXh0L3N3Yy1saW51eC1hcm02NC1tdXNsIjogIjE2LjIuNiIsICJAbmV4dC9zd2MtbGludXgteDY0LWdudSI6ICIxNi4yLjYiLCAiQG5leHQvc3djLWxpbnV4LXg2NC1tdXNsIjogIjE2LjIuNiIsICJAbmV4dC9zd2Mtd2luMzItYXJtNjQtbXN2YyI6ICIxNi4yLjYiLCAiQG5leHQvc3djLXdpbjMyLXg2NC1tc3ZjIjogIjE2LjIuNiIsICJzaGFycCI6ICJeMC4zNC41IiB9LCAicGVlckRlcGVuZGVuY2llcyI6IHsgIkBvcGVudGVsZW1ldHJ5L2FwaSI6ICJeMS4xLjAiLCAiQHBsYXl3cmlnaHQvdGVzdCI6ICJeMS41MS4xIiwgImJhYmVsLXBsdWdpbi1yZWFjdC1jb21waWxlciI6ICIqIiwgInJlYWN0IjogIl4xOC4yLjAgfHwgMTkuMC4wLXJjLWRlNjhkMmY0LTIwMjQxMjA0IHx8IF4xOS4wLjAiLCAicmVhY3QtZG9tIjogIl4xOC4yLjAgfHwgMTkuMC4wLXJjLWRlNjhkMmY0LTIwMjQxMjA0IHx8IF4xOS4wLjAiLCAic2FzcyI6ICJeMS4zLjAiIH0sICJvcHRpb25hbFBlZXJzIjogWyJAb3BlbnRlbGVtZXRyeS9hcGkiLCAiQHBsYXl3cmlnaHQvdGVzdCIsICJiYWJlbC1wbHVnaW4tcmVhY3QtY29tcGlsZXIiLCAic2FzcyJdLCAiYmluIjogeyAibmV4dCI6ICJkaXN0L2Jpbi9uZXh0IiB9IH0sICJzaGE1MTItcU9WZ0tKZzErQXQxNU5wZVVQK2VKZ0NIdlRDZ1hzb2d3ZXE4N1JpL0l4N1BrcVFIZzRzZGFYbVNGcUtsZ2FJWEU0a1cwZzI1TEU2OFc4N1VBTmxIdHc9PSJdLAoKICAgICJvYnVnIjogWyJvYnVnQDIuMS4xIiwgIiIsIHt9LCAic2hhNTEyLXVUcUY5TXVQcmFBUStJc25QZjM2NlJHNGNQOVJ0VWk3TUxPMU4zS0VjK3diMGE2eUtwZUwwbG1rMklCMWpZNUtIUEFsVGM2VC9KUmRDL1lxeEhOd2tRPT0iXSwKCiAgICAicGF0aGUiOiBbInBhdGhlQDIuMC4zIiwgIiIsIHt9LCAic2hhNTEyLVdVakdjQXFQMWdRYWNvUWUrT0JKc0ZBN0xkNER5WHVVSWpaNWNjNzVjTEh2SjdkdE5zVHVncGh4SUFEd3NwUytBcmFBVWVQQ0tyU1Z0UExGai9GODh3PT0iXSwKCiAgICAicGljb2NvbG9ycyI6IFsicGljb2NvbG9yc0AxLjEuMSIsICIiLCB7fSwgInNoYTUxMi14Y2VIMnNuaHRiNU05bGlxRHNtRXc1NmxlMzc2bVRaa0VYL2pFYi9SeE5GeWVnTnVsN2VOc2xDWFA5RkRqL0xjdTBYOEtFeU1jZVAybnRwYUhyREVWQT09Il0sCgogICAgInBpY29tYXRjaCI6IFsicGljb21hdGNoQDQuMC40IiwgIiIsIHt9LCAic2hhNTEyLVFQODhCQUt2TWFtLzNOeEg2dmoybzIxUjZNanhaVUFkNm5sd0FTL3BuR3ZOOUlWTG9jTEh4R1lJekZoZzZmVVErNXRoNlA0ZHY0ZVc5algzRFNJajdBPT0iXSwKCiAgICAicG9zdGNzcyI6IFsicG9zdGNzc0A4LjUuMTUiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAibmFub2lkIjogIl4zLjMuMTIiLCAicGljb2NvbG9ycyI6ICJeMS4xLjEiLCAic291cmNlLW1hcC1qcyI6ICJeMS4yLjEiIH0gfSwgInNoYTUxMi1GZlI4c2pkNGVtMlQ2ZmIzSTJNd0FKVTdIV1ZNcjl6YmErZW5tUWVlV0ZmQ2JtK1VPQy8wWDREUzhYdHBVVE13V01HYmpLWVA3eGpmTmVrenlHbUIzQT09Il0sCgogICAgInJlYWN0IjogWyJyZWFjdEAxOS4yLjYiLCAiIiwge30sICJzaGE1MTItc2ZXR0dmYXZpMHhyOFBnMHNWc3lITUFPemlWWUtnUExOclM3aWcraXZNTmIzd2JDQnczS3h0ZmxzR0JBd0QzZ1lRbEUvQUVac1RMZ1RvUnJTQ2piMFE9PSJdLAoKICAgICJyZWFjdC1kYXktcGlja2VyIjogWyJyZWFjdC1kYXktcGlja2VyQDkuMTQuMCIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAZGF0ZS1mbnMvdHoiOiAiXjEuNC4xIiwgIkB0YWJieV9haS9oaWpyaS1jb252ZXJ0ZXIiOiAiMS4wLjUiLCAiZGF0ZS1mbnMiOiAiXjQuMS4wIiwgImRhdGUtZm5zLWphbGFsaSI6ICI0LjEuMC0wIiB9LCAicGVlckRlcGVuZGVuY2llcyI6IHsgInJlYWN0IjogIj49MTYuOC4wIiB9IH0sICJzaGE1MTItdEJhb0RXalB3ZTBNNXBHcnVtNEgwU1I2THlrK0JPOW9IbnA5SmJLcEdLVzJtbHJhTlBnUDlCTWZzZzVwV3B3cnNzQVJtZXFrN1lCbDJvWHV0WlRhSEE9PSJdLAoKICAgICJyZWFjdC1kb20iOiBbInJlYWN0LWRvbUAxOS4yLjYiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAic2NoZWR1bGVyIjogIl4wLjI3LjAiIH0sICJwZWVyRGVwZW5kZW5jaWVzIjogeyAicmVhY3QiOiAiXjE5LjIuNiIgfSB9LCAic2hhNTEyLTBwck1JK2h2QmJQanNXbnhETHhsQ0d5TThQTjZVdVdqRVVDWW1aaE82N3hJVjlYYXNhL3IvdkRucStYeXE0TG8yN2c4UVNiTzVZekFSdTBEMVNwczNnPT0iXSwKCiAgICAicmVxdWlyZS1kaXJlY3RvcnkiOiBbInJlcXVpcmUtZGlyZWN0b3J5QDIuMS4xIiwgIiIsIHt9LCAic2hhNTEyLWZHeEVJNyt3c0c5eHJ2ZGpzcmxtTDIyT01UVGlIUndBTXJvaUVlTWdxOGd6b0xDL1BRcjdSc1JEU1RMVWcvYlpBWnRGK1RWSWtIYzYvNFJJS3J1aStRPT0iXSwKCiAgICAicmVzZWxlY3QiOiBbInJlc2VsZWN0QDUuMi4wIiwgIiIsIHt9LCAic2hhNTEyLUFnWjNVT1ptM1luZGZySjRPWWpnclQ3Ym1DbS8xaXFranZFZkgvb1lqemg2UEQycXc0UXVUM2pqblhJcnBkdDRNVHBNWGNsTVQzbFhibVJZK1hSYWt3PT0iXSwKCiAgICAicm9sbGRvd24iOiBbInJvbGxkb3duQDEuMC4xIiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIkBveGMtcHJvamVjdC90eXBlcyI6ICI9MC4xMzAuMCIsICJAcm9sbGRvd24vcGx1Z2ludXRpbHMiOiAiXjEuMC4wIiB9LCAib3B0aW9uYWxEZXBlbmRlbmNpZXMiOiB7ICJAcm9sbGRvd24vYmluZGluZy1hbmRyb2lkLWFybTY0IjogIjEuMC4xIiwgIkByb2xsZG93bi9iaW5kaW5nLWRhcndpbi1hcm02NCI6ICIxLjAuMSIsICJAcm9sbGRvd24vYmluZGluZy1kYXJ3aW4teDY0IjogIjEuMC4xIiwgIkByb2xsZG93bi9iaW5kaW5nLWZyZWVic2QteDY0IjogIjEuMC4xIiwgIkByb2xsZG93bi9iaW5kaW5nLWxpbnV4LWFybS1nbnVlYWJpaGYiOiAiMS4wLjEiLCAiQHJvbGxkb3duL2JpbmRpbmctbGludXgtYXJtNjQtZ251IjogIjEuMC4xIiwgIkByb2xsZG93bi9iaW5kaW5nLWxpbnV4LWFybTY0LW11c2wiOiAiMS4wLjEiLCAiQHJvbGxkb3duL2JpbmRpbmctbGludXgtcHBjNjQtZ251IjogIjEuMC4xIiwgIkByb2xsZG93bi9iaW5kaW5nLWxpbnV4LXMzOTB4LWdudSI6ICIxLjAuMSIsICJAcm9sbGRvd24vYmluZGluZy1saW51eC14NjQtZ251IjogIjEuMC4xIiwgIkByb2xsZG93bi9iaW5kaW5nLWxpbnV4LXg2NC1tdXNsIjogIjEuMC4xIiwgIkByb2xsZG93bi9iaW5kaW5nLW9wZW5oYXJtb255LWFybTY0IjogIjEuMC4xIiwgIkByb2xsZG93bi9iaW5kaW5nLXdhc20zMi13YXNpIjogIjEuMC4xIiwgIkByb2xsZG93bi9iaW5kaW5nLXdpbjMyLWFybTY0LW1zdmMiOiAiMS4wLjEiLCAiQHJvbGxkb3duL2JpbmRpbmctd2luMzIteDY0LW1zdmMiOiAiMS4wLjEiIH0sICJiaW4iOiB7ICJyb2xsZG93biI6ICJiaW4vY2xpLm1qcyIgfSB9LCAic2hhNTEyLVgwS1FIbGpObkVrV05xcWl6OXpKckd1bmgxQjBIZ094TFh2bkZwQ09jYWR6Y3k1cW9oWjN0cU1FVWcwMHZuY29Sb3ZYdUszWnFDVDlLbm5Lem9JbkZRPT0iXSwKCiAgICAicnhqcyI6IFsicnhqc0A3LjguMiIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJ0c2xpYiI6ICJeMi4xLjAiIH0gfSwgInNoYTUxMi1kaEtmOTAzVS9QUVpZNmJvTk50QUdkV2JHODVXQWJqVC8xeFlvWklDN0ZBWTB5V2FwT0JRVnNWckRsNThXODYvL2UxVnBNTkJ0UlY0TWFYZmRNeVNGQT09Il0sCgogICAgInNjaGVkdWxlciI6IFsic2NoZWR1bGVyQDAuMjcuMCIsICIiLCB7fSwgInNoYTUxMi1lTnYrV3JWYkt1MWYzdmJZSlQveHRpRjVzeUE1SFBJTXRmOUlnWS9uS2cwc1dxekFVRXZxWS94bTdPY1pjL3FhZkx4L2lPOUZnT21lU0FwNHY1dGkvUT09Il0sCgogICAgInNlbXZlciI6IFsic2VtdmVyQDcuOC4wIiwgIiIsIHsgImJpbiI6IHsgInNlbXZlciI6ICJiaW4vc2VtdmVyLmpzIiB9IH0sICJzaGE1MTItQWNNN2RWLzV1bDRFZWtvUTI5QWdtNXZyaThKTnFSeWozOW8wcXBYNnZERjJHWnJ0dXRabDVSd2dEMVhuWmppVEFmbmNzSmhNSTQ4UVFIM3NOODdZTkE9PSJdLAoKICAgICJzaGFycCI6IFsic2hhcnBAMC4zNC41IiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIkBpbWcvY29sb3VyIjogIl4xLjAuMCIsICJkZXRlY3QtbGliYyI6ICJeMi4xLjIiLCAic2VtdmVyIjogIl43LjcuMyIgfSwgIm9wdGlvbmFsRGVwZW5kZW5jaWVzIjogeyAiQGltZy9zaGFycC1kYXJ3aW4tYXJtNjQiOiAiMC4zNC41IiwgIkBpbWcvc2hhcnAtZGFyd2luLXg2NCI6ICIwLjM0LjUiLCAiQGltZy9zaGFycC1saWJ2aXBzLWRhcndpbi1hcm02NCI6ICIxLjIuNCIsICJAaW1nL3NoYXJwLWxpYnZpcHMtZGFyd2luLXg2NCI6ICIxLjIuNCIsICJAaW1nL3NoYXJwLWxpYnZpcHMtbGludXgtYXJtIjogIjEuMi40IiwgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC1hcm02NCI6ICIxLjIuNCIsICJAaW1nL3NoYXJwLWxpYnZpcHMtbGludXgtcHBjNjQiOiAiMS4yLjQiLCAiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4LXJpc2N2NjQiOiAiMS4yLjQiLCAiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4LXMzOTB4IjogIjEuMi40IiwgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC14NjQiOiAiMS4yLjQiLCAiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4bXVzbC1hcm02NCI6ICIxLjIuNCIsICJAaW1nL3NoYXJwLWxpYnZpcHMtbGludXhtdXNsLXg2NCI6ICIxLjIuNCIsICJAaW1nL3NoYXJwLWxpbnV4LWFybSI6ICIwLjM0LjUiLCAiQGltZy9zaGFycC1saW51eC1hcm02NCI6ICIwLjM0LjUiLCAiQGltZy9zaGFycC1saW51eC1wcGM2NCI6ICIwLjM0LjUiLCAiQGltZy9zaGFycC1saW51eC1yaXNjdjY0IjogIjAuMzQuNSIsICJAaW1nL3NoYXJwLWxpbnV4LXMzOTB4IjogIjAuMzQuNSIsICJAaW1nL3NoYXJwLWxpbnV4LXg2NCI6ICIwLjM0LjUiLCAiQGltZy9zaGFycC1saW51eG11c2wtYXJtNjQiOiAiMC4zNC41IiwgIkBpbWcvc2hhcnAtbGludXhtdXNsLXg2NCI6ICIwLjM0LjUiLCAiQGltZy9zaGFycC13YXNtMzIiOiAiMC4zNC41IiwgIkBpbWcvc2hhcnAtd2luMzItYXJtNjQiOiAiMC4zNC41IiwgIkBpbWcvc2hhcnAtd2luMzItaWEzMiI6ICIwLjM0LjUiLCAiQGltZy9zaGFycC13aW4zMi14NjQiOiAiMC4zNC41IiB9IH0sICJzaGE1MTItT3U5STVGdDlXTmNDYlhyVTljTWdQQmNDSzhMaXdMcWNieXdXM3Q0b0RWMzduMXB6cHVOTHNZaUFWOGVPRG5qYnRRbFNEd1oyY1VFZVF6NEU1NEhsdGc9PSJdLAoKICAgICJzaGVsbC1xdW90ZSI6IFsic2hlbGwtcXVvdGVAMS44LjMiLCAiIiwge30sICJzaGE1MTItT2JtbklGNGhYTmcxQnFobkhtZ2JERVRGOGRMUENnZ1pXQmprUWZoWnBic3pabll1cjVEVWxqVGNDSGlpNUxDM0o1RTB5ZU8vMUxJTXlIK1V2SFFneXc9PSJdLAoKICAgICJzaWdpbmZvIjogWyJzaWdpbmZvQDIuMC4wIiwgIiIsIHt9LCAic2hhNTEyLXlieDBXTzEvOGJTQkxFV1hadkVkN2dNVzNTbjNKRmxXM1R2WDFuUkViRExSTlFOYWVOTjhXSzBtZUJ3UGRBYU9JN1R0UlJSSm4vRXMxemhyckNIdTdnPT0iXSwKCiAgICAic291cmNlLW1hcC1qcyI6IFsic291cmNlLW1hcC1qc0AxLjIuMSIsICIiLCB7fSwgInNoYTUxMi1VWFdNS2hMT3dWS2I3MjhJVXRRUFh4ZllVK3VzZHlidFVySy84dUdFOENRTXZyaE9wd3Z6REJ3ajBRaFNMN01RYzd2SXNJU0JHOFZROCtJRFF4cGZRQT09Il0sCgogICAgInN0YWNrYmFjayI6IFsic3RhY2tiYWNrQDAuMC4yIiwgIiIsIHt9LCAic2hhNTEyLTFYTUpFNWZRbzFqR0g2WS83ZWJud1BPQkVrSUVuVDRRRjMyZDVSMStWWGRYdmVNMElCTUp0OHpmYXhYMVAzUWhWd3JZZSs1NzYramtBTnRTUzJtQmJ3PT0iXSwKCiAgICAic3RkLWVudiI6IFsic3RkLWVudkA0LjEuMCIsICIiLCB7fSwgInNoYTUxMi1ScTd5YmNYMlJ1QzU1cjlvYVBWRVc3L3h1M3RqOHU0R2VCWUhCV0N5Y2hGdHpNSXI4NkE3ZTNQUEVCUFQzN3NIU3RLWDMrVGlYL0ZyL0FDbUpMVmxMUT09Il0sCgogICAgInN0cmluZy13aWR0aCI6IFsic3RyaW5nLXdpZHRoQDQuMi4zIiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgImVtb2ppLXJlZ2V4IjogIl44LjAuMCIsICJpcy1mdWxsd2lkdGgtY29kZS1wb2ludCI6ICJeMy4wLjAiLCAic3RyaXAtYW5zaSI6ICJeNi4wLjEiIH0gfSwgInNoYTUxMi13S3lRUlFwakowc0lwNjJFclNaZEdzak1KV3NhcDVvUk5paEhodTZHN0pWTy85aklCNlV5ZXZMK3RYdU9xcm5nOGovY3hLVFd5V1V3dlNUcmlpWnovZz09Il0sCgogICAgInN0cmlwLWFuc2kiOiBbInN0cmlwLWFuc2lANi4wLjEiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiYW5zaS1yZWdleCI6ICJeNS4wLjEiIH0gfSwgInNoYTUxMi1ZMzhWUFNIY3FrRnJDcEZuUTl2dVNYbXF1dXY1b1hPS3BHZVQ2YUdycjNvM0djOUFsVmE2SkJmVVNPQ25ieEdHWkYrLzBvb0k3S3JQdVVTenRVZFU1QT09Il0sCgogICAgInN0eWxlZC1qc3giOiBbInN0eWxlZC1qc3hANS4xLjYiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiY2xpZW50LW9ubHkiOiAiMC4wLjEiIH0sICJwZWVyRGVwZW5kZW5jaWVzIjogeyAicmVhY3QiOiAiPj0gMTYuOC4wIHx8IDE3LngueCB8fCBeMTguMC4wLTAgfHwgXjE5LjAuMC0wIiB9IH0sICJzaGE1MTItcVNWeURUZU1vdGR2UVlvSFdMTkd3UkZKSEMraStadmRCUllvc09GZ0MrV2cxdng0ZnJOMi9SRy9OQTdTWXFxdktOTGYzOVAyTFNSQTJwdTZuMFhZWkE9PSJdLAoKICAgICJzdXBwb3J0cy1jb2xvciI6IFsic3VwcG9ydHMtY29sb3JAOC4xLjEiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiaGFzLWZsYWciOiAiXjQuMC4wIiB9IH0sICJzaGE1MTItTXBVRU4yT29kdFV6eHZLUWw3MmNVRjdSUTVFaUhzR3ZTc1ZHMGlhOWM1UmJXR0wyQ0k0QzdFcFBTOFVUQklwbG5selppTnVWNTZ3K0Z1Tnh5M3R5MlE9PSJdLAoKICAgICJ0YWlsd2luZC1tZXJnZSI6IFsidGFpbHdpbmQtbWVyZ2VAMy42LjAiLCAiIiwge30sICJzaGE1MTItdXhMN3FBVlFyaXFSUVBBeUszcGo2NlZxc2tXcW9aMzdQVzk0andPVHdOZnEvejlveXUxVitlcXJacXRSMitmQ2lYZFlPWmUvTW9kdDhHdHZxTnp1K3c9PSJdLAoKICAgICJ0YWlsd2luZGNzcyI6IFsidGFpbHdpbmRjc3NANC4zLjAiLCAiIiwge30sICJzaGE1MTIteTZueE1HQjFuTVc5UjZrOTZlNWdkSUZ6Y2ZML2dUSlJOYXFHZXMxWXZrTG5QVlh6V2dicUZGMnlMQzBUOEc3NzRuMjRjeDNQZThYcktvbmlDT0FIK1E9PSJdLAoKICAgICJ0YXBhYmxlIjogWyJ0YXBhYmxlQDIuMy4zIiwgIiIsIHt9LCAic2hhNTEyLXV4Yy96cHFGZzZ4N0M4dk9FN2xoNkxiZGE4ZUVMOXptVm0vUExlVFBCUmhoMXhDZ2RXYVErSjFDVWllR3BJZm0ySGR0c1VwUnYrSHNoaWFzQk1jYzZBPT0iXSwKCiAgICAidGlueWJlbmNoIjogWyJ0aW55YmVuY2hAMi45LjAiLCAiIiwge30sICJzaGE1MTItMCtEVXZxV01WYWxMbWhhNmxyNGtEOGlBTUsxSHpWMC9hS25DdFdiOXY5NjQxVG5QL01GYjdQYzJieG94UWpUWEFFcnJ5WFZnVU9mdjJZcU5sbHFHZWc9PSJdLAoKICAgICJ0aW55ZXhlYyI6IFsidGlueWV4ZWNAMS4xLjIiLCAiIiwge30sICJzaGE1MTItZEFxU3FFL1JhYnBCS0k4K2gyNkdmTHE2VmIzSlZYczMwWFlRamRNamFqL2MydFM4SVlZTWJJelA1OTlLdFJqN2M1Ny93WUFwYjNRamdSZ1htckN1a0E9PSJdLAoKICAgICJ0aW55Z2xvYmJ5IjogWyJ0aW55Z2xvYmJ5QDAuMi4xNiIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJmZGlyIjogIl42LjUuMCIsICJwaWNvbWF0Y2giOiAiXjQuMC40IiB9IH0sICJzaGE1MTItcG45OVZob0FDWVI4bkZIaHhxaXgrdXZzYlhpbmVBYXNXbTVvalhvTjh4RXdLNUtkMy9UcmhObjF3Qnl1RDUyVXhXUkx5OHB1K2tSTW5pRWk2RXE5Wmc9PSJdLAoKICAgICJ0aW55cmFpbmJvdyI6IFsidGlueXJhaW5ib3dAMy4xLjAiLCAiIiwge30sICJzaGE1MTItQmYrSUxtQmdyZXRVcmRKeHpYTTBTZ1hMWjNYZmlhVXVPai9JS1FIdVRYaXArMDVYbit1eUVZZFZnMGtZRGlwVEJjTHJDVnlVekFQejdRbUFyYjBtbXc9PSJdLAoKICAgICJ0cmVlLWtpbGwiOiBbInRyZWUta2lsbEAxLjIuMiIsICIiLCB7ICJiaW4iOiB7ICJ0cmVlLWtpbGwiOiAiY2xpLmpzIiB9IH0sICJzaGE1MTItTDBPcnBpOHFHcFJHLy9OZCtIOTB2RkIrM2lIbnVlMXpTU0dtTk9PQ2gxR0xKN3JVS1Z3VjJIdmlqcGhHUVMyVW1oVVpld1M5Vmd2eFlJZGdyK2ZHMUE9PSJdLAoKICAgICJ0c2xpYiI6IFsidHNsaWJAMi44LjEiLCAiIiwge30sICJzaGE1MTItb0pGdTk0SFFiK0tWZHVTVVFMN3ducG1xbmZtTHNPQS9uQWg2YjZFSDB3Q0VvSzAvbVBlWFU2YzN3S0RWODNNa091SFBSSHRTWEtLVTk5SUJhelMvMnc9PSJdLAoKICAgICJ0eXBlc2NyaXB0IjogWyJ0eXBlc2NyaXB0QDUuOS4zIiwgIiIsIHsgImJpbiI6IHsgInRzYyI6ICJiaW4vdHNjIiwgInRzc2VydmVyIjogImJpbi90c3NlcnZlciIgfSB9LCAic2hhNTEyLWpsMXZaelBEaW5McjllVXQzSi90N1Y2RmdORXc5UWp2QlBkeXN6OUtmUURENDFmUXJDMlk0dktRZGlhVXBGVDRiWGxiMVJIaExwcDh3dG02TTVUZ1N3PT0iXSwKCiAgICAidW5kaWNpLXR5cGVzIjogWyJ1bmRpY2ktdHlwZXNANi4yMS4wIiwgIiIsIHt9LCAic2hhNTEyLWl3RFpxZzBRQUdyZzlSYXY1SDRuME02NGMzbWtSNTljSjZ3UXArN0M0bkkwZ3NtRXhhZWRhWUxOTzQ0ZVQ0QXRCQndqYlRpR1BNbHQyTWQwVDlIOUpRPT0iXSwKCiAgICAidXNlLXN5bmMtZXh0ZXJuYWwtc3RvcmUiOiBbInVzZS1zeW5jLWV4dGVybmFsLXN0b3JlQDEuNi4wIiwgIiIsIHsgInBlZXJEZXBlbmRlbmNpZXMiOiB7ICJyZWFjdCI6ICJeMTYuOC4wIHx8IF4xNy4wLjAgfHwgXjE4LjAuMCB8fCBeMTkuMC4wIiB9IH0sICJzaGE1MTItUHA2R1N3R1AvTnJQSXJ4VkZBSWtPUWV5dzhsRmVuT0hpalFXa1VUckR2ckY0QUxxeWxQMkMvS0NrZVM5ZHBVTTNLdllSUWhuYTV2dDdJTDk1K1pROXc9PSJdLAoKICAgICJ2aXRlIjogWyJ2aXRlQDguMC4xMyIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJsaWdodG5pbmdjc3MiOiAiXjEuMzIuMCIsICJwaWNvbWF0Y2giOiAiXjQuMC40IiwgInBvc3Rjc3MiOiAiXjguNS4xNCIsICJyb2xsZG93biI6ICIxLjAuMSIsICJ0aW55Z2xvYmJ5IjogIl4wLjIuMTYiIH0sICJvcHRpb25hbERlcGVuZGVuY2llcyI6IHsgImZzZXZlbnRzIjogIn4yLjMuMyIgfSwgInBlZXJEZXBlbmRlbmNpZXMiOiB7ICJAdHlwZXMvbm9kZSI6ICJeMjAuMTkuMCB8fCA+PTIyLjEyLjAiLCAiQHZpdGVqcy9kZXZ0b29scyI6ICJeMC4xLjE4IiwgImVzYnVpbGQiOiAiXjAuMjcuMCB8fCBeMC4yOC4wIiwgImppdGkiOiAiPj0xLjIxLjAiLCAibGVzcyI6ICJeNC4wLjAiLCAic2FzcyI6ICJeMS43MC4wIiwgInNhc3MtZW1iZWRkZWQiOiAiXjEuNzAuMCIsICJzdHlsdXMiOiAiPj0wLjU0LjgiLCAic3VnYXJzcyI6ICJeNS4wLjAiLCAidGVyc2VyIjogIl41LjE2LjAiLCAidHN4IjogIl40LjguMSIsICJ5YW1sIjogIl4yLjQuMiIgfSwgIm9wdGlvbmFsUGVlcnMiOiBbIkB0eXBlcy9ub2RlIiwgIkB2aXRlanMvZGV2dG9vbHMiLCAiZXNidWlsZCIsICJqaXRpIiwgImxlc3MiLCAic2FzcyIsICJzYXNzLWVtYmVkZGVkIiwgInN0eWx1cyIsICJzdWdhcnNzIiwgInRlcnNlciIsICJ0c3giLCAieWFtbCJdLCAiYmluIjogeyAidml0ZSI6ICJiaW4vdml0ZS5qcyIgfSB9LCAic2hhNTEyLU1GdGpCWWd6bVN4bWdBNFJBZmpJeVhXcEdlMW9BTG5qZ1VUenpWN1FMeC9US3hDemp0TUg2RmQ5L2VWSys1RmcxcU5vejVWQXdzbU1zL05vZnJtSnZ3PT0iXSwKCiAgICAidml0ZXN0IjogWyJ2aXRlc3RANC4xLjYiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiQHZpdGVzdC9leHBlY3QiOiAiNC4xLjYiLCAiQHZpdGVzdC9tb2NrZXIiOiAiNC4xLjYiLCAiQHZpdGVzdC9wcmV0dHktZm9ybWF0IjogIjQuMS42IiwgIkB2aXRlc3QvcnVubmVyIjogIjQuMS42IiwgIkB2aXRlc3Qvc25hcHNob3QiOiAiNC4xLjYiLCAiQHZpdGVzdC9zcHkiOiAiNC4xLjYiLCAiQHZpdGVzdC91dGlscyI6ICI0LjEuNiIsICJlcy1tb2R1bGUtbGV4ZXIiOiAiXjIuMC4wIiwgImV4cGVjdC10eXBlIjogIl4xLjMuMCIsICJtYWdpYy1zdHJpbmciOiAiXjAuMzAuMjEiLCAib2J1ZyI6ICJeMi4xLjEiLCAicGF0aGUiOiAiXjIuMC4zIiwgInBpY29tYXRjaCI6ICJeNC4wLjMiLCAic3RkLWVudiI6ICJeNC4wLjAtcmMuMSIsICJ0aW55YmVuY2giOiAiXjIuOS4wIiwgInRpbnlleGVjIjogIl4xLjAuMiIsICJ0aW55Z2xvYmJ5IjogIl4wLjIuMTUiLCAidGlueXJhaW5ib3ciOiAiXjMuMS4wIiwgInZpdGUiOiAiXjYuMC4wIHx8IF43LjAuMCB8fCBeOC4wLjAiLCAid2h5LWlzLW5vZGUtcnVubmluZyI6ICJeMi4zLjAiIH0sICJwZWVyRGVwZW5kZW5jaWVzIjogeyAiQGVkZ2UtcnVudGltZS92bSI6ICIqIiwgIkBvcGVudGVsZW1ldHJ5L2FwaSI6ICJeMS45LjAiLCAiQHR5cGVzL25vZGUiOiAiXjIwLjAuMCB8fCBeMjIuMC4wIHx8ID49MjQuMC4wIiwgIkB2aXRlc3QvYnJvd3Nlci1wbGF5d3JpZ2h0IjogIjQuMS42IiwgIkB2aXRlc3QvYnJvd3Nlci1wcmV2aWV3IjogIjQuMS42IiwgIkB2aXRlc3QvYnJvd3Nlci13ZWJkcml2ZXJpbyI6ICI0LjEuNiIsICJAdml0ZXN0L2NvdmVyYWdlLWlzdGFuYnVsIjogIjQuMS42IiwgIkB2aXRlc3QvY292ZXJhZ2UtdjgiOiAiNC4xLjYiLCAiQHZpdGVzdC91aSI6ICI0LjEuNiIsICJoYXBweS1kb20iOiAiKiIsICJqc2RvbSI6ICIqIiB9LCAib3B0aW9uYWxQZWVycyI6IFsiQGVkZ2UtcnVudGltZS92bSIsICJAb3BlbnRlbGVtZXRyeS9hcGkiLCAiQHR5cGVzL25vZGUiLCAiQHZpdGVzdC9icm93c2VyLXBsYXl3cmlnaHQiLCAiQHZpdGVzdC9icm93c2VyLXByZXZpZXciLCAiQHZpdGVzdC9icm93c2VyLXdlYmRyaXZlcmlvIiwgIkB2aXRlc3QvY292ZXJhZ2UtaXN0YW5idWwiLCAiQHZpdGVzdC9jb3ZlcmFnZS12OCIsICJAdml0ZXN0L3VpIiwgImhhcHB5LWRvbSIsICJqc2RvbSJdLCAiYmluIjogeyAidml0ZXN0IjogInZpdGVzdC5tanMiIH0gfSwgInNoYTUxMi02bHZqYlMzcDliNENyZENtZ3V6YmgyLzR1b1hoR0UycTcxUjRPWDVzcUY5UjFibzlYZDZmR3JNQWZ2cDV3bkN6bEJuRlZkQ09wNm9udVRRVmJvOGlVUT09Il0sCgogICAgIndoeS1pcy1ub2RlLXJ1bm5pbmciOiBbIndoeS1pcy1ub2RlLXJ1bm5pbmdAMi4zLjAiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAic2lnaW5mbyI6ICJeMi4wLjAiLCAic3RhY2tiYWNrIjogIjAuMC4yIiB9LCAiYmluIjogeyAid2h5LWlzLW5vZGUtcnVubmluZyI6ICJjbGkuanMiIH0gfSwgInNoYTUxMi1oVXJtYVdCZFZEY3h2WXFueWgwOXp1bkt6Uk9XamJaVGlOeThkQkVqa1M3ZWhFRFFpYlhKN1h2bG10Ynd1VGNsVWlJeU4rQ3lYUUQ0Vm1rbzhmTm04dz09Il0sCgogICAgIndyYXAtYW5zaSI6IFsid3JhcC1hbnNpQDcuMC4wIiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgImFuc2ktc3R5bGVzIjogIl40LjAuMCIsICJzdHJpbmctd2lkdGgiOiAiXjQuMS4wIiwgInN0cmlwLWFuc2kiOiAiXjYuMC4wIiB9IH0sICJzaGE1MTItWVZHSWoya2FtTFNUeHc2TnNaam9CeGZTd3NuMHljZGVzbWM0cCtRMjFjNXpQdVoxcGwrTmZ4VmR4UHRkSHZtTlZPUTZYU1lHNEFVdHl0L0ZpN0QxNlE9PSJdLAoKICAgICJ5MThuIjogWyJ5MThuQDUuMC44IiwgIiIsIHt9LCAic2hhNTEyLTBwZkZ6ZWdlRFdKSEpJQW1UTFJQMkR3SGpkRjVzN2pvOXR1enRkUXhBaElOQ2R2UyszbkdJTnFQZDAwQXBocUpSLzBMaEFOVVM2Lys3U0NiOThZT2ZBPT0iXSwKCiAgICAieWFyZ3MiOiBbInlhcmdzQDE3LjcuMiIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJjbGl1aSI6ICJeOC4wLjEiLCAiZXNjYWxhZGUiOiAiXjMuMS4xIiwgImdldC1jYWxsZXItZmlsZSI6ICJeMi4wLjUiLCAicmVxdWlyZS1kaXJlY3RvcnkiOiAiXjIuMS4xIiwgInN0cmluZy13aWR0aCI6ICJeNC4yLjMiLCAieTE4biI6ICJeNS4wLjUiLCAieWFyZ3MtcGFyc2VyIjogIl4yMS4xLjEiIH0gfSwgInNoYTUxMi03ZFN6elJRKytDS25OSS9rcktuWVJWN0pLS1BVWE1FaDYxc29hSEtnOW1yV0VoekZXaEZueFB4R2wrNjljRDFPdTYzQzEzTlVQQ25tSWNydnFDdU02dz09Il0sCgogICAgInlhcmdzLXBhcnNlciI6IFsieWFyZ3MtcGFyc2VyQDIxLjEuMSIsICIiLCB7fSwgInNoYTUxMi10VnBzSlc3RGRqZWNBaUZwYklCMWUzcXhJUXNFNk5vUGM1L2VUZHJiYklDNGgwTFZzV2hub2EzZyttMkhjbEJJdWpIenN4WjRWSlZBK0dVdWMyL0xCdz09Il0sCgogICAgInpvZCI6IFsiem9kQDQuNC4zIiwgIiIsIHt9LCAic2hhNTEyLXl0RU5GaklKRmwyVXdZZ2xkZTJqY2hXMkh3bTRHSkZMRGlTWFdkVHJKUUJJTjlGY3lwN240RGh4SkVpV05BSk1WMS9CcVdmVy9ra2c3MVVEY0hKeVRRPT0iXSwKCiAgICAiQHRhaWx3aW5kY3NzL294aWRlLXdhc20zMi13YXNpL0BlbW5hcGkvY29yZSI6IFsiQGVtbmFwaS9jb3JlQDEuMTAuMCIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAZW1uYXBpL3dhc2ktdGhyZWFkcyI6ICIxLjIuMSIsICJ0c2xpYiI6ICJeMi40LjAiIH0sICJidW5kbGVkIjogdHJ1ZSB9LCAic2hhNTEyLXlxNk9rSjRwODJDQWZQbDB1OW1RZWJRSEtQSmtZN1dySXVrMjA1Y1RZblllK2syWjhZQmgxMUZyYlJHL0g2aWhpcnFjYWNPZ2wyQklPOG95TVFMZVh3PT0iXSwKCiAgICAiQHRhaWx3aW5kY3NzL294aWRlLXdhc20zMi13YXNpL0BlbW5hcGkvcnVudGltZSI6IFsiQGVtbmFwaS9ydW50aW1lQDEuMTAuMCIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJ0c2xpYiI6ICJeMi40LjAiIH0sICJidW5kbGVkIjogdHJ1ZSB9LCAic2hhNTEyLWV3dllsazg2eFVvR0kwelFSTnEvbUMrMTZSMVFlRGxLUXkyMUtpM29TWVhOZ0xiNDVHVjFQNkEwTSsvczZueUN1TkRxZTVWcGFZODRCelhHd1Zid0ZBPT0iXSwKCiAgICAiQHRhaWx3aW5kY3NzL294aWRlLXdhc20zMi13YXNpL0BlbW5hcGkvd2FzaS10aHJlYWRzIjogWyJAZW1uYXBpL3dhc2ktdGhyZWFkc0AxLjIuMSIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJ0c2xpYiI6ICJeMi40LjAiIH0sICJidW5kbGVkIjogdHJ1ZSB9LCAic2hhNTEyLXVUSUk3T1lGKy9NZXMvTXJjSU9ZcDV5T3RTTUxCV1NJb0xQcGNnd2lwb2lLYmxpNmszMjJ0Y29Gc3hvSUl4UERxVzAxU1FHQWdrbzRFelppMkJOdjJ3PT0iXSwKCiAgICAiQHRhaWx3aW5kY3NzL294aWRlLXdhc20zMi13YXNpL0BuYXBpLXJzL3dhc20tcnVudGltZSI6IFsiQG5hcGktcnMvd2FzbS1ydW50aW1lQDEuMS40IiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIkB0eWJ5cy93YXNtLXV0aWwiOiAiXjAuMTAuMSIgfSwgInBlZXJEZXBlbmRlbmNpZXMiOiB7ICJAZW1uYXBpL2NvcmUiOiAiXjEuNy4xIiwgIkBlbW5hcGkvcnVudGltZSI6ICJeMS43LjEiIH0sICJidW5kbGVkIjogdHJ1ZSB9LCAic2hhNTEyLTNOUU5OZ0ExWVNsSmIva01IMWlsZEFTUDlIVzcvN2tZblJJMnN6V0phb2ZhUzFoV21iR0k0SCtkMysyMmFHelhYTjlJSituK0dpRlZjR2lwSlAxOG93PT0iXSwKCiAgICAiQHRhaWx3aW5kY3NzL294aWRlLXdhc20zMi13YXNpL0B0eWJ5cy93YXNtLXV0aWwiOiBbIkB0eWJ5cy93YXNtLXV0aWxAMC4xMC4yIiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgInRzbGliIjogIl4yLjQuMCIgfSwgImJ1bmRsZWQiOiB0cnVlIH0sICJzaGE1MTItUm9CdkoyWDB3dUtsV0ZJanJ3ZmZHdzFJcVpIS1FxekljaEthYWRaWmZuTnBzQVlwMm1NMGgzNkp0UENqTkRBSEdnWWV6LzE1dU1CcGZHd2NoaGlNZ2c9PSJdLAoKICAgICJAdGFpbHdpbmRjc3Mvb3hpZGUtd2FzbTMyLXdhc2kvdHNsaWIiOiBbInRzbGliQDIuOC4xIiwgIiIsIHsgImJ1bmRsZWQiOiB0cnVlIH0sICJzaGE1MTItb0pGdTk0SFFiK0tWZHVTVVFMN3ducG1xbmZtTHNPQS9uQWg2YjZFSDB3Q0VvSzAvbVBlWFU2YzN3S0RWODNNa091SFBSSHRTWEtLVTk5SUJhelMvMnc9PSJdLAoKICAgICJjaGFsay9zdXBwb3J0cy1jb2xvciI6IFsic3VwcG9ydHMtY29sb3JANy4yLjAiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiaGFzLWZsYWciOiAiXjQuMC4wIiB9IH0sICJzaGE1MTItcXBDQXZSbDlzdHVPSHZlS3NuN0huY0pSdnY1MDFxSWFjS3pRbE8vK0x3eGM5KzBxMndMeXY0RGZ2dDgwL0RQbjJwcU9Cc0pkRGlvZ1hHUjkrT3Z3Unc9PSJdLAoKICAgICJuZXh0L3Bvc3Rjc3MiOiBbInBvc3Rjc3NAOC40LjMxIiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIm5hbm9pZCI6ICJeMy4zLjYiLCAicGljb2NvbG9ycyI6ICJeMS4wLjAiLCAic291cmNlLW1hcC1qcyI6ICJeMS4wLjIiIH0gfSwgInNoYTUxMi1QUzA4SWJvaWE5bXRzLzJ5Z1YzZUxwWTVnaG5VY2ZMVi9FWFRPVzFFMnFZeEpLR0dCVXROak43NkZZSG5NczM2Um1BUm40MWJDMEFabW4rclIwT1ZwUT09Il0sCiAgfQp9Cg==" }, { "path": ".env.example", "kind": "text", "content": "# Your tenant public key. Get it from the desk's Developers tab.\n# `cpk_live_*` / `cpk_test_*` keys auto-route same-origin requests\n# to hosted Cimplify in both dev and prod. Anything else (`mock-dev`,\n# empty) falls back to the local `cimplify dev` mock.\nNEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev\n\n# Optional. Forces a fixed canonical URL for sitemap.xml, robots.txt,\n# OpenGraph, and llms.txt. Leave unset and the storefront derives the\n# canonical from the request `Host` header automatically.\n# NEXT_PUBLIC_SITE_URL=\n" }], "storefront-services": [{ "path": "vitest.config.ts", "kind": "text", "content": 'import { defineConfig } from "vitest/config";\n\nexport default defineConfig({\n test: {\n environment: "node",\n include: ["__tests__/**/*.test.ts"],\n globals: false,\n },\n});\n' }, { "path": "__tests__/cart-flow.test.ts", "kind": "text", "content": 'import { createCartFlowSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateCartFlowSuite({ seed: brand.mock.seed, businessId: brand.mock.businessId });\n' }, { "path": "__tests__/brand.test.ts", "kind": "text", "content": 'import { createBrandSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateBrandSuite({ brand });\n' }, { "path": "__tests__/contract.test.ts", "kind": "text", "content": 'import { createContractSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateContractSuite({ seed: brand.mock.seed });\n' }, { "path": "AGENTS.md", "kind": "text", "content": '# AGENTS.md \u2014 Services / spa / wellness storefront template\n\nIf you are an AI agent (Claude, Cursor, Aider, devin, \u2026) working on this storefront, **start here.**\n\n## TL;DR for rebranding\n\n1. **Edit `lib/brand.ts`** \u2014 every visible string lives here.\n2. **Edit `app/globals.css`** \u2014 `@theme { \u2026 }` for palette + radius.\n3. **Edit `.env.local`** \u2014 set `NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY` (the only required var).\n## Aesthetic\n\n- **Fraunces serif + Inter** \u2014 warm, slightly editorial headings.\n- **Sand + sage palette** \u2014 warm cream background, deep sage primary.\n- **Generous radius**: `1rem` \u2014 calm, soft.\n- Lower-contrast than retail / fashion.\n- Schema.org `@type` is `BeautySalon`.\n\n## Page surface\n\n```\napp/\n page.tsx Home \u2014 hero ("Now booking"), service strips,\n category grid, newsletter\n shop/page.tsx Treatment menu (SDK <CataloguePage/>)\n search/page.tsx Search\n collections/[slug]/page.tsx Collection landing\n categories/[slug]/page.tsx Category landing (e.g. memberships, packages)\n\n book/page.tsx \u2B50 Services-specific: full booking widget\n (treatment + 14-day date + 30-min slot grid)\n \u2192 adds to cart with slot as note \u2192 /checkout\n\n cart/page.tsx, checkout/page.tsx, orders/[id]/page.tsx\n\n account/page.tsx <CimplifyAccount /> (iframe)\n account/orders/page.tsx <CimplifyAccount section="orders" />\n account/addresses/page.tsx <CimplifyAccount section="addresses" />\n account/settings/page.tsx <CimplifyAccount section="settings" />\n login/page.tsx, signup/page.tsx redirects \u2192 /account\n\n contact/page.tsx, track-order/page.tsx (find a booking)\n\n about/page.tsx, faq/page.tsx\n shipping/page.tsx (what to expect at the studio), returns/page.tsx (cancellations),\n accessibility/page.tsx, terms/page.tsx, privacy/page.tsx\n\n sitemap-page/page.tsx, sitemap.ts, robots.ts, llms.txt/route.ts, opensearch.xml/route.ts\n error.tsx, not-found.tsx\n```\n\n## File \u2194 brand-field map\n\n| File | Reads from `brand` |\n|---|---|\n| `app/layout.tsx` | identity, contact, socials, BeautySalon JSON-LD |\n| `app/page.tsx` | `brand.hero` ("Now booking") |\n| `app/about/page.tsx` | `brand.about` |\n| `app/faq/page.tsx` | `brand.faq` (booking, contraindications, memberships) |\n| `app/shipping/page.tsx` | `brand.shipping` (what to expect at the studio) |\n| `app/returns/page.tsx` | `brand.returns` (cancellation policy) |\n| `app/accessibility/page.tsx` | `brand.accessibility` |\n| `app/terms/page.tsx`, `app/privacy/page.tsx` | `brand.terms`, `brand.privacy` |\n| `app/contact/page.tsx` | `brand.contactPage`, `brand.contact` |\n| `app/track-order/page.tsx` | `brand.trackOrder` ("find a booking") |\n| `app/account/*/page.tsx` | `brand.account` (iframe owns UI) |\n| `app/book/*` | derives bookable services from product list (`type: "service"`) |\n| `app/llms.txt/route.ts` | `brand.llms`, contact, currency |\n| `components/header.tsx`, `footer.tsx` | `brand.header`, `brand.footer`, `brand.contact`, `brand.socials` |\n\n## Services-specific notes\n\n- **Bookable services are `type: "service"` products.** The `/book` page filters the product list to type=service and treats them as treatments. Their `duration_minutes` is shown on the chip.\n- Cart-item notes carry the booked time slot \u2014 checkout sends them through Cimplify\'s order pipeline. The merchant\'s downstream system reads the note to schedule the appointment.\n- Mock seed: `--seed services` (Serene Spa).\n\n## Known TODOs\n\n- `/book` widget shows every slot as available. For production, wire `useAvailableSlots({ serviceId, date })` from `@cimplify/sdk/react` to filter to genuinely-free windows.\n- Contact + newsletter fake submits.\n\n## Customizing SDK components\n\nFor anything beyond `lib/brand.ts` + `app/globals.css`, lean on the SDK\'s prebuilt components rather than reinvent. **Especially for service scheduling, booking widgets, and add-on selectors** \u2014 the SDK already gets price math, slot matching, and cart payload contracts right. Default to ejecting and restyling:\n\n```bash\ncimplify add cart-drawer\ncimplify add product-page\ncimplify add booking-card\n```\n\nThen edit the local copy. **Don\'t change the cart payload shape** (`scheduled_start`, `scheduled_end`, `staff_id`, `customer_inputs`, etc.) unless you\'re also touching the SDK mock + backend lens. Full ejection rules and the customizer contract are in the SDK-level [`AGENTS.md`](../../AGENTS.md) \u2192 "Don\'t reinvent product customization".\n\n## Quick start\n\n```bash\nbun install\nbun dev\n```\n\nOpen <http://localhost:3000>.\n' }, { "path": "postcss.config.mjs", "kind": "text", "content": 'const config = {\n plugins: {\n "@tailwindcss/postcss": {},\n },\n};\n\nexport default config;\n' }, { "path": "components/cart-pill.tsx", "kind": "text", "content": '"use client";\n\nimport { useCartDrawer } from "@cimplify/sdk/react";\nimport { useCartCount } from "@/lib/cart";\n\n/**\n * Cart pill \u2014 dynamic island. Reads the live cart count via the SDK and\n * opens the side cart drawer on click (instead of navigating to /cart).\n * Wrap in `<Suspense fallback={<CartPillSkeleton/>}>` so the cached\n * header chrome streams without blocking on the cart fetch.\n */\nexport function CartPill() {\n const { count } = useCartCount();\n const { open } = useCartDrawer();\n return (\n <button\n type="button"\n onClick={open}\n aria-label={`Open cart, ${count} ${count === 1 ? "item" : "items"}`}\n className="inline-flex items-center gap-1.5 px-4 py-2.5 sm:py-2 rounded-full bg-foreground text-background text-xs font-semibold tracking-wide transition-transform hover:scale-105 cursor-pointer"\n >\n Cart \xB7 {count}\n </button>\n );\n}\n\nexport function CartPillSkeleton() {\n return (\n <span\n aria-hidden\n className="inline-flex items-center gap-1.5 px-4 py-2.5 sm:py-2 rounded-full bg-foreground/80 text-background text-xs font-semibold tracking-wide"\n >\n Cart \xB7 \u2026\n </span>\n );\n}\n' }, { "path": "components/collection-strip.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport type { Collection, Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "./store-product-card";\n\ninterface CollectionStripProps {\n collection: Collection;\n products: Product[];\n collectionHref?: string;\n}\n\n/**\n * Horizontal strip of products under a collection title. Cards come from the\n * SDK so every variant (Food, Bundle, Composite, Service, \u2026) renders\n * correctly; clicks open the shared URL-driven product modal.\n */\nexport function CollectionStrip({ collection, products, collectionHref }: CollectionStripProps) {\n if (products.length === 0) return null;\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 pt-12">\n <header className="grid grid-cols-[1fr_auto] items-end gap-4 mb-5">\n <h2 className="font-serif text-[28px] font-semibold m-0">{collection.name}</h2>\n {collectionHref && (\n <Link\n href={collectionHref}\n className="text-[13px] font-semibold text-primary hover:underline"\n >\n See all \u2192\n </Link>\n )}\n {collection.description && (\n <p className="col-span-full m-0 mt-1 text-sm text-muted-foreground">\n {collection.description}\n </p>\n )}\n </header>\n <div className="grid grid-flow-col auto-cols-[minmax(220px,1fr)] gap-4 overflow-x-auto snap-x snap-mandatory pb-2">\n {products.slice(0, 8).map((p) => (\n <div key={p.id} className="snap-start">\n <StoreProductCard product={p} />\n </div>\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "components/hero.tsx", "kind": "text", "content": 'interface HeroProps {\n badge?: string;\n title: string;\n subtitle?: string;\n}\n\nexport function Hero({ badge, title, subtitle }: HeroProps) {\n return (\n <section className="px-6 sm:px-8 py-16 text-center bg-gradient-to-b from-accent to-background">\n {badge && (\n <span className="inline-block mb-4 px-3.5 py-1.5 rounded-full bg-card border border-accent text-accent-foreground text-[11px] font-semibold uppercase tracking-[0.12em]">\n {badge}\n </span>\n )}\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold m-0 -tracking-[0.02em]">\n {title}\n </h1>\n {subtitle && (\n <p className="mx-auto mt-2 max-w-xl text-base text-muted-foreground">\n {subtitle}\n </p>\n )}\n </section>\n );\n}\n' }, { "path": "components/account-iframe.tsx", "kind": "text", "content": '"use client";\n\nimport { CimplifyAccount } from "@cimplify/sdk/react";\n\n/**\n * Cimplify Account portal \u2014 iframe-mounted UI hosted by Cimplify Link.\n * Handles sign-in, sign-up, OTP, addresses, payment methods, sessions,\n * and order history. The iframe owns auth state; we just choose which\n * `section` to land on.\n */\nexport function AccountIframe({ section }: { section?: string }) {\n return <CimplifyAccount section={section} />;\n}\n' }, { "path": "components/policy-page.tsx", "kind": "text", "content": 'import type { BrandPolicySection } from "@/lib/brand";\n\ninterface PolicyShape {\n eyebrow: string;\n title: string;\n lastUpdated?: string;\n sections: BrandPolicySection[];\n}\n\n/**\n * Shared layout for shipping / returns / accessibility / terms / privacy.\n * Reads a `{ eyebrow, title, lastUpdated, sections[] }` block from brand.\n */\nexport function PolicyPage({ policy }: { policy: PolicyShape }) {\n return (\n <article className="max-w-3xl mx-auto px-6 sm:px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {policy.eyebrow}\n </p>\n <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-2 -tracking-[0.02em]">\n {policy.title}\n </h1>\n {policy.lastUpdated && (\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {policy.lastUpdated}\n </p>\n )}\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {policy.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="text-2xl font-semibold mt-0">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "components/header.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport { Suspense } from "react";\nimport { NavLink } from "./nav-link";\nimport { CartPill, CartPillSkeleton } from "./cart-pill";\nimport { MobileNav } from "./mobile-nav";\nimport { brand } from "@/lib/brand";\n\n/**\n * Server-rendered header chrome. Brand mark + nav layout streams from the\n * cache; the active-link styling and live cart count are dynamic islands\n * mounted in their own Suspense boundaries so the chrome never blocks.\n */\nexport function Header() {\n return (\n <header className="sticky top-0 z-30 flex items-center justify-between px-6 sm:px-8 py-4 border-b border-border bg-background/90 backdrop-blur-md">\n <Link href="/" className="flex items-baseline gap-2">\n <span className="font-serif text-[22px] font-semibold -tracking-[0.02em]">\n {brand.shortName}\n </span>\n <span className="hidden sm:inline text-[11px] font-medium uppercase tracking-[0.12em] text-muted-foreground">\n {brand.microTag}\n </span>\n </Link>\n <div className="flex items-center gap-3 sm:gap-6">\n <nav className="hidden sm:flex items-center gap-6">\n {brand.header.nav.map((link) => (\n <Suspense key={link.href} fallback={<NavLinkFallback>{link.label}</NavLinkFallback>}>\n <NavLink href={link.href}>{link.label}</NavLink>\n </Suspense>\n ))}\n </nav>\n <Suspense fallback={<CartPillSkeleton />}>\n <CartPill />\n </Suspense>\n <div className="sm:hidden">\n <MobileNav />\n </div>\n </div>\n </header>\n );\n}\n\nfunction NavLinkFallback({ children }: { children: React.ReactNode }) {\n return (\n <span className="text-[13px] font-medium tracking-wide text-muted-foreground">\n {children}\n </span>\n );\n}\n' }, { "path": "components/product-modal.tsx", "kind": "text", "content": '"use client";\n\nimport { useEffect } from "react";\nimport Image from "next/image";\nimport { useRouter, useSearchParams, usePathname } from "next/navigation";\nimport { ProductSheet, useProduct, useCart } from "@cimplify/sdk/react";\n\n/**\n * URL-driven product modal. Reads `?product=<slug>` and renders the SDK\'s\n * `<ProductSheet/>` \u2014 vertical layout with image-on-top, then header, then\n * the variant/add-on/composite/bundle customizer. Closing the modal clears\n * the search param. Deep-linkable and survives reloads.\n */\nexport function ProductModal() {\n const router = useRouter();\n const pathname = usePathname();\n const searchParams = useSearchParams();\n const slug = searchParams?.get("product") ?? null;\n\n const { product } = useProduct(slug ?? "", { enabled: Boolean(slug) });\n const { addItem } = useCart();\n\n useEffect(() => {\n if (!slug) return;\n const original = document.body.style.overflow;\n document.body.style.overflow = "hidden";\n return () => {\n document.body.style.overflow = original;\n };\n }, [slug]);\n\n useEffect(() => {\n if (!slug) return;\n const onKey = (e: KeyboardEvent) => {\n if (e.key === "Escape") close();\n };\n window.addEventListener("keydown", onKey);\n return () => window.removeEventListener("keydown", onKey);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [slug]);\n\n if (!slug) return null;\n\n function close() {\n const next = new URLSearchParams(searchParams?.toString() ?? "");\n next.delete("product");\n const qs = next.toString();\n router.replace(qs ? `${pathname}?${qs}` : pathname, { scroll: false });\n }\n\n return (\n <div\n role="dialog"\n aria-modal="true"\n onClick={close}\n className="fixed inset-0 z-[100] flex items-end sm:items-center justify-center sm:p-6 bg-foreground/50 backdrop-blur-sm"\n >\n <div\n onClick={(e) => e.stopPropagation()}\n className="relative w-full sm:max-w-lg max-h-[92vh] overflow-auto bg-card sm:rounded-3xl rounded-t-3xl shadow-2xl"\n >\n <button\n onClick={close}\n aria-label="Close product details"\n className="absolute top-4 right-4 z-10 w-9 h-9 rounded-full bg-card border border-border text-sm cursor-pointer transition-colors hover:bg-muted"\n >\n \u2715\n </button>\n {product ? (\n <ProductSheet\n product={product}\n onClose={close}\n onAddToCart={async (p, qty, options) => {\n await addItem(p, qty, options);\n close();\n }}\n renderImage={({ src, alt, className }) => (\n <Image\n src={src}\n alt={alt}\n width={1200}\n height={900}\n className={className}\n style={{ width: "100%", height: "auto", objectFit: "cover" }}\n priority\n />\n )}\n classNames={{\n root: "p-6 sm:p-8 gap-4",\n image: "rounded-2xl overflow-hidden -mx-6 sm:-mx-8 -mt-6 sm:-mt-8 mb-2",\n header: "flex items-baseline justify-between gap-4",\n name: "font-serif text-2xl font-semibold m-0",\n price: "text-lg font-semibold text-primary",\n description: "text-sm text-muted-foreground leading-relaxed",\n customizer: "pt-2",\n }}\n />\n ) : (\n <div className="p-8 text-center text-muted-foreground">Loading\u2026</div>\n )}\n </div>\n </div>\n );\n}\n' }, { "path": "components/mobile-nav.tsx", "kind": "text", "content": '"use client";\n\nimport { useEffect, useState } from "react";\nimport Link from "next/link";\nimport { brand } from "@/lib/brand";\n\n/**\n * Hamburger button + slide-in drawer for narrow viewports. Header hides\n * its inline nav links below `sm` and renders this in their place; the\n * cart pill stays in the header chrome.\n */\nexport function MobileNav() {\n const [open, setOpen] = useState(false);\n\n useEffect(() => {\n if (!open) return;\n const onKey = (event: KeyboardEvent) => {\n if (event.key === "Escape") setOpen(false);\n };\n document.addEventListener("keydown", onKey);\n const previousOverflow = document.body.style.overflow;\n document.body.style.overflow = "hidden";\n return () => {\n document.removeEventListener("keydown", onKey);\n document.body.style.overflow = previousOverflow;\n };\n }, [open]);\n\n return (\n <>\n <button\n type="button"\n onClick={() => setOpen(true)}\n aria-label="Open menu"\n aria-expanded={open}\n aria-controls="mobile-nav-drawer"\n className="grid place-items-center w-11 h-11 -mr-2 rounded-md text-foreground hover:bg-muted transition-colors"\n >\n <svg\n width="20"\n height="20"\n viewBox="0 0 24 24"\n fill="none"\n stroke="currentColor"\n strokeWidth="2"\n strokeLinecap="round"\n strokeLinejoin="round"\n aria-hidden="true"\n >\n <line x1="3" y1="6" x2="21" y2="6" />\n <line x1="3" y1="12" x2="21" y2="12" />\n <line x1="3" y1="18" x2="21" y2="18" />\n </svg>\n </button>\n\n {open ? (\n <div className="fixed inset-0 z-50 sm:hidden">\n <button\n type="button"\n onClick={() => setOpen(false)}\n aria-label="Close menu"\n className="absolute inset-0 bg-background/80 backdrop-blur-sm"\n />\n <nav\n id="mobile-nav-drawer"\n aria-label="Mobile navigation"\n className="absolute inset-y-0 right-0 w-[85%] max-w-sm flex flex-col bg-background border-l border-border shadow-2xl"\n >\n <div className="flex items-center justify-between px-6 py-4 border-b border-border">\n <span className="text-xs font-semibold uppercase tracking-[0.16em] text-muted-foreground">\n Menu\n </span>\n <button\n type="button"\n onClick={() => setOpen(false)}\n aria-label="Close menu"\n className="grid place-items-center w-11 h-11 -mr-2 rounded-md text-foreground hover:bg-muted transition-colors"\n >\n <svg\n width="20"\n height="20"\n viewBox="0 0 24 24"\n fill="none"\n stroke="currentColor"\n strokeWidth="2"\n strokeLinecap="round"\n strokeLinejoin="round"\n aria-hidden="true"\n >\n <line x1="18" y1="6" x2="6" y2="18" />\n <line x1="6" y1="6" x2="18" y2="18" />\n </svg>\n </button>\n </div>\n <ul className="flex flex-col gap-1 px-3 py-4">\n {brand.header.nav.map((link) => (\n <li key={link.href}>\n <Link\n href={link.href}\n onClick={() => setOpen(false)}\n className="block px-3 py-3 rounded-md text-base font-medium text-foreground hover:bg-muted transition-colors"\n >\n {link.label}\n </Link>\n </li>\n ))}\n </ul>\n </nav>\n </div>\n ) : null}\n </>\n );\n}\n' }, { "path": "components/providers.tsx", "kind": "text", "content": '"use client";\n\nimport { useMemo, type ReactNode } from "react";\nimport { createCimplifyClient } from "@cimplify/sdk";\nimport { CimplifyProvider, CartDrawerProvider } from "@cimplify/sdk/react";\n\n// Same-origin client \u2014 every request goes through the Next.js rewrite in\n// next.config.ts, so no CORS preflight ever hits the browser.\nexport function Providers({ children }: { children: ReactNode }) {\n const client = useMemo(() => {\n const baseUrl =\n typeof window !== "undefined" ? window.location.origin : "http://127.0.0.1:8787";\n const publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY ?? "mock-dev";\n return createCimplifyClient({\n baseUrl,\n publicKey,\n suppressPublicKeyWarning: true,\n });\n }, []);\n\n return (\n <CimplifyProvider client={client}>\n <CartDrawerProvider>{children}</CartDrawerProvider>\n </CimplifyProvider>\n );\n}\n' }, { "path": "components/footer.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport { brand } from "@/lib/brand";\n\nconst ICONS: Record<string, React.ReactNode> = {\n instagram: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" aria-hidden className="w-5 h-5">\n <rect x="3" y="3" width="18" height="18" rx="5" />\n <circle cx="12" cy="12" r="4" />\n <circle cx="17.5" cy="6.5" r="0.75" fill="currentColor" />\n </svg>\n ),\n x: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M18 2h3l-7.5 8.6L22 22h-6.6l-5-6.5L4 22H1l8-9.2L1.4 2H8l4.6 6 5.4-6z" />\n </svg>\n ),\n tiktok: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M16 3v3.5a4.5 4.5 0 0 0 4.5 4.5V14a7.5 7.5 0 0 1-4.5-1.5V16a5 5 0 1 1-5-5v3a2 2 0 1 0 2 2V3z" />\n </svg>\n ),\n facebook: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M14 9V7a1 1 0 0 1 1-1h2V3h-3a4 4 0 0 0-4 4v2H8v3h2v9h3v-9h2.5l.5-3H13z" />\n </svg>\n ),\n youtube: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M22.5 6.5a2.6 2.6 0 0 0-1.8-1.8C19 4.2 12 4.2 12 4.2s-7 0-8.7.5A2.6 2.6 0 0 0 1.5 6.5C1 8.2 1 12 1 12s0 3.8.5 5.5a2.6 2.6 0 0 0 1.8 1.8C5 19.8 12 19.8 12 19.8s7 0 8.7-.5a2.6 2.6 0 0 0 1.8-1.8c.5-1.7.5-5.5.5-5.5s0-3.8-.5-5.5zM10 15.5v-7l6 3.5-6 3.5z" />\n </svg>\n ),\n linkedin: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M4 4h4v16H4zM6 2.5a2 2 0 1 0 0 4 2 2 0 0 0 0-4zM10 8h4v2.5h.1c.6-1.1 2-2.5 4-2.5 4 0 4.9 2.6 4.9 6V20h-4v-5c0-1.5-.5-3-2.3-3-1.7 0-2.5 1.3-2.5 3v5h-4z" />\n </svg>\n ),\n whatsapp: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M12 2a10 10 0 0 0-8.6 15l-1.4 5 5.2-1.4A10 10 0 1 0 12 2zm5 14.2c-.2.6-1.2 1.2-1.7 1.2-.5.1-1.1.1-1.7-.1-.4-.1-.9-.3-1.5-.6a8.4 8.4 0 0 1-3.7-3.4c-.7-1-1-1.8-1-2.5 0-.7.4-1.1.6-1.3.2-.2.4-.2.5-.2h.4c.1 0 .3 0 .4.3l.6 1.4c.1.2 0 .3 0 .4l-.3.4-.3.3c-.1.1-.2.2-.1.4.2.4.7 1.1 1.4 1.8.9.8 1.7 1.1 1.9 1.2.2.1.3.1.5-.1l.6-.7c.2-.2.3-.2.5-.1l1.4.7c.2.1.3.2.4.3.1.2.1.6 0 1z" />\n </svg>\n ),\n};\n\nconst FALLBACK_ICON = (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" aria-hidden className="w-5 h-5">\n <circle cx="12" cy="12" r="9" />\n </svg>\n);\n\nexport async function Footer() {\n "use cache";\n const year = new Date().getFullYear();\n return (\n <footer className="mt-12 px-6 sm:px-8 py-10 text-xs text-muted-foreground border-t border-border bg-card">\n <div className="max-w-7xl mx-auto">\n <div className="grid gap-10 md:grid-cols-[1.4fr_repeat(4,1fr)]">\n <div>\n <p className="font-serif text-2xl text-foreground m-0 mb-2">{brand.name}</p>\n <p className="leading-relaxed mb-4 max-w-sm">{brand.footer.blurb}</p>\n <address className="not-italic space-y-1">\n <p className="m-0">{brand.contact.address}</p>\n <p className="m-0">\n <a\n href={`tel:${brand.contact.phoneTel}`}\n className="hover:text-foreground transition-colors"\n >\n {brand.contact.phone}\n </a>\n </p>\n <p className="m-0">\n <a\n href={`mailto:${brand.contact.email}`}\n className="hover:text-foreground transition-colors"\n >\n {brand.contact.email}\n </a>\n </p>\n <p className="m-0">{brand.contact.hours}</p>\n </address>\n <div className="flex items-center gap-3 mt-5">\n {brand.socials.map((s) => (\n <a\n key={s.label}\n href={s.href}\n aria-label={s.label}\n target="_blank"\n rel="noopener noreferrer"\n className="inline-flex items-center justify-center w-9 h-9 rounded-full border border-border text-muted-foreground hover:text-foreground hover:border-foreground transition-colors"\n >\n {(s.icon && ICONS[s.icon]) ?? FALLBACK_ICON}\n </a>\n ))}\n </div>\n </div>\n {brand.footer.sitemap.map((section) => (\n <nav key={section.title} aria-labelledby={`footer-${section.title}`}>\n <p\n id={`footer-${section.title}`}\n className="font-semibold text-foreground mb-3 text-[13px] uppercase tracking-[0.08em]"\n >\n {section.title}\n </p>\n <ul className="space-y-2 m-0 p-0 list-none">\n {section.links.map((link) => (\n <li key={link.label}>\n <Link\n href={link.href}\n className="hover:text-foreground transition-colors"\n >\n {link.label}\n </Link>\n </li>\n ))}\n </ul>\n </nav>\n ))}\n </div>\n <div className="mt-12 pt-6 border-t border-border flex flex-col sm:flex-row items-center justify-between gap-3">\n <p className="m-0">\xA9 {year} {brand.name}. All rights reserved.</p>\n {brand.footer.poweredBy && (\n <p className="m-0 inline-flex items-center gap-1.5">\n <span className="text-muted-foreground/80">Powered by</span>\n <a\n href={brand.footer.poweredBy.href}\n target="_blank"\n rel="noopener noreferrer"\n aria-label={brand.footer.poweredBy.label}\n className="inline-flex items-center gap-1 font-serif text-foreground hover:text-primary transition-colors"\n >\n <span className="font-semibold tracking-tight">{brand.footer.poweredBy.label}</span>\n <svg\n viewBox="0 0 12 12"\n aria-hidden\n className="w-3 h-3 opacity-70"\n fill="none"\n stroke="currentColor"\n strokeWidth="1.5"\n >\n <path d="M3 9L9 3M9 3H4M9 3v5" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n </a>\n </p>\n )}\n </div>\n </div>\n </footer>\n );\n}\n' }, { "path": "components/store-product-card.tsx", "kind": "text", "content": '"use client";\n\nimport Link from "next/link";\nimport {\n CardVariant,\n FoodProductCard,\n RetailProductCard,\n WholesaleProductCard,\n DigitalProductCard,\n BundleProductCard,\n CompositeProductCard,\n StandardServiceCard,\n CompactServiceCard,\n ScheduleServiceCard,\n RentalServiceCard,\n AccommodationCard,\n LeaseServiceCard,\n SubscriptionCard,\n} from "@cimplify/sdk/react";\nimport type { CardLayoutProps } from "@cimplify/sdk/react";\nimport type { Product } from "@cimplify/sdk";\nimport { PRODUCT_TYPE, RENDER_HINT, DURATION_UNIT } from "@cimplify/sdk";\n\nconst RENTAL_UNITS = new Set<string>([DURATION_UNIT.Days, DURATION_UNIT.Hours]);\nconst LEASE_UNITS = new Set<string>([DURATION_UNIT.Weeks, DURATION_UNIT.Months, DURATION_UNIT.Years]);\n\nconst VARIANT_CARDS: Record<CardVariant, React.ComponentType<CardLayoutProps>> = {\n [CardVariant.Food]: FoodProductCard,\n [CardVariant.Retail]: RetailProductCard,\n [CardVariant.Wholesale]: WholesaleProductCard,\n [CardVariant.Digital]: DigitalProductCard,\n [CardVariant.Bundle]: BundleProductCard,\n [CardVariant.Composite]: CompositeProductCard,\n [CardVariant.Standard]: StandardServiceCard,\n [CardVariant.Compact]: CompactServiceCard,\n [CardVariant.Schedule]: ScheduleServiceCard,\n [CardVariant.Rental]: RentalServiceCard,\n [CardVariant.Accommodation]: AccommodationCard,\n [CardVariant.Lease]: LeaseServiceCard,\n [CardVariant.Subscription]: SubscriptionCard,\n};\n\nfunction resolveVariant(p: Product): CardVariant {\n if (p.type === PRODUCT_TYPE.Bundle) return CardVariant.Bundle;\n if (p.type === PRODUCT_TYPE.Composite) return CardVariant.Composite;\n if (p.quantity_pricing && p.quantity_pricing.length > 1) return CardVariant.Wholesale;\n if (p.type === PRODUCT_TYPE.Digital) return CardVariant.Digital;\n if (p.type === PRODUCT_TYPE.Service) {\n if (p.duration_unit && RENTAL_UNITS.has(p.duration_unit)) return CardVariant.Rental;\n if (p.duration_unit === DURATION_UNIT.Nights) return CardVariant.Accommodation;\n if (p.duration_unit && LEASE_UNITS.has(p.duration_unit)) return CardVariant.Lease;\n if (p.billing_plans && p.billing_plans.length > 0 && !p.duration_minutes) return CardVariant.Subscription;\n return CardVariant.Standard;\n }\n if (p.render_hint === RENDER_HINT.Food) return CardVariant.Food;\n return CardVariant.Retail;\n}\n\ninterface Props {\n product: Product;\n /** Override the auto-detected card variant. */\n variant?: CardVariant;\n}\n\n/**\n * Variant-aware product card that opens the URL-driven product modal on\n * click. Uses `next/link` with `?product=<slug>` so the link is statically\n * pre-renderable \u2014 no `useSearchParams` dependency means cards don\'t\n * become dynamic islands. The modal (which already reads `useSearchParams`\n * inside a Suspense boundary in the root layout) handles the rest.\n */\nexport function StoreProductCard({ product, variant }: Props) {\n const slug = product.slug || product.id;\n const Card = VARIANT_CARDS[variant ?? resolveVariant(product)];\n const href = `?product=${encodeURIComponent(slug)}`;\n\n return (\n <Card\n product={product}\n renderLink={({ className, children }) => (\n <Link href={href} scroll={false} prefetch={false} className={className}>\n {children}\n </Link>\n )}\n />\n );\n}\n' }, { "path": "components/json-ld.tsx", "kind": "text", "content": 'import { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\n\n/**\n * Streams the organization JSON-LD script tag in async so it doesn\'t block\n * the rest of the layout shell. `getSiteUrl` reads request headers, and\n * any await on dynamic data inside `RootLayout` makes Next 16 mark the\n * whole route as blocking.\n */\nexport async function OrganizationJsonLd(): Promise<React.ReactElement> {\n const siteUrl = await getSiteUrl();\n const ld = {\n "@context": "https://schema.org",\n "@type": brand.schemaType,\n name: brand.name,\n url: siteUrl,\n description: brand.description,\n email: brand.contact.email,\n telephone: brand.contact.phoneTel,\n address: {\n "@type": "PostalAddress",\n streetAddress: brand.contact.streetAddress,\n addressLocality: brand.contact.city,\n addressCountry: brand.contact.countryCode,\n },\n sameAs: brand.socials.map((s) => s.href),\n };\n return (\n <script\n type="application/ld+json"\n dangerouslySetInnerHTML={{ __html: JSON.stringify(ld) }}\n />\n );\n}\n' }, { "path": "components/nav-link.tsx", "kind": "text", "content": '"use client";\n\nimport Link from "next/link";\nimport { usePathname } from "next/navigation";\n\nexport function NavLink({ href, children }: { href: string; children: React.ReactNode }) {\n const pathname = usePathname();\n const active = pathname === href;\n return (\n <Link\n href={href}\n className={[\n "text-[13px] font-medium tracking-wide transition-colors",\n active ? "text-primary" : "text-muted-foreground hover:text-foreground",\n ].join(" ")}\n >\n {children}\n </Link>\n );\n}\n' }, { "path": "components/category-grid.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CategoryGrid as SdkCategoryGrid } from "@cimplify/sdk/react";\nimport type { Category } from "@cimplify/sdk";\n\n/**\n * Homepage category tiles \u2014 defers to the SDK\'s <CategoryGrid/> for layout\n * and accessibility, and routes selections to /categories/:slug.\n */\nexport function CategoryGrid({ categories }: { categories?: Category[] }) {\n const router = useRouter();\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 pt-14">\n <h2 className="font-serif text-[28px] font-semibold m-0 mb-5">Browse the menu</h2>\n <SdkCategoryGrid\n categories={categories}\n onSelect={(c) => router.push(`/categories/${c.slug}`)}\n classNames={{\n item: "flex flex-col gap-1 p-6 bg-card border border-border rounded-2xl cursor-pointer text-left transition-all hover:border-primary hover:-translate-y-0.5",\n name: "font-serif text-lg font-semibold text-foreground",\n description: "text-xs text-muted-foreground",\n count: "text-xs text-muted-foreground mt-1",\n }}\n />\n </section>\n );\n}\n' }, { "path": "components/cart-drawer.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CartDrawer as SdkCartDrawer } from "@cimplify/sdk/react";\n\n/**\n * Side-drawer cart. Auto-opens when an item is added (via\n * `<CartDrawerProvider>` in `providers.tsx`). The header\'s cart pill\n * also calls `useCartDrawer().open()` to reveal it on click.\n */\nexport function CartDrawer() {\n const router = useRouter();\n return <SdkCartDrawer onCheckout={() => router.push("/checkout")} />;\n}\n' }, { "path": "next.config.ts", "kind": "text", "content": 'import type { NextConfig } from "next";\n\n// Same-origin proxy target for the storefront API. The public key\n// decides: a real `cpk_live_\u2026` / `cpk_test_\u2026` only resolves against\n// hosted Cimplify, so browser cart writes go to the same place the\n// server catalogue reads come from. Anything else (`mock-dev`, empty)\n// falls back to the local `cimplify dev` mock in dev.\nconst publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY?.trim() ?? "";\nconst keyTargetsHostedCimplify =\n publicKey.startsWith("cpk_live_") || publicKey.startsWith("cpk_test_");\nconst STOREFRONT_URL =\n process.env.NODE_ENV === "production" || keyTargetsHostedCimplify\n ? "https://storefronts.cimplify.io"\n : "http://127.0.0.1:8787";\n\nif (STOREFRONT_URL === "http://127.0.0.1:8787") {\n console.warn(\n "[cimplify] next.config resolved the local storefront API URL; production deploys must run with NODE_ENV=production.",\n );\n}\n\nconst nextConfig: NextConfig = {\n // Enable Next 16\'s `cacheComponents` mode so we can use `\'use cache\'` +\n // `cacheTag` / `cacheLife` for SSR caching. Cached chrome streams first,\n // dynamic Suspense boundaries fill in when ready.\n cacheComponents: true,\n async redirects() {\n // Config-level so cacheComponents needn\'t prerender a redirect()-only page.\n return [\n { source: "/login", destination: "/account", permanent: false },\n { source: "/signup", destination: "/account", permanent: false },\n ];\n },\n async rewrites() {\n return [\n { source: "/api/v1/:path*", destination: `${STOREFRONT_URL}/api/v1/:path*` },\n { source: "/img/:path*", destination: `${STOREFRONT_URL}/img/:path*` },\n { source: "/elements/:path*", destination: `${STOREFRONT_URL}/elements/:path*` },\n { source: "/_mock/:path*", destination: `${STOREFRONT_URL}/_mock/:path*` },\n ];\n },\n images: {\n loader: "custom",\n loaderFile: "./lib/cimplify-loader.ts",\n remotePatterns: [\n { protocol: "http", hostname: "127.0.0.1", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "3000", pathname: "/img/**" },\n { protocol: "https", hostname: "loremflickr.com", pathname: "/**" },\n { protocol: "https", hostname: "images.unsplash.com", pathname: "/**" },\n { protocol: "https", hostname: "static-tmp.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "storefrontassetscdn.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "cdn.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "res.cloudinary.com", pathname: "/cimplify/**" },\n ],\n },\n};\n\nexport default nextConfig;\n' }, { "path": ".claude/skills/cimplify-storefront/SKILL.md", "kind": "text", "content": "---\nname: cimplify-storefront\ndescription: Build, customize, rebrand, or deploy a Cimplify-scaffolded storefront. Triggers when the user asks to create a storefront, rebrand a Cimplify template, change the palette, add a page, deploy to Cimplify, or works in a project containing `lib/brand.ts` and `next.config.ts` with `cacheComponents`.\n---\n\n# Cimplify Storefront skill\n\nYou're working on a project scaffolded from `cimplify init`. The architecture is opinionated and the rebrand surface is intentionally small. Read `AGENTS.md` at the project root for the file \u2194 brand-field map for *this template's* industry; this skill gives you the playbook that's the same across all six.\n\n## The contract \u2014 never break\n\n1. **`lib/brand.ts` is the only place for content edits.** Every visible string reads from this file. If a string isn't in `brand`, *add a field* to the `Brand` interface \u2014 don't hardcode it in a page or component.\n2. **`app/globals.css` `@theme { \u2026 }`** holds palette + radius + font references. To re-skin the entire site, edit only this block.\n3. **Server Components are cached** via `'use cache'` + `cacheTag(tags.X())` + `cacheLife(\"hours\")`. Don't downgrade them to `\"use client\"` to avoid an issue \u2014 fix the issue.\n4. **Client islands** (anything reading `useSearchParams`, `usePathname`, `useRouter`, `useState`) live behind `<Suspense>`.\n5. **`cacheComponents: true`** in `next.config.ts` is non-negotiable. Don't turn it off.\n6. **`bun run test:run` (vitest)** is the canonical test runner. `bun test` will show false failures because Bun's `vi` shim is incomplete.\n7. **Cart, checkout, orders stay client.** They're session-bound. Don't try to SSR them.\n\n## The brand-field schema (in `lib/brand.ts`)\n\n```\nidentity: name, shortName, microTag, description, schemaType, currency, locale\ncontact: address, streetAddress, city, countryCode, phone, phoneTel, email, privacyEmail, hours\nsocials: [{ label, href, icon }] // icon \u2208 instagram|x|tiktok|facebook|youtube|linkedin|whatsapp\nheader: { nav: [{ label, href }] }\nhero: { badge, title, subtitle, primaryCtaLabel, secondaryCtaLabel?, secondaryCtaHref? }\noptional: trustItems[]?, brandStrip?, promo?, tradeIn? // render conditionally\nnewsletter: { eyebrow, title, body, placeholder, submitLabel, successLabel }\nabout: { eyebrow, title, paragraphs[], sections[{ heading, body }] }\nfaq: { eyebrow, title, sections[{ title, items[{ q, a }] }], contactPrompt, contactEmail }\nterms,\nprivacy,\nshipping,\nreturns,\naccessibility: { eyebrow, title, lastUpdated, sections[{ heading, body | { intro, bullets[] } }] }\naccount: eyebrows + titles for /account, /login, /signup\ncontactPage: { eyebrow, title, body, reasons[], directLines[{ label, value, href }] }\ntrackOrder: { eyebrow, title, body }\nfooter: { blurb, sitemap[{ title, links[] }], poweredBy? }\nllms: { summary } // opens /llms.txt\nmock: { seed, businessId }\n```\n\n## Playbook \u2014 common tasks\n\n### Rebrand a storefront for a new merchant\n\n1. Edit `lib/brand.ts`. Replace every field with the merchant's content. Use the brief / context the user gave you.\n2. Edit `app/globals.css` `@theme { \u2026 }`. Change `--color-primary`, `--color-background`, `--color-foreground`, optionally `--radius`. Use OKLCH; if the brand only gave hex, convert.\n3. (Optional) Swap fonts in `app/layout.tsx` \u2014 `next/font/google` import + the variable wired into the `<html>` className.\n4. Set `.env.local`: `NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY` (optional: `NEXT_PUBLIC_SITE_URL`).\n\nDon't touch any other file. If the rebrand needs content not in the schema, add the field to `Brand` first, populate it, then read it from the page.\n\n### Add a new section / component\n\n1. Build it as a Server Component in `components/` (or a client island in `*-client.tsx` if interactive).\n2. Read merchant copy from `brand`. Add new fields to the `Brand` interface if needed.\n3. Wrap interactive bits in `<Suspense fallback={\u2026}>` so the cached chrome streams.\n4. Compose into the page.\n\n### Wire a Server Action that mutates data\n\n```ts\n\"use server\";\nimport { getServerClient, revalidateProducts } from \"@cimplify/sdk/server\";\n\nexport async function createProduct(input: ProductInput) {\n await getServerClient().catalogue.createProduct(input);\n revalidateProducts();\n}\n```\n\nAfter every mutation, call the matching `revalidate*` helper from `@cimplify/sdk/server`: `revalidateProducts`, `revalidateProduct(id)`, `revalidateCategories`, `revalidateCategory(id)`, `revalidateCollections`, `revalidateCollection(id)`, `revalidateBusiness`.\n\n### Add a Server Component data fetch\n\n```ts\nimport { cacheTag, cacheLife } from \"next/cache\";\nimport { getServerClient, tags } from \"@cimplify/sdk/server\";\n\nasync function getX() {\n \"use cache\";\n cacheTag(tags.products());\n cacheLife(\"hours\");\n const r = await getServerClient().catalogue.getProducts({ limit: 24 });\n if (!r.ok) throw new Error(r.error.message);\n return r.value.items;\n}\n```\n\n### Eject an SDK component for deeper customization\n\nTry `classNames`, `renderImage`, `renderLink`, slot props first. If those run out:\n\n```bash\ncimplify list\ncimplify add product-card --dir src/components/cimplify\n```\n\nOnce ejected, the file is yours. Hooks/types still come from `@cimplify/sdk`.\n\n### Deploy\n\n```bash\ncimplify login\ncimplify projects create my-store\ncimplify link <project-id>\ncimplify env push\ncimplify deploy --prod\ncimplify logs --follow\ncimplify domains add my-store.com\n```\n\n## Pitfalls \u2014 explicit \u274C list\n\n- \u274C Hardcoding any visible string in a page or component. Always `brand.X`.\n- \u274C Disabling `cacheComponents: true` to silence a warning. Fix the warning by wrapping the dynamic call in `<Suspense>`.\n- \u274C Using `unstable_cache` \u2014 Next 16's canonical primitive is `'use cache'` + `cacheTag` + `cacheLife`.\n- \u274C Bypassing `getServerClient()` and calling `createCimplifyClient` directly in a Server Component \u2014 loses per-request memoization.\n- \u274C Mutating data without calling the matching `revalidate*` helper.\n- \u274C Adding an `app/error.tsx` handler that calls `reset()` without logging \u2014 silently swallowed errors hide bugs.\n- \u274C Running `bun test` and reporting failures. Use `bun run test:run` (vitest).\n- \u274C Editing the per-template `AGENTS.md` to remove notes about TODOs (contact form, newsletter fake submits) \u2014 they're real.\n\n## Where things live\n\n| Need | Look here |\n|---|---|\n| Which page reads which `brand.X` field | `AGENTS.md` at project root |\n| Architectural rules | `AGENTS.md` at project root + this skill |\n| Running locally | `bun dev` (boots mock + Next together) |\n| Switching mock seed | edit `dev:mock` in `package.json` |\n| Full SDK reference | `docs/sdk/storefronts.md` in the Cimplify repo |\n\n## What to do when the user asks something out-of-scope\n\nIf the user asks for something the template doesn't support out of the box:\n\n1. **Check first** \u2014 is there an SDK component or hook that does it? `@cimplify/sdk/react` has 50+ components, 30+ hooks. Search `node_modules/@cimplify/sdk/dist/react.d.mts` if needed.\n2. **Compose from existing parts** before authoring new ones.\n3. **If genuinely missing** \u2014 build the new component, hoist its strings into `brand.ts`, document in `AGENTS.md`.\n\nDon't introduce competing patterns (a new caching strategy, a new auth flow, a new theming system). The template's opinions are intentional.\n" }, { "path": ".cursor/rules/cimplify-storefront.mdc", "kind": "binary", "content": "LS0tCmRlc2NyaXB0aW9uOiBDaW1wbGlmeSBzdG9yZWZyb250IOKAlCBhcmNoaXRlY3R1cmFsIHJ1bGVzIGFuZCByZWJyYW5kIGNvbnRyYWN0IGZvciBhIHByb2plY3Qgc2NhZmZvbGRlZCBmcm9tIGBjaW1wbGlmeSBpbml0YC4gVHJpZ2dlcnMgd2hlbiBmaWxlcyBsaWtlIGxpYi9icmFuZC50cywgYXBwL2dsb2JhbHMuY3NzLCBjb21wb25lbnRzL2hlYWRlci50c3gsIG9yIC5jaW1wbGlmeS9wcm9qZWN0Lmpzb24gYXJlIGluIHRoZSB3b3Jrc3BhY2UuCmdsb2JzOiBbImxpYi9icmFuZC50cyIsICJhcHAvKioiLCAiY29tcG9uZW50cy8qKiIsICIuZW52LmxvY2FsIiwgIm5leHQuY29uZmlnLnRzIl0KYWx3YXlzQXBwbHk6IHRydWUKLS0tCgojIENpbXBsaWZ5IHN0b3JlZnJvbnQKClRoaXMgcHJvamVjdCB3YXMgc2NhZmZvbGRlZCBmcm9tIGEgQ2ltcGxpZnkgdGVtcGxhdGUuIFJlYWQgYEFHRU5UUy5tZGAgYXQgdGhlIHByb2plY3Qgcm9vdCBmb3IgdGhlIGZpbGUg4oaUIGBicmFuZC5YYCBmaWVsZCBtYXAgYW5kIGAuY2xhdWRlL3NraWxscy9jaW1wbGlmeS1zdG9yZWZyb250L1NLSUxMLm1kYCBmb3IgdGhlIGZ1bGwgcGxheWJvb2suCgojIyBUaGUgY29udHJhY3QKCjEuICoqYGxpYi9icmFuZC50c2AqKiDigJQgZXZlcnkgdmlzaWJsZSBzdHJpbmcuIElmIHNvbWV0aGluZyBpc24ndCB0aGVyZSwgYWRkIGEgZmllbGQgdG8gdGhlIGBCcmFuZGAgaW50ZXJmYWNlOyBkb24ndCBoYXJkY29kZS4KMi4gKipgYXBwL2dsb2JhbHMuY3NzYCBgQHRoZW1lYCBibG9jayoqIOKAlCBwYWxldHRlLCByYWRpdXMsIGZvbnQgcmVmZXJlbmNlcy4KMy4gKipTZXJ2ZXIgQ29tcG9uZW50cyBhcmUgY2FjaGVkKiogdmlhIGAndXNlIGNhY2hlJ2AgKyBgY2FjaGVUYWcodGFncy5YKCkpYCArIGBjYWNoZUxpZmUoImhvdXJzIilgLiBEb24ndCBkb3duZ3JhZGUgdG8gY2xpZW50IHRvIHNpbGVuY2UgYSB3YXJuaW5nLgo0LiAqKkNsaWVudCBpc2xhbmRzKiogYmVoaW5kIGA8U3VzcGVuc2U+YC4KNS4gKipgY2FjaGVDb21wb25lbnRzOiB0cnVlYCoqIGluIGBuZXh0LmNvbmZpZy50c2AgaXMgbm9uLW5lZ290aWFibGUuCjYuIFVzZSAqKmBidW4gcnVuIHRlc3Q6cnVuYCoqICh2aXRlc3QpLCBub3QgYGJ1biB0ZXN0YC4KCiMjIERvbid0CgotIEhhcmRjb2RlIHZpc2libGUgc3RyaW5ncyBpbiBwYWdlcy9jb21wb25lbnRzLgotIFVzZSBgdW5zdGFibGVfY2FjaGVgIChOZXh0IDE2IHVzZXMgYCd1c2UgY2FjaGUnYCkuCi0gQnlwYXNzIGBnZXRTZXJ2ZXJDbGllbnQoKWAgKGxvc2VzIHBlci1yZXF1ZXN0IG1lbW9pemF0aW9uKS4KLSBNdXRhdGUgd2l0aG91dCBjYWxsaW5nIHRoZSBtYXRjaGluZyBgcmV2YWxpZGF0ZSpgIGhlbHBlciBmcm9tIGBAY2ltcGxpZnkvc2RrL3NlcnZlcmAuCg==" }, { "path": ".gitignore", "kind": "text", "content": "node_modules\n.next\nout\n.env\n.env.local\n.env*.local\n.DS_Store\n*.tsbuildinfo\nnext-env.d.ts\n" }, { "path": "app/about/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `About \u2014 ${brand.name}`,\n description: brand.description,\n};\n\nexport default function AboutPage() {\n const a = brand.about;\n const titleParts = a.title.split("\\n");\n return (\n <article className="max-w-3xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {a.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-6 -tracking-[0.02em]">\n {titleParts.map((line, i) => (\n <span key={i}>\n {line}\n {i < titleParts.length - 1 && <br />}\n </span>\n ))}\n </h1>\n <div className="prose prose-lg max-w-none space-y-5 text-foreground/90 leading-relaxed">\n {a.paragraphs.map((p, i) => (\n <p key={i}>{p}</p>\n ))}\n {a.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="font-serif text-3xl font-semibold mt-10 mb-3">{s.heading}</h2>\n <p>{s.body}</p>\n </div>\n ))}\n </div>\n </article>\n );\n}\n' }, { "path": "app/account/orders/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Orders \u2014 ${brand.name}`,\n};\n\nexport default function OrdersPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Your orders\n </h1>\n <AccountIframe section="orders" />\n </article>\n );\n}\n' }, { "path": "app/account/settings/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Settings \u2014 ${brand.name}`,\n};\n\nexport default function SettingsPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Settings\n </h1>\n <AccountIframe section="settings" />\n </article>\n );\n}\n' }, { "path": "app/account/addresses/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Addresses \u2014 ${brand.name}`,\n};\n\nexport default function AddressesPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Your addresses\n </h1>\n <AccountIframe section="addresses" />\n </article>\n );\n}\n' }, { "path": "app/account/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Account \u2014 ${brand.name}`,\n description: brand.account.signupSubtitle,\n};\n\nexport default function AccountPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {brand.account.accountEyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n {brand.account.accountTitle}\n </h1>\n <AccountIframe />\n </article>\n );\n}\n' }, { "path": "app/.well-known/ucp/route.ts", "kind": "text", "content": 'import { NextResponse } from "next/server";\n\n/**\n * UCP (Universal Commerce Protocol) manifest discovery endpoint.\n *\n * Agents \u2014 Claude, ChatGPT, Gemini, MCP clients \u2014 probe\n * `https://<your-domain>/.well-known/ucp` to learn what commerce\n * capabilities your storefront supports. We resolve the business\'s\n * UCP handle from the public key (via the storefront API), then forward\n * to Cimplify for the canonical manifest. Edge-cached for an hour\n * because capabilities change rarely.\n */\nconst UCP_API_BASE = "https://api.cimplify.io";\nconst STOREFRONT_API_BASE =\n process.env.NODE_ENV === "production"\n ? "https://storefronts.cimplify.io"\n : "http://127.0.0.1:8787";\n\nexport async function GET() {\n const publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY;\n if (!publicKey) {\n return NextResponse.json(\n {\n error: "NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY not set",\n remediation:\n "Set NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY in .env.local (and your deployment env).",\n },\n { status: 500 },\n );\n }\n\n try {\n // Step 1 \u2014 resolve the business handle from the public key.\n const businessResp = await fetch(`${STOREFRONT_API_BASE}/api/v1/business`, {\n headers: { "X-API-Key": publicKey, "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n });\n\n if (!businessResp.ok) {\n return NextResponse.json(\n { error: `Failed to resolve business: ${businessResp.status}` },\n { status: businessResp.status },\n );\n }\n\n const businessJson = await businessResp.json();\n const handle: string | undefined = businessJson?.data?.handle;\n if (!handle) {\n return NextResponse.json(\n { error: "Business has no handle configured" },\n { status: 500 },\n );\n }\n\n // Step 2 \u2014 fetch the canonical UCP manifest.\n const manifestResp = await fetch(\n `${UCP_API_BASE}/ucp/v1/${handle}/manifest`,\n {\n headers: { "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n },\n );\n\n if (!manifestResp.ok) {\n return NextResponse.json(\n { error: `Upstream UCP manifest fetch failed: ${manifestResp.status}` },\n { status: manifestResp.status },\n );\n }\n\n const manifest = await manifestResp.json();\n return NextResponse.json(manifest, {\n headers: {\n "Content-Type": "application/json",\n "Cache-Control": "public, max-age=3600, s-maxage=3600",\n },\n });\n } catch (error) {\n return NextResponse.json(\n {\n error: "Failed to fetch UCP manifest",\n detail: error instanceof Error ? error.message : String(error),\n },\n { status: 500 },\n );\n }\n}\n' }, { "path": "app/search/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { SearchClient } from "./search-client";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Search \u2014 ${brand.name}`,\n description: `Search ${brand.name} \u2014 products, collections, categories.`,\n};\n\nexport default function SearchPage() {\n return (\n <article className="max-w-7xl mx-auto px-6 sm:px-8 py-10">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Search\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Find anything.\n </h1>\n <Suspense fallback={<SearchSkeleton />}>\n <SearchClient />\n </Suspense>\n </article>\n );\n}\n\nfunction SearchSkeleton() {\n return (\n <div>\n <div className="h-12 w-full max-w-xl bg-muted rounded animate-pulse mb-8" />\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {Array.from({ length: 8 }).map((_, i) => (\n <div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </div>\n );\n}\n' }, { "path": "app/search/search-client.tsx", "kind": "text", "content": '"use client";\n\nimport { SearchPage } from "@cimplify/sdk/react";\n\nexport function SearchClient() {\n return <SearchPage />;\n}\n' }, { "path": "app/faq/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `FAQ \u2014 ${brand.name}`,\n description: "Delivery, pickup, custom cakes, allergens, payment \u2014 answers to the questions we hear most often.",\n};\n\nexport default function FaqPage() {\n const f = brand.faq;\n return (\n <article className="max-w-3xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {f.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-10 -tracking-[0.02em]">\n {f.title}\n </h1>\n <div className="space-y-12">\n {f.sections.map((section) => (\n <section key={section.title}>\n <h2 className="font-serif text-2xl font-semibold mb-5">{section.title}</h2>\n <dl className="space-y-6">\n {section.items.map((item) => (\n <div key={item.q}>\n <dt className="font-medium text-foreground mb-1.5">{item.q}</dt>\n <dd className="text-muted-foreground leading-relaxed">{item.a}</dd>\n </div>\n ))}\n </dl>\n </section>\n ))}\n </div>\n <p className="mt-12 pt-8 border-t border-border text-sm text-muted-foreground">\n {f.contactPrompt}{" "}\n <a\n href={`mailto:${f.contactEmail}`}\n className="text-primary font-semibold hover:underline"\n >\n {f.contactEmail}\n </a>{" "}\n and a real human will reply within 24 hours.\n </p>\n </article>\n );\n}\n' }, { "path": "app/robots.ts", "kind": "text", "content": 'import type { MetadataRoute } from "next";\nimport { getSiteUrl } from "@/lib/site-url";\n\nexport default async function robots(): Promise<MetadataRoute.Robots> {\n const siteUrl = await getSiteUrl();\n return {\n rules: [\n {\n userAgent: "*",\n allow: "/",\n disallow: ["/cart", "/checkout", "/orders/", "/api/"],\n },\n ],\n sitemap: `${siteUrl}/sitemap.xml`,\n host: siteUrl,\n };\n}\n' }, { "path": "app/track-order/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { TrackOrderForm } from "./track-order-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Track an order \u2014 ${brand.name}`,\n description: brand.trackOrder.body,\n};\n\nexport default function TrackOrderPage() {\n const t = brand.trackOrder;\n return (\n <article className="max-w-2xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {t.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,5vw,3rem)] font-semibold mb-4 -tracking-[0.02em]">\n {t.title}\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">{t.body}</p>\n <TrackOrderForm />\n </article>\n );\n}\n' }, { "path": "app/track-order/track-order-form.tsx", "kind": "text", "content": '"use client";\n\nimport { useState } from "react";\nimport { useRouter } from "next/navigation";\n\n/**\n * Guest order tracker. Routes /orders/<id> with the entered id; the\n * post-checkout confirmation page already lives at /orders/[id] so this\n * just sends the visitor there. Replace the redirect with a real lookup\n * (e.g. Cimplify orders API + email-match guard) for production.\n */\nexport function TrackOrderForm() {\n const router = useRouter();\n const [orderId, setOrderId] = useState("");\n const [email, setEmail] = useState("");\n\n return (\n <form\n onSubmit={(e) => {\n e.preventDefault();\n if (!orderId.trim()) return;\n router.push(`/orders/${encodeURIComponent(orderId.trim())}`);\n }}\n className="space-y-4 rounded-2xl border border-border bg-card p-6"\n >\n <div>\n <label\n htmlFor="orderId"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order number\n </label>\n <input\n id="orderId"\n name="orderId"\n required\n value={orderId}\n onChange={(e) => setOrderId(e.target.value)}\n placeholder="ord_abc123\u2026"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <div>\n <label\n htmlFor="email"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order email\n </label>\n <input\n id="email"\n name="email"\n type="email"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n placeholder="you@email.com"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <button\n type="submit"\n className="w-full inline-flex items-center justify-center px-5 py-3 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors"\n >\n Track order\n </button>\n </form>\n );\n}\n' }, { "path": "app/cart/page.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CartPage as SdkCartPage } from "@cimplify/sdk/react";\n\nexport default function CartPage() {\n const router = useRouter();\n return <SdkCartPage onCheckout={() => router.push("/checkout")} />;\n}\n' }, { "path": "app/llms.txt/route.ts", "kind": "text", "content": 'import { cacheTag, cacheLife } from "next/cache";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\n\nasync function buildLlmsTxt(SITE_URL: string): Promise<string> {\n "use cache";\n cacheTag(tags.products(), tags.categories(), tags.collections());\n cacheLife("hours");\n\n const client = getServerClient();\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n\n const products: Product[] = productsRes.ok ? productsRes.value.items : [];\n const categories = categoriesRes.ok ? categoriesRes.value : [];\n const collections = collectionsRes.ok ? collectionsRes.value : [];\n\n const lines: string[] = [];\n lines.push(`# ${brand.name}`);\n lines.push("");\n lines.push(`> ${brand.llms.summary}`);\n lines.push("");\n lines.push("## Browse");\n lines.push(`- [Home](${SITE_URL}/)`);\n lines.push(`- [Shop](${SITE_URL}/shop): Full menu with filter and sort.`);\n\n if (categories.length > 0) {\n lines.push("");\n lines.push("## Categories");\n for (const c of categories) {\n lines.push(\n `- [${c.name}](${SITE_URL}/categories/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (collections.length > 0) {\n lines.push("");\n lines.push("## Collections");\n for (const c of collections) {\n lines.push(\n `- [${c.name}](${SITE_URL}/collections/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (products.length > 0) {\n lines.push("");\n lines.push("## Products");\n for (const p of products) {\n const slug = p.slug ?? p.id;\n const price = `${brand.currency} ${p.default_price}`;\n const desc = p.description ? ` \u2014 ${p.description.replace(/\\s+/g, " ").slice(0, 200)}` : "";\n lines.push(`- [${p.name}](${SITE_URL}/shop?product=${slug}) (${price})${desc}`);\n }\n }\n\n lines.push("");\n lines.push("## Information");\n lines.push(`- [About](${SITE_URL}/about)`);\n lines.push(`- [FAQ](${SITE_URL}/faq)`);\n lines.push(`- [Terms of Service](${SITE_URL}/terms)`);\n lines.push(`- [Privacy Policy](${SITE_URL}/privacy)`);\n lines.push(`- [Sitemap (XML)](${SITE_URL}/sitemap.xml)`);\n lines.push("");\n lines.push("## Contact");\n lines.push(`- Email: ${brand.contact.email}`);\n lines.push(`- Phone: ${brand.contact.phone}`);\n lines.push(`- Address: ${brand.contact.address}`);\n\n return lines.join("\\n") + "\\n";\n}\n\n/**\n * `/llms.txt` \u2014 machine-readable site index for LLMs (per llmstxt.org).\n * Lets coding agents and chat assistants find products, categories, and\n * support pages without scraping HTML. Plain Markdown so it streams cheaply\n * into context windows.\n */\nexport async function GET(): Promise<Response> {\n const body = await buildLlmsTxt(await getSiteUrl());\n return new Response(body, {\n headers: {\n "Content-Type": "text/plain; charset=utf-8",\n "Cache-Control": "public, max-age=0, s-maxage=3600, stale-while-revalidate=86400",\n },\n });\n}\n' }, { "path": "app/sitemap.ts", "kind": "text", "content": 'import type { MetadataRoute } from "next";\nimport { getServerClient, type Product } from "@cimplify/sdk/server";\nimport { getSiteUrl } from "@/lib/site-url";\n\nconst STATIC_ROUTES: { path: string; priority: number; changeFrequency: "daily" | "weekly" | "monthly" }[] = [\n { path: "/", priority: 1.0, changeFrequency: "daily" },\n { path: "/shop", priority: 0.9, changeFrequency: "daily" },\n { path: "/about", priority: 0.5, changeFrequency: "monthly" },\n { path: "/faq", priority: 0.4, changeFrequency: "monthly" },\n { path: "/terms", priority: 0.2, changeFrequency: "monthly" },\n { path: "/privacy", priority: 0.2, changeFrequency: "monthly" },\n];\n\nexport default async function sitemap(): Promise<MetadataRoute.Sitemap> {\n const now = new Date();\n const siteUrl = await getSiteUrl();\n const client = getServerClient();\n\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n\n const products = productsRes.ok ? productsRes.value.items : [];\n const categories = categoriesRes.ok ? categoriesRes.value : [];\n const collections = collectionsRes.ok ? collectionsRes.value : [];\n\n const staticEntries: MetadataRoute.Sitemap = STATIC_ROUTES.map((r) => ({\n url: `${siteUrl}${r.path}`,\n lastModified: now,\n changeFrequency: r.changeFrequency,\n priority: r.priority,\n }));\n\n // Bakery uses a URL-driven product modal (`?product=<slug>` on the home or\n // shop pages). Emit those as deep-linkable URLs so search engines and LLMs\n // can index each product canonically.\n const productEntries: MetadataRoute.Sitemap = products.map((p: Product) => ({\n url: `${siteUrl}/shop?product=${encodeURIComponent(p.slug ?? p.id)}`,\n lastModified: p.updated_at ? new Date(p.updated_at) : now,\n changeFrequency: "weekly",\n priority: 0.7,\n }));\n\n const categoryEntries: MetadataRoute.Sitemap = categories.map((c) => ({\n url: `${siteUrl}/categories/${c.slug}`,\n lastModified: now,\n changeFrequency: "weekly",\n priority: 0.6,\n }));\n\n const collectionEntries: MetadataRoute.Sitemap = collections.map((c) => ({\n url: `${siteUrl}/collections/${c.slug}`,\n lastModified: now,\n changeFrequency: "weekly",\n priority: 0.6,\n }));\n\n return [...staticEntries, ...categoryEntries, ...collectionEntries, ...productEntries];\n}\n' }, { "path": "app/opensearch.xml/route.ts", "kind": "text", "content": 'import { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\n\n/**\n * OpenSearch description document \u2014 lets browsers add this site to the\n * address bar\'s search engine list. When users press Tab after typing the\n * domain, they get an inline search box that hits /search?q=...\n */\nexport async function GET(): Promise<Response> {\n const siteUrl = await getSiteUrl();\n const xml = `<?xml version="1.0" encoding="UTF-8"?>\n<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">\n <ShortName>${escapeXml(brand.shortName)}</ShortName>\n <Description>Search ${escapeXml(brand.name)}</Description>\n <InputEncoding>UTF-8</InputEncoding>\n <Url type="text/html" method="get" template="${siteUrl}/search?q={searchTerms}" />\n <Url type="application/opensearchdescription+xml" rel="self" template="${siteUrl}/opensearch.xml" />\n <moz:SearchForm>${siteUrl}/search</moz:SearchForm>\n</OpenSearchDescription>\n`;\n return new Response(xml, {\n headers: {\n "Content-Type": "application/opensearchdescription+xml; charset=utf-8",\n "Cache-Control": "public, max-age=86400",\n },\n });\n}\n\nfunction escapeXml(s: string): string {\n return s\n .replace(/&/g, "&amp;")\n .replace(/</g, "&lt;")\n .replace(/>/g, "&gt;")\n .replace(/"/g, "&quot;")\n .replace(/\'/g, "&apos;");\n}\n' }, { "path": "app/contact/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { ContactForm } from "./contact-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Contact \u2014 ${brand.name}`,\n description: brand.contactPage.body,\n};\n\nexport default function ContactPage() {\n const c = brand.contactPage;\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {c.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-4 -tracking-[0.02em]">\n {c.title}\n </h1>\n <p className="text-lg text-muted-foreground max-w-2xl mb-12 leading-relaxed">{c.body}</p>\n\n <div className="grid grid-cols-1 lg:grid-cols-[1.5fr_1fr] gap-10 items-start">\n <ContactForm reasons={c.reasons} />\n <aside className="space-y-5 lg:pl-10 lg:border-l border-border">\n <div>\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Direct lines\n </p>\n <ul className="space-y-3 list-none p-0 m-0">\n {c.directLines.map((line) => (\n <li key={line.label}>\n <p className="text-xs text-muted-foreground m-0">{line.label}</p>\n <a\n href={line.href}\n className="text-foreground hover:text-primary transition-colors"\n >\n {line.value}\n </a>\n </li>\n ))}\n </ul>\n </div>\n <div>\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Visit\n </p>\n <p className="text-foreground m-0">{brand.contact.address}</p>\n <p className="text-xs text-muted-foreground mt-1">{brand.contact.hours}</p>\n </div>\n </aside>\n </div>\n </article>\n );\n}\n' }, { "path": "app/contact/contact-form.tsx", "kind": "text", "content": `"use client";
3533
3533
 
3534
3534
  import { useState } from "react";
3535
3535
 
@@ -4034,7 +4034,7 @@ export const brand: Brand = {
4034
4034
  businessId: "bus_serene_spa",
4035
4035
  },
4036
4036
  };
4037
- ` }, { "path": "lib/cimplify-loader.ts", "kind": "text", "content": 'import type { ImageLoader } from "next/image";\n\nimport { assetUrl, isCimplifyAsset } from "@cimplify/sdk";\n\nconst cdnBase = process.env.NEXT_PUBLIC_CIMPLIFY_CDN_URL?.trim() || undefined;\n\nconst cimplifyImageLoader: ImageLoader = ({ src, width, quality }) => {\n if (isCimplifyAsset(src, cdnBase)) {\n return assetUrl(src, {\n base: cdnBase,\n w: width,\n quality,\n format: "auto",\n });\n }\n return src;\n};\n\nexport default cimplifyImageLoader;\n' }, { "path": "lib/site.config.ts", "kind": "text", "content": "/**\n * Canonical absolute URL. The platform overwrites this at build time with the\n * storefront's primary domain; the default only applies to local builds. It's\n * per-deploy config, not per-request, so it stays a static constant \u2014 keeping\n * the root layout prerenderable under `cacheComponents`.\n */\nexport const SITE_URL = \"https://example.com\";\n" }, { "path": "lib/cart.ts", "kind": "text", "content": '"use client";\n\nimport { useCart } from "@cimplify/sdk/react";\n\n/**\n * Reactive cart count for the header pill. Subscribes to the SDK cart\n * state \u2014 updates immediately on add / remove / quantity change.\n */\nexport function useCartCount(): { count: number } {\n const { itemCount } = useCart();\n return { count: itemCount };\n}\n' }, { "path": "lib/site-url.ts", "kind": "text", "content": 'import { SITE_URL } from "./site.config";\n\n/** Canonical absolute URL for this storefront. */\nexport async function getSiteUrl(): Promise<string> {\n return SITE_URL;\n}\n' }, { "path": "metadata.json", "kind": "text", "content": '{\n "id": "services",\n "name": "Services",\n "tagline": "Bookable-services storefront with calendar slots and staff profiles.",\n "industry": "services",\n "tags": ["services", "bookings", "appointments"],\n "stability": "stable",\n "schemaType": "BeautySalon",\n "mock": {\n "seedName": "services",\n "seedBusinessId": "bus_serene_spa"\n }\n}\n' }, { "path": "tsconfig.json", "kind": "text", "content": '{\n "compilerOptions": {\n "target": "ES2022",\n "lib": ["dom", "dom.iterable", "esnext"],\n "allowJs": false,\n "skipLibCheck": true,\n "strict": true,\n "noEmit": true,\n "esModuleInterop": true,\n "module": "esnext",\n "moduleResolution": "bundler",\n "resolveJsonModule": true,\n "isolatedModules": true,\n "jsx": "preserve",\n "incremental": true,\n "plugins": [{ "name": "next" }],\n "paths": {\n "@/*": ["./*"]\n }\n },\n "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts"],\n "exclude": ["node_modules"]\n}\n' }, { "path": "CLAUDE.md", "kind": "text", "content": "# CLAUDE.md\n\nThis project was scaffolded from a Cimplify storefront template (`cimplify init`).\n\n## Read these first\n\n- **`AGENTS.md`** at the project root \u2014 the file \u2194 `brand.X` field map for *this template's* industry, plus the architectural rules.\n- **`.claude/skills/cimplify-storefront/SKILL.md`** \u2014 the playbook for common tasks (rebrand, add a section, wire a Server Action, deploy, eject a component). Invoke it whenever you're asked to change content, palette, copy, or routing.\n\n## The two-line summary\n\n1. **Edit `lib/brand.ts`** for any visible string.\n2. **Edit `app/globals.css` `@theme` block** for any palette / radius / font change.\n\nThat covers ~95% of what merchants ask for. For anything else, follow the `cimplify-storefront` skill.\n\n## Don't do\n\n- Hardcode strings in pages or components.\n- Disable `cacheComponents: true` in `next.config.ts`.\n- Use `unstable_cache` \u2014 Next 16's canonical primitive is `'use cache'`.\n- Run `bun test` (Bun's `vi` shim is incomplete) \u2014 use `bun run test:run`.\n" }, { "path": "README.md", "kind": "text", "content": "# __STOREFRONT_NAME__\n\nA Cimplify storefront scaffolded from the **bakery** template \u2014 Next.js 16 (App Router), React 19, Tailwind v4. Warm food-styled palette, Playfair Display + Inter typography, designed for bakeries, p\xE2tisseries, and food businesses.\n\nFor a different industry, scaffold with `--template`:\n\n```bash\ncimplify init my-store --template retail # electronics / consumer goods\ncimplify init my-store --template bakery # default \u2014 food / p\xE2tisserie\n```\n\n## Run\n\n```bash\nbun install\nbun dev\n```\n\nTwo things start in parallel:\n\n- `cimplify-mock` \u2014 the Cimplify mock API on `http://127.0.0.1:8787` (Akua's Bakery seeded by default).\n- `next dev` \u2014 this storefront on `http://localhost:3000`.\n\nOpen the storefront in your browser. Edit `app/page.tsx` to start customising. The SDK ships full pages (`<CataloguePage>`, `<ProductPage>`, `<CartPage>`, `<CheckoutPage>`) and layouts (`<DefaultProductLayout>`, `<FoodProductLayout>`, etc.) that you can swap in.\n\n## Structure\n\n```\napp/\n layout.tsx # root layout, fonts, providers, header/footer/modal\n page.tsx # home\n shop/page.tsx # full catalogue (SDK <CataloguePage/>)\n collections/[slug]/ # collection landing\n categories/[slug]/ # category landing\n cart/page.tsx # SDK <CartPage/>\n checkout/page.tsx # SDK <CheckoutPage/>\n orders/[id]/page.tsx # post-checkout thank-you\n globals.css # Tailwind import + theme tokens\ncomponents/\n providers.tsx # CimplifyProvider client wrapper\n header.tsx, footer.tsx, hero.tsx\n store-product-card.tsx # SDK <ProductCard/> wired to URL-driven modal\n product-modal.tsx # ?product=<slug> deep-linkable modal\n collection-strip.tsx # horizontal product strip\n category-grid.tsx # SDK <CategoryGrid/> with router navigation\nlib/\n cart.ts # useCartCount() for the header pill\n```\n\n## Switch seed\n\n```bash\ncimplify-mock --seed restaurant # Mama's Kitchen\ncimplify-mock --seed retail # Currents Electronics\ncimplify-mock --seed services # Serene Spa\ncimplify-mock --seed grocery # FreshMart\n```\n\n## Go live\n\n```diff\n# .env.local\n- NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev\n+ NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=<your tenant key>\n```\n\nDeploy with `cimplify deploy --prod` after linking the project. See [`cimplify` CLI docs](https://www.cimplify.dev/docs/cli). `next.config.ts` already whitelists the SDK image hosts under `images.remotePatterns`.\n" }, { "path": ".env.example", "kind": "text", "content": "# Your tenant public key. Get it from the desk's Developers tab.\n# `cpk_live_*` / `cpk_test_*` keys auto-route same-origin requests\n# to hosted Cimplify in both dev and prod. Anything else (`mock-dev`,\n# empty) falls back to the local `cimplify dev` mock.\nNEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev\n\n# Optional. Forces a fixed canonical URL for sitemap.xml, robots.txt,\n# OpenGraph, and llms.txt. Leave unset and the storefront derives the\n# canonical from the request `Host` header automatically.\n# NEXT_PUBLIC_SITE_URL=\n" }], "storefront-bakery": [{ "path": "vitest.config.ts", "kind": "text", "content": 'import { defineConfig } from "vitest/config";\n\nexport default defineConfig({\n test: {\n environment: "node",\n include: ["__tests__/**/*.test.ts"],\n globals: false,\n },\n});\n' }, { "path": "__tests__/cart-flow.test.ts", "kind": "text", "content": 'import { createCartFlowSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateCartFlowSuite({ seed: brand.mock.seed, businessId: brand.mock.businessId });\n' }, { "path": "__tests__/brand.test.ts", "kind": "text", "content": 'import { createBrandSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateBrandSuite({ brand });\n' }, { "path": "__tests__/contract.test.ts", "kind": "text", "content": 'import { createContractSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateContractSuite({ seed: brand.mock.seed });\n' }, { "path": "AGENTS.md", "kind": "text", "content": "# AGENTS.md \u2014 Bakery storefront template\n\nIf you are an AI agent (Claude, Cursor, Aider, devin, \u2026) working on this storefront, **start here.**\n\n## TL;DR for rebranding\n\nTo rebrand this storefront end-to-end as a different merchant:\n\n1. **Edit `lib/brand.ts`.** Every visible string reads from this file.\n2. **Edit `app/globals.css`** \u2014 the `@theme { \u2026 }` block holds the design tokens (palette, radius, font references).\n3. **Edit `.env.local`** \u2014 set `NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY` (optional: `NEXT_PUBLIC_SITE_URL`).\n\nThat is the entire rebrand. Do not modify any `.tsx` file in `app/` or `components/` for content changes \u2014 they are design-only and read from `brand`.\n\n## Rules\n\n- \u2705 All content edits \u2192 `lib/brand.ts`\n- \u2705 All design-token edits \u2192 `app/globals.css` `@theme` block\n- \u2705 Server Components stay cached via `'use cache'` + `cacheTag(tags.X())` + `cacheLife(\"hours\")`\n- \u2705 Client islands (header active link, cart pill, product modal) live behind `<Suspense>`\n- \u274C Don't hardcode strings. If a string isn't in `brand`, add a new field to the `Brand` interface.\n- \u274C Don't disable `cacheComponents: true` in `next.config.ts`.\n- \u274C Don't use `unstable_cache` \u2014 Next 16 uses the `'use cache'` directive.\n\n## Page surface\n\n```\napp/\n page.tsx Home \u2014 hero, collection strips, category grid\n shop/page.tsx Full catalogue (SDK <CataloguePage/>)\n search/page.tsx Search (SDK <SearchPage/>)\n collections/[slug]/page.tsx Collection landing\n categories/[slug]/page.tsx Category landing\n\n cart/page.tsx SDK <CartPage/>\n checkout/page.tsx SDK <CheckoutPage/>\n orders/[id]/page.tsx Post-checkout confirmation (also target of /track-order)\n\n account/page.tsx <CimplifyAccount /> (iframe \u2014 handles sign-in, dashboard)\n account/orders/page.tsx <CimplifyAccount section=\"orders\" />\n account/addresses/page.tsx <CimplifyAccount section=\"addresses\" />\n account/settings/page.tsx <CimplifyAccount section=\"settings\" />\n login/page.tsx redirect \u2192 /account (iframe owns sign-in UI)\n signup/page.tsx redirect \u2192 /account (iframe owns sign-up UI)\n\n contact/page.tsx Contact form (Server Action wiring TODO; currently fakes submit)\n track-order/page.tsx Guest order lookup \u2192 /orders/[id]\n\n about/page.tsx Brand story\n faq/page.tsx FAQ\n shipping/page.tsx Standalone shipping policy\n returns/page.tsx Standalone returns policy\n accessibility/page.tsx Accessibility statement (WCAG 2.1 AA)\n terms/page.tsx Terms of Service\n privacy/page.tsx Privacy Policy\n\n sitemap-page/page.tsx Human-readable HTML sitemap\n sitemap.ts XML sitemap (search engines)\n robots.ts robots.txt\n llms.txt/route.ts LLM-friendly Markdown index\n opensearch.xml/route.ts Browser address-bar search description\n error.tsx, not-found.tsx Global boundaries\n```\n\n## File \u2194 brand-field map\n\n| File | Reads from `brand` |\n|---|---|\n| `app/layout.tsx` | identity, contact, socials, schemaType (Organization JSON-LD), `metadataBase` |\n| `app/page.tsx` | `brand.hero` (Hero badge / title / subtitle) |\n| `app/about/page.tsx` | `brand.about` |\n| `app/faq/page.tsx` | `brand.faq` |\n| `app/terms/page.tsx` | `brand.terms` |\n| `app/privacy/page.tsx` | `brand.privacy` |\n| `app/shipping/page.tsx` | `brand.shipping` |\n| `app/returns/page.tsx` | `brand.returns` |\n| `app/accessibility/page.tsx` | `brand.accessibility` |\n| `app/contact/page.tsx` | `brand.contactPage`, `brand.contact` |\n| `app/track-order/page.tsx` | `brand.trackOrder` |\n| `app/account/*/page.tsx` | `brand.account` (eyebrows + titles only \u2014 Cimplify Link iframe owns the UI) |\n| `app/login/page.tsx`, `app/signup/page.tsx` | `brand.account` (metadata only \u2014 both redirect to `/account`) |\n| `app/llms.txt/route.ts` | `brand.llms`, contact, currency |\n| `app/opensearch.xml/route.ts` | `brand.shortName`, `brand.name` |\n| `app/sitemap.ts`, `robots.ts` | derives URLs from product/category data |\n| `app/not-found.tsx` | `brand.name` (page title) |\n| `components/header.tsx` | `brand.shortName`, `brand.microTag`, `brand.header.nav` |\n| `components/footer.tsx` | `brand.footer`, `brand.contact`, `brand.socials` |\n\n## Bakery-specific notes\n\n- Product detail uses a **URL-driven modal** (`?product=<slug>`), not a static `/products/[slug]` route. Fits impulse-purchase food UX.\n- Schema.org `@type` is `Bakery` \u2014 set in `brand.schemaType`.\n- Mock seed: `--seed default`. To preview a different industry, edit `dev:mock` in `package.json`.\n\n## Known TODOs (not blocking, but worth knowing)\n\n- `app/contact/contact-form.tsx` fakes a successful submit. In production, wire a Server Action that calls `client.support.sendMessage(...)` from `@cimplify/sdk/server`.\n- `components/newsletter.tsx` (homepage strip) similarly fakes submit. Wire to a real list provider.\n- Hero / lookbook imagery is Unsplash placeholder \u2014 replace with merchant assets.\n\n## Customizing SDK components\n\nFor anything beyond `lib/brand.ts` + `app/globals.css`, lean on the SDK's prebuilt components rather than reinvent. **Especially for product customization** (variants, add-ons, bundles, composites, services with scheduling) \u2014 the SDK already gets price math, axis matching, and cart payload contracts right. Default to ejecting and restyling:\n\n```bash\ncimplify add cart-drawer\ncimplify add variant-selector\ncimplify add product-page\n```\n\nThen edit the local copy. **Don't change the cart payload shape** unless you're also touching the SDK mock + backend lens. Full ejection rules and the customizer contract are in the SDK-level [`AGENTS.md`](../../AGENTS.md) \u2192 \"Don't reinvent product customization\".\n\n## Quick start\n\n```bash\nbun install\nbun dev # boots mock + Next dev server\n```\n\nOpen <http://localhost:3000>.\n" }, { "path": "postcss.config.mjs", "kind": "text", "content": 'const config = {\n plugins: {\n "@tailwindcss/postcss": {},\n },\n};\n\nexport default config;\n' }, { "path": "components/cart-pill.tsx", "kind": "text", "content": '"use client";\n\nimport { useCartDrawer } from "@cimplify/sdk/react";\nimport { useCartCount } from "@/lib/cart";\n\n/**\n * Cart pill \u2014 dynamic island. Reads the live cart count via the SDK and\n * opens the side cart drawer on click (instead of navigating to /cart).\n * Wrap in `<Suspense fallback={<CartPillSkeleton/>}>` so the cached\n * header chrome streams without blocking on the cart fetch.\n */\nexport function CartPill() {\n const { count } = useCartCount();\n const { open } = useCartDrawer();\n return (\n <button\n type="button"\n onClick={open}\n aria-label={`Open cart, ${count} ${count === 1 ? "item" : "items"}`}\n className="inline-flex items-center gap-1.5 px-4 py-2.5 sm:py-2 rounded-full bg-foreground text-background text-xs font-semibold tracking-wide transition-transform hover:scale-105 cursor-pointer"\n >\n Cart \xB7 {count}\n </button>\n );\n}\n\nexport function CartPillSkeleton() {\n return (\n <span\n aria-hidden\n className="inline-flex items-center gap-1.5 px-4 py-2.5 sm:py-2 rounded-full bg-foreground/80 text-background text-xs font-semibold tracking-wide"\n >\n Cart \xB7 \u2026\n </span>\n );\n}\n' }, { "path": "components/collection-strip.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport type { Collection, Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "./store-product-card";\n\ninterface CollectionStripProps {\n collection: Collection;\n products: Product[];\n collectionHref?: string;\n}\n\n/**\n * Horizontal strip of products under a collection title. Cards come from the\n * SDK so every variant (Food, Bundle, Composite, Service, \u2026) renders\n * correctly; clicks open the shared URL-driven product modal.\n */\nexport function CollectionStrip({ collection, products, collectionHref }: CollectionStripProps) {\n if (products.length === 0) return null;\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 pt-12">\n <header className="grid grid-cols-[1fr_auto] items-end gap-4 mb-5">\n <h2 className="font-serif text-[28px] font-semibold m-0">{collection.name}</h2>\n {collectionHref && (\n <Link\n href={collectionHref}\n className="text-[13px] font-semibold text-primary hover:underline"\n >\n See all \u2192\n </Link>\n )}\n {collection.description && (\n <p className="col-span-full m-0 mt-1 text-sm text-muted-foreground">\n {collection.description}\n </p>\n )}\n </header>\n <div className="grid grid-flow-col auto-cols-[minmax(220px,1fr)] gap-4 overflow-x-auto snap-x snap-mandatory pb-2">\n {products.slice(0, 8).map((p) => (\n <div key={p.id} className="snap-start">\n <StoreProductCard product={p} />\n </div>\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "components/hero.tsx", "kind": "text", "content": 'interface HeroProps {\n badge?: string;\n title: string;\n subtitle?: string;\n}\n\nexport function Hero({ badge, title, subtitle }: HeroProps) {\n return (\n <section className="px-6 sm:px-8 py-16 text-center bg-gradient-to-b from-accent to-background">\n {badge && (\n <span className="inline-block mb-4 px-3.5 py-1.5 rounded-full bg-card border border-accent text-accent-foreground text-[11px] font-semibold uppercase tracking-[0.12em]">\n {badge}\n </span>\n )}\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold m-0 -tracking-[0.02em]">\n {title}\n </h1>\n {subtitle && (\n <p className="mx-auto mt-2 max-w-xl text-base text-muted-foreground">\n {subtitle}\n </p>\n )}\n </section>\n );\n}\n' }, { "path": "components/account-iframe.tsx", "kind": "text", "content": '"use client";\n\nimport { CimplifyAccount } from "@cimplify/sdk/react";\n\n/**\n * Cimplify Account portal \u2014 iframe-mounted UI hosted by Cimplify Link.\n * Handles sign-in, sign-up, OTP, addresses, payment methods, sessions,\n * and order history. The iframe owns auth state; we just choose which\n * `section` to land on.\n */\nexport function AccountIframe({ section }: { section?: string }) {\n return <CimplifyAccount section={section} />;\n}\n' }, { "path": "components/policy-page.tsx", "kind": "text", "content": 'import type { BrandPolicySection } from "@/lib/brand";\n\ninterface PolicyShape {\n eyebrow: string;\n title: string;\n lastUpdated?: string;\n sections: BrandPolicySection[];\n}\n\n/**\n * Shared layout for shipping / returns / accessibility / terms / privacy.\n * Reads a `{ eyebrow, title, lastUpdated, sections[] }` block from brand.\n */\nexport function PolicyPage({ policy }: { policy: PolicyShape }) {\n return (\n <article className="max-w-3xl mx-auto px-6 sm:px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {policy.eyebrow}\n </p>\n <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-2 -tracking-[0.02em]">\n {policy.title}\n </h1>\n {policy.lastUpdated && (\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {policy.lastUpdated}\n </p>\n )}\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {policy.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="text-2xl font-semibold mt-0">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "components/header.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport { Suspense } from "react";\nimport { NavLink } from "./nav-link";\nimport { CartPill, CartPillSkeleton } from "./cart-pill";\nimport { MobileNav } from "./mobile-nav";\nimport { brand } from "@/lib/brand";\n\n/**\n * Server-rendered header chrome. Brand mark + nav layout streams from the\n * cache; the active-link styling and live cart count are dynamic islands\n * mounted in their own Suspense boundaries so the chrome never blocks.\n */\nexport function Header() {\n return (\n <header className="sticky top-0 z-30 flex items-center justify-between px-6 sm:px-8 py-4 border-b border-border bg-background/90 backdrop-blur-md">\n <Link href="/" className="flex items-baseline gap-2">\n <span className="font-serif text-[22px] font-semibold -tracking-[0.02em]">\n {brand.shortName}\n </span>\n <span className="hidden sm:inline text-[11px] font-medium uppercase tracking-[0.12em] text-muted-foreground">\n {brand.microTag}\n </span>\n </Link>\n <div className="flex items-center gap-3 sm:gap-6">\n <nav className="hidden sm:flex items-center gap-6">\n {brand.header.nav.map((link) => (\n <Suspense key={link.href} fallback={<NavLinkFallback>{link.label}</NavLinkFallback>}>\n <NavLink href={link.href}>{link.label}</NavLink>\n </Suspense>\n ))}\n </nav>\n <Suspense fallback={<CartPillSkeleton />}>\n <CartPill />\n </Suspense>\n <div className="sm:hidden">\n <MobileNav />\n </div>\n </div>\n </header>\n );\n}\n\nfunction NavLinkFallback({ children }: { children: React.ReactNode }) {\n return (\n <span className="text-[13px] font-medium tracking-wide text-muted-foreground">\n {children}\n </span>\n );\n}\n' }, { "path": "components/product-modal.tsx", "kind": "text", "content": '"use client";\n\nimport { useEffect } from "react";\nimport Image from "next/image";\nimport { useRouter, useSearchParams, usePathname } from "next/navigation";\nimport { ProductSheet, useProduct, useCart } from "@cimplify/sdk/react";\n\n/**\n * URL-driven product modal. Reads `?product=<slug>` and renders the SDK\'s\n * `<ProductSheet/>` \u2014 vertical layout with image-on-top, then header, then\n * the variant/add-on/composite/bundle customizer. Closing the modal clears\n * the search param. Deep-linkable and survives reloads.\n */\nexport function ProductModal() {\n const router = useRouter();\n const pathname = usePathname();\n const searchParams = useSearchParams();\n const slug = searchParams?.get("product") ?? null;\n\n const { product } = useProduct(slug ?? "", { enabled: Boolean(slug) });\n const { addItem } = useCart();\n\n useEffect(() => {\n if (!slug) return;\n const original = document.body.style.overflow;\n document.body.style.overflow = "hidden";\n return () => {\n document.body.style.overflow = original;\n };\n }, [slug]);\n\n useEffect(() => {\n if (!slug) return;\n const onKey = (e: KeyboardEvent) => {\n if (e.key === "Escape") close();\n };\n window.addEventListener("keydown", onKey);\n return () => window.removeEventListener("keydown", onKey);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [slug]);\n\n if (!slug) return null;\n\n function close() {\n const next = new URLSearchParams(searchParams?.toString() ?? "");\n next.delete("product");\n const qs = next.toString();\n router.replace(qs ? `${pathname}?${qs}` : pathname, { scroll: false });\n }\n\n return (\n <div\n role="dialog"\n aria-modal="true"\n onClick={close}\n className="fixed inset-0 z-[100] flex items-end sm:items-center justify-center sm:p-6 bg-foreground/50 backdrop-blur-sm"\n >\n <div\n onClick={(e) => e.stopPropagation()}\n className="relative w-full sm:max-w-lg max-h-[92vh] overflow-auto bg-card sm:rounded-3xl rounded-t-3xl shadow-2xl"\n >\n <button\n onClick={close}\n aria-label="Close product details"\n className="absolute top-4 right-4 z-10 w-9 h-9 rounded-full bg-card border border-border text-sm cursor-pointer transition-colors hover:bg-muted"\n >\n \u2715\n </button>\n {product ? (\n <ProductSheet\n product={product}\n onClose={close}\n onAddToCart={async (p, qty, options) => {\n await addItem(p, qty, options);\n close();\n }}\n renderImage={({ src, alt, className }) => (\n <Image\n src={src}\n alt={alt}\n width={1200}\n height={900}\n className={className}\n style={{ width: "100%", height: "auto", objectFit: "cover" }}\n priority\n />\n )}\n classNames={{\n root: "p-6 sm:p-8 gap-4",\n image: "rounded-2xl overflow-hidden -mx-6 sm:-mx-8 -mt-6 sm:-mt-8 mb-2",\n header: "flex items-baseline justify-between gap-4",\n name: "font-serif text-2xl font-semibold m-0",\n price: "text-lg font-semibold text-primary",\n description: "text-sm text-muted-foreground leading-relaxed",\n customizer: "pt-2",\n }}\n />\n ) : (\n <div className="p-8 text-center text-muted-foreground">Loading\u2026</div>\n )}\n </div>\n </div>\n );\n}\n' }, { "path": "components/mobile-nav.tsx", "kind": "text", "content": '"use client";\n\nimport { useEffect, useState } from "react";\nimport Link from "next/link";\nimport { brand } from "@/lib/brand";\n\n/**\n * Hamburger button + slide-in drawer for narrow viewports. Header hides\n * its inline nav links below `sm` and renders this in their place; the\n * cart pill stays in the header chrome.\n */\nexport function MobileNav() {\n const [open, setOpen] = useState(false);\n\n useEffect(() => {\n if (!open) return;\n const onKey = (event: KeyboardEvent) => {\n if (event.key === "Escape") setOpen(false);\n };\n document.addEventListener("keydown", onKey);\n const previousOverflow = document.body.style.overflow;\n document.body.style.overflow = "hidden";\n return () => {\n document.removeEventListener("keydown", onKey);\n document.body.style.overflow = previousOverflow;\n };\n }, [open]);\n\n return (\n <>\n <button\n type="button"\n onClick={() => setOpen(true)}\n aria-label="Open menu"\n aria-expanded={open}\n aria-controls="mobile-nav-drawer"\n className="grid place-items-center w-11 h-11 -mr-2 rounded-md text-foreground hover:bg-muted transition-colors"\n >\n <svg\n width="20"\n height="20"\n viewBox="0 0 24 24"\n fill="none"\n stroke="currentColor"\n strokeWidth="2"\n strokeLinecap="round"\n strokeLinejoin="round"\n aria-hidden="true"\n >\n <line x1="3" y1="6" x2="21" y2="6" />\n <line x1="3" y1="12" x2="21" y2="12" />\n <line x1="3" y1="18" x2="21" y2="18" />\n </svg>\n </button>\n\n {open ? (\n <div className="fixed inset-0 z-50 sm:hidden">\n <button\n type="button"\n onClick={() => setOpen(false)}\n aria-label="Close menu"\n className="absolute inset-0 bg-background/80 backdrop-blur-sm"\n />\n <nav\n id="mobile-nav-drawer"\n aria-label="Mobile navigation"\n className="absolute inset-y-0 right-0 w-[85%] max-w-sm flex flex-col bg-background border-l border-border shadow-2xl"\n >\n <div className="flex items-center justify-between px-6 py-4 border-b border-border">\n <span className="text-xs font-semibold uppercase tracking-[0.16em] text-muted-foreground">\n Menu\n </span>\n <button\n type="button"\n onClick={() => setOpen(false)}\n aria-label="Close menu"\n className="grid place-items-center w-11 h-11 -mr-2 rounded-md text-foreground hover:bg-muted transition-colors"\n >\n <svg\n width="20"\n height="20"\n viewBox="0 0 24 24"\n fill="none"\n stroke="currentColor"\n strokeWidth="2"\n strokeLinecap="round"\n strokeLinejoin="round"\n aria-hidden="true"\n >\n <line x1="18" y1="6" x2="6" y2="18" />\n <line x1="6" y1="6" x2="18" y2="18" />\n </svg>\n </button>\n </div>\n <ul className="flex flex-col gap-1 px-3 py-4">\n {brand.header.nav.map((link) => (\n <li key={link.href}>\n <Link\n href={link.href}\n onClick={() => setOpen(false)}\n className="block px-3 py-3 rounded-md text-base font-medium text-foreground hover:bg-muted transition-colors"\n >\n {link.label}\n </Link>\n </li>\n ))}\n </ul>\n </nav>\n </div>\n ) : null}\n </>\n );\n}\n' }, { "path": "components/providers.tsx", "kind": "text", "content": '"use client";\n\nimport { useMemo, type ReactNode } from "react";\nimport { createCimplifyClient } from "@cimplify/sdk";\nimport { CimplifyProvider, CartDrawerProvider } from "@cimplify/sdk/react";\n\n// Same-origin client \u2014 every request goes through the Next.js rewrite in\n// next.config.ts, so no CORS preflight ever hits the browser.\nexport function Providers({ children }: { children: ReactNode }) {\n const client = useMemo(() => {\n const baseUrl =\n typeof window !== "undefined" ? window.location.origin : "http://127.0.0.1:8787";\n const publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY ?? "mock-dev";\n return createCimplifyClient({\n baseUrl,\n publicKey,\n suppressPublicKeyWarning: true,\n });\n }, []);\n\n return (\n <CimplifyProvider client={client}>\n <CartDrawerProvider>{children}</CartDrawerProvider>\n </CimplifyProvider>\n );\n}\n' }, { "path": "components/footer.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport { brand } from "@/lib/brand";\n\nconst ICONS: Record<string, React.ReactNode> = {\n instagram: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" aria-hidden className="w-5 h-5">\n <rect x="3" y="3" width="18" height="18" rx="5" />\n <circle cx="12" cy="12" r="4" />\n <circle cx="17.5" cy="6.5" r="0.75" fill="currentColor" />\n </svg>\n ),\n x: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M18 2h3l-7.5 8.6L22 22h-6.6l-5-6.5L4 22H1l8-9.2L1.4 2H8l4.6 6 5.4-6z" />\n </svg>\n ),\n tiktok: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M16 3v3.5a4.5 4.5 0 0 0 4.5 4.5V14a7.5 7.5 0 0 1-4.5-1.5V16a5 5 0 1 1-5-5v3a2 2 0 1 0 2 2V3z" />\n </svg>\n ),\n facebook: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M14 9V7a1 1 0 0 1 1-1h2V3h-3a4 4 0 0 0-4 4v2H8v3h2v9h3v-9h2.5l.5-3H13z" />\n </svg>\n ),\n youtube: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M22.5 6.5a2.6 2.6 0 0 0-1.8-1.8C19 4.2 12 4.2 12 4.2s-7 0-8.7.5A2.6 2.6 0 0 0 1.5 6.5C1 8.2 1 12 1 12s0 3.8.5 5.5a2.6 2.6 0 0 0 1.8 1.8C5 19.8 12 19.8 12 19.8s7 0 8.7-.5a2.6 2.6 0 0 0 1.8-1.8c.5-1.7.5-5.5.5-5.5s0-3.8-.5-5.5zM10 15.5v-7l6 3.5-6 3.5z" />\n </svg>\n ),\n linkedin: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M4 4h4v16H4zM6 2.5a2 2 0 1 0 0 4 2 2 0 0 0 0-4zM10 8h4v2.5h.1c.6-1.1 2-2.5 4-2.5 4 0 4.9 2.6 4.9 6V20h-4v-5c0-1.5-.5-3-2.3-3-1.7 0-2.5 1.3-2.5 3v5h-4z" />\n </svg>\n ),\n whatsapp: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M12 2a10 10 0 0 0-8.6 15l-1.4 5 5.2-1.4A10 10 0 1 0 12 2zm5 14.2c-.2.6-1.2 1.2-1.7 1.2-.5.1-1.1.1-1.7-.1-.4-.1-.9-.3-1.5-.6a8.4 8.4 0 0 1-3.7-3.4c-.7-1-1-1.8-1-2.5 0-.7.4-1.1.6-1.3.2-.2.4-.2.5-.2h.4c.1 0 .3 0 .4.3l.6 1.4c.1.2 0 .3 0 .4l-.3.4-.3.3c-.1.1-.2.2-.1.4.2.4.7 1.1 1.4 1.8.9.8 1.7 1.1 1.9 1.2.2.1.3.1.5-.1l.6-.7c.2-.2.3-.2.5-.1l1.4.7c.2.1.3.2.4.3.1.2.1.6 0 1z" />\n </svg>\n ),\n};\n\nconst FALLBACK_ICON = (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" aria-hidden className="w-5 h-5">\n <circle cx="12" cy="12" r="9" />\n </svg>\n);\n\nexport async function Footer() {\n "use cache";\n const year = new Date().getFullYear();\n return (\n <footer className="mt-12 px-6 sm:px-8 py-10 text-xs text-muted-foreground border-t border-border bg-card">\n <div className="max-w-7xl mx-auto">\n <div className="grid gap-10 md:grid-cols-[1.4fr_repeat(4,1fr)]">\n <div>\n <p className="font-serif text-2xl text-foreground m-0 mb-2">{brand.name}</p>\n <p className="leading-relaxed mb-4 max-w-sm">{brand.footer.blurb}</p>\n <address className="not-italic space-y-1">\n <p className="m-0">{brand.contact.address}</p>\n <p className="m-0">\n <a\n href={`tel:${brand.contact.phoneTel}`}\n className="hover:text-foreground transition-colors"\n >\n {brand.contact.phone}\n </a>\n </p>\n <p className="m-0">\n <a\n href={`mailto:${brand.contact.email}`}\n className="hover:text-foreground transition-colors"\n >\n {brand.contact.email}\n </a>\n </p>\n <p className="m-0">{brand.contact.hours}</p>\n </address>\n <div className="flex items-center gap-3 mt-5">\n {brand.socials.map((s) => (\n <a\n key={s.label}\n href={s.href}\n aria-label={s.label}\n target="_blank"\n rel="noopener noreferrer"\n className="inline-flex items-center justify-center w-9 h-9 rounded-full border border-border text-muted-foreground hover:text-foreground hover:border-foreground transition-colors"\n >\n {(s.icon && ICONS[s.icon]) ?? FALLBACK_ICON}\n </a>\n ))}\n </div>\n </div>\n {brand.footer.sitemap.map((section) => (\n <nav key={section.title} aria-labelledby={`footer-${section.title}`}>\n <p\n id={`footer-${section.title}`}\n className="font-semibold text-foreground mb-3 text-[13px] uppercase tracking-[0.08em]"\n >\n {section.title}\n </p>\n <ul className="space-y-2 m-0 p-0 list-none">\n {section.links.map((link) => (\n <li key={link.label}>\n <Link\n href={link.href}\n className="hover:text-foreground transition-colors"\n >\n {link.label}\n </Link>\n </li>\n ))}\n </ul>\n </nav>\n ))}\n </div>\n <div className="mt-12 pt-6 border-t border-border flex flex-col sm:flex-row items-center justify-between gap-3">\n <p className="m-0">\xA9 {year} {brand.name}. All rights reserved.</p>\n {brand.footer.poweredBy && (\n <p className="m-0 inline-flex items-center gap-1.5">\n <span className="text-muted-foreground/80">Powered by</span>\n <a\n href={brand.footer.poweredBy.href}\n target="_blank"\n rel="noopener noreferrer"\n aria-label={brand.footer.poweredBy.label}\n className="inline-flex items-center gap-1 font-serif text-foreground hover:text-primary transition-colors"\n >\n <span className="font-semibold tracking-tight">{brand.footer.poweredBy.label}</span>\n <svg\n viewBox="0 0 12 12"\n aria-hidden\n className="w-3 h-3 opacity-70"\n fill="none"\n stroke="currentColor"\n strokeWidth="1.5"\n >\n <path d="M3 9L9 3M9 3H4M9 3v5" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n </a>\n </p>\n )}\n </div>\n </div>\n </footer>\n );\n}\n' }, { "path": "components/store-product-card.tsx", "kind": "text", "content": '"use client";\n\nimport Link from "next/link";\nimport {\n CardVariant,\n FoodProductCard,\n RetailProductCard,\n WholesaleProductCard,\n DigitalProductCard,\n BundleProductCard,\n CompositeProductCard,\n StandardServiceCard,\n CompactServiceCard,\n ScheduleServiceCard,\n RentalServiceCard,\n AccommodationCard,\n LeaseServiceCard,\n SubscriptionCard,\n} from "@cimplify/sdk/react";\nimport type { CardLayoutProps } from "@cimplify/sdk/react";\nimport type { Product } from "@cimplify/sdk";\nimport { PRODUCT_TYPE, RENDER_HINT, DURATION_UNIT } from "@cimplify/sdk";\n\nconst RENTAL_UNITS = new Set<string>([DURATION_UNIT.Days, DURATION_UNIT.Hours]);\nconst LEASE_UNITS = new Set<string>([DURATION_UNIT.Weeks, DURATION_UNIT.Months, DURATION_UNIT.Years]);\n\nconst VARIANT_CARDS: Record<CardVariant, React.ComponentType<CardLayoutProps>> = {\n [CardVariant.Food]: FoodProductCard,\n [CardVariant.Retail]: RetailProductCard,\n [CardVariant.Wholesale]: WholesaleProductCard,\n [CardVariant.Digital]: DigitalProductCard,\n [CardVariant.Bundle]: BundleProductCard,\n [CardVariant.Composite]: CompositeProductCard,\n [CardVariant.Standard]: StandardServiceCard,\n [CardVariant.Compact]: CompactServiceCard,\n [CardVariant.Schedule]: ScheduleServiceCard,\n [CardVariant.Rental]: RentalServiceCard,\n [CardVariant.Accommodation]: AccommodationCard,\n [CardVariant.Lease]: LeaseServiceCard,\n [CardVariant.Subscription]: SubscriptionCard,\n};\n\nfunction resolveVariant(p: Product): CardVariant {\n if (p.type === PRODUCT_TYPE.Bundle) return CardVariant.Bundle;\n if (p.type === PRODUCT_TYPE.Composite) return CardVariant.Composite;\n if (p.quantity_pricing && p.quantity_pricing.length > 1) return CardVariant.Wholesale;\n if (p.type === PRODUCT_TYPE.Digital) return CardVariant.Digital;\n if (p.type === PRODUCT_TYPE.Service) {\n if (p.duration_unit && RENTAL_UNITS.has(p.duration_unit)) return CardVariant.Rental;\n if (p.duration_unit === DURATION_UNIT.Nights) return CardVariant.Accommodation;\n if (p.duration_unit && LEASE_UNITS.has(p.duration_unit)) return CardVariant.Lease;\n if (p.billing_plans && p.billing_plans.length > 0 && !p.duration_minutes) return CardVariant.Subscription;\n return CardVariant.Standard;\n }\n if (p.render_hint === RENDER_HINT.Food) return CardVariant.Food;\n return CardVariant.Retail;\n}\n\ninterface Props {\n product: Product;\n /** Override the auto-detected card variant. */\n variant?: CardVariant;\n}\n\n/**\n * Variant-aware product card that opens the URL-driven product modal on\n * click. Uses `next/link` with `?product=<slug>` so the link is statically\n * pre-renderable \u2014 no `useSearchParams` dependency means cards don\'t\n * become dynamic islands. The modal (which already reads `useSearchParams`\n * inside a Suspense boundary in the root layout) handles the rest.\n */\nexport function StoreProductCard({ product, variant }: Props) {\n const slug = product.slug || product.id;\n const Card = VARIANT_CARDS[variant ?? resolveVariant(product)];\n const href = `?product=${encodeURIComponent(slug)}`;\n\n return (\n <Card\n product={product}\n renderLink={({ className, children }) => (\n <Link href={href} scroll={false} prefetch={false} className={className}>\n {children}\n </Link>\n )}\n />\n );\n}\n' }, { "path": "components/json-ld.tsx", "kind": "text", "content": 'import { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\n\n/**\n * Streams the organization JSON-LD script tag in async so it doesn\'t block\n * the rest of the layout shell. `getSiteUrl` reads request headers, and\n * any await on dynamic data inside `RootLayout` makes Next 16 mark the\n * whole route as blocking.\n */\nexport async function OrganizationJsonLd(): Promise<React.ReactElement> {\n const siteUrl = await getSiteUrl();\n const ld = {\n "@context": "https://schema.org",\n "@type": brand.schemaType,\n name: brand.name,\n url: siteUrl,\n description: brand.description,\n email: brand.contact.email,\n telephone: brand.contact.phoneTel,\n address: {\n "@type": "PostalAddress",\n streetAddress: brand.contact.streetAddress,\n addressLocality: brand.contact.city,\n addressCountry: brand.contact.countryCode,\n },\n sameAs: brand.socials.map((s) => s.href),\n };\n return (\n <script\n type="application/ld+json"\n dangerouslySetInnerHTML={{ __html: JSON.stringify(ld) }}\n />\n );\n}\n' }, { "path": "components/nav-link.tsx", "kind": "text", "content": '"use client";\n\nimport Link from "next/link";\nimport { usePathname } from "next/navigation";\n\nexport function NavLink({ href, children }: { href: string; children: React.ReactNode }) {\n const pathname = usePathname();\n const active = pathname === href;\n return (\n <Link\n href={href}\n className={[\n "text-[13px] font-medium tracking-wide transition-colors",\n active ? "text-primary" : "text-muted-foreground hover:text-foreground",\n ].join(" ")}\n >\n {children}\n </Link>\n );\n}\n' }, { "path": "components/category-grid.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CategoryGrid as SdkCategoryGrid } from "@cimplify/sdk/react";\nimport type { Category } from "@cimplify/sdk";\n\n/**\n * Homepage category tiles \u2014 defers to the SDK\'s <CategoryGrid/> for layout\n * and accessibility, and routes selections to /categories/:slug.\n */\nexport function CategoryGrid({ categories }: { categories?: Category[] }) {\n const router = useRouter();\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 pt-14">\n <h2 className="font-serif text-[28px] font-semibold m-0 mb-5">Browse the menu</h2>\n <SdkCategoryGrid\n categories={categories}\n onSelect={(c) => router.push(`/categories/${c.slug}`)}\n classNames={{\n item: "flex flex-col gap-1 p-6 bg-card border border-border rounded-2xl cursor-pointer text-left transition-all hover:border-primary hover:-translate-y-0.5",\n name: "font-serif text-lg font-semibold text-foreground",\n description: "text-xs text-muted-foreground",\n count: "text-xs text-muted-foreground mt-1",\n }}\n />\n </section>\n );\n}\n' }, { "path": "components/cart-drawer.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CartDrawer as SdkCartDrawer } from "@cimplify/sdk/react";\n\n/**\n * Side-drawer cart. Auto-opens when an item is added (via\n * `<CartDrawerProvider>` in `providers.tsx`). The header\'s cart pill\n * also calls `useCartDrawer().open()` to reveal it on click.\n */\nexport function CartDrawer() {\n const router = useRouter();\n return <SdkCartDrawer onCheckout={() => router.push("/checkout")} />;\n}\n' }, { "path": "next.config.ts", "kind": "text", "content": 'import type { NextConfig } from "next";\n\n// Same-origin proxy target for the storefront API. The public key\n// decides: a real `cpk_live_\u2026` / `cpk_test_\u2026` only resolves against\n// hosted Cimplify, so browser cart writes go to the same place the\n// server catalogue reads come from. Anything else (`mock-dev`, empty)\n// falls back to the local `cimplify dev` mock in dev.\nconst publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY?.trim() ?? "";\nconst keyTargetsHostedCimplify =\n publicKey.startsWith("cpk_live_") || publicKey.startsWith("cpk_test_");\nconst STOREFRONT_URL =\n process.env.NODE_ENV === "production" || keyTargetsHostedCimplify\n ? "https://storefronts.cimplify.io"\n : "http://127.0.0.1:8787";\n\nif (STOREFRONT_URL === "http://127.0.0.1:8787") {\n console.warn(\n "[cimplify] next.config resolved the local storefront API URL; production deploys must run with NODE_ENV=production.",\n );\n}\n\nconst nextConfig: NextConfig = {\n // Enable Next 16\'s `cacheComponents` mode so we can use `\'use cache\'` +\n // `cacheTag` / `cacheLife` for SSR caching. Cached chrome streams first,\n // dynamic Suspense boundaries fill in when ready.\n cacheComponents: true,\n async redirects() {\n // Config-level so cacheComponents needn\'t prerender a redirect()-only page.\n return [\n { source: "/login", destination: "/account", permanent: false },\n { source: "/signup", destination: "/account", permanent: false },\n ];\n },\n async rewrites() {\n return [\n { source: "/api/v1/:path*", destination: `${STOREFRONT_URL}/api/v1/:path*` },\n { source: "/img/:path*", destination: `${STOREFRONT_URL}/img/:path*` },\n { source: "/elements/:path*", destination: `${STOREFRONT_URL}/elements/:path*` },\n { source: "/_mock/:path*", destination: `${STOREFRONT_URL}/_mock/:path*` },\n ];\n },\n images: {\n loader: "custom",\n loaderFile: "./lib/cimplify-loader.ts",\n remotePatterns: [\n { protocol: "http", hostname: "127.0.0.1", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "3000", pathname: "/img/**" },\n { protocol: "https", hostname: "loremflickr.com", pathname: "/**" },\n { protocol: "https", hostname: "images.unsplash.com", pathname: "/**" },\n { protocol: "https", hostname: "static-tmp.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "storefrontassetscdn.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "cdn.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "res.cloudinary.com", pathname: "/cimplify/**" },\n ],\n },\n};\n\nexport default nextConfig;\n' }, { "path": ".claude/skills/cimplify-storefront/SKILL.md", "kind": "text", "content": "---\nname: cimplify-storefront\ndescription: Build, customize, rebrand, or deploy a Cimplify-scaffolded storefront. Triggers when the user asks to create a storefront, rebrand a Cimplify template, change the palette, add a page, deploy to Cimplify, or works in a project containing `lib/brand.ts` and `next.config.ts` with `cacheComponents`.\n---\n\n# Cimplify Storefront skill\n\nYou're working on a project scaffolded from `cimplify init`. The architecture is opinionated and the rebrand surface is intentionally small. Read `AGENTS.md` at the project root for the file \u2194 brand-field map for *this template's* industry; this skill gives you the playbook that's the same across all six.\n\n## The contract \u2014 never break\n\n1. **`lib/brand.ts` is the only place for content edits.** Every visible string reads from this file. If a string isn't in `brand`, *add a field* to the `Brand` interface \u2014 don't hardcode it in a page or component.\n2. **`app/globals.css` `@theme { \u2026 }`** holds palette + radius + font references. To re-skin the entire site, edit only this block.\n3. **Server Components are cached** via `'use cache'` + `cacheTag(tags.X())` + `cacheLife(\"hours\")`. Don't downgrade them to `\"use client\"` to avoid an issue \u2014 fix the issue.\n4. **Client islands** (anything reading `useSearchParams`, `usePathname`, `useRouter`, `useState`) live behind `<Suspense>`.\n5. **`cacheComponents: true`** in `next.config.ts` is non-negotiable. Don't turn it off.\n6. **`bun run test:run` (vitest)** is the canonical test runner. `bun test` will show false failures because Bun's `vi` shim is incomplete.\n7. **Cart, checkout, orders stay client.** They're session-bound. Don't try to SSR them.\n\n## The brand-field schema (in `lib/brand.ts`)\n\n```\nidentity: name, shortName, microTag, description, schemaType, currency, locale\ncontact: address, streetAddress, city, countryCode, phone, phoneTel, email, privacyEmail, hours\nsocials: [{ label, href, icon }] // icon \u2208 instagram|x|tiktok|facebook|youtube|linkedin|whatsapp\nheader: { nav: [{ label, href }] }\nhero: { badge, title, subtitle, primaryCtaLabel, secondaryCtaLabel?, secondaryCtaHref? }\noptional: trustItems[]?, brandStrip?, promo?, tradeIn? // render conditionally\nnewsletter: { eyebrow, title, body, placeholder, submitLabel, successLabel }\nabout: { eyebrow, title, paragraphs[], sections[{ heading, body }] }\nfaq: { eyebrow, title, sections[{ title, items[{ q, a }] }], contactPrompt, contactEmail }\nterms,\nprivacy,\nshipping,\nreturns,\naccessibility: { eyebrow, title, lastUpdated, sections[{ heading, body | { intro, bullets[] } }] }\naccount: eyebrows + titles for /account, /login, /signup\ncontactPage: { eyebrow, title, body, reasons[], directLines[{ label, value, href }] }\ntrackOrder: { eyebrow, title, body }\nfooter: { blurb, sitemap[{ title, links[] }], poweredBy? }\nllms: { summary } // opens /llms.txt\nmock: { seed, businessId }\n```\n\n## Playbook \u2014 common tasks\n\n### Rebrand a storefront for a new merchant\n\n1. Edit `lib/brand.ts`. Replace every field with the merchant's content. Use the brief / context the user gave you.\n2. Edit `app/globals.css` `@theme { \u2026 }`. Change `--color-primary`, `--color-background`, `--color-foreground`, optionally `--radius`. Use OKLCH; if the brand only gave hex, convert.\n3. (Optional) Swap fonts in `app/layout.tsx` \u2014 `next/font/google` import + the variable wired into the `<html>` className.\n4. Set `.env.local`: `NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY` (optional: `NEXT_PUBLIC_SITE_URL`).\n\nDon't touch any other file. If the rebrand needs content not in the schema, add the field to `Brand` first, populate it, then read it from the page.\n\n### Add a new section / component\n\n1. Build it as a Server Component in `components/` (or a client island in `*-client.tsx` if interactive).\n2. Read merchant copy from `brand`. Add new fields to the `Brand` interface if needed.\n3. Wrap interactive bits in `<Suspense fallback={\u2026}>` so the cached chrome streams.\n4. Compose into the page.\n\n### Wire a Server Action that mutates data\n\n```ts\n\"use server\";\nimport { getServerClient, revalidateProducts } from \"@cimplify/sdk/server\";\n\nexport async function createProduct(input: ProductInput) {\n await getServerClient().catalogue.createProduct(input);\n revalidateProducts();\n}\n```\n\nAfter every mutation, call the matching `revalidate*` helper from `@cimplify/sdk/server`: `revalidateProducts`, `revalidateProduct(id)`, `revalidateCategories`, `revalidateCategory(id)`, `revalidateCollections`, `revalidateCollection(id)`, `revalidateBusiness`.\n\n### Add a Server Component data fetch\n\n```ts\nimport { cacheTag, cacheLife } from \"next/cache\";\nimport { getServerClient, tags } from \"@cimplify/sdk/server\";\n\nasync function getX() {\n \"use cache\";\n cacheTag(tags.products());\n cacheLife(\"hours\");\n const r = await getServerClient().catalogue.getProducts({ limit: 24 });\n if (!r.ok) throw new Error(r.error.message);\n return r.value.items;\n}\n```\n\n### Eject an SDK component for deeper customization\n\nTry `classNames`, `renderImage`, `renderLink`, slot props first. If those run out:\n\n```bash\ncimplify list\ncimplify add product-card --dir src/components/cimplify\n```\n\nOnce ejected, the file is yours. Hooks/types still come from `@cimplify/sdk`.\n\n### Deploy\n\n```bash\ncimplify login\ncimplify projects create my-store\ncimplify link <project-id>\ncimplify env push\ncimplify deploy --prod\ncimplify logs --follow\ncimplify domains add my-store.com\n```\n\n## Pitfalls \u2014 explicit \u274C list\n\n- \u274C Hardcoding any visible string in a page or component. Always `brand.X`.\n- \u274C Disabling `cacheComponents: true` to silence a warning. Fix the warning by wrapping the dynamic call in `<Suspense>`.\n- \u274C Using `unstable_cache` \u2014 Next 16's canonical primitive is `'use cache'` + `cacheTag` + `cacheLife`.\n- \u274C Bypassing `getServerClient()` and calling `createCimplifyClient` directly in a Server Component \u2014 loses per-request memoization.\n- \u274C Mutating data without calling the matching `revalidate*` helper.\n- \u274C Adding an `app/error.tsx` handler that calls `reset()` without logging \u2014 silently swallowed errors hide bugs.\n- \u274C Running `bun test` and reporting failures. Use `bun run test:run` (vitest).\n- \u274C Editing the per-template `AGENTS.md` to remove notes about TODOs (contact form, newsletter fake submits) \u2014 they're real.\n\n## Where things live\n\n| Need | Look here |\n|---|---|\n| Which page reads which `brand.X` field | `AGENTS.md` at project root |\n| Architectural rules | `AGENTS.md` at project root + this skill |\n| Running locally | `bun dev` (boots mock + Next together) |\n| Switching mock seed | edit `dev:mock` in `package.json` |\n| Full SDK reference | `docs/sdk/storefronts.md` in the Cimplify repo |\n\n## What to do when the user asks something out-of-scope\n\nIf the user asks for something the template doesn't support out of the box:\n\n1. **Check first** \u2014 is there an SDK component or hook that does it? `@cimplify/sdk/react` has 50+ components, 30+ hooks. Search `node_modules/@cimplify/sdk/dist/react.d.mts` if needed.\n2. **Compose from existing parts** before authoring new ones.\n3. **If genuinely missing** \u2014 build the new component, hoist its strings into `brand.ts`, document in `AGENTS.md`.\n\nDon't introduce competing patterns (a new caching strategy, a new auth flow, a new theming system). The template's opinions are intentional.\n" }, { "path": ".cursor/rules/cimplify-storefront.mdc", "kind": "binary", "content": "LS0tCmRlc2NyaXB0aW9uOiBDaW1wbGlmeSBzdG9yZWZyb250IOKAlCBhcmNoaXRlY3R1cmFsIHJ1bGVzIGFuZCByZWJyYW5kIGNvbnRyYWN0IGZvciBhIHByb2plY3Qgc2NhZmZvbGRlZCBmcm9tIGBjaW1wbGlmeSBpbml0YC4gVHJpZ2dlcnMgd2hlbiBmaWxlcyBsaWtlIGxpYi9icmFuZC50cywgYXBwL2dsb2JhbHMuY3NzLCBjb21wb25lbnRzL2hlYWRlci50c3gsIG9yIC5jaW1wbGlmeS9wcm9qZWN0Lmpzb24gYXJlIGluIHRoZSB3b3Jrc3BhY2UuCmdsb2JzOiBbImxpYi9icmFuZC50cyIsICJhcHAvKioiLCAiY29tcG9uZW50cy8qKiIsICIuZW52LmxvY2FsIiwgIm5leHQuY29uZmlnLnRzIl0KYWx3YXlzQXBwbHk6IHRydWUKLS0tCgojIENpbXBsaWZ5IHN0b3JlZnJvbnQKClRoaXMgcHJvamVjdCB3YXMgc2NhZmZvbGRlZCBmcm9tIGEgQ2ltcGxpZnkgdGVtcGxhdGUuIFJlYWQgYEFHRU5UUy5tZGAgYXQgdGhlIHByb2plY3Qgcm9vdCBmb3IgdGhlIGZpbGUg4oaUIGBicmFuZC5YYCBmaWVsZCBtYXAgYW5kIGAuY2xhdWRlL3NraWxscy9jaW1wbGlmeS1zdG9yZWZyb250L1NLSUxMLm1kYCBmb3IgdGhlIGZ1bGwgcGxheWJvb2suCgojIyBUaGUgY29udHJhY3QKCjEuICoqYGxpYi9icmFuZC50c2AqKiDigJQgZXZlcnkgdmlzaWJsZSBzdHJpbmcuIElmIHNvbWV0aGluZyBpc24ndCB0aGVyZSwgYWRkIGEgZmllbGQgdG8gdGhlIGBCcmFuZGAgaW50ZXJmYWNlOyBkb24ndCBoYXJkY29kZS4KMi4gKipgYXBwL2dsb2JhbHMuY3NzYCBgQHRoZW1lYCBibG9jayoqIOKAlCBwYWxldHRlLCByYWRpdXMsIGZvbnQgcmVmZXJlbmNlcy4KMy4gKipTZXJ2ZXIgQ29tcG9uZW50cyBhcmUgY2FjaGVkKiogdmlhIGAndXNlIGNhY2hlJ2AgKyBgY2FjaGVUYWcodGFncy5YKCkpYCArIGBjYWNoZUxpZmUoImhvdXJzIilgLiBEb24ndCBkb3duZ3JhZGUgdG8gY2xpZW50IHRvIHNpbGVuY2UgYSB3YXJuaW5nLgo0LiAqKkNsaWVudCBpc2xhbmRzKiogYmVoaW5kIGA8U3VzcGVuc2U+YC4KNS4gKipgY2FjaGVDb21wb25lbnRzOiB0cnVlYCoqIGluIGBuZXh0LmNvbmZpZy50c2AgaXMgbm9uLW5lZ290aWFibGUuCjYuIFVzZSAqKmBidW4gcnVuIHRlc3Q6cnVuYCoqICh2aXRlc3QpLCBub3QgYGJ1biB0ZXN0YC4KCiMjIERvbid0CgotIEhhcmRjb2RlIHZpc2libGUgc3RyaW5ncyBpbiBwYWdlcy9jb21wb25lbnRzLgotIFVzZSBgdW5zdGFibGVfY2FjaGVgIChOZXh0IDE2IHVzZXMgYCd1c2UgY2FjaGUnYCkuCi0gQnlwYXNzIGBnZXRTZXJ2ZXJDbGllbnQoKWAgKGxvc2VzIHBlci1yZXF1ZXN0IG1lbW9pemF0aW9uKS4KLSBNdXRhdGUgd2l0aG91dCBjYWxsaW5nIHRoZSBtYXRjaGluZyBgcmV2YWxpZGF0ZSpgIGhlbHBlciBmcm9tIGBAY2ltcGxpZnkvc2RrL3NlcnZlcmAuCg==" }, { "path": ".gitignore", "kind": "text", "content": "node_modules\n.next\nout\n.env\n.env.local\n.env*.local\n.DS_Store\n*.tsbuildinfo\nnext-env.d.ts\n" }, { "path": "app/about/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `About \u2014 ${brand.name}`,\n description: brand.description,\n};\n\nexport default function AboutPage() {\n const a = brand.about;\n const titleParts = a.title.split("\\n");\n return (\n <article className="max-w-3xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {a.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-6 -tracking-[0.02em]">\n {titleParts.map((line, i) => (\n <span key={i}>\n {line}\n {i < titleParts.length - 1 && <br />}\n </span>\n ))}\n </h1>\n <div className="prose prose-lg max-w-none space-y-5 text-foreground/90 leading-relaxed">\n {a.paragraphs.map((p, i) => (\n <p key={i}>{p}</p>\n ))}\n {a.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="font-serif text-3xl font-semibold mt-10 mb-3">{s.heading}</h2>\n <p>{s.body}</p>\n </div>\n ))}\n </div>\n </article>\n );\n}\n' }, { "path": "app/account/orders/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Orders \u2014 ${brand.name}`,\n};\n\nexport default function OrdersPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Your orders\n </h1>\n <AccountIframe section="orders" />\n </article>\n );\n}\n' }, { "path": "app/account/settings/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Settings \u2014 ${brand.name}`,\n};\n\nexport default function SettingsPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Settings\n </h1>\n <AccountIframe section="settings" />\n </article>\n );\n}\n' }, { "path": "app/account/addresses/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Addresses \u2014 ${brand.name}`,\n};\n\nexport default function AddressesPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Your addresses\n </h1>\n <AccountIframe section="addresses" />\n </article>\n );\n}\n' }, { "path": "app/account/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Account \u2014 ${brand.name}`,\n description: brand.account.signupSubtitle,\n};\n\nexport default function AccountPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {brand.account.accountEyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n {brand.account.accountTitle}\n </h1>\n <AccountIframe />\n </article>\n );\n}\n' }, { "path": "app/.well-known/ucp/route.ts", "kind": "text", "content": 'import { NextResponse } from "next/server";\n\n/**\n * UCP (Universal Commerce Protocol) manifest discovery endpoint.\n *\n * Agents \u2014 Claude, ChatGPT, Gemini, MCP clients \u2014 probe\n * `https://<your-domain>/.well-known/ucp` to learn what commerce\n * capabilities your storefront supports. We resolve the business\'s\n * UCP handle from the public key (via the storefront API), then forward\n * to Cimplify for the canonical manifest. Edge-cached for an hour\n * because capabilities change rarely.\n */\nconst UCP_API_BASE = "https://api.cimplify.io";\nconst STOREFRONT_API_BASE =\n process.env.NODE_ENV === "production"\n ? "https://storefronts.cimplify.io"\n : "http://127.0.0.1:8787";\n\nexport async function GET() {\n const publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY;\n if (!publicKey) {\n return NextResponse.json(\n {\n error: "NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY not set",\n remediation:\n "Set NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY in .env.local (and your deployment env).",\n },\n { status: 500 },\n );\n }\n\n try {\n // Step 1 \u2014 resolve the business handle from the public key.\n const businessResp = await fetch(`${STOREFRONT_API_BASE}/api/v1/business`, {\n headers: { "X-API-Key": publicKey, "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n });\n\n if (!businessResp.ok) {\n return NextResponse.json(\n { error: `Failed to resolve business: ${businessResp.status}` },\n { status: businessResp.status },\n );\n }\n\n const businessJson = await businessResp.json();\n const handle: string | undefined = businessJson?.data?.handle;\n if (!handle) {\n return NextResponse.json(\n { error: "Business has no handle configured" },\n { status: 500 },\n );\n }\n\n // Step 2 \u2014 fetch the canonical UCP manifest.\n const manifestResp = await fetch(\n `${UCP_API_BASE}/ucp/v1/${handle}/manifest`,\n {\n headers: { "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n },\n );\n\n if (!manifestResp.ok) {\n return NextResponse.json(\n { error: `Upstream UCP manifest fetch failed: ${manifestResp.status}` },\n { status: manifestResp.status },\n );\n }\n\n const manifest = await manifestResp.json();\n return NextResponse.json(manifest, {\n headers: {\n "Content-Type": "application/json",\n "Cache-Control": "public, max-age=3600, s-maxage=3600",\n },\n });\n } catch (error) {\n return NextResponse.json(\n {\n error: "Failed to fetch UCP manifest",\n detail: error instanceof Error ? error.message : String(error),\n },\n { status: 500 },\n );\n }\n}\n' }, { "path": "app/search/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { SearchClient } from "./search-client";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Search \u2014 ${brand.name}`,\n description: `Search ${brand.name} \u2014 products, collections, categories.`,\n};\n\nexport default function SearchPage() {\n return (\n <article className="max-w-7xl mx-auto px-6 sm:px-8 py-10">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Search\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Find anything.\n </h1>\n <Suspense fallback={<SearchSkeleton />}>\n <SearchClient />\n </Suspense>\n </article>\n );\n}\n\nfunction SearchSkeleton() {\n return (\n <div>\n <div className="h-12 w-full max-w-xl bg-muted rounded animate-pulse mb-8" />\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {Array.from({ length: 8 }).map((_, i) => (\n <div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </div>\n );\n}\n' }, { "path": "app/search/search-client.tsx", "kind": "text", "content": '"use client";\n\nimport { SearchPage } from "@cimplify/sdk/react";\n\nexport function SearchClient() {\n return <SearchPage />;\n}\n' }, { "path": "app/faq/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `FAQ \u2014 ${brand.name}`,\n description: "Delivery, pickup, custom cakes, allergens, payment \u2014 answers to the questions we hear most often.",\n};\n\nexport default function FaqPage() {\n const f = brand.faq;\n return (\n <article className="max-w-3xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {f.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-10 -tracking-[0.02em]">\n {f.title}\n </h1>\n <div className="space-y-12">\n {f.sections.map((section) => (\n <section key={section.title}>\n <h2 className="font-serif text-2xl font-semibold mb-5">{section.title}</h2>\n <dl className="space-y-6">\n {section.items.map((item) => (\n <div key={item.q}>\n <dt className="font-medium text-foreground mb-1.5">{item.q}</dt>\n <dd className="text-muted-foreground leading-relaxed">{item.a}</dd>\n </div>\n ))}\n </dl>\n </section>\n ))}\n </div>\n <p className="mt-12 pt-8 border-t border-border text-sm text-muted-foreground">\n {f.contactPrompt}{" "}\n <a\n href={`mailto:${f.contactEmail}`}\n className="text-primary font-semibold hover:underline"\n >\n {f.contactEmail}\n </a>{" "}\n and a real human will reply within 24 hours.\n </p>\n </article>\n );\n}\n' }, { "path": "app/robots.ts", "kind": "text", "content": 'import type { MetadataRoute } from "next";\nimport { getSiteUrl } from "@/lib/site-url";\n\nexport default async function robots(): Promise<MetadataRoute.Robots> {\n const siteUrl = await getSiteUrl();\n return {\n rules: [\n {\n userAgent: "*",\n allow: "/",\n disallow: ["/cart", "/checkout", "/orders/", "/api/"],\n },\n ],\n sitemap: `${siteUrl}/sitemap.xml`,\n host: siteUrl,\n };\n}\n' }, { "path": "app/track-order/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { TrackOrderForm } from "./track-order-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Track an order \u2014 ${brand.name}`,\n description: brand.trackOrder.body,\n};\n\nexport default function TrackOrderPage() {\n const t = brand.trackOrder;\n return (\n <article className="max-w-2xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {t.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,5vw,3rem)] font-semibold mb-4 -tracking-[0.02em]">\n {t.title}\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">{t.body}</p>\n <TrackOrderForm />\n </article>\n );\n}\n' }, { "path": "app/track-order/track-order-form.tsx", "kind": "text", "content": '"use client";\n\nimport { useState } from "react";\nimport { useRouter } from "next/navigation";\n\n/**\n * Guest order tracker. Routes /orders/<id> with the entered id; the\n * post-checkout confirmation page already lives at /orders/[id] so this\n * just sends the visitor there. Replace the redirect with a real lookup\n * (e.g. Cimplify orders API + email-match guard) for production.\n */\nexport function TrackOrderForm() {\n const router = useRouter();\n const [orderId, setOrderId] = useState("");\n const [email, setEmail] = useState("");\n\n return (\n <form\n onSubmit={(e) => {\n e.preventDefault();\n if (!orderId.trim()) return;\n router.push(`/orders/${encodeURIComponent(orderId.trim())}`);\n }}\n className="space-y-4 rounded-2xl border border-border bg-card p-6"\n >\n <div>\n <label\n htmlFor="orderId"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order number\n </label>\n <input\n id="orderId"\n name="orderId"\n required\n value={orderId}\n onChange={(e) => setOrderId(e.target.value)}\n placeholder="ord_abc123\u2026"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <div>\n <label\n htmlFor="email"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order email\n </label>\n <input\n id="email"\n name="email"\n type="email"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n placeholder="you@email.com"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <button\n type="submit"\n className="w-full inline-flex items-center justify-center px-5 py-3 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors"\n >\n Track order\n </button>\n </form>\n );\n}\n' }, { "path": "app/cart/page.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CartPage as SdkCartPage } from "@cimplify/sdk/react";\n\nexport default function CartPage() {\n const router = useRouter();\n return <SdkCartPage onCheckout={() => router.push("/checkout")} />;\n}\n' }, { "path": "app/llms.txt/route.ts", "kind": "text", "content": 'import { cacheTag, cacheLife } from "next/cache";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\n\nasync function buildLlmsTxt(SITE_URL: string): Promise<string> {\n "use cache";\n cacheTag(tags.products(), tags.categories(), tags.collections());\n cacheLife("hours");\n\n const client = getServerClient();\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n\n const products: Product[] = productsRes.ok ? productsRes.value.items : [];\n const categories = categoriesRes.ok ? categoriesRes.value : [];\n const collections = collectionsRes.ok ? collectionsRes.value : [];\n\n const lines: string[] = [];\n lines.push(`# ${brand.name}`);\n lines.push("");\n lines.push(`> ${brand.llms.summary}`);\n lines.push("");\n lines.push("## Browse");\n lines.push(`- [Home](${SITE_URL}/)`);\n lines.push(`- [Shop](${SITE_URL}/shop): Full menu with filter and sort.`);\n\n if (categories.length > 0) {\n lines.push("");\n lines.push("## Categories");\n for (const c of categories) {\n lines.push(\n `- [${c.name}](${SITE_URL}/categories/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (collections.length > 0) {\n lines.push("");\n lines.push("## Collections");\n for (const c of collections) {\n lines.push(\n `- [${c.name}](${SITE_URL}/collections/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (products.length > 0) {\n lines.push("");\n lines.push("## Products");\n for (const p of products) {\n const slug = p.slug ?? p.id;\n const price = `${brand.currency} ${p.default_price}`;\n const desc = p.description ? ` \u2014 ${p.description.replace(/\\s+/g, " ").slice(0, 200)}` : "";\n lines.push(`- [${p.name}](${SITE_URL}/shop?product=${slug}) (${price})${desc}`);\n }\n }\n\n lines.push("");\n lines.push("## Information");\n lines.push(`- [About](${SITE_URL}/about)`);\n lines.push(`- [FAQ](${SITE_URL}/faq)`);\n lines.push(`- [Terms of Service](${SITE_URL}/terms)`);\n lines.push(`- [Privacy Policy](${SITE_URL}/privacy)`);\n lines.push(`- [Sitemap (XML)](${SITE_URL}/sitemap.xml)`);\n lines.push("");\n lines.push("## Contact");\n lines.push(`- Email: ${brand.contact.email}`);\n lines.push(`- Phone: ${brand.contact.phone}`);\n lines.push(`- Address: ${brand.contact.address}`);\n\n return lines.join("\\n") + "\\n";\n}\n\n/**\n * `/llms.txt` \u2014 machine-readable site index for LLMs (per llmstxt.org).\n * Lets coding agents and chat assistants find products, categories, and\n * support pages without scraping HTML. Plain Markdown so it streams cheaply\n * into context windows.\n */\nexport async function GET(): Promise<Response> {\n const body = await buildLlmsTxt(await getSiteUrl());\n return new Response(body, {\n headers: {\n "Content-Type": "text/plain; charset=utf-8",\n "Cache-Control": "public, max-age=0, s-maxage=3600, stale-while-revalidate=86400",\n },\n });\n}\n' }, { "path": "app/sitemap.ts", "kind": "text", "content": 'import type { MetadataRoute } from "next";\nimport { getServerClient, type Product } from "@cimplify/sdk/server";\nimport { getSiteUrl } from "@/lib/site-url";\n\nconst STATIC_ROUTES: { path: string; priority: number; changeFrequency: "daily" | "weekly" | "monthly" }[] = [\n { path: "/", priority: 1.0, changeFrequency: "daily" },\n { path: "/shop", priority: 0.9, changeFrequency: "daily" },\n { path: "/about", priority: 0.5, changeFrequency: "monthly" },\n { path: "/faq", priority: 0.4, changeFrequency: "monthly" },\n { path: "/terms", priority: 0.2, changeFrequency: "monthly" },\n { path: "/privacy", priority: 0.2, changeFrequency: "monthly" },\n];\n\nexport default async function sitemap(): Promise<MetadataRoute.Sitemap> {\n const now = new Date();\n const siteUrl = await getSiteUrl();\n const client = getServerClient();\n\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n\n const products = productsRes.ok ? productsRes.value.items : [];\n const categories = categoriesRes.ok ? categoriesRes.value : [];\n const collections = collectionsRes.ok ? collectionsRes.value : [];\n\n const staticEntries: MetadataRoute.Sitemap = STATIC_ROUTES.map((r) => ({\n url: `${siteUrl}${r.path}`,\n lastModified: now,\n changeFrequency: r.changeFrequency,\n priority: r.priority,\n }));\n\n // Bakery uses a URL-driven product modal (`?product=<slug>` on the home or\n // shop pages). Emit those as deep-linkable URLs so search engines and LLMs\n // can index each product canonically.\n const productEntries: MetadataRoute.Sitemap = products.map((p: Product) => ({\n url: `${siteUrl}/shop?product=${encodeURIComponent(p.slug ?? p.id)}`,\n lastModified: p.updated_at ? new Date(p.updated_at) : now,\n changeFrequency: "weekly",\n priority: 0.7,\n }));\n\n const categoryEntries: MetadataRoute.Sitemap = categories.map((c) => ({\n url: `${siteUrl}/categories/${c.slug}`,\n lastModified: now,\n changeFrequency: "weekly",\n priority: 0.6,\n }));\n\n const collectionEntries: MetadataRoute.Sitemap = collections.map((c) => ({\n url: `${siteUrl}/collections/${c.slug}`,\n lastModified: now,\n changeFrequency: "weekly",\n priority: 0.6,\n }));\n\n return [...staticEntries, ...categoryEntries, ...collectionEntries, ...productEntries];\n}\n' }, { "path": "app/opensearch.xml/route.ts", "kind": "text", "content": 'import { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\n\n/**\n * OpenSearch description document \u2014 lets browsers add this site to the\n * address bar\'s search engine list. When users press Tab after typing the\n * domain, they get an inline search box that hits /search?q=...\n */\nexport async function GET(): Promise<Response> {\n const siteUrl = await getSiteUrl();\n const xml = `<?xml version="1.0" encoding="UTF-8"?>\n<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">\n <ShortName>${escapeXml(brand.shortName)}</ShortName>\n <Description>Search ${escapeXml(brand.name)}</Description>\n <InputEncoding>UTF-8</InputEncoding>\n <Url type="text/html" method="get" template="${siteUrl}/search?q={searchTerms}" />\n <Url type="application/opensearchdescription+xml" rel="self" template="${siteUrl}/opensearch.xml" />\n <moz:SearchForm>${siteUrl}/search</moz:SearchForm>\n</OpenSearchDescription>\n`;\n return new Response(xml, {\n headers: {\n "Content-Type": "application/opensearchdescription+xml; charset=utf-8",\n "Cache-Control": "public, max-age=86400",\n },\n });\n}\n\nfunction escapeXml(s: string): string {\n return s\n .replace(/&/g, "&amp;")\n .replace(/</g, "&lt;")\n .replace(/>/g, "&gt;")\n .replace(/"/g, "&quot;")\n .replace(/\'/g, "&apos;");\n}\n' }, { "path": "app/contact/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { ContactForm } from "./contact-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Contact \u2014 ${brand.name}`,\n description: brand.contactPage.body,\n};\n\nexport default function ContactPage() {\n const c = brand.contactPage;\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {c.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-4 -tracking-[0.02em]">\n {c.title}\n </h1>\n <p className="text-lg text-muted-foreground max-w-2xl mb-12 leading-relaxed">{c.body}</p>\n\n <div className="grid grid-cols-1 lg:grid-cols-[1.5fr_1fr] gap-10 items-start">\n <ContactForm reasons={c.reasons} />\n <aside className="space-y-5 lg:pl-10 lg:border-l border-border">\n <div>\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Direct lines\n </p>\n <ul className="space-y-3 list-none p-0 m-0">\n {c.directLines.map((line) => (\n <li key={line.label}>\n <p className="text-xs text-muted-foreground m-0">{line.label}</p>\n <a\n href={line.href}\n className="text-foreground hover:text-primary transition-colors"\n >\n {line.value}\n </a>\n </li>\n ))}\n </ul>\n </div>\n <div>\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Visit\n </p>\n <p className="text-foreground m-0">{brand.contact.address}</p>\n <p className="text-xs text-muted-foreground mt-1">{brand.contact.hours}</p>\n </div>\n </aside>\n </div>\n </article>\n );\n}\n' }, { "path": "app/contact/contact-form.tsx", "kind": "text", "content": `"use client";
4037
+ ` }, { "path": "lib/cimplify-loader.ts", "kind": "text", "content": 'import type { ImageLoader } from "next/image";\n\nimport { assetUrl, isCimplifyAsset } from "@cimplify/sdk";\n\nconst cdnBase = process.env.NEXT_PUBLIC_CIMPLIFY_CDN_URL?.trim() || undefined;\n\nconst cimplifyImageLoader: ImageLoader = ({ src, width, quality }) => {\n if (isCimplifyAsset(src, cdnBase)) {\n return assetUrl(src, {\n base: cdnBase,\n w: width,\n quality,\n format: "auto",\n });\n }\n return src;\n};\n\nexport default cimplifyImageLoader;\n' }, { "path": "lib/site.config.ts", "kind": "text", "content": "/**\n * Canonical absolute URL. The platform overwrites this at build time with the\n * storefront's primary domain; the default only applies to local builds. It's\n * per-deploy config, not per-request, so it stays a static constant \u2014 keeping\n * the root layout prerenderable under `cacheComponents`.\n */\nexport const SITE_URL = \"https://example.com\";\n" }, { "path": "lib/cart.ts", "kind": "text", "content": '"use client";\n\nimport { useCart } from "@cimplify/sdk/react";\n\n/**\n * Reactive cart count for the header pill. Subscribes to the SDK cart\n * state \u2014 updates immediately on add / remove / quantity change.\n */\nexport function useCartCount(): { count: number } {\n const { itemCount } = useCart();\n return { count: itemCount };\n}\n' }, { "path": "lib/site-url.ts", "kind": "text", "content": 'import { SITE_URL } from "./site.config";\n\n/** Canonical absolute URL for this storefront. */\nexport async function getSiteUrl(): Promise<string> {\n return SITE_URL;\n}\n' }, { "path": "metadata.json", "kind": "text", "content": '{\n "id": "services",\n "name": "Services",\n "tagline": "Bookable-services storefront with calendar slots and staff profiles.",\n "industry": "services",\n "tags": ["services", "bookings", "appointments"],\n "stability": "stable",\n "schemaType": "BeautySalon",\n "mock": {\n "seedName": "services",\n "seedBusinessId": "bus_serene_spa"\n }\n}\n' }, { "path": "tsconfig.json", "kind": "text", "content": '{\n "compilerOptions": {\n "target": "ES2022",\n "lib": ["dom", "dom.iterable", "esnext"],\n "allowJs": false,\n "skipLibCheck": true,\n "strict": true,\n "noEmit": true,\n "esModuleInterop": true,\n "module": "esnext",\n "moduleResolution": "bundler",\n "resolveJsonModule": true,\n "isolatedModules": true,\n "jsx": "preserve",\n "incremental": true,\n "plugins": [{ "name": "next" }],\n "paths": {\n "@/*": ["./*"]\n }\n },\n "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts"],\n "exclude": ["node_modules"]\n}\n' }, { "path": "CLAUDE.md", "kind": "text", "content": "# CLAUDE.md\n\nThis project was scaffolded from a Cimplify storefront template (`cimplify init`).\n\n## Read these first\n\n- **`AGENTS.md`** at the project root \u2014 the file \u2194 `brand.X` field map for *this template's* industry, plus the architectural rules.\n- **`.claude/skills/cimplify-storefront/SKILL.md`** \u2014 the playbook for common tasks (rebrand, add a section, wire a Server Action, deploy, eject a component). Invoke it whenever you're asked to change content, palette, copy, or routing.\n\n## The two-line summary\n\n1. **Edit `lib/brand.ts`** for any visible string.\n2. **Edit `app/globals.css` `@theme` block** for any palette / radius / font change.\n\nThat covers ~95% of what merchants ask for. For anything else, follow the `cimplify-storefront` skill.\n\n## Don't do\n\n- Hardcode strings in pages or components.\n- Disable `cacheComponents: true` in `next.config.ts`.\n- Use `unstable_cache` \u2014 Next 16's canonical primitive is `'use cache'`.\n- Run `bun test` (Bun's `vi` shim is incomplete) \u2014 use `bun run test:run`.\n" }, { "path": "README.md", "kind": "text", "content": "# __STOREFRONT_NAME__\n\nA Cimplify storefront scaffolded from the **bakery** template \u2014 Next.js 16 (App Router), React 19, Tailwind v4. Warm food-styled palette, Playfair Display + Inter typography, designed for bakeries, p\xE2tisseries, and food businesses.\n\nFor a different industry, scaffold with `--template`:\n\n```bash\ncimplify init my-store --template retail # electronics / consumer goods\ncimplify init my-store --template bakery # default \u2014 food / p\xE2tisserie\n```\n\n## Run\n\n```bash\nbun install\nbun dev\n```\n\nTwo things start in parallel:\n\n- `cimplify-mock` \u2014 the Cimplify mock API on `http://127.0.0.1:8787` (Akua's Bakery seeded by default).\n- `next dev` \u2014 this storefront on `http://localhost:3000`.\n\nOpen the storefront in your browser. Edit `app/page.tsx` to start customising. The SDK ships full pages (`<CataloguePage>`, `<ProductPage>`, `<CartPage>`, `<CheckoutPage>`) and layouts (`<DefaultProductLayout>`, `<FoodProductLayout>`, etc.) that you can swap in.\n\n## Structure\n\n```\napp/\n layout.tsx # root layout, fonts, providers, header/footer/modal\n page.tsx # home\n shop/page.tsx # full catalogue (SDK <CataloguePage/>)\n collections/[slug]/ # collection landing\n categories/[slug]/ # category landing\n cart/page.tsx # SDK <CartPage/>\n checkout/page.tsx # SDK <CheckoutPage/>\n orders/[id]/page.tsx # post-checkout thank-you\n globals.css # Tailwind import + theme tokens\ncomponents/\n providers.tsx # CimplifyProvider client wrapper\n header.tsx, footer.tsx, hero.tsx\n store-product-card.tsx # SDK <ProductCard/> wired to URL-driven modal\n product-modal.tsx # ?product=<slug> deep-linkable modal\n collection-strip.tsx # horizontal product strip\n category-grid.tsx # SDK <CategoryGrid/> with router navigation\nlib/\n cart.ts # useCartCount() for the header pill\n```\n\n## Switch seed\n\n```bash\ncimplify-mock --seed restaurant # Mama's Kitchen\ncimplify-mock --seed retail # Currents Electronics\ncimplify-mock --seed services # Serene Spa\ncimplify-mock --seed grocery # FreshMart\n```\n\n## Go live\n\n```diff\n# .env.local\n- NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev\n+ NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=<your tenant key>\n```\n\nDeploy with `cimplify deploy --prod` after linking the project. See [`cimplify` CLI docs](https://www.cimplify.dev/docs/cli). `next.config.ts` already whitelists the SDK image hosts under `images.remotePatterns`.\n" }, { "path": "bun.lock", "kind": "binary", "content": "ewogICJsb2NrZmlsZVZlcnNpb24iOiAxLAogICJjb25maWdWZXJzaW9uIjogMSwKICAid29ya3NwYWNlcyI6IHsKICAgICIiOiB7CiAgICAgICJuYW1lIjogIl9fU1RPUkVGUk9OVF9OQU1FX18iLAogICAgICAiZGVwZW5kZW5jaWVzIjogewogICAgICAgICJAY2ltcGxpZnkvc2RrIjogIl4wLjQ4LjIiLAogICAgICAgICJuZXh0IjogIl4xNi4yLjQiLAogICAgICAgICJyZWFjdCI6ICJeMTkuMC4wIiwKICAgICAgICAicmVhY3QtZG9tIjogIl4xOS4wLjAiLAogICAgICB9LAogICAgICAiZGV2RGVwZW5kZW5jaWVzIjogewogICAgICAgICJAdGFpbHdpbmRjc3MvcG9zdGNzcyI6ICJeNC4yLjQiLAogICAgICAgICJAdHlwZXMvbm9kZSI6ICJeMjIuMTAuMCIsCiAgICAgICAgIkB0eXBlcy9yZWFjdCI6ICJeMTkuMC4wIiwKICAgICAgICAiQHR5cGVzL3JlYWN0LWRvbSI6ICJeMTkuMC4wIiwKICAgICAgICAiY29uY3VycmVudGx5IjogIl45LjAuMCIsCiAgICAgICAgInRhaWx3aW5kY3NzIjogIl40LjIuNCIsCiAgICAgICAgInR5cGVzY3JpcHQiOiAiXjUuOS4zIiwKICAgICAgICAidml0ZXN0IjogIl40LjEuNSIsCiAgICAgIH0sCiAgICB9LAogIH0sCiAgInBhY2thZ2VzIjogewogICAgIkBhbGxvYy9xdWljay1scnUiOiBbIkBhbGxvYy9xdWljay1scnVANS4yLjAiLCAiIiwge30sICJzaGE1MTItVXJjQUJCKzRiVXJGQUJ3Ymx1VElCRXJYd3Zic1UvVjdUWldmbWJnSmZia3dpQnV6aVM5Z3hkT0RVeXVpZWNmZEdRODVqZ2xNVzZqdVMzK3o1VHNLTHc9PSJdLAoKICAgICJAYmFiZWwvcnVudGltZSI6IFsiQGJhYmVsL3J1bnRpbWVANy4yOS4yIiwgIiIsIHt9LCAic2hhNTEyLUppRFNoSDQ1ektIV3lHZTRaTlZSckNqQno4Tmg5VE1tWkcxa2g0UVRLOGhDQlRXQmk4RGEraTdzMWZKdzcvbFlwTTRjY2VwU05mcXpaL1F2QUJCaTVnPT0iXSwKCiAgICAiQGJhc2UtdWkvcmVhY3QiOiBbIkBiYXNlLXVpL3JlYWN0QDEuNS4wIiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIkBiYWJlbC9ydW50aW1lIjogIl43LjI5LjIiLCAiQGJhc2UtdWkvdXRpbHMiOiAiMC4yLjkiLCAiQGZsb2F0aW5nLXVpL3JlYWN0LWRvbSI6ICJeMi4xLjgiLCAiQGZsb2F0aW5nLXVpL3V0aWxzIjogIl4wLjIuMTEiLCAidXNlLXN5bmMtZXh0ZXJuYWwtc3RvcmUiOiAiXjEuNi4wIiB9LCAicGVlckRlcGVuZGVuY2llcyI6IHsgIkBkYXRlLWZucy90eiI6ICJeMS4yLjAiLCAiQHR5cGVzL3JlYWN0IjogIl4xNyB8fCBeMTggfHwgXjE5IiwgImRhdGUtZm5zIjogIl40LjAuMCIsICJyZWFjdCI6ICJeMTcgfHwgXjE4IHx8IF4xOSIsICJyZWFjdC1kb20iOiAiXjE3IHx8IF4xOCB8fCBeMTkiIH0sICJvcHRpb25hbFBlZXJzIjogWyJAZGF0ZS1mbnMvdHoiLCAiQHR5cGVzL3JlYWN0IiwgImRhdGUtZm5zIl0gfSwgInNoYTUxMi16MWdTQWxjZWQxeVkraU0rbUhERXRJa0Q4VUkzRWJzNTJNdUJQeHZWNmY1aFJ1dGsreHZDSC93dUI3aERxRHpLOUpHNUZvTXo1bmhycXRTczF3anQxQT09Il0sCgogICAgIkBiYXNlLXVpL3V0aWxzIjogWyJAYmFzZS11aS91dGlsc0AwLjIuOSIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAYmFiZWwvcnVudGltZSI6ICJeNy4yOS4yIiwgIkBmbG9hdGluZy11aS91dGlscyI6ICJeMC4yLjExIiwgInJlc2VsZWN0IjogIl41LjEuMSIsICJ1c2Utc3luYy1leHRlcm5hbC1zdG9yZSI6ICJeMS42LjAiIH0sICJwZWVyRGVwZW5kZW5jaWVzIjogeyAiQHR5cGVzL3JlYWN0IjogIl4xNyB8fCBeMTggfHwgXjE5IiwgInJlYWN0IjogIl4xNyB8fCBeMTggfHwgXjE5IiwgInJlYWN0LWRvbSI6ICJeMTcgfHwgXjE4IHx8IF4xOSIgfSwgIm9wdGlvbmFsUGVlcnMiOiBbIkB0eXBlcy9yZWFjdCJdIH0sICJzaGE1MTIteC9QRERDWXpvcVBwanJkeWIzVmN5eWxUSTJJalVYRXRZREdpNWZvaDdLc25tTkpJSWFWd0EyR0xnREgxZHBzMUdnWGlKYkE2MGhNK0F5dVRmUXpJdnc9PSJdLAoKICAgICJAY2ltcGxpZnkvc2RrIjogWyJAY2ltcGxpZnkvc2RrQDAuNDguMiIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAYmFzZS11aS9yZWFjdCI6ICJeMS40LjEiLCAiY2xzeCI6ICJeMi4xLjEiLCAiZGF0ZS1mbnMiOiAiXjQuMS4wIiwgImxpYnBob25lbnVtYmVyLWpzIjogIjEuMTIuNDEiLCAicmVhY3QtZGF5LXBpY2tlciI6ICJeOS4xNC4wIiwgInRhaWx3aW5kLW1lcmdlIjogIl4zLjUuMCIsICJ6b2QiOiAiXjQuNC4zIiB9LCAicGVlckRlcGVuZGVuY2llcyI6IHsgIkBwYXlzdGFjay9pbmxpbmUtanMiOiAiXjIuMjIuOCIsICJtc3ciOiAiPj0yLjAuMCIsICJyZWFjdCI6ICI+PTE3LjAuMCIsICJ2aXRlc3QiOiAiPj0yLjAuMCIgfSwgIm9wdGlvbmFsUGVlcnMiOiBbIkBwYXlzdGFjay9pbmxpbmUtanMiLCAibXN3IiwgInJlYWN0IiwgInZpdGVzdCJdLCAiYmluIjogeyAiY2ltcGxpZnktbW9jayI6ICJkaXN0L21vY2svY2xpLm1qcyIgfSB9LCAic2hhNTEyLTZJaDhNNkxZVkhDdU84c2JnZWc3VmdQTUJIWHZNK2NodFkzLzJWZkdWRWZFeU5UY0d4UzZlcDFOMDdNS21tTG5xQWNMZTZteFQwbVFiL2h0cldQZ1hBPT0iXSwKCiAgICAiQGRhdGUtZm5zL3R6IjogWyJAZGF0ZS1mbnMvdHpAMS40LjEiLCAiIiwge30sICJzaGE1MTItUDVMVU5odGJqNllmSTNpSmp3NUVMOWVVQUc2T2l0RDBXM2ZXUWNwUWpEUmMvUUlzTDB0Uk51TzFQY0R2UGNjV0wxZlNUWFhkRTFkcytsOTVEVi9PRkE9PSJdLAoKICAgICJAZW1uYXBpL2NvcmUiOiBbIkBlbW5hcGkvY29yZUAxLjEwLjAiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiQGVtbmFwaS93YXNpLXRocmVhZHMiOiAiMS4yLjEiLCAidHNsaWIiOiAiXjIuNC4wIiB9IH0sICJzaGE1MTIteXE2T2tKNHA4MkNBZlBsMHU5bVFlYlFIS1BKa1k3V3JJdWsyMDVjVFluWWUrazJaOFlCaDExRnJiUkcvSDZpaGlycWNhY09nbDJCSU84b3lNUUxlWHc9PSJdLAoKICAgICJAZW1uYXBpL3J1bnRpbWUiOiBbIkBlbW5hcGkvcnVudGltZUAxLjEwLjAiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAidHNsaWIiOiAiXjIuNC4wIiB9IH0sICJzaGE1MTItZXd2WWxrODZ4VW9HSTB6UVJOcS9tQysxNlIxUWVEbEtReTIxS2kzb1NZWE5nTGI0NUdWMVA2QTBNKy9zNm55Q3VORHFlNVZwYVk4NEJ6WEd3VmJ3RkE9PSJdLAoKICAgICJAZW1uYXBpL3dhc2ktdGhyZWFkcyI6IFsiQGVtbmFwaS93YXNpLXRocmVhZHNAMS4yLjEiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAidHNsaWIiOiAiXjIuNC4wIiB9IH0sICJzaGE1MTItdVRJSTdPWUYrL01lcy9NcmNJT1lwNXlPdFNNTEJXU0lvTFBwY2d3aXBvaUtibGk2azMyMnRjb0ZzeG9JSXhQRHFXMDFTUUdBZ2tvNEV6WmkyQk52Mnc9PSJdLAoKICAgICJAZmxvYXRpbmctdWkvY29yZSI6IFsiQGZsb2F0aW5nLXVpL2NvcmVAMS43LjUiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiQGZsb2F0aW5nLXVpL3V0aWxzIjogIl4wLjIuMTEiIH0gfSwgInNoYTUxMi0xSWg0V1RXeXcwK2xLeUZNY0JIR2JiNVU1RnR1SEp1dWpveXlyNXpUYVdTNUVZTWVUNkpiMkF1RGVmdHNDc0V1Y2hPK21NMmlqNStxOWNyaHlkekxoUT09Il0sCgogICAgIkBmbG9hdGluZy11aS9kb20iOiBbIkBmbG9hdGluZy11aS9kb21AMS43LjYiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiQGZsb2F0aW5nLXVpL2NvcmUiOiAiXjEuNy41IiwgIkBmbG9hdGluZy11aS91dGlscyI6ICJeMC4yLjExIiB9IH0sICJzaGE1MTItOWdaU0FJNVhNMzY4ODBQUE1tLy85ZGZpRW5nWW9DNkFtMml6RVMxRkY0MDZZRnNqdnlCTW1lSjJnNFNBanUzeFd3dHV5bk5SRkwyczloZ3hwTEk1U1E9PSJdLAoKICAgICJAZmxvYXRpbmctdWkvcmVhY3QtZG9tIjogWyJAZmxvYXRpbmctdWkvcmVhY3QtZG9tQDIuMS44IiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIkBmbG9hdGluZy11aS9kb20iOiAiXjEuNy42IiB9LCAicGVlckRlcGVuZGVuY2llcyI6IHsgInJlYWN0IjogIj49MTYuOC4wIiwgInJlYWN0LWRvbSI6ICI+PTE2LjguMCIgfSB9LCAic2hhNTEyLWNDNTJiSHdNL24vQ3hTODdGSDB5V2RuZ0VacmpkdExXL3FWcnVvNjhxZytwcks3WlE0WUdkdXQyR3lEVnBvR2VBWWUvaDg5OXJWZU9WbTZPaTQwazJBPT0iXSwKCiAgICAiQGZsb2F0aW5nLXVpL3V0aWxzIjogWyJAZmxvYXRpbmctdWkvdXRpbHNAMC4yLjExIiwgIiIsIHt9LCAic2hhNTEyLVJpQi95SWg3OHBjSXhsNmxMTUcwQ2dCWEFaMlkwZVZIcU1QWXVndSs5VTBBZVQ2WUJlaUpwZjdsYmRKTkl1Z0ZQNVNJandOUmdvNERoUjFReGkyNkdnPT0iXSwKCiAgICAiQGltZy9jb2xvdXIiOiBbIkBpbWcvY29sb3VyQDEuMS4wIiwgIiIsIHt9LCAic2hhNTEyLVRkNzZxN2o1N28vdExWZGdTNzQ2Y1lBUmZTeXhrOGlFZlJ4ZXdMOWg0T016WWhiVzRUQWNwcGwwbVQ0ZXlxWGRkaDZML2p3b003NW1vN2l4YS9wQ2VRPT0iXSwKCiAgICAiQGltZy9zaGFycC1kYXJ3aW4tYXJtNjQiOiBbIkBpbWcvc2hhcnAtZGFyd2luLWFybTY0QDAuMzQuNSIsICIiLCB7ICJvcHRpb25hbERlcGVuZGVuY2llcyI6IHsgIkBpbWcvc2hhcnAtbGlidmlwcy1kYXJ3aW4tYXJtNjQiOiAiMS4yLjQiIH0sICJvcyI6ICJkYXJ3aW4iLCAiY3B1IjogImFybTY0IiB9LCAic2hhNTEyLWltdFEzV01KWGJNWTRmeGIvTmRwNkhCVE5WdFdDVUkwV2RvYnloZUdmNSthZDZ4WDhWSURPOHUyeEU0cWMvZnIwOENLRy83ZERzZUZ0bjZNNmcvcjN3PT0iXSwKCiAgICAiQGltZy9zaGFycC1kYXJ3aW4teDY0IjogWyJAaW1nL3NoYXJwLWRhcndpbi14NjRAMC4zNC41IiwgIiIsIHsgIm9wdGlvbmFsRGVwZW5kZW5jaWVzIjogeyAiQGltZy9zaGFycC1saWJ2aXBzLWRhcndpbi14NjQiOiAiMS4yLjQiIH0sICJvcyI6ICJkYXJ3aW4iLCAiY3B1IjogIng2NCIgfSwgInNoYTUxMi1ZTkVGQUYvNEtRL1BlVzBOK3IrYVZWc29JWTAvcXh4aWtGMlNXZHArTlJrbU1CN3k5TEJaQVZxUTR5aEdDbS9IM0gyNzBPU3lrcW1RTUtMQmhCSkRFdz09Il0sCgogICAgIkBpbWcvc2hhcnAtbGlidmlwcy1kYXJ3aW4tYXJtNjQiOiBbIkBpbWcvc2hhcnAtbGlidmlwcy1kYXJ3aW4tYXJtNjRAMS4yLjQiLCAiIiwgeyAib3MiOiAiZGFyd2luIiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi16cWpqbzdSYXRGZkZvUDBNa1E1MWpmdUZaQm5WRTJwUmlheWRLSjFHL3JIWnZuc3JIQU9jUUFMSWk5c0E1Y281eGVuUWRUdWdDdnRiMWN1Zjc4VmY0Zz09Il0sCgogICAgIkBpbWcvc2hhcnAtbGlidmlwcy1kYXJ3aW4teDY0IjogWyJAaW1nL3NoYXJwLWxpYnZpcHMtZGFyd2luLXg2NEAxLjIuNCIsICIiLCB7ICJvcyI6ICJkYXJ3aW4iLCAiY3B1IjogIng2NCIgfSwgInNoYTUxMi0xSU9kNXhmVmhsR3dYK3pYdjJOOTNrMHlNT052VWxBTnlsYkp3MWVUYWg4Sy9KdHBpMTVLQytXU2lhWC9uQm1ibTJIeFJNMWdaMG5TZGpTc3JaYkdLZz09Il0sCgogICAgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC1hcm0iOiBbIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC1hcm1AMS4yLjQiLCAiIiwgeyAib3MiOiAibGludXgiLCAiY3B1IjogImFybSIgfSwgInNoYTUxMi1iRkk3eGNLRkVMZGlOQ1ZvdjhlNDRJYTR1MmJ5QStsM1h0c0FqK1E4dGZDd082QlE4aURvallkdm9QTXFzS0RrdW9PbytYNkhaQTBzMHExMUFOTVE4QT09Il0sCgogICAgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC1hcm02NCI6IFsiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4LWFybTY0QDEuMi40IiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi1leGNqWDhEZnNJY0oxMHgxS3pyNFJjV2UxZWRDOVBxdURSUlB4M1lWQ3ZRditVNXA3WWluMnMzMmZ0emlrWG9qYjFQSUZjLzlNdDI4L3kraVJrbGtydz09Il0sCgogICAgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC1wcGM2NCI6IFsiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4LXBwYzY0QDEuMi40IiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJwcGM2NCIgfSwgInNoYTUxMi1GTXV2R2lqTERZRzZsVytiL1V2eWlsVVd1NUF5dSszcjJkMVM4bm90aUdDSXlZVS83NmVpZzFVZk1ta1o3dndnT3J6S3psUWJGU3VRZmdtN0dZVVBwQT09Il0sCgogICAgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC1yaXNjdjY0IjogWyJAaW1nL3NoYXJwLWxpYnZpcHMtbGludXgtcmlzY3Y2NEAxLjIuNCIsICIiLCB7ICJvcyI6ICJsaW51eCIsICJjcHUiOiAibm9uZSIgfSwgInNoYTUxMi1vVkRiY1I0elVDMGNlODJ0ZXViU20reDZFVGl4dEtaQmgvcWJSRUlPY0kzY1VMekR5YjE4U3IvV2N5eDdOUlFlUXpPaUhUTmJaRkYxVXdQUzJzY3lHQT09Il0sCgogICAgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC1zMzkweCI6IFsiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4LXMzOTB4QDEuMi40IiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJzMzkweCIgfSwgInNoYTUxMi1xbXA5VnJ6Z1BnTW9HWnlQdnJRSHFrMDJ1eWpBMC9RclRPMjZUcWs2bDRaVjBNUFdJVzZMVGtxT0lvditKMXlFdTdNYkZRYURwd2R3SktoYkp2dVJ4UT09Il0sCgogICAgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC14NjQiOiBbIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC14NjRAMS4yLjQiLCAiIiwgeyAib3MiOiAibGludXgiLCAiY3B1IjogIng2NCIgfSwgInNoYTUxMi10SnhpaUxzbUhjOUF4MWJ6M29hT1lCVVJUWEdJUkRPREJxaHZlVkhvbnJISjkvK2s4OXFiTGwwYmNKbnMrZTR0NHJ2YU5CeGFFWnNGdFNmQWRxdVBydz09Il0sCgogICAgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eG11c2wtYXJtNjQiOiBbIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eG11c2wtYXJtNjRAMS4yLjQiLCAiIiwgeyAib3MiOiAibGludXgiLCAiY3B1IjogImFybTY0IiB9LCAic2hhNTEyLUZWUUh1d3gxSUl1Tm93OVFBYllVekorRW44S2NWbTlMazUrdUdVUUpIYVptTUVDWm1PbGl4OUhuSDduMVRSa1hNUzBwR3hJSm9rSVZCOVN1cVpHR1h3PT0iXSwKCiAgICAiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4bXVzbC14NjQiOiBbIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eG11c2wteDY0QDEuMi40IiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJ4NjQiIH0sICJzaGE1MTItK0xweUJrN0w0NFpJWHd6L1ZZZmdsYVgvb2t4ZXpFU2M2VXhEU295bzJLczZKeGM0WTdzR2pwZ1U5czRQTWdxZ2pqMWdaQ3lsVGllTmFtcUExTUY3RGc9PSJdLAoKICAgICJAaW1nL3NoYXJwLWxpbnV4LWFybSI6IFsiQGltZy9zaGFycC1saW51eC1hcm1AMC4zNC41IiwgIiIsIHsgIm9wdGlvbmFsRGVwZW5kZW5jaWVzIjogeyAiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4LWFybSI6ICIxLjIuNCIgfSwgIm9zIjogImxpbnV4IiwgImNwdSI6ICJhcm0iIH0sICJzaGE1MTItOWRMcXN2d3RnMXV1WEJHWktzeGVtOTU5NSt1anYwc0o2Vmk4d2NUQU5TRnB3Vi9HT05hdDVlQ2t6UW8vMU82elJJa2gwbS84KzVCanJScjdqRFVTWnc9PSJdLAoKICAgICJAaW1nL3NoYXJwLWxpbnV4LWFybTY0IjogWyJAaW1nL3NoYXJwLWxpbnV4LWFybTY0QDAuMzQuNSIsICIiLCB7ICJvcHRpb25hbERlcGVuZGVuY2llcyI6IHsgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC1hcm02NCI6ICIxLjIuNCIgfSwgIm9zIjogImxpbnV4IiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi1iS1F6YUpSWS9ia1BPWHlLeDVFVnVwN3FrYW9qRUNHNk5MWXN3Z2t0T1pqYVhlY1NBZUNXaVp3d2lGZjMvWStPMUhyYXVpRTNGVnNHeEZnOGMyNHJaZz09Il0sCgogICAgIkBpbWcvc2hhcnAtbGludXgtcHBjNjQiOiBbIkBpbWcvc2hhcnAtbGludXgtcHBjNjRAMC4zNC41IiwgIiIsIHsgIm9wdGlvbmFsRGVwZW5kZW5jaWVzIjogeyAiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4LXBwYzY0IjogIjEuMi40IiB9LCAib3MiOiAibGludXgiLCAiY3B1IjogInBwYzY0IiB9LCAic2hhNTEyLTd6em53TmFxVzZZdHNmckdHREE2QlJrSVNLQUFFMUpvMFFkcE5ZWE5NSHUyKzBkVHJQZmxUTE5rcGM4bDdNVVA1TTE2WkpjVXZ5c1ZXV3JNZWZacXVBPT0iXSwKCiAgICAiQGltZy9zaGFycC1saW51eC1yaXNjdjY0IjogWyJAaW1nL3NoYXJwLWxpbnV4LXJpc2N2NjRAMC4zNC41IiwgIiIsIHsgIm9wdGlvbmFsRGVwZW5kZW5jaWVzIjogeyAiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4LXJpc2N2NjQiOiAiMS4yLjQiIH0sICJvcyI6ICJsaW51eCIsICJjcHUiOiAibm9uZSIgfSwgInNoYTUxMi01MWdKdUxQVEthN3BpWVBhVnM4R21CeW83L1U3LzdUWk9xK2NuWEpJSFpLYXZJUkhBUDc3ZTNOMkhFbDNkZ2lxZEQvdzB5VWZpSm5JSTc3UHVEREZkdz09Il0sCgogICAgIkBpbWcvc2hhcnAtbGludXgtczM5MHgiOiBbIkBpbWcvc2hhcnAtbGludXgtczM5MHhAMC4zNC41IiwgIiIsIHsgIm9wdGlvbmFsRGVwZW5kZW5jaWVzIjogeyAiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4LXMzOTB4IjogIjEuMi40IiB9LCAib3MiOiAibGludXgiLCAiY3B1IjogInMzOTB4IiB9LCAic2hhNTEyLW5RdENrMFBkS2ZobzNlQzVNcmJRb2lnSjJnZDFDZ2RkVU1rYWJVaityQmV2czh0WjJjVUxPeDQ2RTdveVgrMDRXR2ZBQmdJd21NQzBWcWllVGlSNGpnPT0iXSwKCiAgICAiQGltZy9zaGFycC1saW51eC14NjQiOiBbIkBpbWcvc2hhcnAtbGludXgteDY0QDAuMzQuNSIsICIiLCB7ICJvcHRpb25hbERlcGVuZGVuY2llcyI6IHsgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC14NjQiOiAiMS4yLjQiIH0sICJvcyI6ICJsaW51eCIsICJjcHUiOiAieDY0IiB9LCAic2hhNTEyLU1FemQ4SFBLeFZ4VmVud0FhK0pSUHdFQzdRRmpvUFd1UzVOWm5CdDZCM3B1N0VHMkdlMGlkMW9MSFpwUEpkbjNPUUsrQlFEaXc5elN0aUhCVEpRUVFRPT0iXSwKCiAgICAiQGltZy9zaGFycC1saW51eG11c2wtYXJtNjQiOiBbIkBpbWcvc2hhcnAtbGludXhtdXNsLWFybTY0QDAuMzQuNSIsICIiLCB7ICJvcHRpb25hbERlcGVuZGVuY2llcyI6IHsgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eG11c2wtYXJtNjQiOiAiMS4yLjQiIH0sICJvcyI6ICJsaW51eCIsICJjcHUiOiAiYXJtNjQiIH0sICJzaGE1MTItZnBySlI2R3RSc010Nkt5ZnE0NElzQ2hWWmVHTjk3Z1REMzMxd2VSMWV4MWMxcnlwREVBQk42VG0yeGExd0U2bFliNURkRW5rMDNOWlBxQTdJZDIxeWc9PSJdLAoKICAgICJAaW1nL3NoYXJwLWxpbnV4bXVzbC14NjQiOiBbIkBpbWcvc2hhcnAtbGludXhtdXNsLXg2NEAwLjM0LjUiLCAiIiwgeyAib3B0aW9uYWxEZXBlbmRlbmNpZXMiOiB7ICJAaW1nL3NoYXJwLWxpYnZpcHMtbGludXhtdXNsLXg2NCI6ICIxLjIuNCIgfSwgIm9zIjogImxpbnV4IiwgImNwdSI6ICJ4NjQiIH0sICJzaGE1MTItSmc4d05UMU1Vekl2aEJGeFZpcXJFaFdER3pxeW1vM3NWN3o3WnNhV2JaTkRMWFJKWm9SR3JqdWxwNjBZWXRWNHdmWThWSUtjV2lkam9qbExjV3JkOFE9PSJdLAoKICAgICJAaW1nL3NoYXJwLXdhc20zMiI6IFsiQGltZy9zaGFycC13YXNtMzJAMC4zNC41IiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIkBlbW5hcGkvcnVudGltZSI6ICJeMS43LjAiIH0sICJjcHUiOiAibm9uZSIgfSwgInNoYTUxMi1PZFdURWlWa1kyUEh3cWtiQkk4ZnJGeFFRRmVrSGFTU2tVSUprd3pjbFdaZTY0TzFYNFVsVWpxcXFMYVBiVXBNT1FrNkZCdS9IdGxHWE5ibElzMGh1dz09Il0sCgogICAgIkBpbWcvc2hhcnAtd2luMzItYXJtNjQiOiBbIkBpbWcvc2hhcnAtd2luMzItYXJtNjRAMC4zNC41IiwgIiIsIHsgIm9zIjogIndpbjMyIiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi1XUTNBZ1dDV1lTYjJ5dCtJRzhtbkM2SmRrOVdoczdPMGd4cGhibHNMdmRoU3BTVHRtdTY5WkcxR2tiNk51dnhzTkFDd2lQVjZjTlNaTnp0MEtQc3c3Zz09Il0sCgogICAgIkBpbWcvc2hhcnAtd2luMzItaWEzMiI6IFsiQGltZy9zaGFycC13aW4zMi1pYTMyQDAuMzQuNSIsICIiLCB7ICJvcyI6ICJ3aW4zMiIsICJjcHUiOiAiaWEzMiIgfSwgInNoYTUxMi1GVjltLzdObWVDbVNIREQ1ajQrNHBOSThDcDNhVytKdkxvWGNUVW8wSXF5alNmQVpKOGRJVW1pangxcWFKc0lpVStIb3N3NnhNNUtpakFXUkpDU2dOZz09Il0sCgogICAgIkBpbWcvc2hhcnAtd2luMzIteDY0IjogWyJAaW1nL3NoYXJwLXdpbjMyLXg2NEAwLjM0LjUiLCAiIiwgeyAib3MiOiAid2luMzIiLCAiY3B1IjogIng2NCIgfSwgInNoYTUxMi0rMjlZTXNxWTIvOWVGRWlXOTNlcVdudUxjV2N1Zm93WGV3d1NOSVQ2VXdaZFVVQ3JNM29Gak1XSC9aNi9UTW1iNGhsRmVubWZBVmJwV2V1cDJqcnlDdz09Il0sCgogICAgIkBqcmlkZ2V3ZWxsL2dlbi1tYXBwaW5nIjogWyJAanJpZGdld2VsbC9nZW4tbWFwcGluZ0AwLjMuMTMiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiQGpyaWRnZXdlbGwvc291cmNlbWFwLWNvZGVjIjogIl4xLjUuMCIsICJAanJpZGdld2VsbC90cmFjZS1tYXBwaW5nIjogIl4wLjMuMjQiIH0gfSwgInNoYTUxMi0ya2t0LzduaUo2TWdFUHhGMGJZZFE2ZXRaYUErZlF2RGNMS2NraHkxeUlRT3phb0tqQkJqU2o2My9hTFZqWUUzcWhSdDVkdk0rdVV5ZkNnNlVLQ0JiQT09Il0sCgogICAgIkBqcmlkZ2V3ZWxsL3JlbWFwcGluZyI6IFsiQGpyaWRnZXdlbGwvcmVtYXBwaW5nQDIuMy41IiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIkBqcmlkZ2V3ZWxsL2dlbi1tYXBwaW5nIjogIl4wLjMuNSIsICJAanJpZGdld2VsbC90cmFjZS1tYXBwaW5nIjogIl4wLjMuMjQiIH0gfSwgInNoYTUxMi1MSTl1LytsYVlHNERzMVRES1NKVzJZUHJJbGNWWU93aTJmVUM2eEI0M2x1ZUNqZ3hWNGxmZk9DWkN0WUZpSDZUTk9YK3RRS1h4OTdUNElLSGJoeUhFUT09Il0sCgogICAgIkBqcmlkZ2V3ZWxsL3Jlc29sdmUtdXJpIjogWyJAanJpZGdld2VsbC9yZXNvbHZlLXVyaUAzLjEuMiIsICIiLCB7fSwgInNoYTUxMi1iUklTZ0NJalAyMC90YldTUFdNRWk1NFFWUFJaRXhrdUQ5bEpMK1VJeFVLdHdWSkE4d1cxVHJiMWpNczFSRlhvMUNCVE5aLzVocEM5UXZtS1dkb3BLdz09Il0sCgogICAgIkBqcmlkZ2V3ZWxsL3NvdXJjZW1hcC1jb2RlYyI6IFsiQGpyaWRnZXdlbGwvc291cmNlbWFwLWNvZGVjQDEuNS41IiwgIiIsIHt9LCAic2hhNTEyLWNZUTkzMTBncnF4dWVXYmwrV3VJVUlhaVVhRGNqN1dPcTVmVmhFbGpOVmdSZk9VaFk5ZnkyelR2Zm9xV3NuZWJoOFNsNzBWU2NGYklDdkpuTEtCME9nPT0iXSwKCiAgICAiQGpyaWRnZXdlbGwvdHJhY2UtbWFwcGluZyI6IFsiQGpyaWRnZXdlbGwvdHJhY2UtbWFwcGluZ0AwLjMuMzEiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiQGpyaWRnZXdlbGwvcmVzb2x2ZS11cmkiOiAiXjMuMS4wIiwgIkBqcmlkZ2V3ZWxsL3NvdXJjZW1hcC1jb2RlYyI6ICJeMS40LjE0IiB9IH0sICJzaGE1MTItenpOUitTZFFTREp6Yzhqb2FlUDhRUW9DUXI4TnVZeDJkSUl5dGwxUWVCRVpISjl1VzZoZWJzcllnYno4aEp3VVFhbzNUV0NNdG1mVjhOdTF0d09MQXc9PSJdLAoKICAgICJAbmFwaS1ycy93YXNtLXJ1bnRpbWUiOiBbIkBuYXBpLXJzL3dhc20tcnVudGltZUAxLjEuNCIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAdHlieXMvd2FzbS11dGlsIjogIl4wLjEwLjEiIH0sICJwZWVyRGVwZW5kZW5jaWVzIjogeyAiQGVtbmFwaS9jb3JlIjogIl4xLjcuMSIsICJAZW1uYXBpL3J1bnRpbWUiOiAiXjEuNy4xIiB9IH0sICJzaGE1MTItM05RTk5nQTFZU2xKYi9rTUgxaWxkQVNQOUhXNy83a1luUkkyc3pXSmFvZmFTMWhXbWJHSTRIK2QzKzIyYUd6WFhOOUlKK24rR2lGVmNHaXBKUDE4b3c9PSJdLAoKICAgICJAbmV4dC9lbnYiOiBbIkBuZXh0L2VudkAxNi4yLjYiLCAiIiwge30sICJzaGE1MTItZ2Q4SG9ITjR1Zmo3M1dtUjNKbVZvbHJwSlI0N0lMSzZMb3VQNXhFbFBnbGFWeGlyNmUxYTdWenZUdkRXa09vUFhUOXJra1R6eUN4QnU0eWVaZlp3Y3c9PSJdLAoKICAgICJAbmV4dC9zd2MtZGFyd2luLWFybTY0IjogWyJAbmV4dC9zd2MtZGFyd2luLWFybTY0QDE2LjIuNiIsICIiLCB7ICJvcyI6ICJkYXJ3aW4iLCAiY3B1IjogImFybTY0IiB9LCAic2hhNTEyLVpKR2trY05mWWdyck1rcU9kWjd6b0xhMVRPeTBxcGNNZmsvejRNaC9GS1V6NDBnVk8rSE5RV3FtTHhmNjdaNVdCNjREUnAwZGhFYnlIZmVsKzZzSlVnPT0iXSwKCiAgICAiQG5leHQvc3djLWRhcndpbi14NjQiOiBbIkBuZXh0L3N3Yy1kYXJ3aW4teDY0QDE2LjIuNiIsICIiLCB7ICJvcyI6ICJkYXJ3aW4iLCAiY3B1IjogIng2NCIgfSwgInNoYTUxMi12L1lMQkhJWTEzMkNlZDNwdUJKN1lKS3cxbHFzQ3JnY05vMmFSSmxDRXlRcnJDZVJKbHZHbG5teGhQeE5RSTNLRTNOMURONXI5VFBOUHZrYTNucTVSUT09Il0sCgogICAgIkBuZXh0L3N3Yy1saW51eC1hcm02NC1nbnUiOiBbIkBuZXh0L3N3Yy1saW51eC1hcm02NC1nbnVAMTYuMi42IiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi1SUE92cWxZQmJjUWprejlWUVFEWjJUMmJBUklqWFpWMUtGbHQrVjJNcjZTVy9lNEk5ZmNLc2FBMGhkeWYyRkhvVGxzVjJ4bkJkNVk5MTJyUC8xQ2U2dz09Il0sCgogICAgIkBuZXh0L3N3Yy1saW51eC1hcm02NC1tdXNsIjogWyJAbmV4dC9zd2MtbGludXgtYXJtNjQtbXVzbEAxNi4yLjYiLCAiIiwgeyAib3MiOiAibGludXgiLCAiY3B1IjogImFybTY0IiB9LCAic2hhNTEyLVVSVVR1MStkTWt4SnNQRmdtK09lRXZxOXdmNXN1ancwRXZnWXk4MFRER0hUU0xUbklIZXFiMEV1OEEzc0M5NUlSZ2plalFMK2tDNG13KzR5UHhpQVhBPT0iXSwKCiAgICAiQG5leHQvc3djLWxpbnV4LXg2NC1nbnUiOiBbIkBuZXh0L3N3Yy1saW51eC14NjQtZ251QDE2LjIuNiIsICIiLCB7ICJvcyI6ICJsaW51eCIsICJjcHUiOiAieDY0IiB9LCAic2hhNTEyLURPajE4Mm1QVjhHM1VrcmF5TG9SRU01WUVZSStEazV3djdPeDl4bDFmRmliQUVMRXNGRDBsRFBmSEllSUxsdXRNTWZkeWhsellQRUxHM3BldUthdXJ3PT0iXSwKCiAgICAiQG5leHQvc3djLWxpbnV4LXg2NC1tdXNsIjogWyJAbmV4dC9zd2MtbGludXgteDY0LW11c2xAMTYuMi42IiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJ4NjQiIH0sICJzaGE1MTItSEtRNVNQL1YvdWI3M1V2RjduL3plSmx4azJrTG10TDdXenJnNFdmbWtqbU5vczVvbkoydEt1N3laT1BkTDE4QTZTdmZuM21heDI5eW0rcnk3TmtLNGc9PSJdLAoKICAgICJAbmV4dC9zd2Mtd2luMzItYXJtNjQtbXN2YyI6IFsiQG5leHQvc3djLXdpbjMyLWFybTY0LW1zdmNAMTYuMi42IiwgIiIsIHsgIm9zIjogIndpbjMyIiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi1MWlhwVGxQeVM1djdIaFNtbnZzTEdQM2lJWWdZT0JuYzhyOEFybFQ1NXNHSFY4OWJSMkhsRGRCaldRK1BZNlNKTW1rOFR1VkdGdXhhbG5QM2svMER3Zz09Il0sCgogICAgIkBuZXh0L3N3Yy13aW4zMi14NjQtbXN2YyI6IFsiQG5leHQvc3djLXdpbjMyLXg2NC1tc3ZjQDE2LjIuNiIsICIiLCB7ICJvcyI6ICJ3aW4zMiIsICJjcHUiOiAieDY0IiB9LCAic2hhNTEyLUYwKzRpMGg5SjZDNGVFM0VBUFdzb0NrN1VXL2Riek9qeXp4WTBxbkRVT1lGdTZGRm1kWjZsOTcvWGRWMy9OejNWWXlPN1VXanlFSlVYa0dxY29YZk1BPT0iXSwKCiAgICAiQG94Yy1wcm9qZWN0L3R5cGVzIjogWyJAb3hjLXByb2plY3QvdHlwZXNAMC4xMzAuMCIsICIiLCB7fSwgInNoYTUxMi1pYkQydXN4OUpSdTdmNXB1MnRNS01JNGNwQTROZ1hKUW9ZUlA0cFE3UHhtbjFsNmsvNTNxV3RRV1pheWhZeTNYNFFaa3Q5ME90K21KRWFlWG91aW82UT09Il0sCgogICAgIkByb2xsZG93bi9iaW5kaW5nLWFuZHJvaWQtYXJtNjQiOiBbIkByb2xsZG93bi9iaW5kaW5nLWFuZHJvaWQtYXJtNjRAMS4wLjEiLCAiIiwgeyAib3MiOiAiYW5kcm9pZCIsICJjcHUiOiAiYXJtNjQiIH0sICJzaGE1MTItZkpJM0kwcjNDM09qL3pkQkNwYUNtQlJaWWYwN3hwYXE0eUNmRERvU0ZtK2JlV056YklsMjZwdVc4UnJhVWR1Z29Kdy85NXplck5PbjZqYXNBaHpTbWc9PSJdLAoKICAgICJAcm9sbGRvd24vYmluZGluZy1kYXJ3aW4tYXJtNjQiOiBbIkByb2xsZG93bi9iaW5kaW5nLWRhcndpbi1hcm02NEAxLjAuMSIsICIiLCB7ICJvcyI6ICJkYXJ3aW4iLCAiY3B1IjogImFybTY0IiB9LCAic2hhNTEyLWNLbkFoV0VzVjdUUGNBLzVFQXRlRHA2S2NKWkJRMkcrQnFFN3pheU1NaTdrTXZ3UnNidjdXVDlhT25uMFdObDRTS0VJZjQzdmpTMzFpVVB1ODBuelhnPT0iXSwKCiAgICAiQHJvbGxkb3duL2JpbmRpbmctZGFyd2luLXg2NCI6IFsiQHJvbGxkb3duL2JpbmRpbmctZGFyd2luLXg2NEAxLjAuMSIsICIiLCB7ICJvcyI6ICJkYXJ3aW4iLCAiY3B1IjogIng2NCIgfSwgInNoYTUxMi1ZS3JWd1FqSVJCUG8rNUcvdTAzd0dqYmR5NHE3cHl6Q2U5M0RLOVZKN3prVm1lZzhMSjdHYmdzaUhXZFI0eFNvZTRDQVhSRDdCY2pnYnRyNjRia1hOZz09Il0sCgogICAgIkByb2xsZG93bi9iaW5kaW5nLWZyZWVic2QteDY0IjogWyJAcm9sbGRvd24vYmluZGluZy1mcmVlYnNkLXg2NEAxLjAuMSIsICIiLCB7ICJvcyI6ICJmcmVlYnNkIiwgImNwdSI6ICJ4NjQiIH0sICJzaGE1MTItei9vQnNSRW80NlNzRnFCd1l0RmUwa3BKZUJpakFUNDhPL1dYTEk0c3VpQ0xCa3IwM1JUdFRKTUN6U2REZDJ6bmxoOFZKaXpMMDlYVmtRZ2s4SVpvbnc9PSJdLAoKICAgICJAcm9sbGRvd24vYmluZGluZy1saW51eC1hcm0tZ251ZWFiaWhmIjogWyJAcm9sbGRvd24vYmluZGluZy1saW51eC1hcm0tZ251ZWFiaWhmQDEuMC4xIiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJhcm0iIH0sICJzaGE1MTItaWs4cTdHTTExenh2WXhGYzJQZURjVDZUQnZoQ1FNYVV4ZnBoL001bDlzS3VUcy9TamczTCtCeXcwRjd3MFpWTEJabXgzMFArZ0cwRUN6ek4rTUZjbVE9PSJdLAoKICAgICJAcm9sbGRvd24vYmluZGluZy1saW51eC1hcm02NC1nbnUiOiBbIkByb2xsZG93bi9iaW5kaW5nLWxpbnV4LWFybTY0LWdudUAxLjAuMSIsICIiLCB7ICJvcyI6ICJsaW51eCIsICJjcHUiOiAiYXJtNjQiIH0sICJzaGE1MTItUW9TeDJFa3lycmRaNmtjeUU4c3RxWjYydDBZcmE4RnM1aWE5bE94SnJoNlRNUUpLN2dRS21zY2RUSGY3cE9YS1JFS3JWd090SmNRRzNxVlNmYzg2NkE9PSJdLAoKICAgICJAcm9sbGRvd24vYmluZGluZy1saW51eC1hcm02NC1tdXNsIjogWyJAcm9sbGRvd24vYmluZGluZy1saW51eC1hcm02NC1tdXNsQDEuMC4xIiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi11d053RnB3S2VOaVphd2ZBV0JnZzBWSXp0UFRWM2loaGgxdlYzMzRoOWl2bk5Mb3J4blFNVTZGejh3RzFaYjRRaDlMQzEvTWtjeVQzWWxEWEczUnNnZz09Il0sCgogICAgIkByb2xsZG93bi9iaW5kaW5nLWxpbnV4LXBwYzY0LWdudSI6IFsiQHJvbGxkb3duL2JpbmRpbmctbGludXgtcHBjNjQtZ251QDEuMC4xIiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJwcGM2NCIgfSwgInNoYTUxMi16WTFidWw3T1dyN0RGQmlKKyt3b2ZYdm5yOEI0NWNlM1FzUVVoS3JJaFhzeWdBaDdiVGt3eWVNMWJpMWEyZzVDL3lDL044VFp5R0RFb01mbS9sOW1wZz09Il0sCgogICAgIkByb2xsZG93bi9iaW5kaW5nLWxpbnV4LXMzOTB4LWdudSI6IFsiQHJvbGxkb3duL2JpbmRpbmctbGludXgtczM5MHgtZ251QDEuMC4xIiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJzMzkweCIgfSwgInNoYTUxMi0wZnJsc1QvZjRGdDZJN1NNRVNUS25GM2Nac2RpY1FuMWRDTWtGL2pUOXdETEUrZ0dvaVFmdjFubVQ5ZStzN3MvZmVrdnZ5NnRaTTJqSHZJMnRrYkpEUT09Il0sCgogICAgIkByb2xsZG93bi9iaW5kaW5nLWxpbnV4LXg2NC1nbnUiOiBbIkByb2xsZG93bi9iaW5kaW5nLWxpbnV4LXg2NC1nbnVAMS4wLjEiLCAiIiwgeyAib3MiOiAibGludXgiLCAiY3B1IjogIng2NCIgfSwgInNoYTUxMi1YQUJWbUdwOVRnMFdzcFRWdndkdVRjNGZwcXk2Sm5BVXJTUWU2T3V5cUQvMDNuSTdyME85T1dVa01Jd0ZyaktBSXFvbHZxb0E0WnJKcHBnd0UwR3htdz09Il0sCgogICAgIkByb2xsZG93bi9iaW5kaW5nLWxpbnV4LXg2NC1tdXNsIjogWyJAcm9sbGRvd24vYmluZGluZy1saW51eC14NjQtbXVzbEAxLjAuMSIsICIiLCB7ICJvcyI6ICJsaW51eCIsICJjcHUiOiAieDY0IiB9LCAic2hhNTEyLWJWNGZ6c3d1elZjS0Q5MG8vVk02UXFLeG54bERxMGcyQklTRExOVm14cm5ocHYxRERieVBoQ0lqWWZ2ellMVitNdmtLS25RdDJRNkFPODZTRUJVTFVRPT0iXSwKCiAgICAiQHJvbGxkb3duL2JpbmRpbmctb3Blbmhhcm1vbnktYXJtNjQiOiBbIkByb2xsZG93bi9iaW5kaW5nLW9wZW5oYXJtb255LWFybTY0QDEuMC4xIiwgIiIsIHsgIm9zIjogIm5vbmUiLCAiY3B1IjogImFybTY0IiB9LCAic2hhNTEyLS9NaDBaaHEzT1A3ZlZzMGtjUUhaUDZsWkV0aE1HVGFTZjhVQlFZU0ZFWkRXR1hYbEVDK25KNkVxZW5hSzJ0NExCWE1lM0ErSy9HMkJWWFhkdE9yNFBRPT0iXSwKCiAgICAiQHJvbGxkb3duL2JpbmRpbmctd2FzbTMyLXdhc2kiOiBbIkByb2xsZG93bi9iaW5kaW5nLXdhc20zMi13YXNpQDEuMC4xIiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIkBlbW5hcGkvY29yZSI6ICIxLjEwLjAiLCAiQGVtbmFwaS9ydW50aW1lIjogIjEuMTAuMCIsICJAbmFwaS1ycy93YXNtLXJ1bnRpbWUiOiAiXjEuMS40IiB9LCAiY3B1IjogIm5vbmUiIH0sICJzaGE1MTItKzF4YzlYNDVsOHVmc0JBbTZHanZ4MnFEUklZOWxUVnQwY2dXTmNKKzFnZGhYdmtieGVQQTYweVJUd1NUdVhMMDlDTWh5Sm1qcFY3RTNOb3l4YnFGUVE9PSJdLAoKICAgICJAcm9sbGRvd24vYmluZGluZy13aW4zMi1hcm02NC1tc3ZjIjogWyJAcm9sbGRvd24vYmluZGluZy13aW4zMi1hcm02NC1tc3ZjQDEuMC4xIiwgIiIsIHsgIm9zIjogIndpbjMyIiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi0xRCtVcVpkZm51UitKeTFHZ01Kd2k4NWJENDBIMjF1Tm1PUFJXUWh3NG9SU3VvbFovQjVyaXhaNDVESzJLWE9UQ3ZtVkNlY2F1V2dFaGJ3OGJJN3RPdz09Il0sCgogICAgIkByb2xsZG93bi9iaW5kaW5nLXdpbjMyLXg2NC1tc3ZjIjogWyJAcm9sbGRvd24vYmluZGluZy13aW4zMi14NjQtbXN2Y0AxLjAuMSIsICIiLCB7ICJvcyI6ICJ3aW4zMiIsICJjcHUiOiAieDY0IiB9LCAic2hhNTEyLUlOQXljYVd1aGxPSzN3azRtUkhHc2Rnd1lXbWQ5Y0NoZFBkRTlid1dteTZybjlWcVZOWU5GR2hPZFhyb2ZYVXh3SEluY1NpUE5iOHRObThrbkRWSWVRPT0iXSwKCiAgICAiQHJvbGxkb3duL3BsdWdpbnV0aWxzIjogWyJAcm9sbGRvd24vcGx1Z2ludXRpbHNAMS4wLjEiLCAiIiwge30sICJzaGE1MTItMmo5Ykd0NUpoOGhqK3ZQdGd6UHRsNzJqMHlSeEhBeXVtb282VE5mQWpzTEIwNFV0cFN2UGJQY0RjQk14ejduKzlDWUIwYzFHeFFGeFlSZzJqaW1xR3c9PSJdLAoKICAgICJAc3RhbmRhcmQtc2NoZW1hL3NwZWMiOiBbIkBzdGFuZGFyZC1zY2hlbWEvc3BlY0AxLjEuMCIsICIiLCB7fSwgInNoYTUxMi1sMmFGeTVqQUxobmlHNUhncXJENmpYTGkvclVXckt2cU4vcUp4NnlvSnNnS2hibFZkK2lxcVU0UkNYYXZtL2pQaXR5RG81VEN2S01ucGpLbk9yaXkwdz09Il0sCgogICAgIkBzd2MvaGVscGVycyI6IFsiQHN3Yy9oZWxwZXJzQDAuNS4xNSIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJ0c2xpYiI6ICJeMi44LjAiIH0gfSwgInNoYTUxMi1KUTVUdU1pNDVPd2k0L0JJTUFKQm9TUW9PSnUxMm9Pay9nQURxbGNVTDlKRWRIQjh2eWpVU3N4cWVOWG5tWEhqWUtNaTJXY1l0ZXpHRUVocVVJL0UyZz09Il0sCgogICAgIkB0YWJieV9haS9oaWpyaS1jb252ZXJ0ZXIiOiBbIkB0YWJieV9haS9oaWpyaS1jb252ZXJ0ZXJAMS4wLjUiLCAiIiwge30sICJzaGE1MTItcjViQ2xLcmNJdXNEb28wNDlkU0w4Q2F3bkhSNm1SZER3aGxRdUlnWlJOdHk2OHEweDhrM0xmMUJ0UEFNeFJmL0dnbkhCbklPNHVqZDMrR1FkTFd6eFE9PSJdLAoKICAgICJAdGFpbHdpbmRjc3Mvbm9kZSI6IFsiQHRhaWx3aW5kY3NzL25vZGVANC4zLjAiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiQGpyaWRnZXdlbGwvcmVtYXBwaW5nIjogIl4yLjMuNSIsICJlbmhhbmNlZC1yZXNvbHZlIjogIl41LjIxLjAiLCAiaml0aSI6ICJeMi42LjEiLCAibGlnaHRuaW5nY3NzIjogIjEuMzIuMCIsICJtYWdpYy1zdHJpbmciOiAiXjAuMzAuMjEiLCAic291cmNlLW1hcC1qcyI6ICJeMS4yLjEiLCAidGFpbHdpbmRjc3MiOiAiNC4zLjAiIH0gfSwgInNoYTUxMi1hRmI0Z1VoRk9nZGg5QVhvNEl6QkVPekJra0F4bTlWaWd3REpuTUlZdjNsY2ZYQ0pWZXNOZmJFYUJsNEJOZ1ZSeWlkOTJBbWR2aXF3QlVCUktTZVkzZz09Il0sCgogICAgIkB0YWlsd2luZGNzcy9veGlkZSI6IFsiQHRhaWx3aW5kY3NzL294aWRlQDQuMy4wIiwgIiIsIHsgIm9wdGlvbmFsRGVwZW5kZW5jaWVzIjogeyAiQHRhaWx3aW5kY3NzL294aWRlLWFuZHJvaWQtYXJtNjQiOiAiNC4zLjAiLCAiQHRhaWx3aW5kY3NzL294aWRlLWRhcndpbi1hcm02NCI6ICI0LjMuMCIsICJAdGFpbHdpbmRjc3Mvb3hpZGUtZGFyd2luLXg2NCI6ICI0LjMuMCIsICJAdGFpbHdpbmRjc3Mvb3hpZGUtZnJlZWJzZC14NjQiOiAiNC4zLjAiLCAiQHRhaWx3aW5kY3NzL294aWRlLWxpbnV4LWFybS1nbnVlYWJpaGYiOiAiNC4zLjAiLCAiQHRhaWx3aW5kY3NzL294aWRlLWxpbnV4LWFybTY0LWdudSI6ICI0LjMuMCIsICJAdGFpbHdpbmRjc3Mvb3hpZGUtbGludXgtYXJtNjQtbXVzbCI6ICI0LjMuMCIsICJAdGFpbHdpbmRjc3Mvb3hpZGUtbGludXgteDY0LWdudSI6ICI0LjMuMCIsICJAdGFpbHdpbmRjc3Mvb3hpZGUtbGludXgteDY0LW11c2wiOiAiNC4zLjAiLCAiQHRhaWx3aW5kY3NzL294aWRlLXdhc20zMi13YXNpIjogIjQuMy4wIiwgIkB0YWlsd2luZGNzcy9veGlkZS13aW4zMi1hcm02NC1tc3ZjIjogIjQuMy4wIiwgIkB0YWlsd2luZGNzcy9veGlkZS13aW4zMi14NjQtbXN2YyI6ICI0LjMuMCIgfSB9LCAic2hhNTEyLUY3SFpHQmVOOUkwL0F1dUpTNVB3Y0Q4eGF5eDVyaTVHaGpZVURCRVZZVWtleHlBL2dpd2JETmpSVnJ4U2V6RTNUMjUwT1UySy93cC9sdFd4M1VPZWZnPT0iXSwKCiAgICAiQHRhaWx3aW5kY3NzL294aWRlLWFuZHJvaWQtYXJtNjQiOiBbIkB0YWlsd2luZGNzcy9veGlkZS1hbmRyb2lkLWFybTY0QDQuMy4wIiwgIiIsIHsgIm9zIjogImFuZHJvaWQiLCAiY3B1IjogImFybTY0IiB9LCAic2hhNTEyLVRKUGlxNjd0S2xMdU9iUDZSa3d2VkdEb3hDTUJWdERnS2tMZmEvdXlqNy9GeXh2UXdIUytVT25WclhYZ2JFc2ZVYU1naVZ2QzRLYkpuUnIyNmhvNE5nPT0iXSwKCiAgICAiQHRhaWx3aW5kY3NzL294aWRlLWRhcndpbi1hcm02NCI6IFsiQHRhaWx3aW5kY3NzL294aWRlLWRhcndpbi1hcm02NEA0LjMuMCIsICIiLCB7ICJvcyI6ICJkYXJ3aW4iLCAiY3B1IjogImFybTY0IiB9LCAic2hhNTEyLW9NTi9XWlJiK1NPMzdCbVVFbEVnZUVXdVU4RS9IWFJraU9EeEp4TGUxVVRIVlhMcmRWU2dmYUpWN3BTbGhSR01TT2lYTHV4VElqZnNGM3dZdno4Y2dRPT0iXSwKCiAgICAiQHRhaWx3aW5kY3NzL294aWRlLWRhcndpbi14NjQiOiBbIkB0YWlsd2luZGNzcy9veGlkZS1kYXJ3aW4teDY0QDQuMy4wIiwgIiIsIHsgIm9zIjogImRhcndpbiIsICJjcHUiOiAieDY0IiB9LCAic2hhNTEyLU42Q1VtdTRhNmJLVkFEZnc3N3AraXc2WWQ5UTNPQmhlMHZlYURYK1FhemZ1VllsUXNIZkRneEJyc2pRL0lXK3p5d0w4bVRyTmQwU2RKVC96Z3R2TWRBPT0iXSwKCiAgICAiQHRhaWx3aW5kY3NzL294aWRlLWZyZWVic2QteDY0IjogWyJAdGFpbHdpbmRjc3Mvb3hpZGUtZnJlZWJzZC14NjRANC4zLjAiLCAiIiwgeyAib3MiOiAiZnJlZWJzZCIsICJjcHUiOiAieDY0IiB9LCAic2hhNTEyLXpETDVoQmtRZEg1QzZNcHFiSzNnUUFnUDgwdHNNd1NJMjZ2ak96akp0TkNNVW8wbEZnT0l0ekhLQkl1cE9aTlF4dDNvdVBIN1JQaHZOaGlUZkNlNUNRPT0iXSwKCiAgICAiQHRhaWx3aW5kY3NzL294aWRlLWxpbnV4LWFybS1nbnVlYWJpaGYiOiBbIkB0YWlsd2luZGNzcy9veGlkZS1saW51eC1hcm0tZ251ZWFiaWhmQDQuMy4wIiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJhcm0iIH0sICJzaGE1MTItUjA2SGROaTdBN09Fb01zZjZkNHRqWjcxUkNXblpRUEhqMm1ub3RTRlVSak5MZEJDK2NJZ1hRN2w4MUNxZW9pUWZ0amY2T09ibHhYTUluTWdOMlZ6TUE9PSJdLAoKICAgICJAdGFpbHdpbmRjc3Mvb3hpZGUtbGludXgtYXJtNjQtZ251IjogWyJAdGFpbHdpbmRjc3Mvb3hpZGUtbGludXgtYXJtNjQtZ251QDQuMy4wIiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi1xVEpIRUxYOGpldGpoUlFIQ0xpbGtWTG15YnB6TlFBdGFJL2dhb1ZvaWRuL3VmYk5EYkFvOEtsSzJKK3lQb2M4d1F4dkR4Q21oLzVscjhuQzErbFRiZz09Il0sCgogICAgIkB0YWlsd2luZGNzcy9veGlkZS1saW51eC1hcm02NC1tdXNsIjogWyJAdGFpbHdpbmRjc3Mvb3hpZGUtbGludXgtYXJtNjQtbXVzbEA0LjMuMCIsICIiLCB7ICJvcyI6ICJsaW51eCIsICJjcHUiOiAiYXJtNjQiIH0sICJzaGE1MTItWjZzdWtpUXNuZ25XTytsMzlYNHBQYmlXVDgxSUMrUExLRitQSHhJbHlaYkdOYjlNT0RmWWxYRVZsRnZlajVCT1pJbldYMDFrVnl6ZUx2SHNYaGZjelE9PSJdLAoKICAgICJAdGFpbHdpbmRjc3Mvb3hpZGUtbGludXgteDY0LWdudSI6IFsiQHRhaWx3aW5kY3NzL294aWRlLWxpbnV4LXg2NC1nbnVANC4zLjAiLCAiIiwgeyAib3MiOiAibGludXgiLCAiY3B1IjogIng2NCIgfSwgInNoYTUxMi1EUk5kUVJwU0d6UkdmQVJWdVZreHZNOFExMm5oMTlsNEJGL0c3ekdBMW9lKzl3Y0M2c2FGQkhUSVNycEljS3poaVh0U3JsU3JsdUNmdk11bGVkb0NUUT09Il0sCgogICAgIkB0YWlsd2luZGNzcy9veGlkZS1saW51eC14NjQtbXVzbCI6IFsiQHRhaWx3aW5kY3NzL294aWRlLWxpbnV4LXg2NC1tdXNsQDQuMy4wIiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJ4NjQiIH0sICJzaGE1MTItWjBJQURiRG84Ymg2STdoMklRTXg2MDFBZFhCTGZGcEVkVW90ZnQ4NmV2ZC84WlBmbFplOUNPUE84UTF2dytwZkxXSVVvOXpOL0pHWnZ3dUFKcWR1cWc9PSJdLAoKICAgICJAdGFpbHdpbmRjc3Mvb3hpZGUtd2FzbTMyLXdhc2kiOiBbIkB0YWlsd2luZGNzcy9veGlkZS13YXNtMzItd2FzaUA0LjMuMCIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAZW1uYXBpL2NvcmUiOiAiXjEuMTAuMCIsICJAZW1uYXBpL3J1bnRpbWUiOiAiXjEuMTAuMCIsICJAZW1uYXBpL3dhc2ktdGhyZWFkcyI6ICJeMS4yLjEiLCAiQG5hcGktcnMvd2FzbS1ydW50aW1lIjogIl4xLjEuNCIsICJAdHlieXMvd2FzbS11dGlsIjogIl4wLjEwLjEiLCAidHNsaWIiOiAiXjIuOC4xIiB9LCAiY3B1IjogIm5vbmUiIH0sICJzaGE1MTItSE5aR09VeEVtRWxrc1lSN1M2c0M1alRlTkdwb2JBc3k5dTdHdTBBc2tKOC8yMEZSOUdxZWJVeUIrSEJjVS9heDZCSHVpdUppK09kYTRCK1lYNkgxeUE9PSJdLAoKICAgICJAdGFpbHdpbmRjc3Mvb3hpZGUtd2luMzItYXJtNjQtbXN2YyI6IFsiQHRhaWx3aW5kY3NzL294aWRlLXdpbjMyLWFybTY0LW1zdmNANC4zLjAiLCAiIiwgeyAib3MiOiAid2luMzIiLCAiY3B1IjogImFybTY0IiB9LCAic2hhNTEyLVBlK1JQVlRpMVQrcXltdXVScGNkdndTVlpqbmxsL2Y3bjhnQnhNTWgzeExUY3RNREtxcGRmR2ltYk15aW9xdExoVVlaeGRKOXdHTmhWN01LSHZnWnNRPT0iXSwKCiAgICAiQHRhaWx3aW5kY3NzL294aWRlLXdpbjMyLXg2NC1tc3ZjIjogWyJAdGFpbHdpbmRjc3Mvb3hpZGUtd2luMzIteDY0LW1zdmNANC4zLjAiLCAiIiwgeyAib3MiOiAid2luMzIiLCAiY3B1IjogIng2NCIgfSwgInNoYTUxMi1NdnJmMmtYVy95ZVcvT1RlelpsQ0dPaXJYUmNVdUxJQngvNVkxMkJhUE03d0pvcnlHNmRmUy9OSkw4YUJQcXRURXgvVm00VDR2S3pGVWNLRFQrVEtVQT09Il0sCgogICAgIkB0YWlsd2luZGNzcy9wb3N0Y3NzIjogWyJAdGFpbHdpbmRjc3MvcG9zdGNzc0A0LjMuMCIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAYWxsb2MvcXVpY2stbHJ1IjogIl41LjIuMCIsICJAdGFpbHdpbmRjc3Mvbm9kZSI6ICI0LjMuMCIsICJAdGFpbHdpbmRjc3Mvb3hpZGUiOiAiNC4zLjAiLCAicG9zdGNzcyI6ICJeOC41LjEwIiwgInRhaWx3aW5kY3NzIjogIjQuMy4wIiB9IH0sICJzaGE1MTItSm0wNVRqeCs5eUNMR3Y1cXcxYys4NFBzZHM4TW55ckVRWUNCK0ZGazJsZ0dpVWpsUnFkeGtlNG1WVHVZcmoyeG5WWnFLaW0yQXByNXlTdVFSWUF3L3c9PSJdLAoKICAgICJAdHlieXMvd2FzbS11dGlsIjogWyJAdHlieXMvd2FzbS11dGlsQDAuMTAuMiIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJ0c2xpYiI6ICJeMi40LjAiIH0gfSwgInNoYTUxMi1Sb0J2SjJYMHd1S2xXRklqcndmZkd3MUlxWkhLUXF6SWNoS2FhZFpaZm5OcHNBWXAybU0waDM2SnRQQ2pOREFIR2dZZXovMTV1TUJwZkd3Y2hoaU1nZz09Il0sCgogICAgIkB0eXBlcy9jaGFpIjogWyJAdHlwZXMvY2hhaUA1LjIuMyIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAdHlwZXMvZGVlcC1lcWwiOiAiKiIsICJhc3NlcnRpb24tZXJyb3IiOiAiXjIuMC4xIiB9IH0sICJzaGE1MTItTXc1NThvZUE5ZkZidjY1L3k0bUh0WERzOWJQbkZNWkFML2p4ZFBGVXBPSEhJWFg5MW1jZ0VIYlM1TGFocitwd1pGUjhBN0dRbGVSV2VJNmNHRkMyVUE9PSJdLAoKICAgICJAdHlwZXMvZGVlcC1lcWwiOiBbIkB0eXBlcy9kZWVwLWVxbEA0LjAuMiIsICIiLCB7fSwgInNoYTUxMi1jOWg5ZFZWTWlnTVBjNGJ3VHZDNWR4cXRxSlp3UVBlUHNXalBscFNPbm9qYm9yNnBHcWRrNTQxbGZBN0FxRlFyNXBCMUJSZHEwanVZOWRiODFCd3lGdz09Il0sCgogICAgIkB0eXBlcy9lc3RyZWUiOiBbIkB0eXBlcy9lc3RyZWVAMS4wLjkiLCAiIiwge30sICJzaGE1MTItR2hkUGd5MWVsNC9JbVAwNVgwNVV3NGN3Mi9NOTNCQ1VtbkV2V1pOU3RsQ3pFS01FNEZraytZcG9BNU9pSE5RbW9TN0NhZmI4WGEzUHlhOG0xUXJ6ZWc9PSJdLAoKICAgICJAdHlwZXMvbm9kZSI6IFsiQHR5cGVzL25vZGVAMjIuMTkuMTkiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAidW5kaWNpLXR5cGVzIjogIn42LjIxLjAiIH0gfSwgInNoYTUxMi1keWgveE8yRmg1YllyZldhYXFHclJRUUdrTmRtWXc2QW1hQVV2WWVVTU5UV1F0dmI3OTZpa0xkbVRjaFJtT2xPaUlKMVREWGZXZ1Z4MVFrVWxRNkhldz09Il0sCgogICAgIkB0eXBlcy9yZWFjdCI6IFsiQHR5cGVzL3JlYWN0QDE5LjIuMTUiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiY3NzdHlwZSI6ICJeMy4yLjIiIH0gfSwgInNoYTUxMi1lUndjR05IdmUrRThxdEVRU1NSbDZ1cmgrckZvcDR2OGdtNk84ckd2MjVDb2RidkZkTGpBMXZWUTFLa2lGRTB3MFVQT25iOHREaUZLTDVscDBydFk1UT09Il0sCgogICAgIkB0eXBlcy9yZWFjdC1kb20iOiBbIkB0eXBlcy9yZWFjdC1kb21AMTkuMi4zIiwgIiIsIHsgInBlZXJEZXBlbmRlbmNpZXMiOiB7ICJAdHlwZXMvcmVhY3QiOiAiXjE5LjIuMCIgfSB9LCAic2hhNTEyLWpwMkwvZVk2Zm4rS2dWVlFBT3FZSXRiRjBWWS9ZQXBlNU16MkYwYXlrU084Z3gzMWJZQ1p5dlNlWXhDSEt2ekhHNWVaamMrenlhUzVCckJXeWEyK2tRPT0iXSwKCiAgICAiQHZpdGVzdC9leHBlY3QiOiBbIkB2aXRlc3QvZXhwZWN0QDQuMS42IiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIkBzdGFuZGFyZC1zY2hlbWEvc3BlYyI6ICJeMS4xLjAiLCAiQHR5cGVzL2NoYWkiOiAiXjUuMi4yIiwgIkB2aXRlc3Qvc3B5IjogIjQuMS42IiwgIkB2aXRlc3QvdXRpbHMiOiAiNC4xLjYiLCAiY2hhaSI6ICJeNi4yLjIiLCAidGlueXJhaW5ib3ciOiAiXjMuMS4wIiB9IH0sICJzaGE1MTItN0VIRHF1UHRoQUxTVjBqaGhqZ0VXOEZYYXZpTXg3clNxdThXNm9xQ29BdU9oS292ODE0UDk5UURWMXB4TUEzUVB2MjFZdWR2Sm5nSWhqck5JNG9wTGc9PSJdLAoKICAgICJAdml0ZXN0L21vY2tlciI6IFsiQHZpdGVzdC9tb2NrZXJANC4xLjYiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiQHZpdGVzdC9zcHkiOiAiNC4xLjYiLCAiZXN0cmVlLXdhbGtlciI6ICJeMy4wLjMiLCAibWFnaWMtc3RyaW5nIjogIl4wLjMwLjIxIiB9LCAicGVlckRlcGVuZGVuY2llcyI6IHsgIm1zdyI6ICJeMi40LjkiLCAidml0ZSI6ICJeNi4wLjAgfHwgXjcuMC4wIHx8IF44LjAuMCIgfSwgIm9wdGlvbmFsUGVlcnMiOiBbIm1zdyIsICJ2aXRlIl0gfSwgInNoYTUxMi1NQ0ZjNjNjek1qRUluT2xjWTJjcFFDdkNOK0tnYkFuKzYweHU5Y01nUDRzS2FMQzVKTkFLdzdKSDhRZEFub0FDODhoVzFJaVNOWitHZ1ZYbE4xVWNNUT09Il0sCgogICAgIkB2aXRlc3QvcHJldHR5LWZvcm1hdCI6IFsiQHZpdGVzdC9wcmV0dHktZm9ybWF0QDQuMS42IiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgInRpbnlyYWluYm93IjogIl4zLjEuMCIgfSB9LCAic2hhNTEyLWg1U3hEL0l6TmhaWW5yU1pSc1VaUUlDK3ZEMEdZOGNVdnEwaXdzbWtGS2l4UkNLTExXcUNYYS9GSVE0UzFSK3NJK1BHb29qa0hzZE5yYlppTTlRcGd3PT0iXSwKCiAgICAiQHZpdGVzdC9ydW5uZXIiOiBbIkB2aXRlc3QvcnVubmVyQDQuMS42IiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIkB2aXRlc3QvdXRpbHMiOiAiNC4xLjYiLCAicGF0aGUiOiAiXjIuMC4zIiB9IH0sICJzaGE1MTItbk9QQ21uMit5RDBaTm1LZHNYR3YvVXhNTVdiTXVLZUQ2R3lZbmNOd2RrWUR4cFF2clBTS1lqMnJXdURqQzJZNGI2dzZoamlwNWRCS0Z6RVV1WmUzdkE9PSJdLAoKICAgICJAdml0ZXN0L3NuYXBzaG90IjogWyJAdml0ZXN0L3NuYXBzaG90QDQuMS42IiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIkB2aXRlc3QvcHJldHR5LWZvcm1hdCI6ICI0LjEuNiIsICJAdml0ZXN0L3V0aWxzIjogIjQuMS42IiwgIm1hZ2ljLXN0cmluZyI6ICJeMC4zMC4yMSIsICJwYXRoZSI6ICJeMi4wLjMiIH0gfSwgInNoYTUxMi1ZaHNkRTZ4QVZmVERtemp4TDJaRFV2amorWnNneU9LZStUZFF6cWtENzJ3SU9tSGthOE51R1E2TnBUTlp2OUQyWjYzZmJ3V0tKUGVWcEV3NEVRZ1l4dz09Il0sCgogICAgIkB2aXRlc3Qvc3B5IjogWyJAdml0ZXN0L3NweUA0LjEuNiIsICIiLCB7fSwgInNoYTUxMi1KRkt4TXg2dWRod0toL0xkbzI3MGUxN1FYNzEwdmd1bk1rdVBBdlhqSFN2QzZvcUxXQUhoVmhqZy9JNzFxMHUwQ0JTRXJJT0RWMUtqdjBGUU5TV2pkZz09Il0sCgogICAgIkB2aXRlc3QvdXRpbHMiOiBbIkB2aXRlc3QvdXRpbHNANC4xLjYiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiQHZpdGVzdC9wcmV0dHktZm9ybWF0IjogIjQuMS42IiwgImNvbnZlcnQtc291cmNlLW1hcCI6ICJeMi4wLjAiLCAidGlueXJhaW5ib3ciOiAiXjMuMS4wIiB9IH0sICJzaGE1MTItRnhJWStVODFSM0xHS0N4YUhIRlJRNStnNi9pUmdHTG1lSFdkcDJBbWo0bGpRUnJFSVdIbVp5RGZEWUJSWmxweXFBN3FLeHRTOUREMWRoazhSblJJVlE9PSJdLAoKICAgICJhbnNpLXJlZ2V4IjogWyJhbnNpLXJlZ2V4QDUuMC4xIiwgIiIsIHt9LCAic2hhNTEyLXF1SlFYbFRTVUdMMkxIOVNVWG84VndzWTRzb2FuaGdvNkxOU204NEUxTEJjRThzM08wd3BkaVJ6eVI5ei9aWkpNbE1XdjM3cU9PYjlwZEpsTVVFS0ZRPT0iXSwKCiAgICAiYW5zaS1zdHlsZXMiOiBbImFuc2ktc3R5bGVzQDQuMy4wIiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgImNvbG9yLWNvbnZlcnQiOiAiXjIuMC4xIiB9IH0sICJzaGE1MTItemJCOXJDSkFUMXJiamlWRGIyaHFLRkhOWUx4Z3RrOE5VUnhaM0lad0QzRjZOdHhiWFpRQ25uU2kxTGt4K0lEb2hkUGxGcDIyMndWQUxJaGVaSlFTRWc9PSJdLAoKICAgICJhc3NlcnRpb24tZXJyb3IiOiBbImFzc2VydGlvbi1lcnJvckAyLjAuMSIsICIiLCB7fSwgInNoYTUxMi1Jemk4UlFjZmZxQ2VOVmdGaWdLbGkxc3NrbElicEhuQ1ljNkFrblhHWW9CNmdySnF5ZWJ5N2p2MTJKVVFnbVRBbklEbmJjazF1eGtzVDRkek4zUFdCQT09Il0sCgogICAgImJhc2VsaW5lLWJyb3dzZXItbWFwcGluZyI6IFsiYmFzZWxpbmUtYnJvd3Nlci1tYXBwaW5nQDIuMTAuMzEiLCAiIiwgeyAiYmluIjogeyAiYmFzZWxpbmUtYnJvd3Nlci1tYXBwaW5nIjogImRpc3QvY2xpLmNqcyIgfSB9LCAic2hhNTEyLU11allPM2VQNzJ1dm1TRTBpNHdsdHNvZFJmSXBaQVRQM2p2elJOUkdHeGd6SWQ3YVZvY1ZKSlYzbmYwMXFuenpLRkd4UVZDOWJwV3hsNWNqeFRyLzdRPT0iXSwKCiAgICAiY2FuaXVzZS1saXRlIjogWyJjYW5pdXNlLWxpdGVAMS4wLjMwMDAxNzkzIiwgIiIsIHt9LCAic2hhNTEyLWl3U3NZV2FDT29oMjZjVjhOd05SVmlIbHJmVXZZc0hEZlJWY2J0bXcwS2c2UEpJWlpYd01rajE0NDJGWUxCR2tlVWYxanVBc1UzRFRmeFc1NzltclBBPT0iXSwKCiAgICAiY2hhaSI6IFsiY2hhaUA2LjIuMiIsICIiLCB7fSwgInNoYTUxMi1OVVBSbHVPZk9pVEtCS3ZXUHRTRDRQaEZ2V0NxT2kwQkdTdE5XczU3WDlqczdYR1RwclNtRm96NUYwdFdoUjRXUGpOZVI5alhxZEM3L1VwU0pUbmxSZz09Il0sCgogICAgImNoYWxrIjogWyJjaGFsa0A0LjEuMiIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJhbnNpLXN0eWxlcyI6ICJeNC4xLjAiLCAic3VwcG9ydHMtY29sb3IiOiAiXjcuMS4wIiB9IH0sICJzaGE1MTItb0tuYmhGeVJJWHBVdWV6OGlCTW15RWE0bmJqNElPUXl1aGMvd3k5a1k3L1dWUGN3SU85VkE2NjhQdThSa083KzBHNzZTTFJPZXl3OUNwUTA2MWk0bUE9PSJdLAoKICAgICJjbGllbnQtb25seSI6IFsiY2xpZW50LW9ubHlAMC4wLjEiLCAiIiwge30sICJzaGE1MTItSVYzT3UwalNNelpyZDNwWjQ4bkxrVDlEQTdBZzFwblB6YWlRaHBXN2MzUmJjcXF6dnp6VnUrTDhnZnFNcC84SU0yTVF0U2lxYUN4cnJjZnU4SThyTUE9PSJdLAoKICAgICJjbGl1aSI6IFsiY2xpdWlAOC4wLjEiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAic3RyaW5nLXdpZHRoIjogIl40LjIuMCIsICJzdHJpcC1hbnNpIjogIl42LjAuMSIsICJ3cmFwLWFuc2kiOiAiXjcuMC4wIiB9IH0sICJzaGE1MTItQlNlTm55dXM3NUM0Ly9OUTlnUXQxL2NzVFh5by84U2IrYWZMQWt6QXB0RnVNc29kOUhGb2tHTnVkWnBpL29RVjczaG5WSytzUis1UFZSTWQrRHI3WVE9PSJdLAoKICAgICJjbHN4IjogWyJjbHN4QDIuMS4xIiwgIiIsIHt9LCAic2hhNTEyLWVZbTBRV0J0VXJCV1pXRzBkMzg2T0dBdzE2Wjk5NVBpT1ZvMkI3YmpXU2JIZWRHbDVlMFpXYXE2NWtPR2dVU05lc0VJRGtCOUlTYlRnL0pLOWRoQ1pBPT0iXSwKCiAgICAiY29sb3ItY29udmVydCI6IFsiY29sb3ItY29udmVydEAyLjAuMSIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJjb2xvci1uYW1lIjogIn4xLjEuNCIgfSB9LCAic2hhNTEyLVJSRUNQc2o3aXUveGI1b0tZY3NGSFNwcEZObnNqLzUyT1ZUUktiNHpQNW9uWHdWRjN6Vm1tVG9OY09mR0MrQ1JEcGZLL1U1ODRmTWczOFpIQ2FFbEtRPT0iXSwKCiAgICAiY29sb3ItbmFtZSI6IFsiY29sb3ItbmFtZUAxLjEuNCIsICIiLCB7fSwgInNoYTUxMi1kT3krM0F1VzNhMndOYlpISXVNWnBUY2dqR3VMVS91QkwvdWJjWkY5T1hiRG84ZmY0Tzh5VnA1QmYwZWZTOHVFb1lvNXE0Rng3ZFk5T2dRR1hnQXNRQT09Il0sCgogICAgImNvbmN1cnJlbnRseSI6IFsiY29uY3VycmVudGx5QDkuMi4xIiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgImNoYWxrIjogIjQuMS4yIiwgInJ4anMiOiAiNy44LjIiLCAic2hlbGwtcXVvdGUiOiAiMS44LjMiLCAic3VwcG9ydHMtY29sb3IiOiAiOC4xLjEiLCAidHJlZS1raWxsIjogIjEuMi4yIiwgInlhcmdzIjogIjE3LjcuMiIgfSwgImJpbiI6IHsgImNvbmMiOiAiZGlzdC9iaW4vY29uY3VycmVudGx5LmpzIiwgImNvbmN1cnJlbnRseSI6ICJkaXN0L2Jpbi9jb25jdXJyZW50bHkuanMiIH0gfSwgInNoYTUxMi1mc2ZyTzBNeFY2NFpub3k4L2wxdlZJampIYTI5U1p5eXFQZ1FCd2hpRGNhVzh3SmMyVzNYV1ZPR3g0TTNvSkJudi96ZFVaSUlwMWdEZVM5OEd6UDhOZz09Il0sCgogICAgImNvbnZlcnQtc291cmNlLW1hcCI6IFsiY29udmVydC1zb3VyY2UtbWFwQDIuMC4wIiwgIiIsIHt9LCAic2hhNTEyLUt2cDQ1OUhyVjJGRUoxQ0FzaTFLdStNWTNrYXNIMTlURnlrVHoyeFdtTWVxNmJrMk5VM1hYdmZKK1E2MW0weGt0V3d0KzFIU1lmM0pac1RtczNhUkpnPT0iXSwKCiAgICAiY3NzdHlwZSI6IFsiY3NzdHlwZUAzLjIuMyIsICIiLCB7fSwgInNoYTUxMi16MUhHS2NZeTJ4QThBR1Fmd3JuMFBBeStQQjdYL0dTajNVVkpXOXFLeW40M3hXYStnbDVuWG1VNHFxTE1SeldWTEZDOEt1c1VYOFQvMGtDaU9ZcEFJUT09Il0sCgogICAgImRhdGUtZm5zIjogWyJkYXRlLWZuc0A0LjIuMSIsICIiLCB7fSwgInNoYTUxMi0zN1JoU2R4YUcxc3VlbjZWREN6YTZyTnJRZm9veVFoNTdIRlZQd1FHRXEyUVdsaVZMelBRWjhPYTAxN3dlT3UrSFpDbnpJN04zUGYvd3lvQktmRXFyQT09Il0sCgogICAgImRhdGUtZm5zLWphbGFsaSI6IFsiZGF0ZS1mbnMtamFsYWxpQDQuMS4wLTAiLCAiIiwge30sICJzaGE1MTItaFRJUC96K3QrcUt3QkRjbW1zbm1qV1RkdXhDZys1S2ZkcVdRdmIyWC84Qzkra25ZWTZlcE4vcGZ4ZER1eVZsU1ZlRnowc001ZUVmd0lVUTcwVTRja2c9PSJdLAoKICAgICJkZXRlY3QtbGliYyI6IFsiZGV0ZWN0LWxpYmNAMi4xLjIiLCAiIiwge30sICJzaGE1MTItQnRqMkJPT084M28zV3lINTllOE1nWHN4RVFWY2Fya1VPcEVZcnViQjB1cnduTjEweVEzNjRyc2lCeVUxMW5abHFXWVptMDVpL29mN2lvNG16aWhCdFE9PSJdLAoKICAgICJlbW9qaS1yZWdleCI6IFsiZW1vamktcmVnZXhAOC4wLjAiLCAiIiwge30sICJzaGE1MTItTVNqWXpjV05PQTBld0FIcHowTXhwWUZ2d2c2eWp5MU5HM3h0ZW9xejY0NFZDby9SUGducjEvR0d0K2ljM2lKVHpROEV1M1RkTTE0U2F3blZVbUdFNkE9PSJdLAoKICAgICJlbmhhbmNlZC1yZXNvbHZlIjogWyJlbmhhbmNlZC1yZXNvbHZlQDUuMjEuNSIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJncmFjZWZ1bC1mcyI6ICJeNC4yLjQiLCAidGFwYWJsZSI6ICJeMi4zLjMiIH0gfSwgInNoYTUxMi1tTENOYnJRbGkxMUsxeVNVbXVOdDRaVUIzT3BHSURxNHEydlRCVGY1Y0wybHBzUmpJOVFLcVNEMG5kalc4Rnl2Y1cvSmo0NmdNZTlzeXlIQXN2TWEvQT09Il0sCgogICAgImVzLW1vZHVsZS1sZXhlciI6IFsiZXMtbW9kdWxlLWxleGVyQDIuMS4wIiwgIiIsIHt9LCAic2hhNTEyLW4yN3pUWU1qWXUxYWo0TWpDV3pTUDdHOXI3NXV0c2FvYzhtNjF3ZUsrVzhKTUJHR1F5YmQ0M0dzdENYWjNXTm1TRnRHVDl3aTU5cVFUVzZtaFRSNUxRPT0iXSwKCiAgICAiZXNjYWxhZGUiOiBbImVzY2FsYWRlQDMuMi4wIiwgIiIsIHt9LCAic2hhNTEyLVdVajJxbHhhUXRPNGc2UHE1YzI5R1RjV0dEeWQ4aXRMOHpUbGlwZ0VDejNKZXNBaWlPS290ZDhKVTZvdEIzUEFDZ0c2eGtKVXlWaGJvTVMrYmplL2pBPT0iXSwKCiAgICAiZXN0cmVlLXdhbGtlciI6IFsiZXN0cmVlLXdhbGtlckAzLjAuMyIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAdHlwZXMvZXN0cmVlIjogIl4xLjAuMCIgfSB9LCAic2hhNTEyLTdSVUtmWGdTTU1renQ2WnVYbXFhcE91ckxHUFBmZ2o2bDl1Ulo3bFJHb2x2azB5MnlvY2MzNUxkY3hLQzVQUVpkbjJETXFpb0FRMk5vV2NyVEttbTZnPT0iXSwKCiAgICAiZXhwZWN0LXR5cGUiOiBbImV4cGVjdC10eXBlQDEuMy4wIiwgIiIsIHt9LCAic2hhNTEyLWtudnllYXVZaHFqT1l2UTY2TXpuU01zODN3bUhyQ3ljTkVONkFvKzJBZVlFZnhVSWt1aVZ4ZEVhMXFsR0VQSytXZTNuMFRIaURjaVlTc0NjZ1cvRG9BPT0iXSwKCiAgICAiZmRpciI6IFsiZmRpckA2LjUuMCIsICIiLCB7ICJwZWVyRGVwZW5kZW5jaWVzIjogeyAicGljb21hdGNoIjogIl4zIHx8IF40IiB9LCAib3B0aW9uYWxQZWVycyI6IFsicGljb21hdGNoIl0gfSwgInNoYTUxMi10SWJZdFpidWNPczBCUkdxUEprc2hKVVlkTCtTREg3ZFZNOGdqeStFUnAzV0FVakxFRkpFKzAya2FueUh0d2pXT253cktZQml3QW1NMHA0a0xKQW5YZz09Il0sCgogICAgImZzZXZlbnRzIjogWyJmc2V2ZW50c0AyLjMuMyIsICIiLCB7ICJvcyI6ICJkYXJ3aW4iIH0sICJzaGE1MTItNXhvRGZYK2ZMN2ZhQVRuYWdtV1BwYkZ0d2gvUjc3V21NTXFxSEdTNjVDM3Z2QjBZSHJnRitCMVltWjM0NDF0TWo1bjYzazAyMTJYTm9Kd3psaGZmUXc9PSJdLAoKICAgICJnZXQtY2FsbGVyLWZpbGUiOiBbImdldC1jYWxsZXItZmlsZUAyLjAuNSIsICIiLCB7fSwgInNoYTUxMi1EeUZQM0JNLzNZSFRRT0NVTC93ME9aSFIwbHBLZUdyeG90Y0hXY3FORWRubHRxRndYVmZoRUJROTRlSW8zNEFmUXBvMHJHa2k0Y3lJaWZ0WTA2aDJGZz09Il0sCgogICAgImdyYWNlZnVsLWZzIjogWyJncmFjZWZ1bC1mc0A0LjIuMTEiLCAiIiwge30sICJzaGE1MTItUmJKNS9qbUZjTk5DY0RWNW85ZVRuQkxKL0hzeldWMFA3M2JjK0ZmNG5TL3JKaitZYVM2SUd5aU9MMFZvQllYK2wxV3JsM2s2M2gvS3JIK25oSjBYdlE9PSJdLAoKICAgICJoYXMtZmxhZyI6IFsiaGFzLWZsYWdANC4wLjAiLCAiIiwge30sICJzaGE1MTItRXlrSlQvUTFLalRXY3RwcGdJQWdmU08wdEtWdVpVamhnTXIxN2txVHVtTWw2QWZ2M0VJU2xlVTdxWlV6b1hERlRBSFREQzROT29HL1p4VTNFdmxNUFE9PSJdLAoKICAgICJpcy1mdWxsd2lkdGgtY29kZS1wb2ludCI6IFsiaXMtZnVsbHdpZHRoLWNvZGUtcG9pbnRAMy4wLjAiLCAiIiwge30sICJzaGE1MTItenltbTUrdStzQ3NTV3lEOXFOYWVqVjNERnZoQ0tjbEtkaXpZYUpVdUhBODNSTGpiN25TdUduZGRDSEd2MGhrK0tZN0JNQWxzV2VLNFVlZzZFVjZYUWc9PSJdLAoKICAgICJqaXRpIjogWyJqaXRpQDIuNy4wIiwgIiIsIHsgImJpbiI6IHsgImppdGkiOiAibGliL2ppdGktY2xpLm1qcyIgfSB9LCAic2hhNTEyLUFDLzdKb2ZKdlpHcnJuZVdOYUVuSmVPTFV4K0psR3Q3dE5hMHdaaVJQVDRNWTF3bWZLanQyKzZPMnAydXoyK3NrbGw4T1pabUpNTnFla2U3a0tiTmdRPT0iXSwKCiAgICAibGlicGhvbmVudW1iZXItanMiOiBbImxpYnBob25lbnVtYmVyLWpzQDEuMTIuNDEiLCAiIiwge30sICJzaGE1MTItbHNtTW1HWEJ4WElLL1ZNTEVqMGtMNk10VXMxa0JHajFuVEN6aTZ6Z1FvRzFERXdxd3QyRFF5SHhjTHlrY2VJeEFuZkUzaHlhN051SWg2UHBDNlMzZkE9PSJdLAoKICAgICJsaWdodG5pbmdjc3MiOiBbImxpZ2h0bmluZ2Nzc0AxLjMyLjAiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiZGV0ZWN0LWxpYmMiOiAiXjIuMC4zIiB9LCAib3B0aW9uYWxEZXBlbmRlbmNpZXMiOiB7ICJsaWdodG5pbmdjc3MtYW5kcm9pZC1hcm02NCI6ICIxLjMyLjAiLCAibGlnaHRuaW5nY3NzLWRhcndpbi1hcm02NCI6ICIxLjMyLjAiLCAibGlnaHRuaW5nY3NzLWRhcndpbi14NjQiOiAiMS4zMi4wIiwgImxpZ2h0bmluZ2Nzcy1mcmVlYnNkLXg2NCI6ICIxLjMyLjAiLCAibGlnaHRuaW5nY3NzLWxpbnV4LWFybS1nbnVlYWJpaGYiOiAiMS4zMi4wIiwgImxpZ2h0bmluZ2Nzcy1saW51eC1hcm02NC1nbnUiOiAiMS4zMi4wIiwgImxpZ2h0bmluZ2Nzcy1saW51eC1hcm02NC1tdXNsIjogIjEuMzIuMCIsICJsaWdodG5pbmdjc3MtbGludXgteDY0LWdudSI6ICIxLjMyLjAiLCAibGlnaHRuaW5nY3NzLWxpbnV4LXg2NC1tdXNsIjogIjEuMzIuMCIsICJsaWdodG5pbmdjc3Mtd2luMzItYXJtNjQtbXN2YyI6ICIxLjMyLjAiLCAibGlnaHRuaW5nY3NzLXdpbjMyLXg2NC1tc3ZjIjogIjEuMzIuMCIgfSB9LCAic2hhNTEyLU5YWUJ6aW5OcmJsZnJhUEd5cmJQb0QxOUMxaDlsZkkvMW16Z1dZdlhVVGU0MTRHei9YMUZEMlhCWlNaTTdyUlRyTUE4SkwzT3RBYUdpZnJJS2hRNXlRPT0iXSwKCiAgICAibGlnaHRuaW5nY3NzLWFuZHJvaWQtYXJtNjQiOiBbImxpZ2h0bmluZ2Nzcy1hbmRyb2lkLWFybTY0QDEuMzIuMCIsICIiLCB7ICJvcyI6ICJhbmRyb2lkIiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi1ZSzcvQ2xUdDRrQUswdm82dzNYK1BubTBEMmNmMnZQSGJoT1hkb050aTFHYTBhbDFQNFRCWmh3akFUdmpOd0xFQkNuS3ZqSmMyalFnSFhIME5Fd2xBZz09Il0sCgogICAgImxpZ2h0bmluZ2Nzcy1kYXJ3aW4tYXJtNjQiOiBbImxpZ2h0bmluZ2Nzcy1kYXJ3aW4tYXJtNjRAMS4zMi4wIiwgIiIsIHsgIm9zIjogImRhcndpbiIsICJjcHUiOiAiYXJtNjQiIH0sICJzaGE1MTItUnplRzlKdTViYWcyQnYxL2x3bFZKdkJFM3E2VHRYc2tkWkxMQ3lmZzVwdCtITHo5QnFsSUNPN0xaTTdWSE5UVG4vNVBSaEhGQlNqazVsYzRjbXNjUFE9PSJdLAoKICAgICJsaWdodG5pbmdjc3MtZGFyd2luLXg2NCI6IFsibGlnaHRuaW5nY3NzLWRhcndpbi14NjRAMS4zMi4wIiwgIiIsIHsgIm9zIjogImRhcndpbiIsICJjcHUiOiAieDY0IiB9LCAic2hhNTEyLVUrUXNCcDJtL3Myd3FwVVlULzZ3bmxhZ2RaYnRaZG5kU211dC9OSnFsQ2NNTFRXcDVtdUNySUQrSzVVSjZqcUQyQkZzaGVqQ1lYbmlQRGJOaDczVjh3PT0iXSwKCiAgICAibGlnaHRuaW5nY3NzLWZyZWVic2QteDY0IjogWyJsaWdodG5pbmdjc3MtZnJlZWJzZC14NjRAMS4zMi4wIiwgIiIsIHsgIm9zIjogImZyZWVic2QiLCAiY3B1IjogIng2NCIgfSwgInNoYTUxMi1KQ1RpZ2VkRWtzWmszdEhUVHRobk1kVmZHZjYxRmt5OEppMkU0WWpVVEVRWDE0eGl5L2xUelhudTF2d2laZTNiWWUwcStTcHNTSC9DVGVEWEs2V0hpZz09Il0sCgogICAgImxpZ2h0bmluZ2Nzcy1saW51eC1hcm0tZ251ZWFiaWhmIjogWyJsaWdodG5pbmdjc3MtbGludXgtYXJtLWdudWVhYmloZkAxLjMyLjAiLCAiIiwgeyAib3MiOiAibGludXgiLCAiY3B1IjogImFybSIgfSwgInNoYTUxMi14NnJubnBSYTJHTDB6UU9rdDZydHMzWURQemR1THBXdndBRjZFTWhYRlZaWEQ0dFByQmtFRnF6R293ekNzSVdzUGpxU0srdHlORU9EVUJYZWVWSFNrdz09Il0sCgogICAgImxpZ2h0bmluZ2Nzcy1saW51eC1hcm02NC1nbnUiOiBbImxpZ2h0bmluZ2Nzcy1saW51eC1hcm02NC1nbnVAMS4zMi4wIiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi0wbm5NeW95T0xSSlhmYk1PaWxhU1JjTEgzSnc1ejlIRE5HZlQvZ3dDUGdhRGpueDBpOHc3dkJ6RkxGUjFmNkNNTEtGOGdWYmVibWtVTjNmYS9rUUpwUT09Il0sCgogICAgImxpZ2h0bmluZ2Nzcy1saW51eC1hcm02NC1tdXNsIjogWyJsaWdodG5pbmdjc3MtbGludXgtYXJtNjQtbXVzbEAxLjMyLjAiLCAiIiwgeyAib3MiOiAibGludXgiLCAiY3B1IjogImFybTY0IiB9LCAic2hhNTEyLVVwUWtvZW5yNFVKRXpnVklZcEk4MGxERnZSbVBWZzZvcWJvTkhmb0g0Q1FJZk5BK0hPclo3TW83S1pQMDJkQzZMamdoUFFKZUJzdlhoSm9kL3duSUJnPT0iXSwKCiAgICAibGlnaHRuaW5nY3NzLWxpbnV4LXg2NC1nbnUiOiBbImxpZ2h0bmluZ2Nzcy1saW51eC14NjQtZ251QDEuMzIuMCIsICIiLCB7ICJvcyI6ICJsaW51eCIsICJjcHUiOiAieDY0IiB9LCAic2hhNTEyLVY3UXI1MkloWm1kS1BWcitWdHc4bytXTHNRSllDVGQ4bG9JZnBEYU1SV0dVWmZCT1lFSmV5SklrcUdJRE1aUHdQeDI0cFVNZndTeHhJOHBoci9NYk9BPT0iXSwKCiAgICAibGlnaHRuaW5nY3NzLWxpbnV4LXg2NC1tdXNsIjogWyJsaWdodG5pbmdjc3MtbGludXgteDY0LW11c2xAMS4zMi4wIiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJ4NjQiIH0sICJzaGE1MTItYlljTHArVmIwYXdzaVhnLzgwdUNSZXpDWUhOZzEvbDNtdDBnekhuV1Y5WFAxVzVzS2E1L1RDZEdXYVIvekJNMlBlRi9IYnNRdi9qMlVSTk9pVnV4V2c9PSJdLAoKICAgICJsaWdodG5pbmdjc3Mtd2luMzItYXJtNjQtbXN2YyI6IFsibGlnaHRuaW5nY3NzLXdpbjMyLWFybTY0LW1zdmNAMS4zMi4wIiwgIiIsIHsgIm9zIjogIndpbjMyIiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi04U2JDOEJSNDBwUzZiYUNNOHNidFlEU3dFVlFkNEpsRlRPbGFEM2dXR0hmVGhUY0FCbk5EQmRhNmVUWmVxYm9mYWxJSmhGeDBxS3pnSEptY1BUbkdkdz09Il0sCgogICAgImxpZ2h0bmluZ2Nzcy13aW4zMi14NjQtbXN2YyI6IFsibGlnaHRuaW5nY3NzLXdpbjMyLXg2NC1tc3ZjQDEuMzIuMCIsICIiLCB7ICJvcyI6ICJ3aW4zMiIsICJjcHUiOiAieDY0IiB9LCAic2hhNTEyLUFtcTlCL1NvWllkRGkxa0Zyb2pub3FQTHhZaFE0V281WGlMOEVWSnJWc0I4QVJvQzFQV1c2Vkd0VDBXS0NlbWp5OGFDK2xvdUpualM3VTE4eDNiMDZRPT0iXSwKCiAgICAibWFnaWMtc3RyaW5nIjogWyJtYWdpYy1zdHJpbmdAMC4zMC4yMSIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAanJpZGdld2VsbC9zb3VyY2VtYXAtY29kZWMiOiAiXjEuNS41IiB9IH0sICJzaGE1MTItdmQyRjRZVXlFWEtHY0xIb3ErVEV5Q2p4dWVTZUhuRnh5eWpOcDgweWcwWFY0dlVobkRlci9sdnZscU0vYXJCNWJYUU41SzIvM29pbnlDUnl4OFQyQ1E9PSJdLAoKICAgICJuYW5vaWQiOiBbIm5hbm9pZEAzLjMuMTIiLCAiIiwgeyAiYmluIjogeyAibmFub2lkIjogImJpbi9uYW5vaWQuY2pzIiB9IH0sICJzaGE1MTItWkI5UkgvMzlxcHE1VnU2WStObVVhRmhRUjZwcCtNMlh0NzZYQm5Fd0RhR2NWQXFobHZ4cmwzQjJiS1M1RDNOSDNRUjc2djNhU3JLYUYvS2l5N2xFdFE9PSJdLAoKICAgICJuZXh0IjogWyJuZXh0QDE2LjIuNiIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAbmV4dC9lbnYiOiAiMTYuMi42IiwgIkBzd2MvaGVscGVycyI6ICIwLjUuMTUiLCAiYmFzZWxpbmUtYnJvd3Nlci1tYXBwaW5nIjogIl4yLjkuMTkiLCAiY2FuaXVzZS1saXRlIjogIl4xLjAuMzAwMDE1NzkiLCAicG9zdGNzcyI6ICI4LjQuMzEiLCAic3R5bGVkLWpzeCI6ICI1LjEuNiIgfSwgIm9wdGlvbmFsRGVwZW5kZW5jaWVzIjogeyAiQG5leHQvc3djLWRhcndpbi1hcm02NCI6ICIxNi4yLjYiLCAiQG5leHQvc3djLWRhcndpbi14NjQiOiAiMTYuMi42IiwgIkBuZXh0L3N3Yy1saW51eC1hcm02NC1nbnUiOiAiMTYuMi42IiwgIkBuZXh0L3N3Yy1saW51eC1hcm02NC1tdXNsIjogIjE2LjIuNiIsICJAbmV4dC9zd2MtbGludXgteDY0LWdudSI6ICIxNi4yLjYiLCAiQG5leHQvc3djLWxpbnV4LXg2NC1tdXNsIjogIjE2LjIuNiIsICJAbmV4dC9zd2Mtd2luMzItYXJtNjQtbXN2YyI6ICIxNi4yLjYiLCAiQG5leHQvc3djLXdpbjMyLXg2NC1tc3ZjIjogIjE2LjIuNiIsICJzaGFycCI6ICJeMC4zNC41IiB9LCAicGVlckRlcGVuZGVuY2llcyI6IHsgIkBvcGVudGVsZW1ldHJ5L2FwaSI6ICJeMS4xLjAiLCAiQHBsYXl3cmlnaHQvdGVzdCI6ICJeMS41MS4xIiwgImJhYmVsLXBsdWdpbi1yZWFjdC1jb21waWxlciI6ICIqIiwgInJlYWN0IjogIl4xOC4yLjAgfHwgMTkuMC4wLXJjLWRlNjhkMmY0LTIwMjQxMjA0IHx8IF4xOS4wLjAiLCAicmVhY3QtZG9tIjogIl4xOC4yLjAgfHwgMTkuMC4wLXJjLWRlNjhkMmY0LTIwMjQxMjA0IHx8IF4xOS4wLjAiLCAic2FzcyI6ICJeMS4zLjAiIH0sICJvcHRpb25hbFBlZXJzIjogWyJAb3BlbnRlbGVtZXRyeS9hcGkiLCAiQHBsYXl3cmlnaHQvdGVzdCIsICJiYWJlbC1wbHVnaW4tcmVhY3QtY29tcGlsZXIiLCAic2FzcyJdLCAiYmluIjogeyAibmV4dCI6ICJkaXN0L2Jpbi9uZXh0IiB9IH0sICJzaGE1MTItcU9WZ0tKZzErQXQxNU5wZVVQK2VKZ0NIdlRDZ1hzb2d3ZXE4N1JpL0l4N1BrcVFIZzRzZGFYbVNGcUtsZ2FJWEU0a1cwZzI1TEU2OFc4N1VBTmxIdHc9PSJdLAoKICAgICJvYnVnIjogWyJvYnVnQDIuMS4xIiwgIiIsIHt9LCAic2hhNTEyLXVUcUY5TXVQcmFBUStJc25QZjM2NlJHNGNQOVJ0VWk3TUxPMU4zS0VjK3diMGE2eUtwZUwwbG1rMklCMWpZNUtIUEFsVGM2VC9KUmRDL1lxeEhOd2tRPT0iXSwKCiAgICAicGF0aGUiOiBbInBhdGhlQDIuMC4zIiwgIiIsIHt9LCAic2hhNTEyLVdVakdjQXFQMWdRYWNvUWUrT0JKc0ZBN0xkNER5WHVVSWpaNWNjNzVjTEh2SjdkdE5zVHVncGh4SUFEd3NwUytBcmFBVWVQQ0tyU1Z0UExGai9GODh3PT0iXSwKCiAgICAicGljb2NvbG9ycyI6IFsicGljb2NvbG9yc0AxLjEuMSIsICIiLCB7fSwgInNoYTUxMi14Y2VIMnNuaHRiNU05bGlxRHNtRXc1NmxlMzc2bVRaa0VYL2pFYi9SeE5GeWVnTnVsN2VOc2xDWFA5RkRqL0xjdTBYOEtFeU1jZVAybnRwYUhyREVWQT09Il0sCgogICAgInBpY29tYXRjaCI6IFsicGljb21hdGNoQDQuMC40IiwgIiIsIHt9LCAic2hhNTEyLVFQODhCQUt2TWFtLzNOeEg2dmoybzIxUjZNanhaVUFkNm5sd0FTL3BuR3ZOOUlWTG9jTEh4R1lJekZoZzZmVVErNXRoNlA0ZHY0ZVc5algzRFNJajdBPT0iXSwKCiAgICAicG9zdGNzcyI6IFsicG9zdGNzc0A4LjUuMTUiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAibmFub2lkIjogIl4zLjMuMTIiLCAicGljb2NvbG9ycyI6ICJeMS4xLjEiLCAic291cmNlLW1hcC1qcyI6ICJeMS4yLjEiIH0gfSwgInNoYTUxMi1GZlI4c2pkNGVtMlQ2ZmIzSTJNd0FKVTdIV1ZNcjl6YmErZW5tUWVlV0ZmQ2JtK1VPQy8wWDREUzhYdHBVVE13V01HYmpLWVA3eGpmTmVrenlHbUIzQT09Il0sCgogICAgInJlYWN0IjogWyJyZWFjdEAxOS4yLjYiLCAiIiwge30sICJzaGE1MTItc2ZXR0dmYXZpMHhyOFBnMHNWc3lITUFPemlWWUtnUExOclM3aWcraXZNTmIzd2JDQnczS3h0ZmxzR0JBd0QzZ1lRbEUvQUVac1RMZ1RvUnJTQ2piMFE9PSJdLAoKICAgICJyZWFjdC1kYXktcGlja2VyIjogWyJyZWFjdC1kYXktcGlja2VyQDkuMTQuMCIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAZGF0ZS1mbnMvdHoiOiAiXjEuNC4xIiwgIkB0YWJieV9haS9oaWpyaS1jb252ZXJ0ZXIiOiAiMS4wLjUiLCAiZGF0ZS1mbnMiOiAiXjQuMS4wIiwgImRhdGUtZm5zLWphbGFsaSI6ICI0LjEuMC0wIiB9LCAicGVlckRlcGVuZGVuY2llcyI6IHsgInJlYWN0IjogIj49MTYuOC4wIiB9IH0sICJzaGE1MTItdEJhb0RXalB3ZTBNNXBHcnVtNEgwU1I2THlrK0JPOW9IbnA5SmJLcEdLVzJtbHJhTlBnUDlCTWZzZzVwV3B3cnNzQVJtZXFrN1lCbDJvWHV0WlRhSEE9PSJdLAoKICAgICJyZWFjdC1kb20iOiBbInJlYWN0LWRvbUAxOS4yLjYiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAic2NoZWR1bGVyIjogIl4wLjI3LjAiIH0sICJwZWVyRGVwZW5kZW5jaWVzIjogeyAicmVhY3QiOiAiXjE5LjIuNiIgfSB9LCAic2hhNTEyLTBwck1JK2h2QmJQanNXbnhETHhsQ0d5TThQTjZVdVdqRVVDWW1aaE82N3hJVjlYYXNhL3IvdkRucStYeXE0TG8yN2c4UVNiTzVZekFSdTBEMVNwczNnPT0iXSwKCiAgICAicmVxdWlyZS1kaXJlY3RvcnkiOiBbInJlcXVpcmUtZGlyZWN0b3J5QDIuMS4xIiwgIiIsIHt9LCAic2hhNTEyLWZHeEVJNyt3c0c5eHJ2ZGpzcmxtTDIyT01UVGlIUndBTXJvaUVlTWdxOGd6b0xDL1BRcjdSc1JEU1RMVWcvYlpBWnRGK1RWSWtIYzYvNFJJS3J1aStRPT0iXSwKCiAgICAicmVzZWxlY3QiOiBbInJlc2VsZWN0QDUuMi4wIiwgIiIsIHt9LCAic2hhNTEyLUFnWjNVT1ptM1luZGZySjRPWWpnclQ3Ym1DbS8xaXFranZFZkgvb1lqemg2UEQycXc0UXVUM2pqblhJcnBkdDRNVHBNWGNsTVQzbFhibVJZK1hSYWt3PT0iXSwKCiAgICAicm9sbGRvd24iOiBbInJvbGxkb3duQDEuMC4xIiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIkBveGMtcHJvamVjdC90eXBlcyI6ICI9MC4xMzAuMCIsICJAcm9sbGRvd24vcGx1Z2ludXRpbHMiOiAiXjEuMC4wIiB9LCAib3B0aW9uYWxEZXBlbmRlbmNpZXMiOiB7ICJAcm9sbGRvd24vYmluZGluZy1hbmRyb2lkLWFybTY0IjogIjEuMC4xIiwgIkByb2xsZG93bi9iaW5kaW5nLWRhcndpbi1hcm02NCI6ICIxLjAuMSIsICJAcm9sbGRvd24vYmluZGluZy1kYXJ3aW4teDY0IjogIjEuMC4xIiwgIkByb2xsZG93bi9iaW5kaW5nLWZyZWVic2QteDY0IjogIjEuMC4xIiwgIkByb2xsZG93bi9iaW5kaW5nLWxpbnV4LWFybS1nbnVlYWJpaGYiOiAiMS4wLjEiLCAiQHJvbGxkb3duL2JpbmRpbmctbGludXgtYXJtNjQtZ251IjogIjEuMC4xIiwgIkByb2xsZG93bi9iaW5kaW5nLWxpbnV4LWFybTY0LW11c2wiOiAiMS4wLjEiLCAiQHJvbGxkb3duL2JpbmRpbmctbGludXgtcHBjNjQtZ251IjogIjEuMC4xIiwgIkByb2xsZG93bi9iaW5kaW5nLWxpbnV4LXMzOTB4LWdudSI6ICIxLjAuMSIsICJAcm9sbGRvd24vYmluZGluZy1saW51eC14NjQtZ251IjogIjEuMC4xIiwgIkByb2xsZG93bi9iaW5kaW5nLWxpbnV4LXg2NC1tdXNsIjogIjEuMC4xIiwgIkByb2xsZG93bi9iaW5kaW5nLW9wZW5oYXJtb255LWFybTY0IjogIjEuMC4xIiwgIkByb2xsZG93bi9iaW5kaW5nLXdhc20zMi13YXNpIjogIjEuMC4xIiwgIkByb2xsZG93bi9iaW5kaW5nLXdpbjMyLWFybTY0LW1zdmMiOiAiMS4wLjEiLCAiQHJvbGxkb3duL2JpbmRpbmctd2luMzIteDY0LW1zdmMiOiAiMS4wLjEiIH0sICJiaW4iOiB7ICJyb2xsZG93biI6ICJiaW4vY2xpLm1qcyIgfSB9LCAic2hhNTEyLVgwS1FIbGpObkVrV05xcWl6OXpKckd1bmgxQjBIZ094TFh2bkZwQ09jYWR6Y3k1cW9oWjN0cU1FVWcwMHZuY29Sb3ZYdUszWnFDVDlLbm5Lem9JbkZRPT0iXSwKCiAgICAicnhqcyI6IFsicnhqc0A3LjguMiIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJ0c2xpYiI6ICJeMi4xLjAiIH0gfSwgInNoYTUxMi1kaEtmOTAzVS9QUVpZNmJvTk50QUdkV2JHODVXQWJqVC8xeFlvWklDN0ZBWTB5V2FwT0JRVnNWckRsNThXODYvL2UxVnBNTkJ0UlY0TWFYZmRNeVNGQT09Il0sCgogICAgInNjaGVkdWxlciI6IFsic2NoZWR1bGVyQDAuMjcuMCIsICIiLCB7fSwgInNoYTUxMi1lTnYrV3JWYkt1MWYzdmJZSlQveHRpRjVzeUE1SFBJTXRmOUlnWS9uS2cwc1dxekFVRXZxWS94bTdPY1pjL3FhZkx4L2lPOUZnT21lU0FwNHY1dGkvUT09Il0sCgogICAgInNlbXZlciI6IFsic2VtdmVyQDcuOC4wIiwgIiIsIHsgImJpbiI6IHsgInNlbXZlciI6ICJiaW4vc2VtdmVyLmpzIiB9IH0sICJzaGE1MTItQWNNN2RWLzV1bDRFZWtvUTI5QWdtNXZyaThKTnFSeWozOW8wcXBYNnZERjJHWnJ0dXRabDVSd2dEMVhuWmppVEFmbmNzSmhNSTQ4UVFIM3NOODdZTkE9PSJdLAoKICAgICJzaGFycCI6IFsic2hhcnBAMC4zNC41IiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIkBpbWcvY29sb3VyIjogIl4xLjAuMCIsICJkZXRlY3QtbGliYyI6ICJeMi4xLjIiLCAic2VtdmVyIjogIl43LjcuMyIgfSwgIm9wdGlvbmFsRGVwZW5kZW5jaWVzIjogeyAiQGltZy9zaGFycC1kYXJ3aW4tYXJtNjQiOiAiMC4zNC41IiwgIkBpbWcvc2hhcnAtZGFyd2luLXg2NCI6ICIwLjM0LjUiLCAiQGltZy9zaGFycC1saWJ2aXBzLWRhcndpbi1hcm02NCI6ICIxLjIuNCIsICJAaW1nL3NoYXJwLWxpYnZpcHMtZGFyd2luLXg2NCI6ICIxLjIuNCIsICJAaW1nL3NoYXJwLWxpYnZpcHMtbGludXgtYXJtIjogIjEuMi40IiwgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC1hcm02NCI6ICIxLjIuNCIsICJAaW1nL3NoYXJwLWxpYnZpcHMtbGludXgtcHBjNjQiOiAiMS4yLjQiLCAiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4LXJpc2N2NjQiOiAiMS4yLjQiLCAiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4LXMzOTB4IjogIjEuMi40IiwgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC14NjQiOiAiMS4yLjQiLCAiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4bXVzbC1hcm02NCI6ICIxLjIuNCIsICJAaW1nL3NoYXJwLWxpYnZpcHMtbGludXhtdXNsLXg2NCI6ICIxLjIuNCIsICJAaW1nL3NoYXJwLWxpbnV4LWFybSI6ICIwLjM0LjUiLCAiQGltZy9zaGFycC1saW51eC1hcm02NCI6ICIwLjM0LjUiLCAiQGltZy9zaGFycC1saW51eC1wcGM2NCI6ICIwLjM0LjUiLCAiQGltZy9zaGFycC1saW51eC1yaXNjdjY0IjogIjAuMzQuNSIsICJAaW1nL3NoYXJwLWxpbnV4LXMzOTB4IjogIjAuMzQuNSIsICJAaW1nL3NoYXJwLWxpbnV4LXg2NCI6ICIwLjM0LjUiLCAiQGltZy9zaGFycC1saW51eG11c2wtYXJtNjQiOiAiMC4zNC41IiwgIkBpbWcvc2hhcnAtbGludXhtdXNsLXg2NCI6ICIwLjM0LjUiLCAiQGltZy9zaGFycC13YXNtMzIiOiAiMC4zNC41IiwgIkBpbWcvc2hhcnAtd2luMzItYXJtNjQiOiAiMC4zNC41IiwgIkBpbWcvc2hhcnAtd2luMzItaWEzMiI6ICIwLjM0LjUiLCAiQGltZy9zaGFycC13aW4zMi14NjQiOiAiMC4zNC41IiB9IH0sICJzaGE1MTItT3U5STVGdDlXTmNDYlhyVTljTWdQQmNDSzhMaXdMcWNieXdXM3Q0b0RWMzduMXB6cHVOTHNZaUFWOGVPRG5qYnRRbFNEd1oyY1VFZVF6NEU1NEhsdGc9PSJdLAoKICAgICJzaGVsbC1xdW90ZSI6IFsic2hlbGwtcXVvdGVAMS44LjMiLCAiIiwge30sICJzaGE1MTItT2JtbklGNGhYTmcxQnFobkhtZ2JERVRGOGRMUENnZ1pXQmprUWZoWnBic3pabll1cjVEVWxqVGNDSGlpNUxDM0o1RTB5ZU8vMUxJTXlIK1V2SFFneXc9PSJdLAoKICAgICJzaWdpbmZvIjogWyJzaWdpbmZvQDIuMC4wIiwgIiIsIHt9LCAic2hhNTEyLXlieDBXTzEvOGJTQkxFV1hadkVkN2dNVzNTbjNKRmxXM1R2WDFuUkViRExSTlFOYWVOTjhXSzBtZUJ3UGRBYU9JN1R0UlJSSm4vRXMxemhyckNIdTdnPT0iXSwKCiAgICAic291cmNlLW1hcC1qcyI6IFsic291cmNlLW1hcC1qc0AxLjIuMSIsICIiLCB7fSwgInNoYTUxMi1VWFdNS2hMT3dWS2I3MjhJVXRRUFh4ZllVK3VzZHlidFVySy84dUdFOENRTXZyaE9wd3Z6REJ3ajBRaFNMN01RYzd2SXNJU0JHOFZROCtJRFF4cGZRQT09Il0sCgogICAgInN0YWNrYmFjayI6IFsic3RhY2tiYWNrQDAuMC4yIiwgIiIsIHt9LCAic2hhNTEyLTFYTUpFNWZRbzFqR0g2WS83ZWJud1BPQkVrSUVuVDRRRjMyZDVSMStWWGRYdmVNMElCTUp0OHpmYXhYMVAzUWhWd3JZZSs1NzYramtBTnRTUzJtQmJ3PT0iXSwKCiAgICAic3RkLWVudiI6IFsic3RkLWVudkA0LjEuMCIsICIiLCB7fSwgInNoYTUxMi1ScTd5YmNYMlJ1QzU1cjlvYVBWRVc3L3h1M3RqOHU0R2VCWUhCV0N5Y2hGdHpNSXI4NkE3ZTNQUEVCUFQzN3NIU3RLWDMrVGlYL0ZyL0FDbUpMVmxMUT09Il0sCgogICAgInN0cmluZy13aWR0aCI6IFsic3RyaW5nLXdpZHRoQDQuMi4zIiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgImVtb2ppLXJlZ2V4IjogIl44LjAuMCIsICJpcy1mdWxsd2lkdGgtY29kZS1wb2ludCI6ICJeMy4wLjAiLCAic3RyaXAtYW5zaSI6ICJeNi4wLjEiIH0gfSwgInNoYTUxMi13S3lRUlFwakowc0lwNjJFclNaZEdzak1KV3NhcDVvUk5paEhodTZHN0pWTy85aklCNlV5ZXZMK3RYdU9xcm5nOGovY3hLVFd5V1V3dlNUcmlpWnovZz09Il0sCgogICAgInN0cmlwLWFuc2kiOiBbInN0cmlwLWFuc2lANi4wLjEiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiYW5zaS1yZWdleCI6ICJeNS4wLjEiIH0gfSwgInNoYTUxMi1ZMzhWUFNIY3FrRnJDcEZuUTl2dVNYbXF1dXY1b1hPS3BHZVQ2YUdycjNvM0djOUFsVmE2SkJmVVNPQ25ieEdHWkYrLzBvb0k3S3JQdVVTenRVZFU1QT09Il0sCgogICAgInN0eWxlZC1qc3giOiBbInN0eWxlZC1qc3hANS4xLjYiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiY2xpZW50LW9ubHkiOiAiMC4wLjEiIH0sICJwZWVyRGVwZW5kZW5jaWVzIjogeyAicmVhY3QiOiAiPj0gMTYuOC4wIHx8IDE3LngueCB8fCBeMTguMC4wLTAgfHwgXjE5LjAuMC0wIiB9IH0sICJzaGE1MTItcVNWeURUZU1vdGR2UVlvSFdMTkd3UkZKSEMraStadmRCUllvc09GZ0MrV2cxdng0ZnJOMi9SRy9OQTdTWXFxdktOTGYzOVAyTFNSQTJwdTZuMFhZWkE9PSJdLAoKICAgICJzdXBwb3J0cy1jb2xvciI6IFsic3VwcG9ydHMtY29sb3JAOC4xLjEiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiaGFzLWZsYWciOiAiXjQuMC4wIiB9IH0sICJzaGE1MTItTXBVRU4yT29kdFV6eHZLUWw3MmNVRjdSUTVFaUhzR3ZTc1ZHMGlhOWM1UmJXR0wyQ0k0QzdFcFBTOFVUQklwbG5selppTnVWNTZ3K0Z1Tnh5M3R5MlE9PSJdLAoKICAgICJ0YWlsd2luZC1tZXJnZSI6IFsidGFpbHdpbmQtbWVyZ2VAMy42LjAiLCAiIiwge30sICJzaGE1MTItdXhMN3FBVlFyaXFSUVBBeUszcGo2NlZxc2tXcW9aMzdQVzk0andPVHdOZnEvejlveXUxVitlcXJacXRSMitmQ2lYZFlPWmUvTW9kdDhHdHZxTnp1K3c9PSJdLAoKICAgICJ0YWlsd2luZGNzcyI6IFsidGFpbHdpbmRjc3NANC4zLjAiLCAiIiwge30sICJzaGE1MTIteTZueE1HQjFuTVc5UjZrOTZlNWdkSUZ6Y2ZML2dUSlJOYXFHZXMxWXZrTG5QVlh6V2dicUZGMnlMQzBUOEc3NzRuMjRjeDNQZThYcktvbmlDT0FIK1E9PSJdLAoKICAgICJ0YXBhYmxlIjogWyJ0YXBhYmxlQDIuMy4zIiwgIiIsIHt9LCAic2hhNTEyLXV4Yy96cHFGZzZ4N0M4dk9FN2xoNkxiZGE4ZUVMOXptVm0vUExlVFBCUmhoMXhDZ2RXYVErSjFDVWllR3BJZm0ySGR0c1VwUnYrSHNoaWFzQk1jYzZBPT0iXSwKCiAgICAidGlueWJlbmNoIjogWyJ0aW55YmVuY2hAMi45LjAiLCAiIiwge30sICJzaGE1MTItMCtEVXZxV01WYWxMbWhhNmxyNGtEOGlBTUsxSHpWMC9hS25DdFdiOXY5NjQxVG5QL01GYjdQYzJieG94UWpUWEFFcnJ5WFZnVU9mdjJZcU5sbHFHZWc9PSJdLAoKICAgICJ0aW55ZXhlYyI6IFsidGlueWV4ZWNAMS4xLjIiLCAiIiwge30sICJzaGE1MTItZEFxU3FFL1JhYnBCS0k4K2gyNkdmTHE2VmIzSlZYczMwWFlRamRNamFqL2MydFM4SVlZTWJJelA1OTlLdFJqN2M1Ny93WUFwYjNRamdSZ1htckN1a0E9PSJdLAoKICAgICJ0aW55Z2xvYmJ5IjogWyJ0aW55Z2xvYmJ5QDAuMi4xNiIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJmZGlyIjogIl42LjUuMCIsICJwaWNvbWF0Y2giOiAiXjQuMC40IiB9IH0sICJzaGE1MTItcG45OVZob0FDWVI4bkZIaHhxaXgrdXZzYlhpbmVBYXNXbTVvalhvTjh4RXdLNUtkMy9UcmhObjF3Qnl1RDUyVXhXUkx5OHB1K2tSTW5pRWk2RXE5Wmc9PSJdLAoKICAgICJ0aW55cmFpbmJvdyI6IFsidGlueXJhaW5ib3dAMy4xLjAiLCAiIiwge30sICJzaGE1MTItQmYrSUxtQmdyZXRVcmRKeHpYTTBTZ1hMWjNYZmlhVXVPai9JS1FIdVRYaXArMDVYbit1eUVZZFZnMGtZRGlwVEJjTHJDVnlVekFQejdRbUFyYjBtbXc9PSJdLAoKICAgICJ0cmVlLWtpbGwiOiBbInRyZWUta2lsbEAxLjIuMiIsICIiLCB7ICJiaW4iOiB7ICJ0cmVlLWtpbGwiOiAiY2xpLmpzIiB9IH0sICJzaGE1MTItTDBPcnBpOHFHcFJHLy9OZCtIOTB2RkIrM2lIbnVlMXpTU0dtTk9PQ2gxR0xKN3JVS1Z3VjJIdmlqcGhHUVMyVW1oVVpld1M5Vmd2eFlJZGdyK2ZHMUE9PSJdLAoKICAgICJ0c2xpYiI6IFsidHNsaWJAMi44LjEiLCAiIiwge30sICJzaGE1MTItb0pGdTk0SFFiK0tWZHVTVVFMN3ducG1xbmZtTHNPQS9uQWg2YjZFSDB3Q0VvSzAvbVBlWFU2YzN3S0RWODNNa091SFBSSHRTWEtLVTk5SUJhelMvMnc9PSJdLAoKICAgICJ0eXBlc2NyaXB0IjogWyJ0eXBlc2NyaXB0QDUuOS4zIiwgIiIsIHsgImJpbiI6IHsgInRzYyI6ICJiaW4vdHNjIiwgInRzc2VydmVyIjogImJpbi90c3NlcnZlciIgfSB9LCAic2hhNTEyLWpsMXZaelBEaW5McjllVXQzSi90N1Y2RmdORXc5UWp2QlBkeXN6OUtmUURENDFmUXJDMlk0dktRZGlhVXBGVDRiWGxiMVJIaExwcDh3dG02TTVUZ1N3PT0iXSwKCiAgICAidW5kaWNpLXR5cGVzIjogWyJ1bmRpY2ktdHlwZXNANi4yMS4wIiwgIiIsIHt9LCAic2hhNTEyLWl3RFpxZzBRQUdyZzlSYXY1SDRuME02NGMzbWtSNTljSjZ3UXArN0M0bkkwZ3NtRXhhZWRhWUxOTzQ0ZVQ0QXRCQndqYlRpR1BNbHQyTWQwVDlIOUpRPT0iXSwKCiAgICAidXNlLXN5bmMtZXh0ZXJuYWwtc3RvcmUiOiBbInVzZS1zeW5jLWV4dGVybmFsLXN0b3JlQDEuNi4wIiwgIiIsIHsgInBlZXJEZXBlbmRlbmNpZXMiOiB7ICJyZWFjdCI6ICJeMTYuOC4wIHx8IF4xNy4wLjAgfHwgXjE4LjAuMCB8fCBeMTkuMC4wIiB9IH0sICJzaGE1MTItUHA2R1N3R1AvTnJQSXJ4VkZBSWtPUWV5dzhsRmVuT0hpalFXa1VUckR2ckY0QUxxeWxQMkMvS0NrZVM5ZHBVTTNLdllSUWhuYTV2dDdJTDk1K1pROXc9PSJdLAoKICAgICJ2aXRlIjogWyJ2aXRlQDguMC4xMyIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJsaWdodG5pbmdjc3MiOiAiXjEuMzIuMCIsICJwaWNvbWF0Y2giOiAiXjQuMC40IiwgInBvc3Rjc3MiOiAiXjguNS4xNCIsICJyb2xsZG93biI6ICIxLjAuMSIsICJ0aW55Z2xvYmJ5IjogIl4wLjIuMTYiIH0sICJvcHRpb25hbERlcGVuZGVuY2llcyI6IHsgImZzZXZlbnRzIjogIn4yLjMuMyIgfSwgInBlZXJEZXBlbmRlbmNpZXMiOiB7ICJAdHlwZXMvbm9kZSI6ICJeMjAuMTkuMCB8fCA+PTIyLjEyLjAiLCAiQHZpdGVqcy9kZXZ0b29scyI6ICJeMC4xLjE4IiwgImVzYnVpbGQiOiAiXjAuMjcuMCB8fCBeMC4yOC4wIiwgImppdGkiOiAiPj0xLjIxLjAiLCAibGVzcyI6ICJeNC4wLjAiLCAic2FzcyI6ICJeMS43MC4wIiwgInNhc3MtZW1iZWRkZWQiOiAiXjEuNzAuMCIsICJzdHlsdXMiOiAiPj0wLjU0LjgiLCAic3VnYXJzcyI6ICJeNS4wLjAiLCAidGVyc2VyIjogIl41LjE2LjAiLCAidHN4IjogIl40LjguMSIsICJ5YW1sIjogIl4yLjQuMiIgfSwgIm9wdGlvbmFsUGVlcnMiOiBbIkB0eXBlcy9ub2RlIiwgIkB2aXRlanMvZGV2dG9vbHMiLCAiZXNidWlsZCIsICJqaXRpIiwgImxlc3MiLCAic2FzcyIsICJzYXNzLWVtYmVkZGVkIiwgInN0eWx1cyIsICJzdWdhcnNzIiwgInRlcnNlciIsICJ0c3giLCAieWFtbCJdLCAiYmluIjogeyAidml0ZSI6ICJiaW4vdml0ZS5qcyIgfSB9LCAic2hhNTEyLU1GdGpCWWd6bVN4bWdBNFJBZmpJeVhXcEdlMW9BTG5qZ1VUenpWN1FMeC9US3hDemp0TUg2RmQ5L2VWSys1RmcxcU5vejVWQXdzbU1zL05vZnJtSnZ3PT0iXSwKCiAgICAidml0ZXN0IjogWyJ2aXRlc3RANC4xLjYiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiQHZpdGVzdC9leHBlY3QiOiAiNC4xLjYiLCAiQHZpdGVzdC9tb2NrZXIiOiAiNC4xLjYiLCAiQHZpdGVzdC9wcmV0dHktZm9ybWF0IjogIjQuMS42IiwgIkB2aXRlc3QvcnVubmVyIjogIjQuMS42IiwgIkB2aXRlc3Qvc25hcHNob3QiOiAiNC4xLjYiLCAiQHZpdGVzdC9zcHkiOiAiNC4xLjYiLCAiQHZpdGVzdC91dGlscyI6ICI0LjEuNiIsICJlcy1tb2R1bGUtbGV4ZXIiOiAiXjIuMC4wIiwgImV4cGVjdC10eXBlIjogIl4xLjMuMCIsICJtYWdpYy1zdHJpbmciOiAiXjAuMzAuMjEiLCAib2J1ZyI6ICJeMi4xLjEiLCAicGF0aGUiOiAiXjIuMC4zIiwgInBpY29tYXRjaCI6ICJeNC4wLjMiLCAic3RkLWVudiI6ICJeNC4wLjAtcmMuMSIsICJ0aW55YmVuY2giOiAiXjIuOS4wIiwgInRpbnlleGVjIjogIl4xLjAuMiIsICJ0aW55Z2xvYmJ5IjogIl4wLjIuMTUiLCAidGlueXJhaW5ib3ciOiAiXjMuMS4wIiwgInZpdGUiOiAiXjYuMC4wIHx8IF43LjAuMCB8fCBeOC4wLjAiLCAid2h5LWlzLW5vZGUtcnVubmluZyI6ICJeMi4zLjAiIH0sICJwZWVyRGVwZW5kZW5jaWVzIjogeyAiQGVkZ2UtcnVudGltZS92bSI6ICIqIiwgIkBvcGVudGVsZW1ldHJ5L2FwaSI6ICJeMS45LjAiLCAiQHR5cGVzL25vZGUiOiAiXjIwLjAuMCB8fCBeMjIuMC4wIHx8ID49MjQuMC4wIiwgIkB2aXRlc3QvYnJvd3Nlci1wbGF5d3JpZ2h0IjogIjQuMS42IiwgIkB2aXRlc3QvYnJvd3Nlci1wcmV2aWV3IjogIjQuMS42IiwgIkB2aXRlc3QvYnJvd3Nlci13ZWJkcml2ZXJpbyI6ICI0LjEuNiIsICJAdml0ZXN0L2NvdmVyYWdlLWlzdGFuYnVsIjogIjQuMS42IiwgIkB2aXRlc3QvY292ZXJhZ2UtdjgiOiAiNC4xLjYiLCAiQHZpdGVzdC91aSI6ICI0LjEuNiIsICJoYXBweS1kb20iOiAiKiIsICJqc2RvbSI6ICIqIiB9LCAib3B0aW9uYWxQZWVycyI6IFsiQGVkZ2UtcnVudGltZS92bSIsICJAb3BlbnRlbGVtZXRyeS9hcGkiLCAiQHR5cGVzL25vZGUiLCAiQHZpdGVzdC9icm93c2VyLXBsYXl3cmlnaHQiLCAiQHZpdGVzdC9icm93c2VyLXByZXZpZXciLCAiQHZpdGVzdC9icm93c2VyLXdlYmRyaXZlcmlvIiwgIkB2aXRlc3QvY292ZXJhZ2UtaXN0YW5idWwiLCAiQHZpdGVzdC9jb3ZlcmFnZS12OCIsICJAdml0ZXN0L3VpIiwgImhhcHB5LWRvbSIsICJqc2RvbSJdLCAiYmluIjogeyAidml0ZXN0IjogInZpdGVzdC5tanMiIH0gfSwgInNoYTUxMi02bHZqYlMzcDliNENyZENtZ3V6YmgyLzR1b1hoR0UycTcxUjRPWDVzcUY5UjFibzlYZDZmR3JNQWZ2cDV3bkN6bEJuRlZkQ09wNm9udVRRVmJvOGlVUT09Il0sCgogICAgIndoeS1pcy1ub2RlLXJ1bm5pbmciOiBbIndoeS1pcy1ub2RlLXJ1bm5pbmdAMi4zLjAiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAic2lnaW5mbyI6ICJeMi4wLjAiLCAic3RhY2tiYWNrIjogIjAuMC4yIiB9LCAiYmluIjogeyAid2h5LWlzLW5vZGUtcnVubmluZyI6ICJjbGkuanMiIH0gfSwgInNoYTUxMi1oVXJtYVdCZFZEY3h2WXFueWgwOXp1bkt6Uk9XamJaVGlOeThkQkVqa1M3ZWhFRFFpYlhKN1h2bG10Ynd1VGNsVWlJeU4rQ3lYUUQ0Vm1rbzhmTm04dz09Il0sCgogICAgIndyYXAtYW5zaSI6IFsid3JhcC1hbnNpQDcuMC4wIiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgImFuc2ktc3R5bGVzIjogIl40LjAuMCIsICJzdHJpbmctd2lkdGgiOiAiXjQuMS4wIiwgInN0cmlwLWFuc2kiOiAiXjYuMC4wIiB9IH0sICJzaGE1MTItWVZHSWoya2FtTFNUeHc2TnNaam9CeGZTd3NuMHljZGVzbWM0cCtRMjFjNXpQdVoxcGwrTmZ4VmR4UHRkSHZtTlZPUTZYU1lHNEFVdHl0L0ZpN0QxNlE9PSJdLAoKICAgICJ5MThuIjogWyJ5MThuQDUuMC44IiwgIiIsIHt9LCAic2hhNTEyLTBwZkZ6ZWdlRFdKSEpJQW1UTFJQMkR3SGpkRjVzN2pvOXR1enRkUXhBaElOQ2R2UyszbkdJTnFQZDAwQXBocUpSLzBMaEFOVVM2Lys3U0NiOThZT2ZBPT0iXSwKCiAgICAieWFyZ3MiOiBbInlhcmdzQDE3LjcuMiIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJjbGl1aSI6ICJeOC4wLjEiLCAiZXNjYWxhZGUiOiAiXjMuMS4xIiwgImdldC1jYWxsZXItZmlsZSI6ICJeMi4wLjUiLCAicmVxdWlyZS1kaXJlY3RvcnkiOiAiXjIuMS4xIiwgInN0cmluZy13aWR0aCI6ICJeNC4yLjMiLCAieTE4biI6ICJeNS4wLjUiLCAieWFyZ3MtcGFyc2VyIjogIl4yMS4xLjEiIH0gfSwgInNoYTUxMi03ZFN6elJRKytDS25OSS9rcktuWVJWN0pLS1BVWE1FaDYxc29hSEtnOW1yV0VoekZXaEZueFB4R2wrNjljRDFPdTYzQzEzTlVQQ25tSWNydnFDdU02dz09Il0sCgogICAgInlhcmdzLXBhcnNlciI6IFsieWFyZ3MtcGFyc2VyQDIxLjEuMSIsICIiLCB7fSwgInNoYTUxMi10VnBzSlc3RGRqZWNBaUZwYklCMWUzcXhJUXNFNk5vUGM1L2VUZHJiYklDNGgwTFZzV2hub2EzZyttMkhjbEJJdWpIenN4WjRWSlZBK0dVdWMyL0xCdz09Il0sCgogICAgInpvZCI6IFsiem9kQDQuNC4zIiwgIiIsIHt9LCAic2hhNTEyLXl0RU5GaklKRmwyVXdZZ2xkZTJqY2hXMkh3bTRHSkZMRGlTWFdkVHJKUUJJTjlGY3lwN240RGh4SkVpV05BSk1WMS9CcVdmVy9ra2c3MVVEY0hKeVRRPT0iXSwKCiAgICAiQHRhaWx3aW5kY3NzL294aWRlLXdhc20zMi13YXNpL0BlbW5hcGkvY29yZSI6IFsiQGVtbmFwaS9jb3JlQDEuMTAuMCIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAZW1uYXBpL3dhc2ktdGhyZWFkcyI6ICIxLjIuMSIsICJ0c2xpYiI6ICJeMi40LjAiIH0sICJidW5kbGVkIjogdHJ1ZSB9LCAic2hhNTEyLXlxNk9rSjRwODJDQWZQbDB1OW1RZWJRSEtQSmtZN1dySXVrMjA1Y1RZblllK2syWjhZQmgxMUZyYlJHL0g2aWhpcnFjYWNPZ2wyQklPOG95TVFMZVh3PT0iXSwKCiAgICAiQHRhaWx3aW5kY3NzL294aWRlLXdhc20zMi13YXNpL0BlbW5hcGkvcnVudGltZSI6IFsiQGVtbmFwaS9ydW50aW1lQDEuMTAuMCIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJ0c2xpYiI6ICJeMi40LjAiIH0sICJidW5kbGVkIjogdHJ1ZSB9LCAic2hhNTEyLWV3dllsazg2eFVvR0kwelFSTnEvbUMrMTZSMVFlRGxLUXkyMUtpM29TWVhOZ0xiNDVHVjFQNkEwTSsvczZueUN1TkRxZTVWcGFZODRCelhHd1Zid0ZBPT0iXSwKCiAgICAiQHRhaWx3aW5kY3NzL294aWRlLXdhc20zMi13YXNpL0BlbW5hcGkvd2FzaS10aHJlYWRzIjogWyJAZW1uYXBpL3dhc2ktdGhyZWFkc0AxLjIuMSIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJ0c2xpYiI6ICJeMi40LjAiIH0sICJidW5kbGVkIjogdHJ1ZSB9LCAic2hhNTEyLXVUSUk3T1lGKy9NZXMvTXJjSU9ZcDV5T3RTTUxCV1NJb0xQcGNnd2lwb2lLYmxpNmszMjJ0Y29Gc3hvSUl4UERxVzAxU1FHQWdrbzRFelppMkJOdjJ3PT0iXSwKCiAgICAiQHRhaWx3aW5kY3NzL294aWRlLXdhc20zMi13YXNpL0BuYXBpLXJzL3dhc20tcnVudGltZSI6IFsiQG5hcGktcnMvd2FzbS1ydW50aW1lQDEuMS40IiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIkB0eWJ5cy93YXNtLXV0aWwiOiAiXjAuMTAuMSIgfSwgInBlZXJEZXBlbmRlbmNpZXMiOiB7ICJAZW1uYXBpL2NvcmUiOiAiXjEuNy4xIiwgIkBlbW5hcGkvcnVudGltZSI6ICJeMS43LjEiIH0sICJidW5kbGVkIjogdHJ1ZSB9LCAic2hhNTEyLTNOUU5OZ0ExWVNsSmIva01IMWlsZEFTUDlIVzcvN2tZblJJMnN6V0phb2ZhUzFoV21iR0k0SCtkMysyMmFHelhYTjlJSituK0dpRlZjR2lwSlAxOG93PT0iXSwKCiAgICAiQHRhaWx3aW5kY3NzL294aWRlLXdhc20zMi13YXNpL0B0eWJ5cy93YXNtLXV0aWwiOiBbIkB0eWJ5cy93YXNtLXV0aWxAMC4xMC4yIiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgInRzbGliIjogIl4yLjQuMCIgfSwgImJ1bmRsZWQiOiB0cnVlIH0sICJzaGE1MTItUm9CdkoyWDB3dUtsV0ZJanJ3ZmZHdzFJcVpIS1FxekljaEthYWRaWmZuTnBzQVlwMm1NMGgzNkp0UENqTkRBSEdnWWV6LzE1dU1CcGZHd2NoaGlNZ2c9PSJdLAoKICAgICJAdGFpbHdpbmRjc3Mvb3hpZGUtd2FzbTMyLXdhc2kvdHNsaWIiOiBbInRzbGliQDIuOC4xIiwgIiIsIHsgImJ1bmRsZWQiOiB0cnVlIH0sICJzaGE1MTItb0pGdTk0SFFiK0tWZHVTVVFMN3ducG1xbmZtTHNPQS9uQWg2YjZFSDB3Q0VvSzAvbVBlWFU2YzN3S0RWODNNa091SFBSSHRTWEtLVTk5SUJhelMvMnc9PSJdLAoKICAgICJjaGFsay9zdXBwb3J0cy1jb2xvciI6IFsic3VwcG9ydHMtY29sb3JANy4yLjAiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiaGFzLWZsYWciOiAiXjQuMC4wIiB9IH0sICJzaGE1MTItcXBDQXZSbDlzdHVPSHZlS3NuN0huY0pSdnY1MDFxSWFjS3pRbE8vK0x3eGM5KzBxMndMeXY0RGZ2dDgwL0RQbjJwcU9Cc0pkRGlvZ1hHUjkrT3Z3Unc9PSJdLAoKICAgICJuZXh0L3Bvc3Rjc3MiOiBbInBvc3Rjc3NAOC40LjMxIiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIm5hbm9pZCI6ICJeMy4zLjYiLCAicGljb2NvbG9ycyI6ICJeMS4wLjAiLCAic291cmNlLW1hcC1qcyI6ICJeMS4wLjIiIH0gfSwgInNoYTUxMi1QUzA4SWJvaWE5bXRzLzJ5Z1YzZUxwWTVnaG5VY2ZMVi9FWFRPVzFFMnFZeEpLR0dCVXROak43NkZZSG5NczM2Um1BUm40MWJDMEFabW4rclIwT1ZwUT09Il0sCiAgfQp9Cg==" }, { "path": ".env.example", "kind": "text", "content": "# Your tenant public key. Get it from the desk's Developers tab.\n# `cpk_live_*` / `cpk_test_*` keys auto-route same-origin requests\n# to hosted Cimplify in both dev and prod. Anything else (`mock-dev`,\n# empty) falls back to the local `cimplify dev` mock.\nNEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev\n\n# Optional. Forces a fixed canonical URL for sitemap.xml, robots.txt,\n# OpenGraph, and llms.txt. Leave unset and the storefront derives the\n# canonical from the request `Host` header automatically.\n# NEXT_PUBLIC_SITE_URL=\n" }], "storefront-bakery": [{ "path": "vitest.config.ts", "kind": "text", "content": 'import { defineConfig } from "vitest/config";\n\nexport default defineConfig({\n test: {\n environment: "node",\n include: ["__tests__/**/*.test.ts"],\n globals: false,\n },\n});\n' }, { "path": "__tests__/cart-flow.test.ts", "kind": "text", "content": 'import { createCartFlowSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateCartFlowSuite({ seed: brand.mock.seed, businessId: brand.mock.businessId });\n' }, { "path": "__tests__/brand.test.ts", "kind": "text", "content": 'import { createBrandSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateBrandSuite({ brand });\n' }, { "path": "__tests__/contract.test.ts", "kind": "text", "content": 'import { createContractSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateContractSuite({ seed: brand.mock.seed });\n' }, { "path": "AGENTS.md", "kind": "text", "content": "# AGENTS.md \u2014 Bakery storefront template\n\nIf you are an AI agent (Claude, Cursor, Aider, devin, \u2026) working on this storefront, **start here.**\n\n## TL;DR for rebranding\n\nTo rebrand this storefront end-to-end as a different merchant:\n\n1. **Edit `lib/brand.ts`.** Every visible string reads from this file.\n2. **Edit `app/globals.css`** \u2014 the `@theme { \u2026 }` block holds the design tokens (palette, radius, font references).\n3. **Edit `.env.local`** \u2014 set `NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY` (optional: `NEXT_PUBLIC_SITE_URL`).\n\nThat is the entire rebrand. Do not modify any `.tsx` file in `app/` or `components/` for content changes \u2014 they are design-only and read from `brand`.\n\n## Rules\n\n- \u2705 All content edits \u2192 `lib/brand.ts`\n- \u2705 All design-token edits \u2192 `app/globals.css` `@theme` block\n- \u2705 Server Components stay cached via `'use cache'` + `cacheTag(tags.X())` + `cacheLife(\"hours\")`\n- \u2705 Client islands (header active link, cart pill, product modal) live behind `<Suspense>`\n- \u274C Don't hardcode strings. If a string isn't in `brand`, add a new field to the `Brand` interface.\n- \u274C Don't disable `cacheComponents: true` in `next.config.ts`.\n- \u274C Don't use `unstable_cache` \u2014 Next 16 uses the `'use cache'` directive.\n\n## Page surface\n\n```\napp/\n page.tsx Home \u2014 hero, collection strips, category grid\n shop/page.tsx Full catalogue (SDK <CataloguePage/>)\n search/page.tsx Search (SDK <SearchPage/>)\n collections/[slug]/page.tsx Collection landing\n categories/[slug]/page.tsx Category landing\n\n cart/page.tsx SDK <CartPage/>\n checkout/page.tsx SDK <CheckoutPage/>\n orders/[id]/page.tsx Post-checkout confirmation (also target of /track-order)\n\n account/page.tsx <CimplifyAccount /> (iframe \u2014 handles sign-in, dashboard)\n account/orders/page.tsx <CimplifyAccount section=\"orders\" />\n account/addresses/page.tsx <CimplifyAccount section=\"addresses\" />\n account/settings/page.tsx <CimplifyAccount section=\"settings\" />\n login/page.tsx redirect \u2192 /account (iframe owns sign-in UI)\n signup/page.tsx redirect \u2192 /account (iframe owns sign-up UI)\n\n contact/page.tsx Contact form (Server Action wiring TODO; currently fakes submit)\n track-order/page.tsx Guest order lookup \u2192 /orders/[id]\n\n about/page.tsx Brand story\n faq/page.tsx FAQ\n shipping/page.tsx Standalone shipping policy\n returns/page.tsx Standalone returns policy\n accessibility/page.tsx Accessibility statement (WCAG 2.1 AA)\n terms/page.tsx Terms of Service\n privacy/page.tsx Privacy Policy\n\n sitemap-page/page.tsx Human-readable HTML sitemap\n sitemap.ts XML sitemap (search engines)\n robots.ts robots.txt\n llms.txt/route.ts LLM-friendly Markdown index\n opensearch.xml/route.ts Browser address-bar search description\n error.tsx, not-found.tsx Global boundaries\n```\n\n## File \u2194 brand-field map\n\n| File | Reads from `brand` |\n|---|---|\n| `app/layout.tsx` | identity, contact, socials, schemaType (Organization JSON-LD), `metadataBase` |\n| `app/page.tsx` | `brand.hero` (Hero badge / title / subtitle) |\n| `app/about/page.tsx` | `brand.about` |\n| `app/faq/page.tsx` | `brand.faq` |\n| `app/terms/page.tsx` | `brand.terms` |\n| `app/privacy/page.tsx` | `brand.privacy` |\n| `app/shipping/page.tsx` | `brand.shipping` |\n| `app/returns/page.tsx` | `brand.returns` |\n| `app/accessibility/page.tsx` | `brand.accessibility` |\n| `app/contact/page.tsx` | `brand.contactPage`, `brand.contact` |\n| `app/track-order/page.tsx` | `brand.trackOrder` |\n| `app/account/*/page.tsx` | `brand.account` (eyebrows + titles only \u2014 Cimplify Link iframe owns the UI) |\n| `app/login/page.tsx`, `app/signup/page.tsx` | `brand.account` (metadata only \u2014 both redirect to `/account`) |\n| `app/llms.txt/route.ts` | `brand.llms`, contact, currency |\n| `app/opensearch.xml/route.ts` | `brand.shortName`, `brand.name` |\n| `app/sitemap.ts`, `robots.ts` | derives URLs from product/category data |\n| `app/not-found.tsx` | `brand.name` (page title) |\n| `components/header.tsx` | `brand.shortName`, `brand.microTag`, `brand.header.nav` |\n| `components/footer.tsx` | `brand.footer`, `brand.contact`, `brand.socials` |\n\n## Bakery-specific notes\n\n- Product detail uses a **URL-driven modal** (`?product=<slug>`), not a static `/products/[slug]` route. Fits impulse-purchase food UX.\n- Schema.org `@type` is `Bakery` \u2014 set in `brand.schemaType`.\n- Mock seed: `--seed default`. To preview a different industry, edit `dev:mock` in `package.json`.\n\n## Known TODOs (not blocking, but worth knowing)\n\n- `app/contact/contact-form.tsx` fakes a successful submit. In production, wire a Server Action that calls `client.support.sendMessage(...)` from `@cimplify/sdk/server`.\n- `components/newsletter.tsx` (homepage strip) similarly fakes submit. Wire to a real list provider.\n- Hero / lookbook imagery is Unsplash placeholder \u2014 replace with merchant assets.\n\n## Customizing SDK components\n\nFor anything beyond `lib/brand.ts` + `app/globals.css`, lean on the SDK's prebuilt components rather than reinvent. **Especially for product customization** (variants, add-ons, bundles, composites, services with scheduling) \u2014 the SDK already gets price math, axis matching, and cart payload contracts right. Default to ejecting and restyling:\n\n```bash\ncimplify add cart-drawer\ncimplify add variant-selector\ncimplify add product-page\n```\n\nThen edit the local copy. **Don't change the cart payload shape** unless you're also touching the SDK mock + backend lens. Full ejection rules and the customizer contract are in the SDK-level [`AGENTS.md`](../../AGENTS.md) \u2192 \"Don't reinvent product customization\".\n\n## Quick start\n\n```bash\nbun install\nbun dev # boots mock + Next dev server\n```\n\nOpen <http://localhost:3000>.\n" }, { "path": "postcss.config.mjs", "kind": "text", "content": 'const config = {\n plugins: {\n "@tailwindcss/postcss": {},\n },\n};\n\nexport default config;\n' }, { "path": "components/cart-pill.tsx", "kind": "text", "content": '"use client";\n\nimport { useCartDrawer } from "@cimplify/sdk/react";\nimport { useCartCount } from "@/lib/cart";\n\n/**\n * Cart pill \u2014 dynamic island. Reads the live cart count via the SDK and\n * opens the side cart drawer on click (instead of navigating to /cart).\n * Wrap in `<Suspense fallback={<CartPillSkeleton/>}>` so the cached\n * header chrome streams without blocking on the cart fetch.\n */\nexport function CartPill() {\n const { count } = useCartCount();\n const { open } = useCartDrawer();\n return (\n <button\n type="button"\n onClick={open}\n aria-label={`Open cart, ${count} ${count === 1 ? "item" : "items"}`}\n className="inline-flex items-center gap-1.5 px-4 py-2.5 sm:py-2 rounded-full bg-foreground text-background text-xs font-semibold tracking-wide transition-transform hover:scale-105 cursor-pointer"\n >\n Cart \xB7 {count}\n </button>\n );\n}\n\nexport function CartPillSkeleton() {\n return (\n <span\n aria-hidden\n className="inline-flex items-center gap-1.5 px-4 py-2.5 sm:py-2 rounded-full bg-foreground/80 text-background text-xs font-semibold tracking-wide"\n >\n Cart \xB7 \u2026\n </span>\n );\n}\n' }, { "path": "components/collection-strip.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport type { Collection, Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "./store-product-card";\n\ninterface CollectionStripProps {\n collection: Collection;\n products: Product[];\n collectionHref?: string;\n}\n\n/**\n * Horizontal strip of products under a collection title. Cards come from the\n * SDK so every variant (Food, Bundle, Composite, Service, \u2026) renders\n * correctly; clicks open the shared URL-driven product modal.\n */\nexport function CollectionStrip({ collection, products, collectionHref }: CollectionStripProps) {\n if (products.length === 0) return null;\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 pt-12">\n <header className="grid grid-cols-[1fr_auto] items-end gap-4 mb-5">\n <h2 className="font-serif text-[28px] font-semibold m-0">{collection.name}</h2>\n {collectionHref && (\n <Link\n href={collectionHref}\n className="text-[13px] font-semibold text-primary hover:underline"\n >\n See all \u2192\n </Link>\n )}\n {collection.description && (\n <p className="col-span-full m-0 mt-1 text-sm text-muted-foreground">\n {collection.description}\n </p>\n )}\n </header>\n <div className="grid grid-flow-col auto-cols-[minmax(220px,1fr)] gap-4 overflow-x-auto snap-x snap-mandatory pb-2">\n {products.slice(0, 8).map((p) => (\n <div key={p.id} className="snap-start">\n <StoreProductCard product={p} />\n </div>\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "components/hero.tsx", "kind": "text", "content": 'interface HeroProps {\n badge?: string;\n title: string;\n subtitle?: string;\n}\n\nexport function Hero({ badge, title, subtitle }: HeroProps) {\n return (\n <section className="px-6 sm:px-8 py-16 text-center bg-gradient-to-b from-accent to-background">\n {badge && (\n <span className="inline-block mb-4 px-3.5 py-1.5 rounded-full bg-card border border-accent text-accent-foreground text-[11px] font-semibold uppercase tracking-[0.12em]">\n {badge}\n </span>\n )}\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold m-0 -tracking-[0.02em]">\n {title}\n </h1>\n {subtitle && (\n <p className="mx-auto mt-2 max-w-xl text-base text-muted-foreground">\n {subtitle}\n </p>\n )}\n </section>\n );\n}\n' }, { "path": "components/account-iframe.tsx", "kind": "text", "content": '"use client";\n\nimport { CimplifyAccount } from "@cimplify/sdk/react";\n\n/**\n * Cimplify Account portal \u2014 iframe-mounted UI hosted by Cimplify Link.\n * Handles sign-in, sign-up, OTP, addresses, payment methods, sessions,\n * and order history. The iframe owns auth state; we just choose which\n * `section` to land on.\n */\nexport function AccountIframe({ section }: { section?: string }) {\n return <CimplifyAccount section={section} />;\n}\n' }, { "path": "components/policy-page.tsx", "kind": "text", "content": 'import type { BrandPolicySection } from "@/lib/brand";\n\ninterface PolicyShape {\n eyebrow: string;\n title: string;\n lastUpdated?: string;\n sections: BrandPolicySection[];\n}\n\n/**\n * Shared layout for shipping / returns / accessibility / terms / privacy.\n * Reads a `{ eyebrow, title, lastUpdated, sections[] }` block from brand.\n */\nexport function PolicyPage({ policy }: { policy: PolicyShape }) {\n return (\n <article className="max-w-3xl mx-auto px-6 sm:px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {policy.eyebrow}\n </p>\n <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-2 -tracking-[0.02em]">\n {policy.title}\n </h1>\n {policy.lastUpdated && (\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {policy.lastUpdated}\n </p>\n )}\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {policy.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="text-2xl font-semibold mt-0">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "components/header.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport { Suspense } from "react";\nimport { NavLink } from "./nav-link";\nimport { CartPill, CartPillSkeleton } from "./cart-pill";\nimport { MobileNav } from "./mobile-nav";\nimport { brand } from "@/lib/brand";\n\n/**\n * Server-rendered header chrome. Brand mark + nav layout streams from the\n * cache; the active-link styling and live cart count are dynamic islands\n * mounted in their own Suspense boundaries so the chrome never blocks.\n */\nexport function Header() {\n return (\n <header className="sticky top-0 z-30 flex items-center justify-between px-6 sm:px-8 py-4 border-b border-border bg-background/90 backdrop-blur-md">\n <Link href="/" className="flex items-baseline gap-2">\n <span className="font-serif text-[22px] font-semibold -tracking-[0.02em]">\n {brand.shortName}\n </span>\n <span className="hidden sm:inline text-[11px] font-medium uppercase tracking-[0.12em] text-muted-foreground">\n {brand.microTag}\n </span>\n </Link>\n <div className="flex items-center gap-3 sm:gap-6">\n <nav className="hidden sm:flex items-center gap-6">\n {brand.header.nav.map((link) => (\n <Suspense key={link.href} fallback={<NavLinkFallback>{link.label}</NavLinkFallback>}>\n <NavLink href={link.href}>{link.label}</NavLink>\n </Suspense>\n ))}\n </nav>\n <Suspense fallback={<CartPillSkeleton />}>\n <CartPill />\n </Suspense>\n <div className="sm:hidden">\n <MobileNav />\n </div>\n </div>\n </header>\n );\n}\n\nfunction NavLinkFallback({ children }: { children: React.ReactNode }) {\n return (\n <span className="text-[13px] font-medium tracking-wide text-muted-foreground">\n {children}\n </span>\n );\n}\n' }, { "path": "components/product-modal.tsx", "kind": "text", "content": '"use client";\n\nimport { useEffect } from "react";\nimport Image from "next/image";\nimport { useRouter, useSearchParams, usePathname } from "next/navigation";\nimport { ProductSheet, useProduct, useCart } from "@cimplify/sdk/react";\n\n/**\n * URL-driven product modal. Reads `?product=<slug>` and renders the SDK\'s\n * `<ProductSheet/>` \u2014 vertical layout with image-on-top, then header, then\n * the variant/add-on/composite/bundle customizer. Closing the modal clears\n * the search param. Deep-linkable and survives reloads.\n */\nexport function ProductModal() {\n const router = useRouter();\n const pathname = usePathname();\n const searchParams = useSearchParams();\n const slug = searchParams?.get("product") ?? null;\n\n const { product } = useProduct(slug ?? "", { enabled: Boolean(slug) });\n const { addItem } = useCart();\n\n useEffect(() => {\n if (!slug) return;\n const original = document.body.style.overflow;\n document.body.style.overflow = "hidden";\n return () => {\n document.body.style.overflow = original;\n };\n }, [slug]);\n\n useEffect(() => {\n if (!slug) return;\n const onKey = (e: KeyboardEvent) => {\n if (e.key === "Escape") close();\n };\n window.addEventListener("keydown", onKey);\n return () => window.removeEventListener("keydown", onKey);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [slug]);\n\n if (!slug) return null;\n\n function close() {\n const next = new URLSearchParams(searchParams?.toString() ?? "");\n next.delete("product");\n const qs = next.toString();\n router.replace(qs ? `${pathname}?${qs}` : pathname, { scroll: false });\n }\n\n return (\n <div\n role="dialog"\n aria-modal="true"\n onClick={close}\n className="fixed inset-0 z-[100] flex items-end sm:items-center justify-center sm:p-6 bg-foreground/50 backdrop-blur-sm"\n >\n <div\n onClick={(e) => e.stopPropagation()}\n className="relative w-full sm:max-w-lg max-h-[92vh] overflow-auto bg-card sm:rounded-3xl rounded-t-3xl shadow-2xl"\n >\n <button\n onClick={close}\n aria-label="Close product details"\n className="absolute top-4 right-4 z-10 w-9 h-9 rounded-full bg-card border border-border text-sm cursor-pointer transition-colors hover:bg-muted"\n >\n \u2715\n </button>\n {product ? (\n <ProductSheet\n product={product}\n onClose={close}\n onAddToCart={async (p, qty, options) => {\n await addItem(p, qty, options);\n close();\n }}\n renderImage={({ src, alt, className }) => (\n <Image\n src={src}\n alt={alt}\n width={1200}\n height={900}\n className={className}\n style={{ width: "100%", height: "auto", objectFit: "cover" }}\n priority\n />\n )}\n classNames={{\n root: "p-6 sm:p-8 gap-4",\n image: "rounded-2xl overflow-hidden -mx-6 sm:-mx-8 -mt-6 sm:-mt-8 mb-2",\n header: "flex items-baseline justify-between gap-4",\n name: "font-serif text-2xl font-semibold m-0",\n price: "text-lg font-semibold text-primary",\n description: "text-sm text-muted-foreground leading-relaxed",\n customizer: "pt-2",\n }}\n />\n ) : (\n <div className="p-8 text-center text-muted-foreground">Loading\u2026</div>\n )}\n </div>\n </div>\n );\n}\n' }, { "path": "components/mobile-nav.tsx", "kind": "text", "content": '"use client";\n\nimport { useEffect, useState } from "react";\nimport Link from "next/link";\nimport { brand } from "@/lib/brand";\n\n/**\n * Hamburger button + slide-in drawer for narrow viewports. Header hides\n * its inline nav links below `sm` and renders this in their place; the\n * cart pill stays in the header chrome.\n */\nexport function MobileNav() {\n const [open, setOpen] = useState(false);\n\n useEffect(() => {\n if (!open) return;\n const onKey = (event: KeyboardEvent) => {\n if (event.key === "Escape") setOpen(false);\n };\n document.addEventListener("keydown", onKey);\n const previousOverflow = document.body.style.overflow;\n document.body.style.overflow = "hidden";\n return () => {\n document.removeEventListener("keydown", onKey);\n document.body.style.overflow = previousOverflow;\n };\n }, [open]);\n\n return (\n <>\n <button\n type="button"\n onClick={() => setOpen(true)}\n aria-label="Open menu"\n aria-expanded={open}\n aria-controls="mobile-nav-drawer"\n className="grid place-items-center w-11 h-11 -mr-2 rounded-md text-foreground hover:bg-muted transition-colors"\n >\n <svg\n width="20"\n height="20"\n viewBox="0 0 24 24"\n fill="none"\n stroke="currentColor"\n strokeWidth="2"\n strokeLinecap="round"\n strokeLinejoin="round"\n aria-hidden="true"\n >\n <line x1="3" y1="6" x2="21" y2="6" />\n <line x1="3" y1="12" x2="21" y2="12" />\n <line x1="3" y1="18" x2="21" y2="18" />\n </svg>\n </button>\n\n {open ? (\n <div className="fixed inset-0 z-50 sm:hidden">\n <button\n type="button"\n onClick={() => setOpen(false)}\n aria-label="Close menu"\n className="absolute inset-0 bg-background/80 backdrop-blur-sm"\n />\n <nav\n id="mobile-nav-drawer"\n aria-label="Mobile navigation"\n className="absolute inset-y-0 right-0 w-[85%] max-w-sm flex flex-col bg-background border-l border-border shadow-2xl"\n >\n <div className="flex items-center justify-between px-6 py-4 border-b border-border">\n <span className="text-xs font-semibold uppercase tracking-[0.16em] text-muted-foreground">\n Menu\n </span>\n <button\n type="button"\n onClick={() => setOpen(false)}\n aria-label="Close menu"\n className="grid place-items-center w-11 h-11 -mr-2 rounded-md text-foreground hover:bg-muted transition-colors"\n >\n <svg\n width="20"\n height="20"\n viewBox="0 0 24 24"\n fill="none"\n stroke="currentColor"\n strokeWidth="2"\n strokeLinecap="round"\n strokeLinejoin="round"\n aria-hidden="true"\n >\n <line x1="18" y1="6" x2="6" y2="18" />\n <line x1="6" y1="6" x2="18" y2="18" />\n </svg>\n </button>\n </div>\n <ul className="flex flex-col gap-1 px-3 py-4">\n {brand.header.nav.map((link) => (\n <li key={link.href}>\n <Link\n href={link.href}\n onClick={() => setOpen(false)}\n className="block px-3 py-3 rounded-md text-base font-medium text-foreground hover:bg-muted transition-colors"\n >\n {link.label}\n </Link>\n </li>\n ))}\n </ul>\n </nav>\n </div>\n ) : null}\n </>\n );\n}\n' }, { "path": "components/providers.tsx", "kind": "text", "content": '"use client";\n\nimport { useMemo, type ReactNode } from "react";\nimport { createCimplifyClient } from "@cimplify/sdk";\nimport { CimplifyProvider, CartDrawerProvider } from "@cimplify/sdk/react";\n\n// Same-origin client \u2014 every request goes through the Next.js rewrite in\n// next.config.ts, so no CORS preflight ever hits the browser.\nexport function Providers({ children }: { children: ReactNode }) {\n const client = useMemo(() => {\n const baseUrl =\n typeof window !== "undefined" ? window.location.origin : "http://127.0.0.1:8787";\n const publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY ?? "mock-dev";\n return createCimplifyClient({\n baseUrl,\n publicKey,\n suppressPublicKeyWarning: true,\n });\n }, []);\n\n return (\n <CimplifyProvider client={client}>\n <CartDrawerProvider>{children}</CartDrawerProvider>\n </CimplifyProvider>\n );\n}\n' }, { "path": "components/footer.tsx", "kind": "text", "content": 'import Link from "next/link";\nimport { brand } from "@/lib/brand";\n\nconst ICONS: Record<string, React.ReactNode> = {\n instagram: (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" aria-hidden className="w-5 h-5">\n <rect x="3" y="3" width="18" height="18" rx="5" />\n <circle cx="12" cy="12" r="4" />\n <circle cx="17.5" cy="6.5" r="0.75" fill="currentColor" />\n </svg>\n ),\n x: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M18 2h3l-7.5 8.6L22 22h-6.6l-5-6.5L4 22H1l8-9.2L1.4 2H8l4.6 6 5.4-6z" />\n </svg>\n ),\n tiktok: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M16 3v3.5a4.5 4.5 0 0 0 4.5 4.5V14a7.5 7.5 0 0 1-4.5-1.5V16a5 5 0 1 1-5-5v3a2 2 0 1 0 2 2V3z" />\n </svg>\n ),\n facebook: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M14 9V7a1 1 0 0 1 1-1h2V3h-3a4 4 0 0 0-4 4v2H8v3h2v9h3v-9h2.5l.5-3H13z" />\n </svg>\n ),\n youtube: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M22.5 6.5a2.6 2.6 0 0 0-1.8-1.8C19 4.2 12 4.2 12 4.2s-7 0-8.7.5A2.6 2.6 0 0 0 1.5 6.5C1 8.2 1 12 1 12s0 3.8.5 5.5a2.6 2.6 0 0 0 1.8 1.8C5 19.8 12 19.8 12 19.8s7 0 8.7-.5a2.6 2.6 0 0 0 1.8-1.8c.5-1.7.5-5.5.5-5.5s0-3.8-.5-5.5zM10 15.5v-7l6 3.5-6 3.5z" />\n </svg>\n ),\n linkedin: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M4 4h4v16H4zM6 2.5a2 2 0 1 0 0 4 2 2 0 0 0 0-4zM10 8h4v2.5h.1c.6-1.1 2-2.5 4-2.5 4 0 4.9 2.6 4.9 6V20h-4v-5c0-1.5-.5-3-2.3-3-1.7 0-2.5 1.3-2.5 3v5h-4z" />\n </svg>\n ),\n whatsapp: (\n <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">\n <path d="M12 2a10 10 0 0 0-8.6 15l-1.4 5 5.2-1.4A10 10 0 1 0 12 2zm5 14.2c-.2.6-1.2 1.2-1.7 1.2-.5.1-1.1.1-1.7-.1-.4-.1-.9-.3-1.5-.6a8.4 8.4 0 0 1-3.7-3.4c-.7-1-1-1.8-1-2.5 0-.7.4-1.1.6-1.3.2-.2.4-.2.5-.2h.4c.1 0 .3 0 .4.3l.6 1.4c.1.2 0 .3 0 .4l-.3.4-.3.3c-.1.1-.2.2-.1.4.2.4.7 1.1 1.4 1.8.9.8 1.7 1.1 1.9 1.2.2.1.3.1.5-.1l.6-.7c.2-.2.3-.2.5-.1l1.4.7c.2.1.3.2.4.3.1.2.1.6 0 1z" />\n </svg>\n ),\n};\n\nconst FALLBACK_ICON = (\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" aria-hidden className="w-5 h-5">\n <circle cx="12" cy="12" r="9" />\n </svg>\n);\n\nexport async function Footer() {\n "use cache";\n const year = new Date().getFullYear();\n return (\n <footer className="mt-12 px-6 sm:px-8 py-10 text-xs text-muted-foreground border-t border-border bg-card">\n <div className="max-w-7xl mx-auto">\n <div className="grid gap-10 md:grid-cols-[1.4fr_repeat(4,1fr)]">\n <div>\n <p className="font-serif text-2xl text-foreground m-0 mb-2">{brand.name}</p>\n <p className="leading-relaxed mb-4 max-w-sm">{brand.footer.blurb}</p>\n <address className="not-italic space-y-1">\n <p className="m-0">{brand.contact.address}</p>\n <p className="m-0">\n <a\n href={`tel:${brand.contact.phoneTel}`}\n className="hover:text-foreground transition-colors"\n >\n {brand.contact.phone}\n </a>\n </p>\n <p className="m-0">\n <a\n href={`mailto:${brand.contact.email}`}\n className="hover:text-foreground transition-colors"\n >\n {brand.contact.email}\n </a>\n </p>\n <p className="m-0">{brand.contact.hours}</p>\n </address>\n <div className="flex items-center gap-3 mt-5">\n {brand.socials.map((s) => (\n <a\n key={s.label}\n href={s.href}\n aria-label={s.label}\n target="_blank"\n rel="noopener noreferrer"\n className="inline-flex items-center justify-center w-9 h-9 rounded-full border border-border text-muted-foreground hover:text-foreground hover:border-foreground transition-colors"\n >\n {(s.icon && ICONS[s.icon]) ?? FALLBACK_ICON}\n </a>\n ))}\n </div>\n </div>\n {brand.footer.sitemap.map((section) => (\n <nav key={section.title} aria-labelledby={`footer-${section.title}`}>\n <p\n id={`footer-${section.title}`}\n className="font-semibold text-foreground mb-3 text-[13px] uppercase tracking-[0.08em]"\n >\n {section.title}\n </p>\n <ul className="space-y-2 m-0 p-0 list-none">\n {section.links.map((link) => (\n <li key={link.label}>\n <Link\n href={link.href}\n className="hover:text-foreground transition-colors"\n >\n {link.label}\n </Link>\n </li>\n ))}\n </ul>\n </nav>\n ))}\n </div>\n <div className="mt-12 pt-6 border-t border-border flex flex-col sm:flex-row items-center justify-between gap-3">\n <p className="m-0">\xA9 {year} {brand.name}. All rights reserved.</p>\n {brand.footer.poweredBy && (\n <p className="m-0 inline-flex items-center gap-1.5">\n <span className="text-muted-foreground/80">Powered by</span>\n <a\n href={brand.footer.poweredBy.href}\n target="_blank"\n rel="noopener noreferrer"\n aria-label={brand.footer.poweredBy.label}\n className="inline-flex items-center gap-1 font-serif text-foreground hover:text-primary transition-colors"\n >\n <span className="font-semibold tracking-tight">{brand.footer.poweredBy.label}</span>\n <svg\n viewBox="0 0 12 12"\n aria-hidden\n className="w-3 h-3 opacity-70"\n fill="none"\n stroke="currentColor"\n strokeWidth="1.5"\n >\n <path d="M3 9L9 3M9 3H4M9 3v5" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n </a>\n </p>\n )}\n </div>\n </div>\n </footer>\n );\n}\n' }, { "path": "components/store-product-card.tsx", "kind": "text", "content": '"use client";\n\nimport Link from "next/link";\nimport {\n CardVariant,\n FoodProductCard,\n RetailProductCard,\n WholesaleProductCard,\n DigitalProductCard,\n BundleProductCard,\n CompositeProductCard,\n StandardServiceCard,\n CompactServiceCard,\n ScheduleServiceCard,\n RentalServiceCard,\n AccommodationCard,\n LeaseServiceCard,\n SubscriptionCard,\n} from "@cimplify/sdk/react";\nimport type { CardLayoutProps } from "@cimplify/sdk/react";\nimport type { Product } from "@cimplify/sdk";\nimport { PRODUCT_TYPE, RENDER_HINT, DURATION_UNIT } from "@cimplify/sdk";\n\nconst RENTAL_UNITS = new Set<string>([DURATION_UNIT.Days, DURATION_UNIT.Hours]);\nconst LEASE_UNITS = new Set<string>([DURATION_UNIT.Weeks, DURATION_UNIT.Months, DURATION_UNIT.Years]);\n\nconst VARIANT_CARDS: Record<CardVariant, React.ComponentType<CardLayoutProps>> = {\n [CardVariant.Food]: FoodProductCard,\n [CardVariant.Retail]: RetailProductCard,\n [CardVariant.Wholesale]: WholesaleProductCard,\n [CardVariant.Digital]: DigitalProductCard,\n [CardVariant.Bundle]: BundleProductCard,\n [CardVariant.Composite]: CompositeProductCard,\n [CardVariant.Standard]: StandardServiceCard,\n [CardVariant.Compact]: CompactServiceCard,\n [CardVariant.Schedule]: ScheduleServiceCard,\n [CardVariant.Rental]: RentalServiceCard,\n [CardVariant.Accommodation]: AccommodationCard,\n [CardVariant.Lease]: LeaseServiceCard,\n [CardVariant.Subscription]: SubscriptionCard,\n};\n\nfunction resolveVariant(p: Product): CardVariant {\n if (p.type === PRODUCT_TYPE.Bundle) return CardVariant.Bundle;\n if (p.type === PRODUCT_TYPE.Composite) return CardVariant.Composite;\n if (p.quantity_pricing && p.quantity_pricing.length > 1) return CardVariant.Wholesale;\n if (p.type === PRODUCT_TYPE.Digital) return CardVariant.Digital;\n if (p.type === PRODUCT_TYPE.Service) {\n if (p.duration_unit && RENTAL_UNITS.has(p.duration_unit)) return CardVariant.Rental;\n if (p.duration_unit === DURATION_UNIT.Nights) return CardVariant.Accommodation;\n if (p.duration_unit && LEASE_UNITS.has(p.duration_unit)) return CardVariant.Lease;\n if (p.billing_plans && p.billing_plans.length > 0 && !p.duration_minutes) return CardVariant.Subscription;\n return CardVariant.Standard;\n }\n if (p.render_hint === RENDER_HINT.Food) return CardVariant.Food;\n return CardVariant.Retail;\n}\n\ninterface Props {\n product: Product;\n /** Override the auto-detected card variant. */\n variant?: CardVariant;\n}\n\n/**\n * Variant-aware product card that opens the URL-driven product modal on\n * click. Uses `next/link` with `?product=<slug>` so the link is statically\n * pre-renderable \u2014 no `useSearchParams` dependency means cards don\'t\n * become dynamic islands. The modal (which already reads `useSearchParams`\n * inside a Suspense boundary in the root layout) handles the rest.\n */\nexport function StoreProductCard({ product, variant }: Props) {\n const slug = product.slug || product.id;\n const Card = VARIANT_CARDS[variant ?? resolveVariant(product)];\n const href = `?product=${encodeURIComponent(slug)}`;\n\n return (\n <Card\n product={product}\n renderLink={({ className, children }) => (\n <Link href={href} scroll={false} prefetch={false} className={className}>\n {children}\n </Link>\n )}\n />\n );\n}\n' }, { "path": "components/json-ld.tsx", "kind": "text", "content": 'import { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\n\n/**\n * Streams the organization JSON-LD script tag in async so it doesn\'t block\n * the rest of the layout shell. `getSiteUrl` reads request headers, and\n * any await on dynamic data inside `RootLayout` makes Next 16 mark the\n * whole route as blocking.\n */\nexport async function OrganizationJsonLd(): Promise<React.ReactElement> {\n const siteUrl = await getSiteUrl();\n const ld = {\n "@context": "https://schema.org",\n "@type": brand.schemaType,\n name: brand.name,\n url: siteUrl,\n description: brand.description,\n email: brand.contact.email,\n telephone: brand.contact.phoneTel,\n address: {\n "@type": "PostalAddress",\n streetAddress: brand.contact.streetAddress,\n addressLocality: brand.contact.city,\n addressCountry: brand.contact.countryCode,\n },\n sameAs: brand.socials.map((s) => s.href),\n };\n return (\n <script\n type="application/ld+json"\n dangerouslySetInnerHTML={{ __html: JSON.stringify(ld) }}\n />\n );\n}\n' }, { "path": "components/nav-link.tsx", "kind": "text", "content": '"use client";\n\nimport Link from "next/link";\nimport { usePathname } from "next/navigation";\n\nexport function NavLink({ href, children }: { href: string; children: React.ReactNode }) {\n const pathname = usePathname();\n const active = pathname === href;\n return (\n <Link\n href={href}\n className={[\n "text-[13px] font-medium tracking-wide transition-colors",\n active ? "text-primary" : "text-muted-foreground hover:text-foreground",\n ].join(" ")}\n >\n {children}\n </Link>\n );\n}\n' }, { "path": "components/category-grid.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CategoryGrid as SdkCategoryGrid } from "@cimplify/sdk/react";\nimport type { Category } from "@cimplify/sdk";\n\n/**\n * Homepage category tiles \u2014 defers to the SDK\'s <CategoryGrid/> for layout\n * and accessibility, and routes selections to /categories/:slug.\n */\nexport function CategoryGrid({ categories }: { categories?: Category[] }) {\n const router = useRouter();\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 pt-14">\n <h2 className="font-serif text-[28px] font-semibold m-0 mb-5">Browse the menu</h2>\n <SdkCategoryGrid\n categories={categories}\n onSelect={(c) => router.push(`/categories/${c.slug}`)}\n classNames={{\n item: "flex flex-col gap-1 p-6 bg-card border border-border rounded-2xl cursor-pointer text-left transition-all hover:border-primary hover:-translate-y-0.5",\n name: "font-serif text-lg font-semibold text-foreground",\n description: "text-xs text-muted-foreground",\n count: "text-xs text-muted-foreground mt-1",\n }}\n />\n </section>\n );\n}\n' }, { "path": "components/cart-drawer.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CartDrawer as SdkCartDrawer } from "@cimplify/sdk/react";\n\n/**\n * Side-drawer cart. Auto-opens when an item is added (via\n * `<CartDrawerProvider>` in `providers.tsx`). The header\'s cart pill\n * also calls `useCartDrawer().open()` to reveal it on click.\n */\nexport function CartDrawer() {\n const router = useRouter();\n return <SdkCartDrawer onCheckout={() => router.push("/checkout")} />;\n}\n' }, { "path": "next.config.ts", "kind": "text", "content": 'import type { NextConfig } from "next";\n\n// Same-origin proxy target for the storefront API. The public key\n// decides: a real `cpk_live_\u2026` / `cpk_test_\u2026` only resolves against\n// hosted Cimplify, so browser cart writes go to the same place the\n// server catalogue reads come from. Anything else (`mock-dev`, empty)\n// falls back to the local `cimplify dev` mock in dev.\nconst publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY?.trim() ?? "";\nconst keyTargetsHostedCimplify =\n publicKey.startsWith("cpk_live_") || publicKey.startsWith("cpk_test_");\nconst STOREFRONT_URL =\n process.env.NODE_ENV === "production" || keyTargetsHostedCimplify\n ? "https://storefronts.cimplify.io"\n : "http://127.0.0.1:8787";\n\nif (STOREFRONT_URL === "http://127.0.0.1:8787") {\n console.warn(\n "[cimplify] next.config resolved the local storefront API URL; production deploys must run with NODE_ENV=production.",\n );\n}\n\nconst nextConfig: NextConfig = {\n // Enable Next 16\'s `cacheComponents` mode so we can use `\'use cache\'` +\n // `cacheTag` / `cacheLife` for SSR caching. Cached chrome streams first,\n // dynamic Suspense boundaries fill in when ready.\n cacheComponents: true,\n async redirects() {\n // Config-level so cacheComponents needn\'t prerender a redirect()-only page.\n return [\n { source: "/login", destination: "/account", permanent: false },\n { source: "/signup", destination: "/account", permanent: false },\n ];\n },\n async rewrites() {\n return [\n { source: "/api/v1/:path*", destination: `${STOREFRONT_URL}/api/v1/:path*` },\n { source: "/img/:path*", destination: `${STOREFRONT_URL}/img/:path*` },\n { source: "/elements/:path*", destination: `${STOREFRONT_URL}/elements/:path*` },\n { source: "/_mock/:path*", destination: `${STOREFRONT_URL}/_mock/:path*` },\n ];\n },\n images: {\n loader: "custom",\n loaderFile: "./lib/cimplify-loader.ts",\n remotePatterns: [\n { protocol: "http", hostname: "127.0.0.1", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "8787", pathname: "/img/**" },\n { protocol: "http", hostname: "localhost", port: "3000", pathname: "/img/**" },\n { protocol: "https", hostname: "loremflickr.com", pathname: "/**" },\n { protocol: "https", hostname: "images.unsplash.com", pathname: "/**" },\n { protocol: "https", hostname: "static-tmp.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "storefrontassetscdn.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "cdn.cimplify.io", pathname: "/**" },\n { protocol: "https", hostname: "res.cloudinary.com", pathname: "/cimplify/**" },\n ],\n },\n};\n\nexport default nextConfig;\n' }, { "path": ".claude/skills/cimplify-storefront/SKILL.md", "kind": "text", "content": "---\nname: cimplify-storefront\ndescription: Build, customize, rebrand, or deploy a Cimplify-scaffolded storefront. Triggers when the user asks to create a storefront, rebrand a Cimplify template, change the palette, add a page, deploy to Cimplify, or works in a project containing `lib/brand.ts` and `next.config.ts` with `cacheComponents`.\n---\n\n# Cimplify Storefront skill\n\nYou're working on a project scaffolded from `cimplify init`. The architecture is opinionated and the rebrand surface is intentionally small. Read `AGENTS.md` at the project root for the file \u2194 brand-field map for *this template's* industry; this skill gives you the playbook that's the same across all six.\n\n## The contract \u2014 never break\n\n1. **`lib/brand.ts` is the only place for content edits.** Every visible string reads from this file. If a string isn't in `brand`, *add a field* to the `Brand` interface \u2014 don't hardcode it in a page or component.\n2. **`app/globals.css` `@theme { \u2026 }`** holds palette + radius + font references. To re-skin the entire site, edit only this block.\n3. **Server Components are cached** via `'use cache'` + `cacheTag(tags.X())` + `cacheLife(\"hours\")`. Don't downgrade them to `\"use client\"` to avoid an issue \u2014 fix the issue.\n4. **Client islands** (anything reading `useSearchParams`, `usePathname`, `useRouter`, `useState`) live behind `<Suspense>`.\n5. **`cacheComponents: true`** in `next.config.ts` is non-negotiable. Don't turn it off.\n6. **`bun run test:run` (vitest)** is the canonical test runner. `bun test` will show false failures because Bun's `vi` shim is incomplete.\n7. **Cart, checkout, orders stay client.** They're session-bound. Don't try to SSR them.\n\n## The brand-field schema (in `lib/brand.ts`)\n\n```\nidentity: name, shortName, microTag, description, schemaType, currency, locale\ncontact: address, streetAddress, city, countryCode, phone, phoneTel, email, privacyEmail, hours\nsocials: [{ label, href, icon }] // icon \u2208 instagram|x|tiktok|facebook|youtube|linkedin|whatsapp\nheader: { nav: [{ label, href }] }\nhero: { badge, title, subtitle, primaryCtaLabel, secondaryCtaLabel?, secondaryCtaHref? }\noptional: trustItems[]?, brandStrip?, promo?, tradeIn? // render conditionally\nnewsletter: { eyebrow, title, body, placeholder, submitLabel, successLabel }\nabout: { eyebrow, title, paragraphs[], sections[{ heading, body }] }\nfaq: { eyebrow, title, sections[{ title, items[{ q, a }] }], contactPrompt, contactEmail }\nterms,\nprivacy,\nshipping,\nreturns,\naccessibility: { eyebrow, title, lastUpdated, sections[{ heading, body | { intro, bullets[] } }] }\naccount: eyebrows + titles for /account, /login, /signup\ncontactPage: { eyebrow, title, body, reasons[], directLines[{ label, value, href }] }\ntrackOrder: { eyebrow, title, body }\nfooter: { blurb, sitemap[{ title, links[] }], poweredBy? }\nllms: { summary } // opens /llms.txt\nmock: { seed, businessId }\n```\n\n## Playbook \u2014 common tasks\n\n### Rebrand a storefront for a new merchant\n\n1. Edit `lib/brand.ts`. Replace every field with the merchant's content. Use the brief / context the user gave you.\n2. Edit `app/globals.css` `@theme { \u2026 }`. Change `--color-primary`, `--color-background`, `--color-foreground`, optionally `--radius`. Use OKLCH; if the brand only gave hex, convert.\n3. (Optional) Swap fonts in `app/layout.tsx` \u2014 `next/font/google` import + the variable wired into the `<html>` className.\n4. Set `.env.local`: `NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY` (optional: `NEXT_PUBLIC_SITE_URL`).\n\nDon't touch any other file. If the rebrand needs content not in the schema, add the field to `Brand` first, populate it, then read it from the page.\n\n### Add a new section / component\n\n1. Build it as a Server Component in `components/` (or a client island in `*-client.tsx` if interactive).\n2. Read merchant copy from `brand`. Add new fields to the `Brand` interface if needed.\n3. Wrap interactive bits in `<Suspense fallback={\u2026}>` so the cached chrome streams.\n4. Compose into the page.\n\n### Wire a Server Action that mutates data\n\n```ts\n\"use server\";\nimport { getServerClient, revalidateProducts } from \"@cimplify/sdk/server\";\n\nexport async function createProduct(input: ProductInput) {\n await getServerClient().catalogue.createProduct(input);\n revalidateProducts();\n}\n```\n\nAfter every mutation, call the matching `revalidate*` helper from `@cimplify/sdk/server`: `revalidateProducts`, `revalidateProduct(id)`, `revalidateCategories`, `revalidateCategory(id)`, `revalidateCollections`, `revalidateCollection(id)`, `revalidateBusiness`.\n\n### Add a Server Component data fetch\n\n```ts\nimport { cacheTag, cacheLife } from \"next/cache\";\nimport { getServerClient, tags } from \"@cimplify/sdk/server\";\n\nasync function getX() {\n \"use cache\";\n cacheTag(tags.products());\n cacheLife(\"hours\");\n const r = await getServerClient().catalogue.getProducts({ limit: 24 });\n if (!r.ok) throw new Error(r.error.message);\n return r.value.items;\n}\n```\n\n### Eject an SDK component for deeper customization\n\nTry `classNames`, `renderImage`, `renderLink`, slot props first. If those run out:\n\n```bash\ncimplify list\ncimplify add product-card --dir src/components/cimplify\n```\n\nOnce ejected, the file is yours. Hooks/types still come from `@cimplify/sdk`.\n\n### Deploy\n\n```bash\ncimplify login\ncimplify projects create my-store\ncimplify link <project-id>\ncimplify env push\ncimplify deploy --prod\ncimplify logs --follow\ncimplify domains add my-store.com\n```\n\n## Pitfalls \u2014 explicit \u274C list\n\n- \u274C Hardcoding any visible string in a page or component. Always `brand.X`.\n- \u274C Disabling `cacheComponents: true` to silence a warning. Fix the warning by wrapping the dynamic call in `<Suspense>`.\n- \u274C Using `unstable_cache` \u2014 Next 16's canonical primitive is `'use cache'` + `cacheTag` + `cacheLife`.\n- \u274C Bypassing `getServerClient()` and calling `createCimplifyClient` directly in a Server Component \u2014 loses per-request memoization.\n- \u274C Mutating data without calling the matching `revalidate*` helper.\n- \u274C Adding an `app/error.tsx` handler that calls `reset()` without logging \u2014 silently swallowed errors hide bugs.\n- \u274C Running `bun test` and reporting failures. Use `bun run test:run` (vitest).\n- \u274C Editing the per-template `AGENTS.md` to remove notes about TODOs (contact form, newsletter fake submits) \u2014 they're real.\n\n## Where things live\n\n| Need | Look here |\n|---|---|\n| Which page reads which `brand.X` field | `AGENTS.md` at project root |\n| Architectural rules | `AGENTS.md` at project root + this skill |\n| Running locally | `bun dev` (boots mock + Next together) |\n| Switching mock seed | edit `dev:mock` in `package.json` |\n| Full SDK reference | `docs/sdk/storefronts.md` in the Cimplify repo |\n\n## What to do when the user asks something out-of-scope\n\nIf the user asks for something the template doesn't support out of the box:\n\n1. **Check first** \u2014 is there an SDK component or hook that does it? `@cimplify/sdk/react` has 50+ components, 30+ hooks. Search `node_modules/@cimplify/sdk/dist/react.d.mts` if needed.\n2. **Compose from existing parts** before authoring new ones.\n3. **If genuinely missing** \u2014 build the new component, hoist its strings into `brand.ts`, document in `AGENTS.md`.\n\nDon't introduce competing patterns (a new caching strategy, a new auth flow, a new theming system). The template's opinions are intentional.\n" }, { "path": ".cursor/rules/cimplify-storefront.mdc", "kind": "binary", "content": "LS0tCmRlc2NyaXB0aW9uOiBDaW1wbGlmeSBzdG9yZWZyb250IOKAlCBhcmNoaXRlY3R1cmFsIHJ1bGVzIGFuZCByZWJyYW5kIGNvbnRyYWN0IGZvciBhIHByb2plY3Qgc2NhZmZvbGRlZCBmcm9tIGBjaW1wbGlmeSBpbml0YC4gVHJpZ2dlcnMgd2hlbiBmaWxlcyBsaWtlIGxpYi9icmFuZC50cywgYXBwL2dsb2JhbHMuY3NzLCBjb21wb25lbnRzL2hlYWRlci50c3gsIG9yIC5jaW1wbGlmeS9wcm9qZWN0Lmpzb24gYXJlIGluIHRoZSB3b3Jrc3BhY2UuCmdsb2JzOiBbImxpYi9icmFuZC50cyIsICJhcHAvKioiLCAiY29tcG9uZW50cy8qKiIsICIuZW52LmxvY2FsIiwgIm5leHQuY29uZmlnLnRzIl0KYWx3YXlzQXBwbHk6IHRydWUKLS0tCgojIENpbXBsaWZ5IHN0b3JlZnJvbnQKClRoaXMgcHJvamVjdCB3YXMgc2NhZmZvbGRlZCBmcm9tIGEgQ2ltcGxpZnkgdGVtcGxhdGUuIFJlYWQgYEFHRU5UUy5tZGAgYXQgdGhlIHByb2plY3Qgcm9vdCBmb3IgdGhlIGZpbGUg4oaUIGBicmFuZC5YYCBmaWVsZCBtYXAgYW5kIGAuY2xhdWRlL3NraWxscy9jaW1wbGlmeS1zdG9yZWZyb250L1NLSUxMLm1kYCBmb3IgdGhlIGZ1bGwgcGxheWJvb2suCgojIyBUaGUgY29udHJhY3QKCjEuICoqYGxpYi9icmFuZC50c2AqKiDigJQgZXZlcnkgdmlzaWJsZSBzdHJpbmcuIElmIHNvbWV0aGluZyBpc24ndCB0aGVyZSwgYWRkIGEgZmllbGQgdG8gdGhlIGBCcmFuZGAgaW50ZXJmYWNlOyBkb24ndCBoYXJkY29kZS4KMi4gKipgYXBwL2dsb2JhbHMuY3NzYCBgQHRoZW1lYCBibG9jayoqIOKAlCBwYWxldHRlLCByYWRpdXMsIGZvbnQgcmVmZXJlbmNlcy4KMy4gKipTZXJ2ZXIgQ29tcG9uZW50cyBhcmUgY2FjaGVkKiogdmlhIGAndXNlIGNhY2hlJ2AgKyBgY2FjaGVUYWcodGFncy5YKCkpYCArIGBjYWNoZUxpZmUoImhvdXJzIilgLiBEb24ndCBkb3duZ3JhZGUgdG8gY2xpZW50IHRvIHNpbGVuY2UgYSB3YXJuaW5nLgo0LiAqKkNsaWVudCBpc2xhbmRzKiogYmVoaW5kIGA8U3VzcGVuc2U+YC4KNS4gKipgY2FjaGVDb21wb25lbnRzOiB0cnVlYCoqIGluIGBuZXh0LmNvbmZpZy50c2AgaXMgbm9uLW5lZ290aWFibGUuCjYuIFVzZSAqKmBidW4gcnVuIHRlc3Q6cnVuYCoqICh2aXRlc3QpLCBub3QgYGJ1biB0ZXN0YC4KCiMjIERvbid0CgotIEhhcmRjb2RlIHZpc2libGUgc3RyaW5ncyBpbiBwYWdlcy9jb21wb25lbnRzLgotIFVzZSBgdW5zdGFibGVfY2FjaGVgIChOZXh0IDE2IHVzZXMgYCd1c2UgY2FjaGUnYCkuCi0gQnlwYXNzIGBnZXRTZXJ2ZXJDbGllbnQoKWAgKGxvc2VzIHBlci1yZXF1ZXN0IG1lbW9pemF0aW9uKS4KLSBNdXRhdGUgd2l0aG91dCBjYWxsaW5nIHRoZSBtYXRjaGluZyBgcmV2YWxpZGF0ZSpgIGhlbHBlciBmcm9tIGBAY2ltcGxpZnkvc2RrL3NlcnZlcmAuCg==" }, { "path": ".gitignore", "kind": "text", "content": "node_modules\n.next\nout\n.env\n.env.local\n.env*.local\n.DS_Store\n*.tsbuildinfo\nnext-env.d.ts\n" }, { "path": "app/about/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `About \u2014 ${brand.name}`,\n description: brand.description,\n};\n\nexport default function AboutPage() {\n const a = brand.about;\n const titleParts = a.title.split("\\n");\n return (\n <article className="max-w-3xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {a.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-6 -tracking-[0.02em]">\n {titleParts.map((line, i) => (\n <span key={i}>\n {line}\n {i < titleParts.length - 1 && <br />}\n </span>\n ))}\n </h1>\n <div className="prose prose-lg max-w-none space-y-5 text-foreground/90 leading-relaxed">\n {a.paragraphs.map((p, i) => (\n <p key={i}>{p}</p>\n ))}\n {a.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="font-serif text-3xl font-semibold mt-10 mb-3">{s.heading}</h2>\n <p>{s.body}</p>\n </div>\n ))}\n </div>\n </article>\n );\n}\n' }, { "path": "app/account/orders/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Orders \u2014 ${brand.name}`,\n};\n\nexport default function OrdersPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Your orders\n </h1>\n <AccountIframe section="orders" />\n </article>\n );\n}\n' }, { "path": "app/account/settings/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Settings \u2014 ${brand.name}`,\n};\n\nexport default function SettingsPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Settings\n </h1>\n <AccountIframe section="settings" />\n </article>\n );\n}\n' }, { "path": "app/account/addresses/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Addresses \u2014 ${brand.name}`,\n};\n\nexport default function AddressesPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Your addresses\n </h1>\n <AccountIframe section="addresses" />\n </article>\n );\n}\n' }, { "path": "app/account/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Account \u2014 ${brand.name}`,\n description: brand.account.signupSubtitle,\n};\n\nexport default function AccountPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {brand.account.accountEyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n {brand.account.accountTitle}\n </h1>\n <AccountIframe />\n </article>\n );\n}\n' }, { "path": "app/.well-known/ucp/route.ts", "kind": "text", "content": 'import { NextResponse } from "next/server";\n\n/**\n * UCP (Universal Commerce Protocol) manifest discovery endpoint.\n *\n * Agents \u2014 Claude, ChatGPT, Gemini, MCP clients \u2014 probe\n * `https://<your-domain>/.well-known/ucp` to learn what commerce\n * capabilities your storefront supports. We resolve the business\'s\n * UCP handle from the public key (via the storefront API), then forward\n * to Cimplify for the canonical manifest. Edge-cached for an hour\n * because capabilities change rarely.\n */\nconst UCP_API_BASE = "https://api.cimplify.io";\nconst STOREFRONT_API_BASE =\n process.env.NODE_ENV === "production"\n ? "https://storefronts.cimplify.io"\n : "http://127.0.0.1:8787";\n\nexport async function GET() {\n const publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY;\n if (!publicKey) {\n return NextResponse.json(\n {\n error: "NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY not set",\n remediation:\n "Set NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY in .env.local (and your deployment env).",\n },\n { status: 500 },\n );\n }\n\n try {\n // Step 1 \u2014 resolve the business handle from the public key.\n const businessResp = await fetch(`${STOREFRONT_API_BASE}/api/v1/business`, {\n headers: { "X-API-Key": publicKey, "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n });\n\n if (!businessResp.ok) {\n return NextResponse.json(\n { error: `Failed to resolve business: ${businessResp.status}` },\n { status: businessResp.status },\n );\n }\n\n const businessJson = await businessResp.json();\n const handle: string | undefined = businessJson?.data?.handle;\n if (!handle) {\n return NextResponse.json(\n { error: "Business has no handle configured" },\n { status: 500 },\n );\n }\n\n // Step 2 \u2014 fetch the canonical UCP manifest.\n const manifestResp = await fetch(\n `${UCP_API_BASE}/ucp/v1/${handle}/manifest`,\n {\n headers: { "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n },\n );\n\n if (!manifestResp.ok) {\n return NextResponse.json(\n { error: `Upstream UCP manifest fetch failed: ${manifestResp.status}` },\n { status: manifestResp.status },\n );\n }\n\n const manifest = await manifestResp.json();\n return NextResponse.json(manifest, {\n headers: {\n "Content-Type": "application/json",\n "Cache-Control": "public, max-age=3600, s-maxage=3600",\n },\n });\n } catch (error) {\n return NextResponse.json(\n {\n error: "Failed to fetch UCP manifest",\n detail: error instanceof Error ? error.message : String(error),\n },\n { status: 500 },\n );\n }\n}\n' }, { "path": "app/search/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { SearchClient } from "./search-client";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Search \u2014 ${brand.name}`,\n description: `Search ${brand.name} \u2014 products, collections, categories.`,\n};\n\nexport default function SearchPage() {\n return (\n <article className="max-w-7xl mx-auto px-6 sm:px-8 py-10">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Search\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Find anything.\n </h1>\n <Suspense fallback={<SearchSkeleton />}>\n <SearchClient />\n </Suspense>\n </article>\n );\n}\n\nfunction SearchSkeleton() {\n return (\n <div>\n <div className="h-12 w-full max-w-xl bg-muted rounded animate-pulse mb-8" />\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {Array.from({ length: 8 }).map((_, i) => (\n <div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </div>\n );\n}\n' }, { "path": "app/search/search-client.tsx", "kind": "text", "content": '"use client";\n\nimport { SearchPage } from "@cimplify/sdk/react";\n\nexport function SearchClient() {\n return <SearchPage />;\n}\n' }, { "path": "app/faq/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `FAQ \u2014 ${brand.name}`,\n description: "Delivery, pickup, custom cakes, allergens, payment \u2014 answers to the questions we hear most often.",\n};\n\nexport default function FaqPage() {\n const f = brand.faq;\n return (\n <article className="max-w-3xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {f.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-10 -tracking-[0.02em]">\n {f.title}\n </h1>\n <div className="space-y-12">\n {f.sections.map((section) => (\n <section key={section.title}>\n <h2 className="font-serif text-2xl font-semibold mb-5">{section.title}</h2>\n <dl className="space-y-6">\n {section.items.map((item) => (\n <div key={item.q}>\n <dt className="font-medium text-foreground mb-1.5">{item.q}</dt>\n <dd className="text-muted-foreground leading-relaxed">{item.a}</dd>\n </div>\n ))}\n </dl>\n </section>\n ))}\n </div>\n <p className="mt-12 pt-8 border-t border-border text-sm text-muted-foreground">\n {f.contactPrompt}{" "}\n <a\n href={`mailto:${f.contactEmail}`}\n className="text-primary font-semibold hover:underline"\n >\n {f.contactEmail}\n </a>{" "}\n and a real human will reply within 24 hours.\n </p>\n </article>\n );\n}\n' }, { "path": "app/robots.ts", "kind": "text", "content": 'import type { MetadataRoute } from "next";\nimport { getSiteUrl } from "@/lib/site-url";\n\nexport default async function robots(): Promise<MetadataRoute.Robots> {\n const siteUrl = await getSiteUrl();\n return {\n rules: [\n {\n userAgent: "*",\n allow: "/",\n disallow: ["/cart", "/checkout", "/orders/", "/api/"],\n },\n ],\n sitemap: `${siteUrl}/sitemap.xml`,\n host: siteUrl,\n };\n}\n' }, { "path": "app/track-order/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { TrackOrderForm } from "./track-order-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Track an order \u2014 ${brand.name}`,\n description: brand.trackOrder.body,\n};\n\nexport default function TrackOrderPage() {\n const t = brand.trackOrder;\n return (\n <article className="max-w-2xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {t.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,5vw,3rem)] font-semibold mb-4 -tracking-[0.02em]">\n {t.title}\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">{t.body}</p>\n <TrackOrderForm />\n </article>\n );\n}\n' }, { "path": "app/track-order/track-order-form.tsx", "kind": "text", "content": '"use client";\n\nimport { useState } from "react";\nimport { useRouter } from "next/navigation";\n\n/**\n * Guest order tracker. Routes /orders/<id> with the entered id; the\n * post-checkout confirmation page already lives at /orders/[id] so this\n * just sends the visitor there. Replace the redirect with a real lookup\n * (e.g. Cimplify orders API + email-match guard) for production.\n */\nexport function TrackOrderForm() {\n const router = useRouter();\n const [orderId, setOrderId] = useState("");\n const [email, setEmail] = useState("");\n\n return (\n <form\n onSubmit={(e) => {\n e.preventDefault();\n if (!orderId.trim()) return;\n router.push(`/orders/${encodeURIComponent(orderId.trim())}`);\n }}\n className="space-y-4 rounded-2xl border border-border bg-card p-6"\n >\n <div>\n <label\n htmlFor="orderId"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order number\n </label>\n <input\n id="orderId"\n name="orderId"\n required\n value={orderId}\n onChange={(e) => setOrderId(e.target.value)}\n placeholder="ord_abc123\u2026"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <div>\n <label\n htmlFor="email"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order email\n </label>\n <input\n id="email"\n name="email"\n type="email"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n placeholder="you@email.com"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <button\n type="submit"\n className="w-full inline-flex items-center justify-center px-5 py-3 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors"\n >\n Track order\n </button>\n </form>\n );\n}\n' }, { "path": "app/cart/page.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CartPage as SdkCartPage } from "@cimplify/sdk/react";\n\nexport default function CartPage() {\n const router = useRouter();\n return <SdkCartPage onCheckout={() => router.push("/checkout")} />;\n}\n' }, { "path": "app/llms.txt/route.ts", "kind": "text", "content": 'import { cacheTag, cacheLife } from "next/cache";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\n\nasync function buildLlmsTxt(SITE_URL: string): Promise<string> {\n "use cache";\n cacheTag(tags.products(), tags.categories(), tags.collections());\n cacheLife("hours");\n\n const client = getServerClient();\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n\n const products: Product[] = productsRes.ok ? productsRes.value.items : [];\n const categories = categoriesRes.ok ? categoriesRes.value : [];\n const collections = collectionsRes.ok ? collectionsRes.value : [];\n\n const lines: string[] = [];\n lines.push(`# ${brand.name}`);\n lines.push("");\n lines.push(`> ${brand.llms.summary}`);\n lines.push("");\n lines.push("## Browse");\n lines.push(`- [Home](${SITE_URL}/)`);\n lines.push(`- [Shop](${SITE_URL}/shop): Full menu with filter and sort.`);\n\n if (categories.length > 0) {\n lines.push("");\n lines.push("## Categories");\n for (const c of categories) {\n lines.push(\n `- [${c.name}](${SITE_URL}/categories/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (collections.length > 0) {\n lines.push("");\n lines.push("## Collections");\n for (const c of collections) {\n lines.push(\n `- [${c.name}](${SITE_URL}/collections/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (products.length > 0) {\n lines.push("");\n lines.push("## Products");\n for (const p of products) {\n const slug = p.slug ?? p.id;\n const price = `${brand.currency} ${p.default_price}`;\n const desc = p.description ? ` \u2014 ${p.description.replace(/\\s+/g, " ").slice(0, 200)}` : "";\n lines.push(`- [${p.name}](${SITE_URL}/shop?product=${slug}) (${price})${desc}`);\n }\n }\n\n lines.push("");\n lines.push("## Information");\n lines.push(`- [About](${SITE_URL}/about)`);\n lines.push(`- [FAQ](${SITE_URL}/faq)`);\n lines.push(`- [Terms of Service](${SITE_URL}/terms)`);\n lines.push(`- [Privacy Policy](${SITE_URL}/privacy)`);\n lines.push(`- [Sitemap (XML)](${SITE_URL}/sitemap.xml)`);\n lines.push("");\n lines.push("## Contact");\n lines.push(`- Email: ${brand.contact.email}`);\n lines.push(`- Phone: ${brand.contact.phone}`);\n lines.push(`- Address: ${brand.contact.address}`);\n\n return lines.join("\\n") + "\\n";\n}\n\n/**\n * `/llms.txt` \u2014 machine-readable site index for LLMs (per llmstxt.org).\n * Lets coding agents and chat assistants find products, categories, and\n * support pages without scraping HTML. Plain Markdown so it streams cheaply\n * into context windows.\n */\nexport async function GET(): Promise<Response> {\n const body = await buildLlmsTxt(await getSiteUrl());\n return new Response(body, {\n headers: {\n "Content-Type": "text/plain; charset=utf-8",\n "Cache-Control": "public, max-age=0, s-maxage=3600, stale-while-revalidate=86400",\n },\n });\n}\n' }, { "path": "app/sitemap.ts", "kind": "text", "content": 'import type { MetadataRoute } from "next";\nimport { getServerClient, type Product } from "@cimplify/sdk/server";\nimport { getSiteUrl } from "@/lib/site-url";\n\nconst STATIC_ROUTES: { path: string; priority: number; changeFrequency: "daily" | "weekly" | "monthly" }[] = [\n { path: "/", priority: 1.0, changeFrequency: "daily" },\n { path: "/shop", priority: 0.9, changeFrequency: "daily" },\n { path: "/about", priority: 0.5, changeFrequency: "monthly" },\n { path: "/faq", priority: 0.4, changeFrequency: "monthly" },\n { path: "/terms", priority: 0.2, changeFrequency: "monthly" },\n { path: "/privacy", priority: 0.2, changeFrequency: "monthly" },\n];\n\nexport default async function sitemap(): Promise<MetadataRoute.Sitemap> {\n const now = new Date();\n const siteUrl = await getSiteUrl();\n const client = getServerClient();\n\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n\n const products = productsRes.ok ? productsRes.value.items : [];\n const categories = categoriesRes.ok ? categoriesRes.value : [];\n const collections = collectionsRes.ok ? collectionsRes.value : [];\n\n const staticEntries: MetadataRoute.Sitemap = STATIC_ROUTES.map((r) => ({\n url: `${siteUrl}${r.path}`,\n lastModified: now,\n changeFrequency: r.changeFrequency,\n priority: r.priority,\n }));\n\n // Bakery uses a URL-driven product modal (`?product=<slug>` on the home or\n // shop pages). Emit those as deep-linkable URLs so search engines and LLMs\n // can index each product canonically.\n const productEntries: MetadataRoute.Sitemap = products.map((p: Product) => ({\n url: `${siteUrl}/shop?product=${encodeURIComponent(p.slug ?? p.id)}`,\n lastModified: p.updated_at ? new Date(p.updated_at) : now,\n changeFrequency: "weekly",\n priority: 0.7,\n }));\n\n const categoryEntries: MetadataRoute.Sitemap = categories.map((c) => ({\n url: `${siteUrl}/categories/${c.slug}`,\n lastModified: now,\n changeFrequency: "weekly",\n priority: 0.6,\n }));\n\n const collectionEntries: MetadataRoute.Sitemap = collections.map((c) => ({\n url: `${siteUrl}/collections/${c.slug}`,\n lastModified: now,\n changeFrequency: "weekly",\n priority: 0.6,\n }));\n\n return [...staticEntries, ...categoryEntries, ...collectionEntries, ...productEntries];\n}\n' }, { "path": "app/opensearch.xml/route.ts", "kind": "text", "content": 'import { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\n\n/**\n * OpenSearch description document \u2014 lets browsers add this site to the\n * address bar\'s search engine list. When users press Tab after typing the\n * domain, they get an inline search box that hits /search?q=...\n */\nexport async function GET(): Promise<Response> {\n const siteUrl = await getSiteUrl();\n const xml = `<?xml version="1.0" encoding="UTF-8"?>\n<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">\n <ShortName>${escapeXml(brand.shortName)}</ShortName>\n <Description>Search ${escapeXml(brand.name)}</Description>\n <InputEncoding>UTF-8</InputEncoding>\n <Url type="text/html" method="get" template="${siteUrl}/search?q={searchTerms}" />\n <Url type="application/opensearchdescription+xml" rel="self" template="${siteUrl}/opensearch.xml" />\n <moz:SearchForm>${siteUrl}/search</moz:SearchForm>\n</OpenSearchDescription>\n`;\n return new Response(xml, {\n headers: {\n "Content-Type": "application/opensearchdescription+xml; charset=utf-8",\n "Cache-Control": "public, max-age=86400",\n },\n });\n}\n\nfunction escapeXml(s: string): string {\n return s\n .replace(/&/g, "&amp;")\n .replace(/</g, "&lt;")\n .replace(/>/g, "&gt;")\n .replace(/"/g, "&quot;")\n .replace(/\'/g, "&apos;");\n}\n' }, { "path": "app/contact/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { ContactForm } from "./contact-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Contact \u2014 ${brand.name}`,\n description: brand.contactPage.body,\n};\n\nexport default function ContactPage() {\n const c = brand.contactPage;\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {c.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-4 -tracking-[0.02em]">\n {c.title}\n </h1>\n <p className="text-lg text-muted-foreground max-w-2xl mb-12 leading-relaxed">{c.body}</p>\n\n <div className="grid grid-cols-1 lg:grid-cols-[1.5fr_1fr] gap-10 items-start">\n <ContactForm reasons={c.reasons} />\n <aside className="space-y-5 lg:pl-10 lg:border-l border-border">\n <div>\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Direct lines\n </p>\n <ul className="space-y-3 list-none p-0 m-0">\n {c.directLines.map((line) => (\n <li key={line.label}>\n <p className="text-xs text-muted-foreground m-0">{line.label}</p>\n <a\n href={line.href}\n className="text-foreground hover:text-primary transition-colors"\n >\n {line.value}\n </a>\n </li>\n ))}\n </ul>\n </div>\n <div>\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Visit\n </p>\n <p className="text-foreground m-0">{brand.contact.address}</p>\n <p className="text-xs text-muted-foreground mt-1">{brand.contact.hours}</p>\n </div>\n </aside>\n </div>\n </article>\n );\n}\n' }, { "path": "app/contact/contact-form.tsx", "kind": "text", "content": `"use client";
4038
4038
 
4039
4039
  import { useState } from "react";
4040
4040
 
@@ -5700,7 +5700,7 @@ export const brand: Brand = {
5700
5700
  businessId: "bus_wellspring_pharmacy",
5701
5701
  },
5702
5702
  };
5703
- ` }, { "path": "lib/cimplify-loader.ts", "kind": "text", "content": 'import type { ImageLoader } from "next/image";\n\nimport { assetUrl, isCimplifyAsset } from "@cimplify/sdk";\n\nconst cdnBase = process.env.NEXT_PUBLIC_CIMPLIFY_CDN_URL?.trim() || undefined;\n\nconst cimplifyImageLoader: ImageLoader = ({ src, width, quality }) => {\n if (isCimplifyAsset(src, cdnBase)) {\n return assetUrl(src, {\n base: cdnBase,\n w: width,\n quality,\n format: "auto",\n });\n }\n return src;\n};\n\nexport default cimplifyImageLoader;\n' }, { "path": "lib/site.config.ts", "kind": "text", "content": "/**\n * Canonical absolute URL. The platform overwrites this at build time with the\n * storefront's primary domain; the default only applies to local builds. It's\n * per-deploy config, not per-request, so it stays a static constant \u2014 keeping\n * the root layout prerenderable under `cacheComponents`.\n */\nexport const SITE_URL = \"https://example.com\";\n" }, { "path": "lib/cart.ts", "kind": "text", "content": '"use client";\n\nimport { useCart } from "@cimplify/sdk/react";\n\n/**\n * Reactive cart count for the header pill. Subscribes to the SDK cart\n * state \u2014 updates immediately on add / remove / quantity change.\n */\nexport function useCartCount(): { count: number } {\n const { itemCount } = useCart();\n return { count: itemCount };\n}\n' }, { "path": "lib/site-url.ts", "kind": "text", "content": 'import { SITE_URL } from "./site.config";\n\n/** Canonical absolute URL for this storefront. */\nexport async function getSiteUrl(): Promise<string> {\n return SITE_URL;\n}\n' }, { "path": "metadata.json", "kind": "text", "content": '{\n "id": "pharmacy",\n "name": "Pharmacy",\n "tagline": "Licensed community pharmacy with prescription uploads, pharmacist consults, and OTC catalog.",\n "industry": "healthcare",\n "tags": ["healthcare", "regulated", "prescription"],\n "stability": "stable",\n "schemaType": "Pharmacy",\n "mock": {\n "seedName": "pharmacy",\n "seedBusinessId": "bus_wellspring_pharmacy"\n }\n}\n' }, { "path": "tsconfig.json", "kind": "text", "content": '{\n "compilerOptions": {\n "target": "ES2022",\n "lib": ["dom", "dom.iterable", "esnext"],\n "allowJs": false,\n "skipLibCheck": true,\n "strict": true,\n "noEmit": true,\n "esModuleInterop": true,\n "module": "esnext",\n "moduleResolution": "bundler",\n "resolveJsonModule": true,\n "isolatedModules": true,\n "jsx": "preserve",\n "incremental": true,\n "plugins": [{ "name": "next" }],\n "paths": {\n "@/*": ["./*"]\n }\n },\n "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts"],\n "exclude": ["node_modules"]\n}\n' }, { "path": "CLAUDE.md", "kind": "text", "content": "# CLAUDE.md\n\nThis project was scaffolded from a Cimplify storefront template (`cimplify init`).\n\n## Read these first\n\n- **`AGENTS.md`** at the project root \u2014 the file \u2194 `brand.X` field map for *this template's* industry, plus the architectural rules.\n- **`.claude/skills/cimplify-storefront/SKILL.md`** \u2014 the playbook for common tasks (rebrand, add a section, wire a Server Action, deploy, eject a component). Invoke it whenever you're asked to change content, palette, copy, or routing.\n\n## The two-line summary\n\n1. **Edit `lib/brand.ts`** for any visible string.\n2. **Edit `app/globals.css` `@theme` block** for any palette / radius / font change.\n\nThat covers ~95% of what merchants ask for. For anything else, follow the `cimplify-storefront` skill.\n\n## Don't do\n\n- Hardcode strings in pages or components.\n- Disable `cacheComponents: true` in `next.config.ts`.\n- Use `unstable_cache` \u2014 Next 16's canonical primitive is `'use cache'`.\n- Run `bun test` (Bun's `vi` shim is incomplete) \u2014 use `bun run test:run`.\n" }, { "path": "README.md", "kind": "text", "content": '# __STOREFRONT_NAME__\n\nA Cimplify storefront scaffolded from the **pharmacy** template \u2014 Next.js 16 (App Router), React 19, Tailwind v4. Clinical white + calm teal palette, Inter + JetBrains Mono typography, trustworthy community-pharmacy aesthetic.\n\n## Run\n\n```bash\nbun install\nbun dev\n```\n\nTwo things start in parallel:\n\n- `cimplify-mock --seed pharmacy` \u2014 the Cimplify mock API on `http://127.0.0.1:8787`, seeded with Wellspring Pharmacy.\n- `next dev` \u2014 this storefront on `http://localhost:3000`.\n\nOpen the storefront in your browser. Edit `app/page.tsx` to start customising.\n\n## Structure\n\n```\napp/\n layout.tsx # root layout, fonts, providers, header/footer/modal\n page.tsx # home\n shop/page.tsx # full catalogue (SDK <CataloguePage/>)\n collections/[slug]/ # collection landing\n categories/[slug]/ # category landing\n cart/page.tsx # SDK <CartPage/>\n checkout/page.tsx # SDK <CheckoutPage/>\n orders/[id]/page.tsx # post-checkout thank-you\n about, faq, terms, privacy\n globals.css # Tailwind import + theme tokens\ncomponents/\n providers.tsx # CimplifyProvider client wrapper\n header.tsx, footer.tsx, hero.tsx\n store-product-card.tsx # SDK <ProductCard/>\n trade-in-cta.tsx # repurposed as a prescription-upload CTA\n collection-strip.tsx # horizontal product strip\n category-tiles.tsx # SDK category grid\nlib/\n brand.ts # every visible string (pharmacy copy)\n cart.ts # useCartCount() for the header pill\n```\n\n## Pharmacy-specific behaviour\n\n- Prescription-required products (insulin, amoxicillin, etc.) ship with `input_fields` on the catalogue payload \u2014 the SDK product page renders prescription upload + consent signature + DOB inputs automatically. Look at how `paracetamol`, `amoxicillin-rx`, and `baby-formula` differ in the mock seed.\n- `brand.tradeIn` is repurposed as the "Send us your script" CTA on the home page.\n- `Pharmacy` schema.org `@type` is set in `brand.schemaType` so JSON-LD on every page identifies the business correctly.\n\n## Switch the seed\n\nThis template is wired to the `pharmacy` seed. To preview a different industry without re-scaffolding:\n\n```bash\ncimplify-mock --seed restaurant # Mama\'s Kitchen\ncimplify-mock --seed services # Serene Spa\ncimplify-mock --seed grocery # FreshMart\ncimplify-mock --seed retail # Currents Electronics\n```\n\nFor a fresh scaffold with another design altogether:\n\n```bash\ncimplify init my-store --template bakery\ncimplify init my-store --template restaurant\ncimplify init my-store --template services\ncimplify init my-store --template grocery\ncimplify init my-store --template fashion\ncimplify init my-store --template retail\n```\n\n## Go live\n\n```diff\n# .env.local\n- NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev\n+ NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=<your tenant key>\n```\n\nDeploy with `cimplify deploy --prod` after linking the project. See [`cimplify` CLI docs](https://www.cimplify.dev/docs/cli). `next.config.ts` already whitelists the SDK image hosts under `images.remotePatterns`.\n' }, { "path": ".env.example", "kind": "text", "content": "# Your tenant public key. Get it from the desk's Developers tab.\n# `cpk_live_*` / `cpk_test_*` keys auto-route same-origin requests\n# to hosted Cimplify in both dev and prod. Anything else (`mock-dev`,\n# empty) falls back to the local `cimplify dev` mock.\nNEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev\n\n# Optional. Forces a fixed canonical URL for sitemap.xml, robots.txt,\n# OpenGraph, and llms.txt. Leave unset and the storefront derives the\n# canonical from the request `Host` header automatically.\n# NEXT_PUBLIC_SITE_URL=\n" }] };
5703
+ ` }, { "path": "lib/cimplify-loader.ts", "kind": "text", "content": 'import type { ImageLoader } from "next/image";\n\nimport { assetUrl, isCimplifyAsset } from "@cimplify/sdk";\n\nconst cdnBase = process.env.NEXT_PUBLIC_CIMPLIFY_CDN_URL?.trim() || undefined;\n\nconst cimplifyImageLoader: ImageLoader = ({ src, width, quality }) => {\n if (isCimplifyAsset(src, cdnBase)) {\n return assetUrl(src, {\n base: cdnBase,\n w: width,\n quality,\n format: "auto",\n });\n }\n return src;\n};\n\nexport default cimplifyImageLoader;\n' }, { "path": "lib/site.config.ts", "kind": "text", "content": "/**\n * Canonical absolute URL. The platform overwrites this at build time with the\n * storefront's primary domain; the default only applies to local builds. It's\n * per-deploy config, not per-request, so it stays a static constant \u2014 keeping\n * the root layout prerenderable under `cacheComponents`.\n */\nexport const SITE_URL = \"https://example.com\";\n" }, { "path": "lib/cart.ts", "kind": "text", "content": '"use client";\n\nimport { useCart } from "@cimplify/sdk/react";\n\n/**\n * Reactive cart count for the header pill. Subscribes to the SDK cart\n * state \u2014 updates immediately on add / remove / quantity change.\n */\nexport function useCartCount(): { count: number } {\n const { itemCount } = useCart();\n return { count: itemCount };\n}\n' }, { "path": "lib/site-url.ts", "kind": "text", "content": 'import { SITE_URL } from "./site.config";\n\n/** Canonical absolute URL for this storefront. */\nexport async function getSiteUrl(): Promise<string> {\n return SITE_URL;\n}\n' }, { "path": "metadata.json", "kind": "text", "content": '{\n "id": "pharmacy",\n "name": "Pharmacy",\n "tagline": "Licensed community pharmacy with prescription uploads, pharmacist consults, and OTC catalog.",\n "industry": "healthcare",\n "tags": ["healthcare", "regulated", "prescription"],\n "stability": "stable",\n "schemaType": "Pharmacy",\n "mock": {\n "seedName": "pharmacy",\n "seedBusinessId": "bus_wellspring_pharmacy"\n }\n}\n' }, { "path": "tsconfig.json", "kind": "text", "content": '{\n "compilerOptions": {\n "target": "ES2022",\n "lib": ["dom", "dom.iterable", "esnext"],\n "allowJs": false,\n "skipLibCheck": true,\n "strict": true,\n "noEmit": true,\n "esModuleInterop": true,\n "module": "esnext",\n "moduleResolution": "bundler",\n "resolveJsonModule": true,\n "isolatedModules": true,\n "jsx": "preserve",\n "incremental": true,\n "plugins": [{ "name": "next" }],\n "paths": {\n "@/*": ["./*"]\n }\n },\n "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts"],\n "exclude": ["node_modules"]\n}\n' }, { "path": "CLAUDE.md", "kind": "text", "content": "# CLAUDE.md\n\nThis project was scaffolded from a Cimplify storefront template (`cimplify init`).\n\n## Read these first\n\n- **`AGENTS.md`** at the project root \u2014 the file \u2194 `brand.X` field map for *this template's* industry, plus the architectural rules.\n- **`.claude/skills/cimplify-storefront/SKILL.md`** \u2014 the playbook for common tasks (rebrand, add a section, wire a Server Action, deploy, eject a component). Invoke it whenever you're asked to change content, palette, copy, or routing.\n\n## The two-line summary\n\n1. **Edit `lib/brand.ts`** for any visible string.\n2. **Edit `app/globals.css` `@theme` block** for any palette / radius / font change.\n\nThat covers ~95% of what merchants ask for. For anything else, follow the `cimplify-storefront` skill.\n\n## Don't do\n\n- Hardcode strings in pages or components.\n- Disable `cacheComponents: true` in `next.config.ts`.\n- Use `unstable_cache` \u2014 Next 16's canonical primitive is `'use cache'`.\n- Run `bun test` (Bun's `vi` shim is incomplete) \u2014 use `bun run test:run`.\n" }, { "path": "README.md", "kind": "text", "content": '# __STOREFRONT_NAME__\n\nA Cimplify storefront scaffolded from the **pharmacy** template \u2014 Next.js 16 (App Router), React 19, Tailwind v4. Clinical white + calm teal palette, Inter + JetBrains Mono typography, trustworthy community-pharmacy aesthetic.\n\n## Run\n\n```bash\nbun install\nbun dev\n```\n\nTwo things start in parallel:\n\n- `cimplify-mock --seed pharmacy` \u2014 the Cimplify mock API on `http://127.0.0.1:8787`, seeded with Wellspring Pharmacy.\n- `next dev` \u2014 this storefront on `http://localhost:3000`.\n\nOpen the storefront in your browser. Edit `app/page.tsx` to start customising.\n\n## Structure\n\n```\napp/\n layout.tsx # root layout, fonts, providers, header/footer/modal\n page.tsx # home\n shop/page.tsx # full catalogue (SDK <CataloguePage/>)\n collections/[slug]/ # collection landing\n categories/[slug]/ # category landing\n cart/page.tsx # SDK <CartPage/>\n checkout/page.tsx # SDK <CheckoutPage/>\n orders/[id]/page.tsx # post-checkout thank-you\n about, faq, terms, privacy\n globals.css # Tailwind import + theme tokens\ncomponents/\n providers.tsx # CimplifyProvider client wrapper\n header.tsx, footer.tsx, hero.tsx\n store-product-card.tsx # SDK <ProductCard/>\n trade-in-cta.tsx # repurposed as a prescription-upload CTA\n collection-strip.tsx # horizontal product strip\n category-tiles.tsx # SDK category grid\nlib/\n brand.ts # every visible string (pharmacy copy)\n cart.ts # useCartCount() for the header pill\n```\n\n## Pharmacy-specific behaviour\n\n- Prescription-required products (insulin, amoxicillin, etc.) ship with `input_fields` on the catalogue payload \u2014 the SDK product page renders prescription upload + consent signature + DOB inputs automatically. Look at how `paracetamol`, `amoxicillin-rx`, and `baby-formula` differ in the mock seed.\n- `brand.tradeIn` is repurposed as the "Send us your script" CTA on the home page.\n- `Pharmacy` schema.org `@type` is set in `brand.schemaType` so JSON-LD on every page identifies the business correctly.\n\n## Switch the seed\n\nThis template is wired to the `pharmacy` seed. To preview a different industry without re-scaffolding:\n\n```bash\ncimplify-mock --seed restaurant # Mama\'s Kitchen\ncimplify-mock --seed services # Serene Spa\ncimplify-mock --seed grocery # FreshMart\ncimplify-mock --seed retail # Currents Electronics\n```\n\nFor a fresh scaffold with another design altogether:\n\n```bash\ncimplify init my-store --template bakery\ncimplify init my-store --template restaurant\ncimplify init my-store --template services\ncimplify init my-store --template grocery\ncimplify init my-store --template fashion\ncimplify init my-store --template retail\n```\n\n## Go live\n\n```diff\n# .env.local\n- NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev\n+ NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=<your tenant key>\n```\n\nDeploy with `cimplify deploy --prod` after linking the project. See [`cimplify` CLI docs](https://www.cimplify.dev/docs/cli). `next.config.ts` already whitelists the SDK image hosts under `images.remotePatterns`.\n' }, { "path": "bun.lock", "kind": "binary", "content": "ewogICJsb2NrZmlsZVZlcnNpb24iOiAxLAogICJjb25maWdWZXJzaW9uIjogMSwKICAid29ya3NwYWNlcyI6IHsKICAgICIiOiB7CiAgICAgICJuYW1lIjogIl9fU1RPUkVGUk9OVF9OQU1FX18iLAogICAgICAiZGVwZW5kZW5jaWVzIjogewogICAgICAgICJAY2ltcGxpZnkvc2RrIjogIl4wLjQ4LjIiLAogICAgICAgICJuZXh0IjogIl4xNi4yLjQiLAogICAgICAgICJyZWFjdCI6ICJeMTkuMC4wIiwKICAgICAgICAicmVhY3QtZG9tIjogIl4xOS4wLjAiLAogICAgICB9LAogICAgICAiZGV2RGVwZW5kZW5jaWVzIjogewogICAgICAgICJAdGFpbHdpbmRjc3MvcG9zdGNzcyI6ICJeNC4yLjQiLAogICAgICAgICJAdHlwZXMvbm9kZSI6ICJeMjIuMTAuMCIsCiAgICAgICAgIkB0eXBlcy9yZWFjdCI6ICJeMTkuMC4wIiwKICAgICAgICAiQHR5cGVzL3JlYWN0LWRvbSI6ICJeMTkuMC4wIiwKICAgICAgICAiY29uY3VycmVudGx5IjogIl45LjAuMCIsCiAgICAgICAgInRhaWx3aW5kY3NzIjogIl40LjIuNCIsCiAgICAgICAgInR5cGVzY3JpcHQiOiAiXjUuOS4zIiwKICAgICAgICAidml0ZXN0IjogIl40LjEuNSIsCiAgICAgIH0sCiAgICB9LAogIH0sCiAgInBhY2thZ2VzIjogewogICAgIkBhbGxvYy9xdWljay1scnUiOiBbIkBhbGxvYy9xdWljay1scnVANS4yLjAiLCAiIiwge30sICJzaGE1MTItVXJjQUJCKzRiVXJGQUJ3Ymx1VElCRXJYd3Zic1UvVjdUWldmbWJnSmZia3dpQnV6aVM5Z3hkT0RVeXVpZWNmZEdRODVqZ2xNVzZqdVMzK3o1VHNLTHc9PSJdLAoKICAgICJAYmFiZWwvcnVudGltZSI6IFsiQGJhYmVsL3J1bnRpbWVANy4yOS4yIiwgIiIsIHt9LCAic2hhNTEyLUppRFNoSDQ1ektIV3lHZTRaTlZSckNqQno4Tmg5VE1tWkcxa2g0UVRLOGhDQlRXQmk4RGEraTdzMWZKdzcvbFlwTTRjY2VwU05mcXpaL1F2QUJCaTVnPT0iXSwKCiAgICAiQGJhc2UtdWkvcmVhY3QiOiBbIkBiYXNlLXVpL3JlYWN0QDEuNS4wIiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIkBiYWJlbC9ydW50aW1lIjogIl43LjI5LjIiLCAiQGJhc2UtdWkvdXRpbHMiOiAiMC4yLjkiLCAiQGZsb2F0aW5nLXVpL3JlYWN0LWRvbSI6ICJeMi4xLjgiLCAiQGZsb2F0aW5nLXVpL3V0aWxzIjogIl4wLjIuMTEiLCAidXNlLXN5bmMtZXh0ZXJuYWwtc3RvcmUiOiAiXjEuNi4wIiB9LCAicGVlckRlcGVuZGVuY2llcyI6IHsgIkBkYXRlLWZucy90eiI6ICJeMS4yLjAiLCAiQHR5cGVzL3JlYWN0IjogIl4xNyB8fCBeMTggfHwgXjE5IiwgImRhdGUtZm5zIjogIl40LjAuMCIsICJyZWFjdCI6ICJeMTcgfHwgXjE4IHx8IF4xOSIsICJyZWFjdC1kb20iOiAiXjE3IHx8IF4xOCB8fCBeMTkiIH0sICJvcHRpb25hbFBlZXJzIjogWyJAZGF0ZS1mbnMvdHoiLCAiQHR5cGVzL3JlYWN0IiwgImRhdGUtZm5zIl0gfSwgInNoYTUxMi16MWdTQWxjZWQxeVkraU0rbUhERXRJa0Q4VUkzRWJzNTJNdUJQeHZWNmY1aFJ1dGsreHZDSC93dUI3aERxRHpLOUpHNUZvTXo1bmhycXRTczF3anQxQT09Il0sCgogICAgIkBiYXNlLXVpL3V0aWxzIjogWyJAYmFzZS11aS91dGlsc0AwLjIuOSIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAYmFiZWwvcnVudGltZSI6ICJeNy4yOS4yIiwgIkBmbG9hdGluZy11aS91dGlscyI6ICJeMC4yLjExIiwgInJlc2VsZWN0IjogIl41LjEuMSIsICJ1c2Utc3luYy1leHRlcm5hbC1zdG9yZSI6ICJeMS42LjAiIH0sICJwZWVyRGVwZW5kZW5jaWVzIjogeyAiQHR5cGVzL3JlYWN0IjogIl4xNyB8fCBeMTggfHwgXjE5IiwgInJlYWN0IjogIl4xNyB8fCBeMTggfHwgXjE5IiwgInJlYWN0LWRvbSI6ICJeMTcgfHwgXjE4IHx8IF4xOSIgfSwgIm9wdGlvbmFsUGVlcnMiOiBbIkB0eXBlcy9yZWFjdCJdIH0sICJzaGE1MTIteC9QRERDWXpvcVBwanJkeWIzVmN5eWxUSTJJalVYRXRZREdpNWZvaDdLc25tTkpJSWFWd0EyR0xnREgxZHBzMUdnWGlKYkE2MGhNK0F5dVRmUXpJdnc9PSJdLAoKICAgICJAY2ltcGxpZnkvc2RrIjogWyJAY2ltcGxpZnkvc2RrQDAuNDguMiIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAYmFzZS11aS9yZWFjdCI6ICJeMS40LjEiLCAiY2xzeCI6ICJeMi4xLjEiLCAiZGF0ZS1mbnMiOiAiXjQuMS4wIiwgImxpYnBob25lbnVtYmVyLWpzIjogIjEuMTIuNDEiLCAicmVhY3QtZGF5LXBpY2tlciI6ICJeOS4xNC4wIiwgInRhaWx3aW5kLW1lcmdlIjogIl4zLjUuMCIsICJ6b2QiOiAiXjQuNC4zIiB9LCAicGVlckRlcGVuZGVuY2llcyI6IHsgIkBwYXlzdGFjay9pbmxpbmUtanMiOiAiXjIuMjIuOCIsICJtc3ciOiAiPj0yLjAuMCIsICJyZWFjdCI6ICI+PTE3LjAuMCIsICJ2aXRlc3QiOiAiPj0yLjAuMCIgfSwgIm9wdGlvbmFsUGVlcnMiOiBbIkBwYXlzdGFjay9pbmxpbmUtanMiLCAibXN3IiwgInJlYWN0IiwgInZpdGVzdCJdLCAiYmluIjogeyAiY2ltcGxpZnktbW9jayI6ICJkaXN0L21vY2svY2xpLm1qcyIgfSB9LCAic2hhNTEyLTZJaDhNNkxZVkhDdU84c2JnZWc3VmdQTUJIWHZNK2NodFkzLzJWZkdWRWZFeU5UY0d4UzZlcDFOMDdNS21tTG5xQWNMZTZteFQwbVFiL2h0cldQZ1hBPT0iXSwKCiAgICAiQGRhdGUtZm5zL3R6IjogWyJAZGF0ZS1mbnMvdHpAMS40LjEiLCAiIiwge30sICJzaGE1MTItUDVMVU5odGJqNllmSTNpSmp3NUVMOWVVQUc2T2l0RDBXM2ZXUWNwUWpEUmMvUUlzTDB0Uk51TzFQY0R2UGNjV0wxZlNUWFhkRTFkcytsOTVEVi9PRkE9PSJdLAoKICAgICJAZW1uYXBpL2NvcmUiOiBbIkBlbW5hcGkvY29yZUAxLjEwLjAiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiQGVtbmFwaS93YXNpLXRocmVhZHMiOiAiMS4yLjEiLCAidHNsaWIiOiAiXjIuNC4wIiB9IH0sICJzaGE1MTIteXE2T2tKNHA4MkNBZlBsMHU5bVFlYlFIS1BKa1k3V3JJdWsyMDVjVFluWWUrazJaOFlCaDExRnJiUkcvSDZpaGlycWNhY09nbDJCSU84b3lNUUxlWHc9PSJdLAoKICAgICJAZW1uYXBpL3J1bnRpbWUiOiBbIkBlbW5hcGkvcnVudGltZUAxLjEwLjAiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAidHNsaWIiOiAiXjIuNC4wIiB9IH0sICJzaGE1MTItZXd2WWxrODZ4VW9HSTB6UVJOcS9tQysxNlIxUWVEbEtReTIxS2kzb1NZWE5nTGI0NUdWMVA2QTBNKy9zNm55Q3VORHFlNVZwYVk4NEJ6WEd3VmJ3RkE9PSJdLAoKICAgICJAZW1uYXBpL3dhc2ktdGhyZWFkcyI6IFsiQGVtbmFwaS93YXNpLXRocmVhZHNAMS4yLjEiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAidHNsaWIiOiAiXjIuNC4wIiB9IH0sICJzaGE1MTItdVRJSTdPWUYrL01lcy9NcmNJT1lwNXlPdFNNTEJXU0lvTFBwY2d3aXBvaUtibGk2azMyMnRjb0ZzeG9JSXhQRHFXMDFTUUdBZ2tvNEV6WmkyQk52Mnc9PSJdLAoKICAgICJAZmxvYXRpbmctdWkvY29yZSI6IFsiQGZsb2F0aW5nLXVpL2NvcmVAMS43LjUiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiQGZsb2F0aW5nLXVpL3V0aWxzIjogIl4wLjIuMTEiIH0gfSwgInNoYTUxMi0xSWg0V1RXeXcwK2xLeUZNY0JIR2JiNVU1RnR1SEp1dWpveXlyNXpUYVdTNUVZTWVUNkpiMkF1RGVmdHNDc0V1Y2hPK21NMmlqNStxOWNyaHlkekxoUT09Il0sCgogICAgIkBmbG9hdGluZy11aS9kb20iOiBbIkBmbG9hdGluZy11aS9kb21AMS43LjYiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiQGZsb2F0aW5nLXVpL2NvcmUiOiAiXjEuNy41IiwgIkBmbG9hdGluZy11aS91dGlscyI6ICJeMC4yLjExIiB9IH0sICJzaGE1MTItOWdaU0FJNVhNMzY4ODBQUE1tLy85ZGZpRW5nWW9DNkFtMml6RVMxRkY0MDZZRnNqdnlCTW1lSjJnNFNBanUzeFd3dHV5bk5SRkwyczloZ3hwTEk1U1E9PSJdLAoKICAgICJAZmxvYXRpbmctdWkvcmVhY3QtZG9tIjogWyJAZmxvYXRpbmctdWkvcmVhY3QtZG9tQDIuMS44IiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIkBmbG9hdGluZy11aS9kb20iOiAiXjEuNy42IiB9LCAicGVlckRlcGVuZGVuY2llcyI6IHsgInJlYWN0IjogIj49MTYuOC4wIiwgInJlYWN0LWRvbSI6ICI+PTE2LjguMCIgfSB9LCAic2hhNTEyLWNDNTJiSHdNL24vQ3hTODdGSDB5V2RuZ0VacmpkdExXL3FWcnVvNjhxZytwcks3WlE0WUdkdXQyR3lEVnBvR2VBWWUvaDg5OXJWZU9WbTZPaTQwazJBPT0iXSwKCiAgICAiQGZsb2F0aW5nLXVpL3V0aWxzIjogWyJAZmxvYXRpbmctdWkvdXRpbHNAMC4yLjExIiwgIiIsIHt9LCAic2hhNTEyLVJpQi95SWg3OHBjSXhsNmxMTUcwQ2dCWEFaMlkwZVZIcU1QWXVndSs5VTBBZVQ2WUJlaUpwZjdsYmRKTkl1Z0ZQNVNJandOUmdvNERoUjFReGkyNkdnPT0iXSwKCiAgICAiQGltZy9jb2xvdXIiOiBbIkBpbWcvY29sb3VyQDEuMS4wIiwgIiIsIHt9LCAic2hhNTEyLVRkNzZxN2o1N28vdExWZGdTNzQ2Y1lBUmZTeXhrOGlFZlJ4ZXdMOWg0T016WWhiVzRUQWNwcGwwbVQ0ZXlxWGRkaDZML2p3b003NW1vN2l4YS9wQ2VRPT0iXSwKCiAgICAiQGltZy9zaGFycC1kYXJ3aW4tYXJtNjQiOiBbIkBpbWcvc2hhcnAtZGFyd2luLWFybTY0QDAuMzQuNSIsICIiLCB7ICJvcHRpb25hbERlcGVuZGVuY2llcyI6IHsgIkBpbWcvc2hhcnAtbGlidmlwcy1kYXJ3aW4tYXJtNjQiOiAiMS4yLjQiIH0sICJvcyI6ICJkYXJ3aW4iLCAiY3B1IjogImFybTY0IiB9LCAic2hhNTEyLWltdFEzV01KWGJNWTRmeGIvTmRwNkhCVE5WdFdDVUkwV2RvYnloZUdmNSthZDZ4WDhWSURPOHUyeEU0cWMvZnIwOENLRy83ZERzZUZ0bjZNNmcvcjN3PT0iXSwKCiAgICAiQGltZy9zaGFycC1kYXJ3aW4teDY0IjogWyJAaW1nL3NoYXJwLWRhcndpbi14NjRAMC4zNC41IiwgIiIsIHsgIm9wdGlvbmFsRGVwZW5kZW5jaWVzIjogeyAiQGltZy9zaGFycC1saWJ2aXBzLWRhcndpbi14NjQiOiAiMS4yLjQiIH0sICJvcyI6ICJkYXJ3aW4iLCAiY3B1IjogIng2NCIgfSwgInNoYTUxMi1ZTkVGQUYvNEtRL1BlVzBOK3IrYVZWc29JWTAvcXh4aWtGMlNXZHArTlJrbU1CN3k5TEJaQVZxUTR5aEdDbS9IM0gyNzBPU3lrcW1RTUtMQmhCSkRFdz09Il0sCgogICAgIkBpbWcvc2hhcnAtbGlidmlwcy1kYXJ3aW4tYXJtNjQiOiBbIkBpbWcvc2hhcnAtbGlidmlwcy1kYXJ3aW4tYXJtNjRAMS4yLjQiLCAiIiwgeyAib3MiOiAiZGFyd2luIiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi16cWpqbzdSYXRGZkZvUDBNa1E1MWpmdUZaQm5WRTJwUmlheWRLSjFHL3JIWnZuc3JIQU9jUUFMSWk5c0E1Y281eGVuUWRUdWdDdnRiMWN1Zjc4VmY0Zz09Il0sCgogICAgIkBpbWcvc2hhcnAtbGlidmlwcy1kYXJ3aW4teDY0IjogWyJAaW1nL3NoYXJwLWxpYnZpcHMtZGFyd2luLXg2NEAxLjIuNCIsICIiLCB7ICJvcyI6ICJkYXJ3aW4iLCAiY3B1IjogIng2NCIgfSwgInNoYTUxMi0xSU9kNXhmVmhsR3dYK3pYdjJOOTNrMHlNT052VWxBTnlsYkp3MWVUYWg4Sy9KdHBpMTVLQytXU2lhWC9uQm1ibTJIeFJNMWdaMG5TZGpTc3JaYkdLZz09Il0sCgogICAgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC1hcm0iOiBbIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC1hcm1AMS4yLjQiLCAiIiwgeyAib3MiOiAibGludXgiLCAiY3B1IjogImFybSIgfSwgInNoYTUxMi1iRkk3eGNLRkVMZGlOQ1ZvdjhlNDRJYTR1MmJ5QStsM1h0c0FqK1E4dGZDd082QlE4aURvallkdm9QTXFzS0RrdW9PbytYNkhaQTBzMHExMUFOTVE4QT09Il0sCgogICAgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC1hcm02NCI6IFsiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4LWFybTY0QDEuMi40IiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi1leGNqWDhEZnNJY0oxMHgxS3pyNFJjV2UxZWRDOVBxdURSUlB4M1lWQ3ZRditVNXA3WWluMnMzMmZ0emlrWG9qYjFQSUZjLzlNdDI4L3kraVJrbGtydz09Il0sCgogICAgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC1wcGM2NCI6IFsiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4LXBwYzY0QDEuMi40IiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJwcGM2NCIgfSwgInNoYTUxMi1GTXV2R2lqTERZRzZsVytiL1V2eWlsVVd1NUF5dSszcjJkMVM4bm90aUdDSXlZVS83NmVpZzFVZk1ta1o3dndnT3J6S3psUWJGU3VRZmdtN0dZVVBwQT09Il0sCgogICAgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC1yaXNjdjY0IjogWyJAaW1nL3NoYXJwLWxpYnZpcHMtbGludXgtcmlzY3Y2NEAxLjIuNCIsICIiLCB7ICJvcyI6ICJsaW51eCIsICJjcHUiOiAibm9uZSIgfSwgInNoYTUxMi1vVkRiY1I0elVDMGNlODJ0ZXViU20reDZFVGl4dEtaQmgvcWJSRUlPY0kzY1VMekR5YjE4U3IvV2N5eDdOUlFlUXpPaUhUTmJaRkYxVXdQUzJzY3lHQT09Il0sCgogICAgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC1zMzkweCI6IFsiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4LXMzOTB4QDEuMi40IiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJzMzkweCIgfSwgInNoYTUxMi1xbXA5VnJ6Z1BnTW9HWnlQdnJRSHFrMDJ1eWpBMC9RclRPMjZUcWs2bDRaVjBNUFdJVzZMVGtxT0lvditKMXlFdTdNYkZRYURwd2R3SktoYkp2dVJ4UT09Il0sCgogICAgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC14NjQiOiBbIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC14NjRAMS4yLjQiLCAiIiwgeyAib3MiOiAibGludXgiLCAiY3B1IjogIng2NCIgfSwgInNoYTUxMi10SnhpaUxzbUhjOUF4MWJ6M29hT1lCVVJUWEdJUkRPREJxaHZlVkhvbnJISjkvK2s4OXFiTGwwYmNKbnMrZTR0NHJ2YU5CeGFFWnNGdFNmQWRxdVBydz09Il0sCgogICAgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eG11c2wtYXJtNjQiOiBbIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eG11c2wtYXJtNjRAMS4yLjQiLCAiIiwgeyAib3MiOiAibGludXgiLCAiY3B1IjogImFybTY0IiB9LCAic2hhNTEyLUZWUUh1d3gxSUl1Tm93OVFBYllVekorRW44S2NWbTlMazUrdUdVUUpIYVptTUVDWm1PbGl4OUhuSDduMVRSa1hNUzBwR3hJSm9rSVZCOVN1cVpHR1h3PT0iXSwKCiAgICAiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4bXVzbC14NjQiOiBbIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eG11c2wteDY0QDEuMi40IiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJ4NjQiIH0sICJzaGE1MTItK0xweUJrN0w0NFpJWHd6L1ZZZmdsYVgvb2t4ZXpFU2M2VXhEU295bzJLczZKeGM0WTdzR2pwZ1U5czRQTWdxZ2pqMWdaQ3lsVGllTmFtcUExTUY3RGc9PSJdLAoKICAgICJAaW1nL3NoYXJwLWxpbnV4LWFybSI6IFsiQGltZy9zaGFycC1saW51eC1hcm1AMC4zNC41IiwgIiIsIHsgIm9wdGlvbmFsRGVwZW5kZW5jaWVzIjogeyAiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4LWFybSI6ICIxLjIuNCIgfSwgIm9zIjogImxpbnV4IiwgImNwdSI6ICJhcm0iIH0sICJzaGE1MTItOWRMcXN2d3RnMXV1WEJHWktzeGVtOTU5NSt1anYwc0o2Vmk4d2NUQU5TRnB3Vi9HT05hdDVlQ2t6UW8vMU82elJJa2gwbS84KzVCanJScjdqRFVTWnc9PSJdLAoKICAgICJAaW1nL3NoYXJwLWxpbnV4LWFybTY0IjogWyJAaW1nL3NoYXJwLWxpbnV4LWFybTY0QDAuMzQuNSIsICIiLCB7ICJvcHRpb25hbERlcGVuZGVuY2llcyI6IHsgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC1hcm02NCI6ICIxLjIuNCIgfSwgIm9zIjogImxpbnV4IiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi1iS1F6YUpSWS9ia1BPWHlLeDVFVnVwN3FrYW9qRUNHNk5MWXN3Z2t0T1pqYVhlY1NBZUNXaVp3d2lGZjMvWStPMUhyYXVpRTNGVnNHeEZnOGMyNHJaZz09Il0sCgogICAgIkBpbWcvc2hhcnAtbGludXgtcHBjNjQiOiBbIkBpbWcvc2hhcnAtbGludXgtcHBjNjRAMC4zNC41IiwgIiIsIHsgIm9wdGlvbmFsRGVwZW5kZW5jaWVzIjogeyAiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4LXBwYzY0IjogIjEuMi40IiB9LCAib3MiOiAibGludXgiLCAiY3B1IjogInBwYzY0IiB9LCAic2hhNTEyLTd6em53TmFxVzZZdHNmckdHREE2QlJrSVNLQUFFMUpvMFFkcE5ZWE5NSHUyKzBkVHJQZmxUTE5rcGM4bDdNVVA1TTE2WkpjVXZ5c1ZXV3JNZWZacXVBPT0iXSwKCiAgICAiQGltZy9zaGFycC1saW51eC1yaXNjdjY0IjogWyJAaW1nL3NoYXJwLWxpbnV4LXJpc2N2NjRAMC4zNC41IiwgIiIsIHsgIm9wdGlvbmFsRGVwZW5kZW5jaWVzIjogeyAiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4LXJpc2N2NjQiOiAiMS4yLjQiIH0sICJvcyI6ICJsaW51eCIsICJjcHUiOiAibm9uZSIgfSwgInNoYTUxMi01MWdKdUxQVEthN3BpWVBhVnM4R21CeW83L1U3LzdUWk9xK2NuWEpJSFpLYXZJUkhBUDc3ZTNOMkhFbDNkZ2lxZEQvdzB5VWZpSm5JSTc3UHVEREZkdz09Il0sCgogICAgIkBpbWcvc2hhcnAtbGludXgtczM5MHgiOiBbIkBpbWcvc2hhcnAtbGludXgtczM5MHhAMC4zNC41IiwgIiIsIHsgIm9wdGlvbmFsRGVwZW5kZW5jaWVzIjogeyAiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4LXMzOTB4IjogIjEuMi40IiB9LCAib3MiOiAibGludXgiLCAiY3B1IjogInMzOTB4IiB9LCAic2hhNTEyLW5RdENrMFBkS2ZobzNlQzVNcmJRb2lnSjJnZDFDZ2RkVU1rYWJVaityQmV2czh0WjJjVUxPeDQ2RTdveVgrMDRXR2ZBQmdJd21NQzBWcWllVGlSNGpnPT0iXSwKCiAgICAiQGltZy9zaGFycC1saW51eC14NjQiOiBbIkBpbWcvc2hhcnAtbGludXgteDY0QDAuMzQuNSIsICIiLCB7ICJvcHRpb25hbERlcGVuZGVuY2llcyI6IHsgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC14NjQiOiAiMS4yLjQiIH0sICJvcyI6ICJsaW51eCIsICJjcHUiOiAieDY0IiB9LCAic2hhNTEyLU1FemQ4SFBLeFZ4VmVud0FhK0pSUHdFQzdRRmpvUFd1UzVOWm5CdDZCM3B1N0VHMkdlMGlkMW9MSFpwUEpkbjNPUUsrQlFEaXc5elN0aUhCVEpRUVFRPT0iXSwKCiAgICAiQGltZy9zaGFycC1saW51eG11c2wtYXJtNjQiOiBbIkBpbWcvc2hhcnAtbGludXhtdXNsLWFybTY0QDAuMzQuNSIsICIiLCB7ICJvcHRpb25hbERlcGVuZGVuY2llcyI6IHsgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eG11c2wtYXJtNjQiOiAiMS4yLjQiIH0sICJvcyI6ICJsaW51eCIsICJjcHUiOiAiYXJtNjQiIH0sICJzaGE1MTItZnBySlI2R3RSc010Nkt5ZnE0NElzQ2hWWmVHTjk3Z1REMzMxd2VSMWV4MWMxcnlwREVBQk42VG0yeGExd0U2bFliNURkRW5rMDNOWlBxQTdJZDIxeWc9PSJdLAoKICAgICJAaW1nL3NoYXJwLWxpbnV4bXVzbC14NjQiOiBbIkBpbWcvc2hhcnAtbGludXhtdXNsLXg2NEAwLjM0LjUiLCAiIiwgeyAib3B0aW9uYWxEZXBlbmRlbmNpZXMiOiB7ICJAaW1nL3NoYXJwLWxpYnZpcHMtbGludXhtdXNsLXg2NCI6ICIxLjIuNCIgfSwgIm9zIjogImxpbnV4IiwgImNwdSI6ICJ4NjQiIH0sICJzaGE1MTItSmc4d05UMU1Vekl2aEJGeFZpcXJFaFdER3pxeW1vM3NWN3o3WnNhV2JaTkRMWFJKWm9SR3JqdWxwNjBZWXRWNHdmWThWSUtjV2lkam9qbExjV3JkOFE9PSJdLAoKICAgICJAaW1nL3NoYXJwLXdhc20zMiI6IFsiQGltZy9zaGFycC13YXNtMzJAMC4zNC41IiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIkBlbW5hcGkvcnVudGltZSI6ICJeMS43LjAiIH0sICJjcHUiOiAibm9uZSIgfSwgInNoYTUxMi1PZFdURWlWa1kyUEh3cWtiQkk4ZnJGeFFRRmVrSGFTU2tVSUprd3pjbFdaZTY0TzFYNFVsVWpxcXFMYVBiVXBNT1FrNkZCdS9IdGxHWE5ibElzMGh1dz09Il0sCgogICAgIkBpbWcvc2hhcnAtd2luMzItYXJtNjQiOiBbIkBpbWcvc2hhcnAtd2luMzItYXJtNjRAMC4zNC41IiwgIiIsIHsgIm9zIjogIndpbjMyIiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi1XUTNBZ1dDV1lTYjJ5dCtJRzhtbkM2SmRrOVdoczdPMGd4cGhibHNMdmRoU3BTVHRtdTY5WkcxR2tiNk51dnhzTkFDd2lQVjZjTlNaTnp0MEtQc3c3Zz09Il0sCgogICAgIkBpbWcvc2hhcnAtd2luMzItaWEzMiI6IFsiQGltZy9zaGFycC13aW4zMi1pYTMyQDAuMzQuNSIsICIiLCB7ICJvcyI6ICJ3aW4zMiIsICJjcHUiOiAiaWEzMiIgfSwgInNoYTUxMi1GVjltLzdObWVDbVNIREQ1ajQrNHBOSThDcDNhVytKdkxvWGNUVW8wSXF5alNmQVpKOGRJVW1pangxcWFKc0lpVStIb3N3NnhNNUtpakFXUkpDU2dOZz09Il0sCgogICAgIkBpbWcvc2hhcnAtd2luMzIteDY0IjogWyJAaW1nL3NoYXJwLXdpbjMyLXg2NEAwLjM0LjUiLCAiIiwgeyAib3MiOiAid2luMzIiLCAiY3B1IjogIng2NCIgfSwgInNoYTUxMi0rMjlZTXNxWTIvOWVGRWlXOTNlcVdudUxjV2N1Zm93WGV3d1NOSVQ2VXdaZFVVQ3JNM29Gak1XSC9aNi9UTW1iNGhsRmVubWZBVmJwV2V1cDJqcnlDdz09Il0sCgogICAgIkBqcmlkZ2V3ZWxsL2dlbi1tYXBwaW5nIjogWyJAanJpZGdld2VsbC9nZW4tbWFwcGluZ0AwLjMuMTMiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiQGpyaWRnZXdlbGwvc291cmNlbWFwLWNvZGVjIjogIl4xLjUuMCIsICJAanJpZGdld2VsbC90cmFjZS1tYXBwaW5nIjogIl4wLjMuMjQiIH0gfSwgInNoYTUxMi0ya2t0LzduaUo2TWdFUHhGMGJZZFE2ZXRaYUErZlF2RGNMS2NraHkxeUlRT3phb0tqQkJqU2o2My9hTFZqWUUzcWhSdDVkdk0rdVV5ZkNnNlVLQ0JiQT09Il0sCgogICAgIkBqcmlkZ2V3ZWxsL3JlbWFwcGluZyI6IFsiQGpyaWRnZXdlbGwvcmVtYXBwaW5nQDIuMy41IiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIkBqcmlkZ2V3ZWxsL2dlbi1tYXBwaW5nIjogIl4wLjMuNSIsICJAanJpZGdld2VsbC90cmFjZS1tYXBwaW5nIjogIl4wLjMuMjQiIH0gfSwgInNoYTUxMi1MSTl1LytsYVlHNERzMVRES1NKVzJZUHJJbGNWWU93aTJmVUM2eEI0M2x1ZUNqZ3hWNGxmZk9DWkN0WUZpSDZUTk9YK3RRS1h4OTdUNElLSGJoeUhFUT09Il0sCgogICAgIkBqcmlkZ2V3ZWxsL3Jlc29sdmUtdXJpIjogWyJAanJpZGdld2VsbC9yZXNvbHZlLXVyaUAzLjEuMiIsICIiLCB7fSwgInNoYTUxMi1iUklTZ0NJalAyMC90YldTUFdNRWk1NFFWUFJaRXhrdUQ5bEpMK1VJeFVLdHdWSkE4d1cxVHJiMWpNczFSRlhvMUNCVE5aLzVocEM5UXZtS1dkb3BLdz09Il0sCgogICAgIkBqcmlkZ2V3ZWxsL3NvdXJjZW1hcC1jb2RlYyI6IFsiQGpyaWRnZXdlbGwvc291cmNlbWFwLWNvZGVjQDEuNS41IiwgIiIsIHt9LCAic2hhNTEyLWNZUTkzMTBncnF4dWVXYmwrV3VJVUlhaVVhRGNqN1dPcTVmVmhFbGpOVmdSZk9VaFk5ZnkyelR2Zm9xV3NuZWJoOFNsNzBWU2NGYklDdkpuTEtCME9nPT0iXSwKCiAgICAiQGpyaWRnZXdlbGwvdHJhY2UtbWFwcGluZyI6IFsiQGpyaWRnZXdlbGwvdHJhY2UtbWFwcGluZ0AwLjMuMzEiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiQGpyaWRnZXdlbGwvcmVzb2x2ZS11cmkiOiAiXjMuMS4wIiwgIkBqcmlkZ2V3ZWxsL3NvdXJjZW1hcC1jb2RlYyI6ICJeMS40LjE0IiB9IH0sICJzaGE1MTItenpOUitTZFFTREp6Yzhqb2FlUDhRUW9DUXI4TnVZeDJkSUl5dGwxUWVCRVpISjl1VzZoZWJzcllnYno4aEp3VVFhbzNUV0NNdG1mVjhOdTF0d09MQXc9PSJdLAoKICAgICJAbmFwaS1ycy93YXNtLXJ1bnRpbWUiOiBbIkBuYXBpLXJzL3dhc20tcnVudGltZUAxLjEuNCIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAdHlieXMvd2FzbS11dGlsIjogIl4wLjEwLjEiIH0sICJwZWVyRGVwZW5kZW5jaWVzIjogeyAiQGVtbmFwaS9jb3JlIjogIl4xLjcuMSIsICJAZW1uYXBpL3J1bnRpbWUiOiAiXjEuNy4xIiB9IH0sICJzaGE1MTItM05RTk5nQTFZU2xKYi9rTUgxaWxkQVNQOUhXNy83a1luUkkyc3pXSmFvZmFTMWhXbWJHSTRIK2QzKzIyYUd6WFhOOUlKK24rR2lGVmNHaXBKUDE4b3c9PSJdLAoKICAgICJAbmV4dC9lbnYiOiBbIkBuZXh0L2VudkAxNi4yLjYiLCAiIiwge30sICJzaGE1MTItZ2Q4SG9ITjR1Zmo3M1dtUjNKbVZvbHJwSlI0N0lMSzZMb3VQNXhFbFBnbGFWeGlyNmUxYTdWenZUdkRXa09vUFhUOXJra1R6eUN4QnU0eWVaZlp3Y3c9PSJdLAoKICAgICJAbmV4dC9zd2MtZGFyd2luLWFybTY0IjogWyJAbmV4dC9zd2MtZGFyd2luLWFybTY0QDE2LjIuNiIsICIiLCB7ICJvcyI6ICJkYXJ3aW4iLCAiY3B1IjogImFybTY0IiB9LCAic2hhNTEyLVpKR2trY05mWWdyck1rcU9kWjd6b0xhMVRPeTBxcGNNZmsvejRNaC9GS1V6NDBnVk8rSE5RV3FtTHhmNjdaNVdCNjREUnAwZGhFYnlIZmVsKzZzSlVnPT0iXSwKCiAgICAiQG5leHQvc3djLWRhcndpbi14NjQiOiBbIkBuZXh0L3N3Yy1kYXJ3aW4teDY0QDE2LjIuNiIsICIiLCB7ICJvcyI6ICJkYXJ3aW4iLCAiY3B1IjogIng2NCIgfSwgInNoYTUxMi12L1lMQkhJWTEzMkNlZDNwdUJKN1lKS3cxbHFzQ3JnY05vMmFSSmxDRXlRcnJDZVJKbHZHbG5teGhQeE5RSTNLRTNOMURONXI5VFBOUHZrYTNucTVSUT09Il0sCgogICAgIkBuZXh0L3N3Yy1saW51eC1hcm02NC1nbnUiOiBbIkBuZXh0L3N3Yy1saW51eC1hcm02NC1nbnVAMTYuMi42IiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi1SUE92cWxZQmJjUWprejlWUVFEWjJUMmJBUklqWFpWMUtGbHQrVjJNcjZTVy9lNEk5ZmNLc2FBMGhkeWYyRkhvVGxzVjJ4bkJkNVk5MTJyUC8xQ2U2dz09Il0sCgogICAgIkBuZXh0L3N3Yy1saW51eC1hcm02NC1tdXNsIjogWyJAbmV4dC9zd2MtbGludXgtYXJtNjQtbXVzbEAxNi4yLjYiLCAiIiwgeyAib3MiOiAibGludXgiLCAiY3B1IjogImFybTY0IiB9LCAic2hhNTEyLVVSVVR1MStkTWt4SnNQRmdtK09lRXZxOXdmNXN1ancwRXZnWXk4MFRER0hUU0xUbklIZXFiMEV1OEEzc0M5NUlSZ2plalFMK2tDNG13KzR5UHhpQVhBPT0iXSwKCiAgICAiQG5leHQvc3djLWxpbnV4LXg2NC1nbnUiOiBbIkBuZXh0L3N3Yy1saW51eC14NjQtZ251QDE2LjIuNiIsICIiLCB7ICJvcyI6ICJsaW51eCIsICJjcHUiOiAieDY0IiB9LCAic2hhNTEyLURPajE4Mm1QVjhHM1VrcmF5TG9SRU01WUVZSStEazV3djdPeDl4bDFmRmliQUVMRXNGRDBsRFBmSEllSUxsdXRNTWZkeWhsellQRUxHM3BldUthdXJ3PT0iXSwKCiAgICAiQG5leHQvc3djLWxpbnV4LXg2NC1tdXNsIjogWyJAbmV4dC9zd2MtbGludXgteDY0LW11c2xAMTYuMi42IiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJ4NjQiIH0sICJzaGE1MTItSEtRNVNQL1YvdWI3M1V2RjduL3plSmx4azJrTG10TDdXenJnNFdmbWtqbU5vczVvbkoydEt1N3laT1BkTDE4QTZTdmZuM21heDI5eW0rcnk3TmtLNGc9PSJdLAoKICAgICJAbmV4dC9zd2Mtd2luMzItYXJtNjQtbXN2YyI6IFsiQG5leHQvc3djLXdpbjMyLWFybTY0LW1zdmNAMTYuMi42IiwgIiIsIHsgIm9zIjogIndpbjMyIiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi1MWlhwVGxQeVM1djdIaFNtbnZzTEdQM2lJWWdZT0JuYzhyOEFybFQ1NXNHSFY4OWJSMkhsRGRCaldRK1BZNlNKTW1rOFR1VkdGdXhhbG5QM2svMER3Zz09Il0sCgogICAgIkBuZXh0L3N3Yy13aW4zMi14NjQtbXN2YyI6IFsiQG5leHQvc3djLXdpbjMyLXg2NC1tc3ZjQDE2LjIuNiIsICIiLCB7ICJvcyI6ICJ3aW4zMiIsICJjcHUiOiAieDY0IiB9LCAic2hhNTEyLUYwKzRpMGg5SjZDNGVFM0VBUFdzb0NrN1VXL2Riek9qeXp4WTBxbkRVT1lGdTZGRm1kWjZsOTcvWGRWMy9OejNWWXlPN1VXanlFSlVYa0dxY29YZk1BPT0iXSwKCiAgICAiQG94Yy1wcm9qZWN0L3R5cGVzIjogWyJAb3hjLXByb2plY3QvdHlwZXNAMC4xMzAuMCIsICIiLCB7fSwgInNoYTUxMi1pYkQydXN4OUpSdTdmNXB1MnRNS01JNGNwQTROZ1hKUW9ZUlA0cFE3UHhtbjFsNmsvNTNxV3RRV1pheWhZeTNYNFFaa3Q5ME90K21KRWFlWG91aW82UT09Il0sCgogICAgIkByb2xsZG93bi9iaW5kaW5nLWFuZHJvaWQtYXJtNjQiOiBbIkByb2xsZG93bi9iaW5kaW5nLWFuZHJvaWQtYXJtNjRAMS4wLjEiLCAiIiwgeyAib3MiOiAiYW5kcm9pZCIsICJjcHUiOiAiYXJtNjQiIH0sICJzaGE1MTItZkpJM0kwcjNDM09qL3pkQkNwYUNtQlJaWWYwN3hwYXE0eUNmRERvU0ZtK2JlV056YklsMjZwdVc4UnJhVWR1Z29Kdy85NXplck5PbjZqYXNBaHpTbWc9PSJdLAoKICAgICJAcm9sbGRvd24vYmluZGluZy1kYXJ3aW4tYXJtNjQiOiBbIkByb2xsZG93bi9iaW5kaW5nLWRhcndpbi1hcm02NEAxLjAuMSIsICIiLCB7ICJvcyI6ICJkYXJ3aW4iLCAiY3B1IjogImFybTY0IiB9LCAic2hhNTEyLWNLbkFoV0VzVjdUUGNBLzVFQXRlRHA2S2NKWkJRMkcrQnFFN3pheU1NaTdrTXZ3UnNidjdXVDlhT25uMFdObDRTS0VJZjQzdmpTMzFpVVB1ODBuelhnPT0iXSwKCiAgICAiQHJvbGxkb3duL2JpbmRpbmctZGFyd2luLXg2NCI6IFsiQHJvbGxkb3duL2JpbmRpbmctZGFyd2luLXg2NEAxLjAuMSIsICIiLCB7ICJvcyI6ICJkYXJ3aW4iLCAiY3B1IjogIng2NCIgfSwgInNoYTUxMi1ZS3JWd1FqSVJCUG8rNUcvdTAzd0dqYmR5NHE3cHl6Q2U5M0RLOVZKN3prVm1lZzhMSjdHYmdzaUhXZFI0eFNvZTRDQVhSRDdCY2pnYnRyNjRia1hOZz09Il0sCgogICAgIkByb2xsZG93bi9iaW5kaW5nLWZyZWVic2QteDY0IjogWyJAcm9sbGRvd24vYmluZGluZy1mcmVlYnNkLXg2NEAxLjAuMSIsICIiLCB7ICJvcyI6ICJmcmVlYnNkIiwgImNwdSI6ICJ4NjQiIH0sICJzaGE1MTItei9vQnNSRW80NlNzRnFCd1l0RmUwa3BKZUJpakFUNDhPL1dYTEk0c3VpQ0xCa3IwM1JUdFRKTUN6U2REZDJ6bmxoOFZKaXpMMDlYVmtRZ2s4SVpvbnc9PSJdLAoKICAgICJAcm9sbGRvd24vYmluZGluZy1saW51eC1hcm0tZ251ZWFiaWhmIjogWyJAcm9sbGRvd24vYmluZGluZy1saW51eC1hcm0tZ251ZWFiaWhmQDEuMC4xIiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJhcm0iIH0sICJzaGE1MTItaWs4cTdHTTExenh2WXhGYzJQZURjVDZUQnZoQ1FNYVV4ZnBoL001bDlzS3VUcy9TamczTCtCeXcwRjd3MFpWTEJabXgzMFArZ0cwRUN6ek4rTUZjbVE9PSJdLAoKICAgICJAcm9sbGRvd24vYmluZGluZy1saW51eC1hcm02NC1nbnUiOiBbIkByb2xsZG93bi9iaW5kaW5nLWxpbnV4LWFybTY0LWdudUAxLjAuMSIsICIiLCB7ICJvcyI6ICJsaW51eCIsICJjcHUiOiAiYXJtNjQiIH0sICJzaGE1MTItUW9TeDJFa3lycmRaNmtjeUU4c3RxWjYydDBZcmE4RnM1aWE5bE94SnJoNlRNUUpLN2dRS21zY2RUSGY3cE9YS1JFS3JWd090SmNRRzNxVlNmYzg2NkE9PSJdLAoKICAgICJAcm9sbGRvd24vYmluZGluZy1saW51eC1hcm02NC1tdXNsIjogWyJAcm9sbGRvd24vYmluZGluZy1saW51eC1hcm02NC1tdXNsQDEuMC4xIiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi11d053RnB3S2VOaVphd2ZBV0JnZzBWSXp0UFRWM2loaGgxdlYzMzRoOWl2bk5Mb3J4blFNVTZGejh3RzFaYjRRaDlMQzEvTWtjeVQzWWxEWEczUnNnZz09Il0sCgogICAgIkByb2xsZG93bi9iaW5kaW5nLWxpbnV4LXBwYzY0LWdudSI6IFsiQHJvbGxkb3duL2JpbmRpbmctbGludXgtcHBjNjQtZ251QDEuMC4xIiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJwcGM2NCIgfSwgInNoYTUxMi16WTFidWw3T1dyN0RGQmlKKyt3b2ZYdm5yOEI0NWNlM1FzUVVoS3JJaFhzeWdBaDdiVGt3eWVNMWJpMWEyZzVDL3lDL044VFp5R0RFb01mbS9sOW1wZz09Il0sCgogICAgIkByb2xsZG93bi9iaW5kaW5nLWxpbnV4LXMzOTB4LWdudSI6IFsiQHJvbGxkb3duL2JpbmRpbmctbGludXgtczM5MHgtZ251QDEuMC4xIiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJzMzkweCIgfSwgInNoYTUxMi0wZnJsc1QvZjRGdDZJN1NNRVNUS25GM2Nac2RpY1FuMWRDTWtGL2pUOXdETEUrZ0dvaVFmdjFubVQ5ZStzN3MvZmVrdnZ5NnRaTTJqSHZJMnRrYkpEUT09Il0sCgogICAgIkByb2xsZG93bi9iaW5kaW5nLWxpbnV4LXg2NC1nbnUiOiBbIkByb2xsZG93bi9iaW5kaW5nLWxpbnV4LXg2NC1nbnVAMS4wLjEiLCAiIiwgeyAib3MiOiAibGludXgiLCAiY3B1IjogIng2NCIgfSwgInNoYTUxMi1YQUJWbUdwOVRnMFdzcFRWdndkdVRjNGZwcXk2Sm5BVXJTUWU2T3V5cUQvMDNuSTdyME85T1dVa01Jd0ZyaktBSXFvbHZxb0E0WnJKcHBnd0UwR3htdz09Il0sCgogICAgIkByb2xsZG93bi9iaW5kaW5nLWxpbnV4LXg2NC1tdXNsIjogWyJAcm9sbGRvd24vYmluZGluZy1saW51eC14NjQtbXVzbEAxLjAuMSIsICIiLCB7ICJvcyI6ICJsaW51eCIsICJjcHUiOiAieDY0IiB9LCAic2hhNTEyLWJWNGZ6c3d1elZjS0Q5MG8vVk02UXFLeG54bERxMGcyQklTRExOVm14cm5ocHYxRERieVBoQ0lqWWZ2ellMVitNdmtLS25RdDJRNkFPODZTRUJVTFVRPT0iXSwKCiAgICAiQHJvbGxkb3duL2JpbmRpbmctb3Blbmhhcm1vbnktYXJtNjQiOiBbIkByb2xsZG93bi9iaW5kaW5nLW9wZW5oYXJtb255LWFybTY0QDEuMC4xIiwgIiIsIHsgIm9zIjogIm5vbmUiLCAiY3B1IjogImFybTY0IiB9LCAic2hhNTEyLS9NaDBaaHEzT1A3ZlZzMGtjUUhaUDZsWkV0aE1HVGFTZjhVQlFZU0ZFWkRXR1hYbEVDK25KNkVxZW5hSzJ0NExCWE1lM0ErSy9HMkJWWFhkdE9yNFBRPT0iXSwKCiAgICAiQHJvbGxkb3duL2JpbmRpbmctd2FzbTMyLXdhc2kiOiBbIkByb2xsZG93bi9iaW5kaW5nLXdhc20zMi13YXNpQDEuMC4xIiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIkBlbW5hcGkvY29yZSI6ICIxLjEwLjAiLCAiQGVtbmFwaS9ydW50aW1lIjogIjEuMTAuMCIsICJAbmFwaS1ycy93YXNtLXJ1bnRpbWUiOiAiXjEuMS40IiB9LCAiY3B1IjogIm5vbmUiIH0sICJzaGE1MTItKzF4YzlYNDVsOHVmc0JBbTZHanZ4MnFEUklZOWxUVnQwY2dXTmNKKzFnZGhYdmtieGVQQTYweVJUd1NUdVhMMDlDTWh5Sm1qcFY3RTNOb3l4YnFGUVE9PSJdLAoKICAgICJAcm9sbGRvd24vYmluZGluZy13aW4zMi1hcm02NC1tc3ZjIjogWyJAcm9sbGRvd24vYmluZGluZy13aW4zMi1hcm02NC1tc3ZjQDEuMC4xIiwgIiIsIHsgIm9zIjogIndpbjMyIiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi0xRCtVcVpkZm51UitKeTFHZ01Kd2k4NWJENDBIMjF1Tm1PUFJXUWh3NG9SU3VvbFovQjVyaXhaNDVESzJLWE9UQ3ZtVkNlY2F1V2dFaGJ3OGJJN3RPdz09Il0sCgogICAgIkByb2xsZG93bi9iaW5kaW5nLXdpbjMyLXg2NC1tc3ZjIjogWyJAcm9sbGRvd24vYmluZGluZy13aW4zMi14NjQtbXN2Y0AxLjAuMSIsICIiLCB7ICJvcyI6ICJ3aW4zMiIsICJjcHUiOiAieDY0IiB9LCAic2hhNTEyLUlOQXljYVd1aGxPSzN3azRtUkhHc2Rnd1lXbWQ5Y0NoZFBkRTlid1dteTZybjlWcVZOWU5GR2hPZFhyb2ZYVXh3SEluY1NpUE5iOHRObThrbkRWSWVRPT0iXSwKCiAgICAiQHJvbGxkb3duL3BsdWdpbnV0aWxzIjogWyJAcm9sbGRvd24vcGx1Z2ludXRpbHNAMS4wLjEiLCAiIiwge30sICJzaGE1MTItMmo5Ykd0NUpoOGhqK3ZQdGd6UHRsNzJqMHlSeEhBeXVtb282VE5mQWpzTEIwNFV0cFN2UGJQY0RjQk14ejduKzlDWUIwYzFHeFFGeFlSZzJqaW1xR3c9PSJdLAoKICAgICJAc3RhbmRhcmQtc2NoZW1hL3NwZWMiOiBbIkBzdGFuZGFyZC1zY2hlbWEvc3BlY0AxLjEuMCIsICIiLCB7fSwgInNoYTUxMi1sMmFGeTVqQUxobmlHNUhncXJENmpYTGkvclVXckt2cU4vcUp4NnlvSnNnS2hibFZkK2lxcVU0UkNYYXZtL2pQaXR5RG81VEN2S01ucGpLbk9yaXkwdz09Il0sCgogICAgIkBzd2MvaGVscGVycyI6IFsiQHN3Yy9oZWxwZXJzQDAuNS4xNSIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJ0c2xpYiI6ICJeMi44LjAiIH0gfSwgInNoYTUxMi1KUTVUdU1pNDVPd2k0L0JJTUFKQm9TUW9PSnUxMm9Pay9nQURxbGNVTDlKRWRIQjh2eWpVU3N4cWVOWG5tWEhqWUtNaTJXY1l0ZXpHRUVocVVJL0UyZz09Il0sCgogICAgIkB0YWJieV9haS9oaWpyaS1jb252ZXJ0ZXIiOiBbIkB0YWJieV9haS9oaWpyaS1jb252ZXJ0ZXJAMS4wLjUiLCAiIiwge30sICJzaGE1MTItcjViQ2xLcmNJdXNEb28wNDlkU0w4Q2F3bkhSNm1SZER3aGxRdUlnWlJOdHk2OHEweDhrM0xmMUJ0UEFNeFJmL0dnbkhCbklPNHVqZDMrR1FkTFd6eFE9PSJdLAoKICAgICJAdGFpbHdpbmRjc3Mvbm9kZSI6IFsiQHRhaWx3aW5kY3NzL25vZGVANC4zLjAiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiQGpyaWRnZXdlbGwvcmVtYXBwaW5nIjogIl4yLjMuNSIsICJlbmhhbmNlZC1yZXNvbHZlIjogIl41LjIxLjAiLCAiaml0aSI6ICJeMi42LjEiLCAibGlnaHRuaW5nY3NzIjogIjEuMzIuMCIsICJtYWdpYy1zdHJpbmciOiAiXjAuMzAuMjEiLCAic291cmNlLW1hcC1qcyI6ICJeMS4yLjEiLCAidGFpbHdpbmRjc3MiOiAiNC4zLjAiIH0gfSwgInNoYTUxMi1hRmI0Z1VoRk9nZGg5QVhvNEl6QkVPekJra0F4bTlWaWd3REpuTUlZdjNsY2ZYQ0pWZXNOZmJFYUJsNEJOZ1ZSeWlkOTJBbWR2aXF3QlVCUktTZVkzZz09Il0sCgogICAgIkB0YWlsd2luZGNzcy9veGlkZSI6IFsiQHRhaWx3aW5kY3NzL294aWRlQDQuMy4wIiwgIiIsIHsgIm9wdGlvbmFsRGVwZW5kZW5jaWVzIjogeyAiQHRhaWx3aW5kY3NzL294aWRlLWFuZHJvaWQtYXJtNjQiOiAiNC4zLjAiLCAiQHRhaWx3aW5kY3NzL294aWRlLWRhcndpbi1hcm02NCI6ICI0LjMuMCIsICJAdGFpbHdpbmRjc3Mvb3hpZGUtZGFyd2luLXg2NCI6ICI0LjMuMCIsICJAdGFpbHdpbmRjc3Mvb3hpZGUtZnJlZWJzZC14NjQiOiAiNC4zLjAiLCAiQHRhaWx3aW5kY3NzL294aWRlLWxpbnV4LWFybS1nbnVlYWJpaGYiOiAiNC4zLjAiLCAiQHRhaWx3aW5kY3NzL294aWRlLWxpbnV4LWFybTY0LWdudSI6ICI0LjMuMCIsICJAdGFpbHdpbmRjc3Mvb3hpZGUtbGludXgtYXJtNjQtbXVzbCI6ICI0LjMuMCIsICJAdGFpbHdpbmRjc3Mvb3hpZGUtbGludXgteDY0LWdudSI6ICI0LjMuMCIsICJAdGFpbHdpbmRjc3Mvb3hpZGUtbGludXgteDY0LW11c2wiOiAiNC4zLjAiLCAiQHRhaWx3aW5kY3NzL294aWRlLXdhc20zMi13YXNpIjogIjQuMy4wIiwgIkB0YWlsd2luZGNzcy9veGlkZS13aW4zMi1hcm02NC1tc3ZjIjogIjQuMy4wIiwgIkB0YWlsd2luZGNzcy9veGlkZS13aW4zMi14NjQtbXN2YyI6ICI0LjMuMCIgfSB9LCAic2hhNTEyLUY3SFpHQmVOOUkwL0F1dUpTNVB3Y0Q4eGF5eDVyaTVHaGpZVURCRVZZVWtleHlBL2dpd2JETmpSVnJ4U2V6RTNUMjUwT1UySy93cC9sdFd4M1VPZWZnPT0iXSwKCiAgICAiQHRhaWx3aW5kY3NzL294aWRlLWFuZHJvaWQtYXJtNjQiOiBbIkB0YWlsd2luZGNzcy9veGlkZS1hbmRyb2lkLWFybTY0QDQuMy4wIiwgIiIsIHsgIm9zIjogImFuZHJvaWQiLCAiY3B1IjogImFybTY0IiB9LCAic2hhNTEyLVRKUGlxNjd0S2xMdU9iUDZSa3d2VkdEb3hDTUJWdERnS2tMZmEvdXlqNy9GeXh2UXdIUytVT25WclhYZ2JFc2ZVYU1naVZ2QzRLYkpuUnIyNmhvNE5nPT0iXSwKCiAgICAiQHRhaWx3aW5kY3NzL294aWRlLWRhcndpbi1hcm02NCI6IFsiQHRhaWx3aW5kY3NzL294aWRlLWRhcndpbi1hcm02NEA0LjMuMCIsICIiLCB7ICJvcyI6ICJkYXJ3aW4iLCAiY3B1IjogImFybTY0IiB9LCAic2hhNTEyLW9NTi9XWlJiK1NPMzdCbVVFbEVnZUVXdVU4RS9IWFJraU9EeEp4TGUxVVRIVlhMcmRWU2dmYUpWN3BTbGhSR01TT2lYTHV4VElqZnNGM3dZdno4Y2dRPT0iXSwKCiAgICAiQHRhaWx3aW5kY3NzL294aWRlLWRhcndpbi14NjQiOiBbIkB0YWlsd2luZGNzcy9veGlkZS1kYXJ3aW4teDY0QDQuMy4wIiwgIiIsIHsgIm9zIjogImRhcndpbiIsICJjcHUiOiAieDY0IiB9LCAic2hhNTEyLU42Q1VtdTRhNmJLVkFEZnc3N3AraXc2WWQ5UTNPQmhlMHZlYURYK1FhemZ1VllsUXNIZkRneEJyc2pRL0lXK3p5d0w4bVRyTmQwU2RKVC96Z3R2TWRBPT0iXSwKCiAgICAiQHRhaWx3aW5kY3NzL294aWRlLWZyZWVic2QteDY0IjogWyJAdGFpbHdpbmRjc3Mvb3hpZGUtZnJlZWJzZC14NjRANC4zLjAiLCAiIiwgeyAib3MiOiAiZnJlZWJzZCIsICJjcHUiOiAieDY0IiB9LCAic2hhNTEyLXpETDVoQmtRZEg1QzZNcHFiSzNnUUFnUDgwdHNNd1NJMjZ2ak96akp0TkNNVW8wbEZnT0l0ekhLQkl1cE9aTlF4dDNvdVBIN1JQaHZOaGlUZkNlNUNRPT0iXSwKCiAgICAiQHRhaWx3aW5kY3NzL294aWRlLWxpbnV4LWFybS1nbnVlYWJpaGYiOiBbIkB0YWlsd2luZGNzcy9veGlkZS1saW51eC1hcm0tZ251ZWFiaWhmQDQuMy4wIiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJhcm0iIH0sICJzaGE1MTItUjA2SGROaTdBN09Fb01zZjZkNHRqWjcxUkNXblpRUEhqMm1ub3RTRlVSak5MZEJDK2NJZ1hRN2w4MUNxZW9pUWZ0amY2T09ibHhYTUluTWdOMlZ6TUE9PSJdLAoKICAgICJAdGFpbHdpbmRjc3Mvb3hpZGUtbGludXgtYXJtNjQtZ251IjogWyJAdGFpbHdpbmRjc3Mvb3hpZGUtbGludXgtYXJtNjQtZ251QDQuMy4wIiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi1xVEpIRUxYOGpldGpoUlFIQ0xpbGtWTG15YnB6TlFBdGFJL2dhb1ZvaWRuL3VmYk5EYkFvOEtsSzJKK3lQb2M4d1F4dkR4Q21oLzVscjhuQzErbFRiZz09Il0sCgogICAgIkB0YWlsd2luZGNzcy9veGlkZS1saW51eC1hcm02NC1tdXNsIjogWyJAdGFpbHdpbmRjc3Mvb3hpZGUtbGludXgtYXJtNjQtbXVzbEA0LjMuMCIsICIiLCB7ICJvcyI6ICJsaW51eCIsICJjcHUiOiAiYXJtNjQiIH0sICJzaGE1MTItWjZzdWtpUXNuZ25XTytsMzlYNHBQYmlXVDgxSUMrUExLRitQSHhJbHlaYkdOYjlNT0RmWWxYRVZsRnZlajVCT1pJbldYMDFrVnl6ZUx2SHNYaGZjelE9PSJdLAoKICAgICJAdGFpbHdpbmRjc3Mvb3hpZGUtbGludXgteDY0LWdudSI6IFsiQHRhaWx3aW5kY3NzL294aWRlLWxpbnV4LXg2NC1nbnVANC4zLjAiLCAiIiwgeyAib3MiOiAibGludXgiLCAiY3B1IjogIng2NCIgfSwgInNoYTUxMi1EUk5kUVJwU0d6UkdmQVJWdVZreHZNOFExMm5oMTlsNEJGL0c3ekdBMW9lKzl3Y0M2c2FGQkhUSVNycEljS3poaVh0U3JsU3JsdUNmdk11bGVkb0NUUT09Il0sCgogICAgIkB0YWlsd2luZGNzcy9veGlkZS1saW51eC14NjQtbXVzbCI6IFsiQHRhaWx3aW5kY3NzL294aWRlLWxpbnV4LXg2NC1tdXNsQDQuMy4wIiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJ4NjQiIH0sICJzaGE1MTItWjBJQURiRG84Ymg2STdoMklRTXg2MDFBZFhCTGZGcEVkVW90ZnQ4NmV2ZC84WlBmbFplOUNPUE84UTF2dytwZkxXSVVvOXpOL0pHWnZ3dUFKcWR1cWc9PSJdLAoKICAgICJAdGFpbHdpbmRjc3Mvb3hpZGUtd2FzbTMyLXdhc2kiOiBbIkB0YWlsd2luZGNzcy9veGlkZS13YXNtMzItd2FzaUA0LjMuMCIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAZW1uYXBpL2NvcmUiOiAiXjEuMTAuMCIsICJAZW1uYXBpL3J1bnRpbWUiOiAiXjEuMTAuMCIsICJAZW1uYXBpL3dhc2ktdGhyZWFkcyI6ICJeMS4yLjEiLCAiQG5hcGktcnMvd2FzbS1ydW50aW1lIjogIl4xLjEuNCIsICJAdHlieXMvd2FzbS11dGlsIjogIl4wLjEwLjEiLCAidHNsaWIiOiAiXjIuOC4xIiB9LCAiY3B1IjogIm5vbmUiIH0sICJzaGE1MTItSE5aR09VeEVtRWxrc1lSN1M2c0M1alRlTkdwb2JBc3k5dTdHdTBBc2tKOC8yMEZSOUdxZWJVeUIrSEJjVS9heDZCSHVpdUppK09kYTRCK1lYNkgxeUE9PSJdLAoKICAgICJAdGFpbHdpbmRjc3Mvb3hpZGUtd2luMzItYXJtNjQtbXN2YyI6IFsiQHRhaWx3aW5kY3NzL294aWRlLXdpbjMyLWFybTY0LW1zdmNANC4zLjAiLCAiIiwgeyAib3MiOiAid2luMzIiLCAiY3B1IjogImFybTY0IiB9LCAic2hhNTEyLVBlK1JQVlRpMVQrcXltdXVScGNkdndTVlpqbmxsL2Y3bjhnQnhNTWgzeExUY3RNREtxcGRmR2ltYk15aW9xdExoVVlaeGRKOXdHTmhWN01LSHZnWnNRPT0iXSwKCiAgICAiQHRhaWx3aW5kY3NzL294aWRlLXdpbjMyLXg2NC1tc3ZjIjogWyJAdGFpbHdpbmRjc3Mvb3hpZGUtd2luMzIteDY0LW1zdmNANC4zLjAiLCAiIiwgeyAib3MiOiAid2luMzIiLCAiY3B1IjogIng2NCIgfSwgInNoYTUxMi1NdnJmMmtYVy95ZVcvT1RlelpsQ0dPaXJYUmNVdUxJQngvNVkxMkJhUE03d0pvcnlHNmRmUy9OSkw4YUJQcXRURXgvVm00VDR2S3pGVWNLRFQrVEtVQT09Il0sCgogICAgIkB0YWlsd2luZGNzcy9wb3N0Y3NzIjogWyJAdGFpbHdpbmRjc3MvcG9zdGNzc0A0LjMuMCIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAYWxsb2MvcXVpY2stbHJ1IjogIl41LjIuMCIsICJAdGFpbHdpbmRjc3Mvbm9kZSI6ICI0LjMuMCIsICJAdGFpbHdpbmRjc3Mvb3hpZGUiOiAiNC4zLjAiLCAicG9zdGNzcyI6ICJeOC41LjEwIiwgInRhaWx3aW5kY3NzIjogIjQuMy4wIiB9IH0sICJzaGE1MTItSm0wNVRqeCs5eUNMR3Y1cXcxYys4NFBzZHM4TW55ckVRWUNCK0ZGazJsZ0dpVWpsUnFkeGtlNG1WVHVZcmoyeG5WWnFLaW0yQXByNXlTdVFSWUF3L3c9PSJdLAoKICAgICJAdHlieXMvd2FzbS11dGlsIjogWyJAdHlieXMvd2FzbS11dGlsQDAuMTAuMiIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJ0c2xpYiI6ICJeMi40LjAiIH0gfSwgInNoYTUxMi1Sb0J2SjJYMHd1S2xXRklqcndmZkd3MUlxWkhLUXF6SWNoS2FhZFpaZm5OcHNBWXAybU0waDM2SnRQQ2pOREFIR2dZZXovMTV1TUJwZkd3Y2hoaU1nZz09Il0sCgogICAgIkB0eXBlcy9jaGFpIjogWyJAdHlwZXMvY2hhaUA1LjIuMyIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAdHlwZXMvZGVlcC1lcWwiOiAiKiIsICJhc3NlcnRpb24tZXJyb3IiOiAiXjIuMC4xIiB9IH0sICJzaGE1MTItTXc1NThvZUE5ZkZidjY1L3k0bUh0WERzOWJQbkZNWkFML2p4ZFBGVXBPSEhJWFg5MW1jZ0VIYlM1TGFocitwd1pGUjhBN0dRbGVSV2VJNmNHRkMyVUE9PSJdLAoKICAgICJAdHlwZXMvZGVlcC1lcWwiOiBbIkB0eXBlcy9kZWVwLWVxbEA0LjAuMiIsICIiLCB7fSwgInNoYTUxMi1jOWg5ZFZWTWlnTVBjNGJ3VHZDNWR4cXRxSlp3UVBlUHNXalBscFNPbm9qYm9yNnBHcWRrNTQxbGZBN0FxRlFyNXBCMUJSZHEwanVZOWRiODFCd3lGdz09Il0sCgogICAgIkB0eXBlcy9lc3RyZWUiOiBbIkB0eXBlcy9lc3RyZWVAMS4wLjkiLCAiIiwge30sICJzaGE1MTItR2hkUGd5MWVsNC9JbVAwNVgwNVV3NGN3Mi9NOTNCQ1VtbkV2V1pOU3RsQ3pFS01FNEZraytZcG9BNU9pSE5RbW9TN0NhZmI4WGEzUHlhOG0xUXJ6ZWc9PSJdLAoKICAgICJAdHlwZXMvbm9kZSI6IFsiQHR5cGVzL25vZGVAMjIuMTkuMTkiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAidW5kaWNpLXR5cGVzIjogIn42LjIxLjAiIH0gfSwgInNoYTUxMi1keWgveE8yRmg1YllyZldhYXFHclJRUUdrTmRtWXc2QW1hQVV2WWVVTU5UV1F0dmI3OTZpa0xkbVRjaFJtT2xPaUlKMVREWGZXZ1Z4MVFrVWxRNkhldz09Il0sCgogICAgIkB0eXBlcy9yZWFjdCI6IFsiQHR5cGVzL3JlYWN0QDE5LjIuMTUiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiY3NzdHlwZSI6ICJeMy4yLjIiIH0gfSwgInNoYTUxMi1lUndjR05IdmUrRThxdEVRU1NSbDZ1cmgrckZvcDR2OGdtNk84ckd2MjVDb2RidkZkTGpBMXZWUTFLa2lGRTB3MFVQT25iOHREaUZLTDVscDBydFk1UT09Il0sCgogICAgIkB0eXBlcy9yZWFjdC1kb20iOiBbIkB0eXBlcy9yZWFjdC1kb21AMTkuMi4zIiwgIiIsIHsgInBlZXJEZXBlbmRlbmNpZXMiOiB7ICJAdHlwZXMvcmVhY3QiOiAiXjE5LjIuMCIgfSB9LCAic2hhNTEyLWpwMkwvZVk2Zm4rS2dWVlFBT3FZSXRiRjBWWS9ZQXBlNU16MkYwYXlrU084Z3gzMWJZQ1p5dlNlWXhDSEt2ekhHNWVaamMrenlhUzVCckJXeWEyK2tRPT0iXSwKCiAgICAiQHZpdGVzdC9leHBlY3QiOiBbIkB2aXRlc3QvZXhwZWN0QDQuMS42IiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIkBzdGFuZGFyZC1zY2hlbWEvc3BlYyI6ICJeMS4xLjAiLCAiQHR5cGVzL2NoYWkiOiAiXjUuMi4yIiwgIkB2aXRlc3Qvc3B5IjogIjQuMS42IiwgIkB2aXRlc3QvdXRpbHMiOiAiNC4xLjYiLCAiY2hhaSI6ICJeNi4yLjIiLCAidGlueXJhaW5ib3ciOiAiXjMuMS4wIiB9IH0sICJzaGE1MTItN0VIRHF1UHRoQUxTVjBqaGhqZ0VXOEZYYXZpTXg3clNxdThXNm9xQ29BdU9oS292ODE0UDk5UURWMXB4TUEzUVB2MjFZdWR2Sm5nSWhqck5JNG9wTGc9PSJdLAoKICAgICJAdml0ZXN0L21vY2tlciI6IFsiQHZpdGVzdC9tb2NrZXJANC4xLjYiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiQHZpdGVzdC9zcHkiOiAiNC4xLjYiLCAiZXN0cmVlLXdhbGtlciI6ICJeMy4wLjMiLCAibWFnaWMtc3RyaW5nIjogIl4wLjMwLjIxIiB9LCAicGVlckRlcGVuZGVuY2llcyI6IHsgIm1zdyI6ICJeMi40LjkiLCAidml0ZSI6ICJeNi4wLjAgfHwgXjcuMC4wIHx8IF44LjAuMCIgfSwgIm9wdGlvbmFsUGVlcnMiOiBbIm1zdyIsICJ2aXRlIl0gfSwgInNoYTUxMi1NQ0ZjNjNjek1qRUluT2xjWTJjcFFDdkNOK0tnYkFuKzYweHU5Y01nUDRzS2FMQzVKTkFLdzdKSDhRZEFub0FDODhoVzFJaVNOWitHZ1ZYbE4xVWNNUT09Il0sCgogICAgIkB2aXRlc3QvcHJldHR5LWZvcm1hdCI6IFsiQHZpdGVzdC9wcmV0dHktZm9ybWF0QDQuMS42IiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgInRpbnlyYWluYm93IjogIl4zLjEuMCIgfSB9LCAic2hhNTEyLWg1U3hEL0l6TmhaWW5yU1pSc1VaUUlDK3ZEMEdZOGNVdnEwaXdzbWtGS2l4UkNLTExXcUNYYS9GSVE0UzFSK3NJK1BHb29qa0hzZE5yYlppTTlRcGd3PT0iXSwKCiAgICAiQHZpdGVzdC9ydW5uZXIiOiBbIkB2aXRlc3QvcnVubmVyQDQuMS42IiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIkB2aXRlc3QvdXRpbHMiOiAiNC4xLjYiLCAicGF0aGUiOiAiXjIuMC4zIiB9IH0sICJzaGE1MTItbk9QQ21uMit5RDBaTm1LZHNYR3YvVXhNTVdiTXVLZUQ2R3lZbmNOd2RrWUR4cFF2clBTS1lqMnJXdURqQzJZNGI2dzZoamlwNWRCS0Z6RVV1WmUzdkE9PSJdLAoKICAgICJAdml0ZXN0L3NuYXBzaG90IjogWyJAdml0ZXN0L3NuYXBzaG90QDQuMS42IiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIkB2aXRlc3QvcHJldHR5LWZvcm1hdCI6ICI0LjEuNiIsICJAdml0ZXN0L3V0aWxzIjogIjQuMS42IiwgIm1hZ2ljLXN0cmluZyI6ICJeMC4zMC4yMSIsICJwYXRoZSI6ICJeMi4wLjMiIH0gfSwgInNoYTUxMi1ZaHNkRTZ4QVZmVERtemp4TDJaRFV2amorWnNneU9LZStUZFF6cWtENzJ3SU9tSGthOE51R1E2TnBUTlp2OUQyWjYzZmJ3V0tKUGVWcEV3NEVRZ1l4dz09Il0sCgogICAgIkB2aXRlc3Qvc3B5IjogWyJAdml0ZXN0L3NweUA0LjEuNiIsICIiLCB7fSwgInNoYTUxMi1KRkt4TXg2dWRod0toL0xkbzI3MGUxN1FYNzEwdmd1bk1rdVBBdlhqSFN2QzZvcUxXQUhoVmhqZy9JNzFxMHUwQ0JTRXJJT0RWMUtqdjBGUU5TV2pkZz09Il0sCgogICAgIkB2aXRlc3QvdXRpbHMiOiBbIkB2aXRlc3QvdXRpbHNANC4xLjYiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiQHZpdGVzdC9wcmV0dHktZm9ybWF0IjogIjQuMS42IiwgImNvbnZlcnQtc291cmNlLW1hcCI6ICJeMi4wLjAiLCAidGlueXJhaW5ib3ciOiAiXjMuMS4wIiB9IH0sICJzaGE1MTItRnhJWStVODFSM0xHS0N4YUhIRlJRNStnNi9pUmdHTG1lSFdkcDJBbWo0bGpRUnJFSVdIbVp5RGZEWUJSWmxweXFBN3FLeHRTOUREMWRoazhSblJJVlE9PSJdLAoKICAgICJhbnNpLXJlZ2V4IjogWyJhbnNpLXJlZ2V4QDUuMC4xIiwgIiIsIHt9LCAic2hhNTEyLXF1SlFYbFRTVUdMMkxIOVNVWG84VndzWTRzb2FuaGdvNkxOU204NEUxTEJjRThzM08wd3BkaVJ6eVI5ei9aWkpNbE1XdjM3cU9PYjlwZEpsTVVFS0ZRPT0iXSwKCiAgICAiYW5zaS1zdHlsZXMiOiBbImFuc2ktc3R5bGVzQDQuMy4wIiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgImNvbG9yLWNvbnZlcnQiOiAiXjIuMC4xIiB9IH0sICJzaGE1MTItemJCOXJDSkFUMXJiamlWRGIyaHFLRkhOWUx4Z3RrOE5VUnhaM0lad0QzRjZOdHhiWFpRQ25uU2kxTGt4K0lEb2hkUGxGcDIyMndWQUxJaGVaSlFTRWc9PSJdLAoKICAgICJhc3NlcnRpb24tZXJyb3IiOiBbImFzc2VydGlvbi1lcnJvckAyLjAuMSIsICIiLCB7fSwgInNoYTUxMi1Jemk4UlFjZmZxQ2VOVmdGaWdLbGkxc3NrbElicEhuQ1ljNkFrblhHWW9CNmdySnF5ZWJ5N2p2MTJKVVFnbVRBbklEbmJjazF1eGtzVDRkek4zUFdCQT09Il0sCgogICAgImJhc2VsaW5lLWJyb3dzZXItbWFwcGluZyI6IFsiYmFzZWxpbmUtYnJvd3Nlci1tYXBwaW5nQDIuMTAuMzEiLCAiIiwgeyAiYmluIjogeyAiYmFzZWxpbmUtYnJvd3Nlci1tYXBwaW5nIjogImRpc3QvY2xpLmNqcyIgfSB9LCAic2hhNTEyLU11allPM2VQNzJ1dm1TRTBpNHdsdHNvZFJmSXBaQVRQM2p2elJOUkdHeGd6SWQ3YVZvY1ZKSlYzbmYwMXFuenpLRkd4UVZDOWJwV3hsNWNqeFRyLzdRPT0iXSwKCiAgICAiY2FuaXVzZS1saXRlIjogWyJjYW5pdXNlLWxpdGVAMS4wLjMwMDAxNzkzIiwgIiIsIHt9LCAic2hhNTEyLWl3U3NZV2FDT29oMjZjVjhOd05SVmlIbHJmVXZZc0hEZlJWY2J0bXcwS2c2UEpJWlpYd01rajE0NDJGWUxCR2tlVWYxanVBc1UzRFRmeFc1NzltclBBPT0iXSwKCiAgICAiY2hhaSI6IFsiY2hhaUA2LjIuMiIsICIiLCB7fSwgInNoYTUxMi1OVVBSbHVPZk9pVEtCS3ZXUHRTRDRQaEZ2V0NxT2kwQkdTdE5XczU3WDlqczdYR1RwclNtRm96NUYwdFdoUjRXUGpOZVI5alhxZEM3L1VwU0pUbmxSZz09Il0sCgogICAgImNoYWxrIjogWyJjaGFsa0A0LjEuMiIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJhbnNpLXN0eWxlcyI6ICJeNC4xLjAiLCAic3VwcG9ydHMtY29sb3IiOiAiXjcuMS4wIiB9IH0sICJzaGE1MTItb0tuYmhGeVJJWHBVdWV6OGlCTW15RWE0bmJqNElPUXl1aGMvd3k5a1k3L1dWUGN3SU85VkE2NjhQdThSa083KzBHNzZTTFJPZXl3OUNwUTA2MWk0bUE9PSJdLAoKICAgICJjbGllbnQtb25seSI6IFsiY2xpZW50LW9ubHlAMC4wLjEiLCAiIiwge30sICJzaGE1MTItSVYzT3UwalNNelpyZDNwWjQ4bkxrVDlEQTdBZzFwblB6YWlRaHBXN2MzUmJjcXF6dnp6VnUrTDhnZnFNcC84SU0yTVF0U2lxYUN4cnJjZnU4SThyTUE9PSJdLAoKICAgICJjbGl1aSI6IFsiY2xpdWlAOC4wLjEiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAic3RyaW5nLXdpZHRoIjogIl40LjIuMCIsICJzdHJpcC1hbnNpIjogIl42LjAuMSIsICJ3cmFwLWFuc2kiOiAiXjcuMC4wIiB9IH0sICJzaGE1MTItQlNlTm55dXM3NUM0Ly9OUTlnUXQxL2NzVFh5by84U2IrYWZMQWt6QXB0RnVNc29kOUhGb2tHTnVkWnBpL29RVjczaG5WSytzUis1UFZSTWQrRHI3WVE9PSJdLAoKICAgICJjbHN4IjogWyJjbHN4QDIuMS4xIiwgIiIsIHt9LCAic2hhNTEyLWVZbTBRV0J0VXJCV1pXRzBkMzg2T0dBdzE2Wjk5NVBpT1ZvMkI3YmpXU2JIZWRHbDVlMFpXYXE2NWtPR2dVU05lc0VJRGtCOUlTYlRnL0pLOWRoQ1pBPT0iXSwKCiAgICAiY29sb3ItY29udmVydCI6IFsiY29sb3ItY29udmVydEAyLjAuMSIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJjb2xvci1uYW1lIjogIn4xLjEuNCIgfSB9LCAic2hhNTEyLVJSRUNQc2o3aXUveGI1b0tZY3NGSFNwcEZObnNqLzUyT1ZUUktiNHpQNW9uWHdWRjN6Vm1tVG9OY09mR0MrQ1JEcGZLL1U1ODRmTWczOFpIQ2FFbEtRPT0iXSwKCiAgICAiY29sb3ItbmFtZSI6IFsiY29sb3ItbmFtZUAxLjEuNCIsICIiLCB7fSwgInNoYTUxMi1kT3krM0F1VzNhMndOYlpISXVNWnBUY2dqR3VMVS91QkwvdWJjWkY5T1hiRG84ZmY0Tzh5VnA1QmYwZWZTOHVFb1lvNXE0Rng3ZFk5T2dRR1hnQXNRQT09Il0sCgogICAgImNvbmN1cnJlbnRseSI6IFsiY29uY3VycmVudGx5QDkuMi4xIiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgImNoYWxrIjogIjQuMS4yIiwgInJ4anMiOiAiNy44LjIiLCAic2hlbGwtcXVvdGUiOiAiMS44LjMiLCAic3VwcG9ydHMtY29sb3IiOiAiOC4xLjEiLCAidHJlZS1raWxsIjogIjEuMi4yIiwgInlhcmdzIjogIjE3LjcuMiIgfSwgImJpbiI6IHsgImNvbmMiOiAiZGlzdC9iaW4vY29uY3VycmVudGx5LmpzIiwgImNvbmN1cnJlbnRseSI6ICJkaXN0L2Jpbi9jb25jdXJyZW50bHkuanMiIH0gfSwgInNoYTUxMi1mc2ZyTzBNeFY2NFpub3k4L2wxdlZJampIYTI5U1p5eXFQZ1FCd2hpRGNhVzh3SmMyVzNYV1ZPR3g0TTNvSkJudi96ZFVaSUlwMWdEZVM5OEd6UDhOZz09Il0sCgogICAgImNvbnZlcnQtc291cmNlLW1hcCI6IFsiY29udmVydC1zb3VyY2UtbWFwQDIuMC4wIiwgIiIsIHt9LCAic2hhNTEyLUt2cDQ1OUhyVjJGRUoxQ0FzaTFLdStNWTNrYXNIMTlURnlrVHoyeFdtTWVxNmJrMk5VM1hYdmZKK1E2MW0weGt0V3d0KzFIU1lmM0pac1RtczNhUkpnPT0iXSwKCiAgICAiY3NzdHlwZSI6IFsiY3NzdHlwZUAzLjIuMyIsICIiLCB7fSwgInNoYTUxMi16MUhHS2NZeTJ4QThBR1Fmd3JuMFBBeStQQjdYL0dTajNVVkpXOXFLeW40M3hXYStnbDVuWG1VNHFxTE1SeldWTEZDOEt1c1VYOFQvMGtDaU9ZcEFJUT09Il0sCgogICAgImRhdGUtZm5zIjogWyJkYXRlLWZuc0A0LjIuMSIsICIiLCB7fSwgInNoYTUxMi0zN1JoU2R4YUcxc3VlbjZWREN6YTZyTnJRZm9veVFoNTdIRlZQd1FHRXEyUVdsaVZMelBRWjhPYTAxN3dlT3UrSFpDbnpJN04zUGYvd3lvQktmRXFyQT09Il0sCgogICAgImRhdGUtZm5zLWphbGFsaSI6IFsiZGF0ZS1mbnMtamFsYWxpQDQuMS4wLTAiLCAiIiwge30sICJzaGE1MTItaFRJUC96K3QrcUt3QkRjbW1zbm1qV1RkdXhDZys1S2ZkcVdRdmIyWC84Qzkra25ZWTZlcE4vcGZ4ZER1eVZsU1ZlRnowc001ZUVmd0lVUTcwVTRja2c9PSJdLAoKICAgICJkZXRlY3QtbGliYyI6IFsiZGV0ZWN0LWxpYmNAMi4xLjIiLCAiIiwge30sICJzaGE1MTItQnRqMkJPT084M28zV3lINTllOE1nWHN4RVFWY2Fya1VPcEVZcnViQjB1cnduTjEweVEzNjRyc2lCeVUxMW5abHFXWVptMDVpL29mN2lvNG16aWhCdFE9PSJdLAoKICAgICJlbW9qaS1yZWdleCI6IFsiZW1vamktcmVnZXhAOC4wLjAiLCAiIiwge30sICJzaGE1MTItTVNqWXpjV05PQTBld0FIcHowTXhwWUZ2d2c2eWp5MU5HM3h0ZW9xejY0NFZDby9SUGducjEvR0d0K2ljM2lKVHpROEV1M1RkTTE0U2F3blZVbUdFNkE9PSJdLAoKICAgICJlbmhhbmNlZC1yZXNvbHZlIjogWyJlbmhhbmNlZC1yZXNvbHZlQDUuMjEuNSIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJncmFjZWZ1bC1mcyI6ICJeNC4yLjQiLCAidGFwYWJsZSI6ICJeMi4zLjMiIH0gfSwgInNoYTUxMi1tTENOYnJRbGkxMUsxeVNVbXVOdDRaVUIzT3BHSURxNHEydlRCVGY1Y0wybHBzUmpJOVFLcVNEMG5kalc4Rnl2Y1cvSmo0NmdNZTlzeXlIQXN2TWEvQT09Il0sCgogICAgImVzLW1vZHVsZS1sZXhlciI6IFsiZXMtbW9kdWxlLWxleGVyQDIuMS4wIiwgIiIsIHt9LCAic2hhNTEyLW4yN3pUWU1qWXUxYWo0TWpDV3pTUDdHOXI3NXV0c2FvYzhtNjF3ZUsrVzhKTUJHR1F5YmQ0M0dzdENYWjNXTm1TRnRHVDl3aTU5cVFUVzZtaFRSNUxRPT0iXSwKCiAgICAiZXNjYWxhZGUiOiBbImVzY2FsYWRlQDMuMi4wIiwgIiIsIHt9LCAic2hhNTEyLVdVajJxbHhhUXRPNGc2UHE1YzI5R1RjV0dEeWQ4aXRMOHpUbGlwZ0VDejNKZXNBaWlPS290ZDhKVTZvdEIzUEFDZ0c2eGtKVXlWaGJvTVMrYmplL2pBPT0iXSwKCiAgICAiZXN0cmVlLXdhbGtlciI6IFsiZXN0cmVlLXdhbGtlckAzLjAuMyIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAdHlwZXMvZXN0cmVlIjogIl4xLjAuMCIgfSB9LCAic2hhNTEyLTdSVUtmWGdTTU1renQ2WnVYbXFhcE91ckxHUFBmZ2o2bDl1Ulo3bFJHb2x2azB5MnlvY2MzNUxkY3hLQzVQUVpkbjJETXFpb0FRMk5vV2NyVEttbTZnPT0iXSwKCiAgICAiZXhwZWN0LXR5cGUiOiBbImV4cGVjdC10eXBlQDEuMy4wIiwgIiIsIHt9LCAic2hhNTEyLWtudnllYXVZaHFqT1l2UTY2TXpuU01zODN3bUhyQ3ljTkVONkFvKzJBZVlFZnhVSWt1aVZ4ZEVhMXFsR0VQSytXZTNuMFRIaURjaVlTc0NjZ1cvRG9BPT0iXSwKCiAgICAiZmRpciI6IFsiZmRpckA2LjUuMCIsICIiLCB7ICJwZWVyRGVwZW5kZW5jaWVzIjogeyAicGljb21hdGNoIjogIl4zIHx8IF40IiB9LCAib3B0aW9uYWxQZWVycyI6IFsicGljb21hdGNoIl0gfSwgInNoYTUxMi10SWJZdFpidWNPczBCUkdxUEprc2hKVVlkTCtTREg3ZFZNOGdqeStFUnAzV0FVakxFRkpFKzAya2FueUh0d2pXT253cktZQml3QW1NMHA0a0xKQW5YZz09Il0sCgogICAgImZzZXZlbnRzIjogWyJmc2V2ZW50c0AyLjMuMyIsICIiLCB7ICJvcyI6ICJkYXJ3aW4iIH0sICJzaGE1MTItNXhvRGZYK2ZMN2ZhQVRuYWdtV1BwYkZ0d2gvUjc3V21NTXFxSEdTNjVDM3Z2QjBZSHJnRitCMVltWjM0NDF0TWo1bjYzazAyMTJYTm9Kd3psaGZmUXc9PSJdLAoKICAgICJnZXQtY2FsbGVyLWZpbGUiOiBbImdldC1jYWxsZXItZmlsZUAyLjAuNSIsICIiLCB7fSwgInNoYTUxMi1EeUZQM0JNLzNZSFRRT0NVTC93ME9aSFIwbHBLZUdyeG90Y0hXY3FORWRubHRxRndYVmZoRUJROTRlSW8zNEFmUXBvMHJHa2k0Y3lJaWZ0WTA2aDJGZz09Il0sCgogICAgImdyYWNlZnVsLWZzIjogWyJncmFjZWZ1bC1mc0A0LjIuMTEiLCAiIiwge30sICJzaGE1MTItUmJKNS9qbUZjTk5DY0RWNW85ZVRuQkxKL0hzeldWMFA3M2JjK0ZmNG5TL3JKaitZYVM2SUd5aU9MMFZvQllYK2wxV3JsM2s2M2gvS3JIK25oSjBYdlE9PSJdLAoKICAgICJoYXMtZmxhZyI6IFsiaGFzLWZsYWdANC4wLjAiLCAiIiwge30sICJzaGE1MTItRXlrSlQvUTFLalRXY3RwcGdJQWdmU08wdEtWdVpVamhnTXIxN2txVHVtTWw2QWZ2M0VJU2xlVTdxWlV6b1hERlRBSFREQzROT29HL1p4VTNFdmxNUFE9PSJdLAoKICAgICJpcy1mdWxsd2lkdGgtY29kZS1wb2ludCI6IFsiaXMtZnVsbHdpZHRoLWNvZGUtcG9pbnRAMy4wLjAiLCAiIiwge30sICJzaGE1MTItenltbTUrdStzQ3NTV3lEOXFOYWVqVjNERnZoQ0tjbEtkaXpZYUpVdUhBODNSTGpiN25TdUduZGRDSEd2MGhrK0tZN0JNQWxzV2VLNFVlZzZFVjZYUWc9PSJdLAoKICAgICJqaXRpIjogWyJqaXRpQDIuNy4wIiwgIiIsIHsgImJpbiI6IHsgImppdGkiOiAibGliL2ppdGktY2xpLm1qcyIgfSB9LCAic2hhNTEyLUFDLzdKb2ZKdlpHcnJuZVdOYUVuSmVPTFV4K0psR3Q3dE5hMHdaaVJQVDRNWTF3bWZLanQyKzZPMnAydXoyK3NrbGw4T1pabUpNTnFla2U3a0tiTmdRPT0iXSwKCiAgICAibGlicGhvbmVudW1iZXItanMiOiBbImxpYnBob25lbnVtYmVyLWpzQDEuMTIuNDEiLCAiIiwge30sICJzaGE1MTItbHNtTW1HWEJ4WElLL1ZNTEVqMGtMNk10VXMxa0JHajFuVEN6aTZ6Z1FvRzFERXdxd3QyRFF5SHhjTHlrY2VJeEFuZkUzaHlhN051SWg2UHBDNlMzZkE9PSJdLAoKICAgICJsaWdodG5pbmdjc3MiOiBbImxpZ2h0bmluZ2Nzc0AxLjMyLjAiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiZGV0ZWN0LWxpYmMiOiAiXjIuMC4zIiB9LCAib3B0aW9uYWxEZXBlbmRlbmNpZXMiOiB7ICJsaWdodG5pbmdjc3MtYW5kcm9pZC1hcm02NCI6ICIxLjMyLjAiLCAibGlnaHRuaW5nY3NzLWRhcndpbi1hcm02NCI6ICIxLjMyLjAiLCAibGlnaHRuaW5nY3NzLWRhcndpbi14NjQiOiAiMS4zMi4wIiwgImxpZ2h0bmluZ2Nzcy1mcmVlYnNkLXg2NCI6ICIxLjMyLjAiLCAibGlnaHRuaW5nY3NzLWxpbnV4LWFybS1nbnVlYWJpaGYiOiAiMS4zMi4wIiwgImxpZ2h0bmluZ2Nzcy1saW51eC1hcm02NC1nbnUiOiAiMS4zMi4wIiwgImxpZ2h0bmluZ2Nzcy1saW51eC1hcm02NC1tdXNsIjogIjEuMzIuMCIsICJsaWdodG5pbmdjc3MtbGludXgteDY0LWdudSI6ICIxLjMyLjAiLCAibGlnaHRuaW5nY3NzLWxpbnV4LXg2NC1tdXNsIjogIjEuMzIuMCIsICJsaWdodG5pbmdjc3Mtd2luMzItYXJtNjQtbXN2YyI6ICIxLjMyLjAiLCAibGlnaHRuaW5nY3NzLXdpbjMyLXg2NC1tc3ZjIjogIjEuMzIuMCIgfSB9LCAic2hhNTEyLU5YWUJ6aW5OcmJsZnJhUEd5cmJQb0QxOUMxaDlsZkkvMW16Z1dZdlhVVGU0MTRHei9YMUZEMlhCWlNaTTdyUlRyTUE4SkwzT3RBYUdpZnJJS2hRNXlRPT0iXSwKCiAgICAibGlnaHRuaW5nY3NzLWFuZHJvaWQtYXJtNjQiOiBbImxpZ2h0bmluZ2Nzcy1hbmRyb2lkLWFybTY0QDEuMzIuMCIsICIiLCB7ICJvcyI6ICJhbmRyb2lkIiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi1ZSzcvQ2xUdDRrQUswdm82dzNYK1BubTBEMmNmMnZQSGJoT1hkb050aTFHYTBhbDFQNFRCWmh3akFUdmpOd0xFQkNuS3ZqSmMyalFnSFhIME5Fd2xBZz09Il0sCgogICAgImxpZ2h0bmluZ2Nzcy1kYXJ3aW4tYXJtNjQiOiBbImxpZ2h0bmluZ2Nzcy1kYXJ3aW4tYXJtNjRAMS4zMi4wIiwgIiIsIHsgIm9zIjogImRhcndpbiIsICJjcHUiOiAiYXJtNjQiIH0sICJzaGE1MTItUnplRzlKdTViYWcyQnYxL2x3bFZKdkJFM3E2VHRYc2tkWkxMQ3lmZzVwdCtITHo5QnFsSUNPN0xaTTdWSE5UVG4vNVBSaEhGQlNqazVsYzRjbXNjUFE9PSJdLAoKICAgICJsaWdodG5pbmdjc3MtZGFyd2luLXg2NCI6IFsibGlnaHRuaW5nY3NzLWRhcndpbi14NjRAMS4zMi4wIiwgIiIsIHsgIm9zIjogImRhcndpbiIsICJjcHUiOiAieDY0IiB9LCAic2hhNTEyLVUrUXNCcDJtL3Myd3FwVVlULzZ3bmxhZ2RaYnRaZG5kU211dC9OSnFsQ2NNTFRXcDVtdUNySUQrSzVVSjZqcUQyQkZzaGVqQ1lYbmlQRGJOaDczVjh3PT0iXSwKCiAgICAibGlnaHRuaW5nY3NzLWZyZWVic2QteDY0IjogWyJsaWdodG5pbmdjc3MtZnJlZWJzZC14NjRAMS4zMi4wIiwgIiIsIHsgIm9zIjogImZyZWVic2QiLCAiY3B1IjogIng2NCIgfSwgInNoYTUxMi1KQ1RpZ2VkRWtzWmszdEhUVHRobk1kVmZHZjYxRmt5OEppMkU0WWpVVEVRWDE0eGl5L2xUelhudTF2d2laZTNiWWUwcStTcHNTSC9DVGVEWEs2V0hpZz09Il0sCgogICAgImxpZ2h0bmluZ2Nzcy1saW51eC1hcm0tZ251ZWFiaWhmIjogWyJsaWdodG5pbmdjc3MtbGludXgtYXJtLWdudWVhYmloZkAxLjMyLjAiLCAiIiwgeyAib3MiOiAibGludXgiLCAiY3B1IjogImFybSIgfSwgInNoYTUxMi14NnJubnBSYTJHTDB6UU9rdDZydHMzWURQemR1THBXdndBRjZFTWhYRlZaWEQ0dFByQmtFRnF6R293ekNzSVdzUGpxU0srdHlORU9EVUJYZWVWSFNrdz09Il0sCgogICAgImxpZ2h0bmluZ2Nzcy1saW51eC1hcm02NC1nbnUiOiBbImxpZ2h0bmluZ2Nzcy1saW51eC1hcm02NC1nbnVAMS4zMi4wIiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi0wbm5NeW95T0xSSlhmYk1PaWxhU1JjTEgzSnc1ejlIRE5HZlQvZ3dDUGdhRGpueDBpOHc3dkJ6RkxGUjFmNkNNTEtGOGdWYmVibWtVTjNmYS9rUUpwUT09Il0sCgogICAgImxpZ2h0bmluZ2Nzcy1saW51eC1hcm02NC1tdXNsIjogWyJsaWdodG5pbmdjc3MtbGludXgtYXJtNjQtbXVzbEAxLjMyLjAiLCAiIiwgeyAib3MiOiAibGludXgiLCAiY3B1IjogImFybTY0IiB9LCAic2hhNTEyLVVwUWtvZW5yNFVKRXpnVklZcEk4MGxERnZSbVBWZzZvcWJvTkhmb0g0Q1FJZk5BK0hPclo3TW83S1pQMDJkQzZMamdoUFFKZUJzdlhoSm9kL3duSUJnPT0iXSwKCiAgICAibGlnaHRuaW5nY3NzLWxpbnV4LXg2NC1nbnUiOiBbImxpZ2h0bmluZ2Nzcy1saW51eC14NjQtZ251QDEuMzIuMCIsICIiLCB7ICJvcyI6ICJsaW51eCIsICJjcHUiOiAieDY0IiB9LCAic2hhNTEyLVY3UXI1MkloWm1kS1BWcitWdHc4bytXTHNRSllDVGQ4bG9JZnBEYU1SV0dVWmZCT1lFSmV5SklrcUdJRE1aUHdQeDI0cFVNZndTeHhJOHBoci9NYk9BPT0iXSwKCiAgICAibGlnaHRuaW5nY3NzLWxpbnV4LXg2NC1tdXNsIjogWyJsaWdodG5pbmdjc3MtbGludXgteDY0LW11c2xAMS4zMi4wIiwgIiIsIHsgIm9zIjogImxpbnV4IiwgImNwdSI6ICJ4NjQiIH0sICJzaGE1MTItYlljTHArVmIwYXdzaVhnLzgwdUNSZXpDWUhOZzEvbDNtdDBnekhuV1Y5WFAxVzVzS2E1L1RDZEdXYVIvekJNMlBlRi9IYnNRdi9qMlVSTk9pVnV4V2c9PSJdLAoKICAgICJsaWdodG5pbmdjc3Mtd2luMzItYXJtNjQtbXN2YyI6IFsibGlnaHRuaW5nY3NzLXdpbjMyLWFybTY0LW1zdmNAMS4zMi4wIiwgIiIsIHsgIm9zIjogIndpbjMyIiwgImNwdSI6ICJhcm02NCIgfSwgInNoYTUxMi04U2JDOEJSNDBwUzZiYUNNOHNidFlEU3dFVlFkNEpsRlRPbGFEM2dXR0hmVGhUY0FCbk5EQmRhNmVUWmVxYm9mYWxJSmhGeDBxS3pnSEptY1BUbkdkdz09Il0sCgogICAgImxpZ2h0bmluZ2Nzcy13aW4zMi14NjQtbXN2YyI6IFsibGlnaHRuaW5nY3NzLXdpbjMyLXg2NC1tc3ZjQDEuMzIuMCIsICIiLCB7ICJvcyI6ICJ3aW4zMiIsICJjcHUiOiAieDY0IiB9LCAic2hhNTEyLUFtcTlCL1NvWllkRGkxa0Zyb2pub3FQTHhZaFE0V281WGlMOEVWSnJWc0I4QVJvQzFQV1c2Vkd0VDBXS0NlbWp5OGFDK2xvdUpualM3VTE4eDNiMDZRPT0iXSwKCiAgICAibWFnaWMtc3RyaW5nIjogWyJtYWdpYy1zdHJpbmdAMC4zMC4yMSIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAanJpZGdld2VsbC9zb3VyY2VtYXAtY29kZWMiOiAiXjEuNS41IiB9IH0sICJzaGE1MTItdmQyRjRZVXlFWEtHY0xIb3ErVEV5Q2p4dWVTZUhuRnh5eWpOcDgweWcwWFY0dlVobkRlci9sdnZscU0vYXJCNWJYUU41SzIvM29pbnlDUnl4OFQyQ1E9PSJdLAoKICAgICJuYW5vaWQiOiBbIm5hbm9pZEAzLjMuMTIiLCAiIiwgeyAiYmluIjogeyAibmFub2lkIjogImJpbi9uYW5vaWQuY2pzIiB9IH0sICJzaGE1MTItWkI5UkgvMzlxcHE1VnU2WStObVVhRmhRUjZwcCtNMlh0NzZYQm5Fd0RhR2NWQXFobHZ4cmwzQjJiS1M1RDNOSDNRUjc2djNhU3JLYUYvS2l5N2xFdFE9PSJdLAoKICAgICJuZXh0IjogWyJuZXh0QDE2LjIuNiIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAbmV4dC9lbnYiOiAiMTYuMi42IiwgIkBzd2MvaGVscGVycyI6ICIwLjUuMTUiLCAiYmFzZWxpbmUtYnJvd3Nlci1tYXBwaW5nIjogIl4yLjkuMTkiLCAiY2FuaXVzZS1saXRlIjogIl4xLjAuMzAwMDE1NzkiLCAicG9zdGNzcyI6ICI4LjQuMzEiLCAic3R5bGVkLWpzeCI6ICI1LjEuNiIgfSwgIm9wdGlvbmFsRGVwZW5kZW5jaWVzIjogeyAiQG5leHQvc3djLWRhcndpbi1hcm02NCI6ICIxNi4yLjYiLCAiQG5leHQvc3djLWRhcndpbi14NjQiOiAiMTYuMi42IiwgIkBuZXh0L3N3Yy1saW51eC1hcm02NC1nbnUiOiAiMTYuMi42IiwgIkBuZXh0L3N3Yy1saW51eC1hcm02NC1tdXNsIjogIjE2LjIuNiIsICJAbmV4dC9zd2MtbGludXgteDY0LWdudSI6ICIxNi4yLjYiLCAiQG5leHQvc3djLWxpbnV4LXg2NC1tdXNsIjogIjE2LjIuNiIsICJAbmV4dC9zd2Mtd2luMzItYXJtNjQtbXN2YyI6ICIxNi4yLjYiLCAiQG5leHQvc3djLXdpbjMyLXg2NC1tc3ZjIjogIjE2LjIuNiIsICJzaGFycCI6ICJeMC4zNC41IiB9LCAicGVlckRlcGVuZGVuY2llcyI6IHsgIkBvcGVudGVsZW1ldHJ5L2FwaSI6ICJeMS4xLjAiLCAiQHBsYXl3cmlnaHQvdGVzdCI6ICJeMS41MS4xIiwgImJhYmVsLXBsdWdpbi1yZWFjdC1jb21waWxlciI6ICIqIiwgInJlYWN0IjogIl4xOC4yLjAgfHwgMTkuMC4wLXJjLWRlNjhkMmY0LTIwMjQxMjA0IHx8IF4xOS4wLjAiLCAicmVhY3QtZG9tIjogIl4xOC4yLjAgfHwgMTkuMC4wLXJjLWRlNjhkMmY0LTIwMjQxMjA0IHx8IF4xOS4wLjAiLCAic2FzcyI6ICJeMS4zLjAiIH0sICJvcHRpb25hbFBlZXJzIjogWyJAb3BlbnRlbGVtZXRyeS9hcGkiLCAiQHBsYXl3cmlnaHQvdGVzdCIsICJiYWJlbC1wbHVnaW4tcmVhY3QtY29tcGlsZXIiLCAic2FzcyJdLCAiYmluIjogeyAibmV4dCI6ICJkaXN0L2Jpbi9uZXh0IiB9IH0sICJzaGE1MTItcU9WZ0tKZzErQXQxNU5wZVVQK2VKZ0NIdlRDZ1hzb2d3ZXE4N1JpL0l4N1BrcVFIZzRzZGFYbVNGcUtsZ2FJWEU0a1cwZzI1TEU2OFc4N1VBTmxIdHc9PSJdLAoKICAgICJvYnVnIjogWyJvYnVnQDIuMS4xIiwgIiIsIHt9LCAic2hhNTEyLXVUcUY5TXVQcmFBUStJc25QZjM2NlJHNGNQOVJ0VWk3TUxPMU4zS0VjK3diMGE2eUtwZUwwbG1rMklCMWpZNUtIUEFsVGM2VC9KUmRDL1lxeEhOd2tRPT0iXSwKCiAgICAicGF0aGUiOiBbInBhdGhlQDIuMC4zIiwgIiIsIHt9LCAic2hhNTEyLVdVakdjQXFQMWdRYWNvUWUrT0JKc0ZBN0xkNER5WHVVSWpaNWNjNzVjTEh2SjdkdE5zVHVncGh4SUFEd3NwUytBcmFBVWVQQ0tyU1Z0UExGai9GODh3PT0iXSwKCiAgICAicGljb2NvbG9ycyI6IFsicGljb2NvbG9yc0AxLjEuMSIsICIiLCB7fSwgInNoYTUxMi14Y2VIMnNuaHRiNU05bGlxRHNtRXc1NmxlMzc2bVRaa0VYL2pFYi9SeE5GeWVnTnVsN2VOc2xDWFA5RkRqL0xjdTBYOEtFeU1jZVAybnRwYUhyREVWQT09Il0sCgogICAgInBpY29tYXRjaCI6IFsicGljb21hdGNoQDQuMC40IiwgIiIsIHt9LCAic2hhNTEyLVFQODhCQUt2TWFtLzNOeEg2dmoybzIxUjZNanhaVUFkNm5sd0FTL3BuR3ZOOUlWTG9jTEh4R1lJekZoZzZmVVErNXRoNlA0ZHY0ZVc5algzRFNJajdBPT0iXSwKCiAgICAicG9zdGNzcyI6IFsicG9zdGNzc0A4LjUuMTUiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAibmFub2lkIjogIl4zLjMuMTIiLCAicGljb2NvbG9ycyI6ICJeMS4xLjEiLCAic291cmNlLW1hcC1qcyI6ICJeMS4yLjEiIH0gfSwgInNoYTUxMi1GZlI4c2pkNGVtMlQ2ZmIzSTJNd0FKVTdIV1ZNcjl6YmErZW5tUWVlV0ZmQ2JtK1VPQy8wWDREUzhYdHBVVE13V01HYmpLWVA3eGpmTmVrenlHbUIzQT09Il0sCgogICAgInJlYWN0IjogWyJyZWFjdEAxOS4yLjYiLCAiIiwge30sICJzaGE1MTItc2ZXR0dmYXZpMHhyOFBnMHNWc3lITUFPemlWWUtnUExOclM3aWcraXZNTmIzd2JDQnczS3h0ZmxzR0JBd0QzZ1lRbEUvQUVac1RMZ1RvUnJTQ2piMFE9PSJdLAoKICAgICJyZWFjdC1kYXktcGlja2VyIjogWyJyZWFjdC1kYXktcGlja2VyQDkuMTQuMCIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAZGF0ZS1mbnMvdHoiOiAiXjEuNC4xIiwgIkB0YWJieV9haS9oaWpyaS1jb252ZXJ0ZXIiOiAiMS4wLjUiLCAiZGF0ZS1mbnMiOiAiXjQuMS4wIiwgImRhdGUtZm5zLWphbGFsaSI6ICI0LjEuMC0wIiB9LCAicGVlckRlcGVuZGVuY2llcyI6IHsgInJlYWN0IjogIj49MTYuOC4wIiB9IH0sICJzaGE1MTItdEJhb0RXalB3ZTBNNXBHcnVtNEgwU1I2THlrK0JPOW9IbnA5SmJLcEdLVzJtbHJhTlBnUDlCTWZzZzVwV3B3cnNzQVJtZXFrN1lCbDJvWHV0WlRhSEE9PSJdLAoKICAgICJyZWFjdC1kb20iOiBbInJlYWN0LWRvbUAxOS4yLjYiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAic2NoZWR1bGVyIjogIl4wLjI3LjAiIH0sICJwZWVyRGVwZW5kZW5jaWVzIjogeyAicmVhY3QiOiAiXjE5LjIuNiIgfSB9LCAic2hhNTEyLTBwck1JK2h2QmJQanNXbnhETHhsQ0d5TThQTjZVdVdqRVVDWW1aaE82N3hJVjlYYXNhL3IvdkRucStYeXE0TG8yN2c4UVNiTzVZekFSdTBEMVNwczNnPT0iXSwKCiAgICAicmVxdWlyZS1kaXJlY3RvcnkiOiBbInJlcXVpcmUtZGlyZWN0b3J5QDIuMS4xIiwgIiIsIHt9LCAic2hhNTEyLWZHeEVJNyt3c0c5eHJ2ZGpzcmxtTDIyT01UVGlIUndBTXJvaUVlTWdxOGd6b0xDL1BRcjdSc1JEU1RMVWcvYlpBWnRGK1RWSWtIYzYvNFJJS3J1aStRPT0iXSwKCiAgICAicmVzZWxlY3QiOiBbInJlc2VsZWN0QDUuMi4wIiwgIiIsIHt9LCAic2hhNTEyLUFnWjNVT1ptM1luZGZySjRPWWpnclQ3Ym1DbS8xaXFranZFZkgvb1lqemg2UEQycXc0UXVUM2pqblhJcnBkdDRNVHBNWGNsTVQzbFhibVJZK1hSYWt3PT0iXSwKCiAgICAicm9sbGRvd24iOiBbInJvbGxkb3duQDEuMC4xIiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIkBveGMtcHJvamVjdC90eXBlcyI6ICI9MC4xMzAuMCIsICJAcm9sbGRvd24vcGx1Z2ludXRpbHMiOiAiXjEuMC4wIiB9LCAib3B0aW9uYWxEZXBlbmRlbmNpZXMiOiB7ICJAcm9sbGRvd24vYmluZGluZy1hbmRyb2lkLWFybTY0IjogIjEuMC4xIiwgIkByb2xsZG93bi9iaW5kaW5nLWRhcndpbi1hcm02NCI6ICIxLjAuMSIsICJAcm9sbGRvd24vYmluZGluZy1kYXJ3aW4teDY0IjogIjEuMC4xIiwgIkByb2xsZG93bi9iaW5kaW5nLWZyZWVic2QteDY0IjogIjEuMC4xIiwgIkByb2xsZG93bi9iaW5kaW5nLWxpbnV4LWFybS1nbnVlYWJpaGYiOiAiMS4wLjEiLCAiQHJvbGxkb3duL2JpbmRpbmctbGludXgtYXJtNjQtZ251IjogIjEuMC4xIiwgIkByb2xsZG93bi9iaW5kaW5nLWxpbnV4LWFybTY0LW11c2wiOiAiMS4wLjEiLCAiQHJvbGxkb3duL2JpbmRpbmctbGludXgtcHBjNjQtZ251IjogIjEuMC4xIiwgIkByb2xsZG93bi9iaW5kaW5nLWxpbnV4LXMzOTB4LWdudSI6ICIxLjAuMSIsICJAcm9sbGRvd24vYmluZGluZy1saW51eC14NjQtZ251IjogIjEuMC4xIiwgIkByb2xsZG93bi9iaW5kaW5nLWxpbnV4LXg2NC1tdXNsIjogIjEuMC4xIiwgIkByb2xsZG93bi9iaW5kaW5nLW9wZW5oYXJtb255LWFybTY0IjogIjEuMC4xIiwgIkByb2xsZG93bi9iaW5kaW5nLXdhc20zMi13YXNpIjogIjEuMC4xIiwgIkByb2xsZG93bi9iaW5kaW5nLXdpbjMyLWFybTY0LW1zdmMiOiAiMS4wLjEiLCAiQHJvbGxkb3duL2JpbmRpbmctd2luMzIteDY0LW1zdmMiOiAiMS4wLjEiIH0sICJiaW4iOiB7ICJyb2xsZG93biI6ICJiaW4vY2xpLm1qcyIgfSB9LCAic2hhNTEyLVgwS1FIbGpObkVrV05xcWl6OXpKckd1bmgxQjBIZ094TFh2bkZwQ09jYWR6Y3k1cW9oWjN0cU1FVWcwMHZuY29Sb3ZYdUszWnFDVDlLbm5Lem9JbkZRPT0iXSwKCiAgICAicnhqcyI6IFsicnhqc0A3LjguMiIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJ0c2xpYiI6ICJeMi4xLjAiIH0gfSwgInNoYTUxMi1kaEtmOTAzVS9QUVpZNmJvTk50QUdkV2JHODVXQWJqVC8xeFlvWklDN0ZBWTB5V2FwT0JRVnNWckRsNThXODYvL2UxVnBNTkJ0UlY0TWFYZmRNeVNGQT09Il0sCgogICAgInNjaGVkdWxlciI6IFsic2NoZWR1bGVyQDAuMjcuMCIsICIiLCB7fSwgInNoYTUxMi1lTnYrV3JWYkt1MWYzdmJZSlQveHRpRjVzeUE1SFBJTXRmOUlnWS9uS2cwc1dxekFVRXZxWS94bTdPY1pjL3FhZkx4L2lPOUZnT21lU0FwNHY1dGkvUT09Il0sCgogICAgInNlbXZlciI6IFsic2VtdmVyQDcuOC4wIiwgIiIsIHsgImJpbiI6IHsgInNlbXZlciI6ICJiaW4vc2VtdmVyLmpzIiB9IH0sICJzaGE1MTItQWNNN2RWLzV1bDRFZWtvUTI5QWdtNXZyaThKTnFSeWozOW8wcXBYNnZERjJHWnJ0dXRabDVSd2dEMVhuWmppVEFmbmNzSmhNSTQ4UVFIM3NOODdZTkE9PSJdLAoKICAgICJzaGFycCI6IFsic2hhcnBAMC4zNC41IiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIkBpbWcvY29sb3VyIjogIl4xLjAuMCIsICJkZXRlY3QtbGliYyI6ICJeMi4xLjIiLCAic2VtdmVyIjogIl43LjcuMyIgfSwgIm9wdGlvbmFsRGVwZW5kZW5jaWVzIjogeyAiQGltZy9zaGFycC1kYXJ3aW4tYXJtNjQiOiAiMC4zNC41IiwgIkBpbWcvc2hhcnAtZGFyd2luLXg2NCI6ICIwLjM0LjUiLCAiQGltZy9zaGFycC1saWJ2aXBzLWRhcndpbi1hcm02NCI6ICIxLjIuNCIsICJAaW1nL3NoYXJwLWxpYnZpcHMtZGFyd2luLXg2NCI6ICIxLjIuNCIsICJAaW1nL3NoYXJwLWxpYnZpcHMtbGludXgtYXJtIjogIjEuMi40IiwgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC1hcm02NCI6ICIxLjIuNCIsICJAaW1nL3NoYXJwLWxpYnZpcHMtbGludXgtcHBjNjQiOiAiMS4yLjQiLCAiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4LXJpc2N2NjQiOiAiMS4yLjQiLCAiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4LXMzOTB4IjogIjEuMi40IiwgIkBpbWcvc2hhcnAtbGlidmlwcy1saW51eC14NjQiOiAiMS4yLjQiLCAiQGltZy9zaGFycC1saWJ2aXBzLWxpbnV4bXVzbC1hcm02NCI6ICIxLjIuNCIsICJAaW1nL3NoYXJwLWxpYnZpcHMtbGludXhtdXNsLXg2NCI6ICIxLjIuNCIsICJAaW1nL3NoYXJwLWxpbnV4LWFybSI6ICIwLjM0LjUiLCAiQGltZy9zaGFycC1saW51eC1hcm02NCI6ICIwLjM0LjUiLCAiQGltZy9zaGFycC1saW51eC1wcGM2NCI6ICIwLjM0LjUiLCAiQGltZy9zaGFycC1saW51eC1yaXNjdjY0IjogIjAuMzQuNSIsICJAaW1nL3NoYXJwLWxpbnV4LXMzOTB4IjogIjAuMzQuNSIsICJAaW1nL3NoYXJwLWxpbnV4LXg2NCI6ICIwLjM0LjUiLCAiQGltZy9zaGFycC1saW51eG11c2wtYXJtNjQiOiAiMC4zNC41IiwgIkBpbWcvc2hhcnAtbGludXhtdXNsLXg2NCI6ICIwLjM0LjUiLCAiQGltZy9zaGFycC13YXNtMzIiOiAiMC4zNC41IiwgIkBpbWcvc2hhcnAtd2luMzItYXJtNjQiOiAiMC4zNC41IiwgIkBpbWcvc2hhcnAtd2luMzItaWEzMiI6ICIwLjM0LjUiLCAiQGltZy9zaGFycC13aW4zMi14NjQiOiAiMC4zNC41IiB9IH0sICJzaGE1MTItT3U5STVGdDlXTmNDYlhyVTljTWdQQmNDSzhMaXdMcWNieXdXM3Q0b0RWMzduMXB6cHVOTHNZaUFWOGVPRG5qYnRRbFNEd1oyY1VFZVF6NEU1NEhsdGc9PSJdLAoKICAgICJzaGVsbC1xdW90ZSI6IFsic2hlbGwtcXVvdGVAMS44LjMiLCAiIiwge30sICJzaGE1MTItT2JtbklGNGhYTmcxQnFobkhtZ2JERVRGOGRMUENnZ1pXQmprUWZoWnBic3pabll1cjVEVWxqVGNDSGlpNUxDM0o1RTB5ZU8vMUxJTXlIK1V2SFFneXc9PSJdLAoKICAgICJzaWdpbmZvIjogWyJzaWdpbmZvQDIuMC4wIiwgIiIsIHt9LCAic2hhNTEyLXlieDBXTzEvOGJTQkxFV1hadkVkN2dNVzNTbjNKRmxXM1R2WDFuUkViRExSTlFOYWVOTjhXSzBtZUJ3UGRBYU9JN1R0UlJSSm4vRXMxemhyckNIdTdnPT0iXSwKCiAgICAic291cmNlLW1hcC1qcyI6IFsic291cmNlLW1hcC1qc0AxLjIuMSIsICIiLCB7fSwgInNoYTUxMi1VWFdNS2hMT3dWS2I3MjhJVXRRUFh4ZllVK3VzZHlidFVySy84dUdFOENRTXZyaE9wd3Z6REJ3ajBRaFNMN01RYzd2SXNJU0JHOFZROCtJRFF4cGZRQT09Il0sCgogICAgInN0YWNrYmFjayI6IFsic3RhY2tiYWNrQDAuMC4yIiwgIiIsIHt9LCAic2hhNTEyLTFYTUpFNWZRbzFqR0g2WS83ZWJud1BPQkVrSUVuVDRRRjMyZDVSMStWWGRYdmVNMElCTUp0OHpmYXhYMVAzUWhWd3JZZSs1NzYramtBTnRTUzJtQmJ3PT0iXSwKCiAgICAic3RkLWVudiI6IFsic3RkLWVudkA0LjEuMCIsICIiLCB7fSwgInNoYTUxMi1ScTd5YmNYMlJ1QzU1cjlvYVBWRVc3L3h1M3RqOHU0R2VCWUhCV0N5Y2hGdHpNSXI4NkE3ZTNQUEVCUFQzN3NIU3RLWDMrVGlYL0ZyL0FDbUpMVmxMUT09Il0sCgogICAgInN0cmluZy13aWR0aCI6IFsic3RyaW5nLXdpZHRoQDQuMi4zIiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgImVtb2ppLXJlZ2V4IjogIl44LjAuMCIsICJpcy1mdWxsd2lkdGgtY29kZS1wb2ludCI6ICJeMy4wLjAiLCAic3RyaXAtYW5zaSI6ICJeNi4wLjEiIH0gfSwgInNoYTUxMi13S3lRUlFwakowc0lwNjJFclNaZEdzak1KV3NhcDVvUk5paEhodTZHN0pWTy85aklCNlV5ZXZMK3RYdU9xcm5nOGovY3hLVFd5V1V3dlNUcmlpWnovZz09Il0sCgogICAgInN0cmlwLWFuc2kiOiBbInN0cmlwLWFuc2lANi4wLjEiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiYW5zaS1yZWdleCI6ICJeNS4wLjEiIH0gfSwgInNoYTUxMi1ZMzhWUFNIY3FrRnJDcEZuUTl2dVNYbXF1dXY1b1hPS3BHZVQ2YUdycjNvM0djOUFsVmE2SkJmVVNPQ25ieEdHWkYrLzBvb0k3S3JQdVVTenRVZFU1QT09Il0sCgogICAgInN0eWxlZC1qc3giOiBbInN0eWxlZC1qc3hANS4xLjYiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiY2xpZW50LW9ubHkiOiAiMC4wLjEiIH0sICJwZWVyRGVwZW5kZW5jaWVzIjogeyAicmVhY3QiOiAiPj0gMTYuOC4wIHx8IDE3LngueCB8fCBeMTguMC4wLTAgfHwgXjE5LjAuMC0wIiB9IH0sICJzaGE1MTItcVNWeURUZU1vdGR2UVlvSFdMTkd3UkZKSEMraStadmRCUllvc09GZ0MrV2cxdng0ZnJOMi9SRy9OQTdTWXFxdktOTGYzOVAyTFNSQTJwdTZuMFhZWkE9PSJdLAoKICAgICJzdXBwb3J0cy1jb2xvciI6IFsic3VwcG9ydHMtY29sb3JAOC4xLjEiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiaGFzLWZsYWciOiAiXjQuMC4wIiB9IH0sICJzaGE1MTItTXBVRU4yT29kdFV6eHZLUWw3MmNVRjdSUTVFaUhzR3ZTc1ZHMGlhOWM1UmJXR0wyQ0k0QzdFcFBTOFVUQklwbG5selppTnVWNTZ3K0Z1Tnh5M3R5MlE9PSJdLAoKICAgICJ0YWlsd2luZC1tZXJnZSI6IFsidGFpbHdpbmQtbWVyZ2VAMy42LjAiLCAiIiwge30sICJzaGE1MTItdXhMN3FBVlFyaXFSUVBBeUszcGo2NlZxc2tXcW9aMzdQVzk0andPVHdOZnEvejlveXUxVitlcXJacXRSMitmQ2lYZFlPWmUvTW9kdDhHdHZxTnp1K3c9PSJdLAoKICAgICJ0YWlsd2luZGNzcyI6IFsidGFpbHdpbmRjc3NANC4zLjAiLCAiIiwge30sICJzaGE1MTIteTZueE1HQjFuTVc5UjZrOTZlNWdkSUZ6Y2ZML2dUSlJOYXFHZXMxWXZrTG5QVlh6V2dicUZGMnlMQzBUOEc3NzRuMjRjeDNQZThYcktvbmlDT0FIK1E9PSJdLAoKICAgICJ0YXBhYmxlIjogWyJ0YXBhYmxlQDIuMy4zIiwgIiIsIHt9LCAic2hhNTEyLXV4Yy96cHFGZzZ4N0M4dk9FN2xoNkxiZGE4ZUVMOXptVm0vUExlVFBCUmhoMXhDZ2RXYVErSjFDVWllR3BJZm0ySGR0c1VwUnYrSHNoaWFzQk1jYzZBPT0iXSwKCiAgICAidGlueWJlbmNoIjogWyJ0aW55YmVuY2hAMi45LjAiLCAiIiwge30sICJzaGE1MTItMCtEVXZxV01WYWxMbWhhNmxyNGtEOGlBTUsxSHpWMC9hS25DdFdiOXY5NjQxVG5QL01GYjdQYzJieG94UWpUWEFFcnJ5WFZnVU9mdjJZcU5sbHFHZWc9PSJdLAoKICAgICJ0aW55ZXhlYyI6IFsidGlueWV4ZWNAMS4xLjIiLCAiIiwge30sICJzaGE1MTItZEFxU3FFL1JhYnBCS0k4K2gyNkdmTHE2VmIzSlZYczMwWFlRamRNamFqL2MydFM4SVlZTWJJelA1OTlLdFJqN2M1Ny93WUFwYjNRamdSZ1htckN1a0E9PSJdLAoKICAgICJ0aW55Z2xvYmJ5IjogWyJ0aW55Z2xvYmJ5QDAuMi4xNiIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJmZGlyIjogIl42LjUuMCIsICJwaWNvbWF0Y2giOiAiXjQuMC40IiB9IH0sICJzaGE1MTItcG45OVZob0FDWVI4bkZIaHhxaXgrdXZzYlhpbmVBYXNXbTVvalhvTjh4RXdLNUtkMy9UcmhObjF3Qnl1RDUyVXhXUkx5OHB1K2tSTW5pRWk2RXE5Wmc9PSJdLAoKICAgICJ0aW55cmFpbmJvdyI6IFsidGlueXJhaW5ib3dAMy4xLjAiLCAiIiwge30sICJzaGE1MTItQmYrSUxtQmdyZXRVcmRKeHpYTTBTZ1hMWjNYZmlhVXVPai9JS1FIdVRYaXArMDVYbit1eUVZZFZnMGtZRGlwVEJjTHJDVnlVekFQejdRbUFyYjBtbXc9PSJdLAoKICAgICJ0cmVlLWtpbGwiOiBbInRyZWUta2lsbEAxLjIuMiIsICIiLCB7ICJiaW4iOiB7ICJ0cmVlLWtpbGwiOiAiY2xpLmpzIiB9IH0sICJzaGE1MTItTDBPcnBpOHFHcFJHLy9OZCtIOTB2RkIrM2lIbnVlMXpTU0dtTk9PQ2gxR0xKN3JVS1Z3VjJIdmlqcGhHUVMyVW1oVVpld1M5Vmd2eFlJZGdyK2ZHMUE9PSJdLAoKICAgICJ0c2xpYiI6IFsidHNsaWJAMi44LjEiLCAiIiwge30sICJzaGE1MTItb0pGdTk0SFFiK0tWZHVTVVFMN3ducG1xbmZtTHNPQS9uQWg2YjZFSDB3Q0VvSzAvbVBlWFU2YzN3S0RWODNNa091SFBSSHRTWEtLVTk5SUJhelMvMnc9PSJdLAoKICAgICJ0eXBlc2NyaXB0IjogWyJ0eXBlc2NyaXB0QDUuOS4zIiwgIiIsIHsgImJpbiI6IHsgInRzYyI6ICJiaW4vdHNjIiwgInRzc2VydmVyIjogImJpbi90c3NlcnZlciIgfSB9LCAic2hhNTEyLWpsMXZaelBEaW5McjllVXQzSi90N1Y2RmdORXc5UWp2QlBkeXN6OUtmUURENDFmUXJDMlk0dktRZGlhVXBGVDRiWGxiMVJIaExwcDh3dG02TTVUZ1N3PT0iXSwKCiAgICAidW5kaWNpLXR5cGVzIjogWyJ1bmRpY2ktdHlwZXNANi4yMS4wIiwgIiIsIHt9LCAic2hhNTEyLWl3RFpxZzBRQUdyZzlSYXY1SDRuME02NGMzbWtSNTljSjZ3UXArN0M0bkkwZ3NtRXhhZWRhWUxOTzQ0ZVQ0QXRCQndqYlRpR1BNbHQyTWQwVDlIOUpRPT0iXSwKCiAgICAidXNlLXN5bmMtZXh0ZXJuYWwtc3RvcmUiOiBbInVzZS1zeW5jLWV4dGVybmFsLXN0b3JlQDEuNi4wIiwgIiIsIHsgInBlZXJEZXBlbmRlbmNpZXMiOiB7ICJyZWFjdCI6ICJeMTYuOC4wIHx8IF4xNy4wLjAgfHwgXjE4LjAuMCB8fCBeMTkuMC4wIiB9IH0sICJzaGE1MTItUHA2R1N3R1AvTnJQSXJ4VkZBSWtPUWV5dzhsRmVuT0hpalFXa1VUckR2ckY0QUxxeWxQMkMvS0NrZVM5ZHBVTTNLdllSUWhuYTV2dDdJTDk1K1pROXc9PSJdLAoKICAgICJ2aXRlIjogWyJ2aXRlQDguMC4xMyIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJsaWdodG5pbmdjc3MiOiAiXjEuMzIuMCIsICJwaWNvbWF0Y2giOiAiXjQuMC40IiwgInBvc3Rjc3MiOiAiXjguNS4xNCIsICJyb2xsZG93biI6ICIxLjAuMSIsICJ0aW55Z2xvYmJ5IjogIl4wLjIuMTYiIH0sICJvcHRpb25hbERlcGVuZGVuY2llcyI6IHsgImZzZXZlbnRzIjogIn4yLjMuMyIgfSwgInBlZXJEZXBlbmRlbmNpZXMiOiB7ICJAdHlwZXMvbm9kZSI6ICJeMjAuMTkuMCB8fCA+PTIyLjEyLjAiLCAiQHZpdGVqcy9kZXZ0b29scyI6ICJeMC4xLjE4IiwgImVzYnVpbGQiOiAiXjAuMjcuMCB8fCBeMC4yOC4wIiwgImppdGkiOiAiPj0xLjIxLjAiLCAibGVzcyI6ICJeNC4wLjAiLCAic2FzcyI6ICJeMS43MC4wIiwgInNhc3MtZW1iZWRkZWQiOiAiXjEuNzAuMCIsICJzdHlsdXMiOiAiPj0wLjU0LjgiLCAic3VnYXJzcyI6ICJeNS4wLjAiLCAidGVyc2VyIjogIl41LjE2LjAiLCAidHN4IjogIl40LjguMSIsICJ5YW1sIjogIl4yLjQuMiIgfSwgIm9wdGlvbmFsUGVlcnMiOiBbIkB0eXBlcy9ub2RlIiwgIkB2aXRlanMvZGV2dG9vbHMiLCAiZXNidWlsZCIsICJqaXRpIiwgImxlc3MiLCAic2FzcyIsICJzYXNzLWVtYmVkZGVkIiwgInN0eWx1cyIsICJzdWdhcnNzIiwgInRlcnNlciIsICJ0c3giLCAieWFtbCJdLCAiYmluIjogeyAidml0ZSI6ICJiaW4vdml0ZS5qcyIgfSB9LCAic2hhNTEyLU1GdGpCWWd6bVN4bWdBNFJBZmpJeVhXcEdlMW9BTG5qZ1VUenpWN1FMeC9US3hDemp0TUg2RmQ5L2VWSys1RmcxcU5vejVWQXdzbU1zL05vZnJtSnZ3PT0iXSwKCiAgICAidml0ZXN0IjogWyJ2aXRlc3RANC4xLjYiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiQHZpdGVzdC9leHBlY3QiOiAiNC4xLjYiLCAiQHZpdGVzdC9tb2NrZXIiOiAiNC4xLjYiLCAiQHZpdGVzdC9wcmV0dHktZm9ybWF0IjogIjQuMS42IiwgIkB2aXRlc3QvcnVubmVyIjogIjQuMS42IiwgIkB2aXRlc3Qvc25hcHNob3QiOiAiNC4xLjYiLCAiQHZpdGVzdC9zcHkiOiAiNC4xLjYiLCAiQHZpdGVzdC91dGlscyI6ICI0LjEuNiIsICJlcy1tb2R1bGUtbGV4ZXIiOiAiXjIuMC4wIiwgImV4cGVjdC10eXBlIjogIl4xLjMuMCIsICJtYWdpYy1zdHJpbmciOiAiXjAuMzAuMjEiLCAib2J1ZyI6ICJeMi4xLjEiLCAicGF0aGUiOiAiXjIuMC4zIiwgInBpY29tYXRjaCI6ICJeNC4wLjMiLCAic3RkLWVudiI6ICJeNC4wLjAtcmMuMSIsICJ0aW55YmVuY2giOiAiXjIuOS4wIiwgInRpbnlleGVjIjogIl4xLjAuMiIsICJ0aW55Z2xvYmJ5IjogIl4wLjIuMTUiLCAidGlueXJhaW5ib3ciOiAiXjMuMS4wIiwgInZpdGUiOiAiXjYuMC4wIHx8IF43LjAuMCB8fCBeOC4wLjAiLCAid2h5LWlzLW5vZGUtcnVubmluZyI6ICJeMi4zLjAiIH0sICJwZWVyRGVwZW5kZW5jaWVzIjogeyAiQGVkZ2UtcnVudGltZS92bSI6ICIqIiwgIkBvcGVudGVsZW1ldHJ5L2FwaSI6ICJeMS45LjAiLCAiQHR5cGVzL25vZGUiOiAiXjIwLjAuMCB8fCBeMjIuMC4wIHx8ID49MjQuMC4wIiwgIkB2aXRlc3QvYnJvd3Nlci1wbGF5d3JpZ2h0IjogIjQuMS42IiwgIkB2aXRlc3QvYnJvd3Nlci1wcmV2aWV3IjogIjQuMS42IiwgIkB2aXRlc3QvYnJvd3Nlci13ZWJkcml2ZXJpbyI6ICI0LjEuNiIsICJAdml0ZXN0L2NvdmVyYWdlLWlzdGFuYnVsIjogIjQuMS42IiwgIkB2aXRlc3QvY292ZXJhZ2UtdjgiOiAiNC4xLjYiLCAiQHZpdGVzdC91aSI6ICI0LjEuNiIsICJoYXBweS1kb20iOiAiKiIsICJqc2RvbSI6ICIqIiB9LCAib3B0aW9uYWxQZWVycyI6IFsiQGVkZ2UtcnVudGltZS92bSIsICJAb3BlbnRlbGVtZXRyeS9hcGkiLCAiQHR5cGVzL25vZGUiLCAiQHZpdGVzdC9icm93c2VyLXBsYXl3cmlnaHQiLCAiQHZpdGVzdC9icm93c2VyLXByZXZpZXciLCAiQHZpdGVzdC9icm93c2VyLXdlYmRyaXZlcmlvIiwgIkB2aXRlc3QvY292ZXJhZ2UtaXN0YW5idWwiLCAiQHZpdGVzdC9jb3ZlcmFnZS12OCIsICJAdml0ZXN0L3VpIiwgImhhcHB5LWRvbSIsICJqc2RvbSJdLCAiYmluIjogeyAidml0ZXN0IjogInZpdGVzdC5tanMiIH0gfSwgInNoYTUxMi02bHZqYlMzcDliNENyZENtZ3V6YmgyLzR1b1hoR0UycTcxUjRPWDVzcUY5UjFibzlYZDZmR3JNQWZ2cDV3bkN6bEJuRlZkQ09wNm9udVRRVmJvOGlVUT09Il0sCgogICAgIndoeS1pcy1ub2RlLXJ1bm5pbmciOiBbIndoeS1pcy1ub2RlLXJ1bm5pbmdAMi4zLjAiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAic2lnaW5mbyI6ICJeMi4wLjAiLCAic3RhY2tiYWNrIjogIjAuMC4yIiB9LCAiYmluIjogeyAid2h5LWlzLW5vZGUtcnVubmluZyI6ICJjbGkuanMiIH0gfSwgInNoYTUxMi1oVXJtYVdCZFZEY3h2WXFueWgwOXp1bkt6Uk9XamJaVGlOeThkQkVqa1M3ZWhFRFFpYlhKN1h2bG10Ynd1VGNsVWlJeU4rQ3lYUUQ0Vm1rbzhmTm04dz09Il0sCgogICAgIndyYXAtYW5zaSI6IFsid3JhcC1hbnNpQDcuMC4wIiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgImFuc2ktc3R5bGVzIjogIl40LjAuMCIsICJzdHJpbmctd2lkdGgiOiAiXjQuMS4wIiwgInN0cmlwLWFuc2kiOiAiXjYuMC4wIiB9IH0sICJzaGE1MTItWVZHSWoya2FtTFNUeHc2TnNaam9CeGZTd3NuMHljZGVzbWM0cCtRMjFjNXpQdVoxcGwrTmZ4VmR4UHRkSHZtTlZPUTZYU1lHNEFVdHl0L0ZpN0QxNlE9PSJdLAoKICAgICJ5MThuIjogWyJ5MThuQDUuMC44IiwgIiIsIHt9LCAic2hhNTEyLTBwZkZ6ZWdlRFdKSEpJQW1UTFJQMkR3SGpkRjVzN2pvOXR1enRkUXhBaElOQ2R2UyszbkdJTnFQZDAwQXBocUpSLzBMaEFOVVM2Lys3U0NiOThZT2ZBPT0iXSwKCiAgICAieWFyZ3MiOiBbInlhcmdzQDE3LjcuMiIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJjbGl1aSI6ICJeOC4wLjEiLCAiZXNjYWxhZGUiOiAiXjMuMS4xIiwgImdldC1jYWxsZXItZmlsZSI6ICJeMi4wLjUiLCAicmVxdWlyZS1kaXJlY3RvcnkiOiAiXjIuMS4xIiwgInN0cmluZy13aWR0aCI6ICJeNC4yLjMiLCAieTE4biI6ICJeNS4wLjUiLCAieWFyZ3MtcGFyc2VyIjogIl4yMS4xLjEiIH0gfSwgInNoYTUxMi03ZFN6elJRKytDS25OSS9rcktuWVJWN0pLS1BVWE1FaDYxc29hSEtnOW1yV0VoekZXaEZueFB4R2wrNjljRDFPdTYzQzEzTlVQQ25tSWNydnFDdU02dz09Il0sCgogICAgInlhcmdzLXBhcnNlciI6IFsieWFyZ3MtcGFyc2VyQDIxLjEuMSIsICIiLCB7fSwgInNoYTUxMi10VnBzSlc3RGRqZWNBaUZwYklCMWUzcXhJUXNFNk5vUGM1L2VUZHJiYklDNGgwTFZzV2hub2EzZyttMkhjbEJJdWpIenN4WjRWSlZBK0dVdWMyL0xCdz09Il0sCgogICAgInpvZCI6IFsiem9kQDQuNC4zIiwgIiIsIHt9LCAic2hhNTEyLXl0RU5GaklKRmwyVXdZZ2xkZTJqY2hXMkh3bTRHSkZMRGlTWFdkVHJKUUJJTjlGY3lwN240RGh4SkVpV05BSk1WMS9CcVdmVy9ra2c3MVVEY0hKeVRRPT0iXSwKCiAgICAiQHRhaWx3aW5kY3NzL294aWRlLXdhc20zMi13YXNpL0BlbW5hcGkvY29yZSI6IFsiQGVtbmFwaS9jb3JlQDEuMTAuMCIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJAZW1uYXBpL3dhc2ktdGhyZWFkcyI6ICIxLjIuMSIsICJ0c2xpYiI6ICJeMi40LjAiIH0sICJidW5kbGVkIjogdHJ1ZSB9LCAic2hhNTEyLXlxNk9rSjRwODJDQWZQbDB1OW1RZWJRSEtQSmtZN1dySXVrMjA1Y1RZblllK2syWjhZQmgxMUZyYlJHL0g2aWhpcnFjYWNPZ2wyQklPOG95TVFMZVh3PT0iXSwKCiAgICAiQHRhaWx3aW5kY3NzL294aWRlLXdhc20zMi13YXNpL0BlbW5hcGkvcnVudGltZSI6IFsiQGVtbmFwaS9ydW50aW1lQDEuMTAuMCIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJ0c2xpYiI6ICJeMi40LjAiIH0sICJidW5kbGVkIjogdHJ1ZSB9LCAic2hhNTEyLWV3dllsazg2eFVvR0kwelFSTnEvbUMrMTZSMVFlRGxLUXkyMUtpM29TWVhOZ0xiNDVHVjFQNkEwTSsvczZueUN1TkRxZTVWcGFZODRCelhHd1Zid0ZBPT0iXSwKCiAgICAiQHRhaWx3aW5kY3NzL294aWRlLXdhc20zMi13YXNpL0BlbW5hcGkvd2FzaS10aHJlYWRzIjogWyJAZW1uYXBpL3dhc2ktdGhyZWFkc0AxLjIuMSIsICIiLCB7ICJkZXBlbmRlbmNpZXMiOiB7ICJ0c2xpYiI6ICJeMi40LjAiIH0sICJidW5kbGVkIjogdHJ1ZSB9LCAic2hhNTEyLXVUSUk3T1lGKy9NZXMvTXJjSU9ZcDV5T3RTTUxCV1NJb0xQcGNnd2lwb2lLYmxpNmszMjJ0Y29Gc3hvSUl4UERxVzAxU1FHQWdrbzRFelppMkJOdjJ3PT0iXSwKCiAgICAiQHRhaWx3aW5kY3NzL294aWRlLXdhc20zMi13YXNpL0BuYXBpLXJzL3dhc20tcnVudGltZSI6IFsiQG5hcGktcnMvd2FzbS1ydW50aW1lQDEuMS40IiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIkB0eWJ5cy93YXNtLXV0aWwiOiAiXjAuMTAuMSIgfSwgInBlZXJEZXBlbmRlbmNpZXMiOiB7ICJAZW1uYXBpL2NvcmUiOiAiXjEuNy4xIiwgIkBlbW5hcGkvcnVudGltZSI6ICJeMS43LjEiIH0sICJidW5kbGVkIjogdHJ1ZSB9LCAic2hhNTEyLTNOUU5OZ0ExWVNsSmIva01IMWlsZEFTUDlIVzcvN2tZblJJMnN6V0phb2ZhUzFoV21iR0k0SCtkMysyMmFHelhYTjlJSituK0dpRlZjR2lwSlAxOG93PT0iXSwKCiAgICAiQHRhaWx3aW5kY3NzL294aWRlLXdhc20zMi13YXNpL0B0eWJ5cy93YXNtLXV0aWwiOiBbIkB0eWJ5cy93YXNtLXV0aWxAMC4xMC4yIiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgInRzbGliIjogIl4yLjQuMCIgfSwgImJ1bmRsZWQiOiB0cnVlIH0sICJzaGE1MTItUm9CdkoyWDB3dUtsV0ZJanJ3ZmZHdzFJcVpIS1FxekljaEthYWRaWmZuTnBzQVlwMm1NMGgzNkp0UENqTkRBSEdnWWV6LzE1dU1CcGZHd2NoaGlNZ2c9PSJdLAoKICAgICJAdGFpbHdpbmRjc3Mvb3hpZGUtd2FzbTMyLXdhc2kvdHNsaWIiOiBbInRzbGliQDIuOC4xIiwgIiIsIHsgImJ1bmRsZWQiOiB0cnVlIH0sICJzaGE1MTItb0pGdTk0SFFiK0tWZHVTVVFMN3ducG1xbmZtTHNPQS9uQWg2YjZFSDB3Q0VvSzAvbVBlWFU2YzN3S0RWODNNa091SFBSSHRTWEtLVTk5SUJhelMvMnc9PSJdLAoKICAgICJjaGFsay9zdXBwb3J0cy1jb2xvciI6IFsic3VwcG9ydHMtY29sb3JANy4yLjAiLCAiIiwgeyAiZGVwZW5kZW5jaWVzIjogeyAiaGFzLWZsYWciOiAiXjQuMC4wIiB9IH0sICJzaGE1MTItcXBDQXZSbDlzdHVPSHZlS3NuN0huY0pSdnY1MDFxSWFjS3pRbE8vK0x3eGM5KzBxMndMeXY0RGZ2dDgwL0RQbjJwcU9Cc0pkRGlvZ1hHUjkrT3Z3Unc9PSJdLAoKICAgICJuZXh0L3Bvc3Rjc3MiOiBbInBvc3Rjc3NAOC40LjMxIiwgIiIsIHsgImRlcGVuZGVuY2llcyI6IHsgIm5hbm9pZCI6ICJeMy4zLjYiLCAicGljb2NvbG9ycyI6ICJeMS4wLjAiLCAic291cmNlLW1hcC1qcyI6ICJeMS4wLjIiIH0gfSwgInNoYTUxMi1QUzA4SWJvaWE5bXRzLzJ5Z1YzZUxwWTVnaG5VY2ZMVi9FWFRPVzFFMnFZeEpLR0dCVXROak43NkZZSG5NczM2Um1BUm40MWJDMEFabW4rclIwT1ZwUT09Il0sCiAgfQp9Cg==" }, { "path": ".env.example", "kind": "text", "content": "# Your tenant public key. Get it from the desk's Developers tab.\n# `cpk_live_*` / `cpk_test_*` keys auto-route same-origin requests\n# to hosted Cimplify in both dev and prod. Anything else (`mock-dev`,\n# empty) falls back to the local `cimplify dev` mock.\nNEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev\n\n# Optional. Forces a fixed canonical URL for sitemap.xml, robots.txt,\n# OpenGraph, and llms.txt. Leave unset and the storefront derives the\n# canonical from the request `Host` header automatically.\n# NEXT_PUBLIC_SITE_URL=\n" }] };
5704
5704
  var REGISTRY = { "order-detail-page": { "name": "order-detail-page", "title": "OrderDetailPage", "description": "Single order detail view with live status polling.", "type": "component", "registryDependencies": ["order-summary", "cn"], "files": [{ "path": "order-detail-page.tsx", "content": '"use client";\n\nimport React from "react";\nimport type { Order, LineItem, OrderStatus } from "@cimplify/sdk";\nimport { OrderSummary } from "@cimplify/sdk/react";\nimport { cn } from "@cimplify/sdk/react";\n\nexport interface OrderDetailPageClassNames {\n root?: string;\n header?: string;\n title?: string;\n backButton?: string;\n summary?: string;\n}\n\nexport interface OrderDetailPageProps {\n /** Order ID to display. */\n orderId: string;\n /** Pre-fetched order for SSR. */\n order?: Order;\n /** Poll for status updates. Default: true. */\n poll?: boolean;\n /** Called when back button is clicked. */\n onBack?: () => void;\n /** Custom line item renderer. */\n renderLineItem?: (item: LineItem) => React.ReactNode;\n /** Called when the reorder button is clicked. */\n onReorder?: (order: Order) => void;\n /** Called when the order status changes during polling. */\n onStatusChange?: (previousStatus: OrderStatus, newStatus: OrderStatus, order: Order) => void;\n /** Back button label. */\n backLabel?: string;\n className?: string;\n classNames?: OrderDetailPageClassNames;\n}\n\n/**\n * OrderDetailPage \u2014 single order detail view with live status polling.\n *\n * SSR-friendly: pass `order` prop for server rendering.\n */\nexport function OrderDetailPage({\n orderId,\n order,\n poll = true,\n onBack,\n renderLineItem,\n onReorder,\n onStatusChange,\n backLabel = "Back to orders",\n className,\n classNames,\n}: OrderDetailPageProps): React.ReactElement {\n return (\n <div data-cimplify-order-detail-page className={cn(className, classNames?.root)}>\n {/* Header */}\n <div data-cimplify-order-detail-header className={classNames?.header}>\n {onBack && (\n <button\n type="button"\n onClick={onBack}\n data-cimplify-order-detail-back\n className={classNames?.backButton}\n >\n {backLabel}\n </button>\n )}\n <h1 data-cimplify-order-detail-title className={classNames?.title}>\n Order Details\n </h1>\n </div>\n\n {/* Order summary */}\n <div data-cimplify-order-detail-content className={classNames?.summary}>\n <OrderSummary\n order={order}\n orderId={order ? undefined : orderId}\n poll={poll}\n renderLineItem={renderLineItem}\n onReorder={onReorder}\n onStatusChange={onStatusChange}\n />\n </div>\n </div>\n );\n}\n' }] }, "cart-drawer": { "name": "cart-drawer", "title": "CartDrawer", "description": "Slide-in side cart drawer with provider context, free-shipping progress, animated subtotal, and empty state. Auto-opens on add-to-cart.", "type": "component", "registryDependencies": ["cart-summary", "price", "cn"], "files": [{ "path": "cart-drawer.tsx", "content": '"use client";\n\nimport React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";\nimport { CartSummary } from "./cart-summary";\nimport { Price } from "@cimplify/sdk/react";\nimport { useCart } from "@cimplify/sdk/react";\nimport { parsePrice } from "@cimplify/sdk";\nimport { cn } from "@cimplify/sdk/react";\n\ninterface CartDrawerCtx {\n isOpen: boolean;\n open: () => void;\n close: () => void;\n toggle: () => void;\n}\n\nconst CartDrawerContext = createContext<CartDrawerCtx | null>(null);\n\nexport function useCartDrawer(): CartDrawerCtx {\n const ctx = useContext(CartDrawerContext);\n if (!ctx) {\n throw new Error("useCartDrawer must be used within <CartDrawerProvider>");\n }\n return ctx;\n}\n\ninterface ProviderProps {\n children: React.ReactNode;\n /** Auto-open the drawer whenever the cart\'s pendingOpCount goes from 0 \u2192 >0. Default: true. */\n openOnAdd?: boolean;\n}\n\nexport function CartDrawerProvider({ children, openOnAdd = true }: ProviderProps): React.ReactElement {\n const [isOpen, setIsOpen] = useState(false);\n const cart = useCart();\n const lastPendingRef = useRef(0);\n\n const open = useCallback(() => setIsOpen(true), []);\n const close = useCallback(() => setIsOpen(false), []);\n const toggle = useCallback(() => setIsOpen((v) => !v), []);\n\n useEffect(() => {\n if (!openOnAdd) return;\n if (cart.pendingOpCount > lastPendingRef.current && cart.pendingOpCount > 0) {\n setIsOpen(true);\n }\n lastPendingRef.current = cart.pendingOpCount;\n }, [cart.pendingOpCount, openOnAdd]);\n\n const value = useMemo<CartDrawerCtx>(() => ({ isOpen, open, close, toggle }), [isOpen, open, close, toggle]);\n return <CartDrawerContext.Provider value={value}>{children}</CartDrawerContext.Provider>;\n}\n\nexport interface CartDrawerProps {\n /** Called when "Checkout" is clicked. Drawer auto-closes first. */\n onCheckout?: () => void;\n /** Called when "Continue Shopping" is clicked. Defaults to closing. */\n onContinueShopping?: () => void;\n /** Called when the empty state\'s CTA is clicked. */\n onShop?: () => void;\n /** Heading. */\n title?: string;\n /** Free-shipping threshold (in business currency, numeric). 0 disables the progress bar. */\n freeShippingThreshold?: number;\n /** Custom class on the panel. */\n className?: string;\n}\n\n/**\n * Animate a number toward `target` over ~250ms. Used for subtotal so it\n * feels alive when items are added/removed.\n */\nfunction useAnimatedNumber(target: number, durationMs = 250) {\n const [value, setValue] = useState(target);\n const fromRef = useRef(target);\n const startRef = useRef<number | null>(null);\n useEffect(() => {\n fromRef.current = value;\n startRef.current = null;\n let raf = 0;\n const tick = (t: number) => {\n if (startRef.current == null) startRef.current = t;\n const p = Math.min(1, (t - startRef.current) / durationMs);\n const eased = 1 - Math.pow(1 - p, 3);\n setValue(fromRef.current + (target - fromRef.current) * eased);\n if (p < 1) raf = requestAnimationFrame(tick);\n };\n raf = requestAnimationFrame(tick);\n return () => cancelAnimationFrame(raf);\n // value intentionally omitted \u2014 only re-trigger on target change\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [target, durationMs]);\n return value;\n}\n\nexport function CartDrawer({\n onCheckout,\n onContinueShopping,\n onShop,\n title = "Cart",\n freeShippingThreshold = 0,\n className,\n}: CartDrawerProps): React.ReactElement {\n const { isOpen, close } = useCartDrawer();\n const cart = useCart();\n const subtotalNum = parsePrice(cart.subtotal);\n const animatedSubtotal = useAnimatedNumber(subtotalNum);\n\n // Lock body scroll + close on Escape while open.\n useEffect(() => {\n if (!isOpen) return;\n const original = document.body.style.overflow;\n document.body.style.overflow = "hidden";\n const onKey = (e: KeyboardEvent) => {\n if (e.key === "Escape") close();\n };\n window.addEventListener("keydown", onKey);\n return () => {\n document.body.style.overflow = original;\n window.removeEventListener("keydown", onKey);\n };\n }, [isOpen, close]);\n\n const handleCheckout = () => {\n close();\n onCheckout?.();\n };\n\n const handleContinue = () => {\n onContinueShopping?.();\n close();\n };\n\n const handleShop = () => {\n onShop?.();\n close();\n };\n\n // Free-shipping progress\n const showShippingBar = freeShippingThreshold > 0 && !cart.isEmpty;\n const remainingForShipping = Math.max(0, freeShippingThreshold - subtotalNum);\n const shippingProgress = freeShippingThreshold > 0 ? Math.min(100, (subtotalNum / freeShippingThreshold) * 100) : 0;\n const shippingUnlocked = subtotalNum >= freeShippingThreshold && freeShippingThreshold > 0;\n\n return (\n <div\n data-cimplify-cart-drawer\n data-open={isOpen ? "true" : "false"}\n aria-hidden={!isOpen}\n className={cn(\n "fixed inset-0 z-[200]",\n isOpen ? "pointer-events-auto" : "pointer-events-none",\n )}\n >\n {/* Backdrop */}\n <div\n onClick={close}\n className={cn(\n "absolute inset-0 bg-foreground/40 backdrop-blur-sm transition-opacity duration-300",\n isOpen ? "opacity-100" : "opacity-0",\n )}\n />\n\n {/* Panel */}\n <aside\n role="dialog"\n aria-modal="true"\n aria-label={title}\n className={cn(\n "absolute top-0 right-0 h-full w-full sm:max-w-[480px] bg-background shadow-2xl flex flex-col",\n "transition-transform duration-300",\n isOpen ? "translate-x-0" : "translate-x-full",\n // ease-out cubic-bezier for a "pull" feel\n "[transition-timing-function:cubic-bezier(0.16,1,0.3,1)]",\n className,\n )}\n >\n {/* Header */}\n <header className="relative flex items-center justify-between gap-4 px-6 py-5 shrink-0">\n <div className="flex items-baseline gap-2">\n <h2 className="text-xl font-bold tracking-tight m-0">{title}</h2>\n {cart.itemCount > 0 && (\n <span className="text-sm text-muted-foreground tabular-nums">\n {cart.itemCount} {cart.itemCount === 1 ? "item" : "items"}\n </span>\n )}\n </div>\n <button\n type="button"\n onClick={close}\n aria-label="Close cart"\n className="grid place-items-center w-9 h-9 rounded-full hover:bg-muted text-muted-foreground hover:text-foreground transition-colors"\n >\n <svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24" aria-hidden>\n <path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />\n </svg>\n </button>\n </header>\n\n {/* Free-shipping bar */}\n {showShippingBar && (\n <div data-cimplify-cart-shipping-bar className="px-6 pb-4 shrink-0">\n <div className="flex items-baseline justify-between gap-2 mb-1.5 text-xs">\n {shippingUnlocked ? (\n <span className="font-medium text-primary">\u2713 Free shipping unlocked</span>\n ) : (\n <span className="text-muted-foreground">\n <Price amount={remainingForShipping} className="font-semibold text-foreground" /> away from free shipping\n </span>\n )}\n </div>\n <div className="h-1 rounded-full bg-muted overflow-hidden">\n <div\n className="h-full bg-primary transition-[width] duration-500 ease-out"\n style={{ width: `${shippingProgress}%` }}\n />\n </div>\n </div>\n )}\n\n {/* Items */}\n <div className="flex-1 overflow-y-auto px-6">\n {cart.isEmpty ? (\n <EmptyState onShop={handleShop} />\n ) : (\n <CartSummary showTotals={false} showCheckoutButton={false} />\n )}\n </div>\n\n {/* Footer */}\n {!cart.isEmpty && (\n <footer className="border-t border-border px-6 py-5 shrink-0 space-y-3 bg-background">\n <div className="flex items-baseline justify-between">\n <span className="text-sm text-muted-foreground">Subtotal</span>\n <Price amount={animatedSubtotal} className="text-lg font-bold tabular-nums" />\n </div>\n <p className="text-[11px] text-muted-foreground">\n Tax and shipping calculated at checkout.\n </p>\n <button\n type="button"\n onClick={handleCheckout}\n className="w-full h-12 rounded-full bg-foreground text-background font-semibold text-sm hover:bg-foreground/90 active:scale-[0.99] transition-all"\n >\n Checkout \u2014 <Price amount={cart.subtotal} className="tabular-nums" />\n </button>\n <button\n type="button"\n onClick={handleContinue}\n className="w-full text-xs font-medium text-muted-foreground hover:text-foreground transition-colors"\n >\n Continue shopping\n </button>\n <div className="flex items-center justify-center gap-1.5 text-[10px] text-muted-foreground/70 uppercase tracking-wider pt-1">\n <svg className="w-3 h-3" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24" aria-hidden>\n <path strokeLinecap="round" strokeLinejoin="round" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />\n </svg>\n <span>Secure checkout</span>\n </div>\n </footer>\n )}\n </aside>\n </div>\n );\n}\n\nfunction EmptyState({ onShop }: { onShop: () => void }) {\n return (\n <div data-cimplify-cart-empty className="h-full grid place-items-center text-center py-16">\n <div className="space-y-5 max-w-[260px]">\n <div className="relative w-20 h-20 mx-auto">\n <div className="absolute inset-0 rounded-full bg-muted" />\n <svg\n className="relative w-10 h-10 m-auto top-1/2 -translate-y-1/2 text-muted-foreground/60"\n fill="none"\n stroke="currentColor"\n strokeWidth="1.5"\n viewBox="0 0 24 24"\n aria-hidden\n >\n <path strokeLinecap="round" strokeLinejoin="round" d="M16 11V7a4 4 0 00-8 0v4M5 9h14l-1 12H6L5 9z" />\n </svg>\n </div>\n <div>\n <p className="text-base font-semibold m-0">Your cart is empty</p>\n <p className="text-sm text-muted-foreground mt-1">\n Discover something you\'ll love.\n </p>\n </div>\n <button\n type="button"\n onClick={onShop}\n className="inline-flex items-center gap-1.5 h-11 px-6 rounded-full bg-foreground text-background text-sm font-semibold hover:bg-foreground/90 active:scale-[0.99] transition-all"\n >\n Shop now\n <svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2.5" viewBox="0 0 24 24" aria-hidden>\n <path strokeLinecap="round" strokeLinejoin="round" d="M14 5l7 7m0 0l-7 7m7-7H3" />\n </svg>\n </button>\n </div>\n </div>\n );\n}\n' }] }, "booking-card": { "name": "booking-card", "title": "BookingCard", "description": "Single booking display with status, time, and action buttons.", "type": "component", "registryDependencies": ["price", "cn"], "files": [{ "path": "booking-card.tsx", "content": '"use client";\n\nimport React from "react";\nimport type { CustomerBooking, BookingStatus } from "@cimplify/sdk";\nimport { Price } from "@cimplify/sdk/react";\nimport { cn } from "@cimplify/sdk/react";\n\nexport interface BookingCardClassNames {\n root?: string;\n service?: string;\n status?: string;\n time?: string;\n confirmationCode?: string;\n total?: string;\n actions?: string;\n cancelButton?: string;\n rescheduleButton?: string;\n}\n\nexport interface BookingCardProps {\n /** The booking to display. */\n booking: CustomerBooking;\n /** Called when cancel is clicked. */\n onCancel?: (booking: CustomerBooking) => void;\n /** Called when reschedule is clicked. */\n onReschedule?: (booking: CustomerBooking) => void;\n /** Custom renderer for the booking. */\n renderBooking?: (booking: CustomerBooking) => React.ReactNode;\n className?: string;\n classNames?: BookingCardClassNames;\n}\n\nconst STATUS_LABELS: Record<string, string> = {\n pending: "Pending",\n confirmed: "Confirmed",\n in_progress: "In Progress",\n completed: "Completed",\n cancelled: "Cancelled",\n no_show: "No Show",\n};\n\nfunction isActiveStatus(status: BookingStatus): boolean {\n const s = String(status).toLowerCase();\n return s !== "completed" && s !== "cancelled" && s !== "no_show";\n}\n\nfunction getFirstServiceItem(booking: CustomerBooking) {\n return booking.service_items[0] ?? null;\n}\n\nfunction formatScheduledTime(start?: string | null, end?: string | null): string | null {\n if (!start) return null;\n try {\n const startDate = new Date(start);\n const options: Intl.DateTimeFormatOptions = {\n month: "short",\n day: "numeric",\n hour: "numeric",\n minute: "2-digit",\n };\n let formatted = startDate.toLocaleString(undefined, options);\n if (end) {\n const endDate = new Date(end);\n formatted += ` \u2013 ${endDate.toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit" })}`;\n }\n return formatted;\n } catch {\n return start;\n }\n}\n\nexport function BookingCard({\n booking,\n onCancel,\n onReschedule,\n renderBooking,\n className,\n classNames,\n}: BookingCardProps): React.ReactElement {\n if (renderBooking) {\n return (\n <div\n data-cimplify-booking-card\n data-status={booking.status}\n className={cn(className, classNames?.root)}\n >\n {renderBooking(booking)}\n </div>\n );\n }\n\n const firstItem = getFirstServiceItem(booking);\n const active = isActiveStatus(booking.status);\n const scheduledTime = firstItem\n ? formatScheduledTime(firstItem.scheduled_start, firstItem.scheduled_end)\n : null;\n\n return (\n <div\n data-cimplify-booking-card\n data-status={booking.status}\n className={cn(\n "rounded-xl border border-border bg-background p-4 flex flex-col gap-3",\n className,\n classNames?.root,\n )}\n >\n <div data-cimplify-booking-main className="flex items-center justify-between gap-2">\n <span data-cimplify-booking-service className={cn("font-semibold text-foreground", classNames?.service)}>\n Booking #{booking.order_id.slice(0, 8)}\n </span>\n <span\n data-cimplify-booking-status\n data-status={booking.status}\n className={cn(\n "text-xs font-medium px-2 py-0.5 rounded-full",\n active ? "bg-primary/10 text-primary" : "bg-muted text-muted-foreground",\n classNames?.status,\n )}\n >\n {STATUS_LABELS[booking.status] ?? booking.status}\n </span>\n </div>\n\n {firstItem?.confirmation_code && (\n <span data-cimplify-booking-code className={cn("text-xs font-mono text-muted-foreground tracking-wide", classNames?.confirmationCode)}>\n {firstItem.confirmation_code}\n </span>\n )}\n\n {scheduledTime && (\n <time\n data-cimplify-booking-time\n dateTime={firstItem?.scheduled_start ?? undefined}\n className={cn("text-sm text-foreground", classNames?.time)}\n >\n {scheduledTime}\n </time>\n )}\n\n <span data-cimplify-booking-total className={cn("text-sm font-medium text-foreground", classNames?.total)}>\n <Price amount={booking.total_price} />\n </span>\n\n {active && (onCancel || onReschedule) && (\n <div data-cimplify-booking-actions className={cn("flex gap-2 pt-1 border-t border-border mt-1", classNames?.actions)}>\n {onReschedule && (\n <button\n type="button"\n onClick={() => onReschedule(booking)}\n data-cimplify-booking-reschedule\n className={cn(\n "flex-1 text-sm font-medium py-2 rounded-lg border border-border bg-background text-foreground transition-colors hover:bg-muted cursor-pointer",\n classNames?.rescheduleButton,\n )}\n >\n Reschedule\n </button>\n )}\n {onCancel && (\n <button\n type="button"\n onClick={() => onCancel(booking)}\n data-cimplify-booking-cancel\n className={cn(\n "flex-1 text-sm font-medium py-2 rounded-lg border border-border bg-background text-destructive transition-colors hover:bg-destructive/10 cursor-pointer",\n classNames?.cancelButton,\n )}\n >\n Cancel\n </button>\n )}\n </div>\n )}\n </div>\n );\n}\n' }] }, "order-history": { "name": "order-history", "title": "OrderHistory", "description": "List of past orders with status and totals.", "type": "component", "registryDependencies": ["price", "cn"], "files": [{ "path": "order-history.tsx", "content": '"use client";\n\nimport React from "react";\nimport type { Order, OrderStatus } from "@cimplify/sdk";\nimport { useOrders } from "@cimplify/sdk/react";\nimport { Price } from "@cimplify/sdk/react";\nimport { cn } from "@cimplify/sdk/react";\n\nexport interface OrderHistoryClassNames {\n root?: string;\n item?: string;\n orderId?: string;\n status?: string;\n date?: string;\n total?: string;\n empty?: string;\n loading?: string;\n reorderButton?: string;\n}\n\nexport interface OrderHistoryProps {\n /** Override orders (skips fetch). For SSR, pass pre-fetched orders. */\n orders?: Order[];\n /** Filter by status. */\n status?: OrderStatus;\n /** Max orders to display. */\n limit?: number;\n /** Called when an order row is clicked. */\n onOrderClick?: (order: Order) => void;\n /** Custom order row renderer. */\n renderOrder?: (order: Order) => React.ReactNode;\n /** Called when the reorder button is clicked. If provided, a reorder button is rendered. */\n onReorder?: (order: Order) => void;\n /** Text shown when empty. */\n emptyMessage?: string;\n className?: string;\n classNames?: OrderHistoryClassNames;\n}\n\nconst STATUS_LABELS: Record<OrderStatus, string> = {\n pending: "Pending",\n created: "Created",\n confirmed: "Confirmed",\n in_preparation: "In Preparation",\n ready_to_serve: "Ready",\n partially_served: "Partially Served",\n served: "Served",\n delivered: "Delivered",\n picked_up: "Picked Up",\n completed: "Completed",\n cancelled: "Cancelled",\n refunded: "Refunded",\n};\n\n/**\n * OrderHistory \u2014 list of past orders with status and totals.\n *\n * Fetches via `useOrders` unless pre-loaded orders are passed.\n */\nexport function OrderHistory({\n orders: ordersProp,\n status,\n limit,\n onOrderClick,\n renderOrder,\n onReorder,\n emptyMessage = "No orders yet",\n className,\n classNames,\n}: OrderHistoryProps): React.ReactElement {\n const { orders: fetched, isLoading } = useOrders({\n status,\n limit,\n enabled: ordersProp === undefined,\n });\n\n const orders = ordersProp ?? fetched;\n\n if (isLoading && orders.length === 0) {\n return (\n <div\n data-cimplify-order-history\n aria-busy="true"\n className={cn(className, classNames?.root, classNames?.loading)}\n />\n );\n }\n\n if (orders.length === 0) {\n return (\n <div\n data-cimplify-order-history\n data-empty\n className={cn(className, classNames?.root, classNames?.empty)}\n >\n <p>{emptyMessage}</p>\n </div>\n );\n }\n\n return (\n <div data-cimplify-order-history className={cn(className, classNames?.root)}>\n {orders.map((order) => (\n <button\n key={order.id}\n type="button"\n onClick={() => onOrderClick?.(order)}\n data-cimplify-order-history-item\n data-status={order.status}\n className={classNames?.item}\n >\n {renderOrder ? (\n renderOrder(order)\n ) : (\n <>\n <div data-cimplify-order-history-main>\n <span data-cimplify-order-history-id className={classNames?.orderId}>\n #{order.user_friendly_id}\n </span>\n <span\n data-cimplify-order-history-status\n data-status={order.status}\n className={classNames?.status}\n >\n {STATUS_LABELS[order.status] ?? order.status}\n </span>\n </div>\n <div data-cimplify-order-history-details>\n <time\n data-cimplify-order-history-date\n dateTime={order.created_at}\n className={classNames?.date}\n >\n {new Date(order.created_at).toLocaleDateString()}\n </time>\n <span data-cimplify-order-history-items>\n {order.total_quantity} {order.total_quantity === 1 ? "item" : "items"}\n </span>\n <span data-cimplify-order-history-total className={classNames?.total}>\n <Price amount={order.total_price} />\n </span>\n </div>\n {onReorder && (\n <button\n type="button"\n onClick={(e) => {\n e.stopPropagation();\n onReorder(order);\n }}\n data-cimplify-order-reorder\n className={classNames?.reorderButton}\n >\n Reorder\n </button>\n )}\n </>\n )}\n </button>\n ))}\n </div>\n );\n}\n' }] }, "compact-service-card": { "name": "compact-service-card", "title": "CompactServiceCard", "description": "Horizontal service card with thumbnail for list views.", "type": "component", "registryDependencies": ["price", "cn"], "files": [{ "path": "cards/compact-service-card.tsx", "content": '"use client";\n\nimport React from "react";\nimport type { ServiceCardLayoutProps } from "./standard-service-card";\nimport { Price } from "@cimplify/sdk/react";\nimport { parsePrice } from "@cimplify/sdk";\nimport { cn } from "@cimplify/sdk/react";\n\nfunction formatDuration(minutes: number, unit?: string): string {\n if (unit && unit !== "minutes") return `${minutes} ${unit}`;\n if (minutes >= 60) {\n const h = Math.floor(minutes / 60);\n const m = minutes % 60;\n return m > 0 ? `${h}h ${m}m` : `${h} hr`;\n }\n return `${minutes} min`;\n}\n\nexport function CompactServiceCard({\n product,\n renderImage,\n renderLink,\n className,\n}: ServiceCardLayoutProps): React.ReactElement {\n const image = product.image_url || product.images?.[0] || "";\n const hasDeposit = product.deposit_type && product.deposit_type !== "none" && product.deposit_amount;\n const hasBillingPlans = product.billing_plans && product.billing_plans.length > 0;\n const href = `/products/${product.slug}`;\n\n const content = (\n <div className="flex items-center gap-4 p-3">\n {/* Thumbnail */}\n <div className="w-[72px] h-[72px] rounded-xl overflow-hidden bg-muted shrink-0">\n {renderImage ? (\n renderImage({ src: image, alt: product.name, className: "w-full h-full object-cover" })\n ) : image ? (\n <img src={image} alt={product.name} className="w-full h-full object-cover" />\n ) : null}\n </div>\n\n {/* Info */}\n <div className="flex-1 min-w-0">\n <h3 className="text-[14px] font-semibold text-foreground leading-tight truncate">\n {product.name}\n </h3>\n {product.description && (\n <p className="text-[12px] text-muted-foreground mt-0.5 truncate">\n {product.description}\n </p>\n )}\n <div className="flex items-center gap-2 mt-2 flex-wrap">\n {product.duration_minutes != null && (\n <span className="text-[10.5px] font-medium text-muted-foreground px-1.5 py-0.5 bg-muted rounded">\n {formatDuration(product.duration_minutes, product.duration_unit)}\n </span>\n )}\n <span className="flex items-center gap-1 text-[10.5px] font-medium text-emerald-600">\n <span className="w-[6px] h-[6px] rounded-full bg-emerald-500" />\n Available\n </span>\n {hasDeposit && (\n <span className="text-[10.5px] font-medium text-amber-600">\n <Price amount={product.deposit_amount!} /> deposit\n </span>\n )}\n {hasBillingPlans && (\n <span className="inline-flex items-center gap-0.5 text-[10.5px] font-semibold text-primary px-1.5 py-0.5 bg-primary/10 rounded">\n Subscription\n </span>\n )}\n </div>\n </div>\n\n {/* Price + Chevron */}\n <div className="flex items-center gap-2 shrink-0">\n <Price amount={product.default_price} className="text-sm font-bold" />\n <svg className="w-4 h-4 text-muted-foreground group-hover:text-primary transition-colors" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">\n <path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />\n </svg>\n </div>\n </div>\n );\n\n const shellClass = cn(\n "group block text-left cursor-pointer bg-card rounded-[14px] overflow-hidden",\n "shadow-[0_1px_3px_rgba(0,0,0,0.04),0_6px_24px_rgba(0,0,0,0.06)]",\n "border border-transparent",\n "transition-all duration-300 ease-[cubic-bezier(0.19,1,0.22,1)]",\n "hover:-translate-y-[1px] hover:shadow-[0_2px_6px_rgba(0,0,0,0.04),0_12px_40px_rgba(0,0,0,0.10)] hover:border-primary/20",\n className,\n );\n\n if (renderLink) {\n return renderLink({ href, className: shellClass, children: content });\n }\n\n return <a href={href} className={shellClass} data-cimplify-card>{content}</a>;\n}\n' }] }, "volume-pricing": { "name": "volume-pricing", "title": "VolumePricing", "description": "Collapsible volume pricing tier table.", "type": "component", "registryDependencies": ["price", "cn"], "files": [{ "path": "volume-pricing.tsx", "content": '"use client";\n\nimport React from "react";\nimport type { QuantityPricingTier } from "@cimplify/sdk";\nimport { Price } from "@cimplify/sdk/react";\nimport { cn } from "@cimplify/sdk/react";\n\nexport interface VolumePricingClassNames {\n root?: string;\n trigger?: string;\n triggerIcon?: string;\n panel?: string;\n tier?: string;\n tierActive?: string;\n tierRange?: string;\n tierPrice?: string;\n}\n\nexport interface VolumePricingProps {\n tiers: QuantityPricingTier[];\n currentQuantity?: number;\n defaultOpen?: boolean;\n className?: string;\n classNames?: VolumePricingClassNames;\n}\n\nfunction formatRange(tier: QuantityPricingTier): string {\n if (tier.max_quantity != null) {\n return `${tier.min_quantity}\u2013${tier.max_quantity} units`;\n }\n return `${tier.min_quantity}+ units`;\n}\n\nfunction isActiveTier(tier: QuantityPricingTier, quantity: number): boolean {\n if (quantity < tier.min_quantity) return false;\n if (tier.max_quantity != null && quantity > tier.max_quantity) return false;\n return true;\n}\n\nexport function VolumePricing({\n tiers,\n currentQuantity,\n defaultOpen = false,\n className,\n classNames,\n}: VolumePricingProps): React.ReactElement | null {\n if (tiers.length < 2) return null;\n\n const sorted = [...tiers].sort((a, b) => a.min_quantity - b.min_quantity);\n\n return (\n <details\n open={defaultOpen || undefined}\n data-cimplify-volume-pricing\n className={cn("border border-border", className, classNames?.root)}\n >\n <summary\n data-cimplify-volume-pricing-trigger\n className={cn(\n "flex items-center justify-between px-4 py-3 cursor-pointer select-none text-sm font-medium list-none [&::-webkit-details-marker]:hidden",\n classNames?.trigger,\n )}\n >\n Volume pricing\n <svg\n viewBox="0 0 12 12"\n fill="none"\n aria-hidden="true"\n className={cn(\n "w-3.5 h-3.5 text-muted-foreground transition-transform [[open]>&]:rotate-180",\n classNames?.triggerIcon,\n )}\n >\n <path d="M2 4.5l4 4 4-4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />\n </svg>\n </summary>\n <div\n data-cimplify-volume-pricing-panel\n className={cn("border-t border-border divide-y divide-border", classNames?.panel)}\n >\n {sorted.map((tier, i) => {\n const active = currentQuantity != null && isActiveTier(tier, currentQuantity);\n return (\n <div\n key={i}\n data-cimplify-volume-pricing-tier\n data-active={active || undefined}\n className={cn(\n "flex items-center justify-between px-4 py-2.5 text-sm",\n active\n ? cn("bg-primary/5 font-medium", classNames?.tierActive)\n : classNames?.tier,\n )}\n >\n <span\n data-cimplify-volume-pricing-range\n className={cn("text-muted-foreground", classNames?.tierRange)}\n >\n {formatRange(tier)}\n </span>\n <span data-cimplify-volume-pricing-price className={classNames?.tierPrice}>\n <Price amount={tier.unit_price} prefix="" /> /ea\n </span>\n </div>\n );\n })}\n </div>\n </details>\n );\n}\n' }] }, "delivery-estimate": { "name": "delivery-estimate", "title": "DeliveryEstimate", "description": "Delivery fee + ETA preview at the cart/checkout edge, sourced from /delivery/fee with location.", "type": "component", "registryDependencies": ["price", "cn"], "files": [{ "path": "delivery-estimate.tsx", "content": '"use client";\n\nimport React, { useState, useEffect, useRef } from "react";\nimport type { DeliveryFeeResponse } from "../delivery";\nimport type { CimplifyError } from "@cimplify/sdk";\nimport { Price } from "@cimplify/sdk/react";\nimport { useOptionalCimplifyClient } from "@cimplify/sdk/react";\nimport { cn } from "@cimplify/sdk/react";\n\nexport interface DeliveryEstimateClassNames {\n root?: string;\n fee?: string;\n currency?: string;\n notServiceable?: string;\n loading?: string;\n error?: string;\n}\n\nexport interface DeliveryEstimateProps {\n /** Drop-off latitude. */\n latitude: number;\n /** Drop-off longitude. */\n longitude: number;\n /** Country code for regional fee lookup. */\n country?: string;\n /** Called when the delivery fee is successfully calculated. */\n onFeeCalculated?: (fee: DeliveryFeeResponse) => void;\n className?: string;\n classNames?: DeliveryEstimateClassNames;\n}\n\n/**\n * DeliveryEstimate \u2014 displays a pre-checkout delivery fee estimate.\n *\n * Fetches the delivery fee from `client.delivery.getFee` when coordinates change.\n * Shows fee amount with currency, a "not serviceable" message, or loading state.\n */\nexport function DeliveryEstimate({\n latitude,\n longitude,\n country,\n onFeeCalculated,\n className,\n classNames,\n}: DeliveryEstimateProps): React.ReactElement | null {\n const context = useOptionalCimplifyClient();\n const client = context?.client ?? null;\n\n const [result, setResult] = useState<DeliveryFeeResponse | null>(null);\n const [isLoading, setIsLoading] = useState(true);\n const [error, setError] = useState<CimplifyError | null>(null);\n const requestIdRef = useRef(0);\n\n useEffect(() => {\n if (!client) return;\n\n const nextRequestId = ++requestIdRef.current;\n setIsLoading(true);\n setError(null);\n\n void (async () => {\n const res = await client.delivery.getFee(latitude, longitude, country);\n\n if (nextRequestId !== requestIdRef.current) return;\n\n if (!res.ok) {\n setError(res.error);\n setResult(null);\n setIsLoading(false);\n return;\n }\n\n setResult(res.value);\n setError(null);\n setIsLoading(false);\n onFeeCalculated?.(res.value);\n })();\n }, [client, latitude, longitude, country]); // eslint-disable-line react-hooks/exhaustive-deps -- onFeeCalculated is a callback prop, not a reactive dependency\n\n if (!client) {\n return null;\n }\n\n if (isLoading) {\n return (\n <div\n data-cimplify-delivery-estimate\n data-serviceable="false"\n aria-busy="true"\n className={cn(className, classNames?.root, classNames?.loading)}\n />\n );\n }\n\n if (error) {\n return (\n <div\n data-cimplify-delivery-estimate\n data-serviceable="false"\n className={cn(className, classNames?.root, classNames?.error)}\n >\n <span>{error.message}</span>\n </div>\n );\n }\n\n if (!result || !result.serviceable) {\n return (\n <div\n data-cimplify-delivery-estimate\n data-serviceable="false"\n className={cn(className, classNames?.root, classNames?.notServiceable)}\n >\n <span>Not serviceable</span>\n </div>\n );\n }\n\n return (\n <div\n data-cimplify-delivery-estimate\n data-serviceable="true"\n className={cn(className, classNames?.root)}\n >\n {result.fee != null && (\n <span data-cimplify-delivery-fee className={classNames?.fee}>\n <Price amount={result.fee} />\n </span>\n )}\n {result.currency && (\n <span data-cimplify-delivery-currency className={classNames?.currency}>\n {result.currency}\n </span>\n )}\n </div>\n );\n}\n' }] }, "retail-product-card": { "name": "retail-product-card", "title": "RetailProductCard", "description": "Product card for retail with color swatches, sale badge, wishlist, and sold-out overlay.", "type": "component", "registryDependencies": ["price", "cn"], "files": [{ "path": "cards/retail-product-card.tsx", "content": '"use client";\n\nimport React from "react";\nimport type { Product, ProductWithDetails } from "@cimplify/sdk";\nimport type { CardLayoutProps } from "@cimplify/sdk/react";\nimport { CardShell, CardImage, WishlistButton, SoldOutOverlay, SaleBadge, LowStockBadge } from "@cimplify/sdk/react";\nimport { Price } from "@cimplify/sdk/react";\nimport { parsePrice, isOnSale, getBasePrice, getDiscountPercentage } from "@cimplify/sdk";\nimport { cn } from "@cimplify/sdk/react";\n\nfunction getColorSwatches(product: Product): string[] {\n const variants = (product as ProductWithDetails).variants;\n if (!variants || variants.length === 0) return [];\n\n const seen = new Set<string>();\n for (const v of variants) {\n if (v.display_attributes) {\n for (const attr of v.display_attributes) {\n if (attr.axis_name.toLowerCase() === "color" && !seen.has(attr.value_name)) {\n seen.add(attr.value_name);\n }\n }\n }\n }\n\n return Array.from(seen).slice(0, 5);\n}\n\nexport function RetailProductCard({\n product,\n renderImage,\n renderLink,\n className,\n}: CardLayoutProps): React.ReactElement {\n const image = product.image_url || product.images?.[0] || "";\n const outOfStock = product.inventory_status?.in_stock === false;\n const onSale = isOnSale(product);\n const swatches = getColorSwatches(product);\n\n return (\n <CardShell product={product} renderLink={renderLink} disabled={outOfStock} className={className}>\n {outOfStock && <SoldOutOverlay />}\n\n <CardImage src={image} alt={product.name} aspectRatio="3/4" renderImage={renderImage}>\n {/* Sale badge */}\n {onSale && (\n <div className="absolute top-3 left-3">\n <SaleBadge product={product} />\n </div>\n )}\n\n {/* Wishlist */}\n <WishlistButton className="absolute top-3 right-3" />\n\n {/* Low stock */}\n {product.inventory_status?.low_stock && product.inventory_status?.stock_level && (\n <span className="absolute bottom-3 left-3 text-[11px] font-semibold bg-amber-50 text-amber-700 border border-amber-200/60 px-2 py-0.5 rounded-md">\n Only {product.inventory_status.stock_level} left\n </span>\n )}\n </CardImage>\n\n <div className="p-3.5">\n {/* Brand */}\n {product.vendor && (\n <p className="text-[10px] font-semibold text-muted-foreground uppercase tracking-widest">\n {product.vendor}\n </p>\n )}\n\n {/* Name */}\n <h3 className="text-[14.5px] font-semibold text-foreground leading-snug tracking-tight mt-1 truncate">\n {product.name}\n </h3>\n\n {/* Price */}\n <div className="flex items-center gap-2 mt-2">\n <Price amount={product.default_price} className="text-sm font-bold" />\n {onSale && (\n <Price amount={getBasePrice(product)} className="text-xs text-muted-foreground line-through" />\n )}\n </div>\n\n {/* Color swatches */}\n {swatches.length > 0 && (\n <div className="flex gap-1.5 mt-2.5">\n {swatches.map((name) => (\n <span\n key={name}\n title={name}\n className="text-[9px] font-medium text-muted-foreground px-1.5 py-0.5 bg-muted rounded"\n >\n {name}\n </span>\n ))}\n </div>\n )}\n </div>\n </CardShell>\n );\n}\n' }] }, "collection-page": { "name": "collection-page", "title": "CollectionPage", "description": "Curated product collection with header and grid.", "type": "component", "registryDependencies": ["product-grid", "cn"], "files": [{ "path": "collection-page.tsx", "content": '"use client";\n\nimport React from "react";\nimport type { Product, Collection } from "@cimplify/sdk";\nimport { useCollection } from "@cimplify/sdk/react";\nimport type { CollectionLayoutProps } from "./collection-layouts/shared";\nimport { DefaultCollectionLayout } from "./collection-layouts/default-collection-layout";\nimport { FeaturedCollectionLayout } from "./collection-layouts/featured-collection-layout";\nimport { CatalogueCollectionLayout } from "./collection-layouts/catalogue-collection-layout";\nimport { cn } from "@cimplify/sdk/react";\n\nexport type { CollectionLayoutProps };\n\nexport enum CollectionTemplate {\n Default = "default",\n Featured = "featured",\n Catalogue = "catalogue",\n}\n\nexport interface CollectionPageClassNames {\n root?: string;\n loading?: string;\n}\n\nexport interface CollectionPageProps {\n /** Collection slug or ID. */\n collectionId?: string;\n /** Pre-fetched collection for SSR. */\n collection?: Collection;\n /** Pre-fetched products for SSR. */\n products?: Product[];\n /** Per-slug page map. Highest priority \u2014 maps a collection slug to a custom layout. */\n pages?: Record<string, React.ComponentType<CollectionLayoutProps>>;\n /** Per-type template map. Overrides built-in layouts. */\n templates?: Partial<Record<CollectionTemplate | string, React.ComponentType<CollectionLayoutProps>>>;\n /** Custom card renderer. */\n renderCard?: (product: Product) => React.ReactNode;\n /** Custom image renderer. */\n renderImage?: (props: { src: string; alt: string; className?: string }) => React.ReactNode;\n /** Custom link renderer. */\n renderLink?: (props: { href: string; className?: string; children: React.ReactNode }) => React.ReactElement;\n /** Quick add handler for cards. */\n onQuickAdd?: (product: Product) => void;\n className?: string;\n classNames?: CollectionPageClassNames;\n}\n\nconst BUILT_IN_LAYOUTS: Record<string, React.ComponentType<CollectionLayoutProps>> = {\n [CollectionTemplate.Default]: DefaultCollectionLayout,\n [CollectionTemplate.Featured]: FeaturedCollectionLayout,\n [CollectionTemplate.Catalogue]: CatalogueCollectionLayout,\n};\n\nconst LARGE_COLLECTION_THRESHOLD = 30;\n\nfunction resolveTemplateKey(collection: Collection, productCount: number): CollectionTemplate | string {\n const metaTemplate = collection.metadata?.page_template;\n if (typeof metaTemplate === "string" && metaTemplate.trim()) {\n return metaTemplate.trim();\n }\n\n if (collection.tags?.includes("featured") || collection.metadata?.is_featured === true) {\n return CollectionTemplate.Featured;\n }\n\n if (productCount > LARGE_COLLECTION_THRESHOLD) {\n return CollectionTemplate.Catalogue;\n }\n\n return CollectionTemplate.Default;\n}\n\nfunction resolveLayout(\n collection: Collection,\n productCount: number,\n pages?: Record<string, React.ComponentType<CollectionLayoutProps>>,\n templates?: Partial<Record<string, React.ComponentType<CollectionLayoutProps>>>,\n): React.ComponentType<CollectionLayoutProps> {\n if (pages?.[collection.slug]) {\n return pages[collection.slug];\n }\n\n const key = resolveTemplateKey(collection, productCount);\n\n if (templates?.[key]) {\n return templates[key]!;\n }\n\n if (BUILT_IN_LAYOUTS[key]) {\n return BUILT_IN_LAYOUTS[key];\n }\n\n return DefaultCollectionLayout;\n}\n\nexport function CollectionPage({\n collectionId,\n collection: collectionProp,\n products: productsProp,\n pages,\n templates,\n renderCard,\n renderImage,\n renderLink,\n onQuickAdd,\n className,\n classNames,\n}: CollectionPageProps): React.ReactElement {\n const resolvedId = collectionId || collectionProp?.slug || collectionProp?.id || "";\n const {\n collection: fetchedCollection,\n products: fetchedProducts,\n isLoading,\n } = useCollection(collectionProp ? null : resolvedId, {\n enabled: !collectionProp && resolvedId.length > 0,\n });\n\n const collection = collectionProp ?? fetchedCollection;\n const products = productsProp ?? fetchedProducts;\n\n if (isLoading && !collection) {\n return (\n <div\n data-cimplify-collection-page\n aria-busy="true"\n className={cn(className, classNames?.root, classNames?.loading)}\n >\n <div className="animate-pulse space-y-6">\n <div className="h-64 lg:h-80 bg-muted rounded-2xl" />\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">\n {Array.from({ length: 8 }).map((_, i) => (\n <div key={i}>\n <div className="aspect-square bg-muted rounded-[14px]" />\n <div className="mt-3 space-y-2">\n <div className="h-4 w-3/4 bg-muted rounded" />\n <div className="h-3 w-1/2 bg-muted rounded" />\n </div>\n </div>\n ))}\n </div>\n </div>\n </div>\n );\n }\n\n if (!collection) {\n return (\n <div data-cimplify-collection-page className={cn(className, classNames?.root)}>\n <p className="text-muted-foreground">Collection not found.</p>\n </div>\n );\n }\n\n const Layout = resolveLayout(collection, products.length, pages, templates);\n\n return (\n <div data-cimplify-collection-page className={cn(className, classNames?.root)}>\n <Layout\n collection={collection}\n products={products}\n renderCard={renderCard}\n renderImage={renderImage}\n renderLink={renderLink}\n onQuickAdd={onQuickAdd}\n />\n </div>\n );\n}\n' }] }, "deals-page": { "name": "deals-page", "title": "DealsPage", "description": "Promotions landing page with deal banners and on-sale products.", "type": "component", "registryDependencies": ["deal-banner", "product-grid", "sale-badge", "product-card", "cn"], "files": [{ "path": "deals-page.tsx", "content": '"use client";\n\nimport React from "react";\nimport type { Product, Deal, ProductDealInfo } from "@cimplify/sdk";\nimport { useDeals, useProductsOnSale } from "@cimplify/sdk/react";\nimport { DealBanner } from "@cimplify/sdk/react";\nimport { ProductGrid } from "./product-grid";\nimport { SaleBadge } from "./sale-badge";\nimport { ProductCard } from "@cimplify/sdk/react";\nimport { cn } from "@cimplify/sdk/react";\n\nexport interface DealsPageClassNames {\n root?: string;\n header?: string;\n title?: string;\n bannerSection?: string;\n productsSection?: string;\n productsTitle?: string;\n empty?: string;\n loading?: string;\n}\n\nexport interface DealsPageProps {\n /** Page title. */\n title?: string;\n /** Pre-fetched deals for SSR. */\n deals?: Deal[];\n /** Pre-fetched products on sale for SSR. */\n productsOnSale?: ProductDealInfo[];\n /** Pre-fetched product objects for rendering. */\n products?: Product[];\n /** Location ID for location-specific deals. */\n locationId?: string;\n /** Custom image renderer. */\n renderImage?: (props: { src: string; alt: string; className?: string }) => React.ReactNode;\n /** Grid column config. */\n columns?: { sm?: number; md?: number; lg?: number; xl?: number };\n className?: string;\n classNames?: DealsPageClassNames;\n}\n\n/**\n * DealsPage \u2014 promotions landing page with deal banners and on-sale products.\n *\n * SSR-friendly: pass `deals` and `productsOnSale` props for server rendering.\n */\nexport function DealsPage({\n title = "Deals & Offers",\n deals: dealsProp,\n productsOnSale: productsOnSaleProp,\n products: productsProp,\n locationId,\n renderImage,\n columns,\n className,\n classNames,\n}: DealsPageProps): React.ReactElement {\n const { deals: fetchedDeals, isLoading: dealsLoading } = useDeals({\n locationId,\n enabled: dealsProp === undefined,\n });\n const { products: fetchedOnSale, isLoading: productsLoading } = useProductsOnSale({\n enabled: productsOnSaleProp === undefined,\n });\n\n const deals = dealsProp ?? fetchedDeals;\n const onSale = productsOnSaleProp ?? fetchedOnSale;\n const isLoading = dealsLoading || productsLoading;\n\n const isEmpty = deals.length === 0 && onSale.length === 0;\n\n if (isLoading && isEmpty) {\n return (\n <div\n data-cimplify-deals-page\n aria-busy="true"\n className={cn(className, classNames?.root, classNames?.loading)}\n />\n );\n }\n\n return (\n <div data-cimplify-deals-page className={cn(className, classNames?.root)}>\n {/* Header */}\n <div data-cimplify-deals-header className={classNames?.header}>\n <h1 data-cimplify-deals-title className={classNames?.title}>\n {title}\n </h1>\n </div>\n\n {/* Deal banners */}\n {deals.length > 0 && (\n <div data-cimplify-deals-banner-section className={classNames?.bannerSection}>\n <DealBanner deals={deals} />\n </div>\n )}\n\n {/* Products on sale */}\n {(onSale.length > 0 || (productsProp && productsProp.length > 0)) && (\n <div data-cimplify-deals-products className={classNames?.productsSection}>\n <h2 data-cimplify-deals-products-title className={classNames?.productsTitle}>\n On Sale Now\n </h2>\n {productsProp ? (\n <ProductGrid\n products={productsProp}\n renderImage={renderImage}\n columns={columns}\n />\n ) : (\n <div data-cimplify-deals-sale-list>\n {onSale.map((dealInfo: ProductDealInfo) => (\n <div key={`${dealInfo.product_id}-${dealInfo.deal_id}`} data-cimplify-deals-sale-item>\n <SaleBadge\n product={{ id: dealInfo.product_id, name: dealInfo.deal_name } as Product}\n dealInfo={dealInfo}\n showOriginalPrice\n />\n </div>\n ))}\n </div>\n )}\n </div>\n )}\n\n {/* Empty state */}\n {isEmpty && (\n <div data-cimplify-deals-empty className={classNames?.empty}>\n <p>No deals available right now. Check back soon!</p>\n </div>\n )}\n </div>\n );\n}\n' }] }, "wholesale-product-layout": { "name": "wholesale-product-layout", "title": "WholesaleProductLayout", "description": "B2B wholesale layout with price range, volume pricing, MOQ, and inventory.", "type": "component", "registryDependencies": ["product-image-gallery", "product-customizer", "price", "price-range", "volume-pricing", "cn"], "files": [{ "path": "layouts/wholesale-product-layout.tsx", "content": '"use client";\n\nimport React from "react";\nimport type { ProductLayoutProps } from "@cimplify/sdk/react";\nimport {\n TwoColumnGrid,\n RelatedProductsSection,\n CustomAttributesTable,\n PropertiesTable,\n TagPills,\n InventoryBadge,\n} from "@cimplify/sdk/react";\nimport { ProductImageGallery } from "@cimplify/sdk/react";\nimport { ProductCustomizer } from "@cimplify/sdk/react";\nimport { PriceRange } from "@cimplify/sdk/react";\nimport { Price } from "@cimplify/sdk/react";\nimport { VolumePricing } from "@cimplify/sdk/react";\nimport { cn } from "@cimplify/sdk/react";\n\nexport function WholesaleProductLayout({\n product,\n onAddToCart,\n renderBreadcrumb,\n relatedProducts,\n showRelated = true,\n className,\n}: ProductLayoutProps): React.ReactElement {\n const images = product.images || (product.image_url ? [product.image_url] : []);\n const hasTiers = product.quantity_pricing && product.quantity_pricing.length > 1;\n\n return (\n <div data-cimplify-product-layout="wholesale" className={cn("space-y-8", className)}>\n {renderBreadcrumb?.(product)}\n\n <TwoColumnGrid\n left={\n <ProductImageGallery\n images={images}\n productName={product.name}\n />\n }\n right={\n <div className="space-y-6">\n {/* Tags */}\n <TagPills tags={product.tags || []} />\n\n {/* SKU + Name */}\n <div>\n {product.sku && (\n <p className="text-xs text-muted-foreground font-medium uppercase tracking-wider mb-1">\n SKU: {product.sku}\n </p>\n )}\n <h1 data-cimplify-product-layout-name className="text-3xl lg:text-4xl font-extrabold tracking-tight">\n {product.name}\n </h1>\n </div>\n\n {/* Price range */}\n <div>\n {hasTiers ? (\n <PriceRange product={product} className="text-2xl font-extrabold" />\n ) : (\n <Price amount={product.default_price} className="text-2xl font-semibold" />\n )}\n </div>\n\n {/* Inventory */}\n <InventoryBadge product={product} />\n\n {/* Min order notice */}\n {product.min_order_quantity != null && product.min_order_quantity > 1 && (\n <div data-cimplify-product-layout-moq className="flex items-center gap-3 p-3 bg-amber-50 border border-amber-200 text-sm text-amber-800">\n Minimum order quantity: <strong>{product.min_order_quantity} units</strong>\n </div>\n )}\n\n {/* Description */}\n {product.description && (\n <p data-cimplify-product-layout-description className="text-muted-foreground leading-relaxed">\n {product.description}\n </p>\n )}\n\n {/* Volume pricing \u2014 open by default for wholesale */}\n {hasTiers && (\n <VolumePricing\n tiers={product.quantity_pricing!}\n defaultOpen\n />\n )}\n\n {/* Product details */}\n {(product.sku || product.barcode || product.vendor) && (\n <div data-cimplify-product-layout-details className="space-y-1 text-sm text-muted-foreground">\n {product.barcode && <p>Barcode: <span className="font-mono text-foreground">{product.barcode}</span></p>}\n {product.vendor && <p>Supplier: <span className="text-foreground">{product.vendor}</span></p>}\n {product.hs_code && <p>HS Code: <span className="font-mono text-foreground">{product.hs_code}</span></p>}\n </div>\n )}\n\n {/* Custom Attributes */}\n <CustomAttributesTable attributes={product.custom_attributes || []} />\n\n {/* Properties */}\n <PropertiesTable properties={product.properties || []} />\n\n {/* Customizer (variants, billing plans, quantity, add to cart) */}\n <ProductCustomizer\n product={product}\n onAddToCart={onAddToCart}\n />\n </div>\n }\n />\n\n {showRelated && relatedProducts && relatedProducts.length > 0 && (\n <RelatedProductsSection\n products={relatedProducts}\n title="Related products"\n />\n )}\n </div>\n );\n}\n' }] }, "category-filter": { "name": "category-filter", "title": "CategoryFilter", "description": "Selectable category chips for filtering products.", "type": "component", "registryDependencies": ["cn"], "files": [{ "path": "category-filter.tsx", "content": '"use client";\n\nimport React, { useCallback } from "react";\nimport { Tabs } from "@base-ui/react/tabs";\nimport type { Category } from "@cimplify/sdk";\nimport { useCategories } from "@cimplify/sdk/react";\nimport { cn } from "@cimplify/sdk/react";\n\nexport interface CategoryFilterClassNames {\n root?: string;\n item?: string;\n allButton?: string;\n count?: string;\n}\n\nexport interface CategoryFilterProps {\n /** Currently selected category ID. Null means "all". */\n selectedId?: string | null;\n /** Called when a category is selected. Null means "all". */\n onSelect: (categoryId: string | null) => void;\n /** Label for the "all" option. Default: "All". */\n allLabel?: string;\n /** Show product counts per category. Default: true. */\n showCounts?: boolean;\n className?: string;\n classNames?: CategoryFilterClassNames;\n}\n\n/** Sentinel value representing the "all" tab (no category filter). */\nconst ALL_VALUE = "__all__";\n\n/**\n * CategoryFilter \u2014 horizontal or vertical list of category chips.\n *\n * Fetches categories via `useCategories` and renders selectable buttons.\n * The parent controls selection state via `selectedId` + `onSelect`.\n *\n * Built on Base UI Tabs for accessible keyboard navigation and ARIA roles.\n */\nexport function CategoryFilter({\n selectedId = null,\n onSelect,\n allLabel = "All",\n showCounts = true,\n className,\n classNames,\n}: CategoryFilterProps): React.ReactElement {\n const { categories, isLoading } = useCategories();\n\n const handleValueChange = useCallback(\n (value: string | number | null) => {\n onSelect(value === ALL_VALUE ? null : String(value));\n },\n [onSelect],\n );\n\n if (isLoading) {\n return (\n <div\n data-cimplify-category-filter\n aria-busy="true"\n className={cn(className, classNames?.root)}\n />\n );\n }\n\n return (\n <Tabs.Root\n value={selectedId ?? ALL_VALUE}\n onValueChange={handleValueChange}\n >\n <Tabs.List\n data-cimplify-category-filter\n aria-label="Filter by category"\n className={cn(className, classNames?.root)}\n >\n <Tabs.Tab\n value={ALL_VALUE}\n data-cimplify-category-filter-item\n data-selected={selectedId === null || undefined}\n className={cn(classNames?.item, classNames?.allButton)}\n >\n {allLabel}\n </Tabs.Tab>\n\n {categories.map((category: Category) => (\n <Tabs.Tab\n key={category.id}\n value={category.id}\n data-cimplify-category-filter-item\n data-selected={selectedId === category.id || undefined}\n className={classNames?.item}\n >\n {category.name}\n {showCounts && category.product_count != null && (\n <span data-cimplify-category-count className={classNames?.count}>\n {category.product_count}\n </span>\n )}\n </Tabs.Tab>\n ))}\n </Tabs.List>\n </Tabs.Root>\n );\n}\n' }] }, "add-on-selector": { "name": "add-on-selector", "title": "AddOnSelector", "description": "Modifier groups with single-select or multi-select options.", "type": "component", "registryDependencies": ["price"], "files": [{ "path": "add-on-selector.tsx", "content": '"use client";\n\nimport React, { useCallback, useMemo } from "react";\nimport { Checkbox } from "@base-ui/react/checkbox";\nimport type { AddOnWithOptions } from "@cimplify/sdk";\nimport { Price } from "@cimplify/sdk/react";\nimport { parsePrice } from "@cimplify/sdk";\nimport { cn } from "@cimplify/sdk/react";\n\nexport interface AddOnSelectorClassNames {\n root?: string;\n group?: string;\n header?: string;\n name?: string;\n required?: string;\n constraint?: string;\n validation?: string;\n options?: string;\n option?: string;\n optionSelected?: string;\n optionName?: string;\n optionDescription?: string;\n}\n\nexport interface AddOnSelectorProps {\n addOns: AddOnWithOptions[];\n selectedOptions: string[];\n onOptionsChange: (optionIds: string[]) => void;\n className?: string;\n classNames?: AddOnSelectorClassNames;\n}\n\nexport function AddOnSelector({\n addOns,\n selectedOptions,\n onOptionsChange,\n className,\n classNames,\n}: AddOnSelectorProps): React.ReactElement | null {\n const selectedSet = useMemo(() => new Set(selectedOptions), [selectedOptions]);\n\n const isOptionSelected = useCallback(\n (optionId: string) => selectedSet.has(optionId),\n [selectedSet],\n );\n\n const handleCheckedChange = useCallback(\n (addOn: AddOnWithOptions, optionId: string, checked: boolean) => {\n const isSelected = selectedSet.has(optionId);\n\n if (addOn.is_mutually_exclusive || !addOn.is_multiple_allowed) {\n const groupOptionIds = new Set(addOn.options.map((o) => o.id));\n const withoutGroup = selectedOptions.filter((id) => !groupOptionIds.has(id));\n\n if (isSelected) {\n if (!addOn.is_required) {\n onOptionsChange(withoutGroup);\n }\n } else {\n onOptionsChange([...withoutGroup, optionId]);\n }\n } else {\n if (isSelected) {\n onOptionsChange(selectedOptions.filter((id) => id !== optionId));\n } else {\n let currentCount = 0;\n for (const option of addOn.options) {\n if (selectedSet.has(option.id)) {\n currentCount += 1;\n }\n }\n\n if (addOn.max_selections && currentCount >= addOn.max_selections) {\n return;\n }\n\n onOptionsChange([...selectedOptions, optionId]);\n }\n }\n },\n [selectedOptions, selectedSet, onOptionsChange],\n );\n\n if (!addOns || addOns.length === 0) {\n return null;\n }\n\n return (\n <div data-cimplify-addon-selector className={cn("space-y-6", className, classNames?.root)}>\n {addOns.map((addOn) => {\n let currentSelections = 0;\n for (const option of addOn.options) {\n if (selectedSet.has(option.id)) {\n currentSelections += 1;\n }\n }\n const minMet = !addOn.min_selections || currentSelections >= addOn.min_selections;\n const isSingleSelect = addOn.is_mutually_exclusive || !addOn.is_multiple_allowed;\n\n return (\n <div\n key={addOn.id}\n data-cimplify-addon-group\n className={cn(classNames?.group)}\n >\n <div\n data-cimplify-addon-header\n className={cn("flex items-center justify-between py-3", classNames?.header)}\n >\n <div>\n <span\n data-cimplify-addon-name\n className={cn("text-base font-bold", classNames?.name)}\n >\n {addOn.name}\n </span>\n {(addOn.min_selections || addOn.max_selections) && (\n <span\n data-cimplify-addon-constraint\n className={cn("block text-xs text-muted-foreground mt-0.5", classNames?.constraint)}\n >\n {addOn.min_selections && addOn.max_selections\n ? `Choose ${addOn.min_selections}\\u2013${addOn.max_selections}`\n : addOn.min_selections\n ? `Choose at least ${addOn.min_selections}`\n : `Choose up to ${addOn.max_selections}`}\n </span>\n )}\n </div>\n {(addOn.is_required || !minMet) && (\n <span\n data-cimplify-addon-required\n className={cn(\n "text-xs font-semibold px-2.5 py-1 rounded",\n !minMet\n ? "text-destructive bg-destructive/10"\n : "text-destructive bg-destructive/10",\n classNames?.required,\n )}\n >\n Required\n </span>\n )}\n </div>\n\n <div\n data-cimplify-addon-options\n className={cn("divide-y divide-border", classNames?.options)}\n >\n {addOn.options.map((option) => {\n const isSelected = isOptionSelected(option.id);\n\n return (\n <Checkbox.Root\n key={option.id}\n checked={isSelected}\n onCheckedChange={(checked) =>\n handleCheckedChange(addOn, option.id, checked)\n }\n value={option.id}\n data-cimplify-addon-option\n data-selected={isSelected || undefined}\n className={cn(\n "w-full flex items-center gap-3 py-4 transition-colors text-left cursor-pointer",\n isSelected ? classNames?.optionSelected : classNames?.option,\n )}\n >\n <Checkbox.Indicator\n className="hidden"\n keepMounted={false}\n />\n\n {/* Visual indicator: radio circle for single-select, checkbox square for multi-select */}\n {isSingleSelect ? (\n <span\n data-cimplify-addon-radio\n className={cn(\n "w-5 h-5 rounded-full border-2 flex items-center justify-center shrink-0 transition-colors",\n isSelected ? "border-primary" : "border-muted-foreground/30",\n )}\n >\n {isSelected && <span className="w-2.5 h-2.5 rounded-full bg-primary" />}\n </span>\n ) : (\n <span\n data-cimplify-addon-checkbox\n className={cn(\n "w-5 h-5 rounded-sm border-2 flex items-center justify-center shrink-0 transition-colors",\n isSelected ? "border-primary bg-primary" : "border-muted-foreground/30",\n )}\n >\n {isSelected && (\n <svg viewBox="0 0 12 12" className="w-3 h-3 text-primary-foreground" fill="none">\n <path d="M2 6l3 3 5-5" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>\n </svg>\n )}\n </span>\n )}\n\n <span\n data-cimplify-addon-option-name\n className={cn(\n "flex-1 min-w-0 text-sm",\n classNames?.optionName,\n )}\n >\n {option.name}\n </span>\n\n {option.default_price != null && (\n <span className="text-sm text-muted-foreground">\n +<Price amount={option.default_price} />\n </span>\n )}\n </Checkbox.Root>\n );\n })}\n </div>\n </div>\n );\n })}\n </div>\n );\n}\n' }] }, "cart-page": { "name": "cart-page", "title": "CartPage", "description": "Full-page cart with summary, discount input, and checkout.", "type": "component", "registryDependencies": ["cart-summary", "discount-input", "cn"], "files": [{ "path": "cart-page.tsx", "content": '"use client";\n\nimport React from "react";\nimport { useCart } from "@cimplify/sdk/react";\nimport type { DiscountValidation } from "@cimplify/sdk";\nimport type { CartLayoutProps } from "./cart-layouts/shared";\nimport { DefaultCartLayout } from "./cart-layouts/default-cart-layout";\nimport { CompactCartLayout } from "./cart-layouts/compact-cart-layout";\nimport { cn } from "@cimplify/sdk/react";\n\nexport type { CartLayoutProps };\n\nexport enum CartTemplate {\n Default = "default",\n Compact = "compact",\n}\n\nexport interface CartPageClassNames {\n root?: string;\n}\n\nexport interface CartPageProps {\n /** Explicit template. */\n template?: CartTemplate;\n /** Layout map for overrides. */\n layouts?: Partial<Record<CartTemplate | string, React.ComponentType<CartLayoutProps>>>;\n /** Page title. */\n title?: string;\n /** Called when checkout is initiated. */\n onCheckout?: () => void;\n /** Called when "continue shopping" is clicked. */\n onContinueShopping?: () => void;\n /** Called when a valid discount is applied. */\n onDiscountApply?: (validation: DiscountValidation) => void;\n /** Called when discount is removed. */\n onDiscountClear?: () => void;\n /** Show discount code input. */\n showDiscount?: boolean;\n /** Checkout button text. */\n checkoutLabel?: string;\n className?: string;\n classNames?: CartPageClassNames;\n}\n\nconst BUILT_IN_LAYOUTS: Record<string, React.ComponentType<CartLayoutProps>> = {\n [CartTemplate.Default]: DefaultCartLayout,\n [CartTemplate.Compact]: CompactCartLayout,\n};\n\nexport function CartPage({\n template,\n layouts,\n onCheckout,\n onContinueShopping,\n onDiscountApply,\n onDiscountClear,\n showDiscount = true,\n checkoutLabel,\n className,\n classNames,\n}: CartPageProps): React.ReactElement {\n const cart = useCart();\n\n const key = template ?? CartTemplate.Default;\n const Layout = layouts?.[key] ?? BUILT_IN_LAYOUTS[key] ?? DefaultCartLayout;\n\n return (\n <div data-cimplify-cart-page className={cn(className, classNames?.root)}>\n <Layout\n cart={cart}\n onCheckout={onCheckout}\n onContinueShopping={onContinueShopping}\n onDiscountApply={onDiscountApply}\n onDiscountClear={onDiscountClear}\n showDiscount={showDiscount}\n checkoutLabel={checkoutLabel}\n />\n </div>\n );\n}\n' }] }, "chat-widget": { "name": "chat-widget", "title": "ChatWidget", "description": "Embeddable chat widget with AI shopping assistant powered by the support channel.", "type": "component", "registryDependencies": ["cn"], "files": [{ "path": "chat-widget.tsx", "content": '"use client";\n\nimport React, {\n useState,\n useRef,\n useEffect,\n useCallback,\n type KeyboardEvent,\n} from "react";\nimport type { CimplifyClient } from "../client";\nimport type { CimplifyError } from "@cimplify/sdk";\nimport type { ChatMessage, ChatWidgetStarter } from "../types/support";\nimport { useChat } from "./hooks/use-chat";\nimport { useOptionalCimplifyClient } from "@cimplify/sdk/react";\nimport { cn } from "@cimplify/sdk/react";\n\n/* \u2500\u2500\u2500 Public API \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\nexport interface ChatWidgetClassNames {\n root?: string;\n bubble?: string;\n panel?: string;\n header?: string;\n messages?: string;\n input?: string;\n welcome?: string;\n}\n\nexport interface ChatWidgetProps {\n client?: CimplifyClient;\n /** Business display name shown in the header */\n businessName?: string;\n /** Greeting text on the welcome screen */\n greeting?: string;\n /** Subtitle on the welcome screen */\n subtitle?: string;\n /** Conversation starters */\n starters?: ChatWidgetStarter[];\n /** Input placeholder */\n placeholder?: string;\n /** Position of the bubble */\n position?: "bottom-right" | "bottom-left";\n /** Start with the panel open */\n defaultOpen?: boolean;\n /** Poll interval in ms (default: 3000) */\n pollInterval?: number;\n className?: string;\n classNames?: ChatWidgetClassNames;\n}\n\nexport function ChatWidget({\n client: clientProp,\n businessName = "Support",\n greeting = "Hi there!",\n subtitle = "Ask us anything \u2014 we typically reply in a few seconds.",\n starters,\n placeholder = "Type a message\\u2026",\n position = "bottom-right",\n defaultOpen = false,\n pollInterval,\n className,\n classNames,\n}: ChatWidgetProps) {\n const context = useOptionalCimplifyClient();\n const client = clientProp ?? context?.client;\n\n const [open, setOpen] = useState(defaultOpen);\n const [unread, setUnread] = useState(0);\n\n const { messages, isLoading, isSending, error, isActive, send, startConversation } =\n useChat({ client, starters, pollInterval });\n\n const prevCountRef = useRef(messages.length);\n\n // Track unread when panel is closed\n useEffect(() => {\n if (!open && messages.length > prevCountRef.current) {\n const newMessages = messages.slice(prevCountRef.current);\n const incomingCount = newMessages.filter(\n (m) => m.sender_type !== "customer",\n ).length;\n if (incomingCount > 0) setUnread((n) => n + incomingCount);\n }\n prevCountRef.current = messages.length;\n }, [messages, open]);\n\n const handleOpen = useCallback(() => {\n setOpen(true);\n setUnread(0);\n }, []);\n\n const handleClose = useCallback(() => setOpen(false), []);\n\n const isLeft = position === "bottom-left";\n\n return (\n <div\n className={cn(\n "fixed bottom-6 z-[9999]",\n isLeft ? "left-6" : "right-6",\n className,\n classNames?.root,\n )}\n >\n {/* Bubble */}\n {!open && (\n <Bubble\n unread={unread}\n onClick={handleOpen}\n className={classNames?.bubble}\n />\n )}\n\n {/* Panel */}\n {open && (\n <Panel\n businessName={businessName}\n greeting={greeting}\n subtitle={subtitle}\n starters={starters}\n placeholder={placeholder}\n messages={messages}\n isLoading={isLoading}\n isSending={isSending}\n error={error}\n isActive={isActive}\n isLeft={isLeft}\n onClose={handleClose}\n onSend={send}\n onStartConversation={startConversation}\n classNames={classNames}\n />\n )}\n </div>\n );\n}\n\n/* \u2500\u2500\u2500 Bubble \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\nfunction Bubble({\n unread,\n onClick,\n className,\n}: {\n unread: number;\n onClick: () => void;\n className?: string;\n}) {\n return (\n <button\n type="button"\n onClick={onClick}\n aria-label="Open chat"\n className={cn(\n "relative flex h-14 w-14 items-center justify-center rounded-full",\n "bg-foreground text-background shadow-lg",\n "transition-transform duration-300 ease-out",\n "hover:scale-[1.08] active:scale-95",\n className,\n )}\n >\n <ChatIcon className="h-6 w-6" />\n {unread > 0 && (\n <span\n className={cn(\n "absolute -right-0.5 -top-0.5 flex h-5 w-5 items-center justify-center",\n "rounded-full bg-red-500 text-[10px] font-bold text-white",\n "ring-2 ring-background",\n )}\n >\n {unread > 9 ? "9+" : unread}\n </span>\n )}\n </button>\n );\n}\n\n/* \u2500\u2500\u2500 Panel \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\nfunction Panel({\n businessName,\n greeting,\n subtitle,\n starters,\n placeholder,\n messages,\n isLoading,\n isSending,\n error,\n isActive,\n isLeft,\n onClose,\n onSend,\n onStartConversation,\n classNames,\n}: {\n businessName: string;\n greeting: string;\n subtitle: string;\n starters?: ChatWidgetStarter[];\n placeholder: string;\n messages: ChatMessage[];\n isLoading: boolean;\n isSending: boolean;\n error: CimplifyError | null;\n isActive: boolean;\n isLeft: boolean;\n onClose: () => void;\n onSend: (content: string) => Promise<void>;\n onStartConversation: (text?: string) => Promise<void>;\n classNames?: ChatWidgetClassNames;\n}) {\n return (\n <div\n className={cn(\n "flex flex-col overflow-hidden rounded-2xl bg-background shadow-2xl",\n "w-[400px] max-w-[calc(100vw-3rem)]",\n "h-[min(600px,calc(100vh-6rem))]",\n "animate-in fade-in slide-in-from-bottom-4 duration-300",\n isLeft ? "origin-bottom-left" : "origin-bottom-right",\n classNames?.panel,\n )}\n >\n {/* Header */}\n <Header\n businessName={businessName}\n onClose={onClose}\n className={classNames?.header}\n />\n\n {/* Body */}\n {!isActive && !isLoading ? (\n <Welcome\n greeting={greeting}\n subtitle={subtitle}\n starters={starters}\n onStarter={onStartConversation}\n className={classNames?.welcome}\n />\n ) : (\n <MessageList\n messages={messages}\n isLoading={isLoading}\n isSending={isSending}\n className={classNames?.messages}\n />\n )}\n\n {/* Error */}\n {error != null && (\n <div className="border-t border-red-200 bg-red-50 px-4 py-2 text-xs text-red-600">\n Something went wrong. Please try again.\n </div>\n )}\n\n {/* Input \u2014 shown once conversation is active or loading */}\n {(isActive || isLoading) && (\n <ChatInput\n placeholder={placeholder}\n isSending={isSending}\n onSend={onSend}\n className={classNames?.input}\n />\n )}\n\n {/* Footer */}\n <div className="flex-shrink-0 border-t border-border bg-background px-4 py-1.5 text-center text-[10px] text-muted-foreground">\n Powered by{" "}\n <span className="font-semibold text-foreground">Cimplify</span>\n </div>\n </div>\n );\n}\n\n/* \u2500\u2500\u2500 Header \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\nfunction Header({\n businessName,\n onClose,\n className,\n}: {\n businessName: string;\n onClose: () => void;\n className?: string;\n}) {\n return (\n <div\n className={cn(\n "flex flex-shrink-0 items-center gap-3 bg-foreground px-5 py-4 text-background",\n className,\n )}\n >\n <div className="flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-[10px] bg-white/15">\n <ChatIcon className="h-5 w-5" />\n </div>\n <div className="min-w-0 flex-1">\n <div className="truncate text-[15px] font-semibold leading-tight">\n {businessName}\n </div>\n <div className="flex items-center gap-1.5 text-xs text-white/60">\n <span className="h-[7px] w-[7px] flex-shrink-0 rounded-full bg-emerald-400" />\n Online\n </div>\n </div>\n <button\n type="button"\n onClick={onClose}\n aria-label="Close chat"\n className={cn(\n "flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg",\n "bg-white/10 text-white/70 transition-colors hover:bg-white/20 hover:text-white",\n )}\n >\n <CloseIcon className="h-[18px] w-[18px]" />\n </button>\n </div>\n );\n}\n\n/* \u2500\u2500\u2500 Welcome \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\nfunction Welcome({\n greeting,\n subtitle,\n starters,\n onStarter,\n className,\n}: {\n greeting: string;\n subtitle: string;\n starters?: ChatWidgetStarter[];\n onStarter: (text?: string) => Promise<void>;\n className?: string;\n}) {\n const defaultStarters: ChatWidgetStarter[] = [\n { icon: "\\ud83d\\udd0d", text: "Help me find a product" },\n { icon: "\\ud83d\\uded2", text: "Check my cart" },\n { icon: "\\ud83d\\udce6", text: "Track my order" },\n ];\n\n const items = starters ?? defaultStarters;\n\n return (\n <div\n className={cn(\n "flex flex-1 flex-col items-center justify-center overflow-y-auto bg-muted/50 px-8 py-10 text-center",\n className,\n )}\n >\n <div className="mb-5 flex h-16 w-16 items-center justify-center rounded-2xl bg-muted">\n <ChatIcon className="h-7 w-7 text-muted-foreground" />\n </div>\n <h3 className="mb-2 text-xl font-bold text-foreground">{greeting}</h3>\n <p className="mb-6 max-w-[280px] text-sm leading-relaxed text-muted-foreground">\n {subtitle}\n </p>\n <div className="flex w-full max-w-[300px] flex-col gap-2">\n {items.map((s) => (\n <button\n key={s.text}\n type="button"\n onClick={() => onStarter(s.text)}\n className={cn(\n "flex items-center gap-3 rounded-xl border border-border bg-background px-4 py-3",\n "text-left text-[13px] font-medium text-foreground",\n "transition-all hover:-translate-y-px hover:border-foreground hover:shadow-sm",\n )}\n >\n {s.icon && (\n <span className="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg bg-muted text-[15px]">\n {s.icon}\n </span>\n )}\n <span>{s.text}</span>\n </button>\n ))}\n </div>\n </div>\n );\n}\n\n/* \u2500\u2500\u2500 Messages \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\nfunction MessageList({\n messages,\n isLoading,\n isSending,\n className,\n}: {\n messages: ChatMessage[];\n isLoading: boolean;\n isSending: boolean;\n className?: string;\n}) {\n const bottomRef = useRef<HTMLDivElement>(null);\n\n useEffect(() => {\n bottomRef.current?.scrollIntoView({ behavior: "smooth" });\n }, [messages.length, isSending]);\n\n return (\n <div\n className={cn(\n "flex flex-1 flex-col gap-1 overflow-y-auto bg-muted/50 px-5 py-5",\n className,\n )}\n >\n {isLoading && messages.length === 0 && (\n <div className="flex flex-1 items-center justify-center">\n <TypingDots />\n </div>\n )}\n\n {messages.map((msg, i) => {\n const isCustomer = msg.sender_type === "customer";\n const prev = messages[i - 1];\n const sameSenderAsPrev = prev?.sender_type === msg.sender_type;\n const isOptimistic = msg.id.startsWith("opt_");\n\n return (\n <div key={msg.id}>\n {!sameSenderAsPrev && i > 0 && <div className="h-3" />}\n <div\n className={cn(\n "flex max-w-[85%]",\n isCustomer ? "ml-auto" : "mr-auto",\n )}\n >\n <div\n className={cn(\n "rounded-2xl px-3.5 py-2.5 text-sm leading-relaxed",\n isCustomer\n ? "bg-foreground text-background"\n : "border border-border bg-background text-foreground",\n isCustomer\n ? sameSenderAsPrev\n ? "rounded-tr"\n : "rounded-br"\n : sameSenderAsPrev\n ? "rounded-tl"\n : "rounded-bl",\n isOptimistic && "opacity-70",\n )}\n >\n {!isCustomer && !sameSenderAsPrev && (\n <span className="mb-1 inline-flex items-center gap-1 rounded bg-violet-100 px-1.5 py-0.5 text-[10px] font-semibold text-violet-600">\n <SparkleIcon className="h-[11px] w-[11px]" />\n AI\n </span>\n )}\n <div className="whitespace-pre-wrap">{msg.content}</div>\n </div>\n </div>\n </div>\n );\n })}\n\n {isSending && (\n <div className="mr-auto mt-1">\n <TypingDots />\n </div>\n )}\n\n <div ref={bottomRef} />\n </div>\n );\n}\n\n/* \u2500\u2500\u2500 Input \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\nfunction ChatInput({\n placeholder,\n isSending,\n onSend,\n className,\n}: {\n placeholder: string;\n isSending: boolean;\n onSend: (content: string) => Promise<void>;\n className?: string;\n}) {\n const [value, setValue] = useState("");\n const textareaRef = useRef<HTMLTextAreaElement>(null);\n\n const handleSend = useCallback(() => {\n const trimmed = value.trim();\n if (!trimmed || isSending) return;\n setValue("");\n if (textareaRef.current) {\n textareaRef.current.style.height = "auto";\n }\n onSend(trimmed);\n }, [value, isSending, onSend]);\n\n const handleKeyDown = useCallback(\n (e: KeyboardEvent<HTMLTextAreaElement>) => {\n if (e.key === "Enter" && !e.shiftKey) {\n e.preventDefault();\n handleSend();\n }\n },\n [handleSend],\n );\n\n const handleInput = useCallback(() => {\n const el = textareaRef.current;\n if (!el) return;\n el.style.height = "auto";\n el.style.height = `${Math.min(el.scrollHeight, 100)}px`;\n }, []);\n\n const hasContent = value.trim().length > 0;\n\n return (\n <div\n className={cn(\n "flex flex-shrink-0 items-end gap-2 border-t border-border bg-background px-4 py-3",\n className,\n )}\n >\n <div\n className={cn(\n "flex flex-1 items-end rounded-xl border-[1.5px] border-transparent bg-muted px-1",\n "transition-colors focus-within:border-foreground focus-within:bg-background",\n )}\n >\n <textarea\n ref={textareaRef}\n value={value}\n onChange={(e) => setValue(e.target.value)}\n onKeyDown={handleKeyDown}\n onInput={handleInput}\n placeholder={placeholder}\n rows={1}\n className={cn(\n "max-h-[100px] min-h-[20px] flex-1 resize-none bg-transparent px-2.5 py-2",\n "text-sm text-foreground outline-none placeholder:text-muted-foreground",\n )}\n />\n </div>\n\n <button\n type="button"\n onClick={handleSend}\n disabled={!hasContent || isSending}\n aria-label="Send message"\n className={cn(\n "flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-[10px]",\n "transition-all duration-200",\n hasContent && !isSending\n ? "bg-foreground text-background hover:scale-105 active:scale-95"\n : "bg-muted text-muted-foreground",\n )}\n >\n <SendIcon className="h-[18px] w-[18px]" />\n </button>\n </div>\n );\n}\n\n/* \u2500\u2500\u2500 Typing dots \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\nfunction TypingDots() {\n return (\n <div className="flex items-center gap-1 px-4 py-3">\n {[0, 1, 2].map((i) => (\n <span\n key={i}\n className="h-[7px] w-[7px] animate-bounce rounded-full bg-muted-foreground/40"\n style={{ animationDelay: `${i * 160}ms` }}\n />\n ))}\n </div>\n );\n}\n\n/* \u2500\u2500\u2500 Icons (inline SVG \u2014 no icon library) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\nfunction ChatIcon({ className }: { className?: string }) {\n return (\n <svg\n className={className}\n viewBox="0 0 24 24"\n fill="none"\n stroke="currentColor"\n strokeWidth={2}\n strokeLinecap="round"\n strokeLinejoin="round"\n >\n <path d="M21 11.5a8.38 8.38 0 01-.9 3.8 8.5 8.5 0 01-7.6 4.7 8.38 8.38 0 01-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 01-.9-3.8 8.5 8.5 0 014.7-7.6 8.38 8.38 0 013.8-.9h.5a8.48 8.48 0 018 8v.5z" />\n </svg>\n );\n}\n\nfunction CloseIcon({ className }: { className?: string }) {\n return (\n <svg\n className={className}\n viewBox="0 0 24 24"\n fill="none"\n stroke="currentColor"\n strokeWidth={2}\n strokeLinecap="round"\n strokeLinejoin="round"\n >\n <path d="M18 6L6 18M6 6l12 12" />\n </svg>\n );\n}\n\nfunction SendIcon({ className }: { className?: string }) {\n return (\n <svg\n className={className}\n viewBox="0 0 24 24"\n fill="none"\n stroke="currentColor"\n strokeWidth={2}\n strokeLinecap="round"\n strokeLinejoin="round"\n >\n <line x1="22" y1="2" x2="11" y2="13" />\n <polygon points="22 2 15 22 11 13 2 9 22 2" />\n </svg>\n );\n}\n\nfunction SparkleIcon({ className }: { className?: string }) {\n return (\n <svg\n className={className}\n viewBox="0 0 24 24"\n fill="none"\n stroke="currentColor"\n strokeWidth={2.5}\n >\n <path d="M12 2v4m0 12v4M4.93 4.93l2.83 2.83m8.48 8.48l2.83 2.83M2 12h4m12 0h4M4.93 19.07l2.83-2.83m8.48-8.48l2.83-2.83" />\n </svg>\n );\n}\n' }] }, "product-customizer": { "name": "product-customizer", "title": "ProductCustomizer", "description": "Full product configuration with variants, add-ons, and add-to-cart.", "type": "component", "registryDependencies": ["price", "quantity-selector", "variant-selector", "add-on-selector", "composite-selector", "bundle-selector"], "files": [{ "path": "product-customizer.tsx", "content": '"use client";\n\nimport React, { useState, useCallback, useMemo, useEffect, useRef } from "react";\nimport { Button } from "@base-ui/react/button";\nimport type {\n ProductWithDetails,\n VariantView,\n AddOnOption,\n ComponentSelectionInput,\n CompositePriceResult,\n} from "@cimplify/sdk";\nimport type { ProductBillingPlan } from "@cimplify/sdk";\nimport type { BundleSelectionInput } from "@cimplify/sdk";\nimport { parsePrice, getUnitPriceAtQuantity } from "@cimplify/sdk";\nimport { formatDuration } from "./utils/format-duration";\nimport { useCart, useQuote } from "@cimplify/sdk/react";\nimport type { AddToCartOptions } from "@cimplify/sdk/react";\nimport { Price } from "@cimplify/sdk/react";\nimport { QuantitySelector } from "@cimplify/sdk/react";\nimport { VariantSelector } from "@cimplify/sdk/react";\nimport { AddOnSelector } from "@cimplify/sdk/react";\nimport { CompositeSelector } from "@cimplify/sdk/react";\nimport { BundleSelector } from "@cimplify/sdk/react";\nimport { BillingPlanSelector } from "@cimplify/sdk/react";\nimport { VolumePricing } from "@cimplify/sdk/react";\nimport { CustomerInputFields } from "./customer-input-fields";\nimport { DateSlotPicker } from "@cimplify/sdk/react";\nimport type { AvailableSlot } from "@cimplify/sdk";\nimport { cn } from "@cimplify/sdk/react";\n\nexport interface ProductCustomizerClassNames {\n root?: string;\n actions?: string;\n submitButton?: string;\n submitButtonAdded?: string;\n validation?: string;\n specialInstructions?: string;\n depositInfo?: string;\n allergens?: string;\n duration?: string;\n}\n\nexport interface ProductCustomizerProps {\n product: ProductWithDetails;\n onAddToCart?: (\n product: ProductWithDetails,\n quantity: number,\n options: AddToCartOptions,\n ) => void | Promise<void>;\n /** Lets the parent swap its gallery for `variant.images` on selection. */\n onVariantChange?: (\n variantId: string | undefined,\n variant: VariantView | undefined,\n ) => void;\n showSpecialInstructions?: boolean;\n className?: string;\n classNames?: ProductCustomizerClassNames;\n}\n\nexport function ProductCustomizer({\n product,\n onAddToCart,\n onVariantChange,\n showSpecialInstructions = true,\n className,\n classNames,\n}: ProductCustomizerProps): React.ReactElement {\n const [quantity, setQuantity] = useState(product.min_order_quantity ?? 1);\n const [isAdded, setIsAdded] = useState(false);\n const [isSubmitting, setIsSubmitting] = useState(false);\n const [selectedVariantId, setSelectedVariantId] = useState<string | undefined>();\n const [selectedVariant, setSelectedVariant] = useState<VariantView | undefined>();\n const [selectedAddOnOptionIds, setSelectedAddOnOptionIds] = useState<string[]>([]);\n\n const [compositeSelections, setCompositeSelections] = useState<ComponentSelectionInput[]>([]);\n const [compositePrice, setCompositePrice] = useState<CompositePriceResult | null>(null);\n const [compositeReady, setCompositeReady] = useState(false);\n\n const [bundleSelections, setBundleSelections] = useState<BundleSelectionInput[]>([]);\n const [bundleTotalPrice, setBundleTotalPrice] = useState<number | null>(null);\n const [bundleReady, setBundleReady] = useState(false);\n const [selectedBillingPlan, setSelectedBillingPlan] = useState<ProductBillingPlan | null>(null);\n const [customerInputValues, setCustomerInputValues] = useState<Record<string, unknown>>({});\n const [specialInstructions, setSpecialInstructions] = useState("");\n const [selectedSlot, setSelectedSlot] = useState<AvailableSlot | null>(null);\n\n const cart = useCart();\n\n const productType = product.type || "product";\n const isComposite = productType === "composite";\n const isBundle = productType === "bundle";\n const isDigital = productType === "digital";\n const isService = productType === "service";\n const isStandard = !isComposite && !isBundle;\n\n const hasVariants = isStandard && product.variants && product.variants.length > 0;\n const hasAddOns = isStandard && product.add_ons && product.add_ons.length > 0;\n\n useEffect(() => {\n setQuantity(product.min_order_quantity ?? 1);\n setIsAdded(false);\n setIsSubmitting(false);\n setSelectedVariantId(undefined);\n setSelectedVariant(undefined);\n setSelectedAddOnOptionIds([]);\n setCompositeSelections([]);\n setCompositePrice(null);\n setCompositeReady(false);\n setBundleSelections([]);\n setBundleTotalPrice(null);\n setBundleReady(false);\n setSelectedBillingPlan(null);\n setCustomerInputValues({});\n setSpecialInstructions("");\n setSelectedSlot(null);\n }, [product.id, product.min_order_quantity]);\n\n const selectedAddOnOptions = useMemo(() => {\n if (!product.add_ons) return [];\n const options: AddOnOption[] = [];\n for (const addOn of product.add_ons) {\n for (const option of addOn.options) {\n if (selectedAddOnOptionIds.includes(option.id)) {\n options.push(option);\n }\n }\n }\n return options;\n }, [product.add_ons, selectedAddOnOptionIds]);\n\n const normalizedAddOnOptionIds = useMemo(() => {\n if (selectedAddOnOptionIds.length === 0) return [];\n return Array.from(new Set(selectedAddOnOptionIds.map((id) => id.trim()).filter(Boolean))).sort();\n }, [selectedAddOnOptionIds]);\n\n const localTotalPrice = useMemo(() => {\n if (isComposite && compositePrice) {\n return parsePrice(compositePrice.final_price) * quantity;\n }\n\n if (isBundle && bundleTotalPrice != null) {\n return bundleTotalPrice * quantity;\n }\n\n let price = parsePrice(product.default_price);\n\n if (selectedVariant?.price_adjustment) {\n price += parsePrice(selectedVariant.price_adjustment);\n }\n\n for (const option of selectedAddOnOptions) {\n if (option.default_price) {\n price += parsePrice(option.default_price);\n }\n }\n\n return price * quantity;\n }, [product.default_price, selectedVariant, selectedAddOnOptions, quantity, isComposite, compositePrice, isBundle, bundleTotalPrice]);\n\n const requiredAddOnsSatisfied = useMemo(() => {\n if (!product.add_ons) return true;\n\n for (const addOn of product.add_ons) {\n if (addOn.is_required) {\n const selectedInGroup = selectedAddOnOptionIds.filter((id) =>\n addOn.options.some((opt) => opt.id === id),\n ).length;\n\n const minRequired = addOn.min_selections || 1;\n if (selectedInGroup < minRequired) {\n return false;\n }\n }\n }\n return true;\n }, [product.add_ons, selectedAddOnOptionIds]);\n\n const quoteInput = useMemo(\n () => ({\n productId: product.id,\n quantity,\n variantId: selectedVariantId,\n addOnOptionIds: normalizedAddOnOptionIds.length > 0 ? normalizedAddOnOptionIds : undefined,\n bundleSelections: isBundle && bundleSelections.length > 0 ? bundleSelections : undefined,\n compositeSelections: isComposite && compositeSelections.length > 0 ? compositeSelections : undefined,\n }),\n [product.id, quantity, selectedVariantId, normalizedAddOnOptionIds, isBundle, bundleSelections, isComposite, compositeSelections],\n );\n\n const requiredInputsSatisfied = useMemo(() => {\n if (!product.input_fields || product.input_fields.length === 0) return true;\n return product.input_fields.every((field) => {\n if (!field.is_required) return true;\n const val = customerInputValues[field.id];\n return val !== undefined && val !== "" && val !== null;\n });\n }, [product.input_fields, customerInputValues]);\n\n const quoteEnabled = isComposite\n ? compositeReady && requiredInputsSatisfied\n : isBundle\n ? bundleReady && requiredInputsSatisfied\n : requiredAddOnsSatisfied && requiredInputsSatisfied;\n\n const { quote } = useQuote(quoteInput, {\n enabled: quoteEnabled,\n });\n\n const quoteId = quote?.quote_id;\n const quotedTotalPrice = useMemo(() => {\n if (!quote) return undefined;\n\n const priceInfo = quote.quoted_total_price_info ?? quote.final_price_info;\n const perUnit = priceInfo.pre_tax_price;\n if (perUnit === undefined || perUnit === null) return undefined;\n return parsePrice(perUnit) * quantity;\n }, [quote, quantity]);\n\n const displayTotalPrice = quotedTotalPrice ?? localTotalPrice;\n\n const handleVariantChange = useCallback(\n (variantId: string | undefined, variant: VariantView | undefined) => {\n setSelectedVariantId(variantId);\n setSelectedVariant(variant);\n onVariantChange?.(variantId, variant);\n },\n [onVariantChange],\n );\n\n const addedTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n useEffect(() => () => clearTimeout(addedTimerRef.current), []);\n\n const handleAddToCart = useCallback(async () => {\n if (isSubmitting) return;\n\n setIsSubmitting(true);\n\n const options: AddToCartOptions = {\n variantId: selectedVariantId,\n variant: selectedVariant\n ? { id: selectedVariant.id, name: selectedVariant.name || "", price_adjustment: selectedVariant.price_adjustment }\n : undefined,\n quoteId,\n addOnOptionIds: normalizedAddOnOptionIds.length > 0 ? normalizedAddOnOptionIds : undefined,\n addOnOptions: selectedAddOnOptions.length > 0\n ? selectedAddOnOptions.map((opt) => ({\n id: opt.id,\n name: opt.name,\n add_on_id: opt.add_on_id,\n default_price: opt.default_price,\n }))\n : undefined,\n compositeSelections: isComposite && compositeSelections.length > 0 ? compositeSelections : undefined,\n bundleSelections: isBundle && bundleSelections.length > 0 ? bundleSelections : undefined,\n billingPlanId: selectedBillingPlan?.id,\n ...(isService && selectedSlot\n ? {\n scheduledStart: selectedSlot.start_time,\n scheduledEnd: selectedSlot.end_time,\n staffId: selectedSlot.available_staff?.[0]?.staff_id,\n resourceId: selectedSlot.available_resources?.[0]?.resource_id,\n }\n : {}),\n customerInputs: Object.keys(customerInputValues).length > 0\n ? Object.entries(customerInputValues)\n .filter(([, v]) => v !== undefined && v !== "")\n .map(([fieldId, value]) => ({ field_id: fieldId, value }))\n : undefined,\n specialInstructions: specialInstructions.trim() || undefined,\n };\n\n try {\n if (onAddToCart) {\n await onAddToCart(product, quantity, options);\n } else {\n await cart.addItem(product, quantity, options);\n }\n setIsAdded(true);\n clearTimeout(addedTimerRef.current);\n addedTimerRef.current = setTimeout(() => {\n setIsAdded(false);\n setQuantity(product.min_order_quantity ?? 1);\n }, 2000);\n } catch {\n // Caller handles errors via onAddToCart or cart hook error state\n } finally {\n setIsSubmitting(false);\n }\n }, [product, quantity, selectedVariantId, selectedVariant, quoteId, normalizedAddOnOptionIds, selectedAddOnOptions, isComposite, compositeSelections, isBundle, bundleSelections, selectedBillingPlan, customerInputValues, specialInstructions, isService, selectedSlot, isSubmitting, onAddToCart, cart]);\n\n return (\n <div data-cimplify-customizer className={cn("space-y-6", className, classNames?.root)}>\n {isComposite && product.groups && product.composite_id && (\n <CompositeSelector\n compositeId={product.composite_id}\n groups={product.groups}\n onSelectionsChange={setCompositeSelections}\n onPriceChange={setCompositePrice}\n onReady={setCompositeReady}\n skipPriceFetch\n />\n )}\n\n {isBundle && product.components && (\n <BundleSelector\n components={product.components}\n bundlePrice={product.bundle_price}\n discountValue={product.discount_value}\n pricingType={product.pricing_type}\n onSelectionsChange={setBundleSelections}\n onPriceChange={setBundleTotalPrice}\n onReady={setBundleReady}\n />\n )}\n\n {isService && (\n <DateSlotPicker\n serviceId={product.id}\n selectedSlot={selectedSlot}\n onSlotSelect={(slot) => setSelectedSlot(slot)}\n participantCount={quantity}\n />\n )}\n\n {hasVariants && (\n <VariantSelector\n variants={product.variants!}\n variantAxes={product.variant_axes}\n basePrice={product.default_price}\n selectedVariantId={selectedVariantId}\n onVariantChange={handleVariantChange}\n productName={product.name}\n />\n )}\n\n {hasAddOns && (\n <AddOnSelector\n addOns={product.add_ons!}\n selectedOptions={selectedAddOnOptionIds}\n onOptionsChange={setSelectedAddOnOptionIds}\n />\n )}\n\n {/* Billing plans */}\n {product.billing_plans && product.billing_plans.length > 0 && (\n <BillingPlanSelector\n productId={product.id}\n plans={product.billing_plans}\n onPlanSelect={setSelectedBillingPlan}\n selectedPlanId={selectedBillingPlan?.id ?? null}\n showOneTimePurchase\n />\n )}\n\n {/* Volume pricing tiers */}\n {product.quantity_pricing && product.quantity_pricing.length > 1 && (\n <VolumePricing\n tiers={product.quantity_pricing}\n currentQuantity={quantity}\n />\n )}\n\n {/* Customer input fields */}\n {product.input_fields && product.input_fields.length > 0 && (\n <CustomerInputFields\n fields={product.input_fields}\n values={customerInputValues}\n onChange={setCustomerInputValues}\n />\n )}\n\n {/* Deposit info for service products */}\n {product.deposit_type && product.deposit_type !== "none" && product.deposit_amount && (\n <div data-cimplify-customizer-deposit className={cn("text-sm text-muted-foreground", classNames?.depositInfo)}>\n {product.deposit_type === "fixed" ? (\n <span>Deposit required: <Price amount={product.deposit_amount} /></span>\n ) : (\n <span>Deposit required: {parsePrice(product.deposit_amount)}%</span>\n )}\n </div>\n )}\n\n {/* Allergens */}\n {product.allergies && product.allergies.length > 0 && (\n <div data-cimplify-customizer-allergens className={cn("flex flex-wrap gap-1.5", classNames?.allergens)}>\n {product.allergies.map((allergen) => (\n <span\n key={allergen}\n data-cimplify-allergen-tag\n className="inline-block text-xs px-2 py-0.5 rounded-full bg-muted text-muted-foreground"\n >\n {allergen}\n </span>\n ))}\n </div>\n )}\n\n {/* Service duration */}\n {isService && product.duration_minutes != null && (\n <div data-cimplify-customizer-duration className={cn("text-sm text-muted-foreground", classNames?.duration)}>\n Duration: {formatDuration(product.duration_minutes, product.duration_unit)}\n </div>\n )}\n\n {/* Special instructions */}\n {showSpecialInstructions && !isDigital && (\n <div data-cimplify-customizer-special-instructions>\n <textarea\n value={specialInstructions}\n onChange={(e) => setSpecialInstructions(e.target.value)}\n placeholder="Special instructions (e.g., no onions, extra sauce)"\n rows={2}\n data-cimplify-customizer-textarea\n className={cn(\n "w-full rounded-md border border-input bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring resize-none",\n classNames?.specialInstructions,\n )}\n />\n </div>\n )}\n\n <div\n data-cimplify-customizer-actions\n className={cn("pt-4 border-t border-border", classNames?.actions)}\n >\n {!quoteEnabled && (\n <p\n id="cimplify-customizer-validation"\n data-cimplify-customizer-validation\n className={cn("text-sm text-destructive mb-3", classNames?.validation)}\n >\n Please select all required options\n </p>\n )}\n <div className="flex items-center gap-4">\n <div className="flex flex-col gap-1">\n <QuantitySelector\n value={quantity}\n onChange={setQuantity}\n min={product.min_order_quantity ?? 1}\n />\n {product.min_order_quantity && product.min_order_quantity > 1 && (\n <span data-cimplify-customizer-min-hint className="text-xs text-muted-foreground text-center">\n Min. {product.min_order_quantity}\n </span>\n )}\n </div>\n\n {product.quantity_pricing && product.quantity_pricing.length > 0 && (\n <div data-cimplify-customizer-unit-price className="text-sm text-muted-foreground text-right shrink-0">\n <Price amount={getUnitPriceAtQuantity(product.quantity_pricing, quantity, parsePrice(product.default_price))} className="font-medium text-foreground" /> ea.\n </div>\n )}\n\n <Button\n onClick={handleAddToCart}\n disabled={isAdded || isSubmitting || !quoteEnabled}\n aria-describedby={!quoteEnabled ? "cimplify-customizer-validation" : undefined}\n data-cimplify-customizer-submit\n className={cn(\n "flex-1 h-14 text-base bg-primary text-primary-foreground font-medium hover:bg-primary/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed rounded-full",\n isAdded && classNames?.submitButtonAdded,\n classNames?.submitButton,\n )}\n >\n {isAdded ? "Added to Cart" : (\n <>\n Add to Cart &middot; <Price amount={displayTotalPrice} />\n </>\n )}\n </Button>\n </div>\n </div>\n\n </div>\n );\n}\n' }] }, "cart-summary": { "name": "cart-summary", "title": "CartSummary", "description": "Cart line items with quantity controls and totals.", "type": "component", "registryDependencies": ["price", "quantity-selector"], "files": [{ "path": "cart-summary.tsx", "content": '"use client";\n\nimport React from "react";\nimport type { UseCartItem } from "@cimplify/sdk/react";\nimport type { DisplayCart } from "@cimplify/sdk";\nimport { useCart } from "@cimplify/sdk/react";\nimport { getVariantDisplayName } from "@cimplify/sdk";\nimport { INPUT_FIELD_TYPE } from "@cimplify/sdk";\nimport { parsePrice } from "@cimplify/sdk";\nimport { Price } from "@cimplify/sdk/react";\nimport { QuantitySelector } from "@cimplify/sdk/react";\nimport { useProductPrice } from "@cimplify/sdk/react";\nimport { cn } from "@cimplify/sdk/react";\n\nexport interface CartSummaryClassNames {\n root?: string;\n item?: string;\n itemName?: string;\n itemPrice?: string;\n quantity?: string;\n removeButton?: string;\n totals?: string;\n subtotal?: string;\n tax?: string;\n deliveryFee?: string;\n serviceCharge?: string;\n discount?: string;\n total?: string;\n checkout?: string;\n empty?: string;\n serviceInfo?: string;\n digitalInfo?: string;\n bundleBreakdown?: string;\n compositeBreakdown?: string;\n customerInputs?: string;\n}\n\nexport interface CartSummaryProps {\n /** Optional server cart for extended pricing details (delivery fee, service charge, discounts). */\n cart?: DisplayCart;\n onCheckout?: () => void;\n onItemRemove?: (itemId: string) => void;\n onQuantityChange?: (itemId: string, quantity: number) => void;\n emptyMessage?: string;\n /** Render the totals block (subtotal/tax/total) below the items. Default true. Hosts that show their own totals (drawer, page sidebar) should pass false. */\n showTotals?: boolean;\n /** Render the "Proceed to Checkout" button. Defaults to true if `onCheckout` is provided. */\n showCheckoutButton?: boolean;\n className?: string;\n classNames?: CartSummaryClassNames;\n}\n\nfunction CartLineItemRow({\n item,\n onRemove,\n onQuantityChange,\n classNames,\n}: {\n item: UseCartItem;\n onRemove: (itemId: string) => void;\n onQuantityChange: (itemId: string, qty: number) => void;\n classNames?: CartSummaryClassNames;\n}): React.ReactElement {\n const { unitPrice } = useProductPrice({\n product: item.product,\n variant: item.variant,\n addOnOptions: item.addOnOptions,\n });\n const hasComposite = item.compositeSelections && item.compositeSelections.length > 0;\n const hasBundle = item.bundleSelections && item.bundleSelections.length > 0;\n\n const isOptimistic = (item as { isOptimistic?: boolean }).isOptimistic === true;\n\n const lineTotal = parsePrice(unitPrice) * item.quantity;\n const imageUrl = item.product.image_url;\n const variantLabel = item.variant ? getVariantDisplayName(item.variant, item.product.name) : null;\n\n return (\n <div\n data-cimplify-cart-item\n data-optimistic={isOptimistic ? "true" : undefined}\n className={cn(\n "group relative flex gap-3 py-4 border-b border-border last:border-b-0 transition-opacity",\n isOptimistic && "opacity-60",\n classNames?.item,\n )}\n >\n {/* Thumbnail */}\n <div\n data-cimplify-cart-item-image\n className="relative w-20 h-20 shrink-0 rounded-md overflow-hidden bg-muted"\n >\n {imageUrl ? (\n <img\n src={imageUrl}\n alt=""\n className="w-full h-full object-cover"\n loading="lazy"\n />\n ) : (\n <div className="w-full h-full grid place-items-center text-muted-foreground/40">\n <svg className="w-6 h-6" fill="none" stroke="currentColor" strokeWidth="1.5" viewBox="0 0 24 24" aria-hidden>\n <path strokeLinecap="round" strokeLinejoin="round" d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 001.5-1.5V6a1.5 1.5 0 00-1.5-1.5H3.75A1.5 1.5 0 002.25 6v12a1.5 1.5 0 001.5 1.5zm10.5-11.25h.008v.008h-.008V8.25zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z" />\n </svg>\n </div>\n )}\n </div>\n\n {/* Info column */}\n <div data-cimplify-cart-item-info className="flex-1 min-w-0 flex flex-col">\n <div className="flex items-start justify-between gap-3">\n <h3\n data-cimplify-cart-item-name\n className={cn("text-sm font-medium leading-tight m-0 truncate", classNames?.itemName)}\n >\n {item.product.name}\n </h3>\n <button\n type="button"\n onClick={() => onRemove(item.id)}\n data-cimplify-cart-item-remove\n className={cn(\n "shrink-0 -mr-1 -mt-1 grid place-items-center w-7 h-7 rounded-full text-muted-foreground/60 hover:text-foreground hover:bg-muted transition-colors",\n classNames?.removeButton,\n )}\n aria-label={`Remove ${item.product.name}`}\n >\n <svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24" aria-hidden>\n <path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />\n </svg>\n </button>\n </div>\n\n {/* Pills row: variant, add-ons, badges */}\n {(variantLabel || (item.addOnOptions && item.addOnOptions.length > 0) || hasComposite || hasBundle) && (\n <div data-cimplify-cart-item-pills className="flex flex-wrap gap-1.5 mt-1.5">\n {variantLabel && (\n <span\n data-cimplify-cart-item-variant\n className="inline-flex items-center px-2 py-0.5 rounded-full bg-muted text-[11px] font-medium text-foreground"\n >\n {variantLabel}\n </span>\n )}\n {item.addOnOptions?.map((opt) => (\n <span\n key={opt.id}\n data-cimplify-cart-item-addon\n className="inline-flex items-center px-2 py-0.5 rounded-full bg-muted text-[11px] font-medium text-muted-foreground"\n >\n + {opt.name}\n </span>\n ))}\n {(hasComposite || hasBundle) && (\n <span\n data-cimplify-cart-item-badge\n className="inline-flex items-center px-2 py-0.5 rounded-full bg-primary/10 text-[11px] font-semibold uppercase tracking-wider text-primary"\n >\n {hasComposite ? "Custom" : "Bundle"}\n </span>\n )}\n </div>\n )}\n\n {/* Special instructions */}\n {item.specialInstructions && (\n <p\n data-cimplify-cart-item-instructions\n className="mt-1.5 text-xs italic text-muted-foreground line-clamp-2"\n >\n \\u201C{item.specialInstructions}\\u201D\n </p>\n )}\n\n {/* Customer input values */}\n {item.customerInputs && item.customerInputs.length > 0 && (\n <dl data-cimplify-cart-customer-inputs className={cn("mt-1.5 text-xs space-y-0.5", classNames?.customerInputs)}>\n {item.customerInputs.map((input) => (\n <div key={input.field_id} data-cimplify-cart-input className="flex gap-1.5">\n <dt data-cimplify-cart-input-label className="text-muted-foreground">{input.field_name}:</dt>\n <dd data-cimplify-cart-input-value className="text-foreground m-0 truncate">\n {input.field_type === INPUT_FIELD_TYPE.File || input.field_type === INPUT_FIELD_TYPE.Image\n ? String(input.value).split("/").pop() || "Uploaded file"\n : String(input.value)}\n </dd>\n </div>\n ))}\n </dl>\n )}\n\n {/* Service: scheduled details */}\n {(item.lineType === "service" || item.product.type === "service") && item.scheduledStart && (\n <div data-cimplify-cart-service-info className={cn("mt-1.5 flex items-center gap-1.5 text-xs text-muted-foreground", classNames?.serviceInfo)}>\n <svg className="w-3.5 h-3.5 shrink-0" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24" aria-hidden>\n <path strokeLinecap="round" strokeLinejoin="round" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />\n </svg>\n <span>\n {new Date(item.scheduledStart).toLocaleDateString(undefined, { month: "short", day: "numeric" })} at{" "}\n {new Date(item.scheduledStart).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}\n </span>\n {item.serviceStatus && (\n <span data-status={item.serviceStatus} className="ml-auto px-1.5 py-0.5 rounded-full bg-muted text-[10px] uppercase tracking-wider">\n {item.serviceStatus}\n </span>\n )}\n </div>\n )}\n\n {/* Digital: instant-delivery badge */}\n {(item.lineType === "digital" || item.product.type === "digital") && (\n <div data-cimplify-cart-digital-info className={cn("mt-1.5 flex items-center gap-1 text-xs text-muted-foreground", classNames?.digitalInfo)}>\n <svg className="w-3.5 h-3.5 shrink-0" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24" aria-hidden>\n <path strokeLinecap="round" strokeLinejoin="round" d="M13 10V3L4 14h7v7l9-11h-7z" />\n </svg>\n <span>Instant digital delivery</span>\n </div>\n )}\n\n {/* Bundle / composite component breakdown */}\n {(item.bundleResolved || item.compositeResolved) && (\n <ul\n data-cimplify-cart-composite-breakdown={item.compositeResolved ? "" : undefined}\n data-cimplify-cart-bundle-breakdown={item.bundleResolved ? "" : undefined}\n className={cn("mt-1.5 pl-3 border-l border-border space-y-0.5 text-xs text-muted-foreground", item.compositeResolved ? classNames?.compositeBreakdown : classNames?.bundleBreakdown)}\n >\n {((item.bundleResolved?.selections ?? item.compositeResolved?.selections ?? []) as Array<{\n component_id?: string;\n product_name?: string;\n component_name?: string;\n quantity: number;\n product_type?: string;\n }>).map((sel) => {\n const name = sel.product_name ?? sel.component_name ?? "";\n const compId = sel.component_id ?? `${name}-${sel.quantity}`;\n return (\n <li key={compId} data-product-type={sel.product_type} className="flex items-baseline gap-1.5 m-0">\n <span className="font-medium text-foreground/80">{sel.quantity}\\u00D7</span>\n <span className="truncate">{name}</span>\n </li>\n );\n })}\n </ul>\n )}\n\n {/* Bottom row: line price + qty stepper */}\n <div className="mt-3 flex items-center justify-between gap-3">\n <Price\n amount={lineTotal}\n className={cn("text-sm font-semibold tabular-nums", classNames?.itemPrice)}\n />\n <div data-cimplify-cart-item-controls className={classNames?.quantity}>\n <QuantitySelector\n value={item.quantity}\n onChange={(qty) => onQuantityChange(item.id, qty)}\n min={0}\n />\n </div>\n </div>\n </div>\n </div>\n );\n}\n\n/**\n * CartSummary \u2014 renders cart line items + totals.\n *\n * NOT a drawer or modal \u2014 just the cart content. Templates wrap this in\n * their own drawer/modal shell with animations.\n */\nexport function CartSummary({\n cart: serverCart,\n onCheckout,\n onItemRemove,\n onQuantityChange,\n emptyMessage = "Your cart is empty",\n showTotals = true,\n showCheckoutButton,\n className,\n classNames,\n}: CartSummaryProps): React.ReactElement {\n const {\n items,\n itemCount,\n subtotal,\n tax,\n total,\n isEmpty,\n isOptimistic,\n pendingOpCount,\n removeItem,\n updateQuantity,\n } = useCart();\n\n const deliveryFee = serverCart?.delivery_fee;\n const serviceCharge = serverCart?.service_charge;\n const totalDiscounts = serverCart?.total_discounts;\n\n const handleRemove = (itemId: string) => {\n if (onItemRemove) {\n onItemRemove(itemId);\n } else {\n void removeItem(itemId);\n }\n };\n\n const handleQuantityChange = (itemId: string, qty: number) => {\n if (onQuantityChange) {\n onQuantityChange(itemId, qty);\n } else {\n void updateQuantity(itemId, qty);\n }\n };\n\n return (\n <div\n data-cimplify-cart-summary\n data-optimistic={isOptimistic ? "true" : undefined}\n data-pending-ops={pendingOpCount > 0 ? pendingOpCount : undefined}\n className={cn(className, classNames?.root)}\n >\n {isEmpty ? (\n <div data-cimplify-cart-empty className={classNames?.empty}>\n <p>{emptyMessage}</p>\n </div>\n ) : (\n <>\n {/* Line items */}\n <div data-cimplify-cart-items>\n {items.map((item) => (\n <CartLineItemRow\n key={item.id}\n item={item}\n onRemove={handleRemove}\n onQuantityChange={handleQuantityChange}\n classNames={classNames}\n />\n ))}\n </div>\n\n {/* Totals */}\n {showTotals && (\n <div\n data-cimplify-cart-totals\n className={cn("mt-6 pt-4 border-t border-border space-y-2 text-sm", classNames?.totals)}\n >\n <div data-cimplify-cart-subtotal className={cn("flex items-baseline justify-between", classNames?.subtotal)}>\n <span className="text-muted-foreground">\n Subtotal ({itemCount} {itemCount === 1 ? "item" : "items"})\n </span>\n <Price amount={subtotal} className="tabular-nums" />\n </div>\n\n {totalDiscounts != null && parsePrice(totalDiscounts) > 0 && (\n <div data-cimplify-cart-discount className={cn("flex items-baseline justify-between", classNames?.discount)}>\n <span className="text-muted-foreground">Discount</span>\n <Price amount={totalDiscounts} prefix="-" className="tabular-nums text-primary" />\n </div>\n )}\n\n {deliveryFee != null && (\n <div data-cimplify-cart-delivery-fee className={cn("flex items-baseline justify-between", classNames?.deliveryFee)}>\n <span className="text-muted-foreground">Delivery</span>\n {parsePrice(deliveryFee) > 0 ? (\n <Price amount={deliveryFee} className="tabular-nums" />\n ) : (\n <span className="font-medium text-primary">Free</span>\n )}\n </div>\n )}\n\n {serviceCharge != null && parsePrice(serviceCharge) > 0 && (\n <div data-cimplify-cart-service-charge className={cn("flex items-baseline justify-between", classNames?.serviceCharge)}>\n <span className="text-muted-foreground">Service charge</span>\n <Price amount={serviceCharge} className="tabular-nums" />\n </div>\n )}\n\n <div data-cimplify-cart-tax className={cn("flex items-baseline justify-between", classNames?.tax)}>\n <span className="text-muted-foreground">Tax</span>\n <Price amount={tax} className="tabular-nums" />\n </div>\n\n <div data-cimplify-cart-total className={cn("flex items-baseline justify-between pt-3 mt-1 border-t border-border", classNames?.total)}>\n <span className="font-semibold">Total</span>\n <Price amount={total} className="text-base font-bold tabular-nums" />\n </div>\n </div>\n )}\n\n {(showCheckoutButton ?? Boolean(onCheckout)) && onCheckout && (\n <button\n type="button"\n onClick={onCheckout}\n data-cimplify-cart-checkout\n className={cn(\n "mt-4 w-full h-12 rounded-full bg-foreground text-background font-semibold text-sm hover:bg-foreground/90 active:scale-[0.99] transition-all",\n classNames?.checkout,\n )}\n >\n Proceed to Checkout\n </button>\n )}\n </>\n )}\n </div>\n );\n}\n' }] }, "cn": { "name": "cn", "title": "cn", "description": "Utility that merges Tailwind CSS classes with clsx + tailwind-merge.", "type": "utility", "registryDependencies": [], "files": [{ "path": "utils/cn.ts", "content": 'import { clsx, type ClassValue } from "clsx";\nimport { twMerge } from "tailwind-merge";\n\nexport function cn(...inputs: ClassValue[]): string {\n return twMerge(clsx(inputs));\n}\n' }] }, "search-input": { "name": "search-input", "title": "SearchInput", "description": "Search bar with debounced results dropdown.", "type": "component", "registryDependencies": ["cn"], "files": [{ "path": "search-input.tsx", "content": '"use client";\n\nimport React, { useCallback, useMemo } from "react";\nimport { Field } from "@base-ui/react/field";\nimport { Input } from "@base-ui/react/input";\nimport { useSearch } from "@cimplify/sdk/react";\nimport type { UseSearchOptions } from "@cimplify/sdk/react";\nimport { cn } from "@cimplify/sdk/react";\nimport { formatPrice } from "@cimplify/sdk";\n\nexport interface SearchInputClassNames {\n root?: string;\n input?: string;\n clearButton?: string;\n results?: string;\n resultItem?: string;\n empty?: string;\n loading?: string;\n resultImage?: string;\n resultName?: string;\n resultPrice?: string;\n categoryFilter?: string;\n}\n\nexport interface SearchInputProps {\n /** Placeholder text for the input. */\n placeholder?: string;\n /** Search options forwarded to useSearch. */\n searchOptions?: UseSearchOptions;\n /** Called when a product result is clicked. */\n onResultClick?: (product: import("../types/product").Product) => void;\n /** Custom result item renderer. */\n renderResult?: (product: import("../types/product").Product) => React.ReactNode;\n /** Show inline results dropdown. Default: true. */\n showResults?: boolean;\n /** Optional category ID to scope search results. */\n categoryId?: string;\n /** Display name for the active category filter. */\n categoryName?: string;\n className?: string;\n classNames?: SearchInputClassNames;\n}\n\nconst SearchIcon = () => (\n <svg\n xmlns="http://www.w3.org/2000/svg"\n width="16"\n height="16"\n viewBox="0 0 24 24"\n fill="none"\n stroke="currentColor"\n strokeWidth="2"\n strokeLinecap="round"\n strokeLinejoin="round"\n aria-hidden="true"\n data-cimplify-search-icon\n >\n <circle cx="11" cy="11" r="8" />\n <line x1="21" y1="21" x2="16.65" y2="16.65" />\n </svg>\n);\n\n/**\n * SearchInput \u2014 search bar with debounced results dropdown.\n *\n * Wraps `useSearch` with a controlled input and optional inline results list.\n * Uses Base UI Field.Root + Input for accessible form semantics.\n */\nexport function SearchInput({\n placeholder = "Search products...",\n searchOptions,\n onResultClick,\n renderResult,\n showResults = true,\n categoryId,\n categoryName,\n className,\n classNames,\n}: SearchInputProps): React.ReactElement {\n const mergedOptions = useMemo<UseSearchOptions>(\n () => (categoryId ? { ...searchOptions, category: categoryId } : searchOptions ?? {}),\n [searchOptions, categoryId],\n );\n const { results, isLoading, query, setQuery, clear } = useSearch(mergedOptions);\n\n const handleValueChange = useCallback(\n (value: string) => {\n setQuery(value);\n },\n [setQuery],\n );\n\n return (\n <Field.Root\n data-cimplify-search\n className={cn(className, classNames?.root)}\n style={{ position: "relative" }}\n >\n <Field.Label className="sr-only">Search products</Field.Label>\n\n <div style={{ position: "relative", display: "flex", alignItems: "center" }}>\n <span\n data-cimplify-search-icon-wrapper\n style={{\n position: "absolute",\n left: "0.5rem",\n pointerEvents: "none",\n display: "flex",\n alignItems: "center",\n }}\n >\n <SearchIcon />\n </span>\n\n <Input\n type="search"\n value={query}\n onValueChange={handleValueChange}\n placeholder={placeholder}\n data-cimplify-search-input\n className={classNames?.input}\n />\n\n {query.length > 0 && (\n <button\n type="button"\n onClick={clear}\n data-cimplify-search-clear\n className={classNames?.clearButton}\n aria-label="Clear search"\n >\n &times;\n </button>\n )}\n </div>\n\n {categoryId && (\n <div data-cimplify-search-category-filter className={classNames?.categoryFilter}>\n Filtered by: {categoryName || categoryId}\n </div>\n )}\n\n {showResults && query.length > 0 && (\n <div data-cimplify-search-results className={classNames?.results}>\n {isLoading && (\n <div data-cimplify-search-loading className={classNames?.loading} aria-busy="true">\n Searching...\n </div>\n )}\n\n {!isLoading && results.length === 0 && query.length >= 2 && (\n <div data-cimplify-search-empty className={classNames?.empty}>\n No results found\n </div>\n )}\n\n {!isLoading &&\n results.map((product) => (\n <button\n key={product.id}\n type="button"\n onClick={() => onResultClick?.(product)}\n data-cimplify-search-result\n className={classNames?.resultItem}\n style={!renderResult ? { display: "flex", alignItems: "center", gap: "0.5rem", width: "100%" } : undefined}\n >\n {renderResult ? (\n renderResult(product)\n ) : (\n <>\n {(product.images?.[0] || product.image_url) && (\n <img\n src={product.images?.[0] || product.image_url}\n alt=""\n data-cimplify-search-result-image\n className={classNames?.resultImage}\n style={{ width: "2rem", height: "2rem", objectFit: "cover", borderRadius: "0.25rem", flexShrink: 0 }}\n />\n )}\n <span data-cimplify-search-result-name className={classNames?.resultName} style={{ flex: 1, textAlign: "left" }}>\n {product.name}\n </span>\n {product.default_price !== undefined && (\n <span data-cimplify-search-result-price className={classNames?.resultPrice} style={{ flexShrink: 0 }}>\n {formatPrice(product.default_price)}\n </span>\n )}\n </>\n )}\n </button>\n ))}\n </div>\n )}\n </Field.Root>\n );\n}\n' }] }, "food-product-card": { "name": "food-product-card", "title": "FoodProductCard", "description": "Product card for food items with tags, badges, and quick-add.", "type": "component", "registryDependencies": ["price", "cn"], "files": [{ "path": "cards/food-product-card.tsx", "content": '"use client";\n\nimport React from "react";\nimport { PRODUCT_TYPE } from "@cimplify/sdk";\nimport type { CardLayoutProps } from "@cimplify/sdk/react";\nimport { CardShell, CardImage, QuickAddButton } from "@cimplify/sdk/react";\nimport { Price } from "@cimplify/sdk/react";\nimport { cn } from "@cimplify/sdk/react";\n\n\nexport function FoodProductCard({\n product,\n onQuickAdd,\n renderImage,\n renderLink,\n className,\n}: CardLayoutProps): React.ReactElement {\n const image = product.image_url || product.images?.[0] || "";\n const tags = product.tags || [];\n const isSignature = product.metadata?.is_signature === true;\n const isNew = product.metadata?.is_new === true;\n const isComposite = product.type === PRODUCT_TYPE.Composite;\n\n return (\n <CardShell product={product} renderLink={renderLink} className={className}>\n <CardImage src={image} alt={product.name} aspectRatio="square" renderImage={renderImage}>\n {/* Badges */}\n {isSignature && (\n <span className="absolute top-3 left-3 inline-flex items-center gap-1 text-[11px] font-semibold tracking-wide bg-amber-50 text-amber-700 border border-amber-200/60 px-2 py-0.5 rounded-md">\n <svg className="w-3 h-3 opacity-70" fill="currentColor" viewBox="0 0 16 16"><path d="M8 .75l1.85 4.95L15 6.2l-3.7 3.2 1.1 5.1L8 12.15 3.6 14.5l1.1-5.1L1 6.2l5.15-.5z" /></svg>\n Signature\n </span>\n )}\n {isNew && !isSignature && (\n <span className="absolute top-3 left-3 text-[11px] font-semibold tracking-wide bg-emerald-50 text-emerald-700 border border-emerald-200/60 px-2 py-0.5 rounded-md">\n New\n </span>\n )}\n {isComposite && (\n <span className="absolute top-3 left-3 text-[11px] font-semibold tracking-wide bg-violet-50 text-violet-700 border border-violet-200/60 px-2 py-0.5 rounded-md">\n Customizable\n </span>\n )}\n\n {/* Quick add */}\n <QuickAddButton\n onClick={() => onQuickAdd?.(product)}\n className="absolute bottom-3 right-3"\n />\n </CardImage>\n\n <div className="p-3.5">\n <h3 className="text-[14.5px] font-semibold text-foreground leading-snug tracking-tight truncate">\n {product.name}\n </h3>\n {product.description && (\n <p className="text-[12.5px] text-muted-foreground mt-1 leading-relaxed line-clamp-2">\n {product.description}\n </p>\n )}\n <div className="flex items-center justify-between mt-3">\n {isComposite ? (\n <span className="text-sm font-bold">From <Price amount={product.default_price} /></span>\n ) : (\n <Price amount={product.default_price} className="text-sm font-bold" />\n )}\n {tags.length > 0 && (\n <div className="flex gap-1">\n {tags.slice(0, 2).map((tag) => (\n <span key={tag} className="text-[10px] font-medium text-muted-foreground px-1.5 py-0.5 bg-muted rounded">\n {tag}\n </span>\n ))}\n </div>\n )}\n </div>\n </div>\n </CardShell>\n );\n}\n' }] }, "search-page": { "name": "search-page", "title": "SearchPage", "description": "Dedicated search page with input and results grid.", "type": "component", "registryDependencies": ["product-grid", "cn"], "files": [{ "path": "search-page.tsx", "content": '"use client";\n\nimport React from "react";\nimport type { Product, Category, PropertyFacet } from "@cimplify/sdk";\nimport { useSearch } from "@cimplify/sdk/react";\nimport type { UseSearchOptions } from "./hooks/use-search";\nimport type { SearchLayoutProps } from "./search-layouts/shared";\nimport { DefaultSearchLayout } from "./search-layouts/default-search-layout";\nimport { CompactSearchLayout } from "./search-layouts/compact-search-layout";\nimport { cn } from "@cimplify/sdk/react";\n\nexport type { SearchLayoutProps };\n\nexport enum SearchTemplate {\n Default = "default",\n Compact = "compact",\n}\n\nexport interface SearchPageClassNames {\n root?: string;\n}\n\nexport interface SearchPageProps {\n /** Explicit search template. */\n template?: SearchTemplate;\n /** Layout map for overrides. AI or developer can provide custom layouts keyed by template name. */\n layouts?: Partial<Record<SearchTemplate | string, React.ComponentType<SearchLayoutProps>>>;\n /** Pre-fetched categories for filters. */\n categories?: Category[];\n /** Pre-fetched property facets for sidebar. */\n facets?: PropertyFacet[];\n /** Search options passed to useSearch. */\n searchOptions?: UseSearchOptions;\n /** Placeholder text for search input. */\n placeholder?: string;\n /** Custom card renderer. */\n renderCard?: (product: Product) => React.ReactNode;\n /** Custom image renderer. */\n renderImage?: (props: { src: string; alt: string; className?: string }) => React.ReactNode;\n /** Custom link renderer. */\n renderLink?: (props: { href: string; className?: string; children: React.ReactNode }) => React.ReactElement;\n /** Quick-add handler. */\n onQuickAdd?: (product: Product) => void;\n className?: string;\n classNames?: SearchPageClassNames;\n}\n\nconst BUILT_IN_LAYOUTS: Record<string, React.ComponentType<SearchLayoutProps>> = {\n [SearchTemplate.Default]: DefaultSearchLayout,\n [SearchTemplate.Compact]: CompactSearchLayout,\n};\n\nexport function SearchPage({\n template,\n layouts,\n categories,\n facets,\n searchOptions,\n placeholder,\n renderCard,\n renderImage,\n renderLink,\n onQuickAdd,\n className,\n classNames,\n}: SearchPageProps): React.ReactElement {\n const search = useSearch(searchOptions);\n\n const key = template ?? SearchTemplate.Default;\n const Layout = layouts?.[key] ?? BUILT_IN_LAYOUTS[key] ?? DefaultSearchLayout;\n\n return (\n <div data-cimplify-search-page className={cn(className, classNames?.root)}>\n <Layout\n search={search}\n facets={facets}\n categories={categories}\n renderCard={renderCard}\n renderImage={renderImage}\n renderLink={renderLink}\n onQuickAdd={onQuickAdd}\n placeholder={placeholder}\n />\n </div>\n );\n}\n' }] }, "food-product-layout": { "name": "food-product-layout", "title": "FoodProductLayout", "description": "Restaurant product layout with allergens, ingredients, pairings, and dietary tags.", "type": "component", "registryDependencies": ["product-image-gallery", "product-customizer", "price", "cn"], "files": [{ "path": "layouts/food-product-layout.tsx", "content": '"use client";\n\nimport React from "react";\nimport type { ProductLayoutProps } from "@cimplify/sdk/react";\nimport {\n TwoColumnGrid,\n RelatedProductsSection,\n MetadataStringList,\n CustomAttributesTable,\n TagPills,\n getMetadataStringList,\n} from "@cimplify/sdk/react";\nimport { ProductImageGallery } from "@cimplify/sdk/react";\nimport { ProductCustomizer } from "@cimplify/sdk/react";\nimport { Price } from "@cimplify/sdk/react";\nimport { parsePrice } from "@cimplify/sdk";\nimport { cn } from "@cimplify/sdk/react";\n\n\nexport function FoodProductLayout({\n product,\n onAddToCart,\n renderBreadcrumb,\n relatedProducts,\n showRelated = true,\n className,\n}: ProductLayoutProps): React.ReactElement {\n const images = product.images || (product.image_url ? [product.image_url] : []);\n const tags = product.tags || [];\n const ingredients = getMetadataStringList(product.metadata, "ingredients");\n const pairings = getMetadataStringList(product.metadata, "pairings");\n\n return (\n <div data-cimplify-product-layout="food" className={cn("space-y-8", className)}>\n {renderBreadcrumb?.(product)}\n\n <TwoColumnGrid\n left={\n <ProductImageGallery\n images={images}\n productName={product.name}\n />\n }\n right={\n <div className="space-y-6">\n {/* Tags + Allergens */}\n <div className="flex flex-wrap gap-1.5">\n {tags.map((tag) => (\n <span key={tag} className="inline-flex items-center gap-1 px-2.5 py-1 bg-green-100 text-green-800 text-xs font-medium rounded-full">\n {tag}\n </span>\n ))}\n {product.allergies && product.allergies.map((allergy) => (\n <span key={allergy} className="inline-flex items-center gap-1 px-2.5 py-1 bg-amber-100 text-amber-800 text-xs font-medium rounded-full">\n Contains {allergy}\n </span>\n ))}\n </div>\n\n {/* Name + Price */}\n <div>\n <h1 data-cimplify-product-layout-name className="text-3xl lg:text-4xl font-bold tracking-tight">\n {product.name}\n </h1>\n <Price amount={product.default_price} className="text-2xl font-semibold mt-2 block" />\n </div>\n\n {/* Quick stats */}\n {(product.calories != null || product.preparation_time_minutes != null) && (\n <div data-cimplify-product-layout-stats className="flex flex-wrap gap-4 py-3 border-y border-border">\n {product.preparation_time_minutes != null && (\n <span className="flex items-center gap-1.5 text-sm text-muted-foreground">\n <strong className="text-foreground">{product.preparation_time_minutes} min</strong> prep time\n </span>\n )}\n {product.calories != null && (\n <span className="flex items-center gap-1.5 text-sm text-muted-foreground">\n <strong className="text-foreground">{product.calories}</strong> calories\n </span>\n )}\n </div>\n )}\n\n {/* Description */}\n {product.description && (\n <p data-cimplify-product-layout-description className="text-muted-foreground leading-relaxed">\n {product.description}\n </p>\n )}\n\n {/* Ingredients + Pairings */}\n {(ingredients.length > 0 || pairings.length > 0) && (\n <div className="grid sm:grid-cols-2 gap-4">\n <MetadataStringList items={ingredients} label="Key Ingredients" />\n <MetadataStringList items={pairings} label="Pairings" />\n </div>\n )}\n\n {/* Custom Attributes */}\n <CustomAttributesTable attributes={product.custom_attributes || []} />\n\n {/* Customizer (variants, add-ons, composites, bundles, billing, volume) */}\n <ProductCustomizer\n product={product}\n onAddToCart={onAddToCart}\n />\n </div>\n }\n />\n\n {showRelated && relatedProducts && relatedProducts.length > 0 && (\n <RelatedProductsSection\n products={relatedProducts}\n title="You may also enjoy"\n />\n )}\n </div>\n );\n}\n' }] }, "catalogue-page": { "name": "catalogue-page", "title": "CataloguePage", "description": "Browse all products with category filtering and search.", "type": "component", "registryDependencies": ["product-grid", "category-filter", "search-input", "cn"], "files": [{ "path": "catalogue-page.tsx", "content": '"use client";\n\nimport React, { useState, useCallback } from "react";\nimport type { Product, Category, PropertyFacet } from "@cimplify/sdk";\nimport { useProducts, useCategories } from "@cimplify/sdk/react";\nimport type { CatalogueLayoutProps } from "./catalogue-layouts/shared";\nimport { DefaultCatalogueLayout } from "./catalogue-layouts/default-catalogue-layout";\nimport { CompactCatalogueLayout } from "./catalogue-layouts/compact-catalogue-layout";\nimport { cn } from "@cimplify/sdk/react";\n\nexport type { CatalogueLayoutProps };\n\nexport enum CatalogueTemplate {\n Default = "default",\n Compact = "compact",\n}\n\nexport interface CataloguePageClassNames {\n root?: string;\n}\n\nexport interface CataloguePageProps {\n /** Explicit template. */\n template?: CatalogueTemplate;\n /** Layout map for overrides. */\n layouts?: Partial<Record<CatalogueTemplate | string, React.ComponentType<CatalogueLayoutProps>>>;\n /** Page title. */\n title?: string;\n /** Pre-fetched products for SSR. */\n products?: Product[];\n /** Pre-fetched categories for SSR. */\n categories?: Category[];\n /** Pre-fetched property facets for sidebar. */\n facets?: PropertyFacet[];\n /** Available tags for filtering. */\n availableTags?: string[];\n /** Products per page. */\n pageSize?: number;\n /** Default sort. */\n defaultSort?: { by: string; order: string };\n /** Custom card renderer. */\n renderCard?: (product: Product) => React.ReactNode;\n /** Custom image renderer. */\n renderImage?: (props: { src: string; alt: string; className?: string }) => React.ReactNode;\n /** Custom link renderer. */\n renderLink?: (props: { href: string; className?: string; children: React.ReactNode }) => React.ReactElement;\n /** Quick-add handler. */\n onQuickAdd?: (product: Product) => void;\n /** Product click handler. */\n onProductClick?: (product: Product) => void;\n className?: string;\n classNames?: CataloguePageClassNames;\n}\n\nconst BUILT_IN_LAYOUTS: Record<string, React.ComponentType<CatalogueLayoutProps>> = {\n [CatalogueTemplate.Default]: DefaultCatalogueLayout,\n [CatalogueTemplate.Compact]: CompactCatalogueLayout,\n};\n\nfunction useDebounce(value: string, delay: number): string {\n const [debounced, setDebounced] = React.useState(value);\n React.useEffect(() => {\n const timer = setTimeout(() => setDebounced(value), delay);\n return () => clearTimeout(timer);\n }, [value, delay]);\n return debounced;\n}\n\nexport function CataloguePage({\n template,\n layouts,\n title,\n products: productsProp,\n categories: categoriesProp,\n facets,\n availableTags,\n pageSize,\n defaultSort,\n renderCard,\n renderImage,\n renderLink,\n onQuickAdd,\n onProductClick,\n className,\n classNames,\n}: CataloguePageProps): React.ReactElement {\n const [selectedCategory, setSelectedCategory] = useState<string | null>(null);\n const [searchQuery, setSearchQuery] = useState("");\n const [sortBy, setSortBy] = useState(defaultSort?.by ?? "created_at");\n const [sortOrder, setSortOrder] = useState(defaultSort?.order ?? "desc");\n const [inStockOnly, setInStockOnly] = useState(false);\n const [selectedTags, setSelectedTags] = useState<string[]>([]);\n const [minPrice, setMinPrice] = useState("");\n const [maxPrice, setMaxPrice] = useState("");\n const [page, setPage] = useState(1);\n\n const debouncedSearch = useDebounce(searchQuery, 300);\n\n const { products: fetchedProducts, isLoading, pagination } = useProducts({\n enabled: productsProp === undefined,\n category: selectedCategory ?? undefined,\n search: debouncedSearch.length >= 2 ? debouncedSearch : undefined,\n sort_by: sortBy as "name" | "price" | "created_at" | "updated_at",\n sort_order: sortOrder as "asc" | "desc",\n in_stock: inStockOnly || undefined,\n tags: selectedTags.length > 0 ? selectedTags : undefined,\n min_price: minPrice ? parseFloat(minPrice) : undefined,\n max_price: maxPrice ? parseFloat(maxPrice) : undefined,\n page,\n limit: pageSize,\n });\n\n const { categories: fetchedCategories } = useCategories({\n enabled: categoriesProp === undefined,\n });\n\n const products = productsProp ?? fetchedProducts;\n const categories = categoriesProp ?? fetchedCategories;\n const totalPages = pagination?.total_pages ?? 1;\n\n const handleCategoryChange = useCallback((id: string | null) => {\n setSelectedCategory(id);\n setPage(1);\n }, []);\n\n const handleSortChange = useCallback((by: string, order: string) => {\n setSortBy(by);\n setSortOrder(order);\n setPage(1);\n }, []);\n\n const handleTagToggle = useCallback((tag: string) => {\n setSelectedTags((prev) =>\n prev.includes(tag) ? prev.filter((t) => t !== tag) : [...prev, tag],\n );\n setPage(1);\n }, []);\n\n const handlePriceRangeChange = useCallback((min: string, max: string) => {\n setMinPrice(min);\n setMaxPrice(max);\n setPage(1);\n }, []);\n\n const key = template ?? CatalogueTemplate.Default;\n const Layout = layouts?.[key] ?? BUILT_IN_LAYOUTS[key] ?? DefaultCatalogueLayout;\n\n return (\n <div data-cimplify-catalogue-page className={cn(className, classNames?.root)}>\n <Layout\n products={products}\n categories={categories}\n facets={facets}\n isLoading={isLoading}\n selectedCategory={selectedCategory}\n onCategoryChange={handleCategoryChange}\n searchQuery={searchQuery}\n onSearchChange={setSearchQuery}\n sortBy={sortBy}\n sortOrder={sortOrder}\n onSortChange={handleSortChange}\n inStockOnly={inStockOnly}\n onInStockChange={setInStockOnly}\n selectedTags={selectedTags}\n availableTags={availableTags}\n onTagToggle={handleTagToggle}\n minPrice={minPrice}\n maxPrice={maxPrice}\n onPriceRangeChange={handlePriceRangeChange}\n page={page}\n totalPages={totalPages}\n onPageChange={setPage}\n renderCard={renderCard}\n renderImage={renderImage}\n renderLink={renderLink}\n onQuickAdd={onQuickAdd}\n onProductClick={onProductClick}\n title={title}\n />\n </div>\n );\n}\n' }] }, "default-product-layout": { "name": "default-product-layout", "title": "DefaultProductLayout", "description": "Two-column product layout for retail/physical products with sale badges, specs, and properties.", "type": "component", "registryDependencies": ["product-image-gallery", "product-customizer", "price", "price-range", "availability-badge", "sale-badge", "cn"], "files": [{ "path": "layouts/default-product-layout.tsx", "content": '"use client";\n\nimport React from "react";\nimport type { ProductLayoutProps } from "@cimplify/sdk/react";\nimport {\n TwoColumnGrid,\n RelatedProductsSection,\n CustomAttributesTable,\n PropertiesTable,\n TagPills,\n InventoryBadge,\n} from "@cimplify/sdk/react";\nimport { ProductImageGallery } from "@cimplify/sdk/react";\nimport { ProductCustomizer } from "@cimplify/sdk/react";\nimport { Price } from "@cimplify/sdk/react";\nimport { PriceRange } from "@cimplify/sdk/react";\nimport { AvailabilityBadge } from "@cimplify/sdk/react";\nimport { SaleBadge } from "@cimplify/sdk/react";\nimport { parsePrice, isOnSale, getBasePrice, getDiscountPercentage } from "@cimplify/sdk";\nimport { cn } from "@cimplify/sdk/react";\n\nexport function DefaultProductLayout({\n product,\n onAddToCart,\n renderBreadcrumb,\n relatedProducts,\n showRelated = true,\n className,\n}: ProductLayoutProps): React.ReactElement {\n const images = product.images || (product.image_url ? [product.image_url] : []);\n const hasRange = (product.quantity_pricing && product.quantity_pricing.length > 1)\n || (product.variants && product.variants.length > 1);\n const onSale = isOnSale(product);\n const hasPhysicalDetails = product.sku || product.vendor || product.material;\n\n return (\n <div data-cimplify-product-layout="default" className={cn("space-y-8", className)}>\n {renderBreadcrumb?.(product)}\n\n <TwoColumnGrid\n left={\n <ProductImageGallery\n images={images}\n productName={product.name}\n />\n }\n right={\n <div className="space-y-6">\n {/* Brand + Tags */}\n <div className="flex flex-wrap items-center gap-2">\n {product.vendor && (\n <span className="text-xs font-semibold text-muted-foreground uppercase tracking-widest">\n {product.vendor}\n </span>\n )}\n <TagPills tags={product.tags || []} />\n </div>\n\n {/* Name */}\n <h1 data-cimplify-product-layout-name className="text-3xl lg:text-4xl font-extrabold tracking-tight">\n {product.name}\n </h1>\n\n {/* Price */}\n <div className="flex items-baseline gap-3">\n {hasRange ? (\n <PriceRange product={product} className="text-2xl font-extrabold" />\n ) : onSale ? (\n <>\n <Price amount={product.default_price} className="text-2xl font-extrabold" />\n <Price amount={getBasePrice(product)} className="text-lg text-muted-foreground line-through" />\n <span className="text-sm font-bold text-destructive bg-destructive/10 px-2 py-0.5">\n Save {getDiscountPercentage(product)}%\n </span>\n </>\n ) : (\n <Price amount={product.default_price} className="text-2xl font-semibold" />\n )}\n </div>\n\n {/* Inventory */}\n <InventoryBadge product={product} />\n\n {/* Description */}\n {product.description && (\n <p data-cimplify-product-layout-description className="text-muted-foreground leading-relaxed">\n {product.description}\n </p>\n )}\n\n {/* Physical product details */}\n {hasPhysicalDetails && (\n <div data-cimplify-product-layout-details className="space-y-1 text-sm text-muted-foreground">\n {product.sku && <p>SKU: <span className="font-mono text-foreground">{product.sku}</span></p>}\n {product.barcode && <p>Barcode: <span className="font-mono text-foreground">{product.barcode}</span></p>}\n {product.material && <p>Material: <span className="text-foreground">{product.material}</span></p>}\n {product.vendor && <p>Brand: <span className="text-foreground">{product.vendor}</span></p>}\n {(product.length_mm || product.width_mm || product.height_mm) && (\n <p>Dimensions: <span className="text-foreground">\n {[product.length_mm, product.width_mm, product.height_mm]\n .filter(Boolean)\n .map((d) => `${(d! / 10).toFixed(1)} cm`)\n .join(" x ")}\n </span></p>\n )}\n {product.item_condition && product.item_condition !== "new" && (\n <p>Condition: <span className="text-foreground capitalize">{product.item_condition}</span></p>\n )}\n </div>\n )}\n\n {/* Custom Attributes */}\n <CustomAttributesTable attributes={product.custom_attributes || []} />\n\n {/* Properties */}\n <PropertiesTable properties={product.properties || []} />\n\n {/* Customizer */}\n <ProductCustomizer\n product={product}\n onAddToCart={onAddToCart}\n />\n </div>\n }\n />\n\n {showRelated && relatedProducts && relatedProducts.length > 0 && (\n <RelatedProductsSection\n products={relatedProducts}\n />\n )}\n </div>\n );\n}\n' }] }, "account": { "name": "account", "title": "CimplifyAccount", "description": "Iframe wrapper for the Cimplify account portal \u2014 sign-in, orders, addresses, settings.", "type": "component", "registryDependencies": ["cn"], "files": [{ "path": "account.tsx", "content": '"use client";\n\nimport React, { useEffect, useRef, useState, useMemo } from "react";\nimport type { CimplifyClient } from "../client";\nimport { useOptionalCimplify } from "@cimplify/sdk/react";\nimport { cn } from "@cimplify/sdk/react";\n\nexport interface CimplifyAccountProps {\n /** CimplifyClient instance. Falls back to provider context. */\n client?: CimplifyClient;\n /** Override the Link base URL. */\n linkUrl?: string;\n /** Initial section to show: "dashboard" | "orders" | "addresses" | "payment-methods" | "sessions" | "settings". */\n section?: string;\n /** Appearance variables \u2014 passed to Link for theming. */\n appearance?: {\n theme?: "light" | "dark";\n variables?: {\n primaryColor?: string;\n fontFamily?: string;\n borderRadius?: string;\n };\n };\n /** Called when the user logs out from the account portal. */\n onLogout?: () => void;\n /** Additional CSS class for the container. */\n className?: string;\n}\n\nconst DEFAULT_LINK_URL = "https://link.cimplify.io";\n\nexport function CimplifyAccount({\n client: clientProp,\n linkUrl,\n section,\n appearance,\n onLogout,\n className,\n}: CimplifyAccountProps): React.ReactElement {\n const context = useOptionalCimplify();\n const client = clientProp ?? context?.client;\n const resolvedLinkUrl = linkUrl || DEFAULT_LINK_URL;\n\n const iframeRef = useRef<HTMLIFrameElement | null>(null);\n const [height, setHeight] = useState(400);\n const [isReady, setIsReady] = useState(false);\n\n const iframeSrc = useMemo(() => {\n const path = section ? `/elements/account/${section}` : "/elements/account";\n const url = new URL(path, resolvedLinkUrl);\n if (client) {\n const businessId = client.getBusinessId?.() ?? "";\n if (businessId) url.searchParams.set("businessId", businessId);\n }\n return url.toString();\n }, [resolvedLinkUrl, section, client]);\n\n // Listen for messages from the iframe\n useEffect(() => {\n function handleMessage(event: MessageEvent) {\n if (!event.data || typeof event.data !== "object") return;\n\n // Only accept messages from our iframe\n if (iframeRef.current?.contentWindow && event.source !== iframeRef.current.contentWindow) {\n return;\n }\n\n switch (event.data.type) {\n case "ready":\n setIsReady(true);\n if (typeof event.data.height === "number") {\n setHeight(event.data.height);\n }\n // Send init with token + appearance\n sendInit();\n break;\n\n case "height_change":\n if (typeof event.data.height === "number") {\n setHeight(event.data.height);\n }\n break;\n\n case "logout_complete":\n onLogout?.();\n break;\n }\n }\n\n window.addEventListener("message", handleMessage);\n return () => window.removeEventListener("message", handleMessage);\n }, [appearance, client, onLogout]);\n\n function sendInit() {\n const contentWindow = iframeRef.current?.contentWindow;\n if (!contentWindow) return;\n\n const token = client?.getAccessToken?.();\n contentWindow.postMessage(\n {\n type: "init",\n token: token ?? undefined,\n appearance: appearance ?? undefined,\n },\n resolvedLinkUrl,\n );\n }\n\n return (\n <div\n data-cimplify-account\n className={cn("relative overflow-hidden", className)}\n >\n {!isReady && (\n <div className="flex items-center justify-center py-16">\n <div className="w-6 h-6 border-2 border-border border-t-foreground rounded-full animate-spin" />\n </div>\n )}\n <iframe\n ref={iframeRef}\n src={iframeSrc}\n style={{\n border: "none",\n width: "100%",\n height: `${height}px`,\n display: isReady ? "block" : "none",\n overflow: "hidden",\n background: "transparent",\n }}\n allow="geolocation"\n sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox"\n />\n </div>\n );\n}\n' }] }, "subscription-card": { "name": "subscription-card", "title": "SubscriptionCard", "description": "Subscription card with billing plan options, trial badge, and setup fee.", "type": "component", "registryDependencies": ["price", "cn"], "files": [{ "path": "cards/subscription-card.tsx", "content": '"use client";\n\nimport React from "react";\nimport type { ServiceCardLayoutProps } from "./standard-service-card";\nimport { CardShell, CardImage } from "@cimplify/sdk/react";\nimport { Price } from "@cimplify/sdk/react";\nimport { cn } from "@cimplify/sdk/react";\n\nconst FREQUENCY_LABELS: Record<string, string> = {\n weekly: "/week",\n biweekly: "/2wk",\n monthly: "/mo",\n quarterly: "/qtr",\n annually: "/yr",\n};\n\nexport function SubscriptionCard({\n product,\n onBook,\n renderImage,\n renderLink,\n className,\n}: ServiceCardLayoutProps): React.ReactElement {\n const image = product.image_url || product.images?.[0] || "";\n const plans = product.billing_plans || [];\n const primaryPlan = plans[0];\n const frequency = primaryPlan?.frequency\n ? FREQUENCY_LABELS[primaryPlan.frequency] || `/${primaryPlan.frequency}`\n : "/mo";\n const hasTrial = primaryPlan?.trial_days != null && primaryPlan.trial_days > 0;\n const tags = product.tags || [];\n\n return (\n <CardShell product={product} renderLink={renderLink} className={className}>\n <CardImage src={image} alt={product.name} aspectRatio="16/9" renderImage={renderImage}>\n <div className="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent pointer-events-none" />\n\n {/* Price on image */}\n <span className="absolute bottom-3 right-3 text-white font-bold text-lg drop-shadow-sm">\n <Price amount={product.default_price} />{frequency}\n </span>\n\n {/* Trial badge */}\n {hasTrial && (\n <span className="absolute top-3 left-3 text-[11px] font-semibold tracking-wide bg-emerald-50/90 text-emerald-700 border border-emerald-200/50 backdrop-blur px-2 py-0.5 rounded-md">\n {primaryPlan!.trial_days}-day free trial\n </span>\n )}\n </CardImage>\n\n <div className="p-4">\n <h3 className="text-[14.5px] font-bold text-foreground leading-snug tracking-tight">\n {product.name}\n </h3>\n {product.description && (\n <p className="text-[12.5px] text-muted-foreground mt-1 leading-relaxed line-clamp-2">\n {product.description}\n </p>\n )}\n\n {/* Tags */}\n {tags.length > 0 && (\n <div className="flex flex-wrap gap-1 mt-2.5">\n {tags.slice(0, 3).map((tag) => (\n <span key={tag} className="text-[10px] font-medium text-muted-foreground px-1.5 py-0.5 bg-muted rounded">\n {tag}\n </span>\n ))}\n </div>\n )}\n\n {/* Billing plan options */}\n {plans.length > 1 && (\n <div className="mt-3 space-y-1">\n {plans.slice(0, 3).map((plan) => {\n const label = plan.frequency\n ? plan.frequency.charAt(0).toUpperCase() + plan.frequency.slice(1)\n : "Standard";\n const hasMarkup = plan.markup_type && plan.markup_amount;\n const isSavings = plan.markup_type === "percentage" && plan.markup_amount != null && plan.markup_amount < 0;\n\n return (\n <div key={plan.id} className="flex items-center justify-between text-[11px]">\n <span className="text-muted-foreground">{label}</span>\n <span className="font-medium">\n {hasMarkup && isSavings && (\n <span className="text-emerald-600 mr-1">Save {Math.abs(plan.markup_amount!)}%</span>\n )}\n </span>\n </div>\n );\n })}\n </div>\n )}\n\n <div className="flex items-center justify-between mt-3 pt-3 border-t border-border">\n <div className="flex items-center gap-2 text-[11px] text-muted-foreground">\n {hasTrial && (\n <span className="text-emerald-600 font-medium">Free trial</span>\n )}\n {primaryPlan?.setup_fee != null && primaryPlan.setup_fee > 0 && (\n <span><Price amount={primaryPlan.setup_fee} /> setup</span>\n )}\n </div>\n <button\n onClick={(e) => { e.preventDefault(); e.stopPropagation(); onBook?.(product); }}\n className="text-[13px] font-semibold text-primary hover:text-primary/80 transition-colors"\n >\n Subscribe &rarr;\n </button>\n </div>\n </div>\n </CardShell>\n );\n}\n' }] }, "product-image-gallery": { "name": "product-image-gallery", "title": "ProductImageGallery", "description": "Main image with thumbnail strip for product images.", "type": "component", "registryDependencies": [], "files": [{ "path": "product-image-gallery.tsx", "content": '"use client";\n\nimport React, { useEffect, useMemo, useState } from "react";\nimport { RadioGroup } from "@base-ui/react/radio-group";\nimport { Radio } from "@base-ui/react/radio";\n\nexport interface ProductImageGalleryProps {\n images: string[];\n productName: string;\n aspectRatio?: "square" | "4/3" | "16/10" | "3/4";\n className?: string;\n}\n\nconst ASPECT_STYLES: Record<string, React.CSSProperties> = {\n square: { aspectRatio: "1/1" },\n "4/3": { aspectRatio: "4/3" },\n "16/10": { aspectRatio: "16/10" },\n "3/4": { aspectRatio: "3/4" },\n};\n\n/**\n * ProductImageGallery \u2014 main image + thumbnail strip.\n *\n * Uses plain `<img>` for framework-agnostic rendering (not Next.js Image).\n */\nexport function ProductImageGallery({\n images,\n productName,\n aspectRatio = "4/3",\n className,\n}: ProductImageGalleryProps): React.ReactElement | null {\n const normalizedImages = useMemo(\n () =>\n images.filter(\n (image): image is string =>\n typeof image === "string" && image.trim().length > 0,\n ),\n [images],\n );\n\n const [selectedImage, setSelectedImage] = useState(0);\n\n useEffect(() => {\n setSelectedImage(0);\n }, [normalizedImages.length, productName]);\n\n if (normalizedImages.length === 0) {\n return null;\n }\n\n const activeImage = normalizedImages[selectedImage] || normalizedImages[0];\n\n return (\n <div data-cimplify-image-gallery className={className}>\n <div\n data-cimplify-image-gallery-main\n style={{ position: "relative", overflow: "hidden", ...ASPECT_STYLES[aspectRatio] }}\n >\n <img\n src={activeImage}\n alt={productName}\n style={{ width: "100%", height: "100%", objectFit: "cover" }}\n data-cimplify-image-gallery-active\n />\n </div>\n\n {normalizedImages.length > 1 && (\n <RadioGroup\n aria-label={`${productName} image thumbnails`}\n value={String(selectedImage)}\n onValueChange={(value) => setSelectedImage(Number(value))}\n data-cimplify-image-gallery-thumbnails\n style={{ display: "flex", gap: "0.5rem", marginTop: "0.75rem" }}\n >\n {normalizedImages.map((image, index) => {\n const isSelected = selectedImage === index;\n return (\n <Radio.Root\n key={`${image}-${index}`}\n value={String(index)}\n data-cimplify-image-gallery-thumb\n data-selected={isSelected || undefined}\n style={{\n width: "4rem",\n height: "4rem",\n overflow: "hidden",\n padding: 0,\n border: "none",\n cursor: "pointer",\n }}\n >\n <img\n src={image}\n alt=""\n style={{ width: "100%", height: "100%", objectFit: "cover" }}\n />\n </Radio.Root>\n );\n })}\n </RadioGroup>\n )}\n </div>\n );\n}\n' }] }, "order-summary": { "name": "order-summary", "title": "OrderSummary", "description": "Single order detail view with line items and totals.", "type": "component", "registryDependencies": ["price", "cn"], "files": [{ "path": "order-summary.tsx", "content": '"use client";\n\nimport React from "react";\nimport type { Order, LineItem, OrderStatus, PaymentState } from "@cimplify/sdk";\nimport { useOrder } from "@cimplify/sdk/react";\nimport { Price } from "@cimplify/sdk/react";\nimport { parsePrice } from "@cimplify/sdk";\nimport { cn } from "@cimplify/sdk/react";\n\nexport interface OrderSummaryClassNames {\n root?: string;\n header?: string;\n orderId?: string;\n status?: string;\n paymentState?: string;\n fulfillmentDetails?: string;\n deliveryAddress?: string;\n items?: string;\n lineItem?: string;\n notes?: string;\n totals?: string;\n customer?: string;\n loading?: string;\n serviceInfo?: string;\n digitalInfo?: string;\n bundleBreakdown?: string;\n compositeBreakdown?: string;\n trackingLink?: string;\n reorderButton?: string;\n}\n\nexport interface OrderSummaryProps {\n /** Pass an Order object directly (skips fetch). */\n order?: Order;\n /** Or pass an order ID to fetch via useOrder. */\n orderId?: string;\n /** Poll for status updates. */\n poll?: boolean;\n /** Custom line item renderer. */\n renderLineItem?: (item: LineItem) => React.ReactNode;\n /** Called when the reorder button is clicked. */\n onReorder?: (order: Order) => void;\n /** Called when the order status changes during polling. */\n onStatusChange?: (previousStatus: OrderStatus, newStatus: OrderStatus, order: Order) => void;\n className?: string;\n classNames?: OrderSummaryClassNames;\n}\n\nconst STATUS_LABELS: Record<OrderStatus, string> = {\n pending: "Pending",\n created: "Created",\n confirmed: "Confirmed",\n in_preparation: "In Preparation",\n ready_to_serve: "Ready",\n partially_served: "Partially Served",\n served: "Served",\n delivered: "Delivered",\n picked_up: "Picked Up",\n completed: "Completed",\n cancelled: "Cancelled",\n refunded: "Refunded",\n};\n\nconst PAYMENT_STATE_LABELS: Record<PaymentState, string> = {\n not_paid: "Not Paid",\n paid: "Paid",\n partially_refunded: "Partially Refunded",\n refunded: "Refunded",\n};\n\n/**\n * OrderSummary \u2014 displays a single order\'s details, line items, and totals.\n *\n * Accepts either a pre-loaded `order` object or an `orderId` to fetch.\n * Supports polling for live status updates.\n */\nexport function OrderSummary({\n order: orderProp,\n orderId,\n poll = false,\n renderLineItem,\n onReorder,\n onStatusChange,\n className,\n classNames,\n}: OrderSummaryProps): React.ReactElement {\n const { order: fetched, isLoading } = useOrder(orderProp ? null : orderId, {\n enabled: !orderProp && !!orderId,\n poll,\n onStatusChange,\n });\n\n const order = orderProp ?? fetched;\n\n if (isLoading && !order) {\n return (\n <div\n data-cimplify-order-summary\n aria-busy="true"\n className={cn(className, classNames?.root, classNames?.loading)}\n >\n <div data-cimplify-order-summary-skeleton />\n </div>\n );\n }\n\n if (!order) {\n return (\n <div data-cimplify-order-summary className={cn(className, classNames?.root)}>\n <p>Order not found.</p>\n </div>\n );\n }\n\n return (\n <div data-cimplify-order-summary className={cn(className, classNames?.root)}>\n {/* Header */}\n <div data-cimplify-order-header className={classNames?.header}>\n <span data-cimplify-order-id className={classNames?.orderId}>\n Order #{order.user_friendly_id}\n </span>\n <span\n data-cimplify-order-status\n data-status={order.status}\n className={classNames?.status}\n >\n {STATUS_LABELS[order.status] ?? order.status}\n </span>\n {order.payment_state && (\n <span\n data-cimplify-order-payment-state\n data-payment-state={order.payment_state}\n className={classNames?.paymentState}\n >\n {PAYMENT_STATE_LABELS[order.payment_state] ?? order.payment_state}\n </span>\n )}\n </div>\n\n {/* Fulfillment details */}\n {order.order_type === "pickup" && order.pickup_time && (\n <div data-cimplify-order-fulfillment className={classNames?.fulfillmentDetails}>\n <span>Pickup time: {new Date(order.pickup_time).toLocaleString()}</span>\n </div>\n )}\n {order.order_type === "dine-in" && order.table_number && (\n <div data-cimplify-order-fulfillment className={classNames?.fulfillmentDetails}>\n <span>Table: {order.table_number}</span>\n </div>\n )}\n\n {/* Date */}\n <time data-cimplify-order-date dateTime={order.created_at}>\n {new Date(order.created_at).toLocaleDateString(undefined, {\n year: "numeric",\n month: "long",\n day: "numeric",\n hour: "2-digit",\n minute: "2-digit",\n })}\n </time>\n\n {/* Customer info */}\n {(order.customer_name || order.customer_email) && (\n <div data-cimplify-order-customer className={classNames?.customer}>\n {order.customer_name && (\n <span data-cimplify-order-customer-name>{order.customer_name}</span>\n )}\n {order.customer_email && (\n <span data-cimplify-order-customer-email>{order.customer_email}</span>\n )}\n </div>\n )}\n\n {/* Delivery address */}\n {order.delivery_address && (\n <div data-cimplify-order-delivery-address className={classNames?.deliveryAddress}>\n <span>Delivery address</span>\n <p>{order.delivery_address}</p>\n </div>\n )}\n\n {/* Line items */}\n <div data-cimplify-order-items className={classNames?.items}>\n {order.items.map((item) =>\n renderLineItem ? (\n <React.Fragment key={item.id}>{renderLineItem(item)}</React.Fragment>\n ) : (\n <div key={item.id} data-cimplify-order-line-item className={classNames?.lineItem}>\n <div data-cimplify-order-line-info>\n <span data-cimplify-order-line-qty>{item.quantity}&times;</span>\n <span data-cimplify-order-line-key>{item.line_key}</span>\n </div>\n <Price amount={item.price} />\n {item.fulfillment_type === "digital" && item.fulfillment_id && (\n <a\n href={item.fulfillment_id}\n target="_blank"\n rel="noopener noreferrer"\n data-cimplify-order-line-download\n className="text-sm text-primary underline"\n >\n Download\n </a>\n )}\n\n {/* Service items: show scheduling and confirmation details */}\n {item.configuration.type === "service" && (\n <div data-cimplify-order-service-info className={classNames?.serviceInfo}>\n {item.configuration.scheduled_start && (\n <span>{"\\u{1F4C5}"} {new Date(item.configuration.scheduled_start).toLocaleDateString()} at {new Date(item.configuration.scheduled_start).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}</span>\n )}\n {item.configuration.confirmation_code && (\n <span>Confirmation: {item.configuration.confirmation_code}</span>\n )}\n {item.configuration.service_status && (\n <span data-status={item.configuration.service_status}>{item.configuration.service_status}</span>\n )}\n </div>\n )}\n\n {/* Digital items: instant delivery badge */}\n {item.configuration.type === "digital" && (\n <div data-cimplify-order-digital-info className={classNames?.digitalInfo}>\n <span>{"\\u26A1"} Instant digital delivery</span>\n </div>\n )}\n\n {/* Bundle items: component breakdown */}\n {item.configuration.type === "bundle" && item.configuration.resolved && (\n <div data-cimplify-order-bundle-breakdown className={classNames?.bundleBreakdown}>\n {item.configuration.resolved.selections.map((sel) => (\n <div key={sel.component_id} data-product-type={sel.product_type}>\n <span>{sel.quantity}&times; {sel.product_name}</span>\n {sel.product_type === "service" && sel.scheduling?.scheduled_start && (\n <span> &mdash; {new Date(sel.scheduling.scheduled_start).toLocaleDateString()}</span>\n )}\n {sel.product_type === "digital" && <span> &mdash; Digital</span>}\n </div>\n ))}\n </div>\n )}\n\n {/* Composite items: component breakdown with pricing */}\n {item.configuration.type === "composite" && item.configuration.resolved && (\n <div data-cimplify-order-composite-breakdown className={classNames?.compositeBreakdown}>\n {item.configuration.resolved.selections.map((sel) => (\n <div key={sel.component_id} data-product-type={sel.product_type}>\n <span>{sel.quantity}&times; {sel.component_name}</span>\n {sel.product_type === "service" && sel.scheduling?.scheduled_start && (\n <span> &mdash; {new Date(sel.scheduling.scheduled_start).toLocaleDateString()}</span>\n )}\n <Price amount={sel.unit_price} />\n </div>\n ))}\n </div>\n )}\n </div>\n ),\n )}\n </div>\n\n {/* Customer notes / special instructions */}\n {order.customer_notes && order.customer_notes.length > 0 && (\n <div data-cimplify-order-notes className={classNames?.notes}>\n <span>Notes</span>\n {order.customer_notes.map((note, index) => (\n <p key={index} data-cimplify-order-note>{note}</p>\n ))}\n </div>\n )}\n\n {/* Totals */}\n <div data-cimplify-order-totals className={classNames?.totals}>\n {order.total_discount != null && parsePrice(order.total_discount) !== 0 && (\n <div data-cimplify-order-discount>\n <span>Discount</span>\n <Price amount={order.total_discount} prefix="-" />\n </div>\n )}\n {order.delivery_fee != null && parsePrice(order.delivery_fee) > 0 && (\n <div data-cimplify-order-delivery-fee>\n <span>Delivery fee</span>\n <Price amount={order.delivery_fee} />\n </div>\n )}\n {order.service_charge != null && parsePrice(order.service_charge) !== 0 && (\n <div data-cimplify-order-service-charge>\n <span>Service charge</span>\n <Price amount={order.service_charge} />\n </div>\n )}\n {order.tax != null && parsePrice(order.tax) !== 0 && (\n <div data-cimplify-order-tax>\n <span>Tax</span>\n <Price amount={order.tax} />\n </div>\n )}\n <div data-cimplify-order-total>\n <span>Total</span>\n <Price amount={order.total_price} />\n </div>\n </div>\n\n {/* Tracking */}\n {order.tracking_link && (\n <a\n href={order.tracking_link}\n target="_blank"\n rel="noopener noreferrer"\n data-cimplify-order-tracking\n className={classNames?.trackingLink}\n >\n Track your order\n </a>\n )}\n\n {onReorder && (\n <button\n type="button"\n onClick={() => onReorder(order)}\n data-cimplify-order-reorder\n className={classNames?.reorderButton}\n >\n Reorder\n </button>\n )}\n </div>\n );\n}\n' }] }, "schedule-service-card": { "name": "schedule-service-card", "title": "ScheduleServiceCard", "description": "Service card with next available time slot pills.", "type": "component", "registryDependencies": ["price", "cn"], "files": [{ "path": "cards/schedule-service-card.tsx", "content": '"use client";\n\nimport React from "react";\nimport type { ServiceCardLayoutProps } from "./standard-service-card";\nimport { Price } from "@cimplify/sdk/react";\nimport { cn } from "@cimplify/sdk/react";\nimport { formatDuration } from "../utils/format-duration";\n\nexport function ScheduleServiceCard({\n product,\n slots,\n onBook,\n renderImage,\n renderLink,\n className,\n}: ServiceCardLayoutProps): React.ReactElement {\n const image = product.image_url || product.images?.[0] || "";\n const href = `/products/${product.slug}`;\n const durationLabel = product.duration_minutes != null\n ? formatDuration(product.duration_minutes, product.duration_unit)\n : null;\n\n const displaySlots = (slots || []).slice(0, 3);\n\n const shellClass = cn(\n "group block text-left cursor-pointer bg-card rounded-[14px] overflow-hidden",\n "shadow-[0_1px_3px_rgba(0,0,0,0.04),0_6px_24px_rgba(0,0,0,0.06)]",\n "transition-all duration-300 ease-[cubic-bezier(0.19,1,0.22,1)]",\n "hover:-translate-y-1 hover:shadow-[0_2px_6px_rgba(0,0,0,0.04),0_12px_40px_rgba(0,0,0,0.10)]",\n className,\n );\n\n const inner = (\n <div className="p-4">\n <div className="flex items-start justify-between gap-3">\n <div className="flex-1 min-w-0">\n <h3 className="text-[14.5px] font-bold text-foreground leading-snug tracking-tight">\n {product.name}\n </h3>\n <p className="text-[12.5px] text-muted-foreground mt-0.5">\n {durationLabel && <>{durationLabel} \xB7 </>}\n <Price amount={product.default_price} />\n </p>\n </div>\n {image && (\n <div className="w-14 h-14 rounded-xl overflow-hidden bg-muted shrink-0">\n {renderImage ? (\n renderImage({ src: image, alt: product.name, className: "w-full h-full object-cover" })\n ) : (\n <img src={image} alt={product.name} className="w-full h-full object-cover" />\n )}\n </div>\n )}\n </div>\n\n {product.description && (\n <p className="text-[12px] text-muted-foreground mt-2 line-clamp-2 leading-relaxed">\n {product.description}\n </p>\n )}\n\n {displaySlots.length > 0 && (\n <div className="mt-4 pt-3 border-t border-border">\n <p className="text-[10px] font-semibold text-muted-foreground uppercase tracking-widest mb-2">\n {product.general_service_capacity != null && product.general_service_capacity > 1\n ? "Upcoming classes"\n : "Next available"\n }\n </p>\n <div className="flex gap-2 flex-wrap">\n {displaySlots.map((slot, i) => (\n <button\n key={i}\n onClick={(e) => { e.preventDefault(); e.stopPropagation(); onBook?.(product); }}\n className={cn(\n "text-[12.5px] font-medium px-3 py-1.5 rounded-lg transition-all duration-200 ease-out hover:scale-[1.04]",\n i === 0\n ? "bg-primary text-primary-foreground font-semibold shadow-sm"\n : "bg-muted text-foreground hover:bg-muted/80",\n )}\n >\n {new Date(slot.start_time).toLocaleString(undefined, {\n weekday: "short",\n hour: "numeric",\n minute: "2-digit",\n })}\n </button>\n ))}\n </div>\n </div>\n )}\n </div>\n );\n\n if (renderLink) {\n return renderLink({ href, className: shellClass, children: inner });\n }\n\n return <a href={href} className={shellClass} data-cimplify-card>{inner}</a>;\n}\n' }] }, "recently-viewed": { "name": "recently-viewed", "title": "RecentlyViewed", "description": "Horizontally scrollable rail of recently viewed products, hydrated from local activity state.", "type": "component", "registryDependencies": ["product-card", "cn"], "files": [{ "path": "recently-viewed.tsx", "content": '"use client";\n\nimport React from "react";\nimport { useActivityState } from "./hooks/use-activity-state";\nimport { cn } from "@cimplify/sdk/react";\n\ninterface ViewedProduct {\n product_id: string;\n product_name?: string;\n category_id?: string;\n}\n\nexport interface RecentlyViewedClassNames {\n root?: string;\n item?: string;\n empty?: string;\n loading?: string;\n}\n\nexport interface RecentlyViewedProps {\n /** Maximum number of viewed products to display. */\n limit?: number;\n /** Called when a viewed product is clicked. */\n onProductClick?: (product: ViewedProduct) => void;\n /** Custom product renderer. */\n renderProduct?: (product: ViewedProduct) => React.ReactNode;\n /** Text shown when no viewed products exist. */\n emptyMessage?: string;\n className?: string;\n classNames?: RecentlyViewedClassNames;\n}\n\n/**\n * RecentlyViewed \u2014 displays products the user has recently viewed during their session.\n *\n * Extracts `state.activity.viewed_products` from the activity state.\n * Returns `null` when there are no viewed products.\n */\nexport function RecentlyViewed({\n limit,\n onProductClick,\n renderProduct,\n emptyMessage,\n className,\n classNames,\n}: RecentlyViewedProps): React.ReactElement | null {\n const { state, isLoading } = useActivityState();\n\n const rawViewed = state?.activity?.viewed_products ?? [];\n const viewed = limit ? rawViewed.slice(0, limit) : rawViewed;\n\n if (isLoading && viewed.length === 0) {\n return (\n <div\n data-cimplify-recently-viewed\n aria-busy="true"\n className={cn(className, classNames?.root, classNames?.loading)}\n />\n );\n }\n\n if (viewed.length === 0) {\n return null;\n }\n\n return (\n <div data-cimplify-recently-viewed className={cn(className, classNames?.root)}>\n {viewed.map((product) => (\n <button\n key={product.product_id}\n type="button"\n onClick={() => onProductClick?.(product)}\n data-cimplify-recently-viewed-item\n className={classNames?.item}\n >\n {renderProduct ? (\n renderProduct(product)\n ) : (\n <span>{product.product_name ?? product.product_id}</span>\n )}\n </button>\n ))}\n </div>\n );\n}\n' }] }, "bundle-selector": { "name": "bundle-selector", "title": "BundleSelector", "description": "Bundle component picker with variant choices and price summary.", "type": "component", "registryDependencies": ["price"], "files": [{ "path": "bundle-selector.tsx", "content": '"use client";\n\nimport React, { useState, useCallback, useMemo, useEffect, useRef, useId } from "react";\nimport { RadioGroup } from "@base-ui/react/radio-group";\nimport { Radio } from "@base-ui/react/radio";\nimport type { BundleComponentView, BundleComponentVariantView, BundlePriceType, DurationUnit } from "@cimplify/sdk";\nimport type { Money } from "@cimplify/sdk";\nimport type { BundleSelectionInput } from "@cimplify/sdk";\nimport { parsePrice } from "@cimplify/sdk";\nimport { Price } from "@cimplify/sdk/react";\nimport { cn } from "@cimplify/sdk/react";\n\nexport interface BundleSelectorClassNames {\n root?: string;\n heading?: string;\n components?: string;\n component?: string;\n componentHeader?: string;\n componentQty?: string;\n componentName?: string;\n typeBadge?: string;\n serviceBadge?: string;\n digitalBadge?: string;\n variantPicker?: string;\n variantOption?: string;\n variantOptionSelected?: string;\n variantAdjustment?: string;\n summary?: string;\n savings?: string;\n}\n\nexport interface BundleSelectorProps {\n components: BundleComponentView[];\n bundlePrice?: Money;\n discountValue?: Money;\n pricingType?: BundlePriceType;\n onSelectionsChange: (selections: BundleSelectionInput[]) => void;\n onPriceChange?: (price: number) => void;\n onReady?: (ready: boolean) => void;\n className?: string;\n classNames?: BundleSelectorClassNames;\n}\n\nexport function BundleSelector({\n components,\n bundlePrice,\n discountValue,\n pricingType,\n onSelectionsChange,\n onPriceChange,\n onReady,\n className,\n classNames,\n}: BundleSelectorProps): React.ReactElement | null {\n const [variantChoices, setVariantChoices] = useState<Record<string, string>>({});\n const lastComponentIds = useRef("");\n\n useEffect(() => {\n const ids = components.map((c) => c.id).sort().join();\n if (ids === lastComponentIds.current) return;\n lastComponentIds.current = ids;\n\n const defaults: Record<string, string> = {};\n for (const comp of components) {\n if (comp.variant_id) {\n defaults[comp.id] = comp.variant_id;\n } else if (comp.available_variants.length > 0) {\n const defaultVariant =\n comp.available_variants.find((v) => v.is_default) || comp.available_variants[0];\n if (defaultVariant) {\n defaults[comp.id] = defaultVariant.id;\n }\n }\n }\n setVariantChoices(defaults);\n }, [components]);\n\n const selections = useMemo((): BundleSelectionInput[] => {\n return components.map((comp) => ({\n component_id: comp.id,\n variant_id: variantChoices[comp.id],\n quantity: comp.quantity,\n }));\n }, [components, variantChoices]);\n\n useEffect(() => {\n onSelectionsChange(selections);\n }, [selections, onSelectionsChange]);\n\n useEffect(() => {\n onReady?.(components.length > 0 && selections.length > 0);\n }, [components, selections, onReady]);\n\n const totalPrice = useMemo(() => {\n if (pricingType === "fixed" && bundlePrice) {\n return parsePrice(bundlePrice);\n }\n const componentsTotal = components.reduce((sum, comp) => {\n return sum + getComponentPrice(comp, variantChoices[comp.id]) * comp.quantity;\n }, 0);\n if (pricingType === "percentage_discount" && discountValue) {\n return componentsTotal * (1 - parsePrice(discountValue) / 100);\n }\n if (pricingType === "fixed_discount" && discountValue) {\n return componentsTotal - parsePrice(discountValue);\n }\n return componentsTotal;\n }, [components, variantChoices, pricingType, bundlePrice, discountValue]);\n\n useEffect(() => {\n onPriceChange?.(totalPrice);\n }, [totalPrice, onPriceChange]);\n\n const handleVariantChange = useCallback(\n (componentId: string, variantId: string) => {\n setVariantChoices((prev) => ({ ...prev, [componentId]: variantId }));\n },\n [],\n );\n\n if (components.length === 0) {\n return null;\n }\n\n return (\n <div data-cimplify-bundle-selector className={cn("space-y-4", className, classNames?.root)}>\n <div\n data-cimplify-bundle-heading\n className={cn("flex items-center justify-between py-3", classNames?.heading)}\n >\n <span className="text-base font-bold">Included in this bundle</span>\n </div>\n\n <div data-cimplify-bundle-components className={cn("divide-y divide-border", classNames?.components)}>\n {components.map((comp) => (\n <BundleComponentCard\n key={comp.id}\n component={comp}\n selectedVariantId={variantChoices[comp.id]}\n onVariantChange={(variantId) =>\n handleVariantChange(comp.id, variantId)\n }\n classNames={classNames}\n />\n ))}\n </div>\n\n {bundlePrice && (\n <div\n data-cimplify-bundle-summary\n className={cn("border-t border-border pt-4 flex justify-between text-sm", classNames?.summary)}\n >\n <span className="text-muted-foreground">Bundle price</span>\n <Price amount={bundlePrice} className="font-medium text-primary" />\n </div>\n )}\n {discountValue && (\n <div\n data-cimplify-bundle-savings\n className={cn("flex justify-between text-sm", classNames?.savings)}\n >\n <span className="text-muted-foreground">You save</span>\n <Price amount={discountValue} className="text-green-600 font-medium" />\n </div>\n )}\n </div>\n );\n}\n\nfunction getComponentPrice(\n component: BundleComponentView,\n selectedVariantId: string | undefined,\n): number {\n if (!selectedVariantId || component.available_variants.length === 0) {\n return parsePrice(component.effective_price);\n }\n if (selectedVariantId === component.variant_id) {\n return parsePrice(component.effective_price);\n }\n const bakedAdj = component.variant_id\n ? component.available_variants.find((v) => v.id === component.variant_id)\n : undefined;\n const selectedAdj = component.available_variants.find((v) => v.id === selectedVariantId);\n if (!selectedAdj) return parsePrice(component.effective_price);\n return parsePrice(component.effective_price)\n - parsePrice(bakedAdj?.price_adjustment ?? "0")\n + parsePrice(selectedAdj.price_adjustment);\n}\n\nfunction formatDuration(minutes: number, unit?: DurationUnit): string {\n if (unit === "hours" || (!unit && minutes >= 60 && minutes % 60 === 0)) {\n const h = Math.round(minutes / 60);\n return `${h}h`;\n }\n if (unit === "days" || unit === "nights") {\n const d = Math.round(minutes / 1440);\n return `${d}${unit === "nights" ? "n" : "d"}`;\n }\n return `${minutes}min`;\n}\n\ninterface BundleComponentCardProps {\n component: BundleComponentView;\n selectedVariantId?: string;\n onVariantChange: (variantId: string) => void;\n classNames?: BundleSelectorClassNames;\n}\n\nfunction BundleComponentCard({\n component,\n selectedVariantId,\n onVariantChange,\n classNames,\n}: BundleComponentCardProps): React.ReactElement {\n const idPrefix = useId();\n const showVariantPicker =\n component.allow_variant_choice && component.available_variants.length > 1;\n\n const displayPrice = useMemo(\n () => getComponentPrice(component, selectedVariantId),\n [component, selectedVariantId],\n );\n\n const labelId = `${idPrefix}-bundle-component-${component.id}`;\n\n return (\n <div\n data-cimplify-bundle-component\n className={cn("py-4", classNames?.component)}\n >\n <div\n data-cimplify-bundle-component-header\n className={cn("flex items-center justify-between gap-3", classNames?.componentHeader)}\n >\n <div className="flex items-center gap-2">\n {component.quantity > 1 && (\n <span\n data-cimplify-bundle-component-qty\n className={cn("text-xs font-medium text-primary bg-primary/10 px-1.5 py-0.5 rounded", classNames?.componentQty)}\n >\n &times;{component.quantity}\n </span>\n )}\n <span\n id={labelId}\n data-cimplify-bundle-component-name\n className={cn("text-sm", classNames?.componentName)}\n >\n {component.product_name}\n </span>\n {component.product_type === "service" && (\n <span\n data-cimplify-bundle-type-badge="service"\n className={cn(\n "text-[10px] uppercase tracking-wider font-medium text-blue-600",\n classNames?.typeBadge,\n classNames?.serviceBadge,\n )}\n >\n <svg viewBox="0 0 16 16" fill="none" className="inline-block w-3 h-3 mr-0.5 -mt-px" aria-hidden="true">\n <circle cx="8" cy="8" r="7" stroke="currentColor" strokeWidth="1.5"/>\n <path d="M8 4v4l2.5 1.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>\n </svg>\n Service{component.duration_minutes != null && (\n <> &middot; {formatDuration(component.duration_minutes, component.duration_unit)}</>\n )}\n </span>\n )}\n {component.product_type === "digital" && (\n <span\n data-cimplify-bundle-type-badge="digital"\n className={cn(\n "text-[10px] uppercase tracking-wider font-medium text-violet-600",\n classNames?.typeBadge,\n classNames?.digitalBadge,\n )}\n >\n <svg viewBox="0 0 16 16" fill="none" className="inline-block w-3 h-3 mr-0.5 -mt-px" aria-hidden="true">\n <path d="M9 2L4 9h4l-1 5 5-7H8l1-5z" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>\n </svg>\n Digital\n </span>\n )}\n </div>\n <span className="text-sm text-muted-foreground">\n <Price amount={displayPrice} />\n </span>\n </div>\n\n {showVariantPicker && (\n <RadioGroup\n aria-labelledby={labelId}\n value={selectedVariantId ?? ""}\n onValueChange={(value) => {\n onVariantChange(value);\n }}\n data-cimplify-bundle-variant-picker\n className={cn("mt-3 divide-y divide-border", classNames?.variantPicker)}\n >\n {component.available_variants.map((variant: BundleComponentVariantView) => {\n const isSelected = selectedVariantId === variant.id;\n const adjustment = parsePrice(variant.price_adjustment);\n\n return (\n <Radio.Root\n key={variant.id}\n value={variant.id}\n data-cimplify-bundle-variant-option\n data-selected={isSelected || undefined}\n className={cn(\n "w-full flex items-center gap-3 py-3 transition-colors cursor-pointer",\n isSelected ? classNames?.variantOptionSelected : classNames?.variantOption,\n )}\n >\n <span\n className={cn(\n "w-5 h-5 rounded-full border-2 flex items-center justify-center shrink-0 transition-colors",\n isSelected ? "border-primary" : "border-muted-foreground/30",\n )}\n >\n {isSelected && <span className="w-2.5 h-2.5 rounded-full bg-primary" />}\n </span>\n <span className="flex-1 text-sm">\n {variant.display_name}\n </span>\n {adjustment !== 0 && (\n <span\n data-cimplify-bundle-variant-adjustment\n className={cn("text-sm text-muted-foreground", classNames?.variantAdjustment)}\n >\n {adjustment > 0 ? "+" : ""}\n <Price amount={variant.price_adjustment} />\n </span>\n )}\n </Radio.Root>\n );\n })}\n </RadioGroup>\n )}\n </div>\n );\n}\n' }] }, "accommodation-card": { "name": "accommodation-card", "title": "AccommodationCard", "description": "Hotel/accommodation card with per-night pricing, amenities, capacity, and cancellation.", "type": "component", "registryDependencies": ["price", "volume-pricing", "cn"], "files": [{ "path": "cards/accommodation-card.tsx", "content": '"use client";\n\nimport React from "react";\nimport type { ServiceCardLayoutProps } from "./standard-service-card";\nimport { CardShell, CardImage } from "@cimplify/sdk/react";\nimport { Price } from "@cimplify/sdk/react";\nimport { VolumePricing } from "@cimplify/sdk/react";\nimport { cn } from "@cimplify/sdk/react";\n\nexport function AccommodationCard({\n product,\n onBook,\n renderImage,\n renderLink,\n className,\n}: ServiceCardLayoutProps): React.ReactElement {\n const image = product.image_url || product.images?.[0] || "";\n const hasTiers = product.quantity_pricing && product.quantity_pricing.length > 1;\n const cancellationHours = product.cancellation_window_minutes\n ? Math.floor(product.cancellation_window_minutes / 60)\n : null;\n const tags = product.tags || [];\n\n return (\n <CardShell product={product} renderLink={renderLink} className={className}>\n <CardImage src={image} alt={product.name} aspectRatio="16/9" renderImage={renderImage}>\n <div className="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent pointer-events-none" />\n\n <span className="absolute bottom-3 left-3 inline-flex items-center text-[12px] font-semibold px-2.5 py-1 rounded-lg bg-background/92 backdrop-blur-xl text-foreground shadow-sm">\n Per night\n </span>\n\n <span className="absolute bottom-3 right-3 text-white font-bold text-lg drop-shadow-sm">\n <Price amount={product.default_price} />/night\n </span>\n </CardImage>\n\n <div className="p-4">\n <h3 className="text-[14.5px] font-bold text-foreground leading-snug tracking-tight">\n {product.name}\n </h3>\n {product.description && (\n <p className="text-[12.5px] text-muted-foreground mt-1 leading-relaxed line-clamp-2">\n {product.description}\n </p>\n )}\n\n {/* Amenity / tag pills */}\n {tags.length > 0 && (\n <div className="flex flex-wrap gap-1 mt-2.5">\n {tags.slice(0, 4).map((tag) => (\n <span key={tag} className="text-[10px] font-medium text-muted-foreground px-1.5 py-0.5 bg-muted rounded">\n {tag}\n </span>\n ))}\n </div>\n )}\n\n {/* Guest capacity */}\n {product.general_service_capacity != null && (\n <div className="flex items-center gap-1.5 mt-2 text-[11px] text-muted-foreground">\n <svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="1.5" viewBox="0 0 24 24">\n <path strokeLinecap="round" strokeLinejoin="round" d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0" />\n </svg>\n {product.general_service_capacity} guest{product.general_service_capacity > 1 ? "s" : ""}\n </div>\n )}\n\n {/* Extended stay pricing */}\n {hasTiers && (\n <div className="mt-3">\n <VolumePricing tiers={product.quantity_pricing!} />\n </div>\n )}\n\n <div className="flex items-center justify-between mt-3 pt-3 border-t border-border">\n <div className="flex items-center gap-2 text-[11px] text-muted-foreground">\n {cancellationHours != null && (\n <span className="text-emerald-600 font-medium">\n Free cancellation {cancellationHours}h\n </span>\n )}\n {product.min_order_quantity != null && product.min_order_quantity > 1 && (\n <span>Min. {product.min_order_quantity} nights</span>\n )}\n </div>\n <button\n onClick={(e) => { e.preventDefault(); e.stopPropagation(); onBook?.(product); }}\n className="text-[13px] font-semibold text-primary hover:text-primary/80 transition-colors"\n >\n Book room &rarr;\n </button>\n </div>\n </div>\n </CardShell>\n );\n}\n' }] }, "booking-list": { "name": "booking-list", "title": "BookingList", "description": "List of booking cards with optional self-fetching.", "type": "component", "registryDependencies": ["booking-card", "cn"], "files": [{ "path": "booking-list.tsx", "content": '"use client";\n\nimport React from "react";\nimport type { CustomerBooking } from "@cimplify/sdk";\nimport { useBookings } from "@cimplify/sdk/react";\nimport { BookingCard } from "@cimplify/sdk/react";\nimport { cn } from "@cimplify/sdk/react";\n\nexport interface BookingListClassNames {\n root?: string;\n item?: string;\n loading?: string;\n empty?: string;\n}\n\nexport interface BookingListProps {\n /** Pre-fetched bookings (skips fetch). */\n bookings?: CustomerBooking[];\n /** Filter: "all", "upcoming", or "past". */\n filter?: "all" | "upcoming" | "past";\n /** Called when cancel is clicked on a booking. */\n onCancel?: (booking: CustomerBooking) => void;\n /** Called when reschedule is clicked on a booking. */\n onReschedule?: (booking: CustomerBooking) => void;\n /** Called when a booking is clicked. */\n onBookingClick?: (booking: CustomerBooking) => void;\n /** Custom booking renderer. */\n renderBooking?: (booking: CustomerBooking) => React.ReactNode;\n /** Text shown when empty. */\n emptyMessage?: string;\n className?: string;\n classNames?: BookingListClassNames;\n}\n\nexport function BookingList({\n bookings: bookingsProp,\n filter,\n onCancel,\n onReschedule,\n onBookingClick,\n renderBooking,\n emptyMessage = "No bookings yet",\n className,\n classNames,\n}: BookingListProps): React.ReactElement {\n const { bookings: fetched, isLoading } = useBookings({\n filter,\n enabled: bookingsProp === undefined,\n });\n\n const bookings = bookingsProp ?? fetched;\n\n if (isLoading && bookings.length === 0) {\n return (\n <div\n data-cimplify-booking-list\n aria-busy="true"\n className={cn(className, classNames?.root, classNames?.loading)}\n />\n );\n }\n\n if (bookings.length === 0) {\n return (\n <div\n data-cimplify-booking-list\n data-empty\n className={cn(className, classNames?.root, classNames?.empty)}\n >\n <p>{emptyMessage}</p>\n </div>\n );\n }\n\n return (\n <div data-cimplify-booking-list className={cn(className, classNames?.root)}>\n {bookings.map((booking) => (\n <div\n key={booking.order_id}\n data-cimplify-booking-list-item\n className={classNames?.item}\n onClick={() => onBookingClick?.(booking)}\n role={onBookingClick ? "button" : undefined}\n tabIndex={onBookingClick ? 0 : undefined}\n onKeyDown={\n onBookingClick\n ? (e) => {\n if (e.key === "Enter" || e.key === " ") {\n e.preventDefault();\n onBookingClick(booking);\n }\n }\n : undefined\n }\n >\n <BookingCard\n booking={booking}\n onCancel={onCancel}\n onReschedule={onReschedule}\n renderBooking={renderBooking}\n />\n </div>\n ))}\n </div>\n );\n}\n' }] }, "lease-service-card": { "name": "lease-service-card", "title": "LeaseServiceCard", "description": "Long-term lease card with per-month/year pricing, volume tiers, and billing.", "type": "component", "registryDependencies": ["price", "volume-pricing", "cn"], "files": [{ "path": "cards/lease-service-card.tsx", "content": '"use client";\n\nimport React from "react";\nimport type { ServiceCardLayoutProps } from "./standard-service-card";\nimport { CardShell, CardImage } from "@cimplify/sdk/react";\nimport { Price } from "@cimplify/sdk/react";\nimport { VolumePricing } from "@cimplify/sdk/react";\nimport { cn } from "@cimplify/sdk/react";\n\nconst UNIT_LABELS: Record<string, string> = {\n weeks: "week",\n months: "mo",\n years: "yr",\n};\n\nexport function LeaseServiceCard({\n product,\n onBook,\n renderImage,\n renderLink,\n className,\n}: ServiceCardLayoutProps): React.ReactElement {\n const image = product.image_url || product.images?.[0] || "";\n const hasDeposit = product.deposit_type && product.deposit_type !== "none" && product.deposit_amount;\n const hasTiers = product.quantity_pricing && product.quantity_pricing.length > 1;\n const unit = product.duration_unit ? UNIT_LABELS[product.duration_unit] || product.duration_unit : "mo";\n const tags = product.tags || [];\n const cancellationHours = product.cancellation_window_minutes\n ? Math.floor(product.cancellation_window_minutes / 60)\n : null;\n\n return (\n <CardShell product={product} renderLink={renderLink} className={className}>\n <CardImage src={image} alt={product.name} aspectRatio="16/9" renderImage={renderImage}>\n <div className="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent pointer-events-none" />\n\n <span className="absolute bottom-3 left-3 inline-flex items-center text-[12px] font-semibold px-2.5 py-1 rounded-lg bg-background/92 backdrop-blur-xl text-foreground shadow-sm">\n Per {unit}\n </span>\n\n <span className="absolute bottom-3 right-3 text-white font-bold text-lg drop-shadow-sm">\n <Price amount={product.default_price} />/{unit}\n </span>\n </CardImage>\n\n <div className="p-4">\n <h3 className="text-[14.5px] font-bold text-foreground leading-snug tracking-tight">\n {product.name}\n </h3>\n {product.description && (\n <p className="text-[12.5px] text-muted-foreground mt-1 leading-relaxed line-clamp-2">\n {product.description}\n </p>\n )}\n\n {/* Tags as amenity pills */}\n {tags.length > 0 && (\n <div className="flex flex-wrap gap-1 mt-2.5">\n {tags.slice(0, 4).map((tag) => (\n <span key={tag} className="text-[10px] font-medium text-muted-foreground px-1.5 py-0.5 bg-muted rounded">\n {tag}\n </span>\n ))}\n </div>\n )}\n\n {/* Volume tiers (lease terms) */}\n {hasTiers && (\n <div className="mt-3">\n <VolumePricing tiers={product.quantity_pricing!} />\n </div>\n )}\n\n {/* Info row */}\n <div className="flex flex-wrap items-center gap-2 mt-3">\n {product.min_order_quantity != null && product.min_order_quantity > 1 && (\n <span className="text-[10.5px] font-medium text-muted-foreground px-1.5 py-0.5 bg-muted rounded">\n Min. {product.min_order_quantity} {unit}{product.min_order_quantity > 1 ? "s" : ""}\n </span>\n )}\n {hasDeposit && (\n <span className="text-[10.5px] font-medium text-amber-600">\n <Price amount={product.deposit_amount!} /> deposit\n </span>\n )}\n {cancellationHours != null && (\n <span className="text-[10.5px] font-medium text-muted-foreground">\n {cancellationHours >= 720 ? `${Math.floor(cancellationHours / 720)} month notice` : `${cancellationHours}h notice`}\n </span>\n )}\n </div>\n\n <div className="flex items-center justify-between mt-3 pt-3 border-t border-border">\n {product.billing_plans && product.billing_plans.length > 0 && (\n <span className="text-[10.5px] font-semibold text-primary px-1.5 py-0.5 bg-primary/10 rounded">\n {product.billing_plans[0].frequency} billing\n </span>\n )}\n <button\n onClick={(e) => { e.preventDefault(); e.stopPropagation(); onBook?.(product); }}\n className="text-[13px] font-semibold text-primary hover:text-primary/80 transition-colors ml-auto"\n >\n Enquire now &rarr;\n </button>\n </div>\n </div>\n </CardShell>\n );\n}\n' }] }, "billing-plan-selector": { "name": "billing-plan-selector", "title": "BillingPlanSelector", "description": "Subscription / billing-plan picker \u2014 surfaces eligible plans with pricing and trial periods.", "type": "component", "registryDependencies": ["price", "cn"], "files": [{ "path": "billing-plan-selector.tsx", "content": '"use client";\n\nimport React from "react";\nimport type { ProductBillingPlan, BillingFrequency } from "@cimplify/sdk";\nimport { useBillingPlans } from "./hooks/use-billing-plans";\nimport { Price } from "@cimplify/sdk/react";\nimport { cn } from "@cimplify/sdk/react";\n\nexport interface BillingPlanSelectorClassNames {\n root?: string;\n plan?: string;\n activePlan?: string;\n frequency?: string;\n price?: string;\n setupFee?: string;\n trialBadge?: string;\n empty?: string;\n loading?: string;\n}\n\nexport interface BillingPlanSelectorProps {\n /** Product ID to fetch billing plans for. */\n productId: string;\n /** Override plans (skips fetch). For SSR, pass pre-fetched plans. */\n plans?: ProductBillingPlan[];\n /** Called when a plan is selected. */\n onPlanSelect?: (plan: ProductBillingPlan | null) => void;\n /** Currently selected plan ID. */\n selectedPlanId?: string | null;\n /** Show a "One-time purchase" option at the top. */\n showOneTimePurchase?: boolean;\n /** Label for the one-time purchase option. Default: "One-time purchase". */\n oneTimePurchaseLabel?: string;\n /** Custom plan card renderer. */\n renderPlan?: (plan: ProductBillingPlan, isActive: boolean) => React.ReactNode;\n className?: string;\n classNames?: BillingPlanSelectorClassNames;\n}\n\nconst FREQUENCY_LABELS: Record<BillingFrequency, string> = {\n weekly: "Weekly",\n biweekly: "Biweekly",\n monthly: "Monthly",\n quarterly: "Quarterly",\n annually: "Annually",\n};\n\nfunction formatMarkup(plan: ProductBillingPlan): string | null {\n if (!plan.markup_type || plan.markup_amount == null) return null;\n if (plan.markup_type === "percentage") return `+${plan.markup_amount}%`;\n return null;\n}\n\n/**\n * BillingPlanSelector \u2014 subscription/installment plan comparison and selection.\n *\n * Renders plan cards with frequency, markup, trial, and setup fee details.\n * Returns `null` when no plans are available (and one-time purchase is hidden).\n */\nexport function BillingPlanSelector({\n productId,\n plans: plansProp,\n onPlanSelect,\n selectedPlanId,\n showOneTimePurchase = false,\n oneTimePurchaseLabel = "One-time purchase",\n renderPlan,\n className,\n classNames,\n}: BillingPlanSelectorProps): React.ReactElement | null {\n const { plans: fetched, isLoading } = useBillingPlans(productId, {\n enabled: plansProp === undefined,\n });\n\n const plans = plansProp ?? fetched;\n\n if (isLoading && plans.length === 0) {\n return (\n <div\n data-cimplify-billing-plan-selector\n aria-busy="true"\n className={cn(className, classNames?.root, classNames?.loading)}\n />\n );\n }\n\n if (plans.length === 0 && !showOneTimePurchase) {\n return null;\n }\n\n const isOneTimePurchaseSelected = selectedPlanId === null;\n\n return (\n <div data-cimplify-billing-plan-selector className={cn(className, classNames?.root)}>\n {showOneTimePurchase && (\n <button\n type="button"\n onClick={() => onPlanSelect?.(null)}\n data-cimplify-billing-plan\n data-plan-type="one-time"\n data-active={isOneTimePurchaseSelected || undefined}\n aria-pressed={isOneTimePurchaseSelected}\n className={cn(\n classNames?.plan,\n isOneTimePurchaseSelected && classNames?.activePlan,\n )}\n >\n <span data-cimplify-billing-frequency className={classNames?.frequency}>\n {oneTimePurchaseLabel}\n </span>\n </button>\n )}\n\n {plans.map((plan) => {\n const isActive = selectedPlanId === plan.id;\n const markup = formatMarkup(plan);\n\n return (\n <button\n key={plan.id}\n type="button"\n onClick={() => onPlanSelect?.(plan)}\n data-cimplify-billing-plan\n data-plan-type={plan.plan_type}\n data-active={isActive || undefined}\n aria-pressed={isActive}\n className={cn(classNames?.plan, isActive && classNames?.activePlan)}\n >\n {renderPlan ? (\n renderPlan(plan, isActive)\n ) : (\n <>\n <span data-cimplify-billing-frequency className={classNames?.frequency}>\n {FREQUENCY_LABELS[plan.frequency]}\n </span>\n\n {markup && (\n <span data-cimplify-billing-markup className={classNames?.price}>\n {markup}\n </span>\n )}\n\n {plan.markup_type === "fixed" && plan.markup_amount != null && (\n <span data-cimplify-billing-markup className={classNames?.price}>\n +<Price amount={plan.markup_amount} />\n </span>\n )}\n\n {plan.trial_days > 0 && (\n <span data-cimplify-billing-trial className={classNames?.trialBadge}>\n {plan.trial_days}-day trial\n </span>\n )}\n\n {plan.setup_fee > 0 && (\n <span data-cimplify-billing-setup-fee className={classNames?.setupFee}>\n Setup fee: <Price amount={plan.setup_fee} />\n </span>\n )}\n\n {plan.plan_type === "installment" && plan.installment_periods != null && (\n <span data-cimplify-billing-periods>\n {plan.installment_periods} payments\n </span>\n )}\n </>\n )}\n </button>\n );\n })}\n </div>\n );\n}\n' }] }, "digital-product-card": { "name": "digital-product-card", "title": "DigitalProductCard", "description": "Digital product card with type badge, file info, and event details.", "type": "component", "registryDependencies": ["price", "cn"], "files": [{ "path": "cards/digital-product-card.tsx", "content": '"use client";\n\nimport React from "react";\nimport type { CardLayoutProps } from "@cimplify/sdk/react";\nimport { CardShell, CardImage } from "@cimplify/sdk/react";\nimport { Price } from "@cimplify/sdk/react";\n\nconst TYPE_BADGES: Record<string, { label: string; color: string }> = {\n download: { label: "Download", color: "bg-sky-50 text-sky-700 border-sky-200/60" },\n license: { label: "License", color: "bg-blue-50 text-blue-700 border-blue-200/60" },\n event_ticket: { label: "Event", color: "bg-violet-50 text-violet-700 border-violet-200/60" },\n access_pass: { label: "Access", color: "bg-emerald-50 text-emerald-700 border-emerald-200/60" },\n gift_card: { label: "Gift Card", color: "bg-amber-50 text-amber-700 border-amber-200/60" },\n};\n\nexport function DigitalProductCard({\n product,\n renderImage,\n renderLink,\n className,\n}: CardLayoutProps): React.ReactElement {\n const image = product.image_url || product.images?.[0] || "";\n const typeBadge = product.digital_type ? TYPE_BADGES[product.digital_type] : null;\n const isTicket = product.digital_type === "event_ticket";\n\n return (\n <CardShell product={product} renderLink={renderLink} className={className}>\n <CardImage src={image} alt={product.name} aspectRatio="4/3" renderImage={renderImage}>\n {typeBadge && (\n <span className={`absolute top-3 left-3 text-[11px] font-semibold tracking-wide border px-2 py-0.5 rounded-md ${typeBadge.color}`}>\n {typeBadge.label}\n </span>\n )}\n </CardImage>\n\n <div className="p-3.5">\n <h3 className="text-[14.5px] font-semibold text-foreground leading-snug tracking-tight truncate">\n {product.name}\n </h3>\n\n {/* Event: date + venue */}\n {isTicket && (product.event_date || product.venue) && (\n <p className="text-[12px] text-muted-foreground mt-1 truncate">\n {product.event_date && new Date(product.event_date).toLocaleDateString(undefined, { month: "short", day: "numeric" })}\n {product.event_date && product.venue && " \xB7 "}\n {product.venue}\n </p>\n )}\n\n {/* Non-event: description */}\n {!isTicket && product.description && (\n <p className="text-[12px] text-muted-foreground mt-1 line-clamp-1">\n {product.description}\n </p>\n )}\n\n <div className="flex items-center justify-between mt-2.5">\n <Price amount={product.default_price} className="text-sm font-bold" />\n\n {/* File info pill */}\n {product.file_type && (\n <span className="text-[10px] text-muted-foreground bg-muted px-1.5 py-0.5 rounded">\n {product.file_type.toUpperCase()}\n {product.file_size_mb != null && ` \xB7 ${product.file_size_mb >= 1024 ? `${(product.file_size_mb / 1024).toFixed(1)}GB` : `${product.file_size_mb}MB`}`}\n </span>\n )}\n </div>\n </div>\n </CardShell>\n );\n}\n' }] }, "price-range": { "name": "price-range", "title": "PriceRange", "description": "Displays min-max price range for products with variants or tiers.", "type": "component", "registryDependencies": [], "files": [{ "path": "price-range.tsx", "content": '"use client";\n\nimport React from "react";\nimport type { Product, ProductWithDetails } from "@cimplify/sdk";\nimport type { CurrencyCode } from "@cimplify/sdk";\nimport { getPriceRange, formatPriceRange } from "@cimplify/sdk";\nimport { useOptionalCimplify } from "@cimplify/sdk/react";\n\nexport interface PriceRangeProps {\n product: Product | ProductWithDetails;\n currency?: CurrencyCode;\n className?: string;\n}\n\nexport function PriceRange({ product, currency, className }: PriceRangeProps): React.ReactElement | null {\n const context = useOptionalCimplify();\n const resolvedCurrency = currency ?? (context?.displayCurrency as CurrencyCode | undefined) ?? "USD";\n const range = getPriceRange(product);\n\n if (!range) return null;\n\n return (\n <span data-cimplify-price-range className={className}>\n {formatPriceRange(range.min, range.max, resolvedCurrency)}\n </span>\n );\n}\n' }] }, "order-history-page": { "name": "order-history-page", "title": "OrderHistoryPage", "description": "Order list with status filtering and inline detail view.", "type": "component", "registryDependencies": ["order-history", "order-summary", "cn"], "files": [{ "path": "order-history-page.tsx", "content": '"use client";\n\nimport React, { useState, useCallback } from "react";\nimport { Tabs } from "@base-ui/react/tabs";\nimport type { Order, OrderStatus } from "@cimplify/sdk";\nimport { OrderHistory } from "@cimplify/sdk/react";\nimport { OrderSummary } from "@cimplify/sdk/react";\nimport { cn } from "@cimplify/sdk/react";\n\nexport interface OrderHistoryPageClassNames {\n root?: string;\n header?: string;\n title?: string;\n filters?: string;\n filterButton?: string;\n list?: string;\n detail?: string;\n backButton?: string;\n}\n\nexport interface OrderHistoryPageProps {\n /** Page title. */\n title?: string;\n /** Pre-fetched orders for SSR. */\n orders?: Order[];\n /** Max orders to show per page. */\n limit?: number;\n /** Called when navigating to an order detail (e.g. for routing). */\n onOrderNavigate?: (order: Order) => void;\n /** Called when the reorder button is clicked. */\n onReorder?: (order: Order) => void;\n /** Show status filter tabs. Default: true. */\n showFilters?: boolean;\n /** Custom order row renderer. */\n renderOrder?: (order: Order) => React.ReactNode;\n className?: string;\n classNames?: OrderHistoryPageClassNames;\n}\n\nconst STATUS_FILTERS: { label: string; value: OrderStatus | undefined; tabValue: string }[] = [\n { label: "All", value: undefined, tabValue: "all" },\n { label: "Active", value: "confirmed", tabValue: "confirmed" },\n { label: "Completed", value: "completed", tabValue: "completed" },\n { label: "Cancelled", value: "cancelled", tabValue: "cancelled" },\n];\n\n/**\n * OrderHistoryPage \u2014 order list with inline detail view and status filtering.\n *\n * SSR-friendly: pass `orders` prop for server rendering.\n * Supports both inline detail view and external navigation via `onOrderNavigate`.\n */\nexport function OrderHistoryPage({\n title = "Order History",\n orders: ordersProp,\n limit = 20,\n onOrderNavigate,\n onReorder,\n showFilters = true,\n renderOrder,\n className,\n classNames,\n}: OrderHistoryPageProps): React.ReactElement {\n const [statusFilter, setStatusFilter] = useState<OrderStatus | undefined>(undefined);\n const [selectedOrder, setSelectedOrder] = useState<Order | null>(null);\n\n const handleTabChange = useCallback((value: string | number | null) => {\n const filter = STATUS_FILTERS.find((f) => f.tabValue === value);\n setStatusFilter(filter?.value);\n }, []);\n\n const handleOrderClick = useCallback(\n (order: Order) => {\n if (onOrderNavigate) {\n onOrderNavigate(order);\n } else {\n setSelectedOrder(order);\n }\n },\n [onOrderNavigate],\n );\n\n const handleBack = useCallback(() => {\n setSelectedOrder(null);\n }, []);\n\n const activeTabValue = STATUS_FILTERS.find((f) => f.value === statusFilter)?.tabValue ?? "all";\n\n // Inline detail view\n if (selectedOrder && !onOrderNavigate) {\n return (\n <div data-cimplify-order-history-page className={cn(className, classNames?.root)}>\n <div data-cimplify-order-history-detail className={classNames?.detail}>\n <button\n type="button"\n onClick={handleBack}\n data-cimplify-order-history-back\n className={classNames?.backButton}\n >\n Back to orders\n </button>\n <OrderSummary order={selectedOrder} onReorder={onReorder} />\n </div>\n </div>\n );\n }\n\n return (\n <div data-cimplify-order-history-page className={cn(className, classNames?.root)}>\n {/* Header */}\n <div data-cimplify-order-history-header className={classNames?.header}>\n <h1 data-cimplify-order-history-title className={classNames?.title}>\n {title}\n </h1>\n </div>\n\n {/* Status filter */}\n {showFilters && (\n <Tabs.Root value={activeTabValue} onValueChange={handleTabChange}>\n <Tabs.List data-cimplify-order-history-filters className={classNames?.filters}>\n {STATUS_FILTERS.map((filter) => (\n <Tabs.Tab\n key={filter.tabValue}\n value={filter.tabValue}\n data-cimplify-order-filter\n data-selected={statusFilter === filter.value || undefined}\n className={classNames?.filterButton}\n >\n {filter.label}\n </Tabs.Tab>\n ))}\n </Tabs.List>\n </Tabs.Root>\n )}\n\n {/* Order list */}\n <div data-cimplify-order-history-list className={classNames?.list}>\n <OrderHistory\n orders={ordersProp}\n status={statusFilter}\n limit={limit}\n onOrderClick={handleOrderClick}\n renderOrder={renderOrder}\n onReorder={onReorder}\n />\n </div>\n </div>\n );\n}\n' }] }, "wholesale-product-card": { "name": "wholesale-product-card", "title": "WholesaleProductCard", "description": "B2B product card with price range, MOQ badge, and stock count.", "type": "component", "registryDependencies": ["price", "price-range", "cn"], "files": [{ "path": "cards/wholesale-product-card.tsx", "content": '"use client";\n\nimport React from "react";\nimport type { CardLayoutProps } from "@cimplify/sdk/react";\nimport { CardShell, CardImage, QuickAddButton } from "@cimplify/sdk/react";\nimport { Price } from "@cimplify/sdk/react";\nimport { PriceRange } from "@cimplify/sdk/react";\nimport { cn } from "@cimplify/sdk/react";\n\nexport function WholesaleProductCard({\n product,\n onQuickAdd,\n renderImage,\n renderLink,\n className,\n}: CardLayoutProps): React.ReactElement {\n const image = product.image_url || product.images?.[0] || "";\n const hasTiers = product.quantity_pricing && product.quantity_pricing.length > 1;\n const status = product.inventory_status;\n\n return (\n <CardShell product={product} renderLink={renderLink} className={className}>\n <CardImage src={image} alt={product.name} aspectRatio="square" renderImage={renderImage}>\n {/* MOQ badge */}\n {product.min_order_quantity != null && product.min_order_quantity > 1 && (\n <span className="absolute top-3 left-3 text-[11px] font-semibold tracking-wide bg-zinc-800 text-white px-2 py-0.5 rounded-md">\n MOQ: {product.min_order_quantity}\n </span>\n )}\n\n <QuickAddButton\n onClick={() => onQuickAdd?.(product)}\n className="absolute bottom-3 right-3"\n />\n </CardImage>\n\n <div className="p-3.5">\n {/* SKU */}\n {product.sku && (\n <p className="text-[10px] text-muted-foreground font-mono">\n {product.sku}\n </p>\n )}\n\n {/* Name */}\n <h3 className="text-[14.5px] font-semibold text-foreground leading-snug tracking-tight mt-1 truncate">\n {product.name}\n </h3>\n\n {/* Price range or single */}\n <div className="mt-2">\n {hasTiers ? (\n <PriceRange product={product} className="text-sm font-bold" />\n ) : (\n <Price amount={product.default_price} className="text-sm font-bold" />\n )}\n </div>\n\n {/* Stock */}\n {status && (\n <div className="flex items-center gap-1.5 mt-2">\n <span className={cn(\n "w-[7px] h-[7px] rounded-full shrink-0",\n status.in_stock\n ? status.low_stock ? "bg-amber-500" : "bg-emerald-500"\n : "bg-red-500",\n )} />\n <span className={cn(\n "text-[10.5px] font-medium",\n status.in_stock\n ? status.low_stock ? "text-amber-600" : "text-emerald-600"\n : "text-red-600",\n )}>\n {!status.in_stock\n ? "Out of stock"\n : status.stock_level != null\n ? `${status.stock_level.toLocaleString()} in stock`\n : "In stock"\n }\n </span>\n </div>\n )}\n </div>\n </CardShell>\n );\n}\n' }] }, "availability-badge": { "name": "availability-badge", "title": "AvailabilityBadge", "description": "Displays in-stock / out-of-stock status for tracked products.", "type": "component", "registryDependencies": ["cn"], "files": [{ "path": "availability-badge.tsx", "content": '"use client";\n\nimport React from "react";\nimport type { Product } from "@cimplify/sdk";\nimport { cn } from "@cimplify/sdk/react";\n\nexport interface AvailabilityBadgeClassNames {\n root?: string;\n dot?: string;\n label?: string;\n lowStock?: string;\n stockCount?: string;\n}\n\nexport interface AvailabilityBadgeProps {\n /** The product to check availability for. */\n product: Product;\n /** Override availability (e.g. from location_availability). */\n isAvailable?: boolean;\n /** Override stock status (e.g. from location_availability). */\n isInStock?: boolean;\n /** Current stock quantity. When provided, enables "Only X left" display. */\n stockQuantity?: number;\n /** Threshold at which stock is considered low. Default: 5. */\n lowStockThreshold?: number;\n /** Location context for availability checks. Passed through as a data attribute. */\n locationId?: string;\n className?: string;\n classNames?: AvailabilityBadgeClassNames;\n}\n\n/**\n * AvailabilityBadge \u2014 displays in-stock / out-of-stock status for tracked products.\n *\n * Returns `null` for products that don\'t have inventory tracking enabled,\n * since there\'s no meaningful stock state to show.\n */\nexport function AvailabilityBadge({\n product,\n isAvailable,\n isInStock,\n stockQuantity,\n lowStockThreshold = 5,\n locationId,\n className,\n classNames,\n}: AvailabilityBadgeProps): React.ReactElement | null {\n if (product.is_tracked !== true) {\n return null;\n }\n\n const outOfStock =\n isInStock === false ||\n isAvailable === false ||\n (stockQuantity !== undefined && stockQuantity <= 0);\n const isLowStock =\n !outOfStock &&\n stockQuantity !== undefined &&\n stockQuantity > 0 &&\n stockQuantity <= lowStockThreshold;\n\n const stockState = outOfStock\n ? "out_of_stock"\n : isLowStock\n ? "low_stock"\n : "in_stock";\n const label = outOfStock ? "Out of Stock" : "In Stock";\n\n return (\n <span\n data-cimplify-availability-badge\n data-stock-state={stockState}\n {...(locationId ? { "data-location-id": locationId } : undefined)}\n className={cn(className, classNames?.root, isLowStock && classNames?.lowStock)}\n style={{ display: "inline-flex", alignItems: "center", gap: "0.375rem" }}\n >\n <span\n data-cimplify-availability-dot\n className={classNames?.dot}\n style={{\n display: "inline-block",\n width: "0.5rem",\n height: "0.5rem",\n borderRadius: "9999px",\n }}\n />\n <span data-cimplify-availability-label className={classNames?.label}>\n {label}\n </span>\n {stockQuantity !== undefined && isLowStock && (\n <span data-cimplify-availability-stock-count className={classNames?.stockCount}>\n Only {stockQuantity} left\n </span>\n )}\n </span>\n );\n}\n' }] }, "product-sheet": { "name": "product-sheet", "title": "ProductSheet", "description": "Full product detail view with gallery, header, and customizer.", "type": "component", "registryDependencies": ["price", "product-image-gallery", "product-customizer", "cn"], "files": [{ "path": "product-sheet.tsx", "content": '"use client";\n\nimport React, { useState } from "react";\nimport type { Product, ProductWithDetails, VariantView } from "@cimplify/sdk";\nimport type { AddToCartOptions } from "@cimplify/sdk/react";\nimport { useProduct } from "@cimplify/sdk/react";\nimport { Price } from "@cimplify/sdk/react";\nimport { ProductImageGallery } from "@cimplify/sdk/react";\nimport { ProductCustomizer } from "@cimplify/sdk/react";\nimport { cn } from "@cimplify/sdk/react";\n\nexport interface ProductSheetClassNames {\n root?: string;\n image?: string;\n header?: string;\n name?: string;\n price?: string;\n description?: string;\n customizer?: string;\n loading?: string;\n}\n\nexport interface ProductSheetProps {\n /** A slim Product (triggers lazy-fetch) or a fully-loaded ProductWithDetails. */\n product: Product | ProductWithDetails;\n /** Called when the sheet should close. */\n onClose?: () => void;\n /** Override the default add-to-cart behavior. */\n onAddToCart?: (\n product: ProductWithDetails,\n quantity: number,\n options: AddToCartOptions,\n ) => void | Promise<void>;\n /** Custom image renderer (e.g. Next.js Image). */\n renderImage?: (props: {\n src: string;\n alt: string;\n className?: string;\n }) => React.ReactNode;\n className?: string;\n classNames?: ProductSheetClassNames;\n}\n\nfunction isProductWithDetails(\n product: Product | ProductWithDetails,\n): product is ProductWithDetails {\n return "variants" in product;\n}\n\n/**\n * ProductSheet \u2014 full product detail view composing gallery, header, and customizer.\n *\n * When given a slim `Product`, it lazy-fetches the full details via `useProduct`.\n * When given a `ProductWithDetails`, it skips the fetch entirely.\n */\nexport function ProductSheet({\n product,\n onClose,\n onAddToCart,\n renderImage,\n className,\n classNames,\n}: ProductSheetProps): React.ReactElement {\n const needsFetch = !isProductWithDetails(product);\n const { product: fetched, isLoading } = useProduct(\n product.slug ?? product.id,\n { enabled: needsFetch },\n );\n const fullProduct = needsFetch ? fetched : (product as ProductWithDetails);\n\n // Loading state\n if (isLoading && !fullProduct) {\n return (\n <div\n data-cimplify-product-sheet\n aria-busy="true"\n className={cn(className, classNames?.root, classNames?.loading)}\n >\n <div\n data-cimplify-product-sheet-skeleton\n style={{\n display: "flex",\n flexDirection: "column",\n gap: "1rem",\n }}\n >\n <div\n style={{\n aspectRatio: "4/3",\n backgroundColor: "rgba(0,0,0,0.06)",\n borderRadius: "0.5rem",\n }}\n />\n <div\n style={{\n height: "1.5rem",\n width: "60%",\n backgroundColor: "rgba(0,0,0,0.06)",\n borderRadius: "0.25rem",\n }}\n />\n <div\n style={{\n height: "1rem",\n width: "30%",\n backgroundColor: "rgba(0,0,0,0.06)",\n borderRadius: "0.25rem",\n }}\n />\n </div>\n </div>\n );\n }\n\n // Error state\n if (!fullProduct) {\n return (\n <div\n data-cimplify-product-sheet\n className={cn(className, classNames?.root)}\n >\n <p>Product not found.</p>\n </div>\n );\n }\n\n const [selectedVariant, setSelectedVariant] = useState<VariantView | undefined>(\n undefined,\n );\n\n const variantImages = selectedVariant?.images?.filter(Boolean) ?? [];\n const productImages: string[] = [];\n if (fullProduct.images && fullProduct.images.length > 0) {\n productImages.push(...fullProduct.images.filter(Boolean));\n } else if (fullProduct.image_url) {\n productImages.push(fullProduct.image_url);\n }\n const images: string[] =\n variantImages.length > 0 ? variantImages : productImages;\n\n const hasMultipleImages = images.length > 1;\n const singleImage = images[0];\n\n return (\n <div\n data-cimplify-product-sheet\n className={cn(className, classNames?.root)}\n style={{ display: "flex", flexDirection: "column", gap: "1rem" }}\n >\n {/* Image area */}\n {hasMultipleImages ? (\n <ProductImageGallery\n images={images}\n productName={fullProduct.name}\n className={classNames?.image}\n />\n ) : singleImage ? (\n <div data-cimplify-product-sheet-image className={classNames?.image}>\n {renderImage ? (\n renderImage({ src: singleImage, alt: fullProduct.name })\n ) : (\n <img\n src={singleImage}\n alt={fullProduct.name}\n style={{ width: "100%", height: "auto", objectFit: "cover" }}\n />\n )}\n </div>\n ) : null}\n\n {/* Header */}\n <div data-cimplify-product-sheet-header className={classNames?.header}>\n <h2\n data-cimplify-product-sheet-name\n className={classNames?.name}\n style={{ margin: 0 }}\n >\n {fullProduct.name}\n </h2>\n <Price\n amount={fullProduct.default_price}\n className={classNames?.price}\n />\n </div>\n\n {/* Description \u2014 merchant-authored HTML from the catalogue. */}\n {fullProduct.description && (\n <div\n data-cimplify-product-sheet-description\n className={cn("text-sm leading-relaxed text-muted-foreground [&_p]:m-0 [&_p+p]:mt-2", classNames?.description)}\n dangerouslySetInnerHTML={{ __html: fullProduct.description }}\n />\n )}\n\n {/* Customizer */}\n <ProductCustomizer\n product={fullProduct}\n onAddToCart={onAddToCart}\n onVariantChange={(_id, variant) => setSelectedVariant(variant)}\n className={classNames?.customizer}\n />\n </div>\n );\n}\n' }] }, "bookings-page": { "name": "bookings-page", "title": "BookingsPage", "description": "Account-area page listing a customer's bookings with filters and detail view.", "type": "component", "registryDependencies": ["booking-list", "booking-card", "cn"], "files": [{ "path": "bookings-page.tsx", "content": '"use client";\n\nimport React, { useState, useCallback } from "react";\nimport { Tabs } from "@base-ui/react/tabs";\nimport type { CustomerBooking } from "@cimplify/sdk";\nimport { BookingList } from "@cimplify/sdk/react";\nimport { BookingCard } from "@cimplify/sdk/react";\nimport { cn } from "@cimplify/sdk/react";\n\nexport interface BookingsPageClassNames {\n root?: string;\n header?: string;\n title?: string;\n filters?: string;\n filterButton?: string;\n list?: string;\n detail?: string;\n backButton?: string;\n}\n\nexport interface BookingsPageProps {\n /** Page title. */\n title?: string;\n /** Pre-fetched bookings for SSR. */\n bookings?: CustomerBooking[];\n /** Called when navigating to a booking detail (e.g. for routing). */\n onBookingNavigate?: (booking: CustomerBooking) => void;\n /** Called when cancel is clicked. */\n onCancel?: (booking: CustomerBooking) => void;\n /** Called when reschedule is clicked. */\n onReschedule?: (booking: CustomerBooking) => void;\n /** Show filter tabs. Default: true. */\n showFilters?: boolean;\n /** Custom booking renderer. */\n renderBooking?: (booking: CustomerBooking) => React.ReactNode;\n className?: string;\n classNames?: BookingsPageClassNames;\n}\n\nconst BOOKING_FILTERS: { label: string; value: "all" | "upcoming" | "past" }[] = [\n { label: "All", value: "all" },\n { label: "Upcoming", value: "upcoming" },\n { label: "Past", value: "past" },\n];\n\nexport function BookingsPage({\n title = "My Bookings",\n bookings: bookingsProp,\n onBookingNavigate,\n onCancel,\n onReschedule,\n showFilters = true,\n renderBooking,\n className,\n classNames,\n}: BookingsPageProps): React.ReactElement {\n const [filter, setFilter] = useState<"all" | "upcoming" | "past">("all");\n const [selectedBooking, setSelectedBooking] = useState<CustomerBooking | null>(null);\n\n const handleBookingClick = useCallback(\n (booking: CustomerBooking) => {\n if (onBookingNavigate) {\n onBookingNavigate(booking);\n } else {\n setSelectedBooking(booking);\n }\n },\n [onBookingNavigate],\n );\n\n const handleBack = useCallback(() => {\n setSelectedBooking(null);\n }, []);\n\n if (selectedBooking && !onBookingNavigate) {\n return (\n <div data-cimplify-bookings-page className={cn(className, classNames?.root)}>\n <div data-cimplify-bookings-detail className={classNames?.detail}>\n <button\n type="button"\n onClick={handleBack}\n data-cimplify-bookings-back\n className={classNames?.backButton}\n >\n Back to bookings\n </button>\n <BookingCard\n booking={selectedBooking}\n onCancel={onCancel}\n onReschedule={onReschedule}\n />\n </div>\n </div>\n );\n }\n\n return (\n <div data-cimplify-bookings-page className={cn(className, classNames?.root)}>\n <div data-cimplify-bookings-header className={classNames?.header}>\n <h1 data-cimplify-bookings-title className={classNames?.title}>\n {title}\n </h1>\n </div>\n\n {showFilters && (\n <Tabs.Root\n value={filter}\n onValueChange={(value) => setFilter(value as "all" | "upcoming" | "past")}\n >\n <Tabs.List data-cimplify-bookings-filters className={classNames?.filters}>\n {BOOKING_FILTERS.map((f) => (\n <Tabs.Tab\n key={f.value}\n value={f.value}\n data-cimplify-booking-filter\n data-selected={filter === f.value || undefined}\n className={classNames?.filterButton}\n >\n {f.label}\n </Tabs.Tab>\n ))}\n </Tabs.List>\n </Tabs.Root>\n )}\n\n <div data-cimplify-bookings-list className={classNames?.list}>\n <BookingList\n bookings={bookingsProp}\n filter={filter}\n onCancel={onCancel}\n onReschedule={onReschedule}\n onBookingClick={handleBookingClick}\n renderBooking={renderBooking}\n />\n </div>\n </div>\n );\n}\n' }] }, "currency-selector": { "name": "currency-selector", "title": "CurrencySelector", "description": "Multi-currency switcher backed by the FX provider \u2014 locks display currency and quote ID.", "type": "component", "registryDependencies": ["cn"], "files": [{ "path": "currency-selector.tsx", "content": '"use client";\n\nimport React, { useState, useEffect, useRef } from "react";\nimport type { CurrencyCode } from "@cimplify/sdk";\nimport type { CimplifyError } from "@cimplify/sdk";\nimport type { FxRateResponse } from "../types/fx";\nimport { useOptionalCimplifyClient } from "@cimplify/sdk/react";\nimport { cn } from "@cimplify/sdk/react";\n\nexport interface CurrencySelectorClassNames {\n root?: string;\n option?: string;\n activeOption?: string;\n rate?: string;\n label?: string;\n loading?: string;\n}\n\nexport interface CurrencySelectorProps {\n /** Available currencies to display. */\n currencies: CurrencyCode[];\n /** Currently selected currency. */\n currentCurrency?: CurrencyCode;\n /** Called when a currency is selected. */\n onCurrencyChange?: (currency: CurrencyCode) => void;\n /** Show the exchange rate next to each currency. Requires `baseCurrency`. */\n showRate?: boolean;\n /** Base currency used for rate display. */\n baseCurrency?: CurrencyCode;\n /** Custom currency option renderer. */\n renderCurrency?: (\n currency: CurrencyCode,\n isActive: boolean,\n rate: FxRateResponse | null,\n ) => React.ReactNode;\n className?: string;\n classNames?: CurrencySelectorClassNames;\n}\n\n/**\n * CurrencySelector \u2014 currency switcher for international customers.\n *\n * Renders selectable currency options with optional exchange rate display.\n * Returns `null` when there is only one currency (no selector needed).\n */\nexport function CurrencySelector({\n currencies,\n currentCurrency,\n onCurrencyChange,\n showRate = false,\n baseCurrency,\n renderCurrency,\n className,\n classNames,\n}: CurrencySelectorProps): React.ReactElement | null {\n const context = useOptionalCimplifyClient();\n const client = context?.client ?? null;\n\n const [rates, setRates] = useState<Record<string, FxRateResponse>>({});\n const [isLoadingRates, setIsLoadingRates] = useState(false);\n const requestIdRef = useRef(0);\n\n useEffect(() => {\n if (!showRate || !baseCurrency || !client) return;\n\n const targets = currencies.filter((c) => c !== baseCurrency);\n if (targets.length === 0) return;\n\n const nextRequestId = ++requestIdRef.current;\n setIsLoadingRates(true);\n\n void (async () => {\n const fetched: Record<string, FxRateResponse> = {};\n\n await Promise.all(\n targets.map(async (currency) => {\n const res = await client.fx.getRate(baseCurrency, currency);\n if (res.ok) {\n fetched[currency] = res.value;\n }\n }),\n );\n\n if (nextRequestId !== requestIdRef.current) return;\n\n setRates(fetched);\n setIsLoadingRates(false);\n })();\n }, [client, showRate, baseCurrency, currencies]);\n\n if (currencies.length <= 1) {\n return null;\n }\n\n return (\n <div data-cimplify-currency-selector className={cn(className, classNames?.root)}>\n {currencies.map((currency) => {\n const isActive = currentCurrency === currency;\n const rate = rates[currency] ?? null;\n\n return (\n <button\n key={currency}\n type="button"\n onClick={() => onCurrencyChange?.(currency)}\n data-cimplify-currency-option\n data-active={isActive || undefined}\n aria-pressed={isActive}\n className={cn(\n classNames?.option,\n isActive && classNames?.activeOption,\n )}\n >\n {renderCurrency ? (\n renderCurrency(currency, isActive, rate)\n ) : (\n <>\n <span data-cimplify-currency-label className={classNames?.label}>\n {currency}\n </span>\n {showRate && baseCurrency && currency !== baseCurrency && (\n <span data-cimplify-currency-rate className={classNames?.rate}>\n {isLoadingRates\n ? "..."\n : rate\n ? `1 ${baseCurrency} = ${rate.rate.toFixed(4)} ${currency}`\n : null}\n </span>\n )}\n </>\n )}\n </button>\n );\n })}\n </div>\n );\n}\n' }] }, "store-nav": { "name": "store-nav", "title": "StoreNav", "description": "Top navigation bar with brand, categories, cart badge, and search.", "type": "component", "registryDependencies": ["cn"], "files": [{ "path": "store-nav.tsx", "content": '"use client";\n\nimport React from "react";\nimport { useCart, useCategories } from "@cimplify/sdk/react";\nimport type { Category } from "@cimplify/sdk";\nimport { cn } from "@cimplify/sdk/react";\n\nexport interface StoreNavClassNames {\n root?: string;\n brand?: string;\n categories?: string;\n categoryLink?: string;\n actions?: string;\n cartButton?: string;\n cartCount?: string;\n searchButton?: string;\n}\n\nexport interface StoreNavProps {\n /** Store/brand name. */\n storeName?: string;\n /** Custom brand element (logo, etc.). Overrides storeName. */\n renderBrand?: () => React.ReactNode;\n /** Override categories (skips fetch). */\n categories?: Category[];\n /** Called when a category link is clicked. */\n onCategoryClick?: (category: Category) => void;\n /** Called when the cart button is clicked. */\n onCartClick?: () => void;\n /** Called when the search button is clicked. */\n onSearchClick?: () => void;\n /** Hide category navigation. */\n hideCategories?: boolean;\n /** Hide the cart button. */\n hideCart?: boolean;\n /** Hide the search button. */\n hideSearch?: boolean;\n className?: string;\n classNames?: StoreNavClassNames;\n}\n\n/**\n * StoreNav \u2014 top navigation bar with brand, category links, cart badge, and search.\n *\n * Fetches categories via `useCategories` and cart count via `useCart`.\n * Renders as a semantic `<nav>` element.\n */\nexport function StoreNav({\n storeName,\n renderBrand,\n categories: categoriesProp,\n onCategoryClick,\n onCartClick,\n onSearchClick,\n hideCategories = false,\n hideCart = false,\n hideSearch = false,\n className,\n classNames,\n}: StoreNavProps): React.ReactElement {\n const { categories: fetched } = useCategories({\n enabled: !hideCategories && categoriesProp === undefined,\n });\n const { itemCount } = useCart();\n\n const categories = categoriesProp ?? fetched;\n\n return (\n <nav data-cimplify-store-nav className={cn(className, classNames?.root)}>\n {/* Brand */}\n <div data-cimplify-store-nav-brand className={classNames?.brand}>\n {renderBrand ? renderBrand() : storeName && <span>{storeName}</span>}\n </div>\n\n {/* Category links */}\n {!hideCategories && categories.length > 0 && (\n <div data-cimplify-store-nav-categories className={classNames?.categories}>\n {categories.map((category: Category) => (\n <button\n key={category.id}\n type="button"\n onClick={() => onCategoryClick?.(category)}\n data-cimplify-store-nav-category\n className={classNames?.categoryLink}\n >\n {category.name}\n </button>\n ))}\n </div>\n )}\n\n {/* Actions */}\n <div data-cimplify-store-nav-actions className={classNames?.actions}>\n {!hideSearch && (\n <button\n type="button"\n onClick={onSearchClick}\n data-cimplify-store-nav-search\n className={classNames?.searchButton}\n aria-label="Search"\n >\n Search\n </button>\n )}\n\n {!hideCart && (\n <button\n type="button"\n onClick={onCartClick}\n data-cimplify-store-nav-cart\n className={classNames?.cartButton}\n aria-label={`Cart (${itemCount} items)`}\n >\n Cart\n {itemCount > 0 && (\n <span data-cimplify-store-nav-cart-count className={classNames?.cartCount}>\n {itemCount}\n </span>\n )}\n </button>\n )}\n </div>\n </nav>\n );\n}\n' }] }, "deal-banner": { "name": "deal-banner", "title": "DealBanner", "description": "Displays active deals and promotions.", "type": "component", "registryDependencies": ["price", "cn"], "files": [{ "path": "deal-banner.tsx", "content": '"use client";\n\nimport React from "react";\nimport type { Deal } from "@cimplify/sdk";\nimport { useDeals } from "@cimplify/sdk/react";\nimport { Price } from "@cimplify/sdk/react";\nimport { cn } from "@cimplify/sdk/react";\n\nexport interface DealBannerClassNames {\n root?: string;\n item?: string;\n description?: string;\n value?: string;\n badge?: string;\n empty?: string;\n}\n\nexport interface DealBannerProps {\n /** Override deals (skips useDeals fetch). For SSR, pass pre-fetched deals. */\n deals?: Deal[];\n /** Location ID for location-specific deals. */\n locationId?: string;\n /** Called when a deal is clicked. */\n onDealClick?: (deal: Deal) => void;\n /** Custom deal card renderer. */\n renderDeal?: (deal: Deal) => React.ReactNode;\n /** Maximum deals to show. Default: all. */\n limit?: number;\n className?: string;\n classNames?: DealBannerClassNames;\n}\n\nfunction formatBenefitLabel(deal: Deal): string {\n switch (deal.benefit_type) {\n case "percentage":\n return `${deal.value}% off`;\n case "fixed":\n return `Save`;\n case "free_item":\n return "Free item";\n case "buy_x_get_y_free":\n return `Buy ${deal.buy_quantity ?? ""} get ${deal.get_quantity ?? 1} free`;\n case "points":\n return `Earn ${deal.value} points`;\n default:\n return "Special offer";\n }\n}\n\n/**\n * DealBanner \u2014 displays active deals/promotions.\n *\n * Renders as a horizontal scrollable strip or grid of deal cards.\n */\nexport function DealBanner({\n deals: dealsProp,\n locationId,\n onDealClick,\n renderDeal,\n limit,\n className,\n classNames,\n}: DealBannerProps): React.ReactElement | null {\n const { deals: fetched, isLoading } = useDeals({\n locationId,\n enabled: dealsProp === undefined,\n });\n\n const allDeals = dealsProp ?? fetched;\n const deals = limit ? allDeals.slice(0, limit) : allDeals;\n\n if (isLoading && deals.length === 0) {\n return (\n <div\n data-cimplify-deal-banner\n aria-busy="true"\n className={cn(className, classNames?.root)}\n />\n );\n }\n\n if (deals.length === 0) {\n return null;\n }\n\n return (\n <div data-cimplify-deal-banner className={cn(className, classNames?.root)}>\n {deals.map((deal) => (\n <button\n key={deal.id}\n type="button"\n onClick={() => onDealClick?.(deal)}\n data-cimplify-deal-item\n data-benefit-type={deal.benefit_type}\n className={classNames?.item}\n >\n {renderDeal ? (\n renderDeal(deal)\n ) : (\n <>\n <span data-cimplify-deal-badge className={classNames?.badge}>\n {formatBenefitLabel(deal)}\n </span>\n <span data-cimplify-deal-description className={classNames?.description}>\n {deal.description}\n </span>\n {deal.benefit_type === "fixed" && (\n <span data-cimplify-deal-value className={classNames?.value}>\n <Price amount={deal.value} />\n </span>\n )}\n {deal.min_order_value && (\n <span data-cimplify-deal-minimum>\n Min. order <Price amount={deal.min_order_value} />\n </span>\n )}\n </>\n )}\n </button>\n ))}\n </div>\n );\n}\n' }] }, "date-slot-picker": { "name": "date-slot-picker", "title": "DateSlotPicker", "description": "Horizontal date strip with slot picker for service scheduling.", "type": "component", "registryDependencies": ["slot-picker", "cn"], "files": [{ "path": "date-slot-picker.tsx", "content": '"use client";\n\nimport React, { useState, useMemo, useCallback } from "react";\nimport { Tabs } from "@base-ui/react/tabs";\nimport type { AvailableSlot, DayAvailability } from "@cimplify/sdk";\nimport type { DurationUnit, SchedulingMode } from "@cimplify/sdk";\nimport { useServiceAvailability } from "@cimplify/sdk/react";\nimport { SlotPicker } from "@cimplify/sdk/react";\nimport { cn } from "@cimplify/sdk/react";\n\nexport interface DateSlotPickerClassNames {\n root?: string;\n dateStrip?: string;\n dateButton?: string;\n nav?: string;\n navButton?: string;\n slots?: string;\n loading?: string;\n}\n\nexport interface DateSlotPickerProps {\n /** Service ID to fetch availability and slots for. */\n serviceId: string;\n /** Number of days to show in the date strip. Default: 7. */\n daysToShow?: number;\n /** Number of participants. */\n participantCount?: number;\n /** Currently selected slot. */\n selectedSlot?: AvailableSlot | null;\n /** Called when a slot is selected. */\n onSlotSelect?: (slot: AvailableSlot, date: string) => void;\n /** Pre-fetched availability data (skips fetch). */\n availability?: DayAvailability[];\n /** Show price on slots. Default: true. */\n showPrice?: boolean;\n /** Forwarded to `<SlotPicker>` to render multi-day stay labels. */\n schedulingMode?: SchedulingMode;\n /** Forwarded to `<SlotPicker>` \u2014 unit for the stay summary in multi-day mode. */\n durationUnit?: DurationUnit;\n /** Forwarded to `<SlotPicker>` \u2014 value for the stay summary in multi-day mode. */\n durationValue?: number;\n /** Forwarded to `<SlotPicker>` \u2014 hide slots already in the past. Default: true. */\n hideElapsedSlots?: boolean;\n /** Forwarded to `<SlotPicker>` \u2014 minimum lead time in minutes. Default: 0. */\n minLeadMinutes?: number;\n className?: string;\n classNames?: DateSlotPickerClassNames;\n}\n\nfunction formatDate(dateStr: string): string {\n const date = new Date(dateStr + "T00:00:00");\n return date.toLocaleDateString(undefined, { weekday: "short", month: "short", day: "numeric" });\n}\n\nfunction toDateString(date: Date): string {\n return date.toISOString().split("T")[0];\n}\n\nfunction addDays(date: Date, days: number): Date {\n const result = new Date(date);\n result.setDate(result.getDate() + days);\n return result;\n}\n\nexport function DateSlotPicker({\n serviceId,\n daysToShow = 7,\n participantCount,\n selectedSlot,\n onSlotSelect,\n availability: availabilityProp,\n showPrice = true,\n schedulingMode,\n durationUnit,\n durationValue,\n hideElapsedSlots = true,\n minLeadMinutes = 0,\n className,\n classNames,\n}: DateSlotPickerProps): React.ReactElement {\n const [offset, setOffset] = useState(0);\n const [selectedDate, setSelectedDate] = useState<string>(toDateString(new Date()));\n\n const dateRange = useMemo(() => {\n const today = new Date();\n const start = addDays(today, offset);\n const dates: string[] = [];\n for (let i = 0; i < daysToShow; i++) {\n dates.push(toDateString(addDays(start, i)));\n }\n return {\n dates,\n startDate: dates[0],\n endDate: dates[dates.length - 1],\n };\n }, [offset, daysToShow]);\n\n const { days: fetchedDays, isLoading: availabilityLoading } = useServiceAvailability(\n serviceId,\n dateRange.startDate,\n dateRange.endDate,\n {\n participantCount,\n enabled: availabilityProp === undefined,\n },\n );\n\n const days = availabilityProp ?? fetchedDays;\n\n const availabilityMap = useMemo(() => {\n const map = new Map<string, DayAvailability>();\n for (const day of days) {\n map.set(day.date, day);\n }\n return map;\n }, [days]);\n\n const handlePrev = useCallback(() => {\n setOffset((prev) => Math.max(0, prev - daysToShow));\n }, [daysToShow]);\n\n const handleNext = useCallback(() => {\n setOffset((prev) => prev + daysToShow);\n }, [daysToShow]);\n\n const handleDateChange = useCallback((value: string | number | null) => {\n if (typeof value === "string") {\n setSelectedDate(value);\n }\n }, []);\n\n const handleSlotSelect = useCallback(\n (slot: AvailableSlot) => {\n onSlotSelect?.(slot, selectedDate);\n },\n [onSlotSelect, selectedDate],\n );\n\n return (\n <Tabs.Root\n value={selectedDate}\n onValueChange={handleDateChange}\n data-cimplify-date-slot-picker\n className={cn("flex flex-col gap-4", className, classNames?.root)}\n >\n <div\n data-cimplify-date-nav\n className={cn("flex items-center justify-end gap-2", classNames?.nav)}\n >\n <button\n type="button"\n onClick={handlePrev}\n disabled={offset === 0}\n aria-label="Previous dates"\n data-cimplify-date-nav-prev\n className={cn(\n "grid place-items-center w-8 h-8 rounded-md border border-border text-muted-foreground transition-colors hover:bg-muted hover:text-foreground disabled:cursor-not-allowed disabled:opacity-40",\n classNames?.navButton,\n )}\n >\n &larr;\n </button>\n <button\n type="button"\n onClick={handleNext}\n aria-label="Next dates"\n data-cimplify-date-nav-next\n className={cn(\n "grid place-items-center w-8 h-8 rounded-md border border-border text-muted-foreground transition-colors hover:bg-muted hover:text-foreground",\n classNames?.navButton,\n )}\n >\n &rarr;\n </button>\n </div>\n\n <Tabs.List\n data-cimplify-date-strip\n className={cn("grid grid-cols-7 gap-1 sm:gap-2", classNames?.dateStrip)}\n >\n {dateRange.dates.map((date) => {\n const dayInfo = availabilityMap.get(date);\n const hasAvailability = dayInfo?.has_availability !== false;\n const isSelected = selectedDate === date;\n return (\n <Tabs.Tab\n key={date}\n value={date}\n data-cimplify-date-button\n data-selected={isSelected || undefined}\n data-available={hasAvailability || undefined}\n data-fully-booked={(!hasAvailability) || undefined}\n className={cn(\n "flex flex-col items-center justify-center rounded-md border border-border bg-background px-1 py-2 text-center text-xs font-medium text-foreground transition-colors hover:border-foreground/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring data-[selected]:border-foreground data-[selected]:bg-foreground data-[selected]:text-background data-[fully-booked]:cursor-not-allowed data-[fully-booked]:opacity-40",\n classNames?.dateButton,\n )}\n >\n {formatDate(date)}\n </Tabs.Tab>\n );\n })}\n </Tabs.List>\n\n {availabilityLoading && (\n <div\n data-cimplify-date-slot-loading\n aria-busy="true"\n className={cn("h-32 rounded-md bg-muted/40 animate-pulse", classNames?.loading)}\n />\n )}\n\n <div data-cimplify-date-slots className={classNames?.slots}>\n <SlotPicker\n serviceId={serviceId}\n date={selectedDate}\n participantCount={participantCount}\n selectedSlot={selectedSlot}\n onSlotSelect={handleSlotSelect}\n showPrice={showPrice}\n schedulingMode={schedulingMode}\n durationUnit={durationUnit}\n durationValue={durationValue}\n hideElapsedSlots={hideElapsedSlots}\n minLeadMinutes={minLeadMinutes}\n />\n </div>\n </Tabs.Root>\n );\n}\n' }] }, "product-page": { "name": "product-page", "title": "ProductPage", "description": "Smart product page resolver with per-slug and per-type template routing.", "type": "component", "registryDependencies": ["default-product-layout", "food-product-layout", "wholesale-product-layout", "service-product-layout", "digital-product-layout", "cn"], "files": [{ "path": "product-page.tsx", "content": '"use client";\n\nimport React from "react";\nimport type { Product, ProductWithDetails } from "@cimplify/sdk";\nimport { PRODUCT_TYPE, RENDER_HINT } from "@cimplify/sdk";\nimport type { ProductLayoutProps } from "@cimplify/sdk/react";\nimport { DefaultProductLayout } from "@cimplify/sdk/react";\nimport { FoodProductLayout } from "@cimplify/sdk/react";\nimport { WholesaleProductLayout } from "@cimplify/sdk/react";\nimport { ServiceProductLayout } from "@cimplify/sdk/react";\nimport { DigitalProductLayout } from "@cimplify/sdk/react";\nimport { BundleProductLayout } from "./layouts/bundle-product-layout";\nimport { CompositeProductLayout } from "./layouts/composite-product-layout";\nimport type { AddToCartOptions } from "@cimplify/sdk/react";\nimport { useProduct } from "@cimplify/sdk/react";\nimport { cn } from "@cimplify/sdk/react";\n\nexport type { ProductLayoutProps };\n\nexport enum ProductTemplate {\n Default = "default",\n Food = "food",\n Wholesale = "wholesale",\n Service = "service",\n Digital = "digital",\n Bundle = "bundle",\n Composite = "composite",\n Physical = "physical",\n}\n\nexport interface ProductPageClassNames {\n root?: string;\n loading?: string;\n}\n\nexport interface ProductPageProps {\n /** Product slug or ID \u2014 used for client-side fetch when `product` is not provided. */\n productId?: string;\n /** Pre-fetched product for SSR. Skips client-side fetch when provided. */\n product?: ProductWithDetails;\n /** Per-slug page map. Highest priority \u2014 maps a product slug to a custom component. */\n pages?: Record<string, React.ComponentType<ProductLayoutProps>>;\n /** Per-type template map. Overrides built-in layouts for specific template keys. */\n templates?: Partial<Record<ProductTemplate | string, React.ComponentType<ProductLayoutProps>>>;\n /** Override add-to-cart behavior. */\n onAddToCart?: (\n product: ProductWithDetails,\n quantity: number,\n options: AddToCartOptions,\n ) => void | Promise<void>;\n /** Custom image renderer (e.g. Next.js Image). */\n renderImage?: (props: { src: string; alt: string; className?: string }) => React.ReactNode;\n /** Breadcrumb renderer. Receives the product for context. */\n renderBreadcrumb?: (product: ProductWithDetails) => React.ReactNode;\n /** Pre-fetched related products. */\n relatedProducts?: Product[];\n /** Show related products section. Default: true. */\n showRelated?: boolean;\n className?: string;\n classNames?: ProductPageClassNames;\n}\n\nconst BUILT_IN_LAYOUTS: Record<string, React.ComponentType<ProductLayoutProps>> = {\n [ProductTemplate.Food]: FoodProductLayout,\n [ProductTemplate.Wholesale]: WholesaleProductLayout,\n [ProductTemplate.Service]: ServiceProductLayout,\n [ProductTemplate.Digital]: DigitalProductLayout,\n [ProductTemplate.Bundle]: BundleProductLayout,\n [ProductTemplate.Composite]: CompositeProductLayout,\n [ProductTemplate.Default]: DefaultProductLayout,\n};\n\nfunction resolveTemplateKey(product: ProductWithDetails): ProductTemplate | string {\n const metaTemplate = product.metadata?.page_template;\n if (typeof metaTemplate === "string" && metaTemplate.trim()) {\n return metaTemplate.trim();\n }\n\n if (product.type === PRODUCT_TYPE.Bundle) {\n return ProductTemplate.Bundle;\n }\n\n if (product.type === PRODUCT_TYPE.Composite) {\n return ProductTemplate.Composite;\n }\n\n if (product.quantity_pricing && product.quantity_pricing.length > 1) {\n return ProductTemplate.Wholesale;\n }\n\n if (product.type === PRODUCT_TYPE.Service) {\n return ProductTemplate.Service;\n }\n\n if (product.type === PRODUCT_TYPE.Digital) {\n return ProductTemplate.Digital;\n }\n\n if (product.render_hint === RENDER_HINT.Food) {\n return ProductTemplate.Food;\n }\n\n if (product.render_hint === RENDER_HINT.Physical) {\n return ProductTemplate.Physical;\n }\n\n return ProductTemplate.Default;\n}\n\nfunction resolveLayout(\n product: ProductWithDetails,\n pages?: Record<string, React.ComponentType<ProductLayoutProps>>,\n templates?: Partial<Record<string, React.ComponentType<ProductLayoutProps>>>,\n): React.ComponentType<ProductLayoutProps> {\n // 1. Per-slug page (AI-generated, highest priority)\n if (pages?.[product.slug]) {\n return pages[product.slug];\n }\n\n const key = resolveTemplateKey(product);\n\n // 2. Consumer-provided template override\n if (templates?.[key]) {\n return templates[key];\n }\n\n // 3. Built-in layout\n if (BUILT_IN_LAYOUTS[key]) {\n return BUILT_IN_LAYOUTS[key];\n }\n\n // 4. Fallback\n return DefaultProductLayout;\n}\n\nexport function ProductPage({\n productId,\n product: productProp,\n pages,\n templates,\n onAddToCart,\n renderImage,\n renderBreadcrumb,\n relatedProducts,\n showRelated = true,\n className,\n classNames,\n}: ProductPageProps): React.ReactElement {\n const resolvedId = productId || productProp?.slug || productProp?.id || "";\n const { product: fetched, isLoading } = useProduct(resolvedId, {\n enabled: !productProp && resolvedId.length > 0,\n });\n const product = productProp ?? fetched;\n\n if (isLoading && !product) {\n return (\n <div\n data-cimplify-product-page\n aria-busy="true"\n className={cn(className, classNames?.root, classNames?.loading)}\n >\n <div className="grid lg:grid-cols-2 gap-8 lg:gap-12 animate-pulse">\n <div className="aspect-square bg-muted" />\n <div className="space-y-4 py-4">\n <div className="h-8 w-3/5 bg-muted" />\n <div className="h-6 w-2/5 bg-muted" />\n <div className="h-4 w-4/5 bg-muted" />\n <div className="h-4 w-3/5 bg-muted" />\n <div className="h-14 w-full bg-muted mt-8" />\n </div>\n </div>\n </div>\n );\n }\n\n if (!product) {\n return (\n <div data-cimplify-product-page className={cn(className, classNames?.root)}>\n <p className="text-muted-foreground">Product not found.</p>\n </div>\n );\n }\n\n const Layout = resolveLayout(product, pages, templates);\n\n return (\n <div data-cimplify-product-page className={cn(className, classNames?.root)}>\n <Layout\n product={product}\n onAddToCart={onAddToCart}\n renderImage={renderImage}\n renderBreadcrumb={renderBreadcrumb}\n relatedProducts={relatedProducts}\n showRelated={showRelated}\n />\n </div>\n );\n}\n' }] }, "rental-service-card": { "name": "rental-service-card", "title": "RentalServiceCard", "description": "Rental card with per-day/hour pricing, deposit, and availability count.", "type": "component", "registryDependencies": ["price", "cn"], "files": [{ "path": "cards/rental-service-card.tsx", "content": '"use client";\n\nimport React from "react";\nimport type { ServiceCardLayoutProps } from "./standard-service-card";\nimport { CardShell, CardImage, QuickAddButton } from "@cimplify/sdk/react";\nimport { Price } from "@cimplify/sdk/react";\nimport { cn } from "@cimplify/sdk/react";\n\nconst UNIT_LABELS: Record<string, string> = {\n minutes: "min",\n hours: "hr",\n days: "day",\n nights: "night",\n weeks: "week",\n months: "mo",\n years: "yr",\n};\n\nexport function RentalServiceCard({\n product,\n onBook,\n renderImage,\n renderLink,\n className,\n}: ServiceCardLayoutProps): React.ReactElement {\n const image = product.image_url || product.images?.[0] || "";\n const hasDeposit = product.deposit_type && product.deposit_type !== "none" && product.deposit_amount;\n const unit = product.duration_unit ? UNIT_LABELS[product.duration_unit] || product.duration_unit : "day";\n const status = product.inventory_status;\n\n return (\n <CardShell product={product} renderLink={renderLink} className={className}>\n <CardImage src={image} alt={product.name} aspectRatio="16/9" renderImage={renderImage}>\n <div className="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent pointer-events-none" />\n\n {/* Per-unit pill */}\n <span className="absolute bottom-3 left-3 inline-flex items-center text-[12px] font-semibold px-2.5 py-1 rounded-lg bg-background/92 backdrop-blur-xl text-foreground shadow-sm">\n Per {unit}\n </span>\n\n {/* Price */}\n <span className="absolute bottom-3 right-3 text-white font-bold text-lg drop-shadow-sm">\n <Price amount={product.default_price} />/{unit}\n </span>\n\n {/* Capacity badge */}\n {product.general_service_capacity != null && product.general_service_capacity > 1 && (\n <span className="absolute top-3 left-3 text-[11px] font-semibold tracking-wide bg-blue-50/90 text-blue-700 border border-blue-200/50 backdrop-blur px-2 py-0.5 rounded-md">\n Up to {product.general_service_capacity} guests\n </span>\n )}\n </CardImage>\n\n <div className="p-4">\n <h3 className="text-[14.5px] font-bold text-foreground leading-snug tracking-tight">\n {product.name}\n </h3>\n {product.description && (\n <p className="text-[12.5px] text-muted-foreground mt-1 leading-relaxed line-clamp-2">\n {product.description}\n </p>\n )}\n\n {/* Info pills */}\n <div className="flex flex-wrap items-center gap-2 mt-3">\n {product.min_order_quantity != null && product.min_order_quantity > 1 && (\n <span className="text-[10.5px] font-medium text-muted-foreground px-1.5 py-0.5 bg-muted rounded">\n Min. {product.min_order_quantity} {unit}{product.min_order_quantity > 1 ? "s" : ""}\n </span>\n )}\n {hasDeposit && (\n <span className="text-[10.5px] font-medium text-amber-600">\n <Price amount={product.deposit_amount!} /> deposit\n </span>\n )}\n </div>\n\n <div className="flex items-center justify-between mt-3 pt-3 border-t border-border">\n {/* Availability */}\n {status && (\n <div className="flex items-center gap-1.5 text-[12px]">\n <span className={cn(\n "w-[7px] h-[7px] rounded-full shrink-0",\n status.in_stock ? "bg-emerald-500" : "bg-red-500",\n )} />\n <span className={status.in_stock ? "text-muted-foreground" : "text-red-600"}>\n {status.in_stock\n ? status.stock_level != null\n ? `${status.stock_level} available`\n : "Available"\n : "Unavailable"\n }\n </span>\n </div>\n )}\n <button\n onClick={(e) => { e.preventDefault(); e.stopPropagation(); onBook?.(product); }}\n className="text-[13px] font-semibold text-primary hover:text-primary/80 transition-colors"\n >\n Reserve now &rarr;\n </button>\n </div>\n </div>\n </CardShell>\n );\n}\n' }] }, "variant-selector": { "name": "variant-selector", "title": "VariantSelector", "description": "Select product variants via axis chips or direct list.", "type": "component", "registryDependencies": ["price"], "files": [{ "path": "variant-selector.tsx", "content": '"use client";\n\nimport React, { useState, useEffect, useRef, useId } from "react";\nimport { RadioGroup } from "@base-ui/react/radio-group";\nimport { Radio } from "@base-ui/react/radio";\nimport type { VariantView, VariantAxisWithValues } from "@cimplify/sdk";\nimport type { Money } from "@cimplify/sdk";\nimport { parsePrice } from "@cimplify/sdk";\nimport { getVariantDisplayName } from "@cimplify/sdk";\nimport { Price } from "@cimplify/sdk/react";\nimport { cn } from "@cimplify/sdk/react";\n\nexport interface VariantSelectorClassNames {\n root?: string;\n axisLabel?: string;\n axisOptions?: string;\n option?: string;\n optionSelected?: string;\n listLabel?: string;\n list?: string;\n name?: string;\n pricing?: string;\n adjustment?: string;\n}\n\nexport interface VariantSelectorProps {\n variants: VariantView[];\n variantAxes?: VariantAxisWithValues[];\n basePrice?: Money;\n selectedVariantId?: string;\n onVariantChange: (variantId: string | undefined, variant: VariantView | undefined) => void;\n productName?: string;\n className?: string;\n classNames?: VariantSelectorClassNames;\n}\n\nexport function VariantSelector({\n variants,\n variantAxes,\n basePrice,\n selectedVariantId,\n onVariantChange,\n productName,\n className,\n classNames,\n}: VariantSelectorProps): React.ReactElement | null {\n const [axisSelections, setAxisSelections] = useState<Record<string, string>>({});\n const initialized = useRef(false);\n const idPrefix = useId();\n\n useEffect(() => {\n initialized.current = false;\n }, [variants]);\n\n useEffect(() => {\n if (initialized.current) return;\n if (!variants || variants.length === 0) return;\n\n const defaultVariant = variants.find((v) => v.is_default) || variants[0];\n if (!defaultVariant) return;\n\n initialized.current = true;\n onVariantChange(defaultVariant.id, defaultVariant);\n\n if (defaultVariant.display_attributes) {\n const initial: Record<string, string> = {};\n for (const attr of defaultVariant.display_attributes) {\n initial[attr.axis_id] = attr.value_id;\n }\n setAxisSelections(initial);\n }\n }, [variants, onVariantChange]);\n\n useEffect(() => {\n if (!initialized.current) return;\n if (!variantAxes || variantAxes.length === 0) return;\n\n const match = variants.find((v) => {\n if (!v.display_attributes) return false;\n return v.display_attributes.every(\n (attr) => axisSelections[attr.axis_id] === attr.value_id,\n );\n });\n\n if (match && match.id !== selectedVariantId) {\n onVariantChange(match.id, match);\n }\n }, [axisSelections, variants, variantAxes, selectedVariantId, onVariantChange]);\n\n if (!variants || variants.length <= 1) {\n return null;\n }\n\n const basePriceNum = basePrice != null ? parsePrice(basePrice) : 0;\n\n if (variantAxes && variantAxes.length > 0) {\n return (\n <div data-cimplify-variant-selector className={cn("space-y-5", className, classNames?.root)}>\n {variantAxes.map((axis) => {\n const labelId = `${idPrefix}-axis-${axis.id}`;\n return (\n <div key={axis.id} data-cimplify-variant-axis>\n <label\n id={labelId}\n data-cimplify-variant-axis-label\n className={cn("block text-xs font-medium uppercase tracking-wider text-muted-foreground mb-3", classNames?.axisLabel)}\n >\n {axis.name}\n </label>\n <RadioGroup\n aria-labelledby={labelId}\n value={axisSelections[axis.id] ?? ""}\n onValueChange={(value) => {\n setAxisSelections((prev) => ({\n ...prev,\n [axis.id]: value,\n }));\n }}\n data-cimplify-variant-axis-options\n className={cn("flex flex-wrap gap-2", classNames?.axisOptions)}\n >\n {axis.values.map((value) => {\n const isSelected = axisSelections[axis.id] === value.id;\n return (\n <Radio.Root\n key={value.id}\n value={value.id}\n data-cimplify-variant-option\n data-selected={isSelected || undefined}\n className={cn(\n "px-4 py-2 border text-sm font-medium transition-colors border-border hover:border-primary/50",\n isSelected && "bg-primary text-primary-foreground border-primary",\n isSelected ? classNames?.optionSelected : classNames?.option,\n )}\n >\n {value.name}\n </Radio.Root>\n );\n })}\n </RadioGroup>\n </div>\n );\n })}\n </div>\n );\n }\n\n const listLabelId = `${idPrefix}-variant-list`;\n\n return (\n <div data-cimplify-variant-selector className={cn(className, classNames?.root)}>\n <div\n data-cimplify-variant-list-header\n className={cn("flex items-center justify-between py-3", classNames?.listLabel)}\n >\n <label id={listLabelId} className="text-base font-bold">\n Options\n </label>\n <span className="text-xs font-semibold text-destructive bg-destructive/10 px-2.5 py-1 rounded">\n Required\n </span>\n </div>\n <RadioGroup\n aria-labelledby={listLabelId}\n value={selectedVariantId ?? ""}\n onValueChange={(value) => {\n const variant = variants.find((v) => v.id === value);\n onVariantChange(variant?.id, variant);\n }}\n data-cimplify-variant-list\n className={cn("divide-y divide-border", classNames?.list)}\n >\n {variants.map((variant) => {\n const isSelected = selectedVariantId === variant.id;\n const adjustment = parsePrice(variant.price_adjustment);\n\n return (\n <Radio.Root\n key={variant.id}\n value={variant.id}\n data-cimplify-variant-option\n data-selected={isSelected || undefined}\n className={cn(\n "w-full flex items-center gap-3 py-4 transition-colors cursor-pointer",\n isSelected ? classNames?.optionSelected : classNames?.option,\n )}\n >\n <span\n data-cimplify-variant-radio\n className={cn(\n "w-5 h-5 rounded-full border-2 flex items-center justify-center shrink-0 transition-colors",\n isSelected ? "border-primary" : "border-muted-foreground/30",\n )}\n >\n {isSelected && <span className="w-2.5 h-2.5 rounded-full bg-primary" />}\n </span>\n <span\n data-cimplify-variant-name\n className={cn("flex-1 min-w-0 text-sm", classNames?.name)}\n >\n {getVariantDisplayName(variant, productName)}\n </span>\n <span data-cimplify-variant-pricing className={cn("text-sm text-muted-foreground", classNames?.pricing)}>\n {adjustment > 0 ? "+" : adjustment < 0 ? "" : "+"}\n <Price amount={variant.price_adjustment} />\n </span>\n </Radio.Root>\n );\n })}\n </RadioGroup>\n </div>\n );\n}\n' }] }, "price": { "name": "price", "title": "Price", "description": "Renders a formatted price in the display currency.", "type": "component", "registryDependencies": [], "files": [{ "path": "price.tsx", "content": '"use client";\n\nimport React from "react";\nimport { formatPrice } from "@cimplify/sdk";\nimport type { CurrencyCode } from "@cimplify/sdk";\nimport { useOptionalCimplify } from "@cimplify/sdk/react";\n\nexport interface PriceProps {\n /** The amount in base (business) currency. */\n amount: number | string;\n /** Explicit currency \u2014 skips provider lookup when provided. */\n currency?: CurrencyCode;\n /** Optional CSS class name for the wrapping span. */\n className?: string;\n /** Optional prefix rendered before the formatted price (e.g. "+"). */\n prefix?: string;\n}\n\n/**\n * Price \u2014 renders a formatted price value.\n *\n * When used inside CimplifyProvider: reads displayCurrency and convertPrice\n * from context for FX conversion.\n *\n * When used outside a provider (or with explicit `currency` prop): formats\n * directly \u2014 no provider required. Works in any React environment.\n */\nexport function Price({ amount, currency, className, prefix }: PriceProps): React.ReactElement {\n const context = useOptionalCimplify();\n const resolvedCurrency = currency ?? (context?.displayCurrency as CurrencyCode | undefined) ?? "USD";\n const convertedAmount = !currency && context?.convertPrice ? context.convertPrice(amount) : amount;\n const resolvedAmount = typeof convertedAmount === "string" ? parseFloat(convertedAmount) || 0 : convertedAmount;\n\n return (\n <span className={className}>\n {prefix}\n {formatPrice(resolvedAmount, resolvedCurrency)}\n </span>\n );\n}\n' }] }, "customer-input-fields": { "name": "customer-input-fields", "title": "CustomerInputFields", "description": "Per-product custom input fields \u2014 text, number, date, time, file upload, image upload, single/multi-select.", "type": "component", "registryDependencies": ["cn"], "files": [{ "path": "customer-input-fields.tsx", "content": '"use client";\n\nimport React, { useCallback } from "react";\nimport { INPUT_FIELD_TYPE, type AddressValue, type DateRangeValue, type LocationValue, type PhoneValue, type ProductInputField, type SignatureValue } from "@cimplify/sdk";\nimport { parsePrice } from "@cimplify/sdk";\nimport { Price } from "@cimplify/sdk/react";\nimport { useOptionalCimplifyClient } from "@cimplify/sdk/react";\nimport { DatePicker } from "./date-picker";\nimport { TimePicker } from "./time-picker";\nimport { cn } from "@cimplify/sdk/react";\n\nexport interface CustomerInputFieldsClassNames {\n root?: string;\n field?: string;\n label?: string;\n required?: string;\n helpText?: string;\n input?: string;\n select?: string;\n textarea?: string;\n fileInput?: string;\n colorInput?: string;\n radioGroup?: string;\n radioOption?: string;\n checkboxLabel?: string;\n priceAdjustment?: string;\n addressInput?: string;\n phoneInput?: string;\n signatureCanvas?: string;\n multiSelectGroup?: string;\n multiSelectOption?: string;\n dateRangeInput?: string;\n locationInput?: string;\n}\n\nexport interface CustomerInputFieldsProps {\n fields: ProductInputField[];\n values: Record<string, unknown>;\n onChange: (values: Record<string, unknown>) => void;\n onFileUpload?: (file: File, field: ProductInputField) => Promise<string>;\n className?: string;\n classNames?: CustomerInputFieldsClassNames;\n}\n\nexport function CustomerInputFields({\n fields,\n values,\n onChange,\n onFileUpload,\n className,\n classNames,\n}: CustomerInputFieldsProps): React.ReactElement | null {\n const clientContext = useOptionalCimplifyClient();\n\n if (fields.length === 0) return null;\n\n const sorted = [...fields].sort((a, b) => a.display_order - b.display_order);\n\n const setValue = useCallback(\n (fieldId: string, value: unknown) => {\n onChange({ ...values, [fieldId]: value });\n },\n [values, onChange],\n );\n\n const defaultFileUpload = useCallback(\n async (file: File): Promise<string> => {\n if (onFileUpload) return onFileUpload(file, {} as ProductInputField);\n if (!clientContext?.client) throw new Error("No upload provider available");\n const result = await clientContext.client.uploads.upload(file);\n if (!result.ok) throw new Error(result.error.message);\n return result.value.url;\n },\n [onFileUpload, clientContext],\n );\n\n return (\n <div data-cimplify-customer-inputs className={cn("space-y-4", className, classNames?.root)}>\n {sorted.map((field) => (\n <InputField\n key={field.id}\n field={field}\n value={values[field.id]}\n onValueChange={(value) => setValue(field.id, value)}\n onFileUpload={onFileUpload ?? (clientContext?.client ? (_file, _field) => defaultFileUpload(_file) : undefined)}\n classNames={classNames}\n />\n ))}\n </div>\n );\n}\n\nfunction InputField({\n field,\n value,\n onValueChange,\n onFileUpload,\n classNames,\n}: {\n field: ProductInputField;\n value: unknown;\n onValueChange: (value: unknown) => void;\n onFileUpload?: (file: File, field: ProductInputField) => Promise<string>;\n classNames?: CustomerInputFieldsClassNames;\n}): React.ReactElement {\n const hasPriceAdjustment = field.price_adjustment != null && parsePrice(field.price_adjustment) > 0;\n\n return (\n <div data-cimplify-customer-input data-field-type={field.field_type} className={classNames?.field}>\n <div className="flex items-center gap-2 mb-1.5">\n <label className={cn("text-sm font-semibold", classNames?.label)}>\n {field.name}\n </label>\n {field.is_required && (\n <span className={cn("text-xs text-destructive", classNames?.required)}>Required</span>\n )}\n {hasPriceAdjustment && (\n <span className={cn("text-xs text-muted-foreground", classNames?.priceAdjustment)}>\n +<Price amount={field.price_adjustment!} />\n </span>\n )}\n </div>\n\n {field.help_text && (\n <p className={cn("text-xs text-muted-foreground mb-1.5", classNames?.helpText)}>\n {field.help_text}\n </p>\n )}\n\n <FieldInput\n field={field}\n value={value}\n onValueChange={onValueChange}\n onFileUpload={onFileUpload}\n classNames={classNames}\n />\n </div>\n );\n}\n\nfunction FieldInput({\n field,\n value,\n onValueChange,\n onFileUpload,\n classNames,\n}: {\n field: ProductInputField;\n value: unknown;\n onValueChange: (value: unknown) => void;\n onFileUpload?: (file: File, field: ProductInputField) => Promise<string>;\n classNames?: CustomerInputFieldsClassNames;\n}): React.ReactElement {\n const inputClass = cn(\n "w-full rounded-md border border-input bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring",\n classNames?.input,\n );\n\n switch (field.field_type) {\n case INPUT_FIELD_TYPE.Text:\n case INPUT_FIELD_TYPE.Url:\n return (\n <input\n type={field.field_type === INPUT_FIELD_TYPE.Url ? "url" : "text"}\n value={typeof value === "string" ? value : ""}\n onChange={(e) => onValueChange(e.target.value)}\n placeholder={field.placeholder}\n required={field.is_required}\n maxLength={field.validation?.max_length ?? undefined}\n className={inputClass}\n />\n );\n\n case INPUT_FIELD_TYPE.Textarea:\n return (\n <textarea\n value={typeof value === "string" ? value : ""}\n onChange={(e) => onValueChange(e.target.value)}\n placeholder={field.placeholder}\n required={field.is_required}\n maxLength={field.validation?.max_length ?? undefined}\n rows={3}\n className={cn(inputClass, "resize-none", classNames?.textarea)}\n />\n );\n\n case INPUT_FIELD_TYPE.Number:\n return (\n <input\n type="number"\n value={typeof value === "number" ? value : ""}\n onChange={(e) => onValueChange(e.target.value ? parseFloat(e.target.value) : undefined)}\n placeholder={field.placeholder}\n required={field.is_required}\n min={field.validation?.min_value ?? undefined}\n max={field.validation?.max_value ?? undefined}\n className={inputClass}\n />\n );\n\n case INPUT_FIELD_TYPE.Select:\n return (\n <select\n value={typeof value === "string" ? value : ""}\n onChange={(e) => onValueChange(e.target.value || undefined)}\n required={field.is_required}\n className={cn(inputClass, classNames?.select)}\n >\n <option value="">{field.placeholder || "Select..."}</option>\n {(field.options || []).map((opt) => (\n <option key={opt} value={opt}>{opt}</option>\n ))}\n </select>\n );\n\n case INPUT_FIELD_TYPE.Radio:\n return (\n <div className={cn("space-y-2", classNames?.radioGroup)}>\n {(field.options || []).map((opt) => (\n <label\n key={opt}\n className={cn(\n "flex items-center gap-2 text-sm cursor-pointer",\n classNames?.radioOption,\n )}\n >\n <input\n type="radio"\n name={field.id}\n value={opt}\n checked={value === opt}\n onChange={() => onValueChange(opt)}\n className="accent-primary"\n />\n {opt}\n </label>\n ))}\n </div>\n );\n\n case INPUT_FIELD_TYPE.Checkbox:\n return (\n <label className={cn("flex items-center gap-2 text-sm cursor-pointer", classNames?.checkboxLabel)}>\n <input\n type="checkbox"\n checked={value === true}\n onChange={(e) => onValueChange(e.target.checked)}\n className="accent-primary w-4 h-4"\n />\n {field.placeholder || field.name}\n </label>\n );\n\n case INPUT_FIELD_TYPE.Color:\n return (\n <input\n type="color"\n value={typeof value === "string" ? value : "#000000"}\n onChange={(e) => onValueChange(e.target.value)}\n className={cn("w-12 h-10 rounded-md border border-input cursor-pointer", classNames?.colorInput)}\n />\n );\n\n case INPUT_FIELD_TYPE.Date:\n return (\n <DatePicker\n value={typeof value === "string" ? value : ""}\n onChange={(next) => onValueChange(next || undefined)}\n required={field.is_required}\n placeholder={field.placeholder ?? "Select a date"}\n aria-label={field.name}\n />\n );\n\n case INPUT_FIELD_TYPE.File:\n case INPUT_FIELD_TYPE.Image:\n return (\n <FileUploadInput\n field={field}\n value={value}\n onValueChange={onValueChange}\n onFileUpload={onFileUpload}\n classNames={classNames}\n />\n );\n\n case INPUT_FIELD_TYPE.Email:\n return (\n <input\n type="email"\n value={typeof value === "string" ? value : ""}\n onChange={(e) => onValueChange(e.target.value)}\n placeholder={field.placeholder || "email@example.com"}\n required={field.is_required}\n className={inputClass}\n />\n );\n\n case INPUT_FIELD_TYPE.DateTime: {\n const stringValue = typeof value === "string" ? value : "";\n const [datePart, timePartRaw] = stringValue.includes("T")\n ? stringValue.split("T", 2)\n : [stringValue, ""];\n const timePart = (timePartRaw ?? "").slice(0, 5);\n const commit = (nextDate: string, nextTime: string): void => {\n if (!nextDate && !nextTime) {\n onValueChange(undefined);\n return;\n }\n if (!nextDate) {\n onValueChange(`${nextTime}`);\n return;\n }\n const combined = `${nextDate}T${nextTime || "00:00"}`;\n const parsed = new Date(combined);\n onValueChange(Number.isNaN(parsed.getTime()) ? combined : parsed.toISOString());\n };\n return (\n <div className="grid grid-cols-1 sm:grid-cols-[1fr_auto] gap-2">\n <DatePicker\n value={datePart ?? ""}\n onChange={(next) => commit(next, timePart)}\n placeholder={field.placeholder ?? "Select a date"}\n aria-label={`${field.name} date`}\n required={field.is_required}\n />\n <TimePicker\n value={timePart}\n onChange={(next) => commit(datePart ?? "", next)}\n placeholder="Time"\n aria-label={`${field.name} time`}\n />\n </div>\n );\n }\n\n case INPUT_FIELD_TYPE.Time:\n return (\n <TimePicker\n value={typeof value === "string" ? value : ""}\n onChange={(next) => onValueChange(next || undefined)}\n required={field.is_required}\n placeholder={field.placeholder ?? "Select a time"}\n aria-label={field.name}\n />\n );\n\n case INPUT_FIELD_TYPE.MultiSelect:\n return (\n <MultiSelectInput\n field={field}\n value={value}\n onValueChange={onValueChange}\n classNames={classNames}\n />\n );\n\n case INPUT_FIELD_TYPE.DateRange:\n return (\n <DateRangeInput\n field={field}\n value={value}\n onValueChange={onValueChange}\n classNames={classNames}\n />\n );\n\n case INPUT_FIELD_TYPE.Address:\n return (\n <AddressInput\n field={field}\n value={value}\n onValueChange={onValueChange}\n classNames={classNames}\n inputClass={inputClass}\n />\n );\n\n case INPUT_FIELD_TYPE.Location:\n return (\n <LocationInput\n field={field}\n value={value}\n onValueChange={onValueChange}\n classNames={classNames}\n inputClass={inputClass}\n />\n );\n\n case INPUT_FIELD_TYPE.Phone:\n return (\n <PhoneInput\n field={field}\n value={value}\n onValueChange={onValueChange}\n classNames={classNames}\n inputClass={inputClass}\n />\n );\n\n case INPUT_FIELD_TYPE.Signature:\n return (\n <SignatureInput\n field={field}\n value={value}\n onValueChange={onValueChange}\n classNames={classNames}\n />\n );\n\n default:\n return (\n <input\n type="text"\n value={typeof value === "string" ? value : ""}\n onChange={(e) => onValueChange(e.target.value)}\n placeholder={field.placeholder}\n className={inputClass}\n />\n );\n }\n}\n\nfunction FileUploadInput({\n field,\n value,\n onValueChange,\n onFileUpload,\n classNames,\n}: {\n field: ProductInputField;\n value: unknown;\n onValueChange: (value: unknown) => void;\n onFileUpload?: (file: File, field: ProductInputField) => Promise<string>;\n classNames?: CustomerInputFieldsClassNames;\n}): React.ReactElement {\n const [isUploading, setIsUploading] = React.useState(false);\n const fileUrl = typeof value === "string" ? value : undefined;\n const acceptedFormats = field.validation?.accepted_formats;\n const accept = acceptedFormats\n ? acceptedFormats.map((f) => `.${f}`).join(",")\n : field.field_type === INPUT_FIELD_TYPE.Image\n ? "image/*"\n : undefined;\n\n const handleFile = async (file: File) => {\n if (!onFileUpload) return;\n\n if (field.validation?.max_size_mb) {\n const maxBytes = field.validation.max_size_mb * 1024 * 1024;\n if (file.size > maxBytes) {\n return;\n }\n }\n\n setIsUploading(true);\n try {\n const url = await onFileUpload(file, field);\n onValueChange(url);\n } finally {\n setIsUploading(false);\n }\n };\n\n return (\n <div className={cn("space-y-2", classNames?.fileInput)}>\n {fileUrl ? (\n <div className="flex items-center gap-3">\n {field.field_type === INPUT_FIELD_TYPE.Image && (\n <img src={fileUrl} alt="Uploaded" className="w-16 h-16 object-cover rounded-md border border-border" />\n )}\n <div className="flex-1 min-w-0">\n <p className="text-sm text-foreground truncate">{fileUrl.split("/").pop()}</p>\n </div>\n <button\n type="button"\n onClick={() => onValueChange(undefined)}\n className="text-xs text-muted-foreground hover:text-destructive transition-colors"\n >\n Remove\n </button>\n </div>\n ) : (\n <label className="flex flex-col items-center justify-center w-full h-24 border-2 border-dashed border-border rounded-lg cursor-pointer hover:border-primary/40 transition-colors">\n <input\n type="file"\n accept={accept}\n onChange={(e) => {\n const file = e.target.files?.[0];\n if (file) void handleFile(file);\n }}\n className="hidden"\n disabled={isUploading}\n />\n {isUploading ? (\n <span className="text-sm text-muted-foreground">Uploading...</span>\n ) : (\n <>\n <span className="text-sm text-muted-foreground">\n {field.placeholder || `Upload ${field.field_type === INPUT_FIELD_TYPE.Image ? "image" : "file"}`}\n </span>\n {acceptedFormats && (\n <span className="text-xs text-muted-foreground/60 mt-1">\n {acceptedFormats.map((f) => f.toUpperCase()).join(", ")}\n {field.validation?.max_size_mb && ` \xB7 Max ${field.validation.max_size_mb}MB`}\n </span>\n )}\n </>\n )}\n </label>\n )}\n </div>\n );\n}\n\nfunction MultiSelectInput({\n field,\n value,\n onValueChange,\n classNames,\n}: {\n field: ProductInputField;\n value: unknown;\n onValueChange: (value: unknown) => void;\n classNames?: CustomerInputFieldsClassNames;\n}): React.ReactElement {\n const selected = Array.isArray(value) ? (value as string[]) : [];\n const options = field.options || [];\n const maxSelections = field.validation?.max_selections;\n\n const toggle = (opt: string) => {\n const next = selected.includes(opt)\n ? selected.filter((s) => s !== opt)\n : maxSelections && selected.length >= maxSelections\n ? selected\n : [...selected, opt];\n onValueChange(next.length > 0 ? next : undefined);\n };\n\n return (\n <div className={cn("space-y-2", classNames?.multiSelectGroup)}>\n {options.map((opt) => (\n <label\n key={opt}\n className={cn(\n "flex items-center gap-2 text-sm cursor-pointer",\n classNames?.multiSelectOption,\n )}\n >\n <input\n type="checkbox"\n checked={selected.includes(opt)}\n onChange={() => toggle(opt)}\n className="accent-primary w-4 h-4"\n />\n {opt}\n </label>\n ))}\n {maxSelections && (\n <p className="text-xs text-muted-foreground">\n {selected.length}/{maxSelections} selected\n </p>\n )}\n </div>\n );\n}\n\nfunction DateRangeInput({\n field,\n value,\n onValueChange,\n classNames,\n}: {\n field: ProductInputField;\n value: unknown;\n onValueChange: (value: unknown) => void;\n classNames?: CustomerInputFieldsClassNames;\n}): React.ReactElement {\n const range = (value && typeof value === "object" ? value : {}) as DateRangeValue;\n\n const update = (key: "start" | "end", v: string) => {\n const next = { ...range, [key]: v };\n onValueChange(next.start || next.end ? next : undefined);\n };\n\n return (\n <div className={cn("grid grid-cols-1 sm:grid-cols-2 gap-3", classNames?.dateRangeInput)}>\n <div>\n <label className="text-xs text-muted-foreground mb-1 block">Start</label>\n <DatePicker\n value={range.start || ""}\n onChange={(next) => update("start", next)}\n required={field.is_required}\n placeholder="Start date"\n aria-label={`${field.name} start date`}\n maxDate={range.end ? new Date(`${range.end}T00:00`) : undefined}\n />\n </div>\n <div>\n <label className="text-xs text-muted-foreground mb-1 block">End</label>\n <DatePicker\n value={range.end || ""}\n onChange={(next) => update("end", next)}\n required={field.is_required}\n placeholder="End date"\n aria-label={`${field.name} end date`}\n minDate={range.start ? new Date(`${range.start}T00:00`) : undefined}\n />\n </div>\n </div>\n );\n}\n\nfunction AddressInput({\n field,\n value,\n onValueChange,\n classNames,\n inputClass,\n}: {\n field: ProductInputField;\n value: unknown;\n onValueChange: (value: unknown) => void;\n classNames?: CustomerInputFieldsClassNames;\n inputClass: string;\n}): React.ReactElement {\n const clientContext = useOptionalCimplifyClient();\n const addr = (value && typeof value === "object" ? value : null) as AddressValue | null;\n const [inputText, setInputText] = React.useState(addr?.formatted_address ?? "");\n const [suggestions, setSuggestions] = React.useState<Array<{ description: string; place_id: string }>>([]);\n const [isOpen, setIsOpen] = React.useState(false);\n const [highlightedIndex, setHighlightedIndex] = React.useState(-1);\n const debounceRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);\n const seqRef = React.useRef(0);\n const sessionTokenRef = React.useRef(createSessionToken());\n const blurTimeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);\n\n React.useEffect(() => {\n return () => {\n if (debounceRef.current) clearTimeout(debounceRef.current);\n if (blurTimeoutRef.current) clearTimeout(blurTimeoutRef.current);\n seqRef.current += 1;\n };\n }, []);\n\n const fetchSuggestions = React.useCallback(async (query: string) => {\n const client = clientContext?.client;\n if (!client || query.length < 3) {\n setSuggestions([]);\n setIsOpen(false);\n return;\n }\n\n const seq = ++seqRef.current;\n const result = await client.places.autocomplete(query, sessionTokenRef.current);\n if (seqRef.current !== seq) return;\n\n if (result.ok && result.value.predictions.length > 0) {\n setSuggestions(result.value.predictions);\n setHighlightedIndex(0);\n setIsOpen(true);\n } else {\n setSuggestions([]);\n setIsOpen(false);\n }\n }, [clientContext]);\n\n const handleInputChange = (nextValue: string) => {\n setInputText(nextValue);\n if (debounceRef.current) clearTimeout(debounceRef.current);\n debounceRef.current = setTimeout(() => void fetchSuggestions(nextValue.trim()), 300);\n };\n\n const selectSuggestion = React.useCallback(async (suggestion: { description: string; place_id: string }) => {\n if (debounceRef.current) clearTimeout(debounceRef.current);\n if (blurTimeoutRef.current) clearTimeout(blurTimeoutRef.current);\n\n const fallback = suggestion.description.split(",")[0]?.trim() || suggestion.description;\n setInputText(fallback);\n setSuggestions([]);\n setIsOpen(false);\n\n const client = clientContext?.client;\n if (!client) return;\n\n const result = await client.places.details(suggestion.place_id, sessionTokenRef.current);\n sessionTokenRef.current = createSessionToken();\n\n if (result.ok) {\n const d = result.value;\n const addressValue: AddressValue = {\n formatted_address: d.formatted_address || fallback,\n street_address: d.street_address,\n apartment: d.apartment,\n city: d.city,\n region: d.region,\n postal_code: d.postal_code,\n country: d.country,\n latitude: d.latitude,\n longitude: d.longitude,\n place_id: d.place_id,\n };\n setInputText(addressValue.formatted_address);\n onValueChange(addressValue);\n }\n }, [clientContext, onValueChange]);\n\n return (\n <div className={cn("space-y-2 relative", classNames?.addressInput)}>\n <input\n type="text"\n value={inputText}\n onChange={(e) => handleInputChange(e.target.value)}\n onFocus={() => { if (suggestions.length > 0) setIsOpen(true); }}\n onBlur={() => { blurTimeoutRef.current = setTimeout(() => setIsOpen(false), 120); }}\n onKeyDown={(e) => {\n if (e.key === "Escape") { setIsOpen(false); return; }\n if (!isOpen || suggestions.length === 0) return;\n if (e.key === "ArrowDown") { e.preventDefault(); setHighlightedIndex((i) => i < suggestions.length - 1 ? i + 1 : 0); }\n else if (e.key === "ArrowUp") { e.preventDefault(); setHighlightedIndex((i) => i > 0 ? i - 1 : suggestions.length - 1); }\n else if (e.key === "Enter" && highlightedIndex >= 0) { e.preventDefault(); void selectSuggestion(suggestions[highlightedIndex]); }\n }}\n placeholder={field.placeholder || "Search for an address..."}\n required={field.is_required}\n autoComplete="off"\n aria-autocomplete="list"\n aria-expanded={isOpen && suggestions.length > 0}\n className={inputClass}\n />\n {isOpen && suggestions.length > 0 && (\n <div role="listbox" className="absolute left-0 right-0 top-full z-20 mt-1 overflow-hidden rounded-md border border-border bg-background shadow-lg">\n {suggestions.map((s, i) => (\n <button\n key={s.place_id}\n type="button"\n role="option"\n aria-selected={i === highlightedIndex}\n onMouseDown={(e) => { e.preventDefault(); void selectSuggestion(s); }}\n className={cn("block w-full px-3 py-2.5 text-left text-sm transition-colors", i === highlightedIndex ? "bg-muted text-foreground" : "text-foreground hover:bg-muted/70")}\n >\n {s.description}\n </button>\n ))}\n </div>\n )}\n {addr && (\n <input\n type="text"\n value={addr.apartment || ""}\n onChange={(e) => onValueChange({ ...addr, apartment: e.target.value || undefined })}\n placeholder="Apt, suite, unit (optional)"\n className={inputClass}\n />\n )}\n </div>\n );\n}\n\nfunction LocationInput({\n field,\n value,\n onValueChange,\n classNames,\n inputClass,\n}: {\n field: ProductInputField;\n value: unknown;\n onValueChange: (value: unknown) => void;\n classNames?: CustomerInputFieldsClassNames;\n inputClass: string;\n}): React.ReactElement {\n const clientContext = useOptionalCimplifyClient();\n const loc = (value && typeof value === "object" ? value : null) as LocationValue | null;\n const [inputText, setInputText] = React.useState(loc?.label ?? "");\n const [suggestions, setSuggestions] = React.useState<Array<{ description: string; place_id: string }>>([]);\n const [isOpen, setIsOpen] = React.useState(false);\n const [highlightedIndex, setHighlightedIndex] = React.useState(-1);\n const debounceRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);\n const seqRef = React.useRef(0);\n const sessionTokenRef = React.useRef(createSessionToken());\n const blurTimeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);\n\n React.useEffect(() => {\n return () => {\n if (debounceRef.current) clearTimeout(debounceRef.current);\n if (blurTimeoutRef.current) clearTimeout(blurTimeoutRef.current);\n seqRef.current += 1;\n };\n }, []);\n\n const fetchSuggestions = React.useCallback(async (query: string) => {\n const client = clientContext?.client;\n if (!client || query.length < 3) {\n setSuggestions([]);\n setIsOpen(false);\n return;\n }\n\n const seq = ++seqRef.current;\n const result = await client.places.autocomplete(query, sessionTokenRef.current);\n if (seqRef.current !== seq) return;\n\n if (result.ok && result.value.predictions.length > 0) {\n setSuggestions(result.value.predictions);\n setHighlightedIndex(0);\n setIsOpen(true);\n } else {\n setSuggestions([]);\n setIsOpen(false);\n }\n }, [clientContext]);\n\n const handleInputChange = (nextValue: string) => {\n setInputText(nextValue);\n if (debounceRef.current) clearTimeout(debounceRef.current);\n debounceRef.current = setTimeout(() => void fetchSuggestions(nextValue.trim()), 300);\n };\n\n const selectSuggestion = React.useCallback(async (suggestion: { description: string; place_id: string }) => {\n if (debounceRef.current) clearTimeout(debounceRef.current);\n if (blurTimeoutRef.current) clearTimeout(blurTimeoutRef.current);\n\n const label = suggestion.description.split(",")[0]?.trim() || suggestion.description;\n setInputText(label);\n setSuggestions([]);\n setIsOpen(false);\n\n const client = clientContext?.client;\n if (!client) return;\n\n const result = await client.places.details(suggestion.place_id, sessionTokenRef.current);\n sessionTokenRef.current = createSessionToken();\n\n if (result.ok) {\n const d = result.value;\n onValueChange({\n latitude: d.latitude,\n longitude: d.longitude,\n label: d.formatted_address || label,\n } satisfies LocationValue);\n setInputText(d.formatted_address || label);\n }\n }, [clientContext, onValueChange]);\n\n return (\n <div className={cn("relative", classNames?.locationInput)}>\n <input\n type="text"\n value={inputText}\n onChange={(e) => handleInputChange(e.target.value)}\n onFocus={() => { if (suggestions.length > 0) setIsOpen(true); }}\n onBlur={() => { blurTimeoutRef.current = setTimeout(() => setIsOpen(false), 120); }}\n onKeyDown={(e) => {\n if (e.key === "Escape") { setIsOpen(false); return; }\n if (!isOpen || suggestions.length === 0) return;\n if (e.key === "ArrowDown") { e.preventDefault(); setHighlightedIndex((i) => i < suggestions.length - 1 ? i + 1 : 0); }\n else if (e.key === "ArrowUp") { e.preventDefault(); setHighlightedIndex((i) => i > 0 ? i - 1 : suggestions.length - 1); }\n else if (e.key === "Enter" && highlightedIndex >= 0) { e.preventDefault(); void selectSuggestion(suggestions[highlightedIndex]); }\n }}\n placeholder={field.placeholder || "Search for a location..."}\n required={field.is_required}\n autoComplete="off"\n aria-autocomplete="list"\n aria-expanded={isOpen && suggestions.length > 0}\n className={inputClass}\n />\n {isOpen && suggestions.length > 0 && (\n <div role="listbox" className="absolute left-0 right-0 top-full z-20 mt-1 overflow-hidden rounded-md border border-border bg-background shadow-lg">\n {suggestions.map((s, i) => (\n <button\n key={s.place_id}\n type="button"\n role="option"\n aria-selected={i === highlightedIndex}\n onMouseDown={(e) => { e.preventDefault(); void selectSuggestion(s); }}\n className={cn("block w-full px-3 py-2.5 text-left text-sm transition-colors", i === highlightedIndex ? "bg-muted text-foreground" : "text-foreground hover:bg-muted/70")}\n >\n {s.description}\n </button>\n ))}\n </div>\n )}\n {loc && (\n <p className="text-xs text-muted-foreground mt-1">\n {loc.latitude.toFixed(4)}, {loc.longitude.toFixed(4)}\n </p>\n )}\n </div>\n );\n}\n\nfunction PhoneInput({\n field,\n value,\n onValueChange,\n classNames,\n inputClass,\n}: {\n field: ProductInputField;\n value: unknown;\n onValueChange: (value: unknown) => void;\n classNames?: CustomerInputFieldsClassNames;\n inputClass: string;\n}): React.ReactElement {\n const phone = (value && typeof value === "object" ? value : null) as PhoneValue | null;\n const [code, setCode] = React.useState(phone?.country_code ?? "+1");\n const [number, setNumber] = React.useState(phone?.number ?? "");\n\n const update = (nextCode: string, nextNumber: string) => {\n setCode(nextCode);\n setNumber(nextNumber);\n if (nextNumber) {\n onValueChange({\n country_code: nextCode,\n number: nextNumber,\n formatted: `${nextCode} ${nextNumber}`,\n } satisfies PhoneValue);\n } else {\n onValueChange(undefined);\n }\n };\n\n return (\n <div className={cn("flex gap-2", classNames?.phoneInput)}>\n <input\n type="text"\n value={code}\n onChange={(e) => update(e.target.value, number)}\n placeholder="+1"\n className={cn(inputClass, "w-20 shrink-0")}\n />\n <input\n type="tel"\n value={number}\n onChange={(e) => update(code, e.target.value)}\n placeholder={field.placeholder || "Phone number"}\n required={field.is_required}\n className={inputClass}\n />\n </div>\n );\n}\n\nfunction SignatureInput({\n field,\n value,\n onValueChange,\n classNames,\n}: {\n field: ProductInputField;\n value: unknown;\n onValueChange: (value: unknown) => void;\n classNames?: CustomerInputFieldsClassNames;\n}): React.ReactElement {\n const sig = (value && typeof value === "object" ? value : null) as SignatureValue | null;\n const canvasRef = React.useRef<HTMLCanvasElement>(null);\n const isDrawing = React.useRef(false);\n\n const getCtx = () => canvasRef.current?.getContext("2d") ?? null;\n\n const startDraw = (e: React.PointerEvent) => {\n const ctx = getCtx();\n if (!ctx) return;\n isDrawing.current = true;\n const rect = canvasRef.current!.getBoundingClientRect();\n ctx.beginPath();\n ctx.moveTo(e.clientX - rect.left, e.clientY - rect.top);\n canvasRef.current!.setPointerCapture(e.pointerId);\n };\n\n const draw = (e: React.PointerEvent) => {\n if (!isDrawing.current) return;\n const ctx = getCtx();\n if (!ctx) return;\n const rect = canvasRef.current!.getBoundingClientRect();\n ctx.lineWidth = 2;\n ctx.lineCap = "round";\n const style = getComputedStyle(canvasRef.current!);\n ctx.strokeStyle = style.color || "#000";\n ctx.lineTo(e.clientX - rect.left, e.clientY - rect.top);\n ctx.stroke();\n };\n\n const endDraw = () => {\n if (!isDrawing.current) return;\n isDrawing.current = false;\n const canvas = canvasRef.current;\n if (!canvas) return;\n onValueChange({\n data_url: canvas.toDataURL("image/png"),\n signer_name: sig?.signer_name,\n } satisfies SignatureValue);\n };\n\n const clear = () => {\n const canvas = canvasRef.current;\n if (!canvas) return;\n const ctx = canvas.getContext("2d");\n if (ctx) ctx.clearRect(0, 0, canvas.width, canvas.height);\n onValueChange(undefined);\n };\n\n return (\n <div className={cn("space-y-2", classNames?.signatureCanvas)}>\n <div className="relative rounded-md border border-input overflow-hidden">\n <canvas\n ref={canvasRef}\n width={400}\n height={150}\n className="w-full touch-none cursor-crosshair bg-background text-foreground"\n onPointerDown={startDraw}\n onPointerMove={draw}\n onPointerUp={endDraw}\n onPointerLeave={endDraw}\n />\n {sig?.data_url && (\n <button\n type="button"\n onClick={clear}\n className="absolute top-1.5 right-1.5 text-xs text-muted-foreground hover:text-destructive transition-colors"\n >\n Clear\n </button>\n )}\n </div>\n {!sig?.data_url && (\n <p className="text-xs text-muted-foreground">\n {field.placeholder || "Draw your signature above"}\n </p>\n )}\n </div>\n );\n}\n\nfunction createSessionToken(): string {\n if (typeof crypto !== "undefined" && crypto.randomUUID) {\n return crypto.randomUUID();\n }\n return `places_${Date.now()}_${Math.random().toString(16).slice(2)}`;\n}\n' }] }, "staff-picker": { "name": "staff-picker", "title": "StaffPicker", "description": "Staff member selection list with avatar and bio.", "type": "component", "registryDependencies": ["cn"], "files": [{ "path": "staff-picker.tsx", "content": '"use client";\n\nimport { Radio } from "@base-ui/react/radio";\nimport { RadioGroup } from "@base-ui/react/radio-group";\nimport React from "react";\nimport type { Staff } from "@cimplify/sdk";\nimport { cn } from "@cimplify/sdk/react";\n\nconst ANY_VALUE = "__any__";\n\nexport interface StaffPickerClassNames {\n root?: string;\n option?: string;\n avatar?: string;\n name?: string;\n bio?: string;\n}\n\nexport interface StaffPickerProps {\n /** List of available staff members. */\n staff: Staff[];\n /** Currently selected staff ID, or null for "Any available". */\n selectedStaffId?: string | null;\n /** Called when a staff member is selected. Passes null for "Any available". */\n onStaffSelect?: (staffId: string | null) => void;\n /** Show "Any available" option. Default: true. */\n showAnyOption?: boolean;\n /** Label for the "Any available" option. */\n anyLabel?: string;\n className?: string;\n classNames?: StaffPickerClassNames;\n}\n\nexport function StaffPicker({\n staff,\n selectedStaffId,\n onStaffSelect,\n showAnyOption = true,\n anyLabel = "Any available",\n className,\n classNames,\n}: StaffPickerProps): React.ReactElement {\n const groupValue =\n selectedStaffId === null ? ANY_VALUE : (selectedStaffId ?? "");\n\n return (\n <RadioGroup\n data-cimplify-staff-picker\n className={cn(className, classNames?.root)}\n value={groupValue}\n onValueChange={(value) => {\n onStaffSelect?.(value === ANY_VALUE ? null : value);\n }}\n >\n {showAnyOption && (\n <Radio.Root\n value={ANY_VALUE}\n data-cimplify-staff-option\n data-selected={selectedStaffId === null || undefined}\n data-any\n className={classNames?.option}\n >\n <span data-cimplify-staff-name className={classNames?.name}>\n {anyLabel}\n </span>\n </Radio.Root>\n )}\n {staff.map((member) => (\n <Radio.Root\n key={member.id}\n value={member.id}\n data-cimplify-staff-option\n data-selected={selectedStaffId === member.id || undefined}\n className={classNames?.option}\n >\n {member.avatar_url && (\n <img\n src={member.avatar_url}\n alt={member.name}\n data-cimplify-staff-avatar\n className={classNames?.avatar}\n />\n )}\n <span data-cimplify-staff-name className={classNames?.name}>\n {member.name}\n </span>\n {member.bio && (\n <span data-cimplify-staff-bio className={classNames?.bio}>\n {member.bio}\n </span>\n )}\n </Radio.Root>\n ))}\n </RadioGroup>\n );\n}\n' }] }, "checkout-page": { "name": "checkout-page", "title": "CheckoutPage", "description": "Multi-step checkout with auth, address, and payment.", "type": "component", "registryDependencies": ["cn"], "files": [{ "path": "checkout-page.tsx", "content": '"use client";\n\nimport React from "react";\nimport type { ProcessCheckoutResult } from "@cimplify/sdk";\nimport { useCimplifyClient } from "@cimplify/sdk/react";\nimport { CimplifyCheckout } from "@cimplify/sdk/react";\nimport type { CimplifyCheckoutProps } from "@cimplify/sdk/react";\nimport { cn } from "@cimplify/sdk/react";\n\nexport interface CheckoutPageClassNames {\n root?: string;\n header?: string;\n title?: string;\n checkout?: string;\n}\n\nexport interface CheckoutPageProps {\n /** Page title. */\n title?: string;\n /** Called after successful checkout. */\n onComplete: (result: ProcessCheckoutResult) => void;\n /** Called on checkout failure. */\n onError?: (error: { code: string; message: string }) => void;\n /** Props forwarded to CimplifyCheckout. */\n checkoutProps?: Partial<\n Omit<CimplifyCheckoutProps, "client" | "onComplete" | "onError">\n >;\n className?: string;\n classNames?: CheckoutPageClassNames;\n}\n\n/**\n * CheckoutPage \u2014 thin page shell around CimplifyCheckout.\n *\n * Reads the CimplifyClient from CimplifyProvider context,\n * renders a page header, and delegates all checkout logic\n * to CimplifyCheckout.\n */\nexport function CheckoutPage({\n title = "Checkout",\n onComplete,\n onError,\n checkoutProps,\n className,\n classNames,\n}: CheckoutPageProps): React.ReactElement {\n const { client } = useCimplifyClient();\n\n return (\n <div data-cimplify-checkout-page className={cn(className, classNames?.root)}>\n <div data-cimplify-checkout-header className={classNames?.header}>\n <h1 data-cimplify-checkout-title className={classNames?.title}>\n {title}\n </h1>\n </div>\n\n <CimplifyCheckout\n client={client}\n onComplete={onComplete}\n onError={onError}\n className={classNames?.checkout}\n {...checkoutProps}\n />\n </div>\n );\n}\n' }] }, "session-message-banner": { "name": "session-message-banner", "title": "SessionMessageBanner", "description": "Top-of-page banner for session-scoped messages (promo nudges, abandoned cart prompts, low-stock alerts) with dismiss tracking.", "type": "component", "registryDependencies": ["cn"], "files": [{ "path": "session-message-banner.tsx", "content": '"use client";\n\nimport React from "react";\nimport type { SessionMessage } from "../activity";\nimport { useActivityState } from "./hooks/use-activity-state";\nimport { cn } from "@cimplify/sdk/react";\n\nexport interface SessionMessageBannerClassNames {\n root?: string;\n message?: string;\n text?: string;\n dismissButton?: string;\n empty?: string;\n}\n\nexport interface SessionMessageBannerProps {\n /** Override messages (skips fetch). For SSR, pass pre-fetched messages. */\n messages?: SessionMessage[];\n /** Called when a message is dismissed. Receives the message code. */\n onDismiss?: (code: string) => void;\n /** Custom message renderer. */\n renderMessage?: (message: SessionMessage) => React.ReactNode;\n className?: string;\n classNames?: SessionMessageBannerClassNames;\n}\n\n/**\n * SessionMessageBanner \u2014 renders activity-based session messages.\n *\n * Displays dismissible message cards with level-based styling (info, promotion, urgency, suggestion).\n * Returns `null` when there are no messages.\n */\nexport function SessionMessageBanner({\n messages: messagesProp,\n onDismiss,\n renderMessage,\n className,\n classNames,\n}: SessionMessageBannerProps): React.ReactElement | null {\n const { messages: fetched, isLoading, dismissMessage } = useActivityState({\n enabled: messagesProp === undefined,\n });\n\n const messages = messagesProp ?? fetched;\n\n const handleDismiss = async (code: string): Promise<void> => {\n if (onDismiss) {\n onDismiss(code);\n } else {\n await dismissMessage(code);\n }\n };\n\n if (isLoading && messages.length === 0) {\n return (\n <div\n data-cimplify-session-message-banner\n aria-busy="true"\n className={cn(className, classNames?.root)}\n />\n );\n }\n\n if (messages.length === 0) {\n return null;\n }\n\n return (\n <div data-cimplify-session-message-banner className={cn(className, classNames?.root)}>\n {messages.map((msg) => (\n <div\n key={msg.code}\n data-cimplify-session-message\n data-level={msg.level}\n className={classNames?.message}\n >\n {renderMessage ? (\n renderMessage(msg)\n ) : (\n <>\n <span data-cimplify-session-message-text className={classNames?.text}>\n {msg.text}\n </span>\n {msg.dismissible && (\n <button\n type="button"\n onClick={() => void handleDismiss(msg.code)}\n data-cimplify-session-message-dismiss\n className={classNames?.dismissButton}\n aria-label="Dismiss message"\n >\n &times;\n </button>\n )}\n </>\n )}\n </div>\n ))}\n </div>\n );\n}\n' }] }, "resource-picker": { "name": "resource-picker", "title": "ResourcePicker", "description": "Staff / room / resource picker for bookable services \u2014 used by services and restaurant reservation flows.", "type": "component", "registryDependencies": ["cn"], "files": [{ "path": "resource-picker.tsx", "content": '"use client";\n\nimport { Radio } from "@base-ui/react/radio";\nimport { RadioGroup } from "@base-ui/react/radio-group";\nimport React from "react";\nimport type { Room } from "../types/business";\nimport { cn } from "@cimplify/sdk/react";\n\nconst ANY_VALUE = "__any__";\n\nexport interface Resource {\n id: string;\n name: string;\n description?: string;\n capacity?: number;\n floor?: string;\n image_url?: string;\n is_available?: boolean;\n}\n\nexport interface ResourcePickerClassNames {\n root?: string;\n option?: string;\n image?: string;\n name?: string;\n description?: string;\n meta?: string;\n capacity?: string;\n floor?: string;\n unavailable?: string;\n}\n\nexport interface ResourcePickerProps {\n /** List of available resources (rooms, equipment, etc.). */\n resources: Resource[];\n /** Currently selected resource ID, or null for "Any available". */\n selectedResourceId?: string | null;\n /** Called when a resource is selected. Passes null for "Any available". */\n onResourceSelect?: (resourceId: string | null) => void;\n /** Show "Any available" option. Default: true. */\n showAnyOption?: boolean;\n /** Label for the "Any available" option. */\n anyLabel?: string;\n /** Custom image renderer (e.g. Next.js Image). */\n renderImage?: (props: {\n src: string;\n alt: string;\n className?: string;\n }) => React.ReactNode;\n className?: string;\n classNames?: ResourcePickerClassNames;\n}\n\nexport function roomToResource(room: Room): Resource {\n return {\n id: room.id,\n name: room.name,\n capacity: room.capacity,\n floor: room.floor,\n is_available: room.status === "available",\n };\n}\n\nexport function ResourcePicker({\n resources,\n selectedResourceId,\n onResourceSelect,\n showAnyOption = true,\n anyLabel = "Any available",\n renderImage,\n className,\n classNames,\n}: ResourcePickerProps): React.ReactElement {\n const groupValue =\n selectedResourceId === null ? ANY_VALUE : (selectedResourceId ?? "");\n\n return (\n <RadioGroup\n data-cimplify-resource-picker\n className={cn("flex flex-col gap-2", className, classNames?.root)}\n value={groupValue}\n onValueChange={(value) => {\n onResourceSelect?.(value === ANY_VALUE ? null : value);\n }}\n >\n {showAnyOption && (\n <Radio.Root\n value={ANY_VALUE}\n data-cimplify-resource-option\n data-selected={selectedResourceId === null || undefined}\n data-any\n className={cn(\n "flex items-center gap-3 rounded-lg border border-border p-3 transition-colors cursor-pointer",\n "hover:bg-muted data-[checked]:border-primary data-[checked]:bg-primary/5",\n classNames?.option,\n )}\n >\n <span data-cimplify-resource-name className={cn("font-medium text-foreground", classNames?.name)}>\n {anyLabel}\n </span>\n </Radio.Root>\n )}\n {resources.map((resource) => {\n const unavailable = resource.is_available === false;\n return (\n <Radio.Root\n key={resource.id}\n value={resource.id}\n disabled={unavailable}\n data-cimplify-resource-option\n data-selected={selectedResourceId === resource.id || undefined}\n data-unavailable={unavailable || undefined}\n className={cn(\n "flex items-center gap-3 rounded-lg border border-border p-3 transition-colors cursor-pointer",\n "hover:bg-muted data-[checked]:border-primary data-[checked]:bg-primary/5",\n unavailable && "opacity-50 cursor-not-allowed",\n classNames?.option,\n unavailable ? classNames?.unavailable : undefined,\n )}\n >\n {resource.image_url && (\n renderImage ? (\n renderImage({\n src: resource.image_url,\n alt: resource.name,\n className: classNames?.image,\n })\n ) : (\n <img\n src={resource.image_url}\n alt={resource.name}\n data-cimplify-resource-image\n className={cn("w-10 h-10 rounded-lg object-cover", classNames?.image)}\n />\n )\n )}\n <div className="flex flex-col gap-0.5 flex-1 min-w-0">\n <span data-cimplify-resource-name className={cn("font-medium text-foreground", classNames?.name)}>\n {resource.name}\n </span>\n {resource.description && (\n <span data-cimplify-resource-description className={cn("text-sm text-muted-foreground truncate", classNames?.description)}>\n {resource.description}\n </span>\n )}\n <div data-cimplify-resource-meta className={cn("flex items-center gap-2 text-xs text-muted-foreground", classNames?.meta)}>\n {resource.capacity !== undefined && (\n <span data-cimplify-resource-capacity className={classNames?.capacity}>\n Up to {resource.capacity}\n </span>\n )}\n {resource.floor && (\n <span data-cimplify-resource-floor className={classNames?.floor}>\n {resource.floor}\n </span>\n )}\n </div>\n </div>\n </Radio.Root>\n );\n })}\n </RadioGroup>\n );\n}\n' }] }, "category-grid": { "name": "category-grid", "title": "CategoryGrid", "description": "Responsive grid of category cards.", "type": "component", "registryDependencies": ["cn"], "files": [{ "path": "category-grid.tsx", "content": '"use client";\n\nimport React from "react";\nimport type { Category } from "@cimplify/sdk";\nimport { useCategories } from "@cimplify/sdk/react";\nimport { cn } from "@cimplify/sdk/react";\n\nexport interface CategoryGridClassNames {\n root?: string;\n item?: string;\n name?: string;\n description?: string;\n count?: string;\n empty?: string;\n}\n\nexport interface CategoryGridProps {\n /** Override categories (skips useCategories fetch). */\n categories?: Category[];\n /** Called when a category card is clicked. */\n onSelect?: (category: Category) => void;\n /** Custom card renderer. */\n renderCard?: (category: Category) => React.ReactNode;\n /** Responsive column counts. */\n columns?: { sm?: number; md?: number; lg?: number };\n /** Text shown when empty. */\n emptyMessage?: string;\n className?: string;\n classNames?: CategoryGridClassNames;\n}\n\n/**\n * CategoryGrid \u2014 responsive grid of category cards.\n *\n * Fetches categories via `useCategories` unless overridden.\n * Uses `React.useId()` for hydration-safe responsive CSS.\n */\nexport function CategoryGrid({\n categories: categoriesProp,\n onSelect,\n renderCard,\n columns,\n emptyMessage,\n className,\n classNames,\n}: CategoryGridProps): React.ReactElement {\n const { categories: fetched, isLoading } = useCategories({\n enabled: categoriesProp === undefined,\n });\n const categories = categoriesProp ?? fetched;\n\n const rawId = React.useId();\n const gridId = `cimplify-cat-grid-${rawId.replace(/:/g, "")}`;\n\n const sm = columns?.sm ?? 2;\n const md = columns?.md ?? 3;\n const lg = columns?.lg ?? 4;\n\n if (isLoading && categories.length === 0) {\n return (\n <div\n data-cimplify-category-grid\n aria-busy="true"\n className={cn(className, classNames?.root)}\n />\n );\n }\n\n if (categories.length === 0) {\n return (\n <div\n data-cimplify-category-grid\n data-empty\n className={cn(className, classNames?.root, classNames?.empty)}\n >\n <p>{emptyMessage ?? "No categories found"}</p>\n </div>\n );\n }\n\n const css = [\n `#${gridId}{display:grid;grid-template-columns:repeat(${sm},1fr);gap:1rem}`,\n `@media(min-width:768px){#${gridId}{grid-template-columns:repeat(${md},1fr)}}`,\n `@media(min-width:1024px){#${gridId}{grid-template-columns:repeat(${lg},1fr)}}`,\n ].join("");\n\n return (\n <>\n <style dangerouslySetInnerHTML={{ __html: css }} />\n <div\n id={gridId}\n data-cimplify-category-grid\n className={cn(className, classNames?.root)}\n >\n {categories.map((category: Category) => (\n <button\n key={category.id}\n type="button"\n onClick={() => onSelect?.(category)}\n data-cimplify-category-card\n className={classNames?.item}\n >\n {renderCard ? (\n renderCard(category)\n ) : (\n <>\n <span data-cimplify-category-name className={classNames?.name}>\n {category.name}\n </span>\n {category.description && (\n <span data-cimplify-category-description className={classNames?.description}>\n {category.description}\n </span>\n )}\n {category.product_count != null && (\n <span data-cimplify-category-count className={classNames?.count}>\n {category.product_count} {category.product_count === 1 ? "product" : "products"}\n </span>\n )}\n </>\n )}\n </button>\n ))}\n </div>\n </>\n );\n}\n' }] }, "booking-page": { "name": "booking-page", "title": "BookingPage", "description": "Multi-step booking flow: service, staff, resource, date/slot, confirmation.", "type": "component", "registryDependencies": ["date-slot-picker", "staff-picker", "resource-picker", "price", "cn"], "files": [{ "path": "booking-page.tsx", "content": '"use client";\n\nimport React, { useState, useCallback } from "react";\nimport type { Product } from "@cimplify/sdk";\nimport type { Service, Staff, AvailableSlot, CustomerBooking } from "@cimplify/sdk";\nimport type { Resource } from "./resource-picker";\nimport { useCart } from "@cimplify/sdk/react";\nimport { useCimplifyClient } from "@cimplify/sdk/react";\nimport { DateSlotPicker } from "@cimplify/sdk/react";\nimport { StaffPicker } from "@cimplify/sdk/react";\nimport { ResourcePicker } from "./resource-picker";\nimport { Price } from "@cimplify/sdk/react";\nimport { cn } from "@cimplify/sdk/react";\nimport { formatDuration } from "./utils/format-duration";\nimport { parsePrice } from "@cimplify/sdk";\n\nexport interface BookingPageClassNames {\n root?: string;\n header?: string;\n title?: string;\n serviceInfo?: string;\n step?: string;\n stepTitle?: string;\n summary?: string;\n summaryRow?: string;\n confirmButton?: string;\n backButton?: string;\n error?: string;\n rescheduleInfo?: string;\n depositInfo?: string;\n cancellationPolicy?: string;\n resourceStep?: string;\n}\n\nexport interface BookingPageProps {\n /** The service being booked. */\n service: Service;\n /** The underlying product (for deposit/cancellation fields). Falls back to service fields. */\n product?: Product;\n /** Optional staff list for staff selection step. */\n staff?: Staff[];\n /** Optional resources for resource selection step (rooms, equipment, etc.). */\n resources?: Resource[];\n /** Number of participants. */\n participantCount?: number;\n /** Page title. */\n title?: string;\n /** Called after successfully adding to cart. */\n onBooked?: (slot: AvailableSlot, staffId: string | null) => void;\n /** Called when user wants to go back. */\n onBack?: () => void;\n /** Existing booking for reschedule mode. */\n existingBooking?: CustomerBooking;\n /** Called after a successful reschedule. */\n onRescheduled?: (booking: CustomerBooking, newSlot: AvailableSlot) => void;\n /** Show price on slots. Default: true. */\n showPrice?: boolean;\n className?: string;\n classNames?: BookingPageClassNames;\n}\n\nconst STEP = {\n SELECT_SLOT: "select-slot",\n SELECT_RESOURCE: "select-resource",\n SELECT_STAFF: "select-staff",\n CONFIRM: "confirm",\n} as const;\n\ntype BookingStep = (typeof STEP)[keyof typeof STEP];\n\nexport function BookingPage({\n service,\n product,\n staff,\n resources,\n participantCount,\n title,\n onBooked,\n onBack,\n existingBooking,\n onRescheduled,\n showPrice = true,\n className,\n classNames,\n}: BookingPageProps): React.ReactElement {\n const { addItem } = useCart();\n const { client } = useCimplifyClient();\n\n const isReschedule = existingBooking !== undefined;\n\n const [step, setStep] = useState<BookingStep>(STEP.SELECT_SLOT);\n const [selectedSlot, setSelectedSlot] = useState<AvailableSlot | null>(null);\n const [selectedDate, setSelectedDate] = useState<string | null>(null);\n const [selectedStaffId, setSelectedStaffId] = useState<string | null>(null);\n const [selectedResourceId, setSelectedResourceId] = useState<string | null>(null);\n const [isSubmitting, setIsSubmitting] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n const hasResourceStep = resources && resources.length > 0;\n const hasStaffStep = staff && staff.length > 0;\n\n // Deposit info \u2014 prefer product fields, then fall back to service metadata\n const depositType = product?.deposit_type;\n const depositAmount = product?.deposit_amount;\n const hasDeposit = depositType !== undefined && depositType !== "none" && depositAmount !== undefined && parsePrice(depositAmount) !== 0;\n\n // Cancellation policy\n const cancellationMinutes = product?.cancellation_window_minutes;\n const noShowFee = product?.no_show_fee;\n\n const handleSlotSelect = useCallback(\n (slot: AvailableSlot, date: string) => {\n setSelectedSlot(slot);\n setSelectedDate(date);\n setError(null);\n if (hasResourceStep) {\n setStep(STEP.SELECT_RESOURCE);\n } else if (hasStaffStep) {\n setStep(STEP.SELECT_STAFF);\n } else {\n setStep(STEP.CONFIRM);\n }\n },\n [hasResourceStep, hasStaffStep],\n );\n\n const handleResourceSelect = useCallback(\n (resourceId: string | null) => {\n setSelectedResourceId(resourceId);\n if (hasStaffStep) {\n setStep(STEP.SELECT_STAFF);\n } else {\n setStep(STEP.CONFIRM);\n }\n },\n [hasStaffStep],\n );\n\n const handleStaffSelect = useCallback((staffId: string | null) => {\n setSelectedStaffId(staffId);\n setStep(STEP.CONFIRM);\n }, []);\n\n const handleBack = useCallback(() => {\n setError(null);\n if (step === STEP.CONFIRM && hasStaffStep) {\n setStep(STEP.SELECT_STAFF);\n } else if (step === STEP.CONFIRM && hasResourceStep) {\n setStep(STEP.SELECT_RESOURCE);\n } else if (step === STEP.SELECT_STAFF && hasResourceStep) {\n setStep(STEP.SELECT_RESOURCE);\n } else if (step === STEP.CONFIRM || step === STEP.SELECT_STAFF || step === STEP.SELECT_RESOURCE) {\n setStep(STEP.SELECT_SLOT);\n } else {\n onBack?.();\n }\n }, [step, hasStaffStep, hasResourceStep, onBack]);\n\n const handleConfirm = useCallback(async () => {\n if (!selectedSlot || !selectedDate) return;\n\n setIsSubmitting(true);\n setError(null);\n\n try {\n if (isReschedule && existingBooking) {\n // Reschedule mode \u2014 call the scheduling API instead of adding to cart\n const serviceItem = existingBooking.service_items[0];\n const result = await client.scheduling.rescheduleBooking({\n order_id: existingBooking.order_id,\n line_item_id: serviceItem?.service_id ?? existingBooking.order_id,\n new_start_time: selectedSlot.start_time,\n new_end_time: selectedSlot.end_time,\n new_staff_id: selectedStaffId || undefined,\n reschedule_type: "customer",\n });\n\n if (!result.ok) {\n throw result.error;\n }\n\n onRescheduled?.(existingBooking, selectedSlot);\n } else {\n // Normal booking mode \u2014 add to cart\n const serviceProduct = {\n id: service.product_id || service.id,\n business_id: service.business_id || "",\n category_id: service.category_id || undefined,\n name: service.name,\n slug: service.id,\n description: service.description || undefined,\n image_url: service.image_url || undefined,\n default_price: (service.price || "0") as Product["default_price"],\n type: "service" as const,\n inventory_type: "none" as const,\n variant_strategy: "fetch_all" as const,\n is_active: service.is_available,\n created_at: new Date().toISOString(),\n updated_at: new Date().toISOString(),\n };\n\n await addItem(serviceProduct, 1, {\n scheduledStart: selectedSlot.start_time,\n scheduledEnd: selectedSlot.end_time,\n staffId: selectedStaffId || undefined,\n resourceId: selectedResourceId || undefined,\n });\n\n onBooked?.(selectedSlot, selectedStaffId);\n }\n } catch (err) {\n const fallbackMessage = isReschedule ? "Failed to reschedule booking" : "Failed to add booking to cart";\n setError(err instanceof Error ? err.message : fallbackMessage);\n } finally {\n setIsSubmitting(false);\n }\n }, [selectedSlot, selectedDate, selectedStaffId, service, addItem, onBooked, isReschedule, existingBooking, client, onRescheduled]);\n\n return (\n <div data-cimplify-booking-page className={cn(className, classNames?.root)}>\n <div data-cimplify-booking-page-header className={classNames?.header}>\n {onBack && step === STEP.SELECT_SLOT && (\n <button\n type="button"\n onClick={onBack}\n data-cimplify-booking-page-back\n className={classNames?.backButton}\n >\n Back\n </button>\n )}\n {step !== STEP.SELECT_SLOT && (\n <button\n type="button"\n onClick={handleBack}\n data-cimplify-booking-page-back\n className={classNames?.backButton}\n >\n Back\n </button>\n )}\n <h1 data-cimplify-booking-page-title className={classNames?.title}>\n {title || (isReschedule ? "Reschedule Booking" : `Book ${service.name}`)}\n </h1>\n </div>\n\n {isReschedule && existingBooking && (\n <div data-cimplify-booking-reschedule-info className={classNames?.rescheduleInfo}>\n <span>Rescheduling booking from {new Date(existingBooking.service_items[0]?.scheduled_start ?? existingBooking.created_at).toLocaleDateString()}</span>\n </div>\n )}\n\n <div data-cimplify-booking-service-info className={classNames?.serviceInfo}>\n <span data-cimplify-booking-service-name>{service.name}</span>\n <span data-cimplify-booking-service-duration>{formatDuration(service.duration_minutes, service.duration_unit)}</span>\n {service.price && (\n <span data-cimplify-booking-service-price>\n <Price amount={service.price} />\n </span>\n )}\n {hasDeposit && (\n <span data-cimplify-booking-deposit-info className={classNames?.depositInfo}>\n Deposit: {depositType === "percentage" ? `${parsePrice(depositAmount!)}%` : <Price amount={depositAmount!} />}\n </span>\n )}\n {cancellationMinutes !== undefined && cancellationMinutes > 0 && (\n <span data-cimplify-booking-cancellation-policy className={classNames?.cancellationPolicy}>\n Free cancellation up to {cancellationMinutes >= 60 ? `${Math.floor(cancellationMinutes / 60)} hour${Math.floor(cancellationMinutes / 60) !== 1 ? "s" : ""}` : `${cancellationMinutes} minute${cancellationMinutes !== 1 ? "s" : ""}`} before\n </span>\n )}\n {noShowFee !== undefined && parsePrice(noShowFee) !== 0 && (\n <span data-cimplify-booking-no-show-fee className={classNames?.cancellationPolicy}>\n No-show fee: <Price amount={noShowFee} />\n </span>\n )}\n </div>\n\n {error && (\n <div data-cimplify-booking-error className={classNames?.error}>\n {error}\n </div>\n )}\n\n {step === STEP.SELECT_SLOT && (\n <div data-cimplify-booking-step="select-slot" className={classNames?.step}>\n <h2 data-cimplify-booking-step-title className={classNames?.stepTitle}>\n Select a date &amp; time\n </h2>\n <DateSlotPicker\n serviceId={service.id}\n participantCount={participantCount}\n selectedSlot={selectedSlot}\n onSlotSelect={handleSlotSelect}\n showPrice={showPrice}\n />\n </div>\n )}\n\n {step === STEP.SELECT_RESOURCE && resources && (\n <div data-cimplify-booking-step="select-resource" className={cn(classNames?.step, classNames?.resourceStep)}>\n <h2 data-cimplify-booking-step-title className={classNames?.stepTitle}>\n Choose a room\n </h2>\n <ResourcePicker\n resources={resources}\n selectedResourceId={selectedResourceId}\n onResourceSelect={handleResourceSelect}\n />\n </div>\n )}\n\n {step === STEP.SELECT_STAFF && staff && (\n <div data-cimplify-booking-step="select-staff" className={classNames?.step}>\n <h2 data-cimplify-booking-step-title className={classNames?.stepTitle}>\n Choose a provider\n </h2>\n <StaffPicker\n staff={staff}\n selectedStaffId={selectedStaffId}\n onStaffSelect={handleStaffSelect}\n />\n </div>\n )}\n\n {step === STEP.CONFIRM && selectedSlot && (\n <div data-cimplify-booking-step="confirm" className={classNames?.step}>\n <h2 data-cimplify-booking-step-title className={classNames?.stepTitle}>\n Confirm your booking\n </h2>\n <div data-cimplify-booking-summary className={classNames?.summary}>\n <div data-cimplify-booking-summary-row className={classNames?.summaryRow}>\n <span>Service</span>\n <span>{service.name}</span>\n </div>\n <div data-cimplify-booking-summary-row className={classNames?.summaryRow}>\n <span>Date</span>\n <span>{selectedDate}</span>\n </div>\n <div data-cimplify-booking-summary-row className={classNames?.summaryRow}>\n <span>Time</span>\n <span>\n {new Date(selectedSlot.start_time).toLocaleTimeString(undefined, {\n hour: "numeric",\n minute: "2-digit",\n })}\n </span>\n </div>\n <div data-cimplify-booking-summary-row className={classNames?.summaryRow}>\n <span>Duration</span>\n <span>{formatDuration(service.duration_minutes, service.duration_unit)}</span>\n </div>\n {selectedResourceId && resources && (\n <div data-cimplify-booking-summary-row className={classNames?.summaryRow}>\n <span>Room</span>\n <span>{resources.find((r) => r.id === selectedResourceId)?.name ?? "Selected"}</span>\n </div>\n )}\n {selectedStaffId && staff && (\n <div data-cimplify-booking-summary-row className={classNames?.summaryRow}>\n <span>Provider</span>\n <span>{staff.find((s) => s.id === selectedStaffId)?.name ?? "Selected"}</span>\n </div>\n )}\n {(selectedSlot.price || service.price) && (\n <div data-cimplify-booking-summary-row className={classNames?.summaryRow}>\n <span>Price</span>\n <span>\n <Price amount={selectedSlot.price || service.price!} />\n </span>\n </div>\n )}\n {hasDeposit && (\n <div data-cimplify-booking-summary-row className={cn(classNames?.summaryRow, classNames?.depositInfo)}>\n <span>Deposit</span>\n <span>\n {depositType === "percentage" ? `${parsePrice(depositAmount!)}%` : <Price amount={depositAmount!} />}\n </span>\n </div>\n )}\n </div>\n <button\n type="button"\n onClick={handleConfirm}\n disabled={isSubmitting}\n data-cimplify-booking-confirm\n className={classNames?.confirmButton}\n >\n {isSubmitting\n ? (isReschedule ? "Rescheduling\u2026" : "Adding to cart\u2026")\n : (isReschedule ? "Reschedule" : "Confirm Booking")}\n </button>\n </div>\n )}\n </div>\n );\n}\n' }] }, "product-card": { "name": "product-card", "title": "ProductCard", "description": "Product display card with modal or link mode.", "type": "component", "registryDependencies": ["price", "product-sheet", "cn"], "files": [{ "path": "product-card.tsx", "content": '"use client";\n\nimport React, { useCallback, useState } from "react";\nimport { Dialog } from "@base-ui/react/dialog";\nimport type { Product, ProductWithDetails } from "@cimplify/sdk";\nimport { PRODUCT_TYPE, RENDER_HINT, DURATION_UNIT } from "@cimplify/sdk";\nimport { useProduct } from "@cimplify/sdk/react";\nimport { Price } from "@cimplify/sdk/react";\nimport { ProductSheet } from "@cimplify/sdk/react";\nimport { CardVariant } from "@cimplify/sdk/react";\nimport type { CardLayoutProps } from "@cimplify/sdk/react";\nimport { FoodProductCard } from "@cimplify/sdk/react";\nimport { RetailProductCard } from "@cimplify/sdk/react";\nimport { WholesaleProductCard } from "@cimplify/sdk/react";\nimport { DigitalProductCard } from "@cimplify/sdk/react";\nimport { StandardServiceCard } from "@cimplify/sdk/react";\nimport { RentalServiceCard } from "@cimplify/sdk/react";\nimport { AccommodationCard } from "@cimplify/sdk/react";\nimport { LeaseServiceCard } from "@cimplify/sdk/react";\nimport { SubscriptionCard } from "@cimplify/sdk/react";\nimport { BundleProductCard } from "./cards/bundle-product-card";\nimport { CompositeProductCard } from "./cards/composite-product-card";\nimport { cn } from "@cimplify/sdk/react";\n\nconst ASPECT_STYLES: Record<string, React.CSSProperties> = {\n square: { aspectRatio: "1/1" },\n "4/3": { aspectRatio: "4/3" },\n "16/10": { aspectRatio: "16/10" },\n "3/4": { aspectRatio: "3/4" },\n};\n\nexport interface ProductCardClassNames {\n root?: string;\n imageContainer?: string;\n image?: string;\n body?: string;\n name?: string;\n description?: string;\n price?: string;\n badges?: string;\n badge?: string;\n modal?: string;\n modalOverlay?: string;\n}\n\nexport interface ProductCardProps {\n /** The product to display. */\n product: Product;\n /** Display mode: "card" opens a modal, "page" renders as a link. Auto-detected from product.display_mode. */\n displayMode?: "card" | "page";\n /** Explicit card variant. Auto-detected from product data when omitted. */\n variant?: CardVariant;\n /** Per-slug card map. Highest priority \u2014 maps a product slug to a custom card component. */\n cards?: Record<string, React.ComponentType<CardLayoutProps>>;\n /** Link href for page mode. Default: `/menu/${product.slug}` */\n href?: string;\n /** Custom modal content renderer. Receives the fully-loaded product and a close callback. */\n renderModal?: (product: ProductWithDetails, onClose: () => void) => React.ReactNode;\n /** Custom image renderer (e.g. Next.js Image). */\n renderImage?: (props: {\n src: string;\n alt: string;\n className?: string;\n }) => React.ReactNode;\n /** Custom link renderer for page mode (e.g. Next.js Link). */\n renderLink?: (props: {\n href: string;\n className?: string;\n children: React.ReactNode;\n }) => React.ReactElement;\n /** Called when quick-add button is clicked (if the resolved card supports it). */\n onQuickAdd?: (product: Product) => void;\n /** Replace the entire default card body. */\n children?: React.ReactNode;\n /** Image aspect ratio. Default: "4/3". */\n aspectRatio?: "square" | "4/3" | "16/10" | "3/4";\n className?: string;\n classNames?: ProductCardClassNames;\n}\n\nconst BUILT_IN_CARDS: Record<string, React.ComponentType<CardLayoutProps>> = {\n [CardVariant.Food]: FoodProductCard,\n [CardVariant.Retail]: RetailProductCard,\n [CardVariant.Wholesale]: WholesaleProductCard,\n [CardVariant.Digital]: DigitalProductCard,\n [CardVariant.Standard]: StandardServiceCard,\n [CardVariant.Rental]: RentalServiceCard,\n [CardVariant.Accommodation]: AccommodationCard,\n [CardVariant.Lease]: LeaseServiceCard,\n [CardVariant.Bundle]: BundleProductCard,\n [CardVariant.Composite]: CompositeProductCard,\n [CardVariant.Subscription]: SubscriptionCard,\n};\n\nconst RENTAL_UNITS = new Set<string>([DURATION_UNIT.Days, DURATION_UNIT.Hours]);\nconst LEASE_UNITS = new Set<string>([DURATION_UNIT.Weeks, DURATION_UNIT.Months, DURATION_UNIT.Years]);\n\nfunction resolveCardVariant(product: Product): CardVariant {\n if (product.type === PRODUCT_TYPE.Bundle) {\n return CardVariant.Bundle;\n }\n if (product.type === PRODUCT_TYPE.Composite) {\n return CardVariant.Composite;\n }\n if (product.quantity_pricing && product.quantity_pricing.length > 1) {\n return CardVariant.Wholesale;\n }\n if (product.type === PRODUCT_TYPE.Digital) {\n return CardVariant.Digital;\n }\n if (product.type === PRODUCT_TYPE.Service) {\n if (product.duration_unit && RENTAL_UNITS.has(product.duration_unit)) {\n return CardVariant.Rental;\n }\n if (product.duration_unit === DURATION_UNIT.Nights) {\n return CardVariant.Accommodation;\n }\n if (product.duration_unit && LEASE_UNITS.has(product.duration_unit)) {\n return CardVariant.Lease;\n }\n if (product.billing_plans && product.billing_plans.length > 0 && !product.duration_minutes) {\n return CardVariant.Subscription;\n }\n return CardVariant.Standard;\n }\n if (product.render_hint === RENDER_HINT.Food) {\n return CardVariant.Food;\n }\n if (product.render_hint === RENDER_HINT.Physical) {\n return CardVariant.Retail;\n }\n return CardVariant.Retail;\n}\n\n/**\n * ProductCard \u2014 a product display card with two modes:\n *\n * - **card** (default): clickable button that opens a Base UI Dialog modal\n * - **page**: a plain `<a>` link for SEO-friendly product pages\n */\nexport function ProductCard({\n product,\n displayMode,\n variant,\n cards,\n href,\n renderModal,\n renderImage,\n renderLink,\n onQuickAdd,\n children,\n aspectRatio = "4/3",\n className,\n classNames,\n}: ProductCardProps): React.ReactElement {\n const mode = displayMode ?? product.display_mode ?? "card";\n const [isOpen, setIsOpen] = useState(false);\n const [shouldFetch, setShouldFetch] = useState(false);\n\n // Prefetch on pointer enter, always fetch when open\n const { product: productDetails } = useProduct(\n product.slug ?? product.id,\n { enabled: shouldFetch || isOpen },\n );\n\n const handlePrefetch = useCallback(() => {\n setShouldFetch(true);\n }, []);\n\n const handleOpenChange = useCallback((open: boolean) => {\n setIsOpen(open);\n if (open) {\n setShouldFetch(true);\n }\n }, []);\n\n const handleClose = useCallback(() => {\n setIsOpen(false);\n }, []);\n\n const imageUrl = product.image_url || product.images?.[0];\n\n const cardBody = (() => {\n if (children) return children;\n\n // Resolve card variant: slug map \u2192 explicit variant \u2192 auto-detect\n const SlugCard = cards?.[product.slug];\n if (SlugCard) {\n return <SlugCard product={product} onQuickAdd={onQuickAdd} renderImage={renderImage} renderLink={renderLink} />;\n }\n\n const key = variant ?? resolveCardVariant(product);\n const ResolvedCard = BUILT_IN_CARDS[key];\n if (ResolvedCard) {\n return <ResolvedCard product={product} onQuickAdd={onQuickAdd} renderImage={renderImage} renderLink={renderLink} />;\n }\n\n // Fallback: minimal default\n return (\n <>\n {imageUrl && (\n <div\n data-cimplify-product-card-image-container\n className={cn("overflow-hidden rounded-t-xl", classNames?.imageContainer)}\n style={ASPECT_STYLES[aspectRatio]}\n >\n {renderImage ? (\n renderImage({ src: imageUrl, alt: product.name, className: classNames?.image })\n ) : (\n <img\n src={imageUrl}\n alt={product.name}\n className={cn("w-full h-full object-cover transition-transform duration-300 group-hover:scale-105", classNames?.image)}\n data-cimplify-product-card-image\n />\n )}\n </div>\n )}\n <div data-cimplify-product-card-body className={cn("p-4 flex flex-col gap-1", classNames?.body)}>\n <span data-cimplify-product-card-name className={cn("font-semibold text-foreground leading-tight", classNames?.name)}>\n {product.name}\n </span>\n {product.description && (\n <span data-cimplify-product-card-description className={cn("text-sm text-muted-foreground line-clamp-2", classNames?.description)}>\n {product.description}\n </span>\n )}\n <Price amount={product.default_price} className={cn("text-sm font-medium text-foreground mt-1", classNames?.price)} />\n </div>\n </>\n );\n })();\n\n // Page mode \u2014 render as a link\n if (mode === "page") {\n const linkHref = href ?? `/menu/${product.slug}`;\n const linkClassName = cn("group block no-underline text-[inherit] rounded-xl border border-border bg-background transition-shadow duration-200 hover:shadow-md", className, classNames?.root);\n\n if (renderLink) {\n return renderLink({ href: linkHref, className: linkClassName, children: cardBody });\n }\n\n return (\n <a\n href={linkHref}\n data-cimplify-product-card\n data-display-mode="page"\n className={linkClassName}\n >\n {cardBody}\n </a>\n );\n }\n\n // Card mode \u2014 render as Base UI Dialog\n return (\n <Dialog.Root open={isOpen} onOpenChange={handleOpenChange}>\n <Dialog.Trigger\n onPointerEnter={handlePrefetch}\n data-cimplify-product-card\n data-display-mode="card"\n className={cn(\n "group block w-full text-left p-0 cursor-pointer font-[inherit] text-[inherit] rounded-xl border border-border bg-background transition-shadow duration-200 hover:shadow-md",\n className,\n classNames?.root,\n )}\n >\n {cardBody}\n </Dialog.Trigger>\n\n <Dialog.Portal>\n <Dialog.Backdrop\n data-cimplify-product-card-backdrop\n className={cn(\n "fixed inset-0 z-50 bg-black/50 backdrop-blur-sm transition-opacity",\n classNames?.modalOverlay,\n )}\n />\n <Dialog.Popup\n data-cimplify-product-card-modal\n className={cn(\n "fixed z-50 rounded-2xl p-0 max-w-lg w-full h-fit max-h-[85vh] overflow-auto bg-background shadow-2xl outline-none",\n classNames?.modal,\n )}\n >\n {isOpen && (\n productDetails ? (\n renderModal ? (\n renderModal(productDetails, handleClose)\n ) : (\n <ProductSheet\n product={productDetails}\n onClose={handleClose}\n renderImage={renderImage}\n />\n )\n ) : (\n <div\n data-cimplify-product-card-modal-loading\n aria-busy="true"\n className="flex flex-col"\n >\n <div className="aspect-[5/2] bg-muted animate-pulse" />\n <div className="p-6 space-y-3">\n <div className="h-5 w-3/5 bg-muted rounded animate-pulse" />\n <div className="h-7 w-2/5 bg-muted rounded animate-pulse" />\n <div className="h-4 w-4/5 bg-muted rounded animate-pulse" />\n </div>\n <div className="mx-6 border-t border-border pt-4 pb-6">\n <div className="h-14 bg-muted rounded-full animate-pulse" />\n </div>\n </div>\n )\n )}\n </Dialog.Popup>\n </Dialog.Portal>\n </Dialog.Root>\n );\n}\n' }] }, "service-product-layout": { "name": "service-product-layout", "title": "ServiceProductLayout", "description": "Service booking layout with duration, deposit, staff requirements, and cancellation policy.", "type": "component", "registryDependencies": ["product-image-gallery", "product-customizer", "price", "cn"], "files": [{ "path": "layouts/service-product-layout.tsx", "content": '"use client";\n\nimport React from "react";\nimport type { ProductLayoutProps } from "@cimplify/sdk/react";\nimport {\n TwoColumnGrid,\n RelatedProductsSection,\n CustomAttributesTable,\n TagPills,\n} from "@cimplify/sdk/react";\nimport { ProductImageGallery } from "@cimplify/sdk/react";\nimport { ProductCustomizer } from "@cimplify/sdk/react";\nimport { Price } from "@cimplify/sdk/react";\nimport { parsePrice } from "@cimplify/sdk";\nimport { cn } from "@cimplify/sdk/react";\nimport { formatDuration } from "../utils/format-duration";\n\nexport function ServiceProductLayout({\n product,\n onAddToCart,\n renderBreadcrumb,\n relatedProducts,\n showRelated = true,\n className,\n}: ProductLayoutProps): React.ReactElement {\n const images = product.images || (product.image_url ? [product.image_url] : []);\n const hasDeposit = product.deposit_type && product.deposit_type !== "none" && product.deposit_amount;\n const depositAmount = hasDeposit ? parsePrice(product.deposit_amount!) : 0;\n const cancellationHours = product.cancellation_window_minutes\n ? Math.floor(product.cancellation_window_minutes / 60)\n : null;\n\n return (\n <div data-cimplify-product-layout="service" className={cn("space-y-8", className)}>\n {renderBreadcrumb?.(product)}\n\n <TwoColumnGrid\n left={\n <ProductImageGallery\n images={images}\n productName={product.name}\n />\n }\n right={\n <div className="space-y-6">\n {/* Tags */}\n <TagPills tags={product.tags || []} />\n\n {/* Name + Price */}\n <div>\n <h1 data-cimplify-product-layout-name className="text-3xl lg:text-4xl font-extrabold tracking-tight">\n {product.name}\n </h1>\n <Price amount={product.default_price} className="text-2xl font-bold text-primary mt-2 block" />\n </div>\n\n {/* Service info pills */}\n <div data-cimplify-product-layout-service-info className="flex flex-wrap gap-2">\n {product.duration_minutes != null && (\n <span className="inline-flex items-center gap-1.5 px-3 py-1.5 bg-muted text-sm text-muted-foreground rounded-full">\n {formatDuration(product.duration_minutes, product.duration_unit)}\n </span>\n )}\n {hasDeposit && (\n <span className="inline-flex items-center gap-1.5 px-3 py-1.5 bg-muted text-sm text-muted-foreground rounded-full">\n <Price amount={depositAmount} /> deposit\n </span>\n )}\n {product.general_service_capacity != null && product.general_service_capacity > 1 && (\n <span className="inline-flex items-center gap-1.5 px-3 py-1.5 bg-muted text-sm text-muted-foreground rounded-full">\n Up to {product.general_service_capacity} people\n </span>\n )}\n {product.buffer_before_minutes != null && product.buffer_before_minutes > 0 && (\n <span className="inline-flex items-center gap-1.5 px-3 py-1.5 bg-muted text-sm text-muted-foreground rounded-full">\n {product.buffer_before_minutes} min buffer\n </span>\n )}\n </div>\n\n {/* Description */}\n {product.description && (\n <p data-cimplify-product-layout-description className="text-muted-foreground leading-relaxed">\n {product.description}\n </p>\n )}\n\n {/* Staff & Resource requirements */}\n {(product.requires_specific_staff || product.requires_specific_resource) && (\n <div data-cimplify-product-layout-requirements className="text-sm text-muted-foreground space-y-1">\n {product.requires_specific_staff && (\n <p>Staff selection required</p>\n )}\n {product.requires_specific_resource && (\n <p>Resource selection required</p>\n )}\n </div>\n )}\n\n {/* Custom Attributes */}\n <CustomAttributesTable attributes={product.custom_attributes || []} />\n\n {/* Customizer (variants, scheduling, add to cart) */}\n <ProductCustomizer\n product={product}\n onAddToCart={onAddToCart}\n showSpecialInstructions={false}\n />\n\n {/* Cancellation policy */}\n {(cancellationHours != null || product.no_show_fee) && (\n <div data-cimplify-product-layout-cancellation className="border border-border p-4 space-y-2 text-sm">\n <h3 className="font-semibold">Cancellation Policy</h3>\n {cancellationHours != null && (\n <p className="text-muted-foreground">\n Free cancellation up to <strong className="text-foreground">{cancellationHours} hours</strong> before the booking\n </p>\n )}\n {product.no_show_fee && (\n <p className="text-muted-foreground">\n No-show fee: <Price amount={product.no_show_fee} className="font-medium text-foreground" />\n </p>\n )}\n {hasDeposit && (\n <p className="text-muted-foreground">\n {product.deposit_type === "fixed"\n ? <><Price amount={depositAmount} className="font-medium text-foreground" /> deposit charged at booking</>\n : <>{parsePrice(product.deposit_amount!)}% deposit charged at booking</>\n }\n </p>\n )}\n </div>\n )}\n </div>\n }\n />\n\n {showRelated && relatedProducts && relatedProducts.length > 0 && (\n <RelatedProductsSection\n products={relatedProducts}\n title="Other services"\n />\n )}\n </div>\n );\n}\n' }] }, "product-grid": { "name": "product-grid", "title": "ProductGrid", "description": "Responsive CSS grid that renders ProductCards.", "type": "component", "registryDependencies": ["product-card", "cn"], "files": [{ "path": "product-grid.tsx", "content": '"use client";\n\nimport React from "react";\nimport type { Product } from "@cimplify/sdk";\nimport { ProductCard } from "@cimplify/sdk/react";\nimport { cn } from "@cimplify/sdk/react";\n\nexport interface ProductGridClassNames {\n root?: string;\n item?: string;\n empty?: string;\n}\n\nexport interface ProductGridProps {\n /** Products to display in the grid. */\n products: Product[];\n /** Responsive column counts at each breakpoint. */\n columns?: { sm?: number; md?: number; lg?: number; xl?: number };\n /** Custom card renderer per product. */\n renderCard?: (product: Product) => React.ReactNode;\n /** Custom image renderer passed to default ProductCards. */\n renderImage?: (props: {\n src: string;\n alt: string;\n className?: string;\n }) => React.ReactNode;\n /** Text shown when `products` is empty. */\n emptyMessage?: string;\n className?: string;\n classNames?: ProductGridClassNames;\n}\n\n/**\n * ProductGrid \u2014 responsive CSS grid that renders ProductCards.\n *\n * Injects an inline `<style>` tag with media queries for responsive columns.\n * Uses `React.useId()` for a hydration-safe, collision-free CSS selector.\n */\nexport function ProductGrid({\n products,\n columns,\n renderCard,\n renderImage,\n emptyMessage,\n className,\n classNames,\n}: ProductGridProps): React.ReactElement {\n const rawId = React.useId();\n // CSS selectors can\'t contain colons, so strip them from the React-generated ID\n const gridId = `cimplify-grid-${rawId.replace(/:/g, "")}`;\n\n const sm = columns?.sm ?? 1;\n const md = columns?.md ?? 2;\n const lg = columns?.lg ?? 3;\n const xl = columns?.xl ?? 4;\n const css = React.useMemo(\n () =>\n [\n `#${gridId}{display:grid;grid-template-columns:repeat(${sm},1fr);gap:1rem}`,\n `@media(min-width:768px){#${gridId}{grid-template-columns:repeat(${md},1fr)}}`,\n `@media(min-width:1024px){#${gridId}{grid-template-columns:repeat(${lg},1fr)}}`,\n `@media(min-width:1280px){#${gridId}{grid-template-columns:repeat(${xl},1fr)}}`,\n ].join(""),\n [gridId, sm, md, lg, xl],\n );\n\n if (products.length === 0) {\n return (\n <div\n data-cimplify-product-grid\n data-empty\n className={cn(className, classNames?.root, classNames?.empty)}\n >\n <p>{emptyMessage ?? "No products found"}</p>\n </div>\n );\n }\n\n return (\n <>\n <style dangerouslySetInnerHTML={{ __html: css }} />\n <div\n id={gridId}\n data-cimplify-product-grid\n className={cn(className, classNames?.root)}\n >\n {products.map((product) => (\n <div\n key={product.id}\n data-cimplify-product-grid-item\n className={classNames?.item}\n >\n {renderCard\n ? renderCard(product)\n : (\n <ProductCard\n product={product}\n renderImage={renderImage}\n />\n )}\n </div>\n ))}\n </div>\n </>\n );\n}\n' }] }, "composite-selector": { "name": "composite-selector", "title": "CompositeSelector", "description": "Composite product builder with group constraints and live pricing.", "type": "component", "registryDependencies": ["price"], "files": [{ "path": "composite-selector.tsx", "content": '"use client";\n\nimport React, { useState, useCallback, useMemo, useEffect } from "react";\nimport { Checkbox } from "@base-ui/react/checkbox";\nimport { NumberField } from "@base-ui/react/number-field";\nimport type {\n CompositeGroupView,\n CompositeComponentView,\n ComponentSelectionInput,\n CompositePriceResult,\n} from "@cimplify/sdk";\nimport { Price } from "@cimplify/sdk/react";\nimport { parsePrice } from "@cimplify/sdk";\nimport { cn } from "@cimplify/sdk/react";\nimport { useCimplifyClient } from "@cimplify/sdk/react";\n\nexport interface CompositeSelectorClassNames {\n root?: string;\n group?: string;\n groupHeader?: string;\n groupName?: string;\n required?: string;\n groupDescription?: string;\n groupConstraint?: string;\n validation?: string;\n components?: string;\n component?: string;\n componentSelected?: string;\n componentInfo?: string;\n componentName?: string;\n typeBadge?: string;\n serviceBadge?: string;\n digitalBadge?: string;\n badgePopular?: string;\n badgePremium?: string;\n componentDescription?: string;\n componentCalories?: string;\n qty?: string;\n qtyButton?: string;\n qtyValue?: string;\n summary?: string;\n summaryLine?: string;\n summaryTotal?: string;\n calculating?: string;\n priceError?: string;\n}\n\nexport interface CompositeSelectorProps {\n compositeId: string;\n groups: CompositeGroupView[];\n onSelectionsChange: (selections: ComponentSelectionInput[]) => void;\n onPriceChange?: (price: CompositePriceResult | null) => void;\n onReady?: (ready: boolean) => void;\n skipPriceFetch?: boolean;\n className?: string;\n classNames?: CompositeSelectorClassNames;\n}\n\nexport function CompositeSelector({\n compositeId,\n groups,\n onSelectionsChange,\n onPriceChange,\n onReady,\n skipPriceFetch,\n className,\n classNames,\n}: CompositeSelectorProps): React.ReactElement | null {\n const { client } = useCimplifyClient();\n\n const [groupSelections, setGroupSelections] = useState<\n Record<string, Record<string, number>>\n >({});\n const [priceResult, setPriceResult] = useState<CompositePriceResult | null>(null);\n const [isPriceLoading, setIsPriceLoading] = useState(false);\n const [priceError, setPriceError] = useState(false);\n\n const selections = useMemo((): ComponentSelectionInput[] => {\n const result: ComponentSelectionInput[] = [];\n for (const groupSels of Object.values(groupSelections)) {\n for (const [componentId, qty] of Object.entries(groupSels)) {\n if (qty > 0) {\n result.push({ component_id: componentId, quantity: qty });\n }\n }\n }\n return result;\n }, [groupSelections]);\n\n useEffect(() => {\n onSelectionsChange(selections);\n }, [selections, onSelectionsChange]);\n\n useEffect(() => {\n onPriceChange?.(priceResult);\n }, [priceResult, onPriceChange]);\n\n const allGroupsSatisfied = useMemo(() => {\n for (const group of groups) {\n const groupSels = groupSelections[group.id] || {};\n const totalSelected = Object.values(groupSels).reduce((sum, q) => sum + q, 0);\n if (totalSelected < group.min_selections) return false;\n }\n return true;\n }, [groups, groupSelections]);\n\n const sortedGroups = useMemo(\n () =>\n [...groups]\n .sort((a, b) => a.display_order - b.display_order)\n .map((group) => ({\n ...group,\n _sortedComponents: group.components\n .filter((component) => component.is_available && !component.is_archived)\n .sort((a, b) => a.display_order - b.display_order),\n })),\n [groups],\n );\n\n useEffect(() => {\n onReady?.(allGroupsSatisfied);\n }, [allGroupsSatisfied, onReady]);\n\n useEffect(() => {\n if (skipPriceFetch || !allGroupsSatisfied || selections.length === 0) return;\n\n let cancelled = false;\n const timer = setTimeout(() => {\n void (async () => {\n setIsPriceLoading(true);\n setPriceError(false);\n try {\n const result = await client.catalogue.calculateCompositePrice(compositeId, selections);\n if (cancelled) return;\n if (result.ok) {\n setPriceResult(result.value);\n } else {\n setPriceError(true);\n }\n } catch {\n if (!cancelled) setPriceError(true);\n } finally {\n if (!cancelled) setIsPriceLoading(false);\n }\n })();\n }, 300);\n\n return () => {\n cancelled = true;\n clearTimeout(timer);\n };\n }, [selections, allGroupsSatisfied, compositeId, client, skipPriceFetch]);\n\n const toggleComponent = useCallback(\n (group: CompositeGroupView, component: CompositeComponentView) => {\n setGroupSelections((prev) => {\n const groupSels = { ...(prev[group.id] || {}) };\n const currentQty = groupSels[component.id] || 0;\n\n if (currentQty > 0) {\n if (group.min_selections > 0) {\n const totalOthers = Object.entries(groupSels)\n .filter(([id]) => id !== component.id)\n .reduce((sum, [, q]) => sum + q, 0);\n if (totalOthers < group.min_selections) {\n return prev;\n }\n }\n delete groupSels[component.id];\n } else {\n const totalSelected = Object.values(groupSels).reduce((sum, q) => sum + q, 0);\n if (group.max_selections && totalSelected >= group.max_selections) {\n if (group.max_selections === 1) {\n return { ...prev, [group.id]: { [component.id]: 1 } };\n }\n return prev;\n }\n groupSels[component.id] = 1;\n }\n\n return { ...prev, [group.id]: groupSels };\n });\n },\n [],\n );\n\n const updateQuantity = useCallback(\n (group: CompositeGroupView, componentId: string, newValue: number) => {\n setGroupSelections((prev) => {\n const groupSels = { ...(prev[group.id] || {}) };\n const current = groupSels[componentId] || 0;\n const next = Math.max(0, newValue);\n\n if (next === current) return prev;\n\n const delta = next - current;\n\n if (group.max_quantity_per_component && next > group.max_quantity_per_component) {\n return prev;\n }\n\n const totalAfter = Object.entries(groupSels)\n .reduce((sum, [id, q]) => sum + (id === componentId ? next : q), 0);\n\n if (delta > 0 && group.max_selections && totalAfter > group.max_selections) {\n return prev;\n }\n\n if (delta < 0 && totalAfter < group.min_selections) {\n return prev;\n }\n\n if (next === 0) {\n delete groupSels[componentId];\n } else {\n groupSels[componentId] = next;\n }\n\n return { ...prev, [group.id]: groupSels };\n });\n },\n [],\n );\n\n if (groups.length === 0) {\n return null;\n }\n\n return (\n <div data-cimplify-composite-selector className={cn("space-y-6", className, classNames?.root)}>\n {sortedGroups.map((group) => {\n const groupSels = groupSelections[group.id] || {};\n const totalSelected = Object.values(groupSels).reduce((sum, q) => sum + q, 0);\n const minMet = totalSelected >= group.min_selections;\n const isSingleSelect = group.max_selections === 1;\n\n return (\n <div\n key={group.id}\n data-cimplify-composite-group\n className={cn(classNames?.group)}\n >\n <div\n data-cimplify-composite-group-header\n className={cn("flex items-center justify-between py-3", classNames?.groupHeader)}\n >\n <div>\n <span\n data-cimplify-composite-group-name\n className={cn("text-base font-bold", classNames?.groupName)}\n >\n {group.name}\n </span>\n {group.description && (\n <span\n data-cimplify-composite-group-description\n className={cn("block text-xs text-muted-foreground mt-0.5", classNames?.groupDescription)}\n >\n {group.description}\n </span>\n )}\n <span\n data-cimplify-composite-group-constraint\n className={cn("block text-xs text-muted-foreground mt-0.5", classNames?.groupConstraint)}\n >\n {group.min_selections > 0 && group.max_selections\n ? `Choose ${group.min_selections}\\u2013${group.max_selections}`\n : group.min_selections > 0\n ? `Choose at least ${group.min_selections}`\n : group.max_selections\n ? `Choose up to ${group.max_selections}`\n : "Choose as many as you like"}\n </span>\n </div>\n {group.min_selections > 0 && (\n <span\n data-cimplify-composite-required\n className={cn(\n "text-xs font-semibold px-2.5 py-1 rounded shrink-0",\n !minMet\n ? "text-destructive bg-destructive/10"\n : "text-destructive bg-destructive/10",\n classNames?.required,\n )}\n >\n Required\n </span>\n )}\n </div>\n\n <div\n data-cimplify-composite-components\n role={isSingleSelect ? "radiogroup" : "group"}\n aria-label={group.name}\n className={cn("divide-y divide-border", classNames?.components)}\n >\n {group._sortedComponents.map((component) => {\n const qty = groupSels[component.id] || 0;\n const isSelected = qty > 0;\n const displayName = component.display_name || component.id;\n\n return (\n <Checkbox.Root\n key={component.id}\n checked={isSelected}\n onCheckedChange={() => toggleComponent(group, component)}\n value={component.id}\n data-cimplify-composite-component\n data-selected={isSelected || undefined}\n className={cn(\n "w-full flex items-center gap-3 py-4 transition-colors text-left cursor-pointer",\n isSelected ? classNames?.componentSelected : classNames?.component,\n )}\n >\n <Checkbox.Indicator\n className="hidden"\n keepMounted={false}\n />\n\n {/* Visual indicator: radio circle for single-select, checkbox square for multi-select */}\n {isSingleSelect ? (\n <span\n data-cimplify-composite-radio\n className={cn(\n "w-5 h-5 rounded-full border-2 flex items-center justify-center shrink-0 transition-colors",\n isSelected ? "border-primary" : "border-muted-foreground/30",\n )}\n >\n {isSelected && <span className="w-2.5 h-2.5 rounded-full bg-primary" />}\n </span>\n ) : (\n <span\n data-cimplify-composite-checkbox\n className={cn(\n "w-5 h-5 rounded-sm border-2 flex items-center justify-center shrink-0 transition-colors",\n isSelected ? "border-primary bg-primary" : "border-muted-foreground/30",\n )}\n >\n {isSelected && (\n <svg viewBox="0 0 12 12" className="w-3 h-3 text-primary-foreground" fill="none">\n <path d="M2 6l3 3 5-5" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>\n </svg>\n )}\n </span>\n )}\n\n <div\n data-cimplify-composite-component-info\n className={cn("flex-1 min-w-0", classNames?.componentInfo)}\n >\n <span\n data-cimplify-composite-component-name\n className={cn("text-sm", classNames?.componentName)}\n >\n {displayName}\n </span>\n {component.product_type === "service" && (\n <span\n data-cimplify-composite-type-badge="service"\n className={cn(\n "text-[10px] uppercase tracking-wider font-medium text-blue-600",\n classNames?.typeBadge,\n classNames?.serviceBadge,\n )}\n >\n <svg viewBox="0 0 16 16" fill="none" className="inline-block w-3 h-3 mr-0.5 -mt-px" aria-hidden="true">\n <circle cx="8" cy="8" r="7" stroke="currentColor" strokeWidth="1.5"/>\n <path d="M8 4v4l2.5 1.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>\n </svg>\n Service\n </span>\n )}\n {component.product_type === "digital" && (\n <span\n data-cimplify-composite-type-badge="digital"\n className={cn(\n "text-[10px] uppercase tracking-wider font-medium text-violet-600",\n classNames?.typeBadge,\n classNames?.digitalBadge,\n )}\n >\n <svg viewBox="0 0 16 16" fill="none" className="inline-block w-3 h-3 mr-0.5 -mt-px" aria-hidden="true">\n <path d="M9 2L4 9h4l-1 5 5-7H8l1-5z" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>\n </svg>\n Digital\n </span>\n )}\n {component.is_popular && (\n <span\n data-cimplify-composite-badge="popular"\n className={cn("text-[10px] uppercase tracking-wider text-primary font-medium", classNames?.badgePopular)}\n >\n Popular\n </span>\n )}\n {component.is_premium && (\n <span\n data-cimplify-composite-badge="premium"\n className={cn("text-[10px] uppercase tracking-wider text-amber-600 font-medium", classNames?.badgePremium)}\n >\n Premium\n </span>\n )}\n {component.display_description && (\n <span\n data-cimplify-composite-component-description\n className={cn("block text-xs text-muted-foreground truncate", classNames?.componentDescription)}\n >\n {component.display_description}\n </span>\n )}\n {component.calories != null && (\n <span\n data-cimplify-composite-component-calories\n className={cn("block text-xs text-muted-foreground/60", classNames?.componentCalories)}\n >\n {component.calories} cal\n </span>\n )}\n </div>\n\n {group.allow_quantity && isSelected && (\n <NumberField.Root\n value={qty}\n onValueChange={(val) => {\n if (val != null) {\n updateQuantity(group, component.id, val);\n }\n }}\n min={0}\n max={group.max_quantity_per_component || undefined}\n step={1}\n >\n <NumberField.Group\n data-cimplify-composite-qty\n onClick={(e: React.MouseEvent) => e.stopPropagation()}\n className={cn("flex items-center gap-2", classNames?.qty)}\n >\n <NumberField.Decrement\n aria-label={`Decrease ${displayName} quantity`}\n className={cn("w-6 h-6 border border-border flex items-center justify-center text-xs hover:bg-muted disabled:opacity-30", classNames?.qtyButton)}\n >\n &#x2212;\n </NumberField.Decrement>\n <NumberField.Input\n readOnly\n className={cn("w-4 text-center text-sm font-medium bg-transparent border-none outline-none [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none", classNames?.qtyValue)}\n />\n <NumberField.Increment\n aria-label={`Increase ${displayName} quantity`}\n className={cn("w-6 h-6 border border-border flex items-center justify-center text-xs hover:bg-muted disabled:opacity-30", classNames?.qtyButton)}\n >\n +\n </NumberField.Increment>\n </NumberField.Group>\n </NumberField.Root>\n )}\n\n {component.price != null && (\n <span className="text-sm text-muted-foreground shrink-0">\n +<Price amount={component.price} />\n </span>\n )}\n </Checkbox.Root>\n );\n })}\n </div>\n </div>\n );\n })}\n\n {priceResult && (\n <div\n data-cimplify-composite-summary\n className={cn("border-t border-border pt-4 space-y-1 text-sm", classNames?.summary)}\n >\n {parsePrice(priceResult.base_price) !== 0 && (\n <div\n data-cimplify-composite-summary-line\n className={cn("flex justify-between text-muted-foreground", classNames?.summaryLine)}\n >\n <span>Base</span>\n <Price amount={priceResult.base_price} />\n </div>\n )}\n {parsePrice(priceResult.components_total) !== 0 && (\n <div\n data-cimplify-composite-summary-line\n className={cn("flex justify-between text-muted-foreground", classNames?.summaryLine)}\n >\n <span>Selections</span>\n <Price amount={priceResult.components_total} />\n </div>\n )}\n <div\n data-cimplify-composite-summary-total\n className={cn("flex justify-between font-medium pt-1 border-t border-border", classNames?.summaryTotal)}\n >\n <span>Total</span>\n <Price amount={priceResult.final_price} className="text-primary" />\n </div>\n </div>\n )}\n\n {isPriceLoading && (\n <div\n data-cimplify-composite-calculating\n className={cn("flex items-center gap-2 text-sm text-muted-foreground", classNames?.calculating)}\n >\n Calculating price...\n </div>\n )}\n\n {priceError && !isPriceLoading && (\n <div\n data-cimplify-composite-price-error\n className={cn("text-sm text-destructive", classNames?.priceError)}\n >\n Unable to calculate price\n </div>\n )}\n </div>\n );\n}\n' }] }, "location-picker": { "name": "location-picker", "title": "LocationPicker", "description": "Branch / pickup-point selector for businesses with multiple locations.", "type": "component", "registryDependencies": ["cn"], "files": [{ "path": "location-picker.tsx", "content": '"use client";\n\nimport React from "react";\nimport type { Location } from "../types/business";\nimport { useLocations } from "@cimplify/sdk/react";\nimport { cn } from "@cimplify/sdk/react";\n\nexport interface LocationPickerClassNames {\n root?: string;\n item?: string;\n activeItem?: string;\n empty?: string;\n loading?: string;\n}\n\nexport interface LocationPickerProps {\n /** Override locations (skips useLocations fetch). For SSR, pass pre-fetched locations. */\n locations?: Location[];\n /** Override the current location. */\n currentLocation?: Location | null;\n /** Called when a location is selected. */\n onLocationChange?: (location: Location) => void;\n /** Custom location renderer. */\n renderLocation?: (location: Location, isActive: boolean) => React.ReactNode;\n /** Text shown when no locations are available. */\n emptyMessage?: string;\n className?: string;\n classNames?: LocationPickerClassNames;\n}\n\n/**\n * LocationPicker \u2014 renders a location selector for multi-location businesses.\n *\n * Uses `useLocations` to fetch locations unless overridden via props.\n * Returns `null` when there is only one location (no picker needed).\n */\nexport function LocationPicker({\n locations: locationsProp,\n currentLocation: currentLocationProp,\n onLocationChange,\n renderLocation,\n emptyMessage,\n className,\n classNames,\n}: LocationPickerProps): React.ReactElement | null {\n const {\n locations: fetched,\n currentLocation: fetchedCurrent,\n setCurrentLocation,\n isLoading,\n } = useLocations();\n\n const locations = locationsProp ?? fetched;\n const currentLocation = currentLocationProp ?? fetchedCurrent;\n\n const handleSelect = (location: Location): void => {\n if (onLocationChange) {\n onLocationChange(location);\n } else {\n setCurrentLocation(location);\n }\n };\n\n if (isLoading && locations.length === 0) {\n return (\n <div\n data-cimplify-location-picker\n aria-busy="true"\n className={cn(className, classNames?.root, classNames?.loading)}\n />\n );\n }\n\n // No picker needed for single location\n if (locations.length <= 1) {\n return null;\n }\n\n if (locations.length === 0) {\n return (\n <div\n data-cimplify-location-picker\n data-empty\n className={cn(className, classNames?.root, classNames?.empty)}\n >\n <p>{emptyMessage ?? "No locations available"}</p>\n </div>\n );\n }\n\n return (\n <div data-cimplify-location-picker className={cn(className, classNames?.root)}>\n {locations.map((location) => {\n const isActive = currentLocation?.id === location.id;\n return (\n <button\n key={location.id}\n type="button"\n onClick={() => handleSelect(location)}\n data-cimplify-location-item\n data-active={isActive || undefined}\n aria-pressed={isActive}\n className={cn(classNames?.item, isActive && classNames?.activeItem)}\n >\n {renderLocation ? (\n renderLocation(location, isActive)\n ) : (\n <span>{location.name}</span>\n )}\n </button>\n );\n })}\n </div>\n );\n}\n' }] }, "recommendation-carousel": { "name": "recommendation-carousel", "title": "RecommendationCarousel", "description": "Personalized product carousel powered by /activity/recommendations \u2014 frequently bought, related, trending.", "type": "component", "registryDependencies": ["product-card", "cn"], "files": [{ "path": "recommendation-carousel.tsx", "content": '"use client";\n\nimport React from "react";\nimport type { ActivityRecommendation } from "../activity";\nimport { useRecommendations } from "./hooks/use-recommendations";\nimport { cn } from "@cimplify/sdk/react";\n\nexport interface RecommendationCarouselClassNames {\n root?: string;\n item?: string;\n reason?: string;\n empty?: string;\n loading?: string;\n}\n\nexport interface RecommendationCarouselProps {\n /** Override recommendations (skips fetch). For SSR, pass pre-fetched data. */\n recommendations?: ActivityRecommendation[];\n /** Location ID for location-specific recommendations. */\n locationId?: string;\n /** Maximum recommendations to fetch. */\n limit?: number;\n /** Called when a product recommendation is clicked. */\n onProductClick?: (recommendation: ActivityRecommendation) => void;\n /** Custom product card renderer. */\n renderProduct?: (recommendation: ActivityRecommendation) => React.ReactNode;\n /** Text shown when no recommendations are available. */\n emptyMessage?: string;\n className?: string;\n classNames?: RecommendationCarouselClassNames;\n}\n\n/**\n * RecommendationCarousel \u2014 displays personalized product recommendations.\n *\n * Renders a horizontal scrollable list of product cards with reason labels.\n * Returns `null` when there are no recommendations.\n */\nexport function RecommendationCarousel({\n recommendations: recommendationsProp,\n locationId,\n limit,\n onProductClick,\n renderProduct,\n emptyMessage,\n className,\n classNames,\n}: RecommendationCarouselProps): React.ReactElement | null {\n const { recommendations: fetched, isLoading } = useRecommendations({\n locationId,\n limit,\n enabled: recommendationsProp === undefined,\n });\n\n const recommendations = recommendationsProp ?? fetched;\n\n if (isLoading && recommendations.length === 0) {\n return (\n <div\n data-cimplify-recommendation-carousel\n aria-busy="true"\n className={cn(className, classNames?.root, classNames?.loading)}\n />\n );\n }\n\n if (recommendations.length === 0) {\n return null;\n }\n\n return (\n <div\n data-cimplify-recommendation-carousel\n className={cn(className, classNames?.root)}\n style={{ display: "flex", overflowX: "auto", gap: "1rem" }}\n >\n {recommendations.map((rec, index) => {\n const productId = rec.product?.id ?? String(index);\n return (\n <button\n key={String(productId)}\n type="button"\n onClick={() => onProductClick?.(rec)}\n data-cimplify-recommendation-item\n className={classNames?.item}\n style={{ flexShrink: 0 }}\n >\n {renderProduct ? (\n renderProduct(rec)\n ) : (\n <>\n <span data-cimplify-recommendation-name>\n {rec.product?.name ?? rec.product?.id ?? "Product"}\n </span>\n {rec.reason && (\n <span data-cimplify-recommendation-reason className={classNames?.reason}>\n {rec.reason}\n </span>\n )}\n </>\n )}\n </button>\n );\n })}\n </div>\n );\n}\n' }] }, "discount-input": { "name": "discount-input", "title": "DiscountInput", "description": "Discount code input with inline validation.", "type": "component", "registryDependencies": ["price", "cn"], "files": [{ "path": "discount-input.tsx", "content": '"use client";\n\nimport React, { useState, useCallback } from "react";\nimport { Field } from "@base-ui/react/field";\nimport { Input } from "@base-ui/react/input";\nimport type { DiscountValidation } from "@cimplify/sdk";\nimport { useValidateDiscount } from "@cimplify/sdk/react";\nimport { Price } from "@cimplify/sdk/react";\nimport { cn } from "@cimplify/sdk/react";\n\nexport interface DiscountInputClassNames {\n root?: string;\n input?: string;\n button?: string;\n result?: string;\n error?: string;\n success?: string;\n}\n\nexport interface DiscountInputProps {\n /** Current order subtotal for validation. */\n orderSubtotal: string;\n /** Location ID for location-specific discounts. */\n locationId?: string;\n /** Called when a valid discount is applied. */\n onApply?: (validation: DiscountValidation) => void;\n /** Called when discount is cleared. */\n onClear?: () => void;\n /** Placeholder text. */\n placeholder?: string;\n className?: string;\n classNames?: DiscountInputClassNames;\n}\n\n/**\n * DiscountInput \u2014 discount code input with inline validation.\n *\n * Wraps `useValidateDiscount` with a Base UI Field + Input and apply button.\n * Shows validation result inline (success with amount, or error via Field.Error).\n */\nexport function DiscountInput({\n orderSubtotal,\n locationId,\n onApply,\n onClear,\n placeholder = "Discount code",\n className,\n classNames,\n}: DiscountInputProps): React.ReactElement {\n const [code, setCode] = useState("");\n const [appliedValidation, setAppliedValidation] =\n useState<DiscountValidation | null>(null);\n const { validate, isValidating, error } = useValidateDiscount();\n\n const handleApply = useCallback(async () => {\n const trimmed = code.trim();\n if (!trimmed) return;\n\n const result = await validate(trimmed, orderSubtotal, locationId);\n if (result) {\n if (result.is_eligible) {\n setAppliedValidation(result);\n onApply?.(result);\n } else {\n setAppliedValidation(result);\n }\n }\n }, [code, validate, orderSubtotal, locationId, onApply]);\n\n const handleClear = useCallback(() => {\n setCode("");\n setAppliedValidation(null);\n onClear?.();\n }, [onClear]);\n\n const handleKeyDown = useCallback(\n (e: React.KeyboardEvent) => {\n if (e.key === "Enter") {\n void handleApply();\n }\n },\n [handleApply],\n );\n\n const isApplied = appliedValidation?.is_eligible === true;\n const hasError =\n !!error || (!!appliedValidation && !appliedValidation.is_eligible);\n\n const errorMessage = error\n ? error.message\n : appliedValidation && !appliedValidation.is_eligible\n ? (appliedValidation.ineligibility_reason ?? "This code is not valid.")\n : undefined;\n\n return (\n <Field.Root\n data-cimplify-discount\n invalid={hasError}\n disabled={isApplied}\n className={cn(className, classNames?.root)}\n >\n <div data-cimplify-discount-form>\n <Input\n type="text"\n value={code}\n onValueChange={(value) => setCode(value)}\n onKeyDown={handleKeyDown}\n placeholder={placeholder}\n data-cimplify-discount-input\n className={classNames?.input}\n aria-label="Discount code"\n />\n {isApplied ? (\n <button\n type="button"\n onClick={handleClear}\n data-cimplify-discount-clear\n className={classNames?.button}\n >\n Remove\n </button>\n ) : (\n <button\n type="button"\n onClick={handleApply}\n disabled={isValidating || code.trim().length === 0}\n data-cimplify-discount-apply\n className={classNames?.button}\n >\n {isValidating ? "Checking..." : "Apply"}\n </button>\n )}\n </div>\n\n {hasError && errorMessage && (\n <Field.Error\n match={true}\n data-cimplify-discount-error\n className={classNames?.error}\n >\n {errorMessage}\n </Field.Error>\n )}\n\n {isApplied && appliedValidation.discount_amount && (\n <Field.Description\n data-cimplify-discount-success\n className={classNames?.success}\n >\n <span>Discount applied</span>\n <Price amount={appliedValidation.discount_amount} prefix="-" />\n </Field.Description>\n )}\n </Field.Root>\n );\n}\n' }] }, "standard-service-card": { "name": "standard-service-card", "title": "StandardServiceCard", "description": "Service card with hero image, duration, deposit, and availability.", "type": "component", "registryDependencies": ["price", "cn"], "files": [{ "path": "cards/standard-service-card.tsx", "content": '"use client";\n\nimport React from "react";\nimport type { Product } from "@cimplify/sdk";\nimport type { AvailableSlot } from "@cimplify/sdk";\nimport type { CardLayoutProps } from "@cimplify/sdk/react";\nimport { CardShell, CardImage } from "@cimplify/sdk/react";\nimport { Price } from "@cimplify/sdk/react";\nimport { cn } from "@cimplify/sdk/react";\n\nexport interface ServiceCardLayoutProps extends CardLayoutProps {\n slots?: AvailableSlot[];\n onBook?: (product: Product) => void;\n}\n\nfunction formatDuration(minutes: number, unit?: string): string {\n if (unit && unit !== "minutes") return `${minutes} ${unit}`;\n if (minutes >= 60) {\n const h = Math.floor(minutes / 60);\n const m = minutes % 60;\n return m > 0 ? `${h}h ${m}m` : `${h} hr${h > 1 ? "s" : ""}`;\n }\n return `${minutes} min`;\n}\n\nexport function StandardServiceCard({\n product,\n onBook,\n renderImage,\n renderLink,\n className,\n}: ServiceCardLayoutProps): React.ReactElement {\n const image = product.image_url || product.images?.[0] || "";\n const hasDeposit = product.deposit_type && product.deposit_type !== "none" && product.deposit_amount;\n\n return (\n <CardShell product={product} renderLink={renderLink} className={className}>\n <CardImage src={image} alt={product.name} aspectRatio="16/9" renderImage={renderImage}>\n <div className="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent pointer-events-none" />\n\n {/* Duration pill */}\n {product.duration_minutes != null && (\n <span className="absolute bottom-3 left-3 inline-flex items-center gap-1 text-[12px] font-semibold px-2.5 py-1 rounded-lg bg-background/92 backdrop-blur-xl text-foreground shadow-sm">\n {formatDuration(product.duration_minutes, product.duration_unit)}\n </span>\n )}\n\n {/* Price on image */}\n <span className="absolute bottom-3 right-3 text-white font-bold text-lg drop-shadow-sm">\n <Price amount={product.default_price} />\n </span>\n </CardImage>\n\n <div className="p-4">\n <h3 className="text-[14.5px] font-bold text-foreground leading-snug tracking-tight">\n {product.name}\n </h3>\n {product.description && (\n <p className="text-[12.5px] text-muted-foreground mt-1 leading-relaxed line-clamp-2">\n {product.description}\n </p>\n )}\n\n <div className="flex items-center justify-between mt-3 pt-3 border-t border-border">\n <div className="flex items-center gap-2 text-[12px] text-muted-foreground">\n <span className="w-[7px] h-[7px] rounded-full bg-emerald-500 animate-pulse" />\n <span>Available</span>\n {hasDeposit && (\n <>\n <span className="text-border">\xB7</span>\n <span><Price amount={product.deposit_amount!} /> deposit</span>\n </>\n )}\n </div>\n <button\n onClick={(e) => { e.preventDefault(); e.stopPropagation(); onBook?.(product); }}\n className="text-[13px] font-semibold text-primary hover:text-primary/80 transition-colors"\n >\n Book now &rarr;\n </button>\n </div>\n </div>\n </CardShell>\n );\n}\n' }] }, "quantity-selector": { "name": "quantity-selector", "title": "QuantitySelector", "description": "Controlled increment/decrement quantity input.", "type": "component", "registryDependencies": [], "files": [{ "path": "quantity-selector.tsx", "content": '"use client";\n\nimport React from "react";\nimport { NumberField } from "@base-ui/react/number-field";\nimport { cn } from "@cimplify/sdk/react";\n\nexport interface QuantitySelectorClassNames {\n root?: string;\n button?: string;\n value?: string;\n}\n\nexport interface QuantitySelectorProps {\n value: number;\n onChange: (value: number) => void;\n min?: number;\n max?: number;\n className?: string;\n classNames?: QuantitySelectorClassNames;\n}\n\nexport function QuantitySelector({\n value,\n onChange,\n min = 1,\n max,\n className,\n classNames,\n}: QuantitySelectorProps): React.ReactElement {\n return (\n <NumberField.Root\n value={value}\n onValueChange={(val) => {\n if (val != null) {\n onChange(val);\n }\n }}\n min={min}\n max={max}\n step={1}\n >\n <NumberField.Group\n data-cimplify-quantity\n className={cn("inline-flex items-center gap-3 border border-border px-2", className, classNames?.root)}\n >\n <NumberField.Decrement\n aria-label="Decrease quantity"\n data-cimplify-quantity-decrement\n className={cn(\n "w-10 h-10 flex items-center justify-center hover:text-primary transition-colors disabled:opacity-30",\n classNames?.button,\n )}\n >\n &#x2212;\n </NumberField.Decrement>\n <NumberField.Input\n data-cimplify-quantity-value\n aria-live="polite"\n readOnly\n className={cn("w-8 text-center font-medium bg-transparent border-none outline-none [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none", classNames?.value)}\n />\n <NumberField.Increment\n aria-label="Increase quantity"\n data-cimplify-quantity-increment\n className={cn(\n "w-10 h-10 flex items-center justify-center hover:text-primary transition-colors disabled:opacity-30",\n classNames?.button,\n )}\n >\n +\n </NumberField.Increment>\n </NumberField.Group>\n </NumberField.Root>\n );\n}\n' }] }, "digital-product-layout": { "name": "digital-product-layout", "title": "DigitalProductLayout", "description": "Digital product layout with file details, download limits, event info, and access passes.", "type": "component", "registryDependencies": ["product-image-gallery", "product-customizer", "price", "cn"], "files": [{ "path": "layouts/digital-product-layout.tsx", "content": '"use client";\n\nimport React from "react";\nimport type { ProductLayoutProps } from "@cimplify/sdk/react";\nimport {\n TwoColumnGrid,\n RelatedProductsSection,\n CustomAttributesTable,\n TagPills,\n} from "@cimplify/sdk/react";\nimport { ProductImageGallery } from "@cimplify/sdk/react";\nimport { ProductCustomizer } from "@cimplify/sdk/react";\nimport { Price } from "@cimplify/sdk/react";\nimport { cn } from "@cimplify/sdk/react";\n\nconst DIGITAL_TYPE_LABELS: Record<string, string> = {\n download: "Digital download",\n license: "Software license",\n event_ticket: "Event ticket",\n access_pass: "Access pass",\n gift_card: "Gift card",\n};\n\nexport function DigitalProductLayout({\n product,\n onAddToCart,\n renderBreadcrumb,\n relatedProducts,\n showRelated = true,\n className,\n}: ProductLayoutProps): React.ReactElement {\n const images = product.images || (product.image_url ? [product.image_url] : []);\n const typeLabel = product.digital_type\n ? DIGITAL_TYPE_LABELS[product.digital_type] || product.digital_type\n : "Digital product";\n\n const isTicket = product.digital_type === "event_ticket";\n const isAccessPass = product.digital_type === "access_pass";\n const isDownload = product.digital_type === "download" || product.digital_type === "license";\n\n return (\n <div data-cimplify-product-layout="digital" className={cn("space-y-8", className)}>\n {renderBreadcrumb?.(product)}\n\n <TwoColumnGrid\n left={\n <ProductImageGallery\n images={images}\n productName={product.name}\n />\n }\n right={\n <div className="space-y-6">\n {/* Type badge + tags */}\n <div className="flex flex-wrap items-center gap-2">\n <span data-cimplify-product-layout-badge className="text-xs font-semibold uppercase tracking-wider text-primary bg-primary/10 px-2.5 py-1 rounded-full">\n {typeLabel}\n </span>\n <TagPills tags={product.tags || []} />\n </div>\n\n {/* Name + Price */}\n <div>\n <h1 data-cimplify-product-layout-name className="text-3xl lg:text-4xl font-extrabold tracking-tight">\n {product.name}\n </h1>\n <Price amount={product.default_price} className="text-2xl font-bold mt-2 block" />\n </div>\n\n {/* Description */}\n {product.description && (\n <p data-cimplify-product-layout-description className="text-muted-foreground leading-relaxed">\n {product.description}\n </p>\n )}\n\n {/* Delivery notice */}\n {isDownload && (\n <div data-cimplify-product-layout-delivery className="flex items-center gap-3 px-4 py-3 bg-muted rounded-lg">\n <svg className="w-5 h-5 text-primary shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">\n <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />\n </svg>\n <div>\n <p className="text-sm font-medium">Instant delivery</p>\n <p className="text-xs text-muted-foreground">Download immediately after purchase</p>\n </div>\n </div>\n )}\n\n {/* Event info */}\n {isTicket && (product.event_date || product.venue) && (\n <div data-cimplify-product-layout-event className="border border-border p-4 space-y-2">\n {product.event_date && (\n <div className="flex items-center gap-2 text-sm">\n <span className="text-muted-foreground">Date:</span>\n <span className="font-medium">{new Date(product.event_date).toLocaleDateString(undefined, { weekday: "long", year: "numeric", month: "long", day: "numeric" })}</span>\n </div>\n )}\n {product.venue && (\n <div className="flex items-center gap-2 text-sm">\n <span className="text-muted-foreground">Venue:</span>\n <span className="font-medium">{product.venue}</span>\n </div>\n )}\n {product.ticket_type && (\n <div className="flex items-center gap-2 text-sm">\n <span className="text-muted-foreground">Type:</span>\n <span className="font-medium capitalize">{product.ticket_type}</span>\n </div>\n )}\n </div>\n )}\n\n {/* Access pass info */}\n {isAccessPass && (product.access_type || product.access_duration_days) && (\n <div data-cimplify-product-layout-access className="border border-border p-4 space-y-2">\n {product.access_type && (\n <div className="flex items-center gap-2 text-sm">\n <span className="text-muted-foreground">Access:</span>\n <span className="font-medium capitalize">{product.access_type}</span>\n </div>\n )}\n {product.access_level && (\n <div className="flex items-center gap-2 text-sm">\n <span className="text-muted-foreground">Level:</span>\n <span className="font-medium capitalize">{product.access_level}</span>\n </div>\n )}\n {product.access_duration_days && (\n <div className="flex items-center gap-2 text-sm">\n <span className="text-muted-foreground">Duration:</span>\n <span className="font-medium">{product.access_duration_days} days</span>\n </div>\n )}\n </div>\n )}\n\n {/* File details */}\n {isDownload && (product.file_size_mb || product.file_type || product.version || product.max_downloads) && (\n <div data-cimplify-product-layout-file-details className="grid grid-cols-2 gap-3">\n {product.file_type && (\n <div className="px-3 py-2 bg-muted text-center">\n <p className="text-xs text-muted-foreground">Format</p>\n <p className="text-sm font-semibold">{product.file_type.toUpperCase()}</p>\n </div>\n )}\n {product.file_size_mb != null && (\n <div className="px-3 py-2 bg-muted text-center">\n <p className="text-xs text-muted-foreground">Size</p>\n <p className="text-sm font-semibold">{product.file_size_mb >= 1024 ? `${(product.file_size_mb / 1024).toFixed(1)} GB` : `${product.file_size_mb} MB`}</p>\n </div>\n )}\n {product.max_downloads != null && (\n <div className="px-3 py-2 bg-muted text-center">\n <p className="text-xs text-muted-foreground">Downloads</p>\n <p className="text-sm font-semibold">{product.max_downloads}</p>\n </div>\n )}\n {product.version && (\n <div className="px-3 py-2 bg-muted text-center">\n <p className="text-xs text-muted-foreground">Version</p>\n <p className="text-sm font-semibold">v{product.version}</p>\n </div>\n )}\n {product.download_expires_days != null && (\n <div className="px-3 py-2 bg-muted text-center">\n <p className="text-xs text-muted-foreground">Expires</p>\n <p className="text-sm font-semibold">{product.download_expires_days} days</p>\n </div>\n )}\n {product.max_activations != null && (\n <div className="px-3 py-2 bg-muted text-center">\n <p className="text-xs text-muted-foreground">Activations</p>\n <p className="text-sm font-semibold">{product.max_activations}</p>\n </div>\n )}\n </div>\n )}\n\n {/* Custom Attributes */}\n <CustomAttributesTable attributes={product.custom_attributes || []} />\n\n {/* Customizer (license variants, billing plans, add to cart) */}\n <ProductCustomizer\n product={product}\n onAddToCart={onAddToCart}\n showSpecialInstructions={false}\n />\n </div>\n }\n />\n\n {showRelated && relatedProducts && relatedProducts.length > 0 && (\n <RelatedProductsSection\n products={relatedProducts}\n title="More digital products"\n />\n )}\n </div>\n );\n}\n' }] }, "slot-picker": { "name": "slot-picker", "title": "SlotPicker", "description": "Time slot grid for a single day with morning/afternoon/evening grouping.", "type": "component", "registryDependencies": ["price", "cn"], "files": [{ "path": "slot-picker.tsx", "content": '"use client";\n\nimport { Radio } from "@base-ui/react/radio";\nimport { RadioGroup } from "@base-ui/react/radio-group";\nimport React, { useMemo } from "react";\nimport type { AvailableSlot } from "@cimplify/sdk";\nimport type { DurationUnit, SchedulingMode } from "@cimplify/sdk";\nimport { useAvailableSlots } from "@cimplify/sdk/react";\nimport { Price } from "@cimplify/sdk/react";\nimport { cn } from "@cimplify/sdk/react";\n\nexport interface SlotPickerClassNames {\n root?: string;\n group?: string;\n groupLabel?: string;\n slot?: string;\n slotTime?: string;\n slotPrice?: string;\n loading?: string;\n empty?: string;\n}\n\nexport interface SlotPickerProps {\n /** Pre-fetched slots (skips fetch). */\n slots?: AvailableSlot[];\n /** Service ID \u2014 used to fetch slots when `slots` prop is not provided. */\n serviceId?: string;\n /** Date string (YYYY-MM-DD) \u2014 used to fetch slots when `slots` prop is not provided. */\n date?: string;\n /** Number of participants for capacity-based availability. */\n participantCount?: number;\n /** Currently selected slot. */\n selectedSlot?: AvailableSlot | null;\n /** Called when a slot is selected. */\n onSlotSelect?: (slot: AvailableSlot) => void;\n /** Whether to group slots by time of day. Default: true. Ignored when `schedulingMode` is `"multi_day"`. */\n groupByTimeOfDay?: boolean;\n /** Show price on each slot. Default: true. */\n showPrice?: boolean;\n /**\n * Hide slots whose `start_time` is already in the past. Default: true.\n * Set to false to show elapsed slots greyed-out (still un-selectable).\n */\n hideElapsedSlots?: boolean;\n /**\n * Minimum lead time (in minutes) before a slot can be booked. Slots whose\n * start is sooner than `now + minLeadMinutes` are filtered out (when\n * `hideElapsedSlots` is true) or marked unavailable. Default: 0.\n */\n minLeadMinutes?: number;\n /**\n * Service scheduling mode. When `"multi_day"`, each slot renders as a\n * stay summary (`"3 nights: Fri Apr 5, 3:00 PM \u2192 Mon Apr 8, 11:00 AM"`)\n * instead of the time-of-day label. Defaults to `"intraday"`.\n */\n schedulingMode?: SchedulingMode;\n /** Service duration unit \u2014 used for the stay summary in multi-day mode. */\n durationUnit?: DurationUnit;\n /** Service duration value \u2014 used for the stay summary in multi-day mode. */\n durationValue?: number;\n /** Text shown when no slots available. */\n emptyMessage?: string;\n className?: string;\n classNames?: SlotPickerClassNames;\n}\n\ninterface SlotGroup {\n label: string;\n slots: AvailableSlot[];\n}\n\nfunction getTimeOfDay(timeStr: string): "morning" | "afternoon" | "evening" {\n const hour = parseInt(timeStr.split("T").pop()?.split(":")[0] ?? timeStr.split(":")[0], 10);\n if (hour < 12) return "morning";\n if (hour < 17) return "afternoon";\n return "evening";\n}\n\nconst TIME_OF_DAY_LABELS: Record<string, string> = {\n morning: "Morning",\n afternoon: "Afternoon",\n evening: "Evening",\n};\n\nfunction groupSlots(slots: AvailableSlot[]): SlotGroup[] {\n const groups: Record<string, AvailableSlot[]> = {};\n for (const slot of slots) {\n const tod = getTimeOfDay(slot.start_time);\n if (!groups[tod]) groups[tod] = [];\n groups[tod].push(slot);\n }\n return (["morning", "afternoon", "evening"] as const)\n .filter((tod) => groups[tod]?.length)\n .map((tod) => ({ label: TIME_OF_DAY_LABELS[tod], slots: groups[tod] }));\n}\n\nfunction formatTime(timeStr: string): string {\n try {\n const date = new Date(timeStr);\n if (!isNaN(date.getTime())) {\n return date.toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit" });\n }\n } catch {\n // noop\n }\n\n const parts = timeStr.split(":");\n if (parts.length >= 2) {\n const hour = parseInt(parts[0], 10);\n const minute = parts[1];\n const ampm = hour >= 12 ? "PM" : "AM";\n const displayHour = hour % 12 || 12;\n return `${displayHour}:${minute} ${ampm}`;\n }\n return timeStr;\n}\n\nfunction pluralizeUnit(unit: DurationUnit | undefined, value: number | undefined): string {\n if (!unit) return value === 1 ? "day" : "days";\n const v = value ?? 1;\n if (unit === "minutes") return v === 1 ? "minute" : "minutes";\n if (unit === "hours") return v === 1 ? "hour" : "hours";\n if (unit === "days") return v === 1 ? "day" : "days";\n if (unit === "weeks") return v === 1 ? "week" : "weeks";\n if (unit === "months") return v === 1 ? "month" : "months";\n return unit;\n}\n\nfunction formatStaySummary(\n slot: AvailableSlot,\n durationUnit: DurationUnit | undefined,\n durationValue: number | undefined,\n): string {\n const start = new Date(slot.start_time);\n const end = new Date(slot.end_time);\n const startLabel = start.toLocaleString(undefined, {\n weekday: "short",\n month: "short",\n day: "numeric",\n hour: "numeric",\n minute: "2-digit",\n });\n const endLabel = end.toLocaleString(undefined, {\n weekday: "short",\n month: "short",\n day: "numeric",\n hour: "numeric",\n minute: "2-digit",\n });\n const unitLabel = pluralizeUnit(durationUnit, durationValue);\n if (durationValue !== undefined) {\n return `${durationValue} ${unitLabel}: ${startLabel} \u2192 ${endLabel}`;\n }\n return `${startLabel} \u2192 ${endLabel}`;\n}\n\nfunction slotToValue(slot: AvailableSlot): string {\n return `${slot.start_time}|${slot.end_time}`;\n}\n\nexport function SlotPicker({\n slots: slotsProp,\n serviceId,\n date,\n participantCount,\n selectedSlot,\n onSlotSelect,\n groupByTimeOfDay = true,\n showPrice = true,\n schedulingMode = "intraday",\n durationUnit,\n durationValue,\n hideElapsedSlots = true,\n minLeadMinutes = 0,\n emptyMessage = "No available slots",\n className,\n classNames,\n}: SlotPickerProps): React.ReactElement {\n const isMultiDay = schedulingMode === "multi_day";\n const { slots: fetched, isLoading } = useAvailableSlots(\n serviceId ?? null,\n date ?? null,\n {\n participantCount,\n enabled: slotsProp === undefined && !!serviceId && !!date,\n },\n );\n\n const rawSlots = slotsProp ?? fetched;\n // Drop slots that have already elapsed (or fall within the lead-time\n // window). Default behaviour because nothing the merchant can do at\n // the backend stops a client clock from being slightly ahead of the\n // last availability response \u2014 this is the same defence other\n // booking flows ship by default.\n const slots = useMemo(() => {\n if (!hideElapsedSlots) return rawSlots;\n const cutoff = Date.now() + minLeadMinutes * 60_000;\n return rawSlots.filter((slot) => {\n const start = Date.parse(slot.start_time);\n return Number.isNaN(start) || start >= cutoff;\n });\n }, [rawSlots, hideElapsedSlots, minLeadMinutes]);\n\n if (isLoading && slots.length === 0) {\n return (\n <div\n data-cimplify-slot-picker\n aria-busy="true"\n className={cn(className, classNames?.root, classNames?.loading)}\n />\n );\n }\n\n if (slots.length === 0) {\n return (\n <div\n data-cimplify-slot-picker\n data-empty\n className={cn(className, classNames?.root, classNames?.empty)}\n >\n <p>{emptyMessage}</p>\n </div>\n );\n }\n\n const groups = groupByTimeOfDay && !isMultiDay\n ? groupSlots(slots)\n : [{ label: "", slots }];\n\n const slotsByValue = new Map<string, AvailableSlot>();\n for (const slot of slots) {\n slotsByValue.set(slotToValue(slot), slot);\n }\n\n const selectedValue = selectedSlot ? slotToValue(selectedSlot) : "";\n\n return (\n <RadioGroup\n data-cimplify-slot-picker\n className={cn("flex flex-col gap-4", className, classNames?.root)}\n value={selectedValue}\n onValueChange={(value: string) => {\n const slot = slotsByValue.get(value);\n // Slots default to available; treat as unavailable only when the\n // backend explicitly returns `is_available: false`.\n if (slot && slot.is_available !== false) {\n onSlotSelect?.(slot);\n }\n }}\n >\n {groups.map((group) => (\n <div\n key={group.label || "all"}\n data-cimplify-slot-group\n className={cn("flex flex-col gap-2", classNames?.group)}\n >\n {group.label && (\n <div\n data-cimplify-slot-group-label\n className={cn(\n "text-xs font-medium uppercase tracking-[0.12em] text-muted-foreground",\n classNames?.groupLabel,\n )}\n >\n {group.label}\n </div>\n )}\n <div\n className={cn(\n isMultiDay\n ? "flex flex-col gap-2"\n : "grid grid-cols-3 sm:grid-cols-4 gap-2",\n )}\n >\n {group.slots.map((slot) => {\n const value = slotToValue(slot);\n const isSelected =\n selectedSlot?.start_time === slot.start_time &&\n selectedSlot?.end_time === slot.end_time;\n return (\n <Radio.Root\n key={value}\n value={value}\n disabled={slot.is_available === false}\n data-cimplify-slot\n data-selected={isSelected || undefined}\n data-unavailable={slot.is_available === false || undefined}\n className={cn(\n "inline-flex items-center justify-center gap-2 rounded-md border border-border bg-background px-3 py-2 text-sm font-medium text-foreground transition-colors hover:border-foreground/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring data-[selected]:border-foreground data-[selected]:bg-foreground data-[selected]:text-background data-[unavailable]:cursor-not-allowed data-[unavailable]:opacity-40 data-[unavailable]:line-through",\n isMultiDay && "justify-between text-left",\n classNames?.slot,\n )}\n >\n <span data-cimplify-slot-time className={classNames?.slotTime}>\n {isMultiDay\n ? formatStaySummary(slot, durationUnit, durationValue)\n : formatTime(slot.start_time)}\n </span>\n {showPrice && slot.price && (\n <span\n data-cimplify-slot-price\n className={cn("text-xs opacity-70", classNames?.slotPrice)}\n >\n <Price amount={slot.price} />\n </span>\n )}\n </Radio.Root>\n );\n })}\n </div>\n </div>\n ))}\n </RadioGroup>\n );\n}\n' }] }, "sale-badge": { "name": "sale-badge", "title": "SaleBadge", "description": "Sale/discount indicator with percentage and original price.", "type": "component", "registryDependencies": ["price", "cn"], "files": [{ "path": "sale-badge.tsx", "content": '"use client";\n\nimport React from "react";\nimport type { Product, ProductDealInfo } from "@cimplify/sdk";\nimport type { ProductWithPrice } from "@cimplify/sdk";\nimport {\n isOnSale,\n getDiscountPercentage,\n getBasePrice,\n parsePrice,\n} from "@cimplify/sdk";\nimport { Price } from "@cimplify/sdk/react";\nimport { cn } from "@cimplify/sdk/react";\n\nexport interface SaleBadgeClassNames {\n root?: string;\n percentage?: string;\n label?: string;\n originalPrice?: string;\n}\n\nexport interface SaleBadgeProps {\n /** Product, optionally enriched with price_info for sale detection. */\n product: Product & Partial<ProductWithPrice>;\n /** Deal info from useProductDeals / useProductsOnSale. */\n dealInfo?: ProductDealInfo;\n /** Override badge text entirely. */\n label?: string;\n /** Show the original (pre-discount) price with strikethrough styling. */\n showOriginalPrice?: boolean;\n /** Show the percentage off. Default: true. */\n showPercentage?: boolean;\n className?: string;\n classNames?: SaleBadgeClassNames;\n}\n\n/**\n * SaleBadge \u2014 shows a sale/discount indicator for a product.\n *\n * Returns `null` when there\'s no deal, no sale price difference, and no label override,\n * so it\'s safe to render unconditionally \u2014 it simply won\'t show for non-sale products.\n */\nexport function SaleBadge({\n product,\n dealInfo,\n label,\n showOriginalPrice = false,\n showPercentage = true,\n className,\n classNames,\n}: SaleBadgeProps): React.ReactElement | null {\n const onSale = isOnSale(product);\n const hasDeal = dealInfo !== undefined;\n\n if (!hasDeal && !onSale && !label) {\n return null;\n }\n\n // Percentage: prefer dealInfo when it\'s a percentage benefit, else compute from prices\n let percentage: number | null = null;\n if (hasDeal && dealInfo.benefit_type === "percentage") {\n percentage = parsePrice(dealInfo.value);\n } else if (onSale) {\n percentage = getDiscountPercentage(product);\n }\n\n // Badge text: explicit label > deal label > computed "X% off"\n const badgeText =\n label ??\n dealInfo?.label ??\n (percentage != null && percentage > 0 ? `${percentage}% off` : null);\n\n if (!badgeText) {\n return null;\n }\n\n return (\n <span\n data-cimplify-sale-badge\n className={cn(className, classNames?.root)}\n style={{ display: "inline-flex", alignItems: "center", gap: "0.375rem" }}\n >\n {showPercentage && percentage != null && percentage > 0 && (\n <span data-cimplify-sale-percentage className={classNames?.percentage}>\n -{percentage}%\n </span>\n )}\n <span data-cimplify-sale-label className={classNames?.label}>\n {badgeText}\n </span>\n {showOriginalPrice && onSale && (\n <span data-cimplify-sale-original-price className={classNames?.originalPrice}>\n <Price amount={getBasePrice(product)} />\n </span>\n )}\n </span>\n );\n}\n' }] } };
5705
5705
  var REGISTRY_INDEX = { "components": [{ "name": "price", "title": "Price", "description": "Renders a formatted price in the display currency.", "type": "component", "registryDependencies": [] }, { "name": "quantity-selector", "title": "QuantitySelector", "description": "Controlled increment/decrement quantity input.", "type": "component", "registryDependencies": [] }, { "name": "variant-selector", "title": "VariantSelector", "description": "Select product variants via axis chips or direct list.", "type": "component", "registryDependencies": ["price"] }, { "name": "add-on-selector", "title": "AddOnSelector", "description": "Modifier groups with single-select or multi-select options.", "type": "component", "registryDependencies": ["price"] }, { "name": "bundle-selector", "title": "BundleSelector", "description": "Bundle component picker with variant choices and price summary.", "type": "component", "registryDependencies": ["price"] }, { "name": "composite-selector", "title": "CompositeSelector", "description": "Composite product builder with group constraints and live pricing.", "type": "component", "registryDependencies": ["price"] }, { "name": "product-customizer", "title": "ProductCustomizer", "description": "Full product configuration with variants, add-ons, and add-to-cart.", "type": "component", "registryDependencies": ["price", "quantity-selector", "variant-selector", "add-on-selector", "composite-selector", "bundle-selector"] }, { "name": "product-image-gallery", "title": "ProductImageGallery", "description": "Main image with thumbnail strip for product images.", "type": "component", "registryDependencies": [] }, { "name": "cart-summary", "title": "CartSummary", "description": "Cart line items with quantity controls and totals.", "type": "component", "registryDependencies": ["price", "quantity-selector"] }, { "name": "availability-badge", "title": "AvailabilityBadge", "description": "Displays in-stock / out-of-stock status for tracked products.", "type": "component", "registryDependencies": ["cn"] }, { "name": "sale-badge", "title": "SaleBadge", "description": "Sale/discount indicator with percentage and original price.", "type": "component", "registryDependencies": ["price", "cn"] }, { "name": "product-sheet", "title": "ProductSheet", "description": "Full product detail view with gallery, header, and customizer.", "type": "component", "registryDependencies": ["price", "product-image-gallery", "product-customizer", "cn"] }, { "name": "product-card", "title": "ProductCard", "description": "Product display card with modal or link mode.", "type": "component", "registryDependencies": ["price", "product-sheet", "cn"] }, { "name": "product-grid", "title": "ProductGrid", "description": "Responsive CSS grid that renders ProductCards.", "type": "component", "registryDependencies": ["product-card", "cn"] }, { "name": "cn", "title": "cn", "description": "Utility that merges Tailwind CSS classes with clsx + tailwind-merge.", "type": "utility", "registryDependencies": [] }, { "name": "search-input", "title": "SearchInput", "description": "Search bar with debounced results dropdown.", "type": "component", "registryDependencies": ["cn"] }, { "name": "category-filter", "title": "CategoryFilter", "description": "Selectable category chips for filtering products.", "type": "component", "registryDependencies": ["cn"] }, { "name": "discount-input", "title": "DiscountInput", "description": "Discount code input with inline validation.", "type": "component", "registryDependencies": ["price", "cn"] }, { "name": "category-grid", "title": "CategoryGrid", "description": "Responsive grid of category cards.", "type": "component", "registryDependencies": ["cn"] }, { "name": "deal-banner", "title": "DealBanner", "description": "Displays active deals and promotions.", "type": "component", "registryDependencies": ["price", "cn"] }, { "name": "order-summary", "title": "OrderSummary", "description": "Single order detail view with line items and totals.", "type": "component", "registryDependencies": ["price", "cn"] }, { "name": "order-history", "title": "OrderHistory", "description": "List of past orders with status and totals.", "type": "component", "registryDependencies": ["price", "cn"] }, { "name": "store-nav", "title": "StoreNav", "description": "Top navigation bar with brand, categories, cart badge, and search.", "type": "component", "registryDependencies": ["cn"] }, { "name": "catalogue-page", "title": "CataloguePage", "description": "Browse all products with category filtering and search.", "type": "component", "registryDependencies": ["product-grid", "category-filter", "search-input", "cn"] }, { "name": "product-page", "title": "ProductPage", "description": "Smart product page resolver with per-slug and per-type template routing.", "type": "component", "registryDependencies": ["default-product-layout", "food-product-layout", "wholesale-product-layout", "service-product-layout", "digital-product-layout", "cn"] }, { "name": "cart-page", "title": "CartPage", "description": "Full-page cart with summary, discount input, and checkout.", "type": "component", "registryDependencies": ["cart-summary", "discount-input", "cn"] }, { "name": "checkout-page", "title": "CheckoutPage", "description": "Multi-step checkout with auth, address, and payment.", "type": "component", "registryDependencies": ["cn"] }, { "name": "collection-page", "title": "CollectionPage", "description": "Curated product collection with header and grid.", "type": "component", "registryDependencies": ["product-grid", "cn"] }, { "name": "order-detail-page", "title": "OrderDetailPage", "description": "Single order detail view with live status polling.", "type": "component", "registryDependencies": ["order-summary", "cn"] }, { "name": "order-history-page", "title": "OrderHistoryPage", "description": "Order list with status filtering and inline detail view.", "type": "component", "registryDependencies": ["order-history", "order-summary", "cn"] }, { "name": "search-page", "title": "SearchPage", "description": "Dedicated search page with input and results grid.", "type": "component", "registryDependencies": ["product-grid", "cn"] }, { "name": "deals-page", "title": "DealsPage", "description": "Promotions landing page with deal banners and on-sale products.", "type": "component", "registryDependencies": ["deal-banner", "product-grid", "sale-badge", "product-card", "cn"] }, { "name": "slot-picker", "title": "SlotPicker", "description": "Time slot grid for a single day with morning/afternoon/evening grouping.", "type": "component", "registryDependencies": ["price", "cn"] }, { "name": "date-slot-picker", "title": "DateSlotPicker", "description": "Horizontal date strip with slot picker for service scheduling.", "type": "component", "registryDependencies": ["slot-picker", "cn"] }, { "name": "staff-picker", "title": "StaffPicker", "description": "Staff member selection list with avatar and bio.", "type": "component", "registryDependencies": ["cn"] }, { "name": "booking-card", "title": "BookingCard", "description": "Single booking display with status, time, and action buttons.", "type": "component", "registryDependencies": ["price", "cn"] }, { "name": "booking-list", "title": "BookingList", "description": "List of booking cards with optional self-fetching.", "type": "component", "registryDependencies": ["booking-card", "cn"] }, { "name": "booking-page", "title": "BookingPage", "description": "Multi-step booking flow: service, staff, resource, date/slot, confirmation.", "type": "component", "registryDependencies": ["date-slot-picker", "staff-picker", "resource-picker", "price", "cn"] }, { "name": "bookings-page", "title": "BookingsPage", "description": "Account-area page listing a customer's bookings with filters and detail view.", "type": "component", "registryDependencies": ["booking-list", "booking-card", "cn"] }, { "name": "food-product-card", "title": "FoodProductCard", "description": "Product card for food items with tags, badges, and quick-add.", "type": "component", "registryDependencies": ["price", "cn"] }, { "name": "retail-product-card", "title": "RetailProductCard", "description": "Product card for retail with color swatches, sale badge, wishlist, and sold-out overlay.", "type": "component", "registryDependencies": ["price", "cn"] }, { "name": "wholesale-product-card", "title": "WholesaleProductCard", "description": "B2B product card with price range, MOQ badge, and stock count.", "type": "component", "registryDependencies": ["price", "price-range", "cn"] }, { "name": "digital-product-card", "title": "DigitalProductCard", "description": "Digital product card with type badge, file info, and event details.", "type": "component", "registryDependencies": ["price", "cn"] }, { "name": "standard-service-card", "title": "StandardServiceCard", "description": "Service card with hero image, duration, deposit, and availability.", "type": "component", "registryDependencies": ["price", "cn"] }, { "name": "compact-service-card", "title": "CompactServiceCard", "description": "Horizontal service card with thumbnail for list views.", "type": "component", "registryDependencies": ["price", "cn"] }, { "name": "schedule-service-card", "title": "ScheduleServiceCard", "description": "Service card with next available time slot pills.", "type": "component", "registryDependencies": ["price", "cn"] }, { "name": "rental-service-card", "title": "RentalServiceCard", "description": "Rental card with per-day/hour pricing, deposit, and availability count.", "type": "component", "registryDependencies": ["price", "cn"] }, { "name": "accommodation-card", "title": "AccommodationCard", "description": "Hotel/accommodation card with per-night pricing, amenities, capacity, and cancellation.", "type": "component", "registryDependencies": ["price", "volume-pricing", "cn"] }, { "name": "lease-service-card", "title": "LeaseServiceCard", "description": "Long-term lease card with per-month/year pricing, volume tiers, and billing.", "type": "component", "registryDependencies": ["price", "volume-pricing", "cn"] }, { "name": "subscription-card", "title": "SubscriptionCard", "description": "Subscription card with billing plan options, trial badge, and setup fee.", "type": "component", "registryDependencies": ["price", "cn"] }, { "name": "volume-pricing", "title": "VolumePricing", "description": "Collapsible volume pricing tier table.", "type": "component", "registryDependencies": ["price", "cn"] }, { "name": "price-range", "title": "PriceRange", "description": "Displays min-max price range for products with variants or tiers.", "type": "component", "registryDependencies": [] }, { "name": "default-product-layout", "title": "DefaultProductLayout", "description": "Two-column product layout for retail/physical products with sale badges, specs, and properties.", "type": "component", "registryDependencies": ["product-image-gallery", "product-customizer", "price", "price-range", "availability-badge", "sale-badge", "cn"] }, { "name": "food-product-layout", "title": "FoodProductLayout", "description": "Restaurant product layout with allergens, ingredients, pairings, and dietary tags.", "type": "component", "registryDependencies": ["product-image-gallery", "product-customizer", "price", "cn"] }, { "name": "wholesale-product-layout", "title": "WholesaleProductLayout", "description": "B2B wholesale layout with price range, volume pricing, MOQ, and inventory.", "type": "component", "registryDependencies": ["product-image-gallery", "product-customizer", "price", "price-range", "volume-pricing", "cn"] }, { "name": "service-product-layout", "title": "ServiceProductLayout", "description": "Service booking layout with duration, deposit, staff requirements, and cancellation policy.", "type": "component", "registryDependencies": ["product-image-gallery", "product-customizer", "price", "cn"] }, { "name": "digital-product-layout", "title": "DigitalProductLayout", "description": "Digital product layout with file details, download limits, event info, and access passes.", "type": "component", "registryDependencies": ["product-image-gallery", "product-customizer", "price", "cn"] }, { "name": "chat-widget", "title": "ChatWidget", "description": "Embeddable chat widget with AI shopping assistant powered by the support channel.", "type": "component", "registryDependencies": ["cn"] }, { "name": "cart-drawer", "title": "CartDrawer", "description": "Slide-in side cart drawer with provider context, free-shipping progress, animated subtotal, and empty state. Auto-opens on add-to-cart.", "type": "component", "registryDependencies": ["cart-summary", "price", "cn"] }, { "name": "account", "title": "CimplifyAccount", "description": "Iframe wrapper for the Cimplify account portal \u2014 sign-in, orders, addresses, settings.", "type": "component", "registryDependencies": ["cn"] }, { "name": "billing-plan-selector", "title": "BillingPlanSelector", "description": "Subscription / billing-plan picker \u2014 surfaces eligible plans with pricing and trial periods.", "type": "component", "registryDependencies": ["price", "cn"] }, { "name": "currency-selector", "title": "CurrencySelector", "description": "Multi-currency switcher backed by the FX provider \u2014 locks display currency and quote ID.", "type": "component", "registryDependencies": ["cn"] }, { "name": "customer-input-fields", "title": "CustomerInputFields", "description": "Per-product custom input fields \u2014 text, number, date, time, file upload, image upload, single/multi-select.", "type": "component", "registryDependencies": ["cn"] }, { "name": "delivery-estimate", "title": "DeliveryEstimate", "description": "Delivery fee + ETA preview at the cart/checkout edge, sourced from /delivery/fee with location.", "type": "component", "registryDependencies": ["price", "cn"] }, { "name": "location-picker", "title": "LocationPicker", "description": "Branch / pickup-point selector for businesses with multiple locations.", "type": "component", "registryDependencies": ["cn"] }, { "name": "resource-picker", "title": "ResourcePicker", "description": "Staff / room / resource picker for bookable services \u2014 used by services and restaurant reservation flows.", "type": "component", "registryDependencies": ["cn"] }, { "name": "recently-viewed", "title": "RecentlyViewed", "description": "Horizontally scrollable rail of recently viewed products, hydrated from local activity state.", "type": "component", "registryDependencies": ["product-card", "cn"] }, { "name": "recommendation-carousel", "title": "RecommendationCarousel", "description": "Personalized product carousel powered by /activity/recommendations \u2014 frequently bought, related, trending.", "type": "component", "registryDependencies": ["product-card", "cn"] }, { "name": "session-message-banner", "title": "SessionMessageBanner", "description": "Top-of-page banner for session-scoped messages (promo nudges, abandoned cart prompts, low-stock alerts) with dismiss tracking.", "type": "component", "registryDependencies": ["cn"] }] };
5706
5706