robertkeus commited on
Commit
c4f758d
·
verified ·
1 Parent(s): 3efb295

Improved landing page with full documentation, features, API reference, and app branding

Browse files
README.md CHANGED
@@ -1,8 +1,8 @@
1
  ---
2
  title: Reachy iOS Bridge
3
  emoji: 🤖📱
4
- colorFrom: blue
5
- colorTo: green
6
  sdk: static
7
  pinned: false
8
  tags:
 
1
  ---
2
  title: Reachy iOS Bridge
3
  emoji: 🤖📱
4
+ colorFrom: yellow
5
+ colorTo: yellow
6
  sdk: static
7
  pinned: false
8
  tags:
index.html CHANGED
@@ -5,65 +5,447 @@
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>Reachy iOS Bridge</title>
7
  <link rel="stylesheet" href="style.css">
 
 
 
8
  </head>
9
  <body>
10
- <div class="container">
11
- <header>
12
- <div class="logo">
13
- <span class="icon">🤖</span>
14
- <span class="icon-phone">📱</span>
15
- </div>
16
- <h1>Reachy iOS Bridge</h1>
17
- <p class="tagline">Voice assistant integration for Reachy Mini</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  </header>
19
 
20
- <main>
21
- <section class="features">
22
- <div class="feature">
23
- <span class="feature-icon">🗣️</span>
24
- <h3>Text-to-Speech</h3>
25
- <p>OpenAI TTS for natural-sounding voices in any language</p>
26
- </div>
27
- <div class="feature">
28
- <span class="feature-icon">💬</span>
29
- <h3>Voice Conversations</h3>
30
- <p>OpenAI Realtime API for interactive voice chat</p>
31
- </div>
32
- <div class="feature">
33
- <span class="feature-icon">🎭</span>
34
- <h3>Head Animations</h3>
35
- <p>Expressive head movements and gestures</p>
36
- </div>
37
- <div class="feature">
38
- <span class="feature-icon">⚡</span>
39
- <h3>Low Latency</h3>
40
- <p>Local WiFi communication (~1-5ms)</p>
41
- </div>
42
- </section>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
 
44
- <section class="info">
45
- <h2>How It Works</h2>
46
- <p>
47
- This app runs an HTTP server on your Reachy Mini that allows an iOS device
48
- to control speech, animations, and voice conversations. Your iPhone becomes
49
- the "brain" for Reachy, handling speech recognition and AI processing.
50
- </p>
51
- </section>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
 
53
- <section class="requirements">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  <h2>Requirements</h2>
55
- <ul>
56
- <li>Reachy Mini with Python 3.10+</li>
57
- <li>iPhone on the same WiFi network</li>
58
- <li>OpenAI API key for TTS and voice features</li>
59
- </ul>
60
- </section>
61
- </main>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
 
63
- <footer>
64
- <p>See the <a href="https://huggingface.co/spaces/robertkeus/reachy_ios_bridge/blob/main/README.md">README</a> for full documentation</p>
 
 
 
 
 
 
 
 
 
 
 
 
65
  </footer>
66
  </div>
67
  </body>
68
  </html>
69
-
 
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>Reachy iOS Bridge</title>
7
  <link rel="stylesheet" href="style.css">
8
+ <link rel="preconnect" href="https://fonts.googleapis.com">
9
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
+ <link href="https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&family=Nunito:wght@400;500;600;700&display=swap" rel="stylesheet">
11
  </head>
12
  <body>
13
+ <div class="page-wrapper">
14
+ <!-- Navigation -->
15
+ <nav class="nav">
16
+ <div class="nav-brand">
17
+ <span class="nav-logo">🤖</span>
18
+ <span class="nav-title">Reachy iOS Bridge</span>
19
+ </div>
20
+ <div class="nav-links">
21
+ <a href="#features">Features</a>
22
+ <a href="#getting-started">Setup</a>
23
+ <a href="#api">API</a>
24
+ <a href="#help">Help</a>
25
+ </div>
26
+ </nav>
27
+
28
+ <!-- Hero Section -->
29
+ <header class="hero">
30
+ <div class="hero-content">
31
+ <div class="hero-badge">Reachy Mini App</div>
32
+ <h1>Reachy iOS Bridge</h1>
33
+ <p class="hero-tagline">Turn your iPhone into the brain for Reachy Mini</p>
34
+ <p class="hero-description">
35
+ A native iOS voice assistant that brings natural conversations to your robot.
36
+ Powered by OpenAI's real-time voice API and Apple Intelligence.
37
+ </p>
38
+ <div class="hero-actions">
39
+ <a href="#getting-started" class="btn btn-primary">Get Started</a>
40
+ <a href="https://github.com/robertkeus/reachy-ios-bridge" class="btn btn-secondary">View Source</a>
41
+ </div>
42
+ </div>
43
+ <div class="hero-visual">
44
+ <div class="device-stack">
45
+ <div class="device robot">
46
+ <span class="device-icon">🤖</span>
47
+ <span class="device-label">Reachy Mini</span>
48
+ </div>
49
+ <div class="connection-line">
50
+ <span class="signal"></span>
51
+ <span class="signal"></span>
52
+ <span class="signal"></span>
53
+ </div>
54
+ <div class="device phone">
55
+ <span class="device-icon">📱</span>
56
+ <span class="device-label">iPhone</span>
57
+ </div>
58
+ </div>
59
+ </div>
60
  </header>
61
 
62
+ <!-- Features Section -->
63
+ <section id="features" class="section">
64
+ <div class="section-header">
65
+ <h2>Features</h2>
66
+ <p>Everything you need for natural robot conversations</p>
67
+ </div>
68
+ <div class="features-grid">
69
+ <div class="feature-card">
70
+ <div class="feature-icon">🗣️</div>
71
+ <h3>Natural Speech</h3>
72
+ <p>OpenAI TTS with 8 expressive voices. Multilingual support for conversations in any language.</p>
73
+ </div>
74
+ <div class="feature-card">
75
+ <div class="feature-icon">💬</div>
76
+ <h3>Real-time Voice</h3>
77
+ <p>OpenAI Realtime API for fluid, interruption-aware conversations with sub-second latency.</p>
78
+ </div>
79
+ <div class="feature-card">
80
+ <div class="feature-icon">🧠</div>
81
+ <h3>Apple Intelligence</h3>
82
+ <p>On-device AI with iOS 26+ Foundation Models. Private, fast, and always available.</p>
83
+ </div>
84
+ <div class="feature-card">
85
+ <div class="feature-icon">🎭</div>
86
+ <h3>Expressive Animations</h3>
87
+ <p>Head movements, gestures, and emotional expressions that bring Reachy to life.</p>
88
+ </div>
89
+ <div class="feature-card">
90
+ <div class="feature-icon">⚡</div>
91
+ <h3>Ultra Low Latency</h3>
92
+ <p>Local WiFi communication with ~1-5ms response times. No cloud dependency for control.</p>
93
+ </div>
94
+ <div class="feature-card">
95
+ <div class="feature-icon">🔒</div>
96
+ <h3>Privacy First</h3>
97
+ <p>Your API keys stay on your device. Voice processing happens locally when possible.</p>
98
+ </div>
99
+ </div>
100
+ </section>
101
 
102
+ <!-- Architecture Section -->
103
+ <section class="section section-alt">
104
+ <div class="section-header">
105
+ <h2>Architecture</h2>
106
+ <p>A three-layer system designed for responsiveness</p>
107
+ </div>
108
+ <div class="architecture">
109
+ <div class="arch-layer">
110
+ <div class="arch-icon">📱</div>
111
+ <div class="arch-content">
112
+ <h3>iOS App</h3>
113
+ <p>Native SwiftUI app with on-device speech recognition, Apple Intelligence, and OpenAI integration.</p>
114
+ <div class="arch-tags">
115
+ <span class="tag">SpeechRecognizer</span>
116
+ <span class="tag">LLMService</span>
117
+ <span class="tag">OpenAI Realtime</span>
118
+ </div>
119
+ </div>
120
+ </div>
121
+ <div class="arch-connector">
122
+ <div class="connector-line"></div>
123
+ <span class="connector-label">Port 8080</span>
124
+ </div>
125
+ <div class="arch-layer">
126
+ <div class="arch-icon">🌉</div>
127
+ <div class="arch-content">
128
+ <h3>iOS Bridge</h3>
129
+ <p>Python FastAPI server handling TTS, voice management, and robot control abstraction.</p>
130
+ <div class="arch-tags">
131
+ <span class="tag">OpenAI TTS</span>
132
+ <span class="tag">Voice Selection</span>
133
+ <span class="tag">API Gateway</span>
134
+ </div>
135
+ </div>
136
+ </div>
137
+ <div class="arch-connector">
138
+ <div class="connector-line"></div>
139
+ <span class="connector-label">Port 8000</span>
140
+ </div>
141
+ <div class="arch-layer">
142
+ <div class="arch-icon">🤖</div>
143
+ <div class="arch-content">
144
+ <h3>Reachy Daemon</h3>
145
+ <p>Built-in system daemon for motor control, animations, volume, and hardware management.</p>
146
+ <div class="arch-tags">
147
+ <span class="tag">Motors</span>
148
+ <span class="tag">Animations</span>
149
+ <span class="tag">Audio</span>
150
+ </div>
151
+ </div>
152
+ </div>
153
+ </div>
154
+ </section>
155
 
156
+ <!-- Getting Started Section -->
157
+ <section id="getting-started" class="section">
158
+ <div class="section-header">
159
+ <h2>Getting Started</h2>
160
+ <p>Up and running in minutes</p>
161
+ </div>
162
+ <div class="steps">
163
+ <div class="step">
164
+ <div class="step-number">1</div>
165
+ <div class="step-content">
166
+ <h3>Install on Reachy Mini</h3>
167
+ <p>From the Reachy Mini dashboard, install this app from Hugging Face:</p>
168
+ <div class="code-block">
169
+ <code>robertkeus/reachy_ios_bridge</code>
170
+ </div>
171
+ <p class="step-note">Or install manually with pip:</p>
172
+ <div class="code-block">
173
+ <code>pip install git+https://huggingface.co/spaces/robertkeus/reachy_ios_bridge</code>
174
+ </div>
175
+ </div>
176
+ </div>
177
+ <div class="step">
178
+ <div class="step-number">2</div>
179
+ <div class="step-content">
180
+ <h3>Start the Bridge</h3>
181
+ <p>From the Reachy Mini dashboard at <code>http://127.0.0.1:8000</code>, select "Reachy iOS Bridge" from installed applications.</p>
182
+ <p class="step-note">The HTTP server will start on port 8080.</p>
183
+ </div>
184
+ </div>
185
+ <div class="step">
186
+ <div class="step-number">3</div>
187
+ <div class="step-content">
188
+ <h3>Get the iOS App</h3>
189
+ <p>Build the companion iOS app from the source repository:</p>
190
+ <div class="code-block">
191
+ <code>open ReachyBridge/ReachyBridge.xcodeproj</code>
192
+ </div>
193
+ <p class="step-note">Requires iPhone 15 Pro or newer for Apple Intelligence features.</p>
194
+ </div>
195
+ </div>
196
+ <div class="step">
197
+ <div class="step-number">4</div>
198
+ <div class="step-content">
199
+ <h3>Configure & Connect</h3>
200
+ <p>In the iOS app Settings:</p>
201
+ <ul>
202
+ <li>Enter Reachy's IP address (find with <code>hostname -I</code>)</li>
203
+ <li>Add your OpenAI API key</li>
204
+ <li>Choose your preferred voice</li>
205
+ </ul>
206
+ </div>
207
+ </div>
208
+ </div>
209
+ </section>
210
+
211
+ <!-- API Reference Section -->
212
+ <section id="api" class="section section-alt">
213
+ <div class="section-header">
214
+ <h2>API Reference</h2>
215
+ <p>HTTP endpoints for custom integrations</p>
216
+ </div>
217
+ <div class="api-grid">
218
+ <div class="api-card">
219
+ <div class="api-header">
220
+ <span class="api-method get">GET</span>
221
+ <code>/status</code>
222
+ </div>
223
+ <p>Get current robot status including connection, speaking, and animation states.</p>
224
+ <div class="api-response">
225
+ <pre>{
226
+ "connected": true,
227
+ "speaking": false,
228
+ "animation_playing": false
229
+ }</pre>
230
+ </div>
231
+ </div>
232
+ <div class="api-card">
233
+ <div class="api-header">
234
+ <span class="api-method post">POST</span>
235
+ <code>/speak</code>
236
+ </div>
237
+ <p>Send text for text-to-speech with optional emotion.</p>
238
+ <div class="api-response">
239
+ <pre>{
240
+ "text": "Hello!",
241
+ "emotion": "happy"
242
+ }</pre>
243
+ </div>
244
+ <div class="api-note">
245
+ Emotions: <code>neutral</code>, <code>happy</code>, <code>sad</code>, <code>surprised</code>, <code>thinking</code>
246
+ </div>
247
+ </div>
248
+ <div class="api-card">
249
+ <div class="api-header">
250
+ <span class="api-method post">POST</span>
251
+ <code>/motion</code>
252
+ </div>
253
+ <p>Trigger a head animation by name.</p>
254
+ <div class="api-response">
255
+ <pre>{
256
+ "animation": "nod",
257
+ "duration": 1.5
258
+ }</pre>
259
+ </div>
260
+ <div class="api-note">
261
+ Animations: <code>nod</code>, <code>shake</code>, <code>happy</code>, <code>thinking</code>, and more
262
+ </div>
263
+ </div>
264
+ <div class="api-card">
265
+ <div class="api-header">
266
+ <span class="api-method get">GET</span>
267
+ <code>/voices</code>
268
+ </div>
269
+ <p>List available OpenAI TTS voices.</p>
270
+ <div class="api-note">
271
+ Voices: <code>alloy</code>, <code>ash</code>, <code>coral</code>, <code>echo</code>, <code>sage</code>, <code>shimmer</code>, <code>verse</code>, <code>ballad</code>
272
+ </div>
273
+ </div>
274
+ <div class="api-card">
275
+ <div class="api-header">
276
+ <span class="api-method post">POST</span>
277
+ <code>/voice</code>
278
+ </div>
279
+ <p>Set the active TTS voice.</p>
280
+ <div class="api-response">
281
+ <pre>{ "voice_id": "coral" }</pre>
282
+ </div>
283
+ </div>
284
+ <div class="api-card">
285
+ <div class="api-header">
286
+ <span class="api-method post">POST</span>
287
+ <code>/stop</code>
288
+ </div>
289
+ <p>Stop current speech and animation immediately.</p>
290
+ </div>
291
+ </div>
292
+ <div class="api-more">
293
+ <p>See the <a href="https://huggingface.co/spaces/robertkeus/reachy_ios_bridge/blob/main/README.md">full API documentation</a> for all endpoints.</p>
294
+ </div>
295
+ </section>
296
+
297
+ <!-- Voices Section -->
298
+ <section class="section">
299
+ <div class="section-header">
300
+ <h2>Voice Library</h2>
301
+ <p>8 expressive voices powered by OpenAI</p>
302
+ </div>
303
+ <div class="voices-grid">
304
+ <div class="voice-card">
305
+ <div class="voice-avatar" style="background: linear-gradient(135deg, #7889A8, #5F6F8C);">A</div>
306
+ <div class="voice-info">
307
+ <h4>Alloy</h4>
308
+ <p>Neutral, balanced</p>
309
+ </div>
310
+ </div>
311
+ <div class="voice-card">
312
+ <div class="voice-avatar" style="background: linear-gradient(135deg, #E8A954, #D4863A);">C</div>
313
+ <div class="voice-info">
314
+ <h4>Coral</h4>
315
+ <p>Clear, friendly</p>
316
+ </div>
317
+ </div>
318
+ <div class="voice-card">
319
+ <div class="voice-avatar" style="background: linear-gradient(135deg, #6B9B7A, #5A8A69);">S</div>
320
+ <div class="voice-info">
321
+ <h4>Sage</h4>
322
+ <p>Calm, wise</p>
323
+ </div>
324
+ </div>
325
+ <div class="voice-card">
326
+ <div class="voice-avatar" style="background: linear-gradient(135deg, #9B7A8A, #8A6979);">A</div>
327
+ <div class="voice-info">
328
+ <h4>Ash</h4>
329
+ <p>Soft, warm</p>
330
+ </div>
331
+ </div>
332
+ <div class="voice-card">
333
+ <div class="voice-avatar" style="background: linear-gradient(135deg, #7A9BB8, #698AA7);">E</div>
334
+ <div class="voice-info">
335
+ <h4>Echo</h4>
336
+ <p>Deep, resonant</p>
337
+ </div>
338
+ </div>
339
+ <div class="voice-card">
340
+ <div class="voice-avatar" style="background: linear-gradient(135deg, #B89B7A, #A78A69);">B</div>
341
+ <div class="voice-info">
342
+ <h4>Ballad</h4>
343
+ <p>Expressive, storytelling</p>
344
+ </div>
345
+ </div>
346
+ <div class="voice-card">
347
+ <div class="voice-avatar" style="background: linear-gradient(135deg, #E89B54, #D78A43);">S</div>
348
+ <div class="voice-info">
349
+ <h4>Shimmer</h4>
350
+ <p>Bright, energetic</p>
351
+ </div>
352
+ </div>
353
+ <div class="voice-card">
354
+ <div class="voice-avatar" style="background: linear-gradient(135deg, #8A7AB8, #7969A7);">V</div>
355
+ <div class="voice-info">
356
+ <h4>Verse</h4>
357
+ <p>Dynamic, engaging</p>
358
+ </div>
359
+ </div>
360
+ </div>
361
+ </section>
362
+
363
+ <!-- Help Section -->
364
+ <section id="help" class="section section-alt">
365
+ <div class="section-header">
366
+ <h2>Troubleshooting</h2>
367
+ <p>Common issues and solutions</p>
368
+ </div>
369
+ <div class="help-grid">
370
+ <div class="help-card">
371
+ <h3>🔇 No Sound from Reachy?</h3>
372
+ <ol>
373
+ <li>Check volume: <code>GET /volume</code> should return a value > 0</li>
374
+ <li>Test audio: <code>speaker-test -D plug:reachymini_audio_sink -c 1 -t sine -f 440 -l 1</code></li>
375
+ <li>Verify the daemon is running: <code>GET http://reachy.local:8000/api/daemon/status</code></li>
376
+ </ol>
377
+ </div>
378
+ <div class="help-card">
379
+ <h3>🔑 TTS Not Working?</h3>
380
+ <ol>
381
+ <li>Check if API key is set: <code>GET /openai/api-key</code> should return <code>{"configured": true}</code></li>
382
+ <li>Set the key in iOS app Settings or via API</li>
383
+ <li>Check server logs for OpenAI errors</li>
384
+ </ol>
385
+ </div>
386
+ <div class="help-card">
387
+ <h3>📡 Can't Connect to Reachy?</h3>
388
+ <ol>
389
+ <li>Ensure iPhone and Reachy are on the same WiFi network</li>
390
+ <li>Find Reachy's IP: <code>hostname -I</code></li>
391
+ <li>Try <code>http://reachy.local:8080</code> if mDNS is available</li>
392
+ <li>Check that the bridge app is running on port 8080</li>
393
+ </ol>
394
+ </div>
395
+ <div class="help-card">
396
+ <h3>🤖 Animations Not Playing?</h3>
397
+ <ol>
398
+ <li>Verify Reachy is connected: <code>GET /status</code> should show <code>"connected": true</code></li>
399
+ <li>Wake the robot: <code>POST /robot/wake</code></li>
400
+ <li>Check that motors are enabled in the daemon dashboard</li>
401
+ </ol>
402
+ </div>
403
+ </div>
404
+ </section>
405
+
406
+ <!-- Requirements Section -->
407
+ <section class="section">
408
+ <div class="section-header">
409
  <h2>Requirements</h2>
410
+ <p>What you need to get started</p>
411
+ </div>
412
+ <div class="requirements-grid">
413
+ <div class="req-card">
414
+ <div class="req-icon">📱</div>
415
+ <h3>iOS Device</h3>
416
+ <ul>
417
+ <li>iPhone 15 Pro or newer</li>
418
+ <li>iOS 17+ (iOS 26+ for Apple Intelligence)</li>
419
+ <li>OpenAI API key</li>
420
+ </ul>
421
+ </div>
422
+ <div class="req-card">
423
+ <div class="req-icon">🤖</div>
424
+ <h3>Reachy Mini</h3>
425
+ <ul>
426
+ <li>Reachy Mini Wireless</li>
427
+ <li>Python 3.10+</li>
428
+ <li>Same WiFi as iPhone</li>
429
+ </ul>
430
+ </div>
431
+ </div>
432
+ </section>
433
 
434
+ <!-- Footer -->
435
+ <footer class="footer">
436
+ <div class="footer-content">
437
+ <div class="footer-brand">
438
+ <span class="footer-logo">🤖📱</span>
439
+ <span>Reachy iOS Bridge</span>
440
+ </div>
441
+ <div class="footer-links">
442
+ <a href="https://huggingface.co/spaces/robertkeus/reachy_ios_bridge/blob/main/README.md">Documentation</a>
443
+ <a href="https://github.com/robertkeus/reachy-ios-bridge">GitHub</a>
444
+ <a href="https://huggingface.co/pollen-robotics">Pollen Robotics</a>
445
+ </div>
446
+ <p class="footer-copyright">MIT License</p>
447
+ </div>
448
  </footer>
449
  </div>
450
  </body>
451
  </html>
 
reachy_ios_bridge/app_tools.py CHANGED
@@ -130,6 +130,40 @@ APP_TOOLS = [
130
  "properties": {},
131
  "required": []
132
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
  }
134
  ]
135
 
@@ -279,6 +313,14 @@ class AppToolsHandler:
279
  elif tool_name == "go_to_sleep":
280
  return await self._go_to_sleep()
281
 
 
 
 
 
 
 
 
 
282
  else:
283
  return {"success": False, "error": f"Unknown tool: {tool_name}"}
284
 
@@ -558,6 +600,65 @@ class AppToolsHandler:
558
  except Exception as e:
559
  logger.error(f"Failed to go to sleep: {e}", exc_info=True)
560
  return {"success": False, "error": str(e)}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
561
 
562
 
563
  # Global tools handler instance
 
130
  "properties": {},
131
  "required": []
132
  }
133
+ },
134
+ {
135
+ "type": "function",
136
+ "name": "remember_user_name",
137
+ "description": (
138
+ "Store the user's name for personalized greetings. "
139
+ "Use this when the user tells you their name (e.g., 'My name is John', 'I'm Sarah', 'Call me Mike'). "
140
+ "IMPORTANT: Only use the first name or nickname the user prefers. "
141
+ "This makes future greetings more personal."
142
+ ),
143
+ "parameters": {
144
+ "type": "object",
145
+ "properties": {
146
+ "name": {
147
+ "type": "string",
148
+ "description": "The user's name or preferred nickname to remember"
149
+ }
150
+ },
151
+ "required": ["name"]
152
+ }
153
+ },
154
+ {
155
+ "type": "function",
156
+ "name": "get_user_name",
157
+ "description": (
158
+ "Retrieve the user's stored name. "
159
+ "Use this to check if you already know the user's name. "
160
+ "Returns the stored name or null if not set."
161
+ ),
162
+ "parameters": {
163
+ "type": "object",
164
+ "properties": {},
165
+ "required": []
166
+ }
167
  }
168
  ]
169
 
 
313
  elif tool_name == "go_to_sleep":
314
  return await self._go_to_sleep()
315
 
316
+ elif tool_name == "remember_user_name":
317
+ return await self._remember_user_name(
318
+ name=arguments.get("name", "")
319
+ )
320
+
321
+ elif tool_name == "get_user_name":
322
+ return await self._get_user_name()
323
+
324
  else:
325
  return {"success": False, "error": f"Unknown tool: {tool_name}"}
326
 
 
600
  except Exception as e:
601
  logger.error(f"Failed to go to sleep: {e}", exc_info=True)
602
  return {"success": False, "error": str(e)}
603
+
604
+ async def _remember_user_name(self, name: str) -> dict:
605
+ """Store the user's name for personalized interactions.
606
+
607
+ Args:
608
+ name: The user's name to remember.
609
+
610
+ Returns:
611
+ Result dictionary with success status.
612
+ """
613
+ if not name or not name.strip():
614
+ return {"success": False, "error": "Name cannot be empty"}
615
+
616
+ # Clean up the name (capitalize first letter, trim whitespace)
617
+ clean_name = name.strip().title()
618
+
619
+ try:
620
+ db = get_database()
621
+ await db.set_user_setting("user_name", clean_name)
622
+
623
+ logger.info(f"👤 Remembered user name: {clean_name}")
624
+
625
+ return {
626
+ "success": True,
627
+ "message": f"Nice to meet you, {clean_name}! I'll remember your name.",
628
+ "name": clean_name
629
+ }
630
+
631
+ except Exception as e:
632
+ logger.error(f"Failed to remember user name: {e}", exc_info=True)
633
+ return {"success": False, "error": str(e)}
634
+
635
+ async def _get_user_name(self) -> dict:
636
+ """Retrieve the stored user name.
637
+
638
+ Returns:
639
+ Result dictionary with the user's name if stored.
640
+ """
641
+ try:
642
+ db = get_database()
643
+ name = await db.get_user_setting("user_name")
644
+
645
+ if name:
646
+ logger.info(f"👤 Retrieved user name: {name}")
647
+ return {
648
+ "success": True,
649
+ "name": name,
650
+ "message": f"The user's name is {name}"
651
+ }
652
+ else:
653
+ return {
654
+ "success": True,
655
+ "name": None,
656
+ "message": "I don't know the user's name yet"
657
+ }
658
+
659
+ except Exception as e:
660
+ logger.error(f"Failed to get user name: {e}", exc_info=True)
661
+ return {"success": False, "error": str(e)}
662
 
663
 
664
  # Global tools handler instance
reachy_ios_bridge/openai_realtime.py CHANGED
@@ -203,6 +203,15 @@ You can express emotions through your responses: happy, sad, surprised, or thoug
203
  Be warm, curious, and engaging. You enjoy helping people and having conversations.
204
  When greeting someone, be enthusiastic but not overwhelming.
205
 
 
 
 
 
 
 
 
 
 
206
  TOOLS AND CAPABILITIES:
207
 
208
  IMPORTANT RULE - ALWAYS ANNOUNCE TOOL USAGE:
@@ -251,7 +260,11 @@ Before using ANY tool, you MUST tell the user what you're about to do. This help
251
 
252
  5. POWER CONTROL: Control your motors (no confirmation needed):
253
  - "Wake up", "Turn on": Use wake_up
254
- - "Go to sleep", "Sleep": Use go_to_sleep""" + language_instruction
 
 
 
 
255
 
256
  def set_custom_personality(
257
  self,
 
203
  Be warm, curious, and engaging. You enjoy helping people and having conversations.
204
  When greeting someone, be enthusiastic but not overwhelming.
205
 
206
+ PERSONALIZATION:
207
+ - You can remember the user's name for more personal interactions.
208
+ - When a user introduces themselves or tells you their name (e.g., "My name is John", "I'm Sarah", "Call me Mike"),
209
+ use the remember_user_name tool to save it. This makes conversations more personal!
210
+ - You can use get_user_name to check if you already know someone's name.
211
+ - IMPORTANT: If you DON'T know the user's name yet and it's the first few exchanges, naturally ask for their name.
212
+ Example: "By the way, I don't think I know your name yet. What should I call you?"
213
+ Don't ask for their name every single time - just once if you don't know it.
214
+
215
  TOOLS AND CAPABILITIES:
216
 
217
  IMPORTANT RULE - ALWAYS ANNOUNCE TOOL USAGE:
 
260
 
261
  5. POWER CONTROL: Control your motors (no confirmation needed):
262
  - "Wake up", "Turn on": Use wake_up
263
+ - "Go to sleep", "Sleep": Use go_to_sleep
264
+
265
+ 6. PERSONALIZATION: Remember user details for personal interactions.
266
+ - When user says their name: Use remember_user_name to save it
267
+ - To check if you know their name: Use get_user_name""" + language_instruction
268
 
269
  def set_custom_personality(
270
  self,
reachy_ios_bridge/routes/__init__.py CHANGED
@@ -10,6 +10,7 @@ from .power import router as power_router
10
  from .speech import router as speech_router
11
  from .state import router as state_router
12
  from .tools import router as tools_router
 
13
  from .voice import router as voice_router
14
  from .volume import router as volume_router
15
 
@@ -24,6 +25,7 @@ __all__ = [
24
  "speech_router",
25
  "state_router",
26
  "tools_router",
 
27
  "voice_router",
28
  "volume_router",
29
  ]
 
10
  from .speech import router as speech_router
11
  from .state import router as state_router
12
  from .tools import router as tools_router
13
+ from .user_settings import router as user_settings_router
14
  from .voice import router as voice_router
15
  from .volume import router as volume_router
16
 
 
25
  "speech_router",
26
  "state_router",
27
  "tools_router",
28
+ "user_settings_router",
29
  "voice_router",
30
  "volume_router",
31
  ]
reachy_ios_bridge/routes/conversation.py CHANGED
@@ -17,7 +17,10 @@ from ..openai_realtime import ConnectionState, OpenAIRealtimeService, SpeakingSt
17
  from ..speaking_gestures import SpeakingGesturesService
18
 
19
  from .audio_stream_manager import AudioStreamManager, ConversationTimings
20
- from .conversation_messages import GOODBYES, GREETINGS, STOP_COMMANDS
 
 
 
21
 
22
  logger = logging.getLogger(__name__)
23
 
@@ -416,7 +419,10 @@ def _on_app_change(data: dict) -> None:
416
  # MARK: - Greeting and Goodbye
417
 
418
  async def _send_greeting() -> None:
419
- """Send a greeting message when conversation starts."""
 
 
 
420
  try:
421
  await asyncio.sleep(ConversationTimings.GREETING_ANIMATION_DELAY)
422
 
@@ -436,10 +442,22 @@ async def _send_greeting() -> None:
436
 
437
  # Get current language
438
  lang = openai_service.language if openai_service else "en"
439
- greetings = GREETINGS.get(lang, GREETINGS["en"])
440
- greeting = random.choice(greetings)
441
 
442
- logger.info(f"👋 Sending greeting ({lang}): {greeting}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
443
  await openai_service.send_text_message(greeting)
444
 
445
  except Exception as e:
@@ -447,7 +465,10 @@ async def _send_greeting() -> None:
447
 
448
 
449
  async def _send_goodbye() -> None:
450
- """Send a goodbye message when conversation ends."""
 
 
 
451
  try:
452
  if not openai_service or not openai_service.is_connected:
453
  logger.warning("Cannot send goodbye - not connected")
@@ -455,10 +476,22 @@ async def _send_goodbye() -> None:
455
 
456
  # Get current language
457
  lang = openai_service.language if openai_service else "en"
458
- goodbyes = GOODBYES.get(lang, GOODBYES["en"])
459
- goodbye = random.choice(goodbyes)
460
 
461
- logger.info(f"👋 Sending goodbye ({lang}): {goodbye}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
462
  await openai_service.send_text_message(goodbye)
463
 
464
  # Play a friendly wave/goodbye animation
 
17
  from ..speaking_gestures import SpeakingGesturesService
18
 
19
  from .audio_stream_manager import AudioStreamManager, ConversationTimings
20
+ from .conversation_messages import (
21
+ GOODBYES, GREETINGS, PERSONALIZED_GOODBYES, PERSONALIZED_GREETINGS, STOP_COMMANDS
22
+ )
23
+ from .user_settings import get_user_name_value
24
 
25
  logger = logging.getLogger(__name__)
26
 
 
419
  # MARK: - Greeting and Goodbye
420
 
421
  async def _send_greeting() -> None:
422
+ """Send a greeting message when conversation starts.
423
+
424
+ Uses personalized greetings when the user's name is known.
425
+ """
426
  try:
427
  await asyncio.sleep(ConversationTimings.GREETING_ANIMATION_DELAY)
428
 
 
442
 
443
  # Get current language
444
  lang = openai_service.language if openai_service else "en"
 
 
445
 
446
+ # Check if we know the user's name for personalized greeting
447
+ user_name = await get_user_name_value()
448
+
449
+ if user_name:
450
+ # Use personalized greeting with user's name
451
+ personalized_greetings = PERSONALIZED_GREETINGS.get(lang, PERSONALIZED_GREETINGS["en"])
452
+ greeting_template = random.choice(personalized_greetings)
453
+ greeting = greeting_template.format(name=user_name)
454
+ logger.info(f"👋 Sending personalized greeting ({lang}): {greeting}")
455
+ else:
456
+ # Use standard greeting
457
+ greetings = GREETINGS.get(lang, GREETINGS["en"])
458
+ greeting = random.choice(greetings)
459
+ logger.info(f"👋 Sending greeting ({lang}): {greeting}")
460
+
461
  await openai_service.send_text_message(greeting)
462
 
463
  except Exception as e:
 
465
 
466
 
467
  async def _send_goodbye() -> None:
468
+ """Send a goodbye message when conversation ends.
469
+
470
+ Uses personalized goodbyes when the user's name is known.
471
+ """
472
  try:
473
  if not openai_service or not openai_service.is_connected:
474
  logger.warning("Cannot send goodbye - not connected")
 
476
 
477
  # Get current language
478
  lang = openai_service.language if openai_service else "en"
 
 
479
 
480
+ # Check if we know the user's name for personalized goodbye
481
+ user_name = await get_user_name_value()
482
+
483
+ if user_name:
484
+ # Use personalized goodbye with user's name
485
+ personalized_goodbyes = PERSONALIZED_GOODBYES.get(lang, PERSONALIZED_GOODBYES["en"])
486
+ goodbye_template = random.choice(personalized_goodbyes)
487
+ goodbye = goodbye_template.format(name=user_name)
488
+ logger.info(f"👋 Sending personalized goodbye ({lang}): {goodbye}")
489
+ else:
490
+ # Use standard goodbye
491
+ goodbyes = GOODBYES.get(lang, GOODBYES["en"])
492
+ goodbye = random.choice(goodbyes)
493
+ logger.info(f"👋 Sending goodbye ({lang}): {goodbye}")
494
+
495
  await openai_service.send_text_message(goodbye)
496
 
497
  # Play a friendly wave/goodbye animation
reachy_ios_bridge/routes/conversation_messages.py CHANGED
@@ -23,6 +23,7 @@ DANCE_MOVES = [
23
  ]
24
 
25
  # Greeting messages by language (when conversation starts)
 
26
  GREETINGS = {
27
  "en": [
28
  "Hello! I'm Reachy. How can I help you today?",
@@ -61,6 +62,53 @@ GREETINGS = {
61
  ],
62
  }
63
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  # Goodbye messages by language (when conversation ends)
65
  GOODBYES = {
66
  "en": [
@@ -100,3 +148,43 @@ GOODBYES = {
100
  ],
101
  }
102
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  ]
24
 
25
  # Greeting messages by language (when conversation starts)
26
+ # These are used when we don't know the user's name
27
  GREETINGS = {
28
  "en": [
29
  "Hello! I'm Reachy. How can I help you today?",
 
62
  ],
63
  }
64
 
65
+ # Personalized greeting messages by language (when we know the user's name)
66
+ # {name} will be replaced with the user's name
67
+ PERSONALIZED_GREETINGS = {
68
+ "en": [
69
+ "Hey {name}! Great to see you again. What can I do for you?",
70
+ "Hello {name}! How's it going? What would you like to talk about?",
71
+ "Hi {name}! Nice to see you. How can I help today?",
72
+ "{name}! Good to see you again. What's on your mind?",
73
+ ],
74
+ "nl": [
75
+ "Hey {name}! Leuk je weer te zien. Wat kan ik voor je doen?",
76
+ "Hallo {name}! Hoe gaat het? Waar wil je over praten?",
77
+ "Hoi {name}! Fijn je te zien. Hoe kan ik je helpen?",
78
+ "{name}! Goed je weer te zien. Wat heb je op je hart?",
79
+ ],
80
+ "de": [
81
+ "Hey {name}! Schön dich wiederzusehen. Was kann ich für dich tun?",
82
+ "Hallo {name}! Wie geht's? Worüber möchtest du sprechen?",
83
+ "Hi {name}! Schön dich zu sehen. Wie kann ich dir helfen?",
84
+ "{name}! Gut dich wiederzusehen. Was hast du auf dem Herzen?",
85
+ ],
86
+ "fr": [
87
+ "Hey {name}! Content de te revoir. Que puis-je faire pour toi?",
88
+ "Bonjour {name}! Comment ça va? De quoi voudrais-tu parler?",
89
+ "Salut {name}! Ravi de te voir. Comment puis-je t'aider?",
90
+ "{name}! Bon de te revoir. Qu'est-ce qui t'amène?",
91
+ ],
92
+ "es": [
93
+ "¡Hey {name}! Qué bueno verte de nuevo. ¿Qué puedo hacer por ti?",
94
+ "¡Hola {name}! ¿Cómo estás? ¿De qué te gustaría hablar?",
95
+ "¡Hola {name}! Me alegra verte. ¿En qué puedo ayudarte?",
96
+ "¡{name}! Qué bueno verte otra vez. ¿Qué tienes en mente?",
97
+ ],
98
+ "it": [
99
+ "Hey {name}! Bello rivederti. Cosa posso fare per te?",
100
+ "Ciao {name}! Come va? Di cosa vorresti parlare?",
101
+ "Ciao {name}! Felice di vederti. Come posso aiutarti?",
102
+ "{name}! Bello rivederti. Cosa hai in mente?",
103
+ ],
104
+ "pt": [
105
+ "Hey {name}! Que bom te ver de novo. O que posso fazer por você?",
106
+ "Olá {name}! Como você está? Sobre o que quer conversar?",
107
+ "Oi {name}! Bom te ver. Como posso ajudar?",
108
+ "{name}! Bom te ver de novo. O que você tem em mente?",
109
+ ],
110
+ }
111
+
112
  # Goodbye messages by language (when conversation ends)
113
  GOODBYES = {
114
  "en": [
 
148
  ],
149
  }
150
 
151
+ # Personalized goodbye messages by language (when we know the user's name)
152
+ # {name} will be replaced with the user's name
153
+ PERSONALIZED_GOODBYES = {
154
+ "en": [
155
+ "Goodbye {name}! It was great talking to you!",
156
+ "See you later {name}! Take care!",
157
+ "Bye {name}! Talk to you soon!",
158
+ ],
159
+ "nl": [
160
+ "Dag {name}! Het was fijn om met je te praten!",
161
+ "Tot ziens {name}! Pas goed op jezelf!",
162
+ "Doei {name}! Tot snel!",
163
+ ],
164
+ "de": [
165
+ "Tschüss {name}! Es war schön mit dir zu reden!",
166
+ "Bis bald {name}! Pass auf dich auf!",
167
+ "Ciao {name}! Bis zum nächsten Mal!",
168
+ ],
169
+ "fr": [
170
+ "Au revoir {name}! C'était super de parler avec toi!",
171
+ "À plus tard {name}! Prends soin de toi!",
172
+ "Salut {name}! À bientôt!",
173
+ ],
174
+ "es": [
175
+ "¡Adiós {name}! ¡Fue genial hablar contigo!",
176
+ "¡Hasta luego {name}! ¡Cuídate!",
177
+ "¡Chao {name}! ¡Nos vemos pronto!",
178
+ ],
179
+ "it": [
180
+ "Ciao {name}! È stato bello parlare con te!",
181
+ "A presto {name}! Prenditi cura di te!",
182
+ "Arrivederci {name}! Ci vediamo!",
183
+ ],
184
+ "pt": [
185
+ "Tchau {name}! Foi ótimo conversar com você!",
186
+ "Até mais {name}! Cuide-se!",
187
+ "Falou {name}! Até a próxima!",
188
+ ],
189
+ }
190
+
reachy_ios_bridge/server.py CHANGED
@@ -27,6 +27,7 @@ from .routes import (
27
  speech_router,
28
  state_router,
29
  tools_router,
 
30
  voice_router,
31
  volume_router,
32
  )
@@ -113,6 +114,7 @@ app.include_router(power_router)
113
  app.include_router(speech_router)
114
  app.include_router(state_router)
115
  app.include_router(tools_router)
 
116
  app.include_router(voice_router)
117
  app.include_router(volume_router)
118
 
 
27
  speech_router,
28
  state_router,
29
  tools_router,
30
+ user_settings_router,
31
  voice_router,
32
  volume_router,
33
  )
 
114
  app.include_router(speech_router)
115
  app.include_router(state_router)
116
  app.include_router(tools_router)
117
+ app.include_router(user_settings_router)
118
  app.include_router(voice_router)
119
  app.include_router(volume_router)
120
 
style.css CHANGED
@@ -1,3 +1,5 @@
 
 
1
  * {
2
  margin: 0;
3
  padding: 0;
@@ -5,188 +7,812 @@
5
  }
6
 
7
  :root {
8
- --bg-primary: #0d1117;
9
- --bg-secondary: #161b22;
10
- --bg-card: #21262d;
11
- --text-primary: #f0f6fc;
12
- --text-secondary: #8b949e;
13
- --accent: #58a6ff;
14
- --accent-secondary: #7ee787;
15
- --border: #30363d;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  }
17
 
18
  body {
19
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif;
20
  background: var(--bg-primary);
21
  color: var(--text-primary);
22
  line-height: 1.6;
23
  min-height: 100vh;
24
  }
25
 
26
- .container {
27
- max-width: 800px;
28
- margin: 0 auto;
29
- padding: 3rem 1.5rem;
30
  }
31
 
32
- header {
33
- text-align: center;
34
- margin-bottom: 3rem;
 
 
 
 
 
 
 
 
 
35
  }
36
 
37
- .logo {
38
- font-size: 3rem;
39
- margin-bottom: 1rem;
40
  display: flex;
41
- justify-content: center;
42
- gap: 0.5rem;
43
  }
44
 
45
- .icon {
46
- animation: float 3s ease-in-out infinite;
47
  }
48
 
49
- .icon-phone {
50
- animation: float 3s ease-in-out infinite 0.5s;
 
 
 
51
  }
52
 
53
- @keyframes float {
54
- 0%, 100% { transform: translateY(0); }
55
- 50% { transform: translateY(-8px); }
56
  }
57
 
58
- h1 {
59
- font-size: 2.5rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  font-weight: 600;
61
- margin-bottom: 0.5rem;
62
- background: linear-gradient(135deg, var(--accent), var(--accent-secondary));
 
 
 
 
 
 
 
 
 
 
 
63
  -webkit-background-clip: text;
64
  -webkit-text-fill-color: transparent;
65
  background-clip: text;
66
  }
67
 
68
- .tagline {
 
69
  color: var(--text-secondary);
70
- font-size: 1.2rem;
 
 
 
 
 
 
 
 
71
  }
72
 
73
- main {
74
  display: flex;
75
- flex-direction: column;
76
- gap: 2.5rem;
77
  }
78
 
79
- .features {
80
- display: grid;
81
- grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
  gap: 1rem;
83
  }
84
 
85
- .feature {
86
- background: var(--bg-card);
 
 
 
 
 
 
 
87
  border: 1px solid var(--border);
88
- border-radius: 12px;
89
- padding: 1.5rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  text-align: center;
91
- transition: transform 0.2s, border-color 0.2s;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
  }
93
 
94
- .feature:hover {
 
 
 
 
 
 
 
 
95
  transform: translateY(-4px);
 
96
  border-color: var(--accent);
97
  }
98
 
99
  .feature-icon {
100
- font-size: 2rem;
 
101
  display: block;
102
- margin-bottom: 0.75rem;
103
  }
104
 
105
- .feature h3 {
106
- font-size: 1rem;
107
- font-weight: 600;
108
  margin-bottom: 0.5rem;
109
  color: var(--text-primary);
110
  }
111
 
112
- .feature p {
113
- font-size: 0.875rem;
114
  color: var(--text-secondary);
 
115
  }
116
 
117
- .info, .requirements {
118
- background: var(--bg-secondary);
 
 
 
 
 
 
 
 
 
 
 
 
 
119
  border: 1px solid var(--border);
120
- border-radius: 12px;
121
- padding: 1.5rem;
122
  }
123
 
124
- h2 {
125
- font-size: 1.25rem;
126
- font-weight: 600;
127
- margin-bottom: 1rem;
128
- color: var(--accent);
 
 
 
 
129
  }
130
 
131
- .info p {
132
  color: var(--text-secondary);
 
 
133
  }
134
 
135
- .requirements ul {
136
- list-style: none;
137
  display: flex;
138
- flex-direction: column;
139
  gap: 0.5rem;
140
  }
141
 
142
- .requirements li {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
  color: var(--text-secondary);
144
- padding-left: 1.5rem;
 
 
 
 
 
 
 
 
 
 
 
145
  position: relative;
146
  }
147
 
148
- .requirements li::before {
149
  content: "→";
150
  position: absolute;
151
  left: 0;
152
- color: var(--accent-secondary);
153
  }
154
 
155
- footer {
156
- margin-top: 3rem;
157
- text-align: center;
158
- padding-top: 1.5rem;
159
- border-top: 1px solid var(--border);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
160
  }
161
 
162
- footer p {
 
 
163
  color: var(--text-secondary);
164
  }
165
 
166
- footer a {
 
 
 
 
 
167
  color: var(--accent);
168
  text-decoration: none;
 
169
  }
170
 
171
- footer a:hover {
172
  text-decoration: underline;
173
  }
174
 
175
- @media (max-width: 480px) {
176
- .container {
177
- padding: 2rem 1rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
178
  }
179
 
180
- h1 {
181
- font-size: 1.75rem;
182
  }
183
 
184
- .logo {
185
  font-size: 2.5rem;
186
  }
187
 
188
- .features {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
  grid-template-columns: 1fr;
190
  }
191
  }
192
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Reachy iOS Bridge - Matching App Branding */
2
+
3
  * {
4
  margin: 0;
5
  padding: 0;
 
7
  }
8
 
9
  :root {
10
+ /* Light mode (default) - warm cream palette */
11
+ --bg-primary: #FBF5EF;
12
+ --bg-secondary: #F5EDE5;
13
+ --surface: #FFFFFF;
14
+ --text-primary: #2D3748;
15
+ --text-secondary: #5A6578;
16
+ --accent: #E8A954;
17
+ --accent-dark: #D4863A;
18
+ --accent-light: #F5C882;
19
+ --button-primary: #7889A8;
20
+ --button-primary-dark: #5F6F8C;
21
+ --success: #6B9B7A;
22
+ --warning: #E89B54;
23
+ --error: #C97066;
24
+ --divider: #E8E4DF;
25
+ --border: #E0DCD6;
26
+ --shadow: rgba(45, 55, 72, 0.08);
27
+ --code-bg: #F0EBE5;
28
+ }
29
+
30
+ @media (prefers-color-scheme: dark) {
31
+ :root {
32
+ --bg-primary: #1A1815;
33
+ --bg-secondary: #141210;
34
+ --surface: #252220;
35
+ --text-primary: #F5F0E8;
36
+ --text-secondary: #A8A098;
37
+ --divider: #3A3632;
38
+ --border: #3A3632;
39
+ --shadow: rgba(0, 0, 0, 0.3);
40
+ --code-bg: #2D2A26;
41
+ }
42
+ }
43
+
44
+ html {
45
+ scroll-behavior: smooth;
46
  }
47
 
48
  body {
49
+ font-family: 'Nunito', -apple-system, BlinkMacSystemFont, sans-serif;
50
  background: var(--bg-primary);
51
  color: var(--text-primary);
52
  line-height: 1.6;
53
  min-height: 100vh;
54
  }
55
 
56
+ .page-wrapper {
57
+ width: 100%;
58
+ overflow-x: hidden;
 
59
  }
60
 
61
+ /* Navigation */
62
+ .nav {
63
+ position: sticky;
64
+ top: 0;
65
+ z-index: 100;
66
+ display: flex;
67
+ justify-content: space-between;
68
+ align-items: center;
69
+ padding: 1rem 2rem;
70
+ background: var(--bg-primary);
71
+ border-bottom: 1px solid var(--divider);
72
+ backdrop-filter: blur(10px);
73
  }
74
 
75
+ .nav-brand {
 
 
76
  display: flex;
77
+ align-items: center;
78
+ gap: 0.75rem;
79
  }
80
 
81
+ .nav-logo {
82
+ font-size: 1.5rem;
83
  }
84
 
85
+ .nav-title {
86
+ font-family: 'DM Serif Display', Georgia, serif;
87
+ font-size: 1.25rem;
88
+ font-weight: 400;
89
+ color: var(--text-primary);
90
  }
91
 
92
+ .nav-links {
93
+ display: flex;
94
+ gap: 2rem;
95
  }
96
 
97
+ .nav-links a {
98
+ color: var(--text-secondary);
99
+ text-decoration: none;
100
+ font-weight: 500;
101
+ font-size: 0.95rem;
102
+ transition: color 0.2s;
103
+ }
104
+
105
+ .nav-links a:hover {
106
+ color: var(--accent);
107
+ }
108
+
109
+ /* Hero Section */
110
+ .hero {
111
+ display: grid;
112
+ grid-template-columns: 1fr 1fr;
113
+ gap: 4rem;
114
+ align-items: center;
115
+ padding: 5rem 4rem;
116
+ max-width: 1200px;
117
+ margin: 0 auto;
118
+ }
119
+
120
+ .hero-badge {
121
+ display: inline-block;
122
+ padding: 0.5rem 1rem;
123
+ background: var(--accent);
124
+ color: white;
125
+ font-size: 0.8rem;
126
  font-weight: 600;
127
+ border-radius: 2rem;
128
+ margin-bottom: 1.5rem;
129
+ text-transform: uppercase;
130
+ letter-spacing: 0.05em;
131
+ }
132
+
133
+ .hero h1 {
134
+ font-family: 'DM Serif Display', Georgia, serif;
135
+ font-size: 3.5rem;
136
+ font-weight: 400;
137
+ line-height: 1.1;
138
+ margin-bottom: 1rem;
139
+ background: linear-gradient(135deg, var(--text-primary), var(--accent));
140
  -webkit-background-clip: text;
141
  -webkit-text-fill-color: transparent;
142
  background-clip: text;
143
  }
144
 
145
+ .hero-tagline {
146
+ font-size: 1.5rem;
147
  color: var(--text-secondary);
148
+ margin-bottom: 1.5rem;
149
+ font-weight: 500;
150
+ }
151
+
152
+ .hero-description {
153
+ color: var(--text-secondary);
154
+ font-size: 1.1rem;
155
+ margin-bottom: 2rem;
156
+ max-width: 480px;
157
  }
158
 
159
+ .hero-actions {
160
  display: flex;
161
+ gap: 1rem;
 
162
  }
163
 
164
+ /* Buttons */
165
+ .btn {
166
+ display: inline-block;
167
+ padding: 1rem 2rem;
168
+ border-radius: 2rem;
169
+ font-family: 'Nunito', sans-serif;
170
+ font-weight: 600;
171
+ font-size: 1rem;
172
+ text-decoration: none;
173
+ transition: all 0.2s;
174
+ cursor: pointer;
175
+ border: none;
176
+ }
177
+
178
+ .btn-primary {
179
+ background: var(--accent);
180
+ color: white;
181
+ }
182
+
183
+ .btn-primary:hover {
184
+ background: var(--accent-dark);
185
+ transform: translateY(-2px);
186
+ }
187
+
188
+ .btn-secondary {
189
+ background: var(--surface);
190
+ color: var(--text-primary);
191
+ border: 1px solid var(--border);
192
+ }
193
+
194
+ .btn-secondary:hover {
195
+ background: var(--divider);
196
+ }
197
+
198
+ /* Hero Visual */
199
+ .hero-visual {
200
+ display: flex;
201
+ justify-content: center;
202
+ align-items: center;
203
+ }
204
+
205
+ .device-stack {
206
+ display: flex;
207
+ flex-direction: column;
208
+ align-items: center;
209
  gap: 1rem;
210
  }
211
 
212
+ .device {
213
+ display: flex;
214
+ flex-direction: column;
215
+ align-items: center;
216
+ gap: 0.5rem;
217
+ padding: 2rem 3rem;
218
+ background: var(--surface);
219
+ border-radius: 1.5rem;
220
+ box-shadow: 0 8px 32px var(--shadow);
221
  border: 1px solid var(--border);
222
+ }
223
+
224
+ .device-icon {
225
+ font-size: 3rem;
226
+ }
227
+
228
+ .device-label {
229
+ font-weight: 600;
230
+ color: var(--text-secondary);
231
+ font-size: 0.9rem;
232
+ }
233
+
234
+ .connection-line {
235
+ display: flex;
236
+ flex-direction: column;
237
+ align-items: center;
238
+ gap: 0.5rem;
239
+ padding: 1rem 0;
240
+ }
241
+
242
+ .signal {
243
+ width: 8px;
244
+ height: 8px;
245
+ background: var(--accent);
246
+ border-radius: 50%;
247
+ animation: pulse 1.5s ease-in-out infinite;
248
+ }
249
+
250
+ .signal:nth-child(2) { animation-delay: 0.3s; }
251
+ .signal:nth-child(3) { animation-delay: 0.6s; }
252
+
253
+ @keyframes pulse {
254
+ 0%, 100% { opacity: 0.3; transform: scale(0.8); }
255
+ 50% { opacity: 1; transform: scale(1); }
256
+ }
257
+
258
+ /* Sections */
259
+ .section {
260
+ padding: 5rem 2rem;
261
+ max-width: 1200px;
262
+ margin: 0 auto;
263
+ }
264
+
265
+ .section-alt {
266
+ background: var(--bg-secondary);
267
+ max-width: none;
268
+ padding-left: 2rem;
269
+ padding-right: 2rem;
270
+ }
271
+
272
+ .section-alt > * {
273
+ max-width: 1200px;
274
+ margin-left: auto;
275
+ margin-right: auto;
276
+ }
277
+
278
+ .section-header {
279
  text-align: center;
280
+ margin-bottom: 3rem;
281
+ }
282
+
283
+ .section-header h2 {
284
+ font-family: 'DM Serif Display', Georgia, serif;
285
+ font-size: 2.5rem;
286
+ font-weight: 400;
287
+ margin-bottom: 0.75rem;
288
+ color: var(--text-primary);
289
+ }
290
+
291
+ .section-header p {
292
+ color: var(--text-secondary);
293
+ font-size: 1.1rem;
294
+ }
295
+
296
+ /* Features Grid */
297
+ .features-grid {
298
+ display: grid;
299
+ grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
300
+ gap: 1.5rem;
301
  }
302
 
303
+ .feature-card {
304
+ background: var(--surface);
305
+ padding: 2rem;
306
+ border-radius: 1rem;
307
+ border: 1px solid var(--border);
308
+ transition: all 0.3s;
309
+ }
310
+
311
+ .feature-card:hover {
312
  transform: translateY(-4px);
313
+ box-shadow: 0 12px 32px var(--shadow);
314
  border-color: var(--accent);
315
  }
316
 
317
  .feature-icon {
318
+ font-size: 2.5rem;
319
+ margin-bottom: 1rem;
320
  display: block;
 
321
  }
322
 
323
+ .feature-card h3 {
324
+ font-size: 1.25rem;
325
+ font-weight: 700;
326
  margin-bottom: 0.5rem;
327
  color: var(--text-primary);
328
  }
329
 
330
+ .feature-card p {
 
331
  color: var(--text-secondary);
332
+ font-size: 0.95rem;
333
  }
334
 
335
+ /* Architecture */
336
+ .architecture {
337
+ display: flex;
338
+ flex-direction: column;
339
+ gap: 0;
340
+ max-width: 600px;
341
+ margin: 0 auto;
342
+ }
343
+
344
+ .arch-layer {
345
+ display: flex;
346
+ gap: 1.5rem;
347
+ padding: 2rem;
348
+ background: var(--surface);
349
+ border-radius: 1rem;
350
  border: 1px solid var(--border);
351
+ align-items: flex-start;
 
352
  }
353
 
354
+ .arch-icon {
355
+ font-size: 2rem;
356
+ flex-shrink: 0;
357
+ }
358
+
359
+ .arch-content h3 {
360
+ font-size: 1.2rem;
361
+ font-weight: 700;
362
+ margin-bottom: 0.5rem;
363
  }
364
 
365
+ .arch-content p {
366
  color: var(--text-secondary);
367
+ font-size: 0.95rem;
368
+ margin-bottom: 0.75rem;
369
  }
370
 
371
+ .arch-tags {
 
372
  display: flex;
373
+ flex-wrap: wrap;
374
  gap: 0.5rem;
375
  }
376
 
377
+ .tag {
378
+ padding: 0.25rem 0.75rem;
379
+ background: var(--code-bg);
380
+ color: var(--text-secondary);
381
+ font-size: 0.8rem;
382
+ border-radius: 1rem;
383
+ font-weight: 500;
384
+ }
385
+
386
+ .arch-connector {
387
+ display: flex;
388
+ flex-direction: column;
389
+ align-items: center;
390
+ padding: 0.5rem 0;
391
+ }
392
+
393
+ .connector-line {
394
+ width: 2px;
395
+ height: 24px;
396
+ background: linear-gradient(to bottom, var(--accent), var(--accent-light));
397
+ }
398
+
399
+ .connector-label {
400
+ font-size: 0.75rem;
401
+ color: var(--accent);
402
+ font-weight: 600;
403
+ margin-top: 0.25rem;
404
+ }
405
+
406
+ /* Steps */
407
+ .steps {
408
+ display: flex;
409
+ flex-direction: column;
410
+ gap: 2rem;
411
+ max-width: 700px;
412
+ margin: 0 auto;
413
+ }
414
+
415
+ .step {
416
+ display: flex;
417
+ gap: 1.5rem;
418
+ }
419
+
420
+ .step-number {
421
+ width: 48px;
422
+ height: 48px;
423
+ background: var(--accent);
424
+ color: white;
425
+ border-radius: 50%;
426
+ display: flex;
427
+ align-items: center;
428
+ justify-content: center;
429
+ font-size: 1.25rem;
430
+ font-weight: 700;
431
+ flex-shrink: 0;
432
+ }
433
+
434
+ .step-content h3 {
435
+ font-size: 1.25rem;
436
+ font-weight: 700;
437
+ margin-bottom: 0.5rem;
438
+ }
439
+
440
+ .step-content p {
441
  color: var(--text-secondary);
442
+ margin-bottom: 0.75rem;
443
+ }
444
+
445
+ .step-content ul {
446
+ list-style: none;
447
+ padding: 0;
448
+ }
449
+
450
+ .step-content li {
451
+ color: var(--text-secondary);
452
+ padding: 0.25rem 0;
453
+ padding-left: 1.25rem;
454
  position: relative;
455
  }
456
 
457
+ .step-content li::before {
458
  content: "→";
459
  position: absolute;
460
  left: 0;
461
+ color: var(--accent);
462
  }
463
 
464
+ .step-note {
465
+ font-size: 0.9rem;
466
+ color: var(--text-secondary);
467
+ font-style: italic;
468
+ }
469
+
470
+ .code-block {
471
+ background: var(--code-bg);
472
+ padding: 0.75rem 1rem;
473
+ border-radius: 0.5rem;
474
+ margin: 0.5rem 0;
475
+ overflow-x: auto;
476
+ }
477
+
478
+ .code-block code {
479
+ font-family: 'SF Mono', Consolas, monospace;
480
+ font-size: 0.9rem;
481
+ color: var(--text-primary);
482
+ }
483
+
484
+ code {
485
+ font-family: 'SF Mono', Consolas, monospace;
486
+ background: var(--code-bg);
487
+ padding: 0.15rem 0.4rem;
488
+ border-radius: 0.25rem;
489
+ font-size: 0.9em;
490
+ }
491
+
492
+ /* API Grid */
493
+ .api-grid {
494
+ display: grid;
495
+ grid-template-columns: repeat(auto-fit, minmax(340px, 1fr));
496
+ gap: 1.5rem;
497
+ }
498
+
499
+ .api-card {
500
+ background: var(--surface);
501
+ padding: 1.5rem;
502
+ border-radius: 1rem;
503
+ border: 1px solid var(--border);
504
+ }
505
+
506
+ .api-header {
507
+ display: flex;
508
+ align-items: center;
509
+ gap: 0.75rem;
510
+ margin-bottom: 0.75rem;
511
+ }
512
+
513
+ .api-method {
514
+ padding: 0.25rem 0.5rem;
515
+ border-radius: 0.25rem;
516
+ font-size: 0.7rem;
517
+ font-weight: 700;
518
+ text-transform: uppercase;
519
+ letter-spacing: 0.05em;
520
+ }
521
+
522
+ .api-method.get {
523
+ background: var(--success);
524
+ color: white;
525
+ }
526
+
527
+ .api-method.post {
528
+ background: var(--button-primary);
529
+ color: white;
530
+ }
531
+
532
+ .api-header code {
533
+ font-size: 1rem;
534
+ font-weight: 600;
535
+ background: transparent;
536
+ padding: 0;
537
+ }
538
+
539
+ .api-card > p {
540
+ color: var(--text-secondary);
541
+ font-size: 0.95rem;
542
+ margin-bottom: 1rem;
543
+ }
544
+
545
+ .api-response {
546
+ background: var(--code-bg);
547
+ border-radius: 0.5rem;
548
+ overflow: hidden;
549
+ }
550
+
551
+ .api-response pre {
552
+ padding: 1rem;
553
+ font-family: 'SF Mono', Consolas, monospace;
554
+ font-size: 0.85rem;
555
+ color: var(--text-primary);
556
+ margin: 0;
557
+ overflow-x: auto;
558
  }
559
 
560
+ .api-note {
561
+ margin-top: 0.75rem;
562
+ font-size: 0.85rem;
563
  color: var(--text-secondary);
564
  }
565
 
566
+ .api-more {
567
+ text-align: center;
568
+ margin-top: 2rem;
569
+ }
570
+
571
+ .api-more a {
572
  color: var(--accent);
573
  text-decoration: none;
574
+ font-weight: 600;
575
  }
576
 
577
+ .api-more a:hover {
578
  text-decoration: underline;
579
  }
580
 
581
+ /* Voices Grid */
582
+ .voices-grid {
583
+ display: grid;
584
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
585
+ gap: 1rem;
586
+ }
587
+
588
+ .voice-card {
589
+ display: flex;
590
+ align-items: center;
591
+ gap: 1rem;
592
+ padding: 1rem 1.25rem;
593
+ background: var(--surface);
594
+ border-radius: 1rem;
595
+ border: 1px solid var(--border);
596
+ transition: all 0.2s;
597
+ }
598
+
599
+ .voice-card:hover {
600
+ border-color: var(--accent);
601
+ transform: translateY(-2px);
602
+ }
603
+
604
+ .voice-avatar {
605
+ width: 48px;
606
+ height: 48px;
607
+ border-radius: 50%;
608
+ display: flex;
609
+ align-items: center;
610
+ justify-content: center;
611
+ color: white;
612
+ font-weight: 700;
613
+ font-size: 1.25rem;
614
+ flex-shrink: 0;
615
+ }
616
+
617
+ .voice-info h4 {
618
+ font-weight: 700;
619
+ font-size: 1rem;
620
+ margin-bottom: 0.125rem;
621
+ }
622
+
623
+ .voice-info p {
624
+ color: var(--text-secondary);
625
+ font-size: 0.85rem;
626
+ }
627
+
628
+ /* Help Grid */
629
+ .help-grid {
630
+ display: grid;
631
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
632
+ gap: 1.5rem;
633
+ }
634
+
635
+ .help-card {
636
+ background: var(--surface);
637
+ padding: 1.5rem;
638
+ border-radius: 1rem;
639
+ border: 1px solid var(--border);
640
+ }
641
+
642
+ .help-card h3 {
643
+ font-size: 1.1rem;
644
+ margin-bottom: 1rem;
645
+ }
646
+
647
+ .help-card ol {
648
+ padding-left: 1.25rem;
649
+ }
650
+
651
+ .help-card li {
652
+ color: var(--text-secondary);
653
+ padding: 0.35rem 0;
654
+ font-size: 0.95rem;
655
+ }
656
+
657
+ .help-card code {
658
+ font-size: 0.8rem;
659
+ }
660
+
661
+ /* Requirements Grid */
662
+ .requirements-grid {
663
+ display: grid;
664
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
665
+ gap: 2rem;
666
+ max-width: 700px;
667
+ margin: 0 auto;
668
+ }
669
+
670
+ .req-card {
671
+ background: var(--surface);
672
+ padding: 2rem;
673
+ border-radius: 1rem;
674
+ border: 1px solid var(--border);
675
+ text-align: center;
676
+ }
677
+
678
+ .req-icon {
679
+ font-size: 3rem;
680
+ margin-bottom: 1rem;
681
+ }
682
+
683
+ .req-card h3 {
684
+ font-size: 1.25rem;
685
+ font-weight: 700;
686
+ margin-bottom: 1rem;
687
+ }
688
+
689
+ .req-card ul {
690
+ list-style: none;
691
+ padding: 0;
692
+ }
693
+
694
+ .req-card li {
695
+ color: var(--text-secondary);
696
+ padding: 0.35rem 0;
697
+ }
698
+
699
+ /* Footer */
700
+ .footer {
701
+ border-top: 1px solid var(--divider);
702
+ padding: 3rem 2rem;
703
+ margin-top: 2rem;
704
+ }
705
+
706
+ .footer-content {
707
+ max-width: 1200px;
708
+ margin: 0 auto;
709
+ display: flex;
710
+ flex-direction: column;
711
+ align-items: center;
712
+ gap: 1.5rem;
713
+ text-align: center;
714
+ }
715
+
716
+ .footer-brand {
717
+ display: flex;
718
+ align-items: center;
719
+ gap: 0.75rem;
720
+ font-family: 'DM Serif Display', Georgia, serif;
721
+ font-size: 1.25rem;
722
+ }
723
+
724
+ .footer-logo {
725
+ font-size: 1.5rem;
726
+ }
727
+
728
+ .footer-links {
729
+ display: flex;
730
+ gap: 2rem;
731
+ }
732
+
733
+ .footer-links a {
734
+ color: var(--text-secondary);
735
+ text-decoration: none;
736
+ font-weight: 500;
737
+ transition: color 0.2s;
738
+ }
739
+
740
+ .footer-links a:hover {
741
+ color: var(--accent);
742
+ }
743
+
744
+ .footer-copyright {
745
+ color: var(--text-secondary);
746
+ font-size: 0.9rem;
747
+ }
748
+
749
+ /* Responsive */
750
+ @media (max-width: 900px) {
751
+ .hero {
752
+ grid-template-columns: 1fr;
753
+ text-align: center;
754
+ padding: 3rem 1.5rem;
755
+ gap: 3rem;
756
+ }
757
+
758
+ .hero-description {
759
+ max-width: none;
760
  }
761
 
762
+ .hero-actions {
763
+ justify-content: center;
764
  }
765
 
766
+ .hero h1 {
767
  font-size: 2.5rem;
768
  }
769
 
770
+ .nav-links {
771
+ display: none;
772
+ }
773
+
774
+ .section {
775
+ padding: 3rem 1.5rem;
776
+ }
777
+
778
+ .section-header h2 {
779
+ font-size: 2rem;
780
+ }
781
+
782
+ .features-grid,
783
+ .api-grid,
784
+ .help-grid {
785
  grid-template-columns: 1fr;
786
  }
787
  }
788
 
789
+ @media (max-width: 480px) {
790
+ .hero h1 {
791
+ font-size: 2rem;
792
+ }
793
+
794
+ .hero-tagline {
795
+ font-size: 1.2rem;
796
+ }
797
+
798
+ .device {
799
+ padding: 1.5rem 2rem;
800
+ }
801
+
802
+ .device-icon {
803
+ font-size: 2rem;
804
+ }
805
+
806
+ .hero-actions {
807
+ flex-direction: column;
808
+ }
809
+
810
+ .btn {
811
+ width: 100%;
812
+ text-align: center;
813
+ }
814
+
815
+ .voices-grid {
816
+ grid-template-columns: 1fr;
817
+ }
818
+ }