PageSourceSearch

https://tabbi.ai/talk

html tabbi.ai collected 2026-10-05 16:18:41 UTC 131,227 bytes, 1,845 lines download raw bytes

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26<!DOCTYPE html>
27<html>
28<meta name="viewport" content="width=device-width, initial-scale=1" />
29<link rel="stylesheet" href="style0926.css">
30<link rel="stylesheet" href="style-header0926.css">
31
32<head>
33<script src="scripts/RecordRTC_2023-04-19.js"></script>
vendor: 1 bytes, line 33
33
34<script src='scripts/jquery-3.5.1.min.js'></script>
vendor: 1 bytes, line 34
34
35<script src='scripts/jqcookie.js'></script>
vendor: 1 bytes, line 35
35
36<script src="scripts/basicScripts0926.js"></script>
vendor: 1 bytes, line 36
36
37<script src="scripts/talkScriptsTalk0926.js"></script>
37
38
39
40  
41    <!-- Google Tag Manager -->
42    
42<script>
vendor: 351 bytes, lines 42-46
42(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
43      new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
44      j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
45      'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
46      })(window,document,'script','dataLayer','
46GTM-TQF9Z4N
vendor: 3 bytes, line 46
46');
46</script>
46
47    <!-- End Google Tag Manager -->
48  
49
50    
51<title>Tabbi Talk &middot; English Speaking Practice with AI Feedback</title>
52  <link rel="icon" href="images/favicon-32x32.png" />
53  <meta name="viewport" content="width=device-width, initial-scale=1" />
54
55
56  <meta property="og:site_name" content="Tabbi AI" />
57  <meta property="og:type" content="website" />
58  <meta property="og:title" content="Tabbi Talk · English Speaking Coach" />
59  <meta property="og:description" content="Speak for up to 2 minutes. Get your words corrected, feedback on grammar and tone, and a native version to listen to." />
60  <meta property="og:url" content="https://tabbi.ai/talk" />
61  <meta property="og:locale" content="en_US" />
62  <meta property="og:image" content="https://tabbi.ai/images/og-tabbi-en.png" />
63  <meta property="og:image:width" content="1200" />
64  <meta property="og:image:height" content="630" />
65  <meta property="og:image:alt" content="Tabbi AI: speak and write English with confidence" />
66  <meta name="twitter:card" content="summary_large_image" />
67  <meta name="twitter:site" content="@tabbiai" />
68  <meta http-equiv="content-type" content="text/html; charset=utf-8" />
69
70<meta name="description" content="Speak for up to 2 minutes. Get your words corrected, feedback on grammar and tone, and a native version to listen to."/>
71<meta name="keywords" content="English Speaking Practice, Language Pronunciation Aid, Real-time Speech Correction, Non-native English Support, Interactive Language Learning"/>
72</head>
73
74<body>
75<div class='area-audio invisible'>
76<audio id='voiceGenPlayer' controls><source id='audioSource' type="audio/mpeg">Your browser does not support the audio element.</audio> 
77<audio id="playbackPlayer" controls></audio>
78</div>
79
80
81<!-- Google Tag Manager (noscript) -->
vendor: 68 bytes, lines 81-82
81
82<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=
82GTM-TQF9Z4N
vendor: 83 bytes, lines 82-83
82"
83height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
84<!-- End Google Tag Manager (noscript) -->
85
86
87
88<header>
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110<script>
111let userIsMember=false;
112let userIsLegacy=false;
113let isTabbiDomain=true;
114let maxCharacters=1000;
115let correctionCount=0;
116let useCustomerIO_thisRequest=false;
117let isNewUser=false;
118let isNewGuest=true;
119/* Welcome cards (2026-09-27, April: existing users must not suddenly see them): a browser's very first page view marks it as new
120   since launch; the cards (WEB-INF/aiccPopups.inc) only ever open in a marked browser. Any page, so it also works when the first page is Home. */
121try {if (isNewGuest && !localStorage.getItem('tabbiWelcome1')) {localStorage.setItem('tabbiWelcome1','{}');}} catch (e) {}
122let FF_transcriptionPauseDetect=1400;
123let FF_defaultAudioPlaybackRate=1.0;
124let USER_maxQuizViewsPerDay=2;
125let USER_maxCorrectionCount=5;
126let USER_memberChatsRemainingToday=5;
127let FF_quizzesOnDeckToShow=5;
128
129let FF_EchoInputTextPromptLabel="echo_input_text";
130let FF_ActualReadModeTextPromptLabel="cleanup_pasted_content";
131let FF_TargetLanguagePromptLabel="target_language_rephrase_edit";
132let quizGenerationNeeded=false;
133let quizViewCountToday=0;
134let quizzesAvailable=0;
135let showLanguageSurvey=true;
136let thisPageTargetValue="talk";
137let inputType="TALK";
138let label2Icon={"original":"&#x1F331;","test-quiz-generator-feb21a":"","test-quiz-generator-march18b":"","talk_rephrase_edit_corrected":"✏️","write_quiz_correction-recall":"🏆","talk_rephrase_edit_eloquent":"💎","talk_rephrase_coach_feedback":"👨‍🏫","talk_rephrase_edit_casual":"😉","talk_rephrase_edit_interview-response-STAR":"🗣️","talk_interview_coach_STAR-feedback":"📝","multi_summarize_edit_summary":"➖","multi_rephrase_edit_persuade":"🤝"};
139let ccErrorMess="To increase your text limit to 5000 characters, join Tabbi Plus!";
140
141let fullAskTabbiOn=false;
142</script>
142
143
144<div>
145
146
147
148
149<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/orioncactus/[email protected]/dist/web/variable/pretendardvariable-dynamic-subset.min.css">
150
151<style>
152.popup-message-button-left, .popup-message-button-right {border-radius:8px;}
153#langDialog .popup-message-button-left, #langDialog .popup-message-button-right {border-radius:8px;}
154.button-popup, .button-join-overlay {border-radius:8px;}
155/* Plans: was 8px in a slightly different black (#212427) */
156.plan-button, .plan-button-gray {border-radius:8px;}
157.plan-button {background-color:#211d1e;}
158.plan-button:hover {background-color:#2e292a;}
159/* after payment: "Start using Tabbi" was 3px */
160.success-button {border-radius:8px; background-color:#211d1e;}
161.success-button:hover {background-color:#2e292a;}
162/* page surfaces: 8px (they were square) */
163.oneResult {border-radius:8px; overflow:hidden;}
164.button-prompt-chooser {border-radius:8px;}
165.dailyPageProgressHeader {border-radius:8px;}
166.faq-container {border-radius:8px; overflow:hidden;}
167.section-type-E-inner .info-1, .section-type-F-inner .info-1, .section-typeF-inner .info-1 {border-radius:8px;}
168.plan {border-radius:8px;}   /* Plans cards (style-infopage<VER>.css had 10px) */
169/* floating: 12px (they were 18px) */
170.popup-message {border-radius:12px;}
171#langDialog {border-radius:12px;}
172.area-alert {border-radius:12px;}
173/* Join / Plus buttons: the versioned stylesheets draw a red ring 2px outside them on any :focus, so a mouse click left what looked like a
174   double line (April). Keep the ring for keyboard users only. */
175.button-join-overlay:focus:not(:focus-visible), .button-popup:focus:not(:focus-visible) {outline:none;}
176</style>
177
178
179<style>
180/* Width (2026-09-28, April: "cap Write, Talk and Read at 1,100px"). They had no maximum width: on a 1,680px screen the text box and the
181   answers ran ~1,650px wide (lines too long to read, buttons stranded in the middle). History already stops at 1,100px. Phones and
182   screens under ~1,130px are unchanged. */
183.main-box.write-page, .main-box.talk-page, .main-box.read-page {max-width:1100px; margin-left:auto; margin-right:auto;}
184/* The reset/send bar is fixed to the bottom of the window at 100% width with left:auto, so it started at the centred column's left edge
185   and ran past the right edge: its buttons sat right of centre (April: "buttons are in wrong place"). Pin it to the window's left. */
186.main-box.write-page .area-buttons-talk, .main-box.talk-page .area-buttons-talk, .main-box.read-page .area-buttons-talk {left:0;}
187/* Read has no AI Style bar, so its box sat 8px under the header while Write/Talk start 16px down (April: "the box is too close to
188   header"). Same 16px there: the whole Read area (box, and the answer that replaces it) moves down 8px. */
189.main-box.read-page .input-mode-area.read-mode {margin-top:8px;}
190/* Desktop/tablet: a guide line in the AI Style bar's slot and look (April: "add the border around the guide text just like Write and Talk") (same 16px top, 44px tall, same bottom gap), so the box lines up with
191   Write/Talk (April: "read still feel too close to nav"). Phones: hidden, the 8px above keeps 16px under the header. */
192.read-intro {display:none;}
193@media (min-width:751px) {
194  .main-box.read-page .read-intro {display:flex; align-items:center; justify-content:center; text-align:center; box-sizing:border-box; height:44px; margin:16px 16px .2rem; padding:0 12px;
195    background:#f5f7fa; border:1px solid #d3d3d3; border-radius:8px; font-size:16px; line-height:1.3; color:#475569;}   /* the AI Style bar's look, so Read matches Write/Talk (April) */
196  .main-box.read-page .input-mode-area.read-mode {margin-top:0;}
197}
198/* Tabs above the answer (2026-09-28, April: "use the old design but make the color for width fill out to the side"): the original tabs
199   (white strip right under the header, grey block behind the selected tab) stay as they were; since the column is centred now, the
200   strip's white is extended to both window edges (box-shadow + clip-path, no layout change), so it still reads as a band under the
201   header. The tabs themselves stay in the column, lined up with the card as before. */
202.main-box.write-page .js-tabtopbar-talk, .main-box.talk-page .js-tabtopbar-talk, .main-box.read-page .js-tabtopbar-talk {
203  box-shadow:0 0 0 100vmax #fff; clip-path:inset(0 -100vmax);}
204/* the first tab starts where the box starts (April: "the button should start where the box start not before"): the strip begins at the
205   column's edge, the card is inset 16px (8px on phones), so the first tab moves in by the same amount */
206.main-box.write-page .js-tabtopbar-talk .js-tabtop:first-child, .main-box.talk-page .js-tabtopbar-talk .js-tabtop:first-child, .main-box.read-page .js-tabtopbar-talk .js-tabtop:first-child {margin-left:16px;}
207@media only screen and (max-width:750px) {
208  .main-box.write-page .js-tabtopbar-talk .js-tabtop:first-child, .main-box.talk-page .js-tabtopbar-talk .js-tabtop:first-child, .main-box.read-page .js-tabtopbar-talk .js-tabtop:first-child {margin-left:8px;}
209}
210/* No jump on submit (2026-09-29, April: "unlike Read, on Talk and Write, when I submit, the screen jumps up a bit"): sending swaps the
211   AI Style bar for the Original/Corrected/Edited tabs, and the tabs strip w
211as 17px shorter, so the box and then the answer moved up.
212   The strip now takes the bar's height, so the box and the answer stay where they were. Read has neither.
213   Tabs at the strip's bottom (April: "B + remove white empty space under the red line", then "move the whole tabs a bit up so that we
214   will have some space just like the current production. (The cool gray)"): the tabs were centred in the strip (style.css
215   align-self:center), which left white under the selected tab's red line. Now they sit at the strip's bottom, so the red line ends the
216   white strip and the page's cool grey shows between the tabs and the box (9px, production has 8px); the box still doesn't move. */
217.main-box.write-page .js-tabtopbar-talk, .main-box.talk-page .js-tabtopbar-talk {min-height:62px; align-items:flex-end;}
218.main-box.write-page .js-tabtopbar-talk .js-tabtop, .main-box.talk-page .js-tabtopbar-talk .js-tabtop {align-self:flex-end;}
219@media only screen and (max-width:750px) {
220  .main-box.write-page .js-tabtopbar-talk, .main-box.talk-page .js-tabtopbar-talk {min-height:63px;}
221}
222</style>
223<script>
224/* While the answer loads, select the tab it will open on (2026-09-29, April: "when I submit, it goes to original tab then flash to
225   corrected. It should directly go to corrected."). Sending showed the tabs with Original selected over the faded text, then jumped to
226   Corrected when the answer came. The answer opens on Corrected, or on the first other tab when a style has no Corrected (e.g.
227   translation hides it), so that tab is selected from the start. Write and Talk only; the answer's own tab switch is unchanged. */
228(function(){
229  function pickTab(){
230    const box=document.querySelector('.main-box.write-page, .main-box.talk-page'); if (!box) {return;}
231    const vis=function(e){return e && e.getBoundingClientRect().width>0;};
232    const tabs=[].slice.call(box.querySelectorAll('.js-tabtopbar-talk .js-tabtop')); if (!tabs.length || !vis(tabs[0].parentNode)) {return;}
233    const target=[box.querySelector('.js-tabtopbar-talk .corrected-tab')].concat(tabs.filter(function(t){return !t.classList.contains('original-tab');})).filter(vis)[0];
234    if (!target) {return;}
235    tabs.forEach(function(t){t.classList.toggle('js-selected', t===target);});
236  }
237  document.addEventListener('DOMContentLoaded', function(){
238    if (!document.querySelector('.main-box.write-page, .main-box.talk-page') || typeof window.showLoadingAnimation!=='function') {return;}
239    const original=window.showLoadingAnimation;
240    window.showLoadingAnimation=function(){ const r=original.apply(this, arguments); setTimeout(pickTab, 0); return r; };
241  });
242})();
243</script>
243
244
245<style>
246/* Site typeface (2026-09-25): one family everywhere. The versioned stylesheets still declare the system stack on body
247   (style/style-infopage) and Verdana on .js-tab-main; this block comes later in the cascade, so it wins at equal
248   specificity without a version bump. Form controls are listed because they never inherit the body font (the Daily
249   start card is a <button>, so it rendered in Arial). Korean pages already set the same stack in langTypography.inc. */
250body, button, input, select, textarea, .js-tab-main {font-family:"Pretendard Variable", Pretendard, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Apple SD Gothic Neo", "Noto Sans KR", sans-serif;}
251/* Header side room (2026-09-26, April): the logo sat 16px from the left while the menu had 28px on the right. Equal 24px on both
252   sides on desktop: halfway between the 16px content edge of Write/Talk/Read and the old menu inset, so the header reads level
253   with the app pages without hugging the corner. Phones and tablets (the hamburger layout under 851px) keep the stylesheet's spacing. */
254@media (min-width: 851px) {
255  .navbar {padding-left:24px; padding-right:24px;}
256  .header-logo {margin-left:0;}
257  .account-info-bar .float-right.float-right {padding-right:24px;}   /* doubled class: outranks the 20px rule further down */
258}
259/* Menu, variation 1 (2026-09-25): ink text in one typeface, red only on the active page's underline, rounded ink CTA. Logo and strip unchanged. */
260.nav-menu span.nav-link, .account-info-bar {font-family:"Pretendard Variable", Pretendard, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Apple SD Gothic Neo", "Noto Sans KR", sans-serif;}
261.nav-menu span.nav-link {position:relative; color:#3d3a3b; font-weight:500; font-size:15px; letter-spacing:-.005em; padding:9px 13px; border-radius:9px;}
262.nav-menu span.nav-link:hover {color:#211d1e; background:#f3f1f2;}
263.nav-menu span.nav-link.js-active {color:#211d1e; font-weight:650;}
264.nav-menu span.nav-link.js-active::after {content:""; position:absolute; left:13px; right:13px; bottom:4px; height:2px; border-radius:2px; background:#bf3143;}
265.nav-menu span.nav-link.tryforfree {color:#fff; background:#211d1e; font-weight:650; padding:10px 16px; border-radius:8px;   /* 2026-09-26: 10px like every other button; was the site's only pill */ box-shadow:0 8px 20px -10px rgba(33,29,30,.6);}
266.nav-menu span.nav-link.tryforfree:hover {color:#fff; background:#2e292a;}
267.nav-menu span.nav-link.tryforfree::after {display:none;}
268/* Header refresh A (2026-09-27, April picked option A of three mock-ups). Quieter grey tabs, the current page ink at 600 (was a jump
269   to 650); account links set apart by a thin divider (Log in / History); Join as the ink button like Try for Free; the chats strip
270   without its own divider line and in a smaller, muted type; the logo without the "Unlock your potential" tagline. */
271.nav-menu span.nav-link {color:#5d6167; letter-spacing:-.01em;}
272.nav-menu span.nav-link:hover {color:#211d1e; background:#f4f4f2;}
273.nav-menu span.nav-link.js-active {color:#211d1e; font-weight:600;}
274.nav-menu span.nav-link.tryforfree, .nav-menu span.nav-link.tryforfree:hover {color:#fff;}
275.account-info-bar, .account-info-bar .float-right {background:#f5f7fa;}   /* cool grey utility strip + hairline, same grey as the app background (April 2026-09-27; without any divider the strip and the menu read as one block). Warm #f7f6f4 with #efeef0 was tried; April: "cool grey for now, revisit warm later". */
276.account-info-bar {border-bottom:1px solid #e8eaee;}
277.account-info-bar .float-right {font-size:13px; color:#736769;}
278@media only screen and (min-width: 851px) {
279  .nav-menu .nav-item.nav-sep {margin-left:12px; padding-left:12px; border-left:1px solid #e6e6e8;}
280  .nav-menu .nav-item.nav-join {margin-left:4px;}
281}
282.lang-switch-mobile a, .lang-switch a {font-weight:500; display:inline-block; text-align:center;}
283/* The selected language is bold, which is wider; each link reserves its bold width with a hidden zero-height copy placed BEFORE the text (after it, the link's baseline would drop and the separators sink) so nothing shifts (April, 2026-09-25). */
284.lang-switch-mobile a::before, .lang-switch a::before {content:attr(data-t); display:block; height:0; overflow:hidden; visibility:hidden; font-weight:700;}
285/* Strip inset (2026-09-25, April: "a bit of space on the right of Español"): the text sat 3px from the window edge; 20px matches the menu's right edge. */
286.account-info-bar .float-right {padding-right:20px;}
287@media only screen and (max-width: 750px) { .account-info-bar .float-right {padding-right:16px;} }
288/* Language switch (2026-09-25): in the account strip on wide screens; below the 850px menu breakpoint the strip has no room, so it becomes a stacked block at the end of the hamburger menu. */
289@media only screen and (max-width: 850px) {
290  .account-info-bar .lang-switch {display:none;}
291  /* tighter phone menu (2026-09-25): the desktop padding plus per-row margins made the list twice as tall as it needs to be */
292  .nav-menu {padding-top:12px; padding-bottom:12px;}   /* 2026-09-27 (April): more room above the first item and below the last (was .4rem) */
293  .nav-menu {border-radius:12px; border:1px solid #e6e6e8; margin-top:6px; box-shadow:0 22px 52px -20px rgba(0,0,0,.35), 0 5px 14px -8px rgba(0,0,0,.18);}   /* 2026-09-28: the phone menu floats, so it follows the pop-up shape (12px, soft shadow), with a small gap under the header */
294  .nav-menu .nav-item {margin:0 .5rem;}
295  .nav-menu span.nav-link {padding:7px 12px; font-size:15px;}
296  .nav-menu span.nav-link.js-active::after {bottom:3px;}
297  .nav-menu span.nav-link.tryforfree {margin:4px 0 2px; padding:9px 16px;}
298  .nav-menu span.nav-link.lang-switch-mobile {flex-direction:column; align-items:center; gap:6px; white-space:normal; padding:8px 12px 10px; border-top:1px solid #eee; margin-top:4px; border-radius:0; font-size:14px;}
299}
300@media only screen and (min-width: 851px) { .nav-item.lang-item {display:none;} }
301/* Daily's red dot (2026-09-25): hug the word like a badge — the menu's 6px flex gap is enough, no extra margin; lifted a little. */
302.nav-menu span.nav-link .daily-noti-dot {margin-left:-2px; position:relative; top:-4px;}
303</style>
304<script>
305/* Phone menu (2026-09-27, April): open it just below the menu button. It was pinned at 55px from the top, inside a header that is
306   ~96px tall, so it covered the close X. Measured on each open because the header's height varies by page and account. */
307document.addEventListener('click', function(e) {
308  var h=e.target && e.target.closest ? e.target.closest('.hamburger') : null; if (!h) return;
309  var m=document.querySelector('.nav-menu'); if (!m) return;
310  if (window.innerWidth<=850) {m.style.top=Math.round(h.getBoundingClientRect().bottom+10)+'px';} else {m.style.top='';}
311}, true);
312</script>
312
313
314
315  <div class='account-info-bar'><span class='float-right'>
316  
317
318  
319  
320    <span class='topRemMessage'>You have</span>
321      <span class='chatsRemaining maxwidth1'>5</span> <span class='maxwidth2'> free chats remaining </span> |
322      <a class='subsMessage' href="#" onclick='showX($(".area-agreement"));'>Sign up</a> for more
323  
324  
325
326  </span></div>
327
328
329<div class="horizontal-line">
330<nav class="navbar">
331<a class='header-logo' target="_self" href="./home" aria-current="page"><img src="images/tabbi-logo-v2.png" width="185" alt="Tabbi AI"></a>
332<ul class="nav-menu">
333
334  
335    
336        
337          <li class="nav-item">
338              <a href="#" class="nav-link">
339              <span class='dailyMenuItem nav-link new-content' onclick='catoutThenLink("MENU ITEM: Guest/Daily","daily");'>
340              Daily
341              <span class="daily-noti-dot"></span></span></a>
342              
343              
343<script>try{if(localStorage.getItem('dailyGuestDoneId')=='475'){var gd=document.querySelector('.dailyMenuItem .daily-noti-dot');if(gd){gd.remove();}}}catch(e){}</script>
343
344          </li>
345          <li class="nav-item">
346              <a href="#" class="nav-link"><span class='chatMenuItem nav-link' onclick='catoutThenLink("MENU ITEM: Guest/Write","write");'>Write</span></a>
347          </li>
348          <li class="nav-item">
349              <a href="#" class="nav-link"><span class='talkMenuItem nav-link js-active' onclick='catoutThenLink("MENU ITEM: Guest/Talk","talk");'>Talk</span></a>
350          </li>
351        
352          <li class="nav-item">
353              <a href="#" class="nav-link"><span class='readMenuItem nav-link' onclick='catoutThenLink("MENU ITEM: Guest/Read","read");'>Read</span></a>
354          </li>
355        
356
357        
358        <li class="nav-item nav-sep">
359            <a href="#" class="nav-link"><span class='loginMenuItem nav-link' onclick='catoutThenLink("MENU ITEM: Guest/Login","https://auth.tabbi.ai/login?client_id=34t7p7b7r0sc2799h1rcvjksse&response_type=code&scope=email+openid+phone&redirect_uri=https%3A%2F%2Ftabbi.ai%2Fauth.jsp%3Fpage=talk");'>Log in</span></a>
360        </li>            
361        <li class="nav-item nav-join">
362            <a href="#" class="nav-link"><span class="joinMenuItem tryforfree nav-link" onclick="catout('MENU ITEM: Guest/Join'); showX($('.area-agreement'));">Join</span></a>
363          </li>
364    
365  
366
367</ul>
368        <div class="hamburger">
369            <span class="bar"></span>
370            <span class="bar"></span>
371            <span class="bar"></span>
372        </div>
373    </nav>
374    
375  </div>
376</div>
377
378</header>
379<style id="popup-phone-fit">
380/* Phones (2026-09-24): style0911.css sizes .popup-message as content + 48px padding, so the box ran past the screen. Border-box here keeps the 14px side margins the stylesheet intended; desktop untouched. */
381@media only screen and (max-width: 430px) {
382  .popup-message {box-sizing:border-box; width:min(392px, calc(100vw - 28px));}
383}
384</style>
385
386
387
388<script src="scripts/popupsScripts0926.js"></script>
388
389<div class='popupOverlay invisible'></div>
390
391<script>
392$(function(){setupSnippetFunctionality();});
393</script>
393
394
395
396
397<!-- history unlock (Tabbi Plus upsell for locked history rows) start -->
398  <div class="popup-message history-unlock-dialog invisible">
399      <span class="popup-close-button" onclick="hideX($('.popup-message'));"><img src="images/close-x-trans.png"></span>
400      <span class="popup-message-title">Unlock your history</span>
401      <span class="popup-message-text">Free accounts keep chats for 24 hours. Tabbi Plus keeps everything &mdash; this one, and every chat you've ever written.</span>
402      <div class="popup-message-button-area">
403          <button type="button" class="popup-message-button-left pmb-ghost" onclick="hideX($('.popup-message'));">Not now</button>
404          <button type="button" class="popup-message-button-right pmb-ink" onclick='catoutThenLink("UPSELL: history unlock","plans");'>Start 14-day free trial</button>   
405      </div>
406  </div>
407<!-- history unlock end -->
408
409<!-- delete all chats (History page) start -->
410  <div class="popup-message delete-all-dialog invisible">
411      <span class="popup-close-button" onclick="hideX($('.popup-message'));"><img src="images/close-x-trans.png"></span>
412      <span class="popup-message-title">Delete all chats?</span>
413      <span class="popup-message-text">This removes <span class="delete-all-count">every chat</span> from your History. It can't be undone.</span>
414      <div class="popup-message-button-area">
415          <button type="button" class="popup-message-button-left pmb-ghost" onclick="hideX($('.popup-message'));">Cancel</button>
416          <button type="button" class="popup-message-button-right pmb-ink" onclick="deleteAllChats();">Delete all</button>
417      </div>
418  </div>
419<!-- delete all chats end -->
420
421<div class='movablePopup snippetOptions toolsTarget js-bs-top invisible prevent-select' starOn=false>
422  <div class='tool-grabber'><center><img class="movable" src="images/movable.png"></center></div>
423  <div class='tool-box'>
424    <div class='clickable'><span id='toolbox-star' class='snip-tool-button button-favorite' onclick='toggleStarGoldSnippet(this);'><img class='icon-multi image-one' src='images/highlight-black.png'><img class='icon-multi image-two invisible' src='images/highlight-yellow.png'></span></div>
425    <div class='clickable'><span id='toolbox-read' class='snip-tool-button button-read-aloud' onclick='setDiffState(false,nowTabContent); toggleAudio(this,$(".snip").text());'><img class='icon-multi image-one' src='images/read.png'><img class='icon-multi image-two invisible' src='images/read-selected.png'></span></div>
426    <div class='clickable copy-position'><span class='guide-copied'>Copied</span><span id='toolbox-copy' class='snip-tool-button button-copy' onclick='clipboardSimple(this,"snippetOptions","snip");'><img class='icon-multi image-one' src='images/copy.png' ><img class='icon-multi image-two invisible' src='images/copy-selected.png' ></span></div>
427    <div class='clickable'><span id='toolbox-meaning' class='snip-tool-button button-meaning aiButton' onclick='fetchSnippetResults(this,"meaning");'><img class='icon-multi image-one' src='images/meaning-black.png'><img class='icon-multi image-two invisible' src='images/meaning-red.png'></span></div>
428    <div class='clickable'><span id='toolbox-rephrase' class='snip-tool-button button-rephrase aiButton' onclick='fetchSnippetResults(this,"rephrased");'><img class='icon-multi image-one' src='images/rephrase-black.png'><img class='icon-multi image-two invisible' src='images/rephrase-red.png'></span></div>
429  
430  </div>
431  
432  <div class='snipTitle invisible'><span class='snip'></span></div>
433  <div class='snipContext invisible'></div>
434  <div class='snipAIArea popupTextToCopy'></div>
435  <center><img class='spinner invisible' src='images/loading-gif.gif' loading='lazy'></center>
436</div>
437
438<style id="read-popup">
439/* Read highlight pop-up (2026-09-28, April). The last pop-up in the old grey 5px style. Floating layers are rounded with a shadow
440   (quiz, Ask Tabbi, dialogs: 12px; page surfaces 8px), so it becomes the same family as Ask Tabbi: a white 12px card whose icon row
441   sits on the light warm grey strip, answers (Meaning / Rephrased / Translation) on white under a small grey label, Copy as the
442   toolbar's copy icon whose tip turns into "Copied". Scoped to .snippetOptions; the pop-up is still placed with left/top (no
443   transform), which tabbiTips.inc relies on for its fixed-position tips. */
444.snippetOptions {width:16rem; padding:0; border:1px solid #e7e3de; border-radius:12px; background:#fff; overflow-x:hidden; overflow-y:auto;
445  box-shadow:0 22px 52px -20px rgba(0,0,0,.35), 0 5px 14px -8px rgba(0,0,0,.18);}
446.snippetOptions .tool-box {background:#f6f5f3; padding:6px 8px; gap:2px; justify-content:space-between;}
447.snippetOptions .snipAIArea:not(:empty) {border-top:1px solid #ece8e3;}
448.snippetOptions .oneSnipAI {margin:0; padding:12px 14px 10px; border:0; border-radius:0; background:#fff; font-size:15px; line-height:1.55; color:#2f2b2c; display:flex; flex-wrap:wrap; align-items:center;}
449.snippetOptions .oneSnipAI.invisible {display:none;}
450.snippetOptions .oneSnipAI .copyTarget {flex:1 1 100%;}
451.snippetOptions .oneSnipAI .snipAITitle {display:block; margin:0 0 4px; font-size:12.5px; font-weight:700; letter-spacing:.06em; text-transform:uppercase; color:#736769;}
452.snippetOptions .oneSnipAI .guide-copied {display:none;}
453.snippetOptions .oneSnipAI .popupCopyButton {position:relative; display:inline-flex; width:32px; height:32px;
453 margin:6px -8px -6px auto; padding:0; border:0; border-radius:8px;
454  background:transparent url('images/copy.png') center / 20px 20px no-repeat; font-size:0; color:transparent; opacity:.75; cursor:pointer; bottom:auto; right:auto;}
455.snippetOptions .oneSnipAI .popupCopyButton:hover {background-color:#f4f2ef; opacity:1;}
456.snippetOptions .spinner {margin:12px 0;}
457/* "Copied": the same one-pill behaviour as Ask Tabbi (script toggles .visible on .guide-copied for 2 s) */
458@media (hover:hover) {
459  .snippetOptions .oneSnipAI .popupCopyButton::after {content:'Copy'; position:absolute; right:0; bottom:calc(100% + 6px); background:#211d1e; color:#fff;
460    font-size:12px; line-height:1; font-weight:500; padding:6px 9px; border-radius:6px; white-space:nowrap; pointer-events:none; opacity:0; transition:opacity .12s ease .6s;}
461  .snippetOptions .oneSnipAI .popupCopyButton:hover::after {opacity:1;}
462}
463.snippetOptions .oneSnipAI .guide-copied.visible + .popupCopyButton {opacity:1;}
464.snippetOptions .oneSnipAI .guide-copied.visible + .popupCopyButton::after {content:'Copied'; position:absolute; right:0; bottom:calc(100% + 6px); background:#211d1e; color:#fff;
465  font-size:12px; line-height:1; font-weight:500; padding:6px 9px; border-radius:6px; white-space:nowrap; pointer-events:none; opacity:1; transition:none;}
466</style>
467<script>
468// The script writes "<span class='snipAITitle'>meaning</span>: text"; with the title as its own label line, drop the ": " it leaves.
469$(function(){
470  var area=document.querySelector('.snippetOptions .snipAIArea'); if (!area || !window.MutationObserver) return;
471  new MutationObserver(function(){
472    area.querySelectorAll('.oneSnipAI .snipAITitle').forEach(function(t){
473      var n=t.nextSibling; if (n && n.nodeType===3 && /^\s*:\s*/.test(n.nodeValue)) {n.nodeValue=n.nodeValue.replace(/^\s*:\s*/,'');}
474    });
475  }).observe(area, {childList:true});
476});
477</script>
vendor: 1 bytes, line 477
477
478<script>
479$(function(){setupQuizFunctionality();});
480</script>
480
481
482<div class='quiz-onoff cat1 invisible'><span class='quiz-button' onclick='toggleQuiz();'><img src='images/quiz-cat1.gif' width="70" height="70" loading="lazy"/></span></div>
483<div class='quiz-onoff cat2 invisible'><span class='quiz-button' onclick='toggleQuiz();'><img src='images/quiz-cat2.gif' width="70" height="70" loading="lazy"/></span></div>
484
485
486<style id="quiz-card">
487.quizPopup {width:min(352px, calc(100vw - 24px)); max-height:min(34rem, calc(100dvh - 24px)); padding:20px 18px 16px; box-sizing:border-box;
488  background:#f6f5f3; border:1px solid #e7e3de; border-radius:12px; color:#211d1e; line-height:1.45;
489  box-shadow:0 22px 52px -20px rgba(0,0,0,.35), 0 5px 14px -8px rgba(0,0,0,.18);}
490.quizPopup.glowingDrag {background:#f1efeb; border:1px solid #d8d8dc;}
491.quizPopup .popup-close-button {top:12px; right:12px; width:32px; height:32px; border-radius:8px; display:flex; align-items:center; justify-content:center; opacity:.6;}
492.quizPopup .popup-close-button:hover {opacity:1; background:#ebe8e3;}
493.quizPopup .popup-close-button img {width:16px;}
494.quizPopup .quizAIArea {padding:0; font-size:14px; color:#736769;}   /* "Loading..." */
495.quizPopup img.spinner {width:30px; height:30px;}   /* style0926.css sets every img in the quiz to 25px wide, which squeezed the round loader (2026-09-28) */
496.quizPopup .oneQuizAI {color:#211d1e;}
497.quizPopup .oneQuizAI .quizTitle {font-size:1.2rem; font-weight:700; letter-spacing:-.015em; line-height:1.25; padding:2px 36px 12px 2px;}
498/* 2026-09-28 (April: "the quiz card is too white"): the card opens over Write's white text box, so it is a light warm grey (#f6f5f3; April
499   compared it with the cool #f5f7fa and chose warm) with white answer rows and a white "Why?" panel, and a small tag says what popped up. */
500.quizPopup .oneQuizAI .quizTitle::before {content:'Quick quiz'; display:block; width:fit-content; margin:0 0 10px; padding:4px 10px; border-radius:999px;
501  background:#fbe9ec; color:#bf3143; font-size:12px; font-weight:700; letter-spacing:.02em; line-height:1.2;}
502/* the two answers: tappable cards with a round marker */
503.quizPopup .oneQuizAI .quiz-choice-all {display:flex; align-items:center; gap:12px; margin:0 0 8px; padding:12px 14px; padding-right:12px;
504  background:#fff; border:1.5px solid #e7e3de; border-radius:8px; transition:border-color .15s, background-color .15s, opacity .15s;}
505.quizPopup.unclicked .oneQuizAI .quiz-choice-all {cursor:pointer;}
506.quizPopup.unclicked .oneQuizAI .quiz-choice-all:hover {border-color:#211d1e;}
507.quizPopup .oneQuizAI .quiz-choice-all .quiz-choice {flex:1 1 auto; margin:0; padding:0; border:0 !important; background:transparent; font-size:15px; line-height:1.4; color:#211d1e;}
508.quizPopup.unclicked .oneQuizAI .quiz-choice-all:hover .quiz-choice {border:0 !important;}
509.quizPopup .oneQuizAI .quiz-choice-icon {flex:0 0 22px; width:22px; height:22px; border-radius:50%; border:1.5px solid #c9c9c5; box-sizing:border-box;
510  background:#fff center / 12px 12px no-repeat;}
511.quizPopup .oneQuizAI .quiz-choice-icon img {display:none !important;}
512.quizPopup.unclicked .oneQuizAI .quiz-choice-all:hover .quiz-choice-icon {border-color:#211d1e;}
513/* answered: the better sentence turns green with a check; a wrong pick turns red with an x; the other fades */
514.quizPopup:not(.unclicked) .oneQuizAI .quiz-correct {border-color:#488474; background:#eff6f2;}
515.quizPopup:not(.unclicked) .oneQuizAI .quiz-correct .quiz-choice-icon {border:0; background-color:#488474;
516  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M2.5 6.2l2.3 2.3 4.7-4.9' fill='none' stroke='white' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");}
517.quizPopup:not(.unclicked) .oneQuizAI .quiz-incorrect.choice-chosen {border-color:#bf3143; background:#fdf1f2;}
518.quizPopup:not(.unclicked) .oneQuizAI .quiz-incorrect.choice-chosen .quiz-choice-icon {border:0; background-color:#bf3143;
519  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M3.3 3.3l5.4 5.4M8.7 3.3l-5.4 5.4' fill='none' stroke='white' stroke-width='1.8' stroke-linecap='round'/%3E%3C/svg%3E");}
520.quizPopup:not(.unclicked) .oneQuizAI .quiz-incorrect:not(.choice-chosen) {opacity:.55;}
521/* the verdict (added by the script below) and the reason, in one soft panel */
522.quizPopup .oneQuizAI .quiz-result-explained {margin:6px 0 0; padding:12px 14px; border-radius:8px; background:#fff; border:1px solid #e7e3de; font-size:14px; line-height:1.5; color:#3d3a3b;}
523.quizPopup .oneQuizAI .quiz-result-explanation {font-weight:700; color:#211d1e;}
524.quizPopup .quiz-verdict {display:block; font-size:15px; font-weight:700; margin:0 0 4px;}
525.quizPopup .quiz-verdict.ok {color:#3a7362;}
526.quizPopup .quiz-verdict.no {color:#bf3143;}
527.quizPopup .quizAITool-box .result-message {display:none !important;}   /* shown in the panel instead */
528/* actions: Review in History as a quiet link, Next Quiz as the ink button */
529.quizPopup .quizAITool-box {display:flex; align-items:center; gap:12px; margin-top:14px;}
530.quizPopup .quizAITool-box:not(:has(> span:not(.invisible):not(.result-message))) {margin-top:0;}
531.quizPopup .quiz-history-link {margin:0; font-size:14px; text-decoration:none;}
531   /* style0926.css underlines this span too: with the link's own underline it showed two lines */
532.quizPopup .quiz-history-link a {color:#5d6167; text-decoration:underline; text-decoration-color:#c9c9c5; text-underline-offset:3px;}
533.quizPopup .quiz-history-link a:hover {color:#211d1e; text-decoration-color:#211d1e;}
534.quizPopup .another-quiz-button {margin-left:auto; font-size:14px;}
535.quizPopup .another-quiz-button a {display:inline-flex; align-items:center; height:38px; padding:0 16px; border-radius:8px; background:#211d1e; color:#fff;
536  font-weight:650; text-decoration:none; box-shadow:0 6px 16px -8px rgba(33,29,30,.5);}
537.quizPopup .another-quiz-button a:hover {background:#2e292a;}
538</style>
539<script>
540/* Verdict at the top of the "Why?" panel, right after an answer: "Meow! That's right." / "Hiss... The other one is better."
541   Worked out from the classes the quiz script sets, and rebuilt with each quiz (the panel is replaced), so it never goes stale. */
542document.addEventListener('click', function(e) {
543  var c=e.target && e.target.closest ? e.target.closest('.quizPopup .quiz-choice-all') : null; if (!c) return;
544  setTimeout(function(){
545    var box=document.querySelector('.quizPopup .quiz-result-explained'); if (!box || box.querySelector('.quiz-verdict')) return;
546    var picked=document.querySelector('.quizPopup .quiz-choice-all.choice-chosen'); if (!picked) return;
547    var ok=picked.classList.contains('quiz-correct'); var v=document.createElement('span');
548    v.className='quiz-verdict '+(ok?'ok':'no'); v.textContent=ok ? 'Meow! That’s right.' : 'Hiss… The other one is better.';
549    box.insertBefore(v, box.firstChild);
550  }, 0);
551});
552/* A new quiz starts unanswered (2026-09-27 fix). The quiz script removes .unclicked on an answer but never puts it back for the
553   next quiz, and the card above reads .unclicked to know "not answered yet", so Next Quiz showed the new answer already marked.
554   Whenever the quiz area is refilled ("Loading...", then the new question) and nothing is chosen in it, mark the card unanswered. */
555(function(){
556  function init() {
557    var area=document.querySelector('.quizPopup .quizAIArea'); if (!area || !window.MutationObserver) return;
558    new MutationObserver(function(){
559      if (!area.querySelector('.choice-chosen')) {var p=area.closest('.quizPopup'); if (p) p.classList.add('unclicked');}
560    }).observe(area, {childList:true});
561  }
562  if (document.readyState==='loading') document.addEventListener('DOMContentLoaded', init); else init();
563})();
564</script>
564
565<div class='movablePopup quizPopup toolsTarget js-bs-top invisible prevent-select unclicked' starOn=false>
566  <span class='popup-close-button' onclick='closePopupQuiz();'><img class='mainImg' src="images/close-x-trans.png"/></span>
567  <div class='quizAIArea'></div>
568  <center><img class='spinner invisible' src='images/loading-gif.gif' loading='lazy'></center>
569  <div class='quizAITool-box'>
570    <span class='result-message correct-choice invisible'>Meow!</span>
571    <span class='result-message incorrect-choice invisible'>Hiss...</span>
572    <span class='quiz-history-link invisible'><a href='historyOne.jsp'>Review in History</a></span>
573    <span class='another-quiz-button invisible'><a href='#' onclick='fetchAndShowQuizResults();'>Next Quiz</a></span>
574  </div>
575</div>
576<script>
577// Quiz card position (2026-09-28, April: "on mobile the quiz is not in the center and touches the right side").
578// Replaces toggleQuiz from scripts/popupsScripts*.js (loaded above, so this later definition wins; no version bump).
579// The old code centred with jQuery .width(), which leaves out the card's padding and border (38px), so the card sat 19px
580// right of centre: on a phone its right edge met the screen edge. Centre on the window with the full outer width instead,
581// and keep at least 12px on each side. Dragging still works: the card is only placed when it opens.
582async function toggleQuiz() {
583    currentMovablePopup=$('.movablePopup.quizPopup');
584    toggleX(currentMovablePopup);
585    if (!quizShown) {
586        currentMovablePopup.addClass('unclicked');
587        let vw=document.documentElement.clientWidth, w=$(".quizPopup").outerWidth();
588        let xpLeft=Math.max(12, Math.round((vw-w)/2)) + window.scrollX;
589        let xpTop=180;
590        $(".quizPopup").css({left: xpLeft, top: xpTop});
591        quizShown=true;
592        fetchAndShowQuizResults();
593    }
594}
595</script>
595
596
597<script>
598$(function(){setupAskTabbiFunctionality();});
599</script>
599
600
601<div id="floating-AT" class='ask invisible' style='position: absolute;'><span><img src="images/asktabbi.png"></span></div>
602<div class='movablePopup askTabbiPopup toolsTarget js-bs-top invisible prevent-select unclicked' starOn=false>
603  <span class='popup-close-button' onclick='closePopupAskTabbi();'><img class='mainImg' src="images/close-x-trans.png"/></span>
604  <div class="askTabbiArea">
605
606    <div class="askTabbiResults atAvailable">
607      <div class="askTabbiResults-title"><span class="at-tag">Ask Tabbi</span><span class='atTargetInTitle'></span></div>
608      <div class="askTabbiResults-rest">
609        <div class="askTabbiResults-loading"><center><img class="spinner" src="images/loading-gif.gif" loading="lazy"></center></div>
610        <div class="askTabbiResults-text popupTextToCopy invisible"></div>
611      </div>
612    </div>
613
614    <div class="askTabbiResults atUnavailable invisible">
615      
616      <div class="askTabbiResults-title"><span class="at-tag">Ask Tabbi</span>2 explanations per chat</div>
617      <div class="askTabbiResults-basic">
618          <div class="askTabbiResults-basic-text">You've used both explanations for this chat. Start a new chat for 2 more, or get unlimited explanations with <strong>Tabbi Plus</strong>.</div>
619          <a href="https://tabbi.ai/plans" target="_blank" onclick="closePopupAskTabbi();"><button class="askTabbiResults-basic-button">Try Plus free for 14 days</button></a>
620      </div>
621    </div>
622
623  </div>
624</div>
625
626
627<style id="ask-tabbi-card">
628/* Ask Tabbi (2026-09-28, April: "let's work on ask tabbi feature to match the design"). April compared four designs (A warm card with a
629   pill, B all white, C/C2 warm with the logo, D) and chose D: a white card, so the explanation reads on white, with a light warm grey
630   header strip (#f6f5f3, the quiz card's colour) holding the Ask Tabbi cat logo, "Ask Tabbi" and the word in quotes. The category the
631   answer starts with ("Nuanced improvement") is a small grey label, Copy is the toolbar's copy icon whose tip turns into "Copied".
632   The dotted strip is gone (the whole card still drags). Scoped to .askTabbiPopup so the old style0926.css rules lose. */
633.askTabbiPopup {width:min(352px, calc(100vw - 24px)); max-height:min(40rem, calc(100dvh - 200px)); padding:0 18px 16px; box-sizing:border-box;
634  background:#fff; border:1px solid #e7e3de; border-radius:12px; color:#211d1e; line-height:1.45; overflow-x:hidden; overflow-y:auto; overscroll-behavior:contain;
635  box-shadow:0 22px 52px -20px rgba(0,0,0,.35), 0 5px 14px -8px rgba(0,0,0,.18);}
636.askTabbiPopup::before {display:none;}
637.askTabbiPopup.glowingDrag {background:#fbfaf9; border:1px solid #d8d8dc;}
638.askTabbiPopup .popup-close-button {top:12px; right:12px; width:32px; height:32px; border-radius:8px; display:flex; align-items:center; justify-content:center; opacity:.6; z-index:1;}
639.askTabbiPopup .popup-close-button:hover {opacity:1; background:#ebe8e3;}
640.askTabbiPopup .popup-close-button img {width:16px;}
641.askTabbiPopup .askTabbiArea {max-height:none; overflow:visible; padding:0;}
642/* header strip: logo + "Ask Tabbi", then the word in quotes (added words green and underlined, removed words red and struck through) */
643.askTabbiPopup .askTabbiResults-title {margin:0 -18px 14px; padding:18px 54px 14px 20px; background:#f6f5f3; border-bottom:1px solid #ece8e3;
644  font-size:1.35rem; font-weight:700; letter-spacing:-.015em; line-height:1.25;}
645.askTabbiPopup .at-tag {display:flex; align-items:center; gap:7px; margin:0 0 8px; color:#bf3143; font-size:13.5px; font-weight:700; letter-spacing:.01em; line-height:1.2;}
646.askTabbiPopup .at-tag::before {content:''; flex:0 0 22px; width:22px; height:22px; background:url('images/asktabbi.png') center / contain no-repeat;}
647.askTabbiPopup .atTargetInTitle::before {content:'\201C'; color:#211d1e;}
648.askTabbiPopup .atTargetInTitle::after {content:'\201D'; color:#211d1e;}
649.askTabbiPopup .atTargetInTitle span {background:none; border:0; padding:0;}
650.askTabbiPopup .atTargetInTitle .diff-delete {color:#c0606c;}   /* the light pink of the answer text is hard to read at title size */
651.askTabbiPopup .askTabbiResults-loading {margin:1.25rem 0 .5rem;}
652.askTabbiPopup img.spinner {width:30px; height:30px;}   /* style0926.css sets every img here to 25px wide, which squeezed the round loader */
653/* the answer, on the white card;
653 "Copied" + Copy sit at its foot. A long answer scrolls the whole card (it opens 180px down). */
654.askTabbiPopup .askTabbiResults-text {padding:0 2px; font-size:15px; line-height:1.55; color:#2f2b2c; max-height:none; overflow:visible;}
655.askTabbiPopup .askTabbiResults-text:not(.invisible) {display:flex; flex-wrap:wrap; align-items:center;}
656.askTabbiPopup .askTabbiResults-text .copyTarget {flex:1 1 100%;}
657.askTabbiPopup .askTabbiResults-text p {margin:0 0 .6rem;}
658.askTabbiPopup .askTabbiResults-text p:last-child {margin-bottom:0;}
659.askTabbiPopup .ask-tabbi-title {display:block; margin:0 0 6px; font-size:12.5px; font-weight:700; letter-spacing:.06em; text-transform:uppercase; color:#736769;}
660/* Copy is the answer toolbar's copy icon (images/copy.png), April: "can we use icon for Copy?". Placed in the card's corner, mirroring the close x
661   (April: "more bottom more right"): its centre sits under the x's centre and as far from the bottom as the x is from the top. The script writes the word "Copy"
662   into this span, so the word is hidden (font-size 0) and the icon is its background; the same "Copy" tip as the toolbar on hover. */
663.askTabbiPopup .askTabbiResults .popupCopyButton {position:relative; display:inline-flex; width:32px; height:32px; margin:8px -21px -8px auto; padding:0; box-sizing:border-box;
664  border:0; border-radius:8px; background:transparent url('images/copy.png') center / 20px 20px no-repeat; font-size:0; color:transparent; opacity:.75; cursor:pointer;}
665.askTabbiPopup .askTabbiResults .popupCopyButton:hover {background-color:#f4f2ef; opacity:1;}
666@media (hover:hover) {
667  .askTabbiPopup .askTabbiResults .popupCopyButton::after {content:'Copy'; position:absolute; right:0; bottom:calc(100% + 6px); background:#211d1e; color:#fff;
668    font-size:12px; line-height:1; font-weight:500; padding:6px 9px; border-radius:6px; white-space:nowrap; pointer-events:none; opacity:0; transition:opacity .12s ease .6s;}
669  .askTabbiPopup .askTabbiResults .popupCopyButton:hover::after {opacity:1;}
670}
671/* "Copied" is not shown as its own grey word: the Copy tip itself turns into "Copied" (April: "so that I don't need to see two"),
672   like the answer toolbar. The script still toggles .visible on this span for 2 s after a copy; the rules below key off it. */
673.askTabbiPopup .askTabbiResults .guide-copied {display:none;}
674.askTabbiPopup .askTabbiResults .guide-copied.visible + .popupCopyButton {opacity:1;}   /* the faded icon would fade the pill too */
675.askTabbiPopup .askTabbiResults .guide-copied.visible + .popupCopyButton::after {content:'Copied'; position:absolute; right:0; bottom:calc(100% + 6px);
676  background:#211d1e; color:#fff; font-size:12px; line-height:1; font-weight:500; padding:6px 9px; border-radius:6px; white-space:nowrap; pointer-events:none;
677  opacity:1; transition:none;}
678/* limit reached: no header strip, so the card's own top padding comes back here; the site's red button */
679.askTabbiPopup .askTabbiResults-basic {padding:0 2px;}
680.askTabbiPopup .askTabbiResults-basic-title {font-size:1.2rem; font-weight:700; line-height:1.25; padding-right:36px;}
681.askTabbiPopup .askTabbiResults-basic-text {font-size:15px; line-height:1.5; color:#3d3a3b; margin:8px 0 16px;}
682.askTabbiPopup .askTabbiResults-basic-button {height:42px; padding:0 16px; border:0; border-radius:8px; background:#bf3143; color:#fff; font-size:15px; font-weight:650;
683  box-shadow:0 6px 16px -8px rgba(191,49,67,.7);}
684.askTabbiPopup .askTabbiResults-basic-button:hover {background:#a3283a;}
685</style>
686<script>
687// Ask Tabbi position (2026-09-28): replaces openPopupAskTabbi from scripts/popupsScripts*.js (loaded above; no version bump).
688// Same bug the quiz had: it centred with jQuery .width(), which leaves out padding and border, so the card sat right of centre
689// and could touch the right edge on phones; and the "limit reached" card was never placed at all. Both now use atPlace().
690function atPlace(p) {
691  let vw=document.documentElement.clientWidth, w=p.outerWidth();
692  p.css({left: Math.max(12, Math.round((vw-w)/2)) + window.scrollX, top: 180});
693}
694// The script adds ": " inside the category span ("Nuanced Improvement: "); as a small label it reads better without it.
695$(function(){
696  var box=document.querySelector('.askTabbiPopup .askTabbiResults-text'); if (!box || !window.MutationObserver) return;
697  new MutationObserver(function(){
698    var t=box.querySelector('.ask-tabbi-title'); if (t && /:\s*$/.test(t.textContent)) {t.textContent=t.textContent.replace(/:\s*$/,'');}
699  }).observe(box, {childList:true});
700});
701async function openPopupAskTabbi(fullContext, insertOrDelete, spanContent) {
702  if (askTabbiInProgress) {return;}
703  currentMovablePopup=$('.movablePopup.askTabbiPopup');
704  if (!(fullAskTabbiOn || askTabbiCount<MAX_AskTabbi)) {
705    hideX($('.askTabbiResults')); showX($('.atUnavailable')); showX(currentMovablePopup);
706    if (!askTabbiShown) {atPlace(currentMovablePopup); askTabbiShown=true;}
707    return;
708  }
709
710  askTabbiInProgress=true;
711  if (!askTabbiShown) {
712    hideX($('.askTabbiResults')); showX($('.atAvailable'));
713    showX(currentMovablePopup);
714    currentMovablePopup.addClass('unclicked');
715    atPlace(currentMovablePopup);
716    askTabbiShown=true;
717  }
718  fetchAndShowAskTabbiResults(fullContext, insertOrDelete, spanContent);
719  askTabbiCount+=1;
720}
721</script>
721
722
723
724<div class='movablePopup feedbackPopup js-bs-top invisible' role='dialog' aria-label='Feedback'>
725  <span class='popup-close-button' onclick='closePopupFeedback();'><img class='mainImg' src="images/close-x-trans.png" alt="Close"/></span>
726  <div class='fbp-head'>Feedback</div>
727  <div class='fbp-text'></div>
728</div>
729<style id="feedback-card">
730.feedbackPopup {position:absolute; z-index:1000; width:min(352px, calc(100vw - 24px)); max-height:min(34rem, calc(100dvh - 32px)); padding:0 18px 16px; box-sizing:border-box;
731  background:#fff; border:1px solid #e7e3de; border-radius:12px; color:#211d1e; line-height:1.45; overflow-x:hidden; overflow-y:auto; overscroll-behavior:contain;
732  box-shadow:0 22px 52px -20px rgba(0,0,0,.35), 0 5px 14px -8px rgba(0,0,0,.18);}
733.feedbackPopup.glowingDrag {background:#fbfaf9; border:1px solid #d8d8dc;}
734.feedbackPopup .popup-close-button {top:12px; right:12px; width:32px; height:32px; border-radius:8px; display:flex; align-items:center; justify-content:center; opacity:.6; z-index:1;}
735.feedbackPopup .popup-close-button:hover {opacity:1; background:#ebe8e3;}
736.feedbackPopup .popup-close-button img {width:16px;}
737.feedbackPopup .fbp-head {margin:0 -18px 14px; padding:18px 54px 14px 20px; background:#f6f5f3; border-bottom:1px solid #ece8e3; font-size:1.2rem; font-weight:700; letter-spacing:-.01em; line-height:1.25; cursor:move;}
738.feedbackPopup .fbp-text {padding:0 2px; font-size:15px; line-height:1.55; color:#2f2b2c;}
739.feedbackPopup .fbp-text p {margin:0 0 .6rem;} .feedbackPopup .fbp-text p:last-child {margin-bottom:0;}
740</style>
741<script>
742function closePopupFeedback() {hideX($('.feedbackPopup')); $('.feedback-button').removeClass('js-active');}
743// Runs before the button's own handler (capture phase) and stops it, so the answer is no longer swapped for the feedback.
744document.addEventListener('click', function(e) {
745  var b=e.target && e.target.closest ? e.target.closest('.oneResult-corrected .feedback-button') : null; if (!b) return;
746  e.stopPropagation(); e.preventDefault();
747  var pop=document.querySelector('.feedbackPopup'); if (!pop) return;
748  if (!pop.classList.contains('invisible') && pop._from===b) {closePopupFeedback(); return;}   // a second click closes it
749  var scope=b.closest('.oneResultCombo') || b.closest('.js-roof-node') || document;
750  var src=scope.querySelector('.oneResult-feedback .contentSpan') || scope.querySelector('.oneResult-feedback .result-content');
751  pop.querySelector('.fbp-text').innerHTML=src ? src.innerHTML : '';
752  pop._from=b; currentMovablePopup=$(pop); showX($(pop)); $('.feedback-button').removeClass('js-active'); b.classList.add('js-active');
753  var vw=document.documentElement.clientWidth, vh=window.innerHeight, w=pop.offsetWidth, h=pop.offsetHeight, r=b.getBoundingClientRect(), left, top;
754  if (vw<=750) {left=Math.round((vw-w)/2); top=Math.max(r.bottom+8, vh-h-16);}   // phone: low on the screen, the text above stays readable
755  else {left=Math.min(Math.max(12, Math.round(r.right-w)), vw-w-12); top=r.bottom+8;}   // desktop: under the button, on the card's right side
756  pop.style.left=(left+window.scrollX)+'px'; pop.style.top=(top+window.scrollY)+'px';
757}, true);
758$(function(){
759  if (typeof setupMovablePopup==='function') setupMovablePopup('.feedbackPopup','.fbp-tool-button_NOT_USED');
760  // the drag code moves whichever pop-up is "current": make this one current as soon as it is grabbed
761  var pop=document.querySelector('.feedbackPopup'); if (!pop) return;
762  ['mousedown','touchstart'].forEach(function(t){ pop.addEventListener(t, function(){ currentMovablePopup=$(pop); }, true); });
763});
764</script>
764
765
766
767
768
769
770
771
772
773
774
775
776<script>
777/* Star fix (2026-09-28, April: "star on History isn't saved"). basicScripts' setFavorite() (the star in an answer's toolbar on
778   Write/Talk/Read and in History's detail view) sent the page-wide globalRsetIdString/globalCorrIdString, which are only filled after a
779   highlight or a Daily save. With nothing highlighted it sent rsetId=0 and nothing was saved; after highlighting chat A, a star on chat B
780   went to A. This copy is identical except it uses the chat the star sits in (.oneResult rsetId, else the History .oneResultCombo corrId,
781   and the result's corrId); Daily keeps the page-wide ids, as before. Overridden here (loaded after basicScripts) to avoid an asset-version
782   bump; fold it into basicScripts at the next bump. */
783if (typeof setFavorite==='function') {
784  setFavorite=async function(newVal,x) {
785    setStarState('.oneResult',newVal,x);
786    var setLabel=$(x).closest('.oneResult').attr("rsetId");
787    if (!setLabel || setLabel==0) {setLabel=$(x).closest('.oneResultCombo').attr("corrId");}
788    var rs=(setLabel && (""+setLabel).indexOf("RS")===0) ? (""+setLabel).substring(2) : "0";
789    var co=$(x).closest('.oneResult').attr("corrId") || "0000";
790    if ($('.dailyText').length > 0) {   // Daily: the lesson may not be in History yet, so it is saved first (unchanged)
791      if (newVal==true && (!dailySavedToHistory || !globalRsetIdString || !globalCorrIdString)) {
792        const result=await saveNowDailyToHistory(); globalRsetIdString=result.corrId; globalCorrIdString=result.results[1].airId;
793      }
794      rs=globalRsetIdString; co=globalCorrIdString;
795    }
796    const response=await fetch(baseServerURL+"api/apiFavorite.jsp?onOff="+newVal+"&rsetId="+rs+"&corrIds="+co);
797    const data=await response.json();
798    favArray[nowTabIndex]=newVal;
799    setTabStarClass(nowTabIndex,x);
800    var oneRequestTop=$('.oneRequest[corrid="'+setLabel+'"]');   // the matching row in the History list, if shown
801    setStarState('.oneRequest',(data.aggFave=="true"), oneRequestTop.find('.button-favorite'));
802    if (data.aggFave=="true") {oneRequestTop.addClass('favorite');} else {oneRequestTop.removeClass('favorite');}
803    if (co=="0000") {if (newVal) {oneRequestTop.addClass('favOriginal');} else {oneRequestTop.removeClass('favOriginal');}}
804  };
805}
806</script>
806
807<!-- tutorial1 HTML START -->
808
809
810<style id="tabbi-welcome">
811.tutorialPopup {width:min(400px, calc(100vw - 24px)); min-height:0; max-height:calc(100dvh - 32px); padding:24px 22px 20px; box-sizing:border-box; overflow:auto;
812  background:#fff; border:1px solid #e6e6e8; border-radius:12px; color:#211d1e; line-height:1.45; text-align:left;
813  box-shadow:0 22px 52px -20px rgba(0,0,0,.35), 0 5px 14px -8px rgba(0,0,0,.18);}
814/* centred here: the shared script centres by the content width (padding left out), which pushed the card past a phone's edge */
815.tutorialPopup.tutorialPopup {left:50% !important; transform:translateX(-50%);}
816/* In the visible window, a little above the middle (2026-09-27, April: it sat high on tall screens; the shared script pins top:120px).
817   Fixed, so scrolling does not move it. top 45% with a -45% shift: even a card as tall as the window (max-height below) keeps ~14px
818   top and bottom, so it can never run off a phone's screen. */
819.tutorialPopup.tutorialPopup {position:fixed !important; top:45% !important; transform:translate(-50%,-45%);}
820.tutorialPopup .popup-close-button {top:12px; right:12px; width:32px; height:32px; border-radius:8px; display:flex; align-items:center; justify-content:center; opacity:.6;}
821.tutorialPopup .popup-close-button:hover {opacity:1; background:#f5f7fa;}
822.tutorialPopup .popup-close-button img {width:16px;}
823.tw-eyebrow {font-size:13px; font-weight:650; color:#bf3143; margin:0 0 6px; padding-right:36px;}
824.tw-title {font-size:1.35rem; font-weight:700; letter-spacing:-.015em; line-height:1.25; margin:0 0 18px; padding-right:24px;}
825.tw-steps {list-style:none; margin:0; padding:0; display:flex; flex-direction:column; gap:14px;}
826.tw-steps li {display:flex; gap:12px; align-items:flex-start; margin:0;}
827.tw-num {flex:0 0 26px; width:26px; height:26px; border-radius:50%; background:#eef1f5; color:#211d1e; font-size:13px; font-weight:700; display:inline-flex; align-items:center; justify-content:center; margin-top:1px;}
828.tw-step-title {display:block; font-size:15px; font-weight:650; line-height:1.35;}
829.tw-step-text {display:block; font-size:14px; line-height:1.45; color:#5d6167; margin-top:2px;}
830.tw-note {margin:18px 0 0; padding:10px 12px; border-radius:8px; background:#f5f7fa; font-size:13.5px; color:#3d3a3b;}
831.tw-steps + .tw-actions {margin-top:20px;}   /* members: no note above the buttons */
832.tw-actions {display:flex; align-items:center; gap:12px; margin-top:16px;}
833.tutorialPopup a.tw-link, .tutorialPopup a.tw-link:link, .tutorialPopup a.tw-link:visited {font-size:14px; color:#5d6167; text-decoration:underline; text-decoration-color:#c9c9c5; text-underline-offset:3px;}   /* a:link elsewhere outranked a plain .tw-link */
834.tutorialPopup a.tw-link:hover {color:#211d1e; text-decoration-color:#211d1e;}
835.tw-go {margin-left:auto; display:inline-flex; align-items:center; height:42px; padding:0 18px; border:0; border-radius:8px; background:#211d1e; color:#fff; font:inherit; font-size:15px; font-weight:650; cursor:pointer; box-shadow:0 6px 16px -8px rgba(33,29,30,.5);}
836.tw-go:hover {background:#2e292a;}
837</style>
838<div class="movablePopup tutorialPopup toolsTarget js-bs-top invisible prevent-select unclicked" staron="false" role="dialog" aria-labelledby="tw-title">
839  <span class="popup-close-button" onclick='tabbiWelcomeDone();' aria-label="Close"><img class="mainImg" src="images/close-x-trans.png" alt=""></span>
840  <div class="tw-eyebrow">Welcome to Tabbi Talk</div>
841  <div class="tw-title" id="tw-title">Speak, then hear a better way to say it</div>
842  <ol class="tw-steps">
843    <li><span class="tw-num">1</span><span><span class="tw-step-title">Tap the mic and speak</span><span class="tw-step-text">Up to 2 minutes.</span></span></li>
844    <li><span class="tw-num">2</span><span><span class="tw-step-title">Get feedback</span><span class="tw-step-text">A corrected version, plus tips on grammar and tone.</span></span></li>
845    <li><span class="tw-num">3</span><span><span class="tw-step-title">Compare</span><span class="tw-step-text">Hear the better version, then your own.</span></span></li>
846    
847  </ol>
848  <div class="tw-note">5 free chats. No account needed.</div>
849  <div class="tw-actions">
850    <a class="tw-link" href="how-it-works">How it works</a>
851    <button type="button" class="tw-go" onclick="tabbiWelcomeDone(false);">Start talking</button>
852  </div>
853</div>
854<script>
855/* Once per page per browser (localStorage "tabbiWelcome1" = {write:1, talk:1, ...}). Without storage (private modes that block it),
856   fall back to the old rule: first visit only. Language first (2026-09-27, April): if the native-language question is open (members who
857   have not answered it), the card waits until that question is answered or closed, then opens. It is marked seen only when it shows. */
858var TW_PAGE='talk', TW_ELIGIBLE=true;
859function tabbiWelcomeDone(startDaily) {
860  closePopupX(".tutorialPopup");
861  if (startDaily && typeof dailyNow==='function' && !$('.daily-button-wrapper').hasClass('invisible')) {dailyNow();}   // Daily: start today's lesson
862}
863function tabbiWelcomeShow() {
864  try {var seen=JSON.parse(localStorage.getItem('tabbiWelcome1')||'{}')||{}; seen[TW_PAGE]=1; localStorage.setItem('tabbiWelcome1', JSON.stringify(seen));} catch (e) {}
865  showPopupX($('.tutorialPopup'));
866}
867$(function(){ setTimeout(function(){
868  if (!TW_ELIGIBLE) return;
869  var seen={}, stored=true;
870  try {seen=JSON.parse(localStorage.getItem('tabbiWelcome1')||'{}')||{};} catch (e) {stored=false;}
871  if (!stored && !(typeof isNewGuest!=='undefined' && isNewGuest)) return;
872  // No mark = this browser used Tabbi before the cards launched (the header marks a browser only at its first-ever page view).
873  // Mark every card as seen and show nothing, so existing guests and members never get a sudden run of welcome cards.
874  if (stored && localStorage.getItem('tabbiWelcome1')===null) {
875    try {localStorage.setItem('tabbiWelcome1', JSON.stringify({write:1, talk:1, read:1, daily:1, other:1, before:1}));} catch (e) {}
876    return;
877  }
878  if (seen[TW_PAGE]) return;
879  var q=document.querySelector('.surveyPopup');
880  if (q && !q.classList.contains('invisible') && window.MutationObserver) {
881    var mo=new MutationObserver(function(){ if (q.classList.contains('invisible')) {mo.disconnect(); setTimeout(tabbiWelcomeShow, 250);} });
882    mo.observe(q, {attributes:true, attributeFilter:['class']});
883    return;
884  }
885  tabbiWelcomeShow();
886}, 0); });
887</script>
887
888  <!-- tutorial END -->
889  
890
891
892<!-- shared backdrop for every .popup-message dialog (CSS shows it when one is open) -->
893<div class="popup-scrim" onclick="hideX($('.popup-message'));"></div>
894
895<div class="popup-message prompt-switch-confirm invisible">
896    <span class="popup-close-button" onclick="hideX($('.popup-message'));"><img src="images/close-x-trans.png"></span>
897    <span class="popup-chip">Detected&nbsp;·&nbsp;<span class='detected-language-name'>Non-English</span></span>
898    <span class="popup-message-title">Translate instead?</span>
899    <span class="popup-message-text">This doesn't look like English.</span>
900    <div class="popup-message-button-area">
901        <button class="popup-message-button-left pmb-ghost" onclick="hideX($('.popup-message'));">Keep editing</button>
902        <button class="popup-message-button-right pmb-primary"
903        onclick="switchToTranslationPrompt('write_rephrase_edit_translate-eng'); hideX($('.popup-message'));">Translate</button>
904    </div>
905</div>
906<!-- Modal behavior: Esc to close, focus management, focus trap, roles/aria, overlay backdrop-click.
907     Non-invasive: watches .popup-message via MutationObserver; no change to the scattered open-triggers. -->
908<script>
909(function(){
910  if(window.__pmModalHelper) return; window.__pmModalHelper=true;
911  var lastFocused=null;
912  function focusable(el){
913    var f=el.querySelectorAll('button, a[href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
914    return Array.prototype.filter.call(f, function(n){ return n.offsetParent!==null && !n.disabled; });
915  }
916  function openDialog(el){
917    try{
918      el.setAttribute('role','alertdialog'); el.setAttribute('aria-modal','true');
919      var title=el.querySelector('.popup-message-title, .alert-msg1, .langRedirMessage');
920      if(title){ if(!title.id){ title.id='pm-t-'+Math.random().toString(36).slice(2,7); } el.setAttribute('aria-labelledby', title.id); }
921      lastFocused=document.activeElement;
922      // Focus the dialog itself (standard dialog pattern), not the primary button: mouse users then see no
923      // keyboard-focus ring on open. Tab still lands on the first button - the container is tabindex=-1 and
924      // excluded from focusable() - and Esc / focus-trap / return-focus behave as before.
925      if(!el.hasAttribute('tabindex')){ el.setAttribute('tabindex','-1'); }
926      setTimeout(function(){ try{ el.focus({preventScroll:true}); }catch(e){ try{ el.focus(); }catch(e2){} } }, 40);
927    }catch(e){}
928  }
929  function closeReturn(){ if(lastFocused && lastFocused.focus){ try{ lastFocused.focus(); }catch(e){} } lastFocused=null; }
930  function observe(el){
931    var open=!el.classList.contains('invisible'); if(open){ openDialog(el); }
932    new MutationObserver(function(){
933      var now=!el.classList.contains('invisible');
934      if(now && !open){ openDialog(el); } else if(!now && open){ closeReturn(); }
935      open=now;
936    }).observe(el,{attributes:true, attributeFilter:['class']});
937  }
938  function openEl(){ return document.querySelector('.popup-message:not(.invisible), .overlay-join:not(.invisible), .overlay-subscribe:not(.invisible)'); }
939  document.addEventListener('DOMContentLoaded', function(){
940    document.querySelectorAll('.popup-message').forEach(observe);
941  });
942  // clicking an overlay's own dark area (not the card) dismisses it
943  document.addEventListener('click', function(e){
944    if(e.target.classList && (e.target.classList.contains('overlay-join')||e.target.classList.contains('overlay-subscribe'))){ try{ hideX($(e.target)); }catch(err){} }
945  });
946  // Esc closes; Tab stays trapped inside the open dialog
947  document.addEventListener('keydown', function(e){
948    var dlg=openEl(); if(!dlg) return;
949    if(e.key==='Escape'){ try{ hideX($('.popup-message')); hideX($('.overlay-join')); hideX($('.overlay-subscribe')); }catch(err){} return; }
950    if(e.key==='Tab'){
951      var f=focusable(dlg); if(!f.length) return;
952      var first=f[0], last=f[f.length-1];
953      if(e.shiftKey && document.activeElement===first){ e.preventDefault(); last.focus(); }
954      else if(!e.shiftKey && document.activeElement===last){ e.preventDefault(); first.focus(); }
955    }
956  });
957})();
958</script>
958
959
960
961<style id="alert-phone-fit">
962/* Phones (2026-09-24): style<VER>.css still carries the pre-redesign .area-alert look under 750px (300px, 7px corners, white, thin text). Match the desktop card and keep it inside the screen. */
963@media only screen and (max-width: 750px) {
964  .area-alert {box-sizing:border-box; width:min(392px, calc(100vw - 28px)); padding:26px 24px 22px; border-radius:12px;
965    background-color:#fff; border:1px solid #e6e6e8; color:#211d1e; font-size:.95rem; line-height:1.4; font-weight:inherit; z-index:1200;
966    box-shadow:0 22px 52px -20px rgba(0,0,0,.35), 0 5px 14px -8px rgba(0,0,0,.18);}
967}
968/* Neutral backdrop and a white card, like the style chooser and the dialogs (April: "I don't like red color overlay ... match with that").
969   style<VER>.css gives .overlay a reddish rgba(38,20,23,.46) and .area-alert a pink card. */
970.overlay-join, .overlay-subscribe {background-color:rgba(20,20,22,.42);}
971.overlay-join .area-alert, .overlay-subscribe .area-alert {text-align:left; background-color:#fff; border:1px solid #e6e6e8; box-shadow:0 22px 52px -20px rgba(0,0,0,.35), 0 5px 14px -8px rgba(0,0,0,.18);}
972/* Left-aligned like every other pop-up (History unlock, dialogs, Ask Tabbi, quiz, welcome, contact); these two were the only centred ones
973   (April: "is it to be center aligned? considering other similar modal?"). */
974.overlay-join .area-alert .popup-chip, .overlay-subscribe .area-alert .popup-chip {margin-left:0;}
975.overlay-join .area-alert .overlay-body, .overlay-subscribe .area-alert .overlay-body {padding:0;}
976.overlay-join .area-alert .alert-msg1, .overlay-subscribe .area-alert .alert-msg1 {padding-left:0; padding-right:0;}
977/* The Plus button is a link, and a site link colour turned its text dark on the red (April: "color is wrong"): white like Join Tabbi. */
978a.button-popup, a.button-popup:link, a.button-popup:visited, a.button-popup:hover {color:#fff; text-decoration:none;}
979</style>
980<div class="overlay-join overlay invisible">
981  <div class="area-alert">
982    <span class="button-close-overlay" onclick="hideX($('.overlay-join'));">&times;</span>
983    <span class="popup-chip gate">Free account</span>
984    
985    <span class="alert-msg1">You've used your 5 free chats</span>
986    <p class="overlay-body">Join free for 10 chats a day. Your History and streak are saved.</p>
987    <span class="button-join-overlay" onclick='window.location.assign("https://auth.tabbi.ai/signup?client_id=34t7p7b7r0sc2799h1rcvjksse&response_type=code&scope=email+openid+phone&redirect_uri=https%3A%2F%2Ftabbi.ai%2Fauth.jsp");'>Join Tabbi</span>
988    <span class="link-login-overlay">Already have an account? <a href="https://auth.tabbi.ai/login?client_id=34t7p7b7r0sc2799h1rcvjksse&response_type=code&scope=email+openid+phone&redirect_uri=https%3A%2F%2Ftabbi.ai%2Fauth.jsp">Log in</a></span>
989    <span class="alert-msg2">By joining, you agree to the <a target="_blank" href="terms">Terms of Use</a> and <a target="_blank" href="privacy">Privacy Policy</a>.</span>
990  </div>
991</div>
992
993<div class="overlay-subscribe overlay invisible">
994  <div class="area-alert">
995    <span class="button-close-overlay" onclick="hideX($('.overlay-subscribe'));">&times;</span>
996    <span class="popup-chip warn">Daily limit</span>
997    
998    <span class="alert-msg1">You've used your 10 free chats</span>
999    
1000    <p class="overlay-body">Free accounts can send 10 chats in 24 hours. Try again later, or get unlimited chats with Tabbi Plus.</p>
1001    <a href="plans" class="button-popup">Try Plus free for 14 days</a>   
1002  </div>
1003</div>
1004
1005
1006
1007
1008<script> $(function(){ 
1009     onLoadAllPagesNew(null, null); 
1010    /* onLoadTalkPage */
1011    onLoadInputPage("TALK_PAGE", 2); 
1012    $(".quiz-onoff").remove();
1013    /* removed customerio stuff cio. */
1014    let isFirefox = typeof InstallTrigger !== 'undefined';
1015    if (isFirefox) {$($('.js-area-input')).attr('contenteditable', 'true');}
1016    $('.mic-play-combo').removeClass('invisible');
1017});
1018</script>
1018
1019
1020<div class='main-box talk-page'>
1021
1022
1023
1024<script>
1025let numSelected=3;
1026let selectedChanged=false;
1027let promptModeFilter='talk';
1028let promptListCSV_write="write_rephrase_edit_corrected,write_rephrase_edit_eloquent,write_rephrase_coach_feedback";
1029let promptListCSV_talk="talk_rephrase_edit_corrected,talk_rephrase_edit_eloquent,talk_rephrase_coach_feedback";
1030currentlySelectedPromptListCSV="talk_rephrase_edit_corrected,talk_rephrase_edit_eloquent,talk_rephrase_coach_feedback";
1031</script>
1031
1032
1033
1034<style id="ai-style-chooser">
1035.overlay-prompt-chooser.overlay {background-color:rgba(20,20,22,.42);}
1036.overlay-prompt-chooser .prompt-menu {display:flex; flex-direction:column; width:min(440px, calc(100vw - 24px)); max-width:none; max-height:min(82vh, 680px);
1037  padding:0; background:#fff; border:1px solid #e6e6e8; border-radius:12px; color:#211d1e; overflow:hidden;
1038  box-shadow:0 22px 52px -20px rgba(0,0,0,.35), 0 5px 14px -8px rgba(0,0,0,.18);}
1039.overlay-prompt-chooser .prompt-menu-header {position:relative; height:auto; display:block; padding:20px 56px 6px 22px; background:#fff; justify-content:flex-start;}
1040.overlay-prompt-chooser .prompt-menu-title {margin:0; font-size:1.2rem; line-height:1.3; font-weight:700; letter-spacing:-.015em; text-align:left;}
1041.overlay-prompt-chooser .button-close-prompt-menu {position:absolute; top:14px; right:14px; margin:0; transform:none; width:32px; height:32px; border-radius:8px;
1042  display:flex; align-items:center; justify-content:center; opacity:.6;}
1043.overlay-prompt-chooser .button-close-prompt-menu:hover {opacity:1; background:#f5f7fa;}
1044.overlay-prompt-chooser .button-close-prompt-menu img {height:16px; width:16px;}
1045@media (hover:none) {.overlay-prompt-chooser .button-close-prompt-menu {width:40px; height:40px; top:10px; right:10px;}}   /* bigger tap target on phones */
1046.overlay-prompt-chooser .prompt-menu-subtitle {padding:14px 22px 6px; font-size:12px; font-weight:650; letter-spacing:.04em; text-transform:uppercase; color:#736769;}
1047.overlay-prompt-chooser .prompt-menu-list {overflow:visible; padding:0 12px 4px; flex:0 0 auto;}
1048.overlay-prompt-chooser .prompt-menu-list.style-list {overflow-y:auto; flex:1 1 auto; min-height:0;}   /* only the style list scrolls */
1049.overlay-prompt-chooser .prompt-menu-item {display:flex; align-items:center; gap:12px; margin:0 0 4px; padding:11px 12px; text-indent:0;
1050  border:1.5px solid transparent; border-radius:8px; background:#fff; color:#211d1e; font-size:15px; font-weight:500; line-height:1.3; cursor:pointer;}
1051.overlay-prompt-chooser .prompt-menu-item:last-of-type {border-bottom:1.5px solid transparent;}
1052.overlay-prompt-chooser .prompt-menu-item:hover {background:#f5f7fa; font-weight:500;}
1053.overlay-prompt-chooser .prompt-menu-item .ddIconSpan {flex:0 0 22px; text-align:center; font-size:17px; line-height:1;}
1054.overlay-prompt-chooser .prompt-menu-item .ddTitleSpan {flex:1 1 auto;}
1055.overlay-prompt-chooser .prompt-menu-item.selected {background:#fff; border-color:#211d1e;}
1056.overlay-prompt-chooser .prompt-menu-item.selected::after {content:""; flex:0 0 20px; height:20px; border-radius:50%; background:#211d1e center / 11px 11px no-repeat;
1057  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M2.5 6.2l2.3 2.3 4.7-4.9' fill='none' stroke='white' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");}
1058.overlay-prompt-chooser .prompt-menu-item.pinned, .overlay-prompt-chooser .prompt-menu-item.pinned:hover {background:#f5f7fa; border-color:transparent; color:#3d3a3b; cursor:default;}
1059.overlay-prompt-chooser .prompt-menu-item.pinned::after {display:none;}
1060.overlay-prompt-chooser .dd-pin {flex:0 0 auto; font-size:12px; font-weight:600; color:#5d6167; background:#fff; border:1px solid #e6e6e8; border-radius:999px; padding:3px 9px;}
1061.overlay-prompt-chooser .prompt-menu-list.style-list {padding-bottom:12px;}   /* no footer: a little room below the last style */
1062</style>
1063<div class='overlay-prompt-chooser overlay talk-prompts invisible'>
1064  <div class='prompt-menu'>
1065    <div class="prompt-menu-header">
1066      <span class="prompt-menu-title">Select AI Style</span>
1067      <span class='float-right button button-close-prompt-menu' onclick='togglePromptChooser();'><img src='images/close-x-trans.png'></span>      
1068    </div>
1069
1070    <div class="prompt-menu-subtitle invisible"><span class='prompt-count'>3</span> selected</div>
1071    <div class="prompt-menu-subtitle">Always included</div>
1072    <div class='prompt-menu-list'>
1073      
1074        <div class='prompt-menu-item selected pinned' 
1075         value="talk_rephrase_edit_corrected" >
1076        <span class='ddIconSpan'>✏️</span> <span class='ddTitleSpan'>Minimal Correction</span><span class='dd-pin'>Always on</span>
1077        </div>
1078    </div>
1079    <div class="prompt-menu-subtitle">Style</div>
1080    <div class='prompt-menu-list style-list'>
1081      
1082      
1083        <div class='style-prompt prompt-menu-item selected' 
1084        onclick='togglePChoice(this);' value="talk_rephrase_edit_eloquent" >
1085        <span class='ddIconSpan'>💎</span> <span class='ddTitleSpan'>Eloquent Correction</span>
1086        </div>
1087        <div class='style-prompt prompt-menu-item' 
1088        onclick='togglePChoice(this);' value="talk_rephrase_edit_casual" >
1089        <span class='ddIconSpan'>😉</span> <span class='ddTitleSpan'> Casual Correction</span>
1090        </div>
1091        <div class='style-prompt prompt-menu-item' 
1092        onclick='togglePChoice(this);' value="talk_rephrase_edit_interview-response-STAR" >
1093        <span class='ddIconSpan'>🗣️</span> <span class='ddTitleSpan'>Job Interview Response</span>
1094        </div>
1095        <div class='style-prompt prompt-menu-item' 
1096        onclick='togglePChoice(this);' value="multi_summarize_edit_summary" >
1097        <span class='ddIconSpan'>➖</span> <span class='ddTitleSpan'>Shorten</span>
1098        </div>
1099        <div class='style-prompt prompt-menu-item' 
1100        onclick='togglePChoice(this);' value="multi_rephrase_edit_persuade" >
1101        <span class='ddIconSpan'>🤝</span> <span class='ddTitleSpan'>Persuasive Tone</span>
1102        </div>
1103    </div>
1104
1105    <div class="prompt-menu-subtitle invisible">[Feedback]</div>
1106    <div class='prompt-menu-list invisible'>
1107        <div class="prompt-menu-item selected" onclick="togglePChoice(this);" value="write_rephrase_coach_feedback"><span class="ddIconSpan">👩‍🏫</span> <span class="ddTitleSpan">Writing Feedback</span></div>
1108    </div>
1109
1110    
1111  </div>
1112</div>
1113
1114<div class='button-prompt-chooser' onclick='togglePromptChooser();'>
1115    <span class='prompt-select-title'>AI Style:&nbsp;<span class='selected-prompt-mode'></span></span>
1116</div>
1117
1118<script>(function(){var s=document.querySelector('.style-prompt.selected'), t=document.querySelector('.selected-prompt-mode'); if (s && t) t.innerHTML=s.innerHTML;})();</script>
1118
1119
1120<!-- END: Above is the code for prompt selection  -->
1121
1122<style>
1123  .oneRowFlexParent {display: flex; flex-direction: row; justify-content: space-evenly; align-items: stretch;}
1124  .oneColumnFlexParent {display: flex; flex-direction: column; justify-content: space-evenly; align-items: stretch;}
1125  .showAsFlexChild {flex-grow: 1; flex-basis: 0; display: flex;}
1126</style>
1127
1128
1129
1130<div class='oneRequest'>
1131<div class="area-main1 js-roof-node tabbed-set-holder">
1132        <div class='js-tabtopbar-talk invisible'>
1133            <div class='tabtop0 original-tab clickable js-tabtop js-tabtop-talk js-selected' 
1134            onclick='showOneTab(this,"oneResult","oneResult-original");nowTabIndex=0;submitActive(true);'>
1135            Original</div>
1136            <div class='tabtop1 corrected-tab clickable js-tabtop js-tabtop-talk' 
1137            onclick='showOneTab(this,"oneResult","oneResult-corrected");nowTabIndex=1;submitActive(false);'>
1138            Corrected</div>
1139            <div class='tabtop2 eloquent-tab clickable js-tabtop js-tabtop-talk' 
1140            onclick='showOneTab(this,"oneResult","oneResult-eloquent");nowTabIndex=2;submitActive(false);'>
1141            Edited</div>
1142        </div>
1143
1144        <div class="area-audiogroup oneResultCombo">
1145
1146            <div class="area-main3">
1147
1148                        
1149                        <div class="input-mode-area talk-mode ">
1150                            <div class="js-bs-top oneResult oneResult-original original-text js-tab-main content-set before-submit">
1151                            <div class="area-talk-input js-area-input copyable"  contentEditable="plaintext-only" oninput="onTextInput(this);">
1152                                <div class='cursor-talk-input'></div>
1153                            </div> 
1154                            <span class="button-big-mic button-state js-active"><img class="bigmicrophone" src="images/bigmic.png" width="200" alt="Start"/></span>
1155                            </div>
1156                            <div class="area-tools-combo1 status-message"></div>
1157                        </div>
1158                        
1159                        
1160                          
1161
1162            </div>
1163
1164            <div class="area-buttons-talk button-set1">
1165                <span class="button-reset-text button-state"></span> 
1166                <span class=" talk-mode button-toggle-voice button-state js-active"></span>
1167                <span class="button-submit-talk button-state"></span>
1168            </div>
1169            <div class="area-buttons-talk button-set2">
1170                <span class="button-restart-text button-state js-active"></span>
1171                <span class="button-submit-talk button-state"></span>
1172            </div>
1173        </div>       
1174</div>
1175</div>
1176
1177</div>
1178
1179
1180
1181<!-- agreement (consent dialog) start -->
1182
1183 <div class="area-agreement popup-message agreement-dialog invisible">
1184   <span class="popup-close-button" onclick="hideX($('.popup-message'));"><img src="images/close-x-trans.png"></span>
1185   <span class="popup-message-title">Create your account</span>
1186   <span class="popup-message-text">By continuing, you agree to Tabbi's <a target="_blank" href="terms">Terms</a> and <a target="_blank" href="privacy">Privacy</a>.</span>
1187   <div class="popup-message-button-area">
1188     <button type="button" class="popup-message-button-left pmb-ghost" onclick="hideX($('.popup-message'));">Cancel</button>
1189     <button type="button" class="popup-message-button-right pmb-ink" onclick='catoutThenLink("AGREEMENT: I understand","https://auth.tabbi.ai/signup?client_id=34t7p7b7r0sc2799h1rcvjksse&response_type=code&scope=email+openid+phone&redirect_uri=https%3A%2F%2Ftabbi.ai%2Fauth.jsp%3Fpage=talk");'>Continue to sign up</button>
1190   </div>
1191 </div>
1192    <!-- agreement end -->
1193
1194</body>
1195
1196
1197</html>
1198
1199
1200<style>
1201@media (hover:hover) {
1202  [data-tip] {position:relative;}
1203  .tool-box span[data-tip] {display:inline-block;}
1204  [data-tip]::after {content:attr(data-tip); position:absolute; left:50%; bottom:calc(100% + 8px); transform:translateX(-50%);
1205    background:#211d1e; color:#fff; font-size:12px; line-height:1; font-weight:500; font-style:normal; letter-spacing:normal; text-transform:none; text-align:left;
1206    padding:6px 9px; border-radius:6px; white-space:nowrap; pointer-events:none; opacity:0; transition:opacity .12s ease .6s; z-index:50;}
1207  [data-tip]:hover::after {opacity:1;}
1208  .tt-warm [data-tip]:hover::after {transition-delay:0s;}
1209  [data-tip][data-tip-off]::after {display:none;}
1210  .tip-below[data-tip]::after {bottom:auto; top:calc(100% + 8px);}
1211  .tip-right[data-tip]::after {left:auto; right:0; transform:none;}
1212  .tip-left[data-tip]::after {left:0; transform:none;}
1213  /* the star opens the toolbar and Copy closes it: open inward so the answer card never cuts them off */
1214  .tool-box .button-favorite[data-tip]::after {left:0; transform:none;}
1215  .tool-box .button-copy[data-tip]::after {left:auto; right:0; transform:none;}
1216  /* The Read highlight pop-up (.snippetOptions) scrolls (overflow:auto) and its toolbar sits at its top edge, so a tip drawn above an
1217     icon would be cut off. There the tip is placed against the window instead (position:fixed; --tt-x/--tt-y set by the script on hover),
1218     so the pop-up's edge cannot clip it. The pop-up is moved with left/top, never a transform, which keeps fixed = window. */
1219  .snippetOptions .tool-box [data-tip]::after {position:fixed; left:var(--tt-x,0); top:var(--tt-y,0); right:auto; bottom:auto; transform:translate(-50%, calc(-100% - 8px));}
1220  /* first and last icons open inward, so a pop-up near the window's edge keeps them on screen */
1221  .snippetOptions .tool-box > .clickable:first-child [data-tip]::after {transform:translate(-18px, c
1221alc(-100% - 8px));}
1222  .snippetOptions .tool-box > .clickable:last-child [data-tip]::after {transform:translate(calc(-100% + 18px), calc(-100% - 8px));}
1223}
1224/* "Copied" (2026-09-27, April): tapping Copy turns its tip into "Copied" for 1.5 s, in the same ink pill and the same spot.
1225   Outside the hover query on purpose: phones have no tips, and this is their only sign the copy worked.
1226   Shows even when the Copy tip has retired. Replaces the old grey .guide-copied label in toolbars. */
1227.tool-box .guide-copied {display:none !important;}
1228.tool-box .button-copy.tt-flash {position:relative; display:inline-block;}
1229.tool-box .button-copy.tt-flash::after {content:'Copied'; display:block; position:absolute; top:auto; left:auto; right:0; bottom:calc(100% + 8px); transform:none;
1230  background:#211d1e; color:#fff; font-size:12px; line-height:1; font-weight:500; font-style:normal; letter-spacing:normal; text-transform:none;
1231  padding:6px 9px; border-radius:6px; white-space:nowrap; pointer-events:none; opacity:1; transition:none; z-index:50;}
1232.snippetOptions .tool-box .button-copy.tt-flash::after {position:fixed; left:var(--tt-x,0); top:var(--tt-y,0); right:auto; bottom:auto; transform:translate(-50%, calc(-100% - 8px));}
1233</style>
1234<script>
1235// Inside the highlight pop-up, tips are placed against the window: remember where the icon is (see the style above).
1236function ttPlaceFixed(el) {
1237  if (!el || !el.closest || !el.closest('.snippetOptions')) return;
1238  var r=el.getBoundingClientRect(); el.style.setProperty('--tt-x', (r.left+r.width/2)+'px'); el.style.setProperty('--tt-y', r.top+'px');
1239}
1240(function(){
1241  // Copy -> "Copied" for 1.5 s, on every device (see the style above).
1242  if (document.addEventListener) document.addEventListener('click', function(e) {
1243    var b=e.target && e.target.closest ? e.target.closest('.tool-box .button-copy') : null; if (!b) return;
1244    ttPlaceFixed(b); b.classList.add('tt-flash'); clearTimeout(b.ttFlash); b.ttFlash=setTimeout(function(){b.classList.remove('tt-flash');}, 1500);
1245  }, true);
1246})();
1247(function(){
1248  if (!window.matchMedia || !window.matchMedia('(hover:hover)').matches || !document.addEventListener) return;
1249  var LEARN=3, STORE='tabbiTips1', WAIT=600, COOL=800;
1250  var TT_KEEP=/Free account needed|Record first/;
1251  var seen={};
1252  try {seen=JSON.parse(localStorage.getItem(STORE)||'{}')||{};} catch (e) {}
1253  function save() {try {localStorage.setItem(STORE, JSON.stringify(seen));} catch (e) {}}
1254
1255  // [selector, learning key, 'use' = retires after 3 clicks | 'view' = retires after shown 3 times]
1256  var KEYS=[
1257    ['#toolbox-star','highlight','use'], ['.button-meaning','meaning','use'], ['.button-rephrase','rephrase','use'], ['.button-native','native','use'],
1258    ['.button-favorite, .hx-row-star .img-button','star','use'], ['.button-read-aloud','listen','use'],
1259    ['.button-audio-mphone, .mic-in-toolbar','record','use'], ['.button-audio-playback','playback','use'],
1260    ['.button-translate','translate','use'], ['.button-diff-show','changes','use'], ['.button-copy','copy','use'],
1261    ['.dailyArrow','day','use'], ['.dailyLink','source','use'], ['.hx-del','delete','use'],
1262    ['.hx-search-btn, .hx-search-clear','search','use'], ['.hx-star-toggle','only-starred','use'], ['.hx-hl-toggle','only-highlighted','use'],
1263    ['.backButton','back','use'], ['.hx-pill-hl','highlighted','view'], ['.streakLeft','streak','view'], ['.streakRight','dots','view']];
1264  function keyOf(el) {
1265    for (var i=0;i<KEYS.length;i++) {if (el.matches(KEYS[i][0])) return {k:KEYS[i][1], kind:KEYS[i][2]};}
1266    return {k:'tip:'+(el.getAttribute('data-tip')||''), kind:'use'};
1267  }
1268  function learned(el, info) {return !TT_KEEP.test(el.getAttribute('data-tip')||'') && (seen[info.k]||0)>=LEARN;}
1269
1270  // Answer-toolbar labels, read from the icon's state at the moment the pointer arrives.
1271  function shown(b, sel) {var i=b.querySelector(sel); return !!(i && !i.classList.contains('invisible'));}
1272  function member() {return (typeof userIsMember==='undefined') || !!userIsMember;}
1273  var TOOL=[
1274    // Read highlight pop-up: its first button reuses .button-favorite but saves a highlight (toggleStarGoldSnippet)
1275    ['#toolbox-star', function(b){var p=b.closest('.toolsTarget'); var on=p ? p.getAttribute('starOn')=='true' : shown(b,'.image-two'); return on ? 'Highlighted. Tap to remove' : 'Highlight it';}],
1276    ['.button-meaning', function(){return 'Explain the meaning';}],
1277    ['.button-rephrase', function(){return 'Say it another way';}],
1278    ['.button-native', function(){return 'See it in your language';}],
1279    ['.button-favorite', function(b){var r=b.closest('.oneResult'); var on=r ? r.getAttribute('starOn')=='true' : shown(b,'.image-two'); return on ? 'Starred. Tap to remove' : 'Star to find it later';}],
1280    ['.button-read-aloud', function(b){return shown(b,'.image-two') ? 'Pause' : 'Hear it read aloud';}],
1281    ['.button-audio-mphone', function(b){return shown(b,'.image-two') ? 'Stop recording' : 'Record yourself saying it';}],
1282    ['.button-audio-playback', function(b){return shown(b,'.image-four') ? 'Record first, then hear yourself' : (shown(b,'.image-three') ? 'Pause' : 'Play your recording');}],
1283    ['.button-translate', function(b){return !member() ? 'See it in your language. Free account needed' : (shown(b,'.image-two') ? 'Back to English' : 'See it in your language');}],
1284    ['.button-diff-show', function(b){return shown(b,'.image-three') ? '' : (shown(b,'.image-two') ? 'Hide the changes' : 'Show what changed');}],
1285    ['.button-copy', function(){return 'Copy';}]];
1286  function labelToolbar(t) {
1287    var box=t.closest('.tool-box'); if (!box) return;
1288    for (var i=0;i<TOOL.length;i++) {
1289      var b=t.closest(TOOL[i][0]); if (!b || !box.contains(b)) continue;
1290      if (b.hasAttribute('data-tip') && !b.hasAttribute('data-tip-auto')) return;   // the page set its own words (Daily)
1291      var text=TOOL[i][1](b);
1292      if (text) {b.setAttribute('data-tip', text); b.setAttribute('data-tip-auto','');} else {b.removeAttribute('data-tip');}
1293      return;
1294    }
1295  }
1296
1297  var root=document.documentElement, cur=null, showT=null, coolT=null;
1298  document.addEventListener('mouseover', function(e) {
1299    var t=e.target; if (!t || !t.closest) return;
1300    labelToolbar(t);
1301    var el=t.closest('[data-tip]');
1302    if (el===cur) return;
1303    cur=el; clearTimeout(showT);
1304    if (!el) return;
1305    clearTimeout(coolT);
1306    ttPlaceFixed(el);
1307    var info=keyOf(el);
1308    if (learned(el, info)) {el.setAttribute('data-tip-off',''); return;}
1309    el.removeAttribute('data-tip-off');
1310    var warm=root.classList.contains('tt-warm');
1311    showT=setTimeout(function(){
1312      if (cur!==el) return;
1313      root.classList.add('tt-warm');
1314      if (info.kind=='view') {seen[info.k]=(seen[info.k]||0)+1; save();}
1315    }, warm ? 0 : WAIT);
1316  }, true);
1317  document.addEventListener('mouseout', function(e) {
1318    if (!cur) return;
1319    if (e.relatedTarget && cur.contains(e.relatedTarget)) return;
1320    var r=e.relatedTarget && e.relatedTarget.closest ? e.relatedTarget.closest('[data-tip]') : null;
1321    if (r) return;   // moving straight onto another tip: the mouseover above takes it
1322    cur=null; clearTimeout(showT);
1323    clearTimeout(coolT); coolT=setTimeout(function(){root.classList.remove('tt-warm');}, COOL);
1324  }, true);
1325  // Using a control hides its tip at once (they just acted) and counts toward retiring it.
1326  document.addEventListener('click', function(e) {
1327    var el=e.target && e.target.closest ? e.target.closest('[data-tip]') : null; if (!el) return;
1328    el.setAttribute('data-tip-off','');
1329    var info=keyOf(el); if (info.kind!='use') return;
1330    seen[info.k]=(seen[info.k]||0)+1; save();
1331  }, true);
1332})();
1333</script>
1333
1334
1335<style>
1336/* Toolbox row (2026-09-25, April: the mic/play pill and its icons sat ~3px high on Daily). In standards mode the inline-block
1337   icon spans sit on the baseline, so each cell is ~6px taller than its 26px icon; flex-centre the cells and the pill instead.
1338   The :not() guards keep .invisible and the always-hidden .mic-in-toolbar at display:none (this rule outranks both). */
1339.tool-box > .clickable:not(.invisible):not(.mic-in-toolbar), .tool-box .mic-play-combo:not(.invisible), .tool-box .mic-play-combo > .clickable:not(.invisible) {display:flex; align-items:center; justify-content:center;}
1340/* The highlighter icon beside a chat title is 20x21; style0926.css pins .has-highlights to 16x16, which squeezed it (2026-09-26). */
1341img.has-highlights {height:auto;}
1342/* Width cap, Read guide bar and the tab band moved to WEB-INF/tabbiPageLayout.inc (loaded in the header, before the page box). */
1343</style>
1344<div class='template invisible js-bs-top oneRequest locked'>
1345<div class="blur-overlay"></div> <img src="images/locked.png" class="locked-icon">
1346<div class="requestBasicsAll"> 
1347  <div class="requestBasics1 button-favorite center">
1348    <span class='img-button clickable' onclick="toggleFavoriteReq(this);">
1349      <img width="25px;" class="icon-multi image-one" src="images/star.png">
1350      <img width="25px;" class="icon-multi image-two invisible" src="images/star-selected.png">
1351    </span>
1352  </div> 
1353  <div class="requestBasics1 button-highlight center invisible">
1354    <span class='img-button clickable' onclick="toggleHighlightReq(this);">
1355      <img width="25px;" class="icon-multi image-one invisible" src="images/highlight-black.png">
1356      <img width="25px;" class="icon-multi image-two" src="images/highlight-yellow.png">
1357    </span>
1358  </div> 
1359  <div class='requestBasics' onclick='clickRequestBasics(this);' >
1360    <span class='summarySpan'>
1361      <span class='summary-img invisible'><img src='images/challenge.png' width=16/></span>
1362      <span class='title'><span class='titleText'>GENERATED TITLE</span><img class="has-highlights invisible" src="images/highlight-cut.png"></span> 
1363    </span>
1364    <span class='inputSpan'>INPUT</span>
1365    <div class="info-row">
1366    
1367    <span class="timestampSpan">TIME</span> &#183; <span class="request-mode"><img width="12px" src="images/talk-history.png"></span> &#183; <span class="mode-description">Tabbi Correction</span> &#183; <span class="wordcountSpan">0 words</span>
1368    </div>  
1369  </div> 
1370  <div class='requestSnippets invisible' ></div> 
1371  <div class="requestBasics2 center">
1372    <span class='img-button clickable' onclick="toggleCorr(this,'oneRequest');">
1373      <img class="history-delete gray-icon" width=25px; src='images/delete-black.png'/>
1374      <img class="history-delete red-icon invisible" width=25px; src='images/delete-red.png'/>
1375    </span>
1376  </div>
1377</div>
1378</div>
1379
1380<div class='template invisible js-bs-top oneResult' rsetId=0 corrId=0 diffOn=false starOn=false>
1381
1382  <div class='js-tab-main content-set'>
1383  
1384    <div class='cell-title'>
1385      <span class='iconSpan'>OOOO</span> <span class="titleSpan">TITLE</span>
1386      <span class='button feedback-button invisible'>Feedback</span>
1387    </div>
1388
1389    <div class='result-text'><div class='result-content copyable'>
1390    <span class="contentSpan"><span class='temp-text'>Content</span></span>
1391    </div></div>
1392
1393    <div class='result-diff invisible' onclick='setDiffState(false,this);'><div class='result-content'>
1394      <span class="diffSpan"><span class='temp-text'>DIFF</span></span>
1395    </div></div>
1396
1397    <div class='area-tools-combo2'>
1398      <div class='tool-box'>
1399        <div class='clickable'><span class='button-favorite' onclick='toggleFavorite(this);'><img class='icon-multi image-one' src='images/star.png'><img class='icon-multi image-two invisible' src='images/star-selected.png'></span></div>
1400        
1401        <div class='clickable'><span class='button-read-aloud' onclick='buttonReadAloud(this);'>
1402          <img class='icon-multi image-one' src='images/read.png'>
1403          <img class='icon-multi image-two invisible' src='images/read-selected.png'>
1404        </span></div>
1405
1406        <div class='clickable mic-in-toolbar'><img class='icon-multi image-one' src='images/mic-black.png'><img class='icon-multi image-two invisible' src='images/mic-red.png'></div>
1407
1408        <div class='mic-play-combo invisible'>
1409        <div class='clickable mphone-div invisible'><span class='button-audio-mphone' onclick='toggleRecording(this);'>
1410          <img class='icon-multi image-one' src='images/mic-black.png'>
1411          <img class='icon-multi image-two invisible' src='images/mic-red.png'>
1412        </span></div>
1413        <div class='clickable playback-div invisible'><span class='button-audio-playback' onclick='togglePlayback(this);'>
1414          <img class='icon-multi image-one' src='images/play-black.png'>
1415          <img class='icon-multi image-two invisible' src='images/play-red.png'>
1416          <img class='icon-multi image-three invisible' src='images/pause-red.png'>
1417          <img class='icon-multi image-four invisible' src='images/play-gray.png'>
1418        </span></div>
1419        </div>
1420
1421        
1422        <div class='clickable diff-in-toolbar'><span class='button-diff-show'><img class='icon-multi image-one' src='images/diff.png' ><img class='icon-multi image-two invisible' src='images/diff-selected.png' ><img class='icon-multi image-three invisible' src='images/diff-disabled.png' ></span></div>
1423        <div class='clickable copy-position'><span class='guide-copied'>Copied</span><span class='button-copy' onclick='clipboardHTarget(this,"oneResult","result-content");'><img class='icon-multi image-one' src='images/copy.png' ><img class='icon-multi image-two invisible' src='images/copy-selected.png' ></span></div>
1424
1425      </div>
1426    </div>
1427  </div>
1428
1429</div>
1430<script>
1431let translationRetrieved=false;
1432let translationInProgress=false;
1433let transOn=false;
1434async function showTranslatedTextAsDiff(buttonX) {
1435  let buttonJQ=$(buttonX);
1436  let fromHistory=$(".history-pagination").length>0;
1437  if (fromHistory) {
1438    translationRetrieved=buttonJQ.closest(".oneResult").attr('translationRetrieved'); 
1439    translationInProgress=buttonJQ.closest(".oneResult").attr('translationInProgress');
1440  }
1441   if (!userIsMember) {showX($('.popup-message.translate-access')); return;}
1442  transOn=!transOn;
1443  if (!translationInProgress && !translationRetrieved) {
1444    translationInProgress=true; if (fromHistory) {buttonJQ.closest(".oneResult").attr('translationInProgress',true);}
1445    buttonJQ.closest(".oneResult").find(".diffSpan").html("<div class='fulltext-translation'>Translating...</div>");
1446    toggleDiff(buttonX);
1447    try {
1448    let x=getTargetTextJQ(buttonX,"oneResult","result-content");
1449    let xHtml= x.find(".contentSpan").html();
1450    if (x.find('.dailyText').length>0) {xHtml=x.find('.dailyTitle').html()+" --- "+x.find('.dailyText').html();}
1451    console.log("To be translated: "+xHtml);
1452    let properText= xHtml
1453      .replace(/&nbsp;/gi, ' ')
1454      .replace(/<br\s*\/?>/gi, '\n')
1455      .replace(/<p>([\s\S]*?)<\/p>/g, '$1\n\n')
1456      .replace(/<[^>]*>/g, '')
1457      .replace(/\n{3,}/g, '\n\n')
1458      .trim();
1459    let translatedText= await fetchTranslatedText(properText);
1460    buttonJQ.closest(".oneResult").find(".diffSpan").html("<div class='fulltext-translation'>"+translatedText+"</div>");
1461    translationRetrieved=true; if (fromHistory) {buttonJQ.closest(".oneResult").attr('translationRetrieved',true);}
1462    } catch (e) {  // [fix] was stuck on "Translating..." forever + all translate buttons dead
1463      console.log("translate error:",e);
1464      buttonJQ.closest(".oneResult").find(".diffSpan").html("<div class='fulltext-translation'>Translation unavailable. Please try again.</div>");
1465    } finally {
1466      translationInProgress=false; if (fromHistory) {buttonJQ.closest(".oneResult").attr('translationInProgress',false);}
1467    }
1468  } else {toggleDiff(buttonX);}
1469}
1470async function fetchTranslatedText(text) { 
1471  let url = baseServerURL + 'api/fetcherMini.jsp';
1472  let payload = {input: text,pretext: '',posttext: '', action: 'native'};
1473  let response = await fetch(url, {method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify(payload)});
1474  let responseText = await response.text();
1475  hideX($(".snippetOptions .spinner"));
1476  responseText=toHTML(responseText.trim(),true);
1477  return responseText;
1478}
1479</script>
1479
1480
1481
1482<div class="invisible assets">
1483<div class="countdown"><span class="number">3</span><span class="number">2</span><span class="number">1</span></div>
1484
1485<div class="loading-visuals">
1486<div class="dot-container"><div class="dot"></div><div class="dot"></div><div class="dot"></div></div>
1487</div>
1488
1489<svg class="CloseSVG" width="50px" height="50px" viewBox="0 0 201 201" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
1490    <title>close</title>
1491    <g class="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"><path class="shape-close" fill="#212427" fill-rule="nonzero" d="M100.683765,0.285933465 C45.3504317,0.285933465 0.683765055,44.9526001 0.683765055,100.285933 C0.683765055,155.619267 45.3504317,200.285933 100.683765,200.285933 C156.017098,200.285933 200.683765,155.619267 200.683765,100.285933 C200.683765,44.9526001 156.017098,0.285933465 100.683765,0.285933465 Z M151.572654,134.063711 C156.240114,138.731172 156.240114,146.28633 151.572654,150.952203 C149.12813,153.396727 146.238924,154.508116 143.127813,154.508116 C140.016781,154.508116 136.90559,153.396687 134.682971,150.952203 L101.351225,117.61887 L67.7960666,150.952203 C65.3515428,153.396727 62.4623365,154.508116 59.3512254,154.508116 C56.2401143,154.508116 53.1290032,153.396687 50.9063841,150.952203 C46.2389238,146.284743 46.2389238,138.729584 50.9063841,134.063711 L84.4615428,100.508553 L50.9063841,67.1736319 C46.2389238,62.5061716 46.2389238,54.9510128 50.9063841,50.2851398 C55.5738444,45.6176795 63.1290032,45.6176795 67.7948762,50.2851398 L101.350035,83.8402985 L134.905194,50.2851398 C139.572654,45.6176795 147.127813,45.6176795 151.793686,50.2851398 C156.461146,54.9526001 156.461146,62.5077589 151.793686,67.1736319 L118.240114,100.729981 L151.572654,134.063711 Z" ></path></g>
1492</svg>
1493
1494<svg class="RestartSVG" width="50px" height="50px" viewBox="0 0 201 201" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
1495    <title>Restart</title>
1496    <g class="g-restart" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
1497        <path class="shape-restart" d="M100,0.762600046 C73.4792818,0.762600046 48.0422985,11.2994593 29.2903999,30.053 C10.5385013,48.8065406 0,74.2398293 0,100.7626 C0,127.285371 10.5368593,152.720302 29.2903999,171.4722 C48.0439405,190.224099 73.4772293,200.7626 100,200.7626 C126.522771,200.7626 151.95
14977701,190.225741 170.7096,171.4722 C189.461499,152.71866 200,127.285371 200,100.7626 C200,74.2398293 189.463141,48.8048985 170.7096,30.053 C151.956059,11.3011014 126.522771,0.762600046 100,0.762600046 L100,0.762600046 Z M159.400169,116.369204 L119.217002,88.1165627 L139.240236,81.4507717 C133.857275,70.6187589 124.210392,62.5129141 112.613607,59.0782132 C101.01518,55.6434303 88.5124096,57.1876294 78.0991125,63.3404935 C67.6841734,69.4947944 60.2999975,79.7028386 57.7150434,91.5180126 C55.1301303,103.334418 57.5755371,115.694743 64.467451,125.634728 C71.3594305,135.57348 82.0760913,142.199452 94.0480784,143.921523 C106.020066,145.643718 118.170212,142.308359 127.584338,134.713881 C129.986453,132.784833 133.237001,132.282908 136.108899,133.39739 C138.982439,134.513472 141.041535,137.07589 141.511153,140.122704 C141.980977,143.167835 140.791168,146.23226 138.389053,148.162868 C125.114942,158.803995 107.987414,163.393449 91.1726915,160.81301 C74.3563271,158.2329 59.3905222,148.720745 49.9168726,134.588677 C40.443223,120.456609 37.329127,103.000677 41.3315572,86.4650984 C45.3339874,69.9295199 56.0855412,55.8294714 70.9729395,47.5939068 C85.8603378,39.3583423 103.518239,37.740129 119.654806,43.1362259 C135.789731,48.5323228 148.92427,60.4447866 155.860995,75.9799628 L174.7812,69.7037417 L159.400169,116.369204 Z" fill="#212427" fill-rule="nonzero" transform="translate(100.000000, 100.762600) scale(-1, 1) translate(-100.000000, -100.762600) "></path>
1498    </g>
1499</svg>
1500
1501<svg class="MicSVG" width="50px" height="50px" viewBox="0 0 206 205" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
1502    <title>mic</title>
1503    <g class="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"><path class="shape-mic" fill="#212427" fill-rule="nonzero" d="M103.046334,0 C75.9189527,0 49.9012319,10.7772904 30.720689,29.9591226 C11.5388569,49.1426738 0.762855755,75.1578161 0.762855755,102.284768 C0.762855755,129.41172 11.5388569,155.429871 30.720689,174.610413 C49.9042402,193.792245 75.9193825,204.568247 103.046334,204.568247 C130.173286,204.568247 156.191437,193.792245 175.37198,174.610413 C194.553812,155.426862 205.329813,129.41172 205.329813,102.284768 C205.329813,84.330353 200.604085,66.6913878 191.625803,51.142384 C182.64924,35.5933802 169.736003,22.6801431 154.187429,13.7040101 C138.638425,4.72572819 120.99946,0 103.045045,0 L103.046334,0 Z M81.9155047,58.2434395 C81.9155047,50.6906979 85.9445791,43.7104071 92.4869363,39.9331768 C99.0292935,36.1575795 107.089161,36.1575795 113.62937,39.9331768 C120.171727,43.7104501 124.200801,50.6906979 124.200801,58.2434395 L124.200801,99.6876803 C124.200801,107.240422 120.171727,114.220713 113.62937,117.997943 C107.088732,121.77354 99.0288638,121.77354 92.4869363,117.997943 C85.9445791,114.22067 81.9155047,107.240422 81.9155047,99.6876803 L81.9155047,58.2434395 Z M142.083015,99.6876803 C142.067906,109.198015 138.584446,118.377428 132.282158,125.502121 C125.98176,132.626814 117.297439,137.20814 107.859305,138.386564 L107.859305,157.640597 L128.291762,157.640597 C130.984516,157.640597 133.16705,159.823001 133.16705,162.514165 C133.16705,165.205329 130.984645,167.387733 128.291762,167.387733 L77.7510534,167.387733 C75.0616542,167.387733 72.8791638,165.205329 72.8791638,162.514165 C72.8791638,159.823001 75.0615683,157.640597 77.7527321,157.640597 L98.2337527,157.640597 L98.2337527,138.386564 C88.7956191,137.208054 80.1112985,132.626814 73.8109006,125.502121 C67.5087837,118.377428 64.0250848,109.197585 64.0100429,99.6876803 L64.0100429,77.2323059 C64.0100429,74.541228 66.1924475,72.3587375 68.8836113,72.3587375 C71.5746891,72.3587375 73.7571796,74.5411421 73.7571796,77.2323059 L73.7571796,99.7113176 C73.7571796,110.166712 79.3355674,119.828325 88.3894884,125.056022 C97.4446988,130.283719 108.600185,130.283719 117.655825,125.056022 C126.711035,119.828325 132.288134,110.166712 132.288134,99.7113176 L132.288134,77.2323059 C132.288134,74.541228 134.470538,72.3587375 137.161702,72.3587375 C139.854456,72.3587375 142.035271,74.5411421 142.035271,77.2323059 L142.083015,99.6876803 Z" ></path></g>
1504</svg>
1505
1506<svg class="PauseSVG" width="50px" height="50px" viewBox="0 0 201 201" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
1507    <title>pause</title>
1508    <g class="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"><path class="shape-pause" fill="#212427" fill-rule="nonzero" d="M171.155386,30.2412184 C189.249591,48.3370606 200.44414,73.3374699 200.44414,100.952464 C200.44414,128.567458 189.250819,153.567458 171.155386,171.66371 C153.057907,189.757915 128.055042,200.952464 100.44414,200.952464 C72.8332385,200.952464 47.8291462,189.759143 29.7328947,171.66371 C11.6386895,153.566231 0.444140435,128.563366 0.444140435,100.952464 C0.444140435,73.3415622 11.6374618,48.3374699 29.7345317,30.2412184 C47.8303739,12.1470132 72.8307831,0.950827218 100.445777,0.950827218 C128.05627,0.950827218 153.059135,12.1441485 171.155386,30.2412184 Z M71.9607553,56.5041093 C69.3295145,56.5041093 67.1985192,58.6365777 67.1985192,61.2663453 L67.1985192,140.628761 C67.1985192,143.260002 69.3309877,145.390997 71.9607553,145.390997 L88.8735069,145.390997 C91.5047477,145.390997 93.635743,143.258529 93.635743,140.628761 L93.635743,61.2663453 C93.635743,58.6351045 91.5032745,56.5041093 88.8735069,56.5041093 L71.9607553,56.5041093 Z M112.012728,56.5041093 C109.381487,56.5041093 107.250492,58.6365777 107.250492,61.2663453 L107.250492,140.628761 C107.250492,143.260002 109.38296,145.390997 112.012728,145.390997 L128.925479,145.390997 C131.55672,145.390997 133.687715,143.258529 133.687715,140.628761 L133.687715,61.2663453 C133.687715,58.6351045 131.555247,56.5041093 128.925479,56.5041093 L112.012728,56.5041093 Z" ></path></g>
1509</svg>
1510
1511
1512<svg class="SubmitSVG" width="50px" height="50px" viewBox="0 0 201 201" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
1513    <title>submit</title>
1514    <g class="base" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
1515        <g class="shape-submit" transform="translate(0.294576, 0.319687)" fill="#212427" fill-rule="nonzero">
1516            <path d="M145.921053,52.6315789 C145.730147,52.6330474 145.540714,52.6712293 145.363008,52.741718 L37.2915789,92.9334474 C37.0272481,93.0303684 36.8495489,93.2800188 36.8435345,93.560515 C36.8378102,93.8424699 37.0052218,94.0979962 37.2651504,94.2066805 L80.723797,112.169462 L113.655376,79.2378835 C114.636353,78.2231466 115.98297,77.6430714 117.395677,77.6313421 C118.806917,77.619594 120.163835,78.174688 121.162594,79.173297 C122.161165,80.1704023 122.716278,81.5287857 122.704549,82.9402143 C122.691332,84.3514549 122.112744,85.6995376 121.098008,86.680515 L88.0021429,119.780891 L102.433346,162.247056 L102.434814,162.247056 C102.500897,162.534883 102.766698,162.730214 103.060416,162.70965 C103.724175,162.70965 103.655153,162.436508 103.718311,162.277921 L147.248386,54.6688985 L147.249855,54.6688985 C147.424606,54.2195376 147.373208,53.7143496 147.108877,53.3090489 C146.846012,52.9052143 146.40399,52.6540865 145.922336,52.6315789 L145.921053,52.6315789 Z" class="Path1"></path>
1517            <path class="path2" d="M100,0 C73.4785714,0 48.0413534,10.5349624 29.2894737,29.2894737 C10.5349624,48.0424812 0,73.4774436 0,100 C0,126.522556 10.5349624,151.958647 29.2894737,170.710526 C48.0424812,189.465038 73.4774436,200 100,200 C126.522556,200 151.958647,189.465038 170.710526,170.710526 C189.465038,151.957519 200,126.522556 200,100 C199.97063,73.487594 189.425188,48.0676692 170.678195,29.3233083 C151.931203,10.5763158 126.512782,0.0308270677 100.001504,0 L100,0 Z M157,58.6165414 L113.473684,166.225564 C112.64985,168.303496 111.218045,170.083459 109.366165,171.333083 C107.514361,172.584248 105.329323,173.248045 103.093985,173.236432 L102.762102,173.236432 L102.76357,173.236432 C100.472668,173.183414 98.2552992,172.425677 96.4109383,171.065827 C94.5679684,169.705977 93.1890211,167.810113 92.4635699,165.63688 L77.7271038,122.32485 L33.2421414,103.936128 C30.4490211,102.780414 28.2417654,100.545338 27.1199609,97.7406391 C25.9965398,94.9343233 26.0538331,91.7947744 27.2770925,89.0323684 C28.5018293,86.268609 30.7882955,84.1158271 33.6210774,83.0628195 L141.700025,42.8710902 C144.623822,41.7843985 147.85604,41.8783835 150.710927,43.1339549 C153.565739,44.3910226 155.81995,46.709782 156.994762,49.5982406 C158.168108,52.4882782 158.17104,55.7219248 157.000636,58.6132782 L157,58.6165414 Z" ></path>
1518        </g>
1519    </g>
1520</svg>
1521
1522<svg class="ResetSVG" width="50px" height="50px" viewBox="0 0 200 201" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
1523    <title>reset</title>
1524    <g class="icons" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"><path d="M100,0.762600046 C73.4792818,0.762600046 48.0422985,11.2994593 29.2903999,30.053 C10.5385013,48.8065406 0,74.2398293 0,100.7626 C0,127.285371 10.5368593,152.720302 29.2903999,171.4722 C48.0439405,190.224099 73.4772293,200.7626 100,200.7626 C126.522771,200.7626 151.95
15247701,190.225741 170.7096,171.4722 C189.461499,152.71866 200,127.285371 200,100.7626 C200,74.2398293 189.463141,48.8048985 170.7096,30.053 C151.956059,11.3011014 126.522771,0.762600046 100,0.762600046 L100,0.762600046 Z M159.400169,116.369204 L119.217002,88.1165627 L139.240236,81.4507717 C133.857275,70.6187589 124.210392,62.5129141 112.613607,59.0782132 C101.01518,55.6434303 88.5124096,57.1876294 78.0991125,63.3404935 C67.6841734,69.4947944 60.2999975,79.7028386 57.7150434,91.5180126 C55.1301303,103.334418 57.5755371,115.694743 64.467451,125.634728 C71.3594305,135.57348 82.0760913,142.199452 94.0480784,143.921523 C106.020066,145.643718 118.170212,142.308359 127.584338,134.713881 C129.986453,132.784833 133.237001,132.282908 136.108899,133.39739 C138.982439,134.513472 141.041535,137.07589 141.511153,140.122704 C141.980977,143.167835 140.791168,146.23226 138.389053,148.162868 C125.114942,158.803995 107.987414,163.393449 91.1726915,160.81301 C74.3563271,158.2329 59.3905222,148.720745 49.9168726,134.588677 C40.443223,120.456609 37.329127,103.000677 41.3315572,86.4650984 C45.3339874,69.9295199 56.0855412,55.8294714 70.9729395,47.5939068 C85.8603378,39.3583423 103.518239,37.740129 119.654806,43.1362259 C135.789731,48.5323228 148.92427,60.4447866 155.860995,75.9799628 L174.7812,69.7037417 L159.400169,116.369204 Z" class="shape-reset" fill="#212427" fill-rule="nonzero" transform="translate(100.000000, 100.762600) scale(-1, 1) translate(-100.000000, -100.762600) "></path></g>
1525</svg>
1526
1527<svg class="DoneSVG" width="50px" height="50px" viewBox="0 0 200 200" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
1528    <title>done-bg</title>
1529    <g class="base" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
1530        <g class="shape-submit" fill="#212427" fill-rule="nonzero">
1531            <path d="M100,0 C73.4783354,0 48.0435435,10.5370077 29.2889898,29.2907042 C10.5352934,48.0444006 0,73.4813354 0,100.001714 C0,126.522093 10.5352934,151.958171 29.2889898,170.712724 C48.0426863,189.466421 73.4796211,200.001714 100,200.001714 C126.520379,200.001714 151.956457,189.466421 170.71101,170.712724 C189.464707,151.959028 200,126.522093 200,100.001714 C200,82.4484635 195.379505,65.20336 186.603523,49.9995714 C177.826255,34.7983543 165.201217,22.1737453 150.000429,13.3964771 C134.797497,4.62049458 117.552394,0 99.9982857,0 L100,0 Z M148.30069,75.5042215 L108.30069,135.505079 L108.30069,135.503404 C106.832469,137.724979 104.540608,139.268544 101.932456,139.794323 C99.3241761,140.318344 96.6138088,139.782604 94.4006343,138.30433 L64.4002057,118.303473 C61.649595,116.235929 60.1563022,112.899538 60.4458921,109.470918 C60.7354819,106.042298 62.7679253,103.001968 65.8266832,101.425232 C68.8836412,99.8481948 72.5382077,99.9536678 75.4996786,101.703138 L97.1998457,116.202774 L131.700339,64.5034642 L131.700339,64.5051386 C133.769554,61.7545279 137.105987,60.2612352 140.534608,60.550825 C143.963228,60.8404491 147.001843,62.8728582 148.580294,65.9299019 C150.157331,68.9885313 150.051858,72.6431406 148.300716,75.6046115 L148.30069,75.5042215 Z" id="Shape"></path>
1532        </g>
1533    </g>
1534</svg>
1535
1536</div>
1537
1538<style>
1539  .main-box.talk-page .area-buttons-talk .button-set1 .button-submit-talk {position:relative;}
1540  /* the one-time hint: same ink pill as the site's tooltips (tabbiTips.inc) */
1541  .main-box.talk-page .tf-hint {position:absolute; left:50%; bottom:calc(100% + 8px); transform:translateX(-50%); background:#211d1e; color:#fff;
1542    font-size:12px; line-height:1; font-weight:500; padding:6px 9px; border-radius:6px; white-space:nowrap; pointer-events:none; opacity:0;
1543    transition:opacity .2s ease; z-index:60;}
1544  .main-box.talk-page .tf-hint.on {opacity:1;}
1545  .main-box.talk-page .area-buttons-talk .button-submit-talk.tf-busy {opacity:.55; pointer-events:none;}
1546  .main-box.talk-page .tf-note {color:#bf3143;}
1547  /* 3-2-1 countdown replaced by the real state */
1548  .main-box.talk-page .countdown-animation {display:none !important;}
1549  .main-box.talk-page .button-big-mic.tf-connecting img {animation:tfPulse 1s ease-in-out infinite;}
1550  @keyframes tfPulse {0%,100% {transform:scale(1); opacity:1;} 50% {transform:scale(.93); opacity:.55;}}
1550
1551  .main-box.talk-page .talk-mode:has(.area-talk-input.recording-on) .status-message::before {content:'Listening \00B7  '; font-weight:600; color:#bf3143;
1552    padding-left:16px; background:radial-gradient(circle at 5px 50%, #bf3143 4.5px, transparent 5.5px) no-repeat; animation:tfBlink 1.2s ease-in-out infinite;}
1553  @keyframes tfBlink {0%,100% {opacity:1;} 50% {opacity:.45;}}
1554</style>
1555<script>
1556(function(){
1557  if (!document.querySelector('.main-box.talk-page')) {return;}
1558  const sleep=(ms)=>new Promise(r=>setTimeout(r,ms));
1559  const q=(s)=>document.querySelector('.area-audiogroup '+s);
1560  const isRecording=()=>{const t=q('.button-toggle-voice'); return !!(t && t.classList.contains('voiceIsOn'));};
1561  const spokenText=()=>{const a=q('.js-area-input'); return a ? a.innerText.replace(/\s+/g,' ').trim() : '';};
1562
1563  // Fix 2: use the name the server really saved the recording under.
1564  window.__tfUpload=null;
1565  // Fix 5 (April 2026-09-29): pausing and resuming makes one audio piece per segment, and the page gave the play button (and the saved
1566  // copy) only the newest piece. With 2+ pieces we join them in order into one 16 kHz WAV and use that for both. One piece = unchanged.
1567  const parts=[];
1568  async function doUpload(blob, name){   // true once the server has it; only then does the chat point at it
1569    const fd=new FormData(); fd.append('file', new File([blob], name)); fd.append('filename', name);
1570    const r=await fetch(baseServerURL+'api/fileUpload.jsp', {method:'POST', body:fd});
1571    const d=r.ok ? await r.json() : null;
1572    if (!d || d.success!=='true') {console.error('audio upload refused: '+name+' ('+r.status+')'); return false;}
1573    latestPlaybackId=d.savedAs || name;   // guests: scrambled name (fix 2)
1574    console.log('audioFile uploaded: '+latestPlaybackId);
1575    return true;
1576  }
1577  function wavFrom(buf){
1578    const n=buf.length, rate=buf.sampleRate, data=buf.getChannelData(0), out=new DataView(new ArrayBuffer(44+n*2));
1579    const w=(o,t)=>{for (let i=0;i<t.length;i++) {out.setUint8(o+i, t.charCodeAt(i));}};
1580    w(0,'RIFF'); out.setUint32(4,36+n*2,true); w(8,'WAVE'); w(12,'fmt '); out.setUint32(16,16,true); out.setUint16(20,1,true); out.setUint16(22,1,true);
1581    out.setUint32(24,rate,true); out.setUint32(28,rate*2,true); out.setUint16(32,2,true); out.setUint16(34,16,true); w(36,'data'); out.setUint32(40,n*2,true);
1582    for (let i=0,o=44;i<n;i++,o+=2) {const v=Math.max(-1,Math.min(1,data[i])); out.setInt16(o, v<0 ? v*0x8000 : v*0x7FFF, true);}
1583    return new Blob([out], {type:'audio/wav'});
1584  }
1585  async function joinParts(blobs){
1586    const AC=window.AudioContext||window.webkitAudioContext, OAC=window.OfflineAudioContext||window.webkitOfflineAudioContext;
1587    const ctx=new AC();
1588    try {
1589      const bufs=[];
1590      for (const b of blobs) {const ab=await b.arrayBuffer(); bufs.push(await new Promise((res,rej)=>ctx.decodeAudioData(ab,res,rej)));}
1591      const rate=16000, total=bufs.reduce((n,b)=>n+Math.ceil(b.duration*rate),0);
1592      const off=new OAC(1,total,rate); let t=0;
1593      for (const b of bufs) {const src=off.createBufferSource(); src.buffer=b; src.connect(off.destination); src.start(t); t+=b.duration;}
1594      return wavFrom(await off.startRendering());
1595    } finally {try {ctx.close();} catch (e) {}}
1596  }
1597  // Each piece is uploaded as before (small, always playable), so the chat never points at a file that isn't there. With 2+ pieces
1598  // the joined copy follows and takes over only once the server has it (the web server refuses uploads over 1 MB, about 30 s of WAV;
1599  // a refused or slow joined copy leaves the chat on the last piece, never on a missing file - April's 17:03 talk, 2026-09-29).
1600  window.uploadAudioFile=function(audioBlob, filename){
1601    if (audioBlob && audioBlob.size>0) {parts.push(audioBlob);}
1602    window.__tfUpload=(async function(){
1603      try {
1604        const saved=await doUpload(audioBlob, filename);
1605        if (!saved && latestPlaybackId===filename) {latestPlaybackId='';}   // nothing to play rather than a broken play button
1606        if (parts.length<2) {return;}
1607        let joined=null;
1608        try {joined=await joinParts(parts);} catch (e) {console.error('joining the recording failed; keeping the last piece', e);}
1609        if (!joined) {return;}
1610        const pp=document.getElementById('playbackPlayer');
1611        if (pp) {pp.src=URL.createObjectURL(joined); try {pp.playbackRate=FF_defaultAudioPlaybackRate;} catch (e) {}}
1612        await doUpload(joined, filename.replace(/\.[A-Za-z0-9]+$/,'')+'-all.wav');
1613      } catch (e) {console.error('audio upload failed', e);}
1614    })();
1615    return window.__tfUpload;
1616  };
1617  document.addEventListener('click', function(ev){   // a new talk starts from nothing
1618    if (ev.target && ev.target.closest && ev.target.closest('.area-audiogroup .button-reset-text, .area-audiogroup .button-restart-text')) {parts.length=0;}
1619  }, true);
1620
1621  // Fix 7 (April 2026-09-29: "when there is a longer pause on talking, I want to see space"): a silence of 2 s or more between two
1622  // stretches of speech starts a new paragraph (a blank line: a single line break looked like ordinary wrapping). The transcription service reports when each word was spoken; the
1623  // page joins its turns with a space, so the turn that follows a long silence gets a leading newline before the page sees it.
1624  // (The page's script is versioned and shared, so this wraps the socket instead of editing it.)
1625  const GAP_MS=2000;
1626  let resumedJustNow=false;   // the first words after pause/resume already start on a new line
1627  (function(){
1628    const NativeWS=window.WebSocket; if (!NativeWS) {return;}
1629    function TfWS(url, protocols){
1630      const ws=(protocols===undefined) ? new NativeWS(url) : new NativeWS(url, protocols);
1631      if (String(url).indexOf('streaming.assemblyai.com')<0) {return ws;}
1632      const span={}, lineBreak={}; let handler=null;
1633      Object.defineProperty(ws, 'onmessage', {configurable:true, get:function(){return handler;}, set:function(fn){handler=fn;}});
1634      ws.addEventListener('message', function(ev){
1635        if (!handler) {return;}
1636        let out=ev.data;
1637        try {
1638          const d=JSON.parse(ev.data);
1639          const w=d && d.type==='Turn' && Array.isArray(d.words) ? d.words : null;
1640          if (w && w.length) {
1641            const n=d.turn_order||0;
1642            span[n]={start:w[0].start, end:w[w.length-1].end};
1643            if (lineBreak[n]===undefined) {
1644              const prev=span[n-1];
1645              lineBreak[n]=!resumedJustNow && !!prev && (w[0].start-prev.end)>=GAP_MS;
1646              resumedJustNow=false;
1647            }
1648            if (lineBreak[n] && d.transcript) {d.transcript='\n\n'+d.transcript; out=JSON.stringify(d);}
1649          }
1650        } catch (e) {}
1651        handler.call(ws, {data:out});
1652      });
1653      return ws;
1654    }
1655    TfWS.prototype=NativeWS.prototype;
1656    ['CONNECTING','OPEN','CLOSING','CLOSED'].forEach(function(k){TfWS[k]=NativeWS[k];});
1657    window.WebSocket=TfWS;
1658  })();
1659  document.addEventListener('click', function(ev){
1660    if (ev.target && ev.target.closest && ev.target.closest('.area-audiogroup .button-toggle-voice') && !isRecording()) {resumedJustNow=true;}
1661  }, true);
1662
1663  // The empty box holds the markup's line breaks, and each segment starts with a newline: trim blank space before the first words.
1664  let trimming=false;
1665  function trimLeadingBlank(){
1666    const a=q('.talk-mode .js-area-input'); if (!a || trimming) {return;}
1667    let n=a.firstChild; trimming=true;
1668    try {
1669      while (n) {
1670        if (n.nodeType===3) {
1671          const t=n.nodeValue.replace(/^[\s\u00a0]+/,'');
1672          if (t.length) {if (t!==n.nodeValue) {n.nodeValue=t;} break;}
1673          const next=n.nextSibling; a.removeChild(n); n=next; continue;
1674        }
1675        if (n.nodeType===1 && n.classList.contains('cursor-talk-input')) {n=n.nextSibling; continue;}
1676        if (n.nodeName==='BR') {const next=n.nextSibling; a.removeChild(n); n=next; continue;}
1677        break;
1678      }
1679    } finally {trimming=false;}
1680  }
1681  function note(msg){
1682    const s=q('.status-message'); if (!s) {return;}
1683    s.innerHTML='<span class="tf-note">'+msg+'</span>';
1684  }
1685  function captions(){   // tooltip texts (shown by tabbiTips on hover-capable screens)
1686    const reset=document.querySelectorAll('.area-audiogroup .button-reset-text, .area-audiogroup .button-restart-text');
1687    reset.forEach(b=>b.setAttribute('data-tip','Start over'));
1688    document.querySelectorAll('.area-audiogroup .button-submit-talk').forEach(b=>b.setAttribute('data-tip','Get feedback'));
1689    const t=q('.button-toggle-voice');
1690    if (t) {t.setAttribute('data-tip', t.classList.contains('timeOver') ? "Time's up" : isRecording() ? 'Pause' : (spokenText() ? 'Resume' : 'Record'));}
1691  }
1692  // The one-time hint for the key step (phones included).
1693  const HINT_KEY='tabbiTalkHint1', HINT_TIMES=3;
1694  let hintTimer=null, wasRecording=false;
1695  function hintCount(){try {return parseInt(localStorage.getItem(HINT_KEY)||'0',10)||0;} catch (e) {return HINT_TIMES;}}
1696  function hideHint(){clearTimeout(hintTimer); const h=q('.tf-hint'); if (h) {h.classList.remove('on');}}
1697  function onRecordingChange(){
1698    const now=isRecording();
1699    if (now) {const big=q('.button-big-mic'); if (big) {big.classList.remove('tf-connecting');}}
1700    if (now && !wasRecording && hintCount()<HINT_TIMES) {
1701      clearTimeout(hintTimer);
1702      hintTimer=setTimeout(function(){
1703        const s=q('.button-set1 .button-submit-talk'); if (!s || !isRecording()) {return;}
1704        let h=s.querySelector('.tf-hint'); if (!h) {h=document.createElement('span'); h.className='tf-hint'; h.textContent='Tap when you\u2019re done'; s.appendChild(h);}
1705        h.classList.add('on');
1706        try {localStorage.setItem(HINT_KEY, String(hintCount()+1));} catch (e) {}
1707      }, 3500);
1708    }
1709    if (!now) {hideHint();}
1710    wasRecording=now;
1711  }
1712  // Fix 1a: "Get feedback" stays usable while recording.
1713  function keepSubmitUsable(){
1714    const s=q('.button-set1 .button-submit-talk');
1715    if (s && isRecording() && !s.classList.contains('js-active')) {s.classList.add('js-active');}
1716  }
1717
1718  // Fix 1b: one step from speaking to feedback.
1719  let passThrough=false;
1720  document.addEventListener('click', async function(ev){
1721    const b=ev.target && ev.target.closest && ev.target.closest('.area-audiogroup .button-submit-talk');
1722    if (!b || passThrough) {return;}
1723    if (!isRecording() && spokenText() && !window.__tfUpload) {return;}   // normal case: let the page send it
1724    ev.preventDefault(); ev.stopImmediatePropagation();
1725    b.classList.add('tf-busy');
1726    try {
1727      if (isRecording()) {
1728        $(q('.button-toggle-voice')).trigger('click');                 // stop; the page waits for the last words
1729        const t0=Date.now();
1730        while (Date.now()-t0<8000 && (isRecording() || !b.classList.contains('js-active'))) {await sleep(120);}
1731        const t1=Date.now();                                            // the recording's upload starts just after the stop
1732        while (!window.__tfUpload && Date.now()-t1<2000) {await sleep(100);}
1733      }
1734      if (!spokenText()) {note("We didn\u2019t catch any words. Tap Record and try again, a little closer to the mic."); return;}
1735      if (window.__tfUpload) {await Promise.race([window.__tfUpload, sleep(10000)]); window.__tfUpload=null;}
1736      passThrough=true; b.classList.remove('tf-busy'); b.click();
1737    } finally {passThrough=false; b.classList.remove('tf-busy');}
1738  }, true);
1739
1740  $(function(){
1741    setTimeout(function(){
1742      captions();
1743      const t=q('.button-toggle-voice'), s=q('.button-set1 .button-submit-talk'), a=q('.js-area-input');
1744      const mo=new MutationObserver(function(){keepSubmitUsable(); captions(); onRecordingChange();});
1745      const sb=q('.button-set1 .button-submit-talk'); if (sb) {sb.addEventListener('mouseenter', hideHint); sb.addEventListener('click', hideHint, true);}
1746      if (t) {mo.observe(t, {attributes:true, attributeFilter:['class']});}
1747      if (s) {mo.observe(s, {attributes:true, attributeFilter:['class']});}
1748      if (a) {new MutationObserver(function(){trimLeadingBlank(); captions();}).observe(a, {childList:true, subtree:true, characterData:true});}
1749      // (1) no browser tooltips from the icons' built-in SVG titles
1750      document.querySelectorAll('.area-audiogroup .button-state svg title').forEach(function(t){t.remove();});
1751      // (3) tapping the mic: pulse + "Getting the mic ready..." until recording truly starts
1752      document.querySelectorAll('.area-audiogroup .button-big-mic, .area-audiogroup .button-toggle-voice').forEach(function(m){
1753        m.addEventListener('click', function(){
1754          if (isRecording()) {return;}
1755          const big=q('.button-big-mic'); if (big) {big.classList.add('tf-connecting');}
1756          setTimeout(function(){ if (!isRecording()) {const st=q('.status-message'); if (st && /connecting/i.test(st.textContent)) {st.textContent='Getting the mic ready\u2026';}} }, 0);
1757        }, true);
1758      });
1759    }, 0);
1760  });
1761})();
1762</script>
1762
1763
1764
1765<style>
1766  .tf-replay {display:inline-flex; cursor:pointer;} .tf-replay svg {width:26px; height:26px; display:block;}
1767</style>
1768<script>
1769(function(){
1770  const REPLAY='<svg viewBox="0 0 26 26" fill="none" stroke="#211d1e" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M5.2 9.2A8.6 8.6 0 1 1 4.6 15"/><path d="M4.6 4.8v4.6h4.6"/></svg>';
1771  const PAUSE_IMG='images/pause-line-black.png';
1772  function player(){return document.getElementById('playbackPlayer');}
1773  function fileOf(el){   // History: the chat's saved recording; Talk: none (the page loads the player itself)
1774    const c=el && el.closest && el.closest('.hasPlayback');
1775    const id=c && c.getAttribute('playbackId');
1776    return id ? '/audex/'+id : null;
1777  }
1778  function isLoaded(el,p){const f=fileOf(el); return !f || p.dataset.tfFile===f;}
1779  function start(p){
1780    try {stopRealAloud();} catch (e) {}
1781    try {p.playbackRate=FF_defaultAudioPlaybackRate;} catch (e) {}
1782    const r=p.play(); if (r && r.catch) {r.catch(function(){});}
1783  }
1784  function load(el,p){   // point the player at this chat's recording, if it isn't already
1785    const f=fileOf(el); if (!f || p.dataset.tfFile===f) {return;}
1786    p.pause(); p.src=f; p.dataset.tfFile=f; p.load();
1787  }
1788  let queued=false;
1789  function renderPlayback(){
1790    const p=player(); if (!p) {return;}
1791    const active=!p.paused && !p.ended;
1792    try {playbackOn=active;} catch (e) {}
1793    document.querySelectorAll('.button-audio-playback').forEach(function(b){
1794      const mine=isLoaded(b,p);
1795      const playing=mine && active, started=mine && p.currentTime>0.05 && !p.ended;
1796      let pz=b.querySelector('.tf-pause');
1797      if (!pz) {pz=document.createElement('img'); pz.className='icon-multi tf-pause'; pz.src=PAUSE_IMG; pz.alt='Pause'; pz.style.display='none'; b.appendChild(pz);}
1798      b.querySelectorAll('.image-two, .image-three').forEach(function(i){if (i.style.display!=='none') {i.style.display='none';}});
1799      const one=b.querySelector('.image-one'); if (one) {one.style.display=playing?'none':'';}
1800      pz.style.display=playing?'':'none';
1801      const combo=b.closest('.mic-play-combo');
1802      if (combo) {
1803        let r=combo.querySelector('.tf-replay-div');
1804        if (!r) {
1805          r=document.createElement('div'); r.className='clickable tf-replay-div invisible';
1806          r.innerHTML='<span class="tf-replay" data-tip="Play from the start" role="button" aria-label="Play from the start">'+REPLAY+'</span>';
1807          combo.appendChild(r);
1808        }
1809        const hide=!(started || playing);
1810        if (r.classList.contains('invisible')!==hide) {r.classList.toggle('invisible', hide);}
1811      }
1812    });
1813  }
1814  function later(){ if (queued) {return;} queued=true; setTimeout(function(){queued=false; renderPlayback();},0); }
1815  // Result cards are copies of a hidden template, and copies don't keep element listeners: listen at the document instead.
1816  document.addEventListener('click', function(ev){
1817    const r=ev.target && ev.target.closest && ev.target.closest('.tf-replay-div'); if (!r) {return;}
1818    ev.stopPropagation(); const p=player(); if (!p) {return;}
1819    load(r,p); if (!p.src) {return;}
1820    p.currentTime=0; start(p); renderPlayback();
1821  });
1822  window.togglePlayback=function(xButton){
1823    const p=player(); if (!p) {return false;}
1824    const b=xButton && xButton.jquery ? xButton[0] : xButton;
1825    if (b && !isLoaded(b,p)) {load(b,p); start(p); renderPlayback(); return false;}   // another chat's recording
1826    load(b,p); if (!p.src) {return false;}
1827    if (p.paused || p.ended) {if (p.ended) {p.currentTime=0;} start(p);}
1828    else {p.pause();}
1829    renderPlayback(); return false;
1830  };
1831  window.setPlaybackState=function(xButton,on){   // older callers (e.g. "stop playback" when read-aloud starts)
1832    const p=player(); if (!p) {return;}
1833    if (on) {if (p.paused || p.ended) {window.togglePlayback(xButton);}}
1834    else {p.pause();}
1835    renderPlayback();
1836  };
1837  const p=player(); if (!p) {return;}
1838  ['play','pause','ended','emptied','loadedmetadata'].forEach(function(ev){p.addEventListener(ev, later);});
1839  p.addEventListener('ended', function(){p.currentTime=0; later();});
1840  new MutationObserver(function(m){ if (m.some(function(x){return x.addedNodes && x.addedNodes.length;})) {later();} })
1841    .observe(document.body, {childList:true, subtree:true});
1842  $(function(){setTimeout(renderPlayback,0);});
1843})();
1844</script>
1844
1845

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.