PageSourceSearch

https://soulor-ai.vercel.app/ad

html soulor-ai.vercel.app collected 2026-10-03 07:46:14 UTC 41,822 bytes, 1,102 lines download raw bytes

1<!DOCTYPE html>
2<html lang="en">
3<head>
4<meta charset="UTF-8">
5<meta name="viewport" content="width=900, initial-scale=1.0">
6<title>Soulor AI — Ad</title>
7<link rel="icon" type="image/png" href="/icon-192.png">
8<link rel="apple-touch-icon" href="/icon-512.png">
9<link rel="preconnect" href="https://fonts.googleapis.com">
10<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
11<style>
12*{box-sizing:border-box;margin:0;padding:0}
13:root{
14  --bg:#0a0a14;
15  --surface:#12121e;
16  --surface2:#1a1a2e;
17  --border:rgba(255,255,255,0.08);
18  --accent:#7c3aed;
19  --accent2:#a855f7;
20  --text:#ffffff;
21  --text2:rgba(255,255,255,0.55);
22  --bubble-ai:rgba(99,102,241,0.18);
23  --bubble-user:rgba(124,58,237,0.5);
24}
25
26html,body{
27  width:100%; height:100%;
28  overflow:hidden;
29  background:#000;
30  font-family:'Inter',sans-serif;
31}
32body{
33  display:flex;
34  align-items:center;
35  justify-content:center;
36  padding:16px;
37  background:
38    radial-gradient(circle at 20% 20%, rgba(124,58,237,0.28), transparent 28%),
39    radial-gradient(circle at 80% 18%, rgba(59,130,246,0.2), transparent 24%),
40    radial-gradient(circle at 50% 100%, rgba(236,72,153,0.16), transparent 34%),
41    #030308;
42}
43
44/* ── Outer container ── */
45.stage{
46  /* flex-shrink:0 so a viewport narrower than 900px scales the stage down
47     uniformly (scaleStage) instead of letting the flex row squeeze its width
48     while its absolutely-positioned children keep their 900px coordinates —
49     which crops the phone's right edge. Matters standalone at small windows,
50     and in the landing hero, which frames this page at 820px. */
51  width:900px; height:620px; flex-shrink:0;
52  background:var(--bg);
53  position:relative; overflow:hidden;
54  background:
55    radial-gradient(ellipse 120% 50% at 50% 0%, rgba(109,40,217,0.35) 0%, transparent 60%),
56    radial-gradient(ellipse 80% 40% at 80% 100%, rgba(236,72,153,0.2) 0%, transparent 55%),
57    #0a0a14;
58  transform-origin:center center;
59  border-radius:32px;
60  box-shadow:
61    0 0 0 1px rgba(255,255,255,0.04),
62    0 40px 120px rgba(0,0,0,0.88),
63    0 0 100px rgba(124,58,237,0.14);
64}
65.stage::before{
66  content:'';
67  position:absolute; inset:-20%;
68  background:
69    radial-gradient(circle at 25% 25%, rgba(124,58,237,0.18), transparent 22%),
70    radial-gradient(circle at 75% 30%, rgba(96,165,250,0.16), transparent 24%),
71    radial-gradient(circle at 55% 75%, rgba(236,72,153,0.12), transparent 26%);
72  filter:blur(30px);
73  animation:auroraShift 12s ease-in-out infinite alternate;
74  pointer-events:none;
75}
76@keyframes auroraShift{
77  from{transform:translate3d(-8px,-10px,0) scale(1)}
78  to{transform:translate3d(10px,12px,0) scale(1.06)}
79}
80.orb{
81  position:absolute; border-radius:50%; filter:blur(8px);
82  opacity:0.55; pointer-events:none; z-index:1;
83  animation:orbFloat 10s ease-in-out infinite alternate;
84}
85.orb.one{
86  width:110px; height:110px; top:50px; left:30px;
87  background:radial-gradient(circle at 35% 35%, rgba(167,139,250,0.9), rgba(124,58,237,0.05) 72%);
88}
89.orb.two{
90  width:140px; height:140px; right:30px; bottom:50px;
91  background:radial-gradient(circle at 35% 35%, rgba(244,114,182,0.7), rgba(236,72,153,0.04) 72%);
92  animation-duration:13s;
93}
94@keyframes orbFloat{
95  from{transform:translateY(-8px) translateX(-6px)}
96  to{transform:translateY(10px) translateX(8px)}
97}
98.phone-halo{
99  position:absolute;
100  width:300px; height:300px;
101  left:300px; top:160px;
102  border-radius:50%;
103  background:radial-gradient(circle, rgba(124,58,237,0.26), rgba(124,58,237,0.04) 58%, transparent 72%);
104  filter:blur(16px);
105  z-index:1;
106  pointer-events:none;
107}
108
109/* ── Caption overlays ── */
110.caption{
111  position:absolute; z-index:50;
112  left:0; right:0;
113  text-align:center;
114  padding:0 28px;
115  opacity:0; transition:opacity 0.4s;
116  pointer-events:none;
117}
118.caption.top{ top:16px; }
119.caption.bottom{ bottom:8px; }
120.caption.mid{ top:50%; transform:translateY(-50%); }
121.caption.hero-center{
122  top:50%;
123  transform:translateY(-54%);
124  padding:0 34px;
125}
126.caption h1{
127  font-size:42px; font-weight:900; line-height:1.1;
128  letter-spacing:-0.03em; color:#fff;
129  text-shadow:0 2px 20px rgba(0,0,0,0.8);
130}
131.caption h2{
132  font-size:34px; font-weight:800; line-height:1.2;
133  letter-spacing:-0.02em; color:#fff;
134  text-shadow:0 2px 20px rgba(0,0,0,0.8);
135}
136.caption p{
137  font-size:18px; font-weight:600; color:rgba(255,255,255,0.8);
138  margin-top:8px; text-shadow:0 1px 10px rgba(0,0,0,0.8);
139}
140.caption .tag{
141  display:inline-block; background:rgba(124,58,237,0.8);
142  border-radius:999px; padding:5px 16px;
143  font-size:13px; font-weight:700; letter-spacing:0.04em;
144  text-transform:uppercase; color:#fff; margin-bottom:12px;
145}
146.caption.hero-center .tag{
147  margin-bottom:18px;
148}
149.caption.hero-center h1{
150  font-size:56px;
151  line-height:0.98;
152}
153.caption.show{ opacity:1; }
154.caption-card{
155  max-width:620px;
156  margin:0 auto;
157  padding:32px 44px;
158  border-radius:24px;
159  background:linear-gradient(180deg, rgba(11,11,22,0.78), rgba(18,18,34,0.62));
160  border:1px solid rgba(255,255,255,0.08);
161  box-shadow:
162    0 24px 60px rgba(0,0,0,0.45),
163    inset 0 1px 0 rgba(255,255,255,0.06);
164  backdrop-filter:blur(12px);
165}
166.caption-card .tag{ margin-bottom:10px; }
167.caption-card p{ margin-top:10px; }
168
169/* ── Phone frame ── */
170.phone{
171  position:absolute;
172  width:740px; height:540px;
173  left:80px; top:40px;
174  border-radius:28px;
175  background:var(--surface);
176  border:2px solid rgba(255,255,255,0.12);
177  box-shadow:
178    0 0 0 1px rgba(255,255,255,0.05),
179    0 40px 80px rgba(0,0,0,0.8),
180    inset 0 1px 0 rgba(255,255,255,0.1);
181  overflow:hidden;
182  opacity:0; transform:translateY(40px) scale(0.96);
183  transition:opacity 0.5s, transform 0.5s cubic-bezier(0.22,1,0.36,1);
184  z-index:2;
185}
186.phone.show{ opacity:1; transform:translateY(0) scale(1); }
187
188/* ── Views inside phone ── */
189.view{
190  position:absolute; inset:0;
191  display:flex; flex-direction:column;
192  opacity:0; transition:opacity 0.3s;
193}
194.view.active{ opacity:1; }
195
196/* ── Topbar ── */
197.topbar{
198  display:flex; align-items:center; gap:10px;
199  padding:18px 20px 12px;
200  border-bottom:1px solid var(--border);
201  background:var(--surface);
202  flex-shrink:0;
203}
204.tb-back{
205  width:28px; height:28px; display:flex; align-items:center; justify-content:center;
206  color:rgba(255,255,255,0.5); font-size:18px;
207}
208.tb-avatar{
209  width:38px; height:38px; border-radius:50%;
210  background:linear-gradient(135deg,#8b5cf6,#4f46e5);
211  display:flex; align-items:center; justify-content:center;
212  font-size:17px; flex-shrink:0;
213  box-shadow:0 0 0 2px rgba(99,102,241,0.4);
214  position:relative;
215  overflow:hidden;
216}
217.tb-avatar::after{
218  content:'';
219  position:absolute; inset:1px;
220  border-radius:50%;
221  background:radial-gradient(circle at 35% 30%, rgba(255,255,255,0.28), transparent 38%);
222}
223.avatar-photo{
224  width:100%;
225  height:100%;
226  object-fit:cover;
227  position:relative;
228  z-index:1;
229}
230.tb-info{ flex:1; }
231.tb-name{ font-size:15px; font-weight:700; color:#fff; }
232.tb-status{ font-size:11px; color:rgba(255,255,255,0.45); margin-top:1px; }
233.tb-actions{ display:flex; gap:6px; }
234.tb-btn{
235  width:32px; height:32px; border-radius:50%;
236  background:rgba(255,255,255,0.06); border:none;
237  display:flex; align-items:center; justify-content:center;
238  color:rgba(255,255,255,0.6); cursor:pointer;
239}
240
241/* ── Chat area ── */
242.chat-area{
243  flex:1; overflow:hidden;
244  padding:16px 14px; display:flex; flex-direction:column; gap:10px;
245}
246.msg-row{ display:flex; opacity:0; transform:translateY(22px) scale(0.4); transform-origin:bottom center; transition:opacity 0.5s, transform 0.5s cubic-bezier(.34,1.56,.64,1); }
247.msg-row.ai{ justify-content:flex-start; transform-origin:bottom left; }
248.msg-row.user{ justify-content:flex-end; transform-origin:bottom right; }
249.msg-row.show{ opacity:1; transform:translateY(0) scale(1); }
250
251.bubble{
252  max-width:min(75%, 420px); border-radius:18px;
253  padding:10px 14px; font-size:14px; line-height:1.5;
254}
255.bubble.ai{
256  background:var(--bubble-ai);
257  border:1px solid rgba(99,102,241,0.2);
258  color:rgba(255,255,255,0.92);
259  border-bottom-left-radius:5px;
260}
261.bubble.user{
262  background:var(--bubble-user);
263  color:#fff; font-weight:500;
264  border-bottom-right-radius:5px;
265}
266
267.typing{ display:flex; gap:4px; align-items:center; padding:12px 16px; opacity:0; transition:opacity 0.3s; }
268.typing.show{ opacity:1; }
269.typing span{
270  width:7px; height:7px; border-radius:50%;
271  background:rgba(99,102,241,0.7);
272  animation:typingDot 1.2s ease-in-out infinite;
273}
274.typing span:nth-child(2){ animation-delay:0.2s; }
275.typing span:nth-child(3){ animation-delay:0.4s; }
276@keyframes typingDot{ 0%,60%,100%{transform:translateY(0);opacity:0.4} 30%{transform:translateY(-5px);opacity:1} }
277
278.voice-card{
279  display:flex;
280  align-items:center;
281  gap:10px;
282  min-width:190px;
283  background:#17172a;
284  border:1px solid rgba(255,255,255,0.08);
285  border-radius:18px;
286  border-bottom-left-radius:5px;
287  padding:10px 14px;
288  color:rgba(255,255,255,0.8);
289  font-size:11px;
290  box-shadow:0 10px 26px rgba(0,0,0,0.18);
291}
292.voice-play{
293  width:34px; height:34px; border-radius:50%;
294  background:var(--accent);
295  display:flex; align-items:center; justify-content:center;
296  color:#fff; font-size:12px; flex-shrink:0;
297}
298.voice-wave{
299  display:flex; align-items:center; gap:3px; flex:1; height:22px;
300}
301.voice-bar{
302  width:3px; border-radius:999px;
303  background:linear-gradient(180deg,#c4b5fd,#8b9cfb);
304  animation:voiceWave 1.1s ease-in-out infinite;
305}
306.voice-bar:nth-child(1){ height:8px; animation-delay:0s; }
307.voice-bar:nth-child(2){ height:14px; animation-delay:0.1s; }
308.voice-bar:nth-child(3){ height:10px; animation-delay:0.2s; }
309.voice-bar:nth-child(4){ height:16px; animation-delay:0.3s; }
310.voice-bar:nth-child(5){ height:9px; animation-delay:0.15s; }
311.voice-bar:nth-child(6){ height:13px; animation-delay:0.25s; }
312.voice-time{ font-size:11.5px; color:rgba(255,255,255,0.45); font-variant-numeric:tabular-nums; white-space:nowrap; flex-shrink:0; }
313@keyframes voiceWave{
314  0%,100%{transform:scaleY(0.72);opacity:0.7}
315  50%{transform:scaleY(1.08);opacity:1}
316}
317
318.stack{
319  max-width:min(75%, 420px);
320  display:flex;
321  flex-direction:column;
322  gap:6px;
323}
324.stack .bubble.ai{
325  width:fit-content;
326}
327.meme-card{
328  position:relative;
329  overflow:hidden;
330  background:#120f20;
331  border:1px solid rgba(255,255,255,0.08);
332  border-radius:14px;
333  width:148px;
334  min-height:126px;
335  display:flex;
336  flex-direction:column;
337  align-items:stretch;
338  justify-content:flex-end;
339  gap:0;
340  color:#fff;
341  box-shadow:
342    inset 0 1px 0 rgba(255,255,255,0.06),
343    0 14px 28px rgba(0,0,0,0.24);
344}
345.meme-thumb{
346  width:100%;
347  height:100%;
348  object-fit:cover;
349  position:absolute;
350  inset:0;
351}
352.meme-overlay{
353  position:relative;
354  z-index:1;
355  margin-top:auto;
356  padding:10px 10px 8px;
357  background:linear-gradient(180deg, rgba(5,5,10,0) 0%, rgba(5,5,10,0.74) 45%, rgba(5,5,10,0.92) 100%);
358}
359.meme-label{
360  font-size:10px;
361  font-weight:700;
362  letter-spacing:0.08em;
363  color:rgba(255,255,255,0.66);
364  text-transform:uppercase;
365}
366.meme-cap{
367  font-size:12px;
368  font-weight:700;
369  color:rgba(255,255,255,0.94);
370  text-align:left;
371  margin-top:4px;
372  line-height:1.25;
373}
374.meme-credit{
375  margin-top:6px;
376  font-size:9px;
377  font-weight:700;
378  letter-spacing:0.06em;
379  color:rgba(255,255,255,0.52);
380  text-transform:uppercase;
381}
382
383/* ── Mode bar ── */
384.mode-bar{
385  display:flex; gap:6px; padding:10px 14px;
386  border-top:1px solid var(--border);
387  background:var(--surface);
388  flex-shrink:0;
389}
390.mode-tag{
391  display:flex; align-items:center; gap:5px;
392  padding:6px 12px; border-radius:999px;
393  font-size:12px; font-weight:600;
394  background:rgba(99,102,241,0.15); border:1px solid rgba(99,102,241,0.3);
395  color:#a5b4fc;
396}
397.mode-tag.sim{ background:rgba(236,72,153,0.15); border-color:rgba(236,72,153,0.3); color:#f9a8d4; }
398
399/* ── Switch button ── */
400.switch-btn{
401  position:absolute; bottom:82px; right:16px;
402  background:linear-gradient(135deg,var(--accent),var(--accent2));
403  border:none; border-radius:22px; padding:10px 18px;
404  font-size:13px; font-weight:700; color:#fff;
405  display:flex; align-items:center; gap:7px;
406  box-shadow:0 4px 20px rgba(124,58,237,0.5);
407  opacity:0; transform:scale(0.25);
408  transition:opacity 0.45s, transform 0.45s cubic-bezier(.34,1.56,.64,1);
409  cursor:pointer; z-index:10;
410}
411.switch-btn.show{ opacity:1; transform:scale(1); }
412.switch-btn.tap{ transform:scale(0.88); }
413
414/* ── Exaggerated pulse pop, for send-btn and brand-cta on demand ── */
415@keyframes btnPulse{ 0%{transform:scale(1)} 40%{transform:scale(1.45)} 100%{transform:scale(1)} }
416.pulse{ animation:btnPulse 0.5s cubic-bezier(.34,1.56,.64,1); }
417
418/* ── Phone-wide punch/zoom for emphasis at key beats ── */
419@keyframes phonePunch{ 0%{transform:scale(1)} 45%{transform:scale(1.045)} 100%{transform:scale(1)} }
420.phone.punch{ animation:phonePunch 0.55s cubic-bezier(.34,1.56,.64,1); }
421
422/* ── Ripple ── */
423.ripple{
424  position:absolute; border-radius:50%;
425  background:rgba(124,58,237,0.4);
426  width:60px; height:60px;
427  margin-left:-30px; margin-top:-30px;
428  animation:rippleAnim 0.6s ease-out forwards;
429  pointer-events:none; z-index:20;
430}
431@keyframes rippleAnim{ from{opacity:1;transform:scale(0)} to{opacity:0;transform:scale(2.5)} }
432
433/* ── Simulation view ── */
434.sim-header{
435  background:linear-gradient(135deg,rgba(236,72,153,0.15),rgba(124,58,237,0.1));
436  padding:18px 20px 14px;
437  border-bottom:1px solid var(--border);
438  flex-shrink:0;
439}
440.sim-title{ font-size:16px; font-weight:800; color:#fff; }
441.sim-sub{ font-size:12px; color:rgba(255,255,255,0.45); margin-top:3px; }
442
443.sim-topic{
444  margin:10px 14px 4px;
445  background:rgba(255,255,255,0.04);
446  border-radius:12px; padding:10px 14px;
447  font-size:12px; color:rgba(255,255,255,0.5); flex-shrink:0;
448  border-left:3px solid rgba(124,58,237,0.6);
449  font-style:italic;
450}
451
452.sim-list{ flex:1; overflow:hidden; padding:8px 10px; display:grid; grid-template-columns:1fr 1fr; grid-auto-rows:min-content; gap:8px 10px; }
453
454.analyst{
455  display:flex; align-items:flex-start; gap:10px;
456  padding:10px 12px; border-radius:14px;
457  background:rgba(255,255,255,0.04); border:1px solid rgba(255,255,255,0.06);
458  opacity:0; transform:translateX(16px);
459  transition:opacity 0.4s, transform 0.4s cubic-bezier(0.22,1,0.36,1);
460}
461.analyst.show{ opacity:1; transform:translateX(0); }
462.analyst-dot{ width:9px; height:9px; border-radius:50%; flex-shrink:0; margin-top:5px; }
463.analyst-name{ font-size:12px; font-weight:700; color:rgba(255,255,255,0.9); margin-bottom:3px; }
464.analyst-text{ font-size:12px; color:rgba(255,255,255,0.55); line-height:1.45; }
465
466.sim-back-btn{
467  margin:6px 10px 10px;
468  background:rgba(99,102,241,0.15); border:1px solid rgba(99,102,241,0.3);
469  border-radius:12px; padding:10px 14px;
470  font-size:13px; font-weight:700; color:#a5b4fc;
471  display:flex; align-items:center; justify-content:center; gap:7px;
472  opacity:0; transform:translateY(8px);
473  transition:opacity 0.4s, transform 0.4s;
474  flex-shrink:0;
475}
476.sim-back-btn.show{ opacity:1; transform:translateY(0); }
477
478/* ── Input bar ── */
479.input-bar{
480  display:flex; align-items:center; gap:8px;
481  padding:10px 14px 14px;
482  border-top:1px solid var(--border);
483  background:var(--surface);
484  flex-shrink:0;
485}
486.input-field{
487  flex:1; background:rgba(255,255,255,0.06);
488  border:1px solid rgba(255,255,255,0.1); border-radius:22px;
489  padding:9px 16px; font-size:13px; color:rgba(255,255,255,0.4);
490}
491.send-btn{
492  width:36px; height:36px; border-radius:50%;
493  background:var(--accent); border:none;
494  display:flex; align-items:center; justify-content:center;
495  color:#fff;
496}
497
498/* ── Branding screen ── */
499.brand-screen{
500  position:absolute; inset:0; z-index:40;
501  display:flex; flex-direction:column; align-items:center; justify-content:center;
502  background:
503    radial-gradient(ellipse 100% 60% at 50% 30%, rgba(124,58,237,0.5) 0%, transparent 65%),
504    #0a0a14;
505  opacity:0; transition:opacity 0.6s;
506}
507.brand-screen.show{ opacity:1; }
508.brand-logo{
509  width:80px; height:80px; border-radius:22px;
510  background:linear-gradient(135deg,#6366f1,#7c3aed);
511  display:flex; align-items:center; justify-content:center;
512  margin-bottom:20px;
513  box-shadow:0 0 40px rgba(124,58,237,0.6);
514  overflow:hidden;
515}
516.brand-logo img{
517  width:100%;
518  height:100%;
519  object-fit:cover;
520}
521.brand-name{ font-size:36px; font-weight:900; letter-spacing:-0.03em; color:#fff; margin-bottom:8px; }
522.brand-sub{ font-size:15px; color:rgba(255,255,255,0.5); margin-bottom:32px; text-align:center; padding:0 40px; }
523.brand-url{
524  font-size:12px; color:rgba(255,255,255,0.3);
525  font-weight:600; letter-spacing:0.04em;
526}
527.brand-cta{
528  margin-top:28px; background:linear-gradient(135deg,var(--accent),var(--accent2));
529  border:none; border-radius:999px; padding:14px 36px;
530  font-size:16px; font-weight:800; color:#fff;
531  box-shadow:0 4px 24px rgba(124,58,237,0.5);
532  cursor:pointer;
533  text-decoration:none;
534  display:inline-flex;
535  align-items:center;
536  justify-content:center;
537  transform:scale(0.5);
538  transition:transform 0.5s 0.15s cubic-bezier(.34,1.56,.64,1);
539}
540.brand-screen.show .brand-cta{ transform:scale(1); }
541
542/* ── Progress dots ── */
543.progress-bar{
544  position:absolute; bottom:22px; left:50%; transform:translateX(-50%);
545  display:flex; gap:6px; z-index:60;
546}
547.prog-dot{
548  width:6px; height:6px; border-radius:50%;
549  background:rgba(255,255,255,0.2); transition:all 0.3s;
550}
551.prog-dot.active{ background:#fff; width:18px; border-radius:3px; }
552
553/* ── Transition overlay ── */
554.trans-overlay{
555  position:absolute; inset:0; z-index:35;
556  background:rgba(10,10,20,0.95);
557  opacity:0; pointer-events:none;
558  transition:opacity 0.25s;
559}
560.trans-overlay.flash{ opacity:1; }
561
562/* ── Step label ── */
563.step-label{
564  position:absolute; top:24px; left:50%; transform:translateX(-50%);
565  display:flex; align-items:center; gap:6px; z-index:45;
566  opacity:0; transition:opacity 0.4s; white-space:nowrap;
567}
568.step-label.show{ opacity:1; }
569.step-dot{ width:6px; height:6px; border-radius:50%; background:var(--accent); }
570.step-text{ font-size:11px; font-weight:700; letter-spacing:0.08em; text-transform:uppercase; color:rgba(255,255,255,0.5); }
571</style>
572</head>
573<body>
574<div class="stage" id="stage">
575
576  <!-- Progress dots -->
577  <div class="progress-bar">
578    <div class="prog-dot active" id="d0"></div>
579    <div class="prog-dot" id="d1"></div>
580    <div class="prog-dot" id="d2"></div>
581    <div class="prog-dot" id="d3"></div>
582    <div class="prog-dot" id="d4"></div>
583  </div>
584
585  <!-- Transition overlay -->
586  <div class="trans-overlay" id="transOverlay"></div>
587
588  <div class="orb one"></div>
589  <div class="orb two"></div>
590  <div class="phone-halo"></div>
591
592  <!-- Step label -->
593  <div class="step-label" id="stepLabel">
594    <div class="step-dot"></div>
595    <div class="step-text" id="stepText">Companion Mode</div>
596  </div>
597
598  <!-- Caption: hook -->
599  <div class="caption hero-center" id="captionHook">
600    <div class="tag" id="copyHookTag">Soulor AI</div>
601    <h1 id="copyHookTitle">An AI that knows<br>your context.</h1>
602  </div>
603
604  <!-- Caption: switch -->
605  <div class="caption mid" id="captionSwitch">
606    <div class="caption-card">
607      <div class="tag" id="copySwitchTag">Simulation Mode</div>
608      <h2 id="copySwitchTitle">Need real-world advice?<br>Get five honest takes on the same moment.</h2>
609      <p><span id="copySwitchSub" style="color:#a78bfa">One tap opens 5 perspectives.</span></p>
610    </div>
611  </div>
612
613  <!-- Phone -->
614  <div class="phone" id="phone">
615
616    <!-- VIEW 1: Companion chat -->
617    <div class="view active" id="viewCompanion">
618      <div class="topbar">
619        <div class="tb-back">‹</div>
620        <div class="tb-avatar"><img class="avatar-photo" src="/default-avatar.png" alt="0816"></div>
621        <div class="tb-info">
622          <div class="tb-name">0816</div>
623          <div class="tb-status" id="copyActiveNow">Active now</div>
624        </div>
625        <div class="tb-actions">
626          <div class="tb-btn"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="23 7 16 12 23 17 23 7"/><rect x="1" y="5" width="15" height="14" rx="2"/></svg></div>
627          <div class="tb-btn"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg></div>
628        </div>
629      </div>
630
631      <div class="chat-area" id="chatArea">
632        <div class="msg-row user" id="m0"><div class="bubble user" id="copyUserMsg1">saw my friends posted hangout pics and i wasn't invited 😶</div></div>
633        <div class="msg-row ai" id="mTyping1">
634          <div class="bubble ai typing show" id="typing1"><span></span><span></span><span></span></div>
635        </div>
636        <div class="msg-row ai" id="m1">
637          <div class="stack">
638            <div class="bubble ai" id="copyAiMsg1">ow... that hurts.<br>that feels deliberate.</div>
639            <div class="voice-card">
640              <div class="voice-play">
641                <svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M8 5v14l11-7z"/></svg>
642              </div>
643              <div class="voice-wave" aria-hidden="true">
644                <span class="voice-bar"></span>
645                <span class="voice-bar"></span>
646                <span class="voice-bar"></span>
647                <span class="voice-bar"></span>
648                <span class="voice-bar"></span>
649                <span class="voice-bar"></span>
650              </div>
651              <div class="voice-time">0:12</div>
652            </div>
653            <div class="meme-card">
654              <img class="meme-thumb" src="https://media.giphy.com/media/l0HlvtIPzPdt2usKs/giphy.gif" alt="Reaction meme from Giphy">
655              <div class="meme-overlay">
656                <div class="meme-label" id="copyMemeLabel">Meme</div>
657                <div class="meme-cap" id="copyMemeCap">me processing the group chat</div>
658                <div class="meme-credit">Powered by Giphy</div>
659              </div>
660            </div>
661          </div>
662        </div>
663        <div class="msg-row user" id="m2"><div class="bubble user" id="copyUserMsg2">looks planned and they definitely know me well enough to invite me 😤</div></div>
664      </div>
665
666      <div class="mode-bar">
667        <div class="mode-tag">
668          <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
669          <span id="copyCompanionModeTag">Companion</span>
670        </div>
671      </div>
672      <div class="input-bar">
673        <div class="input-field" id="copyMessagePlaceholder">Message 0816…</div>
674        <div class="send-btn" id="sendBtn"><svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M2 21l21-9L2 3v7l15 2-15 2z"/></svg></div>
675      </div>
676    </div>
677
678    <!-- VIEW 2: Simulation -->
679    <div class="view" id="viewSim">
680      <div class="sim-header">
681        <div style="display:flex;align-items:center;gap:10px">
682          <div style="width:36px;height:36px;border-radius:10px;background:rgba(236,72,153,0.2);display:flex;align-items:center;justify-content:center">
683            <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#f472b6" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/></svg>
684          </div>
685          <div>
686            <div class="sim-title" id="copySimPanelTitle">Simulation Panel</div>
687            <div class="sim-sub" id="copySimPanelSub">5 perspectives on your situation</div>
688          </div>
689        </div>
690      </div>
691
692      <div class="sim-topic" id="copySimTopic">"Friends hung out without me and posted it"</div>
693
694      <div class="sim-list">
695        <div class="analyst" id="a0">
696          <div class="analyst-dot" style="background:#fbbf24"></div>
697          <div><div class="analyst-name" id="copyAnalyst0Name">Optimist ☀️</div><div class="analyst-text" id="copyAnalyst0Text">Maybe it was last-minute and they felt bad about it. One honest conversation could clear the air.</div></div>
698        </div>
699        <div class="analyst" id="a1">
700          <div class="analyst-dot" style="background:#94a3b8"></div>
701          <div><div class="analyst-name" id="copyAnalyst1Name">Cynic 🌧️</div><div class="analyst-text" id="copyAnalyst1Text">If it happened once and they posted it, it wasn't an accident. Pay attention to the pattern.</div></div>
702        </div>
703        <div class="analyst" id="a2">
704          <div class="analyst-dot" style="background:#4ade80"></div>
705          <div><div class="analyst-name" id="copyAnalyst2Name">Mentor 🧭</div><div class="analyst-text" id="copyAnalyst2Text">Don't ghost or spiral. Just say "hey, saw the pics — would've loved to join." Simple and direct.</div></div>
706        </div>
707        <div class="analyst" id="a3">
708          <div class="analyst-dot" style="background:#60a5fa"></div>
709          <div><div class="analyst-name" id="copyAnalyst3Name">Observer 👁️</div><div class="analyst-text" id="copyAnalyst3Text">The fact they posted it publicly says something. They either forgot you or wanted you to see it.</div></div>
710        </div>
711        <div class="analyst" id="a4">
712          <div class="analyst-dot" style="background:#c084fc"></div>
713          <div><div class="analyst-name" id="copyAnalyst4Name">Gossiper 💬</div><div class="analyst-text" id="copyAnalyst4Text">Someone in that group definitely thought about whether to invite you. Someone made that call.</div></div>
714        </div>
715      </div>
716
717      <div class="sim-back-btn" id="simBackBtn">
718        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
719        <span id="copyBackTo0816">Back to 0816</span>
720      </div>
721    </div>
722
723    <!-- Switch button (floats over companion view) -->
724    <div class="switch-btn" id="switchBtn">
725      <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/></svg>
726      <span id="copySimulationBtn">Simulation</span>
727    </div>
728
729  </div><!-- end phone -->
730
731  <!-- Branding screen -->
732  <div class="brand-screen" id="brandScreen">
733    <div class="brand-logo">
734      <img src="/icon-192.png" alt="Soulor AI icon">
735    </div>
736    <div class="brand-name" id="copyBrandName">Soulor AI</div>
737    <div class="brand-sub" id="copyBrandSub">Talk it out. Then see the whole picture.</div>
738    <a class="brand-cta" href="/app" id="copyBrandCta">Try it free →</a>
739    <div class="brand-url" style="margin-top:20px">vercel.app</div>
740  </div>
741
742</div><!-- end stage -->
743
744<script>
745const $ = id => document.getElementById(id);
746const show = id => $(id)?.classList.add('show');
747const hide = id => $(id)?.classList.remove('show');
748const delay = ms => new Promise(r => setTimeout(r, ms));
749const AD_LOCALE = location.pathname === '/ad2' ? 'zh' : 'en';
750const AD_COPY = {
751  en: {
752    pageTitle: 'Soulor AI — Ad',
753    stepCompanion: 'Companion Mode',
754    stepSimulation: 'Simulation Mode',
755    hookTag: 'Soulor AI',
756    hookTitle: 'An AI that knows<br>your context.',
757    switchTag: 'Simulation Mode',
758    switchTitle: 'Need real-world advice?<br>Get five honest takes on the same moment.',
759    switchSub: 'One tap opens 5 perspectives.',
760    activeNow: 'Active now',
761    userMsg1: "saw my friends posted hangout pics and i wasn't invited 😶",
762    aiMsg1: 'ow... that hurts.<br>that feels deliberate.',
763    memeCap: 'me processing the group chat',
764    memeLabel: 'Meme',
765    userMsg2: 'looks planned and they definitely know me well enough to invite me 😤',
766    companionModeTag: 'Companion',
767    messagePlaceholder: 'Message 0816…',
768    simPanelTitle: 'Simulation Panel',
769    simPanelSub: '5 perspectives on your situation',
770    simTopic: '"Friends hung out without me and posted it"',
771    analyst0Name: 'Optimist ☀️',
772    analyst0Text: 'Maybe it was last-minute and they felt bad about it. One honest conversation could clear the air.',
773    analyst1Name: 'Cynic 🌧️',
774    analyst1Text: "If it happened once and they posted it, it wasn't an accident. Pay attention to the pattern.",
775    analyst2Name: 'Mentor 🧭',
776    analyst2Text: 'Don\'t ghost or spiral. Just say "hey, saw the pics — would\'ve loved to join." Simple and direct.',
777    analyst3Name: 'Observer 👁️',
778    analyst3Text: 'The fact they posted it publicly says something. They either forgot you or wanted you to see it.',
779    analyst4Name: 'Gossiper 💬',
780    analyst4Text: 'Someone in that group definitely thought about whether to invite you. Someone made that call.',
781    backTo0816: 'Back to 0816',
782    simulationBtn: 'Simulation',
783    brandName: 'Soulor AI',
784    brandSub: 'Talk it out. Then see the whole picture.',
785    brandCta: 'Try it free →',
786    switchBack: 'So — which one hit different? 🤔',
787  },
788  zh: {
789    pageTitle: 'Soulor AI — 中文广告',
790    stepCompanion: '陪伴模式',
791    stepSimulation: '模拟模式',
792    hookTag: 'Soulor AI',
793    hookTitle: '一个真正懂你<br>处境的 AI。',
794    switchTag: '模拟模式',
795    switchTitle: '想听现实一点的建议?<br>同一件事,给你五种真实视角。',
796    switchSub: '一点进入 5 种不同解读。',
797    activeNow: '在线中',
798    userMsg1: '我朋友发了聚会照片,但根本没叫我 😶',
799    aiMsg1: '啊……这很伤人。<br>看起来真的像故意的。',
800    memeCap: '我正在疯狂分析这个群聊',
801    memeLabel: '梗图',
802    userMsg2: '看起来就是有计划的,而且他们明明知道该叫上我 😤',
803    companionModeTag: '陪伴',
804    messagePlaceholder: '给 0816 发消息…',
805    simPanelTitle: '模拟面板',
806    simPanelSub: '关于这件事的 5 种视角',
807    simTopic: '“朋友们背着我聚会,还发出来了”',
808    analyst0Name: '乐观派 ☀️',
809    analyst0Text: '也许是临时决定的,他们后来才意识到没叫你。一次坦白的沟通,可能就能说开。',
810    analyst1Name: '怀疑派 🌧️',
811    analyst1Text: '如果事情发生了,还公开发出来,那就不太像意外。重点要看这是不是一个模式。',
812    analyst2Name: '导师派 🧭',
813    analyst2Text: '别直接消失,也别先崩溃。就简单说一句:“我看到照片了,本来也很想一起去。”直接但不失分寸。',
814    analyst3Name: '观察派 👁️',
815    analyst3Text: '他们既然公开发了,说明这件事本身就有态度。可能是忘了你,也可能是默认你会看到。',
816    analyst4Name: '八卦派 💬',
817    analyst4Text: '那一群人里一定有人想过“要不要叫你”。只是最后,有人做了不叫你的决定。',
818    backTo0816: '回到 0816',
819    simulationBtn: '模拟',
820    brandName: 'Soulor AI',
821    brandSub: '先把话说出来,再看清整件事。',
822    brandCta: '免费试试 →',
823    switchBack: '所以呢,哪一种说法最戳你? 🤔',
824  },
825};
826
827function applyAdLocale() {
828  const copy = AD_COPY[AD_LOCALE] || AD_COPY.en;
829  document.title = copy.pageTitle;
830  const htmlMap = {
831    copyHookTitle: copy.hookTitle,
832    copySwitchTitle: copy.switchTitle,
833    copyAiMsg1: copy.aiMsg1,
834  };
835  const textMap = {
836    stepText: copy.stepCompanion,
837    copyHookTag: copy.hookTag,
838    copySwitchTag: copy.switchTag,
839    copySwitchSub: copy.switchSub,
840    copyActiveNow: copy.activeNow,
841    copyUserMsg1: copy.userMsg1,
842    copyMemeCap: copy.memeCap,
843    copyMemeLabel: copy.memeLabel,
844    copyUserMsg2: copy.userMsg2,
845    copyCompanionModeTag: copy.companionModeTag,
846    copyMessagePlaceholder: copy.messagePlaceholder,
847    copySimPanelTitle: copy.simPanelTitle,
848    copySimPanelSub: copy.simPanelSub,
849    copySimTopic: copy.simTopic,
850    copyAnalyst0Name: copy.analyst0Name,
851    copyAnalyst0Text: copy.analyst0Text,
852    copyAnalyst1Name: copy.analyst1Name,
853    copyAnalyst1Text: copy.analyst1Text,
854    copyAnalyst2Name: copy.analyst2Name,
855    copyAnalyst2Text: copy.analyst2Text,
856    copyAnalyst3Name: copy.analyst3Name,
857    copyAnalyst3Text: copy.analyst3Text,
858    copyAnalyst4Name: copy.analyst4Name,
859    copyAnalyst4Text: copy.analyst4Text,
860    copyBackTo0816: copy.backTo0816,
861    copySimulationBtn: copy.simulationBtn,
862    copyBrandName: copy.brandName,
863    copyBrandSub: copy.brandSub,
864    copyBrandCta: copy.brandCta,
865  };
866  Object.entries(htmlMap).forEach(([id, value]) => {
867    const el = $(id);
868    if (el) el.innerHTML = value;
869  });
870  Object.entries(textMap).forEach(([id, value]) => {
871    const el = $(id);
872    if (el) el.textContent = value;
873  });
874}
875
876function scaleStage() {
877  const stage = $('stage');
878  if (!stage) return;
879  const scale = Math.min(window.innerWidth / 900, window.innerHeight / 620, 1);
880  stage.style.transform = `scale(${scale})`;
881}
882
883function setDot(n) {
884  for(let i=0;i<5;i++) {
885    const d = $('d'+i);
886    d.classList.toggle('active', i===n);
887  }
888}
889
890function flash() {
891  const o = $('transOverlay');
892  o.classList.add('flash');
893  return delay(250).then(() => o.classList.remove('flash'));
894}
895
896// Brief exaggerated pop/punch on an element — classes aren't `forwards`, and
897// need removing after each play so the same class can retrigger the keyframe
898// animation next time (CSS animations only fire when the class is *added*,
899// not while merely present).
900function punch(el) {
901  el.classList.add('punch');
902  return delay(600).then(() => el.classList.remove('punch'));
903}
904function pulse(el) {
905  el.classList.add('pulse');
906  return delay(550).then(() => el.classList.remove('pulse'));
907}
908
909// Punches the camera in on a specific element until it dominates the frame,
910// holds, then eases back out — rather than everything staying pinned at one
911// small fixed phone size the whole time. Scales `.phone` itself (inline
912// style, not a class), anchored at the target element's on-phone position via
913// `transform-origin` — but a pure origin-anchored scale only keeps that one
914// point fixed; it doesn't move the target toward the frame's center, so a
915// target near an edge grows "in place" and its far side clips out of
916// `.stage`'s overflow:hidden. Fixed by pairing the origin with an explicit
917// translate that carries the target's center to the phone's own center as
918// part of the same transform, so the enlarged target always ends up fully
919// inside the frame regardless of where it started.
920//
921// Scale is computed from the target's own size, not a fixed constant — a
922// small round button and a wide message bubble have very different
923// width/height ratios to the phone frame, so each ends up at whatever scale
924// actually fills `fillFraction` of the frame on its tighter axis, instead of
925// every element (regardless of shape) getting the same magic-number zoom.
926function zoomTo(targetEl, fillFraction = 0.82, holdMs = 550) {
927  const phone = $('phone');
928  const pr = phone.getBoundingClientRect();
929  const tr = targetEl.getBoundingClientRect();
930  const originXpx = (tr.left + tr.width / 2) - pr.left;
931  const originYpx = (tr.top + tr.height / 2) - pr.top;
932  const dx = pr.width / 2 - originXpx;
933  const dy = pr.height / 2 - originYpx;
934  const scaleX = (pr.width * fillFraction) / tr.width;
935  const scaleY = (pr.height * fillFraction) / tr.height;
936  const scale = Math.min(scaleX, scaleY, 3.2); // capped so tiny elements (send-btn) don't blow up absurdly
937  phone.style.transition = 'transform 0.5s cubic-bezier(.4,0,.2,1)';
938  phone.style.transformOrigin = `${originXpx}px ${originYpx}px`;
939  phone.style.transform = `translate(${dx}px, ${dy}px) scale(${scale})`;
940  return delay(holdMs).then(() => {
941    phone.style.transform = 'scale(1)';
942    return delay(500);
943  }).then(() => {
944    phone.style.transition = '';
945    phone.style.transformOrigin = '';
946    phone.style.transform = '';
947  });
948}
949
950async function run() {
951  const copy = AD_COPY[AD_LOCALE] || AD_COPY.en;
952  // ── ACT 0: Hook ─────────────────────────────
953  setDot(0);
954  $('stepText').textContent = copy.stepCompanion;
955  show('captionHook');
956  show('stepLabel');
957  await delay(1500);
958  hide('captionHook');
959  await delay(400);
960
961  // ── ACT 1: Phone appears ─────────────────────
962  show('phone');
963  await delay(400);
964
965  // Messages appear one by one, each zoomed to fill the frame in turn as its
966  // moment comes — not everything staying pinned at one small fixed size.
967  // The zoom starts the instant the message starts appearing (not after),
968  // so appear and enlarge read as one motion instead of two separate beats.
969  pulse($('sendBtn'));
970  show('m0');
971  await zoomTo(document.querySelector('#m0 .bubble'));
972  $('typing1').classList.remove('show');
973  show('m1');
974  await zoomTo(document.querySelector('#m1 .bubble'));
975  pulse($('sendBtn'));
976  show('m2');
977  await zoomTo(document.querySelector('#m2 .bubble'));
978  await delay(200);
979
980  // Switch button appears and is zoomed at the same moment, before the tap
981  show('switchBtn');
982  setDot(1);
983  await zoomTo($('switchBtn'));
984
985  // ── ACT 2: Tap switch button ──────────────────
986  setDot(2);
987  $('switchBtn').classList.add('tap');
988  const ripple = document.createElement('div');
989  ripple.className = 'ripple';
990  const btn = $('switchBtn');
991  const br = btn.getBoundingClientRect();
992  const sr = $('stage').getBoundingClientRect();
993  ripple.style.left = (br.left - sr.left + br.width/2) + 'px';
994  ripple.style.top  = (br.top  - sr.top  + br.height/2) + 'px';
995  $('phone').appendChild(ripple);
996  await delay(120);
997  $('switchBtn').classList.remove('tap');
998  await delay(280);
999  // Manual overlay control instead of flash(): the swap must happen only once
1000  // the overlay has actually finished fading to opaque (its own 0.25s
1001  // transition), and must stay covered through the .view opacity crossfade
1002  // (0.3s) before fading out — otherwise both views ghost through each other.
1003  $('transOverlay').classList.add('flash');
1004  await delay(260);
1005  punch($('phone'));
1006
1007  // ── ACT 3: Simulation panel ───────────────────
1008  $('stepText').textContent = copy.stepSimulation;
1009  $('viewCompanion').classList.remove('active');
1010  $('switchBtn').classList.remove('show');
1011  $('viewSim').classList.add('active');
1012  await delay(300);
1013  $('transOverlay').classList.remove('flash');
1014  await delay(250);
1015  show('captionSwitch');
1016  await delay(1050);
1017  hide('captionSwitch');
1018  await delay(180);
1019
1020  // Enlarge sweeps across the analysts as each one appears: only the first
1021  // box is zoomed while it's the only one shown, then the zoom glides to
1022  // each new box the instant it appears, and only zooms back out once the
1023  // last box has had its moment — rather than jumping straight to one box
1024  // once all five are already sitting there at rest. Targets are measured
1025  // once up front (while the phone is still at rest scale) so each glide is
1026  // a clean transition between two known transforms, not a moving target.
1027  const phone = $('phone');
1028  const pr0 = phone.getBoundingClientRect();
1029  const analystTargets = [0,1,2,3,4].map(i => {
1030    const tr = $('a'+i).getBoundingClientRect();
1031    const originXpx = (tr.left + tr.width / 2) - pr0.left;
1032    const originYpx = (tr.top + tr.height / 2) - pr0.top;
1033    const scale = Math.min((pr0.width * 0.82) / tr.width, (pr0.height * 0.82) / tr.height, 3.2);
1034    return { originXpx, originYpx, dx: pr0.width / 2 - originXpx, dy: pr0.height / 2 - originYpx, scale };
1035  });
1036  phone.style.transition = 'transform 0.5s cubic-bezier(.4,0,.2,1)';
1037  for (let i = 0; i < 5; i++) {
1038    show('a'+i);
1039    const t = analystTargets[i];
1040    phone.style.transformOrigin = `${t.originXpx}px ${t.originYpx}px`;
1041    phone.style.transform = `translate(${t.dx}px, ${t.dy}px) scale(${t.scale})`;
1042    await delay(420);
1043  }
1044  phone.style.transform = 'scale(1)';
1045  await delay(450);
1046  phone.style.transition = '';
1047  phone.style.transformOrigin = '';
1048  phone.style.transform = '';
1049  show('simBackBtn');
1050  setDot(3);
1051  await delay(700);
1052
1053  // ── ACT 4: Switch back ─────────────────────────
1054  setDot(4);
1055  $('transOverlay').classList.add('flash');
1055 // see ACT 3 comment above
1056  await delay(260);
1057  punch($('phone'));
1058  $('viewSim').classList.remove('active');
1059  $('viewCompanion').classList.add('active');
1060  $('stepText').textContent = copy.stepCompanion;
1061  await delay(300);
1062  $('transOverlay').classList.remove('flash');
1063  await delay(250);
1064
1065  const switchBack = document.createElement('div');
1066  switchBack.className = 'msg-row ai show';
1067  switchBack.innerHTML = `<div class="bubble ai">${copy.switchBack}</div>`;
1068  $('chatArea').appendChild(switchBack);
1069  await delay(1400);
1070
1071  // ── ACT 5: Branding ───────────────────────────
1072  await flash();
1073  show('brandScreen');
1074  hide('stepLabel');
1075  await delay(2500);
1076
1077  // ── Loop ──────────────────────────────────────
1078  hide('brandScreen');
1079  // Reset
1080  $('viewSim').classList.remove('active');
1081  $('viewCompanion').classList.add('active');
1082  switchBack.remove();
1083  ['m0','m1','m2'].forEach(id => hide(id));
1084  $('typing1').classList.add('show');
1085  ['a0','a1','a2','a3','a4','simBackBtn'].forEach(id => hide(id));
1086  hide('switchBtn');
1087  hide('phone');
1088  show('stepLabel');
1089  await delay(300);
1090  run();
1091}
1092
1093// Start
1094window.addEventListener('load', () => {
1095  applyAdLocale();
1096  scaleStage();
1097  delay(800).then(run);
1098});
1099window.addEventListener('resize', scaleStage);
1100</script>
1100
1101</body>
1102</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.