ui updates

This commit is contained in:
Siddharth
2025-11-16 01:27:03 -07:00
parent 6287fa90c8
commit c080168fb5
11 changed files with 161 additions and 118 deletions
+5 -2
View File
@@ -9,7 +9,8 @@ const Switch = React.forwardRef<
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50",
"data-[state=checked]:bg-[#7c3aed] data-[state=unchecked]:bg-[#23232a]",
className
)}
{...props}
@@ -17,7 +18,9 @@ const Switch = React.forwardRef<
>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0"
"pointer-events-none block h-4 w-4 rounded-full shadow-lg ring-0 transition-transform",
"bg-[#f5f5f7] dark:bg-[#23232a]",
"data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitives.Root>
@@ -28,11 +28,11 @@ export default function PlaybackControls({
}
return (
<div className="flex items-center gap-4 px-4">
<div className="flex items-center gap-4 px-4 rounded-xl py-3">
<Button
onClick={onTogglePlayPause}
size="icon"
className="w-8 h-8 rounded-full bg-primary text-primary-foreground hover:bg-primary/90 transition-colors shadow-md"
className="w-8 h-8 rounded-full bg-transparent text-slate-200 hover:bg-[#18181b] transition-colors border border-white"
aria-label={isPlaying ? 'Pause' : 'Play'}
>
{isPlaying ? (
@@ -41,7 +41,7 @@ export default function PlaybackControls({
<MdPlayArrow width={18} height={18} />
)}
</Button>
<span className="text-xs text-muted-foreground font-mono">
<span className="text-xs text-slate-400 font-mono">
{formatTime(currentTime)}
</span>
<input
@@ -51,12 +51,12 @@ export default function PlaybackControls({
value={currentTime}
onChange={handleSeekChange}
step="0.01"
className="flex-1 h-2 accent-blue-500 rounded-full transition-all duration-[33ms]"
className="flex-1 h-2 rounded-full transition-all duration-[33ms] custom-playback-range"
style={{
background: `linear-gradient(to right, #3b82f6 0%, #3b82f6 ${(currentTime / duration) * 100}%, #e5e7eb ${(currentTime / duration) * 100}%, #e5e7eb 100%)`
}}
background: `linear-gradient(to right, #7c3aed 0%, #7c3aed ${(currentTime / duration) * 100}%, #23232a ${(currentTime / duration) * 100}%, #23232a 100%)`,
}}
/>
<span className="text-xs text-muted-foreground font-mono">
<span className="text-xs text-slate-400 font-mono">
{formatTime(duration)}
</span>
</div>
+40 -38
View File
@@ -64,12 +64,12 @@ export function SettingsPanel({ selected, onWallpaperChange, selectedZoomDepth,
};
return (
<div className="flex-[3] min-w-0 bg-card border border-border rounded-xl p-8 flex flex-col shadow-sm">
<div className="flex-[3] min-w-0 bg-[#18181b] border border-[#23232a] rounded-xl p-8 flex flex-col shadow-lg">
<div className="mb-6">
<div className="flex items-center justify-between mb-3">
<span className="text-sm font-semibold text-slate-600">Zoom Level</span>
<span className="text-sm font-semibold text-slate-200">Zoom Level</span>
{zoomEnabled && selectedZoomDepth && (
<span className="text-xs uppercase tracking-wide text-slate-400">
<span className="text-xs uppercase tracking-wide text-slate-400/80">
Active · {ZOOM_DEPTH_OPTIONS.find(o => o.depth === selectedZoomDepth)?.label}
</span>
)}
@@ -81,32 +81,32 @@ export function SettingsPanel({ selected, onWallpaperChange, selectedZoomDepth,
<Button
key={option.depth}
type="button"
variant="outline"
disabled={!zoomEnabled}
onClick={() => onZoomDepthChange?.(option.depth)}
className={cn(
"h-auto w-full rounded-lg border bg-muted/30 px-2 py-2.5 text-center shadow-sm transition-all",
"flex flex-col items-center justify-center gap-0.5",
"h-auto w-full rounded-xl border px-2 py-3 text-center shadow-lg transition-all flex flex-col items-center justify-center gap-1",
"duration-150 ease-in-out",
zoomEnabled ? "opacity-100" : "opacity-60",
isActive
? "border-primary/70 bg-primary/10 text-primary shadow-primary/20"
: "border-border/60 hover:border-primary/40 hover:bg-muted/60"
? "border-[#7c3aed] bg-white text-black shadow-[#7c3aed]/20 scale-105"
: "border-[#23232a] bg-[#23232a] text-slate-200 hover:border-[#7c3aed] hover:scale-105"
)}
style={isActive ? { background: '#fff', color: '#111' } : undefined}
>
<span className="text-xs font-semibold tracking-tight">{option.label}</span>
<span className={cn("text-sm font-semibold tracking-tight", isActive ? "text-black" : "text-slate-200")}>{option.label}</span>
</Button>
);
})}
</div>
{!zoomEnabled && (
<p className="text-xs text-slate-400 mt-2">Select a zoom item in the timeline to adjust its depth.</p>
<p className="text-xs text-slate-400/80 mt-2">Select a zoom item in the timeline to adjust its depth.</p>
)}
{zoomEnabled && (
<Button
onClick={handleDeleteClick}
variant="destructive"
size="sm"
className="mt-3 w-full gap-2"
className="mt-3 w-full gap-2 bg-[#7c3aed] text-white border-none hover:bg-[#a78bfa]"
>
<Trash2 className="w-4 h-4" />
Delete Zoom
@@ -120,14 +120,14 @@ export function SettingsPanel({ selected, onWallpaperChange, selectedZoomDepth,
checked={showShadow}
onCheckedChange={onShadowChange}
/>
<div className="text-sm">Shadow</div>
<div className="text-sm text-slate-200">Shadow</div>
</div>
<div className="flex items-center gap-2">
<Switch
checked={showBlur}
onCheckedChange={onBlurChange}
/>
<div className="text-sm">Blur Background</div>
<div className="text-sm text-slate-200">Blur Background</div>
</div>
</div>
</div>
@@ -135,7 +135,7 @@ export function SettingsPanel({ selected, onWallpaperChange, selectedZoomDepth,
<Button
onClick={() => setShowCropDropdown(!showCropDropdown)}
variant="outline"
className="w-full gap-2"
className="w-full gap-2 bg-[#23232a] text-slate-200 border-none"
>
<Crop className="w-4 h-4" />
Crop Video
@@ -145,20 +145,20 @@ export function SettingsPanel({ selected, onWallpaperChange, selectedZoomDepth,
{showCropDropdown && cropRegion && onCropChange && (
<>
<div
className="fixed inset-0 bg-black/70 backdrop-blur-sm z-40 animate-in fade-in duration-200"
className="fixed inset-0 bg-black/80 backdrop-blur-sm z-40 animate-in fade-in duration-200"
onClick={() => setShowCropDropdown(false)}
/>
<div className="fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-50 bg-card rounded-2xl shadow-2xl border border-border/50 p-8 w-[90vw] max-w-5xl animate-in zoom-in-95 duration-200">
<div className="fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-50 bg-[#23232a] rounded-2xl shadow-2xl border border-[#312e81] p-8 w-[90vw] max-w-5xl animate-in zoom-in-95 duration-200">
<div className="flex items-center justify-between mb-6">
<div>
<span className="text-xl font-bold text-foreground">Crop Video</span>
<p className="text-sm text-muted-foreground mt-2">Drag on each side to adjust the crop area</p>
<span className="text-xl font-bold text-slate-200">Crop Video</span>
<p className="text-sm text-slate-400 mt-2">Drag on each side to adjust the crop area</p>
</div>
<Button
variant="ghost"
size="icon"
onClick={() => setShowCropDropdown(false)}
className="hover:bg-muted"
className="hover:bg-[#312e81] text-slate-200"
>
<X className="w-5 h-5" />
</Button>
@@ -179,11 +179,11 @@ export function SettingsPanel({ selected, onWallpaperChange, selectedZoomDepth,
</div>
</>
)}
<Tabs defaultValue="image" className="mb-6">
<TabsList className="mb-4">
<TabsTrigger value="image">Image</TabsTrigger>
<TabsTrigger value="color">Color</TabsTrigger>
<TabsTrigger value="gradient">Gradient</TabsTrigger>
<Tabs defaultValue="image" className="mb-6 text-slate-200">
<TabsList className="mb-4 bg-[#23232a] border-none text-slate-200">
<TabsTrigger value="image" className="text-slate-200">Image</TabsTrigger>
<TabsTrigger value="color" className="text-slate-200">Color</TabsTrigger>
<TabsTrigger value="gradient" className="text-slate-200">Gradient</TabsTrigger>
</TabsList>
<TabsContent value="image">
@@ -194,8 +194,8 @@ export function SettingsPanel({ selected, onWallpaperChange, selectedZoomDepth,
className={cn(
"aspect-square rounded-lg border-2 overflow-hidden cursor-pointer transition-all w-16 h-16",
selected === path
? "border-primary/40 ring-1 ring-primary/40 scale-105"
: "border-border hover:border-primary/60 hover:scale-105"
? "border-[#7c3aed] ring-1 ring-[#7c3aed] scale-105"
: "border-[#23232a] hover:border-[#7c3aed] hover:scale-105"
)}
style={{ backgroundImage: `url(${path})`, backgroundSize: "cover", backgroundPosition: "center" }}
aria-label={`Wallpaper ${idx + 1}`}
@@ -207,14 +207,16 @@ export function SettingsPanel({ selected, onWallpaperChange, selectedZoomDepth,
</TabsContent>
<TabsContent value="color">
<Colorful
color={hsva}
disableAlpha={true}
onChange={(color) => {
setHsva(color.hsva);
onWallpaperChange(hsvaToHex(color.hsva));
}}
/>
<div className="p-2">
<Colorful
color={hsva}
disableAlpha={true}
onChange={(color) => {
setHsva(color.hsva);
onWallpaperChange(hsvaToHex(color.hsva));
}}
/>
</div>
</TabsContent>
<TabsContent value="gradient">
@@ -224,7 +226,7 @@ export function SettingsPanel({ selected, onWallpaperChange, selectedZoomDepth,
key={g}
className={cn(
"aspect-square rounded-lg border-2 overflow-hidden cursor-pointer transition-all w-16 h-16",
gradient === g ? "border-primary ring-1 ring-primary/40 scale-105" : "border-border hover:border-primary/60 hover:scale-105"
gradient === g ? "border-[#7c3aed] ring-1 ring-[#7c3aed] scale-105" : "border-[#23232a] hover:border-[#7c3aed] hover:scale-105"
)}
style={{ background: g }}
aria-label={`Gradient ${idx + 1}`}
@@ -239,7 +241,7 @@ export function SettingsPanel({ selected, onWallpaperChange, selectedZoomDepth,
<Button
type="button"
size="lg"
className="w-full py-5 text-lg flex items-center justify-center gap-3 bg-primary text-white rounded-xl shadow-lg hover:bg-primary/90 transition-all"
className="w-full py-5 text-lg flex items-center justify-center gap-3 bg-[#7c3aed] text-white rounded-xl shadow-lg hover:bg-[#a78bfa] transition-all"
>
<Download className="w-6 h-6" />
<span className="text-lg">Export Video</span>
@@ -249,9 +251,9 @@ export function SettingsPanel({ selected, onWallpaperChange, selectedZoomDepth,
onClick={() => {
window.electronAPI?.openExternalUrl('https://github.com/siddharthvaddem/pangolin/issues/new');
}}
className="w-full mt-3 flex items-center justify-center gap-1 text-[10px] text-muted-foreground/60 hover:text-muted-foreground/90 transition-colors py-1"
className="w-full mt-3 flex items-center justify-center gap-1 text-[10px] text-slate-400/80 hover:text-slate-200 transition-colors py-1"
>
<Bug className="w-3 h-3 text-black" />
<Bug className="w-3 h-3 text-white" />
<span>Report Bug</span>
</button>
</div>
+3 -3
View File
@@ -171,10 +171,10 @@ export default function VideoEditor() {
}
return (
<div className="flex h-screen bg-background p-8 gap-8">
<div className="flex h-screen bg-background bg-black p-8 gap-8">
<Toaster position="top-center" />
<div className="flex flex-col flex-[7] min-w-0 gap-8">
<div className="flex flex-col gap-6 flex-1">
<div className="flex flex-col flex-[7] min-w-0 gap-6">
<div className="flex flex-col gap-3 flex-1">
{videoPath && (
<>
<div className="flex justify-center w-full">
@@ -1,3 +1,11 @@
.itemDark {
background: #23232a;
border: 1px solid #312e81;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0,0,0,0.18);
color: #e5e7eb;
transition: box-shadow 0.2s, border 0.2s, background 0.2s;
}
.squircle {
border-radius: 12px;
-corner-smoothing: antialiased;
@@ -26,6 +26,7 @@ export default function Item({ id, span, rowId, isSelected = false, onSelect }:
{...listeners}
{...attributes}
onPointerDownCapture={() => onSelect?.()}
className={cn(glassStyles.itemDark)}
>
<div style={itemContentStyle}>
<div
@@ -2,9 +2,10 @@
position: relative;
border-radius: 12px;
-corner-smoothing: antialiased;
background: radial-gradient(circle at 60% 55%, rgba(104, 61, 196, 0.92) 60%, rgba(60, 20, 120, 0.85) 100%);
background: #7c3aed;
border: none;
box-shadow: 0 2px 8px 0 rgba(88,36,204,0.10) inset, 0 1px 3px 0 rgba(0,0,0,0.10);
box-shadow: 0 2px 12px 0 rgba(88,36,204,0.14) inset, 0 2px 8px 0 rgba(0,0,0,0.10), 0 1px 6px 0 rgba(124,58,237,0.08);
margin: 0 4px;
backdrop-filter: blur(2px) saturate(120%);
-webkit-backdrop-filter: blur(2px) saturate(120%);
}
@@ -12,13 +13,13 @@
.zoomEndCap {
position: absolute;
top: 0;
background: #3c3c3c;
width: 18px;
height: 100%;
background: #361e5a;
width: 18px;
height: 100%;
pointer-events: none;
z-index: 2;
transition: background 0.2s, box-shadow 0.2s;
}
.zoomEndCap.left {
+1 -1
View File
@@ -10,7 +10,7 @@ export default function Row({ id, children }: RowProps) {
return (
<div
className="border-b border-slate-100 bg-gradient-to-b from-slate-50/30 to-white/50"
className="border-b border-[#18181b] bg-[#18181b]"
style={{ ...rowWrapperStyle, minHeight: 88 }}
>
<div ref={setNodeRef} style={rowStyle}>
@@ -4,7 +4,7 @@ interface SubrowProps {
export default function Subrow({ children }: SubrowProps) {
return (
<div style={{ height: 50, position: "relative" }}>
<div className={cn("flex items-center min-h-[32px] gap-1 px-2 py-0.5 bg-[#23232a] rounded-md text-slate-300")}>
{children}
</div>
);
@@ -151,27 +151,42 @@ function PlaybackCursor({
<div
className="absolute top-0 bottom-0 pointer-events-none z-50"
style={{
[sideProperty === "right" ? "marginRight" : "marginLeft"]: `${sidebarWidth}px`,
[sideProperty === "right" ? "marginRight" : "marginLeft"]: `${sidebarWidth - 8}px`, // reduce margin
}}
>
<div
className="absolute top-0 bottom-0 w-[2px] bg-red-500/90 shadow-[0_0_8px_rgba(239,68,68,0.5)]"
className="absolute top-3 bottom-3 w-[2px] bg-red-500/90 shadow-[0_0_8px_rgba(239,68,68,0.5)]"
style={{
[sideProperty]: `${offset}px`,
}}
>
{/* Inverted triangle at top */}
<div
className="absolute -top-0.5 -left-[5px] w-0 h-0"
style={{
borderLeft: '6px solid transparent',
borderRight: '6px solid transparent',
borderTop: '8px solid rgb(239 68 68)',
filter: 'drop-shadow(0 2px 4px rgba(0,0,0,0.3))',
}}
/>
<div
className="absolute -top-2 left-1/2 -translate-x-1/2 flex flex-col items-center"
style={{ width: '32px' }}
>
<div
style={{
width: '8px',
height: '8px',
background: '#ef4444',
borderRadius: '12px 12px 12px 12px/14px 14px 8px 8px',
boxShadow: '0 2px 8px rgba(0,0,0,0.10)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '11px',
fontWeight: 600,
color: '#ef4444',
letterSpacing: '-0.5px',
position: 'relative',
}}
>
</div>
</div>
{/* Subtle glow at top */}
<div className="absolute -top-1 left-1/2 -translate-x-1/2 w-3 h-3 bg-red-500/30 rounded-full blur-sm" />
<div className="absolute -top-1 left-1/2 -translate-x-1/2 w-2 h-2 bg-red-500/30 rounded-full blur-sm" />
</div>
</div>
);
@@ -180,9 +195,11 @@ function PlaybackCursor({
function TimelineAxis({
intervalMs,
videoDurationMs,
currentTimeMs,
}: {
intervalMs: number;
videoDurationMs: number;
currentTimeMs: number;
}) {
const { sidebarWidth, direction, range, valueToPixels } = useTimelineContext();
const sideProperty = direction === "rtl" ? "right" : "left";
@@ -225,7 +242,7 @@ function TimelineAxis({
return (
<div
className="h-10 bg-gradient-to-b from-slate-50 to-slate-100/50 border-b border-slate-200/60 relative overflow-hidden"
className="h-10 bg-black border-b border-[#18181b] relative overflow-hidden"
style={{
[sideProperty === "right" ? "marginRight" : "marginLeft"]: `${sidebarWidth}px`,
}}
@@ -244,24 +261,33 @@ function TimelineAxis({
return (
<div key={marker.time} style={markerStyle}>
<div
style={{
width: "1px",
height: "60%",
backgroundColor: "#cbd5e1",
opacity: 0.5,
}}
/>
<span
style={{
paddingLeft: "4px",
alignSelf: "flex-start",
paddingTop: "3px",
}}
className="text-[10px] text-slate-500 font-medium select-none tracking-tight"
>
{marker.label}
</span>
<div style={{ display: 'flex', alignItems: 'center', height: '100%' }}>
<div
style={{
width: '5px',
height: '5px',
borderRadius: '50%',
backgroundColor: marker.time === currentTimeMs ? '#7c3aed' : '#94a3b8',
boxShadow: marker.time === currentTimeMs ? '0 0 4px #7c3aed55' : 'none',
marginRight: '5px',
marginTop: '2px',
transition: 'background 0.2s, box-shadow 0.2s',
}}
/>
<span
style={{
fontWeight: marker.time === currentTimeMs ? 700 : 500,
color: marker.time === currentTimeMs ? '#7c3aed' : '#94a3b8',
fontSize: '11px',
letterSpacing: '-0.5px',
textShadow: marker.time === currentTimeMs ? '0 1px 6px #7c3aed33' : 'none',
marginTop: '2px',
}}
className="select-none"
>
{marker.label}
</span>
</div>
</div>
);
})}
@@ -273,7 +299,7 @@ function Timeline({
items,
videoDurationMs,
intervalMs,
currentTimeMs,
currentTimeMs,
onSeek,
onSelectZoom,
selectedZoomId,
@@ -308,10 +334,10 @@ function Timeline({
<div
ref={setTimelineRef}
style={style}
className="select-none bg-white min-h-[120px] relative cursor-pointer"
className="select-none bg-black min-h-[120px] relative cursor-pointer"
onClick={handleTimelineClick}
>
<TimelineAxis intervalMs={intervalMs} videoDurationMs={videoDurationMs} />
<TimelineAxis intervalMs={intervalMs} videoDurationMs={videoDurationMs} currentTimeMs={currentTimeMs} />
<PlaybackCursor currentTimeMs={currentTimeMs} videoDurationMs={videoDurationMs} />
<Row id={ROW_ID}>
{items.map((item) => (
@@ -338,7 +364,6 @@ export default function TimelineEditor({
zoomRegions,
onZoomAdded,
onZoomSpanChange,
// Removed unused onZoomDelete prop
selectedZoomId,
onSelectZoom,
}: TimelineEditorProps) {
@@ -445,33 +470,33 @@ export default function TimelineEditor({
if (!videoDuration || videoDuration === 0) {
return (
<div className="flex-1 flex items-center justify-center bg-gray-50 border border-gray-300 rounded-lg">
<span className="text-gray-500 text-sm">Load a video to see timeline</span>
<div className="flex-1 flex items-center justify-center rounded-lg">
<span className="text-slate-400 text-sm">Load a video to see timeline</span>
</div>
);
}
return (
<div className="flex-1 flex flex-col bg-white border border-slate-200 rounded-xl shadow-sm overflow-hidden">
<div className="flex items-center gap-3 px-4 py-2.5 border-b border-slate-100 bg-gradient-to-b from-white to-slate-50/50">
<Button onClick={handleAddZoom} variant="outline" size="sm" className="gap-2 h-8 px-3 text-xs">
<Plus className="w-3.5 h-3.5" />
<div className="flex-1 flex flex-col bg-black border border-none rounded-xl shadow-lg overflow-hidden">
<div className="flex items-center gap-3 px-4 py-2.5">
<Button onClick={handleAddZoom} variant="outline" size="sm" className="gap-2 h-8 px-3 text-xs bg-[#23232a] border-none text-slate-200 hover:bg-white hover:text-black">
<Plus className="w-3.5 h-3.5 text-slate-400" />
Add Zoom
</Button>
<div className="flex-1" />
<div className="flex items-center gap-3 text-[10px] text-slate-400 font-medium">
<span className="flex items-center gap-1">
<kbd className="px-1.5 py-0.5 bg-slate-100 border border-slate-200 rounded text-slate-600">Command + Shift + Scroll</kbd>
<kbd className="px-1.5 py-0.5 bg-[#23232a] border border-[#312e81] rounded text-slate-300">Command + Shift + Scroll</kbd>
<span>Pan</span>
</span>
<span className="text-slate-300"></span>
<span className="text-slate-600"></span>
<span className="flex items-center gap-1">
<kbd className="px-1.5 py-0.5 bg-slate-100 border border-slate-200 rounded text-slate-600">Command + Scroll</kbd>
<kbd className="px-1.5 py-0.5 bg-[#23232a] border border-[#312e81] rounded text-slate-300">Command + Scroll</kbd>
<span>Zoom</span>
</span>
</div>
</div>
<div className="flex-1 overflow-x-auto overflow-y-hidden">
<div className="mt-4 flex-1 overflow-x-auto overflow-y-hidden bg-[#000]">
<TimelineWrapper
range={clampedRange}
videoDuration={videoDuration}
+17 -14
View File
@@ -87,39 +87,42 @@
width: 16px;
height: 16px;
border-radius: 50%;
background: #3b82f6;
background: #fff;
cursor: pointer;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
box-shadow: 0 2px 8px rgba(255,255,255,0.32);
border: 2px solid #fff;
transition: all 0.08s ease-out;
}
input[type="range"]::-webkit-slider-thumb:hover {
background: #2563eb;
background: #fff;
transform: scale(1.15);
box-shadow: 0 3px 10px rgba(59, 130, 246, 0.5);
box-shadow: 0 3px 10px rgba(255,255,255,0.5);
border-color: #7c3aed;
}
input[type="range"]::-webkit-slider-thumb:active {
transform: scale(1.25);
}
input[type="range"]::-moz-range-thumb {
width: 16px;
height: 16px;
border-radius: 50%;
background: #3b82f6;
background: #fff;
cursor: pointer;
border: none;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
border: 2px solid #fff;
box-shadow: 0 2px 8px rgba(255,255,255,0.32);
transition: all 0.08s ease-out;
}
input[type="range"]::-moz-range-thumb:hover {
background: #2563eb;
background: #fff;
transform: scale(1.15);
box-shadow: 0 3px 10px rgba(59, 130, 246, 0.5);
box-shadow: 0 3px 10px rgba(255,255,255,0.5);
border-color: #7c3aed;
}
input[type="range"]::-moz-range-thumb:active {
transform: scale(1.25);
}