domma-cms 0.6.12 → 0.6.14

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.
@@ -1,5 +1,5 @@
1
1
  {
2
- "/": 139,
2
+ "/": 140,
3
3
  "/about": 71,
4
4
  "/blog": 36,
5
5
  "/contact": 30,
@@ -16,7 +16,7 @@
16
16
  "/resources/dependencies": 2,
17
17
  "/resources/components": 6,
18
18
  "/gdpr": 3,
19
- "/scratch": 67,
19
+ "/scratch": 69,
20
20
  "/getting-started": 3,
21
21
  "/resources/pro": 1,
22
22
  "/todo": 23,
@@ -0,0 +1,67 @@
1
+ <div class="view-header">
2
+ <h1><span data-icon="search"></span> Site Search</h1>
3
+ <div>
4
+ <button id="save-settings-btn" class="btn btn-primary">
5
+ <span data-icon="save"></span> Save
6
+ </button>
7
+ </div>
8
+ </div>
9
+
10
+ <div class="card mb-4">
11
+ <div class="card-header"><h2>Settings</h2></div>
12
+ <div class="card-body">
13
+
14
+ <div class="row mb-3">
15
+ <div class="col">
16
+ <label class="form-label">Search placeholder text</label>
17
+ <input id="field-placeholder" type="text" class="form-input" placeholder="Search pages...">
18
+ <span class="form-hint">Displayed inside the search input when empty.</span>
19
+ </div>
20
+ </div>
21
+
22
+ <div class="row mb-3">
23
+ <div class="col">
24
+ <label class="form-check-label">
25
+ <input id="field-keyboard-shortcut" type="checkbox">
26
+ Enable keyboard shortcut (Ctrl+K / ⌘K)
27
+ </label>
28
+ <span class="form-hint">Allows visitors to open the search overlay using a keyboard shortcut.</span>
29
+ </div>
30
+ </div>
31
+
32
+ <div class="row mb-3">
33
+ <div class="col-6">
34
+ <label class="form-label">Max results</label>
35
+ <input id="field-max-results" type="number" class="form-input" min="1" max="50" placeholder="10">
36
+ <span class="form-hint">Maximum number of results to return per search (1–50).</span>
37
+ </div>
38
+ <div class="col-6">
39
+ <label class="form-label">Minimum query length</label>
40
+ <input id="field-min-query-length" type="number" class="form-input" min="1" max="5" placeholder="2">
41
+ <span class="form-hint">Minimum characters required before searching (1–5).</span>
42
+ </div>
43
+ </div>
44
+
45
+ <div class="row">
46
+ <div class="col-6">
47
+ <label class="form-label">Debounce delay (ms)</label>
48
+ <input id="field-debounce-ms" type="number" class="form-input" min="100" max="1000" placeholder="300">
49
+ <span class="form-hint">Delay after typing stops before search fires (100–1000ms).</span>
50
+ </div>
51
+ </div>
52
+
53
+ </div>
54
+ </div>
55
+
56
+ <div class="card mb-4">
57
+ <div class="card-header"><h2>How it works</h2></div>
58
+ <div class="card-body">
59
+ <p class="text-muted mb-2">Site Search adds a search icon to the public navbar. Visitors can click it or use
60
+ the keyboard shortcut to open a full-screen overlay with live results.</p>
61
+ <ul class="text-muted" style="padding-left:1.25rem;line-height:1.8;">
62
+ <li>Results are weighted: title matches score highest, followed by tags, description, and page content.</li>
63
+ <li>Draft pages and private pages are excluded from results.</li>
64
+ <li>Use <kbd>↑</kbd> <kbd>↓</kbd> to navigate results, <kbd>↵</kbd> to open, <kbd>Esc</kbd> to close.</li>
65
+ </ul>
66
+ </div>
67
+ </div>
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Site Search Plugin — Admin Settings View
3
+ */
4
+ import {apiRequest} from '/admin/js/api.js';
5
+
6
+ export const siteSearchView = {
7
+ templateUrl: '/plugins/site-search/admin/templates/site-search.html',
8
+
9
+ async onMount($container) {
10
+ let settings = {};
11
+ try {
12
+ settings = await apiRequest('/plugins/site-search/settings');
13
+ } catch {
14
+ E.toast('Could not load settings.', {type: 'error'});
15
+ }
16
+
17
+ $container.find('#field-placeholder').val(settings.placeholder || 'Search pages...');
18
+ $container.find('#field-keyboard-shortcut').prop('checked', settings.keyboardShortcut !== false);
19
+ $container.find('#field-max-results').val(settings.maxResults ?? 10);
20
+ $container.find('#field-min-query-length').val(settings.minQueryLength ?? 2);
21
+ $container.find('#field-debounce-ms').val(settings.debounceMs ?? 300);
22
+
23
+ $container.find('#save-settings-btn').off('click').on('click', async () => {
24
+ const data = {
25
+ placeholder: $container.find('#field-placeholder').val() || 'Search pages...',
26
+ keyboardShortcut: $container.find('#field-keyboard-shortcut').prop('checked'),
27
+ maxResults: parseInt($container.find('#field-max-results').val(), 10) || 10,
28
+ minQueryLength: parseInt($container.find('#field-min-query-length').val(), 10) || 2,
29
+ debounceMs: parseInt($container.find('#field-debounce-ms').val(), 10) || 300
30
+ };
31
+
32
+ try {
33
+ await apiRequest('/plugins/site-search/settings', {
34
+ method: 'PUT',
35
+ body: JSON.stringify(data)
36
+ });
37
+ E.toast('Settings saved.', {type: 'success'});
38
+ } catch {
39
+ E.toast('Failed to save settings.', {type: 'error'});
40
+ }
41
+ });
42
+
43
+ Domma.icons.scan();
44
+ }
45
+ };
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Site Search Plugin — Default Configuration
3
+ */
4
+ export default {
5
+ placeholder: 'Search pages...',
6
+ keyboardShortcut: true,
7
+ maxResults: 10,
8
+ minQueryLength: 2,
9
+ debounceMs: 300
10
+ };
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Site Search Plugin — Server
3
+ *
4
+ * Endpoints (prefix: /api/plugins/site-search):
5
+ * GET /search?q=term — public: full-text search across all published pages
6
+ * GET /settings — public: return plugin settings (used by inject script)
7
+ * PUT /settings — admin-auth: save user overrides
8
+ */
9
+ import {listPages} from '../../server/services/content.js';
10
+ import {getPluginSettings, savePluginState} from '../../server/services/plugins.js';
11
+
12
+ /**
13
+ * Strip markdown syntax and shortcodes from content for plain-text searching.
14
+ *
15
+ * @param {string} content
16
+ * @returns {string}
17
+ */
18
+ function stripMarkdown(content) {
19
+ if (!content) return '';
20
+ return content
21
+ // Remove shortcodes (self-closing and wrapping)
22
+ .replace(/\[[a-z][^\]]*\/\]/gi, '')
23
+ .replace(/\[[a-z][^\]]*\][\s\S]*?\[\/[a-z]+\]/gi, '')
24
+ .replace(/\[[a-z][^\]]*\]/gi, '')
25
+ // Remove HTML tags
26
+ .replace(/<[^>]+>/g, '')
27
+ // Remove headings (keep text)
28
+ .replace(/^#{1,6}\s+/gm, '')
29
+ // Remove bold / italic
30
+ .replace(/\*{1,3}([^*]+)\*{1,3}/g, '$1')
31
+ .replace(/_{1,3}([^_]+)_{1,3}/g, '$1')
32
+ // Remove images
33
+ .replace(/!\[[^\]]*\]\([^)]*\)/g, '')
34
+ // Remove links (keep text)
35
+ .replace(/\[([^\]]+)\]\([^)]*\)/g, '$1')
36
+ // Remove code blocks
37
+ .replace(/```[\s\S]*?```/g, '')
38
+ .replace(/`[^`]+`/g, '')
39
+ // Remove blockquotes marker
40
+ .replace(/^>\s+/gm, '')
41
+ // Collapse whitespace
42
+ .replace(/\s+/g, ' ')
43
+ .trim();
44
+ }
45
+
46
+ /**
47
+ * Build a snippet of ~length chars around the first occurrence of term in content.
48
+ *
49
+ * @param {string} rawContent - raw markdown content
50
+ * @param {string} term
51
+ * @param {number} length
52
+ * @returns {string}
53
+ */
54
+ function buildSnippet(rawContent, term, length = 120) {
55
+ const text = stripMarkdown(rawContent);
56
+ if (!text) return '';
57
+
58
+ const lower = text.toLowerCase();
59
+ const idx = lower.indexOf(term.toLowerCase());
60
+
61
+ if (idx === -1) {
62
+ return text.length > length ? text.slice(0, length) + '…' : text;
63
+ }
64
+
65
+ const half = Math.floor(length / 2);
66
+ const start = Math.max(0, idx - half);
67
+ const end = Math.min(text.length, start + length);
68
+ const snippet = text.slice(start, end);
69
+
70
+ return (start > 0 ? '…' : '') + snippet + (end < text.length ? '…' : '');
71
+ }
72
+
73
+ /**
74
+ * Score a page against a search query using weighted field matching.
75
+ *
76
+ * @param {object} page
77
+ * @param {string} query
78
+ * @param {string[]} words - query split into words
79
+ * @returns {number}
80
+ */
81
+ function scorePage(page, query, words) {
82
+ let score = 0;
83
+ const title = (page.title || '').toLowerCase();
84
+ const description = (page.description || '').toLowerCase();
85
+ const tagsRaw = Array.isArray(page.tags) ? page.tags : [];
86
+ const tags = tagsRaw.map(t => t.toLowerCase());
87
+ const stripped = stripMarkdown(page.content || '').toLowerCase();
88
+ const q = query.toLowerCase();
89
+
90
+ // Title scoring
91
+ if (title === q) {
92
+ score += 100;
93
+ } else if (title.includes(q)) {
94
+ score += 60;
95
+ } else if (words.every(w => title.includes(w))) {
96
+ score += 40;
97
+ } else if (words.some(w => title.includes(w))) {
98
+ score += 20;
99
+ }
100
+
101
+ // Tags scoring
102
+ if (tags.includes(q)) {
103
+ score += 30;
104
+ } else if (tags.some(t => t.includes(q))) {
105
+ score += 20;
106
+ } else if (words.every(w => tags.some(t => t.includes(w)))) {
107
+ score += 15;
108
+ } else if (words.some(w => tags.some(t => t.includes(w)))) {
109
+ score += 8;
110
+ }
111
+
112
+ // Description scoring
113
+ if (description.includes(q)) {
114
+ score += 25;
115
+ } else if (words.some(w => description.includes(w))) {
116
+ score += 10;
117
+ }
118
+
119
+ // Content scoring
120
+ if (stripped.includes(q)) {
121
+ score += 10;
122
+ } else if (words.every(w => stripped.includes(w))) {
123
+ score += 5;
124
+ } else if (words.some(w => stripped.includes(w))) {
125
+ score += 2;
126
+ }
127
+
128
+ return score;
129
+ }
130
+
131
+ export default async function siteSearchPlugin(fastify, options) {
132
+ const {authenticate, requireAdmin} = options.auth;
133
+
134
+ // -------------------------------------------------------------------------
135
+ // Public search endpoint
136
+ // -------------------------------------------------------------------------
137
+
138
+ fastify.get('/search', async (request, reply) => {
139
+ const settings = getPluginSettings('site-search');
140
+ const minLen = settings.minQueryLength ?? 2;
141
+ const maxResults = settings.maxResults ?? 10;
142
+
143
+ const q = (request.query.q || '').trim();
144
+ if (q.length < minLen) {
145
+ return reply.status(400).send({error: `Query must be at least ${minLen} characters`});
146
+ }
147
+
148
+ const pages = await listPages();
149
+ const words = q.toLowerCase().split(/\s+/).filter(Boolean);
150
+
151
+ const results = [];
152
+ for (const page of pages) {
153
+ // Filter out drafts and private pages
154
+ if (page.status === 'draft') continue;
155
+ if (page.visibility === 'private') continue;
156
+
157
+ const score = scorePage(page, q, words);
158
+ if (score === 0) continue;
159
+
160
+ results.push({
161
+ title: page.title || 'Untitled',
162
+ url: page.urlPath,
163
+ description: page.description || '',
164
+ tags: Array.isArray(page.tags) ? page.tags : [],
165
+ snippet: buildSnippet(page.content || '', q),
166
+ score
167
+ });
168
+ }
169
+
170
+ results.sort((a, b) => b.score - a.score);
171
+
172
+ return {results: results.slice(0, maxResults)};
173
+ });
174
+
175
+ // -------------------------------------------------------------------------
176
+ // Settings routes
177
+ // -------------------------------------------------------------------------
178
+
179
+ fastify.get('/settings', async () => {
180
+ return getPluginSettings('site-search');
181
+ });
182
+
183
+ fastify.put('/settings', {preHandler: [authenticate, requireAdmin]}, async (request) => {
184
+ const body = request.body || {};
185
+ savePluginState('site-search', {settings: body});
186
+ return {ok: true};
187
+ });
188
+ }
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "site-search",
3
+ "displayName": "Site Search",
4
+ "version": "1.0.0",
5
+ "description": "Full-text search for the public site. Search icon in navbar, Cmd+K shortcut, weighted results.",
6
+ "author": "Darryl Waterhouse",
7
+ "date": "2026-03-19",
8
+ "icon": "search",
9
+ "admin": {
10
+ "sidebar": [
11
+ {
12
+ "id": "site-search",
13
+ "text": "Site Search",
14
+ "icon": "search",
15
+ "url": "#/plugins/site-search",
16
+ "section": "#/plugins/site-search"
17
+ }
18
+ ],
19
+ "routes": [
20
+ {
21
+ "path": "/plugins/site-search",
22
+ "view": "plugin-site-search",
23
+ "title": "Site Search - Domma CMS"
24
+ }
25
+ ],
26
+ "views": {
27
+ "plugin-site-search": {
28
+ "entry": "site-search/admin/views/site-search.js",
29
+ "exportName": "siteSearchView"
30
+ }
31
+ }
32
+ },
33
+ "inject": {
34
+ "headLate": "public/inject-head.html",
35
+ "bodyEnd": "public/inject-body.html"
36
+ }
37
+ }
@@ -0,0 +1,17 @@
1
+ <!-- site-search: load settings then initialise search UI -->
2
+ <script>
3
+ (function () {
4
+ fetch('/api/plugins/site-search/settings')
5
+ .then(function (r) {
6
+ return r.json();
7
+ })
8
+ .then(function (cfg) {
9
+ window.__SITE_SEARCH__ = cfg;
10
+ var s = document.createElement('script');
11
+ s.src = '/plugins/site-search/public/search.js';
12
+ document.body.appendChild(s);
13
+ })
14
+ .catch(function () {
15
+ });
16
+ }());
17
+ </script>
@@ -0,0 +1 @@
1
+ <link rel="stylesheet" href="/plugins/site-search/public/search.css">
@@ -0,0 +1 @@
1
+ .site-search-trigger{display:inline-flex;align-items:center;justify-content:center;background:none;border:none;cursor:pointer;padding:6px 8px;border-radius:var(--dm-radius, 6px);color:inherit;opacity:.75;transition:opacity .15s,background .15s;font-size:var(--dm-font-size-sm, 14px)}.site-search-trigger:hover{opacity:1;background:var(--dm-surface-hover, rgba(0, 0, 0, .06))}.site-search-trigger svg,.site-search-trigger span[data-icon]{width:18px;height:18px}.site-search-shortcut-hint{display:inline-block;font-size:11px;line-height:1;padding:2px 5px;border-radius:4px;border:1px solid var(--dm-border, rgba(0, 0, 0, .15));color:var(--dm-text-muted, #888);margin-left:4px;vertical-align:middle;font-family:var(--dm-font-mono, monospace)}.site-search-overlay{position:fixed;inset:0;z-index:10000;background:#00000073;backdrop-filter:blur(3px);-webkit-backdrop-filter:blur(3px);display:flex;align-items:flex-start;justify-content:center;padding-top:10vh;animation:ss-overlay-in .15s ease}@keyframes ss-overlay-in{0%{opacity:0}to{opacity:1}}.site-search-panel{width:100%;max-width:600px;margin:0 16px;border-radius:var(--dm-radius-lg, 10px);box-shadow:0 20px 60px #00000059;background:var(--dm-bg, #fff);overflow:hidden;animation:ss-panel-in .15s ease}@keyframes ss-panel-in{0%{transform:translateY(-12px);opacity:0}to{transform:translateY(0);opacity:1}}.site-search-header{display:flex;align-items:center;padding:12px 16px;border-bottom:1px solid var(--dm-border, rgba(0, 0, 0, .1));gap:8px}.site-search-header span[data-icon],.site-search-header svg{width:18px;height:18px;flex-shrink:0;opacity:.5}.site-search-input{flex:1;border:none;outline:none;background:transparent;font-size:var(--dm-font-size-lg, 16px);color:var(--dm-text, #111);font-family:var(--dm-font-sans, sans-serif);padding:0}.site-search-input::placeholder{color:var(--dm-text-muted, #aaa)}.site-search-close-btn{display:inline-flex;align-items:center;justify-content:center;background:none;border:none;cursor:pointer;color:var(--dm-text-muted, #999);border-radius:var(--dm-radius, 6px);padding:4px 8px;font-size:12px;font-family:var(--dm-font-mono, monospace);border:1px solid var(--dm-border, rgba(0, 0, 0, .15));transition:color .15s;flex-shrink:0}.site-search-close-btn:hover{color:var(--dm-text, #111)}.site-search-results{max-height:60vh;overflow-y:auto;padding:8px 0}.site-search-result{display:block;padding:10px 16px;text-decoration:none;color:var(--dm-text, #111);border-left:3px solid transparent;transition:background .1s,border-color .1s;cursor:pointer}.site-search-result:hover,.site-search-result.active{background:var(--dm-surface-hover, rgba(0, 0, 0, .05));border-left-color:var(--dm-primary, #5b6af0)}.site-search-result-title{font-weight:600;font-size:var(--dm-font-size-sm, 14px);margin-bottom:2px;color:var(--dm-text, #111)}.site-search-result-snippet{font-size:12px;color:var(--dm-text-muted, #888);line-height:1.4;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.site-search-result-snippet mark{background:var(--dm-primary-subtle, rgba(91, 106, 240, .15));color:inherit;border-radius:2px;padding:0 1px}.site-search-loading,.site-search-empty{padding:24px 16px;text-align:center;color:var(--dm-text-muted, #888);font-size:var(--dm-font-size-sm, 14px)}.site-search-loading-dots{display:inline-flex;gap:4px}.site-search-loading-dots span{width:6px;height:6px;border-radius:50%;background:var(--dm-text-muted, #aaa);animation:ss-dot-pulse 1.2s ease-in-out infinite}.site-search-loading-dots span:nth-child(2){animation-delay:.2s}.site-search-loading-dots span:nth-child(3){animation-delay:.4s}@keyframes ss-dot-pulse{0%,80%,to{transform:scale(.6);opacity:.4}40%{transform:scale(1);opacity:1}}.site-search-results:not(:empty){border-top:1px solid var(--dm-border, rgba(0, 0, 0, .07))}.site-search-footer{padding:6px 16px;border-top:1px solid var(--dm-border, rgba(0, 0, 0, .07));display:flex;gap:12px;font-size:11px;color:var(--dm-text-muted, #aaa)}.site-search-footer kbd{display:inline-block;padding:1px 4px;border-radius:3px;border:1px solid var(--dm-border, rgba(0, 0, 0, .2));font-family:var(--dm-font-mono, monospace);font-size:10px}@media(max-width:640px){.site-search-overlay{padding-top:5vh;align-items:flex-start}.site-search-panel{margin:0 8px;border-radius:var(--dm-radius, 6px)}.site-search-results{max-height:70vh}.site-search-shortcut-hint{display:none}}
@@ -0,0 +1 @@
1
+ (function(){"use strict";var f=window.__SITE_SEARCH__||{},S=f.placeholder||"Search pages...",b=f.keyboardShortcut!==!1,N=f.debounceMs||300,d=null,o=null,c=null,m=null,u=!1;function h(){var e=document.querySelector("#site-navbar, .navbar");if(e){var t=e.querySelector(".navbar-actions");if(!t){var a=e.querySelector(".navbar-container")||e;t=document.createElement("div"),t.className="navbar-actions",a.appendChild(t)}if(!t.querySelector(".site-search-trigger")){var s=/Mac|iPhone|iPad|iPod/.test(navigator.platform||navigator.userAgent),r=s?"\u2318K":"Ctrl+K",n=document.createElement("button");n.className="navbar-action-link site-search-trigger",n.setAttribute("aria-label","Search"),n.setAttribute("type","button");var i=document.createElement("span");if(i.setAttribute("data-icon","search"),n.appendChild(i),b){var l=document.createElement("span");l.className="site-search-shortcut-hint",l.textContent=r,n.appendChild(l)}t.insertBefore(n,t.firstChild),n.addEventListener("click",E),window.Domma&&Domma.icons&&Domma.icons.scan&&Domma.icons.scan(n)}}}function g(){if(document.querySelector("#site-navbar .navbar-actions, .navbar .navbar-actions")){h();return}var e=new MutationObserver(function(){var t=document.querySelector("#site-navbar, .navbar");t&&(e.disconnect(),h())});e.observe(document.body,{childList:!0,subtree:!0}),setTimeout(function(){e.disconnect(),h()},5e3)}function w(){var e=document.createElement("div");e.className="site-search-overlay",e.setAttribute("role","dialog"),e.setAttribute("aria-modal","true"),e.setAttribute("aria-label","Site search");var t=document.createElement("div");t.className="site-search-panel",t.setAttribute("role","search");var a=document.createElement("div");a.className="site-search-header";var s=document.createElement("span");s.setAttribute("data-icon","search"),a.appendChild(s),o=document.createElement("input"),o.className="site-search-input",o.type="search",o.setAttribute("autocomplete","off"),o.setAttribute("autocorrect","off"),o.setAttribute("spellcheck","false"),o.setAttribute("aria-label","Search"),o.placeholder=S,a.appendChild(o);var r=document.createElement("button");r.className="site-search-close-btn",r.setAttribute("type","button"),r.setAttribute("aria-label","Close search"),r.textContent="Esc",a.appendChild(r),t.appendChild(a),c=document.createElement("div"),c.className="site-search-results",c.setAttribute("role","listbox"),c.setAttribute("aria-live","polite"),t.appendChild(c);var n=document.createElement("div");return n.className="site-search-footer",n.innerHTML="<span><kbd>\u2191</kbd><kbd>\u2193</kbd> navigate</span><span><kbd>\u21B5</kbd> open</span><span><kbd>Esc</kbd> close</span>",t.appendChild(n),e.appendChild(t),e.addEventListener("click",function(i){i.target===e&&v()}),r.addEventListener("click",v),o.addEventListener("input",function(){clearTimeout(m);var i=o.value.trim();if(!i){C();return}m=setTimeout(function(){k(i)},N)}),o.addEventListener("keydown",D),window.Domma&&Domma.icons&&Domma.icons.scan&&Domma.icons.scan(a),e}function E(){u||(u=!0,d=w(),document.body.appendChild(d),document.body.style.overflow="hidden",setTimeout(function(){o&&o.focus()},30))}function v(){u&&(u=!1,clearTimeout(m),d&&d.parentNode&&d.parentNode.removeChild(d),d=null,o=null,c=null,document.body.style.overflow="")}function k(e){c&&(L(),fetch("/api/plugins/site-search/search?q="+encodeURIComponent(e)).then(function(t){return t.json()}).then(function(t){T(t.results||[],e)}).catch(function(){C();var t=document.createElement("div");t.className="site-search-empty",t.textContent="Search unavailable. Please try again.",c.appendChild(t)}))}function L(){c&&(c.innerHTML='<div class="site-search-loading"><div class="site-search-loading-dots"><span></span><span></span><span></span></div></div>')}function C(){c&&(c.innerHTML="")}function T(e,t){if(c){if(c.innerHTML="",!e.length){var a=document.createElement("div");a.className="site-search-empty",a.textContent='No results found for "'+t+'"',c.appendChild(a);return}for(var s=document.createDocumentFragment(),r=0;r<e.length;r++){var n=e[r],i=document.createElement("a");i.className="site-search-result",i.href=n.url||"/",i.setAttribute("role","option"),i.setAttribute("tabindex","-1"),i.setAttribute("data-index",String(r));var l=document.createElement("div");if(l.className="site-search-result-title",y(l,n.title||"Untitled",t),i.appendChild(l),n.snippet){var p=document.createElement("div");p.className="site-search-result-snippet",y(p,n.snippet,t),i.appendChild(p)}i.addEventListener("click",v),s.appendChild(i)}c.appendChild(s)}}function y(e,t,a){if(!a){e.textContent=t;return}var s=a.trim().split(/\s+/).filter(Boolean);if(!s.length){e.textContent=t;return}var r;try{r=new RegExp("("+s.map(function(p){return p.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}).join("|")+")","gi")}catch{e.textContent=t;return}for(var n=t.split(r),i=0;i<n.length;i++){if(r.test(n[i])){var l=document.createElement("mark");l.textContent=n[i],e.appendChild(l)}else e.appendChild(document.createTextNode(n[i]));r.lastIndex=0}}function D(e){if(e.key==="Escape"){e.preventDefault(),v();return}if(c){var t=c.querySelectorAll(".site-search-result");if(t.length){for(var a=c.querySelector(".site-search-result.active"),s=-1,r=0;r<t.length;r++)if(t[r]===a){s=r;break}if(e.key==="ArrowDown")e.preventDefault(),A(t,s<t.length-1?s+1:0);else if(e.key==="ArrowUp")e.preventDefault(),A(t,s>0?s-1:t.length-1);else if(e.key==="Enter"&&a){e.preventDefault();var n=a.getAttribute("href");v(),window.location.href=n}}}}function A(e,t){for(var a=0;a<e.length;a++)e[a].classList.toggle("active",a===t);e[t]&&e[t].scrollIntoView({block:"nearest"})}b&&document.addEventListener("keydown",function(e){if((e.metaKey||e.ctrlKey)&&e.key==="k"){var t=document.activeElement&&document.activeElement.tagName;if((t==="INPUT"||t==="TEXTAREA"||t==="SELECT")&&!u)return;e.preventDefault(),u?v():E()}}),document.readyState==="loading"?document.addEventListener("DOMContentLoaded",g):g()})();
@@ -1 +1 @@
1
- body,button,input,select,textarea{font-family:Roboto,sans-serif}.site-main{min-height:calc(100vh - 60px);padding-top:2rem;padding-bottom:4rem}.site-main.with-sidebar{display:grid;grid-template-columns:260px 1fr;gap:0}.site-sidebar{min-height:100%;border-right:1px solid var(--border-color, rgba(255,255,255,.08))}.site-content{overflow:hidden}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}.container{max-width:860px;margin:0 auto;padding:0 1.5rem}.page-title{font-size:2rem;font-weight:700;margin-bottom:1.5rem;line-height:1.2}.page-body{line-height:1.7;font-size:1rem}.page-body h1,.page-body h2,.page-body h3,.page-body h4{margin-top:2rem;margin-bottom:.75rem;font-weight:600}.page-body h2{font-size:1.5rem}.page-body h3{font-size:1.25rem}.page-body p{margin-bottom:1rem}.page-body ul,.page-body ol{margin-bottom:1rem;padding-left:1.5rem}.page-body a{color:var(--primary, #5b8cff)}.page-body a:hover{text-decoration:underline}.page-body code{font-family:Fira Code,Courier New,monospace;font-size:.9em;background:#ffffff0f;padding:.15em .35em;border-radius:3px}.page-body pre{background:#0000004d;border:1px solid rgba(255,255,255,.08);border-radius:6px;padding:1rem;overflow-x:auto;margin-bottom:1rem}.page-body pre code{background:none;padding:0}.page-body img{max-width:100%;border-radius:6px}.page-body blockquote{border-left:3px solid var(--primary, #5b8cff);margin:1.5rem 0;padding:.75rem 1rem;background:#5b8cff0f;border-radius:0 6px 6px 0}h3.accordion-header{margin:0}.accordion-button{all:unset;display:flex;align-items:center;justify-content:space-between;width:100%;cursor:pointer;font:inherit}.page-body .card-header h2{margin:0;font-size:1rem;font-weight:600;line-height:1.4}.card[data-collapsible] .card-header{cursor:pointer;user-select:none;display:flex;align-items:center;justify-content:space-between}.card[data-collapsible] .card-header:after{content:"\25be";font-size:1.1em;line-height:1;display:inline-block;transition:transform .25s ease;flex-shrink:0}.card[data-collapsible].is-collapsed .card-header:after{transform:rotate(-90deg)}.card[data-collapsible] .card-body{overflow:hidden;max-height:4000px;opacity:1;transition:max-height .3s ease,opacity .25s ease}.card[data-collapsible].is-collapsed .card-body{max-height:0;opacity:0}.navbar-link span[data-icon],.navbar-link svg,.navbar-dropdown-toggle span[data-icon],.navbar-dropdown-toggle svg,.navbar-dropdown-item span[data-icon],.navbar-dropdown-item svg{width:13px!important;height:13px!important;margin-right:10px!important}.navbar-dropdown-toggle{font-size:var(--dm-font-size-base)}@media(min-width:993px){.navbar-dropdown-toggle{font-size:var(--dm-font-size-sm)}}@media(min-width:1201px){.navbar-dropdown-toggle{font-size:var(--dm-font-size-xs)}}.dm-reduced-motion *,.dm-reduced-motion *:before,.dm-reduced-motion *:after{animation-duration:.001ms!important;animation-iteration-count:1!important;transition-duration:.001ms!important;scroll-behavior:auto!important}.page-footer{border-top:1px solid var(--border-color, rgba(255,255,255,.08));padding:1.5rem 0}.footer-inner{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:1rem}.footer-inner p{margin:0;color:var(--text-muted, #888);font-size:.875rem}.footer-links{display:flex;gap:1.25rem}.footer-links a{color:var(--text-muted, #888);font-size:.875rem;text-decoration:none}.footer-links a:hover{color:var(--text, #eee)}.footer-social{display:flex;gap:.5rem;align-items:center}.footer-social-link{display:inline-flex;align-items:center;justify-content:center;width:1.75rem;height:1.75rem;color:var(--text-muted, #888);transition:color .15s}.footer-social-link:hover{color:var(--text, #eee)}.footer-social-link svg{width:1rem;height:1rem}.footer-motion-switch{font-size:.8rem;color:var(--text-muted, #888);white-space:nowrap}.footer-motion-switch .form-switch-label{color:var(--text-muted, #888)}.footer-motion-switch .form-switch-input{width:2rem;height:1.125rem}.footer-motion-switch .form-switch-input:after{width:.875rem;height:.875rem}.footer-motion-switch .form-switch-input:checked:after{transform:translate(.875rem)}.dm-slideover-header{display:flex;align-items:center;justify-content:space-between;padding:.875rem 1.25rem;border-bottom:1px solid var(--border-color, rgba(255, 255, 255, .08));flex-shrink:0}.dm-slideover-title{margin:0;font-size:1rem;font-weight:600;line-height:1.4}.dm-slideover-body{padding:1.25rem;overflow-y:auto;flex:1}@media(max-width:768px){.site-main.with-sidebar{grid-template-columns:1fr}.site-sidebar{display:none}}.dm-spacer{display:block;width:100%}.hero-breakout{width:calc(100vw - 2rem);margin-left:calc(50% - 50vw + 1rem);margin-right:calc(50% - 50vw + 1rem)}.site-main:has(.page-body>.hero-breakout:first-child){padding-top:0}body[data-layout=landing]>.site-main{padding-top:0}body[data-layout=landing]>.site-main .container{max-width:none;padding:0}body[data-layout=landing] .page-body{padding-left:1.5rem;padding-right:1.5rem}body[data-layout=landing] .page-body>p,body[data-layout=landing] .page-body>h1,body[data-layout=landing] .page-body>h2,body[data-layout=landing] .page-body>h3,body[data-layout=landing] .page-body>ul,body[data-layout=landing] .page-body>ol,body[data-layout=landing] .page-body>blockquote{max-width:860px;margin-left:auto;margin-right:auto}body[data-layout=landing] .page-body .hero-breakout{width:calc(100% + 3rem);margin-left:-1.5rem;margin-right:-1.5rem}body[data-layout=landing] .page-body .grid-breakout{width:calc(100% + 3rem);margin-left:-1.5rem;margin-right:-1.5rem;padding-left:1.5rem;padding-right:1.5rem}.page-body .card{transition:transform .2s ease,box-shadow .2s ease}.page-body .card:hover{transform:translateY(-3px);box-shadow:0 8px 24px #00000059}.page-body .card-header-icon-inline{display:flex;align-items:center;gap:.6rem}.page-body .card-header-icon-inline [data-icon]{flex-shrink:0;line-height:0}.page-body .card-header-icon-inline [data-icon] svg,.page-body .card-header-icon-inline>svg{display:block;width:1.25rem;height:1.25rem}.page-body .card-header-icon-stacked{display:flex;flex-direction:column;align-items:center;text-align:center;gap:.35rem;padding-top:.25rem}.page-body .card-header-icon-stacked [data-icon],.page-body .card-header-icon-stacked svg{width:2rem;height:2rem}.hero.hero-dark{background:linear-gradient(135deg,#1f2937,#111827);color:#e2e8f0}.hero .hero-content{position:relative;z-index:2}.hero.hero-left .hero-content{text-align:left;align-items:flex-start;max-width:62%}@media(max-width:768px){.hero.hero-left .hero-content{max-width:100%}}.hero .hero-cta{display:flex;gap:.85rem;flex-wrap:wrap;margin-top:1.75rem}.hero .hero-cta a{display:inline-flex;align-items:center;gap:.4rem;padding:.55rem 1.35rem;border-radius:6px;font-size:.95rem;font-weight:500;text-decoration:none;transition:background .2s ease,border-color .2s ease,transform .15s ease,box-shadow .2s ease}.hero .hero-cta a:first-child{background:#ffffffeb;color:#111;border:1px solid transparent}.hero .hero-cta a:first-child:hover{background:#fff;box-shadow:0 4px 16px #00000040;transform:translateY(-2px)}.hero .hero-cta a:last-child{background:transparent;color:#fff;border:1px solid rgba(255,255,255,.4)}.hero .hero-cta a:last-child:hover{border-color:#ffffffbf;background:#ffffff14;transform:translateY(-2px)}.hero .hero-label{display:inline-block;margin-bottom:.9rem;padding:.2rem .8rem;border-radius:999px;font-size:.72rem;font-weight:600;letter-spacing:.07em;text-transform:uppercase;color:#ffffffb3;border:1px solid rgba(255,255,255,.22)}.grid-breakout{width:calc(100vw - 2rem);margin-left:calc(50% - 50vw + 1rem);margin-right:calc(50% - 50vw + 1rem)}.dm-breadcrumbs{position:fixed;z-index:200;display:inline-flex;align-items:center;gap:.2rem;padding:.3rem .8rem;border-radius:999px;backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);background:#00000047;border:1px solid rgba(255,255,255,.11);box-shadow:0 2px 10px #00000038;font-size:.72rem;font-weight:500;letter-spacing:.01em;line-height:1.4;max-width:calc(100vw - 2rem);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dm-breadcrumbs .dm-breadcrumbs-item{color:#ffffffa6}.dm-breadcrumbs .dm-breadcrumbs-link{display:inline-flex;align-items:center;gap:.25rem;color:#ffffff8c;text-decoration:none;transition:color .15s}.dm-breadcrumbs .dm-breadcrumbs-home-icon{flex-shrink:0;vertical-align:middle}.dm-breadcrumbs .dm-breadcrumbs-link:hover{color:#fffffff2}.dm-breadcrumbs .dm-breadcrumbs-current{color:#ffffffeb;font-weight:600}.dm-breadcrumbs .dm-breadcrumbs-separator{color:#ffffff47;font-size:.8em;line-height:1;margin:0 .05rem}[data-mode=light] .dm-breadcrumbs{background:#ffffff8c;border-color:#00000012;box-shadow:0 2px 10px #00000014}[data-mode=light] .dm-breadcrumbs .dm-breadcrumbs-item,[data-mode=light] .dm-breadcrumbs .dm-breadcrumbs-link{color:#0000008c}[data-mode=light] .dm-breadcrumbs .dm-breadcrumbs-link:hover{color:#000000e6}[data-mode=light] .dm-breadcrumbs .dm-breadcrumbs-current{color:#000000d9}[data-mode=light] .dm-breadcrumbs .dm-breadcrumbs-separator{color:#00000040}.dm-collection-display{margin:1.5rem 0}.dm-collection-list{display:flex;flex-direction:column;gap:0}.dm-collection-list-item{padding:1rem 0;border-bottom:1px solid var(--border-color, rgba(255, 255, 255, .08))}.dm-collection-list-item:last-child{border-bottom:none}.dm-collection-list-item strong{display:block;font-size:1rem;margin-bottom:.25rem}.dm-collection-list-item p{margin:0;color:var(--text-muted, #888);font-size:.9rem}.dm-collection-empty p{color:var(--text-muted, #888);font-style:italic}.hero-gradient-purple{background:linear-gradient(135deg,#ede9fe,#ddd6fe);color:#1e1b4b}.hero-gradient-blue{background:linear-gradient(135deg,#dbeafe,#bfdbfe);color:#1e3a5f}.hero-gradient-green{background:linear-gradient(135deg,#d1fae5,#a7f3d0);color:#064e3b}.hero-gradient-sunset{background:linear-gradient(135deg,#fef3c7,#fde68a);color:#78350f}.hero-gradient-ocean{background:linear-gradient(135deg,#e0f2fe,#bae6fd);color:#0c4a6e}.hero-gradient-rose{background:linear-gradient(135deg,#fce7f3,#fbcfe8);color:#831843}.hero-gradient-forest{background:linear-gradient(135deg,#dcfce7,#bbf7d0);color:#14532d}.hero-gradient-night{background:linear-gradient(135deg,#334155,#1e293b);color:#e2e8f0}.hero-gradient-ocean-light{background:linear-gradient(135deg,#e0f2fe,#caf0f8);color:#1e293b}.hero-gradient-ocean-dark{background:linear-gradient(135deg,#0c4a6e,#164e63);color:#e2e8f0}.hero-gradient-forest-light{background:linear-gradient(135deg,#d1fae5,#c6f6dc);color:#1e293b}.hero-gradient-forest-dark{background:linear-gradient(135deg,#1a4731,#166534);color:#e2e8f0}.hero-gradient-sunset-light{background:linear-gradient(135deg,#fde8d8,#fddcc9);color:#1e293b}.hero-gradient-sunset-dark{background:linear-gradient(135deg,#6b3727,#7c4036);color:#f5ede8}.hero-gradient-royal-light{background:linear-gradient(135deg,#e8f0fd,#dce8fc);color:#1e293b}.hero-gradient-royal-dark{background:linear-gradient(135deg,#1e3465,#263d7a);color:#e2e8f0}.hero-gradient-lemon-light{background:linear-gradient(135deg,#fefce8,#fef9c3);color:#1e293b}.hero-gradient-lemon-dark{background:linear-gradient(135deg,#5c4d1a,#6b5920);color:#fefce8}.hero-gradient-silver-light{background:linear-gradient(135deg,#f1f5f9,#e2e8f0);color:#1e293b}.hero-gradient-silver-dark{background:linear-gradient(135deg,#2d3748,#374151);color:#e2e8f0}.hero-gradient-charcoal-light{background:linear-gradient(135deg,#eceff1,#e1e7eb);color:#1e293b}.hero-gradient-charcoal-dark{background:linear-gradient(135deg,#2c3843,#374451);color:#e2e8f0}.hero-gradient-christmas-light{background:linear-gradient(135deg,#fde8ea,#fdd5d8);color:#1e293b}.hero-gradient-christmas-dark{background:linear-gradient(135deg,#5c0f1d,#7a1525);color:#fde8ea}.hero-gradient-unicorn-light{background:linear-gradient(135deg,#f5e8fd,#edd6fb);color:#1e293b}.hero-gradient-unicorn-dark{background:linear-gradient(135deg,#3d1a5a,#4a2068);color:#f5e8fd}.hero-gradient-dreamy-light{background:linear-gradient(135deg,#f5ede8,#eeddd4);color:#1e293b}.hero-gradient-dreamy-dark{background:linear-gradient(135deg,#3d2820,#503328);color:#f5ede8}.hero-gradient-grayve-light{background:linear-gradient(135deg,#e0f7f9,#cbf2f5);color:#1e293b}.hero-gradient-grayve-dark{background:linear-gradient(135deg,#00363d,#00444d);color:#e0f7f9}.hero-gradient-mint-light{background:linear-gradient(135deg,#d8f5ea,#c5efdd);color:#1e293b}.hero-gradient-mint-dark{background:linear-gradient(135deg,#134d33,#195f3f);color:#d8f5ea}.hero-gradient-wedding-light{background:linear-gradient(135deg,#faf3e0,#f5e9c7);color:#1e293b}.hero-gradient-wedding-dark{background:linear-gradient(135deg,#5c4418,#6f5320);color:#faf3e0}.tabs-centered{text-align:center}.tabs-centered .tab-list{display:inline-flex}.tabs-centered .tab-content{text-align:left}
1
+ body,button,input,select,textarea{font-family:Roboto,sans-serif}.site-main{min-height:calc(100vh - 60px);padding-top:2rem;padding-bottom:4rem}.site-main.with-sidebar{display:grid;grid-template-columns:260px 1fr;gap:0}.site-sidebar{min-height:100%;border-right:1px solid var(--border-color, rgba(255,255,255,.08))}.site-content{overflow:hidden}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}.container{max-width:860px;margin:0 auto;padding:0 1.5rem}.page-title{font-size:clamp(1.5rem,4vw,2rem);font-weight:700;margin-bottom:1.5rem;line-height:1.2}.page-body{line-height:1.7;font-size:1rem}.page-body h1,.page-body h2,.page-body h3,.page-body h4{margin-top:2rem;margin-bottom:.75rem;font-weight:600}.page-body h2{font-size:clamp(1.2rem,3vw,1.5rem)}.page-body h3{font-size:clamp(1.1rem,2.5vw,1.25rem)}.page-body p{margin-bottom:1rem}.page-body ul,.page-body ol{margin-bottom:1rem;padding-left:1.5rem}.page-body a{color:var(--primary, #5b8cff)}.page-body a:hover{text-decoration:underline}.page-body code{font-family:Fira Code,Courier New,monospace;font-size:.9em;background:#ffffff0f;padding:.15em .35em;border-radius:3px}.page-body pre{background:#0000004d;border:1px solid rgba(255,255,255,.08);border-radius:6px;padding:1rem;overflow-x:auto;margin-bottom:1rem}.page-body pre code{background:none;padding:0}.page-body img{max-width:100%;border-radius:6px}.page-body blockquote{border-left:3px solid var(--primary, #5b8cff);margin:1.5rem 0;padding:.75rem 1rem;background:#5b8cff0f;border-radius:0 6px 6px 0}h3.accordion-header{margin:0}.accordion-button{all:unset;display:flex;align-items:center;justify-content:space-between;width:100%;cursor:pointer;font:inherit}.page-body .card-header h2{margin:0;font-size:1rem;font-weight:600;line-height:1.4}.card[data-collapsible] .card-header{cursor:pointer;user-select:none;display:flex;align-items:center;justify-content:space-between}.card[data-collapsible] .card-header:after{content:"\25be";font-size:1.1em;line-height:1;display:inline-block;transition:transform .25s ease;flex-shrink:0}.card[data-collapsible].is-collapsed .card-header:after{transform:rotate(-90deg)}.card[data-collapsible] .card-body{overflow:hidden;max-height:4000px;opacity:1;transition:max-height .3s ease,opacity .25s ease}.card[data-collapsible].is-collapsed .card-body{max-height:0;opacity:0}.navbar-link span[data-icon],.navbar-link svg,.navbar-dropdown-toggle span[data-icon],.navbar-dropdown-toggle svg,.navbar-dropdown-item span[data-icon],.navbar-dropdown-item svg{width:13px!important;height:13px!important;margin-right:10px!important}.navbar-dropdown-toggle{font-size:var(--dm-font-size-base)}@media(min-width:993px){.navbar-dropdown-toggle{font-size:var(--dm-font-size-sm)}}@media(min-width:1201px){.navbar-dropdown-toggle{font-size:var(--dm-font-size-xs)}}.dm-reduced-motion *,.dm-reduced-motion *:before,.dm-reduced-motion *:after{animation-duration:.001ms!important;animation-iteration-count:1!important;transition-duration:.001ms!important;scroll-behavior:auto!important}.page-footer{border-top:1px solid var(--border-color, rgba(255,255,255,.08));padding:1.5rem 0}.footer-inner{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:1rem}.footer-inner p{margin:0;color:var(--text-muted, #888);font-size:.875rem}.footer-links{display:flex;gap:1.25rem}.footer-links a{color:var(--text-muted, #888);font-size:.875rem;text-decoration:none}.footer-links a:hover{color:var(--text, #eee)}.footer-social{display:flex;gap:.5rem;align-items:center}.footer-social-link{display:inline-flex;align-items:center;justify-content:center;width:1.75rem;height:1.75rem;color:var(--text-muted, #888);transition:color .15s}.footer-social-link:hover{color:var(--text, #eee)}.footer-social-link svg{width:1rem;height:1rem}.footer-motion-switch{font-size:.8rem;color:var(--text-muted, #888);white-space:nowrap}.footer-motion-switch .form-switch-label{color:var(--text-muted, #888)}.footer-motion-switch .form-switch-input{width:2rem;height:1.125rem}.footer-motion-switch .form-switch-input:after{width:.875rem;height:.875rem}.footer-motion-switch .form-switch-input:checked:after{transform:translate(.875rem)}.dm-slideover-header{display:flex;align-items:center;justify-content:space-between;padding:.875rem 1.25rem;border-bottom:1px solid var(--border-color, rgba(255, 255, 255, .08));flex-shrink:0}.dm-slideover-title{margin:0;font-size:1rem;font-weight:600;line-height:1.4}.dm-slideover-body{padding:1.25rem;overflow-y:auto;flex:1}@media(max-width:768px){.site-main.with-sidebar{grid-template-columns:1fr}.site-sidebar{display:none}}.dm-spacer{display:block;width:100%}.hero-breakout{width:calc(100vw - 2rem);margin-left:calc(50% - 50vw + 1rem);margin-right:calc(50% - 50vw + 1rem)}.site-main:has(.page-body>.hero-breakout:first-child){padding-top:0}body[data-layout=landing]>.site-main{padding-top:0}body[data-layout=landing]>.site-main .container{max-width:none;padding:0}body[data-layout=landing] .page-body{padding-left:1.5rem;padding-right:1.5rem}body[data-layout=landing] .page-body>p,body[data-layout=landing] .page-body>h1,body[data-layout=landing] .page-body>h2,body[data-layout=landing] .page-body>h3,body[data-layout=landing] .page-body>ul,body[data-layout=landing] .page-body>ol,body[data-layout=landing] .page-body>blockquote{max-width:860px;margin-left:auto;margin-right:auto}body[data-layout=landing] .page-body .hero-breakout{width:calc(100% + 3rem);margin-left:-1.5rem;margin-right:-1.5rem}body[data-layout=landing] .page-body .grid-breakout{width:calc(100% + 3rem);margin-left:-1.5rem;margin-right:-1.5rem;padding-left:1.5rem;padding-right:1.5rem}.page-body .card{transition:transform .2s ease,box-shadow .2s ease}.page-body .card:hover{transform:translateY(-3px);box-shadow:0 8px 24px #00000059}.page-body .card-header-icon-inline{display:flex;align-items:center;gap:.6rem}.page-body .card-header-icon-inline [data-icon]{flex-shrink:0;line-height:0}.page-body .card-header-icon-inline [data-icon] svg,.page-body .card-header-icon-inline>svg{display:block;width:1.25rem;height:1.25rem}.page-body .card-header-icon-stacked{display:flex;flex-direction:column;align-items:center;text-align:center;gap:.35rem;padding-top:.25rem}.page-body .card-header-icon-stacked [data-icon],.page-body .card-header-icon-stacked svg{width:2rem;height:2rem}.hero.hero-dark{background:linear-gradient(135deg,#1f2937,#111827);color:#e2e8f0}.hero .hero-content{position:relative;z-index:2}.hero.hero-left .hero-content{text-align:left;align-items:flex-start;max-width:62%}@media(max-width:768px){.hero.hero-left .hero-content{max-width:100%}}.hero .hero-cta{display:flex;gap:.85rem;flex-wrap:wrap;margin-top:1.75rem}.hero .hero-cta a{display:inline-flex;align-items:center;gap:.4rem;padding:.55rem 1.35rem;border-radius:6px;font-size:.95rem;font-weight:500;text-decoration:none;transition:background .2s ease,border-color .2s ease,transform .15s ease,box-shadow .2s ease}.hero .hero-cta a:first-child{background:#ffffffeb;color:#111;border:1px solid transparent}.hero .hero-cta a:first-child:hover{background:#fff;box-shadow:0 4px 16px #00000040;transform:translateY(-2px)}.hero .hero-cta a:last-child{background:transparent;color:#fff;border:1px solid rgba(255,255,255,.4)}.hero .hero-cta a:last-child:hover{border-color:#ffffffbf;background:#ffffff14;transform:translateY(-2px)}.hero .hero-label{display:inline-block;margin-bottom:.9rem;padding:.2rem .8rem;border-radius:999px;font-size:.72rem;font-weight:600;letter-spacing:.07em;text-transform:uppercase;color:#ffffffb3;border:1px solid rgba(255,255,255,.22)}.grid-breakout{width:calc(100vw - 2rem);margin-left:calc(50% - 50vw + 1rem);margin-right:calc(50% - 50vw + 1rem)}.dm-breadcrumbs{position:fixed;z-index:200;display:inline-flex;align-items:center;gap:.2rem;padding:.3rem .8rem;border-radius:999px;backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);background:#00000047;border:1px solid rgba(255,255,255,.11);box-shadow:0 2px 10px #00000038;font-size:.72rem;font-weight:500;letter-spacing:.01em;line-height:1.4;max-width:calc(100vw - 2rem);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dm-breadcrumbs .dm-breadcrumbs-item{color:#ffffffa6}.dm-breadcrumbs .dm-breadcrumbs-link{display:inline-flex;align-items:center;gap:.25rem;color:#ffffff8c;text-decoration:none;transition:color .15s}.dm-breadcrumbs .dm-breadcrumbs-home-icon{flex-shrink:0;vertical-align:middle}.dm-breadcrumbs .dm-breadcrumbs-link:hover{color:#fffffff2}.dm-breadcrumbs .dm-breadcrumbs-current{color:#ffffffeb;font-weight:600}.dm-breadcrumbs .dm-breadcrumbs-separator{color:#ffffff47;font-size:.8em;line-height:1;margin:0 .05rem}[data-mode=light] .dm-breadcrumbs{background:#ffffff8c;border-color:#00000012;box-shadow:0 2px 10px #00000014}[data-mode=light] .dm-breadcrumbs .dm-breadcrumbs-item,[data-mode=light] .dm-breadcrumbs .dm-breadcrumbs-link{color:#0000008c}[data-mode=light] .dm-breadcrumbs .dm-breadcrumbs-link:hover{color:#000000e6}[data-mode=light] .dm-breadcrumbs .dm-breadcrumbs-current{color:#000000d9}[data-mode=light] .dm-breadcrumbs .dm-breadcrumbs-separator{color:#00000040}.dm-collection-display{margin:1.5rem 0}.dm-collection-list{display:flex;flex-direction:column;gap:0}.dm-collection-list-item{padding:1rem 0;border-bottom:1px solid var(--border-color, rgba(255, 255, 255, .08))}.dm-collection-list-item:last-child{border-bottom:none}.dm-collection-list-item strong{display:block;font-size:1rem;margin-bottom:.25rem}.dm-collection-list-item p{margin:0;color:var(--text-muted, #888);font-size:.9rem}.dm-collection-empty p{color:var(--text-muted, #888);font-style:italic}.hero-gradient-purple{background:linear-gradient(135deg,#ede9fe,#ddd6fe);color:#1e1b4b}.hero-gradient-blue{background:linear-gradient(135deg,#dbeafe,#bfdbfe);color:#1e3a5f}.hero-gradient-green{background:linear-gradient(135deg,#d1fae5,#a7f3d0);color:#064e3b}.hero-gradient-sunset{background:linear-gradient(135deg,#fef3c7,#fde68a);color:#78350f}.hero-gradient-ocean{background:linear-gradient(135deg,#e0f2fe,#bae6fd);color:#0c4a6e}.hero-gradient-rose{background:linear-gradient(135deg,#fce7f3,#fbcfe8);color:#831843}.hero-gradient-forest{background:linear-gradient(135deg,#dcfce7,#bbf7d0);color:#14532d}.hero-gradient-night{background:linear-gradient(135deg,#334155,#1e293b);color:#e2e8f0}.hero-gradient-ocean-light{background:linear-gradient(135deg,#e0f2fe,#caf0f8);color:#1e293b}.hero-gradient-ocean-dark{background:linear-gradient(135deg,#0c4a6e,#164e63);color:#e2e8f0}.hero-gradient-forest-light{background:linear-gradient(135deg,#d1fae5,#c6f6dc);color:#1e293b}.hero-gradient-forest-dark{background:linear-gradient(135deg,#1a4731,#166534);color:#e2e8f0}.hero-gradient-sunset-light{background:linear-gradient(135deg,#fde8d8,#fddcc9);color:#1e293b}.hero-gradient-sunset-dark{background:linear-gradient(135deg,#6b3727,#7c4036);color:#f5ede8}.hero-gradient-royal-light{background:linear-gradient(135deg,#e8f0fd,#dce8fc);color:#1e293b}.hero-gradient-royal-dark{background:linear-gradient(135deg,#1e3465,#263d7a);color:#e2e8f0}.hero-gradient-lemon-light{background:linear-gradient(135deg,#fefce8,#fef9c3);color:#1e293b}.hero-gradient-lemon-dark{background:linear-gradient(135deg,#5c4d1a,#6b5920);color:#fefce8}.hero-gradient-silver-light{background:linear-gradient(135deg,#f1f5f9,#e2e8f0);color:#1e293b}.hero-gradient-silver-dark{background:linear-gradient(135deg,#2d3748,#374151);color:#e2e8f0}.hero-gradient-charcoal-light{background:linear-gradient(135deg,#eceff1,#e1e7eb);color:#1e293b}.hero-gradient-charcoal-dark{background:linear-gradient(135deg,#2c3843,#374451);color:#e2e8f0}.hero-gradient-christmas-light{background:linear-gradient(135deg,#fde8ea,#fdd5d8);color:#1e293b}.hero-gradient-christmas-dark{background:linear-gradient(135deg,#5c0f1d,#7a1525);color:#fde8ea}.hero-gradient-unicorn-light{background:linear-gradient(135deg,#f5e8fd,#edd6fb);color:#1e293b}.hero-gradient-unicorn-dark{background:linear-gradient(135deg,#3d1a5a,#4a2068);color:#f5e8fd}.hero-gradient-dreamy-light{background:linear-gradient(135deg,#f5ede8,#eeddd4);color:#1e293b}.hero-gradient-dreamy-dark{background:linear-gradient(135deg,#3d2820,#503328);color:#f5ede8}.hero-gradient-grayve-light{background:linear-gradient(135deg,#e0f7f9,#cbf2f5);color:#1e293b}.hero-gradient-grayve-dark{background:linear-gradient(135deg,#00363d,#00444d);color:#e0f7f9}.hero-gradient-mint-light{background:linear-gradient(135deg,#d8f5ea,#c5efdd);color:#1e293b}.hero-gradient-mint-dark{background:linear-gradient(135deg,#134d33,#195f3f);color:#d8f5ea}.hero-gradient-wedding-light{background:linear-gradient(135deg,#faf3e0,#f5e9c7);color:#1e293b}.hero-gradient-wedding-dark{background:linear-gradient(135deg,#5c4418,#6f5320);color:#faf3e0}.tabs-centered{text-align:center}.tabs-centered .tab-list{display:inline-flex}.tabs-centered .tab-content{text-align:left}.site-main{overflow-x:hidden}@media(max-width:768px){.hero .hero-cta a,.dm-so-trigger,.dm-cta-trigger{min-height:44px;padding:.6rem 1.25rem}}
@@ -90,7 +90,7 @@ function renderCollectionBlocks(entries, blockTemplate, emptyMsg, ctaOpts, cols)
90
90
 
91
91
  const validCols = ['2', '3', '4', '5', '6'].includes(String(cols)) ? cols : '';
92
92
  const wrapperClass = validCols
93
- ? `dm-collection-display dm-collection-blocks grid grid-cols-${validCols} gap-4`
93
+ ? `dm-collection-display dm-collection-blocks grid ${responsiveGridCols(validCols).join(' ')} gap-4`
94
94
  : 'dm-collection-display dm-collection-blocks';
95
95
 
96
96
  return `<div class="${wrapperClass}">\n${items.join('\n')}\n</div>`;
@@ -145,7 +145,7 @@ function renderCollectionCards(entries, visibleFields, titleField, columns, empt
145
145
  }
146
146
  return `<div class="card">${title ? `<div class="card-header">${title}</div>` : ''}<div class="card-body">${body || '&nbsp;'}</div>${footer}</div>`;
147
147
  }).join('\n');
148
- return `<div class="dm-collection-display grid grid-cols-${cols} gap-4">\n${cards}\n</div>`;
148
+ return `<div class="dm-collection-display grid ${responsiveGridCols(cols).join(' ')} gap-4">\n${cards}\n</div>`;
149
149
  }
150
150
 
151
151
  function renderCollectionList(entries, visibleFields, titleField, emptyMsg, ctaOpts) {
@@ -174,6 +174,37 @@ function renderCollectionList(entries, visibleFields, titleField, emptyMsg, ctaO
174
174
  return `<div class="dm-collection-display dm-collection-list">\n${items}\n</div>`;
175
175
  }
176
176
 
177
+ /**
178
+ * Render a collection as a Domma accordion.
179
+ * @param {object[]} entries
180
+ * @param {string} titleField - entry data field to use as header text
181
+ * @param {string} bodyField - entry data field to use as body content (Markdown)
182
+ * @param {boolean} multiple - allow multiple panels open simultaneously
183
+ * @param {string} emptyMsg
184
+ * @returns {string}
185
+ */
186
+ function renderCollectionAccordion(entries, titleField, bodyField, multiple, emptyMsg) {
187
+ if (!entries.length) {
188
+ return `<div class="dm-collection-display dm-collection-empty"><p>${escapeHtmlText(emptyMsg)}</p></div>`;
189
+ }
190
+
191
+ const multiAttr = multiple ? ' data-multi="true"' : '';
192
+ const items = entries.map(e => {
193
+ const title = escapeHtmlText(String(e.data?.[titleField] ?? e.id ?? '(untitled)'));
194
+ const bodyVal = String(e.data?.[bodyField] ?? '');
195
+ const bodyHtml = marked.parse(bodyVal);
196
+ return (
197
+ `<div class="accordion-item">\n` +
198
+ ` <h3 class="accordion-header"><button class="accordion-button" type="button">${title}` +
199
+ `<span class="accordion-icon" data-icon="chevron-down"></span></button></h3>\n` +
200
+ ` <div class="accordion-body"><div class="accordion-content">${bodyHtml}</div></div>\n` +
201
+ `</div>`
202
+ );
203
+ }).join('\n');
204
+
205
+ return `<div class="dm-collection-display accordion"${multiAttr}>\n${items}\n</div>`;
206
+ }
207
+
177
208
  /**
178
209
  * Process [view slug="..." display="table|cards|list" /] shortcodes.
179
210
  * Executes the View's aggregation pipeline and renders results using the
@@ -244,6 +275,11 @@ async function processViewBlocks(markdown) {
244
275
  replacement = renderCollectionCards(entries, fields, titleField, columns, emptyMsg, ctaOpts);
245
276
  } else if (display === 'list') {
246
277
  replacement = renderCollectionList(entries, fields, titleField, emptyMsg, ctaOpts);
278
+ } else if (display === 'accordion') {
279
+ const accordionTitleField = attrs['title-field'] || 'title';
280
+ const bodyField = attrs['body-field'] || 'description';
281
+ const multiple = attrs.multiple === 'true';
282
+ replacement = renderCollectionAccordion(entries, accordionTitleField, bodyField, multiple, emptyMsg);
247
283
  } else if (display === 'block') {
248
284
  const blockName = attrs.block || viewConfig?.display?.block || '';
249
285
  if (blockName) {
@@ -333,6 +369,11 @@ async function processCollectionBlocks(markdown) {
333
369
  replacement = renderCollectionCards(entries, fields, titleField, columns, emptyMsg, ctaOpts);
334
370
  } else if (display === 'list') {
335
371
  replacement = renderCollectionList(entries, fields, titleField, emptyMsg, ctaOpts);
372
+ } else if (display === 'accordion') {
373
+ const accordionTitleField = attrs['title-field'] || 'title';
374
+ const bodyField = attrs['body-field'] || 'description';
375
+ const multiple = attrs.multiple === 'true';
376
+ replacement = renderCollectionAccordion(entries, accordionTitleField, bodyField, multiple, emptyMsg);
336
377
  } else if (display === 'block') {
337
378
  const blockName = attrs.block || '';
338
379
  if (blockName) {
@@ -454,14 +495,36 @@ function processPluginShortcodes(markdown) {
454
495
  * @param {string} markdown
455
496
  * @returns {string}
456
497
  */
498
+
499
+ /**
500
+ * Return the responsive Domma grid-cols class list for a given column count.
501
+ * Outputs mobile-first stacking: single column on small screens, then
502
+ * intermediate and full column counts at md/lg breakpoints.
503
+ *
504
+ * @param {string|number} cols
505
+ * @returns {string[]}
506
+ */
507
+ function responsiveGridCols(cols) {
508
+ const map = {
509
+ '1': ['grid-cols-1'],
510
+ '2': ['grid-cols-1', 'grid-cols-md-2'],
511
+ '3': ['grid-cols-1', 'grid-cols-md-2', 'grid-cols-lg-3'],
512
+ '4': ['grid-cols-1', 'grid-cols-md-2', 'grid-cols-lg-4'],
513
+ '5': ['grid-cols-1', 'grid-cols-md-2', 'grid-cols-lg-5'],
514
+ '6': ['grid-cols-1', 'grid-cols-md-3', 'grid-cols-lg-6'],
515
+ };
516
+ return map[String(cols)] ?? [`grid-cols-${cols}`];
517
+ }
518
+
457
519
  function processGridBlocks(markdown) {
458
520
  const {scrubbed, restore} = scrubCodeRegions(markdown);
459
521
  // Pass 1: [col span="N"]...[/col] → <div class="col-span-N">...</div>
522
+ // [col]...[/col] → <div class="col">...</div> (enables flex stacking in [row])
460
523
  let result = scrubbed.replace(
461
524
  /\[col([^\]]*)\]([\s\S]*?)\[\/col\]/gi,
462
525
  (_, attrStr, body) => {
463
526
  const attrs = parseShortcodeAttrs(attrStr);
464
- const cls = attrs.span ? ` class="col-span-${attrs.span}"` : '';
527
+ const cls = attrs.span ? ` class="col-span-${attrs.span}"` : ' class="col"';
465
528
  const id = attrs.id ? ` id="${escapeAttr(attrs.id)}"` : '';
466
529
  return `<div${cls}${id}>${marked.parse(processCardBlocks(body.trim()))}</div>`;
467
530
  }
@@ -480,13 +543,20 @@ function processGridBlocks(markdown) {
480
543
  }
481
544
  );
482
545
 
483
- // Pass 3: [grid cols="N" gap="N"]...[/grid] → <div class="grid grid-cols-N ...">...</div>
546
+ // Pass 3: [grid cols="N" gap="N"]...[/grid] → <div class="grid grid-cols-1 grid-cols-md-N ...">
547
+ // responsive="false" disables the mobile-first stacking behaviour
484
548
  result = result.replace(
485
549
  /\[grid([^\]]*)\]([\s\S]*?)\[\/grid\]/gi,
486
550
  (_, attrStr, inner) => {
487
551
  const attrs = parseShortcodeAttrs(attrStr);
488
552
  const classes = ['grid'];
489
- if (attrs.cols) classes.push(`grid-cols-${attrs.cols}`);
553
+ if (attrs.cols) {
554
+ if (attrs.responsive === 'false') {
555
+ classes.push(`grid-cols-${attrs.cols}`);
556
+ } else {
557
+ classes.push(...responsiveGridCols(attrs.cols));
558
+ }
559
+ }
490
560
  if (attrs.gap) classes.push(`gap-${attrs.gap}`);
491
561
  if (attrs.class) classes.push(attrs.class);
492
562
  if (attrs.fullwidth === 'true') classes.push('grid-breakout');
@@ -1277,7 +1347,7 @@ function processHeroBlocks(markdown) {
1277
1347
  const blobs = 'blobs' in attrs;
1278
1348
  const blobsType = attrs['blobs-type'] || 'float-blobs';
1279
1349
 
1280
- const classes = ['hero'];
1350
+ const classes = ['hero', 'hero-responsive'];
1281
1351
  if (size) classes.push(`hero-${size}`);
1282
1352
  if (variant) classes.push(`hero-${variant}`);
1283
1353
  if (align) classes.push(`hero-${align}`);