diff --git a/README.md b/README.md
index fb40d32..1fbfcd5 100644
--- a/README.md
+++ b/README.md
@@ -189,7 +189,7 @@ entry to `~/.ssh/config` and keys at `~/.ssh/id_ed25519_frame` and
This is a first public test, so reports are really useful, especially from
Windows and Linux. The quickest way is **Report a problem** in the app (the
-warning-sign button at the top, or **Help → Report a Problem…**). It adds
+speech-bubble button at the top, or **Help → Report a Problem…**). It adds
diagnostics with personal details removed, shows you exactly what's included,
and sends it privately to the maintainer; nothing is published. Without the app,
use the [feedback form](https://frame-control.pages.dev/feedback/). Please include:
diff --git a/docs/devices.md b/docs/devices.md
index 1ed3200..847d8b7 100644
--- a/docs/devices.md
+++ b/docs/devices.md
@@ -3,8 +3,9 @@
Frame Control can manage more than one Steam Frame, and each headset can be
reached at more than one address: a LAN IP at home, another at the office, its
mDNS name (`frame.local`), its Tailscale IP or MagicDNS name. The **Devices**
-tab (key 5) lists them, and the connection pill in the header shows what the
-app is doing to reach the one in use, step by step, as it happens.
+tab (key 5) lists them. The connection pill in the header says whether the one in
+use is connected and how (Tailscale, or the network's name); click it to add,
+edit or reorder that headset's addresses, or to see each step of connecting.
The code is in three modules, all stdlib-only Python on your computer:
@@ -57,7 +58,7 @@ and ranks them:
5. addresses that only ever worked on other networks;
6. Tailscale addresses while Tailscale is off.
-Your order on the Devices tab breaks ties. The best-ranked address that answers
+Your order (on the Devices tab, or in the pill's dialog) breaks ties. The best-ranked address that answers
wins; one that answers first waits up to 0.35 s for a better-ranked one that is
still trying. If SSH to the winner fails in a way another address could fix
(a different device answered there, or the link dropped), the next one that
@@ -72,6 +73,12 @@ status --json`, including the Mac app's own CLI) with buttons to add their
MagicDNS name or IP. **Find on this network** asks mDNS for SteamOS devkit
services and checks `ALIAS.local` and `frame.local`.
+**The pill's dialog** lists the same addresses, with what each one answered, and
+can add, edit, reorder and remove them without leaving the page you're on. When
+the headset reports a LAN IP on the same network as this computer and that IP
+isn't saved, it offers to add it, so at home the app connects directly rather
+than over Tailscale. A new or edited address is tested straight away.
+
## Networks
A network is told apart by its default gateway: the router's IP address plus its
@@ -122,7 +129,8 @@ file per headset instead, so saving or forgetting one headset's key never touche
another's: a different device answering at one of its
addresses is refused, and the pill says so. A headset's first connection trusts
the key it shows, as Set Up Connection does. After reinstalling SteamOS the
-headset has a new key; **Forget identity** on the Devices tab lets the next
+headset has a new key; **Forget identity** (Devices tab → **Advanced**, with the
+SSH alias, user and port) lets the next
connection save the new one.
## One server at a time
diff --git a/docs/frame-control.md b/docs/frame-control.md
index 3ec3b4c..eedc055 100644
--- a/docs/frame-control.md
+++ b/docs/frame-control.md
@@ -22,9 +22,9 @@ The window has five tabs: **Home** (headset view, status, screenshots),
the catalogue, display settings, reports), **Tools** (sending files and text,
Flatpaks, remote and power) and **Devices** (your headsets and their addresses).
Keys 1–5 switch between them. Files can be dropped anywhere in the window. A
-connection pill in the header always shows which headset, which network this
-computer is on, the address in use or being tried, and each step of connecting
-as it happens; click it for the whole timeline. When the Frame can't be
+connection pill in the header always shows which headset and whether it's
+connected, and how (Tailscale, or the network's name). Click it to add, edit or
+reorder the headset's addresses, or to see each step of connecting. When the Frame can't be
reached, a banner says why in plain words, what was tried, and counts down to
the next try, filling everything in once it answers. Flatpak and Android installs run in the background; the bottom bar
counts them while they run.
diff --git a/docs/privacy.md b/docs/privacy.md
index db7004f..51b7d9a 100644
--- a/docs/privacy.md
+++ b/docs/privacy.md
@@ -97,7 +97,7 @@ The same error is sent at most once every 10 minutes.
## Report a problem
-**Report a problem** is the warning-sign button in the header, also in the
+**Report a problem** is the speech-bubble button in the header, also in the
Privacy panel and under **Help → Report a Problem…**. It sends the report
privately to Frame Control's PostHog project as a `problem_report` event, the
same way as the analytics above, so only the maintainer can read it and
diff --git a/ui/index.html b/ui/index.html
index 09870da..1e15f1b 100644
--- a/ui/index.html
+++ b/ui/index.html
@@ -71,7 +71,8 @@
@media (max-width: 1250px) { .brand b { display: none; } }
@media (max-width: 1150px) { .grid-top, .grid-3 { grid-template-columns: 1fr; } header { gap: 14px; padding: env(safe-area-inset-top) calc(18px + env(safe-area-inset-right)) 0 calc(18px + env(safe-area-inset-left)); } nav a { padding: 0 10px; } }
@media (max-width: 960px) { header { gap: 10px; } nav { gap: 0; } nav a { padding: 0 8px; font-size: 13px; letter-spacing: .4px; }
- .brand, #refreshAll { display: none; } .pill .pt span { display: none; } }
+ .brand, #refreshAll { display: none; }
+ .pill .pt > b, .pill .via { display: none; } .pill .pt > span { font-size: 13px; color: var(--bright); } }
nav svg { display: none; }
html { -webkit-text-size-adjust: 100%; }
body { -webkit-tap-highlight-color: transparent; }
@@ -103,7 +104,27 @@
@keyframes pulse { 50% { opacity: .35; } }
.pill .pt { display: flex; flex-direction: column; align-items: flex-start; min-width: 0; line-height: 1.2; text-align: left; }
.pill .pt b { font-weight: 500; font-size: 13px; color: var(--bright); max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
- .pill .pt span { font-size: 11.5px; color: var(--muted); max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+ .pill .pt > span { font-size: 11.5px; color: var(--muted); max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+ .conn-now { display: flex; gap: 10px; align-items: baseline; }
+ .conn-now .dot { position: relative; top: -1px; }
+ .conn-now b { color: var(--bright); font-weight: 500; font-size: 15px; }
+ .conn-offer { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; margin-top: 14px; padding: 10px 12px; border-radius: 4px;
+ background: rgba(26,159,255,.1); border-left: 3px solid var(--blue); font-size: 13px; }
+ .conn-offer .grow { flex: 1 1 260px; }
+ .h3-note { text-transform: none; letter-spacing: 0; font-weight: 400; margin-left: 6px; }
+ #connAddrs .item { gap: 6px; }
+ #connAddrs .t { font-size: 13.5px; overflow-wrap: anywhere; }
+ .conn-add { grid-template-columns: 2fr 1.2fr auto; }
+ .conn-edit { grid-template-columns: 2fr 1.2fr auto auto; }
+ .addr-add button.small, .dev-name button.small { height: 40px; }
+ .dev-name { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 10px; align-items: end; max-width: 520px; }
+ .adv { margin-top: 18px; border-top: 1px solid rgba(255,255,255,.06); padding-top: 4px; }
+ .adv .dev-form { grid-template-columns: 1.3fr 1fr .6fr; }
+ .conn-more { margin-top: 16px; }
+ .conn-more summary { margin-top: 0; }
+ /* The dialog scrolls when the steps are open in a short window; its buttons stay in reach. */
+ #connDlg .rep-actions { position: sticky; bottom: -22px; margin: 16px -22px -22px; padding: 12px 22px;
+ background: #1e2329; border-top: 1px solid rgba(255,255,255,.06); }
.devsel { background: var(--btn); color: var(--text); border: 0; border-radius: 3px; height: 40px; padding: 0 8px; font: inherit; font-size: 13px; max-width: 160px; }
.dlg { background: #1e2329; color: var(--text); border: 1px solid rgba(255,255,255,.1); border-radius: 4px;
padding: 22px; width: min(640px, 94vw); box-shadow: 0 20px 60px rgba(0,0,0,.6); }
@@ -144,8 +165,8 @@
.addr .t { overflow-wrap: anywhere; }
.addr .s { white-space: normal; }
.addr-edit { display: grid; grid-template-columns: 2fr 1fr 1.4fr auto auto; gap: 8px; align-items: center; width: 100%; }
- .addr-add { display: grid; grid-template-columns: 2fr 1fr 1.4fr auto; gap: 8px; margin-top: 12px; }
- @media (max-width: 700px) { .dev-form, .addr-edit, .addr-add { grid-template-columns: 1fr; } }
+ .addr-add { display: grid; grid-template-columns: 2fr 1.4fr auto; gap: 8px; margin-top: 12px; }
+ @media (max-width: 700px) { .dev-form, .addr-edit, .addr-add, .adv .dev-form, .conn-add, .conn-edit { grid-template-columns: 1fr; } }
.found { margin-top: 10px; }
/* ---- drop anywhere ---- */
@@ -460,7 +481,8 @@
header { height: calc(52px + env(safe-area-inset-top)); padding: env(safe-area-inset-top) calc(14px + env(safe-area-inset-right)) 0 calc(14px + env(safe-area-inset-left)); gap: 10px;
backdrop-filter: none; background: rgb(23, 29, 37); }
.brand svg { width: 26px; height: 26px; }
- .brand { display: flex; } #refreshAll { display: inline-flex; } .pill .pt span { display: block; } /* the tabs are at the bottom here */
+ .brand { display: flex; } #refreshAll { display: inline-flex; } /* the tabs are at the bottom here */
+ .pill .pt > b { display: block; } .pill .via { display: inline; } .pill .pt > span { font-size: 11.5px; color: var(--muted); }
header .chip { height: 30px; padding: 0 10px; }
nav { position: fixed; left: 0; right: 0; bottom: 0; z-index: 25; gap: 0;
height: calc(58px + env(safe-area-inset-bottom)); padding: 0 env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
@@ -672,8 +694,8 @@
-