diff --git a/content/homepage/timelines/introduction.md b/content/homepage/timelines/introduction.md index 30018ab..6023acb 100644 --- a/content/homepage/timelines/introduction.md +++ b/content/homepage/timelines/introduction.md @@ -36,5 +36,3 @@ Join a collaborative community of developers and media professionals! Contribute ## Resources & Documentation Access the full [documentation](#), API references, and community forums to unlock OTIO’s full potential. ---- - diff --git a/next.config.ts b/next.config.ts index f886d7e..e9ebcc5 100644 --- a/next.config.ts +++ b/next.config.ts @@ -41,10 +41,6 @@ const nextConfig: NextConfig = { { source: "/(.*)", headers: [ - { - key: "Cross-Origin-Embedder-Policy", - value: "require-corp", - }, { key: "Cross-Origin-Opener-Policy", value: "same-origin", diff --git a/package.json b/package.json index 0fdddee..5b8eb6d 100644 --- a/package.json +++ b/package.json @@ -84,5 +84,6 @@ }, "resolutions": { "string-width": "4.2.3" - } + }, + "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" } diff --git a/src/app/globals.css b/src/app/globals.css index 6a28b90..7cca588 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -187,19 +187,19 @@ --top-nav-height: 73px; --track-height: 38px; - --track-header-width: 120px; - - /* Track label colors - light mode matching clip colors (blue-250, teal-250, violet-250, rose-250, slate-250) */ - --track-label-h1: oklch(0.75 0.12 250); /* Blue - matches blue-200/300 gradient */ - --track-label-h2: oklch(0.75 0.10 180); /* Teal - matches teal-200/300 gradient */ - --track-label-h3: oklch(0.75 0.12 300); /* Violet - matches violet-200/300 gradient */ - --track-label-img: oklch(0.75 0.10 15); /* Rose - matches rose-200/300 gradient */ - --track-label-p: oklch(0.70 0.02 250); /* Slate - matches slate-200/300 gradient */ - --track-label-ul: oklch(0.75 0.12 85); /* Amber - matches amber-200/300 gradient */ - --track-label-embed: oklch(0.75 0.15 25); /* Red - matches red-200/300 gradient */ - + --track-header-width: 88px; + + /* Track label colors - light mode */ + --track-label-h1: oklch(0.72 0.14 250); /* Blue */ + --track-label-h2: oklch(0.72 0.12 175); /* Teal */ + --track-label-h3: oklch(0.72 0.14 295); /* Violet */ + --track-label-img: oklch(0.72 0.12 15); /* Rose */ + --track-label-p: oklch(0.72 0.03 250); /* Slate */ + --track-label-ul: oklch(0.72 0.14 85); /* Amber */ + --track-label-embed: oklch(0.72 0.16 25); /* Red */ + /* Track header background - light mode */ - --track-header-bg: oklch(0.85 0.01 240); /* Light gray-blue */ + --track-header-bg: oklch(0.94 0.005 250); /* Light cool gray */ } .dark { @@ -231,17 +231,17 @@ --input: 217.2 32.6% 17.5%; --ring: 212.7 26.8% 83.9%; - /* Track label colors - dark mode matching clip colors */ - --track-label-h1: oklch(0.50 0.15 250); /* Blue - matches dark mode blue-600/40-20 */ - --track-label-h2: oklch(0.50 0.12 180); /* Teal - matches dark mode teal-600/40-20 */ - --track-label-h3: oklch(0.50 0.15 300); /* Violet - matches dark mode violet-600/40-20 */ - --track-label-img: oklch(0.50 0.12 15); /* Rose - matches dark mode rose-600/40-20 */ - --track-label-p: oklch(0.45 0.08 250); /* Slate - matches dark mode slate-600/40-20 */ - --track-label-ul: oklch(0.50 0.15 85); /* Amber - matches dark mode amber-600/40-20 */ - --track-label-embed: oklch(0.50 0.18 25); /* Red - matches dark mode red-600/40-20 */ - + /* Track label colors - dark mode */ + --track-label-h1: oklch(0.48 0.16 250); /* Blue */ + --track-label-h2: oklch(0.48 0.13 175); /* Teal */ + --track-label-h3: oklch(0.48 0.16 295); /* Violet */ + --track-label-img: oklch(0.48 0.13 15); /* Rose */ + --track-label-p: oklch(0.43 0.06 250); /* Slate */ + --track-label-ul: oklch(0.48 0.16 85); /* Amber */ + --track-label-embed: oklch(0.48 0.19 25); /* Red */ + /* Track header background - dark mode */ - --track-header-bg: oklch(0.18 0.02 240); /* Darker gray-blue */ + --track-header-bg: oklch(0.15 0.015 260); /* Deep blue-gray */ } } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 36932da..cbabd78 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -47,15 +47,14 @@ export default function RootLayout({ }>) { return ( - + -
+
{children} diff --git a/src/components/layout/dark-mode.tsx b/src/components/layout/dark-mode.tsx index 29b2a6e..5977ef4 100644 --- a/src/components/layout/dark-mode.tsx +++ b/src/components/layout/dark-mode.tsx @@ -5,36 +5,20 @@ import { Moon, Sun } from "lucide-react"; import { useTheme } from "next-themes"; import { Button } from "@/components/ui/button"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; export function ModeToggle({ style }: { style?: React.CSSProperties }) { - const { setTheme } = useTheme(); + const { resolvedTheme, setTheme } = useTheme(); return ( - - - - - - setTheme("light")}> - Light - - setTheme("dark")}> - Dark - - setTheme("system")}> - System - - - + ); } diff --git a/src/components/nle/index.tsx b/src/components/nle/index.tsx index 6f2dc1d..b83a3a9 100644 --- a/src/components/nle/index.tsx +++ b/src/components/nle/index.tsx @@ -8,7 +8,7 @@ import { Button } from "@/components/ui/button"; import { KeyboardShortcutDisplay } from "@/components/nle/keyboard-shortcut-display"; import { ScrollContext } from "@/components/nle/scroll-context"; import { TimelineTicks } from "@/components/nle/timeline-ticks"; -import { Play, Pause, FastForward, Rewind, Monitor, Eye, ZoomIn, ZoomOut } from "lucide-react"; +import { Play, Pause, FastForward, Rewind, Monitor, Eye, ZoomIn, ZoomOut, Heading1, Heading2, Heading3, Image, AlignLeft, List, Video } from "lucide-react"; import { ContentRenderer } from "@/components/nle/content-renderer"; import { Sequence } from "@/components/nle/sequence"; import { SequenceSelector } from "@/components/nle/sequence-selector"; @@ -19,7 +19,6 @@ import "@/styles/nle.css"; const EditorialInterfaceComponent = ({ markdown }: { markdown: string }) => { const [scrollPercentage, setScrollPercentage] = useState(0); const [isPlaying, setIsPlaying] = useState(false); - const [playheadPosition, setPlayheadPosition] = useState(0); const [isScrolling, setIsScrolling] = useState(false); const [ffwState, setFfwState] = useState(false); const [rewindState, setRewindState] = useState(false); @@ -95,6 +94,11 @@ const EditorialInterfaceComponent = ({ markdown }: { markdown: string }) => { } }, [minZoomLevel, zoomLevel]); + // Derive playhead position from scroll percentage and calculated timeline width. + // Uses calculatedTimelineWidth (computed synchronously from state) instead of + // timelineWidth (updated async by ResizeObserver) to stay in sync after zoom changes. + const playheadPosition = scrollPercentage * calculatedTimelineWidth; + const verticalSectionRef = useRef(null); const playButtonRef = useRef(null); const playheadRef = useRef(null); @@ -206,17 +210,6 @@ const EditorialInterfaceComponent = ({ markdown }: { markdown: string }) => { // }; // }, [handleScroll]); - // Update the playhead position and height calculation - // Separate concerns: ResizeObserver for size changes, effect for position updates - useEffect(() => { - if (timelineWrapperRef.current) { - const width = timelineWrapperRef.current.scrollWidth; - const position = scrollPercentage * width; - setPlayheadPosition(position); - setTimelineWidth(width); - } - }, [scrollPercentage, zoomLevel]); - // ResizeObserver - track both scroll width (content) and client width (viewport) useEffect(() => { const updateTimelineDimensions = () => { @@ -253,13 +246,7 @@ const EditorialInterfaceComponent = ({ markdown }: { markdown: string }) => { setIsPlaying(false); setFfwState(false); setRewindState(false); - - if (!timelineWrapperRef.current) return; - - // Update playhead position - const timelineWidth = timelineWrapperRef.current.scrollWidth; - const newPosition = percentage * timelineWidth; - setPlayheadPosition(newPosition); + setScrollPercentage(percentage); }, []); @@ -310,43 +297,75 @@ const EditorialInterfaceComponent = ({ markdown }: { markdown: string }) => { } }, []); - const handlePlayheadDrag = useCallback((e: any, data: { x: number }) => { + const handlePlayheadDrag = useCallback((e: any) => { setIsPlaying(false); setFfwState(false); setRewindState(false); if (!timelineWrapperRef.current) return; - const timelineWidth = timelineWrapperRef.current.scrollWidth; - const maxX = timelineWidth - 2; - const clampedX = Math.max(0, Math.min(maxX, data.x)); - const newPercentage = clampedX / timelineWidth; + // Compute position directly from the mouse event relative to the timeline wrapper, + // bypassing react-draggable's internal coordinate tracking which can drift. + const wrapperRect = timelineWrapperRef.current.getBoundingClientRect(); + const posInContent = e.clientX - wrapperRect.left + timelineWrapperRef.current.scrollLeft; + const newPercentage = Math.max(0, Math.min(1, posInContent / calculatedTimelineWidth)); - setPlayheadPosition(clampedX); setScrollPercentage(newPercentage); - // Auto-scroll timeline if playhead is dragged near the left edge + // Auto-scroll timeline if playhead is dragged near edges const viewportWidth = timelineWrapperRef.current.clientWidth; const currentScrollLeft = timelineWrapperRef.current.scrollLeft; - const playheadVisualPosition = clampedX - currentScrollLeft; - - // If playhead is too close to left edge (within 50px), scroll left + const playheadVisualPosition = posInContent - currentScrollLeft; + if (playheadVisualPosition < 50 && currentScrollLeft > 0) { - const newScrollLeft = Math.max(0, clampedX - 100); // Keep playhead 100px from edge + const newScrollLeft = Math.max(0, posInContent - 100); timelineWrapperRef.current.scrollLeft = newScrollLeft; if (timelineTicksRef.current) { timelineTicksRef.current.scrollLeft = newScrollLeft; } - } - // If playhead is too close to right edge, scroll right - else if (playheadVisualPosition > viewportWidth - 50) { - const newScrollLeft = clampedX - viewportWidth + 100; + } else if (playheadVisualPosition > viewportWidth - 50) { + const newScrollLeft = posInContent - viewportWidth + 100; timelineWrapperRef.current.scrollLeft = newScrollLeft; if (timelineTicksRef.current) { timelineTicksRef.current.scrollLeft = newScrollLeft; } } - }, []); + }, [calculatedTimelineWidth]); + + // Handle click-to-snap on the timeline track area: snap playhead on mousedown, + // then follow through with drag via document-level mousemove/mouseup. + const handleTimelineMouseDown = useCallback((e: React.MouseEvent) => { + // Only handle left clicks, and ignore clicks on the playhead itself + if (e.button !== 0) return; + if (playheadRef.current && playheadRef.current.contains(e.target as Node)) return; + + e.preventDefault(); + setIsPlaying(false); + setFfwState(false); + setRewindState(false); + + if (!timelineWrapperRef.current) return; + + const computePercentage = (clientX: number) => { + const wrapperRect = timelineWrapperRef.current!.getBoundingClientRect(); + const posInContent = clientX - wrapperRect.left + timelineWrapperRef.current!.scrollLeft; + return Math.max(0, Math.min(1, posInContent / calculatedTimelineWidth)); + }; + + // Snap to click position immediately + setScrollPercentage(computePercentage(e.clientX)); + + const onMouseMove = (moveEvent: MouseEvent) => { + setScrollPercentage(computePercentage(moveEvent.clientX)); + }; + const onMouseUp = () => { + document.removeEventListener("mousemove", onMouseMove); + document.removeEventListener("mouseup", onMouseUp); + }; + + document.addEventListener("mousemove", onMouseMove); + document.addEventListener("mouseup", onMouseUp); + }, [calculatedTimelineWidth]); // Update keyboard shortcuts useEffect(() => { @@ -539,12 +558,13 @@ const EditorialInterfaceComponent = ({ markdown }: { markdown: string }) => { const deltaTime = (timestamp - lastTimestamp) / 1000; lastTimestamp = timestamp; + let hitBounds = false; + setScrollPercentage((prev) => { let newPercentage = prev; const speedMultiplier = getSpeedMultiplier(); if (isPlaying) { - // Use actual delta time instead of hardcoded 1/60 newPercentage += percentagePerSecond * deltaTime; } else if (ffwState) { newPercentage += (percentagePerSecond * speedMultiplier) * deltaTime; @@ -557,19 +577,21 @@ const EditorialInterfaceComponent = ({ markdown }: { markdown: string }) => { // Clamp between 0 and 1 newPercentage = Math.max(0, Math.min(1, newPercentage)); - // Stop playing if we hit the bounds if (newPercentage >= 1 || newPercentage <= 0) { - // Schedule state updates for the next batch to avoid infinite loop - Promise.resolve().then(() => { - setIsPlaying(false); - setFfwState(false); - setRewindState(false); - }); + hitBounds = true; } return newPercentage; }); + // Stop playback outside the updater to avoid cascading state updates + if (hitBounds) { + setIsPlaying(false); + setFfwState(false); + setRewindState(false); + return; // Don't schedule next frame + } + animationId = requestAnimationFrame(animate); }; @@ -612,10 +634,10 @@ const EditorialInterfaceComponent = ({ markdown }: { markdown: string }) => { }} />
*/} -
@@ -630,15 +652,14 @@ const EditorialInterfaceComponent = ({ markdown }: { markdown: string }) => { />
-
+
{getTimecodeFromScroll(scrollPercentage)}
@@ -733,7 +733,7 @@ const EditorialInterfaceComponent = ({ markdown }: { markdown: string }) => {
-
+
{getTimelineDurationTimecode()}
@@ -794,80 +794,52 @@ const EditorialInterfaceComponent = ({ markdown }: { markdown: string }) => { {/* Fixed track headers column */}
-
{"

"}

+
- - + +
-
{"

"}

+
- - + +
-
{"

"}

+
- - + +
-
{""}
+
- - + +
-
{"

"}

+
- - + +
-
{"
    "}
+
- - + +
-
{""}
+
- - + +
@@ -877,6 +849,7 @@ const EditorialInterfaceComponent = ({ markdown }: { markdown: string }) => { id="timelineWrapper" ref={timelineWrapperRef} className="timelineWrapper" + onMouseDown={handleTimelineMouseDown} onScroll={(e) => { // Sync timeline scroll with ticks if (timelineTicksRef.current) { @@ -892,15 +865,14 @@ const EditorialInterfaceComponent = ({ markdown }: { markdown: string }) => { {/* Playhead - positioned absolutely over the entire timeline */}
", name: "Embed", type: "embed" as const }, ]; +const CLIP_ICONS: Record = { + h1: Heading1, + h2: Heading2, + h3: Heading3, + img: Image, + p: AlignLeft, + ul: List, + embed: Video, +}; + +const ClipIcon = memo(({ type }: { type: string }) => { + const Icon = CLIP_ICONS[type]; + if (!Icon) return null; + return ; +}); +ClipIcon.displayName = "ClipIcon"; + interface ClipRendererProps { item: TrackItem; totalDurationMs: number; @@ -36,21 +54,21 @@ const ClipRenderer = memo(({ item, totalDurationMs, timelineWidth }: ClipRendere const getClipColor = () => { switch (item.type) { case "h1": - return "bg-gradient-to-b from-blue-200/90 to-blue-300/90 border-blue-400/80 text-blue-900 dark:from-blue-600/40 dark:to-blue-600/20 dark:border-blue-400/60 dark:text-blue-50"; + return "bg-gradient-to-b from-blue-200 to-blue-300/80 border-blue-300/60 border-l-blue-500 text-blue-900 dark:from-blue-500/30 dark:to-blue-700/18 dark:border-blue-500/25 dark:border-l-blue-400 dark:text-blue-100"; case "h2": - return "bg-gradient-to-b from-teal-200/90 to-teal-300/90 border-teal-400/80 text-teal-900 dark:from-teal-600/40 dark:to-teal-600/20 dark:border-teal-400/60 dark:text-teal-50"; + return "bg-gradient-to-b from-teal-200 to-teal-300/80 border-teal-300/60 border-l-teal-500 text-teal-900 dark:from-teal-500/30 dark:to-teal-700/18 dark:border-teal-500/25 dark:border-l-teal-400 dark:text-teal-100"; case "h3": - return "bg-gradient-to-b from-violet-200/90 to-violet-300/90 border-violet-400/80 text-violet-900 dark:from-violet-600/40 dark:to-violet-600/20 dark:border-violet-400/60 dark:text-violet-50"; + return "bg-gradient-to-b from-violet-200 to-violet-300/80 border-violet-300/60 border-l-violet-500 text-violet-900 dark:from-violet-500/30 dark:to-violet-700/18 dark:border-violet-500/25 dark:border-l-violet-400 dark:text-violet-100"; case "img": - return "bg-gradient-to-b from-rose-200/90 to-rose-300/90 border-rose-400/80 text-rose-900 dark:from-rose-600/40 dark:to-rose-600/20 dark:border-rose-400/60 dark:text-rose-50"; + return "bg-gradient-to-b from-rose-200 to-rose-300/80 border-rose-300/60 border-l-rose-500 text-rose-900 dark:from-rose-500/30 dark:to-rose-700/18 dark:border-rose-500/25 dark:border-l-rose-400 dark:text-rose-100"; case "p": - return "bg-gradient-to-b from-slate-200/90 to-slate-300/90 border-slate-400/80 text-slate-900 dark:from-slate-600/40 dark:to-slate-600/20 dark:border-slate-400/60 dark:text-slate-200"; + return "bg-gradient-to-b from-slate-200 to-slate-300/80 border-slate-300/50 border-l-slate-400 text-slate-800 dark:from-slate-500/25 dark:to-slate-700/15 dark:border-slate-500/20 dark:border-l-slate-400 dark:text-slate-200"; case "embed": - return "bg-gradient-to-b from-red-200/90 to-red-300/90 border-red-400/80 text-red-900 dark:from-red-600/40 dark:to-red-600/20 dark:border-red-400/60 dark:text-red-50"; + return "bg-gradient-to-b from-red-200 to-red-300/80 border-red-300/60 border-l-red-500 text-red-900 dark:from-red-500/30 dark:to-red-700/18 dark:border-red-500/25 dark:border-l-red-400 dark:text-red-100"; case "ul": - return "bg-gradient-to-b from-amber-200/90 to-amber-300/90 border-amber-400/80 text-amber-900 dark:from-amber-600/40 dark:to-amber-600/20 dark:border-amber-400/60 dark:text-amber-50"; + return "bg-gradient-to-b from-amber-200 to-amber-300/80 border-amber-300/60 border-l-amber-500 text-amber-900 dark:from-amber-500/30 dark:to-amber-700/18 dark:border-amber-500/25 dark:border-l-amber-400 dark:text-amber-100"; default: - return "bg-gradient-to-b from-gray-200/90 to-gray-300/90 border-gray-400/80 text-gray-900 dark:from-gray-600/40 dark:to-gray-600/20 dark:border-gray-400/60 dark:text-gray-50"; + return "bg-gradient-to-b from-gray-200 to-gray-300/80 border-gray-300/60 border-l-gray-400 text-gray-900 dark:from-gray-500/25 dark:to-gray-700/15 dark:border-gray-500/20 dark:border-l-gray-400 dark:text-gray-200"; } }; @@ -61,9 +79,10 @@ const ClipRenderer = memo(({ item, totalDurationMs, timelineWidth }: ClipRendere width: `${Math.max(20, width - 1)}px`, // Reduced gap between clips minWidth: "20px", }} - className={`clip border ${getClipColor()} rounded-md px-2 py-0.5 text-xs font-medium overflow-hidden shadow hover:brightness-95 dark:hover:brightness-110 transition-all cursor-pointer flex items-center select-none`} + className={`clip border ${getClipColor()} px-2 py-0.5 text-xs font-medium overflow-hidden shadow-sm hover:shadow hover:brightness-[0.97] dark:hover:brightness-110 transition-all duration-150 cursor-pointer flex items-center select-none`} title={`${item.name} (${item.start}ms - ${item.end}ms)`} > +
{item.name}
); diff --git a/src/styles/nle.css b/src/styles/nle.css index 7704af1..7b460cc 100644 --- a/src/styles/nle.css +++ b/src/styles/nle.css @@ -1,7 +1,7 @@ .uiContainer { display: flex; flex-direction: column; - height: calc(100vh - 73px); + height: calc(100dvh - 73px); padding: 16px; width: 100vw; overflow: hidden; @@ -14,7 +14,7 @@ border: 2px solid hsl(var(--border)); border-bottom-width: 1px; border-radius: var(--radius) var(--radius) 0 0; - height: calc(100vh - 22dvh - 64px); + height: calc(100dvh - 22dvh - 64px); width: calc(100vw - 32px); background-color: hsl(var(--background)); -ms-overflow-style: none; @@ -52,7 +52,7 @@ grid-template-columns: 1fr auto 1fr; height: 40px; align-items: center; - padding: 0px 8px; + padding: 0px 12px; border: 2px solid hsl(var(--border)); border-top-width: 1px; margin: 0px 0; @@ -69,16 +69,18 @@ .playbackControlsButtonWrapper { display: flex; justify-content: center; - gap: 8px; + gap: 2px; } .playbackControlButton { padding: 0; margin: 0; - transition: background-color 0.15s ease; - min-width: 36px; - height: 36px; + width: 30px !important; + min-width: 30px !important; + height: 28px !important; border: none !important; + border-radius: 6px !important; + transition: background-color 0.15s ease, box-shadow 0.15s ease; } .zoomControls { @@ -87,12 +89,6 @@ gap: 4px; } -@media (max-width: 768px) { - .zoomControls { - display: none; - } -} - .timelineWrapperContainer { display: flex; flex-direction: column; @@ -242,12 +238,17 @@ } .track-label { - padding: 2px 8px; - border-radius: 3px; - font-size: 0.7em; - min-width: 52px; - text-align: center; - align-items: baseline; + display: flex; + align-items: center; + justify-content: center; + padding: 3px; + border-radius: 4px; + min-width: 24px; + color: rgba(0, 0, 0, 0.6); +} + +.dark .track-label { + color: rgba(255, 255, 255, 0.7); } .track-label[data-track="h1"] { @@ -287,6 +288,13 @@ color: hsl(var(--muted-foreground)); cursor: pointer; padding: 4px; + border-radius: 3px; + transition: color 0.15s ease, background-color 0.15s ease; +} + +.track-controls button:hover { + color: hsl(var(--foreground)); + background: hsl(var(--accent)); } .track-content { @@ -305,7 +313,8 @@ position: absolute; height: calc(100% - 4px); margin: 2px 0; - border-radius: 2px; + border-radius: 3px; + border-left-width: 3px; user-select: none; -webkit-user-select: none; -moz-user-select: none; @@ -314,3 +323,116 @@ contain: layout style paint; } +/* Alternating track row backgrounds for depth */ +.track-content:nth-child(even) { + background: color-mix(in oklch, var(--track-header-bg), black 4%); +} +.track-header:nth-child(even) { + background: color-mix(in oklch, var(--track-header-bg), black 4%); +} + +/* Pro timecode display */ +.timecodeDisplay { + font-family: 'SF Mono', 'Menlo', 'Consolas', monospace; + font-variant-numeric: tabular-nums; + letter-spacing: 0.04em; + font-size: 0.8rem; + padding: 2px 10px; + border-radius: 4px; + background: hsl(var(--muted)); + border: 1px solid hsl(var(--border)); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.06); + color: hsl(var(--foreground)); +} + +.dark .timecodeDisplay { + background: rgba(0, 0, 0, 0.35); + border-color: rgba(255, 255, 255, 0.06); + box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.4); +} + +/* Playback button active glow */ +.playbackControlButton[data-active="true"] { + background: rgba(59, 130, 246, 0.12) !important; + box-shadow: 0 0 8px rgba(59, 130, 246, 0.15), inset 0 0 4px rgba(59, 130, 246, 0.08); +} + +.dark .playbackControlButton[data-active="true"] { + background: rgba(59, 130, 246, 0.2) !important; + box-shadow: 0 0 12px rgba(59, 130, 246, 0.25), inset 0 0 6px rgba(59, 130, 246, 0.12); +} + +/* Mobile overrides — must be last to win over base rules */ +@media (max-width: 768px) { + :root { + --track-header-width: 40px; + } + + .uiContainer { + padding: 8px; + } + + .programMonitor { + width: calc(100vw - 16px); + } + + .transportControls { + height: 48px; + padding: 0px 8px; + } + + .playbackControlButton { + width: 40px !important; + min-width: 40px !important; + height: 36px !important; + } + + .playbackControlsButtonWrapper { + gap: 4px; + } + + .timecodeDisplay { + font-size: 0.7rem; + padding: 2px 6px; + } + + .zoomControls { + display: none; + } + + .track-controls { + display: none; + } + + .timelineWrapperContainer { + width: calc(100vw - 16px); + } + + .contentRenderer { + max-width: 94%; + font-size: 0.85rem; + } + + .contentRenderer h1 { + font-size: 1.35rem; + margin-top: 1.25rem; + margin-bottom: 0.5rem; + } + + .contentRenderer h2 { + font-size: 1.15rem; + margin-top: 1rem; + margin-bottom: 0.4rem; + } + + .contentRenderer h3 { + font-size: 1rem; + margin-top: 0.85rem; + margin-bottom: 0.35rem; + } + + .contentRenderer p { + margin-bottom: 0.6rem; + } +} +