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

main
Last change on this file since 48ebfa7 was 75c6919, checked in by roboburr <roboburr@…>, 3 months ago

fix(mobiel): <fieldset> in admin forms doesn't shrink → entire form too wide

Root cause deeper than the links row: a <fieldset> has UA default
min-inline-size:min-content and therefore won't shrink below its content → on
Appearance (admin-site-edit) the entire form (Identity/Behaviour) overflowed off
screen on mobile. Fix: min-width:0 on .site-form + fieldsets + form fields, and
max-width:100%/border-box on inputs. Also admin-seo (same .site-form/fieldsets)
proactively fixed. Measured: Identity fieldset 424→≤360px.

Co-Authored-By: Claude <noreply@…>

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