diff --git a/docs/ui-direction.md b/docs/ui-direction.md
new file mode 100644
index 0000000..4a539af
--- /dev/null
+++ b/docs/ui-direction.md
@@ -0,0 +1,223 @@
+# Resident UI direction
+
+Research snapshot: 2026-09-06. The reference study and remaining slices are design
+proposals, not a completed accessibility audit. See the implementation record below.
+
+## Recommended references
+
+Use Rally for legibility and physical feedback, bsky38 for strong row hierarchy,
+plyr.fm for component discipline, and Doodl for personal visual identity. Use
+Typeahead/pub-search stats for compact information grouping. Keep ZDS's
+forest/paper palette and familiar account navigation. These references suggest
+patterns to adapt; they do not require importing their frameworks or themes.
+
+| Project | Evidence inspected | Worth adapting | Limits |
+| --- | --- | --- | --- |
+| [Rally](https://rally.waow.tech) | Live sign-in page and keyboard entry; `rally/src/style.css`, `src/ui/handle-autocomplete.ts` | Clear hierarchy, 18px body type, Atkinson Hyperlegible font, generous controls, visible focus, help disclosed beside its field, slight button depression | Ruled notebook background and large hard shadows are too visually active for every account section. Font choice alone does not establish accessibility. |
+| [plyr.fm](https://plyr.fm) | Live track list and search overlay; `frontend/src/lib/components/BottomSheet.svelte`, `ConfirmDialog.svelte`, `SearchModal.svelte`; design-token documentation | Recognizable item identity, separate primary and secondary information, shared surface/type/spacing tokens, modal lifecycle and short transitions | Some secondary text is visually subdued. Search focuses its input on opening; this inspection did not establish correct focus return. Do not copy its custom overlays wholesale. |
+| [Doodl](https://doodl.waow.tech) | Live drawing toolbar; `doodl/src/ui/slots.ts`, `src/style.css`, `index.html` | Small explicit slot registry, meaningful fallback labels, bounded artwork, approachable controls | Its viewport explicitly disables zoom. ZDS must preserve zoom. ZDS should retain visible action labels alongside unfamiliar drawings, and keep its individual-drawing configuration. |
+| [Haunt](https://haunt.at) | Live home page; local `haunt/src/styles/globals.css` | A distinctive identity mark, generous breathing room, limited atmospheric color | Subdued small text and ambient animation are unsuitable models for account tasks. Local checkout is among the user's projects; the live site credits aly.codes. Useful as an atmosphere reference, not an account-layout reference. |
+| [Pensieve](https://pensieve.waow.tech) | Live sign-in page; `pensieve/src/public/index.html`, `style.css` | One clear next step; explicit empty/blocked/ready states in source; labeled progress and status | Signed-in progress was inspected in source, not exercised live. Sparse sign-in UI does not demonstrate how well a dense account page works. |
+
+Source paths above are relative to each sibling repository under
+`~/tangled.org/zzstoatzz.io/`. The additional references below were reviewed
+following the initial shortlist.
+
+Rally's concrete details are especially useful: `style.css:91` defines a 4px
+focus outline, `:281` begins its large button treatment, and `:293` supplies
+pressed feedback. Its skip link was the first keyboard stop in the live page.
+Plyr's native confirmation dialog uses `showModal()`; its bottom sheet contains
+explicit focus handling and a reduced-motion override. These are implementation
+references to review independently, not proof of conformance.
+
+### Additional references: bsky38 and the stats pages
+
+- **[The race to 38](https://bsky38.waow.tech/):** local checkout is
+ `~/tangled.org/zzstoatzz.io/race38`; its live source link names
+ `tangled.org/waow.tech/bsky38`. This is Nate's companion viewer, distinct from
+ the upstream voting site at bsky38.com, which the initial review mistakenly
+ substituted. The correct live UI combines paper/ink surfaces, crisp framing,
+ blue actions, serif headings, and readable sans-serif controls. The chart and
+ replay controls occupy the primary space; race details and selection editing
+ use disclosure. Portraits connect visual identity to the data.
+ Source reviewed: `race38/web/style.css`, `docs/design-origin.md`, and
+ `docs/ui-rigor.md`. The strongest transferable ideas are stable DOM identity,
+ explicit interaction contracts, bounded visual emphasis, native controls,
+ and feedback tied to accepted events. CSS includes 44px control targets,
+ focus styling, pressed-state selectors, and reduced-motion overrides. These
+ are implementation evidence, not a completed accessibility audit. For ZDS,
+ borrow the clear surface separation, intentional artwork placement, and
+ continuity through updates. Account actions need prompt, accurate confirmation;
+ do not copy the race's delayed animated count arrival into security feedback.
+- **[Typeahead stats](https://typeahead.waow.tech/stats):** summary metrics are
+ grouped near their relevant chart, range controls sit beside the content they
+ affect, and the traffic chart is supplemented by named totals and a table.
+ Selecting 24h updated the search count, latency, and chart granularity in place.
+ Source: `typeahead/src/pages/stats.ts` and `theme.ts`. Particularly useful:
+ number/unit pairs wrap together, and home/docs/stats share theme tokens. For
+ ZDS, keep summaries short and technical detail close to the task. Do not copy
+ the very small captions. The inspected 24h control was 34px high and exposed
+ its selection through a CSS class without `aria-pressed`; preserve explicit
+ programmatic selection state when adapting this interaction.
+- **[pub-search stats](https://pub-search.waow.tech/stats) and
+ [search](https://pub-search.waow.tech/):** the stats page achieves density with
+ aligned rows, whitespace, a compact metric strip, and section-local controls
+ instead of boxing every fact. The search page makes its primary task immediate.
+ Source: `pub-search/site/stats.html`, `dashboard.css`, and `components.css`.
+ ZDS can use this economy for metadata inside expanded app details. The stats
+ CSS has 9–12px labels and very subdued text/border tokens; these need stronger
+ treatment for a friendly account UI. Chart inspection was visual, not a full
+ keyboard or screen-reader test.
+
+This strengthens the recommendation for fewer, better-structured rows rather
+than a separate decorative card for every piece of metadata. Doodl artwork can
+occupy the row's identity position while names, state, and controls remain clear.
+
+### Anti-slop review
+
+Adopt an explicit design critique before implementation and again on rendered
+output: every visual choice should support this account task, hierarchy should
+survive without decoration, and repeated panels/chips/animations should earn
+their space. Evaluate real content, awkward lengths, failure states, and generic
+as well as custom artwork. Accessibility and predictable behavior take priority
+over stylistic prohibitions or novelty.
+
+The exact named anti-slop skill is pending identification. No matching installed
+skill was found in the local skill locations searched, and web discovery found
+several unrelated implementations. Do not claim one is installed or applied
+until its source and linked guidance have been reviewed. The recommendation is
+to use it as a critique discipline alongside the existing frontend-design skill,
+not to introduce another runtime design system or replace the accessibility
+acceptance baseline.
+
+## What ZDS should feel like
+
+A quiet, well-made account utility with personal illustrations. Clear headings
+and readable content carry the page; artwork helps people recognize places.
+Surfaces should have visibly different roles: page background, section surface,
+editable field, and active control. Borders and spacing should remain useful
+without shadows or color perception.
+
+Use a small type scale: comfortable body and field text, stronger section
+headings, and secondary copy that remains readable. Keep monospace for protocol
+identifiers. Try Atkinson Hyperlegible as a comparison against the system sans
+before accepting an additional font asset; its name is not a substitute for
+testing. Avoid making every sentence the same weight or every section a stack
+of nested boxes.
+
+“Silky” should first mean continuity: stable layout while artwork loads, retained
+form input after errors, immediate pending feedback, no duplicate submissions,
+and predictable focus when changing views. Add restrained press/surface feedback
+after that. A proposed motion budget is 120–180ms for small feedback and at most
+200ms for an overlay, with no ornamental looping motion in account views. Under
+reduced motion, remove spatial movement and keep clear static state changes.
+
+## Slottable presentation contract
+
+Keep the registry and rendering under `src/internal/web`. Each slot describes a
+visual purpose and fallback; it does not carry authentication, authorization,
+navigation, or storage behavior.
+
+- ZDS owns the semantic element, visible label, accessible name, keyboard
+ behavior, focus ring, state indicator, and target dimensions.
+- Artwork occupies a reserved box with `object-fit: contain`. A large drawing
+ does not enlarge or shift the form; a small drawing does not shrink the target.
+- Decorative images use empty alt text. If future artwork conveys content,
+ explicitly define a separate content slot with an appropriate text equivalent.
+- An operator can select individual drawings. No iconset membership is required.
+ Unconfigured deployments retain generic defaults.
+- Missing, broken, unusually proportioned, mostly white, mostly black, and
+ transparent artwork must all leave the interface usable. Do not use a blanket
+ white tile to rescue every asset. Preserve the fallback until a replacement
+ loads, and evaluate artwork against both themes.
+- New slots can cover section illustrations, empty-state artwork, or header
+ marks. Introduce them where they aid recognition; do not make arbitrary HTML,
+ CSS, scripts, or control geometry remotely replaceable.
+- Keep the current explicit build-time artwork bundling. No remote resolution
+ on startup or on account requests, and no drawing dependency in auth/storage.
+
+## Accessibility acceptance baseline
+
+Target WCAG 2.2 AA across complete resident flows, with a few deliberate comfort
+targets beyond the minimum. The [W3C's WCAG 2.2 overview](https://www.w3.org/WAI/standards-guidelines/wcag/new-in-22/)
+specifically covers unobscured focus, target spacing, accessible authentication,
+and alternatives to dragging. Its AA target-size minimum is 24 CSS pixels with
+exceptions; use at least 44px targets for ZDS standalone controls as a stronger
+product target. Focus Appearance is AAA, not an AA requirement; nevertheless
+use a conspicuous, contrasting focus indicator.
+
+Use the full [WCAG 2.2 standard](https://www.w3.org/TR/WCAG22/) as the audit basis:
+
+- Keyboard-only completion, logical focus order, a skip link, current-page
+ indication, meaningful headings, and focus that is not hidden by sticky UI.
+- Text contrast of at least 4.5:1 for normal text and 3:1 for qualifying large
+ text; sufficient non-text contrast for required control/state information.
+ Never communicate active, revoked, error, or danger by color alone.
+- Text resize to 200%, reflow at 320 CSS pixels, browser zoom, and text-spacing
+ overrides without losing information or controls. Long DIDs/scopes must wrap.
+- Labels and instructions connected to inputs; specific errors near the field;
+ preserve entered values and explain recovery. Allow password managers and
+ paste. Avoid repeated entry when the value is already available.
+- Announce concise results and pending state, not an entire refreshed account
+ page. Keep critical feedback present long enough to find and act on.
+- Test light/dark, forced colors, reduced motion, keyboard, touch, and a screen
+ reader. Automated checks supplement manual flow tests; they do not certify it.
+
+For confirmations, prefer native dialog semantics and follow the
+[WAI modal-dialog interaction guidance](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/):
+intentional initial focus, contained keyboard navigation, an accessible name,
+and sensible focus restoration. Use dialogs only when the decision warrants an
+interruption; ordinary help and technical details fit native disclosure better.
+
+## Successive implementation slices
+
+1. **Shared foundation.** Consolidate presentation tokens and control states
+ inside the web layer. Improve section hierarchy, target sizing, readable
+ secondary text, and focus styling across existing pages. Compare a real
+ account fixture with generic and instance artwork in both themes.
+2. **Navigation and feedback.** Give view changes appropriate page titles and
+ focus behavior. Narrow live announcements. Add local pending/error/success
+ handling without losing input or rebuilding focused controls unnecessarily.
+ Current `account.html` marks the whole resident section live, and `showView`
+ changes classes/current navigation without updating focus or title.
+3. **Account actions.** Polish field groups and credential rows. Replace ad hoc
+ prompts/confirmations only where a well-tested inline form or native dialog
+ improves the task. Test cancellation, failures, retries, and duplicate clicks.
+4. **Additional artwork slots.** Add a few useful section/empty-state slots after
+ the surrounding layout and semantic contract are stable. Expand fixtures for
+ asset failure and unusual drawings before broadening configurability.
+
+Keep each runtime slice separately reviewable and deployable. Use browser
+request/response evidence for reported failures, existing required tests/smokes,
+and a small consistent manual flow matrix. Before/after startup benchmarks and
+asset-size checks should show whether presentation work adds cost; deploy once
+per verified slice, avoiding restarts for research documents alone. This review
+does not add account-management surfaces or alter the planned lifetime work.
+
+The original research changed no runtime files or sibling projects. Live inspection covered public desktop surfaces and
+limited keyboard interactions, not a full mobile or assistive-technology audit.
+
+## Implementation: first resident UI slice (2026-09-07)
+
+The first release implements distinct account-section surfaces, a consolidated
+identity summary, a larger type hierarchy, 44px controls, 48px fields, and mobile
+navigation that wraps into two columns. Existing drawing slots retain their
+labels and reserved sizes. Layout and palette changes stay in the web assets;
+there are no additional fonts, packages, remote requests, or account surfaces.
+
+View navigation now updates the document title and focuses its heading. Modified
+link clicks retain browser behavior. The whole resident region is no longer a
+live announcement; the existing status element is an atomic polite status region.
+Hover/press feedback respects pointer and motion preferences. Session lifetimes
+and endpoint behavior are unchanged. Remaining action feedback/dialog work is
+still a subsequent slice.
+
+Validation: existing test suite, endpoint smoke, permissioned smoke, diff check,
+and Zig Zen completed successfully. Browser checks covered sign-in, reload
+persistence, active navigation/focus, custom artwork, desktop, 390px and 320px
+reflow, and the light palette through a local preview proxy. This is not a full
+assistive-technology audit. Startup probe: five warm-cache samples per scenario,
+ReleaseSafe, 10,000 history entries. Before/after median readiness was 16.19/20.96ms
+fresh, 9.02/8.79ms restart, and 9.68/9.57ms with history. Fresh database creation
+varied; restart results show no observed regression. These exclude VM boot and
+proxy routing.
diff --git a/src/internal/web/account.html b/src/internal/web/account.html
index 38393a2..23614f5 100644
--- a/src/internal/web/account.html
+++ b/src/internal/web/account.html
@@ -7,24 +7,24 @@
@@ -99,26 +107,27 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-->
+Skip to account contentzds
-
account
-
The resident control center for an account hosted on this PDS.
+
Your account
+
Manage your sign-in methods and the apps connected to your account.
-
-
+
+
-
signed in as-
-
status-
-
email-
-
spaces0
+
Signed in as-
+
Status-
+
Email-
+
Spaces0
-
home
+
Overview
This PDS stores your public repo, private permissioned-space records, blobs, credentials, and app access state. Use the sections above to manage what this server can control.
View private records in permissioned spaces for this account.
-
record set
-
selected record
Choose a record.
+
Record set
+
Selected record
Choose a record.
-
account
-
Passkeys
Use a passkey to sign in without typing this account password.
-
App passwords
Use an app password for clients that should not receive your main account password.
Most clients should leave this off.
-
Email
Send yourself a verification email, then enter the code from it.
Change email
Request an update code at your current address, then enter the new address and the code.
-
Account status
Deactivation hides the account until it is reactivated. Takedowns and suspensions are operator actions, not resident controls.
+
Sign-in & account
+
Passkeys
Use a passkey to sign in without typing this account password.
+
App passwords
Use an app password for clients that should not receive your main account password.
Most clients should leave this off.
+
Email
Send yourself a verification email, then enter the code from it.
Change email
Request an update code at your current address, then enter the new address and the code.
+
Account status
Deactivation hides the account until it is reactivated. Takedowns and suspensions are operator actions, not resident controls.
-
about
+
About this PDS
ZDS is the PDS hosting this account. It stores repo data, blobs, account credentials, OAuth token state, and experimental permissioned-space data for residents on this server.
This page is local to the PDS. It is not a Bluesky app view, and it only manages things this PDS is responsible for.
@@ -173,9 +182,21 @@ async function fail(r,msg){if(r.ok){const t=await r.text();return t?JSON.parse(t
const authed=(url,opts={})=>fetch(url,{...opts,headers:{...(opts.headers||{}),authorization:`Bearer ${accessJwt}`}});
async function xrpc(path,params={},options={}){const qs=new URLSearchParams(params);return fail(await authed(path+(qs.size?'?'+qs:''),options),'request failed')}
function viewName(){const p=location.pathname; if(p.endsWith('/security'))return'manage'; if(p.endsWith('/sessions')||p.endsWith('/apps'))return'sessions'; if(p.endsWith('/spaces'))return'spaces'; if(p.endsWith('/manage'))return'manage'; if(p.endsWith('/about'))return'about'; return'home'}
-function showView(name=viewName()){for(const el of document.querySelectorAll('.view'))el.classList.toggle('on',el.id==='view-'+name);for(const a of document.querySelectorAll('.nav a'))a.setAttribute('aria-current',a.dataset.view===name?'page':'false')}
-document.addEventListener('click',e=>{const a=e.target.closest('a[data-view]');if(!a)return;e.preventDefault();history.pushState(null,'',a.href);showView(a.dataset.view)});
-addEventListener('popstate',()=>showView());
+function showView(name=viewName(),moveFocus=false){
+ for(const el of document.querySelectorAll('.view'))el.classList.toggle('on',el.id==='view-'+name);
+ for(const a of document.querySelectorAll('.nav a')){if(a.dataset.view===name)a.setAttribute('aria-current','page');else a.removeAttribute('aria-current')}
+ const heading=$('view-'+name).querySelector('h2');
+ document.title=heading.textContent+' · ZDS';
+ heading.tabIndex=-1;
+ if(moveFocus)heading.focus();
+}
+document.addEventListener('click',e=>{
+ if(!(e.target instanceof Element)||e.button!==0||e.metaKey||e.ctrlKey||e.shiftKey||e.altKey)return;
+ const a=e.target.closest('a[data-view]');if(!a)return;
+ e.preventDefault();if(location.pathname!==a.pathname)history.pushState(null,'',a.href);
+ showView(a.dataset.view,true);
+});
+addEventListener('popstate',()=>showView(viewName(),resident.classList.contains('on')));
function updateHeader(){ $('account-handle').textContent=account.handle; $('account-did').textContent=account.did; $('account-status').textContent=account.status||'active'; $('account-email-state').textContent=account.emailConfirmed?'verified':'needs verification'; $('email-copy').textContent=`Current email: ${account.email}${account.emailConfirmed?' (verified)':' (not verified)'}`; $('verify-email-block').classList.toggle('hidden',!!account.emailConfirmed); }
function quickChecks(sessions){const rows=[['email',account.emailConfirmed?'verified':'needs verification',account.emailConfirmed?'active':'inactive'],['app access',`${(sessions.oauthGrants||[]).filter(g=>g.active).length} active`, 'active'],['direct API tokens',`${(sessions.sessions||[]).filter(s=>s.active).length} active`, 'active'],['permissioned spaces',`${spaces.length}`, spaces.length?'active':'inactive']];$('home-checks').innerHTML=rows.map(([a,b,c])=>`