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

main
Last change on this file since 746d98a was 746d98a, checked in by Robin <roboburr@…>, 3 hours ago

Wardmodus: een besloten Klonkt met alleen het profiel openbaar

Robin, 30-9: "Ward Profile mode gaat actief wanneer ward gekoppeld is, dus
wanneer de persoon guardian(s) heeft. Een besloten Klonkt met enkel het profiel
van de ward publiek. Wij denken dat dit het beste valt bij emancipation om de
functie weer naar normaal Klonkt te brengen."

AFGELEID, NIET OPGESLAGEN. Heeft de site geaccepteerde guardians, dan staat de
modus aan; is de set leeg, dan uit. listGuardians telt alleen geaccepteerde
guardianships, dus een voorstel zet niets dicht, en de set leegmaken is
emancipatie (FEP-633c 3.4) -- precies waar het ontwerp de weg terug legt. Een
schakelaar zou een tweede waarheid naast de guardianship zijn.

STANDAARD DICHT (middleware/ward-profile.js, in server.js direct na de
siteresolver en de schrijfpoort). Een bezoeker ziet op / het profiel en verder
niets: een pagina gaat terug naar het profiel, al het andere is 404, een POST
bereikt zijn handler niet, en een htmx-navigatie krijgt HX-Redirect in plaats
van een brokstuk. Wat niet op de lijst staat komt er niet langs, zodat een
nieuwe route niet vanzelf een kind openzet.

Open blijft: inloggen, account, beheer, het guardianpaneel, /lang, de bestanden
van de pagina zelf, en /audio/stream -- die bewaakt elk bestand zelf, en een
track die de ward op de fediverse opende moet daar speelbaar blijven. De
federatie (ActivityPub, WebFinger, OAuth voor Shaer, /media, /assets, /og) hangt
al VOOR de poort; de paden staan toch op de lijst, voor als de volgorde ooit
omgaat, want dan zou een ward stil van de fediverse verdwijnen.

Wie alles ziet: de ward zelf, en god, kijker en sitebeheer. Een andere
ingelogde gebruiker is gewoon een bezoeker.

DE PROFIELPAGINA (pages/ward-profile.ejs) mag verzorgd, want dit is alles wat
een bezoeker ziet: foto met een ring in de accentkleur, de naam groot, de
handle op een pil, de bio, de links als brede knoppen in hun merkkleur, een
volgknop die de bestaande fediverse-dialoog opent (een volgverzoek aan een ward
gaat eerst langs de guardians), en een neutrale regel dat berichten alleen
voor volgers zijn. Nergens "kind" of "ward": dat de account guardians heeft
staat al openbaar op de fediverse. Dezelfde bronnen als de profielkop, zodat
het profiel op geen twee plekken iets anders kan zeggen. De chrome biedt een
bezoeker niets aan wat dicht is: geen zoekknop, geen archief, geen
Lezen/Grid-schakelaar.

DE PROFIELLINKS verhuizen van Appearance naar het account (/account#links),
bij naam, bio en foto. Twee vallen onderweg, allebei getoetst:

  • Appearance schreef profile_links altijd, uit de formuliervelden. Met de velden weg zou elke keer opslaan daar de links wissen. Die pagina schrijft ze niet meer.
  • ownedSite haalde profile_links niet op, dus de accountpagina toonde nul links en opslaan wiste ze. Gevonden bij het bekijken, niet door een toets; nu wel.

buildProfileLinks staat in PlatformIcons.js, en de rijen-knoppen in een eigen
module (mod/profile-links.js).

Getoetst: gewone Klonkt zonder guardians, een voorstel zet niets dicht,
gekoppeld alleen het profiel, pagina's terug, 404 voor wat geen pagina is, geen
handler voor een POST, HX-Redirect, wat open moet blijft open, ward en beheer
zien alles, een andere ingelogde is een bezoeker, emancipatie maakt het weer
gewoon, de links bij het account en Appearance wist ze niet. Tegenproef per
zaak. Volledige suite 1325 groen.

Nagekeken in de browser op 375 en 1280 pixels: het profiel past op een
telefoonscherm, knoppen van 55 pixels, geen overloop, de volgknop opent de
dialoog; de accountpagina toont de bestaande links en voegt er een toe.

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

  • Property mode set to 100644
File size: 20.8 KB
RevLine 
[7bc636b]1<div class="container admin-site-edit-page">
[bbf9d1a]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>
[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>
[bbf9d1a]11 <legend><%= t('asite.identity') %></legend>
[7bc636b]12 <label>
[bbf9d1a]13 <span><%= t('asite.slug') %> <% if (!isNew) { %><em><%= t('asite.slug_fixed') %></em><% } %></span>
[3068cc0]14 <span class="slug-url">
15 <span class="slug-url-host" id="slug-host"
[72ec6a4]16 data-prefix="/">website.com/</span><input
[3068cc0]17 type="text" name="slug" id="slug-input" class="slug-url-input" value="<%= site.slug %>" required <% if (!isNew) { %>readonly<% } %>
[bbf9d1a]18 pattern="[a-z0-9_-]{2,40}" placeholder="<%= t('asite.slug_placeholder') %>"
[3068cc0]19 autocapitalize="none" autocorrect="off" spellcheck="false">
20 </span>
[7bc636b]21 </label>
22 <label>
[bbf9d1a]23 <span><%= t('asite.field_title') %> <small class="form-hint-inline"><%= t('asite.field_title_hint') %></small></span>
[7bc636b]24 <input type="text" name="title" value="<%= site.title || '' %>" required maxlength="200">
25 </label>
26 <label>
[bbf9d1a]27 <span><%= t('asite.tagline') %> <small class="form-hint-inline"><%= t('asite.tagline_hint') %></small></span>
[7bc636b]28 <input type="text" name="tagline" value="<%= site.tagline || '' %>" maxlength="200">
29 </label>
30 <label>
[bbf9d1a]31 <span><%= t('asite.bio') %> <small class="form-hint-inline"><%= t('asite.bio_hint') %></small></span>
[7bc636b]32 <textarea name="description" rows="3" maxlength="500"><%= site.description || '' %></textarea>
33 </label>
34 <label>
[bbf9d1a]35 <span><%= t('asite.profile_photo') %></span>
[7bc636b]36 <div class="photo-picker" id="photo-picker">
37 <div class="photo-thumb" id="photo-thumb" <%= site.profile_photo ? '' : 'data-empty' %>>
[99989f9]38 <img id="photo-preview" src="<%= avatar(site.profile_photo || '', 128) %>" alt="" <%= site.profile_photo ? '' : 'hidden' %>>
[7bc636b]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">
[bbf9d1a]44 <input type="text" name="profile_photo" id="photo-url" value="<%= site.profile_photo || '' %>" placeholder="<%= t('asite.photo_url_placeholder') %>">
[7bc636b]45 <div class="photo-actions">
[bbf9d1a]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>
[7bc636b]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>
[bbf9d1a]55 <span><%= t('asite.language') %></span>
[7bc636b]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' : '' %>>
[bbf9d1a]60 <span><%= t('asite.profile_enabled') %></span>
[7bc636b]61 </label>
62 </fieldset>
63
64 <fieldset>
[bbf9d1a]65 <legend><%= t('asite.appearance') %></legend>
[7bc636b]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">
[bbf9d1a]72 <legend><%= t('asite.accent_color') %></legend>
[7bc636b]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">
[bbf9d1a]89 <legend><%= t('asite.theme_default') %></legend>
[7bc636b]90 <% const td = site.theme_override || ''; %>
91 <label class="cb">
92 <input type="radio" name="theme_override" value="" <%= td === '' ? 'checked' : '' %>>
[bbf9d1a]93 <span><%= t('asite.theme_auto') %></span>
[7bc636b]94 </label>
95 <label class="cb">
96 <input type="radio" name="theme_override" value="light" <%= td === 'light' ? 'checked' : '' %>>
[bbf9d1a]97 <span><%= t('asite.theme_light') %></span>
[7bc636b]98 </label>
99 <label class="cb">
100 <input type="radio" name="theme_override" value="dark" <%= td === 'dark' ? 'checked' : '' %>>
[bbf9d1a]101 <span><%= t('asite.theme_dark') %></span>
[7bc636b]102 </label>
103 </fieldset>
104
105 <fieldset class="palette-grid">
[bbf9d1a]106 <legend><%= t('asite.palette') %></legend>
[7bc636b]107 <% palettes.forEach(function(p) { %>
108 <label class="palette-swatch">
[34076d7]109 <input type="radio" name="palette" value="<%= p.key %>" <%= (site.palette || 'klonkt') === p.key ? 'checked' : '' %>>
[7bc636b]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
[34076d7]126 <%# Live preview: selecting an accent / theme / palette re-themes this page
127 instantly (reverts on reload; persists on save). %>
[0475b13]128 <%# Het script van deze pagina staat in assets/js/mod/admin-site-edit.js (shaer-bqr). %>
[34076d7]129
[7bc636b]130 <fieldset>
[bbf9d1a]131 <legend><%= t('asite.behavior') %></legend>
[7bc636b]132 <label class="cb">
133 <input type="checkbox" name="is_public" value="1" <%= site.is_public ? 'checked' : '' %>>
[bbf9d1a]134 <span><%= t('asite.is_public') %></span>
[7bc636b]135 </label>
136 <label class="cb">
137 <input type="checkbox" name="robots_index" value="1" <%= site.robots_index ? 'checked' : '' %>>
[bbf9d1a]138 <span><%= t('asite.robots_index') %></span>
[7bc636b]139 </label>
[cb01666]140 <% if (typeof audioEnabled === 'undefined' || audioEnabled) { %>
[7bc636b]141 <label class="cb">
142 <input type="checkbox" name="enable_audio_player" value="1" <%= site.enable_audio_player ? 'checked' : '' %>>
[bbf9d1a]143 <span><%= t('asite.enable_audio') %></span>
[7bc636b]144 </label>
[cb01666]145 <% } %>
[acbc9fc]146 <label class="cb">
147 <input type="checkbox" name="approve_followers" value="1" <%= site.approve_followers ? 'checked' : '' %>>
148 <span><%= t('asite.approve_followers') %></span>
149 </label>
[7bc636b]150 </fieldset>
151
[746d98a]152 <%# De profiellinks staan sinds 30-9 bij je profiel, naast je naam, bio en foto:
153 dat is waar ze bij horen, en in de wardmodus is dat profiel het enige dat
154 een bezoeker ziet. %>
155 <p class="form-hint profile-links-moved"><%= t('asite.links_moved') %> <a href="/account#links"><%= t('asite.links_moved_link') %></a></p>
[7bc636b]156
[e9128bc]157 <%# Aliassen en verhuizen staan sinds 14-8 op /admin/migrate, bij elkaar met
158 exporteren, importeren en ophalen (Robin: alle migratie-opties op de
159 migrate-pagina). Ze hoorden hier ook niet thuis: dit scherm gaat over hoe
160 je site eruitziet, en een verhuizing is geen uiterlijk. Alleen de
161 wegwijzer blijft, want wie ze hier zocht moet ze kunnen vinden. %>
[ccaa530]162 <fieldset>
163 <legend><%= t('asite.aliases') %></legend>
[e9128bc]164 <p class="form-hint"><%= t('asite.moved_to_migrate') %></p>
165 <p><a href="/admin/migrate" class="btn"><%= t('admin.b_migrate') %></a></p>
[ccaa530]166 </fieldset>
167
[7bc636b]168 <fieldset>
[bbf9d1a]169 <legend><%= t('asite.feed_view') %></legend>
[e46ebaa]170 <%# Twee vragen, en ze zijn niet hetzelfde: WELKE tweede weergave je site
171 aanbiedt naast Grid, en waarin hij opent. %>
172 <label>
173 <span><%= t('asite.feed_alt') %></span>
174 <select name="feed_alt_view">
175 <% var _a = ['timeline','auto'].indexOf(site.feed_alt_view) >= 0 ? site.feed_alt_view : 'reader'; %>
176 <option value="reader" <%= _a === 'reader' ? 'selected' : '' %>><%= t('asite.feed_alt_reader') %></option>
177 <option value="timeline" <%= _a === 'timeline' ? 'selected' : '' %>><%= t('asite.feed_alt_timeline') %></option>
178 <option value="auto" <%= _a === 'auto' ? 'selected' : '' %>><%= t('asite.feed_alt_auto') %></option>
179 </select>
180 </label>
[7bc636b]181 <label>
[bbf9d1a]182 <span><%= t('asite.feed_default') %></span>
[7bc636b]183 <select name="feed_view_default">
[e46ebaa]184 <option value="reader" <%= site.feed_view_default !== 'grid' ? 'selected' : '' %>><%= t('asite.feed_alt_' + _a) %></option>
[bbf9d1a]185 <option value="grid" <%= site.feed_view_default === 'grid' ? 'selected' : '' %>><%= t('asite.feed_grid') %></option>
[7bc636b]186 </select>
187 </label>
188 <label class="cb">
189 <input type="checkbox" name="feed_view_switch" value="1" <%= site.feed_view_switch ? 'checked' : '' %>>
[bbf9d1a]190 <span><%= t('asite.feed_switch') %></span>
[7bc636b]191 </label>
[520c993]192 <%# Alleen van toepassing op Lezen: geeft elk bericht daar een eigen scherm. %>
193 <label class="cb">
194 <input type="checkbox" name="reader_full_page" value="1" <%= site.reader_full_page ? 'checked' : '' %>>
195 <span><%= t('asite.reader_full_page') %></span>
196 </label>
[7bc636b]197 <label class="cb">
198 <input type="checkbox" name="show_search" value="1" <%= site.show_search ? 'checked' : '' %>>
[bbf9d1a]199 <span><%= t('asite.show_search') %></span>
[7bc636b]200 </label>
201 <label class="cb">
202 <input type="checkbox" name="show_archive_link" value="1" <%= site.show_archive_link ? 'checked' : '' %>>
[bbf9d1a]203 <span><%= t('asite.show_archive') %></span>
[7bc636b]204 </label>
205 </fieldset>
206
[6623453]207 <fieldset class="seo-pointer">
[bbf9d1a]208 <legend><%= t('asite.seo') %></legend>
209 <p><%= t('asite.seo_pointer') %> <a href="/admin/seo"><%= t('asite.seo_link') %></a>.</p>
[7bc636b]210 </fieldset>
211
212 <fieldset>
[bbf9d1a]213 <legend><%= t('asite.custom_legend') %> <em><%= t('asite.optional') %></em></legend>
[7bc636b]214 <label>
[bbf9d1a]215 <span><%= t('asite.custom_css') %></span>
[7bc636b]216 <textarea name="custom_css" rows="3" placeholder=".foo { color: red; }"><%= site.custom_css || '' %></textarea>
217 </label>
218 <label>
[bbf9d1a]219 <span><%= t('asite.custom_head') %></span>
[7bc636b]220 <textarea name="custom_head_html" rows="3" placeholder="<meta name=&quot;...&quot;>"><%= site.custom_head_html || '' %></textarea>
221 </label>
222 <label>
[bbf9d1a]223 <span><%= t('asite.custom_foot') %></span>
[7bc636b]224 <textarea name="custom_foot_html" rows="3"><%= site.custom_foot_html || '' %></textarea>
225 </label>
226 </fieldset>
227
[bbf9d1a]228 <button type="submit" class="btn btn-primary"><%= isNew ? t('asite.submit_create') : t('asite.submit_save') %></button>
[7bc636b]229 </form>
[0ca7e9a4]230
[7bc636b]231</div>
232
233
234
235<style>
236.admin-site-edit-page { max-width: 760px; margin: 3rem auto; padding: 0 1rem; }
237.admin-site-edit-page h1 { font-family: var(--font-display, serif); font-size: 1.8rem; margin: 0 0 1.5rem; }
238
[75c6919]239.site-form { display: flex; flex-direction: column; gap: 1.5rem; min-width: 0; }
[834bcc3]240/* Mobile: a <fieldset> has a browser-default min-inline-size:min-content and
241 therefore does NOT shrink → the entire form became wider than the screen. min-width:0
242 forces it to shrink; form fields get max-width:100% + border-box so they
243 never overflow their fieldset. */
[75c6919]244.site-form fieldset { min-width: 0; }
245.site-form input, .site-form select, .site-form textarea { min-width: 0; max-width: 100%; box-sizing: border-box; }
[7bc636b]246.site-form fieldset {
247 border: 1px solid var(--rule);
248 border-radius: 8px;
249 padding: 1rem 1.25rem;
250 background: var(--paper-2);
251 display: flex;
252 flex-direction: column;
253 gap: 0.75rem;
254}
255.site-form fieldset.palette-grid {
256 display: grid;
257 grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
258 background: var(--paper);
259 margin-top: 0.5rem;
260}
261/* Theme default radios — horizontal, sits between accent + palette */
262.site-form fieldset.theme-default {
263 display: flex; flex-direction: row; flex-wrap: wrap; gap: 1rem;
264 background: var(--paper);
265 margin-top: 0.5rem;
266}
267.site-form fieldset.theme-default label.cb { font-size: 0.9rem; color: var(--ink); }
268.site-form fieldset legend { font-family: var(--font-display, serif); font-size: 1rem; padding: 0 0.4rem; }
269.site-form label { display: flex; flex-direction: column; gap: 0.25rem; font-size: 0.85rem; color: var(--ink-muted, var(--ink-soft)); }
270.site-form label.cb { flex-direction: row; align-items: center; gap: 0.5rem; }
271.site-form input[type="text"], .site-form input[type="color"], .site-form textarea {
272 padding: 0.55rem 0.85rem;
273 border: 1px solid var(--rule);
274 border-radius: 5px;
275 background: var(--paper);
276 color: var(--ink);
277 font-family: inherit;
278 font-size: 0.95rem;
279}
280.site-form input[type="color"] { padding: 0.2rem; height: 38px; }
281.site-form textarea { resize: vertical; min-height: 60px; font-family: var(--font-mono, monospace); font-size: 0.85rem; }
282.site-form input[readonly] { opacity: 0.6; cursor: not-allowed; background: var(--paper-2); }
283
[834bcc3]284/* Slug as URL field: dimmed host prefix + the slug itself in the accent colour. */
[3068cc0]285.slug-url {
286 display: flex; align-items: stretch;
287 border: 1px solid var(--rule); border-radius: 5px;
288 background: var(--paper); overflow: hidden;
289 transition: border-color 120ms;
290}
291.slug-url:focus-within { border-color: var(--accent); }
292.slug-url-host {
293 display: inline-flex; align-items: center;
294 padding-left: 0.85rem;
295 color: var(--ink-muted, var(--ink-soft));
296 white-space: nowrap; user-select: none;
297 font-size: 0.95rem;
298}
299.site-form .slug-url-input {
300 flex: 1 1 auto; min-width: 0;
301 border: none !important; border-radius: 0 !important;
302 background: transparent !important;
303 color: var(--accent) !important; font-weight: 700;
304 padding: 0.55rem 0.85rem 0.55rem 0.1rem;
305}
306.site-form .slug-url-input:focus { outline: none; }
307.site-form .slug-url-input[readonly] {
308 opacity: 1; cursor: default; background: transparent !important;
309}
310
[834bcc3]311/* Selects: same look as the inputs, with a custom chevron instead of the bare
312 native arrow. The open option list is still rendered by the browser. */
[974de34]313.site-form select {
314 appearance: none;
315 -webkit-appearance: none;
316 padding: 0.55rem 2.2rem 0.55rem 0.85rem;
317 border: 1px solid var(--rule);
318 border-radius: 5px;
319 background-color: var(--paper);
320 color: var(--ink);
321 font-family: inherit;
322 font-size: 0.95rem;
323 cursor: pointer;
324 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");
325 background-repeat: no-repeat;
326 background-position: right 0.7rem center;
327 transition: border-color 0.15s ease, box-shadow 0.15s ease;
328}
329.site-form select:hover { border-color: var(--ink-soft, #888); }
330.site-form select:focus,
331.site-form input[type="text"]:focus,
332.site-form textarea:focus {
333 outline: none;
334 border-color: var(--accent, #c2410c);
335 box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent, #c2410c) 22%, transparent);
336}
337
[7bc636b]338/* Reuse the palette-swatch styles from account.ejs */
339.palette-swatch {
340 display: flex; flex-direction: column; gap: 0.4rem;
341 padding: 0.5rem;
342 border: 1px solid var(--rule); border-radius: 6px; background: var(--paper);
343 cursor: pointer; font-size: 0.85rem; transition: border-color 120ms;
344}
345.palette-swatch:hover { border-color: var(--accent); }
346.palette-swatch input[type="radio"] { position: absolute; opacity: 0; pointer-events: none; }
347.palette-swatch:has(input[type="radio"]:checked) { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent); }
348.palette-preview {
349 display: grid; grid-template-columns: 1fr 1fr;
350 width: 100%; height: 44px; border-radius: 4px; overflow: hidden;
351 border: 1px solid var(--rule);
352}
353.pp-half {
354 position: relative;
355 display: flex; align-items: center; justify-content: center;
356 font-family: var(--font-display, serif); font-weight: 600; font-size: 1rem;
357}
358.pp-letter { line-height: 1; }
359.pp-accent {
360 position: absolute; bottom: 4px; right: 4px;
361 width: 9px; height: 9px; border-radius: 50%;
362 border: 1px solid rgba(0,0,0,0.12);
363}
364.palette-swatch-name { font-weight: 500; color: var(--ink); text-align: center; }
365
366/* Accent picker: row of small colored swatches */
367.site-form fieldset.accent-grid {
368 display: grid;
369 grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
370 gap: 0.5rem;
371 background: var(--paper);
372 margin-top: 0.5rem;
373}
374.accent-swatch {
375 display: flex; flex-direction: column; align-items: center; gap: 0.4rem;
376 padding: 0.55rem 0.4rem;
377 border: 1px solid var(--rule); border-radius: 6px; background: var(--paper);
378 cursor: pointer; font-size: 0.78rem;
379 transition: border-color 120ms, box-shadow 120ms;
380}
381.accent-swatch:hover { border-color: var(--accent); }
382.accent-swatch input[type="radio"] { position: absolute; opacity: 0; pointer-events: none; }
383.accent-swatch:has(input[type="radio"]:checked) {
384 border-color: var(--accent);
385 box-shadow: 0 0 0 2px var(--accent);
386}
387.accent-dot {
388 width: 28px; height: 28px; border-radius: 50%;
389 border: 1px solid rgba(0,0,0,0.1);
390 box-shadow: inset 0 -2px 4px rgba(0,0,0,0.12);
391}
392.accent-swatch-name { color: var(--ink); text-align: center; font-weight: 500; }
393
394.audio-flash { padding: 0.75rem 1rem; border-radius: 6px; margin-bottom: 1rem; font-size: 0.9rem; }
395.audio-flash--ok { background: rgba(40, 160, 90, 0.15); color: #2a9d5e; border: 1px solid rgba(40, 160, 90, 0.3); }
396.audio-flash--err { background: rgba(200, 60, 60, 0.15); color: #c33; border: 1px solid rgba(200, 60, 60, 0.3); }
397
398.form-hint { font-size: 0.8rem; color: var(--ink-muted, var(--ink-soft)); margin: 0; }
399.form-hint-inline { font-size: 0.78rem; color: var(--ink-muted, var(--ink-soft)); font-weight: 400; margin-left: .25rem; }
400
401/* P63 — Profile photo picker
402 Mobile-first: thumb stacks above input/buttons. ≥520px the thumb floats
403 to the left and the controls fill the remaining width. */
404.photo-picker {
405 display: flex;
406 flex-direction: column;
407 gap: .65rem;
408 align-items: stretch;
409}
410.photo-thumb {
411 position: relative;
412 width: 88px; height: 88px;
413 border-radius: 50%;
414 border: 1px solid var(--rule);
415 background: var(--paper-2);
416 overflow: hidden;
417 flex-shrink: 0;
418 display: flex; align-items: center; justify-content: center;
419}
420.photo-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
421.photo-thumb .photo-empty {
422 display: flex; color: var(--ink-muted, var(--ink-soft)); opacity: .55;
423}
424.photo-thumb .photo-empty svg { width: 36px; height: 36px; }
425.photo-thumb:not([data-empty]) .photo-empty { display: none; }
426.photo-thumb[data-empty] img { display: none; }
427
428.photo-controls {
429 display: flex;
430 flex-direction: column;
431 gap: .5rem;
432 flex: 1;
433 min-width: 0;
434}
435.photo-controls input[type="text"] { width: 100%; }
436.photo-actions {
437 display: flex; align-items: center; flex-wrap: wrap;
438 gap: .4rem;
439}
440.photo-actions .btn { padding: .45rem .75rem; font-size: .85rem; }
441.photo-status { margin-left: .25rem; }
442.photo-status.is-error { color: #c2410c; }
443
444@media (min-width: 520px) {
445 .photo-picker { flex-direction: row; align-items: flex-start; gap: 1rem; }
446 .photo-controls { gap: .55rem; }
447}
[834bcc3]448/* Mobile-first: platform selector on its own line (full width), URL input + remove
449 button below. Prevents horizontal overflow on phones (fixed 160px column +
450 non-shrinkable input together ~447px > screen width). */
[7bc636b]451</style>
Note: See TracBrowser for help on using the repository browser.