@gotcos/glasses-server 7.0.18 → 7.0.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13,6 +13,42 @@
13
13
  <link rel="icon" href="/control/mark.svg" type="image/svg+xml">
14
14
  <!-- iOS "Add to Home Screen" ignores SVG icons: without this, a saved Control Web shortcut is a grey letter. -->
15
15
  <link rel="apple-touch-icon" href="/control/apple-touch-icon.png">
16
+ <!-- 7.0.28: the sign-in card and the first paint must look right before, or without, the 10 stylesheets below
17
+ (Miles 19:28: "Connect this browser" with no styling, the mark twice at full size, when the phone opened the page
18
+ before it could reach the Mac). These rules are copies of app.css, word for word (a test compares them), so the
19
+ stylesheets that follow change nothing when they arrive; only the tokens it needs, and the dark set also follows
20
+ the system when theme.js has not run. theme.js retries a stylesheet that failed. -->
21
+ <style id="critical-css">
22
+ :root { --page: #f5f0e6; --ink: #1f1712; --sub: #66635e; --panel: #ffffff; --panel-border: #e3d9c6; --accent: #89662d; --on-accent: #ffffff; --btn-bg: #ffffff; --btn-border: #a08a66; --grey: #5c5a55; --display: 'Fraunces', 'Iowan Old Style', Georgia, serif; --body: 'DM Sans', -apple-system, BlinkMacSystemFont, 'Helvetica Neue', sans-serif; --gutter: 16px; --radius: 12px; --safe-top: env(safe-area-inset-top, 0px); --safe-bottom: env(safe-area-inset-bottom, 0px); }
23
+ :root[data-theme='dark'] { --page: #17120e; --ink: #efe7dc; --sub: #b3a796; --panel: #221b15; --panel-border: #3a2f25; --accent: #d9b06a; --on-accent: #17120e; --btn-bg: #221b15; --btn-border: #7a6650; --grey: #a39887; }
24
+ @media (prefers-color-scheme: dark) { :root:not([data-theme]) { --page: #17120e; --ink: #efe7dc; --sub: #b3a796; --panel: #221b15; --panel-border: #3a2f25; --accent: #d9b06a; --on-accent: #17120e; --btn-bg: #221b15; --btn-border: #7a6650; --grey: #a39887; } }
25
+ *, *::before, *::after { box-sizing: border-box; }
26
+ html { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; background: var(--page); }
27
+ body { margin: 0; min-height: 100dvh; background: var(--page); color: var(--ink); font: 400 16px/1.45 var(--body); -webkit-font-smoothing: antialiased; -webkit-tap-highlight-color: transparent; }
28
+ button, input, select, textarea { font: inherit; color: inherit; }
29
+ button { cursor: pointer; background: none; border: 0; padding: 0; text-align: inherit; }
30
+ .hidden { display: none !important; }
31
+ .mark { width: 28px; height: 28px; color: var(--ink); flex: none; }
32
+ .first-paint { display: none; }
33
+ html.fp-wait .first-paint { position: fixed; inset: 0; z-index: 70; display: grid; place-items: center; background: var(--page); color: var(--ink); pointer-events: none; animation: fp-failsafe 1ms linear 25s forwards; }
34
+ .first-paint .mark { width: 44px; height: 44px; }
35
+ @keyframes fp-failsafe { to { visibility: hidden; } }
36
+ .noscript-note { margin: 0; min-height: 100dvh; display: grid; place-items: center; padding: 24px var(--gutter); font-size: 17px; color: var(--ink); text-align: center; }
37
+ .auth-gate { min-height: 100dvh; display: grid; place-items: center; padding: calc(var(--safe-top) + 24px) var(--gutter) calc(var(--safe-bottom) + 24px); }
38
+ .auth-card { width: 100%; max-width: 420px; }
39
+ .auth-card .mark { width: 44px; height: 44px; margin-bottom: 22px; }
40
+ .auth-card h1 { font: 500 40px/1.05 var(--display); margin: 0 0 10px; letter-spacing: -0.01em; }
41
+ .auth-card p { color: var(--sub); margin: 0 0 24px; }
42
+ .auth-card label { display: block; font-weight: 600; font-size: 15px; margin-bottom: 8px; }
43
+ .auth-card .btn { margin-top: 12px; }
44
+ .auth-foot { font-size: 14px; color: var(--sub); margin-top: 18px; }
45
+ .auth-offline p { color: var(--sub); }
46
+ .field { width: 100%; min-height: 48px; padding: 12px 14px; border-radius: var(--radius); border: 1px solid var(--panel-border); background: var(--panel); font-size: 16px; /* 16px keeps iOS Safari from zooming on focus */ }
47
+ .btn { display: inline-flex; align-items: center; justify-content: center; gap: 8px; min-height: 44px; padding: 0 18px; border-radius: var(--radius); border: 1px solid var(--btn-border); background: var(--btn-bg); font-weight: 600; font-size: 15px; white-space: nowrap; transition: transform 120ms ease; }
48
+ .btn.primary { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
49
+ .btn.block { width: 100%; }
50
+ .link-btn { color: var(--accent); font-weight: 600; font-size: 15px; min-height: 44px; display: inline-flex; align-items: center; }
51
+ </style>
16
52
  <link rel="stylesheet" href="/control/app.css">
17
53
  <link rel="stylesheet" href="/control/control.css">
18
54
  <link rel="stylesheet" href="/control/panes/speakers.css">
@@ -24,6 +60,14 @@
24
60
  <link rel="stylesheet" href="/control/panes/memories.css">
25
61
  <link rel="stylesheet" href="/control/panes/today-actions.css">
26
62
  <script src="/control/theme.js"></script>
63
+ <!-- The app's scripts start downloading now, while the page parses; theme.js runs them after the first paint
64
+ (#boot-scripts, the same list). -->
65
+ <link rel="preload" href="/control/vendor/qrcode-encoder.js" as="script">
66
+ <link rel="preload" href="/control/memory-review.js" as="script">
67
+ <link rel="preload" href="/control/app.js" as="script">
68
+ <link rel="preload" href="/control/shell.js" as="script">
69
+ <link rel="preload" href="/control/panes/cos-chat.js" as="script">
70
+ <link rel="preload" href="/control/panes/questions.js" as="script">
27
71
  </head>
28
72
  <body>
29
73
  <svg width="0" height="0" style="position:absolute" aria-hidden="true">
@@ -53,13 +97,19 @@
53
97
  <symbol id="i-link" viewBox="0 0 24 24"><path d="M10 14a4 4 0 0 0 5.7 0l3-3a4 4 0 0 0-5.7-5.7l-1 1M14 10a4 4 0 0 0-5.7 0l-3 3a4 4 0 0 0 5.7 5.7l1-1"/></symbol>
54
98
  </svg>
55
99
 
100
+ <!-- 7.0.19: the first paint. Shows at once, before any script or answer from the Mac: the mark on the page's own
101
+ background, true whichever comes next (the sign-in card or the app). Out of the layout (fixed), so nothing moves
102
+ when it goes. app.js removes it; a CSS failsafe hides it after 25 s if the scripts never run. -->
103
+ <div id="first-paint" class="first-paint" aria-hidden="true"><svg class="mark" width="44" height="44"><use href="#i-mark"/></svg></div>
104
+ <noscript><p class="noscript-note">COS Control Web needs JavaScript. Turn it on for this site, then reload.</p></noscript>
105
+
56
106
  <main id="auth-gate" class="auth-gate hidden" aria-labelledby="auth-title">
57
107
  <div class="auth-card">
58
- <svg class="mark" aria-hidden="true"><use href="#i-mark"/></svg>
108
+ <svg class="mark" width="44" height="44" aria-hidden="true"><use href="#i-mark"/></svg>
59
109
  <h1 id="auth-title">Connect this browser</h1>
60
110
  <p id="auth-intro">COS runs on your Mac. Sign in once. You stay signed in while you use COS here, for up to 30 days.</p>
61
111
  <div id="auth-offline" class="auth-offline hidden" role="status">
62
- <p id="auth-offline-text">Your Mac may be asleep, or this device may be offline or off Tailscale. This page checks again every 30 seconds.</p>
112
+ <p id="auth-offline-text">Your Mac may be asleep, or this device may be offline or off Tailscale. This page checks again in a few seconds, then every 30 seconds.</p>
63
113
  <button type="button" class="btn primary block" id="auth-retry">Try again</button>
64
114
  </div>
65
115
  <form id="auth-form">
@@ -84,7 +134,7 @@
84
134
 
85
135
  <div id="app-shell" class="app-shell hidden">
86
136
  <nav class="rail" aria-label="Sections">
87
- <div class="rail-head"><svg class="mark" aria-hidden="true"><use href="#i-mark"/></svg><span class="rail-brand">Control</span></div>
137
+ <div class="rail-head"><svg class="mark" width="28" height="28" aria-hidden="true"><use href="#i-mark"/></svg><span class="rail-brand">Control</span></div>
88
138
  <button type="button" class="rail-btn rail-today" data-pane="today" aria-label="Today"><svg class="icon"><use href="#i-today"/></svg><span class="rail-text"><span class="rail-label">Today</span><span class="rail-story" data-story="today"></span></span><span class="badge-dot hidden" data-badge="today"></span></button>
89
139
  <div class="rail-sep"></div>
90
140
  <button type="button" class="rail-btn" data-pane="messages" aria-label="Messages"><svg class="icon"><use href="#i-messages"/></svg><span class="rail-text"><span class="rail-label">Messages</span><span class="rail-story" data-story="messages"></span></span><span class="badge-dot hidden" data-badge="messages"></span></button>
@@ -98,6 +148,9 @@
98
148
  <div class="rail-spacer"></div>
99
149
  <button type="button" class="rail-settings" data-pane="settings" aria-label="Settings"><svg class="icon"><use href="#i-settings"/></svg><span class="rail-label">Settings</span></button>
100
150
  <p class="server-line" data-server-line><span class="dot ok" data-host-dot></span><span class="server-text" data-server-text>Server connected</span></p>
151
+ <!-- 7.0.29: the folded Duo's side rail only (app.css, html.short-screen): the top bar's Back, at thumb height. Shown
152
+ where the top bar shows Back today, on a reader, and it calls the same goBack. Never drawn from 600 px. -->
153
+ <button type="button" class="rail-back" id="rail-back" aria-label="Back" hidden><svg class="icon"><use href="#i-back"/></svg></button>
101
154
  </nav>
102
155
 
103
156
  <div class="main-col">
@@ -113,7 +166,7 @@
113
166
  <div class="topbar-left">
114
167
  <button type="button" id="topbar-back" class="topbar-back hidden"><svg class="icon"><use href="#i-back"/></svg><span id="topbar-back-label">Back</span></button>
115
168
  <button type="button" id="section-btn" class="section-btn" aria-haspopup="dialog" aria-controls="section-menu">
116
- <svg class="mark" aria-hidden="true"><use href="#i-mark"/></svg><span id="section-name">Today</span><span class="caret" aria-hidden="true"></span>
169
+ <svg class="mark" width="28" height="28" aria-hidden="true"><use href="#i-mark"/></svg><span id="section-name">Today</span><span class="caret" aria-hidden="true"></span>
117
170
  </button>
118
171
  </div>
119
172
  <button type="button" class="icon-btn" data-open-ask aria-label="Ask COS or find a session"><svg class="icon"><use href="#i-ask"/></svg></button>
@@ -197,16 +250,29 @@
197
250
  <div class="cx-controls sx-views" id="sessions-views"></div>
198
251
  <div class="split sx-split" id="sessions-split">
199
252
  <div class="split-list" id="sessions-list-col">
200
- <div class="sx-tools">
201
- <form class="cx-search sx-search" id="sessions-search-form" role="search">
202
- <input id="sessions-q" class="field" type="search" placeholder="Search titles, transcripts…" enterkeyhint="search" autocomplete="off" aria-label="Search titles, transcripts">
203
- <button type="button" class="cx-clear hidden" id="sessions-clear" data-sx="clear" aria-label="Clear search">Clear</button>
204
- </form>
205
- <select id="sessions-recency" class="field sx-select" aria-label="Recency">
206
- <option value="newest">Newest</option>
207
- <option value="oldest">Oldest</option>
208
- <option value="match">Best match</option>
209
- </select>
253
+ <!-- 7.0.23 step 2b: the list's own head (DESIGN_PLAN v2 "The list"): the Sessions menu, Search, one quiet line and the sort menu. -->
254
+ <div class="sx-lhead" id="sessions-lhead">
255
+ <div class="sx-lh-top">
256
+ <button type="button" class="sx-view-btn" id="sessions-view-btn" data-sx="view-menu" aria-haspopup="menu" aria-expanded="false" aria-controls="sessions-view-menu"><span>Sessions</span><span class="caret" aria-hidden="true"></span></button>
257
+ <div class="sx-menu hidden" id="sessions-view-menu" role="menu" aria-label="Sessions">
258
+ <button type="button" role="menuitem" data-sx="view" data-view="sessions">Sessions</button>
259
+ <button type="button" role="menuitem" data-sx="view" data-view="usage">Usage</button>
260
+ <button type="button" role="menuitem" data-sx="view" data-view="cost">COS cost</button>
261
+ <button type="button" role="menuitem" data-sx="refresh">Refresh</button>
262
+ </div>
263
+ </div>
264
+ <div class="sx-tools">
265
+ <form class="cx-search sx-search" id="sessions-search-form" role="search">
266
+ <input id="sessions-q" class="field" type="search" placeholder="Search titles and transcripts" enterkeyhint="search" autocomplete="off" aria-label="Search titles and transcripts">
267
+ <button type="button" class="cx-clear hidden" id="sessions-clear" data-sx="clear" aria-label="Clear search">Clear</button>
268
+ </form>
269
+ </div>
270
+ <div class="sx-tools sx-tools-line">
271
+ <p class="sx-quiet hidden" id="sessions-quiet"></p>
272
+ <button type="button" class="cx-btn quiet sx-new-folder hidden" id="sessions-new-folder" data-sx="new-folder">New folder</button>
273
+ <select id="sessions-sort" class="field sx-select" aria-label="Sort"><option value="updated">Updated</option><option value="opened">Opened</option></select>
274
+ </div>
275
+ <p class="sx-org-note hidden" id="sessions-org-note" role="status"></p>
210
276
  </div>
211
277
  <p class="cx-note sx-hint hidden" id="sessions-hint"></p>
212
278
  <div id="sessions-hooks"></div>
@@ -281,12 +347,9 @@
281
347
 
282
348
  <div id="toast" class="toast hidden" role="status"></div>
283
349
 
284
- <script src="/control/vendor/qrcode-encoder.js"></script>
285
- <script src="/control/memory-review.js"></script>
286
- <script src="/control/app.js"></script>
287
- <script src="/control/shell.js"></script>
288
- <script src="/control/panes/cos-chat.js"></script>
289
- <script src="/control/panes/questions.js"></script>
350
+ <!-- The app's scripts, in the order they run. theme.js adds them one frame after the page is drawn, so the first paint
351
+ (#first-paint) never waits for them, and runs boot only once every one of them has run (W1_first_open_speed.md). -->
352
+ <script type="application/json" id="boot-scripts">["/control/vendor/qrcode-encoder.js", "/control/memory-review.js", "/control/app.js", "/control/shell.js", "/control/panes/cos-chat.js", "/control/panes/questions.js"]</script>
290
353
  <!-- Loaded when first opened, then in the background once the page is idle (app.js ensurePane). -->
291
354
  <script type="application/json" id="lazy-panes">{
292
355
  "messages": "/control/panes/messages.js",
@@ -492,3 +492,283 @@
492
492
  .u-breakdown summary::-webkit-details-marker { display: none; }
493
493
  .u-breakdown summary .u-chev { color: var(--accent); transform: rotate(-90deg); transition: transform 120ms ease; }
494
494
  .u-breakdown[open] summary .u-chev { transform: none; }
495
+
496
+ /* ── the frame (7.0.23, step 1 of the chat-style Sessions; DESIGN_PLAN v2 "The frame") ──────────────────────────────
497
+ While Sessions shows its list and reader (app.js sxFrameSync sets html.sx-frame), the page is one screen, never
498
+ taller: the app shell is the visible box the keyboard kit publishes (--vv-top, --vv-h from viewportFit, as kb-chat
499
+ uses them), so the document has nothing left to scroll. The rail, the list and the reader's body each scroll by
500
+ themselves; the reader's header stays at its top and the dock (activity with Stop, the Mac's question, the question
501
+ cards, the reply box, its status and the queue) stays at its bottom. Layout, not a proven fix for the 20:16 report:
502
+ app.js sxFrameCrumb reports where the parts sit against the visible area. In a Safari tab (not the Home Screen app)
503
+ a fixed shell ends above Safari's own bottom bar; the app has no such bar. */
504
+ html.sx-frame, html.sx-frame body { height: 100%; overflow: hidden; overscroll-behavior: none; }
505
+ html.sx-frame body { min-height: 0; }
506
+ html.sx-frame .app-shell {
507
+ position: fixed; left: 0; right: 0; top: var(--vv-top, 0px); height: var(--vv-h, 100dvh); min-height: 0;
508
+ display: flex; flex-direction: column; overflow: hidden;
509
+ }
510
+ html.sx-frame .main-col { flex: 1 1 auto; min-height: 0; min-width: 0; display: flex; flex-direction: column; overflow: hidden; }
511
+ html.sx-frame .topbar, html.sx-frame .navbar { flex: none; position: relative; }
512
+ html.sx-frame .workspace {
513
+ flex: 1 1 auto; min-height: 0; min-width: 0; display: flex; flex-direction: column; overflow: hidden;
514
+ padding-bottom: calc(var(--safe-bottom) + 8px);
515
+ }
516
+ html.sx-frame #pane-sessions { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; width: 100%; }
517
+ /* What sits over every pane (the Mac not answering, the sign-in ending, a session's waiting question cards) keeps its
518
+ place above Sessions, but never takes the screen: past 35 percent it scrolls by itself. */
519
+ html.sx-frame #conn-notice, html.sx-frame #sq-host, html.sx-frame .workspace > :not(.pane) { flex: none; }
520
+ html.sx-frame #sq-host { max-height: 35%; overflow: auto; overscroll-behavior: contain; margin-bottom: 10px; }
521
+ /* In the frame a waiting question is its one line at every width (questions.css shows it so under 600 px): a tap opens
522
+ the cards. The open session's own cards sit in the reader's dock either way. */
523
+ html.sx-frame .sq-host .sq-banner { display: flex; align-items: center; gap: 10px; width: 100%; min-height: 44px; padding: 10px 14px; border-radius: 14px;
524
+ background: var(--panel); border: 1px solid color-mix(in srgb, var(--hot) 45%, var(--panel-border)); color: var(--ink); font: 600 15px/1.3 var(--body); text-align: left; }
525
+ html.sx-frame .sq-host .sq-banner-text { flex: 1; min-width: 0; }
526
+ html.sx-frame .sq-host .sq-banner-act { flex: none; color: var(--accent); }
527
+ html.sx-frame .sq-host .sq-cards:not(.open) { display: none; }
528
+ html.sx-frame #pane-sessions > .sx { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
529
+ html.sx-frame .sx-hero, html.sx-frame .sx-views { flex: none; }
530
+ /* In the frame the Sessions header is one line (the same title, sources line, counts and Refresh), so the list and the
531
+ reader keep the height; the view switch keeps its own row. */
532
+ html.sx-frame .sx-hero { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 16px; padding-top: 0; padding-bottom: 8px; margin-bottom: 0; }
533
+ html.sx-frame .sx-hero .cx-glyph { display: none; }
534
+ html.sx-frame .sx-hero .cx-hero-text { flex: 1 1 260px; display: flex; align-items: baseline; gap: 12px; min-width: 0; }
535
+ html.sx-frame .sx-hero .cx-title { font-size: 22px; flex: none; }
536
+ html.sx-frame .sx-hero .cx-detail-line { margin: 0; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
537
+ html.sx-frame .sx-hero .cx-stats { order: 2; margin-top: 0; flex: none; }
538
+ html.sx-frame .sx-hero .cx-actions { order: 3; flex: none; }
539
+ html.sx-frame .sx-views { margin: 4px 0 8px; }
540
+ /* The reader's header in one row: the badge, the title on one line, Hide list; the subtitle is one line under it. */
541
+ html.sx-frame .sx-head-line { flex-wrap: nowrap; }
542
+ html.sx-frame .sx-reader-title { min-width: 0; flex: 1 1 auto; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: 20px; }
543
+ html.sx-frame .sx-subtitle { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 4px; }
544
+ html.sx-frame .sx-reader-head { padding-bottom: 8px; margin-bottom: 0; }
545
+ html.sx-frame .sx-split { flex: 1 1 auto; min-height: 0; margin-top: 0; }
546
+ html.sx-frame .sx-split > .split-list { min-height: 0; overflow: auto; overscroll-behavior: contain; }
547
+ html.sx-frame .sx-split > .sx-reader { min-height: 0; flex-direction: column; overflow: hidden; }
548
+ html.sx-frame .sx-reader-head { flex: none; }
549
+ /* The conversation between the header and the dock; it opens at the newest turn (app.js sxToNewest). */
550
+ .sx-body { min-height: 0; }
551
+ html.sx-frame .sx-body { flex: 1 1 0; overflow: auto; overscroll-behavior: contain; padding-bottom: 8px; }
552
+ /* The dock takes at most 72 percent of the reader (with the keyboard up, what the reader has below its head). 72, not 60:
553
+ at 951 by 539 with files, a queued note and the busy note, 60 left a 33 px window above the pinned reply box, too
554
+ short for a 44 px Cancel (dock_sweep). The reply box and its status always
555
+ show: what can grow above or below them (the activity, the queue) gives way or scrolls inside its own box. */
556
+ html.sx-frame .sx-dock { flex: 0 0 auto; max-height: 72%; min-height: 0; display: flex; flex-direction: column; position: relative;
557
+ padding-top: 8px; border-top: 1px solid var(--hairline); }
558
+ html.sx-frame .sx-dock > .sx-activity, html.sx-frame .sx-dock > .sx-mac-q { flex: none; }
559
+ /* What it is doing now keeps about four lines in the dock and scrolls past that, so it never pushes the reply box out. */
560
+ html.sx-frame .sx-dock > .sx-activity { flex: 0 4 auto; min-height: 32px; max-height: 96px; overflow: auto; }
561
+ /* The reply part never shrinks: a dock capped at 60 percent with overflow hidden would clip the reply box instead (951 by
562
+ 539, Go to the question shown: the box ran 17 px past the screen). What gives way is the activity; the queue inside the
563
+ reply part has its own cap and scrolls. */
564
+ html.sx-frame .sx-dock .sx-chat { margin-top: 0; flex: none; display: flex; flex-direction: column; }
565
+ html.sx-frame .sx-dock .sx-chat > * { flex: none; }
566
+ /* Go to the question sits on the dock's top edge, over the last line of the conversation, so it takes no room from the
567
+ dock at any height (a dock line squeezed its own 44 px button on short screens). */
568
+ html.sx-frame #session-reader > .sx-goq { position: absolute; right: 8px; left: auto; bottom: calc(var(--sx-dock-h, 120px) + 6px); margin: 0; z-index: 2;
569
+ padding: 0; border: 0; background: transparent; }
570
+ /* Only the button shows there, so it covers the end of one line, not the conversation; the sentence stays for screen
571
+ readers. */
572
+ html.sx-frame #session-reader > .sx-goq > span { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
573
+ html.sx-frame #session-reader > .sx-goq .cx-btn { background: var(--panel); border-color: color-mix(in srgb, var(--accent) 55%, var(--panel-border));
574
+ box-shadow: 0 6px 18px rgba(0, 0, 0, 0.25); }
575
+ /* The session's question cards end the conversation (app.js openSession), so they scroll with it; their own footer
576
+ (Submit) sticks to the bottom of the conversation the way it sticks to the page elsewhere. */
577
+ html.sx-frame .sx-body #session-questions:not(:empty) { margin-top: 12px; }
578
+ /* An empty status line under the reply box takes no room in the frame (it keeps its 20 px when it says something). */
579
+ html.sx-frame .sx-dock #session-status:empty { min-height: 0; margin: 0; }
580
+ html.sx-frame .sx-dock #session-queue { flex: none; max-height: none; overflow: visible; }
581
+ /* With files in the reply box (7.0.23 regression, attach5_check): the chip row wrapped and grew (5 chips at 1119 wide
582
+ were 221 px), the reply form spilled past the reader's clipped bottom, and Send sat below the screen with nothing to
583
+ scroll (2 files on the open Duo; 1 file with the keyboard up at 449). The chip row now scrolls inside two rows (one
584
+ while typing), and the dock scrolls by itself as a last resort, so Send can always be reached. */
585
+ /* THE DOCK RULE (lead 08:47, after three overflows: the reply box at 951 by 539, the sign-in screen, Send under files):
586
+ the dock never exceeds the visible height (it is capped inside the reader, which is inside the visible box); its
587
+ inside scrolls; and the reply box, Send included, is always in view, pinned to the dock's bottom while what is above
588
+ it scrolls. dock_sweep.cjs checks every combination the dock can hold at five sizes, with and without the keyboard. */
589
+ html.sx-frame #session-reader { position: relative; }
590
+ html.sx-frame .sx-dock { overflow-y: auto; overscroll-behavior: contain; }
591
+ /* The reply part's own box is dissolved into the dock (display: contents) so the reply box can stick to the dock's
592
+ bottom: a sticky box cannot leave its parent, and .sx-chat also holds the queue. The reply box goes last; the
593
+ busy note, the status line and the queue sit above it and scroll with the dock. Its own rows are capped against the
594
+ visible height (--vv-h), so it always fits in the dock: the chip row, then the draft, each scroll inside. */
595
+ html.sx-frame .sx-dock .sx-chat { display: contents; }
596
+ html.sx-frame .sx-dock #session-form { order: 10; position: sticky; bottom: 0; z-index: 1; background: var(--page); }
597
+ html.sx-frame .sx-dock #session-input { max-height: min(140px, calc(var(--vv-h, 600px) * 0.1)); }
598
+ /* A swipe that reaches the end of the activity or the chip row carries on into the dock (no overscroll containment):
599
+ a swipe over the activity line used to stop there and never move the dock. */
600
+ html.sx-frame .sx-dock #session-files:not(.hidden) { max-height: max(52px, min(104px, calc(var(--vv-h, 600px) * 0.1))); overflow-y: auto; }
601
+ html.sx-frame.kb-up .sx-dock #session-files:not(.hidden) { max-height: 52px; }
602
+ html.sx-frame.kb-up .sx-dock #session-input { max-height: min(4.5em, calc(var(--vv-h, 600px) * 0.17)); }
603
+
604
+ /* Phone and narrow (under 760): one column at a time, the list or the reader, each filling the frame. */
605
+ @media (max-width: 759px) {
606
+ html.sx-frame .sx-split { display: flex; flex-direction: column; }
607
+ html.sx-frame .sx-split > .split-list { flex: 1 1 auto; }
608
+ html.sx-frame .sx-split.reading > .sx-reader { flex: 1 1 auto; display: flex; }
609
+ }
610
+ /* From 600: the rail is a column of the shell and scrolls by itself (its labels from 1100 wide and 721 tall, control.css). */
611
+ @media (min-width: 600px) {
612
+ html.sx-frame .app-shell { display: grid; grid-template-columns: 74px minmax(0, 1fr); grid-template-rows: minmax(0, 1fr); }
613
+ html.sx-frame .rail { position: static; top: auto; height: auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain; }
614
+ }
615
+ @media (min-width: 1100px) and (min-height: 721px) {
616
+ html.sx-frame .app-shell { grid-template-columns: 248px minmax(0, 1fr); }
617
+ }
618
+ /* From 760: the list, the divider and the reader side by side, each the full height of the split. */
619
+ @media (min-width: 760px) {
620
+ html.sx-frame .split.sx-split { grid-template-rows: minmax(0, 1fr); align-items: stretch; }
621
+ html.sx-frame .sx-split > .split-list { position: static; top: auto; max-height: none; height: 100%; }
622
+ /* relative, not static: the divider's 44 px touch area (::before) is placed against it and must stay its own size. */
623
+ html.sx-frame .sx-handle { position: relative; top: auto; height: auto; align-self: stretch; }
624
+ html.sx-frame .sx-split > .sx-reader { display: flex; height: 100%; }
625
+ }
626
+ /* An on-screen keyboard up (app.js html.kb-up): the header goes to one line and the conversation keeps the rest. */
627
+ html.sx-frame.kb-up .sx-hero, html.sx-frame.kb-up .sx-views, html.sx-frame.kb-up .sx-subtitle,
628
+ html.sx-frame.kb-up .navbar, html.sx-frame.kb-up #conn-notice, html.sx-frame.kb-up #sq-host { display: none; }
629
+ /* Short screens (under 600 tall, Comet's 951 x 539): the subtitle and counts step aside. */
630
+ @media (max-height: 599px) {
631
+ /* The notices over the pane (the Mac not answering, the sign-in ending) are one line each: the same words, cut with
632
+ an ellipsis, their buttons kept. */
633
+ html.sx-frame .conn-notice { margin-bottom: 8px; gap: 6px; grid-template-columns: minmax(0, 1fr); }
634
+ html.sx-frame .conn-line { flex-wrap: nowrap; min-width: 0; padding: 6px 10px 6px 14px; }
635
+ html.sx-frame .conn-line > .btn { flex: none; }
636
+ html.sx-frame .conn-line > :not(.btn):not(button) { min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
637
+ html.sx-frame .conn-line .conn-sub { flex: 1 1 0; }
638
+ html.sx-frame #sq-host { margin-bottom: 8px; }
639
+ html.sx-frame .sq-host .sq-banner { min-height: 44px; padding: 6px 14px; }
640
+ html.sx-frame .sx-views { margin: 2px 0 6px; }
641
+ /* The Sessions header (title and Refresh) and the view switch share one row. */
642
+ html.sx-frame #pane-sessions > .sx { display: grid; grid-template-columns: auto minmax(0, 1fr); grid-template-rows: auto minmax(0, 1fr); column-gap: 18px; }
643
+ html.sx-frame .sx-hero { grid-column: 1; grid-row: 1; flex-wrap: nowrap; padding-bottom: 6px; border-bottom: 0; background: none; margin: 0; padding-left: 0; padding-right: 0; }
644
+ html.sx-frame .sx-hero .cx-hero-text { flex: none; }
645
+ html.sx-frame .sx-views { grid-column: 2; grid-row: 1; align-self: center; margin: 0 0 6px; }
646
+ html.sx-frame .sx-split, html.sx-frame .sx-usage { grid-column: 1 / -1; grid-row: 2; }
647
+ html.sx-frame .sx-subtitle, html.sx-frame .sx-hero .cx-stats, html.sx-frame .sx-hero .cx-detail-line { display: none; }
648
+ }
649
+ html.sx-frame.kb-up .sx-reader-head { padding: 2px 0 6px; margin-bottom: 4px; }
650
+ html.sx-frame.kb-up .sx-reader-title { font-size: 17px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
651
+ /* While typing the reply area wins: the dock takes what it needs up to the whole reader below its head, and the
652
+ conversation gives way (it can reach 0 on a very short screen: 382 by 557 with the keyboard up). */
653
+ html.sx-frame.kb-up .sx-dock { max-height: none; flex: 0 1 auto; }
654
+ html.sx-frame.kb-up #session-input { max-height: 4.5em; }
655
+ /* While typing, the dock drops the spacing around the reply box: at 382 by 557 with the keyboard up (267 px visible) the
656
+ dock needed 187 px inside its 155 and the reply box ran 18 px past the screen (MX's step 6 found the same in Messages). */
657
+ html.sx-frame.kb-up .sx-dock { padding-top: 4px; }
658
+ html.sx-frame.kb-up .sx-dock > .sx-activity { margin-bottom: 4px; }
659
+ html.sx-frame.kb-up .sx-dock .sx-chat { padding-top: 0; }
660
+ html.sx-frame.kb-up .sx-dock #session-form { margin-top: 0; }
661
+ /* Touch at any size in the frame: app.css floors controls to 44 px only under 600 wide or 720 tall, so on the open Duo
662
+ (1119 by 787 at 0.85) Copy session was 32 px and the labelled rail's Settings 28 px (MX's step 6 found the same gap). */
663
+ @media (any-pointer: coarse) {
664
+ html.sx-frame .sx-foot .cx-btn, html.sx-frame .rail-settings { min-height: 44px; }
665
+ }
666
+
667
+ /* ── the list (7.0.23, step 2b-1; DESIGN_PLAN v2 "The list", the 23:33 mock) ─────────────────────────────────────────
668
+ The list's own head (the Sessions menu, Search, one quiet line, the sort menu), then groups with plain headings and
669
+ rows divided by hairlines (no boxed cards). A row's state is on its left edge and always in words too; the open row
670
+ has the panel background and a 1 px outline, so selection never rests on colour alone. */
671
+ .sx-lhead { display: grid; gap: 8px; padding: 2px 2px 8px; border-bottom: 1px solid var(--hairline); position: sticky; top: 0; z-index: 3; background: var(--page); }
672
+ /* Under 600 px the top bar already names Sessions; the view switch (Sessions, Usage, COS cost) stays for the other two. */
673
+ @media (max-width: 599px) { .sx-lhead .sx-lh-top { display: none; } }
674
+ @media (min-width: 600px) { .sx-views.sx-wide-hide { display: none; } }
675
+ .sx-lh-top { position: relative; display: flex; align-items: center; justify-content: space-between; gap: 8px; min-width: 0; }
676
+ .sx-view-btn { display: inline-flex; align-items: center; gap: 8px; min-height: 36px; font: 600 18px/1.1 var(--body); color: var(--ink); }
677
+ .sx-view-btn .caret { width: 7px; height: 7px; border-right: 2px solid var(--sub); border-bottom: 2px solid var(--sub); transform: rotate(45deg) translateY(-2px); }
678
+ .sx-menu { position: absolute; top: calc(100% + 4px); left: 0; z-index: 30; min-width: 200px; display: grid; padding: 6px; border-radius: 12px;
679
+ background: var(--panel); border: 1px solid var(--panel-border); box-shadow: 0 12px 32px rgba(0, 0, 0, 0.28); }
680
+ .sx-menu.hidden { display: none; }
681
+ .sx-menu [role='menuitem'] { min-height: 40px; padding: 0 12px; border-radius: 8px; text-align: left; font: 500 15px/1.2 var(--body); color: var(--ink); }
682
+ .sx-menu [role='menuitem']:hover, .sx-menu [role='menuitem']:focus-visible { background: var(--qopen); }
683
+ .sx-lhead .sx-tools { margin: 0; }
684
+ .sx-tools-line { display: flex; align-items: center; gap: 8px; }
685
+ .sx-quiet { margin: 0; flex: 1 1 auto; min-width: 0; font-size: 12.5px; line-height: 1.35; color: var(--sub); }
686
+ .sx-tools-line .sx-select { margin-left: auto; }
687
+
688
+ .sx-groups { padding-bottom: 8px; }
689
+ .sx-gh { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; width: 100%; margin: 0; padding: 14px 12px 6px; font: 600 13px/1.3 var(--body); color: var(--sub); }
690
+ .sx-g:first-child .sx-gh { padding-top: 10px; }
691
+ .sx-gh .n { font: 500 12.5px/1.3 var(--body); color: var(--tertiary); font-variant-numeric: tabular-nums; }
692
+ .sx-gh.state { color: var(--ink); }
693
+ .sx-gh.needs .n { color: var(--accent); }
694
+ .sx-gh.working .n { color: var(--run-green); }
695
+ button.sx-gh { align-items: center; text-align: left; min-height: 36px; }
696
+ button.sx-gh:hover { color: var(--ink); }
697
+ .sx-gh .fold { display: inline-flex; align-items: center; gap: 8px; }
698
+ .sx-gh .chev { width: 7px; height: 7px; border-right: 1.6px solid currentColor; border-bottom: 1.6px solid currentColor; transform: rotate(45deg) translate(-2px, -1px); }
699
+ button.sx-gh[aria-expanded='false'] .chev { transform: rotate(-45deg); }
700
+ .sx-rows { display: grid; }
701
+ .sx-r { position: relative; display: grid; grid-template-columns: minmax(0, 1fr); border-radius: 10px; }
702
+ .sx-rows > .sx-r + .sx-r::before { content: ''; position: absolute; top: 0; left: 12px; right: 0; height: 1px; background: var(--hairline); }
703
+ .sx-r:hover { background: color-mix(in srgb, var(--panel) 55%, transparent); }
704
+ .sx-r[aria-selected='true'] { background: var(--panel); box-shadow: inset 0 0 0 1px var(--btn-border); }
705
+ .sx-r[aria-selected='true']::before, .sx-r[aria-selected='true'] + .sx-r::before { background: transparent !important; }
706
+ .sx-r-edge { position: absolute; left: 0; top: 6px; bottom: 6px; width: 3px; border-radius: 2px; }
707
+ .sx-r.running .sx-r-edge { background: var(--run-green); }
708
+ .sx-r.waiting .sx-r-edge { background: var(--accent); }
709
+ .sx-r.failed .sx-r-edge { background: var(--hot); }
710
+ .sx-r-open { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 3px 10px; align-items: baseline; min-width: 0; min-height: 56px; padding: 9px 12px 9px 12px; text-align: left; color: inherit; width: 100%; }
711
+ .sx-r-open:focus-visible { outline-offset: -2px; }
712
+ .sx-r-title { font: 600 14px/1.3 var(--body); color: var(--ink); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
713
+ .sx-r-time { font-size: 12px; line-height: 1.3; color: var(--sub); white-space: nowrap; font-variant-numeric: tabular-nums; }
714
+ .sx-r-sub { grid-column: 1 / -1; display: flex; align-items: center; gap: 7px; min-width: 0; font-size: 12.5px; line-height: 1.35; color: var(--sub); }
715
+ .sx-r-sub .sx-glyph { width: 18px; height: 18px; flex: none; }
716
+ .sx-r-sub .t { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
717
+ .sx-r-sub .t b.failed { color: var(--hot); font-weight: 600; }
718
+ .sx-r-snip { grid-column: 1 / -1; font-size: 12.5px; line-height: 1.4; color: var(--sub); display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
719
+ .sx-tag { flex: none; display: inline-flex; align-items: center; height: 18px; padding: 0 6px; border-radius: 5px; border: 1px solid var(--panel-border); font: 600 11px/1 var(--body); color: var(--ink); }
720
+ .sx-r.compact .sx-r-open { min-height: 40px; padding-top: 6px; padding-bottom: 6px; align-items: center; }
721
+ .sx-r.compact .sx-r-title { font-weight: 500; }
722
+ .sx-more-older { margin: 6px 12px 0; }
723
+ /* Touch: every target in the list reaches 44 px. */
724
+ @media (any-pointer: coarse) {
725
+ .sx-r-open { min-height: 58px; }
726
+ .sx-r.compact .sx-r-open { min-height: 44px; }
727
+ button.sx-gh, .sx-view-btn, .sx-menu [role='menuitem'] { min-height: 44px; }
728
+ .sx-lhead #sessions-q, .sx-lhead #sessions-sort { height: 44px; min-height: 44px; }
729
+ }
730
+
731
+ /* ── folders, COS pins and rename (7.0.23, step 2b-2) ── */
732
+ .sx-r { grid-template-columns: minmax(0, 1fr) auto; align-items: center; }
733
+ .sx-r-more { width: 32px; height: 32px; margin-right: 6px; border-radius: 8px; display: grid; place-items: center; color: var(--sub); font: 600 18px/1 var(--body); }
734
+ .sx-r-more:hover, .sx-r-more[aria-expanded='true'] { background: var(--qopen); color: var(--ink); }
735
+ .sx-r-orig { grid-column: 1 / -1; font-size: 12px; line-height: 1.3; color: var(--tertiary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
736
+ .sx-gh-row { display: flex; align-items: center; }
737
+ .sx-gh-row .sx-gh { flex: 1 1 auto; }
738
+ .sx-empty-folder { margin: 2px 12px 8px; font-size: 13px; color: var(--sub); }
739
+ .sx-new-folder { flex: none; }
740
+ .sx-org-note { margin: 0; font-size: 13px; color: var(--hot); }
741
+
742
+ /* ── the list fits its column at every width (7.0.27; Miles 13:01 on 7.0.26, the open Duo) ──────────────────────────
743
+ THE RULE: at every list width from the divider's minimum (240 px) to its maximum the list's content fits the column, so
744
+ the column never scrolls sideways and nothing in it is cut. The head is one grid column that grows to its widest
745
+ item's min-content width; the reach line, New folder and the sort menu on one unwrapped row measured 272 px at the
746
+ minimum, so the head, the search field with it, and the column were 32 px too wide and the column panned on x. Now
747
+ the reach line has its own full-width row and New folder and the sort menu share the next one, wrapping under each
748
+ other if they must. A folder heading may shrink below its name, so a long folder name never widens the list, and
749
+ it ends in an ellipsis like a title (rows already could). Nothing is hidden to make it fit: width_sweep.cjs
750
+ drags the real divider from its minimum to its maximum and checks the fit (each rule here is killed there). */
751
+ .sx-tools-line { flex-wrap: wrap; row-gap: 6px; }
752
+ .sx-tools-line .sx-quiet { flex: 1 0 100%; }
753
+ .sx-gh-row .sx-gh, .sx-gh .fold { min-width: 0; }
754
+ .sx-gh .fold .nm { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
755
+ .sx-org-note.hidden { display: none; }
756
+ .sx-inline { display: grid; gap: 8px; padding: 10px 12px 12px; margin: 6px 0; border-radius: 10px; background: var(--qopen); }
757
+ .sx-inline-label { font: 600 13px/1.3 var(--body); color: var(--ink); }
758
+ .sx-inline-hint { margin: 0; font-size: 12.5px; color: var(--sub); }
759
+ .sx-inline-acts { display: flex; flex-wrap: wrap; gap: 8px; }
760
+ .sx-pop { position: fixed; z-index: 60; max-width: min(320px, calc(100vw - 16px)); }
761
+ .sx-pop .sx-pop-note { margin: 0; padding: 8px 12px; font-size: 13px; color: var(--sub); }
762
+ .sx-pop [role='menuitemradio'] { min-height: 40px; padding: 0 12px; border-radius: 8px; text-align: left; font: 500 15px/1.2 var(--body); color: var(--ink); }
763
+ .sx-pop [role='menuitemradio'][aria-checked='true']::after { content: ' (here)'; color: var(--sub); }
764
+ .sx-pop [role='menuitemradio']:hover, .sx-pop [role='menuitemradio']:focus-visible { background: var(--qopen); }
765
+ @media (any-pointer: coarse) {
766
+ .sx-r-more { width: 44px; height: 44px; }
767
+ .sx-pop [role='menuitemradio'], .sx-pop [role='menuitem'], .sx-new-folder, .sx-inline .cx-btn { min-height: 44px; }
768
+ }
769
+
770
+ /* Q1 (23:40): a question waits at the end of the conversation, out of view. */
771
+ .sx-goq { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin: 0 0 8px; padding: 6px 6px 6px 12px; border-radius: 10px;
772
+ border: 1px solid color-mix(in srgb, var(--accent) 45%, var(--panel-border)); background: var(--panel); font-size: 14px; color: var(--ink); }
773
+ .sx-goq[hidden] { display: none; }
774
+ @media (any-pointer: coarse) { .sx-goq .cx-btn { min-height: 44px; } }
@@ -267,6 +267,7 @@
267
267
  if (window.requestAnimationFrame) window.requestAnimationFrame(go); else go();
268
268
  }
269
269
  function fit() {
270
+ if (O.open) panCheck();
270
271
  if (KB.frame || !O.open) return;
271
272
  if (!window.requestAnimationFrame) return applyFit(); // every browser with visualViewport has frames
272
273
  KB.frame = window.requestAnimationFrame(applyFit);
@@ -321,6 +322,49 @@
321
322
  // opt.from: a session opened from the pet's list ({ scroll }), which gets Back to sessions and its own history entry,
322
323
  // so the phone's Back returns to the list too (Miles's iPhone, 11/07). opt.again: the list shown again from there,
323
324
  // its entry already current.
325
+ // Miles 11/10 20:16 (IMG_0570): with the panel open on his Duo, the panel and the page under it moved together. Two
326
+ // stories fit: the page scrolled behind the panel, or the visible area panned past the layout viewport (the page
327
+ // drawn at 0.85). These numbers tell them apart, so nothing is fixed on a guess (lead 20:26): while the panel is open,
328
+ // ONE layout report per open when window.scrollY, visualViewport.pageTop or visualViewport.offsetTop first changes,
329
+ // with the values at open and now, the panel's top and bottom, and whether a keyboard was up. layoutCrumb adds
330
+ // innerHeight (ih), the visible height (vvh) and scale (vvs) itself. Numbers only, inside layoutCrumb's caps.
331
+ var PAN = { at: null, sent: false, fn: null };
332
+ function panNum(v) { return typeof v === 'number' && isFinite(v) ? Math.round(v) : null; }
333
+ function panOffsets() {
334
+ var vv = window.visualViewport;
335
+ return { sy: panNum(window.scrollY), pt: vv ? panNum(vv.pageTop) : null, ot: vv ? panNum(vv.offsetTop) : null,
336
+ sx: panNum(window.scrollX), pl: vv ? panNum(vv.pageLeft) : null, ol: vv ? panNum(vv.offsetLeft) : null };
337
+ }
338
+ function panState() {
339
+ var r = layer && typeof layer.getBoundingClientRect === 'function' ? layer.getBoundingClientRect() : null, o = panOffsets();
340
+ var rail = document.querySelector ? document.querySelector('.rail') : null, rr = rail && rail.getBoundingClientRect ? rail.getBoundingClientRect() : null;
341
+ o.pTop = r ? panNum(r.top) : null; o.pBot = r ? panNum(r.bottom) : null; o.pL = r ? panNum(r.left) : null; o.pR = r ? panNum(r.right) : null;
342
+ o.railL = rr ? panNum(rr.left) : null; o.railR = rr ? panNum(rr.right) : null;
343
+ return o;
344
+ }
345
+ // Runs on the window's scroll and, through fit, on every visualViewport resize and scroll while the panel is open.
346
+ // The panel's box is read only once something has moved, and only once per open.
347
+ function panCheck() {
348
+ // PAN.at is set only between open and close (close stops the watch before it clears O.open).
349
+ if (PAN.sent || !PAN.at) return;
350
+ var moved = panOffsets(), was = PAN.at;
351
+ // Up and down (sy, pt, ot) and, like split-drag, sideways (sx, pl, ol: Q1 20:44, lead 20:4x).
352
+ if (moved.sy === was.sy && moved.pt === was.pt && moved.ot === was.ot && moved.sx === was.sx && moved.pl === was.pl && moved.ol === was.ol) return;
353
+ PAN.sent = true;
354
+ var now = panState();
355
+ if (W.layoutCrumb) W.layoutCrumb('ta-pan', { kind: O.open.kind, kb: !!KB.on, sy0: was.sy, pt0: was.pt, ot0: was.ot, sy: now.sy, pt: now.pt, ot: now.ot,
356
+ sx0: was.sx, pl0: was.pl, ol0: was.ol, sx: now.sx, pl: now.pl, ol: now.ol,
357
+ pTop: now.pTop, pBot: now.pBot, pL: now.pL, pR: now.pR, railL: now.railL, railR: now.railR });
358
+ }
359
+ function panWatch(on) {
360
+ if (on) {
361
+ PAN.at = panOffsets(); PAN.sent = false; PAN.fn = panCheck;
362
+ window.addEventListener('scroll', panCheck, { passive: true });
363
+ } else if (PAN.fn) {
364
+ if (window.removeEventListener) window.removeEventListener('scroll', panCheck, { passive: true });
365
+ PAN.fn = null; PAN.at = null;
366
+ }
367
+ }
324
368
  function open(kind, key, opener, opt) {
325
369
  opt = opt || {};
326
370
  build();
@@ -334,6 +378,7 @@
334
378
  document.documentElement.classList.add('ta-open');
335
379
  var shell = $('app-shell'); if (shell) shell.setAttribute('inert', '');
336
380
  listenViewport(true); applyFit();
381
+ panWatch(true);
337
382
  // The entry names what is open, so a reload can open it again and Back still closes it (restore below).
338
383
  var entry = { cosTodayOverlay: 1, kind: kind, key: O.open.key };
339
384
  if (O.back) entry.from = 'pet';
@@ -390,6 +435,7 @@
390
435
  function close(how) {
391
436
  if (!O.open) return;
392
437
  stopSession();
438
+ panWatch(false);
393
439
  O.open = null;
394
440
  if (layer) layer.hidden = true;
395
441
  unfit();
@@ -2098,7 +2098,9 @@
2098
2098
  chatGrow(box);
2099
2099
  }
2100
2100
  function chatGrow(box) {
2101
- var vv = window.visualViewport, h = vv ? vv.height : window.innerHeight || 600;
2101
+ // The visible height the one way Control Web reads it (COSWeb.viewportFit): a visible height of 0 (the unfolded Duo
2102
+ // after a keyboard closed, 11/10) reads as innerHeight, never as a 96 px box.
2103
+ var fit = W.viewportFit ? W.viewportFit() : null, h = fit ? fit.height : window.innerHeight || 600;
2102
2104
  var max = Math.max(96, Math.min(320, Math.round(h * 0.42)));
2103
2105
  box.style.height = 'auto';
2104
2106
  box.style.height = Math.min(box.scrollHeight + 2, max) + 'px';
@@ -2111,9 +2113,11 @@
2111
2113
  // With the on-screen keyboard up, keep the text box and the Plan row inside what is visible: scroll only as much as
2112
2114
  // needed, never past the top of the box. On a folded or unfolded Duo the visual viewport tells us what is visible.
2113
2115
  function chatKeepInView() {
2114
- var box = $('wk-chat-text'), row = $('wk-cmp-actions'), vv = window.visualViewport;
2116
+ var box = $('wk-chat-text'), row = $('wk-cmp-actions'), fit = W.viewportFit ? W.viewportFit() : null;
2115
2117
  if (!box || !row || document.activeElement !== box) return;
2116
- var top = vv ? vv.offsetTop : 0, height = vv ? vv.height : window.innerHeight;
2118
+ // Through viewportFit, so a visible height of 0 (11/10) never puts the visible bottom above its top and scrolls the
2119
+ // page away; it reads as innerHeight from offset 0.
2120
+ var top = fit ? fit.top : 0, height = fit ? fit.height : window.innerHeight;
2117
2121
  var bar = document.querySelector('.topbar'), barBottom = bar ? bar.getBoundingClientRect().bottom : 0;
2118
2122
  var visTop = Math.max(top, barBottom > top ? barBottom : top) + 6, visBottom = top + height - 8;
2119
2123
  var boxTop = box.getBoundingClientRect().top, rowBottom = row.getBoundingClientRect().bottom, delta = 0;