@ngxsplayer/ngx-smart-player-ui-video 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3833 @@
1
+ import * as i0 from '@angular/core';
2
+ import { inject, DestroyRef, EventEmitter, ChangeDetectorRef, ElementRef, ViewChild, Output, Input, ChangeDetectionStrategy, Component, NgModule } from '@angular/core';
3
+ import * as i1 from '@angular/common';
4
+ import { DOCUMENT, CommonModule } from '@angular/common';
5
+ import { SmartPlayerComponent, SmartPlayerIconComponent, SmartSettingsComponent } from '@ngxsplayer/ngx-smart-player';
6
+ import { NgxspVolumeControl } from '@ngxsplayer/ngx-smart-player-ui-core';
7
+
8
+ const CONTINUE_SECTION_ID = '__continue';
9
+ const FALLBACK_SECTION_ID = '__fallback';
10
+ /**
11
+ * A shelf holds all of its section: it is a row one scrolls, so cutting it at a row's worth only
12
+ * hides cards behind an edge that looks like the end. The cap is there to keep a catalogue of
13
+ * thousands from being drawn at once, not to make a preview.
14
+ */
15
+ const SHELF_RAIL_MAX = 60;
16
+ /** Below this, seeing the row and seeing it all are the same thing, and Show all says nothing. */
17
+ const SHELF_WORTH_EXPANDING = 4;
18
+ const QUEUE_SECTION_ID = '__queue';
19
+ /** How long the resume offer stays once the video plays. */
20
+ const RESUME_PROMPT_MS = 10_000;
21
+ const DEFAULT_VIDEO_NAV_ITEMS = [
22
+ { id: 'home', label: 'Home', icon: 'home', active: true },
23
+ { id: 'library', label: 'Library', icon: 'layers' },
24
+ { id: 'video', label: 'Video', icon: 'video' },
25
+ { id: 'live', label: 'Live', icon: 'broadcast' },
26
+ { id: 'favorites', label: 'Favorites', icon: 'heart' },
27
+ { id: 'downloads', label: 'Downloads', icon: 'download' },
28
+ { id: 'settings', label: 'Settings', icon: 'settings' },
29
+ ];
30
+ const OFFLINE_VIDEO_TOOL_CARDS = [
31
+ { id: 'offline-library', label: 'Local library', value: 'Open folder', icon: 'folder', tone: 'teal', active: true },
32
+ { id: 'engine', label: 'Local engine', value: 'Adaptive', icon: 'network', tone: 'blue' },
33
+ { id: 'tracks', label: 'Local tracks', value: 'Selectable', icon: 'sliders', tone: 'blue' },
34
+ { id: 'subtitles', label: 'Sidecars', value: 'VTT / SRT', icon: 'captions', tone: 'neutral' },
35
+ { id: 'offline', label: 'Resume', value: 'Progress', icon: 'cloud', tone: 'gold' },
36
+ { id: 'diagnostics', label: 'Local telemetry', value: 'Inspect', icon: 'waveform', tone: 'red' },
37
+ ];
38
+ const ONLINE_VIDEO_TOOL_CARDS = [
39
+ { id: 'online-catalog', label: 'Remote catalog', value: 'Configure', icon: 'broadcast', tone: 'teal', active: true },
40
+ { id: 'engine', label: 'Adaptive engine', value: 'HLS / DASH', icon: 'network', tone: 'blue' },
41
+ { id: 'tracks', label: 'Manifests', value: 'Audio / video', icon: 'sliders', tone: 'blue' },
42
+ { id: 'subtitles', label: 'Timed text', value: 'Ready', icon: 'captions', tone: 'neutral' },
43
+ { id: 'cast', label: 'Devices', value: 'Cast ready', icon: 'devices', tone: 'gold' },
44
+ { id: 'diagnostics', label: 'Telemetry', value: 'Live signals', icon: 'waveform', tone: 'red' },
45
+ ];
46
+ const DEFAULT_VIDEO_TOOL_CARDS = OFFLINE_VIDEO_TOOL_CARDS;
47
+ const OFFLINE_VIDEO_CARDS = [
48
+ {
49
+ id: 'offline-library',
50
+ title: 'Local video library',
51
+ subtitle: 'Folders, files, resume',
52
+ meta: 'Offline workspace',
53
+ },
54
+ {
55
+ id: 'tracks',
56
+ title: 'Track inspection',
57
+ subtitle: 'Audio, video, subtitles',
58
+ meta: 'Local media',
59
+ },
60
+ {
61
+ id: 'offline',
62
+ title: 'Resume sessions',
63
+ subtitle: 'Progress and continuity',
64
+ meta: 'Persisted locally',
65
+ },
66
+ {
67
+ id: 'devices',
68
+ title: 'Device playback',
69
+ subtitle: 'DLNA / UPnP',
70
+ meta: 'Local network',
71
+ },
72
+ ];
73
+ const ONLINE_VIDEO_CARDS = [
74
+ {
75
+ id: 'vod',
76
+ title: 'Video on demand',
77
+ subtitle: 'Catalog VOD and featured rails',
78
+ meta: 'Online catalog',
79
+ },
80
+ {
81
+ id: 'live',
82
+ title: 'Live playback',
83
+ subtitle: 'Latency-aware streams',
84
+ meta: 'Live source',
85
+ },
86
+ {
87
+ id: 'online-catalog',
88
+ title: 'Remote catalog',
89
+ subtitle: 'JSON configuration and feeds',
90
+ meta: 'Configure',
91
+ },
92
+ {
93
+ id: 'devices',
94
+ title: 'Multi-device playback',
95
+ subtitle: 'Cast, tracks, subtitles',
96
+ meta: 'Delivery tools',
97
+ },
98
+ ];
99
+ const WAVEFORM_BARS = [
100
+ 22, 42, 28, 64, 35, 78, 48, 30, 58, 74, 44, 88,
101
+ 52, 36, 66, 46, 76, 54, 32, 62, 84, 48, 28, 56,
102
+ 70, 38, 80, 50, 30, 64, 42, 72, 58, 34, 86, 46,
103
+ ];
104
+ class NgxSmartPlayerVideoUiComponent {
105
+ /**
106
+ * Keeps what is playing in sight in the queue. One scrolls down a queue to see what is
107
+ * coming, and the next video then starts out of view; it only scrolls when what plays
108
+ * changes, and inside the list rather than moving the page under everything else.
109
+ */
110
+ ngAfterViewChecked() {
111
+ const queue = this.queueListEl?.nativeElement;
112
+ const playing = queue?.querySelector('.ngxsp-yt-suggestion.active');
113
+ const key = playing && queue
114
+ ? `${Array.prototype.indexOf.call(queue.children, playing)}:${queue.clientHeight}`
115
+ : '';
116
+ if (!queue || !playing || key === this.queueFollowed)
117
+ return;
118
+ this.queueFollowed = key;
119
+ const reduced = typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches;
120
+ // Measured against the list itself: offsetTop answers for whichever ancestor happens to be
121
+ // positioned, which is not this one, and the answer is then a scroll to nowhere.
122
+ const box = queue.getBoundingClientRect();
123
+ const row = playing.getBoundingClientRect();
124
+ queue.scrollTo({
125
+ top: queue.scrollTop + (row.top - box.top) - (box.height - row.height) / 2,
126
+ behavior: reduced ? 'auto' : 'smooth',
127
+ });
128
+ }
129
+ constructor() {
130
+ inject(DestroyRef).onDestroy(() => clearTimeout(this.resumePromptTimer));
131
+ }
132
+ config = null;
133
+ /** The host app's logo in the masthead (an image URL); without it, a generic play tile. */
134
+ brandLogoSrc = '';
135
+ plugins = [];
136
+ ports;
137
+ engines;
138
+ debug = false;
139
+ startTimeSec = null;
140
+ /** Incremented by the host when the viewer picks something to watch; the poster preview then gives way to the player. */
141
+ set playRequest(value) {
142
+ if (value > this.playRequestValue)
143
+ this.posterPreviewDismissed = true;
144
+ this.playRequestValue = value;
145
+ }
146
+ get playRequest() {
147
+ return this.playRequestValue;
148
+ }
149
+ playRequestValue = 0;
150
+ canPrev = false;
151
+ canNext = false;
152
+ previousLabel = 'Previous video';
153
+ playerBackLabel = 'Back';
154
+ nextLabel = 'Next video';
155
+ playLabel = 'Play';
156
+ pauseLabel = 'Pause';
157
+ transportLabel = 'Video playback controls';
158
+ cinemaMode = false;
159
+ cinemaLabel = 'Enter cinema mode';
160
+ cinemaActiveLabel = 'Exit cinema mode';
161
+ shuffleActive = false;
162
+ shuffleLabel = 'Shuffle: off';
163
+ repeatMode = 'off';
164
+ repeatLabel = 'Repeat: off';
165
+ hasPlaylistControls = false;
166
+ castEnabled = false;
167
+ castConnected = false;
168
+ /** Set when the video loads where it was left (see `VideoUiResumeOffer`). */
169
+ set resumeOffer(offer) {
170
+ if (offer?.key !== this.offer?.key) {
171
+ this.clearResumePromptTimer();
172
+ this.resumePromptShown = !!offer;
173
+ }
174
+ this.offer = offer;
175
+ }
176
+ get resumeOffer() {
177
+ return this.offer;
178
+ }
179
+ offer = null;
180
+ resumePromptShown = false;
181
+ resumePromptTimer;
182
+ chooseResume(choice) {
183
+ if (choice === 'restart')
184
+ this.smartPlayer?.seekTo(0);
185
+ this.closeResumePrompt();
186
+ this.resumeChoice.emit(choice);
187
+ }
188
+ closeResumePrompt() {
189
+ this.clearResumePromptTimer();
190
+ this.resumePromptShown = false;
191
+ this.cdr.markForCheck();
192
+ }
193
+ clearResumePromptTimer() {
194
+ clearTimeout(this.resumePromptTimer);
195
+ this.resumePromptTimer = undefined;
196
+ }
197
+ /** Tabs of the info panel (library, engine, tracks, subtitles, progress, telemetry): their ids are panel ids. */
198
+ infoTabs = [];
199
+ infoLabel = 'Info';
200
+ /** The info panel open, shown over the video (in the sheet on phones); null when closed. */
201
+ set infoPanel(panel) {
202
+ this.info = panel;
203
+ if (panel && this.isSheetLayout())
204
+ this.queueSheetOpen = true;
205
+ }
206
+ get infoPanel() {
207
+ return this.info && !this.isSheetLayout() ? this.info : null;
208
+ }
209
+ info = null;
210
+ /** The words of the player's menus (tracks, speed, subtitles). */
211
+ playerMenuLabels = {};
212
+ heroEyebrow = 'Featured';
213
+ heroTitle = '';
214
+ heroSubtitle = '';
215
+ heroMeta = '';
216
+ heroArtworkUrl = '';
217
+ preferPosterPreview = true;
218
+ runtimeModeLabel = 'Video runtime mode';
219
+ onlineLabel = 'Online';
220
+ offlineLabel = 'Offline';
221
+ navigationLabel = 'Video navigation';
222
+ menuLabel = 'Menu';
223
+ searchPlaceholder = 'Search';
224
+ searchLabel = 'Search videos';
225
+ searchResultsTitle = 'Search results';
226
+ searchResultsSummary = '';
227
+ searchResultsEmptyTitle = 'No result';
228
+ searchResultsEmptyBody = 'Try another title, format, or tag.';
229
+ searchClearLabel = 'Clear search';
230
+ searchFilterLabel = 'Filter search results';
231
+ searchFilters = [];
232
+ searchActionLabels = {};
233
+ notificationsLabel = 'Notifications';
234
+ settingsLabel = 'Player settings';
235
+ sourceChangeLabel = 'Open file';
236
+ sourceChangeTitle = '';
237
+ sourceDropTitle = '';
238
+ sourceDropHint = '';
239
+ sourceChangeAccept = 'video/*';
240
+ /** When set (desktop shell), the title bar offers folder import and keeps single-file import as a secondary action. */
241
+ folderImportLabel = '';
242
+ libraryStatus = '';
243
+ /**
244
+ * Sliding a finger up or down over the picture: brightness on the left half, sound on the
245
+ * right. Off where it would get in the way — a pointer that only ever clicks, say.
246
+ */
247
+ touchAdjust = true;
248
+ /** How bright the picture is, 10 to 100. Below 100 a veil is drawn over it; nothing is brightened past its own. */
249
+ brightness = 100;
250
+ brightnessChange = new EventEmitter();
251
+ /** "More": what opens a card's own menu, for a screen reader and for a tooltip. */
252
+ cardActionsLabel = 'More';
253
+ /** The way out of a card's menu, for whoever cannot see where the menu stops. */
254
+ cardMenuCloseLabel = 'Close the menu';
255
+ /** The action chosen on a card's menu: which action, on which video. */
256
+ cardAction = new EventEmitter();
257
+ /** The strip along the bottom: a question, or how far a long job has got. Null when there is nothing to say. */
258
+ promptBar = null;
259
+ /** True when the viewer took the first way out, false for the second. */
260
+ promptAnswer = new EventEmitter();
261
+ libraryBusy = false;
262
+ sourceTypeLocalLabel = 'LOCAL';
263
+ sourceTypeStreamLabel = 'STREAM';
264
+ languageLabel = 'Language';
265
+ activeLanguage = '';
266
+ languageOptions = [];
267
+ statusMessage = '';
268
+ statusKind = 'info';
269
+ likeLabel = 'Like';
270
+ likeActive = false;
271
+ dislikeLabel = 'Dislike';
272
+ shareLabel = 'Share';
273
+ watchLaterLabel = 'Watch later';
274
+ watchLaterActive = false;
275
+ queueActionLabel = 'Show or hide queue';
276
+ queueShortLabel = 'Queue';
277
+ queueAddActiveLabel = 'Add to queue';
278
+ queueAddActiveShortLabel = 'Add';
279
+ queueActionLabels = {};
280
+ moreLabel = 'More options';
281
+ moreShortLabel = 'More';
282
+ browseLibraryLabel = 'Browse library';
283
+ /**
284
+ * Where the view stands, from its own name down to what is open. Entering a series or a folder
285
+ * is not another page: it is another step here, and everything else keeps its shape.
286
+ */
287
+ trail = [];
288
+ trailLabel = 'Where you are';
289
+ showAllLabel = 'Show all';
290
+ showLessLabel = 'Show less';
291
+ emptyCatalogLabel = 'No catalog section available.';
292
+ continueWatchingMeta = '';
293
+ continueWatching = [];
294
+ managedQueue = false;
295
+ upNextItems = [];
296
+ /** Lists the viewer created; null hides the block entirely. */
297
+ playlistPanel = null;
298
+ /** Media servers block above the library grid; null hides it entirely. */
299
+ serverForm = null;
300
+ showResumeProgress = false;
301
+ mediaDetail = null;
302
+ sidePanelTitle = 'Catalog';
303
+ sidePanelMeta = '';
304
+ sections = [];
305
+ navItems = DEFAULT_VIDEO_NAV_ITEMS;
306
+ toolCards = DEFAULT_VIDEO_TOOL_CARDS;
307
+ /** The tools open their panel inside the queue column, which is a sheet on a phone. */
308
+ set inspectorPanel(panel) {
309
+ this.inspector = panel;
310
+ if (panel && this.isSheetLayout())
311
+ this.queueSheetOpen = true;
312
+ }
313
+ get inspectorPanel() {
314
+ return this.inspector;
315
+ }
316
+ /** What the side column shows: its own panel, or on phones the info panel. */
317
+ get sidePanel() {
318
+ return this.inspector ?? (this.info && this.isSheetLayout() ? this.info : null);
319
+ }
320
+ inspector = null;
321
+ /**
322
+ * Every panel the host offers the side column. It used to hand one at a time, so a single
323
+ * panel still works: it is the list of one.
324
+ */
325
+ inspectorPanels = [];
326
+ /** The queue's own switch, which used to be "whatever showed when no panel did". */
327
+ queueOpen = true;
328
+ collapsedPanels = new Set();
329
+ closedPanels = new Set();
330
+ get knownPanels() {
331
+ if (this.inspectorPanels.length)
332
+ return this.inspectorPanels;
333
+ return this.inspector ? [this.inspector] : [];
334
+ }
335
+ /** The ones actually stacked: what the host offers, minus what the viewer put away. */
336
+ get openPanels() {
337
+ return this.knownPanels.filter((panel) => !this.closedPanels.has(panel.id));
338
+ }
339
+ isPanelOpen(id) {
340
+ if (id === 'queue')
341
+ return this.queueOpen;
342
+ return this.knownPanels.some((panel) => panel.id === id) && !this.closedPanels.has(id);
343
+ }
344
+ isPanelCollapsed(id) {
345
+ return this.collapsedPanels.has(id);
346
+ }
347
+ /** Folds a panel to its header, so several open ones stay usable without hiding any. */
348
+ togglePanelCollapsed(id) {
349
+ if (!this.collapsedPanels.delete(id))
350
+ this.collapsedPanels.add(id);
351
+ }
352
+ togglePanel(id) {
353
+ if (id === 'queue') {
354
+ this.queueOpen = !this.queueOpen;
355
+ return;
356
+ }
357
+ if (this.closedPanels.delete(id))
358
+ this.collapsedPanels.delete(id);
359
+ else
360
+ this.closePanel(id);
361
+ }
362
+ /**
363
+ * Puts a panel away. The host is told, since it is the one that decides what it offers; a host
364
+ * that keeps offering it gets it back from the pill, with no round trip.
365
+ */
366
+ closePanel(id) {
367
+ this.closedPanels.add(id);
368
+ this.panelClosed.emit(id);
369
+ if (this.knownPanels.length <= 1)
370
+ this.closeInspector.emit();
371
+ }
372
+ inspectorCloseLabel = 'Close panel';
373
+ libraryVisible = true;
374
+ /**
375
+ * Whether the area shows the player. A host with views of its own shows it only while watching, and gives the whole
376
+ * area to the view being browsed the rest of the time; the player then fills that area rather than sitting in it.
377
+ */
378
+ playerView = true;
379
+ /**
380
+ * Whether liking, sharing and keeping a video for later sit in the player's own controls. A host whose player has
381
+ * the screen to itself needs them there: there is no page around it to put them on.
382
+ */
383
+ watchActionsInPlayer = false;
384
+ /**
385
+ * Whether the area leads with the video being watched — its title, its buttons, its details. A home view turns it
386
+ * off so it opens on where videos come from, rather than on the last one that was played.
387
+ */
388
+ showActiveMedia = true;
389
+ closeLabel = 'Close';
390
+ queueVisible = false;
391
+ /**
392
+ * What the side panel lists: the queue, or the videos of `sections` (a library, a collection, a list) in full. With
393
+ * `sections`, the library grid under the player gives way to the panel while the panel is beside it; on phones,
394
+ * where the panel is a sheet, the grid stays and shows them all.
395
+ */
396
+ sidePanelContent = 'queue';
397
+ /** "Back to Lists": shown above the side panel's title while something is open inside it; null at the top. */
398
+ sideBackLabel = null;
399
+ searchTerm = '';
400
+ runtimeMode = 'offline';
401
+ /** False when the host offers the online/offline choice elsewhere (its settings). */
402
+ runtimeModeInHeader = true;
403
+ set activeViewMode(value) {
404
+ this.viewMode = value || 'grid';
405
+ }
406
+ get activeViewMode() {
407
+ return this.viewMode;
408
+ }
409
+ libraryTitle = 'Media library';
410
+ nowPlayingLabel = 'Now playing';
411
+ fullscreenHintLabel = 'Double-click for fullscreen';
412
+ fullscreenLabel = 'Fullscreen';
413
+ readyStatusLabel = 'Ready';
414
+ playingStatusLabel = 'Playing';
415
+ pausedStatusLabel = 'Paused';
416
+ bufferingStatusLabel = 'Buffering';
417
+ loadingStatusLabel = 'Loading';
418
+ endedStatusLabel = 'Ended';
419
+ errorStatusLabel = 'Error';
420
+ miniPlayerVisible = true;
421
+ miniPlayerMeta = '';
422
+ miniPlayerPlayLabel = 'Play from mini player';
423
+ miniPlayerPauseLabel = 'Pause from mini player';
424
+ miniPlayerFocusLabel = 'Open current video';
425
+ miniPlayerCloseLabel = 'Hide mini player';
426
+ miniPlayerRestoreLabel = 'Show mini player';
427
+ outputVolume = 100;
428
+ outputVolumeLabel = 'Output volume';
429
+ outputMuteLabel = 'Mute output';
430
+ outputUnmuteLabel = 'Restore sound';
431
+ // Public media output aliases intentionally mirror native media event names.
432
+ // eslint-disable-next-line @angular-eslint/no-output-native
433
+ ended = new EventEmitter();
434
+ videoSize = new EventEmitter();
435
+ // eslint-disable-next-line @angular-eslint/no-output-native
436
+ progress = new EventEmitter();
437
+ /** The window's size (desktop): the whole app or a floating mini player ('bar' is an audio layout, shown in full). */
438
+ compactMode = 'full';
439
+ /** True where the host can resize its window: shows the button that makes it a mini player. */
440
+ compactModes = false;
441
+ /**
442
+ * Down the side, across the top, or away. A narrow window still takes the row whatever is
443
+ * chosen — there is no side to put it down — but away stays away.
444
+ */
445
+ menuLayout = 'vertical';
446
+ compactMiniLabel = 'Mini player';
447
+ compactRestoreLabel = 'Full window';
448
+ compactModeChange = new EventEmitter();
449
+ /** "Show in Finder" for the video playing, where the host can show its file (desktop); empty hides the button. */
450
+ revealFileLabel = '';
451
+ revealFile = new EventEmitter();
452
+ prev = new EventEmitter();
453
+ next = new EventEmitter();
454
+ toggleShuffle = new EventEmitter();
455
+ cycleRepeat = new EventEmitter();
456
+ cast = new EventEmitter();
457
+ /** A subtitle file the viewer chose was read (ok), or could not be. */
458
+ /** The Infos button: open the info panel on its last tab, or close it. */
459
+ infoToggle = new EventEmitter();
460
+ /** Back out of what the side panel shows: a server folder, a list, a search (see `sideBackLabel`). */
461
+ sideBack = new EventEmitter();
462
+ /** Leaving the player. Reaching it from the mini player left nothing behind to go back to. */
463
+ playerBack = new EventEmitter();
464
+ /** The viewer's answer to `resumeOffer`: keep going from where it was, or start over (the player went back to 0). */
465
+ resumeChoice = new EventEmitter();
466
+ infoTabSelect = new EventEmitter();
467
+ subtitlesLoaded = new EventEmitter();
468
+ selectContinueWatching = new EventEmitter();
469
+ selectSectionItem = new EventEmitter();
470
+ selectDetailEpisode = new EventEmitter();
471
+ chapterSeek = new EventEmitter();
472
+ navSelect = new EventEmitter();
473
+ toolSelect = new EventEmitter();
474
+ detailAction = new EventEmitter();
475
+ inspectorAction = new EventEmitter();
476
+ settingChange = new EventEmitter();
477
+ settingAction = new EventEmitter();
478
+ closeInspector = new EventEmitter();
479
+ /** Which panel the viewer put away, when the host offers several. */
480
+ panelClosed = new EventEmitter();
481
+ openSource = new EventEmitter();
482
+ toggleLibrary = new EventEmitter();
483
+ /** A step of the trail the viewer asked to go back to. */
484
+ trailStep = new EventEmitter();
485
+ toggleQueue = new EventEmitter();
486
+ toggleCinema = new EventEmitter();
487
+ miniPlayerFocus = new EventEmitter();
488
+ miniPlayerClose = new EventEmitter();
489
+ miniPlayerRestore = new EventEmitter();
490
+ queueAddActive = new EventEmitter();
491
+ connectServer = new EventEmitter();
492
+ /** Looks for servers of this kind on the local network. */
493
+ discoverServers = new EventEmitter();
494
+ connectDiscoveredServer = new EventEmitter();
495
+ selectServer = new EventEmitter();
496
+ disconnectServer = new EventEmitter();
497
+ createPlaylist = new EventEmitter();
498
+ selectPlaylist = new EventEmitter();
499
+ /** Files the panel's target item in the given list. */
500
+ addToPlaylist = new EventEmitter();
501
+ /** Takes the panel's target out of the given list. */
502
+ removeFromPlaylist = new EventEmitter();
503
+ renamePlaylist = new EventEmitter();
504
+ deletePlaylist = new EventEmitter();
505
+ watchLaterToggle = new EventEmitter();
506
+ queueMoveUp = new EventEmitter();
507
+ queueMoveDown = new EventEmitter();
508
+ queueRemove = new EventEmitter();
509
+ searchFilterChange = new EventEmitter();
510
+ searchResultQueueAdd = new EventEmitter();
511
+ searchResultWatchLater = new EventEmitter();
512
+ searchResultFavorite = new EventEmitter();
513
+ searchResultDownload = new EventEmitter();
514
+ // eslint-disable-next-line @angular-eslint/no-output-native
515
+ search = new EventEmitter();
516
+ searchTermChange = new EventEmitter();
517
+ viewModeChange = new EventEmitter();
518
+ runtimeModeChange = new EventEmitter();
519
+ primaryPlay = new EventEmitter();
520
+ primaryPlaybackToggle = new EventEmitter();
521
+ playerStateChange = new EventEmitter();
522
+ sourceFileChange = new EventEmitter();
523
+ addFolder = new EventEmitter();
524
+ languageChange = new EventEmitter();
525
+ outputVolumeChange = new EventEmitter();
526
+ outputMuteToggle = new EventEmitter();
527
+ waveformBars = WAVEFORM_BARS;
528
+ searchOpen = false;
529
+ railExpanded = false;
530
+ fileDragActive = false;
531
+ playerState = null;
532
+ posterPreviewDismissed = false;
533
+ viewMode = 'grid';
534
+ cdr = inject(ChangeDetectorRef);
535
+ host = inject(ElementRef);
536
+ fileDragDepth = 0;
537
+ cardMenuBox;
538
+ smartPlayer;
539
+ document = inject(DOCUMENT);
540
+ contentScroll;
541
+ queueListEl;
542
+ queueFollowed = '';
543
+ videoFileInput;
544
+ // On phones the queue is a sheet over the page, opened from the queue button and closed from
545
+ // its own header: move focus into it so Escape and the close button are reachable right away.
546
+ set queuePanel(panel) {
547
+ if (!panel || !this.queueSheetOpen)
548
+ return;
549
+ setTimeout(() => panel.nativeElement.focus());
550
+ }
551
+ // The same viewport the mobile sheet styles use.
552
+ isSheetLayout() {
553
+ return typeof matchMedia === 'function' && matchMedia('(max-width: 860px)').matches;
554
+ }
555
+ queueSheetOpen = false;
556
+ get queuePanelActive() {
557
+ return this.queueSheetOpen || (this.queueVisible && !this.isSheetLayout());
558
+ }
559
+ onQueueAction() {
560
+ if (!this.isSheetLayout()) {
561
+ this.toggleQueue.emit();
562
+ return;
563
+ }
564
+ if (this.queueSheetOpen) {
565
+ this.closeQueueSheet();
566
+ return;
567
+ }
568
+ this.queueSheetOpen = true;
569
+ }
570
+ closeQueueSheet() {
571
+ if (!this.queueSheetOpen)
572
+ return;
573
+ this.queueSheetOpen = false;
574
+ // A tool panel opened this sheet: closing the sheet is how it is dismissed here.
575
+ if (this.sidePanel)
576
+ this.closeInspector.emit();
577
+ setTimeout(() => {
578
+ const buttons = Array.from(this.host.nativeElement.querySelectorAll('.ngxsp-yt-queueButton'));
579
+ buttons.find((button) => button.offsetParent !== null)?.focus();
580
+ });
581
+ }
582
+ /** True when the detail block only repeats the title bar above it, as it does for the playing media. */
583
+ get detailEchoesHeader() {
584
+ return this.mediaDetail?.title === this.displayHeroTitle;
585
+ }
586
+ get isPlaying() {
587
+ return this.playerState?.kind === 'playing';
588
+ }
589
+ get isLiveSource() {
590
+ return !!this.config?.source?.live;
591
+ }
592
+ get cinemaButtonLabel() {
593
+ return this.cinemaMode ? this.cinemaActiveLabel : this.cinemaLabel;
594
+ }
595
+ get miniPlayerPlaybackLabel() {
596
+ return this.isPlaying ? this.miniPlayerPauseLabel : this.miniPlayerPlayLabel;
597
+ }
598
+ get isOfflineMode() {
599
+ return this.runtimeMode === 'offline';
600
+ }
601
+ get showPosterPreview() {
602
+ return !!this.config && !!this.displayHeroArtwork && this.preferPosterPreview && !this.posterPreviewDismissed;
603
+ }
604
+ get sourceTypeLabel() {
605
+ if (this.config?.source?.type)
606
+ return this.config.source.type.toUpperCase();
607
+ return this.isOfflineMode ? this.sourceTypeLocalLabel : this.sourceTypeStreamLabel;
608
+ }
609
+ get playbackStateLabel() {
610
+ if (!this.config)
611
+ return this.isOfflineMode ? 'Offline ready' : 'Online ready';
612
+ return this.isLiveSource ? 'Live source' : 'On demand';
613
+ }
614
+ get primarySourceLabel() {
615
+ return this.isOfflineMode ? 'Open folder' : 'Configure catalog';
616
+ }
617
+ get primarySourceShortLabel() {
618
+ return this.isOfflineMode ? 'Open' : 'Catalog';
619
+ }
620
+ get primarySourceIcon() {
621
+ return this.isOfflineMode ? 'folder' : 'broadcast';
622
+ }
623
+ get emptyKicker() {
624
+ return this.isOfflineMode ? 'Video Offline' : 'Video Online';
625
+ }
626
+ get emptyTitle() {
627
+ return this.isOfflineMode ? 'Open a folder to start playback' : 'Connect a catalog to start streaming';
628
+ }
629
+ get emptyBody() {
630
+ return this.isOfflineMode
631
+ ? 'Build a local queue, resume progress, inspect tracks, subtitles, nearby devices, and offline eligibility from one focused player.'
632
+ : 'Connect a remote catalog, validate VOD/live streams, adaptive manifests, subtitles, casting, and telemetry in the online player.';
633
+ }
634
+ get libraryMeta() {
635
+ const count = this.itemCount;
636
+ if (count)
637
+ return `${count} item${count > 1 ? 's' : ''}`;
638
+ return this.config ? this.sourceTypeLabel : 'No source selected';
639
+ }
640
+ get railMeta() {
641
+ const count = this.railItems.length;
642
+ return `${count} ready`;
643
+ }
644
+ get displayHeroTitle() {
645
+ return this.heroTitle || this.activeCard?.item.title || 'NGXSP video cockpit';
646
+ }
647
+ get displayHeroSubtitle() {
648
+ return this.heroSubtitle || this.activeCard?.item.subtitle || (this.isOfflineMode
649
+ ? 'A production-grade offline shell for local folders, queues, resume state, subtitles, and device playback.'
650
+ : 'A production-grade online shell for VOD, live streams, adaptive manifests, subtitles, casting, and telemetry.');
651
+ }
652
+ get displayHeroMeta() {
653
+ return this.heroMeta || `${this.playbackStateLabel} | ${this.displayQueueItems.length} queued`;
654
+ }
655
+ get displayHeroArtwork() {
656
+ return this.heroArtworkUrl || this.activeCard?.item.artworkUrl || this.featuredItems.find((card) => !!card.item.artworkUrl)?.item.artworkUrl || '';
657
+ }
658
+ get timelineMarkers() {
659
+ const chapters = this.mediaDetail?.chapters ?? [];
660
+ return chapters
661
+ .flatMap((chapter) => {
662
+ const timeSec = this.chapterTimeSec(chapter);
663
+ if (!Number.isFinite(timeSec) || timeSec < 0)
664
+ return [];
665
+ const marker = {
666
+ id: chapter.id,
667
+ timeSec,
668
+ title: chapter.title,
669
+ };
670
+ const label = chapter.time || chapter.label;
671
+ if (label)
672
+ marker.label = label;
673
+ if (chapter.active !== undefined)
674
+ marker.active = chapter.active;
675
+ return [marker];
676
+ });
677
+ }
678
+ get timelineDurationSec() {
679
+ const durationSec = this.timeLabelToSeconds(this.activeCard?.item.duration);
680
+ return Number.isFinite(durationSec) && durationSec > 0 ? durationSec : null;
681
+ }
682
+ get hasSearchQuery() {
683
+ return this.searchTerm.trim().length > 0;
684
+ }
685
+ get searchResultItems() {
686
+ if (!this.hasSearchQuery)
687
+ return [];
688
+ return this.sections
689
+ .flatMap((section) => this.toRefs(section.id, section.items))
690
+ .slice(0, 18)
691
+ .map((card, index) => ({ ...card, index }));
692
+ }
693
+ expandedSections = new Set();
694
+ isSectionExpanded(id) {
695
+ return this.expandedSections.has(id);
696
+ }
697
+ /** Shows the rest of a shelf, or puts it back to a row. */
698
+ toggleSection(id) {
699
+ if (!this.expandedSections.delete(id))
700
+ this.expandedSections.add(id);
701
+ }
702
+ /**
703
+ * The view as a stack of titled shelves — one per section, empty ones left out, each holding a
704
+ * row until it is asked for the rest. A view with nothing to shelve keeps what it showed before,
705
+ * under no heading of its own.
706
+ */
707
+ get librarySections() {
708
+ const filled = this.sections.filter((section) => section.items.length);
709
+ if (filled.length) {
710
+ return filled.map((section) => {
711
+ const cards = this.toRefs(section.id, section.items);
712
+ const expanded = this.isSectionExpanded(section.id);
713
+ return {
714
+ id: section.id,
715
+ title: section.title,
716
+ count: section.items.length,
717
+ // A lone section named after the view would say the same thing twice.
718
+ titled: filled.length > 1 || section.title !== this.libraryTitle,
719
+ hidden: cards.length > SHELF_WORTH_EXPANDING,
720
+ cards: expanded ? cards : cards.slice(0, SHELF_RAIL_MAX),
721
+ };
722
+ });
723
+ }
724
+ const cards = this.continueWatching.length
725
+ ? this.toRefs(CONTINUE_SECTION_ID, this.continueWatching.slice(0, SHELF_RAIL_MAX))
726
+ : this.toRefs(FALLBACK_SECTION_ID, this.fallbackCards);
727
+ if (!cards.length)
728
+ return [];
729
+ return [{ id: FALLBACK_SECTION_ID, title: '', count: cards.length, titled: false, hidden: false, cards }];
730
+ }
731
+ get featuredItems() {
732
+ const sectionItems = this.sections.flatMap((section) => this.toRefs(section.id, section.items));
733
+ if (sectionItems.length)
734
+ return this.sidePanelContent === 'sections' ? sectionItems : sectionItems.slice(0, 8);
735
+ if (this.continueWatching.length)
736
+ return this.toRefs(CONTINUE_SECTION_ID, this.continueWatching.slice(0, 8));
737
+ return this.toRefs(FALLBACK_SECTION_ID, this.fallbackCards);
738
+ }
739
+ get railItems() {
740
+ if (this.continueWatching.length)
741
+ return this.toRefs(CONTINUE_SECTION_ID, this.continueWatching.slice(0, 8));
742
+ return this.featuredItems.slice(0, 8);
743
+ }
744
+ /** The sections listed by the side panel when it shows them (`sidePanelContent`). */
745
+ get sideSectionGroups() {
746
+ return this.sections.map((section) => ({ id: section.id, title: section.title, cards: this.toRefs(section.id, section.items) }));
747
+ }
748
+ /** How many videos the side panel lists. */
749
+ get sidePanelCount() {
750
+ return this.sidePanelContent === 'sections'
751
+ ? this.sections.reduce((count, section) => count + section.items.length, 0)
752
+ : this.displayQueueItems.length;
753
+ }
754
+ get displayQueueItems() {
755
+ const managedItems = this.managedQueue ? this.toRefs(QUEUE_SECTION_ID, this.upNextItems) : [];
756
+ const sectionItems = this.sections.flatMap((section) => this.toRefs(section.id, section.items));
757
+ return (this.managedQueue ? managedItems : sectionItems.length ? sectionItems : this.featuredItems)
758
+ .slice(0, 12)
759
+ .map((card, index) => ({ ...card, index }));
760
+ }
761
+ get hasManagedQueue() {
762
+ return this.managedQueue;
763
+ }
764
+ get isLibraryFocused() {
765
+ const activeNavId = this.navItems.find((item) => item.active)?.id;
766
+ return !!activeNavId && ['home', 'library', 'history', 'watch-later', 'favorites', 'downloads'].includes(activeNavId);
767
+ }
768
+ queueItemActionLabel(action, card) {
769
+ const fallback = {
770
+ play: 'Play from queue',
771
+ moveUp: 'Move up in queue',
772
+ moveDown: 'Move down in queue',
773
+ remove: 'Remove from queue',
774
+ };
775
+ return `${this.queueActionLabels[action] ?? fallback[action]} - ${card.item.title}`;
776
+ }
777
+ searchResultActionLabel(action, card) {
778
+ const fallback = {
779
+ play: this.playLabel,
780
+ queue: this.queueAddActiveLabel,
781
+ watchLater: this.watchLaterLabel,
782
+ favorite: this.likeLabel,
783
+ download: 'Download',
784
+ };
785
+ return `${this.searchActionLabels[action] ?? fallback[action]} - ${card.item.title}`;
786
+ }
787
+ canMoveQueueItem(card, direction) {
788
+ if (!this.hasManagedQueue)
789
+ return false;
790
+ return direction === 'up' ? card.index > 0 : card.index < this.displayQueueItems.length - 1;
791
+ }
792
+ toggleRail() {
793
+ this.railExpanded = !this.railExpanded;
794
+ }
795
+ selectNav(item) {
796
+ if (item.disabled)
797
+ return;
798
+ this.navSelect.emit(item.id);
799
+ }
800
+ selectViewMode(mode) {
801
+ if (this.activeViewMode === mode)
802
+ return;
803
+ this.viewMode = mode;
804
+ this.viewModeChange.emit(mode);
805
+ }
806
+ selectSearchFilter(filter) {
807
+ if (filter.disabled || filter.active)
808
+ return;
809
+ this.searchFilterChange.emit(filter.id);
810
+ }
811
+ selectRuntimeMode(mode) {
812
+ if (this.runtimeMode === mode)
813
+ return;
814
+ this.runtimeMode = mode;
815
+ this.runtimeModeChange.emit(mode);
816
+ }
817
+ onPrimarySourceAction() {
818
+ if (this.isOfflineMode) {
819
+ this.openSource.emit();
820
+ return;
821
+ }
822
+ this.toolSelect.emit('online-catalog');
823
+ }
824
+ playlistDraftName = '';
825
+ playlistRenameDraft = '';
826
+ renamingPlaylistId = null;
827
+ serverKindDraft = '';
828
+ serverAddressDraft = '';
829
+ serverUsernameDraft = '';
830
+ serverPasswordDraft = '';
831
+ selectedServerKind(form) {
832
+ const id = this.serverKindDraft || form.kinds?.[0]?.id;
833
+ return form.kinds?.find((kind) => kind.id === id);
834
+ }
835
+ onServerKindChange(event) {
836
+ this.serverKindDraft = event.target?.value ?? '';
837
+ }
838
+ onServerAddressInput(event) {
839
+ this.serverAddressDraft = event.target?.value ?? '';
840
+ }
841
+ onServerUsernameInput(event) {
842
+ this.serverUsernameDraft = event.target?.value ?? '';
843
+ }
844
+ onServerPasswordInput(event) {
845
+ this.serverPasswordDraft = event.target?.value ?? '';
846
+ }
847
+ submitServer(event) {
848
+ event.preventDefault();
849
+ const address = this.serverAddressDraft.trim();
850
+ if (!address)
851
+ return;
852
+ const kind = this.serverForm ? this.selectedServerKind(this.serverForm) : undefined;
853
+ const credentials = kind?.credentials !== false;
854
+ this.connectServer.emit({
855
+ ...(kind ? { kind: kind.id } : {}),
856
+ address,
857
+ username: credentials ? this.serverUsernameDraft.trim() : '',
858
+ password: credentials ? this.serverPasswordDraft : '',
859
+ });
860
+ // The password is handed over once and never kept in the form.
861
+ this.serverPasswordDraft = '';
862
+ }
863
+ onPlaylistDraftInput(event) {
864
+ this.playlistDraftName = event.target?.value ?? '';
865
+ }
866
+ submitCreatePlaylist(event) {
867
+ event.preventDefault();
868
+ const name = this.playlistDraftName.trim();
869
+ if (!name)
870
+ return;
871
+ this.createPlaylist.emit(name);
872
+ this.playlistDraftName = '';
873
+ }
874
+ startRenamePlaylist(list) {
875
+ this.renamingPlaylistId = list.id;
876
+ this.playlistRenameDraft = list.name;
877
+ }
878
+ onPlaylistRenameInput(event) {
879
+ this.playlistRenameDraft = event.target?.value ?? '';
880
+ }
881
+ submitRenamePlaylist(event) {
882
+ event.preventDefault();
883
+ const name = this.playlistRenameDraft.trim();
884
+ if (!name || !this.renamingPlaylistId)
885
+ return;
886
+ this.renamePlaylist.emit({ id: this.renamingPlaylistId, name });
887
+ this.cancelRenamePlaylist();
888
+ }
889
+ cancelRenamePlaylist() {
890
+ this.renamingPlaylistId = null;
891
+ this.playlistRenameDraft = '';
892
+ }
893
+ selectCard(card) {
894
+ if (this.isSyntheticSection(card.sectionId)) {
895
+ this.toolSelect.emit(card.item.id);
896
+ return;
897
+ }
898
+ if (card.sectionId === CONTINUE_SECTION_ID) {
899
+ this.selectContinueWatching.emit(card.item.id);
900
+ return;
901
+ }
902
+ this.selectSectionItem.emit({ sectionId: card.sectionId, itemId: card.item.id });
903
+ }
904
+ selectMediaChapter(chapter) {
905
+ const timeSec = this.chapterTimeSec(chapter);
906
+ if (this.smartPlayer && Number.isFinite(timeSec) && timeSec >= 0) {
907
+ this.smartPlayer.seekTo(timeSec);
908
+ this.chapterSeek.emit({ id: chapter.id, timeSec });
909
+ }
910
+ this.detailAction.emit(chapter.actionId || chapter.id);
911
+ }
912
+ chapterTimeSec(chapter) {
913
+ const explicit = Number(chapter.timeSec);
914
+ if (Number.isFinite(explicit) && explicit >= 0)
915
+ return explicit;
916
+ return this.timeLabelToSeconds(chapter.time);
917
+ }
918
+ timeLabelToSeconds(value) {
919
+ if (!value)
920
+ return Number.NaN;
921
+ const parts = value.trim().split(':').map((part) => Number(part));
922
+ if (parts.some((part) => !Number.isFinite(part) || part < 0))
923
+ return Number.NaN;
924
+ if (parts.length === 2)
925
+ return (parts[0] * 60) + parts[1];
926
+ if (parts.length === 3)
927
+ return (parts[0] * 3600) + (parts[1] * 60) + parts[2];
928
+ return Number.NaN;
929
+ }
930
+ isSyntheticSection(sectionId) {
931
+ return sectionId === FALLBACK_SECTION_ID;
932
+ }
933
+ get itemCount() {
934
+ return this.sections.reduce((total, section) => total + section.items.length, 0);
935
+ }
936
+ get fallbackCards() {
937
+ return this.isOfflineMode ? OFFLINE_VIDEO_CARDS : ONLINE_VIDEO_CARDS;
938
+ }
939
+ get activeCard() {
940
+ return [...this.displayQueueItems, ...this.railItems, ...this.featuredItems].find((card) => card.item.active);
941
+ }
942
+ toRefs(sectionId, items) {
943
+ return items.map((item, index) => ({ sectionId, item, index }));
944
+ }
945
+ toggleSearch() {
946
+ this.searchOpen = !this.searchOpen;
947
+ this.search.emit();
948
+ }
949
+ clearSearch() {
950
+ this.searchOpen = false;
951
+ this.searchFilterChange.emit('all');
952
+ if (this.searchTerm)
953
+ this.searchTermChange.emit('');
954
+ }
955
+ onSearchInput(event) {
956
+ this.searchTermChange.emit(event.target.value);
957
+ }
958
+ togglePrimaryPlayback() {
959
+ if (this.config && this.showPosterPreview) {
960
+ this.posterPreviewDismissed = true;
961
+ this.cdr.detectChanges();
962
+ this.setOutputVolumePercent(this.outputVolume);
963
+ this.primaryPlaybackToggle.emit('play');
964
+ this.cdr.markForCheck();
965
+ return;
966
+ }
967
+ if (this.config && this.smartPlayer) {
968
+ const intent = this.isPlaying ? 'pause' : 'play';
969
+ this.smartPlayer.togglePlay();
970
+ this.primaryPlaybackToggle.emit(intent);
971
+ return;
972
+ }
973
+ this.primaryPlay.emit();
974
+ }
975
+ toggleMiniPlayerPlayback() {
976
+ this.togglePrimaryPlayback();
977
+ }
978
+ /**
979
+ * How far the area is scrolled. A host that moves between views reads it before leaving one, so coming back
980
+ * can put the viewer where they were rather than at the top of a list they had already walked down. On a
981
+ * narrow layout the page scrolls instead of the panel, so whichever one moved is the one reported.
982
+ */
983
+ contentScrollTop() {
984
+ const panel = this.contentScroll?.nativeElement;
985
+ if (panel && panel.scrollTop > 0)
986
+ return panel.scrollTop;
987
+ return this.document.defaultView?.scrollY ?? 0;
988
+ }
989
+ /** Puts the area back where it was, or at the top with 0 on entering a view for the first time. */
990
+ setContentScrollTop(value) {
991
+ const top = Number.isFinite(value) && value > 0 ? value : 0;
992
+ const panel = this.contentScroll?.nativeElement;
993
+ // After the new view's rows exist, otherwise there is nothing tall enough to scroll through yet.
994
+ requestAnimationFrame(() => {
995
+ if (panel)
996
+ panel.scrollTop = top;
997
+ this.document.defaultView?.scrollTo({ top, behavior: 'auto' });
998
+ });
999
+ }
1000
+ /** Moves the playhead, for a host driven from outside the page: a lock screen scrubber, a remote, another device. */
1001
+ seekTo(timeSec) {
1002
+ if (Number.isFinite(timeSec) && timeSec >= 0)
1003
+ this.smartPlayer?.seekTo(timeSec);
1004
+ }
1005
+ setOutputVolumePercent(value) {
1006
+ this.smartPlayer?.setOutputVolume(this.normalizeOutputVolume(value) / 100);
1007
+ }
1008
+ onOutputVolumeRatioChange(value) {
1009
+ const nextVolume = this.normalizeOutputVolume(value * 100);
1010
+ this.setOutputVolumePercent(nextVolume);
1011
+ this.outputVolumeChange.emit(nextVolume);
1012
+ }
1013
+ openSourcePicker() {
1014
+ this.videoFileInput?.nativeElement.click();
1015
+ }
1016
+ onSourceFileInput(event) {
1017
+ const input = event.target;
1018
+ const file = input?.files?.[0];
1019
+ if (file)
1020
+ this.sourceFileChange.emit(file);
1021
+ if (input) {
1022
+ input.value = '';
1023
+ input.blur();
1024
+ }
1025
+ }
1026
+ onFileDragEnter(event) {
1027
+ if (!this.dragHasFiles(event))
1028
+ return;
1029
+ event.preventDefault();
1030
+ this.fileDragDepth += 1;
1031
+ this.fileDragActive = true;
1032
+ this.cdr.markForCheck();
1033
+ }
1034
+ onFileDragOver(event) {
1035
+ if (!this.dragHasFiles(event))
1036
+ return;
1037
+ event.preventDefault();
1038
+ if (event.dataTransfer)
1039
+ event.dataTransfer.dropEffect = 'copy';
1040
+ }
1041
+ onFileDragLeave(event) {
1042
+ if (!this.dragHasFiles(event))
1043
+ return;
1044
+ event.preventDefault();
1045
+ this.fileDragDepth = Math.max(0, this.fileDragDepth - 1);
1046
+ if (this.fileDragDepth === 0)
1047
+ this.fileDragActive = false;
1048
+ this.cdr.markForCheck();
1049
+ }
1050
+ onFileDrop(event) {
1051
+ if (!this.dragHasFiles(event))
1052
+ return;
1053
+ event.preventDefault();
1054
+ this.fileDragDepth = 0;
1055
+ this.fileDragActive = false;
1056
+ const file = event.dataTransfer?.files?.[0];
1057
+ if (file)
1058
+ this.sourceFileChange.emit(file);
1059
+ this.cdr.markForCheck();
1060
+ }
1061
+ onPlayerStateChange(state) {
1062
+ this.playerState = state;
1063
+ // The offer stays a few seconds of playback, then goes: resuming is what happens when nothing is chosen.
1064
+ if (this.resumePromptShown && state.kind === 'playing' && !this.resumePromptTimer) {
1065
+ this.resumePromptTimer = setTimeout(() => this.closeResumePrompt(), RESUME_PROMPT_MS);
1066
+ }
1067
+ this.playerStateChange.emit(state);
1068
+ this.cdr.markForCheck();
1069
+ }
1070
+ posterInitial(title) {
1071
+ return (title.trim().charAt(0) || 'V').toUpperCase();
1072
+ }
1073
+ mediaCardMeta(card) {
1074
+ const values = [
1075
+ card.kindLabel ?? '',
1076
+ card.parentTitle ?? '',
1077
+ card.seasonLabel ?? '',
1078
+ card.releaseYear ?? '',
1079
+ card.meta ?? '',
1080
+ card.duration ?? '',
1081
+ ].filter(Boolean);
1082
+ return [...new Set(values)].join(' | ');
1083
+ }
1084
+ /** What a finger is changing right now, and where it stands. Null when nothing is being changed. */
1085
+ touchAdjusting = null;
1086
+ touchStart = null;
1087
+ /** A drag is not a click: the one that follows it is swallowed rather than reaching a control. */
1088
+ swallowNextClick = false;
1089
+ /** How far the picture has to be crossed to go from nothing to everything. */
1090
+ static TOUCH_TRAVEL = 0.6;
1091
+ static TOUCH_THRESHOLD = 12;
1092
+ static MIN_BRIGHTNESS = 10;
1093
+ /**
1094
+ * A gesture starts on the picture, never on something built to be pressed: a button, a slider,
1095
+ * a menu. Those keep the events that belong to them.
1096
+ */
1097
+ onFramePointerDown(event) {
1098
+ this.touchStart = null;
1099
+ if (!this.touchAdjust || !this.config)
1100
+ return;
1101
+ const target = event.target;
1102
+ if (target?.closest('button, input, select, a, [role="slider"], [role="menu"], [role="menuitem"]'))
1103
+ return;
1104
+ const frame = event.currentTarget;
1105
+ const box = frame.getBoundingClientRect();
1106
+ if (box.width <= 0)
1107
+ return;
1108
+ this.touchStart = {
1109
+ x: event.clientX,
1110
+ y: event.clientY,
1111
+ kind: event.clientX - box.left < box.width / 2 ? 'brightness' : 'volume',
1112
+ from: 0,
1113
+ pointerId: event.pointerId,
1114
+ };
1115
+ }
1116
+ /**
1117
+ * Nothing happens until the finger has travelled further down than across: a tap stays a tap,
1118
+ * and a sideways drag is somebody else's gesture.
1119
+ */
1120
+ onFramePointerMove(event) {
1121
+ const start = this.touchStart;
1122
+ if (!start || event.pointerId !== start.pointerId)
1123
+ return;
1124
+ const dx = event.clientX - start.x;
1125
+ const dy = event.clientY - start.y;
1126
+ if (!this.touchAdjusting) {
1127
+ if (Math.abs(dy) < NgxSmartPlayerVideoUiComponent.TOUCH_THRESHOLD || Math.abs(dy) <= Math.abs(dx))
1128
+ return;
1129
+ start.from = start.kind === 'brightness' ? this.brightness : this.outputVolume;
1130
+ event.currentTarget.setPointerCapture?.(event.pointerId);
1131
+ this.swallowNextClick = true;
1132
+ }
1133
+ event.preventDefault();
1134
+ const box = event.currentTarget.getBoundingClientRect();
1135
+ const travel = Math.max(1, box.height * NgxSmartPlayerVideoUiComponent.TOUCH_TRAVEL);
1136
+ // Up is more, which is the way every other player and every phone has taught it.
1137
+ const floor = start.kind === 'brightness' ? NgxSmartPlayerVideoUiComponent.MIN_BRIGHTNESS : 0;
1138
+ const raw = start.from - (dy / travel) * 100;
1139
+ const value = Math.round(Math.max(floor, Math.min(100, raw)));
1140
+ this.touchAdjusting = { kind: start.kind, value };
1141
+ if (start.kind === 'brightness') {
1142
+ this.brightness = value;
1143
+ this.brightnessChange.emit(value);
1144
+ }
1145
+ else {
1146
+ this.setOutputVolumePercent(value);
1147
+ this.outputVolume = value;
1148
+ this.outputVolumeChange.emit(value);
1149
+ }
1150
+ }
1151
+ onFramePointerUp(event) {
1152
+ if (this.touchStart && event.pointerId === this.touchStart.pointerId) {
1153
+ event.currentTarget.releasePointerCapture?.(event.pointerId);
1154
+ }
1155
+ if (this.swallowNextClick) {
1156
+ this.swallowNextClick = false;
1157
+ this.swallowTheClickThatEndsADrag();
1158
+ }
1159
+ this.touchStart = null;
1160
+ this.touchAdjusting = null;
1161
+ }
1162
+ /**
1163
+ * Le clic qui clôt un glissement mettrait sinon en pause la vidéo dont le glissement ne
1164
+ * parlait pas. Posé ici plutôt que dans le gabarit : avaler un clic n'est pas une interaction,
1165
+ * et en capture il est arrêté avant d'atteindre la moindre commande.
1166
+ */
1167
+ swallowTheClickThatEndsADrag() {
1168
+ const view = this.document.defaultView;
1169
+ if (!view)
1170
+ return;
1171
+ const swallow = (event) => {
1172
+ event.preventDefault();
1173
+ event.stopPropagation();
1174
+ };
1175
+ view.addEventListener('click', swallow, { capture: true, once: true });
1176
+ // Rendu au bout d'un tour : un glissement qui ne produit aucun clic ne doit pas manger
1177
+ // le clic suivant, qui serait celui de quelqu'un d'autre.
1178
+ queueMicrotask(() => view.removeEventListener('click', swallow, { capture: true }));
1179
+ }
1180
+ /** The card menu while it is open: which card, and where the gesture put it. */
1181
+ cardMenu = null;
1182
+ /**
1183
+ * Opens a card's own menu where the gesture happened — a right click, or the button that
1184
+ * appears on the card — and keeps it inside the window rather than off its edge.
1185
+ */
1186
+ openCardMenu(event, card) {
1187
+ if (!card.actions?.length)
1188
+ return;
1189
+ event.preventDefault();
1190
+ event.stopPropagation();
1191
+ const width = 280;
1192
+ const height = 60 + card.actions.length * 36;
1193
+ const view = this.document.defaultView;
1194
+ const maxX = (view?.innerWidth ?? width) - width - 8;
1195
+ const maxY = (view?.innerHeight ?? height) - height - 8;
1196
+ this.cardMenu = {
1197
+ card,
1198
+ x: Math.max(8, Math.min(event.clientX, Math.max(8, maxX))),
1199
+ y: Math.max(8, Math.min(event.clientY, Math.max(8, maxY))),
1200
+ };
1201
+ // Le menu prend le focus : sans cela, Échap ne lui parvenait jamais après un clic droit,
1202
+ // et il n'y avait aucun moyen d'en sortir au clavier.
1203
+ queueMicrotask(() => this.cardMenuBox?.nativeElement.focus());
1204
+ }
1205
+ closeCardMenu() {
1206
+ this.cardMenu = null;
1207
+ }
1208
+ chooseCardAction(action) {
1209
+ const card = this.cardMenu?.card;
1210
+ this.closeCardMenu();
1211
+ if (card)
1212
+ this.cardAction.emit({ actionId: action.id, cardId: card.id });
1213
+ }
1214
+ /** Whether the strip is waiting for an answer rather than reporting: both ways out have a name. */
1215
+ get promptAsking() {
1216
+ const prompt = this.promptBar;
1217
+ return !!prompt?.confirmLabel && !!prompt?.cancelLabel;
1218
+ }
1219
+ safeProgressPercent(value) {
1220
+ if (!Number.isFinite(value))
1221
+ return 0;
1222
+ return Math.max(0, Math.min(100, value));
1223
+ }
1224
+ artVariant(card) {
1225
+ const variants = ['aurora', 'orbit', 'ember', 'ridge', 'signal', 'fog'];
1226
+ const seed = `${card.sectionId}:${card.item.id}:${card.item.title}`;
1227
+ let hash = 0;
1228
+ for (let i = 0; i < seed.length; i += 1) {
1229
+ hash = (hash * 31 + seed.charCodeAt(i)) >>> 0;
1230
+ }
1231
+ return variants[hash % variants.length];
1232
+ }
1233
+ normalizeOutputVolume(value) {
1234
+ return Math.max(0, Math.min(100, Math.round(Number.isFinite(value) ? value : this.outputVolume)));
1235
+ }
1236
+ dragHasFiles(event) {
1237
+ return Array.from(event.dataTransfer?.types ?? []).includes('Files');
1238
+ }
1239
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.33", ngImport: i0, type: NgxSmartPlayerVideoUiComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1240
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.33", type: NgxSmartPlayerVideoUiComponent, isStandalone: true, selector: "ngxsp-video-player-ui", inputs: { config: "config", brandLogoSrc: "brandLogoSrc", plugins: "plugins", ports: "ports", engines: "engines", debug: "debug", startTimeSec: "startTimeSec", playRequest: "playRequest", canPrev: "canPrev", canNext: "canNext", previousLabel: "previousLabel", playerBackLabel: "playerBackLabel", nextLabel: "nextLabel", playLabel: "playLabel", pauseLabel: "pauseLabel", transportLabel: "transportLabel", cinemaMode: "cinemaMode", cinemaLabel: "cinemaLabel", cinemaActiveLabel: "cinemaActiveLabel", shuffleActive: "shuffleActive", shuffleLabel: "shuffleLabel", repeatMode: "repeatMode", repeatLabel: "repeatLabel", hasPlaylistControls: "hasPlaylistControls", castEnabled: "castEnabled", castConnected: "castConnected", resumeOffer: "resumeOffer", infoTabs: "infoTabs", infoLabel: "infoLabel", infoPanel: "infoPanel", playerMenuLabels: "playerMenuLabels", heroEyebrow: "heroEyebrow", heroTitle: "heroTitle", heroSubtitle: "heroSubtitle", heroMeta: "heroMeta", heroArtworkUrl: "heroArtworkUrl", preferPosterPreview: "preferPosterPreview", runtimeModeLabel: "runtimeModeLabel", onlineLabel: "onlineLabel", offlineLabel: "offlineLabel", navigationLabel: "navigationLabel", menuLabel: "menuLabel", searchPlaceholder: "searchPlaceholder", searchLabel: "searchLabel", searchResultsTitle: "searchResultsTitle", searchResultsSummary: "searchResultsSummary", searchResultsEmptyTitle: "searchResultsEmptyTitle", searchResultsEmptyBody: "searchResultsEmptyBody", searchClearLabel: "searchClearLabel", searchFilterLabel: "searchFilterLabel", searchFilters: "searchFilters", searchActionLabels: "searchActionLabels", notificationsLabel: "notificationsLabel", settingsLabel: "settingsLabel", sourceChangeLabel: "sourceChangeLabel", sourceChangeTitle: "sourceChangeTitle", sourceDropTitle: "sourceDropTitle", sourceDropHint: "sourceDropHint", sourceChangeAccept: "sourceChangeAccept", folderImportLabel: "folderImportLabel", libraryStatus: "libraryStatus", touchAdjust: "touchAdjust", brightness: "brightness", cardActionsLabel: "cardActionsLabel", cardMenuCloseLabel: "cardMenuCloseLabel", promptBar: "promptBar", libraryBusy: "libraryBusy", sourceTypeLocalLabel: "sourceTypeLocalLabel", sourceTypeStreamLabel: "sourceTypeStreamLabel", languageLabel: "languageLabel", activeLanguage: "activeLanguage", languageOptions: "languageOptions", statusMessage: "statusMessage", statusKind: "statusKind", likeLabel: "likeLabel", likeActive: "likeActive", dislikeLabel: "dislikeLabel", shareLabel: "shareLabel", watchLaterLabel: "watchLaterLabel", watchLaterActive: "watchLaterActive", queueActionLabel: "queueActionLabel", queueShortLabel: "queueShortLabel", queueAddActiveLabel: "queueAddActiveLabel", queueAddActiveShortLabel: "queueAddActiveShortLabel", queueActionLabels: "queueActionLabels", moreLabel: "moreLabel", moreShortLabel: "moreShortLabel", browseLibraryLabel: "browseLibraryLabel", trail: "trail", trailLabel: "trailLabel", showAllLabel: "showAllLabel", showLessLabel: "showLessLabel", emptyCatalogLabel: "emptyCatalogLabel", continueWatchingMeta: "continueWatchingMeta", continueWatching: "continueWatching", managedQueue: "managedQueue", upNextItems: "upNextItems", playlistPanel: "playlistPanel", serverForm: "serverForm", showResumeProgress: "showResumeProgress", mediaDetail: "mediaDetail", sidePanelTitle: "sidePanelTitle", sidePanelMeta: "sidePanelMeta", sections: "sections", navItems: "navItems", toolCards: "toolCards", inspectorPanel: "inspectorPanel", inspectorPanels: "inspectorPanels", inspectorCloseLabel: "inspectorCloseLabel", libraryVisible: "libraryVisible", playerView: "playerView", watchActionsInPlayer: "watchActionsInPlayer", showActiveMedia: "showActiveMedia", closeLabel: "closeLabel", queueVisible: "queueVisible", sidePanelContent: "sidePanelContent", sideBackLabel: "sideBackLabel", searchTerm: "searchTerm", runtimeMode: "runtimeMode", runtimeModeInHeader: "runtimeModeInHeader", activeViewMode: "activeViewMode", libraryTitle: "libraryTitle", nowPlayingLabel: "nowPlayingLabel", fullscreenHintLabel: "fullscreenHintLabel", fullscreenLabel: "fullscreenLabel", readyStatusLabel: "readyStatusLabel", playingStatusLabel: "playingStatusLabel", pausedStatusLabel: "pausedStatusLabel", bufferingStatusLabel: "bufferingStatusLabel", loadingStatusLabel: "loadingStatusLabel", endedStatusLabel: "endedStatusLabel", errorStatusLabel: "errorStatusLabel", miniPlayerVisible: "miniPlayerVisible", miniPlayerMeta: "miniPlayerMeta", miniPlayerPlayLabel: "miniPlayerPlayLabel", miniPlayerPauseLabel: "miniPlayerPauseLabel", miniPlayerFocusLabel: "miniPlayerFocusLabel", miniPlayerCloseLabel: "miniPlayerCloseLabel", miniPlayerRestoreLabel: "miniPlayerRestoreLabel", outputVolume: "outputVolume", outputVolumeLabel: "outputVolumeLabel", outputMuteLabel: "outputMuteLabel", outputUnmuteLabel: "outputUnmuteLabel", compactMode: "compactMode", compactModes: "compactModes", menuLayout: "menuLayout", compactMiniLabel: "compactMiniLabel", compactRestoreLabel: "compactRestoreLabel", revealFileLabel: "revealFileLabel" }, outputs: { brightnessChange: "brightnessChange", cardAction: "cardAction", promptAnswer: "promptAnswer", ended: "ended", videoSize: "videoSize", progress: "progress", compactModeChange: "compactModeChange", revealFile: "revealFile", prev: "prev", next: "next", toggleShuffle: "toggleShuffle", cycleRepeat: "cycleRepeat", cast: "cast", infoToggle: "infoToggle", sideBack: "sideBack", playerBack: "playerBack", resumeChoice: "resumeChoice", infoTabSelect: "infoTabSelect", subtitlesLoaded: "subtitlesLoaded", selectContinueWatching: "selectContinueWatching", selectSectionItem: "selectSectionItem", selectDetailEpisode: "selectDetailEpisode", chapterSeek: "chapterSeek", navSelect: "navSelect", toolSelect: "toolSelect", detailAction: "detailAction", inspectorAction: "inspectorAction", settingChange: "settingChange", settingAction: "settingAction", closeInspector: "closeInspector", panelClosed: "panelClosed", openSource: "openSource", toggleLibrary: "toggleLibrary", trailStep: "trailStep", toggleQueue: "toggleQueue", toggleCinema: "toggleCinema", miniPlayerFocus: "miniPlayerFocus", miniPlayerClose: "miniPlayerClose", miniPlayerRestore: "miniPlayerRestore", queueAddActive: "queueAddActive", connectServer: "connectServer", discoverServers: "discoverServers", connectDiscoveredServer: "connectDiscoveredServer", selectServer: "selectServer", disconnectServer: "disconnectServer", createPlaylist: "createPlaylist", selectPlaylist: "selectPlaylist", addToPlaylist: "addToPlaylist", removeFromPlaylist: "removeFromPlaylist", renamePlaylist: "renamePlaylist", deletePlaylist: "deletePlaylist", watchLaterToggle: "watchLaterToggle", queueMoveUp: "queueMoveUp", queueMoveDown: "queueMoveDown", queueRemove: "queueRemove", searchFilterChange: "searchFilterChange", searchResultQueueAdd: "searchResultQueueAdd", searchResultWatchLater: "searchResultWatchLater", searchResultFavorite: "searchResultFavorite", searchResultDownload: "searchResultDownload", search: "search", searchTermChange: "searchTermChange", viewModeChange: "viewModeChange", runtimeModeChange: "runtimeModeChange", primaryPlay: "primaryPlay", primaryPlaybackToggle: "primaryPlaybackToggle", playerStateChange: "playerStateChange", sourceFileChange: "sourceFileChange", addFolder: "addFolder", languageChange: "languageChange", outputVolumeChange: "outputVolumeChange", outputMuteToggle: "outputMuteToggle" }, viewQueries: [{ propertyName: "cardMenuBox", first: true, predicate: ["cardMenuBox"], descendants: true }, { propertyName: "smartPlayer", first: true, predicate: SmartPlayerComponent, descendants: true }, { propertyName: "contentScroll", first: true, predicate: ["contentScroll"], descendants: true }, { propertyName: "queueListEl", first: true, predicate: ["queueList"], descendants: true }, { propertyName: "videoFileInput", first: true, predicate: ["videoFileInput"], descendants: true }, { propertyName: "queuePanel", first: true, predicate: ["queuePanel"], descendants: true }], ngImport: i0, template: `
1241
+ <section
1242
+ class="ngxsp-yt-shell"
1243
+ [class.ngxsp-yt-online]="runtimeMode === 'online'"
1244
+ [class.ngxsp-yt-offline]="runtimeMode === 'offline'"
1245
+ [class.ngxsp-yt-playing]="isPlaying"
1246
+ [class.ngxsp-yt-playerView]="playerView"
1247
+ [class.ngxsp-yt-cinema]="cinemaMode"
1248
+ [class.ngxsp-has-source]="!!config"
1249
+ [class.ngxsp-queue-hidden]="!queueVisible"
1250
+ [class.ngxsp-mini-hidden]="!miniPlayerVisible"
1251
+ [class.ngxsp-yt-dragActive]="fileDragActive"
1252
+ [class.ngxsp-yt-library-focus]="isLibraryFocused"
1253
+ [class.ngxsp-yt-menuRow]="menuLayout === 'horizontal'"
1254
+ [class.ngxsp-yt-menuOff]="menuLayout === 'hidden'"
1255
+ [class.ngxsp-yt-railExpanded]="railExpanded"
1256
+ [class.ngxsp-yt-compact]="compactMode === 'mini'"
1257
+ (dragenter)="onFileDragEnter($event)"
1258
+ (dragover)="onFileDragOver($event)"
1259
+ (dragleave)="onFileDragLeave($event)"
1260
+ (drop)="onFileDrop($event)"
1261
+ >
1262
+ @if (fileDragActive) {
1263
+ <div class="ngxsp-yt-dropOverlay" aria-hidden="true">
1264
+ <span class="ngxsp-yt-dropIcon"><ngxsp-smart-icon name="folder" /></span>
1265
+ <strong>{{ sourceDropTitle || sourceChangeTitle || sourceChangeLabel }}</strong>
1266
+ @if (sourceDropHint) {
1267
+ <span>{{ sourceDropHint }}</span>
1268
+ }
1269
+ </div>
1270
+ }
1271
+
1272
+ @if (promptBar) {
1273
+ <div class="ngxsp-yt-convert" [class.asking]="promptAsking" [class.danger]="!!promptBar.danger" [attr.role]="promptAsking ? 'alertdialog' : 'status'" aria-live="polite">
1274
+ <span class="ngxsp-yt-convertIcon" aria-hidden="true"><ngxsp-smart-icon [name]="promptBar.danger ? 'trash' : 'download'" /></span>
1275
+ <span class="ngxsp-yt-convertText">
1276
+ <strong>{{ promptBar.message }}</strong>
1277
+ @if (promptBar.hint) { <small>{{ promptBar.hint }}</small> }
1278
+ @if (promptBar.percent !== undefined) {
1279
+ <em role="progressbar" [attr.aria-valuenow]="safeProgressPercent(promptBar.percent)" aria-valuemin="0" aria-valuemax="100">
1280
+ <i [style.width.%]="safeProgressPercent(promptBar.percent)"></i>
1281
+ </em>
1282
+ }
1283
+ </span>
1284
+ @if (promptAsking) {
1285
+ <span class="ngxsp-yt-convertActions">
1286
+ <button type="button" class="keep" (click)="promptAnswer.emit(true)">{{ promptBar.confirmLabel }}</button>
1287
+ <button type="button" (click)="promptAnswer.emit(false)">{{ promptBar.cancelLabel }}</button>
1288
+ </span>
1289
+ }
1290
+ </div>
1291
+ }
1292
+
1293
+ @if (cardMenu) {
1294
+ <button
1295
+ class="ngxsp-yt-cardMenuScrim"
1296
+ type="button"
1297
+ [attr.aria-label]="cardMenuCloseLabel"
1298
+ (click)="closeCardMenu()"
1299
+ (contextmenu)="$event.preventDefault(); closeCardMenu()"
1300
+ ></button>
1301
+ <div
1302
+ #cardMenuBox
1303
+ class="ngxsp-yt-cardMenu"
1304
+ role="menu"
1305
+ tabindex="-1"
1306
+ [attr.aria-label]="cardMenu.card.title"
1307
+ [style.left.px]="cardMenu.x"
1308
+ [style.top.px]="cardMenu.y"
1309
+ (keydown.escape)="closeCardMenu()"
1310
+ >
1311
+ <strong class="ngxsp-yt-cardMenuTitle">{{ cardMenu.card.title }}</strong>
1312
+ @for (action of cardMenu.card.actions; track action.id) {
1313
+ <button type="button" role="menuitem" [class.danger]="!!action.danger" (click)="chooseCardAction(action)">
1314
+ <ngxsp-smart-icon [name]="action.icon" />
1315
+ <span>{{ action.label }}</span>
1316
+ </button>
1317
+ }
1318
+ </div>
1319
+ }
1320
+
1321
+ <header class="ngxsp-yt-topbar" data-tauri-drag-region>
1322
+ <div class="ngxsp-yt-brand">
1323
+ <button class="ngxsp-yt-railMenu" type="button" [attr.aria-expanded]="railExpanded" [attr.aria-label]="menuLabel" [title]="menuLabel" (click)="toggleRail()">
1324
+ <ngxsp-smart-icon name="menu" />
1325
+ </button>
1326
+ @if (brandLogoSrc) {
1327
+ <img class="ngxsp-yt-brandLogo" [src]="brandLogoSrc" alt="" width="26" height="26" />
1328
+ } @else {
1329
+ <span class="ngxsp-yt-brandIcon" aria-hidden="true"><ngxsp-smart-icon name="play" /></span>
1330
+ }
1331
+ <strong>NGXSP Video</strong>
1332
+ </div>
1333
+
1334
+ <label class="ngxsp-yt-search">
1335
+ <ngxsp-smart-icon name="search" />
1336
+ <input type="search" [placeholder]="searchPlaceholder" [value]="searchTerm" (input)="onSearchInput($event)" (focus)="searchOpen = true" [attr.aria-label]="searchLabel" />
1337
+ </label>
1338
+
1339
+ <div class="ngxsp-yt-actions">
1340
+ @if (statusMessage) {
1341
+ <span class="ngxsp-yt-status" [class.success]="statusKind === 'success'" [class.error]="statusKind === 'error'" [attr.role]="statusKind === 'error' ? 'alert' : 'status'" [attr.aria-live]="statusKind === 'error' ? 'assertive' : 'polite'" aria-atomic="true" [title]="statusMessage">
1342
+ {{ statusMessage }}
1343
+ </span>
1344
+ }
1345
+ @if (runtimeModeInHeader) {
1346
+ <div class="ngxsp-yt-mode" role="group" [attr.aria-label]="runtimeModeLabel">
1347
+ <button type="button" [class.active]="runtimeMode === 'online'" [attr.aria-pressed]="runtimeMode === 'online'" (click)="selectRuntimeMode('online')"><i aria-hidden="true"></i>{{ onlineLabel }}</button>
1348
+ <button type="button" [class.active]="runtimeMode === 'offline'" [attr.aria-pressed]="runtimeMode === 'offline'" (click)="selectRuntimeMode('offline')"><i aria-hidden="true"></i>{{ offlineLabel }}</button>
1349
+ </div>
1350
+ }
1351
+ @if (folderImportLabel) {
1352
+ <!-- A folder is what builds the library; picking a single file next to it led nowhere. -->
1353
+ <button class="ngxsp-yt-sourceButton" type="button" [disabled]="libraryBusy" [attr.aria-label]="folderImportLabel" [title]="folderImportLabel" (click)="addFolder.emit()">
1354
+ <ngxsp-smart-icon name="folder" />
1355
+ <span>{{ folderImportLabel }}</span>
1356
+ </button>
1357
+ } @else {
1358
+ <button class="ngxsp-yt-sourceButton" type="button" [attr.aria-label]="sourceChangeTitle || sourceChangeLabel" [title]="sourceChangeTitle || sourceChangeLabel" (click)="videoFileInput.click()">
1359
+ <ngxsp-smart-icon name="folder" />
1360
+ <span>{{ sourceChangeLabel }}</span>
1361
+ </button>
1362
+ }
1363
+ <input #videoFileInput class="ngxsp-yt-fileInput" type="file" [accept]="sourceChangeAccept" aria-hidden="true" tabindex="-1" (change)="onSourceFileInput($event)" />
1364
+ @if (languageOptions.length) {
1365
+ <div class="ngxsp-yt-languageSwitch" role="group" [attr.aria-label]="languageLabel">
1366
+ @for (language of languageOptions; track language.id) {
1367
+ <button type="button" [class.active]="language.id === activeLanguage" [attr.aria-pressed]="language.id === activeLanguage" (click)="languageChange.emit(language.id)">
1368
+ {{ language.label }}
1369
+ </button>
1370
+ }
1371
+ </div>
1372
+ }
1373
+ @if (compactModes && compactMode === 'full') {
1374
+ <button class="ngxsp-yt-round ngxsp-yt-windowButton" type="button" [attr.aria-label]="compactMiniLabel" [title]="compactMiniLabel" (click)="compactModeChange.emit('mini')">
1375
+ <ngxsp-smart-icon name="window-mini" />
1376
+ </button>
1377
+ }
1378
+ <button class="ngxsp-yt-round ngxsp-yt-queueButton" type="button" [class.active]="queuePanelActive" [attr.aria-pressed]="queuePanelActive" [attr.aria-label]="queueActionLabel" [title]="queueActionLabel" (click)="onQueueAction()">
1379
+ <ngxsp-smart-icon name="panel-right" />
1380
+ </button>
1381
+ <button class="ngxsp-yt-round ngxsp-yt-notificationsButton" type="button" [attr.aria-label]="notificationsLabel" [title]="notificationsLabel" (click)="toolSelect.emit('notifications')">
1382
+ <ngxsp-smart-icon name="bell" />
1383
+ </button>
1384
+ <button class="ngxsp-yt-round" type="button" [attr.aria-label]="settingsLabel" [title]="settingsLabel" (click)="toolSelect.emit('settings')">
1385
+ <ngxsp-smart-icon name="settings" />
1386
+ </button>
1387
+ </div>
1388
+ </header>
1389
+
1390
+ <aside class="ngxsp-yt-rail" [attr.aria-label]="navigationLabel">
1391
+ @for (item of navItems; track item.id) {
1392
+ <button class="ngxsp-yt-railButton" type="button" [class.active]="item.active" [disabled]="item.disabled" [attr.aria-current]="item.active ? 'page' : null" [attr.aria-label]="item.label" [title]="item.label" (click)="selectNav(item)">
1393
+ <ngxsp-smart-icon [name]="item.icon" />
1394
+ <span>{{ item.label }}</span>
1395
+ </button>
1396
+ }
1397
+ </aside>
1398
+
1399
+ <div class="ngxsp-yt-body" [class.ngxsp-yt-playing]="playerView" [class.ngxsp-yt-sources]="!showActiveMedia" [class.ngxsp-yt-hasInfoTabs]="infoTabs.length > 0" [class.ngxsp-yt-sectionsBeside]="sidePanelContent === 'sections' && queueVisible">
1400
+ <section #contentScroll class="ngxsp-yt-watch" [class.player]="playerView" [attr.aria-label]="displayHeroTitle">
1401
+ <!-- Watching: the player has the whole area. Browsing: the area belongs to the view being read. -->
1402
+ @if (playerView) {
1403
+ <section class="ngxsp-yt-playerShell">
1404
+ @if (playerView) {
1405
+ <button class="ngxsp-yt-playerBack" type="button" [attr.aria-label]="playerBackLabel" [title]="playerBackLabel" (click)="playerBack.emit()">
1406
+ <ngxsp-smart-icon name="chevron-left" />
1407
+ <span>{{ playerBackLabel }}</span>
1408
+ </button>
1409
+ }
1410
+ <div
1411
+ class="ngxsp-yt-playerFrame"
1412
+ [class.empty]="!config"
1413
+ [class.ngxsp-yt-infoOpen]="!!infoPanel"
1414
+ (pointerdown)="onFramePointerDown($event)"
1415
+ (pointermove)="onFramePointerMove($event)"
1416
+ (pointerup)="onFramePointerUp($event)"
1417
+ (pointercancel)="onFramePointerUp($event)"
1418
+ >
1419
+ @if (brightness < 100) {
1420
+ <span class="ngxsp-yt-veil" aria-hidden="true" [style.opacity]="(100 - brightness) / 100"></span>
1421
+ }
1422
+ @if (touchAdjusting; as adjusting) {
1423
+ <span class="ngxsp-yt-touchBadge" role="status" aria-live="polite">
1424
+ <ngxsp-smart-icon [name]="adjusting.kind === 'brightness' ? 'spark' : adjusting.value === 0 ? 'volume-mute' : adjusting.value < 50 ? 'volume-low' : 'volume-high'" />
1425
+ <strong>{{ adjusting.value }}%</strong>
1426
+ <em><i [style.width.%]="adjusting.value"></i></em>
1427
+ </span>
1428
+ }
1429
+ @if (compactMode === 'mini') {
1430
+ <!-- A mini window has no title bar: this strip moves it. -->
1431
+ <div class="ngxsp-yt-compactDrag" aria-hidden="true"></div>
1432
+ }
1433
+ @if (showPosterPreview) {
1434
+ <button class="ngxsp-yt-previewPlayer" type="button" [attr.aria-label]="playLabel + ' - ' + displayHeroTitle" (click)="togglePrimaryPlayback()">
1435
+ <img [src]="displayHeroArtwork" alt="" />
1436
+ <span class="ngxsp-yt-previewOverlay" aria-hidden="true">
1437
+ <span class="ngxsp-yt-previewPlay"><ngxsp-smart-icon name="play" /></span>
1438
+ </span>
1439
+ </button>
1440
+ } @else if (config) {
1441
+ <ngx-smart-player
1442
+ [config]="config"
1443
+ [plugins]="plugins"
1444
+ [ports]="ports"
1445
+ [engines]="engines"
1446
+ [debug]="debug"
1447
+ [startTimeSec]="startTimeSec"
1448
+ [playRequest]="playRequest"
1449
+ [canPrev]="canPrev"
1450
+ [canNext]="canNext"
1451
+ [previousLabel]="previousLabel"
1452
+ [nextLabel]="nextLabel"
1453
+ [playLabel]="playLabel"
1454
+ [pauseLabel]="pauseLabel"
1455
+ [shuffleActive]="shuffleActive"
1456
+ [shuffleLabel]="shuffleLabel"
1457
+ [repeatMode]="repeatMode"
1458
+ [repeatLabel]="repeatLabel"
1459
+ [hasPlaylistControls]="hasPlaylistControls"
1460
+ [mediaMode]="'video'"
1461
+ [castEnabled]="castEnabled"
1462
+ [castConnected]="castConnected"
1463
+ [menuLabels]="playerMenuLabels"
1464
+ [documentShortcuts]="false"
1465
+ [nowPlayingLabel]="nowPlayingLabel"
1466
+ [fullscreenHintLabel]="fullscreenHintLabel"
1467
+ [fullscreenLabel]="fullscreenLabel"
1468
+ [readyStatusLabel]="readyStatusLabel"
1469
+ [playingStatusLabel]="playingStatusLabel"
1470
+ [pausedStatusLabel]="pausedStatusLabel"
1471
+ [bufferingStatusLabel]="bufferingStatusLabel"
1472
+ [loadingStatusLabel]="loadingStatusLabel"
1473
+ [endedStatusLabel]="endedStatusLabel"
1474
+ [errorStatusLabel]="errorStatusLabel"
1475
+ [outputVolume]="outputVolume / 100"
1476
+ [outputVolumeLabel]="outputVolumeLabel"
1477
+ [outputMuteLabel]="outputMuteLabel"
1478
+ [outputUnmuteLabel]="outputUnmuteLabel"
1479
+ [timelineDurationSec]="timelineDurationSec"
1480
+ [timelineMarkers]="timelineMarkers"
1481
+ (ended)="ended.emit()"
1482
+ (videoSize)="videoSize.emit($event)"
1483
+ (progress)="progress.emit($event)"
1484
+ (prev)="prev.emit()"
1485
+ (next)="next.emit()"
1486
+ (toggleShuffle)="toggleShuffle.emit()"
1487
+ (cycleRepeat)="cycleRepeat.emit()"
1488
+ (cast)="cast.emit()"
1489
+ (subtitlesLoaded)="subtitlesLoaded.emit($event)"
1490
+ (stateChange)="onPlayerStateChange($event)"
1491
+ (playbackToggle)="primaryPlaybackToggle.emit($event)"
1492
+ (outputVolumeChange)="onOutputVolumeRatioChange($event)"
1493
+ >
1494
+ <div ngxsp-player-inline-controls class="ngxsp-yt-playerInlineControls">
1495
+ <!-- What a viewer does about the video they are watching, without leaving it. -->
1496
+ @if (watchActionsInPlayer && compactMode !== 'mini') {
1497
+ <button class="ngxsp-yt-playerAction" type="button" [class.active]="likeActive" [attr.aria-pressed]="likeActive" [attr.aria-label]="likeLabel" [title]="likeLabel" (click)="toolSelect.emit('like')">
1498
+ <ngxsp-smart-icon name="thumb-up" />
1499
+ </button>
1500
+ <button class="ngxsp-yt-playerAction" type="button" [class.active]="watchLaterActive" [attr.aria-pressed]="watchLaterActive" [attr.aria-label]="watchLaterLabel" [title]="watchLaterLabel" (click)="watchLaterToggle.emit()">
1501
+ <ngxsp-smart-icon name="clock" />
1502
+ </button>
1503
+ <button class="ngxsp-yt-playerAction" type="button" [attr.aria-label]="queueAddActiveLabel" [title]="queueAddActiveLabel" (click)="queueAddActive.emit()">
1504
+ <ngxsp-smart-icon name="playlist-plus" />
1505
+ </button>
1506
+ <button class="ngxsp-yt-playerAction" type="button" [attr.aria-label]="shareLabel" [title]="shareLabel" (click)="toolSelect.emit('share')">
1507
+ <ngxsp-smart-icon name="share" />
1508
+ </button>
1509
+ }
1510
+ @if (compactMode === 'mini') {
1511
+ <button class="ngxsp-yt-playerAction" type="button" [attr.aria-label]="compactRestoreLabel" [title]="compactRestoreLabel" (click)="compactModeChange.emit('full')">
1512
+ <ngxsp-smart-icon name="window-restore" />
1513
+ </button>
1514
+ } @else if (compactModes) {
1515
+ <button class="ngxsp-yt-playerAction" type="button" [attr.aria-label]="compactMiniLabel" [title]="compactMiniLabel" (click)="compactModeChange.emit('mini')">
1516
+ <ngxsp-smart-icon name="window-mini" />
1517
+ </button>
1518
+ }
1519
+ <button class="ngxsp-yt-playerAction" type="button" [class.active]="cinemaMode" [attr.aria-pressed]="cinemaMode" [attr.aria-label]="cinemaButtonLabel" [title]="cinemaButtonLabel" (click)="toggleCinema.emit()">
1520
+ <ngxsp-smart-icon [name]="cinemaMode ? 'cinema-active' : 'cinema'" />
1521
+ </button>
1522
+ <button class="ngxsp-yt-playerAction ngxsp-yt-queueButton" type="button" [class.active]="queuePanelActive" [attr.aria-expanded]="queuePanelActive" [attr.aria-label]="queueActionLabel" [title]="queueActionLabel" (click)="onQueueAction()">
1523
+ <ngxsp-smart-icon name="panel-right" />
1524
+ </button>
1525
+ @if (infoTabs.length) {
1526
+ <button class="ngxsp-yt-playerAction ngxsp-yt-infoButton" type="button" [class.active]="!!infoPanel" [attr.aria-pressed]="!!infoPanel" [attr.aria-label]="infoLabel" [title]="infoLabel" (click)="infoToggle.emit()">
1527
+ <ngxsp-smart-icon name="info" />
1528
+ </button>
1529
+ }
1530
+ </div>
1531
+ </ngx-smart-player>
1532
+ } @else {
1533
+ <div class="ngxsp-yt-emptyPlayer">
1534
+ <button class="ngxsp-yt-bigPlay" type="button" [attr.aria-label]="primarySourceLabel" (click)="isOfflineMode ? videoFileInput.click() : onPrimarySourceAction()">
1535
+ <ngxsp-smart-icon [name]="primarySourceIcon" />
1536
+ </button>
1537
+ <span>{{ emptyKicker }}</span>
1538
+ <strong>{{ emptyTitle }}</strong>
1539
+ <p>{{ emptyBody }}</p>
1540
+ </div>
1541
+ }
1542
+ @if (resumePromptShown && resumeOffer && config && !showPosterPreview) {
1543
+ <div class="ngxsp-yt-resumePrompt" role="group" [attr.aria-label]="resumeOffer.message">
1544
+ <span>{{ resumeOffer.message }}</span>
1545
+ <button class="primary" type="button" (click)="chooseResume('resume')">{{ resumeOffer.resumeLabel }}</button>
1546
+ <button type="button" (click)="chooseResume('restart')">{{ resumeOffer.restartLabel }}</button>
1547
+ </div>
1548
+ }
1549
+ @if (infoPanel; as panel) {
1550
+ <!-- Info panels float over the right of the picture; on phones they open in the sheet instead. -->
1551
+ <section class="ngxsp-yt-infoOverlay" [attr.aria-label]="panel.title">
1552
+ <div class="ngxsp-yt-infoTabs" role="tablist" [attr.aria-label]="infoLabel">
1553
+ @for (tab of infoTabs; track tab.id) {
1554
+ <button type="button" role="tab" [class.active]="tab.id === panel.id" [attr.aria-selected]="tab.id === panel.id" [title]="tab.label" (click)="infoTabSelect.emit(tab.id)">
1555
+ <ngxsp-smart-icon [name]="tab.icon" />
1556
+ <span>{{ tab.label }}</span>
1557
+ </button>
1558
+ }
1559
+ <button class="ngxsp-yt-infoClose" type="button" [attr.aria-label]="inspectorCloseLabel" [title]="inspectorCloseLabel" (click)="closeInspector.emit()">
1560
+ <ngxsp-smart-icon name="close" />
1561
+ </button>
1562
+ </div>
1563
+ <div class="ngxsp-yt-infoHead" role="tabpanel">
1564
+ <strong>{{ panel.title }}</strong>
1565
+ @if (panel.subtitle) { <small>{{ panel.subtitle }}</small> }
1566
+ </div>
1567
+ <ng-container *ngTemplateOutlet="panelBody; context: { $implicit: panel }" />
1568
+ </section>
1569
+ }
1570
+ @if (showPosterPreview || !config) {
1571
+ <section class="ngxsp-yt-transportBar" [attr.aria-label]="transportLabel">
1572
+ <div class="ngxsp-yt-transportCluster primary">
1573
+ <button class="ngxsp-yt-transportButton" type="button" [disabled]="!canPrev" [attr.aria-label]="previousLabel" [title]="previousLabel" (click)="prev.emit()">
1574
+ <ngxsp-smart-icon name="prev" />
1575
+ </button>
1576
+ <button class="ngxsp-yt-transportPlay" type="button" [attr.aria-label]="isPlaying ? pauseLabel : playLabel" [title]="isPlaying ? pauseLabel : playLabel" (click)="togglePrimaryPlayback()">
1577
+ @if (isPlaying) { <ngxsp-smart-icon name="pause" /> } @else { <ngxsp-smart-icon name="play" /> }
1578
+ </button>
1579
+ <button class="ngxsp-yt-transportButton" type="button" [disabled]="!canNext" [attr.aria-label]="nextLabel" [title]="nextLabel" (click)="next.emit()">
1580
+ <ngxsp-smart-icon name="next" />
1581
+ </button>
1582
+ </div>
1583
+ <div class="ngxsp-yt-transportInfo" aria-hidden="true">
1584
+ <span>{{ heroEyebrow }}</span>
1585
+ <strong>{{ displayHeroTitle }}</strong>
1586
+ </div>
1587
+ <div class="ngxsp-yt-transportCluster secondary">
1588
+ <button class="ngxsp-yt-transportButton" type="button" [class.active]="repeatMode !== 'off'" [attr.aria-pressed]="repeatMode !== 'off'" [attr.aria-label]="repeatLabel" [title]="repeatLabel" (click)="cycleRepeat.emit()">
1589
+ <ngxsp-smart-icon [name]="repeatMode === 'one' ? 'repeat-one' : 'repeat'" />
1590
+ </button>
1591
+ <ngxsp-volume-control
1592
+ class="ngxsp-yt-volumeControl"
1593
+ [volume]="outputVolume / 100"
1594
+ [step]="0.05"
1595
+ [volumeLabel]="outputVolumeLabel"
1596
+ [muteLabel]="outputMuteLabel"
1597
+ [unmuteLabel]="outputUnmuteLabel"
1598
+ (muteToggle)="outputMuteToggle.emit()"
1599
+ (volumeChange)="onOutputVolumeRatioChange($event)"
1600
+ />
1601
+ <button class="ngxsp-yt-transportButton ngxsp-yt-transportCinema" type="button" [class.active]="cinemaMode" [attr.aria-pressed]="cinemaMode" [attr.aria-label]="cinemaButtonLabel" [title]="cinemaButtonLabel" (click)="toggleCinema.emit()">
1602
+ <ngxsp-smart-icon [name]="cinemaMode ? 'cinema-active' : 'cinema'" />
1603
+ </button>
1604
+ <button class="ngxsp-yt-transportButton ngxsp-yt-queueButton" type="button" [class.active]="queuePanelActive" [attr.aria-expanded]="queuePanelActive" [attr.aria-label]="queueActionLabel" [title]="queueActionLabel" (click)="onQueueAction()">
1605
+ <ngxsp-smart-icon name="panel-right" />
1606
+ </button>
1607
+ @if (compactMode === 'mini') {
1608
+ <button class="ngxsp-yt-transportButton" type="button" [attr.aria-label]="compactRestoreLabel" [title]="compactRestoreLabel" (click)="compactModeChange.emit('full')">
1609
+ <ngxsp-smart-icon name="window-restore" />
1610
+ </button>
1611
+ } @else if (compactModes) {
1612
+ <button class="ngxsp-yt-transportButton" type="button" [attr.aria-label]="compactMiniLabel" [title]="compactMiniLabel" (click)="compactModeChange.emit('mini')">
1613
+ <ngxsp-smart-icon name="window-mini" />
1614
+ </button>
1615
+ }
1616
+ </div>
1617
+ </section>
1618
+ }
1619
+ </div>
1620
+ </section>
1621
+ } @else {
1622
+
1623
+ <!-- What is being watched. A home view opens on where videos come from instead. -->
1624
+ @if (showActiveMedia) {
1625
+ <section class="ngxsp-yt-metaBar">
1626
+ <div class="ngxsp-yt-titleGroup" [attr.aria-label]="displayHeroTitle + ' - ' + displayHeroSubtitle">
1627
+ <h1>{{ displayHeroTitle }}</h1>
1628
+ @if (displayHeroSubtitle) { <p>{{ displayHeroSubtitle }}</p> }
1629
+ @if (displayHeroMeta) { <small>{{ displayHeroMeta }}</small> }
1630
+ </div>
1631
+ <div class="ngxsp-yt-watchActions">
1632
+ <span class="ngxsp-yt-rating">
1633
+ <button type="button" [class.active]="likeActive" [attr.aria-pressed]="likeActive" [attr.aria-label]="likeLabel" [title]="likeLabel" (click)="toolSelect.emit('like')"><ngxsp-smart-icon name="thumb-up" /><span>{{ likeLabel }}</span></button>
1634
+ <button type="button" [attr.aria-label]="dislikeLabel" [title]="dislikeLabel" (click)="toolSelect.emit('dislike')"><ngxsp-smart-icon name="thumb-down" /><span>{{ dislikeLabel }}</span></button>
1635
+ </span>
1636
+ <button type="button" [attr.aria-label]="shareLabel" [title]="shareLabel" (click)="toolSelect.emit('share')"><ngxsp-smart-icon name="share" /><span>{{ shareLabel }}</span></button>
1637
+ <button type="button" [class.active]="watchLaterActive" [attr.aria-pressed]="watchLaterActive" [attr.aria-label]="watchLaterLabel" [title]="watchLaterLabel" (click)="watchLaterToggle.emit()"><ngxsp-smart-icon name="clock" /><span>{{ watchLaterLabel }}</span></button>
1638
+ <button type="button" [attr.aria-label]="queueAddActiveLabel" [title]="queueAddActiveLabel" (click)="queueAddActive.emit()"><ngxsp-smart-icon name="playlist-plus" /><span>{{ queueAddActiveShortLabel }}</span></button>
1639
+ @if (revealFileLabel) {
1640
+ <button type="button" [attr.aria-label]="revealFileLabel" [title]="revealFileLabel" (click)="revealFile.emit()"><ngxsp-smart-icon name="folder" /><span>{{ revealFileLabel }}</span></button>
1641
+ }
1642
+ <button type="button" class="ngxsp-yt-moreAction" [attr.aria-label]="moreLabel" [title]="moreLabel" (click)="toolSelect.emit('more')"><ngxsp-smart-icon name="more" /><span>{{ moreShortLabel }}</span></button>
1643
+ </div>
1644
+ </section>
1645
+ }
1646
+
1647
+ <!-- Phones: the queue is a sheet, and this row is the one place that opens it. -->
1648
+ <button class="ngxsp-yt-queueButton ngxsp-yt-queueOpen" type="button" [attr.aria-expanded]="queueSheetOpen" [title]="queueActionLabel" (click)="onQueueAction()">
1649
+ <ngxsp-smart-icon name="panel-right" />
1650
+ <span>{{ sidePanelTitle }}</span>
1651
+ <small>{{ sidePanelCount }}</small>
1652
+ </button>
1653
+
1654
+ @if (toolCards.length) {
1655
+ <section class="ngxsp-yt-toolRow">
1656
+ @for (tool of toolCards; track tool.id) {
1657
+ <button class="ngxsp-yt-tool" type="button" [class.active]="tool.active" [attr.data-tool]="tool.id" [attr.data-tone]="tool.tone || 'neutral'" (click)="toolSelect.emit(tool.id)">
1658
+ <span class="ngxsp-yt-toolIcon" aria-hidden="true"><ngxsp-smart-icon [name]="tool.icon" /></span>
1659
+ <span class="ngxsp-yt-toolCopy"><small>{{ tool.label }}</small><strong>{{ tool.value }}</strong></span>
1660
+ </button>
1661
+ }
1662
+ </section>
1663
+ }
1664
+
1665
+ @if (hasSearchQuery) {
1666
+ <section class="ngxsp-yt-searchWorkspace" [attr.aria-label]="searchResultsTitle">
1667
+ <div class="ngxsp-yt-searchHead">
1668
+ <div>
1669
+ <strong>{{ searchResultsTitle }}</strong>
1670
+ @if (searchResultsSummary) { <small>{{ searchResultsSummary }}</small> }
1671
+ </div>
1672
+ <button type="button" [attr.aria-label]="searchClearLabel" [title]="searchClearLabel" (click)="clearSearch()">
1673
+ <ngxsp-smart-icon name="close" />
1674
+ </button>
1675
+ </div>
1676
+ @if (searchFilters.length) {
1677
+ <div class="ngxsp-yt-searchFilters" role="group" [attr.aria-label]="searchFilterLabel">
1678
+ @for (filter of searchFilters; track filter.id) {
1679
+ <button type="button" [class.active]="filter.active" [disabled]="filter.disabled" [attr.aria-pressed]="!!filter.active" (click)="selectSearchFilter(filter)">
1680
+ <span>{{ filter.label }}</span>
1681
+ @if (filter.count !== undefined) { <small>{{ filter.count }}</small> }
1682
+ </button>
1683
+ }
1684
+ </div>
1685
+ }
1686
+ <div class="ngxsp-yt-searchResults" [class.list]="activeViewMode === 'list'">
1687
+ @for (card of searchResultItems; track card.sectionId + ':' + card.item.id) {
1688
+ <article class="ngxsp-yt-searchResult" [class.active]="card.item.active" [attr.data-art]="artVariant(card)">
1689
+ <button class="ngxsp-yt-searchResultMain" type="button" [attr.aria-current]="card.item.active ? 'true' : null" [attr.aria-label]="searchResultActionLabel('play', card)" [title]="searchResultActionLabel('play', card)" (click)="selectCard(card)">
1690
+ <span class="ngxsp-yt-searchThumb" [class.has-image]="!!card.item.artworkUrl">
1691
+ @if (card.item.artworkUrl) {
1692
+ <img [src]="card.item.artworkUrl" alt="" />
1693
+ } @else {
1694
+ <span class="ngxsp-yt-art" aria-hidden="true"><ngxsp-smart-icon name="video" /></span>
1695
+ }
1696
+ @if (card.item.duration && !card.item.artworkUrl) { <small>{{ card.item.duration }}</small> }
1697
+ </span>
1698
+ <span class="ngxsp-yt-searchCopy">
1699
+ @if (card.item.kindLabel || card.item.episodeLabel) { <small>{{ card.item.episodeLabel || card.item.kindLabel }}</small> }
1700
+ <strong>{{ card.item.title }}</strong>
1701
+ @if (card.item.subtitle) { <em>{{ card.item.subtitle }}</em> }
1702
+ @if (mediaCardMeta(card.item)) { <span>{{ mediaCardMeta(card.item) }}</span> }
1703
+ </span>
1704
+ </button>
1705
+ <span class="ngxsp-yt-searchActions">
1706
+ <button type="button" [attr.aria-label]="searchResultActionLabel('queue', card)" [title]="searchResultActionLabel('queue', card)" (click)="searchResultQueueAdd.emit(card.item.id)">
1707
+ <ngxsp-smart-icon name="playlist-plus" />
1708
+ </button>
1709
+ <button type="button" [attr.aria-label]="searchResultActionLabel('watchLater', card)" [title]="searchResultActionLabel('watchLater', card)" (click)="searchResultWatchLater.emit(card.item.id)">
1710
+ <ngxsp-smart-icon name="clock" />
1711
+ </button>
1712
+ <button type="button" [attr.aria-label]="searchResultActionLabel('favorite', card)" [title]="searchResultActionLabel('favorite', card)" (click)="searchResultFavorite.emit(card.item.id)">
1713
+ <ngxsp-smart-icon name="heart" />
1714
+ </button>
1715
+ <button type="button" [attr.aria-label]="searchResultActionLabel('download', card)" [title]="searchResultActionLabel('download', card)" (click)="searchResultDownload.emit(card.item.id)">
1716
+ <ngxsp-smart-icon name="download" />
1717
+ </button>
1718
+ </span>
1719
+ </article>
1720
+ } @empty {
1721
+ <div class="ngxsp-yt-searchEmpty">
1722
+ <ngxsp-smart-icon name="search" />
1723
+ <strong>{{ searchResultsEmptyTitle }}</strong>
1724
+ <span>{{ searchResultsEmptyBody }}</span>
1725
+ </div>
1726
+ }
1727
+ </div>
1728
+ </section>
1729
+ }
1730
+
1731
+ @if (showActiveMedia) {
1732
+ @if (mediaDetail; as detail) {
1733
+ <section class="ngxsp-yt-mediaDetail" [attr.aria-label]="detail.title">
1734
+ <div class="ngxsp-yt-mediaHero" [class.echoesHeader]="detailEchoesHeader">
1735
+ <div class="ngxsp-yt-mediaPoster" [class.has-image]="!!detail.posterUrl">
1736
+ @if (detail.posterUrl) {
1737
+ <img [src]="detail.posterUrl" alt="" />
1738
+ } @else {
1739
+ <span>{{ posterInitial(detail.title) }}</span>
1740
+ }
1741
+ </div>
1742
+ <div class="ngxsp-yt-mediaDetailBody">
1743
+ <span class="ngxsp-yt-mediaKicker">{{ detail.kicker || detail.kindLabel || heroEyebrow }}</span>
1744
+ <h2>{{ detail.title }}</h2>
1745
+ @if (detail.subtitle) { <p class="ngxsp-yt-mediaSubtitle">{{ detail.subtitle }}</p> }
1746
+ @if (detail.metaItems?.length) {
1747
+ <div class="ngxsp-yt-mediaMeta">
1748
+ @for (meta of detail.metaItems; track meta) {
1749
+ <span>{{ meta }}</span>
1750
+ }
1751
+ </div>
1752
+ }
1753
+ @if (detail.summary) { <p class="ngxsp-yt-mediaSummary">{{ detail.summary }}</p> }
1754
+ @if (showResumeProgress && detail.progressPercent !== undefined) {
1755
+ <div class="ngxsp-yt-mediaProgress">
1756
+ <span><i [style.width.%]="safeProgressPercent(detail.progressPercent)"></i></span>
1757
+ @if (detail.progressLabel) { <small>{{ detail.progressLabel }}</small> }
1758
+ </div>
1759
+ }
1760
+ @if (detail.tags?.length) {
1761
+ <div class="ngxsp-yt-mediaTags">
1762
+ @for (tag of detail.tags; track tag) {
1763
+ <span>{{ tag }}</span>
1764
+ }
1765
+ </div>
1766
+ }
1767
+ @if (detail.actions?.length) {
1768
+ <div class="ngxsp-yt-mediaActions">
1769
+ @for (action of detail.actions; track action.id) {
1770
+ <button type="button" [class.primary]="action.primary" [disabled]="action.disabled" (click)="detailAction.emit(action.id)">
1771
+ <ngxsp-smart-icon [name]="action.icon" />
1772
+ <span>{{ action.label }}</span>
1773
+ </button>
1774
+ }
1775
+ </div>
1776
+ }
1777
+ </div>
1778
+ </div>
1779
+ @if (detail.chapters?.length || detail.checklist?.length) {
1780
+ <div class="ngxsp-yt-mediaWorkspace">
1781
+ @if (detail.workspaceTitle || detail.workspaceSummary) {
1782
+ <div class="ngxsp-yt-mediaWorkspaceHead">
1783
+ @if (detail.workspaceTitle) { <strong>{{ detail.workspaceTitle }}</strong> }
1784
+ @if (detail.workspaceSummary) { <small>{{ detail.workspaceSummary }}</small> }
1785
+ </div>
1786
+ }
1787
+ <div class="ngxsp-yt-mediaWorkspaceGrid">
1788
+ @if (detail.chapters?.length) {
1789
+ <div class="ngxsp-yt-chapterPanel">
1790
+ @if (detail.chaptersTitle) { <strong class="ngxsp-yt-panelTitle">{{ detail.chaptersTitle }}</strong> }
1791
+ <div class="ngxsp-yt-chapterList">
1792
+ @for (chapter of detail.chapters; track chapter.id) {
1793
+ <button type="button" class="ngxsp-yt-mediaChapter" [class.active]="chapter.active" (click)="selectMediaChapter(chapter)">
1794
+ <span class="ngxsp-yt-chapterTime">
1795
+ @if (chapter.time) { <em>{{ chapter.time }}</em> }
1796
+ @if (chapter.label) { <b>{{ chapter.label }}</b> }
1797
+ </span>
1798
+ <span class="ngxsp-yt-chapterCopy">
1799
+ <strong>{{ chapter.title }}</strong>
1800
+ @if (chapter.summary) { <small>{{ chapter.summary }}</small> }
1801
+ </span>
1802
+ </button>
1803
+ }
1804
+ </div>
1805
+ </div>
1806
+ }
1807
+ @if (detail.checklist?.length) {
1808
+ <div class="ngxsp-yt-checkPanel">
1809
+ @if (detail.checklistTitle) { <strong class="ngxsp-yt-panelTitle">{{ detail.checklistTitle }}</strong> }
1810
+ <div class="ngxsp-yt-checkList">
1811
+ @for (item of detail.checklist; track item.id) {
1812
+ <div class="ngxsp-yt-checkItem" [attr.data-tone]="item.tone || 'neutral'">
1813
+ <span aria-hidden="true"></span>
1814
+ <div>
1815
+ <strong>{{ item.label }}</strong>
1816
+ @if (item.description) { <small>{{ item.description }}</small> }
1817
+ </div>
1818
+ @if (item.value) { <em>{{ item.value }}</em> }
1819
+ </div>
1820
+ }
1821
+ </div>
1822
+ </div>
1823
+ }
1824
+ </div>
1825
+ </div>
1826
+ }
1827
+ @if (detail.episodes?.length) {
1828
+ <div class="ngxsp-yt-episodeRail">
1829
+ @if (detail.episodesTitle) { <strong class="ngxsp-yt-panelTitle">{{ detail.episodesTitle }}</strong> }
1830
+ <div>
1831
+ @for (episode of detail.episodes; track episode.id) {
1832
+ <button type="button" [class.active]="episode.active" (click)="selectDetailEpisode.emit(episode.id)">
1833
+ <span class="ngxsp-yt-episodeThumb" [class.has-image]="!!episode.artworkUrl">
1834
+ @if (episode.artworkUrl) {
1835
+ <img [src]="episode.artworkUrl" alt="" />
1836
+ } @else {
1837
+ <i>{{ posterInitial(episode.title) }}</i>
1838
+ }
1839
+ @if (showResumeProgress && episode.progressPercent !== undefined) {
1840
+ <em><i [style.width.%]="safeProgressPercent(episode.progressPercent)"></i></em>
1841
+ }
1842
+ </span>
1843
+ <span>
1844
+ <strong>{{ episode.title }}</strong>
1845
+ @if (episode.subtitle) { <small>{{ episode.subtitle }}</small> }
1846
+ @if (episode.duration) { <em>{{ episode.duration }}</em> }
1847
+ </span>
1848
+ </button>
1849
+ }
1850
+ </div>
1851
+ </div>
1852
+ }
1853
+ </section>
1854
+ }
1855
+ }
1856
+
1857
+ @if (libraryVisible) {
1858
+ <section class="ngxsp-yt-library">
1859
+ <div class="ngxsp-yt-sectionHead">
1860
+ <div>
1861
+ <strong>{{ libraryTitle }}</strong>
1862
+ @if (libraryStatus) {
1863
+ <small class="ngxsp-yt-libraryStatus" [class.busy]="libraryBusy" role="status" aria-live="polite">{{ libraryStatus }}</small>
1864
+ }
1865
+ </div>
1866
+ <button class="ngxsp-yt-sectionLink" type="button" [attr.aria-label]="browseLibraryLabel" [title]="browseLibraryLabel" (click)="toggleLibrary.emit()">
1867
+ <ngxsp-smart-icon name="chevron-up" />
1868
+ </button>
1869
+ </div>
1870
+ @if (trail.length > 1) {
1871
+ <nav class="ngxsp-yt-trail" [attr.aria-label]="trailLabel">
1872
+ @for (step of trail; track step.id; let last = $last) {
1873
+ @if (last) {
1874
+ <b aria-current="page">{{ step.label }}</b>
1875
+ } @else {
1876
+ <button class="ngxsp-yt-trailStep" type="button" (click)="trailStep.emit(step.id)">{{ step.label }}</button>
1877
+ <span aria-hidden="true">/</span>
1878
+ }
1879
+ }
1880
+ </nav>
1881
+ }
1882
+ @for (shelf of librarySections; track shelf.id) {
1883
+ <div class="ngxsp-yt-shelf">
1884
+ @if (shelf.titled) {
1885
+ <div class="ngxsp-yt-shelfHead">
1886
+ <strong>{{ shelf.title }}</strong>
1887
+ <small>{{ shelf.count }}</small>
1888
+ @if (shelf.hidden) {
1889
+ <button class="ngxsp-yt-shelfAll" type="button" [attr.aria-expanded]="isSectionExpanded(shelf.id)" (click)="toggleSection(shelf.id)">{{ isSectionExpanded(shelf.id) ? showLessLabel : showAllLabel }}</button>
1890
+ }
1891
+ </div>
1892
+ }
1893
+ <div class="ngxsp-yt-grid" [class.list]="activeViewMode === 'list'" [class.rail]="activeViewMode !== 'list' && !isSectionExpanded(shelf.id)">
1894
+ @for (card of shelf.cards; track card.sectionId + ':' + card.item.id) {
1895
+ <ng-container *ngTemplateOutlet="mediaCard; context: { $implicit: card }" />
1896
+ }
1897
+ </div>
1898
+ </div>
1899
+ }
1900
+ </section>
1901
+ }
1902
+ }
1903
+ </section>
1904
+
1905
+ <ng-template #mediaCard let-card>
1906
+ <span class="ngxsp-yt-cardWrap">
1907
+ <button class="ngxsp-yt-card" type="button" [class.active]="card.item.active" [class.synthetic]="isSyntheticSection(card.sectionId)" [attr.aria-current]="card.item.active ? 'true' : null" [attr.data-art]="artVariant(card)" (click)="selectCard(card)" (contextmenu)="openCardMenu($event, card.item)">
1908
+ <span class="ngxsp-yt-thumbnail" [class.has-image]="!!card.item.artworkUrl">
1909
+ @if (card.item.artworkUrl) {
1910
+ <img [src]="card.item.artworkUrl" alt="" />
1911
+ } @else {
1912
+ <span class="ngxsp-yt-art" aria-hidden="true"><ngxsp-smart-icon name="video" /></span>
1913
+ }
1914
+ @if (card.item.episodeLabel || card.item.kindLabel) {
1915
+ <small class="ngxsp-yt-thumbKind">{{ card.item.episodeLabel || card.item.kindLabel }}</small>
1916
+ }
1917
+ @if (!card.item.artworkUrl) {
1918
+ <small class="ngxsp-yt-thumbBadge">{{ card.item.duration || card.item.meta || sourceTypeLabel }}</small>
1919
+ }
1920
+ @if (card.item.active) {
1921
+ <span class="ngxsp-yt-nowBadge" aria-hidden="true"><ngxsp-smart-icon [name]="isPlaying ? 'waveform' : 'play'" /></span>
1922
+ }
1923
+ @if (showResumeProgress && card.item.progressPercent !== undefined) {
1924
+ <span class="ngxsp-yt-cardProgress" aria-hidden="true"><i [style.width.%]="safeProgressPercent(card.item.progressPercent)"></i></span>
1925
+ }
1926
+ </span>
1927
+ <span class="ngxsp-yt-cardCopy">
1928
+ <strong>{{ card.item.title }}</strong>
1929
+ @if (card.item.subtitle) { <span>{{ card.item.subtitle }}</span> }
1930
+ @if (mediaCardMeta(card.item)) { <small>{{ mediaCardMeta(card.item) }}</small> }
1931
+ </span>
1932
+ </button>
1933
+ @if (card.item.actions?.length) {
1934
+ <button
1935
+ class="ngxsp-yt-cardMore"
1936
+ type="button"
1937
+ [attr.aria-label]="cardActionsLabel"
1938
+ [attr.aria-expanded]="cardMenu?.card?.id === card.item.id"
1939
+ [title]="cardActionsLabel"
1940
+ (click)="openCardMenu($event, card.item)"
1941
+ >
1942
+ <ngxsp-smart-icon name="more" />
1943
+ </button>
1944
+ }
1945
+ </span>
1946
+ </ng-template>
1947
+
1948
+ <!-- What a panel shows: in the side column, or over the video for the info panels. -->
1949
+ <ng-template #panelBody let-panel>
1950
+ <div class="ngxsp-yt-inspectorScroll">
1951
+ @if (panel.status) {
1952
+ <p class="ngxsp-yt-inspectorStatus">{{ panel.status }}</p>
1953
+ }
1954
+
1955
+ @if (panel.metrics?.length) {
1956
+ <div class="ngxsp-yt-inspectorMetrics">
1957
+ @for (metric of panel.metrics; track metric.label) {
1958
+ <span class="ngxsp-yt-inspectorMetric" [class.success]="metric.tone === 'success'" [class.warning]="metric.tone === 'warning'">
1959
+ <small>{{ metric.label }}</small>
1960
+ <strong>{{ metric.value }}</strong>
1961
+ </span>
1962
+ }
1963
+ </div>
1964
+ }
1965
+
1966
+ @if (panel.settings?.length) {
1967
+ <ngxsp-settings [sections]="panel.settings!" (settingChange)="settingChange.emit($event)" (settingAction)="settingAction.emit($event)" />
1968
+ }
1969
+
1970
+ @if (serverForm; as form) {
1971
+ <section class="ngxsp-yt-servers" [attr.aria-label]="form.heading || form.title">
1972
+ @if (form.heading) { <h2 class="ngxsp-yt-serversHeading">{{ form.heading }}</h2> }
1973
+ @for (server of form.servers; track server.id) {
1974
+ <div class="ngxsp-yt-serverRow" [class.active]="server.active">
1975
+ <button class="ngxsp-yt-serverMain" type="button" [title]="form.openLabel + ' - ' + server.name" (click)="selectServer.emit(server.id)">
1976
+ <ngxsp-smart-icon name="network" />
1977
+ <span>
1978
+ <strong>{{ server.name }}</strong>
1979
+ @if (server.subtitle) { <small>{{ server.subtitle }}</small> }
1980
+ </span>
1981
+ </button>
1982
+ <button class="ngxsp-yt-serverDisconnect" type="button" [attr.aria-label]="form.disconnectLabel + ' - ' + server.name" [title]="form.disconnectLabel" (click)="disconnectServer.emit(server.id)">
1983
+ <ngxsp-smart-icon name="close" />
1984
+ </button>
1985
+ </div>
1986
+ }
1987
+ <form class="ngxsp-yt-serverForm" (submit)="submitServer($event)">
1988
+ <span class="ngxsp-yt-serverTitle">{{ form.title }}</span>
1989
+ @if (form.kinds && form.kinds.length > 1) {
1990
+ <select class="ngxsp-yt-serverKind" [value]="serverKindDraft || form.kinds[0].id" (change)="onServerKindChange($event)" [attr.aria-label]="form.kindLabel || form.title">
1991
+ @for (kind of form.kinds; track kind.id) {
1992
+ <option [value]="kind.id">{{ kind.label }}</option>
1993
+ }
1994
+ </select>
1995
+ }
1996
+ @let serverKind = selectedServerKind(form);
1997
+ <input type="text" inputmode="url" [value]="serverAddressDraft" (input)="onServerAddressInput($event)" [placeholder]="serverKind?.addressPlaceholder || form.addressPlaceholder" [attr.aria-label]="serverKind?.addressPlaceholder || form.addressPlaceholder" />
1998
+ @if (serverKind?.credentials !== false) {
1999
+ <input type="text" autocomplete="username" [value]="serverUsernameDraft" (input)="onServerUsernameInput($event)" [placeholder]="form.usernamePlaceholder" [attr.aria-label]="form.usernamePlaceholder" />
2000
+ <input type="password" autocomplete="current-password" [value]="serverPasswordDraft" (input)="onServerPasswordInput($event)" [placeholder]="form.passwordPlaceholder" [attr.aria-label]="form.passwordPlaceholder" />
2001
+ }
2002
+ <button type="submit" [disabled]="form.busy || !serverAddressDraft.trim()">{{ form.submitLabel }}</button>
2003
+ @if (form.discovery && serverKind?.discoverable) {
2004
+ <button class="secondary" type="button" [disabled]="form.discovery.busy" (click)="discoverServers.emit(serverKind!.id)">{{ form.discovery.busy ? form.discovery.searchingLabel : form.discovery.searchLabel }}</button>
2005
+ }
2006
+ @if (form.hint) { <small class="ngxsp-yt-serverHint">{{ form.hint }}</small> }
2007
+ @if (form.discovery && serverKind?.discoverable && (form.discovery.servers.length || form.discovery.emptyLabel)) {
2008
+ <ul class="ngxsp-yt-discovered" [attr.aria-label]="form.discovery.searchLabel">
2009
+ @for (server of form.discovery.servers; track server.id) {
2010
+ <li>
2011
+ <span><strong>{{ server.name }}</strong>@if (server.subtitle) { <small>{{ server.subtitle }}</small> }</span>
2012
+ <button class="secondary" type="button" [disabled]="form.busy" [attr.aria-label]="form.discovery.connectLabel + ' - ' + server.name" (click)="connectDiscoveredServer.emit(server.id)">{{ form.discovery.connectLabel }}</button>
2013
+ </li>
2014
+ } @empty {
2015
+ <li class="empty">{{ form.discovery.emptyLabel }}</li>
2016
+ }
2017
+ </ul>
2018
+ }
2019
+ </form>
2020
+ </section>
2021
+ }
2022
+
2023
+ @if (panel.trailingSettings?.length) {
2024
+ <ngxsp-settings [sections]="panel.trailingSettings!" (settingChange)="settingChange.emit($event)" (settingAction)="settingAction.emit($event)" />
2025
+ }
2026
+
2027
+ @if (panel.items?.length) {
2028
+ <div class="ngxsp-yt-inspectorList">
2029
+ @for (item of panel.items; track item.label) {
2030
+ <div class="ngxsp-yt-inspectorItem" [class.success]="item.tone === 'success'" [class.warning]="item.tone === 'warning'">
2031
+ <span>
2032
+ <strong>{{ item.label }}</strong>
2033
+ @if (item.description) { <small>{{ item.description }}</small> }
2034
+ </span>
2035
+ @if (item.value) { <em>{{ item.value }}</em> }
2036
+ </div>
2037
+ }
2038
+ </div>
2039
+ }
2040
+ </div>
2041
+
2042
+ @if (panel.actions?.length) {
2043
+ <div class="ngxsp-yt-inspectorActions">
2044
+ @for (action of panel.actions; track action.id) {
2045
+ <button type="button" [class.primary]="action.primary" [disabled]="action.disabled" (click)="inspectorAction.emit(action.id)">
2046
+ <ngxsp-smart-icon [name]="action.icon" />
2047
+ <span>{{ action.label }}</span>
2048
+ </button>
2049
+ }
2050
+ </div>
2051
+ }
2052
+ </ng-template>
2053
+
2054
+ <!-- A row of the side panel: a video of the queue or of the section on show. -->
2055
+ <ng-template #sideRow let-card let-label="label">
2056
+ <button class="ngxsp-yt-suggestionMain" type="button" [attr.aria-current]="card.item.active ? 'true' : null" [attr.aria-label]="label" [title]="label" (click)="selectCard(card)">
2057
+ <span class="ngxsp-yt-suggestionThumb" [class.has-image]="!!card.item.artworkUrl">
2058
+ @if (card.item.artworkUrl) {
2059
+ <img [src]="card.item.artworkUrl" alt="" />
2060
+ } @else {
2061
+ <span class="ngxsp-yt-art" aria-hidden="true"><ngxsp-smart-icon name="video" /></span>
2062
+ }
2063
+ @if (card.item.duration && !card.item.artworkUrl) { <small>{{ card.item.duration }}</small> }
2064
+ @if (card.item.active) {
2065
+ <span class="ngxsp-yt-nowBadge" aria-hidden="true"><ngxsp-smart-icon [name]="isPlaying ? 'waveform' : 'play'" /></span>
2066
+ }
2067
+ @if (showResumeProgress && card.item.progressPercent !== undefined) {
2068
+ <span class="ngxsp-yt-cardProgress" aria-hidden="true"><i [style.width.%]="safeProgressPercent(card.item.progressPercent)"></i></span>
2069
+ }
2070
+ </span>
2071
+ <span class="ngxsp-yt-suggestionCopy">
2072
+ <strong>{{ card.item.title }}</strong>
2073
+ @if (card.item.subtitle) { <span>{{ card.item.subtitle }}</span> }
2074
+ @if (mediaCardMeta(card.item)) { <small>{{ mediaCardMeta(card.item) }}</small> }
2075
+ </span>
2076
+ </button>
2077
+ </ng-template>
2078
+
2079
+ @if (queueVisible || queueSheetOpen) {
2080
+ <aside
2081
+ class="ngxsp-yt-suggestions"
2082
+ #queuePanel
2083
+ tabindex="-1"
2084
+ [class.sheet]="queueSheetOpen"
2085
+ [attr.role]="queueSheetOpen ? 'dialog' : null"
2086
+ [attr.aria-modal]="queueSheetOpen ? 'true' : null"
2087
+ [attr.aria-label]="sidePanelTitle"
2088
+ (keydown.escape)="closeQueueSheet()"
2089
+ >
2090
+ <!-- Phones only: the panel covers the page instead of pushing it down, and closes from here. -->
2091
+ @if (queueSheetOpen) {
2092
+ <div class="ngxsp-yt-sheetHeader">
2093
+ <strong>{{ sidePanel?.title || sidePanelTitle }}</strong>
2094
+ <button class="ngxsp-yt-textButton" type="button" (click)="closeQueueSheet()">{{ closeLabel }}</button>
2095
+ </div>
2096
+ }
2097
+ <div class="ngxsp-yt-panelTabs" role="group" [attr.aria-label]="sidePanelTitle">
2098
+ <button type="button" class="ngxsp-yt-panelPill" [class.active]="isPanelOpen('queue')" [attr.aria-pressed]="isPanelOpen('queue')" (click)="togglePanel('queue')">{{ sidePanelTitle }}</button>
2099
+ @for (panel of knownPanels; track panel.id) {
2100
+ <button type="button" class="ngxsp-yt-panelPill" [class.active]="isPanelOpen(panel.id)" [attr.aria-pressed]="isPanelOpen(panel.id)" (click)="togglePanel(panel.id)">{{ panel.title }}</button>
2101
+ }
2102
+ </div>
2103
+
2104
+ <div class="ngxsp-yt-panelStack">
2105
+ @if (isPanelOpen('queue')) {
2106
+ <section class="ngxsp-yt-stackItem" [class.collapsed]="isPanelCollapsed('queue')">
2107
+ <header class="ngxsp-yt-stackHead">
2108
+ <strong>{{ sidePanelTitle }}</strong>
2109
+ @if (sidePanelCount) { <small>{{ sidePanelCount }}</small> }
2110
+ <button class="ngxsp-yt-stackIcon" type="button" [attr.aria-expanded]="!isPanelCollapsed('queue')" [attr.aria-label]="sidePanelTitle" (click)="togglePanelCollapsed('queue')">
2111
+ <ngxsp-smart-icon [name]="isPanelCollapsed('queue') ? 'chevron-down' : 'chevron-up'" />
2112
+ </button>
2113
+ <button class="ngxsp-yt-stackIcon" type="button" [attr.aria-label]="closeLabel" [title]="closeLabel" (click)="togglePanel('queue')">
2114
+ <ngxsp-smart-icon name="close" />
2115
+ </button>
2116
+ </header>
2117
+ @if (!isPanelCollapsed('queue')) {
2118
+ <!-- A page with nothing to list and nothing to say about it (the lists block above speaks for it) shows no list. -->
2119
+ @let sideListShown = sidePanelContent !== 'sections' || !!sidePanelCount || !!emptyCatalogLabel;
2120
+ @if (sideListShown) {
2121
+ <div class="ngxsp-yt-sectionHead compact">
2122
+ <div>
2123
+ @if (sideBackLabel) {
2124
+ <button class="ngxsp-yt-backButton" type="button" [attr.aria-label]="sideBackLabel" [title]="sideBackLabel" (click)="sideBack.emit()">
2125
+ <ngxsp-smart-icon name="chevron-left" />
2126
+ <span>{{ sideBackLabel }}</span>
2127
+ </button>
2128
+ }
2129
+ <strong>{{ sidePanelTitle }}</strong>
2130
+ @if (sidePanelMeta) { <small>{{ sidePanelMeta }}</small> }
2131
+ </div>
2132
+ <span class="ngxsp-yt-countBadge">{{ sidePanelCount }}</span>
2133
+ </div>
2134
+ }
2135
+ @if (sidePanelContent === 'sections') {
2136
+ @if (sideListShown) {
2137
+ <div class="ngxsp-yt-suggestionList">
2138
+ @for (group of sideSectionGroups; track group.id) {
2139
+ @if (sideSectionGroups.length > 1 && group.cards.length) { <strong class="ngxsp-yt-sideGroupTitle">{{ group.title }}</strong> }
2140
+ @for (card of group.cards; track card.sectionId + ':' + card.item.id) {
2141
+ <article class="ngxsp-yt-suggestion" [class.active]="card.item.active" [attr.data-art]="artVariant(card)">
2142
+ <ng-container *ngTemplateOutlet="sideRow; context: { $implicit: card, label: playLabel + ' - ' + card.item.title }" />
2143
+ </article>
2144
+ }
2145
+ }
2146
+ @if (!sidePanelCount) {
2147
+ <div class="ngxsp-yt-emptyList">{{ emptyCatalogLabel }}</div>
2148
+ }
2149
+ </div>
2150
+ }
2151
+ } @else {
2152
+ <div class="ngxsp-yt-suggestionList" #queueList>
2153
+ @for (card of displayQueueItems; track card.sectionId + ':' + card.item.id) {
2154
+ <article class="ngxsp-yt-suggestion" [class.active]="card.item.active" [class.synthetic]="isSyntheticSection(card.sectionId)" [class.managed]="hasManagedQueue" [attr.data-art]="artVariant(card)">
2155
+ <ng-container *ngTemplateOutlet="sideRow; context: { $implicit: card, label: queueItemActionLabel('play', card) }" />
2156
+ @if (hasManagedQueue) {
2157
+ <span class="ngxsp-yt-queueControls">
2158
+ <button type="button" [disabled]="!canMoveQueueItem(card, 'up')" [attr.aria-label]="queueItemActionLabel('moveUp', card)" [title]="queueItemActionLabel('moveUp', card)" (click)="queueMoveUp.emit(card.item.id)">
2159
+ <ngxsp-smart-icon class="ngxsp-yt-queueUpIcon" name="chevron-up" />
2160
+ </button>
2161
+ <button type="button" [disabled]="!canMoveQueueItem(card, 'down')" [attr.aria-label]="queueItemActionLabel('moveDown', card)" [title]="queueItemActionLabel('moveDown', card)" (click)="queueMoveDown.emit(card.item.id)">
2162
+ <ngxsp-smart-icon class="ngxsp-yt-queueDownIcon" name="chevron-down" />
2163
+ </button>
2164
+ <button type="button" [attr.aria-label]="queueItemActionLabel('remove', card)" [title]="queueItemActionLabel('remove', card)" (click)="queueRemove.emit(card.item.id)">
2165
+ <ngxsp-smart-icon name="close" />
2166
+ </button>
2167
+ </span>
2168
+ }
2169
+ </article>
2170
+ } @empty {
2171
+ <div class="ngxsp-yt-emptyList">{{ emptyCatalogLabel }}</div>
2172
+ }
2173
+ </div>
2174
+ }
2175
+ }
2176
+ </section>
2177
+ }
2178
+ @for (panel of openPanels; track panel.id) {
2179
+ <section class="ngxsp-yt-stackItem" [class.collapsed]="isPanelCollapsed(panel.id)">
2180
+ <section class="ngxsp-yt-inspector" [attr.aria-label]="panel.title">
2181
+ <div class="ngxsp-yt-inspectorHead">
2182
+ <span class="ngxsp-yt-inspectorIcon" aria-hidden="true">
2183
+ <ngxsp-smart-icon [name]="panel.icon || 'settings'" />
2184
+ </span>
2185
+ <span class="ngxsp-yt-inspectorTitle">
2186
+ <strong>{{ panel.title }}</strong>
2187
+ @if (panel.subtitle) { <small>{{ panel.subtitle }}</small> }
2188
+ </span>
2189
+ <button class="ngxsp-yt-inspectorClose" type="button" [attr.aria-expanded]="!isPanelCollapsed(panel.id)" [attr.aria-label]="panel.title" [title]="panel.title" (click)="togglePanelCollapsed(panel.id)">
2190
+ <ngxsp-smart-icon [name]="isPanelCollapsed(panel.id) ? 'chevron-down' : 'chevron-up'" />
2191
+ </button>
2192
+ <button class="ngxsp-yt-inspectorClose" type="button" [attr.aria-label]="inspectorCloseLabel" [title]="inspectorCloseLabel" (click)="closePanel(panel.id)">
2193
+ <ngxsp-smart-icon name="close" />
2194
+ </button>
2195
+ </div>
2196
+
2197
+ @if (!isPanelCollapsed(panel.id)) {
2198
+ <ng-container *ngTemplateOutlet="panelBody; context: { $implicit: panel }" />
2199
+ }
2200
+ </section>
2201
+ </section>
2202
+ }
2203
+ </div>
2204
+ @if (playlistPanel; as lists) {
2205
+ <section class="ngxsp-yt-lists" [attr.aria-label]="lists.title">
2206
+ <div class="ngxsp-yt-sectionHead compact">
2207
+ <div><strong>{{ lists.title }}</strong></div>
2208
+ <span class="ngxsp-yt-countBadge">{{ lists.playlists.length }}</span>
2209
+ </div>
2210
+ <form class="ngxsp-yt-listForm" (submit)="submitCreatePlaylist($event)">
2211
+ <input type="text" [value]="playlistDraftName" (input)="onPlaylistDraftInput($event)" [placeholder]="lists.namePlaceholder" [attr.aria-label]="lists.namePlaceholder" />
2212
+ <button type="submit" [disabled]="!playlistDraftName.trim()">{{ lists.createLabel }}</button>
2213
+ </form>
2214
+ @if (lists.target; as target) {
2215
+ <p class="ngxsp-yt-listTarget">{{ lists.targetLabel }}<strong>{{ target.title }}</strong></p>
2216
+ }
2217
+ <div class="ngxsp-yt-listRows">
2218
+ @for (list of lists.playlists; track list.id) {
2219
+ <div class="ngxsp-yt-listRow" [class.active]="list.active">
2220
+ <button class="ngxsp-yt-listMain" type="button" [attr.aria-current]="list.active ? 'true' : null" [title]="lists.openLabel + ' - ' + list.name" (click)="selectPlaylist.emit(list.id)">
2221
+ <ngxsp-smart-icon name="list" />
2222
+ <span>
2223
+ <strong>{{ list.name }}</strong>
2224
+ @if (list.itemCount !== undefined) { <small>{{ list.itemCount }} {{ lists.itemsCountLabel }}</small> }
2225
+ </span>
2226
+ </button>
2227
+ <span class="ngxsp-yt-listActions">
2228
+ @if (lists.target) {
2229
+ @if (list.containsTarget) {
2230
+ <button type="button" [attr.aria-label]="lists.removeLabel + ' - ' + list.name" [title]="lists.removeLabel" (click)="removeFromPlaylist.emit(list.id)">
2231
+ <ngxsp-smart-icon name="check" />
2232
+ </button>
2233
+ } @else {
2234
+ <button type="button" [attr.aria-label]="lists.addLabel + ' - ' + list.name" [title]="lists.addLabel" (click)="addToPlaylist.emit(list.id)">
2235
+ <ngxsp-smart-icon name="plus" />
2236
+ </button>
2237
+ }
2238
+ }
2239
+ <button type="button" [attr.aria-label]="lists.renameLabel + ' - ' + list.name" [title]="lists.renameLabel" (click)="startRenamePlaylist(list)">
2240
+ <ngxsp-smart-icon name="tag" />
2241
+ </button>
2242
+ <button type="button" class="danger" [attr.aria-label]="lists.deleteLabel + ' - ' + list.name" [title]="lists.deleteLabel" (click)="deletePlaylist.emit(list.id)">
2243
+ <ngxsp-smart-icon name="close" />
2244
+ </button>
2245
+ </span>
2246
+ @if (renamingPlaylistId === list.id) {
2247
+ <form class="ngxsp-yt-listForm" (submit)="submitRenamePlaylist($event)">
2248
+ <input type="text" [value]="playlistRenameDraft" (input)="onPlaylistRenameInput($event)" [placeholder]="lists.renamePlaceholder" [attr.aria-label]="lists.renamePlaceholder" />
2249
+ <button type="submit" [disabled]="!playlistRenameDraft.trim()">{{ lists.saveLabel }}</button>
2250
+ <button type="button" (click)="cancelRenamePlaylist()">{{ lists.cancelLabel }}</button>
2251
+ </form>
2252
+ }
2253
+ </div>
2254
+ } @empty {
2255
+ <p class="ngxsp-yt-listEmpty">{{ lists.emptyLabel }}</p>
2256
+ }
2257
+ </div>
2258
+ </section>
2259
+ }
2260
+ @if (miniPlayerVisible) {
2261
+ <article class="ngxsp-yt-miniPlayer" [class.playing]="isPlaying" [attr.aria-label]="nowPlayingLabel">
2262
+ <button class="ngxsp-yt-miniArtwork" type="button" [attr.aria-label]="miniPlayerFocusLabel + ' - ' + displayHeroTitle" [title]="miniPlayerFocusLabel" (click)="miniPlayerFocus.emit()">
2263
+ @if (displayHeroArtwork) {
2264
+ <img [src]="displayHeroArtwork" alt="" />
2265
+ } @else {
2266
+ <span class="ngxsp-yt-art" aria-hidden="true"><ngxsp-smart-icon name="video" /></span>
2267
+ }
2268
+ <span class="ngxsp-yt-miniPlayBadge" aria-hidden="true">
2269
+ @if (isPlaying) { <ngxsp-smart-icon name="waveform" /> } @else { <ngxsp-smart-icon name="play" /> }
2270
+ </span>
2271
+ </button>
2272
+ <span class="ngxsp-yt-miniDetails">
2273
+ <small>{{ miniPlayerMeta || heroEyebrow }}</small>
2274
+ <strong>{{ displayHeroTitle }}</strong>
2275
+ @if (displayHeroSubtitle) { <span>{{ displayHeroSubtitle }}</span> }
2276
+ </span>
2277
+ <button class="ngxsp-yt-miniControl" type="button" [attr.aria-label]="miniPlayerPlaybackLabel" [title]="miniPlayerPlaybackLabel" (click)="toggleMiniPlayerPlayback()">
2278
+ @if (isPlaying) { <ngxsp-smart-icon name="pause" /> } @else { <ngxsp-smart-icon name="play" /> }
2279
+ </button>
2280
+ <button class="ngxsp-yt-miniClose" type="button" [attr.aria-label]="miniPlayerCloseLabel" [title]="miniPlayerCloseLabel" (click)="miniPlayerClose.emit()">
2281
+ <ngxsp-smart-icon name="close" />
2282
+ </button>
2283
+ </article>
2284
+ } @else {
2285
+ <button class="ngxsp-yt-miniRestore" type="button" [attr.aria-label]="miniPlayerRestoreLabel" [title]="miniPlayerRestoreLabel" (click)="miniPlayerRestore.emit()">
2286
+ <ngxsp-smart-icon name="window-mini" />
2287
+ <span>{{ miniPlayerRestoreLabel }}</span>
2288
+ </button>
2289
+ }
2290
+ </aside>
2291
+ }
2292
+ </div>
2293
+ </section>
2294
+ `, isInline: true, styles: [":host{display:block;height:100%;min-height:0;color:var(--nsp-text, #ededf0);font-family:var(--nsp-font, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Ubuntu, sans-serif);font-size:13px;line-height:1.4}.ngxsp-yt-shell{--yt-bg:var(--nsp-bg, #0d0e11);--yt-s0:var(--nsp-surface-0, #121317);--yt-s1:var(--nsp-surface-1, #17181d);--yt-s2:var(--nsp-surface-2, #1e1f25);--yt-s3:var(--nsp-surface-3, #26272e);--yt-line:var(--nsp-line, rgb(255 255 255 / .07));--yt-line-strong:var(--nsp-line-strong, rgb(255 255 255 / .13));--yt-text:var(--nsp-text, #ededf0);--yt-text-2:var(--nsp-text-2, #a3a5ae);--yt-text-3:var(--nsp-text-3, #80828c);--yt-accent:var(--nsp-accent, #8b90ff);--yt-accent-strong:var(--nsp-accent-strong, #5d63ee);--yt-accent-soft:var(--nsp-accent-soft, rgb(139 144 255 / .14));--yt-on-accent:var(--nsp-on-accent, #fff);--yt-success:var(--nsp-success, #3ecf8e);--yt-warning:var(--nsp-warning, #f2a93b);--yt-danger:var(--nsp-danger, #f25f5c);--yt-danger-text:var(--nsp-danger-text, #ffb4b2);--yt-live:var(--nsp-live, #f25f5c);--yt-r-sm:var(--nsp-radius-sm, 6px);--yt-r-md:var(--nsp-radius-md, 10px);--yt-r-lg:var(--nsp-radius-lg, 14px);--yt-ease:var(--nsp-ease, cubic-bezier(.2,0,0,1));--yt-dur:var(--nsp-dur, .14s);--yt-title-h:var(--nsp-titlebar-height, 48px);--yt-rail-w:84px;--yt-side-w:360px;--yt-mode:var(--yt-text-2);position:relative;height:100%;min-height:600px;display:grid;grid-template-columns:var(--yt-rail-w) minmax(0,1fr);grid-template-rows:var(--yt-title-h) minmax(0,1fr);grid-template-areas:\"top top\" \"rail body\";overflow:hidden;background:var(--yt-bg);color:var(--yt-text);box-sizing:border-box}.ngxsp-yt-shell.ngxsp-yt-railExpanded{--yt-rail-w:220px}.ngxsp-yt-shell.ngxsp-yt-menuRow{grid-template-columns:minmax(0,1fr);grid-template-rows:var(--yt-title-h) auto minmax(0,1fr);grid-template-areas:\"top\" \"rail\" \"body\"}.ngxsp-yt-menuRow .ngxsp-yt-rail{flex-direction:row;padding:6px 12px;border-right:0;border-bottom:1px solid var(--yt-line);overflow-x:auto;overflow-y:hidden}.ngxsp-yt-menuRow .ngxsp-yt-railButton{width:auto;min-height:36px;flex:0 0 auto;flex-direction:row;gap:6px;padding:0 10px}.ngxsp-yt-menuRow .ngxsp-yt-railButton span{font-size:12px}.ngxsp-yt-menuRow .ngxsp-yt-railMenu{display:none}.ngxsp-yt-shell.ngxsp-yt-menuOff{grid-template-columns:minmax(0,1fr);grid-template-rows:var(--yt-title-h) minmax(0,1fr);grid-template-areas:\"top\" \"body\"}.ngxsp-yt-menuOff .ngxsp-yt-rail{display:none}.ngxsp-yt-shell.ngxsp-yt-online{--yt-mode:var(--yt-live)}.ngxsp-yt-shell.ngxsp-yt-offline{--yt-mode:var(--yt-success)}.ngxsp-yt-shell *{box-sizing:border-box;letter-spacing:0}.ngxsp-yt-shell *::-webkit-scrollbar{width:10px;height:10px}.ngxsp-yt-shell *::-webkit-scrollbar-track{background:transparent}.ngxsp-yt-shell *::-webkit-scrollbar-thumb{background:var(--yt-s3);border-radius:999px;border:3px solid transparent;background-clip:padding-box}button,input{font:inherit;color:inherit}button{cursor:pointer}img{display:block;width:100%;height:100%;object-fit:cover}h1,h2,p{margin:0}ngxsp-smart-icon{width:18px;height:18px}.ngxsp-yt-fileInput{display:none}.ngxsp-yt-shell button:focus-visible,.ngxsp-yt-shell input:focus-visible{outline:2px solid var(--yt-accent);outline-offset:2px}.ngxsp-yt-topbar{grid-area:top;min-width:0;display:grid;grid-template-columns:auto minmax(200px,1fr) auto;align-items:center;gap:16px;padding:0 calc(12px + var(--nsp-titlebar-inset-end, 0px)) 0 calc(12px + var(--nsp-titlebar-inset-start, 0px));border-bottom:1px solid var(--yt-line);background:var(--yt-s0);-webkit-app-region:drag;-webkit-user-select:none;user-select:none}.ngxsp-yt-topbar button,.ngxsp-yt-topbar input,.ngxsp-yt-topbar label,.ngxsp-yt-topbar .ngxsp-yt-status{-webkit-app-region:no-drag}.ngxsp-yt-brand{min-width:0;display:flex;align-items:center;gap:10px}.ngxsp-yt-railMenu{width:32px;height:32px;display:grid;place-items:center;padding:0;margin-right:6px;border:0;border-radius:var(--yt-r-sm);background:transparent;color:var(--yt-text-2)}.ngxsp-yt-railMenu:hover{background:var(--yt-s2);color:var(--yt-text)}.ngxsp-yt-brandIcon{width:26px;height:26px;flex:0 0 auto;display:grid;place-items:center;border-radius:7px;color:#fff;background:linear-gradient(145deg,#8b90ff,#5d63ee 55%,#4146c9);box-shadow:inset 0 1px #ffffff38}.ngxsp-yt-brandIcon ngxsp-smart-icon{width:14px;height:14px;margin-left:1px}.ngxsp-yt-brandLogo{width:26px;height:26px;flex:0 0 auto;display:block}.ngxsp-yt-brand strong{font-size:13px;font-weight:600;white-space:nowrap}.ngxsp-yt-search{position:relative;justify-self:center;width:100%;max-width:520px;height:34px;display:block;color:var(--yt-text-3)}.ngxsp-yt-search ngxsp-smart-icon{position:absolute;left:11px;top:50%;width:16px;height:16px;transform:translateY(-50%);pointer-events:none}.ngxsp-yt-search input{width:100%;height:100%;padding:0 10px 0 35px;border:1px solid transparent;border-radius:8px;outline:0;background:var(--yt-s2);color:var(--yt-text);font-size:13px;transition:border-color var(--yt-dur) var(--yt-ease),background-color var(--yt-dur) var(--yt-ease),box-shadow var(--yt-dur) var(--yt-ease)}.ngxsp-yt-search input:hover{border-color:var(--yt-line-strong)}.ngxsp-yt-shell .ngxsp-yt-search input:focus,.ngxsp-yt-shell .ngxsp-yt-search input:focus-visible{outline:0;border-color:var(--yt-accent);background:var(--yt-s1);box-shadow:0 0 0 3px var(--yt-accent-soft)}.ngxsp-yt-search input::placeholder{color:var(--yt-text-3)}.ngxsp-yt-actions{min-width:0;display:flex;align-items:center;justify-content:flex-end;gap:8px}.ngxsp-yt-status{min-width:0;max-width:260px;height:26px;display:inline-flex;align-items:center;gap:7px;padding:0 10px;border-radius:999px;background:var(--yt-s2);color:var(--yt-text-2);font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.ngxsp-yt-status:before{content:\"\";width:6px;height:6px;flex:0 0 auto;border-radius:50%;background:var(--yt-accent)}.ngxsp-yt-status.success:before{background:var(--yt-success)}.ngxsp-yt-status.error{color:var(--yt-danger-text);background:#f25f5c29}.ngxsp-yt-status.error:before{background:var(--yt-danger)}.ngxsp-yt-mode,.ngxsp-yt-languageSwitch{flex:0 0 auto;display:inline-flex;align-items:center;gap:2px;padding:2px;border-radius:8px;background:var(--yt-s2)}.ngxsp-yt-mode button,.ngxsp-yt-languageSwitch button{height:32px;display:inline-flex;align-items:center;gap:6px;padding:0 10px;border:0;border-radius:6px;background:transparent;color:var(--yt-text-2);font-size:12px;font-weight:500;white-space:nowrap}.ngxsp-yt-mode button i{width:6px;height:6px;border-radius:50%;background:var(--yt-text-3)}.ngxsp-yt-mode button:hover,.ngxsp-yt-languageSwitch button:hover{color:var(--yt-text)}.ngxsp-yt-mode button.active,.ngxsp-yt-languageSwitch button.active{background:var(--yt-s3);color:var(--yt-text);box-shadow:0 1px 2px #0000004d}.ngxsp-yt-mode button.active i{background:var(--yt-mode);box-shadow:0 0 0 3px color-mix(in srgb,var(--yt-mode) 22%,transparent)}.ngxsp-yt-sourceButton{flex:0 0 auto;height:32px;display:inline-flex;align-items:center;gap:7px;padding:0 12px 0 10px;border:0;border-radius:var(--yt-r-sm);background:var(--yt-accent-strong);color:var(--yt-on-accent);font-size:12px;font-weight:600;white-space:nowrap}.ngxsp-yt-sourceButton ngxsp-smart-icon{width:16px;height:16px}.ngxsp-yt-sourceButton:hover{filter:brightness(1.1)}.ngxsp-yt-sourceButton:disabled{opacity:.7;cursor:progress}.ngxsp-yt-libraryStatus{display:inline-flex;align-items:center;gap:6px}.ngxsp-yt-libraryStatus:before{content:\"\";width:6px;height:6px;flex:0 0 auto;border-radius:50%;background:var(--yt-success)}.ngxsp-yt-libraryStatus.busy:before{background:var(--yt-accent);animation:ngxsp-yt-pulse 1.2s ease-in-out infinite}@keyframes ngxsp-yt-pulse{0%,to{opacity:.35}50%{opacity:1}}.ngxsp-yt-round{width:32px;height:32px;flex:0 0 auto;display:grid;place-items:center;padding:0;border:0;border-radius:var(--yt-r-sm);background:transparent;color:var(--yt-text-2)}.ngxsp-yt-round:hover{background:var(--yt-s2);color:var(--yt-text)}.ngxsp-yt-rail{grid-area:rail;min-width:0;min-height:0;display:flex;flex-direction:column;gap:2px;padding:10px 8px;overflow-y:auto;overflow-x:hidden;border-right:1px solid var(--yt-line);background:var(--yt-s0)}.ngxsp-yt-railButton{width:100%;min-height:56px;flex:0 0 auto;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;padding:6px 2px;border:0;border-radius:var(--yt-r-sm);background:transparent;color:var(--yt-text-2);transition:background-color var(--yt-dur) var(--yt-ease),color var(--yt-dur) var(--yt-ease)}.ngxsp-yt-railButton ngxsp-smart-icon{width:20px;height:20px}.ngxsp-yt-railButton span{max-width:100%;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;font-size:10px;font-weight:500;line-height:1.15;text-align:center;-webkit-hyphens:auto;hyphens:auto;overflow-wrap:anywhere}.ngxsp-yt-railButton:hover:not(:disabled){background:var(--yt-s2);color:var(--yt-text)}.ngxsp-yt-railButton.active{background:var(--yt-s3);color:var(--yt-text)}.ngxsp-yt-railButton.active ngxsp-smart-icon{color:var(--yt-accent)}.ngxsp-yt-railButton:disabled{opacity:.4;cursor:default}.ngxsp-yt-railExpanded .ngxsp-yt-railButton{min-height:36px;flex-direction:row;justify-content:flex-start;gap:12px;padding:0 12px}.ngxsp-yt-railExpanded .ngxsp-yt-railButton span{font-size:13px;text-align:left;-webkit-line-clamp:1;overflow-wrap:normal}.ngxsp-yt-railExpanded .ngxsp-yt-railButton ngxsp-smart-icon{width:18px;height:18px}.ngxsp-yt-body{grid-area:body;min-width:0;min-height:0;display:grid;grid-template-columns:minmax(0,1fr) var(--yt-side-w);overflow:hidden}.ngxsp-queue-hidden .ngxsp-yt-body{grid-template-columns:minmax(0,1fr)}.ngxsp-yt-watch{--yt-gutter:24px;min-width:0;min-height:0;display:flex;flex-direction:column;gap:20px;padding:20px var(--yt-gutter) 40px;overflow:auto}.ngxsp-yt-cinema .ngxsp-yt-body{display:block;overflow:auto}.ngxsp-yt-cinema .ngxsp-yt-watch{overflow:visible;padding-top:16px}.ngxsp-yt-watch.player{padding:0;gap:0;overflow:hidden;height:100%}.ngxsp-yt-cinema .ngxsp-yt-watch.player{padding:0;overflow:hidden;height:100%}.ngxsp-yt-playerBack{position:absolute;top:12px;left:12px;z-index:4;display:none;align-items:center;gap:2px;height:36px;padding:0 14px 0 8px;border:0;border-radius:999px;background:#0000008c;color:#fff;font:inherit;font-size:13px;font-weight:600;cursor:pointer;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)}.ngxsp-yt-playerBack ngxsp-smart-icon{width:18px;height:18px}.ngxsp-yt-playerShell{position:relative}.ngxsp-yt-watch.player .ngxsp-yt-playerShell{flex:1 1 auto;min-height:0}.ngxsp-yt-watch.player .ngxsp-yt-playerFrame{width:100%;min-width:0;height:100%;aspect-ratio:auto;border-radius:0;box-shadow:none}.ngxsp-yt-playerShell{flex:0 0 auto;display:flex;justify-content:center}.ngxsp-yt-playerFrame{position:relative;width:min(100%,calc((100vh - var(--yt-title-h) - 190px) * 16 / 9));min-width:min(100%,620px);aspect-ratio:16 / 9;overflow:hidden;border-radius:var(--yt-r-md);background:#000;box-shadow:0 0 0 1px var(--yt-line),var(--nsp-shadow-media, 0 24px 60px rgb(0 0 0 / .45))}.ngxsp-yt-cinema .ngxsp-yt-playerFrame{width:min(100%,calc((100vh - var(--yt-title-h) - 48px) * 16 / 9))}.ngxsp-yt-playerFrame ngx-smart-player{position:absolute;inset:0;display:block}.ngxsp-yt-previewPlayer{position:absolute;inset:0;width:100%;height:100%;padding:0;border:0;background:#000;overflow:hidden}.ngxsp-yt-previewPlayer img{transition:transform .6s var(--yt-ease),filter var(--yt-dur) var(--yt-ease)}.ngxsp-yt-previewPlayer:hover img{transform:scale(1.015);filter:brightness(.92)}.ngxsp-yt-previewOverlay{position:absolute;inset:0;display:grid;place-items:center;background:linear-gradient(180deg,#0000001a 40%,#0000008c)}.ngxsp-yt-previewPlay{width:72px;height:72px;display:grid;place-items:center;border-radius:999px;background:#fffffff0;color:#0d0e11;box-shadow:0 12px 40px #00000073;transition:transform var(--yt-dur) var(--yt-ease)}.ngxsp-yt-previewPlay ngxsp-smart-icon{width:30px;height:30px;margin-left:3px}.ngxsp-yt-previewPlayer:hover .ngxsp-yt-previewPlay{transform:scale(1.06)}.ngxsp-yt-emptyPlayer{position:absolute;inset:0;display:grid;place-content:center;justify-items:center;gap:8px;padding:24px 24px 88px;background:radial-gradient(80% 90% at 50% 20%,rgb(93 99 238 / .16),transparent 70%),#08090b;text-align:center}.ngxsp-yt-bigPlay{width:64px;height:64px;display:grid;place-items:center;margin-bottom:8px;border:0;border-radius:18px;background:var(--yt-accent-soft);color:var(--yt-accent);transition:background-color var(--yt-dur) var(--yt-ease)}.ngxsp-yt-bigPlay:hover{background:#8b90ff3d}.ngxsp-yt-bigPlay ngxsp-smart-icon{width:28px;height:28px}.ngxsp-yt-emptyPlayer span{color:var(--yt-text-2);font-size:12px;font-weight:600}.ngxsp-yt-emptyPlayer strong{font-size:20px;font-weight:600}.ngxsp-yt-emptyPlayer p{max-width:56ch;color:var(--yt-text-2)}.ngxsp-yt-transportBar{position:absolute;left:0;right:0;bottom:0;z-index:2;display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;gap:12px;padding:40px 12px 10px;background:linear-gradient(180deg,#0000,#000000b8);pointer-events:none}.ngxsp-yt-transportBar>*{pointer-events:auto}.ngxsp-yt-shell.ngxsp-yt-compact{display:block;height:100vh;min-height:0;padding:env(titlebar-area-height,0px) 0 0;box-sizing:border-box}.ngxsp-yt-shell.ngxsp-yt-compact>:not(.ngxsp-yt-body){display:none!important}.ngxsp-yt-compact .ngxsp-yt-body{display:block;height:100%}.ngxsp-yt-compact .ngxsp-yt-body>:not(.ngxsp-yt-watch){display:none!important}.ngxsp-yt-compact .ngxsp-yt-watch{display:block;height:100%;padding:0;overflow:hidden}.ngxsp-yt-compact .ngxsp-yt-watch>:not(.ngxsp-yt-playerShell){display:none!important}.ngxsp-yt-compact .ngxsp-yt-playerShell{height:100%}.ngxsp-yt-compact .ngxsp-yt-playerFrame{position:relative;width:100%;min-width:0;height:100%;aspect-ratio:auto;border-radius:0;box-shadow:none}.ngxsp-yt-compactDrag{position:absolute;top:0;left:0;right:0;z-index:6;height:28px;-webkit-app-region:drag}.ngxsp-yt-compact .ngxsp-yt-playerInlineControls>.ngxsp-yt-playerAction:not(:first-child){display:none}.ngxsp-yt-compact ngx-smart-player ::ng-deep .ngxsp-inline-projection{display:inline-flex}.ngxsp-yt-compact :is(.ngxsp-yt-transportCinema,.ngxsp-yt-transportBar .ngxsp-yt-queueButton){display:none}.ngxsp-yt-transportCluster{display:flex;align-items:center;gap:4px}.ngxsp-yt-transportButton{width:36px;height:36px;display:grid;place-items:center;padding:0;border:0;border-radius:var(--yt-r-sm);background:transparent;color:#ffffffeb;transition:background-color var(--yt-dur) var(--yt-ease)}.ngxsp-yt-transportButton ngxsp-smart-icon{width:20px;height:20px}.ngxsp-yt-transportButton:hover:not(:disabled){background:#ffffff1f}.ngxsp-yt-transportButton.active{color:var(--yt-accent)}.ngxsp-yt-transportButton:disabled{opacity:.4;cursor:default}.ngxsp-yt-transportPlay{width:44px;height:44px;display:grid;place-items:center;margin:0 4px;padding:0;border:0;border-radius:999px;background:#fff;color:#0d0e11;transition:transform var(--yt-dur) var(--yt-ease)}.ngxsp-yt-transportPlay:hover{transform:scale(1.05)}.ngxsp-yt-transportPlay ngxsp-smart-icon{width:20px;height:20px}.ngxsp-yt-transportInfo{min-width:0;display:grid;gap:1px;color:#fff}.ngxsp-yt-transportInfo span{color:#ffffffb8;font-size:12px;font-weight:600}.ngxsp-yt-transportInfo strong{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:14px;font-weight:600}.ngxsp-yt-volumeControl{--ngxsp-control-size:36px;--ngxsp-volume-range-min:64px;--ngxsp-volume-range-max:92px;--ngxsp-volume-gap:2px;--ngxsp-volume-button-border:0;--ngxsp-volume-button-radius:6px;--ngxsp-volume-accent:#fff;--ngxsp-volume-track-bg:rgb(255 255 255 / .28);--ngxsp-volume-thumb-bg:#fff;--ngxsp-volume-thumb-size:12px;--ngxsp-volume-thumb-shadow:none;--ngxsp-focus-ring:0 0 0 2px var(--yt-accent);color:#fff}.ngxsp-yt-playerInlineControls{display:inline-flex;align-items:center;gap:0}.ngxsp-yt-playerAction{width:36px;height:36px;display:grid;place-items:center;padding:0;border:0;border-radius:6px;background:transparent;color:#ffffffeb}.ngxsp-yt-playerAction ngxsp-smart-icon{width:20px;height:20px}.ngxsp-yt-playerAction:hover{background:#ffffff1f}.ngxsp-yt-playerAction.active{color:var(--yt-accent)}.ngxsp-yt-metaBar{flex:0 0 auto;min-width:0;display:flex;align-items:flex-start;justify-content:space-between;flex-wrap:wrap;gap:12px 24px}.ngxsp-yt-titleGroup{flex:1 1 320px;min-width:0;display:grid;gap:4px}.ngxsp-yt-titleGroup h1{font-size:22px;line-height:1.25;font-weight:700}.ngxsp-yt-shell .ngxsp-yt-titleGroup h1{letter-spacing:-.01em}.ngxsp-yt-titleGroup p{color:var(--yt-text-2);font-size:13px;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.ngxsp-yt-titleGroup small{color:var(--yt-text-3);font-size:12px}.ngxsp-yt-watchActions{display:flex;align-items:center;flex-wrap:wrap;gap:8px}.ngxsp-yt-rating{display:inline-flex;border-radius:999px;background:var(--yt-s2);overflow:hidden}.ngxsp-yt-watchActions button{height:34px;display:inline-flex;align-items:center;gap:7px;padding:0 14px 0 12px;border:0;border-radius:999px;background:var(--yt-s2);color:var(--yt-text);font-size:12px;font-weight:500;white-space:nowrap;transition:background-color var(--yt-dur) var(--yt-ease),color var(--yt-dur) var(--yt-ease)}.ngxsp-yt-watchActions button:hover{background:var(--yt-s3)}.ngxsp-yt-watchActions button.active{color:var(--yt-accent)}.ngxsp-yt-watchActions button.active ngxsp-smart-icon ::ng-deep path{fill:var(--yt-accent-soft)}.ngxsp-yt-rating button{border-radius:0}.ngxsp-yt-rating button+button{position:relative;padding-left:12px}.ngxsp-yt-rating button+button:before{content:\"\";position:absolute;left:0;top:9px;bottom:9px;width:1px;background:var(--yt-line-strong)}.ngxsp-yt-rating button+button span{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0)}.ngxsp-yt-moreAction{width:34px;padding:0!important;justify-content:center}.ngxsp-yt-moreAction span{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0)}.ngxsp-yt-watchActions ngxsp-smart-icon{width:17px;height:17px}.ngxsp-yt-toolRow{flex:0 0 auto;display:grid;grid-template-columns:repeat(auto-fit,minmax(146px,1fr));gap:8px}.ngxsp-yt-tool{min-width:0;height:52px;display:grid;grid-template-columns:30px minmax(0,1fr);align-items:center;gap:9px;padding:0 10px 0 9px;border:1px solid var(--yt-line);border-radius:var(--yt-r-md);background:var(--yt-s1);text-align:left;transition:border-color var(--yt-dur) var(--yt-ease),background-color var(--yt-dur) var(--yt-ease)}.ngxsp-yt-tool:hover{border-color:var(--yt-line-strong);background:var(--yt-s2)}.ngxsp-yt-tool.active{border-color:#8b90ff73;background:var(--yt-accent-soft)}.ngxsp-yt-toolIcon{--tone:var(--yt-text-2);width:30px;height:30px;display:grid;place-items:center;border-radius:8px;background:color-mix(in srgb,var(--tone) 16%,transparent);color:var(--tone)}.ngxsp-yt-tool[data-tone=teal] .ngxsp-yt-toolIcon{--tone:var(--nsp-tone-teal, #3fcfbf)}.ngxsp-yt-tool[data-tone=red] .ngxsp-yt-toolIcon{--tone:var(--nsp-tone-red, #f47c7a)}.ngxsp-yt-tool[data-tone=blue] .ngxsp-yt-toolIcon{--tone:var(--nsp-tone-blue, #7cb1ff)}.ngxsp-yt-tool[data-tone=gold] .ngxsp-yt-toolIcon{--tone:var(--nsp-tone-gold, #f2bb5b)}.ngxsp-yt-toolIcon ngxsp-smart-icon{width:17px;height:17px}.ngxsp-yt-toolCopy{min-width:0;display:grid;gap:1px}.ngxsp-yt-toolCopy small,.ngxsp-yt-toolCopy strong{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ngxsp-yt-toolCopy small{color:var(--yt-text-2);font-size:11px}.ngxsp-yt-toolCopy strong{font-size:13px;font-weight:500}.ngxsp-yt-art{position:absolute;inset:0;display:grid;place-items:center;color:#ffffff8c}.ngxsp-yt-art ngxsp-smart-icon{width:26px;height:26px}[data-art] .ngxsp-yt-thumbnail:not(.has-image),[data-art] .ngxsp-yt-suggestionThumb:not(.has-image),[data-art] .ngxsp-yt-searchThumb:not(.has-image){background:radial-gradient(90% 80% at 15% 10%,rgb(255 255 255 / .14),transparent 60%),linear-gradient(140deg,var(--art-a, #3a3c55),var(--art-b, #15161c))}[data-art=aurora]{--art-a:#3a7f78;--art-b:#10252a}[data-art=orbit]{--art-a:#5157c9;--art-b:#1a1b3d}[data-art=ember]{--art-a:#b0603a;--art-b:#301810}[data-art=ridge]{--art-a:#4f6f94;--art-b:#131d2a}[data-art=signal]{--art-a:#a8416a;--art-b:#2c1120}[data-art=fog]{--art-a:#5d6270;--art-b:#1a1b20}.ngxsp-yt-nowBadge{position:absolute;right:8px;top:8px;width:26px;height:26px;display:grid;place-items:center;border-radius:999px;background:var(--yt-accent-strong);color:#fff;box-shadow:0 4px 12px #0006}.ngxsp-yt-nowBadge ngxsp-smart-icon{width:14px;height:14px}.ngxsp-yt-thumbKind{position:absolute;left:8px;top:8px;max-width:calc(100% - 50px);padding:2px 7px;border-radius:4px;background:#000000b8;color:#fff;font-size:12px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px)}.ngxsp-yt-cardProgress{position:absolute;left:0;right:0;bottom:0;height:3px;background:#fff3}.ngxsp-yt-cardProgress i{position:absolute;inset:0 auto 0 0;background:var(--yt-accent)}.ngxsp-yt-thumbBadge,.ngxsp-yt-suggestionThumb>small,.ngxsp-yt-searchThumb>small{position:absolute;right:6px;bottom:7px;max-width:calc(100% - 12px);padding:2px 6px;border-radius:4px;background:#000000c7;color:#fff;font-size:11px;font-weight:600;font-variant-numeric:tabular-nums;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.ngxsp-yt-sectionHead{display:flex;align-items:center;justify-content:space-between;gap:12px}.ngxsp-yt-sectionHead>div{min-width:0;display:grid;gap:2px}.ngxsp-yt-sectionHead strong{font-size:16px;font-weight:600}.ngxsp-yt-sectionHead small{color:var(--yt-text-2);font-size:12px}.ngxsp-yt-sectionHead.compact{padding:14px 16px 10px}.ngxsp-yt-sectionHead.compact strong{font-size:14px}.ngxsp-yt-sectionLink{width:32px;height:32px;display:grid;place-items:center;padding:0;border:0;border-radius:var(--yt-r-sm);background:transparent;color:var(--yt-text-2)}.ngxsp-yt-sectionLink:hover{background:var(--yt-s2);color:var(--yt-text)}.ngxsp-yt-servers{display:grid;gap:8px}.ngxsp-yt-serversHeading{margin:6px 0 2px;font-size:13px;font-weight:600;letter-spacing:.02em;color:var(--yt-text-2);text-transform:uppercase}.ngxsp-yt-serverForm{display:flex;flex-wrap:wrap;align-items:center;gap:8px;padding:10px 12px;border:1px solid var(--yt-line);border-radius:var(--yt-r-md);background:var(--yt-s1)}.ngxsp-yt-serverTitle{font-size:12px;font-weight:600;color:var(--yt-text-2)}.ngxsp-yt-discovered{flex:1 1 100%;display:grid;gap:4px;margin:0;padding:0;list-style:none}.ngxsp-yt-discovered li{display:flex;align-items:center;justify-content:space-between;gap:8px;min-height:36px;padding:2px 0 2px 8px;border-top:1px solid var(--yt-line)}.ngxsp-yt-discovered li span{display:grid;min-width:0}.ngxsp-yt-discovered strong{overflow:hidden;font-size:13px;font-weight:600;text-overflow:ellipsis;white-space:nowrap}.ngxsp-yt-discovered small,.ngxsp-yt-discovered li.empty{overflow:hidden;color:var(--yt-text-2);font-size:12px;text-overflow:ellipsis;white-space:nowrap}.ngxsp-yt-serverKind{height:32px;padding:0 8px;border:1px solid var(--yt-line);border-radius:var(--yt-r-sm);background:var(--yt-s2);color:var(--yt-text);font-size:12px}.ngxsp-yt-serverForm input{flex:1 1 180px;min-width:0;height:32px;padding:0 10px;border:1px solid var(--yt-line);border-radius:var(--yt-r-sm);background:var(--yt-s0, var(--yt-s2));color:var(--yt-text);font-size:12px}.ngxsp-yt-serverForm input::placeholder{color:var(--yt-text-3)}.ngxsp-yt-serverForm button{min-height:32px;padding:0 14px;border:1px solid var(--yt-line);border-radius:var(--yt-r-sm);background:var(--yt-accent, var(--yt-s3));color:var(--yt-text);font-size:12px;font-weight:600;cursor:pointer}.ngxsp-yt-serverForm button.secondary{border-color:var(--yt-line-strong, var(--yt-line));background:transparent}.ngxsp-yt-serverForm button[disabled]{opacity:.5;cursor:default}.ngxsp-yt-serverHint{flex:1 0 100%;color:var(--yt-text-3);font-size:11px}.ngxsp-yt-serverRow{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:6px;border-radius:var(--yt-r-sm)}.ngxsp-yt-serverRow.active{background:var(--yt-s1)}.ngxsp-yt-serverMain{display:flex;align-items:center;gap:8px;min-width:0;padding:6px 8px;border:0;border-radius:var(--yt-r-sm);background:transparent;color:var(--yt-text);text-align:left;cursor:pointer}.ngxsp-yt-serverMain span{display:grid;min-width:0}.ngxsp-yt-serverMain strong{font-size:13px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ngxsp-yt-serverMain small{color:var(--yt-text-3);font-size:11px}.ngxsp-yt-serverMain:hover{background:var(--yt-s2)}.ngxsp-yt-serverDisconnect{flex:0 0 32px;width:32px;height:32px;display:grid;place-items:center;border:0;border-radius:999px;background:transparent;color:var(--yt-text-2);cursor:pointer}.ngxsp-yt-serverDisconnect:hover{background:var(--yt-s2);color:var(--yt-danger, #e5484d)}.ngxsp-yt-lists{display:grid;gap:8px;margin-bottom:16px}.ngxsp-yt-listForm{display:flex;gap:6px}.ngxsp-yt-listForm input{flex:1;min-width:0;height:32px;padding:0 10px;border:1px solid var(--yt-line);border-radius:var(--yt-r-sm);background:var(--yt-s1);color:var(--yt-text);font-size:12px}.ngxsp-yt-listForm input::placeholder{color:var(--yt-text-3)}.ngxsp-yt-listForm button{min-height:32px;height:32px;padding:0 12px;border:1px solid var(--yt-line);border-radius:var(--yt-r-sm);background:var(--yt-s2);color:var(--yt-text);font-size:12px;font-weight:600;cursor:pointer}.ngxsp-yt-listForm button[disabled]{opacity:.5;cursor:default}.ngxsp-yt-listTarget{margin:0;color:var(--yt-text-2);font-size:12px}.ngxsp-yt-listTarget strong{margin-left:4px;color:var(--yt-text)}.ngxsp-yt-listRows{display:grid;gap:4px}.ngxsp-yt-listRow{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:6px;border-radius:var(--yt-r-sm)}.ngxsp-yt-listRow.active{background:var(--yt-s1)}.ngxsp-yt-listMain{display:flex;align-items:center;gap:8px;min-width:0;padding:6px 8px;border:0;border-radius:var(--yt-r-sm);background:transparent;color:var(--yt-text);text-align:left;cursor:pointer}.ngxsp-yt-listMain span{display:grid;min-width:0}.ngxsp-yt-listMain strong{font-size:13px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ngxsp-yt-listMain small{color:var(--yt-text-3);font-size:11px}.ngxsp-yt-listMain:hover{background:var(--yt-s2)}.ngxsp-yt-listActions{display:flex;gap:2px}.ngxsp-yt-listActions button{flex:0 0 32px;width:32px;height:32px;display:grid;place-items:center;border:0;border-radius:999px;background:transparent;color:var(--yt-text-2);cursor:pointer}.ngxsp-yt-listActions button:hover{background:var(--yt-s2);color:var(--yt-text)}.ngxsp-yt-listActions button.danger:hover{color:var(--yt-danger, #e5484d)}.ngxsp-yt-listRow .ngxsp-yt-listForm{grid-column:1 / -1;padding:0 8px 6px}.ngxsp-yt-listEmpty{margin:0;color:var(--yt-text-3);font-size:12px}.ngxsp-yt-countBadge{min-width:24px;height:22px;display:grid;place-items:center;padding:0 7px;border-radius:999px;background:var(--yt-s3);color:var(--yt-text-2);font-size:12px;font-weight:600;font-variant-numeric:tabular-nums}.ngxsp-yt-panelTitle{display:block;color:var(--yt-text-2);font-size:12px;font-weight:600}.ngxsp-yt-searchWorkspace{flex:0 0 auto;display:grid;gap:12px;padding:16px;border:1px solid var(--yt-line);border-radius:var(--yt-r-lg);background:var(--yt-s1)}.ngxsp-yt-searchHead{display:flex;align-items:center;justify-content:space-between;gap:12px}.ngxsp-yt-searchHead>div{min-width:0;display:grid;gap:2px}.ngxsp-yt-searchHead strong{font-size:16px;font-weight:600}.ngxsp-yt-searchHead small{color:var(--yt-text-2);font-size:12px}.ngxsp-yt-searchHead button{width:32px;height:32px;display:grid;place-items:center;padding:0;border:0;border-radius:var(--yt-r-sm);background:transparent;color:var(--yt-text-2)}.ngxsp-yt-searchHead button:hover{background:var(--yt-s3);color:var(--yt-text)}.ngxsp-yt-searchFilters{display:flex;flex-wrap:wrap;gap:6px}.ngxsp-yt-searchFilters button{height:32px;display:inline-flex;align-items:center;gap:6px;padding:0 12px;border:1px solid var(--yt-line-strong);border-radius:999px;background:transparent;color:var(--yt-text-2);font-size:12px;font-weight:500}.ngxsp-yt-searchFilters button small{color:var(--yt-text-3);font-variant-numeric:tabular-nums}.ngxsp-yt-searchFilters button:hover:not(:disabled){color:var(--yt-text);border-color:var(--yt-text-3)}.ngxsp-yt-searchFilters button.active{border-color:transparent;background:var(--yt-text);color:var(--yt-bg)}.ngxsp-yt-searchFilters button.active small{color:var(--yt-bg)}.ngxsp-yt-searchFilters button:disabled{opacity:.4;cursor:default}.ngxsp-yt-searchResults{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:8px}.ngxsp-yt-searchResults.list{grid-template-columns:1fr}.ngxsp-yt-searchResult{position:relative;min-width:0;border-radius:var(--yt-r-md);transition:background-color var(--yt-dur) var(--yt-ease)}.ngxsp-yt-searchResult:hover,.ngxsp-yt-searchResult:focus-within{background:var(--yt-s2)}.ngxsp-yt-searchResult.active{background:var(--yt-accent-soft)}.ngxsp-yt-searchResultMain{width:100%;min-width:0;display:grid;grid-template-columns:128px minmax(0,1fr);align-items:center;gap:12px;padding:8px;border:0;border-radius:inherit;background:transparent;text-align:left}.ngxsp-yt-searchThumb{position:relative;aspect-ratio:16 / 9;overflow:hidden;border-radius:6px;background:var(--yt-s3)}.ngxsp-yt-searchCopy{min-width:0;display:grid;gap:2px}.ngxsp-yt-searchCopy small{color:var(--yt-accent);font-size:11px;font-weight:600}.ngxsp-yt-searchCopy strong{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:13px;font-weight:500}.ngxsp-yt-searchCopy em,.ngxsp-yt-searchCopy span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--yt-text-2);font-size:12px;font-style:normal}.ngxsp-yt-searchActions{position:absolute;right:8px;bottom:8px;display:flex;gap:2px;padding:2px;border-radius:8px;background:var(--yt-s3);opacity:0;transition:opacity var(--yt-dur) var(--yt-ease)}.ngxsp-yt-searchResult:hover .ngxsp-yt-searchActions,.ngxsp-yt-searchResult:focus-within .ngxsp-yt-searchActions{opacity:1}.ngxsp-yt-searchActions button{width:32px;height:32px;display:grid;place-items:center;padding:0;border:0;border-radius:6px;background:transparent;color:var(--yt-text-2)}.ngxsp-yt-searchActions button:hover{background:var(--yt-s2);color:var(--yt-text)}.ngxsp-yt-searchActions ngxsp-smart-icon{width:15px;height:15px}.ngxsp-yt-searchEmpty{grid-column:1 / -1;display:grid;justify-items:center;gap:6px;padding:28px 16px;color:var(--yt-text-2);text-align:center}.ngxsp-yt-searchEmpty ngxsp-smart-icon{width:24px;height:24px;color:var(--yt-text-3)}.ngxsp-yt-searchEmpty strong{color:var(--yt-text);font-weight:600}.ngxsp-yt-mediaDetail{flex:0 0 auto;min-width:0;display:grid;gap:0;border:1px solid var(--yt-line);border-radius:var(--yt-r-lg);background:var(--yt-s1);overflow:hidden}.ngxsp-yt-mediaHero{display:grid;grid-template-columns:minmax(200px,280px) minmax(0,1fr);gap:20px;padding:20px}.ngxsp-yt-mediaPoster{position:relative;align-self:start;aspect-ratio:16 / 9;overflow:hidden;display:grid;place-items:center;border-radius:var(--yt-r-md);background:linear-gradient(140deg,#4f6f94,#131d2a);box-shadow:var(--nsp-shadow-card, 0 12px 30px rgb(0 0 0 / .35))}.ngxsp-yt-mediaPoster span{color:#fffc;font-size:40px;font-weight:700}.ngxsp-yt-mediaDetailBody{min-width:0;display:grid;align-content:start;gap:8px}.ngxsp-yt-mediaKicker{color:var(--yt-accent);font-size:12px;font-weight:600}.ngxsp-yt-mediaDetailBody h2{font-size:18px;line-height:1.3;font-weight:600}.ngxsp-yt-mediaSubtitle{color:var(--yt-text);font-size:13px}.ngxsp-yt-mediaMeta{display:flex;flex-wrap:wrap;align-items:center;gap:4px 0;color:var(--yt-text-2);font-size:12px}.ngxsp-yt-mediaMeta span+span:before{content:\"\";display:inline-block;width:3px;height:3px;margin:0 8px 3px;border-radius:50%;background:var(--yt-text-3)}.ngxsp-yt-mediaSummary{max-width:72ch;color:var(--yt-text-2);font-size:13px;line-height:1.55}.ngxsp-yt-mediaProgress{display:flex;align-items:center;gap:10px;max-width:420px}.ngxsp-yt-mediaProgress>span{position:relative;flex:1;height:4px;border-radius:999px;background:var(--yt-s3);overflow:hidden}.ngxsp-yt-mediaProgress i{position:absolute;inset:0 auto 0 0;background:var(--yt-accent);border-radius:inherit}.ngxsp-yt-mediaProgress small{color:var(--yt-text-2);font-size:12px;white-space:nowrap}.ngxsp-yt-mediaTags{display:flex;flex-wrap:wrap;gap:6px}.ngxsp-yt-mediaTags span{height:22px;display:inline-flex;align-items:center;padding:0 8px;border-radius:6px;background:var(--yt-s3);color:var(--yt-text-2);font-size:11px;font-weight:500}.ngxsp-yt-mediaActions{display:flex;flex-wrap:wrap;gap:8px;margin-top:4px}.ngxsp-yt-mediaActions button,.ngxsp-yt-inspectorActions button,.ngxsp-yt-miniRestore{height:32px;display:inline-flex;align-items:center;justify-content:center;gap:7px;padding:0 12px 0 10px;border:1px solid var(--yt-line-strong);border-radius:var(--yt-r-sm);background:transparent;color:var(--yt-text);font-size:12px;font-weight:500;white-space:nowrap}.ngxsp-yt-mediaActions button ngxsp-smart-icon,.ngxsp-yt-inspectorActions button ngxsp-smart-icon,.ngxsp-yt-miniRestore ngxsp-smart-icon{width:16px;height:16px}.ngxsp-yt-mediaActions button:hover:not(:disabled),.ngxsp-yt-inspectorActions button:hover:not(:disabled),.ngxsp-yt-miniRestore:hover{background:var(--yt-s3)}.ngxsp-yt-mediaActions button.primary,.ngxsp-yt-inspectorActions button.primary{border-color:transparent;background:var(--yt-accent-strong);color:var(--yt-on-accent)}.ngxsp-yt-mediaActions button.primary:hover:not(:disabled),.ngxsp-yt-inspectorActions button.primary:hover:not(:disabled){background:var(--yt-accent-strong);filter:brightness(1.1)}.ngxsp-yt-mediaActions button:disabled,.ngxsp-yt-inspectorActions button:disabled{opacity:.45;cursor:default}.ngxsp-yt-mediaWorkspace{display:grid;gap:14px;padding:16px 20px 20px;border-top:1px solid var(--yt-line)}.ngxsp-yt-mediaWorkspaceHead{display:grid;gap:2px}.ngxsp-yt-mediaWorkspaceHead strong{font-size:14px;font-weight:600}.ngxsp-yt-mediaWorkspaceHead small{color:var(--yt-text-2);font-size:12px}.ngxsp-yt-mediaWorkspaceGrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:16px}.ngxsp-yt-chapterPanel,.ngxsp-yt-checkPanel{min-width:0;display:grid;align-content:start;gap:8px}.ngxsp-yt-chapterList,.ngxsp-yt-checkList{display:grid;gap:2px}.ngxsp-yt-mediaChapter{width:100%;min-width:0;display:grid;grid-template-columns:72px minmax(0,1fr);align-items:start;gap:12px;padding:9px 10px;border:0;border-radius:var(--yt-r-sm);background:transparent;text-align:left;transition:background-color var(--yt-dur) var(--yt-ease)}.ngxsp-yt-mediaChapter:hover{background:var(--yt-s2)}.ngxsp-yt-mediaChapter.active{background:var(--yt-accent-soft)}.ngxsp-yt-chapterTime{display:grid;gap:2px}.ngxsp-yt-chapterTime em{justify-self:start;padding:1px 6px;border-radius:4px;background:var(--yt-s3);color:var(--yt-text);font-family:var(--nsp-font-mono, ui-monospace, Menlo, monospace);font-size:11px;font-style:normal;font-variant-numeric:tabular-nums}.ngxsp-yt-mediaChapter.active .ngxsp-yt-chapterTime em{background:var(--yt-accent-strong);color:#fff}.ngxsp-yt-chapterTime b{color:var(--yt-text-2);font-size:11px;font-weight:500}.ngxsp-yt-chapterCopy{min-width:0;display:grid;gap:2px}.ngxsp-yt-chapterCopy strong{font-size:13px;font-weight:500}.ngxsp-yt-chapterCopy small{color:var(--yt-text-2);font-size:12px}.ngxsp-yt-checkItem{display:grid;grid-template-columns:8px minmax(0,1fr) auto;align-items:start;gap:12px;padding:9px 10px;border-radius:var(--yt-r-sm)}.ngxsp-yt-checkItem:hover{background:var(--yt-s2)}.ngxsp-yt-checkItem>span{--tone:var(--yt-text-3);width:8px;height:8px;margin-top:5px;border-radius:50%;background:var(--tone);box-shadow:0 0 0 3px color-mix(in srgb,var(--tone) 20%,transparent)}.ngxsp-yt-checkItem[data-tone=teal]>span{--tone:var(--nsp-tone-teal, #3fcfbf)}.ngxsp-yt-checkItem[data-tone=red]>span{--tone:var(--nsp-tone-red, #f47c7a)}.ngxsp-yt-checkItem[data-tone=blue]>span{--tone:var(--nsp-tone-blue, #7cb1ff)}.ngxsp-yt-checkItem[data-tone=gold]>span{--tone:var(--nsp-tone-gold, #f2bb5b)}.ngxsp-yt-checkItem>div{min-width:0;display:grid;gap:2px}.ngxsp-yt-checkItem strong{font-size:13px;font-weight:500}.ngxsp-yt-checkItem small{color:var(--yt-text-2);font-size:12px}.ngxsp-yt-checkItem em{color:var(--yt-text-2);font-size:12px;font-style:normal;white-space:nowrap}.ngxsp-yt-episodeRail{display:grid;gap:10px;padding:16px 20px 20px;border-top:1px solid var(--yt-line)}.ngxsp-yt-episodeRail>div{display:grid;grid-auto-flow:column;grid-auto-columns:minmax(200px,220px);gap:12px;overflow-x:auto;padding-bottom:4px}.ngxsp-yt-episodeRail button{min-width:0;display:grid;gap:8px;padding:6px;border:0;border-radius:var(--yt-r-md);background:transparent;text-align:left}.ngxsp-yt-episodeRail button:hover{background:var(--yt-s2)}.ngxsp-yt-episodeRail button.active{background:var(--yt-accent-soft)}.ngxsp-yt-episodeThumb{position:relative;aspect-ratio:16 / 9;overflow:hidden;display:grid;place-items:center;border-radius:6px;background:linear-gradient(140deg,#4f6f94,#131d2a)}.ngxsp-yt-episodeThumb>i{color:#fffc;font-size:22px;font-weight:700;font-style:normal}.ngxsp-yt-episodeThumb>em{position:absolute;left:0;right:0;bottom:0;height:3px;background:#fff3}.ngxsp-yt-episodeThumb>em i{position:absolute;inset:0 auto 0 0;background:var(--yt-accent)}.ngxsp-yt-episodeRail button>span:last-child{min-width:0;display:grid;gap:1px}.ngxsp-yt-episodeRail strong{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:13px;font-weight:500}.ngxsp-yt-episodeRail small,.ngxsp-yt-episodeRail button em{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--yt-text-2);font-size:12px;font-style:normal}.ngxsp-yt-library{flex:0 0 auto;display:grid;gap:12px}.ngxsp-yt-shelf{display:grid;gap:8px}.ngxsp-yt-shelfHead{display:flex;align-items:baseline;gap:8px;min-width:0}.ngxsp-yt-shelfHead strong{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:14px;font-weight:700}.ngxsp-yt-shelfHead small,.ngxsp-yt-shelfAll{flex:0 0 auto}.ngxsp-yt-shelfHead small{color:var(--yt-text-2);font-size:11.5px;font-variant-numeric:tabular-nums}.ngxsp-yt-shelfAll{margin-left:auto;padding:2px 8px;border:0;border-radius:999px;background:transparent;color:var(--yt-accent-strong);font:inherit;font-size:11.5px;font-weight:600;cursor:pointer}.ngxsp-yt-shelfAll:hover,.ngxsp-yt-shelfAll:focus-visible{background:var(--yt-s2)}.ngxsp-yt-trail{display:flex;flex-wrap:wrap;align-items:center;gap:6px;font-size:12px;color:var(--yt-text-2)}.ngxsp-yt-trail b{color:var(--yt-text);font-weight:600}.ngxsp-yt-trailStep{padding:2px 6px;border:0;border-radius:6px;background:transparent;color:inherit;font:inherit;font-size:12px;cursor:pointer}.ngxsp-yt-trailStep:hover,.ngxsp-yt-trailStep:focus-visible{background:var(--yt-s2);color:var(--yt-text)}.ngxsp-yt-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:20px 16px}.ngxsp-yt-grid.list{grid-template-columns:1fr;gap:4px}.ngxsp-yt-grid.rail{--yt-rail-card:clamp(168px, 17vw, 320px);display:flex;gap:16px;overflow-x:auto;overscroll-behavior-x:contain;scroll-snap-type:x proximity;margin-inline:calc(var(--yt-gutter, 24px) * -1);padding-inline:var(--yt-gutter, 24px);scroll-padding-inline:var(--yt-gutter, 24px);padding-bottom:4px;scrollbar-width:thin}.ngxsp-yt-grid.rail>*{flex:0 0 var(--yt-rail-card);scroll-snap-align:start}.ngxsp-yt-card{min-width:0;display:grid;align-content:start;gap:10px;padding:0;border:0;background:transparent;text-align:left}.ngxsp-yt-thumbnail{position:relative;aspect-ratio:16 / 9;overflow:hidden;border-radius:var(--yt-r-md);background:var(--yt-s2);box-shadow:0 0 0 1px var(--yt-line)}.ngxsp-yt-thumbnail img{transition:transform .4s var(--yt-ease)}.ngxsp-yt-card:hover .ngxsp-yt-thumbnail img{transform:scale(1.04)}.ngxsp-yt-card.active .ngxsp-yt-thumbnail{box-shadow:0 0 0 2px var(--yt-accent)}.ngxsp-yt-cardCopy{min-width:0;display:grid;gap:2px;padding:0 2px}.ngxsp-yt-cardCopy strong{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;font-size:13px;font-weight:500;line-height:1.35}.ngxsp-yt-card:hover .ngxsp-yt-cardCopy strong{color:var(--yt-text)}.ngxsp-yt-card.active .ngxsp-yt-cardCopy strong{color:var(--yt-accent)}.ngxsp-yt-cardCopy span,.ngxsp-yt-cardCopy small{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ngxsp-yt-cardCopy span{color:var(--yt-text-2);font-size:12px}.ngxsp-yt-cardCopy small{color:var(--yt-text-3);font-size:11px}.ngxsp-yt-grid.list .ngxsp-yt-card{grid-template-columns:160px minmax(0,1fr);align-items:center;gap:14px;padding:6px;border-radius:var(--yt-r-md)}.ngxsp-yt-grid.list .ngxsp-yt-card:hover{background:var(--yt-s1)}.ngxsp-yt-sheetHeader,.ngxsp-yt-queueOpen{display:none}.ngxsp-yt-textButton{border:1px solid var(--yt-line);border-radius:var(--yt-r-sm);background:var(--yt-s2);color:var(--yt-text);font:inherit;font-size:13px;padding:6px 12px;cursor:pointer}.ngxsp-yt-panelTabs{display:flex;flex-wrap:wrap;gap:6px;padding:12px 8px 8px}.ngxsp-yt-panelPill{padding:6px 11px;border:1px solid var(--yt-line);border-radius:999px;background:transparent;color:inherit;font:inherit;font-size:12px;line-height:1.2;cursor:pointer}.ngxsp-yt-panelPill.active{background:var(--yt-accent-strong);border-color:transparent;color:var(--yt-on-accent);font-weight:600}.ngxsp-yt-panelStack{display:flex;flex-direction:column;gap:10px;min-height:0}.ngxsp-yt-stackItem{border:1px solid var(--yt-line);border-radius:10px;overflow:hidden;display:flex;flex-direction:column;min-height:0}.ngxsp-yt-stackItem.collapsed{flex:0 0 auto}.ngxsp-yt-stackHead{display:flex;align-items:center;gap:8px;padding:7px 8px 7px 11px;background:var(--yt-surface-2,rgba(127,127,127,.08))}.ngxsp-yt-stackHead strong{font-size:12.5px;font-weight:600}.ngxsp-yt-stackHead small{color:var(--yt-text-2);font-size:11px}.ngxsp-yt-stackIcon{display:grid;place-items:center;width:24px;height:24px;padding:0;border:0;border-radius:6px;background:transparent;color:inherit;cursor:pointer;opacity:.65}.ngxsp-yt-stackIcon:hover,.ngxsp-yt-stackIcon:focus-visible{opacity:1}.ngxsp-yt-stackHead .ngxsp-yt-stackIcon:first-of-type{margin-left:auto}.ngxsp-yt-stackItem svg{width:14px;height:14px}.ngxsp-yt-suggestions{min-width:0;min-height:0;display:flex;flex-direction:column;border-left:1px solid var(--yt-line);background:var(--yt-s0);overflow:hidden}.ngxsp-yt-cinema .ngxsp-yt-suggestions{border-left:0;border-top:1px solid var(--yt-line);background:transparent;overflow:visible;padding:0 8px 24px}.ngxsp-yt-suggestionList{flex:1;min-height:0;display:grid;align-content:start;gap:2px;padding:0 8px 12px;overflow:auto}.ngxsp-yt-cinema .ngxsp-yt-suggestionList{grid-template-columns:repeat(auto-fill,minmax(320px,1fr));overflow:visible}.ngxsp-yt-suggestion{position:relative;min-width:0;border-radius:var(--yt-r-md);transition:background-color var(--yt-dur) var(--yt-ease)}.ngxsp-yt-suggestion:hover,.ngxsp-yt-suggestion:focus-within{background:var(--yt-s2)}.ngxsp-yt-suggestion.active{background:var(--yt-accent-soft)}.ngxsp-yt-suggestionMain{width:100%;min-width:0;display:grid;grid-template-columns:140px minmax(0,1fr);align-items:start;gap:12px;padding:8px;border:0;border-radius:inherit;background:transparent;text-align:left}.ngxsp-yt-suggestion.managed .ngxsp-yt-suggestionMain{padding-right:44px}.ngxsp-yt-suggestionThumb{position:relative;aspect-ratio:16 / 9;overflow:hidden;border-radius:6px;background:var(--yt-s3)}.ngxsp-yt-suggestionThumb .ngxsp-yt-nowBadge{left:5px;right:auto;top:5px;width:22px;height:22px}.ngxsp-yt-suggestionCopy{min-width:0;display:grid;gap:3px;padding-top:1px}.ngxsp-yt-suggestionCopy strong{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;font-size:13px;font-weight:500;line-height:1.3}.ngxsp-yt-suggestion.active .ngxsp-yt-suggestionCopy strong{color:var(--yt-accent)}.ngxsp-yt-suggestionCopy span,.ngxsp-yt-suggestionCopy small{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ngxsp-yt-suggestionCopy span{color:var(--yt-text-2);font-size:12px}.ngxsp-yt-suggestionCopy small{color:var(--yt-text-3);font-size:11px}.ngxsp-yt-queueControls{position:absolute;top:4px;right:4px;display:grid;gap:0;opacity:0;transition:opacity var(--yt-dur) var(--yt-ease)}.ngxsp-yt-suggestion:hover .ngxsp-yt-queueControls,.ngxsp-yt-suggestion:focus-within .ngxsp-yt-queueControls,.ngxsp-yt-suggestion.active .ngxsp-yt-queueControls{opacity:1}.ngxsp-yt-queueControls button{width:32px;height:32px;display:grid;place-items:center;padding:0;border:0;border-radius:5px;background:transparent;color:var(--yt-text-2)}.ngxsp-yt-queueControls button:hover:not(:disabled){background:var(--yt-s3);color:var(--yt-text)}.ngxsp-yt-queueControls button:disabled{opacity:.3;cursor:default}.ngxsp-yt-queueControls ngxsp-smart-icon{width:15px;height:15px}.ngxsp-yt-backButton{display:inline-flex;align-items:center;gap:2px;max-width:100%;min-height:32px;margin:0 0 2px -6px;padding:4px 10px 4px 4px;border:0;border-radius:999px;background:transparent;color:var(--yt-text-2);font:inherit;font-size:12px;font-weight:600;cursor:pointer}.ngxsp-yt-backButton ngxsp-smart-icon{flex:none;width:16px;height:16px}.ngxsp-yt-backButton span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ngxsp-yt-backButton:hover{background:var(--yt-s2);color:var(--yt-text)}.ngxsp-yt-sideGroupTitle{grid-column:1 / -1;padding:12px 8px 4px;color:var(--yt-text-2);font-size:12px;font-weight:600;letter-spacing:.02em;text-transform:uppercase}.ngxsp-yt-sideGroupTitle:first-child{padding-top:0}@media(min-width:861px){.ngxsp-yt-sectionsBeside .ngxsp-yt-library{display:none}}.ngxsp-yt-emptyList{margin:8px;padding:28px 16px;border:1px dashed var(--yt-line-strong);border-radius:var(--yt-r-md);color:var(--yt-text-2);text-align:center}.ngxsp-yt-miniPlayer{flex:0 0 auto;display:grid;grid-template-columns:88px minmax(0,1fr) auto auto;align-items:center;gap:10px;margin:0 8px 8px;padding:8px;border:1px solid var(--yt-line);border-radius:var(--yt-r-md);background:var(--yt-s1)}.ngxsp-yt-cinema .ngxsp-yt-miniPlayer{max-width:420px}.ngxsp-yt-miniArtwork{position:relative;aspect-ratio:16 / 9;overflow:hidden;padding:0;border:0;border-radius:5px;background:var(--yt-s3)}.ngxsp-yt-miniPlayBadge{position:absolute;inset:0;display:grid;place-items:center;background:#00000059;color:#fff;opacity:0;transition:opacity var(--yt-dur) var(--yt-ease)}.ngxsp-yt-miniArtwork:hover .ngxsp-yt-miniPlayBadge,.ngxsp-yt-miniPlayer.playing .ngxsp-yt-miniPlayBadge{opacity:1}.ngxsp-yt-miniDetails{min-width:0;display:grid;gap:1px}.ngxsp-yt-miniDetails small{color:var(--yt-text-2);font-size:11px;font-weight:600}.ngxsp-yt-miniDetails strong,.ngxsp-yt-miniDetails span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ngxsp-yt-miniDetails strong{font-size:12px;font-weight:600}.ngxsp-yt-miniDetails span{color:var(--yt-text-2);font-size:11px}.ngxsp-yt-miniControl,.ngxsp-yt-miniClose{width:32px;height:32px;display:grid;place-items:center;padding:0;border:0;border-radius:999px;background:transparent;color:var(--yt-text-2)}.ngxsp-yt-miniControl{background:var(--yt-text);color:var(--yt-bg)}.ngxsp-yt-miniClose:hover{background:var(--yt-s3);color:var(--yt-text)}.ngxsp-yt-miniControl ngxsp-smart-icon,.ngxsp-yt-miniClose ngxsp-smart-icon{width:15px;height:15px}.ngxsp-yt-miniRestore{flex:0 0 auto;margin:0 16px 12px;color:var(--yt-text-2)}.ngxsp-yt-inspector{flex:1;min-height:0;display:flex;flex-direction:column}.ngxsp-yt-inspectorHead{display:grid;grid-template-columns:36px minmax(0,1fr) auto;align-items:center;gap:10px;padding:14px 12px 12px 16px;border-bottom:1px solid var(--yt-line)}.ngxsp-yt-inspectorIcon{width:36px;height:36px;display:grid;place-items:center;border-radius:9px;background:var(--yt-accent-soft);color:var(--yt-accent)}.ngxsp-yt-inspectorTitle{min-width:0;display:grid;gap:1px}.ngxsp-yt-inspectorTitle strong{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:14px;font-weight:600}.ngxsp-yt-inspectorTitle small{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--yt-text-2);font-size:12px}.ngxsp-yt-inspectorClose{width:32px;height:32px;display:grid;place-items:center;padding:0;border:0;border-radius:var(--yt-r-sm);background:transparent;color:var(--yt-text-2)}.ngxsp-yt-inspectorClose:hover{background:var(--yt-s2);color:var(--yt-text)}.ngxsp-yt-inspectorScroll{flex:1;min-height:0;display:grid;align-content:start;gap:14px;padding:14px 16px;overflow:auto}.ngxsp-yt-inspectorScroll ngxsp-settings{--ngxsp-settings-surface:var(--yt-s2);--ngxsp-settings-text:var(--yt-text);--ngxsp-settings-muted:var(--yt-text-2);--ngxsp-settings-line:var(--yt-line);--ngxsp-settings-control:var(--yt-s3);--ngxsp-settings-accent:var(--yt-accent);--ngxsp-settings-radius:var(--yt-r-md);gap:20px}.ngxsp-yt-inspectorStatus{color:var(--yt-text-2);font-size:13px;line-height:1.5}.ngxsp-yt-inspectorMetrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}.ngxsp-yt-inspectorMetric{min-width:0;display:grid;gap:3px;padding:10px 12px;border:1px solid var(--yt-line);border-radius:var(--yt-r-md);background:var(--yt-s1)}.ngxsp-yt-inspectorMetric small{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--yt-text-2);font-size:11px}.ngxsp-yt-inspectorMetric strong{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:15px;font-weight:600;font-variant-numeric:tabular-nums}.ngxsp-yt-inspectorMetric.success strong{color:var(--yt-success)}.ngxsp-yt-inspectorMetric.warning strong{color:var(--yt-warning)}.ngxsp-yt-inspectorList{display:grid;border:1px solid var(--yt-line);border-radius:var(--yt-r-md);overflow:hidden}.ngxsp-yt-inspectorItem{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;padding:10px 12px;background:var(--yt-s1)}.ngxsp-yt-inspectorItem+.ngxsp-yt-inspectorItem{border-top:1px solid var(--yt-line)}.ngxsp-yt-inspectorItem>span{min-width:0;display:grid;gap:2px}.ngxsp-yt-inspectorItem strong{font-size:13px;font-weight:500}.ngxsp-yt-inspectorItem small{color:var(--yt-text-2);font-size:12px}.ngxsp-yt-inspectorItem em{flex:0 0 auto;max-width:45%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;padding:1px 7px;border-radius:999px;background:var(--yt-s3);color:var(--yt-text-2);font-size:11px;font-style:normal;font-weight:500}.ngxsp-yt-inspectorItem.success em{background:#3ecf8e24;color:var(--yt-success)}.ngxsp-yt-inspectorItem.warning em{background:#f2a93b24;color:var(--yt-warning)}.ngxsp-yt-resumePrompt{position:absolute;left:14px;bottom:84px;z-index:6;display:flex;align-items:center;flex-wrap:wrap;gap:8px;max-width:calc(100% - 28px);padding:8px 8px 8px 14px;border-radius:var(--yt-r-md);background:#0c0d11cc;box-shadow:0 10px 30px #00000059,0 0 0 1px #ffffff14;-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);color:#f1f1f4;font-size:13px}.ngxsp-yt-resumePrompt span{margin-right:4px}.ngxsp-yt-resumePrompt button{height:32px;padding:0 12px;border:1px solid rgb(255 255 255 / .22);border-radius:999px;background:transparent;color:inherit;font:inherit;font-weight:600;cursor:pointer}.ngxsp-yt-resumePrompt button.primary{border-color:transparent;background:#fff;color:#111}.ngxsp-yt-resumePrompt button:hover{background:#ffffff24}.ngxsp-yt-resumePrompt button.primary:hover{background:#ffffffdb}.ngxsp-yt-compact .ngxsp-yt-resumePrompt{left:8px;bottom:56px;font-size:12px}.ngxsp-yt-playerFrame{container-type:inline-size;--yt-info-w:min(360px, 42cqw)}.ngxsp-yt-infoOpen ngx-smart-player ::ng-deep .ngxsp-vod-centerDock{transform:translate(calc(var(--yt-info-w) / -2 - 5px));transition:transform .2s ease}.ngxsp-yt-infoOverlay{position:absolute;top:10px;right:10px;bottom:76px;z-index:6;width:var(--yt-info-w);display:flex;flex-direction:column;border-radius:var(--yt-r-md);background:#0c0d11b8;box-shadow:0 12px 40px #0006,0 0 0 1px #ffffff14;-webkit-backdrop-filter:blur(14px) saturate(1.2);backdrop-filter:blur(14px) saturate(1.2);color:#f1f1f4;overflow:hidden}.ngxsp-yt-infoTabs{display:flex;align-items:center;gap:2px;padding:6px;border-bottom:1px solid rgb(255 255 255 / .1);overflow-x:auto;scrollbar-width:none}.ngxsp-yt-infoTabs button{flex:none;display:inline-flex;align-items:center;gap:6px;height:32px;padding:0 10px;border:0;border-radius:var(--yt-r-sm);background:transparent;color:#ffffffb8;font:inherit;font-size:12px;white-space:nowrap;cursor:pointer}.ngxsp-yt-infoTabs button ngxsp-smart-icon{width:16px;height:16px}.ngxsp-yt-infoTabs button:hover{background:#ffffff1a;color:#fff}.ngxsp-yt-infoTabs button.active{background:#ffffff24;color:#fff}.ngxsp-yt-infoTabs button:not(.active) span{display:none}.ngxsp-yt-infoTabs .ngxsp-yt-infoClose{margin-left:auto}.ngxsp-yt-infoHead{display:grid;gap:2px;padding:12px 16px 0}.ngxsp-yt-infoHead strong{font-size:15px;font-weight:600}.ngxsp-yt-infoHead small{color:#ffffffa6;font-size:12px}.ngxsp-yt-infoOverlay .ngxsp-yt-inspectorScroll{--yt-text:#f1f1f4;--yt-text-2:rgb(255 255 255 / .68);--yt-line:rgb(255 255 255 / .12);--yt-s1:rgb(255 255 255 / .06);--yt-s2:rgb(255 255 255 / .1)}.ngxsp-yt-infoOverlay .ngxsp-yt-inspectorActions{--yt-line:rgb(255 255 255 / .12);--yt-s1:rgb(255 255 255 / .06);--yt-text:#f1f1f4}@media(min-width:861px){.ngxsp-yt-hasInfoTabs:not(.ngxsp-yt-sources) .ngxsp-yt-toolRow{display:none}}@media(max-width:860px){.ngxsp-yt-infoOverlay{display:none}}.ngxsp-yt-inspectorActions{display:flex;flex-wrap:wrap;gap:8px;padding:12px 16px 16px;border-top:1px solid var(--yt-line)}.ngxsp-yt-veil{position:absolute;inset:0;z-index:3;pointer-events:none;background:#000}.ngxsp-yt-touchBadge{position:absolute;left:50%;top:50%;z-index:6;transform:translate(-50%,-50%);display:grid;justify-items:center;gap:6px;min-width:112px;padding:14px 18px;pointer-events:none;border-radius:var(--yt-r-lg);background:#000000b8;color:#fff}.ngxsp-yt-touchBadge strong{font-size:15px;font-weight:600;font-variant-numeric:tabular-nums}.ngxsp-yt-touchBadge em{display:block;width:88px;height:4px;border-radius:999px;background:#ffffff47;overflow:hidden}.ngxsp-yt-touchBadge em i{display:block;height:100%;border-radius:999px;background:#fff}.ngxsp-yt-cardWrap{position:relative;display:block;min-width:0}.ngxsp-yt-cardMore{position:absolute;top:8px;right:8px;z-index:2;display:grid;place-items:center;width:30px;height:30px;border:0;border-radius:50%;background:#0000009e;color:#fff;opacity:0;cursor:pointer;transition:opacity var(--yt-dur) var(--yt-ease)}.ngxsp-yt-cardWrap:hover .ngxsp-yt-cardMore,.ngxsp-yt-cardMore:focus-visible,.ngxsp-yt-cardWrap:focus-within .ngxsp-yt-cardMore{opacity:1}@media(hover:none){.ngxsp-yt-cardMore{opacity:1}}.ngxsp-yt-cardMenuScrim{position:fixed;inset:0;z-index:60;padding:0;border:0;background:transparent;cursor:default}.ngxsp-yt-cardMenu{position:fixed;z-index:61;min-width:208px;max-width:280px;display:grid;gap:2px;padding:6px;border:1px solid var(--yt-line-strong);border-radius:var(--yt-r-md);background:var(--yt-s2);box-shadow:0 16px 40px #0000007a}.ngxsp-yt-cardMenuTitle{padding:6px 10px 8px;font-size:11px;font-weight:600;color:var(--yt-text-3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ngxsp-yt-cardMenu button{display:flex;align-items:center;gap:10px;min-height:34px;padding:0 10px;border:0;border-radius:var(--yt-r-sm);background:transparent;color:var(--yt-text);font-size:13px;text-align:left;cursor:pointer}.ngxsp-yt-cardMenu button:hover{background:var(--yt-s3)}.ngxsp-yt-cardMenu button.danger{color:var(--yt-danger-text)}.ngxsp-yt-cardMenu button.danger:hover{background:#f25f5c29}.ngxsp-yt-convert{position:absolute;left:12px;right:12px;bottom:12px;z-index:45;display:flex;align-items:center;gap:12px;padding:10px 14px;border:1px solid var(--yt-line-strong);border-radius:var(--yt-r-lg);background:var(--yt-s2);box-shadow:0 12px 32px #0000006b}.ngxsp-yt-convert.asking{border-color:var(--yt-accent)}.ngxsp-yt-convert.danger{border-color:var(--yt-danger)}.ngxsp-yt-convert.danger .ngxsp-yt-convertIcon{background:#f25f5c29;color:var(--yt-danger-text)}.ngxsp-yt-convert.danger .ngxsp-yt-convertActions button.keep{background:var(--yt-danger);color:var(--yt-on-accent)}.ngxsp-yt-convertIcon{display:grid;place-items:center;width:32px;height:32px;flex:0 0 auto;border-radius:50%;background:var(--yt-accent-soft);color:var(--yt-accent)}.ngxsp-yt-convertText{display:flex;flex-direction:column;gap:4px;min-width:0;flex:1 1 auto}.ngxsp-yt-convertText strong{font-size:13px;font-weight:600}.ngxsp-yt-convertText small{font-size:11px;color:var(--yt-text-2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ngxsp-yt-convertText em{display:block;height:4px;border-radius:999px;background:var(--yt-s3);overflow:hidden}.ngxsp-yt-convertText em i{display:block;height:100%;border-radius:999px;background:var(--yt-accent);transition:width var(--yt-dur) var(--yt-ease)}.ngxsp-yt-convertActions{display:flex;gap:8px;flex:0 0 auto}.ngxsp-yt-convertActions button{min-height:32px;padding:0 14px;border:1px solid var(--yt-line-strong);border-radius:999px;background:var(--yt-s1);color:var(--yt-text);font-size:12px;font-weight:600;cursor:pointer}.ngxsp-yt-convertActions button.keep{border-color:transparent;background:var(--yt-accent-strong);color:var(--yt-on-accent)}@media(max-width:560px){.ngxsp-yt-convert{flex-wrap:wrap}.ngxsp-yt-convertActions{width:100%;justify-content:flex-end}}.ngxsp-yt-dropOverlay{position:absolute;inset:12px;z-index:40;display:grid;place-content:center;justify-items:center;gap:10px;border:2px dashed var(--yt-accent);border-radius:14px;background:var(--nsp-scrim, rgb(13 14 17 / .88));-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);color:var(--yt-text);text-align:center;pointer-events:none}.ngxsp-yt-dropIcon{width:56px;height:56px;display:grid;place-items:center;border-radius:14px;background:var(--yt-accent-soft);color:var(--yt-accent)}.ngxsp-yt-dropIcon ngxsp-smart-icon{width:28px;height:28px}.ngxsp-yt-dropOverlay strong{font-size:16px;font-weight:600}.ngxsp-yt-dropOverlay span:last-child{color:var(--yt-text-2)}@media(hover:none),(pointer:coarse){.ngxsp-yt-queueControls,.ngxsp-yt-searchActions{opacity:1}}@media(max-width:1380px){.ngxsp-yt-shell{--yt-side-w:320px}.ngxsp-yt-suggestionMain{grid-template-columns:120px minmax(0,1fr)}.ngxsp-yt-watchActions button>span{display:none}.ngxsp-yt-watchActions button{width:34px;padding:0;justify-content:center}.ngxsp-yt-rating button+button{padding-left:0}}@media(max-width:1180px){.ngxsp-yt-topbar{grid-template-columns:auto minmax(200px,1fr) auto}.ngxsp-yt-status,.ngxsp-yt-sourceButton span{display:none}.ngxsp-yt-sourceButton{width:32px;padding:0;justify-content:center}.ngxsp-yt-mediaHero{grid-template-columns:200px minmax(0,1fr)}}@media(max-width:1040px)and (min-width:861px){.ngxsp-yt-body,.ngxsp-queue-hidden .ngxsp-yt-body{display:block;overflow:auto}.ngxsp-yt-watch{overflow:visible}.ngxsp-yt-suggestions{border-left:0;border-top:1px solid var(--yt-line);background:transparent;overflow:visible;padding:0 16px 24px}.ngxsp-yt-suggestionList{grid-template-columns:repeat(auto-fill,minmax(300px,1fr));overflow:visible}.ngxsp-yt-playerFrame{width:100%}}@media(max-width:860px){.ngxsp-yt-shell{height:auto;min-height:100%;grid-template-columns:minmax(0,1fr);grid-template-rows:auto auto auto;grid-template-areas:\"top\" \"rail\" \"body\";overflow:visible}.ngxsp-yt-topbar{grid-template-columns:minmax(0,1fr) auto;grid-template-rows:48px auto;gap:0 8px;padding:0 12px 10px}.ngxsp-yt-search{grid-column:1 / -1;grid-row:2}.ngxsp-yt-railMenu,.ngxsp-yt-round,.ngxsp-yt-languageSwitch,.ngxsp-yt-status{display:none}.ngxsp-yt-notificationsButton,.ngxsp-yt-windowButton{display:grid}.ngxsp-yt-rail{flex-direction:row;padding:6px 12px;border-right:0;border-bottom:1px solid var(--yt-line);overflow-x:auto}.ngxsp-yt-railButton{width:auto;min-height:36px;flex:0 0 auto;flex-direction:row;gap:6px;padding:0 10px}.ngxsp-yt-railButton span{font-size:12px}.ngxsp-yt-body,.ngxsp-queue-hidden .ngxsp-yt-body,.ngxsp-yt-cinema .ngxsp-yt-body{display:block;overflow:visible}.sheet>.ngxsp-yt-sheetHeader{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 0;position:sticky;top:0;background:var(--yt-s0);z-index:1}.ngxsp-yt-watch{--yt-gutter:12px;gap:14px;padding:12px;overflow:visible}.ngxsp-yt-playerFrame,.ngxsp-yt-cinema .ngxsp-yt-playerFrame{width:100%;min-width:0;border-radius:var(--yt-r-sm)}.ngxsp-yt-transportBar{padding:28px 6px 6px;gap:4px}.ngxsp-yt-transportInfo,.ngxsp-yt-transportCinema{display:none}.ngxsp-yt-transportBar{grid-template-columns:auto auto;justify-content:space-between}.ngxsp-yt-titleGroup h1{font-size:18px}.ngxsp-yt-watchActions>button:nth-of-type(n+3){display:none}.ngxsp-yt-rating button+button{display:none}.ngxsp-yt-toolRow{display:flex;gap:8px;overflow-x:auto;scrollbar-width:none;padding-bottom:2px}.ngxsp-yt-toolRow::-webkit-scrollbar{display:none}.ngxsp-yt-toolRow .ngxsp-yt-tool{flex:0 0 auto;height:44px;grid-template-columns:24px auto;gap:8px}.ngxsp-yt-toolRow .ngxsp-yt-toolIcon{width:24px;height:24px}.ngxsp-yt-toolRow .ngxsp-yt-toolCopy strong{display:none}.ngxsp-yt-toolRow .ngxsp-yt-tool[data-tool=devices],.ngxsp-yt-toolRow .ngxsp-yt-tool[data-tool=cast]{order:-1}.ngxsp-yt-toolRow .ngxsp-yt-toolCopy small{color:var(--yt-text);font-size:13px}.ngxsp-yt-mediaHero{grid-template-columns:1fr;padding:14px}.ngxsp-yt-mediaHero.echoesHeader .ngxsp-yt-mediaPoster,.ngxsp-yt-mediaHero.echoesHeader .ngxsp-yt-mediaKicker,.ngxsp-yt-mediaHero.echoesHeader .ngxsp-yt-mediaDetailBody>h2,.ngxsp-yt-mediaHero.echoesHeader .ngxsp-yt-mediaSubtitle,.ngxsp-yt-mediaHero.echoesHeader .ngxsp-yt-mediaMeta{display:none}.ngxsp-yt-mediaWorkspace{display:none}.ngxsp-yt-episodeRail{padding:14px}.ngxsp-yt-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:14px 10px}.ngxsp-yt-grid .ngxsp-yt-card:nth-child(n+5){display:none}.ngxsp-yt-searchResults{grid-template-columns:1fr}.ngxsp-yt-queueOpen{display:flex;align-items:center;gap:10px;width:100%;min-height:44px;padding:0 12px;border:1px solid var(--yt-line);border-radius:var(--yt-r-md);background:var(--yt-s1);color:var(--yt-text);font:inherit;font-size:14px;text-align:left;cursor:pointer}.ngxsp-yt-queueOpen span{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ngxsp-yt-queueOpen small{color:var(--yt-text-2);font-size:12px}.ngxsp-yt-suggestions,.ngxsp-yt-cinema .ngxsp-yt-suggestions{display:none}.ngxsp-yt-suggestions.sheet,.ngxsp-yt-cinema .ngxsp-yt-suggestions.sheet{display:flex;position:fixed;inset:0;z-index:40;border:0;overflow-y:auto;background:var(--yt-s0);padding:env(safe-area-inset-top,0px) 12px calc(24px + env(safe-area-inset-bottom,0px))}.ngxsp-yt-suggestionList,.ngxsp-yt-cinema .ngxsp-yt-suggestionList{grid-template-columns:1fr;overflow:visible}.ngxsp-yt-suggestionList .ngxsp-yt-suggestion:nth-child(n+3){display:grid}.ngxsp-yt-suggestionMain{grid-template-columns:128px minmax(0,1fr)}.ngxsp-yt-miniPlayer{display:none}.ngxsp-yt-library-focus .ngxsp-yt-library{order:3}}@media(max-width:540px){.ngxsp-yt-brand strong{font-size:12px}.ngxsp-yt-mode button{padding:0 8px}.ngxsp-yt-railButton span{display:none}.ngxsp-yt-grid{grid-template-columns:1fr 1fr}}@media(prefers-reduced-motion:reduce){.ngxsp-yt-shell *,.ngxsp-yt-shell *:before,.ngxsp-yt-shell *:after{animation:none!important;transition:none!important}}.ngxsp-yt-body.ngxsp-yt-playing{display:grid;grid-template-columns:minmax(0,1fr) var(--yt-side-w);overflow:hidden;min-height:0}.ngxsp-queue-hidden .ngxsp-yt-body.ngxsp-yt-playing{grid-template-columns:minmax(0,1fr)}@media(max-width:1024px){.ngxsp-yt-shell.ngxsp-yt-playerView .ngxsp-yt-topbar,.ngxsp-yt-shell.ngxsp-yt-playerView .ngxsp-yt-rail{display:none}.ngxsp-yt-shell.ngxsp-yt-playerView{height:100vh;height:100dvh;min-height:0;grid-template-areas:none;grid-template-columns:minmax(0,1fr);grid-template-rows:minmax(0,1fr)}.ngxsp-yt-shell.ngxsp-yt-playerView .ngxsp-yt-body{grid-area:auto}.ngxsp-yt-shell.ngxsp-yt-playerView .ngxsp-yt-body{grid-template-columns:minmax(0,1fr);height:100%}.ngxsp-yt-shell.ngxsp-yt-playerView .ngxsp-yt-watch.player{height:100%;padding:0}.ngxsp-yt-shell.ngxsp-yt-playerView .ngxsp-yt-playerShell{flex:1 1 auto;min-height:0}.ngxsp-yt-shell.ngxsp-yt-playerView .ngxsp-yt-playerFrame{height:100%;aspect-ratio:auto;border-radius:0}.ngxsp-yt-shell.ngxsp-yt-playerView .ngxsp-yt-playerBack{display:inline-flex}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: SmartPlayerComponent, selector: "ngx-smart-player", inputs: ["config", "plugins", "ports", "engines", "debug", "startTimeSec", "autoplayOnInit", "playRequest", "canPrev", "canNext", "previousLabel", "nextLabel", "playLabel", "pauseLabel", "shuffleActive", "shuffleLabel", "repeatMode", "repeatLabel", "hasPlaylistControls", "mediaMode", "castEnabled", "castConnected", "documentShortcuts", "nowPlayingLabel", "nowListeningLabel", "fullscreenHintLabel", "fullscreenLabel", "readyStatusLabel", "playingStatusLabel", "pausedStatusLabel", "bufferingStatusLabel", "loadingStatusLabel", "endedStatusLabel", "errorStatusLabel", "outputVolume", "outputVolumeLabel", "outputMuteLabel", "outputUnmuteLabel", "timelineDurationSec", "timelineMarkers", "menuLabels"], outputs: ["ended", "videoSize", "progress", "prev", "next", "toggleShuffle", "cycleRepeat", "subtitles", "subtitlesLoaded", "cast", "stateChange", "playbackToggle", "outputVolumeChange"] }, { kind: "component", type: SmartPlayerIconComponent, selector: "ngxsp-smart-icon", inputs: ["name"] }, { kind: "component", type: SmartSettingsComponent, selector: "ngxsp-settings", inputs: ["sections"], outputs: ["settingChange", "settingAction"] }, { kind: "component", type: NgxspVolumeControl, selector: "ngxsp-volume-control", inputs: ["volume", "muted", "disabled", "compact", "showValue", "step", "volumeLabel", "muteLabel", "unmuteLabel"], outputs: ["volumeChange", "muteToggle"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2295
+ }
2296
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.33", ngImport: i0, type: NgxSmartPlayerVideoUiComponent, decorators: [{
2297
+ type: Component,
2298
+ args: [{ selector: 'ngxsp-video-player-ui', standalone: true, imports: [CommonModule, SmartPlayerComponent, SmartPlayerIconComponent, SmartSettingsComponent, NgxspVolumeControl], template: `
2299
+ <section
2300
+ class="ngxsp-yt-shell"
2301
+ [class.ngxsp-yt-online]="runtimeMode === 'online'"
2302
+ [class.ngxsp-yt-offline]="runtimeMode === 'offline'"
2303
+ [class.ngxsp-yt-playing]="isPlaying"
2304
+ [class.ngxsp-yt-playerView]="playerView"
2305
+ [class.ngxsp-yt-cinema]="cinemaMode"
2306
+ [class.ngxsp-has-source]="!!config"
2307
+ [class.ngxsp-queue-hidden]="!queueVisible"
2308
+ [class.ngxsp-mini-hidden]="!miniPlayerVisible"
2309
+ [class.ngxsp-yt-dragActive]="fileDragActive"
2310
+ [class.ngxsp-yt-library-focus]="isLibraryFocused"
2311
+ [class.ngxsp-yt-menuRow]="menuLayout === 'horizontal'"
2312
+ [class.ngxsp-yt-menuOff]="menuLayout === 'hidden'"
2313
+ [class.ngxsp-yt-railExpanded]="railExpanded"
2314
+ [class.ngxsp-yt-compact]="compactMode === 'mini'"
2315
+ (dragenter)="onFileDragEnter($event)"
2316
+ (dragover)="onFileDragOver($event)"
2317
+ (dragleave)="onFileDragLeave($event)"
2318
+ (drop)="onFileDrop($event)"
2319
+ >
2320
+ @if (fileDragActive) {
2321
+ <div class="ngxsp-yt-dropOverlay" aria-hidden="true">
2322
+ <span class="ngxsp-yt-dropIcon"><ngxsp-smart-icon name="folder" /></span>
2323
+ <strong>{{ sourceDropTitle || sourceChangeTitle || sourceChangeLabel }}</strong>
2324
+ @if (sourceDropHint) {
2325
+ <span>{{ sourceDropHint }}</span>
2326
+ }
2327
+ </div>
2328
+ }
2329
+
2330
+ @if (promptBar) {
2331
+ <div class="ngxsp-yt-convert" [class.asking]="promptAsking" [class.danger]="!!promptBar.danger" [attr.role]="promptAsking ? 'alertdialog' : 'status'" aria-live="polite">
2332
+ <span class="ngxsp-yt-convertIcon" aria-hidden="true"><ngxsp-smart-icon [name]="promptBar.danger ? 'trash' : 'download'" /></span>
2333
+ <span class="ngxsp-yt-convertText">
2334
+ <strong>{{ promptBar.message }}</strong>
2335
+ @if (promptBar.hint) { <small>{{ promptBar.hint }}</small> }
2336
+ @if (promptBar.percent !== undefined) {
2337
+ <em role="progressbar" [attr.aria-valuenow]="safeProgressPercent(promptBar.percent)" aria-valuemin="0" aria-valuemax="100">
2338
+ <i [style.width.%]="safeProgressPercent(promptBar.percent)"></i>
2339
+ </em>
2340
+ }
2341
+ </span>
2342
+ @if (promptAsking) {
2343
+ <span class="ngxsp-yt-convertActions">
2344
+ <button type="button" class="keep" (click)="promptAnswer.emit(true)">{{ promptBar.confirmLabel }}</button>
2345
+ <button type="button" (click)="promptAnswer.emit(false)">{{ promptBar.cancelLabel }}</button>
2346
+ </span>
2347
+ }
2348
+ </div>
2349
+ }
2350
+
2351
+ @if (cardMenu) {
2352
+ <button
2353
+ class="ngxsp-yt-cardMenuScrim"
2354
+ type="button"
2355
+ [attr.aria-label]="cardMenuCloseLabel"
2356
+ (click)="closeCardMenu()"
2357
+ (contextmenu)="$event.preventDefault(); closeCardMenu()"
2358
+ ></button>
2359
+ <div
2360
+ #cardMenuBox
2361
+ class="ngxsp-yt-cardMenu"
2362
+ role="menu"
2363
+ tabindex="-1"
2364
+ [attr.aria-label]="cardMenu.card.title"
2365
+ [style.left.px]="cardMenu.x"
2366
+ [style.top.px]="cardMenu.y"
2367
+ (keydown.escape)="closeCardMenu()"
2368
+ >
2369
+ <strong class="ngxsp-yt-cardMenuTitle">{{ cardMenu.card.title }}</strong>
2370
+ @for (action of cardMenu.card.actions; track action.id) {
2371
+ <button type="button" role="menuitem" [class.danger]="!!action.danger" (click)="chooseCardAction(action)">
2372
+ <ngxsp-smart-icon [name]="action.icon" />
2373
+ <span>{{ action.label }}</span>
2374
+ </button>
2375
+ }
2376
+ </div>
2377
+ }
2378
+
2379
+ <header class="ngxsp-yt-topbar" data-tauri-drag-region>
2380
+ <div class="ngxsp-yt-brand">
2381
+ <button class="ngxsp-yt-railMenu" type="button" [attr.aria-expanded]="railExpanded" [attr.aria-label]="menuLabel" [title]="menuLabel" (click)="toggleRail()">
2382
+ <ngxsp-smart-icon name="menu" />
2383
+ </button>
2384
+ @if (brandLogoSrc) {
2385
+ <img class="ngxsp-yt-brandLogo" [src]="brandLogoSrc" alt="" width="26" height="26" />
2386
+ } @else {
2387
+ <span class="ngxsp-yt-brandIcon" aria-hidden="true"><ngxsp-smart-icon name="play" /></span>
2388
+ }
2389
+ <strong>NGXSP Video</strong>
2390
+ </div>
2391
+
2392
+ <label class="ngxsp-yt-search">
2393
+ <ngxsp-smart-icon name="search" />
2394
+ <input type="search" [placeholder]="searchPlaceholder" [value]="searchTerm" (input)="onSearchInput($event)" (focus)="searchOpen = true" [attr.aria-label]="searchLabel" />
2395
+ </label>
2396
+
2397
+ <div class="ngxsp-yt-actions">
2398
+ @if (statusMessage) {
2399
+ <span class="ngxsp-yt-status" [class.success]="statusKind === 'success'" [class.error]="statusKind === 'error'" [attr.role]="statusKind === 'error' ? 'alert' : 'status'" [attr.aria-live]="statusKind === 'error' ? 'assertive' : 'polite'" aria-atomic="true" [title]="statusMessage">
2400
+ {{ statusMessage }}
2401
+ </span>
2402
+ }
2403
+ @if (runtimeModeInHeader) {
2404
+ <div class="ngxsp-yt-mode" role="group" [attr.aria-label]="runtimeModeLabel">
2405
+ <button type="button" [class.active]="runtimeMode === 'online'" [attr.aria-pressed]="runtimeMode === 'online'" (click)="selectRuntimeMode('online')"><i aria-hidden="true"></i>{{ onlineLabel }}</button>
2406
+ <button type="button" [class.active]="runtimeMode === 'offline'" [attr.aria-pressed]="runtimeMode === 'offline'" (click)="selectRuntimeMode('offline')"><i aria-hidden="true"></i>{{ offlineLabel }}</button>
2407
+ </div>
2408
+ }
2409
+ @if (folderImportLabel) {
2410
+ <!-- A folder is what builds the library; picking a single file next to it led nowhere. -->
2411
+ <button class="ngxsp-yt-sourceButton" type="button" [disabled]="libraryBusy" [attr.aria-label]="folderImportLabel" [title]="folderImportLabel" (click)="addFolder.emit()">
2412
+ <ngxsp-smart-icon name="folder" />
2413
+ <span>{{ folderImportLabel }}</span>
2414
+ </button>
2415
+ } @else {
2416
+ <button class="ngxsp-yt-sourceButton" type="button" [attr.aria-label]="sourceChangeTitle || sourceChangeLabel" [title]="sourceChangeTitle || sourceChangeLabel" (click)="videoFileInput.click()">
2417
+ <ngxsp-smart-icon name="folder" />
2418
+ <span>{{ sourceChangeLabel }}</span>
2419
+ </button>
2420
+ }
2421
+ <input #videoFileInput class="ngxsp-yt-fileInput" type="file" [accept]="sourceChangeAccept" aria-hidden="true" tabindex="-1" (change)="onSourceFileInput($event)" />
2422
+ @if (languageOptions.length) {
2423
+ <div class="ngxsp-yt-languageSwitch" role="group" [attr.aria-label]="languageLabel">
2424
+ @for (language of languageOptions; track language.id) {
2425
+ <button type="button" [class.active]="language.id === activeLanguage" [attr.aria-pressed]="language.id === activeLanguage" (click)="languageChange.emit(language.id)">
2426
+ {{ language.label }}
2427
+ </button>
2428
+ }
2429
+ </div>
2430
+ }
2431
+ @if (compactModes && compactMode === 'full') {
2432
+ <button class="ngxsp-yt-round ngxsp-yt-windowButton" type="button" [attr.aria-label]="compactMiniLabel" [title]="compactMiniLabel" (click)="compactModeChange.emit('mini')">
2433
+ <ngxsp-smart-icon name="window-mini" />
2434
+ </button>
2435
+ }
2436
+ <button class="ngxsp-yt-round ngxsp-yt-queueButton" type="button" [class.active]="queuePanelActive" [attr.aria-pressed]="queuePanelActive" [attr.aria-label]="queueActionLabel" [title]="queueActionLabel" (click)="onQueueAction()">
2437
+ <ngxsp-smart-icon name="panel-right" />
2438
+ </button>
2439
+ <button class="ngxsp-yt-round ngxsp-yt-notificationsButton" type="button" [attr.aria-label]="notificationsLabel" [title]="notificationsLabel" (click)="toolSelect.emit('notifications')">
2440
+ <ngxsp-smart-icon name="bell" />
2441
+ </button>
2442
+ <button class="ngxsp-yt-round" type="button" [attr.aria-label]="settingsLabel" [title]="settingsLabel" (click)="toolSelect.emit('settings')">
2443
+ <ngxsp-smart-icon name="settings" />
2444
+ </button>
2445
+ </div>
2446
+ </header>
2447
+
2448
+ <aside class="ngxsp-yt-rail" [attr.aria-label]="navigationLabel">
2449
+ @for (item of navItems; track item.id) {
2450
+ <button class="ngxsp-yt-railButton" type="button" [class.active]="item.active" [disabled]="item.disabled" [attr.aria-current]="item.active ? 'page' : null" [attr.aria-label]="item.label" [title]="item.label" (click)="selectNav(item)">
2451
+ <ngxsp-smart-icon [name]="item.icon" />
2452
+ <span>{{ item.label }}</span>
2453
+ </button>
2454
+ }
2455
+ </aside>
2456
+
2457
+ <div class="ngxsp-yt-body" [class.ngxsp-yt-playing]="playerView" [class.ngxsp-yt-sources]="!showActiveMedia" [class.ngxsp-yt-hasInfoTabs]="infoTabs.length > 0" [class.ngxsp-yt-sectionsBeside]="sidePanelContent === 'sections' && queueVisible">
2458
+ <section #contentScroll class="ngxsp-yt-watch" [class.player]="playerView" [attr.aria-label]="displayHeroTitle">
2459
+ <!-- Watching: the player has the whole area. Browsing: the area belongs to the view being read. -->
2460
+ @if (playerView) {
2461
+ <section class="ngxsp-yt-playerShell">
2462
+ @if (playerView) {
2463
+ <button class="ngxsp-yt-playerBack" type="button" [attr.aria-label]="playerBackLabel" [title]="playerBackLabel" (click)="playerBack.emit()">
2464
+ <ngxsp-smart-icon name="chevron-left" />
2465
+ <span>{{ playerBackLabel }}</span>
2466
+ </button>
2467
+ }
2468
+ <div
2469
+ class="ngxsp-yt-playerFrame"
2470
+ [class.empty]="!config"
2471
+ [class.ngxsp-yt-infoOpen]="!!infoPanel"
2472
+ (pointerdown)="onFramePointerDown($event)"
2473
+ (pointermove)="onFramePointerMove($event)"
2474
+ (pointerup)="onFramePointerUp($event)"
2475
+ (pointercancel)="onFramePointerUp($event)"
2476
+ >
2477
+ @if (brightness < 100) {
2478
+ <span class="ngxsp-yt-veil" aria-hidden="true" [style.opacity]="(100 - brightness) / 100"></span>
2479
+ }
2480
+ @if (touchAdjusting; as adjusting) {
2481
+ <span class="ngxsp-yt-touchBadge" role="status" aria-live="polite">
2482
+ <ngxsp-smart-icon [name]="adjusting.kind === 'brightness' ? 'spark' : adjusting.value === 0 ? 'volume-mute' : adjusting.value < 50 ? 'volume-low' : 'volume-high'" />
2483
+ <strong>{{ adjusting.value }}%</strong>
2484
+ <em><i [style.width.%]="adjusting.value"></i></em>
2485
+ </span>
2486
+ }
2487
+ @if (compactMode === 'mini') {
2488
+ <!-- A mini window has no title bar: this strip moves it. -->
2489
+ <div class="ngxsp-yt-compactDrag" aria-hidden="true"></div>
2490
+ }
2491
+ @if (showPosterPreview) {
2492
+ <button class="ngxsp-yt-previewPlayer" type="button" [attr.aria-label]="playLabel + ' - ' + displayHeroTitle" (click)="togglePrimaryPlayback()">
2493
+ <img [src]="displayHeroArtwork" alt="" />
2494
+ <span class="ngxsp-yt-previewOverlay" aria-hidden="true">
2495
+ <span class="ngxsp-yt-previewPlay"><ngxsp-smart-icon name="play" /></span>
2496
+ </span>
2497
+ </button>
2498
+ } @else if (config) {
2499
+ <ngx-smart-player
2500
+ [config]="config"
2501
+ [plugins]="plugins"
2502
+ [ports]="ports"
2503
+ [engines]="engines"
2504
+ [debug]="debug"
2505
+ [startTimeSec]="startTimeSec"
2506
+ [playRequest]="playRequest"
2507
+ [canPrev]="canPrev"
2508
+ [canNext]="canNext"
2509
+ [previousLabel]="previousLabel"
2510
+ [nextLabel]="nextLabel"
2511
+ [playLabel]="playLabel"
2512
+ [pauseLabel]="pauseLabel"
2513
+ [shuffleActive]="shuffleActive"
2514
+ [shuffleLabel]="shuffleLabel"
2515
+ [repeatMode]="repeatMode"
2516
+ [repeatLabel]="repeatLabel"
2517
+ [hasPlaylistControls]="hasPlaylistControls"
2518
+ [mediaMode]="'video'"
2519
+ [castEnabled]="castEnabled"
2520
+ [castConnected]="castConnected"
2521
+ [menuLabels]="playerMenuLabels"
2522
+ [documentShortcuts]="false"
2523
+ [nowPlayingLabel]="nowPlayingLabel"
2524
+ [fullscreenHintLabel]="fullscreenHintLabel"
2525
+ [fullscreenLabel]="fullscreenLabel"
2526
+ [readyStatusLabel]="readyStatusLabel"
2527
+ [playingStatusLabel]="playingStatusLabel"
2528
+ [pausedStatusLabel]="pausedStatusLabel"
2529
+ [bufferingStatusLabel]="bufferingStatusLabel"
2530
+ [loadingStatusLabel]="loadingStatusLabel"
2531
+ [endedStatusLabel]="endedStatusLabel"
2532
+ [errorStatusLabel]="errorStatusLabel"
2533
+ [outputVolume]="outputVolume / 100"
2534
+ [outputVolumeLabel]="outputVolumeLabel"
2535
+ [outputMuteLabel]="outputMuteLabel"
2536
+ [outputUnmuteLabel]="outputUnmuteLabel"
2537
+ [timelineDurationSec]="timelineDurationSec"
2538
+ [timelineMarkers]="timelineMarkers"
2539
+ (ended)="ended.emit()"
2540
+ (videoSize)="videoSize.emit($event)"
2541
+ (progress)="progress.emit($event)"
2542
+ (prev)="prev.emit()"
2543
+ (next)="next.emit()"
2544
+ (toggleShuffle)="toggleShuffle.emit()"
2545
+ (cycleRepeat)="cycleRepeat.emit()"
2546
+ (cast)="cast.emit()"
2547
+ (subtitlesLoaded)="subtitlesLoaded.emit($event)"
2548
+ (stateChange)="onPlayerStateChange($event)"
2549
+ (playbackToggle)="primaryPlaybackToggle.emit($event)"
2550
+ (outputVolumeChange)="onOutputVolumeRatioChange($event)"
2551
+ >
2552
+ <div ngxsp-player-inline-controls class="ngxsp-yt-playerInlineControls">
2553
+ <!-- What a viewer does about the video they are watching, without leaving it. -->
2554
+ @if (watchActionsInPlayer && compactMode !== 'mini') {
2555
+ <button class="ngxsp-yt-playerAction" type="button" [class.active]="likeActive" [attr.aria-pressed]="likeActive" [attr.aria-label]="likeLabel" [title]="likeLabel" (click)="toolSelect.emit('like')">
2556
+ <ngxsp-smart-icon name="thumb-up" />
2557
+ </button>
2558
+ <button class="ngxsp-yt-playerAction" type="button" [class.active]="watchLaterActive" [attr.aria-pressed]="watchLaterActive" [attr.aria-label]="watchLaterLabel" [title]="watchLaterLabel" (click)="watchLaterToggle.emit()">
2559
+ <ngxsp-smart-icon name="clock" />
2560
+ </button>
2561
+ <button class="ngxsp-yt-playerAction" type="button" [attr.aria-label]="queueAddActiveLabel" [title]="queueAddActiveLabel" (click)="queueAddActive.emit()">
2562
+ <ngxsp-smart-icon name="playlist-plus" />
2563
+ </button>
2564
+ <button class="ngxsp-yt-playerAction" type="button" [attr.aria-label]="shareLabel" [title]="shareLabel" (click)="toolSelect.emit('share')">
2565
+ <ngxsp-smart-icon name="share" />
2566
+ </button>
2567
+ }
2568
+ @if (compactMode === 'mini') {
2569
+ <button class="ngxsp-yt-playerAction" type="button" [attr.aria-label]="compactRestoreLabel" [title]="compactRestoreLabel" (click)="compactModeChange.emit('full')">
2570
+ <ngxsp-smart-icon name="window-restore" />
2571
+ </button>
2572
+ } @else if (compactModes) {
2573
+ <button class="ngxsp-yt-playerAction" type="button" [attr.aria-label]="compactMiniLabel" [title]="compactMiniLabel" (click)="compactModeChange.emit('mini')">
2574
+ <ngxsp-smart-icon name="window-mini" />
2575
+ </button>
2576
+ }
2577
+ <button class="ngxsp-yt-playerAction" type="button" [class.active]="cinemaMode" [attr.aria-pressed]="cinemaMode" [attr.aria-label]="cinemaButtonLabel" [title]="cinemaButtonLabel" (click)="toggleCinema.emit()">
2578
+ <ngxsp-smart-icon [name]="cinemaMode ? 'cinema-active' : 'cinema'" />
2579
+ </button>
2580
+ <button class="ngxsp-yt-playerAction ngxsp-yt-queueButton" type="button" [class.active]="queuePanelActive" [attr.aria-expanded]="queuePanelActive" [attr.aria-label]="queueActionLabel" [title]="queueActionLabel" (click)="onQueueAction()">
2581
+ <ngxsp-smart-icon name="panel-right" />
2582
+ </button>
2583
+ @if (infoTabs.length) {
2584
+ <button class="ngxsp-yt-playerAction ngxsp-yt-infoButton" type="button" [class.active]="!!infoPanel" [attr.aria-pressed]="!!infoPanel" [attr.aria-label]="infoLabel" [title]="infoLabel" (click)="infoToggle.emit()">
2585
+ <ngxsp-smart-icon name="info" />
2586
+ </button>
2587
+ }
2588
+ </div>
2589
+ </ngx-smart-player>
2590
+ } @else {
2591
+ <div class="ngxsp-yt-emptyPlayer">
2592
+ <button class="ngxsp-yt-bigPlay" type="button" [attr.aria-label]="primarySourceLabel" (click)="isOfflineMode ? videoFileInput.click() : onPrimarySourceAction()">
2593
+ <ngxsp-smart-icon [name]="primarySourceIcon" />
2594
+ </button>
2595
+ <span>{{ emptyKicker }}</span>
2596
+ <strong>{{ emptyTitle }}</strong>
2597
+ <p>{{ emptyBody }}</p>
2598
+ </div>
2599
+ }
2600
+ @if (resumePromptShown && resumeOffer && config && !showPosterPreview) {
2601
+ <div class="ngxsp-yt-resumePrompt" role="group" [attr.aria-label]="resumeOffer.message">
2602
+ <span>{{ resumeOffer.message }}</span>
2603
+ <button class="primary" type="button" (click)="chooseResume('resume')">{{ resumeOffer.resumeLabel }}</button>
2604
+ <button type="button" (click)="chooseResume('restart')">{{ resumeOffer.restartLabel }}</button>
2605
+ </div>
2606
+ }
2607
+ @if (infoPanel; as panel) {
2608
+ <!-- Info panels float over the right of the picture; on phones they open in the sheet instead. -->
2609
+ <section class="ngxsp-yt-infoOverlay" [attr.aria-label]="panel.title">
2610
+ <div class="ngxsp-yt-infoTabs" role="tablist" [attr.aria-label]="infoLabel">
2611
+ @for (tab of infoTabs; track tab.id) {
2612
+ <button type="button" role="tab" [class.active]="tab.id === panel.id" [attr.aria-selected]="tab.id === panel.id" [title]="tab.label" (click)="infoTabSelect.emit(tab.id)">
2613
+ <ngxsp-smart-icon [name]="tab.icon" />
2614
+ <span>{{ tab.label }}</span>
2615
+ </button>
2616
+ }
2617
+ <button class="ngxsp-yt-infoClose" type="button" [attr.aria-label]="inspectorCloseLabel" [title]="inspectorCloseLabel" (click)="closeInspector.emit()">
2618
+ <ngxsp-smart-icon name="close" />
2619
+ </button>
2620
+ </div>
2621
+ <div class="ngxsp-yt-infoHead" role="tabpanel">
2622
+ <strong>{{ panel.title }}</strong>
2623
+ @if (panel.subtitle) { <small>{{ panel.subtitle }}</small> }
2624
+ </div>
2625
+ <ng-container *ngTemplateOutlet="panelBody; context: { $implicit: panel }" />
2626
+ </section>
2627
+ }
2628
+ @if (showPosterPreview || !config) {
2629
+ <section class="ngxsp-yt-transportBar" [attr.aria-label]="transportLabel">
2630
+ <div class="ngxsp-yt-transportCluster primary">
2631
+ <button class="ngxsp-yt-transportButton" type="button" [disabled]="!canPrev" [attr.aria-label]="previousLabel" [title]="previousLabel" (click)="prev.emit()">
2632
+ <ngxsp-smart-icon name="prev" />
2633
+ </button>
2634
+ <button class="ngxsp-yt-transportPlay" type="button" [attr.aria-label]="isPlaying ? pauseLabel : playLabel" [title]="isPlaying ? pauseLabel : playLabel" (click)="togglePrimaryPlayback()">
2635
+ @if (isPlaying) { <ngxsp-smart-icon name="pause" /> } @else { <ngxsp-smart-icon name="play" /> }
2636
+ </button>
2637
+ <button class="ngxsp-yt-transportButton" type="button" [disabled]="!canNext" [attr.aria-label]="nextLabel" [title]="nextLabel" (click)="next.emit()">
2638
+ <ngxsp-smart-icon name="next" />
2639
+ </button>
2640
+ </div>
2641
+ <div class="ngxsp-yt-transportInfo" aria-hidden="true">
2642
+ <span>{{ heroEyebrow }}</span>
2643
+ <strong>{{ displayHeroTitle }}</strong>
2644
+ </div>
2645
+ <div class="ngxsp-yt-transportCluster secondary">
2646
+ <button class="ngxsp-yt-transportButton" type="button" [class.active]="repeatMode !== 'off'" [attr.aria-pressed]="repeatMode !== 'off'" [attr.aria-label]="repeatLabel" [title]="repeatLabel" (click)="cycleRepeat.emit()">
2647
+ <ngxsp-smart-icon [name]="repeatMode === 'one' ? 'repeat-one' : 'repeat'" />
2648
+ </button>
2649
+ <ngxsp-volume-control
2650
+ class="ngxsp-yt-volumeControl"
2651
+ [volume]="outputVolume / 100"
2652
+ [step]="0.05"
2653
+ [volumeLabel]="outputVolumeLabel"
2654
+ [muteLabel]="outputMuteLabel"
2655
+ [unmuteLabel]="outputUnmuteLabel"
2656
+ (muteToggle)="outputMuteToggle.emit()"
2657
+ (volumeChange)="onOutputVolumeRatioChange($event)"
2658
+ />
2659
+ <button class="ngxsp-yt-transportButton ngxsp-yt-transportCinema" type="button" [class.active]="cinemaMode" [attr.aria-pressed]="cinemaMode" [attr.aria-label]="cinemaButtonLabel" [title]="cinemaButtonLabel" (click)="toggleCinema.emit()">
2660
+ <ngxsp-smart-icon [name]="cinemaMode ? 'cinema-active' : 'cinema'" />
2661
+ </button>
2662
+ <button class="ngxsp-yt-transportButton ngxsp-yt-queueButton" type="button" [class.active]="queuePanelActive" [attr.aria-expanded]="queuePanelActive" [attr.aria-label]="queueActionLabel" [title]="queueActionLabel" (click)="onQueueAction()">
2663
+ <ngxsp-smart-icon name="panel-right" />
2664
+ </button>
2665
+ @if (compactMode === 'mini') {
2666
+ <button class="ngxsp-yt-transportButton" type="button" [attr.aria-label]="compactRestoreLabel" [title]="compactRestoreLabel" (click)="compactModeChange.emit('full')">
2667
+ <ngxsp-smart-icon name="window-restore" />
2668
+ </button>
2669
+ } @else if (compactModes) {
2670
+ <button class="ngxsp-yt-transportButton" type="button" [attr.aria-label]="compactMiniLabel" [title]="compactMiniLabel" (click)="compactModeChange.emit('mini')">
2671
+ <ngxsp-smart-icon name="window-mini" />
2672
+ </button>
2673
+ }
2674
+ </div>
2675
+ </section>
2676
+ }
2677
+ </div>
2678
+ </section>
2679
+ } @else {
2680
+
2681
+ <!-- What is being watched. A home view opens on where videos come from instead. -->
2682
+ @if (showActiveMedia) {
2683
+ <section class="ngxsp-yt-metaBar">
2684
+ <div class="ngxsp-yt-titleGroup" [attr.aria-label]="displayHeroTitle + ' - ' + displayHeroSubtitle">
2685
+ <h1>{{ displayHeroTitle }}</h1>
2686
+ @if (displayHeroSubtitle) { <p>{{ displayHeroSubtitle }}</p> }
2687
+ @if (displayHeroMeta) { <small>{{ displayHeroMeta }}</small> }
2688
+ </div>
2689
+ <div class="ngxsp-yt-watchActions">
2690
+ <span class="ngxsp-yt-rating">
2691
+ <button type="button" [class.active]="likeActive" [attr.aria-pressed]="likeActive" [attr.aria-label]="likeLabel" [title]="likeLabel" (click)="toolSelect.emit('like')"><ngxsp-smart-icon name="thumb-up" /><span>{{ likeLabel }}</span></button>
2692
+ <button type="button" [attr.aria-label]="dislikeLabel" [title]="dislikeLabel" (click)="toolSelect.emit('dislike')"><ngxsp-smart-icon name="thumb-down" /><span>{{ dislikeLabel }}</span></button>
2693
+ </span>
2694
+ <button type="button" [attr.aria-label]="shareLabel" [title]="shareLabel" (click)="toolSelect.emit('share')"><ngxsp-smart-icon name="share" /><span>{{ shareLabel }}</span></button>
2695
+ <button type="button" [class.active]="watchLaterActive" [attr.aria-pressed]="watchLaterActive" [attr.aria-label]="watchLaterLabel" [title]="watchLaterLabel" (click)="watchLaterToggle.emit()"><ngxsp-smart-icon name="clock" /><span>{{ watchLaterLabel }}</span></button>
2696
+ <button type="button" [attr.aria-label]="queueAddActiveLabel" [title]="queueAddActiveLabel" (click)="queueAddActive.emit()"><ngxsp-smart-icon name="playlist-plus" /><span>{{ queueAddActiveShortLabel }}</span></button>
2697
+ @if (revealFileLabel) {
2698
+ <button type="button" [attr.aria-label]="revealFileLabel" [title]="revealFileLabel" (click)="revealFile.emit()"><ngxsp-smart-icon name="folder" /><span>{{ revealFileLabel }}</span></button>
2699
+ }
2700
+ <button type="button" class="ngxsp-yt-moreAction" [attr.aria-label]="moreLabel" [title]="moreLabel" (click)="toolSelect.emit('more')"><ngxsp-smart-icon name="more" /><span>{{ moreShortLabel }}</span></button>
2701
+ </div>
2702
+ </section>
2703
+ }
2704
+
2705
+ <!-- Phones: the queue is a sheet, and this row is the one place that opens it. -->
2706
+ <button class="ngxsp-yt-queueButton ngxsp-yt-queueOpen" type="button" [attr.aria-expanded]="queueSheetOpen" [title]="queueActionLabel" (click)="onQueueAction()">
2707
+ <ngxsp-smart-icon name="panel-right" />
2708
+ <span>{{ sidePanelTitle }}</span>
2709
+ <small>{{ sidePanelCount }}</small>
2710
+ </button>
2711
+
2712
+ @if (toolCards.length) {
2713
+ <section class="ngxsp-yt-toolRow">
2714
+ @for (tool of toolCards; track tool.id) {
2715
+ <button class="ngxsp-yt-tool" type="button" [class.active]="tool.active" [attr.data-tool]="tool.id" [attr.data-tone]="tool.tone || 'neutral'" (click)="toolSelect.emit(tool.id)">
2716
+ <span class="ngxsp-yt-toolIcon" aria-hidden="true"><ngxsp-smart-icon [name]="tool.icon" /></span>
2717
+ <span class="ngxsp-yt-toolCopy"><small>{{ tool.label }}</small><strong>{{ tool.value }}</strong></span>
2718
+ </button>
2719
+ }
2720
+ </section>
2721
+ }
2722
+
2723
+ @if (hasSearchQuery) {
2724
+ <section class="ngxsp-yt-searchWorkspace" [attr.aria-label]="searchResultsTitle">
2725
+ <div class="ngxsp-yt-searchHead">
2726
+ <div>
2727
+ <strong>{{ searchResultsTitle }}</strong>
2728
+ @if (searchResultsSummary) { <small>{{ searchResultsSummary }}</small> }
2729
+ </div>
2730
+ <button type="button" [attr.aria-label]="searchClearLabel" [title]="searchClearLabel" (click)="clearSearch()">
2731
+ <ngxsp-smart-icon name="close" />
2732
+ </button>
2733
+ </div>
2734
+ @if (searchFilters.length) {
2735
+ <div class="ngxsp-yt-searchFilters" role="group" [attr.aria-label]="searchFilterLabel">
2736
+ @for (filter of searchFilters; track filter.id) {
2737
+ <button type="button" [class.active]="filter.active" [disabled]="filter.disabled" [attr.aria-pressed]="!!filter.active" (click)="selectSearchFilter(filter)">
2738
+ <span>{{ filter.label }}</span>
2739
+ @if (filter.count !== undefined) { <small>{{ filter.count }}</small> }
2740
+ </button>
2741
+ }
2742
+ </div>
2743
+ }
2744
+ <div class="ngxsp-yt-searchResults" [class.list]="activeViewMode === 'list'">
2745
+ @for (card of searchResultItems; track card.sectionId + ':' + card.item.id) {
2746
+ <article class="ngxsp-yt-searchResult" [class.active]="card.item.active" [attr.data-art]="artVariant(card)">
2747
+ <button class="ngxsp-yt-searchResultMain" type="button" [attr.aria-current]="card.item.active ? 'true' : null" [attr.aria-label]="searchResultActionLabel('play', card)" [title]="searchResultActionLabel('play', card)" (click)="selectCard(card)">
2748
+ <span class="ngxsp-yt-searchThumb" [class.has-image]="!!card.item.artworkUrl">
2749
+ @if (card.item.artworkUrl) {
2750
+ <img [src]="card.item.artworkUrl" alt="" />
2751
+ } @else {
2752
+ <span class="ngxsp-yt-art" aria-hidden="true"><ngxsp-smart-icon name="video" /></span>
2753
+ }
2754
+ @if (card.item.duration && !card.item.artworkUrl) { <small>{{ card.item.duration }}</small> }
2755
+ </span>
2756
+ <span class="ngxsp-yt-searchCopy">
2757
+ @if (card.item.kindLabel || card.item.episodeLabel) { <small>{{ card.item.episodeLabel || card.item.kindLabel }}</small> }
2758
+ <strong>{{ card.item.title }}</strong>
2759
+ @if (card.item.subtitle) { <em>{{ card.item.subtitle }}</em> }
2760
+ @if (mediaCardMeta(card.item)) { <span>{{ mediaCardMeta(card.item) }}</span> }
2761
+ </span>
2762
+ </button>
2763
+ <span class="ngxsp-yt-searchActions">
2764
+ <button type="button" [attr.aria-label]="searchResultActionLabel('queue', card)" [title]="searchResultActionLabel('queue', card)" (click)="searchResultQueueAdd.emit(card.item.id)">
2765
+ <ngxsp-smart-icon name="playlist-plus" />
2766
+ </button>
2767
+ <button type="button" [attr.aria-label]="searchResultActionLabel('watchLater', card)" [title]="searchResultActionLabel('watchLater', card)" (click)="searchResultWatchLater.emit(card.item.id)">
2768
+ <ngxsp-smart-icon name="clock" />
2769
+ </button>
2770
+ <button type="button" [attr.aria-label]="searchResultActionLabel('favorite', card)" [title]="searchResultActionLabel('favorite', card)" (click)="searchResultFavorite.emit(card.item.id)">
2771
+ <ngxsp-smart-icon name="heart" />
2772
+ </button>
2773
+ <button type="button" [attr.aria-label]="searchResultActionLabel('download', card)" [title]="searchResultActionLabel('download', card)" (click)="searchResultDownload.emit(card.item.id)">
2774
+ <ngxsp-smart-icon name="download" />
2775
+ </button>
2776
+ </span>
2777
+ </article>
2778
+ } @empty {
2779
+ <div class="ngxsp-yt-searchEmpty">
2780
+ <ngxsp-smart-icon name="search" />
2781
+ <strong>{{ searchResultsEmptyTitle }}</strong>
2782
+ <span>{{ searchResultsEmptyBody }}</span>
2783
+ </div>
2784
+ }
2785
+ </div>
2786
+ </section>
2787
+ }
2788
+
2789
+ @if (showActiveMedia) {
2790
+ @if (mediaDetail; as detail) {
2791
+ <section class="ngxsp-yt-mediaDetail" [attr.aria-label]="detail.title">
2792
+ <div class="ngxsp-yt-mediaHero" [class.echoesHeader]="detailEchoesHeader">
2793
+ <div class="ngxsp-yt-mediaPoster" [class.has-image]="!!detail.posterUrl">
2794
+ @if (detail.posterUrl) {
2795
+ <img [src]="detail.posterUrl" alt="" />
2796
+ } @else {
2797
+ <span>{{ posterInitial(detail.title) }}</span>
2798
+ }
2799
+ </div>
2800
+ <div class="ngxsp-yt-mediaDetailBody">
2801
+ <span class="ngxsp-yt-mediaKicker">{{ detail.kicker || detail.kindLabel || heroEyebrow }}</span>
2802
+ <h2>{{ detail.title }}</h2>
2803
+ @if (detail.subtitle) { <p class="ngxsp-yt-mediaSubtitle">{{ detail.subtitle }}</p> }
2804
+ @if (detail.metaItems?.length) {
2805
+ <div class="ngxsp-yt-mediaMeta">
2806
+ @for (meta of detail.metaItems; track meta) {
2807
+ <span>{{ meta }}</span>
2808
+ }
2809
+ </div>
2810
+ }
2811
+ @if (detail.summary) { <p class="ngxsp-yt-mediaSummary">{{ detail.summary }}</p> }
2812
+ @if (showResumeProgress && detail.progressPercent !== undefined) {
2813
+ <div class="ngxsp-yt-mediaProgress">
2814
+ <span><i [style.width.%]="safeProgressPercent(detail.progressPercent)"></i></span>
2815
+ @if (detail.progressLabel) { <small>{{ detail.progressLabel }}</small> }
2816
+ </div>
2817
+ }
2818
+ @if (detail.tags?.length) {
2819
+ <div class="ngxsp-yt-mediaTags">
2820
+ @for (tag of detail.tags; track tag) {
2821
+ <span>{{ tag }}</span>
2822
+ }
2823
+ </div>
2824
+ }
2825
+ @if (detail.actions?.length) {
2826
+ <div class="ngxsp-yt-mediaActions">
2827
+ @for (action of detail.actions; track action.id) {
2828
+ <button type="button" [class.primary]="action.primary" [disabled]="action.disabled" (click)="detailAction.emit(action.id)">
2829
+ <ngxsp-smart-icon [name]="action.icon" />
2830
+ <span>{{ action.label }}</span>
2831
+ </button>
2832
+ }
2833
+ </div>
2834
+ }
2835
+ </div>
2836
+ </div>
2837
+ @if (detail.chapters?.length || detail.checklist?.length) {
2838
+ <div class="ngxsp-yt-mediaWorkspace">
2839
+ @if (detail.workspaceTitle || detail.workspaceSummary) {
2840
+ <div class="ngxsp-yt-mediaWorkspaceHead">
2841
+ @if (detail.workspaceTitle) { <strong>{{ detail.workspaceTitle }}</strong> }
2842
+ @if (detail.workspaceSummary) { <small>{{ detail.workspaceSummary }}</small> }
2843
+ </div>
2844
+ }
2845
+ <div class="ngxsp-yt-mediaWorkspaceGrid">
2846
+ @if (detail.chapters?.length) {
2847
+ <div class="ngxsp-yt-chapterPanel">
2848
+ @if (detail.chaptersTitle) { <strong class="ngxsp-yt-panelTitle">{{ detail.chaptersTitle }}</strong> }
2849
+ <div class="ngxsp-yt-chapterList">
2850
+ @for (chapter of detail.chapters; track chapter.id) {
2851
+ <button type="button" class="ngxsp-yt-mediaChapter" [class.active]="chapter.active" (click)="selectMediaChapter(chapter)">
2852
+ <span class="ngxsp-yt-chapterTime">
2853
+ @if (chapter.time) { <em>{{ chapter.time }}</em> }
2854
+ @if (chapter.label) { <b>{{ chapter.label }}</b> }
2855
+ </span>
2856
+ <span class="ngxsp-yt-chapterCopy">
2857
+ <strong>{{ chapter.title }}</strong>
2858
+ @if (chapter.summary) { <small>{{ chapter.summary }}</small> }
2859
+ </span>
2860
+ </button>
2861
+ }
2862
+ </div>
2863
+ </div>
2864
+ }
2865
+ @if (detail.checklist?.length) {
2866
+ <div class="ngxsp-yt-checkPanel">
2867
+ @if (detail.checklistTitle) { <strong class="ngxsp-yt-panelTitle">{{ detail.checklistTitle }}</strong> }
2868
+ <div class="ngxsp-yt-checkList">
2869
+ @for (item of detail.checklist; track item.id) {
2870
+ <div class="ngxsp-yt-checkItem" [attr.data-tone]="item.tone || 'neutral'">
2871
+ <span aria-hidden="true"></span>
2872
+ <div>
2873
+ <strong>{{ item.label }}</strong>
2874
+ @if (item.description) { <small>{{ item.description }}</small> }
2875
+ </div>
2876
+ @if (item.value) { <em>{{ item.value }}</em> }
2877
+ </div>
2878
+ }
2879
+ </div>
2880
+ </div>
2881
+ }
2882
+ </div>
2883
+ </div>
2884
+ }
2885
+ @if (detail.episodes?.length) {
2886
+ <div class="ngxsp-yt-episodeRail">
2887
+ @if (detail.episodesTitle) { <strong class="ngxsp-yt-panelTitle">{{ detail.episodesTitle }}</strong> }
2888
+ <div>
2889
+ @for (episode of detail.episodes; track episode.id) {
2890
+ <button type="button" [class.active]="episode.active" (click)="selectDetailEpisode.emit(episode.id)">
2891
+ <span class="ngxsp-yt-episodeThumb" [class.has-image]="!!episode.artworkUrl">
2892
+ @if (episode.artworkUrl) {
2893
+ <img [src]="episode.artworkUrl" alt="" />
2894
+ } @else {
2895
+ <i>{{ posterInitial(episode.title) }}</i>
2896
+ }
2897
+ @if (showResumeProgress && episode.progressPercent !== undefined) {
2898
+ <em><i [style.width.%]="safeProgressPercent(episode.progressPercent)"></i></em>
2899
+ }
2900
+ </span>
2901
+ <span>
2902
+ <strong>{{ episode.title }}</strong>
2903
+ @if (episode.subtitle) { <small>{{ episode.subtitle }}</small> }
2904
+ @if (episode.duration) { <em>{{ episode.duration }}</em> }
2905
+ </span>
2906
+ </button>
2907
+ }
2908
+ </div>
2909
+ </div>
2910
+ }
2911
+ </section>
2912
+ }
2913
+ }
2914
+
2915
+ @if (libraryVisible) {
2916
+ <section class="ngxsp-yt-library">
2917
+ <div class="ngxsp-yt-sectionHead">
2918
+ <div>
2919
+ <strong>{{ libraryTitle }}</strong>
2920
+ @if (libraryStatus) {
2921
+ <small class="ngxsp-yt-libraryStatus" [class.busy]="libraryBusy" role="status" aria-live="polite">{{ libraryStatus }}</small>
2922
+ }
2923
+ </div>
2924
+ <button class="ngxsp-yt-sectionLink" type="button" [attr.aria-label]="browseLibraryLabel" [title]="browseLibraryLabel" (click)="toggleLibrary.emit()">
2925
+ <ngxsp-smart-icon name="chevron-up" />
2926
+ </button>
2927
+ </div>
2928
+ @if (trail.length > 1) {
2929
+ <nav class="ngxsp-yt-trail" [attr.aria-label]="trailLabel">
2930
+ @for (step of trail; track step.id; let last = $last) {
2931
+ @if (last) {
2932
+ <b aria-current="page">{{ step.label }}</b>
2933
+ } @else {
2934
+ <button class="ngxsp-yt-trailStep" type="button" (click)="trailStep.emit(step.id)">{{ step.label }}</button>
2935
+ <span aria-hidden="true">/</span>
2936
+ }
2937
+ }
2938
+ </nav>
2939
+ }
2940
+ @for (shelf of librarySections; track shelf.id) {
2941
+ <div class="ngxsp-yt-shelf">
2942
+ @if (shelf.titled) {
2943
+ <div class="ngxsp-yt-shelfHead">
2944
+ <strong>{{ shelf.title }}</strong>
2945
+ <small>{{ shelf.count }}</small>
2946
+ @if (shelf.hidden) {
2947
+ <button class="ngxsp-yt-shelfAll" type="button" [attr.aria-expanded]="isSectionExpanded(shelf.id)" (click)="toggleSection(shelf.id)">{{ isSectionExpanded(shelf.id) ? showLessLabel : showAllLabel }}</button>
2948
+ }
2949
+ </div>
2950
+ }
2951
+ <div class="ngxsp-yt-grid" [class.list]="activeViewMode === 'list'" [class.rail]="activeViewMode !== 'list' && !isSectionExpanded(shelf.id)">
2952
+ @for (card of shelf.cards; track card.sectionId + ':' + card.item.id) {
2953
+ <ng-container *ngTemplateOutlet="mediaCard; context: { $implicit: card }" />
2954
+ }
2955
+ </div>
2956
+ </div>
2957
+ }
2958
+ </section>
2959
+ }
2960
+ }
2961
+ </section>
2962
+
2963
+ <ng-template #mediaCard let-card>
2964
+ <span class="ngxsp-yt-cardWrap">
2965
+ <button class="ngxsp-yt-card" type="button" [class.active]="card.item.active" [class.synthetic]="isSyntheticSection(card.sectionId)" [attr.aria-current]="card.item.active ? 'true' : null" [attr.data-art]="artVariant(card)" (click)="selectCard(card)" (contextmenu)="openCardMenu($event, card.item)">
2966
+ <span class="ngxsp-yt-thumbnail" [class.has-image]="!!card.item.artworkUrl">
2967
+ @if (card.item.artworkUrl) {
2968
+ <img [src]="card.item.artworkUrl" alt="" />
2969
+ } @else {
2970
+ <span class="ngxsp-yt-art" aria-hidden="true"><ngxsp-smart-icon name="video" /></span>
2971
+ }
2972
+ @if (card.item.episodeLabel || card.item.kindLabel) {
2973
+ <small class="ngxsp-yt-thumbKind">{{ card.item.episodeLabel || card.item.kindLabel }}</small>
2974
+ }
2975
+ @if (!card.item.artworkUrl) {
2976
+ <small class="ngxsp-yt-thumbBadge">{{ card.item.duration || card.item.meta || sourceTypeLabel }}</small>
2977
+ }
2978
+ @if (card.item.active) {
2979
+ <span class="ngxsp-yt-nowBadge" aria-hidden="true"><ngxsp-smart-icon [name]="isPlaying ? 'waveform' : 'play'" /></span>
2980
+ }
2981
+ @if (showResumeProgress && card.item.progressPercent !== undefined) {
2982
+ <span class="ngxsp-yt-cardProgress" aria-hidden="true"><i [style.width.%]="safeProgressPercent(card.item.progressPercent)"></i></span>
2983
+ }
2984
+ </span>
2985
+ <span class="ngxsp-yt-cardCopy">
2986
+ <strong>{{ card.item.title }}</strong>
2987
+ @if (card.item.subtitle) { <span>{{ card.item.subtitle }}</span> }
2988
+ @if (mediaCardMeta(card.item)) { <small>{{ mediaCardMeta(card.item) }}</small> }
2989
+ </span>
2990
+ </button>
2991
+ @if (card.item.actions?.length) {
2992
+ <button
2993
+ class="ngxsp-yt-cardMore"
2994
+ type="button"
2995
+ [attr.aria-label]="cardActionsLabel"
2996
+ [attr.aria-expanded]="cardMenu?.card?.id === card.item.id"
2997
+ [title]="cardActionsLabel"
2998
+ (click)="openCardMenu($event, card.item)"
2999
+ >
3000
+ <ngxsp-smart-icon name="more" />
3001
+ </button>
3002
+ }
3003
+ </span>
3004
+ </ng-template>
3005
+
3006
+ <!-- What a panel shows: in the side column, or over the video for the info panels. -->
3007
+ <ng-template #panelBody let-panel>
3008
+ <div class="ngxsp-yt-inspectorScroll">
3009
+ @if (panel.status) {
3010
+ <p class="ngxsp-yt-inspectorStatus">{{ panel.status }}</p>
3011
+ }
3012
+
3013
+ @if (panel.metrics?.length) {
3014
+ <div class="ngxsp-yt-inspectorMetrics">
3015
+ @for (metric of panel.metrics; track metric.label) {
3016
+ <span class="ngxsp-yt-inspectorMetric" [class.success]="metric.tone === 'success'" [class.warning]="metric.tone === 'warning'">
3017
+ <small>{{ metric.label }}</small>
3018
+ <strong>{{ metric.value }}</strong>
3019
+ </span>
3020
+ }
3021
+ </div>
3022
+ }
3023
+
3024
+ @if (panel.settings?.length) {
3025
+ <ngxsp-settings [sections]="panel.settings!" (settingChange)="settingChange.emit($event)" (settingAction)="settingAction.emit($event)" />
3026
+ }
3027
+
3028
+ @if (serverForm; as form) {
3029
+ <section class="ngxsp-yt-servers" [attr.aria-label]="form.heading || form.title">
3030
+ @if (form.heading) { <h2 class="ngxsp-yt-serversHeading">{{ form.heading }}</h2> }
3031
+ @for (server of form.servers; track server.id) {
3032
+ <div class="ngxsp-yt-serverRow" [class.active]="server.active">
3033
+ <button class="ngxsp-yt-serverMain" type="button" [title]="form.openLabel + ' - ' + server.name" (click)="selectServer.emit(server.id)">
3034
+ <ngxsp-smart-icon name="network" />
3035
+ <span>
3036
+ <strong>{{ server.name }}</strong>
3037
+ @if (server.subtitle) { <small>{{ server.subtitle }}</small> }
3038
+ </span>
3039
+ </button>
3040
+ <button class="ngxsp-yt-serverDisconnect" type="button" [attr.aria-label]="form.disconnectLabel + ' - ' + server.name" [title]="form.disconnectLabel" (click)="disconnectServer.emit(server.id)">
3041
+ <ngxsp-smart-icon name="close" />
3042
+ </button>
3043
+ </div>
3044
+ }
3045
+ <form class="ngxsp-yt-serverForm" (submit)="submitServer($event)">
3046
+ <span class="ngxsp-yt-serverTitle">{{ form.title }}</span>
3047
+ @if (form.kinds && form.kinds.length > 1) {
3048
+ <select class="ngxsp-yt-serverKind" [value]="serverKindDraft || form.kinds[0].id" (change)="onServerKindChange($event)" [attr.aria-label]="form.kindLabel || form.title">
3049
+ @for (kind of form.kinds; track kind.id) {
3050
+ <option [value]="kind.id">{{ kind.label }}</option>
3051
+ }
3052
+ </select>
3053
+ }
3054
+ @let serverKind = selectedServerKind(form);
3055
+ <input type="text" inputmode="url" [value]="serverAddressDraft" (input)="onServerAddressInput($event)" [placeholder]="serverKind?.addressPlaceholder || form.addressPlaceholder" [attr.aria-label]="serverKind?.addressPlaceholder || form.addressPlaceholder" />
3056
+ @if (serverKind?.credentials !== false) {
3057
+ <input type="text" autocomplete="username" [value]="serverUsernameDraft" (input)="onServerUsernameInput($event)" [placeholder]="form.usernamePlaceholder" [attr.aria-label]="form.usernamePlaceholder" />
3058
+ <input type="password" autocomplete="current-password" [value]="serverPasswordDraft" (input)="onServerPasswordInput($event)" [placeholder]="form.passwordPlaceholder" [attr.aria-label]="form.passwordPlaceholder" />
3059
+ }
3060
+ <button type="submit" [disabled]="form.busy || !serverAddressDraft.trim()">{{ form.submitLabel }}</button>
3061
+ @if (form.discovery && serverKind?.discoverable) {
3062
+ <button class="secondary" type="button" [disabled]="form.discovery.busy" (click)="discoverServers.emit(serverKind!.id)">{{ form.discovery.busy ? form.discovery.searchingLabel : form.discovery.searchLabel }}</button>
3063
+ }
3064
+ @if (form.hint) { <small class="ngxsp-yt-serverHint">{{ form.hint }}</small> }
3065
+ @if (form.discovery && serverKind?.discoverable && (form.discovery.servers.length || form.discovery.emptyLabel)) {
3066
+ <ul class="ngxsp-yt-discovered" [attr.aria-label]="form.discovery.searchLabel">
3067
+ @for (server of form.discovery.servers; track server.id) {
3068
+ <li>
3069
+ <span><strong>{{ server.name }}</strong>@if (server.subtitle) { <small>{{ server.subtitle }}</small> }</span>
3070
+ <button class="secondary" type="button" [disabled]="form.busy" [attr.aria-label]="form.discovery.connectLabel + ' - ' + server.name" (click)="connectDiscoveredServer.emit(server.id)">{{ form.discovery.connectLabel }}</button>
3071
+ </li>
3072
+ } @empty {
3073
+ <li class="empty">{{ form.discovery.emptyLabel }}</li>
3074
+ }
3075
+ </ul>
3076
+ }
3077
+ </form>
3078
+ </section>
3079
+ }
3080
+
3081
+ @if (panel.trailingSettings?.length) {
3082
+ <ngxsp-settings [sections]="panel.trailingSettings!" (settingChange)="settingChange.emit($event)" (settingAction)="settingAction.emit($event)" />
3083
+ }
3084
+
3085
+ @if (panel.items?.length) {
3086
+ <div class="ngxsp-yt-inspectorList">
3087
+ @for (item of panel.items; track item.label) {
3088
+ <div class="ngxsp-yt-inspectorItem" [class.success]="item.tone === 'success'" [class.warning]="item.tone === 'warning'">
3089
+ <span>
3090
+ <strong>{{ item.label }}</strong>
3091
+ @if (item.description) { <small>{{ item.description }}</small> }
3092
+ </span>
3093
+ @if (item.value) { <em>{{ item.value }}</em> }
3094
+ </div>
3095
+ }
3096
+ </div>
3097
+ }
3098
+ </div>
3099
+
3100
+ @if (panel.actions?.length) {
3101
+ <div class="ngxsp-yt-inspectorActions">
3102
+ @for (action of panel.actions; track action.id) {
3103
+ <button type="button" [class.primary]="action.primary" [disabled]="action.disabled" (click)="inspectorAction.emit(action.id)">
3104
+ <ngxsp-smart-icon [name]="action.icon" />
3105
+ <span>{{ action.label }}</span>
3106
+ </button>
3107
+ }
3108
+ </div>
3109
+ }
3110
+ </ng-template>
3111
+
3112
+ <!-- A row of the side panel: a video of the queue or of the section on show. -->
3113
+ <ng-template #sideRow let-card let-label="label">
3114
+ <button class="ngxsp-yt-suggestionMain" type="button" [attr.aria-current]="card.item.active ? 'true' : null" [attr.aria-label]="label" [title]="label" (click)="selectCard(card)">
3115
+ <span class="ngxsp-yt-suggestionThumb" [class.has-image]="!!card.item.artworkUrl">
3116
+ @if (card.item.artworkUrl) {
3117
+ <img [src]="card.item.artworkUrl" alt="" />
3118
+ } @else {
3119
+ <span class="ngxsp-yt-art" aria-hidden="true"><ngxsp-smart-icon name="video" /></span>
3120
+ }
3121
+ @if (card.item.duration && !card.item.artworkUrl) { <small>{{ card.item.duration }}</small> }
3122
+ @if (card.item.active) {
3123
+ <span class="ngxsp-yt-nowBadge" aria-hidden="true"><ngxsp-smart-icon [name]="isPlaying ? 'waveform' : 'play'" /></span>
3124
+ }
3125
+ @if (showResumeProgress && card.item.progressPercent !== undefined) {
3126
+ <span class="ngxsp-yt-cardProgress" aria-hidden="true"><i [style.width.%]="safeProgressPercent(card.item.progressPercent)"></i></span>
3127
+ }
3128
+ </span>
3129
+ <span class="ngxsp-yt-suggestionCopy">
3130
+ <strong>{{ card.item.title }}</strong>
3131
+ @if (card.item.subtitle) { <span>{{ card.item.subtitle }}</span> }
3132
+ @if (mediaCardMeta(card.item)) { <small>{{ mediaCardMeta(card.item) }}</small> }
3133
+ </span>
3134
+ </button>
3135
+ </ng-template>
3136
+
3137
+ @if (queueVisible || queueSheetOpen) {
3138
+ <aside
3139
+ class="ngxsp-yt-suggestions"
3140
+ #queuePanel
3141
+ tabindex="-1"
3142
+ [class.sheet]="queueSheetOpen"
3143
+ [attr.role]="queueSheetOpen ? 'dialog' : null"
3144
+ [attr.aria-modal]="queueSheetOpen ? 'true' : null"
3145
+ [attr.aria-label]="sidePanelTitle"
3146
+ (keydown.escape)="closeQueueSheet()"
3147
+ >
3148
+ <!-- Phones only: the panel covers the page instead of pushing it down, and closes from here. -->
3149
+ @if (queueSheetOpen) {
3150
+ <div class="ngxsp-yt-sheetHeader">
3151
+ <strong>{{ sidePanel?.title || sidePanelTitle }}</strong>
3152
+ <button class="ngxsp-yt-textButton" type="button" (click)="closeQueueSheet()">{{ closeLabel }}</button>
3153
+ </div>
3154
+ }
3155
+ <div class="ngxsp-yt-panelTabs" role="group" [attr.aria-label]="sidePanelTitle">
3156
+ <button type="button" class="ngxsp-yt-panelPill" [class.active]="isPanelOpen('queue')" [attr.aria-pressed]="isPanelOpen('queue')" (click)="togglePanel('queue')">{{ sidePanelTitle }}</button>
3157
+ @for (panel of knownPanels; track panel.id) {
3158
+ <button type="button" class="ngxsp-yt-panelPill" [class.active]="isPanelOpen(panel.id)" [attr.aria-pressed]="isPanelOpen(panel.id)" (click)="togglePanel(panel.id)">{{ panel.title }}</button>
3159
+ }
3160
+ </div>
3161
+
3162
+ <div class="ngxsp-yt-panelStack">
3163
+ @if (isPanelOpen('queue')) {
3164
+ <section class="ngxsp-yt-stackItem" [class.collapsed]="isPanelCollapsed('queue')">
3165
+ <header class="ngxsp-yt-stackHead">
3166
+ <strong>{{ sidePanelTitle }}</strong>
3167
+ @if (sidePanelCount) { <small>{{ sidePanelCount }}</small> }
3168
+ <button class="ngxsp-yt-stackIcon" type="button" [attr.aria-expanded]="!isPanelCollapsed('queue')" [attr.aria-label]="sidePanelTitle" (click)="togglePanelCollapsed('queue')">
3169
+ <ngxsp-smart-icon [name]="isPanelCollapsed('queue') ? 'chevron-down' : 'chevron-up'" />
3170
+ </button>
3171
+ <button class="ngxsp-yt-stackIcon" type="button" [attr.aria-label]="closeLabel" [title]="closeLabel" (click)="togglePanel('queue')">
3172
+ <ngxsp-smart-icon name="close" />
3173
+ </button>
3174
+ </header>
3175
+ @if (!isPanelCollapsed('queue')) {
3176
+ <!-- A page with nothing to list and nothing to say about it (the lists block above speaks for it) shows no list. -->
3177
+ @let sideListShown = sidePanelContent !== 'sections' || !!sidePanelCount || !!emptyCatalogLabel;
3178
+ @if (sideListShown) {
3179
+ <div class="ngxsp-yt-sectionHead compact">
3180
+ <div>
3181
+ @if (sideBackLabel) {
3182
+ <button class="ngxsp-yt-backButton" type="button" [attr.aria-label]="sideBackLabel" [title]="sideBackLabel" (click)="sideBack.emit()">
3183
+ <ngxsp-smart-icon name="chevron-left" />
3184
+ <span>{{ sideBackLabel }}</span>
3185
+ </button>
3186
+ }
3187
+ <strong>{{ sidePanelTitle }}</strong>
3188
+ @if (sidePanelMeta) { <small>{{ sidePanelMeta }}</small> }
3189
+ </div>
3190
+ <span class="ngxsp-yt-countBadge">{{ sidePanelCount }}</span>
3191
+ </div>
3192
+ }
3193
+ @if (sidePanelContent === 'sections') {
3194
+ @if (sideListShown) {
3195
+ <div class="ngxsp-yt-suggestionList">
3196
+ @for (group of sideSectionGroups; track group.id) {
3197
+ @if (sideSectionGroups.length > 1 && group.cards.length) { <strong class="ngxsp-yt-sideGroupTitle">{{ group.title }}</strong> }
3198
+ @for (card of group.cards; track card.sectionId + ':' + card.item.id) {
3199
+ <article class="ngxsp-yt-suggestion" [class.active]="card.item.active" [attr.data-art]="artVariant(card)">
3200
+ <ng-container *ngTemplateOutlet="sideRow; context: { $implicit: card, label: playLabel + ' - ' + card.item.title }" />
3201
+ </article>
3202
+ }
3203
+ }
3204
+ @if (!sidePanelCount) {
3205
+ <div class="ngxsp-yt-emptyList">{{ emptyCatalogLabel }}</div>
3206
+ }
3207
+ </div>
3208
+ }
3209
+ } @else {
3210
+ <div class="ngxsp-yt-suggestionList" #queueList>
3211
+ @for (card of displayQueueItems; track card.sectionId + ':' + card.item.id) {
3212
+ <article class="ngxsp-yt-suggestion" [class.active]="card.item.active" [class.synthetic]="isSyntheticSection(card.sectionId)" [class.managed]="hasManagedQueue" [attr.data-art]="artVariant(card)">
3213
+ <ng-container *ngTemplateOutlet="sideRow; context: { $implicit: card, label: queueItemActionLabel('play', card) }" />
3214
+ @if (hasManagedQueue) {
3215
+ <span class="ngxsp-yt-queueControls">
3216
+ <button type="button" [disabled]="!canMoveQueueItem(card, 'up')" [attr.aria-label]="queueItemActionLabel('moveUp', card)" [title]="queueItemActionLabel('moveUp', card)" (click)="queueMoveUp.emit(card.item.id)">
3217
+ <ngxsp-smart-icon class="ngxsp-yt-queueUpIcon" name="chevron-up" />
3218
+ </button>
3219
+ <button type="button" [disabled]="!canMoveQueueItem(card, 'down')" [attr.aria-label]="queueItemActionLabel('moveDown', card)" [title]="queueItemActionLabel('moveDown', card)" (click)="queueMoveDown.emit(card.item.id)">
3220
+ <ngxsp-smart-icon class="ngxsp-yt-queueDownIcon" name="chevron-down" />
3221
+ </button>
3222
+ <button type="button" [attr.aria-label]="queueItemActionLabel('remove', card)" [title]="queueItemActionLabel('remove', card)" (click)="queueRemove.emit(card.item.id)">
3223
+ <ngxsp-smart-icon name="close" />
3224
+ </button>
3225
+ </span>
3226
+ }
3227
+ </article>
3228
+ } @empty {
3229
+ <div class="ngxsp-yt-emptyList">{{ emptyCatalogLabel }}</div>
3230
+ }
3231
+ </div>
3232
+ }
3233
+ }
3234
+ </section>
3235
+ }
3236
+ @for (panel of openPanels; track panel.id) {
3237
+ <section class="ngxsp-yt-stackItem" [class.collapsed]="isPanelCollapsed(panel.id)">
3238
+ <section class="ngxsp-yt-inspector" [attr.aria-label]="panel.title">
3239
+ <div class="ngxsp-yt-inspectorHead">
3240
+ <span class="ngxsp-yt-inspectorIcon" aria-hidden="true">
3241
+ <ngxsp-smart-icon [name]="panel.icon || 'settings'" />
3242
+ </span>
3243
+ <span class="ngxsp-yt-inspectorTitle">
3244
+ <strong>{{ panel.title }}</strong>
3245
+ @if (panel.subtitle) { <small>{{ panel.subtitle }}</small> }
3246
+ </span>
3247
+ <button class="ngxsp-yt-inspectorClose" type="button" [attr.aria-expanded]="!isPanelCollapsed(panel.id)" [attr.aria-label]="panel.title" [title]="panel.title" (click)="togglePanelCollapsed(panel.id)">
3248
+ <ngxsp-smart-icon [name]="isPanelCollapsed(panel.id) ? 'chevron-down' : 'chevron-up'" />
3249
+ </button>
3250
+ <button class="ngxsp-yt-inspectorClose" type="button" [attr.aria-label]="inspectorCloseLabel" [title]="inspectorCloseLabel" (click)="closePanel(panel.id)">
3251
+ <ngxsp-smart-icon name="close" />
3252
+ </button>
3253
+ </div>
3254
+
3255
+ @if (!isPanelCollapsed(panel.id)) {
3256
+ <ng-container *ngTemplateOutlet="panelBody; context: { $implicit: panel }" />
3257
+ }
3258
+ </section>
3259
+ </section>
3260
+ }
3261
+ </div>
3262
+ @if (playlistPanel; as lists) {
3263
+ <section class="ngxsp-yt-lists" [attr.aria-label]="lists.title">
3264
+ <div class="ngxsp-yt-sectionHead compact">
3265
+ <div><strong>{{ lists.title }}</strong></div>
3266
+ <span class="ngxsp-yt-countBadge">{{ lists.playlists.length }}</span>
3267
+ </div>
3268
+ <form class="ngxsp-yt-listForm" (submit)="submitCreatePlaylist($event)">
3269
+ <input type="text" [value]="playlistDraftName" (input)="onPlaylistDraftInput($event)" [placeholder]="lists.namePlaceholder" [attr.aria-label]="lists.namePlaceholder" />
3270
+ <button type="submit" [disabled]="!playlistDraftName.trim()">{{ lists.createLabel }}</button>
3271
+ </form>
3272
+ @if (lists.target; as target) {
3273
+ <p class="ngxsp-yt-listTarget">{{ lists.targetLabel }}<strong>{{ target.title }}</strong></p>
3274
+ }
3275
+ <div class="ngxsp-yt-listRows">
3276
+ @for (list of lists.playlists; track list.id) {
3277
+ <div class="ngxsp-yt-listRow" [class.active]="list.active">
3278
+ <button class="ngxsp-yt-listMain" type="button" [attr.aria-current]="list.active ? 'true' : null" [title]="lists.openLabel + ' - ' + list.name" (click)="selectPlaylist.emit(list.id)">
3279
+ <ngxsp-smart-icon name="list" />
3280
+ <span>
3281
+ <strong>{{ list.name }}</strong>
3282
+ @if (list.itemCount !== undefined) { <small>{{ list.itemCount }} {{ lists.itemsCountLabel }}</small> }
3283
+ </span>
3284
+ </button>
3285
+ <span class="ngxsp-yt-listActions">
3286
+ @if (lists.target) {
3287
+ @if (list.containsTarget) {
3288
+ <button type="button" [attr.aria-label]="lists.removeLabel + ' - ' + list.name" [title]="lists.removeLabel" (click)="removeFromPlaylist.emit(list.id)">
3289
+ <ngxsp-smart-icon name="check" />
3290
+ </button>
3291
+ } @else {
3292
+ <button type="button" [attr.aria-label]="lists.addLabel + ' - ' + list.name" [title]="lists.addLabel" (click)="addToPlaylist.emit(list.id)">
3293
+ <ngxsp-smart-icon name="plus" />
3294
+ </button>
3295
+ }
3296
+ }
3297
+ <button type="button" [attr.aria-label]="lists.renameLabel + ' - ' + list.name" [title]="lists.renameLabel" (click)="startRenamePlaylist(list)">
3298
+ <ngxsp-smart-icon name="tag" />
3299
+ </button>
3300
+ <button type="button" class="danger" [attr.aria-label]="lists.deleteLabel + ' - ' + list.name" [title]="lists.deleteLabel" (click)="deletePlaylist.emit(list.id)">
3301
+ <ngxsp-smart-icon name="close" />
3302
+ </button>
3303
+ </span>
3304
+ @if (renamingPlaylistId === list.id) {
3305
+ <form class="ngxsp-yt-listForm" (submit)="submitRenamePlaylist($event)">
3306
+ <input type="text" [value]="playlistRenameDraft" (input)="onPlaylistRenameInput($event)" [placeholder]="lists.renamePlaceholder" [attr.aria-label]="lists.renamePlaceholder" />
3307
+ <button type="submit" [disabled]="!playlistRenameDraft.trim()">{{ lists.saveLabel }}</button>
3308
+ <button type="button" (click)="cancelRenamePlaylist()">{{ lists.cancelLabel }}</button>
3309
+ </form>
3310
+ }
3311
+ </div>
3312
+ } @empty {
3313
+ <p class="ngxsp-yt-listEmpty">{{ lists.emptyLabel }}</p>
3314
+ }
3315
+ </div>
3316
+ </section>
3317
+ }
3318
+ @if (miniPlayerVisible) {
3319
+ <article class="ngxsp-yt-miniPlayer" [class.playing]="isPlaying" [attr.aria-label]="nowPlayingLabel">
3320
+ <button class="ngxsp-yt-miniArtwork" type="button" [attr.aria-label]="miniPlayerFocusLabel + ' - ' + displayHeroTitle" [title]="miniPlayerFocusLabel" (click)="miniPlayerFocus.emit()">
3321
+ @if (displayHeroArtwork) {
3322
+ <img [src]="displayHeroArtwork" alt="" />
3323
+ } @else {
3324
+ <span class="ngxsp-yt-art" aria-hidden="true"><ngxsp-smart-icon name="video" /></span>
3325
+ }
3326
+ <span class="ngxsp-yt-miniPlayBadge" aria-hidden="true">
3327
+ @if (isPlaying) { <ngxsp-smart-icon name="waveform" /> } @else { <ngxsp-smart-icon name="play" /> }
3328
+ </span>
3329
+ </button>
3330
+ <span class="ngxsp-yt-miniDetails">
3331
+ <small>{{ miniPlayerMeta || heroEyebrow }}</small>
3332
+ <strong>{{ displayHeroTitle }}</strong>
3333
+ @if (displayHeroSubtitle) { <span>{{ displayHeroSubtitle }}</span> }
3334
+ </span>
3335
+ <button class="ngxsp-yt-miniControl" type="button" [attr.aria-label]="miniPlayerPlaybackLabel" [title]="miniPlayerPlaybackLabel" (click)="toggleMiniPlayerPlayback()">
3336
+ @if (isPlaying) { <ngxsp-smart-icon name="pause" /> } @else { <ngxsp-smart-icon name="play" /> }
3337
+ </button>
3338
+ <button class="ngxsp-yt-miniClose" type="button" [attr.aria-label]="miniPlayerCloseLabel" [title]="miniPlayerCloseLabel" (click)="miniPlayerClose.emit()">
3339
+ <ngxsp-smart-icon name="close" />
3340
+ </button>
3341
+ </article>
3342
+ } @else {
3343
+ <button class="ngxsp-yt-miniRestore" type="button" [attr.aria-label]="miniPlayerRestoreLabel" [title]="miniPlayerRestoreLabel" (click)="miniPlayerRestore.emit()">
3344
+ <ngxsp-smart-icon name="window-mini" />
3345
+ <span>{{ miniPlayerRestoreLabel }}</span>
3346
+ </button>
3347
+ }
3348
+ </aside>
3349
+ }
3350
+ </div>
3351
+ </section>
3352
+ `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{display:block;height:100%;min-height:0;color:var(--nsp-text, #ededf0);font-family:var(--nsp-font, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Ubuntu, sans-serif);font-size:13px;line-height:1.4}.ngxsp-yt-shell{--yt-bg:var(--nsp-bg, #0d0e11);--yt-s0:var(--nsp-surface-0, #121317);--yt-s1:var(--nsp-surface-1, #17181d);--yt-s2:var(--nsp-surface-2, #1e1f25);--yt-s3:var(--nsp-surface-3, #26272e);--yt-line:var(--nsp-line, rgb(255 255 255 / .07));--yt-line-strong:var(--nsp-line-strong, rgb(255 255 255 / .13));--yt-text:var(--nsp-text, #ededf0);--yt-text-2:var(--nsp-text-2, #a3a5ae);--yt-text-3:var(--nsp-text-3, #80828c);--yt-accent:var(--nsp-accent, #8b90ff);--yt-accent-strong:var(--nsp-accent-strong, #5d63ee);--yt-accent-soft:var(--nsp-accent-soft, rgb(139 144 255 / .14));--yt-on-accent:var(--nsp-on-accent, #fff);--yt-success:var(--nsp-success, #3ecf8e);--yt-warning:var(--nsp-warning, #f2a93b);--yt-danger:var(--nsp-danger, #f25f5c);--yt-danger-text:var(--nsp-danger-text, #ffb4b2);--yt-live:var(--nsp-live, #f25f5c);--yt-r-sm:var(--nsp-radius-sm, 6px);--yt-r-md:var(--nsp-radius-md, 10px);--yt-r-lg:var(--nsp-radius-lg, 14px);--yt-ease:var(--nsp-ease, cubic-bezier(.2,0,0,1));--yt-dur:var(--nsp-dur, .14s);--yt-title-h:var(--nsp-titlebar-height, 48px);--yt-rail-w:84px;--yt-side-w:360px;--yt-mode:var(--yt-text-2);position:relative;height:100%;min-height:600px;display:grid;grid-template-columns:var(--yt-rail-w) minmax(0,1fr);grid-template-rows:var(--yt-title-h) minmax(0,1fr);grid-template-areas:\"top top\" \"rail body\";overflow:hidden;background:var(--yt-bg);color:var(--yt-text);box-sizing:border-box}.ngxsp-yt-shell.ngxsp-yt-railExpanded{--yt-rail-w:220px}.ngxsp-yt-shell.ngxsp-yt-menuRow{grid-template-columns:minmax(0,1fr);grid-template-rows:var(--yt-title-h) auto minmax(0,1fr);grid-template-areas:\"top\" \"rail\" \"body\"}.ngxsp-yt-menuRow .ngxsp-yt-rail{flex-direction:row;padding:6px 12px;border-right:0;border-bottom:1px solid var(--yt-line);overflow-x:auto;overflow-y:hidden}.ngxsp-yt-menuRow .ngxsp-yt-railButton{width:auto;min-height:36px;flex:0 0 auto;flex-direction:row;gap:6px;padding:0 10px}.ngxsp-yt-menuRow .ngxsp-yt-railButton span{font-size:12px}.ngxsp-yt-menuRow .ngxsp-yt-railMenu{display:none}.ngxsp-yt-shell.ngxsp-yt-menuOff{grid-template-columns:minmax(0,1fr);grid-template-rows:var(--yt-title-h) minmax(0,1fr);grid-template-areas:\"top\" \"body\"}.ngxsp-yt-menuOff .ngxsp-yt-rail{display:none}.ngxsp-yt-shell.ngxsp-yt-online{--yt-mode:var(--yt-live)}.ngxsp-yt-shell.ngxsp-yt-offline{--yt-mode:var(--yt-success)}.ngxsp-yt-shell *{box-sizing:border-box;letter-spacing:0}.ngxsp-yt-shell *::-webkit-scrollbar{width:10px;height:10px}.ngxsp-yt-shell *::-webkit-scrollbar-track{background:transparent}.ngxsp-yt-shell *::-webkit-scrollbar-thumb{background:var(--yt-s3);border-radius:999px;border:3px solid transparent;background-clip:padding-box}button,input{font:inherit;color:inherit}button{cursor:pointer}img{display:block;width:100%;height:100%;object-fit:cover}h1,h2,p{margin:0}ngxsp-smart-icon{width:18px;height:18px}.ngxsp-yt-fileInput{display:none}.ngxsp-yt-shell button:focus-visible,.ngxsp-yt-shell input:focus-visible{outline:2px solid var(--yt-accent);outline-offset:2px}.ngxsp-yt-topbar{grid-area:top;min-width:0;display:grid;grid-template-columns:auto minmax(200px,1fr) auto;align-items:center;gap:16px;padding:0 calc(12px + var(--nsp-titlebar-inset-end, 0px)) 0 calc(12px + var(--nsp-titlebar-inset-start, 0px));border-bottom:1px solid var(--yt-line);background:var(--yt-s0);-webkit-app-region:drag;-webkit-user-select:none;user-select:none}.ngxsp-yt-topbar button,.ngxsp-yt-topbar input,.ngxsp-yt-topbar label,.ngxsp-yt-topbar .ngxsp-yt-status{-webkit-app-region:no-drag}.ngxsp-yt-brand{min-width:0;display:flex;align-items:center;gap:10px}.ngxsp-yt-railMenu{width:32px;height:32px;display:grid;place-items:center;padding:0;margin-right:6px;border:0;border-radius:var(--yt-r-sm);background:transparent;color:var(--yt-text-2)}.ngxsp-yt-railMenu:hover{background:var(--yt-s2);color:var(--yt-text)}.ngxsp-yt-brandIcon{width:26px;height:26px;flex:0 0 auto;display:grid;place-items:center;border-radius:7px;color:#fff;background:linear-gradient(145deg,#8b90ff,#5d63ee 55%,#4146c9);box-shadow:inset 0 1px #ffffff38}.ngxsp-yt-brandIcon ngxsp-smart-icon{width:14px;height:14px;margin-left:1px}.ngxsp-yt-brandLogo{width:26px;height:26px;flex:0 0 auto;display:block}.ngxsp-yt-brand strong{font-size:13px;font-weight:600;white-space:nowrap}.ngxsp-yt-search{position:relative;justify-self:center;width:100%;max-width:520px;height:34px;display:block;color:var(--yt-text-3)}.ngxsp-yt-search ngxsp-smart-icon{position:absolute;left:11px;top:50%;width:16px;height:16px;transform:translateY(-50%);pointer-events:none}.ngxsp-yt-search input{width:100%;height:100%;padding:0 10px 0 35px;border:1px solid transparent;border-radius:8px;outline:0;background:var(--yt-s2);color:var(--yt-text);font-size:13px;transition:border-color var(--yt-dur) var(--yt-ease),background-color var(--yt-dur) var(--yt-ease),box-shadow var(--yt-dur) var(--yt-ease)}.ngxsp-yt-search input:hover{border-color:var(--yt-line-strong)}.ngxsp-yt-shell .ngxsp-yt-search input:focus,.ngxsp-yt-shell .ngxsp-yt-search input:focus-visible{outline:0;border-color:var(--yt-accent);background:var(--yt-s1);box-shadow:0 0 0 3px var(--yt-accent-soft)}.ngxsp-yt-search input::placeholder{color:var(--yt-text-3)}.ngxsp-yt-actions{min-width:0;display:flex;align-items:center;justify-content:flex-end;gap:8px}.ngxsp-yt-status{min-width:0;max-width:260px;height:26px;display:inline-flex;align-items:center;gap:7px;padding:0 10px;border-radius:999px;background:var(--yt-s2);color:var(--yt-text-2);font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.ngxsp-yt-status:before{content:\"\";width:6px;height:6px;flex:0 0 auto;border-radius:50%;background:var(--yt-accent)}.ngxsp-yt-status.success:before{background:var(--yt-success)}.ngxsp-yt-status.error{color:var(--yt-danger-text);background:#f25f5c29}.ngxsp-yt-status.error:before{background:var(--yt-danger)}.ngxsp-yt-mode,.ngxsp-yt-languageSwitch{flex:0 0 auto;display:inline-flex;align-items:center;gap:2px;padding:2px;border-radius:8px;background:var(--yt-s2)}.ngxsp-yt-mode button,.ngxsp-yt-languageSwitch button{height:32px;display:inline-flex;align-items:center;gap:6px;padding:0 10px;border:0;border-radius:6px;background:transparent;color:var(--yt-text-2);font-size:12px;font-weight:500;white-space:nowrap}.ngxsp-yt-mode button i{width:6px;height:6px;border-radius:50%;background:var(--yt-text-3)}.ngxsp-yt-mode button:hover,.ngxsp-yt-languageSwitch button:hover{color:var(--yt-text)}.ngxsp-yt-mode button.active,.ngxsp-yt-languageSwitch button.active{background:var(--yt-s3);color:var(--yt-text);box-shadow:0 1px 2px #0000004d}.ngxsp-yt-mode button.active i{background:var(--yt-mode);box-shadow:0 0 0 3px color-mix(in srgb,var(--yt-mode) 22%,transparent)}.ngxsp-yt-sourceButton{flex:0 0 auto;height:32px;display:inline-flex;align-items:center;gap:7px;padding:0 12px 0 10px;border:0;border-radius:var(--yt-r-sm);background:var(--yt-accent-strong);color:var(--yt-on-accent);font-size:12px;font-weight:600;white-space:nowrap}.ngxsp-yt-sourceButton ngxsp-smart-icon{width:16px;height:16px}.ngxsp-yt-sourceButton:hover{filter:brightness(1.1)}.ngxsp-yt-sourceButton:disabled{opacity:.7;cursor:progress}.ngxsp-yt-libraryStatus{display:inline-flex;align-items:center;gap:6px}.ngxsp-yt-libraryStatus:before{content:\"\";width:6px;height:6px;flex:0 0 auto;border-radius:50%;background:var(--yt-success)}.ngxsp-yt-libraryStatus.busy:before{background:var(--yt-accent);animation:ngxsp-yt-pulse 1.2s ease-in-out infinite}@keyframes ngxsp-yt-pulse{0%,to{opacity:.35}50%{opacity:1}}.ngxsp-yt-round{width:32px;height:32px;flex:0 0 auto;display:grid;place-items:center;padding:0;border:0;border-radius:var(--yt-r-sm);background:transparent;color:var(--yt-text-2)}.ngxsp-yt-round:hover{background:var(--yt-s2);color:var(--yt-text)}.ngxsp-yt-rail{grid-area:rail;min-width:0;min-height:0;display:flex;flex-direction:column;gap:2px;padding:10px 8px;overflow-y:auto;overflow-x:hidden;border-right:1px solid var(--yt-line);background:var(--yt-s0)}.ngxsp-yt-railButton{width:100%;min-height:56px;flex:0 0 auto;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;padding:6px 2px;border:0;border-radius:var(--yt-r-sm);background:transparent;color:var(--yt-text-2);transition:background-color var(--yt-dur) var(--yt-ease),color var(--yt-dur) var(--yt-ease)}.ngxsp-yt-railButton ngxsp-smart-icon{width:20px;height:20px}.ngxsp-yt-railButton span{max-width:100%;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;font-size:10px;font-weight:500;line-height:1.15;text-align:center;-webkit-hyphens:auto;hyphens:auto;overflow-wrap:anywhere}.ngxsp-yt-railButton:hover:not(:disabled){background:var(--yt-s2);color:var(--yt-text)}.ngxsp-yt-railButton.active{background:var(--yt-s3);color:var(--yt-text)}.ngxsp-yt-railButton.active ngxsp-smart-icon{color:var(--yt-accent)}.ngxsp-yt-railButton:disabled{opacity:.4;cursor:default}.ngxsp-yt-railExpanded .ngxsp-yt-railButton{min-height:36px;flex-direction:row;justify-content:flex-start;gap:12px;padding:0 12px}.ngxsp-yt-railExpanded .ngxsp-yt-railButton span{font-size:13px;text-align:left;-webkit-line-clamp:1;overflow-wrap:normal}.ngxsp-yt-railExpanded .ngxsp-yt-railButton ngxsp-smart-icon{width:18px;height:18px}.ngxsp-yt-body{grid-area:body;min-width:0;min-height:0;display:grid;grid-template-columns:minmax(0,1fr) var(--yt-side-w);overflow:hidden}.ngxsp-queue-hidden .ngxsp-yt-body{grid-template-columns:minmax(0,1fr)}.ngxsp-yt-watch{--yt-gutter:24px;min-width:0;min-height:0;display:flex;flex-direction:column;gap:20px;padding:20px var(--yt-gutter) 40px;overflow:auto}.ngxsp-yt-cinema .ngxsp-yt-body{display:block;overflow:auto}.ngxsp-yt-cinema .ngxsp-yt-watch{overflow:visible;padding-top:16px}.ngxsp-yt-watch.player{padding:0;gap:0;overflow:hidden;height:100%}.ngxsp-yt-cinema .ngxsp-yt-watch.player{padding:0;overflow:hidden;height:100%}.ngxsp-yt-playerBack{position:absolute;top:12px;left:12px;z-index:4;display:none;align-items:center;gap:2px;height:36px;padding:0 14px 0 8px;border:0;border-radius:999px;background:#0000008c;color:#fff;font:inherit;font-size:13px;font-weight:600;cursor:pointer;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)}.ngxsp-yt-playerBack ngxsp-smart-icon{width:18px;height:18px}.ngxsp-yt-playerShell{position:relative}.ngxsp-yt-watch.player .ngxsp-yt-playerShell{flex:1 1 auto;min-height:0}.ngxsp-yt-watch.player .ngxsp-yt-playerFrame{width:100%;min-width:0;height:100%;aspect-ratio:auto;border-radius:0;box-shadow:none}.ngxsp-yt-playerShell{flex:0 0 auto;display:flex;justify-content:center}.ngxsp-yt-playerFrame{position:relative;width:min(100%,calc((100vh - var(--yt-title-h) - 190px) * 16 / 9));min-width:min(100%,620px);aspect-ratio:16 / 9;overflow:hidden;border-radius:var(--yt-r-md);background:#000;box-shadow:0 0 0 1px var(--yt-line),var(--nsp-shadow-media, 0 24px 60px rgb(0 0 0 / .45))}.ngxsp-yt-cinema .ngxsp-yt-playerFrame{width:min(100%,calc((100vh - var(--yt-title-h) - 48px) * 16 / 9))}.ngxsp-yt-playerFrame ngx-smart-player{position:absolute;inset:0;display:block}.ngxsp-yt-previewPlayer{position:absolute;inset:0;width:100%;height:100%;padding:0;border:0;background:#000;overflow:hidden}.ngxsp-yt-previewPlayer img{transition:transform .6s var(--yt-ease),filter var(--yt-dur) var(--yt-ease)}.ngxsp-yt-previewPlayer:hover img{transform:scale(1.015);filter:brightness(.92)}.ngxsp-yt-previewOverlay{position:absolute;inset:0;display:grid;place-items:center;background:linear-gradient(180deg,#0000001a 40%,#0000008c)}.ngxsp-yt-previewPlay{width:72px;height:72px;display:grid;place-items:center;border-radius:999px;background:#fffffff0;color:#0d0e11;box-shadow:0 12px 40px #00000073;transition:transform var(--yt-dur) var(--yt-ease)}.ngxsp-yt-previewPlay ngxsp-smart-icon{width:30px;height:30px;margin-left:3px}.ngxsp-yt-previewPlayer:hover .ngxsp-yt-previewPlay{transform:scale(1.06)}.ngxsp-yt-emptyPlayer{position:absolute;inset:0;display:grid;place-content:center;justify-items:center;gap:8px;padding:24px 24px 88px;background:radial-gradient(80% 90% at 50% 20%,rgb(93 99 238 / .16),transparent 70%),#08090b;text-align:center}.ngxsp-yt-bigPlay{width:64px;height:64px;display:grid;place-items:center;margin-bottom:8px;border:0;border-radius:18px;background:var(--yt-accent-soft);color:var(--yt-accent);transition:background-color var(--yt-dur) var(--yt-ease)}.ngxsp-yt-bigPlay:hover{background:#8b90ff3d}.ngxsp-yt-bigPlay ngxsp-smart-icon{width:28px;height:28px}.ngxsp-yt-emptyPlayer span{color:var(--yt-text-2);font-size:12px;font-weight:600}.ngxsp-yt-emptyPlayer strong{font-size:20px;font-weight:600}.ngxsp-yt-emptyPlayer p{max-width:56ch;color:var(--yt-text-2)}.ngxsp-yt-transportBar{position:absolute;left:0;right:0;bottom:0;z-index:2;display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;gap:12px;padding:40px 12px 10px;background:linear-gradient(180deg,#0000,#000000b8);pointer-events:none}.ngxsp-yt-transportBar>*{pointer-events:auto}.ngxsp-yt-shell.ngxsp-yt-compact{display:block;height:100vh;min-height:0;padding:env(titlebar-area-height,0px) 0 0;box-sizing:border-box}.ngxsp-yt-shell.ngxsp-yt-compact>:not(.ngxsp-yt-body){display:none!important}.ngxsp-yt-compact .ngxsp-yt-body{display:block;height:100%}.ngxsp-yt-compact .ngxsp-yt-body>:not(.ngxsp-yt-watch){display:none!important}.ngxsp-yt-compact .ngxsp-yt-watch{display:block;height:100%;padding:0;overflow:hidden}.ngxsp-yt-compact .ngxsp-yt-watch>:not(.ngxsp-yt-playerShell){display:none!important}.ngxsp-yt-compact .ngxsp-yt-playerShell{height:100%}.ngxsp-yt-compact .ngxsp-yt-playerFrame{position:relative;width:100%;min-width:0;height:100%;aspect-ratio:auto;border-radius:0;box-shadow:none}.ngxsp-yt-compactDrag{position:absolute;top:0;left:0;right:0;z-index:6;height:28px;-webkit-app-region:drag}.ngxsp-yt-compact .ngxsp-yt-playerInlineControls>.ngxsp-yt-playerAction:not(:first-child){display:none}.ngxsp-yt-compact ngx-smart-player ::ng-deep .ngxsp-inline-projection{display:inline-flex}.ngxsp-yt-compact :is(.ngxsp-yt-transportCinema,.ngxsp-yt-transportBar .ngxsp-yt-queueButton){display:none}.ngxsp-yt-transportCluster{display:flex;align-items:center;gap:4px}.ngxsp-yt-transportButton{width:36px;height:36px;display:grid;place-items:center;padding:0;border:0;border-radius:var(--yt-r-sm);background:transparent;color:#ffffffeb;transition:background-color var(--yt-dur) var(--yt-ease)}.ngxsp-yt-transportButton ngxsp-smart-icon{width:20px;height:20px}.ngxsp-yt-transportButton:hover:not(:disabled){background:#ffffff1f}.ngxsp-yt-transportButton.active{color:var(--yt-accent)}.ngxsp-yt-transportButton:disabled{opacity:.4;cursor:default}.ngxsp-yt-transportPlay{width:44px;height:44px;display:grid;place-items:center;margin:0 4px;padding:0;border:0;border-radius:999px;background:#fff;color:#0d0e11;transition:transform var(--yt-dur) var(--yt-ease)}.ngxsp-yt-transportPlay:hover{transform:scale(1.05)}.ngxsp-yt-transportPlay ngxsp-smart-icon{width:20px;height:20px}.ngxsp-yt-transportInfo{min-width:0;display:grid;gap:1px;color:#fff}.ngxsp-yt-transportInfo span{color:#ffffffb8;font-size:12px;font-weight:600}.ngxsp-yt-transportInfo strong{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:14px;font-weight:600}.ngxsp-yt-volumeControl{--ngxsp-control-size:36px;--ngxsp-volume-range-min:64px;--ngxsp-volume-range-max:92px;--ngxsp-volume-gap:2px;--ngxsp-volume-button-border:0;--ngxsp-volume-button-radius:6px;--ngxsp-volume-accent:#fff;--ngxsp-volume-track-bg:rgb(255 255 255 / .28);--ngxsp-volume-thumb-bg:#fff;--ngxsp-volume-thumb-size:12px;--ngxsp-volume-thumb-shadow:none;--ngxsp-focus-ring:0 0 0 2px var(--yt-accent);color:#fff}.ngxsp-yt-playerInlineControls{display:inline-flex;align-items:center;gap:0}.ngxsp-yt-playerAction{width:36px;height:36px;display:grid;place-items:center;padding:0;border:0;border-radius:6px;background:transparent;color:#ffffffeb}.ngxsp-yt-playerAction ngxsp-smart-icon{width:20px;height:20px}.ngxsp-yt-playerAction:hover{background:#ffffff1f}.ngxsp-yt-playerAction.active{color:var(--yt-accent)}.ngxsp-yt-metaBar{flex:0 0 auto;min-width:0;display:flex;align-items:flex-start;justify-content:space-between;flex-wrap:wrap;gap:12px 24px}.ngxsp-yt-titleGroup{flex:1 1 320px;min-width:0;display:grid;gap:4px}.ngxsp-yt-titleGroup h1{font-size:22px;line-height:1.25;font-weight:700}.ngxsp-yt-shell .ngxsp-yt-titleGroup h1{letter-spacing:-.01em}.ngxsp-yt-titleGroup p{color:var(--yt-text-2);font-size:13px;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.ngxsp-yt-titleGroup small{color:var(--yt-text-3);font-size:12px}.ngxsp-yt-watchActions{display:flex;align-items:center;flex-wrap:wrap;gap:8px}.ngxsp-yt-rating{display:inline-flex;border-radius:999px;background:var(--yt-s2);overflow:hidden}.ngxsp-yt-watchActions button{height:34px;display:inline-flex;align-items:center;gap:7px;padding:0 14px 0 12px;border:0;border-radius:999px;background:var(--yt-s2);color:var(--yt-text);font-size:12px;font-weight:500;white-space:nowrap;transition:background-color var(--yt-dur) var(--yt-ease),color var(--yt-dur) var(--yt-ease)}.ngxsp-yt-watchActions button:hover{background:var(--yt-s3)}.ngxsp-yt-watchActions button.active{color:var(--yt-accent)}.ngxsp-yt-watchActions button.active ngxsp-smart-icon ::ng-deep path{fill:var(--yt-accent-soft)}.ngxsp-yt-rating button{border-radius:0}.ngxsp-yt-rating button+button{position:relative;padding-left:12px}.ngxsp-yt-rating button+button:before{content:\"\";position:absolute;left:0;top:9px;bottom:9px;width:1px;background:var(--yt-line-strong)}.ngxsp-yt-rating button+button span{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0)}.ngxsp-yt-moreAction{width:34px;padding:0!important;justify-content:center}.ngxsp-yt-moreAction span{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0)}.ngxsp-yt-watchActions ngxsp-smart-icon{width:17px;height:17px}.ngxsp-yt-toolRow{flex:0 0 auto;display:grid;grid-template-columns:repeat(auto-fit,minmax(146px,1fr));gap:8px}.ngxsp-yt-tool{min-width:0;height:52px;display:grid;grid-template-columns:30px minmax(0,1fr);align-items:center;gap:9px;padding:0 10px 0 9px;border:1px solid var(--yt-line);border-radius:var(--yt-r-md);background:var(--yt-s1);text-align:left;transition:border-color var(--yt-dur) var(--yt-ease),background-color var(--yt-dur) var(--yt-ease)}.ngxsp-yt-tool:hover{border-color:var(--yt-line-strong);background:var(--yt-s2)}.ngxsp-yt-tool.active{border-color:#8b90ff73;background:var(--yt-accent-soft)}.ngxsp-yt-toolIcon{--tone:var(--yt-text-2);width:30px;height:30px;display:grid;place-items:center;border-radius:8px;background:color-mix(in srgb,var(--tone) 16%,transparent);color:var(--tone)}.ngxsp-yt-tool[data-tone=teal] .ngxsp-yt-toolIcon{--tone:var(--nsp-tone-teal, #3fcfbf)}.ngxsp-yt-tool[data-tone=red] .ngxsp-yt-toolIcon{--tone:var(--nsp-tone-red, #f47c7a)}.ngxsp-yt-tool[data-tone=blue] .ngxsp-yt-toolIcon{--tone:var(--nsp-tone-blue, #7cb1ff)}.ngxsp-yt-tool[data-tone=gold] .ngxsp-yt-toolIcon{--tone:var(--nsp-tone-gold, #f2bb5b)}.ngxsp-yt-toolIcon ngxsp-smart-icon{width:17px;height:17px}.ngxsp-yt-toolCopy{min-width:0;display:grid;gap:1px}.ngxsp-yt-toolCopy small,.ngxsp-yt-toolCopy strong{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ngxsp-yt-toolCopy small{color:var(--yt-text-2);font-size:11px}.ngxsp-yt-toolCopy strong{font-size:13px;font-weight:500}.ngxsp-yt-art{position:absolute;inset:0;display:grid;place-items:center;color:#ffffff8c}.ngxsp-yt-art ngxsp-smart-icon{width:26px;height:26px}[data-art] .ngxsp-yt-thumbnail:not(.has-image),[data-art] .ngxsp-yt-suggestionThumb:not(.has-image),[data-art] .ngxsp-yt-searchThumb:not(.has-image){background:radial-gradient(90% 80% at 15% 10%,rgb(255 255 255 / .14),transparent 60%),linear-gradient(140deg,var(--art-a, #3a3c55),var(--art-b, #15161c))}[data-art=aurora]{--art-a:#3a7f78;--art-b:#10252a}[data-art=orbit]{--art-a:#5157c9;--art-b:#1a1b3d}[data-art=ember]{--art-a:#b0603a;--art-b:#301810}[data-art=ridge]{--art-a:#4f6f94;--art-b:#131d2a}[data-art=signal]{--art-a:#a8416a;--art-b:#2c1120}[data-art=fog]{--art-a:#5d6270;--art-b:#1a1b20}.ngxsp-yt-nowBadge{position:absolute;right:8px;top:8px;width:26px;height:26px;display:grid;place-items:center;border-radius:999px;background:var(--yt-accent-strong);color:#fff;box-shadow:0 4px 12px #0006}.ngxsp-yt-nowBadge ngxsp-smart-icon{width:14px;height:14px}.ngxsp-yt-thumbKind{position:absolute;left:8px;top:8px;max-width:calc(100% - 50px);padding:2px 7px;border-radius:4px;background:#000000b8;color:#fff;font-size:12px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px)}.ngxsp-yt-cardProgress{position:absolute;left:0;right:0;bottom:0;height:3px;background:#fff3}.ngxsp-yt-cardProgress i{position:absolute;inset:0 auto 0 0;background:var(--yt-accent)}.ngxsp-yt-thumbBadge,.ngxsp-yt-suggestionThumb>small,.ngxsp-yt-searchThumb>small{position:absolute;right:6px;bottom:7px;max-width:calc(100% - 12px);padding:2px 6px;border-radius:4px;background:#000000c7;color:#fff;font-size:11px;font-weight:600;font-variant-numeric:tabular-nums;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.ngxsp-yt-sectionHead{display:flex;align-items:center;justify-content:space-between;gap:12px}.ngxsp-yt-sectionHead>div{min-width:0;display:grid;gap:2px}.ngxsp-yt-sectionHead strong{font-size:16px;font-weight:600}.ngxsp-yt-sectionHead small{color:var(--yt-text-2);font-size:12px}.ngxsp-yt-sectionHead.compact{padding:14px 16px 10px}.ngxsp-yt-sectionHead.compact strong{font-size:14px}.ngxsp-yt-sectionLink{width:32px;height:32px;display:grid;place-items:center;padding:0;border:0;border-radius:var(--yt-r-sm);background:transparent;color:var(--yt-text-2)}.ngxsp-yt-sectionLink:hover{background:var(--yt-s2);color:var(--yt-text)}.ngxsp-yt-servers{display:grid;gap:8px}.ngxsp-yt-serversHeading{margin:6px 0 2px;font-size:13px;font-weight:600;letter-spacing:.02em;color:var(--yt-text-2);text-transform:uppercase}.ngxsp-yt-serverForm{display:flex;flex-wrap:wrap;align-items:center;gap:8px;padding:10px 12px;border:1px solid var(--yt-line);border-radius:var(--yt-r-md);background:var(--yt-s1)}.ngxsp-yt-serverTitle{font-size:12px;font-weight:600;color:var(--yt-text-2)}.ngxsp-yt-discovered{flex:1 1 100%;display:grid;gap:4px;margin:0;padding:0;list-style:none}.ngxsp-yt-discovered li{display:flex;align-items:center;justify-content:space-between;gap:8px;min-height:36px;padding:2px 0 2px 8px;border-top:1px solid var(--yt-line)}.ngxsp-yt-discovered li span{display:grid;min-width:0}.ngxsp-yt-discovered strong{overflow:hidden;font-size:13px;font-weight:600;text-overflow:ellipsis;white-space:nowrap}.ngxsp-yt-discovered small,.ngxsp-yt-discovered li.empty{overflow:hidden;color:var(--yt-text-2);font-size:12px;text-overflow:ellipsis;white-space:nowrap}.ngxsp-yt-serverKind{height:32px;padding:0 8px;border:1px solid var(--yt-line);border-radius:var(--yt-r-sm);background:var(--yt-s2);color:var(--yt-text);font-size:12px}.ngxsp-yt-serverForm input{flex:1 1 180px;min-width:0;height:32px;padding:0 10px;border:1px solid var(--yt-line);border-radius:var(--yt-r-sm);background:var(--yt-s0, var(--yt-s2));color:var(--yt-text);font-size:12px}.ngxsp-yt-serverForm input::placeholder{color:var(--yt-text-3)}.ngxsp-yt-serverForm button{min-height:32px;padding:0 14px;border:1px solid var(--yt-line);border-radius:var(--yt-r-sm);background:var(--yt-accent, var(--yt-s3));color:var(--yt-text);font-size:12px;font-weight:600;cursor:pointer}.ngxsp-yt-serverForm button.secondary{border-color:var(--yt-line-strong, var(--yt-line));background:transparent}.ngxsp-yt-serverForm button[disabled]{opacity:.5;cursor:default}.ngxsp-yt-serverHint{flex:1 0 100%;color:var(--yt-text-3);font-size:11px}.ngxsp-yt-serverRow{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:6px;border-radius:var(--yt-r-sm)}.ngxsp-yt-serverRow.active{background:var(--yt-s1)}.ngxsp-yt-serverMain{display:flex;align-items:center;gap:8px;min-width:0;padding:6px 8px;border:0;border-radius:var(--yt-r-sm);background:transparent;color:var(--yt-text);text-align:left;cursor:pointer}.ngxsp-yt-serverMain span{display:grid;min-width:0}.ngxsp-yt-serverMain strong{font-size:13px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ngxsp-yt-serverMain small{color:var(--yt-text-3);font-size:11px}.ngxsp-yt-serverMain:hover{background:var(--yt-s2)}.ngxsp-yt-serverDisconnect{flex:0 0 32px;width:32px;height:32px;display:grid;place-items:center;border:0;border-radius:999px;background:transparent;color:var(--yt-text-2);cursor:pointer}.ngxsp-yt-serverDisconnect:hover{background:var(--yt-s2);color:var(--yt-danger, #e5484d)}.ngxsp-yt-lists{display:grid;gap:8px;margin-bottom:16px}.ngxsp-yt-listForm{display:flex;gap:6px}.ngxsp-yt-listForm input{flex:1;min-width:0;height:32px;padding:0 10px;border:1px solid var(--yt-line);border-radius:var(--yt-r-sm);background:var(--yt-s1);color:var(--yt-text);font-size:12px}.ngxsp-yt-listForm input::placeholder{color:var(--yt-text-3)}.ngxsp-yt-listForm button{min-height:32px;height:32px;padding:0 12px;border:1px solid var(--yt-line);border-radius:var(--yt-r-sm);background:var(--yt-s2);color:var(--yt-text);font-size:12px;font-weight:600;cursor:pointer}.ngxsp-yt-listForm button[disabled]{opacity:.5;cursor:default}.ngxsp-yt-listTarget{margin:0;color:var(--yt-text-2);font-size:12px}.ngxsp-yt-listTarget strong{margin-left:4px;color:var(--yt-text)}.ngxsp-yt-listRows{display:grid;gap:4px}.ngxsp-yt-listRow{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:6px;border-radius:var(--yt-r-sm)}.ngxsp-yt-listRow.active{background:var(--yt-s1)}.ngxsp-yt-listMain{display:flex;align-items:center;gap:8px;min-width:0;padding:6px 8px;border:0;border-radius:var(--yt-r-sm);background:transparent;color:var(--yt-text);text-align:left;cursor:pointer}.ngxsp-yt-listMain span{display:grid;min-width:0}.ngxsp-yt-listMain strong{font-size:13px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ngxsp-yt-listMain small{color:var(--yt-text-3);font-size:11px}.ngxsp-yt-listMain:hover{background:var(--yt-s2)}.ngxsp-yt-listActions{display:flex;gap:2px}.ngxsp-yt-listActions button{flex:0 0 32px;width:32px;height:32px;display:grid;place-items:center;border:0;border-radius:999px;background:transparent;color:var(--yt-text-2);cursor:pointer}.ngxsp-yt-listActions button:hover{background:var(--yt-s2);color:var(--yt-text)}.ngxsp-yt-listActions button.danger:hover{color:var(--yt-danger, #e5484d)}.ngxsp-yt-listRow .ngxsp-yt-listForm{grid-column:1 / -1;padding:0 8px 6px}.ngxsp-yt-listEmpty{margin:0;color:var(--yt-text-3);font-size:12px}.ngxsp-yt-countBadge{min-width:24px;height:22px;display:grid;place-items:center;padding:0 7px;border-radius:999px;background:var(--yt-s3);color:var(--yt-text-2);font-size:12px;font-weight:600;font-variant-numeric:tabular-nums}.ngxsp-yt-panelTitle{display:block;color:var(--yt-text-2);font-size:12px;font-weight:600}.ngxsp-yt-searchWorkspace{flex:0 0 auto;display:grid;gap:12px;padding:16px;border:1px solid var(--yt-line);border-radius:var(--yt-r-lg);background:var(--yt-s1)}.ngxsp-yt-searchHead{display:flex;align-items:center;justify-content:space-between;gap:12px}.ngxsp-yt-searchHead>div{min-width:0;display:grid;gap:2px}.ngxsp-yt-searchHead strong{font-size:16px;font-weight:600}.ngxsp-yt-searchHead small{color:var(--yt-text-2);font-size:12px}.ngxsp-yt-searchHead button{width:32px;height:32px;display:grid;place-items:center;padding:0;border:0;border-radius:var(--yt-r-sm);background:transparent;color:var(--yt-text-2)}.ngxsp-yt-searchHead button:hover{background:var(--yt-s3);color:var(--yt-text)}.ngxsp-yt-searchFilters{display:flex;flex-wrap:wrap;gap:6px}.ngxsp-yt-searchFilters button{height:32px;display:inline-flex;align-items:center;gap:6px;padding:0 12px;border:1px solid var(--yt-line-strong);border-radius:999px;background:transparent;color:var(--yt-text-2);font-size:12px;font-weight:500}.ngxsp-yt-searchFilters button small{color:var(--yt-text-3);font-variant-numeric:tabular-nums}.ngxsp-yt-searchFilters button:hover:not(:disabled){color:var(--yt-text);border-color:var(--yt-text-3)}.ngxsp-yt-searchFilters button.active{border-color:transparent;background:var(--yt-text);color:var(--yt-bg)}.ngxsp-yt-searchFilters button.active small{color:var(--yt-bg)}.ngxsp-yt-searchFilters button:disabled{opacity:.4;cursor:default}.ngxsp-yt-searchResults{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:8px}.ngxsp-yt-searchResults.list{grid-template-columns:1fr}.ngxsp-yt-searchResult{position:relative;min-width:0;border-radius:var(--yt-r-md);transition:background-color var(--yt-dur) var(--yt-ease)}.ngxsp-yt-searchResult:hover,.ngxsp-yt-searchResult:focus-within{background:var(--yt-s2)}.ngxsp-yt-searchResult.active{background:var(--yt-accent-soft)}.ngxsp-yt-searchResultMain{width:100%;min-width:0;display:grid;grid-template-columns:128px minmax(0,1fr);align-items:center;gap:12px;padding:8px;border:0;border-radius:inherit;background:transparent;text-align:left}.ngxsp-yt-searchThumb{position:relative;aspect-ratio:16 / 9;overflow:hidden;border-radius:6px;background:var(--yt-s3)}.ngxsp-yt-searchCopy{min-width:0;display:grid;gap:2px}.ngxsp-yt-searchCopy small{color:var(--yt-accent);font-size:11px;font-weight:600}.ngxsp-yt-searchCopy strong{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:13px;font-weight:500}.ngxsp-yt-searchCopy em,.ngxsp-yt-searchCopy span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--yt-text-2);font-size:12px;font-style:normal}.ngxsp-yt-searchActions{position:absolute;right:8px;bottom:8px;display:flex;gap:2px;padding:2px;border-radius:8px;background:var(--yt-s3);opacity:0;transition:opacity var(--yt-dur) var(--yt-ease)}.ngxsp-yt-searchResult:hover .ngxsp-yt-searchActions,.ngxsp-yt-searchResult:focus-within .ngxsp-yt-searchActions{opacity:1}.ngxsp-yt-searchActions button{width:32px;height:32px;display:grid;place-items:center;padding:0;border:0;border-radius:6px;background:transparent;color:var(--yt-text-2)}.ngxsp-yt-searchActions button:hover{background:var(--yt-s2);color:var(--yt-text)}.ngxsp-yt-searchActions ngxsp-smart-icon{width:15px;height:15px}.ngxsp-yt-searchEmpty{grid-column:1 / -1;display:grid;justify-items:center;gap:6px;padding:28px 16px;color:var(--yt-text-2);text-align:center}.ngxsp-yt-searchEmpty ngxsp-smart-icon{width:24px;height:24px;color:var(--yt-text-3)}.ngxsp-yt-searchEmpty strong{color:var(--yt-text);font-weight:600}.ngxsp-yt-mediaDetail{flex:0 0 auto;min-width:0;display:grid;gap:0;border:1px solid var(--yt-line);border-radius:var(--yt-r-lg);background:var(--yt-s1);overflow:hidden}.ngxsp-yt-mediaHero{display:grid;grid-template-columns:minmax(200px,280px) minmax(0,1fr);gap:20px;padding:20px}.ngxsp-yt-mediaPoster{position:relative;align-self:start;aspect-ratio:16 / 9;overflow:hidden;display:grid;place-items:center;border-radius:var(--yt-r-md);background:linear-gradient(140deg,#4f6f94,#131d2a);box-shadow:var(--nsp-shadow-card, 0 12px 30px rgb(0 0 0 / .35))}.ngxsp-yt-mediaPoster span{color:#fffc;font-size:40px;font-weight:700}.ngxsp-yt-mediaDetailBody{min-width:0;display:grid;align-content:start;gap:8px}.ngxsp-yt-mediaKicker{color:var(--yt-accent);font-size:12px;font-weight:600}.ngxsp-yt-mediaDetailBody h2{font-size:18px;line-height:1.3;font-weight:600}.ngxsp-yt-mediaSubtitle{color:var(--yt-text);font-size:13px}.ngxsp-yt-mediaMeta{display:flex;flex-wrap:wrap;align-items:center;gap:4px 0;color:var(--yt-text-2);font-size:12px}.ngxsp-yt-mediaMeta span+span:before{content:\"\";display:inline-block;width:3px;height:3px;margin:0 8px 3px;border-radius:50%;background:var(--yt-text-3)}.ngxsp-yt-mediaSummary{max-width:72ch;color:var(--yt-text-2);font-size:13px;line-height:1.55}.ngxsp-yt-mediaProgress{display:flex;align-items:center;gap:10px;max-width:420px}.ngxsp-yt-mediaProgress>span{position:relative;flex:1;height:4px;border-radius:999px;background:var(--yt-s3);overflow:hidden}.ngxsp-yt-mediaProgress i{position:absolute;inset:0 auto 0 0;background:var(--yt-accent);border-radius:inherit}.ngxsp-yt-mediaProgress small{color:var(--yt-text-2);font-size:12px;white-space:nowrap}.ngxsp-yt-mediaTags{display:flex;flex-wrap:wrap;gap:6px}.ngxsp-yt-mediaTags span{height:22px;display:inline-flex;align-items:center;padding:0 8px;border-radius:6px;background:var(--yt-s3);color:var(--yt-text-2);font-size:11px;font-weight:500}.ngxsp-yt-mediaActions{display:flex;flex-wrap:wrap;gap:8px;margin-top:4px}.ngxsp-yt-mediaActions button,.ngxsp-yt-inspectorActions button,.ngxsp-yt-miniRestore{height:32px;display:inline-flex;align-items:center;justify-content:center;gap:7px;padding:0 12px 0 10px;border:1px solid var(--yt-line-strong);border-radius:var(--yt-r-sm);background:transparent;color:var(--yt-text);font-size:12px;font-weight:500;white-space:nowrap}.ngxsp-yt-mediaActions button ngxsp-smart-icon,.ngxsp-yt-inspectorActions button ngxsp-smart-icon,.ngxsp-yt-miniRestore ngxsp-smart-icon{width:16px;height:16px}.ngxsp-yt-mediaActions button:hover:not(:disabled),.ngxsp-yt-inspectorActions button:hover:not(:disabled),.ngxsp-yt-miniRestore:hover{background:var(--yt-s3)}.ngxsp-yt-mediaActions button.primary,.ngxsp-yt-inspectorActions button.primary{border-color:transparent;background:var(--yt-accent-strong);color:var(--yt-on-accent)}.ngxsp-yt-mediaActions button.primary:hover:not(:disabled),.ngxsp-yt-inspectorActions button.primary:hover:not(:disabled){background:var(--yt-accent-strong);filter:brightness(1.1)}.ngxsp-yt-mediaActions button:disabled,.ngxsp-yt-inspectorActions button:disabled{opacity:.45;cursor:default}.ngxsp-yt-mediaWorkspace{display:grid;gap:14px;padding:16px 20px 20px;border-top:1px solid var(--yt-line)}.ngxsp-yt-mediaWorkspaceHead{display:grid;gap:2px}.ngxsp-yt-mediaWorkspaceHead strong{font-size:14px;font-weight:600}.ngxsp-yt-mediaWorkspaceHead small{color:var(--yt-text-2);font-size:12px}.ngxsp-yt-mediaWorkspaceGrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:16px}.ngxsp-yt-chapterPanel,.ngxsp-yt-checkPanel{min-width:0;display:grid;align-content:start;gap:8px}.ngxsp-yt-chapterList,.ngxsp-yt-checkList{display:grid;gap:2px}.ngxsp-yt-mediaChapter{width:100%;min-width:0;display:grid;grid-template-columns:72px minmax(0,1fr);align-items:start;gap:12px;padding:9px 10px;border:0;border-radius:var(--yt-r-sm);background:transparent;text-align:left;transition:background-color var(--yt-dur) var(--yt-ease)}.ngxsp-yt-mediaChapter:hover{background:var(--yt-s2)}.ngxsp-yt-mediaChapter.active{background:var(--yt-accent-soft)}.ngxsp-yt-chapterTime{display:grid;gap:2px}.ngxsp-yt-chapterTime em{justify-self:start;padding:1px 6px;border-radius:4px;background:var(--yt-s3);color:var(--yt-text);font-family:var(--nsp-font-mono, ui-monospace, Menlo, monospace);font-size:11px;font-style:normal;font-variant-numeric:tabular-nums}.ngxsp-yt-mediaChapter.active .ngxsp-yt-chapterTime em{background:var(--yt-accent-strong);color:#fff}.ngxsp-yt-chapterTime b{color:var(--yt-text-2);font-size:11px;font-weight:500}.ngxsp-yt-chapterCopy{min-width:0;display:grid;gap:2px}.ngxsp-yt-chapterCopy strong{font-size:13px;font-weight:500}.ngxsp-yt-chapterCopy small{color:var(--yt-text-2);font-size:12px}.ngxsp-yt-checkItem{display:grid;grid-template-columns:8px minmax(0,1fr) auto;align-items:start;gap:12px;padding:9px 10px;border-radius:var(--yt-r-sm)}.ngxsp-yt-checkItem:hover{background:var(--yt-s2)}.ngxsp-yt-checkItem>span{--tone:var(--yt-text-3);width:8px;height:8px;margin-top:5px;border-radius:50%;background:var(--tone);box-shadow:0 0 0 3px color-mix(in srgb,var(--tone) 20%,transparent)}.ngxsp-yt-checkItem[data-tone=teal]>span{--tone:var(--nsp-tone-teal, #3fcfbf)}.ngxsp-yt-checkItem[data-tone=red]>span{--tone:var(--nsp-tone-red, #f47c7a)}.ngxsp-yt-checkItem[data-tone=blue]>span{--tone:var(--nsp-tone-blue, #7cb1ff)}.ngxsp-yt-checkItem[data-tone=gold]>span{--tone:var(--nsp-tone-gold, #f2bb5b)}.ngxsp-yt-checkItem>div{min-width:0;display:grid;gap:2px}.ngxsp-yt-checkItem strong{font-size:13px;font-weight:500}.ngxsp-yt-checkItem small{color:var(--yt-text-2);font-size:12px}.ngxsp-yt-checkItem em{color:var(--yt-text-2);font-size:12px;font-style:normal;white-space:nowrap}.ngxsp-yt-episodeRail{display:grid;gap:10px;padding:16px 20px 20px;border-top:1px solid var(--yt-line)}.ngxsp-yt-episodeRail>div{display:grid;grid-auto-flow:column;grid-auto-columns:minmax(200px,220px);gap:12px;overflow-x:auto;padding-bottom:4px}.ngxsp-yt-episodeRail button{min-width:0;display:grid;gap:8px;padding:6px;border:0;border-radius:var(--yt-r-md);background:transparent;text-align:left}.ngxsp-yt-episodeRail button:hover{background:var(--yt-s2)}.ngxsp-yt-episodeRail button.active{background:var(--yt-accent-soft)}.ngxsp-yt-episodeThumb{position:relative;aspect-ratio:16 / 9;overflow:hidden;display:grid;place-items:center;border-radius:6px;background:linear-gradient(140deg,#4f6f94,#131d2a)}.ngxsp-yt-episodeThumb>i{color:#fffc;font-size:22px;font-weight:700;font-style:normal}.ngxsp-yt-episodeThumb>em{position:absolute;left:0;right:0;bottom:0;height:3px;background:#fff3}.ngxsp-yt-episodeThumb>em i{position:absolute;inset:0 auto 0 0;background:var(--yt-accent)}.ngxsp-yt-episodeRail button>span:last-child{min-width:0;display:grid;gap:1px}.ngxsp-yt-episodeRail strong{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:13px;font-weight:500}.ngxsp-yt-episodeRail small,.ngxsp-yt-episodeRail button em{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--yt-text-2);font-size:12px;font-style:normal}.ngxsp-yt-library{flex:0 0 auto;display:grid;gap:12px}.ngxsp-yt-shelf{display:grid;gap:8px}.ngxsp-yt-shelfHead{display:flex;align-items:baseline;gap:8px;min-width:0}.ngxsp-yt-shelfHead strong{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:14px;font-weight:700}.ngxsp-yt-shelfHead small,.ngxsp-yt-shelfAll{flex:0 0 auto}.ngxsp-yt-shelfHead small{color:var(--yt-text-2);font-size:11.5px;font-variant-numeric:tabular-nums}.ngxsp-yt-shelfAll{margin-left:auto;padding:2px 8px;border:0;border-radius:999px;background:transparent;color:var(--yt-accent-strong);font:inherit;font-size:11.5px;font-weight:600;cursor:pointer}.ngxsp-yt-shelfAll:hover,.ngxsp-yt-shelfAll:focus-visible{background:var(--yt-s2)}.ngxsp-yt-trail{display:flex;flex-wrap:wrap;align-items:center;gap:6px;font-size:12px;color:var(--yt-text-2)}.ngxsp-yt-trail b{color:var(--yt-text);font-weight:600}.ngxsp-yt-trailStep{padding:2px 6px;border:0;border-radius:6px;background:transparent;color:inherit;font:inherit;font-size:12px;cursor:pointer}.ngxsp-yt-trailStep:hover,.ngxsp-yt-trailStep:focus-visible{background:var(--yt-s2);color:var(--yt-text)}.ngxsp-yt-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:20px 16px}.ngxsp-yt-grid.list{grid-template-columns:1fr;gap:4px}.ngxsp-yt-grid.rail{--yt-rail-card:clamp(168px, 17vw, 320px);display:flex;gap:16px;overflow-x:auto;overscroll-behavior-x:contain;scroll-snap-type:x proximity;margin-inline:calc(var(--yt-gutter, 24px) * -1);padding-inline:var(--yt-gutter, 24px);scroll-padding-inline:var(--yt-gutter, 24px);padding-bottom:4px;scrollbar-width:thin}.ngxsp-yt-grid.rail>*{flex:0 0 var(--yt-rail-card);scroll-snap-align:start}.ngxsp-yt-card{min-width:0;display:grid;align-content:start;gap:10px;padding:0;border:0;background:transparent;text-align:left}.ngxsp-yt-thumbnail{position:relative;aspect-ratio:16 / 9;overflow:hidden;border-radius:var(--yt-r-md);background:var(--yt-s2);box-shadow:0 0 0 1px var(--yt-line)}.ngxsp-yt-thumbnail img{transition:transform .4s var(--yt-ease)}.ngxsp-yt-card:hover .ngxsp-yt-thumbnail img{transform:scale(1.04)}.ngxsp-yt-card.active .ngxsp-yt-thumbnail{box-shadow:0 0 0 2px var(--yt-accent)}.ngxsp-yt-cardCopy{min-width:0;display:grid;gap:2px;padding:0 2px}.ngxsp-yt-cardCopy strong{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;font-size:13px;font-weight:500;line-height:1.35}.ngxsp-yt-card:hover .ngxsp-yt-cardCopy strong{color:var(--yt-text)}.ngxsp-yt-card.active .ngxsp-yt-cardCopy strong{color:var(--yt-accent)}.ngxsp-yt-cardCopy span,.ngxsp-yt-cardCopy small{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ngxsp-yt-cardCopy span{color:var(--yt-text-2);font-size:12px}.ngxsp-yt-cardCopy small{color:var(--yt-text-3);font-size:11px}.ngxsp-yt-grid.list .ngxsp-yt-card{grid-template-columns:160px minmax(0,1fr);align-items:center;gap:14px;padding:6px;border-radius:var(--yt-r-md)}.ngxsp-yt-grid.list .ngxsp-yt-card:hover{background:var(--yt-s1)}.ngxsp-yt-sheetHeader,.ngxsp-yt-queueOpen{display:none}.ngxsp-yt-textButton{border:1px solid var(--yt-line);border-radius:var(--yt-r-sm);background:var(--yt-s2);color:var(--yt-text);font:inherit;font-size:13px;padding:6px 12px;cursor:pointer}.ngxsp-yt-panelTabs{display:flex;flex-wrap:wrap;gap:6px;padding:12px 8px 8px}.ngxsp-yt-panelPill{padding:6px 11px;border:1px solid var(--yt-line);border-radius:999px;background:transparent;color:inherit;font:inherit;font-size:12px;line-height:1.2;cursor:pointer}.ngxsp-yt-panelPill.active{background:var(--yt-accent-strong);border-color:transparent;color:var(--yt-on-accent);font-weight:600}.ngxsp-yt-panelStack{display:flex;flex-direction:column;gap:10px;min-height:0}.ngxsp-yt-stackItem{border:1px solid var(--yt-line);border-radius:10px;overflow:hidden;display:flex;flex-direction:column;min-height:0}.ngxsp-yt-stackItem.collapsed{flex:0 0 auto}.ngxsp-yt-stackHead{display:flex;align-items:center;gap:8px;padding:7px 8px 7px 11px;background:var(--yt-surface-2,rgba(127,127,127,.08))}.ngxsp-yt-stackHead strong{font-size:12.5px;font-weight:600}.ngxsp-yt-stackHead small{color:var(--yt-text-2);font-size:11px}.ngxsp-yt-stackIcon{display:grid;place-items:center;width:24px;height:24px;padding:0;border:0;border-radius:6px;background:transparent;color:inherit;cursor:pointer;opacity:.65}.ngxsp-yt-stackIcon:hover,.ngxsp-yt-stackIcon:focus-visible{opacity:1}.ngxsp-yt-stackHead .ngxsp-yt-stackIcon:first-of-type{margin-left:auto}.ngxsp-yt-stackItem svg{width:14px;height:14px}.ngxsp-yt-suggestions{min-width:0;min-height:0;display:flex;flex-direction:column;border-left:1px solid var(--yt-line);background:var(--yt-s0);overflow:hidden}.ngxsp-yt-cinema .ngxsp-yt-suggestions{border-left:0;border-top:1px solid var(--yt-line);background:transparent;overflow:visible;padding:0 8px 24px}.ngxsp-yt-suggestionList{flex:1;min-height:0;display:grid;align-content:start;gap:2px;padding:0 8px 12px;overflow:auto}.ngxsp-yt-cinema .ngxsp-yt-suggestionList{grid-template-columns:repeat(auto-fill,minmax(320px,1fr));overflow:visible}.ngxsp-yt-suggestion{position:relative;min-width:0;border-radius:var(--yt-r-md);transition:background-color var(--yt-dur) var(--yt-ease)}.ngxsp-yt-suggestion:hover,.ngxsp-yt-suggestion:focus-within{background:var(--yt-s2)}.ngxsp-yt-suggestion.active{background:var(--yt-accent-soft)}.ngxsp-yt-suggestionMain{width:100%;min-width:0;display:grid;grid-template-columns:140px minmax(0,1fr);align-items:start;gap:12px;padding:8px;border:0;border-radius:inherit;background:transparent;text-align:left}.ngxsp-yt-suggestion.managed .ngxsp-yt-suggestionMain{padding-right:44px}.ngxsp-yt-suggestionThumb{position:relative;aspect-ratio:16 / 9;overflow:hidden;border-radius:6px;background:var(--yt-s3)}.ngxsp-yt-suggestionThumb .ngxsp-yt-nowBadge{left:5px;right:auto;top:5px;width:22px;height:22px}.ngxsp-yt-suggestionCopy{min-width:0;display:grid;gap:3px;padding-top:1px}.ngxsp-yt-suggestionCopy strong{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;font-size:13px;font-weight:500;line-height:1.3}.ngxsp-yt-suggestion.active .ngxsp-yt-suggestionCopy strong{color:var(--yt-accent)}.ngxsp-yt-suggestionCopy span,.ngxsp-yt-suggestionCopy small{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ngxsp-yt-suggestionCopy span{color:var(--yt-text-2);font-size:12px}.ngxsp-yt-suggestionCopy small{color:var(--yt-text-3);font-size:11px}.ngxsp-yt-queueControls{position:absolute;top:4px;right:4px;display:grid;gap:0;opacity:0;transition:opacity var(--yt-dur) var(--yt-ease)}.ngxsp-yt-suggestion:hover .ngxsp-yt-queueControls,.ngxsp-yt-suggestion:focus-within .ngxsp-yt-queueControls,.ngxsp-yt-suggestion.active .ngxsp-yt-queueControls{opacity:1}.ngxsp-yt-queueControls button{width:32px;height:32px;display:grid;place-items:center;padding:0;border:0;border-radius:5px;background:transparent;color:var(--yt-text-2)}.ngxsp-yt-queueControls button:hover:not(:disabled){background:var(--yt-s3);color:var(--yt-text)}.ngxsp-yt-queueControls button:disabled{opacity:.3;cursor:default}.ngxsp-yt-queueControls ngxsp-smart-icon{width:15px;height:15px}.ngxsp-yt-backButton{display:inline-flex;align-items:center;gap:2px;max-width:100%;min-height:32px;margin:0 0 2px -6px;padding:4px 10px 4px 4px;border:0;border-radius:999px;background:transparent;color:var(--yt-text-2);font:inherit;font-size:12px;font-weight:600;cursor:pointer}.ngxsp-yt-backButton ngxsp-smart-icon{flex:none;width:16px;height:16px}.ngxsp-yt-backButton span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ngxsp-yt-backButton:hover{background:var(--yt-s2);color:var(--yt-text)}.ngxsp-yt-sideGroupTitle{grid-column:1 / -1;padding:12px 8px 4px;color:var(--yt-text-2);font-size:12px;font-weight:600;letter-spacing:.02em;text-transform:uppercase}.ngxsp-yt-sideGroupTitle:first-child{padding-top:0}@media(min-width:861px){.ngxsp-yt-sectionsBeside .ngxsp-yt-library{display:none}}.ngxsp-yt-emptyList{margin:8px;padding:28px 16px;border:1px dashed var(--yt-line-strong);border-radius:var(--yt-r-md);color:var(--yt-text-2);text-align:center}.ngxsp-yt-miniPlayer{flex:0 0 auto;display:grid;grid-template-columns:88px minmax(0,1fr) auto auto;align-items:center;gap:10px;margin:0 8px 8px;padding:8px;border:1px solid var(--yt-line);border-radius:var(--yt-r-md);background:var(--yt-s1)}.ngxsp-yt-cinema .ngxsp-yt-miniPlayer{max-width:420px}.ngxsp-yt-miniArtwork{position:relative;aspect-ratio:16 / 9;overflow:hidden;padding:0;border:0;border-radius:5px;background:var(--yt-s3)}.ngxsp-yt-miniPlayBadge{position:absolute;inset:0;display:grid;place-items:center;background:#00000059;color:#fff;opacity:0;transition:opacity var(--yt-dur) var(--yt-ease)}.ngxsp-yt-miniArtwork:hover .ngxsp-yt-miniPlayBadge,.ngxsp-yt-miniPlayer.playing .ngxsp-yt-miniPlayBadge{opacity:1}.ngxsp-yt-miniDetails{min-width:0;display:grid;gap:1px}.ngxsp-yt-miniDetails small{color:var(--yt-text-2);font-size:11px;font-weight:600}.ngxsp-yt-miniDetails strong,.ngxsp-yt-miniDetails span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ngxsp-yt-miniDetails strong{font-size:12px;font-weight:600}.ngxsp-yt-miniDetails span{color:var(--yt-text-2);font-size:11px}.ngxsp-yt-miniControl,.ngxsp-yt-miniClose{width:32px;height:32px;display:grid;place-items:center;padding:0;border:0;border-radius:999px;background:transparent;color:var(--yt-text-2)}.ngxsp-yt-miniControl{background:var(--yt-text);color:var(--yt-bg)}.ngxsp-yt-miniClose:hover{background:var(--yt-s3);color:var(--yt-text)}.ngxsp-yt-miniControl ngxsp-smart-icon,.ngxsp-yt-miniClose ngxsp-smart-icon{width:15px;height:15px}.ngxsp-yt-miniRestore{flex:0 0 auto;margin:0 16px 12px;color:var(--yt-text-2)}.ngxsp-yt-inspector{flex:1;min-height:0;display:flex;flex-direction:column}.ngxsp-yt-inspectorHead{display:grid;grid-template-columns:36px minmax(0,1fr) auto;align-items:center;gap:10px;padding:14px 12px 12px 16px;border-bottom:1px solid var(--yt-line)}.ngxsp-yt-inspectorIcon{width:36px;height:36px;display:grid;place-items:center;border-radius:9px;background:var(--yt-accent-soft);color:var(--yt-accent)}.ngxsp-yt-inspectorTitle{min-width:0;display:grid;gap:1px}.ngxsp-yt-inspectorTitle strong{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:14px;font-weight:600}.ngxsp-yt-inspectorTitle small{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--yt-text-2);font-size:12px}.ngxsp-yt-inspectorClose{width:32px;height:32px;display:grid;place-items:center;padding:0;border:0;border-radius:var(--yt-r-sm);background:transparent;color:var(--yt-text-2)}.ngxsp-yt-inspectorClose:hover{background:var(--yt-s2);color:var(--yt-text)}.ngxsp-yt-inspectorScroll{flex:1;min-height:0;display:grid;align-content:start;gap:14px;padding:14px 16px;overflow:auto}.ngxsp-yt-inspectorScroll ngxsp-settings{--ngxsp-settings-surface:var(--yt-s2);--ngxsp-settings-text:var(--yt-text);--ngxsp-settings-muted:var(--yt-text-2);--ngxsp-settings-line:var(--yt-line);--ngxsp-settings-control:var(--yt-s3);--ngxsp-settings-accent:var(--yt-accent);--ngxsp-settings-radius:var(--yt-r-md);gap:20px}.ngxsp-yt-inspectorStatus{color:var(--yt-text-2);font-size:13px;line-height:1.5}.ngxsp-yt-inspectorMetrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}.ngxsp-yt-inspectorMetric{min-width:0;display:grid;gap:3px;padding:10px 12px;border:1px solid var(--yt-line);border-radius:var(--yt-r-md);background:var(--yt-s1)}.ngxsp-yt-inspectorMetric small{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--yt-text-2);font-size:11px}.ngxsp-yt-inspectorMetric strong{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:15px;font-weight:600;font-variant-numeric:tabular-nums}.ngxsp-yt-inspectorMetric.success strong{color:var(--yt-success)}.ngxsp-yt-inspectorMetric.warning strong{color:var(--yt-warning)}.ngxsp-yt-inspectorList{display:grid;border:1px solid var(--yt-line);border-radius:var(--yt-r-md);overflow:hidden}.ngxsp-yt-inspectorItem{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;padding:10px 12px;background:var(--yt-s1)}.ngxsp-yt-inspectorItem+.ngxsp-yt-inspectorItem{border-top:1px solid var(--yt-line)}.ngxsp-yt-inspectorItem>span{min-width:0;display:grid;gap:2px}.ngxsp-yt-inspectorItem strong{font-size:13px;font-weight:500}.ngxsp-yt-inspectorItem small{color:var(--yt-text-2);font-size:12px}.ngxsp-yt-inspectorItem em{flex:0 0 auto;max-width:45%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;padding:1px 7px;border-radius:999px;background:var(--yt-s3);color:var(--yt-text-2);font-size:11px;font-style:normal;font-weight:500}.ngxsp-yt-inspectorItem.success em{background:#3ecf8e24;color:var(--yt-success)}.ngxsp-yt-inspectorItem.warning em{background:#f2a93b24;color:var(--yt-warning)}.ngxsp-yt-resumePrompt{position:absolute;left:14px;bottom:84px;z-index:6;display:flex;align-items:center;flex-wrap:wrap;gap:8px;max-width:calc(100% - 28px);padding:8px 8px 8px 14px;border-radius:var(--yt-r-md);background:#0c0d11cc;box-shadow:0 10px 30px #00000059,0 0 0 1px #ffffff14;-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);color:#f1f1f4;font-size:13px}.ngxsp-yt-resumePrompt span{margin-right:4px}.ngxsp-yt-resumePrompt button{height:32px;padding:0 12px;border:1px solid rgb(255 255 255 / .22);border-radius:999px;background:transparent;color:inherit;font:inherit;font-weight:600;cursor:pointer}.ngxsp-yt-resumePrompt button.primary{border-color:transparent;background:#fff;color:#111}.ngxsp-yt-resumePrompt button:hover{background:#ffffff24}.ngxsp-yt-resumePrompt button.primary:hover{background:#ffffffdb}.ngxsp-yt-compact .ngxsp-yt-resumePrompt{left:8px;bottom:56px;font-size:12px}.ngxsp-yt-playerFrame{container-type:inline-size;--yt-info-w:min(360px, 42cqw)}.ngxsp-yt-infoOpen ngx-smart-player ::ng-deep .ngxsp-vod-centerDock{transform:translate(calc(var(--yt-info-w) / -2 - 5px));transition:transform .2s ease}.ngxsp-yt-infoOverlay{position:absolute;top:10px;right:10px;bottom:76px;z-index:6;width:var(--yt-info-w);display:flex;flex-direction:column;border-radius:var(--yt-r-md);background:#0c0d11b8;box-shadow:0 12px 40px #0006,0 0 0 1px #ffffff14;-webkit-backdrop-filter:blur(14px) saturate(1.2);backdrop-filter:blur(14px) saturate(1.2);color:#f1f1f4;overflow:hidden}.ngxsp-yt-infoTabs{display:flex;align-items:center;gap:2px;padding:6px;border-bottom:1px solid rgb(255 255 255 / .1);overflow-x:auto;scrollbar-width:none}.ngxsp-yt-infoTabs button{flex:none;display:inline-flex;align-items:center;gap:6px;height:32px;padding:0 10px;border:0;border-radius:var(--yt-r-sm);background:transparent;color:#ffffffb8;font:inherit;font-size:12px;white-space:nowrap;cursor:pointer}.ngxsp-yt-infoTabs button ngxsp-smart-icon{width:16px;height:16px}.ngxsp-yt-infoTabs button:hover{background:#ffffff1a;color:#fff}.ngxsp-yt-infoTabs button.active{background:#ffffff24;color:#fff}.ngxsp-yt-infoTabs button:not(.active) span{display:none}.ngxsp-yt-infoTabs .ngxsp-yt-infoClose{margin-left:auto}.ngxsp-yt-infoHead{display:grid;gap:2px;padding:12px 16px 0}.ngxsp-yt-infoHead strong{font-size:15px;font-weight:600}.ngxsp-yt-infoHead small{color:#ffffffa6;font-size:12px}.ngxsp-yt-infoOverlay .ngxsp-yt-inspectorScroll{--yt-text:#f1f1f4;--yt-text-2:rgb(255 255 255 / .68);--yt-line:rgb(255 255 255 / .12);--yt-s1:rgb(255 255 255 / .06);--yt-s2:rgb(255 255 255 / .1)}.ngxsp-yt-infoOverlay .ngxsp-yt-inspectorActions{--yt-line:rgb(255 255 255 / .12);--yt-s1:rgb(255 255 255 / .06);--yt-text:#f1f1f4}@media(min-width:861px){.ngxsp-yt-hasInfoTabs:not(.ngxsp-yt-sources) .ngxsp-yt-toolRow{display:none}}@media(max-width:860px){.ngxsp-yt-infoOverlay{display:none}}.ngxsp-yt-inspectorActions{display:flex;flex-wrap:wrap;gap:8px;padding:12px 16px 16px;border-top:1px solid var(--yt-line)}.ngxsp-yt-veil{position:absolute;inset:0;z-index:3;pointer-events:none;background:#000}.ngxsp-yt-touchBadge{position:absolute;left:50%;top:50%;z-index:6;transform:translate(-50%,-50%);display:grid;justify-items:center;gap:6px;min-width:112px;padding:14px 18px;pointer-events:none;border-radius:var(--yt-r-lg);background:#000000b8;color:#fff}.ngxsp-yt-touchBadge strong{font-size:15px;font-weight:600;font-variant-numeric:tabular-nums}.ngxsp-yt-touchBadge em{display:block;width:88px;height:4px;border-radius:999px;background:#ffffff47;overflow:hidden}.ngxsp-yt-touchBadge em i{display:block;height:100%;border-radius:999px;background:#fff}.ngxsp-yt-cardWrap{position:relative;display:block;min-width:0}.ngxsp-yt-cardMore{position:absolute;top:8px;right:8px;z-index:2;display:grid;place-items:center;width:30px;height:30px;border:0;border-radius:50%;background:#0000009e;color:#fff;opacity:0;cursor:pointer;transition:opacity var(--yt-dur) var(--yt-ease)}.ngxsp-yt-cardWrap:hover .ngxsp-yt-cardMore,.ngxsp-yt-cardMore:focus-visible,.ngxsp-yt-cardWrap:focus-within .ngxsp-yt-cardMore{opacity:1}@media(hover:none){.ngxsp-yt-cardMore{opacity:1}}.ngxsp-yt-cardMenuScrim{position:fixed;inset:0;z-index:60;padding:0;border:0;background:transparent;cursor:default}.ngxsp-yt-cardMenu{position:fixed;z-index:61;min-width:208px;max-width:280px;display:grid;gap:2px;padding:6px;border:1px solid var(--yt-line-strong);border-radius:var(--yt-r-md);background:var(--yt-s2);box-shadow:0 16px 40px #0000007a}.ngxsp-yt-cardMenuTitle{padding:6px 10px 8px;font-size:11px;font-weight:600;color:var(--yt-text-3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ngxsp-yt-cardMenu button{display:flex;align-items:center;gap:10px;min-height:34px;padding:0 10px;border:0;border-radius:var(--yt-r-sm);background:transparent;color:var(--yt-text);font-size:13px;text-align:left;cursor:pointer}.ngxsp-yt-cardMenu button:hover{background:var(--yt-s3)}.ngxsp-yt-cardMenu button.danger{color:var(--yt-danger-text)}.ngxsp-yt-cardMenu button.danger:hover{background:#f25f5c29}.ngxsp-yt-convert{position:absolute;left:12px;right:12px;bottom:12px;z-index:45;display:flex;align-items:center;gap:12px;padding:10px 14px;border:1px solid var(--yt-line-strong);border-radius:var(--yt-r-lg);background:var(--yt-s2);box-shadow:0 12px 32px #0000006b}.ngxsp-yt-convert.asking{border-color:var(--yt-accent)}.ngxsp-yt-convert.danger{border-color:var(--yt-danger)}.ngxsp-yt-convert.danger .ngxsp-yt-convertIcon{background:#f25f5c29;color:var(--yt-danger-text)}.ngxsp-yt-convert.danger .ngxsp-yt-convertActions button.keep{background:var(--yt-danger);color:var(--yt-on-accent)}.ngxsp-yt-convertIcon{display:grid;place-items:center;width:32px;height:32px;flex:0 0 auto;border-radius:50%;background:var(--yt-accent-soft);color:var(--yt-accent)}.ngxsp-yt-convertText{display:flex;flex-direction:column;gap:4px;min-width:0;flex:1 1 auto}.ngxsp-yt-convertText strong{font-size:13px;font-weight:600}.ngxsp-yt-convertText small{font-size:11px;color:var(--yt-text-2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ngxsp-yt-convertText em{display:block;height:4px;border-radius:999px;background:var(--yt-s3);overflow:hidden}.ngxsp-yt-convertText em i{display:block;height:100%;border-radius:999px;background:var(--yt-accent);transition:width var(--yt-dur) var(--yt-ease)}.ngxsp-yt-convertActions{display:flex;gap:8px;flex:0 0 auto}.ngxsp-yt-convertActions button{min-height:32px;padding:0 14px;border:1px solid var(--yt-line-strong);border-radius:999px;background:var(--yt-s1);color:var(--yt-text);font-size:12px;font-weight:600;cursor:pointer}.ngxsp-yt-convertActions button.keep{border-color:transparent;background:var(--yt-accent-strong);color:var(--yt-on-accent)}@media(max-width:560px){.ngxsp-yt-convert{flex-wrap:wrap}.ngxsp-yt-convertActions{width:100%;justify-content:flex-end}}.ngxsp-yt-dropOverlay{position:absolute;inset:12px;z-index:40;display:grid;place-content:center;justify-items:center;gap:10px;border:2px dashed var(--yt-accent);border-radius:14px;background:var(--nsp-scrim, rgb(13 14 17 / .88));-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);color:var(--yt-text);text-align:center;pointer-events:none}.ngxsp-yt-dropIcon{width:56px;height:56px;display:grid;place-items:center;border-radius:14px;background:var(--yt-accent-soft);color:var(--yt-accent)}.ngxsp-yt-dropIcon ngxsp-smart-icon{width:28px;height:28px}.ngxsp-yt-dropOverlay strong{font-size:16px;font-weight:600}.ngxsp-yt-dropOverlay span:last-child{color:var(--yt-text-2)}@media(hover:none),(pointer:coarse){.ngxsp-yt-queueControls,.ngxsp-yt-searchActions{opacity:1}}@media(max-width:1380px){.ngxsp-yt-shell{--yt-side-w:320px}.ngxsp-yt-suggestionMain{grid-template-columns:120px minmax(0,1fr)}.ngxsp-yt-watchActions button>span{display:none}.ngxsp-yt-watchActions button{width:34px;padding:0;justify-content:center}.ngxsp-yt-rating button+button{padding-left:0}}@media(max-width:1180px){.ngxsp-yt-topbar{grid-template-columns:auto minmax(200px,1fr) auto}.ngxsp-yt-status,.ngxsp-yt-sourceButton span{display:none}.ngxsp-yt-sourceButton{width:32px;padding:0;justify-content:center}.ngxsp-yt-mediaHero{grid-template-columns:200px minmax(0,1fr)}}@media(max-width:1040px)and (min-width:861px){.ngxsp-yt-body,.ngxsp-queue-hidden .ngxsp-yt-body{display:block;overflow:auto}.ngxsp-yt-watch{overflow:visible}.ngxsp-yt-suggestions{border-left:0;border-top:1px solid var(--yt-line);background:transparent;overflow:visible;padding:0 16px 24px}.ngxsp-yt-suggestionList{grid-template-columns:repeat(auto-fill,minmax(300px,1fr));overflow:visible}.ngxsp-yt-playerFrame{width:100%}}@media(max-width:860px){.ngxsp-yt-shell{height:auto;min-height:100%;grid-template-columns:minmax(0,1fr);grid-template-rows:auto auto auto;grid-template-areas:\"top\" \"rail\" \"body\";overflow:visible}.ngxsp-yt-topbar{grid-template-columns:minmax(0,1fr) auto;grid-template-rows:48px auto;gap:0 8px;padding:0 12px 10px}.ngxsp-yt-search{grid-column:1 / -1;grid-row:2}.ngxsp-yt-railMenu,.ngxsp-yt-round,.ngxsp-yt-languageSwitch,.ngxsp-yt-status{display:none}.ngxsp-yt-notificationsButton,.ngxsp-yt-windowButton{display:grid}.ngxsp-yt-rail{flex-direction:row;padding:6px 12px;border-right:0;border-bottom:1px solid var(--yt-line);overflow-x:auto}.ngxsp-yt-railButton{width:auto;min-height:36px;flex:0 0 auto;flex-direction:row;gap:6px;padding:0 10px}.ngxsp-yt-railButton span{font-size:12px}.ngxsp-yt-body,.ngxsp-queue-hidden .ngxsp-yt-body,.ngxsp-yt-cinema .ngxsp-yt-body{display:block;overflow:visible}.sheet>.ngxsp-yt-sheetHeader{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 0;position:sticky;top:0;background:var(--yt-s0);z-index:1}.ngxsp-yt-watch{--yt-gutter:12px;gap:14px;padding:12px;overflow:visible}.ngxsp-yt-playerFrame,.ngxsp-yt-cinema .ngxsp-yt-playerFrame{width:100%;min-width:0;border-radius:var(--yt-r-sm)}.ngxsp-yt-transportBar{padding:28px 6px 6px;gap:4px}.ngxsp-yt-transportInfo,.ngxsp-yt-transportCinema{display:none}.ngxsp-yt-transportBar{grid-template-columns:auto auto;justify-content:space-between}.ngxsp-yt-titleGroup h1{font-size:18px}.ngxsp-yt-watchActions>button:nth-of-type(n+3){display:none}.ngxsp-yt-rating button+button{display:none}.ngxsp-yt-toolRow{display:flex;gap:8px;overflow-x:auto;scrollbar-width:none;padding-bottom:2px}.ngxsp-yt-toolRow::-webkit-scrollbar{display:none}.ngxsp-yt-toolRow .ngxsp-yt-tool{flex:0 0 auto;height:44px;grid-template-columns:24px auto;gap:8px}.ngxsp-yt-toolRow .ngxsp-yt-toolIcon{width:24px;height:24px}.ngxsp-yt-toolRow .ngxsp-yt-toolCopy strong{display:none}.ngxsp-yt-toolRow .ngxsp-yt-tool[data-tool=devices],.ngxsp-yt-toolRow .ngxsp-yt-tool[data-tool=cast]{order:-1}.ngxsp-yt-toolRow .ngxsp-yt-toolCopy small{color:var(--yt-text);font-size:13px}.ngxsp-yt-mediaHero{grid-template-columns:1fr;padding:14px}.ngxsp-yt-mediaHero.echoesHeader .ngxsp-yt-mediaPoster,.ngxsp-yt-mediaHero.echoesHeader .ngxsp-yt-mediaKicker,.ngxsp-yt-mediaHero.echoesHeader .ngxsp-yt-mediaDetailBody>h2,.ngxsp-yt-mediaHero.echoesHeader .ngxsp-yt-mediaSubtitle,.ngxsp-yt-mediaHero.echoesHeader .ngxsp-yt-mediaMeta{display:none}.ngxsp-yt-mediaWorkspace{display:none}.ngxsp-yt-episodeRail{padding:14px}.ngxsp-yt-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:14px 10px}.ngxsp-yt-grid .ngxsp-yt-card:nth-child(n+5){display:none}.ngxsp-yt-searchResults{grid-template-columns:1fr}.ngxsp-yt-queueOpen{display:flex;align-items:center;gap:10px;width:100%;min-height:44px;padding:0 12px;border:1px solid var(--yt-line);border-radius:var(--yt-r-md);background:var(--yt-s1);color:var(--yt-text);font:inherit;font-size:14px;text-align:left;cursor:pointer}.ngxsp-yt-queueOpen span{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ngxsp-yt-queueOpen small{color:var(--yt-text-2);font-size:12px}.ngxsp-yt-suggestions,.ngxsp-yt-cinema .ngxsp-yt-suggestions{display:none}.ngxsp-yt-suggestions.sheet,.ngxsp-yt-cinema .ngxsp-yt-suggestions.sheet{display:flex;position:fixed;inset:0;z-index:40;border:0;overflow-y:auto;background:var(--yt-s0);padding:env(safe-area-inset-top,0px) 12px calc(24px + env(safe-area-inset-bottom,0px))}.ngxsp-yt-suggestionList,.ngxsp-yt-cinema .ngxsp-yt-suggestionList{grid-template-columns:1fr;overflow:visible}.ngxsp-yt-suggestionList .ngxsp-yt-suggestion:nth-child(n+3){display:grid}.ngxsp-yt-suggestionMain{grid-template-columns:128px minmax(0,1fr)}.ngxsp-yt-miniPlayer{display:none}.ngxsp-yt-library-focus .ngxsp-yt-library{order:3}}@media(max-width:540px){.ngxsp-yt-brand strong{font-size:12px}.ngxsp-yt-mode button{padding:0 8px}.ngxsp-yt-railButton span{display:none}.ngxsp-yt-grid{grid-template-columns:1fr 1fr}}@media(prefers-reduced-motion:reduce){.ngxsp-yt-shell *,.ngxsp-yt-shell *:before,.ngxsp-yt-shell *:after{animation:none!important;transition:none!important}}.ngxsp-yt-body.ngxsp-yt-playing{display:grid;grid-template-columns:minmax(0,1fr) var(--yt-side-w);overflow:hidden;min-height:0}.ngxsp-queue-hidden .ngxsp-yt-body.ngxsp-yt-playing{grid-template-columns:minmax(0,1fr)}@media(max-width:1024px){.ngxsp-yt-shell.ngxsp-yt-playerView .ngxsp-yt-topbar,.ngxsp-yt-shell.ngxsp-yt-playerView .ngxsp-yt-rail{display:none}.ngxsp-yt-shell.ngxsp-yt-playerView{height:100vh;height:100dvh;min-height:0;grid-template-areas:none;grid-template-columns:minmax(0,1fr);grid-template-rows:minmax(0,1fr)}.ngxsp-yt-shell.ngxsp-yt-playerView .ngxsp-yt-body{grid-area:auto}.ngxsp-yt-shell.ngxsp-yt-playerView .ngxsp-yt-body{grid-template-columns:minmax(0,1fr);height:100%}.ngxsp-yt-shell.ngxsp-yt-playerView .ngxsp-yt-watch.player{height:100%;padding:0}.ngxsp-yt-shell.ngxsp-yt-playerView .ngxsp-yt-playerShell{flex:1 1 auto;min-height:0}.ngxsp-yt-shell.ngxsp-yt-playerView .ngxsp-yt-playerFrame{height:100%;aspect-ratio:auto;border-radius:0}.ngxsp-yt-shell.ngxsp-yt-playerView .ngxsp-yt-playerBack{display:inline-flex}}\n"] }]
3353
+ }], ctorParameters: () => [], propDecorators: { config: [{
3354
+ type: Input
3355
+ }], brandLogoSrc: [{
3356
+ type: Input
3357
+ }], plugins: [{
3358
+ type: Input
3359
+ }], ports: [{
3360
+ type: Input
3361
+ }], engines: [{
3362
+ type: Input
3363
+ }], debug: [{
3364
+ type: Input
3365
+ }], startTimeSec: [{
3366
+ type: Input
3367
+ }], playRequest: [{
3368
+ type: Input
3369
+ }], canPrev: [{
3370
+ type: Input
3371
+ }], canNext: [{
3372
+ type: Input
3373
+ }], previousLabel: [{
3374
+ type: Input
3375
+ }], playerBackLabel: [{
3376
+ type: Input
3377
+ }], nextLabel: [{
3378
+ type: Input
3379
+ }], playLabel: [{
3380
+ type: Input
3381
+ }], pauseLabel: [{
3382
+ type: Input
3383
+ }], transportLabel: [{
3384
+ type: Input
3385
+ }], cinemaMode: [{
3386
+ type: Input
3387
+ }], cinemaLabel: [{
3388
+ type: Input
3389
+ }], cinemaActiveLabel: [{
3390
+ type: Input
3391
+ }], shuffleActive: [{
3392
+ type: Input
3393
+ }], shuffleLabel: [{
3394
+ type: Input
3395
+ }], repeatMode: [{
3396
+ type: Input
3397
+ }], repeatLabel: [{
3398
+ type: Input
3399
+ }], hasPlaylistControls: [{
3400
+ type: Input
3401
+ }], castEnabled: [{
3402
+ type: Input
3403
+ }], castConnected: [{
3404
+ type: Input
3405
+ }], resumeOffer: [{
3406
+ type: Input
3407
+ }], infoTabs: [{
3408
+ type: Input
3409
+ }], infoLabel: [{
3410
+ type: Input
3411
+ }], infoPanel: [{
3412
+ type: Input
3413
+ }], playerMenuLabels: [{
3414
+ type: Input
3415
+ }], heroEyebrow: [{
3416
+ type: Input
3417
+ }], heroTitle: [{
3418
+ type: Input
3419
+ }], heroSubtitle: [{
3420
+ type: Input
3421
+ }], heroMeta: [{
3422
+ type: Input
3423
+ }], heroArtworkUrl: [{
3424
+ type: Input
3425
+ }], preferPosterPreview: [{
3426
+ type: Input
3427
+ }], runtimeModeLabel: [{
3428
+ type: Input
3429
+ }], onlineLabel: [{
3430
+ type: Input
3431
+ }], offlineLabel: [{
3432
+ type: Input
3433
+ }], navigationLabel: [{
3434
+ type: Input
3435
+ }], menuLabel: [{
3436
+ type: Input
3437
+ }], searchPlaceholder: [{
3438
+ type: Input
3439
+ }], searchLabel: [{
3440
+ type: Input
3441
+ }], searchResultsTitle: [{
3442
+ type: Input
3443
+ }], searchResultsSummary: [{
3444
+ type: Input
3445
+ }], searchResultsEmptyTitle: [{
3446
+ type: Input
3447
+ }], searchResultsEmptyBody: [{
3448
+ type: Input
3449
+ }], searchClearLabel: [{
3450
+ type: Input
3451
+ }], searchFilterLabel: [{
3452
+ type: Input
3453
+ }], searchFilters: [{
3454
+ type: Input
3455
+ }], searchActionLabels: [{
3456
+ type: Input
3457
+ }], notificationsLabel: [{
3458
+ type: Input
3459
+ }], settingsLabel: [{
3460
+ type: Input
3461
+ }], sourceChangeLabel: [{
3462
+ type: Input
3463
+ }], sourceChangeTitle: [{
3464
+ type: Input
3465
+ }], sourceDropTitle: [{
3466
+ type: Input
3467
+ }], sourceDropHint: [{
3468
+ type: Input
3469
+ }], sourceChangeAccept: [{
3470
+ type: Input
3471
+ }], folderImportLabel: [{
3472
+ type: Input
3473
+ }], libraryStatus: [{
3474
+ type: Input
3475
+ }], touchAdjust: [{
3476
+ type: Input
3477
+ }], brightness: [{
3478
+ type: Input
3479
+ }], brightnessChange: [{
3480
+ type: Output
3481
+ }], cardActionsLabel: [{
3482
+ type: Input
3483
+ }], cardMenuCloseLabel: [{
3484
+ type: Input
3485
+ }], cardAction: [{
3486
+ type: Output
3487
+ }], promptBar: [{
3488
+ type: Input
3489
+ }], promptAnswer: [{
3490
+ type: Output
3491
+ }], libraryBusy: [{
3492
+ type: Input
3493
+ }], sourceTypeLocalLabel: [{
3494
+ type: Input
3495
+ }], sourceTypeStreamLabel: [{
3496
+ type: Input
3497
+ }], languageLabel: [{
3498
+ type: Input
3499
+ }], activeLanguage: [{
3500
+ type: Input
3501
+ }], languageOptions: [{
3502
+ type: Input
3503
+ }], statusMessage: [{
3504
+ type: Input
3505
+ }], statusKind: [{
3506
+ type: Input
3507
+ }], likeLabel: [{
3508
+ type: Input
3509
+ }], likeActive: [{
3510
+ type: Input
3511
+ }], dislikeLabel: [{
3512
+ type: Input
3513
+ }], shareLabel: [{
3514
+ type: Input
3515
+ }], watchLaterLabel: [{
3516
+ type: Input
3517
+ }], watchLaterActive: [{
3518
+ type: Input
3519
+ }], queueActionLabel: [{
3520
+ type: Input
3521
+ }], queueShortLabel: [{
3522
+ type: Input
3523
+ }], queueAddActiveLabel: [{
3524
+ type: Input
3525
+ }], queueAddActiveShortLabel: [{
3526
+ type: Input
3527
+ }], queueActionLabels: [{
3528
+ type: Input
3529
+ }], moreLabel: [{
3530
+ type: Input
3531
+ }], moreShortLabel: [{
3532
+ type: Input
3533
+ }], browseLibraryLabel: [{
3534
+ type: Input
3535
+ }], trail: [{
3536
+ type: Input
3537
+ }], trailLabel: [{
3538
+ type: Input
3539
+ }], showAllLabel: [{
3540
+ type: Input
3541
+ }], showLessLabel: [{
3542
+ type: Input
3543
+ }], emptyCatalogLabel: [{
3544
+ type: Input
3545
+ }], continueWatchingMeta: [{
3546
+ type: Input
3547
+ }], continueWatching: [{
3548
+ type: Input
3549
+ }], managedQueue: [{
3550
+ type: Input
3551
+ }], upNextItems: [{
3552
+ type: Input
3553
+ }], playlistPanel: [{
3554
+ type: Input
3555
+ }], serverForm: [{
3556
+ type: Input
3557
+ }], showResumeProgress: [{
3558
+ type: Input
3559
+ }], mediaDetail: [{
3560
+ type: Input
3561
+ }], sidePanelTitle: [{
3562
+ type: Input
3563
+ }], sidePanelMeta: [{
3564
+ type: Input
3565
+ }], sections: [{
3566
+ type: Input
3567
+ }], navItems: [{
3568
+ type: Input
3569
+ }], toolCards: [{
3570
+ type: Input
3571
+ }], inspectorPanel: [{
3572
+ type: Input
3573
+ }], inspectorPanels: [{
3574
+ type: Input
3575
+ }], inspectorCloseLabel: [{
3576
+ type: Input
3577
+ }], libraryVisible: [{
3578
+ type: Input
3579
+ }], playerView: [{
3580
+ type: Input
3581
+ }], watchActionsInPlayer: [{
3582
+ type: Input
3583
+ }], showActiveMedia: [{
3584
+ type: Input
3585
+ }], closeLabel: [{
3586
+ type: Input
3587
+ }], queueVisible: [{
3588
+ type: Input
3589
+ }], sidePanelContent: [{
3590
+ type: Input
3591
+ }], sideBackLabel: [{
3592
+ type: Input
3593
+ }], searchTerm: [{
3594
+ type: Input
3595
+ }], runtimeMode: [{
3596
+ type: Input
3597
+ }], runtimeModeInHeader: [{
3598
+ type: Input
3599
+ }], activeViewMode: [{
3600
+ type: Input
3601
+ }], libraryTitle: [{
3602
+ type: Input
3603
+ }], nowPlayingLabel: [{
3604
+ type: Input
3605
+ }], fullscreenHintLabel: [{
3606
+ type: Input
3607
+ }], fullscreenLabel: [{
3608
+ type: Input
3609
+ }], readyStatusLabel: [{
3610
+ type: Input
3611
+ }], playingStatusLabel: [{
3612
+ type: Input
3613
+ }], pausedStatusLabel: [{
3614
+ type: Input
3615
+ }], bufferingStatusLabel: [{
3616
+ type: Input
3617
+ }], loadingStatusLabel: [{
3618
+ type: Input
3619
+ }], endedStatusLabel: [{
3620
+ type: Input
3621
+ }], errorStatusLabel: [{
3622
+ type: Input
3623
+ }], miniPlayerVisible: [{
3624
+ type: Input
3625
+ }], miniPlayerMeta: [{
3626
+ type: Input
3627
+ }], miniPlayerPlayLabel: [{
3628
+ type: Input
3629
+ }], miniPlayerPauseLabel: [{
3630
+ type: Input
3631
+ }], miniPlayerFocusLabel: [{
3632
+ type: Input
3633
+ }], miniPlayerCloseLabel: [{
3634
+ type: Input
3635
+ }], miniPlayerRestoreLabel: [{
3636
+ type: Input
3637
+ }], outputVolume: [{
3638
+ type: Input
3639
+ }], outputVolumeLabel: [{
3640
+ type: Input
3641
+ }], outputMuteLabel: [{
3642
+ type: Input
3643
+ }], outputUnmuteLabel: [{
3644
+ type: Input
3645
+ }], ended: [{
3646
+ type: Output
3647
+ }], videoSize: [{
3648
+ type: Output
3649
+ }], progress: [{
3650
+ type: Output
3651
+ }], compactMode: [{
3652
+ type: Input
3653
+ }], compactModes: [{
3654
+ type: Input
3655
+ }], menuLayout: [{
3656
+ type: Input
3657
+ }], compactMiniLabel: [{
3658
+ type: Input
3659
+ }], compactRestoreLabel: [{
3660
+ type: Input
3661
+ }], compactModeChange: [{
3662
+ type: Output
3663
+ }], revealFileLabel: [{
3664
+ type: Input
3665
+ }], revealFile: [{
3666
+ type: Output
3667
+ }], prev: [{
3668
+ type: Output
3669
+ }], next: [{
3670
+ type: Output
3671
+ }], toggleShuffle: [{
3672
+ type: Output
3673
+ }], cycleRepeat: [{
3674
+ type: Output
3675
+ }], cast: [{
3676
+ type: Output
3677
+ }], infoToggle: [{
3678
+ type: Output
3679
+ }], sideBack: [{
3680
+ type: Output
3681
+ }], playerBack: [{
3682
+ type: Output
3683
+ }], resumeChoice: [{
3684
+ type: Output
3685
+ }], infoTabSelect: [{
3686
+ type: Output
3687
+ }], subtitlesLoaded: [{
3688
+ type: Output
3689
+ }], selectContinueWatching: [{
3690
+ type: Output
3691
+ }], selectSectionItem: [{
3692
+ type: Output
3693
+ }], selectDetailEpisode: [{
3694
+ type: Output
3695
+ }], chapterSeek: [{
3696
+ type: Output
3697
+ }], navSelect: [{
3698
+ type: Output
3699
+ }], toolSelect: [{
3700
+ type: Output
3701
+ }], detailAction: [{
3702
+ type: Output
3703
+ }], inspectorAction: [{
3704
+ type: Output
3705
+ }], settingChange: [{
3706
+ type: Output
3707
+ }], settingAction: [{
3708
+ type: Output
3709
+ }], closeInspector: [{
3710
+ type: Output
3711
+ }], panelClosed: [{
3712
+ type: Output
3713
+ }], openSource: [{
3714
+ type: Output
3715
+ }], toggleLibrary: [{
3716
+ type: Output
3717
+ }], trailStep: [{
3718
+ type: Output
3719
+ }], toggleQueue: [{
3720
+ type: Output
3721
+ }], toggleCinema: [{
3722
+ type: Output
3723
+ }], miniPlayerFocus: [{
3724
+ type: Output
3725
+ }], miniPlayerClose: [{
3726
+ type: Output
3727
+ }], miniPlayerRestore: [{
3728
+ type: Output
3729
+ }], queueAddActive: [{
3730
+ type: Output
3731
+ }], connectServer: [{
3732
+ type: Output
3733
+ }], discoverServers: [{
3734
+ type: Output
3735
+ }], connectDiscoveredServer: [{
3736
+ type: Output
3737
+ }], selectServer: [{
3738
+ type: Output
3739
+ }], disconnectServer: [{
3740
+ type: Output
3741
+ }], createPlaylist: [{
3742
+ type: Output
3743
+ }], selectPlaylist: [{
3744
+ type: Output
3745
+ }], addToPlaylist: [{
3746
+ type: Output
3747
+ }], removeFromPlaylist: [{
3748
+ type: Output
3749
+ }], renamePlaylist: [{
3750
+ type: Output
3751
+ }], deletePlaylist: [{
3752
+ type: Output
3753
+ }], watchLaterToggle: [{
3754
+ type: Output
3755
+ }], queueMoveUp: [{
3756
+ type: Output
3757
+ }], queueMoveDown: [{
3758
+ type: Output
3759
+ }], queueRemove: [{
3760
+ type: Output
3761
+ }], searchFilterChange: [{
3762
+ type: Output
3763
+ }], searchResultQueueAdd: [{
3764
+ type: Output
3765
+ }], searchResultWatchLater: [{
3766
+ type: Output
3767
+ }], searchResultFavorite: [{
3768
+ type: Output
3769
+ }], searchResultDownload: [{
3770
+ type: Output
3771
+ }], search: [{
3772
+ type: Output
3773
+ }], searchTermChange: [{
3774
+ type: Output
3775
+ }], viewModeChange: [{
3776
+ type: Output
3777
+ }], runtimeModeChange: [{
3778
+ type: Output
3779
+ }], primaryPlay: [{
3780
+ type: Output
3781
+ }], primaryPlaybackToggle: [{
3782
+ type: Output
3783
+ }], playerStateChange: [{
3784
+ type: Output
3785
+ }], sourceFileChange: [{
3786
+ type: Output
3787
+ }], addFolder: [{
3788
+ type: Output
3789
+ }], languageChange: [{
3790
+ type: Output
3791
+ }], outputVolumeChange: [{
3792
+ type: Output
3793
+ }], outputMuteToggle: [{
3794
+ type: Output
3795
+ }], cardMenuBox: [{
3796
+ type: ViewChild,
3797
+ args: ['cardMenuBox']
3798
+ }], smartPlayer: [{
3799
+ type: ViewChild,
3800
+ args: [SmartPlayerComponent]
3801
+ }], contentScroll: [{
3802
+ type: ViewChild,
3803
+ args: ['contentScroll']
3804
+ }], queueListEl: [{
3805
+ type: ViewChild,
3806
+ args: ['queueList']
3807
+ }], videoFileInput: [{
3808
+ type: ViewChild,
3809
+ args: ['videoFileInput']
3810
+ }], queuePanel: [{
3811
+ type: ViewChild,
3812
+ args: ['queuePanel']
3813
+ }] } });
3814
+
3815
+ class NgxSmartPlayerVideoUiModule {
3816
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.33", ngImport: i0, type: NgxSmartPlayerVideoUiModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
3817
+ static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "20.3.33", ngImport: i0, type: NgxSmartPlayerVideoUiModule, imports: [NgxSmartPlayerVideoUiComponent], exports: [NgxSmartPlayerVideoUiComponent] });
3818
+ static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "20.3.33", ngImport: i0, type: NgxSmartPlayerVideoUiModule, imports: [NgxSmartPlayerVideoUiComponent] });
3819
+ }
3820
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.33", ngImport: i0, type: NgxSmartPlayerVideoUiModule, decorators: [{
3821
+ type: NgModule,
3822
+ args: [{
3823
+ imports: [NgxSmartPlayerVideoUiComponent],
3824
+ exports: [NgxSmartPlayerVideoUiComponent],
3825
+ }]
3826
+ }] });
3827
+
3828
+ /**
3829
+ * Generated bundle index. Do not edit.
3830
+ */
3831
+
3832
+ export { NgxSmartPlayerVideoUiComponent, NgxSmartPlayerVideoUiModule };
3833
+ //# sourceMappingURL=ngxsplayer-ngx-smart-player-ui-video.mjs.map