| 1 | <div class="container admin-site-edit-page">
|
|---|
| 2 | <p><a href="/admin" class="btn">← <%= t('asite.back_admin') %></a></p>
|
|---|
| 3 | <h1><%= isNew ? t('asite.title_new') : t('asite.title_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><%= t('asite.identity') %></legend>
|
|---|
| 12 | <label>
|
|---|
| 13 | <span><%= t('asite.slug') %> <% if (!isNew) { %><em><%= t('asite.slug_fixed') %></em><% } %></span>
|
|---|
| 14 | <span class="slug-url">
|
|---|
| 15 | <span class="slug-url-host" id="slug-host"
|
|---|
| 16 | data-prefix="<%= (typeof tenancy !== 'undefined' && tenancy === 'hub') ? '/user/' : '/' %>">website.com/</span><input
|
|---|
| 17 | type="text" name="slug" id="slug-input" class="slug-url-input" value="<%= site.slug %>" required <% if (!isNew) { %>readonly<% } %>
|
|---|
| 18 | pattern="[a-z0-9_-]{2,40}" placeholder="<%= t('asite.slug_placeholder') %>"
|
|---|
| 19 | autocapitalize="none" autocorrect="off" spellcheck="false">
|
|---|
| 20 | </span>
|
|---|
| 21 | </label>
|
|---|
| 22 | <label>
|
|---|
| 23 | <span><%= t('asite.field_title') %> <small class="form-hint-inline"><%= t('asite.field_title_hint') %></small></span>
|
|---|
| 24 | <input type="text" name="title" value="<%= site.title || '' %>" required maxlength="200">
|
|---|
| 25 | </label>
|
|---|
| 26 | <%# Assign (or reassign) owner — ONLY god AND only in hub mode. In hub this
|
|---|
| 27 | gives each user their own self-managed Klonkt. In solo (one site,
|
|---|
| 28 | one owner) this is pointless/confusing, so it's hidden. %>
|
|---|
| 29 | <% if (user && user.role === 'god' && (typeof tenancy !== 'undefined' && tenancy === 'hub')) { %>
|
|---|
| 30 | <label>
|
|---|
| 31 | <span><%= t('asite.owner') %> <small class="form-hint-inline"><%= t('asite.owner_hint') %></small></span>
|
|---|
| 32 | <% var _godSuffix = t('asite.owner_god_suffix'); %>
|
|---|
| 33 | <select name="owner_id">
|
|---|
| 34 | <% (typeof users !== 'undefined' ? users : []).forEach(function(u){ %>
|
|---|
| 35 | <option value="<%= u.id %>" <%= (site.owner_id === u.id) ? 'selected' : '' %>><%= u.username %><%= u.role === 'god' ? _godSuffix : '' %></option>
|
|---|
| 36 | <% }); %>
|
|---|
| 37 | </select>
|
|---|
| 38 | </label>
|
|---|
| 39 | <% } %>
|
|---|
| 40 | <label>
|
|---|
| 41 | <span><%= t('asite.tagline') %> <small class="form-hint-inline"><%= t('asite.tagline_hint') %></small></span>
|
|---|
| 42 | <input type="text" name="tagline" value="<%= site.tagline || '' %>" maxlength="200">
|
|---|
| 43 | </label>
|
|---|
| 44 | <label>
|
|---|
| 45 | <span><%= t('asite.bio') %> <small class="form-hint-inline"><%= t('asite.bio_hint') %></small></span>
|
|---|
| 46 | <textarea name="description" rows="3" maxlength="500"><%= site.description || '' %></textarea>
|
|---|
| 47 | </label>
|
|---|
| 48 | <label>
|
|---|
| 49 | <span><%= t('asite.profile_photo') %></span>
|
|---|
| 50 | <div class="photo-picker" id="photo-picker">
|
|---|
| 51 | <div class="photo-thumb" id="photo-thumb" <%= site.profile_photo ? '' : 'data-empty' %>>
|
|---|
| 52 | <img id="photo-preview" src="<%= site.profile_photo || '' %>" alt="" <%= site.profile_photo ? '' : 'hidden' %>>
|
|---|
| 53 | <span class="photo-empty" aria-hidden="true">
|
|---|
| 54 | <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>
|
|---|
| 55 | </span>
|
|---|
| 56 | </div>
|
|---|
| 57 | <div class="photo-controls">
|
|---|
| 58 | <input type="text" name="profile_photo" id="photo-url" value="<%= site.profile_photo || '' %>" placeholder="<%= t('asite.photo_url_placeholder') %>">
|
|---|
| 59 | <div class="photo-actions">
|
|---|
| 60 | <button type="button" class="btn" id="photo-upload-trigger"><%= t('asite.photo_upload') %></button>
|
|---|
| 61 | <button type="button" class="btn" id="photo-clear" <%= site.profile_photo ? '' : 'hidden' %>><%= t('asite.photo_clear') %></button>
|
|---|
| 62 | <small id="photo-status" class="form-hint photo-status"></small>
|
|---|
| 63 | </div>
|
|---|
| 64 | <input type="file" id="photo-upload-field" accept="image/jpeg,image/png,image/webp,image/gif" hidden>
|
|---|
| 65 | </div>
|
|---|
| 66 | </div>
|
|---|
| 67 | </label>
|
|---|
| 68 | <label>
|
|---|
| 69 | <span><%= t('asite.language') %></span>
|
|---|
| 70 | <input type="text" name="language" value="<%= site.language || 'nl' %>" maxlength="8">
|
|---|
| 71 | </label>
|
|---|
| 72 | <label class="cb">
|
|---|
| 73 | <input type="checkbox" name="profile_enabled" value="1" <%= site.profile_enabled ? 'checked' : '' %>>
|
|---|
| 74 | <span><%= t('asite.profile_enabled') %></span>
|
|---|
| 75 | </label>
|
|---|
| 76 | </fieldset>
|
|---|
| 77 |
|
|---|
| 78 | <fieldset>
|
|---|
| 79 | <legend><%= t('asite.appearance') %></legend>
|
|---|
| 80 |
|
|---|
| 81 | <%# ── Curated accent picker ───────────────────────────────────────
|
|---|
| 82 | Was a free-form <input type="color">; now a fixed set of 8 hand-
|
|---|
| 83 | picked colors that work in both light and dark themes. The hex is
|
|---|
| 84 | still what's stored in the DB — we just constrain WHICH hex. %>
|
|---|
| 85 | <fieldset class="accent-grid">
|
|---|
| 86 | <legend><%= t('asite.accent_color') %></legend>
|
|---|
| 87 | <% const _curAccent = (site.accent || '#c2410c').toLowerCase(); %>
|
|---|
| 88 | <% accents.forEach(function(a) { %>
|
|---|
| 89 | <label class="accent-swatch" title="<%= a.name %>">
|
|---|
| 90 | <input type="radio" name="accent" value="<%= a.color %>" <%= a.color.toLowerCase() === _curAccent ? 'checked' : '' %>>
|
|---|
| 91 | <span class="accent-dot" style="background: <%= a.color %>;" aria-hidden="true"></span>
|
|---|
| 92 | <span class="accent-swatch-name"><%= a.name %></span>
|
|---|
| 93 | </label>
|
|---|
| 94 | <% }); %>
|
|---|
| 95 | </fieldset>
|
|---|
| 96 |
|
|---|
| 97 | <%# ── Default theme for new visitors ───────────────────────────
|
|---|
| 98 | 'auto' (empty value) lets the browser/OS decide via prefers-color-scheme.
|
|---|
| 99 | 'light' / 'dark' force the site default for visitors who haven't picked
|
|---|
| 100 | their own preference yet. Logged-in users with users.theme set, and
|
|---|
| 101 | visitors with a localStorage override, still win over this default. %>
|
|---|
| 102 | <fieldset class="theme-default">
|
|---|
| 103 | <legend><%= t('asite.theme_default') %></legend>
|
|---|
| 104 | <% const td = site.theme_override || ''; %>
|
|---|
| 105 | <label class="cb">
|
|---|
| 106 | <input type="radio" name="theme_override" value="" <%= td === '' ? 'checked' : '' %>>
|
|---|
| 107 | <span><%= t('asite.theme_auto') %></span>
|
|---|
| 108 | </label>
|
|---|
| 109 | <label class="cb">
|
|---|
| 110 | <input type="radio" name="theme_override" value="light" <%= td === 'light' ? 'checked' : '' %>>
|
|---|
| 111 | <span><%= t('asite.theme_light') %></span>
|
|---|
| 112 | </label>
|
|---|
| 113 | <label class="cb">
|
|---|
| 114 | <input type="radio" name="theme_override" value="dark" <%= td === 'dark' ? 'checked' : '' %>>
|
|---|
| 115 | <span><%= t('asite.theme_dark') %></span>
|
|---|
| 116 | </label>
|
|---|
| 117 | </fieldset>
|
|---|
| 118 |
|
|---|
| 119 | <fieldset class="palette-grid">
|
|---|
| 120 | <legend><%= t('asite.palette') %></legend>
|
|---|
| 121 | <% palettes.forEach(function(p) { %>
|
|---|
| 122 | <label class="palette-swatch">
|
|---|
| 123 | <input type="radio" name="palette" value="<%= p.key %>" <%= (site.palette || 'sage') === p.key ? 'checked' : '' %>>
|
|---|
| 124 | <span class="palette-preview" aria-hidden="true">
|
|---|
| 125 | <span class="pp-half pp-light" style="background: <%= p.light.paper %>; color: <%= p.light.ink %>;">
|
|---|
| 126 | <span class="pp-letter">Aa</span>
|
|---|
| 127 | <span class="pp-accent" style="background: <%= p.light.accent %>;"></span>
|
|---|
| 128 | </span>
|
|---|
| 129 | <span class="pp-half pp-dark" style="background: <%= p.dark.paper %>; color: <%= p.dark.ink %>;">
|
|---|
| 130 | <span class="pp-letter">Aa</span>
|
|---|
| 131 | <span class="pp-accent" style="background: <%= p.dark.accent %>;"></span>
|
|---|
| 132 | </span>
|
|---|
| 133 | </span>
|
|---|
| 134 | <span class="palette-swatch-name"><%= p.name %></span>
|
|---|
| 135 | </label>
|
|---|
| 136 | <% }); %>
|
|---|
| 137 | </fieldset>
|
|---|
| 138 | </fieldset>
|
|---|
| 139 |
|
|---|
| 140 | <fieldset>
|
|---|
| 141 | <legend><%= t('asite.behavior') %></legend>
|
|---|
| 142 | <label class="cb">
|
|---|
| 143 | <input type="checkbox" name="is_public" value="1" <%= site.is_public ? 'checked' : '' %>>
|
|---|
| 144 | <span><%= t('asite.is_public') %></span>
|
|---|
| 145 | </label>
|
|---|
| 146 | <label class="cb">
|
|---|
| 147 | <input type="checkbox" name="robots_index" value="1" <%= site.robots_index ? 'checked' : '' %>>
|
|---|
| 148 | <span><%= t('asite.robots_index') %></span>
|
|---|
| 149 | </label>
|
|---|
| 150 | <label class="cb">
|
|---|
| 151 | <input type="checkbox" name="require_login_to_comment" value="1" <%= site.require_login_to_comment ? 'checked' : '' %>>
|
|---|
| 152 | <span><%= t('asite.require_login_comment') %></span>
|
|---|
| 153 | </label>
|
|---|
| 154 | <label>
|
|---|
| 155 | <span><%= t('asite.comment_moderation') %></span>
|
|---|
| 156 | <select name="comments_moderation_mode">
|
|---|
| 157 | <option value="trust" <%= (site.comments_moderation_mode || 'trust') === 'trust' ? 'selected' : '' %>><%= t('asite.moderation_trust') %></option>
|
|---|
| 158 | <option value="moderate" <%= site.comments_moderation_mode === 'moderate' ? 'selected' : '' %>><%= t('asite.moderation_moderate') %></option>
|
|---|
| 159 | </select>
|
|---|
| 160 | </label>
|
|---|
| 161 | <% if (typeof audioEnabled === 'undefined' || audioEnabled) { %>
|
|---|
| 162 | <label class="cb">
|
|---|
| 163 | <input type="checkbox" name="enable_audio_player" value="1" <%= site.enable_audio_player ? 'checked' : '' %>>
|
|---|
| 164 | <span><%= t('asite.enable_audio') %></span>
|
|---|
| 165 | </label>
|
|---|
| 166 | <% } %>
|
|---|
| 167 | </fieldset>
|
|---|
| 168 |
|
|---|
| 169 | <fieldset class="profile-links-fieldset">
|
|---|
| 170 | <legend><%= t('asite.links') %></legend>
|
|---|
| 171 | <p class="form-hint"><%= t('asite.links_hint') %></p>
|
|---|
| 172 |
|
|---|
| 173 | <div id="profile-links-rows">
|
|---|
| 174 | <% (parsedLinks || []).forEach(function(l) { %>
|
|---|
| 175 | <div class="profile-link-row">
|
|---|
| 176 | <select name="profile_link_platform[]">
|
|---|
| 177 | <% platforms.forEach(function(p) { %>
|
|---|
| 178 | <option value="<%= p.key %>" <%= l.platform === p.key ? 'selected' : '' %>><%= p.label %></option>
|
|---|
| 179 | <% }); %>
|
|---|
| 180 | </select>
|
|---|
| 181 | <input type="text" name="profile_link_url[]" value="<%= l.url %>" placeholder="https://…" required>
|
|---|
| 182 | <button type="button" class="btn btn-danger pl-remove">×</button>
|
|---|
| 183 | </div>
|
|---|
| 184 | <% }); %>
|
|---|
| 185 | </div>
|
|---|
| 186 |
|
|---|
| 187 | <template id="profile-link-template">
|
|---|
| 188 | <div class="profile-link-row">
|
|---|
| 189 | <select name="profile_link_platform[]">
|
|---|
| 190 | <% platforms.forEach(function(p) { %>
|
|---|
| 191 | <option value="<%= p.key %>"><%= p.label %></option>
|
|---|
| 192 | <% }); %>
|
|---|
| 193 | </select>
|
|---|
| 194 | <input type="text" name="profile_link_url[]" placeholder="https://…" required>
|
|---|
| 195 | <button type="button" class="btn btn-danger pl-remove">×</button>
|
|---|
| 196 | </div>
|
|---|
| 197 | </template>
|
|---|
| 198 |
|
|---|
| 199 | <button type="button" id="profile-link-add" class="btn"><%= t('asite.link_add') %></button>
|
|---|
| 200 | </fieldset>
|
|---|
| 201 |
|
|---|
| 202 | <fieldset>
|
|---|
| 203 | <legend><%= t('asite.feed_view') %></legend>
|
|---|
| 204 | <label>
|
|---|
| 205 | <span><%= t('asite.feed_default') %></span>
|
|---|
| 206 | <select name="feed_view_default">
|
|---|
| 207 | <option value="timeline" <%= (site.feed_view_default || 'timeline') === 'timeline' ? 'selected' : '' %>><%= t('asite.feed_timeline') %></option>
|
|---|
| 208 | <option value="grid" <%= site.feed_view_default === 'grid' ? 'selected' : '' %>><%= t('asite.feed_grid') %></option>
|
|---|
| 209 | </select>
|
|---|
| 210 | </label>
|
|---|
| 211 | <label class="cb">
|
|---|
| 212 | <input type="checkbox" name="feed_view_switch" value="1" <%= site.feed_view_switch ? 'checked' : '' %>>
|
|---|
| 213 | <span><%= t('asite.feed_switch') %></span>
|
|---|
| 214 | </label>
|
|---|
| 215 | <label class="cb">
|
|---|
| 216 | <input type="checkbox" name="show_search" value="1" <%= site.show_search ? 'checked' : '' %>>
|
|---|
| 217 | <span><%= t('asite.show_search') %></span>
|
|---|
| 218 | </label>
|
|---|
| 219 | <label class="cb">
|
|---|
| 220 | <input type="checkbox" name="show_archive_link" value="1" <%= site.show_archive_link ? 'checked' : '' %>>
|
|---|
| 221 | <span><%= t('asite.show_archive') %></span>
|
|---|
| 222 | </label>
|
|---|
| 223 | </fieldset>
|
|---|
| 224 |
|
|---|
| 225 | <fieldset class="seo-pointer">
|
|---|
| 226 | <legend><%= t('asite.seo') %></legend>
|
|---|
| 227 | <p><%= t('asite.seo_pointer') %> <a href="/admin/seo"><%= t('asite.seo_link') %></a>.</p>
|
|---|
| 228 | </fieldset>
|
|---|
| 229 |
|
|---|
| 230 | <fieldset>
|
|---|
| 231 | <legend><%= t('asite.custom_legend') %> <em><%= t('asite.optional') %></em></legend>
|
|---|
| 232 | <label>
|
|---|
| 233 | <span><%= t('asite.custom_css') %></span>
|
|---|
| 234 | <textarea name="custom_css" rows="3" placeholder=".foo { color: red; }"><%= site.custom_css || '' %></textarea>
|
|---|
| 235 | </label>
|
|---|
| 236 | <label>
|
|---|
| 237 | <span><%= t('asite.custom_head') %></span>
|
|---|
| 238 | <textarea name="custom_head_html" rows="3" placeholder="<meta name="...">"><%= site.custom_head_html || '' %></textarea>
|
|---|
| 239 | </label>
|
|---|
| 240 | <label>
|
|---|
| 241 | <span><%= t('asite.custom_foot') %></span>
|
|---|
| 242 | <textarea name="custom_foot_html" rows="3"><%= site.custom_foot_html || '' %></textarea>
|
|---|
| 243 | </label>
|
|---|
| 244 | </fieldset>
|
|---|
| 245 |
|
|---|
| 246 | <button type="submit" class="btn btn-primary"><%= isNew ? t('asite.submit_create') : t('asite.submit_save') %></button>
|
|---|
| 247 | </form>
|
|---|
| 248 | </div>
|
|---|
| 249 |
|
|---|
| 250 | <script>
|
|---|
| 251 | (function() {
|
|---|
| 252 | // Slug URL field: show the real host as a dimmed prefix (the slug itself is
|
|---|
| 253 | // coloured via CSS). Hub → host/user/<slug>, otherwise host/<slug>.
|
|---|
| 254 | var sh = document.getElementById('slug-host');
|
|---|
| 255 | if (sh) sh.textContent = location.host + (sh.dataset.prefix || '/');
|
|---|
| 256 | })();
|
|---|
| 257 | (function() {
|
|---|
| 258 | // Profile-links repeater: add row from <template>, remove on click.
|
|---|
| 259 | var rows = document.getElementById('profile-links-rows');
|
|---|
| 260 | var tpl = document.getElementById('profile-link-template');
|
|---|
| 261 | var add = document.getElementById('profile-link-add');
|
|---|
| 262 | if (!rows || !tpl || !add) return;
|
|---|
| 263 |
|
|---|
| 264 | add.addEventListener('click', function() {
|
|---|
| 265 | var clone = tpl.content.cloneNode(true);
|
|---|
| 266 | rows.appendChild(clone);
|
|---|
| 267 | });
|
|---|
| 268 | rows.addEventListener('click', function(e) {
|
|---|
| 269 | if (e.target && e.target.classList.contains('pl-remove')) {
|
|---|
| 270 | var row = e.target.closest('.profile-link-row');
|
|---|
| 271 | if (row) row.remove();
|
|---|
| 272 | }
|
|---|
| 273 | });
|
|---|
| 274 | })();
|
|---|
| 275 |
|
|---|
| 276 | // P63 — Profile photo picker: upload via /admin/sites/upload-photo,
|
|---|
| 277 | // then write the returned URL into the visible input. Live thumb preview.
|
|---|
| 278 | (function() {
|
|---|
| 279 | var picker = document.getElementById('photo-picker');
|
|---|
| 280 | if (!picker) return;
|
|---|
| 281 | var thumb = document.getElementById('photo-thumb');
|
|---|
| 282 | var preview = document.getElementById('photo-preview');
|
|---|
| 283 | var urlEl = document.getElementById('photo-url');
|
|---|
| 284 | var trigger = document.getElementById('photo-upload-trigger');
|
|---|
| 285 | var clear = document.getElementById('photo-clear');
|
|---|
| 286 | var field = document.getElementById('photo-upload-field');
|
|---|
| 287 | var status = document.getElementById('photo-status');
|
|---|
| 288 |
|
|---|
| 289 | function showPreview(url) {
|
|---|
| 290 | if (url) {
|
|---|
| 291 | preview.src = url;
|
|---|
| 292 | preview.hidden = false;
|
|---|
| 293 | thumb.removeAttribute('data-empty');
|
|---|
| 294 | clear.hidden = false;
|
|---|
| 295 | } else {
|
|---|
| 296 | preview.src = '';
|
|---|
| 297 | preview.hidden = true;
|
|---|
| 298 | thumb.setAttribute('data-empty', '');
|
|---|
| 299 | clear.hidden = true;
|
|---|
| 300 | }
|
|---|
| 301 | }
|
|---|
| 302 |
|
|---|
| 303 | if (urlEl) {
|
|---|
| 304 | urlEl.addEventListener('input', function() {
|
|---|
| 305 | showPreview(urlEl.value.trim());
|
|---|
| 306 | });
|
|---|
| 307 | }
|
|---|
| 308 |
|
|---|
| 309 | if (trigger && field) {
|
|---|
| 310 | trigger.addEventListener('click', function() { field.click(); });
|
|---|
| 311 | field.addEventListener('change', async function() {
|
|---|
| 312 | var file = field.files && field.files[0];
|
|---|
| 313 | if (!file) return;
|
|---|
| 314 | status.classList.remove('is-error');
|
|---|
| 315 | status.textContent = 'Uploaden…';
|
|---|
| 316 | try {
|
|---|
| 317 | var fd = new FormData();
|
|---|
| 318 | fd.append('photo', file);
|
|---|
| 319 | var r = await fetch('/admin/sites/upload-photo', {
|
|---|
| 320 | method: 'POST',
|
|---|
| 321 | body: fd,
|
|---|
| 322 | credentials: 'same-origin',
|
|---|
| 323 | });
|
|---|
| 324 | var j = await r.json();
|
|---|
| 325 | if (!r.ok || !j.ok) throw new Error(j.error || ('Upload mislukt (' + r.status + ')'));
|
|---|
| 326 | urlEl.value = j.url;
|
|---|
| 327 | showPreview(j.url);
|
|---|
| 328 | status.textContent = 'Geüpload ✓';
|
|---|
| 329 | setTimeout(function() { status.textContent = ''; }, 2000);
|
|---|
| 330 | } catch (e) {
|
|---|
| 331 | status.classList.add('is-error');
|
|---|
| 332 | status.textContent = 'Mislukt: ' + e.message;
|
|---|
| 333 | } finally {
|
|---|
| 334 | field.value = '';
|
|---|
| 335 | }
|
|---|
| 336 | });
|
|---|
| 337 | }
|
|---|
| 338 |
|
|---|
| 339 | if (clear) {
|
|---|
| 340 | clear.addEventListener('click', function() {
|
|---|
| 341 | urlEl.value = '';
|
|---|
| 342 | showPreview('');
|
|---|
| 343 | status.textContent = '';
|
|---|
| 344 | // Note: doesn't delete the file from disk — saving the form with empty
|
|---|
| 345 | // URL leaves the file orphaned on the server. Acceptable for now.
|
|---|
| 346 | });
|
|---|
| 347 | }
|
|---|
| 348 | })();
|
|---|
| 349 | </script>
|
|---|
| 350 |
|
|---|
| 351 | <style>
|
|---|
| 352 | .admin-site-edit-page { max-width: 760px; margin: 3rem auto; padding: 0 1rem; }
|
|---|
| 353 | .admin-site-edit-page h1 { font-family: var(--font-display, serif); font-size: 1.8rem; margin: 0 0 1.5rem; }
|
|---|
| 354 |
|
|---|
| 355 | .site-form { display: flex; flex-direction: column; gap: 1.5rem; min-width: 0; }
|
|---|
| 356 | /* Mobile: a <fieldset> has a browser-default min-inline-size:min-content and
|
|---|
| 357 | therefore does NOT shrink → the entire form became wider than the screen. min-width:0
|
|---|
| 358 | forces it to shrink; form fields get max-width:100% + border-box so they
|
|---|
| 359 | never overflow their fieldset. */
|
|---|
| 360 | .site-form fieldset { min-width: 0; }
|
|---|
| 361 | .site-form input, .site-form select, .site-form textarea { min-width: 0; max-width: 100%; box-sizing: border-box; }
|
|---|
| 362 | .site-form fieldset {
|
|---|
| 363 | border: 1px solid var(--rule);
|
|---|
| 364 | border-radius: 8px;
|
|---|
| 365 | padding: 1rem 1.25rem;
|
|---|
| 366 | background: var(--paper-2);
|
|---|
| 367 | display: flex;
|
|---|
| 368 | flex-direction: column;
|
|---|
| 369 | gap: 0.75rem;
|
|---|
| 370 | }
|
|---|
| 371 | .site-form fieldset.palette-grid {
|
|---|
| 372 | display: grid;
|
|---|
| 373 | grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
|---|
| 374 | background: var(--paper);
|
|---|
| 375 | margin-top: 0.5rem;
|
|---|
| 376 | }
|
|---|
| 377 | /* Theme default radios — horizontal, sits between accent + palette */
|
|---|
| 378 | .site-form fieldset.theme-default {
|
|---|
| 379 | display: flex; flex-direction: row; flex-wrap: wrap; gap: 1rem;
|
|---|
| 380 | background: var(--paper);
|
|---|
| 381 | margin-top: 0.5rem;
|
|---|
| 382 | }
|
|---|
| 383 | .site-form fieldset.theme-default label.cb { font-size: 0.9rem; color: var(--ink); }
|
|---|
| 384 | .site-form fieldset legend { font-family: var(--font-display, serif); font-size: 1rem; padding: 0 0.4rem; }
|
|---|
| 385 | .site-form label { display: flex; flex-direction: column; gap: 0.25rem; font-size: 0.85rem; color: var(--ink-muted, var(--ink-soft)); }
|
|---|
| 386 | .site-form label.cb { flex-direction: row; align-items: center; gap: 0.5rem; }
|
|---|
| 387 | .site-form input[type="text"], .site-form input[type="color"], .site-form textarea {
|
|---|
| 388 | padding: 0.55rem 0.85rem;
|
|---|
| 389 | border: 1px solid var(--rule);
|
|---|
| 390 | border-radius: 5px;
|
|---|
| 391 | background: var(--paper);
|
|---|
| 392 | color: var(--ink);
|
|---|
| 393 | font-family: inherit;
|
|---|
| 394 | font-size: 0.95rem;
|
|---|
| 395 | }
|
|---|
| 396 | .site-form input[type="color"] { padding: 0.2rem; height: 38px; }
|
|---|
| 397 | .site-form textarea { resize: vertical; min-height: 60px; font-family: var(--font-mono, monospace); font-size: 0.85rem; }
|
|---|
| 398 | .site-form input[readonly] { opacity: 0.6; cursor: not-allowed; background: var(--paper-2); }
|
|---|
| 399 |
|
|---|
| 400 | /* Slug as URL field: dimmed host prefix + the slug itself in the accent colour. */
|
|---|
| 401 | .slug-url {
|
|---|
| 402 | display: flex; align-items: stretch;
|
|---|
| 403 | border: 1px solid var(--rule); border-radius: 5px;
|
|---|
| 404 | background: var(--paper); overflow: hidden;
|
|---|
| 405 | transition: border-color 120ms;
|
|---|
| 406 | }
|
|---|
| 407 | .slug-url:focus-within { border-color: var(--accent); }
|
|---|
| 408 | .slug-url-host {
|
|---|
| 409 | display: inline-flex; align-items: center;
|
|---|
| 410 | padding-left: 0.85rem;
|
|---|
| 411 | color: var(--ink-muted, var(--ink-soft));
|
|---|
| 412 | white-space: nowrap; user-select: none;
|
|---|
| 413 | font-size: 0.95rem;
|
|---|
| 414 | }
|
|---|
| 415 | .site-form .slug-url-input {
|
|---|
| 416 | flex: 1 1 auto; min-width: 0;
|
|---|
| 417 | border: none !important; border-radius: 0 !important;
|
|---|
| 418 | background: transparent !important;
|
|---|
| 419 | color: var(--accent) !important; font-weight: 700;
|
|---|
| 420 | padding: 0.55rem 0.85rem 0.55rem 0.1rem;
|
|---|
| 421 | }
|
|---|
| 422 | .site-form .slug-url-input:focus { outline: none; }
|
|---|
| 423 | .site-form .slug-url-input[readonly] {
|
|---|
| 424 | opacity: 1; cursor: default; background: transparent !important;
|
|---|
| 425 | }
|
|---|
| 426 |
|
|---|
| 427 | /* Selects: same look as the inputs, with a custom chevron instead of the bare
|
|---|
| 428 | native arrow. The open option list is still rendered by the browser. */
|
|---|
| 429 | .site-form select {
|
|---|
| 430 | appearance: none;
|
|---|
| 431 | -webkit-appearance: none;
|
|---|
| 432 | padding: 0.55rem 2.2rem 0.55rem 0.85rem;
|
|---|
| 433 | border: 1px solid var(--rule);
|
|---|
| 434 | border-radius: 5px;
|
|---|
| 435 | background-color: var(--paper);
|
|---|
| 436 | color: var(--ink);
|
|---|
| 437 | font-family: inherit;
|
|---|
| 438 | font-size: 0.95rem;
|
|---|
| 439 | cursor: pointer;
|
|---|
| 440 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%23999' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
|
|---|
| 441 | background-repeat: no-repeat;
|
|---|
| 442 | background-position: right 0.7rem center;
|
|---|
| 443 | transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
|---|
| 444 | }
|
|---|
| 445 | .site-form select:hover { border-color: var(--ink-soft, #888); }
|
|---|
| 446 | .site-form select:focus,
|
|---|
| 447 | .site-form input[type="text"]:focus,
|
|---|
| 448 | .site-form textarea:focus {
|
|---|
| 449 | outline: none;
|
|---|
| 450 | border-color: var(--accent, #c2410c);
|
|---|
| 451 | box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent, #c2410c) 22%, transparent);
|
|---|
| 452 | }
|
|---|
| 453 |
|
|---|
| 454 | /* Reuse the palette-swatch styles from account.ejs */
|
|---|
| 455 | .palette-swatch {
|
|---|
| 456 | display: flex; flex-direction: column; gap: 0.4rem;
|
|---|
| 457 | padding: 0.5rem;
|
|---|
| 458 | border: 1px solid var(--rule); border-radius: 6px; background: var(--paper);
|
|---|
| 459 | cursor: pointer; font-size: 0.85rem; transition: border-color 120ms;
|
|---|
| 460 | }
|
|---|
| 461 | .palette-swatch:hover { border-color: var(--accent); }
|
|---|
| 462 | .palette-swatch input[type="radio"] { position: absolute; opacity: 0; pointer-events: none; }
|
|---|
| 463 | .palette-swatch:has(input[type="radio"]:checked) { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent); }
|
|---|
| 464 | .palette-preview {
|
|---|
| 465 | display: grid; grid-template-columns: 1fr 1fr;
|
|---|
| 466 | width: 100%; height: 44px; border-radius: 4px; overflow: hidden;
|
|---|
| 467 | border: 1px solid var(--rule);
|
|---|
| 468 | }
|
|---|
| 469 | .pp-half {
|
|---|
| 470 | position: relative;
|
|---|
| 471 | display: flex; align-items: center; justify-content: center;
|
|---|
| 472 | font-family: var(--font-display, serif); font-weight: 600; font-size: 1rem;
|
|---|
| 473 | }
|
|---|
| 474 | .pp-letter { line-height: 1; }
|
|---|
| 475 | .pp-accent {
|
|---|
| 476 | position: absolute; bottom: 4px; right: 4px;
|
|---|
| 477 | width: 9px; height: 9px; border-radius: 50%;
|
|---|
| 478 | border: 1px solid rgba(0,0,0,0.12);
|
|---|
| 479 | }
|
|---|
| 480 | .palette-swatch-name { font-weight: 500; color: var(--ink); text-align: center; }
|
|---|
| 481 |
|
|---|
| 482 | /* Accent picker: row of small colored swatches */
|
|---|
| 483 | .site-form fieldset.accent-grid {
|
|---|
| 484 | display: grid;
|
|---|
| 485 | grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
|
|---|
| 486 | gap: 0.5rem;
|
|---|
| 487 | background: var(--paper);
|
|---|
| 488 | margin-top: 0.5rem;
|
|---|
| 489 | }
|
|---|
| 490 | .accent-swatch {
|
|---|
| 491 | display: flex; flex-direction: column; align-items: center; gap: 0.4rem;
|
|---|
| 492 | padding: 0.55rem 0.4rem;
|
|---|
| 493 | border: 1px solid var(--rule); border-radius: 6px; background: var(--paper);
|
|---|
| 494 | cursor: pointer; font-size: 0.78rem;
|
|---|
| 495 | transition: border-color 120ms, box-shadow 120ms;
|
|---|
| 496 | }
|
|---|
| 497 | .accent-swatch:hover { border-color: var(--accent); }
|
|---|
| 498 | .accent-swatch input[type="radio"] { position: absolute; opacity: 0; pointer-events: none; }
|
|---|
| 499 | .accent-swatch:has(input[type="radio"]:checked) {
|
|---|
| 500 | border-color: var(--accent);
|
|---|
| 501 | box-shadow: 0 0 0 2px var(--accent);
|
|---|
| 502 | }
|
|---|
| 503 | .accent-dot {
|
|---|
| 504 | width: 28px; height: 28px; border-radius: 50%;
|
|---|
| 505 | border: 1px solid rgba(0,0,0,0.1);
|
|---|
| 506 | box-shadow: inset 0 -2px 4px rgba(0,0,0,0.12);
|
|---|
| 507 | }
|
|---|
| 508 | .accent-swatch-name { color: var(--ink); text-align: center; font-weight: 500; }
|
|---|
| 509 |
|
|---|
| 510 | .audio-flash { padding: 0.75rem 1rem; border-radius: 6px; margin-bottom: 1rem; font-size: 0.9rem; }
|
|---|
| 511 | .audio-flash--ok { background: rgba(40, 160, 90, 0.15); color: #2a9d5e; border: 1px solid rgba(40, 160, 90, 0.3); }
|
|---|
| 512 | .audio-flash--err { background: rgba(200, 60, 60, 0.15); color: #c33; border: 1px solid rgba(200, 60, 60, 0.3); }
|
|---|
| 513 |
|
|---|
| 514 | .form-hint { font-size: 0.8rem; color: var(--ink-muted, var(--ink-soft)); margin: 0; }
|
|---|
| 515 | .form-hint-inline { font-size: 0.78rem; color: var(--ink-muted, var(--ink-soft)); font-weight: 400; margin-left: .25rem; }
|
|---|
| 516 |
|
|---|
| 517 | /* P63 — Profile photo picker
|
|---|
| 518 | Mobile-first: thumb stacks above input/buttons. ≥520px the thumb floats
|
|---|
| 519 | to the left and the controls fill the remaining width. */
|
|---|
| 520 | .photo-picker {
|
|---|
| 521 | display: flex;
|
|---|
| 522 | flex-direction: column;
|
|---|
| 523 | gap: .65rem;
|
|---|
| 524 | align-items: stretch;
|
|---|
| 525 | }
|
|---|
| 526 | .photo-thumb {
|
|---|
| 527 | position: relative;
|
|---|
| 528 | width: 88px; height: 88px;
|
|---|
| 529 | border-radius: 50%;
|
|---|
| 530 | border: 1px solid var(--rule);
|
|---|
| 531 | background: var(--paper-2);
|
|---|
| 532 | overflow: hidden;
|
|---|
| 533 | flex-shrink: 0;
|
|---|
| 534 | display: flex; align-items: center; justify-content: center;
|
|---|
| 535 | }
|
|---|
| 536 | .photo-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
|---|
| 537 | .photo-thumb .photo-empty {
|
|---|
| 538 | display: flex; color: var(--ink-muted, var(--ink-soft)); opacity: .55;
|
|---|
| 539 | }
|
|---|
| 540 | .photo-thumb .photo-empty svg { width: 36px; height: 36px; }
|
|---|
| 541 | .photo-thumb:not([data-empty]) .photo-empty { display: none; }
|
|---|
| 542 | .photo-thumb[data-empty] img { display: none; }
|
|---|
| 543 |
|
|---|
| 544 | .photo-controls {
|
|---|
| 545 | display: flex;
|
|---|
| 546 | flex-direction: column;
|
|---|
| 547 | gap: .5rem;
|
|---|
| 548 | flex: 1;
|
|---|
| 549 | min-width: 0;
|
|---|
| 550 | }
|
|---|
| 551 | .photo-controls input[type="text"] { width: 100%; }
|
|---|
| 552 | .photo-actions {
|
|---|
| 553 | display: flex; align-items: center; flex-wrap: wrap;
|
|---|
| 554 | gap: .4rem;
|
|---|
| 555 | }
|
|---|
| 556 | .photo-actions .btn { padding: .45rem .75rem; font-size: .85rem; }
|
|---|
| 557 | .photo-status { margin-left: .25rem; }
|
|---|
| 558 | .photo-status.is-error { color: #c2410c; }
|
|---|
| 559 |
|
|---|
| 560 | @media (min-width: 520px) {
|
|---|
| 561 | .photo-picker { flex-direction: row; align-items: flex-start; gap: 1rem; }
|
|---|
| 562 | .photo-controls { gap: .55rem; }
|
|---|
| 563 | }
|
|---|
| 564 | /* Mobile-first: platform selector on its own line (full width), URL input + remove
|
|---|
| 565 | button below. Prevents horizontal overflow on phones (fixed 160px column +
|
|---|
| 566 | non-shrinkable input together ~447px > screen width). */
|
|---|
| 567 | .profile-link-row {
|
|---|
| 568 | display: grid; grid-template-columns: 1fr auto; gap: 0.5rem; margin-bottom: 0.5rem;
|
|---|
| 569 | }
|
|---|
| 570 | .profile-link-row select { padding: 0.45rem 0.6rem; grid-column: 1 / -1; }
|
|---|
| 571 | .profile-link-row input { min-width: 0; }
|
|---|
| 572 | @media (min-width: 520px) {
|
|---|
| 573 | .profile-link-row { grid-template-columns: 160px 1fr auto; }
|
|---|
| 574 | .profile-link-row select { grid-column: auto; }
|
|---|
| 575 | }
|
|---|
| 576 | .profile-links-fieldset details > label { margin-top: 0.4rem; }
|
|---|
| 577 | </style>
|
|---|