source: Klonkt/src/views/pages/admin-site-edit.ejs@ 7881080

main
Last change on this file since 7881080 was 98ecf51, checked in by roboburr <roboburr@…>, 3 months ago

Hub #1+#2: site admin permissions work + assign owner to another user

#1 — canAdminSite was reading user.siteRoles which was NEVER populated (dead
code) → only god and the literal owner passed admin checks. Now canAdminSite
queries site_members directly (an assigned co-admin). requireSiteManager/BySlug
use canAdminSite so collaborators can access site management too.

#2 — owner_id was hard-coded to the creating god → you couldn't assign a site to
another user (the core of hub mode was missing). Now: god-only "Owner" select in
the site form; create/save set owner_id + a site_members-admin row for that owner
(upsert). Non-god doesn't see the field and cannot change the owner.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@…>

  • Property mode set to 100644
File size: 25.5 KB
RevLine 
[7bc636b]1<div class="container admin-site-edit-page">
[8b014ef]2 <p><a href="/admin" class="btn">&larr; Beheer</a></p>
[544cd86]3 <h1><%= isNew ? 'Nieuwe site' : 'Uiterlijk: ' + site.title %></h1>
[7bc636b]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>
[544cd86]11 <legend>Identiteit</legend>
[7bc636b]12 <label>
[3068cc0]13 <span>Slug (URL) <% if (!isNew) { %><em>(vast)</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="jouwslug"
19 autocapitalize="none" autocorrect="off" spellcheck="false">
20 </span>
[7bc636b]21 </label>
22 <label>
[544cd86]23 <span>Titel <small class="form-hint-inline">— getoond in de kop en als weergavenaam</small></span>
[7bc636b]24 <input type="text" name="title" value="<%= site.title || '' %>" required maxlength="200">
25 </label>
[98ecf51]26 <%# Eigenaar (her)toewijzen — ALLEEN god. In hub-modus geef je zo elke
27 gebruiker z'n eigen, zelf te beheren Klonkt. Niet-god ziet dit niet. %>
28 <% if (user && user.role === 'god') { %>
29 <label>
30 <span>Eigenaar <small class="form-hint-inline">— wie deze Klonkt zelf mag beheren</small></span>
31 <select name="owner_id">
32 <% (typeof users !== 'undefined' ? users : []).forEach(function(u){ %>
33 <option value="<%= u.id %>" <%= (site.owner_id === u.id) ? 'selected' : '' %>><%= u.username %><%= u.role === 'god' ? ' (god)' : '' %></option>
34 <% }); %>
35 </select>
36 </label>
37 <% } %>
[7bc636b]38 <label>
[544cd86]39 <span>Tagline <small class="form-hint-inline">— korte oneliner</small></span>
[7bc636b]40 <input type="text" name="tagline" value="<%= site.tagline || '' %>" maxlength="200">
41 </label>
42 <label>
[544cd86]43 <span>Bio / omschrijving <small class="form-hint-inline">— getoond in de profielkop en gebruikt voor SEO</small></span>
[7bc636b]44 <textarea name="description" rows="3" maxlength="500"><%= site.description || '' %></textarea>
45 </label>
46 <label>
[544cd86]47 <span>Profielfoto</span>
[7bc636b]48 <div class="photo-picker" id="photo-picker">
49 <div class="photo-thumb" id="photo-thumb" <%= site.profile_photo ? '' : 'data-empty' %>>
50 <img id="photo-preview" src="<%= site.profile_photo || '' %>" alt="" <%= site.profile_photo ? '' : 'hidden' %>>
51 <span class="photo-empty" aria-hidden="true">
52 <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>
53 </span>
54 </div>
55 <div class="photo-controls">
56 <input type="text" name="profile_photo" id="photo-url" value="<%= site.profile_photo || '' %>" placeholder="/media/avatars/foo.jpg of https://…">
57 <div class="photo-actions">
[544cd86]58 <button type="button" class="btn" id="photo-upload-trigger">📷 Uploaden</button>
[7bc636b]59 <button type="button" class="btn" id="photo-clear" <%= site.profile_photo ? '' : 'hidden' %>>Verwijder</button>
60 <small id="photo-status" class="form-hint photo-status"></small>
61 </div>
62 <input type="file" id="photo-upload-field" accept="image/jpeg,image/png,image/webp,image/gif" hidden>
63 </div>
64 </div>
65 </label>
66 <label>
[544cd86]67 <span>Taal (ISO-code)</span>
[7bc636b]68 <input type="text" name="language" value="<%= site.language || 'nl' %>" maxlength="8">
69 </label>
70 <label class="cb">
71 <input type="checkbox" name="profile_enabled" value="1" <%= site.profile_enabled ? 'checked' : '' %>>
[544cd86]72 <span>Profielkop tonen onder de navigatie</span>
[7bc636b]73 </label>
74 </fieldset>
75
76 <fieldset>
[544cd86]77 <legend>Vormgeving</legend>
[7bc636b]78
79 <%# ── Curated accent picker ───────────────────────────────────────
80 Was a free-form <input type="color">; now a fixed set of 8 hand-
81 picked colors that work in both light and dark themes. The hex is
82 still what's stored in the DB — we just constrain WHICH hex. %>
83 <fieldset class="accent-grid">
[544cd86]84 <legend>Accentkleur</legend>
[7bc636b]85 <% const _curAccent = (site.accent || '#c2410c').toLowerCase(); %>
86 <% accents.forEach(function(a) { %>
87 <label class="accent-swatch" title="<%= a.name %>">
88 <input type="radio" name="accent" value="<%= a.color %>" <%= a.color.toLowerCase() === _curAccent ? 'checked' : '' %>>
89 <span class="accent-dot" style="background: <%= a.color %>;" aria-hidden="true"></span>
90 <span class="accent-swatch-name"><%= a.name %></span>
91 </label>
92 <% }); %>
93 </fieldset>
94
95 <%# ── Default theme for new visitors ───────────────────────────
96 'auto' (empty value) lets the browser/OS decide via prefers-color-scheme.
97 'light' / 'dark' force the site default for visitors who haven't picked
98 their own preference yet. Logged-in users with users.theme set, and
99 visitors with a localStorage override, still win over this default. %>
100 <fieldset class="theme-default">
[544cd86]101 <legend>Standaardthema voor nieuwe bezoekers</legend>
[7bc636b]102 <% const td = site.theme_override || ''; %>
103 <label class="cb">
104 <input type="radio" name="theme_override" value="" <%= td === '' ? 'checked' : '' %>>
105 <span>Auto (volg device-voorkeur)</span>
106 </label>
107 <label class="cb">
108 <input type="radio" name="theme_override" value="light" <%= td === 'light' ? 'checked' : '' %>>
[544cd86]109 <span>Licht</span>
[7bc636b]110 </label>
111 <label class="cb">
112 <input type="radio" name="theme_override" value="dark" <%= td === 'dark' ? 'checked' : '' %>>
[544cd86]113 <span>Donker</span>
[7bc636b]114 </label>
115 </fieldset>
116
117 <fieldset class="palette-grid">
[544cd86]118 <legend>Palet</legend>
[7bc636b]119 <% palettes.forEach(function(p) { %>
120 <label class="palette-swatch">
121 <input type="radio" name="palette" value="<%= p.key %>" <%= (site.palette || 'sage') === p.key ? 'checked' : '' %>>
122 <span class="palette-preview" aria-hidden="true">
123 <span class="pp-half pp-light" style="background: <%= p.light.paper %>; color: <%= p.light.ink %>;">
124 <span class="pp-letter">Aa</span>
125 <span class="pp-accent" style="background: <%= p.light.accent %>;"></span>
126 </span>
127 <span class="pp-half pp-dark" style="background: <%= p.dark.paper %>; color: <%= p.dark.ink %>;">
128 <span class="pp-letter">Aa</span>
129 <span class="pp-accent" style="background: <%= p.dark.accent %>;"></span>
130 </span>
131 </span>
132 <span class="palette-swatch-name"><%= p.name %></span>
133 </label>
134 <% }); %>
135 </fieldset>
136 </fieldset>
137
138 <fieldset>
[544cd86]139 <legend>Gedrag</legend>
[7bc636b]140 <label class="cb">
141 <input type="checkbox" name="is_public" value="1" <%= site.is_public ? 'checked' : '' %>>
[544cd86]142 <span>Publieke site (uitvinken voor een besloten kring)</span>
[7bc636b]143 </label>
144 <label class="cb">
145 <input type="checkbox" name="robots_index" value="1" <%= site.robots_index ? 'checked' : '' %>>
[544cd86]146 <span>Zoekmachines mogen indexeren (sitemap.xml is verborgen indien uit)</span>
[7bc636b]147 </label>
148 <label class="cb">
149 <input type="checkbox" name="require_login_to_comment" value="1" <%= site.require_login_to_comment ? 'checked' : '' %>>
[544cd86]150 <span>Inloggen vereist om te reageren</span>
[7bc636b]151 </label>
152 <label>
[544cd86]153 <span>Reactie-moderatie</span>
[7bc636b]154 <select name="comments_moderation_mode">
[544cd86]155 <option value="trust" <%= (site.comments_moderation_mode || 'trust') === 'trust' ? 'selected' : '' %>>Vertrouwen (automatisch goedkeuren)</option>
156 <option value="moderate" <%= site.comments_moderation_mode === 'moderate' ? 'selected' : '' %>>Modereren (eerst in de wachtrij)</option>
[7bc636b]157 </select>
158 </label>
159 <label class="cb">
160 <input type="checkbox" name="enable_audio_player" value="1" <%= site.enable_audio_player ? 'checked' : '' %>>
[544cd86]161 <span>Audiospeler + embeds inschakelen</span>
[7bc636b]162 </label>
[2c246d4]163 <%# Prutter is een Hub-only premium-functie — toggle alleen tonen in Hub-modus. %>
164 <% if (typeof tenancy !== 'undefined' && tenancy === 'hub') { %>
[7bc636b]165 <label class="cb">
166 <input type="checkbox" name="enable_prutter" value="1" <%= site.enable_prutter ? 'checked' : '' %>>
[2c246d4]167 <span>Prutter (DM&rsquo;s tussen leden) inschakelen <small class="form-hint-inline">— Hub-functie, premium</small></span>
[7bc636b]168 </label>
[2c246d4]169 <% } %>
[7bc636b]170 </fieldset>
171
172 <fieldset class="profile-links-fieldset">
[544cd86]173 <legend>Social / streaming-links</legend>
174 <p class="form-hint">Getoond als merk-iconen op de profielkop. Voeg er zoveel toe als je wilt.</p>
[7bc636b]175
176 <div id="profile-links-rows">
177 <% (parsedLinks || []).forEach(function(l) { %>
178 <div class="profile-link-row">
179 <select name="profile_link_platform[]">
180 <% platforms.forEach(function(p) { %>
181 <option value="<%= p.key %>" <%= l.platform === p.key ? 'selected' : '' %>><%= p.label %></option>
182 <% }); %>
183 </select>
184 <input type="text" name="profile_link_url[]" value="<%= l.url %>" placeholder="https://…" required>
185 <button type="button" class="btn btn-danger pl-remove">×</button>
186 </div>
187 <% }); %>
188 </div>
189
190 <template id="profile-link-template">
191 <div class="profile-link-row">
192 <select name="profile_link_platform[]">
193 <% platforms.forEach(function(p) { %>
194 <option value="<%= p.key %>"><%= p.label %></option>
195 <% }); %>
196 </select>
197 <input type="text" name="profile_link_url[]" placeholder="https://…" required>
198 <button type="button" class="btn btn-danger pl-remove">×</button>
199 </div>
200 </template>
201
[544cd86]202 <button type="button" id="profile-link-add" class="btn">+ Link toevoegen</button>
[7bc636b]203 </fieldset>
204
205 <fieldset>
[544cd86]206 <legend>Feed-weergave</legend>
[7bc636b]207 <label>
[544cd86]208 <span>Standaardweergave voor de homepage</span>
[7bc636b]209 <select name="feed_view_default">
[544cd86]210 <option value="timeline" <%= (site.feed_view_default || 'timeline') === 'timeline' ? 'selected' : '' %>>Tijdlijn (chronologische lijst)</option>
211 <option value="grid" <%= site.feed_view_default === 'grid' ? 'selected' : '' %>>Grid (kaarten)</option>
[7bc636b]212 </select>
213 </label>
214 <label class="cb">
215 <input type="checkbox" name="feed_view_switch" value="1" <%= site.feed_view_switch ? 'checked' : '' %>>
[544cd86]216 <span>Tijdlijn ↔ grid-schakelaar boven de feed tonen</span>
[7bc636b]217 </label>
218 <label class="cb">
219 <input type="checkbox" name="show_search" value="1" <%= site.show_search ? 'checked' : '' %>>
[544cd86]220 <span>Zoekknop in de navigatie tonen</span>
[7bc636b]221 </label>
222 <label class="cb">
223 <input type="checkbox" name="show_archive_link" value="1" <%= site.show_archive_link ? 'checked' : '' %>>
[544cd86]224 <span>Archief-link in de navigatie tonen</span>
[7bc636b]225 </label>
226 </fieldset>
227
228 <fieldset>
229 <legend>SEO &amp; social</legend>
230 <label>
[544cd86]231 <span>Titel-sjabloon (gebruik {title} en {site})</span>
[7bc636b]232 <input type="text" name="title_template" value="<%= site.title_template || '{title} — {site}' %>" maxlength="200">
233 </label>
234 <label>
[544cd86]235 <span>Canonieke basis-URL (voor productie-HTTPS)</span>
236 <input type="text" name="canonical" value="<%= site.canonical || '' %>" placeholder="https://jouw-domein.com">
[7bc636b]237 </label>
238 <label>
[544cd86]239 <span>Twitter / X-handle (met @)</span>
240 <input type="text" name="twitter" value="<%= site.twitter || '' %>" placeholder="@jouwnaam" maxlength="64">
[7bc636b]241 </label>
242 <details>
[544cd86]243 <summary>Zoekmachine-verificatie</summary>
[7bc636b]244 <label>
[544cd86]245 <span>Google site-verificatie</span>
[7bc636b]246 <input type="text" name="google_verification" value="<%= site.google_verification || '' %>" maxlength="200">
247 </label>
248 <label>
249 <span>Bing (msvalidate.01)</span>
250 <input type="text" name="bing_verification" value="<%= site.bing_verification || '' %>" maxlength="200">
251 </label>
252 <label>
253 <span>Pinterest (p:domain_verify)</span>
254 <input type="text" name="pinterest_verification" value="<%= site.pinterest_verification || '' %>" maxlength="200">
255 </label>
256 <label>
257 <span>Yandex</span>
258 <input type="text" name="yandex_verification" value="<%= site.yandex_verification || '' %>" maxlength="200">
259 </label>
260 </details>
261 </fieldset>
262
263 <fieldset>
[544cd86]264 <legend>Eigen CSS &amp; HTML <em>(optioneel)</em></legend>
[7bc636b]265 <label>
[544cd86]266 <span>Eigen CSS (geïnjecteerd als &lt;style&gt; in &lt;head&gt;)</span>
[7bc636b]267 <textarea name="custom_css" rows="3" placeholder=".foo { color: red; }"><%= site.custom_css || '' %></textarea>
268 </label>
269 <label>
[544cd86]270 <span>Eigen &lt;head&gt;-HTML (analytics, extra meta&rsquo;s)</span>
[7bc636b]271 <textarea name="custom_head_html" rows="3" placeholder="<meta name=&quot;...&quot;>"><%= site.custom_head_html || '' %></textarea>
272 </label>
273 <label>
[544cd86]274 <span>Eigen footer-HTML</span>
[7bc636b]275 <textarea name="custom_foot_html" rows="3"><%= site.custom_foot_html || '' %></textarea>
276 </label>
277 </fieldset>
278
[544cd86]279 <button type="submit" class="btn btn-primary"><%= isNew ? 'Site aanmaken' : 'Wijzigingen opslaan' %></button>
[7bc636b]280 </form>
281</div>
282
283<script>
[3068cc0]284(function() {
285 // Slug-URL-veld: toon de echte host als gedimde prefix (de slug zelf is
286 // gekleurd via CSS). Hub → host/user/<slug>, anders host/<slug>.
287 var sh = document.getElementById('slug-host');
288 if (sh) sh.textContent = location.host + (sh.dataset.prefix || '/');
289})();
[7bc636b]290(function() {
291 // Profile-links repeater: add row from <template>, remove on click.
292 var rows = document.getElementById('profile-links-rows');
293 var tpl = document.getElementById('profile-link-template');
294 var add = document.getElementById('profile-link-add');
295 if (!rows || !tpl || !add) return;
296
297 add.addEventListener('click', function() {
298 var clone = tpl.content.cloneNode(true);
299 rows.appendChild(clone);
300 });
301 rows.addEventListener('click', function(e) {
302 if (e.target && e.target.classList.contains('pl-remove')) {
303 var row = e.target.closest('.profile-link-row');
304 if (row) row.remove();
305 }
306 });
307})();
308
309// P63 — Profile photo picker: upload via /admin/sites/upload-photo,
310// then write the returned URL into the visible input. Live thumb preview.
311(function() {
312 var picker = document.getElementById('photo-picker');
313 if (!picker) return;
314 var thumb = document.getElementById('photo-thumb');
315 var preview = document.getElementById('photo-preview');
316 var urlEl = document.getElementById('photo-url');
317 var trigger = document.getElementById('photo-upload-trigger');
318 var clear = document.getElementById('photo-clear');
319 var field = document.getElementById('photo-upload-field');
320 var status = document.getElementById('photo-status');
321
322 function showPreview(url) {
323 if (url) {
324 preview.src = url;
325 preview.hidden = false;
326 thumb.removeAttribute('data-empty');
327 clear.hidden = false;
328 } else {
329 preview.src = '';
330 preview.hidden = true;
331 thumb.setAttribute('data-empty', '');
332 clear.hidden = true;
333 }
334 }
335
336 if (urlEl) {
337 urlEl.addEventListener('input', function() {
338 showPreview(urlEl.value.trim());
339 });
340 }
341
342 if (trigger && field) {
343 trigger.addEventListener('click', function() { field.click(); });
344 field.addEventListener('change', async function() {
345 var file = field.files && field.files[0];
346 if (!file) return;
347 status.classList.remove('is-error');
348 status.textContent = 'Uploaden…';
349 try {
350 var fd = new FormData();
351 fd.append('photo', file);
352 var r = await fetch('/admin/sites/upload-photo', {
353 method: 'POST',
354 body: fd,
355 credentials: 'same-origin',
356 });
357 var j = await r.json();
358 if (!r.ok || !j.ok) throw new Error(j.error || ('Upload mislukt (' + r.status + ')'));
359 urlEl.value = j.url;
360 showPreview(j.url);
361 status.textContent = 'Geüpload ✓';
362 setTimeout(function() { status.textContent = ''; }, 2000);
363 } catch (e) {
364 status.classList.add('is-error');
365 status.textContent = 'Mislukt: ' + e.message;
366 } finally {
367 field.value = '';
368 }
369 });
370 }
371
372 if (clear) {
373 clear.addEventListener('click', function() {
374 urlEl.value = '';
375 showPreview('');
376 status.textContent = '';
377 // Note: doesn't delete the file from disk — saving the form with empty
378 // URL leaves the file orphaned on the server. Acceptable for now.
379 });
380 }
381})();
382</script>
383
384<style>
385.admin-site-edit-page { max-width: 760px; margin: 3rem auto; padding: 0 1rem; }
386.admin-site-edit-page h1 { font-family: var(--font-display, serif); font-size: 1.8rem; margin: 0 0 1.5rem; }
387
388.site-form { display: flex; flex-direction: column; gap: 1.5rem; }
389.site-form fieldset {
390 border: 1px solid var(--rule);
391 border-radius: 8px;
392 padding: 1rem 1.25rem;
393 background: var(--paper-2);
394 display: flex;
395 flex-direction: column;
396 gap: 0.75rem;
397}
398.site-form fieldset.palette-grid {
399 display: grid;
400 grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
401 background: var(--paper);
402 margin-top: 0.5rem;
403}
404/* Theme default radios — horizontal, sits between accent + palette */
405.site-form fieldset.theme-default {
406 display: flex; flex-direction: row; flex-wrap: wrap; gap: 1rem;
407 background: var(--paper);
408 margin-top: 0.5rem;
409}
410.site-form fieldset.theme-default label.cb { font-size: 0.9rem; color: var(--ink); }
411.site-form fieldset legend { font-family: var(--font-display, serif); font-size: 1rem; padding: 0 0.4rem; }
412.site-form label { display: flex; flex-direction: column; gap: 0.25rem; font-size: 0.85rem; color: var(--ink-muted, var(--ink-soft)); }
413.site-form label.cb { flex-direction: row; align-items: center; gap: 0.5rem; }
414.site-form input[type="text"], .site-form input[type="color"], .site-form textarea {
415 padding: 0.55rem 0.85rem;
416 border: 1px solid var(--rule);
417 border-radius: 5px;
418 background: var(--paper);
419 color: var(--ink);
420 font-family: inherit;
421 font-size: 0.95rem;
422}
423.site-form input[type="color"] { padding: 0.2rem; height: 38px; }
424.site-form textarea { resize: vertical; min-height: 60px; font-family: var(--font-mono, monospace); font-size: 0.85rem; }
425.site-form input[readonly] { opacity: 0.6; cursor: not-allowed; background: var(--paper-2); }
426
[3068cc0]427/* Slug als URL-veld: gedimde host-prefix + de slug zelf in de accentkleur. */
428.slug-url {
429 display: flex; align-items: stretch;
430 border: 1px solid var(--rule); border-radius: 5px;
431 background: var(--paper); overflow: hidden;
432 transition: border-color 120ms;
433}
434.slug-url:focus-within { border-color: var(--accent); }
435.slug-url-host {
436 display: inline-flex; align-items: center;
437 padding-left: 0.85rem;
438 color: var(--ink-muted, var(--ink-soft));
439 white-space: nowrap; user-select: none;
440 font-size: 0.95rem;
441}
442.site-form .slug-url-input {
443 flex: 1 1 auto; min-width: 0;
444 border: none !important; border-radius: 0 !important;
445 background: transparent !important;
446 color: var(--accent) !important; font-weight: 700;
447 padding: 0.55rem 0.85rem 0.55rem 0.1rem;
448}
449.site-form .slug-url-input:focus { outline: none; }
450.site-form .slug-url-input[readonly] {
451 opacity: 1; cursor: default; background: transparent !important;
452}
453
[974de34]454/* Selects: zelfde look als de inputs, met een eigen chevron i.p.v. de kale
455 native pijl. De geopende optielijst blijft door de browser gerenderd. */
456.site-form select {
457 appearance: none;
458 -webkit-appearance: none;
459 padding: 0.55rem 2.2rem 0.55rem 0.85rem;
460 border: 1px solid var(--rule);
461 border-radius: 5px;
462 background-color: var(--paper);
463 color: var(--ink);
464 font-family: inherit;
465 font-size: 0.95rem;
466 cursor: pointer;
467 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");
468 background-repeat: no-repeat;
469 background-position: right 0.7rem center;
470 transition: border-color 0.15s ease, box-shadow 0.15s ease;
471}
472.site-form select:hover { border-color: var(--ink-soft, #888); }
473.site-form select:focus,
474.site-form input[type="text"]:focus,
475.site-form textarea:focus {
476 outline: none;
477 border-color: var(--accent, #c2410c);
478 box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent, #c2410c) 22%, transparent);
479}
480
[7bc636b]481/* Reuse the palette-swatch styles from account.ejs */
482.palette-swatch {
483 display: flex; flex-direction: column; gap: 0.4rem;
484 padding: 0.5rem;
485 border: 1px solid var(--rule); border-radius: 6px; background: var(--paper);
486 cursor: pointer; font-size: 0.85rem; transition: border-color 120ms;
487}
488.palette-swatch:hover { border-color: var(--accent); }
489.palette-swatch input[type="radio"] { position: absolute; opacity: 0; pointer-events: none; }
490.palette-swatch:has(input[type="radio"]:checked) { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent); }
491.palette-preview {
492 display: grid; grid-template-columns: 1fr 1fr;
493 width: 100%; height: 44px; border-radius: 4px; overflow: hidden;
494 border: 1px solid var(--rule);
495}
496.pp-half {
497 position: relative;
498 display: flex; align-items: center; justify-content: center;
499 font-family: var(--font-display, serif); font-weight: 600; font-size: 1rem;
500}
501.pp-letter { line-height: 1; }
502.pp-accent {
503 position: absolute; bottom: 4px; right: 4px;
504 width: 9px; height: 9px; border-radius: 50%;
505 border: 1px solid rgba(0,0,0,0.12);
506}
507.palette-swatch-name { font-weight: 500; color: var(--ink); text-align: center; }
508
509/* Accent picker: row of small colored swatches */
510.site-form fieldset.accent-grid {
511 display: grid;
512 grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
513 gap: 0.5rem;
514 background: var(--paper);
515 margin-top: 0.5rem;
516}
517.accent-swatch {
518 display: flex; flex-direction: column; align-items: center; gap: 0.4rem;
519 padding: 0.55rem 0.4rem;
520 border: 1px solid var(--rule); border-radius: 6px; background: var(--paper);
521 cursor: pointer; font-size: 0.78rem;
522 transition: border-color 120ms, box-shadow 120ms;
523}
524.accent-swatch:hover { border-color: var(--accent); }
525.accent-swatch input[type="radio"] { position: absolute; opacity: 0; pointer-events: none; }
526.accent-swatch:has(input[type="radio"]:checked) {
527 border-color: var(--accent);
528 box-shadow: 0 0 0 2px var(--accent);
529}
530.accent-dot {
531 width: 28px; height: 28px; border-radius: 50%;
532 border: 1px solid rgba(0,0,0,0.1);
533 box-shadow: inset 0 -2px 4px rgba(0,0,0,0.12);
534}
535.accent-swatch-name { color: var(--ink); text-align: center; font-weight: 500; }
536
537.audio-flash { padding: 0.75rem 1rem; border-radius: 6px; margin-bottom: 1rem; font-size: 0.9rem; }
538.audio-flash--ok { background: rgba(40, 160, 90, 0.15); color: #2a9d5e; border: 1px solid rgba(40, 160, 90, 0.3); }
539.audio-flash--err { background: rgba(200, 60, 60, 0.15); color: #c33; border: 1px solid rgba(200, 60, 60, 0.3); }
540
541.form-hint { font-size: 0.8rem; color: var(--ink-muted, var(--ink-soft)); margin: 0; }
542.form-hint-inline { font-size: 0.78rem; color: var(--ink-muted, var(--ink-soft)); font-weight: 400; margin-left: .25rem; }
543
544/* P63 — Profile photo picker
545 Mobile-first: thumb stacks above input/buttons. ≥520px the thumb floats
546 to the left and the controls fill the remaining width. */
547.photo-picker {
548 display: flex;
549 flex-direction: column;
550 gap: .65rem;
551 align-items: stretch;
552}
553.photo-thumb {
554 position: relative;
555 width: 88px; height: 88px;
556 border-radius: 50%;
557 border: 1px solid var(--rule);
558 background: var(--paper-2);
559 overflow: hidden;
560 flex-shrink: 0;
561 display: flex; align-items: center; justify-content: center;
562}
563.photo-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
564.photo-thumb .photo-empty {
565 display: flex; color: var(--ink-muted, var(--ink-soft)); opacity: .55;
566}
567.photo-thumb .photo-empty svg { width: 36px; height: 36px; }
568.photo-thumb:not([data-empty]) .photo-empty { display: none; }
569.photo-thumb[data-empty] img { display: none; }
570
571.photo-controls {
572 display: flex;
573 flex-direction: column;
574 gap: .5rem;
575 flex: 1;
576 min-width: 0;
577}
578.photo-controls input[type="text"] { width: 100%; }
579.photo-actions {
580 display: flex; align-items: center; flex-wrap: wrap;
581 gap: .4rem;
582}
583.photo-actions .btn { padding: .45rem .75rem; font-size: .85rem; }
584.photo-status { margin-left: .25rem; }
585.photo-status.is-error { color: #c2410c; }
586
587@media (min-width: 520px) {
588 .photo-picker { flex-direction: row; align-items: flex-start; gap: 1rem; }
589 .photo-controls { gap: .55rem; }
590}
591.profile-link-row {
592 display: grid; grid-template-columns: 160px 1fr auto; gap: 0.5rem; margin-bottom: 0.5rem;
593}
594.profile-link-row select { padding: 0.45rem 0.6rem; }
595.profile-links-fieldset details > label { margin-top: 0.4rem; }
596</style>
Note: See TracBrowser for help on using the repository browser.