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