| 1 | <div class="container admin-site-edit-page">
|
|---|
| 2 | <p><a href="/admin/sites" class="btn">← Sites</a></p>
|
|---|
| 3 | <h1><%= isNew ? 'New site' : 'Edit: ' + site.title %></h1>
|
|---|
| 4 |
|
|---|
| 5 | <% if (typeof success !== 'undefined' && success) { %><div class="audio-flash audio-flash--ok"><%= success %></div><% } %>
|
|---|
| 6 | <% if (error) { %><div class="audio-flash audio-flash--err"><%= error %></div><% } %>
|
|---|
| 7 |
|
|---|
| 8 | <form method="post" action="<%= isNew ? '/admin/sites/create' : '/admin/sites/' + site.slug + '/save' %>" class="site-form">
|
|---|
| 9 |
|
|---|
| 10 | <fieldset>
|
|---|
| 11 | <legend>Identity</legend>
|
|---|
| 12 | <label>
|
|---|
| 13 | <span>Slug <% if (!isNew) { %><em>(immutable)</em><% } %></span>
|
|---|
| 14 | <input type="text" name="slug" value="<%= site.slug %>" required <% if (!isNew) { %>readonly<% } %>
|
|---|
| 15 | pattern="[a-z0-9_-]{2,40}"
|
|---|
| 16 | placeholder="bedrijf1">
|
|---|
| 17 | </label>
|
|---|
| 18 | <label>
|
|---|
| 19 | <span>Title <small class="form-hint-inline">— shown in the header and as display name</small></span>
|
|---|
| 20 | <input type="text" name="title" value="<%= site.title || '' %>" required maxlength="200">
|
|---|
| 21 | </label>
|
|---|
| 22 | <label>
|
|---|
| 23 | <span>Tagline <small class="form-hint-inline">— short one-liner</small></span>
|
|---|
| 24 | <input type="text" name="tagline" value="<%= site.tagline || '' %>" maxlength="200">
|
|---|
| 25 | </label>
|
|---|
| 26 | <label>
|
|---|
| 27 | <span>Bio / description <small class="form-hint-inline">— shown in the profile header and used for SEO</small></span>
|
|---|
| 28 | <textarea name="description" rows="3" maxlength="500"><%= site.description || '' %></textarea>
|
|---|
| 29 | </label>
|
|---|
| 30 | <label>
|
|---|
| 31 | <span>Profile photo</span>
|
|---|
| 32 | <div class="photo-picker" id="photo-picker">
|
|---|
| 33 | <div class="photo-thumb" id="photo-thumb" <%= site.profile_photo ? '' : 'data-empty' %>>
|
|---|
| 34 | <img id="photo-preview" src="<%= site.profile_photo || '' %>" alt="" <%= site.profile_photo ? '' : 'hidden' %>>
|
|---|
| 35 | <span class="photo-empty" aria-hidden="true">
|
|---|
| 36 | <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
|
|---|
| 37 | </span>
|
|---|
| 38 | </div>
|
|---|
| 39 | <div class="photo-controls">
|
|---|
| 40 | <input type="text" name="profile_photo" id="photo-url" value="<%= site.profile_photo || '' %>" placeholder="/media/avatars/foo.jpg of https://…">
|
|---|
| 41 | <div class="photo-actions">
|
|---|
| 42 | <button type="button" class="btn" id="photo-upload-trigger">📷 Upload</button>
|
|---|
| 43 | <button type="button" class="btn" id="photo-clear" <%= site.profile_photo ? '' : 'hidden' %>>Verwijder</button>
|
|---|
| 44 | <small id="photo-status" class="form-hint photo-status"></small>
|
|---|
| 45 | </div>
|
|---|
| 46 | <input type="file" id="photo-upload-field" accept="image/jpeg,image/png,image/webp,image/gif" hidden>
|
|---|
| 47 | </div>
|
|---|
| 48 | </div>
|
|---|
| 49 | </label>
|
|---|
| 50 | <label>
|
|---|
| 51 | <span>Language (ISO code)</span>
|
|---|
| 52 | <input type="text" name="language" value="<%= site.language || 'nl' %>" maxlength="8">
|
|---|
| 53 | </label>
|
|---|
| 54 | <label class="cb">
|
|---|
| 55 | <input type="checkbox" name="profile_enabled" value="1" <%= site.profile_enabled ? 'checked' : '' %>>
|
|---|
| 56 | <span>Show profile header below the nav</span>
|
|---|
| 57 | </label>
|
|---|
| 58 | </fieldset>
|
|---|
| 59 |
|
|---|
| 60 | <fieldset>
|
|---|
| 61 | <legend>Appearance</legend>
|
|---|
| 62 |
|
|---|
| 63 | <%# ── Curated accent picker ───────────────────────────────────────
|
|---|
| 64 | Was a free-form <input type="color">; now a fixed set of 8 hand-
|
|---|
| 65 | picked colors that work in both light and dark themes. The hex is
|
|---|
| 66 | still what's stored in the DB — we just constrain WHICH hex. %>
|
|---|
| 67 | <fieldset class="accent-grid">
|
|---|
| 68 | <legend>Accent color</legend>
|
|---|
| 69 | <% const _curAccent = (site.accent || '#c2410c').toLowerCase(); %>
|
|---|
| 70 | <% accents.forEach(function(a) { %>
|
|---|
| 71 | <label class="accent-swatch" title="<%= a.name %>">
|
|---|
| 72 | <input type="radio" name="accent" value="<%= a.color %>" <%= a.color.toLowerCase() === _curAccent ? 'checked' : '' %>>
|
|---|
| 73 | <span class="accent-dot" style="background: <%= a.color %>;" aria-hidden="true"></span>
|
|---|
| 74 | <span class="accent-swatch-name"><%= a.name %></span>
|
|---|
| 75 | </label>
|
|---|
| 76 | <% }); %>
|
|---|
| 77 | </fieldset>
|
|---|
| 78 |
|
|---|
| 79 | <%# ── Default theme for new visitors ───────────────────────────
|
|---|
| 80 | 'auto' (empty value) lets the browser/OS decide via prefers-color-scheme.
|
|---|
| 81 | 'light' / 'dark' force the site default for visitors who haven't picked
|
|---|
| 82 | their own preference yet. Logged-in users with users.theme set, and
|
|---|
| 83 | visitors with a localStorage override, still win over this default. %>
|
|---|
| 84 | <fieldset class="theme-default">
|
|---|
| 85 | <legend>Default theme for new visitors</legend>
|
|---|
| 86 | <% const td = site.theme_override || ''; %>
|
|---|
| 87 | <label class="cb">
|
|---|
| 88 | <input type="radio" name="theme_override" value="" <%= td === '' ? 'checked' : '' %>>
|
|---|
| 89 | <span>Auto (volg device-voorkeur)</span>
|
|---|
| 90 | </label>
|
|---|
| 91 | <label class="cb">
|
|---|
| 92 | <input type="radio" name="theme_override" value="light" <%= td === 'light' ? 'checked' : '' %>>
|
|---|
| 93 | <span>Light</span>
|
|---|
| 94 | </label>
|
|---|
| 95 | <label class="cb">
|
|---|
| 96 | <input type="radio" name="theme_override" value="dark" <%= td === 'dark' ? 'checked' : '' %>>
|
|---|
| 97 | <span>Dark</span>
|
|---|
| 98 | </label>
|
|---|
| 99 | </fieldset>
|
|---|
| 100 |
|
|---|
| 101 | <fieldset class="palette-grid">
|
|---|
| 102 | <legend>Palette</legend>
|
|---|
| 103 | <% palettes.forEach(function(p) { %>
|
|---|
| 104 | <label class="palette-swatch">
|
|---|
| 105 | <input type="radio" name="palette" value="<%= p.key %>" <%= (site.palette || 'sage') === p.key ? 'checked' : '' %>>
|
|---|
| 106 | <span class="palette-preview" aria-hidden="true">
|
|---|
| 107 | <span class="pp-half pp-light" style="background: <%= p.light.paper %>; color: <%= p.light.ink %>;">
|
|---|
| 108 | <span class="pp-letter">Aa</span>
|
|---|
| 109 | <span class="pp-accent" style="background: <%= p.light.accent %>;"></span>
|
|---|
| 110 | </span>
|
|---|
| 111 | <span class="pp-half pp-dark" style="background: <%= p.dark.paper %>; color: <%= p.dark.ink %>;">
|
|---|
| 112 | <span class="pp-letter">Aa</span>
|
|---|
| 113 | <span class="pp-accent" style="background: <%= p.dark.accent %>;"></span>
|
|---|
| 114 | </span>
|
|---|
| 115 | </span>
|
|---|
| 116 | <span class="palette-swatch-name"><%= p.name %></span>
|
|---|
| 117 | </label>
|
|---|
| 118 | <% }); %>
|
|---|
| 119 | </fieldset>
|
|---|
| 120 | </fieldset>
|
|---|
| 121 |
|
|---|
| 122 | <fieldset>
|
|---|
| 123 | <legend>Behaviour</legend>
|
|---|
| 124 | <label class="cb">
|
|---|
| 125 | <input type="checkbox" name="is_public" value="1" <%= site.is_public ? 'checked' : '' %>>
|
|---|
| 126 | <span>Public site (uncheck for closed circle)</span>
|
|---|
| 127 | </label>
|
|---|
| 128 | <label class="cb">
|
|---|
| 129 | <input type="checkbox" name="robots_index" value="1" <%= site.robots_index ? 'checked' : '' %>>
|
|---|
| 130 | <span>Search engines may index (sitemap.xml is hidden if off)</span>
|
|---|
| 131 | </label>
|
|---|
| 132 | <label class="cb">
|
|---|
| 133 | <input type="checkbox" name="require_login_to_comment" value="1" <%= site.require_login_to_comment ? 'checked' : '' %>>
|
|---|
| 134 | <span>Require login to comment</span>
|
|---|
| 135 | </label>
|
|---|
| 136 | <label>
|
|---|
| 137 | <span>Comment moderation</span>
|
|---|
| 138 | <select name="comments_moderation_mode">
|
|---|
| 139 | <option value="trust" <%= (site.comments_moderation_mode || 'trust') === 'trust' ? 'selected' : '' %>>Trust mode (auto-approve)</option>
|
|---|
| 140 | <option value="moderate" <%= site.comments_moderation_mode === 'moderate' ? 'selected' : '' %>>Moderate (queue for review)</option>
|
|---|
| 141 | </select>
|
|---|
| 142 | </label>
|
|---|
| 143 | <label class="cb">
|
|---|
| 144 | <input type="checkbox" name="enable_audio_player" value="1" <%= site.enable_audio_player ? 'checked' : '' %>>
|
|---|
| 145 | <span>Enable audio player + embeds</span>
|
|---|
| 146 | </label>
|
|---|
| 147 | <label class="cb">
|
|---|
| 148 | <input type="checkbox" name="enable_prutter" value="1" <%= site.enable_prutter ? 'checked' : '' %>>
|
|---|
| 149 | <span>Enable Prutter (direct messaging)</span>
|
|---|
| 150 | </label>
|
|---|
| 151 | </fieldset>
|
|---|
| 152 |
|
|---|
| 153 | <fieldset class="profile-links-fieldset">
|
|---|
| 154 | <legend>Social / streaming links</legend>
|
|---|
| 155 | <p class="form-hint">Shown as branded icons on the profile header. Add as many as you like.</p>
|
|---|
| 156 |
|
|---|
| 157 | <div id="profile-links-rows">
|
|---|
| 158 | <% (parsedLinks || []).forEach(function(l) { %>
|
|---|
| 159 | <div class="profile-link-row">
|
|---|
| 160 | <select name="profile_link_platform[]">
|
|---|
| 161 | <% platforms.forEach(function(p) { %>
|
|---|
| 162 | <option value="<%= p.key %>" <%= l.platform === p.key ? 'selected' : '' %>><%= p.label %></option>
|
|---|
| 163 | <% }); %>
|
|---|
| 164 | </select>
|
|---|
| 165 | <input type="text" name="profile_link_url[]" value="<%= l.url %>" placeholder="https://…" required>
|
|---|
| 166 | <button type="button" class="btn btn-danger pl-remove">×</button>
|
|---|
| 167 | </div>
|
|---|
| 168 | <% }); %>
|
|---|
| 169 | </div>
|
|---|
| 170 |
|
|---|
| 171 | <template id="profile-link-template">
|
|---|
| 172 | <div class="profile-link-row">
|
|---|
| 173 | <select name="profile_link_platform[]">
|
|---|
| 174 | <% platforms.forEach(function(p) { %>
|
|---|
| 175 | <option value="<%= p.key %>"><%= p.label %></option>
|
|---|
| 176 | <% }); %>
|
|---|
| 177 | </select>
|
|---|
| 178 | <input type="text" name="profile_link_url[]" placeholder="https://…" required>
|
|---|
| 179 | <button type="button" class="btn btn-danger pl-remove">×</button>
|
|---|
| 180 | </div>
|
|---|
| 181 | </template>
|
|---|
| 182 |
|
|---|
| 183 | <button type="button" id="profile-link-add" class="btn">+ Add link</button>
|
|---|
| 184 | </fieldset>
|
|---|
| 185 |
|
|---|
| 186 | <fieldset>
|
|---|
| 187 | <legend>Feed view</legend>
|
|---|
| 188 | <label>
|
|---|
| 189 | <span>Default view for the homepage</span>
|
|---|
| 190 | <select name="feed_view_default">
|
|---|
| 191 | <option value="timeline" <%= (site.feed_view_default || 'timeline') === 'timeline' ? 'selected' : '' %>>Timeline (chronological list)</option>
|
|---|
| 192 | <option value="grid" <%= site.feed_view_default === 'grid' ? 'selected' : '' %>>Grid (cards)</option>
|
|---|
| 193 | </select>
|
|---|
| 194 | </label>
|
|---|
| 195 | <label class="cb">
|
|---|
| 196 | <input type="checkbox" name="feed_view_switch" value="1" <%= site.feed_view_switch ? 'checked' : '' %>>
|
|---|
| 197 | <span>Show timeline ↔ grid switcher above the feed</span>
|
|---|
| 198 | </label>
|
|---|
| 199 | <label class="cb">
|
|---|
| 200 | <input type="checkbox" name="show_search" value="1" <%= site.show_search ? 'checked' : '' %>>
|
|---|
| 201 | <span>Show search button in nav</span>
|
|---|
| 202 | </label>
|
|---|
| 203 | <label class="cb">
|
|---|
| 204 | <input type="checkbox" name="show_archive_link" value="1" <%= site.show_archive_link ? 'checked' : '' %>>
|
|---|
| 205 | <span>Show archive link in nav</span>
|
|---|
| 206 | </label>
|
|---|
| 207 | </fieldset>
|
|---|
| 208 |
|
|---|
| 209 | <fieldset>
|
|---|
| 210 | <legend>SEO & social</legend>
|
|---|
| 211 | <label>
|
|---|
| 212 | <span>Title template (use {title} and {site})</span>
|
|---|
| 213 | <input type="text" name="title_template" value="<%= site.title_template || '{title} — {site}' %>" maxlength="200">
|
|---|
| 214 | </label>
|
|---|
| 215 | <label>
|
|---|
| 216 | <span>Canonical base URL (for production HTTPS)</span>
|
|---|
| 217 | <input type="text" name="canonical" value="<%= site.canonical || '' %>" placeholder="https://your-domain.com">
|
|---|
| 218 | </label>
|
|---|
| 219 | <label>
|
|---|
| 220 | <span>Twitter handle (with @)</span>
|
|---|
| 221 | <input type="text" name="twitter" value="<%= site.twitter || '' %>" placeholder="@yourname" maxlength="64">
|
|---|
| 222 | </label>
|
|---|
| 223 | <details>
|
|---|
| 224 | <summary>Search-engine verification</summary>
|
|---|
| 225 | <label>
|
|---|
| 226 | <span>Google site verification</span>
|
|---|
| 227 | <input type="text" name="google_verification" value="<%= site.google_verification || '' %>" maxlength="200">
|
|---|
| 228 | </label>
|
|---|
| 229 | <label>
|
|---|
| 230 | <span>Bing (msvalidate.01)</span>
|
|---|
| 231 | <input type="text" name="bing_verification" value="<%= site.bing_verification || '' %>" maxlength="200">
|
|---|
| 232 | </label>
|
|---|
| 233 | <label>
|
|---|
| 234 | <span>Pinterest (p:domain_verify)</span>
|
|---|
| 235 | <input type="text" name="pinterest_verification" value="<%= site.pinterest_verification || '' %>" maxlength="200">
|
|---|
| 236 | </label>
|
|---|
| 237 | <label>
|
|---|
| 238 | <span>Yandex</span>
|
|---|
| 239 | <input type="text" name="yandex_verification" value="<%= site.yandex_verification || '' %>" maxlength="200">
|
|---|
| 240 | </label>
|
|---|
| 241 | </details>
|
|---|
| 242 | </fieldset>
|
|---|
| 243 |
|
|---|
| 244 | <fieldset>
|
|---|
| 245 | <legend>Custom CSS & HTML <em>(optional)</em></legend>
|
|---|
| 246 | <label>
|
|---|
| 247 | <span>Custom CSS (injected as <style> in <head>)</span>
|
|---|
| 248 | <textarea name="custom_css" rows="3" placeholder=".foo { color: red; }"><%= site.custom_css || '' %></textarea>
|
|---|
| 249 | </label>
|
|---|
| 250 | <label>
|
|---|
| 251 | <span>Custom <head> HTML (analytics, extra metas)</span>
|
|---|
| 252 | <textarea name="custom_head_html" rows="3" placeholder="<meta name="...">"><%= site.custom_head_html || '' %></textarea>
|
|---|
| 253 | </label>
|
|---|
| 254 | <label>
|
|---|
| 255 | <span>Custom footer HTML</span>
|
|---|
| 256 | <textarea name="custom_foot_html" rows="3"><%= site.custom_foot_html || '' %></textarea>
|
|---|
| 257 | </label>
|
|---|
| 258 | </fieldset>
|
|---|
| 259 |
|
|---|
| 260 | <button type="submit" class="btn btn-primary"><%= isNew ? 'Create site' : 'Save changes' %></button>
|
|---|
| 261 | </form>
|
|---|
| 262 | </div>
|
|---|
| 263 |
|
|---|
| 264 | <script>
|
|---|
| 265 | (function() {
|
|---|
| 266 | // Profile-links repeater: add row from <template>, remove on click.
|
|---|
| 267 | var rows = document.getElementById('profile-links-rows');
|
|---|
| 268 | var tpl = document.getElementById('profile-link-template');
|
|---|
| 269 | var add = document.getElementById('profile-link-add');
|
|---|
| 270 | if (!rows || !tpl || !add) return;
|
|---|
| 271 |
|
|---|
| 272 | add.addEventListener('click', function() {
|
|---|
| 273 | var clone = tpl.content.cloneNode(true);
|
|---|
| 274 | rows.appendChild(clone);
|
|---|
| 275 | });
|
|---|
| 276 | rows.addEventListener('click', function(e) {
|
|---|
| 277 | if (e.target && e.target.classList.contains('pl-remove')) {
|
|---|
| 278 | var row = e.target.closest('.profile-link-row');
|
|---|
| 279 | if (row) row.remove();
|
|---|
| 280 | }
|
|---|
| 281 | });
|
|---|
| 282 | })();
|
|---|
| 283 |
|
|---|
| 284 | // P63 — Profile photo picker: upload via /admin/sites/upload-photo,
|
|---|
| 285 | // then write the returned URL into the visible input. Live thumb preview.
|
|---|
| 286 | (function() {
|
|---|
| 287 | var picker = document.getElementById('photo-picker');
|
|---|
| 288 | if (!picker) return;
|
|---|
| 289 | var thumb = document.getElementById('photo-thumb');
|
|---|
| 290 | var preview = document.getElementById('photo-preview');
|
|---|
| 291 | var urlEl = document.getElementById('photo-url');
|
|---|
| 292 | var trigger = document.getElementById('photo-upload-trigger');
|
|---|
| 293 | var clear = document.getElementById('photo-clear');
|
|---|
| 294 | var field = document.getElementById('photo-upload-field');
|
|---|
| 295 | var status = document.getElementById('photo-status');
|
|---|
| 296 |
|
|---|
| 297 | function showPreview(url) {
|
|---|
| 298 | if (url) {
|
|---|
| 299 | preview.src = url;
|
|---|
| 300 | preview.hidden = false;
|
|---|
| 301 | thumb.removeAttribute('data-empty');
|
|---|
| 302 | clear.hidden = false;
|
|---|
| 303 | } else {
|
|---|
| 304 | preview.src = '';
|
|---|
| 305 | preview.hidden = true;
|
|---|
| 306 | thumb.setAttribute('data-empty', '');
|
|---|
| 307 | clear.hidden = true;
|
|---|
| 308 | }
|
|---|
| 309 | }
|
|---|
| 310 |
|
|---|
| 311 | if (urlEl) {
|
|---|
| 312 | urlEl.addEventListener('input', function() {
|
|---|
| 313 | showPreview(urlEl.value.trim());
|
|---|
| 314 | });
|
|---|
| 315 | }
|
|---|
| 316 |
|
|---|
| 317 | if (trigger && field) {
|
|---|
| 318 | trigger.addEventListener('click', function() { field.click(); });
|
|---|
| 319 | field.addEventListener('change', async function() {
|
|---|
| 320 | var file = field.files && field.files[0];
|
|---|
| 321 | if (!file) return;
|
|---|
| 322 | status.classList.remove('is-error');
|
|---|
| 323 | status.textContent = 'Uploaden…';
|
|---|
| 324 | try {
|
|---|
| 325 | var fd = new FormData();
|
|---|
| 326 | fd.append('photo', file);
|
|---|
| 327 | var r = await fetch('/admin/sites/upload-photo', {
|
|---|
| 328 | method: 'POST',
|
|---|
| 329 | body: fd,
|
|---|
| 330 | credentials: 'same-origin',
|
|---|
| 331 | });
|
|---|
| 332 | var j = await r.json();
|
|---|
| 333 | if (!r.ok || !j.ok) throw new Error(j.error || ('Upload mislukt (' + r.status + ')'));
|
|---|
| 334 | urlEl.value = j.url;
|
|---|
| 335 | showPreview(j.url);
|
|---|
| 336 | status.textContent = 'Geüpload ✓';
|
|---|
| 337 | setTimeout(function() { status.textContent = ''; }, 2000);
|
|---|
| 338 | } catch (e) {
|
|---|
| 339 | status.classList.add('is-error');
|
|---|
| 340 | status.textContent = 'Mislukt: ' + e.message;
|
|---|
| 341 | } finally {
|
|---|
| 342 | field.value = '';
|
|---|
| 343 | }
|
|---|
| 344 | });
|
|---|
| 345 | }
|
|---|
| 346 |
|
|---|
| 347 | if (clear) {
|
|---|
| 348 | clear.addEventListener('click', function() {
|
|---|
| 349 | urlEl.value = '';
|
|---|
| 350 | showPreview('');
|
|---|
| 351 | status.textContent = '';
|
|---|
| 352 | // Note: doesn't delete the file from disk — saving the form with empty
|
|---|
| 353 | // URL leaves the file orphaned on the server. Acceptable for now.
|
|---|
| 354 | });
|
|---|
| 355 | }
|
|---|
| 356 | })();
|
|---|
| 357 | </script>
|
|---|
| 358 |
|
|---|
| 359 | <style>
|
|---|
| 360 | .admin-site-edit-page { max-width: 760px; margin: 3rem auto; padding: 0 1rem; }
|
|---|
| 361 | .admin-site-edit-page h1 { font-family: var(--font-display, serif); font-size: 1.8rem; margin: 0 0 1.5rem; }
|
|---|
| 362 |
|
|---|
| 363 | .site-form { display: flex; flex-direction: column; gap: 1.5rem; }
|
|---|
| 364 | .site-form fieldset {
|
|---|
| 365 | border: 1px solid var(--rule);
|
|---|
| 366 | border-radius: 8px;
|
|---|
| 367 | padding: 1rem 1.25rem;
|
|---|
| 368 | background: var(--paper-2);
|
|---|
| 369 | display: flex;
|
|---|
| 370 | flex-direction: column;
|
|---|
| 371 | gap: 0.75rem;
|
|---|
| 372 | }
|
|---|
| 373 | .site-form fieldset.palette-grid {
|
|---|
| 374 | display: grid;
|
|---|
| 375 | grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
|---|
| 376 | background: var(--paper);
|
|---|
| 377 | margin-top: 0.5rem;
|
|---|
| 378 | }
|
|---|
| 379 | /* Theme default radios — horizontal, sits between accent + palette */
|
|---|
| 380 | .site-form fieldset.theme-default {
|
|---|
| 381 | display: flex; flex-direction: row; flex-wrap: wrap; gap: 1rem;
|
|---|
| 382 | background: var(--paper);
|
|---|
| 383 | margin-top: 0.5rem;
|
|---|
| 384 | }
|
|---|
| 385 | .site-form fieldset.theme-default label.cb { font-size: 0.9rem; color: var(--ink); }
|
|---|
| 386 | .site-form fieldset legend { font-family: var(--font-display, serif); font-size: 1rem; padding: 0 0.4rem; }
|
|---|
| 387 | .site-form label { display: flex; flex-direction: column; gap: 0.25rem; font-size: 0.85rem; color: var(--ink-muted, var(--ink-soft)); }
|
|---|
| 388 | .site-form label.cb { flex-direction: row; align-items: center; gap: 0.5rem; }
|
|---|
| 389 | .site-form input[type="text"], .site-form input[type="color"], .site-form textarea {
|
|---|
| 390 | padding: 0.55rem 0.85rem;
|
|---|
| 391 | border: 1px solid var(--rule);
|
|---|
| 392 | border-radius: 5px;
|
|---|
| 393 | background: var(--paper);
|
|---|
| 394 | color: var(--ink);
|
|---|
| 395 | font-family: inherit;
|
|---|
| 396 | font-size: 0.95rem;
|
|---|
| 397 | }
|
|---|
| 398 | .site-form input[type="color"] { padding: 0.2rem; height: 38px; }
|
|---|
| 399 | .site-form textarea { resize: vertical; min-height: 60px; font-family: var(--font-mono, monospace); font-size: 0.85rem; }
|
|---|
| 400 | .site-form input[readonly] { opacity: 0.6; cursor: not-allowed; background: var(--paper-2); }
|
|---|
| 401 |
|
|---|
| 402 | /* Reuse the palette-swatch styles from account.ejs */
|
|---|
| 403 | .palette-swatch {
|
|---|
| 404 | display: flex; flex-direction: column; gap: 0.4rem;
|
|---|
| 405 | padding: 0.5rem;
|
|---|
| 406 | border: 1px solid var(--rule); border-radius: 6px; background: var(--paper);
|
|---|
| 407 | cursor: pointer; font-size: 0.85rem; transition: border-color 120ms;
|
|---|
| 408 | }
|
|---|
| 409 | .palette-swatch:hover { border-color: var(--accent); }
|
|---|
| 410 | .palette-swatch input[type="radio"] { position: absolute; opacity: 0; pointer-events: none; }
|
|---|
| 411 | .palette-swatch:has(input[type="radio"]:checked) { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent); }
|
|---|
| 412 | .palette-preview {
|
|---|
| 413 | display: grid; grid-template-columns: 1fr 1fr;
|
|---|
| 414 | width: 100%; height: 44px; border-radius: 4px; overflow: hidden;
|
|---|
| 415 | border: 1px solid var(--rule);
|
|---|
| 416 | }
|
|---|
| 417 | .pp-half {
|
|---|
| 418 | position: relative;
|
|---|
| 419 | display: flex; align-items: center; justify-content: center;
|
|---|
| 420 | font-family: var(--font-display, serif); font-weight: 600; font-size: 1rem;
|
|---|
| 421 | }
|
|---|
| 422 | .pp-letter { line-height: 1; }
|
|---|
| 423 | .pp-accent {
|
|---|
| 424 | position: absolute; bottom: 4px; right: 4px;
|
|---|
| 425 | width: 9px; height: 9px; border-radius: 50%;
|
|---|
| 426 | border: 1px solid rgba(0,0,0,0.12);
|
|---|
| 427 | }
|
|---|
| 428 | .palette-swatch-name { font-weight: 500; color: var(--ink); text-align: center; }
|
|---|
| 429 |
|
|---|
| 430 | /* Accent picker: row of small colored swatches */
|
|---|
| 431 | .site-form fieldset.accent-grid {
|
|---|
| 432 | display: grid;
|
|---|
| 433 | grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
|
|---|
| 434 | gap: 0.5rem;
|
|---|
| 435 | background: var(--paper);
|
|---|
| 436 | margin-top: 0.5rem;
|
|---|
| 437 | }
|
|---|
| 438 | .accent-swatch {
|
|---|
| 439 | display: flex; flex-direction: column; align-items: center; gap: 0.4rem;
|
|---|
| 440 | padding: 0.55rem 0.4rem;
|
|---|
| 441 | border: 1px solid var(--rule); border-radius: 6px; background: var(--paper);
|
|---|
| 442 | cursor: pointer; font-size: 0.78rem;
|
|---|
| 443 | transition: border-color 120ms, box-shadow 120ms;
|
|---|
| 444 | }
|
|---|
| 445 | .accent-swatch:hover { border-color: var(--accent); }
|
|---|
| 446 | .accent-swatch input[type="radio"] { position: absolute; opacity: 0; pointer-events: none; }
|
|---|
| 447 | .accent-swatch:has(input[type="radio"]:checked) {
|
|---|
| 448 | border-color: var(--accent);
|
|---|
| 449 | box-shadow: 0 0 0 2px var(--accent);
|
|---|
| 450 | }
|
|---|
| 451 | .accent-dot {
|
|---|
| 452 | width: 28px; height: 28px; border-radius: 50%;
|
|---|
| 453 | border: 1px solid rgba(0,0,0,0.1);
|
|---|
| 454 | box-shadow: inset 0 -2px 4px rgba(0,0,0,0.12);
|
|---|
| 455 | }
|
|---|
| 456 | .accent-swatch-name { color: var(--ink); text-align: center; font-weight: 500; }
|
|---|
| 457 |
|
|---|
| 458 | .audio-flash { padding: 0.75rem 1rem; border-radius: 6px; margin-bottom: 1rem; font-size: 0.9rem; }
|
|---|
| 459 | .audio-flash--ok { background: rgba(40, 160, 90, 0.15); color: #2a9d5e; border: 1px solid rgba(40, 160, 90, 0.3); }
|
|---|
| 460 | .audio-flash--err { background: rgba(200, 60, 60, 0.15); color: #c33; border: 1px solid rgba(200, 60, 60, 0.3); }
|
|---|
| 461 |
|
|---|
| 462 | .form-hint { font-size: 0.8rem; color: var(--ink-muted, var(--ink-soft)); margin: 0; }
|
|---|
| 463 | .form-hint-inline { font-size: 0.78rem; color: var(--ink-muted, var(--ink-soft)); font-weight: 400; margin-left: .25rem; }
|
|---|
| 464 |
|
|---|
| 465 | /* P63 — Profile photo picker
|
|---|
| 466 | Mobile-first: thumb stacks above input/buttons. ≥520px the thumb floats
|
|---|
| 467 | to the left and the controls fill the remaining width. */
|
|---|
| 468 | .photo-picker {
|
|---|
| 469 | display: flex;
|
|---|
| 470 | flex-direction: column;
|
|---|
| 471 | gap: .65rem;
|
|---|
| 472 | align-items: stretch;
|
|---|
| 473 | }
|
|---|
| 474 | .photo-thumb {
|
|---|
| 475 | position: relative;
|
|---|
| 476 | width: 88px; height: 88px;
|
|---|
| 477 | border-radius: 50%;
|
|---|
| 478 | border: 1px solid var(--rule);
|
|---|
| 479 | background: var(--paper-2);
|
|---|
| 480 | overflow: hidden;
|
|---|
| 481 | flex-shrink: 0;
|
|---|
| 482 | display: flex; align-items: center; justify-content: center;
|
|---|
| 483 | }
|
|---|
| 484 | .photo-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
|---|
| 485 | .photo-thumb .photo-empty {
|
|---|
| 486 | display: flex; color: var(--ink-muted, var(--ink-soft)); opacity: .55;
|
|---|
| 487 | }
|
|---|
| 488 | .photo-thumb .photo-empty svg { width: 36px; height: 36px; }
|
|---|
| 489 | .photo-thumb:not([data-empty]) .photo-empty { display: none; }
|
|---|
| 490 | .photo-thumb[data-empty] img { display: none; }
|
|---|
| 491 |
|
|---|
| 492 | .photo-controls {
|
|---|
| 493 | display: flex;
|
|---|
| 494 | flex-direction: column;
|
|---|
| 495 | gap: .5rem;
|
|---|
| 496 | flex: 1;
|
|---|
| 497 | min-width: 0;
|
|---|
| 498 | }
|
|---|
| 499 | .photo-controls input[type="text"] { width: 100%; }
|
|---|
| 500 | .photo-actions {
|
|---|
| 501 | display: flex; align-items: center; flex-wrap: wrap;
|
|---|
| 502 | gap: .4rem;
|
|---|
| 503 | }
|
|---|
| 504 | .photo-actions .btn { padding: .45rem .75rem; font-size: .85rem; }
|
|---|
| 505 | .photo-status { margin-left: .25rem; }
|
|---|
| 506 | .photo-status.is-error { color: #c2410c; }
|
|---|
| 507 |
|
|---|
| 508 | @media (min-width: 520px) {
|
|---|
| 509 | .photo-picker { flex-direction: row; align-items: flex-start; gap: 1rem; }
|
|---|
| 510 | .photo-controls { gap: .55rem; }
|
|---|
| 511 | }
|
|---|
| 512 | .profile-link-row {
|
|---|
| 513 | display: grid; grid-template-columns: 160px 1fr auto; gap: 0.5rem; margin-bottom: 0.5rem;
|
|---|
| 514 | }
|
|---|
| 515 | .profile-link-row select { padding: 0.45rem 0.6rem; }
|
|---|
| 516 | .profile-links-fieldset details > label { margin-top: 0.4rem; }
|
|---|
| 517 | </style>
|
|---|