Add 'Never Goes Dark' feature with animations for player and TV connection status

This commit is contained in:
Matt Batchelder
2026-02-21 12:35:28 -05:00
parent 201e4e4606
commit e5c3be6ec5
3 changed files with 510 additions and 4 deletions

View File

@@ -4535,3 +4535,428 @@ p:last-child { margin-bottom: 0; }
.uc-db-bar--3 { transform: scaleY(.40) !important; }
.uc-db-bar--4 { transform: scaleY(.62) !important; }
}
/* ═══════════════════════════════════════════════════════════
NEVER GOES DARK ANIMATION (.platform-visual.has-ngd)
Sequence (10 s loop):
0 30 % : Connected dots travel player → cloud, cloud green
30 45 % : Breaking line turns red, × icon fades in
45 72 % : Offline × visible, ✓ on player, TV still plays
72 85 % : Reconnect × fades, dots resume, cloud back to green
85 100% : Connected steady state before next loop
═══════════════════════════════════════════════════════════ */
/* ── Wrapper overrides ─────────────────────────────────────── */
.platform-visual.has-ngd {
background: none !important;
border: none !important;
border-radius: 0;
aspect-ratio: unset;
padding: 0;
overflow: visible;
position: relative;
font-size: inherit;
}
/* ── Stage ─────────────────────────────────────────────────── */
.ngd-stage {
position: relative;
width: 100%;
max-width: 400px;
aspect-ratio: 4/3;
margin: 0 auto;
}
/* ── TV ─────────────────────────────────────────────────────── */
.ngd-tv {
position: absolute;
left: 8px;
top: 18px;
display: flex;
flex-direction: column;
align-items: center;
}
.ngd-tv__body {
width: 160px;
height: 120px;
background: #111;
border: 4px solid #111;
border-radius: 6px 6px 4px 4px;
outline: 1px solid #000;
padding: 3px;
display: flex;
align-items: stretch;
position: relative;
box-shadow: 0 10px 36px rgba(0,0,0,.55);
}
/* HDMI port stub on right side */
.ngd-tv__port {
position: absolute;
right: -5px;
top: 50px;
width: 8px;
height: 14px;
background: #1a1a1a;
border: 1px solid #000;
border-radius: 2px;
}
.ngd-tv__screen {
width: 100%;
height: 100%;
border-radius: 2px;
position: relative;
overflow: hidden;
background:
repeating-linear-gradient(
180deg,
transparent, transparent 3px,
rgba(0,0,0,.10) 3px, rgba(0,0,0,.10) 4px
),
linear-gradient(135deg, #0c1016 0%, #151c28 60%, #0c1016 100%);
}
/* Scanline sweep */
.ngd-tv__screen::after {
content: '';
position: absolute;
left: 0; width: 100%; height: 3px;
background: linear-gradient(90deg, transparent, rgba(74,222,128,.22), transparent);
animation: da-scan 4s linear infinite;
pointer-events: none;
}
/* TV feet */
.ngd-tv__feet {
display: flex;
gap: 50px;
margin-top: 3px;
}
.ngd-tv__foot {
width: 22px;
height: 6px;
background: #181818;
border-radius: 0 0 3px 3px;
}
/* ── Menu board content (always playing) ───────────────────── */
.ngd-menu {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
padding: 7px 6px 4px;
gap: 3px;
}
.ngd-menu__hd {
display: flex;
align-items: center;
gap: 5px;
padding-bottom: 4px;
border-bottom: 1px solid rgba(255,200,80,.18);
flex-shrink: 0;
}
.ngd-menu__logo {
width: 9px; height: 9px;
background: #4CAF50;
border-radius: 50%;
flex-shrink: 0;
}
.ngd-menu__ttl {
flex: 1; height: 5px;
background: rgba(255,200,80,.45);
border-radius: 2px;
max-width: 55px;
}
.ngd-menu__cols {
display: flex;
gap: 6px;
flex: 1;
}
.ngd-menu__col {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
}
.ngd-menu__cat {
height: 4px;
background: rgba(76,175,80,.5);
border-radius: 1px;
margin-bottom: 1px;
}
.ngd-menu__row {
display: flex;
align-items: center;
gap: 3px;
padding: 2px 3px;
border-radius: 2px;
transition: background .3s;
}
.ngd-menu__row--hl {
background: rgba(76,175,80,.14);
animation: ngd-hl-pulse 3.5s ease-in-out infinite;
}
.ngd-menu__name {
flex: 1; height: 4px;
background: rgba(255,255,255,.25);
border-radius: 1px;
}
.ngd-menu__row--hl .ngd-menu__name {
background: rgba(76,175,80,.55);
}
.ngd-menu__price {
width: 16px; height: 4px;
background: rgba(255,200,80,.4);
border-radius: 1px;
}
.ngd-menu__row--hl .ngd-menu__price {
background: rgba(255,200,80,.7);
}
/* Scrolling ticker at bottom */
.ngd-menu__ticker {
height: 6px;
background: rgba(76,175,80,.12);
border-radius: 1px;
overflow: hidden;
flex-shrink: 0;
}
.ngd-menu__ticker-inner {
width: 250%;
height: 100%;
background: repeating-linear-gradient(
90deg,
rgba(76,175,80,.5) 0px, rgba(76,175,80,.5) 30px,
transparent 30px, transparent 50px
);
animation: ngd-ticker 4s linear infinite;
}
/* ── HDMI bridge ────────────────────────────────────────────── */
.ngd-hdmi {
position: absolute;
left: 168px;
top: 112px;
width: 14px;
height: 3px;
background: #252525;
border-radius: 1px;
}
/* ── Player device ──────────────────────────────────────────── */
.ngd-player {
position: absolute;
left: 182px;
top: 102px;
}
.ngd-player__body {
display: flex;
align-items: center;
gap: 5px;
width: 96px;
height: 28px;
background: #1a1a1a;
border: 1.5px solid #2d2d2d;
border-radius: 4px;
padding: 4px 6px;
box-shadow: 0 4px 16px rgba(0,0,0,.5), inset 0 1px 0 rgba(255,255,255,.04);
position: relative;
}
.ngd-player__led {
width: 6px;
height: 6px;
border-radius: 50%;
flex-shrink: 0;
background: #4CAF50;
box-shadow: 0 0 6px rgba(76,175,80,.8);
animation: ngd-led 10s ease-in-out infinite;
}
.ngd-player__brand {
flex: 1;
height: 5px;
background: rgba(255,255,255,.07);
border-radius: 2px;
}
.ngd-player__port {
width: 8px; height: 11px;
background: #0d0d0d;
border: 1px solid #222;
border-radius: 1px;
flex-shrink: 0;
}
/* Checkmark badge — visible only during offline phase */
.ngd-player__check {
position: absolute;
top: -14px;
right: -11px;
width: 24px; height: 24px;
opacity: 0;
transform: scale(0.4);
transform-origin: center;
animation: ngd-check-show 10s ease-in-out infinite;
filter: drop-shadow(0 0 4px rgba(76,175,80,.6));
}
/* ── Signal wrap (cloud + vertical line to player) ─────────── */
.ngd-signal-wrap {
position: absolute;
/* horizontally centred on player: player left=182px, width=96px → centre=230px */
left: 193px; /* 230 cloud_half(37px) = 193 */
top: 8px;
width: 74px;
display: flex;
flex-direction: column;
align-items: center;
}
.ngd-cloud {
width: 74px;
flex-shrink: 0;
}
.ngd-cloud__svg {
width: 100%;
height: auto;
display: block;
}
.ngd-cloud__path {
stroke: #4CAF50;
opacity: .85;
animation: ngd-cloud-color 10s ease-in-out infinite;
}
/* Vertical signal line */
.ngd-signal-line {
width: 3px;
height: 44px; /* gap from cloud bottom to player top */
background: rgba(76,175,80,.35);
border-radius: 2px;
position: relative;
overflow: visible;
animation: ngd-line-col 10s ease-in-out infinite;
}
/* Dots container — hidden during break phase */
.ngd-signal__dots {
position: absolute;
inset: 0;
overflow: hidden;
animation: ngd-dots-vis 10s linear infinite;
}
.ngd-signal__dot {
position: absolute;
left: 50%;
transform: translateX(-50%);
width: 5px; height: 5px;
border-radius: 50%;
background: #4CAF50;
box-shadow: 0 0 5px rgba(76,175,80,.8);
animation: ngd-dot-up 1.8s ease-in-out infinite;
}
.ngd-signal__dot--2 { animation-delay: -0.6s; }
.ngd-signal__dot--3 { animation-delay: -1.2s; }
/* Break × badge — centered on line */
.ngd-signal__break {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%) scale(0);
width: 18px; height: 18px;
opacity: 0;
animation: ngd-break-show 10s ease-in-out infinite;
filter: drop-shadow(0 0 4px rgba(239,68,68,.5));
}
/* ═══════════════════════════════════════════════════════════
KEYFRAMES (10 s cycle)
═══════════════════════════════════════════════════════════ */
/* Ticker scroll */
@keyframes ngd-ticker {
from { transform: translateX(0); }
to { transform: translateX(-50%); }
}
/* Highlighted menu row pulsing */
@keyframes ngd-hl-pulse {
0%, 100% { background: rgba(76,175,80,.14); }
50% { background: rgba(76,175,80,.28); }
}
/* Player LED: green → amber → red during disconnect → back to green */
@keyframes ngd-led {
0%, 29% { background: #4CAF50; box-shadow: 0 0 6px rgba(76,175,80,.8); }
33% { background: #f59e0b; box-shadow: 0 0 5px rgba(245,158,11,.6); }
38%, 71% { background: #ef4444; box-shadow: 0 0 4px rgba(239,68,68,.4); }
75% { background: #4CAF50; box-shadow: 0 0 6px rgba(76,175,80,.8); }
100% { background: #4CAF50; box-shadow: 0 0 6px rgba(76,175,80,.8); }
}
/* Cloud stroke: green → red → green */
@keyframes ngd-cloud-color {
0%, 29% { stroke: #4CAF50; opacity: .85; }
36%, 71% { stroke: #ef4444; opacity: 1; }
76%, 100% { stroke: #4CAF50; opacity: .85; }
}
/* Signal line colour */
@keyframes ngd-line-col {
0%, 29% { background: rgba(76,175,80,.35); }
36%, 71% { background: rgba(239,68,68,.45); }
76%, 100% { background: rgba(76,175,80,.35); }
}
/* Dots container: visible during connected phases only */
@keyframes ngd-dots-vis {
0%, 29% { opacity: 1; }
33% { opacity: 0; }
71% { opacity: 0; }
75%, 100% { opacity: 1; }
}
/* Single dot travelling bottom → top */
@keyframes ngd-dot-up {
0% { bottom: 1px; opacity: 0; }
8% { opacity: 1; }
85% { opacity: 1; }
100% { bottom: calc(100% - 5px); opacity: 0; }
}
/* Break × badge: appears when disconnected */
@keyframes ngd-break-show {
0%, 29% { opacity: 0; transform: translate(-50%,-50%) scale(0); }
38%, 71% { opacity: 1; transform: translate(-50%,-50%) scale(1); }
76%, 100% { opacity: 0; transform: translate(-50%,-50%) scale(0); }
}
/* Checkmark: appears when offline, confirming local playback */
@keyframes ngd-check-show {
0%, 44% { opacity: 0; transform: scale(0.4); }
52%, 71% { opacity: 1; transform: scale(1); }
77%, 100% { opacity: 0; transform: scale(0.4); }
}
/* ── Responsive ─────────────────────────────────────────────── */
@media (max-width: 640px) {
.ngd-stage { max-width: 320px; }
/* Scale the fixed-px elements down proportionally */
.ngd-tv__body { width: 128px; height: 96px; }
.ngd-tv__port { top: 40px; }
.ngd-tv__foot { width: 18px; }
.ngd-hdmi { left: 134px; top: 90px; }
.ngd-player { left: 146px; top: 82px; }
.ngd-player__body { width: 76px; height: 24px; }
.ngd-signal-wrap { left: 154px; width: 60px; }
.ngd-signal-line { height: 36px; }
}
/* ── Reduced-motion overrides ───────────────────────────────── */
@media (prefers-reduced-motion: reduce) {
.ngd-tv__screen::after,
.ngd-menu__ticker-inner,
.ngd-menu__row--hl,
.ngd-player__led,
.ngd-signal__dot,
.ngd-signal__dots,
.ngd-signal__break,
.ngd-cloud__path,
.ngd-signal-line,
.ngd-player__check { animation: none !important; }
/* Static fallback states */
.ngd-player__led { background: #4CAF50; box-shadow: 0 0 5px rgba(76,175,80,.6); }
.ngd-cloud__path { stroke: #4CAF50; opacity: .8; }
.ngd-signal__dots { opacity: 1; }
}