PageSourceSearch

https://www.memoirmaker.ai/auth.html

html memoirmaker.ai collected 2026-10-05 12:21:03 UTC 58,610 bytes, 1,210 lines download raw bytes

1<!DOCTYPE html>
2<html lang="en">
3<head>
4    <link rel="icon" type="image/x-icon" href="/favicon.ico">
5    <link rel="icon" type="image/svg+xml" href="/favicon.svg">
6    <link rel="apple-touch-icon" href="/apple-touch-icon.png">
7    <meta charset="UTF-8">
8    <meta name="viewport" content="width=device-width, initial-scale=1.0">
9    <title>Login</title>
10    <meta name="description" content="Immortalize the story of your life with our AI-powered memoir service. Capture and share your life's narrative, leaving a legacy that inspires generations.">
11    <link rel="canonical" href="https://www.memoirmaker.ai/auth.html">
12
13    <!-- Open Graph -->
14    <meta property="og:type" content="website">
15    <meta property="og:site_name" content="MemoirMaker.ai">
16    <meta property="og:title" content="MemoirMaker.ai - Transform Memories into a Lasting Legacy">
17    <meta property="og:description" content="Immortalize the story of your life with our AI-powered memoir service. Capture and share your life's narrative, leaving a legacy that inspires generations.">
18    <meta property="og:image" content="https://www.memoirmaker.ai/assets/img/custom/og-image.jpg">
19    <meta property="og:url" content="https://www.memoirmaker.ai/auth.html">
20
21    <!-- Twitter Card -->
22    <meta name="twitter:card" content="summary_large_image">
23
24    <!-- Google Fonts -->
25    <link href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,500;0,600;0,700;1,400&family=Open+Sans:wght@300;400;500;600;700&display=swap" rel="stylesheet">
26    <link href="https://fonts.googleapis.com/icon?family=Material+Icons+Round" rel="stylesheet">
27
28    <!-- Bootstrap 5 -->
29    <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
30    <link href="https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.css" rel="stylesheet">
31
32    <!-- Custom CSS -->
33    <link href="/assets/css/style.css?v=20260619-intent" rel="stylesheet">
34
35      
36
37
38
39
40
41
42
43
44
45<script type="application/ld+json">
46{"@context": "https://schema.org", "@graph": [{"@id": "https://www.memoirmaker.ai/#organization", "@type": "Organization", "description": "MemoirMaker.ai turns memories into a finished memoir \u2014 speak or type fragments, the AI shapes them into chapters in your voice. Co-author support, recurring characters, AI cover art, DOCX + PDF export. Start free, finish a memoir on the web, continue on mobile, then turn the same manuscript into a narrated film with MemoirMovies.com.", "logo": "https://www.memoirmaker.ai/assets/img/custom/logo.png", "name": "MemoirMaker.ai", "url": "https://www.memoirmaker.ai"}, {"@id": "https://www.memoirmaker.ai/#website", "@type": "WebSite", "description": "MemoirMaker.ai turns memories into a finished memoir \u2014 speak or type fragments, the AI shapes them into chapters in your voice. Co-author support, recurring characters, AI cover art, DOCX + PDF export. Start free, finish a memoir on the web, continue on mobile, then turn the same manuscript into a narrated film with MemoirMovies.com.", "inLanguage": "en-US", "name": "MemoirMaker.ai", "publisher": {"@id": "https://www.memoirmaker.ai/#organization"}, "url": "https://www.memoirmaker.ai"}]}
47</script>
vendor: 99 bytes, lines 47-57
47
48
49  
50
51    
52    
53
54    
55    
56    
57    <script async src="https://www.googletagmanager.com/gtag/js?id=
57G-QN7YTR6R5X
vendor: 16 bytes, lines 57-58
57"></script>
58    
58<script>
59      
vendor: 145 bytes, lines 59-62
59window.dataLayer = window.dataLayer || [];
60      function gtag(){dataLayer.push(arguments);}
61      gtag('js', new Date());
62      gtag('config', '
62G-QN7YTR6R5X
vendor: 10 bytes, lines 62-63
62');
63      
63gtag('config', 'AW-11496617174');
64    </script>
64
65    
66    
67    
67<script>
vendor: 403 bytes, lines 67-69
67
68      !function(f,b,e,v,n,t,s){if(f.fbq)return;n=f.fbq=function(){n.callMethod?n.callMethod.apply(n,arguments):n.queue.push(arguments)};if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';n.queue=[];t=b.createElement(e);t.async=!0;t.src=v;s=b.getElementsByTagName(e)[0];s.parentNode.insertBefore(t,s)}(window,document,'script','https://connect.facebook.net/en_US/fbevents.js');
69      fbq('init', '
69320704642247362
vendor: 34 bytes, lines 69-70
69'); fbq('track', 'PageView');
70    
70</script>
70
71    <noscript><img height="1" width="1" style="display:none" src="https://www.facebook.com/tr?id=320704642247362&ev=PageView&noscript=1"/></noscript>
72    
73    
74    
75    <meta name="saashub-verification" content="e4socv195w2b" />
vendor: 64 bytes, lines 75-76
75
76<script async src="https://www.googletagmanager.com/gtag/js?id=
76AW-11496617174
vendor: 12 bytes, line 76
76"></script>
77</head>
78<body>
79    
80
81
82
83
84
85
86
87<nav class="navbar navbar-expand-xl sticky-top shadow-sm hcwf-navbar" style="background: var(--primary-color, #2c3e50); padding: 8px 0; z-index: 1030;">
88    <style>
89      .hcwf-navbar .container-fluid { display: flex; align-items: center; justify-content: space-between; }
90      .hcwf-navbar .navbar-brand { flex: 0 0 min(350px, calc(100% - 84px)); min-width: 0; width: min(350px, calc(100% - 84px)); max-width: min(350px, calc(100% - 84px)); margin-right: .75rem; overflow: visible; padding: 0; }
91      .hcwf-navbar .hcwf-brand-stack { display: flex; flex-direction: column; align-items: flex-start; gap: 2px; }
92      .hcwf-navbar .hcwf-logo-link { display: block; width: 350px; max-width: 100%; line-height: 0; }
93      .hcwf-navbar .navbar-brand img { width: 350px !important; max-width: 100% !important; height: auto; max-height: none; object-fit: contain; object-position: left center; display: block; }
94      .hcwf-navbar .hcwf-support-phone { color: #ffffff !important; display: flex; align-items: center; justify-content: center; min-height: 44px; font-size: .875rem; font-weight: 600; line-height: 1.2; opacity: .9; text-decoration: none; text-align: center; width: 100%; white-space: nowrap; padding: .25rem .35rem; }
95      .hcwf-navbar .hcwf-support-phone:hover { opacity: 1; text-decoration: underline; }
96      .hcwf-navbar .navbar-toggler { flex: 0 0 58px; width: 58px; height: 58px; margin-left: auto; display: inline-flex; align-items: center; justify-content: center; padding: 0; border-color: rgba(255,255,255,.35); }
97      @media (min-width: 75rem) { .hcwf-navbar .navbar-toggler { display: none !important; } }
98      @media (max-width: 74.9375rem) {
99        .hcwf-navbar .container-fluid { flex-wrap: nowrap !important; gap: .5rem; }
100        .hcwf-navbar .navbar-brand { flex-basis: calc(100% - 74px); width: calc(100% - 74px); max-width: calc(100% - 74px); }
101        .hcwf-navbar .navbar-brand img { width: 100%; max-width: 350px !important; }
102        .hcwf-navbar .navbar-collapse { position: absolute; top: 100%; left: 0; right: 0; background: var(--primary-color, #2c3e50); padding: .75rem 1rem 1rem; border-top: 1px solid rgba(255,255,255,.08); box-shadow: 0 12px 24px rgba(0,0,0,.22); }
103        .hcwf-navbar .navbar-nav { align-items: flex-start !important; }
104      }
105      @media (max-width: 767px) {
106        .hcwf-navbar { padding-top: 6px !important; padding-bottom: 6px !important; }
107        .hcwf-navbar .container-fluid { gap: .35rem; }
108        .hcwf-navbar .navbar-brand { flex-basis: calc(100% - 66px); width: calc(100% - 66px); max-width: calc(100% - 66px); margin-right: .35rem; }
109        .hcwf-navbar .navbar-brand img { max-width: min(350px, 100%) !important; }
110        .hcwf-navbar .hcwf-support-phone { font-size: .875rem; }
111        .hcwf-navbar .navbar-toggler { flex-basis: 52px; width: 52px; height: 52px; }
112      }
113          
114      .hcwf-navbar .nav-link.hcwf-nav-cta,
115      .hcwf-navbar .navbar-nav > .nav-item > .nav-link.hcwf-nav-cta {
116        color: var(--mm-accent-text, var(--hcwf-navbar-text, #FFFFFF)) !important;
117      }
118</style>
119    <div class="container-fluid d-flex align-items-center justify-content-between">
120        <div class="navbar-brand hcwf-brand-stack">
121            <a class="hcwf-logo-link" href="/" aria-label="MemoirMaker.ai home">
122                <img src="https://www.memoirmaker.ai/assets/img/custom/logo.png" alt="MemoirMaker.ai">
123            </a>
124            
125            <a class="hcwf-support-phone" href="tel:+19786438662">24/7 Sales / Support: +1 (978) 643-8662</a>
126            
127        </div>
128        <button class="navbar-toggler text-white" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-label="Open navigation menu">
129            <i class="bi bi-list"></i>
130        </button>
131        <div class="collapse navbar-collapse" id="navbarNav">
132            <ul class="navbar-nav ms-auto align-items-center">
133                <li class="nav-item"><a class="nav-link px-3 text-white" href="/">Home</a></li>
134                <li class="nav-item"><a class="nav-link px-3 text-white" href="/#pricing">Pricing</a></li>
135                <li class="nav-item"><a class="nav-link px-3 text-white" href="/contact">Contact</a></li>
136                
137                <li class="nav-item dropdown">
138                    <a class="nav-link px-3 text-white dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">Resources</a>
139                    <ul class="dropdown-menu dropdown-menu-end shadow border-0">
140                        <li><a class="dropdown-item py-2" href="/blog"><i class="bi bi-journal-text me-2"></i>Blog</a></li>
141                        <li><a class="dropdown-item py-2" href="/learn/"><i class="bi bi-book me-2"></i>Guides</a></li>
142                        
143                        <li><a class="dropdown-item py-2" href="/ai-memoir-writer"><i class="bi bi-stars me-2"></i>AI Memoir Writer</a></li>
144                        
145                        <li><a class="dropdown-item py-2" href="/ai-memoir-writing-service"><i class="bi bi-magic me-2"></i>AI Memoir Writing Service</a></li>
146                        
147                        <li><a class="dropdown-item py-2" href="/memoir-writing-app"><i class="bi bi-phone me-2"></i>Memoir Writing App</a></li>
148                        
149                        <li><a class="dropdown-item py-2" href="/write-my-life-story"><i class="bi bi-pencil-square me-2"></i>Write My Life Story</a></li>
150                        
151                        <li><a class="dropdown-item py-2" href="/autobiography-writing-help"><i class="bi bi-journal-richtext me-2"></i>Autobiography Writing Help</a></li>
152                        
153                        <li><a class="dropdown-item py-2" href="/gift-a-memoir"><i class="bi bi-gift me-2"></i>Gift a Memoir</a></li>
154                        
155                        <li><a class="dropdown-item py-2" href="/memoir-writing-service"><i class="bi bi-journal-text me-2"></i>Memoir Writing Service</a></li>
156                        
157                        <li><a class="dropdown-item py-2" href="/memoir-ghostwriting">
157<i class="bi bi-person-lines-fill me-2"></i>Memoir Ghostwriting</a></li>
158                        
159                        <li><a class="dropdown-item py-2" href="/memoirmaker-vs-ghostwriter"><i class="bi bi-columns-gap me-2"></i>MemoirMaker vs Ghostwriter</a></li>
160                        
161                        <li><a class="dropdown-item py-2" href="/voice-to-memoir"><i class="bi bi-mic me-2"></i>Voice to Memoir</a></li>
162                        
163                        <li><a class="dropdown-item py-2" href="/family-history-book"><i class="bi bi-people me-2"></i>Family History Book</a></li>
164                        
165                        <li><a class="dropdown-item py-2" href="/memoir-writing-prompts"><i class="bi bi-card-checklist me-2"></i>Memoir Writing Prompts</a></li>
166                        
167                        <li><a class="dropdown-item py-2" href="/how-to-write-a-memoir"><i class="bi bi-map me-2"></i>How to Write a Memoir</a></li>
168                        
169                        <li><a class="dropdown-item py-2" href="/memoir-structure"><i class="bi bi-diagram-3 me-2"></i>Memoir Structure</a></li>
170                        
171                        <li><a class="dropdown-item py-2" href="/faq"><i class="bi bi-question-circle me-2"></i>FAQ</a></li>
172                    </ul>
173                </li>
174                
175                <li class="nav-item"><a class="nav-link px-3 text-white" href="https://www.archieboy.com/affiliate/">Affiliates</a></li>
176                
177                
178
179<li class="nav-item"><a class="nav-link px-3 text-white" href="/auth.html" style="font-weight: 600;">Login</a></li>
180<li class="nav-item"><a class="nav-link px-3 ms-lg-2 hcwf-nav-cta" href="/register" style="background: #e8c547; --mm-accent-text: #1a1a2e; color: #1a1a2e; border-radius: 50px; padding: 8px 25px; font-weight: 600;">Join Free</a></li>
181
182            </ul>
183        </div>
184    </div>
185</nav>
186
187    <main id="main">
188        
189<style>
190  .auth-shell {
191    min-height: 70vh;
192    display: flex;
193    align-items: center;
194    justify-content: center;
195    padding: 3rem 1rem;
196    background: var(--page-bg, #f6f8fb);
197  }
198  .auth-card {
199    width: 100%;
200    max-width: 500px;
201    margin: 0 auto;
202    background: var(--surface-bg, #ffffff);
203    border: 1px solid var(--border-color, rgba(0,0,0,.08));
204    border-radius: 20px;
205    box-shadow: 0 10px 35px rgba(0,0,0,.12);
206    padding: 2.5rem;
207    color: var(--text-dark, #213547);
208    font-family: var(--font-body, inherit);
209  }
210  .auth-card h1,.auth-card h2,.auth-card h3,.auth-card h4,.auth-card h5,.auth-card h6 { color: var(--text-dark, #213547); font-family: var(--font-display, var(--font-body, inherit)); }
211  .auth-title { text-align: center; margin-bottom: .5rem; color: var(--text-dark, #213547); }
212  .auth-subtitle { text-align: center; color: var(--muted-text, #5b6878); margin-bottom: 1.5rem; }
213  .auth-card .form-label { font-weight: 600; color: var(--text-dark, #213547); }
214  .auth-card .form-text, .auth-card .text-muted, .auth-card small, .auth-card p.small { color: var(--muted-text, #5b6878) !important; }
215  .auth-card .form-control {
216    border-radius: 12px; padding: .8rem .95rem; background: var(--surface-alt-bg, #ffffff);
217    border: 1px solid var(--border-color, #d9e2ec); color: var(--text-dark, #213547);
218  }
219  .auth-card .form-control::placeholder { color: var(--muted-text, #6c757d) !important; opacity: 1; }
220  .auth-card .form-control:focus {
221    background: var(--surface-alt-bg, #ffffff); border-color: var(--secondary-color, var(--accent_bg, #b7eaf8));
222    color: var(--text-dark, #213547); box-shadow: 0 0 0 .2rem color-mix(in srgb, var(--secondary-color, #b7eaf8) 35%, transparent);
223  }
224  .auth-card .btn-primary {
225    border-radius: 999px; padding: .85rem 1.1rem; font-weight: 700; background: var(--primary-color, #2c3e50);
226    border: 1px solid var(--primary-color, #2c3e50); color: var(--mm-brand-text, #ffffff) !important;
227  }
228  .auth-card .btn-outline-secondary {
229    border-radius: 999px; padding: .8rem 1.1rem; color: var(--text-dark, #213547) !important;
230    border: 1px solid var(--border-color, #d9e2ec); background: transparent;
231  }
232  .auth-card .btn-outline-secondary:hover { background: var(--surface-alt-bg, #eef6fb); border-color: var(--secondary-color, #b7eaf8); }
233  .auth-brand-link { color: var(--primary-color, #2c3e50); text-decoration: none; font-weight: 600; }
234  .auth-brand-link:hover { color: color-mix(in srgb, var(--primary-color, #2c3e50) 82%, white); }
235  .auth-icon { color: var(--primary-color, #2c3e50); }
236  .auth-card hr { border-color: var(--border-color, #d9e2ec); opacity: .8; }
237</style>
238
239<section class="auth-shell">
240  <div class="container">
241    <div class="auth-card">
242      <div id="login-form-content">
243        <div class="text-center mb-4">
244          <i class="bi bi-box-arrow-in-right auth-icon" style="font-size:2.5rem;"></i>
245          <h3 class="auth-title mt-2">Welcome Back</h3>
246          <p class="auth-subtitle small">Sign in to your account.</p>
247        </div>
248
249        
250
251        <form id="login-form">
252          <div class="mb-3">
253            <label class="form-label small">Email Address</label>
254            <input type="text" class="form-control" id="login-username" required placeholder="[email protected]" inputmode="email" autocomplete="username" autocapitalize="off" spellcheck="false" autofocus>
255          </div>
256          <div class="mb-3">
257            <label class="form-label small">Password</label>
258            <input type="password" class="form-control" id="login-password" required autocomplete="current-password">
259          </div>
260          <div class="text-end mb-3">
261            <a href="/forgot-password" class="small auth-brand-link">Forgot Password?</a>
262          </div>
263          <button type="submit" class="btn btn-primary w-100" id="login-btn" data-idle-label="Login">Login</button>
264        </form>
265
266        
267        <div class="mt-4 text-center">
268          <div class="d-flex align-items-center my-3">
269            <hr class="flex-grow-1"> <span class="mx-3 small" style="color: var(--muted-text, #5b6878);">OR</span> <hr class="flex-grow-1">
270          </div>
271          <a href="/login/google" class="btn btn-outline-secondary w-100 d-flex align-items-center justify-content-center py-2">
272            <img src="https://www.gstatic.com/images/branding/product/1x/gsa_512dp.png" style="height: 18px; margin-right: 10px;"> Sign in with Google
273          </a>
274        </div>
275        
276
277        
278        <div class="text-center mt-4">
279          
280          <p class="small text-muted">Don't have an account? <a href="/register" class="auth-brand-link fw-bold">Sign Up</a></p>
281          
282          
283          
284        </div>
285      </div>
286
287      <div id="message-container" class="text-center" style="display:none;">
288        <div id="status-icon" class="mb-3"></div>
289        <h4 id="status-title"></h4>
290        <p id="status-text" class="text-muted"></p>
291      </div>
292    </div>
293  </div>
294</section>
295
296    </main>
297
298    <footer id="footer" style="background: var(--mm-navbar-bg, #1a1a2e); padding: 50px 0 20px 0; color: #ccc; font-size: 14px;">
299    <div class="container">
300        <div class="row">
301            <!-- C1: Company Info -->
302            <div class="col-lg-3 col-md-6 mb-4">
303                <div class="mb-3">
304                    <a href="/" aria-label="MemoirMaker.ai home"><img src="https://www.memoirmaker.ai/assets/img/custom/logo.png" alt="MemoirMaker.ai" style="max-height: 45px;"></a>
305                </div>
306                <p style="line-height: 1.8; margin: 0;">
307                    Archieboy Holdings, LLC<br>
308                    #1006, 771 Boston Post Rd STE 11<br>
309                    Marlborough, MA 01752<br>
310                    Phone: +1 978-643-8662<br>
311                    Text: +1 978-643-8662<br>
312                    Email: <a href="#" class="footer-email-link" style="color: var(--mm-accent, #C4A265);"
313                        aria-label="Email MemoirMaker.ai"
314                        data-name="hello" data-domain="memoirmaker.ai"
315                        onclick="this.href='mai'+'lto:'+this.dataset.name+'@'+this.dataset.domain; return true;">
316                        <span class="footer-email" data-name="hello" data-domain="memoirmaker.ai">hello [at] memoirmaker.ai</span>
317                    </a>
318                </p>
319            </div>
320
321            <!-- C2: Quick Links -->
322            <div class="col-lg-3 col-md-6 mb-4">
323                <h5 class="text-white mb-3">Quick Links</h5>
324                <ul style="list-style: none; padding: 0; margin: 0; line-height: 2.2;">
325                    <li><i class="bi bi-chevron-right" style="color: var(--mm-accent, #C4A265); font-size: 14px;"></i> <a href="/" style="color: #ccc;">Home</a></li>
326                    <li><i class="bi bi-chevron-right" style="color: var(--mm-accent, #C4A265); font-size: 14px;"></i> <a href="/blog" style="color: #ccc;">Blog</a></li>
327                    <li><i class="bi bi-chevron-right" style="color: var(--mm-accent, #C4A265); font-size: 14px;"></i> <a href="/faq" style="color: #ccc;">FAQ</a></li>
328                    <li><i class="bi bi-chevron-right" style="color: var(--mm-accent, #C4A265); font-size: 14px;"></i> <a href="/privacy" style="color: #ccc;">Privacy Policy</a></li>
329                    <li><i class="bi bi-chevron-right" style="color: var(--mm-accent, #C4A265); font-size: 14px;"></i> <a href="/terms" style="color: #ccc;">Terms of Service</a></li>
330                    
331                </ul>
332            </div>
333
334            <div class="col-lg-3 col-md-6 mb-4">
335                <h5 class="text-white mb-3">Archieboy Network</h5>
336                <div style="background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1); border-radius: 4px; padding: 14px;">
337                    <a href="https://www.archieboy.com/" target="_blank" rel="noopener" aria-label="Visit Archieboy Holdings">
338                        <img src="https://www.archieboy.com/static/img/logo.png" alt="Archieboy Holdings, LLC" style="width: 190px; max-width: 100%; height: auto; display: block; margin-bottom: 12px;">
339                    </a>
340                    <p style="line-height: 1.7; margin: 0;">
341                        This site is part of the <a href="https://www.archieboy.com/" target="_blank" rel="noopener" style="color: var(--mm-accent, #C4A265);">Archieboy Holdings, LLC</a> network of websites. To become an affiliate of this website and dozens more in our network, visit <a href="https://www.archieboy.com/affiliate/" target="_blank" rel="noopener" style="color: var(--mm-accent, #C4A265);">Our Affiliate Program Page</a>.
342                    </p>
343                </div>
344            </div>
345
346            <!-- C4: Latest Updates -->
347            <div class="col-lg-3 col-md-6 mb-4">
348                <h5 class="text-white mb-3">Latest Updates</h5>
349                <div style="background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1); border-radius: 4px; padding: 12px; max-height: 150px; overflow-y: auto;">
350                    <ul id="updates-list" style="list-style: none; padding: 0; margin: 0;">
351                        
352                        <li style="margin-bottom: 10px; position: relative;" data-id="50">
353                            <span style="font-weight: bold; color: var(--mm-accent, #C4A265);">&#8226; 2026-10-05:</span> Memoir films are now hand-drawn animated movies, scripted from your memoir and made one part at a time with your approval.
354                            
355                        </li>
356                        
357                        <li style="margin-bottom: 10px; position: relative;" data-id="49">
358                            <span style="font-weight: bold; color: var(--mm-accent, #C4A265);">&#8226; 2026-09-24:</span> We&#39;ve upgraded the AI that writes your chapters — expect more thoughtful, natural-sounding memoir prose.
359                            
360                        </li>
361                        
362                        <li style="margin-bottom: 10px; position: relative;" data-id="48">
363                            <span style="font-weight: bold; color: var(--mm-accent, #C4A265);">&#8226; 2026-09-22:</span> iPhone Pro subscribers can now keep writing past three sections, start more memoirs, and download normally.
364                            
365                        </li>
366                        
367                        <li style="margin-bottom: 10px; position: relative;" data-id="47">
368                            <span style="font-weight: bold; color: var(--mm-accent, #C4A265);">&#8226; 2026-09-01:</span> Voice dictation now matches your memoir&#39;s language, for more accurate recording in Danish, Swedish, Norwegian, Finnish, Polish, and more.
369                            
370                        </li>
371                        
372                        <li style="margin-bottom: 10px; position: relative;" data-id="46">
373                            <span style="font-weight: bold; color: var(--mm-accent, #C4A265);">&#8226; 2026-08-27:</span> You can now enter a coupon code at checkout for 10% off your memoir credit pack.
374                            
375                        </li>
376                        
377                    </ul>
378                    
379                </div>
380            </div>
381        </div>
382    </div>
383
384    <!-- Copyright Bar -->
385    <div class="container text-center mt-4 pt-3" style="border-top: 1px solid rgba(255,255,255,0.1);">
386        <p class="mb-0" style="color: #999;">
387            &copy; <a href="/api/login" style="color: #999; text-decoration: none;">Copyright</a> 2026, <strong><a href="https://www.archieboy.com" style="color: #999; text-decoration: none;" target="_blank">Archieboy Holdings, LLC.</a></strong> All Rights Reserved.
388        </p>
389    </div>
390</footer>
391
392<!-- Anti-spam email rendering -->
393<script>
394document.addEventListener('DOMContentLoaded', function() {
395    document.querySelectorAll('.footer-email').forEach(function(el) {
396        var n = el.dataset.name, d = el.dataset.domain;
397        el.textContent = n + '\u0040' + d;
398    });
399});
400</script>
400
401
402
403
404    
404<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
404
405    
405<script>
406      (function(){
407        var key='memoirmaker_anon_id', anon='';
408        try {
409          anon=localStorage.getItem(key)||'';
410          if(!anon){ anon=(crypto.randomUUID ? crypto.randomUUID() : String(Date.now())+Math.random()); localStorage.setItem(key,anon); }
411        } catch(e) {}
412        window.mmTrack=function(eventName, properties, bookid){
413          var body=JSON.stringify({event:eventName,anonymous_id:anon,bookid:bookid||null,source:'web',properties:properties||{}});
414          try {
415            if(navigator.sendBeacon){ navigator.sendBeacon('/api/product-event',new Blob([body],{type:'application/json'})); }
416            else { fetch('/api/product-event',{method:'POST',headers:{'Content-Type':'application/json'},body:body,keepalive:true}); }
417          } catch(e) {}
418        };
419        document.addEventListener('click',function(e){
420          var target=e.target.closest && e.target.closest('[data-mm-event]');
421          if(target){ window.mmTrack(target.dataset.mmEvent,{label:(target.textContent||'').trim(),href:target.getAttribute('href')||''},target.dataset.mmBookid||null); }
422        });
423      })();
424    </script>
424
425    
426<script>
427function showMessage(type, title, text, doRedirect) {
428  document.getElementById('login-form-content').style.display = 'none';
429  const mc = document.getElementById('message-container');
430  mc.style.display = 'block';
431  document.getElementById('status-title').innerText = title;
432  document.getElementById('status-text').innerText = text;
433  if (type === 'success') {
434    document.getElementById('status-icon').innerHTML = '<i class="bi bi-check-circle-fill text-success" style="font-size:3rem;"></i>';
435    if (doRedirect) setTimeout(() => { window.location.href = loginRedirectTarget(); }, 700);
436  } else {
437    document.getElementById('status-icon').innerHTML = '<i class="bi bi-exclamation-triangle-fill text-danger" style="font-size:3rem;"></i>';
438    const btn = document.createElement('button');
439    btn.className = 'btn btn-secondary mt-3';
440    btn.innerText = 'Try Again';
441    btn.onclick = () => { document.getElementById('login-form-content').style.display = 'block'; mc.style.display = 'none'; btn.remove(); };
442    mc.appendChild(btn);
443  }
444}
445
446function loginRedirectTarget() {
447  const params = new URLSearchParams(window.location.search);
448  const next = params.get('next') || '';
449  if (next.startsWith('/') && !next.startsWith('//')) return next;
450  return '/';
451}
452
453document.getElementById('login-form').addEventListener('submit', async (e) => {
454  e.preventDefault();
455  const btn = document.getElementById('login-btn');
456  btn.disabled = true;
457  btn.innerText = 'Signing in...';
458  try {
459    const resp = await fetch('/api/login', {
460      method: 'POST',
461      headers: { 'Content-Type': 'application/json' },
462      body: JSON.stringify({
463        username: document.getElementById('login-username').value,
464        password: document.getElementById('login-password').value
465      })
466    });
467    if (resp.ok || resp.redirected) {
468      showMessage('success', 'Login Successful', 'Welcome back!', true);
469    } else {
470      let msg = 'Invalid credentials';
471      try { const result = await resp.json(); msg = result.error || msg; } catch(e) {}
472      showMessage('error', 'Login Failed', msg);
473      btn.disabled = false;
474      btn.innerText = btn.dataset.idleLabel || 'Login';
475    }
476  } catch (err) {
477    window.location.href = '/';
478  }
479});
480</script>
480
481
482
483
484
485
486
487
488
489
490
491
492
493<div class="riley-dock" id="rileyDock">
494  <button type="button" class="riley-launch" id="rileyLaunch"
495          aria-expanded="false" aria-controls="rileyWidget"
496          
497          aria-label="Questions? Ask Riley">
498    <img src="/riley/static/riley-96.jpg" alt="" width="34" height="34" aria-hidden="true">
499    <span class="riley-launch-label">Questions? Ask Riley</span>
500  </button>
501
502<div class="riley-widget riley-engaged riley-floating" id="rileyWidget"
503     hidden
504     style="--riley-h: 480px;">
505
506  <div class="riley-head">
507    <img class="riley-face" src="/riley/static/riley-96.jpg" alt="Riley" width="38" height="38">
508    <div class="riley-who">
509      <strong>Riley</strong>
510      <span class="riley-sub" id="rileyStatus">Support &middot; AI agent</span>
511    </div>
512    <button type="button" class="riley-mode" id="rileyModeBtn"
513            title="Switch to voice" aria-label="Talk with Riley">
514      <span class="riley-mode-ico" aria-hidden="true"></span>
515      <span id="rileyModeLbl">Talk</span>
516    </button>
517    
518    <button type="button" class="riley-close" id="rileyClose"
519            title="Close" aria-label="Close chat">&times;</button>
520    
521  </div>
522
523  <div class="riley-body">
524    <div class="riley-log" id="rileyLog" role="log" aria-live="polite"></div>
525
526    
527    
528  </div>
529
530  <form class="riley-input" id="rileyForm" autocomplete="off">
531    <input type="text" id="rileyText" maxlength="1200"
532           placeholder="Ask me anything&hellip;" aria-label="Message to Riley">
533    <button type="submit" id="rileySend" aria-label="Send">&#8593;</button>
534  </form>
535
536  <p class="riley-foot">
537    Riley is an AI agent. She can look things up, email you details, and pass
538    anything she can&rsquo;t finish to a person.
539  </p>
540</div>
541
542</div>
543
544
545
546<style>
547/* Riley takes her colours from the site she is standing on.
548 *
549 * Every HCWF site injects a palette into :root at render time, and the pair we
550 * build on is --mm-navbar-bg / --mm-navbar-text. That pair is already a
551 * validated dark surface with a legible foreground on each site — it is what
552 * the header and footer use — so deriving from it keeps the widget on-brand
553 * without anyone hand-picking a colour per site. A site with a LIGHT navbar
554 * gets a light widget for the same reason, and it still reads correctly,
555 * because the foreground travels with the background.
556 *
557 * Everything else — muted text, bubbles, borders, the input well — is mixed
558 * from those two, so there are no third-party colours to drift out of step.
559 * Each mix has a plain rgba fallback declared first for anything too old to
560 * know color-mix().
561 *
562 * Override per include with .
563 */
564.riley-widget {
565  --rw-surface: var(--mm-navbar-bg, var(--primary-color, #1F3B5B));
566  --rw-text: var(--mm-navbar-text, #ffffff);
567  --rw-accent: var(--mm-accent, var(--secondary-color, #F2DFAD));
568  --rw-accent-text: var(--mm-accent-text, #213547);
569
570  display: flex; flex-direction: column;
571  height: var(--riley-h, 480px);
572  background: var(--rw-surface);
573  color: var(--rw-text);
574  border: 1px solid rgba(255,255,255,.16);
575  border: 1px solid color-mix(in srgb, var(--rw-text) 16%, transparent);
576  border-radius: 14px;
577  box-shadow: 0 14px 38px rgba(0,0,0,.28);
578  overflow: hidden; font-size: 15px; line-height: 1.45;
579  font-family: var(--font-body, inherit);
580}
581/* Light variant: swap only the surface pair — the rest re-derives itself. */
582.riley-widget.riley-light {
583  --rw-surface: var(--surface-bg, #ffffff);
584  --rw-text: var(--text-dark, #213547);
585  box-shadow: 0 10px 30px rgba(0,0,0,.10);
586}
587
588.riley-head {
589  display: flex; align-items: center; gap: 10px;
590  padding: 12px 14px; flex: 0 0 auto;
591  border-bottom: 1px solid rgba(255,255,255,.12);
592  border-bottom: 1px solid color-mix(in srgb, var(--rw-text) 12%, transparent);
593  background: rgba(0,0,0,.12);
594  background: color-mix(in srgb, var(--rw-surface) 88%, #000);
595}
596.riley-face { width: 38px; height: 38px; border-radius: 50%; object-fit: cover; flex: 0 0 auto; }
597.riley-who { display: flex; flex-direction: column; line-height: 1.2; min-width: 0; }
598.riley-who strong { font-size: 15px; color: var(--rw-text); }
599.riley-sub {
600  font-size: .875rem;
601  color: rgba(255,255,255,.70);
602  color: color-mix(in srgb, var(--rw-text) 70%, transparent);
603}
604.riley-mode {
605  margin-left: auto; display: inline-flex; align-items: center; gap: 6px;
606  border-radius: 999px; padding: 6px 12px; font-size: .875rem; font-weight: 700;
607  cursor: pointer; white-space: nowrap;
608  background: var(--rw-accent); color: var(--rw-accent-text); border: 0;
609}
610.riley-mode:hover { filter: brightness(1.06); }
611.riley-mode.is-live { background: #c0392b; color: #fff; }
612.riley-mode-ico {
613  display: inline-block; width: .5625rem; height: .5625rem; border-radius: 50%;
614  background: currentColor; flex: 0 0 auto;
615}
616.riley-mode.is-live .riley-mode-ico { animation: riley-pulse 1.3s ease-in-out infinite; }
617@keyframes riley-pulse { 0%,100% { opacity: 1 } 50% { opacity: .25 } }
618
619/* The log and the landing screen share this box; the landing sits on top. */
620.riley-body { position: relative; flex: 1 1 auto; min-height: 0; }
621.riley-log {
622  position: absolute; inset: 0; overflow-y: auto; padding: 14px;
623  display: flex; flex-direction: column; gap: 10px; overscroll-behavior: contain;
624}
625.riley-msg { max-width: 88%; padding: 9px 12px; border-radius: 13px; white-space: pre-wrap;
626             word-wrap: break-word; }
627.riley-msg.riley-them {
628  background: rgba(255,255,255,.11);
629  background: color-mix(in srgb, var(--rw-text) 11%, transparent);
630  color: var(--rw-text);
631  border-bottom-left-radius: 4px; align-self: flex-start;
632}
633/* The visitor's own words wear the site's accent — on SPP that is the same
634   gold as the primary call to action. */
635.riley-msg.riley-me {
636  background: var(--rw-accent); color: var(--rw-accent-text);
637  border-bottom-right-radius: 4px; align-self: flex-end;
638}
639.riley-msg.riley-note {
640  background: none; font-size: .875rem; font-style: italic;
641  color: rgba(255,255,255,.62);
642  color: color-mix(in srgb, var(--rw-text) 62%, transparent);
643  align-self: center; text-align: center; max-width: 100%; padding: 2px 0;
644}
645.riley-typing { display: inline-flex; gap: 4px; align-items: center; }
646.riley-typing i {
647  width: 6px; height: 6px; border-radius: 50%;
648  background: rgba(255,255,255,.65);
649  background: color-mix(in srgb, var(--rw-text) 65%, transparent);
650  animation: riley-bounce 1.1s infinite;
651}
652.riley-typing i:nth-child(2) { animation-delay: .15s }
653.riley-typing i:nth-child(3) { animation-delay: .3s }
654@keyframes riley-bounce { 0%,60%,100% { transform: translateY(0); opacity:.5 }
655                          30% { transform: translateY(-4px); opacity:1 } }
656
657/* Everything here is sized to survive the shortest widget we ship (480px)
658   without the microphone disclosure being pushed under the footer. Keep the
659   total under roughly 370px if you change it. */
660.riley-landing {
661  position: absolute; inset: 0; background: var(--rw-surface); z-index: 2;
662  display: flex; flex-direction: column; align-items: center; justify-content: center;
663  text-align: center; padding: 14px 20px; overflow-y: auto;
664}
665.riley-portrait {
666  width: 112px; height: 112px; border-radius: 50%; object-fit: cover;
667  box-shadow: 0 6px 22px rgba(0,0,0,.35); flex: 0 0 auto;
668  border: 2px solid rgba(255,255,255,.22);
669  border: 2px solid color-mix(in srgb, var(--rw-text) 22%, transparent);
670}
671.riley-landing-title {
672  font-size: 20px; font-weight: 700; margin: 11px 0 0; color: var(--rw-text);
673  font-family: var(--font-display, inherit);
674}
675.riley-landing-copy {
676  font-size: 14px; margin: 6px 0 0; max-width: 34ch;
677  color: rgba(255,255,255,.78);
678  color: color-mix(in srgb, var(--rw-text) 78%, transparent);
679}
680.riley-landing-cta {
681  margin-top: 13px; border: 0; border-radius: 999px; cursor: pointer;
682  background: var(--rw-accent); color: var(--rw-accent-text);
683  font-weight: 700; font-size: 15.5px; padding: 12px 28px;
684  box-shadow: 0 4px 16px rgba(0,0,0,.28);
685}
686.riley-landing-cta:hover { filter: brightness(1.06); }
687.riley-landing-alt {
688  margin-top: 9px; background: none; border: 0; cursor: pointer;
689  font-size: 13.5px; text-decoration: underline; padding: 3px;
690  color: rgba(255,255,255,.80);
691  color: color-mix(in srgb, var(--rw-text) 80%, transparent);
692}
693.riley-landing-note {
694  font-size: 11px; line-height: 1.35; margin: 11px 0 0; max-width: 40ch;
695  color: rgba(255,255,255,.62);
696  color: color-mix(in srgb, var(--rw-text) 62%, transparent);
697}
698.riley-widget.riley-engaged .riley-landing { display: none; }
699
700.riley-input {
701  display: flex; gap: 8px; padding: 10px; flex: 0 0 auto;
702  border-top: 1px solid rgba(255,255,255,.12);
703  border-top: 1px solid color-mix(in srgb, var(--rw-text) 12%, transparent);
704  background: rgba(0,0,0,.10);
705  background: color-mix(in srgb, var(--rw-surface) 92%, #000);
706}
707.riley-input input {
708  flex: 1 1 auto; border-radius: 999px; padding: 10px 14px;
709  font: inherit; font-size: 15px; min-width: 0;
710  color: var(--rw-text);
711  background: rgba(255,255,255,.10);
712  background: color-mix(in srgb, var(--rw-text) 10%, transparent);
713  border: 1px solid rgba(255,255,255,.22);
714  border: 1px solid color-mix(in srgb, var(--rw-text) 22%, transparent);
715}
716.riley-input input::placeholder {
717  color: rgba(255,255,255,.55);
718  color: color-mix(in srgb, var(--rw-text) 55%, transparent);
719}
720.riley-input input:focus { outline: 2px solid var(--rw-accent); outline-offset: -1px; }
721.riley-input button {
722  flex: 0 0 auto; width: 40px; height: 40px; border-radius: 50%; border: 0;
723  background: var(--rw-accent); color: var(--rw-accent-text);
724  font-size: 17px; font-weight: 700; cursor: pointer;
725}
726.riley-input button:disabled { opacity: .45; cursor: default; }
727.riley-foot {
728  margin: 0; padding: 8px 14px 11px; font-size: .875rem; flex: 0 0 auto;
729  color: rgba(255,255,255,.64);
730  color: color-mix(in srgb, var(--rw-text) 64%, transparent);
731  background: rgba(0,0,0,.10);
732  background: color-mix(in srgb, var(--rw-surface) 92%, #000);
733}
734
735/* Before engagement, and while speaking, the typing row is out of the way. */
736.riley-widget:not(.riley-engaged) .riley-input,
737.riley-widget.riley-voice .riley-input { display: none; }
738.riley-widget:not(.riley-engaged) .riley-mode { visibility: hidden; }
739
740/* ── floating launcher ────────────────────────────────────────────────
741   Fixed bottom-right, above ordinary page furniture but below anything that
742   deliberately takes over the screen. The panel is a sibling of the button so
743   the button keeps its place while the panel opens above it. */
744.riley-dock {
745  position: fixed; right: 20px; bottom: 20px; z-index: 2147483000;
746  display: flex; flex-direction: column; align-items: flex-end; gap: 10px;
747  font-family: var(--font-body, inherit);
748}
749.riley-launch {
750  display: inline-flex; align-items: center; gap: 10px; cursor: pointer;
751  border: 0; border-radius: 999px; padding: 8px 18px 8px 8px;
752  background: var(--mm-navbar-bg, var(--primary-color, #1F3B5B));
753  color: var(--mm-navbar-text, #fff);
754  box-shadow: 0 8px 26px rgba(0,0,0,.30);
755  font-size: 15px; font-weight: 600; line-height: 1;
756  max-width: min(78vw, 320px);
757}
758.riley-launch img { border-radius: 50%; object-fit: cover; flex: 0 0 auto; }
759.riley-launch:hover { filter: brightness(1.08); }
760.riley-launch-label { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
761.riley-dock.is-open .riley-launch { display: none; }
762
763.riley-widget.riley-floating {
764  width: min(92vw, 400px);
765  box-shadow: 0 18px 48px rgba(0,0,0,.34);
766}
767.riley-close {
768  background: none; border: 0; cursor: pointer; font-size: 24px; line-height: 1;
769  padding: 0 2px; margin-left: 6px;
770  color: rgba(255,255,255,.75);
771  color: color-mix(in srgb, var(--rw-text) 75%, transparent);
772}
773.riley-close:hover { color: var(--rw-text); }
774
775@media (max-width: 575.98px) {
776  /* On a phone the panel takes the width it needs; the launcher shrinks to
777     the avatar so it never straddles the screen. */
778  .riley-dock { right: 12px; bottom: 12px; left: 12px; align-items: flex-end; }
779  .riley-widget.riley-floating { width: 100%; }
780  .riley-launch-label { display: none; }
781  .riley-launch { padding: 8px; }
782}
783@media (max-width: 575.98px) {
784  .riley-widget { height: min(70vh, 480px); font-size: 14.5px; }
785  .riley-portrait { width: 104px; height: 104px; }
786  .riley-landing-title { font-size: 19px; margin-top: 10px; }
787  .riley-landing-note { margin-top: 10px; }
788}
789@media (prefers-reduced-motion: reduce) {
790  .riley-typing i, .riley-mode.is-live .riley-mode-ico { animation: none; }
791}
792</style>
793
794
795<script>
796(function () {
797  "use strict";
798  var root = document.getElementById('rileyWidget');
799  if (!root || root.dataset.booted) { return; }
800  root.dataset.booted = '1';
801
802  var logEl    = document.getElementById('rileyLog');
803  var formEl   = document.getElementById('rileyForm');
804  var textEl   = document.getElementById('rileyText');
805  var sendEl   = document.getElementById('rileySend');
806  var modeBtn  = document.getElementById('rileyModeBtn');
807  var modeLbl  = document.getElementById('rileyModeLbl');
808  var statusEl = document.getElementById('rileyStatus');
809  var talkCta  = document.getElementById('rileyLandingTalk');
810  var typeCta  = document.getElementById('rileyLandingType');
811  var hasLanding = !!document.getElementById('rileyLanding');
812  var dock      = document.getElementById('rileyDock');
813  var launchBtn = document.getElementById('rileyLaunch');
814  var closeBtn  = document.getElementById('rileyClose');
815  var floating  = !!dock;
816
817  // Carried across page loads so the thread survives navigation exactly,
818  // rather than relying on the server's two-hour resume window.
819  var STORE_CONV = 'rileyConvId', STORE_OPEN = 'rileyOpen';
820  function remember(k, v) { try { sessionStorage.setItem(k, v); } catch (e) {} }
821  function recall(k) { try { return sessionStorage.getItem(k); } catch (e) { return null; } }
822
823  var convId = recall('rileyConvId') || null;
824  var busy = false;
825  var voice = null;          // live voice session, or null
826  var connecting = false;    // mint + mic permission in flight
827  var voiceAttempt = 0;      // generation token; invalidates every older in-flight attempt
828  var started = false;       // session fetched
829  var engaged = !hasLanding; // with no landing screen the chat is live at once
830  var pending = null;        // session payload (held back only if there IS a landing)
831  var freshVisit = false;    // greeting painted this visit but not yet stored
832
833  // ---- rendering ----------------------------------------------------------
834
835  function bubble(who, text) {
836    var el = document.createElement('div');
837    el.className = 'riley-msg riley-' + who;
838    el.textContent = text;
839    logEl.appendChild(el);
840    logEl.scrollTop = logEl.scrollHeight;
841    return el;
842  }
843
844  function note(text) { return bubble('note', text); }
845
846  function typing() {
847    var el = document.createElement('div');
848    el.className = 'riley-msg riley-them';
849    el.innerHTML = '<span class="riley-typing"><i></i><i></i><i></i></span>';
850    logEl.appendChild(el);
851    logEl.scrollTop = logEl.scrollHeight;
852    return el;
853  }
854
855  function vEvent(what) {
856    // Voice outcomes, so a session that yields no transcript can be told
857    // apart from one the visitor never really started.
858    if (!convId) { return; }
859    post('/riley/api/log', { conv_id: convId, role: 'event', content: what });
860  }
861
862  function post(path, body) {
863    return fetch(path, {
864      method: 'POST',
865      headers: { 'Content-Type': 'application/json' },
866      credentials: 'same-origin',
867      body: JSON.stringify(body || {})
868    }).then(function (r) { return r.json().catch(function () { return {}; }); });
869  }
870
871  // ---- opening ------------------------------------------------------------
872  //
873  // By default Riley says hello as soon as the panel is on screen and the text
874  // box is ready to type in. The earlier design held everything behind a
875  // "Talk with Riley" button and nobody pressed it.
876  //
877  // A return visit still opens on a clean greeting rather than a wall of last
878  // week's transcript — she carries the history in her own context, so she
879  // answers as though she remembers, which she does.
880
881  // `carryOver` is true when the panel was ALREADY open and the visitor simply
882  // moved to another page. Wiping the transcript there would be wrong — they
883  // are mid-conversation and following it around the site is the whole point
884  // of the launcher. A genuinely new visit still opens on a clean greeting.
885  var carryOver = false;
886
887  function paintOpening(d) {
888    var history = d.history || [];
889    if (carryOver && history.length) {
890      history.forEach(function (m) {
891        bubble(m.role === 'user' ? 'me' : 'them', m.content);
892      });
893      freshVisit = false;
894      return;
895    }
896    if (d.opening) { bubble('them', d.opening); }
897    if (history.length) {
898      note('We have spoken before — I still have our conversation.');
899    }
900    freshVisit = true;
901  }
902
903  function boot() {
904    if (started) { return; }
905    started = true;
906    post('/riley/api/session', { page_url: location.href, conv_id: convId })
907      .then(function (d) {
908        if (d.error) {
909          pending = null;
910          if (!hasLanding) { note('Riley is offline just now.'); }
911          return;
912        }
913        convId = d.conv_id;
914        remember(STORE_CONV, convId);
915        pending = d;
916        if (!hasLanding) { paintOpening(d); }
917      })
918      .catch(function () {
919        pending = null;
920        if (!hasLanding) { note('Riley is offline just now.'); }
921      });
922  }
923
924  function engage(mode) {
925    if (engaged) { return; }
926    engaged = true;
927    root.classList.add('riley-engaged');
928
929    var d = pending || {};
930    var history = d.history || [];
931    if (history.length) {
932      note('Carrying on from last time.');
933      history.forEach(function (m) {
934        bubble(m.role === 'user' ? 'me' : 'them', m.content);
935      });
936    } else if (d.opening) {
937      bubble('them', d.opening);
938    } else if (!pending) {
939      note('Riley is offline just now.');
940      return;
941    }
942
943    if (mode === 'voice') { startVoice(); }
944    else { textEl.focus(); }
945  }
946
947  if (talkCta) { talkCta.addEventListener('click', function () { engage('voice'); }); }
948  if (typeCta) { typeCta.addEventListener('click', function () { engage('text'); }); }
949
950  // ---- text lane ----------------------------------------------------------
951
952  formEl.addEventListener('submit', function (e) {
953    e.preventDefault();
954    var msg = (textEl.value || '').trim();
955    if (!msg || busy) { return; }
956    textEl.value = '';
957    bubble('me', msg);
958    busy = true; sendEl.disabled = true;
959    var dots = typing();
960    post('/riley/api/text', { message: msg, conv_id: convId,
961                              page_url: location.href, fresh_visit: freshVisit })
962      .then(function (d) {
963        dots.remove();
964        if (d.conv_id) { convId = d.conv_id; remember(STORE_CONV, convId); }
965        bubble('them', d.reply || "Sorry — I didn't catch that. Try again?");
966      })
967      .catch(function () {
968        dots.remove();
969        bubble('them', 'Sorry, something went wrong on my end. Try that once more?');
970      })
971      .finally(function () {
972        busy = false; sendEl.disabled = false; freshVisit = false; textEl.focus();
973      });
974  });
975
976  // ---- voice lane: GPT-Live 1 / Gleam --------------------------------------
977  // WebRTC owns continuous duplex audio. Server sideband owns tools, persisted
978  // transcripts and billing; this page forwards only observed function IDs.
979  function setVoiceUI(on, label) {
980    root.classList.toggle('riley-voice', on);
981    modeBtn.classList.toggle('is-live', on);
982    modeLbl.textContent = on ? 'Stop' : 'Talk';
983    modeBtn.title = on ? 'Switch back to typing' : 'Switch to voice';
984    modeBtn.setAttribute('aria-label', on ? 'Stop voice chat' : 'Talk with Riley');
985    statusEl.innerHTML = label || (on ? 'Listening&hellip;' : 'Support &middot; AI agent');
986  }
987
988  function closeVoiceParts(parts, immediately) {
989    if (!parts || parts.disposed) return;
990    parts.closing = true;
991    try { parts.mic && parts.mic.getTracks().forEach(function(t){t.stop();}); } catch(e) {}
992    try { parts.audio && parts.audio.pause(); if(parts.audio) parts.audio.srcObject=null; } catch(e) {}
993    if (parts.timer) clearTimeout(parts.timer);
994    if (parts.meter) clearInterval(parts.meter);
995    try { parts.audioContext && parts.audioContext.close(); } catch(e) {}
996    function dispose() {
997      if(parts.disposed) return;
998      parts.disposed=true;
999      clearTimeout(parts.closeTimer);clearTimeout(parts.connectTimer);
1000      try { parts.dc && parts.dc.close(); } catch(e) {}
1001      try { parts.pc && parts.pc.close(); } catch(e) {}
1002    }
1003    parts.dispose=dispose;
1004    if (parts.liveId) {
1005      post('/riley/api/live-close',{conv_id:convId,live_id:parts.liveId}).catch(function(){});
1006    }
1007    if (!immediately && parts.dc && parts.dc.readyState==='open') {
1008      try { parts.dc.send(JSON.stringify({type:'session.close'})); } catch(e) {}
1009      parts.closeTimer=setTimeout(dispose,15000);
1010    } else dispose();
1011  }
1012
1013  function stopVoice(silent) {
1014    ++voiceAttempt; connecting=false;
1015    var old=voice;voice=null;
1016    closeVoiceParts(old);
1017    setVoiceUI(false);
1018    if(old && !silent) note('Back to typing — Riley still has the conversation.');
1019    textEl.focus();
1020  }
1021
1022  function caption(parts, role, msg) {
1023    if(!msg.delta) return;
1024    var entry=parts.captions[role];
1025    if(entry && msg.start_ms!=null && entry.end!=null && msg.start_ms-entry.end>900) entry=null;
1026    if(!entry) {
1027      entry={element:bubble(role==='user'?'me':'them',''),text:'',end:null};
1028      parts.captions[role]=entry;
1029    }
1030    entry.text+=msg.delta;entry.end=msg.end_ms;
1031    entry.element.textContent=entry.text;
1032    logEl.scrollTop=logEl.scrollHeight;
1033  }
1034
1035  function handleEvent(ev,parts) {
1036    var msg;try {msg=JSON.parse(ev.data);}catch(e){return;}
1037    if(msg.type==='session.closed') {
1038      if(parts.dispose) parts.dispose();
1039      else if(voice===parts) stopVoice(true);
1040      return;
1041    }
1042    if(parts.closing || voice!==parts || parts.attempt!==voiceAttempt) return;
1043    if(msg.type==='session.started') {
1044      clearTimeout(parts.connectTimer);parts.ready=true;connecting=false;
1045      setVoiceUI(true,'Listening&hellip;');vEvent('voice_connected:gpt-live-1:gleam');
1046    } else if(msg.type==='session.input_transcript.delta') {
1047      caption(parts,'user',msg);
1048    } else if(msg.type==='session.output_transcript.delta') {
1049      caption(parts,'assistant',msg);
1050    } else if(msg.type==='response.event') {
1051      var event=msg.event||{}, item=event.item||{};
1052      if(event.type==='response.output_item.done' && item.type==='function_call' && !parts.calls[item.call_id]) {
1053        parts.calls[item.call_id]=true;
1054        function forward(retry) {
1055          return post('/riley/api/live-tool',{conv_id:convId,live_id:parts.liveId,call_id:item.call_id})
1056            .then(function(d){
1057              if(!d || !d.ok) {
1058                if(retry && !parts.closing) return new Promise(function(resolve){setTimeout(resolve,500);}).then(function(){return forward(false);});
1059                throw new Error('tool not accepted');
1060              }
1061            });
1062        }
1063        forward(true).catch(function(){
1064          if(voice===parts && !parts.closing) {note('Voice lost its support connection. Please try again or continue typing.');stopVoice(true);}
1065        });
1066      }
1067    } else if(msg.type==='error') {
1068      vEvent('voice_failed:live_command');note('Voice encountered a connection problem. Typing still works.');stopVoice(true);
1069    }
1070  }
1071
1072  function monitorPlayback(parts,stream) {
1073    try {
1074      var Ctx=window.AudioContext||window.webkitAudioContext;
1075      var ctx=parts.audioContext=new Ctx();ctx.resume();
1076      var source=ctx.createMediaStreamSource(stream),analyser=ctx.createAnalyser();
1077      analyser.fftSize=256;source.connect(analyser);var samples=new Float32Array(256);
1078      parts.meter=setInterval(function(){
1079        if(voice!==parts || !parts.ready || parts.closing) return;
1080        analyser.getFloatTimeDomainData(samples);var energy=0;
1081        for(var i=0;i<samples.length;i++) energy+=samples[i]*samples[i];
1082        setVoiceUI(true,Math.sqrt(energy/samples.length)>.007 ? 'Riley is speaking&hellip;' : 'Listening&hellip;');
1083      },150);
1084    } catch(e) {} // Voice itself does not depend on the level indicator.
1085  }
1086
1087  function gatherIce(pc) {
1088    if(pc.iceGatheringState==='complete') return Promise.resolve();
1089    return new Promise(function(resolve,reject){
1090      var timer=setTimeout(function(){pc.removeEventListener('icegatheringstatechange',check);reject(new Error('ICE timeout'));},10000);
1091      function check(){if(pc.iceGatheringState==='complete'){clearTimeout(timer);pc.removeEventListener('icegatheringstatechange',check);resolve();}}
1092      pc.addEventListener('icegatheringstatechange',check);check();
1093    });
1094  }
1095
1096  function startVoice() {
1097    if(voice || connecting) {stopVoice();return;}
1098    if(!navigator.mediaDevices || !window.RTCPeerConnection) {note("This browser can't do voice — typing works fine though.");return;}
1099    connecting=true;var attempt=++voiceAttempt,parts=null;
1100    setVoiceUI(true,'Connecting&hellip;');
1101    navigator.mediaDevices.getUserMedia({audio:{channelCount:1,echoCancellation:true,noiseSuppression:true}})
1102    .then(function(mic){
1103      if(attempt!==voiceAttempt){mic.getTracks().forEach(function(t){t.stop();});throw new Error('aborted');}
1104      var pc=new RTCPeerConnection(),audio=new Audio();audio.autoplay=true;audio.setAttribute('playsinline','');
1105      parts={pc:pc,audio:audio,mic:mic,dc:null,attempt:attempt,captions:{},calls:{},ready:false};voice=parts;
1106      pc.ontrack=function(e){
1107        if(parts.closing)return;
1108        var stream=e.streams[0]||new MediaStream([e.track]);audio.srcObject=stream;
1109        audio.play().catch(function(){if(voice===parts)note('Your browser blocked audio playback. Stop voice and try Talk again.');});
1110        monitorPlayback(parts,stream);
1111      };
1112      pc.onconnectionstatechange=function(){
1113        if(!parts.closing && voice===parts && pc.connectionState==='failed') {note('Voice disconnected. You can continue typing.');stopVoice(true);}
1114      };
1115      mic.getTracks().forEach(function(t){pc.addTrack(t,mic);});
1116      var dc=parts.dc=pc.createDataChannel('oai-events');
1117      dc.onmessage=function(e){handleEvent(e,parts);};
1118      dc.onclose=function(){if(!parts.closing && voice===parts){note('Voice conversation ended.');stopVoice(true);}};
1119      parts.connectTimer=setTimeout(function(){if(voice===parts && !parts.ready){note('Voice could not connect. Please try again.');stopVoice(true);}},45000);
1120      return pc.createOffer().then(function(offer){return pc.setLocalDescription(offer);})
1121        .then(function(){return gatherIce(pc);})
1122        .then(function(){
1123          if(attempt!==voiceAttempt || parts.closing)throw new Error('aborted');
1124          return post('/riley/api/live-session',{conv_id:convId,page_url:location.href,sdp:pc.localDescription.sdp});
1125        }).then(function(d){
1126          if(!d || !d.sdp || !d.live_id)throw new Error((d&&d.detail)||'no voice session');
1127          parts.liveId=d.live_id;
1128          if(attempt!==voiceAttempt || parts.closing) {
1129            post('/riley/api/live-close',{conv_id:d.conv_id||convId,live_id:d.live_id}).catch(function(){});
1130            throw new Error('aborted');
1131          }
1132          convId=d.conv_id||convId;remember(STORE_CONV,convId);
1133          parts.timer=setTimeout(function(){if(voice===parts){note('That is the limit for one voice session — carry on by typing.');stopVoice(true);}},((d.max_seconds||600)-10)*1000);
1134          return pc.setRemoteDescription({type:'answer',sdp:d.sdp});
1135        });
1136    }).catch(function(err){
1137      closeVoiceParts(parts,true);
1138      if(voice===parts)voice=null;
1139      if(attempt!==voiceAttempt || (err&&err.message==='aborted'))return;
1140      connecting=false;setVoiceUI(false);
1141      if(err&&err.name==='NotAllowedError'){vEvent('voice_failed:mic_denied');note('No microphone access — happy to keep going by text.');}
1142      else {vEvent('voice_failed:live_start');note("Couldn't start voice just now. Typing still works.");}
1143      textEl.focus();
1144    });
1145  }
1146
1147  modeBtn.addEventListener('click',startVoice);
1148  window.addEventListener('beforeunload',function(){
1149    if(voice && voice.liveId && navigator.sendBeacon)navigator.sendBeacon('/riley/api/live-close',new Blob([JSON.stringify({conv_id:convId,live_id:voice.liveId})],{type:'application/json'}));
1150    stopVoice(true);
1151  });
1152
1153  // ---- floating launcher --------------------------------------------------
1154  //
1155  // The panel is on every page now, so it must NOT open a conversation just
1156  // because a page loaded — that would create a row per pageview across the
1157  // whole site and drown the very numbers we are trying to read. A
1158  // conversation begins when somebody actually opens the chat.
1159
1160  function openPanel() {
1161    if (!floating) { return; }
1162    dock.classList.add('is-open');
1163    root.hidden = false;
1164    launchBtn.setAttribute('aria-expanded', 'true');
1165    remember(STORE_OPEN, '1');
1166    boot();
1167    if (engaged) { setTimeout(function () { textEl.focus(); }, 60); }
1168  }
1169
1170  function closePanel() {
1171    if (!floating) { return; }
1172    stopVoice(true);
1173    dock.classList.remove('is-open');
1174    root.hidden = true;
1175    launchBtn.setAttribute('aria-expanded', 'false');
1176    remember(STORE_OPEN, '0');
1177    launchBtn.focus();
1178  }
1179
1180  if (floating) {
1181    launchBtn.addEventListener('click', openPanel);
1182    if (closeBtn) { closeBtn.addEventListener('click', closePanel); }
1183    document.addEventListener('keydown', function (e) {
1184      if (e.key === 'Escape' && !root.hidden) { closePanel(); }
1185    });
1186    // Stay open as they move around the site, transcript and all.
1187    if (recall(STORE_OPEN) === '1') { carryOver = true; openPanel(); }
1188  }
1189
1190  // Boot on first sight — inline mode only. Floating mode boots on open.
1191  if (floating) {
1192    /* handled by openPanel */
1193  } else if (root.offsetParent !== null) {
1194    boot();
1195  } else {
1196    var io = new IntersectionObserver(function (entries) {
1197      entries.forEach(function (en) {
1198        if (en.isIntersecting) { boot(); io.disconnect(); }
1199      });
1200    });
1201    io.observe(root);
1202    document.addEventListener('click', function once() {
1203      if (root.offsetParent !== null) { boot(); document.removeEventListener('click', once); }
1204    });
1205  }
1206})();
1207</script>
1207
1208
1209</body>
1210</html>

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.