Compare commits

..

6 Commits

Author SHA1 Message Date
Alex Dunmow
e6348edd10 fix(pastel-dream): reliable layout via theme CSS + graceful empty media
Host Tailwind (v4, source(none)) does not scan installed .bnp theme
templates, so grid/aspect/gap/padding utilities used only in this theme
were silently dropped: custom persona blocks collapsed to a single
full-width column, media boxes rendered as thin strips, and p-5 loss
clipped the first glyph of every description. Move the theme's layout
primitives into css.input_css_append as plain CSS with explicit
media queries (mobile-first, collapse to 1 column) so the render no
longer depends on the JIT content scan.

- Add .pd-section / .pd-grid(--2/--3/--lg-2/--lg-4) / .pd-menu-cols /
  .pd-menu-list / .pd-media(--square) / .pd-card-body helpers to both
  manifest.yaml (served) and assets/style.css. Semantic tokens only.
- seasonal_specials + cake_gallery: reliable responsive grid, OMIT the
  media box when no image is set (no ballooning empty boxes / thin
  strips), real body padding.
- menu-list: honor columns=2 as a true two-up board; reliable item spacing.
- hours-location / feature-grid: reliable responsive grids.
- Tighten section rhythm (py-14/py-24 -> .pd-section) to remove dead bands.
- navbar drawer width pinned in its inline <style>; body overflow-x: clip
  guards against horizontal scroll on small screens.

No .reveal/scroll-reveal content-hiding in this theme (stats count-up
only, values render by default). No version bump.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 17:26:33 +08:00
Alex Dunmow
8eaf3a68aa fix(pastel-dream): collapse multi-line ninjatpl comments to single-line (load-fatal lexer error) 2026-07-05 17:06:50 +08:00
Alex Dunmow
834772eef4 bump to 0.2.2 2026-07-05 16:51:02 +08:00
Alex Dunmow
d48114cb8f bump to 0.2.1 2026-07-05 16:44:49 +08:00
Alex Dunmow
dcccb7dd8a docs: publishing README 2026-07-05 13:43:27 +08:00
Alex Dunmow
1ee98d318a feat(pastel-dream): WO-TF-023 bakery/patisserie theme — full Wave A rebuild
Pivot from the wellness/spa WZ-021 state to the locked bakery/patisserie
persona. Codeless .bnp, verified against cms a0f07e2ae (OVERRIDE-CONTRACT).

- 49 mandatory template overrides (+ manifest entries), re-skinned from the
  field-correct coffee sibling: every content field, provider var, custom tag
  and JS preserved verbatim; only styling + copy changed. §4 button/card reads
  reconciled (link/style/label, text/media).
- 7 page templates: default, landing, article, full-width, blog-index,
  contact, auth.
- 4 persona blocks: seasonal_specials, cake_gallery, order_ribbon, bakers_note
  (old 6 duplicate-builtin blocks deleted -> now overrides).
- 5 dual-mode presets (morning-haze/berry-cream/matcha-cream/lavender-honey/
  peach-blossom), all 19 tokens, tuned dusk-berry dark.
- Bundled OFL fonts: Fraunces (heading) + Quicksand (body); all font-family via
  var(--font-*). Themed email-safe wrapper. master_pages + demo seed (home,
  about, blog x3, contact w/ data-table + row_inserted email workflow, login).
- CSS-FIRST motion: watercolor washes, scalloped edges, soft pills; reduced-
  motion honored.

Gates: make -> exit 0 (codeless .bnp); check-safety -> 19 ok 13 skip, exit 0.
Known limits per contract §11/§12 (auth trio + page-suggestions + provider-
backed lists inert until cms fix). Visuals UNVERIFIED until Wave B.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 12:41:03 +08:00
97 changed files with 2792 additions and 1419 deletions

View File

@ -1,280 +1,85 @@
# Pastel Dream — Build Report
# Pastel Dream — Build Report (Wave A, WO-TF-023)
Implementation pass status against the spec
(`themes/docs/works/pastel-dream.md`) and gating UAT
(`themes/docs/uat/pastel-dream.md`), executed under wave-1 fonts policy
(`themes/docs/FONTS.md`).
Bakery / patisserie persona. Codeless `.bnp` theme (no Go, no wasm). Built and
verified against cms main at `a0f07e2ae` (OVERRIDE-CONTRACT freeze).
## What landed
### Identity & module
- `plugin.mod` written verbatim from spec §"plugin.mod": `name = "pastel-dream"`,
`kind = "theme"`, `scope = "@themes"`, `display_name = "Pastel Dream"`
(12 chars), `description` = 100 chars, `categories = ["templates"]`, 9 tags
matching spec, `[compatibility].block_core = ">=0.11.0 <0.12.0"`,
`version = "0.1.0"`.
- `go.mod` module path `git.dev.alexdunmow.com/block/themes/pastel-dream`,
`go 1.26.4`, `git.dev.alexdunmow.com/block/core v0.11.1`, no replace
directives, indirect dependencies pinned to the same versions gotham uses.
- `go.sum` is fully tidied (42 lines) — `go mod tidy` exits clean.
### Identity
- `plugin.mod`: `name = "pastel-dream"`, `kind = "theme"`, `scope = "@themes"`,
`version = "0.2.0"`, bakery tags, `required_icon_packs = ["lucide", "simple-icons"]`.
- `manifest.yaml`: system template `pastel-dream`, 7 page templates, email
wrapper, 49 template overrides, `css.input_css_append` (pastel utility layer),
icon packs, presets/fonts/master-pages references.
### Build artefact
- `pastel-dream.so` produced by `make` (local CGO build, no container):
size **21,517,984 bytes (≈21 MB)** which sits inside the normal range
(gotham is ~21 MB, lcars is ~30 MB).
- Exported symbol `Registration` confirmed present via `nm -D`:
`git.dev.alexdunmow.com/block/themes/pastel-dream.Registration`.
- `go vet ./...` exits 0.
### Overrides (49 mandatory keys)
- One `templates/overrides/pastel-dream/<key>.ninjatpl` per mandatory key, plus a
`template_overrides` entry per key. Re-skinned from the field-correct `coffee`
sibling (frozen at the same cms pin): every content field, provider var, custom
tag, and JS block preserved verbatim; only styling and copy changed to the
pastel bakery identity. Legacy button/card reads already reconciled per §4
(`link`/`style`/`label`, `text`/`media`).
### System template and page templates (register.go)
- `RegisterSystemTemplate` called exactly once with `Key: "pastel-dream"`.
- 4 page templates registered per spec §"Page templates":
- `default` — Slots: `header, main, footer`.
- `landing` — Slots: `hero, main, cta, footer`.
- `article` — Slots: `header, main, footer`.
- `full-width` — Slots: `header, main, footer`.
- All four templates render via `RenderPastelDefault` / `…Landing` /
`…Article` / `…FullWidth`, each templ-backed in `template.templ`.
### Page templates (7)
- `default`, `landing`, `article`, `full-width`, `blog-index`, `contact`, `auth`
under `templates/pastel-dream/`. Soft watercolor-wash chrome (`pd-paper` body).
### Schema loading order
- `br.LoadSchemasFromFS(Schemas())` is called at `register.go:72`, strictly
before the first `br.Register` at `register.go:77`. UAT §3 line-order
check passes.
### Persona blocks (4)
- `pastel-dream:seasonal_specials`, `pastel-dream:cake_gallery`,
`pastel-dream:order_ribbon`, `pastel-dream:bakers_note`. Each is a
`blocks/blocks.yaml` definition + `<key>.schema.json` (draft-07) +
`<key>.ninjatpl`. Media via `{% img %}`; arrays via `x-editor: collection`.
The six old duplicate-builtin blocks (soft-navbar, watercolor-hero,
affirmation, testimonial-soft, feature-grid-soft, cozy-footer) were deleted;
their surfaces are now the navbar/hero/quote/testimonial/feature-grid/footer
overrides.
### Theme-owned blocks (6 in total)
Each block has one `<key>.go` + `<key>.templ` pair under repo root, one
`schemas/<key>.schema.json` (draft-07), and exactly one `br.Register` call
in `register.go`. All blocks declare `Source: "pastel-dream"` on
`BlockMeta`. Standalone signature `func(ctx, content) string` is used
throughout (no `children` argument).
### Presets (5, dual-mode)
- `morning-haze`, `berry-cream`, `matcha-cream`, `lavender-honey`,
`peach-blossom`. All `mode: "both"`, all 19 tokens per light + dark. Dark is a
tuned dusk plum/berry ground (not a lightened pastel) per the WO.
| Key | Title | Category | Schema fields |
|--------------------|--------------------|-------------|------------------------------------------------------------------------------------------|
| `soft-navbar` | Soft Navbar | Navigation | logo:text, menuName:menu-select, ctaText:text, ctaHref:link |
| `watercolor-hero` | Watercolor Hero | Theme | eyebrow:text, headline:richtext, body:richtext, image:media, ctaText:text, ctaHref:link |
| `affirmation` | Affirmation Strip | Theme | quote:textarea, author:text, palette:select(blush\|mint\|butter\|sky) |
| `testimonial-soft` | Soft Testimonial | Theme | quote:richtext, name:text, role:text, avatar:media, rating:number |
| `feature-grid-soft`| Feature Trio | Layout | intro:richtext, items:collection(icon:media, title:text, body:textarea) |
| `cozy-footer` | Cozy Footer | Navigation | showSignup:select, affirmation:textarea, menuName:menu-select, social:array(link) |
### Fonts (bundled, OFL)
- `fonts.json`: **Fraunces** 400/600 (heading) + **Quicksand** 400/700 (body),
woff2 in `assets/fonts/`, OFL-1.1 recorded in `assets/fonts/OFL.txt`. Every
`font-family` resolves through `var(--font-heading|body|mono, <fallback>)`.
All `x-editor` values fall within the allowed set
`{text, richtext, media, color, select, number, slug, textarea, array, collection, bucket-picker, menu-select, template-select, link}`.
### Email
- `templates/email/pastel-dream.ninjatpl`: table-based, inline styles, pastel
hex fallbacks for clients without CSS custom properties, scalloped divider.
### Built-in overrides
Four overrides wired via `br.RegisterTemplateOverride("pastel-dream", …)`:
- `heading` — Caveat Brush at H1/H2 (display face), Nunito H3+, soft
`.brush-underline` accent.
- `text``font-body` with `line-height: 1.75` per spec.
- `button``.pastel-pill` (border-radius 9999px, tinted shadow, 2px
hover lift, `cubic-bezier(.22,1,.36,1)` easing).
- `card``.pastel-card` (border-radius 20px, blush-tinted shadow,
`border-color: hsl(var(--border))`).
### Master pages + seed
- `master_pages.json`: default/landing/blog/auth masters, "Petal & Crumb" brand.
- `seed/seed.json` (`demo: true`): home, about, blog index + 3 posts, contact,
login. Contact form → `contact_submissions` data table via
`formConfig.targetTable`. `seed/workflows.json`: `row_inserted`
email-admins ("Form Submission Notification", recipients admins/all). Bakery
voice, no LLM-isms, no em-dashes, no fabricated stats.
### Master pages
`DefaultMasterPages()` returns exactly two entries:
### Motion tier: CSS-FIRST
- Watercolor wash + gentle `pd-breathe` blob drift + brush draw-in only. No
canvas, no JS showpiece. All animation disabled under
`prefers-reduced-motion: reduce`.
1. **`pastel-dream:default-master`** — `PageTemplates: ["default", "article"]`.
Blocks: `pastel-dream:soft-navbar` (header), `slot` (main), `pastel-dream:cozy-footer` (footer).
2. **`pastel-dream:landing-master`** — `PageTemplates: ["landing"]`.
Blocks: `pastel-dream:watercolor-hero` (hero), `slot` (main),
`pastel-dream:affirmation` (cta), `pastel-dream:cozy-footer` (footer).
`slot` block `Content.slotName` matches its slot name in every case.
### Presets (presets.json)
Three presets present, ids exactly `blush-morning`, `mint-meadow`,
`twilight-petal`. All 19 tokens declared per preset. Every value matches
`^\d+ \d+% \d+%$` (HSL triple, no `hsl()` wrapper). `blush-morning` and
`mint-meadow` carry `lightColors`; `twilight-petal` carries `darkColors`.
Values copied verbatim from spec §"Variants (presets.json)".
### Fonts (wave-1 policy)
- `fonts.json = []` (literal). Embed succeeds.
- No woff2s bundled. `assets/fonts/web/` holds a `README.txt` placeholder
so the directory is non-empty (required by `//go:embed assets/*`).
- CSS variable consumer pattern in use everywhere:
`var(--font-heading, "Caveat Brush", …)`, `var(--font-body, "Nunito", …)`,
`var(--font-mono, "JetBrains Mono", …)`. Fallback stacks degrade
gracefully when no admin has assigned fonts.
- `RECOMMENDED_FONTS.md` shipped at theme root with Google Fonts picker
recommendations for Caveat Brush, Nunito, JetBrains Mono per spec §5.
### CSS strategy
- `--radius-soft: 20px;` declared once.
- `@keyframes pastel-shimmer` and `@keyframes breathe` declared.
- `cubic-bezier(.22,1,.36,1)` used in `.pastel-pill` and feature card
transitions.
- `.bg-watercolor-blush` / `.bg-watercolor-mint` utility classes provided
for hero / footer backdrops.
- `prefers-reduced-motion: reduce` media query disables both animations and
the pill hover transform.
- Theme CSS is injected via `CSSManifest.InputCSSAppend` (reads
`assets/css/pastel-dream.css`). The asset endpoint also serves a
mirrored `assets/style.css` so direct loads work in case the manifest is
bypassed.
- No hardcoded hex / rgb / named colors in `*.templ`, `*.go`, or
`assets/css/` *for runtime UI* — every color resolves through
`hsl(var(--token))`. The **email wrapper is an explicit exception**: it
carries hex fallbacks because email clients cannot read CSS custom
properties. The colors are sourced from `EmailContext.Colors` when
present; the hex fallbacks only fire when no preset is bound to the
email (e.g. raw previews). See "Open items / deferred" below.
### Email wrapper
- `tr.RegisterEmailWrapper("pastel-dream", PastelEmailWrapper)` called once.
- 560 px centered frame, cream paper backdrop, top-right watercolor blob
watermark (inline SVG with `fill = hsl(var(--primary))` via
`pastelEmailPrimary`).
- Caveat Brush masthead (`'Caveat Brush', 'Sacramento', cursive`),
Nunito body.
- Footer carries an `<a>` for the unsubscribe URL token and a one-line
Caveat Brush affirmation.
- A reusable mint CTA pill style helper (`pastelEmailPillStyle`) is
exported for body content to consume.
## Build output
## Verification (both exit 0)
```
$ cd /home/alex/src/blockninja/themes/pastel-dream
$ go mod tidy # ← exits 0
$ /home/alex/go/bin/templ generate # ← exits 0, 12 *_templ.go files
$ make # ← exits 0
CGO_ENABLED=1 go build -buildmode=plugin -ldflags="-s -w" -o pastel-dream.so .
$ ls -lh pastel-dream.so
-rw-rw-r-- 1 alex alex 21M Jun 6 13:25 pastel-dream.so
$ nm -D pastel-dream.so | grep Registration
000000000140c420 D git.dev.alexdunmow.com/block/themes/pastel-dream.Registration
$ cd themes/pastel-dream && make
Built pastel-dream@0.2.0 (codeless) → pastel-dream-0.2.0.bnp # EXIT 0
$ cd check-safety && go run . themes/pastel-dream
32 checks: 19 ok 13 skip -> OK # EXIT 0
```
## Safety check
Runtime template rendering is not exercised by these gates (codeless build +
check-safety are static). Visual correctness stays UNVERIFIED until Wave B.
The task brief points at
`/home/alex/src/blockninja/backend/go.mod` / `./cmd/check-safety`. **That
backend path does not exist on this system.** The canonical check-safety
tool lives at `/home/alex/src/blockninja/check-safety/` and reads the CMS
SDK version from `/home/alex/src/blockninja/cms/backend/go.mod`. I ran it
from that location:
```
$ cd /home/alex/src/blockninja/check-safety
$ go run . --plugin-dir /home/alex/src/blockninja/themes/pastel-dream
```
### Per-plugin result: PASS
Every check that names `pastel-dream` reports `OK`:
```
=== Check 1: Secret env var reads outside config.Load() ===
OK: No secret env var reads outside config.Load() (1 plugin roots scanned)
OK: pastel-dream
=== Check 2b: Plugin proto ownership ===
OK: pastel-dream
=== Check 2c: Standalone plugin SDK import boundaries ===
OK: Standalone plugin imports and go.mod stay on SDK version v0.11.1
OK: pastel-dream
=== Check 3: Go code compiles and passes go fix, golangci-lint --fix, go vet, and strict lint ===
OK: 1 Go module(s) cleared the Go lint pipeline
=== Check 6: No hardcoded colors in frontend (use theme tokens) ===
OK: No hardcoded colors in .templ files
=== Check 11: No placeholder code; only shipped features ===
=== Check 12: No reinvented utilities (use helpers) ===
OK: pastel-dream
=== Check 17: No TODO markers in production code ===
OK: pastel-dream
=== Check 18: Plugin segmentation (safety-rules.yml) ===
OK: pastel-dream
=== Check 21: Plugin presets.json validation ===
OK: pastel-dream presets valid
```
Total: 10 OK lines mentioning `pastel-dream`, zero `FAIL:` lines that
reference any path inside `themes/pastel-dream/`.
### Whole-run exit code: 1 (non-zero), but not pastel-dream-related
The `check-safety` binary always exits 1 in this layout because, when
invoked from inside its own source directory, several of its own internal
files trip the same regexes the tool enforces. The three FAILs the tool
prints all live inside the tool's own source tree, not the plugin:
```
FAIL: 21 placeholder reference(s) found:
check_placeholder.go:51, colors.go:183, comingsoon.go:163-231, main.go:19, …
FAIL: 10 TODO marker(s) found:
check_todos.go:34, todo.go:86, todo.go:115, frontend.go:108, main.go:25
FAIL: 3 hand-rolled HTML sanitization pattern(s):
check_htmlsanitize.go:37-38, htmlsanitize.go:41
```
None of those paths is in `themes/pastel-dream/`. The strict
`./cmd/check-safety . --plugin-dir <pastel-dream>` exit-0 requirement
from the task brief cannot be satisfied from this checkout without
patching `check-safety` itself (out of scope and outside the theme
directory). The **plugin-specific verdict** is PASS.
## Open items / deferred
These were scoped out of this implementation pass and should be picked up
in wave-2 / UAT sign-off:
- **Bundled woff2 fonts.** Per wave-1 policy (`themes/docs/FONTS.md`),
this pass ships `fonts.json = []`. Spec §"Bundled fonts" lists Caveat
Brush, Nunito (300/400/600/700), and JetBrains Mono — these should be
sourced (OFL is fine for all three), placed under `assets/fonts/web/`,
declared in `fonts.json`, and licence-attributed in `LICENSES.md`. The
matching UAT §11 checks (woff2 magic, 200 OK on fetch, FOUT ≤200ms,
`@font-face` rule count ≥ 3) become real then.
- **Marketplace assets (UAT §12).** Six 1440×900 PNG screenshots, demo
seed for "Linden & Loom" (4 posts / 3 services / 2 testimonials), and
the launch copy line require a running container, design QA, and the
marketplace folder at `themes/docs/marketplace/pastel-dream/`. None of
those can land inside the theme directory.
- **Email wrapper hex fallbacks vs UAT §5 regex.** UAT §5 reads:
`grep -rE "#[0-9a-fA-F]{3,8}|rgb\(|rgba\(" pastel-dream/*.templ … returns
zero matches`. The email wrapper carries hex fallbacks for clients that
cannot resolve CSS custom properties (Gmail web, Outlook, etc.). At
runtime `EmailContext.Colors` is populated from the active preset so
the hex values never reach an actual recipient — they are pure
no-active-preset fallbacks. The check-safety colors check accepts this
pattern (gotham follows the same convention). A future pass could move
the fallback table into a hex-free Go file via `fmt.Sprintf` from HSL
triples; deferred.
- **Live container deploy.** `make rebuild` and the `instance-pastel-dream`
container live in the host CMS workflow. UAT §2 checks about
`podman ps` showing `Up`, `make logs` showing zero ERROR / panic, and
any tests against `https://pastel-dream.localdev.blockninjacms.com/`
require that container, which is out of scope for this build pass.
- **DOM-based gates (UAT §5 line 7, §6, §7, §8, §10, §13).** These need
the running URL with Chrome DevTools / Playwright. Computed-style and
network-request assertions are deferred to manual UAT.
- **Regression gate (UAT §14).** Comparing rendered HTML SHA-256 across
multi-theme installs and uninstall behaviour against the real CMS DB is
out of scope for the build pass.
- **Sign-off (UAT §15).** Three named reviewers (Designer, Engineer,
Marketplace owner) must independently tick boxes. Cannot be self-applied.
## Where everything lives
```
themes/pastel-dream/
├── BUILD_REPORT.md ← this file
├── Makefile ← `make` builds .so locally, `make rebuild` deploys
├── RECOMMENDED_FONTS.md ← wave-1 Google Fonts picker recommendations
├── plugin.mod ← TOML metadata (locked to spec)
├── go.mod / go.sum ← block/core v0.11.1, no replace directives
├── presets.json ← 3 presets, all 19 tokens, HSL triples
├── fonts.json ← []
├── embed.go ← //go:embed wiring
├── registration.go ← exports Registration + ThemeCSSManifest
├── register.go ← Register() + DefaultMasterPages()
├── helpers.go ← getString / getSlice / getBoolish / safeLink
├── template.templ ← 4 page templates (Default, Landing, Article, FullWidth)
├── email_wrapper.templ ← PastelEmailWrapper
├── <block>.go + <block>.templ ← one pair per block + override
├── *_templ.go ← templ-generated; committed
├── schemas/ ← one *.schema.json per block
├── assets/css/pastel-dream.css← injected via CSSManifest.InputCSSAppend
├── assets/style.css ← served at /templates/pastel-dream/style.css
└── assets/fonts/web/ ← reserved for wave-2 bundled woff2s
```
## Known limits (platform gaps, per OVERRIDE-CONTRACT §11/§12)
- `auth-form`, `auth-status`, `password-reset-form`, `page-suggestions` overrides
are inert until the compiled-block override-dispatch fix lands (authored
correctly to model).
- Provider-backed overrides (navbar/footer menus, featured/related/popular posts,
category-list, breadcrumbs, blog-hero, sidebar-nav, author-bio-hero) render
themed chrome but empty lists until the codeless loader registers overrides via
the source path. Authored faithful to the builtin provider var names.
- `preview.png` is the old (wellness) render and must be regenerated in Wave B.

24
README.md Normal file
View File

@ -0,0 +1,24 @@
# Pastel Dream
A soft bakery window in the morning light. Watercolor washes fade behind scalloped edges, and rounded menu cards hold the day's specials in blush, berry, and lilac. The dark mode shifts to a warm dusk with deeper berry tones, so evening pages feel cosy rather than harsh. It looks sweet and unhurried.
## Good for
We built Pastel Dream for food and hospitality with a gentle touch. Treat these as starting points, not limits. If the mood fits your brand, use it:
- Bakeries and patisseries
- Cafes and dessert bars
- Florists and gift shops
- Wedding and party catering
## What you get
- Five color presets, each with a tuned light mode and a tuned dusk-toned dark mode: Morning Haze, Berry Cream, Matcha Cream, Lavender Honey, and Peach Blossom.
- Fraunces for headings and Quicksand for body, both bundled. Fonts stay admin-controlled, so you can swap them from the typography settings.
- Four persona blocks: a baker's note, a cake gallery, an order ribbon with a call to action, and a seasonal specials board.
- A demo site that installs when you activate the theme: a home page, an about page, a short blog, and a contact page whose form emails your admins.
- Every core block restyled to match, plus a themed login screen and a themed 404 page.
## Install
Install from the BlockNinja registry, then pick a preset and assign your fonts in the admin. Activate the demo content if you want a fully built starting point.

View File

@ -1,45 +1,45 @@
# Recommended fonts — Pastel Dream
This pass ships `fonts.json = []` per the wave-1 fonts policy (see
`themes/docs/FONTS.md`). The visual identity is realised through CSS variable
fallback stacks (`--font-heading`, `--font-body`, `--font-mono`) so the theme
renders sensibly out of the box.
This theme **bundles** its typographic identity as woff2 files in
`assets/fonts/` (see `fonts.json`). Both families are licensed under the SIL
Open Font License 1.1 (`assets/fonts/OFL.txt`). No admin setup is needed for
the intended look.
To match the intended Pastel Dream aesthetic, an admin should open the
typography panel and add the fonts below from the Google Fonts tab, then
assign each to its slot. All three are in the curated Google Fonts list and
require no upload.
## Bundled (source = template)
## Display / hand-lettered (assign to Heading)
| Slot | Family | Weights | Role |
|------|--------|---------|------|
| Heading | **Fraunces** | 400, 600 | Sweet, soft-serif display for headings, drop-caps, and signatures |
| Body | **Quicksand** | 400, 700 | Rounded geometric sans for body copy, menus, and accents |
- `google:Caveat Brush` — primary display face for H1 and H2. Hand-lettered,
warm, and rounded. Picks up beautifully behind the brush-stroke underline.
- Alternates (pick one): `google:Sacramento`, `google:Homemade Apple`.
## Body (assign to Body)
- `google:Nunito` — softened sans, ships with weights 300, 400, 600, 700.
Used at line-height 1.75 per the spec.
- Alternate: `google:Quicksand` for an even softer, more rounded body.
## Mono / numerals (assign to Mono)
- `google:JetBrains Mono` — used for eyebrow labels, small caps tracking, and
date strings.
- Alternate: `google:Fira Code`.
## How to assign
Assign in the typography panel:
1. Open `/admin/system/typography` (or your instance's typography panel).
2. Open the Google Fonts tab.
3. Search for each family above and click "Add".
4. In the Heading / Body / Mono slots, pick the family you added.
5. Save. The CMS injects an `@import` link and re-renders.
2. In the **Heading** slot pick **Fraunces**; in the **Body** slot pick
**Quicksand**. Leave **Mono** on the system default, or pick a mono if you
use price/number strings heavily.
3. Save. The CMS emits the `@font-face` rules from the bundled files and
re-renders.
## Why no bundled woff2 in this pass
If no fonts are assigned, the theme still renders correctly through the CSS
variable fallback stacks (`--font-heading` → Fraunces/Playfair/serif;
`--font-body` → Quicksand/Nunito/system sans; `--font-mono` → JetBrains
Mono/system mono). The theme never hardcodes a `font-family` — every family
resolves through `var(--font-heading|body|mono, <fallback>)` so the admin font
picker keeps working.
Wave-1 implementation policy ships every theme with `fonts.json = []` and
relies on the CSS variable consumer pattern. A future wave will bundle
distinctive commercial display faces (or self-host the OFL fonts above) for
themes where the typographic identity is core. See
`themes/docs/FONTS.md` for the full rationale.
## Alternates from Google Fonts
If you prefer a different feel, these all suit the bakery persona and are in
the curated Google Fonts list:
- Heading: `google:Comfortaa`, `google:Pacifico` (script), `google:Playfair Display`.
- Body: `google:Nunito`, `google:Comfortaa`, `google:Varela Round`.
- Mono: `google:JetBrains Mono`, `google:Fira Code`.
## Licensing
- **Fraunces** — Copyright 2018 The Fraunces Project Authors. OFL-1.1.
- **Quicksand** — Copyright 2011 The Quicksand Project Authors. OFL-1.1.
Full text and copyright notices in `assets/fonts/OFL.txt`.

29
RECOMMENDED_ICONS.md Normal file
View File

@ -0,0 +1,29 @@
# Recommended icons — Pastel Dream
This theme renders icons through the CMS sprite system only
(`<svg><use href="/icons/<pack>.svg#<name>"/></svg>`). It declares its packs in
`plugin.mod` `required_icon_packs`. Until the loader auto-installs declared
packs, install them by hand before activation.
## Required packs
| Pack | Used by |
|------|---------|
| **lucide** (bundled) | Navigation, hero and CTA arrows, contact and hours icons, timeline markers, gallery and card chrome. This is the primary pack and is bundled with the CMS. |
| **simple-icons** (bundled) | Brand marks in the footer and social-links override (Instagram, Facebook, etc.). |
Both packs ship bundled with the CMS, so in most instances nothing needs to be
installed. If a fresh instance is missing one:
1. Open `/admin/icons`.
2. Find **Lucide** and **Simple Icons** and click **Install** / **Enable**.
3. Save. Templates that reference `lucide:*` and `simple-icons:*` now resolve.
## Notes
- Block content stores icon references as `"pack:name"` strings and the
templates split them at render time. Do not inline raw SVG.
- If an icon name is missing from a pack, the sprite `<use>` simply renders
nothing rather than breaking layout. Prefer the bundled Lucide names already
used across the overrides (`arrow-right`, `map-pin`, `navigation`,
`croissant`, `menu`, `x`, `chevron-down`).

View File

@ -1,187 +0,0 @@
/* Pastel Dream theme-specific CSS appended to host Tailwind input.
*
* Rules:
* - All colors must consume tokens via hsl(var(--token)). No hardcoded hex/rgb/named colors.
* - All font-family declarations must consume var(--font-*) with a graceful fallback stack.
* - Animations must be guarded by prefers-reduced-motion: reduce.
*/
:root {
--radius-soft: 20px;
}
/* --- Watercolor utility classes ---------------------------------------- */
/* Soft radial wash backgrounds, tinted from the active palette via primary
* and accent tokens. Used as decorative layers behind hero and footer.
*/
.bg-watercolor-blush {
background-image:
radial-gradient(60% 70% at 20% 30%, hsl(var(--primary) / 0.18) 0%, transparent 65%),
radial-gradient(50% 60% at 80% 70%, hsl(var(--accent) / 0.15) 0%, transparent 60%);
}
.bg-watercolor-mint {
background-image:
radial-gradient(60% 70% at 25% 35%, hsl(var(--secondary) / 0.45) 0%, transparent 65%),
radial-gradient(55% 65% at 75% 65%, hsl(var(--primary) / 0.12) 0%, transparent 60%);
}
/* --- Soft shadow utilities -------------------------------------------- */
/* Shadows are large, low-alpha, and tinted toward primary rather than grey. */
.shadow-soft {
box-shadow: 0 12px 40px -8px hsl(var(--primary) / 0.18),
0 4px 12px -2px hsl(var(--primary) / 0.10);
}
.shadow-soft-hover {
box-shadow: 0 16px 48px -8px hsl(var(--primary) / 0.24),
0 6px 16px -2px hsl(var(--primary) / 0.14);
}
/* --- Deckle edge ------------------------------------------------------- */
/* Hand-torn paper effect for testimonial cards. Uses mask-image so it
* renders against whatever background lies beneath.
*/
.deckle-edge {
-webkit-mask-image:
radial-gradient(circle at 6px 6px, transparent 4px, hsl(var(--card)) 4.5px),
linear-gradient(to bottom, hsl(var(--card)) 0%, hsl(var(--card)) 100%);
mask-image:
radial-gradient(circle at 6px 6px, transparent 4px, hsl(var(--card)) 4.5px),
linear-gradient(to bottom, hsl(var(--card)) 0%, hsl(var(--card)) 100%);
-webkit-mask-composite: source-over;
mask-composite: add;
border: 1px dashed hsl(var(--border));
}
/* --- Pill button override --------------------------------------------- */
/* Spec §"Block overrides": every button is pill (9999px), tinted shadow,
* 2px hover lift, easing cubic-bezier(.22,1,.36,1).
*/
.pastel-pill {
border-radius: 9999px;
padding: 0.75rem 1.75rem;
font-family: var(--font-body, "Nunito", "Quicksand", system-ui, sans-serif);
font-weight: 600;
background-color: hsl(var(--primary));
color: hsl(var(--primary-foreground));
box-shadow: 0 8px 24px -6px hsl(var(--primary) / 0.40);
transition: transform 240ms cubic-bezier(.22,1,.36,1),
box-shadow 240ms cubic-bezier(.22,1,.36,1),
background-color 240ms cubic-bezier(.22,1,.36,1);
border: none;
cursor: pointer;
display: inline-block;
}
.pastel-pill:hover {
transform: translateY(-2px);
box-shadow: 0 12px 32px -6px hsl(var(--primary) / 0.50);
background-color: hsl(var(--primary) / 0.92);
}
.pastel-pill:focus-visible {
outline: 2px solid hsl(var(--ring));
outline-offset: 2px;
}
/* --- Soft card -------------------------------------------------------- */
.pastel-card {
border-radius: var(--radius-soft);
background-color: hsl(var(--card));
color: hsl(var(--card-foreground));
border: 1px solid hsl(var(--border));
box-shadow: 0 12px 40px -8px hsl(var(--primary) / 0.18),
0 4px 12px -2px hsl(var(--primary) / 0.10);
}
/* --- Brush-stroke underline for headings ------------------------------ */
.brush-underline {
position: relative;
display: inline-block;
}
.brush-underline::after {
content: "";
position: absolute;
left: 0;
right: 0;
bottom: -0.2em;
height: 0.22em;
background-color: hsl(var(--accent) / 0.55);
border-radius: 9999px;
z-index: -1;
}
/* --- Typography fallbacks --------------------------------------------- */
/* Spec §"Visual language" pairs Caveat Brush + Nunito; admin can override
* via the font picker. Fallback stacks degrade gracefully.
*/
.font-display {
font-family: var(--font-heading, "Caveat Brush", "Sacramento", "Brush Script MT", cursive);
letter-spacing: 0.01em;
}
.font-body {
font-family: var(--font-body, "Nunito", "Quicksand", "Helvetica Neue", system-ui, sans-serif);
line-height: 1.75;
}
.font-mono {
font-family: var(--font-mono, "JetBrains Mono", "Fira Code", ui-monospace, monospace);
}
/* --- Animations ------------------------------------------------------- */
/* @keyframes pastel-shimmer drift the accent overlay slowly across the
* affirmation strip. Spec §13.10: animation-duration >= 6s.
*/
@keyframes pastel-shimmer {
0% {
background-position: 0% 50%;
opacity: 0.85;
}
50% {
background-position: 100% 50%;
opacity: 1;
}
100% {
background-position: 0% 50%;
opacity: 0.85;
}
}
/* @keyframes breathe gentle scale pulse, used by hero CTA and watercolor
* blobs for an in-and-out breath rhythm.
*/
@keyframes breathe {
0%, 100% {
transform: scale(1);
opacity: 0.85;
}
50% {
transform: scale(1.04);
opacity: 1;
}
}
.animate-pastel-shimmer {
background-size: 200% 200%;
animation: pastel-shimmer 8s ease-in-out infinite;
}
.animate-breathe {
animation: breathe 6s ease-in-out infinite;
}
/* --- Accessibility: respect prefers-reduced-motion -------------------- */
@media (prefers-reduced-motion: reduce) {
.animate-pastel-shimmer,
.animate-breathe,
.pastel-pill {
animation: none !important;
transition: none !important;
}
.pastel-pill:hover {
transform: none;
}
}

109
assets/fonts/OFL.txt Normal file
View File

@ -0,0 +1,109 @@
Bundled fonts — SIL Open Font License 1.1
=========================================
This directory bundles latin-subset woff2 files for two families, both
licensed under the SIL Open Font License, Version 1.1 (OFL-1.1).
Copyright notices
-----------------
Fraunces: Copyright 2018 The Fraunces Project Authors (https://github.com/undercasetype/Fraunces)
Quicksand: Copyright 2011 The Quicksand Project Authors (https://github.com/andrew-paglinawan/QuicksandFamily)
Files
-----
fraunces-latin-400.woff2, fraunces-latin-600.woff2 — Fraunces (sweet display headings)
quicksand-400.woff2, quicksand-700.woff2 — Quicksand (rounded body)
Full license text (SIL OFL 1.1)
-------------------------------
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,5 +0,0 @@
This directory is reserved for bundled woff2 font files.
In this wave-1 implementation pass, `fonts.json = []` and no woff2s are
bundled. See ../../../RECOMMENDED_FONTS.md for the Google Fonts the admin
should add via the typography panel.

View File

@ -1,133 +1,293 @@
/* Pastel Dream theme stylesheet served at /templates/pastel-dream/style.css
/* Pastel Dream theme styles bakery / patisserie.
*
* The bulk of the theme CSS (utility classes, keyframes, --radius-soft) is
* injected into the host Tailwind input via CSSManifest.InputCSSAppend so it
* participates in Tailwind's purge. This file holds anything an admin's
* browser may load directly when rendering a page it currently mirrors the
* injected utilities so the page still gets variables and keyframes even if
* the CSSManifest pipeline is bypassed.
* Dual-mode via the 19 shadcn-style HSL token CSS variables (--background,
* --foreground, --primary, --accent, --border, --muted, --card, ...) consumed
* as hsl(var(--token)). No literal colors anywhere. Font families resolve ONLY
* through the BlockNinja font variables (--font-heading, --font-body,
* --font-mono) with the fallback stacks below never hardcoded, so the admin
* font picker keeps working.
*
* Motion tier: CSS-FIRST. Every animation is a gentle CSS drift and is disabled
* under prefers-reduced-motion. No canvas, no JS showpiece.
*/
:root {
--radius-soft: 20px;
--pd-heading-fallback: "Fraunces", "Playfair Display", Georgia, "Times New Roman", serif;
--pd-body-fallback: "Quicksand", "Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
--pd-hand-fallback: "Quicksand", "Comfortaa", "Segoe Print", cursive;
--pd-mono-fallback: "JetBrains Mono", "Fira Code", Menlo, Consolas, monospace;
--pd-radius: 1.5rem;
}
.bg-watercolor-blush {
/* --- Watercolor wash background ----------------------------------------- */
/* The signature "morning haze": soft radial pastel washes tinted from the
* active palette, drifting slowly. Reads as watercolor on cream in light mode
* and as a dusk-lilac glow in dark mode all from the same tokens. */
body.pd-paper {
background-color: hsl(var(--background));
background-image:
radial-gradient(60% 70% at 20% 30%, hsl(var(--primary) / 0.18) 0%, transparent 65%),
radial-gradient(50% 60% at 80% 70%, hsl(var(--accent) / 0.15) 0%, transparent 60%);
radial-gradient(60% 55% at 15% 12%, hsl(var(--primary) / 0.14) 0%, transparent 60%),
radial-gradient(55% 50% at 85% 20%, hsl(var(--accent) / 0.12) 0%, transparent 58%),
radial-gradient(65% 60% at 75% 85%, hsl(var(--secondary) / 0.40) 0%, transparent 62%),
radial-gradient(50% 50% at 25% 90%, hsl(var(--primary) / 0.08) 0%, transparent 60%);
background-attachment: fixed;
background-repeat: no-repeat;
/* Guard against horizontal scroll on small screens from any off-canvas
* chrome (mobile drawer). `clip` does not create a scroll container, so
* it will not break the sticky navbar the way `hidden` can. */
overflow-x: clip;
}
.bg-watercolor-mint {
background-image:
radial-gradient(60% 70% at 25% 35%, hsl(var(--secondary) / 0.45) 0%, transparent 65%),
radial-gradient(55% 65% at 75% 65%, hsl(var(--primary) / 0.12) 0%, transparent 60%);
/* --- Type roles --------------------------------------------------------- */
.pd-display {
font-family: var(--font-heading, var(--pd-heading-fallback));
font-optical-sizing: auto;
font-feature-settings: "liga" 1, "dlig" 1;
letter-spacing: -0.005em;
}
.pd-body {
font-family: var(--font-body, var(--pd-body-fallback));
line-height: 1.7;
}
.pd-mono {
font-family: var(--font-mono, var(--pd-mono-fallback));
}
/* Sweet accent lettering (badges, captions, section titles). Routed through
* --font-heading with a rounded fallback: unassigned it renders Quicksand; if
* the admin sets a heading font the accents follow it (3-slot model intended). */
.pd-hand {
font-family: var(--font-heading, var(--pd-hand-fallback));
letter-spacing: 0.005em;
line-height: 1.2;
}
.shadow-soft {
box-shadow: 0 12px 40px -8px hsl(var(--primary) / 0.18),
0 4px 12px -2px hsl(var(--primary) / 0.10);
/* --- Brush underline ---------------------------------------------------- */
.pd-doodle-underline {
background-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 12' preserveAspectRatio='none'%3E%3Cpath d='M2 8 Q 22 1 44 6 T 88 6 T 118 6' fill='none' stroke='currentColor' stroke-width='4' stroke-linecap='round' opacity='0.35'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: 0 100%;
background-size: 100% 0.55em;
padding-bottom: 0.16em;
}
.deckle-edge {
border: 1px dashed hsl(var(--border));
/* --- Drop-cap ----------------------------------------------------------- */
.pd-dropcap > p:first-of-type::first-letter {
font-family: var(--font-heading, var(--pd-heading-fallback));
font-size: 3.8em;
line-height: 0.8;
float: left;
padding: 0.06em 0.14em 0 0;
color: hsl(var(--primary));
}
.pastel-pill {
border-radius: 9999px;
padding: 0.75rem 1.75rem;
font-family: var(--font-body, "Nunito", "Quicksand", system-ui, sans-serif);
font-weight: 600;
/* --- Soft pill button (icing-smooth, lifts on hover) -------------------- */
.pd-tag {
position: relative;
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.7rem 1.5rem;
background-color: hsl(var(--primary));
color: hsl(var(--primary-foreground));
box-shadow: 0 8px 24px -6px hsl(var(--primary) / 0.40);
transition: transform 240ms cubic-bezier(.22,1,.36,1),
box-shadow 240ms cubic-bezier(.22,1,.36,1),
background-color 240ms cubic-bezier(.22,1,.36,1);
border: none;
border-radius: 9999px;
font-family: var(--font-body, var(--pd-body-fallback));
font-weight: 600;
letter-spacing: 0.01em;
box-shadow: 0 8px 22px -8px hsl(var(--primary) / 0.45);
transition: transform 220ms cubic-bezier(.22,1,.36,1), box-shadow 220ms cubic-bezier(.22,1,.36,1), background-color 220ms ease;
cursor: pointer;
display: inline-block;
}
.pastel-pill:hover {
.pd-tag:hover {
transform: translateY(-2px);
box-shadow: 0 12px 32px -6px hsl(var(--primary) / 0.50);
box-shadow: 0 14px 30px -8px hsl(var(--primary) / 0.52);
background-color: hsl(var(--primary) / 0.92);
}
.pastel-pill:focus-visible {
.pd-tag:focus-visible {
outline: 2px solid hsl(var(--ring));
outline-offset: 2px;
outline-offset: 3px;
}
.pd-tag--ghost {
background-color: transparent;
color: hsl(var(--primary));
box-shadow: none;
border: 1.5px solid hsl(var(--primary) / 0.45);
}
.pd-tag--ghost:hover {
background-color: hsl(var(--primary) / 0.08);
box-shadow: none;
}
.pastel-card {
border-radius: var(--radius-soft);
/* --- Scalloped edges (piped-icing / doily trim) ------------------------- */
.pd-torn-top {
-webkit-mask-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 12' preserveAspectRatio='none'%3E%3Cpath d='M0 6 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 L120 12 L0 12 Z' fill='black'/%3E%3C/svg%3E");
mask-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 12' preserveAspectRatio='none'%3E%3Cpath d='M0 6 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 L120 12 L0 12 Z' fill='black'/%3E%3C/svg%3E");
-webkit-mask-repeat: no-repeat;
mask-repeat: no-repeat;
-webkit-mask-size: 100% 100%;
mask-size: 100% 100%;
}
.pd-torn-bottom {
-webkit-mask-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 12' preserveAspectRatio='none'%3E%3Cpath d='M0 0 L120 0 L120 6 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 Z' fill='black'/%3E%3C/svg%3E");
mask-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 12' preserveAspectRatio='none'%3E%3Cpath d='M0 0 L120 0 L120 6 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 Z' fill='black'/%3E%3C/svg%3E");
-webkit-mask-repeat: no-repeat;
mask-repeat: no-repeat;
-webkit-mask-size: 100% 100%;
mask-size: 100% 100%;
}
/* --- Surfaces (rounded-everything) ------------------------------------- */
.pd-card {
background-color: hsl(var(--card));
color: hsl(var(--card-foreground));
border: 1px solid hsl(var(--border));
box-shadow: 0 12px 40px -8px hsl(var(--primary) / 0.18),
0 4px 12px -2px hsl(var(--primary) / 0.10);
border-radius: var(--pd-radius);
box-shadow: 0 12px 40px -14px hsl(var(--primary) / 0.22), 0 4px 12px -6px hsl(var(--primary) / 0.10);
}
.pd-frame {
border: 1px solid hsl(var(--border));
background-color: hsl(var(--card));
border-radius: var(--pd-radius);
}
.brush-underline {
/* --- Menu card (soft pastel board for the menu-list override) ---------- */
/* Not an inverted chalkboard: a soft card with a primary wash. Text inherits
* card-foreground so item names and section titles stay legible in both modes. */
.pd-chalkboard {
position: relative;
display: inline-block;
background-color: hsl(var(--card));
background-image:
radial-gradient(70% 60% at 12% 8%, hsl(var(--primary) / 0.10) 0%, transparent 60%),
radial-gradient(60% 55% at 90% 92%, hsl(var(--accent) / 0.08) 0%, transparent 60%);
color: hsl(var(--card-foreground));
border: 1px solid hsl(var(--border));
border-radius: var(--pd-radius);
box-shadow: 0 16px 48px -18px hsl(var(--primary) / 0.28);
}
.pd-chalk-rule {
border-color: hsl(var(--border));
border-style: solid;
}
.pd-chalk-muted {
color: hsl(var(--muted-foreground));
}
.brush-underline::after {
content: "";
position: absolute;
left: 0;
right: 0;
bottom: -0.2em;
height: 0.22em;
background-color: hsl(var(--accent) / 0.55);
/* --- Hours: today highlight -------------------------------------------- */
.pd-hours-today {
background-color: hsl(var(--accent) / 0.16);
color: hsl(var(--accent-foreground));
border-left: 3px solid hsl(var(--accent));
border-radius: 0 9999px 9999px 0;
padding-left: 0.75rem;
}
/* --- Location pin ------------------------------------------------------- */
.pd-pin {
color: hsl(var(--accent));
}
/* --- Soft badge (timeline markers) ------------------------------------- */
.pd-stamp {
display: inline-flex;
align-items: center;
justify-content: center;
width: 4rem;
height: 4rem;
border-radius: 9999px;
z-index: -1;
background-color: hsl(var(--accent));
color: hsl(var(--accent-foreground));
border: 3px solid hsl(var(--card));
box-shadow: 0 8px 20px -8px hsl(var(--accent) / 0.55);
font-family: var(--font-heading, var(--pd-hand-fallback));
font-size: 1.05rem;
text-align: center;
line-height: 1.05;
transition: transform 200ms cubic-bezier(.22,1,.36,1);
}
.pd-stamp:hover {
transform: scale(1.06);
}
.pd-stamp-sm {
width: 2.25rem;
height: 2.25rem;
font-size: 0.95rem;
box-shadow: none;
}
.font-display {
font-family: var(--font-heading, "Caveat Brush", "Sacramento", "Brush Script MT", cursive);
letter-spacing: 0.01em;
/* --- Brush divider draw-in --------------------------------------------- */
.pd-doodle-draw path,
.pd-doodle-draw ellipse,
.pd-doodle-draw circle {
stroke-dasharray: 260;
stroke-dashoffset: 260;
animation: pd-draw 1.6s ease forwards;
}
.font-body {
font-family: var(--font-body, "Nunito", "Quicksand", "Helvetica Neue", system-ui, sans-serif);
line-height: 1.75;
@keyframes pd-draw {
to { stroke-dashoffset: 0; }
}
.font-mono {
font-family: var(--font-mono, "JetBrains Mono", "Fira Code", ui-monospace, monospace);
}
@keyframes pastel-shimmer {
0% { background-position: 0% 50%; opacity: 0.85; }
50% { background-position: 100% 50%; opacity: 1; }
100% { background-position: 0% 50%; opacity: 0.85; }
}
@keyframes breathe {
0%, 100% { transform: scale(1); opacity: 0.85; }
50% { transform: scale(1.04); opacity: 1; }
}
.animate-pastel-shimmer {
background-size: 200% 200%;
animation: pastel-shimmer 8s ease-in-out infinite;
}
.animate-breathe {
animation: breathe 6s ease-in-out infinite;
/* --- Gentle atmosphere: slow wash drift (CSS-first signature) ----------- */
@keyframes pd-breathe {
0%, 100% { transform: scale(1); opacity: 0.9; }
50% { transform: scale(1.05); opacity: 1; }
}
.pd-breathe {
animation: pd-breathe 8s ease-in-out infinite;
}
/* --- Motion safety: static fallback ------------------------------------ */
@media (prefers-reduced-motion: reduce) {
.animate-pastel-shimmer,
.animate-breathe,
.pastel-pill {
animation: none !important;
transition: none !important;
}
.pastel-pill:hover {
transform: none;
.pd-tag,
.pd-stamp { transition: none; }
.pd-tag:hover,
.pd-stamp:hover { transform: none; }
.pd-breathe { animation: none; }
.pd-doodle-draw path,
.pd-doodle-draw ellipse,
.pd-doodle-draw circle {
stroke-dasharray: none;
stroke-dashoffset: 0;
animation: none;
}
}
/* --- Layout helpers (theme-owned, semantic tokens only) -----------------
* Installed .bnp themes live outside the host Tailwind @source globs, so the
* host build does NOT compile grid / aspect-ratio / gap / padding utilities
* that appear only in this theme's own templates. The theme therefore owns
* those layout primitives here as plain CSS: mobile-first, collapsing to a
* single column on small screens, with explicit media queries so the render
* is reliable regardless of the JIT content scan. No colors layout only. */
.pd-section { padding-top: 2.75rem; padding-bottom: 2.75rem; }
@media (min-width: 768px) {
.pd-section { padding-top: 4rem; padding-bottom: 4rem; }
}
.pd-grid { display: grid; grid-template-columns: minmax(0, 1fr); gap: 2rem; }
@media (min-width: 640px) {
.pd-grid--2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (min-width: 1024px) {
.pd-grid--lg-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.pd-grid--3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.pd-grid--lg-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}
/* Two-up menu board: one column on phones, two side by side from md. */
.pd-menu-cols { display: grid; grid-template-columns: minmax(0, 1fr); gap: 2.5rem 3rem; }
@media (min-width: 768px) {
.pd-menu-cols--2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
.pd-menu-list { margin-top: 1rem; }
.pd-menu-list > li + li { margin-top: 1.25rem; }
/* Media frames: a fixed aspect box so a photo fills gracefully. The EMPTY
* state is omitted in-template (no ballooning boxes when no photo is set). */
.pd-media { position: relative; width: 100%; overflow: hidden; aspect-ratio: 4 / 3; }
.pd-media--square { aspect-ratio: 1 / 1; }
.pd-media img { display: block; width: 100%; height: 100%; object-fit: cover; }
/* Card body padding (host may not emit p-5 for theme templates). */
.pd-card-body { padding: 1.25rem 1.25rem 1.5rem; }
@media (min-width: 768px) { .pd-card-body { padding: 1.5rem; } }

View File

@ -1,10 +0,0 @@
<section class="relative py-16 overflow-hidden animate-pastel-shimmer" style="background-image: linear-gradient(90deg, hsl(var(--background)) 0%, {% if palette == "mint" %}hsl(var(--secondary) / 0.55){% elif palette == "butter" %}hsl(var(--accent) / 0.55){% elif palette == "sky" %}hsl(var(--secondary) / 0.40){% else %}hsl(var(--primary) / 0.35){% endif %} 50%, hsl(var(--background)) 100%);" data-block="pastel-dream:affirmation" data-palette="{% if palette == "mint" or palette == "butter" or palette == "sky" %}{{ palette }}{% else %}blush{% endif %}">
<div class="relative z-10 max-w-3xl mx-auto px-6 text-center">
<p class="font-display text-3xl md:text-4xl leading-snug" style="color: hsl(var(--foreground));">
&ldquo;{{ quote|default:"You are doing enough." }}&rdquo;
</p>
{% if author %}<p class="font-body mt-4 text-sm uppercase tracking-[0.2em]" style="color: hsl(var(--muted-foreground));">
{{ author }}
</p>{% endif %}
</div>
</section>

View File

@ -1,29 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Affirmation Strip",
"description": "Slow-shimmer band carrying a short affirmation in display type.",
"type": "object",
"properties": {
"quote": {
"type": "string",
"title": "Affirmation",
"description": "Short calming line (one or two sentences).",
"default": "You are doing enough.",
"x-editor": "textarea"
},
"author": {
"type": "string",
"title": "Attribution",
"description": "Optional source for the line.",
"x-editor": "text"
},
"palette": {
"type": "string",
"title": "Palette",
"description": "Which watercolor wash to use behind the strip.",
"default": "blush",
"enum": ["blush", "mint", "butter", "sky"],
"x-editor": "select"
}
}
}

View File

@ -0,0 +1,14 @@
<section data-block="pastel-dream:bakers_note" class="pd-section bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div class="mx-auto max-w-3xl px-4">
<div class="pd-card relative overflow-hidden p-8 text-center md:p-12">
<div class="pointer-events-none absolute -right-16 -top-16 h-48 w-48 rounded-full bg-accent/15 pd-breathe" aria-hidden="true"></div>
<div class="pointer-events-none absolute -bottom-20 -left-12 h-56 w-56 rounded-full bg-primary/10 pd-breathe" aria-hidden="true"></div>
<div class="relative">
{% if kicker %}<p class="pd-hand text-2xl text-accent">{{ kicker }}</p>{% endif %}
{% if body %}<div class="pd-body mx-auto mt-4 max-w-xl text-lg text-foreground [&_strong]:text-primary">{{ body|safe }}</div>{% endif %}
{% if signatureName %}<p class="pd-display mt-6 text-2xl text-primary">{{ signatureName }}</p>{% endif %}
{% if signatureRole %}<p class="pd-body text-sm text-muted-foreground">{{ signatureRole }}</p>{% endif %}
</div>
</div>
</div>
</section>

View File

@ -0,0 +1,30 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Baker's Note",
"description": "A signed note from the baker in sweet display type over a watercolor blob",
"type": "object",
"properties": {
"kicker": {
"type": "string",
"title": "Kicker",
"default": "A note from the kitchen",
"x-editor": "text"
},
"body": {
"type": "string",
"title": "Note",
"description": "Rich text note",
"x-editor": "richtext"
},
"signatureName": {
"type": "string",
"title": "Signature Name",
"x-editor": "text"
},
"signatureRole": {
"type": "string",
"title": "Signature Role",
"x-editor": "text"
}
}
}

View File

@ -1,42 +1,33 @@
# Pastel Dream theme block definitions (codeless .bnp, WO-WZ-021).
# Keys are fully qualified (pastel-dream:<key>) because the codeless loader
# registers definitions verbatim — there is no PluginBlockRegistry prefixing
# in the codeless path (unlike the wasm/templ path). The qualified keys match
# the hardcoded data-block attributes and the master_pages.json references.
# Pastel Dream theme block definitions (codeless .bnp, WO-TF-023).
# Only true persona furniture stays a theme block. Everything that duplicated a
# builtin (navbar, hero, quote, testimonial, feature-grid, footer, gallery,
# menu, divider ...) is now a template override of the builtin key under
# templates/overrides/pastel-dream/ — not a block here.
#
# Keys are fully qualified (pastel-dream:<key>): the codeless loader registers
# definitions verbatim (no PluginBlockRegistry prefixing in the codeless path).
blocks:
- key: pastel-dream:soft-navbar
title: Soft Navbar
description: Rounded pill navigation with a watercolor blob behind the logo.
category: navigation
schema: soft-navbar.schema.json
template: soft-navbar.ninjatpl
- key: pastel-dream:watercolor-hero
title: Watercolor Hero
description: Hero with two-tone watercolor blobs layered behind soft display type.
category: theme
schema: watercolor-hero.schema.json
template: watercolor-hero.ninjatpl
- key: pastel-dream:affirmation
title: Affirmation Strip
description: Slow-shimmer band carrying a short calming line in display type.
category: theme
schema: affirmation.schema.json
template: affirmation.ninjatpl
- key: pastel-dream:testimonial-soft
title: Soft Testimonial
description: Quote card with a deckle paper edge, avatar, and rating.
category: theme
schema: testimonial-soft.schema.json
template: testimonial-soft.ninjatpl
- key: pastel-dream:feature-grid-soft
title: Feature Trio
description: Three-up card grid for services, offerings, or core values.
category: layout
schema: feature-grid-soft.schema.json
template: feature-grid-soft.ninjatpl
- key: pastel-dream:cozy-footer
title: Cozy Footer
description: Newsletter signup, closing affirmation, menu column, and social pills.
category: navigation
schema: cozy-footer.schema.json
template: cozy-footer.ninjatpl
- key: pastel-dream:seasonal_specials
title: Seasonal Specials
description: This season's featured bakes in a soft card grid — name, note, price and a sweet badge.
category: content
schema: seasonal_specials.schema.json
template: seasonal_specials.ninjatpl
- key: pastel-dream:cake_gallery
title: Cake Gallery
description: A scalloped showcase of cakes with flavour, price and an order link per cake.
category: content
schema: cake_gallery.schema.json
template: cake_gallery.ninjatpl
- key: pastel-dream:order_ribbon
title: Order Ribbon
description: A rounded call-to-order ribbon with a pickup note and a soft pill button.
category: content
schema: order_ribbon.schema.json
template: order_ribbon.ninjatpl
- key: pastel-dream:bakers_note
title: Baker's Note
description: A signed note from the baker in sweet display type over a watercolor blob.
category: content
schema: bakers_note.schema.json
template: bakers_note.ninjatpl

View File

@ -0,0 +1,27 @@
<section data-block="pastel-dream:cake_gallery" class="pd-section bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div class="mx-auto max-w-6xl px-4">
<div class="text-center">
{% if heading %}<h2 class="pd-display text-3xl text-primary md:text-4xl"><span class="pd-doodle-underline">{{ heading }}</span></h2>{% endif %}
{% if intro %}<p class="pd-body mx-auto mt-3 max-w-2xl text-lg text-muted-foreground">{{ intro }}</p>{% endif %}
</div>
{% if cakes %}
<div class="mt-10 pd-grid pd-grid--2 pd-grid--3">
{% for cake in cakes %}
<figure class="pd-frame overflow-hidden flex flex-col">
{% if cake.image %}<div class="pd-media pd-media--square pd-torn-bottom bg-secondary">{% img cake.image alt=cake.name|default:"Cake" class="h-full w-full object-cover" %}</div>{% endif %}
<figcaption class="pd-card-body">
<div class="flex items-start justify-between gap-3">
<div>
{% if cake.name %}<h3 class="pd-display text-xl text-primary">{{ cake.name }}</h3>{% endif %}
{% if cake.flavour %}<p class="pd-hand text-base text-accent">{{ cake.flavour }}</p>{% endif %}
</div>
{% if cake.price %}<span class="pd-mono shrink-0 text-lg text-accent tabular-nums">{% if currency %}{{ currency }}{% endif %}{{ cake.price }}</span>{% endif %}
</div>
{% if cake.orderUrl %}<a href="{{ cake.orderUrl }}" class="pd-tag pd-tag--ghost mt-4 text-sm">Order this cake<svg class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#arrow-right"></use></svg></a>{% endif %}
</figcaption>
</figure>
{% endfor %}
</div>
{% endif %}
</div>
</section>

View File

@ -0,0 +1,39 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Cake Gallery",
"description": "A scalloped showcase of cakes with flavour, price and an order link per cake",
"type": "object",
"properties": {
"heading": {
"type": "string",
"title": "Heading",
"x-editor": "text"
},
"intro": {
"type": "string",
"title": "Intro",
"x-editor": "textarea"
},
"currency": {
"type": "string",
"title": "Currency Symbol",
"default": "$",
"x-editor": "text"
},
"cakes": {
"type": "array",
"title": "Cakes",
"x-editor": "collection",
"items": {
"type": "object",
"properties": {
"image": { "type": "string", "title": "Image", "x-editor": "media" },
"name": { "type": "string", "title": "Name", "x-editor": "text" },
"flavour": { "type": "string", "title": "Flavour", "x-editor": "text" },
"price": { "type": "string", "title": "Price", "x-editor": "text" },
"orderUrl": { "type": "string", "title": "Order Link", "x-editor": "text" }
}
}
}
}
}

View File

@ -1,23 +0,0 @@
<section class="relative py-16 md:py-20 overflow-hidden bg-watercolor-mint" data-block="pastel-dream:cozy-footer">
<svg class="absolute -top-8 -right-8 w-64 h-64 -z-0" viewBox="0 0 300 300" aria-hidden="true">
<path d="M150,30 C220,30 270,90 270,150 C270,220 200,270 140,270 C80,270 30,210 30,150 C30,90 80,30 150,30 Z" fill="hsl(var(--primary) / 0.16)"></path>
</svg>
<div class="relative z-10 max-w-4xl mx-auto px-4 text-center">
<p class="font-display text-3xl mb-6" style="color: hsl(var(--foreground));">
{{ affirmation|default:"Be gentle with yourself today." }}
</p>
{% if showSignup != "no" and showSignup != "false" and showSignup != "0" and showSignup != "off" and showSignup != false %}<form class="flex flex-col sm:flex-row gap-3 max-w-md mx-auto mb-8" data-pastel-signup="true" onsubmit="event.preventDefault();">
<label class="sr-only" for="pastel-newsletter-email">Email</label>
<input id="pastel-newsletter-email" type="email" placeholder="you@calm.day" class="flex-1 px-5 py-3 font-body rounded-full" style="background-color: hsl(var(--input)); color: hsl(var(--foreground)); border: 1px solid hsl(var(--border));">
<button type="submit" class="pastel-pill">Stay in touch</button>
</form>{% endif %}
{% if social %}<div class="flex justify-center gap-3 flex-wrap mt-6">
{% for href in social %}<a href="{{ href }}" class="px-4 py-2 font-body text-xs uppercase tracking-[0.2em] rounded-full" style="background-color: hsl(var(--card)); color: hsl(var(--card-foreground)); border: 1px solid hsl(var(--border));">
Visit
</a>{% endfor %}
</div>{% endif %}
<p class="font-mono text-xs mt-10" style="color: hsl(var(--muted-foreground));">
Pastel Dream &middot; with care
</p>
</div>
</section>

View File

@ -1,39 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Cozy Footer",
"description": "Newsletter signup, small affirmation line, menu, and social links.",
"type": "object",
"properties": {
"showSignup": {
"type": "string",
"title": "Newsletter signup",
"description": "Toggle the inline email signup form.",
"default": "yes",
"enum": ["yes", "no"],
"x-editor": "select"
},
"affirmation": {
"type": "string",
"title": "Closing line",
"description": "Short closing affirmation displayed beneath the signup.",
"default": "Be gentle with yourself today.",
"x-editor": "textarea"
},
"menuName": {
"type": "string",
"title": "Menu",
"description": "Name of the menu rendered in the footer column.",
"x-editor": "menu-select"
},
"social": {
"type": "array",
"title": "Social links",
"description": "Outbound social links shown as small pills.",
"x-editor": "array",
"items": {
"type": "string",
"x-editor": "link"
}
}
}
}

View File

@ -1,18 +0,0 @@
<section class="py-16 md:py-20" data-block="pastel-dream:feature-grid-soft">
<div class="max-w-6xl mx-auto px-4">
{% if intro %}<div class="font-body text-center max-w-2xl mx-auto mb-12 text-lg" style="color: hsl(var(--foreground) / 0.78);">
{{ intro|safe }}
</div>{% endif %}
{% if items %}<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{% for item in items %}<article class="pastel-card p-8 transition-transform" style="transition-timing-function: cubic-bezier(.22,1,.36,1); transition-duration: 240ms;">
{% if item.icon %}<div class="mb-4">
<img src="{{ item.icon }}" alt="" class="w-12 h-12 object-contain">
</div>{% else %}<div class="mb-4 w-12 h-12 rounded-full" style="background-color: hsl(var(--accent) / 0.5);" aria-hidden="true"></div>{% endif %}
{% if item.title %}<h3 class="font-display text-2xl mb-3" style="color: hsl(var(--foreground));">{{ item.title }}</h3>{% endif %}
{% if item.body %}<p class="font-body text-base" style="color: hsl(var(--foreground) / 0.78); line-height: 1.75;">{{ item.body }}</p>{% endif %}
</article>{% endfor %}
</div>{% else %}<div class="text-center font-body text-base" style="color: hsl(var(--muted-foreground));">
<p>Add up to three cards to the trio.</p>
</div>{% endif %}
</div>
</section>

View File

@ -1,42 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Feature Trio",
"description": "Three-up card grid for services, offerings, or values.",
"type": "object",
"properties": {
"intro": {
"type": "string",
"title": "Intro",
"description": "Optional introduction above the cards.",
"x-editor": "richtext"
},
"items": {
"type": "array",
"title": "Cards",
"description": "List of feature cards. Three is the recommended count.",
"x-editor": "collection",
"items": {
"type": "object",
"properties": {
"icon": {
"type": "string",
"title": "Icon",
"description": "Decorative icon or small illustration.",
"x-editor": "media"
},
"title": {
"type": "string",
"title": "Title",
"x-editor": "text"
},
"body": {
"type": "string",
"title": "Body",
"x-editor": "textarea"
}
},
"required": ["title"]
}
}
}
}

View File

@ -0,0 +1,18 @@
<section data-block="pastel-dream:order_ribbon" class="pd-section bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div class="mx-auto max-w-5xl px-4">
<div class="pd-chalkboard relative overflow-hidden p-8 md:p-12">
<div class="relative flex flex-col items-start gap-6 md:flex-row md:items-center md:justify-between">
<div class="max-w-2xl">
{% if heading %}<h2 class="pd-display text-3xl text-primary md:text-4xl">{{ heading }}</h2>{% endif %}
{% if message %}<p class="pd-body mt-3 text-lg text-muted-foreground">{{ message }}</p>{% endif %}
{% if pickupNote %}<p class="pd-hand mt-3 text-xl text-accent">{{ pickupNote }}</p>{% endif %}
</div>
{% if ctaLabel %}
<div class="shrink-0">
<a href="{{ ctaUrl|default:"/contact" }}" class="pd-tag text-base">{{ ctaLabel }}<svg class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#arrow-right"></use></svg></a>
</div>
{% endif %}
</div>
</div>
</div>
</section>

View File

@ -0,0 +1,37 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Order Ribbon",
"description": "A rounded call-to-order ribbon with a pickup note and a soft pill button",
"type": "object",
"properties": {
"heading": {
"type": "string",
"title": "Heading",
"default": "Order for the weekend",
"x-editor": "text"
},
"message": {
"type": "string",
"title": "Message",
"x-editor": "textarea"
},
"pickupNote": {
"type": "string",
"title": "Pickup Note",
"description": "Small line about lead time or pickup",
"x-editor": "text"
},
"ctaLabel": {
"type": "string",
"title": "Button Label",
"default": "Place an order",
"x-editor": "text"
},
"ctaUrl": {
"type": "string",
"title": "Button Link",
"default": "/contact",
"x-editor": "text"
}
}
}

View File

@ -0,0 +1,26 @@
<section data-block="pastel-dream:seasonal_specials" class="pd-section bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div class="mx-auto max-w-6xl px-4">
<div class="max-w-2xl">
{% if kicker %}<p class="pd-hand text-xl text-accent">{{ kicker }}</p>{% endif %}
{% if heading %}<h2 class="pd-display mt-1 text-3xl text-primary md:text-4xl"><span class="pd-doodle-underline">{{ heading }}</span></h2>{% endif %}
{% if intro %}<p class="pd-body mt-3 text-lg text-muted-foreground">{{ intro }}</p>{% endif %}
</div>
{% if items %}
<div class="mt-10 pd-grid pd-grid--2 pd-grid--3">
{% for item in items %}
<article class="pd-card overflow-hidden flex flex-col">
{% if item.image %}<div class="pd-media bg-secondary">{% img item.image alt=item.name|default:"Seasonal special" class="h-full w-full object-cover" %}</div>{% endif %}
<div class="pd-card-body">
<div class="flex items-start justify-between gap-3">
<h3 class="pd-display text-xl text-primary">{{ item.name }}</h3>
{% if item.price %}<span class="pd-mono shrink-0 text-lg text-accent tabular-nums">{% if currency %}{{ currency }}{% endif %}{{ item.price }}</span>{% endif %}
</div>
{% if item.badge %}<span class="pd-hand mt-2 inline-flex rounded-full bg-accent/15 px-3 py-0.5 text-sm text-accent">{{ item.badge }}</span>{% endif %}
{% if item.note %}<p class="pd-body mt-3 text-sm text-muted-foreground">{{ item.note }}</p>{% endif %}
</div>
</article>
{% endfor %}
</div>
{% endif %}
</div>
</section>

View File

@ -0,0 +1,46 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Seasonal Specials",
"description": "This season's featured bakes in a soft card grid",
"type": "object",
"properties": {
"kicker": {
"type": "string",
"title": "Kicker",
"description": "Small sweet line above the heading",
"default": "This season",
"x-editor": "text"
},
"heading": {
"type": "string",
"title": "Heading",
"x-editor": "text"
},
"intro": {
"type": "string",
"title": "Intro",
"x-editor": "textarea"
},
"currency": {
"type": "string",
"title": "Currency Symbol",
"default": "$",
"x-editor": "text"
},
"items": {
"type": "array",
"title": "Specials",
"x-editor": "collection",
"items": {
"type": "object",
"properties": {
"image": { "type": "string", "title": "Image", "x-editor": "media" },
"name": { "type": "string", "title": "Name", "x-editor": "text" },
"note": { "type": "string", "title": "Note", "x-editor": "textarea" },
"price": { "type": "string", "title": "Price", "x-editor": "text" },
"badge": { "type": "string", "title": "Badge", "x-editor": "text" }
}
}
}
}
}

View File

@ -1,23 +0,0 @@
<nav class="w-full px-4 py-4" data-block="pastel-dream:soft-navbar" data-menu="{{ menuName }}">
<div class="max-w-6xl mx-auto flex items-center justify-between gap-4 px-2 py-2 rounded-full bg-watercolor-blush" style="border-radius: 9999px;">
<a href="/" class="font-display text-2xl tracking-tight px-4 py-2 rounded-full" style="color: hsl(var(--primary-foreground)); background-color: hsl(var(--primary) / 0.15);">
{{ logo|default:"Pastel Dream" }}
</a>
<div class="hidden md:flex items-center gap-6 font-body text-sm" data-pastel-menu="{{ menuName }}">
<a href="/" style="color: hsl(var(--foreground));" class="hover:opacity-70 transition-opacity">Home</a>
<a href="/about" style="color: hsl(var(--foreground));" class="hover:opacity-70 transition-opacity">About</a>
<a href="/journal" style="color: hsl(var(--foreground));" class="hover:opacity-70 transition-opacity">Journal</a>
<a href="/contact" style="color: hsl(var(--foreground));" class="hover:opacity-70 transition-opacity">Contact</a>
</div>
{% if ctaText %}<a href="{{ ctaHref|default:"#" }}" class="pastel-pill text-sm">
{{ ctaText }}
</a>{% endif %}
<button class="md:hidden p-2 rounded-full" aria-label="Open menu" style="color: hsl(var(--foreground));">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="3" y1="6" x2="21" y2="6"></line>
<line x1="3" y1="12" x2="21" y2="12"></line>
<line x1="3" y1="18" x2="21" y2="18"></line>
</svg>
</button>
</div>
</nav>

View File

@ -1,34 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Soft Navbar",
"description": "Rounded pill nav with a watercolor blob behind the logo wordmark.",
"type": "object",
"properties": {
"logo": {
"type": "string",
"title": "Logo Wordmark",
"description": "Plain-text wordmark displayed at the top-left.",
"default": "Pastel Dream",
"x-editor": "text"
},
"menuName": {
"type": "string",
"title": "Menu",
"description": "Name of the menu to render in the pill.",
"default": "main",
"x-editor": "menu-select"
},
"ctaText": {
"type": "string",
"title": "CTA Label",
"description": "Optional pill button to the right of the menu.",
"x-editor": "text"
},
"ctaHref": {
"type": "string",
"title": "CTA Link",
"description": "Target URL or internal page link for the CTA pill.",
"x-editor": "link"
}
}
}

View File

@ -1,27 +0,0 @@
<figure class="pastel-card deckle-edge p-8 max-w-xl mx-auto" data-block="pastel-dream:testimonial-soft">
{% if rating|integer > 0 %}<div class="flex gap-1 mb-4" aria-label="Rating">
{% if rating|integer >= 1 %}<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color: hsl(var(--accent));">
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon>
</svg>{% endif %}{% if rating|integer >= 2 %}<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color: hsl(var(--accent));">
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon>
</svg>{% endif %}{% if rating|integer >= 3 %}<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color: hsl(var(--accent));">
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon>
</svg>{% endif %}{% if rating|integer >= 4 %}<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color: hsl(var(--accent));">
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon>
</svg>{% endif %}{% if rating|integer >= 5 %}<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color: hsl(var(--accent));">
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon>
</svg>{% endif %}
</div>{% endif %}
<blockquote class="font-body text-lg leading-relaxed" style="color: hsl(var(--card-foreground));">
{{ quote|safe }}
</blockquote>
{% if name or role %}<figcaption class="flex items-center gap-4 mt-6 pt-6 border-t" style="border-color: hsl(var(--border));">
{% if avatar %}<img src="{{ avatar }}" alt="{{ name }}" class="w-12 h-12 rounded-full object-cover">{% elif name %}<span class="w-12 h-12 rounded-full flex items-center justify-center font-display text-lg" style="background-color: hsl(var(--secondary)); color: hsl(var(--secondary-foreground));">
{% for w in name|split:" " %}{% if w and forloop.Counter <= 2 %}{{ w|first|upper }}{% endif %}{% endfor %}
</span>{% endif %}
<div>
{% if name %}<p class="font-display text-xl leading-tight" style="color: hsl(var(--foreground));">{{ name }}</p>{% endif %}
{% if role %}<p class="font-body text-sm" style="color: hsl(var(--muted-foreground));">{{ role }}</p>{% endif %}
</div>
</figcaption>{% endif %}
</figure>

View File

@ -1,40 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Soft Testimonial",
"description": "Deckle-edge card carrying a quote, attribution, optional avatar, and rating.",
"type": "object",
"properties": {
"quote": {
"type": "string",
"title": "Quote",
"description": "The testimonial body. Rich text.",
"x-editor": "richtext"
},
"name": {
"type": "string",
"title": "Name",
"description": "Person being quoted.",
"x-editor": "text"
},
"role": {
"type": "string",
"title": "Role",
"description": "Their title, company, or context.",
"x-editor": "text"
},
"avatar": {
"type": "string",
"title": "Avatar",
"description": "Optional portrait or illustration.",
"x-editor": "media"
},
"rating": {
"type": "number",
"title": "Rating",
"description": "Star rating, 0 to 5.",
"minimum": 0,
"maximum": 5,
"x-editor": "number"
}
}
}

View File

@ -1,35 +0,0 @@
<section class="relative overflow-hidden py-20 md:py-28 bg-watercolor-blush" data-block="pastel-dream:watercolor-hero">
<svg class="absolute -top-12 -left-12 w-[28rem] h-[28rem] -z-0 animate-breathe" viewBox="0 0 400 400" aria-hidden="true">
<path d="M200,40 C290,40 360,120 360,200 C360,290 280,360 200,360 C110,360 40,280 40,200 C40,110 110,40 200,40 Z" fill="hsl(var(--primary) / 0.22)"></path>
</svg>
<svg class="absolute -bottom-16 -right-8 w-[24rem] h-[24rem] -z-0 animate-breathe" viewBox="0 0 400 400" aria-hidden="true">
<path d="M120,60 C200,40 320,80 360,180 C400,280 320,360 220,360 C120,360 40,300 40,220 C40,160 60,80 120,60 Z" fill="hsl(var(--accent) / 0.30)"></path>
</svg>
<div class="relative z-10 max-w-6xl mx-auto px-4 grid md:grid-cols-2 gap-12 items-center">
<div>
{% if eyebrow %}<p class="font-mono text-xs uppercase tracking-[0.2em] mb-4" style="color: hsl(var(--muted-foreground));">
{{ eyebrow }}
</p>{% endif %}
<h1 class="font-display brush-underline text-5xl md:text-6xl leading-tight mb-6" style="color: hsl(var(--foreground));">
{{ headline|default:"Soft starts"|safe }}
</h1>
{% if body %}<div class="font-body text-lg max-w-prose mb-8" style="color: hsl(var(--foreground) / 0.78);">
{{ body|safe }}
</div>{% endif %}
{% if ctaText %}<a href="{{ ctaHref|default:"#" }}" class="pastel-pill text-base animate-breathe">
{{ ctaText }}
</a>{% endif %}
</div>
{% if image %}<div class="relative">
<div class="pastel-card overflow-hidden">
<img src="{{ image }}" alt="" class="w-full h-auto block">
</div>
</div>{% else %}<div class="relative h-64 md:h-80" aria-hidden="true">
<svg viewBox="0 0 300 240" class="w-full h-full">
<ellipse cx="150" cy="120" rx="110" ry="80" fill="hsl(var(--secondary) / 0.55)"></ellipse>
<ellipse cx="120" cy="100" rx="70" ry="50" fill="hsl(var(--accent) / 0.45)"></ellipse>
<ellipse cx="180" cy="140" rx="60" ry="44" fill="hsl(var(--primary) / 0.40)"></ellipse>
</svg>
</div>{% endif %}
</div>
</section>

View File

@ -1,43 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Watercolor Hero",
"description": "Hero with two-tone SVG blobs layered behind a soft headline and CTA.",
"type": "object",
"properties": {
"eyebrow": {
"type": "string",
"title": "Eyebrow",
"description": "Small label above the headline (e.g. 'new', 'autumn collection').",
"x-editor": "text"
},
"headline": {
"type": "string",
"title": "Headline",
"description": "Main hero headline. Caveat Brush display.",
"default": "Soft starts",
"x-editor": "richtext"
},
"body": {
"type": "string",
"title": "Body",
"description": "Supporting paragraph below the headline.",
"x-editor": "richtext"
},
"image": {
"type": "string",
"title": "Feature Image",
"description": "Optional hero image; renders to the right of the headline on desktop.",
"x-editor": "media"
},
"ctaText": {
"type": "string",
"title": "CTA Label",
"x-editor": "text"
},
"ctaHref": {
"type": "string",
"title": "CTA Link",
"x-editor": "link"
}
}
}

View File

@ -1 +1,18 @@
[]
[
{
"name": "Fraunces",
"family": "Fraunces",
"variants": [
{ "weight": "400", "style": "normal", "file": "fonts/fraunces-latin-400.woff2" },
{ "weight": "600", "style": "normal", "file": "fonts/fraunces-latin-600.woff2" }
]
},
{
"name": "Quicksand",
"family": "Quicksand",
"variants": [
{ "weight": "400", "style": "normal", "file": "fonts/quicksand-400.woff2" },
{ "weight": "700", "style": "normal", "file": "fonts/quicksand-700.woff2" }
]
}
]

View File

@ -1,231 +1,402 @@
# Pastel Dream theme — codeless .bnp manifest (WO-WZ-021 Phase B1).
# Synthesized into manifest.pb by `ninja plugin build` (no plugin.wasm).
# Pastel Dream theme — codeless .bnp manifest (Wave A, WO-TF-023).
# Bakery / patisserie persona. Synthesized into manifest.pb by
# `ninja plugin build` (no plugin.wasm).
theme_presets: presets.json
bundled_fonts: fonts.json
master_pages: master_pages.json
required_icon_packs:
- lucide
- simple-icons
system_templates:
- key: pastel-dream
title: Pastel Dream
description: "Soft watercolor theme with blush, mint, and butter palettes for wellness, parenting, and spa brands."
description: "Soft bakery and patisserie theme — watercolor washes, scalloped edges, rounded menu cards and seasonal specials."
page_templates:
- system: pastel-dream
key: default
title: Default
description: "Soft layered hero, masthead nav, footer with newsletter."
description: "Header, main and footer over a soft watercolor wash."
slots: [header, main, footer]
- system: pastel-dream
key: landing
title: Soft Landing
description: "Watercolor hero, affirmation strip, CTA, footer."
slots: [hero, main, cta, footer]
title: Landing
description: "Hero, menu, story and an invitation to order."
slots: [hero, menu, story, cta, footer]
- system: pastel-dream
key: article
title: Reading Room
description: "Narrow, centered editorial column with side rail."
title: Article
description: "Narrow reading column with a soft drop-cap for the journal and recipes."
slots: [header, main, footer]
- system: pastel-dream
key: full-width
title: Open Air
description: "Edge-to-edge sections for galleries and seasonal looks."
title: Full Width
description: "Edge-to-edge galleries and counter shots."
slots: [header, main, footer]
template_overrides:
- { template: pastel-dream, block: heading }
- { template: pastel-dream, block: text }
- { template: pastel-dream, block: button }
- { template: pastel-dream, block: card }
- system: pastel-dream
key: blog-index
title: Blog Index
description: "Blog hero, featured posts and a post grid."
slots: [header, featured, main, footer]
- system: pastel-dream
key: contact
title: Contact
description: "Order and enquiry form, hours and pickup details."
slots: [header, main, footer]
- system: pastel-dream
key: auth
title: Auth
description: "Centered soft card for login, register and password reset."
slots: [main, footer]
email_wrappers:
- pastel-dream
template_overrides:
- { template: pastel-dream, block: announcement-bar }
- { template: pastel-dream, block: audio-embed }
- { template: pastel-dream, block: auth-form }
- { template: pastel-dream, block: author-bio }
- { template: pastel-dream, block: author-bio-hero }
- { template: pastel-dream, block: auth-status }
- { template: pastel-dream, block: blog-hero }
- { template: pastel-dream, block: breadcrumbs }
- { template: pastel-dream, block: button }
- { template: pastel-dream, block: card }
- { template: pastel-dream, block: category-list }
- { template: pastel-dream, block: contact-card }
- { template: pastel-dream, block: contact-form }
- { template: pastel-dream, block: countdown }
- { template: pastel-dream, block: cta }
- { template: pastel-dream, block: divider }
- { template: pastel-dream, block: event-list }
- { template: pastel-dream, block: faq }
- { template: pastel-dream, block: featured-posts }
- { template: pastel-dream, block: feature-grid }
- { template: pastel-dream, block: footer }
- { template: pastel-dream, block: gallery }
- { template: pastel-dream, block: heading }
- { template: pastel-dream, block: hero }
- { template: pastel-dream, block: hours-location }
- { template: pastel-dream, block: image }
- { template: pastel-dream, block: logo-strip }
- { template: pastel-dream, block: menu-list }
- { template: pastel-dream, block: navbar }
- { template: pastel-dream, block: newsletter-signup }
- { template: pastel-dream, block: page-suggestions }
- { template: pastel-dream, block: password-reset-form }
- { template: pastel-dream, block: popular-posts }
- { template: pastel-dream, block: post-hero }
- { template: pastel-dream, block: post-metadata }
- { template: pastel-dream, block: pricing }
- { template: pastel-dream, block: quote }
- { template: pastel-dream, block: related-posts }
- { template: pastel-dream, block: rich-section }
- { template: pastel-dream, block: rich-text }
- { template: pastel-dream, block: sidebar-nav }
- { template: pastel-dream, block: social-links }
- { template: pastel-dream, block: stats }
- { template: pastel-dream, block: team }
- { template: pastel-dream, block: testimonial }
- { template: pastel-dream, block: text }
- { template: pastel-dream, block: timeline }
- { template: pastel-dream, block: utility-bar }
- { template: pastel-dream, block: video-embed }
css:
input_css_append: |
/* Pastel Dream — theme-specific CSS appended to host Tailwind input.
/* Pastel Dream theme styles — bakery / patisserie.
*
* Rules:
* - All colors must consume tokens via hsl(var(--token)). No hardcoded hex/rgb/named colors.
* - All font-family declarations must consume var(--font-*) with a graceful fallback stack.
* - Animations must be guarded by prefers-reduced-motion: reduce.
* Dual-mode via the 19 shadcn-style HSL token CSS variables (--background,
* --foreground, --primary, --accent, --border, --muted, --card, ...) consumed
* as hsl(var(--token)). No literal colors anywhere. Font families resolve ONLY
* through the BlockNinja font variables (--font-heading, --font-body,
* --font-mono) with the fallback stacks below — never hardcoded, so the admin
* font picker keeps working.
*
* Motion tier: CSS-FIRST. Every animation is a gentle CSS drift and is disabled
* under prefers-reduced-motion. No canvas, no JS showpiece.
*/
:root {
--radius-soft: 20px;
--pd-heading-fallback: "Fraunces", "Playfair Display", Georgia, "Times New Roman", serif;
--pd-body-fallback: "Quicksand", "Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
--pd-hand-fallback: "Quicksand", "Comfortaa", "Segoe Print", cursive;
--pd-mono-fallback: "JetBrains Mono", "Fira Code", Menlo, Consolas, monospace;
--pd-radius: 1.5rem;
}
/* --- Watercolor utility classes ---------------------------------------- */
/* Soft radial wash backgrounds, tinted from the active palette via primary
* and accent tokens. Used as decorative layers behind hero and footer.
*/
.bg-watercolor-blush {
/* --- Watercolor wash background ----------------------------------------- */
/* The signature "morning haze": soft radial pastel washes tinted from the
* active palette, drifting slowly. Reads as watercolor on cream in light mode
* and as a dusk-lilac glow in dark mode — all from the same tokens. */
body.pd-paper {
background-color: hsl(var(--background));
background-image:
radial-gradient(60% 70% at 20% 30%, hsl(var(--primary) / 0.18) 0%, transparent 65%),
radial-gradient(50% 60% at 80% 70%, hsl(var(--accent) / 0.15) 0%, transparent 60%);
radial-gradient(60% 55% at 15% 12%, hsl(var(--primary) / 0.14) 0%, transparent 60%),
radial-gradient(55% 50% at 85% 20%, hsl(var(--accent) / 0.12) 0%, transparent 58%),
radial-gradient(65% 60% at 75% 85%, hsl(var(--secondary) / 0.40) 0%, transparent 62%),
radial-gradient(50% 50% at 25% 90%, hsl(var(--primary) / 0.08) 0%, transparent 60%);
background-attachment: fixed;
background-repeat: no-repeat;
/* Guard against horizontal scroll on small screens from any off-canvas
* chrome (mobile drawer). `clip` does not create a scroll container, so
* it will not break the sticky navbar the way `hidden` can. */
overflow-x: clip;
}
.bg-watercolor-mint {
background-image:
radial-gradient(60% 70% at 25% 35%, hsl(var(--secondary) / 0.45) 0%, transparent 65%),
radial-gradient(55% 65% at 75% 65%, hsl(var(--primary) / 0.12) 0%, transparent 60%);
/* --- Type roles --------------------------------------------------------- */
.pd-display {
font-family: var(--font-heading, var(--pd-heading-fallback));
font-optical-sizing: auto;
font-feature-settings: "liga" 1, "dlig" 1;
letter-spacing: -0.005em;
}
.pd-body {
font-family: var(--font-body, var(--pd-body-fallback));
line-height: 1.7;
}
.pd-mono {
font-family: var(--font-mono, var(--pd-mono-fallback));
}
/* Sweet accent lettering (badges, captions, section titles). Routed through
* --font-heading with a rounded fallback: unassigned it renders Quicksand; if
* the admin sets a heading font the accents follow it (3-slot model intended). */
.pd-hand {
font-family: var(--font-heading, var(--pd-hand-fallback));
letter-spacing: 0.005em;
line-height: 1.2;
}
/* --- Soft shadow utilities -------------------------------------------- */
/* Shadows are large, low-alpha, and tinted toward primary rather than grey. */
.shadow-soft {
box-shadow: 0 12px 40px -8px hsl(var(--primary) / 0.18),
0 4px 12px -2px hsl(var(--primary) / 0.10);
/* --- Brush underline ---------------------------------------------------- */
.pd-doodle-underline {
background-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 12' preserveAspectRatio='none'%3E%3Cpath d='M2 8 Q 22 1 44 6 T 88 6 T 118 6' fill='none' stroke='currentColor' stroke-width='4' stroke-linecap='round' opacity='0.35'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: 0 100%;
background-size: 100% 0.55em;
padding-bottom: 0.16em;
}
.shadow-soft-hover {
box-shadow: 0 16px 48px -8px hsl(var(--primary) / 0.24),
0 6px 16px -2px hsl(var(--primary) / 0.14);
/* --- Drop-cap ----------------------------------------------------------- */
.pd-dropcap > p:first-of-type::first-letter {
font-family: var(--font-heading, var(--pd-heading-fallback));
font-size: 3.8em;
line-height: 0.8;
float: left;
padding: 0.06em 0.14em 0 0;
color: hsl(var(--primary));
}
/* --- Deckle edge ------------------------------------------------------- */
/* Hand-torn paper effect for testimonial cards. Uses mask-image so it
* renders against whatever background lies beneath.
*/
.deckle-edge {
-webkit-mask-image:
radial-gradient(circle at 6px 6px, transparent 4px, hsl(var(--card)) 4.5px),
linear-gradient(to bottom, hsl(var(--card)) 0%, hsl(var(--card)) 100%);
mask-image:
radial-gradient(circle at 6px 6px, transparent 4px, hsl(var(--card)) 4.5px),
linear-gradient(to bottom, hsl(var(--card)) 0%, hsl(var(--card)) 100%);
-webkit-mask-composite: source-over;
mask-composite: add;
border: 1px dashed hsl(var(--border));
}
/* --- Pill button override --------------------------------------------- */
/* Spec §"Block overrides": every button is pill (9999px), tinted shadow,
* 2px hover lift, easing cubic-bezier(.22,1,.36,1).
*/
.pastel-pill {
border-radius: 9999px;
padding: 0.75rem 1.75rem;
font-family: var(--font-body, "Nunito", "Quicksand", system-ui, sans-serif);
font-weight: 600;
/* --- Soft pill button (icing-smooth, lifts on hover) -------------------- */
.pd-tag {
position: relative;
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.7rem 1.5rem;
background-color: hsl(var(--primary));
color: hsl(var(--primary-foreground));
box-shadow: 0 8px 24px -6px hsl(var(--primary) / 0.40);
transition: transform 240ms cubic-bezier(.22,1,.36,1),
box-shadow 240ms cubic-bezier(.22,1,.36,1),
background-color 240ms cubic-bezier(.22,1,.36,1);
border: none;
border-radius: 9999px;
font-family: var(--font-body, var(--pd-body-fallback));
font-weight: 600;
letter-spacing: 0.01em;
box-shadow: 0 8px 22px -8px hsl(var(--primary) / 0.45);
transition: transform 220ms cubic-bezier(.22,1,.36,1), box-shadow 220ms cubic-bezier(.22,1,.36,1), background-color 220ms ease;
cursor: pointer;
display: inline-block;
}
.pastel-pill:hover {
.pd-tag:hover {
transform: translateY(-2px);
box-shadow: 0 12px 32px -6px hsl(var(--primary) / 0.50);
box-shadow: 0 14px 30px -8px hsl(var(--primary) / 0.52);
background-color: hsl(var(--primary) / 0.92);
}
.pastel-pill:focus-visible {
.pd-tag:focus-visible {
outline: 2px solid hsl(var(--ring));
outline-offset: 2px;
outline-offset: 3px;
}
.pd-tag--ghost {
background-color: transparent;
color: hsl(var(--primary));
box-shadow: none;
border: 1.5px solid hsl(var(--primary) / 0.45);
}
.pd-tag--ghost:hover {
background-color: hsl(var(--primary) / 0.08);
box-shadow: none;
}
/* --- Soft card -------------------------------------------------------- */
.pastel-card {
border-radius: var(--radius-soft);
/* --- Scalloped edges (piped-icing / doily trim) ------------------------- */
.pd-torn-top {
-webkit-mask-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 12' preserveAspectRatio='none'%3E%3Cpath d='M0 6 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 L120 12 L0 12 Z' fill='black'/%3E%3C/svg%3E");
mask-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 12' preserveAspectRatio='none'%3E%3Cpath d='M0 6 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 a6 6 0 0 1 12 0 L120 12 L0 12 Z' fill='black'/%3E%3C/svg%3E");
-webkit-mask-repeat: no-repeat;
mask-repeat: no-repeat;
-webkit-mask-size: 100% 100%;
mask-size: 100% 100%;
}
.pd-torn-bottom {
-webkit-mask-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 12' preserveAspectRatio='none'%3E%3Cpath d='M0 0 L120 0 L120 6 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 Z' fill='black'/%3E%3C/svg%3E");
mask-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 12' preserveAspectRatio='none'%3E%3Cpath d='M0 0 L120 0 L120 6 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 a6 6 0 0 1 -12 0 Z' fill='black'/%3E%3C/svg%3E");
-webkit-mask-repeat: no-repeat;
mask-repeat: no-repeat;
-webkit-mask-size: 100% 100%;
mask-size: 100% 100%;
}
/* --- Surfaces (rounded-everything) ------------------------------------- */
.pd-card {
background-color: hsl(var(--card));
color: hsl(var(--card-foreground));
border: 1px solid hsl(var(--border));
box-shadow: 0 12px 40px -8px hsl(var(--primary) / 0.18),
0 4px 12px -2px hsl(var(--primary) / 0.10);
border-radius: var(--pd-radius);
box-shadow: 0 12px 40px -14px hsl(var(--primary) / 0.22), 0 4px 12px -6px hsl(var(--primary) / 0.10);
}
.pd-frame {
border: 1px solid hsl(var(--border));
background-color: hsl(var(--card));
border-radius: var(--pd-radius);
}
/* --- Brush-stroke underline for headings ------------------------------ */
.brush-underline {
/* --- Menu card (soft pastel board for the menu-list override) ---------- */
/* Not an inverted chalkboard: a soft card with a primary wash. Text inherits
* card-foreground so item names and section titles stay legible in both modes. */
.pd-chalkboard {
position: relative;
display: inline-block;
background-color: hsl(var(--card));
background-image:
radial-gradient(70% 60% at 12% 8%, hsl(var(--primary) / 0.10) 0%, transparent 60%),
radial-gradient(60% 55% at 90% 92%, hsl(var(--accent) / 0.08) 0%, transparent 60%);
color: hsl(var(--card-foreground));
border: 1px solid hsl(var(--border));
border-radius: var(--pd-radius);
box-shadow: 0 16px 48px -18px hsl(var(--primary) / 0.28);
}
.pd-chalk-rule {
border-color: hsl(var(--border));
border-style: solid;
}
.pd-chalk-muted {
color: hsl(var(--muted-foreground));
}
.brush-underline::after {
content: "";
position: absolute;
left: 0;
right: 0;
bottom: -0.2em;
height: 0.22em;
background-color: hsl(var(--accent) / 0.55);
/* --- Hours: today highlight -------------------------------------------- */
.pd-hours-today {
background-color: hsl(var(--accent) / 0.16);
color: hsl(var(--accent-foreground));
border-left: 3px solid hsl(var(--accent));
border-radius: 0 9999px 9999px 0;
padding-left: 0.75rem;
}
/* --- Location pin ------------------------------------------------------- */
.pd-pin {
color: hsl(var(--accent));
}
/* --- Soft badge (timeline markers) ------------------------------------- */
.pd-stamp {
display: inline-flex;
align-items: center;
justify-content: center;
width: 4rem;
height: 4rem;
border-radius: 9999px;
z-index: -1;
background-color: hsl(var(--accent));
color: hsl(var(--accent-foreground));
border: 3px solid hsl(var(--card));
box-shadow: 0 8px 20px -8px hsl(var(--accent) / 0.55);
font-family: var(--font-heading, var(--pd-hand-fallback));
font-size: 1.05rem;
text-align: center;
line-height: 1.05;
transition: transform 200ms cubic-bezier(.22,1,.36,1);
}
.pd-stamp:hover {
transform: scale(1.06);
}
.pd-stamp-sm {
width: 2.25rem;
height: 2.25rem;
font-size: 0.95rem;
box-shadow: none;
}
/* --- Typography fallbacks --------------------------------------------- */
/* Spec §"Visual language" pairs Caveat Brush + Nunito; admin can override
* via the font picker. Fallback stacks degrade gracefully.
*/
.font-display {
font-family: var(--font-heading, "Caveat Brush", "Sacramento", "Brush Script MT", cursive);
letter-spacing: 0.01em;
/* --- Brush divider draw-in --------------------------------------------- */
.pd-doodle-draw path,
.pd-doodle-draw ellipse,
.pd-doodle-draw circle {
stroke-dasharray: 260;
stroke-dashoffset: 260;
animation: pd-draw 1.6s ease forwards;
}
.font-body {
font-family: var(--font-body, "Nunito", "Quicksand", "Helvetica Neue", system-ui, sans-serif);
line-height: 1.75;
@keyframes pd-draw {
to { stroke-dashoffset: 0; }
}
.font-mono {
font-family: var(--font-mono, "JetBrains Mono", "Fira Code", ui-monospace, monospace);
/* --- Gentle atmosphere: slow wash drift (CSS-first signature) ----------- */
@keyframes pd-breathe {
0%, 100% { transform: scale(1); opacity: 0.9; }
50% { transform: scale(1.05); opacity: 1; }
}
.pd-breathe {
animation: pd-breathe 8s ease-in-out infinite;
}
/* --- Animations ------------------------------------------------------- */
/* @keyframes pastel-shimmer — drift the accent overlay slowly across the
* affirmation strip. Spec §13.10: animation-duration >= 6s.
*/
@keyframes pastel-shimmer {
0% {
background-position: 0% 50%;
opacity: 0.85;
}
50% {
background-position: 100% 50%;
opacity: 1;
}
100% {
background-position: 0% 50%;
opacity: 0.85;
}
}
/* @keyframes breathe — gentle scale pulse, used by hero CTA and watercolor
* blobs for an in-and-out breath rhythm.
*/
@keyframes breathe {
0%, 100% {
transform: scale(1);
opacity: 0.85;
}
50% {
transform: scale(1.04);
opacity: 1;
}
}
.animate-pastel-shimmer {
background-size: 200% 200%;
animation: pastel-shimmer 8s ease-in-out infinite;
}
.animate-breathe {
animation: breathe 6s ease-in-out infinite;
}
/* --- Accessibility: respect prefers-reduced-motion -------------------- */
/* --- Motion safety: static fallback ------------------------------------ */
@media (prefers-reduced-motion: reduce) {
.animate-pastel-shimmer,
.animate-breathe,
.pastel-pill {
animation: none !important;
transition: none !important;
}
.pastel-pill:hover {
transform: none;
.pd-tag,
.pd-stamp { transition: none; }
.pd-tag:hover,
.pd-stamp:hover { transform: none; }
.pd-breathe { animation: none; }
.pd-doodle-draw path,
.pd-doodle-draw ellipse,
.pd-doodle-draw circle {
stroke-dasharray: none;
stroke-dashoffset: 0;
animation: none;
}
}
/* --- Layout helpers (theme-owned, semantic tokens only) -----------------
* Installed .bnp themes live outside the host Tailwind @source globs, so the
* host build does NOT compile grid / aspect-ratio / gap / padding utilities
* that appear only in this theme's own templates. The theme therefore owns
* those layout primitives here as plain CSS: mobile-first, collapsing to a
* single column on small screens, with explicit media queries so the render
* is reliable regardless of the JIT content scan. No colors — layout only. */
.pd-section { padding-top: 2.75rem; padding-bottom: 2.75rem; }
@media (min-width: 768px) {
.pd-section { padding-top: 4rem; padding-bottom: 4rem; }
}
.pd-grid { display: grid; grid-template-columns: minmax(0, 1fr); gap: 2rem; }
@media (min-width: 640px) {
.pd-grid--2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (min-width: 1024px) {
.pd-grid--lg-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.pd-grid--3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.pd-grid--lg-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}
/* Two-up menu board: one column on phones, two side by side from md. */
.pd-menu-cols { display: grid; grid-template-columns: minmax(0, 1fr); gap: 2.5rem 3rem; }
@media (min-width: 768px) {
.pd-menu-cols--2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
.pd-menu-list { margin-top: 1rem; }
.pd-menu-list > li + li { margin-top: 1.25rem; }
/* Media frames: a fixed aspect box so a photo fills gracefully. The EMPTY
* state is omitted in-template (no ballooning boxes when no photo is set). */
.pd-media { position: relative; width: 100%; overflow: hidden; aspect-ratio: 4 / 3; }
.pd-media--square { aspect-ratio: 1 / 1; }
.pd-media img { display: block; width: 100%; height: 100%; object-fit: cover; }
/* Card body padding (host may not emit p-5 for theme templates). */
.pd-card-body { padding: 1.25rem 1.25rem 1.5rem; }
@media (min-width: 768px) { .pd-card-body { padding: 1.5rem; } }

View File

@ -2,11 +2,11 @@
{
"key": "pastel-dream:default-master",
"title": "Pastel Dream Default Master",
"page_templates": ["default", "article"],
"page_templates": ["default", "article", "full-width", "contact"],
"blocks": [
{ "block_key": "pastel-dream:soft-navbar", "title": "Soft Nav", "content": { "menuName": "main", "logo": "Pastel Dream" }, "slot": "header", "sort_order": 0 },
{ "block_key": "slot", "title": "Main Slot", "content": { "slotName": "main", "placeholder": "Page content" }, "slot": "main", "sort_order": 0 },
{ "block_key": "pastel-dream:cozy-footer", "title": "Cozy Footer", "content": { "showSignup": "yes", "affirmation": "Be gentle with yourself today." }, "slot": "footer", "sort_order": 0 }
{ "block_key": "navbar", "title": "Main Navigation", "content": { "menuName": "main", "companyName": "Petal & Crumb" }, "slot": "header", "sort_order": 0 },
{ "block_key": "slot", "title": "Main Content", "content": { "slotName": "main", "placeholder": "Add your page here" }, "slot": "main", "sort_order": 0 },
{ "block_key": "footer", "title": "Site Footer", "content": { "companyName": "Petal & Crumb", "tagline": "A small patisserie, baked fresh each morning." }, "slot": "footer", "sort_order": 0 }
]
},
{
@ -14,10 +14,32 @@
"title": "Pastel Dream Landing Master",
"page_templates": ["landing"],
"blocks": [
{ "block_key": "pastel-dream:watercolor-hero", "title": "Watercolor Hero", "content": { "eyebrow": "new", "headline": "Soft starts", "body": "A gentle place to begin.", "ctaText": "Begin" }, "slot": "hero", "sort_order": 0 },
{ "block_key": "slot", "title": "Main Slot", "content": { "slotName": "main" }, "slot": "main", "sort_order": 0 },
{ "block_key": "pastel-dream:affirmation", "title": "Affirmation Strip", "content": { "quote": "You are doing enough.", "author": "Pastel Dream" }, "slot": "cta", "sort_order": 0 },
{ "block_key": "pastel-dream:cozy-footer", "title": "Cozy Footer", "content": { "showSignup": "yes" }, "slot": "footer", "sort_order": 0 }
{ "block_key": "navbar", "title": "Main Navigation", "content": { "menuName": "main", "companyName": "Petal & Crumb" }, "slot": "hero", "sort_order": 0 },
{ "block_key": "slot", "title": "Hero", "content": { "slotName": "hero", "placeholder": "Drop a hero block" }, "slot": "hero", "sort_order": 1 },
{ "block_key": "slot", "title": "Menu", "content": { "slotName": "menu", "placeholder": "Feature the menu" }, "slot": "menu", "sort_order": 0 },
{ "block_key": "slot", "title": "Story", "content": { "slotName": "story", "placeholder": "Tell the story" }, "slot": "story", "sort_order": 0 },
{ "block_key": "slot", "title": "Call To Action", "content": { "slotName": "cta", "placeholder": "Invite them to order" }, "slot": "cta", "sort_order": 0 },
{ "block_key": "footer", "title": "Site Footer", "content": { "companyName": "Petal & Crumb", "tagline": "A small patisserie, baked fresh each morning." }, "slot": "footer", "sort_order": 0 }
]
},
{
"key": "pastel-dream:blog-master",
"title": "Pastel Dream Blog Master",
"page_templates": ["blog-index"],
"blocks": [
{ "block_key": "navbar", "title": "Main Navigation", "content": { "menuName": "main", "companyName": "Petal & Crumb" }, "slot": "header", "sort_order": 0 },
{ "block_key": "slot", "title": "Featured", "content": { "slotName": "featured", "placeholder": "Featured posts" }, "slot": "featured", "sort_order": 0 },
{ "block_key": "slot", "title": "Posts", "content": { "slotName": "main", "placeholder": "Post grid" }, "slot": "main", "sort_order": 0 },
{ "block_key": "footer", "title": "Site Footer", "content": { "companyName": "Petal & Crumb", "tagline": "A small patisserie, baked fresh each morning." }, "slot": "footer", "sort_order": 0 }
]
},
{
"key": "pastel-dream:auth-master",
"title": "Pastel Dream Auth Master",
"page_templates": ["auth"],
"blocks": [
{ "block_key": "slot", "title": "Auth Form", "content": { "slotName": "main", "placeholder": "Auth form" }, "slot": "main", "sort_order": 0 },
{ "block_key": "slot", "title": "Footer Note", "content": { "slotName": "footer", "placeholder": "Small print" }, "slot": "footer", "sort_order": 0 }
]
}
]

View File

@ -2,8 +2,8 @@
name = "pastel-dream"
display_name = "Pastel Dream"
scope = "@themes"
version = "0.2.0"
description = "Soft watercolor theme with blush, mint, and butter palettes for wellness, parenting, and spa brands."
version = "0.2.2"
description = "Soft bakery and patisserie theme — watercolor washes, scalloped edges, rounded menu cards, and seasonal specials in blush, berry, and lilac."
kind = "theme"
categories = ["templates"]
tags = ["pastel", "soft", "wellness", "parenting", "spa", "doula", "calm", "skincare", "mindfulness"]
tags = ["pastel", "bakery", "patisserie", "cafe", "food", "soft", "rounded", "watercolor", "menu", "seasonal"]

View File

@ -1,88 +1,251 @@
[
{
"id": "blush-morning",
"name": "Blush Morning",
"description": "Default — warm peach paper with rose-blush primary and sage trim.",
"id": "morning-haze",
"name": "Morning Haze",
"description": "Blush rose on warm cream with a mint accent. The default patisserie counter at opening.",
"theme": {
"mode": "light",
"mode": "both",
"lightColors": {
"background": "25 60% 98%",
"foreground": "340 20% 22%",
"card": "0 0% 100%",
"cardForeground": "340 20% 22%",
"popover": "0 0% 100%",
"popoverForeground": "340 20% 22%",
"primary": "350 65% 72%",
"primaryForeground": "340 40% 18%",
"secondary": "150 35% 88%",
"secondaryForeground": "155 35% 22%",
"muted": "30 40% 95%",
"mutedForeground": "340 12% 48%",
"accent": "48 85% 80%",
"accentForeground": "35 50% 22%",
"destructive": "0 75% 65%",
"destructiveForeground": "0 0% 100%",
"border": "340 30% 90%",
"input": "340 30% 92%",
"ring": "350 65% 72%"
}
}
"background": "30 44% 97%",
"foreground": "335 25% 22%",
"card": "30 42% 99%",
"cardForeground": "335 25% 22%",
"popover": "30 42% 99%",
"popoverForeground": "335 25% 22%",
"primary": "340 58% 60%",
"primaryForeground": "40 50% 98%",
"secondary": "18 52% 92%",
"secondaryForeground": "335 25% 24%",
"muted": "30 30% 93%",
"mutedForeground": "335 12% 42%",
"accent": "160 38% 52%",
"accentForeground": "160 40% 12%",
"destructive": "0 70% 55%",
"destructiveForeground": "40 50% 98%",
"border": "24 30% 87%",
"input": "24 30% 87%",
"ring": "340 58% 60%"
},
{
"id": "mint-meadow",
"name": "Mint Meadow",
"description": "Sage-led variant for spa and doula brands; rose drops to accent.",
"theme": {
"mode": "light",
"lightColors": {
"background": "150 40% 97%",
"foreground": "155 35% 18%",
"card": "0 0% 100%",
"cardForeground": "155 35% 18%",
"popover": "0 0% 100%",
"popoverForeground": "155 35% 18%",
"primary": "155 40% 60%",
"primaryForeground": "0 0% 100%",
"secondary": "350 50% 92%",
"secondaryForeground": "345 40% 28%",
"muted": "150 25% 94%",
"mutedForeground": "155 18% 42%",
"accent": "48 80% 82%",
"accentForeground": "35 50% 22%",
"destructive": "5 70% 62%",
"destructiveForeground": "0 0% 100%",
"border": "150 25% 88%",
"input": "150 25% 92%",
"ring": "155 40% 60%"
}
}
},
{
"id": "twilight-petal",
"name": "Twilight Petal",
"description": "Dusky night-mode pastel — plum velvet with blush glow.",
"theme": {
"mode": "dark",
"darkColors": {
"background": "300 25% 12%",
"foreground": "30 40% 94%",
"card": "300 22% 16%",
"cardForeground": "30 40% 94%",
"popover": "300 22% 16%",
"popoverForeground": "30 40% 94%",
"primary": "345 70% 78%",
"primaryForeground": "340 40% 18%",
"secondary": "155 25% 32%",
"secondaryForeground": "150 30% 92%",
"muted": "300 18% 22%",
"background": "290 22% 12%",
"foreground": "320 22% 92%",
"card": "290 20% 16%",
"cardForeground": "320 22% 92%",
"popover": "290 20% 16%",
"popoverForeground": "320 22% 92%",
"primary": "340 64% 72%",
"primaryForeground": "290 30% 12%",
"secondary": "290 18% 22%",
"secondaryForeground": "320 22% 92%",
"muted": "290 15% 20%",
"mutedForeground": "300 12% 70%",
"accent": "48 75% 72%",
"accentForeground": "35 50% 18%",
"destructive": "0 70% 60%",
"destructiveForeground": "0 0% 100%",
"border": "300 20% 28%",
"input": "300 20% 22%",
"ring": "345 70% 78%"
"accent": "160 44% 60%",
"accentForeground": "290 30% 12%",
"destructive": "0 60% 60%",
"destructiveForeground": "320 22% 92%",
"border": "290 16% 26%",
"input": "290 16% 24%",
"ring": "340 64% 72%"
}
}
},
{
"id": "berry-cream",
"name": "Berry Cream",
"description": "Raspberry on ivory with a plum accent. Jam pots and fruit tarts.",
"theme": {
"mode": "both",
"lightColors": {
"background": "350 40% 97%",
"foreground": "345 30% 20%",
"card": "350 38% 99%",
"cardForeground": "345 30% 20%",
"popover": "350 38% 99%",
"popoverForeground": "345 30% 20%",
"primary": "345 62% 52%",
"primaryForeground": "350 40% 98%",
"secondary": "350 46% 92%",
"secondaryForeground": "345 30% 22%",
"muted": "350 28% 93%",
"mutedForeground": "345 14% 40%",
"accent": "285 40% 55%",
"accentForeground": "285 40% 12%",
"destructive": "0 72% 52%",
"destructiveForeground": "350 40% 98%",
"border": "348 28% 87%",
"input": "348 28% 87%",
"ring": "345 62% 52%"
},
"darkColors": {
"background": "330 24% 11%",
"foreground": "340 22% 92%",
"card": "330 22% 15%",
"cardForeground": "340 22% 92%",
"popover": "330 22% 15%",
"popoverForeground": "340 22% 92%",
"primary": "345 68% 66%",
"primaryForeground": "330 30% 11%",
"secondary": "330 20% 21%",
"secondaryForeground": "340 22% 92%",
"muted": "330 16% 19%",
"mutedForeground": "340 14% 70%",
"accent": "285 46% 66%",
"accentForeground": "330 30% 11%",
"destructive": "0 62% 60%",
"destructiveForeground": "340 22% 92%",
"border": "330 16% 25%",
"input": "330 16% 23%",
"ring": "345 68% 66%"
}
}
},
{
"id": "matcha-cream",
"name": "Matcha Cream",
"description": "Soft sage on oat cream with an apricot accent. Green tea cakes and mochi.",
"theme": {
"mode": "both",
"lightColors": {
"background": "44 36% 96%",
"foreground": "150 22% 18%",
"card": "44 34% 99%",
"cardForeground": "150 22% 18%",
"popover": "44 34% 99%",
"popoverForeground": "150 22% 18%",
"primary": "148 34% 42%",
"primaryForeground": "44 40% 98%",
"secondary": "150 30% 90%",
"secondaryForeground": "150 22% 20%",
"muted": "80 20% 91%",
"mutedForeground": "150 12% 38%",
"accent": "28 70% 56%",
"accentForeground": "28 60% 12%",
"destructive": "0 70% 52%",
"destructiveForeground": "44 40% 98%",
"border": "90 18% 84%",
"input": "90 18% 84%",
"ring": "148 34% 42%"
},
"darkColors": {
"background": "275 20% 12%",
"foreground": "44 26% 91%",
"card": "275 18% 16%",
"cardForeground": "44 26% 91%",
"popover": "275 18% 16%",
"popoverForeground": "44 26% 91%",
"primary": "148 40% 58%",
"primaryForeground": "275 30% 12%",
"secondary": "275 16% 22%",
"secondaryForeground": "44 26% 91%",
"muted": "275 13% 20%",
"mutedForeground": "80 14% 68%",
"accent": "28 74% 62%",
"accentForeground": "275 30% 12%",
"destructive": "0 60% 60%",
"destructiveForeground": "44 26% 91%",
"border": "275 14% 26%",
"input": "275 14% 24%",
"ring": "148 40% 58%"
}
}
},
{
"id": "lavender-honey",
"name": "Lavender Honey",
"description": "Lilac on cream with a warm honey accent. Lavender shortbread and glaze.",
"theme": {
"mode": "both",
"lightColors": {
"background": "40 40% 97%",
"foreground": "270 24% 22%",
"card": "40 38% 99%",
"cardForeground": "270 24% 22%",
"popover": "40 38% 99%",
"popoverForeground": "270 24% 22%",
"primary": "265 44% 58%",
"primaryForeground": "40 50% 98%",
"secondary": "265 38% 92%",
"secondaryForeground": "270 24% 24%",
"muted": "265 24% 93%",
"mutedForeground": "270 12% 42%",
"accent": "42 78% 56%",
"accentForeground": "42 60% 12%",
"destructive": "0 70% 55%",
"destructiveForeground": "40 50% 98%",
"border": "265 24% 87%",
"input": "265 24% 87%",
"ring": "265 44% 58%"
},
"darkColors": {
"background": "270 26% 12%",
"foreground": "265 20% 92%",
"card": "270 22% 16%",
"cardForeground": "265 20% 92%",
"popover": "270 22% 16%",
"popoverForeground": "265 20% 92%",
"primary": "265 54% 72%",
"primaryForeground": "270 30% 12%",
"secondary": "270 20% 22%",
"secondaryForeground": "265 20% 92%",
"muted": "270 16% 20%",
"mutedForeground": "265 14% 70%",
"accent": "42 80% 62%",
"accentForeground": "270 30% 12%",
"destructive": "0 60% 60%",
"destructiveForeground": "265 20% 92%",
"border": "270 16% 26%",
"input": "270 16% 24%",
"ring": "265 54% 72%"
}
}
},
{
"id": "peach-blossom",
"name": "Peach Blossom",
"description": "Warm peach on blossom cream with a rose accent. Stone-fruit galettes in spring.",
"theme": {
"mode": "both",
"lightColors": {
"background": "26 50% 97%",
"foreground": "15 30% 22%",
"card": "26 46% 99%",
"cardForeground": "15 30% 22%",
"popover": "26 46% 99%",
"popoverForeground": "15 30% 22%",
"primary": "16 68% 56%",
"primaryForeground": "26 50% 98%",
"secondary": "14 58% 92%",
"secondaryForeground": "15 30% 24%",
"muted": "26 34% 93%",
"mutedForeground": "15 16% 42%",
"accent": "340 58% 62%",
"accentForeground": "340 40% 12%",
"destructive": "0 72% 52%",
"destructiveForeground": "26 50% 98%",
"border": "20 34% 87%",
"input": "20 34% 87%",
"ring": "16 68% 56%"
},
"darkColors": {
"background": "300 22% 12%",
"foreground": "26 26% 91%",
"card": "300 20% 16%",
"cardForeground": "26 26% 91%",
"popover": "300 20% 16%",
"popoverForeground": "26 26% 91%",
"primary": "16 74% 64%",
"primaryForeground": "300 30% 12%",
"secondary": "300 18% 22%",
"secondaryForeground": "26 26% 91%",
"muted": "300 15% 20%",
"mutedForeground": "20 16% 70%",
"accent": "340 60% 68%",
"accentForeground": "300 30% 12%",
"destructive": "0 60% 60%",
"destructiveForeground": "26 26% 91%",
"border": "300 15% 26%",
"input": "300 15% 24%",
"ring": "16 74% 64%"
}
}
}

176
seed/seed.json Normal file
View File

@ -0,0 +1,176 @@
{
"demo": true,
"settings": {
"merge": {
"site_title": "Petal & Crumb",
"site_description": "A small patisserie baking cakes, pastries and seasonal treats."
}
},
"data_tables": [
{
"key": "contact_submissions",
"name": "Contact Submissions",
"description": "Enquiries and cake orders sent from the contact form.",
"primary_key": "id",
"columns": [
{ "key": "name", "label": "Name", "type": "text", "required": true },
{ "key": "email", "label": "Email", "type": "email", "required": true },
{ "key": "reason", "label": "Reason", "type": "text", "required": false },
{ "key": "message", "label": "Message", "type": "text", "required": true }
]
}
],
"menu_items": [
{ "menu": "main", "label": "Home", "page_slug": "/", "sort_order": 0 },
{ "menu": "main", "label": "Menu", "url": "/#menu", "sort_order": 1 },
{ "menu": "main", "label": "About", "page_slug": "/about", "sort_order": 2 },
{ "menu": "main", "label": "Journal", "page_slug": "/blog", "sort_order": 3 },
{ "menu": "main", "label": "Order", "page_slug": "/contact", "sort_order": 4 }
],
"pages": [
{
"slug": "/",
"title": "Petal & Crumb",
"template_key": "default",
"blocks": [
{ "block_key": "navbar", "title": "Navigation", "slot": "header", "sort_order": 0, "content": { "menuName": "main", "companyName": "Petal & Crumb", "ctaLabel": "Order a cake", "ctaUrl": "/contact" } },
{ "block_key": "hero", "title": "Hero", "slot": "main", "sort_order": 0, "content": { "template": "centered", "eyebrow": "Open from 8", "headline": "Cakes and pastries, baked fresh each morning", "subheadline": "A small patisserie on the corner. Everything is made by hand the day you eat it.", "ctaPrimary": { "text": "See the menu", "url": "/#menu" }, "ctaSecondary": { "text": "Order a cake", "url": "/contact" }, "minHeight": "50vh" } },
{ "block_key": "pastel-dream:seasonal_specials", "title": "Seasonal Specials", "slot": "main", "sort_order": 1, "content": { "kicker": "This week", "heading": "On the counter now", "intro": "A short list of what we are proud of this week. It changes with the season.", "currency": "$", "items": [
{ "name": "Strawberry tart", "note": "Vanilla custard, fresh strawberries, a thin glaze.", "price": "8.00", "badge": "New" },
{ "name": "Pistachio croissant", "note": "Laminated overnight, filled with pistachio cream.", "price": "6.50", "badge": "Popular" },
{ "name": "Lemon poppy loaf", "note": "Soft, tangy, dusted with sugar. Sold by the slice.", "price": "5.00", "badge": "" }
] } },
{ "block_key": "menu-list", "title": "Menu", "slot": "main", "sort_order": 2, "content": { "heading": "The full menu", "intro": "Prices are for takeaway. We can box anything for a gift.", "currency": "$", "columns": 2, "sections": [
{ "title": "Pastries", "items": [
{ "name": "Butter croissant", "price": "5.00" },
{ "name": "Pain au chocolat", "price": "5.50" },
{ "name": "Almond croissant", "description": "Filled and topped with almond cream", "price": "6.50" },
{ "name": "Morning bun", "description": "Orange and cinnamon", "price": "6.00", "badges": [{ "label": "V" }] }
] },
{ "title": "Cakes and slices", "items": [
{ "name": "Victoria sponge", "description": "By the slice", "price": "7.00" },
{ "name": "Chocolate torte", "description": "Flourless", "price": "8.00", "badges": [{ "label": "GF" }] },
{ "name": "Carrot cake", "price": "7.50" },
{ "name": "Custard tart", "price": "6.00" }
] }
] } },
{ "block_key": "pastel-dream:order_ribbon", "title": "Order Ribbon", "slot": "main", "sort_order": 3, "content": { "heading": "Order a cake for the weekend", "message": "Birthdays, gatherings, or just because. Tell us the flavour and the date.", "pickupNote": "Two days notice for custom cakes.", "ctaLabel": "Place an order", "ctaUrl": "/contact" } },
{ "block_key": "hours-location", "title": "Hours", "slot": "main", "sort_order": 4, "content": { "heading": "Come and see us", "address": "18 Rose Lane\nFremantle", "phone": "08 1234 5678", "hours": [
{ "day": "Monday", "closed": true },
{ "day": "Tuesday", "hours": "8:00 - 15:00" },
{ "day": "Wednesday", "hours": "8:00 - 15:00" },
{ "day": "Thursday", "hours": "8:00 - 15:00" },
{ "day": "Friday", "hours": "8:00 - 16:00" },
{ "day": "Saturday", "hours": "8:00 - 16:00" },
{ "day": "Sunday", "hours": "9:00 - 14:00" }
] } },
{ "block_key": "footer", "title": "Footer", "slot": "footer", "sort_order": 0, "content": { "companyName": "Petal & Crumb", "tagline": "A small patisserie, baked fresh each morning.", "address": "18 Rose Lane, Fremantle", "email": "hello@petalandcrumb.test", "copyright": "Petal & Crumb" } }
]
},
{
"slug": "/about",
"title": "About",
"template_key": "default",
"blocks": [
{ "block_key": "navbar", "title": "Navigation", "slot": "header", "sort_order": 0, "content": { "menuName": "main", "companyName": "Petal & Crumb", "ctaLabel": "Order a cake", "ctaUrl": "/contact" } },
{ "block_key": "hero", "title": "Hero", "slot": "main", "sort_order": 0, "content": { "template": "centered", "eyebrow": "Our story", "headline": "One oven, one small room", "subheadline": "We opened with a single oven and a short menu. We kept it that way on purpose.", "minHeight": "auto" } },
{ "block_key": "rich-section", "title": "Story", "slot": "main", "sort_order": 1, "content": { "eyebrow": "How we bake", "heading": "Made the same day", "body": "<p>We start before dawn. The pastry is laminated the night before and baked in the morning, so it reaches you the same day it is made.</p><p>The menu is short because we make everything ourselves. When something is out, it is out until the next bake.</p>", "mediaPosition": "right" } },
{ "block_key": "pastel-dream:bakers_note", "title": "Baker's Note", "slot": "main", "sort_order": 2, "content": { "kicker": "A note from the kitchen", "body": "<p>Thank you for buying from a small shop. Every order helps us keep baking by hand. We taste each batch before it reaches the counter, and we would rather sell out than cut a corner.</p>", "signatureName": "Cara Wells", "signatureRole": "Baker and owner" } },
{ "block_key": "team", "title": "Team", "slot": "main", "sort_order": 3, "content": { "heading": "The people at the bench", "members": [
{ "name": "Cara", "role": "Baker and owner", "bio": "Laminates the pastry and runs the morning bake." },
{ "name": "Jonah", "role": "Pastry chef", "bio": "Owns the tarts and the seasonal fruit." },
{ "name": "Noor", "role": "Front of house", "bio": "Boxes your cake and knows the regulars." }
] } },
{ "block_key": "footer", "title": "Footer", "slot": "footer", "sort_order": 0, "content": { "companyName": "Petal & Crumb", "tagline": "A small patisserie, baked fresh each morning.", "email": "hello@petalandcrumb.test", "copyright": "Petal & Crumb" } }
]
},
{
"slug": "/blog",
"title": "Journal",
"template_key": "blog-index",
"blocks": [
{ "block_key": "navbar", "title": "Navigation", "slot": "header", "sort_order": 0, "content": { "menuName": "main", "companyName": "Petal & Crumb", "ctaLabel": "Order a cake", "ctaUrl": "/contact" } },
{ "block_key": "blog-hero", "title": "Journal Hero", "slot": "featured", "sort_order": 0, "content": { "title": "The Journal", "subtitle": "Notes from the bench and the season." } },
{ "block_key": "card", "title": "Post: Strawberries", "slot": "main", "sort_order": 0, "content": { "title": "Strawberries are in", "text": "The first good strawberries of the season arrived. Here is what we are making with them.", "link": "/blog/strawberries-are-in", "linkLabel": "Read on" } },
{ "block_key": "card", "title": "Post: Lamination", "slot": "main", "sort_order": 1, "content": { "title": "Why we laminate overnight", "text": "Cold dough and slow rests make a better croissant. This is how we do it.", "link": "/blog/why-we-laminate-overnight", "linkLabel": "Read on" } },
{ "block_key": "card", "title": "Post: Custom cakes", "slot": "main", "sort_order": 2, "content": { "title": "How to order a custom cake", "text": "Flavours, sizes and lead times, so your order arrives the way you pictured it.", "link": "/blog/how-to-order-a-custom-cake", "linkLabel": "Read on" } },
{ "block_key": "footer", "title": "Footer", "slot": "footer", "sort_order": 0, "content": { "companyName": "Petal & Crumb", "tagline": "A small patisserie, baked fresh each morning.", "email": "hello@petalandcrumb.test", "copyright": "Petal & Crumb" } }
]
},
{
"slug": "/blog/strawberries-are-in",
"parent_slug": "/blog",
"title": "Strawberries are in",
"template_key": "article",
"blocks": [
{ "block_key": "navbar", "title": "Navigation", "slot": "header", "sort_order": 0, "content": { "menuName": "main", "companyName": "Petal & Crumb", "ctaLabel": "Order a cake", "ctaUrl": "/contact" } },
{ "block_key": "heading", "title": "Title", "slot": "main", "sort_order": 0, "content": { "level": 1, "text": "Strawberries are in" } },
{ "block_key": "text", "title": "Body", "slot": "main", "sort_order": 1, "content": { "text": "<p>The first ripe strawberries came in this week. They are small, sweet, and they do not keep, so we use them fast.</p><p>This week they go into the strawberry tart and a plain sponge with cream. When they run out, we move on to the next fruit.</p><p>Come in early. The tarts tend to go before lunch.</p>" } },
{ "block_key": "pastel-dream:cake_gallery", "title": "Cake Gallery", "slot": "main", "sort_order": 2, "content": { "heading": "What we made this week", "intro": "A few things that came out of the oven.", "currency": "$", "cakes": [
{ "name": "Strawberry tart", "flavour": "Vanilla custard", "price": "8.00", "orderUrl": "/contact" },
{ "name": "Strawberry sponge", "flavour": "Cream and jam", "price": "7.00", "orderUrl": "/contact" },
{ "name": "Fruit galette", "flavour": "Seasonal", "price": "6.50", "orderUrl": "/contact" }
] } },
{ "block_key": "footer", "title": "Footer", "slot": "footer", "sort_order": 0, "content": { "companyName": "Petal & Crumb", "tagline": "A small patisserie, baked fresh each morning.", "email": "hello@petalandcrumb.test", "copyright": "Petal & Crumb" } }
]
},
{
"slug": "/blog/why-we-laminate-overnight",
"parent_slug": "/blog",
"title": "Why we laminate overnight",
"template_key": "article",
"blocks": [
{ "block_key": "navbar", "title": "Navigation", "slot": "header", "sort_order": 0, "content": { "menuName": "main", "companyName": "Petal & Crumb", "ctaLabel": "Order a cake", "ctaUrl": "/contact" } },
{ "block_key": "heading", "title": "Title", "slot": "main", "sort_order": 0, "content": { "level": 1, "text": "Why we laminate overnight" } },
{ "block_key": "text", "title": "Body", "slot": "main", "sort_order": 1, "content": { "text": "<p>A croissant is butter and dough folded many times. The butter has to stay cold, or it melts into the dough and the layers are lost.</p><p>We fold in the afternoon and rest the dough in the fridge overnight. Cold and slow gives an even rise and a light crumb.</p><p>In the morning we shape and bake. That is why the croissants are ready when we open and not before.</p>" } },
{ "block_key": "footer", "title": "Footer", "slot": "footer", "sort_order": 0, "content": { "companyName": "Petal & Crumb", "tagline": "A small patisserie, baked fresh each morning.", "email": "hello@petalandcrumb.test", "copyright": "Petal & Crumb" } }
]
},
{
"slug": "/blog/how-to-order-a-custom-cake",
"parent_slug": "/blog",
"title": "How to order a custom cake",
"template_key": "article",
"blocks": [
{ "block_key": "navbar", "title": "Navigation", "slot": "header", "sort_order": 0, "content": { "menuName": "main", "companyName": "Petal & Crumb", "ctaLabel": "Order a cake", "ctaUrl": "/contact" } },
{ "block_key": "heading", "title": "Title", "slot": "main", "sort_order": 0, "content": { "level": 1, "text": "How to order a custom cake" } },
{ "block_key": "text", "title": "Body", "slot": "main", "sort_order": 1, "content": { "text": "<p>Tell us the date, the size, and the flavour. Send a photo if you have one in mind.</p><p>We need two days for most cakes and a week for larger ones. We will confirm the price before we start.</p><p>Pick up from the shop, or ask about local delivery.</p>" } },
{ "block_key": "pastel-dream:order_ribbon", "title": "Order Ribbon", "slot": "main", "sort_order": 2, "content": { "heading": "Ready to order", "message": "Send us the details and we will reply with a quote.", "pickupNote": "Two days notice, a week for tiered cakes.", "ctaLabel": "Start an order", "ctaUrl": "/contact" } },
{ "block_key": "footer", "title": "Footer", "slot": "footer", "sort_order": 0, "content": { "companyName": "Petal & Crumb", "tagline": "A small patisserie, baked fresh each morning.", "email": "hello@petalandcrumb.test", "copyright": "Petal & Crumb" } }
]
},
{
"slug": "/contact",
"title": "Order",
"template_key": "contact",
"blocks": [
{ "block_key": "navbar", "title": "Navigation", "slot": "header", "sort_order": 0, "content": { "menuName": "main", "companyName": "Petal & Crumb", "ctaLabel": "Order a cake", "ctaUrl": "/contact" } },
{ "block_key": "hours-location", "title": "Hours", "slot": "main", "sort_order": 0, "content": { "heading": "Find us", "address": "18 Rose Lane\nFremantle", "phone": "08 1234 5678", "hours": [
{ "day": "Monday", "closed": true },
{ "day": "Tuesday", "hours": "8:00 - 15:00" },
{ "day": "Wednesday", "hours": "8:00 - 15:00" },
{ "day": "Thursday", "hours": "8:00 - 15:00" },
{ "day": "Friday", "hours": "8:00 - 16:00" },
{ "day": "Saturday", "hours": "8:00 - 16:00" },
{ "day": "Sunday", "hours": "9:00 - 14:00" }
] } },
{ "block_key": "contact-form", "title": "Contact Form", "slot": "main", "sort_order": 1, "content": { "heading": "Send us a note", "description": "Ordering a cake or asking a question? Leave the details and we will get back to you.", "submitLabel": "Send", "successMessage": "Thanks. We will reply soon.", "formConfig": { "targetTable": "contact_submissions" }, "fields": [
{ "name": "name", "label": "Name", "type": "text", "required": true, "width": "half" },
{ "name": "email", "label": "Email", "type": "email", "required": true, "width": "half" },
{ "name": "reason", "label": "Reason", "type": "text", "required": false, "width": "full" },
{ "name": "message", "label": "Message", "type": "textarea", "required": true, "width": "full" }
] } },
{ "block_key": "footer", "title": "Footer", "slot": "footer", "sort_order": 0, "content": { "companyName": "Petal & Crumb", "tagline": "A small patisserie, baked fresh each morning.", "email": "hello@petalandcrumb.test", "copyright": "Petal & Crumb" } }
]
},
{
"slug": "/login",
"title": "Login",
"template_key": "auth",
"blocks": [
{ "block_key": "auth-form", "title": "Auth Form", "slot": "main", "sort_order": 0, "content": { "default_tab": "login", "show_social": false } },
{ "block_key": "text", "title": "Footer Note", "slot": "footer", "sort_order": 0, "content": { "text": "<p>Members hear about seasonal bakes first.</p>" } }
]
}
]
}

25
seed/workflows.json Normal file
View File

@ -0,0 +1,25 @@
{
"workflows": [
{
"key": "notify_admins_on_contact",
"name": "Notify admins on contact",
"description": "Email site admins when someone submits the contact or order form.",
"enabled": true,
"trigger": {
"type": "row_inserted",
"table": "contact_submissions"
},
"steps": [
{
"type": "email",
"name": "Email admins",
"template": "Form Submission Notification",
"recipients": {
"mode": "admins",
"scope": "all"
}
}
]
}
]
}

View File

@ -7,7 +7,7 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>{{ site_name }}</title>
<style type="text/css">
body, table, td, p, a, li {
body, table, td, p, a, li, blockquote {
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
@ -28,66 +28,67 @@
padding: 0 !important;
width: 100% !important;
}
.pastel-email-frame {
max-width: 560px;
margin: 0 auto;
a[x-apple-data-detectors] {
color: inherit !important;
text-decoration: none !important;
}
@media only screen and (max-width: 600px) {
.pastel-email-frame {
h1, h2, h3, h4, h5, h6 {
font-family: "Fraunces", "Playfair Display", Georgia, "Times New Roman", serif;
font-weight: 600;
}
@media only screen and (max-width: 620px) {
.pd-email-container {
width: 100% !important;
max-width: 100% !important;
}
.pastel-pad {
.pd-email-padding {
padding-left: 24px !important;
padding-right: 24px !important;
}
}
</style>
</head>
<body style="background-color: {{ colors.background|default:"#fdf7f1" }}; margin: 0; padding: 0; font-family: &amp;#39;Nunito&amp;#39;, &amp;#39;Quicksand&amp;#39;, Helvetica, Arial, sans-serif;">
{% if preview_text %}<div style="display: none; max-height: 0; overflow: hidden;">
{{ preview_text }}
</div>{% endif %}
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="background-color: {{ colors.background|default:"#fdf7f1" }};">
<body style="background-color: {{ colors.background|default:"#faf4ed" }}; margin: 0; padding: 0; font-family: &amp;#39;Quicksand&amp;#39;, &amp;#39;Nunito&amp;#39;, -apple-system, BlinkMacSystemFont, &amp;#39;Segoe UI&amp;#39;, Roboto, sans-serif;">
{% if preview_text %}<div style="display: none; max-height: 0; overflow: hidden; mso-hide: all;">{{ preview_text }}</div>{% endif %}
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0">
<tr>
<td align="center" style="padding: 40px 12px; position: relative;">
<!-- Watermark blob (top-right). Inline SVG so any client that supports it renders the wash. -->
<div style="position: absolute; top: 0; right: 0; width: 200px; height: 200px; opacity: 0.35; pointer-events: none;">
<svg viewBox="0 0 200 200" width="200" height="200" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path d="M100,20 C160,20 180,80 180,120 C180,170 130,180 90,180 C40,180 20,140 20,100 C20,60 50,20 100,20 Z" fill="{{ colors.primary|default:"#e9a1ad" }}"></path>
<td align="center" style="padding: 40px 10px; background-color: {{ colors.background|default:"#faf4ed" }};">
<table role="presentation" class="pd-email-container" width="560" cellspacing="0" cellpadding="0" border="0" style="max-width: 560px; background-color: {{ colors.card|default:"#fdf9f4" }}; border: 1px solid {{ colors.border|default:"#e5d6c9" }}; border-radius: 4px;">
<tr>
<td align="center" style="padding: 32px 40px 16px; border-bottom: 1px dashed {{ colors.border|default:"#e5d6c9" }};">
{% if logo_url %}<img src="{{ logo_url }}" alt="{{ site_name }}" style="max-height: 48px; width: auto; display: block;">{% elif site_name %}<h1 style="margin: 0; font-size: 24px; letter-spacing: -0.01em; color: {{ colors.primary|default:"#d05a83" }};">{{ site_name }}</h1>{% endif %}
</td>
</tr>
<tr>
<td class="pd-email-padding" style="padding: 32px 40px; color: {{ colors.foreground|default:"#47323c" }}; font-size: 16px; line-height: 1.65;">{{ body|safe }}</td>
</tr>
<tr>
<td align="center" style="padding: 16px 40px 8px;">
<!-- inline SVG doodle divider, survives Outlook -->
<table role="presentation" cellspacing="0" cellpadding="0" border="0"><tr><td>
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="32" viewBox="0 0 200 32" fill="none" stroke="{{ colors.primary|default:"#d05a83" }}" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M4 18 L80 18"></path>
<path d="M120 18 L196 18"></path>
<ellipse cx="95" cy="18" rx="6" ry="9"></ellipse>
<path d="M95 9 L95 27"></path>
<ellipse cx="108" cy="18" rx="6" ry="9"></ellipse>
<path d="M108 9 L108 27"></path>
</svg>
</div>
<table role="presentation" class="pastel-email-frame" width="560" cellspacing="0" cellpadding="0" border="0" style="max-width: 560px; background-color: {{ colors.card|default:"#ffffff" }}; border-radius: 20px; border: 1px solid {{ colors.border|default:"#f0d8de" }};">
<!-- Masthead -->
<tr>
<td align="center" class="pastel-pad" style="padding: 36px 40px 8px; border-bottom: 1px solid {{ colors.border|default:"#f0d8de" }};">
{% if logo_url %}<img src="{{ logo_url }}" alt="{{ site_name }}" style="max-height: 56px; width: auto; display: block;">{% elif site_name %}<h1 style="margin: 0; font-family: &amp;#39;Caveat Brush&amp;#39;, &amp;#39;Sacramento&amp;#39;, cursive; font-size: 36px; font-weight: 400; color: {{ colors.foreground|default:"#432e36" }};">
{{ site_name }}
</h1>{% endif %}
</td></tr></table>
</td>
</tr>
<!-- Body -->
<tr>
<td class="pastel-pad" style="padding: 32px 40px 24px; color: {{ colors.foreground|default:"#432e36" }}; font-size: 16px; line-height: 1.75;">
{{ body|safe }}
<td style="padding: 16px 40px 32px; color: {{ colors.mutedForeground|default:"#79626c" }}; font-size: 12px; line-height: 1.6;">
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0">
<tr>
<td align="center">
{% if site_name %}<p style="margin: 0 0 6px; font-size: 13px; color: {{ colors.foreground|default:"#47323c" }};">{{ site_name }}</p>{% endif %}
<p style="margin: 0 0 6px;">Fresh bakes from the morning. Cakes to order all week.</p>
{% if site_url %}<p style="margin: 0 0 8px;"><a href="{{ site_url }}" style="color: {{ colors.primary|default:"#d05a83" }}; text-decoration: none; border-bottom: 1px dashed {{ colors.primary|default:"#d05a83" }};">{{ site_url }}</a></p>{% endif %}
{% if unsubscribe_url %}<p style="margin: 0; font-size: 11px;"><a href="{{ unsubscribe_url }}" style="color: {{ colors.mutedForeground|default:"#79626c" }}; text-decoration: none;">Unsubscribe</a></p>{% endif %}
</td>
</tr>
<!-- Footer -->
<tr>
<td class="pastel-pad" align="center" style="padding: 24px 40px 36px; border-top: 1px solid {{ colors.border|default:"#f0d8de" }};">
<p style="margin: 0 0 12px; font-family: &amp;#39;Caveat Brush&amp;#39;, &amp;#39;Sacramento&amp;#39;, cursive; font-size: 18px; color: {{ colors.mutedForeground|default:"#80707a" }};">
Be gentle with yourself today.
</p>
{% if site_url %}<p style="margin: 0 0 12px; font-size: 13px; color: {{ colors.mutedForeground|default:"#80707a" }};">
<a href="{{ site_url }}" style="color: {{ colors.primary|default:"#e9a1ad" }}; text-decoration: none;">
{{ site_url }}
</a>
</p>{% endif %}
{% if unsubscribe_url %}<p style="margin: 0; font-size: 11px; color: {{ colors.mutedForeground|default:"#80707a" }};">
<a href="{{ unsubscribe_url }}" style="color: {{ colors.mutedForeground|default:"#80707a" }}; text-decoration: underline;">
Unsubscribe
</a>
</p>{% endif %}
</table>
</td>
</tr>
</table>

View File

@ -0,0 +1,21 @@
{# pastel-dream announcement-bar — dismissable strip; dismissal persists in localStorage keyed by variant+message hash. #}
{% set anncls = "bg-accent/15 text-foreground" %}
{% if variant == "promo" %}{% set anncls = "bg-accent text-accent-foreground" %}{% elif variant == "alert" %}{% set anncls = "bg-destructive text-destructive-foreground" %}{% endif %}
<div data-bn-announcement data-bn-annkey="{{ variant|default:'info' }}:{{ message|slugify }}" class="bn-announcement w-full {{ anncls }} {{ class }}">
<div class="relative mx-auto flex max-w-7xl items-center justify-center gap-2 px-10 py-2 text-center text-sm">
<p class="pd-body font-medium">{{ message }}{% if linkText and linkUrl %} <a href="{{ linkUrl }}" class="underline decoration-dashed underline-offset-2 hover:opacity-80">{{ linkText }}</a>{% endif %}</p>
{% if dismissable %}<button type="button" data-bn-ann-dismiss aria-label="Dismiss announcement" class="absolute right-3 top-1/2 inline-flex -translate-y-1/2 items-center justify-center rounded p-1 transition-colors hover:bg-black/10"><svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg></button>{% endif %}
</div>
</div>
{% if dismissable %}<script>
(function(){
var bars=document.querySelectorAll('[data-bn-announcement]:not([data-bn-ann-wired])');
bars.forEach(function(bar){
bar.setAttribute('data-bn-ann-wired','1');
var key='bnann:'+bar.getAttribute('data-bn-annkey');
try{if(localStorage.getItem(key)==='1'){bar.style.display='none';return;}}catch(e){}
var btn=bar.querySelector('[data-bn-ann-dismiss]');
if(btn)btn.addEventListener('click',function(){bar.style.display='none';try{localStorage.setItem(key,'1');}catch(e){}});
});
})();
</script>{% endif %}

View File

@ -0,0 +1,15 @@
<figure class="bn-audio pd-card my-8 overflow-hidden{% if class %} {{ class }}{% endif %}" data-bn-audio data-audio-url="{{ url|default:"" }}" data-audio-media="{% if media %}{{ media|mediaURL }}{% endif %}"><div class="flex items-center gap-4 p-4">{% if artwork %}<div class="h-16 w-16 shrink-0 overflow-hidden rounded-sm bg-secondary">{% img artwork alt=title|default:"" class="h-full w-full object-cover" %}</div>{% endif %}<div class="min-w-0 flex-1">{% if title %}<p class="pd-display truncate text-lg text-primary">{{ title }}</p>{% endif %}{% if artist %}<p class="pd-hand truncate text-lg text-accent">{{ artist }}</p>{% endif %}<div class="mt-2 flex items-center gap-3" data-bn-audio-slot><button type="button" data-bn-audio-play class="pd-tag text-xs" aria-label="Play{% if title %}: {{ title }}{% endif %}"><svg class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#play"></use></svg> Play</button>{% if download %}<a href="{% if media %}{{ media|mediaURL }}{% else %}{{ url }}{% endif %}" download class="pd-body inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground"><svg class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#download"></use></svg> Download</a>{% endif %}</div></div></div></figure><script>
(function(){
if(window.__bnAudioFacade)return;window.__bnAudioFacade=true;
document.addEventListener('click',function(e){
var btn=e.target.closest('[data-bn-audio-play]');if(!btn)return;
var fig=btn.closest('[data-bn-audio]');if(!fig)return;
e.preventDefault();
var src=fig.getAttribute('data-audio-media')||fig.getAttribute('data-audio-url')||'';
if(!src)return;
var slot=btn.closest('[data-bn-audio-slot]');
var a=document.createElement('audio');a.src=src;a.controls=true;a.autoplay=true;a.preload='none';a.style.cssText='width:100%;min-width:12rem;height:2.25rem;';
btn.remove();slot.insertBefore(a,slot.firstChild);
});
})();
</script>

View File

@ -0,0 +1,32 @@
{# pastel-dream auth-form — combined login/register in soft pastel chrome. Endpoints and panel ids match the platform auth flow (/api/auth/login, /api/auth/register, #auth-message, #login-panel/#register-panel). Auth state via the auth_state provider (auth.logged_in / auth.display_name). Captcha, when enabled, is emitted by the platform auth path. #}
{% if auth.logged_in %}
<div class="auth-form-logged-in pd-card mx-auto max-w-md p-6 text-center">
<p class="pd-body text-muted-foreground">You are logged in as <strong class="text-primary">{{ auth.display_name }}</strong></p>
<a href="/account" class="pd-tag mt-4">My account</a>
</div>
{% else %}
{% set defaultTab = default_tab|default:"login" %}
<div class="auth-form pd-card mx-auto max-w-md p-6">
<div id="auth-message"></div>
<div class="mb-5 flex border-b border-dashed border-border">
<button type="button" onclick="document.getElementById('login-panel').classList.remove('hidden');document.getElementById('register-panel').classList.add('hidden');this.className='pd-display flex-1 py-2 text-center border-b-2 border-primary text-primary';this.nextElementSibling.className='pd-display flex-1 py-2 text-center border-b-2 border-transparent text-muted-foreground'" class="pd-display flex-1 py-2 text-center border-b-2 {% if defaultTab == "register" %}border-transparent text-muted-foreground{% else %}border-primary text-primary{% endif %}">Login</button>
<button type="button" onclick="document.getElementById('register-panel').classList.remove('hidden');document.getElementById('login-panel').classList.add('hidden');this.className='pd-display flex-1 py-2 text-center border-b-2 border-primary text-primary';this.previousElementSibling.className='pd-display flex-1 py-2 text-center border-b-2 border-transparent text-muted-foreground'" class="pd-display flex-1 py-2 text-center border-b-2 {% if defaultTab == "register" %}border-primary text-primary{% else %}border-transparent text-muted-foreground{% endif %}">Register</button>
</div>
<div id="login-panel" class="{% if defaultTab == "register" %}hidden{% endif %}">
<form hx-post="/api/auth/login" hx-target="#auth-message" hx-swap="innerHTML">
<div class="mb-3"><label class="pd-hand mb-1 block text-lg text-accent">Email</label><input type="email" name="email" required class="pd-body w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"></div>
<div class="mb-4"><label class="pd-hand mb-1 block text-lg text-accent">Password</label><input type="password" name="password" required class="pd-body w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"></div>
<button type="submit" class="pd-tag w-full justify-center" hx-disabled-elt="this">Login</button>
<p class="pd-body mt-3 text-center text-sm"><a href="/forgot-password" class="text-accent underline decoration-dashed">Forgot password?</a></p>
</form>
</div>
<div id="register-panel" class="{% if defaultTab == "register" %}{% else %}hidden{% endif %}">
<form hx-post="/api/auth/register" hx-target="#auth-message" hx-swap="innerHTML">
<div class="mb-3"><label class="pd-hand mb-1 block text-lg text-accent">Username</label><input type="text" name="username" required pattern="[a-zA-Z0-9_-]{3,30}" class="pd-body w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"></div>
<div class="mb-3"><label class="pd-hand mb-1 block text-lg text-accent">Email</label><input type="email" name="email" required class="pd-body w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"></div>
<div class="mb-4"><label class="pd-hand mb-1 block text-lg text-accent">Password</label><input type="password" name="password" required minlength="8" class="pd-body w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"></div>
<button type="submit" class="pd-tag w-full justify-center" hx-disabled-elt="this">Create account</button>
</form>
</div>
</div>
{% endif %}

View File

@ -0,0 +1,25 @@
{# pastel-dream auth-status — header login state. Auth state via the auth_state provider; endpoints match the platform (/login, /login?tab=register, /api/auth/logout, /api/auth/resend-verification). #}
{% if not auth.logged_in %}
<div class="auth-status flex items-center gap-2">
<a href="/login" class="pd-body rounded-lg px-3 py-1.5 text-sm font-medium opacity-80 transition-colors hover:bg-muted hover:opacity-100">Login</a>
<a href="/login?tab=register" class="pd-tag text-sm">Sign up</a>
</div>
{% else %}
{% if not auth.email_verified %}<div class="fixed inset-x-0 top-16 z-40 bg-accent px-4 py-2 text-center text-sm text-accent-foreground">Please verify your email address. <button hx-post="/api/auth/resend-verification" hx-swap="outerHTML" class="font-medium underline">Resend verification email</button></div>{% endif %}
<div class="auth-status relative">
<button type="button" onclick="this.nextElementSibling.classList.toggle('hidden')" class="pd-body flex items-center gap-2 rounded-lg px-3 py-1.5 text-sm font-medium opacity-90 transition-colors hover:bg-muted hover:opacity-100">
<span class="pd-hand flex h-7 w-7 items-center justify-center rounded-full bg-accent text-sm text-accent-foreground">{{ auth.display_name|first|upper }}</span>
<span>{{ auth.display_name }}</span>
<svg class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#chevron-down"></use></svg>
</button>
<div id="auth-dropdown-menu" class="pd-card absolute right-0 top-full z-50 mt-1 hidden w-48 py-1 shadow-lg">
<a href="/account" class="pd-body block px-4 py-2 text-sm text-foreground hover:bg-muted">My profile</a>
<a href="/account/reviews" class="pd-body block px-4 py-2 text-sm text-foreground hover:bg-muted">My reviews</a>
<hr class="my-1 border-dashed border-border">
<button hx-post="/api/auth/logout" hx-swap="none" class="pd-body block w-full px-4 py-2 text-left text-sm text-destructive hover:bg-muted">Logout</button>
</div>
</div>
<script>
(function(){document.addEventListener('click',function(e){var menu=document.getElementById('auth-dropdown-menu');var btn=document.querySelector('.auth-status > button');if(!menu||!btn)return;if(!btn.contains(e.target)&&!menu.contains(e.target)){menu.classList.add('hidden');}});})();
</script>
{% endif %}

View File

@ -0,0 +1,20 @@
{% if author %}
<section class="border-b border-dashed border-border bg-background py-12 text-foreground md:py-16{% if class %} {{ class }}{% endif %}">
<div class="mx-auto flex max-w-3xl flex-col items-center px-4 text-center">
{% if showAvatar and author.avatar_url %}<div class="mb-5 h-24 w-24 overflow-hidden rounded-full bg-secondary ring-2 ring-border">{% img author.avatar_url alt=author.display_name class="h-full w-full object-cover" %}</div>{% endif %}
<h1 class="pd-display text-3xl text-primary md:text-4xl">{{ author.display_name }}</h1>
{% if author.job_title or author.company %}<p class="pd-hand mt-1 text-xl text-accent">{{ author.job_title }}{% if author.job_title and author.company %} · {% endif %}{{ author.company }}</p>{% endif %}
{% if author.bio %}<p class="pd-body mt-4 max-w-2xl leading-relaxed text-muted-foreground">{{ author.bio }}</p>{% endif %}
<div class="pd-body mt-5 flex flex-wrap items-center justify-center gap-4 text-sm text-muted-foreground">
{% if showPostCount %}<span>{{ author.post_count|default:0 }} post{% if author.post_count != 1 %}s{% endif %}</span>{% endif %}
{% if author.location %}<span>{{ author.location }}</span>{% endif %}
</div>
{% if showSocial %}<div class="mt-5 flex items-center justify-center gap-3">
{% if author.website_url %}<a href="{{ author.website_url }}" target="_blank" rel="noopener" class="text-muted-foreground transition-colors hover:text-accent" title="Website"><svg class="h-5 w-5" aria-hidden="true"><use href="/icons/lucide.svg#globe"></use></svg></a>{% endif %}
{% if author.twitter_handle %}<a href="https://twitter.com/{{ author.twitter_handle|cut:"@" }}" target="_blank" rel="noopener" class="text-muted-foreground transition-colors hover:text-accent" title="Twitter"><svg class="h-5 w-5" aria-hidden="true"><use href="/icons/simple-icons.svg#x"></use></svg></a>{% endif %}
{% if author.linkedin_url %}<a href="{{ author.linkedin_url }}" target="_blank" rel="noopener" class="text-muted-foreground transition-colors hover:text-accent" title="LinkedIn"><svg class="h-5 w-5" aria-hidden="true"><use href="/icons/simple-icons.svg#linkedin"></use></svg></a>{% endif %}
{% if author.github_url %}<a href="{{ author.github_url }}" target="_blank" rel="noopener" class="text-muted-foreground transition-colors hover:text-accent" title="GitHub"><svg class="h-5 w-5" aria-hidden="true"><use href="/icons/simple-icons.svg#github"></use></svg></a>{% endif %}
</div>{% endif %}
</div>
</section>
{% endif %}

View File

@ -0,0 +1,12 @@
{% if post.author_name %}
<aside class="pd-card p-6{% if class %} {{ class }}{% endif %}">
{% if heading %}<p class="pd-hand mb-4 text-xl text-accent">{{ heading }}</p>{% endif %}
<div class="flex items-start gap-4">
{% if showAvatar and post.author_avatar %}<div class="h-14 w-14 flex-shrink-0 overflow-hidden rounded-full bg-secondary ring-2 ring-border">{% img post.author_avatar alt=post.author_name class="h-full w-full object-cover" %}</div>{% endif %}
<div class="min-w-0">
<h3 class="pd-display text-base text-primary">{% if post.author_slug %}<a href="/author/{{ post.author_slug }}" class="transition-colors hover:text-accent">{{ post.author_name }}</a>{% else %}{{ post.author_name }}{% endif %}</h3>
{% if post.author_bio %}<p class="pd-body mt-1 text-sm text-muted-foreground">{{ post.author_bio }}</p>{% endif %}
</div>
</div>
</aside>
{% endif %}

View File

@ -0,0 +1,22 @@
<section class="bg-background py-12 text-foreground md:py-16{% if class %} {{ class }}{% endif %}">
<div class="mx-auto max-w-6xl px-4">
<div class="max-w-2xl">
{% if title %}<h1 class="pd-display text-4xl text-primary md:text-5xl"><span class="pd-doodle-underline">{{ title }}</span></h1>{% endif %}
{% if subtitle %}<p class="pd-body mt-3 text-lg text-muted-foreground">{{ subtitle }}</p>{% endif %}
</div>
{% if posts %}
<div class="mt-10 grid grid-cols-1 gap-6 md:grid-cols-3">
{% for post in posts %}
<article class="group flex flex-col{% if forloop.First %} md:col-span-3 md:flex-row md:items-stretch md:gap-6{% endif %}">
{% if post.featured_image_url %}<a href="{{ post.url }}" class="pd-frame mb-3 block overflow-hidden rounded-sm p-1.5{% if forloop.First %} md:mb-0 md:w-1/2{% endif %}">{% img post.featured_image_url alt=post.title class="aspect-video h-full w-full rounded-sm object-cover transition-transform duration-300 group-hover:scale-105" %}</a>{% endif %}
<div class="flex flex-col justify-center{% if forloop.First %} md:w-1/2{% endif %}">
<h2 class="pd-display {% if forloop.First %}text-2xl{% else %}text-lg{% endif %} text-primary"><a href="{{ post.url }}" class="transition-colors hover:text-accent">{{ post.title }}</a></h2>
{% if showExcerpt and post.excerpt %}<p class="pd-body mt-2 text-sm text-muted-foreground">{{ post.excerpt }}</p>{% endif %}
<div class="pd-hand mt-3 flex flex-wrap items-center gap-x-3 text-lg text-accent">{% if post.author_name %}<span>{{ post.author_name }}</span>{% endif %}{% if post.published_at %}<span class="text-muted-foreground">{{ post.published_at|date:"M j, Y" }}</span>{% endif %}</div>
</div>
</article>
{% endfor %}
</div>
{% endif %}
</div>
</section>

View File

@ -0,0 +1,12 @@
{% set arrows = template == "arrows" %}
<nav aria-label="Breadcrumb" class="py-3{% if class %} {{ class }}{% endif %}">
<ol class="pd-body flex flex-wrap items-center gap-1 text-sm">
{% if showHome %}<li class="flex items-center"><a href="/" class="text-muted-foreground transition-colors hover:text-accent">{{ homeLabel|default:"Home" }}</a></li>{% endif %}
{% for item in breadcrumbs %}
<li class="flex items-center">
{% if showHome or not forloop.First %}{% if arrows %}<svg class="mx-2 h-4 w-4 text-accent" aria-hidden="true"><use href="/icons/lucide.svg#chevron-right"></use></svg>{% else %}<span class="mx-2 text-accent">{{ separator|default:"/" }}</span>{% endif %}{% endif %}
{% if item.is_current %}{% if showCurrent %}<span class="pd-display text-foreground" aria-current="page">{{ item.label }}</span>{% endif %}{% else %}<a href="{{ item.url }}" class="text-muted-foreground transition-colors hover:text-accent">{{ item.label }}</a>{% endif %}
</li>
{% endfor %}
</ol>
</nav>

View File

@ -1,3 +1,5 @@
<a href="{{ href|default:"#" }}" class="pastel-pill" data-block-type="button" data-variant="{{ variant|default:"primary" }}">
{{ text|default:"Continue" }}
</a>
{% with align=align|default:"left" style=style|default:"primary" %}
<div class="bn-button flex {% if align == "center" %}justify-center{% elif align == "right" %}justify-end{% else %}justify-start{% endif %}{% if class %} {{ class }}{% endif %}">
{% if link %}<a href="{{ link }}"{% if newTab %} target="_blank" rel="noopener"{% endif %} class="pd-tag {% if style == "secondary" %}bg-secondary text-secondary-foreground{% elif style == "outline" or style == "ghost" %}pd-tag--ghost{% elif style == "destructive" %}bg-destructive text-destructive-foreground{% endif %} {% if size == "lg" %}text-base px-6 py-3{% elif size == "sm" %}text-xs px-3 py-1.5{% endif %}">{% if icon and iconPosition != "right" %}<svg class="h-4 w-4" aria-hidden="true"><use href="/icons/{{ icon|split:":"|first }}.svg#{{ icon|split:":"|last }}"></use></svg>{% endif %}<span>{{ label|default:"Learn more" }}</span>{% if icon and iconPosition == "right" %}<svg class="h-4 w-4" aria-hidden="true"><use href="/icons/{{ icon|split:":"|first }}.svg#{{ icon|split:":"|last }}"></use></svg>{% endif %}</a>{% else %}<button type="button" class="pd-tag {% if style == "secondary" %}bg-secondary text-secondary-foreground{% elif style == "outline" or style == "ghost" %}pd-tag--ghost{% elif style == "destructive" %}bg-destructive text-destructive-foreground{% endif %} {% if size == "lg" %}text-base px-6 py-3{% elif size == "sm" %}text-xs px-3 py-1.5{% endif %}">{% if icon and iconPosition != "right" %}<svg class="h-4 w-4" aria-hidden="true"><use href="/icons/{{ icon|split:":"|first }}.svg#{{ icon|split:":"|last }}"></use></svg>{% endif %}<span>{{ label|default:"Learn more" }}</span>{% if icon and iconPosition == "right" %}<svg class="h-4 w-4" aria-hidden="true"><use href="/icons/{{ icon|split:":"|first }}.svg#{{ icon|split:":"|last }}"></use></svg>{% endif %}</button>{% endif %}
</div>
{% endwith %}

View File

@ -1,9 +1,12 @@
<div class="pastel-card p-6" data-block-type="card" style="border-radius: 20px;">
{% if title %}<h3 class="font-display text-2xl mb-3" style="color: hsl(var(--card-foreground));">{{ title }}</h3>{% endif %}
{% if body %}<div class="font-body" style="color: hsl(var(--card-foreground) / 0.85); line-height: 1.75;">
{{ body|safe }}
</div>{% endif %}
{% if footer %}<div class="mt-6 pt-4 border-t font-body text-sm" style="border-color: hsl(var(--border)); color: hsl(var(--muted-foreground));">
{{ footer|safe }}
</div>{% endif %}
{% with layout=layout|default:"vertical" %}
<article class="bn-card pd-card overflow-hidden transition-shadow hover:shadow-md {% if layout == "horizontal" %}flex flex-col sm:flex-row{% endif %}{% if class %} {{ class }}{% endif %}">
{% if link %}<a href="{{ link }}" class="contents">{% endif %}
{% if media %}<div class="overflow-hidden bg-secondary {% if layout == "horizontal" %}sm:w-2/5 sm:shrink-0{% endif %}">{% if layout == "horizontal" %}{% img media alt=title|default:"" class="h-full w-full object-cover" %}{% else %}{% img media alt=title|default:"" class="aspect-video h-full w-full object-cover" %}{% endif %}</div>{% endif %}
<div class="flex flex-col gap-2 p-5 md:p-6 {% if layout == "horizontal" %}justify-center{% endif %}">
{% if title %}<h3 class="pd-display text-xl text-primary">{{ title }}</h3>{% endif %}
{% if text %}<div class="pd-body text-sm leading-relaxed text-muted-foreground">{{ text|safe }}</div>{% endif %}
{% if linkLabel and link %}<span class="pd-hand mt-2 inline-flex items-center gap-1 text-lg text-accent">{{ linkLabel }} <svg class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#arrow-right"></use></svg></span>{% endif %}
</div>
{% if link %}</a>{% endif %}
</article>
{% endwith %}

View File

@ -0,0 +1,17 @@
{% set pills = style == "pills" %}
<section class="bg-background text-foreground{% if class %} {{ class }}{% endif %}">
{% if heading %}<h2 class="pd-hand mb-4 text-2xl text-accent">{{ heading }}</h2>{% endif %}
{% if pills %}
<div class="flex flex-wrap gap-2">
{% for cat in categories %}
<a href="{{ cat.url }}" class="pd-body inline-flex items-center gap-1.5 rounded-full border border-dashed border-border bg-card px-3 py-1 text-sm text-card-foreground transition-colors hover:border-accent hover:text-accent">{{ cat.name }}{% if showCount %}<span class="text-xs text-muted-foreground">{{ cat.post_count }}</span>{% endif %}</a>
{% endfor %}
</div>
{% else %}
<ul class="space-y-1">
{% for cat in categories %}
<li><a href="{{ cat.url }}" class="pd-body flex items-center justify-between rounded-md px-3 py-2 text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"><span>{{ cat.name }}</span>{% if showCount %}<span class="rounded-full bg-muted px-2 py-0.5 text-xs text-muted-foreground">{{ cat.post_count }}</span>{% endif %}</a></li>
{% endfor %}
</ul>
{% endif %}
</section>

View File

@ -0,0 +1,17 @@
<section class="py-16 md:py-24 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div class="mx-auto max-w-4xl px-4">
{% if heading %}<h2 class="pd-display text-3xl text-primary md:text-4xl"><span class="pd-doodle-underline">{{ heading }}</span></h2>{% endif %}
{% if description %}<p class="pd-body mt-3 max-w-2xl text-lg text-muted-foreground">{{ description }}</p>{% endif %}
<div class="mt-10 grid grid-cols-1 gap-4 {% if columns == 3 %}sm:grid-cols-2 lg:grid-cols-3{% elif columns == 1 %}{% else %}sm:grid-cols-2{% endif %}">
{% for channel in channels %}
<div class="pd-card flex items-start gap-4 p-5">
{% if channel.icon %}<span class="inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-accent/15 text-accent"><svg class="h-5 w-5" aria-hidden="true"><use href="/icons/{{ channel.icon|split:":"|first }}.svg#{{ channel.icon|split:":"|last }}"></use></svg></span>{% endif %}
<div class="min-w-0">
{% if channel.label %}<div class="pd-hand text-lg text-accent">{{ channel.label }}</div>{% endif %}
{% if channel.value %}<div class="pd-body mt-0.5 break-words font-medium">{% if channel.link %}<a href="{{ channel.link }}" class="transition-colors hover:text-accent">{{ channel.value }}</a>{% else %}{{ channel.value }}{% endif %}</div>{% endif %}
</div>
</div>
{% endfor %}
</div>
</div>
</section>

View File

@ -0,0 +1,39 @@
{% with bid=_block_id|default:context.blockId %}
<section id="contact-form-{{ bid }}" class="py-16 md:py-24 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div class="mx-auto max-w-2xl px-4">
{% if form.submitted %}
<div class="pd-card pd-torn-top pd-torn-bottom p-8 text-center">
<div class="mx-auto mb-4 inline-flex h-12 w-12 items-center justify-center rounded-full bg-accent/15 text-accent"><svg class="h-6 w-6" aria-hidden="true"><use href="/icons/lucide.svg#check"></use></svg></div>
<p class="pd-display text-xl text-primary">{{ form.message|default:successMessage|default:"Thanks. Your message is on its way." }}</p>
</div>
{% else %}
{% if heading %}<h2 class="pd-display text-3xl text-primary md:text-4xl"><span class="pd-doodle-underline">{{ heading }}</span></h2>{% endif %}
{% if description %}<p class="pd-body mt-3 text-lg text-muted-foreground">{{ description }}</p>{% endif %}
{% if form.errors._form %}<div class="pd-body mt-6 rounded-md border border-destructive/50 bg-destructive/10 px-4 py-3 text-sm text-destructive">{{ form.errors._form }}</div>{% endif %}
{% with formAction="/api/blocks/"|add:bid|add:"/submit" formTarget="#contact-form-"|add:bid %}
{% form action=formAction hx_target=formTarget hx_swap="outerHTML" class="mt-8 grid grid-cols-1 gap-5 sm:grid-cols-2" %}
{% for field in fields %}
<div class="{% if field.width == "half" %}sm:col-span-1{% else %}sm:col-span-2{% endif %}">
{% if field.label %}<label for="cf-{{ bid }}-{{ field.name }}" class="pd-hand mb-1.5 block text-lg text-accent">{{ field.label }}{% if field.required %} <span class="text-destructive">*</span>{% endif %}</label>{% endif %}
{% if field.type == "textarea" %}
<textarea id="cf-{{ bid }}-{{ field.name }}" name="{{ field.name }}" rows="5"{% if field.required %} required{% endif %}{% if field.placeholder %} placeholder="{{ field.placeholder }}"{% endif %} class="pd-body w-full rounded-md border border-input bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"></textarea>
{% elif field.type == "select" %}
<select id="cf-{{ bid }}-{{ field.name }}" name="{{ field.name }}"{% if field.required %} required{% endif %} class="pd-body w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring">
{% if field.placeholder %}<option value="">{{ field.placeholder }}</option>{% endif %}
{% for opt in field.options %}<option value="{{ opt.value }}">{{ opt.label|default:opt.value }}</option>{% endfor %}
</select>
{% else %}
<input id="cf-{{ bid }}-{{ field.name }}" name="{{ field.name }}" type="{{ field.type|default:"text" }}"{% if field.required %} required{% endif %}{% if field.placeholder %} placeholder="{{ field.placeholder }}"{% endif %} class="pd-body w-full rounded-md border border-input bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring">
{% endif %}
</div>
{% endfor %}
<div class="hidden" aria-hidden="true"><label>Leave this field empty<input type="text" name="website" tabindex="-1" autocomplete="off"></label></div>
<div class="sm:col-span-2">
<button type="submit" class="pd-tag">{{ submitLabel|default:"Send message" }}</button>
</div>
{% endform %}
{% endwith %}
{% endif %}
</div>
</section>
{% endwith %}

View File

@ -0,0 +1,42 @@
<section class="py-16 md:py-24 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div class="mx-auto max-w-4xl px-4 text-center">
{% if heading %}<h2 class="pd-display text-3xl text-primary md:text-4xl">{{ heading }}</h2>{% endif %}
{% if intro %}<p class="pd-body mx-auto mt-4 max-w-2xl text-lg text-muted-foreground">{{ intro }}</p>{% endif %}
<div class="mt-10" data-countdown>
<div class="flex items-start justify-center gap-3 sm:gap-6" data-countdown-timer data-target="{{ target }}" role="timer" aria-label="Time remaining">
<div class="flex flex-col items-center {% if variant == "boxes" %}pd-card min-w-[4.5rem] px-3 py-4{% endif %}"><span class="pd-mono text-4xl font-bold tabular-nums text-accent sm:text-5xl" data-unit="days">--</span><span class="pd-hand mt-1 text-lg text-muted-foreground">{{ labelDays|default:"Days" }}</span></div>
<div class="flex flex-col items-center {% if variant == "boxes" %}pd-card min-w-[4.5rem] px-3 py-4{% endif %}"><span class="pd-mono text-4xl font-bold tabular-nums text-accent sm:text-5xl" data-unit="hours">--</span><span class="pd-hand mt-1 text-lg text-muted-foreground">{{ labelHours|default:"Hours" }}</span></div>
<div class="flex flex-col items-center {% if variant == "boxes" %}pd-card min-w-[4.5rem] px-3 py-4{% endif %}"><span class="pd-mono text-4xl font-bold tabular-nums text-accent sm:text-5xl" data-unit="minutes">--</span><span class="pd-hand mt-1 text-lg text-muted-foreground">{{ labelMinutes|default:"Minutes" }}</span></div>
<div class="flex flex-col items-center {% if variant == "boxes" %}pd-card min-w-[4.5rem] px-3 py-4{% endif %}"><span class="pd-mono text-4xl font-bold tabular-nums text-accent sm:text-5xl" data-unit="seconds">--</span><span class="pd-hand mt-1 text-lg text-muted-foreground">{{ labelSeconds|default:"Seconds" }}</span></div>
</div>
<div class="mt-8 hidden" data-countdown-expired>
<p class="pd-display text-xl text-primary">{{ expiredMessage|default:"This has started." }}</p>
{% if expiredUrl and expiredLabel %}<a href="{{ expiredUrl }}" class="pd-tag mt-4 text-base px-5 py-3">{{ expiredLabel }}<svg class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#arrow-right"></use></svg></a>{% endif %}
</div>
</div>
<noscript><p class="pd-body mt-6 text-muted-foreground">Counting down to <time datetime="{{ target }}">{{ target }}</time>.</p></noscript>
</div>
</section>
<script>
(function(){
document.querySelectorAll('[data-countdown-timer]').forEach(function(el){
var target = new Date(el.getAttribute('data-target')).getTime();
if (isNaN(target)) return;
var wrap = el.closest('[data-countdown]');
var expired = wrap ? wrap.querySelector('[data-countdown-expired]') : null;
function pad(n){ return (n < 10 ? '0' : '') + n; }
function set(unit, value){ var node = el.querySelector('[data-unit="' + unit + '"]'); if (node) node.textContent = value; }
var interval;
function tick(){
var diff = target - Date.now();
if (diff <= 0){ el.style.display = 'none'; if (expired) expired.classList.remove('hidden'); if (interval) clearInterval(interval); return; }
set('days', Math.floor(diff / 86400000));
set('hours', pad(Math.floor(diff % 86400000 / 3600000)));
set('minutes', pad(Math.floor(diff % 3600000 / 60000)));
set('seconds', pad(Math.floor(diff % 60000 / 1000)));
}
tick();
interval = setInterval(tick, 1000);
});
})();
</script>

View File

@ -0,0 +1,14 @@
<section class="pd-section bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div class="mx-auto max-w-6xl px-4">
<div class="{% if background == "band" %}pd-torn-top pd-torn-bottom rounded-2xl bg-primary px-6 py-14 text-center text-primary-foreground md:px-12{% elif background == "card" %}pd-card px-6 py-14 text-center md:px-12{% else %}flex flex-col items-center gap-8 rounded-2xl bg-muted px-6 py-12 text-foreground md:flex-row md:justify-between md:px-12 md:text-left{% endif %}">
<div class="{% if background != "split" %}mx-auto max-w-2xl{% endif %}">
{% if heading %}<h2 class="pd-display text-3xl md:text-4xl {% if background == "band" %}text-primary-foreground{% else %}text-primary{% endif %}">{{ heading }}</h2>{% endif %}
{% if text %}<p class="pd-body mt-3 text-lg {% if background == "band" %}text-primary-foreground/80{% else %}text-muted-foreground{% endif %}">{{ text }}</p>{% endif %}
</div>
<div class="flex flex-wrap items-center justify-center gap-3 {% if background != "split" %}mt-8{% else %}shrink-0{% endif %}">
{% if primaryUrl and primaryLabel %}<a href="{{ primaryUrl }}" class="pd-tag text-base px-6 py-3 {% if background == "band" %}bg-background text-foreground{% endif %}">{{ primaryLabel }}<svg class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#arrow-right"></use></svg></a>{% endif %}
{% if secondaryUrl and secondaryLabel %}<a href="{{ secondaryUrl }}" class="pd-tag pd-tag--ghost text-base px-6 py-3 {% if background == "band" %}border-primary-foreground/40 text-primary-foreground{% endif %}">{{ secondaryLabel }}</a>{% endif %}
</div>
</div>
</div>
</section>

View File

@ -0,0 +1,17 @@
{% with variant=variant|default:"line" width=width|default:"full" %}
<div class="bn-divider pd-doodle-draw mx-auto my-8 flex items-center justify-center {% if width == "narrow" %}max-w-[200px]{% elif width == "wide" %}max-w-3xl{% else %}w-full{% endif %}{% if class %} {{ class }}{% endif %}" role="separator">
{% if variant == "ornament" or variant == "spacer-rule" %}
<span class="h-px flex-1 bg-border"></span>
<svg xmlns="http://www.w3.org/2000/svg" width="72" height="26" viewBox="0 0 72 26" fill="none" stroke="hsl(var(--accent))" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" class="mx-4 shrink-0" aria-hidden="true">
<ellipse cx="30" cy="13" rx="6" ry="9"></ellipse><path d="M30 4 L30 22"></path>
<ellipse cx="42" cy="13" rx="6" ry="9"></ellipse><path d="M42 4 L42 22"></path>
<path d="M4 13 L18 13"></path><path d="M54 13 L68 13"></path>
</svg>
<span class="h-px flex-1 bg-border"></span>
{% else %}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 12" preserveAspectRatio="none" class="h-3 w-full" fill="none" stroke="hsl(var(--border))" stroke-width="2" stroke-linecap="round" aria-hidden="true">
<path d="M2 7 Q 40 2 80 7 T 160 6 T 240 7 T 298 6"></path>
</svg>
{% endif %}
</div>
{% endwith %}

View File

@ -0,0 +1,40 @@
<section class="py-16 md:py-24 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div class="mx-auto max-w-6xl px-4">
{% if heading %}<h2 class="pd-display text-3xl text-primary md:text-4xl"><span class="pd-doodle-underline">{{ heading }}</span></h2>{% endif %}
{% if intro %}<p class="pd-body mt-3 max-w-2xl text-lg text-muted-foreground">{{ intro }}</p>{% endif %}
{% if layout == "cards" %}
<div class="mt-10 grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
{% for event in events %}{% if not (hidePast and event.past) %}
<article class="pd-card flex flex-col overflow-hidden{% if event.past %} opacity-60{% endif %}">
{% if event.media %}<div class="aspect-video overflow-hidden bg-secondary">{% img event.media alt=event.title class="h-full w-full object-cover" %}</div>{% endif %}
<div class="flex flex-1 flex-col gap-2 p-5">
<div class="pd-hand text-lg text-accent"><time{% if event.datetime %} datetime="{{ event.datetime }}"{% endif %}>{{ event.start }}{% if event.end %} &ndash; {{ event.end }}{% endif %}</time>{% if event.timezone %} <span class="text-muted-foreground">{{ event.timezone }}</span>{% endif %}</div>
<h3 class="pd-display text-lg text-primary">{{ event.title }}</h3>
{% if event.venue %}<p class="pd-body text-sm text-muted-foreground">{{ event.venue }}</p>{% endif %}
{% if event.description %}<p class="pd-body text-sm text-muted-foreground">{{ event.description }}</p>{% endif %}
{% if event.link %}<a href="{{ event.link }}" class="pd-hand mt-auto inline-flex items-center gap-1 pt-2 text-lg text-accent hover:underline">{{ event.linkLabel|default:"Details" }}<svg class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#arrow-right"></use></svg></a>{% endif %}
</div>
</article>
{% endif %}{% endfor %}
</div>
{% else %}
<ul class="mt-10 divide-y divide-dashed divide-border border-y border-dashed border-border">
{% for event in events %}{% if not (hidePast and event.past) %}
<li class="flex flex-col gap-3 py-5 sm:flex-row sm:items-center sm:gap-6{% if event.past %} opacity-60{% endif %}">
<div class="shrink-0 sm:w-52">
<div class="pd-display text-sm text-primary"><time{% if event.datetime %} datetime="{{ event.datetime }}"{% endif %}>{{ event.start }}</time></div>
{% if event.end %}<div class="pd-body text-xs text-muted-foreground">to {{ event.end }}</div>{% endif %}
{% if event.timezone %}<div class="pd-body text-xs text-muted-foreground">{{ event.timezone }}</div>{% endif %}
</div>
<div class="min-w-0 flex-1">
<h3 class="pd-display text-base text-primary">{{ event.title }}</h3>
{% if event.venue %}<p class="pd-body text-sm text-muted-foreground">{{ event.venue }}</p>{% endif %}
{% if event.description %}<p class="pd-body mt-1 text-sm text-muted-foreground">{{ event.description }}</p>{% endif %}
</div>
{% if event.link %}<a href="{{ event.link }}" class="pd-tag pd-tag--ghost shrink-0 text-xs">{{ event.linkLabel|default:"Details" }}</a>{% endif %}
</li>
{% endif %}{% endfor %}
</ul>
{% endif %}
</div>
</section>

View File

@ -0,0 +1,17 @@
<section class="py-16 md:py-24 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div class="mx-auto max-w-3xl px-4">
{% if heading %}<h2 class="pd-display text-center text-3xl text-primary md:text-4xl">{{ heading }}</h2>{% endif %}
{% if intro %}<p class="pd-body mx-auto mt-4 max-w-2xl text-center text-lg text-muted-foreground">{{ intro }}</p>{% endif %}
<div class="mt-10 divide-y divide-dashed divide-border border-y border-dashed border-border">
{% for item in items %}
<details class="group py-2"{% if variant == "open-list" %} open{% endif %}>
<summary class="pd-display flex cursor-pointer list-none items-center justify-between gap-4 py-3 text-lg text-primary marker:content-none [&::-webkit-details-marker]:hidden">
<span>{{ item.question }}</span>
<svg class="h-5 w-5 shrink-0 text-accent transition-transform duration-200 group-open:rotate-180" aria-hidden="true"><use href="/icons/lucide.svg#chevron-down"></use></svg>
</summary>
<div class="pd-body pb-4 text-muted-foreground [&_a]:text-accent [&_a]:underline [&_a]:decoration-dashed">{{ item.answer|safe }}</div>
</details>
{% endfor %}
</div>
</div>
</section>

View File

@ -0,0 +1,17 @@
<section class="py-16 md:py-24 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div class="mx-auto max-w-6xl px-4">
{% if eyebrow %}<p class="pd-hand text-center text-2xl text-accent">{{ eyebrow }}</p>{% endif %}
{% if heading %}<h2 class="pd-display mt-1 text-center text-3xl text-primary md:text-4xl"><span class="pd-doodle-underline">{{ heading }}</span></h2>{% endif %}
{% if intro %}<p class="pd-body mx-auto mt-4 max-w-2xl text-center text-lg text-muted-foreground">{{ intro }}</p>{% endif %}
<div class="mt-12 pd-grid pd-grid--2 {% if columns == 2 %}pd-grid--lg-2{% elif columns == 4 %}pd-grid--lg-4{% else %}pd-grid--3{% endif %}">
{% for item in items %}
<div class="{% if layout == "card" %}pd-card p-6 transition-shadow hover:shadow-md{% elif layout == "centered" %}text-center{% endif %}">
{% if item.icon %}<div class="mb-4 inline-flex h-11 w-11 items-center justify-center rounded-full bg-accent/15 text-accent{% if layout == "centered" %} mx-auto{% endif %}"><svg class="h-6 w-6" aria-hidden="true"><use href="/icons/{{ item.icon|split:":"|first }}.svg#{{ item.icon|split:":"|last }}"></use></svg></div>{% endif %}
<h3 class="pd-display text-xl text-primary">{% if item.linkUrl %}<a href="{{ item.linkUrl }}" class="transition-colors hover:text-accent">{{ item.title }}</a>{% else %}{{ item.title }}{% endif %}</h3>
{% if item.text %}<p class="pd-body mt-2 text-muted-foreground">{{ item.text }}</p>{% endif %}
{% if item.linkUrl and item.linkLabel %}<a href="{{ item.linkUrl }}" class="pd-hand mt-3 inline-flex items-center gap-1 text-lg text-accent hover:underline">{{ item.linkLabel }}<svg class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#arrow-right"></use></svg></a>{% endif %}
</div>
{% endfor %}
</div>
</div>
</section>

View File

@ -0,0 +1,21 @@
<section class="py-12 md:py-16 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div class="mx-auto max-w-6xl px-4">
{% if heading %}<h2 class="pd-display mb-8 text-2xl text-primary md:text-3xl"><span class="pd-doodle-underline">{{ heading }}</span></h2>{% endif %}
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 {% if columns == 2 %}lg:grid-cols-2{% elif columns == 4 %}lg:grid-cols-4{% else %}lg:grid-cols-3{% endif %}">
{% for post in posts %}
<article class="pd-card group flex flex-col overflow-hidden transition-shadow hover:shadow-md">
{% if showFeaturedImage and post.featured_image_url %}<a href="{{ post.url }}" class="block aspect-video overflow-hidden bg-secondary">{% img post.featured_image_url alt=post.title class="h-full w-full object-cover transition-transform duration-300 group-hover:scale-105" %}</a>{% endif %}
<div class="flex flex-1 flex-col p-5">
<h3 class="pd-display text-lg text-primary"><a href="{{ post.url }}" class="transition-colors hover:text-accent">{{ post.title }}</a></h3>
{% if showExcerpt and post.excerpt %}<p class="pd-body mt-2 flex-1 text-sm text-muted-foreground">{{ post.excerpt }}</p>{% endif %}
<div class="pd-hand mt-4 flex flex-wrap items-center gap-x-3 gap-y-1 text-base text-accent">
{% if showAuthor and post.author_name %}<span>{{ post.author_name }}</span>{% endif %}
{% if showDate and post.published_at %}<span class="text-muted-foreground">{{ post.published_at|date:"M j, Y" }}</span>{% endif %}
{% if post.reading_time %}<span class="text-muted-foreground">{{ post.reading_time }} min</span>{% endif %}
</div>
</div>
</article>
{% endfor %}
</div>
</div>
</section>

View File

@ -0,0 +1,38 @@
{# pastel-dream footer — scalloped top edge, soft pastel chrome, builtin footer_menus provider. #}
{% set fl = layout %}
{% if not fl %}{% if template == "centered" %}{% set fl = "centered" %}{% else %}{% set fl = "columns" %}{% endif %}{% endif %}
{% macro sociallinks(cls) %}{% if social_items %}<div class="flex items-center gap-3 {{ cls }}">{% for s in social_items %}<a href="{{ s.url }}" target="_blank" rel="noopener" title="{{ s.label }}" class="text-muted-foreground transition-colors hover:text-accent"><span class="sr-only">{{ s.label }}</span>{% if s.icon %}<svg class="h-5 w-5" aria-hidden="true"><use href="/icons/{{ s.icon|split:":"|first }}.svg#{{ s.icon|split:":"|last }}"></use></svg>{% else %}{{ s.label }}{% endif %}</a>{% endfor %}</div>{% endif %}{% endmacro %}
{% macro newsletter() %}{% if showNewsletter %}<div class="max-w-sm"><h3 class="pd-hand text-2xl text-accent">{{ newsletterTitle|default:"Stay in the loop" }}</h3>{% if newsletterDescription %}<p class="pd-body mt-1 text-sm text-muted-foreground">{{ newsletterDescription }}</p>{% endif %}<div class="mt-3">{% signup_form list=newsletterListSlug|default:"main" source="footer" submit=newsletterButtonText|default:"Subscribe" %}</div></div>{% endif %}{% endmacro %}
{% macro legalbar() %}<div class="mt-10 flex flex-col items-center justify-between gap-3 border-t border-dashed border-border pt-6 text-sm text-muted-foreground sm:flex-row"><p class="pd-body">{% if copyright %}&copy; {{ copyright }}{% else %}&copy; {{ companyName }}{% endif %}</p>{% if legal_items %}<nav class="flex flex-wrap items-center gap-x-4 gap-y-1">{% for l in legal_items %}<a href="{{ l.url|default:'#' }}"{% if l.open_in_new_tab %} target="_blank" rel="noopener"{% endif %} class="pd-body transition-colors hover:text-foreground">{{ l.label }}</a>{% endfor %}</nav>{% endif %}</div>{% endmacro %}
<footer class="pd-torn-top w-full bg-muted/50 text-foreground {{ class }}">
<div class="{% if fullWidth %}w-full px-4 sm:px-6 lg:px-8{% else %}mx-auto max-w-7xl px-4 sm:px-6 lg:px-8{% endif %} py-16">
{% if fl == "minimal" %}
<div class="flex flex-col items-center justify-between gap-6 sm:flex-row">
<div class="flex items-center gap-3">{% if logo %}<img src="{{ logo }}" alt="{{ companyName }}" class="h-8 w-auto"/>{% elif companyName %}<span class="pd-display text-xl text-primary">{{ companyName }}</span>{% endif %}{% if tagline %}<span class="pd-hand hidden text-lg text-accent sm:inline">{{ tagline }}</span>{% endif %}</div>
{{ sociallinks("") }}
</div>
{{ legalbar() }}
{% elif fl == "centered" %}
<div class="flex flex-col items-center gap-5 text-center">
{% if logo %}<img src="{{ logo }}" alt="{{ companyName }}" class="h-10 w-auto"/>{% elif companyName %}<span class="pd-display text-2xl text-primary">{{ companyName }}</span>{% endif %}
{% if tagline %}<p class="pd-hand max-w-md text-xl text-accent">{{ tagline }}</p>{% endif %}
{% if columns %}<nav class="flex flex-wrap items-center justify-center gap-x-6 gap-y-2">{% for col in columns %}{% for item in col.items %}<a href="{{ item.url|default:'#' }}"{% if item.open_in_new_tab %} target="_blank" rel="noopener"{% endif %} class="pd-body text-sm text-muted-foreground transition-colors hover:text-foreground">{{ item.label }}</a>{% endfor %}{% endfor %}</nav>{% endif %}
{{ newsletter() }}
{{ sociallinks("justify-center") }}
</div>
{{ legalbar() }}
{% else %}
<div class="grid grid-cols-2 gap-8 md:grid-cols-4 lg:grid-cols-5">
<div class="col-span-2 lg:col-span-2">
{% if logo %}<img src="{{ logo }}" alt="{{ companyName }}" class="h-9 w-auto"/>{% elif companyName %}<span class="pd-display text-2xl text-primary">{{ companyName }}</span>{% endif %}
{% if tagline %}<p class="pd-hand mt-2 max-w-xs text-xl text-accent">{{ tagline }}</p>{% endif %}
{% if address or email or phone %}<address class="pd-body mt-4 space-y-1 text-sm not-italic text-muted-foreground">{% if address %}<div>{{ address }}</div>{% endif %}{% if email %}<div><a href="mailto:{{ email }}" class="transition-colors hover:text-foreground">{{ email }}</a></div>{% endif %}{% if phone %}<div><a href="tel:{{ phone }}" class="transition-colors hover:text-foreground">{{ phone }}</a></div>{% endif %}</address>{% endif %}
{{ sociallinks("mt-5") }}
</div>
{% for col in columns %}<div><h3 class="pd-hand text-xl text-accent">{{ col.header }}</h3><ul class="mt-3 space-y-2">{% for item in col.items %}<li><a href="{{ item.url|default:'#' }}"{% if item.open_in_new_tab %} target="_blank" rel="noopener"{% endif %} class="pd-body text-sm text-muted-foreground transition-colors hover:text-foreground">{{ item.label }}</a></li>{% endfor %}</ul></div>{% endfor %}
{% if showNewsletter %}<div class="col-span-2 lg:col-span-1">{{ newsletter() }}</div>{% endif %}
</div>
{{ legalbar() }}
{% endif %}
</div>
</footer>

View File

@ -0,0 +1,9 @@
{% with layout=layout|default:"grid" gap=gap|default:"md" cols=columns|default:3 %}
<div class="bn-gallery my-8{% if class %} {{ class }}{% endif %}" data-bn-gallery data-lightbox="{% if lightbox %}true{% else %}false{% endif %}">
<div class="{% if layout == "carousel" %}flex snap-x snap-mandatory overflow-x-auto pb-2 {% if gap == "sm" %}gap-2{% elif gap == "lg" %}gap-6{% else %}gap-4{% endif %}{% elif layout == "masonry" %}columns-2 {% if cols == 4 %}md:columns-4{% elif cols == 2 %}md:columns-2{% else %}md:columns-3{% endif %} {% if gap == "sm" %}gap-2{% elif gap == "lg" %}gap-6{% else %}gap-4{% endif %}{% else %}grid grid-cols-2 {% if cols == 4 %}md:grid-cols-4{% elif cols == 2 %}md:grid-cols-2{% else %}md:grid-cols-3{% endif %} {% if gap == "sm" %}gap-2{% elif gap == "lg" %}gap-6{% else %}gap-4{% endif %}{% endif %}">
{% for image in images %}
<a href="{{ image.src|mediaURL }}" data-bn-gallery-item data-caption="{{ image.caption|default:"" }}" class="group relative block overflow-hidden rounded-sm pd-frame p-1.5 bg-card focus:outline-none focus-visible:ring-2 focus-visible:ring-ring {% if layout == "carousel" %}w-64 shrink-0 snap-start sm:w-80{% elif layout == "masonry" %}mb-4 w-full break-inside-avoid{% endif %}">{% if layout == "grid" %}{% img image.src alt=image.alt|default:"" class="aspect-square w-full rounded-sm object-cover" %}{% else %}{% img image.src alt=image.alt|default:"" class="h-auto w-full rounded-sm" %}{% endif %}{% if image.caption %}<span class="sr-only">{{ image.caption }}</span>{% endif %}</a>
{% endfor %}
</div>
</div>
{% endwith %}

View File

@ -1 +1,6 @@
{% if level|integer == 1 %}<h1 class="font-display text-5xl md:text-6xl leading-tight brush-underline {{ textClass }}" style="color: hsl(var(--foreground));">{{ text }}</h1>{% elif level|integer == 3 %}<h3 class="font-body text-3xl font-semibold tracking-tight {{ textClass }}" style="color: hsl(var(--foreground));">{{ text }}</h3>{% elif level|integer == 4 %}<h4 class="font-body text-2xl font-semibold {{ textClass }}" style="color: hsl(var(--foreground));">{{ text }}</h4>{% elif level|integer == 5 %}<h5 class="font-body text-xl font-medium {{ textClass }}" style="color: hsl(var(--foreground));">{{ text }}</h5>{% elif level|integer == 6 %}<h6 class="font-body text-lg font-medium {{ textClass }}" style="color: hsl(var(--foreground));">{{ text }}</h6>{% else %}<h2 class="font-display text-4xl md:text-5xl leading-tight brush-underline {{ textClass }}" style="color: hsl(var(--foreground));">{{ text }}</h2>{% endif %}
{% if level == 1 %}<h1 class="pd-display text-4xl md:text-5xl leading-tight text-primary flex-1 {{ textClass }}">{{ text }}</h1>
{% elif level == 3 %}<h3 class="pd-display text-2xl text-primary flex-1 {{ textClass }}"><span class="pd-doodle-underline">{{ text }}</span></h3>
{% elif level == 4 %}<h4 class="pd-display text-xl text-primary flex-1 {{ textClass }}">{{ text }}</h4>
{% elif level == 5 %}<h5 class="pd-display text-lg font-medium text-primary flex-1 {{ textClass }}">{{ text }}</h5>
{% elif level == 6 %}<h6 class="pd-hand text-xl text-accent flex-1 {{ textClass }}">{{ text }}</h6>
{% else %}<h2 class="pd-display text-3xl md:text-4xl text-primary flex-1 {{ textClass }}"><span class="pd-doodle-underline">{{ text }}</span></h2>{% endif %}

View File

@ -0,0 +1,36 @@
{# pastel-dream hero — watercolor chrome; styled-text fields accept {text} object OR bare string. #}
{% set eyebrowText = eyebrow.text|default:eyebrow %}
{% set headlineText = headline.text|default:headline %}
{% set subText = subheadline.text|default:subheadline %}
{% set descText = description.text|default:description %}
{% set pText = ctaPrimary.text %}
{% set pUrl = ctaPrimary.url %}
{% set sText = ctaSecondary.text %}
{% set sUrl = ctaSecondary.url %}
{% set variant = template|default:"centered" %}
{% set hasImage = backgroundImage %}
{% set showOverlay = hasImage and overlayColor != "none" and overlayColor != "" %}
{% macro herocontent(align) %}
{% if eyebrowText %}<span class="pd-hand mb-2 inline-block text-2xl text-accent">{{ eyebrowText }}</span>{% endif %}
{% if headlineText %}<h1 class="pd-display text-4xl leading-tight text-primary sm:text-5xl lg:text-6xl">{{ headlineText }}</h1>{% endif %}
{% if subText %}<h2 class="pd-body mt-4 max-w-2xl text-xl text-muted-foreground sm:text-2xl{% if align == "center" %} mx-auto{% endif %}">{{ subText }}</h2>{% endif %}
{% if descText %}<p class="pd-body mt-4 max-w-xl leading-relaxed text-muted-foreground{% if align == "center" %} mx-auto{% endif %}">{{ descText }}</p>{% endif %}
{% if pText or sText %}<div class="mt-8 flex flex-col gap-4 sm:flex-row{% if align == "center" %} justify-center{% endif %}">
{% if pText and pUrl %}<a href="{{ pUrl }}" class="pd-tag text-base px-6 py-3">{{ pText }}</a>{% endif %}
{% if sText and sUrl %}<a href="{{ sUrl }}" class="pd-tag pd-tag--ghost text-base px-6 py-3">{{ sText }}</a>{% endif %}
</div>{% endif %}
{% if trustLogos %}<div class="mt-10"><p class="pd-hand mb-3 text-lg text-accent">Regulars love</p><div class="flex flex-wrap items-center gap-6{% if align == "center" %} justify-center{% endif %}">{% for logo in trustLogos %}{% if logo.url %}<a href="{{ logo.url }}" target="_blank" rel="noopener" class="opacity-60 transition-opacity hover:opacity-100">{% img logo.image alt=logo.alt class="h-8 w-auto" %}</a>{% else %}{% img logo.image alt=logo.alt class="h-8 w-auto opacity-60" %}{% endif %}{% endfor %}</div></div>{% endif %}
{% endmacro %}
{% if variant == "split" %}
<section class="hero grid grid-cols-1 overflow-hidden bg-background text-foreground lg:grid-cols-2{% if class %} {{ class }}{% endif %}">
<div class="relative min-h-[320px] bg-secondary {% if mediaPosition == "left" %}order-1 lg:order-1{% else %}order-1 lg:order-2{% endif %}">{% if hasImage %}{% img backgroundImage alt=headlineText class="absolute inset-0 h-full w-full object-cover" layout="hero" %}{% endif %}</div>
<div class="flex flex-col justify-center px-6 py-16 lg:px-12 {% if mediaPosition == "left" %}order-2 lg:order-2{% else %}order-2 lg:order-1{% endif %}"><div class="max-w-lg text-left">{{ herocontent("left") }}</div></div>
</section>
{% else %}
{% set align = "center" %}
{% if variant == "left-aligned" %}{% set align = "left" %}{% endif %}
<section class="hero relative flex flex-col overflow-hidden bg-background text-foreground {% if minHeight == "screen" %}min-h-screen{% elif minHeight == "50vh" %}min-h-[50vh]{% elif minHeight == "auto" %}min-h-[400px]{% else %}min-h-[75vh]{% endif %} {% if verticalAlign == "top" %}justify-start pt-20{% elif verticalAlign == "bottom" %}justify-end pb-20{% else %}justify-center{% endif %}{% if class %} {{ class }}{% endif %}">
{% if hasImage %}<div class="absolute inset-0 z-0">{% img backgroundImage alt=headlineText class="h-full w-full object-cover" layout="hero" %}{% if showOverlay %}<div class="absolute inset-0 {% if overlayColor == "light" %}bg-background/60{% elif overlayColor == "primary" %}bg-primary/50{% elif overlayOpacity <= 30 %}bg-black/30{% elif overlayOpacity <= 50 %}bg-black/50{% elif overlayOpacity <= 70 %}bg-black/70{% else %}bg-black/80{% endif %}"></div>{% endif %}</div>{% endif %}
<div class="relative z-10 mx-auto w-full max-w-4xl px-4 sm:px-6 lg:px-8 {% if variant == "left-aligned" %}text-left{% else %}text-center{% endif %}">{{ herocontent(align) }}</div>
</section>
{% endif %}

View File

@ -0,0 +1,40 @@
<section class="pd-section bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div class="mx-auto max-w-5xl px-4">
{% if heading %}<h2 class="pd-display text-3xl text-primary md:text-4xl"><span class="pd-doodle-underline">{{ heading }}</span></h2>{% endif %}
<div class="mt-10 pd-grid pd-grid--2">
<div>
{% if hours %}
<table class="pd-body w-full text-sm" data-pd-hours>
<tbody class="divide-y divide-dashed divide-border">
{% for row in hours %}
<tr data-day="{{ row.day }}">
<th scope="row" class="pd-display py-2 pr-4 text-left text-base text-primary">{{ row.day }}</th>
<td class="py-2 text-right {% if row.closed %}text-muted-foreground{% else %}text-foreground{% endif %} tabular-nums">{% if row.closed %}Closed{% else %}{{ row.hours }}{% endif %}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{% if specialHoursNote %}<p class="pd-body mt-4 rounded-md border border-dashed border-border bg-muted px-3 py-2 text-sm text-muted-foreground">{{ specialHoursNote }}</p>{% endif %}
{% if address %}<div class="mt-6"><div class="pd-hand text-lg text-accent">Find us</div><address class="pd-body mt-1 whitespace-pre-line not-italic">{{ address }}</address></div>{% endif %}
{% if phone %}<div class="mt-4"><div class="pd-hand text-lg text-accent">Call ahead</div><a href="tel:{{ phone }}" class="pd-body mt-1 inline-block font-medium transition-colors hover:text-accent">{{ phone }}</a></div>{% endif %}
{% if directionsUrl %}<a href="{{ directionsUrl }}" target="_blank" rel="noopener" class="pd-tag mt-6 text-sm"><svg class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#navigation"></use></svg>Get directions</a>{% endif %}
</div>
{% if mapEmbedUrl %}
<div class="pd-frame aspect-square w-full overflow-hidden rounded-sm bg-secondary md:aspect-auto">
<button type="button" data-map-src="{{ mapEmbedUrl }}" onclick="var f=document.createElement('iframe');f.src=this.getAttribute('data-map-src');f.setAttribute('class','h-full w-full border-0');f.setAttribute('loading','lazy');f.setAttribute('referrerpolicy','no-referrer-when-downgrade');f.setAttribute('title','Map');this.parentNode.replaceChild(f,this);" class="pd-body flex h-full min-h-[16rem] w-full items-center justify-center gap-2 text-sm font-medium transition-colors hover:bg-muted"><svg class="h-5 w-5 pd-pin" aria-hidden="true"><use href="/icons/lucide.svg#map-pin"></use></svg>{{ mapButtonLabel|default:"Load map" }}</button>
</div>
{% endif %}
</div>
</div>
</section>
<script>
(function(){
var days=['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'];
var today=days[new Date().getDay()];
document.querySelectorAll('[data-pd-hours] tr[data-day]').forEach(function(tr){
var d=(tr.getAttribute('data-day')||'').trim();
if(d.toLowerCase().indexOf(today.toLowerCase())===0){tr.querySelector('th').classList.add('pd-hours-today');}
});
})();
</script>

View File

@ -0,0 +1,8 @@
{% with size=size|default:"full" align=align|default:"center" %}
<figure class="my-8 flex flex-1 flex-col {% if align == "left" %}items-start{% elif align == "right" %}items-end{% else %}items-center{% endif %}{% if class %} {{ class }}{% endif %}">
<div class="pd-frame pd-torn-bottom relative overflow-hidden p-2 {% if size == "xs" %}max-w-[200px] w-full{% elif size == "sm" %}max-w-[400px] w-full{% elif size == "md" %}max-w-[600px] w-full{% elif size == "lg" %}max-w-[800px] w-full{% else %}w-full{% endif %}">
{% if src %}{% img src alt=alt class="block h-auto w-full rounded-sm object-cover" layout=size %}{% else %}<div class="flex h-48 w-full items-center justify-center pd-hand text-2xl text-muted-foreground">Add an image</div>{% endif %}
</div>
{% if caption %}<figcaption class="pd-hand mt-3 text-center text-xl text-accent">{{ caption }}</figcaption>{% endif %}
</figure>
{% endwith %}

View File

@ -0,0 +1,15 @@
<section class="py-12 md:py-16 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div class="mx-auto max-w-6xl px-4">
{% if heading %}<p class="pd-hand text-center text-2xl text-accent">{{ heading }}</p>{% endif %}
<div class="mt-8 {% if marquee %}group relative overflow-hidden [mask-image:linear-gradient(to_right,transparent,black_8%,black_92%,transparent)]{% endif %}">
<div class="flex flex-wrap items-center justify-center gap-x-10 gap-y-6 {% if marquee %}flex-nowrap [animation:logo-marquee_30s_linear_infinite] motion-reduce:[animation:none] group-hover:[animation-play-state:paused]{% endif %}">
{% for logo in logos %}
{% if logo.url %}<a href="{{ logo.url }}" class="block shrink-0 opacity-80 transition hover:opacity-100">{% endif %}
<span class="inline-flex h-8 items-center{% if grayscale %} grayscale transition-[filter] hover:grayscale-0{% endif %}">{% img logo.image alt=logo.alt class="h-8 w-auto object-contain" %}</span>
{% if logo.url %}</a>{% endif %}
{% endfor %}
</div>
</div>
</div>
</section>
{% if marquee %}<style>@keyframes logo-marquee{from{transform:translateX(0)}to{transform:translateX(-50%)}}</style>{% endif %}

View File

@ -0,0 +1,31 @@
<section class="pd-section bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div class="mx-auto max-w-5xl px-4">
{% if heading %}<h2 class="pd-display text-3xl text-primary md:text-4xl"><span class="pd-doodle-underline">{{ heading }}</span></h2>{% endif %}
{% if intro %}<p class="pd-body mt-3 max-w-2xl text-lg text-muted-foreground">{{ intro }}</p>{% endif %}
<div class="pd-chalkboard mt-10 p-6 md:p-10">
<div class="pd-menu-cols {% if columns == 2 %}pd-menu-cols--2{% endif %}">
{% for section in sections %}
<div class="menu-section">
{% if section.title %}<h3 class="pd-hand border-b pd-chalk-rule pb-2 text-3xl">{{ section.title }}</h3>{% endif %}
{% if section.note %}<p class="pd-body pd-chalk-muted mt-2 text-sm">{{ section.note }}</p>{% endif %}
<ul class="pd-menu-list">
{% for item in section.items %}
<li class="flex items-baseline gap-4">
<div class="min-w-0 flex-1">
<div class="flex flex-wrap items-baseline gap-x-2 gap-y-1">
<span class="pd-display text-lg">{{ item.name }}</span>
{% for badge in item.badges %}{% if badge.label %}<span class="pd-hand rounded-full border pd-chalk-rule px-2 py-0.5 text-sm pd-chalk-muted">{{ badge.label }}</span>{% endif %}{% endfor %}
</div>
{% if item.description %}<p class="pd-body pd-chalk-muted mt-0.5 text-sm">{{ item.description }}</p>{% endif %}
</div>
{% if item.price %}<span class="pd-mono shrink-0 tabular-nums">{% if currency %}{{ currency }}{% endif %}{{ item.price }}</span>{% endif %}
</li>
{% endfor %}
</ul>
</div>
{% endfor %}
</div>
{% if footnote %}<p class="pd-body pd-chalk-muted mt-10 text-xs">{{ footnote }}</p>{% endif %}
</div>
</div>
</section>

View File

@ -0,0 +1,74 @@
{# pastel-dream navbar — paper-strip chrome over the builtin menu provider. One navbar per page (fixed drawer ids). #}
{% set nv = variant %}
{% if not nv %}{% if template == "centered" %}{% set nv = "centered-logo" %}{% else %}{% set nv = "standard" %}{% endif %}{% endif %}
{% set sb = stickyBehavior %}
{% if not sb %}{% if sticky %}{% set sb = "sticky" %}{% else %}{% set sb = "none" %}{% endif %}{% endif %}
{% set cta_text = cta.text|default:ctaLabel %}
{% set cta_url = cta.url|default:ctaUrl %}
{% macro navlink(item) %}<a href="{{ item.url|default:'#' }}"{% if item.open_in_new_tab %} target="_blank" rel="noopener"{% endif %}{% if item.title %} title="{{ item.title }}"{% endif %} class="pd-body px-3 py-2 text-sm font-medium text-foreground/80 transition-colors hover:text-accent {{ item.css_class }}">{{ item.label }}</a>{% endmacro %}
{% macro dropdown(item) %}<div class="relative group"><button type="button" class="pd-body inline-flex items-center gap-1 px-3 py-2 text-sm font-medium text-foreground/80 transition-colors hover:text-accent">{{ item.label }}<svg class="h-4 w-4 transition-transform group-hover:rotate-180" aria-hidden="true"><use href="/icons/lucide.svg#chevron-down"></use></svg></button><div class="absolute left-0 z-50 mt-1 w-56 invisible opacity-0 transition-all duration-200 group-hover:visible group-hover:opacity-100"><div class="pd-card py-2 shadow-lg">{% for child in item.children %}{% if child.item_type == "header" %}<div class="pd-hand px-4 py-2 text-base text-accent">{{ child.label }}</div>{% else %}<a href="{{ child.url|default:'#' }}"{% if child.open_in_new_tab %} target="_blank" rel="noopener"{% endif %} class="pd-body block px-4 py-2 text-sm text-foreground transition-colors hover:bg-muted {{ child.css_class }}">{{ child.label }}</a>{% endif %}{% endfor %}</div></div></div>{% endmacro %}
{% macro brand(size) %}<a href="/" class="flex flex-shrink-0 items-center gap-2">{% if logoType == "text" and logoText.text %}<span class="pd-display text-2xl text-primary {{ logoText.color }}">{{ logoText.text }}</span>{% elif logo %}<img src="{{ logo }}" alt="{{ alt|default:companyName }}" class="{{ size }} w-auto"{% if logoMaxWidth or logoMaxHeight %} style="{% if logoMaxWidth %}max-width:{{ logoMaxWidth }};{% endif %}{% if logoMaxHeight %}max-height:{{ logoMaxHeight }};{% endif %}"{% endif %}/>{% elif companyName %}<span class="pd-display text-2xl text-primary">{{ companyName }}</span>{% endif %}</a>{% endmacro %}
{% macro ctabtn(extra) %}{% if cta_text %}<a href="{{ cta_url|default:'#' }}" class="pd-tag text-sm {{ extra }}">{{ cta_text }}</a>{% endif %}{% endmacro %}
<nav data-bn-navbar data-bn-sticky="{{ sb }}" class="bn-navbar w-full {% if sb == "sticky" or sb == "shrink-on-scroll" %}sticky top-0 z-40 {% endif %}bg-background/95 backdrop-blur border-b border-dashed border-border {{ class }}">
{% if showUtilityBar %}<div class="w-full border-b border-border/60 bg-muted/60 text-xs text-muted-foreground {% if not showOnMobile %}hidden sm:block{% endif %}"><div class="mx-auto flex h-8 max-w-7xl items-center justify-between px-4 sm:px-6 lg:px-8"><div class="pd-body">{{ utilityBarText|safe }}</div><div class="flex items-center gap-4">{% if utilityBarPhone %}<a href="tel:{{ utilityBarPhone }}" class="hover:text-foreground">{{ utilityBarPhone }}</a>{% endif %}{% if utilityBarEmail %}<a href="mailto:{{ utilityBarEmail }}" class="hover:text-foreground">{{ utilityBarEmail }}</a>{% endif %}</div></div></div>{% endif %}
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div class="bn-navbar-row flex h-16 items-center justify-between">
{% if nv == "centered-logo" %}
<div class="hidden flex-1 items-center gap-1 md:flex">{% for item in menu.items %}{% if item.children %}{{ dropdown(item) }}{% else %}{{ navlink(item) }}{% endif %}{% endfor %}</div>
<div class="flex items-center justify-center">{{ brand("h-9") }}</div>
<div class="hidden flex-1 items-center justify-end gap-3 md:flex">{{ ctabtn("") }}</div>
{% else %}
<div class="flex items-center">{{ brand("h-9") }}</div>
<div class="hidden items-center gap-1 md:flex">{% for item in menu.items %}{% if item.children %}{{ dropdown(item) }}{% else %}{{ navlink(item) }}{% endif %}{% endfor %}{{ ctabtn("ml-3") }}</div>
{% endif %}
<div class="flex items-center md:hidden">
<label for="bn-navbar-toggle" data-bn-nav-open aria-controls="bn-navbar-drawer" aria-expanded="false" class="inline-flex cursor-pointer items-center justify-center rounded-md p-2 transition-colors hover:bg-muted"><span class="sr-only">Open menu</span><svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/></svg></label>
</div>
</div>
</div>
<input type="checkbox" id="bn-navbar-toggle" class="bn-navbar-toggle" aria-hidden="true"/>
<label for="bn-navbar-toggle" class="bn-navbar-overlay fixed inset-0 z-40 bg-black/50 md:hidden" aria-hidden="true"></label>
<div id="bn-navbar-drawer" class="bn-navbar-drawer fixed right-0 top-0 z-50 flex h-full w-80 max-w-[85vw] flex-col bg-background text-foreground shadow-xl md:hidden" role="dialog" aria-modal="true" aria-label="Site menu">
<div class="flex items-center justify-between border-b border-dashed border-border p-4">{{ brand("h-8") }}<label for="bn-navbar-toggle" data-bn-nav-close class="cursor-pointer rounded-md p-2 text-muted-foreground hover:bg-muted hover:text-foreground"><span class="sr-only">Close menu</span><svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg></label></div>
<nav class="flex-1 overflow-y-auto py-4" aria-label="Mobile">{% for item in menu.items %}{% if item.children %}<details class="group/acc"><summary class="pd-body flex cursor-pointer list-none items-center justify-between px-6 py-3 text-base font-medium transition-colors hover:bg-muted">{{ item.label }}<svg class="h-4 w-4 transition-transform group-open/acc:rotate-180" aria-hidden="true"><use href="/icons/lucide.svg#chevron-down"></use></svg></summary><div class="pb-2 pl-8">{% for child in item.children %}{% if child.item_type != "header" %}<a href="{{ child.url|default:'#' }}"{% if child.open_in_new_tab %} target="_blank" rel="noopener"{% endif %} class="pd-body block px-2 py-2 text-sm text-muted-foreground transition-colors hover:text-foreground">{{ child.label }}</a>{% endif %}{% endfor %}</div></details>{% else %}<a href="{{ item.url|default:'#' }}"{% if item.open_in_new_tab %} target="_blank" rel="noopener"{% endif %} class="pd-body block px-6 py-3 text-base font-medium transition-colors hover:bg-muted">{{ item.label }}</a>{% endif %}{% endfor %}</nav>
{% if cta_text %}<div class="border-t border-dashed border-border p-4"><a href="{{ cta_url|default:'#' }}" class="pd-tag w-full justify-center">{{ cta_text }}</a></div>{% endif %}
</div>
<style>
.bn-navbar-toggle{position:absolute;width:1px;height:1px;opacity:0;pointer-events:none;}
.bn-navbar-drawer{width:20rem;max-width:85vw;transform:translateX(100%);transition:transform .3s ease;}
.bn-navbar-overlay{opacity:0;pointer-events:none;transition:opacity .3s ease;}
.bn-navbar-toggle:checked ~ .bn-navbar-drawer{transform:translateX(0);}
.bn-navbar-toggle:checked ~ .bn-navbar-overlay{opacity:1;pointer-events:auto;}
.bn-navbar-drawer details > summary::-webkit-details-marker{display:none;}
</style>
<script>
(function(){
if(window.__bnPastelNavInit)return;window.__bnPastelNavInit=1;
function ready(fn){if(document.readyState!='loading')fn();else document.addEventListener('DOMContentLoaded',fn);}
ready(function(){
document.querySelectorAll('[data-bn-navbar]').forEach(function(nav){
var toggle=nav.querySelector('.bn-navbar-toggle');
var drawer=nav.querySelector('.bn-navbar-drawer');
var opener=nav.querySelector('[data-bn-nav-open]');
if(!toggle||!drawer)return;
function focusables(){return drawer.querySelectorAll('a[href],button,label[for],input,summary,[tabindex]:not([tabindex="-1"])');}
toggle.addEventListener('change',function(){
var open=toggle.checked;
document.documentElement.style.overflow=open?'hidden':'';
document.body.style.overflow=open?'hidden':'';
if(opener)opener.setAttribute('aria-expanded',open?'true':'false');
if(open){var f=focusables();if(f.length)f[0].focus();}else if(opener&&opener.focus)opener.focus();
});
drawer.addEventListener('keydown',function(e){
if(e.key==='Escape'){toggle.checked=false;toggle.dispatchEvent(new Event('change'));return;}
if(e.key!=='Tab')return;
var f=focusables();if(!f.length)return;
var first=f[0],last=f[f.length-1];
if(e.shiftKey&&document.activeElement===first){e.preventDefault();last.focus();}
else if(!e.shiftKey&&document.activeElement===last){e.preventDefault();first.focus();}
});
drawer.querySelectorAll('a[href]').forEach(function(a){a.addEventListener('click',function(){toggle.checked=false;toggle.dispatchEvent(new Event('change'));});});
});
});
})();
</script>

View File

@ -0,0 +1,9 @@
<section class="py-12 bg-background text-foreground md:py-16{% if class %} {{ class }}{% endif %}">
<div class="mx-auto max-w-3xl px-4">
<div class="pd-torn-top pd-torn-bottom rounded-2xl {% if layout == "inline" %}bg-primary px-6 py-10 text-primary-foreground md:px-12{% else %}pd-card px-6 py-10 md:px-10{% endif %} text-center">
{% if title %}<h2 class="pd-display text-2xl md:text-3xl {% if layout == "inline" %}text-primary-foreground{% else %}text-primary{% endif %}">{{ title }}</h2>{% endif %}
{% if description %}<p class="pd-body mx-auto mt-3 max-w-xl {% if layout == "inline" %}text-primary-foreground/80{% else %}text-muted-foreground{% endif %}">{{ description }}</p>{% endif %}
<div class="mx-auto mt-6 max-w-md">{% signup_form list=listSlug source=source submit=submitText success_message=successMessage placeholder=placeholder %}</div>
</div>
</div>
</section>

View File

@ -0,0 +1,20 @@
{# pastel-dream page-suggestions — themed 404 helper. Suggestions (when the platform provides them) list {url|slug, title, excerpt}; otherwise a friendly hand-drawn "lost your way" panel with a route home. #}
<section class="py-16 md:py-24 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div class="mx-auto max-w-3xl px-4 text-center">
<p class="pd-hand text-3xl text-accent">Lost your way?</p>
<h2 class="pd-display mt-1 text-3xl text-primary md:text-4xl">{{ title|default:"Were you looking for..." }}</h2>
{% if suggestions %}
<ul class="mt-8 space-y-3 text-left">
{% for s in suggestions %}
<li class="pd-card p-4">
<a href="{{ s.url|default:s.slug }}" class="pd-display text-lg text-primary transition-colors hover:text-accent">{{ s.title }}</a>
{% if showDescription and s.excerpt %}<p class="pd-body mt-1 text-sm text-muted-foreground">{{ s.excerpt }}</p>{% endif %}
</li>
{% endfor %}
</ul>
{% else %}
<p class="pd-body mx-auto mt-4 max-w-xl text-muted-foreground">{{ emptyMessage|default:"That page has wandered off. Head back to the counter and start again." }}</p>
{% endif %}
<a href="/" class="pd-tag mt-8"><svg class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#home"></use></svg>Back home</a>
</div>
</section>

View File

@ -0,0 +1,13 @@
{# pastel-dream password-reset-form — request panel in soft pastel chrome. Endpoint matches the platform (/api/auth/request-password-reset, #reset-message). #}
<div class="password-reset-form pd-card mx-auto max-w-md p-6">
<div id="reset-message"></div>
<div id="request-reset-panel">
<h2 class="pd-display text-2xl text-primary"><span class="pd-doodle-underline">Reset password</span></h2>
<p class="pd-body mt-2 text-muted-foreground">Enter your email address and we will send you a link to reset your password.</p>
<form hx-post="/api/auth/request-password-reset" hx-target="#reset-message" hx-swap="innerHTML" class="mt-4">
<div class="mb-4"><label class="pd-hand mb-1 block text-lg text-accent">Email</label><input type="email" name="email" required class="pd-body w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"></div>
<button type="submit" class="pd-tag w-full justify-center" hx-disabled-elt="this">Send reset link</button>
</form>
<p class="pd-body mt-4 text-center text-sm"><a href="/login" class="text-accent underline decoration-dashed">Back to login</a></p>
</div>
</div>

View File

@ -0,0 +1,15 @@
<section class="bg-background text-foreground{% if class %} {{ class }}{% endif %}">
{% if heading %}<h2 class="pd-hand mb-4 text-2xl text-accent">{{ heading }}</h2>{% endif %}
<ol class="space-y-4">
{% for post in posts %}
<li class="flex items-start gap-3">
{% if showRank %}<span class="pd-display mt-0.5 text-2xl text-accent/60">{{ forloop.Counter }}</span>{% endif %}
{% if showThumbnail and post.featured_image_url %}<a href="{{ post.url }}" class="pd-frame h-12 w-12 flex-shrink-0 overflow-hidden rounded-sm p-0.5">{% img post.featured_image_url alt=post.title class="h-full w-full rounded-sm object-cover" %}</a>{% endif %}
<div class="min-w-0">
<h3 class="pd-display text-sm text-primary"><a href="{{ post.url }}" class="transition-colors hover:text-accent">{{ post.title }}</a></h3>
{% if post.published_at %}<p class="pd-body mt-0.5 text-xs text-muted-foreground">{{ post.published_at|date:"M j, Y" }}</p>{% endif %}
</div>
</li>
{% endfor %}
</ol>
</section>

View File

@ -0,0 +1,19 @@
{% set center = align != "left" %}
{% if post.featured_image_url %}
<section class="hero relative flex min-h-[50vh] flex-col justify-end overflow-hidden bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div class="absolute inset-0 z-0">{% img post.featured_image_url alt=post.featured_image_alt class="h-full w-full object-cover" layout="hero" %}<div class="absolute inset-0 bg-black/60"></div></div>
<div class="relative z-10 mx-auto w-full max-w-4xl px-4 py-12 sm:px-6 lg:px-8 {% if center %}text-center{% endif %}">
<h1 class="pd-display text-3xl leading-tight text-white sm:text-4xl lg:text-5xl">{{ post.title }}</h1>
{% if showExcerpt and post.excerpt %}<p class="pd-body mt-4 max-w-2xl text-lg text-white/80{% if center %} mx-auto{% endif %}">{{ post.excerpt }}</p>{% endif %}
{% if showMeta %}<div class="pd-hand mt-6 flex flex-wrap items-center gap-x-3 gap-y-1 text-lg text-white/90{% if center %} justify-center{% endif %}">{% if post.author_name %}<span>{{ post.author_name }}</span>{% endif %}{% if post.published_at %}<span>{{ post.published_at|date:"F j, Y" }}</span>{% endif %}{% if post.reading_time %}<span>{{ post.reading_time }} min</span>{% endif %}</div>{% endif %}
</div>
</section>
{% else %}
<section class="bg-background py-14 text-foreground md:py-20{% if class %} {{ class }}{% endif %}">
<div class="mx-auto w-full max-w-3xl px-4 {% if center %}text-center{% endif %}">
<h1 class="pd-display text-3xl leading-tight text-primary sm:text-4xl lg:text-5xl">{{ post.title }}</h1>
{% if showExcerpt and post.excerpt %}<p class="pd-body mt-4 text-lg text-muted-foreground{% if center %} mx-auto max-w-2xl{% endif %}">{{ post.excerpt }}</p>{% endif %}
{% if showMeta %}<div class="pd-hand mt-6 flex flex-wrap items-center gap-x-3 gap-y-1 text-lg text-accent{% if center %} justify-center{% endif %}">{% if post.author_name %}<span>{{ post.author_name }}</span>{% endif %}{% if post.published_at %}<span class="text-muted-foreground">{{ post.published_at|date:"F j, Y" }}</span>{% endif %}{% if post.reading_time %}<span class="text-muted-foreground">{{ post.reading_time }} min</span>{% endif %}</div>{% endif %}
</div>
</section>
{% endif %}

View File

@ -0,0 +1,8 @@
{% if post %}
<div class="pd-body flex flex-wrap items-center gap-x-4 gap-y-2 text-sm text-muted-foreground{% if class %} {{ class }}{% endif %}">
{% if showAuthor and post.author_name %}<span class="flex items-center gap-2">{% if showAvatar and post.author_avatar %}{% img post.author_avatar alt=post.author_name class="h-7 w-7 rounded-full object-cover" %}{% endif %}<span class="pd-display text-foreground">{{ post.author_name }}</span></span>{% endif %}
{% if showDate and post.published_at %}<time datetime="{{ post.published_at|date:"Y-m-d" }}">{{ post.published_at|date:"F j, Y" }}</time>{% endif %}
{% if showReadingTime and post.reading_time %}<span>{{ post.reading_time }} min read</span>{% endif %}
{% if post.categories %}<span class="flex flex-wrap gap-1.5">{% for cat in post.categories %}<a href="{{ cat.url|default:'#' }}" class="rounded-full bg-muted px-2 py-0.5 text-xs text-muted-foreground transition-colors hover:text-accent">{{ cat.name }}</a>{% endfor %}</span>{% endif %}
</div>
{% endif %}

View File

@ -0,0 +1,24 @@
<section class="py-16 md:py-24 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div class="mx-auto max-w-6xl px-4">
{% if heading %}<h2 class="pd-display text-center text-3xl text-primary md:text-4xl">{{ heading }}</h2>{% endif %}
{% if intro %}<p class="pd-body mx-auto mt-4 max-w-2xl text-center text-lg text-muted-foreground">{{ intro }}</p>{% endif %}
<div class="mt-12 grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
{% for tier in tiers %}
<div class="pd-card relative flex flex-col p-6 {% if tier.highlighted %}ring-2 ring-accent{% endif %}">
{% if tier.badge %}<span class="pd-hand absolute -top-4 left-1/2 -translate-x-1/2 rotate-[-3deg] rounded-full bg-accent px-4 py-1 text-lg text-accent-foreground shadow-sm">{{ tier.badge }}</span>{% endif %}
<h3 class="pd-display text-xl text-primary">{{ tier.name }}</h3>
<p class="mt-4 flex items-baseline gap-1">
<span class="pd-display text-4xl text-accent">{% if currency %}{{ currency }}{% else %}${% endif %}{{ tier.price }}</span>
{% if tier.period %}<span class="pd-body text-sm text-muted-foreground">{{ tier.period }}</span>{% endif %}
</p>
{% if tier.description %}<p class="pd-body mt-3 text-sm text-muted-foreground">{{ tier.description }}</p>{% endif %}
{% if tier.features %}<ul class="mt-6 flex-1 space-y-3 text-sm">
{% for feature in tier.features %}<li class="pd-body flex items-start gap-2"><svg class="mt-0.5 h-4 w-4 shrink-0 text-accent" aria-hidden="true"><use href="/icons/lucide.svg#check"></use></svg><span>{{ feature }}</span></li>{% endfor %}
</ul>{% endif %}
{% if tier.ctaUrl and tier.ctaLabel %}<a href="{{ tier.ctaUrl }}" class="pd-tag mt-8 w-full justify-center {% if not tier.highlighted %}pd-tag--ghost{% endif %}">{{ tier.ctaLabel }}</a>{% endif %}
</div>
{% endfor %}
</div>
{% if billingNote %}<p class="pd-body mt-8 text-center text-sm text-muted-foreground">{{ billingNote }}</p>{% endif %}
</div>
</section>

View File

@ -0,0 +1,6 @@
{% with style=style|default:"block" align=align|default:"left" %}
<figure class="bn-quote my-8 {% if style == "inline-card" %}pd-card p-6 md:p-8{% elif style == "pull" %}border-l-4 border-accent pl-6 md:pl-8 py-2{% else %}border-l-2 border-dashed border-border pl-5 py-1{% endif %}{% if align == "center" %} text-center{% endif %}{% if class %} {{ class }}{% endif %}">
<blockquote{% if cite %} cite="{{ cite }}"{% endif %} class="pd-display {% if style == "pull" %}text-2xl md:text-3xl leading-snug{% else %}text-xl md:text-2xl leading-relaxed{% endif %} text-primary">{{ quote|safe }}</blockquote>
{% if attribution or role or cite %}<figcaption class="pd-hand mt-4 text-xl text-accent{% if align == "center" %} flex flex-col items-center{% endif %}">{% if attribution %}<span>{{ attribution }}</span>{% endif %}{% if role %}<span class="{% if attribution %}block {% endif %}text-lg text-muted-foreground">{{ role }}</span>{% endif %}{% if cite %}<a href="{{ cite }}" class="{% if attribution or role %}mt-1 {% endif %}pd-body inline-block text-sm underline decoration-dashed underline-offset-2 hover:text-foreground">Source</a>{% endif %}</figcaption>{% endif %}
</figure>
{% endwith %}

View File

@ -0,0 +1,16 @@
{% if posts %}
<section class="border-t border-dashed border-border py-12 md:py-16 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div class="mx-auto max-w-6xl px-4">
<h2 class="pd-display mb-8 text-2xl text-primary">{{ heading|default:"More from the counter" }}</h2>
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 {% if columns == 2 %}lg:grid-cols-2{% else %}lg:grid-cols-3{% endif %}">
{% for post in posts %}
<article class="group flex flex-col">
{% if post.featured_image_url %}<a href="{{ post.url }}" class="pd-frame mb-3 block aspect-video overflow-hidden rounded-sm p-1.5">{% img post.featured_image_url alt=post.title class="h-full w-full rounded-sm object-cover transition-transform duration-300 group-hover:scale-105" %}</a>{% endif %}
<h3 class="pd-display text-base text-primary"><a href="{{ post.url }}" class="transition-colors hover:text-accent">{{ post.title }}</a></h3>
{% if post.published_at %}<p class="pd-hand mt-1 text-base text-accent">{{ post.published_at|date:"M j, Y" }}</p>{% endif %}
</article>
{% endfor %}
</div>
</div>
</section>
{% endif %}

View File

@ -0,0 +1,13 @@
<section class="py-16 md:py-24 {% if background == "muted" %}bg-muted text-foreground{% elif background == "card" %}bg-card text-card-foreground{% else %}bg-background text-foreground{% endif %}{% if class %} {{ class }}{% endif %}">
<div class="mx-auto grid max-w-6xl items-center gap-10 px-4 md:grid-cols-2 md:gap-16">
<div class="{% if mediaPosition == "left" %}md:order-2{% else %}md:order-1{% endif %}">
{% if eyebrow %}<p class="pd-hand text-2xl text-accent">{{ eyebrow }}</p>{% endif %}
{% if heading %}<h2 class="pd-display mt-1 text-3xl text-primary md:text-4xl"><span class="pd-doodle-underline">{{ heading }}</span></h2>{% endif %}
{% if body %}<div class="pd-body mt-4 space-y-4 text-lg text-muted-foreground [&_a]:text-accent [&_a]:underline [&_a]:decoration-dashed [&_strong]:text-primary">{{ body|safe }}</div>{% endif %}
{% if ctaUrl and ctaLabel %}<a href="{{ ctaUrl }}" class="pd-tag mt-6 text-base px-5 py-3">{{ ctaLabel }}<svg class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#arrow-right"></use></svg></a>{% endif %}
</div>
<div class="{% if mediaPosition == "left" %}md:order-1{% else %}md:order-2{% endif %}">
{% if media %}<div class="pd-frame pd-torn-bottom overflow-hidden rounded-sm p-2">{% img media alt=mediaAlt class="h-auto w-full rounded-sm object-cover" %}</div>{% endif %}
</div>
</div>
</section>

View File

@ -0,0 +1 @@
{% if html %}<article class="rich-text pd-body prose prose-lg max-w-none text-foreground [&_h1]:pd-display [&_h2]:pd-display [&_h3]:pd-display [&_h1]:text-primary [&_h2]:text-primary [&_h3]:text-primary [&_a]:text-accent [&_a]:decoration-dashed [&_blockquote]:border-l-2 [&_blockquote]:border-accent [&_blockquote]:italic">{{ html|safe }}</article>{% endif %}

View File

@ -0,0 +1,12 @@
{% set compact = template == "compact" %}
<nav class="flex-1 {% if compact %}w-56 py-4 pr-3{% else %}w-64 py-6 pr-4{% endif %}{% if showBorder %} border-r border-dashed border-border{% endif %}{% if class %} {{ class }}{% endif %}">
{% if title %}<h3 class="pd-hand {% if compact %}px-2 mb-3 text-lg{% else %}px-3 mb-4 text-xl{% endif %} text-accent">{{ title }}</h3>{% endif %}
<ul class="{% if compact %}space-y-0.5{% else %}space-y-1{% endif %}">
{% for item in menu.items %}
<li><a href="{{ item.url|default:'#' }}"{% if item.open_in_new_tab %} target="_blank" rel="noopener"{% endif %}{% if item.title %} title="{{ item.title }}"{% endif %} data-bn-sidebar-link class="pd-body flex items-center rounded-md text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground {% if compact %}px-2 py-1.5{% else %}px-3 py-2{% endif %} {{ item.css_class }}">{% if item.icon %}<svg class="mr-2 h-4 w-4" aria-hidden="true"><use href="/icons/{{ item.icon|split:":"|first }}.svg#{{ item.icon|split:":"|last }}"></use></svg>{% endif %}{{ item.label }}</a></li>
{% endfor %}
</ul>
</nav>
{% if highlightCurrent %}<script>
(function(){var p=window.location.pathname;document.querySelectorAll('[data-bn-sidebar-link]').forEach(function(a){if(a.getAttribute('href')===p){a.classList.remove('text-muted-foreground');a.classList.add('text-foreground','bg-muted','font-medium');}});})();
</script>{% endif %}

View File

@ -0,0 +1,13 @@
<div class="social-links{% if class %} {{ class }}{% endif %}">
{% if heading %}<p class="pd-hand mb-3 text-xl text-accent">{{ heading }}</p>{% endif %}
<ul class="flex {% if style == "stack" %}flex-col gap-2{% else %}flex-wrap items-center gap-3{% endif %}">
{% for link in links %}{% if link.url %}
<li>
<a href="{{ link.url }}" target="_blank" rel="noopener" aria-label="{{ link.label|default:link.network }}" class="inline-flex items-center gap-2 rounded-full border border-dashed border-border text-foreground transition-colors hover:border-accent hover:text-accent {% if style == "stack" %}w-full px-4 py-2{% else %}p-2.5{% endif %}">
{% if link.network %}<svg class="h-5 w-5" aria-hidden="true"><use href="/icons/{{ link.network|split:":"|first }}.svg#{{ link.network|split:":"|last }}"></use></svg>{% endif %}
{% if style == "stack" and link.label %}<span class="pd-body text-sm font-medium">{{ link.label }}</span>{% endif %}
</a>
</li>
{% endif %}{% endfor %}
</ul>
</div>

View File

@ -0,0 +1,44 @@
<section class="py-16 md:py-24 bg-background text-foreground{% if class %} {{ class }}{% endif %}"{% if countUp %} data-stats{% endif %}>
<div class="mx-auto max-w-6xl px-4">
{% if heading %}<h2 class="pd-display text-center text-3xl text-primary md:text-4xl">{{ heading }}</h2>{% endif %}
{% if intro %}<p class="pd-body mx-auto mt-4 max-w-2xl text-center text-lg text-muted-foreground">{{ intro }}</p>{% endif %}
<dl class="mt-12 grid grid-cols-2 gap-8 {% if columns == 2 %}md:grid-cols-2{% elif columns == 3 %}md:grid-cols-3{% else %}md:grid-cols-4{% endif %}">
{% for item in items %}
<div class="text-center">
<dd class="pd-display text-4xl text-accent md:text-5xl">{% if item.prefix %}{{ item.prefix }}{% endif %}<span{% if countUp %} data-countup-value="{{ item.value }}"{% endif %}>{{ item.value }}</span>{% if item.suffix %}{{ item.suffix }}{% endif %}</dd>
{% if item.label %}<dt class="pd-hand mt-2 text-xl text-muted-foreground">{{ item.label }}</dt>{% endif %}
</div>
{% endfor %}
</dl>
</div>
</section>
{% if countUp %}<script>
(function(){
var sections = document.querySelectorAll('[data-stats]');
var reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
function animate(section){
section.querySelectorAll('[data-countup-value]').forEach(function(node){
var raw = node.getAttribute('data-countup-value') || '';
var target = parseFloat(raw.replace(/[^0-9.\-]/g, ''));
if (isNaN(target)) return;
var decimals = (raw.split('.')[1] || '').length;
var start = null, dur = 1200;
function step(ts){
if (start === null) start = ts;
var p = Math.min((ts - start) / dur, 1);
var eased = 1 - Math.pow(1 - p, 3);
node.textContent = (target * eased).toFixed(decimals);
if (p < 1) requestAnimationFrame(step); else node.textContent = target.toFixed(decimals);
}
requestAnimationFrame(step);
});
}
sections.forEach(function(section){
if (reduce || !('IntersectionObserver' in window)) return;
var io = new IntersectionObserver(function(entries){
entries.forEach(function(e){ if (e.isIntersecting){ animate(section); io.disconnect(); } });
}, { threshold: 0.3 });
io.observe(section);
});
})();
</script>{% endif %}

View File

@ -0,0 +1,21 @@
<section class="py-16 md:py-24 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div class="mx-auto max-w-6xl px-4">
{% if heading %}<h2 class="pd-display text-center text-3xl text-primary md:text-4xl"><span class="pd-doodle-underline">{{ heading }}</span></h2>{% endif %}
{% if intro %}<p class="pd-body mx-auto mt-4 max-w-2xl text-center text-lg text-muted-foreground">{{ intro }}</p>{% endif %}
<div class="mt-12 {% if layout == "list" %}mx-auto max-w-3xl space-y-6{% elif layout == "single" %}mx-auto max-w-xl{% else %}grid grid-cols-1 gap-8 sm:grid-cols-2 {% if columns == 2 %}lg:grid-cols-2{% elif columns == 4 %}lg:grid-cols-4{% else %}lg:grid-cols-3{% endif %}{% endif %}">
{% for member in members %}
<div class="{% if layout == "list" %}pd-card flex items-start gap-5 p-5{% else %}text-center{% endif %}">
{% if member.photo %}<span class="{% if layout == "list" %}h-20 w-20{% else %}mx-auto h-28 w-28{% endif %} block shrink-0 overflow-hidden rounded-full bg-secondary ring-2 ring-border">{% img member.photo alt=member.name class="h-full w-full object-cover" %}</span>{% endif %}
<div class="{% if layout != "list" %}mt-4{% endif %}">
{% if member.name %}<h3 class="pd-display text-lg text-primary">{{ member.name }}</h3>{% endif %}
{% if member.role %}<p class="pd-hand text-lg text-accent">{{ member.role }}</p>{% endif %}
{% if member.bio %}<p class="pd-body mt-2 text-sm text-muted-foreground">{{ member.bio }}</p>{% endif %}
{% if member.socials %}<div class="mt-3 flex gap-3 {% if layout != "list" %}justify-center{% endif %}">
{% for social in member.socials %}{% if social.url %}<a href="{{ social.url }}" class="text-muted-foreground transition-colors hover:text-accent" aria-label="Social link"><svg class="h-5 w-5" aria-hidden="true"><use href="/icons/{{ social.icon|split:":"|first }}.svg#{{ social.icon|split:":"|last }}"></use></svg></a>{% endif %}{% endfor %}
</div>{% endif %}
</div>
</div>
{% endfor %}
</div>
</div>
</section>

View File

@ -0,0 +1,26 @@
<section class="py-16 md:py-24 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div class="mx-auto max-w-6xl px-4">
{% if heading %}<h2 class="pd-display text-center text-3xl text-primary md:text-4xl"><span class="pd-doodle-underline">{{ heading }}</span></h2>{% endif %}
<div class="mt-12 {% if layout == "single" %}mx-auto max-w-3xl{% elif layout == "carousel" %}flex snap-x snap-mandatory gap-6 overflow-x-auto pb-4{% else %}grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3{% endif %}">
{% for q in quotes %}
<figure class="pd-card flex flex-col p-6{% if layout == "carousel" %} min-w-[18rem] max-w-sm shrink-0 snap-start{% endif %}">
{% if q.rating %}<div class="mb-3 flex gap-0.5" aria-label="Rated {{ q.rating }} out of 5">
<svg class="h-4 w-4 {% if q.rating >= 1 %}text-accent{% else %}text-muted-foreground/30{% endif %}" aria-hidden="true"><use href="/icons/lucide.svg#star"></use></svg>
<svg class="h-4 w-4 {% if q.rating >= 2 %}text-accent{% else %}text-muted-foreground/30{% endif %}" aria-hidden="true"><use href="/icons/lucide.svg#star"></use></svg>
<svg class="h-4 w-4 {% if q.rating >= 3 %}text-accent{% else %}text-muted-foreground/30{% endif %}" aria-hidden="true"><use href="/icons/lucide.svg#star"></use></svg>
<svg class="h-4 w-4 {% if q.rating >= 4 %}text-accent{% else %}text-muted-foreground/30{% endif %}" aria-hidden="true"><use href="/icons/lucide.svg#star"></use></svg>
<svg class="h-4 w-4 {% if q.rating >= 5 %}text-accent{% else %}text-muted-foreground/30{% endif %}" aria-hidden="true"><use href="/icons/lucide.svg#star"></use></svg>
</div>{% endif %}
<blockquote class="pd-body flex-1 text-lg leading-relaxed text-foreground">{% if q.quote %}&ldquo;{{ q.quote }}&rdquo;{% endif %}</blockquote>
<figcaption class="mt-5 flex items-center gap-3">
{% if q.avatar %}<span class="h-10 w-10 shrink-0 overflow-hidden rounded-full bg-secondary">{% img q.avatar alt=q.name class="h-full w-full object-cover" %}</span>{% endif %}
<span>
{% if q.name %}<span class="pd-display block text-base text-primary">{{ q.name }}</span>{% endif %}
{% if q.role %}<span class="pd-hand block text-lg text-accent">{{ q.role }}</span>{% endif %}
</span>
</figcaption>
</figure>
{% endfor %}
</div>
</div>
</section>

View File

@ -1,3 +1 @@
<div class="font-body prose max-w-none {{ class }}" style="color: hsl(var(--foreground)); line-height: 1.75;">
{{ text|safe }}
</div>
<div class="pd-body prose max-w-none flex-1 text-foreground [&_a]:text-accent [&_a]:underline [&_a]:decoration-dashed [&_strong]:text-primary {{ proseClass }}">{{ text|safe }}</div>

View File

@ -0,0 +1,21 @@
<section class="py-16 md:py-24 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
<div class="mx-auto max-w-4xl px-4">
{% if heading %}<h2 class="pd-display text-center text-3xl text-primary md:text-4xl">{{ heading }}</h2>{% endif %}
{% if intro %}<p class="pd-body mx-auto mt-4 max-w-2xl text-center text-lg text-muted-foreground">{{ intro }}</p>{% endif %}
<ol class="mt-12 {% if orientation == "horizontal" %}grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-4{% else %}relative space-y-8 before:absolute before:left-4 before:top-2 before:h-[calc(100%-1rem)] before:w-px before:bg-border{% endif %}">
{% for step in steps %}
<li class="{% if orientation == "horizontal" %}text-center{% else %}relative pl-14{% endif %}">
<span class="pd-stamp pd-stamp-sm {% if orientation == "horizontal" %}mx-auto{% else %}absolute left-0 top-0 ring-4 ring-background{% endif %}">
{% if marker == "numbered" %}{{ forloop.counter }}{% elif marker == "dot" %}<span class="h-2 w-2 rounded-full bg-accent-foreground"></span>{% else %}<svg class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#croissant"></use></svg>{% endif %}
</span>
<div class="{% if orientation == "horizontal" %}mt-4{% else %}pt-1{% endif %}">
{% if step.label %}<p class="pd-hand text-lg text-accent">{{ step.label }}</p>{% endif %}
{% if step.title %}<h3 class="pd-display mt-1 text-lg text-primary">{{ step.title }}</h3>{% endif %}
{% if step.text %}<p class="pd-body mt-1 text-muted-foreground">{{ step.text }}</p>{% endif %}
{% if step.media %}<div class="pd-frame mt-3 overflow-hidden rounded-sm p-1.5">{% img step.media alt=step.title class="h-auto w-full rounded-sm object-cover" %}</div>{% endif %}
</div>
</li>
{% endfor %}
</ol>
</div>
</section>

View File

@ -0,0 +1,10 @@
{# pastel-dream utility-bar — thin contact strip; left/right item lists {icon,label,link}. #}
{% set ubcls = "bg-muted text-muted-foreground border-b border-dashed border-border" %}
{% if variant == "primary" %}{% set ubcls = "bg-primary text-primary-foreground" %}{% elif variant == "dark" %}{% set ubcls = "bg-foreground text-background" %}{% endif %}
{% macro ubitem(it) %}{% if it.link %}<a href="{{ it.link }}" class="inline-flex items-center gap-1.5 transition-opacity hover:opacity-80">{% if it.icon %}<svg class="h-3.5 w-3.5" aria-hidden="true"><use href="/icons/{{ it.icon|split:":"|first }}.svg#{{ it.icon|split:":"|last }}"></use></svg>{% endif %}{% if it.label %}<span>{{ it.label }}</span>{% endif %}</a>{% else %}<span class="inline-flex items-center gap-1.5">{% if it.icon %}<svg class="h-3.5 w-3.5" aria-hidden="true"><use href="/icons/{{ it.icon|split:":"|first }}.svg#{{ it.icon|split:":"|last }}"></use></svg>{% endif %}{% if it.label %}<span>{{ it.label }}</span>{% endif %}</span>{% endif %}{% endmacro %}
<div class="pd-body w-full text-xs {% if not showOnMobile %}hidden sm:block{% endif %} {{ ubcls }} {{ class }}">
<div class="mx-auto flex h-9 max-w-7xl items-center justify-between gap-4 px-4 sm:px-6 lg:px-8">
<div class="flex flex-wrap items-center gap-4">{% for it in leftItems %}{{ ubitem(it) }}{% endfor %}</div>
<div class="flex flex-wrap items-center justify-end gap-4">{% for it in rightItems %}{{ ubitem(it) }}{% endfor %}</div>
</div>
</div>

View File

@ -0,0 +1,22 @@
{% with aspect=aspect|default:"16:9" %}<figure class="bn-video my-8{% if class %} {{ class }}{% endif %}"><div class="bn-video-frame pd-frame relative w-full overflow-hidden rounded-sm bg-secondary p-1.5 {% if aspect == "4:3" %}aspect-[4/3]{% elif aspect == "1:1" %}aspect-square{% elif aspect == "21:9" %}aspect-[21/9]{% else %}aspect-video{% endif %}" data-bn-video data-video-url="{{ url|default:"" }}" data-video-media="{% if media %}{{ media|mediaURL }}{% endif %}"><button type="button" data-bn-video-play class="group absolute inset-0 flex h-full w-full items-center justify-center focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring" aria-label="Play video{% if title %}: {{ title }}{% endif %}">{% if poster %}{% img poster alt=title|default:"" class="absolute inset-0 h-full w-full rounded-sm object-cover" %}{% endif %}<span class="absolute inset-0 bg-foreground/10 transition-colors group-hover:bg-foreground/20" aria-hidden="true"></span><span class="relative inline-flex h-16 w-16 items-center justify-center rounded-full bg-accent text-accent-foreground shadow-lg transition-transform group-hover:scale-105 motion-reduce:transform-none"><svg class="h-7 w-7" aria-hidden="true"><use href="/icons/lucide.svg#play"></use></svg></span></button></div>{% if caption %}<figcaption class="pd-hand mt-3 text-center text-xl text-accent">{{ caption }}</figcaption>{% endif %}</figure><script>
(function(){
if(window.__bnVideoFacade)return;window.__bnVideoFacade=true;
function yt(u){var m=u.match(/(?:youtube\.com\/(?:watch\?v=|embed\/|shorts\/)|youtu\.be\/)([\w-]{6,})/);return m?m[1]:null;}
function vim(u){var m=u.match(/vimeo\.com\/(?:video\/)?(\d+)/);return m?m[1]:null;}
document.addEventListener('click',function(e){
var btn=e.target.closest('[data-bn-video-play]');if(!btn)return;
var frame=btn.closest('[data-bn-video]');if(!frame)return;
e.preventDefault();
var url=frame.getAttribute('data-video-url')||'',media=frame.getAttribute('data-video-media')||'',node;
var vstyle='position:absolute;inset:0;width:100%;height:100%;object-fit:contain;background:rgba(0,0,0,1);';
if(media){node=document.createElement('video');node.src=media;node.controls=true;node.autoplay=true;node.setAttribute('playsinline','');node.style.cssText=vstyle;}
else{var y=yt(url),v=vim(url),embed;
if(y)embed='https://www.youtube-nocookie.com/embed/'+y+'?autoplay=1&rel=0';
else if(v)embed='https://player.vimeo.com/video/'+v+'?autoplay=1';
if(embed){node=document.createElement('iframe');node.src=embed;node.allow='autoplay; fullscreen; picture-in-picture';node.setAttribute('allowfullscreen','');node.setAttribute('title',btn.getAttribute('aria-label')||'Video');node.style.cssText='position:absolute;inset:0;width:100%;height:100%;border:0;';}
else if(url){node=document.createElement('video');node.src=url;node.controls=true;node.autoplay=true;node.setAttribute('playsinline','');node.style.cssText=vstyle;}
}
if(node){btn.remove();frame.appendChild(node);}
});
})();
</script>{% endwith %}

View File

@ -1,21 +1,15 @@
<!doctype html>
<html lang="en">
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
{{ head_html|safe }}
<body class="font-body antialiased min-h-screen flex flex-col" style="background-color: hsl(var(--background)); color: hsl(var(--foreground));">
<body class="pd-paper pd-body bg-background text-foreground antialiased min-h-screen flex flex-col">
{{ admin_banner_html|safe }}
<header class="w-full">
{{ slots.header|safe }}
<div class="max-w-3xl mx-auto px-4">{{ slots.header|safe }}</div>
</header>
<main class="flex-grow w-full max-w-2xl mx-auto px-4 py-16">
{% if slots.main %}<article class="font-body prose max-w-none" style="line-height: 1.75; color: hsl(var(--foreground));">
{{ slots.main|safe }}
</article>{% else %}<div class="py-20 text-center" style="color: hsl(var(--muted-foreground));">
<p>This page is still resting. Add a block to begin.</p>
</div>{% endif %}
<main class="flex-grow max-w-2xl mx-auto w-full px-4 py-12 pd-dropcap">
{% if slots.main %}<article class="prose prose-lg max-w-none">{{ slots.main|safe }}</article>{% else %}<div class="py-20 text-center pd-body text-muted-foreground italic"><p>Write something honest here.</p></div>{% endif %}
</main>
<footer class="w-full mt-auto">
{{ slots.footer|safe }}
</footer>
<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>
{{ body_end_html|safe }}
</body>
</html>

View File

@ -0,0 +1,19 @@
<!doctype html>
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
{{ head_html|safe }}
<body class="pd-paper pd-body bg-background text-foreground antialiased min-h-screen flex flex-col">
{{ admin_banner_html|safe }}
<main class="flex flex-grow w-full flex-col items-center justify-center px-4 py-12">
<div class="w-full max-w-md">
<a href="/" class="mb-6 flex items-center justify-center" aria-label="Home">
{% if site_settings.Logo %}<img src="{{ site_settings.Logo }}" alt="{{ site_settings.LogoAlt|default:site_settings.Title }}" class="h-10 w-auto">{% else %}<span class="pd-display text-3xl text-primary">{{ site_settings.Title|default:"Petal & Crumb" }}</span>{% endif %}
</a>
<div>
{% if slots.main %}{{ slots.main|safe }}{% else %}<div class="pd-card p-6 text-center pd-body text-muted-foreground"><p>No form assigned to this page.</p></div>{% endif %}
</div>
{% if slots.footer %}<div class="pd-body mt-6 text-center text-sm text-muted-foreground">{{ slots.footer|safe }}</div>{% endif %}
</div>
</main>
{{ body_end_html|safe }}
</body>
</html>

View File

@ -0,0 +1,15 @@
<!doctype html>
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
{{ head_html|safe }}
<body class="pd-paper pd-body bg-background text-foreground antialiased min-h-screen flex flex-col">
{{ admin_banner_html|safe }}
<header class="w-full">{{ slots.header|safe }}</header>
<main class="flex-grow w-full">
{% if title %}<div class="mx-auto max-w-6xl px-4 pt-12"><h1 class="pd-display text-4xl text-primary md:text-5xl"><span class="pd-doodle-underline">{{ title }}</span></h1>{% if page_meta.MetaDescription %}<p class="pd-body mt-3 max-w-2xl text-lg text-muted-foreground">{{ page_meta.MetaDescription }}</p>{% endif %}</div>{% endif %}
{% if slots.featured %}<section class="w-full">{{ slots.featured|safe }}</section>{% endif %}
{% if slots.main %}<section class="w-full">{{ slots.main|safe }}</section>{% else %}<div class="py-20 text-center pd-hand text-2xl text-muted-foreground"><p>No stories brewed yet.</p></div>{% endif %}
</main>
<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>
{{ body_end_html|safe }}
</body>
</html>

View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
{{ head_html|safe }}
<body class="pd-paper pd-body bg-background text-foreground antialiased min-h-screen flex flex-col">
{{ admin_banner_html|safe }}
<header class="w-full">{{ slots.header|safe }}</header>
<main class="flex-grow w-full">
{% if slots.main %}{{ slots.main|safe }}{% else %}<div class="py-20 text-center pd-hand text-2xl text-muted-foreground"><p>Add a contact form and your hours.</p></div>{% endif %}
</main>
<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>
{{ body_end_html|safe }}
</body>
</html>

View File

@ -1,19 +1,13 @@
<!doctype html>
<html lang="en">
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
{{ head_html|safe }}
<body class="font-body antialiased min-h-screen flex flex-col" style="background-color: hsl(var(--background)); color: hsl(var(--foreground));">
<body class="pd-paper pd-body bg-background text-foreground antialiased min-h-screen flex flex-col">
{{ admin_banner_html|safe }}
<header class="w-full">
{{ slots.header|safe }}
</header>
<main class="flex-grow w-full max-w-4xl mx-auto px-4 py-12">
{% if slots.main %}{{ slots.main|safe }}{% else %}<div class="py-20 text-center font-body" style="color: hsl(var(--muted-foreground));">
<p>This page is still resting. Add a block to begin.</p>
</div>{% endif %}
<header class="w-full">{{ slots.header|safe }}</header>
<main class="flex-grow w-full">
{% if slots.main %}{{ slots.main|safe }}{% else %}<div class="py-20 text-center pd-hand text-2xl text-muted-foreground"><p>Pour something in here.</p></div>{% endif %}
</main>
<footer class="w-full mt-auto">
{{ slots.footer|safe }}
</footer>
<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>
{{ body_end_html|safe }}
</body>
</html>

View File

@ -1,19 +1,13 @@
<!doctype html>
<html lang="en">
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
{{ head_html|safe }}
<body class="font-body antialiased min-h-screen flex flex-col" style="background-color: hsl(var(--background)); color: hsl(var(--foreground));">
<body class="pd-paper pd-body bg-background text-foreground antialiased min-h-screen flex flex-col">
{{ admin_banner_html|safe }}
<header class="w-full">
{{ slots.header|safe }}
</header>
<header class="w-full">{{ slots.header|safe }}</header>
<main class="flex-grow w-full">
{% if slots.main %}{{ slots.main|safe }}{% else %}<div class="max-w-4xl mx-auto py-20 px-4 text-center" style="color: hsl(var(--muted-foreground));">
<p>This page is still resting. Add a block to begin.</p>
</div>{% endif %}
{% if slots.main %}{{ slots.main|safe }}{% else %}<div class="max-w-3xl mx-auto py-20 px-4 text-center pd-body text-muted-foreground italic"><p>Add a gallery, a wide photo, a confession.</p></div>{% endif %}
</main>
<footer class="w-full mt-auto">
{{ slots.footer|safe }}
</footer>
<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>
{{ body_end_html|safe }}
</body>
</html>

View File

@ -1,22 +1,19 @@
<!doctype html>
<html lang="en">
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
{{ head_html|safe }}
<body class="font-body antialiased min-h-screen flex flex-col" style="background-color: hsl(var(--background)); color: hsl(var(--foreground));">
<body class="pd-paper pd-body bg-background text-foreground antialiased min-h-screen flex flex-col">
{{ admin_banner_html|safe }}
<section class="w-full">{{ slots.hero|safe }}</section>
<section class="w-full">
{{ slots.hero|safe }}
<div class="max-w-5xl mx-auto px-4">{{ slots.menu|safe }}</div>
</section>
<main class="flex-grow w-full">
{% if slots.main %}<div class="max-w-6xl mx-auto px-4 py-20">
{{ slots.main|safe }}
</div>{% endif %}
</main>
<section class="w-full">
{{ slots.cta|safe }}
<div class="max-w-3xl mx-auto px-4 py-12">{{ slots.story|safe }}</div>
</section>
<footer class="w-full mt-auto">
{{ slots.footer|safe }}
</footer>
<section class="w-full">
<div class="max-w-5xl mx-auto px-4">{{ slots.cta|safe }}</div>
</section>
<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>
{{ body_end_html|safe }}
</body>
</html>