mukul-chauhan-methdai commited on
Commit
9b1f90c
·
1 Parent(s): 20f0740

v1 cleanup: drop fork artifacts, add MethdAI logo, fix install URLs

Browse files

- Delete plan.md (internal POC doc, outdated)
- Delete index.html + style.css at repo root (marketing page from
upstream fork by Toon Beerten — not MethdAI's work)
- Add src/reachy_mini_receptionist/static/logo.png (MethdAI brand)
- Update dashboard.html to load /static/logo.png (was 404'ing on a
filename that never existed in the repo)
- README: fix install URL to spaces/MethdAI/receptionist-app, drop
GEMINI_LIVE_MODEL from optional table (no longer dashboard-exposed),
remove stale 'requires confirmation' note on register_guest

README.md CHANGED
@@ -40,7 +40,7 @@ A real **[Reachy Mini](https://www.pollen-robotics.com/reachy-mini/)** robot fro
40
  ### Option 1 — Install via the Reachy Mini Control app (recommended)
41
 
42
  1. Open Reachy Mini Control on your computer
43
- 2. **Install from Hugging Face** → search `methdai/reachy_mini_receptionist` → Install
44
  3. Toggle the app **On**
45
  4. Open the dashboard at `http://<your-robot-hostname>.local:7860/dashboard`
46
  5. Follow the welcome banner — it tells you exactly which API keys to add and where to find them
@@ -49,11 +49,8 @@ A real **[Reachy Mini](https://www.pollen-robotics.com/reachy-mini/)** robot fro
49
 
50
  ```bash
51
  # SSH into the robot
52
- git clone git@github.com:methdai/reachy_mini_receptionist.git
53
- cd reachy_mini_receptionist
54
-
55
- # Install the package in editable mode
56
- /venvs/apps_venv/bin/pip install -e .
57
 
58
  # Make sure the Reachy Mini daemon is running
59
  sudo systemctl status reachy-mini-daemon
@@ -86,7 +83,6 @@ The welcome banner on first launch tells you which keys are missing. Click each
86
  |---|---|
87
  | `VOICE_BACKEND` | Switch between `gemini` (default) and `openai` |
88
  | `GEMINI_LIVE_VOICE` | Pick a voice — Puck, Charon, Kore, Aoede, etc. |
89
- | `GEMINI_LIVE_MODEL` | Override the default Gemini Live model |
90
  | `OPENAI_API_KEY` | Required only if `VOICE_BACKEND=openai` |
91
  | `RESEND_FROM` | Sender address — defaults to Resend's sandbox sender (delivers only to your Resend account); set to `reception@yourdomain.com` after verifying your domain at Resend |
92
  | `FACE_TTL_DAYS` | How long a registered face is remembered (default 90 days) |
@@ -102,7 +98,7 @@ The welcome banner on first launch tells you which keys are missing. Click each
102
 
103
  **Voice + tools** — Either `gemini_live.py` or `openai_realtime.py` (chosen by `VOICE_BACKEND`) handles bi-directional audio with the LLM. Tools available to the LLM:
104
  - `get_today_calendar` — fetch today's appointments
105
- - `register_guest` — save a new visitor's face under a name (requires confirmation)
106
  - `lookup_employee` — find a host in the directory
107
  - `send_email` — notify the host
108
 
 
40
  ### Option 1 — Install via the Reachy Mini Control app (recommended)
41
 
42
  1. Open Reachy Mini Control on your computer
43
+ 2. **Install from Hugging Face** → paste `MethdAI/receptionist-app` → Install
44
  3. Toggle the app **On**
45
  4. Open the dashboard at `http://<your-robot-hostname>.local:7860/dashboard`
46
  5. Follow the welcome banner — it tells you exactly which API keys to add and where to find them
 
49
 
50
  ```bash
51
  # SSH into the robot
52
+ /venvs/apps_venv/bin/pip install --upgrade \
53
+ git+https://huggingface.co/spaces/MethdAI/receptionist-app
 
 
 
54
 
55
  # Make sure the Reachy Mini daemon is running
56
  sudo systemctl status reachy-mini-daemon
 
83
  |---|---|
84
  | `VOICE_BACKEND` | Switch between `gemini` (default) and `openai` |
85
  | `GEMINI_LIVE_VOICE` | Pick a voice — Puck, Charon, Kore, Aoede, etc. |
 
86
  | `OPENAI_API_KEY` | Required only if `VOICE_BACKEND=openai` |
87
  | `RESEND_FROM` | Sender address — defaults to Resend's sandbox sender (delivers only to your Resend account); set to `reception@yourdomain.com` after verifying your domain at Resend |
88
  | `FACE_TTL_DAYS` | How long a registered face is remembered (default 90 days) |
 
98
 
99
  **Voice + tools** — Either `gemini_live.py` or `openai_realtime.py` (chosen by `VOICE_BACKEND`) handles bi-directional audio with the LLM. Tools available to the LLM:
100
  - `get_today_calendar` — fetch today's appointments
101
+ - `register_guest` — save a new visitor's face under a name
102
  - `lookup_employee` — find a host in the directory
103
  - `send_email` — notify the host
104
 
index.html DELETED
@@ -1,141 +0,0 @@
1
- <!doctype html>
2
- <html>
3
-
4
- <head>
5
- <meta charset="utf-8" />
6
- <meta name="viewport" content="width=device-width, initial-scale=1" />
7
- <title>Reachy Mini AI Receptionist</title>
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=Space+Grotesk:wght@400;500;600;700&family=Manrope:wght@400;500;600&display=swap" rel="stylesheet">
11
- <link rel="stylesheet" href="style.css" />
12
- </head>
13
-
14
- <body>
15
- <header class="hero">
16
- <div class="topline">
17
- <div class="brand">
18
- <span class="logo">🤖</span>
19
- <span class="brand-name">Reachy Mini</span>
20
- </div>
21
- <div class="pill">Realtime voice · Vision aware · Expressive motion</div>
22
- </div>
23
- <div class="hero-grid">
24
- <div class="hero-copy">
25
- <p class="eyebrow">AI Receptionist</p>
26
- <h1>
27
- Face-aware receptionist with tool-calling automation.
28
- <a class="live-demo-badge" href="#live-demo">Live demo</a>
29
- </h1>
30
- <p class="lede">
31
- A camera-aware front-desk assistant for Reachy Mini. Greet visitors naturally, register guests, check appointments, and log handoff actions from a single dashboard.
32
- </p>
33
- <p class="lede">
34
- Built for reception workflows: realtime face detection to know when someone is present, face recognition to personalize interactions, and structured tool calls so the AI can register guests, query appointments, and trigger handoff actions reliably.
35
- </p>
36
- <div class="hero-actions">
37
- <a class="btn primary" href="#highlights">Explore features</a>
38
- <a class="btn ghost" href="#story">See how it feels</a>
39
- </div>
40
- <div class="hero-badges">
41
- <span>Realtime face detection</span>
42
- <span>Visitor face recognition</span>
43
- <span>Model tool-calling workflows</span>
44
- <span>Low-latency voice + dashboard ops</span>
45
- </div>
46
- </div>
47
- <div class="hero-visual">
48
- <div class="glass-card">
49
- <img src="screenshot.png" alt="Reachy Mini AI Receptionist screenshot" class="hero-gif">
50
- <p class="caption">Reachy Mini can greet, identify, and assist visitors with receptionist-specific context.</p>
51
- <div class="video-embed-wrapper" id="live-demo">
52
- <iframe
53
- src="https://www.youtube.com/embed/4U9uj5b9p2Y"
54
- title="Reachy Mini AI Receptionist live demo"
55
- loading="lazy"
56
- allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
57
- allowfullscreen>
58
- </iframe>
59
- </div>
60
- <p class="video-link"><a href="https://youtu.be/4U9uj5b9p2Y" target="_blank" rel="noopener">Watch on YouTube</a></p>
61
- </div>
62
- </div>
63
- </div>
64
- </header>
65
-
66
- <section id="highlights" class="section features">
67
- <div class="section-header">
68
- <p class="eyebrow">What’s inside</p>
69
- <h2>All-in-one receptionist layer for your robot</h2>
70
- <p class="intro">
71
- The app blends realtime speech, vision, and workflow tools so Reachy Mini can run a front desk flow.
72
- </p>
73
- </div>
74
- <div class="feature-grid">
75
- <div class="feature-card">
76
- <span class="icon">🎤</span>
77
- <h3>Natural voice chat</h3>
78
- <p>Talk freely and get fast, high-quality replies powered by realtime models.</p>
79
- </div>
80
- <div class="feature-card">
81
- <span class="icon">🎥</span>
82
- <h3>Face-aware onboarding</h3>
83
- <p>Recognize known visitors, register new guests, and keep the latest face context synced with the conversation.</p>
84
- </div>
85
- <div class="feature-card">
86
- <span class="icon">💃</span>
87
- <h3>Expressive motion</h3>
88
- <p>Use subtle head and antenna gestures during welcome and registration interactions.</p>
89
- </div>
90
- <div class="feature-card">
91
- <span class="icon">🧠</span>
92
- <h3>Calendar-aware assistance</h3>
93
- <p>Use appointment context to welcome guests on time and guide follow-up actions.</p>
94
- </div>
95
- <div class="feature-card">
96
- <span class="icon">🌐</span>
97
- <h3>Ready for your setup</h3>
98
- <p>Works in console mode or web mode with dashboard APIs for quick operator visibility.</p>
99
- </div>
100
- </div>
101
- </section>
102
-
103
- <section id="story" class="section story">
104
- <div class="story-grid">
105
- <div class="story-card">
106
- <p class="eyebrow">How it feels</p>
107
- <h3>From greeting to handoff in seconds</h3>
108
- <ul class="story-list">
109
- <li><span>👋</span> Greet visitors naturally with low-latency voice conversation.</li>
110
- <li><span>👀</span> Use camera context to identify known guests or register new ones.</li>
111
- <li><span>📅</span> Check appointment context and respond with relevant timing cues.</li>
112
- <li><span>📨</span> Log handoff actions in the outbox for host follow-up.</li>
113
- </ul>
114
- </div>
115
- <div class="story-card secondary">
116
- <p class="eyebrow">Where it shines</p>
117
- <h3>Great for offices, demos, and guided reception flows</h3>
118
- <p class="story-text">
119
- Show how Reachy Mini can handle repeatable visitor workflows while staying expressive and conversational. It is ideal for front-desk demos, events, and product showcases.
120
- </p>
121
- <div class="chips">
122
- <span class="chip">Guest recognition</span>
123
- <span class="chip">Calendar check</span>
124
- <span class="chip">Outbox logging</span>
125
- <span class="chip">Dashboard APIs</span>
126
- <span class="chip">Realtime conversation</span>
127
- </div>
128
- </div>
129
- </div>
130
- </section>
131
-
132
- <footer class="footer">
133
- <p>
134
- Reachy Mini AI Receptionist by Toon Beerten (<a href="mailto:toon@neontreebot.be">toon@neontreebot.be</a>), based on the Reachy Mini conversation app by <a href="https://github.com/pollen-robotics" target="_blank" rel="noopener">Pollen Robotics</a>.
135
- Explore more apps on <a href="https://huggingface.co/spaces/pollen-robotics/Reachy_Mini_Apps" target="_blank" rel="noopener">Hugging Face Spaces</a>.
136
- </p>
137
- </footer>
138
-
139
- </body>
140
-
141
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
plan.md DELETED
@@ -1,89 +0,0 @@
1
- # Reachy Mini AI Receptionist — Plan
2
-
3
- ## What This App Does
4
-
5
- An interactive AI receptionist that:
6
-
7
- 1. Talks to visitors via OpenAI Realtime API (low-latency speech in/out).
8
- 2. Monitors camera frames for face detection/recognition in a background worker.
9
- 3. Stores guest face crops as PNG files in a persistent `guests/` directory.
10
- 4. Uses a hardcoded POC calendar for appointment context.
11
- 5. Exposes a dashboard with live video, guests, calendar, outbox, and debug logs.
12
-
13
- Start command:
14
-
15
- ```bash
16
- python -m reachy_mini_receptionist.main
17
- ```
18
-
19
- ---
20
-
21
- ## Architecture
22
-
23
- The receptionist extends the conversation base stack and adds receptionist-specific modules:
24
-
25
- ```text
26
- src/reachy_mini_receptionist/
27
- ├── face_db.py # File-based face store (PNG per guest in guests/)
28
- ├── face_recognition_worker.py # Background detection/recognition + event emission
29
- ├── calendar_data.py # Hardcoded appointment data
30
- ├── main.py # App entrypoint + dashboard API mounting
31
- └── tools/
32
- ├── get_today_calendar.py
33
- ├── register_guest.py
34
- ├── send_email.py
35
- └── check_current_face.py # Legacy compatibility path
36
- ```
37
-
38
- ---
39
-
40
- ## Key Design Decisions
41
-
42
- ### 1) Face database is file-based
43
-
44
- - Guests are stored as grayscale PNG crops in `guests/`.
45
- - `FaceDatabase` enforces capacity with FIFO-style eviction when full.
46
- - No SQL database is required.
47
-
48
- ### 2) Face recognition runs in a background worker
49
-
50
- - `FaceRecognitionWorker` runs independently from the realtime audio loop.
51
- - Worker state is consumed by tools and dashboard endpoints.
52
- - Stable face transitions emit context events to the model.
53
-
54
- ### 3) Calendar is static for POC
55
-
56
- - `calendar_data.py` returns hardcoded appointments.
57
- - Easy to swap later for Google/Microsoft calendar integrations.
58
-
59
- ### 4) Dashboard API is mounted in-app
60
-
61
- - `GET /dashboard` serves the receptionist dashboard.
62
- - `GET /video_feed` streams annotated MJPEG.
63
- - `GET /api/guests`, `/api/calendar`, `/api/outbox`, `/api/face_status`, `/api/logs` expose app state.
64
-
65
- ### 5) Profile is intentionally locked
66
-
67
- - `LOCKED_PROFILE` is set to `_reachy_mini_receptionist_locked_profile` in `config.py`.
68
- - Current locked tool allow-list is:
69
- - `get_today_calendar`
70
- - `register_guest`
71
- - `send_email`
72
-
73
- ---
74
-
75
- ## Face Context Event Behavior
76
-
77
- The receptionist flow is push-based:
78
-
79
- - Stable face transitions are emitted by `FaceRecognitionWorker`.
80
- - `OpenaiRealtimeHandler` injects these as context-only user items.
81
- - No automatic `response.create` is triggered by these face context updates.
82
-
83
- ---
84
-
85
- ## Known Notes
86
-
87
- - With `--no-camera`, recognition and registration tools cannot operate.
88
- - Output language behavior is controlled by profile instructions.
89
- - If profile/tool loading fails, the app can fall back to default model behavior; monitor startup logs.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/reachy_mini_receptionist/static/dashboard.html CHANGED
@@ -943,7 +943,7 @@
943
  <body>
944
 
945
  <header>
946
- <img src="/static/methdai-logo.png" alt="MethdAI"
947
  class="brand-logo"
948
  onerror="this.style.display='none'; this.nextElementSibling.style.display='inline';" />
949
  <span class="brand-logo-fallback" style="font-size:1.4rem; display:none;">🤖</span>
 
943
  <body>
944
 
945
  <header>
946
+ <img src="/static/logo.png" alt="MethdAI"
947
  class="brand-logo"
948
  onerror="this.style.display='none'; this.nextElementSibling.style.display='inline';" />
949
  <span class="brand-logo-fallback" style="font-size:1.4rem; display:none;">🤖</span>
src/reachy_mini_receptionist/static/logo.png ADDED

Git LFS Details

  • SHA256: 5401c5baf4e2a503b752dbd7abd69998a6e403852339e2fed727971ce9f24fd5
  • Pointer size: 130 Bytes
  • Size of remote file: 18.3 kB
style.css DELETED
@@ -1,437 +0,0 @@
1
- :root {
2
- --bg: #060c1d;
3
- --panel: #0c172b;
4
- --glass: rgba(17, 27, 48, 0.7);
5
- --card: rgba(255, 255, 255, 0.04);
6
- --accent: #7af5c4;
7
- --accent-2: #f6c452;
8
- --text: #e8edf7;
9
- --muted: #9fb3ce;
10
- --border: rgba(255, 255, 255, 0.08);
11
- --shadow: 0 25px 70px rgba(0, 0, 0, 0.45);
12
- font-family: "Space Grotesk", "Manrope", system-ui, -apple-system, sans-serif;
13
- }
14
-
15
- * {
16
- margin: 0;
17
- padding: 0;
18
- box-sizing: border-box;
19
- }
20
-
21
- body {
22
- background: radial-gradient(circle at 20% 20%, rgba(122, 245, 196, 0.12), transparent 30%),
23
- radial-gradient(circle at 80% 0%, rgba(246, 196, 82, 0.14), transparent 32%),
24
- radial-gradient(circle at 50% 70%, rgba(124, 142, 255, 0.1), transparent 30%),
25
- var(--bg);
26
- color: var(--text);
27
- min-height: 100vh;
28
- line-height: 1.6;
29
- padding-bottom: 3rem;
30
- }
31
-
32
- a {
33
- color: inherit;
34
- text-decoration: none;
35
- }
36
-
37
- .hero {
38
- padding: 3.5rem clamp(1.5rem, 3vw, 3rem) 2.5rem;
39
- position: relative;
40
- overflow: hidden;
41
- }
42
-
43
- .hero::after {
44
- content: "";
45
- position: absolute;
46
- inset: 0;
47
- background: linear-gradient(120deg, rgba(122, 245, 196, 0.12), rgba(246, 196, 82, 0.08), transparent);
48
- pointer-events: none;
49
- }
50
-
51
- .topline {
52
- display: flex;
53
- align-items: center;
54
- justify-content: space-between;
55
- max-width: 1200px;
56
- margin: 0 auto 2rem;
57
- position: relative;
58
- z-index: 2;
59
- }
60
-
61
- .brand {
62
- display: flex;
63
- align-items: center;
64
- gap: 0.5rem;
65
- font-weight: 700;
66
- letter-spacing: 0.5px;
67
- color: var(--text);
68
- }
69
-
70
- .logo {
71
- display: inline-flex;
72
- align-items: center;
73
- justify-content: center;
74
- width: 2.2rem;
75
- height: 2.2rem;
76
- border-radius: 10px;
77
- background: linear-gradient(145deg, rgba(122, 245, 196, 0.15), rgba(124, 142, 255, 0.15));
78
- box-shadow: 0 10px 30px rgba(0, 0, 0, 0.25);
79
- }
80
-
81
- .brand-name {
82
- font-size: 1.1rem;
83
- }
84
-
85
- .pill {
86
- background: rgba(255, 255, 255, 0.06);
87
- border: 1px solid var(--border);
88
- padding: 0.6rem 1rem;
89
- border-radius: 999px;
90
- color: var(--muted);
91
- font-size: 0.9rem;
92
- box-shadow: 0 12px 30px rgba(0, 0, 0, 0.2);
93
- }
94
-
95
- .hero-grid {
96
- display: grid;
97
- grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
98
- gap: clamp(1.5rem, 2.5vw, 2.5rem);
99
- max-width: 1200px;
100
- margin: 0 auto;
101
- position: relative;
102
- z-index: 2;
103
- align-items: center;
104
- }
105
-
106
- .hero-copy h1 {
107
- font-size: clamp(2.6rem, 4vw, 3.6rem);
108
- margin-bottom: 1rem;
109
- line-height: 1.1;
110
- letter-spacing: -0.5px;
111
- }
112
-
113
- .live-demo-badge {
114
- display: inline-flex;
115
- align-items: center;
116
- margin-left: 0.65rem;
117
- padding: 0.2rem 0.55rem;
118
- font-size: 0.34em;
119
- line-height: 1;
120
- letter-spacing: 0.02em;
121
- font-weight: 600;
122
- border-radius: 999px;
123
- border: 1px solid var(--border);
124
- color: var(--muted);
125
- background: rgba(255, 255, 255, 0.04);
126
- vertical-align: middle;
127
- }
128
-
129
- .live-demo-badge:hover {
130
- color: var(--text);
131
- border-color: rgba(255, 255, 255, 0.22);
132
- background: rgba(255, 255, 255, 0.07);
133
- }
134
-
135
- .eyebrow {
136
- display: inline-flex;
137
- align-items: center;
138
- gap: 0.5rem;
139
- text-transform: uppercase;
140
- letter-spacing: 1px;
141
- font-size: 0.8rem;
142
- color: var(--muted);
143
- margin-bottom: 0.75rem;
144
- }
145
-
146
- .eyebrow::before {
147
- content: "";
148
- display: inline-block;
149
- width: 24px;
150
- height: 2px;
151
- background: linear-gradient(90deg, var(--accent), var(--accent-2));
152
- border-radius: 999px;
153
- }
154
-
155
- .lede {
156
- font-size: 1.1rem;
157
- color: var(--muted);
158
- max-width: 620px;
159
- }
160
-
161
- .hero-actions {
162
- display: flex;
163
- gap: 1rem;
164
- align-items: center;
165
- margin: 1.6rem 0 1.2rem;
166
- flex-wrap: wrap;
167
- }
168
-
169
- .btn {
170
- display: inline-flex;
171
- align-items: center;
172
- justify-content: center;
173
- gap: 0.6rem;
174
- padding: 0.85rem 1.4rem;
175
- border-radius: 12px;
176
- font-weight: 700;
177
- border: 1px solid transparent;
178
- cursor: pointer;
179
- transition: transform 0.2s ease, box-shadow 0.2s ease, background 0.2s ease, border-color 0.2s ease;
180
- }
181
-
182
- .btn.primary {
183
- background: linear-gradient(135deg, #7af5c4, #7c8eff);
184
- color: #0a0f1f;
185
- box-shadow: 0 15px 30px rgba(122, 245, 196, 0.25);
186
- }
187
-
188
- .btn.primary:hover {
189
- transform: translateY(-2px);
190
- box-shadow: 0 25px 45px rgba(122, 245, 196, 0.35);
191
- }
192
-
193
- .btn.ghost {
194
- background: rgba(255, 255, 255, 0.05);
195
- border-color: var(--border);
196
- color: var(--text);
197
- }
198
-
199
- .btn.ghost:hover {
200
- border-color: rgba(255, 255, 255, 0.3);
201
- transform: translateY(-2px);
202
- }
203
-
204
- .btn.wide {
205
- width: 100%;
206
- justify-content: center;
207
- }
208
-
209
- .hero-badges {
210
- display: flex;
211
- flex-wrap: wrap;
212
- gap: 0.6rem;
213
- color: var(--muted);
214
- font-size: 0.9rem;
215
- }
216
-
217
- .hero-badges span {
218
- padding: 0.5rem 0.8rem;
219
- border-radius: 10px;
220
- border: 1px solid var(--border);
221
- background: rgba(255, 255, 255, 0.04);
222
- }
223
-
224
- .hero-visual .glass-card {
225
- background: rgba(255, 255, 255, 0.03);
226
- border: 1px solid var(--border);
227
- border-radius: 18px;
228
- padding: 1.2rem;
229
- box-shadow: var(--shadow);
230
- backdrop-filter: blur(10px);
231
- }
232
-
233
- .hero-gif {
234
- width: 100%;
235
- display: block;
236
- border-radius: 14px;
237
- border: 1px solid var(--border);
238
- box-shadow: 0 12px 35px rgba(0, 0, 0, 0.35);
239
- }
240
-
241
- .caption {
242
- margin-top: 0.75rem;
243
- color: var(--muted);
244
- font-size: 0.95rem;
245
- }
246
-
247
- .video-embed-wrapper {
248
- margin-top: 0.9rem;
249
- position: relative;
250
- width: 100%;
251
- aspect-ratio: 16 / 9;
252
- border-radius: 12px;
253
- overflow: hidden;
254
- border: 1px solid var(--border);
255
- background: rgba(0, 0, 0, 0.35);
256
- }
257
-
258
- .video-embed-wrapper iframe {
259
- width: 100%;
260
- height: 100%;
261
- display: block;
262
- border: 0;
263
- }
264
-
265
- .video-link {
266
- margin-top: 0.55rem;
267
- font-size: 0.85rem;
268
- color: var(--muted);
269
- }
270
-
271
- .video-link a {
272
- text-decoration: underline;
273
- text-underline-offset: 2px;
274
- }
275
-
276
- .section {
277
- max-width: 1200px;
278
- margin: 0 auto;
279
- padding: clamp(2rem, 4vw, 3.5rem) clamp(1.5rem, 3vw, 3rem);
280
- }
281
-
282
- .section-header {
283
- text-align: center;
284
- max-width: 780px;
285
- margin: 0 auto 2rem;
286
- }
287
-
288
- .section-header h2 {
289
- font-size: clamp(2rem, 3vw, 2.6rem);
290
- margin-bottom: 0.5rem;
291
- }
292
-
293
- .intro {
294
- color: var(--muted);
295
- font-size: 1.05rem;
296
- }
297
-
298
- .feature-grid {
299
- display: grid;
300
- grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
301
- gap: 1rem;
302
- }
303
-
304
- .feature-card {
305
- background: rgba(255, 255, 255, 0.03);
306
- border: 1px solid var(--border);
307
- border-radius: 16px;
308
- padding: 1.25rem;
309
- box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
310
- transition: transform 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
311
- }
312
-
313
- .feature-card:hover {
314
- transform: translateY(-4px);
315
- border-color: rgba(122, 245, 196, 0.3);
316
- box-shadow: 0 18px 40px rgba(0, 0, 0, 0.3);
317
- }
318
-
319
- .feature-card .icon {
320
- width: 48px;
321
- height: 48px;
322
- border-radius: 12px;
323
- display: grid;
324
- place-items: center;
325
- background: rgba(122, 245, 196, 0.14);
326
- margin-bottom: 0.8rem;
327
- font-size: 1.4rem;
328
- }
329
-
330
- .feature-card h3 {
331
- margin-bottom: 0.35rem;
332
- }
333
-
334
- .feature-card p {
335
- color: var(--muted);
336
- }
337
-
338
- .story {
339
- padding-top: 1rem;
340
- }
341
-
342
- .story-grid {
343
- display: grid;
344
- grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
345
- gap: 1rem;
346
- }
347
-
348
- .story-card {
349
- background: rgba(255, 255, 255, 0.03);
350
- border: 1px solid var(--border);
351
- border-radius: 18px;
352
- padding: 1.5rem;
353
- box-shadow: var(--shadow);
354
- }
355
-
356
- .story-card.secondary {
357
- background: linear-gradient(145deg, rgba(124, 142, 255, 0.08), rgba(122, 245, 196, 0.06));
358
- }
359
-
360
- .story-card h3 {
361
- margin-bottom: 0.8rem;
362
- }
363
-
364
- .story-list {
365
- list-style: none;
366
- display: grid;
367
- gap: 0.7rem;
368
- color: var(--muted);
369
- font-size: 0.98rem;
370
- }
371
-
372
- .story-list li {
373
- display: flex;
374
- gap: 0.7rem;
375
- align-items: flex-start;
376
- }
377
-
378
- .story-text {
379
- color: var(--muted);
380
- line-height: 1.7;
381
- margin-bottom: 1rem;
382
- }
383
-
384
- .chips {
385
- display: flex;
386
- flex-wrap: wrap;
387
- gap: 0.5rem;
388
- }
389
-
390
- .chip {
391
- padding: 0.45rem 0.8rem;
392
- border-radius: 12px;
393
- background: rgba(0, 0, 0, 0.2);
394
- border: 1px solid var(--border);
395
- color: var(--text);
396
- font-size: 0.9rem;
397
- }
398
-
399
- .footer {
400
- text-align: center;
401
- color: var(--muted);
402
- padding: 2rem 1.5rem 0;
403
- }
404
-
405
- .footer a {
406
- color: var(--text);
407
- border-bottom: 1px solid transparent;
408
- }
409
-
410
- .footer a:hover {
411
- border-color: rgba(255, 255, 255, 0.5);
412
- }
413
-
414
- @media (max-width: 768px) {
415
- .hero {
416
- padding-top: 2.5rem;
417
- }
418
-
419
- .topline {
420
- flex-direction: column;
421
- gap: 0.8rem;
422
- align-items: flex-start;
423
- }
424
-
425
- .hero-actions {
426
- width: 100%;
427
- }
428
-
429
- .btn {
430
- width: 100%;
431
- justify-content: center;
432
- }
433
-
434
- .hero-badges {
435
- gap: 0.4rem;
436
- }
437
- }