Apply the Nocturne fidelity pass to the web UI

This commit is contained in:
Fisto
2026-09-21 19:31:29 +02:00
parent 267c31337c
commit a4d61781e9
10 changed files with 1336 additions and 100 deletions
+128 -27
View File
@@ -1,41 +1,126 @@
"use strict";
/* ─────────────────────────────────────────────────────────────────────────
Veskslop icons — Phosphor geometry.
Replaces the hand-drawn 16-unit set. Three things change:
1. Grid. Phosphor draws on 256 units; the old set drew on 16, so curves
were quantised to half-pixels and the mic, gear and camera all had
visibly different optical weights from each other.
2. Terminals. Round caps and joins throughout, as Phosphor regular.
3. Stroke. Held at a constant ~1.35 device pixels whatever the render
size, instead of a fixed 1.3 unit stroke that thickened as icons grew.
A 44px icon and a 13px icon now read as the same weight of line.
The API is unchanged — VK.icon(name, size) and VK.iconEl(name, size) —
and every previous name still resolves, so nothing needs editing to
adopt this. New names are listed at the bottom.
If you would rather ship the real Phosphor set, swap this file for
@phosphor-icons/web and keep VK.icon as a thin wrapper; the names below
are Phosphor's own, so the mapping is one to one.
───────────────────────────────────────────────────────────────────────── */
window.VK = window.VK ?? {};
const ICON_PATHS = {
hash: '<path d="M6 2L4.5 14M11.5 2L10 14M2.5 5.5h12M2 10.5h12"/>',
volume: '<path d="M8.5 2.5 4.5 6H2v4h2.5l4 3.5z"/><path d="M11 5.5a3.5 3.5 0 0 1 0 5"/>',
mic: '<rect x="5.5" y="1.5" width="5" height="7.5" rx="2.5"/><path d="M3 7.5a5 5 0 0 0 10 0M8 12.5v2"/>',
/* ── Channels and navigation ── */
hash: '<path d="M96 40 72 216M184 40l-24 176M52 96h152M44 160h152"/>',
users: '<circle cx="104" cy="96" r="44"/><path d="M24 200a88 88 0 0 1 160 0"/><path d="M160 54a44 44 0 0 1 0 84"/><path d="M232 200a88 88 0 0 0-40-38"/>',
user: '<circle cx="128" cy="92" r="48"/><path d="M36 204a96 96 0 0 1 184 0"/>',
chat: '<path d="M52 212l12-36A88 88 0 1 1 128 216a87 87 0 0 1-40-10z"/><path d="M96 120h64M96 152h40"/>',
activity: '<path d="M24 128h40l24-64 40 128 24-64h80"/>',
robot: '<rect x="28" y="72" width="200" height="136" rx="24"/><path d="M128 28v44"/><circle cx="128" cy="24" r="10"/><path d="M92 136v12M164 136v12"/><path d="M104 176h48"/>',
/* ── Voice ── */
mic: '<rect x="96" y="24" width="64" height="112" rx="32"/><path d="M196 124a68 68 0 0 1-136 0"/><path d="M128 192v40M92 232h72"/>',
micSlash:
'<rect x="5.5" y="1.5" width="5" height="7.5" rx="2.5"/><path d="M3 7.5a5 5 0 0 0 10 0M8 12.5v2"/><path d="M2.5 13.5 13.5 2.5"/>',
speaker: '<path d="M3 10V8a5 5 0 0 1 10 0v2"/><rect x="1.5" y="9.5" width="3" height="4.5" rx="1.5"/><rect x="11.5" y="9.5" width="3" height="4.5" rx="1.5"/>',
speakerSlash:
'<path d="M3 10V8a5 5 0 0 1 10 0v2"/><rect x="1.5" y="9.5" width="3" height="4.5" rx="1.5"/><rect x="11.5" y="9.5" width="3" height="4.5" rx="1.5"/><path d="M2 14 14 2"/>',
monitor: '<rect x="1.5" y="2.5" width="13" height="9" rx="1.5"/><path d="M5.5 14h5M8 11.5v2.5"/>',
camera: '<rect x="1.5" y="4.5" width="9" height="7" rx="1.5"/><path d="M10.5 8l4-2.2v4.4z"/>',
caretDown: '<path d="M3.5 6l4.5 4.5L12.5 6"/>',
caretUp: '<path d="M3.5 10L8 5.5l4.5 4.5"/>',
x: '<path d="M3.5 3.5l9 9M12.5 3.5l-9 9"/>',
lock: '<rect x="3.5" y="7" width="9" height="7" rx="1.5"/><path d="M5.5 7V5a2.5 2.5 0 0 1 5 0v2"/>',
search: '<circle cx="7" cy="7" r="4.5"/><path d="M10.5 10.5 14 14"/>',
gear: '<circle cx="8" cy="8" r="2.2"/><path d="M8 1.8v2M8 12.2v2M1.8 8h2M12.2 8h2M3.6 3.6l1.4 1.4M11 11l1.4 1.4M12.4 3.6 11 5M5 11l-1.4 1.4"/>',
wave: '<path d="M2 6.5V4.5h6v2M5 4.5V12"/><path d="M10.5 5.5a3.2 3.2 0 0 1 0 5"/><path d="M12.8 3.5a6 6 0 0 1 0 9"/>',
warning: '<path d="M8 2 1.5 13.5h13z"/><path d="M8 6.5v3.5M8 12v.5"/>',
refresh: '<path d="M13.5 8a5.5 5.5 0 1 1-1.6-3.9"/><path d="M13.5 2.5V6H10"/>',
play: '<path d="M5 3.5 12.5 8 5 12.5z"/>',
microphone: '<rect x="5.5" y="1.5" width="5" height="7.5" rx="2.5"/><path d="M3 7.5a5 5 0 0 0 10 0M8 12.5v2"/>',
users: '<circle cx="6" cy="5.5" r="2.5"/><path d="M1.5 13.5c0-2.5 2-4 4.5-4s4.5 1.5 4.5 4"/><path d="M11 3.5a2.5 2.5 0 0 1 0 5"/>',
activity: '<path d="M1.5 8h3l2-4.5 2.5 9L11 8h3.5"/>'
'<rect x="96" y="24" width="64" height="112" rx="32"/><path d="M196 124a68 68 0 0 1-136 0"/><path d="M128 192v40M92 232h72"/><path d="M44 36l168 184"/>',
volume: '<path d="M152 36 88 88H40v80h48l64 52z"/><path d="M188 96a44 44 0 0 1 0 64"/>',
speaker: '<path d="M152 36 88 88H40v80h48l64 52z"/><path d="M188 96a44 44 0 0 1 0 64"/><path d="M212 68a80 80 0 0 1 0 120"/>',
speakerSlash: '<path d="M152 36 88 88H40v80h48l64 52z"/><path d="M188 100l44 56M232 100l-44 56"/>',
headphones: '<path d="M44 176v-48a84 84 0 0 1 168 0v48"/><rect x="20" y="152" width="48" height="68" rx="20"/><rect x="188" y="152" width="48" height="68" rx="20"/>',
headphonesSlash:
'<path d="M44 176v-48a84 84 0 0 1 168 0v48"/><rect x="20" y="152" width="48" height="68" rx="20"/><rect x="188" y="152" width="48" height="68" rx="20"/><path d="M44 36l168 184"/>',
waveform: '<path d="M32 108v40M72 76v104M112 40v176M152 76v104M192 100v56M232 116v24"/>',
wave: '<path d="M32 92V64h96v28M80 64v128M52 192h56"/><path d="M168 96a44 44 0 0 1 0 64"/><path d="M200 70a80 80 0 0 1 0 116"/>',
/* ── Media ── */
monitor: '<rect x="24" y="48" width="208" height="136" rx="20"/><path d="M96 216h64M128 184v32"/>',
screenshare: '<rect x="24" y="48" width="208" height="136" rx="20"/><path d="M96 216h64"/><path d="M128 76v72M100 104l28-28 28 28"/>',
camera: '<rect x="24" y="64" width="144" height="128" rx="20"/><path d="M168 112l64-36v104l-64-36z"/>',
image: '<rect x="32" y="48" width="192" height="160" rx="20"/><circle cx="96" cy="100" r="16"/><path d="M32 172l56-48 48 40 32-24 56 48"/>',
film: '<rect x="32" y="48" width="192" height="160" rx="20"/><path d="M88 48v160M168 48v160M32 128h192M32 88h56M32 168h56M168 88h56M168 168h56"/>',
audio: '<circle cx="76" cy="188" r="32"/><circle cx="196" cy="164" r="28"/><path d="M108 188V72l116-28v120"/>',
folder: '<path d="M216 208H40a12 12 0 0 1-12-12V60a12 12 0 0 1 12-12h56l28 36h92a12 12 0 0 1 12 12v100a12 12 0 0 1-12 12z"/>',
play: '<path d="M76 40l148 88-148 88z"/>',
pause: '<rect x="76" y="48" width="40" height="160" rx="10"/><rect x="140" y="48" width="40" height="160" rx="10"/>',
stop: '<rect x="56" y="56" width="144" height="144" rx="20"/>',
/* ── Controls ── */
caretDown: '<path d="M208 96l-80 80-80-80"/>',
caretUp: '<path d="M48 160l80-80 80 80"/>',
caretRight: '<path d="M96 48l80 80-80 80"/>',
caretLeft: '<path d="M160 208 80 128l80-80"/>',
x: '<path d="M200 56 56 200M200 200 56 56"/>',
check: '<path d="M216 72 104 184l-56-56"/>',
plus: '<path d="M40 128h176M128 40v176"/>',
minus: '<path d="M40 128h176"/>',
dots: '<circle cx="128" cy="128" r="14"/><circle cx="60" cy="128" r="14"/><circle cx="196" cy="128" r="14"/>',
search: '<circle cx="116" cy="116" r="76"/><path d="M170 170l54 54"/>',
funnel: '<path d="M32 56h192l-76 88v72l-40-24v-48z"/>',
gear: '<circle cx="128" cy="128" r="40"/><path d="M128 20l26 22 34-4 13 32 32 13-4 34 22 27-22 27 4 34-32 13-13 32-34-4-26 22-26-22-34 4-13-32-32-13 4-34-22-27 22-27-4-34 32-13 13-32 34 4z"/>',
sliders: '<path d="M40 88h72M156 88h60M40 168h28M112 168h104"/><circle cx="134" cy="88" r="22"/><circle cx="90" cy="168" r="22"/>',
refresh: '<path d="M176 92h44V48"/><path d="M220 92A100 100 0 0 0 44 78"/><path d="M80 164H36v44"/><path d="M36 164a100 100 0 0 0 176 14"/>',
download: '<path d="M128 32v128M80 112l48 48 48-48M40 208h176"/>',
copy: '<rect x="80" y="80" width="144" height="144" rx="16"/><path d="M176 80V48a16 16 0 0 0-16-16H48a16 16 0 0 0-16 16v112a16 16 0 0 0 16 16h32"/>',
pencil: '<path d="M92 216H48a8 8 0 0 1-8-8v-44L164 40l52 52z"/><path d="M136 68l52 52"/>',
trash: '<path d="M36 64h184M96 64V40h64v24M60 64v148a16 16 0 0 0 16 16h104a16 16 0 0 0 16-16V64M108 108v72M148 108v72"/>',
pin: '<path d="M100 28h56l-8 60 40 44H68l40-44z"/><path d="M128 132v96"/>',
popout: '<path d="M144 40h72v72"/><path d="M128 128 216 40"/><path d="M188 152v56a12 12 0 0 1-12 12H48a12 12 0 0 1-12-12V80a12 12 0 0 1 12-12h56"/>',
lock: '<rect x="40" y="88" width="176" height="128" rx="20"/><path d="M88 88V56a40 40 0 0 1 80 0v32"/>',
star: '<path d="M128 24l30 62 68 10-49 48 12 68-61-32-61 32 12-68-49-48 68-10z"/>',
/* ── Status ── */
info: '<circle cx="128" cy="128" r="96"/><path d="M120 124h12v56M104 180h48"/><circle cx="126" cy="86" r="10"/>',
warning: '<path d="M110 40 22 190a20 20 0 0 0 18 30h176a20 20 0 0 0 18-30L146 40a20 20 0 0 0-36 0z"/><path d="M128 104v40"/><circle cx="128" cy="180" r="10"/>',
error: '<circle cx="128" cy="128" r="96"/><path d="M160 96l-64 64M96 96l64 64"/>',
ok: '<circle cx="128" cy="128" r="96"/><path d="M172 100l-56 56-32-32"/>',
clock: '<circle cx="128" cy="128" r="96"/><path d="M128 72v56h48"/>'
};
const FILLED = new Set(["star"]);
/* Aliases — every name the app used before this file landed. */
ICON_PATHS.microphone = ICON_PATHS.mic;
ICON_PATHS.deafen = ICON_PATHS.headphonesSlash;
ICON_PATHS.listen = ICON_PATHS.headphones;
ICON_PATHS.video = ICON_PATHS.camera;
ICON_PATHS.close = ICON_PATHS.x;
ICON_PATHS.settings = ICON_PATHS.gear;
ICON_PATHS.tts = ICON_PATHS.wave;
ICON_PATHS.magnify = ICON_PATHS.search;
ICON_PATHS.star = '<path d="M8 1.6l1.9 4 4.4.6-3.2 3 .8 4.3L8 11.5 4.1 13.5l.8-4.3-3.2-3 4.4-.6z"/>';
const FILLED = new Set(["star", "play", "dots"]);
/* Solid marks keep a hairline of their own so they hold shape at 10-11px. */
const HAIRLINE = new Set(["dots"]);
window.VK.icon = function icon(name, size = 13) {
const path = ICON_PATHS[name] ?? "";
const fill = FILLED.has(name) ? "currentColor" : "none";
return `<svg width="${size}" height="${size}" viewBox="0 0 16 16" fill="${fill}" stroke="currentColor" stroke-width="1.3" aria-hidden="true">${path}</svg>`;
const path = ICON_PATHS[name];
if (!path) {
if (name) console.warn("VK.icon: unknown icon", name);
return "";
}
const filled = FILLED.has(name);
/* Constant optical weight: ~1.35 device px of stroke at any render size. */
const sw = Math.min(30, Math.max(12, +((1.35 / size) * 256).toFixed(1)));
return (
`<svg width="${size}" height="${size}" viewBox="0 0 256 256" ` +
`fill="${filled ? "currentColor" : "none"}" ` +
`stroke="${filled && !HAIRLINE.has(name) ? "none" : "currentColor"}" ` +
`stroke-width="${sw}" stroke-linecap="round" stroke-linejoin="round" ` +
`aria-hidden="true" focusable="false">${path}</svg>`
);
};
window.VK.iconEl = function iconEl(name, size = 13) {
@@ -44,3 +129,19 @@ window.VK.iconEl = function iconEl(name, size = 13) {
span.innerHTML = window.VK.icon(name, size);
return span;
};
/* Fills any empty [data-icon] span. Safe to call repeatedly — it skips
anything already rendered, so it will not fight whatever app.js does. */
window.VK.hydrateIcons = function hydrateIcons(root = document) {
root.querySelectorAll("[data-icon]").forEach((el) => {
if (el.firstElementChild) return;
const size = Number(el.dataset.iconSize) || 13;
el.innerHTML = window.VK.icon(el.dataset.icon, size);
});
};
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => window.VK.hydrateIcons());
} else {
window.VK.hydrateIcons();
}
+4
View File
@@ -6,8 +6,11 @@
<title>veskslop debug</title>
<link rel="icon" href="data:," />
<link rel="stylesheet" href="nocturne.css" />
<link rel="stylesheet" href="tokens-status.css" />
<link rel="stylesheet" href="style.css" />
<link rel="stylesheet" href="media.css" />
<link rel="stylesheet" href="style-overrides.css" />
<link rel="stylesheet" href="settings-popups.css" />
</head>
<body>
<div class="vk-app">
@@ -622,6 +625,7 @@
<div id="toasts" class="vk-toasts"></div>
<script src="icons.js"></script>
<script src="polish.js"></script>
<script src="api.js"></script>
<script src="app.js"></script>
<script src="media.js"></script>
+134 -73
View File
@@ -6,7 +6,7 @@
right: 0;
top: var(--media-top, 50px);
bottom: 0;
/* Below the settings slide-over (80); media modals stay above it. */
/* Chrome 40 · media 70 · settings 1000 · settings dialogs 1050 · toasts 1100 */
z-index: 70;
display: flex;
background: var(--color-bg);
@@ -38,7 +38,7 @@
.vk-media-title {
flex: 1;
font-weight: 600;
font-weight: 500;
}
.vk-media-toolbar {
@@ -71,7 +71,7 @@
gap: 6px;
aspect-ratio: 1;
overflow: hidden;
border-radius: var(--radius-sm, 6px);
border-radius: var(--radius-sm);
background: var(--color-neutral-900);
box-shadow: inset 0 0 0 1px var(--color-neutral-800);
color: var(--color-neutral-300);
@@ -99,7 +99,7 @@
justify-content: center;
aspect-ratio: 1;
overflow: hidden;
border-radius: var(--radius-sm, 6px);
border-radius: var(--radius-sm);
background: var(--color-neutral-900);
box-shadow: inset 0 0 0 1px var(--color-neutral-800);
cursor: pointer;
@@ -124,10 +124,10 @@
position: absolute;
top: 4px;
left: 6px;
font: 600 10px/1 ui-monospace, Menlo, monospace;
font: 500 10px/1 ui-monospace, Menlo, monospace;
letter-spacing: 0.08em;
color: rgba(255, 255, 255, 0.75);
text-shadow: 0 0 3px rgba(0, 0, 0, 0.8);
color: var(--color-neutral-100);
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.85);
pointer-events: none;
}
@@ -137,8 +137,8 @@
right: 6px;
font-size: 20px;
line-height: 1;
color: rgba(255, 255, 255, 0.72);
text-shadow: 0 0 3px rgba(0, 0, 0, 0.8);
color: var(--color-neutral-100);
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.85);
cursor: pointer;
}
@@ -149,7 +149,7 @@
max-height: 46vh;
overflow-y: auto;
padding: 10px 12px;
border-radius: var(--radius-sm, 6px);
border-radius: var(--radius-sm);
background: var(--color-neutral-900);
box-shadow: inset 0 0 0 1px var(--color-neutral-800);
}
@@ -212,7 +212,7 @@
flex-direction: column;
gap: 4px;
padding: 8px 10px;
border-radius: var(--radius-sm, 6px);
border-radius: var(--radius-sm);
background: var(--color-bg);
box-shadow: inset 0 0 0 1px var(--color-neutral-800);
}
@@ -258,14 +258,14 @@
.vk-media-viewer-media video {
max-width: 100%;
max-height: 100%;
border-radius: var(--radius-sm, 6px);
border-radius: var(--radius-sm);
}
.vk-media-viewer-media iframe {
width: min(880px, 100%);
aspect-ratio: 16 / 9;
border: 0;
border-radius: var(--radius-sm, 6px);
border-radius: var(--radius-sm);
}
.vk-media-viewer.vertical .vk-media-viewer-media {
@@ -297,7 +297,7 @@
padding: 0;
border: none;
background: none;
color: var(--color-accent-400, #7aa2ff);
color: var(--color-accent-300);
font: inherit;
text-align: left;
cursor: pointer;
@@ -310,9 +310,9 @@
}
.vk-media-card-kind {
font: 600 10px/1 ui-monospace, Menlo, monospace;
font: 500 10px/1 ui-monospace, Menlo, monospace;
letter-spacing: 0.08em;
color: var(--color-accent-400, var(--color-neutral-300));
color: var(--color-accent-300);
}
.vk-media-card-name {
@@ -354,7 +354,7 @@
border-radius: 50%;
background: var(--color-neutral-800);
color: var(--color-neutral-300);
font: 600 9px/1 ui-monospace, Menlo, monospace;
font: 500 9px/1 ui-monospace, Menlo, monospace;
cursor: help;
}
@@ -362,7 +362,7 @@
width: 90px;
padding: 3px 6px;
border: none;
border-radius: var(--radius-sm, 6px);
border-radius: var(--radius-sm);
background: var(--color-neutral-900);
color: var(--color-neutral-200);
box-shadow: inset 0 0 0 1px var(--color-neutral-800);
@@ -384,23 +384,23 @@
}
.vk-seg-image {
background: #4f8cff;
background: var(--color-data-1);
}
.vk-seg-gif {
background: #8a6cff;
background: var(--color-data-2);
}
.vk-seg-video {
background: #ff7a45;
background: var(--color-data-3);
}
.vk-seg-audio {
background: #2fbf71;
background: var(--color-data-4);
}
.vk-seg-other {
background: #6b7280;
background: var(--color-data-5);
}
.vk-media-modal {
@@ -424,7 +424,7 @@
gap: 10px;
width: min(520px, 92vw);
padding: 14px;
border-radius: var(--radius-lg, 12px);
border-radius: var(--radius-lg);
background: var(--color-bg);
box-shadow: inset 0 0 0 1px var(--color-neutral-800);
}
@@ -465,7 +465,8 @@
.vk-media-tree-head {
padding: 6px 8px;
border: none;
border-radius: var(--radius-sm, 6px);
border-radius: var(--radius-sm);
cursor: pointer;
background: var(--color-neutral-900);
color: var(--color-neutral-200);
font: 500 12px/1.2 inherit;
@@ -510,7 +511,7 @@
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--color-accent-400, #4f8cff);
background: var(--color-accent-400);
animation: vk-media-pulse 1.2s ease-in-out infinite;
}
@@ -531,21 +532,22 @@
}
.vk-media-error {
color: #ff8080;
color: var(--color-danger-300);
}
/* Tinted, not flooded — and 10px so the count is actually readable. */
.vk-media-badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 14px;
height: 14px;
min-width: 16px;
height: 16px;
margin-left: 4px;
padding: 0 4px;
border-radius: 7px;
background: #e5484d;
color: #fff;
font: 600 9px/1 ui-monospace, Menlo, monospace;
border-radius: 8px;
background: var(--color-danger-800);
color: var(--color-danger-200);
font: 500 10px/1 ui-monospace, Menlo, monospace;
}
/* Bot tab: backfill jobs, stats, and settings. */
@@ -565,11 +567,11 @@
.vk-bot-limited {
padding: 8px 10px;
border-radius: 6px;
font: 500 11px/1.4 ui-monospace, Menlo, monospace;
color: #ffb0b0;
background: color-mix(in srgb, #ff5555 14%, transparent);
border: 1px solid color-mix(in srgb, #ff5555 35%, transparent);
border-radius: var(--radius-sm);
font: 400 11px/1.4 ui-monospace, Menlo, monospace;
color: var(--color-danger-200);
background: var(--color-danger-900);
box-shadow: inset 0 0 0 1px var(--color-danger-700);
}
.vk-bot-limited[hidden] {
@@ -577,8 +579,8 @@
}
.vk-bot-section {
border: 1px solid var(--color-divider);
border-radius: 6px;
box-shadow: inset 0 0 0 1px var(--color-neutral-800);
border-radius: var(--radius-md);
padding: 8px 10px;
background: var(--color-surface);
}
@@ -592,7 +594,7 @@
}
.vk-bot-title {
font-weight: 600;
font-weight: 500;
font-size: 12px;
}
@@ -625,8 +627,8 @@
}
.vk-bot-job {
border: 1px solid var(--color-neutral-800);
border-radius: 5px;
box-shadow: inset 0 0 0 1px var(--color-neutral-800);
border-radius: var(--radius-sm);
padding: 6px 8px;
display: flex;
flex-direction: column;
@@ -634,7 +636,7 @@
}
.vk-bot-job-running {
border-color: color-mix(in srgb, var(--color-accent, #4f8cff) 45%, transparent);
box-shadow: inset 0 0 0 1px var(--color-accent-700);
}
.vk-bot-job-head {
@@ -646,7 +648,7 @@
.vk-bot-job-title {
flex: 1;
font-weight: 600;
font-weight: 500;
font-size: 11px;
}
@@ -656,27 +658,28 @@
}
.vk-bot-state {
font: 600 10px/1.4 ui-monospace, Menlo, monospace;
font: 500 10px/1.4 ui-monospace, Menlo, monospace;
letter-spacing: 0.06em;
text-transform: uppercase;
padding: 0 5px;
border-radius: 3px;
padding: 1px 6px;
border-radius: var(--radius-sm);
background: var(--color-neutral-800);
color: var(--color-neutral-300);
}
.vk-bot-state-running {
background: color-mix(in srgb, var(--color-accent, #4f8cff) 25%, transparent);
color: var(--color-accent-300, #9fc1ff);
background: var(--color-accent-900);
color: var(--color-accent-200);
}
.vk-bot-state-paused {
background: color-mix(in srgb, #ffaa33 20%, transparent);
color: #ffc880;
background: var(--color-warn-900);
color: var(--color-warn-200);
}
.vk-bot-state-done {
background: color-mix(in srgb, #44cc88 20%, transparent);
color: #8fe0b8;
background: var(--color-ok-900);
color: var(--color-ok-200);
}
.vk-bot-job-member {
@@ -687,14 +690,14 @@
.vk-bot-progress {
height: 4px;
border-radius: 2px;
border-radius: var(--radius-sm);
background: var(--color-neutral-800);
overflow: hidden;
}
.vk-bot-progress-fill {
height: 100%;
background: var(--color-accent-400, #4f8cff);
background: var(--color-accent-400);
}
.vk-bot-job-actions {
@@ -710,7 +713,7 @@
.vk-bot-group-title {
cursor: pointer;
font: 600 11px/1.6 ui-monospace, Menlo, monospace;
font: 500 11px/1.6 ui-monospace, Menlo, monospace;
color: var(--color-neutral-300);
}
@@ -739,11 +742,11 @@
}
.vk-media-limited {
color: #ff8080;
color: var(--color-danger-300);
}
.vk-media-done-note {
color: color-mix(in srgb, var(--color-neutral-400) 55%, transparent);
color: var(--color-neutral-500);
font-style: italic;
}
@@ -754,7 +757,7 @@
.vk-bot-eta {
cursor: help;
text-decoration: underline dotted color-mix(in srgb, var(--color-neutral-400) 60%, transparent);
text-decoration: underline dotted var(--color-neutral-600);
}
/* Stream buffer: scrollable room while the next page loads. */
@@ -778,7 +781,7 @@
/* Advanced drawer: the "Do not include" group. */
.vk-media-exclude-title {
cursor: pointer;
font: 600 11px/1.6 ui-monospace, Menlo, monospace;
font: 500 11px/1.6 ui-monospace, Menlo, monospace;
color: var(--color-neutral-300);
}
@@ -800,8 +803,8 @@
/* The YouTube title catch-up lane, inside the Bot tab backfill section. */
.vk-bot-lane {
border: 1px dashed var(--color-neutral-700);
border-radius: 5px;
box-shadow: inset 0 0 0 1px var(--color-neutral-800);
border-radius: var(--radius-sm);
padding: 6px 8px;
margin-bottom: 8px;
display: flex;
@@ -814,15 +817,15 @@
}
.vk-bot-progress-cached {
background: var(--color-accent-400, #4f8cff);
background: var(--color-accent-400);
}
.vk-bot-progress-pending {
background: var(--color-neutral-700);
background: var(--color-neutral-800);
}
.vk-bot-progress-failed {
background: #c04040;
background: var(--color-danger-700);
}
.vk-bot-history {
@@ -830,20 +833,20 @@
}
.vk-bot-history-title {
font: 600 11px/1.6 ui-monospace, Menlo, monospace;
font: 500 11px/1.6 ui-monospace, Menlo, monospace;
color: var(--color-neutral-300);
margin-bottom: 2px;
}
.vk-bot-history-entry {
font: 400 10px/1.5 ui-monospace, Menlo, monospace;
color: var(--color-neutral-500);
color: var(--color-neutral-400);
}
.vk-bot-hint {
margin-top: 8px;
font: 400 10px/1.5 ui-monospace, Menlo, monospace;
color: var(--color-neutral-500);
color: var(--color-neutral-400);
}
/* Infinite/page toggle and pager. */
@@ -856,7 +859,7 @@
.vk-media-toggle {
display: inline-flex;
border: 1px solid var(--color-divider);
border-radius: 5px;
border-radius: var(--radius-md);
overflow: hidden;
}
@@ -870,8 +873,14 @@
}
.vk-toggle-half.vk-toggle-on {
background: var(--color-accent-400, #4f8cff);
color: #fff;
background: var(--color-accent-900);
color: var(--color-accent-200);
box-shadow: inset 0 0 0 1px var(--color-accent-700);
}
.vk-toggle-half:focus-visible {
outline: 2px solid var(--color-accent);
outline-offset: -2px;
}
.vk-media-page-nav {
@@ -893,7 +902,59 @@
}
.vk-media-viewer-title {
font: 600 13px/1.5 ui-monospace, Menlo, monospace;
font: 500 13px/1.5 ui-monospace, Menlo, monospace;
color: var(--color-text);
margin-bottom: 6px;
}
/* ── Focus: these controls are bare <button>s with background and border
reset, so they need the accent ring pinned back on. ───────────────────── */
.vk-media-link:focus-visible,
.vk-media-tree-head:focus-visible,
.vk-media-play:focus-visible,
.vk-media-media:focus-visible,
.vk-bot-group-title:focus-visible,
.vk-media-exclude-title:focus-visible,
.vk-media-advanced summary:focus-visible {
outline: 2px solid var(--color-accent);
outline-offset: 2px;
border-radius: var(--radius-sm);
}
/* ── Storage bar legend: hue is never the only channel. Render one of these
per segment beside the bar so the categories survive a colour-blind
reader, a projector, and a screenshot. ────────────────────────────────── */
.vk-media-usage-legend {
display: flex;
flex-wrap: wrap;
gap: 4px 12px;
margin-top: 6px;
}
.vk-media-usage-key {
display: inline-flex;
align-items: center;
gap: 6px;
font: 400 10px/1.4 ui-monospace, Menlo, monospace;
color: var(--color-neutral-400);
}
.vk-media-usage-key i {
width: 8px;
height: 8px;
flex: none;
border-radius: 2px;
background: var(--color-data-5);
}
.vk-media-usage-key.image i { background: var(--color-data-1); }
.vk-media-usage-key.gif i { background: var(--color-data-2); }
.vk-media-usage-key.video i { background: var(--color-data-3); }
.vk-media-usage-key.audio i { background: var(--color-data-4); }
/* Over the warn threshold the bar itself should say so. */
.vk-media-usage.over {
box-shadow: inset 0 0 0 1px var(--color-warn-700);
}
+76
View File
@@ -0,0 +1,76 @@
"use strict";
/* ─────────────────────────────────────────────────────────────────────────
Veskslop UI polish — progressive enhancement, no markup edits.
Three things CSS cannot reach:
· close buttons that are the letter "X"
· the play control that is a ▶ character at 44px
· help text trapped in a native title tooltip
Each fix is idempotent and runs again on DOM changes, so it survives the
Media and Bot tabs rebuilding their trees. Load AFTER icons.js.
───────────────────────────────────────────────────────────────────────── */
(function () {
const VK = window.VK;
if (!VK || !VK.icon) return;
const GLYPH_CLOSE = /^[xX✕✖×]$/;
const GLYPH_PLAY = /^[▶►▸]$/;
function polish(root) {
if (!root || root.nodeType !== 1) return;
/* Text-glyph close buttons → the x icon, with a real label. */
root.querySelectorAll("button, .vk-chip").forEach((el) => {
const text = (el.textContent || "").trim();
if (GLYPH_CLOSE.test(text)) {
el.innerHTML = VK.icon("x", 13);
el.classList.add("vk-icon-btn");
if (!el.getAttribute("aria-label")) el.setAttribute("aria-label", "Close");
} else if (GLYPH_PLAY.test(text)) {
el.innerHTML = VK.icon("play", 22);
if (!el.getAttribute("aria-label")) el.setAttribute("aria-label", "Play");
}
});
/* .vk-media-play is a ▶ character styled at 44px. */
root.querySelectorAll(".vk-media-play").forEach((el) => {
if (el.querySelector("svg")) return;
el.innerHTML = VK.icon("play", 22);
if (!el.getAttribute("aria-label")) el.setAttribute("aria-label", "Play");
});
/* Help marks: move title → data-tip so the styled tooltip is the only
one that fires, and keep the text reachable for screen readers. */
root.querySelectorAll(".vk-info").forEach((el) => {
const tip = el.getAttribute("title");
if (tip) {
el.dataset.tip = tip;
el.removeAttribute("title");
el.setAttribute("aria-label", tip);
}
if (!el.textContent.trim()) el.textContent = "i";
if (!el.hasAttribute("tabindex")) el.tabIndex = 0;
el.setAttribute("role", "note");
});
if (VK.hydrateIcons) VK.hydrateIcons(root);
}
function run() {
polish(document.body);
new MutationObserver((records) => {
for (const r of records) {
r.addedNodes.forEach((n) => polish(n));
}
}).observe(document.body, { childList: true, subtree: true });
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", run);
} else {
run();
}
})();
+476
View File
@@ -0,0 +1,476 @@
/* ─────────────────────────────────────────────────────────────────────────
Veskslop — settings, menus, dialogs and toasts.
The shell reads as Nocturne; these surfaces did not. They were built from
flat 1px rules, bare native controls and text glyphs, so opening Settings
or a scrape dialog felt like leaving the app. Nothing here changes
behaviour or markup — it is presentation only.
Load LAST: nocturne.css · tokens-status.css · style.css · media.css ·
style-overrides.css · settings-popups.css
───────────────────────────────────────────────────────────────────────── */
/* ── 1. The scrim ─────────────────────────────────────────────────────────
A flat 55% wash left the page behind perfectly legible, so the eye kept
reading it. Deepen it, blur what is behind, and fade it in. */
.vk-overlay {
background: color-mix(in srgb, var(--color-bg) 72%, transparent);
backdrop-filter: blur(3px) saturate(0.9);
animation: vk-scrim-in 140ms ease-out;
}
@keyframes vk-scrim-in {
from {
opacity: 0;
}
}
/* ── 2. Settings panel ────────────────────────────────────────────────────
A slide-over with a square left edge butting into a blurred page reads as
a torn sheet. Round the leading corners, give it the system's hairline,
and let it arrive from the edge it is attached to. */
.vk-settings-panel {
width: min(720px, 100%);
border-radius: var(--radius-lg) 0 0 var(--radius-lg);
box-shadow: var(--shadow-lg), inset 1px 0 0 var(--color-neutral-800);
animation: vk-panel-in 200ms cubic-bezier(0.2, 0.7, 0.3, 1);
}
@keyframes vk-panel-in {
from {
transform: translateX(18px);
opacity: 0;
}
}
/* Head: sticky, so the title survives a long scroll, and on the system's
fading rule rather than a flat one. */
.vk-settings-head {
position: sticky;
top: 0;
z-index: 2;
padding: 13px 16px;
background: var(--color-surface);
box-shadow: none;
background-image: linear-gradient(
to right,
transparent,
var(--color-neutral-800) 40px,
var(--color-neutral-800) calc(100% - 40px),
transparent
);
background-repeat: no-repeat;
background-position: bottom;
background-size: 100% 1px;
}
.vk-settings-title {
letter-spacing: -0.01em;
}
.vk-settings-body {
gap: 26px;
padding: 18px 16px 28px;
scrollbar-width: thin;
scrollbar-color: var(--color-neutral-800) transparent;
}
.vk-settings-body::-webkit-scrollbar {
width: 10px;
}
.vk-settings-body::-webkit-scrollbar-thumb {
border: 3px solid transparent;
border-radius: 6px;
background: var(--color-neutral-800) padding-box;
}
.vk-settings-body::-webkit-scrollbar-thumb:hover {
background: var(--color-neutral-700) padding-box;
}
/* ── 3. Section heads ─────────────────────────────────────────────────────
Label, breathing room, then the actions pinned right on the same
baseline — previously the buttons crowded the label. */
.vk-section-head {
align-items: center;
gap: 12px;
padding: 0 2px 9px;
}
.vk-settings-actions {
gap: 6px;
}
/* ── 4. Cards and rows ────────────────────────────────────────────────────
The card was a flat fill; rows ran into each other. Give the card the
system edge and separate rows with fading rules, inset from both ends. */
.vk-settings-card {
padding: 4px 12px;
box-shadow: inset 0 0 0 1px var(--color-neutral-800);
}
.vk-status-row {
align-items: center;
gap: 16px;
padding: 9px 0;
box-shadow: none;
background: linear-gradient(
to right,
transparent,
var(--color-neutral-800) 16px,
var(--color-neutral-800) calc(100% - 16px),
transparent
)
no-repeat bottom / 100% 1px;
}
.vk-status-row:last-child,
.vk-settings-card > .vk-status-row:last-of-type {
background: none;
}
.vk-status-row .vk-status-label {
display: flex;
align-items: center;
gap: 6px;
color: var(--color-neutral-400);
}
.vk-status-row .vk-status-value {
display: flex;
align-items: center;
gap: 8px;
justify-content: flex-end;
font-variant-numeric: tabular-nums;
}
/* ── 5. The info affordance ───────────────────────────────────────────────
.vk-info was an unstyled letter "i" sitting in the text, and its help
text only existed as a native browser tooltip — half a second of delay,
no styling, and invisible on touch. Make it a real mark with a themed
tooltip. polish.js moves title → data-tip so the native one stops
double-firing; the title fallback below keeps it working without JS. */
.vk-info {
display: inline-grid;
place-items: center;
width: 14px;
height: 14px;
flex: none;
border-radius: 50%;
box-shadow: inset 0 0 0 1px var(--color-neutral-700);
font: 500 9px/1 var(--font-heading);
color: var(--color-neutral-400);
cursor: help;
position: relative;
transition: color 120ms, box-shadow 120ms;
}
.vk-info:hover,
.vk-info:focus-visible {
color: var(--color-accent-300);
box-shadow: inset 0 0 0 1px var(--color-accent-700);
}
.vk-info::after {
content: attr(data-tip);
position: absolute;
bottom: calc(100% + 8px);
left: 50%;
translate: -50% 0;
z-index: 30;
width: max-content;
max-width: 280px;
padding: 8px 10px;
border-radius: var(--radius-md);
background: var(--color-neutral-900);
box-shadow: var(--shadow-md), inset 0 0 0 1px var(--color-neutral-800);
font: 400 11.5px/1.5 var(--font-body);
letter-spacing: 0;
text-transform: none;
text-align: left;
color: var(--color-neutral-200);
opacity: 0;
pointer-events: none;
transition: opacity 120ms;
}
.vk-info:not([data-tip])::after {
content: attr(title);
}
.vk-info:hover::after,
.vk-info:focus-visible::after {
opacity: 1;
}
/* ── 6. Collapsible groups ────────────────────────────────────────────────
The summary was a bare line of uppercase text; nothing said it was a
control. Give it a row of its own, a hover tint and a caret that turns. */
.vk-settings-summary {
padding: 7px 10px;
border-radius: var(--radius-sm);
background: var(--color-neutral-900);
transition: background 120ms, color 120ms;
}
.vk-settings-summary:hover {
background: var(--color-neutral-800);
color: var(--color-accent-200);
}
.vk-settings-summary .vk-caret {
display: inline-flex;
color: var(--color-neutral-400);
transition: transform 160ms cubic-bezier(0.2, 0.7, 0.3, 1);
}
.vk-settings-group[open] > .vk-settings-summary {
color: var(--color-accent-200);
}
.vk-settings-group-body {
gap: 24px;
padding: 16px 0 4px 10px;
box-shadow: inset 1px 0 0 var(--color-neutral-900);
margin-left: 4px;
}
/* ── 7. Native controls ───────────────────────────────────────────────────
Selects kept the platform arrow and a 1px border; numbers kept their
spinners; ranges kept the platform track. All three read as unstyled. */
.vk-select {
appearance: none;
padding: 5px 26px 5px 9px;
border: none;
border-radius: var(--radius-sm);
box-shadow: inset 0 0 0 1px var(--color-neutral-800);
background-color: var(--color-bg);
background-image: none;
cursor: pointer;
transition: box-shadow 120ms;
}
/* The caret is a mask, not a coloured data-URI, so it takes its colour from
the token rather than from a hex baked into the URL. */
.vk-select-wrap {
position: relative;
display: inline-flex;
}
.vk-select-wrap::after,
.vk-status-value:has(> .vk-select)::after {
content: "";
position: absolute;
right: 9px;
top: 50%;
translate: 0 -50%;
width: 9px;
height: 9px;
pointer-events: none;
background: var(--color-neutral-400);
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 256 256' fill='none' stroke='%23000' stroke-width='26' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M208 96l-80 80-80-80'/%3E%3C/svg%3E")
center / contain no-repeat;
}
.vk-status-value:has(> .vk-select) {
position: relative;
}
.vk-select:hover {
box-shadow: inset 0 0 0 1px var(--color-neutral-700);
}
.vk-select:focus-visible {
outline: 2px solid var(--color-accent);
outline-offset: 2px;
}
.vk-media-input,
input[type="number"].input {
font-variant-numeric: tabular-nums;
text-align: right;
}
.vk-media-input::-webkit-outer-spin-button,
.vk-media-input::-webkit-inner-spin-button {
appearance: none;
margin: 0;
}
input[type="range"] {
appearance: none;
height: 18px;
background: none;
cursor: pointer;
}
input[type="range"]::-webkit-slider-runnable-track {
height: 3px;
border-radius: 2px;
background: var(--color-neutral-800);
}
input[type="range"]::-moz-range-track {
height: 3px;
border-radius: 2px;
background: var(--color-neutral-800);
}
input[type="range"]::-webkit-slider-thumb {
appearance: none;
width: 12px;
height: 12px;
margin-top: -4.5px;
border-radius: 50%;
background: var(--color-accent-200);
box-shadow: 0 0 0 1px var(--color-accent);
}
input[type="range"]::-moz-range-thumb {
width: 12px;
height: 12px;
border: none;
border-radius: 50%;
background: var(--color-accent-200);
box-shadow: 0 0 0 1px var(--color-accent);
}
input[type="range"]:focus-visible {
outline: 2px solid var(--color-accent);
outline-offset: 2px;
border-radius: var(--radius-sm);
}
/* ── 8. Menus ─────────────────────────────────────────────────────────────
The guild picker floated with no edge, and the active item was a tint
with nothing to anchor it. */
.vk-menu {
padding: 5px;
box-shadow: var(--shadow-lg), inset 0 0 0 1px var(--color-neutral-800);
animation: vk-menu-in 120ms ease-out;
scrollbar-width: thin;
scrollbar-color: var(--color-neutral-800) transparent;
}
@keyframes vk-menu-in {
from {
transform: translateY(-4px);
opacity: 0;
}
}
.vk-menu-item {
border-radius: var(--radius-sm);
transition: background 100ms, color 100ms;
}
.vk-menu-item.active {
box-shadow: inset 2px 0 0 var(--color-accent);
}
/* ── 9. Dialogs ───────────────────────────────────────────────────────────
The voice-profile drawer was square-topped and flat-bottomed against the
scrim; the media modals had their own unrelated corner radii. */
.vk-profile-panel {
border-radius: var(--radius-lg);
box-shadow: var(--shadow-lg), inset 0 0 0 1px var(--color-neutral-800);
}
.vk-profile-panel .vk-settings-head {
cursor: grab;
border-radius: var(--radius-lg) var(--radius-lg) 0 0;
}
.vk-profile-panel .vk-settings-head:active {
cursor: grabbing;
}
/* A grab handle, so the drag target is discoverable. */
.vk-profile-panel .vk-settings-head::before {
content: "";
position: absolute;
top: 6px;
left: 50%;
translate: -50% 0;
width: 34px;
height: 3px;
border-radius: 2px;
background: var(--color-neutral-800);
}
.vk-profile-panel .vk-settings-head {
position: relative;
padding-top: 15px;
}
.vk-media-modal-box {
border-radius: var(--radius-lg);
box-shadow: var(--shadow-lg), inset 0 0 0 1px var(--color-neutral-800);
}
.vk-media-head strong {
font: 500 14px/1.2 var(--font-heading);
letter-spacing: -0.01em;
}
/* ── 10. Toasts ───────────────────────────────────────────────────────────
Icon slot, roles carried by tokens, and an entrance that comes from the
corner they live in. */
.vk-toast {
display: flex;
align-items: flex-start;
gap: 9px;
line-height: 1.5;
box-shadow: var(--shadow-md), inset 0 0 0 1px var(--color-neutral-800);
animation: vk-toast-in 160ms cubic-bezier(0.2, 0.7, 0.3, 1);
}
@keyframes vk-toast-in {
from {
transform: translateY(6px);
opacity: 0;
}
}
.vk-toast .vk-icon {
flex: none;
margin-top: 1px;
}
.vk-toast.error .vk-icon {
color: var(--color-danger-400);
}
.vk-toast.warn .vk-icon {
color: var(--color-warn-400);
}
.vk-toast.ok .vk-icon {
color: var(--color-ok-400);
}
/* ── 11. Motion ───────────────────────────────────────────────────────────*/
@media (prefers-reduced-motion: reduce) {
.vk-overlay,
.vk-settings-panel,
.vk-menu,
.vk-toast {
animation: none;
}
.vk-settings-summary .vk-caret {
transition: none;
}
}
+168
View File
@@ -0,0 +1,168 @@
/* ─────────────────────────────────────────────────────────────────────────
Veskslop style overrides — Nocturne fidelity pass.
Small corrections to style.css. Load LAST, after style.css and media.css.
Fold into style.css when convenient; kept separate here so nothing you
edited gets clobbered.
───────────────────────────────────────────────────────────────────────── */
/* ── 1. Weight ────────────────────────────────────────────────────────────
Nocturne: headings never go past 500; hierarchy is size and space.
"Speaking" was font-weight 700, which also reflows the row as it toggles.
Carry it in color instead — the level meter beside it is the loud signal. */
.vk-person-name.speaking,
.vk-voice-member .speaking,
.vk-right-name .speaking,
.vk-right-name .vk-name.speaking {
font-weight: 500;
color: var(--color-accent-200);
}
/* ── 2. Focus ─────────────────────────────────────────────────────────────
nocturne.css gives :focus-visible a 2px accent ring, but every control
built from a bare <button> that resets background and border loses the
ring's contrast against its own surface. Pin it back on explicitly. */
.vk-rail-row:focus-visible,
.vk-rail-voice-head:focus-visible,
.vk-menu-item:focus-visible,
.vk-guild-mark:focus-visible,
.vk-scope-btn:focus-visible,
.vk-console-tab:focus-visible,
.vk-console-button:focus-visible,
.vk-chip:focus-visible,
.vk-star:focus-visible,
.vk-tab:focus-visible,
.vk-settings-summary:focus-visible {
outline: 2px solid var(--color-accent);
outline-offset: 2px;
border-radius: var(--radius-sm);
}
/* Inset the ring where the control sits flush in a scroller, so it is not
clipped by the overflow edge. */
.vk-rail-row:focus-visible,
.vk-menu-item:focus-visible {
outline-offset: -2px;
}
/* ── 3. Presence ──────────────────────────────────────────────────────────
idle and dnd both landed on neutral-200, so they were indistinguishable.
idle drops a step; dnd keeps the bright dot and adds the bar that makes
it readable without color. */
.vk-presence.idle {
background: var(--color-neutral-400);
}
.vk-presence.dnd {
display: grid;
place-items: center;
background: var(--color-neutral-200);
}
.vk-presence.dnd::after {
content: "";
width: 4px;
height: 1.5px;
background: var(--color-surface);
}
/* ── 4. Rules ─────────────────────────────────────────────────────────────
Nocturne rules fade to transparent over 48px a side. The channel header
does this; the settings panel's dozens of flat rules do not, which is
what makes that panel read as a different product. */
.vk-settings-head,
.vk-status-row {
box-shadow: none;
background: linear-gradient(
to right,
transparent,
var(--color-neutral-900) 24px,
var(--color-neutral-900) calc(100% - 24px),
transparent
)
no-repeat bottom / 100% 1px;
}
.vk-settings-head {
background: linear-gradient(
to right,
transparent,
var(--color-neutral-800) 24px,
var(--color-neutral-800) calc(100% - 24px),
transparent
)
no-repeat bottom / 100% 1px;
}
.vk-status-row:last-child {
background: none;
}
/* ── 5. Toasts ────────────────────────────────────────────────────────────
The error toast was a neutral-200 stripe — the same mark the ok toast
used, one hue apart. Now they are genuinely different roles. */
.vk-toast.error {
box-shadow: var(--shadow-md), inset 3px 0 0 var(--color-danger-400);
}
.vk-toast.warn {
box-shadow: var(--shadow-md), inset 3px 0 0 var(--color-warn-400);
}
.vk-toast.ok {
box-shadow: var(--shadow-md), inset 3px 0 0 var(--color-ok-400);
}
/* ── 6. Offline ───────────────────────────────────────────────────────────
opacity .5 + saturate .6 pushes every label under 3:1 — the one state
where you most need to read what is on screen. Dim the chrome, not the
type, and say why. */
.vk-offline .vk-body,
.vk-offline .vk-console {
opacity: 1;
filter: none;
pointer-events: none;
}
.vk-offline .vk-body::after {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
background: color-mix(in srgb, var(--color-bg) 45%, transparent);
}
.vk-offline .vk-body {
position: relative;
}
.vk-offline .vk-composer,
.vk-offline .vk-rail,
.vk-offline .vk-right {
opacity: 0.72;
}
/* ── 7. The bridge dot ────────────────────────────────────────────────────
.error was neutral-200 — a white dot reads as "on". */
.vk-dot.error {
background: var(--color-danger-400);
}
.vk-dot.warn {
background: var(--color-warn-400);
}
/* ── 8. QR ────────────────────────────────────────────────────────────────
Pure white is correct here (scanner contrast), but give it the system's
edge so it is not a bare rectangle floating on the panel. */
.vk-qr {
box-shadow: var(--shadow-sm);
}
+86
View File
@@ -0,0 +1,86 @@
/* ─────────────────────────────────────────────────────────────────────────
Nocturne status + data tokens — additive layer for Veskslop.
Why this file exists: Nocturne ships one accent and a neutral ramp. It has
no error / warning / success role and no categorical data scale, so every
feature that needed one invented its own hex (#e5484d, #ffaa33, #44cc88,
#4f8cff, #ff8080 …). That is how a system drifts.
These roles are generated the same way Nocturne's own ramps are: OKLCH,
on the SAME shared lightness scale, so step 400 of danger has the same
visual weight as step 400 of accent or neutral. Chroma stays low — the
accent is still the loudest thing on screen.
Lightness scale, read off Nocturne's neutral ramp:
200 .924 · 300 .859 · 400 .763 · 700 .443 · 800 .339 · 900 .240
Load AFTER nocturne.css and BEFORE style.css.
───────────────────────────────────────────────────────────────────────── */
:root {
/* danger — hue 25. Failed jobs, rate limits, destructive confirms. */
--color-danger-200: oklch(0.924 0.045 25);
--color-danger-300: oklch(0.859 0.075 25);
--color-danger-400: oklch(0.763 0.105 25);
--color-danger-700: oklch(0.443 0.085 25);
--color-danger-800: oklch(0.339 0.065 25);
--color-danger-900: oklch(0.24 0.045 25);
/* warn — hue 75. Paused, degraded, over-threshold, mentions. */
--color-warn-200: oklch(0.924 0.045 75);
--color-warn-300: oklch(0.859 0.075 75);
--color-warn-400: oklch(0.763 0.105 75);
--color-warn-700: oklch(0.443 0.085 75);
--color-warn-800: oklch(0.339 0.065 75);
--color-warn-900: oklch(0.24 0.045 75);
/* ok — hue 155. Completed, healthy, connected. Used sparingly: the
accent already carries "live", so ok is for terminal success only. */
--color-ok-200: oklch(0.924 0.05 155);
--color-ok-300: oklch(0.859 0.08 155);
--color-ok-400: oklch(0.763 0.11 155);
--color-ok-700: oklch(0.443 0.09 155);
--color-ok-800: oklch(0.339 0.07 155);
--color-ok-900: oklch(0.24 0.05 155);
/* Categorical data scale — for the media storage bar and nothing else.
One lightness, five hues stepping away from the accent, all at low
chroma so the bar reads as a muted family rather than five alarms.
Always pair with a text legend: hue is never the only channel. */
--color-data-1: oklch(0.62 0.075 289); /* image — the accent's own hue */
--color-data-2: oklch(0.62 0.075 250); /* gif */
--color-data-3: oklch(0.62 0.075 205); /* video */
--color-data-4: oklch(0.62 0.075 155); /* audio */
--color-data-5: oklch(0.62 0.02 289); /* other — near-neutral */
/* Mention highlight: was a loose #d29922 declared mid-stylesheet. */
--color-mention: var(--color-warn-400);
}
/* ── Status helpers ────────────────────────────────────────────────────────
Tinted, never flooded. Full-opacity ink on the tint so small text clears
contrast; the fill is a deep ramp step, not an alpha wash. */
.vk-status-tint-danger {
background: var(--color-danger-900);
box-shadow: inset 0 0 0 1px var(--color-danger-700);
color: var(--color-danger-200);
}
.vk-status-tint-warn {
background: var(--color-warn-900);
box-shadow: inset 0 0 0 1px var(--color-warn-700);
color: var(--color-warn-200);
}
.vk-status-tint-ok {
background: var(--color-ok-900);
box-shadow: inset 0 0 0 1px var(--color-ok-700);
color: var(--color-ok-200);
}
.vk-status-tint-neutral {
background: var(--color-neutral-900);
box-shadow: inset 0 0 0 1px var(--color-neutral-800);
color: var(--color-neutral-300);
}