@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 !important; } @media (min-width: 40rem) { .sm\:flex.hidden { display: flex !important; } .sm\:grid.hidden { display: grid !important; } } /* ================= 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 !important; } /* 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; }