commit 54a24066941ba29ad595692eda9a5fada8668116
Author: Dion Timmer <825343+diontimmer@users.noreply.github.com>
Date: Fri Mar 13 01:00:30 2026 -0400
Initial commit: VeryExtraOS ARG site
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..904fab1
--- /dev/null
+++ b/README.md
@@ -0,0 +1 @@
+# veryextra ARG site
diff --git a/ops/docker-compose.yml b/ops/docker-compose.yml
new file mode 100644
index 0000000..35d526b
--- /dev/null
+++ b/ops/docker-compose.yml
@@ -0,0 +1,9 @@
+services:
+ veryextra-arg:
+ image: nginx:alpine
+ container_name: veryextra-arg
+ restart: unless-stopped
+ ports:
+ - "8008:80"
+ volumes:
+ - ../site:/usr/share/nginx/html:ro
diff --git a/site/assets/audio/violin.mp3 b/site/assets/audio/violin.mp3
new file mode 100644
index 0000000..1553626
Binary files /dev/null and b/site/assets/audio/violin.mp3 differ
diff --git a/site/fakeweb/bbs-luna-board.html b/site/fakeweb/bbs-luna-board.html
new file mode 100644
index 0000000..74225bb
--- /dev/null
+++ b/site/fakeweb/bbs-luna-board.html
@@ -0,0 +1,5 @@
+
LunaBoard BBS
+[Thread] Show your desktop screenshots (248 replies)
+[Thread] Best old internet sounds?? (93 replies)
+[Thread] Help: my css gradients look too modern
+post your ASCII signatures here
diff --git a/site/fakeweb/fansite-winamp-skins.html b/site/fakeweb/fansite-winamp-skins.html
new file mode 100644
index 0000000..c8e76c3
--- /dev/null
+++ b/site/fakeweb/fansite-winamp-skins.html
@@ -0,0 +1,9 @@
+Winamp Skins Archive
+It really whips the llama's ass.
+
+ | Skin | Style | Status |
+ | Neon Bubblegum | Y2K | Download mirror offline |
+ | Crystal Lavender | Futuristic | Available |
+ | Night Kitty | Dark Cute | Available |
+
+need old download badges?
diff --git a/site/fakeweb/geocities-ponzu-home.html b/site/fakeweb/geocities-ponzu-home.html
new file mode 100644
index 0000000..4ce336f
--- /dev/null
+++ b/site/fakeweb/geocities-ponzu-home.html
@@ -0,0 +1,10 @@
+~* Ponzu's Cosmic Corner *~
+Welcome to my personal homepage!!!
+
+
+ - mood: sparkling
+ - currently listening: eurodance mix cd
+ - favorite browser: internet explorer 6
+
+visit my anime shrine
+back to pink webring
diff --git a/site/fakeweb/index.json b/site/fakeweb/index.json
new file mode 100644
index 0000000..1bce296
--- /dev/null
+++ b/site/fakeweb/index.json
@@ -0,0 +1,92 @@
+{
+ "pages": [
+ {
+ "route": "home://portal",
+ "title": "Retro Portal",
+ "file": "/fakeweb/portal.html",
+ "favorite": true,
+ "tags": [
+ "portal",
+ "home",
+ "retro"
+ ]
+ },
+ {
+ "route": "site://geocities/ponzu-home",
+ "title": "Ponzu Home Page",
+ "file": "/fakeweb/geocities-ponzu-home.html",
+ "favorite": true,
+ "tags": [
+ "geocities",
+ "anime",
+ "home"
+ ]
+ },
+ {
+ "route": "site://webring/pink-directory",
+ "title": "Pink WebRing Directory",
+ "file": "/fakeweb/webring-pink-directory.html",
+ "favorite": true,
+ "tags": [
+ "webring",
+ "directory",
+ "links"
+ ]
+ },
+ {
+ "route": "site://fansite/winamp-skins",
+ "title": "Winamp Skins Archive",
+ "file": "/fakeweb/fansite-winamp-skins.html",
+ "favorite": true,
+ "tags": [
+ "music",
+ "skins",
+ "download"
+ ]
+ },
+ {
+ "route": "site://bbs/luna-board",
+ "title": "LunaBoard BBS",
+ "file": "/fakeweb/bbs-luna-board.html",
+ "favorite": true,
+ "tags": [
+ "bbs",
+ "forum",
+ "community"
+ ]
+ },
+ {
+ "route": "site://museum/under-construction",
+ "title": "Under Construction Museum",
+ "file": "/fakeweb/museum-under-construction.html",
+ "favorite": false,
+ "tags": [
+ "museum",
+ "construction",
+ "nostalgia"
+ ]
+ },
+ {
+ "route": "site://otaku/sailor-shrine",
+ "title": "Sailor Shrine",
+ "file": "/fakeweb/otaku-sailor-shrine.html",
+ "favorite": false,
+ "tags": [
+ "anime",
+ "shrine",
+ "otaku"
+ ]
+ },
+ {
+ "route": "site://lab/ascii-lounge",
+ "title": "ASCII Lounge",
+ "file": "/fakeweb/lab-ascii-lounge.html",
+ "favorite": false,
+ "tags": [
+ "ascii",
+ "art",
+ "lab"
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/site/fakeweb/lab-ascii-lounge.html b/site/fakeweb/lab-ascii-lounge.html
new file mode 100644
index 0000000..33facd1
--- /dev/null
+++ b/site/fakeweb/lab-ascii-lounge.html
@@ -0,0 +1,8 @@
+ASCII Lounge
+
+ (^_^)
+ <( โ )>
+ / \
+
+Share your signatures and keyboard art.
+go to BBS thread
diff --git a/site/fakeweb/museum-under-construction.html b/site/fakeweb/museum-under-construction.html
new file mode 100644
index 0000000..4ead906
--- /dev/null
+++ b/site/fakeweb/museum-under-construction.html
@@ -0,0 +1,9 @@
+Under Construction Museum
+Collection of classic web relics:
+
+ - ๐ง Under construction banner
+ - ๐ฌ Guestbook link
+ - ๐ Netscape Now! badge
+ - ๐งฑ Counter widget
+
+escape the museum
diff --git a/site/fakeweb/otaku-sailor-shrine.html b/site/fakeweb/otaku-sailor-shrine.html
new file mode 100644
index 0000000..cbc4d31
--- /dev/null
+++ b/site/fakeweb/otaku-sailor-shrine.html
@@ -0,0 +1,9 @@
+Sailor Shrine โ
+This shrine is dedicated to magical girl aesthetics.
+Top 3 opening themes:
+
+ - Moonlight Densetsu
+ - Sailor Star Song
+ - La Soldier
+
+back to ponzu home
diff --git a/site/fakeweb/portal.html b/site/fakeweb/portal.html
new file mode 100644
index 0000000..3203c30
--- /dev/null
+++ b/site/fakeweb/portal.html
@@ -0,0 +1,13 @@
+RETRO PORTAL 2000
+ welcome surfer~
+Pick your vibe:
+
+ASCII Lounge ยท Sailor Shrine
+
+This page is hand-coded with love in Notepad.
diff --git a/site/fakeweb/webring-pink-directory.html b/site/fakeweb/webring-pink-directory.html
new file mode 100644
index 0000000..f2cb71b
--- /dev/null
+++ b/site/fakeweb/webring-pink-directory.html
@@ -0,0 +1,9 @@
+Pink WebRing Directory
+All links approved by glitter council.
+
+ - Ponzu Home Page
+ - Winamp Skins Archive
+ - LunaBoard BBS
+ - ASCII Lounge
+
+return to portal
diff --git a/site/index.html b/site/index.html
new file mode 100644
index 0000000..3d104b3
--- /dev/null
+++ b/site/index.html
@@ -0,0 +1,89 @@
+
+
+
+
+
+ veryextra.net
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/site/script.js b/site/script.js
new file mode 100644
index 0000000..34f249e
--- /dev/null
+++ b/site/script.js
@@ -0,0 +1,2156 @@
+const ICON_APPS = {
+ mycomputer: {
+ title: "My Computer",
+ type: "explorer",
+ startPath: "/",
+ },
+ notepad: {
+ title: "Notepad",
+ type: "notepad",
+ },
+ paint: {
+ title: "Paint",
+ type: "paint",
+ },
+ ie: {
+ title: "Internet Explorer",
+ type: "ie",
+ },
+ recycle: {
+ title: "Recycle Bin",
+ type: "explorer",
+ startPath: "/Recycle Bin",
+ },
+};
+
+const RETRO_WEB_HOME = 'home://portal';
+let retroWebIndexPromise = null;
+let retroWebRoutes = new Map();
+
+async function loadRetroWebIndex() {
+ if (retroWebIndexPromise) return retroWebIndexPromise;
+
+ retroWebIndexPromise = fetch('/fakeweb/index.json', { cache: 'no-store' })
+ .then((res) => (res.ok ? res.json() : { pages: [] }))
+ .catch(() => ({ pages: [] }))
+ .then((data) => {
+ const pages = Array.isArray(data.pages) ? data.pages : [];
+ retroWebRoutes = new Map(
+ pages
+ .filter((p) => p && p.route && p.file)
+ .map((p) => [String(p.route).toLowerCase(), p]),
+ );
+ return pages;
+ });
+
+ return retroWebIndexPromise;
+}
+
+function retroRouteKey(url) {
+ return String(url || '').trim().toLowerCase();
+}
+
+function normalizeRetroUrl(raw) {
+ if (!raw) return RETRO_WEB_HOME;
+ const text = String(raw).trim();
+ if (!text) return RETRO_WEB_HOME;
+
+ if (text.startsWith('home://') || text.startsWith('site://') || text.startsWith('search://')) {
+ return text;
+ }
+
+ if (/^https?:\/\//i.test(text)) {
+ return `search://${encodeURIComponent(text)}`;
+ }
+
+ if (text.includes(' ')) {
+ return `search://${encodeURIComponent(text)}`;
+ }
+
+ if (text.includes('.')) {
+ return `site://${text.toLowerCase()}`;
+ }
+
+ return `search://${encodeURIComponent(text)}`;
+}
+
+function buildRetroSearchPage(query, pages) {
+ const q = decodeURIComponent(query || '').trim() || 'empty search';
+ const needle = q.toLowerCase();
+
+ const matches = pages
+ .filter((p) => {
+ const hay = `${p.route || ''} ${p.title || ''} ${(p.tags || []).join(' ')}`.toLowerCase();
+ return hay.includes(needle);
+ })
+ .slice(0, 12)
+ .map((p) => `${p.title || p.route}${p.route}
`)
+ .join('');
+
+ return {
+ title: `Search results for "${q}"`,
+ html: `
+ RetroSearch 2000
+ Results for: ${q}
+ ${matches || '- No direct matches. Try home://portal
'}
+ `,
+ };
+}
+
+async function getRetroPage(url) {
+ const pages = await loadRetroWebIndex();
+
+ if (url.startsWith('search://')) {
+ return buildRetroSearchPage(url.slice('search://'.length), pages);
+ }
+
+ const entry = retroWebRoutes.get(retroRouteKey(url));
+ if (!entry) return null;
+
+ try {
+ const res = await fetch(entry.file, { cache: 'no-store' });
+ if (!res.ok) return null;
+ const html = await res.text();
+ return {
+ title: entry.title || entry.route,
+ html,
+ route: entry.route,
+ };
+ } catch {
+ return null;
+ }
+}
+
+function htmlEscape(text) {
+ return String(text || '').replace(/[&<>"']/g, (m) => ({
+ '&': '&',
+ '<': '<',
+ '>': '>',
+ '"': '"',
+ "'": ''',
+ }[m]));
+}
+
+const FILE_SYSTEM = {
+ type: 'folder',
+ name: 'My Computer',
+ children: [
+ {
+ type: 'folder',
+ name: 'C:',
+ children: [
+ { type: 'folder', name: 'Program Files', children: [
+ { type: 'file', name: 'readme.txt', fileType: 'txt', content: 'Welcome to veryextraOS 1.0\n\nThis is a mock file system.' },
+ ]},
+ { type: 'folder', name: 'WINDOWS', children: [
+ { type: 'file', name: 'win.ini', fileType: 'txt', content: '[fonts]\n[extensions]\n[desktop]' },
+ ]},
+ { type: 'folder', name: 'Documents and Settings', children: [
+ { type: 'folder', name: 'Dion', children: [
+ { type: 'folder', name: 'My Documents', children: [
+ { type: 'file', name: 'shopping-list.txt', fileType: 'txt', content: 'milk\ncoffee\nstickers' },
+ { type: 'file', name: 'desktop-ideas.txt', fileType: 'txt', content: 'Make everything cuter\nAdd sounds\nAdd themes' },
+ ]},
+ { type: 'folder', name: 'My Pictures', children: [
+ { type: 'file', name: 'wallpaper-preview.jpg', fileType: 'image', content: 'https://wallpaperaccess.com/full/4810999.jpg' },
+ { type: 'file', name: 'pink-theme.png', fileType: 'image', content: 'https://picsum.photos/seed/pinktheme/720/450' },
+ ]},
+ ]},
+ ]},
+ ],
+ },
+ {
+ type: 'folder',
+ name: 'D:',
+ children: [
+ { type: 'folder', name: 'Music', children: [
+ { type: 'file', name: 'favorites.txt', fileType: 'txt', content: 'Track 01\nTrack 02\nTrack 03' },
+ { type: 'file', name: 'violin.mp3', fileType: 'audio', content: '/assets/audio/violin.mp3' },
+ ]},
+ ],
+ },
+ {
+ type: 'folder',
+ name: 'Recycle Bin',
+ children: [
+ { type: 'file', name: 'old-theme.zip', fileType: 'txt', content: 'Archive is empty.' },
+ { type: 'file', name: 'notes (1).txt', fileType: 'txt', content: 'Draft notes moved to recycle bin.' },
+ ],
+ },
+ ],
+};
+
+const statusEl = document.getElementById('status');
+const clockEl = document.getElementById('clock');
+const iconsEl = document.getElementById('icons');
+const iconButtons = Array.from(document.querySelectorAll('.desktop-icon'));
+const resetLayoutBtn = document.getElementById('reset-layout-btn');
+const startBtn = document.getElementById('start-btn');
+const taskbarWindowsEl = document.getElementById('taskbar-windows');
+const showDesktopBtn = document.getElementById('show-desktop-btn');
+const windowTemplate = document.getElementById('window-template');
+
+const ICON_LAYOUT_KEY = 'veryextraOS.iconLayout.v1';
+const selectedIcons = new Set();
+const windowsById = new Map();
+const startMenuEl = document.getElementById('start-menu');
+
+let selectionBoxEl = null;
+let topWindowZ = 1000;
+let windowSeq = 0;
+
+function clamp(value, min, max) {
+ return Math.max(min, Math.min(max, value));
+}
+
+function setStatus(text) {
+ if (statusEl) statusEl.textContent = text;
+}
+
+let contextMenuEl = null;
+
+function ensureContextMenu() {
+ if (contextMenuEl) return contextMenuEl;
+
+ contextMenuEl = document.createElement('div');
+ contextMenuEl.id = 'context-menu';
+ contextMenuEl.className = 'hidden';
+ contextMenuEl.setAttribute('role', 'menu');
+ contextMenuEl.addEventListener('click', (e) => e.stopPropagation());
+ document.body.appendChild(contextMenuEl);
+ return contextMenuEl;
+}
+
+function hideContextMenu() {
+ if (!contextMenuEl) return;
+ contextMenuEl.classList.add('hidden');
+ contextMenuEl.innerHTML = '';
+}
+
+function showContextMenu(x, y, items = []) {
+ if (!items.length) {
+ hideContextMenu();
+ return;
+ }
+
+ const menu = ensureContextMenu();
+ menu.innerHTML = '';
+
+ items.forEach((item) => {
+ if (item === 'separator') {
+ const sep = document.createElement('div');
+ sep.className = 'context-menu-sep';
+ menu.appendChild(sep);
+ return;
+ }
+
+ const btn = document.createElement('button');
+ btn.type = 'button';
+ btn.className = 'context-menu-item';
+ btn.textContent = item.label || 'Action';
+ btn.disabled = Boolean(item.disabled);
+ btn.setAttribute('role', 'menuitem');
+ btn.addEventListener('click', () => {
+ hideContextMenu();
+ item.action?.();
+ });
+ menu.appendChild(btn);
+ });
+
+ menu.classList.remove('hidden');
+ menu.style.left = '0px';
+ menu.style.top = '0px';
+
+ const pad = 6;
+ const menuRect = menu.getBoundingClientRect();
+ const maxLeft = Math.max(pad, window.innerWidth - menuRect.width - pad);
+ const maxTop = Math.max(pad, window.innerHeight - menuRect.height - pad);
+ menu.style.left = `${clamp(x, pad, maxLeft)}px`;
+ menu.style.top = `${clamp(y, pad, maxTop)}px`;
+}
+
+function openPropertiesWindow(title, rows = []) {
+ const htmlRows = rows
+ .map((row) => `${htmlEscape(row.label)}: ${htmlEscape(row.value)}
`)
+ .join('');
+
+ openAppFromDescriptor({
+ title: `${title} Properties`,
+ width: 360,
+ height: 240,
+ html: `
+
+
${htmlEscape(title)}
+
${htmlRows || '
No details available.
'}
+
+ `,
+ });
+}
+
+function parsePosition(btn) {
+ return {
+ x: parseInt(btn.style.left || '0', 10) || 0,
+ y: parseInt(btn.style.top || '0', 10) || 0,
+ };
+}
+
+function setIconPosition(btn, x, y) {
+ const maxX = Math.max(0, iconsEl.clientWidth - btn.offsetWidth - 6);
+ const maxY = Math.max(0, iconsEl.clientHeight - btn.offsetHeight - 6);
+ btn.style.left = `${clamp(x, 0, maxX)}px`;
+ btn.style.top = `${clamp(y, 0, maxY)}px`;
+}
+
+function saveIconLayout() {
+ const payload = iconButtons.map((btn) => ({
+ app: btn.dataset.app,
+ ...parsePosition(btn),
+ }));
+ localStorage.setItem(ICON_LAYOUT_KEY, JSON.stringify(payload));
+}
+
+function loadIconLayout() {
+ try {
+ return JSON.parse(localStorage.getItem(ICON_LAYOUT_KEY) || 'null');
+ } catch {
+ return null;
+ }
+}
+
+function resetIconLayout() {
+ iconButtons.forEach((btn, i) => {
+ setIconPosition(btn, 8, 8 + i * 106);
+ });
+ saveIconLayout();
+ setStatus('Icons reset to vertical stack');
+}
+
+function applyInitialLayout() {
+ const saved = loadIconLayout();
+ if (Array.isArray(saved) && saved.length) {
+ const savedMap = new Map(saved.map((item) => [item.app, item]));
+ iconButtons.forEach((btn, i) => {
+ const s = savedMap.get(btn.dataset.app);
+ if (s) {
+ setIconPosition(btn, Number(s.x) || 0, Number(s.y) || 0);
+ } else {
+ setIconPosition(btn, 8, 8 + i * 106);
+ }
+ });
+ return;
+ }
+ resetIconLayout();
+}
+
+function clearSelection() {
+ selectedIcons.forEach((btn) => btn.classList.remove('selected'));
+ selectedIcons.clear();
+}
+
+function setSelection(buttons) {
+ clearSelection();
+ buttons.forEach((btn) => {
+ selectedIcons.add(btn);
+ btn.classList.add('selected');
+ });
+ setStatus(`${selectedIcons.size} item(s) selected`);
+}
+
+function toggleSelection(btn) {
+ if (selectedIcons.has(btn)) {
+ selectedIcons.delete(btn);
+ btn.classList.remove('selected');
+ } else {
+ selectedIcons.add(btn);
+ btn.classList.add('selected');
+ }
+ if (selectedIcons.size) {
+ setStatus(`${selectedIcons.size} item(s) selected`);
+ } else {
+ setStatus(windowsById.size ? `${windowsById.size} window(s) open` : 'No windows open');
+ }
+}
+
+function toIconRect(btn) {
+ return {
+ left: btn.offsetLeft,
+ top: btn.offsetTop,
+ right: btn.offsetLeft + btn.offsetWidth,
+ bottom: btn.offsetTop + btn.offsetHeight,
+ };
+}
+
+function intersects(a, b) {
+ return !(a.right < b.left || a.left > b.right || a.bottom < b.top || a.top > b.bottom);
+}
+
+function ensureSelectionBox() {
+ if (!selectionBoxEl) {
+ selectionBoxEl = document.createElement('div');
+ selectionBoxEl.id = 'selection-box';
+ selectionBoxEl.hidden = true;
+ iconsEl.appendChild(selectionBoxEl);
+ }
+ return selectionBoxEl;
+}
+
+function getWindowRect(winEl) {
+ return winEl.getBoundingClientRect();
+}
+
+function splitPath(path) {
+ return path.split('/').filter(Boolean);
+}
+
+function joinPath(parts) {
+ return `/${parts.join('/')}` || '/';
+}
+
+function getNodeByPath(path) {
+ const parts = splitPath(path);
+ let node = FILE_SYSTEM;
+ for (const part of parts) {
+ if (!node.children) return null;
+ const next = node.children.find((child) => child.name === part);
+ if (!next) return null;
+ node = next;
+ }
+ return node;
+}
+
+function updateFileByPath(path, updater) {
+ const node = getNodeByPath(path);
+ if (!node || node.type !== 'file') return false;
+ updater(node);
+ return true;
+}
+
+function listFolder(path) {
+ const node = getNodeByPath(path);
+ if (!node || node.type !== 'folder') return [];
+
+ const rank = (item) => {
+ if (item.type === 'folder') return 0;
+ if (item.fileType === 'audio') return 1;
+ if (item.fileType === 'image') return 2;
+ if (item.fileType === 'txt') return 3;
+ return 4;
+ };
+
+ return [...(node.children || [])].sort((a, b) => {
+ const ra = rank(a);
+ const rb = rank(b);
+ if (ra !== rb) return ra - rb;
+ return a.name.localeCompare(b.name);
+ });
+}
+
+function fileIconFor(item) {
+ if (item.type === 'folder') return '๐';
+ if (item.fileType === 'image') return '๐ผ๏ธ';
+ if (item.fileType === 'audio') return '๐ต';
+ if (item.fileType === 'txt') return '๐';
+ return '๐ฆ';
+}
+
+function itemTypeLabel(item) {
+ if (item.type === 'folder') return 'File Folder';
+ if (item.fileType === 'image') return 'Image File';
+ if (item.fileType === 'audio') return 'Audio File';
+ if (item.fileType === 'txt') return 'Text Document';
+ return 'File';
+}
+
+function itemSizeLabel(item) {
+ if (item.type === 'folder') return `${(item.children || []).length} item(s)`;
+ const raw = String(item.content || '').length;
+ const kb = Math.max(1, Math.round(raw / 40));
+ return `${kb} KB`;
+}
+
+function itemModifiedLabel(item) {
+ const seed = item.name.length * 37;
+ const month = ((seed % 12) + 1).toString().padStart(2, '0');
+ const day = ((seed % 27) + 1).toString().padStart(2, '0');
+ return `${month}/${day}/2006 3:${(seed % 60).toString().padStart(2, '0')} PM`;
+}
+
+function renderExplorerDetails(path, selectedItem) {
+ return selectedItem ? `
+ ${selectedItem.name}
+ Type: ${itemTypeLabel(selectedItem)}
+ Size: ${itemSizeLabel(selectedItem)}
+ Modified: ${itemModifiedLabel(selectedItem)}
+ Location: ${path === '/' ? 'My Computer' : path}
+ ` : `
+ No item selected
+ Select a file or folder to view details.
+ `;
+}
+
+function renderExplorer(path, selectedItem, nav, selectedAll = false) {
+ const items = listFolder(path);
+ const breadcrumb = path === '/' ? 'My Computer' : `My Computer > ${splitPath(path).join(' > ')}`;
+
+ const list = items.map((item) => {
+ const encodedName = encodeURIComponent(item.name);
+ const selected = selectedAll || (selectedItem && selectedItem.name === item.name && selectedItem.type === item.type);
+ return `
+
+ `;
+ }).join('');
+
+ const detailsHtml = selectedAll
+ ? `${items.length} item(s) selected
Press Enter to open the first selected item.
`
+ : renderExplorerDetails(path, selectedItem);
+
+ return `
+
+
+
+
+
+
+ ${list || 'This folder is empty.
'}
+
+
+
+
+
+ `;
+}
+
+function renderNotepadApp(filePath, fileName, textValue = '') {
+ const escapedName = String(fileName || 'Untitled').replace(/"/g, '"');
+ const escapedPath = String(filePath || '').replace(/"/g, '"');
+ const escapedText = String(textValue).replace(/[&<>]/g, (m) => ({ '&': '&', '<': '<', '>': '>' }[m]));
+
+ return `
+
+
+
+
+ ${escapedName}
+
+
+
+ `;
+}
+
+function wireNotepadInteractions(winEl, options = {}) {
+ const contentEl = winEl.querySelector('.window-content');
+ if (!contentEl) return;
+
+ const editorEl = contentEl.querySelector('.xp-note-editor');
+ const filePath = options.filePath || null;
+ const fileName = options.fileName || 'Untitled.txt';
+
+ if (!editorEl) return;
+
+ function updateDirtyTitle(isDirty) {
+ const titleEl = winEl.querySelector('.window-title');
+ if (!titleEl) return;
+ titleEl.textContent = `${isDirty ? '* ' : ''}${fileName}`;
+
+ const entry = windowsById.get(winEl.dataset.windowId || '');
+ if (entry) {
+ entry.title = titleEl.textContent;
+ updateTaskbarButtons();
+ }
+ }
+
+ let dirty = false;
+ editorEl.addEventListener('input', () => {
+ dirty = true;
+ updateDirtyTitle(true);
+ });
+
+ contentEl.querySelectorAll('.xp-note-btn').forEach((btn) => {
+ btn.addEventListener('click', () => {
+ const action = btn.getAttribute('data-note-action');
+ if (action === 'new') {
+ editorEl.value = '';
+ dirty = true;
+ updateDirtyTitle(true);
+ return;
+ }
+
+ if (action === 'save') {
+ if (filePath) {
+ updateFileByPath(filePath, (node) => {
+ node.content = editorEl.value;
+ });
+ dirty = false;
+ updateDirtyTitle(false);
+ setStatus(`${fileName} saved`);
+ }
+ }
+ });
+ });
+
+ editorEl.addEventListener('keydown', (e) => {
+ if ((e.ctrlKey || e.metaKey) && (e.key === 's' || e.key === 'S')) {
+ e.preventDefault();
+ contentEl.querySelector('[data-note-action="save"]')?.click();
+ }
+ });
+
+}
+
+function openNotepadFile(filePath, fileName, textValue = '') {
+ const winEl = openAppFromDescriptor({
+ title: fileName || 'Notepad',
+ html: renderNotepadApp(filePath, fileName, textValue),
+ });
+ if (winEl) wireNotepadInteractions(winEl, { filePath, fileName });
+}
+
+function renderPaintApp() {
+ return `
+
+ `;
+}
+
+function wirePaintInteractions(winEl) {
+ const contentEl = winEl.querySelector('.window-content');
+ if (!contentEl) return;
+
+ const canvas = contentEl.querySelector('.xp-paint-canvas');
+ const ctx = canvas?.getContext('2d');
+ const toolEl = contentEl.querySelector('.xp-paint-tool');
+ const colorEl = contentEl.querySelector('.xp-paint-color');
+ const sizeEl = contentEl.querySelector('.xp-paint-size');
+ if (!canvas || !ctx || !toolEl || !colorEl || !sizeEl) return;
+
+ const undoStack = [];
+ const redoStack = [];
+
+ function snapshot() {
+ try {
+ return canvas.toDataURL('image/png');
+ } catch {
+ return null;
+ }
+ }
+
+ function pushUndoState() {
+ const shot = snapshot();
+ if (!shot) return;
+ undoStack.push(shot);
+ if (undoStack.length > 30) undoStack.shift();
+ redoStack.length = 0;
+ }
+
+ function restoreSnapshot(dataUrl) {
+ if (!dataUrl) return;
+ const img = new Image();
+ img.onload = () => {
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+ ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
+ };
+ img.src = dataUrl;
+ }
+
+ ctx.fillStyle = '#ffffff';
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
+
+ let drawing = false;
+ let lastX = 0;
+ let lastY = 0;
+
+ function applyStrokeStyle() {
+ const tool = toolEl.value;
+ ctx.lineCap = 'round';
+ ctx.lineJoin = 'round';
+ ctx.lineWidth = Number(sizeEl.value || 4);
+ if (tool === 'eraser') {
+ ctx.strokeStyle = '#ffffff';
+ } else {
+ ctx.strokeStyle = colorEl.value || '#d13f8c';
+ }
+ }
+
+ function pointFromEvent(e) {
+ const rect = canvas.getBoundingClientRect();
+ return {
+ x: (e.clientX - rect.left) * (canvas.width / rect.width),
+ y: (e.clientY - rect.top) * (canvas.height / rect.height),
+ };
+ }
+
+ canvas.addEventListener('pointerdown', (e) => {
+ e.preventDefault();
+ pushUndoState();
+ drawing = true;
+ const p = pointFromEvent(e);
+ lastX = p.x;
+ lastY = p.y;
+ applyStrokeStyle();
+ canvas.setPointerCapture(e.pointerId);
+ });
+
+ canvas.addEventListener('pointermove', (e) => {
+ if (!drawing) return;
+ const p = pointFromEvent(e);
+ applyStrokeStyle();
+ ctx.beginPath();
+ ctx.moveTo(lastX, lastY);
+ ctx.lineTo(p.x, p.y);
+ ctx.stroke();
+ lastX = p.x;
+ lastY = p.y;
+ });
+
+ function stopDrawing() {
+ if (!drawing) return;
+ drawing = false;
+ }
+
+ canvas.addEventListener('pointerup', stopDrawing);
+ canvas.addEventListener('pointercancel', stopDrawing);
+ canvas.addEventListener('pointerleave', stopDrawing);
+
+ contentEl.querySelectorAll('.xp-paint-btn').forEach((btn) => {
+ btn.addEventListener('click', () => {
+ const action = btn.getAttribute('data-paint-action');
+
+ if (action === 'undo') {
+ if (!undoStack.length) return;
+ const current = snapshot();
+ if (current) redoStack.push(current);
+ const prev = undoStack.pop();
+ restoreSnapshot(prev);
+ return;
+ }
+
+ if (action === 'redo') {
+ if (!redoStack.length) return;
+ const current = snapshot();
+ if (current) undoStack.push(current);
+ const next = redoStack.pop();
+ restoreSnapshot(next);
+ return;
+ }
+
+ if (action === 'clear') {
+ pushUndoState();
+ ctx.fillStyle = '#ffffff';
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
+ return;
+ }
+
+ if (action === 'save') {
+ const a = document.createElement('a');
+ a.href = canvas.toDataURL('image/png');
+ a.download = `paint-${Date.now()}.png`;
+ a.click();
+ }
+ });
+ });
+}
+
+function openPaintApp() {
+ const winEl = openAppFromDescriptor({
+ title: 'Paint',
+ html: renderPaintApp(),
+ });
+ if (winEl) wirePaintInteractions(winEl);
+}
+
+async function renderIeApp(startUrl = RETRO_WEB_HOME) {
+ await loadRetroWebIndex();
+
+ return `
+
+
+
+
+
+
+
+
+
Loading...
+
+ `;
+}
+
+function wrapRetroShell(page, url) {
+ const stamp = Math.floor(Math.abs(url.split('').reduce((a, c) => a + c.charCodeAt(0), 0)) % 9000) + 1000;
+ return `
+
+
+
Visitors: ${stamp} ยท Best viewed at 800x600 ยท Netscape/IE compatible
+
${page.html}
+
+
+ `;
+}
+
+function wireIeInteractions(winEl, startUrl = RETRO_WEB_HOME) {
+ const contentEl = winEl.querySelector('.window-content');
+ if (!contentEl) return;
+
+ const root = contentEl.querySelector('.xp-ie');
+ const addr = contentEl.querySelector('.xp-ie-address');
+ const body = contentEl.querySelector('.xp-ie-content');
+ const form = contentEl.querySelector('.xp-ie-address-row');
+ if (!root || !addr || !body || !form) return;
+
+ const backStack = [];
+ const forwardStack = [];
+ let currentUrl = normalizeRetroUrl(startUrl);
+
+ function setTitle(url, page) {
+ const titleEl = winEl.querySelector('.window-title');
+ if (titleEl) {
+ titleEl.textContent = `${page?.title || 'Internet Explorer'} - Internet Explorer`;
+ }
+
+ const entry = windowsById.get(winEl.dataset.windowId || '');
+ if (entry) {
+ entry.title = `IE: ${page?.title || url}`;
+ updateTaskbarButtons();
+ }
+ }
+
+ function updateNavButtons() {
+ contentEl.querySelectorAll('.xp-ie-btn').forEach((btn) => {
+ const action = btn.getAttribute('data-ie-action');
+ if (action === 'back') btn.disabled = backStack.length === 0;
+ if (action === 'forward') btn.disabled = forwardStack.length === 0;
+ });
+ }
+
+ async function renderPage(url, pushHistory = false) {
+ const normalized = normalizeRetroUrl(url);
+ if (pushHistory && normalized !== currentUrl) {
+ backStack.push(currentUrl);
+ forwardStack.length = 0;
+ }
+
+ currentUrl = normalized;
+ root.dataset.url = currentUrl;
+ addr.value = currentUrl;
+ body.innerHTML = 'Dialing up... Loading retro page...
';
+
+ const page = await getRetroPage(currentUrl);
+ if (!page) {
+ body.innerHTML = `
+
+
The page cannot be displayed
+
URL: ${htmlEscape(currentUrl)}
+
Try home://portal or run a search.
+
+ `;
+ setTitle(currentUrl, { title: 'Cannot display page' });
+ } else {
+ body.innerHTML = wrapRetroShell(page, currentUrl);
+ setTitle(currentUrl, page);
+ }
+
+ updateNavButtons();
+
+ body.querySelectorAll('a[href]').forEach((a) => {
+ a.addEventListener('click', (e) => {
+ e.preventDefault();
+ const href = a.getAttribute('href');
+ if (href) renderPage(href, true);
+ });
+ });
+ }
+
+ contentEl.querySelectorAll('.xp-ie-btn').forEach((btn) => {
+ btn.addEventListener('click', async () => {
+ const action = btn.getAttribute('data-ie-action');
+ if (action === 'home') {
+ renderPage(RETRO_WEB_HOME, true);
+ return;
+ }
+ if (action === 'refresh') {
+ renderPage(currentUrl, false);
+ return;
+ }
+ if (action === 'back' && backStack.length) {
+ forwardStack.push(currentUrl);
+ const prev = backStack.pop();
+ if (prev) renderPage(prev, false);
+ return;
+ }
+ if (action === 'forward' && forwardStack.length) {
+ backStack.push(currentUrl);
+ const next = forwardStack.pop();
+ if (next) renderPage(next, false);
+ }
+ });
+ });
+
+ form.addEventListener('submit', (e) => {
+ e.preventDefault();
+ renderPage(addr.value, true);
+ });
+
+ renderPage(currentUrl, false);
+}
+
+async function openIeApp() {
+ const html = await renderIeApp(RETRO_WEB_HOME);
+ const winEl = openAppFromDescriptor({
+ title: 'Internet Explorer',
+ html,
+ });
+ if (winEl) wireIeInteractions(winEl, RETRO_WEB_HOME);
+}
+
+function renderImageViewer(fileName, imageUrl) {
+ const safeName = htmlEscape(fileName || 'Image');
+ const safeUrl = htmlEscape(imageUrl || '');
+ return `
+
+
+
+
+
+
+ ${safeName}
+
+
+

+
+
+ `;
+}
+
+function wireImageViewerInteractions(winEl) {
+ const contentEl = winEl.querySelector('.window-content');
+ if (!contentEl) return;
+
+ const img = contentEl.querySelector('.xp-image-content');
+ const stage = contentEl.querySelector('.xp-image-stage');
+ if (!img || !stage) return;
+
+ let scale = 1;
+ let fitMode = true;
+
+ function applyScale() {
+ if (fitMode) {
+ img.style.maxWidth = '100%';
+ img.style.maxHeight = '100%';
+ img.style.width = 'auto';
+ img.style.height = 'auto';
+ img.style.transform = 'none';
+ return;
+ }
+
+ img.style.maxWidth = 'none';
+ img.style.maxHeight = 'none';
+ img.style.width = 'auto';
+ img.style.height = 'auto';
+ img.style.transformOrigin = 'center center';
+ img.style.transform = `scale(${scale})`;
+ }
+
+ contentEl.querySelectorAll('.xp-image-btn').forEach((btn) => {
+ btn.addEventListener('click', () => {
+ const action = btn.getAttribute('data-img-action');
+
+ if (action === 'fit') {
+ fitMode = true;
+ applyScale();
+ return;
+ }
+
+ if (action === 'actual') {
+ fitMode = false;
+ scale = 1;
+ applyScale();
+ return;
+ }
+
+ if (action === 'zoom-in') {
+ fitMode = false;
+ scale = Math.min(6, scale + 0.2);
+ applyScale();
+ return;
+ }
+
+ if (action === 'zoom-out') {
+ fitMode = false;
+ scale = Math.max(0.2, scale - 0.2);
+ applyScale();
+ }
+ });
+ });
+
+ img.addEventListener('dblclick', () => {
+ fitMode = !fitMode;
+ if (!fitMode) scale = 1;
+ applyScale();
+ });
+
+ applyScale();
+}
+
+function openImageViewer(fileName, imageUrl) {
+ const winEl = openAppFromDescriptor({
+ title: fileName || 'Image Viewer',
+ html: renderImageViewer(fileName, imageUrl),
+ });
+ if (winEl) wireImageViewerInteractions(winEl);
+}
+
+function renderAudioPlayer(fileName, audioUrl) {
+ const safeName = htmlEscape(fileName || 'Track');
+ const safeUrl = htmlEscape(audioUrl || '');
+ return `
+
+
+
WA
+
+
${safeName} โข ${safeName} โข ${safeName}
+
+
+
+
+
+
+
+
+
00:00 / 00:00
+
+
+ `;
+}
+
+function wireAudioPlayerInteractions(winEl) {
+ const contentEl = winEl.querySelector('.window-content');
+ if (!contentEl) return;
+
+ const audio = contentEl.querySelector('.xp-audio-element');
+ const seek = contentEl.querySelector('.xp-audio-seek');
+ const timeEl = contentEl.querySelector('.xp-audio-time');
+ if (!audio || !seek || !timeEl) return;
+
+ function fmt(sec) {
+ if (!Number.isFinite(sec) || sec < 0) return '00:00';
+ const m = Math.floor(sec / 60).toString().padStart(2, '0');
+ const s = Math.floor(sec % 60).toString().padStart(2, '0');
+ return `${m}:${s}`;
+ }
+
+ function updateTime() {
+ const dur = Number.isFinite(audio.duration) ? audio.duration : 0;
+ const cur = Number.isFinite(audio.currentTime) ? audio.currentTime : 0;
+ const pct = dur > 0 ? Math.round((cur / dur) * 1000) : 0;
+ seek.value = String(pct);
+ timeEl.textContent = `${fmt(cur)} / ${fmt(dur)}`;
+ }
+
+ let seeking = false;
+ seek.addEventListener('input', () => {
+ seeking = true;
+ const dur = Number.isFinite(audio.duration) ? audio.duration : 0;
+ const pct = Number(seek.value) / 1000;
+ if (dur > 0) audio.currentTime = pct * dur;
+ updateTime();
+ });
+ seek.addEventListener('change', () => {
+ seeking = false;
+ });
+
+ contentEl.querySelectorAll('.xp-audio-btn').forEach((btn) => {
+ btn.addEventListener('click', async () => {
+ const action = btn.getAttribute('data-audio-action');
+ if (action === 'play') {
+ try { await audio.play(); } catch {}
+ return;
+ }
+ if (action === 'pause') {
+ audio.pause();
+ return;
+ }
+ if (action === 'stop') {
+ audio.pause();
+ audio.currentTime = 0;
+ updateTime();
+ }
+ });
+ });
+
+ audio.addEventListener('timeupdate', () => {
+ if (!seeking) updateTime();
+ });
+ audio.addEventListener('loadedmetadata', updateTime);
+ audio.addEventListener('ended', () => {
+ audio.currentTime = 0;
+ updateTime();
+ });
+
+ updateTime();
+}
+
+function openAudioPlayer(fileName, audioUrl) {
+ const winEl = openAppFromDescriptor({
+ title: `Winamp - ${fileName || 'Track'}`,
+ html: renderAudioPlayer(fileName, audioUrl),
+ width: 420,
+ height: 180,
+ });
+ if (winEl) wireAudioPlayerInteractions(winEl);
+}
+
+function openVirtualFile(item, fullPath) {
+ if (item.fileType === 'txt') {
+ openNotepadFile(fullPath || null, item.name, String(item.content || ''));
+ return;
+ }
+
+ if (item.fileType === 'image') {
+ openImageViewer(item.name, item.content);
+ return;
+ }
+
+ if (item.fileType === 'audio') {
+ openAudioPlayer(item.name, item.content);
+ return;
+ }
+
+ openAppFromDescriptor({
+ title: item.name,
+ html: `Cannot open this file type.
`,
+ });
+}
+
+function wireExplorerInteractions(winEl, startPath) {
+ const initialPath = startPath;
+ let currentPath = startPath;
+ let selectedKey = null;
+ let selectedAll = false;
+ const backStack = [];
+ const forwardStack = [];
+
+ const contentEl = winEl.querySelector('.window-content');
+ if (!contentEl) return;
+
+ function folderItems() {
+ return listFolder(currentPath);
+ }
+
+ function selectedItemFromKey() {
+ if (!selectedKey) return null;
+ const [type, encoded] = selectedKey.split('|');
+ const name = decodeURIComponent(encoded || '');
+ const folder = getNodeByPath(currentPath);
+ if (!folder || !folder.children) return null;
+ return folder.children.find((child) => child.type === type && child.name === name) || null;
+ }
+
+ function firstSelectableKey() {
+ const items = folderItems();
+ if (!items.length) return null;
+ return `${items[0].type}|${encodeURIComponent(items[0].name)}`;
+ }
+
+ function openItemFromKey(key) {
+ if (!key) return;
+ const [type, encoded] = key.split('|');
+ const name = decodeURIComponent(encoded || '');
+ const folder = getNodeByPath(currentPath);
+ if (!folder || !folder.children) return;
+ const item = folder.children.find((child) => child.type === type && child.name === name);
+ if (!item) return;
+
+ const fullPath = joinPath([...splitPath(currentPath), item.name]);
+
+ if (item.type === 'folder') {
+ navigateTo(fullPath);
+ return;
+ }
+
+ openVirtualFile(item, fullPath);
+ }
+
+ function selectByIndex(index) {
+ const items = folderItems();
+ if (!items.length) return;
+ const i = clamp(index, 0, items.length - 1);
+ selectedAll = false;
+ selectedKey = `${items[i].type}|${encodeURIComponent(items[i].name)}`;
+ rerender();
+ }
+
+ function moveSelection(delta) {
+ const items = folderItems();
+ if (!items.length) return;
+
+ if (!selectedKey || selectedAll) {
+ selectByIndex(delta > 0 ? 0 : items.length - 1);
+ return;
+ }
+
+ const currentIndex = items.findIndex((it) => `${it.type}|${encodeURIComponent(it.name)}` === selectedKey);
+ if (currentIndex === -1) {
+ selectByIndex(0);
+ return;
+ }
+
+ selectByIndex(currentIndex + delta);
+ }
+
+ function navigateTo(path, pushHistory = true) {
+ const node = getNodeByPath(path);
+ if (!node || node.type !== 'folder') return;
+ if (path === currentPath) return;
+
+ if (pushHistory) {
+ backStack.push(currentPath);
+ forwardStack.length = 0;
+ }
+
+ currentPath = path;
+ selectedKey = null;
+ selectedAll = false;
+ rerender();
+ }
+
+ function rerender() {
+ const selectedItem = selectedAll ? null : selectedItemFromKey();
+ contentEl.innerHTML = renderExplorer(currentPath, selectedItem, {
+ canBack: backStack.length > 0,
+ canForward: forwardStack.length > 0,
+ }, selectedAll);
+
+ const titleEl = winEl.querySelector('.window-title');
+ if (titleEl) {
+ if (initialPath === '/Recycle Bin') {
+ titleEl.textContent = 'Recycle Bin';
+ } else {
+ titleEl.textContent = currentPath === '/' ? 'My Computer' : splitPath(currentPath).slice(-1)[0] || 'Explorer';
+ }
+ }
+
+ contentEl.querySelectorAll('.xp-nav-btn').forEach((btn) => {
+ btn.addEventListener('click', () => {
+ const action = btn.getAttribute('data-action');
+
+ if (action === 'back' && backStack.length) {
+ forwardStack.push(currentPath);
+ currentPath = backStack.pop() || '/';
+ selectedKey = null;
+ selectedAll = false;
+ rerender();
+ return;
+ }
+
+ if (action === 'forward' && forwardStack.length) {
+ backStack.push(currentPath);
+ currentPath = forwardStack.pop() || '/';
+ selectedKey = null;
+ selectedAll = false;
+ rerender();
+ return;
+ }
+
+ if (action === 'root') {
+ navigateTo('/');
+ return;
+ }
+
+ if (action === 'up') {
+ const parts = splitPath(currentPath);
+ if (!parts.length) return;
+ parts.pop();
+ navigateTo(joinPath(parts));
+ }
+ });
+ });
+
+ contentEl.querySelectorAll('.xp-side-link').forEach((btn) => {
+ btn.addEventListener('click', () => {
+ const jump = btn.getAttribute('data-jump') || '/';
+ navigateTo(jump);
+ });
+ });
+
+ contentEl.querySelectorAll('.xp-file-item').forEach((btn) => {
+ btn.addEventListener('click', () => {
+ const kind = btn.getAttribute('data-kind');
+ const encoded = btn.getAttribute('data-name');
+ if (!kind || !encoded) return;
+ selectedAll = false;
+ selectedKey = `${kind}|${encoded}`;
+
+ contentEl.querySelectorAll('.xp-file-item.selected').forEach((el) => el.classList.remove('selected'));
+ btn.classList.add('selected');
+
+ const folder = getNodeByPath(currentPath);
+ const name = decodeURIComponent(encoded);
+ const item = folder?.children?.find((child) => child.type === kind && child.name === name) || null;
+ const detailsEl = contentEl.querySelector('.xp-details');
+ if (detailsEl) detailsEl.innerHTML = renderExplorerDetails(currentPath, item);
+ });
+
+ btn.addEventListener('dblclick', () => {
+ const encoded = btn.getAttribute('data-name');
+ const kind = btn.getAttribute('data-kind');
+ const name = decodeURIComponent(encoded || '');
+ if (!name || !kind) return;
+
+ const folder = getNodeByPath(currentPath);
+ if (!folder || !folder.children) return;
+ const item = folder.children.find((child) => child.name === name && child.type === kind);
+ if (!item) return;
+
+ const fullPath = joinPath([...splitPath(currentPath), item.name]);
+
+ if (item.type === 'folder') {
+ navigateTo(fullPath);
+ return;
+ }
+
+ openVirtualFile(item, fullPath);
+ });
+
+ btn.addEventListener('contextmenu', (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+
+ const encoded = btn.getAttribute('data-name');
+ const kind = btn.getAttribute('data-kind');
+ const name = decodeURIComponent(encoded || '');
+ if (!name || !kind) return;
+
+ const folder = getNodeByPath(currentPath);
+ if (!folder || !folder.children) return;
+ const item = folder.children.find((child) => child.name === name && child.type === kind);
+ if (!item) return;
+
+ selectedAll = false;
+ selectedKey = `${kind}|${encodeURIComponent(name)}`;
+ contentEl.querySelectorAll('.xp-file-item.selected').forEach((el) => el.classList.remove('selected'));
+ btn.classList.add('selected');
+ const detailsEl = contentEl.querySelector('.xp-details');
+ if (detailsEl) detailsEl.innerHTML = renderExplorerDetails(currentPath, item);
+
+ const fullPath = joinPath([...splitPath(currentPath), item.name]);
+ const menuItems = [
+ {
+ label: 'Open',
+ action: () => {
+ if (item.type === 'folder') navigateTo(fullPath);
+ else openVirtualFile(item, fullPath);
+ },
+ },
+ item.type === 'folder'
+ ? {
+ label: 'Open in New Window',
+ action: () => openExplorerAtPath(fullPath, item.name),
+ }
+ : {
+ label: 'Open Parent Folder',
+ action: () => navigateTo(currentPath),
+ },
+ 'separator',
+ {
+ label: 'Properties',
+ action: () => {
+ openPropertiesWindow(item.name, [
+ { label: 'Type', value: itemTypeLabel(item) },
+ { label: 'Path', value: fullPath },
+ { label: 'Size', value: itemSizeLabel(item) },
+ { label: 'Modified', value: itemModifiedLabel(item) },
+ ]);
+ },
+ },
+ ];
+
+ showContextMenu(e.clientX, e.clientY, menuItems);
+ });
+ });
+ }
+
+ if (!getNodeByPath(currentPath)) currentPath = '/';
+ rerender();
+
+ winEl.addEventListener('keydown', (e) => {
+ const activeEl = document.activeElement;
+ const isTyping = activeEl && (activeEl.tagName === 'INPUT' || activeEl.tagName === 'TEXTAREA');
+ if (isTyping) return;
+
+ if (e.ctrlKey && (e.key === 'a' || e.key === 'A')) {
+ e.preventDefault();
+ const items = folderItems();
+ if (!items.length) return;
+ selectedAll = true;
+ selectedKey = firstSelectableKey();
+ rerender();
+ return;
+ }
+
+ if (e.key === 'ArrowDown' || e.key === 'ArrowRight') {
+ e.preventDefault();
+ moveSelection(1);
+ return;
+ }
+
+ if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') {
+ e.preventDefault();
+ moveSelection(-1);
+ return;
+ }
+
+ if (e.key === 'Enter') {
+ e.preventDefault();
+ openItemFromKey(selectedKey || firstSelectableKey());
+ return;
+ }
+
+ if (e.key === 'Backspace') {
+ e.preventDefault();
+ const parts = splitPath(currentPath);
+ if (!parts.length) return;
+ parts.pop();
+ navigateTo(joinPath(parts));
+ }
+ });
+}
+
+function setWindowPosition(winEl, left, top) {
+ const maxLeft = Math.max(0, window.innerWidth - winEl.offsetWidth - 8);
+ const maxTop = Math.max(36, window.innerHeight - winEl.offsetHeight - 40);
+ winEl.style.left = `${clamp(left, 0, maxLeft)}px`;
+ winEl.style.top = `${clamp(top, 32, maxTop)}px`;
+ winEl.style.transform = 'none';
+}
+
+function updateTaskbarButtons() {
+ windowsById.forEach((entry) => {
+ const { taskBtn, winEl, title } = entry;
+ taskBtn.textContent = title;
+ taskBtn.classList.toggle('active', !winEl.classList.contains('hidden') && entry.active);
+ });
+}
+
+function activateWindow(id) {
+ const target = windowsById.get(id);
+ if (!target) return;
+
+ windowsById.forEach((entry) => {
+ entry.active = false;
+ entry.winEl.classList.remove('active');
+ });
+
+ target.active = true;
+ target.winEl.classList.add('active');
+ showDesktopBtn?.classList.remove('active');
+ topWindowZ += 1;
+ target.winEl.style.zIndex = String(topWindowZ);
+ updateTaskbarButtons();
+}
+
+function bringToFront(winEl) {
+ const id = winEl.dataset.windowId;
+ if (!id) return;
+ activateWindow(id);
+ winEl.focus();
+}
+
+function minimizeWindow(id) {
+ const entry = windowsById.get(id);
+ if (!entry) return;
+
+ entry.winEl.classList.add('hidden');
+ entry.active = false;
+
+ const nextVisible = Array.from(windowsById.values()).find((w) => !w.winEl.classList.contains('hidden'));
+ if (nextVisible) {
+ activateWindow(nextVisible.id);
+ }
+
+ updateTaskbarButtons();
+ setStatus(`${entry.title} minimized`);
+}
+
+function restoreWindow(id) {
+ const entry = windowsById.get(id);
+ if (!entry) return;
+
+ entry.winEl.classList.remove('hidden');
+ activateWindow(id);
+ setStatus(`${entry.title} restored`);
+}
+
+function toggleShowDesktop() {
+ const entries = Array.from(windowsById.values());
+ if (!entries.length) return;
+
+ const hasVisible = entries.some((entry) => !entry.winEl.classList.contains('hidden'));
+
+ if (hasVisible) {
+ entries.forEach((entry) => {
+ entry.winEl.classList.add('hidden');
+ entry.active = false;
+ });
+ showDesktopBtn?.classList.add('active');
+ updateTaskbarButtons();
+ setStatus('Showing desktop');
+ return;
+ }
+
+ const lastFocused = entries.sort((a, b) => {
+ const za = parseInt(a.winEl.style.zIndex || '0', 10);
+ const zb = parseInt(b.winEl.style.zIndex || '0', 10);
+ return zb - za;
+ })[0];
+
+ entries.forEach((entry) => entry.winEl.classList.remove('hidden'));
+ showDesktopBtn?.classList.remove('active');
+ if (lastFocused) activateWindow(lastFocused.id);
+ else updateTaskbarButtons();
+ setStatus('Windows restored');
+}
+
+function closeWindow(id) {
+ const entry = windowsById.get(id);
+ if (!entry) return;
+
+ entry.winEl.remove();
+ entry.taskBtn.remove();
+ windowsById.delete(id);
+
+ if (windowsById.size) {
+ const newest = Array.from(windowsById.values()).sort((a, b) => {
+ const za = parseInt(a.winEl.style.zIndex || '0', 10);
+ const zb = parseInt(b.winEl.style.zIndex || '0', 10);
+ return zb - za;
+ })[0];
+ if (newest) activateWindow(newest.id);
+ }
+
+ updateTaskbarButtons();
+ setStatus(windowsById.size ? `${windowsById.size} window(s) open` : 'No windows open');
+}
+
+function bindWindowDrag(winEl) {
+ const titlebar = winEl.querySelector('.window-titlebar');
+ const closeBtn = winEl.querySelector('.window-close');
+ const minimizeBtn = winEl.querySelector('.window-minimize');
+ const resizeHandle = winEl.querySelector('.window-resize-handle');
+ const id = winEl.dataset.windowId;
+ if (!titlebar || !closeBtn || !minimizeBtn || !id) return;
+
+ winEl.addEventListener('pointerdown', () => bringToFront(winEl));
+
+ titlebar.addEventListener('pointerdown', (e) => {
+ if (e.button !== 0) return;
+ if (e.target === closeBtn || e.target === minimizeBtn) return;
+
+ bringToFront(winEl);
+
+ const rect = getWindowRect(winEl);
+ setWindowPosition(winEl, rect.left, rect.top);
+
+ const startLeft = rect.left;
+ const startTop = rect.top;
+ const startX = e.clientX;
+ const startY = e.clientY;
+ let moved = false;
+
+ winEl.classList.add('dragging');
+ titlebar.setPointerCapture(e.pointerId);
+
+ const onMove = (ev) => {
+ const dx = ev.clientX - startX;
+ const dy = ev.clientY - startY;
+ if (!moved && Math.hypot(dx, dy) > 2) moved = true;
+ if (!moved) return;
+ setWindowPosition(winEl, startLeft + dx, startTop + dy);
+ };
+
+ const onUp = () => {
+ winEl.classList.remove('dragging');
+ titlebar.removeEventListener('pointermove', onMove);
+ titlebar.removeEventListener('pointerup', onUp);
+ titlebar.removeEventListener('pointercancel', onUp);
+ };
+
+ titlebar.addEventListener('pointermove', onMove);
+ titlebar.addEventListener('pointerup', onUp);
+ titlebar.addEventListener('pointercancel', onUp);
+ });
+
+ if (resizeHandle) {
+ resizeHandle.addEventListener('pointerdown', (e) => {
+ if (e.button !== 0) return;
+ e.stopPropagation();
+ bringToFront(winEl);
+
+ const rect = getWindowRect(winEl);
+ const startX = e.clientX;
+ const startY = e.clientY;
+ const startW = rect.width;
+ const startH = rect.height;
+
+ winEl.classList.add('resizing');
+ resizeHandle.setPointerCapture(e.pointerId);
+
+ const onMove = (ev) => {
+ const dx = ev.clientX - startX;
+ const dy = ev.clientY - startY;
+ const minW = 360;
+ const minH = 240;
+ const maxW = Math.max(minW, window.innerWidth - rect.left - 8);
+ const maxH = Math.max(minH, window.innerHeight - rect.top - 40);
+ const nextW = clamp(startW + dx, minW, maxW);
+ const nextH = clamp(startH + dy, minH, maxH);
+ winEl.style.width = `${nextW}px`;
+ winEl.style.height = `${nextH}px`;
+ };
+
+ const onUp = () => {
+ winEl.classList.remove('resizing');
+ resizeHandle.removeEventListener('pointermove', onMove);
+ resizeHandle.removeEventListener('pointerup', onUp);
+ resizeHandle.removeEventListener('pointercancel', onUp);
+ };
+
+ resizeHandle.addEventListener('pointermove', onMove);
+ resizeHandle.addEventListener('pointerup', onUp);
+ resizeHandle.addEventListener('pointercancel', onUp);
+ });
+ }
+
+ minimizeBtn.addEventListener('click', () => minimizeWindow(id));
+ closeBtn.addEventListener('click', () => closeWindow(id));
+}
+
+function createTaskbarButton(id, title) {
+ const btn = document.createElement('button');
+ btn.className = 'taskbar-btn';
+ btn.type = 'button';
+ btn.textContent = title;
+ btn.title = title;
+ btn.addEventListener('click', () => {
+ const entry = windowsById.get(id);
+ if (!entry) return;
+
+ if (entry.winEl.classList.contains('hidden')) {
+ restoreWindow(id);
+ return;
+ }
+
+ if (entry.active) {
+ minimizeWindow(id);
+ } else {
+ activateWindow(id);
+ setStatus(`${entry.title} focused`);
+ }
+ });
+ taskbarWindowsEl.appendChild(btn);
+ return btn;
+}
+
+function addWindowControls(winEl) {
+ const titlebar = winEl.querySelector('.window-titlebar');
+ const closeBtn = winEl.querySelector('.window-close');
+ if (!titlebar || !closeBtn) return;
+
+ const controls = document.createElement('div');
+ controls.className = 'window-controls';
+
+ const minimizeBtn = document.createElement('button');
+ minimizeBtn.className = 'window-minimize';
+ minimizeBtn.type = 'button';
+ minimizeBtn.textContent = 'โ';
+ minimizeBtn.setAttribute('aria-label', 'minimize');
+
+ closeBtn.classList.add('window-close');
+
+ controls.appendChild(minimizeBtn);
+ controls.appendChild(closeBtn);
+ titlebar.appendChild(controls);
+
+ const resizeHandle = document.createElement('div');
+ resizeHandle.className = 'window-resize-handle';
+ resizeHandle.setAttribute('aria-hidden', 'true');
+ winEl.appendChild(resizeHandle);
+}
+
+function openAppFromDescriptor(descriptor) {
+ if (!descriptor || !windowTemplate) return null;
+
+ hideStartMenu();
+
+ windowSeq += 1;
+ const id = `w${windowSeq}`;
+
+ const winEl = windowTemplate.cloneNode(true);
+ winEl.id = '';
+ winEl.dataset.windowId = id;
+ winEl.classList.remove('hidden');
+
+ const titleEl = winEl.querySelector('.window-title');
+ const contentEl = winEl.querySelector('.window-content');
+
+ if (titleEl) titleEl.textContent = descriptor.title || 'Window';
+ if (contentEl) contentEl.innerHTML = descriptor.html || 'Empty window
';
+
+ addWindowControls(winEl);
+ document.body.appendChild(winEl);
+
+ const offset = ((windowSeq - 1) % 8) * 22;
+ const left = (window.innerWidth - winEl.offsetWidth) / 2 + offset;
+ const top = 68 + offset;
+
+ if (descriptor.width) {
+ winEl.style.width = typeof descriptor.width === 'number' ? `${descriptor.width}px` : String(descriptor.width);
+ }
+ if (descriptor.height) {
+ winEl.style.height = typeof descriptor.height === 'number' ? `${descriptor.height}px` : String(descriptor.height);
+ }
+
+ setWindowPosition(winEl, left, top);
+
+ const taskBtn = createTaskbarButton(id, descriptor.title || 'Window');
+
+ windowsById.set(id, {
+ id,
+ title: descriptor.title || 'Window',
+ winEl,
+ taskBtn,
+ active: false,
+ });
+
+ bindWindowDrag(winEl);
+ activateWindow(id);
+ updateTaskbarButtons();
+
+ winEl.setAttribute('tabindex', '0');
+
+ winEl.focus();
+ setStatus(`${descriptor.title || 'Window'} opened`);
+ return winEl;
+}
+
+function openExplorerAtPath(path = '/', title = 'Explorer') {
+ const winEl = openAppFromDescriptor({
+ title,
+ html: 'Loading...
',
+ });
+ if (winEl) wireExplorerInteractions(winEl, path);
+}
+
+function openSystemPanel(kind) {
+ const panels = {
+ 'control-panel': {
+ title: 'Control Panel',
+ html: 'โ๏ธ Control Panel is under construction.
Try My Documents, Music, Paint, and Notepad from Start.
',
+ },
+ help: {
+ title: 'Help and Support',
+ html: 'โ Need help? Double-click folders/files in Explorer, drag desktop icons, and use the tray reset/show-desktop buttons.
',
+ },
+ run: {
+ title: 'Run',
+ html: '๐ Run shortcut: open Internet, Notepad, Paint, or My Computer from Start.
',
+ },
+ };
+
+ const panel = panels[kind];
+ if (!panel) return;
+ openAppFromDescriptor(panel);
+}
+
+function performSystemAction(action) {
+ if (action === 'logoff') {
+ hideStartMenu();
+ clearSelection();
+ setStatus('Logged off (mock)');
+ return;
+ }
+
+ if (action === 'shutdown') {
+ hideStartMenu();
+ windowsById.forEach((entry) => {
+ entry.winEl.classList.add('hidden');
+ entry.active = false;
+ });
+ showDesktopBtn?.classList.add('active');
+ updateTaskbarButtons();
+ setStatus('It is now safe to turn off your imagination โจ');
+ return;
+ }
+
+ openSystemPanel(action);
+}
+
+function openApp(key) {
+ const app = ICON_APPS[key];
+ if (!app) return;
+
+ if (app.type === 'explorer') {
+ openExplorerAtPath(app.startPath || '/', app.title || 'Explorer');
+ return;
+ }
+
+ if (app.type === 'notepad') {
+ openNotepadFile(null, 'Untitled.txt', '');
+ return;
+ }
+
+ if (app.type === 'paint') {
+ openPaintApp();
+ return;
+ }
+
+ if (app.type === 'ie') {
+ openIeApp();
+ return;
+ }
+
+ openAppFromDescriptor(app);
+}
+
+function bindIconDrag(btn) {
+ btn.addEventListener('pointerdown', (e) => {
+ if (e.button !== 0) return;
+ e.stopPropagation();
+
+ if (e.shiftKey) {
+ toggleSelection(btn);
+ } else if (!selectedIcons.has(btn)) {
+ setSelection([btn]);
+ }
+
+ const dragSet = selectedIcons.size ? Array.from(selectedIcons) : [btn];
+ const starts = dragSet.map((icon) => ({ icon, ...parsePosition(icon) }));
+ const startX = e.clientX;
+ const startY = e.clientY;
+ let moved = false;
+
+ dragSet.forEach((icon) => icon.classList.add('dragging'));
+ btn.setPointerCapture(e.pointerId);
+
+ const onMove = (ev) => {
+ const dx = ev.clientX - startX;
+ const dy = ev.clientY - startY;
+ if (!moved && Math.hypot(dx, dy) > 3) moved = true;
+ if (!moved) return;
+
+ starts.forEach(({ icon, x, y }) => setIconPosition(icon, x + dx, y + dy));
+ setStatus(`Moving ${dragSet.length} item(s)`);
+ };
+
+ const onUp = () => {
+ dragSet.forEach((icon) => icon.classList.remove('dragging'));
+ btn.removeEventListener('pointermove', onMove);
+ btn.removeEventListener('pointerup', onUp);
+ btn.removeEventListener('pointercancel', onUp);
+
+ if (moved) {
+ btn.dataset.wasDragged = '1';
+ saveIconLayout();
+ setStatus('Desktop layout saved');
+ }
+ };
+
+ btn.addEventListener('pointermove', onMove);
+ btn.addEventListener('pointerup', onUp);
+ btn.addEventListener('pointercancel', onUp);
+ });
+
+ btn.addEventListener('click', (e) => {
+ if (btn.dataset.wasDragged === '1') {
+ btn.dataset.wasDragged = '0';
+ return;
+ }
+
+ if (e.shiftKey) {
+ toggleSelection(btn);
+ return;
+ }
+
+ if (!selectedIcons.has(btn) || selectedIcons.size !== 1) {
+ setSelection([btn]);
+ return;
+ }
+ });
+
+ btn.addEventListener('dblclick', (e) => {
+ e.stopPropagation();
+ setSelection([btn]);
+ openApp(btn.dataset.app);
+ });
+
+ btn.addEventListener('contextmenu', (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+
+ if (!selectedIcons.has(btn)) {
+ setSelection([btn]);
+ }
+
+ showContextMenu(e.clientX, e.clientY, [
+ {
+ label: 'Open',
+ action: () => openApp(btn.dataset.app),
+ },
+ {
+ label: 'Open All Selected',
+ disabled: selectedIcons.size <= 1,
+ action: () => {
+ Array.from(selectedIcons).forEach((icon) => openApp(icon.dataset.app));
+ },
+ },
+ 'separator',
+ {
+ label: 'Reset Icon Layout',
+ action: () => {
+ clearSelection();
+ localStorage.removeItem(ICON_LAYOUT_KEY);
+ resetIconLayout();
+ },
+ },
+ {
+ label: 'Properties',
+ action: () => {
+ const appKey = btn.dataset.app || '';
+ const appTitle = ICON_APPS[appKey]?.title || btn.innerText || 'Desktop Item';
+ const pos = parsePosition(btn);
+ openPropertiesWindow(appTitle, [
+ { label: 'Type', value: 'Desktop Shortcut' },
+ { label: 'App Key', value: appKey || 'unknown' },
+ { label: 'Position', value: `${pos.x}, ${pos.y}` },
+ ]);
+ },
+ },
+ ]);
+ });
+}
+
+function showStartMenu() {
+ if (!startMenuEl) return;
+ startMenuEl.classList.remove('hidden');
+ startBtn?.classList.add('active');
+}
+
+function hideStartMenu() {
+ if (!startMenuEl) return;
+ startMenuEl.classList.add('hidden');
+ startBtn?.classList.remove('active');
+}
+
+function toggleStartMenu() {
+ if (!startMenuEl) return;
+ if (startMenuEl.classList.contains('hidden')) {
+ showStartMenu();
+ } else {
+ hideStartMenu();
+ }
+}
+
+function bindDesktopSelection() {
+ const selectionBox = ensureSelectionBox();
+
+ iconsEl.addEventListener('pointerdown', (e) => {
+ if (e.button !== 0) return;
+ if (e.target.closest('.desktop-icon')) return;
+
+ const rect = iconsEl.getBoundingClientRect();
+ const startX = clamp(e.clientX - rect.left, 0, iconsEl.clientWidth);
+ const startY = clamp(e.clientY - rect.top, 0, iconsEl.clientHeight);
+ let moved = false;
+
+ if (!e.shiftKey) clearSelection();
+
+ selectionBox.hidden = false;
+ selectionBox.style.left = `${startX}px`;
+ selectionBox.style.top = `${startY}px`;
+ selectionBox.style.width = '0px';
+ selectionBox.style.height = '0px';
+
+ iconsEl.setPointerCapture(e.pointerId);
+
+ const onMove = (ev) => {
+ const currentX = clamp(ev.clientX - rect.left, 0, iconsEl.clientWidth);
+ const currentY = clamp(ev.clientY - rect.top, 0, iconsEl.clientHeight);
+ const left = Math.min(startX, currentX);
+ const top = Math.min(startY, currentY);
+ const width = Math.abs(currentX - startX);
+ const height = Math.abs(currentY - startY);
+
+ moved = moved || width > 2 || height > 2;
+
+ selectionBox.style.left = `${left}px`;
+ selectionBox.style.top = `${top}px`;
+ selectionBox.style.width = `${width}px`;
+ selectionBox.style.height = `${height}px`;
+
+ if (!moved) return;
+
+ const boxRect = { left, top, right: left + width, bottom: top + height };
+
+ iconButtons.forEach((btn) => {
+ const hit = intersects(boxRect, toIconRect(btn));
+ if (hit) {
+ selectedIcons.add(btn);
+ btn.classList.add('selected');
+ } else if (!e.shiftKey) {
+ selectedIcons.delete(btn);
+ btn.classList.remove('selected');
+ }
+ });
+
+ if (selectedIcons.size) {
+ setStatus(`${selectedIcons.size} item(s) selected`);
+ }
+ };
+
+ const onUp = () => {
+ selectionBox.hidden = true;
+ iconsEl.removeEventListener('pointermove', onMove);
+ iconsEl.removeEventListener('pointerup', onUp);
+ iconsEl.removeEventListener('pointercancel', onUp);
+
+ if (!moved && !e.shiftKey) {
+ clearSelection();
+ setStatus(windowsById.size ? `${windowsById.size} window(s) open` : 'No windows open');
+ }
+ };
+
+ iconsEl.addEventListener('pointermove', onMove);
+ iconsEl.addEventListener('pointerup', onUp);
+ iconsEl.addEventListener('pointercancel', onUp);
+ });
+}
+
+iconButtons.forEach(bindIconDrag);
+bindDesktopSelection();
+
+window.addEventListener('resize', () => {
+ hideContextMenu();
+
+ iconButtons.forEach((btn) => {
+ const { x, y } = parsePosition(btn);
+ setIconPosition(btn, x, y);
+ });
+
+ windowsById.forEach(({ winEl }) => {
+ if (winEl.classList.contains('hidden')) return;
+ const rect = getWindowRect(winEl);
+ setWindowPosition(winEl, rect.left, rect.top);
+ });
+
+ saveIconLayout();
+});
+
+resetLayoutBtn?.addEventListener('click', () => {
+ clearSelection();
+ localStorage.removeItem(ICON_LAYOUT_KEY);
+ resetIconLayout();
+});
+
+startBtn?.addEventListener('click', (e) => {
+ e.stopPropagation();
+ hideContextMenu();
+ toggleStartMenu();
+});
+
+showDesktopBtn?.addEventListener('click', (e) => {
+ e.stopPropagation();
+ hideContextMenu();
+ toggleShowDesktop();
+});
+
+startMenuEl?.querySelectorAll('[data-open-app]').forEach((btn) => {
+ btn.addEventListener('click', () => {
+ const appKey = btn.getAttribute('data-open-app');
+ if (appKey) openApp(appKey);
+ });
+});
+
+startMenuEl?.querySelectorAll('[data-open-path]').forEach((btn) => {
+ btn.addEventListener('click', () => {
+ const path = btn.getAttribute('data-open-path') || '/';
+ const label = btn.textContent?.replace(/^[^\w]+\s*/, '').trim() || 'Explorer';
+ openExplorerAtPath(path, label);
+ });
+});
+
+startMenuEl?.querySelectorAll('[data-system-action]').forEach((btn) => {
+ btn.addEventListener('click', () => {
+ const action = btn.getAttribute('data-system-action');
+ if (action) performSystemAction(action);
+ });
+});
+
+document.addEventListener('click', (e) => {
+ hideContextMenu();
+ if (!startMenuEl || startMenuEl.classList.contains('hidden')) return;
+ if (startMenuEl.contains(e.target)) return;
+ if (startBtn && startBtn.contains(e.target)) return;
+ hideStartMenu();
+});
+
+document.addEventListener('contextmenu', () => {
+ hideContextMenu();
+});
+
+document.addEventListener('keydown', (e) => {
+ if (e.key === 'Escape') {
+ hideContextMenu();
+ hideStartMenu();
+ }
+});
+
+function tickClock() {
+ const d = new Date();
+ const timeText = d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
+ const dateText = d.toLocaleDateString('en-US', { month: 'numeric', day: 'numeric', year: 'numeric' });
+ clockEl.innerHTML = `${timeText}${dateText}`;
+}
+
+applyInitialLayout();
+setInterval(tickClock, 1000);
+tickClock();
diff --git a/site/styles.css b/site/styles.css
new file mode 100644
index 0000000..b3542af
--- /dev/null
+++ b/site/styles.css
@@ -0,0 +1,1087 @@
+:root {
+ --pink-sky-1: #ffdff1;
+ --pink-sky-2: #ffc5e6;
+ --pink-sky-3: #f6a8d4;
+ --panel: #fff7fc;
+ --line: #e7a8ca;
+ --titlebar-1: #ff9acb;
+ --titlebar-2: #f07cb6;
+ --titlebar-border: #c65b93;
+ --taskbar-1: #ffb3d9;
+ --taskbar-2: #ef8dc1;
+}
+
+* { box-sizing: border-box; }
+
+body {
+ margin: 0;
+ font-family: "Tahoma", "MS Sans Serif", "Segoe UI", Verdana, sans-serif;
+ font-size: 12px;
+ background-color: #f6a8d4;
+ background-image: url('https://wallpaperaccess.com/full/4810999.jpg');
+ background-size: cover;
+ background-position: center;
+ background-repeat: no-repeat;
+ background-attachment: fixed;
+ color: #4b1c39;
+ min-height: 100vh;
+}
+
+#desktop {
+ min-height: 100vh;
+ display: flex;
+ flex-direction: column;
+ position: relative;
+}
+
+#topbar {
+ background: linear-gradient(var(--titlebar-1), var(--titlebar-2));
+ color: #fff;
+ padding: 5px 8px;
+ font-size: 13px;
+ border-bottom: 1px solid var(--titlebar-border);
+ display: flex;
+ align-items: center;
+ text-shadow: 0 1px 0 rgba(106, 25, 76, 0.45);
+}
+
+#topbar-title {
+ white-space: nowrap;
+ letter-spacing: 0.2px;
+}
+
+#reset-layout-btn,
+#show-desktop-btn {
+ margin-left: 0;
+ width: 22px;
+ height: 20px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0;
+ background: linear-gradient(#ffeef8, #e8a8cc);
+ color: #4c1431;
+ border-top: 1px solid #fff5fb;
+ border-left: 1px solid #fff5fb;
+ border-right: 1px solid #8f3e6b;
+ border-bottom: 1px solid #8f3e6b;
+ border-radius: 0;
+ box-shadow: inset 0 0 0 1px rgba(255, 220, 241, 0.35);
+ cursor: pointer;
+}
+
+#reset-layout-btn > span {
+ font-size: 13px;
+ line-height: 1;
+ transform: translateY(-1px);
+}
+
+#reset-layout-btn:hover,
+#show-desktop-btn:hover {
+ background: linear-gradient(#fff4fb, #efb7d6);
+}
+
+#reset-layout-btn:active,
+#show-desktop-btn:active,
+#show-desktop-btn.active {
+ border-top-color: #8f3e6b;
+ border-left-color: #8f3e6b;
+ border-right-color: #fff5fb;
+ border-bottom-color: #fff5fb;
+ background: linear-gradient(#dc95be, #f2bbd9);
+}
+
+#reset-layout-btn:focus-visible,
+#show-desktop-btn:focus-visible {
+ outline: 1px dotted #fff;
+ outline-offset: -4px;
+}
+
+#icons {
+ position: relative;
+ padding: 16px;
+ flex: 1;
+ overflow: hidden;
+}
+
+.desktop-icon {
+ position: absolute;
+ width: 112px;
+ border: none;
+ background: transparent;
+ color: #fff;
+ text-shadow: 0 1px 2px rgba(70, 25, 52, 0.65);
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 6px;
+ cursor: default;
+ padding: 8px 6px;
+ border-radius: 4px;
+ touch-action: none;
+ user-select: none;
+}
+
+.desktop-icon:hover {
+ background: rgba(255, 255, 255, 0.16);
+}
+
+.desktop-icon.dragging {
+ cursor: grabbing;
+ background: rgba(255, 255, 255, 0.28);
+}
+
+.desktop-icon .emoji {
+ font-size: 36px;
+ width: 100%;
+ text-align: center;
+ display: block;
+ line-height: 1;
+}
+
+.desktop-icon span:last-child {
+ font-size: 12px;
+ text-align: center;
+ width: 100%;
+ line-height: 1.25;
+}
+
+.desktop-icon.selected {
+ background: rgba(115, 185, 255, 0.34);
+ outline: 1px dotted rgba(255, 255, 255, 0.95);
+}
+
+#taskbar {
+ background: linear-gradient(var(--taskbar-1), var(--taskbar-2));
+ border-top: 1px solid #f8d3e9;
+ display: grid;
+ grid-template-columns: auto 1fr auto;
+ gap: 8px;
+ align-items: center;
+ padding: 4px 6px;
+ color: #fff;
+ min-height: 34px;
+ position: relative;
+ z-index: 40;
+}
+
+#start-btn {
+ background: linear-gradient(var(--titlebar-1), var(--titlebar-2));
+ color: #fff;
+ border-top: 1px solid #ffd4ea;
+ border-left: 1px solid #ffd4ea;
+ border-right: 1px solid #9f3972;
+ border-bottom: 1px solid #9f3972;
+ border-radius: 16px;
+ font-weight: 700;
+ font-style: italic;
+ padding: 5px 16px;
+ text-transform: lowercase;
+ cursor: pointer;
+ text-shadow: 0 1px 0 rgba(94, 20, 64, 0.45);
+}
+
+#start-btn:active,
+#start-btn.active {
+ border-top-color: #9f3972;
+ border-left-color: #9f3972;
+ border-right-color: #ffd4ea;
+ border-bottom-color: #ffd4ea;
+ background: linear-gradient(var(--titlebar-2), var(--titlebar-1));
+}
+
+#taskbar-windows {
+ display: flex;
+ gap: 4px;
+ min-width: 0;
+ overflow-x: auto;
+ padding: 1px;
+}
+
+.taskbar-btn {
+ min-width: 120px;
+ max-width: 180px;
+ background: linear-gradient(#ffeaf7, #efb5d5);
+ color: #5a1b3b;
+ border-top: 1px solid #fff6fd;
+ border-left: 1px solid #fff6fd;
+ border-right: 1px solid #9f4a78;
+ border-bottom: 1px solid #9f4a78;
+ padding: 4px 8px;
+ font-size: 11px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ cursor: pointer;
+}
+
+.taskbar-btn.active {
+ background: linear-gradient(#e7a2c8, #f8d4e9);
+ border-top-color: #9f4a78;
+ border-left-color: #9f4a78;
+ border-right-color: #fff6fd;
+ border-bottom-color: #fff6fd;
+}
+
+#taskbar-right {
+ display: inline-flex;
+ align-items: center;
+ justify-content: flex-end;
+ gap: 8px;
+}
+
+#taskbar-brand {
+ font-size: 11px;
+ font-weight: 700;
+ color: #ffeef9;
+ text-shadow: 0 1px 0 rgba(94, 20, 64, 0.45);
+ white-space: nowrap;
+}
+
+#taskbar-tray-buttons {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+}
+
+#clock {
+ font-size: 11px;
+ padding: 2px 8px;
+ border-top: 1px solid rgba(255, 233, 247, 0.85);
+ border-left: 1px solid rgba(255, 233, 247, 0.85);
+ border-right: 1px solid rgba(163, 69, 122, 0.8);
+ border-bottom: 1px solid rgba(163, 69, 122, 0.8);
+ background: rgba(255, 207, 232, 0.48);
+ min-height: 30px;
+ min-width: 72px;
+ display: inline-flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ line-height: 1.05;
+}
+
+#clock .clock-time {
+ font-weight: 700;
+}
+
+#clock .clock-date {
+ font-size: 10px;
+ opacity: 0.95;
+}
+
+#start-menu {
+ position: absolute;
+ left: 8px;
+ bottom: 36px;
+ width: 420px;
+ border-top: 1px solid #f6bfdc;
+ border-left: 1px solid #f6bfdc;
+ border-right: 1px solid #7f2f5a;
+ border-bottom: 1px solid #7f2f5a;
+ border-radius: 7px 7px 0 0;
+ overflow: hidden;
+ background: #fff6fc;
+ box-shadow: 0 10px 30px rgba(90, 27, 59, 0.42);
+ z-index: 1200;
+}
+
+.start-menu-top {
+ background: linear-gradient(var(--titlebar-1), var(--titlebar-2));
+ color: #fff;
+ padding: 10px 14px;
+ font-size: 16px;
+ font-weight: 700;
+ text-shadow: 0 1px 0 rgba(94, 20, 64, 0.55);
+}
+
+.start-menu-body {
+ display: grid;
+ grid-template-columns: 1fr 0.95fr;
+ min-height: 280px;
+}
+
+.start-col {
+ padding: 8px 6px;
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+}
+
+.start-col-left {
+ background: #fff;
+ border-right: 1px solid #efbfd9;
+}
+
+.start-col-right {
+ background: #ffe7f4;
+}
+
+.start-item {
+ border: none;
+ background: transparent;
+ text-align: left;
+ font-size: 12px;
+ padding: 7px 8px;
+ color: #7d2756;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.start-item.secondary {
+ color: #7a3a61;
+}
+
+.start-item:hover {
+ background: linear-gradient(var(--titlebar-1), var(--titlebar-2));
+ color: #fff;
+}
+
+.start-menu-footer {
+ background: linear-gradient(var(--titlebar-2), #d868a8);
+ padding: 7px;
+ display: flex;
+ justify-content: flex-end;
+ gap: 6px;
+}
+
+.start-footer-btn {
+ background: linear-gradient(#fff3fb, #f2c0de);
+ color: #6f2750;
+ border-top: 1px solid #fff;
+ border-left: 1px solid #fff;
+ border-right: 1px solid #9f4a78;
+ border-bottom: 1px solid #9f4a78;
+ padding: 4px 9px;
+ font-size: 11px;
+ cursor: pointer;
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.start-footer-btn:active {
+ border-top-color: #9f4a78;
+ border-left-color: #9f4a78;
+ border-right-color: #fff;
+ border-bottom-color: #fff;
+}
+
+.app-window {
+ position: fixed;
+ top: 70px;
+ left: 50%;
+ transform: translateX(-50%);
+ width: min(760px, 92vw);
+ min-width: 360px;
+ min-height: 240px;
+ background: var(--panel);
+ border-top: 1px solid #ffd9ed;
+ border-left: 1px solid #ffd9ed;
+ border-right: 1px solid #a55282;
+ border-bottom: 1px solid #a55282;
+ box-shadow: 0 12px 26px rgba(112, 34, 82, 0.32);
+ display: flex;
+ flex-direction: column;
+}
+
+.app-window.dragging {
+ user-select: none;
+}
+
+.app-window.dragging .window-titlebar {
+ cursor: grabbing;
+}
+
+.hidden { display: none !important; }
+
+.window-titlebar {
+ background: linear-gradient(var(--titlebar-1), var(--titlebar-2));
+ color: #fff;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 8px;
+ padding: 5px 6px;
+ border-bottom: 1px solid var(--titlebar-border);
+ cursor: grab;
+ text-shadow: 0 1px 0 rgba(106, 25, 76, 0.45);
+}
+
+.window-title {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ font-size: 12px;
+}
+
+.window-title::before {
+ content: "โฃ";
+ font-size: 11px;
+ color: #ffe8f6;
+}
+
+.window-controls {
+ display: inline-flex;
+ gap: 4px;
+}
+
+.window-minimize,
+.window-close {
+ width: 24px;
+ height: 20px;
+ border-top: 1px solid #ffe8f5;
+ border-left: 1px solid #ffe8f5;
+ border-right: 1px solid #8f3e6b;
+ border-bottom: 1px solid #8f3e6b;
+ background: linear-gradient(#ffd9ee, #e499c2);
+ color: #4c1431;
+ font-weight: 700;
+ cursor: pointer;
+ padding: 0;
+ line-height: 1;
+}
+
+.window-minimize:active,
+.window-close:active {
+ border-top-color: #8f3e6b;
+ border-left-color: #8f3e6b;
+ border-right-color: #ffe8f5;
+ border-bottom-color: #ffe8f5;
+}
+
+.window-content {
+ margin: 4px;
+ padding: 12px;
+ flex: 1;
+ min-height: 0;
+ overflow: auto;
+ background: #fff;
+ border-top: 1px solid #a14f7d;
+ border-left: 1px solid #a14f7d;
+ border-right: 1px solid #ffe1f1;
+ border-bottom: 1px solid #ffe1f1;
+}
+
+.window-resize-handle {
+ position: absolute;
+ width: 14px;
+ height: 14px;
+ right: 1px;
+ bottom: 1px;
+ cursor: nwse-resize;
+ background:
+ linear-gradient(135deg, transparent 0 40%, #b56a95 40% 46%, transparent 46% 56%, #b56a95 56% 62%, transparent 62% 72%, #b56a95 72% 78%, transparent 78% 100%);
+}
+
+.app-window.resizing {
+ user-select: none;
+}
+
+#selection-box {
+ position: absolute;
+ border: 1px dashed rgba(255, 255, 255, 0.95);
+ background: rgba(130, 200, 255, 0.22);
+ pointer-events: none;
+ z-index: 20;
+}
+
+.xp-loading {
+ color: #8c3b67;
+ padding: 10px;
+}
+
+.xp-explorer {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ height: 100%;
+ min-height: 0;
+}
+
+.xp-toolbar {
+ display: grid;
+ grid-template-columns: 28px 28px 28px 34px 1fr;
+ gap: 6px;
+ align-items: center;
+}
+
+.xp-nav-btn {
+ border-top: 1px solid #fff;
+ border-left: 1px solid #fff;
+ border-right: 1px solid #9f4a78;
+ border-bottom: 1px solid #9f4a78;
+ background: linear-gradient(#fff3fb, #f2c0de);
+ color: #6f2750;
+ font-size: 11px;
+ padding: 3px 8px;
+ cursor: pointer;
+}
+
+.xp-nav-icon {
+ width: 28px;
+ height: 24px;
+ padding: 0;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 12px;
+}
+
+.xp-nav-btn:disabled {
+ opacity: 0.55;
+ cursor: default;
+}
+
+.xp-breadcrumb {
+ background: #fff;
+ border: 1px solid #e5b4cf;
+ padding: 5px 8px;
+ font-size: 11px;
+ color: #7d2756;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.xp-explorer-body {
+ display: grid;
+ grid-template-columns: 170px 1fr;
+ gap: 8px;
+ flex: 1;
+ min-height: 0;
+}
+
+.xp-main {
+ display: grid;
+ grid-template-columns: 1fr 210px;
+ gap: 8px;
+ min-height: 0;
+}
+
+.xp-sidebar {
+ background: #fff0f9;
+ border: 1px solid #e7a8ca;
+ padding: 6px;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ min-height: 0;
+ overflow: auto;
+}
+
+.xp-side-title {
+ font-weight: 700;
+ color: #8c3b67;
+ margin-bottom: 4px;
+}
+
+.xp-side-link {
+ border: none;
+ background: transparent;
+ text-align: left;
+ color: #7d2756;
+ font-size: 11px;
+ padding: 4px;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.xp-side-link:hover {
+ background: #ffdff1;
+}
+
+.xp-files {
+ min-height: 0;
+ border: 1px solid #e7a8ca;
+ background: #fff;
+ padding: 6px;
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
+ grid-auto-rows: 96px;
+ align-content: start;
+ gap: 6px;
+ overflow: auto;
+}
+
+.xp-file-item {
+ border: 1px solid transparent;
+ background: transparent;
+ text-align: center;
+ color: #5f2244;
+ padding: 6px;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ gap: 4px;
+ cursor: default;
+ width: 92px;
+ height: 92px;
+ border-radius: 0;
+}
+
+.xp-file-item:hover {
+ border-color: #f0c3dc;
+ background: #fff4fb;
+}
+
+.xp-file-item.selected {
+ border-color: #d88db8;
+ background: #ffe8f5;
+}
+
+.xp-file-icon {
+ font-size: 26px;
+ line-height: 1;
+}
+
+.xp-file-label {
+ font-size: 11px;
+ word-break: break-word;
+ width: 100%;
+ text-align: center;
+}
+
+.xp-empty {
+ color: #8c3b67;
+ padding: 8px;
+ font-size: 11px;
+}
+
+.xp-details {
+ border: 1px solid #e7a8ca;
+ background: #fff0f9;
+ padding: 8px;
+ font-size: 11px;
+ color: #6e2b52;
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ min-height: 0;
+ overflow: auto;
+}
+
+.xp-details-title {
+ font-weight: 700;
+ color: #8c3b67;
+ margin-bottom: 4px;
+}
+
+.xp-notepad {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ height: 100%;
+}
+
+.xp-notepad-toolbar {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ border: 1px solid #e5b4cf;
+ background: #fff0f9;
+ padding: 5px;
+}
+
+.xp-note-btn {
+ border-top: 1px solid #fff;
+ border-left: 1px solid #fff;
+ border-right: 1px solid #9f4a78;
+ border-bottom: 1px solid #9f4a78;
+ background: linear-gradient(#fff3fb, #f2c0de);
+ color: #6f2750;
+ font-size: 11px;
+ padding: 3px 8px;
+ cursor: pointer;
+}
+
+.xp-note-file {
+ margin-left: auto;
+ font-size: 11px;
+ color: #7a3a61;
+}
+
+.xp-note-editor {
+ width: 100%;
+ min-height: 360px;
+ height: 100%;
+ resize: none;
+ border: 1px solid #d58cb8;
+ background: #fff;
+ color: #3d1530;
+ font-family: "Lucida Console", "Courier New", monospace;
+ font-size: 12px;
+ line-height: 1.35;
+ padding: 10px;
+}
+
+.xp-paint {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ height: 100%;
+}
+
+.xp-paint-toolbar {
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 8px;
+ border: 1px solid #e5b4cf;
+ background: #fff0f9;
+ padding: 5px;
+ font-size: 11px;
+ color: #7a3a61;
+}
+
+.xp-paint-toolbar label {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+}
+
+.xp-paint-tool,
+.xp-paint-size,
+.xp-paint-color {
+ font-size: 11px;
+}
+
+input[type="range"] {
+ accent-color: #d24b95;
+}
+
+.xp-paint-btn {
+ border-top: 1px solid #fff;
+ border-left: 1px solid #fff;
+ border-right: 1px solid #9f4a78;
+ border-bottom: 1px solid #9f4a78;
+ background: linear-gradient(#fff3fb, #f2c0de);
+ color: #6f2750;
+ font-size: 11px;
+ padding: 3px 8px;
+ cursor: pointer;
+}
+
+.xp-paint-stage {
+ border: 1px solid #d58cb8;
+ background: #fff;
+ overflow: auto;
+ flex: 1;
+ min-height: 160px;
+}
+
+.xp-paint-canvas {
+ display: block;
+ width: 100%;
+ height: 100%;
+ background: #fff;
+ cursor: crosshair;
+}
+
+.xp-ie {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ height: 100%;
+}
+
+.xp-ie-toolbar {
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 6px;
+ border: 1px solid #e5b4cf;
+ background: #fff0f9;
+ padding: 5px;
+}
+
+.xp-ie-btn {
+ border-top: 1px solid #fff;
+ border-left: 1px solid #fff;
+ border-right: 1px solid #9f4a78;
+ border-bottom: 1px solid #9f4a78;
+ background: linear-gradient(#fff3fb, #f2c0de);
+ color: #6f2750;
+ font-size: 11px;
+ padding: 3px 8px;
+ cursor: pointer;
+}
+
+.xp-ie-btn:disabled {
+ opacity: 0.55;
+ cursor: default;
+}
+
+.xp-ie-address-row {
+ display: grid;
+ grid-template-columns: auto 1fr auto;
+ gap: 6px;
+ align-items: center;
+ border: 1px solid #e5b4cf;
+ background: #fff0f9;
+ padding: 5px;
+ margin: 0;
+}
+
+.xp-ie-address-row span {
+ color: #7a3a61;
+ font-size: 11px;
+}
+
+.xp-ie-address {
+ border: 1px solid #d58cb8;
+ padding: 5px 7px;
+ font-size: 12px;
+ color: #5a1b3b;
+ background: #fff;
+}
+
+.xp-ie-content {
+ border: 1px solid #d58cb8;
+ background: #fff;
+ min-height: 220px;
+ padding: 12px;
+ overflow: auto;
+ flex: 1;
+}
+
+.ie-loading {
+ color: #8c3b67;
+}
+
+.ie-error h3 {
+ margin-top: 0;
+ color: #8d1f54;
+}
+
+.ie-page h2 {
+ margin-top: 0;
+ color: #8c3b67;
+}
+
+.ie-page a {
+ color: #b32972;
+}
+
+.ie-page a:hover {
+ color: #7f1f54;
+}
+
+.ie-results li {
+ margin-bottom: 10px;
+}
+
+.ie-results li div {
+ color: #8d5780;
+ font-size: 11px;
+}
+
+.xp-image-viewer {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ height: 100%;
+}
+
+.xp-image-toolbar {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ border: 1px solid #e5b4cf;
+ background: #fff0f9;
+ padding: 5px;
+}
+
+.xp-image-btn {
+ border-top: 1px solid #fff;
+ border-left: 1px solid #fff;
+ border-right: 1px solid #9f4a78;
+ border-bottom: 1px solid #9f4a78;
+ background: linear-gradient(#fff3fb, #f2c0de);
+ color: #6f2750;
+ font-size: 11px;
+ padding: 3px 8px;
+ cursor: pointer;
+}
+
+.xp-image-title {
+ margin-left: auto;
+ color: #7a3a61;
+ font-size: 11px;
+}
+
+.xp-image-stage {
+ border: 1px solid #d58cb8;
+ background: #fff;
+ flex: 1;
+ min-height: 220px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ overflow: auto;
+}
+
+.xp-image-content {
+ display: block;
+ max-width: 100%;
+ max-height: 100%;
+ image-rendering: auto;
+}
+
+.xp-audio-player {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ height: 100%;
+ background: linear-gradient(#f7d9ec, #e9acd0);
+ border: 1px solid #a25a85;
+ padding: 8px;
+}
+
+.xp-audio-top {
+ display: grid;
+ grid-template-columns: 44px 1fr;
+ gap: 8px;
+ align-items: center;
+}
+
+.xp-audio-logo {
+ width: 44px;
+ height: 44px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-weight: 700;
+ color: #fff;
+ background: linear-gradient(#bf3f8a, #7e1f54);
+ border: 1px solid #5f123d;
+}
+
+.xp-audio-track-wrap {
+ border: 1px solid #8f4a72;
+ background: #220b17;
+ color: #8dff9e;
+ font-family: "Lucida Console", "Courier New", monospace;
+ font-size: 11px;
+ overflow: hidden;
+ white-space: nowrap;
+}
+
+.xp-audio-track-marquee {
+ display: inline-block;
+ padding: 6px 8px;
+ min-width: 100%;
+ animation: xp-audio-scroll 12s linear infinite;
+}
+
+@keyframes xp-audio-scroll {
+ 0% { transform: translateX(100%); }
+ 100% { transform: translateX(-100%); }
+}
+
+.xp-audio-controls {
+ display: grid;
+ grid-template-columns: auto auto auto 1fr;
+ gap: 6px;
+ align-items: center;
+}
+
+.xp-audio-btn {
+ border-top: 1px solid #fff;
+ border-left: 1px solid #fff;
+ border-right: 1px solid #7b355d;
+ border-bottom: 1px solid #7b355d;
+ background: linear-gradient(#fff2fb, #efbfdc);
+ color: #6f2750;
+ font-size: 11px;
+ padding: 3px 8px;
+ cursor: pointer;
+}
+
+.xp-audio-seek {
+ width: 100%;
+}
+
+.xp-audio-time {
+ margin-top: auto;
+ border: 1px solid #8f4a72;
+ background: #fff6fc;
+ color: #6f2750;
+ font-family: "Lucida Console", "Courier New", monospace;
+ font-size: 11px;
+ padding: 4px 6px;
+ text-align: right;
+}
+
+#context-menu {
+ position: fixed;
+ z-index: 3000;
+ min-width: 170px;
+ background: #fff5fc;
+ border-top: 1px solid #fff;
+ border-left: 1px solid #fff;
+ border-right: 1px solid #8e3f6c;
+ border-bottom: 1px solid #8e3f6c;
+ box-shadow: 2px 2px 0 rgba(110, 41, 78, 0.35);
+ padding: 3px;
+}
+
+.context-menu-item {
+ width: 100%;
+ text-align: left;
+ border: none;
+ background: transparent;
+ color: #6f2750;
+ font-size: 11px;
+ padding: 6px 8px;
+ cursor: pointer;
+}
+
+.context-menu-item:hover:not(:disabled),
+.context-menu-item:focus-visible {
+ background: linear-gradient(var(--titlebar-1), var(--titlebar-2));
+ color: #fff;
+ outline: none;
+}
+
+.context-menu-item:disabled {
+ color: #b78aa4;
+ cursor: default;
+}
+
+.context-menu-sep {
+ height: 1px;
+ background: #e2a9cb;
+ margin: 3px 2px;
+}
+
+.xp-props {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ min-height: 100%;
+}
+
+.xp-props-title {
+ font-size: 13px;
+ font-weight: 700;
+ color: #7c2e5b;
+}
+
+.xp-props-body {
+ border: 1px solid #e7a8ca;
+ background: #fff;
+ padding: 8px;
+ font-size: 11px;
+ color: #5f2244;
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+}
+