plum-cms 0.2.1 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +178 -0
  3. data/README.md +64 -0
  4. data/app/assets/builds/tailwind.css +1 -1
  5. data/app/controllers/plum/cp/entries_controller.rb +88 -5
  6. data/app/controllers/plum/cp/static_cache_controller.rb +12 -0
  7. data/app/controllers/plum/form_submissions_controller.rb +13 -0
  8. data/app/controllers/plum/pages_controller.rb +8 -0
  9. data/app/controllers/plum/theme_assets_controller.rb +2 -0
  10. data/app/javascript/controllers/plum/write_controller.js +174 -0
  11. data/app/models/plum/asset.rb +1 -0
  12. data/app/models/plum/content_type.rb +1 -0
  13. data/app/models/plum/entry.rb +57 -0
  14. data/app/models/plum/entry_term.rb +1 -0
  15. data/app/models/plum/form_definition.rb +1 -0
  16. data/app/models/plum/global.rb +1 -0
  17. data/app/models/plum/nav_item.rb +1 -0
  18. data/app/models/plum/nav_menu.rb +1 -0
  19. data/app/models/plum/site.rb +2 -0
  20. data/app/models/plum/site_setting.rb +1 -0
  21. data/app/models/plum/static_cache_invalidation.rb +30 -0
  22. data/app/models/plum/taxonomy.rb +1 -0
  23. data/app/models/plum/term.rb +1 -0
  24. data/app/services/plum/config_sync.rb +252 -0
  25. data/app/services/plum/draft_diff.rb +177 -0
  26. data/app/services/plum/form_renderer.rb +12 -1
  27. data/app/services/plum/liquid_context.rb +0 -2
  28. data/app/views/layouts/plum/write.html.erb +140 -0
  29. data/app/views/plum/cp/dashboard/show.html.erb +10 -3
  30. data/app/views/plum/cp/entries/_form.html.erb +2 -2
  31. data/app/views/plum/cp/entries/diff.html.erb +40 -0
  32. data/app/views/plum/cp/entries/edit.html.erb +24 -0
  33. data/app/views/plum/cp/entries/index.html.erb +3 -0
  34. data/app/views/plum/cp/entries/write.html.erb +60 -0
  35. data/config/plum_routes.rb +5 -0
  36. data/db/engine_migrate/20260811090000_add_draft_data_to_plum_entries.rb +5 -0
  37. data/docs/config-as-code.md +103 -0
  38. data/docs/plum-cli.md +356 -0
  39. data/docs/static-caching.md +163 -0
  40. data/lib/generators/plum/install/templates/plum_initializer.rb +8 -0
  41. data/lib/plum/configuration.rb +12 -1
  42. data/lib/plum/engine.rb +7 -0
  43. data/lib/plum/static_cache/middleware.rb +61 -0
  44. data/lib/plum/static_cache.rb +103 -0
  45. data/lib/plum/version.rb +1 -1
  46. data/lib/tasks/plum_config.rake +48 -0
  47. data/lib/tasks/plum_portability.rake +54 -29
  48. data/lib/tasks/plum_styles.rake +14 -9
  49. metadata +21 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 277d5241551a05b136eebf29f83e6347c7fc15a9d600054c524a245ab18010b4
4
- data.tar.gz: 336177aba310b7b33b4fddab22190b0ac3a1fec3ad330fb1c09552459ffc8def
3
+ metadata.gz: 9daf7a444c4aca840d3395ef9e290708192a904b8d7effa9aa0954819ab3a046
4
+ data.tar.gz: 5987aaecba9e612f37eae6cebb8ad663fcd39fc0add1342762836339bbaf5a3c
5
5
  SHA512:
6
- metadata.gz: 8ad29dfd4e8927c9259276e8c3d0cd916eb3e3b2ae74327ee32f511c67db9779613190a637b92ef4903cc0f6d440d620a6212103fad74d268202007e54b9e84c
7
- data.tar.gz: e01f9d033d20d606da90e74078155e5d825c727b89162b7e750eeac5c9f3aabc048a7c3c8c29ddb241ba7d3aa4a727458f1150b04475a898c44ba8a1d80f1316
6
+ metadata.gz: f7718730fa5d92db2a89d1ef13724d2a7cb0198322ff0bb239982390f4af3125747d3261ca1a61161da64d3dd45119a37d2396181dd7631d3193294428491781
7
+ data.tar.gz: 9ddd57edac63fc212ac7ddfd20f3616551f84a437c72496b31d3f86d30fafdd4ac002454fd14a66e7bbe9faea6d274274e7fd69642790ded89bc29fe39c7596d
data/CHANGELOG.md CHANGED
@@ -1,5 +1,183 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.2 — 2026-08-13
4
+
5
+ ### Fixed
6
+
7
+ - Fixed `plum:site:replace` (and site destroy generally) aborting with
8
+ `RecordNotDestroyed` on any site that has a homepage entry — i.e. every
9
+ real site. The homepage's can't-be-deleted guard now stands down when the
10
+ entry is being removed as part of its site's dependent cascade. Found by
11
+ the first live `plum pull` against production.
12
+ - Fixed `plum connect`'s deployment detection treating an unedited
13
+ `config/deploy.yml` scaffold (the placeholder file `rails new` ships) as
14
+ proof of a Kamal deployment, which steered Once-deployed repos toward a
15
+ broken `via: kamal` remote. A deploy.yml still carrying `kamal init`
16
+ placeholders now counts as absent.
17
+
18
+ ### Added
19
+
20
+ - Added the Plum CLI (`cli/`, Go): `plum pull` replaces your local site with
21
+ a remote's, `plum sync`/`plum check` apply and verify config-as-code files,
22
+ `plum backup` and `plum run` wrap the remaining rake tasks. Remotes are
23
+ defined per-project in `plum.yml` (`plum init`). Every command wraps an
24
+ engine rake task — the CLI adds transport, never behavior. See
25
+ `docs/plum-cli.md`.
26
+ - Added three transports for CLI remotes (`via: ssh|kamal|once`): plain
27
+ ssh/scp (default, inherits ~/.ssh/config/agent/jump hosts); `kamal`, which
28
+ shells out to a local Kamal binary so `config/deploy.yml` is the only
29
+ source of truth for where the app lives; and `once` for 37signals' Once
30
+ tool, whose CLI runs on the server itself — the transport ssh-es to
31
+ `host:` and runs `once exec <once_app> ...` there (`once_app:` is the
32
+ hostname you gave `once deploy --host ...`, distinct from how you reach
33
+ the box). Verified against a live Once deployment. Picking a transport is
34
+ the only thing that changes — the rake tasks behind every command are
35
+ identical either way.
36
+ - Added `plum logs [remote] [--follow]` — recent app logs, heroku-style
37
+ tailing with `--follow`. Plain ssh tails `log/production.log`, Kamal runs
38
+ `kamal app logs`, Once streams `docker logs` from the app's container
39
+ (resolved by Once's own container label; the Rails image logs to stdout,
40
+ not a file).
41
+ - Gave the CLI a heroku-style voice: `▸` step headers, animated spinners
42
+ that settle into timed `✓ ... (700ms)` result lines, remote rake output
43
+ framed behind a dim `│` gutter, arrow-key selects and styled prompts in
44
+ `plum connect`, a colorized help screen and projects list, and a plum
45
+ accent color adaptive to light/dark terminals (via lipgloss + huh).
46
+ Everything degrades to the old plain sequential output when piped, in
47
+ CI, under `NO_COLOR`, or with `TERM=dumb` — scripted stdin answers and
48
+ log parsing are unaffected; Ctrl-C at any prompt exits quietly (130).
49
+ - `plum connect` now answers its own questions where it can: re-runs
50
+ prefill every prompt from the existing plum.yml (Enter-through-everything
51
+ reproduces the current setup, and the stored deployment type outranks
52
+ file detection), and for Once remotes it asks the server itself — `once
53
+ list` over the just-verified SSH connection — offering the deployed apps
54
+ as a pick-list instead of a blank hostname prompt.
55
+ - On an already-configured project, bare `plum connect` skips the interview
56
+ entirely and becomes a health check: it prints the stored connection and
57
+ verifies SSH login, server tooling, and that the app is deployed, exiting
58
+ nonzero on failure (CI-friendly). The wizard runs only on first setup,
59
+ with `--reconfigure`, or when a new ip/host is passed explicitly.
60
+ - A brand-new project's host prompt now offers the servers you already
61
+ have: plum-* aliases from ~/.ssh/config (the ones `plum connect` itself
62
+ wrote) appear as a pick-list, so connecting a second site to the same
63
+ VPS never re-asks for the IP.
64
+ - `plum check`/`plum push` bootstrap themselves on first run: with no
65
+ plum/ directory in the project they offer to run
66
+ `bin/rails plum:config:export` right there (interactive sessions only;
67
+ scripts keep the hard error). Verified live: a first-time `plum check`
68
+ exported the content model, uploaded it through `once exec`'s stdin tar
69
+ pipe — the last transport path that had never run against a real
70
+ server — and production reported no drift.
71
+ - Renamed `plum sync` to `plum push`: push moves structure up, pull moves
72
+ content down — the familiar "push code, pull data" asymmetry, since in
73
+ Plum's model the content model is code. `plum sync` keeps working as an
74
+ alias with a gentle pointer to the new name.
75
+ - `plum connect` now registers the project in the global registry under
76
+ the directory's name (activating it when nothing is active), so
77
+ `plum use`/`--project` work without a separate `plum projects add`.
78
+ - `plum logs` prettifies structured JSON log lines on a terminal: dim
79
+ local-time stamps, color-coded levels and HTTP statuses, and request
80
+ lines compacted to `GET /up 200 (2ms)` with noisy fields dropped. Plain
81
+ Rails log text passes through untouched, and piped output stays raw so
82
+ `plum logs | jq` keeps working. The CLI is now documented in the README
83
+ alongside the engine features.
84
+ - Added `plum tutorial`: a built-in, full-screen interactive tour of Plum
85
+ and the CLI (bubbletea + glamour). Eight chapters — content model,
86
+ themes, writing mode and drafts, static caching, config as code, and
87
+ the CLI workflow — as styled markdown with ←/→ chapter navigation,
88
+ 1–9 jumps, and a plain-markdown fallback when piped. Chapters slide in
89
+ on harmonica spring physics under an animated gradient progress bar;
90
+ the CLI chapters carry live demos (press `d` to watch `plum pull` or a
91
+ connect health check type itself out, spinners included); and the tour
92
+ ends with a three-question interactive quiz. Harmonica earns its name
93
+ throughout: the tour opens with a splash — the PLUM wordmark springs up
94
+ letter by letter while a plum bounces across it on projectile physics —
95
+ wrong quiz answers shake the question on an underdamped spring, and
96
+ finishing the quiz starts a continuous confetti rain — plum-palette
97
+ particles drifting down the finale on gentle projectile physics for as
98
+ long as you stay to enjoy it.
99
+ The splash is a proper title screen: the show loops (the plum relaunches
100
+ while you watch) and holds until you press a pulsing lipgloss "Start the
101
+ tutorial ↵" button — enter begins, q quits, nothing rushes you.
102
+ - Gamified the tutorial. Progress persists to ~/.config/plum/tutorial.json:
103
+ chapters check off as read, demos as watched, and the tour resumes where
104
+ you left off (the title button becomes "Continue the tutorial"). Reading,
105
+ watching, typing, and passing the quiz earn XP, with plum-themed ranks
106
+ climbing in the header (Seedling → Sprout → Sapling → Grower →
107
+ Orchardist). CLI chapters gained typing challenges: press `t` and type
108
+ the real command with strict per-character feedback — typos flash and
109
+ sting but don't land — and nailing it banks XP and rolls the demo as
110
+ your reward. And the title screen hides Plum Drop (`p`): an arcade game
111
+ of catching falling plums (harmonica projectiles) with an arrow-key
112
+ basket, combo multipliers, and a persisted high score. It's endless —
113
+ misses just break your streak, new bests are celebrated the moment they
114
+ happen, and you play until you quit. SSH-served sessions play everything with an ephemeral save.
115
+ - `plum tutorial --serve [addr]` hosts the tour over SSH via charm's wish:
116
+ `ssh -p 2222 your-server` gets the full animated tutorial with nothing
117
+ installed. Sessions run only the tutorial (no shell, no exec, any or no
118
+ key accepted); the host key persists under ~/.config/plum so returning
119
+ visitors aren't warned.
120
+ - Added a Firebase-CLI-style global project registry (`plum projects
121
+ add/list/remove`, `plum use`, `--project`) so a fleet of Plum sites can be
122
+ managed from one dev machine without `cd`-ing into each repo — a local
123
+ `plum.yml` still always takes precedence when one is present.
124
+ - Added `plum connect [ip-or-host]`: guided first-time server setup with no
125
+ YAML editing required. Detects or generates a local SSH key, tests and (on
126
+ confirmation) fixes passwordless login with `ssh-copy-id`, optionally adds
127
+ a `Host` alias to `~/.ssh/config`, and writes `plum.yml` from the answers.
128
+ It also detects the repo's deployment shape (`config/deploy.yml` → Kamal,
129
+ `Dockerfile` alone → Once) and asks the right questions for it, then
130
+ checks the server's tooling — Docker Engine for Kamal/Once, plus the
131
+ `once` binary for Once — offering to install whatever's missing, each
132
+ step gated on explicit confirmation. Deploying the app itself (images,
133
+ registry credentials) intentionally stays manual.
134
+ - Fixed destroying an entry (or its site) crashing with a foreign-key
135
+ violation when the entry was linked from a nav menu — nav items are now
136
+ removed with their entry.
137
+ - Added `rails plum:site:replace` — the "pull" refresh: swaps the local site
138
+ for a production archive in one transactional step, database-agnostic
139
+ (Postgres prod → SQLite dev), refusing to run in production without FORCE.
140
+ - Fixed Plum rake tasks running their bodies twice in the standalone repo
141
+ (engine railtie and application both load lib/tasks; Rake appends actions
142
+ on re-definition).
143
+ - Added config-as-code (phase 1): `Plum::ConfigSync` syncs content types and
144
+ fieldsets between YAML files (`plum/content_types/*.yml`) and the database
145
+ via `rails plum:config:export`, `plum:config:sync` (with PRUNE/FORCE
146
+ guards that never touch entry data), and `plum:config:check` for CI drift
147
+ detection. See `docs/config-as-code.md`.
148
+ - Added full-page static caching: rendered public pages and theme assets are
149
+ written to disk keyed by host + path, served without touching the database,
150
+ and flushed automatically when site content changes. **Off by default,
151
+ explicit opt-in** (`config.static_cache_enabled = true`) — it is only
152
+ correct on a single-server deployment; enabling it on 2+ nodes/dynos
153
+ silently serves stale pages. The file layout supports direct nginx/Caddy
154
+ `try_files` serving. See `docs/static-caching.md`.
155
+ - Added a "Clear page cache" control to the dashboard for administrators.
156
+ - Added a distraction-free writing mode for entries with a rich text field:
157
+ full-screen typographic surface with debounced autosave, Cmd/Ctrl+S,
158
+ word count, and unsaved-changes protection, reachable from the entry editor.
159
+ - Write-mode saves merge into existing entry data, so partial saves never
160
+ wipe unsubmitted blueprint fields, and identical consecutive saves no
161
+ longer stack duplicate revisions.
162
+ - Added a git-style "Review changes" screen for working drafts: field-by-field
163
+ word diffs of draft vs. live content (rich text compared as readable text,
164
+ structured fields as JSON), with publish/discard actions inline. Linked from
165
+ the entry editor's draft banner and the writing surface.
166
+ - Added working drafts for published entries: writing-mode autosaves on a
167
+ published entry are stored in a new `draft_data` column and never touch the
168
+ live content or flush its cached pages. Editors publish or discard the
169
+ draft explicitly (from the writing surface or the entry editor's draft
170
+ banner); publishing records a revision. Draft-status entries still save
171
+ directly.
172
+
173
+ ### Changed
174
+
175
+ - Public `{% form %}` forms now use a honeypot spam trap instead of a
176
+ per-session CSRF token, so cached pages stay valid and public page views no
177
+ longer set session cookies. The control panel keeps standard CSRF
178
+ protection.
179
+ - Theme assets are served with `Cache-Control: public, max-age=3600`.
180
+
3
181
  ## 0.2.1 — 2026-08-07
4
182
 
5
183
  ### Added
data/README.md CHANGED
@@ -243,6 +243,30 @@ bin/rails plum:backup:restore ARCHIVE=/var/backups/plum/site.plum.zip
243
243
  See [the portability guide](docs/portability.md) for archive guarantees and
244
244
  production backup guidance.
245
245
 
246
+ ## The Plum CLI
247
+
248
+ `cli/` ships a Go command line that wraps the engine's rake tasks over SSH,
249
+ [Kamal](https://kamalmanual.com), or [Once](https://once.com) — heroku-style
250
+ ergonomics for self-hosted Plum sites:
251
+
252
+ ```sh
253
+ plum tutorial # interactive guided tour, right in the terminal
254
+ plum connect 203.0.113.5 # guided setup: SSH keys, server tooling, plum.yml
255
+ plum connect # already configured? verifies the setup instead
256
+ plum pull # replace your local site with production's content
257
+ plum push # apply plum/ config-as-code files to the remote
258
+ plum check # fail if the remote drifted from plum/ (CI-friendly)
259
+ plum logs --follow # tail the app's logs, pretty-printed on a TTY
260
+ plum backup # timestamped site archive on the server
261
+ plum run -- TASK # any rake task, any remote
262
+ ```
263
+
264
+ A Firebase-style project registry (`plum use`, `--project`) manages a fleet
265
+ of sites from one machine. Every command degrades to plain, script-friendly
266
+ output when piped or under `NO_COLOR`. See [the CLI guide](docs/plum-cli.md)
267
+ for transports (`via: ssh|kamal|once`), fleet management, and how `pull`
268
+ works across database engines.
269
+
246
270
  ## Content API
247
271
 
248
272
  Published entries are available through a read-only, site-scoped JSON API:
@@ -287,6 +311,46 @@ Images uploaded through Lexxy use Active Storage Direct Upload and render on the
287
311
  front-end as standard `<img>` tags. Text colors are resolved from Lexxy's CSS
288
312
  variables to real color values at render time.
289
313
 
314
+ ## Writing Mode
315
+
316
+ Entries whose content type has a rich text field get a distraction-free
317
+ writing surface ("Write" in the entry editor): a full-screen typographic page
318
+ with just the title and body, debounced autosave, Cmd/Ctrl+S, and a live word
319
+ count. Saves merge into the entry's existing data, so fields that aren't on
320
+ the writing surface are never touched.
321
+
322
+ On a **published** entry, writing-mode saves become a working draft: the live
323
+ version (and its cached pages) stay exactly as they are while you write, and
324
+ nothing goes public until you hit "Publish changes". Drafts can also be
325
+ published or discarded from the entry editor, and publishing records a
326
+ revision. Unpublished (draft-status) entries save directly, since they aren't
327
+ visible on the site anyway.
328
+
329
+ ## Static Page Caching
330
+
331
+ Optional: rendered public pages can be written to disk and served as static
332
+ files — no database or Liquid work on repeat visits. Content saves flush the
333
+ site's cache automatically, and pages re-render lazily on their next hit. The
334
+ on-disk layout (`{host}/{path}/index.html`) can be served directly by nginx or
335
+ synced to a CDN, so page delivery scales independently of the app.
336
+
337
+ **This is only correct on a single-server deployment** and is **off by
338
+ default** — it must be enabled explicitly:
339
+
340
+ ```ruby
341
+ Plum.configure do |config|
342
+ config.static_cache_enabled = true
343
+ config.static_cache_path = "/var/www/site-cache"
344
+ end
345
+ ```
346
+
347
+ Enabling it on a multi-node platform (2+ Heroku dynos, 2+ Render instances,
348
+ any load-balanced deployment) causes visitors to silently see stale content
349
+ served forever from whichever node missed the invalidation — not a crash,
350
+ just wrong pages. Read
351
+ [docs/static-caching.md](docs/static-caching.md) first, especially "Should
352
+ you enable this?", before turning this on.
353
+
290
354
  ## Globals and Navigation
291
355
 
292
356
  Globals are site-wide JSON objects (company info, social links):
@@ -1,2 +1,2 @@
1
1
  /*! tailwindcss v4.2.1 | MIT License | https://tailwindcss.com */
2
- @layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-space-x-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-50:oklch(97.1% .013 17.38);--color-red-200:oklch(88.5% .062 18.334);--color-red-300:oklch(80.8% .114 19.571);--color-red-600:oklch(57.7% .245 27.325);--color-red-700:oklch(50.5% .213 27.518);--color-red-900:oklch(39.6% .141 25.723);--color-amber-700:oklch(55.5% .163 48.998);--color-yellow-100:oklch(97.3% .071 103.193);--color-yellow-800:oklch(47.6% .114 61.907);--color-green-50:oklch(98.2% .018 155.826);--color-green-100:oklch(96.2% .044 156.743);--color-green-200:oklch(92.5% .084 155.995);--color-green-700:oklch(52.7% .154 150.069);--color-green-800:oklch(44.8% .119 151.328);--color-purple-50:oklch(97.7% .014 308.299);--color-purple-100:oklch(94.6% .033 307.174);--color-purple-200:oklch(90.2% .063 306.703);--color-purple-300:oklch(82.7% .119 306.383);--color-purple-400:oklch(71.4% .203 305.504);--color-purple-500:oklch(62.7% .265 303.9);--color-purple-600:oklch(55.8% .288 302.321);--color-purple-700:oklch(49.6% .265 301.924);--color-purple-900:oklch(38.1% .176 304.987);--color-gray-50:oklch(98.5% .002 247.839);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-200:oklch(92.8% .006 264.531);--color-gray-300:oklch(87.2% .01 258.338);--color-gray-400:oklch(70.7% .022 261.325);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-600:oklch(44.6% .03 256.802);--color-gray-700:oklch(37.3% .034 259.733);--color-gray-800:oklch(27.8% .033 256.848);--color-gray-900:oklch(21% .034 264.665);--color-white:#fff;--spacing:.25rem;--container-md:28rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25 / 1.875);--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-normal:0em;--tracking-wider:.05em;--leading-tight:1.25;--radius-md:.375rem;--radius-lg:.5rem;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}button:not(:disabled),[role=button]:not([aria-disabled=true]){cursor:pointer}button:disabled,[role=button][aria-disabled=true]{cursor:not-allowed}}@layer components{.plum-blueprint-field{background:#f9fafb;border:1px solid #e5e7eb;border-radius:.75rem;grid-template-columns:minmax(0,1fr);gap:1rem;padding:1rem;display:grid}.plum-blueprint-field-header{border-bottom:1px solid #e5e7eb;justify-content:space-between;align-items:center;gap:1rem;padding-bottom:.75rem;display:flex}.plum-blueprint-field-title{color:#111827;text-overflow:ellipsis;white-space:nowrap;font-size:.875rem;font-weight:650;overflow:hidden}.plum-blueprint-field-type{color:#6b7280;margin-left:.5rem;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.75rem;font-weight:400}@media (min-width:640px){.plum-blueprint-field{grid-template-columns:repeat(3,minmax(0,1fr))}}.plum-rich-text-content{outline:none;min-height:14rem}.plum-rich-text-content>*+*{margin-top:.75rem}.plum-rich-text-content h2{font-size:1.25rem;font-weight:700;line-height:1.3}.plum-rich-text-content ul{padding-left:1.5rem;list-style:outside}.plum-rich-text-content ol{padding-left:1.5rem;list-style:decimal}.plum-rich-text-content blockquote{color:#4b5563;border-left:.25rem solid #d1d5db;padding-left:1rem}.plum-rich-text-content a{color:#7c3aed;text-decoration:underline}.plum-rich-text-content p.is-editor-empty:first-child:before{color:#9ca3af;content:attr(data-placeholder);float:left;pointer-events:none;height:0}}@layer utilities{.\!visible{visibility:visible!important}.visible{visibility:visible}.sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.inset-y-0{inset-block:calc(var(--spacing) * 0)}.start{inset-inline-start:var(--spacing)}.end{inset-inline-end:var(--spacing)}.left-0{left:calc(var(--spacing) * 0)}.col-span-full{grid-column:1/-1}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.mx-auto{margin-inline:auto}.mt-1{margin-top:calc(var(--spacing) * 1)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mt-6{margin-top:calc(var(--spacing) * 6)}.mt-8{margin-top:calc(var(--spacing) * 8)}.mt-28{margin-top:calc(var(--spacing) * 28)}.mr-2{margin-right:calc(var(--spacing) * 2)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.mb-8{margin-bottom:calc(var(--spacing) * 8)}.ml-2{margin-left:calc(var(--spacing) * 2)}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.table{display:table}.aspect-square{aspect-ratio:1}.h-4{height:calc(var(--spacing) * 4)}.h-5{height:calc(var(--spacing) * 5)}.h-9{height:calc(var(--spacing) * 9)}.h-10{height:calc(var(--spacing) * 10)}.h-12{height:calc(var(--spacing) * 12)}.h-16{height:calc(var(--spacing) * 16)}.h-32{height:calc(var(--spacing) * 32)}.h-44{height:calc(var(--spacing) * 44)}.h-full{height:100%}.min-h-16{min-height:calc(var(--spacing) * 16)}.min-h-48{min-height:calc(var(--spacing) * 48)}.min-h-full{min-height:100%}.w-4{width:calc(var(--spacing) * 4)}.w-5{width:calc(var(--spacing) * 5)}.w-9{width:calc(var(--spacing) * 9)}.w-10{width:calc(var(--spacing) * 10)}.w-12{width:calc(var(--spacing) * 12)}.w-14{width:calc(var(--spacing) * 14)}.w-16{width:calc(var(--spacing) * 16)}.w-32{width:calc(var(--spacing) * 32)}.w-64{width:calc(var(--spacing) * 64)}.w-full{width:100%}.max-w-md{max-width:var(--container-md)}.min-w-0{min-width:calc(var(--spacing) * 0)}.min-w-full{min-width:100%}.flex-1{flex:1}.shrink-0{flex-shrink:0}.grow{flex-grow:1}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.cursor-pointer{cursor:pointer}.list-inside{list-style-position:inside}.list-disc{list-style-type:disc}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-1{gap:calc(var(--spacing) * 1)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-6{gap:calc(var(--spacing) * 6)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-8>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 8) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 8) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-x-3>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 3) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-x-reverse)))}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-gray-200>:not(:last-child)){border-color:var(--color-gray-200)}.justify-self-start{justify-self:flex-start}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.border{border-style:var(--tw-border-style);border-width:1px}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-gray-100{border-color:var(--color-gray-100)}.border-gray-200{border-color:var(--color-gray-200)}.border-gray-300{border-color:var(--color-gray-300)}.border-green-200{border-color:var(--color-green-200)}.border-purple-200{border-color:var(--color-purple-200)}.border-purple-300{border-color:var(--color-purple-300)}.border-purple-500{border-color:var(--color-purple-500)}.border-red-200{border-color:var(--color-red-200)}.border-red-300{border-color:var(--color-red-300)}.border-transparent{border-color:#0000}.border-white\/10{border-color:#ffffff1a}@supports (color:color-mix(in lab, red, red)){.border-white\/10{border-color:color-mix(in oklab, var(--color-white) 10%, transparent)}}.bg-\[\#F6F3EF\]{background-color:#f6f3ef}.bg-gray-50{background-color:var(--color-gray-50)}.bg-gray-100{background-color:var(--color-gray-100)}.bg-green-50{background-color:var(--color-green-50)}.bg-green-100{background-color:var(--color-green-100)}.bg-purple-50{background-color:var(--color-purple-50)}.bg-purple-100{background-color:var(--color-purple-100)}.bg-purple-600{background-color:var(--color-purple-600)}.bg-red-50{background-color:var(--color-red-50)}.bg-white{background-color:var(--color-white)}.bg-yellow-100{background-color:var(--color-yellow-100)}.object-contain{object-fit:contain}.object-cover{object-fit:cover}.p-0{padding:calc(var(--spacing) * 0)}.p-1{padding:calc(var(--spacing) * 1)}.p-2{padding:calc(var(--spacing) * 2)}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.p-6{padding:calc(var(--spacing) * 6)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-5{padding-inline:calc(var(--spacing) * 5)}.px-6{padding-inline:calc(var(--spacing) * 6)}.px-8{padding-inline:calc(var(--spacing) * 8)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:calc(var(--spacing) * 1)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-4{padding-block:calc(var(--spacing) * 4)}.py-6{padding-block:calc(var(--spacing) * 6)}.py-8{padding-block:calc(var(--spacing) * 8)}.py-12{padding-block:calc(var(--spacing) * 12)}.pt-1{padding-top:calc(var(--spacing) * 1)}.pt-2{padding-top:calc(var(--spacing) * 2)}.pt-3{padding-top:calc(var(--spacing) * 3)}.pt-4{padding-top:calc(var(--spacing) * 4)}.pb-2{padding-bottom:calc(var(--spacing) * 2)}.pl-5{padding-left:calc(var(--spacing) * 5)}.pl-64{padding-left:calc(var(--spacing) * 64)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.leading-tight{--tw-leading:var(--leading-tight);line-height:var(--leading-tight)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-normal{--tw-tracking:var(--tracking-normal);letter-spacing:var(--tracking-normal)}.tracking-wider{--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider)}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.text-amber-700{color:var(--color-amber-700)}.text-gray-400{color:var(--color-gray-400)}.text-gray-500{color:var(--color-gray-500)}.text-gray-600{color:var(--color-gray-600)}.text-gray-700{color:var(--color-gray-700)}.text-gray-800{color:var(--color-gray-800)}.text-gray-900{color:var(--color-gray-900)}.text-green-700{color:var(--color-green-700)}.text-green-800{color:var(--color-green-800)}.text-purple-600{color:var(--color-purple-600)}.text-purple-700{color:var(--color-purple-700)}.text-red-600{color:var(--color-red-600)}.text-red-700{color:var(--color-red-700)}.text-white{color:var(--color-white)}.text-yellow-800{color:var(--color-yellow-800)}.capitalize{text-transform:capitalize}.lowercase{text-transform:lowercase}.uppercase{text-transform:uppercase}.italic{font-style:italic}.line-through{text-decoration-line:line-through}.underline{text-decoration-line:underline}.shadow,.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring-2{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring-purple-500{--tw-ring-color:var(--color-purple-500)}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.blur{--tw-blur:blur(8px);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-shadow{transition-property:box-shadow;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.peer-checked\:border-purple-500:is(:where(.peer):checked~*){border-color:var(--color-purple-500)}.peer-checked\:bg-purple-50:is(:where(.peer):checked~*){background-color:var(--color-purple-50)}.peer-checked\:text-purple-700:is(:where(.peer):checked~*){color:var(--color-purple-700)}.peer-focus\:ring-2:is(:where(.peer):focus~*){--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.peer-focus\:ring-purple-500:is(:where(.peer):focus~*){--tw-ring-color:var(--color-purple-500)}.peer-focus\:ring-offset-2:is(:where(.peer):focus~*){--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}@media (hover:hover){.hover\:border-purple-400:hover{border-color:var(--color-purple-400)}.hover\:border-purple-500:hover{border-color:var(--color-purple-500)}.hover\:bg-gray-50:hover{background-color:var(--color-gray-50)}.hover\:bg-gray-100:hover{background-color:var(--color-gray-100)}.hover\:bg-purple-50:hover{background-color:var(--color-purple-50)}.hover\:bg-purple-100:hover{background-color:var(--color-purple-100)}.hover\:bg-purple-700:hover{background-color:var(--color-purple-700)}.hover\:bg-red-50:hover{background-color:var(--color-red-50)}.hover\:text-gray-700:hover{color:var(--color-gray-700)}.hover\:text-gray-900:hover{color:var(--color-gray-900)}.hover\:text-purple-700:hover{color:var(--color-purple-700)}.hover\:text-purple-900:hover{color:var(--color-purple-900)}.hover\:text-red-700:hover{color:var(--color-red-700)}.hover\:text-red-900:hover{color:var(--color-red-900)}.hover\:shadow-md:hover{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a), 0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}}.focus\:border-purple-500:focus{border-color:var(--color-purple-500)}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus\:ring-purple-500:focus{--tw-ring-color:var(--color-purple-500)}.focus\:ring-offset-2:focus{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-30:disabled{opacity:.3}.disabled\:opacity-40:disabled{opacity:.4}.disabled\:opacity-50:disabled{opacity:.5}.has-checked\:border-purple-500:has(:checked){border-color:var(--color-purple-500)}.has-checked\:ring-2:has(:checked){--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.has-checked\:ring-purple-500:has(:checked){--tw-ring-color:var(--color-purple-500)}@media (min-width:40rem){.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.sm\:px-6{padding-inline:calc(var(--spacing) * 6)}}@media (min-width:48rem){.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}@media (min-width:64rem){.lg\:col-span-2{grid-column:span 2/span 2}.lg\:col-start-3{grid-column-start:3}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:px-8{padding-inline:calc(var(--spacing) * 8)}}}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-space-x-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}
2
+ @layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-space-x-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-ease:initial}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-50:oklch(97.1% .013 17.38);--color-red-100:oklch(93.6% .032 17.717);--color-red-200:oklch(88.5% .062 18.334);--color-red-300:oklch(80.8% .114 19.571);--color-red-400:oklch(70.4% .191 22.216);--color-red-600:oklch(57.7% .245 27.325);--color-red-700:oklch(50.5% .213 27.518);--color-red-800:oklch(44.4% .177 26.899);--color-red-900:oklch(39.6% .141 25.723);--color-amber-50:oklch(98.7% .022 95.277);--color-amber-100:oklch(96.2% .059 95.617);--color-amber-200:oklch(92.4% .12 95.746);--color-amber-300:oklch(87.9% .169 91.605);--color-amber-700:oklch(55.5% .163 48.998);--color-amber-800:oklch(47.3% .137 46.201);--color-yellow-100:oklch(97.3% .071 103.193);--color-yellow-800:oklch(47.6% .114 61.907);--color-green-50:oklch(98.2% .018 155.826);--color-green-100:oklch(96.2% .044 156.743);--color-green-200:oklch(92.5% .084 155.995);--color-green-700:oklch(52.7% .154 150.069);--color-green-800:oklch(44.8% .119 151.328);--color-purple-50:oklch(97.7% .014 308.299);--color-purple-100:oklch(94.6% .033 307.174);--color-purple-200:oklch(90.2% .063 306.703);--color-purple-300:oklch(82.7% .119 306.383);--color-purple-400:oklch(71.4% .203 305.504);--color-purple-500:oklch(62.7% .265 303.9);--color-purple-600:oklch(55.8% .288 302.321);--color-purple-700:oklch(49.6% .265 301.924);--color-purple-900:oklch(38.1% .176 304.987);--color-gray-50:oklch(98.5% .002 247.839);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-200:oklch(92.8% .006 264.531);--color-gray-300:oklch(87.2% .01 258.338);--color-gray-400:oklch(70.7% .022 261.325);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-600:oklch(44.6% .03 256.802);--color-gray-700:oklch(37.3% .034 259.733);--color-gray-800:oklch(27.8% .033 256.848);--color-gray-900:oklch(21% .034 264.665);--color-white:#fff;--spacing:.25rem;--container-md:28rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25 / 1.875);--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-normal:0em;--tracking-wider:.05em;--leading-tight:1.25;--radius-sm:.25rem;--radius-md:.375rem;--radius-lg:.5rem;--ease-in-out:cubic-bezier(.4, 0, .2, 1);--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}button:not(:disabled),[role=button]:not([aria-disabled=true]){cursor:pointer}button:disabled,[role=button][aria-disabled=true]{cursor:not-allowed}}@layer components{.plum-blueprint-field{background:#f9fafb;border:1px solid #e5e7eb;border-radius:.75rem;grid-template-columns:minmax(0,1fr);gap:1rem;padding:1rem;display:grid}.plum-blueprint-field-header{border-bottom:1px solid #e5e7eb;justify-content:space-between;align-items:center;gap:1rem;padding-bottom:.75rem;display:flex}.plum-blueprint-field-title{color:#111827;text-overflow:ellipsis;white-space:nowrap;font-size:.875rem;font-weight:650;overflow:hidden}.plum-blueprint-field-type{color:#6b7280;margin-left:.5rem;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.75rem;font-weight:400}@media (min-width:640px){.plum-blueprint-field{grid-template-columns:repeat(3,minmax(0,1fr))}}.plum-rich-text-content{outline:none;min-height:14rem}.plum-rich-text-content>*+*{margin-top:.75rem}.plum-rich-text-content h2{font-size:1.25rem;font-weight:700;line-height:1.3}.plum-rich-text-content ul{padding-left:1.5rem;list-style:outside}.plum-rich-text-content ol{padding-left:1.5rem;list-style:decimal}.plum-rich-text-content blockquote{color:#4b5563;border-left:.25rem solid #d1d5db;padding-left:1rem}.plum-rich-text-content a{color:#7c3aed;text-decoration:underline}.plum-rich-text-content p.is-editor-empty:first-child:before{color:#9ca3af;content:attr(data-placeholder);float:left;pointer-events:none;height:0}}@layer utilities{.\!visible{visibility:visible!important}.invisible{visibility:hidden}.visible{visibility:visible}.sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.inset-y-0{inset-block:calc(var(--spacing) * 0)}.start{inset-inline-start:var(--spacing)}.end{inset-inline-end:var(--spacing)}.left-0{left:calc(var(--spacing) * 0)}.col-span-full{grid-column:1/-1}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.mx-auto{margin-inline:auto}.mt-1{margin-top:calc(var(--spacing) * 1)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mt-6{margin-top:calc(var(--spacing) * 6)}.mt-8{margin-top:calc(var(--spacing) * 8)}.mt-28{margin-top:calc(var(--spacing) * 28)}.mr-2{margin-right:calc(var(--spacing) * 2)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.mb-8{margin-bottom:calc(var(--spacing) * 8)}.ml-1{margin-left:calc(var(--spacing) * 1)}.ml-2{margin-left:calc(var(--spacing) * 2)}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.table{display:table}.aspect-square{aspect-ratio:1}.h-2\.5{height:calc(var(--spacing) * 2.5)}.h-4{height:calc(var(--spacing) * 4)}.h-5{height:calc(var(--spacing) * 5)}.h-9{height:calc(var(--spacing) * 9)}.h-10{height:calc(var(--spacing) * 10)}.h-12{height:calc(var(--spacing) * 12)}.h-16{height:calc(var(--spacing) * 16)}.h-32{height:calc(var(--spacing) * 32)}.h-44{height:calc(var(--spacing) * 44)}.h-full{height:100%}.min-h-16{min-height:calc(var(--spacing) * 16)}.min-h-48{min-height:calc(var(--spacing) * 48)}.min-h-full{min-height:100%}.w-2\.5{width:calc(var(--spacing) * 2.5)}.w-4{width:calc(var(--spacing) * 4)}.w-5{width:calc(var(--spacing) * 5)}.w-9{width:calc(var(--spacing) * 9)}.w-10{width:calc(var(--spacing) * 10)}.w-12{width:calc(var(--spacing) * 12)}.w-14{width:calc(var(--spacing) * 14)}.w-16{width:calc(var(--spacing) * 16)}.w-32{width:calc(var(--spacing) * 32)}.w-64{width:calc(var(--spacing) * 64)}.w-full{width:100%}.max-w-md{max-width:var(--container-md)}.min-w-0{min-width:calc(var(--spacing) * 0)}.min-w-full{min-width:100%}.flex-1{flex:1}.flex-shrink{flex-shrink:1}.shrink-0{flex-shrink:0}.grow{flex-grow:1}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.cursor-pointer{cursor:pointer}.resize{resize:both}.list-inside{list-style-position:inside}.list-disc{list-style-type:disc}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-1{gap:calc(var(--spacing) * 1)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-6{gap:calc(var(--spacing) * 6)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-8>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 8) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 8) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-x-3>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 3) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-x-reverse)))}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-gray-200>:not(:last-child)){border-color:var(--color-gray-200)}.justify-self-start{justify-self:flex-start}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-sm{border-radius:var(--radius-sm)}.border{border-style:var(--tw-border-style);border-width:1px}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-amber-200{border-color:var(--color-amber-200)}.border-amber-300{border-color:var(--color-amber-300)}.border-gray-100{border-color:var(--color-gray-100)}.border-gray-200{border-color:var(--color-gray-200)}.border-gray-300{border-color:var(--color-gray-300)}.border-green-200{border-color:var(--color-green-200)}.border-purple-200{border-color:var(--color-purple-200)}.border-purple-300{border-color:var(--color-purple-300)}.border-purple-500{border-color:var(--color-purple-500)}.border-red-200{border-color:var(--color-red-200)}.border-red-300{border-color:var(--color-red-300)}.border-transparent{border-color:#0000}.border-white\/10{border-color:#ffffff1a}@supports (color:color-mix(in lab, red, red)){.border-white\/10{border-color:color-mix(in oklab, var(--color-white) 10%, transparent)}}.bg-\[\#F6F3EF\]{background-color:#f6f3ef}.bg-amber-50{background-color:var(--color-amber-50)}.bg-amber-100{background-color:var(--color-amber-100)}.bg-gray-50{background-color:var(--color-gray-50)}.bg-gray-100{background-color:var(--color-gray-100)}.bg-green-50{background-color:var(--color-green-50)}.bg-green-100{background-color:var(--color-green-100)}.bg-green-200{background-color:var(--color-green-200)}.bg-purple-50{background-color:var(--color-purple-50)}.bg-purple-100{background-color:var(--color-purple-100)}.bg-purple-600{background-color:var(--color-purple-600)}.bg-red-50{background-color:var(--color-red-50)}.bg-red-100{background-color:var(--color-red-100)}.bg-red-200{background-color:var(--color-red-200)}.bg-white{background-color:var(--color-white)}.bg-yellow-100{background-color:var(--color-yellow-100)}.object-contain{object-fit:contain}.object-cover{object-fit:cover}.p-0{padding:calc(var(--spacing) * 0)}.p-1{padding:calc(var(--spacing) * 1)}.p-2{padding:calc(var(--spacing) * 2)}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.p-6{padding:calc(var(--spacing) * 6)}.p-10{padding:calc(var(--spacing) * 10)}.px-0\.5{padding-inline:calc(var(--spacing) * .5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-5{padding-inline:calc(var(--spacing) * 5)}.px-6{padding-inline:calc(var(--spacing) * 6)}.px-8{padding-inline:calc(var(--spacing) * 8)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:calc(var(--spacing) * 1)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-4{padding-block:calc(var(--spacing) * 4)}.py-5{padding-block:calc(var(--spacing) * 5)}.py-6{padding-block:calc(var(--spacing) * 6)}.py-8{padding-block:calc(var(--spacing) * 8)}.py-12{padding-block:calc(var(--spacing) * 12)}.pt-1{padding-top:calc(var(--spacing) * 1)}.pt-2{padding-top:calc(var(--spacing) * 2)}.pt-3{padding-top:calc(var(--spacing) * 3)}.pt-4{padding-top:calc(var(--spacing) * 4)}.pb-2{padding-bottom:calc(var(--spacing) * 2)}.pl-5{padding-left:calc(var(--spacing) * 5)}.pl-64{padding-left:calc(var(--spacing) * 64)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.align-middle{vertical-align:middle}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[0\.9375rem\]{font-size:.9375rem}.leading-7{--tw-leading:calc(var(--spacing) * 7);line-height:calc(var(--spacing) * 7)}.leading-tight{--tw-leading:var(--leading-tight);line-height:var(--leading-tight)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-normal{--tw-tracking:var(--tracking-normal);letter-spacing:var(--tracking-normal)}.tracking-wider{--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider)}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.text-amber-700{color:var(--color-amber-700)}.text-amber-800{color:var(--color-amber-800)}.text-gray-400{color:var(--color-gray-400)}.text-gray-500{color:var(--color-gray-500)}.text-gray-600{color:var(--color-gray-600)}.text-gray-700{color:var(--color-gray-700)}.text-gray-800{color:var(--color-gray-800)}.text-gray-900{color:var(--color-gray-900)}.text-green-700{color:var(--color-green-700)}.text-green-800{color:var(--color-green-800)}.text-purple-600{color:var(--color-purple-600)}.text-purple-700{color:var(--color-purple-700)}.text-red-600{color:var(--color-red-600)}.text-red-700{color:var(--color-red-700)}.text-red-800{color:var(--color-red-800)}.text-white{color:var(--color-white)}.text-yellow-800{color:var(--color-yellow-800)}.capitalize{text-transform:capitalize}.lowercase{text-transform:lowercase}.uppercase{text-transform:uppercase}.italic{font-style:italic}.line-through{text-decoration-line:line-through}.no-underline{text-decoration-line:none}.underline{text-decoration-line:underline}.decoration-red-400{-webkit-text-decoration-color:var(--color-red-400);-webkit-text-decoration-color:var(--color-red-400);text-decoration-color:var(--color-red-400)}.shadow,.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring-2{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring-purple-500{--tw-ring-color:var(--color-purple-500)}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.blur{--tw-blur:blur(8px);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-shadow{transition-property:box-shadow;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.ease-in-out{--tw-ease:var(--ease-in-out);transition-timing-function:var(--ease-in-out)}.peer-checked\:border-purple-500:is(:where(.peer):checked~*){border-color:var(--color-purple-500)}.peer-checked\:bg-purple-50:is(:where(.peer):checked~*){background-color:var(--color-purple-50)}.peer-checked\:text-purple-700:is(:where(.peer):checked~*){color:var(--color-purple-700)}.peer-focus\:ring-2:is(:where(.peer):focus~*){--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.peer-focus\:ring-purple-500:is(:where(.peer):focus~*){--tw-ring-color:var(--color-purple-500)}.peer-focus\:ring-offset-2:is(:where(.peer):focus~*){--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}@media (hover:hover){.hover\:border-purple-400:hover{border-color:var(--color-purple-400)}.hover\:border-purple-500:hover{border-color:var(--color-purple-500)}.hover\:bg-amber-100:hover{background-color:var(--color-amber-100)}.hover\:bg-gray-50:hover{background-color:var(--color-gray-50)}.hover\:bg-gray-100:hover{background-color:var(--color-gray-100)}.hover\:bg-purple-50:hover{background-color:var(--color-purple-50)}.hover\:bg-purple-100:hover{background-color:var(--color-purple-100)}.hover\:bg-purple-700:hover{background-color:var(--color-purple-700)}.hover\:bg-red-50:hover{background-color:var(--color-red-50)}.hover\:text-gray-700:hover{color:var(--color-gray-700)}.hover\:text-gray-900:hover{color:var(--color-gray-900)}.hover\:text-purple-700:hover{color:var(--color-purple-700)}.hover\:text-purple-900:hover{color:var(--color-purple-900)}.hover\:text-red-700:hover{color:var(--color-red-700)}.hover\:text-red-900:hover{color:var(--color-red-900)}.hover\:shadow-md:hover{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a), 0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}}.focus\:border-purple-500:focus{border-color:var(--color-purple-500)}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus\:ring-purple-500:focus{--tw-ring-color:var(--color-purple-500)}.focus\:ring-offset-2:focus{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-30:disabled{opacity:.3}.disabled\:opacity-40:disabled{opacity:.4}.disabled\:opacity-50:disabled{opacity:.5}.has-checked\:border-purple-500:has(:checked){border-color:var(--color-purple-500)}.has-checked\:ring-2:has(:checked){--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.has-checked\:ring-purple-500:has(:checked){--tw-ring-color:var(--color-purple-500)}@media (min-width:40rem){.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.sm\:px-6{padding-inline:calc(var(--spacing) * 6)}}@media (min-width:48rem){.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}@media (min-width:64rem){.lg\:col-span-2{grid-column:span 2/span 2}.lg\:col-start-3{grid-column-start:3}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:px-8{padding-inline:calc(var(--spacing) * 8)}}}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-space-x-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}
@@ -11,9 +11,9 @@ module Plum
11
11
  ].freeze
12
12
 
13
13
  before_action :set_content_type
14
- before_action :set_entry, only: [ :edit, :update, :destroy, :image_field, :translate ]
14
+ before_action :set_entry, only: [ :edit, :update, :destroy, :image_field, :translate, :write, :diff, :publish_draft, :discard_draft ]
15
15
  before_action :set_form_collections, only: [ :new, :create, :edit, :update ]
16
- before_action :require_editor, only: [ :new, :create, :edit, :update, :destroy, :image_field, :translate ]
16
+ before_action :require_editor, only: [ :new, :create, :edit, :update, :destroy, :image_field, :translate, :write, :diff, :publish_draft, :discard_draft ]
17
17
 
18
18
  def index
19
19
  @entries = @content_type.entries.order(updated_at: :desc)
@@ -38,13 +38,45 @@ module Plum
38
38
  def edit
39
39
  end
40
40
 
41
+ # Distraction-free writing surface for the entry's first rich text
42
+ # field. Saves go through #update with write_mode, which merges the
43
+ # submitted fields into the existing data instead of replacing it.
44
+ def write
45
+ @field = @content_type.fields.find { |field| field["type"] == "rich_text" && field["handle"].present? }
46
+ unless @field
47
+ return redirect_to edit_cp_content_type_entry_path(@content_type, @entry),
48
+ alert: "This content type has no rich text field to write in"
49
+ end
50
+
51
+ render layout: "plum/write"
52
+ end
53
+
41
54
  def update
55
+ # Writing-mode saves on a live entry become a working draft so the
56
+ # published version (and its cached pages) stay untouched.
57
+ return save_write_mode_draft if write_mode? && @entry.published?
58
+
42
59
  @entry.assign_attributes(entry_params)
60
+ # The full form displays draft values when a draft exists, so a form
61
+ # save publishes what the editor saw — the draft is no longer pending.
62
+ @entry.draft_data = nil unless write_mode?
43
63
 
44
64
  if save_entry(@entry)
45
- redirect_to edit_cp_content_type_entry_path(@content_type, @entry), notice: "Entry updated"
65
+ respond_to do |format|
66
+ format.html do
67
+ if write_mode?
68
+ redirect_to write_cp_content_type_entry_path(@content_type, @entry), notice: "Saved"
69
+ else
70
+ redirect_to edit_cp_content_type_entry_path(@content_type, @entry), notice: "Entry updated"
71
+ end
72
+ end
73
+ format.json { render json: { saved: true, saved_at: Time.current.iso8601 } }
74
+ end
46
75
  else
47
- render :edit, status: :unprocessable_entity
76
+ respond_to do |format|
77
+ format.html { render :edit, status: :unprocessable_entity }
78
+ format.json { render json: { saved: false, errors: @entry.errors.full_messages }, status: :unprocessable_entity }
79
+ end
48
80
  end
49
81
  end
50
82
 
@@ -68,6 +100,34 @@ module Plum
68
100
  end
69
101
  end
70
102
 
103
+ # Git-style review of draft vs. live content.
104
+ def diff
105
+ unless @entry.has_draft?
106
+ return redirect_to edit_cp_content_type_entry_path(@content_type, @entry),
107
+ notice: "No draft changes to review"
108
+ end
109
+
110
+ @diff = Plum::DraftDiff.new(@entry)
111
+ end
112
+
113
+ def publish_draft
114
+ published = @entry.publish_draft!(editor: current_user)
115
+
116
+ respond_to do |format|
117
+ format.html do
118
+ redirect_back fallback_location: edit_cp_content_type_entry_path(@content_type, @entry),
119
+ notice: published ? "Draft published" : "No draft changes to publish"
120
+ end
121
+ format.json { render json: { published: published, published_at: Time.current.iso8601 } }
122
+ end
123
+ end
124
+
125
+ def discard_draft
126
+ @entry.discard_draft!
127
+ redirect_back fallback_location: edit_cp_content_type_entry_path(@content_type, @entry),
128
+ notice: "Draft discarded"
129
+ end
130
+
71
131
  def destroy
72
132
  @entry.destroy
73
133
  redirect_to cp_content_type_entries_path(@content_type), notice: "Entry deleted"
@@ -121,11 +181,34 @@ module Plum
121
181
  def entry_params
122
182
  permitted = params.require(:entry).permit(:title, :slug, :status, :published_at, :locale)
123
183
  if params[:entry][:data].is_a?(ActionController::Parameters)
124
- permitted[:data] = params[:entry][:data].permit!.to_h
184
+ submitted = params[:entry][:data].permit!.to_h
185
+ # Writing mode only submits the field being written, so merge it
186
+ # into the stored data rather than wiping the other fields.
187
+ submitted = @entry.data.to_h.merge(submitted) if write_mode? && @entry&.persisted?
188
+ permitted[:data] = submitted
125
189
  end
126
190
  permitted
127
191
  end
128
192
 
193
+ def write_mode?
194
+ params.dig(:entry, :write_mode).present?
195
+ end
196
+
197
+ def save_write_mode_draft
198
+ submitted = params[:entry][:data].is_a?(ActionController::Parameters) ? params[:entry][:data].permit!.to_h : {}
199
+ @entry.save_draft!(title: params.dig(:entry, :title), data: submitted)
200
+
201
+ respond_to do |format|
202
+ format.html { redirect_to write_cp_content_type_entry_path(@content_type, @entry), notice: "Draft saved" }
203
+ format.json { render json: { saved: true, draft: true, saved_at: Time.current.iso8601 } }
204
+ end
205
+ rescue ActiveRecord::RecordInvalid => e
206
+ respond_to do |format|
207
+ format.html { redirect_to write_cp_content_type_entry_path(@content_type, @entry), alert: e.record.errors.full_messages.to_sentence }
208
+ format.json { render json: { saved: false, errors: e.record.errors.full_messages }, status: :unprocessable_entity }
209
+ end
210
+ end
211
+
129
212
  def save_entry(entry)
130
213
  saved = false
131
214
  entry.transaction do
@@ -0,0 +1,12 @@
1
+ module Plum
2
+ module Cp
3
+ class StaticCacheController < BaseController
4
+ before_action :require_admin
5
+
6
+ def destroy
7
+ Plum::StaticCache.flush_site!(current_site)
8
+ redirect_back fallback_location: cp_root_path, notice: "Page cache cleared"
9
+ end
10
+ end
11
+ end
12
+ end
@@ -1,6 +1,15 @@
1
1
  module Plum
2
2
  class FormSubmissionsController < ApplicationController
3
+ # Public forms are served from the static cache, so they can't carry a
4
+ # per-session CSRF token. Submissions are unauthenticated writes guarded
5
+ # by the honeypot below instead.
6
+ skip_forgery_protection
7
+
3
8
  def create
9
+ # Pretend success when the honeypot is filled so bots don't learn
10
+ # they were caught.
11
+ return redirect_to safe_return_path, notice: "Form submitted" if honeypot_tripped?
12
+
4
13
  form_definition = current_site.form_definitions.find_by!(handle: params[:handle])
5
14
  submission = form_definition.form_submissions.build(
6
15
  site: current_site,
@@ -17,6 +26,10 @@ module Plum
17
26
 
18
27
  private
19
28
 
29
+ def honeypot_tripped?
30
+ params.dig(:form_submission, :website).present?
31
+ end
32
+
20
33
  def deliver_submission_notification(submission)
21
34
  return if submission.form_definition.notification_email.blank?
22
35
 
@@ -2,6 +2,10 @@ module Plum
2
2
  class PagesController < ApplicationController
3
3
  HOMEPAGE_SLUG = "home".freeze
4
4
 
5
+ # Search stays dynamic (query-dependent); the middleware also refuses to
6
+ # store any response with a query string or a non-200 status.
7
+ after_action :mark_static_cacheable, only: [ :home, :localized_home, :show ]
8
+
5
9
  def home
6
10
  @site_settings = SiteSetting.instance(current_site)
7
11
  @entry = Entry.for_site(current_site).live
@@ -91,6 +95,10 @@ module Plum
91
95
 
92
96
  PER_PAGE = 12
93
97
 
98
+ def mark_static_cacheable
99
+ response.headers[Plum::StaticCache::MARKER_HEADER] = "store" if response.status == 200
100
+ end
101
+
94
102
  def render_collection(content_type)
95
103
  context = build_context
96
104
  page = [ params.fetch(:page, 1).to_i, 1 ].max
@@ -6,6 +6,8 @@ module Plum
6
6
  asset_path = ThemeResolver.new.find_asset(params[:theme_handle], params[:path])
7
7
  return head :not_found unless asset_path
8
8
 
9
+ expires_in 1.hour, public: true
10
+ response.headers[Plum::StaticCache::MARKER_HEADER] = "store"
9
11
  send_file asset_path,
10
12
  type: Rack::Mime.mime_type(asset_path.extname, "application/octet-stream"),
11
13
  disposition: "inline"