archy/neode-ui/src/views/mesh/mesh-styles.css

494 lines
38 KiB
CSS
Raw Normal View History

/* Mesh view styles extracted from Mesh.vue
* Unscoped mesh-* classes must reach child components (MeshBitcoinPanel, MeshDeadmanPanel)
*/
.mesh-view {
padding: 24px;
max-width: 1600px;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: 16px;
height: 100%;
overflow: hidden;
}
2026-06-11 02:39:24 -04:00
@media (min-width: 921px) {
.mesh-dashboard-panel.mobile-scroll-pad {
padding-bottom: 0;
}
}
@media (min-width: 921px) and (max-width: 1279px) {
.mesh-dashboard-panel {
overflow-y: auto;
}
}
.mesh-header { justify-content: space-between; align-items: center; gap: 16px; flex-shrink: 0; }
.mesh-header-left { flex: 1; }
.mesh-title { font-size: 1.5rem; font-weight: 700; color: rgba(255, 255, 255, 0.95); margin: 0; }
.mesh-subtitle { color: rgba(255, 255, 255, 0.5); font-size: 0.85rem; margin: 2px 0 0; display: flex; align-items: center; gap: 8px; }
.mesh-subtitle-badge { font-size: 0.65rem; font-weight: 600; color: #4ade80; background: rgba(74, 222, 128, 0.12); padding: 1px 6px; border-radius: 4px; text-transform: uppercase; letter-spacing: 0.5px; }
.mesh-flasher-btn { display: inline-flex; align-items: center; gap: 0; padding: 8px 16px; font-size: 0.9rem; text-decoration: none; white-space: nowrap; flex-shrink: 0; }
.mesh-flasher-sep { margin: 0 8px; color: rgba(255, 255, 255, 0.2); }
.mesh-error { color: #ef4444; font-size: 0.85rem; padding: 8px 12px; background: rgba(239, 68, 68, 0.1); border-radius: 8px; border: 1px solid rgba(239, 68, 68, 0.2); flex-shrink: 0; }
.mesh-columns { display: flex; gap: 16px; flex: 1; min-height: 0; overflow: hidden; }
.mesh-left { width: 380px; flex-shrink: 0; display: flex; flex-direction: column; gap: 12px; min-height: 0; overflow-y: auto; overscroll-behavior: contain; }
.mesh-right { flex: 1; min-width: 0; min-height: 0; display: flex; flex-direction: column; gap: 12px; overflow: hidden; overscroll-behavior: contain; }
.mesh-tools-wrapper { display: contents; }
.mesh-tools-tab-bar { display: none; }
.mesh-columns-wide { display: grid; grid-template-columns: minmax(300px, 340px) minmax(420px, 1.1fr) minmax(360px, 0.9fr); gap: 16px; }
.mesh-columns-wide .mesh-left { grid-column: 1; width: auto; }
.mesh-columns-wide .mesh-right { display: contents; }
.mesh-columns-wide .mesh-chat-card { grid-column: 2; grid-row: 1; min-height: 0; overflow: hidden; }
.mesh-columns-wide .mesh-tools-wrapper { grid-column: 3; grid-row: 1; display: flex; flex-direction: column; gap: 0; min-height: 0; overflow: hidden; }
.mesh-columns-wide .mesh-tools-tab-bar { display: flex; gap: 2px; background: rgba(0,0,0,0.3); border-radius: 10px; padding: 3px; flex-shrink: 0; margin-bottom: 12px; }
.mesh-columns-very-wide { grid-template-columns: minmax(300px, 340px) minmax(460px, 1.05fr) minmax(420px, 0.95fr); }
.mesh-columns-very-wide .mesh-tools-wrapper { display: grid; grid-template-rows: minmax(0, 1fr) minmax(0, 0.85fr) minmax(0, 1fr); gap: 12px; overflow: hidden; }
.mesh-columns-very-wide .mesh-tools-wrapper .mesh-bitcoin-panel,
.mesh-columns-very-wide .mesh-tools-wrapper .mesh-deadman-panel,
.mesh-columns-very-wide .mesh-tools-wrapper .mesh-assistant-panel,
.mesh-columns-very-wide .mesh-tools-wrapper .mesh-map-panel { min-height: 0; height: 100%; overflow: hidden; }
.mesh-columns-wide:not(.mesh-columns-very-wide) .mesh-tools-wrapper .mesh-bitcoin-panel,
.mesh-columns-wide:not(.mesh-columns-very-wide) .mesh-tools-wrapper .mesh-deadman-panel,
.mesh-columns-wide:not(.mesh-columns-very-wide) .mesh-tools-wrapper .mesh-assistant-panel,
.mesh-columns-wide:not(.mesh-columns-very-wide) .mesh-tools-wrapper .mesh-map-panel { flex: 1 1 auto; min-height: 0; height: auto; }
.mesh-columns-very-wide .mesh-tools-tab-bar { display: none; }
.mesh-columns-wide .mesh-mobile-back-btn,
.mesh-columns-wide .mesh-tab-bar { display: none; }
.mesh-status-card { padding: 16px; flex-shrink: 0; }
.mesh-status-header { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; }
.mesh-status-indicator { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
.mesh-status-indicator.connected { background: #4ade80; box-shadow: 0 0 6px rgba(74, 222, 128, 0.5); }
.mesh-status-indicator.disconnected { background: rgba(255, 255, 255, 0.3); }
.mesh-section-title { font-size: 0.95rem; font-weight: 600; color: rgba(255, 255, 255, 0.9); margin: 0; }
/* Collapse chevron only used on mobile (hidden on desktop, where the Device
panel is always expanded). Kept small and pushed to the far right. */
.mesh-status-chevron { display: none; width: 16px; height: 16px; margin-left: auto; flex-shrink: 0; color: rgba(255, 255, 255, 0.5); transition: transform 0.2s ease; }
.mesh-status-card:not(.mesh-status-collapsed) .mesh-status-chevron { transform: rotate(180deg); }
.mesh-status-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
.mesh-stat { display: flex; flex-direction: column; gap: 1px; padding: 8px; background: rgba(255, 255, 255, 0.05); border-radius: 6px; }
.mesh-stat-label { font-size: 0.65rem; color: rgba(255, 255, 255, 0.4); text-transform: uppercase; letter-spacing: 0.5px; }
.mesh-stat-value { font-size: 0.8rem; color: rgba(255, 255, 255, 0.85); font-weight: 500; }
.text-green { color: #4ade80; }
.text-orange { color: #fb923c; }
.text-muted { color: rgba(255, 255, 255, 0.4); }
.mesh-loading, .mesh-empty { color: rgba(255, 255, 255, 0.4); font-size: 0.85rem; text-align: center; padding: 16px 0; }
.mesh-detected-devices { margin-top: 10px; padding-top: 10px; border-top: 1px solid rgba(255, 255, 255, 0.06); }
.mesh-device-row { display: flex; align-items: center; gap: 8px; padding: 6px 8px; background: rgba(255, 255, 255, 0.04); border-radius: 6px; }
.mesh-device-indicator { width: 6px; height: 6px; border-radius: 50%; background: #4ade80; box-shadow: 0 0 4px rgba(74, 222, 128, 0.4); flex-shrink: 0; }
.mesh-device-path { font-family: monospace; font-size: 0.8rem; color: rgba(255, 255, 255, 0.7); flex: 1; }
.mesh-connect-btn { padding: 3px 12px; font-size: 0.75rem; flex-shrink: 0; }
.mesh-offgrid-banner { display: flex; align-items: center; gap: 8px; padding: 8px 12px; background: rgba(251, 146, 60, 0.1); border: 1px solid rgba(251, 146, 60, 0.3); border-radius: 8px; flex-shrink: 0; }
.mesh-offgrid-active { border-color: rgba(251, 146, 60, 0.4) !important; color: #fb923c !important; }
.mesh-actions { display: flex; gap: 8px; flex-shrink: 0; }
.mesh-action-btn { flex: 1; padding: 8px 0; font-size: 0.8rem; }
.mesh-peers-card { padding: 14px; flex: 1; min-height: 0; display: flex; flex-direction: column; }
.mesh-peers-card .mesh-section-title { margin-bottom: 10px; flex-shrink: 0; }
.mesh-peer-list { display: flex; flex-direction: column; gap: 4px; overflow-y: auto; flex: 1; min-height: 0; }
.mesh-peer-row { display: flex; align-items: center; gap: 10px; padding: 10px; border-radius: 8px; cursor: pointer; transition: background 0.15s; }
.mesh-peer-row:hover { background: rgba(255, 255, 255, 0.06); }
.mesh-peer-row.active { background: rgba(251, 146, 60, 0.1); border: 1px solid rgba(251, 146, 60, 0.2); }
.mesh-peer-avatar { position: relative; width: 36px; height: 36px; border-radius: 50%; background: rgba(255, 255, 255, 0.08); display: flex; align-items: center; justify-content: center; font-size: 0.9rem; color: rgba(255, 255, 255, 0.6); flex-shrink: 0; font-weight: 600; }
.mesh-peer-search-wrap { position: relative; margin-bottom: 10px; flex-shrink: 0; }
.mesh-peer-search { width: 100%; box-sizing: border-box; padding: 7px 30px 7px 10px; font-size: 0.85rem; border-radius: 8px; border: 1px solid rgba(255,255,255,0.1); background: rgba(0,0,0,0.25); color: rgba(255,255,255,0.9); outline: none; }
.mesh-peer-search::placeholder { color: rgba(255,255,255,0.35); }
.mesh-peer-search:focus { border-color: rgba(251,146,60,0.4); }
.mesh-peer-search-clear { position: absolute; top: 50%; right: 6px; transform: translateY(-50%); display: flex; align-items: center; justify-content: center; width: 20px; height: 20px; line-height: 1; border: none; border-radius: 50%; background: rgba(255,255,255,0.12); color: rgba(255,255,255,0.7); font-size: 15px; cursor: pointer; padding: 0; }
.mesh-peer-search-clear:hover { background: rgba(255,255,255,0.22); color: #fff; }
.mesh-peer-reach { position: absolute; bottom: -1px; right: -1px; width: 10px; height: 10px; border-radius: 50%; border: 2px solid #11131a; }
.mesh-peer-reach.is-reachable { background: #34d399; }
.mesh-peer-reach.is-unreachable { background: rgba(255,255,255,0.25); }
.mesh-peer-avatar.archy { background: rgba(251, 146, 60, 0.15); padding: 0; overflow: hidden; }
.mesh-peer-avatar.archy :deep(> div) { width: 26px; height: 26px; border-radius: 50%; overflow: hidden; }
.mesh-peer-avatar.channel { background: rgba(59, 130, 246, 0.15); color: #3b82f6; font-weight: 700; font-size: 1.1rem; }
.mesh-peer-channel-badge { font-size: 0.6rem; font-weight: 700; color: #3b82f6; background: rgba(59, 130, 246, 0.12); padding: 1px 5px; border-radius: 3px; text-transform: uppercase; letter-spacing: 0.5px; }
.mesh-peer-count { font-size: 0.75rem; font-weight: 600; color: rgba(255, 255, 255, 0.4); background: rgba(255, 255, 255, 0.08); padding: 2px 8px; border-radius: 10px; margin-left: 6px; vertical-align: middle; }
.mesh-peer-row.is-channel { border-bottom: 1px solid rgba(255, 255, 255, 0.04); padding-bottom: 12px; margin-bottom: 4px; }
.mesh-peer-info { flex: 1; min-width: 0; }
.mesh-peer-name { font-weight: 600; font-size: 0.85rem; color: rgba(255, 255, 255, 0.9); display: flex; align-items: center; gap: 6px; }
.mesh-peer-archy-badge { font-size: 0.6rem; font-weight: 700; color: #fb923c; background: rgba(251, 146, 60, 0.12); padding: 1px 5px; border-radius: 3px; text-transform: uppercase; letter-spacing: 0.5px; }
.mesh-peer-sub { font-size: 0.7rem; color: rgba(255, 255, 255, 0.3); font-family: monospace; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mesh-peer-signal { flex-shrink: 0; }
.mesh-signal-bars { display: flex; align-items: flex-end; gap: 2px; height: 14px; }
.mesh-signal-bar { width: 3px; border-radius: 1px; background: rgba(255, 255, 255, 0.12); }
.mesh-signal-bar:nth-child(1) { height: 3px; }
.mesh-signal-bar:nth-child(2) { height: 6px; }
.mesh-signal-bar:nth-child(3) { height: 10px; }
.mesh-signal-bar:nth-child(4) { height: 14px; }
.mesh-signal-bar.active { background: #4ade80; }
.mesh-unread-badge { background: #fb923c; color: #000; font-size: 0.65rem; font-weight: 700; min-width: 18px; height: 18px; border-radius: 9px; display: flex; align-items: center; justify-content: center; padding: 0 5px; flex-shrink: 0; }
.mesh-chat-card { padding: 0; flex: 1; display: flex; flex-direction: column; min-height: 0; overflow: hidden; }
.mesh-chat-empty { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; color: rgba(255, 255, 255, 0.3); gap: 8px; padding: 40px; }
.mesh-chat-empty-icon { font-size: 3rem; opacity: 0.4; }
.mesh-chat-empty p { margin: 0; font-size: 0.9rem; }
.mesh-chat-empty-sub { font-size: 0.75rem !important; color: rgba(255, 255, 255, 0.2); }
.mesh-chat-header { display: flex; align-items: center; gap: 10px; padding: 14px 16px; border-bottom: 1px solid rgba(255, 255, 255, 0.06); flex-shrink: 0; }
/* Floating mobile back button (Teleported to body). Hidden by default; only
shown in the single-column mobile mesh layout (see media query below). */
.mesh-chat-mobile-back { display: none; }
.mesh-chat-header-info { flex: 1; min-width: 0; }
.mesh-chat-header-name { font-weight: 600; font-size: 0.95rem; color: rgba(255, 255, 255, 0.9); display: flex; align-items: center; gap: 6px; }
fix(mesh): DM-via-channel tunnel + disable presence spam Meshcore direct unicast silently drops between our two Archy nodes (firmware reports flood sends with resp_code=6 but nothing arrives). Wrap DMs as channel-1 broadcasts with a [0xD1][dest_prefix(6)][inner] header; receivers filter by prefix and dispatch the inner payload through the existing typed/base64/chunk ladder. Shrink chunk body to 125B so the wrapper still fits the 160B LoRa budget. Auto-heal routing: CMD_RESET_PATH (0x0D) any type-1 contact with path_len=0 on refresh so floods take over. send_text now returns the firmware's flood/direct mode flag for diagnostics. Disable the 120s presence heartbeat broadcaster — its CBOR payload was being re-echoed as plaintext by the shared repeater, spamming every visible node with garbled "Archy-…: av�…fstatusfonline…" messages on channel 0. mesh.broadcast-presence RPC stays registered but no longer transmits. Re-enable only once presence moves off the shared broadcast path. Also: MeshState.cmd_tx behind RwLock so stop()→start() cycles don't fail with "command channel already consumed"; MeshService.send_cmd helper; drop_message_by_id for control envelopes that shouldn't appear as Sent bubbles; self_advert_name reflected into MeshStatus after set; path_len/flags parsed out of RESP_CONTACT. Frontend: unified inbox merges mesh peers with federation nodes by DID/pubkey/name; hide presence/read_receipt/edit/channel_invite/ contact_card from chat stream; publicChannel index → 1 to match the new DM-via-channel routing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 10:24:27 -04:00
.mesh-chat-header-rename { background: transparent; border: none; color: rgba(255, 255, 255, 0.4); cursor: pointer; padding: 2px 4px; font-size: 0.85rem; line-height: 1; }
.mesh-chat-header-rename:hover { color: rgba(255, 255, 255, 0.9); }
.mesh-chat-header-rename-input { background: rgba(255, 255, 255, 0.08); border: 1px solid rgba(255, 255, 255, 0.18); border-radius: 6px; color: rgba(255, 255, 255, 0.95); font-size: 0.95rem; font-weight: 600; padding: 4px 8px; outline: none; min-width: 0; max-width: 220px; }
.mesh-chat-header-rename-input:focus { border-color: rgba(255, 255, 255, 0.4); }
.mesh-chat-header-sub { font-size: 0.7rem; color: rgba(255, 255, 255, 0.3); font-family: monospace; }
.mesh-chat-header-status { flex-shrink: 0; }
.mesh-chat-header-time { font-size: 0.7rem; color: rgba(255, 255, 255, 0.3); }
.mesh-chat-messages { flex: 1; overflow-y: auto; overscroll-behavior: contain; padding: 16px; display: flex; flex-direction: column; gap: 8px; min-height: 0; }
.mesh-chat-no-messages { flex: 1; display: flex; align-items: center; justify-content: center; color: rgba(255, 255, 255, 0.25); font-size: 0.85rem; }
.mesh-chat-bubble-wrapper { display: flex; }
.mesh-chat-bubble-wrapper.sent { justify-content: flex-end; }
.mesh-chat-bubble-wrapper.received { justify-content: flex-start; }
.mesh-chat-bubble { max-width: 75%; padding: 10px 14px; border-radius: 16px; word-break: break-word; }
.mesh-chat-bubble.sent { background: rgba(251, 146, 60, 0.15); border: 1px solid rgba(251, 146, 60, 0.2); border-bottom-right-radius: 4px; }
.mesh-chat-bubble.received { background: rgba(255, 255, 255, 0.06); border: 1px solid rgba(255, 255, 255, 0.08); border-bottom-left-radius: 4px; }
.mesh-chat-bubble-text { color: rgba(255, 255, 255, 0.9); font-size: 0.9rem; line-height: 1.4; }
.mesh-chat-bubble-meta { display: flex; align-items: center; gap: 6px; margin-top: 4px; justify-content: flex-end; }
.mesh-chat-bubble-time { font-size: 0.65rem; color: rgba(255, 255, 255, 0.3); }
.mesh-chat-e2e { font-size: 0.55rem; font-weight: 700; color: #4ade80; padding: 0 3px; border: 1px solid rgba(74, 222, 128, 0.3); border-radius: 3px; }
feat(mesh,ui): per-message transport pill (Mesh/FIPS/Tor) + fix E2E pill Adds a per-message transport badge to archy↔archy mesh chats and fixes the long-broken E2E badge — both meshcore and meshtastic, styled like the existing E2E pill. Transport pill: - New `MeshMessage.transport` ("lora"/"fips"/"tor"), surfaced in the UI beside the E2E badge (Mesh.vue transportLabel() → Mesh/FIPS/Tor, mesh-styles.css). - Sent LoRa → "lora"; sent federation → finalized to the real leg ("fips"/"tor") once the background send resolves (req.send_json transport), via an id-keyed store update. - Received: a post-dispatch stamp on handle_typed_envelope_direct's output (monotonic ids) tags both transports without threading through all 20 typed- dispatch sites — radio wrapper stamps "lora", federation injector stamps the peer's last_transport ("fips"/"tor", default tor; the inbound HTTP carries no FIPS-vs-Tor signal). - Plain native/channel LoRa frames → "lora"; channel broadcasts stay non-E2E. E2E pill fix: - `encrypted` was hardcoded false at every MeshMessage construction site, so the UI badge (Mesh.vue `v-if="msg.encrypted"`) never showed. Now: federation envelopes are E2E (identity-signed over an encrypted transport); the meshcore native-DM receive path already had a real `encrypted` flag (now also tagged with transport). meshtastic-PKI radio E2E flag threading is a noted follow-up. Backend cargo check + frontend vue-tsc build both green. Needs a live radio + multi-transport pass on .116/.228 to confirm end-to-end (see project_transport_pill / project_meshtastic_parity). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 04:29:25 -04:00
/* Per-message transport pill (Mesh / FIPS / Tor), styled like the E2E badge. */
.mesh-chat-transport { font-size: 0.55rem; font-weight: 700; padding: 0 3px; border-radius: 3px; border: 1px solid currentColor; opacity: 0.85; }
.mesh-chat-transport.transport-lora { color: #f59e0b; } /* Mesh/LoRa — amber */
.mesh-chat-transport.transport-fips { color: #a78bfa; } /* FIPS — violet */
.mesh-chat-transport.transport-tor { color: #818cf8; } /* Tor — indigo */
.mesh-chat-ack { font-size: 0.7rem; color: #3b82f6; }
.mesh-chat-compose { padding: 12px 16px; border-top: 1px solid rgba(255, 255, 255, 0.06); flex-shrink: 0; }
.mesh-chat-send-error { color: #ef4444; font-size: 0.75rem; margin-bottom: 6px; }
.mesh-chat-compose-row { display: flex; gap: 8px; }
.mesh-chat-input { flex: 1; background: rgba(255, 255, 255, 0.06); border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 20px; color: rgba(255, 255, 255, 0.9); padding: 10px 16px; font-size: 0.9rem; font-family: inherit; outline: none; }
.mesh-chat-input:focus { border-color: rgba(251, 146, 60, 0.4); }
.mesh-chat-input::placeholder { color: rgba(255, 255, 255, 0.25); }
.mesh-chat-send-btn { padding: 10px 20px; border-radius: 20px; font-size: 0.85rem; background: rgba(251, 146, 60, 0.15); border-color: rgba(251, 146, 60, 0.25); min-width: 72px; display: inline-flex; align-items: center; justify-content: center; }
.mesh-chat-send-btn:hover:not(:disabled) { background: rgba(251, 146, 60, 0.25); }
.mesh-send-spinner { display: inline-block; width: 14px; height: 14px; border: 2px solid rgba(255, 255, 255, 0.2); border-top-color: rgba(251, 146, 60, 0.9); border-radius: 50%; animation: mesh-send-spin 0.7s linear infinite; }
@keyframes mesh-send-spin { to { transform: rotate(360deg); } }
.mesh-mobile-back-btn { display: none; }
/* Floating mobile mesh tab strip (Teleported to body). Hidden on desktop; the
1279px block flips it to flex and the placement mirrors the mobile back
button (pinned above the global tab bar + audio player). */
.mesh-mobile-tabbar {
display: none;
position: fixed;
left: 12px;
right: 12px;
bottom: calc(var(--mobile-tab-bar-height, 72px) + var(--audio-player-height, 0px) + 8px);
z-index: 40;
gap: 4px;
padding: 4px;
border-radius: 14px;
background: rgba(0, 0, 0, 0.55);
backdrop-filter: blur(24px) saturate(140%);
-webkit-backdrop-filter: blur(24px) saturate(140%);
border: 1px solid rgba(255, 255, 255, 0.14);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
}
.mesh-mtab {
flex: 1 1 0;
min-width: 0;
min-height: 40px;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 4px;
padding: 6px 4px;
border: none;
border-radius: 10px;
background: transparent;
color: rgba(255, 255, 255, 0.6);
font-size: 0.78rem;
font-weight: 600;
white-space: nowrap;
cursor: pointer;
transition: background-color 0.15s ease, color 0.15s ease;
}
.mesh-mtab:hover { color: rgba(255, 255, 255, 0.9); }
.mesh-mtab.active { background: rgba(251, 146, 60, 0.2); color: #fff; }
@media (max-width: 1279px) {
.mesh-view { height: auto; overflow: visible; padding: 0 12px 100px 12px; }
.mesh-columns { flex-direction: column; overflow: visible; }
.mesh-left { width: 100%; overflow: visible; }
.mesh-right { min-height: auto; overflow: visible; }
.mesh-chat-card { min-height: 60dvh; max-height: 75dvh; overflow: hidden; display: flex; flex-direction: column; }
/* Single-column mobile mesh: one fixed, internally-scrolling pane that
fills the space between the top tab strip and the floating mesh tab bar.
The page itself never scrolls; each pane scrolls inside its own bounds.
Fixed positioning is relative to the full-height perspective container, so
the offsets line up with the body-teleported tab bar / back button. */
.mesh-left,
.mesh-mobile-tools,
.mesh-chat-card.mesh-chat-card-active {
position: fixed;
left: 12px;
right: 12px;
/* width:auto so left+right govern the box .mesh-left otherwise carries a
fixed 380px width that ignores `right` and overflows the screen. */
width: auto;
box-sizing: border-box;
top: calc(var(--safe-area-top, env(safe-area-inset-top, 0px)) + 96px);
/* Just above the floating mesh tab bar (tabs sit at +8, ~48px tall). */
bottom: calc(var(--mobile-tab-bar-height, 72px) + var(--audio-player-height, 0px) + 72px);
height: auto;
min-height: 0;
max-height: none;
overflow-y: auto;
overscroll-behavior: contain;
z-index: 30;
}
/* Active conversation: the floating tabs are hidden here and the back button
takes the standard spot above the tab bar, so the chat window fills down to
just above the back pill (back pill 44px at +8, plus a 16px gap). When the
keyboard is up it covers the tab bar, so anchor to whichever is taller the
bottom controls or the keyboard so the window sits right above both. */
.mesh-chat-card.mesh-chat-card-active {
bottom: calc(max(var(--mobile-tab-bar-height, 72px) + var(--audio-player-height, 0px), var(--keyboard-inset, 0px)) + 68px);
overflow: hidden; /* the messages list inside does the scrolling */
}
.mesh-tools-wrapper { display: none !important; }
.mesh-mobile-tools { margin-top: 0; display: flex; flex-direction: column; gap: 12px; }
/* The active tool fills the whole fixed pane (no fixed cap that would leave a
bottom margin); the panel itself scrolls if its content is taller. */
.mesh-mobile-tools > * { flex: 1 1 auto; min-height: 0; max-height: none; }
.mesh-mobile-tools .mesh-bitcoin-panel,
.mesh-mobile-tools .mesh-assistant-panel,
.mesh-mobile-tools .mesh-deadman-panel { overflow-y: auto; }
.mesh-mobile-tools .mesh-map-panel { height: 100%; overflow: hidden; }
.mesh-status-grid { grid-template-columns: repeat(2, 1fr); }
/* In a conversation the tabs are hidden, so the back pill sits just above the
tab bar or above the keyboard when it's up, whichever is taller. */
.mesh-chat-mobile-back { display: flex; }
.mesh-chat-mobile-back.mobile-back-btn {
bottom: calc(max(var(--mobile-tab-bar-height, 72px) + var(--audio-player-height, 0px), var(--keyboard-inset, 0px)) + 8px);
}
/* Floating mesh tab strip — same placement logic as the mobile back button. */
.mesh-mobile-tabbar { display: flex; }
.mobile-hidden { display: none !important; }
/* Device panel is a collapsible/expandable accordion on mobile (starts
collapsed). Show the chevron, make the header tappable, and hide the body
when collapsed. */
.mesh-status-chevron { display: block; }
.mesh-status-card .mesh-status-header { cursor: pointer; margin-bottom: 12px; }
.mesh-status-card.mesh-status-collapsed .mesh-status-header { margin-bottom: 0; }
.mesh-status-card.mesh-status-collapsed .mesh-status-grid,
.mesh-status-card.mesh-status-collapsed .mesh-detected-devices { display: none; }
:deep(.mesh-bitcoin-panel),
:deep(.mesh-assistant-panel),
:deep(.mesh-deadman-panel) { flex: none; cursor: pointer; flex-shrink: 0; }
.mesh-mobile-back-btn:hover { color: rgba(255, 255, 255, 0.9); }
}
2026-06-11 02:39:24 -04:00
@media (min-width: 921px) and (max-width: 1279px) {
.mesh-view {
padding: 24px;
}
/* In this range the desktop sidebar (256px) is still shown. The in-pane
fixed elements are positioned relative to the main content area (their
perspective containing block), so they already clear the sidebar but the
body-teleported floating bars are viewport-relative, so nudge them right. */
.mesh-mobile-tabbar,
.mesh-chat-mobile-back.mobile-back-btn {
left: 268px;
}
2026-06-11 02:39:24 -04:00
}
@media (max-width: 920px) {
.mesh-view {
padding-left: 0;
padding-right: 0;
}
}
.mesh-session-badge { font-size: 0.75rem; margin-right: 6px; opacity: 0.7; }
.mesh-session-rotate { background: transparent; border: 1px solid rgba(255,255,255,0.15); color: rgba(255,255,255,0.7); font-size: 0.75rem; line-height: 1; padding: 2px 6px; margin-right: 8px; border-radius: 10px; cursor: pointer; transition: background 0.15s ease, color 0.15s ease; }
.mesh-session-rotate:hover:not(:disabled) { background: rgba(251,146,60,0.2); color: #fff; border-color: rgba(251,146,60,0.4); }
.mesh-session-rotate:disabled { opacity: 0.5; cursor: wait; }
.mesh-outbox-badge { font-size: 0.7rem; padding: 2px 7px; margin-right: 8px; border-radius: 10px; background: rgba(251,146,60,0.2); border: 1px solid rgba(251,146,60,0.4); color: #fff; }
.session-ratchet { color: #4ade80; opacity: 1; }
.session-static { color: #fbbf24; }
.session-none { color: rgba(255,255,255,0.3); }
.mesh-typed-icon { margin-right: 4px; }
.mesh-typed-label { font-weight: 600; font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.05em; }
.typed-invoice { border-left: 3px solid #fb923c; }
.mesh-typed-invoice { padding: 4px 0; }
.mesh-typed-invoice-header { display: flex; align-items: center; gap: 4px; margin-bottom: 4px; color: #fb923c; font-size: 0.75rem; }
.mesh-typed-invoice-amount { font-size: 1.1rem; font-weight: 700; color: #fb923c; }
.mesh-typed-invoice-memo { font-size: 0.8rem; color: rgba(255,255,255,0.7); margin-top: 2px; }
.mesh-typed-invoice-bolt11 { font-size: 0.65rem; color: rgba(255,255,255,0.3); font-family: monospace; margin-top: 4px; word-break: break-all; }
.mesh-typed-paid { background: rgba(74,222,128,0.2); color: #4ade80; font-size: 0.65rem; padding: 1px 6px; border-radius: 4px; margin-left: auto; }
.typed-alert { border-left: 3px solid #ef4444; }
.typed-alert.alert-status { border-left-color: #3b82f6; }
.mesh-typed-alert { padding: 4px 0; }
.mesh-typed-alert-header { display: flex; align-items: center; gap: 4px; margin-bottom: 4px; font-size: 0.75rem; }
.alert-emergency .mesh-typed-alert-header { color: #ef4444; }
.alert-dead_man .mesh-typed-alert-header { color: #ef4444; }
.alert-status .mesh-typed-alert-header { color: #3b82f6; }
.mesh-typed-alert-message { font-size: 0.85rem; color: rgba(255,255,255,0.9); }
.mesh-typed-alert-location { display: block; font-size: 0.75rem; color: #3b82f6; margin-top: 4px; text-decoration: underline; }
.mesh-typed-signed { font-size: 0.6rem; color: #4ade80; border: 1px solid rgba(74,222,128,0.3); padding: 0 4px; border-radius: 3px; margin-left: auto; }
.typed-coordinate { border-left: 3px solid #3b82f6; }
.mesh-typed-coordinate { padding: 4px 0; }
.mesh-typed-coordinate-header { display: flex; align-items: center; gap: 4px; margin-bottom: 4px; color: #3b82f6; font-size: 0.75rem; }
.mesh-typed-coordinate-value { font-size: 0.9rem; font-family: monospace; color: rgba(255,255,255,0.8); }
.mesh-typed-coordinate-label { font-size: 0.8rem; color: rgba(255,255,255,0.6); margin-top: 2px; }
.mesh-typed-coordinate-link { display: inline-block; font-size: 0.75rem; color: #3b82f6; margin-top: 4px; text-decoration: underline; }
.typed-block_header { border-left: 3px solid #a855f7; }
.mesh-typed-block { display: flex; align-items: center; gap: 4px; color: #a855f7; font-size: 0.8rem; }
.mesh-tab-bar { display: flex; gap: 2px; background: rgba(0,0,0,0.3); border-radius: 10px; padding: 3px; flex-shrink: 0; }
.mesh-tab { flex: 1; padding: 8px 12px; border: none; background: transparent; color: rgba(255,255,255,0.5); font-size: 0.82rem; font-weight: 500; border-radius: 8px; cursor: pointer; transition: all 0.2s ease; display: flex; align-items: center; justify-content: center; gap: 6px; }
.mesh-tab:hover { color: rgba(255,255,255,0.8); background: rgba(255,255,255,0.05); }
.mesh-tab.active { color: #fff; background: rgba(255,255,255,0.1); }
.mesh-tab-badge { font-size: 0.65rem; background: rgba(251,146,60,0.2); color: #fb923c; padding: 1px 5px; border-radius: 4px; font-weight: 600; }
.mesh-tab-badge-alert { background: rgba(239,68,68,0.3); color: #ef4444; animation: pulse-alert 1.5s infinite; }
@keyframes pulse-alert { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
.mesh-map-panel { flex: 1; min-height: 400px; padding: 0 !important; overflow: hidden; border-radius: 12px; position: relative; }
/* Bitcoin & Deadman panels (child components) */
.mesh-bitcoin-panel,
.mesh-deadman-panel,
.mesh-assistant-panel { padding: 16px; display: flex; flex-direction: column; gap: 12px; flex: 1; min-height: 0; overflow-y: auto; }
.mesh-assistant-field { display: flex; flex-direction: column; gap: 4px; }
.mesh-assistant-install { padding: 12px; background: rgba(251,146,60,0.08); border: 1px solid rgba(251,146,60,0.25); border-radius: 10px; }
.mesh-assistant-install-btn { display: inline-block; text-align: center; padding: 8px 14px; font-size: 0.8rem; }
.mesh-assistant-allowlist { display: flex; flex-direction: column; gap: 2px; max-height: 180px; overflow-y: auto; overscroll-behavior: contain; border: 1px solid rgba(255,255,255,0.08); border-radius: 10px; padding: 6px; background: rgba(0,0,0,0.2); }
.mesh-assistant-allow-row { display: flex; align-items: center; gap: 8px; padding: 6px 8px; border-radius: 8px; cursor: pointer; font-size: 0.85rem; color: rgba(255,255,255,0.85); }
.mesh-assistant-allow-row:hover { background: rgba(255,255,255,0.06); }
.mesh-assistant-allow-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mesh-assistant-addkey { display: flex; gap: 6px; margin-top: 6px; }
.mesh-assistant-addkey input { flex: 1; min-width: 0; }
.mesh-panel-title { font-size: 1rem; font-weight: 700; color: rgba(255,255,255,0.95); margin: 0; }
.mesh-panel-sub { font-size: 0.8rem; color: rgba(255,255,255,0.45); margin: -4px 0 0; }
.mesh-bitcoin-section { display: flex; flex-direction: column; gap: 8px; }
.mesh-bitcoin-section-header { display: flex; justify-content: space-between; align-items: center; }
.mesh-bitcoin-label { font-size: 0.75rem; font-weight: 600; color: rgba(255,255,255,0.5); text-transform: uppercase; letter-spacing: 0.5px; }
.mesh-bitcoin-height { font-size: 0.85rem; font-weight: 700; color: #fb923c; font-family: monospace; }
.mesh-bitcoin-height.mesh-muted { color: rgba(255,255,255,0.3); font-weight: 400; }
.mesh-bitcoin-hint { font-size: 0.8rem; color: rgba(255,255,255,0.45); margin: 0; }
.mesh-bitcoin-input { width: 100%; background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.1); border-radius: 8px; color: rgba(255,255,255,0.9); padding: 10px 12px; font-size: 0.85rem; font-family: inherit; outline: none; box-sizing: border-box; }
.mesh-bitcoin-input:focus { border-color: rgba(251,146,60,0.4); }
.mesh-bitcoin-input::placeholder { color: rgba(255,255,255,0.25); }
.mesh-bitcoin-input-sm { padding: 8px 12px; font-size: 0.8rem; }
textarea.mesh-bitcoin-input { resize: vertical; min-height: 60px; }
select.mesh-bitcoin-input { cursor: pointer; appearance: none; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='rgba(255,255,255,0.4)' viewBox='0 0 16 16'%3E%3Cpath d='M8 11L3 6h10z'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 12px center; padding-right: 32px; }
select.mesh-bitcoin-input option { background: #1a1a2e; color: rgba(255,255,255,0.9); }
.mesh-bitcoin-advanced { margin-top: 4px; }
.mesh-bitcoin-advanced summary { cursor: pointer; list-style: none; display: flex; align-items: center; gap: 6px; }
.mesh-bitcoin-advanced summary::before { content: '\25B6'; font-size: 0.6rem; color: rgba(255,255,255,0.4); transition: transform 0.2s; }
.mesh-bitcoin-advanced[open] summary::before { transform: rotate(90deg); }
.mesh-block-list { display: flex; flex-direction: column; gap: 4px; }
.mesh-block-row { display: flex; align-items: center; gap: 8px; padding: 6px 8px; background: rgba(255,255,255,0.04); border-radius: 6px; }
.mesh-block-height { font-size: 0.8rem; font-weight: 600; color: #a855f7; font-family: monospace; }
.mesh-block-hash { font-size: 0.7rem; color: rgba(255,255,255,0.35); font-family: monospace; }
.mesh-send-tabs { display: flex; gap: 2px; background: rgba(0,0,0,0.3); border-radius: 8px; padding: 2px; }
.mesh-send-tab { flex: 1; padding: 6px 12px; border: none; background: transparent; color: rgba(255,255,255,0.5); font-size: 0.8rem; font-weight: 500; border-radius: 6px; cursor: pointer; transition: all 0.2s; }
.mesh-send-tab:hover { color: rgba(255,255,255,0.8); }
.mesh-send-tab.active { color: #fff; background: rgba(255,255,255,0.1); }
.mesh-relay-mode { display: flex; gap: 4px; flex-wrap: wrap; }
.mesh-relay-mode-option { display: flex; align-items: center; gap: 6px; padding: 6px 10px; border-radius: 6px; cursor: pointer; font-size: 0.8rem; color: rgba(255,255,255,0.6); transition: all 0.15s; }
.mesh-relay-mode-option.active { color: rgba(255,255,255,0.9); }
.mesh-relay-mode-option small { color: rgba(255,255,255,0.35); font-size: 0.7rem; }
.mesh-relay-mode-option input[type="radio"] { accent-color: #fb923c; }
.mesh-relay-result { padding: 8px 12px; border-radius: 8px; font-size: 0.8rem; }
.mesh-relay-result.success { background: rgba(74,222,128,0.1); border: 1px solid rgba(74,222,128,0.2); color: #4ade80; }
.mesh-relay-result.error { background: rgba(239,68,68,0.1); border: 1px solid rgba(239,68,68,0.2); color: #ef4444; }
/* Deadman panel specifics */
.mesh-deadman-status { display: flex; flex-direction: column; gap: 8px; padding: 12px; background: rgba(0,0,0,0.2); border-radius: 10px; }
.mesh-deadman-indicator { display: inline-flex; align-items: center; font-size: 0.7rem; font-weight: 700; text-transform: uppercase; letter-spacing: 1px; padding: 4px 10px; border-radius: 6px; width: fit-content; }
.mesh-deadman-indicator.armed { background: rgba(251,146,60,0.15); color: #fb923c; border: 1px solid rgba(251,146,60,0.3); }
.mesh-deadman-indicator.disabled { background: rgba(255,255,255,0.06); color: rgba(255,255,255,0.4); border: 1px solid rgba(255,255,255,0.08); }
.mesh-deadman-indicator.triggered { background: rgba(239,68,68,0.15); color: #ef4444; border: 1px solid rgba(239,68,68,0.3); animation: pulse-alert 1.5s infinite; }
.mesh-deadman-timer { font-size: 1.8rem; font-weight: 700; color: #fb923c; font-family: monospace; }
.mesh-deadman-message { font-size: 0.8rem; color: rgba(255,255,255,0.5); font-style: italic; }
.mesh-deadman-checkin-btn { margin-top: 4px; }
.mesh-deadman-config { display: flex; flex-direction: column; gap: 10px; }
.mesh-deadman-field { display: flex; flex-direction: column; gap: 4px; }
.mesh-deadman-info { display: flex; gap: 12px; flex-wrap: wrap; }
.mesh-deadman-info-item { font-size: 0.75rem; color: rgba(255,255,255,0.4); }
/* Reaction chips and action menu (Phase 2a) */
.mesh-chat-reactions { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }
.mesh-chat-reaction-chip { display: inline-flex; align-items: center; gap: 4px; padding: 3px 8px; border-radius: 12px; background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.12); font-size: 0.85rem; line-height: 1.1; }
.mesh-chat-reaction-chip.by-self { background: rgba(251,146,60,0.15); border-color: rgba(251,146,60,0.4); }
.mesh-chat-reaction-count { font-size: 0.7rem; color: rgba(255,255,255,0.55); font-weight: 600; }
.mesh-chat-action-menu { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin-top: 8px; padding: 8px 10px; border-radius: 10px; background: rgba(0,0,0,0.35); border: 1px solid rgba(255,255,255,0.1); }
.mesh-chat-action-btn { background: transparent; border: none; color: rgba(255,255,255,0.75); font-size: 0.8rem; padding: 4px 8px; border-radius: 6px; cursor: pointer; }
.mesh-chat-action-btn:hover { background: rgba(255,255,255,0.08); color: #fff; }
.mesh-chat-reaction-btn { background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.1); color: #fff; font-size: 1.15rem; line-height: 1; padding: 6px 10px; border-radius: 8px; cursor: pointer; transition: transform 0.1s ease, background 0.15s ease; }
.mesh-chat-reaction-btn:hover { background: rgba(251,146,60,0.2); transform: scale(1.1); }
.mesh-chat-action-danger { color: rgba(248, 113, 113, 0.9) !important; }
.mesh-chat-action-danger:hover { background: rgba(239,68,68,0.2) !important; color: #fff !important; }
.mesh-chat-forward-header { font-size: 0.75rem; color: rgba(251,146,60,0.85); font-style: italic; margin-bottom: 3px; }
.mesh-chat-forward-body { }
.mesh-chat-deleted { font-style: italic; opacity: 0.55; }
.mesh-chat-edited { font-size: 0.7rem; opacity: 0.55; font-style: italic; }
/* Telegram-style ⋯ action trigger: tiny, ghosted in the meta row, expands on hover or when menu is open */
.mesh-chat-action-trigger {
background: transparent;
border: none;
color: rgba(255,255,255,0.45);
font-size: 1rem;
line-height: 1;
padding: 2px 6px;
margin-left: 2px;
border-radius: 10px;
cursor: pointer;
opacity: 0;
transform: scale(0.85);
transition: opacity 0.15s ease, transform 0.15s ease, background 0.15s ease, color 0.15s ease;
}
.mesh-chat-bubble:hover .mesh-chat-action-trigger,
.mesh-chat-bubble.menu-open .mesh-chat-action-trigger,
.mesh-chat-action-trigger.active,
.mesh-chat-action-trigger:focus-visible {
opacity: 1;
transform: scale(1);
}
.mesh-chat-action-trigger:hover,
.mesh-chat-action-trigger.active {
background: rgba(255,255,255,0.1);
color: #fff;
}
@media (hover: none) {
.mesh-chat-action-trigger { opacity: 0.7; transform: scale(1); }
}
/* Generic inline spinner for busy buttons */
.mesh-spinner { display: inline-block; width: 1em; height: 1em; border: 2px solid rgba(255,255,255,0.25); border-top-color: #fb923c; border-radius: 50%; animation: mesh-spin 0.7s linear infinite; vertical-align: middle; }
@keyframes mesh-spin { to { transform: rotate(360deg); } }
.mesh-chat-attach-btn.is-busy { opacity: 0.8; cursor: wait; }
.mesh-chat-reaction-btn.is-busy { background: rgba(251,146,60,0.25); }
.mesh-chat-reaction-btn:disabled { opacity: 0.6; cursor: wait; }
/* Reply / attachment pending banner */
.mesh-chat-pending-reply,
.mesh-chat-pending-attachment { display: flex; align-items: flex-start; gap: 8px; padding: 8px 12px; margin: 6px 0; border-radius: 10px; background: rgba(251,146,60,0.1); border: 1px solid rgba(251,146,60,0.25); font-size: 0.85rem; }
.mesh-chat-pending-reply .mesh-typed-icon,
.mesh-chat-pending-attachment .mesh-typed-icon { color: #fb923c; font-size: 1rem; line-height: 1.4; flex: 0 0 auto; }
.mesh-chat-pending-name { flex: 1 1 auto; min-width: 0; color: rgba(255,255,255,0.85); overflow-wrap: anywhere; word-break: break-word; line-height: 1.35; }
.mesh-chat-pending-size { flex: 0 0 auto; color: rgba(255,255,255,0.45); font-size: 0.75rem; margin-left: 4px; }
.mesh-chat-pending-clear { flex: 0 0 auto; align-self: center; margin-left: auto; background: rgba(255,255,255,0.08); border: 1px solid rgba(255,255,255,0.15); color: rgba(255,255,255,0.85); width: 28px; height: 28px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; cursor: pointer; font-size: 0.95rem; line-height: 1; transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease, transform 0.1s ease; }
.mesh-chat-pending-clear:hover { background: rgba(239,68,68,0.3); color: #fff; border-color: rgba(239,68,68,0.6); transform: scale(1.08); }
.mesh-chat-pending-clear:active { transform: scale(0.92); }
feat(mesh): Telegram primitives pass + attachment transport router Bundles the Phase 2b/3/4/5 work that accumulated across prior sessions and the new attachment chunking router from this session. Everything ships in one shot so the full mesh surface stays coherent on-wire. Telegram primitives (variants 13–18, 20–22): - Reply / Reaction / ReadReceipt / Forward / Edit / Delete - Presence heartbeat + last-seen tracking - ChannelInvite + ContactCard payload types - MessageKey (sender_pubkey, sender_seq) as cross-transport identity - Action menu, reply banner, edit banner, tombstones, (edited) marker - Debounced auto-read-receipts on scroll + message arrival Activated prototypes (Phase 4): - PsbtHash send RPC - Contacts CRUD (in-memory alias/notes/pinned/blocked) - Outbox 📤 badge, rotate-prekeys button - Chunked send fallback (MCIIXXTT framing) as auto-failover inside send_typed_wire when a typed wire exceeds the LoRa per-frame budget Unified inbox (Phase 1): - conversations.list + conversations.messages RPCs (UI collapse deferred) Attachment transport router (new this session): - ContentInline variant 23 + ContentInlinePayload carrying file bytes directly in the envelope for small files with no Tor path - mesh.send-content-inline RPC — mirrors to local BlobStore, rides send_typed_wire which auto-chunks over MCIIXXTT framing (~2.3 KB cap) - mesh.transport-advice RPC as single source of truth for tier decisions: auto-mesh / choose / tor-only / impossible - Receive arm writes inline bytes to local BlobStore so the existing content_ref card renderer handles both transports uniformly - MeshState.blob_store field + order-independent propagation from RpcHandler::set_blob_store / set_mesh_service - Frontend handleAttachFile calls advice first, branches into silent auto-send, transport-chooser modal, Tor-only path, or red error - Transport modal with 📡 mesh / 🧅 Tor options + ETA + disabled state when peer has no Tor reachability Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 20:40:19 -04:00
/* Transport chooser modal (attachment size router) */
.mesh-transport-modal-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,0.6); backdrop-filter: blur(4px); display: flex; align-items: center; justify-content: center; z-index: 1000; }
.mesh-transport-modal { max-width: 420px; width: 92%; padding: 24px; display: flex; flex-direction: column; gap: 14px; }
.mesh-transport-title { margin: 0; font-size: 1.1rem; color: #fff; }
.mesh-transport-sub { margin: 0; color: rgba(255,255,255,0.6); font-size: 0.85rem; overflow-wrap: anywhere; }
.mesh-transport-options { display: flex; flex-direction: column; gap: 10px; margin-top: 6px; }
.mesh-transport-option { display: flex; align-items: center; gap: 12px; padding: 14px 16px; border-radius: 12px; background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.12); color: #fff; cursor: pointer; text-align: left; transition: background 0.15s ease, border-color 0.15s ease, transform 0.1s ease; }
.mesh-transport-option:hover:not(:disabled) { background: rgba(255,255,255,0.1); border-color: rgba(255,255,255,0.25); transform: translateY(-1px); }
.mesh-transport-option:disabled { opacity: 0.4; cursor: not-allowed; }
.mesh-transport-icon { font-size: 1.5rem; flex: 0 0 auto; }
.mesh-transport-label { flex: 1 1 auto; font-weight: 600; }
.mesh-transport-meta { flex: 0 0 auto; font-size: 0.75rem; color: rgba(255,255,255,0.5); }
.mesh-transport-cancel { margin-top: 4px; padding: 8px; background: transparent; border: none; color: rgba(255,255,255,0.5); cursor: pointer; font-size: 0.85rem; }
.mesh-transport-cancel:hover { color: #fff; }