/* Marionette — mobile-first palette */ :root { /* ─── Design tokens - mirror reachy_mini_marionette_v2/src/theme.ts ── The shell (Emotions / Telepresence / mobile) uses an MUI theme; we don't pull MUI in here, we just copy its *light* palette so this app stays visually consistent. Values map 1:1 to the theme: ACCENT #FF9500 → --primary / --ring shape.borderRadius 12 → --radius background.default #fafafa → --background background.paper #ffffff → --surface / --card text.primary #111111 → --foreground text.secondary rgba(0,0,0,.65) → --muted-foreground divider rgba(0,0,0,.08) → --border Dark mode lives in the `[data-theme="dark"]` block below; the host decides which palette is active (see index.html bootstrap + the applyTheme()/onThemeChange wiring in main.js). Component styles read these tokens only, so they flip for free. */ color-scheme: light; --radius: 12px; /* theme shape.borderRadius */ --radius-sm: calc(var(--radius) - 4px); --radius-md: calc(var(--radius) - 2px); --radius-lg: var(--radius); --radius-xl: calc(var(--radius) + 4px); --radius-2xl: calc(var(--radius) + 8px); --background: #fafafa; /* theme background.default */ --foreground: #111111; /* theme text.primary */ --surface: #ffffff; /* theme background.paper */ --surface-2: #f2f2f2; --card: #ffffff; /* theme background.paper */ --primary: #FF9500; /* theme ACCENT (palette.primary.main) */ --primary-foreground: #ffffff; --muted: #f2f2f2; --muted-foreground: rgba(0, 0, 0, 0.65); /* theme text.secondary */ --accent: rgba(255, 149, 0, 0.10); /* tinted accent surface */ --destructive: oklch(0.62 0.22 27); --border: rgba(0, 0, 0, 0.08); /* theme divider */ --input: rgba(0, 0, 0, 0.12); --overlay-hover: rgba(0, 0, 0, 0.06); /* subtle hover/press tint */ --ring: #FF9500; /* theme ACCENT */ --mint: oklch(0.84 0.1 165); --coral: oklch(0.78 0.13 30); --primary-soft: #ffb84d; /* lighter accent - gradient end (was --violet) */ --sky: oklch(0.82 0.08 225); --shadow-card: 0 1px 2px rgba(0, 0, 0, 0.04), 0 8px 24px -8px rgba(0, 0, 0, 0.08); --shadow-elev: 0 2px 4px rgba(0, 0, 0, 0.04), 0 16px 40px -12px rgba(0, 0, 0, 0.12); --shadow-focus: 0 0 0 4px rgba(255, 149, 0, 0.15); /* theme ACCENT @ 15% */ --font-display: "Plus Jakarta Sans", ui-sans-serif, system-ui, sans-serif; --font-sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", ui-sans-serif, system-ui, sans-serif; /* theme FONT_FAMILY */ } /* ─── Dark palette ────────────────────────────────────────────────── Activated by `data-theme="dark"` on , set by the host: index.html's bootstrap paints a best guess pre-connect, then main.js applies the host's authoritative `handle.theme` and follows `onThemeChange` live. Only color tokens are overridden here — radii, fonts and the geometry above are shared. The accent stays the same Reachy orange, nudged a touch brighter so it carries on a dark surface. */ [data-theme="dark"] { color-scheme: dark; --background: #0e0e10; --foreground: #f5f5f7; --surface: #1b1b1e; --surface-2: #2a2a2e; --card: #1b1b1e; --primary: #ff9f0a; /* accent, brightened for dark */ --primary-foreground: #1a1205; /* near-black reads better on orange */ --muted: #2a2a2e; --muted-foreground: rgba(255, 255, 255, 0.62); --accent: rgba(255, 159, 10, 0.16); /* tinted accent surface */ --border: rgba(255, 255, 255, 0.12); --input: rgba(255, 255, 255, 0.18); --ring: #ff9f0a; --overlay-hover: rgba(255, 255, 255, 0.08); --shadow-card: 0 1px 2px rgba(0, 0, 0, 0.4), 0 8px 24px -8px rgba(0, 0, 0, 0.6); --shadow-elev: 0 2px 4px rgba(0, 0, 0, 0.45), 0 16px 40px -12px rgba(0, 0, 0, 0.7); --shadow-focus: 0 0 0 4px rgba(255, 159, 10, 0.22); } /* Dark-mode touch-ups. The tokens above re-skin every token-driven surface for free; these few selectors exist only because their backgrounds are fixed light gradients hand-tuned for a light backdrop, which would leave the token-driven text on top unreadable on dark. */ [data-theme="dark"] .sync-banner { background: linear-gradient(135deg, rgba(255, 196, 0, 0.10), rgba(255, 149, 0, 0.06)); border-color: rgba(255, 196, 0, 0.22); } [data-theme="dark"] .sync-banner .ttl { color: oklch(0.9 0.07 85); } [data-theme="dark"] .sync-banner-icon { background: rgba(255, 196, 0, 0.12); border-color: rgba(255, 196, 0, 0.24); } [data-theme="dark"] .row.new { background: linear-gradient(135deg, rgba(255, 149, 0, 0.08), rgba(255, 149, 0, 0.04)); } [data-theme="dark"] .row .emoji, [data-theme="dark"] .folder-head .emoji { background: linear-gradient(135deg, rgba(255, 255, 255, 0.07), rgba(255, 255, 255, 0.03)); } * { box-sizing: border-box; border-color: var(--border); } html, body { margin: 0; background: var(--background); color: var(--foreground); font-family: var(--font-sans); -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; } body { min-height: 100dvh; } h1, h2, h3, h4 { font-family: var(--font-display); letter-spacing: -0.02em; margin: 0; } button { font-family: inherit; cursor: pointer; border: none; background: none; color: inherit; } input { font-family: inherit; } /* ─── Layout shell (mobile-first) ─────────────────────── */ .app-shell { max-width: 28rem; margin: 0 auto; min-height: 100dvh; position: relative; padding-bottom: env(safe-area-inset-bottom); } .topbar { position: sticky; top: 0; z-index: 50; display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: max(12px, env(safe-area-inset-top)) 12px 12px; background: color-mix(in oklab, var(--background) 85%, transparent); backdrop-filter: blur(10px); } /* Left slot reserves the back button's footprint so the cog stays pinned to the right edge even on roots where Back is hidden. Grows to take the leftover width so the page title can ellipsis instead of pushing the right-hand actions off-screen. */ .topbar-left { display: flex; align-items: center; gap: 2px; flex: 1 1 auto; min-width: 0; min-height: 40px; } .topbar-actions { display: flex; align-items: center; gap: 6px; flex-shrink: 0; } /* Title + optional subtitle, stacked. Takes the leftover width so both lines ellipsis instead of pushing the right-hand actions off-screen. */ .topbar-titles { display: flex; flex-direction: column; justify-content: center; /* Grow to claim the leftover width so the title uses every available pixel before ellipsing, and shrink freely (min-width:0) when the back button + EXPERIMENTAL chip + right-hand actions crowd it. */ flex: 1 1 auto; min-width: 0; gap: 1px; } /* Page title (Material-style top app bar). Sits next to the Back chevron, replacing the explicit "Back" label when a view supplies a title. */ .topbar-title { font-family: var(--font-display, inherit); font-size: 20px; font-weight: 800; line-height: 1.15; color: var(--foreground); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; } /* Muted one-line description under the title. */ .topbar-subtitle { font-size: 13px; font-weight: 500; line-height: 1.2; color: var(--muted-foreground); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; } /* Roots have no Back chevron, so give their lone title block a little breathing room from the safe-area edge. */ .topbar-left > .topbar-titles:first-child { padding-left: 8px; } /* "EXPERIMENTAL" build chip. flex:none so it never shrinks, but it stays compact ("EXP") on narrow phones where the full word would starve the page title; the full label appears from 480px up. */ .topbar-exp { flex: none; margin-left: 8px; padding: 2px 7px; border-radius: 999px; font-size: 9px; font-weight: 800; letter-spacing: 0.04em; text-transform: uppercase; background: var(--destructive); color: var(--primary-foreground); } .topbar-exp-full { display: none; } .topbar-exp-short { display: inline; } @media (min-width: 480px) { .topbar-exp-full { display: inline; } .topbar-exp-short { display: none; } } /* Contextual "⋯" actions menu — primary-tinted, borderless icon button, matching the in-app menu trigger convention. */ .topbar-back, .topbar-menu, .topbar-settings { color: var(--primary); } .topbar-back:hover, .topbar-menu:hover, .topbar-settings:hover { background: var(--accent); } .icon-btn { width: 40px; height: 40px; border-radius: 999px; display: inline-grid; place-items: center; transition: background-color .15s ease, transform .15s ease; } .icon-btn:hover { background: var(--surface-2); } .icon-btn:active { transform: scale(0.96); } /* Labeled variant: outlined-primary icon + text pill (topbar Back / Settings). */ .icon-btn--labeled { width: auto; height: 40px; display: inline-flex; align-items: center; gap: 7px; padding: 0 12px; border-radius: 12px; font-size: 14px; font-weight: 600; color: var(--primary); background: transparent; } .icon-btn--labeled:hover { background: var(--accent); } .icon-btn-label { line-height: 1; } /* ─── Step container ──────────────────────────────────── */ .step { padding: 8px 20px 140px; } .step h2 { font-size: 28px; font-weight: 800; line-height: 1.1; margin-top: 14px; } .step p.sub { margin-top: 6px; color: var(--muted-foreground); font-size: 15px; } .card { background: transparent; border: 1px solid var(--border); border-radius: var(--radius-2xl); } /* Announcement banner shown at the top of HomeView for the 2026-05-25 sync architecture change. Dismissible. */ .sync-banner { position: relative; display: flex; gap: 12px; margin: 16px 20px 4px; padding: 14px 42px 14px 14px; background: linear-gradient(135deg, oklch(0.97 0.035 90), oklch(0.98 0.025 60)); border: 1px solid oklch(0.88 0.07 82); border-radius: var(--radius-xl); box-shadow: var(--shadow-card); } .sync-banner-icon { flex-shrink: 0; width: 34px; height: 34px; display: grid; place-items: center; font-size: 17px; border-radius: 10px; background: oklch(0.92 0.08 85 / 0.6); border: 1px solid oklch(0.85 0.09 82 / 0.5); } .sync-banner-content { min-width: 0; } .sync-banner .ttl { font-weight: 700; font-size: 13.5px; line-height: 1.3; margin-bottom: 3px; color: oklch(0.38 0.06 70); } .sync-banner .body { font-size: 12.5px; line-height: 1.45; color: var(--muted-foreground); } .sync-banner .dismiss { position: absolute; top: 8px; right: 8px; width: 26px; height: 26px; display: grid; place-items: center; border: none; background: transparent; border-radius: 50%; cursor: pointer; color: var(--muted-foreground); font-size: 17px; line-height: 1; transition: background-color .15s ease, color .15s ease; } .sync-banner .dismiss:hover { background: var(--overlay-hover); color: var(--foreground); } .list { display: flex; flex-direction: column; gap: 10px; margin-top: 18px; } .row { display: flex; align-items: center; gap: 12px; padding: 14px; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-xl); box-shadow: var(--shadow-card); transition: transform .15s ease, box-shadow .15s ease, border-color .15s ease; text-align: left; width: 100%; } .row { cursor: pointer; } .row:active { transform: scale(0.99); } .row.selected { border-color: var(--primary); box-shadow: var(--shadow-focus); } /* "Default" pill on the default folder's title line. */ .row-badge { display: inline-block; margin-left: 8px; padding: 1px 7px; border-radius: 999px; font-size: 10px; font-weight: 700; letter-spacing: .02em; text-transform: uppercase; vertical-align: middle; color: var(--muted-foreground); background: var(--surface-2); border: 1px solid var(--border); } /* ⋯ more-actions button on a Mine folder row. */ .row-menu { flex-shrink: 0; width: 32px; height: 32px; border-radius: 10px; display: grid; place-items: center; font-size: 20px; line-height: 1; color: var(--primary); background: transparent; transition: background-color .15s ease, color .15s ease; } .row-menu:hover { background: var(--accent); color: var(--primary); } .row .emoji { width: 44px; height: 44px; border-radius: 12px; display: grid; place-items: center; font-size: 22px; background: linear-gradient(135deg, oklch(0.95 0.02 260), oklch(0.97 0.02 285)); flex-shrink: 0; } .row .grow { flex: 1; min-width: 0; } .row .title { font-weight: 600; font-size: 15px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .row .meta { color: var(--muted-foreground); font-size: 12px; margin-top: 2px; } .row .chev { color: var(--muted-foreground); flex-shrink: 0; } /* Anchored popover menu (folder ⋯). Backdrop catches outside clicks. */ .menu-backdrop { position: fixed; inset: 0; z-index: 90; } .menu-popover { position: fixed; z-index: 91; min-width: 176px; padding: 6px; background: var(--surface); border: 1px solid var(--border); border-radius: 14px; box-shadow: var(--shadow-card); animation: menu-pop .12s ease both; } @keyframes menu-pop { from { opacity: 0; transform: translateY(-4px) scale(0.98); } to { opacity: 1; transform: translateY(0) scale(1); } } .menu-item { display: flex; align-items: center; gap: 10px; width: 100%; padding: 10px 12px; border-radius: 10px; font-size: 14px; font-weight: 600; color: var(--foreground); text-align: left; transition: background-color .12s ease; } .menu-item:hover { background: var(--accent); } .menu-item.danger { color: var(--destructive); } .menu-ico { width: 18px; height: 18px; flex-shrink: 0; display: grid; place-items: center; } .menu-ico svg { width: 18px; height: 18px; } .row.new { background: linear-gradient(135deg, oklch(0.97 0.02 270), oklch(0.96 0.04 290)); border: 1px dashed rgba(255, 149, 0, 0.4); } .row.new .emoji { background: linear-gradient(135deg, var(--primary), var(--primary-soft)); color: white; } .segmented { display: inline-flex; padding: 4px; background: var(--surface-2); border-radius: 999px; gap: 4px; border: 1px solid var(--border); } .segmented button { padding: 8px 14px; border-radius: 999px; font-size: 13px; font-weight: 600; color: var(--muted-foreground); } .segmented button.active { background: var(--surface); color: var(--foreground); box-shadow: var(--shadow-card); } /* ─── Forms ───────────────────────────────────────────── */ .field { display: flex; flex-direction: column; gap: 6px; margin-top: 14px; } .field label { font-size: 13px; color: var(--muted-foreground); font-weight: 600; } .input, .textarea { width: 100%; padding: 15px 16px; background: transparent; color: var(--foreground); border: 1px solid var(--border); border-radius: 14px; font-size: 16px; outline: none; transition: box-shadow .15s ease, border-color .15s ease; } .input:focus { border-color: var(--primary); box-shadow: var(--shadow-focus); } /* ─── Select (themed dropdown) ────────────────────────── */ /* Trigger mirrors .input metrics; the listbox opens in an anchored popover (see .select-popover) sized to the trigger width. */ .select { width: 100%; display: flex; align-items: center; gap: 10px; padding: 15px 16px; text-align: left; background: transparent; color: var(--foreground); border: 1px solid var(--border); border-radius: 14px; font-size: 16px; outline: none; cursor: pointer; transition: box-shadow .15s ease, border-color .15s ease; } .select:hover { border-color: color-mix(in oklab, var(--primary) 45%, var(--border)); } .select[aria-expanded="true"] { border-color: var(--primary); box-shadow: var(--shadow-focus); } .select-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .select-label.is-placeholder { color: var(--muted-foreground); } .select-chevron { flex-shrink: 0; color: var(--muted-foreground); transition: transform .15s ease; } .select[aria-expanded="true"] .select-chevron { transform: rotate(180deg); color: var(--primary); } .select-popover { position: fixed; z-index: 91; max-height: 280px; overflow-y: auto; padding: 6px; background: var(--surface); border: 1px solid var(--border); border-radius: 14px; box-shadow: var(--shadow-card); animation: menu-pop .12s ease both; } .select-option { display: flex; align-items: center; gap: 10px; width: 100%; padding: 11px 12px; border-radius: 10px; font-size: 15px; font-weight: 600; color: var(--foreground); text-align: left; transition: background-color .12s ease; } .select-option:hover { background: var(--accent); } .select-option.selected { color: var(--primary); } .select-option-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .select-check { width: 16px; flex-shrink: 0; color: var(--primary); display: inline-grid; place-items: center; } /* ─── Buttons ─────────────────────────────────────────── */ .btn { display: inline-flex; align-items: center; justify-content: center; gap: 8px; padding: 14px 18px; border-radius: 14px; font-weight: 700; font-size: 15px; /* Fixed line-height so a button's height doesn't change whether or not its label contains an emoji (emoji glyphs have a taller line box). */ line-height: 20px; background: var(--surface); color: var(--foreground); border: 1px solid var(--border); box-shadow: var(--shadow-card); transition: transform .15s ease, box-shadow .15s ease, background-color .15s ease; } .btn:active { transform: scale(0.98); } .btn.primary { background: transparent; color: var(--primary); border: 1px solid var(--primary); box-shadow: none; } .btn.primary:hover { background: var(--accent); } .btn.danger { background: transparent; color: var(--destructive); border: 1px solid var(--destructive); box-shadow: none; } .btn.danger:hover { background: color-mix(in oklab, var(--destructive) 12%, transparent); } .btn.ghost { background: transparent; box-shadow: none; } .btn[disabled] { opacity: 0.5; pointer-events: none; } .btn-block { width: 100%; } /* ─── Sticky footer ───────────────────────────────────── */ .footer { position: sticky; bottom: 0; left: 0; right: 0; padding: 12px 20px max(16px, env(safe-area-inset-bottom)); background: color-mix(in oklab, var(--background) 85%, transparent); backdrop-filter: blur(12px); border-top: 1px solid var(--border); } /* ─── Step 2: sound tiles ─────────────────────────────── */ /* Sound type: 3 joined, outlined buttons (segmented), sharing borders. */ .tiles { display: flex; margin-top: 0; } .tile { flex: 1; position: relative; display: flex; flex-direction: column; align-items: center; gap: 8px; padding: 14px 8px; background: transparent; color: var(--foreground); border: 1px solid var(--border); box-shadow: none; text-align: center; transition: border-color .15s ease, color .15s ease, background-color .15s ease; } .tile:not(:first-child) { margin-left: -1px; } .tile:first-child { border-radius: var(--radius-xl) 0 0 var(--radius-xl); } .tile:last-child { border-radius: 0 var(--radius-xl) var(--radius-xl) 0; } .tile.active { border-color: var(--primary); color: var(--primary); background: var(--accent); z-index: 1; } .tile .ico { width: 36px; height: 36px; border-radius: 10px; display: grid; place-items: center; background: linear-gradient(135deg, var(--accent), var(--surface-2)); font-size: 18px; } .tile .ttl { font-weight: 700; font-size: 13px; } .tile .desc { font-size: 11px; line-height: 1.25; color: var(--muted-foreground); } /* Loaded-file block for the upload picker: outlined like the buttons, left-aligned, showing the file name + a "Replace" affordance. */ .file-picker { display: flex; align-items: center; gap: 12px; width: 100%; padding: 12px 16px; margin-top: 14px; background: transparent; color: var(--foreground); border: 1px solid var(--primary); border-radius: 14px; text-align: left; } .file-picker .fp-ico { font-size: 18px; flex-shrink: 0; } .file-picker .fp-body { flex: 1; min-width: 0; } .file-picker .fp-name { display: block; font-weight: 700; font-size: 14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .file-picker .fp-meta { display: block; font-size: 12px; color: var(--muted-foreground); font-variant-numeric: tabular-nums; } .file-picker .fp-action { flex-shrink: 0; font-size: 13px; font-weight: 700; color: var(--primary); } /* Themed range slider — default browser sliders render in accent blue, which clashes with the Reachy-orange theme. Track in --surface-2, thumb in the primary gradient. */ /* Outlined slider, matching the app's outlined-primary language: a thin bordered track (more visible than a flat 4px line) and a smaller knob with a transparent-ish fill ringed by a 2px primary border, instead of the old chunky gradient disc. Track is drawn via the runnable-track pseudo so the thumb can be centered precisely across browsers. */ .slider { -webkit-appearance: none; appearance: none; width: 100%; height: 20px; background: transparent; outline: none; cursor: pointer; } .slider::-webkit-slider-runnable-track { height: 6px; border-radius: 999px; background: var(--surface-2); border: 1px solid var(--border); } .slider::-moz-range-track { height: 6px; border-radius: 999px; background: var(--surface-2); border: 1px solid var(--border); } .slider::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 16px; height: 16px; border-radius: 50%; background: var(--background); border: 2px solid var(--primary); box-shadow: 0 1px 2px oklch(0 0 0 / 0.14); cursor: pointer; transition: background-color .12s ease, box-shadow .12s ease; /* Center the 16px(+2px border)=20px knob on the 6px(+1px border)=8px track. */ margin-top: -6px; } .slider::-moz-range-thumb { width: 16px; height: 16px; border-radius: 50%; background: var(--background); border: 2px solid var(--primary); box-shadow: 0 1px 2px oklch(0 0 0 / 0.14); cursor: pointer; } .slider:hover::-webkit-slider-thumb { background: var(--accent); } .slider:hover::-moz-range-thumb { background: var(--accent); } .slider:active::-webkit-slider-thumb { background: var(--accent); box-shadow: 0 0 0 4px var(--accent); } .slider:focus-visible::-webkit-slider-thumb { box-shadow: 0 0 0 4px var(--accent); } /* Mic sub-selector (inside "Record now") — two joined, outlined buttons sharing borders, full width. Same treatment as the sound-type buttons. */ /* Joined, outlined segmented buttons. Shared by the record-now mic picker (.mic-sub) and the generic tab component (.tabs, see views/components/tabs.js) so Mine/Community + the community sort match the "This device / Robot mic" look. */ .mic-sub { display: flex; margin-top: 12px; } .tabs { display: flex; } .mic-sub .opt, .tabs .opt { /* Match a normal .btn's box metrics so these sit at the same height. */ flex: 1; position: relative; padding: 14px 18px; background: transparent; color: var(--foreground); border: 1px solid var(--border); text-align: center; font-size: 15px; font-weight: 600; transition: border-color .15s, color .15s, background-color .15s; } .mic-sub .opt:not(:first-child), .tabs .opt:not(:first-child) { margin-left: -1px; } .mic-sub .opt:first-child, .tabs .opt:first-child { border-radius: 14px 0 0 14px; } .mic-sub .opt:last-child, .tabs .opt:last-child { border-radius: 0 14px 14px 0; } .mic-sub .opt.active, .tabs .opt.active { border-color: var(--primary); color: var(--primary); background: var(--accent); z-index: 1; } .mic-sub .opt[disabled], .tabs .opt[disabled] { opacity: 0.45; pointer-events: none; } /* Live mic-input meter, embedded inside the Stop button while recording. Bars use the button's text color (white on the danger button), kept faint and short so it stays discreet next to the label. */ .mic-meter { display: inline-flex; align-items: center; gap: 2px; height: 14px; } .mic-meter span { width: 2px; height: 2px; border-radius: 1px; background: currentColor; opacity: 0.7; transition: height .07s linear; } /* ─── Step 3: recording stage ─────────────────────────── */ .stage { position: relative; margin-top: 18px; /* Fixed height — the card NEVER resizes between idle / countdown / recording. The illustration is flex-centered; everything else (badge, countdown, progress readout) is an absolute overlay so nothing reflows. */ height: 260px; display: flex; align-items: center; justify-content: center; border-radius: var(--radius-xl); background: var(--surface); border: 1px solid var(--border); padding: 20px; text-align: center; box-shadow: var(--shadow-card); overflow: hidden; transition: border-color .25s ease, box-shadow .25s ease; } /* Active capture: warm the frame so the recording state reads at a glance even if the REC badge scrolls off. */ .stage.rec { border-color: color-mix(in oklch, var(--destructive) 45%, var(--border)); box-shadow: var(--shadow-card), 0 0 0 3px oklch(0.62 0.22 27 / 0.12); } /* Bottom overlay holding the live status + progress readout. Absolute so it floats over the card's lower edge and never affects the card height. */ .stage-readout { position: absolute; left: 0; right: 0; bottom: 0; padding: 0 16px 12px; } /* Hero: the Reachy-head-in-human-hands illustration. Fixed aspect box so the layout doesn't jump while the asset loads, and so the countdown overlay + REC badge have a stable canvas to sit on. */ .stage-hero { position: relative; width: 100%; aspect-ratio: 16 / 10; max-height: 190px; margin: 2px auto 0; display: flex; align-items: center; justify-content: center; transition: transform .25s ease; } .stage-illu { height: 100%; max-width: 100%; display: grid; place-items: center; filter: drop-shadow(0 12px 26px rgba(0, 0, 0, 0.18)); transition: filter .25s ease, opacity .25s ease, transform .25s ease; } /* Inlined line-art: fit by height, keep strokes crisp + theme-aware regardless of the large viewBox downscale. */ .stage-illu > svg { display: block; height: 100%; width: auto; max-width: 100%; } /* While counting down, push the illustration back so the big 3·2·1 number owns the frame. */ .stage-hero.counting .stage-illu { filter: drop-shadow(0 12px 26px rgba(0, 0, 0, 0.18)) blur(3px); opacity: 0.45; transform: scale(0.97); } /* No asset yet (or load failure): an ambient glow placeholder so the hero box still reads as intentional rather than empty/broken. */ .stage-hero--empty::after { content: ""; position: absolute; left: 50%; top: 50%; width: 150px; height: 150px; transform: translate(-50%, -50%); border-radius: 50%; background: radial-gradient(circle at 50% 42%, var(--primary-soft), transparent 70%); opacity: 0.45; } .countdown { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; font-family: var(--font-display); font-size: 110px; font-weight: 800; line-height: 1; background: linear-gradient(135deg, var(--primary), var(--primary-soft)); -webkit-background-clip: text; background-clip: text; color: transparent; pointer-events: none; } .rec-badge { position: absolute; top: 14px; right: 14px; z-index: 2; display: inline-flex; align-items: center; gap: 8px; padding: 6px 12px; border-radius: 999px; background: oklch(0.62 0.22 27 / 0.12); color: var(--destructive); font-weight: 700; font-size: 12px; } .rec-dot { width: 10px; height: 10px; border-radius: 50%; background: var(--destructive); animation: pulse-ring 1.6s ease-out infinite; } /* Live elapsed-time readout under the record-now Stop button. */ .rec-elapsed { display: flex; align-items: center; justify-content: center; gap: 8px; margin-top: 10px; color: var(--destructive); font-weight: 700; font-size: 15px; font-variant-numeric: tabular-nums; } .rec-elapsed[hidden] { display: none; } /* Status label (warnings, "Get ready…", live Hz) pinned top-left as an absolute overlay so it never shifts the centered illustration. The REC badge owns the top-right corner. */ .stage-status { position: absolute; top: 14px; left: 16px; z-index: 2; max-width: calc(100% - 110px); margin: 0; text-align: left; font-weight: 700; font-size: 13px; line-height: 1.3; color: var(--foreground); } .stage-status[hidden] { display: none; } .capture-meters[hidden] { display: none; } /* Capture guidance: a horizontal stepper sitting BELOW the (fixed-height) stage card. Each step lights up as the capture phase advances; earlier steps read as "done". A single helper line under it mirrors the active step's description. */ .stepper { list-style: none; margin: 22px 0 0; padding: 0; display: flex; align-items: flex-start; } .stepper-step { flex: 1; position: relative; display: flex; flex-direction: column; align-items: center; gap: 9px; text-align: center; min-width: 0; color: var(--muted-foreground); } /* Connector line running from this dot to the next one. Spans the full gap between adjacent dots so the stepper reads edge-to-edge. */ .stepper-step:not(:last-child)::after { content: ""; position: absolute; top: 15px; left: calc(50% + 20px); right: calc(-50% + 20px); height: 2px; background: color-mix(in oklab, var(--primary) 20%, transparent); transition: background .2s ease; } .stepper-dot { flex-shrink: 0; width: 32px; height: 32px; border-radius: 999px; display: grid; place-items: center; font-size: 13.5px; font-weight: 800; color: var(--muted-foreground); background: var(--surface-2); border: 2px solid color-mix(in oklab, var(--primary) 20%, transparent); transition: color .2s ease, background .2s ease, border-color .2s ease, box-shadow .2s ease; } .stepper-label { font-size: 13px; font-weight: 600; line-height: 1.2; max-width: 100%; } .stepper-step.is-active .stepper-dot { color: var(--primary-foreground); background: var(--primary); border-color: var(--primary); box-shadow: 0 0 0 4px var(--primary-soft); } .stepper-step.is-active .stepper-label { color: var(--foreground); font-weight: 700; } .stepper-step.is-done .stepper-dot { color: var(--primary); background: var(--primary-soft); border-color: var(--primary); } .stepper-step.is-done .stepper-label { color: var(--foreground); } .stepper-step.is-done::after { background: var(--primary); } .stepper-desc { margin: 16px auto 0; max-width: 30rem; text-align: center; font-size: 13.5px; line-height: 1.5; color: var(--muted-foreground); min-height: 3em; } .stepper-desc strong { color: var(--foreground); font-weight: 700; } /* Capture progress bar — a full-width strip flush against the card's bottom edge (the card's overflow:hidden + radius round its corners). Only visible while recording. */ .stage-progress { position: absolute; left: 0; right: 0; bottom: 0; height: 5px; background: var(--surface-2); opacity: 0; transition: opacity .2s ease; } .stage.rec .stage-progress { opacity: 1; } .stage-progress > span { display: block; height: 100%; width: 0%; background: linear-gradient(90deg, var(--primary), var(--primary-soft)); transition: width .15s linear; } /* Elapsed / total readout, pinned bottom-left and bottom-right just above the progress strip. */ .capture-meters .time { display: flex; justify-content: space-between; color: var(--muted-foreground); font-size: 12px; font-variant-numeric: tabular-nums; } /* ─── Folder view ─────────────────────────────────────── */ .folder-head { display: flex; align-items: center; gap: 12px; padding: 8px 20px 4px; } .folder-head .emoji { width: 52px; height: 52px; border-radius: 14px; display: grid; place-items: center; font-size: 26px; background: linear-gradient(135deg, oklch(0.95 0.02 260), oklch(0.97 0.02 285)); flex-shrink: 0; } .folder-head .grow { flex: 1; min-width: 0; } .folder-head .title { font-weight: 800; font-size: 20px; font-family: var(--font-display); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .folder-head .meta { color: var(--muted-foreground); font-size: 13px; } .folder-head .row-menu { width: 36px; height: 36px; font-size: 22px; } /* Folder view: primary CTA, moves section, and the collapsible "Playback settings" disclosure. */ /* Folder + Home views: flex the shell + view so the primary CTA docks at the bottom (thumb zone) while the list stays top-aligned and scrolls. */ .app-shell--folder, .app-shell--home { display: flex; flex-direction: column; } .folder-view, .home-view { flex: 1; display: flex; flex-direction: column; } /* Home reuses the .step shell (140px reserved bottom padding meant for a floating CTA); with the dock now in flow, drop that padding and instead reserve space under the list so the last folder clears the sticky dock. */ .home-view { padding-bottom: 0; } .home-view #folder-list { padding-bottom: 88px; } /* Docked primary action: margin-top:auto pushes it to the bottom when content is short; position:sticky keeps it visible above the scrolling list when the content overflows. The trailing .disclosure reserves padding-bottom so its last row isn't hidden behind the dock. */ .cta-dock { margin-top: auto; position: sticky; bottom: 0; z-index: 5; padding: 16px 20px max(24px, calc(env(safe-area-inset-bottom) + 12px)); background: color-mix(in oklab, var(--background) 88%, transparent); backdrop-filter: blur(12px); border-top: 1px solid var(--border); } .folder-body { padding: 10px 20px 4px; } .folder-body .list { margin-top: 10px; } .section-label { font-size: 12px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: var(--muted-foreground); } /* Playback controls (volume + per-folder audio sync) live in the bottom dock, stacked above the primary CTA. The dock owns the outer padding, so here we just stack the rows and add a little breathing room below before the record button. */ .folder-playback { display: flex; flex-direction: column; gap: 10px; margin-bottom: 14px; } .folder-playback .section-label { margin-bottom: 2px; } /* Keep the card chrome but let it span the full section width: the only undesirable inset was the volume-row's horizontal margin (20px). */ .folder-playback .volume-row { margin: 0; } /* Folder empty state: icon badge + title + hint, centered. */ .folder-empty { text-align: center; padding: 24px 20px 4px; display: flex; flex-direction: column; align-items: center; gap: 10px; } .folder-empty-ico { width: 64px; height: 64px; border-radius: 18px; display: grid; place-items: center; font-size: 30px; background: var(--surface-2); border: 1px solid var(--border); } .folder-empty-title { font-weight: 800; font-size: 17px; font-family: var(--font-display); } .folder-empty-sub { color: var(--muted-foreground); font-size: 13px; line-height: 1.45; max-width: 260px; } .disclosure { padding-bottom: max(96px, env(safe-area-inset-bottom)); } .disclosure-toggle { display: flex; align-items: center; justify-content: space-between; width: 100%; padding: 14px 20px; color: var(--foreground); font-size: 14px; font-weight: 600; } .disclosure-toggle:hover { color: var(--primary); } .disclosure-chev { color: var(--muted-foreground); transition: transform .2s ease; } .disclosure.open .disclosure-chev { transform: rotate(180deg); } .disclosure-body { padding-bottom: 6px; } /* Generic move card — used in the folder list and the review screen. Leading icon + name/meta + optional play toggle + contextual action buttons, with a dedicated progress bar pinned to the top edge. */ .move-card { position: relative; overflow: hidden; display: flex; align-items: center; gap: 12px; padding: 12px 14px; border-radius: var(--radius-xl); border: 1px solid var(--border); background: var(--surface); box-shadow: var(--shadow-card); } .move-card .move-card-row { display: flex; align-items: center; gap: 12px; flex: 1; min-width: 0; /* Let a crowded action row (Share + rename + delete on an owned move) drop to a second line on narrow phones instead of starving the title to nothing. Wide layouts keep everything on one line. */ flex-wrap: wrap; row-gap: 10px; } .move-card.highlight { border-color: var(--primary); box-shadow: var(--shadow-focus); } .move-card-play { width: 40px; height: 40px; border-radius: 12px; display: grid; place-items: center; background: linear-gradient(135deg, var(--primary), var(--primary-soft)); color: white; font-size: 15px; flex-shrink: 0; } .move-card-play:disabled { opacity: 0.4; cursor: not-allowed; } /* Loading state: the play button shows a spinner while the move is being prepared/uploaded (no progress bar during this phase). */ .move-card.is-loading .move-card-play { color: transparent; position: relative; } .move-card.is-loading .move-card-play::after { content: ""; position: absolute; inset: 0; margin: auto; width: 16px; height: 16px; border-radius: 50%; border: 2px solid rgba(255, 255, 255, 0.35); border-top-color: #fff; animation: move-card-spin 0.7s linear infinite; } @keyframes move-card-spin { to { transform: rotate(360deg); } } .move-card-ico { width: 34px; height: 34px; flex-shrink: 0; display: grid; place-items: center; font-size: 18px; border-radius: 10px; background: var(--surface-2); } /* Basis of 150px so the row wraps the actions below once the title would be squeezed under ~150px; above that everything stays on one line. */ .move-card-body { flex: 1 1 150px; min-width: 0; } .move-card-title { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } /* Single-line, like the title: without this the meta wrapped to several lines when the action buttons squeezed the body on narrow phones, and the overflowing text bled under the buttons. */ .move-card-meta { color: var(--muted-foreground); font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } /* Live playback readout ("Uploading 43%", "0:03 / 0:07") written into the meta slot by createPlaybackReadout. Tabular digits keep the ticking timer from jittering horizontally; the accent colour marks it as live rather than descriptive. */ .move-card-meta.is-live { color: var(--primary); font-weight: 600; font-variant-numeric: tabular-nums; } .move-card-actions { display: flex; align-items: center; gap: 6px; flex-shrink: 0; margin-left: auto; } /* Compact outlined action buttons on a move card — same primary button language as the rest of the app, sized down to sit inside the row. */ .move-card-actions .btn.move-action { padding: 7px 12px; gap: 6px; font-size: 13px; border-radius: 10px; box-shadow: none; } .move-card-actions .btn.move-action svg { width: 16px; height: 16px; } .move-card-actions .btn.move-action.icon-only { padding: 0; width: 36px; height: 36px; } /* In-flight state (share upload): the button carries its own spinner so the feedback stays on the row that's working, no toast needed. Keep the disabled look readable (opacity stays high) since it's an active state, not an unavailable one. */ .move-card-actions .btn.move-action.is-busy { opacity: 0.85; cursor: progress; } .btn-spinner { display: inline-block; width: 12px; height: 12px; border-radius: 50%; border: 2px solid currentColor; border-top-color: transparent; opacity: 0.8; flex-shrink: 0; animation: move-card-spin 0.7s linear infinite; } /* "Shared" state: swap the outline to the success accent. */ .move-card-actions .btn.move-action.shared { color: oklch(0.5 0.12 165); border-color: oklch(0.5 0.12 165); } .move-card-actions .btn.move-action.shared:hover { background: oklch(0.84 0.1 165 / 0.16); } /* Playback progress — a thin bar flush to the top edge of the card. Hidden (no track) until activated, so idle cards show nothing. */ .move-progress { position: absolute; top: 0; left: 0; right: 0; height: 3px; background: var(--surface-2); z-index: 2; pointer-events: none; overflow: hidden; display: none; } .move-progress.is-active { display: block; } .move-progress > span { display: block; height: 100%; width: 0%; background: linear-gradient(90deg, var(--primary), var(--primary-soft)); transition: width 80ms linear; } .chip { display: inline-flex; align-items: center; gap: 6px; padding: 6px 10px; border-radius: 999px; font-size: 12px; font-weight: 600; background: var(--surface-2); color: var(--muted-foreground); border: 1px solid var(--border); } .chip.shared { background: oklch(0.84 0.1 165 / 0.2); color: oklch(0.4 0.12 165); border-color: transparent; } /* ─── Empty, toast ────────────────────────────────────── */ .empty { text-align: center; padding: 40px 20px; color: var(--muted-foreground); } /* Toast: a single status card, bottom-center. Primary-outlined (accent border + soft accent halo) so it stays legible when it lands over similar surface cards. Accent is theme-aware and per-kind. The bottom bar runs in 3 modes — countdown / determinate / indeterminate (see components/toast.js). */ .toast { --toast-accent: var(--primary); /* Sit above the bottom dock (playback controls + Record CTA) when one is present — main.js sets --toast-dock-offset to the dock height so the toast never lands on top of the sliders — and always clear the device safe-area / home indicator so it can't be clipped at the screen edge. */ position: fixed; left: 50%; transform: translateX(-50%); bottom: calc(var(--toast-dock-offset, 0px) + max(18px, env(safe-area-inset-bottom))); display: flex; align-items: center; gap: 12px; /* Span the full app width, aligned with the content cards' 20px gutters (.app-shell is 28rem; on phones it tracks the viewport edges). */ width: min(calc(28rem - 40px), calc(100vw - 40px)); padding: 16px 16px 14px; background: color-mix(in oklab, var(--toast-accent) 12%, var(--surface)); color: var(--foreground); border: 1.5px solid var(--toast-accent); border-radius: 16px; box-shadow: var(--shadow-elev); z-index: 80; overflow: hidden; font-size: 14px; font-weight: 600; line-height: 1.35; animation: toast-in .26s cubic-bezier(.21, 1.02, .73, 1) both; } .toast.success { --toast-accent: oklch(0.55 0.13 165); } .toast.error { --toast-accent: var(--destructive); } .toast.toast-out { animation: toast-out .2s ease forwards; } .toast-msg { flex: 1; min-width: 0; overflow-wrap: anywhere; } /* With a trailing metric (live Hz), keep the message on a single line so the appearing number can't push it to two lines — ellipsis long names instead. */ .toast.has-detail .toast-msg { overflow-wrap: normal; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } /* Trailing metric (e.g. live "100 Hz" during playback): muted, monospaced, fixed min-width so the digits don't jitter the layout as they tick. */ .toast-detail { flex-shrink: 0; min-width: 54px; text-align: center; font: 600 12px/1 ui-monospace, SFMono-Regular, Menlo, monospace; color: var(--muted-foreground); font-variant-numeric: tabular-nums; padding: 3px 8px; border-radius: 999px; background: color-mix(in oklab, var(--toast-accent) 14%, transparent); } .toast-icon { flex-shrink: 0; width: 22px; height: 22px; border-radius: 999px; display: grid; place-items: center; font-size: 13px; font-weight: 800; color: white; background: var(--toast-accent); } /* Loading spinner inside the toast icon chip — same pattern as the move-card play button (reuses the move-card-spin keyframes). */ .toast-spinner { width: 14px; height: 14px; border-radius: 50%; border: 2px solid rgba(255, 255, 255, 0.4); border-top-color: #fff; animation: move-card-spin 0.7s linear infinite; } .toast-close { flex-shrink: 0; width: 26px; height: 26px; margin: -4px -6px -4px 0; border-radius: 999px; font-size: 19px; line-height: 1; color: var(--muted-foreground); display: grid; place-items: center; transition: background-color .15s ease, color .15s ease; } .toast-close:hover { background: var(--surface-2); color: var(--foreground); } /* Top progress bar (track + fill). */ .toast-bar { position: absolute; left: 0; right: 0; top: 0; height: 3px; background: color-mix(in oklab, var(--toast-accent) 18%, transparent); overflow: hidden; } .toast-bar > i { display: block; height: 100%; background: var(--toast-accent); transform-origin: left center; } /* countdown: drains 100% → 0% over the toast's lifetime (duration set inline). */ .toast-bar.countdown > i { transform: scaleX(1); animation: toast-countdown linear forwards; } @keyframes toast-countdown { from { transform: scaleX(1); } to { transform: scaleX(0); } } /* determinate: width = progress fraction (transform set inline per tick). The bar node is preserved across in-place refreshes (see toast.js), so a short linear transition smooths the stepped updates into a steady fill — used by both the import progress and the live playback bar. */ .toast-bar.determinate > i { transform: scaleX(0); transition: transform .16s linear; } /* indeterminate: a short segment looping for unknown-length operations. */ .toast-bar.indeterminate > i { width: 40%; transform: translateX(-120%); animation: toast-indet 1.15s ease-in-out infinite; } @keyframes toast-indet { 0% { transform: translateX(-120%); } 100% { transform: translateX(280%); } } @keyframes toast-in { from { opacity: 0; transform: translateX(-50%) translateY(12px); } to { opacity: 1; transform: translateX(-50%) translateY(0); } } @keyframes toast-out { from { opacity: 1; transform: translateX(-50%) translateY(0); } to { opacity: 0; transform: translateX(-50%) translateY(12px); } } /* ─── Connection gate ─────────────────────────────────── */ .gate { display: grid; place-items: center; min-height: 100dvh; padding: 20px; } .gate-card { max-width: 26rem; width: 100%; padding: 32px 24px; text-align: center; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-2xl); box-shadow: var(--shadow-elev); } .gate-card .big-emoji { font-size: 64px; } /* Image variant of .big-emoji (the Marionette logo SVG). The div variant above keeps its 64px font sizing for the robot-picker emoji. */ .gate-card img.big-emoji { width: 96px; height: 96px; object-fit: contain; display: block; margin: 0 auto 4px; } .gate-card h1 { font-family: var(--font-display); font-weight: 800; font-size: 28px; margin-top: 6px; } .gate-card p { color: var(--muted-foreground); font-size: 15px; margin-top: 6px; } .gate-card .btn { margin-top: 18px; } .gate-card .divider { margin: 22px 0 6px; color: var(--muted-foreground); font-size: 12px; text-transform: uppercase; letter-spacing: 1px; position: relative; } .gate-card .divider::before, .gate-card .divider::after { content: ""; position: absolute; top: 50%; width: calc(50% - 30px); height: 1px; background: var(--border); } .gate-card .divider::before { left: 0; } .gate-card .divider::after { right: 0; } .robot-list { display: grid; gap: 8px; margin-top: 14px; text-align: left; } /* ─── Settings sheet ──────────────────────────────────── */ .sheet-backdrop { position: fixed; inset: 0; background: oklch(0.18 0.02 260 / 0.4); z-index: 60; animation: fade-in .2s ease both; } /* Settings: a full-screen page that pushes in from the right (native "settings page" feel) instead of a cramped bottom sheet. Sticky header (Back chevron + title) mirrors the app topbar; the body scrolls on its own so the long list of rows has full height to breathe. */ .settings-page { position: fixed; inset: 0; z-index: 70; max-width: 28rem; margin: 0 auto; background: var(--background); color: var(--foreground); display: flex; flex-direction: column; } .settings-head { position: sticky; top: 0; z-index: 1; display: flex; align-items: center; justify-content: space-between; gap: 6px; /* Vertical padding matches the global topbar (12px top/bottom) so the settings header is exactly the same height. Right padding (12px) keeps the close X over the cog it covers; left padding (20px) lines the title up with the rows below. */ padding: max(12px, env(safe-area-inset-top)) 12px 12px 20px; background: color-mix(in oklab, var(--background) 85%, transparent); backdrop-filter: blur(10px); } .settings-title { font-family: var(--font-display); font-weight: 800; font-size: 22px; } .settings-head .icon-btn { color: var(--primary); } .settings-head .icon-btn:hover { background: var(--accent); } .settings-body { flex: 1; overflow-y: auto; -webkit-overflow-scrolling: touch; overscroll-behavior: contain; padding: 4px 20px max(24px, env(safe-area-inset-bottom)); } .settings-page .row-setting { display: flex; align-items: center; justify-content: space-between; padding: 14px 0; border-bottom: 1px solid var(--border); gap: 12px; } .settings-page .row-setting:last-child { border-bottom: none; } .settings-page .row-setting .label { font-weight: 600; font-size: 14px; } .settings-page .row-setting .sublabel { color: var(--muted-foreground); font-size: 12px; margin-top: 2px; } /* Stacked variant: label/help on top, a full-width control (tabs) below. Used for the Audio output + Debug HUD segmented controls. */ .settings-page .row-setting.stack { flex-direction: column; align-items: stretch; gap: 10px; } .segmented.small button { padding: 6px 10px; font-size: 12px; } /* ─── Animations ──────────────────────────────────────── */ @keyframes float-y { 0%,100%{transform:translateY(0)} 50%{transform:translateY(-8px)} } @keyframes pulse-ring { 0% { box-shadow: 0 0 0 0 oklch(0.62 0.22 27 / 0.4); } 70% { box-shadow: 0 0 0 16px oklch(0.62 0.22 27 / 0); } 100% { box-shadow: 0 0 0 0 oklch(0.62 0.22 27 / 0); } } @keyframes slide-up { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: translateY(0); } } @keyframes fade-in { from { opacity: 0; } to { opacity: 1; } } /* ---------------------------------------------------------------------- Waveform trim widget: a decoded waveform canvas with a start bracket, an end bracket, and a draggable playhead. A tap on the waveform toggles play/pause of the selected region. ---------------------------------------------------------------------- Layout in pixels: - Outer .trim-bar: 72 px tall, 12 px horizontal padding. The padding equals half the widest handle (24 px) so when start is at 0 or end is at dur the handles still sit inside the card. - .trim-wave: the waveform; bars inside the selection are drawn in --primary, bars outside are dimmed. - .trim-region: a faint highlight band between the brackets. - .trim-handle: 32 px wide hit area (>=44 px on phones via media query) so taps land reliably. Visible shape is a single thin vertical bar drawn with ::before. - .trim-playhead-new: a thin vertical line marking playback position. */ .trim-bar { position: relative; height: 72px; padding: 0 12px; /* gutter == half the widest handle */ touch-action: none; user-select: none; -webkit-user-select: none; cursor: pointer; } .trim-wave { position: absolute; inset: 0; width: 100%; height: 100%; z-index: 0; } .trim-region { /* No fill: the waveform bars already color the selection. Kept as a zero-cost layout marker for the bracket handles. */ position: absolute; top: 6px; bottom: 6px; background: transparent; z-index: 1; pointer-events: none; } .trim-handle { position: absolute; top: 0; bottom: 0; width: 32px; /* wide hit area for touch */ margin-left: -16px; /* center on the time x */ cursor: ew-resize; display: flex; align-items: center; justify-content: center; z-index: 2; } @media (hover: none) and (pointer: coarse) { /* Phones: even wider hit area. */ .trim-handle { width: 44px; margin-left: -22px; } } .trim-handle::before { /* Vertical spine of the bracket. */ content: ""; position: absolute; width: 6px; top: 6px; bottom: 6px; background: var(--primary); border-radius: 3px; box-shadow: 0 0 0 1px var(--background), 0 1px 3px rgba(0,0,0,0.18); } .trim-handle:hover::before { filter: brightness(1.1); } .trim-handle.dragging::before { box-shadow: 0 0 0 2px var(--background), 0 2px 6px rgba(0,0,0,0.25); } .trim-playhead-new { position: absolute; top: 0; bottom: 0; width: 28px; /* hit area, knob is smaller */ margin-left: -14px; cursor: grab; display: none; /* shown once a cursor exists */ z-index: 3; } .trim-playhead-new.on { display: block; } .trim-playhead-new.dragging { cursor: grabbing; } .trim-playhead-new::before { /* The thin red vertical line itself. */ content: ""; position: absolute; top: 6px; bottom: 6px; left: 50%; width: 2px; margin-left: -1px; background: var(--destructive); box-shadow: 0 0 6px var(--destructive); border-radius: 1px; pointer-events: none; } .trim-link { background: transparent; border: none; padding: 0; font-size: 13px; font-weight: 600; color: var(--primary); /* Fixed line-height so swapping the ▶ / ⏸ glyph (which have different intrinsic heights) doesn't change the row height. */ line-height: 20px; height: 20px; cursor: pointer; } .trim-link:hover { text-decoration: underline; } /* Generic slider field: a leading icon, the themed range (.slider), and a right-aligned value readout. One row, layout-neutral — wrap it in a card (.volume-row) or drop it straight into a form. */ .slider-field { display: flex; align-items: center; gap: 10px; } .slider-field .slider { flex: 1; } /* Disabled (e.g. the sync-offset slider while a move is playing): dim and block interaction, but keep the value readout legible. */ .slider-field.is-disabled { opacity: 0.5; } .slider-field.is-disabled .slider { pointer-events: none; cursor: not-allowed; } .slider-field .sf-ico { width: 28px; height: 28px; display: grid; place-items: center; font-size: 16px; flex-shrink: 0; } .slider-field .sf-val { min-width: 40px; text-align: right; font-variant-numeric: tabular-nums; font-size: 13px; font-weight: 600; color: var(--muted-foreground); } /* Card presentation for a slider field — the speaker/mic volume and the per-folder audio offset bars at the top of Home/Folder, always within reach when playing moves. */ .volume-row { padding: 10px 14px; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-xl); box-shadow: var(--shadow-card); margin: 14px 20px 0; } /* In-app dialogs (confirm / prompt) — themed alternative to the native browser pop-ups, which look out-of-place. */ .dialog-backdrop { position: fixed; inset: 0; background: oklch(0.18 0.02 260 / 0.5); z-index: 100; animation: fade-in .18s ease both; } .dialog { position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%); background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-2xl); box-shadow: var(--shadow-elev); z-index: 110; width: min(420px, calc(100vw - 32px)); padding: 22px 22px 18px; animation: dialog-pop .2s ease both; } .dialog h3 { font-family: var(--font-display); font-weight: 800; font-size: 18px; margin-bottom: 8px; } .dialog p { margin: 0; color: var(--muted-foreground); font-size: 14px; line-height: 1.45; } .dialog .input { margin-top: 14px; } .dialog-actions { display: flex; gap: 10px; margin-top: 20px; } /* Every dialog button is outlined regardless of its semantic kind: a transparent fill with a 1px border + text tinted to its color. Keeps the modal calm and consistent (no heavy filled buttons), while the border/text color still signals intent (neutral cancel, primary confirm, danger). */ .dialog-actions .btn { flex: 1; padding: 12px 14px; background: transparent; box-shadow: none; } .dialog-actions .btn.ghost { border: 1px solid var(--border); color: var(--foreground); } .dialog-actions .btn.ghost:hover { background: var(--surface-2); } .dialog-actions .btn.primary { border: 1px solid var(--primary); color: var(--primary); } .dialog-actions .btn.primary:hover { background: var(--accent); } .dialog-actions .btn.danger { border: 1px solid var(--destructive); color: var(--destructive); } .dialog-actions .btn.danger:hover { background: color-mix(in oklab, var(--destructive) 12%, transparent); } @keyframes dialog-pop { from { opacity: 0; transform: translate(-50%, calc(-50% + 8px)) scale(.97); } to { opacity: 1; transform: translate(-50%, -50%) scale(1); } } /* Tiny debug HUD (toggled from settings) */ .debug-hud { position: fixed; right: 8px; bottom: 8px; background: rgba(0,0,0,0.72); color: #fff; font-family: ui-monospace, monospace; font-size: 11px; padding: 6px 10px; border-radius: 8px; z-index: 90; pointer-events: none; line-height: 1.4; } /* ─── Welcome / onboarding ─────────────────────────────── */ .welcome { text-align: center; padding-bottom: 48px; } /* Vertical centering: the shell becomes a flex column for this view only (set in main.js via .app-shell--welcome) and the onboarding stack grows to fill the height below the topbar, centering its content. */ .app-shell--welcome { display: flex; flex-direction: column; } .app-shell--welcome .welcome { flex: 1; display: flex; flex-direction: column; justify-content: center; padding-bottom: 0; } /* Wizard create steps: center the [form body + action row] together so the validation button sits directly below the form, not pinned to the viewport bottom. `margin-top: auto` above the body and `margin-bottom: auto` below the footer split the free space evenly, centering the whole group. When the content is taller than the viewport both auto margins collapse to 0 and it scrolls from the top. The footer drops its sticky bar chrome here and flows inline. Scoped to the wizard via the modifier main.js adds. */ .app-shell--wizard { display: flex; flex-direction: column; } .app-shell--wizard .step { flex: 1; display: flex; flex-direction: column; padding-bottom: 0; } /* Body content sits flush under the topbar (top-aligned). The primary actions get pushed to the bottom via the .footer rule below, giving the standard 3-zone mobile layout (header / scrolling content / thumb-zone CTA). */ .app-shell--wizard .wiz-body { margin-top: 0; width: 100%; } .app-shell--wizard .wiz-body--top { margin-top: 0; } .app-shell--wizard .footer { /* 3-zone mobile layout: body top-aligned, primary actions pinned to the bottom (thumb zone). margin-top:auto absorbs the slack above the footer so it sits at the bottom of the shell while the body stays flush at top. */ margin-top: auto; position: static; background: none; backdrop-filter: none; border-top: 0; /* No horizontal padding: the parent .step already pads 20px, so the action row aligns with the form fields and the two buttons span the full content width. Extra top padding separates the actions from the form above. */ padding: 32px 0 max(16px, env(safe-area-inset-bottom)); } /* Hero: the icon.svg under a glow, with decorative "strings" above it that gently sway — a nod to the puppeteer control cross in the mark. */ .welcome-hero { position: relative; width: 168px; height: 180px; margin: 14px auto 2px; display: grid; place-items: end center; } .welcome-hero::before { content: ""; position: absolute; left: 50%; bottom: 6px; width: 180px; height: 180px; transform: translateX(-50%); background: radial-gradient(circle at 50% 55%, var(--accent), transparent 70%); z-index: 0; } .welcome-mark { position: relative; z-index: 1; width: 140px; height: 140px; object-fit: contain; } .welcome-title { font-family: var(--font-display); font-size: 34px; font-weight: 800; letter-spacing: -0.02em; margin-top: 10px; } .welcome-tagline { color: var(--primary); font-weight: 700; font-size: 15px; margin-top: 2px; } .welcome-lede { color: var(--muted-foreground); font-size: 15px; line-height: 1.5; margin: 12px auto 0; max-width: 19rem; } .welcome-actions { display: flex; flex-direction: column; gap: 8px; margin-top: 20px; }