Spaces:
Running
Running
| @import "tailwindcss"; | |
| @source "../../reachy_web/templates/**/*.html"; | |
| @source "../../reachy_web/static/js/**/*.js"; | |
| /* dark: utilities follow the theme switch (data-theme), not the OS */ | |
| @custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *)); | |
| /* Fonts, self-hosted (no CDN) — same faces as the PiWRT reference. | |
| Each palette picks its own body font below; Space Grotesk also carries the product name. */ | |
| @font-face { font-family: 'Space Grotesk'; src: url('/static/fonts/SpaceGrotesk.ttf') format('truetype'); font-weight: 100 900; font-style: normal; font-display: swap; } | |
| @font-face { font-family: 'Poppins'; src: url('/static/fonts/Poppins-Regular.ttf') format('truetype'); font-weight: 400; font-style: normal; font-display: swap; } | |
| @font-face { font-family: 'Outfit'; src: url('/static/fonts/Outfit.ttf') format('truetype'); font-weight: 100 900; font-style: normal; font-display: swap; } | |
| @font-face { font-family: 'Sora'; src: url('/static/fonts/Sora.ttf') format('truetype'); font-weight: 100 900; font-style: normal; font-display: swap; } | |
| @font-face { font-family: 'Manrope'; src: url('/static/fonts/Manrope.ttf') format('truetype'); font-weight: 100 900; font-style: normal; font-display: swap; } | |
| @font-face { font-family: 'Share Tech Mono'; src: url('/static/fonts/ShareTechMono-Regular.ttf') format('truetype'); font-weight: 400; font-style: normal; font-display: swap; } | |
| /* Hugging Face's own body typeface (their site loads Source Sans Pro); shipped as | |
| the Source Sans 3 variable TTF, its successor of the same design. */ | |
| @font-face { font-family: 'Source Sans Pro'; src: url('/static/fonts/SourceSans3.ttf') format('truetype'); font-weight: 200 900; font-style: normal; font-display: swap; } | |
| /* ================= PiWRT design tokens ================= */ | |
| /* dark is the default; [data-theme=light] flips it; data-palette recolors both */ | |
| :root { color-scheme: dark; accent-color: var(--acc); --bg: #0b0e14; --card: #151923; --soft: #0f1320; --line: #1e2533; --txt: #e8eaed; --mut: #8a909a; --acc: #3b82f6; --ok: #27c281; --warn: #e0a52b; --bad: #c23a3f; } | |
| [data-theme=light] { color-scheme: light; --bg: #eef1f7; --card: #fff; --soft: #f1f4f9; --line: #d9dfea; --txt: #1b2330; --mut: #5b6675; --acc: #2563eb; --ok: #16a34a; --warn: #b45309; --bad: #dc2626; } | |
| :root[data-palette=ocean] { --bg: #0a141c; --card: #11202e; --soft: #1a3a48; --line: #1f3245; --txt: #e1ecf4; --mut: #85a1b5; --acc: #22d3ee; } | |
| :root[data-palette=ocean][data-theme=light] { --bg: #f0f7fa; --card: #fff; --soft: #cfeaf1; --line: #dde9f0; --txt: #0f1b26; --mut: #5a7387; --acc: #0891b2; } | |
| :root[data-palette=violet] { --bg: #15101f; --card: #1f1a2e; --soft: #2a2342; --line: #2e2942; --txt: #f1edf8; --mut: #a19bb5; --acc: #a78bfa; } | |
| :root[data-palette=violet][data-theme=light] { --bg: #f7f4fb; --card: #fff; --soft: #efe9fa; --line: #e6e0ef; --txt: #1f1b2c; --mut: #6b6480; --acc: #7c3aed; } | |
| :root[data-palette=sunset] { --bg: #1c100a; --card: #2a180f; --soft: #3d2412; --line: #3d2415; --txt: #f5e6d8; --mut: #b59880; --acc: #fb923c; } | |
| :root[data-palette=sunset][data-theme=light] { --bg: #fff7f0; --card: #fff; --soft: #fce7d6; --line: #f5e6d8; --txt: #2c1810; --mut: #7a5a45; --acc: #ea580c; } | |
| :root[data-palette=forest] { --bg: #0e140f; --card: #172218; --soft: #1a3422; --line: #1f2a1f; --txt: #e8f0e5; --mut: #8fa088; --acc: #4ade80; } | |
| :root[data-palette=forest][data-theme=light] { --bg: #f4f8f1; --card: #fff; --soft: #dcefde; --line: #dde5d6; --txt: #1a2614; --mut: #5f6e55; --acc: #16a34a; } | |
| :root[data-palette=rose] { --bg: #1a0b12; --card: #2a1320; --soft: #3d1a2d; --line: #3d1a2a; --txt: #fdf2f8; --mut: #c49aab; --acc: #fb7185; } | |
| :root[data-palette=rose][data-theme=light] { --bg: #fdf2f8; --card: #fff; --soft: #fbe0ea; --line: #f4d9e2; --txt: #1f0e15; --mut: #7a4757; --acc: #e11d48; } | |
| :root[data-palette=mint] { --bg: #06181a; --card: #0e2426; --soft: #133a35; --line: #1b3638; --txt: #e0f5ee; --mut: #7fa89a; --acc: #5eead4; } | |
| :root[data-palette=mint][data-theme=light] { --bg: #f0fbf6; --card: #fff; --soft: #ccf1e8; --line: #d5ede0; --txt: #0a2e20; --mut: #5c7c6e; --acc: #14b8a6; } | |
| :root[data-palette=amber] { --bg: #101114; --card: #1a1c20; --soft: #26292f; --line: #2e323a; --txt: #eceef2; --mut: #9aa0aa; --acc: #e9a23b; } | |
| :root[data-palette=amber][data-theme=light] { --bg: #fafafb; --card: #fff; --soft: #f1f2f4; --line: #e2e5ea; --txt: #17191d; --mut: #5f6672; --acc: #b9761a; } | |
| :root[data-palette=slate] { --bg: #0f172a; --card: #1e293b; --soft: #1e2b40; --line: #334155; --txt: #f1f5f9; --mut: #94a3b8; --acc: #94a3b8; } | |
| :root[data-palette=slate][data-theme=light] { --bg: #f8fafc; --card: #fff; --soft: #e2e8f0; --line: #e2e8f0; --txt: #0f172a; --mut: #64748b; --acc: #475569; } | |
| :root[data-palette=nordic] { --bg: #2e3440; --card: #3b4252; --soft: #3f4f65; --line: #4c566a; --txt: #eceff4; --mut: #a8b0bf; --acc: #88c0d0; } | |
| :root[data-palette=nordic][data-theme=light] { --bg: #eceff4; --card: #fff; --soft: #e5ecf3; --line: #d8dee9; --txt: #2e3440; --mut: #4c566a; --acc: #5e81ac; } | |
| :root[data-palette=one-dark] { --bg: #0e1015; --card: #1b1d22; --soft: #21304a; --line: #2a2d34; --txt: #abb2bf; --mut: #5c6370; --acc: #61afef; } | |
| :root[data-palette=one-dark][data-theme=light] { --bg: #fafafa; --card: #fff; --soft: #dbe7fb; --line: #e5e5e6; --txt: #383a42; --mut: #6b6f76; --acc: #4078f2; } | |
| :root[data-palette=midnight] { --bg: #000; --card: #0a0a0a; --soft: #1a1b3a; --line: #1a1a1a; --txt: #fafafa; --mut: #888; --acc: #818cf8; } | |
| :root[data-palette=midnight][data-theme=light] { --bg: #fff; --card: #f5f5f5; --soft: #e0e1fb; --line: #e5e5e5; --txt: #000; --mut: #666; --acc: #4f46e5; } | |
| :root[data-palette=matrix] { --bg: #000; --card: #0a140a; --soft: #0e1f0e; --line: #163a16; --txt: #4dff77; --mut: #2e9a4f; --acc: #00ff41; } | |
| :root[data-palette=matrix][data-theme=light] { --bg: #eefbf1; --card: #fff; --soft: #d6f7dd; --line: #c3eece; --txt: #08350f; --mut: #4a7d57; --acc: #0ba21e; } | |
| /* Hugging Face: their Ebony dark + Candlelight yellow (#FFD21E) accent; light is a | |
| warm cream with a readable gold accent (pure yellow vanishes on white). */ | |
| :root[data-palette=huggingface] { --bg: #0b0f1a; --card: #161c2b; --soft: #202a40; --line: #26314a; --txt: #f5f6f9; --mut: #8c96ab; --acc: #ffd21e; } | |
| :root[data-palette=huggingface][data-theme=light] { --bg: #fffdf4; --card: #fff; --soft: #fef3cf; --line: #efe4c0; --txt: #17202e; --mut: #5f6a7c; --acc: #d99700; } | |
| /* Each palette brings its own typeface and halo (PiWRT parity) */ | |
| :root[data-palette=ocean] body { background: radial-gradient(1200px 600px at 50% -10%, #11202e, var(--bg)) fixed; font-family: 'Poppins', system-ui, sans-serif; } | |
| :root[data-palette=violet] body { background: radial-gradient(1200px 600px at 50% -10%, #1f1a2e, var(--bg)) fixed; font-family: 'Space Grotesk', system-ui, sans-serif; } | |
| :root[data-palette=sunset] body { background: radial-gradient(1200px 600px at 50% -10%, #2a180f, var(--bg)) fixed; font-family: 'Outfit', system-ui, sans-serif; } | |
| :root[data-palette=forest] body { background: radial-gradient(1200px 600px at 50% -10%, #172218, var(--bg)) fixed; font-family: 'Poppins', system-ui, sans-serif; } | |
| :root[data-palette=rose] body { background: radial-gradient(1200px 600px at 50% -10%, #2a1320, var(--bg)) fixed; font-family: 'Manrope', system-ui, sans-serif; } | |
| :root[data-palette=mint] body { background: radial-gradient(1200px 600px at 50% -10%, #0e2426, var(--bg)) fixed; font-family: 'Poppins', system-ui, sans-serif; } | |
| :root[data-palette=amber] body { background: radial-gradient(1200px 600px at 50% -10%, #1a1c20, var(--bg)) fixed; font-family: 'Sora', system-ui, sans-serif; } | |
| :root[data-palette=slate] body { background: radial-gradient(1200px 600px at 50% -10%, #1e293b, var(--bg)) fixed; font-family: 'Poppins', system-ui, sans-serif; } | |
| :root[data-palette=nordic] body { background: radial-gradient(1200px 600px at 50% -10%, #3b4252, var(--bg)) fixed; font-family: 'Poppins', system-ui, sans-serif; } | |
| :root[data-palette=one-dark] body { background: radial-gradient(1200px 600px at 50% -10%, #1b1d22, var(--bg)) fixed; font-family: 'Share Tech Mono', ui-monospace, monospace; } | |
| :root[data-palette=midnight] body { background: radial-gradient(1200px 600px at 50% -10%, #0a0a0a, var(--bg)) fixed; font-family: 'Poppins', system-ui, sans-serif; } | |
| :root[data-palette=matrix] body { background: radial-gradient(1200px 600px at 50% -10%, #0a140a, var(--bg)) fixed; font-family: 'Share Tech Mono', ui-monospace, monospace; letter-spacing: 0.3px; text-shadow: 0 0 1px #00ff4140; } | |
| :root[data-palette=huggingface] body { background: radial-gradient(1200px 600px at 50% -10%, #161c2b, var(--bg)) fixed; font-family: 'Source Sans Pro', system-ui, sans-serif; } | |
| /* HF's accent is bright yellow/gold: dark text reads on it, white does not. Flip | |
| the accent-filled controls that otherwise hardcode white text. */ | |
| :root[data-palette=huggingface] .tab.on, | |
| :root[data-palette=huggingface] .navmenu-list button.on, | |
| :root[data-palette=huggingface] .cam-seg button.on { color: #0b0f1a; } | |
| /* in light mode every palette fades from white (the font stays the palette's) */ | |
| :root[data-palette][data-theme=light] body { background: radial-gradient(1200px 600px at 50% -10%, #fff, var(--bg)) fixed; } | |
| /* `.hidden` must beat everything it is combined with: Tailwind's own display | |
| utilities emitted after it (`inline-flex` on the empty:hidden pills), AND the | |
| hand-written unlayered classes toggled at runtime (`.skin-list`, catalogue | |
| panes...) which outrank any layered utility. Hence the !important sledgehammer. | |
| Its one victim was the responsive show pattern (`hidden sm:flex` on the store | |
| author/stars columns stayed hidden at every width), so the two responsive | |
| display utilities used with `hidden` are re-asserted just below, later in the | |
| source and equally !important, so above 640px they win back. */ | |
| .hidden { display: none ; } | |
| @media (min-width: 40rem) { | |
| .sm\:flex.hidden { display: flex ; } | |
| .sm\:grid.hidden { display: grid ; } | |
| } | |
| /* ================= utilities → tokens ================= */ | |
| /* The recovered UI is written in white/gray/blue/green utilities. Remapping the | |
| scales onto the tokens makes every existing class — templates AND markup | |
| built in JS — follow the theme and the palette without a rewrite. | |
| gray-800/900 are NOT remapped: they are used as literal darks over video. */ | |
| @theme inline { | |
| --color-bg: var(--bg); | |
| --color-card: var(--card); | |
| --color-soft: var(--soft); | |
| --color-line: var(--line); | |
| --color-txt: var(--txt); | |
| --color-mut: var(--mut); | |
| --color-acc: var(--acc); | |
| --color-ok: var(--ok); | |
| --color-warn: var(--warn); | |
| --color-bad: var(--bad); | |
| --color-gray-50: var(--bg); | |
| --color-gray-100: var(--soft); | |
| --color-gray-200: var(--soft); | |
| --color-gray-300: var(--line); | |
| --color-gray-400: color-mix(in srgb, var(--mut) 60%, var(--line)); | |
| --color-gray-500: var(--mut); | |
| --color-gray-600: color-mix(in srgb, var(--mut) 55%, var(--txt)); | |
| --color-gray-700: color-mix(in srgb, var(--mut) 30%, var(--txt)); | |
| --color-gray-900: var(--txt); | |
| --color-blue-300: color-mix(in srgb, var(--acc) 55%, white); | |
| --color-blue-500: var(--acc); | |
| --color-blue-600: var(--acc); | |
| --color-blue-700: color-mix(in srgb, var(--acc) 85%, black); | |
| --color-blue-800: color-mix(in srgb, var(--acc) 70%, black); | |
| --color-green-400: var(--ok); | |
| --color-green-500: var(--ok); | |
| --color-green-600: color-mix(in srgb, var(--ok) 85%, black); | |
| --color-green-700: color-mix(in srgb, var(--ok) 70%, black); | |
| --color-red-400: color-mix(in srgb, var(--bad) 80%, white); | |
| --color-red-500: var(--bad); | |
| --color-red-600: var(--bad); | |
| --color-red-700: color-mix(in srgb, var(--bad) 85%, black); | |
| } | |
| /* ================= base ================= */ | |
| html { | |
| -webkit-font-smoothing: antialiased; | |
| -moz-osx-font-smoothing: grayscale; | |
| } | |
| body { | |
| font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; | |
| color: var(--txt); | |
| background: radial-gradient(1200px 600px at 50% -10%, var(--soft), var(--bg)) fixed; | |
| transition: color 0.25s; | |
| } | |
| /* ================= PiWRT components ================= */ | |
| .topbar { | |
| position: sticky; | |
| top: 0; | |
| z-index: 30; | |
| backdrop-filter: blur(14px); | |
| background: color-mix(in srgb, var(--bg) 78%, transparent); | |
| border-bottom: 1px solid var(--line); | |
| } | |
| .topbar-inner { | |
| /* width/centering comes from the same container utilities as the page | |
| content (base.html), so the bar always aligns with the layout */ | |
| padding-top: 8px; | |
| padding-bottom: 8px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| gap: 12px; | |
| flex-wrap: wrap; | |
| } | |
| .topbar-brand { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| font-weight: 600; | |
| font-size: 1.05rem; | |
| } | |
| .topbar-brand img { | |
| width: 30px; | |
| height: 30px; | |
| border-radius: 9px; | |
| } | |
| .topbar-controls { | |
| display: flex; | |
| align-items: center; | |
| gap: 14px; | |
| flex-wrap: wrap; | |
| } | |
| /* the 13 palettes on two balanced rows (7 + 6) rather than one long wrapping | |
| line — a grid, so the dots line up in columns instead of drifting */ | |
| .palettes { | |
| display: grid; | |
| grid-template-columns: repeat(7, auto); | |
| justify-items: center; | |
| align-items: center; | |
| gap: 6px; | |
| } | |
| .pdot { | |
| width: 16px; | |
| height: 16px; | |
| border-radius: 50%; | |
| border: 2px solid transparent; | |
| cursor: pointer; | |
| padding: 0; | |
| transition: transform 0.15s, border-color 0.15s; | |
| } | |
| .pdot:hover { transform: scale(1.2); } | |
| .pdot.on { border-color: var(--txt); transform: scale(1.15); } | |
| .theme-ctl { display: flex; align-items: center; gap: 6px; color: var(--mut); font-size: 0.9rem; } | |
| .ios { | |
| appearance: none; | |
| border: none; | |
| position: relative; | |
| width: 38px; | |
| height: 22px; | |
| border-radius: 999px; | |
| background: var(--line); | |
| cursor: pointer; | |
| flex: none; | |
| padding: 0; | |
| transition: background-color 0.18s; | |
| } | |
| .ios:after { | |
| content: ""; | |
| position: absolute; | |
| top: 2px; | |
| left: 2px; | |
| width: 18px; | |
| height: 18px; | |
| border-radius: 50%; | |
| background: #fff; | |
| box-shadow: 0 1px 3px #00000059; | |
| transition: transform 0.18s; | |
| } | |
| .ios.on { background: var(--acc); } | |
| .ios.on:after { transform: translateX(16px); } | |
| .seg { | |
| display: inline-flex; | |
| gap: 4px; | |
| background: var(--soft); | |
| border: 1px solid var(--line); | |
| border-radius: 12px; | |
| padding: 4px; | |
| } | |
| .seg button { | |
| border: none; | |
| cursor: pointer; | |
| border-radius: 9px; | |
| background: transparent; | |
| color: var(--mut); | |
| font-family: inherit; | |
| font-weight: 600; | |
| font-size: 0.85rem; | |
| padding: 7px 14px; | |
| transition: background-color 0.18s, color 0.18s; | |
| } | |
| .seg button:hover:not(.on) { background: color-mix(in srgb, var(--acc) 14%, var(--card)); color: var(--txt); } | |
| .seg button.on { background: var(--acc); color: var(--bg); } | |
| /* the Camera / Simulator switch floats INSIDE the viewport, top-right, icons only. | |
| A translucent dark chip so the icons read over both the video and the 3D scene, | |
| whatever the theme; the active mode fills with the accent. */ | |
| .cam-seg { | |
| position: absolute; | |
| top: 10px; | |
| right: 10px; | |
| z-index: 10; | |
| background: rgba(0, 0, 0, 0.5); | |
| backdrop-filter: blur(6px); | |
| -webkit-backdrop-filter: blur(6px); | |
| border: 1px solid rgba(255, 255, 255, 0.14); | |
| } | |
| .cam-seg button { color: #e8eaed; } | |
| .cam-seg button:hover:not(.on) { background: rgba(255, 255, 255, 0.14); color: #fff; } | |
| .cam-seg button.on { background: var(--acc); color: #fff; } | |
| .cam-seg .ic { font-size: 1.05rem; } | |
| /* compact variant: a switch that only picks a list has no business being as tall | |
| as a real control (the Dances / Emotions one, under its own header) */ | |
| .seg.sm { padding: 3px; border-radius: 10px; } | |
| .seg.sm button { padding: 3px 10px; border-radius: 7px; font-size: 0.8rem; } | |
| /* ================= Hugging Face auth (amber = HF brand, deliberate) ================= */ | |
| #hf-login-modal { | |
| opacity: 0; | |
| visibility: hidden; | |
| transition: opacity 0.25s ease, visibility 0.25s ease; | |
| } | |
| #hf-login-modal.visible { | |
| opacity: 1; | |
| visibility: visible; | |
| } | |
| #hf-login-modal .modal-content { | |
| transform: scale(0.95) translateY(-10px); | |
| transition: transform 0.25s ease; | |
| } | |
| #hf-login-modal.visible .modal-content { | |
| transform: scale(1) translateY(0); | |
| } | |
| #hf-modal-token-input { | |
| transition: border-color 0.2s ease, box-shadow 0.2s ease; | |
| } | |
| #hf-modal-oauth-btn, | |
| #hf-modal-token-btn { | |
| transition: all 0.2s ease; | |
| } | |
| #hf-modal-oauth-btn:hover { | |
| transform: translateY(-1px); | |
| } | |
| #hf-modal-oauth-btn:active, | |
| #hf-modal-token-btn:active { | |
| transform: translateY(0); | |
| } | |
| .hf-modal-head { | |
| background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%); | |
| border-bottom: 2px solid #fcd34d; | |
| border-radius: 18px 18px 0 0; | |
| } | |
| .hf-btn-amber { | |
| background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%); | |
| } | |
| /* header Login/Logout button — .logout is toggled by hf_auth.js */ | |
| .hf-auth-btn { | |
| background-color: #fef3c7; | |
| color: #92400e; | |
| border: 1px solid #fcd34d; | |
| } | |
| .hf-auth-btn:hover { background-color: #fde68a; } | |
| .hf-auth-btn.logout { | |
| background-color: color-mix(in srgb, var(--bad) 12%, var(--card)); | |
| color: var(--bad); | |
| border-color: color-mix(in srgb, var(--bad) 35%, var(--line)); | |
| } | |
| .hf-auth-btn.logout:hover { background-color: color-mix(in srgb, var(--bad) 22%, var(--card)); } | |
| /* relay status chip — state class (ok/warn/err) set by hf_auth.js */ | |
| .hf-relay { background-color: var(--soft); border: 1px solid var(--line); } | |
| .hf-relay .dot { background-color: var(--mut); } | |
| .hf-relay .lbl { color: var(--mut); } | |
| .hf-relay.ok { background-color: color-mix(in srgb, var(--ok) 14%, var(--card)); border-color: color-mix(in srgb, var(--ok) 40%, var(--line)); } | |
| .hf-relay.ok .dot { background-color: var(--ok); } | |
| .hf-relay.ok .lbl { color: var(--ok); } | |
| .hf-relay.warn { background-color: color-mix(in srgb, var(--warn) 14%, var(--card)); border-color: color-mix(in srgb, var(--warn) 40%, var(--line)); } | |
| .hf-relay.warn .dot { background-color: var(--warn); } | |
| .hf-relay.warn .lbl { color: var(--warn); } | |
| .hf-relay.err { background-color: color-mix(in srgb, var(--bad) 14%, var(--card)); border-color: color-mix(in srgb, var(--bad) 40%, var(--line)); } | |
| .hf-relay.err .dot { background-color: var(--bad); } | |
| .hf-relay.err .lbl { color: var(--bad); } | |
| .hf-badge-user { | |
| background-color: color-mix(in srgb, var(--ok) 14%, var(--card)); | |
| border: 1px solid color-mix(in srgb, var(--ok) 40%, var(--line)); | |
| } | |
| /* ================= legacy component classes, tokenized ================= */ | |
| /* APPs styles */ | |
| .app-section { | |
| border-radius: 16px; | |
| border-width: 1px; | |
| border-color: var(--line); | |
| padding: 16px; | |
| gap: 16px; | |
| background: var(--card); | |
| box-shadow: 0 8px 24px #00000040; | |
| } | |
| [data-theme=light] .app-section { box-shadow: 0 6px 18px #141e3c14; } | |
| .installed-app-title { | |
| font-weight: 600; | |
| } | |
| .update-badge { | |
| display: inline-block; | |
| background-color: var(--acc); | |
| color: var(--bg); | |
| font-size: 0.65rem; | |
| font-weight: 600; | |
| padding: 0.125rem 0.375rem; | |
| border-radius: 0.25rem; | |
| margin-left: 0.5rem; | |
| vertical-align: middle; | |
| } | |
| /* Hugging Face App Store Styles */ | |
| .app-section-title { | |
| font-weight: 500; | |
| font-size: 24px; | |
| letter-spacing: 0px; | |
| } | |
| .app-list-item { | |
| padding: 4px; | |
| } | |
| .hf-app-icon { | |
| width: 42px; | |
| height: 42px; | |
| border-radius: 10px; | |
| text-align: center; | |
| font-size: 31px; | |
| background-color: var(--soft); | |
| } | |
| .hf-app-title { | |
| font-weight: 500; | |
| font-size: 18px; | |
| } | |
| .hf-app-description { | |
| font-weight: 400; | |
| font-size: 14px; | |
| line-height: 1.4; | |
| color: var(--mut); | |
| } | |
| .wifi-status { | |
| font-weight: 500; | |
| color: var(--txt); | |
| } | |
| .wifi-status-info { | |
| font-weight: 400; | |
| color: var(--mut); | |
| } | |
| .notification-modal { | |
| position: fixed; | |
| bottom: 24px; | |
| right: 24px; | |
| z-index: 9999; | |
| min-width: 300px; | |
| max-width: 400px; | |
| background: var(--card); | |
| box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2); | |
| border-radius: 8px; | |
| display: none; | |
| animation: fadeInUp 0.3s; | |
| } | |
| @keyframes fadeInUp { | |
| from { | |
| opacity: 0; | |
| transform: translateY(40px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| } | |
| .notification-content { | |
| padding: 20px 28px; | |
| border-radius: 8px; | |
| box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15); | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| } | |
| /* Notification variants */ | |
| .notification-content.info { | |
| background: var(--card); | |
| border: 1px solid var(--line); | |
| } | |
| .notification-content.error { | |
| background: color-mix(in srgb, var(--bad) 12%, var(--card)); | |
| border: 1px solid var(--bad); | |
| color: color-mix(in srgb, var(--bad) 70%, var(--txt)); | |
| } | |
| .close-btn { | |
| background: none; | |
| border: none; | |
| font-size: 1.5em; | |
| cursor: pointer; | |
| color: var(--mut); | |
| } | |
| /* ================= topbar tools (volume / mic) ================= */ | |
| .topbar-tool { | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| color: var(--mut); | |
| } | |
| /* sound sliders (mic + speaker knobs, waveforms) are styled together lower down, | |
| next to .topbar-sound */ | |
| .topbar-tool-val { | |
| font-size: 0.75rem; | |
| font-weight: 600; | |
| /* FIXED width, tabular digits: `--`, `5%` and `100%` all take the same room, | |
| so dragging (which swaps the text) can never reflow and shrink the waveform | |
| beside it. Was min-width only, which let a wider number nudge the layout. */ | |
| width: 3em; | |
| min-width: 3em; | |
| font-variant-numeric: tabular-nums; | |
| text-align: right; | |
| font-variant-numeric: tabular-nums; | |
| } | |
| /* NO ROBOT, NO PROMISES. | |
| Anything that can only act on a real robot — the speaker and mic sliders, the | |
| camera, recording — is hidden while none is connected. What still works stays: | |
| the 3D twin, the skins, and the moves, which play on the simulator. */ | |
| [data-robot="absent"] .robot-only { display: none ; } | |
| /* Skin picker: eight looks on ONE scrollable row, each a real 3D render of the | |
| robot wearing it (skin_thumbs.js). No families, no filters, no search. */ | |
| .skin-list { | |
| display: flex; | |
| flex-wrap: nowrap; | |
| gap: 12px; | |
| overflow-x: auto; | |
| overflow-y: hidden; | |
| padding-bottom: 8px; | |
| /* without this the row's intrinsic width wins and the whole page grows | |
| sideways instead of the strip scrolling */ | |
| min-width: 0; | |
| max-width: 100%; | |
| scrollbar-width: thin; | |
| scrollbar-color: var(--line) transparent; | |
| overscroll-behavior-x: contain; /* don't drag the page back a step */ | |
| } | |
| .skin-list::-webkit-scrollbar { height: 8px; } | |
| .skin-list::-webkit-scrollbar-track { background: transparent; } | |
| .skin-list::-webkit-scrollbar-thumb { background: var(--line); border-radius: 999px; } | |
| .skin-list::-webkit-scrollbar-thumb:hover { background: var(--mut); } | |
| .skin-chip { | |
| flex: 0 0 auto; /* never squeezed — it scrolls out of view */ | |
| width: 104px; | |
| padding: 0; | |
| overflow: hidden; | |
| border-radius: 12px; | |
| border: 1px solid var(--line); | |
| background: var(--soft); | |
| transition: border-color 0.15s, box-shadow 0.15s; | |
| cursor: pointer; | |
| } | |
| .skin-thumb { | |
| display: block; | |
| width: 100%; | |
| aspect-ratio: 1 / 1; /* a square texture swatch */ | |
| background: var(--soft); | |
| } | |
| .skin-img { | |
| display: block; | |
| width: 100%; | |
| height: 100%; | |
| object-fit: cover; | |
| image-rendering: auto; | |
| } | |
| .skin-name { | |
| display: block; | |
| padding: 5px 6px; | |
| font-size: 0.72rem; | |
| font-weight: 500; | |
| line-height: 1.2; | |
| color: var(--mut); | |
| white-space: nowrap; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| text-align: center; | |
| } | |
| .skin-chip:hover { border-color: var(--acc); } | |
| .skin-chip.on { border-color: var(--acc); box-shadow: 0 0 0 2px var(--acc); } | |
| .skin-chip.on .skin-name { background: var(--acc); color: var(--bg); font-weight: 600; } | |
| /* The 3D twin renders on a transparent canvas, so the viewport must stop being a | |
| black box: the robot then stands on the card itself. An #id + class beats the | |
| bg-black utility on its own, so no !important is needed. The camera keeps its | |
| black letterbox — a video does need one. */ | |
| #cam-wrap.immersive { background: transparent; } | |
| /* the status text was white because the box was black; with no box it has to | |
| follow the theme, or it vanishes on a light one */ | |
| #cam-wrap.immersive #cam-msg { color: var(--txt); } | |
| /* the twin's overlay is not a video player: drop the dark gradient, and theme | |
| the lone fullscreen button so it does not vanish on a light background */ | |
| #cam-wrap.immersive #cam-bar { background: none; } | |
| #cam-wrap.immersive #cam-bar button { color: var(--txt); } | |
| /* Moves: ONE line that scrolls sideways, like the skin strip. min-width: 0 is not | |
| optional — a nowrap row's intrinsic width otherwise stretches the whole grid | |
| instead of scrolling (the bug the skin strip already taught us). */ | |
| .move-row { | |
| display: flex; | |
| flex-wrap: nowrap; | |
| gap: 12px; | |
| padding: 8px 0; | |
| overflow-x: auto; | |
| overflow-y: hidden; | |
| min-width: 0; | |
| max-width: 100%; | |
| scrollbar-width: thin; | |
| scrollbar-color: var(--line) transparent; | |
| overscroll-behavior-x: contain; | |
| } | |
| .move-row::-webkit-scrollbar { height: 8px; } | |
| .move-row::-webkit-scrollbar-track { background: transparent; } | |
| .move-row::-webkit-scrollbar-thumb { background: var(--line); border-radius: 999px; } | |
| .move-row::-webkit-scrollbar-thumb:hover { background: var(--mut); } | |
| .move-row .move-card { | |
| flex: 0 0 auto; /* a card is never squeezed; it scrolls out of view */ | |
| width: 168px; | |
| } | |
| /* A spinner while a thumbnail loads (fetch + first bake). It only exists on a | |
| card marked .thumb-loading — set when a card scrolls into view unbaked, cleared | |
| the moment its poster is drawn — so the ~70 off-screen cards never spin. */ | |
| .thumb-spin { display: none; } | |
| .move-card.thumb-loading .thumb-spin { | |
| display: block; | |
| position: absolute; | |
| top: 38%; | |
| left: 50%; | |
| width: 22px; | |
| height: 22px; | |
| margin: -11px 0 0 -11px; | |
| border: 2.5px solid color-mix(in srgb, var(--mut) 30%, transparent); | |
| border-top-color: var(--acc); | |
| border-radius: 50%; | |
| pointer-events: none; | |
| animation: thumb-spin 0.7s linear infinite; | |
| } | |
| @keyframes thumb-spin { to { transform: rotate(360deg); } } | |
| /* Move cards: the 3D thumbnail sits on the card, the label below it. */ | |
| .move-card.on { border-color: var(--acc); box-shadow: 0 0 0 2px var(--acc); } | |
| .move-card.on > span { background: var(--acc); color: var(--bg); } | |
| .move-card:hover .move-thumb { transform: scale(1.04); } | |
| .move-thumb { transition: transform 150ms ease; } | |
| .topbar-daemon { display: flex; align-items: center; gap: 6px; color: var(--mut); } | |
| /* `hidden` alone would lose: Tailwind's utility is declared far above this rule, | |
| and at equal specificity the last one wins. Say it here, where we win. */ | |
| .topbar-daemon.hidden { display: none; } | |
| /* bare icons, no button chrome — only the color reacts */ | |
| .topbar-iconbtn { | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| width: 28px; | |
| height: 28px; | |
| border: none; | |
| background: none; | |
| padding: 0; | |
| color: var(--mut); | |
| cursor: pointer; | |
| font-size: 1.15rem; | |
| transition: color 0.15s; | |
| } | |
| .topbar-iconbtn:hover { color: var(--acc); } | |
| /* .hidden (display:none) and this rule have equal specificity, so whichever is | |
| emitted last wins — and .topbar-iconbtn came last, keeping a hidden button | |
| visible. Make the hidden state explicit and more specific so it always wins. */ | |
| .topbar-iconbtn.hidden { display: none; } | |
| /* destructive actions turn red on hover */ | |
| .topbar-iconbtn.danger:hover { color: var(--bad); } | |
| /* ================= topbar v2: logo spanning 3 rows + burger mobile ================= */ | |
| /* logo and product name: clickable (home), never selectable */ | |
| .topbar-home { | |
| user-select: none; | |
| -webkit-user-select: none; | |
| -webkit-tap-highlight-color: transparent; | |
| text-decoration: none; | |
| color: inherit; | |
| cursor: pointer; | |
| flex: 0 0 auto; | |
| display: inline-flex; | |
| align-items: center; | |
| } | |
| .topbar-home:hover { color: var(--acc); } | |
| .topbar-logo:hover { opacity: 0.85; } | |
| .topbar-logo { | |
| width: 108px; | |
| height: 108px; | |
| border-radius: 0.75rem; /* rounded-xl */ | |
| flex: 0 0 auto; | |
| } | |
| .topbar-rows { | |
| flex: 1; | |
| min-width: 0; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 14px; | |
| } | |
| .topbar-row { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| flex-wrap: wrap; | |
| min-width: 0; | |
| } | |
| .topbar-spring { flex: 1; } | |
| /* row 1: three zones — left identity, centered HF account, actions far right */ | |
| .topbar-row.row-main { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| } | |
| .row-zone { display: flex; align-items: center; gap: 12px; min-width: 0; } | |
| .row-zone-center { justify-content: center; } | |
| .row-zone-right { justify-content: flex-end; } | |
| /* rows 2 and 3 (sound, network info) hug the right edge, under the actions */ | |
| .topbar-row.row-right { justify-content: flex-end; } | |
| /* row 2: account on the left, sound tools pushed right */ | |
| .topbar-row.row-split { justify-content: flex-end; } | |
| .topbar-row.row-split .row-zone { margin-right: auto; } | |
| /* HF account: plain text, not a button */ | |
| .topbar-user { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 6px; | |
| font-size: 0.9rem; | |
| font-weight: 600; | |
| color: var(--mut); | |
| cursor: pointer; | |
| white-space: nowrap; | |
| } | |
| .topbar-user.logged { color: var(--txt); } | |
| /* HF avatar replaces the account icon once logged in — topbar and store alike */ | |
| .topbar-user img, | |
| .hf-avatar { | |
| width: 22px; | |
| height: 22px; | |
| border-radius: 50%; | |
| object-fit: cover; | |
| border: 1px solid var(--line); | |
| } | |
| .topbar-user:hover { color: var(--acc); } | |
| /* One robot state line, last row, left: loader-or-dot · daemon word · soft message */ | |
| .topbar-state { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 8px; | |
| font-size: 0.85rem; | |
| font-weight: 600; | |
| color: var(--txt); | |
| white-space: nowrap; | |
| } | |
| .topbar-state .dot { flex: none; } | |
| /* the status dot on the last topbar line: green Ready, orange in flight, red gone */ | |
| .rs-dot { flex: none; width: 9px; height: 9px; border-radius: 50%; } | |
| .rs-dot.ok { background: var(--ok); box-shadow: 0 0 6px color-mix(in srgb, var(--ok) 70%, transparent); } | |
| .rs-dot.warn { background: var(--warn); box-shadow: 0 0 6px color-mix(in srgb, var(--warn) 70%, transparent); } | |
| .rs-dot.bad { background: var(--bad); box-shadow: 0 0 6px color-mix(in srgb, var(--bad) 70%, transparent); } | |
| .rs-dot.off { background: var(--mut); } | |
| /* No clipping here. This line is the only place the dashboard has to say what is | |
| happening — "Waiting to reconnect…" cut down to "Waiting to reconnec…" is a | |
| message that failed at its one job. It is short, it is nowrap, it gets its | |
| full width. */ | |
| .topbar-state .lbl { flex: none; } | |
| /* the soft message trails the state, quieter, behind a separator */ | |
| .topbar-state .soft { | |
| flex: none; | |
| font-size: 0.82rem; | |
| font-weight: 500; | |
| color: var(--mut); | |
| } | |
| .topbar-state .soft::before { content: "·"; margin-right: 8px; color: var(--line); } | |
| /* the running app, after the daemon label: "Ready · 🕺 Dance". The accent tints | |
| it so it reads as a distinct fact, not more of the status word. */ | |
| .topbar-state .topbar-app { flex: none; font-weight: 600; color: var(--acc); } | |
| .topbar-state .topbar-app::before { content: "·"; margin: 0 8px 0 0; color: var(--line); font-weight: 400; } | |
| /* the loader stands in for the dot: same footprint, no caption */ | |
| .topbar-state .reachy-loader { gap: 0; } | |
| .topbar-title { | |
| font-family: 'Space Grotesk', system-ui, sans-serif; | |
| font-weight: 700; | |
| font-size: 1.5rem; | |
| letter-spacing: -0.02em; | |
| line-height: 1.1; | |
| } | |
| /* daemon switch reads as power: green when on */ | |
| /* the ON/OFF switch is smaller in the menubar, and follows the theme accent | |
| (var(--acc)) like every .ios — the old green forced --ok, which ignored the palette */ | |
| .topbar-daemon .ios { width: 30px; height: 18px; } | |
| .topbar-daemon .ios:after { width: 14px; height: 14px; } | |
| .topbar-daemon .ios.on:after { transform: translateX(12px); } | |
| /* HF relay: just the big status dot (label lives in the tooltip) */ | |
| .topbar .hf-relay { background: none; border: none; padding: 0; } | |
| .topbar .hf-relay .dot { width: 14px; height: 14px; border-radius: 50%; } | |
| .topbar .hf-relay .lbl { display: none; } | |
| .topbar-net { color: var(--mut); font-size: 0.8rem; gap: 16px; } | |
| /* Two sound controls, SAME width and SAME round knob: | |
| · MIC — the SVG waveform IS the slider's rail; the knob rides over it. | |
| · SPEAKER — the SVG waveform sits beside a slider with a thin track line. | |
| The waveform is an SVG stroked in the theme accent (follows theme + palette). */ | |
| .topbar-sound { display: flex; flex-direction: row; flex-wrap: wrap; gap: 6px 16px; align-items: center; justify-content: flex-end; } | |
| /* each channel keeps its intrinsic size (it can still WRAP to a new line on a | |
| narrow topbar, but it never shrinks a slider mid-drag) */ | |
| .topbar-sound .topbar-tool { flex: 0 0 auto; } | |
| /* the MIC's waveform (SVG, theme accent) — its own control rail */ | |
| .wave { color: var(--acc); display: block; height: 24px; } | |
| .wave .wave-path { fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; } | |
| /* MIC = the waveform IS the slider; the knob rides on it. SPEAKER = a plain | |
| slider of the SAME width. */ | |
| .wave-slot { position: relative; width: 110px; height: 24px; flex: 0 0 110px; } | |
| .wave-slot .wave { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; } | |
| .topbar-tool input[type="range"] { | |
| -webkit-appearance: none; | |
| appearance: none; | |
| height: 14px; | |
| margin: 0; | |
| background: transparent; | |
| cursor: pointer; | |
| } | |
| /* the SPEAKER: a classic slider with a thin centred track line. The line is the | |
| theme accent (blue on the default palette), matching the mic's waveform — a | |
| stand-in look while the speaker level itself is still being fixed. */ | |
| .topbar-tool > input[type="range"] { | |
| width: 110px; | |
| background: linear-gradient(to bottom, transparent 6px, | |
| color-mix(in srgb, var(--acc) 70%, transparent) 6px, | |
| color-mix(in srgb, var(--acc) 70%, transparent) 8px, transparent 8px); | |
| } | |
| /* the MIC slider: no track of its own — the waveform behind it is the rail */ | |
| .wave-range { | |
| position: absolute; | |
| left: 0; right: 0; | |
| top: 50%; | |
| transform: translateY(-50%); | |
| width: 100%; | |
| } | |
| .topbar-tool input[type="range"]::-webkit-slider-runnable-track { height: 14px; background: transparent; } | |
| .topbar-tool input[type="range"]::-moz-range-track { height: 14px; background: transparent; } | |
| /* ONE knob for both: a theme-accent ball with a WHITE ring, like the native one */ | |
| .topbar-tool input[type="range"]::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| width: 14px; height: 14px; | |
| border-radius: 50%; | |
| background: var(--acc); | |
| border: 2px solid #fff; | |
| box-shadow: 0 1px 3px rgba(0, 0, 0, 0.45); | |
| cursor: pointer; | |
| } | |
| .topbar-tool input[type="range"]::-moz-range-thumb { | |
| width: 14px; height: 14px; | |
| border-radius: 50%; | |
| background: var(--acc); | |
| border: 2px solid #fff; | |
| box-shadow: 0 1px 3px rgba(0, 0, 0, 0.45); | |
| cursor: pointer; | |
| } | |
| .topbar-net-item { display: inline-flex; align-items: center; gap: 5px; } | |
| .topbar-net-link { color: inherit; cursor: pointer; transition: color 0.15s; } | |
| .topbar-net-link:hover { color: var(--acc); } | |
| /* The robot's vital signs: one lamp per subsystem, hover for the detail. | |
| THREE states, never two — a subsystem we could not ask about must not look | |
| like one that is switched off, so `unknown` is dimmed, not coloured. */ | |
| /* status lamps take the theme accent — only the "Ready" dot stays green */ | |
| .topbar-net-item.st-ok { color: var(--acc); } | |
| .topbar-net-item.st-off { color: var(--mut); opacity: 0.75; } | |
| .topbar-net-item.st-unknown { color: var(--mut); opacity: 0.4; } | |
| .topbar-net-item .ic { font-size: 1.05rem; } | |
| .burger { display: none; } | |
| .rows-close { display: none; } | |
| /* pure mobile mode: compact bar + fullscreen fixed menu. | |
| The rows no longer vanish behind the burger: a COMPACT two-line bar stays | |
| (Pierre's spec, driven by the desktop app's narrow embedded panel): | |
| line 1 = the robot status, line 2 = HF account (smaller) + mic + speaker. | |
| Everything else (name, power, action buttons, vitals) waits in the burger. */ | |
| @media (max-width: 700px) { | |
| .topbar-logo { width: 42px; height: 42px; border-radius: 0.75rem; } | |
| .burger { display: inline-flex; margin-left: auto; } | |
| .topbar-rows { display: flex; flex-direction: column; gap: 4px; } | |
| .topbar-rows:not(.open) .row-main, | |
| .topbar-rows:not(.open) .topbar-net, | |
| .topbar-rows:not(.open) .navmenu-list { display: none; } | |
| .topbar-rows:not(.open) .row-last { order: -1; } | |
| /* STACKED, not shared (Pierre): the HF login gets its OWN line (the 3rd, | |
| after brand and status), and the mic/speaker sliders get theirs below, | |
| full width — squeezed beside the login they were unusably small. */ | |
| .topbar-rows:not(.open) .row-split { display: flex; flex-direction: column; align-items: stretch; gap: 6px; font-size: 0.78rem; } | |
| .topbar-rows:not(.open) .row-split .row-zone { margin-right: 0; } | |
| .topbar-rows:not(.open) .topbar-user img, | |
| .topbar-rows:not(.open) .hf-avatar { width: 18px; height: 18px; } | |
| .topbar-rows:not(.open) .topbar-sound { flex: 1; min-width: 0; display: flex; gap: 8px; flex-wrap: nowrap; justify-content: flex-start; } | |
| .topbar-rows:not(.open) .topbar-tool { flex: 1; min-width: 0; } | |
| .topbar-rows:not(.open) .topbar-tool input[type="range"] { flex: 1; width: 100%; min-width: 0; } | |
| .topbar-rows:not(.open) .topbar-tool-val { font-size: 0.7rem; } | |
| /* the ON/OFF switch + wake button, moved into the brand row (topbar.powerRow) */ | |
| .hdr-power { margin-left: auto; display: flex; align-items: center; gap: 2px; } | |
| .hdr-power + .burger { margin-left: 8px; } | |
| body.menu-open { overflow: hidden; } | |
| /* no backdrop-filter while open, else it becomes the fixed panel's containing block */ | |
| body.menu-open .topbar { | |
| backdrop-filter: none; | |
| -webkit-backdrop-filter: none; | |
| background: transparent; | |
| border-bottom: 0; | |
| } | |
| .topbar-rows.open { | |
| display: flex; | |
| position: fixed; | |
| inset: 0; | |
| z-index: 60; | |
| background: var(--bg); | |
| padding: 74px 22px 26px; | |
| gap: 30px; | |
| overflow-y: auto; | |
| } | |
| .topbar-rows.open .topbar-row { | |
| border-bottom: 1px solid var(--line); | |
| padding-bottom: 22px; | |
| } | |
| .topbar-rows.open .rows-close { | |
| display: inline-flex; | |
| position: absolute; | |
| top: 18px; | |
| right: 18px; | |
| } | |
| .topbar-rows.open .row-main { display: flex; flex-wrap: wrap; gap: 14px; } | |
| } | |
| /* the 4th row topbar.js builds on a phone: the sliders finally get some room */ | |
| .row-sound { display: flex; gap: 18px; } | |
| .row-sound .topbar-tool { flex: 1; min-width: 0; } | |
| .row-sound .topbar-tool input[type="range"] { flex: 1; width: 100%; min-width: 0; } | |
| /* the controller asked for a pose the robot cannot hold: say so, quietly */ | |
| .cp.cp-unreachable { box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--warn) 60%, transparent); } | |
| /* ================= Reachy loader ================= */ | |
| /* Replaces the old cartoon SVG placeholders. Inline SVG, so it takes its | |
| colours from the tokens and follows every theme and palette. */ | |
| .rl-svg { overflow: visible; } | |
| .rl-track, | |
| .rl-arc { | |
| fill: none; | |
| stroke-width: 5; | |
| stroke-linecap: round; | |
| } | |
| .rl-track { stroke: var(--line); } | |
| .rl-arc { | |
| stroke: var(--acc); | |
| /* 2πr ≈ 314 — a quarter drawn, the rest gapped */ | |
| stroke-dasharray: 80 234; | |
| transform-origin: 60px 64px; | |
| animation: rl-spin 1.1s linear infinite; | |
| filter: drop-shadow(0 0 6px color-mix(in srgb, var(--acc) 45%, transparent)); | |
| } | |
| .rl-head { | |
| fill: var(--card); | |
| stroke: var(--txt); | |
| stroke-width: 3; | |
| } | |
| .rl-body { | |
| fill: var(--card); | |
| stroke: var(--txt); | |
| stroke-width: 3; | |
| stroke-linejoin: round; | |
| } | |
| .rl-eyes circle { fill: var(--txt); } | |
| .rl-ant path { | |
| stroke: var(--txt); | |
| stroke-width: 3; | |
| stroke-linecap: round; | |
| fill: none; | |
| } | |
| .rl-ant circle { fill: var(--acc); } | |
| .rl-bot { animation: rl-bob 1.8s ease-in-out infinite; } | |
| .rl-ant-l { transform-origin: 45px 46px; animation: rl-wiggle-l 1.8s ease-in-out infinite; } | |
| .rl-ant-r { transform-origin: 75px 46px; animation: rl-wiggle-r 1.8s ease-in-out infinite; } | |
| .rl-eyes { transform-origin: 60px 62px; animation: rl-blink 3.4s ease-in-out infinite; } | |
| @keyframes rl-spin { to { transform: rotate(360deg); } } | |
| @keyframes rl-bob { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-4px); } } | |
| @keyframes rl-wiggle-l { 0%, 100% { transform: rotate(0deg); } 50% { transform: rotate(-9deg); } } | |
| @keyframes rl-wiggle-r { 0%, 100% { transform: rotate(0deg); } 50% { transform: rotate(9deg); } } | |
| @keyframes rl-blink { 0%, 92%, 100% { transform: scaleY(1); } 96% { transform: scaleY(0.1); } } | |
| /* empty states reuse the same robot, standing still */ | |
| .reachy-loader.is-still .rl-arc { display: none; } | |
| .reachy-loader.is-still .rl-track { opacity: 0.5; } | |
| .reachy-loader.is-still .rl-bot, | |
| .reachy-loader.is-still .rl-ant, | |
| .reachy-loader.is-still .rl-eyes { animation: none; } | |
| .reachy-loader.is-still { opacity: 0.75; } | |
| @media (prefers-reduced-motion: reduce) { | |
| .rl-bot, .rl-ant, .rl-eyes { animation: none; } | |
| .rl-arc { animation-duration: 2.4s; } | |
| } | |
| /* last topbar row: message centred, network info to the right */ | |
| /* two zones now: the soft status on the left, the network info on the right */ | |
| .topbar-row.row-last { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| gap: 12px; | |
| } | |
| @media (max-width: 640px) { | |
| .topbar-row.row-last { grid-template-columns: 1fr; justify-items: center; } | |
| } | |
| /* the loader's eyes are the real Reachy face: two unequal eyes and a bridge */ | |
| .rl-eyes rect { fill: var(--txt); } | |
| /* Click the picture and the head looks there — the pointer says so: an eye over | |
| the image, the usual pointer over the controls that sit on top of it. Only | |
| while a camera view is up (on the 3D twin a drag is an orbit, not an aim). */ | |
| #cam-wrap.cam-aim { cursor: url('/static/assets/cursor-eye.svg') 14 14, crosshair; } | |
| #cam-wrap.cam-aim button, | |
| #cam-wrap.cam-aim select, | |
| #cam-wrap.cam-aim a { cursor: pointer; } | |