@import "tailwindcss"; /* ── Minimal design system ──────────────────────────────────────── Quiet, monochrome chrome in the spirit of Apple / OpenAI interfaces: white (or near-black) canvases, hairline separators instead of boxes, large radii, pill controls, frosted floating bars and one functional accent (system blue) reserved for selection, focus and links. The primary action is a solid black (white in dark) pill. Flagged episodes use system orange (as Mail flags do), which also echoes the LeRobot logo. selects the dark set; see utils/theme.ts. Never hard-code colours in components: every value has a token below. */ @custom-variant dark (&:where(.dark, .dark *)); :root { color-scheme: light; /* Canvas & surfaces */ --bg: #ffffff; --bg-subtle: #f9f9fb; /* sidebar, page wells */ --surface: #ffffff; /* cards */ --surface-muted: #f4f4f6; /* inputs, segmented tracks, code */ --surface-raised: rgba(255, 255, 255, 0.82); /* glass: menus, floating bars */ --media: #0b0b0c; /* behind video and 3D */ /* Text */ --fg: #111113; --fg-secondary: #5f5f66; /* 6.4:1 — any text the user must read */ --fg-tertiary: #8e8e93; /* 3.3:1 — decoration, placeholders */ /* Lines & fills */ --separator: rgba(0, 0, 0, 0.08); --separator-strong: rgba(0, 0, 0, 0.14); --fill: rgba(0, 0, 0, 0.04); --fill-hover: rgba(0, 0, 0, 0.065); --fill-press: rgba(0, 0, 0, 0.1); /* Primary action: solid monochrome pill */ --primary: #111113; --on-primary: #ffffff; /* Functional accent: selection, focus, links */ --accent: #0071e3; --accent-fg: #0066cc; --accent-soft: rgba(0, 113, 227, 0.1); --focus-ring: rgba(0, 113, 227, 0.35); /* Flagged episodes only */ --flag: #ff9500; --flag-fg: #b25f00; --flag-soft: rgba(255, 149, 0, 0.12); /* Status */ --success: #248a3d; --success-soft: rgba(52, 199, 89, 0.12); --warning: #8a6100; /* amber, distinct from the flag orange */ --warning-soft: rgba(255, 204, 0, 0.16); --danger: #d70015; --danger-soft: rgba(255, 59, 48, 0.1); /* Playback position: neutral, like a QuickTime scrubber */ --playhead: #111113; /* Charts */ --chart-grid: rgba(0, 0, 0, 0.06); --chart-axis: rgba(0, 0, 0, 0.12); --chart-1: #0071e3; --chart-2: #28a745; --chart-3: #af52de; --chart-4: #00a3bf; --chart-5: #ff2d55; --chart-6: #5856d6; --chart-7: #a2845e; --chart-8: #30b0c7; --chart-9: #c7a400; --chart-10: #6e8b3d; /* olive: distinct from the tertiary grey */ /* Shape */ --radius-sm: 8px; --radius: 10px; /* controls, inputs */ --radius-lg: 16px; /* cards */ --radius-xl: 20px; /* media frames, sheets */ /* Elevation: almost flat; soft shadow only on floating surfaces */ --shadow-float: 0 0 0 0.5px rgba(0, 0, 0, 0.08), 0 8px 28px -6px rgba(0, 0, 0, 0.16), 0 2px 6px -2px rgba(0, 0, 0, 0.06); --shadow-card: 0 1px 2px rgba(0, 0, 0, 0.03); /* Motion */ /* House curves. --ease-out is transitions.dev's --ease-smooth-out (same curve): every surface that appears or leaves. --ease-spring (the non-overshooting iOS sheet curve) is only for things that physically slide: tab pill, segmented thumb, switch knob, phone sheet. */ --ease-out: cubic-bezier(0.22, 1, 0.36, 1); --ease-smooth-out: var(--ease-out); --ease-spring: cubic-bezier(0.32, 0.72, 0, 1); /* House durations. Careful: --dur-fast (120ms, hover/press) is NOT --duration-fast (250ms). */ --dur-fast: 120ms; --dur: 200ms; --dur-slow: 320ms; /* transitions.dev motion scale, matched by usage (see the transitions-dev skill). */ --duration-stagger: 40ms; --duration-micro: 80ms; --duration-quick: 150ms; /* any close: menu, popover, modal; text/tooltip appear */ --duration-fast: 250ms; /* menu / modal open, icon swap, accordion, sliding tabs */ --duration-medium: 350ms; /* panel close */ --duration-slow: 400ms; /* panel open, skeleton -> content reveal */ --duration-very-slow: 500ms; --scale-large: 0.96; /* modal open / close */ --scale-medium: 0.97; /* menu open */ --scale-small: 0.98; /* tooltip open */ --scale-tiny: 0.99; /* menu close */ --blur-small: 2px; /* icon swap, skeleton reveal (small layers only) */ --blur-medium: 3px; --distance-micro: 4px; --distance-medium: 12px; --scrollbar: rgba(0, 0, 0, 0.16); } :root.dark { color-scheme: dark; --bg: #0f0f10; --bg-subtle: #141416; --surface: #18181a; --surface-muted: #222225; --surface-raised: rgba(32, 32, 35, 0.78); --media: #000000; --fg: #f5f5f7; --fg-secondary: #a1a1a6; --fg-tertiary: #6e6e73; --separator: rgba(255, 255, 255, 0.08); --separator-strong: rgba(255, 255, 255, 0.14); --fill: rgba(255, 255, 255, 0.05); --fill-hover: rgba(255, 255, 255, 0.08); --fill-press: rgba(255, 255, 255, 0.12); --primary: #f5f5f7; --on-primary: #111113; --accent: #0a84ff; --accent-fg: #4da3ff; --accent-soft: rgba(10, 132, 255, 0.16); --focus-ring: rgba(10, 132, 255, 0.45); --flag: #ff9f0a; --flag-fg: #ffb340; --flag-soft: rgba(255, 159, 10, 0.16); --success: #30d158; --success-soft: rgba(48, 209, 88, 0.14); --warning: #ffd60a; --warning-soft: rgba(255, 214, 10, 0.14); --danger: #ff6961; --danger-soft: rgba(255, 69, 58, 0.16); --playhead: #f5f5f7; --chart-grid: rgba(255, 255, 255, 0.06); --chart-axis: rgba(255, 255, 255, 0.14); --chart-1: #0a84ff; --chart-2: #30d158; --chart-3: #bf5af2; --chart-4: #64d2ff; --chart-5: #ff375f; --chart-6: #7d7aff; --chart-7: #ac8e68; --chart-8: #40c8e0; --chart-9: #ffd60a; --chart-10: #a3c76b; --shadow-float: 0 0 0 0.5px rgba(255, 255, 255, 0.1), 0 10px 32px -6px rgba(0, 0, 0, 0.6), 0 2px 8px -2px rgba(0, 0, 0, 0.4); --shadow-card: none; --scrollbar: rgba(255, 255, 255, 0.18); } @theme inline { --color-bg: var(--bg); --color-bg-subtle: var(--bg-subtle); --color-surface: var(--surface); --color-surface-muted: var(--surface-muted); --color-surface-raised: var(--surface-raised); --color-media: var(--media); --color-fg: var(--fg); --color-fg-secondary: var(--fg-secondary); --color-fg-tertiary: var(--fg-tertiary); --color-separator: var(--separator); --color-separator-strong: var(--separator-strong); --color-fill: var(--fill); --color-fill-hover: var(--fill-hover); --color-fill-press: var(--fill-press); --color-primary: var(--primary); --color-on-primary: var(--on-primary); --color-accent: var(--accent); --color-accent-fg: var(--accent-fg); --color-accent-soft: var(--accent-soft); --color-flag: var(--flag); --color-flag-fg: var(--flag-fg); --color-flag-soft: var(--flag-soft); --color-success: var(--success); --color-success-soft: var(--success-soft); --color-warning: var(--warning); --color-warning-soft: var(--warning-soft); --color-danger: var(--danger); --color-danger-soft: var(--danger-soft); --font-sans: var(--font-inter), -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", system-ui, sans-serif; --font-mono: var(--font-geist-mono), "SF Mono", ui-monospace, Menlo, monospace; /* 11px caption; 13px is the workhorse secondary size */ --text-2xs: 0.6875rem; --text-2xs--line-height: 1rem; --text-13: 0.8125rem; --text-13--line-height: 1.125rem; /* Tracking is size-specific (SF-style): small text opens up slightly, large text tightens. Body (14px) keeps the -0.006em set on . */ --text-2xs--letter-spacing: 0.01em; --text-xs--letter-spacing: 0.005em; --text-13--letter-spacing: -0.002em; --text-lg--letter-spacing: -0.012em; --text-xl--letter-spacing: -0.016em; --text-2xl--letter-spacing: -0.02em; --text-3xl--letter-spacing: -0.024em; --text-4xl--letter-spacing: -0.026em; --text-5xl--letter-spacing: -0.03em; --text-6xl--letter-spacing: -0.032em; /* Every bare transition-colors / transition-opacity runs on the hover token and house curve. */ --default-transition-duration: var(--dur-fast); --default-transition-timing-function: var(--ease-out); --radius-sm: var(--radius-sm); --radius-md: var(--radius); --radius-lg: var(--radius-lg); --radius-xl: var(--radius-xl); --shadow-float: var(--shadow-float); --shadow-card: var(--shadow-card); --ease-out: var(--ease-out); --ease-spring: var(--ease-spring); } /* Compatibility: the previous token names, mapped onto the new system so untouched components keep working. New code uses the names above. */ :root, :root.dark { --surface-0: var(--bg); --surface-1: var(--surface); --surface-2: var(--surface-muted); --fg-soft: var(--fg); --fg-muted: var(--fg-secondary); --fg-faint: var(--fg-tertiary); --line: var(--separator); --line-subtle: var(--separator); --line-strong: var(--separator-strong); --fill-strong: var(--fill-hover); --accent-ring: var(--focus-ring); --playhead-glow: transparent; } @theme inline { --color-surface-0: var(--bg); --color-surface-1: var(--surface); --color-surface-2: var(--surface-muted); --color-fg-soft: var(--fg); --color-fg-muted: var(--fg-secondary); --color-fg-faint: var(--fg-tertiary); --color-line: var(--separator); --color-line-subtle: var(--separator); --color-line-strong: var(--separator-strong); --color-fill-strong: var(--fill-hover); } html { font-size: 16px; /* Phones would flash a grey rectangle over pill buttons; feedback is the press state. */ -webkit-tap-highlight-color: transparent; } body { background: var(--bg); color: var(--fg); font-size: 0.875rem; /* 14px UI text */ letter-spacing: -0.006em; font-feature-settings: "cv11" 1, "ss01" 1, "ss03" 1; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-rendering: optimizeLegibility; } ::selection { background: var(--accent-soft); } /* Lucide icons at a lighter 1.75 stroke: closer to SF Symbols. */ svg[stroke-width="2"] { stroke-width: 1.75; } /* Focus: a soft accent halo, keyboard only. Base layer, so components can restyle it, never remove it without a replacement. */ @layer base { :where( a, button, input, select, textarea, summary, [tabindex], [role="button"] ):focus-visible { outline: none; box-shadow: 0 0 0 3px var(--focus-ring); } input[type="range"]:focus-visible { box-shadow: none; } input[type="range"]:focus-visible::-webkit-slider-thumb { box-shadow: 0 0 0 4px var(--focus-ring); } input[type="range"]:focus-visible::-moz-range-thumb { box-shadow: 0 0 0 4px var(--focus-ring); } } /* Numeric readouts always use tabular figures so rows don't jitter. */ .tabular, [data-tabular] { font-variant-numeric: tabular-nums; } /* Titles: tight tracking, like SF Display. */ .title { letter-spacing: -0.022em; font-weight: 600; } /* Quiet section label (sentence case, never uppercase). */ .label { font-size: var(--text-13); line-height: var(--text-13--line-height); font-weight: 500; color: var(--fg-secondary); } /* Frosted floating material: menus, popovers, the playback bar. */ @utility glass { background: var(--surface-raised); -webkit-backdrop-filter: saturate(180%) blur(20px); backdrop-filter: saturate(180%) blur(20px); box-shadow: var(--shadow-float); } /* Translucent chrome bar (top bar). */ @utility chrome { background: color-mix(in srgb, var(--bg) 80%, transparent); -webkit-backdrop-filter: saturate(180%) blur(20px); backdrop-filter: saturate(180%) blur(20px); } @layer components { /* Content card: hairline, large radius, flat. */ .card { @apply rounded-lg border border-separator bg-surface shadow-card; } /* Kept for components that still use the old helper names. */ .panel { @apply rounded-lg border border-separator bg-surface; } .panel-raised { @apply glass rounded-xl; } /* Buttons — pills, no borders, a gentle press. Variants: .btn (tinted secondary), .btn-primary (solid monochrome: the one main action), .btn-ghost (toolbar), .btn-active (selected toggle), .btn-flagged (flag filters/actions), .btn-danger, .btn-icon (circle). Heights: .btn 32px; .btn-sm 28px; .btn-lg 40px. */ .btn { @apply inline-flex h-8 cursor-pointer items-center justify-center gap-1.5 rounded-full bg-fill px-3.5 text-13 font-medium whitespace-nowrap text-fg select-none; transition: background-color var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out), transform var(--dur-fast) var(--ease-out), box-shadow var(--dur-fast) var(--ease-out), opacity var(--dur-fast) var(--ease-out); } .btn:hover { @apply bg-fill-hover; } .btn:active { @apply bg-fill-press; transform: scale(0.97); } .btn:disabled { @apply pointer-events-none cursor-not-allowed opacity-40; } .btn-sm { @apply h-7 gap-1 px-3 text-2xs; } .btn-lg { @apply h-10 px-5 text-sm; } .btn-primary, .btn-primary:hover { @apply bg-primary text-on-primary; } .btn-primary:hover { opacity: 0.86; } .btn-primary:active { @apply bg-primary; opacity: 0.8; } .btn-ghost { @apply bg-transparent text-fg-secondary; } .btn-ghost:hover { @apply bg-fill text-fg; } .btn-active, .btn-active:hover { @apply bg-accent-soft text-accent-fg; } .btn-flagged, .btn-flagged:hover { @apply bg-flag-soft text-flag-fg; } .btn-danger, .btn-danger:hover { @apply bg-danger-soft text-danger; } .btn-icon { @apply aspect-square px-0; } /* Text inputs and selects. */ .field { @apply h-8 rounded-md border border-transparent bg-surface-muted px-3 text-sm text-fg placeholder:text-fg-tertiary; transition: background-color var(--dur-fast) var(--ease-out), border-color var(--dur-fast) var(--ease-out), box-shadow var(--dur-fast) var(--ease-out); } .field:hover { @apply bg-fill-hover; } .field:focus, .field:focus-visible { @apply border-accent bg-surface; outline: none; box-shadow: 0 0 0 3px var(--focus-ring); } /* Loading placeholder. */ .skeleton { position: relative; overflow: hidden; background: var(--fill); border-radius: var(--radius); } .skeleton::after { content: ""; position: absolute; inset: 0; transform: translateX(-100%); background: linear-gradient( 90deg, transparent, var(--fill-hover), transparent ); animation: shimmer 1.6s linear infinite; } } @keyframes shimmer { to { transform: translateX(100%); } } /* Range sliders: thin monochrome track, white knob with a soft shadow (QuickTime / iOS). The filled part is drawn by the component with a background gradient on --range-fill when it wants one. */ input[type="range"].range { -webkit-appearance: none; appearance: none; height: 20px; background: transparent; cursor: pointer; } input[type="range"].range::-webkit-slider-runnable-track { height: 4px; border-radius: 999px; background: linear-gradient( to right, var(--playhead) 0 var(--range-fill, 0%), var(--fill-press) var(--range-fill, 0%) 100% ); } input[type="range"].range::-moz-range-track { height: 4px; border-radius: 999px; background: var(--fill-press); } input[type="range"].range::-moz-range-progress { height: 4px; border-radius: 999px; background: var(--playhead); } input[type="range"].range::-webkit-slider-thumb { -webkit-appearance: none; margin-top: -6px; width: 16px; height: 16px; border-radius: 999px; background: #ffffff; box-shadow: 0 0 0 0.5px rgba(0, 0, 0, 0.12), 0 2px 6px rgba(0, 0, 0, 0.22); transition: transform var(--dur-fast) var(--ease-out); } input[type="range"].range::-moz-range-thumb { width: 16px; height: 16px; border: none; border-radius: 999px; background: #ffffff; box-shadow: 0 0 0 0.5px rgba(0, 0, 0, 0.12), 0 2px 6px rgba(0, 0, 0, 0.22); } input[type="range"].range:active::-moz-range-thumb { transform: scale(1.12); } input[type="range"].range:active::-webkit-slider-thumb { transform: scale(1.12); } /* Scrollbars: thin, overlay-like. */ * { scrollbar-width: thin; scrollbar-color: var(--scrollbar) transparent; } *::-webkit-scrollbar { width: 8px; height: 8px; } *::-webkit-scrollbar-track { background: transparent; } *::-webkit-scrollbar-thumb { background: transparent; border-radius: 8px; border: 2px solid transparent; background-clip: padding-box; } *:hover::-webkit-scrollbar-thumb { background-color: var(--scrollbar); } /* ── Motion ─────────────────────────────────────────────────────── */ @keyframes fadeInUp { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: translateY(0); } } .animate-fade-in-up { animation: fadeInUp var(--dur-slow) var(--ease-out) both; } @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } .animate-fade-in { animation: fadeIn var(--dur) var(--ease-out) both; } .animate-fade-in-late { animation: fadeInUp var(--dur-slow) var(--ease-out) 0.25s both; } /* Menus and popovers: a small spring from just above. */ @keyframes menuPop { from { opacity: 0; transform: translateY(-4px) scale(0.98); } to { opacity: 1; transform: translateY(0) scale(1); } } .animate-menu-pop { animation: menuPop var(--dur) var(--ease-spring) both; transform-origin: top center; } /* ── Shared transitions (transitions.dev patterns on the house tokens) ── All use the `scale` / `opacity` properties and CSS transitions, never keyframes, for anything that can be reopened mid-exit: a transition retargets from the live value, so it reverses smoothly (apple-design: interruptible). Drive them with `data-state` from usePresence(). */ /* Surface that grows from its trigger (menus, popovers). Set transform-origin to the trigger. */ .t-pop { transition: opacity var(--duration-fast) var(--ease-out), scale var(--duration-fast) var(--ease-out); } .t-pop[data-state="open"] { opacity: 1; scale: 1; @starting-style { opacity: 0; scale: var(--scale-medium); } } .t-pop[data-state="closing"] { opacity: 0; scale: var(--scale-tiny); transition-duration: var(--duration-quick); pointer-events: none; } /* Centred overlay content (enlarged camera, fullscreen insight). */ .t-modal { transition: opacity var(--duration-fast) var(--ease-out), scale var(--duration-fast) var(--ease-out); } .t-modal[data-state="open"] { opacity: 1; scale: 1; @starting-style { opacity: 0; scale: var(--scale-large); } } .t-modal[data-state="closing"] { opacity: 0; scale: var(--scale-large); transition-duration: var(--duration-quick); pointer-events: none; } /* For an element that is re-classed rather than mounted (no @starting-style to hook). */ @keyframes t-modal-in { from { opacity: 0; scale: var(--scale-large); } } .t-modal-in { animation: t-modal-in var(--duration-fast) var(--ease-out) both; } /* Opacity-only enter/exit: scrims, veils, backdrops (never animate the blur radius). */ .t-fade { transition: opacity var(--duration-fast) var(--ease-out); } .t-fade[data-state="open"] { opacity: 1; @starting-style { opacity: 0; } } .t-fade[data-state="closing"] { opacity: 0; transition-duration: var(--duration-quick); pointer-events: none; } /* Skeleton -> content: play only on the loading -> loaded change, not on every mount. */ @keyframes t-reveal { from { opacity: 0; } } .t-reveal { animation: t-reveal var(--duration-slow) ease-in-out both; } /* Two icons in one slot cross-fade with blur + scale (IconSwap in ui.tsx). */ .icon-swap { display: inline-grid; place-items: center; } .icon-swap > [data-slot] { grid-area: 1 / 1; display: inline-flex; transition: opacity var(--duration-fast) ease-in-out, scale var(--duration-fast) ease-in-out, filter var(--duration-fast) ease-in-out; } .icon-swap[data-state="on"] > [data-slot="off"], .icon-swap[data-state="off"] > [data-slot="on"] { opacity: 0; scale: 0.25; filter: blur(var(--blur-small)); } .icon-swap[data-instant] > [data-slot] { transition: none; } /* In-flow disclosure that grows / shrinks (Collapse in ui.tsx). Padding goes on the inner element, never the track, or a strip stays visible when closed. */ .collapse-track { display: grid; grid-template-rows: 0fr; transition: grid-template-rows var(--duration-fast) var(--ease-out); } .collapse-track[data-state="open"] { grid-template-rows: 1fr; } .collapse-inner { min-height: 0; overflow: hidden; opacity: 0; transition: opacity var(--duration-fast) var(--ease-out); } .collapse-track[data-state="open"] > .collapse-inner { opacity: 1; } /* One frame without transitions while the theme flips, so 120ms control fades can't lag the instantly re-themed canvas (utils/theme.ts adds and removes it). */ .theme-switching, .theme-switching *, .theme-switching *::before, .theme-switching *::after { transition: none !important; } /* ── Accessibility preferences ──────────────────────────────────── */ /* Reduced motion is not "no feedback": state changes are instant and moving keyframes become a short fade, but spinners keep turning so a load never looks hung. */ @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; scroll-behavior: auto !important; } .animate-spin { animation-duration: 900ms !important; animation-iteration-count: infinite !important; } .animate-menu-pop, .animate-fade-in-up, .animate-fade-in-late, .t-modal-in, .t-reveal { animation-name: fadeIn !important; animation-duration: var(--dur) !important; } .skeleton::after { animation: none !important; } } /* Reduced transparency: frosted surfaces become solid. */ @media (prefers-reduced-transparency: reduce) { :root, :root.dark { --surface-raised: var(--surface); } .glass, .chrome { -webkit-backdrop-filter: none !important; backdrop-filter: none !important; } .chrome { background: var(--bg) !important; } } /* More contrast: firmer separators and secondary text, and an edge on glass. */ @media (prefers-contrast: more) { :root { --separator: rgba(0, 0, 0, 0.2); --separator-strong: rgba(0, 0, 0, 0.32); --fg-secondary: #45454b; --surface-raised: var(--surface); } :root.dark { --separator: rgba(255, 255, 255, 0.22); --separator-strong: rgba(255, 255, 255, 0.34); --fg-secondary: #c7c7cc; --surface-raised: var(--surface); } .glass { outline: 1px solid var(--separator-strong); } } @media (forced-colors: active) { .glass, .card { border: 1px solid CanvasText; } }