Originally stacked on #8375, now merged. Rebased onto main.
Polishes the onboarding landing page, community-link entry, and recovery
sheets. Profile setup uses a consistent sheet with a larger avatar,
inline display name, and draft photo/emoji/animated editing; iOS uses
native controls and coordinated avatar/sheet transitions.
Validation: mobile analysis, full tests, and normal pre-push checks
passed after rebase. Prior iOS build and device walkthrough passed;
native validation was not repeated after this rebase. Snapshots omitted
as requested.
---------
Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: Tom Brow <tomb@block.xyz>
Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
Co-authored-by: Tom Brow <tomb@block.xyz>
Foreground reconnects rebuild preference managers and repeatedly derive
the same NIP-44 keys on the UI isolate. Retain the six crypto objects in
providers scoped to relay configuration and identity while preserving
manager refresh, subscription, and publication behavior. Community
changes, key rotation, and sign-out invalidate the retained objects.
RelayConfig normalizes the signing key once at its shared accessor, so
identity, theme encryption, event signing, and NIP-42 authentication
receive the same value, including when an active configuration key has
surrounding whitespace. The identity-export authorization snapshot uses
the same normalized key for the active-identity check and the outgoing
recovery payload. The branch incorporates main's relay-scoped preference
managers.
### Validation
- Regression exercises six production crypto providers across
reconnects, configuration/key changes, and sign-out. Reintroducing a
session dependency makes it fail.
- Added a loopback WebSocket regression that exercises actual
RelaySocket NIP-42 authentication with a padded key and verifies its
challenge, relay, pubkey and signature. It fails before shared key
normalization.
- Added a connected-theme regression for whitespace-padded keys,
encryption round-trip, and a valid published event signature. It fails
before the normalization fix.
- Added padded-key identity export coverage through authorization, SAS
confirmation, exactly one normalized payload, and successful completion.
Separate relay-change and key-rotation tests verify both fresh and
expired grants fail closed. The padded-key regression failed before the
fix.
- Current head: all 48 focused pairing/crypto/auth tests and mobile
analysis passed. All [CI
checks](https://github.com/block/buzz/actions/runs/39497098787) passed,
including mobile test shards, Android, native Swift tests and release
builds. Codex feedback is addressed; Jude approved this exact head after
independent regression and mutation checks. Fresh [Codex security
review](https://github.com/block/buzz/pull/8442#issuecomment-6300273942)
found no concrete issues.
- Previous head: 122 config/socket/crypto/theme tests, 44 read-state
tests, and all [GitHub CI
checks](https://github.com/block/buzz/actions/runs/39491550425) passed,
including mobile test shards, Android, native Swift tests and release
builds.
- Original implementation: native iOS Debug build and full local
repository checks passed. A paired simulator background/resume capture
had no samples in the targeted crypto constructors or key derivation; an
82 ms UI frame remained. Debug/JIT, profiler overhead and shared-host
activity prevent an overall speedup claim. The later normalization
change is covered by the production-provider regression above.
- Two image decode timing failures occurred in the initial local full
suite; direct baseline/patch runs, the subsequent full repository run,
and the latest full mobile suite passed.
Cold-start credential validation is unchanged: padded persisted keys are
rejected by AuthNotifier before RelayConfig is constructed. The
normalization regressions cover active configurations and identity
export, not broader persisted-key acceptance.
<!-- buzz-review-completed -->
---------
Signed-off-by: Tom Brow <tomb@block.xyz>
## Summary
- Restores `#` and lock icons for public and private channels on Home,
with matching loading placeholders.
- Updates the dark-mode navigation colors to the desktop design-system
tokens (read and unread text, section headings).
- Replaces the chat color picker with the seven desktop accents (Gray,
Chartreuse, Flame, Magenta, Iris, Cobalt, Jade). Saved legacy colors map
to the closest one.
- Uses white text in your own bubbles, and black where white falls below
4.5:1 (Chartreuse, dark-mode Gray). Links (underlined), quotes, tables
and code use the same text color.
- Insets code blocks in bubbles evenly, with corners concentric to the
bubble and even inner padding.
- The swipe-to-reply indicator uses the chosen chat color.
## Testing
- `flutter analyze` and `dart format` are clean. The full mobile suite
passes (3,705 tests).
- Installed and checked on a Pixel 10 and an iPhone Air against a real
community.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Signed-off-by: kenny lopez <klopez4212@gmail.com>
Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
Co-authored-by: Queue <874bf2d815c2c4b459b90438c669c57f2702f3a76bb6e16fa97184259aac3e9e@buzz.block.builderlab.xyz>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
## Summary
One identity can be paired with several communities, but mobile keyed
several caches by pubkey only. Mobile keeps a single `ProviderScope` and
doesn't remount on a community switch, so one community's data showed up
in another, and some of it was published to the other community's relay.
- **Channel groups**: the second community showed the first community's
groups.
- **Read marks** (contexts, publishable set, NIP-RS client and slot
IDs): the first community's context IDs were merged into, and published
to, the second community's read-state slot.
- **Inbox done/unread rows and thread follows**: one shared 500-entry
cap, so activity in one community evicted another's entries.
- **Channel stars and mutes**: union-merged across communities and
published to every relay.
Every one of these is now keyed by pubkey + normalized community relay,
the same way channel sort already is.
**Switching communities.** The relay session rebuilds for the new
community before the previous community's managers are retired. Each
manager (groups, read state, stars, mutes) now checks that the session
still serves its own community before it publishes, merges a fetch, or
handles a live event. Previously, a pending publish flushed on dispose,
or an in-flight fetch, could cross into the next community.
**Old pubkey-only entries.** Channel IDs can repeat across communities,
so nothing tells us which community an old channel-keyed entry belongs
to:
- **Groups, stars, mutes and read marks** are dropped on upgrade, and
each community restores its own from its relay. Any change that never
reached a relay is lost, and a read mark the relay doesn't hold shows as
unread once. That's an accepted tradeoff: such changes would be very old
by now.
- **Inbox, thread follows, and read-state slot and client IDs** seed
every community, and the old entry is kept. The inbox and follows
entries are event IDs, which only ever match their own community. The
slot ID keeps each community publishing to the slot it already has on
its relay.
This PR also removes the **Pulse** feed (`features/pulse/` and the Nostr
filters only it used). Nothing in the app navigates to it anymore, and
its providers were among the community-unscoped caches.
### Related issue
None found.
### Testing
- Each affected store has a regression test through the real provider
that switches A → B → A. For groups, read marks, stars and mutes, the
test sets a value in A, switches to B while A's change is still pending,
and checks two cases: B's data arriving live and B answering A's
in-flight fetch. It asserts that nothing sent to B contains A's IDs, B
shows only its own value, and A comes back unchanged. Each guard was
removed in turn to confirm a test fails without it, except two that the
current switch order never reaches: the groups merge guard and the
read-state live-event guard.
- Upgrade tests cover each kind of old entry: dropped for groups, stars,
mutes and read marks, seeded for inbox, follows and slot IDs.
- `flutter analyze` is clean, and the full mobile suite passes in the
pre-push hook at `89ebfcc12`.
- Not yet exercised in the app.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
With accessibility semantics active, opening and leaving channels
retained disposed composer overlay states through Flutter’s semantics
traversal map. Those states kept departed route and message trees alive.
This upgrades the pinned Flutter SDK from 3.41.7 to 3.47.7, which
includes [the upstream
fix](3c96406cc4).
Includes SDK compatibility and formatter changes, preserves CocoaPods
integration, and gives create-channel radio lists a Material ink surface
required by the newer framework. Widget regressions verify both the
framework portal and the production channel/composer lifecycle: departed
routes, composer elements, and portal states are collectible while their
application’s semantics owner remains alive.
Related issue: no matching open issue or PR found.
### Validation
- [Current-head CI
passed](https://github.com/block/buzz/actions/runs/39085320351): all
mobile test shards, checks, Android APK, iOS release build, and native
simulator tests.
- Framework regression fails on Flutter 3.41.7 with three retained
states and passes on 3.47.7.
- Signed native Debug iOS Simulator build and paired-community
navigation replay: after 18 channel visits and forced GC, retained
disposed portal keys fell from 18 to 0 and route instances from 19 to 1.
- Review follow-up: all 365 focused channel/overlay tests, analyzer, and
complete mobile pre-push checks passed.
- Full local CI passed before the test-only review follow-up, including
Rust and desktop validation, frontend builds, analyzer checks, 3,499
mobile tests (four skipped), and the separate unconfigured-push test.
The replay used iOS 26.5 Simulator with accessibility semantics enabled,
CPU profiling disabled, and Debug/JIT builds. The before app was a
preinstalled emoji candidate; the after app uses this branch. Object
retention and the focused regression establish the fix; absolute heap
differences are not a controlled release-memory measurement. Android
native execution was not tested.
### Create-channel compatibility
The radio groups retain their layout after switching their container to
Material.
| Before (3.41.7) | After (3.47.7) |
| --- | --- |
| <img
src="https://raw.githubusercontent.com/block/buzz/42525e1bd9d9f264d0142c34ec61dbb6937ef758/pr-8419--01-before.png"
width="280" alt="Original create-channel radio groups"> | <img
src="https://raw.githubusercontent.com/block/buzz/42525e1bd9d9f264d0142c34ec61dbb6937ef758/pr-8419--02-after.png"
width="280" alt="Updated create-channel radio groups"> |
---------
Signed-off-by: Tom Brow <tomb@block.xyz>
## Summary
Polish mobile Home and Solo: group Search/Inbox beside a separate
profile avatar, move Inbox and Settings to bottom-up pages with Close
controls, and place tabs opposite the plus/compose action. Morph plus to
compose clockwise when entering Solo and reverse counterclockwise when
returning Home, with a directional quarter-turn of the icon inside a
level button and a subtle scale pulse, respecting reduced motion. Align
Search’s Close control with Inbox, simplify its placeholder, and
initialize selected filter contrast correctly. Collapse the plus menu
when leaving Home. Center Solo’s native-glass New chat empty state and
keep it stationary behind the keyboard.
Standardize Tabler icons and neutral backgrounds, retain SF Back/Close
on iOS, use native Liquid Glass headers, tab container, and actions
(solid grey selected tab, slightly darker on iOS) on iOS and black
actions on Android, and smooth the expanded menu corners. Repair native
header safe-area/return layout and compose movement after cancellation.
### Related issue
None found for this UI refinement; follows #8337.
### Testing
- Formatting, analysis, full mobile suite: 3,487 passed, 4 skipped; 3
push-configuration tests passed. File-size checks passed.
- Base UI built and installed on Pixel and iPhone in place. Pixel
navigation/New chat cancellation checked. Subsequent glass and rotation
refinements are previewed on iPhone; motion feel awaits human
confirmation.
- Draft pending human confirmation of the final Settings entrance/return
behavior on iPhone.
### Pixel snapshots
| Solo | Expanded actions |
| --- | --- |
| <img
src="https://raw.githubusercontent.com/block/buzz/16dc0f8af971e63d788fee20974997fab925dec9/docs/screenshots/mobile-home-solo/pixel-solo.png"
width="280" alt="Solo empty state with New chat" /> | <img
src="https://raw.githubusercontent.com/block/buzz/16dc0f8af971e63d788fee20974997fab925dec9/docs/screenshots/mobile-home-solo/pixel-actions.png"
width="280" alt="Expanded channel actions with smooth corners" /> |
---------
Signed-off-by: kenny lopez <klopez4212@gmail.com>
Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
Aligns mobile with desktop's neutral palette and six chat colors.
Appearance now offers Light, Dark, and System instead of themes, while
preserving community preferences and the single Home and Chat preview
pair.
Chat colors survive local preference reloads without historical-index
remapping, and the Chat preview shows the draft outgoing bubble color.
Channel and section icons are removed, section chevrons sit beside
names, and section dividers use 7% opacity. DM avatars remain.
Validation: `just mobile-check`, 35 focused tests, and the full mobile
suite passed (3,351 tests; 4 skipped). Updated light/dark previews were
rendered and visually checked. The earlier release was verified on
iPhone; this follow-up has not been reinstalled. The earlier
repository-wide lint passed, but full `just ci` was stopped for low disk
space. Hosted CI is running for the new head.
**Intentional design parity:** Section labels and chevrons retain
desktop’s `--text-unavailable` colors: light `#959595`, dark `#595959`.
Verified against `block/buzz-app` main at
`0201b54c496120a44e020834663dc534cae9a79f`. These colors do not meet
4.5:1 text contrast on the mobile section surfaces; the product decision
for this PR is to preserve desktop parity and leave them unchanged.
Kenneth explicitly accepts this known contrast tradeoff for this
PR/release and requests that reviewers treat it as a scoped product
exception rather than a blocker. This does not claim WCAG compliance or
change the repository-wide accessibility target. Any future contrast
adjustment should be coordinated across desktop and mobile.
### Snapshots
Channel list: physical iPhone. Appearance and chat colors:
Flutter-rendered previews.
| Channels | Light | Dark | Chat colors |
| --- | --- | --- | --- |
|

|

|

|

|
---------
Signed-off-by: kenny lopez <klopez4212@gmail.com>
## Summary
Preview buzz-app's Tabler icon style in the mobile app, replacing
app-drawn Lucide glyphs across channels, messages, activity, search, and
settings. A shared `BuzzIcons` mapping uses the bundled Tabler 3.46.0
outline font, matching buzz-app's version, with its MIT license and
source documented.
Native UIKit controls retain SF Symbols. The former 300/500 navigation
glyphs use Tabler's standard outline; existing selection colors and
labels remain.
### Related issue
No matching Tabler mobile migration issue or PR found. Related native
iOS controls: #8055.
### Testing
- Pre-push mobile analysis, formatting, file-size checks, and the full
mobile test suite passed.
- 194 focused widget tests passed, including channels, home, search,
settings, and message content.
- Light and dark community-settings renders inspected.
- Signed iPhone Release build succeeded; installed and launched as Buzz
Tabler on a physical iPhone.
- Full `just ci` attempted; stopped during Rust clippy compilation
because the Mac ran low on disk space. Full repository validation is not
claimed.
Signed-off-by: kenny lopez <klopez4212@gmail.com>
## Summary
Use rounded, grouped rows consistently across mobile sheets and menus,
including Members and list pickers. Keep sheet content above the system
gesture area with 4dp of extra bottom padding on Android and iOS.
### Related issue
No duplicate found; related member-picker work: #4051.
### Testing
- Mobile analysis, formatting, file-size checks, and full mobile test
suite pass.
- Android build installed and launched on Pixel 10.
- Before/after Members previews use sample data (attached below).
- Full `just ci` stopped during Rust compilation because local disk
space ran out.
Signed-off-by: kenny lopez <klopez4212@gmail.com>
## Summary
Inline code on mobile renders as **bold body text on a faint background
wash** — no monospace face, no chip, and it cannot wrap. #5257 diagnosed
this as a missing `highlightBuilder`.
That is no longer the right fix. `gpt_markdown` 1.2.0 deprecates
`highlightBuilder` (removal in 2.0.0), renders inline code as a real
chip, and adds `InlineCodeStyle` for restyling it. The package author
confirmed this on the issue. So this PR is an upgrade — 1.1.6 → 1.2.1 —
plus one theme declaration, rather than the builder the issue originally
asked for.
**Where the style is declared.** `GptMarkdownThemeData` goes in
`AppTheme._buildTheme`, which both `light()` and `dark()` call. That
reaches all four `GptMarkdown` call sites — `message_content`,
`transcript_item_widget`, `token_pill`, `custom_emoji_render` — so the
style is stated once instead of per widget. A widget-level
`inlineCodeStyle` would have covered channel messages only, leaving the
other three on the package's defaults.
**What is declared.** Face, size, ink, chip fill and outline — not the
face alone. A face name on its own leaves the rest on the package's
defaults, which put inline code at 14.1sp beside a fenced block's 13, on
a neutral `onSurface` tint rather than the app's code surface. In dark
that tint is *lighter* than the surface, while every other code surface
in the app is recessed, so the chip read as a different kind of object.
All of it now comes from one `CodeStyle` declaration that the fenced
block reads from too, so the two cannot be edited apart.
**Three adaptations the upgrade requires.** Each was found by running
the gate, not by reading the changelog:
1. **`imageBuilder` widened** to `(context, url, width, height)`. This
is a hard compile error, and it is **not listed in the package's
migration guide**, which states "nothing here stops code compiling".
Worth reporting upstream.
2. **`autolink` now defaults to `true`.** `normalizeBareLinks()` already
rewrites bare URLs into Markdown links before rendering, so both would
run. `message_content` opts out with `autolink: false` to keep current
behaviour exactly. The migration guide argues for dropping the
pre-processor instead — a better fix, but a behavioural change that
belongs in its own PR.
3. **`gpt_markdown.dart` now re-exports `markdown_config.dart`**, making
two direct imports redundant. `flutter analyze` reports `No issues
found!` on 1.1.6 and flags both on 1.2.1, so these warnings are new, not
pre-existing.
**Deliberately out of scope.** The three non-message call sites now
autolink bare URLs, since only `message_content` has a pre-processor to
collide with. Custom inline components (`_MentionMd`, `CustomEmojiMd`,
`_ChannelLinkMd`) could additionally declare `allScopesExceptLinkLabel`
— 1.2.0 offers it as the fix for a `WidgetSpan` chip going blank inside
a link label on iOS — but current behaviour is unchanged without it, so
that stays a separate change.
### Related issue
Fixes#5257
Duplicate scan: searched `gpt_markdown`, `inline code mobile`,
`highlightBuilder` and `InlineCodeStyle` across both PRs and issues. No
open PR touches inline code styling. #6135 (link labels) and #6166 (text
selection) also touch mobile Markdown but address different defects.
### Testing
Full gate, `just ci` — exit 0:
| Stage | Result |
|---|---|
| Rust (33 suites) | 4768 passed, 0 failed |
| Desktop | 5799 passed, 0 failed |
| Mobile | **2011 passed**, 0 failed |
| `flutter analyze` | `No issues found!` |
| Desktop + web build | ok |
Run on the branch with `main` merged in, so these numbers match what CI
builds.
**New regression test** — `renders inline code in the app code style`.
It resolves the `CodeTextSpan` the package tags inline code with, which
carries both the resolved `TextStyle` and the colours the chip behind it
is painted with, so face, size, ink, fill and outline are all asserted
rather than a widget's presence. It is negative-controlled: reverting
only the theme declaration fails it with
```text
Expected: a numeric value within <0.001> of <13.0>
Actual: <14.1>
```
and dropping the declaration entirely falls back to
`packages/gpt_markdown/JetBrainsMono` — so the test measures the real
thing, and it would catch a future regression that silently drops the
theme extension.
The test passes `baseStyle: messageBodyTextStyle`, the style the message
surfaces actually use; the widget's own fallback is the smaller
`bodyMedium`, which would move the expected size.
The test finds paragraphs with `find.byWidgetPredicate((widget) =>
widget is RichText)`, not `find.byType(RichText)`: inline code renders
through `BidiRichText`, a `RichText` subclass, and `byType` matches
exact runtime types.
That is a hazard for any test that reads text back out of a paragraph,
and one landed after this branch was cut:
`message_content_custom_emoji_test.dart` arrived with #6996 and its
`code keeps literal emoji while adjacent known tokens render` case reads
a code span through `find.byType(RichText)`. It passes on `main` and
fails on the merge result, which is what CI builds, so it went red only
once CI was authorized. It now uses the same predicate. The two other
`byType(RichText)` call sites — the rest of that file and
`message_author_meta_test.dart` — were re-run and pass: their content
carries no code span, so the exact type still matches. They were left
alone.
### Screenshots
Rendered through the real `MessageContent` widget with the app's own
fonts loaded, at 390pt wide, 3x DPR. Sample text: ``Set `BUZZ_RELAY_URL`
before launch, then run `just mobile-test` to verify.``
| | Before (1.1.6) | After (1.2.1) |
|---|---|---|
| Light |

|

|
| Dark |

|

|
Before: bold Inter on a flat wash, no chip edge, and `just mobile-test`
breaks across the line with the wash simply ending. After: Geist Mono in
a bordered, rounded chip, and the wrapped fragment gets its own chip on
each line.
---------
Signed-off-by: Tolga Cinisli <tolgacinisli@gmail.com>
Co-authored-by: Tolga Cinisli <tolgacinisli@gmail.com>
**Category:** new-feature
**User Impact:** Mobile now keeps each community’s appearance in sync
with desktop, including theme, accent, and system-mode preference.
**Problem:** Appearance choices were device-local, so the same account
could look different between desktop and mobile. Live sync could also
stop after the relay closed a subscription.
**Solution:** Store each community’s encrypted appearance preference on
its relay using the shared desktop wire contract, restore it from a
local identity-scoped cache, and apply replacement events live. Closed
subscriptions now recover with guarded backoff and fetch the latest
preference so no update is lost during the gap.
<details>
<summary>File changes</summary>
**mobile/lib/app.dart**
Connects community appearance state to the authenticated app lifecycle.
**mobile/lib/features/settings/accent_picker_page.dart**
Aligns mobile accent choices and selection behavior with the shared
catalog.
**mobile/lib/features/settings/settings_page/appearance_section.dart**
Clarifies the active appearance and hides accent controls when the Buzz
theme owns its neutral accent.
**mobile/lib/features/settings/theme_picker_page.dart**
Persists catalog theme choices through the community-scoped provider.
**mobile/lib/shared/theme/accent_colors.dart**
Matches desktop’s accent catalog and wire values.
**mobile/lib/shared/theme/buzz_theme.dart**
Keeps Buzz visually neutral without discarding the user’s stored accent
for other themes.
**mobile/lib/shared/theme/community_theme_preference.dart**
Defines and validates the versioned desktop-compatible appearance
payload.
**mobile/lib/shared/theme/community_theme_provider.dart**
Coordinates cache-first appearance loading with account and community
changes.
**mobile/lib/shared/theme/community_theme_sync.dart**
Adds encrypted NIP-78 relay persistence, live replacement handling,
deterministic ordering, safe seeding, and resilient subscription
recovery.
**mobile/lib/shared/theme/theme.dart**
Exports the community appearance modules.
**mobile/test/features/settings/theme_picker_page_test.dart**
Covers the updated settings behavior.
**mobile/test/shared/crypto/nip44_interop_test.dart**
Proves Dart decrypts a desktop-produced nostr-rs NIP-44 v2 preference.
**mobile/test/shared/theme/buzz_theme_test.dart**
Covers Buzz’s neutral rendering and stored-accent restoration.
**mobile/test/shared/theme/community_theme_preference_test.dart**
Covers wire parsing, validation, migration, and future-version handling.
**mobile/test/shared/theme/community_theme_sync_test.dart**
Covers cache/relay lifecycle, replacement ordering, switching races,
absence-only seeding, and closed-subscription recovery.
</details>
## Reproduction steps
1. Sign into desktop and mobile with the same account and join the same
community relay.
2. On desktop, choose a distinctive non-Buzz theme and accent; mobile
should update without a local toggle.
3. Restart mobile and confirm it restores the same appearance.
4. Change the mobile theme and accent and confirm desktop follows.
5. Leave mobile idle or backgrounded through a relay reconnect, then
change desktop again; mobile should resubscribe and catch up
automatically.
6. Switch communities and confirm each community restores only its own
appearance.
---------
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz>
## Summary
- replace mobile connecting and reconnecting banners with element-shaped
skeletons for channel lists and message timelines
- add a low-contrast two-second shimmer and same-slot reveal, with
reduced-motion support
- align top, section, loaded-row, and skeleton label columns
## Why
Connection banners shifted content and did not match the desktop loading
treatment. The skeletons preserve layout and make reconnects less
disruptive.
## Testing
- `just mobile-check`
- `just mobile-test` — 704 passed, 1 skipped
- Pixel 10 visual verification in loaded and reconnecting states
---------
Signed-off-by: kenny lopez <klopez4212@gmail.com>
## Summary
- reorganize mobile settings around profile, appearance, and connection
cards
- add System/Light/Dark theme pairing, accent selection, and the Buzz
gradient theme
- align avatar badges, status editing, and supporting mobile chrome
## Test plan
- `just mobile-check`
- `just mobile-test`
## Summary
- Refine mobile navigation with icon-only tabs, haptics, a solid active
state, and spring quick actions.
- Bring Create channel and New message closer to desktop with radio
settings, keyboard submission, relay people, and wrapped recipient
chips.
- Keep both sheets draggable below the status area and prevent keyboard
overflow with many recipients.
## Testing
- `just mobile-check`
- `just mobile-test`
- Pixel 10 manual verification