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

main
Last change on this file since 013e16c was 013e16c, checked in by Robin <roboburr@…>, 4 weeks ago

Naar SEO & social, want sameAs is schema.org (Robins vraag)

Hij vroeg of het niet onder SEO en social hoort, "omdat het ook een soort
koppeling is". Dat is het, en sterker dan dat: op die pagina staat een blok
"Uitgever (JSON-LD / rich results)", en sameAs IS een schema.org-eigenschap.
Zelfde vocabulaire, zelfde plek. Het paneel staat nu direct onder Uitgever.

DAARDOOR VOEDT EEN KOPPELING NU TWEE UITGANGEN. Naast schema:sameAs op de
fediverse-actor komt hij in de JSON-LD van de pagina zelf, in _publisher. Dat
is hetzelfde feit, verteld aan de andere lezer -- en het was gratis, want het
blok stond er al.

BETERE LABELS, want de oude waren geschreven voor het audioscherm. "Ben jij dit
in MusicBrainz?" werd "MusicBrainz (dezelfde artiest elders)" als legenda, met
een hint die zegt wat het OPLEVERT in plaats van alleen wat het is: je komt als
sameAs in de JSON-LD en in je fediverse-profiel, en er verandert niets bij
MusicBrainz zelf. Dat laatste is de geruststelling die er hoort te staan. Plus
een veldlabel ("Zoek jezelf op") met de toelichting "de naam waaronder je muziek
uitbrengt" -- want dat is de vraag, niet je sitenaam. Drie talen.

Het zoekveld begint nu op publisher_name en valt terug op de sitetitel: op deze
pagina heb je die naam net ingevuld.

Meeverhuisd: het endpoint (/admin/seo/api/musicbrainz) en het clientblok, in een
nieuwe mod/admin-seo.js. Die pagina had nog geen JavaScript; opzoeken vraagt er
wel om. Het profielscherm is weer helemaal schoon.

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

  • Property mode set to 100644
File size: 21.7 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="/">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 <label>
27 <span><%= t('asite.tagline') %> <small class="form-hint-inline"><%= t('asite.tagline_hint') %></small></span>
28 <input type="text" name="tagline" value="<%= site.tagline || '' %>" maxlength="200">
29 </label>
30 <label>
31 <span><%= t('asite.bio') %> <small class="form-hint-inline"><%= t('asite.bio_hint') %></small></span>
32 <textarea name="description" rows="3" maxlength="500"><%= site.description || '' %></textarea>
33 </label>
34 <label>
35 <span><%= t('asite.profile_photo') %></span>
36 <div class="photo-picker" id="photo-picker">
37 <div class="photo-thumb" id="photo-thumb" <%= site.profile_photo ? '' : 'data-empty' %>>
38 <img id="photo-preview" src="<%= avatar(site.profile_photo || '', 128) %>" alt="" <%= site.profile_photo ? '' : 'hidden' %>>
39 <span class="photo-empty" aria-hidden="true">
40 <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>
41 </span>
42 </div>
43 <div class="photo-controls">
44 <input type="text" name="profile_photo" id="photo-url" value="<%= site.profile_photo || '' %>" placeholder="<%= t('asite.photo_url_placeholder') %>">
45 <div class="photo-actions">
46 <button type="button" class="btn" id="photo-upload-trigger"><%= t('asite.photo_upload') %></button>
47 <button type="button" class="btn" id="photo-clear" <%= site.profile_photo ? '' : 'hidden' %>><%= t('asite.photo_clear') %></button>
48 <small id="photo-status" class="form-hint photo-status"></small>
49 </div>
50 <input type="file" id="photo-upload-field" accept="image/jpeg,image/png,image/webp,image/gif" hidden>
51 </div>
52 </div>
53 </label>
54 <label>
55 <span><%= t('asite.language') %></span>
56 <input type="text" name="language" value="<%= site.language || 'nl' %>" maxlength="8">
57 </label>
58 <label class="cb">
59 <input type="checkbox" name="profile_enabled" value="1" <%= site.profile_enabled ? 'checked' : '' %>>
60 <span><%= t('asite.profile_enabled') %></span>
61 </label>
62 </fieldset>
63
64 <fieldset>
65 <legend><%= t('asite.appearance') %></legend>
66
67 <%# ── Curated accent picker ───────────────────────────────────────
68 Was a free-form <input type="color">; now a fixed set of 8 hand-
69 picked colors that work in both light and dark themes. The hex is
70 still what's stored in the DB — we just constrain WHICH hex. %>
71 <fieldset class="accent-grid">
72 <legend><%= t('asite.accent_color') %></legend>
73 <% const _curAccent = (site.accent || '#c2410c').toLowerCase(); %>
74 <% accents.forEach(function(a) { %>
75 <label class="accent-swatch" title="<%= a.name %>">
76 <input type="radio" name="accent" value="<%= a.color %>" <%= a.color.toLowerCase() === _curAccent ? 'checked' : '' %>>
77 <span class="accent-dot" style="background: <%= a.color %>;" aria-hidden="true"></span>
78 <span class="accent-swatch-name"><%= a.name %></span>
79 </label>
80 <% }); %>
81 </fieldset>
82
83 <%# ── Default theme for new visitors ───────────────────────────
84 'auto' (empty value) lets the browser/OS decide via prefers-color-scheme.
85 'light' / 'dark' force the site default for visitors who haven't picked
86 their own preference yet. Logged-in users with users.theme set, and
87 visitors with a localStorage override, still win over this default. %>
88 <fieldset class="theme-default">
89 <legend><%= t('asite.theme_default') %></legend>
90 <% const td = site.theme_override || ''; %>
91 <label class="cb">
92 <input type="radio" name="theme_override" value="" <%= td === '' ? 'checked' : '' %>>
93 <span><%= t('asite.theme_auto') %></span>
94 </label>
95 <label class="cb">
96 <input type="radio" name="theme_override" value="light" <%= td === 'light' ? 'checked' : '' %>>
97 <span><%= t('asite.theme_light') %></span>
98 </label>
99 <label class="cb">
100 <input type="radio" name="theme_override" value="dark" <%= td === 'dark' ? 'checked' : '' %>>
101 <span><%= t('asite.theme_dark') %></span>
102 </label>
103 </fieldset>
104
105 <fieldset class="palette-grid">
106 <legend><%= t('asite.palette') %></legend>
107 <% palettes.forEach(function(p) { %>
108 <label class="palette-swatch">
109 <input type="radio" name="palette" value="<%= p.key %>" <%= (site.palette || 'klonkt') === p.key ? 'checked' : '' %>>
110 <span class="palette-preview" aria-hidden="true">
111 <span class="pp-half pp-light" style="background: <%= p.light.paper %>; color: <%= p.light.ink %>;">
112 <span class="pp-letter">Aa</span>
113 <span class="pp-accent" style="background: <%= p.light.accent %>;"></span>
114 </span>
115 <span class="pp-half pp-dark" style="background: <%= p.dark.paper %>; color: <%= p.dark.ink %>;">
116 <span class="pp-letter">Aa</span>
117 <span class="pp-accent" style="background: <%= p.dark.accent %>;"></span>
118 </span>
119 </span>
120 <span class="palette-swatch-name"><%= p.name %></span>
121 </label>
122 <% }); %>
123 </fieldset>
124 </fieldset>
125
126 <%# Live preview: selecting an accent / theme / palette re-themes this page
127 instantly (reverts on reload; persists on save). %>
128 <%# Het script van deze pagina staat in assets/js/mod/admin-site-edit.js (shaer-bqr). %>
129
130 <fieldset>
131 <legend><%= t('asite.behavior') %></legend>
132 <label class="cb">
133 <input type="checkbox" name="is_public" value="1" <%= site.is_public ? 'checked' : '' %>>
134 <span><%= t('asite.is_public') %></span>
135 </label>
136 <label class="cb">
137 <input type="checkbox" name="robots_index" value="1" <%= site.robots_index ? 'checked' : '' %>>
138 <span><%= t('asite.robots_index') %></span>
139 </label>
140 <% if (typeof audioEnabled === 'undefined' || audioEnabled) { %>
141 <label class="cb">
142 <input type="checkbox" name="enable_audio_player" value="1" <%= site.enable_audio_player ? 'checked' : '' %>>
143 <span><%= t('asite.enable_audio') %></span>
144 </label>
145 <% } %>
146 </fieldset>
147
148 <fieldset class="profile-links-fieldset">
149 <legend><%= t('asite.links') %></legend>
150 <p class="form-hint"><%= t('asite.links_hint') %></p>
151
152 <div id="profile-links-rows">
153 <% (parsedLinks || []).forEach(function(l) { %>
154 <div class="profile-link-row">
155 <select name="profile_link_platform[]">
156 <% platforms.forEach(function(p) { %>
157 <option value="<%= p.key %>" <%= l.platform === p.key ? 'selected' : '' %>><%= p.label %></option>
158 <% }); %>
159 </select>
160 <input type="text" name="profile_link_url[]" value="<%= l.url %>" placeholder="https://…" required>
161 <button type="button" class="btn btn-danger pl-remove">×</button>
162 </div>
163 <% }); %>
164 </div>
165
166 <template id="profile-link-template">
167 <div class="profile-link-row">
168 <select name="profile_link_platform[]">
169 <% platforms.forEach(function(p) { %>
170 <option value="<%= p.key %>"><%= p.label %></option>
171 <% }); %>
172 </select>
173 <input type="text" name="profile_link_url[]" placeholder="https://…" required>
174 <button type="button" class="btn btn-danger pl-remove">×</button>
175 </div>
176 </template>
177
178 <button type="button" id="profile-link-add" class="btn"><%= t('asite.link_add') %></button>
179 </fieldset>
180
181 <fieldset>
182 <legend><%= t('asite.aliases') %></legend>
183 <p class="form-hint"><%= t('asite.aliases_hint') %></p>
184 <label>
185<%# typeof-guard on purpose: during a deploy an old route can render this new
186 template for one request (view cache fills on first hit). A missing local
187 must degrade to an empty field, not take the whole page down with a 500. %>
188 <textarea name="ap_aliases" rows="3" placeholder="@oud@mastodon.social&#10;https://andere-klonkt.example/ap/users/naam"><%= typeof apAliases !== 'undefined' ? apAliases : '' %></textarea>
189 </label>
190 </fieldset>
191
192 <fieldset>
193 <legend><%= t('asite.feed_view') %></legend>
194 <label>
195 <span><%= t('asite.feed_default') %></span>
196 <select name="feed_view_default">
197 <option value="timeline" <%= (site.feed_view_default || 'timeline') === 'timeline' ? 'selected' : '' %>><%= t('asite.feed_timeline') %></option>
198 <option value="grid" <%= site.feed_view_default === 'grid' ? 'selected' : '' %>><%= t('asite.feed_grid') %></option>
199 </select>
200 </label>
201 <label class="cb">
202 <input type="checkbox" name="feed_view_switch" value="1" <%= site.feed_view_switch ? 'checked' : '' %>>
203 <span><%= t('asite.feed_switch') %></span>
204 </label>
205 <label class="cb">
206 <input type="checkbox" name="show_search" value="1" <%= site.show_search ? 'checked' : '' %>>
207 <span><%= t('asite.show_search') %></span>
208 </label>
209 <label class="cb">
210 <input type="checkbox" name="show_archive_link" value="1" <%= site.show_archive_link ? 'checked' : '' %>>
211 <span><%= t('asite.show_archive') %></span>
212 </label>
213 </fieldset>
214
215 <fieldset class="seo-pointer">
216 <legend><%= t('asite.seo') %></legend>
217 <p><%= t('asite.seo_pointer') %> <a href="/admin/seo"><%= t('asite.seo_link') %></a>.</p>
218 </fieldset>
219
220 <fieldset>
221 <legend><%= t('asite.custom_legend') %> <em><%= t('asite.optional') %></em></legend>
222 <label>
223 <span><%= t('asite.custom_css') %></span>
224 <textarea name="custom_css" rows="3" placeholder=".foo { color: red; }"><%= site.custom_css || '' %></textarea>
225 </label>
226 <label>
227 <span><%= t('asite.custom_head') %></span>
228 <textarea name="custom_head_html" rows="3" placeholder="<meta name=&quot;...&quot;>"><%= site.custom_head_html || '' %></textarea>
229 </label>
230 <label>
231 <span><%= t('asite.custom_foot') %></span>
232 <textarea name="custom_foot_html" rows="3"><%= site.custom_foot_html || '' %></textarea>
233 </label>
234 </fieldset>
235
236 <button type="submit" class="btn btn-primary"><%= isNew ? t('asite.submit_create') : t('asite.submit_save') %></button>
237 </form>
238
239<% if (!isNew) { %>
240 <form method="post" action="/admin/sites/<%= site.slug %>/move" class="admin-form"
241 onsubmit="return confirm('<%= t('asite.move_confirm') %>');">
242 <fieldset>
243 <legend><%= t('asite.move') %></legend>
244 <p class="form-hint"><%= t('asite.move_hint') %></p>
245 <% if (typeof site.moved_to === 'string' && site.moved_to) { %>
246 <p class="form-hint"><strong><%= t('asite.moved_to') %>:</strong> <%= site.moved_to %></p>
247 <% } %>
248 <label>
249 <input type="text" name="move_target" placeholder="@nieuw@andere-server.example" required>
250 </label>
251 <button type="submit" class="btn btn-danger"><%= t('asite.move_btn') %></button>
252 </fieldset>
253 </form>
254<% } %>
255</div>
256
257
258
259<style>
260.admin-site-edit-page { max-width: 760px; margin: 3rem auto; padding: 0 1rem; }
261.admin-site-edit-page h1 { font-family: var(--font-display, serif); font-size: 1.8rem; margin: 0 0 1.5rem; }
262
263.site-form { display: flex; flex-direction: column; gap: 1.5rem; min-width: 0; }
264/* Mobile: a <fieldset> has a browser-default min-inline-size:min-content and
265 therefore does NOT shrink → the entire form became wider than the screen. min-width:0
266 forces it to shrink; form fields get max-width:100% + border-box so they
267 never overflow their fieldset. */
268.site-form fieldset { min-width: 0; }
269.site-form input, .site-form select, .site-form textarea { min-width: 0; max-width: 100%; box-sizing: border-box; }
270.site-form fieldset {
271 border: 1px solid var(--rule);
272 border-radius: 8px;
273 padding: 1rem 1.25rem;
274 background: var(--paper-2);
275 display: flex;
276 flex-direction: column;
277 gap: 0.75rem;
278}
279.site-form fieldset.palette-grid {
280 display: grid;
281 grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
282 background: var(--paper);
283 margin-top: 0.5rem;
284}
285/* Theme default radios — horizontal, sits between accent + palette */
286.site-form fieldset.theme-default {
287 display: flex; flex-direction: row; flex-wrap: wrap; gap: 1rem;
288 background: var(--paper);
289 margin-top: 0.5rem;
290}
291.site-form fieldset.theme-default label.cb { font-size: 0.9rem; color: var(--ink); }
292.site-form fieldset legend { font-family: var(--font-display, serif); font-size: 1rem; padding: 0 0.4rem; }
293.site-form label { display: flex; flex-direction: column; gap: 0.25rem; font-size: 0.85rem; color: var(--ink-muted, var(--ink-soft)); }
294.site-form label.cb { flex-direction: row; align-items: center; gap: 0.5rem; }
295.site-form input[type="text"], .site-form input[type="color"], .site-form textarea {
296 padding: 0.55rem 0.85rem;
297 border: 1px solid var(--rule);
298 border-radius: 5px;
299 background: var(--paper);
300 color: var(--ink);
301 font-family: inherit;
302 font-size: 0.95rem;
303}
304.site-form input[type="color"] { padding: 0.2rem; height: 38px; }
305.site-form textarea { resize: vertical; min-height: 60px; font-family: var(--font-mono, monospace); font-size: 0.85rem; }
306.site-form input[readonly] { opacity: 0.6; cursor: not-allowed; background: var(--paper-2); }
307
308/* Slug as URL field: dimmed host prefix + the slug itself in the accent colour. */
309.slug-url {
310 display: flex; align-items: stretch;
311 border: 1px solid var(--rule); border-radius: 5px;
312 background: var(--paper); overflow: hidden;
313 transition: border-color 120ms;
314}
315.slug-url:focus-within { border-color: var(--accent); }
316.slug-url-host {
317 display: inline-flex; align-items: center;
318 padding-left: 0.85rem;
319 color: var(--ink-muted, var(--ink-soft));
320 white-space: nowrap; user-select: none;
321 font-size: 0.95rem;
322}
323.site-form .slug-url-input {
324 flex: 1 1 auto; min-width: 0;
325 border: none !important; border-radius: 0 !important;
326 background: transparent !important;
327 color: var(--accent) !important; font-weight: 700;
328 padding: 0.55rem 0.85rem 0.55rem 0.1rem;
329}
330.site-form .slug-url-input:focus { outline: none; }
331.site-form .slug-url-input[readonly] {
332 opacity: 1; cursor: default; background: transparent !important;
333}
334
335/* Selects: same look as the inputs, with a custom chevron instead of the bare
336 native arrow. The open option list is still rendered by the browser. */
337.site-form select {
338 appearance: none;
339 -webkit-appearance: none;
340 padding: 0.55rem 2.2rem 0.55rem 0.85rem;
341 border: 1px solid var(--rule);
342 border-radius: 5px;
343 background-color: var(--paper);
344 color: var(--ink);
345 font-family: inherit;
346 font-size: 0.95rem;
347 cursor: pointer;
348 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");
349 background-repeat: no-repeat;
350 background-position: right 0.7rem center;
351 transition: border-color 0.15s ease, box-shadow 0.15s ease;
352}
353.site-form select:hover { border-color: var(--ink-soft, #888); }
354.site-form select:focus,
355.site-form input[type="text"]:focus,
356.site-form textarea:focus {
357 outline: none;
358 border-color: var(--accent, #c2410c);
359 box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent, #c2410c) 22%, transparent);
360}
361
362/* Reuse the palette-swatch styles from account.ejs */
363.palette-swatch {
364 display: flex; flex-direction: column; gap: 0.4rem;
365 padding: 0.5rem;
366 border: 1px solid var(--rule); border-radius: 6px; background: var(--paper);
367 cursor: pointer; font-size: 0.85rem; transition: border-color 120ms;
368}
369.palette-swatch:hover { border-color: var(--accent); }
370.palette-swatch input[type="radio"] { position: absolute; opacity: 0; pointer-events: none; }
371.palette-swatch:has(input[type="radio"]:checked) { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent); }
372.palette-preview {
373 display: grid; grid-template-columns: 1fr 1fr;
374 width: 100%; height: 44px; border-radius: 4px; overflow: hidden;
375 border: 1px solid var(--rule);
376}
377.pp-half {
378 position: relative;
379 display: flex; align-items: center; justify-content: center;
380 font-family: var(--font-display, serif); font-weight: 600; font-size: 1rem;
381}
382.pp-letter { line-height: 1; }
383.pp-accent {
384 position: absolute; bottom: 4px; right: 4px;
385 width: 9px; height: 9px; border-radius: 50%;
386 border: 1px solid rgba(0,0,0,0.12);
387}
388.palette-swatch-name { font-weight: 500; color: var(--ink); text-align: center; }
389
390/* Accent picker: row of small colored swatches */
391.site-form fieldset.accent-grid {
392 display: grid;
393 grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
394 gap: 0.5rem;
395 background: var(--paper);
396 margin-top: 0.5rem;
397}
398.accent-swatch {
399 display: flex; flex-direction: column; align-items: center; gap: 0.4rem;
400 padding: 0.55rem 0.4rem;
401 border: 1px solid var(--rule); border-radius: 6px; background: var(--paper);
402 cursor: pointer; font-size: 0.78rem;
403 transition: border-color 120ms, box-shadow 120ms;
404}
405.accent-swatch:hover { border-color: var(--accent); }
406.accent-swatch input[type="radio"] { position: absolute; opacity: 0; pointer-events: none; }
407.accent-swatch:has(input[type="radio"]:checked) {
408 border-color: var(--accent);
409 box-shadow: 0 0 0 2px var(--accent);
410}
411.accent-dot {
412 width: 28px; height: 28px; border-radius: 50%;
413 border: 1px solid rgba(0,0,0,0.1);
414 box-shadow: inset 0 -2px 4px rgba(0,0,0,0.12);
415}
416.accent-swatch-name { color: var(--ink); text-align: center; font-weight: 500; }
417
418.audio-flash { padding: 0.75rem 1rem; border-radius: 6px; margin-bottom: 1rem; font-size: 0.9rem; }
419.audio-flash--ok { background: rgba(40, 160, 90, 0.15); color: #2a9d5e; border: 1px solid rgba(40, 160, 90, 0.3); }
420.audio-flash--err { background: rgba(200, 60, 60, 0.15); color: #c33; border: 1px solid rgba(200, 60, 60, 0.3); }
421
422.form-hint { font-size: 0.8rem; color: var(--ink-muted, var(--ink-soft)); margin: 0; }
423.form-hint-inline { font-size: 0.78rem; color: var(--ink-muted, var(--ink-soft)); font-weight: 400; margin-left: .25rem; }
424
425/* P63 — Profile photo picker
426 Mobile-first: thumb stacks above input/buttons. ≥520px the thumb floats
427 to the left and the controls fill the remaining width. */
428.photo-picker {
429 display: flex;
430 flex-direction: column;
431 gap: .65rem;
432 align-items: stretch;
433}
434.photo-thumb {
435 position: relative;
436 width: 88px; height: 88px;
437 border-radius: 50%;
438 border: 1px solid var(--rule);
439 background: var(--paper-2);
440 overflow: hidden;
441 flex-shrink: 0;
442 display: flex; align-items: center; justify-content: center;
443}
444.photo-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
445.photo-thumb .photo-empty {
446 display: flex; color: var(--ink-muted, var(--ink-soft)); opacity: .55;
447}
448.photo-thumb .photo-empty svg { width: 36px; height: 36px; }
449.photo-thumb:not([data-empty]) .photo-empty { display: none; }
450.photo-thumb[data-empty] img { display: none; }
451
452.photo-controls {
453 display: flex;
454 flex-direction: column;
455 gap: .5rem;
456 flex: 1;
457 min-width: 0;
458}
459.photo-controls input[type="text"] { width: 100%; }
460.photo-actions {
461 display: flex; align-items: center; flex-wrap: wrap;
462 gap: .4rem;
463}
464.photo-actions .btn { padding: .45rem .75rem; font-size: .85rem; }
465.photo-status { margin-left: .25rem; }
466.photo-status.is-error { color: #c2410c; }
467
468@media (min-width: 520px) {
469 .photo-picker { flex-direction: row; align-items: flex-start; gap: 1rem; }
470 .photo-controls { gap: .55rem; }
471}
472/* Mobile-first: platform selector on its own line (full width), URL input + remove
473 button below. Prevents horizontal overflow on phones (fixed 160px column +
474 non-shrinkable input together ~447px > screen width). */
475.profile-link-row {
476 display: grid; grid-template-columns: 1fr auto; gap: 0.5rem; margin-bottom: 0.5rem;
477}
478.profile-link-row select { padding: 0.45rem 0.6rem; grid-column: 1 / -1; }
479.profile-link-row input { min-width: 0; }
480@media (min-width: 520px) {
481 .profile-link-row { grid-template-columns: 160px 1fr auto; }
482 .profile-link-row select { grid-column: auto; }
483}
484.profile-links-fieldset details > label { margin-top: 0.4rem; }
485</style>
Note: See TracBrowser for help on using the repository browser.