@guardvideo/player-sdk 1.3.0 → 2.0.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.
package/dist/index.esm.js CHANGED
@@ -691,238 +691,664 @@ const ICON = {
691
691
  pause: '<rect x="6" y="4" width="4" height="16" rx="1"/><rect x="14" y="4" width="4" height="16" rx="1"/>',
692
692
  replay: '<path d="M12 5V1L7 6l5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z"/>',
693
693
  volHigh: '<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/>',
694
+ volMid: '<path d="M18.5 12c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM5 9v6h4l5 5V4L9 9H5z"/>',
695
+ volLow: '<path d="M7 9v6h4l5 5V4l-5 5H7z"/>',
694
696
  volMute: '<path d="M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z"/>',
695
697
  fsEnter: '<path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/>',
696
698
  fsExit: '<path d="M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z"/>',
697
699
  settings: '<path d="M19.14,12.94c0.04-0.3,0.06-0.61,0.06-0.94c0-0.32-0.02-0.64-0.07-0.94l2.03-1.58c0.18-0.14,0.23-0.41,0.12-0.61l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39,0.96c-0.5-0.38-1.03-0.7-1.62-0.94L14.4,2.81c-0.04-0.24-0.24-0.41-0.48-0.41h-3.84c-0.24,0-0.43,0.17-0.47,0.41L9.25,5.35C8.66,5.59,8.12,5.92,7.63,6.29L5.24,5.33c-0.22-0.08-0.47,0-0.59,0.22L2.74,8.87C2.62,9.08,2.66,9.34,2.86,9.48l2.03,1.58C4.84,11.36,4.8,11.69,4.8,12s0.02,0.64,0.07,0.94l-2.03,1.58c-0.18,0.14-0.23,0.41-0.12,0.61l1.92,3.32c0.12,0.22,0.37,0.29,0.59,0.22l2.39-.96c0.5,0.38,1.03,0.7,1.62,0.94l0.36,2.54c0.05,0.24,0.24,0.41,0.48,0.41h3.84c0.24,0,0.44-0.17,0.47-0.41l0.36-2.54c0.59-0.24,1.13-0.56,1.62-0.94l2.39,0.96c0.22,0.08,0.47,0,0.59-0.22l1.92-3.32c0.12-0.22,0.07-0.47-0.12-0.61L19.14,12.94zM12,15.6c-1.98,0-3.6-1.62-3.6-3.6s1.62-3.6,3.6-3.6s3.6,1.62,3.6,3.6S13.98,15.6,12,15.6z"/>',
698
- shield: '<path fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>',
699
- };
700
- const SPEEDS = [0.5, 0.75, 1, 1.25, 1.5, 2];
700
+ shield: '<path fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/><path d="M9 12l2 2 4-4" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>'};
701
+ const SPEEDS = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2];
701
702
  let _stylesInjected = false;
702
703
  function injectStyles() {
703
704
  if (_stylesInjected)
704
705
  return;
705
706
  _stylesInjected = true;
706
707
  const css = `
707
- /* ── GuardVideo Player UI ─────────────────────────────────── */
708
+ /* ── GuardVideo Player UI v2 ──────────────────────────────── */
709
+
710
+ /* Google Font import — Outfit for labels, DM Mono for time */
711
+ @import url('https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&family=DM+Mono:wght@400;500&display=swap');
712
+
713
+ /* ── Root ─────────────────────────────────────────────────── */
708
714
  .gvp-root {
709
- --gvp-accent: #44c09b;
715
+ --gvp-accent: #00e5a0;
716
+ --gvp-accent-dim: rgba(0, 229, 160, 0.18);
717
+ --gvp-accent-glow: rgba(0, 229, 160, 0.35);
718
+ --gvp-glass-bg: rgba(8, 8, 14, 0.72);
719
+ --gvp-glass-bdr: rgba(255,255,255,0.07);
720
+ --gvp-text: rgba(255,255,255,0.92);
721
+ --gvp-text-dim: rgba(255,255,255,0.45);
722
+ --gvp-radius: 12px;
723
+ --gvp-font: 'Outfit', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
724
+ --gvp-mono: 'DM Mono', 'SF Mono', 'Fira Mono', monospace;
725
+
710
726
  position: relative;
711
- background: #000;
712
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
713
- border-radius: 10px;
727
+ background: #050508;
728
+ font-family: var(--gvp-font);
729
+ border-radius: var(--gvp-radius);
714
730
  overflow: hidden;
715
731
  -webkit-user-select: none;
732
+ -moz-user-select: none;
733
+ -ms-user-select: none;
716
734
  user-select: none;
717
735
  outline: none;
736
+
737
+ /* Subtle inner vignette for cinema depth */
738
+ box-shadow:
739
+ inset 0 0 80px rgba(0,0,0,0.55),
740
+ 0 24px 80px rgba(0,0,0,0.6);
718
741
  }
719
- .gvp-root:focus-visible { outline: 2px solid var(--gvp-accent); outline-offset: 1px; }
720
742
 
721
- .gvp-video { display: block; width: 100%; height: 100%; object-fit: contain; }
743
+ .gvp-root:focus-visible {
744
+ outline: 2px solid var(--gvp-accent);
745
+ outline-offset: 2px;
746
+ }
722
747
 
723
- /* ── Utility: hide elements ────────────────────────────────── */
724
- .gvp-hidden { display: none !important; }
725
- /* Controls use opacity/translate so they animate out gracefully */
726
- .gvp-controls-hidden { opacity: 0; transform: translateY(6px); pointer-events: none; }
748
+ /* ── Video ────────────────────────────────────────────────── */
749
+ .gvp-video {
750
+ display: block;
751
+ width: 100%;
752
+ height: 100%;
753
+ -o-object-fit: contain;
754
+ object-fit: contain;
755
+ }
756
+
757
+ /* ── Utility ──────────────────────────────────────────────── */
758
+ .gvp-hidden { display: none !important; }
759
+ .gvp-controls-hidden { opacity: 0; -webkit-transform: translateY(8px); transform: translateY(8px); pointer-events: none; }
760
+
761
+ /* ── Gradient overlay (top + bottom) ─────────────────────── */
762
+ .gvp-root::before,
763
+ .gvp-root::after {
764
+ content: '';
765
+ position: absolute;
766
+ left: 0; right: 0;
767
+ pointer-events: none;
768
+ z-index: 2;
769
+ }
770
+ .gvp-root::before {
771
+ top: 0;
772
+ height: 90px;
773
+ background: -webkit-linear-gradient(bottom, transparent, rgba(0,0,0,0.55));
774
+ background: linear-gradient(to bottom, rgba(0,0,0,0.55), transparent);
775
+ border-radius: var(--gvp-radius) var(--gvp-radius) 0 0;
776
+ }
777
+ .gvp-root::after {
778
+ bottom: 0;
779
+ height: 160px;
780
+ background: -webkit-linear-gradient(top, transparent, rgba(0,0,0,0.88));
781
+ background: linear-gradient(to bottom, transparent, rgba(0,0,0,0.88));
782
+ border-radius: 0 0 var(--gvp-radius) var(--gvp-radius);
783
+ }
727
784
 
728
- /* ── Spinner ─────────────────────────────────────────────── */
785
+ /* ── Spinner ──────────────────────────────────────────────── */
729
786
  .gvp-spinner {
730
- position: absolute; inset: 0; display: flex;
731
- align-items: center; justify-content: center;
732
- pointer-events: none; background: rgba(0,0,0,.55); border-radius: 10px;
787
+ position: absolute; inset: 0;
788
+ display: -webkit-box; display: -ms-flexbox; display: flex;
789
+ -webkit-box-align: center; -ms-flex-align: center; align-items: center;
790
+ -webkit-box-pack: center; -ms-flex-pack: center; justify-content: center;
791
+ pointer-events: none;
792
+ z-index: 8;
733
793
  }
734
794
  .gvp-spinner-ring {
735
- width: 48px; height: 48px;
736
- border: 3px solid rgba(255,255,255,.15);
795
+ width: 44px; height: 44px;
796
+ border: 2.5px solid rgba(255,255,255,0.08);
737
797
  border-top-color: var(--gvp-accent);
738
- border-radius: 50%; animation: gvp-spin .8s linear infinite;
798
+ border-radius: 50%;
799
+ -webkit-animation: gvp-spin 0.75s linear infinite;
800
+ animation: gvp-spin 0.75s linear infinite;
801
+ box-shadow: 0 0 16px var(--gvp-accent-glow);
739
802
  }
740
- @keyframes gvp-spin { to { transform: rotate(360deg); } }
803
+ @-webkit-keyframes gvp-spin { to { -webkit-transform: rotate(360deg); transform: rotate(360deg); } }
804
+ @keyframes gvp-spin { to { -webkit-transform: rotate(360deg); transform: rotate(360deg); } }
741
805
 
742
- /* ── Centre play button ────────────────────────────────────── */
806
+ /* ── Centre play overlay ──────────────────────────────────── */
743
807
  .gvp-center-play {
744
- position: absolute; inset: 0; z-index: 3;
745
- display: flex; align-items: center; justify-content: center;
746
- cursor: pointer; background: rgba(0,0,0,.3);
747
- transition: background .2s; border-radius: 10px;
808
+ position: absolute; inset: 0; z-index: 6;
809
+ display: -webkit-box; display: -ms-flexbox; display: flex;
810
+ -webkit-box-align: center; -ms-flex-align: center; align-items: center;
811
+ -webkit-box-pack: center; -ms-flex-pack: center; justify-content: center;
812
+ cursor: pointer;
813
+ background: transparent;
814
+ -webkit-transition: background 0.25s; transition: background 0.25s;
815
+ border-radius: var(--gvp-radius);
748
816
  }
749
- .gvp-center-play:hover { background: rgba(0,0,0,.45); }
817
+ .gvp-center-play:hover { background: rgba(0,0,0,0.22); }
818
+
750
819
  .gvp-center-play-btn {
751
- width: 72px; height: 72px;
752
- background: rgba(255,255,255,.12); backdrop-filter: blur(8px);
753
- border-radius: 50%; display: flex; align-items: center; justify-content: center;
754
- border: 2px solid rgba(255,255,255,.25);
755
- transition: background .2s, transform .15s; color: #fff;
820
+ position: relative;
821
+ width: 76px; height: 76px;
822
+ border-radius: 50%;
823
+ display: -webkit-box; display: -ms-flexbox; display: flex;
824
+ -webkit-box-align: center; -ms-flex-align: center; align-items: center;
825
+ -webkit-box-pack: center; -ms-flex-pack: center; justify-content: center;
826
+ color: #fff;
827
+ /* Glass morphism */
828
+ background: rgba(255,255,255,0.10);
829
+ -webkit-backdrop-filter: blur(16px) saturate(180%);
830
+ backdrop-filter: blur(16px) saturate(180%);
831
+ border: 1px solid rgba(255,255,255,0.18);
832
+ -webkit-transition: background 0.2s, -webkit-transform 0.2s, -webkit-box-shadow 0.2s;
833
+ transition: background 0.2s, transform 0.2s, box-shadow 0.2s;
834
+ box-shadow: 0 4px 32px rgba(0,0,0,0.45), 0 0 0 0 var(--gvp-accent-glow);
835
+ }
836
+ /* Ripple ring on hover */
837
+ .gvp-center-play-btn::after {
838
+ content: '';
839
+ position: absolute; inset: -4px;
840
+ border-radius: 50%;
841
+ border: 1.5px solid rgba(255,255,255,0.12);
842
+ -webkit-transition: border-color 0.2s, opacity 0.2s; transition: border-color 0.2s, opacity 0.2s;
843
+ opacity: 0;
844
+ }
845
+ .gvp-center-play:hover .gvp-center-play-btn::after {
846
+ opacity: 1;
847
+ border-color: var(--gvp-accent);
848
+ }
849
+ .gvp-center-play:hover .gvp-center-play-btn {
850
+ background: var(--gvp-accent-dim);
851
+ -webkit-transform: scale(1.07);
852
+ transform: scale(1.07);
853
+ box-shadow: 0 4px 40px rgba(0,0,0,0.55), 0 0 28px var(--gvp-accent-glow);
756
854
  }
757
- .gvp-center-play:hover .gvp-center-play-btn { background: var(--gvp-accent); transform: scale(1.08); }
758
855
 
759
- /* ── Click-to-toggle overlay ───────────────────────────────── */
760
- .gvp-click-area { position: absolute; inset: 0; cursor: pointer; z-index: 1; }
856
+ /* ── Click-to-toggle overlay ─────────────────────────────── */
857
+ .gvp-click-area {
858
+ position: absolute; inset: 0;
859
+ cursor: pointer; z-index: 4;
860
+ }
761
861
 
762
- /* ── Ripple ───────────────────────────────────────────────── */
862
+ /* ── Ripple animation ─────────────────────────────────────── */
763
863
  .gvp-ripple {
764
- position: absolute; border-radius: 50%; transform: scale(0);
765
- background: rgba(255,255,255,.25);
766
- animation: gvp-ripple-anim .5s ease-out forwards;
767
- pointer-events: none; z-index: 2;
864
+ position: absolute; border-radius: 50%;
865
+ -webkit-transform: scale(0); transform: scale(0);
866
+ background: rgba(255,255,255,0.18);
867
+ -webkit-animation: gvp-ripple-anim 0.55s cubic-bezier(0.22,1,0.36,1) forwards;
868
+ animation: gvp-ripple-anim 0.55s cubic-bezier(0.22,1,0.36,1) forwards;
869
+ pointer-events: none; z-index: 5;
768
870
  }
769
- @keyframes gvp-ripple-anim { to { transform: scale(4); opacity: 0; } }
871
+ @-webkit-keyframes gvp-ripple-anim { to { -webkit-transform: scale(5); transform: scale(5); opacity: 0; } }
872
+ @keyframes gvp-ripple-anim { to { -webkit-transform: scale(5); transform: scale(5); opacity: 0; } }
770
873
 
771
- /* ── Controls bar ──────────────────────────────────────────── */
874
+ /* ── Controls bar ─────────────────────────────────────────── */
772
875
  .gvp-controls {
773
- position: absolute; bottom: 0; left: 0; right: 0; z-index: 10;
774
- background: linear-gradient(to top, rgba(0,0,0,.85) 0%, transparent 100%);
775
- padding: 36px 14px 10px;
776
- transition: opacity .3s, transform .3s;
777
- border-radius: 0 0 10px 10px;
876
+ position: absolute;
877
+ bottom: 0; left: 0; right: 0;
878
+ z-index: 10;
879
+ padding: 0 14px 14px;
880
+ -webkit-transition: opacity 0.35s cubic-bezier(0.4,0,0.2,1),
881
+ -webkit-transform 0.35s cubic-bezier(0.4,0,0.2,1);
882
+ transition: opacity 0.35s cubic-bezier(0.4,0,0.2,1),
883
+ transform 0.35s cubic-bezier(0.4,0,0.2,1);
884
+ border-radius: 0 0 var(--gvp-radius) var(--gvp-radius);
885
+ }
886
+
887
+ /* Inner glass pill that wraps all controls */
888
+ .gvp-controls-inner {
889
+ background: var(--gvp-glass-bg);
890
+ -webkit-backdrop-filter: blur(24px) saturate(160%);
891
+ backdrop-filter: blur(24px) saturate(160%);
892
+ border: 1px solid var(--gvp-glass-bdr);
893
+ border-radius: 10px;
894
+ padding: 10px 12px 10px;
895
+ -webkit-box-shadow: 0 8px 32px rgba(0,0,0,0.5), inset 0 1px 0 rgba(255,255,255,0.05);
896
+ box-shadow: 0 8px 32px rgba(0,0,0,0.5), inset 0 1px 0 rgba(255,255,255,0.05);
897
+ }
898
+
899
+ /* ── Seek row ─────────────────────────────────────────────── */
900
+ .gvp-seek-row {
901
+ display: -webkit-box; display: -ms-flexbox; display: flex;
902
+ -webkit-box-align: center; -ms-flex-align: center; align-items: center;
903
+ gap: 8px;
904
+ margin-bottom: 8px;
778
905
  }
779
906
 
780
- /* ── Seek bar ──────────────────────────────────────────────── */
781
- .gvp-seek-row { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
782
907
  .gvp-seek-wrap {
783
- flex: 1; position: relative; height: 4px; cursor: pointer;
784
- padding: 8px 0; margin: -8px 0; box-sizing: content-box;
785
- /* Prevent page scroll while touch-seeking */
908
+ -webkit-box-flex: 1; -ms-flex: 1; flex: 1;
909
+ position: relative;
910
+ height: 4px;
911
+ cursor: pointer;
912
+ padding: 8px 0;
913
+ margin: -8px 0;
914
+ -webkit-box-sizing: content-box; box-sizing: content-box;
786
915
  touch-action: none;
787
916
  }
917
+
788
918
  .gvp-seek-track {
789
- height: 4px; background: rgba(255,255,255,.2); border-radius: 4px;
790
- position: relative; overflow: hidden; transition: height .15s;
919
+ height: 3px;
920
+ background: rgba(255,255,255,0.12);
921
+ border-radius: 99px;
922
+ position: relative;
923
+ overflow: visible;
924
+ -webkit-transition: height 0.15s; transition: height 0.15s;
791
925
  pointer-events: none;
792
926
  }
793
927
  .gvp-seek-wrap:hover .gvp-seek-track,
794
- .gvp-seek-wrap.gvp-dragging .gvp-seek-track { height: 6px; }
928
+ .gvp-seek-wrap.gvp-dragging .gvp-seek-track { height: 5px; }
795
929
 
796
930
  .gvp-seek-buffered {
797
931
  position: absolute; left: 0; top: 0; height: 100%;
798
- background: rgba(255,255,255,.35); border-radius: 4px; pointer-events: none;
932
+ background: rgba(255,255,255,0.22);
933
+ border-radius: 99px;
934
+ pointer-events: none;
935
+ -webkit-transition: width 0.3s; transition: width 0.3s;
799
936
  }
937
+
800
938
  .gvp-seek-progress {
801
939
  position: absolute; left: 0; top: 0; height: 100%;
802
- background: var(--gvp-accent); border-radius: 4px; pointer-events: none;
940
+ background: -webkit-linear-gradient(left, var(--gvp-accent), color-mix(in srgb, var(--gvp-accent) 80%, #fff 20%));
941
+ background: linear-gradient(to right, var(--gvp-accent), color-mix(in srgb, var(--gvp-accent) 80%, #fff 20%));
942
+ border-radius: 99px;
943
+ pointer-events: none;
944
+ /* Glow on the progress fill */
945
+ -webkit-box-shadow: 0 0 8px var(--gvp-accent-glow);
946
+ box-shadow: 0 0 8px var(--gvp-accent-glow);
803
947
  }
948
+
949
+ /* Thumb — visible on hover/drag */
804
950
  .gvp-seek-thumb {
805
- position: absolute; top: 50%; transform: translate(-50%,-50%);
806
- width: 13px; height: 13px; background: #fff; border-radius: 50%;
807
- box-shadow: 0 1px 4px rgba(0,0,0,.5); pointer-events: none;
808
- opacity: 0; transition: opacity .15s;
951
+ position: absolute; top: 50%;
952
+ -webkit-transform: translate(-50%, -50%) scale(0);
953
+ transform: translate(-50%, -50%) scale(0);
954
+ width: 14px; height: 14px;
955
+ background: #fff;
956
+ border-radius: 50%;
957
+ pointer-events: none;
958
+ -webkit-box-shadow: 0 0 0 3px var(--gvp-accent-dim), 0 2px 6px rgba(0,0,0,0.5);
959
+ box-shadow: 0 0 0 3px var(--gvp-accent-dim), 0 2px 6px rgba(0,0,0,0.5);
960
+ -webkit-transition: -webkit-transform 0.15s cubic-bezier(0.34,1.56,0.64,1);
961
+ transition: transform 0.15s cubic-bezier(0.34,1.56,0.64,1);
809
962
  }
810
963
  .gvp-seek-wrap:hover .gvp-seek-thumb,
811
- .gvp-seek-wrap.gvp-dragging .gvp-seek-thumb { opacity: 1; }
964
+ .gvp-seek-wrap.gvp-dragging .gvp-seek-thumb {
965
+ -webkit-transform: translate(-50%,-50%) scale(1);
966
+ transform: translate(-50%,-50%) scale(1);
967
+ }
812
968
 
969
+ /* Seek tooltip */
813
970
  .gvp-seek-tooltip {
814
- position: absolute; bottom: 24px; transform: translateX(-50%);
815
- background: rgba(0,0,0,.8); color: #fff; font-size: 11px; font-weight: 500;
816
- padding: 3px 7px; border-radius: 4px;
817
- pointer-events: none; white-space: nowrap;
818
- opacity: 0; transition: opacity .1s;
971
+ position: absolute;
972
+ bottom: 26px;
973
+ -webkit-transform: translateX(-50%);
974
+ transform: translateX(-50%);
975
+ background: rgba(10,10,14,0.92);
976
+ -webkit-backdrop-filter: blur(8px);
977
+ backdrop-filter: blur(8px);
978
+ border: 1px solid var(--gvp-glass-bdr);
979
+ color: var(--gvp-text);
980
+ font-family: var(--gvp-mono);
981
+ font-size: 11px;
982
+ font-weight: 500;
983
+ padding: 3px 8px;
984
+ border-radius: 5px;
985
+ pointer-events: none;
986
+ white-space: nowrap;
987
+ opacity: 0;
988
+ -webkit-transition: opacity 0.12s; transition: opacity 0.12s;
989
+ -webkit-box-shadow: 0 4px 12px rgba(0,0,0,0.45); box-shadow: 0 4px 12px rgba(0,0,0,0.45);
819
990
  }
820
991
  .gvp-seek-wrap:hover .gvp-seek-tooltip { opacity: 1; }
821
992
 
822
- /* ── Button row ────────────────────────────────────────────── */
823
- .gvp-btn-row { display: flex; align-items: center; gap: 4px; }
993
+ /* Caret below tooltip */
994
+ .gvp-seek-tooltip::after {
995
+ content: '';
996
+ position: absolute; bottom: -5px; left: 50%;
997
+ -webkit-transform: translateX(-50%); transform: translateX(-50%);
998
+ border: 4px solid transparent;
999
+ border-top-color: rgba(10,10,14,0.92);
1000
+ border-bottom-width: 0;
1001
+ }
1002
+
1003
+ /* ── Button row ───────────────────────────────────────────── */
1004
+ .gvp-btn-row {
1005
+ display: -webkit-box; display: -ms-flexbox; display: flex;
1006
+ -webkit-box-align: center; -ms-flex-align: center; align-items: center;
1007
+ gap: 2px;
1008
+ }
1009
+
824
1010
  .gvp-btn {
825
- background: none; border: none; color: #fff; cursor: pointer;
826
- padding: 5px; border-radius: 6px;
827
- display: flex; align-items: center; justify-content: center;
828
- transition: background .15s, color .15s; flex-shrink: 0;
1011
+ background: none; border: none;
1012
+ color: rgba(255,255,255,0.75);
1013
+ cursor: pointer;
1014
+ padding: 6px;
1015
+ border-radius: 7px;
1016
+ display: -webkit-inline-box; display: -ms-inline-flexbox; display: inline-flex;
1017
+ -webkit-box-align: center; -ms-flex-align: center; align-items: center;
1018
+ -webkit-box-pack: center; -ms-flex-pack: center; justify-content: center;
1019
+ -webkit-transition: background 0.14s, color 0.14s, -webkit-transform 0.12s;
1020
+ transition: background 0.14s, color 0.14s, transform 0.12s;
1021
+ -webkit-flex-shrink: 0; -ms-flex-negative: 0; flex-shrink: 0;
1022
+ line-height: 0;
1023
+ }
1024
+ .gvp-btn:hover {
1025
+ background: rgba(255,255,255,0.09);
1026
+ color: #fff;
1027
+ -webkit-transform: scale(1.08); transform: scale(1.08);
1028
+ }
1029
+ .gvp-btn:active {
1030
+ background: rgba(255,255,255,0.14);
1031
+ -webkit-transform: scale(0.96); transform: scale(0.96);
829
1032
  }
830
- .gvp-btn:hover { background: rgba(255,255,255,.12); color: var(--gvp-accent); }
831
- .gvp-btn:active { background: rgba(255,255,255,.2); }
1033
+ /* Accent highlight on play button */
1034
+ .gvp-btn-play:hover { background: var(--gvp-accent-dim); color: var(--gvp-accent); }
832
1035
 
833
- /* ── Volume ────────────────────────────────────────────────── */
834
- .gvp-volume-wrap { display: flex; align-items: center; gap: 5px; }
1036
+ /* ── Volume group ─────────────────────────────────────────── */
1037
+ .gvp-volume-wrap {
1038
+ display: -webkit-box; display: -ms-flexbox; display: flex;
1039
+ -webkit-box-align: center; -ms-flex-align: center; align-items: center;
1040
+ gap: 2px;
1041
+ }
835
1042
  .gvp-volume-slider {
836
- -webkit-appearance: none; width: 70px; height: 4px;
837
- background: rgba(255,255,255,.25); border-radius: 4px;
838
- outline: none; cursor: pointer; accent-color: var(--gvp-accent);
1043
+ -webkit-appearance: none;
1044
+ -moz-appearance: none;
1045
+ appearance: none;
1046
+ width: 0;
1047
+ max-width: 72px;
1048
+ height: 3px;
1049
+ background: rgba(255,255,255,0.18);
1050
+ border-radius: 99px;
1051
+ outline: none;
1052
+ cursor: pointer;
1053
+ overflow: visible;
1054
+ -webkit-transition: width 0.22s cubic-bezier(0.4,0,0.2,1), opacity 0.22s;
1055
+ transition: width 0.22s cubic-bezier(0.4,0,0.2,1), opacity 0.22s;
1056
+ opacity: 0;
1057
+ /* accent-color is supported in modern browsers */
1058
+ accent-color: var(--gvp-accent);
1059
+ }
1060
+ /* Show slider when volume-wrap is hovered */
1061
+ .gvp-volume-wrap:hover .gvp-volume-slider,
1062
+ .gvp-volume-wrap:focus-within .gvp-volume-slider {
1063
+ width: 72px;
1064
+ opacity: 1;
839
1065
  }
1066
+ /* WebKit thumb */
840
1067
  .gvp-volume-slider::-webkit-slider-thumb {
841
- -webkit-appearance: none; width: 12px; height: 12px;
842
- border-radius: 50%; background: #fff; cursor: pointer;
843
- box-shadow: 0 1px 3px rgba(0,0,0,.4);
1068
+ -webkit-appearance: none;
1069
+ width: 12px; height: 12px;
1070
+ border-radius: 50%;
1071
+ background: #fff;
1072
+ cursor: pointer;
1073
+ -webkit-box-shadow: 0 1px 4px rgba(0,0,0,0.4);
1074
+ box-shadow: 0 1px 4px rgba(0,0,0,0.4);
844
1075
  }
1076
+ /* Firefox thumb */
845
1077
  .gvp-volume-slider::-moz-range-thumb {
846
- width: 12px; height: 12px; border-radius: 50%;
847
- background: #fff; cursor: pointer; border: none;
1078
+ width: 12px; height: 12px;
1079
+ border-radius: 50%;
1080
+ background: #fff;
1081
+ cursor: pointer;
1082
+ border: none;
1083
+ box-shadow: 0 1px 4px rgba(0,0,0,0.4);
1084
+ }
1085
+ /* Firefox track */
1086
+ .gvp-volume-slider::-moz-range-track {
1087
+ background: rgba(255,255,255,0.18);
1088
+ border-radius: 99px;
1089
+ height: 3px;
848
1090
  }
1091
+ /* Edge/IE thumb */
1092
+ .gvp-volume-slider::-ms-thumb {
1093
+ width: 12px; height: 12px;
1094
+ border-radius: 50%;
1095
+ background: #fff;
1096
+ cursor: pointer;
1097
+ border: none;
1098
+ }
1099
+ .gvp-volume-slider::-ms-track {
1100
+ background: rgba(255,255,255,0.18);
1101
+ border-color: transparent;
1102
+ color: transparent;
1103
+ height: 3px;
1104
+ }
1105
+ .gvp-volume-slider::-ms-fill-lower { background: var(--gvp-accent); border-radius: 99px; }
849
1106
 
850
- /* ── Time display ──────────────────────────────────────────── */
1107
+ /* ── Time display ──────────────────────────────────────────── */
851
1108
  .gvp-time {
852
- font-size: 12px; color: rgba(255,255,255,.85);
853
- font-variant-numeric: tabular-nums; white-space: nowrap; letter-spacing: .02em;
1109
+ font-family: var(--gvp-mono);
1110
+ font-size: 12px;
1111
+ font-weight: 500;
1112
+ color: var(--gvp-text);
1113
+ white-space: nowrap;
1114
+ letter-spacing: 0.04em;
1115
+ padding: 0 4px;
1116
+ /* Tabular numerals so digits don't shift width */
1117
+ font-variant-numeric: tabular-nums;
1118
+ -moz-font-feature-settings: "tnum";
1119
+ -webkit-font-feature-settings: "tnum";
1120
+ font-feature-settings: "tnum";
854
1121
  }
855
- .gvp-spacer { flex: 1; }
1122
+ .gvp-time-sep { color: var(--gvp-text-dim); margin: 0 2px; }
856
1123
 
857
- /* ── Popup menus ───────────────────────────────────────────── */
1124
+ .gvp-spacer { -webkit-box-flex: 1; -ms-flex: 1; flex: 1; }
1125
+
1126
+ /* ── Speed button ─────────────────────────────────────────── */
1127
+ .gvp-rate-label {
1128
+ font-family: var(--gvp-font);
1129
+ font-size: 11px; font-weight: 700;
1130
+ color: var(--gvp-text-dim);
1131
+ min-width: 30px; text-align: center;
1132
+ cursor: pointer;
1133
+ padding: 5px 4px;
1134
+ border-radius: 6px;
1135
+ -webkit-transition: background 0.14s, color 0.14s; transition: background 0.14s, color 0.14s;
1136
+ -webkit-flex-shrink: 0; -ms-flex-negative: 0; flex-shrink: 0;
1137
+ letter-spacing: 0.01em;
1138
+ line-height: 1;
1139
+ }
1140
+ .gvp-rate-label:hover { background: rgba(255,255,255,0.09); color: #fff; }
1141
+ .gvp-rate-label-active { color: var(--gvp-accent); }
1142
+
1143
+ /* ── Popup menus ──────────────────────────────────────────── */
858
1144
  .gvp-menu-wrap { position: relative; }
1145
+
859
1146
  .gvp-menu {
860
- position: absolute; bottom: calc(100% + 8px); right: 0;
861
- background: rgba(18,18,22,.95);
862
- backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
863
- border: 1px solid rgba(255,255,255,.08); border-radius: 8px;
864
- min-width: 140px; overflow: hidden;
865
- box-shadow: 0 8px 24px rgba(0,0,0,.5);
866
- animation: gvp-menu-in .1s ease-out; z-index: 20;
1147
+ position: absolute;
1148
+ bottom: calc(100% + 10px);
1149
+ right: 0;
1150
+ background: rgba(12,12,18,0.96);
1151
+ -webkit-backdrop-filter: blur(20px) saturate(180%);
1152
+ backdrop-filter: blur(20px) saturate(180%);
1153
+ border: 1px solid var(--gvp-glass-bdr);
1154
+ border-radius: 10px;
1155
+ min-width: 148px;
1156
+ overflow: hidden;
1157
+ -webkit-box-shadow: 0 12px 40px rgba(0,0,0,0.65), 0 0 0 0.5px rgba(255,255,255,0.04);
1158
+ box-shadow: 0 12px 40px rgba(0,0,0,0.65), 0 0 0 0.5px rgba(255,255,255,0.04);
1159
+ -webkit-animation: gvp-menu-in 0.14s cubic-bezier(0.22,1,0.36,1);
1160
+ animation: gvp-menu-in 0.14s cubic-bezier(0.22,1,0.36,1);
1161
+ z-index: 20;
1162
+ }
1163
+ @-webkit-keyframes gvp-menu-in {
1164
+ from { opacity: 0; -webkit-transform: scale(0.92) translateY(6px); transform: scale(0.92) translateY(6px); }
1165
+ to { opacity: 1; -webkit-transform: none; transform: none; }
867
1166
  }
868
1167
  @keyframes gvp-menu-in {
869
- from { opacity: 0; transform: scale(.95) translateY(4px); }
870
- to { opacity: 1; transform: none; }
1168
+ from { opacity: 0; -webkit-transform: scale(0.92) translateY(6px); transform: scale(0.92) translateY(6px); }
1169
+ to { opacity: 1; -webkit-transform: none; transform: none; }
871
1170
  }
1171
+
872
1172
  .gvp-menu-title {
873
- font-size: 10px; font-weight: 600; text-transform: uppercase;
874
- letter-spacing: .08em; color: rgba(255,255,255,.4); padding: 8px 12px 4px;
1173
+ font-family: var(--gvp-font);
1174
+ font-size: 10px; font-weight: 700;
1175
+ text-transform: uppercase;
1176
+ letter-spacing: 0.12em;
1177
+ color: var(--gvp-text-dim);
1178
+ padding: 10px 13px 5px;
875
1179
  }
1180
+
876
1181
  .gvp-menu-item {
877
- display: flex; align-items: center; justify-content: space-between;
878
- padding: 7px 12px; font-size: 13px; color: rgba(255,255,255,.85);
879
- cursor: pointer; transition: background .12s; gap: 10px;
880
- }
881
- .gvp-menu-item:hover { background: rgba(255,255,255,.07); }
882
- .gvp-menu-item-active { color: var(--gvp-accent); font-weight: 600; }
883
- .gvp-menu-check { font-size: 14px; color: var(--gvp-accent); }
884
- .gvp-menu-sep { height: 1px; background: rgba(255,255,255,.07); margin: 3px 0; }
1182
+ display: -webkit-box; display: -ms-flexbox; display: flex;
1183
+ -webkit-box-align: center; -ms-flex-align: center; align-items: center;
1184
+ -webkit-box-pack: justify; -ms-flex-pack: justify; justify-content: space-between;
1185
+ padding: 7px 13px;
1186
+ font-family: var(--gvp-font);
1187
+ font-size: 13px; font-weight: 500;
1188
+ color: var(--gvp-text);
1189
+ cursor: pointer;
1190
+ -webkit-transition: background 0.1s; transition: background 0.1s;
1191
+ gap: 12px;
1192
+ }
1193
+ .gvp-menu-item:hover { background: rgba(255,255,255,0.06); }
1194
+ .gvp-menu-item-active { color: var(--gvp-accent); }
1195
+
1196
+ .gvp-menu-check {
1197
+ font-size: 13px;
1198
+ color: var(--gvp-accent);
1199
+ line-height: 1;
1200
+ /* Unicode checkmark — crisp on all platforms */
1201
+ }
1202
+
1203
+ .gvp-menu-sep {
1204
+ height: 1px;
1205
+ background: rgba(255,255,255,0.06);
1206
+ margin: 3px 0;
1207
+ }
885
1208
 
886
- /* ── Error overlay ─────────────────────────────────────────── */
1209
+ /* ── Error overlay ────────────────────────────────────────── */
887
1210
  .gvp-error {
888
1211
  position: absolute; inset: 0; z-index: 15;
889
- display: flex; flex-direction: column; align-items: center; justify-content: center;
890
- background: rgba(0,0,0,.8); color: #fff; gap: 10px; padding: 24px;
891
- border-radius: 10px; text-align: center;
1212
+ display: -webkit-box; display: -ms-flexbox; display: flex;
1213
+ -webkit-box-orient: vertical; -webkit-box-direction: normal;
1214
+ -ms-flex-direction: column; flex-direction: column;
1215
+ -webkit-box-align: center; -ms-flex-align: center; align-items: center;
1216
+ -webkit-box-pack: center; -ms-flex-pack: center; justify-content: center;
1217
+ background: rgba(6,6,10,0.88);
1218
+ -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px);
1219
+ color: #fff;
1220
+ gap: 10px;
1221
+ padding: 28px;
1222
+ border-radius: var(--gvp-radius);
1223
+ text-align: center;
1224
+ }
1225
+ .gvp-error-icon { font-size: 40px; line-height: 1; }
1226
+ .gvp-error-code {
1227
+ font-family: var(--gvp-mono);
1228
+ font-size: 10px; font-weight: 500;
1229
+ letter-spacing: 0.14em;
1230
+ color: #f87171;
1231
+ text-transform: uppercase;
1232
+ background: rgba(248,113,113,0.1);
1233
+ border: 1px solid rgba(248,113,113,0.25);
1234
+ padding: 3px 10px; border-radius: 4px;
1235
+ }
1236
+ .gvp-error-msg {
1237
+ font-family: var(--gvp-font);
1238
+ font-size: 14px; font-weight: 400;
1239
+ color: rgba(255,255,255,0.6);
1240
+ max-width: 320px; line-height: 1.6;
892
1241
  }
893
- .gvp-error-icon { font-size: 36px; }
894
- .gvp-error-code { font-size: 11px; font-weight: 700; letter-spacing: .1em; color: #f87171; text-transform: uppercase; }
895
- .gvp-error-msg { font-size: 14px; color: rgba(255,255,255,.7); max-width: 320px; line-height: 1.5; }
896
1242
 
897
- /* ── Secure badge ──────────────────────────────────────────── */
1243
+ /* ── Secure badge ─────────────────────────────────────────── */
898
1244
  .gvp-badge {
899
- position: absolute; top: 10px; right: 12px; z-index: 5;
900
- display: flex; align-items: center; gap: 5px;
901
- background: rgba(0,0,0,.55); backdrop-filter: blur(6px);
902
- border-radius: 20px; padding: 3px 9px 3px 7px;
903
- font-size: 10px; font-weight: 600; color: var(--gvp-accent);
904
- pointer-events: none; letter-spacing: .04em; transition: opacity .3s;
1245
+ position: absolute; top: 12px; right: 14px; z-index: 5;
1246
+ display: -webkit-inline-box; display: -ms-inline-flexbox; display: inline-flex;
1247
+ -webkit-box-align: center; -ms-flex-align: center; align-items: center;
1248
+ gap: 5px;
1249
+ background: rgba(8,8,14,0.60);
1250
+ -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px);
1251
+ border: 1px solid rgba(255,255,255,0.08);
1252
+ border-radius: 99px;
1253
+ padding: 4px 10px 4px 7px;
1254
+ font-family: var(--gvp-font);
1255
+ font-size: 10px; font-weight: 700;
1256
+ color: var(--gvp-accent);
1257
+ pointer-events: none;
1258
+ letter-spacing: 0.06em;
1259
+ text-transform: uppercase;
1260
+ -webkit-transition: opacity 0.3s; transition: opacity 0.3s;
1261
+ -webkit-box-shadow: 0 2px 12px rgba(0,0,0,0.4), 0 0 0 0.5px rgba(255,255,255,0.05);
1262
+ box-shadow: 0 2px 12px rgba(0,0,0,0.4), 0 0 0 0.5px rgba(255,255,255,0.05);
905
1263
  }
906
1264
  .gvp-badge-hidden { opacity: 0; }
907
1265
 
908
- /* ── Forensic watermark ────────────────────────────────────── */
909
- .gvp-watermark { position: absolute; inset: 0; pointer-events: none; overflow: hidden; z-index: 6; }
1266
+ /* ── Forensic watermark ───────────────────────────────────── */
1267
+ .gvp-watermark {
1268
+ position: absolute; inset: 0;
1269
+ pointer-events: none; overflow: hidden; z-index: 6;
1270
+ }
910
1271
  .gvp-watermark-text {
911
- position: absolute; white-space: nowrap; font-size: 13px; font-family: monospace;
912
- color: rgba(255,255,255,.065); transform: rotate(-28deg);
913
- user-select: none; pointer-events: none; letter-spacing: .05em;
1272
+ position: absolute;
1273
+ white-space: nowrap;
1274
+ font-family: var(--gvp-mono);
1275
+ font-size: 12px;
1276
+ color: rgba(255,255,255,0.055);
1277
+ -webkit-transform: rotate(-28deg); transform: rotate(-28deg);
1278
+ -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none;
1279
+ pointer-events: none;
1280
+ letter-spacing: 0.06em;
914
1281
  }
915
1282
 
916
- /* ── Speed label button ────────────────────────────────────── */
917
- .gvp-rate-label {
918
- font-size: 11px; font-weight: 700; color: rgba(255,255,255,.7);
919
- min-width: 28px; text-align: center; cursor: pointer;
920
- padding: 5px 4px; border-radius: 4px; transition: background .12s; flex-shrink: 0;
1283
+ /* ── Live dot (for live streams) ─────────────────────────── */
1284
+ .gvp-live-badge {
1285
+ display: -webkit-inline-box; display: -ms-inline-flexbox; display: inline-flex;
1286
+ -webkit-box-align: center; -ms-flex-align: center; align-items: center;
1287
+ gap: 5px;
1288
+ font-family: var(--gvp-font);
1289
+ font-size: 11px; font-weight: 700;
1290
+ color: #f87171;
1291
+ letter-spacing: 0.08em;
1292
+ text-transform: uppercase;
1293
+ }
1294
+ .gvp-live-dot {
1295
+ width: 7px; height: 7px;
1296
+ background: #f87171;
1297
+ border-radius: 50%;
1298
+ -webkit-animation: gvp-live-pulse 1.5s ease-in-out infinite;
1299
+ animation: gvp-live-pulse 1.5s ease-in-out infinite;
1300
+ }
1301
+ @-webkit-keyframes gvp-live-pulse {
1302
+ 0%, 100% { opacity: 1; -webkit-transform: scale(1); transform: scale(1); }
1303
+ 50% { opacity: 0.5; -webkit-transform: scale(0.7); transform: scale(0.7); }
1304
+ }
1305
+ @keyframes gvp-live-pulse {
1306
+ 0%, 100% { opacity: 1; transform: scale(1); }
1307
+ 50% { opacity: 0.5; transform: scale(0.7); }
1308
+ }
1309
+
1310
+ /* ── Divider between button groups ───────────────────────── */
1311
+ .gvp-divider {
1312
+ width: 1px; height: 18px;
1313
+ background: rgba(255,255,255,0.1);
1314
+ margin: 0 4px;
1315
+ -webkit-flex-shrink: 0; -ms-flex-negative: 0; flex-shrink: 0;
1316
+ }
1317
+
1318
+ /* ── Tooltip on buttons ───────────────────────────────────── */
1319
+ .gvp-btn[title]:hover::after {
1320
+ content: attr(title);
1321
+ position: absolute;
1322
+ bottom: calc(100% + 7px);
1323
+ left: 50%; -webkit-transform: translateX(-50%); transform: translateX(-50%);
1324
+ background: rgba(10,10,14,0.95);
1325
+ border: 1px solid var(--gvp-glass-bdr);
1326
+ color: var(--gvp-text);
1327
+ font-family: var(--gvp-font);
1328
+ font-size: 11px; font-weight: 500;
1329
+ padding: 3px 8px; border-radius: 5px;
1330
+ white-space: nowrap;
1331
+ pointer-events: none;
1332
+ z-index: 30;
1333
+ }
1334
+
1335
+ /* ── Focus ring (keyboard nav) ────────────────────────────── */
1336
+ .gvp-btn:focus-visible,
1337
+ .gvp-rate-label:focus-visible,
1338
+ .gvp-seek-wrap:focus-visible {
1339
+ outline: 2px solid var(--gvp-accent);
1340
+ outline-offset: 2px;
1341
+ }
1342
+
1343
+ /* ── Responsive: hide volume slider text on tiny players ─── */
1344
+ @media (max-width: 380px) {
1345
+ .gvp-volume-slider { display: none; }
1346
+ .gvp-time { font-size: 11px; }
1347
+ .gvp-controls-inner { padding: 8px 10px; }
921
1348
  }
922
- .gvp-rate-label:hover { background: rgba(255,255,255,.1); }
923
1349
  `;
924
1350
  const tag = document.createElement('style');
925
- tag.setAttribute('data-guardvideo', 'player-ui-styles');
1351
+ tag.setAttribute('data-guardvideo', 'player-ui-styles-v2');
926
1352
  tag.textContent = css;
927
1353
  document.head.appendChild(tag);
928
1354
  }
@@ -937,7 +1363,7 @@ class PlayerUI {
937
1363
  this.openMenu = null;
938
1364
  this.hideTimer = null;
939
1365
  this.seekDragging = false;
940
- const accent = config.branding?.accentColor ?? '#44c09b';
1366
+ const accent = config.branding?.accentColor ?? '#00e5a0';
941
1367
  const brandName = config.branding?.name ?? 'GuardVideo';
942
1368
  injectStyles();
943
1369
  container.innerHTML = '';
@@ -945,13 +1371,15 @@ class PlayerUI {
945
1371
  this.root.style.width = config.width ?? '100%';
946
1372
  this.root.style.height = config.height ?? 'auto';
947
1373
  this.root.style.setProperty('--gvp-accent', accent);
1374
+ this.root.style.setProperty('--gvp-accent-dim', this._hexToRgba(accent, 0.18));
1375
+ this.root.style.setProperty('--gvp-accent-glow', this._hexToRgba(accent, 0.35));
948
1376
  this.root.setAttribute('tabindex', '0');
949
1377
  this.root.setAttribute('role', 'region');
950
1378
  this.root.setAttribute('aria-label', `${brandName} video player`);
951
1379
  this.videoEl = el('video', 'gvp-video', { playsinline: '', preload: 'metadata' });
952
1380
  this.badge = el('div', 'gvp-badge');
953
1381
  this.badge.setAttribute('aria-hidden', 'true');
954
- this.badge.appendChild(svgEl(ICON.shield, 14, 14));
1382
+ this.badge.appendChild(svgEl(ICON.shield, 13, 13));
955
1383
  this.badge.appendChild(document.createTextNode(brandName));
956
1384
  this.watermarkDiv = el('div', 'gvp-watermark');
957
1385
  this.watermarkDiv.setAttribute('aria-hidden', 'true');
@@ -971,11 +1399,12 @@ class PlayerUI {
971
1399
  this.centerPlay.setAttribute('role', 'button');
972
1400
  this.centerPlay.setAttribute('aria-label', 'Play');
973
1401
  this.centerPlayBtn = el('div', 'gvp-center-play-btn');
974
- this.centerPlayBtn.appendChild(svgEl(ICON.play, 32, 32));
1402
+ this.centerPlayBtn.appendChild(svgEl(ICON.play, 30, 30));
975
1403
  this.centerPlay.appendChild(this.centerPlayBtn);
976
1404
  this.clickArea = el('div', 'gvp-click-area gvp-hidden');
977
1405
  this.clickArea.setAttribute('aria-hidden', 'true');
978
1406
  this.controls = el('div', 'gvp-controls');
1407
+ const inner = el('div', 'gvp-controls-inner');
979
1408
  const seekRow = el('div', 'gvp-seek-row');
980
1409
  this.seekWrap = el('div', 'gvp-seek-wrap');
981
1410
  this.seekWrap.setAttribute('role', 'slider');
@@ -994,15 +1423,18 @@ class PlayerUI {
994
1423
  seekTrack.append(this.seekBuffered, this.seekProgress);
995
1424
  this.seekWrap.append(seekTrack, this.seekThumb, this.seekTooltip);
996
1425
  seekRow.appendChild(this.seekWrap);
997
- this.controls.appendChild(seekRow);
1426
+ inner.appendChild(seekRow);
998
1427
  const btnRow = el('div', 'gvp-btn-row');
999
- this.playBtn = el('button', 'gvp-btn');
1428
+ this.playBtn = el('button', 'gvp-btn gvp-btn-play');
1000
1429
  this.playBtn.type = 'button';
1001
1430
  this.playBtn.setAttribute('aria-label', 'Play');
1431
+ this.playBtn.title = 'Play (k)';
1432
+ this.playBtn.appendChild(svgEl(ICON.play));
1002
1433
  const volWrap = el('div', 'gvp-volume-wrap');
1003
1434
  this.volBtn = el('button', 'gvp-btn');
1004
1435
  this.volBtn.type = 'button';
1005
1436
  this.volBtn.setAttribute('aria-label', 'Mute');
1437
+ this.volBtn.title = 'Mute (m)';
1006
1438
  this.volBtn.appendChild(svgEl(ICON.volHigh, 18, 18));
1007
1439
  this.volSlider = el('input', 'gvp-volume-slider');
1008
1440
  Object.assign(this.volSlider, { type: 'range', min: '0', max: '1', step: '0.02', value: '1' });
@@ -1012,12 +1444,15 @@ class PlayerUI {
1012
1444
  this.timeEl.setAttribute('aria-live', 'off');
1013
1445
  this.timeEl.textContent = '0:00 / 0:00';
1014
1446
  const spacer = el('div', 'gvp-spacer');
1447
+ const divider = el('div', 'gvp-divider');
1448
+ divider.setAttribute('aria-hidden', 'true');
1015
1449
  const speedWrap = el('div', 'gvp-menu-wrap');
1016
1450
  this.speedLabel = el('span', 'gvp-rate-label');
1017
1451
  this.speedLabel.textContent = '1×';
1018
1452
  this.speedLabel.setAttribute('role', 'button');
1019
1453
  this.speedLabel.setAttribute('aria-label', 'Playback speed');
1020
1454
  this.speedLabel.setAttribute('aria-haspopup', 'menu');
1455
+ this.speedLabel.setAttribute('tabindex', '0');
1021
1456
  this.speedMenu = el('div', 'gvp-menu gvp-hidden');
1022
1457
  this.speedMenu.setAttribute('role', 'menu');
1023
1458
  this.speedMenu.setAttribute('aria-label', 'Playback speed');
@@ -1026,12 +1461,13 @@ class PlayerUI {
1026
1461
  speedTitle.textContent = 'Speed';
1027
1462
  this.speedMenu.appendChild(speedTitle);
1028
1463
  SPEEDS.forEach(r => {
1029
- const item = el('div', r === 1 ? 'gvp-menu-item gvp-menu-item-active' : 'gvp-menu-item');
1464
+ const active = r === 1;
1465
+ const item = el('div', active ? 'gvp-menu-item gvp-menu-item-active' : 'gvp-menu-item');
1030
1466
  item.setAttribute('role', 'menuitemradio');
1031
- item.setAttribute('aria-checked', r === 1 ? 'true' : 'false');
1467
+ item.setAttribute('aria-checked', active ? 'true' : 'false');
1032
1468
  item.dataset['speed'] = String(r);
1033
1469
  item.textContent = r === 1 ? 'Normal' : `${r}×`;
1034
- if (r === 1) {
1470
+ if (active) {
1035
1471
  const check = el('span', 'gvp-menu-check');
1036
1472
  check.setAttribute('aria-hidden', 'true');
1037
1473
  check.textContent = '✓';
@@ -1045,6 +1481,7 @@ class PlayerUI {
1045
1481
  this.settingsBtn.type = 'button';
1046
1482
  this.settingsBtn.setAttribute('aria-label', 'Quality settings');
1047
1483
  this.settingsBtn.setAttribute('aria-haspopup', 'menu');
1484
+ this.settingsBtn.title = 'Quality';
1048
1485
  this.settingsBtn.appendChild(svgEl(ICON.settings, 18, 18));
1049
1486
  this.settingsBtn.classList.add('gvp-hidden');
1050
1487
  this.qualityMenu = el('div', 'gvp-menu gvp-hidden');
@@ -1054,10 +1491,11 @@ class PlayerUI {
1054
1491
  this.fsBtn = el('button', 'gvp-btn');
1055
1492
  this.fsBtn.type = 'button';
1056
1493
  this.fsBtn.setAttribute('aria-label', 'Enter fullscreen');
1494
+ this.fsBtn.title = 'Fullscreen (f)';
1057
1495
  this.fsBtn.appendChild(svgEl(ICON.fsEnter, 18, 18));
1058
- this.playBtn.appendChild(svgEl(ICON.play));
1059
- btnRow.append(this.playBtn, volWrap, this.timeEl, spacer, speedWrap, qualWrap, this.fsBtn);
1060
- this.controls.appendChild(btnRow);
1496
+ btnRow.append(this.playBtn, volWrap, this.timeEl, spacer, speedWrap, divider, qualWrap, this.fsBtn);
1497
+ inner.appendChild(btnRow);
1498
+ this.controls.appendChild(inner);
1061
1499
  this.root.append(this.videoEl, this.badge, this.watermarkDiv, this.spinner, this.errorOverlay, this.centerPlay, this.clickArea, this.controls);
1062
1500
  container.appendChild(this.root);
1063
1501
  this._onFsChangeBound = () => this._onFsChange();
@@ -1078,6 +1516,18 @@ class PlayerUI {
1078
1516
  this._renderWatermark(wmText);
1079
1517
  }
1080
1518
  }
1519
+ _hexToRgba(hex, alpha) {
1520
+ const clean = hex.replace('#', '');
1521
+ const full = clean.length === 3
1522
+ ? clean.split('').map(c => c + c).join('')
1523
+ : clean;
1524
+ const r = parseInt(full.substring(0, 2), 16);
1525
+ const g = parseInt(full.substring(2, 4), 16);
1526
+ const b = parseInt(full.substring(4, 6), 16);
1527
+ if (isNaN(r) || isNaN(g) || isNaN(b))
1528
+ return `rgba(0,229,160,${alpha})`;
1529
+ return `rgba(${r},${g},${b},${alpha})`;
1530
+ }
1081
1531
  _wireEvents(videoId, config) {
1082
1532
  const video = this.videoEl;
1083
1533
  this.corePlayer = new GuardVideoPlayer(video, videoId, {
@@ -1118,8 +1568,13 @@ class PlayerUI {
1118
1568
  config.onTimeUpdate?.(video.currentTime);
1119
1569
  this._onTimeUpdate();
1120
1570
  });
1121
- video.addEventListener('loadedmetadata', () => { this.duration = video.duration || 0; this._onTimeUpdate(); });
1122
- video.addEventListener('durationchange', () => { this.duration = video.duration || 0; });
1571
+ video.addEventListener('loadedmetadata', () => {
1572
+ this.duration = video.duration || 0;
1573
+ this._onTimeUpdate();
1574
+ });
1575
+ video.addEventListener('durationchange', () => {
1576
+ this.duration = video.duration || 0;
1577
+ });
1123
1578
  video.addEventListener('progress', () => {
1124
1579
  if (video.buffered.length > 0) {
1125
1580
  this.bufferedEnd = video.buffered.end(video.buffered.length - 1);
@@ -1134,7 +1589,9 @@ class PlayerUI {
1134
1589
  });
1135
1590
  video.addEventListener('ratechange', () => {
1136
1591
  this.playbackRate = video.playbackRate;
1137
- this.speedLabel.textContent = this.playbackRate === 1 ? '1×' : `${this.playbackRate}×`;
1592
+ const isNormal = this.playbackRate === 1;
1593
+ this.speedLabel.textContent = isNormal ? '1×' : `${this.playbackRate}×`;
1594
+ this.speedLabel.classList.toggle('gvp-rate-label-active', !isNormal);
1138
1595
  });
1139
1596
  this.playBtn.addEventListener('click', (e) => { e.stopPropagation(); this._togglePlay(); });
1140
1597
  this.volBtn.addEventListener('click', (e) => { e.stopPropagation(); this._toggleMute(); });
@@ -1150,7 +1607,9 @@ class PlayerUI {
1150
1607
  window.addEventListener('mouseup', this._seekMouseUpBound);
1151
1608
  });
1152
1609
  this.seekWrap.addEventListener('mousemove', (e) => this._onSeekHover(e.clientX));
1153
- this.seekWrap.addEventListener('mouseleave', () => { this.seekTooltip.style.opacity = '0'; });
1610
+ this.seekWrap.addEventListener('mouseleave', () => {
1611
+ this.seekTooltip.style.opacity = '0';
1612
+ });
1154
1613
  this.seekWrap.addEventListener('touchstart', (e) => {
1155
1614
  e.preventDefault();
1156
1615
  this._startSeekDrag();
@@ -1167,6 +1626,12 @@ class PlayerUI {
1167
1626
  this._resetHideTimer();
1168
1627
  });
1169
1628
  this.speedLabel.addEventListener('click', (e) => { e.stopPropagation(); this._toggleMenu('speed'); });
1629
+ this.speedLabel.addEventListener('keydown', (e) => {
1630
+ if (e.key === 'Enter' || e.key === ' ') {
1631
+ e.preventDefault();
1632
+ this._toggleMenu('speed');
1633
+ }
1634
+ });
1170
1635
  this.speedMenu.addEventListener('click', (e) => {
1171
1636
  e.stopPropagation();
1172
1637
  const item = e.target.closest('[data-speed]');
@@ -1191,6 +1656,9 @@ class PlayerUI {
1191
1656
  this.centerPlay.addEventListener('click', (e) => { e.stopPropagation(); this._togglePlay(); });
1192
1657
  this.fsBtn.addEventListener('click', (e) => { e.stopPropagation(); this._toggleFullscreen(); });
1193
1658
  document.addEventListener('fullscreenchange', this._onFsChangeBound);
1659
+ document.addEventListener('webkitfullscreenchange', this._onFsChangeBound);
1660
+ document.addEventListener('mozfullscreenchange', this._onFsChangeBound);
1661
+ document.addEventListener('MSFullscreenChange', this._onFsChangeBound);
1194
1662
  this.root.addEventListener('mousemove', () => this._resetHideTimer());
1195
1663
  this.root.addEventListener('touchstart', () => this._resetHideTimer(), { passive: true });
1196
1664
  this.root.addEventListener('mouseleave', () => {
@@ -1210,7 +1678,7 @@ class PlayerUI {
1210
1678
  this.centerPlay.classList.toggle('gvp-hidden', !showCenter);
1211
1679
  if (showCenter) {
1212
1680
  this.centerPlayBtn.innerHTML = '';
1213
- this.centerPlayBtn.appendChild(svgEl(ended ? ICON.replay : ICON.play, 32, 32));
1681
+ this.centerPlayBtn.appendChild(svgEl(ended ? ICON.replay : ICON.play, 30, 30));
1214
1682
  this.centerPlay.setAttribute('aria-label', ended ? 'Replay' : 'Play');
1215
1683
  }
1216
1684
  this.clickArea.classList.toggle('gvp-hidden', idle);
@@ -1240,20 +1708,23 @@ class PlayerUI {
1240
1708
  const ended = this.videoEl.ended;
1241
1709
  const playing = this.playerState === PlayerState.PLAYING;
1242
1710
  const icon = ended ? ICON.replay : playing ? ICON.pause : ICON.play;
1243
- const label = playing ? 'Pause (k)' : 'Play (k)';
1711
+ const label = playing ? 'Pause' : 'Play';
1244
1712
  this.playBtn.innerHTML = '';
1245
1713
  this.playBtn.appendChild(svgEl(icon));
1246
1714
  this.playBtn.setAttribute('aria-label', label);
1247
- this.playBtn.title = label;
1715
+ this.playBtn.title = `${label} (k)`;
1248
1716
  }
1249
1717
  _toggleMute() { this.videoEl.muted = !this.videoEl.muted; }
1250
1718
  _onVolumeChange() {
1251
1719
  const v = this.videoEl;
1252
- const muted = v.muted || v.volume === 0;
1720
+ const vol = v.muted ? 0 : v.volume;
1721
+ const muted = vol === 0;
1722
+ const icon = muted ? ICON.volMute : vol < 0.4 ? ICON.volLow : vol < 0.75 ? ICON.volMid : ICON.volHigh;
1253
1723
  this.volBtn.innerHTML = '';
1254
- this.volBtn.appendChild(svgEl(muted ? ICON.volMute : ICON.volHigh, 18, 18));
1724
+ this.volBtn.appendChild(svgEl(icon, 18, 18));
1255
1725
  this.volBtn.setAttribute('aria-label', muted ? 'Unmute' : 'Mute');
1256
- this.volSlider.value = String(muted ? 0 : v.volume);
1726
+ this.volBtn.title = muted ? 'Unmute (m)' : 'Mute (m)';
1727
+ this.volSlider.value = String(vol);
1257
1728
  }
1258
1729
  _startSeekDrag() {
1259
1730
  this.seekDragging = true;
@@ -1401,15 +1872,33 @@ class PlayerUI {
1401
1872
  }, 2800);
1402
1873
  }
1403
1874
  _toggleFullscreen() {
1404
- if (document.fullscreenElement) {
1405
- document.exitFullscreen().catch(() => { });
1875
+ const doc = document;
1876
+ const root = this.root;
1877
+ const isFs = !!(document.fullscreenElement ||
1878
+ doc.webkitFullscreenElement ||
1879
+ doc.mozFullScreenElement ||
1880
+ doc.msFullscreenElement);
1881
+ if (isFs) {
1882
+ (document.exitFullscreen?.() ||
1883
+ doc.webkitExitFullscreen?.() ||
1884
+ doc.mozCancelFullScreen?.() ||
1885
+ (doc.msExitFullscreen?.(), Promise.resolve()))
1886
+ ?.catch?.(() => { });
1406
1887
  }
1407
1888
  else {
1408
- this.root.requestFullscreen().catch(() => { });
1889
+ (root.requestFullscreen?.() ||
1890
+ root.webkitRequestFullscreen?.() ||
1891
+ root.mozRequestFullScreen?.() ||
1892
+ (root.msRequestFullscreen?.(), Promise.resolve()))
1893
+ ?.catch?.(() => { });
1409
1894
  }
1410
1895
  }
1411
1896
  _onFsChange() {
1412
- const fs = !!document.fullscreenElement;
1897
+ const doc = document;
1898
+ const fs = !!(document.fullscreenElement ||
1899
+ doc.webkitFullscreenElement ||
1900
+ doc.mozFullScreenElement ||
1901
+ doc.msFullscreenElement);
1413
1902
  this.fsBtn.innerHTML = '';
1414
1903
  this.fsBtn.appendChild(svgEl(fs ? ICON.fsExit : ICON.fsEnter, 18, 18));
1415
1904
  this.fsBtn.setAttribute('aria-label', fs ? 'Exit fullscreen' : 'Enter fullscreen');
@@ -1438,7 +1927,7 @@ class PlayerUI {
1438
1927
  const sz = 60;
1439
1928
  d.style.cssText = `width:${sz}px;height:${sz}px;left:${e.clientX - rect.left - sz / 2}px;top:${e.clientY - rect.top - sz / 2}px`;
1440
1929
  this.root.appendChild(d);
1441
- setTimeout(() => d.remove(), 600);
1930
+ setTimeout(() => d.remove(), 650);
1442
1931
  }
1443
1932
  _onKeyDown(e) {
1444
1933
  switch (e.code) {
@@ -1493,6 +1982,9 @@ class PlayerUI {
1493
1982
  if (this.hideTimer)
1494
1983
  clearTimeout(this.hideTimer);
1495
1984
  document.removeEventListener('fullscreenchange', this._onFsChangeBound);
1985
+ document.removeEventListener('webkitfullscreenchange', this._onFsChangeBound);
1986
+ document.removeEventListener('mozfullscreenchange', this._onFsChangeBound);
1987
+ document.removeEventListener('MSFullscreenChange', this._onFsChangeBound);
1496
1988
  window.removeEventListener('mousemove', this._seekMouseMoveBound);
1497
1989
  window.removeEventListener('mouseup', this._seekMouseUpBound);
1498
1990
  window.removeEventListener('touchmove', this._seekTouchMoveBound);