Files
KoalaSync/extension/popup.html
T

1762 lines
81 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>KoalaSync</title>
<script src="theme-init.js"></script>
<style>
@font-face {
font-family: 'Twemoji Country Flags';
unicode-range: U+1F1E6-1F1FF, U+1F3F4, U+E0062-E0063, U+E0065, U+E0067, U+E006C, U+E006E, U+E0073-E0074, U+E0077, U+E007F;
src: url('assets/TwemojiCountryFlags.woff2') format('woff2');
font-display: swap;
}
:root {
/* KoalaSync nature system: forest neutrals, eucalyptus brand, warm supporting accents. */
--bg-base: oklch(0.23 0.028 155);
--surface: oklch(0.30 0.035 150);
--surface-deep: oklch(0.205 0.025 155);
--surface-alt: oklch(0.265 0.032 150);
--border-soft: oklch(0.88 0.025 145 / 0.09);
--border-strong: oklch(0.39 0.038 145);
--text-primary: oklch(0.96 0.01 90);
--text-secondary: oklch(0.78 0.02 90);
--accent-green: oklch(0.73 0.13 155);
--accent-green-hover: oklch(0.79 0.12 155);
--accent-terracotta: oklch(0.62 0.14 45);
--success-green: oklch(0.72 0.14 135);
--warning-amber: oklch(0.76 0.14 80);
--danger: oklch(0.55 0.15 25);
--text-on-green: oklch(0.14 0.02 90);
--text-on-warm: oklch(0.16 0.025 45);
--bg: var(--surface-deep);
--card: var(--surface);
--accent: var(--accent-green);
--accent-hover: var(--accent-green-hover);
--text: var(--text-primary);
--text-muted: var(--text-secondary);
--success: var(--success-green);
--warning: var(--warning-amber);
--error: var(--danger);
--status-success-text: oklch(0.84 0.12 140);
--status-warning-text: oklch(0.86 0.13 80);
--status-error-text: oklch(0.78 0.13 25);
--status-neutral-text: oklch(0.76 0.02 110);
--radius: 12px;
--star: var(--warning);
color-scheme: dark;
}
html.theme-light {
--bg-base: oklch(0.965 0.012 105);
--surface: oklch(0.995 0.006 100);
--surface-deep: oklch(0.935 0.018 110);
--surface-alt: oklch(0.91 0.022 115);
--border-soft: oklch(0.28 0.035 140 / 0.10);
--border-strong: oklch(0.73 0.035 125);
--text-primary: oklch(0.21 0.03 140);
--text-secondary: oklch(0.43 0.03 135);
--accent-green: oklch(0.52 0.13 155);
--accent-green-hover: oklch(0.45 0.12 155);
--accent-terracotta: oklch(0.64 0.14 45);
--success-green: oklch(0.48 0.14 135);
--warning-amber: oklch(0.58 0.14 80);
--danger: oklch(0.57 0.17 25);
--text-on-green: oklch(0.16 0.03 140);
--text-on-warm: oklch(0.18 0.03 45);
--status-success-text: oklch(0.39 0.13 140);
--status-warning-text: oklch(0.43 0.12 75);
--status-error-text: oklch(0.46 0.16 25);
--status-neutral-text: oklch(0.43 0.02 120);
color-scheme: light;
}
/* ============================================================
THEME: CYBER — the original indigo / slate look, kept for
people who prefer the pre-nature design. Cool navy neutrals
with an electric indigo brand accent and a violet companion.
============================================================ */
html[data-palette="cyber"] {
--bg-base: oklch(0.215 0.042 265);
--surface: oklch(0.285 0.05 265);
--surface-deep: oklch(0.185 0.04 265);
--surface-alt: oklch(0.25 0.048 265);
--border-soft: oklch(0.9 0.03 265 / 0.10);
--border-strong: oklch(0.42 0.05 265);
--text-primary: oklch(0.97 0.012 265);
--text-secondary: oklch(0.74 0.03 265);
--accent-green: oklch(0.59 0.20 275);
--accent-green-hover: oklch(0.69 0.17 275);
--accent-terracotta: oklch(0.64 0.21 320);
--success-green: oklch(0.74 0.17 150);
--warning-amber: oklch(0.79 0.15 75);
--danger: oklch(0.63 0.20 25);
--text-on-green: oklch(0.99 0.006 275);
--text-on-warm: oklch(0.99 0.006 320);
--status-success-text: oklch(0.82 0.15 150);
--status-warning-text: oklch(0.85 0.14 78);
--status-error-text: oklch(0.78 0.16 25);
--status-neutral-text: oklch(0.74 0.03 265);
color-scheme: dark;
}
html[data-palette="cyber"].theme-light {
--bg-base: oklch(0.965 0.01 265);
--surface: oklch(0.995 0.005 265);
--surface-deep: oklch(0.945 0.014 265);
--surface-alt: oklch(0.915 0.02 265);
--border-soft: oklch(0.30 0.05 265 / 0.12);
--border-strong: oklch(0.79 0.03 265);
--text-primary: oklch(0.26 0.05 265);
--text-secondary: oklch(0.48 0.045 265);
--accent-green: oklch(0.53 0.21 275);
--accent-green-hover: oklch(0.46 0.20 275);
--accent-terracotta: oklch(0.55 0.23 320);
--success-green: oklch(0.52 0.16 150);
--warning-amber: oklch(0.62 0.15 72);
--danger: oklch(0.56 0.21 25);
--text-on-green: oklch(0.99 0.005 275);
--text-on-warm: oklch(0.99 0.005 320);
--status-success-text: oklch(0.45 0.15 150);
--status-warning-text: oklch(0.50 0.13 70);
--status-error-text: oklch(0.50 0.18 25);
--status-neutral-text: oklch(0.48 0.045 265);
color-scheme: light;
}
/* ============================================================
THEME: GRAPHITE — a quiet black-and-white minimal system.
Near-neutral cool greys, a monochrome accent (bright on dark,
ink on light); status hues are the only colour, kept muted so
they still read as success / warning / danger.
============================================================ */
html[data-palette="graphite"] {
--bg-base: oklch(0.19 0.004 260);
--surface: oklch(0.255 0.005 260);
--surface-deep: oklch(0.165 0.004 260);
--surface-alt: oklch(0.225 0.005 260);
--border-soft: oklch(0.92 0.008 260 / 0.11);
--border-strong: oklch(0.42 0.006 260);
--text-primary: oklch(0.97 0.003 260);
--text-secondary: oklch(0.72 0.005 260);
--accent-green: oklch(0.93 0.006 260);
--accent-green-hover: oklch(0.99 0.003 260);
--accent-terracotta: oklch(0.80 0.008 260);
--success-green: oklch(0.74 0.10 155);
--warning-amber: oklch(0.80 0.10 80);
--danger: oklch(0.64 0.14 25);
--text-on-green: oklch(0.20 0.004 260);
--text-on-warm: oklch(0.20 0.004 260);
--status-success-text: oklch(0.81 0.09 152);
--status-warning-text: oklch(0.84 0.10 80);
--status-error-text: oklch(0.76 0.12 25);
--status-neutral-text: oklch(0.72 0.005 260);
color-scheme: dark;
}
html[data-palette="graphite"].theme-light {
--bg-base: oklch(0.955 0.002 260);
--surface: oklch(1 0 0);
--surface-deep: oklch(0.928 0.003 260);
--surface-alt: oklch(0.90 0.004 260);
--border-soft: oklch(0.20 0.006 260 / 0.13);
--border-strong: oklch(0.77 0.004 260);
--text-primary: oklch(0.22 0.005 260);
--text-secondary: oklch(0.46 0.006 260);
--accent-green: oklch(0.28 0.006 260);
--accent-green-hover: oklch(0.18 0.005 260);
--accent-terracotta: oklch(0.44 0.008 260);
--success-green: oklch(0.51 0.12 152);
--warning-amber: oklch(0.60 0.12 72);
--danger: oklch(0.55 0.17 25);
--text-on-green: oklch(0.98 0.002 260);
--text-on-warm: oklch(0.98 0.002 260);
--status-success-text: oklch(0.45 0.12 152);
--status-warning-text: oklch(0.49 0.11 70);
--status-error-text: oklch(0.49 0.16 25);
--status-neutral-text: oklch(0.46 0.006 260);
color-scheme: light;
}
body {
width: 360px;
margin: 0;
padding: 18px;
box-sizing: border-box;
background: var(--bg);
color: var(--text);
font-family: 'Twemoji Country Flags', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
font-size: 14px;
background-image:
radial-gradient(circle at 12% -8%, oklch(0.68 0.13 150 / 0.12), transparent 42%),
radial-gradient(circle at 105% 12%, oklch(0.62 0.14 45 / 0.08), transparent 34%);
transition: background-color 0.25s ease, color 0.25s ease;
}
html.theme-light body {
background-image:
radial-gradient(circle at 8% -5%, oklch(0.72 0.11 145 / 0.24), transparent 40%),
radial-gradient(circle at 105% 5%, oklch(0.82 0.12 65 / 0.2), transparent 38%);
}
/* Per-theme ambient glow: each palette carries its own signature
corner wash so the backdrop matches the accent, not eucalyptus. */
html[data-palette="cyber"] body {
background-image:
radial-gradient(circle at 12% -8%, oklch(0.64 0.19 275 / 0.16), transparent 42%),
radial-gradient(circle at 105% 12%, oklch(0.64 0.21 320 / 0.11), transparent 34%);
}
html[data-palette="cyber"].theme-light body {
background-image:
radial-gradient(circle at 8% -5%, oklch(0.60 0.20 275 / 0.16), transparent 40%),
radial-gradient(circle at 105% 5%, oklch(0.62 0.20 320 / 0.14), transparent 38%);
}
html[data-palette="graphite"] body {
background-image:
radial-gradient(circle at 12% -8%, oklch(0.93 0.006 260 / 0.05), transparent 44%);
}
html[data-palette="graphite"].theme-light body {
background-image:
radial-gradient(circle at 12% -8%, oklch(0.20 0.006 260 / 0.045), transparent 46%);
}
/* The Create Room button carries an inline eucalyptus glow; retint
its shadow per palette so the hero action stays on-theme. */
html[data-palette="cyber"] #createRoomBtn,
html[data-palette="cyber"] #syncTabCreateRoomBtn {
box-shadow: 0 4px 12px oklch(0.64 0.19 275 / 0.4) !important;
}
html[data-palette="graphite"] #createRoomBtn,
html[data-palette="graphite"] #syncTabCreateRoomBtn {
box-shadow: 0 4px 12px oklch(0.20 0.006 260 / 0.4) !important;
}
/* Cyber's brand ink is white (for the dark indigo accent), but its
success/warning fills are bright — so the few surfaces that print
text directly on those fills need dark ink to stay legible. */
html[data-palette="cyber"] .toast-success,
html[data-palette="cyber"] .toast-warning,
html[data-palette="cyber"] .copy-success {
color: oklch(0.22 0.03 265) !important;
}
/* Theme cross-fade: enabled only for the ~260ms around a user-initiated
theme/appearance switch (added by popup.js), so colours glide instead
of snapping — without slowing down normal hover/state transitions. */
html.theme-anim,
html.theme-anim * {
transition: background-color 0.24s ease, border-color 0.24s ease,
color 0.24s ease, box-shadow 0.24s ease, fill 0.24s ease !important;
}
@media (prefers-reduced-motion: reduce) {
html.theme-anim,
html.theme-anim * { transition: none !important; }
}
.header-row {
display: flex;
align-items: center;
margin-bottom: 14px;
padding: 2px 2px 0;
}
h1 {
font-size: 18px;
margin: 0;
color: var(--accent);
letter-spacing: 1px;
text-transform: uppercase;
display: inline-flex;
align-items: center;
gap: 8px;
}
h1 img {
width: 42px;
height: 42px;
object-fit: contain;
border-radius: 4px;
}
.popup-version {
margin-left: auto;
display: flex;
align-items: center;
gap: 4px;
font-size: 10px;
color: var(--text-muted);
text-decoration: none;
opacity: 0.72;
transition: opacity 0.2s, color 0.2s;
}
.popup-version:hover {
opacity: 1;
color: var(--accent);
}
.popup-version svg {
width: 12px;
height: 12px;
display: block;
}
/* Tabs */
.tabs {
display: flex;
gap: 4px;
background: var(--card);
padding: 4px;
border-radius: var(--radius);
margin-bottom: 16px;
border: 1px solid var(--border-soft);
}
.tab-btn {
flex: 1;
padding: 8px;
border: none;
background: transparent;
color: var(--text-muted);
cursor: pointer;
font-weight: 600;
border-radius: 8px;
min-height: 36px;
transition: background-color 0.2s, color 0.2s, box-shadow 0.2s;
white-space: nowrap;
}
.tab-btn.active {
background: var(--accent);
color: var(--text-on-green);
box-shadow: 0 5px 14px oklch(0.38 0.10 150 / 0.22);
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
animation: fadeIn 0.3s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(4px); }
to { opacity: 1; transform: translateY(0); }
}
/* Forms */
.form-group {
margin-bottom: 12px;
}
label {
display: block;
font-size: 12px;
color: var(--text-muted);
margin-bottom: 4px;
font-weight: 650;
letter-spacing: 0.01em;
}
input, select, option {
width: 100%;
box-sizing: border-box;
background: var(--card);
border: 1px solid var(--border-strong);
color: var(--text);
padding: 10px;
border-radius: 8px;
outline: none;
font-family: inherit;
min-height: 40px;
transition: border-color 0.2s, box-shadow 0.2s, background-color 0.2s;
}
input:focus,
select:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px color-mix(in oklch, var(--accent), transparent 82%);
}
button.primary {
width: 100%;
padding: 12px;
background: var(--accent);
border: none;
color: var(--text-on-green);
font-weight: 700;
border-radius: 8px;
cursor: pointer;
transition: background-color 0.2s, box-shadow 0.2s, transform 0.2s;
margin-top: 8px;
}
button.primary:hover {
background: var(--accent-hover);
transform: translateY(-1px);
}
button.secondary {
width: 100%;
padding: 10px;
background: var(--border-strong);
border: none;
color: var(--text);
font-weight: 600;
border-radius: 8px;
cursor: pointer;
margin-top: 8px;
}
button.secondary:hover {
background: oklch(0.38 0.035 125);
}
details > summary:focus-visible {
outline: 2px solid var(--accent-hover) !important;
outline-offset: 3px;
border-radius: 4px;
}
button:focus-visible,
a:focus-visible,
input:focus-visible,
select:focus-visible {
outline: 2px solid var(--accent-hover);
outline-offset: 2px;
}
button:disabled {
cursor: not-allowed;
opacity: 0.48;
transform: none !important;
}
.copy-success {
background: var(--success) !important;
border-color: var(--success) !important;
color: var(--text-on-green) !important;
opacity: 1 !important;
transform: scale(0.95) !important;
transition: transform 0.15s ease-out !important;
}
/* Info Cards */
.info-card {
background: var(--card);
padding: 12px;
border-radius: 10px;
margin-bottom: 12px;
border: 1px solid var(--border-strong);
box-shadow: 0 8px 22px rgba(0, 0, 0, 0.08);
}
.site-access-notice {
display: none;
margin-top: 8px;
margin-bottom: 0;
border-left: 4px solid var(--warning);
background: color-mix(in oklch, var(--warning), var(--card) 90%);
color: var(--text);
font-size: 11px;
line-height: 1.45;
}
.site-access-notice button {
width: auto;
margin-top: 8px;
padding: 6px 10px;
font-size: 11px;
}
.peer-item {
padding: 10px 0;
border-bottom: 1px solid var(--border-strong);
}
.peer-item:last-child { border: 0; }
/* Native, thin modern scrollbars (2026 standard compliant) */
.info-card,
#publicRooms,
#historyList,
#videoDebug,
#lastActionCard,
#logList {
scrollbar-width: thin;
scrollbar-color: var(--border-strong) transparent;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
display: inline-block;
margin-right: 6px;
}
.status-online {
background: var(--success);
box-shadow: 0 0 8px var(--success);
animation: pulseGreen 2s infinite ease-in-out;
}
.status-offline { background: var(--error); }
.status-connecting {
background: var(--warning);
box-shadow: 0 0 8px color-mix(in oklch, var(--warning), transparent 30%);
}
.status-idle { background: var(--status-neutral-text); }
@keyframes pulseGreen {
0% { box-shadow: 0 0 0 0 color-mix(in oklch, var(--success), transparent 30%); }
70% { box-shadow: 0 0 0 6px color-mix(in oklch, var(--success), transparent 100%); }
100% { box-shadow: 0 0 0 0 color-mix(in oklch, var(--success), transparent 100%); }
}
/* Badge and Capsule styles for peers and state */
.badge {
display: inline-block;
padding: 2px 6px;
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
border-radius: 6px;
line-height: 1.2;
vertical-align: middle;
margin-left: 4px;
}
.badge-ready {
background: color-mix(in oklch, var(--success), transparent 85%);
color: var(--status-success-text);
border: 1px solid color-mix(in oklch, var(--success), transparent 70%);
}
.badge-loading {
background: color-mix(in oklch, var(--warning), transparent 85%);
color: var(--status-warning-text);
border: 1px solid color-mix(in oklch, var(--warning), transparent 70%);
}
.lobby-peer-item {
display: inline-flex;
align-items: center;
background: var(--bg-base);
border: 1px solid var(--border-soft);
padding: 4px 8px;
border-radius: 6px;
font-size: 11px;
margin-right: 6px;
margin-bottom: 6px;
color: var(--text-primary);
}
/* Logs */
#logList {
max-height: 200px;
overflow-y: auto;
font-size: 10px;
font-family: monospace;
background: var(--surface-deep);
padding: 8px;
border-radius: 8px;
color: var(--text-secondary);
}
.log-entry { margin-bottom: 4px; }
.log-error { color: var(--status-error-text); }
.log-success { color: var(--status-success-text); }
.conn-ping {
min-width: 48px;
min-height: 24px;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 3px 7px;
border: 1px solid var(--border-soft);
border-radius: 999px;
background: var(--surface-deep);
color: var(--status-neutral-text);
font-family: ui-monospace, "SFMono-Regular", Consolas, monospace;
font-size: 10px;
font-weight: 700;
line-height: 1;
}
.conn-ping:empty { display: none; }
.conn-ping-good {
border-color: color-mix(in oklch, var(--success), transparent 70%);
background: color-mix(in oklch, var(--success), var(--surface-deep) 88%);
color: var(--status-success-text);
}
.conn-ping-warning {
border-color: color-mix(in oklch, var(--warning), transparent 62%);
background: color-mix(in oklch, var(--warning), var(--surface-deep) 86%);
color: var(--status-warning-text);
}
.conn-ping-bad {
border-color: color-mix(in oklch, var(--error), transparent 62%);
background: color-mix(in oklch, var(--error), var(--surface-deep) 87%);
color: var(--status-error-text);
}
/* Invite Link */
.invite-box {
display: flex;
gap: 8px;
margin-top: 8px;
}
.invite-box input { flex: 1; font-size: 11px; }
.invite-box button { width: 40px; padding: 0; }
/* Toast Notifications */
#toast-container {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 9999;
display: flex;
flex-direction: column;
align-items: center;
pointer-events: none;
}
.toast {
pointer-events: auto;
padding: 10px 16px;
margin-bottom: 6px;
border-radius: 8px;
font-size: 12px;
font-weight: 600;
max-width: 280px;
text-align: center;
animation: toastSlideIn 0.3s ease-out, toastFadeOut 0.3s ease-in 2.7s forwards;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
}
.toast-success { background: var(--success); color: var(--text-on-green); }
.toast-error { background: var(--error); color: white; }
.toast-info { background: var(--accent); color: var(--text-on-green); }
.toast-warning { background: var(--warning); color: var(--text-on-warm); }
@keyframes toastSlideIn {
from { opacity: 0; transform: translateY(-20px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes toastFadeOut {
from { opacity: 1; }
to { opacity: 0; transform: translateY(-10px); }
}
/* Eucalyptus confetti: tiny leaves bursting from the copy-invite
button (spawned by popup.js, mirrors the website's effect). The
outer span owns the flight path — fast burst that brakes at its
peak, then sinks — while the inner leaf rocks around its stem. */
.click-leaf {
position: fixed;
z-index: 3000;
margin: -5px 0 0 -4px;
pointer-events: none;
opacity: 0;
animation: leafBurst var(--leaf-dur, 1s) forwards;
will-change: transform, opacity;
}
.click-leaf i {
display: block;
width: 7px;
height: 10px;
border-radius: 0 70% 0 70%;
background: linear-gradient(135deg, oklch(0.68 0.13 150 / 0.95), oklch(0.5 0.1 145 / 0.85));
transform: rotate(var(--leaf-rot, 0deg));
animation: leafRock var(--flutter-dur, 0.55s) ease-in-out infinite alternate;
}
.click-leaf-sage i {
background: linear-gradient(135deg, oklch(0.75 0.09 140 / 0.9), oklch(0.58 0.08 145 / 0.8));
}
.click-leaf-amber i {
background: linear-gradient(135deg, oklch(0.68 0.14 55 / 0.95), oklch(0.55 0.13 40 / 0.85));
}
/* Non-eucalyptus themes retint the burst from their own accent pair so
the particles stay on-theme and, because those vars flip with
light/dark, remain visible against every background. */
html[data-palette="cyber"] .click-leaf i,
html[data-palette="graphite"] .click-leaf i {
background: linear-gradient(135deg, var(--accent), var(--accent-hover));
}
html[data-palette="cyber"] .click-leaf-sage i,
html[data-palette="graphite"] .click-leaf-sage i {
background: linear-gradient(135deg, var(--accent-hover), var(--accent));
}
html[data-palette="cyber"] .click-leaf-amber i {
background: linear-gradient(135deg, var(--accent-terracotta), var(--accent));
}
html[data-palette="graphite"] .click-leaf-amber i {
background: linear-gradient(135deg, var(--text-secondary), var(--accent));
}
@keyframes leafBurst {
0% {
opacity: 0;
transform: translate(0, 0) scale(var(--leaf-scale, 1));
animation-timing-function: cubic-bezier(0.16, 0.84, 0.44, 1);
}
10% { opacity: 1; }
45% {
opacity: 1;
transform: translate(var(--leaf-x, 0), var(--leaf-y, -14px)) scale(var(--leaf-scale, 1));
animation-timing-function: cubic-bezier(0.55, 0.06, 0.68, 0.19);
}
100% {
opacity: 0;
transform: translate(calc(var(--leaf-x, 0) * 1.4), calc(var(--leaf-y, -14px) + 30px)) scale(calc(var(--leaf-scale, 1) * 0.92));
}
}
@keyframes leafRock {
from { transform: rotate(calc(var(--leaf-rot, 0deg) - 24deg)); }
to { transform: rotate(calc(var(--leaf-rot, 0deg) + 24deg)); }
}
@media (prefers-reduced-motion: reduce) {
.click-leaf { display: none; }
}
@keyframes fadeSlideIn {
from { opacity: 0; transform: translateX(5px); }
to { opacity: 1; transform: translateX(0); }
}
.tab-active-animate {
animation: fadeSlideIn 0.15s ease-out forwards;
}
/* Toggle Switch CSS */
.toggle-switch {
position: relative;
display: inline-block;
width: 36px;
height: 20px;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0; left: 0; right: 0; bottom: 0;
background-color: var(--border-strong);
transition: .3s;
border-radius: 20px;
}
.slider:before {
position: absolute;
content: "";
height: 14px;
width: 14px;
left: 3px;
bottom: 3px;
background-color: var(--status-neutral-text);
transition: .3s;
border-radius: 50%;
}
input:checked + .slider {
background-color: var(--accent);
}
input:checked + .slider:before {
transform: translateX(16px);
background-color: white;
}
.toggle-switch:has(input:focus-visible) .slider {
outline: 2px solid var(--accent-hover);
outline-offset: 3px;
}
.popup-footer {
margin-top: 16px;
padding: 11px 2px 2px;
border-top: 1px solid var(--border-soft);
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.footer-review {
flex: none;
min-height: 30px;
display: inline-flex;
align-items: center;
padding: 4px 8px;
border: 1px solid color-mix(in oklch, var(--warning), transparent 68%);
border-radius: 7px;
background: color-mix(in oklch, var(--warning), transparent 91%);
color: var(--status-warning-text) !important;
font-size: 11px;
font-weight: 700;
}
.footer-links {
display: flex;
align-items: center;
gap: 4px;
}
.footer-links a {
min-height: 30px;
display: inline-flex;
align-items: center;
padding: 4px 7px;
border-radius: 6px;
font-size: 11px;
font-weight: 600;
}
.footer-links a:hover {
background: var(--surface-alt);
}
/* Unified interaction layer: the popup is a compact control surface,
so every actionable target gets the same tactile and keyboard cues. */
button,
select,
summary,
a {
-webkit-tap-highlight-color: transparent;
}
button:not(:disabled):active,
.tab-btn:not(:disabled):active {
transform: translateY(1px) scale(0.985);
}
details.form-group {
background: var(--card);
border: 1px solid var(--border-strong);
border-radius: var(--radius);
margin-bottom: 12px;
transition: border-color 0.2s, background-color 0.2s;
overflow: hidden;
}
details.form-group:hover {
border-color: var(--accent);
}
details.form-group > summary {
min-height: 38px;
padding: 10px 14px;
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
font-weight: 700;
font-size: 13px;
color: var(--text-primary);
user-select: none;
list-style: none;
}
details.form-group > summary::-webkit-details-marker {
display: none;
}
details.form-group > summary::after {
content: "";
display: inline-block;
width: 6px;
height: 6px;
border-right: 2px solid var(--text-muted);
border-bottom: 2px solid var(--text-muted);
transform: rotate(-45deg);
transition: transform 0.2s ease;
margin-right: 4px;
}
details[open].form-group > summary::after {
transform: rotate(45deg);
}
details.form-group > .details-content {
padding: 4px 14px 14px 14px;
border-top: 1px solid var(--border-soft);
display: flex;
flex-direction: column;
gap: 12px;
}
/* Settings are deliberately quieter than the control tabs: strong
grouping, neutral controls, and no decorative emoji iconography. */
#tab-settings {
display: none;
}
#tab-settings.active {
display: block;
}
#tab-settings details.form-group {
background: color-mix(in oklch, var(--card), var(--bg) 12%);
border-color: var(--border-soft);
box-shadow: 0 10px 24px oklch(0.08 0.02 140 / 0.16);
}
#tab-settings details.form-group:hover {
border-color: var(--border-strong);
}
#tab-settings details.form-group[open] {
border-color: color-mix(in oklch, var(--accent), var(--border-strong) 72%);
}
#tab-settings details.form-group > summary {
min-height: 44px;
padding: 0 14px;
font-size: 12px;
font-weight: 750;
letter-spacing: 0.025em;
}
#tab-settings details.form-group > .details-content {
gap: 0;
padding: 2px 14px 8px;
}
.settings-row {
display: grid;
grid-template-columns: minmax(0, 1fr) 154px;
align-items: center;
gap: 14px;
min-height: 54px;
border-bottom: 1px solid var(--border-soft);
}
.settings-row:last-child {
border-bottom: 0;
}
.settings-row > label:not(.toggle-switch) {
margin: 0;
color: var(--text);
font-size: 12px;
font-weight: 600;
line-height: 1.3;
letter-spacing: 0;
text-transform: none;
}
.transport-actions,
.sync-actions {
display: flex;
gap: 8px;
margin-bottom: 12px;
}
.sync-actions {
margin-bottom: 15px;
align-items: stretch;
}
.transport-actions .primary,
.sync-actions .primary {
flex: 1;
margin-top: 0;
border: 1px solid transparent;
}
#playBtn {
background: var(--accent);
color: var(--text-on-green);
}
#pauseBtn {
background: var(--surface-alt);
border-color: var(--border-strong);
color: var(--text);
}
#pauseBtn:hover {
background: color-mix(in oklch, var(--surface-alt), var(--text) 7%);
}
#forceSyncBtn {
background: transparent;
border-color: color-mix(in oklch, var(--accent), transparent 42%);
color: var(--accent-hover);
}
#forceSyncBtn:hover {
background: color-mix(in oklch, var(--accent), transparent 88%);
}
.action-icon::before {
content: "";
width: 13px;
height: 13px;
display: inline-block;
margin-right: 6px;
vertical-align: -2px;
background: currentColor;
mask-position: center;
mask-repeat: no-repeat;
mask-size: contain;
}
#playBtn::before {
mask-image: url("icons/play.svg");
}
#pauseBtn::before {
mask-image: url("icons/pause.svg");
}
#forceSyncBtn::before {
mask-image: url("icons/sync.svg");
}
.settings-control,
#tab-settings input[type="text"] {
width: 154px;
height: 40px;
min-height: 40px;
margin: 0;
border-radius: 9px;
border-color: var(--border-strong);
background-color: var(--surface-deep);
color: var(--text);
font-size: 12px;
font-weight: 600;
line-height: 38px;
}
#tab-settings input[type="text"] {
padding: 0 11px;
}
.settings-select {
position: relative;
width: 154px;
}
.settings-select::after {
content: "";
position: absolute;
top: 50%;
right: 12px;
width: 6px;
height: 6px;
border-right: 1.5px solid var(--text-muted);
border-bottom: 1.5px solid var(--text-muted);
transform: translateY(-68%) rotate(45deg);
pointer-events: none;
}
.settings-select select {
appearance: none;
display: block;
padding: 0 32px 0 11px;
cursor: pointer;
text-align: left;
text-align-last: left;
}
.theme-picker {
position: relative;
display: grid;
grid-template-columns: repeat(3, 1fr);
width: 154px;
padding: 3px;
border: 1px solid var(--border-strong);
border-radius: 10px;
background: var(--surface-deep);
}
.theme-picker select {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
border: 0;
}
.theme-choice {
min-width: 0;
min-height: 32px;
margin: 0;
padding: 0 4px;
border: 0;
border-radius: 7px;
background: transparent;
color: var(--text-muted);
font-family: inherit;
font-size: 10px;
font-weight: 700;
line-height: 1;
cursor: pointer;
transition: background-color 0.18s ease, color 0.18s ease, box-shadow 0.18s ease;
}
.theme-choice:hover {
color: var(--text);
}
.theme-choice.active {
background: var(--surface-alt);
color: var(--text);
box-shadow: 0 1px 5px oklch(0.08 0.02 150 / 0.32), inset 0 0 0 1px var(--border-soft);
}
/* Theme (colour palette) picker: a full-width row of swatch cards so
each curated scheme previews its own colours before selection. */
.palette-row {
display: block;
padding: 12px 0 4px;
border-bottom: 1px solid var(--border-soft);
}
.palette-row > label {
margin: 0 0 9px;
color: var(--text);
font-size: 12px;
font-weight: 600;
letter-spacing: 0;
text-transform: none;
}
.palette-picker {
position: relative;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
}
.palette-picker select {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
border: 0;
}
.palette-choice {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
min-height: 62px;
margin: 0;
padding: 10px 4px 9px;
border: 1px solid var(--border-strong);
border-radius: 10px;
background: var(--surface-deep);
color: var(--text-muted);
font-family: inherit;
font-size: 11px;
font-weight: 700;
line-height: 1;
cursor: pointer;
transition: border-color 0.18s ease, background-color 0.18s ease, color 0.18s ease, box-shadow 0.18s ease, transform 0.18s ease;
}
.palette-choice:hover {
color: var(--text);
border-color: var(--accent);
transform: translateY(-1px);
}
.palette-choice.active {
color: var(--text);
border-color: var(--accent);
background: color-mix(in oklch, var(--accent) 10%, var(--surface-deep));
box-shadow: 0 0 0 1px var(--accent), 0 5px 14px oklch(0.08 0.02 260 / 0.28);
}
.palette-swatch {
display: inline-flex;
padding: 3px;
border-radius: 999px;
background: var(--surface-alt);
box-shadow: inset 0 0 0 1px var(--border-soft);
}
.palette-swatch i {
width: 13px;
height: 13px;
border-radius: 50%;
display: block;
box-shadow: inset 0 0 0 1px oklch(0 0 0 / 0.22);
}
.palette-swatch i + i {
margin-left: -5px;
}
.palette-labeltext {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
}
.palette-name {
font-size: 11px;
font-weight: 700;
line-height: 1.1;
}
.palette-desc {
font-size: 9px;
font-weight: 600;
line-height: 1.1;
letter-spacing: 0.01em;
color: var(--text-muted);
opacity: 0.85;
}
.palette-choice.active .palette-desc {
color: var(--accent);
opacity: 1;
}
/* Only the opt-in Dev tab is de-emphasized. tab-dev is the user-facing
"Status" tab and must match the other tabs. */
#devToolsTabBtn {
color: color-mix(in oklch, var(--text-muted), transparent 18%);
font-size: 11px;
}
#devToolsTabBtn.active {
color: var(--text-on-green);
}
.settings-row .toggle-switch {
justify-self: end;
margin: 0 2px 0 0;
}
.settings-action {
justify-self: end;
min-width: 76px;
min-height: 34px;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0 12px;
border: 1px solid color-mix(in oklch, var(--accent), transparent 62%);
border-radius: 9px;
background: color-mix(in oklch, var(--accent), transparent 90%);
color: var(--accent);
font-size: 11px;
font-weight: 750;
text-decoration: none;
}
.settings-action:hover {
background: color-mix(in oklch, var(--accent), transparent 84%);
}
.settings-tools {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
padding: 12px 0 8px;
}
.settings-tools button.secondary {
min-height: 38px;
margin: 0;
padding: 8px;
font-size: 11px;
}
.settings-note {
margin: 0;
color: var(--text-muted);
font-size: 10px;
line-height: 1.45;
text-align: left;
}
.settings-issue-link {
margin: 8px 0 2px;
padding-top: 9px;
border-top: 1px solid var(--border-soft);
font-size: 10px;
text-align: left;
}
.settings-issue-link a {
color: var(--text-muted);
text-decoration: none;
}
.settings-issue-link a:hover {
color: var(--accent);
}
#section-join > .info-card,
#section-active > .info-card,
#tab-sync .info-card {
position: relative;
overflow: hidden;
}
#section-active > .info-card:first-child::before {
content: "";
position: absolute;
inset: 0 auto 0 0;
width: 3px;
background: var(--accent);
}
#playBtn,
#pauseBtn,
#forceSyncBtn {
min-height: 42px;
}
#leaveBtn {
background: transparent;
border: 1px solid color-mix(in oklch, var(--error), transparent 35%);
}
#leaveBtn:hover {
background: color-mix(in oklch, var(--error), transparent 88%);
}
.popup-footer a {
color: var(--text-muted);
text-decoration: none;
transition: color 0.2s, opacity 0.2s;
}
.popup-footer a:hover {
color: var(--accent);
opacity: 1;
}
/* Light theme polish: preserve the same depth hierarchy without
carrying dark-only inline colors into form controls and cards. */
html.theme-light .tabs,
html.theme-light .info-card,
html.theme-light details.form-group,
html.theme-light .form-group {
box-shadow: 0 8px 24px oklch(0.22 0.035 140 / 0.07);
}
html.theme-light input,
html.theme-light select,
html.theme-light option {
color: var(--text) !important;
background-color: var(--surface) !important;
}
html.theme-light button.secondary {
background: var(--surface-alt);
color: var(--text);
border: 1px solid var(--border-strong);
}
html.theme-light button.secondary:hover {
background: oklch(0.86 0.035 125);
}
html.theme-light .mock-card,
html.theme-light .peer-item {
border-color: var(--border-strong);
}
html.theme-light .slider:before {
background-color: oklch(0.62 0.025 125);
}
html.theme-light input:checked + .slider:before {
background-color: white;
}
html.theme-light .popup-footer {
border-top-color: var(--border-soft);
}
html.theme-light .tab-btn.active {
color: var(--text-on-green);
}
.feature-hint {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--accent);
margin-left: 4px;
animation: featurePulse 2s ease-in-out infinite;
cursor: help;
vertical-align: middle;
}
@keyframes featurePulse {
0%, 100% { opacity: 0.4; transform: scale(1); }
50% { opacity: 1; transform: scale(1.3); }
}
@keyframes spotlightPulse {
0%, 100% { box-shadow: 0 0 0 9999px rgba(16, 25, 14, 0.65), 0 0 0 0px var(--accent); }
50% { box-shadow: 0 0 0 9999px rgba(16, 25, 14, 0.65), 0 0 0 6px var(--accent); }
}
@keyframes floatHint {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-4px); }
}
</style>
</head>
<body>
<div id="toast-container"></div>
<main>
<div class="header-row">
<h1 data-i18n="APP_TITLE"><img src="icons/icon128.png" alt="KoalaSync Logo">KoalaSync</h1>
<a href="https://github.com/Shik3i/KoalaSync" target="_blank" class="popup-version">
<svg viewBox="0 0 16 16" fill="currentColor">
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/>
</svg>
<span id="popupVersion">v0.0.0</span>
</a>
</div>
<div class="tabs">
<button class="tab-btn active" data-tab="tab-room" data-i18n="TAB_ROOM" data-i18n-title="TAB_ROOM_TOOLTIP" title="Room settings and connection">Room</button>
<button class="tab-btn" data-tab="tab-sync" data-i18n="TAB_SYNC" data-i18n-title="TAB_SYNC_TOOLTIP" title="Video sync controls and remote actions">Sync</button>
<button class="tab-btn" data-tab="tab-settings" data-i18n="TAB_SETTINGS" data-i18n-title="TAB_SETTINGS_TOOLTIP" title="Extension preferences">Settings</button>
<button class="tab-btn" data-tab="tab-dev" data-i18n="TAB_STATUS" data-i18n-title="TAB_STATUS_TOOLTIP" title="Advanced Diagnostics & Logs">Status</button>
<button id="devToolsTabBtn" class="tab-btn" data-tab="tab-devtools" style="display:none;">Dev</button>
</div>
<!-- Room Tab -->
<div id="tab-room" class="tab-content active">
<!-- JOIN SECTION: Visible when not in a room -->
<div id="section-join">
<button id="createRoomBtn" class="primary" style="margin-bottom: 16px; padding: 14px; font-size: 15px; box-shadow: 0 4px 12px oklch(0.40 0.10 155 / 0.4);" data-i18n="BTN_CREATE_ROOM">+ Create New Room</button>
<details class="form-group" name="settings-accordion">
<summary data-i18n="MANUAL_CONNECT_HEADER">Manual Connect / Advanced</summary>
<div class="details-content">
<!-- Server selection — its own concern, kept visually separate from the room fields. -->
<div style="background: var(--bg); border: 1px solid var(--border-strong); border-radius: 8px; padding: 10px; margin-bottom: 14px;">
<div class="form-group" style="margin-bottom:0;">
<label title="Select which KoalaSync server to use" data-i18n="LABEL_SERVER">Server</label>
<div style="display:flex; gap:4px; margin-bottom:0;">
<button id="serverOfficial" class="tab-btn active" style="flex:1; padding:6px; font-size:11px;" data-i18n="BTN_SERVER_OFFICIAL" data-i18n-title="BTN_SERVER_OFFICIAL_TOOLTIP" title="Use the official reliable server">Official</button>
<button id="serverCustom" class="tab-btn" style="flex:1; padding:6px; font-size:11px;" data-i18n="BTN_SERVER_CUSTOM" data-i18n-title="BTN_SERVER_CUSTOM_TOOLTIP" title="Connect to your own self-hosted server">Custom</button>
</div>
<input type="text" id="serverUrl" data-i18n-placeholder="PLACEHOLDER_SERVER_URL" placeholder="wss://your-server:3000" style="display:none; margin-top:8px;">
</div>
</div>
<!-- Room credentials — distinct group below the server picker. -->
<div class="form-group">
<label for="roomId" title="The unique identifier for your sync room" data-i18n="LABEL_ROOM_ID" data-i18n-title="LABEL_ROOM_ID_TOOLTIP">Room ID</label>
<input type="text" id="roomId" maxlength="64" data-i18n-placeholder="PLACEHOLDER_ROOM_ID" data-i18n-title="PLACEHOLDER_ROOM_ID_TOOLTIP" placeholder="Enter Room ID" title="The unique ID of the room you want to join">
</div>
<div class="form-group">
<label for="password" title="Optional password to restrict room access" data-i18n="LABEL_PASSWORD" data-i18n-title="LABEL_PASSWORD_TOOLTIP">Password (Optional)</label>
<input type="password" id="password" data-i18n-placeholder="PLACEHOLDER_PASSWORD" data-i18n-title="PLACEHOLDER_PASSWORD_TOOLTIP" placeholder="Room Password (optional)" title="Password for the room (leave empty if none)">
</div>
<div id="roomError" style="display:none; color:var(--error); font-size:11px; margin-bottom:8px; text-align:center;"></div>
<button id="joinBtn" class="primary" data-i18n="BTN_JOIN_ROOM" data-i18n-title="BTN_JOIN_ROOM_TOOLTIP" title="Connect to the room">Join Room</button>
<div style="display: flex; justify-content: space-between; align-items: center; margin-top: 1.5rem; margin-bottom: 8px;">
<label style="margin:0;" title="List of publicly available rooms on this server" data-i18n="LABEL_PUBLIC_ROOMS" data-i18n-title="LABEL_PUBLIC_ROOMS_TOOLTIP">Public Rooms</label>
<button id="refreshRooms" class="secondary" style="background:transparent; border:none; color:var(--accent); font-size:10px; cursor:pointer;" data-i18n="BTN_REFRESH" data-i18n-title="BTN_REFRESH_TOOLTIP" title="Refresh the list of public rooms">REFRESH</button>
</div>
<div id="publicRooms" class="info-card" style="max-height: 120px; overflow-y: auto; padding: 4px;">
<div style="text-align:center; color: var(--text-muted); font-size: 11px; padding: 10px;" data-i18n="PUBLIC_ROOMS_REFRESHING">Refreshing...</div>
</div>
</div>
</details>
</div>
<!-- ACTIVE SECTION: Visible when in a room -->
<div id="section-active" style="display:none;">
<div class="info-card" style="margin-bottom: 20px; display: flex; justify-content: space-between; align-items: center; border-left: 4px solid var(--accent);">
<div>
<label style="margin-bottom: 0;" title="The room you are currently connected to" data-i18n="LABEL_ACTIVE_ROOM" data-i18n-title="LABEL_ACTIVE_ROOM_TOOLTIP">Active Room</label>
<div id="activeRoomId" style="font-weight: 700; color: var(--accent); font-size: 16px; letter-spacing: 1px;">NONE</div>
</div>
<div id="activeServer" style="font-size: 10px; color: var(--text-muted); text-align: right; max-width: 120px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">Official Server</div>
</div>
<div class="info-card" style="margin-bottom: 20px;">
<label title="Share this link with friends so they can join" data-i18n="LABEL_INVITE_LINK" data-i18n-title="LABEL_INVITE_LINK_TOOLTIP">Invite Link</label>
<div class="invite-box">
<input type="text" id="inviteLink" readonly>
<button id="copyInvite" class="secondary" title="Copy invite link" aria-label="Copy invite link">📋</button>
</div>
</div>
<div id="hostControlCard" class="info-card" style="margin-bottom: 20px; display:none;">
<div style="display:flex; justify-content:space-between; align-items:center;">
<label style="margin:0;" title="Restrict who can control playback in this room" data-i18n="LABEL_HOST_CONTROL" data-i18n-title="LABEL_HOST_CONTROL_TOOLTIP">Host Control</label>
<span id="hostRoleBadge" style="font-size:10px; font-weight:700; text-transform:uppercase; padding:2px 8px; border-radius:8px; background:var(--accent); color:var(--text-on-green);"></span>
</div>
<div id="hostControlToggleRow" style="display:none; align-items:center; justify-content:space-between; gap:12px; margin-top:12px;">
<label for="hostControlToggle" style="font-size:12px; color:var(--text); line-height:1.3; margin:0; text-transform:none; font-weight:400;" data-i18n="LABEL_HOST_ONLY_TOGGLE">Only I can control playback</label>
<label class="toggle-switch" style="flex:none;">
<input type="checkbox" id="hostControlToggle">
<span class="slider"></span>
</label>
</div>
<div id="hostControlCohostHint" style="display:none; margin-top:10px; padding-top:10px; border-top:1px solid var(--border-strong); font-size:11px; color:var(--text-muted); line-height:1.4;" data-i18n="NOTICE_COHOST_HINT">Tip: grant control to individual viewers in the participant list below.</div>
<div id="hostControlGuestNote" style="display:none; margin-top:10px; font-size:12px; color:var(--text-muted);" data-i18n="NOTICE_HOST_CONTROLS">The host controls playback for everyone.</div>
</div>
<div style="margin-bottom: 20px;">
<label title="Other users currently connected to this room" data-i18n="LABEL_PEERS_IN_ROOM" data-i18n-title="LABEL_PEERS_IN_ROOM_TOOLTIP">Peers in Room</label>
<div id="peerList" class="info-card">
<div style="text-align:center; color: var(--text-muted); font-size: 12px;" data-i18n="NO_PEERS_CONNECTED">No peers connected</div>
</div>
</div>
<button id="leaveBtn" class="secondary" style="color: var(--error); border-color: var(--error);" data-i18n="BTN_LEAVE_ROOM">Leave Room</button>
</div>
</div>
<!-- Sync Tab -->
<div id="tab-sync" class="tab-content">
<!-- SYNC ACTIVE: Visible when in a room -->
<div id="sync-active">
<div class="form-group" style="position: relative;">
<label for="targetTab" title="Choose the browser tab containing the video to sync" data-i18n="LABEL_SELECT_VIDEO" data-i18n-title="LABEL_SELECT_VIDEO_TOOLTIP">Select Video</label>
<select id="targetTab">
<option value="" data-i18n="OPTION_SELECT_TAB">-- Select a Tab --</option>
</select>
<!-- Hint Tooltip -->
<div id="targetTabHint" style="display: none; position: absolute; top: -25px; right: 0; background: var(--accent); color: var(--text-on-green); padding: 4px 8px; border-radius: 6px; font-size: 11px; font-weight: bold; pointer-events: none; animation: floatHint 2s ease-in-out infinite; box-shadow: 0 4px 12px color-mix(in oklch, var(--accent), transparent 60%); z-index: 10;" data-i18n="HINT_SELECT_VIDEO">
Select your video here!
<div style="position: absolute; bottom: -4px; right: 20px; width: 8px; height: 8px; background: var(--accent); transform: rotate(45deg);"></div>
</div>
<div id="siteAccessNotice" class="info-card site-access-notice" role="alert" aria-live="polite">
<div id="siteAccessMessage"></div>
<button id="siteAccessRetry" class="secondary" data-i18n="BTN_RETRY_ACCESS">Try again</button>
</div>
</div>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px;">
<label style="margin: 0;" data-i18n="LABEL_REMOTE_CONTROL">Remote Control</label>
<button id="syncTabCopyInvite" title="Copy Invite Link" style="background:transparent; border: 1px solid var(--border-strong); border-radius: 6px; padding: 4px 8px; font-size: 11px; cursor:pointer; opacity:0.8; transition: opacity 0.2s; color: var(--text-muted); display: flex; align-items: center; gap: 4px; white-space: nowrap;" data-i18n="BTN_COPY_INVITE" data-i18n-title="BTN_COPY_INVITE_TOOLTIP">📋 Invite Link</button>
</div>
<div class="transport-actions">
<button id="playBtn" class="primary action-icon" title="Send a Play command to everyone" data-i18n="BTN_PLAY" data-i18n-title="BTN_PLAY_TOOLTIP">Play</button>
<button id="pauseBtn" class="primary action-icon" title="Send a Pause command to everyone" data-i18n="BTN_PAUSE" data-i18n-title="BTN_PAUSE_TOOLTIP">Pause</button>
</div>
<div class="sync-actions">
<button id="forceSyncBtn" class="primary action-icon" title="Force all users to sync up" data-i18n="BTN_SYNC" data-i18n-title="BTN_SYNC_TOOLTIP">Sync</button>
<select id="forceSyncMode" style="flex: 1; background: var(--card); border: 1px solid var(--border-strong); color: var(--text); padding: 10px 8px; border-radius: 8px; font-size: 11px; font-family: inherit; cursor: pointer; align-self: stretch;" title="Choose sync target" data-i18n-title="OPTION_JUMP_MODE_TOOLTIP">
<option value="jump-to-others" data-i18n="OPTION_JUMP_TO_OTHERS">Jump to Others</option>
<option value="jump-to-me" data-i18n="OPTION_JUMP_TO_ME">Jump to Me</option>
</select>
</div>
<!-- NEW: Last Action Status Card -->
<label title="Shows the most recent play, pause, or seek command" data-i18n="LABEL_LAST_ACTIVITY" data-i18n-title="LABEL_LAST_ACTIVITY_TOOLTIP">Last Activity Status</label>
<div id="lastActionCard" class="info-card" style="margin-bottom: 15px; max-height: 120px; overflow-y: auto;">
<div style="text-align:center; color: var(--text-muted); font-size: 10px;" data-i18n="NO_RECENT_COMMANDS">No recent commands</div>
</div>
<!-- Episode Auto-Sync Lobby Status -->
<div id="episodeLobbyCard" class="info-card" style="display:none; margin-bottom: 15px; border-left: 4px solid var(--star); animation: fadeIn 0.3s ease-out;">
<div style="display:flex; align-items:center; gap: 6px; margin-bottom: 6px;">
<span style="font-size: 16px;"></span>
<span style="font-weight: 700; color: var(--star); font-size: 12px;" data-i18n="LOBBY_HEADER">EPISODE LOBBY</span>
</div>
<div id="lobbyTitle" style="font-size: 11px; color: var(--text); margin-bottom: 6px; font-weight: 600;"></div>
<div id="lobbyPeerStatus" style="font-size: 10px; color: var(--text-muted); margin-bottom: 8px;"></div>
<button id="cancelLobbyBtn" class="secondary" style="margin-top: 4px; padding: 6px 10px; font-size: 11px; width: auto; display: block;" title="Cancel lobby and play anyway" data-i18n="BTN_SKIP_PLAY" data-i18n-title="BTN_SKIP_PLAY_TOOLTIP">Skip & Play anyway</button>
</div>
<div id="peerListSync" class="info-card" style="display:none;"></div>
</div>
<!-- SYNC INACTIVE: Visible when not in a room -->
<div id="sync-inactive" style="display:none; text-align: center; padding: 20px 10px;">
<div style="font-size: 32px; margin-bottom: 12px;">🔒</div>
<h3 style="margin: 0 0 8px 0; color: var(--accent); font-size: 15px;" data-i18n="LOBBY_CONNECT_FIRST">Connect to a room first</h3>
<p style="color: var(--text-muted); font-size: 12px; margin-bottom: 20px;" data-i18n="LOBBY_CONNECT_FIRST_DESC">You need to join a room via an invite link or create a new one to sync videos.</p>
<button id="syncTabCreateRoomBtn" class="primary" style="padding: 12px; font-size: 14px; box-shadow: 0 4px 12px oklch(0.40 0.10 155 / 0.4);" title="Create a new random room and join it" data-i18n="BTN_CREATE_ROOM_ALT" data-i18n-title="BTN_CREATE_ROOM_ALT_TOOLTIP">Create New Room</button>
</div>
</div>
<!-- Settings Tab -->
<div id="tab-settings" class="tab-content">
<details class="form-group" name="settings-accordion" open>
<summary title="Change your username, theme, and language." data-i18n="LABEL_SETTINGS_GROUP_PROFILE" data-i18n-title="LABEL_SETTINGS_GROUP_PROFILE_TOOLTIP">Profile &amp; Appearance</summary>
<div class="details-content">
<div class="settings-row">
<label for="username" title="Username helps others identify you." data-i18n="LABEL_USERNAME" data-i18n-title="LABEL_USERNAME_TOOLTIP">Your Username</label>
<input type="text" id="username" data-i18n-placeholder="PLACEHOLDER_USERNAME" placeholder="Anonymous Koala" maxlength="20">
</div>
<div class="settings-row">
<label id="themeLabel" for="themeSelector" title="Follow the system theme or choose a fixed appearance">Appearance</label>
<div class="theme-picker" role="group" aria-labelledby="themeLabel">
<select id="themeSelector" aria-label="Appearance" tabindex="-1">
<option id="themeOptionSystem" value="system">System</option>
<option id="themeOptionDark" value="dark">Dark</option>
<option id="themeOptionLight" value="light">Light</option>
</select>
<button id="themeChoiceSystem" class="theme-choice" type="button" data-theme-value="system">System</button>
<button id="themeChoiceDark" class="theme-choice" type="button" data-theme-value="dark">Dark</button>
<button id="themeChoiceLight" class="theme-choice" type="button" data-theme-value="light">Light</button>
</div>
</div>
<div class="palette-row">
<label id="paletteLabel" for="paletteSelector" title="Pick a curated colour scheme for the extension">Theme</label>
<div class="palette-picker" role="group" aria-labelledby="paletteLabel">
<select id="paletteSelector" aria-label="Theme" tabindex="-1">
<option value="eucalyptus">Eucalyptus</option>
<option value="cyber">Cyber</option>
<option value="graphite">Graphite</option>
</select>
<button id="paletteChoiceEucalyptus" class="palette-choice" type="button" data-palette-value="eucalyptus">
<span class="palette-swatch" aria-hidden="true">
<i style="background: oklch(0.73 0.13 155)"></i>
<i style="background: oklch(0.62 0.14 45)"></i>
<i style="background: oklch(0.30 0.035 150)"></i>
</span>
<span class="palette-labeltext">
<span class="palette-name">Eucalyptus</span>
<span class="palette-desc">Nature</span>
</span>
</button>
<button id="paletteChoiceCyber" class="palette-choice" type="button" data-palette-value="cyber">
<span class="palette-swatch" aria-hidden="true">
<i style="background: oklch(0.64 0.19 275)"></i>
<i style="background: oklch(0.64 0.21 320)"></i>
<i style="background: oklch(0.185 0.04 265)"></i>
</span>
<span class="palette-labeltext">
<span class="palette-name">Cyber</span>
<span class="palette-desc">Classic</span>
</span>
</button>
<button id="paletteChoiceGraphite" class="palette-choice" type="button" data-palette-value="graphite">
<span class="palette-swatch" aria-hidden="true">
<i style="background: oklch(0.95 0.004 260)"></i>
<i style="background: oklch(0.55 0.006 260)"></i>
<i style="background: oklch(0.20 0.004 260)"></i>
</span>
<span class="palette-labeltext">
<span class="palette-name">Graphite</span>
<span class="palette-desc">Minimal</span>
</span>
</button>
</div>
</div>
<div class="settings-row">
<label for="langSelector" data-i18n="LABEL_LANGUAGE" data-i18n-title="LABEL_LANGUAGE_TOOLTIP" title="Choose your preferred extension language">App Language</label>
<span class="settings-select">
<select id="langSelector" class="settings-control">
<option value="en">🇬🇧 English</option>
<option value="de">🇩🇪 Deutsch</option>
<option value="fr">🇫🇷 Français</option>
<option value="es">🇪🇸 Español</option>
<option value="pt-BR">🇧🇷 Português (Brasil)</option>
<option value="ru">🇷🇺 Русский</option>
<option value="it">🇮🇹 Italiano</option>
<option value="pl">🇵🇱 Polski</option>
<option value="tr">🇹🇷 Türkçe</option>
<option value="nl">🇳🇱 Nederlands</option>
<option value="ja">🇯🇵 日本語</option>
<option value="ko">🇰🇷 한국어</option>
<option value="zh">🇨🇳 中文</option>
<option value="uk">🇺🇦 Українська</option>
<option value="pt">🇵🇹 Português (Portugal)</option>
</select>
</span>
</div>
</div>
</details>
<details class="form-group" name="settings-accordion">
<summary title="Choose which titles are sent to the room." data-i18n="LABEL_PRIVACY_SETTINGS" data-i18n-title="LABEL_PRIVACY_SETTINGS_TOOLTIP">Privacy</summary>
<div class="details-content">
<div class="settings-row">
<label for="sendTabTitle" title="Send the selected browser tab title to the room." data-i18n="LABEL_SEND_TAB_TITLE" data-i18n-title="LABEL_SEND_TAB_TITLE_TOOLTIP">Send tab title</label>
<label class="toggle-switch">
<input type="checkbox" id="sendTabTitle" checked>
<span class="slider"></span>
</label>
</div>
<div class="settings-row">
<label for="mediaTitlePrivacyMode" title="Choose how media titles are sent to the room." data-i18n="LABEL_MEDIA_TITLE_PRIVACY" data-i18n-title="LABEL_MEDIA_TITLE_PRIVACY_TOOLTIP">Media title</label>
<span class="settings-select">
<select id="mediaTitlePrivacyMode" class="settings-control">
<option value="full" data-i18n="OPTION_TITLE_PRIVACY_FULL">Send full title</option>
<option value="episode" data-i18n="OPTION_TITLE_PRIVACY_EPISODE">Send episode ID</option>
<option value="hidden" data-i18n="OPTION_TITLE_PRIVACY_HIDDEN">Do not send</option>
</select>
</span>
</div>
</div>
</details>
<details class="form-group" name="settings-accordion">
<summary title="Customize playback, notifications, and audio preferences." data-i18n="LABEL_SETTINGS_GROUP_SYNC" data-i18n-title="LABEL_SETTINGS_GROUP_SYNC_TOOLTIP">Playback &amp; Sync</summary>
<div class="details-content">
<div class="settings-row">
<label for="autoSyncNextEpisode" title="Pauses automatically and waits for all peers when an episode changes, then sync-starts together." data-i18n="LABEL_AUTO_SYNC_NEXT" data-i18n-title="LABEL_AUTO_SYNC_NEXT_TOOLTIP">Auto-Sync Next Episode</label>
<label class="toggle-switch">
<input type="checkbox" id="autoSyncNextEpisode">
<span class="slider"></span>
</label>
</div>
<div class="settings-row">
<label for="chatEnabled" title="Show encrypted room chat on the selected video tab." data-i18n="LABEL_CHAT_ENABLED" data-i18n-title="LABEL_CHAT_ENABLED_TOOLTIP">Enable Room Chat</label>
<label class="toggle-switch">
<input type="checkbox" id="chatEnabled">
<span class="slider"></span>
</label>
</div>
<div class="settings-row">
<label for="filterNoise" title="Filters out non-video tabs and unrelated domains to keep the list clean" data-i18n="LABEL_HIDE_CLUTTER" data-i18n-title="LABEL_HIDE_CLUTTER_TOOLTIP">Hide Clutter Tabs</label>
<label class="toggle-switch">
<input type="checkbox" id="filterNoise" checked>
<span class="slider"></span>
</label>
</div>
<div class="settings-row">
<label for="autoCopyInvite" title="Automatically copies the invite link to your clipboard when creating a new room." data-i18n="LABEL_AUTO_COPY_INVITE" data-i18n-title="LABEL_AUTO_COPY_INVITE_TOOLTIP">Auto-Copy Invite Link</label>
<label class="toggle-switch">
<input type="checkbox" id="autoCopyInvite" checked>
<span class="slider"></span>
</label>
</div>
<div class="settings-row">
<label for="browserNotifications" title="Shows native system notifications when someone joins/leaves or plays/pauses." data-i18n="LABEL_NOTIFICATIONS" data-i18n-title="LABEL_NOTIFICATIONS_TOOLTIP">Browser Notifications</label>
<label class="toggle-switch">
<input type="checkbox" id="browserNotifications">
<span class="slider"></span>
</label>
</div>
<div class="settings-row">
<label id="audioProcessingLabel" data-i18n="LABEL_AUDIO_PROCESSING" data-i18n-title="LABEL_AUDIO_PROCESSING_TOOLTIP">Audio Processing</label>
<a id="audioSettingsLink" class="settings-action" href="#" data-i18n="AUDIO_OPEN_SETTINGS">Open</a>
</div>
</div>
</details>
<details class="form-group" name="settings-accordion">
<summary title="Tools for fixing connection issues." data-i18n="LABEL_TROUBLESHOOTING" data-i18n-title="LABEL_TROUBLESHOOTING_TOOLTIP">Troubleshooting</summary>
<div class="details-content">
<div class="settings-tools">
<button id="restartTourBtn" class="secondary" title="Restart the onboarding tutorial" data-i18n="BTN_RESTART_TOUR" data-i18n-title="BTN_RESTART_TOUR_TOOLTIP">Restart Tutorial</button>
<button id="regenId" class="secondary" title="Regenerate your internal ID and reconnect" data-i18n="BTN_REGEN_ID" data-i18n-title="BTN_REGEN_ID_TOOLTIP">Regenerate Peer ID</button>
</div>
<p class="settings-note" data-i18n="REGEN_ID_DESC">Use this if you see "Duplicate Identity" errors.</p>
<p class="settings-issue-link"><a href="https://github.com/Shik3i/KoalaSync/issues" target="_blank" data-i18n="REGEN_ID_OTHER_ISSUE">Other issue? Open a GitHub Issue</a></p>
</div>
</details>
</div>
<!-- Dev Tab -->
<div id="tab-dev" class="tab-content">
<label title="Current WebSocket connection state" data-i18n="LABEL_CONN_STATUS" data-i18n-title="LABEL_CONN_STATUS_TOOLTIP">Connection Status</label>
<div id="connStatus" class="info-card" style="display:flex; align-items:center; gap: 10px;">
<span id="connDot" class="status-dot status-offline"></span>
<span style="flex:1; display:flex; align-items:center; gap:6px; min-width:0;">
<span id="connText">Disconnected</span>
<span id="connInfo" data-i18n-title="STATUS_IDLE_TOOLTIP" title="KoalaSync is ready. Join or create a room to connect and start syncing." style="display:none; cursor:help; flex:none; width:15px; height:15px; line-height:14px; text-align:center; border-radius:50%; border:1px solid var(--text-muted); color:var(--text-muted); font-size:10px; font-weight:700; font-style:normal;">i</span>
</span>
<span id="connPing" class="conn-ping"></span>
<button id="retryBtn" class="secondary" style="display:none; width: auto; padding: 4px 8px; font-size: 10px; margin: 0;" title="Attempt to reconnect to the server" data-i18n="BTN_RETRY" data-i18n-title="BTN_RETRY_TOOLTIP">RETRY</button>
<button id="copyLogs" class="btn secondary" style="width: auto; padding: 4px 10px; font-size: 11px; margin: 0;" title="Copy logs to clipboard for sharing" data-i18n="BTN_COPY_LOGS" data-i18n-title="BTN_COPY_LOGS_TOOLTIP">Copy Logs</button>
</div>
<label title="Technical details about the currently selected video element" data-i18n="LABEL_VIDEO_DEBUG" data-i18n-title="LABEL_VIDEO_DEBUG_TOOLTIP">Video Debug Info</label>
<div id="videoDebug" class="info-card" style="font-size: 10px; font-family: monospace; color: var(--text-muted); max-height: 250px; overflow-y: auto; line-height: 1.4;">
No tab selected or video detected.
</div>
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom: 8px;">
<label title="Chronological log of all sync commands in the room" data-i18n="LABEL_HISTORY" data-i18n-title="LABEL_HISTORY_TOOLTIP">Full Action History</label>
</div>
<div id="historyList" class="info-card" style="max-height: 120px; overflow-y: auto; font-size: 10px; margin-bottom: 15px;">
<div style="text-align:center; color: var(--text-muted); font-size: 11px;" data-i18n="HISTORY_EMPTY">No activity yet</div>
</div>
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom: 8px;">
<label title="Technical connection logs for debugging" data-i18n="LABEL_LOGS" data-i18n-title="LABEL_LOGS_TOOLTIP">Logs (Last 50)</label>
<button id="clearLogs" style="background:transparent; border:none; color:var(--accent); font-size:10px; cursor:pointer;" title="Clear log output" data-i18n="BTN_CLEAR" data-i18n-title="BTN_CLEAR_TOOLTIP">CLEAR</button>
</div>
<div id="logList"></div>
</div>
<div id="tab-devtools" class="tab-content">
<label>Remote Seek</label>
<div class="info-card" style="display:flex; gap:8px; margin-bottom:15px;">
<button id="remoteSeekBack" class="secondary" style="flex:1; font-size:12px;">-30s</button>
<button id="remoteSeekForward" class="secondary" style="flex:1; font-size:12px;">+30s</button>
</div>
<button id="remoteSeekFiveMin" class="secondary" style="width:100%; font-size:12px; margin-bottom:15px;">Seek 5:00</button>
<div style="font-size:10px; color:var(--text-muted); text-align:center; margin-top:10px; opacity:0.7;">
Build: <span id="buildIdentifier">__BUILD_TIMESTAMP__</span>
</div>
</div>
<div class="popup-footer">
<nav class="footer-links" aria-label="KoalaSync links">
<a href="https://github.com/Shik3i/KoalaSync" target="_blank" data-i18n="LABEL_GITHUB">GitHub</a>
<a id="settingsSupportLink" href="#" target="_blank" data-i18n="FOOTER_SUPPORT">Support</a>
</nav>
<a id="settingsReviewLink" class="footer-review" href="#" target="_blank" data-i18n="FOOTER_REVIEW">★ Rate us</a>
</div>
</main>
<script src="popup.js" type="module"></script>
<!-- Onboarding Overlay -->
<div id="onboarding-overlay" style="display:none; position:fixed; inset:0; background:transparent; z-index:1000; pointer-events:auto;">
<!-- Spotlight element -->
<div id="onboarding-spotlight" style="position:fixed; pointer-events:none; border:2px solid var(--accent); box-shadow:0 0 0 9999px rgba(16, 25, 14, 0.65); border-radius:8px; transition:all 0.3s cubic-bezier(0.4, 0, 0.2, 1); display:none; opacity:0; z-index:1001; animation: spotlightPulse 2s infinite ease-in-out;"></div>
<!-- Onboarding Card -->
<div id="onboarding-card" style="position:fixed; background:var(--card); padding:16px 20px; border-radius:12px; width:280px; box-shadow:0 8px 32px rgba(0,0,0,0.5); z-index:1002; transition:all 0.3s cubic-bezier(0.4, 0, 0.2, 1); box-sizing:border-box; border:1px solid var(--border-strong); opacity:0; transform: scale(0.9); display:none;">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:8px;">
<span id="onboarding-icon" style="font-size:24px;">👋</span>
<span id="onboarding-step-indicator" style="font-size:10px; font-weight:700; color:var(--accent); text-transform:uppercase; letter-spacing:0.5px;">Step 1 of 5</span>
</div>
<h2 id="onboarding-title" style="color:var(--accent); margin:0 0 6px; font-size:14px; font-weight:700; text-align:left;">Welcome to KoalaSync!</h2>
<p id="onboarding-text" style="color:var(--text-muted); font-size:12px; margin:0 0 16px; line-height:1.4; text-align:left;">Let's get you started.</p>
<!-- Progress Bar -->
<div id="onboarding-progress-container" style="height:4px; background: var(--border-strong); border-radius:2px; margin-bottom:16px; overflow:hidden;">
<div id="onboarding-progress-bar" style="height:100%; width:20%; background:var(--accent); transition:width 0.3s ease;"></div>
</div>
<div style="display:flex; gap:8px; justify-content:space-between; align-items:center;">
<button id="onboarding-skip" class="secondary" style="width:auto; padding:6px 12px; margin:0; font-size:11px;" title="Skip the tutorial" data-i18n="BTN_ONBOARDING_SKIP" data-i18n-title="BTN_ONBOARDING_SKIP_TOOLTIP">Skip</button>
<button id="onboarding-next" class="primary" style="width:auto; padding:6px 16px; margin:0; font-size:11px;" title="Go to next step" data-i18n="BTN_ONBOARDING_NEXT" data-i18n-title="BTN_ONBOARDING_NEXT_TOOLTIP">Next</button>
</div>
<!-- Tooltip pointer arrow -->
<div id="onboarding-arrow" style="position:absolute; width:0; height:0; border:6px solid transparent; transition:all 0.3s ease;"></div>
</div>
</div>
</body>
</html>