openvibers/openvibe.report Commits
openvibe.report/design: every site's design health, read from its code (#44)
Changes
@@@ -272,3 +272,50 @@ html, body { margin: 0; background: var(--bg-primary, #0a0f1c); color: var(--tex.rp-you-next { border-top: 1px solid var(--border, #1f2d47); padding-top: 18px; }.rp-you-next h2 { font-size: 1rem; }@media (max-width: 560px) { .rp-person { align-items: flex-start; } }++++/* Design health (/design, /design/:site; server/views/design.js): a score on a bar, the table's columns, the counted++ fold, the rules and the findings. Colours come from the theme (--success, --warning, --danger and their -text). */++.dh-tr { grid-template-columns: minmax(0, 1.5fr) minmax(0, 1.4fr) repeat(5, minmax(0, .8fr)); }++.dh-score { display: inline-flex; align-items: center; gap: 10px; width: 100%; min-width: 0; --dh-c: var(--success, var(--room-accent)); }++.dh-score b { min-width: 2.4em; font-variant-numeric: tabular-nums; }++.dh-score i { flex: 1 1 auto; min-width: 40px; height: 6px; border-radius: 999px; background: linear-gradient(90deg, var(--dh-c) var(--dh), color-mix(in srgb, var(--text-muted, currentColor) 22%, transparent) var(--dh)); }++.dh-score.is-ok { --dh-c: var(--warning, var(--room-accent)); }++.dh-score.is-low { --dh-c: var(--danger, var(--room-accent)); }++.dh-n { font-variant-numeric: tabular-nums; }++.dh-n.is-zero { color: var(--text-muted); }++.dh-n.is-bad { color: var(--danger-text, var(--danger)); font-weight: 800; }++.dh-fold { margin-top: 12px; }++.dh-fold > summary { display: inline-flex; align-items: baseline; gap: 8px; padding: 8px 16px; border-radius: 999px; border: 1px solid var(--border); background: var(--bg-card); font-weight: 700; cursor: pointer; list-style: none; }++.dh-fold > summary::-webkit-details-marker { display: none; }++.dh-fold > summary span { color: var(--text-muted); font-size: .85rem; font-weight: 600; }++.dh-fold > summary:hover { border-color: var(--room-accent); }++.dh-fold[open] > summary { margin-bottom: 12px; }++.dh-rules { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(260px, 100%), 1fr)); gap: 12px; margin: 0 0 14px; }++.dh-rules > div { padding: 12px 14px; border-radius: 14px; background: var(--bg-card); border: 1px solid var(--border); }++.dh-rules dt { display: flex; justify-content: space-between; gap: 8px; font-weight: 800; }++.dh-rules dt span { color: var(--text-muted); font-size: .78rem; font-weight: 700; font-variant-numeric: tabular-nums; white-space: nowrap; }++.dh-rules dd { margin: 6px 0 0; color: var(--text-secondary); font-size: .92rem; line-height: 1.5; }++.dh-lede { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 16px; }++.dh-lede .dh-score { width: min(280px, 100%); font-size: 1.4rem; }++.dh-finds { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }++.dh-find { display: grid; grid-template-columns: 10.5em minmax(0, 1fr); align-items: baseline; gap: 4px 10px; padding: 10px 14px; border-radius: 12px; background: var(--bg-card); border: 1px solid var(--border); }++.dh-rule { justify-self: start; padding: 2px 8px; border-radius: 999px; border: 1px solid var(--border); font-size: .72rem; font-weight: 800; letter-spacing: .03em; white-space: nowrap; }++.dh-rule--spinner, .dh-rule--missing-reduced-motion { color: var(--danger-text, var(--danger)); border-color: color-mix(in srgb, var(--danger, currentColor) 45%, transparent); }++.dh-where { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: .85rem; overflow-wrap: anywhere; }++.dh-text { grid-column: 2; display: block; overflow-x: auto; padding-top: 2px; color: var(--text-secondary); font-size: .8rem; white-space: pre; }++.dh-none { display: none; }++@media (max-width: 699px) {++ /* A site in two lines: its name and score bar, then only what it has to fix, in words. */++ .dh-tr { display: flex; flex-wrap: wrap; align-items: center; gap: 2px 12px; }++ .dh-tr::after { content: ''; order: 2; flex-basis: 100%; }++ .dh-tr.rp-th { display: none; }++ .dh-tr > .rp-site { order: 0; flex: 1 1 0; min-width: 0; }++ .dh-tr > [data-l="Score"] { order: 1; flex: 0 0 auto; width: 132px; }++ .dh-tr > [data-l]::before { content: none; }++ .dh-tr > .dh-n, .dh-tr > .dh-none { order: 3; font-size: .82rem; color: var(--text-secondary); }++ .dh-tr > .dh-n::after { content: ' ' attr(data-s); color: var(--text-muted); }++ .dh-tr > .dh-n.is-zero { display: none; }++ .dh-tr > .dh-none { display: inline; color: var(--text-muted); }++ .dh-find { grid-template-columns: minmax(0, 1fr); }++ .dh-text { grid-column: 1; }++}nnew file mode 100644@@@ -0,0 +1,105 @@++#!/usr/bin/env node++'use strict';++// Measures every OpenVibe site's design health and writes server/design-health/snapshot.json, the snapshot openvibe.report/design++// shows (plan L45 step 6). The rules are server/design-health/scan.js: where a site hand-writes what the shared kit++// owns (motion durations and curves, theme colours), animates without asking for reduced motion, or spins.++//++// It reads checkouts, so it runs where the repos are (the orchestrator's workstation), never on the server:++//++// node scripts/design-health.js every ~/openvibe/work/OpenVibe.* checkout, at its origin/main++// node scripts/design-health.js --work <dir> another folder of checkouts++// node scripts/design-health.js --repo Live --no-fetch just Live again; every other site keeps its last measure++//++// Each repo is read through a detached worktree of origin/main in a temp folder, removed again afterwards: no++// checkout is touched and no branch moves. The snapshot keeps every count and the first findings of each site, so it++// stays small; commit it with a PR to refresh the page.++++const fs = require('fs');++const os = require('os');++const path = require('path');++const { execFileSync } = require('child_process');++const { scanRepo, WEIGHTS, RULES } = require('../server/design-health/scan');++++const argv = process.argv.slice(2);++const opt = (name) => { const i = argv.indexOf(name); return i >= 0 ? argv[i + 1] : null; };++const WORK = opt('--work') || process.env.OV_WORK_DIR || path.join(os.homedir(), 'openvibe', 'work');++const ONLY = argv.reduce((a, v, i) => (argv[i - 1] === '--repo' ? a.concat(v.toLowerCase()) : a), []);++const FETCH = !argv.includes('--no-fetch');++const OUT = path.join(__dirname, '..', 'server', 'design-health', 'snapshot.json');++const KEEP = 25; // findings kept per site; the counts are always the whole picture++++const git = (dir, args, soft) => {++ try { return execFileSync('git', ['-C', dir, ...args], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim(); } catch (err) {++ if (soft) return null;++ throw new Error(`git ${args.join(' ')} in ${dir}: ${String(err.stderr || err.message).trim()}`);++ }++};++// The repo's name on GitHub, so a clone and a linked worktree of it are one site.++const nameOf = (dir) => { const url = git(dir, ['remote', 'get-url', 'origin'], true) || ''; const m = /([^/]+?)(\.git)?$/.exec(url); return m ? m[1] : path.basename(dir); };++++function repos() {++ const seen = new Map();++ for (const d of fs.readdirSync(WORK).sort()) {++ if (!/^OpenVibe\./.test(d) && d !== 'OpenRestream') continue;++ const dir = path.join(WORK, d);++ if (!fs.existsSync(path.join(dir, '.git'))) continue;++ const name = nameOf(dir);++ if (!seen.has(name)) seen.set(name, dir);++ }++ return [...seen].filter(([name]) => !ONLY.length || ONLY.some((o) => name.toLowerCase().includes(o)));++}++++function keep(findings) {++ const weight = (f) => WEIGHTS[f.rule] || 0;++ const ordered = findings.map((f, i) => ({ f, i })).sort((a, b) => weight(b.f) - weight(a.f) || a.i - b.i);++ const perFile = new Map(), picked = [];++ for (let round = 0; picked.length < KEEP && picked.length < ordered.length; round++) {++ for (const x of ordered) {++ if (picked.length >= KEEP) break;++ if (x.taken || (perFile.get(x.f.file) || 0) !== round) continue;++ x.taken = true; perFile.set(x.f.file, round + 1); picked.push(x);++ }++ }++ return picked.sort((a, b) => weight(b.f) - weight(a.f) || a.i - b.i).map((x) => x.f);++}++++function measure(name, dir, tmp) {++ const wt = path.join(tmp, name.toLowerCase().replace(/[^a-z0-9]+/g, '-'));++ let added = false;++ try {++ if (FETCH) git(dir, ['fetch', '--quiet', 'origin', '+refs/heads/main:refs/remotes/origin/main'], true);++ const commit = git(dir, ['rev-parse', 'origin/main'], true);++ if (!commit) return null;++ git(dir, ['worktree', 'add', '--detach', wt, 'origin/main']);++ added = true;++ const r = scanRepo(wt);++ return {++ repo: name, commit, files_scanned: r.files_scanned, counts: r.counts, score: r.score, findings_total: r.findings.length,++ // The findings kept matter most (by the rule's weight), spread over as many files as possible.++ findings: keep(r.findings).map((f) => ({ rule: f.rule, file: f.file, line: f.line, value: f.value == null ? null : String(f.value).slice(0, 60), text: String(f.text || '').trim().slice(0, 160) })),++ };++ } finally {++ if (added) { git(dir, ['worktree', 'remove', '--force', wt], true); git(dir, ['worktree', 'prune'], true); }++ fs.rmSync(wt, { recursive: true, force: true });++ }++}++++const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'design-health-'));++const sites = [];++try {++ for (const [name, dir] of repos()) {++ const s = measure(name, dir, tmp);++ if (s && s.files_scanned) sites.push(s); // a repo with no stylesheet where the rules read is not a site to score++ process.stdout.write(`${name.padEnd(24)} ${s ? `${String(s.score).padStart(6)} ${s.findings_total} findings` : 'skipped (no origin/main)'}\n`);++ }++} finally { fs.rmSync(tmp, { recursive: true, force: true }); }++// With --repo, only those sites are measured again: the others keep what the snapshot already has.++if (ONLY.length) {++ let before = [];++ try { before = JSON.parse(fs.readFileSync(OUT, 'utf8')).sites || []; } catch { before = []; }++ const fresh = new Set(sites.map((s) => s.repo));++ sites.push(...before.filter((s) => !fresh.has(s.repo)));++}++sites.sort((a, b) => a.repo.localeCompare(b.repo));++fs.writeFileSync(OUT, `${JSON.stringify({ generated_at: new Date().toISOString(), rules: RULES, weights: WEIGHTS, kept: KEEP, sites }, null, 1)}\n`);++process.stdout.write(`\nwrote ${path.relative(process.cwd(), OUT)}: ${sites.length} sites\n`);nnew file mode 100644@@@ -0,0 +1,36 @@++'use strict';++/**++ * Every site's design health (plan L45 step 6): the snapshot scripts/design-health.js writes to server/design-health/snapshot.json++ * from each site's code at its main branch. Read once and kept; a refresh is a new snapshot in a PR.++ *++ * all() → { generated_at, rules, weights, kept, sites: [...] } (sites with a stylesheet to read only)++ * site(slug) → one site by its short name ('live', 'tools', 'openrestream'), or null++ * slugOf(repo) → 'OpenVibe.Live' → 'live'++ */++const fs = require('fs');++const path = require('path');++++// DESIGN_HEALTH_FILE: another snapshot (the tests' fixture).++const FILE = process.env.DESIGN_HEALTH_FILE || path.join(__dirname, '..', 'design-health', 'snapshot.json');++const slugOf = (repo) => String(repo || '').replace(/^OpenVibe\./, '').toLowerCase();++let cached = null;++++function all() {++ if (cached) return cached;++ let snap = null;++ try { snap = JSON.parse(fs.readFileSync(FILE, 'utf8')); } catch { snap = null; }++ const sites = (snap && Array.isArray(snap.sites) ? snap.sites : [])++ // A repo with no stylesheet where the rules read (a library, a service with no pages) is not a site to score.++ .filter((s) => s && s.counts && Number(s.files_scanned) > 0)++ .map((s) => ({ ...s, slug: slugOf(s.repo) }));++ cached = { generated_at: snap ? snap.generated_at : null, rules: snap ? snap.rules : [], weights: snap ? snap.weights : {}, kept: snap ? snap.kept : 0, sites };++ return cached;++}++++function site(slug) {++ const s = String(slug || '').toLowerCase();++ if (!/^[a-z0-9-]{2,40}$/.test(s)) return null;++ return all().sites.find((x) => x.slug === s) || null;++}++++module.exports = { all, site, slugOf, _reset: () => { cached = null; } };nnew file mode 100644@@@ -0,0 +1,492 @@++'use strict';++// The design-health rules (plan L45 step 6), served by openvibe.report/design. They began as the orchestrator's++// tools/design-lint and live here now so the measure is as open as its results; scripts/design-health.js runs them.++// Reads every CSS file and every inline <style> in HTML and JS under public/, server/views/ and++// apps/*/public/, and reports where a site hand-writes what the shared kit already owns:++//++// literal_duration a transition/animation duration written as a literal (.25s, 300ms)++// where the kit says var(--ov-dur-1..4)++// literal_curve cubic-bezier(...) or an ease keyword where the kit says var(--ov-ease-*)++// spinner @keyframes that rotate 360deg, and an animation that runs one forever++// color_drift a hard-coded colour in a file that already uses the theme variables++// missing_reduced_motion a file that animates and never asks prefers-reduced-motion++//++// node_modules, vendor and .min. files are skipped. The scanner never writes; it only reads.++//++// node scan.js <repo dir> JSON of the counts and findings on stdout++// node scan.js <repo dir> --summary one line per rule++//++// The finding kinds are the same keys the report counts by, so a caller can group either way.++++const fs = require('fs');++const path = require('path');++++// ── what the kit owns ─────────────────────────────────────────────────────────────────────────────────────────++// the theme colour variables, from openvibe-shared/theme-loader.js THEME_KEYS++const THEME_KEYS = ['--bg-primary', '--bg-secondary', '--bg-tertiary', '--bg-card', '--bg-hover', '--bg-input',++ '--text-primary', '--text-secondary', '--text-muted', '--accent', '--accent-light', '--accent-dark',++ '--border', '--border-light', '--live-red', '--success', '--warning', '--danger', '--info',++ '--shadow', '--shadow-lg', '--success-text', '--warning-text', '--danger-text', '--info-text',++ '--live-red-text', '--accent-strong', '--on-accent-strong', '--on-accent', '--accent-rgb',++ '--accent-glow', '--color-scheme'];++const THEME_SET = new Set(THEME_KEYS);++++// how much each rule costs a site its score: a spinner weighs most (nothing spins), then a file that++// never asks prefers-reduced-motion, then a hand-written duration or curve, then a hand-written colour++const WEIGHTS = {++ spinner: 4,++ missing_reduced_motion: 2,++ literal_duration: 0.25,++ literal_curve: 0.25,++ color_drift: 0.05++};++++const RULES = ['literal_duration', 'literal_curve', 'spinner', 'color_drift', 'missing_reduced_motion'];++++// the directories a site keeps its styles in++const STYLE_ROOTS = ['public', 'server/views'];++const APP_ROOT = 'apps';++++// the properties that carry a duration, and the ones that carry a curve++const DURATION_PROPS = new Set(['transition', 'transition-duration', 'animation', 'animation-duration']);++const CURVE_PROPS = new Set(['transition', 'transition-timing-function', 'animation', 'animation-timing-function']);++++const SKIP_DIRS = new Set(['node_modules', 'vendor', '.git']);++const CSS_EXT = new Set(['.css']);++const MARKUP_EXT = new Set(['.html', '.htm']);++const SCRIPT_EXT = new Set(['.js', '.mjs', '.cjs']);++++// the keywords an animation shorthand can hold, so the name of the keyframes can be told from the rest++const ANIM_KEYWORDS = new Set(['infinite', 'linear', 'ease', 'ease-in', 'ease-out', 'ease-in-out', 'normal',++ 'reverse', 'alternate', 'alternate-reverse', 'forwards', 'backwards', 'both', 'none', 'running', 'paused',++ 'step-start', 'step-end']);++++// the colours a site may name, when it should be asking the theme++const NAMED_COLOURS = new Set(['white', 'black', 'red', 'green', 'blue', 'yellow', 'orange', 'purple', 'pink',++ 'gray', 'grey', 'silver', 'gold', 'navy', 'teal', 'maroon', 'olive', 'lime', 'aqua', 'fuchsia', 'cyan',++ 'magenta', 'crimson', 'indigo', 'violet', 'salmon', 'tan', 'khaki', 'beige', 'ivory', 'lavender',++ 'turquoise', 'plum', 'orchid', 'coral', 'tomato', 'chocolate', 'sienna', 'darkred', 'darkblue', 'darkgreen']);++++const TIME_RE = /(?<![\w.-])(\d*\.?\d+)(ms|s)\b/gi;++const TIME_ONLY_RE = /^(?:\d*\.?\d+)(?:ms|s)$/i;++const CURVE_FN_RE = /cubic-bezier\s*\(/gi;++const EASE_RE = /(?<![\w-])(ease-in-out|ease-in|ease-out|ease)(?![\w-])/gi;++const HEX_RE = /#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})\b/g;++const FUNC_COLOUR_RE = /\b(?:rgba?|hsla?)\([^)]*\)/gi;++// hue-rotate(360deg) cycles a colour, it does not turn anything: not a spinner (Live's eggRainbow, 2026-11-01).++const SPIN_RE = /(?<!hue-)rotate\(\s*(?:360(?:\.0+)?deg|1turn|2turn|-360(?:\.0+)?deg)\s*\)/i;++++// ── helpers ────────────────────────────────────────────────────────────────────────────────────────────────++// the fallback inside var(--name, fallback) is a stand-in, not a choice: look past it++const VAR_WITH_FALLBACK_RE = /var\(\s*(--[\w-]+)\s*,(?:[^()]*|\([^()]*\))*\)/gi;++function stripVarFallbacks(value) {++ return value.replace(VAR_WITH_FALLBACK_RE, 'var($1)');++}++++// the theme variables a value asks for++function themeVarsIn(value) {++ const found = new Set();++ const re = /var\(\s*(--[\w-]+)/g;++ let m;++ while ((m = re.exec(value))) found.add(m[1]);++ return found;++}++++function isThemeVar(name) {++ return THEME_SET.has(name);++}++++// the hard-coded colours in a value++function coloursIn(value) {++ const out = [];++ const text = stripVarFallbacks(value).replace(/url\([^)]*\)/gi, 'url()');++ let m;++ HEX_RE.lastIndex = 0;++ while ((m = HEX_RE.exec(text))) out.push(m[0]);++ FUNC_COLOUR_RE.lastIndex = 0;++ while ((m = FUNC_COLOUR_RE.exec(text))) out.push(m[0]);++ const re = /(?<![\w-#])([a-zA-Z]{3,20})(?![\w-])/g;++ while ((m = re.exec(text))) {++ const name = m[1].toLowerCase();++ if (NAMED_COLOURS.has(name)) out.push(name);++ }++ return out;++}++++// the duration literals in a value++function durationsIn(value) {++ const out = [];++ const text = stripVarFallbacks(value);++ TIME_RE.lastIndex = 0;++ let m;++ while ((m = TIME_RE.exec(text))) out.push(m[0]);++ return out;++}++++// the curves written out in a value++function curvesIn(value) {++ const out = [];++ const text = stripVarFallbacks(value);++ CURVE_FN_RE.lastIndex = 0;++ let m;++ while ((m = CURVE_FN_RE.exec(text))) out.push(m[0]);++ EASE_RE.lastIndex = 0;++ while ((m = EASE_RE.exec(text))) out.push(m[1]);++ return out;++}++++// the keyframes an animation shorthand names++function animationName(value) {++ const text = value.replace(/var\([^)]*\)/gi, ' ');++ const tokens = text.split(/[\s,]+/).filter(Boolean);++ for (const tok of tokens) {++ const t = tok.toLowerCase();++ if (ANIM_KEYWORDS.has(t)) continue;++ if (TIME_ONLY_RE.test(t)) continue;++ if (/^cubic-bezier\(/i.test(t) || /^steps\(/i.test(t)) continue;++ return tok;++ }++ return null;++}++++// ── the CSS reader ─────────────────────────────────────────────────────────────────────────────────────────++// a small reader: enough to know which declaration sits on which line, which rule it belongs to++// and which keyframes it is inside++function parseCss(src, baseLine) {++ const decls = [];++ const keyframes = [];++ let i = 0;++ let line = baseLine;++ let uid = 0;++ const len = src.length;++++ function skipComment() {++ const end = src.indexOf('*/', i + 2);++ const stop = end < 0 ? len : end + 2;++ for (let k = i; k < stop; k++) if (src[k] === '\n') line++;++ i = stop;++ }++++ function consumeString() {++ const q = src[i];++ let k = i + 1;++ let out = q;++ while (k < len) {++ const ch = src[k];++ if (ch === '\\') { out += src[k] + (src[k + 1] || ''); k += 2; continue; }++ out += ch;++ k++;++ if (ch === q) break;++ }++ for (let x = i; x < k; x++) if (src[x] === '\n') line++;++ i = k;++ return out;++ }++++ function flush(buf, bufLine, chain) {++ const text = buf.trim();++ if (!text) return;++ const colon = text.indexOf(':');++ if (colon < 0) return;++ const prop = text.slice(0, colon).trim();++ const value = text.slice(colon + 1).trim();++ if (!prop || !value) return;++ if (/[{}]/.test(prop)) return;++ const owner = chain[chain.length - 1];++ let kf = null;++ for (const b of chain) if (b.kf) kf = b.kf;++ const decl = { prop, value, line: bufLine, rule: owner, keyframes: kf };++ decls.push(decl);++ if (kf) kf.decls.push(decl);++ }++++ function parseBlock(chain) {++ let buf = '';++ let bufLine = line;++ for (;;) {++ if (i >= len) break;++ const c = src[i];++ if (c === '/' && src[i + 1] === '*') { skipComment(); buf = ''; bufLine = line; continue; }++ if (c === '"' || c === "'") { if (!buf.trim()) bufLine = line; buf += consumeString(); continue; }++ if (c === '{') {++ const prelude = buf.trim();++ buf = '';++ bufLine = line;++ const child = { id: ++uid, type: 'rule', selector: prelude, at: null, keyframes: null, parent: chain[chain.length - 1] };++ const am = /^@([\w-]+)/.exec(prelude);++ if (am) {++ child.type = 'at';++ child.at = am[1].toLowerCase();++ const km = /^@(?:-webkit-)?keyframes\s+("[^"]+"|'[^']+'|[^\s{]+)/i.exec(prelude);++ if (child.at === 'keyframes' || child.at === '-webkit-keyframes') {++ child.keyframes = km ? km[1].replace(/^["']|["']$/g, '') : null;++ keyframes.push({ name: child.keyframes, line: bufLine, decls: [] });++ child.kf = keyframes[keyframes.length - 1];++ }++ }++ i++;++ chain.push(child);++ parseBlock(chain);++ chain.pop();++ buf = '';++ bufLine = line;++ continue;++ }++ if (c === '}') { i++; flush(buf, bufLine, chain); return; }++ if (c === ';') { i++; flush(buf, bufLine, chain); buf = ''; bufLine = line; continue; }++ if (c === '\n') { line++; buf += c; i++; continue; }++ // the declaration starts at its first real character, not at the whitespace before it++ if (!buf.trim()) bufLine = line;++ buf += c;++ i++;++ }++ flush(buf, bufLine, chain);++ }++++ const root = { id: 0, type: 'root', selector: '', at: null, keyframes: null, parent: null };++ parseBlock([root]);++ return { decls, keyframes };++}++++// ── the units a file is made of ─────────────────────────────────────────────────────────────────────────────++// a CSS file is one unit; an HTML or JS file is one unit per inline <style>++function unitsIn(file, text) {++ const units = [];++ const ext = path.extname(file).toLowerCase();++ if (CSS_EXT.has(ext)) {++ units.push({ kind: 'css', line: 1, text });++ return units;++ }++ if (!MARKUP_EXT.has(ext) && !SCRIPT_EXT.has(ext)) return units;++ const re = /<style\b[^>]*>([\s\S]*?)<\/style>/gi;++ let m;++ while ((m = re.exec(text))) {++ const before = text.slice(0, m.index);++ const line = 1 + (before.match(/\n/g) || []).length;++ units.push({ kind: 'inline-style', line, text: m[1] });++ }++ return units;++}++++// ── the walk ─────────────────────────────────────────────────────────────────────────────────────────────────++function styleRoots(repoDir) {++ const roots = [];++ for (const rel of STYLE_ROOTS) {++ const p = path.join(repoDir, rel);++ if (fs.existsSync(p) && fs.statSync(p).isDirectory()) roots.push(p);++ }++ const apps = path.join(repoDir, APP_ROOT);++ if (fs.existsSync(apps) && fs.statSync(apps).isDirectory()) {++ for (const name of fs.readdirSync(apps)) {++ const p = path.join(apps, name, 'public');++ if (fs.existsSync(p) && fs.statSync(p).isDirectory()) roots.push(p);++ }++ }++ return roots;++}++++function walk(dir, out) {++ let entries;++ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (e) { return out; }++ for (const e of entries) {++ const p = path.join(dir, e.name);++ if (e.isDirectory()) {++ if (SKIP_DIRS.has(e.name)) continue;++ walk(p, out);++ } else if (e.isFile()) {++ if (e.name.includes('.min.')) continue;++ out.push(p);++ }++ }++ return out;++}++++// ── the lint ───────────────────────────────────────────────────────────────────────────────────────────────++// scan one repo directory and return its counts, its findings and its score++function scanRepo(repoDir) {++ const root = path.resolve(repoDir);++ const findings = [];++ const files = [];++ const roots = styleRoots(root);++++ for (const r of roots) {++ for (const file of walk(r, [])) {++ const ext = path.extname(file).toLowerCase();++ if (!CSS_EXT.has(ext) && !MARKUP_EXT.has(ext) && !SCRIPT_EXT.has(ext)) continue;++ let text;++ try { text = fs.readFileSync(file, 'utf8'); } catch (e) { continue; }++ files.push(file);++ const rel = path.relative(root, file);++ const units = unitsIn(file, text);++ if (!units.length) continue;++ findings.push(...lintFile(root, rel, text, units, file));++ }++ }++++ // the keyframes of a repo, so an animation can be traced back to the one it runs++ const counts = { literal_duration: 0, literal_curve: 0, spinner: 0, color_drift: 0, missing_reduced_motion: 0, total: 0 };++ const by_kind = {};++ for (const f of findings) {++ counts[f.rule] = (counts[f.rule] || 0) + 1;++ by_kind[f.kind] = (by_kind[f.kind] || 0) + 1;++ }++ counts.total = findings.length;++++ findings.sort((a, b) => (a.file < b.file ? -1 : a.file > b.file ? 1 : a.line - b.line));++ return { repo: path.basename(root), counts, by_kind: by_kind, score: scoreOf(counts), findings, files_scanned: files.length };++}++++// the lint of one file++function lintFile(root, rel, text, units, file) {++ const out = [];++ const lines = text.split('\n');++ const lineText = (n) => (lines[n - 1] || '').trim().slice(0, 200);++++ const parsed = units.map((u) => Object.assign({}, u, parseCss(u.text, u.line)));++ const allDecls = [];++ const allKeyframes = [];++ for (const p of parsed) { allDecls.push(...p.decls); allKeyframes.push(...p.keyframes); }++++ // what this file asks the theme for++ const fileThemeVars = new Set();++ for (const d of allDecls) for (const name of themeVarsIn(d.value)) fileThemeVars.add(name);++ const fileUsesTheme = [...fileThemeVars].some(isThemeVar);++ const fileHasReducedMotion = /prefers-reduced-motion/i.test(text);++++ // which rule uses a theme variable: a rule that does is themed, a hard-coded colour in it is drift++ const ruleThemeVars = new Map();++ for (const d of allDecls) {++ if (!d.rule) continue;++ if (!ruleThemeVars.has(d.rule.id)) ruleThemeVars.set(d.rule.id, new Set());++ const set = ruleThemeVars.get(d.rule.id);++ for (const name of themeVarsIn(d.value)) set.add(name);++ }++++ // the keyframes that turn, anywhere in this file++ const spinNames = new Set();++ for (const kf of allKeyframes) {++ const turns = kf.decls.some((d) => SPIN_RE.test(d.value));++ if (turns && kf.name) spinNames.add(kf.name);++ }++++ // the custom properties this file keeps for itself++ const ownPalette = new Set();++++ // the animation declarations, grouped by the rule they sit in++ const rules = new Map();++ for (const d of allDecls) {++ const id = d.rule ? d.rule.id : 0;++ if (!rules.has(id)) rules.set(id, { rule: d.rule, decls: [] });++ rules.get(id).decls.push(d);++ }++++ for (const d of allDecls) {++ const prop = d.prop.toLowerCase();++ const value = d.value;++ const line = d.line;++ const at = { file: rel, line, text: lineText(line) };++++ // durations written out (the kit's own token definitions are the kit, not drift)++ if (DURATION_PROPS.has(prop) && !/^--ov-/.test(prop)) {++ for (const dur of durationsIn(value)) {++ out.push(Object.assign({ rule: 'literal_duration', kind: 'duration', value: dur, detail: prop + ' uses ' + dur + ' where the kit says var(--ov-dur-1..4)' }, at));++ }++ }++++ // curves written out (the kit's own token definitions are the kit, not drift)++ if (CURVE_PROPS.has(prop) && !/^--ov-/.test(prop)) {++ for (const curve of curvesIn(value)) {++ out.push(Object.assign({ rule: 'literal_curve', kind: 'curve', value: curve, detail: prop + ' uses ' + curve + ' where the kit says var(--ov-ease-out / -spring / -in-out)' }, at));++ }++ }++++ // hard-coded colours, in a file that already uses the theme++ if (fileUsesTheme) {++ const themed = d.rule && ruleThemeVars.get(d.rule.id) && [...ruleThemeVars.get(d.rule.id)].some(isThemeVar);++ const colours = coloursIn(value);++ if (colours.length) {++ // a custom property is a palette: one finding for the property, not one per value++ if (prop.startsWith('--')) {++ if (!ownPalette.has(prop)) {++ ownPalette.add(prop);++ out.push(Object.assign({++ rule: 'color_drift', kind: 'own_palette',++ value: prop + ' ' + colours[0],++ detail: 'the file uses theme variables and still keeps its own ' + prop++ }, at));++ }++ } else {++ for (const colour of colours) {++ out.push(Object.assign({++ rule: 'color_drift', kind: themed ? 'colour_in_themed_rule' : 'colour_in_themed_file',++ value: colour,++ detail: (themed ? 'the rule uses theme variables and still writes ' : 'the file uses theme variables and still writes ') + colour++ }, at));++ }++ }++ }++ }++ }++++ // spinners: the keyframes that turn, and the animations that run them forever++ for (const kf of allKeyframes) {++ if (!kf.name || !spinNames.has(kf.name)) continue;++ out.push({ rule: 'spinner', kind: 'spinner_keyframes', file: rel, line: kf.line, text: lineText(kf.line), value: kf.name, detail: '@keyframes ' + kf.name + ' turns 360deg' });++ }++ for (const group of rules.values()) {++ const hasInfinite = group.decls.some((d) => /^(-webkit-)?animation(-iteration-count)?$/.test(d.prop.toLowerCase()) && /\binfinite\b/i.test(d.value));++ for (const d of group.decls) {++ const prop = d.prop.toLowerCase();++ if (prop !== 'animation' && prop !== '-webkit-animation' && prop !== 'animation-name') continue;++ const name = prop === 'animation-name' ? d.value.trim() : animationName(d.value);++ if (!name || !spinNames.has(name)) continue;++ const infinite = /\binfinite\b/i.test(d.value) || (prop === 'animation-name' && hasInfinite);++ if (!infinite) continue;++ out.push({ rule: 'spinner', kind: 'spinner_usage', file: rel, line: d.line, text: lineText(d.line), value: name, detail: 'animation runs ' + name + ' forever' });++ }++ }++++ // a file that animates and never asks prefers-reduced-motion++ if (!fileHasReducedMotion) {++ const first = allDecls.find((d) => /^(-webkit-)?animation(-name)?$/.test(d.prop.toLowerCase()) && !/^none$/i.test(d.value.trim()));++ if (first) {++ out.push({ rule: 'missing_reduced_motion', kind: 'missing_reduced_motion', file: rel, line: first.line, text: lineText(first.line), value: first.prop, detail: 'the file animates and never asks prefers-reduced-motion' });++ }++ }++++ return out;++}++++// ── the score ───────────────────────────────────────────────────────────────────────────────────────────────++// 100 minus what the findings cost, never below 0++function scoreOf(counts) {++ let score = 100;++ for (const rule of RULES) score -= (counts[rule] || 0) * (WEIGHTS[rule] || 0);++ return Math.max(0, Math.min(100, Math.round(score * 100) / 100));++}++++// ── the command line ───────────────────────────────────────────────────────────────────────────────────────++function main(argv) {++ const args = argv.filter((a) => !a.startsWith('--'));++ const summary = argv.includes('--summary');++ if (!args.length) {++ process.stderr.write('usage: node scan.js <repo dir> [--summary]\n');++ process.exit(2);++ }++ const dir = args[0];++ if (!fs.existsSync(dir)) {++ process.stderr.write('scan: no such directory: ' + dir + '\n');++ process.exit(2);++ }++ const result = scanRepo(dir);++ if (summary) {++ const parts = RULES.map((r) => r + '=' + result.counts[r]);++ process.stdout.write(result.repo + ' score=' + result.score + ' files=' + result.files_scanned + ' ' + parts.join(' ') + '\n');++ } else {++ process.stdout.write(JSON.stringify(result, null, 2) + '\n');++ }++}++++module.exports = { scanRepo, scoreOf, lintFile, parseCss, THEME_KEYS, WEIGHTS, RULES };++++if (require.main === module) main(process.argv.slice(2));nnew file mode 100644@@@ -0,0 +1,4984 @@++{++ "generated_at": "2026-11-01T11:14:41.112Z",++ "rules": [++ "literal_duration",++ "literal_curve",++ "spinner",++ "color_drift",++ "missing_reduced_motion"++ ],++ "weights": {++ "spinner": 4,++ "missing_reduced_motion": 2,++ "literal_duration": 0.25,++ "literal_curve": 0.25,++ "color_drift": 0.05++ },++ "kept": 25,++ "sites": [++ {++ "repo": "OpenVibe.Actor",++ "commit": "ac296db14b66fafbff94a0e3a82e33fbda52f013",++ "files_scanned": 12,++ "counts": {++ "literal_duration": 27,++ "literal_curve": 12,++ "spinner": 0,++ "color_drift": 18,++ "missing_reduced_motion": 0,++ "total": 57++ },++ "score": 89.35,++ "findings_total": 57,++ "findings": [++ {++ "rule": "literal_duration",++ "file": "public/css/actor.css",++ "line": 68,++ "value": ".15s",++ "text": ".mode span { display: flex; flex-direction: column; gap: 2px; height: 100%; padding: 10px 12px; border-radius: 12px; border: 1px solid var(--border, #1f2d47); b"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/actor.css",++ "line": 68,++ "value": ".15s",++ "text": ".mode span { display: flex; flex-direction: column; gap: 2px; height: 100%; padding: 10px 12px; border-radius: 12px; border: 1px solid var(--border, #1f2d47); b"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/actor.css",++ "line": 111,++ "value": ".25s",++ "text": ".tl-item { display: grid; grid-template-columns: 52px 1fr; gap: 10px; font-size: .9rem; position: relative; animation: step-in .25s ease-out; }"++ },++ {++ "rule": "literal_curve",++ "file": "public/css/actor.css",++ "line": 111,++ "value": "ease-out",++ "text": ".tl-item { display: grid; grid-template-columns: 52px 1fr; gap: 10px; font-size: .9rem; position: relative; animation: step-in .25s ease-out; }"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/actor.css",++ "line": 135,++ "value": "1.2s",++ "text": ".dot-pulse { display: inline-block; width: 10px; height: 10px; border-radius: 50%; background: var(--accent, #3b82f6); animation: pulse 1.2s ease-in-out infinit"++ },++ {++ "rule": "literal_curve",++ "file": "public/css/actor.css",++ "line": 135,++ "value": "ease-in-out",++ "text": ".dot-pulse { display: inline-block; width: 10px; height: 10px; border-radius: 50%; background: var(--accent, #3b82f6); animation: pulse 1.2s ease-in-out infinit"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/chat.css",++ "line": 17,++ "value": ".26s",++ "text": "overflow-y: auto; overscroll-behavior: contain; transform: translateX(-104%); transition: transform .26s cubic-bezier(.2, .8, .2, 1); box-shadow: 0 0 40px rgba("++ },++ {++ "rule": "literal_curve",++ "file": "public/css/chat.css",++ "line": 17,++ "value": "cubic-bezier(",++ "text": "overflow-y: auto; overscroll-behavior: contain; transform: translateX(-104%); transition: transform .26s cubic-bezier(.2, .8, .2, 1); box-shadow: 0 0 40px rgba("++ },++ {++ "rule": "literal_duration",++ "file": "public/css/chat.css",++ "line": 37,++ "value": "1.6s",++ "text": ".cs-dot { flex: none; width: 8px; height: 8px; border-radius: 50%; background: var(--accent, #f97316); animation: ct-breathe 1.6s ease-in-out infinite; }"++ },++ {++ "rule": "literal_curve",++ "file": "public/css/chat.css",++ "line": 37,++ "value": "ease-in-out",++ "text": ".cs-dot { flex: none; width: 8px; height: 8px; border-radius: 50%; background: var(--accent, #f97316); animation: ct-breathe 1.6s ease-in-out infinite; }"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/chat.css",++ "line": 60,++ "value": ".32s",++ "text": ".ct-turn.is-new .ct-bubble { animation: ct-rise .32s cubic-bezier(.2, .8, .2, 1) both; }"++ },++ {++ "rule": "literal_curve",++ "file": "public/css/chat.css",++ "line": 60,++ "value": "cubic-bezier(",++ "text": ".ct-turn.is-new .ct-bubble { animation: ct-rise .32s cubic-bezier(.2, .8, .2, 1) both; }"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/chat.css",++ "line": 64,++ "value": "1.6s",++ "text": ".ct-turn[data-open] .ct-avatar { animation: ct-breathe 1.6s ease-in-out infinite; }"++ },++ {++ "rule": "literal_curve",++ "file": "public/css/chat.css",++ "line": 64,++ "value": "ease-in-out",++ "text": ".ct-turn[data-open] .ct-avatar { animation: ct-breathe 1.6s ease-in-out infinite; }"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/chat.css",++ "line": 73,++ "value": ".2s",++ "text": ".cw-chev { flex: none; width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; transition: "++ },++ {++ "rule": "literal_duration",++ "file": "public/css/chat.css",++ "line": 76,++ "value": "2.2s",++ "text": "-webkit-background-clip: text; background-clip: text; color: transparent; animation: ct-shimmer 2.2s linear infinite; font-weight: 550; }"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/task.css",++ "line": 8,++ "value": ".25s",++ "text": ".ps { position: relative; display: grid; grid-template-columns: 20px minmax(0, 1fr); gap: 12px; padding: 5px 0; color: var(--text-secondary, #96a7c2); line-heig"++ },++ {++ "rule": "literal_curve",++ "file": "public/css/task.css",++ "line": 8,++ "value": "ease-out",++ "text": ".ps { position: relative; display: grid; grid-template-columns: 20px minmax(0, 1fr); gap: 12px; padding: 5px 0; color: var(--text-secondary, #96a7c2); line-heig"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/task.css",++ "line": 16,++ "value": "1.4s",++ "text": ".ps.is-active .ps-mark { border-color: var(--accent, #3b82f6); animation: ps-pulse 1.4s ease-out infinite; } /* the step on now: a ring that pulses out, never"++ },++ {++ "rule": "literal_curve",++ "file": "public/css/task.css",++ "line": 16,++ "value": "ease-out",++ "text": ".ps.is-active .ps-mark { border-color: var(--accent, #3b82f6); animation: ps-pulse 1.4s ease-out infinite; } /* the step on now: a ring that pulses out, never"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/task.css",++ "line": 28,++ "value": ".15s",++ "text": "color: var(--accent-light, #60a5fa); background: color-mix(in srgb, var(--accent, #3b82f6) 15%, transparent); border: 1px solid color-mix(in srgb, var(--accent,"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/task.css",++ "line": 51,++ "value": ".3s",++ "text": ".ev-item { padding: 10px 12px; border-radius: 12px; border: 1px solid var(--border, #1f2d47); background: var(--bg-primary, #0a0f1c); scroll-margin-top: 80px; t"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/task.css",++ "line": 51,++ "value": ".3s",++ "text": ".ev-item { padding: 10px 12px; border-radius: 12px; border: 1px solid var(--border, #1f2d47); background: var(--bg-primary, #0a0f1c); scroll-margin-top: 80px; t"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/task.css",++ "line": 91,++ "value": ".15s",++ "text": ".follow-form { display: flex; align-items: flex-end; gap: 8px; padding: 6px 6px 6px 14px; border-radius: 16px; border: 1px solid var(--border-light, #2c3d5c); b"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/task.css",++ "line": 91,++ "value": ".15s",++ "text": ".follow-form { display: flex; align-items: flex-end; gap: 8px; padding: 6px 6px 6px 14px; border-radius: 16px; border: 1px solid var(--border-light, #2c3d5c); b"++ }++ ]++ },++ {++ "repo": "OpenVibe.Bench",++ "commit": "d18fd58d82f16de3284255acba49b8ea0999b01c",++ "files_scanned": 8,++ "counts": {++ "literal_duration": 2,++ "literal_curve": 2,++ "spinner": 0,++ "color_drift": 5,++ "missing_reduced_motion": 0,++ "total": 9++ },++ "score": 98.75,++ "findings_total": 9,++ "findings": [++ {++ "rule": "literal_duration",++ "file": "public/css/bench.css",++ "line": 189,++ "value": ".18s",++ "text": ".br-card-link { height: 100%; display: grid; align-content: start; gap: 8px; padding: 16px; border-radius: 18px; background: var(--bg-card, #0f1729); border: 1p"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/bench.css",++ "line": 189,++ "value": ".18s",++ "text": ".br-card-link { height: 100%; display: grid; align-content: start; gap: 8px; padding: 16px; border-radius: 18px; background: var(--bg-card, #0f1729); border: 1p"++ },++ {++ "rule": "literal_curve",++ "file": "public/css/bench.css",++ "line": 189,++ "value": "ease",++ "text": ".br-card-link { height: 100%; display: grid; align-content: start; gap: 8px; padding: 16px; border-radius: 18px; background: var(--bg-card, #0f1729); border: 1p"++ },++ {++ "rule": "literal_curve",++ "file": "public/css/bench.css",++ "line": 189,++ "value": "ease",++ "text": ".br-card-link { height: 100%; display: grid; align-content: start; gap: 8px; padding: 16px; border-radius: 18px; background: var(--bg-card, #0f1729); border: 1p"++ },++ {++ "rule": "color_drift",++ "file": "public/css/bench.css",++ "line": 617,++ "value": "#f59e0b",++ "text": ".br-contam { padding: 12px 14px; border-radius: 14px; background: color-mix(in srgb, #f59e0b 10%, var(--bg-card)); border: 1px solid color-mix(in srgb, #f59e0b "++ },++ {++ "rule": "color_drift",++ "file": "public/css/bench.css",++ "line": 617,++ "value": "#f59e0b",++ "text": ".br-contam { padding: 12px 14px; border-radius: 14px; background: color-mix(in srgb, #f59e0b 10%, var(--bg-card)); border: 1px solid color-mix(in srgb, #f59e0b "++ },++ {++ "rule": "color_drift",++ "file": "public/css/bench.css",++ "line": 619,++ "value": "#fde68a",++ "text": ".br-contam-chip { display: inline-block; margin-left: 8px; padding: 1px 7px; border-radius: 999px; font-size: .68rem; font-weight: 800; letter-spacing: .02em; c"++ },++ {++ "rule": "color_drift",++ "file": "public/css/bench.css",++ "line": 619,++ "value": "#f59e0b",++ "text": ".br-contam-chip { display: inline-block; margin-left: 8px; padding: 1px 7px; border-radius: 999px; font-size: .68rem; font-weight: 800; letter-spacing: .02em; c"++ },++ {++ "rule": "color_drift",++ "file": "public/css/bench.css",++ "line": 619,++ "value": "#f59e0b",++ "text": ".br-contam-chip { display: inline-block; margin-left: 8px; padding: 1px 7px; border-radius: 999px; font-size: .68rem; font-weight: 800; letter-spacing: .02em; c"++ }++ ]++ },++ {++ "repo": "OpenVibe.Blog",++ "commit": "f754247ce16df1a62cb517e4e420d3f2243fbaeb",++ "files_scanned": 1,++ "counts": {++ "literal_duration": 1,++ "literal_curve": 0,++ "spinner": 0,++ "color_drift": 2,++ "missing_reduced_motion": 0,++ "total": 3++ },++ "score": 99.65,++ "findings_total": 3,++ "findings": [++ {++ "rule": "literal_duration",++ "file": "public/css/blog.css",++ "line": 221,++ "value": ".15s",++ "text": ".toc-fold > summary::after, .series-more > summary::after { content: ''; margin-left: auto; width: 8px; height: 8px; border-right: 2px solid currentColor; borde"++ },++ {++ "rule": "color_drift",++ "file": "public/css/blog.css",++ "line": 60,++ "value": "rgba(34,197,94,.25)",++ "text": ".ov-diff ins { background: rgba(34,197,94,.25); text-decoration: none; }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/blog.css",++ "line": 61,++ "value": "rgba(239,68,68,.25)",++ "text": ".ov-diff del { background: rgba(239,68,68,.25); }"++ }++ ]++ },++ {++ "repo": "OpenVibe.Bot",++ "commit": "ffe8f47f7901d934173a7620a21fbea9b7ab5ae4",++ "files_scanned": 13,++ "counts": {++ "literal_duration": 12,++ "literal_curve": 5,++ "spinner": 0,++ "color_drift": 100,++ "missing_reduced_motion": 0,++ "total": 117++ },++ "score": 90.75,++ "findings_total": 117,++ "findings": [++ {++ "rule": "literal_duration",++ "file": "public/directory.css",++ "line": 31,++ "value": ".15s",++ "text": ".dir-card { background: var(--bg-secondary, #101828); border: 1px solid var(--border, #1f2d47); border-radius: 14px; display: flex; flex-direction: column; tran"++ },++ {++ "rule": "literal_duration",++ "file": "public/directory.css",++ "line": 115,++ "value": "1.4s",++ "text": ".sd-status[data-state=\"waiting\"] .sd-dot, .sd-status[data-state=\"installing\"] .sd-dot { background: #f59e0b; animation: sd-pulse 1.4s ease-in-out infinite; }"++ },++ {++ "rule": "literal_curve",++ "file": "public/directory.css",++ "line": 115,++ "value": "ease-in-out",++ "text": ".sd-status[data-state=\"waiting\"] .sd-dot, .sd-status[data-state=\"installing\"] .sd-dot { background: #f59e0b; animation: sd-pulse 1.4s ease-in-out infinite; }"++ },++ {++ "rule": "literal_duration",++ "file": "public/home.css",++ "line": 27,++ "value": ".15s",++ "text": ".hm-stats a { display: grid; gap: 2px; padding: 14px 16px; border-radius: 14px; border: 1px solid var(--border, rgba(255, 255, 255, .08)); background: var(--bg-"++ },++ {++ "rule": "literal_duration",++ "file": "public/home.css",++ "line": 34,++ "value": ".15s",++ "text": ".hm-card { display: flex; flex-direction: column; min-width: 0; border-radius: 16px; border: 1px solid var(--border, rgba(255, 255, 255, .08)); background: var("++ },++ {++ "rule": "literal_duration",++ "file": "public/panel.css",++ "line": 29,++ "value": "0.12s",++ "text": "touch-action: manipulation; -webkit-tap-highlight-color: transparent; transition: background 0.12s, border-color 0.12s, transform 0.06s;"++ },++ {++ "rule": "literal_duration",++ "file": "public/panel.css",++ "line": 29,++ "value": "0.12s",++ "text": "touch-action: manipulation; -webkit-tap-highlight-color: transparent; transition: background 0.12s, border-color 0.12s, transform 0.06s;"++ },++ {++ "rule": "literal_duration",++ "file": "public/panel.css",++ "line": 29,++ "value": "0.06s",++ "text": "touch-action: manipulation; -webkit-tap-highlight-color: transparent; transition: background 0.12s, border-color 0.12s, transform 0.06s;"++ },++ {++ "rule": "literal_duration",++ "file": "public/panel.css",++ "line": 71,++ "value": "1.2s",++ "text": ".estop-banner[data-latched=\"true\"] .estop-dot { background: #fff; box-shadow: 0 0 0 4px rgba(255, 255, 255, 0.25); animation: pulse 1.2s ease-in-out infinite; }"++ },++ {++ "rule": "literal_curve",++ "file": "public/panel.css",++ "line": 71,++ "value": "ease-in-out",++ "text": ".estop-banner[data-latched=\"true\"] .estop-dot { background: #fff; box-shadow: 0 0 0 4px rgba(255, 255, 255, 0.25); animation: pulse 1.2s ease-in-out infinite; }"++ },++ {++ "rule": "literal_duration",++ "file": "public/panel.css",++ "line": 83,++ "value": "1s",++ "text": ".online[data-state=\"connecting\"] .dot { animation: pulse 1s ease-in-out infinite; }"++ },++ {++ "rule": "literal_curve",++ "file": "public/panel.css",++ "line": 83,++ "value": "ease-in-out",++ "text": ".online[data-state=\"connecting\"] .dot { animation: pulse 1s ease-in-out infinite; }"++ },++ {++ "rule": "literal_duration",++ "file": "public/panel.css",++ "line": 123,++ "value": "0.12s",++ "text": "box-shadow: 0 6px 14px rgba(0, 0, 0, 0.55); transition: transform 0.12s ease-out;"++ },++ {++ "rule": "color_drift",++ "file": "public/directory.css",++ "line": 43,++ "value": "#a78bfa",++ "text": ".dir-browser { color: #a78bfa; } .dir-phone { color: #f472b6; } .dir-onboard { color: #22c55e; } .dir-bridge { color: #f59e0b; } .dir-server { color: #38bdf8; }"++ },++ {++ "rule": "color_drift",++ "file": "public/directory.css",++ "line": 43,++ "value": "#f472b6",++ "text": ".dir-browser { color: #a78bfa; } .dir-phone { color: #f472b6; } .dir-onboard { color: #22c55e; } .dir-bridge { color: #f59e0b; } .dir-server { color: #38bdf8; }"++ },++ {++ "rule": "color_drift",++ "file": "public/directory.css",++ "line": 43,++ "value": "#22c55e",++ "text": ".dir-browser { color: #a78bfa; } .dir-phone { color: #f472b6; } .dir-onboard { color: #22c55e; } .dir-bridge { color: #f59e0b; } .dir-server { color: #38bdf8; }"++ },++ {++ "rule": "color_drift",++ "file": "public/directory.css",++ "line": 43,++ "value": "#f59e0b",++ "text": ".dir-browser { color: #a78bfa; } .dir-phone { color: #f472b6; } .dir-onboard { color: #22c55e; } .dir-bridge { color: #f59e0b; } .dir-server { color: #38bdf8; }"++ },++ {++ "rule": "color_drift",++ "file": "public/docs.css",++ "line": 37,++ "value": "rgba(239, 68, 68, .45)",++ "text": ".docs-main .error { border: 1px solid rgba(239, 68, 68, .45); background: rgba(239, 68, 68, .08); border-radius: 10px; padding: 12px 14px; }"++ },++ {++ "rule": "color_drift",++ "file": "public/docs.css",++ "line": 37,++ "value": "rgba(239, 68, 68, .08)",++ "text": ".docs-main .error { border: 1px solid rgba(239, 68, 68, .45); background: rgba(239, 68, 68, .08); border-radius: 10px; padding: 12px 14px; }"++ },++ {++ "rule": "color_drift",++ "file": "public/docs.css",++ "line": 38,++ "value": "#22c55e",++ "text": ".docs-main [role=\"status\"] strong { color: #22c55e; }"++ },++ {++ "rule": "color_drift",++ "file": "public/home.css",++ "line": 13,++ "value": "#0b1220",++ "text": ".hm-term { border-radius: 16px; background: #0b1220; color: #e6edf7; border: 1px solid rgba(148, 163, 184, .25); box-shadow: var(--shadow-lg, 0 18px 50px rgba(0"++ },++ {++ "rule": "color_drift",++ "file": "public/home.css",++ "line": 13,++ "value": "#e6edf7",++ "text": ".hm-term { border-radius: 16px; background: #0b1220; color: #e6edf7; border: 1px solid rgba(148, 163, 184, .25); box-shadow: var(--shadow-lg, 0 18px 50px rgba(0"++ },++ {++ "rule": "color_drift",++ "file": "public/home.css",++ "line": 13,++ "value": "rgba(148, 163, 184, .25)",++ "text": ".hm-term { border-radius: 16px; background: #0b1220; color: #e6edf7; border: 1px solid rgba(148, 163, 184, .25); box-shadow: var(--shadow-lg, 0 18px 50px rgba(0"++ },++ {++ "rule": "color_drift",++ "file": "public/home.css",++ "line": 14,++ "value": "rgba(148, 163, 184, .1)",++ "text": ".hm-term-top { display: flex; align-items: center; gap: 6px; margin: 0; padding: 10px 14px; background: rgba(148, 163, 184, .1); font-size: 12.5px; }"++ },++ {++ "rule": "color_drift",++ "file": "public/home.css",++ "line": 15,++ "value": "#f87171",++ "text": ".hm-term-top span { width: 10px; height: 10px; border-radius: 50%; background: #f87171; }"++ }++ ]++ },++ {++ "repo": "OpenVibe.Cam",++ "commit": "0ad257ee0decb99121d649162f58e21cf6891c2d",++ "files_scanned": 3,++ "counts": {++ "literal_duration": 0,++ "literal_curve": 0,++ "spinner": 0,++ "color_drift": 27,++ "missing_reduced_motion": 0,++ "total": 27++ },++ "score": 98.65,++ "findings_total": 27,++ "findings": [++ {++ "rule": "color_drift",++ "file": "public/css/cam.css",++ "line": 65,++ "value": "#05070c",++ "text": ".cm-stage { position: relative; overflow: hidden; aspect-ratio: 4 / 3; border-radius: 22px; background: #05070c; box-shadow: inset 0 0 0 1px rgba(255, 255, 255,"++ },++ {++ "rule": "color_drift",++ "file": "public/css/cam.css",++ "line": 65,++ "value": "rgba(255, 255, 255, .06)",++ "text": ".cm-stage { position: relative; overflow: hidden; aspect-ratio: 4 / 3; border-radius: 22px; background: #05070c; box-shadow: inset 0 0 0 1px rgba(255, 255, 255,"++ },++ {++ "rule": "color_drift",++ "file": "public/css/cam.css",++ "line": 65,++ "value": "rgba(0, 0, 0, .8)",++ "text": ".cm-stage { position: relative; overflow: hidden; aspect-ratio: 4 / 3; border-radius: 22px; background: #05070c; box-shadow: inset 0 0 0 1px rgba(255, 255, 255,"++ },++ {++ "rule": "color_drift",++ "file": "public/css/cam.css",++ "line": 72,++ "value": "rgba(255, 255, 255, .55)",++ "text": "color: rgba(255, 255, 255, .55); }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/cam.css",++ "line": 74,++ "value": "rgba(255, 255, 255, .35)",++ "text": "background: linear-gradient(90deg, transparent calc(33.333% - .5px), rgba(255, 255, 255, .35) calc(33.333% - .5px) calc(33.333% + .5px), transparent calc(33.333"++ },++ {++ "rule": "color_drift",++ "file": "public/css/cam.css",++ "line": 74,++ "value": "rgba(255, 255, 255, .35)",++ "text": "background: linear-gradient(90deg, transparent calc(33.333% - .5px), rgba(255, 255, 255, .35) calc(33.333% - .5px) calc(33.333% + .5px), transparent calc(33.333"++ },++ {++ "rule": "color_drift",++ "file": "public/css/cam.css",++ "line": 74,++ "value": "rgba(255, 255, 255, .35)",++ "text": "background: linear-gradient(90deg, transparent calc(33.333% - .5px), rgba(255, 255, 255, .35) calc(33.333% - .5px) calc(33.333% + .5px), transparent calc(33.333"++ },++ {++ "rule": "color_drift",++ "file": "public/css/cam.css",++ "line": 74,++ "value": "rgba(255, 255, 255, .35)",++ "text": "background: linear-gradient(90deg, transparent calc(33.333% - .5px), rgba(255, 255, 255, .35) calc(33.333% - .5px) calc(33.333% + .5px), transparent calc(33.333"++ },++ {++ "rule": "color_drift",++ "file": "public/css/cam.css",++ "line": 129,++ "value": "rgba(0, 0, 0, .7)",++ "text": ".cm-live-frame { position: relative; overflow: hidden; border-radius: 14px; aspect-ratio: 4 / 3; box-shadow: 0 24px 50px -30px rgba(0, 0, 0, .7); }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/cam.css",++ "line": 131,++ "value": "#fff",++ "text": ".cm-live-badge { position: absolute; top: 10px; left: 10px; padding: 3px 8px; border-radius: 6px; background: var(--ovk-live, #ef4444); color: #fff;"++ },++ {++ "rule": "color_drift",++ "file": "public/css/cam.css",++ "line": 134,++ "value": "rgba(10, 12, 18, .66)",++ "text": "background: rgba(10, 12, 18, .66); color: #fff; box-shadow: 0 0 0 2px rgba(255, 255, 255, .7);"++ },++ {++ "rule": "color_drift",++ "file": "public/css/cam.css",++ "line": 134,++ "value": "#fff",++ "text": "background: rgba(10, 12, 18, .66); color: #fff; box-shadow: 0 0 0 2px rgba(255, 255, 255, .7);"++ },++ {++ "rule": "color_drift",++ "file": "public/css/cam.css",++ "line": 134,++ "value": "rgba(255, 255, 255, .7)",++ "text": "background: rgba(10, 12, 18, .66); color: #fff; box-shadow: 0 0 0 2px rgba(255, 255, 255, .7);"++ },++ {++ "rule": "color_drift",++ "file": "public/css/cam.css",++ "line": 137,++ "value": "#fff",++ "text": ".cm-live-flash { position: absolute; inset: 0; background: #fff; opacity: 0;"++ },++ {++ "rule": "color_drift",++ "file": "public/css/cam.css",++ "line": 147,++ "value": "rgba(0, 0, 0, .6)",++ "text": "background: var(--ovk-panel); border: 1px solid var(--ovk-line); box-shadow: 0 24px 50px -30px rgba(0, 0, 0, .6); }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/cam.css",++ "line": 164,++ "value": "rgba(0, 0, 0, .7)",++ "text": ".cm-f-main { position: relative; overflow: hidden; border-radius: 14px; aspect-ratio: 4 / 3; box-shadow: 0 24px 50px -30px rgba(0, 0, 0, .7); }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/cam.css",++ "line": 165,++ "value": "rgba(10, 12, 18, .7)",++ "text": ".cm-f-saved { position: absolute; left: 10px; bottom: 10px; padding: 5px 10px; border-radius: 999px; background: rgba(10, 12, 18, .7); color: #fff;"++ },++ {++ "rule": "color_drift",++ "file": "public/css/cam.css",++ "line": 165,++ "value": "#fff",++ "text": ".cm-f-saved { position: absolute; left: 10px; bottom: 10px; padding: 5px 10px; border-radius: 999px; background: rgba(10, 12, 18, .7); color: #fff;"++ },++ {++ "rule": "color_drift",++ "file": "public/css/cam.css",++ "line": 180,++ "value": "rgba(0, 0, 0, .75)",++ "text": "box-shadow: 0 40px 80px -40px rgba(0, 0, 0, .75), 0 0 0 1px color-mix(in srgb, var(--room-accent) 30%, transparent); }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/cam.css",++ "line": 182,++ "value": "rgba(255, 255, 255, .9)",++ "text": ".cm-vf-c { position: absolute; width: 28px; height: 28px; border: 0 solid rgba(255, 255, 255, .9); }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/cam.css",++ "line": 187,++ "value": "#ffd27a",++ "text": ".cm-vf-focus { position: absolute; left: 60%; top: 42%; width: 64px; height: 64px; margin: -32px 0 0 -32px; border: 2px solid #ffd27a; border-radius: 10px;"++ },++ {++ "rule": "color_drift",++ "file": "public/css/cam.css",++ "line": 191,++ "value": "rgba(10, 12, 18, .6)",++ "text": "background: rgba(10, 12, 18, .6); color: #fff; font-size: 12px; }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/cam.css",++ "line": 191,++ "value": "#fff",++ "text": "background: rgba(10, 12, 18, .6); color: #fff; font-size: 12px; }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/cam.css",++ "line": 192,++ "value": "#ffd27a",++ "text": ".cm-vf-top b { color: #ffd27a; }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/cam.css",++ "line": 193,++ "value": "#fff",++ "text": ".cm-vf-shutter { position: absolute; left: 50%; bottom: 18px; width: 54px; height: 54px; margin-left: -27px; border-radius: 50%; background: #fff;"++ }++ ]++ },++ {++ "repo": "OpenVibe.Casino",++ "commit": "fbca80eca82d056b389826b85233b7278c2c635d",++ "files_scanned": 6,++ "counts": {++ "literal_duration": 0,++ "literal_curve": 0,++ "spinner": 0,++ "color_drift": 0,++ "missing_reduced_motion": 0,++ "total": 0++ },++ "score": 100,++ "findings_total": 0,++ "findings": []++ },++ {++ "repo": "OpenVibe.Chat",++ "commit": "20eb7b56ec9b8261c365056fef38627b9c4f0453",++ "files_scanned": 16,++ "counts": {++ "literal_duration": 29,++ "literal_curve": 7,++ "spinner": 0,++ "color_drift": 65,++ "missing_reduced_motion": 0,++ "total": 101++ },++ "score": 87.75,++ "findings_total": 101,++ "findings": [++ {++ "rule": "literal_duration",++ "file": "public/web/chat.css",++ "line": 123,++ "value": ".25s",++ "text": "@media (prefers-reduced-motion: no-preference) { .oc-ring:not(.oc-ring-gone) { animation: oc-ring-in .25s ease-out; } @keyframes oc-ring-in { from { transform: "++ },++ {++ "rule": "literal_curve",++ "file": "public/web/chat.css",++ "line": 123,++ "value": "ease-out",++ "text": "@media (prefers-reduced-motion: no-preference) { .oc-ring:not(.oc-ring-gone) { animation: oc-ring-in .25s ease-out; } @keyframes oc-ring-in { from { transform: "++ },++ {++ "rule": "literal_duration",++ "file": "public/web/chat.css",++ "line": 151,++ "value": ".15s",++ "text": ".oc-srv-icon { position: relative; display: grid; place-items: center; width: 44px; height: 44px; flex: 0 0 auto; border-radius: 50%; background: hsl(var(--oc-h"++ },++ {++ "rule": "literal_duration",++ "file": "public/web/chat.css",++ "line": 151,++ "value": ".15s",++ "text": ".oc-srv-icon { position: relative; display: grid; place-items: center; width: 44px; height: 44px; flex: 0 0 auto; border-radius: 50%; background: hsl(var(--oc-h"++ },++ {++ "rule": "literal_curve",++ "file": "public/web/chat.css",++ "line": 151,++ "value": "ease",++ "text": ".oc-srv-icon { position: relative; display: grid; place-items: center; width: 44px; height: 44px; flex: 0 0 auto; border-radius: 50%; background: hsl(var(--oc-h"++ },++ {++ "rule": "literal_curve",++ "file": "public/web/chat.css",++ "line": 151,++ "value": "ease",++ "text": ".oc-srv-icon { position: relative; display: grid; place-items: center; width: 44px; height: 44px; flex: 0 0 auto; border-radius: 50%; background: hsl(var(--oc-h"++ },++ {++ "rule": "literal_duration",++ "file": "public/web/chat.css",++ "line": 356,++ "value": "1.6s",++ "text": ".oc-task-running .oc-task-state { background: var(--oc-accent); color: var(--oc-on-accent); animation: oc-work 1.6s ease-in-out infinite; }"++ },++ {++ "rule": "literal_duration",++ "file": "public/web/embed.css",++ "line": 37,++ "value": "1.2s",++ "text": ".em-dots { display: inline-flex; gap: 4px; } .em-dots i { width: 6px; height: 6px; border-radius: 50%; background: currentColor; opacity: .4; animation: emDot 1"++ },++ {++ "rule": "literal_duration",++ "file": "public/web/feed-view.css",++ "line": 9,++ "value": "2s",++ "text": ".fv-live .fv-dot { background: var(--fv-ok); box-shadow: 0 0 0 0 color-mix(in srgb, var(--fv-ok) 60%, transparent); animation: fvPulse 2s infinite; }"++ },++ {++ "rule": "literal_duration",++ "file": "public/web/feed-view.css",++ "line": 15,++ "value": ".15s",++ "text": ".fv-chev { color: var(--fv-muted); transition: transform .15s; }"++ },++ {++ "rule": "literal_duration",++ "file": "public/web/vibe.css",++ "line": 28,++ "value": ".15s",++ "text": "transition: background .15s, border-color .15s;"++ },++ {++ "rule": "literal_duration",++ "file": "public/web/vibe.css",++ "line": 28,++ "value": ".15s",++ "text": "transition: background .15s, border-color .15s;"++ },++ {++ "rule": "literal_duration",++ "file": "public/web/vibe.css",++ "line": 42,++ "value": ".15s",++ "text": ".vb-how summary::before { content: \"▸\"; display: inline-block; margin-right: 6px; transition: transform .15s; }"++ },++ {++ "rule": "literal_duration",++ "file": "public/web/vibe.css",++ "line": 140,++ "value": "1.2s",++ "text": ".vb-dots i { width: 7px; height: 7px; border-radius: 50%; background: var(--oc-muted); opacity: .35; animation: vbDot 1.2s infinite ease-in-out; }"++ },++ {++ "rule": "literal_curve",++ "file": "public/web/vibe.css",++ "line": 140,++ "value": "ease-in-out",++ "text": ".vb-dots i { width: 7px; height: 7px; border-radius: 50%; background: var(--oc-muted); opacity: .35; animation: vbDot 1.2s infinite ease-in-out; }"++ },++ {++ "rule": "literal_duration",++ "file": "public/web/vibe.css",++ "line": 178,++ "value": ".15s",++ "text": "transition: background .15s, border-color .15s, transform .15s;"++ },++ {++ "rule": "literal_duration",++ "file": "public/web/vibe.css",++ "line": 178,++ "value": ".15s",++ "text": "transition: background .15s, border-color .15s, transform .15s;"++ },++ {++ "rule": "literal_duration",++ "file": "public/web/widget.js",++ "line": 49,++ "value": ".15s",++ "text": "'background:var(--c);color:#fff;box-shadow:0 10px 30px rgba(0,0,0,.28);display:grid;place-items:center;transition:transform .15s}' +"++ },++ {++ "rule": "literal_duration",++ "file": "public/web/widget.js",++ "line": 55,++ "value": ".16s",++ "text": "'box-shadow:0 24px 70px rgba(0,0,0,.35);background:#0a0f1c;opacity:0;transform:translateY(10px);pointer-events:none;transition:opacity .16s,transform .2s}' +"++ },++ {++ "rule": "literal_duration",++ "file": "public/web/widget.js",++ "line": 55,++ "value": ".2s",++ "text": "'box-shadow:0 24px 70px rgba(0,0,0,.35);background:#0a0f1c;opacity:0;transform:translateY(10px);pointer-events:none;transition:opacity .16s,transform .2s}' +"++ },++ {++ "rule": "color_drift",++ "file": "public/web/embed.css",++ "line": 3,++ "value": "--bg #ffffff",++ "text": ":root { --bg: #ffffff; --panel: #f4f6fa; --text: #101828; --muted: #5b6577; --line: #e3e7ef; --mine: #eef2fb; color-scheme: light; }"++ },++ {++ "rule": "color_drift",++ "file": "public/web/embed.css",++ "line": 3,++ "value": "--panel #f4f6fa",++ "text": ":root { --bg: #ffffff; --panel: #f4f6fa; --text: #101828; --muted: #5b6577; --line: #e3e7ef; --mine: #eef2fb; color-scheme: light; }"++ },++ {++ "rule": "color_drift",++ "file": "public/web/embed.css",++ "line": 3,++ "value": "--text #101828",++ "text": ":root { --bg: #ffffff; --panel: #f4f6fa; --text: #101828; --muted: #5b6577; --line: #e3e7ef; --mine: #eef2fb; color-scheme: light; }"++ },++ {++ "rule": "color_drift",++ "file": "public/web/embed.css",++ "line": 3,++ "value": "--muted #5b6577",++ "text": ":root { --bg: #ffffff; --panel: #f4f6fa; --text: #101828; --muted: #5b6577; --line: #e3e7ef; --mine: #eef2fb; color-scheme: light; }"++ },++ {++ "rule": "color_drift",++ "file": "public/web/embed.css",++ "line": 3,++ "value": "--line #e3e7ef",++ "text": ":root { --bg: #ffffff; --panel: #f4f6fa; --text: #101828; --muted: #5b6577; --line: #e3e7ef; --mine: #eef2fb; color-scheme: light; }"++ }++ ]++ },++ {++ "repo": "OpenVibe.Codes",++ "commit": "302722579e101e3e1dfe468ae875cdb85e4bd781",++ "files_scanned": 8,++ "counts": {++ "literal_duration": 13,++ "literal_curve": 6,++ "spinner": 0,++ "color_drift": 7,++ "missing_reduced_motion": 0,++ "total": 26++ },++ "score": 94.9,++ "findings_total": 26,++ "findings": [++ {++ "rule": "literal_duration",++ "file": "public/css/c4.css",++ "line": 51,++ "value": ".24s",++ "text": ".c4-progress-content{grid-template-rows:0fr;opacity:0;overflow:hidden;transition:grid-template-rows .24s ease,opacity .24s ease}.c4-progress-content.is-open,.c4"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/c4.css",++ "line": 51,++ "value": ".24s",++ "text": ".c4-progress-content{grid-template-rows:0fr;opacity:0;overflow:hidden;transition:grid-template-rows .24s ease,opacity .24s ease}.c4-progress-content.is-open,.c4"++ },++ {++ "rule": "literal_curve",++ "file": "public/css/c4.css",++ "line": 51,++ "value": "ease",++ "text": ".c4-progress-content{grid-template-rows:0fr;opacity:0;overflow:hidden;transition:grid-template-rows .24s ease,opacity .24s ease}.c4-progress-content.is-open,.c4"++ },++ {++ "rule": "literal_curve",++ "file": "public/css/c4.css",++ "line": 51,++ "value": "ease",++ "text": ".c4-progress-content{grid-template-rows:0fr;opacity:0;overflow:hidden;transition:grid-template-rows .24s ease,opacity .24s ease}.c4-progress-content.is-open,.c4"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/chat.css",++ "line": 24,++ "value": "1.6s",++ "text": ".ch-dot.is-busy{background:var(--ch-accent);animation:ch-breathe 1.6s ease-in-out infinite}"++ },++ {++ "rule": "literal_curve",++ "file": "public/css/chat.css",++ "line": 24,++ "value": "ease-in-out",++ "text": ".ch-dot.is-busy{background:var(--ch-accent);animation:ch-breathe 1.6s ease-in-out infinite}"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/chat.css",++ "line": 69,++ "value": ".2s",++ "text": ".ch-caret{width:14px;height:14px;transition:rotate .2s}"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/chat.css",++ "line": 77,++ "value": ".35s",++ "text": ".ch-step.is-new{animation:ch-in .35s ease-out}"++ },++ {++ "rule": "literal_curve",++ "file": "public/css/chat.css",++ "line": 77,++ "value": "ease-out",++ "text": ".ch-step.is-new{animation:ch-in .35s ease-out}"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/chat.css",++ "line": 83,++ "value": "1s",++ "text": ".ch-dots i{width:6px;height:6px;border-radius:50%;background:var(--ch-accent);animation:ch-hop 1s ease-in-out infinite}"++ },++ {++ "rule": "literal_curve",++ "file": "public/css/chat.css",++ "line": 83,++ "value": "ease-in-out",++ "text": ".ch-dots i{width:6px;height:6px;border-radius:50%;background:var(--ch-accent);animation:ch-hop 1s ease-in-out infinite}"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/chat.css",++ "line": 150,++ "value": ".2s",++ "text": ".ch-send{display:inline-grid;place-items:center;width:36px;height:36px;flex:none;border:0;border-radius:50%;background:var(--ch-accent);color:var(--ch-on-accent"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/chat.css",++ "line": 150,++ "value": ".15s",++ "text": ".ch-send{display:inline-grid;place-items:center;width:36px;height:36px;flex:none;border:0;border-radius:50%;background:var(--ch-accent);color:var(--ch-on-accent"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/chat.css",++ "line": 172,++ "value": ".15s",++ "text": ".ch-mode{position:relative;display:grid;grid-template-columns:auto minmax(0,1fr);align-content:start;gap:2px 11px;padding:12px 14px;border:1px solid var(--ch-li"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/chat.css",++ "line": 172,++ "value": ".15s",++ "text": ".ch-mode{position:relative;display:grid;grid-template-columns:auto minmax(0,1fr);align-content:start;gap:2px 11px;padding:12px 14px;border:1px solid var(--ch-li"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/chat.css",++ "line": 212,++ "value": ".15s",++ "text": ".ch-idea{display:inline-flex;align-items:center;gap:8px;padding:8px 14px 8px 11px;border-radius:14px;border:1px solid var(--ch-line);background:var(--ch-card);c"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/chat.css",++ "line": 212,++ "value": ".15s",++ "text": ".ch-idea{display:inline-flex;align-items:center;gap:8px;padding:8px 14px 8px 11px;border-radius:14px;border:1px solid var(--ch-line);background:var(--ch-card);c"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/chat.css",++ "line": 248,++ "value": ".25s",++ "text": ".ch-side{position:fixed;z-index:10060;top:0;right:0;bottom:0;width:min(340px,88vw);translate:100% 0;transition:translate .25s ease;box-shadow:-12px 0 32px rgba("++ },++ {++ "rule": "literal_curve",++ "file": "public/css/chat.css",++ "line": 248,++ "value": "ease",++ "text": ".ch-side{position:fixed;z-index:10060;top:0;right:0;bottom:0;width:min(340px,88vw);translate:100% 0;transition:translate .25s ease;box-shadow:-12px 0 32px rgba("++ },++ {++ "rule": "color_drift",++ "file": "public/css/chat.css",++ "line": 132,++ "value": "rgba(0,0,0,.14)",++ "text": ".ch-composer{position:relative;margin:0 max(16px,calc((100% - var(--ch-col)) / 2)) 14px;padding:10px 10px 8px 16px;border:1px solid var(--ch-line);border-radius"++ },++ {++ "rule": "color_drift",++ "file": "public/css/chat.css",++ "line": 207,++ "value": "#000",++ "text": ".ch-spark-view{flex:1;min-width:0;overflow:hidden;-webkit-mask-image:linear-gradient(90deg,transparent,#000 5%,#000 95%,transparent);mask-image:linear-gradient("++ },++ {++ "rule": "color_drift",++ "file": "public/css/chat.css",++ "line": 207,++ "value": "#000",++ "text": ".ch-spark-view{flex:1;min-width:0;overflow:hidden;-webkit-mask-image:linear-gradient(90deg,transparent,#000 5%,#000 95%,transparent);mask-image:linear-gradient("++ },++ {++ "rule": "color_drift",++ "file": "public/css/chat.css",++ "line": 207,++ "value": "#000",++ "text": ".ch-spark-view{flex:1;min-width:0;overflow:hidden;-webkit-mask-image:linear-gradient(90deg,transparent,#000 5%,#000 95%,transparent);mask-image:linear-gradient("++ },++ {++ "rule": "color_drift",++ "file": "public/css/chat.css",++ "line": 207,++ "value": "#000",++ "text": ".ch-spark-view{flex:1;min-width:0;overflow:hidden;-webkit-mask-image:linear-gradient(90deg,transparent,#000 5%,#000 95%,transparent);mask-image:linear-gradient("++ },++ {++ "rule": "color_drift",++ "file": "public/css/chat.css",++ "line": 248,++ "value": "rgba(0,0,0,.25)",++ "text": ".ch-side{position:fixed;z-index:10060;top:0;right:0;bottom:0;width:min(340px,88vw);translate:100% 0;transition:translate .25s ease;box-shadow:-12px 0 32px rgba("++ }++ ]++ },++ {++ "repo": "OpenVibe.Community",++ "commit": "0962b43798e79204632f3c74ee82cfe67056f04f",++ "files_scanned": 3,++ "counts": {++ "literal_duration": 7,++ "literal_curve": 0,++ "spinner": 0,++ "color_drift": 30,++ "missing_reduced_motion": 0,++ "total": 37++ },++ "score": 96.75,++ "findings_total": 37,++ "findings": [++ {++ "rule": "literal_duration",++ "file": "public/css/community.css",++ "line": 31,++ "value": ".15s",++ "text": ".btn { display: inline-flex; align-items: center; gap: 7px; padding: 9px 15px; border-radius: 9px; border: 1px solid var(--border-light); background: var(--bg-c"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/community.css",++ "line": 31,++ "value": ".15s",++ "text": ".btn { display: inline-flex; align-items: center; gap: 7px; padding: 9px 15px; border-radius: 9px; border: 1px solid var(--border-light); background: var(--bg-c"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/community.css",++ "line": 31,++ "value": ".1s",++ "text": ".btn { display: inline-flex; align-items: center; gap: 7px; padding: 9px 15px; border-radius: 9px; border: 1px solid var(--border-light); background: var(--bg-c"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/community.css",++ "line": 58,++ "value": ".15s",++ "text": ".card { background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; display: flex; flex-direction: column; tran"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/community.css",++ "line": 58,++ "value": ".15s",++ "text": ".card { background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; display: flex; flex-direction: column; tran"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/community.css",++ "line": 58,++ "value": ".15s",++ "text": ".card { background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; display: flex; flex-direction: column; tran"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/community.css",++ "line": 113,++ "value": ".2s",++ "text": ".shot.nsfw img { filter: blur(20px); transition: filter .2s; }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/community.css",++ "line": 5,++ "value": "--bg-primary #0a0f1c",++ "text": "--bg-primary: #0a0f1c; --bg-secondary: #101828; --bg-tertiary: #172238; --bg-card: #131c2e; --bg-hover: #1c2a44; --bg-input: #0d1424;"++ },++ {++ "rule": "color_drift",++ "file": "public/css/community.css",++ "line": 5,++ "value": "--bg-secondary #101828",++ "text": "--bg-primary: #0a0f1c; --bg-secondary: #101828; --bg-tertiary: #172238; --bg-card: #131c2e; --bg-hover: #1c2a44; --bg-input: #0d1424;"++ },++ {++ "rule": "color_drift",++ "file": "public/css/community.css",++ "line": 5,++ "value": "--bg-tertiary #172238",++ "text": "--bg-primary: #0a0f1c; --bg-secondary: #101828; --bg-tertiary: #172238; --bg-card: #131c2e; --bg-hover: #1c2a44; --bg-input: #0d1424;"++ },++ {++ "rule": "color_drift",++ "file": "public/css/community.css",++ "line": 5,++ "value": "--bg-card #131c2e",++ "text": "--bg-primary: #0a0f1c; --bg-secondary: #101828; --bg-tertiary: #172238; --bg-card: #131c2e; --bg-hover: #1c2a44; --bg-input: #0d1424;"++ },++ {++ "rule": "color_drift",++ "file": "public/css/community.css",++ "line": 5,++ "value": "--bg-hover #1c2a44",++ "text": "--bg-primary: #0a0f1c; --bg-secondary: #101828; --bg-tertiary: #172238; --bg-card: #131c2e; --bg-hover: #1c2a44; --bg-input: #0d1424;"++ },++ {++ "rule": "color_drift",++ "file": "public/css/community.css",++ "line": 5,++ "value": "--bg-input #0d1424",++ "text": "--bg-primary: #0a0f1c; --bg-secondary: #101828; --bg-tertiary: #172238; --bg-card: #131c2e; --bg-hover: #1c2a44; --bg-input: #0d1424;"++ },++ {++ "rule": "color_drift",++ "file": "public/css/community.css",++ "line": 6,++ "value": "--text-primary #e6edf7",++ "text": "--text-primary: #e6edf7; --text-secondary: #96a7c2; --text-muted: #7386a3;"++ },++ {++ "rule": "color_drift",++ "file": "public/css/community.css",++ "line": 6,++ "value": "--text-secondary #96a7c2",++ "text": "--text-primary: #e6edf7; --text-secondary: #96a7c2; --text-muted: #7386a3;"++ },++ {++ "rule": "color_drift",++ "file": "public/css/community.css",++ "line": 6,++ "value": "--text-muted #7386a3",++ "text": "--text-primary: #e6edf7; --text-secondary: #96a7c2; --text-muted: #7386a3;"++ },++ {++ "rule": "color_drift",++ "file": "public/css/community.css",++ "line": 7,++ "value": "--accent #3b82f6",++ "text": "--accent: #3b82f6; --accent-light: #60a5fa; --accent-dark: #1d4ed8; --accent-rgb: 59,130,246; --on-accent: #ffffff;"++ },++ {++ "rule": "color_drift",++ "file": "public/css/community.css",++ "line": 7,++ "value": "--accent-light #60a5fa",++ "text": "--accent: #3b82f6; --accent-light: #60a5fa; --accent-dark: #1d4ed8; --accent-rgb: 59,130,246; --on-accent: #ffffff;"++ },++ {++ "rule": "color_drift",++ "file": "public/css/community.css",++ "line": 7,++ "value": "--accent-dark #1d4ed8",++ "text": "--accent: #3b82f6; --accent-light: #60a5fa; --accent-dark: #1d4ed8; --accent-rgb: 59,130,246; --on-accent: #ffffff;"++ },++ {++ "rule": "color_drift",++ "file": "public/css/community.css",++ "line": 7,++ "value": "--on-accent #ffffff",++ "text": "--accent: #3b82f6; --accent-light: #60a5fa; --accent-dark: #1d4ed8; --accent-rgb: 59,130,246; --on-accent: #ffffff;"++ },++ {++ "rule": "color_drift",++ "file": "public/css/community.css",++ "line": 8,++ "value": "--border #1f2d47",++ "text": "--border: #1f2d47; --border-light: #2c3d5c;"++ },++ {++ "rule": "color_drift",++ "file": "public/css/community.css",++ "line": 8,++ "value": "--border-light #2c3d5c",++ "text": "--border: #1f2d47; --border-light: #2c3d5c;"++ },++ {++ "rule": "color_drift",++ "file": "public/css/community.css",++ "line": 9,++ "value": "--success #22c55e",++ "text": "--success: #22c55e; --warning: #f59e0b; --danger: #ef4444; --info: #38bdf8;"++ },++ {++ "rule": "color_drift",++ "file": "public/css/community.css",++ "line": 9,++ "value": "--warning #f59e0b",++ "text": "--success: #22c55e; --warning: #f59e0b; --danger: #ef4444; --info: #38bdf8;"++ },++ {++ "rule": "color_drift",++ "file": "public/css/community.css",++ "line": 9,++ "value": "--danger #ef4444",++ "text": "--success: #22c55e; --warning: #f59e0b; --danger: #ef4444; --info: #38bdf8;"++ }++ ]++ },++ {++ "repo": "OpenVibe.Coupons",++ "commit": "60c410db42097b4e8e6352e6ffac0b146d08afa7",++ "files_scanned": 1,++ "counts": {++ "literal_duration": 1,++ "literal_curve": 0,++ "spinner": 0,++ "color_drift": 0,++ "missing_reduced_motion": 0,++ "total": 1++ },++ "score": 99.75,++ "findings_total": 1,++ "findings": [++ {++ "rule": "literal_duration",++ "file": "public/css/coupons.css",++ "line": 75,++ "value": ".15s",++ "text": ".cp-stats a { display: flex; flex-direction: column; gap: 2px; height: 100%; padding: 14px 16px; border-radius: 14px; border: 1px solid var(--border, #1f2d47); "++ }++ ]++ },++ {++ "repo": "OpenVibe.Deals",++ "commit": "73e9cac7b3a1f88e05184013267ddd3bec0eff37",++ "files_scanned": 2,++ "counts": {++ "literal_duration": 4,++ "literal_curve": 0,++ "spinner": 0,++ "color_drift": 2,++ "missing_reduced_motion": 0,++ "total": 6++ },++ "score": 98.9,++ "findings_total": 6,++ "findings": [++ {++ "rule": "literal_duration",++ "file": "public/css/deals.css",++ "line": 51,++ "value": ".15s",++ "text": ".deal-item { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 8px 18px; padding: 14px 16px; border-radius: 14px; border: 1px solid var(--border, "++ },++ {++ "rule": "literal_duration",++ "file": "public/css/deals.css",++ "line": 51,++ "value": ".15s",++ "text": ".deal-item { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 8px 18px; padding: 14px 16px; border-radius: 14px; border: 1px solid var(--border, "++ },++ {++ "rule": "literal_duration",++ "file": "public/css/deals.css",++ "line": 139,++ "value": ".15s",++ "text": ".hm-stats a { display: flex; flex-direction: column; gap: 2px; height: 100%; padding: 14px 16px; border-radius: 14px; border: 1px solid var(--border, #1f2d47); "++ },++ {++ "rule": "literal_duration",++ "file": "public/css/deals.css",++ "line": 151,++ "value": ".15s",++ "text": ".hm-card { display: flex; flex-direction: column; gap: 8px; min-width: 0; padding: 14px 16px; border-radius: 14px; border: 1px solid var(--border, #1f2d47); bac"++ },++ {++ "rule": "color_drift",++ "file": "public/css/deal-cards.css",++ "line": 27,++ "value": "#f5b301",++ "text": ".dl-band--pick { color: #f5b301; background: color-mix(in srgb, #f5b301 16%, transparent); }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/deal-cards.css",++ "line": 27,++ "value": "#f5b301",++ "text": ".dl-band--pick { color: #f5b301; background: color-mix(in srgb, #f5b301 16%, transparent); }"++ }++ ]++ },++ {++ "repo": "OpenVibe.Design",++ "commit": "b40d0aa2ab3265977319cc8295e01d83228cfb8e",++ "files_scanned": 41,++ "counts": {++ "literal_duration": 143,++ "literal_curve": 59,++ "spinner": 0,++ "color_drift": 235,++ "missing_reduced_motion": 0,++ "total": 437++ },++ "score": 37.75,++ "findings_total": 437,++ "findings": [++ {++ "rule": "literal_duration",++ "file": "public/css/brand.css",++ "line": 34,++ "value": ".18s",++ "text": ".br-chip{min-height:40px;padding:0 15px;border-radius:999px;border:1px solid var(--border);background:var(--bg-secondary);color:var(--text-primary);font:inherit"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/brand.css",++ "line": 34,++ "value": ".18s",++ "text": ".br-chip{min-height:40px;padding:0 15px;border-radius:999px;border:1px solid var(--border);background:var(--bg-secondary);color:var(--text-primary);font:inherit"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/brand.css",++ "line": 34,++ "value": ".18s",++ "text": ".br-chip{min-height:40px;padding:0 15px;border-radius:999px;border:1px solid var(--border);background:var(--bg-secondary);color:var(--text-primary);font:inherit"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/design.css",++ "line": 26,++ "value": ".15s",++ "text": ".ds-swatch { display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 10px; width: 100%; min-height: 48px; padding: 8px 10px; border-radiu"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/design.css",++ "line": 26,++ "value": ".15s",++ "text": ".ds-swatch { display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 10px; width: 100%; min-height: 48px; padding: 8px 10px; border-radiu"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/design.css",++ "line": 60,++ "value": ".2s",++ "text": ".ds-previews { display: grid; gap: 12px; grid-template-columns: minmax(0, 1fr); transition: filter .2s; }"++ },++ {++ "rule": "literal_curve",++ "file": "public/css/icon-motion.css",++ "line": 14,++ "value": "cubic-bezier(",++ "text": "animation: ovm-draw var(--ovm-draw-time, .9s) cubic-bezier(.65, 0, .35, 1) forwards;"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/icon-motion.css",++ "line": 21,++ "value": ".32s",++ "text": ".ovm-pop { transition: transform .32s cubic-bezier(.34, 1.56, .64, 1); transform-origin: 50% 50%; }"++ },++ {++ "rule": "literal_curve",++ "file": "public/css/icon-motion.css",++ "line": 21,++ "value": "cubic-bezier(",++ "text": ".ovm-pop { transition: transform .32s cubic-bezier(.34, 1.56, .64, 1); transform-origin: 50% 50%; }"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/icons.css",++ "line": 31,++ "value": ".15s",++ "text": ".ic-tile{text-decoration:none;display:grid;justify-items:center;align-content:center;gap:10px;min-height:104px;padding:14px 6px 10px;border-radius:14px;border:1"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/icons.css",++ "line": 31,++ "value": ".15s",++ "text": ".ic-tile{text-decoration:none;display:grid;justify-items:center;align-content:center;gap:10px;min-height:104px;padding:14px 6px 10px;border-radius:14px;border:1"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/icons.css",++ "line": 31,++ "value": ".15s",++ "text": ".ic-tile{text-decoration:none;display:grid;justify-items:center;align-content:center;gap:10px;min-height:104px;padding:14px 6px 10px;border-radius:14px;border:1"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/kit-pages.css",++ "line": 38,++ "value": ".15s",++ "text": "transition: border-color .15s, box-shadow .15s; scroll-margin-top: 72px; }"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/kit-pages.css",++ "line": 38,++ "value": ".15s",++ "text": "transition: border-color .15s, box-shadow .15s; scroll-margin-top: 72px; }"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/marks.css",++ "line": 25,++ "value": ".6s",++ "text": ".mx-lean-bar i{position:absolute;inset:0 auto 0 0;width:var(--l,50%);background:#4f7cff;border-right:2px solid var(--mx-card);transition:width .6s cubic-bezier("++ },++ {++ "rule": "literal_curve",++ "file": "public/css/marks.css",++ "line": 25,++ "value": "cubic-bezier(",++ "text": ".mx-lean-bar i{position:absolute;inset:0 auto 0 0;width:var(--l,50%);background:#4f7cff;border-right:2px solid var(--mx-card);transition:width .6s cubic-bezier("++ },++ {++ "rule": "literal_duration",++ "file": "public/css/marks.css",++ "line": 35,++ "value": ".5s",++ "text": ".mx-top.is-new{animation:mxTopIn .5s cubic-bezier(.2,.9,.25,1.2) both}"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/motion.css",++ "line": 71,++ "value": "1.1s",++ "text": ".mo-race.is-run i{left:100%;transition:left 1.1s var(--mo-ease,ease)}"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/patterns.css",++ "line": 6,++ "value": "180ms",++ "text": ".pgroup{padding:18px}.pcards{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}.pcard{position:relative;display:grid;gap:6px;min-height:94px;pa"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/patterns.css",++ "line": 6,++ "value": "180ms",++ "text": ".pgroup{padding:18px}.pcards{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}.pcard{position:relative;display:grid;gap:6px;min-height:94px;pa"++ },++ {++ "rule": "literal_duration",++ "file": "public/mark-studies.html",++ "line": 49,++ "value": "1.1s",++ "text": ".busy{animation:ringBreathe 1.1s ease-in-out infinite alternate;transform-origin:50px 50px;transform-box:view-box}"++ },++ {++ "rule": "literal_curve",++ "file": "public/mark-studies.html",++ "line": 49,++ "value": "ease-in-out",++ "text": ".busy{animation:ringBreathe 1.1s ease-in-out infinite alternate;transform-origin:50px 50px;transform-box:view-box}"++ },++ {++ "rule": "color_drift",++ "file": "public/css/loading.css",++ "line": 31,++ "value": "#22c55e",++ "text": ".ld-foot output.is-zero { color: #22c55e; }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/loading.css",++ "line": 34,++ "value": "hsl(var(--h)",++ "text": ".ld-thumb, .ld-tile, .ld-avatar, .ld-dot { display: block; background: linear-gradient(135deg, hsl(var(--h, 220) 70% 55%), hsl(calc(var(--h, 220) + 40) 70% 40%)"++ },++ {++ "rule": "color_drift",++ "file": "public/css/motion.css",++ "line": 3,++ "value": "--mo-o #4f7cff",++ "text": ".mo{--mo-o:#4f7cff;--mo-v:#8b5cf6;--mo-face:'OV Brand',Nunito,Inter,-apple-system,BlinkMacSystemFont,sans-serif;max-width:1120px;margin:0 auto;padding:clamp(18p"++ }++ ]++ },++ {++ "repo": "OpenVibe.Domains",++ "commit": "7b292c87b16648b74b9808f7f9be21c6968a8b61",++ "files_scanned": 4,++ "counts": {++ "literal_duration": 6,++ "literal_curve": 1,++ "spinner": 0,++ "color_drift": 0,++ "missing_reduced_motion": 0,++ "total": 7++ },++ "score": 98.25,++ "findings_total": 7,++ "findings": [++ {++ "rule": "literal_duration",++ "file": "public/css/domains.css",++ "line": 37,++ "value": ".18s",++ "text": ".dm-tile { position: relative; display: grid; gap: 4px; min-height: 132px; padding: 16px; border-radius: 16px; background: linear-gradient(160deg, color-mix(in "++ },++ {++ "rule": "literal_duration",++ "file": "public/css/domains.css",++ "line": 37,++ "value": ".18s",++ "text": ".dm-tile { position: relative; display: grid; gap: 4px; min-height: 132px; padding: 16px; border-radius: 16px; background: linear-gradient(160deg, color-mix(in "++ },++ {++ "rule": "literal_duration",++ "file": "public/css/domains.css",++ "line": 124,++ "value": ".9s",++ "text": "html.ov-motion [data-ov-reveal] .dm-bar span { transform: scaleY(.02); transform-origin: bottom; transition: transform .9s cubic-bezier(.2, .8, .2, 1) .15s; }"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/domains.css",++ "line": 124,++ "value": ".15s",++ "text": "html.ov-motion [data-ov-reveal] .dm-bar span { transform: scaleY(.02); transform-origin: bottom; transition: transform .9s cubic-bezier(.2, .8, .2, 1) .15s; }"++ },++ {++ "rule": "literal_curve",++ "file": "public/css/domains.css",++ "line": 124,++ "value": "cubic-bezier(",++ "text": "html.ov-motion [data-ov-reveal] .dm-bar span { transform: scaleY(.02); transform-origin: bottom; transition: transform .9s cubic-bezier(.2, .8, .2, 1) .15s; }"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/domains.css",++ "line": 171,++ "value": ".15s",++ "text": "background: var(--bg-card, #0f1729); border: 1px solid var(--border, #1f2d47); transition: border-color .15s, transform .15s; }"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/domains.css",++ "line": 171,++ "value": ".15s",++ "text": "background: var(--bg-card, #0f1729); border: 1px solid var(--border, #1f2d47); transition: border-color .15s, transform .15s; }"++ }++ ]++ },++ {++ "repo": "OpenVibe.Exchange",++ "commit": "0921f08c9df602814dbb42eaa3786f334fb1bc48",++ "files_scanned": 3,++ "counts": {++ "literal_duration": 5,++ "literal_curve": 2,++ "spinner": 0,++ "color_drift": 2,++ "missing_reduced_motion": 0,++ "total": 9++ },++ "score": 98.15,++ "findings_total": 9,++ "findings": [++ {++ "rule": "literal_duration",++ "file": "public/css/exchange.css",++ "line": 70,++ "value": "2s",++ "text": "html.ov-motion .ex-motif-arrows path { stroke-dasharray: 500; animation: ex-loop 2s cubic-bezier(.2,.7,.2,1) both; }"++ },++ {++ "rule": "literal_curve",++ "file": "public/css/exchange.css",++ "line": 70,++ "value": "cubic-bezier(",++ "text": "html.ov-motion .ex-motif-arrows path { stroke-dasharray: 500; animation: ex-loop 2s cubic-bezier(.2,.7,.2,1) both; }"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/exchange.css",++ "line": 77,++ "value": ".5s",++ "text": "html.ov-motion .ex-motif-passport { animation: ex-stamp .5s cubic-bezier(.2,.9,.3,1.4) 1.2s both; }"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/exchange.css",++ "line": 77,++ "value": "1.2s",++ "text": "html.ov-motion .ex-motif-passport { animation: ex-stamp .5s cubic-bezier(.2,.9,.3,1.4) 1.2s both; }"++ },++ {++ "rule": "literal_curve",++ "file": "public/css/exchange.css",++ "line": 77,++ "value": "cubic-bezier(",++ "text": "html.ov-motion .ex-motif-passport { animation: ex-stamp .5s cubic-bezier(.2,.9,.3,1.4) 1.2s both; }"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/exchange.css",++ "line": 92,++ "value": ".15s",++ "text": ".ex-listing-link { display: grid; gap: 8px; height: 100%; padding: 16px; border-radius: 16px; text-decoration: none; color: var(--text-primary, #e6edf7) !import"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/exchange.css",++ "line": 92,++ "value": ".15s",++ "text": ".ex-listing-link { display: grid; gap: 8px; height: 100%; padding: 16px; border-radius: 16px; text-decoration: none; color: var(--text-primary, #e6edf7) !import"++ },++ {++ "rule": "color_drift",++ "file": "public/css/exchange.css",++ "line": 72,++ "value": "rgba(0, 0, 0, .22)",++ "text": ".ex-motif-item { position: absolute; padding: 9px 14px; border-radius: 12px; font-size: .86rem; font-weight: 800; background: var(--bg-card, #0f1729); border: 1"++ },++ {++ "rule": "color_drift",++ "file": "public/css/exchange.css",++ "line": 74,++ "value": "rgba(0, 0, 0, .28)",++ "text": ".ex-motif-passport { position: absolute; left: 50%; bottom: 8%; translate: -50% 0; display: grid; justify-items: center; padding: 10px 18px; border-radius: 14px"++ }++ ]++ },++ {++ "repo": "OpenVibe.Food",++ "commit": "5c31fd571cb36619fda2714a0cb0b784093560bc",++ "files_scanned": 10,++ "counts": {++ "literal_duration": 4,++ "literal_curve": 4,++ "spinner": 0,++ "color_drift": 8,++ "missing_reduced_motion": 0,++ "total": 16++ },++ "score": 97.6,++ "findings_total": 16,++ "findings": [++ {++ "rule": "literal_duration",++ "file": "public/css/delivery.css",++ "line": 7,++ "value": ".15s",++ "text": ".dv-app { display: flex; flex-direction: column; gap: 6px; padding: 12px 14px; border-radius: 12px; border: 1px solid var(--border, #1f2d47); background: var(--"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/delivery.css",++ "line": 7,++ "value": ".15s",++ "text": ".dv-app { display: flex; flex-direction: column; gap: 6px; padding: 12px 14px; border-radius: 12px; border: 1px solid var(--border, #1f2d47); background: var(--"++ },++ {++ "rule": "literal_curve",++ "file": "public/css/delivery.css",++ "line": 7,++ "value": "ease",++ "text": ".dv-app { display: flex; flex-direction: column; gap: 6px; padding: 12px 14px; border-radius: 12px; border: 1px solid var(--border, #1f2d47); background: var(--"++ },++ {++ "rule": "literal_curve",++ "file": "public/css/delivery.css",++ "line": 7,++ "value": "ease",++ "text": ".dv-app { display: flex; flex-direction: column; gap: 6px; padding: 12px 14px; border-radius: 12px; border: 1px solid var(--border, #1f2d47); background: var(--"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/home.css",++ "line": 13,++ "value": ".15s",++ "text": ".home-cards a { display: flex; flex-direction: column; gap: 4px; height: 100%; box-sizing: border-box; padding: 12px 14px; border-radius: 12px; border: 1px soli"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/home.css",++ "line": 13,++ "value": ".15s",++ "text": ".home-cards a { display: flex; flex-direction: column; gap: 4px; height: 100%; box-sizing: border-box; padding: 12px 14px; border-radius: 12px; border: 1px soli"++ },++ {++ "rule": "literal_curve",++ "file": "public/css/home.css",++ "line": 13,++ "value": "ease",++ "text": ".home-cards a { display: flex; flex-direction: column; gap: 4px; height: 100%; box-sizing: border-box; padding: 12px 14px; border-radius: 12px; border: 1px soli"++ },++ {++ "rule": "literal_curve",++ "file": "public/css/home.css",++ "line": 13,++ "value": "ease",++ "text": ".home-cards a { display: flex; flex-direction: column; gap: 4px; height: 100%; box-sizing: border-box; padding: 12px 14px; border-radius: 12px; border: 1px soli"++ },++ {++ "rule": "color_drift",++ "file": "public/css/app.css",++ "line": 67,++ "value": "rgba(0, 0, 0, .7)",++ "text": ".food-search-panel { flex-direction: column; align-items: stretch; flex-wrap: nowrap; gap: 14px; margin: 0; padding: 22px; border-radius: 18px; border: 1px soli"++ },++ {++ "rule": "color_drift",++ "file": "public/css/delivery.css",++ "line": 19,++ "value": "#22c55e",++ "text": ".dv-budget { color: color-mix(in srgb, #22c55e 75%, var(--text-primary, #e6edf7)); border-color: rgba(74, 222, 128, .45); }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/delivery.css",++ "line": 19,++ "value": "rgba(74, 222, 128, .45)",++ "text": ".dv-budget { color: color-mix(in srgb, #22c55e 75%, var(--text-primary, #e6edf7)); border-color: rgba(74, 222, 128, .45); }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/delivery.css",++ "line": 20,++ "value": "#3b82f6",++ "text": ".dv-mid { color: color-mix(in srgb, #3b82f6 75%, var(--text-primary, #e6edf7)); border-color: rgba(96, 165, 250, .45); }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/delivery.css",++ "line": 20,++ "value": "rgba(96, 165, 250, .45)",++ "text": ".dv-mid { color: color-mix(in srgb, #3b82f6 75%, var(--text-primary, #e6edf7)); border-color: rgba(96, 165, 250, .45); }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/delivery.css",++ "line": 21,++ "value": "#d946ef",++ "text": ".dv-premium { color: color-mix(in srgb, #d946ef 75%, var(--text-primary, #e6edf7)); border-color: rgba(240, 171, 252, .45); }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/delivery.css",++ "line": 21,++ "value": "rgba(240, 171, 252, .45)",++ "text": ".dv-premium { color: color-mix(in srgb, #d946ef 75%, var(--text-primary, #e6edf7)); border-color: rgba(240, 171, 252, .45); }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/pantry.css",++ "line": 6,++ "value": "#000",++ "text": "#bc-video { display: block; width: 100%; max-width: 420px; margin-top: 10px; border-radius: 12px; background: #000; }"++ }++ ]++ },++ {++ "repo": "OpenVibe.Fun",++ "commit": "a287ae83a37868dd4a5d472a154eb74ee4e6ec90",++ "files_scanned": 4,++ "counts": {++ "literal_duration": 7,++ "literal_curve": 2,++ "spinner": 0,++ "color_drift": 4,++ "missing_reduced_motion": 0,++ "total": 13++ },++ "score": 97.55,++ "findings_total": 13,++ "findings": [++ {++ "rule": "literal_duration",++ "file": "public/css/fun.css",++ "line": 67,++ "value": ".5s",++ "text": "html.ov-motion .fu-map-motif .fu-pin { animation: fu-pop .5s cubic-bezier(.2,.9,.3,1.5) both; } .fu-pin-2 { animation-delay: .2s !important; } .fu-pin-3 { anima"++ },++ {++ "rule": "literal_curve",++ "file": "public/css/fun.css",++ "line": 67,++ "value": "cubic-bezier(",++ "text": "html.ov-motion .fu-map-motif .fu-pin { animation: fu-pop .5s cubic-bezier(.2,.9,.3,1.5) both; } .fu-pin-2 { animation-delay: .2s !important; } .fu-pin-3 { anima"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/fun.css",++ "line": 74,++ "value": ".8s",++ "text": "html.ov-motion .fu-flip-card { animation: fu-flip .8s cubic-bezier(.2,.7,.2,1) .9s both; }"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/fun.css",++ "line": 74,++ "value": ".9s",++ "text": "html.ov-motion .fu-flip-card { animation: fu-flip .8s cubic-bezier(.2,.7,.2,1) .9s both; }"++ },++ {++ "rule": "literal_curve",++ "file": "public/css/fun.css",++ "line": 74,++ "value": "cubic-bezier(",++ "text": "html.ov-motion .fu-flip-card { animation: fu-flip .8s cubic-bezier(.2,.7,.2,1) .9s both; }"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/fun.css",++ "line": 87,++ "value": ".15s",++ "text": ".fu-quest-link, .fu-plan-link { transition: border-color .15s, transform .15s; }"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/fun.css",++ "line": 87,++ "value": ".15s",++ "text": ".fu-quest-link, .fu-plan-link { transition: border-color .15s, transform .15s; }"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/fun.css",++ "line": 91,++ "value": ".15s",++ "text": ".fu-plan-card { gap: 6px; align-content: start; height: 100%; padding: 15px 16px; border-radius: 16px; background: var(--bg-card, #0f1729); border: 1px solid va"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/fun.css",++ "line": 91,++ "value": ".15s",++ "text": ".fu-plan-card { gap: 6px; align-content: start; height: 100%; padding: 15px 16px; border-radius: 16px; background: var(--bg-card, #0f1729); border: 1px solid va"++ },++ {++ "rule": "color_drift",++ "file": "public/css/fun.css",++ "line": 65,++ "value": "rgba(0, 0, 0, .25)",++ "text": ".fu-pin { position: absolute; translate: -50% -100%; padding: 5px 10px; border-radius: 10px 10px 10px 2px; font-size: .78rem; font-weight: 800; color: var(--roo"++ },++ {++ "rule": "color_drift",++ "file": "public/css/fun.css",++ "line": 70,++ "value": "rgba(0, 0, 0, .3)",++ "text": ".fu-flip-card { position: absolute; left: 50%; bottom: 8%; translate: -50% 0; display: grid; gap: 2px; width: min(70%, 260px); padding: 10px 14px; border-radius"++ },++ {++ "rule": "color_drift",++ "file": "public/css/fun.css",++ "line": 114,++ "value": "#000",++ "text": ".fu-overlay { display: grid; gap: 4px; max-width: 420px; padding: 14px 16px; border-radius: 16px; background: color-mix(in srgb, #000 70%, transparent); color: "++ },++ {++ "rule": "color_drift",++ "file": "public/css/fun.css",++ "line": 114,++ "value": "#fff",++ "text": ".fu-overlay { display: grid; gap: 4px; max-width: 420px; padding: 14px 16px; border-radius: 16px; background: color-mix(in srgb, #000 70%, transparent); color: "++ }++ ]++ },++ {++ "repo": "OpenVibe.Games",++ "commit": "3b8910cd2dfab9415c56f680b298097aa57bfea1",++ "files_scanned": 3,++ "counts": {++ "literal_duration": 0,++ "literal_curve": 0,++ "spinner": 0,++ "color_drift": 0,++ "missing_reduced_motion": 0,++ "total": 0++ },++ "score": 100,++ "findings_total": 0,++ "findings": []++ },++ {++ "repo": "OpenVibe.Help",++ "commit": "bf94ad1b939d27448c9b037fab8df1f250d0fc3d",++ "files_scanned": 1,++ "counts": {++ "literal_duration": 2,++ "literal_curve": 0,++ "spinner": 0,++ "color_drift": 1,++ "missing_reduced_motion": 0,++ "total": 3++ },++ "score": 99.45,++ "findings_total": 3,++ "findings": [++ {++ "rule": "literal_duration",++ "file": "public/css/app.css",++ "line": 86,++ "value": ".15s",++ "text": ".site-card { display: flex; flex-direction: column; gap: 4px; height: 100%; padding: 12px 14px; border-radius: 12px; border: 1px solid var(--border, #1f2d47); b"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/app.css",++ "line": 86,++ "value": ".15s",++ "text": ".site-card { display: flex; flex-direction: column; gap: 4px; height: 100%; padding: 12px 14px; border-radius: 12px; border: 1px solid var(--border, #1f2d47); b"++ },++ {++ "rule": "color_drift",++ "file": "public/css/app.css",++ "line": 58,++ "value": "#10151f",++ "text": "mark { background: var(--warning, #f59e0b); color: #10151f; border-radius: 3px; }"++ }++ ]++ },++ {++ "repo": "OpenVibe.Host",++ "commit": "6ee9b8afccbe1353c4d94808d57caaa69304de57",++ "files_scanned": 3,++ "counts": {++ "literal_duration": 2,++ "literal_curve": 2,++ "spinner": 0,++ "color_drift": 13,++ "missing_reduced_motion": 0,++ "total": 17++ },++ "score": 98.35,++ "findings_total": 17,++ "findings": [++ {++ "rule": "literal_duration",++ "file": "public/css/catalog.css",++ "line": 225,++ "value": ".15s",++ "text": ".hh-cards a { display: flex; flex-direction: column; gap: 4px; height: 100%; box-sizing: border-box; padding: 12px 14px; border-radius: 12px; border: 1px solid "++ },++ {++ "rule": "literal_duration",++ "file": "public/css/catalog.css",++ "line": 225,++ "value": ".15s",++ "text": ".hh-cards a { display: flex; flex-direction: column; gap: 4px; height: 100%; box-sizing: border-box; padding: 12px 14px; border-radius: 12px; border: 1px solid "++ },++ {++ "rule": "literal_curve",++ "file": "public/css/catalog.css",++ "line": 225,++ "value": "ease",++ "text": ".hh-cards a { display: flex; flex-direction: column; gap: 4px; height: 100%; box-sizing: border-box; padding: 12px 14px; border-radius: 12px; border: 1px solid "++ },++ {++ "rule": "literal_curve",++ "file": "public/css/catalog.css",++ "line": 225,++ "value": "ease",++ "text": ".hh-cards a { display: flex; flex-direction: column; gap: 4px; height: 100%; box-sizing: border-box; padding: 12px 14px; border-radius: 12px; border: 1px solid "++ },++ {++ "rule": "color_drift",++ "file": "public/css/catalog.css",++ "line": 26,++ "value": "rgba(255,255,255,.02)",++ "text": ".hc-table thead th { font-size: 12px; font-weight: 600; letter-spacing: .03em; text-transform: uppercase; color: var(--hs-muted); background: rgba(255,255,255,."++ },++ {++ "rule": "color_drift",++ "file": "public/css/catalog.css",++ "line": 29,++ "value": "rgba(96,165,250,.05)",++ "text": ".hc-table tbody tr:hover > * { background: rgba(96,165,250,.05); }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/console.css",++ "line": 13,++ "value": "#f87171",++ "text": ".cs-bad { color: #f87171; }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/console.css",++ "line": 23,++ "value": "#4ade80",++ "text": ".cs-save a { color: #4ade80; }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/console.css",++ "line": 26,++ "value": "#4ade80",++ "text": ".cs-status.is-ok { color: #4ade80; } .cs-status.is-off { color: var(--text-secondary, #9fb0c8); } .cs-status.is-busy { color: #fbbf24; }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/console.css",++ "line": 26,++ "value": "#fbbf24",++ "text": ".cs-status.is-ok { color: #4ade80; } .cs-status.is-off { color: var(--text-secondary, #9fb0c8); } .cs-status.is-busy { color: #fbbf24; }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/host.css",++ "line": 3,++ "value": "--hs-line rgba(255,255,255,.12)",++ "text": "--hs-card: var(--bg-secondary, #111a2c); --hs-line: rgba(255,255,255,.12); --hs-accent: var(--accent, #60a5fa);"++ },++ {++ "rule": "color_drift",++ "file": "public/css/host.css",++ "line": 4,++ "value": "--hs-ok #34d399",++ "text": "--hs-ok: #34d399; --hs-warn: #fbbf24; --hs-bad: #f87171; }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/host.css",++ "line": 4,++ "value": "--hs-warn #fbbf24",++ "text": "--hs-ok: #34d399; --hs-warn: #fbbf24; --hs-bad: #f87171; }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/host.css",++ "line": 4,++ "value": "--hs-bad #f87171",++ "text": "--hs-ok: #34d399; --hs-warn: #fbbf24; --hs-bad: #f87171; }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/host.css",++ "line": 15,++ "value": "rgba(96,165,250,.08)",++ "text": "tr.active td { background: rgba(96,165,250,.08); }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/host.css",++ "line": 25,++ "value": "#0a0f1c",++ "text": "button, .button { font: inherit; padding: .45rem .9rem; border-radius: 6px; border: 0; background: var(--hs-accent); color: #0a0f1c; cursor: pointer; text-decor"++ },++ {++ "rule": "color_drift",++ "file": "public/css/host.css",++ "line": 27,++ "value": "#0a0f1c",++ "text": "button.danger, .danger { background: var(--hs-bad); color: #0a0f1c; } button.link.danger { background: none; color: var(--hs-bad); }"++ }++ ]++ },++ {++ "repo": "OpenVibe.Inventory",++ "commit": "bb0f57d6ef10f3a9832cc524d4a82d57102963d3",++ "files_scanned": 1,++ "counts": {++ "literal_duration": 0,++ "literal_curve": 0,++ "spinner": 0,++ "color_drift": 9,++ "missing_reduced_motion": 0,++ "total": 9++ },++ "score": 99.55,++ "findings_total": 9,++ "findings": [++ {++ "rule": "color_drift",++ "file": "public/css/app.css",++ "line": 51,++ "value": "--rar #94a3b8",++ "text": ".r-common { --rar: #94a3b8; }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/app.css",++ "line": 91,++ "value": "#fbbf24",++ "text": ".inv-hero-panel { padding: 20px; border-radius: 18px; border: 1px solid var(--border, #1f2d47); background: linear-gradient(160deg, color-mix(in srgb, #fbbf24 8"++ },++ {++ "rule": "color_drift",++ "file": "public/css/app.css",++ "line": 91,++ "value": "rgba(0, 0, 0, .7)",++ "text": ".inv-hero-panel { padding: 20px; border-radius: 18px; border: 1px solid var(--border, #1f2d47); background: linear-gradient(160deg, color-mix(in srgb, #fbbf24 8"++ },++ {++ "rule": "color_drift",++ "file": "public/css/app.css",++ "line": 119,++ "value": "#22c55e",++ "text": ".ws-published { color: #22c55e; border-color: color-mix(in srgb, #22c55e 50%, transparent); }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/app.css",++ "line": 119,++ "value": "#22c55e",++ "text": ".ws-published { color: #22c55e; border-color: color-mix(in srgb, #22c55e 50%, transparent); }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/app.css",++ "line": 120,++ "value": "#f59e0b",++ "text": ".ws-in_review { color: #f59e0b; border-color: color-mix(in srgb, #f59e0b 50%, transparent); }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/app.css",++ "line": 120,++ "value": "#f59e0b",++ "text": ".ws-in_review { color: #f59e0b; border-color: color-mix(in srgb, #f59e0b 50%, transparent); }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/app.css",++ "line": 121,++ "value": "#ef4444",++ "text": ".ws-draft { color: #ef4444; border-color: color-mix(in srgb, #ef4444 50%, transparent); }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/app.css",++ "line": 121,++ "value": "#ef4444",++ "text": ".ws-draft { color: #ef4444; border-color: color-mix(in srgb, #ef4444 50%, transparent); }"++ }++ ]++ },++ {++ "repo": "OpenVibe.Live",++ "commit": "b90121e8c8dfbaa41ecf7caa652ea84983d19a2e",++ "files_scanned": 119,++ "counts": {++ "literal_duration": 931,++ "literal_curve": 448,++ "spinner": 0,++ "color_drift": 1652,++ "missing_reduced_motion": 0,++ "total": 3031++ },++ "score": 0,++ "findings_total": 3031,++ "findings": [++ {++ "rule": "literal_duration",++ "file": "public/banned.html",++ "line": 17,++ "value": "6s",++ "text": "background: radial-gradient(120% 90% at 50% 115%, #4a0b0b 0%, #1a0708 45%, #0b0a0d 75%); animation: breathe 6s ease-in-out infinite; }"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/broadcast.css",++ "line": 33,++ "value": "0.2s",++ "text": "transition: color 0.2s ease;"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/chat-shared.css",++ "line": 17,++ "value": "0.12s",++ "text": "animation: ctxFadeIn 0.12s ease-out;"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/context-cards.css",++ "line": 9,++ "value": "0.35s",++ "text": ".cc-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform 0.35s ease; }"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/cosmetics.css",++ "line": 17,++ "value": "3s",++ "text": "animation: nameFxRainbow 3s linear infinite;"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/features/broadcast.css",++ "line": 73,++ "value": "0.15s",++ "text": "transition: background 0.15s;"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/features/channel.css",++ "line": 14,++ "value": "0.15s",++ "text": "transition: border-color 0.15s ease, transform 0.15s ease;"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/features/chatpage.css",++ "line": 15,++ "value": "0.15s",++ "text": "font-size: 0.82rem; color: var(--text-secondary); transition: background 0.15s;"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/features/content.css",++ "line": 141,++ "value": ".32s",++ "text": "animation: cfIn .32s var(--ease-standard, ease) both;"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/features/dashboard.css",++ "line": 6,++ "value": ".3s",++ "text": "#dash-powerchat-card { transition: box-shadow .3s ease, border-color .3s ease; }"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/features/docs.css",++ "line": 14,++ "value": "0.15s",++ "text": ".docs-tab-arrow { flex: 0 0 auto; display: none; align-items: center; justify-content: center; width: 30px; background: none; border: none; color: var(--text-mu"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/features/featured.css",++ "line": 10,++ "value": "320ms",++ "text": "animation: hfIn 320ms cubic-bezier(.2, .9, .3, 1);"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/features/home.css",++ "line": 31,++ "value": "3.5s",++ "text": ".hero-egg-icon { font-size: 1.3rem; filter: drop-shadow(0 2px 6px rgba(0,0,0,0.4)); animation: eggWobble 3.5s ease-in-out infinite; }"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/features/rundown.css",++ "line": 238,++ "value": ".18s",++ "text": ".rd-more > summary::before { content: ''; width: 8px; height: 8px; border-right: 2px solid currentColor; border-bottom: 2px solid currentColor; transform: trans"++ },++ {++ "rule": "literal_curve",++ "file": "public/css/features/stats.css",++ "line": 6,++ "value": "ease-out",++ "text": "animation: snFade var(--dur-fast, 160ms) ease-out;"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/features/viewer-places.css",++ "line": 8,++ "value": "0.16s",++ "text": "animation: ovp-in 0.16s ease-out;"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/home-fx.css",++ "line": 10,++ "value": "0.4s",++ "text": "pointer-events: none; z-index: 0; opacity: 0; transition: opacity 0.4s;"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/home-tour.css",++ "line": 32,++ "value": "1.1s",++ "text": ".tour-wire { fill: none; stroke-width: 1.6; stroke-linecap: round; stroke: color-mix(in srgb, var(--text-primary) 22%, transparent); stroke-dasharray: 5 7; anim"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/i18n-star.css",++ "line": 15,++ "value": "0.35s",++ "text": "animation: i18nSlideIn 0.35s ease both;"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/media-player.css",++ "line": 82,++ "value": "1.4s",++ "text": "animation: mp-pulse 1.4s ease-out infinite; /* a ring that pulses out: nothing spins */"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/messenger.css",++ "line": 24,++ "value": "0.15s",++ "text": "transition: transform 0.15s, background 0.15s;"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/ov-guide.css",++ "line": 7,++ "value": ".25s",++ "text": ".ovg-scrim { position: absolute; inset: 0; background: rgba(4,5,10,0.72); backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px); animation: ovgFade .25"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/style.css",++ "line": 180,++ "value": "0.4s",++ "text": "transition: background 0.4s ease, border-color 0.4s ease, box-shadow 0.4s ease, backdrop-filter 0.4s ease;"++ },++ {++ "rule": "literal_duration",++ "file": "public/index.html",++ "line": 78,++ "value": "1.5s",++ "text": ".ovsk::after,.ovsk-bar::after{content:'';position:absolute;inset:0;background:linear-gradient(100deg,transparent 20%,color-mix(in srgb,var(--text-primary,#f1f4f"++ },++ {++ "rule": "literal_duration",++ "file": "public/kiosk.html",++ "line": 123,++ "value": ".5s",++ "text": "min-height:1.5em;transition:opacity .5s"++ }++ ]++ },++ {++ "repo": "OpenVibe.Media",++ "commit": "d4dc27191555fab54bd0503266e3e50cee2c6673",++ "files_scanned": 2,++ "counts": {++ "literal_duration": 0,++ "literal_curve": 0,++ "spinner": 0,++ "color_drift": 0,++ "missing_reduced_motion": 0,++ "total": 0++ },++ "score": 100,++ "findings_total": 0,++ "findings": []++ },++ {++ "repo": "OpenVibe.MediaHub",++ "commit": "7a2bef57872feaf52d005d562a8bdcbf53951d51",++ "files_scanned": 9,++ "counts": {++ "literal_duration": 13,++ "literal_curve": 7,++ "spinner": 0,++ "color_drift": 46,++ "missing_reduced_motion": 0,++ "total": 66++ },++ "score": 92.7,++ "findings_total": 66,++ "findings": [++ {++ "rule": "literal_duration",++ "file": "public/css/pics.css",++ "line": 36,++ "value": ".15s",++ "text": ".ovp-drop { position: relative; margin: 6px 0 0; border: 2px dashed color-mix(in srgb, var(--ovp-pink) 55%, var(--ovp-line-strong)); border-radius: 20px; backgr"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/pics.css",++ "line": 36,++ "value": ".15s",++ "text": ".ovp-drop { position: relative; margin: 6px 0 0; border: 2px dashed color-mix(in srgb, var(--ovp-pink) 55%, var(--ovp-line-strong)); border-radius: 20px; backgr"++ },++ {++ "rule": "literal_curve",++ "file": "public/css/pics.css",++ "line": 36,++ "value": "ease",++ "text": ".ovp-drop { position: relative; margin: 6px 0 0; border: 2px dashed color-mix(in srgb, var(--ovp-pink) 55%, var(--ovp-line-strong)); border-radius: 20px; backgr"++ },++ {++ "rule": "literal_curve",++ "file": "public/css/pics.css",++ "line": 36,++ "value": "ease",++ "text": ".ovp-drop { position: relative; margin: 6px 0 0; border: 2px dashed color-mix(in srgb, var(--ovp-pink) 55%, var(--ovp-line-strong)); border-radius: 20px; backgr"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/pics.css",++ "line": 60,++ "value": ".15s",++ "text": ".ovp-progress span { display: block; height: 100%; width: 0; background: var(--ovp-pink); transition: width .15s linear; }"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/pics.css",++ "line": 78,++ "value": ".2s",++ "text": ".ovp-tile img { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform .2s ease; }"++ },++ {++ "rule": "literal_curve",++ "file": "public/css/pics.css",++ "line": 78,++ "value": "ease",++ "text": ".ovp-tile img { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform .2s ease; }"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/video.css",++ "line": 78,++ "value": ".25s",++ "text": ".ovv-card-media img { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform .25s ease; }"++ },++ {++ "rule": "literal_curve",++ "file": "public/css/video.css",++ "line": 78,++ "value": "ease",++ "text": ".ovv-card-media img { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform .25s ease; }"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/video.css",++ "line": 116,++ "value": ".45s",++ "text": ".ovv-player { display: block; width: 100%; aspect-ratio: var(--ovv-ar, 16 / 9); max-height: calc(100vh - 150px); max-height: calc(100dvh - 150px); background: #"++ },++ {++ "rule": "literal_curve",++ "file": "public/css/video.css",++ "line": 116,++ "value": "cubic-bezier(",++ "text": ".ovv-player { display: block; width: 100%; aspect-ratio: var(--ovv-ar, 16 / 9); max-height: calc(100vh - 150px); max-height: calc(100dvh - 150px); background: #"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/video.css",++ "line": 531,++ "value": ".15s",++ "text": ".ovv-pip-bar { position: absolute; inset: 0 0 auto 0; display: flex; align-items: center; gap: 4px; padding: 4px 6px; background: linear-gradient(rgba(0, 0, 0, "++ },++ {++ "rule": "literal_duration",++ "file": "public/css/video.css",++ "line": 547,++ "value": "0.18s",++ "text": ".ovv-owner[open] { animation: ovv-owner-in 0.18s ease-out; }"++ },++ {++ "rule": "literal_curve",++ "file": "public/css/video.css",++ "line": 547,++ "value": "ease-out",++ "text": ".ovv-owner[open] { animation: ovv-owner-in 0.18s ease-out; }"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/video.css",++ "line": 556,++ "value": ".15s",++ "text": "border: 1px solid var(--ovv-line-strong); background: color-mix(in srgb, var(--ovv-card) 70%, transparent); transition: border-color .15s, background .15s;"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/video.css",++ "line": 556,++ "value": ".15s",++ "text": "border: 1px solid var(--ovv-line-strong); background: color-mix(in srgb, var(--ovv-card) 70%, transparent); transition: border-color .15s, background .15s;"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/video.css",++ "line": 582,++ "value": ".15s",++ "text": ".ovv-stat { display: flex; flex-direction: column; gap: 2px; height: 100%; padding: 11px 14px; border: 1px solid var(--ovv-line); border-radius: 14px; backgroun"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/video.css",++ "line": 582,++ "value": ".15s",++ "text": ".ovv-stat { display: flex; flex-direction: column; gap: 2px; height: 100%; padding: 11px 14px; border: 1px solid var(--ovv-line); border-radius: 14px; backgroun"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/video.css",++ "line": 590,++ "value": "1.6s",++ "text": "html.ov-motion .ovv-stat.is-live .ovv-dot { animation: ovv-pulse 1.6s ease-out infinite; }"++ },++ {++ "rule": "literal_curve",++ "file": "public/css/video.css",++ "line": 590,++ "value": "ease-out",++ "text": "html.ov-motion .ovv-stat.is-live .ovv-dot { animation: ovv-pulse 1.6s ease-out infinite; }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/pics.css",++ "line": 6,++ "value": "--ovp-pink #f472b6",++ "text": "--ovp-pink: #f472b6;"++ },++ {++ "rule": "color_drift",++ "file": "public/css/pics.css",++ "line": 7,++ "value": "--ovp-pink-soft #f9a8d4",++ "text": "--ovp-pink-soft: #f9a8d4;"++ },++ {++ "rule": "color_drift",++ "file": "public/css/pics.css",++ "line": 8,++ "value": "--ovp-on-pink #1f0a17",++ "text": "--ovp-on-pink: #1f0a17;"++ },++ {++ "rule": "color_drift",++ "file": "public/css/pics.css",++ "line": 32,++ "value": "#c084fc",++ "text": ".ovp-accent { background: linear-gradient(92deg, var(--ovp-pink), #c084fc 70%); -webkit-background-clip: text; background-clip: text; color: transparent; }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/pics.css",++ "line": 81,++ "value": "rgba(0, 0, 0, .7)",++ "text": ".ovp-flag { position: absolute; left: 8px; top: 8px; padding: 2px 8px; border-radius: 999px; background: rgba(0, 0, 0, .7); color: #fff; font-size: .72rem; font"++ }++ ]++ },++ {++ "repo": "OpenVibe.Meme",++ "commit": "c227e892e228707fc51b80852b3ae83680c22613",++ "files_scanned": 3,++ "counts": {++ "literal_duration": 5,++ "literal_curve": 3,++ "spinner": 0,++ "color_drift": 17,++ "missing_reduced_motion": 0,++ "total": 25++ },++ "score": 97.15,++ "findings_total": 25,++ "findings": [++ {++ "rule": "literal_duration",++ "file": "public/css/meme.css",++ "line": 47,++ "value": "1.6s",++ "text": "html.ov-motion .mm-tree-lines path { animation: mm-grow 1.6s cubic-bezier(.2,.7,.2,1) both; }"++ },++ {++ "rule": "literal_curve",++ "file": "public/css/meme.css",++ "line": 47,++ "value": "cubic-bezier(",++ "text": "html.ov-motion .mm-tree-lines path { animation: mm-grow 1.6s cubic-bezier(.2,.7,.2,1) both; }"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/meme.css",++ "line": 59,++ "value": ".7s",++ "text": "html.ov-motion .mm-sticker { animation: mm-fan .7s cubic-bezier(.2,.9,.3,1.3) both; animation-delay: calc(.9s + var(--d, 0s)); }"++ },++ {++ "rule": "literal_curve",++ "file": "public/css/meme.css",++ "line": 59,++ "value": "cubic-bezier(",++ "text": "html.ov-motion .mm-sticker { animation: mm-fan .7s cubic-bezier(.2,.9,.3,1.3) both; animation-delay: calc(.9s + var(--d, 0s)); }"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/meme.css",++ "line": 66,++ "value": ".15s",++ "text": "background: var(--bg-card, #0f1729); border: 1px solid var(--border, #1f2d47); transition: border-color .15s, transform .15s; }"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/meme.css",++ "line": 66,++ "value": ".15s",++ "text": "background: var(--bg-card, #0f1729); border: 1px solid var(--border, #1f2d47); transition: border-color .15s, transform .15s; }"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/meme.css",++ "line": 144,++ "value": ".9s",++ "text": "html.ov-motion .mm-demo-bars i { animation: mm-bar .9s cubic-bezier(.2,.7,.2,1) both; animation-delay: calc(var(--h) * 30ms); transform-origin: bottom; }"++ },++ {++ "rule": "literal_curve",++ "file": "public/css/meme.css",++ "line": 144,++ "value": "cubic-bezier(",++ "text": "html.ov-motion .mm-demo-bars i { animation: mm-bar .9s cubic-bezier(.2,.7,.2,1) both; animation-delay: calc(var(--h) * 30ms); transform-origin: bottom; }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/meme.css",++ "line": 51,++ "value": "rgba(0, 0, 0, .22)",++ "text": "background: var(--bg-card, #0f1729); border: 1px solid color-mix(in srgb, var(--room-accent) 45%, var(--border, #1f2d47)); box-shadow: 0 8px 22px rgba(0, 0, 0, "++ },++ {++ "rule": "color_drift",++ "file": "public/css/meme.css",++ "line": 56,++ "value": "black",++ "text": ".mm-sticker { position: absolute; padding: 4px 10px; border-radius: 8px; font: 900 .78rem/1 Impact, 'Arial Black', sans-serif; letter-spacing: .04em; text-trans"++ },++ {++ "rule": "color_drift",++ "file": "public/css/meme.css",++ "line": 56,++ "value": "#fff",++ "text": ".mm-sticker { position: absolute; padding: 4px 10px; border-radius: 8px; font: 900 .78rem/1 Impact, 'Arial Black', sans-serif; letter-spacing: .04em; text-trans"++ },++ {++ "rule": "color_drift",++ "file": "public/css/meme.css",++ "line": 57,++ "value": "#000",++ "text": "background: color-mix(in srgb, var(--room-accent) 70%, #000); rotate: -8deg; box-shadow: 0 6px 14px rgba(0, 0, 0, .25); }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/meme.css",++ "line": 57,++ "value": "rgba(0, 0, 0, .25)",++ "text": "background: color-mix(in srgb, var(--room-accent) 70%, #000); rotate: -8deg; box-shadow: 0 6px 14px rgba(0, 0, 0, .25); }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/meme.css",++ "line": 130,++ "value": "#111",++ "text": ".mm-caption-card { position: relative; margin: 0; overflow: hidden; border-radius: 16px; background: #111; container-type: inline-size; box-shadow: 0 14px 34px "++ },++ {++ "rule": "color_drift",++ "file": "public/css/meme.css",++ "line": 130,++ "value": "rgba(0, 0, 0, .3)",++ "text": ".mm-caption-card { position: relative; margin: 0; overflow: hidden; border-radius: 16px; background: #111; container-type: inline-size; box-shadow: 0 14px 34px "++ },++ {++ "rule": "color_drift",++ "file": "public/css/meme.css",++ "line": 133,++ "value": "#fff",++ "text": ".mm-caption-card figcaption span { display: block; text-align: center; color: #fff; text-transform: uppercase; overflow-wrap: anywhere;"++ },++ {++ "rule": "color_drift",++ "file": "public/css/meme.css",++ "line": 134,++ "value": "black",++ "text": "font: 900 clamp(1rem, 7.2cqw, 3.2rem)/1.05 Impact, Haettenschweiler, 'Arial Narrow Bold', 'Arial Black', sans-serif; letter-spacing: .01em;"++ },++ {++ "rule": "color_drift",++ "file": "public/css/meme.css",++ "line": 135,++ "value": "#000",++ "text": "text-shadow: 2px 2px 0 #000, -2px 2px 0 #000, 2px -2px 0 #000, -2px -2px 0 #000, 0 2px 0 #000, 2px 0 0 #000, 0 -2px 0 #000, -2px 0 0 #000; }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/meme.css",++ "line": 135,++ "value": "#000",++ "text": "text-shadow: 2px 2px 0 #000, -2px 2px 0 #000, 2px -2px 0 #000, -2px -2px 0 #000, 0 2px 0 #000, 2px 0 0 #000, 0 -2px 0 #000, -2px 0 0 #000; }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/meme.css",++ "line": 135,++ "value": "#000",++ "text": "text-shadow: 2px 2px 0 #000, -2px 2px 0 #000, 2px -2px 0 #000, -2px -2px 0 #000, 0 2px 0 #000, 2px 0 0 #000, 0 -2px 0 #000, -2px 0 0 #000; }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/meme.css",++ "line": 135,++ "value": "#000",++ "text": "text-shadow: 2px 2px 0 #000, -2px 2px 0 #000, 2px -2px 0 #000, -2px -2px 0 #000, 0 2px 0 #000, 2px 0 0 #000, 0 -2px 0 #000, -2px 0 0 #000; }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/meme.css",++ "line": 135,++ "value": "#000",++ "text": "text-shadow: 2px 2px 0 #000, -2px 2px 0 #000, 2px -2px 0 #000, -2px -2px 0 #000, 0 2px 0 #000, 2px 0 0 #000, 0 -2px 0 #000, -2px 0 0 #000; }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/meme.css",++ "line": 135,++ "value": "#000",++ "text": "text-shadow: 2px 2px 0 #000, -2px 2px 0 #000, 2px -2px 0 #000, -2px -2px 0 #000, 0 2px 0 #000, 2px 0 0 #000, 0 -2px 0 #000, -2px 0 0 #000; }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/meme.css",++ "line": 135,++ "value": "#000",++ "text": "text-shadow: 2px 2px 0 #000, -2px 2px 0 #000, 2px -2px 0 #000, -2px -2px 0 #000, 0 2px 0 #000, 2px 0 0 #000, 0 -2px 0 #000, -2px 0 0 #000; }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/meme.css",++ "line": 135,++ "value": "#000",++ "text": "text-shadow: 2px 2px 0 #000, -2px 2px 0 #000, 2px -2px 0 #000, -2px -2px 0 #000, 0 2px 0 #000, 2px 0 0 #000, 0 -2px 0 #000, -2px 0 0 #000; }"++ }++ ]++ },++ {++ "repo": "OpenVibe.Network",++ "commit": "390bcc1ca2aa5fcd0e391244bb27a6edebd8fecf",++ "files_scanned": 31,++ "counts": {++ "literal_duration": 89,++ "literal_curve": 31,++ "spinner": 0,++ "color_drift": 212,++ "missing_reduced_motion": 2,++ "total": 334++ },++ "score": 55.4,++ "findings_total": 334,++ "findings": [++ {++ "rule": "missing_reduced_motion",++ "file": "public/css/admin.css",++ "line": 43,++ "value": "animation",++ "text": ".section{display:none;animation:sec-in .2s ease}"++ },++ {++ "rule": "missing_reduced_motion",++ "file": "public/css/login.css",++ "line": 17,++ "value": "animation",++ "text": "animation:orb-drift 20s ease-in-out infinite alternate;"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/admin.css",++ "line": 30,++ "value": ".15s",++ "text": "background:transparent;color:var(--text-muted);cursor:pointer;transition:all .15s;white-space:nowrap;"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/admin.css",++ "line": 36,++ "value": ".15s",++ "text": "background:var(--bg-secondary);color:var(--text-muted);cursor:pointer;transition:all .15s;opacity:0;pointer-events:none;"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/admin.css",++ "line": 43,++ "value": ".2s",++ "text": ".section{display:none;animation:sec-in .2s ease}"++ },++ {++ "rule": "literal_curve",++ "file": "public/css/admin.css",++ "line": 43,++ "value": "ease",++ "text": ".section{display:none;animation:sec-in .2s ease}"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/login.css",++ "line": 17,++ "value": "20s",++ "text": "animation:orb-drift 20s ease-in-out infinite alternate;"++ },++ {++ "rule": "literal_curve",++ "file": "public/css/login.css",++ "line": 17,++ "value": "ease-in-out",++ "text": "animation:orb-drift 20s ease-in-out infinite alternate;"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/login.css",++ "line": 49,++ "value": "30s",++ "text": "animation:grid-scroll 30s linear infinite;"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/my.css",++ "line": 38,++ "value": ".15s",++ "text": "background:transparent;color:var(--text-muted);cursor:pointer;transition:all .15s;white-space:nowrap;"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/my.css",++ "line": 65,++ "value": ".2s",++ "text": ".section{display:none;animation:sec-in .2s ease}"++ },++ {++ "rule": "literal_curve",++ "file": "public/css/my.css",++ "line": 65,++ "value": "ease",++ "text": ".section{display:none;animation:sec-in .2s ease}"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/my.css",++ "line": 82,++ "value": ".18s",++ "text": "transition:border-color .18s ease,box-shadow .18s ease,background-color .18s ease;"++ },++ {++ "rule": "literal_duration",++ "file": "public/index.html",++ "line": 65,++ "value": "24s",++ "text": ".bg-orbs .orb{position:absolute;border-radius:50%;filter:blur(110px);opacity:.14;animation:orb-drift 24s ease-in-out infinite alternate}"++ },++ {++ "rule": "literal_curve",++ "file": "public/index.html",++ "line": 65,++ "value": "ease-in-out",++ "text": ".bg-orbs .orb{position:absolute;border-radius:50%;filter:blur(110px);opacity:.14;animation:orb-drift 24s ease-in-out infinite alternate}"++ },++ {++ "rule": "literal_duration",++ "file": "public/index.html",++ "line": 88,++ "value": ".15s",++ "text": "transition:transform .15s, box-shadow .15s, background .15s, border-color .15s;"++ },++ {++ "rule": "literal_duration",++ "file": "public/index.html",++ "line": 88,++ "value": ".15s",++ "text": "transition:transform .15s, box-shadow .15s, background .15s, border-color .15s;"++ },++ {++ "rule": "literal_duration",++ "file": "public/sso-fanout.html",++ "line": 35,++ "value": ".4s",++ "text": ".card{width:100%;max-width:440px;background:var(--bg-card);border:1px solid var(--border);border-radius:18px;padding:30px 28px;position:relative;overflow:hidden"++ },++ {++ "rule": "literal_curve",++ "file": "public/sso-fanout.html",++ "line": 35,++ "value": "cubic-bezier(",++ "text": ".card{width:100%;max-width:440px;background:var(--bg-card);border:1px solid var(--border);border-radius:18px;padding:30px 28px;position:relative;overflow:hidden"++ },++ {++ "rule": "literal_duration",++ "file": "public/sso-fanout.html",++ "line": 42,++ "value": ".45s",++ "text": ".bar i{position:absolute;inset:0 auto 0 0;width:var(--w,0%);background:linear-gradient(90deg,var(--accent),var(--accent-light));transition:width .45s cubic-bezi"++ },++ {++ "rule": "literal_curve",++ "file": "public/sso-fanout.html",++ "line": 42,++ "value": "cubic-bezier(",++ "text": ".bar i{position:absolute;inset:0 auto 0 0;width:var(--w,0%);background:linear-gradient(90deg,var(--accent),var(--accent-light));transition:width .45s cubic-bezi"++ },++ {++ "rule": "literal_duration",++ "file": "public/verify-email.html",++ "line": 38,++ "value": ".5s",++ "text": ".card{width:100%;max-width:480px;background:var(--bg-card);border:1px solid var(--border);border-radius:18px;padding:36px 32px;text-align:center;position:relati"++ },++ {++ "rule": "literal_curve",++ "file": "public/verify-email.html",++ "line": 38,++ "value": "cubic-bezier(",++ "text": ".card{width:100%;max-width:480px;background:var(--bg-card);border:1px solid var(--border);border-radius:18px;padding:36px 32px;text-align:center;position:relati"++ },++ {++ "rule": "literal_duration",++ "file": "public/verify-email.html",++ "line": 39,++ "value": ".6s",++ "text": ".card::before{content:\"\";position:absolute;inset:-40% -20% auto;height:70%;background:radial-gradient(60% 60% at 50% 40%,rgba(var(--accent-rgb),.22),transparent"++ },++ {++ "rule": "literal_duration",++ "file": "public/verify-email.html",++ "line": 45,++ "value": "1.4s",++ "text": ".ring.spin{border-color:var(--accent);animation:ring-pulse 1.4s ease-out infinite}"++ }++ ]++ },++ {++ "repo": "OpenVibe.News",++ "commit": "5c1200cc31425da6aef045cff5cea7bd6c56fca7",++ "files_scanned": 2,++ "counts": {++ "literal_duration": 1,++ "literal_curve": 1,++ "spinner": 0,++ "color_drift": 15,++ "missing_reduced_motion": 0,++ "total": 17++ },++ "score": 98.75,++ "findings_total": 17,++ "findings": [++ {++ "rule": "literal_duration",++ "file": "public/css/briefs.css",++ "line": 14,++ "value": "1.6s",++ "text": ".live-dot { display: inline-block; width: .55em; height: .55em; border-radius: 50%; background: #ef4444; vertical-align: middle; animation: live-pulse 1.6s ease"++ },++ {++ "rule": "literal_curve",++ "file": "public/css/briefs.css",++ "line": 14,++ "value": "ease-in-out",++ "text": ".live-dot { display: inline-block; width: .55em; height: .55em; border-radius: 50%; background: #ef4444; vertical-align: middle; animation: live-pulse 1.6s ease"++ },++ {++ "rule": "color_drift",++ "file": "public/css/briefs.css",++ "line": 2,++ "value": "#a78bfa",++ "text": ".badge-ai { background: color-mix(in srgb, #a78bfa 18%, transparent); color: color-mix(in srgb, #8b5cf6 70%, var(--text-primary, #e6edf7)); border: 1px solid co"++ },++ {++ "rule": "color_drift",++ "file": "public/css/briefs.css",++ "line": 2,++ "value": "#8b5cf6",++ "text": ".badge-ai { background: color-mix(in srgb, #a78bfa 18%, transparent); color: color-mix(in srgb, #8b5cf6 70%, var(--text-primary, #e6edf7)); border: 1px solid co"++ },++ {++ "rule": "color_drift",++ "file": "public/css/briefs.css",++ "line": 2,++ "value": "#a78bfa",++ "text": ".badge-ai { background: color-mix(in srgb, #a78bfa 18%, transparent); color: color-mix(in srgb, #8b5cf6 70%, var(--text-primary, #e6edf7)); border: 1px solid co"++ },++ {++ "rule": "color_drift",++ "file": "public/css/briefs.css",++ "line": 3,++ "value": "#ef4444",++ "text": ".badge-breaking { background: color-mix(in srgb, #ef4444 16%, transparent); color: color-mix(in srgb, #ef4444 75%, var(--text-primary, #e6edf7)); border: 1px so"++ },++ {++ "rule": "color_drift",++ "file": "public/css/briefs.css",++ "line": 3,++ "value": "#ef4444",++ "text": ".badge-breaking { background: color-mix(in srgb, #ef4444 16%, transparent); color: color-mix(in srgb, #ef4444 75%, var(--text-primary, #e6edf7)); border: 1px so"++ },++ {++ "rule": "color_drift",++ "file": "public/css/briefs.css",++ "line": 3,++ "value": "#ef4444",++ "text": ".badge-breaking { background: color-mix(in srgb, #ef4444 16%, transparent); color: color-mix(in srgb, #ef4444 75%, var(--text-primary, #e6edf7)); border: 1px so"++ },++ {++ "rule": "color_drift",++ "file": "public/css/briefs.css",++ "line": 14,++ "value": "#ef4444",++ "text": ".live-dot { display: inline-block; width: .55em; height: .55em; border-radius: 50%; background: #ef4444; vertical-align: middle; animation: live-pulse 1.6s ease"++ },++ {++ "rule": "color_drift",++ "file": "public/css/briefs.css",++ "line": 17,++ "value": "#ef4444",++ "text": ".wire-cluster.is-breaking { border-left: 3px solid #ef4444; padding-left: 10px; }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/news.css",++ "line": 64,++ "value": "rgba(34,197,94,.25)",++ "text": ".ov-diff ins { background: rgba(34,197,94,.25); text-decoration: none; }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/news.css",++ "line": 65,++ "value": "rgba(239,68,68,.25)",++ "text": ".ov-diff del { background: rgba(239,68,68,.25); }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/news.css",++ "line": 90,++ "value": "#ef4444",++ "text": ".live-dot { display: inline-block; width: .55em; height: .55em; border-radius: 50%; background: #ef4444; vertical-align: middle; }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/news.css",++ "line": 91,++ "value": "#a78bfa",++ "text": ".badge-ai { background: color-mix(in srgb, #a78bfa 18%, transparent); color: color-mix(in srgb, #8b5cf6 70%, var(--text-primary, #e6edf7)); border: 1px solid co"++ },++ {++ "rule": "color_drift",++ "file": "public/css/news.css",++ "line": 91,++ "value": "#8b5cf6",++ "text": ".badge-ai { background: color-mix(in srgb, #a78bfa 18%, transparent); color: color-mix(in srgb, #8b5cf6 70%, var(--text-primary, #e6edf7)); border: 1px solid co"++ },++ {++ "rule": "color_drift",++ "file": "public/css/news.css",++ "line": 91,++ "value": "#a78bfa",++ "text": ".badge-ai { background: color-mix(in srgb, #a78bfa 18%, transparent); color: color-mix(in srgb, #8b5cf6 70%, var(--text-primary, #e6edf7)); border: 1px solid co"++ },++ {++ "rule": "color_drift",++ "file": "public/css/news.css",++ "line": 99,++ "value": "#8b5cf6",++ "text": ".nh-kicker .badge-ai { color: color-mix(in srgb, #8b5cf6 55%, var(--text-primary, #e6edf7)); } /* 4.49:1 at 70% on a card */"++ }++ ]++ },++ {++ "repo": "OpenVibe.Play",++ "commit": "d43308f86605aa099d43b012d60c7fe069e0aa9d",++ "files_scanned": 1,++ "counts": {++ "literal_duration": 0,++ "literal_curve": 0,++ "spinner": 0,++ "color_drift": 0,++ "missing_reduced_motion": 0,++ "total": 0++ },++ "score": 100,++ "findings_total": 0,++ "findings": []++ },++ {++ "repo": "OpenVibe.Quest",++ "commit": "3c8533e78a10a5d17c97ee4ba631a412865bf93e",++ "files_scanned": 1,++ "counts": {++ "literal_duration": 2,++ "literal_curve": 0,++ "spinner": 0,++ "color_drift": 8,++ "missing_reduced_motion": 0,++ "total": 10++ },++ "score": 99.1,++ "findings_total": 10,++ "findings": [++ {++ "rule": "literal_duration",++ "file": "public/css/app.css",++ "line": 61,++ "value": ".15s",++ "text": ".quest { display: grid; grid-template-columns: 56px minmax(0, 1fr); gap: 14px; padding: 16px; border-radius: 16px; border: 1px solid var(--border, #1f2d47); bac"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/app.css",++ "line": 61,++ "value": ".15s",++ "text": ".quest { display: grid; grid-template-columns: 56px minmax(0, 1fr); gap: 14px; padding: 16px; border-radius: 16px; border: 1px solid var(--border, #1f2d47); bac"++ },++ {++ "rule": "color_drift",++ "file": "public/css/app.css",++ "line": 63,++ "value": "hsl(var(--h)",++ "text": ".medal { width: 56px; height: 56px; border-radius: 50%; display: grid; place-items: center; font-weight: 800; font-size: 1rem; letter-spacing: .02em; color: hsl"++ },++ {++ "rule": "color_drift",++ "file": "public/css/app.css",++ "line": 63,++ "value": "hsl(var(--h)",++ "text": ".medal { width: 56px; height: 56px; border-radius: 50%; display: grid; place-items: center; font-weight: 800; font-size: 1rem; letter-spacing: .02em; color: hsl"++ },++ {++ "rule": "color_drift",++ "file": "public/css/app.css",++ "line": 63,++ "value": "hsl(var(--h)",++ "text": ".medal { width: 56px; height: 56px; border-radius: 50%; display: grid; place-items: center; font-weight: 800; font-size: 1rem; letter-spacing: .02em; color: hsl"++ },++ {++ "rule": "color_drift",++ "file": "public/css/app.css",++ "line": 63,++ "value": "hsl(var(--h)",++ "text": ".medal { width: 56px; height: 56px; border-radius: 50%; display: grid; place-items: center; font-weight: 800; font-size: 1rem; letter-spacing: .02em; color: hsl"++ },++ {++ "rule": "color_drift",++ "file": "public/css/app.css",++ "line": 63,++ "value": "hsl(var(--h)",++ "text": ".medal { width: 56px; height: 56px; border-radius: 50%; display: grid; place-items: center; font-weight: 800; font-size: 1rem; letter-spacing: .02em; color: hsl"++ },++ {++ "rule": "color_drift",++ "file": "public/css/app.css",++ "line": 64,++ "value": "hsl(var(--h)",++ "text": ".medal.earned { box-shadow: inset 0 0 0 3px hsl(var(--h) 80% 62%), 0 0 0 3px hsl(var(--h) 80% 62% / .25), 0 8px 20px -8px hsl(var(--h) 80% 50%); }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/app.css",++ "line": 64,++ "value": "hsl(var(--h)",++ "text": ".medal.earned { box-shadow: inset 0 0 0 3px hsl(var(--h) 80% 62%), 0 0 0 3px hsl(var(--h) 80% 62% / .25), 0 8px 20px -8px hsl(var(--h) 80% 50%); }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/app.css",++ "line": 64,++ "value": "hsl(var(--h)",++ "text": ".medal.earned { box-shadow: inset 0 0 0 3px hsl(var(--h) 80% 62%), 0 0 0 3px hsl(var(--h) 80% 62% / .25), 0 8px 20px -8px hsl(var(--h) 80% 50%); }"++ }++ ]++ },++ {++ "repo": "OpenVibe.Rent",++ "commit": "de271ae4d15931793564c520eb4aa6c46dc0d9c3",++ "files_scanned": 7,++ "counts": {++ "literal_duration": 4,++ "literal_curve": 2,++ "spinner": 0,++ "color_drift": 3,++ "missing_reduced_motion": 0,++ "total": 9++ },++ "score": 98.35,++ "findings_total": 9,++ "findings": [++ {++ "rule": "literal_duration",++ "file": "public/css/app.css",++ "line": 44,++ "value": ".15s",++ "text": ".listing { --k: 212; display: grid; grid-template-columns: 52px minmax(0, 1fr) auto; gap: 14px; align-items: center; padding: 14px 16px; border-radius: 14px; bo"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/app.css",++ "line": 44,++ "value": ".15s",++ "text": ".listing { --k: 212; display: grid; grid-template-columns: 52px minmax(0, 1fr) auto; gap: 14px; align-items: center; padding: 14px 16px; border-radius: 14px; bo"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/everywhere.css",++ "line": 7,++ "value": ".15s",++ "text": ".ew-site { display: flex; flex-direction: column; gap: 6px; padding: 12px 14px; border-radius: 12px; border: 1px solid var(--border, #1f2d47); background: var(-"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/everywhere.css",++ "line": 7,++ "value": ".15s",++ "text": ".ew-site { display: flex; flex-direction: column; gap: 6px; padding: 12px 14px; border-radius: 12px; border: 1px solid var(--border, #1f2d47); background: var(-"++ },++ {++ "rule": "literal_curve",++ "file": "public/css/everywhere.css",++ "line": 7,++ "value": "ease",++ "text": ".ew-site { display: flex; flex-direction: column; gap: 6px; padding: 12px 14px; border-radius: 12px; border: 1px solid var(--border, #1f2d47); background: var(-"++ },++ {++ "rule": "literal_curve",++ "file": "public/css/everywhere.css",++ "line": 7,++ "value": "ease",++ "text": ".ew-site { display: flex; flex-direction: column; gap: 6px; padding: 12px 14px; border-radius: 12px; border: 1px solid var(--border, #1f2d47); background: var(-"++ },++ {++ "rule": "color_drift",++ "file": "public/css/app.css",++ "line": 47,++ "value": "hsl(var(--k)",++ "text": ".listing-mark { width: 52px; height: 52px; border-radius: 13px; display: grid; place-items: center; font-size: .72rem; font-weight: 800; letter-spacing: .04em; "++ },++ {++ "rule": "color_drift",++ "file": "public/css/app.css",++ "line": 47,++ "value": "hsl(var(--k)",++ "text": ".listing-mark { width: 52px; height: 52px; border-radius: 13px; display: grid; place-items: center; font-size: .72rem; font-weight: 800; letter-spacing: .04em; "++ },++ {++ "rule": "color_drift",++ "file": "public/css/app.css",++ "line": 47,++ "value": "hsl(var(--k)",++ "text": ".listing-mark { width: 52px; height: 52px; border-radius: 13px; display: grid; place-items: center; font-size: .72rem; font-weight: 800; letter-spacing: .04em; "++ }++ ]++ },++ {++ "repo": "OpenVibe.Report",++ "commit": "dce6c1f9cb0ca4fed8f27e9f4336ff1b579c94f4",++ "files_scanned": 4,++ "counts": {++ "literal_duration": 2,++ "literal_curve": 0,++ "spinner": 0,++ "color_drift": 0,++ "missing_reduced_motion": 0,++ "total": 2++ },++ "score": 99.5,++ "findings_total": 2,++ "findings": [++ {++ "rule": "literal_duration",++ "file": "public/css/app.css",++ "line": 124,++ "value": ".18s",++ "text": ".rp-topic { display: flex; flex-direction: column; gap: 6px; height: 100%; padding: 14px 16px; border-radius: 16px; background: var(--bg-card, #0f1729); border:"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/app.css",++ "line": 124,++ "value": ".18s",++ "text": ".rp-topic { display: flex; flex-direction: column; gap: 6px; height: 100%; padding: 14px 16px; border-radius: 16px; background: var(--bg-card, #0f1729); border:"++ }++ ]++ },++ {++ "repo": "OpenVibe.Reviews",++ "commit": "2732d267d97e85bb1c4b858ce4e7199ff13f31d3",++ "files_scanned": 3,++ "counts": {++ "literal_duration": 0,++ "literal_curve": 0,++ "spinner": 0,++ "color_drift": 9,++ "missing_reduced_motion": 0,++ "total": 9++ },++ "score": 99.55,++ "findings_total": 9,++ "findings": [++ {++ "rule": "color_drift",++ "file": "public/css/reviews.css",++ "line": 10,++ "value": "--rv-red #f87171",++ "text": "--rv-red: #f87171;"++ },++ {++ "rule": "color_drift",++ "file": "public/css/reviews.css",++ "line": 11,++ "value": "--rv-ok #34d399",++ "text": "--rv-ok: #34d399;"++ },++ {++ "rule": "color_drift",++ "file": "public/css/reviews.css",++ "line": 12,++ "value": "--rv-warn #fbbf24",++ "text": "--rv-warn: #fbbf24;"++ },++ {++ "rule": "color_drift",++ "file": "public/css/reviews.css",++ "line": 35,++ "value": "#a78bfa",++ "text": ".rv-notice.rv-ai { border-color: #a78bfa; }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/reviews.css",++ "line": 36,++ "value": "#0a0f1c",++ "text": ".rv-button, button { display: inline-block; background: var(--rv-accent); color: #0a0f1c; border: 0; border-radius: 6px; padding: 6px 14px; font: inherit; font-"++ },++ {++ "rule": "color_drift",++ "file": "public/css/reviews.css",++ "line": 54,++ "value": "rgba(52, 211, 153, .22)",++ "text": ".ov-diff ins { background: rgba(52, 211, 153, .22); text-decoration: underline; }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/reviews.css",++ "line": 55,++ "value": "rgba(248, 113, 113, .22)",++ "text": ".ov-diff del { background: rgba(248, 113, 113, .22); }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/reviews.css",++ "line": 109,++ "value": "#f5b301",++ "text": ".rv-stars { color: #f5b301; letter-spacing: 1px; white-space: nowrap; }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/reviews.css",++ "line": 144,++ "value": "#dc2626",++ "text": ".rv-critical { background: color-mix(in srgb, #dc2626 16%, transparent); color: inherit; }"++ }++ ]++ },++ {++ "repo": "OpenVibe.Services",++ "commit": "248e48f1dc87f5d04e67d7944ac976c4b68ef345",++ "files_scanned": 3,++ "counts": {++ "literal_duration": 0,++ "literal_curve": 0,++ "spinner": 0,++ "color_drift": 0,++ "missing_reduced_motion": 0,++ "total": 0++ },++ "score": 100,++ "findings_total": 0,++ "findings": []++ },++ {++ "repo": "OpenVibe.Shoes",++ "commit": "ccc1f8a02f8dd49e44a1ba0122a0eadab59c0b05",++ "files_scanned": 2,++ "counts": {++ "literal_duration": 0,++ "literal_curve": 0,++ "spinner": 0,++ "color_drift": 3,++ "missing_reduced_motion": 0,++ "total": 3++ },++ "score": 99.85,++ "findings_total": 3,++ "findings": [++ {++ "rule": "color_drift",++ "file": "public/css/shoes.css",++ "line": 22,++ "value": "#fff",++ "text": ".sh-btn { display: inline-flex; align-items: center; gap: 7px; min-height: 40px; padding: 8px 14px; border: 1px solid var(--room-accent, var(--accent)); border-"++ },++ {++ "rule": "color_drift",++ "file": "public/css/shoes.css",++ "line": 98,++ "value": "rgba(0, 0, 0, .7)",++ "text": "box-shadow: 0 14px 30px -18px rgba(0, 0, 0, .7); color: var(--ovk-text, #e8ecf3); font-variant-numeric: tabular-nums;"++ },++ {++ "rule": "color_drift",++ "file": "public/css/shoes.css",++ "line": 105,++ "value": "rgba(0, 0, 0, .7)",++ "text": ".sh-mt.is-best { border-color: var(--room-accent); box-shadow: 0 0 0 1px var(--room-accent), 0 14px 30px -18px rgba(0, 0, 0, .7); }"++ }++ ]++ },++ {++ "repo": "OpenVibe.Sources",++ "commit": "ce8f4619ef9e5af96bd8e964419c2efe37f95c56",++ "files_scanned": 1,++ "counts": {++ "literal_duration": 0,++ "literal_curve": 0,++ "spinner": 0,++ "color_drift": 0,++ "missing_reduced_motion": 0,++ "total": 0++ },++ "score": 100,++ "findings_total": 0,++ "findings": []++ },++ {++ "repo": "OpenVibe.Space",++ "commit": "d9becca7da9443a04f2828cf4aa1599b90ab6188",++ "files_scanned": 5,++ "counts": {++ "literal_duration": 3,++ "literal_curve": 0,++ "spinner": 0,++ "color_drift": 29,++ "missing_reduced_motion": 0,++ "total": 32++ },++ "score": 97.8,++ "findings_total": 32,++ "findings": [++ {++ "rule": "literal_duration",++ "file": "public/css/hosting.css",++ "line": 129,++ "value": ".15s",++ "text": ".hd-tile { display: flex; gap: 14px; align-items: flex-start; padding: 18px; border: 1px solid var(--hd-line); border-radius: 16px; background: var(--hd-card); "++ },++ {++ "rule": "literal_duration",++ "file": "public/css/spaces.css",++ "line": 39,++ "value": ".15s",++ "text": ".sp-card { display: flex; flex-direction: column; border: 1px solid var(--border, #2c3d5c); border-radius: 16px; background: var(--bg-secondary, #101828); overf"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/spaces.css",++ "line": 39,++ "value": ".15s",++ "text": ".sp-card { display: flex; flex-direction: column; border: 1px solid var(--border, #2c3d5c); border-radius: 16px; background: var(--bg-secondary, #101828); overf"++ },++ {++ "rule": "color_drift",++ "file": "public/css/hosting.css",++ "line": 44,++ "value": "#fff",++ "text": ".hd-badge--accent { color: color-mix(in srgb, var(--accent-light, #60a5fa) 65%, #fff); border-color: color-mix(in srgb, var(--hd-accent) 50%, transparent); back"++ },++ {++ "rule": "color_drift",++ "file": "public/css/hosting.css",++ "line": 111,++ "value": "#fff",++ "text": ".hd-table a { color: color-mix(in srgb, var(--accent-light, #60a5fa) 70%, #fff); }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/spaces.css",++ "line": 20,++ "value": "#ef4444",++ "text": ".sp-flash.is-bad { border-color: #ef4444; color: #fecaca; background: color-mix(in srgb, #ef4444 12%, transparent); }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/spaces.css",++ "line": 20,++ "value": "#fecaca",++ "text": ".sp-flash.is-bad { border-color: #ef4444; color: #fecaca; background: color-mix(in srgb, #ef4444 12%, transparent); }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/spaces.css",++ "line": 20,++ "value": "#ef4444",++ "text": ".sp-flash.is-bad { border-color: #ef4444; color: #fecaca; background: color-mix(in srgb, #ef4444 12%, transparent); }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/spaces.css",++ "line": 34,++ "value": "#f87171",++ "text": ".sp-btn-danger { background: transparent; color: #f87171; border-color: color-mix(in srgb, #f87171 50%, transparent); }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/spaces.css",++ "line": 34,++ "value": "#f87171",++ "text": ".sp-btn-danger { background: transparent; color: #f87171; border-color: color-mix(in srgb, #f87171 50%, transparent); }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/spaces.css",++ "line": 46,++ "value": "#4ade80",++ "text": ".sp-card-badge.is-live { color: #4ade80; border-color: color-mix(in srgb, #4ade80 45%, transparent); }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/spaces.css",++ "line": 46,++ "value": "#4ade80",++ "text": ".sp-card-badge.is-live { color: #4ade80; border-color: color-mix(in srgb, #4ade80 45%, transparent); }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/spaces.css",++ "line": 59,++ "value": "#fff",++ "text": ".sp-template-icon { width: 40px; height: 40px; border-radius: 12px; display: grid; place-items: center; font-size: 18px; color: #fff; background: var(--tpl); }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/spaces.css",++ "line": 85,++ "value": "#fff",++ "text": ".sp-frame { margin: 20px 0 8px; border: 1px solid var(--border, #2c3d5c); border-radius: 16px; overflow: hidden; background: #fff; }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/spaces.css",++ "line": 114,++ "value": "#f87171",++ "text": ".sp-ed-tab .sp-ed-x:hover { opacity: 1; color: #f87171; }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/spaces.css",++ "line": 121,++ "value": "#f472b6",++ "text": ".sp-ed-code .cm-s-default .cm-tag { color: #f472b6; } .sp-ed-code .cm-s-default .cm-attribute { color: #fbbf24; } .sp-ed-code .cm-s-default .cm-string, .sp-ed-c"++ },++ {++ "rule": "color_drift",++ "file": "public/css/spaces.css",++ "line": 121,++ "value": "#fbbf24",++ "text": ".sp-ed-code .cm-s-default .cm-tag { color: #f472b6; } .sp-ed-code .cm-s-default .cm-attribute { color: #fbbf24; } .sp-ed-code .cm-s-default .cm-string, .sp-ed-c"++ },++ {++ "rule": "color_drift",++ "file": "public/css/spaces.css",++ "line": 121,++ "value": "#4ade80",++ "text": ".sp-ed-code .cm-s-default .cm-tag { color: #f472b6; } .sp-ed-code .cm-s-default .cm-attribute { color: #fbbf24; } .sp-ed-code .cm-s-default .cm-string, .sp-ed-c"++ },++ {++ "rule": "color_drift",++ "file": "public/css/spaces.css",++ "line": 122,++ "value": "#c084fc",++ "text": ".sp-ed-code .cm-s-default .cm-keyword { color: #c084fc; } .sp-ed-code .cm-s-default .cm-comment { color: #7b8aa3; font-style: italic; } .sp-ed-code .cm-s-defaul"++ },++ {++ "rule": "color_drift",++ "file": "public/css/spaces.css",++ "line": 122,++ "value": "#7b8aa3",++ "text": ".sp-ed-code .cm-s-default .cm-keyword { color: #c084fc; } .sp-ed-code .cm-s-default .cm-comment { color: #7b8aa3; font-style: italic; } .sp-ed-code .cm-s-defaul"++ },++ {++ "rule": "color_drift",++ "file": "public/css/spaces.css",++ "line": 122,++ "value": "#fb923c",++ "text": ".sp-ed-code .cm-s-default .cm-keyword { color: #c084fc; } .sp-ed-code .cm-s-default .cm-comment { color: #7b8aa3; font-style: italic; } .sp-ed-code .cm-s-defaul"++ },++ {++ "rule": "color_drift",++ "file": "public/css/spaces.css",++ "line": 123,++ "value": "#60a5fa",++ "text": ".sp-ed-code .cm-s-default .cm-def, .sp-ed-code .cm-s-default .cm-property { color: #60a5fa; } .sp-ed-code .cm-s-default .cm-variable { color: var(--text-primary"++ },++ {++ "rule": "color_drift",++ "file": "public/css/spaces.css",++ "line": 124,++ "value": "#93c5fd",++ "text": ".sp-ed-code .cm-s-default .cm-variable-2, .sp-ed-code .cm-s-default .cm-variable-3, .sp-ed-code .cm-s-default .cm-type { color: #93c5fd; }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/spaces.css",++ "line": 125,++ "value": "#fcd34d",++ "text": ".sp-ed-code .cm-s-default .cm-atom, .sp-ed-code .cm-s-default .cm-qualifier, .sp-ed-code .cm-s-default .cm-builtin { color: #fcd34d; }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/spaces.css",++ "line": 126,++ "value": "#a5b4c8",++ "text": ".sp-ed-code .cm-s-default .cm-meta, .sp-ed-code .cm-s-default .cm-bracket, .sp-ed-code .cm-s-default .cm-operator { color: #a5b4c8; }"++ }++ ]++ },++ {++ "repo": "OpenVibe.Surf",++ "commit": "e605c50166437344f97e78283a39d33bce37636e",++ "files_scanned": 4,++ "counts": {++ "literal_duration": 9,++ "literal_curve": 5,++ "spinner": 0,++ "color_drift": 23,++ "missing_reduced_motion": 0,++ "total": 37++ },++ "score": 95.35,++ "findings_total": 37,++ "findings": [++ {++ "rule": "literal_duration",++ "file": "public/css/surf.css",++ "line": 45,++ "value": "1.1s",++ "text": "html.ov-motion .sf-run-speed i { animation: sf-speed 1.1s linear infinite; animation-delay: calc(var(--i) * -.12s); }"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/surf.css",++ "line": 55,++ "value": ".15s",++ "text": ".sf-tile a { display: grid; gap: 8px; height: 100%; padding: 18px; border-radius: 20px; border: 1px solid var(--border, #1f2d47); background: var(--bg-card, #0f"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/surf.css",++ "line": 55,++ "value": ".15s",++ "text": ".sf-tile a { display: grid; gap: 8px; height: 100%; padding: 18px; border-radius: 20px; border: 1px solid var(--border, #1f2d47); background: var(--bg-card, #0f"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/surf.css",++ "line": 78,++ "value": "1.4s",++ "text": ".sf-demo-steps li.is-now::before { border-color: var(--room-accent); animation: sf-pulse 1.4s ease-out infinite; } /* the step on now pulses out, never spins "++ },++ {++ "rule": "literal_curve",++ "file": "public/css/surf.css",++ "line": 78,++ "value": "ease-out",++ "text": ".sf-demo-steps li.is-now::before { border-color: var(--room-accent); animation: sf-pulse 1.4s ease-out infinite; } /* the step on now pulses out, never spins "++ },++ {++ "rule": "literal_duration",++ "file": "public/css/surf.css",++ "line": 89,++ "value": ".15s",++ "text": ".sf-map-link { display: grid; grid-template-columns: 84px minmax(0, 1fr); gap: 14px; align-items: center; padding: 12px; border-radius: 18px; border: 1px solid "++ },++ {++ "rule": "literal_duration",++ "file": "public/css/surf.css",++ "line": 129,++ "value": ".28s",++ "text": ".sf-tv-screen.is-changing { animation: sf-change .28s ease; }"++ },++ {++ "rule": "literal_curve",++ "file": "public/css/surf.css",++ "line": 129,++ "value": "ease",++ "text": ".sf-tv-screen.is-changing { animation: sf-change .28s ease; }"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/surf.css",++ "line": 160,++ "value": "2.4s",++ "text": "html.ov-motion .sf-party-dot { animation: sf-party-pulse 2.4s ease-in-out infinite; }"++ },++ {++ "rule": "literal_curve",++ "file": "public/css/surf.css",++ "line": 160,++ "value": "ease-in-out",++ "text": "html.ov-motion .sf-party-dot { animation: sf-party-pulse 2.4s ease-in-out infinite; }"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/surf.css",++ "line": 209,++ "value": "1.6s",++ "text": "html.ov-motion .sf-party-playing .sf-party-dot { animation: sf-pulse 1.6s ease-out infinite; }"++ },++ {++ "rule": "literal_curve",++ "file": "public/css/surf.css",++ "line": 209,++ "value": "ease-out",++ "text": "html.ov-motion .sf-party-playing .sf-party-dot { animation: sf-pulse 1.6s ease-out infinite; }"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/surf.css",++ "line": 227,++ "value": "1.6s",++ "text": ".sf-desk-page[data-sf-desk-open=\"1\"] .sf-desk-state::before { background: #22c55e; animation: sf-desk-pulse 1.6s ease-out infinite; }"++ },++ {++ "rule": "literal_curve",++ "file": "public/css/surf.css",++ "line": 227,++ "value": "ease-out",++ "text": ".sf-desk-page[data-sf-desk-open=\"1\"] .sf-desk-state::before { background: #22c55e; animation: sf-desk-pulse 1.6s ease-out infinite; }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/surf.css",++ "line": 47,++ "value": "rgba(0, 0, 0, .55)",++ "text": ".sf-run-hud { position: absolute; left: 50%; bottom: 10%; translate: -50% 0; display: grid; justify-items: center; gap: 2px; padding: 8px 16px; border-radius: 1"++ },++ {++ "rule": "color_drift",++ "file": "public/css/surf.css",++ "line": 47,++ "value": "#fff",++ "text": ".sf-run-hud { position: absolute; left: 50%; bottom: 10%; translate: -50% 0; display: grid; justify-items: center; gap: 2px; padding: 8px 16px; border-radius: 1"++ },++ {++ "rule": "color_drift",++ "file": "public/css/surf.css",++ "line": 66,++ "value": "#0b0f17",++ "text": ".sf-demo-run { display: grid; gap: 10px; padding: 16px; border-radius: 18px; background: #0b0f17; color: #e9f2ff; border: 1px solid color-mix(in srgb, var(--roo"++ },++ {++ "rule": "color_drift",++ "file": "public/css/surf.css",++ "line": 66,++ "value": "#e9f2ff",++ "text": ".sf-demo-run { display: grid; gap: 10px; padding: 16px; border-radius: 18px; background: #0b0f17; color: #e9f2ff; border: 1px solid color-mix(in srgb, var(--roo"++ },++ {++ "rule": "color_drift",++ "file": "public/css/surf.css",++ "line": 69,++ "value": "#4ade80",++ "text": ".sf-demo-hud .is-good { color: #4ade80; font-variant-numeric: tabular-nums; }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/surf.css",++ "line": 70,++ "value": "rgba(255, 255, 255, .12)",++ "text": ".sf-demo-bar { height: 6px; border-radius: 6px; background: rgba(255, 255, 255, .12); overflow: hidden; }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/surf.css",++ "line": 71,++ "value": "#00bef6",++ "text": ".sf-demo-bar i { display: block; height: 100%; background: #00bef6; border-radius: inherit; }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/surf.css",++ "line": 82,++ "value": "#fff",++ "text": ".sf-demo-screen { position: relative; display: flex; align-items: flex-end; gap: 8px; aspect-ratio: 16 / 6; max-height: 210px; padding: 12px; border-radius: 12p"++ },++ {++ "rule": "color_drift",++ "file": "public/css/surf.css",++ "line": 83,++ "value": "#1e293b",++ "text": "background: radial-gradient(120% 120% at 70% 30%, color-mix(in srgb, var(--room-accent) 45%, transparent), transparent 60%), linear-gradient(160deg, #1e293b, #0"++ },++ {++ "rule": "color_drift",++ "file": "public/css/surf.css",++ "line": 83,++ "value": "#0b1020",++ "text": "background: radial-gradient(120% 120% at 70% 30%, color-mix(in srgb, var(--room-accent) 45%, transparent), transparent 60%), linear-gradient(160deg, #1e293b, #0"++ },++ {++ "rule": "color_drift",++ "file": "public/css/surf.css",++ "line": 85,++ "value": "#fff",++ "text": ".sf-pill-live { padding: 1px 7px; border-radius: 5px; background: var(--live-red, #e11d48); color: #fff; font-size: .68rem; font-weight: 800; }"++ }++ ]++ },++ {++ "repo": "OpenVibe.Tips",++ "commit": "eb708f169ed98840d56f248f07f4e5da998c875c",++ "files_scanned": 4,++ "counts": {++ "literal_duration": 3,++ "literal_curve": 0,++ "spinner": 0,++ "color_drift": 9,++ "missing_reduced_motion": 0,++ "total": 12++ },++ "score": 98.8,++ "findings_total": 12,++ "findings": [++ {++ "rule": "literal_duration",++ "file": "public/css/overlay.css",++ "line": 3,++ "value": ".35s",++ "text": "#alert { position: fixed; left: 50%; top: 12%; transform: translateX(-50%); width: min(90vw, 760px); text-align: center; opacity: 0; transition: opacity .35s, t"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/overlay.css",++ "line": 3,++ "value": ".35s",++ "text": "#alert { position: fixed; left: 50%; top: 12%; transform: translateX(-50%); width: min(90vw, 760px); text-align: center; opacity: 0; transition: opacity .35s, t"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/overlay.css",++ "line": 14,++ "value": ".6s",++ "text": ".g .bar span { display: block; height: 100%; background: #3b82f6; transition: width .6s; }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/tips.css",++ "line": 10,++ "value": "--tips-bad #f87171",++ "text": "--tips-bad: #f87171;"++ },++ {++ "rule": "color_drift",++ "file": "public/css/tips.css",++ "line": 11,++ "value": "--tips-good #34d399",++ "text": "--tips-good: #34d399;"++ },++ {++ "rule": "color_drift",++ "file": "public/css/tips.css",++ "line": 40,++ "value": "rgba(59, 130, 246, .12)",++ "text": ".notice { background: rgba(59, 130, 246, .12); border: 1px solid rgba(59, 130, 246, .4); border-radius: 8px; padding: 10px 14px; }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/tips.css",++ "line": 40,++ "value": "rgba(59, 130, 246, .4)",++ "text": ".notice { background: rgba(59, 130, 246, .12); border: 1px solid rgba(59, 130, 246, .4); border-radius: 8px; padding: 10px 14px; }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/tips.css",++ "line": 41,++ "value": "rgba(248, 113, 113, .12)",++ "text": ".notice.error { background: rgba(248, 113, 113, .12); border-color: rgba(248, 113, 113, .5); }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/tips.css",++ "line": 41,++ "value": "rgba(248, 113, 113, .5)",++ "text": ".notice.error { background: rgba(248, 113, 113, .12); border-color: rgba(248, 113, 113, .5); }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/tips.css",++ "line": 112,++ "value": "#b91c1c",++ "text": ".tp-live { display: inline-block; padding: 1px 8px; border-radius: 999px; font-size: .72rem; font-weight: 800; letter-spacing: .04em; text-transform: uppercase;"++ },++ {++ "rule": "color_drift",++ "file": "public/css/tips.css",++ "line": 112,++ "value": "#000",++ "text": ".tp-live { display: inline-block; padding: 1px 8px; border-radius: 999px; font-size: .72rem; font-weight: 800; letter-spacing: .04em; text-transform: uppercase;"++ },++ {++ "rule": "color_drift",++ "file": "public/css/tips.css",++ "line": 112,++ "value": "#fff",++ "text": ".tp-live { display: inline-block; padding: 1px 8px; border-radius: 999px; font-size: .72rem; font-weight: 800; letter-spacing: .04em; text-transform: uppercase;"++ }++ ]++ },++ {++ "repo": "OpenVibe.Today",++ "commit": "2d77c36fbc70f6aeae7c59645822d5bd48631df6",++ "files_scanned": 3,++ "counts": {++ "literal_duration": 5,++ "literal_curve": 3,++ "spinner": 0,++ "color_drift": 9,++ "missing_reduced_motion": 0,++ "total": 17++ },++ "score": 97.55,++ "findings_total": 17,++ "findings": [++ {++ "rule": "literal_duration",++ "file": "public/css/today.css",++ "line": 37,++ "value": "1.4s",++ "text": ".td-pill.is-live::before { content: \"\"; width: 6px; height: 6px; border-radius: 50%; background: #fff; animation: td-pulse 1.4s ease-in-out infinite; }"++ },++ {++ "rule": "literal_curve",++ "file": "public/css/today.css",++ "line": 37,++ "value": "ease-in-out",++ "text": ".td-pill.is-live::before { content: \"\"; width: 6px; height: 6px; border-radius: 50%; background: #fff; animation: td-pulse 1.4s ease-in-out infinite; }"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/today.css",++ "line": 48,++ "value": ".15s",++ "text": ".td-ev-link { display: flex; align-items: center; gap: 14px; padding: 10px 14px 10px 10px; border-radius: 16px; border: 1px solid var(--border, #1f2d47); backgr"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/today.css",++ "line": 48,++ "value": ".15s",++ "text": ".td-ev-link { display: flex; align-items: center; gap: 14px; padding: 10px 14px 10px 10px; border-radius: 16px; border: 1px solid var(--border, #1f2d47); backgr"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/today.css",++ "line": 73,++ "value": ".6s",++ "text": "html.ov-motion .td-ring-dot { animation: td-dot-in .6s cubic-bezier(.2, .8, .2, 1) both; animation-delay: calc(var(--i) * 90ms + 200ms); }"++ },++ {++ "rule": "literal_curve",++ "file": "public/css/today.css",++ "line": 73,++ "value": "cubic-bezier(",++ "text": "html.ov-motion .td-ring-dot { animation: td-dot-in .6s cubic-bezier(.2, .8, .2, 1) both; animation-delay: calc(var(--i) * 90ms + 200ms); }"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/today.css",++ "line": 95,++ "value": "4.5s",++ "text": "html.ov-motion .td-demo-toast { animation: td-toast 4.5s ease-in-out infinite; }"++ },++ {++ "rule": "literal_curve",++ "file": "public/css/today.css",++ "line": 95,++ "value": "ease-in-out",++ "text": "html.ov-motion .td-demo-toast { animation: td-toast 4.5s ease-in-out infinite; }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/today.css",++ "line": 36,++ "value": "#fff",++ "text": ".td-pill.is-live { background: var(--live-red, #e11d48); color: #fff; }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/today.css",++ "line": 37,++ "value": "#fff",++ "text": ".td-pill.is-live::before { content: \"\"; width: 6px; height: 6px; border-radius: 50%; background: #fff; animation: td-pulse 1.4s ease-in-out infinite; }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/today.css",++ "line": 92,++ "value": "rgba(0, 0, 0, .3)",++ "text": ".td-demo-toast { display: flex; align-items: center; gap: 12px; margin-left: 10%; padding: 12px 14px; border-radius: 16px; background: var(--bg-elevated, var(--"++ },++ {++ "rule": "color_drift",++ "file": "public/css/today.css",++ "line": 206,++ "value": "#f87171",++ "text": ".td-remind-out.is-bad { color: #f87171; }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/today.css",++ "line": 252,++ "value": "--k #22c55e",++ "text": ".td-wi.is-band { --k: color-mix(in srgb, var(--room-accent) 55%, #22c55e); }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/today.css",++ "line": 306,++ "value": "#22c55e",++ "text": ".td-remind-ok { display: grid; place-items: center; width: 52px; height: 52px; margin-bottom: 10px; border-radius: 16px; background: color-mix(in srgb, #22c55e "++ },++ {++ "rule": "color_drift",++ "file": "public/css/today.css",++ "line": 306,++ "value": "#22c55e",++ "text": ".td-remind-ok { display: grid; place-items: center; width: 52px; height: 52px; margin-bottom: 10px; border-radius: 16px; background: color-mix(in srgb, #22c55e "++ },++ {++ "rule": "color_drift",++ "file": "public/css/today.css",++ "line": 308,++ "value": "#ef4444",++ "text": ".td-notice--bad { padding: 12px 14px; border-radius: 12px; border: 1px solid color-mix(in srgb, #ef4444 50%, var(--border, #1f2d47)); background: color-mix(in s"++ },++ {++ "rule": "color_drift",++ "file": "public/css/today.css",++ "line": 308,++ "value": "#ef4444",++ "text": ".td-notice--bad { padding: 12px 14px; border-radius: 12px; border: 1px solid color-mix(in srgb, #ef4444 50%, var(--border, #1f2d47)); background: color-mix(in s"++ }++ ]++ },++ {++ "repo": "OpenVibe.Tools",++ "commit": "1f1ac7afc05d127533887403dd4dbfb6430c3e64",++ "files_scanned": 69,++ "counts": {++ "literal_duration": 157,++ "literal_curve": 44,++ "spinner": 0,++ "color_drift": 399,++ "missing_reduced_motion": 11,++ "total": 611++ },++ "score": 7.8,++ "findings_total": 611,++ "findings": [++ {++ "rule": "missing_reduced_motion",++ "file": "apps/audio/public/css/style.css",++ "line": 213,++ "value": "animation",++ "text": ".option-group { animation: fadeIn .25s ease; }"++ },++ {++ "rule": "missing_reduced_motion",++ "file": "apps/docs/public/css/style.css",++ "line": 342,++ "value": "animation",++ "text": "animation: spin 1.4s ease-out infinite;"++ },++ {++ "rule": "missing_reduced_motion",++ "file": "apps/food/public/css/style.css",++ "line": 212,++ "value": "animation",++ "text": ".spinner { width:24px; height:24px; border: 3px solid var(--green); --spin-c: var(--green); border-radius:50%; animation: spin 1.4s ease-out infinite; margin:0 "++ },++ {++ "rule": "missing_reduced_motion",++ "file": "apps/gateway/public/audio.html",++ "line": 67,++ "value": "animation",++ "text": ".showcase-tool { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 20px; border-radius: 16px; background: var(--bg-c"++ },++ {++ "rule": "missing_reduced_motion",++ "file": "apps/gateway/public/dev.html",++ "line": 35,++ "value": "animation",++ "text": ".bg-glow::before,.bg-glow::after{content:'';position:absolute;border-radius:50%;filter:blur(120px);opacity:.35;animation:gd 20s ease-in-out infinite alternate}"++ },++ {++ "rule": "missing_reduced_motion",++ "file": "apps/gateway/public/img.html",++ "line": 67,++ "value": "animation",++ "text": ".showcase-tool { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 20px; border-radius: 16px; background: var(--bg-c"++ },++ {++ "rule": "missing_reduced_motion",++ "file": "apps/gateway/public/net.html",++ "line": 74,++ "value": "animation",++ "text": "animation: glow-drift 20s ease-in-out infinite alternate;"++ },++ {++ "rule": "missing_reduced_motion",++ "file": "apps/img/public/css/style.css",++ "line": 337,++ "value": "animation",++ "text": "animation: spin 1.4s ease-out infinite;"++ },++ {++ "rule": "missing_reduced_motion",++ "file": "apps/maps/public/css/style.css",++ "line": 48,++ "value": "animation",++ "text": ".splash-content { text-align: center; animation: fadeIn 0.5s ease; }"++ },++ {++ "rule": "missing_reduced_motion",++ "file": "apps/text/public/css/shared.css",++ "line": 305,++ "value": "animation",++ "text": "animation: livePulse 2s ease-in-out infinite;"++ },++ {++ "rule": "missing_reduced_motion",++ "file": "apps/yt/public/css/style.css",++ "line": 172,++ "value": "animation",++ "text": "animation: spin 1.4s ease-out infinite;"++ },++ {++ "rule": "literal_duration",++ "file": "apps/gateway/public/codes.html",++ "line": 56,++ "value": ".15s",++ "text": ".cx-tabs a{display:inline-flex;align-items:center;gap:6px;flex:none;padding:7px 12px;border-radius:9px;color:var(--c-tx2);text-decoration:none;font-size:.86rem;"++ },++ {++ "rule": "literal_duration",++ "file": "apps/text/public/badge.html",++ "line": 38,++ "value": ".2s",++ "text": ".shape-pill { padding: .4rem .9rem; border-radius: 20px; border: 1px solid var(--border); background: var(--bg-tertiary); color: var(--text-secondary); font-siz"++ },++ {++ "rule": "literal_duration",++ "file": "apps/text/public/ice.html",++ "line": 44,++ "value": "0.2s",++ "text": "transition: border-color 0.2s, transform 0.2s;"++ },++ {++ "rule": "literal_duration",++ "file": "apps/text/public/peso.html",++ "line": 68,++ "value": "0.3s",++ "text": "transition: transform 0.3s, background 0.2s;"++ },++ {++ "rule": "literal_duration",++ "file": "apps/text/public/slang.html",++ "line": 46,++ "value": "0.2s",++ "text": "font-weight: 600; transition: all 0.2s; font-family: 'Inter', sans-serif;"++ },++ {++ "rule": "literal_duration",++ "file": "apps/text/public/spanish.html",++ "line": 34,++ "value": "0.2s",++ "text": "transition: border-color 0.2s;"++ },++ {++ "rule": "literal_duration",++ "file": "apps/text/public/thumbnail.html",++ "line": 40,++ "value": ".2s",++ "text": ".upload-btn { width: 100%; padding: .75rem; border-radius: 12px; border: 2px dashed var(--border); background: var(--bg-tertiary); color: var(--text-secondary);"++ },++ {++ "rule": "literal_duration",++ "file": "apps/text/public/title.html",++ "line": 36,++ "value": ".2s",++ "text": ".preset-btn { padding: .5rem .25rem; font-size: .7rem; border-radius: 10px; cursor: pointer; border: 2px solid var(--border); background: var(--bg-tertiary); co"++ },++ {++ "rule": "literal_duration",++ "file": "apps/text/public/watermark.html",++ "line": 29,++ "value": ".2s",++ "text": ".drop-zone { border: 2px dashed var(--border); border-radius: var(--radius); padding: 2rem; text-align: center; cursor: pointer; transition: all .2s; }"++ },++ {++ "rule": "literal_duration",++ "file": "apps/text/public/wordmark.html",++ "line": 25,++ "value": ".2s",++ "text": ".wm-card { background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; cursor: pointer; transition: all .2s; }"++ },++ {++ "rule": "color_drift",++ "file": "apps/text/public/compare.html",++ "line": 28,++ "value": "rgba(34,197,94,0.15)",++ "text": ".diff-add { background: rgba(34,197,94,0.15); color: #4ade80; text-decoration: none; }"++ },++ {++ "rule": "color_drift",++ "file": "apps/text/public/json.html",++ "line": 29,++ "value": "#93c5fd",++ "text": ".json-key { color: #93c5fd; }"++ },++ {++ "rule": "color_drift",++ "file": "apps/text/public/transparent.html",++ "line": 25,++ "value": "#141414",++ "text": ".canvas-wrap { background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius); padding: 2rem; text-align: center; margin: 2rem 0; bac"++ },++ {++ "rule": "color_drift",++ "file": "apps/text/public/unicode.html",++ "line": 30,++ "value": "rgba(255, 0, 0, 0.1)",++ "text": ".char-invisible { background: rgba(255, 0, 0, 0.1); }"++ }++ ]++ },++ {++ "repo": "OpenVibe.Tours",++ "commit": "c04d2bc4e070188f55ede2a11c367c7c0003e7c1",++ "files_scanned": 4,++ "counts": {++ "literal_duration": 0,++ "literal_curve": 0,++ "spinner": 0,++ "color_drift": 13,++ "missing_reduced_motion": 0,++ "total": 13++ },++ "score": 99.35,++ "findings_total": 13,++ "findings": [++ {++ "rule": "color_drift",++ "file": "public/css/tours.css",++ "line": 32,++ "value": "#0a0f1c",++ "text": ".tr-pin-n { fill: #0a0f1c; font-size: 9px; font-weight: 800; }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/tours.css",++ "line": 33,++ "value": "rgba(0,0,0,.4)",++ "text": ".tr-walk { filter: drop-shadow(0 2px 8px rgba(0,0,0,.4)); }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/tours.css",++ "line": 50,++ "value": "#b44444",++ "text": ".tr-note-error { border-color: #b44444; color: #ffb4b4; }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/tours.css",++ "line": 50,++ "value": "#ffb4b4",++ "text": ".tr-note-error { border-color: #b44444; color: #ffb4b4; }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/tours.css",++ "line": 51,++ "value": "#8a6a1f",++ "text": ".tr-note-warn { border-color: #8a6a1f; color: #ffd98a; }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/tours.css",++ "line": 51,++ "value": "#ffd98a",++ "text": ".tr-note-warn { border-color: #8a6a1f; color: #ffd98a; }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/tours.css",++ "line": 81,++ "value": "#ef4444",++ "text": ".tr-live-card img, .tr-live-noimg { width: 100%; aspect-ratio: 16/9; object-fit: cover; border-radius: 10px; background: var(--bg-primary, #0a0f1c); display: gr"++ },++ {++ "rule": "color_drift",++ "file": "public/css/tours.css",++ "line": 82,++ "value": "#ef4444",++ "text": ".tr-live-live { position: absolute; top: 16px; left: 16px; padding: 2px 8px; border-radius: 999px; background: #ef4444; color: #fff; font-size: .7rem; font-weig"++ },++ {++ "rule": "color_drift",++ "file": "public/css/tours.css",++ "line": 82,++ "value": "#fff",++ "text": ".tr-live-live { position: absolute; top: 16px; left: 16px; padding: 2px 8px; border-radius: 999px; background: #ef4444; color: #fff; font-size: .7rem; font-weig"++ },++ {++ "rule": "color_drift",++ "file": "public/css/tours.css",++ "line": 83,++ "value": "#ef4444",++ "text": ".tr-live-dot { display: inline-block; width: 9px; height: 9px; border-radius: 50%; background: #ef4444; }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/tours.css",++ "line": 93,++ "value": "#0a0f1c",++ "text": ".tr-map-pin { width: 26px; height: 26px; border-radius: 50%; display: grid; place-items: center; background: var(--room-accent, #F98942); color: #0a0f1c; font-w"++ },++ {++ "rule": "color_drift",++ "file": "public/css/tours.css",++ "line": 93,++ "value": "#0a0f1c",++ "text": ".tr-map-pin { width: 26px; height: 26px; border-radius: 50%; display: grid; place-items: center; background: var(--room-accent, #F98942); color: #0a0f1c; font-w"++ },++ {++ "rule": "color_drift",++ "file": "public/css/tours.css",++ "line": 96,++ "value": "#0a0f1c",++ "text": ".tr-stop-num { width: 30px; height: 30px; border-radius: 50%; display: grid; place-items: center; background: var(--room-accent, #F98942); color: #0a0f1c; font-"++ }++ ]++ },++ {++ "repo": "OpenVibe.Trade",++ "commit": "48bcb670dd00d6206e35ae6e81a9263cd0710f64",++ "files_scanned": 1,++ "counts": {++ "literal_duration": 1,++ "literal_curve": 0,++ "spinner": 0,++ "color_drift": 1,++ "missing_reduced_motion": 0,++ "total": 2++ },++ "score": 99.7,++ "findings_total": 2,++ "findings": [++ {++ "rule": "literal_duration",++ "file": "public/css/trade.css",++ "line": 78,++ "value": ".15s",++ "text": ".tr-stats a { display: flex; flex-direction: column; gap: 2px; height: 100%; padding: 14px 16px; border-radius: 14px; border: 1px solid var(--border, #1f2d47); "++ },++ {++ "rule": "color_drift",++ "file": "public/css/trade.css",++ "line": 30,++ "value": "rgba(245, 158, 11, .07)",++ "text": "tr.is-stale td, tr.is-stale th { background: rgba(245, 158, 11, .07); }"++ }++ ]++ },++ {++ "repo": "OpenVibe.VIP",++ "commit": "daab64a5fbe7c4706bcd82f073b5d20cc3f48a34",++ "files_scanned": 2,++ "counts": {++ "literal_duration": 1,++ "literal_curve": 0,++ "spinner": 0,++ "color_drift": 8,++ "missing_reduced_motion": 0,++ "total": 9++ },++ "score": 99.35,++ "findings_total": 9,++ "findings": [++ {++ "rule": "literal_duration",++ "file": "public/css/vip.css",++ "line": 55,++ "value": ".15s",++ "text": ".creator-list a { display: flex; align-items: center; gap: 12px; height: 100%; background: var(--vip-card); border: 1px solid var(--vip-border); border-radius: "++ },++ {++ "rule": "color_drift",++ "file": "public/css/vip.css",++ "line": 13,++ "value": "--vip-bad #f87171",++ "text": "--vip-bad: #f87171;"++ },++ {++ "rule": "color_drift",++ "file": "public/css/vip.css",++ "line": 14,++ "value": "--vip-good #34d399",++ "text": "--vip-good: #34d399;"++ },++ {++ "rule": "color_drift",++ "file": "public/css/vip.css",++ "line": 49,++ "value": "rgba(59, 130, 246, .12)",++ "text": ".notice { background: rgba(59, 130, 246, .12); border: 1px solid rgba(59, 130, 246, .4); border-radius: 8px; padding: 10px 14px; }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/vip.css",++ "line": 49,++ "value": "rgba(59, 130, 246, .4)",++ "text": ".notice { background: rgba(59, 130, 246, .12); border: 1px solid rgba(59, 130, 246, .4); border-radius: 8px; padding: 10px 14px; }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/vip.css",++ "line": 50,++ "value": "rgba(248, 113, 113, .12)",++ "text": ".notice-error { background: rgba(248, 113, 113, .12); border-color: rgba(248, 113, 113, .5); }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/vip.css",++ "line": 50,++ "value": "rgba(248, 113, 113, .5)",++ "text": ".notice-error { background: rgba(248, 113, 113, .12); border-color: rgba(248, 113, 113, .5); }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/vip.css",++ "line": 63,++ "value": "rgba(255, 255, 255, .06)",++ "text": "code { background: rgba(255, 255, 255, .06); padding: 1px 5px; border-radius: 4px; }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/vip.css",++ "line": 79,++ "value": "#1a0505",++ "text": ".vip-live-pill { flex: none; display: inline-block; border-radius: 999px; padding: 0 8px; font-size: .75rem; font-weight: 700; letter-spacing: .04em; text-trans"++ }++ ]++ },++ {++ "repo": "OpenVibe.Vision",++ "commit": "45060caa486acaad2b9bd97dc1fac2cc4786e2c1",++ "files_scanned": 4,++ "counts": {++ "literal_duration": 11,++ "literal_curve": 5,++ "spinner": 0,++ "color_drift": 7,++ "missing_reduced_motion": 0,++ "total": 23++ },++ "score": 95.65,++ "findings_total": 23,++ "findings": [++ {++ "rule": "literal_duration",++ "file": "public/css/vision.css",++ "line": 61,++ "value": "1.8s",++ "text": "html.ov-motion .vi-race-img path { stroke-dasharray: 420; animation: vi-draw 1.8s cubic-bezier(.2,.7,.2,1) both; }"++ },++ {++ "rule": "literal_curve",++ "file": "public/css/vision.css",++ "line": 61,++ "value": "cubic-bezier(",++ "text": "html.ov-motion .vi-race-img path { stroke-dasharray: 420; animation: vi-draw 1.8s cubic-bezier(.2,.7,.2,1) both; }"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/vision.css",++ "line": 68,++ "value": ".6s",++ "text": "html.ov-motion .vi-race-box { animation: vi-pop .6s cubic-bezier(.2,.9,.3,1.2) both; } html.ov-motion .vi-race-b { animation-delay: .35s; }"++ },++ {++ "rule": "literal_curve",++ "file": "public/css/vision.css",++ "line": 68,++ "value": "cubic-bezier(",++ "text": "html.ov-motion .vi-race-box { animation: vi-pop .6s cubic-bezier(.2,.9,.3,1.2) both; } html.ov-motion .vi-race-b { animation-delay: .35s; }"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/vision.css",++ "line": 108,++ "value": ".15s",++ "text": ".vi-task { display: grid; gap: 6px; height: 100%; padding: 16px 18px; border-radius: 18px; border: 1px solid var(--border, #1f2d47); background: var(--bg-card, "++ },++ {++ "rule": "literal_duration",++ "file": "public/css/vision.css",++ "line": 108,++ "value": ".15s",++ "text": ".vi-task { display: grid; gap: 6px; height: 100%; padding: 16px 18px; border-radius: 18px; border: 1px solid var(--border, #1f2d47); background: var(--bg-card, "++ },++ {++ "rule": "literal_duration",++ "file": "public/css/vision.css",++ "line": 122,++ "value": ".2s",++ "text": ".vi-demo-switch::after { content: ''; position: absolute; top: 3px; left: 3px; width: 16px; height: 16px; border-radius: 50%; background: var(--text-muted, #738"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/vision.css",++ "line": 129,++ "value": ".2s",++ "text": ".vi-check > input[role=switch] { appearance: none; -webkit-appearance: none; position: relative; flex: none; width: 44px; height: 26px; margin: 0; border-radius"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/vision.css",++ "line": 130,++ "value": ".2s",++ "text": ".vi-check > input[role=switch]::after { content: ''; position: absolute; top: 3px; left: 3px; width: 20px; height: 20px; border-radius: 50%; background: var(--t"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/vision.css",++ "line": 130,++ "value": ".2s",++ "text": ".vi-check > input[role=switch]::after { content: ''; position: absolute; top: 3px; left: 3px; width: 20px; height: 20px; border-radius: 50%; background: var(--t"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/vision.css",++ "line": 144,++ "value": ".15s",++ "text": ".vi-drop { position: relative; display: grid; justify-items: center; align-content: center; gap: 6px; min-height: 170px; padding: 24px 20px; border: 2px dashed "++ },++ {++ "rule": "literal_duration",++ "file": "public/css/vision.css",++ "line": 144,++ "value": ".15s",++ "text": ".vi-drop { position: relative; display: grid; justify-items: center; align-content: center; gap: 6px; min-height: 170px; padding: 24px 20px; border: 2px dashed "++ },++ {++ "rule": "literal_curve",++ "file": "public/css/vision.css",++ "line": 144,++ "value": "ease",++ "text": ".vi-drop { position: relative; display: grid; justify-items: center; align-content: center; gap: 6px; min-height: 170px; padding: 24px 20px; border: 2px dashed "++ },++ {++ "rule": "literal_curve",++ "file": "public/css/vision.css",++ "line": 144,++ "value": "ease",++ "text": ".vi-drop { position: relative; display: grid; justify-items: center; align-content: center; gap: 6px; min-height: 170px; padding: 24px 20px; border: 2px dashed "++ },++ {++ "rule": "literal_duration",++ "file": "public/css/vision.css",++ "line": 258,++ "value": ".15s",++ "text": ".vi-options summary::after, .vi-disclose summary::after, .vi-api summary::after { content: ''; flex: none; width: 8px; height: 8px; margin-left: auto; border-ri"++ },++ {++ "rule": "literal_curve",++ "file": "public/css/vision.css",++ "line": 258,++ "value": "ease",++ "text": ".vi-options summary::after, .vi-disclose summary::after, .vi-api summary::after { content: ''; flex: none; width: 8px; height: 8px; margin-left: auto; border-ri"++ },++ {++ "rule": "color_drift",++ "file": "public/css/vision.css",++ "line": 73,++ "value": "#fbfaf6",++ "text": ".vi-demo-receipt { display: grid; gap: 4px; width: min(100%, 300px); margin: 0 auto; padding: 18px 20px; border-radius: 6px; background: #fbfaf6; color: #222; f"++ },++ {++ "rule": "color_drift",++ "file": "public/css/vision.css",++ "line": 73,++ "value": "#222",++ "text": ".vi-demo-receipt { display: grid; gap: 4px; width: min(100%, 300px); margin: 0 auto; padding: 18px 20px; border-radius: 6px; background: #fbfaf6; color: #222; f"++ },++ {++ "rule": "color_drift",++ "file": "public/css/vision.css",++ "line": 73,++ "value": "rgba(0, 0, 0, .25)",++ "text": ".vi-demo-receipt { display: grid; gap: 4px; width: min(100%, 300px); margin: 0 auto; padding: 18px 20px; border-radius: 6px; background: #fbfaf6; color: #222; f"++ },++ {++ "rule": "color_drift",++ "file": "public/css/vision.css",++ "line": 74,++ "value": "#999",++ "text": ".vi-demo-receipt strong { margin-top: 6px; padding-top: 6px; border-top: 1px dashed #999; font-size: 1.05rem; }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/vision.css",++ "line": 75,++ "value": "#6b6b6b",++ "text": ".vi-demo-receipt small { color: #6b6b6b; font-size: .74rem; }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/vision.css",++ "line": 76,++ "value": "#fff",++ "text": ".vi-demo-kicker { justify-self: start; padding: 2px 8px; border-radius: 6px; font: 800 .68rem/1.4 Inter, sans-serif; letter-spacing: .08em; color: #fff; backgro"++ },++ {++ "rule": "color_drift",++ "file": "public/css/vision.css",++ "line": 76,++ "value": "#6b4ab0",++ "text": ".vi-demo-kicker { justify-self: start; padding: 2px 8px; border-radius: 6px; font: 800 .68rem/1.4 Inter, sans-serif; letter-spacing: .08em; color: #fff; backgro"++ }++ ]++ },++ {++ "repo": "OpenVibe.Watch",++ "commit": "eb5557e97c5075c757946d2c60a5551541b13f11",++ "files_scanned": 2,++ "counts": {++ "literal_duration": 1,++ "literal_curve": 0,++ "spinner": 0,++ "color_drift": 0,++ "missing_reduced_motion": 0,++ "total": 1++ },++ "score": 99.75,++ "findings_total": 1,++ "findings": [++ {++ "rule": "literal_duration",++ "file": "public/css/home.css",++ "line": 13,++ "value": ".15s",++ "text": ".hm-card { display: flex; flex-direction: column; justify-content: space-between; gap: 10px; min-width: 0; padding: 14px 16px; border-radius: 16px; border: 1px "++ }++ ]++ },++ {++ "repo": "OpenVibe.Wiki",++ "commit": "ee24a8beabde07168ad0f8cf36f2b6fd8ab2df78",++ "files_scanned": 2,++ "counts": {++ "literal_duration": 6,++ "literal_curve": 3,++ "spinner": 0,++ "color_drift": 7,++ "missing_reduced_motion": 1,++ "total": 17++ },++ "score": 95.4,++ "findings_total": 17,++ "findings": [++ {++ "rule": "missing_reduced_motion",++ "file": "public/css/vibe.css",++ "line": 31,++ "value": "animation",++ "text": ".wk-vp-steps .wk-vp-step-now { border-color: var(--wk-accent); color: var(--wk-text); background: color-mix(in srgb, var(--wk-accent) 12%, transparent); animati"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/vibe.css",++ "line": 14,++ "value": ".15s",++ "text": ".wk-vibe-sizes span { display: block; height: 100%; padding: 10px 12px; border: 1px solid var(--wk-line); border-radius: 12px; background: var(--wk-panel); tran"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/vibe.css",++ "line": 14,++ "value": ".15s",++ "text": ".wk-vibe-sizes span { display: block; height: 100%; padding: 10px 12px; border: 1px solid var(--wk-line); border-radius: 12px; background: var(--wk-panel); tran"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/vibe.css",++ "line": 31,++ "value": "1.6s",++ "text": ".wk-vp-steps .wk-vp-step-now { border-color: var(--wk-accent); color: var(--wk-text); background: color-mix(in srgb, var(--wk-accent) 12%, transparent); animati"++ },++ {++ "rule": "literal_curve",++ "file": "public/css/vibe.css",++ "line": 31,++ "value": "ease-in-out",++ "text": ".wk-vp-steps .wk-vp-step-now { border-color: var(--wk-accent); color: var(--wk-text); background: color-mix(in srgb, var(--wk-accent) 12%, transparent); animati"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/vibe.css",++ "line": 36,++ "value": ".2s",++ "text": ".wk-vp-pages li { display: flex; flex-direction: column; gap: 2px; padding: 10px 12px; border: 1px solid var(--wk-line); border-radius: 12px; background: var(--"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/vibe.css",++ "line": 42,++ "value": "1s",++ "text": ".wk-vp-pages .wk-vp-writing small::before { content: \"\"; display: inline-block; width: 8px; height: 8px; margin-right: 6px; border-radius: 50%; background: var("++ },++ {++ "rule": "literal_curve",++ "file": "public/css/vibe.css",++ "line": 42,++ "value": "ease-in-out",++ "text": ".wk-vp-pages .wk-vp-writing small::before { content: \"\"; display: inline-block; width: 8px; height: 8px; margin-right: 6px; border-radius: 50%; background: var("++ },++ {++ "rule": "literal_duration",++ "file": "public/css/vibe.css",++ "line": 45,++ "value": "1.2s",++ "text": ".wk-vp-dot { width: 10px; height: 10px; border-radius: 50%; background: var(--wk-accent); animation: wk-pulse 1.2s ease-in-out infinite; }"++ },++ {++ "rule": "literal_curve",++ "file": "public/css/vibe.css",++ "line": 45,++ "value": "ease-in-out",++ "text": ".wk-vp-dot { width: 10px; height: 10px; border-radius: 50%; background: var(--wk-accent); animation: wk-pulse 1.2s ease-in-out infinite; }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/vibe.css",++ "line": 18,++ "value": "#f472b6",++ "text": ".wk-vibe h1 span { background: linear-gradient(90deg, var(--wk-accent), color-mix(in srgb, var(--wk-accent) 40%, #f472b6)); -webkit-background-clip: text; backg"++ },++ {++ "rule": "color_drift",++ "file": "public/css/wiki.css",++ "line": 10,++ "value": "--wk-red #f87171",++ "text": "--wk-red: #f87171;"++ },++ {++ "rule": "color_drift",++ "file": "public/css/wiki.css",++ "line": 11,++ "value": "--wk-ok #34d399",++ "text": "--wk-ok: #34d399;"++ },++ {++ "rule": "color_drift",++ "file": "public/css/wiki.css",++ "line": 12,++ "value": "--wk-warn #fbbf24",++ "text": "--wk-warn: #fbbf24;"++ },++ {++ "rule": "color_drift",++ "file": "public/css/wiki.css",++ "line": 35,++ "value": "#a78bfa",++ "text": ".wk-notice.wk-ai { border-color: #a78bfa; }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/wiki.css",++ "line": 56,++ "value": "rgba(52, 211, 153, .22)",++ "text": ".ov-diff ins { background: rgba(52, 211, 153, .22); text-decoration: underline; }"++ },++ {++ "rule": "color_drift",++ "file": "public/css/wiki.css",++ "line": 57,++ "value": "rgba(248, 113, 113, .22)",++ "text": ".ov-diff del { background: rgba(248, 113, 113, .22); }"++ }++ ]++ },++ {++ "repo": "OpenVibe.Work",++ "commit": "44cba11abad694b8bd895aa2275675760d51f3e8",++ "files_scanned": 3,++ "counts": {++ "literal_duration": 4,++ "literal_curve": 2,++ "spinner": 0,++ "color_drift": 3,++ "missing_reduced_motion": 0,++ "total": 9++ },++ "score": 98.35,++ "findings_total": 9,++ "findings": [++ {++ "rule": "literal_duration",++ "file": "public/css/app.css",++ "line": 62,++ "value": ".15s",++ "text": ".job-item { display: grid; grid-template-columns: 44px minmax(0, 1fr); gap: 14px; padding: 14px 16px; border-radius: 14px; border: 1px solid var(--border, #1f2d"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/app.css",++ "line": 62,++ "value": ".15s",++ "text": ".job-item { display: grid; grid-template-columns: 44px minmax(0, 1fr); gap: 14px; padding: 14px 16px; border-radius: 14px; border: 1px solid var(--border, #1f2d"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/home.css",++ "line": 11,++ "value": ".15s",++ "text": ".home-cards a { display: flex; flex-direction: column; gap: 3px; height: 100%; box-sizing: border-box; padding: 12px 14px; border-radius: 12px; border: 1px soli"++ },++ {++ "rule": "literal_duration",++ "file": "public/css/home.css",++ "line": 11,++ "value": ".15s",++ "text": ".home-cards a { display: flex; flex-direction: column; gap: 3px; height: 100%; box-sizing: border-box; padding: 12px 14px; border-radius: 12px; border: 1px soli"++ },++ {++ "rule": "literal_curve",++ "file": "public/css/home.css",++ "line": 11,++ "value": "ease",++ "text": ".home-cards a { display: flex; flex-direction: column; gap: 3px; height: 100%; box-sizing: border-box; padding: 12px 14px; border-radius: 12px; border: 1px soli"++ },++ {++ "rule": "literal_curve",++ "file": "public/css/home.css",++ "line": 11,++ "value": "ease",++ "text": ".home-cards a { display: flex; flex-direction: column; gap: 3px; height: 100%; box-sizing: border-box; padding: 12px 14px; border-radius: 12px; border: 1px soli"++ },++ {++ "rule": "color_drift",++ "file": "public/css/app.css",++ "line": 64,++ "value": "hsl(var(--h)",++ "text": ".job-logo { width: 44px; height: 44px; border-radius: 12px; display: grid; place-items: center; font-weight: 800; font-size: 1.1rem; color: hsl(var(--h) 80% 78%"++ },++ {++ "rule": "color_drift",++ "file": "public/css/app.css",++ "line": 64,++ "value": "hsl(var(--h)",++ "text": ".job-logo { width: 44px; height: 44px; border-radius: 12px; display: grid; place-items: center; font-weight: 800; font-size: 1.1rem; color: hsl(var(--h) 80% 78%"++ },++ {++ "rule": "color_drift",++ "file": "public/css/app.css",++ "line": 64,++ "value": "hsl(var(--h)",++ "text": ".job-logo { width: 44px; height: 44px; border-radius: 12px; display: grid; place-items: center; font-weight: 800; font-size: 1.1rem; color: hsl(var(--h) 80% 78%"++ }++ ]++ },++ {++ "repo": "OpenVibe.Zone",++ "commit": "6ac0483c6338c55ecc8bfe355a5263d276c0e9a2",++ "files_scanned": 1,++ "counts": {++ "literal_duration": 0,++ "literal_curve": 0,++ "spinner": 0,++ "color_drift": 0,++ "missing_reduced_motion": 0,++ "total": 0++ },++ "score": 100,++ "findings_total": 0,++ "findings": []++ }++ ]++}@@@ -9,6 +9,7 @@* GET /api/v1/rundown/t/:slug* GET /api/v1/after-show/:streamId* GET /api/v1/c/:username++ * GET /api/v1/design-health, /api/v1/design-health/:site every site's design health (a snapshot, server/design-health/)** A bad parameter is checked here, before the source is asked, and is a 404 problem+json, exactly like a source* that answered nothing. Every /api/v1 answer is CORS-open and public for two minutes; the app mounts the 120/min@@@ -24,6 +25,7 @@ const { normaliseDays, isSlug, isStreamId, isUsername, streamIdOf } = require('.const { createServiceAuth, CAPS } = require('../auth');const { GROUPS: AI_USAGE_GROUPS, PERSON_GROUPS } = require('../analytics/ai-usage');const GROUPS = Object.keys(AI_USAGE_GROUPS);++const designHealth = require('../data/design-health');const CACHE = 'public, max-age=120';@@@ -69,6 +71,15 @@ function createApi({ network, live, analytics = null, aiUsage = null, config = {return res.json(stats);});++ // Every site's design health (plan L45 step 6): the snapshot scripts/design-health.js wrote from each site's code.++ r.get('/design-health', (req, res) => res.json(designHealth.all()));++ r.get('/design-health/:site', (req, res) => {++ const site = designHealth.site(req.params.site);++ if (!site) return notFound(req, res, 'report.design_site_not_found', 'No site by that name in the design health snapshot.');++ const { generated_at, rules, weights, kept } = designHealth.all();++ return res.json({ generated_at, rules, weights, kept, site });++ });++r.get('/rundown', async (req, res) => {const rundown = await live.rundown();if (!rundown) return notFound(req, res, 'report.rundown_not_found', 'OpenVibe.Live did not answer with The Rundown.');@@@ -4,7 +4,7 @@* Crawl artifacts for openvibe.report, built with openvibe-shared/seo:** GET /robots.txt the public pages are crawlable; the API is not-- * GET /sitemap.xml /, /stats, /rundown and every /rundown/t/<slug> from the Rundown's topics++ * GET /sitemap.xml /, /stats, /design, /rundown and every /rundown/t/<slug> from the Rundown's topics* GET /llms.txt a markdown map of the site and its machine-readable answers** The sitemap reads topics through the same cached client as the pages, so a crawler is no heavier than a reader@@@ -53,6 +53,7 @@ function createDiscoveryRoutes({ config, live, bench }) {const pages = [{ loc: abs('/'), changefreq: 'daily', priority: 1.0 },{ loc: abs('/stats'), changefreq: 'daily', priority: 0.9 },++ { loc: abs('/design'), changefreq: 'weekly', priority: 0.6 },{ loc: abs('/rundown'), changefreq: 'daily', priority: 0.9 },{ loc: abs('/ai'), changefreq: 'daily', priority: 0.9 },...kinds.filter((k) => k && bench.isKindId(k.id)).map((k) => ({@@@ -76,11 +77,13 @@ function createDiscoveryRoutes({ config, live, bench }) {{ title: 'Start here', links: [{ title: 'OpenVibe.Report', url: abs('/'), note: 'the Report room' },{ title: 'Open numbers', url: abs('/stats'), note: 'page views, accounts and traffic for every OpenVibe site' },++ { title: 'Design health', url: abs('/design'), note: 'how closely each site follows the shared design kit, read from its code; per site at /design/<site>' },{ title: 'The Rundown', url: abs('/rundown'), note: 'what streamers talked about, topic by topic' },{ title: 'AI boards', url: abs('/ai'), note: 'Bench ratings, quality and cost across rooms' },] },{ title: 'Machine-readable', links: [{ title: 'Open stats (JSON)', url: abs('/api/v1/stats?days=7'), note: '?days=7, 30 or 90' },++ { title: 'Design health (JSON)', url: abs('/api/v1/design-health'), note: 'every site; /api/v1/design-health/<site> for one' },{ title: 'The Rundown (JSON)', url: abs('/api/v1/rundown') },{ title: 'Topics (JSON)', url: abs('/api/v1/rundown/topics') },{ title: 'Sitemap', url: abs('/sitemap.xml') },@@@ -22,6 +22,8 @@ const express = require('express');const views = require('../views');const { normaliseDays, isSlug, isStreamId, isUsername, streamIdOf } = require('../data/source');const { twinPath } = require('../agent-pages');++const designViews = require('../views/design');++const designHealth = require('../data/design-health');const CACHE = 'public, max-age=120';@@@ -92,6 +94,14 @@ function createPageRoutes(ctx) {r.get('/developers', (req, res) => send(req, res, publicPages.developers().html));++ // Every site's design health (plan L45 step 6), from the snapshot (server/data/design-health.js).++ r.get('/design', (req, res) => send(req, res, designViews.designHealth(designHealth.all())));++ r.get('/design/:site', (req, res) => {++ const site = designHealth.site(req.params.site);++ if (!site) return notFound(req, res);++ return send(req, res, designViews.designSite(designHealth.all(), site));++ });++r.get('/stats', async (req, res) => {const page = await publicPages.stats(normaliseDays(req.query.days));if (!page) return notFound(req, res);nnew file mode 100644@@@ -0,0 +1,102 @@++'use strict';++/**++ * openvibe.report/design: how closely every OpenVibe site follows the shared design kit (plan L45 step 6), read from++ * each site's code (server/design-health/scan.js, snapshot in server/design-health/snapshot.json).++ *++ * designHealth(snapshot) /design: the network at a glance, every site most-to-fix first, and how it is scored++ * designSite(snapshot, site) /design/:site: one site's counts and where, each finding linked to its line on GitHub++ */++const kit = require('openvibe-shared/kit');++const { layout } = require('./index');++++const esc = kit.esc;++const exact = (v) => (v == null ? '—' : Number(v).toLocaleString('en-US'));++const day = (s) => { const t = Date.parse(s || ''); return Number.isFinite(t) ? new Date(t).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric', timeZone: 'UTC' }) : ''; };++const IN_VIEW = 15; // sites before the counted fold++++// The rules in the order they matter, in words a reader knows.++const RULES = [++ ['spinner', 'Spinners', 'A keyframe that turns a whole circle, or an animation that runs one forever. Nothing on OpenVibe spins: a loading state breathes, bobs or fills.'],++ ['missing_reduced_motion', 'No reduced motion', 'A stylesheet that animates and never asks for prefers-reduced-motion, so a person who asked for less movement still gets it.'],++ ['literal_duration', 'Hand-set timings', 'A transition or animation time written by hand (.25s, 300ms) where the kit says var(--ov-dur-1…4), so a look\'s motion and Calm cannot change it.'],++ ['literal_curve', 'Hand-set curves', 'A cubic-bezier() or an ease keyword where the kit says var(--ov-ease-out), var(--ov-ease-spring) or var(--ov-ease-in-out).'],++ ['color_drift', 'Colours outside the theme', 'A colour written by hand in a file that already uses the theme\'s variables: it stays put when a person changes their look.'],++];++const LABEL = Object.fromEntries(RULES.map(([k, l]) => [k, l]));++const SHORT = { spinner: 'Spins', missing_reduced_motion: 'No reduced motion', literal_duration: 'Timing', literal_curve: 'Curve', color_drift: 'Colour' };++// A count in words, for a phone's one-line summary of a site ("931 timings 448 curves").++const WORDS = { spinner: 'spinning', missing_reduced_motion: 'without reduced motion', literal_duration: 'timings', literal_curve: 'curves', color_drift: 'colours' };++++const band = (s) => (s >= 90 ? 'is-good' : s >= 60 ? 'is-ok' : 'is-low');++/** The score as a number on a bar: 0 to 100, coloured by band, with its words for a screen reader. */++const scoreBar = (s) => `<span class="dh-score ${band(Number(s))}" style="--dh:${Math.max(0, Math.min(100, Number(s) || 0))}%"><b>${esc(Number(s).toFixed(Number.isInteger(Number(s)) ? 0 : 1))}</b><i aria-hidden="true"></i></span>`;++const repoUrl = (s) => `https://github.com/OpenVibers/${encodeURIComponent(s.repo)}`;++const median = (xs) => { const v = xs.filter((x) => Number.isFinite(x)).sort((a, b) => a - b); if (!v.length) return null; const m = v.length >> 1; return v.length % 2 ? v[m] : (v[m - 1] + v[m]) / 2; };++++function siteRow(s) {++ const c = s.counts || {};++ return `<div class="rp-tr dh-tr" role="row"><span role="cell" class="rp-site"><a href="/design/${esc(s.slug)}">${esc(s.repo)}</a><small>${exact(s.files_scanned)} ${Number(s.files_scanned) === 1 ? 'file' : 'files'}</small></span>`++ + `<span role="cell" data-l="Score">${scoreBar(s.score)}</span>`++ + RULES.map(([k]) => `<span role="cell" data-l="${esc(LABEL[k])}" data-s="${esc(WORDS[k])}" class="dh-n${c[k] ? '' : ' is-zero'}${k === 'spinner' && c[k] ? ' is-bad' : ''}">${exact(c[k] || 0)}</span>`).join('')++ + (Number(c.total) ? '' : '<span role="cell" class="dh-none">Nothing to fix</span>')++ + '</div>';++}++const tableHead = `<div class="rp-tr rp-th dh-tr" role="row"><span role="columnheader">Site</span><span role="columnheader">Score</span>${RULES.map(([k]) => `<span role="columnheader">${esc(LABEL[k])}</span>`).join('')}</div>`;++++function designHealth(snap) {++ const sites = (snap.sites || []).slice().sort((a, b) => a.score - b.score || a.repo.localeCompare(b.repo));++ const sum = (k) => sites.reduce((n, s) => n + (Number(s.counts && s.counts[k]) || 0), 0);++ const spins = sum('spinner'), still = sum('missing_reduced_motion');++ const clean = sites.filter((s) => Number(s.score) >= 100).length;++ const head = sites.slice(0, IN_VIEW), rest = sites.slice(IN_VIEW);++ const W = snap.weights || {};++ const body = `<header class="rp-page-h"><p class="rp-kicker">Open numbers</p><h1>Design health of every site</h1>++<p class="rp-lede">Read from each site's own code: motion through the kit's timings and curves, colours from the theme, a pause for people who ask for less movement, and nothing that spins. 100 is a site that follows the kit everywhere it was read.${snap.generated_at ? ` Measured ${esc(day(snap.generated_at))}, at each site's main branch.` : ''}</p></header>++<section class="rp-tiles" aria-label="Across the network">++<div class="rp-tile"><span class="rp-tile-l">Sites measured</span><b class="rp-tile-v">${exact(sites.length)}</b><span class="rp-tile-s">${exact(clean)} at 100</span></div>++<div class="rp-tile"><span class="rp-tile-l">Median score</span><b class="rp-tile-v">${median(sites.map((s) => Number(s.score))) == null ? '—' : esc(String(Math.round(median(sites.map((s) => Number(s.score))) * 10) / 10))}</b><span class="rp-tile-s">of 100</span></div>++<div class="rp-tile"><span class="rp-tile-l">Spinners</span><b class="rp-tile-v">${spins ? exact(spins) : 'None'}</b><span class="rp-tile-s">${spins ? 'still to replace' : 'nothing spins'}</span></div>++<div class="rp-tile"><span class="rp-tile-l">Files without reduced motion</span><b class="rp-tile-v">${still ? exact(still) : 'None'}</b><span class="rp-tile-s">${still ? 'animate for everyone' : 'every animation can stop'}</span></div>++</section>++<section aria-labelledby="dh-sites-h"><h2 id="dh-sites-h">Every site</h2><p class="rp-fine">Most to fix first. A site's page shows where, line by line.</p>++${sites.length ? `<div class="rp-table dh-table" role="table" aria-label="Design health, most to fix first">${tableHead}${head.map(siteRow).join('')}</div>++${rest.length ? `<details class="dh-fold"><summary>All ${exact(sites.length)} sites <span>${exact(rest.length)} more</span></summary><div class="rp-table dh-table" role="table" aria-label="Design health, the rest">${tableHead}${rest.map(siteRow).join('')}</div></details>` : ''}`++ : '<p class="rp-fine">No measurement yet.</p>'}++</section>++<section aria-labelledby="dh-how-h"><h2 id="dh-how-h">How it is measured</h2>++<dl class="dh-rules">${RULES.map(([k, l, what]) => `<div><dt>${esc(l)} <span>−${esc(String(W[k] != null ? W[k] : ''))} each</span></dt><dd>${esc(what)}</dd></div>`).join('')}</dl>++<p class="rp-fine">A site's score is 100 minus what its findings cost, never below 0. The rules read every stylesheet, and every <style> in a page or script, under a site's <code>public/</code> and <code>server/views/</code>; a site with none there is left out. The kit's own tokens and the fallback inside <code>var(--name, fallback)</code> never count. The rules are open: <a href="https://github.com/OpenVibers/OpenVibe.Report/blob/main/server/design-health/scan.js"><code>server/design-health/scan.js</code></a>, run by <code>scripts/design-health.js</code>. The same numbers: <a href="/api/v1/design-health"><code>/api/v1/design-health</code></a>.</p></section>`;++ return layout({++ title: 'Design health of every site',++ description: 'How closely each OpenVibe site follows the shared design kit: motion timings and curves, theme colours, reduced motion and nothing that spins, read from its code.',++ body, active: '/stats', path: '/design',++ });++}++++function designSite(snap, s) {++ const c = s.counts || {};++ const commit = String(s.commit || '');++ const finds = (s.findings || []).map((f) => {++ const where = `${f.file}${f.line ? `:${f.line}` : ''}`;++ const href = commit ? `${repoUrl(s)}/blob/${encodeURIComponent(commit)}/${String(f.file).split('/').map(encodeURIComponent).join('/')}${f.line ? `#L${Number(f.line)}` : ''}` : '';++ return `<li class="dh-find"><span class="dh-rule dh-rule--${esc(String(f.rule).replace(/_/g, '-'))}">${esc(SHORT[f.rule] || f.rule)}</span>`++ + `${href ? `<a class="dh-where" href="${esc(href)}">${esc(where)}</a>` : `<span class="dh-where">${esc(where)}</span>`}`++ + `${f.text ? `<code class="dh-text">${esc(f.text)}</code>` : ''}</li>`;++ }).join('');++ const total = Number(s.findings_total) || 0;++ const body = `<header class="rp-page-h"><p class="rp-kicker"><a href="/design">Design health</a></p><h1>${esc(s.repo)}</h1>++<p class="rp-lede dh-lede">${scoreBar(s.score)} <span>${exact(s.files_scanned)} ${Number(s.files_scanned) === 1 ? 'file' : 'files'} read${commit ? ` at <a href="${esc(`${repoUrl(s)}/tree/${encodeURIComponent(commit)}`)}"><code>${esc(commit.slice(0, 7))}</code></a>` : ''}${snap.generated_at ? `, ${esc(day(snap.generated_at))}` : ''}.</span></p></header>++<section class="rp-tiles" aria-label="Findings by rule">${RULES.map(([k, l]) => `<div class="rp-tile"><span class="rp-tile-l">${esc(l)}</span><b class="rp-tile-v">${exact(c[k] || 0)}</b></div>`).join('')}</section>++<section aria-labelledby="dh-where-h"><h2 id="dh-where-h">Where</h2>++${total ? `<p class="rp-fine">${total > (s.findings || []).length ? `${exact((s.findings || []).length)} of ${exact(total)}: the ones that cost most, from as many files as possible.` : `All ${exact(total)}.`} Each opens its line on GitHub.</p><ol class="dh-finds">${finds}</ol>`++ : '<p class="rp-fine">Nothing to fix: this site follows the kit everywhere the rules read.</p>'}++</section>++<p class="rp-fine"><a href="/design">Every site</a> · <a href="/design#dh-how-h">How it is measured</a> · <a href="/api/v1/design-health/${esc(s.slug)}"><code>/api/v1/design-health/${esc(s.slug)}</code></a></p>`;++ return layout({++ title: `${s.repo}: design health`,++ description: `How closely ${s.repo} follows the shared design kit: score ${s.score} of 100, with every finding by rule and line.`,++ body, active: '/stats', path: `/design/${s.slug}`,++ });++}++++module.exports = { designHealth, designSite, scoreBar };@@@ -280,7 +280,7 @@ ${sites ? `<section aria-labelledby="rp-sites-h"><h2 id="rp-sites-h">Every site<<div class="rp-table" role="table" aria-label="Every site"><div class="rp-tr rp-th" role="row"><span role="columnheader">Site</span><span role="columnheader">Page views</span><span role="columnheader">Visitors</span><span role="columnheader">Requests, 24 h</span><span role="columnheader">95% answered in</span></div>${sites}</div></section>` : ''}${L ? `<section aria-labelledby="rp-live-h"><h2 id="rp-live-h">OpenVibe.Live</h2><div class="rp-minis">${[['Streamers', L.streamers], ['Streams', L.streams], ['Hours watched', L.hours_watched], ['Chat messages', L.messages], ['Recordings', L.vods], ['Clips', L.clips], ['AI moments', L.ai_moments]].filter(([, v]) => n(v)).map(([l, v]) => `<div><b>${fmt(v)}</b><span>${esc(l)}</span></div>`).join('')}</div></section>` : ''}${A && A.daily && A.daily.length > 1 ? `<section aria-labelledby="rp-acc-h"><h2 id="rp-acc-h">New accounts a day</h2>${bars(A.daily, 'New accounts a day', { h: 110 })}</section>` : ''}--<section aria-labelledby="rp-how-h"><h2 id="rp-how-h">How these are counted</h2><p class="rp-fine">${esc(d.note || '')} The same numbers: <a href="/api/v1/stats?days=${esc(days)}"><code>/api/v1/stats</code></a>.</p></section>`;++<section aria-labelledby="rp-how-h"><h2 id="rp-how-h">How these are counted</h2><p class="rp-fine">${esc(d.note || '')} The same numbers: <a href="/api/v1/stats?days=${esc(days)}"><code>/api/v1/stats</code></a>. How closely each site follows the design kit, read from its code: <a href="/design">Design health</a>.</p></section>`;return layout({ title: `Every site's numbers, ${days} days`, description: 'Page views, people, streams and how fast each OpenVibe service answers, open to everyone.', body, active: '/stats', path: `/stats${Number(days) === 30 ? '' : `?days=${Number(days)}`}`, agent: true });}nnew file mode 100644@@@ -0,0 +1,91 @@++'use strict';++/**++ * openvibe.report/design (plan L45 step 6): every site's design health from the snapshot, most to fix first, with a++ * counted fold past fifteen; a site's page with its counts and each finding linked to its line at the measured commit;++ * the same as JSON; an unknown site is a 404; a repo with nothing to read is left out; every value from the snapshot++ * is escaped. The real snapshot (server/design-health/snapshot.json) must parse and hold sites.++ */++const assert = require('assert');++const fs = require('fs');++const os = require('os');++const path = require('path');++++const FIXTURE = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'dh-')), 'design-health.json');++const counts = (o) => ({ literal_duration: 0, literal_curve: 0, spinner: 0, color_drift: 0, missing_reduced_motion: 0, total: 0, ...o });++const sites = [++ { repo: 'OpenVibe.Live', commit: 'abcdef1234567890', files_scanned: 119, score: 12.5, findings_total: 40, counts: counts({ spinner: 1, literal_duration: 30, total: 31 }),++ findings: [{ rule: 'spinner', file: 'public/css/style.css', line: 489, value: 'spin', text: '@keyframes spin{to{transform:rotate(360deg)}} <script>alert(1)</script>' }] },++ { repo: 'OpenVibe.Zone', commit: '1111111222222', files_scanned: 1, score: 100, findings_total: 0, counts: counts({}), findings: [] },++ { repo: 'OpenVibe.SDK', commit: '333', files_scanned: 0, score: 100, findings_total: 0, counts: counts({}), findings: [] },++];++for (let i = 0; i < 16; i++) sites.push({ repo: `OpenVibe.Site${i}`, commit: 'c0ffee', files_scanned: 3, score: 90 + i / 2, findings_total: 2, counts: counts({ literal_curve: 2, total: 2 }), findings: [] });++fs.writeFileSync(FIXTURE, JSON.stringify({ generated_at: '2026-11-01T11:00:00.000Z', rules: ['literal_duration', 'literal_curve', 'spinner', 'color_drift', 'missing_reduced_motion'], weights: { spinner: 4, missing_reduced_motion: 2, literal_duration: 0.25, literal_curve: 0.25, color_drift: 0.05 }, kept: 25, sites }));++process.env.DESIGN_HEALTH_FILE = FIXTURE;++++const { boot, check, done } = require('./helpers/boot');++++(async () => {++ const t = await boot();++ try {++ await check('/design: every site with something to read, most to fix first, fifteen in view and the rest in a counted fold', async () => {++ const r = await t.get('/design');++ assert.strictEqual(r.status, 200);++ assert.strictEqual(r.headers.get('cache-control'), 'public, max-age=120');++ assert.ok(r.text.includes('<h1>Design health of every site</h1>'));++ assert.ok(r.text.includes('<link rel="canonical" href="https://openvibe.report/design">'));++ assert.ok(!r.text.includes('OpenVibe.SDK'), 'a repo with no stylesheet is not scored');++ const first = r.text.indexOf('href="/design/live"'), zone = r.text.indexOf('href="/design/zone"');++ assert.ok(first > 0 && zone > first, 'the lowest score comes first');++ assert.match(r.text, /<summary>All 18 sites <span>3 more<\/span><\/summary>/);++ assert.ok(r.text.includes('<b class="rp-tile-v">1</b><span class="rp-tile-s">still to replace</span>'), 'the spinners are counted');++ assert.ok(r.text.includes('class="dh-score is-low" style="--dh:12.5%"'), 'the score is a bar in its band');++ assert.ok(r.text.includes('server/design-health/scan.js'), 'the rules are linked');++ });++++ await check('/design/:site: the counts, and each finding opens its line at the measured commit, escaped', async () => {++ const r = await t.get('/design/live');++ assert.strictEqual(r.status, 200);++ assert.ok(r.text.includes('<h1>OpenVibe.Live</h1>'));++ assert.ok(r.text.includes('href="https://github.com/OpenVibers/OpenVibe.Live/blob/abcdef1234567890/public/css/style.css#L489"'));++ assert.ok(r.text.includes('1 of 40: the ones that cost most'));++ assert.ok(!r.text.includes('<script>alert(1)</script>'), 'a finding\'s text is escaped');++ assert.ok(r.text.includes('<script>alert(1)</script>'));++ const clean = await t.get('/design/zone');++ assert.ok(clean.text.includes('Nothing to fix'));++ });++++ await check('an unknown or malformed site is a 404 page and a 404 problem', async () => {++ assert.strictEqual((await t.get('/design/nope')).status, 404);++ assert.strictEqual((await t.get('/design/sdk')).status, 404, 'left out of the snapshot\'s sites');++ assert.strictEqual((await t.get('/design/%3Cx%3E')).status, 404);++ const api = await t.get('/api/v1/design-health/nope');++ assert.strictEqual(api.status, 404);++ assert.match(api.headers.get('content-type') || '', /problem\+json/);++ });++++ await check('the same as JSON, CORS-open', async () => {++ const r = await t.get('/api/v1/design-health');++ assert.strictEqual(r.status, 200);++ assert.strictEqual(r.headers.get('access-control-allow-origin'), '*');++ const j = r.json();++ assert.strictEqual(j.sites.length, 18);++ assert.strictEqual(j.weights.spinner, 4);++ const one = (await t.get('/api/v1/design-health/live')).json();++ assert.strictEqual(one.site.repo, 'OpenVibe.Live');++ assert.strictEqual(one.site.findings[0].line, 489);++ });++++ await check('the snapshot parses and holds sites with counts', async () => {++ const real = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'server', 'design-health', 'snapshot.json'), 'utf8'));++ assert.ok(Array.isArray(real.sites) && real.sites.length >= 20, `${(real.sites || []).length} sites`);++ for (const s of real.sites) {++ assert.ok(s.repo && s.counts && Number.isFinite(Number(s.score)), s.repo);++ assert.ok(Number(s.files_scanned) > 0, `${s.repo}: nothing to read, so not in the snapshot`);++ }++ });++ } finally {++ await t.close();++ fs.rmSync(path.dirname(FIXTURE), { recursive: true, force: true });++ }++ done();++})().catch((err) => { console.error(err); process.exit(1); });nnew file mode 100644@@@ -0,0 +1,255 @@++'use strict';++// design-lint/test.js — the lint's own tests: plain node and assert, over small fixtures.++//++// node test.js++//++// Each rule has to find what it should, and a clean fixture has to stay clean.++++const assert = require('assert');++const fs = require('fs');++const os = require('os');++const path = require('path');++++const { scanRepo, scoreOf } = require('../server/design-health/scan.js');++++// ── the fixtures ─────────────────────────────────────────────────────────────────────────────────────────────++// a repo where every style already goes through the kit++const CLEAN_CSS = [++ '/* a site that already uses the kit */',++ '.card {',++ ' background: var(--bg-primary);',++ ' color: var(--text-primary);',++ ' border: 1px solid var(--border);',++ ' transition: transform var(--ov-dur-2) var(--ov-ease-out);',++ '}',++ '.card:hover { transform: translateY(-2px); }',++ '.fade { animation: fade var(--ov-dur-3) var(--ov-ease-in-out) both; }',++ '@keyframes fade { from { opacity: 0; } to { opacity: 1; } }',++ '@media (prefers-reduced-motion: reduce) {',++ ' .card, .fade { transition: none; animation: none; }',++ '}',++ ''++].join('\n');++++// a repo that writes its own timing, its own curves, its own spinner and its own colours++const DIRTY_CSS = [++ '/* a site that hand-writes what the kit owns */',++ '.card {',++ ' background: var(--bg-primary);',++ ' transition: transform .25s ease;',++ '}',++ '.panel { animation: fade 300ms cubic-bezier(.4, 0, .2, 1) both; }',++ '.slow { transition-duration: 1.4s; }',++ '.badge { color: #ff006e; background: rgb(0, 255, 65); }',++ '@keyframes spin { to { transform: rotate(360deg); } }',++ '.spinner { animation: spin 1s linear infinite; }',++ ''++].join('\n');++++// an inline <style> in HTML++const DIRTY_HTML = [++ '<!doctype html>',++ '<html><head>',++ '<style>',++ ' .hero { color: #ff006e; background: var(--bg-primary); transition: opacity .2s ease-in-out; }',++ ' .hero2 { animation: pulse 800ms linear infinite; }',++ ' @keyframes pulse { to { transform: rotate(360deg); } }',++ '</style>',++ '</head><body><div class="hero">hi</div></body></html>',++ ''++].join('\n');++++const CLEAN_HTML = [++ '<!doctype html>',++ '<html><head>',++ '<style>',++ ' .hero { color: var(--text-primary); transition: opacity var(--ov-dur-1) var(--ov-ease-out); }',++ '</style>',++ '</head><body><div class="hero">hi</div></body></html>',++ ''++].join('\n');++++// ── the harness ──────────────────────────────────────────────────────────────────────────────────────────────++const TMP = fs.mkdtempSync(path.join(os.tmpdir(), 'design-lint-test-'));++++function makeRepo(files) {++ const dir = fs.mkdtempSync(path.join(TMP, 'repo-'));++ for (const rel of Object.keys(files)) {++ const full = path.join(dir, rel);++ fs.mkdirSync(path.dirname(full), { recursive: true });++ fs.writeFileSync(full, files[rel]);++ }++ return dir;++}++++const results = [];++function test(name, fn) {++ try {++ fn();++ results.push({ name, ok: true });++ } catch (err) {++ results.push({ name, ok: false, err });++ }++}++++const rulesOf = (res, rule) => res.findings.filter((f) => f.rule === rule);++const countOf = (res, rule) => res.counts[rule] || 0;++++// ── the tests ─────────────────────────────────────────────────────────────────────────────────────────────────++const clean = scanRepo(makeRepo({ 'public/css/site.css': CLEAN_CSS, 'public/index.html': CLEAN_HTML }));++++test('a clean file has no findings at all', () => {++ assert.deepStrictEqual(clean.counts, { literal_duration: 0, literal_curve: 0, spinner: 0, color_drift: 0, missing_reduced_motion: 0, total: 0 });++ assert.deepStrictEqual(clean.findings, []);++});++++test('a clean file scores 100', () => {++ assert.strictEqual(clean.score, 100);++});++++test('the kit tokens are never read as drift', () => {++ assert.strictEqual(countOf(clean, 'literal_duration'), 0);++ assert.strictEqual(countOf(clean, 'literal_curve'), 0);++});++++const dirty = scanRepo(makeRepo({ 'public/css/site.css': DIRTY_CSS }));++++test('a literal duration is found, with its file and line', () => {++ const found = rulesOf(dirty, 'literal_duration');++ assert.ok(found.length >= 3, 'expected at least three literal durations, got ' + found.length);++ assert.ok(found.some((f) => f.value === '.25s' && f.file === 'public/css/site.css'));++ assert.ok(found.some((f) => f.value === '300ms'));++ assert.ok(found.some((f) => f.value === '1.4s'));++ assert.ok(found.every((f) => f.line > 0));++ assert.ok(found.every((f) => f.file === 'public/css/site.css'));++});++++test('a literal curve is found, whether a cubic-bezier or an ease keyword', () => {++ const found = rulesOf(dirty, 'literal_curve');++ assert.ok(found.some((f) => /cubic-bezier/i.test(f.value)), 'expected a cubic-bezier curve');++ assert.ok(found.some((f) => /^ease/i.test(f.value)), 'expected an ease keyword');++});++++test('a spinner is found: the keyframes that turn, and the animation that runs them forever', () => {++ const found = rulesOf(dirty, 'spinner');++ assert.ok(found.some((f) => f.kind === 'spinner_keyframes' && f.value === 'spin'), 'expected the spin keyframes');++ assert.ok(found.some((f) => f.kind === 'spinner_usage' && f.value === 'spin'), 'expected the infinite animation');++});++++test('a hard-coded colour is found in a file that already uses the theme', () => {++ const found = rulesOf(dirty, 'color_drift');++ assert.ok(found.some((f) => f.value === '#ff006e'), 'expected the hex colour');++ assert.ok(found.some((f) => /rgb/i.test(f.value)), 'expected the rgb colour');++});++++test('a file that animates without prefers-reduced-motion is found', () => {++ const found = rulesOf(dirty, 'missing_reduced_motion');++ assert.strictEqual(found.length, 1);++ assert.strictEqual(found[0].file, 'public/css/site.css');++});++++test('an inline <style> in HTML is linted like a CSS file', () => {++ const html = scanRepo(makeRepo({ 'public/index.html': DIRTY_HTML }));++ assert.ok(countOf(html, 'literal_duration') >= 1, 'expected a literal duration in the inline style');++ assert.ok(countOf(html, 'literal_curve') >= 1, 'expected a literal curve in the inline style');++ assert.ok(countOf(html, 'spinner') >= 1, 'expected a spinner in the inline style');++ assert.ok(countOf(html, 'color_drift') >= 1, 'expected colour drift in the inline style');++ assert.strictEqual(countOf(html, 'missing_reduced_motion'), 1);++});++++test('an inline <style> that uses the kit is clean', () => {++ const html = scanRepo(makeRepo({ 'public/index.html': CLEAN_HTML }));++ assert.strictEqual(html.counts.total, 0, JSON.stringify(html.findings));++});++++test('the spinner is not reported without infinite', () => {++ const repo = makeRepo({ 'public/css/site.css': '@keyframes spin { to { transform: rotate(360deg); } }\n.a { animation: spin 1s linear; }\n' });++ const res = scanRepo(repo);++ assert.strictEqual(countOf(res, 'spinner'), 1, 'only the keyframes, not the finite animation');++ assert.strictEqual(rulesOf(res, 'spinner')[0].kind, 'spinner_keyframes');++});++++test('a keyframes that does not turn is not a spinner', () => {++ const repo = makeRepo({ 'public/css/site.css': '@keyframes fade { from { opacity: 0; } to { opacity: 1; } }\n.a { animation: fade 1s linear infinite; }\n' });++ const res = scanRepo(repo);++ assert.strictEqual(countOf(res, 'spinner'), 0);++});++++test('a var() fallback is not read as a hard-coded colour', () => {++ const repo = makeRepo({ 'public/css/site.css': '.a { color: var(--accent, #ff006e); background: var(--bg-primary); }\n' });++ const res = scanRepo(repo);++ assert.strictEqual(countOf(res, 'color_drift'), 0, JSON.stringify(rulesOf(res, 'color_drift')));++});++++test('a file with no theme variables is not judged for colour drift', () => {++ const repo = makeRepo({ 'public/css/site.css': '.a { color: #ff006e; background: #000; }\n' });++ const res = scanRepo(repo);++ assert.strictEqual(countOf(res, 'color_drift'), 0);++});++++test('the score is 100 minus the weighted counts, clamped at 0', () => {++ assert.strictEqual(scoreOf({ spinner: 0, missing_reduced_motion: 0, literal_duration: 0, literal_curve: 0, color_drift: 0 }), 100);++ const one = scoreOf({ spinner: 1, missing_reduced_motion: 0, literal_duration: 0, literal_curve: 0, color_drift: 0 });++ assert.ok(one < 100 && one > 90, 'a spinner costs the most, got ' + one);++ const many = scoreOf({ spinner: 100, missing_reduced_motion: 0, literal_duration: 0, literal_curve: 0, color_drift: 0 });++ assert.strictEqual(many, 0, 'the score never goes below 0');++ const over = scoreOf({ spinner: 0, missing_reduced_motion: 0, literal_duration: 0, literal_curve: 0, color_drift: 100000 });++ assert.strictEqual(over, 0, 'a pile of colour drift cannot push the score below 0 either');++});++++test('the weights are in order: spinner, then reduced motion, then duration and curve, then colour', () => {++ const cost = (rule) => 100 - scoreOf({ spinner: 0, missing_reduced_motion: 0, literal_duration: 0, literal_curve: 0, color_drift: 0, [rule]: 1 });++ assert.ok(cost('spinner') > cost('missing_reduced_motion'), 'a spinner weighs more than a missing reduced motion');++ assert.ok(cost('missing_reduced_motion') > cost('literal_duration'), 'a missing reduced motion weighs more than a literal duration');++ assert.strictEqual(cost('literal_duration'), cost('literal_curve'), 'a duration and a curve weigh the same');++ assert.ok(cost('literal_curve') > cost('color_drift'), 'a literal curve weighs more than a hard-coded colour');++});++++test('the score falls as the findings pile up', () => {++ const full = scoreOf({ spinner: 0, missing_reduced_motion: 0, literal_duration: 0, literal_curve: 0, color_drift: 0 });++ const some = scoreOf({ spinner: 0, missing_reduced_motion: 1, literal_duration: 2, literal_curve: 2, color_drift: 0 });++ assert.ok(some < full);++});++++test('node_modules, vendor and .min. files are skipped', () => {++ const repo = makeRepo({++ 'public/css/site.css': CLEAN_CSS,++ 'public/node_modules/pkg/x.css': DIRTY_CSS,++ 'public/vendor/x.css': DIRTY_CSS,++ 'public/css/site.min.css': DIRTY_CSS++ });++ const res = scanRepo(repo);++ assert.strictEqual(res.counts.total, 0, JSON.stringify(res.findings));++});++++test('the findings are sorted by file and line', () => {++ const repo = makeRepo({ 'public/css/b.css': DIRTY_CSS, 'public/css/a.css': DIRTY_CSS });++ const res = scanRepo(repo);++ for (let i = 1; i < res.findings.length; i++) {++ const prev = res.findings[i - 1];++ const cur = res.findings[i];++ assert.ok(prev.file < cur.file || (prev.file === cur.file && prev.line <= cur.line), 'not sorted at ' + i);++ }++});++++test('an empty repo is clean', () => {++ const res = scanRepo(makeRepo({ 'README.md': '# nothing here' }));++ assert.strictEqual(res.counts.total, 0);++ assert.strictEqual(res.score, 100);++});++++// ── the report ───────────────────────────────────────────────────────────────────────────────────────────────++let failed = 0;++for (const r of results) {++ if (r.ok) {++ console.log(' ok ' + r.name);++ } else {++ failed++;++ console.log(' FAIL ' + r.name);++ console.log(' ' + (r.err && r.err.message ? String(r.err.message).split('\n').join('\n ') : r.err));++ }++}++console.log('');++console.log((results.length - failed) + '/' + results.length + ' passed');++try { fs.rmSync(TMP, { recursive: true, force: true }); } catch (e) { /* the temp dir is the OS's problem now */ }++process.exit(failed ? 1 : 0);
@@ -272,3 +272,50 @@ html, body { margin: 0; background: var(--bg-primary, #0a0f1c); color: var(--tex
@@ -272,3 +272,50 @@ html, body { margin: 0; background: var(--bg-primary, #0a0f1c); color: var(--tex
.rp-you-next { border-top: 1px solid var(--border, #1f2d47); padding-top: 18px; }
.rp-you-next { border-top: 1px solid var(--border, #1f2d47); padding-top: 18px; }
.rp-you-next h2 { font-size: 1rem; }
.rp-you-next h2 { font-size: 1rem; }
@media (max-width: 560px) { .rp-person { align-items: flex-start; } }
@media (max-width: 560px) { .rp-person { align-items: flex-start; } }
+
+/* Design health (/design, /design/:site; server/views/design.js): a score on a bar, the table's columns, the counted
+ fold, the rules and the findings. Colours come from the theme (--success, --warning, --danger and their -text). */
+.dh-tr { grid-template-columns: minmax(0, 1.5fr) minmax(0, 1.4fr) repeat(5, minmax(0, .8fr)); }
+.dh-score { display: inline-flex; align-items: center; gap: 10px; width: 100%; min-width: 0; --dh-c: var(--success, var(--room-accent)); }
+.dh-score b { min-width: 2.4em; font-variant-numeric: tabular-nums; }
+.dh-score i { flex: 1 1 auto; min-width: 40px; height: 6px; border-radius: 999px; background: linear-gradient(90deg, var(--dh-c) var(--dh), color-mix(in srgb, var(--text-muted, currentColor) 22%, transparent) var(--dh)); }
+.dh-score.is-ok { --dh-c: var(--warning, var(--room-accent)); }
+.dh-score.is-low { --dh-c: var(--danger, var(--room-accent)); }
+.dh-n { font-variant-numeric: tabular-nums; }
+.dh-n.is-zero { color: var(--text-muted); }
+.dh-n.is-bad { color: var(--danger-text, var(--danger)); font-weight: 800; }
+.dh-fold { margin-top: 12px; }
+.dh-fold > summary { display: inline-flex; align-items: baseline; gap: 8px; padding: 8px 16px; border-radius: 999px; border: 1px solid var(--border); background: var(--bg-card); font-weight: 700; cursor: pointer; list-style: none; }
+.dh-fold > summary::-webkit-details-marker { display: none; }
+.dh-fold > summary span { color: var(--text-muted); font-size: .85rem; font-weight: 600; }
+.dh-fold > summary:hover { border-color: var(--room-accent); }
+.dh-fold[open] > summary { margin-bottom: 12px; }
+.dh-rules { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(260px, 100%), 1fr)); gap: 12px; margin: 0 0 14px; }
+.dh-rules > div { padding: 12px 14px; border-radius: 14px; background: var(--bg-card); border: 1px solid var(--border); }
+.dh-rules dt { display: flex; justify-content: space-between; gap: 8px; font-weight: 800; }
+.dh-rules dt span { color: var(--text-muted); font-size: .78rem; font-weight: 700; font-variant-numeric: tabular-nums; white-space: nowrap; }
+.dh-rules dd { margin: 6px 0 0; color: var(--text-secondary); font-size: .92rem; line-height: 1.5; }
+.dh-lede { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 16px; }
+.dh-lede .dh-score { width: min(280px, 100%); font-size: 1.4rem; }
+.dh-finds { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }
+.dh-find { display: grid; grid-template-columns: 10.5em minmax(0, 1fr); align-items: baseline; gap: 4px 10px; padding: 10px 14px; border-radius: 12px; background: var(--bg-card); border: 1px solid var(--border); }
+.dh-rule { justify-self: start; padding: 2px 8px; border-radius: 999px; border: 1px solid var(--border); font-size: .72rem; font-weight: 800; letter-spacing: .03em; white-space: nowrap; }
+.dh-rule--spinner, .dh-rule--missing-reduced-motion { color: var(--danger-text, var(--danger)); border-color: color-mix(in srgb, var(--danger, currentColor) 45%, transparent); }
+.dh-where { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: .85rem; overflow-wrap: anywhere; }
+.dh-text { grid-column: 2; display: block; overflow-x: auto; padding-top: 2px; color: var(--text-secondary); font-size: .8rem; white-space: pre; }
+.dh-none { display: none; }
+@media (max-width: 699px) {
+ /* A site in two lines: its name and score bar, then only what it has to fix, in words. */
+ .dh-tr { display: flex; flex-wrap: wrap; align-items: center; gap: 2px 12px; }
+ .dh-tr::after { content: ''; order: 2; flex-basis: 100%; }
+ .dh-tr.rp-th { display: none; }
+ .dh-tr > .rp-site { order: 0; flex: 1 1 0; min-width: 0; }
+ .dh-tr > [data-l="Score"] { order: 1; flex: 0 0 auto; width: 132px; }
+ .dh-tr > [data-l]::before { content: none; }
+ .dh-tr > .dh-n, .dh-tr > .dh-none { order: 3; font-size: .82rem; color: var(--text-secondary); }
+ .dh-tr > .dh-n::after { content: ' ' attr(data-s); color: var(--text-muted); }
+ .dh-tr > .dh-n.is-zero { display: none; }
+ .dh-tr > .dh-none { display: inline; color: var(--text-muted); }
+ .dh-find { grid-template-columns: minmax(0, 1fr); }
+ .dh-text { grid-column: 1; }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,105 @@
@@ -0,0 +1,105 @@
+#!/usr/bin/env node
+'use strict';
+// Measures every OpenVibe site's design health and writes server/design-health/snapshot.json, the snapshot openvibe.report/design
+// shows (plan L45 step 6). The rules are server/design-health/scan.js: where a site hand-writes what the shared kit
+// owns (motion durations and curves, theme colours), animates without asking for reduced motion, or spins.
+//
+// It reads checkouts, so it runs where the repos are (the orchestrator's workstation), never on the server:
+//
+// node scripts/design-health.js every ~/openvibe/work/OpenVibe.* checkout, at its origin/main
+// node scripts/design-health.js --work <dir> another folder of checkouts
+// node scripts/design-health.js --repo Live --no-fetch just Live again; every other site keeps its last measure
+//
+// Each repo is read through a detached worktree of origin/main in a temp folder, removed again afterwards: no
+// checkout is touched and no branch moves. The snapshot keeps every count and the first findings of each site, so it
+// stays small; commit it with a PR to refresh the page.
+
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+const { execFileSync } = require('child_process');
+const { scanRepo, WEIGHTS, RULES } = require('../server/design-health/scan');
+
+const argv = process.argv.slice(2);
+const opt = (name) => { const i = argv.indexOf(name); return i >= 0 ? argv[i + 1] : null; };
+const WORK = opt('--work') || process.env.OV_WORK_DIR || path.join(os.homedir(), 'openvibe', 'work');
+const ONLY = argv.reduce((a, v, i) => (argv[i - 1] === '--repo' ? a.concat(v.toLowerCase()) : a), []);
+const FETCH = !argv.includes('--no-fetch');
+const OUT = path.join(__dirname, '..', 'server', 'design-health', 'snapshot.json');
+const KEEP = 25; // findings kept per site; the counts are always the whole picture
+
+const git = (dir, args, soft) => {
+ try { return execFileSync('git', ['-C', dir, ...args], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim(); } catch (err) {
+ if (soft) return null;
+ throw new Error(`git ${args.join(' ')} in ${dir}: ${String(err.stderr || err.message).trim()}`);
+ }
+};
+// The repo's name on GitHub, so a clone and a linked worktree of it are one site.
+const nameOf = (dir) => { const url = git(dir, ['remote', 'get-url', 'origin'], true) || ''; const m = /([^/]+?)(\.git)?$/.exec(url); return m ? m[1] : path.basename(dir); };
+
+function repos() {
+ const seen = new Map();
+ for (const d of fs.readdirSync(WORK).sort()) {
+ if (!/^OpenVibe\./.test(d) && d !== 'OpenRestream') continue;
+ const dir = path.join(WORK, d);
+ if (!fs.existsSync(path.join(dir, '.git'))) continue;
+ const name = nameOf(dir);
+ if (!seen.has(name)) seen.set(name, dir);
+ }
+ return [...seen].filter(([name]) => !ONLY.length || ONLY.some((o) => name.toLowerCase().includes(o)));
+}
+
+function keep(findings) {
+ const weight = (f) => WEIGHTS[f.rule] || 0;
+ const ordered = findings.map((f, i) => ({ f, i })).sort((a, b) => weight(b.f) - weight(a.f) || a.i - b.i);
+ const perFile = new Map(), picked = [];
+ for (let round = 0; picked.length < KEEP && picked.length < ordered.length; round++) {
+ for (const x of ordered) {
+ if (picked.length >= KEEP) break;
+ if (x.taken || (perFile.get(x.f.file) || 0) !== round) continue;
+ x.taken = true; perFile.set(x.f.file, round + 1); picked.push(x);
+ }
+ }
+ return picked.sort((a, b) => weight(b.f) - weight(a.f) || a.i - b.i).map((x) => x.f);
+}
+
+function measure(name, dir, tmp) {
+ const wt = path.join(tmp, name.toLowerCase().replace(/[^a-z0-9]+/g, '-'));
+ let added = false;
+ try {
+ if (FETCH) git(dir, ['fetch', '--quiet', 'origin', '+refs/heads/main:refs/remotes/origin/main'], true);
+ const commit = git(dir, ['rev-parse', 'origin/main'], true);
+ if (!commit) return null;
+ git(dir, ['worktree', 'add', '--detach', wt, 'origin/main']);
+ added = true;
+ const r = scanRepo(wt);
+ return {
+ repo: name, commit, files_scanned: r.files_scanned, counts: r.counts, score: r.score, findings_total: r.findings.length,
+ // The findings kept matter most (by the rule's weight), spread over as many files as possible.
+ findings: keep(r.findings).map((f) => ({ rule: f.rule, file: f.file, line: f.line, value: f.value == null ? null : String(f.value).slice(0, 60), text: String(f.text || '').trim().slice(0, 160) })),
+ };
+ } finally {
+ if (added) { git(dir, ['worktree', 'remove', '--force', wt], true); git(dir, ['worktree', 'prune'], true); }
+ fs.rmSync(wt, { recursive: true, force: true });
+ }
+}
+
+const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'design-health-'));
+const sites = [];
+try {
+ for (const [name, dir] of repos()) {
+ const s = measure(name, dir, tmp);
+ if (s && s.files_scanned) sites.push(s); // a repo with no stylesheet where the rules read is not a site to score
+ process.stdout.write(`${name.padEnd(24)} ${s ? `${String(s.score).padStart(6)} ${s.findings_total} findings` : 'skipped (no origin/main)'}\n`);
+ }
+} finally { fs.rmSync(tmp, { recursive: true, force: true }); }
+// With --repo, only those sites are measured again: the others keep what the snapshot already has.
+if (ONLY.length) {
+ let before = [];
+ try { before = JSON.parse(fs.readFileSync(OUT, 'utf8')).sites || []; } catch { before = []; }
+ const fresh = new Set(sites.map((s) => s.repo));
+ sites.push(...before.filter((s) => !fresh.has(s.repo)));
+}
+sites.sort((a, b) => a.repo.localeCompare(b.repo));
+fs.writeFileSync(OUT, `${JSON.stringify({ generated_at: new Date().toISOString(), rules: RULES, weights: WEIGHTS, kept: KEEP, sites }, null, 1)}\n`);
+process.stdout.write(`\nwrote ${path.relative(process.cwd(), OUT)}: ${sites.length} sites\n`);
new file mode 100644
new file mode 100644
@@ -0,0 +1,36 @@
@@ -0,0 +1,36 @@
+'use strict';
+/**
+ * Every site's design health (plan L45 step 6): the snapshot scripts/design-health.js writes to server/design-health/snapshot.json
+ * from each site's code at its main branch. Read once and kept; a refresh is a new snapshot in a PR.
+ *
+ * all() → { generated_at, rules, weights, kept, sites: [...] } (sites with a stylesheet to read only)
+ * site(slug) → one site by its short name ('live', 'tools', 'openrestream'), or null
+ * slugOf(repo) → 'OpenVibe.Live' → 'live'
+ */
+const fs = require('fs');
+const path = require('path');
+
+// DESIGN_HEALTH_FILE: another snapshot (the tests' fixture).
+const FILE = process.env.DESIGN_HEALTH_FILE || path.join(__dirname, '..', 'design-health', 'snapshot.json');
+const slugOf = (repo) => String(repo || '').replace(/^OpenVibe\./, '').toLowerCase();
+let cached = null;
+
+function all() {
+ if (cached) return cached;
+ let snap = null;
+ try { snap = JSON.parse(fs.readFileSync(FILE, 'utf8')); } catch { snap = null; }
+ const sites = (snap && Array.isArray(snap.sites) ? snap.sites : [])
+ // A repo with no stylesheet where the rules read (a library, a service with no pages) is not a site to score.
+ .filter((s) => s && s.counts && Number(s.files_scanned) > 0)
+ .map((s) => ({ ...s, slug: slugOf(s.repo) }));
+ cached = { generated_at: snap ? snap.generated_at : null, rules: snap ? snap.rules : [], weights: snap ? snap.weights : {}, kept: snap ? snap.kept : 0, sites };
+ return cached;
+}
+
+function site(slug) {
+ const s = String(slug || '').toLowerCase();
+ if (!/^[a-z0-9-]{2,40}$/.test(s)) return null;
+ return all().sites.find((x) => x.slug === s) || null;
+}
+
+module.exports = { all, site, slugOf, _reset: () => { cached = null; } };
new file mode 100644
new file mode 100644
@@ -0,0 +1,492 @@
@@ -0,0 +1,492 @@
+'use strict';
+// The design-health rules (plan L45 step 6), served by openvibe.report/design. They began as the orchestrator's
+// tools/design-lint and live here now so the measure is as open as its results; scripts/design-health.js runs them.
+// Reads every CSS file and every inline <style> in HTML and JS under public/, server/views/ and
+// apps/*/public/, and reports where a site hand-writes what the shared kit already owns:
+//
+// literal_duration a transition/animation duration written as a literal (.25s, 300ms)
+// where the kit says var(--ov-dur-1..4)
+// literal_curve cubic-bezier(...) or an ease keyword where the kit says var(--ov-ease-*)
+// spinner @keyframes that rotate 360deg, and an animation that runs one forever
+// color_drift a hard-coded colour in a file that already uses the theme variables
+// missing_reduced_motion a file that animates and never asks prefers-reduced-motion
+//
+// node_modules, vendor and .min. files are skipped. The scanner never writes; it only reads.
+//
+// node scan.js <repo dir> JSON of the counts and findings on stdout
+// node scan.js <repo dir> --summary one line per rule
+//
+// The finding kinds are the same keys the report counts by, so a caller can group either way.
+
+const fs = require('fs');
+const path = require('path');
+
+// ── what the kit owns ─────────────────────────────────────────────────────────────────────────────────────────
+// the theme colour variables, from openvibe-shared/theme-loader.js THEME_KEYS
+const THEME_KEYS = ['--bg-primary', '--bg-secondary', '--bg-tertiary', '--bg-card', '--bg-hover', '--bg-input',
+ '--text-primary', '--text-secondary', '--text-muted', '--accent', '--accent-light', '--accent-dark',
+ '--border', '--border-light', '--live-red', '--success', '--warning', '--danger', '--info',
+ '--shadow', '--shadow-lg', '--success-text', '--warning-text', '--danger-text', '--info-text',
+ '--live-red-text', '--accent-strong', '--on-accent-strong', '--on-accent', '--accent-rgb',
+ '--accent-glow', '--color-scheme'];
+const THEME_SET = new Set(THEME_KEYS);
+
+// how much each rule costs a site its score: a spinner weighs most (nothing spins), then a file that
+// never asks prefers-reduced-motion, then a hand-written duration or curve, then a hand-written colour
+const WEIGHTS = {
+ spinner: 4,
+ missing_reduced_motion: 2,
+ literal_duration: 0.25,
+ literal_curve: 0.25,
+ color_drift: 0.05
+};
+
+const RULES = ['literal_duration', 'literal_curve', 'spinner', 'color_drift', 'missing_reduced_motion'];
+
+// the directories a site keeps its styles in
+const STYLE_ROOTS = ['public', 'server/views'];
+const APP_ROOT = 'apps';
+
+// the properties that carry a duration, and the ones that carry a curve
+const DURATION_PROPS = new Set(['transition', 'transition-duration', 'animation', 'animation-duration']);
+const CURVE_PROPS = new Set(['transition', 'transition-timing-function', 'animation', 'animation-timing-function']);
+
+const SKIP_DIRS = new Set(['node_modules', 'vendor', '.git']);
+const CSS_EXT = new Set(['.css']);
+const MARKUP_EXT = new Set(['.html', '.htm']);
+const SCRIPT_EXT = new Set(['.js', '.mjs', '.cjs']);
+
+// the keywords an animation shorthand can hold, so the name of the keyframes can be told from the rest
+const ANIM_KEYWORDS = new Set(['infinite', 'linear', 'ease', 'ease-in', 'ease-out', 'ease-in-out', 'normal',
+ 'reverse', 'alternate', 'alternate-reverse', 'forwards', 'backwards', 'both', 'none', 'running', 'paused',
+ 'step-start', 'step-end']);
+
+// the colours a site may name, when it should be asking the theme
+const NAMED_COLOURS = new Set(['white', 'black', 'red', 'green', 'blue', 'yellow', 'orange', 'purple', 'pink',
+ 'gray', 'grey', 'silver', 'gold', 'navy', 'teal', 'maroon', 'olive', 'lime', 'aqua', 'fuchsia', 'cyan',
+ 'magenta', 'crimson', 'indigo', 'violet', 'salmon', 'tan', 'khaki', 'beige', 'ivory', 'lavender',
+ 'turquoise', 'plum', 'orchid', 'coral', 'tomato', 'chocolate', 'sienna', 'darkred', 'darkblue', 'darkgreen']);
+
+const TIME_RE = /(?<![\w.-])(\d*\.?\d+)(ms|s)\b/gi;
+const TIME_ONLY_RE = /^(?:\d*\.?\d+)(?:ms|s)$/i;
+const CURVE_FN_RE = /cubic-bezier\s*\(/gi;
+const EASE_RE = /(?<![\w-])(ease-in-out|ease-in|ease-out|ease)(?![\w-])/gi;
+const HEX_RE = /#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})\b/g;
+const FUNC_COLOUR_RE = /\b(?:rgba?|hsla?)\([^)]*\)/gi;
+// hue-rotate(360deg) cycles a colour, it does not turn anything: not a spinner (Live's eggRainbow, 2026-11-01).
+const SPIN_RE = /(?<!hue-)rotate\(\s*(?:360(?:\.0+)?deg|1turn|2turn|-360(?:\.0+)?deg)\s*\)/i;
+
+// ── helpers ────────────────────────────────────────────────────────────────────────────────────────────────
+// the fallback inside var(--name, fallback) is a stand-in, not a choice: look past it
+const VAR_WITH_FALLBACK_RE = /var\(\s*(--[\w-]+)\s*,(?:[^()]*|\([^()]*\))*\)/gi;
+function stripVarFallbacks(value) {
+ return value.replace(VAR_WITH_FALLBACK_RE, 'var($1)');
+}
+
+// the theme variables a value asks for
+function themeVarsIn(value) {
+ const found = new Set();
+ const re = /var\(\s*(--[\w-]+)/g;
+ let m;
+ while ((m = re.exec(value))) found.add(m[1]);
+ return found;
+}
+
+function isThemeVar(name) {
+ return THEME_SET.has(name);
+}
+
+// the hard-coded colours in a value
+function coloursIn(value) {
+ const out = [];
+ const text = stripVarFallbacks(value).replace(/url\([^)]*\)/gi, 'url()');
+ let m;
+ HEX_RE.lastIndex = 0;
+ while ((m = HEX_RE.exec(text))) out.push(m[0]);
+ FUNC_COLOUR_RE.lastIndex = 0;
+ while ((m = FUNC_COLOUR_RE.exec(text))) out.push(m[0]);
+ const re = /(?<![\w-#])([a-zA-Z]{3,20})(?![\w-])/g;
+ while ((m = re.exec(text))) {
+ const name = m[1].toLowerCase();
+ if (NAMED_COLOURS.has(name)) out.push(name);
+ }
+ return out;
+}
+
+// the duration literals in a value
+function durationsIn(value) {
+ const out = [];
+ const text = stripVarFallbacks(value);
+ TIME_RE.lastIndex = 0;
+ let m;
+ while ((m = TIME_RE.exec(text))) out.push(m[0]);
+ return out;
+}
+
+// the curves written out in a value
+function curvesIn(value) {
+ const out = [];
+ const text = stripVarFallbacks(value);
+ CURVE_FN_RE.lastIndex = 0;
+ let m;
+ while ((m = CURVE_FN_RE.exec(text))) out.push(m[0]);
+ EASE_RE.lastIndex = 0;
+ while ((m = EASE_RE.exec(text))) out.push(m[1]);
+ return out;
+}
+
+// the keyframes an animation shorthand names
+function animationName(value) {
+ const text = value.replace(/var\([^)]*\)/gi, ' ');
+ const tokens = text.split(/[\s,]+/).filter(Boolean);
+ for (const tok of tokens) {
+ const t = tok.toLowerCase();
+ if (ANIM_KEYWORDS.has(t)) continue;
+ if (TIME_ONLY_RE.test(t)) continue;
+ if (/^cubic-bezier\(/i.test(t) || /^steps\(/i.test(t)) continue;
+ return tok;
+ }
+ return null;
+}
+
+// ── the CSS reader ─────────────────────────────────────────────────────────────────────────────────────────
+// a small reader: enough to know which declaration sits on which line, which rule it belongs to
+// and which keyframes it is inside
+function parseCss(src, baseLine) {
+ const decls = [];
+ const keyframes = [];
+ let i = 0;
+ let line = baseLine;
+ let uid = 0;
+ const len = src.length;
+
+ function skipComment() {
+ const end = src.indexOf('*/', i + 2);
+ const stop = end < 0 ? len : end + 2;
+ for (let k = i; k < stop; k++) if (src[k] === '\n') line++;
+ i = stop;
+ }
+
+ function consumeString() {
+ const q = src[i];
+ let k = i + 1;
+ let out = q;
+ while (k < len) {
+ const ch = src[k];
+ if (ch === '\\') { out += src[k] + (src[k + 1] || ''); k += 2; continue; }
+ out += ch;
+ k++;
+ if (ch === q) break;
+ }
+ for (let x = i; x < k; x++) if (src[x] === '\n') line++;
+ i = k;
+ return out;
+ }
+
+ function flush(buf, bufLine, chain) {
+ const text = buf.trim();
+ if (!text) return;
+ const colon = text.indexOf(':');
+ if (colon < 0) return;
+ const prop = text.slice(0, colon).trim();
+ const value = text.slice(colon + 1).trim();
+ if (!prop || !value) return;
+ if (/[{}]/.test(prop)) return;
+ const owner = chain[chain.length - 1];
+ let kf = null;
+ for (const b of chain) if (b.kf) kf = b.kf;
+ const decl = { prop, value, line: bufLine, rule: owner, keyframes: kf };
+ decls.push(decl);
+ if (kf) kf.decls.push(decl);
+ }
+
+ function parseBlock(chain) {
+ let buf = '';
+ let bufLine = line;
+ for (;;) {
+ if (i >= len) break;
+ const c = src[i];
+ if (c === '/' && src[i + 1] === '*') { skipComment(); buf = ''; bufLine = line; continue; }
+ if (c === '"' || c === "'") { if (!buf.trim()) bufLine = line; buf += consumeString(); continue; }
+ if (c === '{') {
+ const prelude = buf.trim();
+ buf = '';
+ bufLine = line;
+ const child = { id: ++uid, type: 'rule', selector: prelude, at: null, keyframes: null, parent: chain[chain.length - 1] };
+ const am = /^@([\w-]+)/.exec(prelude);
+ if (am) {
+ child.type = 'at';
+ child.at = am[1].toLowerCase();
+ const km = /^@(?:-webkit-)?keyframes\s+("[^"]+"|'[^']+'|[^\s{]+)/i.exec(prelude);
+ if (child.at === 'keyframes' || child.at === '-webkit-keyframes') {
+ child.keyframes = km ? km[1].replace(/^["']|["']$/g, '') : null;
+ keyframes.push({ name: child.keyframes, line: bufLine, decls: [] });
+ child.kf = keyframes[keyframes.length - 1];
+ }
+ }
+ i++;
+ chain.push(child);
+ parseBlock(chain);
+ chain.pop();
+ buf = '';
+ bufLine = line;
+ continue;
+ }
+ if (c === '}') { i++; flush(buf, bufLine, chain); return; }
+ if (c === ';') { i++; flush(buf, bufLine, chain); buf = ''; bufLine = line; continue; }
+ if (c === '\n') { line++; buf += c; i++; continue; }
+ // the declaration starts at its first real character, not at the whitespace before it
+ if (!buf.trim()) bufLine = line;
+ buf += c;
+ i++;
+ }
+ flush(buf, bufLine, chain);
+ }
+
+ const root = { id: 0, type: 'root', selector: '', at: null, keyframes: null, parent: null };
+ parseBlock([root]);
+ return { decls, keyframes };
+}
+
+// ── the units a file is made of ─────────────────────────────────────────────────────────────────────────────
+// a CSS file is one unit; an HTML or JS file is one unit per inline <style>
+function unitsIn(file, text) {
+ const units = [];
+ const ext = path.extname(file).toLowerCase();
+ if (CSS_EXT.has(ext)) {
+ units.push({ kind: 'css', line: 1, text });
+ return units;
+ }
+ if (!MARKUP_EXT.has(ext) && !SCRIPT_EXT.has(ext)) return units;
+ const re = /<style\b[^>]*>([\s\S]*?)<\/style>/gi;
+ let m;
+ while ((m = re.exec(text))) {
+ const before = text.slice(0, m.index);
+ const line = 1 + (before.match(/\n/g) || []).length;
+ units.push({ kind: 'inline-style', line, text: m[1] });
+ }
+ return units;
+}
+
+// ── the walk ─────────────────────────────────────────────────────────────────────────────────────────────────
+function styleRoots(repoDir) {
+ const roots = [];
+ for (const rel of STYLE_ROOTS) {
+ const p = path.join(repoDir, rel);
+ if (fs.existsSync(p) && fs.statSync(p).isDirectory()) roots.push(p);
+ }
+ const apps = path.join(repoDir, APP_ROOT);
+ if (fs.existsSync(apps) && fs.statSync(apps).isDirectory()) {
+ for (const name of fs.readdirSync(apps)) {
+ const p = path.join(apps, name, 'public');
+ if (fs.existsSync(p) && fs.statSync(p).isDirectory()) roots.push(p);
+ }
+ }
+ return roots;
+}
+
+function walk(dir, out) {
+ let entries;
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (e) { return out; }
+ for (const e of entries) {
+ const p = path.join(dir, e.name);
+ if (e.isDirectory()) {
+ if (SKIP_DIRS.has(e.name)) continue;
+ walk(p, out);
+ } else if (e.isFile()) {
+ if (e.name.includes('.min.')) continue;
+ out.push(p);
+ }
+ }
+ return out;
+}
+
+// ── the lint ───────────────────────────────────────────────────────────────────────────────────────────────
+// scan one repo directory and return its counts, its findings and its score
+function scanRepo(repoDir) {
+ const root = path.resolve(repoDir);
+ const findings = [];
+ const files = [];
+ const roots = styleRoots(root);
+
+ for (const r of roots) {
+ for (const file of walk(r, [])) {
+ const ext = path.extname(file).toLowerCase();
+ if (!CSS_EXT.has(ext) && !MARKUP_EXT.has(ext) && !SCRIPT_EXT.has(ext)) continue;
+ let text;
+ try { text = fs.readFileSync(file, 'utf8'); } catch (e) { continue; }
+ files.push(file);
+ const rel = path.relative(root, file);
+ const units = unitsIn(file, text);
+ if (!units.length) continue;
+ findings.push(...lintFile(root, rel, text, units, file));
+ }
+ }
+
+ // the keyframes of a repo, so an animation can be traced back to the one it runs
+ const counts = { literal_duration: 0, literal_curve: 0, spinner: 0, color_drift: 0, missing_reduced_motion: 0, total: 0 };
+ const by_kind = {};
+ for (const f of findings) {
+ counts[f.rule] = (counts[f.rule] || 0) + 1;
+ by_kind[f.kind] = (by_kind[f.kind] || 0) + 1;
+ }
+ counts.total = findings.length;
+
+ findings.sort((a, b) => (a.file < b.file ? -1 : a.file > b.file ? 1 : a.line - b.line));
+ return { repo: path.basename(root), counts, by_kind: by_kind, score: scoreOf(counts), findings, files_scanned: files.length };
+}
+
+// the lint of one file
+function lintFile(root, rel, text, units, file) {
+ const out = [];
+ const lines = text.split('\n');
+ const lineText = (n) => (lines[n - 1] || '').trim().slice(0, 200);
+
+ const parsed = units.map((u) => Object.assign({}, u, parseCss(u.text, u.line)));
+ const allDecls = [];
+ const allKeyframes = [];
+ for (const p of parsed) { allDecls.push(...p.decls); allKeyframes.push(...p.keyframes); }
+
+ // what this file asks the theme for
+ const fileThemeVars = new Set();
+ for (const d of allDecls) for (const name of themeVarsIn(d.value)) fileThemeVars.add(name);
+ const fileUsesTheme = [...fileThemeVars].some(isThemeVar);
+ const fileHasReducedMotion = /prefers-reduced-motion/i.test(text);
+
+ // which rule uses a theme variable: a rule that does is themed, a hard-coded colour in it is drift
+ const ruleThemeVars = new Map();
+ for (const d of allDecls) {
+ if (!d.rule) continue;
+ if (!ruleThemeVars.has(d.rule.id)) ruleThemeVars.set(d.rule.id, new Set());
+ const set = ruleThemeVars.get(d.rule.id);
+ for (const name of themeVarsIn(d.value)) set.add(name);
+ }
+
+ // the keyframes that turn, anywhere in this file
+ const spinNames = new Set();
+ for (const kf of allKeyframes) {
+ const turns = kf.decls.some((d) => SPIN_RE.test(d.value));
+ if (turns && kf.name) spinNames.add(kf.name);
+ }
+
+ // the custom properties this file keeps for itself
+ const ownPalette = new Set();
+
+ // the animation declarations, grouped by the rule they sit in
+ const rules = new Map();
+ for (const d of allDecls) {
+ const id = d.rule ? d.rule.id : 0;
+ if (!rules.has(id)) rules.set(id, { rule: d.rule, decls: [] });
+ rules.get(id).decls.push(d);
+ }
+
+ for (const d of allDecls) {
+ const prop = d.prop.toLowerCase();
+ const value = d.value;
+ const line = d.line;
+ const at = { file: rel, line, text: lineText(line) };
+
+ // durations written out (the kit's own token definitions are the kit, not drift)
+ if (DURATION_PROPS.has(prop) && !/^--ov-/.test(prop)) {
+ for (const dur of durationsIn(value)) {
+ out.push(Object.assign({ rule: 'literal_duration', kind: 'duration', value: dur, detail: prop + ' uses ' + dur + ' where the kit says var(--ov-dur-1..4)' }, at));
+ }
+ }
+
+ // curves written out (the kit's own token definitions are the kit, not drift)
+ if (CURVE_PROPS.has(prop) && !/^--ov-/.test(prop)) {
+ for (const curve of curvesIn(value)) {
+ out.push(Object.assign({ rule: 'literal_curve', kind: 'curve', value: curve, detail: prop + ' uses ' + curve + ' where the kit says var(--ov-ease-out / -spring / -in-out)' }, at));
+ }
+ }
+
+ // hard-coded colours, in a file that already uses the theme
+ if (fileUsesTheme) {
+ const themed = d.rule && ruleThemeVars.get(d.rule.id) && [...ruleThemeVars.get(d.rule.id)].some(isThemeVar);
+ const colours = coloursIn(value);
+ if (colours.length) {
+ // a custom property is a palette: one finding for the property, not one per value
+ if (prop.startsWith('--')) {
+ if (!ownPalette.has(prop)) {
+ ownPalette.add(prop);
+ out.push(Object.assign({
+ rule: 'color_drift', kind: 'own_palette',
+ value: prop + ' ' + colours[0],
+ detail: 'the file uses theme variables and still keeps its own ' + prop
+ }, at));
+ }
+ } else {
+ for (const colour of colours) {
+ out.push(Object.assign({
+ rule: 'color_drift', kind: themed ? 'colour_in_themed_rule' : 'colour_in_themed_file',
+ value: colour,
+ detail: (themed ? 'the rule uses theme variables and still writes ' : 'the file uses theme variables and still writes ') + colour
+ }, at));
+ }
+ }
+ }
+ }
+ }
+
+ // spinners: the keyframes that turn, and the animations that run them forever
+ for (const kf of allKeyframes) {
+ if (!kf.name || !spinNames.has(kf.name)) continue;
+ out.push({ rule: 'spinner', kind: 'spinner_keyframes', file: rel, line: kf.line, text: lineText(kf.line), value: kf.name, detail: '@keyframes ' + kf.name + ' turns 360deg' });
+ }
+ for (const group of rules.values()) {
+ const hasInfinite = group.decls.some((d) => /^(-webkit-)?animation(-iteration-count)?$/.test(d.prop.toLowerCase()) && /\binfinite\b/i.test(d.value));
+ for (const d of group.decls) {
+ const prop = d.prop.toLowerCase();
+ if (prop !== 'animation' && prop !== '-webkit-animation' && prop !== 'animation-name') continue;
+ const name = prop === 'animation-name' ? d.value.trim() : animationName(d.value);
+ if (!name || !spinNames.has(name)) continue;
+ const infinite = /\binfinite\b/i.test(d.value) || (prop === 'animation-name' && hasInfinite);
+ if (!infinite) continue;
+ out.push({ rule: 'spinner', kind: 'spinner_usage', file: rel, line: d.line, text: lineText(d.line), value: name, detail: 'animation runs ' + name + ' forever' });
+ }
+ }
+
+ // a file that animates and never asks prefers-reduced-motion
+ if (!fileHasReducedMotion) {
+ const first = allDecls.find((d) => /^(-webkit-)?animation(-name)?$/.test(d.prop.toLowerCase()) && !/^none$/i.test(d.value.trim()));
+ if (first) {
+ out.push({ rule: 'missing_reduced_motion', kind: 'missing_reduced_motion', file: rel, line: first.line, text: lineText(first.line), value: first.prop, detail: 'the file animates and never asks prefers-reduced-motion' });
+ }
+ }
+
+ return out;
+}
+
+// ── the score ───────────────────────────────────────────────────────────────────────────────────────────────
+// 100 minus what the findings cost, never below 0
+function scoreOf(counts) {
+ let score = 100;
+ for (const rule of RULES) score -= (counts[rule] || 0) * (WEIGHTS[rule] || 0);
+ return Math.max(0, Math.min(100, Math.round(score * 100) / 100));
+}
+
+// ── the command line ───────────────────────────────────────────────────────────────────────────────────────
+function main(argv) {
+ const args = argv.filter((a) => !a.startsWith('--'));
+ const summary = argv.includes('--summary');
+ if (!args.length) {
+ process.stderr.write('usage: node scan.js <repo dir> [--summary]\n');
+ process.exit(2);
+ }
+ const dir = args[0];
+ if (!fs.existsSync(dir)) {
+ process.stderr.write('scan: no such directory: ' + dir + '\n');
+ process.exit(2);
+ }
+ const result = scanRepo(dir);
+ if (summary) {
+ const parts = RULES.map((r) => r + '=' + result.counts[r]);
+ process.stdout.write(result.repo + ' score=' + result.score + ' files=' + result.files_scanned + ' ' + parts.join(' ') + '\n');
+ } else {
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
+ }
+}
+
+module.exports = { scanRepo, scoreOf, lintFile, parseCss, THEME_KEYS, WEIGHTS, RULES };
+
+if (require.main === module) main(process.argv.slice(2));
new file mode 100644
new file mode 100644
@@ -0,0 +1,4984 @@
@@ -0,0 +1,4984 @@
+{
+ "generated_at": "2026-11-01T11:14:41.112Z",
+ "rules": [
+ "literal_duration",
+ "literal_curve",
+ "spinner",
+ "color_drift",
+ "missing_reduced_motion"
+ ],
+ "weights": {
+ "spinner": 4,
+ "missing_reduced_motion": 2,
+ "literal_duration": 0.25,
+ "literal_curve": 0.25,
+ "color_drift": 0.05
+ },
+ "kept": 25,
+ "sites": [
+ {
+ "repo": "OpenVibe.Actor",
+ "commit": "ac296db14b66fafbff94a0e3a82e33fbda52f013",
+ "files_scanned": 12,
+ "counts": {
+ "literal_duration": 27,
+ "literal_curve": 12,
+ "spinner": 0,
+ "color_drift": 18,
+ "missing_reduced_motion": 0,
+ "total": 57
+ },
+ "score": 89.35,
+ "findings_total": 57,
+ "findings": [
+ {
+ "rule": "literal_duration",
+ "file": "public/css/actor.css",
+ "line": 68,
+ "value": ".15s",
+ "text": ".mode span { display: flex; flex-direction: column; gap: 2px; height: 100%; padding: 10px 12px; border-radius: 12px; border: 1px solid var(--border, #1f2d47); b"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/actor.css",
+ "line": 68,
+ "value": ".15s",
+ "text": ".mode span { display: flex; flex-direction: column; gap: 2px; height: 100%; padding: 10px 12px; border-radius: 12px; border: 1px solid var(--border, #1f2d47); b"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/actor.css",
+ "line": 111,
+ "value": ".25s",
+ "text": ".tl-item { display: grid; grid-template-columns: 52px 1fr; gap: 10px; font-size: .9rem; position: relative; animation: step-in .25s ease-out; }"
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/css/actor.css",
+ "line": 111,
+ "value": "ease-out",
+ "text": ".tl-item { display: grid; grid-template-columns: 52px 1fr; gap: 10px; font-size: .9rem; position: relative; animation: step-in .25s ease-out; }"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/actor.css",
+ "line": 135,
+ "value": "1.2s",
+ "text": ".dot-pulse { display: inline-block; width: 10px; height: 10px; border-radius: 50%; background: var(--accent, #3b82f6); animation: pulse 1.2s ease-in-out infinit"
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/css/actor.css",
+ "line": 135,
+ "value": "ease-in-out",
+ "text": ".dot-pulse { display: inline-block; width: 10px; height: 10px; border-radius: 50%; background: var(--accent, #3b82f6); animation: pulse 1.2s ease-in-out infinit"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/chat.css",
+ "line": 17,
+ "value": ".26s",
+ "text": "overflow-y: auto; overscroll-behavior: contain; transform: translateX(-104%); transition: transform .26s cubic-bezier(.2, .8, .2, 1); box-shadow: 0 0 40px rgba("
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/css/chat.css",
+ "line": 17,
+ "value": "cubic-bezier(",
+ "text": "overflow-y: auto; overscroll-behavior: contain; transform: translateX(-104%); transition: transform .26s cubic-bezier(.2, .8, .2, 1); box-shadow: 0 0 40px rgba("
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/chat.css",
+ "line": 37,
+ "value": "1.6s",
+ "text": ".cs-dot { flex: none; width: 8px; height: 8px; border-radius: 50%; background: var(--accent, #f97316); animation: ct-breathe 1.6s ease-in-out infinite; }"
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/css/chat.css",
+ "line": 37,
+ "value": "ease-in-out",
+ "text": ".cs-dot { flex: none; width: 8px; height: 8px; border-radius: 50%; background: var(--accent, #f97316); animation: ct-breathe 1.6s ease-in-out infinite; }"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/chat.css",
+ "line": 60,
+ "value": ".32s",
+ "text": ".ct-turn.is-new .ct-bubble { animation: ct-rise .32s cubic-bezier(.2, .8, .2, 1) both; }"
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/css/chat.css",
+ "line": 60,
+ "value": "cubic-bezier(",
+ "text": ".ct-turn.is-new .ct-bubble { animation: ct-rise .32s cubic-bezier(.2, .8, .2, 1) both; }"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/chat.css",
+ "line": 64,
+ "value": "1.6s",
+ "text": ".ct-turn[data-open] .ct-avatar { animation: ct-breathe 1.6s ease-in-out infinite; }"
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/css/chat.css",
+ "line": 64,
+ "value": "ease-in-out",
+ "text": ".ct-turn[data-open] .ct-avatar { animation: ct-breathe 1.6s ease-in-out infinite; }"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/chat.css",
+ "line": 73,
+ "value": ".2s",
+ "text": ".cw-chev { flex: none; width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; transition: "
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/chat.css",
+ "line": 76,
+ "value": "2.2s",
+ "text": "-webkit-background-clip: text; background-clip: text; color: transparent; animation: ct-shimmer 2.2s linear infinite; font-weight: 550; }"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/task.css",
+ "line": 8,
+ "value": ".25s",
+ "text": ".ps { position: relative; display: grid; grid-template-columns: 20px minmax(0, 1fr); gap: 12px; padding: 5px 0; color: var(--text-secondary, #96a7c2); line-heig"
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/css/task.css",
+ "line": 8,
+ "value": "ease-out",
+ "text": ".ps { position: relative; display: grid; grid-template-columns: 20px minmax(0, 1fr); gap: 12px; padding: 5px 0; color: var(--text-secondary, #96a7c2); line-heig"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/task.css",
+ "line": 16,
+ "value": "1.4s",
+ "text": ".ps.is-active .ps-mark { border-color: var(--accent, #3b82f6); animation: ps-pulse 1.4s ease-out infinite; } /* the step on now: a ring that pulses out, never"
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/css/task.css",
+ "line": 16,
+ "value": "ease-out",
+ "text": ".ps.is-active .ps-mark { border-color: var(--accent, #3b82f6); animation: ps-pulse 1.4s ease-out infinite; } /* the step on now: a ring that pulses out, never"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/task.css",
+ "line": 28,
+ "value": ".15s",
+ "text": "color: var(--accent-light, #60a5fa); background: color-mix(in srgb, var(--accent, #3b82f6) 15%, transparent); border: 1px solid color-mix(in srgb, var(--accent,"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/task.css",
+ "line": 51,
+ "value": ".3s",
+ "text": ".ev-item { padding: 10px 12px; border-radius: 12px; border: 1px solid var(--border, #1f2d47); background: var(--bg-primary, #0a0f1c); scroll-margin-top: 80px; t"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/task.css",
+ "line": 51,
+ "value": ".3s",
+ "text": ".ev-item { padding: 10px 12px; border-radius: 12px; border: 1px solid var(--border, #1f2d47); background: var(--bg-primary, #0a0f1c); scroll-margin-top: 80px; t"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/task.css",
+ "line": 91,
+ "value": ".15s",
+ "text": ".follow-form { display: flex; align-items: flex-end; gap: 8px; padding: 6px 6px 6px 14px; border-radius: 16px; border: 1px solid var(--border-light, #2c3d5c); b"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/task.css",
+ "line": 91,
+ "value": ".15s",
+ "text": ".follow-form { display: flex; align-items: flex-end; gap: 8px; padding: 6px 6px 6px 14px; border-radius: 16px; border: 1px solid var(--border-light, #2c3d5c); b"
+ }
+ ]
+ },
+ {
+ "repo": "OpenVibe.Bench",
+ "commit": "d18fd58d82f16de3284255acba49b8ea0999b01c",
+ "files_scanned": 8,
+ "counts": {
+ "literal_duration": 2,
+ "literal_curve": 2,
+ "spinner": 0,
+ "color_drift": 5,
+ "missing_reduced_motion": 0,
+ "total": 9
+ },
+ "score": 98.75,
+ "findings_total": 9,
+ "findings": [
+ {
+ "rule": "literal_duration",
+ "file": "public/css/bench.css",
+ "line": 189,
+ "value": ".18s",
+ "text": ".br-card-link { height: 100%; display: grid; align-content: start; gap: 8px; padding: 16px; border-radius: 18px; background: var(--bg-card, #0f1729); border: 1p"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/bench.css",
+ "line": 189,
+ "value": ".18s",
+ "text": ".br-card-link { height: 100%; display: grid; align-content: start; gap: 8px; padding: 16px; border-radius: 18px; background: var(--bg-card, #0f1729); border: 1p"
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/css/bench.css",
+ "line": 189,
+ "value": "ease",
+ "text": ".br-card-link { height: 100%; display: grid; align-content: start; gap: 8px; padding: 16px; border-radius: 18px; background: var(--bg-card, #0f1729); border: 1p"
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/css/bench.css",
+ "line": 189,
+ "value": "ease",
+ "text": ".br-card-link { height: 100%; display: grid; align-content: start; gap: 8px; padding: 16px; border-radius: 18px; background: var(--bg-card, #0f1729); border: 1p"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/bench.css",
+ "line": 617,
+ "value": "#f59e0b",
+ "text": ".br-contam { padding: 12px 14px; border-radius: 14px; background: color-mix(in srgb, #f59e0b 10%, var(--bg-card)); border: 1px solid color-mix(in srgb, #f59e0b "
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/bench.css",
+ "line": 617,
+ "value": "#f59e0b",
+ "text": ".br-contam { padding: 12px 14px; border-radius: 14px; background: color-mix(in srgb, #f59e0b 10%, var(--bg-card)); border: 1px solid color-mix(in srgb, #f59e0b "
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/bench.css",
+ "line": 619,
+ "value": "#fde68a",
+ "text": ".br-contam-chip { display: inline-block; margin-left: 8px; padding: 1px 7px; border-radius: 999px; font-size: .68rem; font-weight: 800; letter-spacing: .02em; c"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/bench.css",
+ "line": 619,
+ "value": "#f59e0b",
+ "text": ".br-contam-chip { display: inline-block; margin-left: 8px; padding: 1px 7px; border-radius: 999px; font-size: .68rem; font-weight: 800; letter-spacing: .02em; c"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/bench.css",
+ "line": 619,
+ "value": "#f59e0b",
+ "text": ".br-contam-chip { display: inline-block; margin-left: 8px; padding: 1px 7px; border-radius: 999px; font-size: .68rem; font-weight: 800; letter-spacing: .02em; c"
+ }
+ ]
+ },
+ {
+ "repo": "OpenVibe.Blog",
+ "commit": "f754247ce16df1a62cb517e4e420d3f2243fbaeb",
+ "files_scanned": 1,
+ "counts": {
+ "literal_duration": 1,
+ "literal_curve": 0,
+ "spinner": 0,
+ "color_drift": 2,
+ "missing_reduced_motion": 0,
+ "total": 3
+ },
+ "score": 99.65,
+ "findings_total": 3,
+ "findings": [
+ {
+ "rule": "literal_duration",
+ "file": "public/css/blog.css",
+ "line": 221,
+ "value": ".15s",
+ "text": ".toc-fold > summary::after, .series-more > summary::after { content: ''; margin-left: auto; width: 8px; height: 8px; border-right: 2px solid currentColor; borde"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/blog.css",
+ "line": 60,
+ "value": "rgba(34,197,94,.25)",
+ "text": ".ov-diff ins { background: rgba(34,197,94,.25); text-decoration: none; }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/blog.css",
+ "line": 61,
+ "value": "rgba(239,68,68,.25)",
+ "text": ".ov-diff del { background: rgba(239,68,68,.25); }"
+ }
+ ]
+ },
+ {
+ "repo": "OpenVibe.Bot",
+ "commit": "ffe8f47f7901d934173a7620a21fbea9b7ab5ae4",
+ "files_scanned": 13,
+ "counts": {
+ "literal_duration": 12,
+ "literal_curve": 5,
+ "spinner": 0,
+ "color_drift": 100,
+ "missing_reduced_motion": 0,
+ "total": 117
+ },
+ "score": 90.75,
+ "findings_total": 117,
+ "findings": [
+ {
+ "rule": "literal_duration",
+ "file": "public/directory.css",
+ "line": 31,
+ "value": ".15s",
+ "text": ".dir-card { background: var(--bg-secondary, #101828); border: 1px solid var(--border, #1f2d47); border-radius: 14px; display: flex; flex-direction: column; tran"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/directory.css",
+ "line": 115,
+ "value": "1.4s",
+ "text": ".sd-status[data-state=\"waiting\"] .sd-dot, .sd-status[data-state=\"installing\"] .sd-dot { background: #f59e0b; animation: sd-pulse 1.4s ease-in-out infinite; }"
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/directory.css",
+ "line": 115,
+ "value": "ease-in-out",
+ "text": ".sd-status[data-state=\"waiting\"] .sd-dot, .sd-status[data-state=\"installing\"] .sd-dot { background: #f59e0b; animation: sd-pulse 1.4s ease-in-out infinite; }"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/home.css",
+ "line": 27,
+ "value": ".15s",
+ "text": ".hm-stats a { display: grid; gap: 2px; padding: 14px 16px; border-radius: 14px; border: 1px solid var(--border, rgba(255, 255, 255, .08)); background: var(--bg-"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/home.css",
+ "line": 34,
+ "value": ".15s",
+ "text": ".hm-card { display: flex; flex-direction: column; min-width: 0; border-radius: 16px; border: 1px solid var(--border, rgba(255, 255, 255, .08)); background: var("
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/panel.css",
+ "line": 29,
+ "value": "0.12s",
+ "text": "touch-action: manipulation; -webkit-tap-highlight-color: transparent; transition: background 0.12s, border-color 0.12s, transform 0.06s;"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/panel.css",
+ "line": 29,
+ "value": "0.12s",
+ "text": "touch-action: manipulation; -webkit-tap-highlight-color: transparent; transition: background 0.12s, border-color 0.12s, transform 0.06s;"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/panel.css",
+ "line": 29,
+ "value": "0.06s",
+ "text": "touch-action: manipulation; -webkit-tap-highlight-color: transparent; transition: background 0.12s, border-color 0.12s, transform 0.06s;"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/panel.css",
+ "line": 71,
+ "value": "1.2s",
+ "text": ".estop-banner[data-latched=\"true\"] .estop-dot { background: #fff; box-shadow: 0 0 0 4px rgba(255, 255, 255, 0.25); animation: pulse 1.2s ease-in-out infinite; }"
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/panel.css",
+ "line": 71,
+ "value": "ease-in-out",
+ "text": ".estop-banner[data-latched=\"true\"] .estop-dot { background: #fff; box-shadow: 0 0 0 4px rgba(255, 255, 255, 0.25); animation: pulse 1.2s ease-in-out infinite; }"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/panel.css",
+ "line": 83,
+ "value": "1s",
+ "text": ".online[data-state=\"connecting\"] .dot { animation: pulse 1s ease-in-out infinite; }"
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/panel.css",
+ "line": 83,
+ "value": "ease-in-out",
+ "text": ".online[data-state=\"connecting\"] .dot { animation: pulse 1s ease-in-out infinite; }"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/panel.css",
+ "line": 123,
+ "value": "0.12s",
+ "text": "box-shadow: 0 6px 14px rgba(0, 0, 0, 0.55); transition: transform 0.12s ease-out;"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/directory.css",
+ "line": 43,
+ "value": "#a78bfa",
+ "text": ".dir-browser { color: #a78bfa; } .dir-phone { color: #f472b6; } .dir-onboard { color: #22c55e; } .dir-bridge { color: #f59e0b; } .dir-server { color: #38bdf8; }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/directory.css",
+ "line": 43,
+ "value": "#f472b6",
+ "text": ".dir-browser { color: #a78bfa; } .dir-phone { color: #f472b6; } .dir-onboard { color: #22c55e; } .dir-bridge { color: #f59e0b; } .dir-server { color: #38bdf8; }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/directory.css",
+ "line": 43,
+ "value": "#22c55e",
+ "text": ".dir-browser { color: #a78bfa; } .dir-phone { color: #f472b6; } .dir-onboard { color: #22c55e; } .dir-bridge { color: #f59e0b; } .dir-server { color: #38bdf8; }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/directory.css",
+ "line": 43,
+ "value": "#f59e0b",
+ "text": ".dir-browser { color: #a78bfa; } .dir-phone { color: #f472b6; } .dir-onboard { color: #22c55e; } .dir-bridge { color: #f59e0b; } .dir-server { color: #38bdf8; }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/docs.css",
+ "line": 37,
+ "value": "rgba(239, 68, 68, .45)",
+ "text": ".docs-main .error { border: 1px solid rgba(239, 68, 68, .45); background: rgba(239, 68, 68, .08); border-radius: 10px; padding: 12px 14px; }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/docs.css",
+ "line": 37,
+ "value": "rgba(239, 68, 68, .08)",
+ "text": ".docs-main .error { border: 1px solid rgba(239, 68, 68, .45); background: rgba(239, 68, 68, .08); border-radius: 10px; padding: 12px 14px; }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/docs.css",
+ "line": 38,
+ "value": "#22c55e",
+ "text": ".docs-main [role=\"status\"] strong { color: #22c55e; }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/home.css",
+ "line": 13,
+ "value": "#0b1220",
+ "text": ".hm-term { border-radius: 16px; background: #0b1220; color: #e6edf7; border: 1px solid rgba(148, 163, 184, .25); box-shadow: var(--shadow-lg, 0 18px 50px rgba(0"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/home.css",
+ "line": 13,
+ "value": "#e6edf7",
+ "text": ".hm-term { border-radius: 16px; background: #0b1220; color: #e6edf7; border: 1px solid rgba(148, 163, 184, .25); box-shadow: var(--shadow-lg, 0 18px 50px rgba(0"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/home.css",
+ "line": 13,
+ "value": "rgba(148, 163, 184, .25)",
+ "text": ".hm-term { border-radius: 16px; background: #0b1220; color: #e6edf7; border: 1px solid rgba(148, 163, 184, .25); box-shadow: var(--shadow-lg, 0 18px 50px rgba(0"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/home.css",
+ "line": 14,
+ "value": "rgba(148, 163, 184, .1)",
+ "text": ".hm-term-top { display: flex; align-items: center; gap: 6px; margin: 0; padding: 10px 14px; background: rgba(148, 163, 184, .1); font-size: 12.5px; }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/home.css",
+ "line": 15,
+ "value": "#f87171",
+ "text": ".hm-term-top span { width: 10px; height: 10px; border-radius: 50%; background: #f87171; }"
+ }
+ ]
+ },
+ {
+ "repo": "OpenVibe.Cam",
+ "commit": "0ad257ee0decb99121d649162f58e21cf6891c2d",
+ "files_scanned": 3,
+ "counts": {
+ "literal_duration": 0,
+ "literal_curve": 0,
+ "spinner": 0,
+ "color_drift": 27,
+ "missing_reduced_motion": 0,
+ "total": 27
+ },
+ "score": 98.65,
+ "findings_total": 27,
+ "findings": [
+ {
+ "rule": "color_drift",
+ "file": "public/css/cam.css",
+ "line": 65,
+ "value": "#05070c",
+ "text": ".cm-stage { position: relative; overflow: hidden; aspect-ratio: 4 / 3; border-radius: 22px; background: #05070c; box-shadow: inset 0 0 0 1px rgba(255, 255, 255,"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/cam.css",
+ "line": 65,
+ "value": "rgba(255, 255, 255, .06)",
+ "text": ".cm-stage { position: relative; overflow: hidden; aspect-ratio: 4 / 3; border-radius: 22px; background: #05070c; box-shadow: inset 0 0 0 1px rgba(255, 255, 255,"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/cam.css",
+ "line": 65,
+ "value": "rgba(0, 0, 0, .8)",
+ "text": ".cm-stage { position: relative; overflow: hidden; aspect-ratio: 4 / 3; border-radius: 22px; background: #05070c; box-shadow: inset 0 0 0 1px rgba(255, 255, 255,"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/cam.css",
+ "line": 72,
+ "value": "rgba(255, 255, 255, .55)",
+ "text": "color: rgba(255, 255, 255, .55); }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/cam.css",
+ "line": 74,
+ "value": "rgba(255, 255, 255, .35)",
+ "text": "background: linear-gradient(90deg, transparent calc(33.333% - .5px), rgba(255, 255, 255, .35) calc(33.333% - .5px) calc(33.333% + .5px), transparent calc(33.333"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/cam.css",
+ "line": 74,
+ "value": "rgba(255, 255, 255, .35)",
+ "text": "background: linear-gradient(90deg, transparent calc(33.333% - .5px), rgba(255, 255, 255, .35) calc(33.333% - .5px) calc(33.333% + .5px), transparent calc(33.333"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/cam.css",
+ "line": 74,
+ "value": "rgba(255, 255, 255, .35)",
+ "text": "background: linear-gradient(90deg, transparent calc(33.333% - .5px), rgba(255, 255, 255, .35) calc(33.333% - .5px) calc(33.333% + .5px), transparent calc(33.333"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/cam.css",
+ "line": 74,
+ "value": "rgba(255, 255, 255, .35)",
+ "text": "background: linear-gradient(90deg, transparent calc(33.333% - .5px), rgba(255, 255, 255, .35) calc(33.333% - .5px) calc(33.333% + .5px), transparent calc(33.333"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/cam.css",
+ "line": 129,
+ "value": "rgba(0, 0, 0, .7)",
+ "text": ".cm-live-frame { position: relative; overflow: hidden; border-radius: 14px; aspect-ratio: 4 / 3; box-shadow: 0 24px 50px -30px rgba(0, 0, 0, .7); }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/cam.css",
+ "line": 131,
+ "value": "#fff",
+ "text": ".cm-live-badge { position: absolute; top: 10px; left: 10px; padding: 3px 8px; border-radius: 6px; background: var(--ovk-live, #ef4444); color: #fff;"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/cam.css",
+ "line": 134,
+ "value": "rgba(10, 12, 18, .66)",
+ "text": "background: rgba(10, 12, 18, .66); color: #fff; box-shadow: 0 0 0 2px rgba(255, 255, 255, .7);"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/cam.css",
+ "line": 134,
+ "value": "#fff",
+ "text": "background: rgba(10, 12, 18, .66); color: #fff; box-shadow: 0 0 0 2px rgba(255, 255, 255, .7);"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/cam.css",
+ "line": 134,
+ "value": "rgba(255, 255, 255, .7)",
+ "text": "background: rgba(10, 12, 18, .66); color: #fff; box-shadow: 0 0 0 2px rgba(255, 255, 255, .7);"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/cam.css",
+ "line": 137,
+ "value": "#fff",
+ "text": ".cm-live-flash { position: absolute; inset: 0; background: #fff; opacity: 0;"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/cam.css",
+ "line": 147,
+ "value": "rgba(0, 0, 0, .6)",
+ "text": "background: var(--ovk-panel); border: 1px solid var(--ovk-line); box-shadow: 0 24px 50px -30px rgba(0, 0, 0, .6); }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/cam.css",
+ "line": 164,
+ "value": "rgba(0, 0, 0, .7)",
+ "text": ".cm-f-main { position: relative; overflow: hidden; border-radius: 14px; aspect-ratio: 4 / 3; box-shadow: 0 24px 50px -30px rgba(0, 0, 0, .7); }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/cam.css",
+ "line": 165,
+ "value": "rgba(10, 12, 18, .7)",
+ "text": ".cm-f-saved { position: absolute; left: 10px; bottom: 10px; padding: 5px 10px; border-radius: 999px; background: rgba(10, 12, 18, .7); color: #fff;"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/cam.css",
+ "line": 165,
+ "value": "#fff",
+ "text": ".cm-f-saved { position: absolute; left: 10px; bottom: 10px; padding: 5px 10px; border-radius: 999px; background: rgba(10, 12, 18, .7); color: #fff;"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/cam.css",
+ "line": 180,
+ "value": "rgba(0, 0, 0, .75)",
+ "text": "box-shadow: 0 40px 80px -40px rgba(0, 0, 0, .75), 0 0 0 1px color-mix(in srgb, var(--room-accent) 30%, transparent); }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/cam.css",
+ "line": 182,
+ "value": "rgba(255, 255, 255, .9)",
+ "text": ".cm-vf-c { position: absolute; width: 28px; height: 28px; border: 0 solid rgba(255, 255, 255, .9); }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/cam.css",
+ "line": 187,
+ "value": "#ffd27a",
+ "text": ".cm-vf-focus { position: absolute; left: 60%; top: 42%; width: 64px; height: 64px; margin: -32px 0 0 -32px; border: 2px solid #ffd27a; border-radius: 10px;"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/cam.css",
+ "line": 191,
+ "value": "rgba(10, 12, 18, .6)",
+ "text": "background: rgba(10, 12, 18, .6); color: #fff; font-size: 12px; }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/cam.css",
+ "line": 191,
+ "value": "#fff",
+ "text": "background: rgba(10, 12, 18, .6); color: #fff; font-size: 12px; }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/cam.css",
+ "line": 192,
+ "value": "#ffd27a",
+ "text": ".cm-vf-top b { color: #ffd27a; }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/cam.css",
+ "line": 193,
+ "value": "#fff",
+ "text": ".cm-vf-shutter { position: absolute; left: 50%; bottom: 18px; width: 54px; height: 54px; margin-left: -27px; border-radius: 50%; background: #fff;"
+ }
+ ]
+ },
+ {
+ "repo": "OpenVibe.Casino",
+ "commit": "fbca80eca82d056b389826b85233b7278c2c635d",
+ "files_scanned": 6,
+ "counts": {
+ "literal_duration": 0,
+ "literal_curve": 0,
+ "spinner": 0,
+ "color_drift": 0,
+ "missing_reduced_motion": 0,
+ "total": 0
+ },
+ "score": 100,
+ "findings_total": 0,
+ "findings": []
+ },
+ {
+ "repo": "OpenVibe.Chat",
+ "commit": "20eb7b56ec9b8261c365056fef38627b9c4f0453",
+ "files_scanned": 16,
+ "counts": {
+ "literal_duration": 29,
+ "literal_curve": 7,
+ "spinner": 0,
+ "color_drift": 65,
+ "missing_reduced_motion": 0,
+ "total": 101
+ },
+ "score": 87.75,
+ "findings_total": 101,
+ "findings": [
+ {
+ "rule": "literal_duration",
+ "file": "public/web/chat.css",
+ "line": 123,
+ "value": ".25s",
+ "text": "@media (prefers-reduced-motion: no-preference) { .oc-ring:not(.oc-ring-gone) { animation: oc-ring-in .25s ease-out; } @keyframes oc-ring-in { from { transform: "
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/web/chat.css",
+ "line": 123,
+ "value": "ease-out",
+ "text": "@media (prefers-reduced-motion: no-preference) { .oc-ring:not(.oc-ring-gone) { animation: oc-ring-in .25s ease-out; } @keyframes oc-ring-in { from { transform: "
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/web/chat.css",
+ "line": 151,
+ "value": ".15s",
+ "text": ".oc-srv-icon { position: relative; display: grid; place-items: center; width: 44px; height: 44px; flex: 0 0 auto; border-radius: 50%; background: hsl(var(--oc-h"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/web/chat.css",
+ "line": 151,
+ "value": ".15s",
+ "text": ".oc-srv-icon { position: relative; display: grid; place-items: center; width: 44px; height: 44px; flex: 0 0 auto; border-radius: 50%; background: hsl(var(--oc-h"
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/web/chat.css",
+ "line": 151,
+ "value": "ease",
+ "text": ".oc-srv-icon { position: relative; display: grid; place-items: center; width: 44px; height: 44px; flex: 0 0 auto; border-radius: 50%; background: hsl(var(--oc-h"
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/web/chat.css",
+ "line": 151,
+ "value": "ease",
+ "text": ".oc-srv-icon { position: relative; display: grid; place-items: center; width: 44px; height: 44px; flex: 0 0 auto; border-radius: 50%; background: hsl(var(--oc-h"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/web/chat.css",
+ "line": 356,
+ "value": "1.6s",
+ "text": ".oc-task-running .oc-task-state { background: var(--oc-accent); color: var(--oc-on-accent); animation: oc-work 1.6s ease-in-out infinite; }"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/web/embed.css",
+ "line": 37,
+ "value": "1.2s",
+ "text": ".em-dots { display: inline-flex; gap: 4px; } .em-dots i { width: 6px; height: 6px; border-radius: 50%; background: currentColor; opacity: .4; animation: emDot 1"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/web/feed-view.css",
+ "line": 9,
+ "value": "2s",
+ "text": ".fv-live .fv-dot { background: var(--fv-ok); box-shadow: 0 0 0 0 color-mix(in srgb, var(--fv-ok) 60%, transparent); animation: fvPulse 2s infinite; }"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/web/feed-view.css",
+ "line": 15,
+ "value": ".15s",
+ "text": ".fv-chev { color: var(--fv-muted); transition: transform .15s; }"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/web/vibe.css",
+ "line": 28,
+ "value": ".15s",
+ "text": "transition: background .15s, border-color .15s;"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/web/vibe.css",
+ "line": 28,
+ "value": ".15s",
+ "text": "transition: background .15s, border-color .15s;"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/web/vibe.css",
+ "line": 42,
+ "value": ".15s",
+ "text": ".vb-how summary::before { content: \"▸\"; display: inline-block; margin-right: 6px; transition: transform .15s; }"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/web/vibe.css",
+ "line": 140,
+ "value": "1.2s",
+ "text": ".vb-dots i { width: 7px; height: 7px; border-radius: 50%; background: var(--oc-muted); opacity: .35; animation: vbDot 1.2s infinite ease-in-out; }"
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/web/vibe.css",
+ "line": 140,
+ "value": "ease-in-out",
+ "text": ".vb-dots i { width: 7px; height: 7px; border-radius: 50%; background: var(--oc-muted); opacity: .35; animation: vbDot 1.2s infinite ease-in-out; }"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/web/vibe.css",
+ "line": 178,
+ "value": ".15s",
+ "text": "transition: background .15s, border-color .15s, transform .15s;"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/web/vibe.css",
+ "line": 178,
+ "value": ".15s",
+ "text": "transition: background .15s, border-color .15s, transform .15s;"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/web/widget.js",
+ "line": 49,
+ "value": ".15s",
+ "text": "'background:var(--c);color:#fff;box-shadow:0 10px 30px rgba(0,0,0,.28);display:grid;place-items:center;transition:transform .15s}' +"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/web/widget.js",
+ "line": 55,
+ "value": ".16s",
+ "text": "'box-shadow:0 24px 70px rgba(0,0,0,.35);background:#0a0f1c;opacity:0;transform:translateY(10px);pointer-events:none;transition:opacity .16s,transform .2s}' +"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/web/widget.js",
+ "line": 55,
+ "value": ".2s",
+ "text": "'box-shadow:0 24px 70px rgba(0,0,0,.35);background:#0a0f1c;opacity:0;transform:translateY(10px);pointer-events:none;transition:opacity .16s,transform .2s}' +"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/web/embed.css",
+ "line": 3,
+ "value": "--bg #ffffff",
+ "text": ":root { --bg: #ffffff; --panel: #f4f6fa; --text: #101828; --muted: #5b6577; --line: #e3e7ef; --mine: #eef2fb; color-scheme: light; }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/web/embed.css",
+ "line": 3,
+ "value": "--panel #f4f6fa",
+ "text": ":root { --bg: #ffffff; --panel: #f4f6fa; --text: #101828; --muted: #5b6577; --line: #e3e7ef; --mine: #eef2fb; color-scheme: light; }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/web/embed.css",
+ "line": 3,
+ "value": "--text #101828",
+ "text": ":root { --bg: #ffffff; --panel: #f4f6fa; --text: #101828; --muted: #5b6577; --line: #e3e7ef; --mine: #eef2fb; color-scheme: light; }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/web/embed.css",
+ "line": 3,
+ "value": "--muted #5b6577",
+ "text": ":root { --bg: #ffffff; --panel: #f4f6fa; --text: #101828; --muted: #5b6577; --line: #e3e7ef; --mine: #eef2fb; color-scheme: light; }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/web/embed.css",
+ "line": 3,
+ "value": "--line #e3e7ef",
+ "text": ":root { --bg: #ffffff; --panel: #f4f6fa; --text: #101828; --muted: #5b6577; --line: #e3e7ef; --mine: #eef2fb; color-scheme: light; }"
+ }
+ ]
+ },
+ {
+ "repo": "OpenVibe.Codes",
+ "commit": "302722579e101e3e1dfe468ae875cdb85e4bd781",
+ "files_scanned": 8,
+ "counts": {
+ "literal_duration": 13,
+ "literal_curve": 6,
+ "spinner": 0,
+ "color_drift": 7,
+ "missing_reduced_motion": 0,
+ "total": 26
+ },
+ "score": 94.9,
+ "findings_total": 26,
+ "findings": [
+ {
+ "rule": "literal_duration",
+ "file": "public/css/c4.css",
+ "line": 51,
+ "value": ".24s",
+ "text": ".c4-progress-content{grid-template-rows:0fr;opacity:0;overflow:hidden;transition:grid-template-rows .24s ease,opacity .24s ease}.c4-progress-content.is-open,.c4"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/c4.css",
+ "line": 51,
+ "value": ".24s",
+ "text": ".c4-progress-content{grid-template-rows:0fr;opacity:0;overflow:hidden;transition:grid-template-rows .24s ease,opacity .24s ease}.c4-progress-content.is-open,.c4"
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/css/c4.css",
+ "line": 51,
+ "value": "ease",
+ "text": ".c4-progress-content{grid-template-rows:0fr;opacity:0;overflow:hidden;transition:grid-template-rows .24s ease,opacity .24s ease}.c4-progress-content.is-open,.c4"
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/css/c4.css",
+ "line": 51,
+ "value": "ease",
+ "text": ".c4-progress-content{grid-template-rows:0fr;opacity:0;overflow:hidden;transition:grid-template-rows .24s ease,opacity .24s ease}.c4-progress-content.is-open,.c4"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/chat.css",
+ "line": 24,
+ "value": "1.6s",
+ "text": ".ch-dot.is-busy{background:var(--ch-accent);animation:ch-breathe 1.6s ease-in-out infinite}"
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/css/chat.css",
+ "line": 24,
+ "value": "ease-in-out",
+ "text": ".ch-dot.is-busy{background:var(--ch-accent);animation:ch-breathe 1.6s ease-in-out infinite}"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/chat.css",
+ "line": 69,
+ "value": ".2s",
+ "text": ".ch-caret{width:14px;height:14px;transition:rotate .2s}"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/chat.css",
+ "line": 77,
+ "value": ".35s",
+ "text": ".ch-step.is-new{animation:ch-in .35s ease-out}"
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/css/chat.css",
+ "line": 77,
+ "value": "ease-out",
+ "text": ".ch-step.is-new{animation:ch-in .35s ease-out}"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/chat.css",
+ "line": 83,
+ "value": "1s",
+ "text": ".ch-dots i{width:6px;height:6px;border-radius:50%;background:var(--ch-accent);animation:ch-hop 1s ease-in-out infinite}"
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/css/chat.css",
+ "line": 83,
+ "value": "ease-in-out",
+ "text": ".ch-dots i{width:6px;height:6px;border-radius:50%;background:var(--ch-accent);animation:ch-hop 1s ease-in-out infinite}"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/chat.css",
+ "line": 150,
+ "value": ".2s",
+ "text": ".ch-send{display:inline-grid;place-items:center;width:36px;height:36px;flex:none;border:0;border-radius:50%;background:var(--ch-accent);color:var(--ch-on-accent"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/chat.css",
+ "line": 150,
+ "value": ".15s",
+ "text": ".ch-send{display:inline-grid;place-items:center;width:36px;height:36px;flex:none;border:0;border-radius:50%;background:var(--ch-accent);color:var(--ch-on-accent"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/chat.css",
+ "line": 172,
+ "value": ".15s",
+ "text": ".ch-mode{position:relative;display:grid;grid-template-columns:auto minmax(0,1fr);align-content:start;gap:2px 11px;padding:12px 14px;border:1px solid var(--ch-li"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/chat.css",
+ "line": 172,
+ "value": ".15s",
+ "text": ".ch-mode{position:relative;display:grid;grid-template-columns:auto minmax(0,1fr);align-content:start;gap:2px 11px;padding:12px 14px;border:1px solid var(--ch-li"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/chat.css",
+ "line": 212,
+ "value": ".15s",
+ "text": ".ch-idea{display:inline-flex;align-items:center;gap:8px;padding:8px 14px 8px 11px;border-radius:14px;border:1px solid var(--ch-line);background:var(--ch-card);c"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/chat.css",
+ "line": 212,
+ "value": ".15s",
+ "text": ".ch-idea{display:inline-flex;align-items:center;gap:8px;padding:8px 14px 8px 11px;border-radius:14px;border:1px solid var(--ch-line);background:var(--ch-card);c"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/chat.css",
+ "line": 248,
+ "value": ".25s",
+ "text": ".ch-side{position:fixed;z-index:10060;top:0;right:0;bottom:0;width:min(340px,88vw);translate:100% 0;transition:translate .25s ease;box-shadow:-12px 0 32px rgba("
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/css/chat.css",
+ "line": 248,
+ "value": "ease",
+ "text": ".ch-side{position:fixed;z-index:10060;top:0;right:0;bottom:0;width:min(340px,88vw);translate:100% 0;transition:translate .25s ease;box-shadow:-12px 0 32px rgba("
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/chat.css",
+ "line": 132,
+ "value": "rgba(0,0,0,.14)",
+ "text": ".ch-composer{position:relative;margin:0 max(16px,calc((100% - var(--ch-col)) / 2)) 14px;padding:10px 10px 8px 16px;border:1px solid var(--ch-line);border-radius"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/chat.css",
+ "line": 207,
+ "value": "#000",
+ "text": ".ch-spark-view{flex:1;min-width:0;overflow:hidden;-webkit-mask-image:linear-gradient(90deg,transparent,#000 5%,#000 95%,transparent);mask-image:linear-gradient("
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/chat.css",
+ "line": 207,
+ "value": "#000",
+ "text": ".ch-spark-view{flex:1;min-width:0;overflow:hidden;-webkit-mask-image:linear-gradient(90deg,transparent,#000 5%,#000 95%,transparent);mask-image:linear-gradient("
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/chat.css",
+ "line": 207,
+ "value": "#000",
+ "text": ".ch-spark-view{flex:1;min-width:0;overflow:hidden;-webkit-mask-image:linear-gradient(90deg,transparent,#000 5%,#000 95%,transparent);mask-image:linear-gradient("
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/chat.css",
+ "line": 207,
+ "value": "#000",
+ "text": ".ch-spark-view{flex:1;min-width:0;overflow:hidden;-webkit-mask-image:linear-gradient(90deg,transparent,#000 5%,#000 95%,transparent);mask-image:linear-gradient("
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/chat.css",
+ "line": 248,
+ "value": "rgba(0,0,0,.25)",
+ "text": ".ch-side{position:fixed;z-index:10060;top:0;right:0;bottom:0;width:min(340px,88vw);translate:100% 0;transition:translate .25s ease;box-shadow:-12px 0 32px rgba("
+ }
+ ]
+ },
+ {
+ "repo": "OpenVibe.Community",
+ "commit": "0962b43798e79204632f3c74ee82cfe67056f04f",
+ "files_scanned": 3,
+ "counts": {
+ "literal_duration": 7,
+ "literal_curve": 0,
+ "spinner": 0,
+ "color_drift": 30,
+ "missing_reduced_motion": 0,
+ "total": 37
+ },
+ "score": 96.75,
+ "findings_total": 37,
+ "findings": [
+ {
+ "rule": "literal_duration",
+ "file": "public/css/community.css",
+ "line": 31,
+ "value": ".15s",
+ "text": ".btn { display: inline-flex; align-items: center; gap: 7px; padding: 9px 15px; border-radius: 9px; border: 1px solid var(--border-light); background: var(--bg-c"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/community.css",
+ "line": 31,
+ "value": ".15s",
+ "text": ".btn { display: inline-flex; align-items: center; gap: 7px; padding: 9px 15px; border-radius: 9px; border: 1px solid var(--border-light); background: var(--bg-c"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/community.css",
+ "line": 31,
+ "value": ".1s",
+ "text": ".btn { display: inline-flex; align-items: center; gap: 7px; padding: 9px 15px; border-radius: 9px; border: 1px solid var(--border-light); background: var(--bg-c"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/community.css",
+ "line": 58,
+ "value": ".15s",
+ "text": ".card { background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; display: flex; flex-direction: column; tran"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/community.css",
+ "line": 58,
+ "value": ".15s",
+ "text": ".card { background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; display: flex; flex-direction: column; tran"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/community.css",
+ "line": 58,
+ "value": ".15s",
+ "text": ".card { background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; display: flex; flex-direction: column; tran"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/community.css",
+ "line": 113,
+ "value": ".2s",
+ "text": ".shot.nsfw img { filter: blur(20px); transition: filter .2s; }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/community.css",
+ "line": 5,
+ "value": "--bg-primary #0a0f1c",
+ "text": "--bg-primary: #0a0f1c; --bg-secondary: #101828; --bg-tertiary: #172238; --bg-card: #131c2e; --bg-hover: #1c2a44; --bg-input: #0d1424;"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/community.css",
+ "line": 5,
+ "value": "--bg-secondary #101828",
+ "text": "--bg-primary: #0a0f1c; --bg-secondary: #101828; --bg-tertiary: #172238; --bg-card: #131c2e; --bg-hover: #1c2a44; --bg-input: #0d1424;"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/community.css",
+ "line": 5,
+ "value": "--bg-tertiary #172238",
+ "text": "--bg-primary: #0a0f1c; --bg-secondary: #101828; --bg-tertiary: #172238; --bg-card: #131c2e; --bg-hover: #1c2a44; --bg-input: #0d1424;"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/community.css",
+ "line": 5,
+ "value": "--bg-card #131c2e",
+ "text": "--bg-primary: #0a0f1c; --bg-secondary: #101828; --bg-tertiary: #172238; --bg-card: #131c2e; --bg-hover: #1c2a44; --bg-input: #0d1424;"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/community.css",
+ "line": 5,
+ "value": "--bg-hover #1c2a44",
+ "text": "--bg-primary: #0a0f1c; --bg-secondary: #101828; --bg-tertiary: #172238; --bg-card: #131c2e; --bg-hover: #1c2a44; --bg-input: #0d1424;"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/community.css",
+ "line": 5,
+ "value": "--bg-input #0d1424",
+ "text": "--bg-primary: #0a0f1c; --bg-secondary: #101828; --bg-tertiary: #172238; --bg-card: #131c2e; --bg-hover: #1c2a44; --bg-input: #0d1424;"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/community.css",
+ "line": 6,
+ "value": "--text-primary #e6edf7",
+ "text": "--text-primary: #e6edf7; --text-secondary: #96a7c2; --text-muted: #7386a3;"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/community.css",
+ "line": 6,
+ "value": "--text-secondary #96a7c2",
+ "text": "--text-primary: #e6edf7; --text-secondary: #96a7c2; --text-muted: #7386a3;"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/community.css",
+ "line": 6,
+ "value": "--text-muted #7386a3",
+ "text": "--text-primary: #e6edf7; --text-secondary: #96a7c2; --text-muted: #7386a3;"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/community.css",
+ "line": 7,
+ "value": "--accent #3b82f6",
+ "text": "--accent: #3b82f6; --accent-light: #60a5fa; --accent-dark: #1d4ed8; --accent-rgb: 59,130,246; --on-accent: #ffffff;"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/community.css",
+ "line": 7,
+ "value": "--accent-light #60a5fa",
+ "text": "--accent: #3b82f6; --accent-light: #60a5fa; --accent-dark: #1d4ed8; --accent-rgb: 59,130,246; --on-accent: #ffffff;"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/community.css",
+ "line": 7,
+ "value": "--accent-dark #1d4ed8",
+ "text": "--accent: #3b82f6; --accent-light: #60a5fa; --accent-dark: #1d4ed8; --accent-rgb: 59,130,246; --on-accent: #ffffff;"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/community.css",
+ "line": 7,
+ "value": "--on-accent #ffffff",
+ "text": "--accent: #3b82f6; --accent-light: #60a5fa; --accent-dark: #1d4ed8; --accent-rgb: 59,130,246; --on-accent: #ffffff;"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/community.css",
+ "line": 8,
+ "value": "--border #1f2d47",
+ "text": "--border: #1f2d47; --border-light: #2c3d5c;"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/community.css",
+ "line": 8,
+ "value": "--border-light #2c3d5c",
+ "text": "--border: #1f2d47; --border-light: #2c3d5c;"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/community.css",
+ "line": 9,
+ "value": "--success #22c55e",
+ "text": "--success: #22c55e; --warning: #f59e0b; --danger: #ef4444; --info: #38bdf8;"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/community.css",
+ "line": 9,
+ "value": "--warning #f59e0b",
+ "text": "--success: #22c55e; --warning: #f59e0b; --danger: #ef4444; --info: #38bdf8;"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/community.css",
+ "line": 9,
+ "value": "--danger #ef4444",
+ "text": "--success: #22c55e; --warning: #f59e0b; --danger: #ef4444; --info: #38bdf8;"
+ }
+ ]
+ },
+ {
+ "repo": "OpenVibe.Coupons",
+ "commit": "60c410db42097b4e8e6352e6ffac0b146d08afa7",
+ "files_scanned": 1,
+ "counts": {
+ "literal_duration": 1,
+ "literal_curve": 0,
+ "spinner": 0,
+ "color_drift": 0,
+ "missing_reduced_motion": 0,
+ "total": 1
+ },
+ "score": 99.75,
+ "findings_total": 1,
+ "findings": [
+ {
+ "rule": "literal_duration",
+ "file": "public/css/coupons.css",
+ "line": 75,
+ "value": ".15s",
+ "text": ".cp-stats a { display: flex; flex-direction: column; gap: 2px; height: 100%; padding: 14px 16px; border-radius: 14px; border: 1px solid var(--border, #1f2d47); "
+ }
+ ]
+ },
+ {
+ "repo": "OpenVibe.Deals",
+ "commit": "73e9cac7b3a1f88e05184013267ddd3bec0eff37",
+ "files_scanned": 2,
+ "counts": {
+ "literal_duration": 4,
+ "literal_curve": 0,
+ "spinner": 0,
+ "color_drift": 2,
+ "missing_reduced_motion": 0,
+ "total": 6
+ },
+ "score": 98.9,
+ "findings_total": 6,
+ "findings": [
+ {
+ "rule": "literal_duration",
+ "file": "public/css/deals.css",
+ "line": 51,
+ "value": ".15s",
+ "text": ".deal-item { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 8px 18px; padding: 14px 16px; border-radius: 14px; border: 1px solid var(--border, "
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/deals.css",
+ "line": 51,
+ "value": ".15s",
+ "text": ".deal-item { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 8px 18px; padding: 14px 16px; border-radius: 14px; border: 1px solid var(--border, "
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/deals.css",
+ "line": 139,
+ "value": ".15s",
+ "text": ".hm-stats a { display: flex; flex-direction: column; gap: 2px; height: 100%; padding: 14px 16px; border-radius: 14px; border: 1px solid var(--border, #1f2d47); "
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/deals.css",
+ "line": 151,
+ "value": ".15s",
+ "text": ".hm-card { display: flex; flex-direction: column; gap: 8px; min-width: 0; padding: 14px 16px; border-radius: 14px; border: 1px solid var(--border, #1f2d47); bac"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/deal-cards.css",
+ "line": 27,
+ "value": "#f5b301",
+ "text": ".dl-band--pick { color: #f5b301; background: color-mix(in srgb, #f5b301 16%, transparent); }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/deal-cards.css",
+ "line": 27,
+ "value": "#f5b301",
+ "text": ".dl-band--pick { color: #f5b301; background: color-mix(in srgb, #f5b301 16%, transparent); }"
+ }
+ ]
+ },
+ {
+ "repo": "OpenVibe.Design",
+ "commit": "b40d0aa2ab3265977319cc8295e01d83228cfb8e",
+ "files_scanned": 41,
+ "counts": {
+ "literal_duration": 143,
+ "literal_curve": 59,
+ "spinner": 0,
+ "color_drift": 235,
+ "missing_reduced_motion": 0,
+ "total": 437
+ },
+ "score": 37.75,
+ "findings_total": 437,
+ "findings": [
+ {
+ "rule": "literal_duration",
+ "file": "public/css/brand.css",
+ "line": 34,
+ "value": ".18s",
+ "text": ".br-chip{min-height:40px;padding:0 15px;border-radius:999px;border:1px solid var(--border);background:var(--bg-secondary);color:var(--text-primary);font:inherit"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/brand.css",
+ "line": 34,
+ "value": ".18s",
+ "text": ".br-chip{min-height:40px;padding:0 15px;border-radius:999px;border:1px solid var(--border);background:var(--bg-secondary);color:var(--text-primary);font:inherit"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/brand.css",
+ "line": 34,
+ "value": ".18s",
+ "text": ".br-chip{min-height:40px;padding:0 15px;border-radius:999px;border:1px solid var(--border);background:var(--bg-secondary);color:var(--text-primary);font:inherit"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/design.css",
+ "line": 26,
+ "value": ".15s",
+ "text": ".ds-swatch { display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 10px; width: 100%; min-height: 48px; padding: 8px 10px; border-radiu"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/design.css",
+ "line": 26,
+ "value": ".15s",
+ "text": ".ds-swatch { display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 10px; width: 100%; min-height: 48px; padding: 8px 10px; border-radiu"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/design.css",
+ "line": 60,
+ "value": ".2s",
+ "text": ".ds-previews { display: grid; gap: 12px; grid-template-columns: minmax(0, 1fr); transition: filter .2s; }"
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/css/icon-motion.css",
+ "line": 14,
+ "value": "cubic-bezier(",
+ "text": "animation: ovm-draw var(--ovm-draw-time, .9s) cubic-bezier(.65, 0, .35, 1) forwards;"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/icon-motion.css",
+ "line": 21,
+ "value": ".32s",
+ "text": ".ovm-pop { transition: transform .32s cubic-bezier(.34, 1.56, .64, 1); transform-origin: 50% 50%; }"
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/css/icon-motion.css",
+ "line": 21,
+ "value": "cubic-bezier(",
+ "text": ".ovm-pop { transition: transform .32s cubic-bezier(.34, 1.56, .64, 1); transform-origin: 50% 50%; }"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/icons.css",
+ "line": 31,
+ "value": ".15s",
+ "text": ".ic-tile{text-decoration:none;display:grid;justify-items:center;align-content:center;gap:10px;min-height:104px;padding:14px 6px 10px;border-radius:14px;border:1"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/icons.css",
+ "line": 31,
+ "value": ".15s",
+ "text": ".ic-tile{text-decoration:none;display:grid;justify-items:center;align-content:center;gap:10px;min-height:104px;padding:14px 6px 10px;border-radius:14px;border:1"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/icons.css",
+ "line": 31,
+ "value": ".15s",
+ "text": ".ic-tile{text-decoration:none;display:grid;justify-items:center;align-content:center;gap:10px;min-height:104px;padding:14px 6px 10px;border-radius:14px;border:1"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/kit-pages.css",
+ "line": 38,
+ "value": ".15s",
+ "text": "transition: border-color .15s, box-shadow .15s; scroll-margin-top: 72px; }"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/kit-pages.css",
+ "line": 38,
+ "value": ".15s",
+ "text": "transition: border-color .15s, box-shadow .15s; scroll-margin-top: 72px; }"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/marks.css",
+ "line": 25,
+ "value": ".6s",
+ "text": ".mx-lean-bar i{position:absolute;inset:0 auto 0 0;width:var(--l,50%);background:#4f7cff;border-right:2px solid var(--mx-card);transition:width .6s cubic-bezier("
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/css/marks.css",
+ "line": 25,
+ "value": "cubic-bezier(",
+ "text": ".mx-lean-bar i{position:absolute;inset:0 auto 0 0;width:var(--l,50%);background:#4f7cff;border-right:2px solid var(--mx-card);transition:width .6s cubic-bezier("
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/marks.css",
+ "line": 35,
+ "value": ".5s",
+ "text": ".mx-top.is-new{animation:mxTopIn .5s cubic-bezier(.2,.9,.25,1.2) both}"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/motion.css",
+ "line": 71,
+ "value": "1.1s",
+ "text": ".mo-race.is-run i{left:100%;transition:left 1.1s var(--mo-ease,ease)}"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/patterns.css",
+ "line": 6,
+ "value": "180ms",
+ "text": ".pgroup{padding:18px}.pcards{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}.pcard{position:relative;display:grid;gap:6px;min-height:94px;pa"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/patterns.css",
+ "line": 6,
+ "value": "180ms",
+ "text": ".pgroup{padding:18px}.pcards{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}.pcard{position:relative;display:grid;gap:6px;min-height:94px;pa"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/mark-studies.html",
+ "line": 49,
+ "value": "1.1s",
+ "text": ".busy{animation:ringBreathe 1.1s ease-in-out infinite alternate;transform-origin:50px 50px;transform-box:view-box}"
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/mark-studies.html",
+ "line": 49,
+ "value": "ease-in-out",
+ "text": ".busy{animation:ringBreathe 1.1s ease-in-out infinite alternate;transform-origin:50px 50px;transform-box:view-box}"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/loading.css",
+ "line": 31,
+ "value": "#22c55e",
+ "text": ".ld-foot output.is-zero { color: #22c55e; }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/loading.css",
+ "line": 34,
+ "value": "hsl(var(--h)",
+ "text": ".ld-thumb, .ld-tile, .ld-avatar, .ld-dot { display: block; background: linear-gradient(135deg, hsl(var(--h, 220) 70% 55%), hsl(calc(var(--h, 220) + 40) 70% 40%)"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/motion.css",
+ "line": 3,
+ "value": "--mo-o #4f7cff",
+ "text": ".mo{--mo-o:#4f7cff;--mo-v:#8b5cf6;--mo-face:'OV Brand',Nunito,Inter,-apple-system,BlinkMacSystemFont,sans-serif;max-width:1120px;margin:0 auto;padding:clamp(18p"
+ }
+ ]
+ },
+ {
+ "repo": "OpenVibe.Domains",
+ "commit": "7b292c87b16648b74b9808f7f9be21c6968a8b61",
+ "files_scanned": 4,
+ "counts": {
+ "literal_duration": 6,
+ "literal_curve": 1,
+ "spinner": 0,
+ "color_drift": 0,
+ "missing_reduced_motion": 0,
+ "total": 7
+ },
+ "score": 98.25,
+ "findings_total": 7,
+ "findings": [
+ {
+ "rule": "literal_duration",
+ "file": "public/css/domains.css",
+ "line": 37,
+ "value": ".18s",
+ "text": ".dm-tile { position: relative; display: grid; gap: 4px; min-height: 132px; padding: 16px; border-radius: 16px; background: linear-gradient(160deg, color-mix(in "
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/domains.css",
+ "line": 37,
+ "value": ".18s",
+ "text": ".dm-tile { position: relative; display: grid; gap: 4px; min-height: 132px; padding: 16px; border-radius: 16px; background: linear-gradient(160deg, color-mix(in "
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/domains.css",
+ "line": 124,
+ "value": ".9s",
+ "text": "html.ov-motion [data-ov-reveal] .dm-bar span { transform: scaleY(.02); transform-origin: bottom; transition: transform .9s cubic-bezier(.2, .8, .2, 1) .15s; }"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/domains.css",
+ "line": 124,
+ "value": ".15s",
+ "text": "html.ov-motion [data-ov-reveal] .dm-bar span { transform: scaleY(.02); transform-origin: bottom; transition: transform .9s cubic-bezier(.2, .8, .2, 1) .15s; }"
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/css/domains.css",
+ "line": 124,
+ "value": "cubic-bezier(",
+ "text": "html.ov-motion [data-ov-reveal] .dm-bar span { transform: scaleY(.02); transform-origin: bottom; transition: transform .9s cubic-bezier(.2, .8, .2, 1) .15s; }"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/domains.css",
+ "line": 171,
+ "value": ".15s",
+ "text": "background: var(--bg-card, #0f1729); border: 1px solid var(--border, #1f2d47); transition: border-color .15s, transform .15s; }"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/domains.css",
+ "line": 171,
+ "value": ".15s",
+ "text": "background: var(--bg-card, #0f1729); border: 1px solid var(--border, #1f2d47); transition: border-color .15s, transform .15s; }"
+ }
+ ]
+ },
+ {
+ "repo": "OpenVibe.Exchange",
+ "commit": "0921f08c9df602814dbb42eaa3786f334fb1bc48",
+ "files_scanned": 3,
+ "counts": {
+ "literal_duration": 5,
+ "literal_curve": 2,
+ "spinner": 0,
+ "color_drift": 2,
+ "missing_reduced_motion": 0,
+ "total": 9
+ },
+ "score": 98.15,
+ "findings_total": 9,
+ "findings": [
+ {
+ "rule": "literal_duration",
+ "file": "public/css/exchange.css",
+ "line": 70,
+ "value": "2s",
+ "text": "html.ov-motion .ex-motif-arrows path { stroke-dasharray: 500; animation: ex-loop 2s cubic-bezier(.2,.7,.2,1) both; }"
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/css/exchange.css",
+ "line": 70,
+ "value": "cubic-bezier(",
+ "text": "html.ov-motion .ex-motif-arrows path { stroke-dasharray: 500; animation: ex-loop 2s cubic-bezier(.2,.7,.2,1) both; }"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/exchange.css",
+ "line": 77,
+ "value": ".5s",
+ "text": "html.ov-motion .ex-motif-passport { animation: ex-stamp .5s cubic-bezier(.2,.9,.3,1.4) 1.2s both; }"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/exchange.css",
+ "line": 77,
+ "value": "1.2s",
+ "text": "html.ov-motion .ex-motif-passport { animation: ex-stamp .5s cubic-bezier(.2,.9,.3,1.4) 1.2s both; }"
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/css/exchange.css",
+ "line": 77,
+ "value": "cubic-bezier(",
+ "text": "html.ov-motion .ex-motif-passport { animation: ex-stamp .5s cubic-bezier(.2,.9,.3,1.4) 1.2s both; }"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/exchange.css",
+ "line": 92,
+ "value": ".15s",
+ "text": ".ex-listing-link { display: grid; gap: 8px; height: 100%; padding: 16px; border-radius: 16px; text-decoration: none; color: var(--text-primary, #e6edf7) !import"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/exchange.css",
+ "line": 92,
+ "value": ".15s",
+ "text": ".ex-listing-link { display: grid; gap: 8px; height: 100%; padding: 16px; border-radius: 16px; text-decoration: none; color: var(--text-primary, #e6edf7) !import"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/exchange.css",
+ "line": 72,
+ "value": "rgba(0, 0, 0, .22)",
+ "text": ".ex-motif-item { position: absolute; padding: 9px 14px; border-radius: 12px; font-size: .86rem; font-weight: 800; background: var(--bg-card, #0f1729); border: 1"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/exchange.css",
+ "line": 74,
+ "value": "rgba(0, 0, 0, .28)",
+ "text": ".ex-motif-passport { position: absolute; left: 50%; bottom: 8%; translate: -50% 0; display: grid; justify-items: center; padding: 10px 18px; border-radius: 14px"
+ }
+ ]
+ },
+ {
+ "repo": "OpenVibe.Food",
+ "commit": "5c31fd571cb36619fda2714a0cb0b784093560bc",
+ "files_scanned": 10,
+ "counts": {
+ "literal_duration": 4,
+ "literal_curve": 4,
+ "spinner": 0,
+ "color_drift": 8,
+ "missing_reduced_motion": 0,
+ "total": 16
+ },
+ "score": 97.6,
+ "findings_total": 16,
+ "findings": [
+ {
+ "rule": "literal_duration",
+ "file": "public/css/delivery.css",
+ "line": 7,
+ "value": ".15s",
+ "text": ".dv-app { display: flex; flex-direction: column; gap: 6px; padding: 12px 14px; border-radius: 12px; border: 1px solid var(--border, #1f2d47); background: var(--"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/delivery.css",
+ "line": 7,
+ "value": ".15s",
+ "text": ".dv-app { display: flex; flex-direction: column; gap: 6px; padding: 12px 14px; border-radius: 12px; border: 1px solid var(--border, #1f2d47); background: var(--"
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/css/delivery.css",
+ "line": 7,
+ "value": "ease",
+ "text": ".dv-app { display: flex; flex-direction: column; gap: 6px; padding: 12px 14px; border-radius: 12px; border: 1px solid var(--border, #1f2d47); background: var(--"
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/css/delivery.css",
+ "line": 7,
+ "value": "ease",
+ "text": ".dv-app { display: flex; flex-direction: column; gap: 6px; padding: 12px 14px; border-radius: 12px; border: 1px solid var(--border, #1f2d47); background: var(--"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/home.css",
+ "line": 13,
+ "value": ".15s",
+ "text": ".home-cards a { display: flex; flex-direction: column; gap: 4px; height: 100%; box-sizing: border-box; padding: 12px 14px; border-radius: 12px; border: 1px soli"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/home.css",
+ "line": 13,
+ "value": ".15s",
+ "text": ".home-cards a { display: flex; flex-direction: column; gap: 4px; height: 100%; box-sizing: border-box; padding: 12px 14px; border-radius: 12px; border: 1px soli"
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/css/home.css",
+ "line": 13,
+ "value": "ease",
+ "text": ".home-cards a { display: flex; flex-direction: column; gap: 4px; height: 100%; box-sizing: border-box; padding: 12px 14px; border-radius: 12px; border: 1px soli"
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/css/home.css",
+ "line": 13,
+ "value": "ease",
+ "text": ".home-cards a { display: flex; flex-direction: column; gap: 4px; height: 100%; box-sizing: border-box; padding: 12px 14px; border-radius: 12px; border: 1px soli"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/app.css",
+ "line": 67,
+ "value": "rgba(0, 0, 0, .7)",
+ "text": ".food-search-panel { flex-direction: column; align-items: stretch; flex-wrap: nowrap; gap: 14px; margin: 0; padding: 22px; border-radius: 18px; border: 1px soli"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/delivery.css",
+ "line": 19,
+ "value": "#22c55e",
+ "text": ".dv-budget { color: color-mix(in srgb, #22c55e 75%, var(--text-primary, #e6edf7)); border-color: rgba(74, 222, 128, .45); }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/delivery.css",
+ "line": 19,
+ "value": "rgba(74, 222, 128, .45)",
+ "text": ".dv-budget { color: color-mix(in srgb, #22c55e 75%, var(--text-primary, #e6edf7)); border-color: rgba(74, 222, 128, .45); }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/delivery.css",
+ "line": 20,
+ "value": "#3b82f6",
+ "text": ".dv-mid { color: color-mix(in srgb, #3b82f6 75%, var(--text-primary, #e6edf7)); border-color: rgba(96, 165, 250, .45); }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/delivery.css",
+ "line": 20,
+ "value": "rgba(96, 165, 250, .45)",
+ "text": ".dv-mid { color: color-mix(in srgb, #3b82f6 75%, var(--text-primary, #e6edf7)); border-color: rgba(96, 165, 250, .45); }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/delivery.css",
+ "line": 21,
+ "value": "#d946ef",
+ "text": ".dv-premium { color: color-mix(in srgb, #d946ef 75%, var(--text-primary, #e6edf7)); border-color: rgba(240, 171, 252, .45); }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/delivery.css",
+ "line": 21,
+ "value": "rgba(240, 171, 252, .45)",
+ "text": ".dv-premium { color: color-mix(in srgb, #d946ef 75%, var(--text-primary, #e6edf7)); border-color: rgba(240, 171, 252, .45); }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/pantry.css",
+ "line": 6,
+ "value": "#000",
+ "text": "#bc-video { display: block; width: 100%; max-width: 420px; margin-top: 10px; border-radius: 12px; background: #000; }"
+ }
+ ]
+ },
+ {
+ "repo": "OpenVibe.Fun",
+ "commit": "a287ae83a37868dd4a5d472a154eb74ee4e6ec90",
+ "files_scanned": 4,
+ "counts": {
+ "literal_duration": 7,
+ "literal_curve": 2,
+ "spinner": 0,
+ "color_drift": 4,
+ "missing_reduced_motion": 0,
+ "total": 13
+ },
+ "score": 97.55,
+ "findings_total": 13,
+ "findings": [
+ {
+ "rule": "literal_duration",
+ "file": "public/css/fun.css",
+ "line": 67,
+ "value": ".5s",
+ "text": "html.ov-motion .fu-map-motif .fu-pin { animation: fu-pop .5s cubic-bezier(.2,.9,.3,1.5) both; } .fu-pin-2 { animation-delay: .2s !important; } .fu-pin-3 { anima"
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/css/fun.css",
+ "line": 67,
+ "value": "cubic-bezier(",
+ "text": "html.ov-motion .fu-map-motif .fu-pin { animation: fu-pop .5s cubic-bezier(.2,.9,.3,1.5) both; } .fu-pin-2 { animation-delay: .2s !important; } .fu-pin-3 { anima"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/fun.css",
+ "line": 74,
+ "value": ".8s",
+ "text": "html.ov-motion .fu-flip-card { animation: fu-flip .8s cubic-bezier(.2,.7,.2,1) .9s both; }"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/fun.css",
+ "line": 74,
+ "value": ".9s",
+ "text": "html.ov-motion .fu-flip-card { animation: fu-flip .8s cubic-bezier(.2,.7,.2,1) .9s both; }"
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/css/fun.css",
+ "line": 74,
+ "value": "cubic-bezier(",
+ "text": "html.ov-motion .fu-flip-card { animation: fu-flip .8s cubic-bezier(.2,.7,.2,1) .9s both; }"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/fun.css",
+ "line": 87,
+ "value": ".15s",
+ "text": ".fu-quest-link, .fu-plan-link { transition: border-color .15s, transform .15s; }"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/fun.css",
+ "line": 87,
+ "value": ".15s",
+ "text": ".fu-quest-link, .fu-plan-link { transition: border-color .15s, transform .15s; }"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/fun.css",
+ "line": 91,
+ "value": ".15s",
+ "text": ".fu-plan-card { gap: 6px; align-content: start; height: 100%; padding: 15px 16px; border-radius: 16px; background: var(--bg-card, #0f1729); border: 1px solid va"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/fun.css",
+ "line": 91,
+ "value": ".15s",
+ "text": ".fu-plan-card { gap: 6px; align-content: start; height: 100%; padding: 15px 16px; border-radius: 16px; background: var(--bg-card, #0f1729); border: 1px solid va"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/fun.css",
+ "line": 65,
+ "value": "rgba(0, 0, 0, .25)",
+ "text": ".fu-pin { position: absolute; translate: -50% -100%; padding: 5px 10px; border-radius: 10px 10px 10px 2px; font-size: .78rem; font-weight: 800; color: var(--roo"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/fun.css",
+ "line": 70,
+ "value": "rgba(0, 0, 0, .3)",
+ "text": ".fu-flip-card { position: absolute; left: 50%; bottom: 8%; translate: -50% 0; display: grid; gap: 2px; width: min(70%, 260px); padding: 10px 14px; border-radius"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/fun.css",
+ "line": 114,
+ "value": "#000",
+ "text": ".fu-overlay { display: grid; gap: 4px; max-width: 420px; padding: 14px 16px; border-radius: 16px; background: color-mix(in srgb, #000 70%, transparent); color: "
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/fun.css",
+ "line": 114,
+ "value": "#fff",
+ "text": ".fu-overlay { display: grid; gap: 4px; max-width: 420px; padding: 14px 16px; border-radius: 16px; background: color-mix(in srgb, #000 70%, transparent); color: "
+ }
+ ]
+ },
+ {
+ "repo": "OpenVibe.Games",
+ "commit": "3b8910cd2dfab9415c56f680b298097aa57bfea1",
+ "files_scanned": 3,
+ "counts": {
+ "literal_duration": 0,
+ "literal_curve": 0,
+ "spinner": 0,
+ "color_drift": 0,
+ "missing_reduced_motion": 0,
+ "total": 0
+ },
+ "score": 100,
+ "findings_total": 0,
+ "findings": []
+ },
+ {
+ "repo": "OpenVibe.Help",
+ "commit": "bf94ad1b939d27448c9b037fab8df1f250d0fc3d",
+ "files_scanned": 1,
+ "counts": {
+ "literal_duration": 2,
+ "literal_curve": 0,
+ "spinner": 0,
+ "color_drift": 1,
+ "missing_reduced_motion": 0,
+ "total": 3
+ },
+ "score": 99.45,
+ "findings_total": 3,
+ "findings": [
+ {
+ "rule": "literal_duration",
+ "file": "public/css/app.css",
+ "line": 86,
+ "value": ".15s",
+ "text": ".site-card { display: flex; flex-direction: column; gap: 4px; height: 100%; padding: 12px 14px; border-radius: 12px; border: 1px solid var(--border, #1f2d47); b"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/app.css",
+ "line": 86,
+ "value": ".15s",
+ "text": ".site-card { display: flex; flex-direction: column; gap: 4px; height: 100%; padding: 12px 14px; border-radius: 12px; border: 1px solid var(--border, #1f2d47); b"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/app.css",
+ "line": 58,
+ "value": "#10151f",
+ "text": "mark { background: var(--warning, #f59e0b); color: #10151f; border-radius: 3px; }"
+ }
+ ]
+ },
+ {
+ "repo": "OpenVibe.Host",
+ "commit": "6ee9b8afccbe1353c4d94808d57caaa69304de57",
+ "files_scanned": 3,
+ "counts": {
+ "literal_duration": 2,
+ "literal_curve": 2,
+ "spinner": 0,
+ "color_drift": 13,
+ "missing_reduced_motion": 0,
+ "total": 17
+ },
+ "score": 98.35,
+ "findings_total": 17,
+ "findings": [
+ {
+ "rule": "literal_duration",
+ "file": "public/css/catalog.css",
+ "line": 225,
+ "value": ".15s",
+ "text": ".hh-cards a { display: flex; flex-direction: column; gap: 4px; height: 100%; box-sizing: border-box; padding: 12px 14px; border-radius: 12px; border: 1px solid "
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/catalog.css",
+ "line": 225,
+ "value": ".15s",
+ "text": ".hh-cards a { display: flex; flex-direction: column; gap: 4px; height: 100%; box-sizing: border-box; padding: 12px 14px; border-radius: 12px; border: 1px solid "
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/css/catalog.css",
+ "line": 225,
+ "value": "ease",
+ "text": ".hh-cards a { display: flex; flex-direction: column; gap: 4px; height: 100%; box-sizing: border-box; padding: 12px 14px; border-radius: 12px; border: 1px solid "
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/css/catalog.css",
+ "line": 225,
+ "value": "ease",
+ "text": ".hh-cards a { display: flex; flex-direction: column; gap: 4px; height: 100%; box-sizing: border-box; padding: 12px 14px; border-radius: 12px; border: 1px solid "
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/catalog.css",
+ "line": 26,
+ "value": "rgba(255,255,255,.02)",
+ "text": ".hc-table thead th { font-size: 12px; font-weight: 600; letter-spacing: .03em; text-transform: uppercase; color: var(--hs-muted); background: rgba(255,255,255,."
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/catalog.css",
+ "line": 29,
+ "value": "rgba(96,165,250,.05)",
+ "text": ".hc-table tbody tr:hover > * { background: rgba(96,165,250,.05); }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/console.css",
+ "line": 13,
+ "value": "#f87171",
+ "text": ".cs-bad { color: #f87171; }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/console.css",
+ "line": 23,
+ "value": "#4ade80",
+ "text": ".cs-save a { color: #4ade80; }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/console.css",
+ "line": 26,
+ "value": "#4ade80",
+ "text": ".cs-status.is-ok { color: #4ade80; } .cs-status.is-off { color: var(--text-secondary, #9fb0c8); } .cs-status.is-busy { color: #fbbf24; }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/console.css",
+ "line": 26,
+ "value": "#fbbf24",
+ "text": ".cs-status.is-ok { color: #4ade80; } .cs-status.is-off { color: var(--text-secondary, #9fb0c8); } .cs-status.is-busy { color: #fbbf24; }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/host.css",
+ "line": 3,
+ "value": "--hs-line rgba(255,255,255,.12)",
+ "text": "--hs-card: var(--bg-secondary, #111a2c); --hs-line: rgba(255,255,255,.12); --hs-accent: var(--accent, #60a5fa);"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/host.css",
+ "line": 4,
+ "value": "--hs-ok #34d399",
+ "text": "--hs-ok: #34d399; --hs-warn: #fbbf24; --hs-bad: #f87171; }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/host.css",
+ "line": 4,
+ "value": "--hs-warn #fbbf24",
+ "text": "--hs-ok: #34d399; --hs-warn: #fbbf24; --hs-bad: #f87171; }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/host.css",
+ "line": 4,
+ "value": "--hs-bad #f87171",
+ "text": "--hs-ok: #34d399; --hs-warn: #fbbf24; --hs-bad: #f87171; }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/host.css",
+ "line": 15,
+ "value": "rgba(96,165,250,.08)",
+ "text": "tr.active td { background: rgba(96,165,250,.08); }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/host.css",
+ "line": 25,
+ "value": "#0a0f1c",
+ "text": "button, .button { font: inherit; padding: .45rem .9rem; border-radius: 6px; border: 0; background: var(--hs-accent); color: #0a0f1c; cursor: pointer; text-decor"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/host.css",
+ "line": 27,
+ "value": "#0a0f1c",
+ "text": "button.danger, .danger { background: var(--hs-bad); color: #0a0f1c; } button.link.danger { background: none; color: var(--hs-bad); }"
+ }
+ ]
+ },
+ {
+ "repo": "OpenVibe.Inventory",
+ "commit": "bb0f57d6ef10f3a9832cc524d4a82d57102963d3",
+ "files_scanned": 1,
+ "counts": {
+ "literal_duration": 0,
+ "literal_curve": 0,
+ "spinner": 0,
+ "color_drift": 9,
+ "missing_reduced_motion": 0,
+ "total": 9
+ },
+ "score": 99.55,
+ "findings_total": 9,
+ "findings": [
+ {
+ "rule": "color_drift",
+ "file": "public/css/app.css",
+ "line": 51,
+ "value": "--rar #94a3b8",
+ "text": ".r-common { --rar: #94a3b8; }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/app.css",
+ "line": 91,
+ "value": "#fbbf24",
+ "text": ".inv-hero-panel { padding: 20px; border-radius: 18px; border: 1px solid var(--border, #1f2d47); background: linear-gradient(160deg, color-mix(in srgb, #fbbf24 8"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/app.css",
+ "line": 91,
+ "value": "rgba(0, 0, 0, .7)",
+ "text": ".inv-hero-panel { padding: 20px; border-radius: 18px; border: 1px solid var(--border, #1f2d47); background: linear-gradient(160deg, color-mix(in srgb, #fbbf24 8"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/app.css",
+ "line": 119,
+ "value": "#22c55e",
+ "text": ".ws-published { color: #22c55e; border-color: color-mix(in srgb, #22c55e 50%, transparent); }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/app.css",
+ "line": 119,
+ "value": "#22c55e",
+ "text": ".ws-published { color: #22c55e; border-color: color-mix(in srgb, #22c55e 50%, transparent); }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/app.css",
+ "line": 120,
+ "value": "#f59e0b",
+ "text": ".ws-in_review { color: #f59e0b; border-color: color-mix(in srgb, #f59e0b 50%, transparent); }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/app.css",
+ "line": 120,
+ "value": "#f59e0b",
+ "text": ".ws-in_review { color: #f59e0b; border-color: color-mix(in srgb, #f59e0b 50%, transparent); }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/app.css",
+ "line": 121,
+ "value": "#ef4444",
+ "text": ".ws-draft { color: #ef4444; border-color: color-mix(in srgb, #ef4444 50%, transparent); }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/app.css",
+ "line": 121,
+ "value": "#ef4444",
+ "text": ".ws-draft { color: #ef4444; border-color: color-mix(in srgb, #ef4444 50%, transparent); }"
+ }
+ ]
+ },
+ {
+ "repo": "OpenVibe.Live",
+ "commit": "b90121e8c8dfbaa41ecf7caa652ea84983d19a2e",
+ "files_scanned": 119,
+ "counts": {
+ "literal_duration": 931,
+ "literal_curve": 448,
+ "spinner": 0,
+ "color_drift": 1652,
+ "missing_reduced_motion": 0,
+ "total": 3031
+ },
+ "score": 0,
+ "findings_total": 3031,
+ "findings": [
+ {
+ "rule": "literal_duration",
+ "file": "public/banned.html",
+ "line": 17,
+ "value": "6s",
+ "text": "background: radial-gradient(120% 90% at 50% 115%, #4a0b0b 0%, #1a0708 45%, #0b0a0d 75%); animation: breathe 6s ease-in-out infinite; }"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/broadcast.css",
+ "line": 33,
+ "value": "0.2s",
+ "text": "transition: color 0.2s ease;"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/chat-shared.css",
+ "line": 17,
+ "value": "0.12s",
+ "text": "animation: ctxFadeIn 0.12s ease-out;"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/context-cards.css",
+ "line": 9,
+ "value": "0.35s",
+ "text": ".cc-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform 0.35s ease; }"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/cosmetics.css",
+ "line": 17,
+ "value": "3s",
+ "text": "animation: nameFxRainbow 3s linear infinite;"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/features/broadcast.css",
+ "line": 73,
+ "value": "0.15s",
+ "text": "transition: background 0.15s;"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/features/channel.css",
+ "line": 14,
+ "value": "0.15s",
+ "text": "transition: border-color 0.15s ease, transform 0.15s ease;"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/features/chatpage.css",
+ "line": 15,
+ "value": "0.15s",
+ "text": "font-size: 0.82rem; color: var(--text-secondary); transition: background 0.15s;"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/features/content.css",
+ "line": 141,
+ "value": ".32s",
+ "text": "animation: cfIn .32s var(--ease-standard, ease) both;"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/features/dashboard.css",
+ "line": 6,
+ "value": ".3s",
+ "text": "#dash-powerchat-card { transition: box-shadow .3s ease, border-color .3s ease; }"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/features/docs.css",
+ "line": 14,
+ "value": "0.15s",
+ "text": ".docs-tab-arrow { flex: 0 0 auto; display: none; align-items: center; justify-content: center; width: 30px; background: none; border: none; color: var(--text-mu"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/features/featured.css",
+ "line": 10,
+ "value": "320ms",
+ "text": "animation: hfIn 320ms cubic-bezier(.2, .9, .3, 1);"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/features/home.css",
+ "line": 31,
+ "value": "3.5s",
+ "text": ".hero-egg-icon { font-size: 1.3rem; filter: drop-shadow(0 2px 6px rgba(0,0,0,0.4)); animation: eggWobble 3.5s ease-in-out infinite; }"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/features/rundown.css",
+ "line": 238,
+ "value": ".18s",
+ "text": ".rd-more > summary::before { content: ''; width: 8px; height: 8px; border-right: 2px solid currentColor; border-bottom: 2px solid currentColor; transform: trans"
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/css/features/stats.css",
+ "line": 6,
+ "value": "ease-out",
+ "text": "animation: snFade var(--dur-fast, 160ms) ease-out;"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/features/viewer-places.css",
+ "line": 8,
+ "value": "0.16s",
+ "text": "animation: ovp-in 0.16s ease-out;"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/home-fx.css",
+ "line": 10,
+ "value": "0.4s",
+ "text": "pointer-events: none; z-index: 0; opacity: 0; transition: opacity 0.4s;"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/home-tour.css",
+ "line": 32,
+ "value": "1.1s",
+ "text": ".tour-wire { fill: none; stroke-width: 1.6; stroke-linecap: round; stroke: color-mix(in srgb, var(--text-primary) 22%, transparent); stroke-dasharray: 5 7; anim"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/i18n-star.css",
+ "line": 15,
+ "value": "0.35s",
+ "text": "animation: i18nSlideIn 0.35s ease both;"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/media-player.css",
+ "line": 82,
+ "value": "1.4s",
+ "text": "animation: mp-pulse 1.4s ease-out infinite; /* a ring that pulses out: nothing spins */"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/messenger.css",
+ "line": 24,
+ "value": "0.15s",
+ "text": "transition: transform 0.15s, background 0.15s;"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/ov-guide.css",
+ "line": 7,
+ "value": ".25s",
+ "text": ".ovg-scrim { position: absolute; inset: 0; background: rgba(4,5,10,0.72); backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px); animation: ovgFade .25"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/style.css",
+ "line": 180,
+ "value": "0.4s",
+ "text": "transition: background 0.4s ease, border-color 0.4s ease, box-shadow 0.4s ease, backdrop-filter 0.4s ease;"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/index.html",
+ "line": 78,
+ "value": "1.5s",
+ "text": ".ovsk::after,.ovsk-bar::after{content:'';position:absolute;inset:0;background:linear-gradient(100deg,transparent 20%,color-mix(in srgb,var(--text-primary,#f1f4f"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/kiosk.html",
+ "line": 123,
+ "value": ".5s",
+ "text": "min-height:1.5em;transition:opacity .5s"
+ }
+ ]
+ },
+ {
+ "repo": "OpenVibe.Media",
+ "commit": "d4dc27191555fab54bd0503266e3e50cee2c6673",
+ "files_scanned": 2,
+ "counts": {
+ "literal_duration": 0,
+ "literal_curve": 0,
+ "spinner": 0,
+ "color_drift": 0,
+ "missing_reduced_motion": 0,
+ "total": 0
+ },
+ "score": 100,
+ "findings_total": 0,
+ "findings": []
+ },
+ {
+ "repo": "OpenVibe.MediaHub",
+ "commit": "7a2bef57872feaf52d005d562a8bdcbf53951d51",
+ "files_scanned": 9,
+ "counts": {
+ "literal_duration": 13,
+ "literal_curve": 7,
+ "spinner": 0,
+ "color_drift": 46,
+ "missing_reduced_motion": 0,
+ "total": 66
+ },
+ "score": 92.7,
+ "findings_total": 66,
+ "findings": [
+ {
+ "rule": "literal_duration",
+ "file": "public/css/pics.css",
+ "line": 36,
+ "value": ".15s",
+ "text": ".ovp-drop { position: relative; margin: 6px 0 0; border: 2px dashed color-mix(in srgb, var(--ovp-pink) 55%, var(--ovp-line-strong)); border-radius: 20px; backgr"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/pics.css",
+ "line": 36,
+ "value": ".15s",
+ "text": ".ovp-drop { position: relative; margin: 6px 0 0; border: 2px dashed color-mix(in srgb, var(--ovp-pink) 55%, var(--ovp-line-strong)); border-radius: 20px; backgr"
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/css/pics.css",
+ "line": 36,
+ "value": "ease",
+ "text": ".ovp-drop { position: relative; margin: 6px 0 0; border: 2px dashed color-mix(in srgb, var(--ovp-pink) 55%, var(--ovp-line-strong)); border-radius: 20px; backgr"
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/css/pics.css",
+ "line": 36,
+ "value": "ease",
+ "text": ".ovp-drop { position: relative; margin: 6px 0 0; border: 2px dashed color-mix(in srgb, var(--ovp-pink) 55%, var(--ovp-line-strong)); border-radius: 20px; backgr"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/pics.css",
+ "line": 60,
+ "value": ".15s",
+ "text": ".ovp-progress span { display: block; height: 100%; width: 0; background: var(--ovp-pink); transition: width .15s linear; }"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/pics.css",
+ "line": 78,
+ "value": ".2s",
+ "text": ".ovp-tile img { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform .2s ease; }"
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/css/pics.css",
+ "line": 78,
+ "value": "ease",
+ "text": ".ovp-tile img { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform .2s ease; }"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/video.css",
+ "line": 78,
+ "value": ".25s",
+ "text": ".ovv-card-media img { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform .25s ease; }"
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/css/video.css",
+ "line": 78,
+ "value": "ease",
+ "text": ".ovv-card-media img { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform .25s ease; }"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/video.css",
+ "line": 116,
+ "value": ".45s",
+ "text": ".ovv-player { display: block; width: 100%; aspect-ratio: var(--ovv-ar, 16 / 9); max-height: calc(100vh - 150px); max-height: calc(100dvh - 150px); background: #"
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/css/video.css",
+ "line": 116,
+ "value": "cubic-bezier(",
+ "text": ".ovv-player { display: block; width: 100%; aspect-ratio: var(--ovv-ar, 16 / 9); max-height: calc(100vh - 150px); max-height: calc(100dvh - 150px); background: #"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/video.css",
+ "line": 531,
+ "value": ".15s",
+ "text": ".ovv-pip-bar { position: absolute; inset: 0 0 auto 0; display: flex; align-items: center; gap: 4px; padding: 4px 6px; background: linear-gradient(rgba(0, 0, 0, "
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/video.css",
+ "line": 547,
+ "value": "0.18s",
+ "text": ".ovv-owner[open] { animation: ovv-owner-in 0.18s ease-out; }"
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/css/video.css",
+ "line": 547,
+ "value": "ease-out",
+ "text": ".ovv-owner[open] { animation: ovv-owner-in 0.18s ease-out; }"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/video.css",
+ "line": 556,
+ "value": ".15s",
+ "text": "border: 1px solid var(--ovv-line-strong); background: color-mix(in srgb, var(--ovv-card) 70%, transparent); transition: border-color .15s, background .15s;"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/video.css",
+ "line": 556,
+ "value": ".15s",
+ "text": "border: 1px solid var(--ovv-line-strong); background: color-mix(in srgb, var(--ovv-card) 70%, transparent); transition: border-color .15s, background .15s;"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/video.css",
+ "line": 582,
+ "value": ".15s",
+ "text": ".ovv-stat { display: flex; flex-direction: column; gap: 2px; height: 100%; padding: 11px 14px; border: 1px solid var(--ovv-line); border-radius: 14px; backgroun"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/video.css",
+ "line": 582,
+ "value": ".15s",
+ "text": ".ovv-stat { display: flex; flex-direction: column; gap: 2px; height: 100%; padding: 11px 14px; border: 1px solid var(--ovv-line); border-radius: 14px; backgroun"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/video.css",
+ "line": 590,
+ "value": "1.6s",
+ "text": "html.ov-motion .ovv-stat.is-live .ovv-dot { animation: ovv-pulse 1.6s ease-out infinite; }"
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/css/video.css",
+ "line": 590,
+ "value": "ease-out",
+ "text": "html.ov-motion .ovv-stat.is-live .ovv-dot { animation: ovv-pulse 1.6s ease-out infinite; }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/pics.css",
+ "line": 6,
+ "value": "--ovp-pink #f472b6",
+ "text": "--ovp-pink: #f472b6;"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/pics.css",
+ "line": 7,
+ "value": "--ovp-pink-soft #f9a8d4",
+ "text": "--ovp-pink-soft: #f9a8d4;"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/pics.css",
+ "line": 8,
+ "value": "--ovp-on-pink #1f0a17",
+ "text": "--ovp-on-pink: #1f0a17;"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/pics.css",
+ "line": 32,
+ "value": "#c084fc",
+ "text": ".ovp-accent { background: linear-gradient(92deg, var(--ovp-pink), #c084fc 70%); -webkit-background-clip: text; background-clip: text; color: transparent; }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/pics.css",
+ "line": 81,
+ "value": "rgba(0, 0, 0, .7)",
+ "text": ".ovp-flag { position: absolute; left: 8px; top: 8px; padding: 2px 8px; border-radius: 999px; background: rgba(0, 0, 0, .7); color: #fff; font-size: .72rem; font"
+ }
+ ]
+ },
+ {
+ "repo": "OpenVibe.Meme",
+ "commit": "c227e892e228707fc51b80852b3ae83680c22613",
+ "files_scanned": 3,
+ "counts": {
+ "literal_duration": 5,
+ "literal_curve": 3,
+ "spinner": 0,
+ "color_drift": 17,
+ "missing_reduced_motion": 0,
+ "total": 25
+ },
+ "score": 97.15,
+ "findings_total": 25,
+ "findings": [
+ {
+ "rule": "literal_duration",
+ "file": "public/css/meme.css",
+ "line": 47,
+ "value": "1.6s",
+ "text": "html.ov-motion .mm-tree-lines path { animation: mm-grow 1.6s cubic-bezier(.2,.7,.2,1) both; }"
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/css/meme.css",
+ "line": 47,
+ "value": "cubic-bezier(",
+ "text": "html.ov-motion .mm-tree-lines path { animation: mm-grow 1.6s cubic-bezier(.2,.7,.2,1) both; }"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/meme.css",
+ "line": 59,
+ "value": ".7s",
+ "text": "html.ov-motion .mm-sticker { animation: mm-fan .7s cubic-bezier(.2,.9,.3,1.3) both; animation-delay: calc(.9s + var(--d, 0s)); }"
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/css/meme.css",
+ "line": 59,
+ "value": "cubic-bezier(",
+ "text": "html.ov-motion .mm-sticker { animation: mm-fan .7s cubic-bezier(.2,.9,.3,1.3) both; animation-delay: calc(.9s + var(--d, 0s)); }"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/meme.css",
+ "line": 66,
+ "value": ".15s",
+ "text": "background: var(--bg-card, #0f1729); border: 1px solid var(--border, #1f2d47); transition: border-color .15s, transform .15s; }"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/meme.css",
+ "line": 66,
+ "value": ".15s",
+ "text": "background: var(--bg-card, #0f1729); border: 1px solid var(--border, #1f2d47); transition: border-color .15s, transform .15s; }"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/meme.css",
+ "line": 144,
+ "value": ".9s",
+ "text": "html.ov-motion .mm-demo-bars i { animation: mm-bar .9s cubic-bezier(.2,.7,.2,1) both; animation-delay: calc(var(--h) * 30ms); transform-origin: bottom; }"
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/css/meme.css",
+ "line": 144,
+ "value": "cubic-bezier(",
+ "text": "html.ov-motion .mm-demo-bars i { animation: mm-bar .9s cubic-bezier(.2,.7,.2,1) both; animation-delay: calc(var(--h) * 30ms); transform-origin: bottom; }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/meme.css",
+ "line": 51,
+ "value": "rgba(0, 0, 0, .22)",
+ "text": "background: var(--bg-card, #0f1729); border: 1px solid color-mix(in srgb, var(--room-accent) 45%, var(--border, #1f2d47)); box-shadow: 0 8px 22px rgba(0, 0, 0, "
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/meme.css",
+ "line": 56,
+ "value": "black",
+ "text": ".mm-sticker { position: absolute; padding: 4px 10px; border-radius: 8px; font: 900 .78rem/1 Impact, 'Arial Black', sans-serif; letter-spacing: .04em; text-trans"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/meme.css",
+ "line": 56,
+ "value": "#fff",
+ "text": ".mm-sticker { position: absolute; padding: 4px 10px; border-radius: 8px; font: 900 .78rem/1 Impact, 'Arial Black', sans-serif; letter-spacing: .04em; text-trans"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/meme.css",
+ "line": 57,
+ "value": "#000",
+ "text": "background: color-mix(in srgb, var(--room-accent) 70%, #000); rotate: -8deg; box-shadow: 0 6px 14px rgba(0, 0, 0, .25); }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/meme.css",
+ "line": 57,
+ "value": "rgba(0, 0, 0, .25)",
+ "text": "background: color-mix(in srgb, var(--room-accent) 70%, #000); rotate: -8deg; box-shadow: 0 6px 14px rgba(0, 0, 0, .25); }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/meme.css",
+ "line": 130,
+ "value": "#111",
+ "text": ".mm-caption-card { position: relative; margin: 0; overflow: hidden; border-radius: 16px; background: #111; container-type: inline-size; box-shadow: 0 14px 34px "
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/meme.css",
+ "line": 130,
+ "value": "rgba(0, 0, 0, .3)",
+ "text": ".mm-caption-card { position: relative; margin: 0; overflow: hidden; border-radius: 16px; background: #111; container-type: inline-size; box-shadow: 0 14px 34px "
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/meme.css",
+ "line": 133,
+ "value": "#fff",
+ "text": ".mm-caption-card figcaption span { display: block; text-align: center; color: #fff; text-transform: uppercase; overflow-wrap: anywhere;"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/meme.css",
+ "line": 134,
+ "value": "black",
+ "text": "font: 900 clamp(1rem, 7.2cqw, 3.2rem)/1.05 Impact, Haettenschweiler, 'Arial Narrow Bold', 'Arial Black', sans-serif; letter-spacing: .01em;"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/meme.css",
+ "line": 135,
+ "value": "#000",
+ "text": "text-shadow: 2px 2px 0 #000, -2px 2px 0 #000, 2px -2px 0 #000, -2px -2px 0 #000, 0 2px 0 #000, 2px 0 0 #000, 0 -2px 0 #000, -2px 0 0 #000; }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/meme.css",
+ "line": 135,
+ "value": "#000",
+ "text": "text-shadow: 2px 2px 0 #000, -2px 2px 0 #000, 2px -2px 0 #000, -2px -2px 0 #000, 0 2px 0 #000, 2px 0 0 #000, 0 -2px 0 #000, -2px 0 0 #000; }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/meme.css",
+ "line": 135,
+ "value": "#000",
+ "text": "text-shadow: 2px 2px 0 #000, -2px 2px 0 #000, 2px -2px 0 #000, -2px -2px 0 #000, 0 2px 0 #000, 2px 0 0 #000, 0 -2px 0 #000, -2px 0 0 #000; }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/meme.css",
+ "line": 135,
+ "value": "#000",
+ "text": "text-shadow: 2px 2px 0 #000, -2px 2px 0 #000, 2px -2px 0 #000, -2px -2px 0 #000, 0 2px 0 #000, 2px 0 0 #000, 0 -2px 0 #000, -2px 0 0 #000; }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/meme.css",
+ "line": 135,
+ "value": "#000",
+ "text": "text-shadow: 2px 2px 0 #000, -2px 2px 0 #000, 2px -2px 0 #000, -2px -2px 0 #000, 0 2px 0 #000, 2px 0 0 #000, 0 -2px 0 #000, -2px 0 0 #000; }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/meme.css",
+ "line": 135,
+ "value": "#000",
+ "text": "text-shadow: 2px 2px 0 #000, -2px 2px 0 #000, 2px -2px 0 #000, -2px -2px 0 #000, 0 2px 0 #000, 2px 0 0 #000, 0 -2px 0 #000, -2px 0 0 #000; }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/meme.css",
+ "line": 135,
+ "value": "#000",
+ "text": "text-shadow: 2px 2px 0 #000, -2px 2px 0 #000, 2px -2px 0 #000, -2px -2px 0 #000, 0 2px 0 #000, 2px 0 0 #000, 0 -2px 0 #000, -2px 0 0 #000; }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/meme.css",
+ "line": 135,
+ "value": "#000",
+ "text": "text-shadow: 2px 2px 0 #000, -2px 2px 0 #000, 2px -2px 0 #000, -2px -2px 0 #000, 0 2px 0 #000, 2px 0 0 #000, 0 -2px 0 #000, -2px 0 0 #000; }"
+ }
+ ]
+ },
+ {
+ "repo": "OpenVibe.Network",
+ "commit": "390bcc1ca2aa5fcd0e391244bb27a6edebd8fecf",
+ "files_scanned": 31,
+ "counts": {
+ "literal_duration": 89,
+ "literal_curve": 31,
+ "spinner": 0,
+ "color_drift": 212,
+ "missing_reduced_motion": 2,
+ "total": 334
+ },
+ "score": 55.4,
+ "findings_total": 334,
+ "findings": [
+ {
+ "rule": "missing_reduced_motion",
+ "file": "public/css/admin.css",
+ "line": 43,
+ "value": "animation",
+ "text": ".section{display:none;animation:sec-in .2s ease}"
+ },
+ {
+ "rule": "missing_reduced_motion",
+ "file": "public/css/login.css",
+ "line": 17,
+ "value": "animation",
+ "text": "animation:orb-drift 20s ease-in-out infinite alternate;"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/admin.css",
+ "line": 30,
+ "value": ".15s",
+ "text": "background:transparent;color:var(--text-muted);cursor:pointer;transition:all .15s;white-space:nowrap;"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/admin.css",
+ "line": 36,
+ "value": ".15s",
+ "text": "background:var(--bg-secondary);color:var(--text-muted);cursor:pointer;transition:all .15s;opacity:0;pointer-events:none;"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/admin.css",
+ "line": 43,
+ "value": ".2s",
+ "text": ".section{display:none;animation:sec-in .2s ease}"
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/css/admin.css",
+ "line": 43,
+ "value": "ease",
+ "text": ".section{display:none;animation:sec-in .2s ease}"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/login.css",
+ "line": 17,
+ "value": "20s",
+ "text": "animation:orb-drift 20s ease-in-out infinite alternate;"
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/css/login.css",
+ "line": 17,
+ "value": "ease-in-out",
+ "text": "animation:orb-drift 20s ease-in-out infinite alternate;"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/login.css",
+ "line": 49,
+ "value": "30s",
+ "text": "animation:grid-scroll 30s linear infinite;"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/my.css",
+ "line": 38,
+ "value": ".15s",
+ "text": "background:transparent;color:var(--text-muted);cursor:pointer;transition:all .15s;white-space:nowrap;"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/my.css",
+ "line": 65,
+ "value": ".2s",
+ "text": ".section{display:none;animation:sec-in .2s ease}"
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/css/my.css",
+ "line": 65,
+ "value": "ease",
+ "text": ".section{display:none;animation:sec-in .2s ease}"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/my.css",
+ "line": 82,
+ "value": ".18s",
+ "text": "transition:border-color .18s ease,box-shadow .18s ease,background-color .18s ease;"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/index.html",
+ "line": 65,
+ "value": "24s",
+ "text": ".bg-orbs .orb{position:absolute;border-radius:50%;filter:blur(110px);opacity:.14;animation:orb-drift 24s ease-in-out infinite alternate}"
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/index.html",
+ "line": 65,
+ "value": "ease-in-out",
+ "text": ".bg-orbs .orb{position:absolute;border-radius:50%;filter:blur(110px);opacity:.14;animation:orb-drift 24s ease-in-out infinite alternate}"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/index.html",
+ "line": 88,
+ "value": ".15s",
+ "text": "transition:transform .15s, box-shadow .15s, background .15s, border-color .15s;"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/index.html",
+ "line": 88,
+ "value": ".15s",
+ "text": "transition:transform .15s, box-shadow .15s, background .15s, border-color .15s;"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/sso-fanout.html",
+ "line": 35,
+ "value": ".4s",
+ "text": ".card{width:100%;max-width:440px;background:var(--bg-card);border:1px solid var(--border);border-radius:18px;padding:30px 28px;position:relative;overflow:hidden"
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/sso-fanout.html",
+ "line": 35,
+ "value": "cubic-bezier(",
+ "text": ".card{width:100%;max-width:440px;background:var(--bg-card);border:1px solid var(--border);border-radius:18px;padding:30px 28px;position:relative;overflow:hidden"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/sso-fanout.html",
+ "line": 42,
+ "value": ".45s",
+ "text": ".bar i{position:absolute;inset:0 auto 0 0;width:var(--w,0%);background:linear-gradient(90deg,var(--accent),var(--accent-light));transition:width .45s cubic-bezi"
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/sso-fanout.html",
+ "line": 42,
+ "value": "cubic-bezier(",
+ "text": ".bar i{position:absolute;inset:0 auto 0 0;width:var(--w,0%);background:linear-gradient(90deg,var(--accent),var(--accent-light));transition:width .45s cubic-bezi"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/verify-email.html",
+ "line": 38,
+ "value": ".5s",
+ "text": ".card{width:100%;max-width:480px;background:var(--bg-card);border:1px solid var(--border);border-radius:18px;padding:36px 32px;text-align:center;position:relati"
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/verify-email.html",
+ "line": 38,
+ "value": "cubic-bezier(",
+ "text": ".card{width:100%;max-width:480px;background:var(--bg-card);border:1px solid var(--border);border-radius:18px;padding:36px 32px;text-align:center;position:relati"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/verify-email.html",
+ "line": 39,
+ "value": ".6s",
+ "text": ".card::before{content:\"\";position:absolute;inset:-40% -20% auto;height:70%;background:radial-gradient(60% 60% at 50% 40%,rgba(var(--accent-rgb),.22),transparent"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/verify-email.html",
+ "line": 45,
+ "value": "1.4s",
+ "text": ".ring.spin{border-color:var(--accent);animation:ring-pulse 1.4s ease-out infinite}"
+ }
+ ]
+ },
+ {
+ "repo": "OpenVibe.News",
+ "commit": "5c1200cc31425da6aef045cff5cea7bd6c56fca7",
+ "files_scanned": 2,
+ "counts": {
+ "literal_duration": 1,
+ "literal_curve": 1,
+ "spinner": 0,
+ "color_drift": 15,
+ "missing_reduced_motion": 0,
+ "total": 17
+ },
+ "score": 98.75,
+ "findings_total": 17,
+ "findings": [
+ {
+ "rule": "literal_duration",
+ "file": "public/css/briefs.css",
+ "line": 14,
+ "value": "1.6s",
+ "text": ".live-dot { display: inline-block; width: .55em; height: .55em; border-radius: 50%; background: #ef4444; vertical-align: middle; animation: live-pulse 1.6s ease"
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/css/briefs.css",
+ "line": 14,
+ "value": "ease-in-out",
+ "text": ".live-dot { display: inline-block; width: .55em; height: .55em; border-radius: 50%; background: #ef4444; vertical-align: middle; animation: live-pulse 1.6s ease"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/briefs.css",
+ "line": 2,
+ "value": "#a78bfa",
+ "text": ".badge-ai { background: color-mix(in srgb, #a78bfa 18%, transparent); color: color-mix(in srgb, #8b5cf6 70%, var(--text-primary, #e6edf7)); border: 1px solid co"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/briefs.css",
+ "line": 2,
+ "value": "#8b5cf6",
+ "text": ".badge-ai { background: color-mix(in srgb, #a78bfa 18%, transparent); color: color-mix(in srgb, #8b5cf6 70%, var(--text-primary, #e6edf7)); border: 1px solid co"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/briefs.css",
+ "line": 2,
+ "value": "#a78bfa",
+ "text": ".badge-ai { background: color-mix(in srgb, #a78bfa 18%, transparent); color: color-mix(in srgb, #8b5cf6 70%, var(--text-primary, #e6edf7)); border: 1px solid co"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/briefs.css",
+ "line": 3,
+ "value": "#ef4444",
+ "text": ".badge-breaking { background: color-mix(in srgb, #ef4444 16%, transparent); color: color-mix(in srgb, #ef4444 75%, var(--text-primary, #e6edf7)); border: 1px so"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/briefs.css",
+ "line": 3,
+ "value": "#ef4444",
+ "text": ".badge-breaking { background: color-mix(in srgb, #ef4444 16%, transparent); color: color-mix(in srgb, #ef4444 75%, var(--text-primary, #e6edf7)); border: 1px so"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/briefs.css",
+ "line": 3,
+ "value": "#ef4444",
+ "text": ".badge-breaking { background: color-mix(in srgb, #ef4444 16%, transparent); color: color-mix(in srgb, #ef4444 75%, var(--text-primary, #e6edf7)); border: 1px so"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/briefs.css",
+ "line": 14,
+ "value": "#ef4444",
+ "text": ".live-dot { display: inline-block; width: .55em; height: .55em; border-radius: 50%; background: #ef4444; vertical-align: middle; animation: live-pulse 1.6s ease"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/briefs.css",
+ "line": 17,
+ "value": "#ef4444",
+ "text": ".wire-cluster.is-breaking { border-left: 3px solid #ef4444; padding-left: 10px; }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/news.css",
+ "line": 64,
+ "value": "rgba(34,197,94,.25)",
+ "text": ".ov-diff ins { background: rgba(34,197,94,.25); text-decoration: none; }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/news.css",
+ "line": 65,
+ "value": "rgba(239,68,68,.25)",
+ "text": ".ov-diff del { background: rgba(239,68,68,.25); }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/news.css",
+ "line": 90,
+ "value": "#ef4444",
+ "text": ".live-dot { display: inline-block; width: .55em; height: .55em; border-radius: 50%; background: #ef4444; vertical-align: middle; }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/news.css",
+ "line": 91,
+ "value": "#a78bfa",
+ "text": ".badge-ai { background: color-mix(in srgb, #a78bfa 18%, transparent); color: color-mix(in srgb, #8b5cf6 70%, var(--text-primary, #e6edf7)); border: 1px solid co"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/news.css",
+ "line": 91,
+ "value": "#8b5cf6",
+ "text": ".badge-ai { background: color-mix(in srgb, #a78bfa 18%, transparent); color: color-mix(in srgb, #8b5cf6 70%, var(--text-primary, #e6edf7)); border: 1px solid co"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/news.css",
+ "line": 91,
+ "value": "#a78bfa",
+ "text": ".badge-ai { background: color-mix(in srgb, #a78bfa 18%, transparent); color: color-mix(in srgb, #8b5cf6 70%, var(--text-primary, #e6edf7)); border: 1px solid co"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/news.css",
+ "line": 99,
+ "value": "#8b5cf6",
+ "text": ".nh-kicker .badge-ai { color: color-mix(in srgb, #8b5cf6 55%, var(--text-primary, #e6edf7)); } /* 4.49:1 at 70% on a card */"
+ }
+ ]
+ },
+ {
+ "repo": "OpenVibe.Play",
+ "commit": "d43308f86605aa099d43b012d60c7fe069e0aa9d",
+ "files_scanned": 1,
+ "counts": {
+ "literal_duration": 0,
+ "literal_curve": 0,
+ "spinner": 0,
+ "color_drift": 0,
+ "missing_reduced_motion": 0,
+ "total": 0
+ },
+ "score": 100,
+ "findings_total": 0,
+ "findings": []
+ },
+ {
+ "repo": "OpenVibe.Quest",
+ "commit": "3c8533e78a10a5d17c97ee4ba631a412865bf93e",
+ "files_scanned": 1,
+ "counts": {
+ "literal_duration": 2,
+ "literal_curve": 0,
+ "spinner": 0,
+ "color_drift": 8,
+ "missing_reduced_motion": 0,
+ "total": 10
+ },
+ "score": 99.1,
+ "findings_total": 10,
+ "findings": [
+ {
+ "rule": "literal_duration",
+ "file": "public/css/app.css",
+ "line": 61,
+ "value": ".15s",
+ "text": ".quest { display: grid; grid-template-columns: 56px minmax(0, 1fr); gap: 14px; padding: 16px; border-radius: 16px; border: 1px solid var(--border, #1f2d47); bac"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/app.css",
+ "line": 61,
+ "value": ".15s",
+ "text": ".quest { display: grid; grid-template-columns: 56px minmax(0, 1fr); gap: 14px; padding: 16px; border-radius: 16px; border: 1px solid var(--border, #1f2d47); bac"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/app.css",
+ "line": 63,
+ "value": "hsl(var(--h)",
+ "text": ".medal { width: 56px; height: 56px; border-radius: 50%; display: grid; place-items: center; font-weight: 800; font-size: 1rem; letter-spacing: .02em; color: hsl"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/app.css",
+ "line": 63,
+ "value": "hsl(var(--h)",
+ "text": ".medal { width: 56px; height: 56px; border-radius: 50%; display: grid; place-items: center; font-weight: 800; font-size: 1rem; letter-spacing: .02em; color: hsl"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/app.css",
+ "line": 63,
+ "value": "hsl(var(--h)",
+ "text": ".medal { width: 56px; height: 56px; border-radius: 50%; display: grid; place-items: center; font-weight: 800; font-size: 1rem; letter-spacing: .02em; color: hsl"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/app.css",
+ "line": 63,
+ "value": "hsl(var(--h)",
+ "text": ".medal { width: 56px; height: 56px; border-radius: 50%; display: grid; place-items: center; font-weight: 800; font-size: 1rem; letter-spacing: .02em; color: hsl"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/app.css",
+ "line": 63,
+ "value": "hsl(var(--h)",
+ "text": ".medal { width: 56px; height: 56px; border-radius: 50%; display: grid; place-items: center; font-weight: 800; font-size: 1rem; letter-spacing: .02em; color: hsl"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/app.css",
+ "line": 64,
+ "value": "hsl(var(--h)",
+ "text": ".medal.earned { box-shadow: inset 0 0 0 3px hsl(var(--h) 80% 62%), 0 0 0 3px hsl(var(--h) 80% 62% / .25), 0 8px 20px -8px hsl(var(--h) 80% 50%); }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/app.css",
+ "line": 64,
+ "value": "hsl(var(--h)",
+ "text": ".medal.earned { box-shadow: inset 0 0 0 3px hsl(var(--h) 80% 62%), 0 0 0 3px hsl(var(--h) 80% 62% / .25), 0 8px 20px -8px hsl(var(--h) 80% 50%); }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/app.css",
+ "line": 64,
+ "value": "hsl(var(--h)",
+ "text": ".medal.earned { box-shadow: inset 0 0 0 3px hsl(var(--h) 80% 62%), 0 0 0 3px hsl(var(--h) 80% 62% / .25), 0 8px 20px -8px hsl(var(--h) 80% 50%); }"
+ }
+ ]
+ },
+ {
+ "repo": "OpenVibe.Rent",
+ "commit": "de271ae4d15931793564c520eb4aa6c46dc0d9c3",
+ "files_scanned": 7,
+ "counts": {
+ "literal_duration": 4,
+ "literal_curve": 2,
+ "spinner": 0,
+ "color_drift": 3,
+ "missing_reduced_motion": 0,
+ "total": 9
+ },
+ "score": 98.35,
+ "findings_total": 9,
+ "findings": [
+ {
+ "rule": "literal_duration",
+ "file": "public/css/app.css",
+ "line": 44,
+ "value": ".15s",
+ "text": ".listing { --k: 212; display: grid; grid-template-columns: 52px minmax(0, 1fr) auto; gap: 14px; align-items: center; padding: 14px 16px; border-radius: 14px; bo"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/app.css",
+ "line": 44,
+ "value": ".15s",
+ "text": ".listing { --k: 212; display: grid; grid-template-columns: 52px minmax(0, 1fr) auto; gap: 14px; align-items: center; padding: 14px 16px; border-radius: 14px; bo"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/everywhere.css",
+ "line": 7,
+ "value": ".15s",
+ "text": ".ew-site { display: flex; flex-direction: column; gap: 6px; padding: 12px 14px; border-radius: 12px; border: 1px solid var(--border, #1f2d47); background: var(-"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/everywhere.css",
+ "line": 7,
+ "value": ".15s",
+ "text": ".ew-site { display: flex; flex-direction: column; gap: 6px; padding: 12px 14px; border-radius: 12px; border: 1px solid var(--border, #1f2d47); background: var(-"
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/css/everywhere.css",
+ "line": 7,
+ "value": "ease",
+ "text": ".ew-site { display: flex; flex-direction: column; gap: 6px; padding: 12px 14px; border-radius: 12px; border: 1px solid var(--border, #1f2d47); background: var(-"
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/css/everywhere.css",
+ "line": 7,
+ "value": "ease",
+ "text": ".ew-site { display: flex; flex-direction: column; gap: 6px; padding: 12px 14px; border-radius: 12px; border: 1px solid var(--border, #1f2d47); background: var(-"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/app.css",
+ "line": 47,
+ "value": "hsl(var(--k)",
+ "text": ".listing-mark { width: 52px; height: 52px; border-radius: 13px; display: grid; place-items: center; font-size: .72rem; font-weight: 800; letter-spacing: .04em; "
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/app.css",
+ "line": 47,
+ "value": "hsl(var(--k)",
+ "text": ".listing-mark { width: 52px; height: 52px; border-radius: 13px; display: grid; place-items: center; font-size: .72rem; font-weight: 800; letter-spacing: .04em; "
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/app.css",
+ "line": 47,
+ "value": "hsl(var(--k)",
+ "text": ".listing-mark { width: 52px; height: 52px; border-radius: 13px; display: grid; place-items: center; font-size: .72rem; font-weight: 800; letter-spacing: .04em; "
+ }
+ ]
+ },
+ {
+ "repo": "OpenVibe.Report",
+ "commit": "dce6c1f9cb0ca4fed8f27e9f4336ff1b579c94f4",
+ "files_scanned": 4,
+ "counts": {
+ "literal_duration": 2,
+ "literal_curve": 0,
+ "spinner": 0,
+ "color_drift": 0,
+ "missing_reduced_motion": 0,
+ "total": 2
+ },
+ "score": 99.5,
+ "findings_total": 2,
+ "findings": [
+ {
+ "rule": "literal_duration",
+ "file": "public/css/app.css",
+ "line": 124,
+ "value": ".18s",
+ "text": ".rp-topic { display: flex; flex-direction: column; gap: 6px; height: 100%; padding: 14px 16px; border-radius: 16px; background: var(--bg-card, #0f1729); border:"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/app.css",
+ "line": 124,
+ "value": ".18s",
+ "text": ".rp-topic { display: flex; flex-direction: column; gap: 6px; height: 100%; padding: 14px 16px; border-radius: 16px; background: var(--bg-card, #0f1729); border:"
+ }
+ ]
+ },
+ {
+ "repo": "OpenVibe.Reviews",
+ "commit": "2732d267d97e85bb1c4b858ce4e7199ff13f31d3",
+ "files_scanned": 3,
+ "counts": {
+ "literal_duration": 0,
+ "literal_curve": 0,
+ "spinner": 0,
+ "color_drift": 9,
+ "missing_reduced_motion": 0,
+ "total": 9
+ },
+ "score": 99.55,
+ "findings_total": 9,
+ "findings": [
+ {
+ "rule": "color_drift",
+ "file": "public/css/reviews.css",
+ "line": 10,
+ "value": "--rv-red #f87171",
+ "text": "--rv-red: #f87171;"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/reviews.css",
+ "line": 11,
+ "value": "--rv-ok #34d399",
+ "text": "--rv-ok: #34d399;"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/reviews.css",
+ "line": 12,
+ "value": "--rv-warn #fbbf24",
+ "text": "--rv-warn: #fbbf24;"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/reviews.css",
+ "line": 35,
+ "value": "#a78bfa",
+ "text": ".rv-notice.rv-ai { border-color: #a78bfa; }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/reviews.css",
+ "line": 36,
+ "value": "#0a0f1c",
+ "text": ".rv-button, button { display: inline-block; background: var(--rv-accent); color: #0a0f1c; border: 0; border-radius: 6px; padding: 6px 14px; font: inherit; font-"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/reviews.css",
+ "line": 54,
+ "value": "rgba(52, 211, 153, .22)",
+ "text": ".ov-diff ins { background: rgba(52, 211, 153, .22); text-decoration: underline; }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/reviews.css",
+ "line": 55,
+ "value": "rgba(248, 113, 113, .22)",
+ "text": ".ov-diff del { background: rgba(248, 113, 113, .22); }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/reviews.css",
+ "line": 109,
+ "value": "#f5b301",
+ "text": ".rv-stars { color: #f5b301; letter-spacing: 1px; white-space: nowrap; }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/reviews.css",
+ "line": 144,
+ "value": "#dc2626",
+ "text": ".rv-critical { background: color-mix(in srgb, #dc2626 16%, transparent); color: inherit; }"
+ }
+ ]
+ },
+ {
+ "repo": "OpenVibe.Services",
+ "commit": "248e48f1dc87f5d04e67d7944ac976c4b68ef345",
+ "files_scanned": 3,
+ "counts": {
+ "literal_duration": 0,
+ "literal_curve": 0,
+ "spinner": 0,
+ "color_drift": 0,
+ "missing_reduced_motion": 0,
+ "total": 0
+ },
+ "score": 100,
+ "findings_total": 0,
+ "findings": []
+ },
+ {
+ "repo": "OpenVibe.Shoes",
+ "commit": "ccc1f8a02f8dd49e44a1ba0122a0eadab59c0b05",
+ "files_scanned": 2,
+ "counts": {
+ "literal_duration": 0,
+ "literal_curve": 0,
+ "spinner": 0,
+ "color_drift": 3,
+ "missing_reduced_motion": 0,
+ "total": 3
+ },
+ "score": 99.85,
+ "findings_total": 3,
+ "findings": [
+ {
+ "rule": "color_drift",
+ "file": "public/css/shoes.css",
+ "line": 22,
+ "value": "#fff",
+ "text": ".sh-btn { display: inline-flex; align-items: center; gap: 7px; min-height: 40px; padding: 8px 14px; border: 1px solid var(--room-accent, var(--accent)); border-"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/shoes.css",
+ "line": 98,
+ "value": "rgba(0, 0, 0, .7)",
+ "text": "box-shadow: 0 14px 30px -18px rgba(0, 0, 0, .7); color: var(--ovk-text, #e8ecf3); font-variant-numeric: tabular-nums;"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/shoes.css",
+ "line": 105,
+ "value": "rgba(0, 0, 0, .7)",
+ "text": ".sh-mt.is-best { border-color: var(--room-accent); box-shadow: 0 0 0 1px var(--room-accent), 0 14px 30px -18px rgba(0, 0, 0, .7); }"
+ }
+ ]
+ },
+ {
+ "repo": "OpenVibe.Sources",
+ "commit": "ce8f4619ef9e5af96bd8e964419c2efe37f95c56",
+ "files_scanned": 1,
+ "counts": {
+ "literal_duration": 0,
+ "literal_curve": 0,
+ "spinner": 0,
+ "color_drift": 0,
+ "missing_reduced_motion": 0,
+ "total": 0
+ },
+ "score": 100,
+ "findings_total": 0,
+ "findings": []
+ },
+ {
+ "repo": "OpenVibe.Space",
+ "commit": "d9becca7da9443a04f2828cf4aa1599b90ab6188",
+ "files_scanned": 5,
+ "counts": {
+ "literal_duration": 3,
+ "literal_curve": 0,
+ "spinner": 0,
+ "color_drift": 29,
+ "missing_reduced_motion": 0,
+ "total": 32
+ },
+ "score": 97.8,
+ "findings_total": 32,
+ "findings": [
+ {
+ "rule": "literal_duration",
+ "file": "public/css/hosting.css",
+ "line": 129,
+ "value": ".15s",
+ "text": ".hd-tile { display: flex; gap: 14px; align-items: flex-start; padding: 18px; border: 1px solid var(--hd-line); border-radius: 16px; background: var(--hd-card); "
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/spaces.css",
+ "line": 39,
+ "value": ".15s",
+ "text": ".sp-card { display: flex; flex-direction: column; border: 1px solid var(--border, #2c3d5c); border-radius: 16px; background: var(--bg-secondary, #101828); overf"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/spaces.css",
+ "line": 39,
+ "value": ".15s",
+ "text": ".sp-card { display: flex; flex-direction: column; border: 1px solid var(--border, #2c3d5c); border-radius: 16px; background: var(--bg-secondary, #101828); overf"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/hosting.css",
+ "line": 44,
+ "value": "#fff",
+ "text": ".hd-badge--accent { color: color-mix(in srgb, var(--accent-light, #60a5fa) 65%, #fff); border-color: color-mix(in srgb, var(--hd-accent) 50%, transparent); back"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/hosting.css",
+ "line": 111,
+ "value": "#fff",
+ "text": ".hd-table a { color: color-mix(in srgb, var(--accent-light, #60a5fa) 70%, #fff); }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/spaces.css",
+ "line": 20,
+ "value": "#ef4444",
+ "text": ".sp-flash.is-bad { border-color: #ef4444; color: #fecaca; background: color-mix(in srgb, #ef4444 12%, transparent); }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/spaces.css",
+ "line": 20,
+ "value": "#fecaca",
+ "text": ".sp-flash.is-bad { border-color: #ef4444; color: #fecaca; background: color-mix(in srgb, #ef4444 12%, transparent); }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/spaces.css",
+ "line": 20,
+ "value": "#ef4444",
+ "text": ".sp-flash.is-bad { border-color: #ef4444; color: #fecaca; background: color-mix(in srgb, #ef4444 12%, transparent); }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/spaces.css",
+ "line": 34,
+ "value": "#f87171",
+ "text": ".sp-btn-danger { background: transparent; color: #f87171; border-color: color-mix(in srgb, #f87171 50%, transparent); }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/spaces.css",
+ "line": 34,
+ "value": "#f87171",
+ "text": ".sp-btn-danger { background: transparent; color: #f87171; border-color: color-mix(in srgb, #f87171 50%, transparent); }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/spaces.css",
+ "line": 46,
+ "value": "#4ade80",
+ "text": ".sp-card-badge.is-live { color: #4ade80; border-color: color-mix(in srgb, #4ade80 45%, transparent); }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/spaces.css",
+ "line": 46,
+ "value": "#4ade80",
+ "text": ".sp-card-badge.is-live { color: #4ade80; border-color: color-mix(in srgb, #4ade80 45%, transparent); }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/spaces.css",
+ "line": 59,
+ "value": "#fff",
+ "text": ".sp-template-icon { width: 40px; height: 40px; border-radius: 12px; display: grid; place-items: center; font-size: 18px; color: #fff; background: var(--tpl); }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/spaces.css",
+ "line": 85,
+ "value": "#fff",
+ "text": ".sp-frame { margin: 20px 0 8px; border: 1px solid var(--border, #2c3d5c); border-radius: 16px; overflow: hidden; background: #fff; }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/spaces.css",
+ "line": 114,
+ "value": "#f87171",
+ "text": ".sp-ed-tab .sp-ed-x:hover { opacity: 1; color: #f87171; }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/spaces.css",
+ "line": 121,
+ "value": "#f472b6",
+ "text": ".sp-ed-code .cm-s-default .cm-tag { color: #f472b6; } .sp-ed-code .cm-s-default .cm-attribute { color: #fbbf24; } .sp-ed-code .cm-s-default .cm-string, .sp-ed-c"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/spaces.css",
+ "line": 121,
+ "value": "#fbbf24",
+ "text": ".sp-ed-code .cm-s-default .cm-tag { color: #f472b6; } .sp-ed-code .cm-s-default .cm-attribute { color: #fbbf24; } .sp-ed-code .cm-s-default .cm-string, .sp-ed-c"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/spaces.css",
+ "line": 121,
+ "value": "#4ade80",
+ "text": ".sp-ed-code .cm-s-default .cm-tag { color: #f472b6; } .sp-ed-code .cm-s-default .cm-attribute { color: #fbbf24; } .sp-ed-code .cm-s-default .cm-string, .sp-ed-c"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/spaces.css",
+ "line": 122,
+ "value": "#c084fc",
+ "text": ".sp-ed-code .cm-s-default .cm-keyword { color: #c084fc; } .sp-ed-code .cm-s-default .cm-comment { color: #7b8aa3; font-style: italic; } .sp-ed-code .cm-s-defaul"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/spaces.css",
+ "line": 122,
+ "value": "#7b8aa3",
+ "text": ".sp-ed-code .cm-s-default .cm-keyword { color: #c084fc; } .sp-ed-code .cm-s-default .cm-comment { color: #7b8aa3; font-style: italic; } .sp-ed-code .cm-s-defaul"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/spaces.css",
+ "line": 122,
+ "value": "#fb923c",
+ "text": ".sp-ed-code .cm-s-default .cm-keyword { color: #c084fc; } .sp-ed-code .cm-s-default .cm-comment { color: #7b8aa3; font-style: italic; } .sp-ed-code .cm-s-defaul"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/spaces.css",
+ "line": 123,
+ "value": "#60a5fa",
+ "text": ".sp-ed-code .cm-s-default .cm-def, .sp-ed-code .cm-s-default .cm-property { color: #60a5fa; } .sp-ed-code .cm-s-default .cm-variable { color: var(--text-primary"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/spaces.css",
+ "line": 124,
+ "value": "#93c5fd",
+ "text": ".sp-ed-code .cm-s-default .cm-variable-2, .sp-ed-code .cm-s-default .cm-variable-3, .sp-ed-code .cm-s-default .cm-type { color: #93c5fd; }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/spaces.css",
+ "line": 125,
+ "value": "#fcd34d",
+ "text": ".sp-ed-code .cm-s-default .cm-atom, .sp-ed-code .cm-s-default .cm-qualifier, .sp-ed-code .cm-s-default .cm-builtin { color: #fcd34d; }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/spaces.css",
+ "line": 126,
+ "value": "#a5b4c8",
+ "text": ".sp-ed-code .cm-s-default .cm-meta, .sp-ed-code .cm-s-default .cm-bracket, .sp-ed-code .cm-s-default .cm-operator { color: #a5b4c8; }"
+ }
+ ]
+ },
+ {
+ "repo": "OpenVibe.Surf",
+ "commit": "e605c50166437344f97e78283a39d33bce37636e",
+ "files_scanned": 4,
+ "counts": {
+ "literal_duration": 9,
+ "literal_curve": 5,
+ "spinner": 0,
+ "color_drift": 23,
+ "missing_reduced_motion": 0,
+ "total": 37
+ },
+ "score": 95.35,
+ "findings_total": 37,
+ "findings": [
+ {
+ "rule": "literal_duration",
+ "file": "public/css/surf.css",
+ "line": 45,
+ "value": "1.1s",
+ "text": "html.ov-motion .sf-run-speed i { animation: sf-speed 1.1s linear infinite; animation-delay: calc(var(--i) * -.12s); }"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/surf.css",
+ "line": 55,
+ "value": ".15s",
+ "text": ".sf-tile a { display: grid; gap: 8px; height: 100%; padding: 18px; border-radius: 20px; border: 1px solid var(--border, #1f2d47); background: var(--bg-card, #0f"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/surf.css",
+ "line": 55,
+ "value": ".15s",
+ "text": ".sf-tile a { display: grid; gap: 8px; height: 100%; padding: 18px; border-radius: 20px; border: 1px solid var(--border, #1f2d47); background: var(--bg-card, #0f"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/surf.css",
+ "line": 78,
+ "value": "1.4s",
+ "text": ".sf-demo-steps li.is-now::before { border-color: var(--room-accent); animation: sf-pulse 1.4s ease-out infinite; } /* the step on now pulses out, never spins "
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/css/surf.css",
+ "line": 78,
+ "value": "ease-out",
+ "text": ".sf-demo-steps li.is-now::before { border-color: var(--room-accent); animation: sf-pulse 1.4s ease-out infinite; } /* the step on now pulses out, never spins "
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/surf.css",
+ "line": 89,
+ "value": ".15s",
+ "text": ".sf-map-link { display: grid; grid-template-columns: 84px minmax(0, 1fr); gap: 14px; align-items: center; padding: 12px; border-radius: 18px; border: 1px solid "
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/surf.css",
+ "line": 129,
+ "value": ".28s",
+ "text": ".sf-tv-screen.is-changing { animation: sf-change .28s ease; }"
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/css/surf.css",
+ "line": 129,
+ "value": "ease",
+ "text": ".sf-tv-screen.is-changing { animation: sf-change .28s ease; }"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/surf.css",
+ "line": 160,
+ "value": "2.4s",
+ "text": "html.ov-motion .sf-party-dot { animation: sf-party-pulse 2.4s ease-in-out infinite; }"
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/css/surf.css",
+ "line": 160,
+ "value": "ease-in-out",
+ "text": "html.ov-motion .sf-party-dot { animation: sf-party-pulse 2.4s ease-in-out infinite; }"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/surf.css",
+ "line": 209,
+ "value": "1.6s",
+ "text": "html.ov-motion .sf-party-playing .sf-party-dot { animation: sf-pulse 1.6s ease-out infinite; }"
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/css/surf.css",
+ "line": 209,
+ "value": "ease-out",
+ "text": "html.ov-motion .sf-party-playing .sf-party-dot { animation: sf-pulse 1.6s ease-out infinite; }"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/surf.css",
+ "line": 227,
+ "value": "1.6s",
+ "text": ".sf-desk-page[data-sf-desk-open=\"1\"] .sf-desk-state::before { background: #22c55e; animation: sf-desk-pulse 1.6s ease-out infinite; }"
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/css/surf.css",
+ "line": 227,
+ "value": "ease-out",
+ "text": ".sf-desk-page[data-sf-desk-open=\"1\"] .sf-desk-state::before { background: #22c55e; animation: sf-desk-pulse 1.6s ease-out infinite; }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/surf.css",
+ "line": 47,
+ "value": "rgba(0, 0, 0, .55)",
+ "text": ".sf-run-hud { position: absolute; left: 50%; bottom: 10%; translate: -50% 0; display: grid; justify-items: center; gap: 2px; padding: 8px 16px; border-radius: 1"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/surf.css",
+ "line": 47,
+ "value": "#fff",
+ "text": ".sf-run-hud { position: absolute; left: 50%; bottom: 10%; translate: -50% 0; display: grid; justify-items: center; gap: 2px; padding: 8px 16px; border-radius: 1"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/surf.css",
+ "line": 66,
+ "value": "#0b0f17",
+ "text": ".sf-demo-run { display: grid; gap: 10px; padding: 16px; border-radius: 18px; background: #0b0f17; color: #e9f2ff; border: 1px solid color-mix(in srgb, var(--roo"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/surf.css",
+ "line": 66,
+ "value": "#e9f2ff",
+ "text": ".sf-demo-run { display: grid; gap: 10px; padding: 16px; border-radius: 18px; background: #0b0f17; color: #e9f2ff; border: 1px solid color-mix(in srgb, var(--roo"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/surf.css",
+ "line": 69,
+ "value": "#4ade80",
+ "text": ".sf-demo-hud .is-good { color: #4ade80; font-variant-numeric: tabular-nums; }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/surf.css",
+ "line": 70,
+ "value": "rgba(255, 255, 255, .12)",
+ "text": ".sf-demo-bar { height: 6px; border-radius: 6px; background: rgba(255, 255, 255, .12); overflow: hidden; }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/surf.css",
+ "line": 71,
+ "value": "#00bef6",
+ "text": ".sf-demo-bar i { display: block; height: 100%; background: #00bef6; border-radius: inherit; }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/surf.css",
+ "line": 82,
+ "value": "#fff",
+ "text": ".sf-demo-screen { position: relative; display: flex; align-items: flex-end; gap: 8px; aspect-ratio: 16 / 6; max-height: 210px; padding: 12px; border-radius: 12p"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/surf.css",
+ "line": 83,
+ "value": "#1e293b",
+ "text": "background: radial-gradient(120% 120% at 70% 30%, color-mix(in srgb, var(--room-accent) 45%, transparent), transparent 60%), linear-gradient(160deg, #1e293b, #0"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/surf.css",
+ "line": 83,
+ "value": "#0b1020",
+ "text": "background: radial-gradient(120% 120% at 70% 30%, color-mix(in srgb, var(--room-accent) 45%, transparent), transparent 60%), linear-gradient(160deg, #1e293b, #0"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/surf.css",
+ "line": 85,
+ "value": "#fff",
+ "text": ".sf-pill-live { padding: 1px 7px; border-radius: 5px; background: var(--live-red, #e11d48); color: #fff; font-size: .68rem; font-weight: 800; }"
+ }
+ ]
+ },
+ {
+ "repo": "OpenVibe.Tips",
+ "commit": "eb708f169ed98840d56f248f07f4e5da998c875c",
+ "files_scanned": 4,
+ "counts": {
+ "literal_duration": 3,
+ "literal_curve": 0,
+ "spinner": 0,
+ "color_drift": 9,
+ "missing_reduced_motion": 0,
+ "total": 12
+ },
+ "score": 98.8,
+ "findings_total": 12,
+ "findings": [
+ {
+ "rule": "literal_duration",
+ "file": "public/css/overlay.css",
+ "line": 3,
+ "value": ".35s",
+ "text": "#alert { position: fixed; left: 50%; top: 12%; transform: translateX(-50%); width: min(90vw, 760px); text-align: center; opacity: 0; transition: opacity .35s, t"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/overlay.css",
+ "line": 3,
+ "value": ".35s",
+ "text": "#alert { position: fixed; left: 50%; top: 12%; transform: translateX(-50%); width: min(90vw, 760px); text-align: center; opacity: 0; transition: opacity .35s, t"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/overlay.css",
+ "line": 14,
+ "value": ".6s",
+ "text": ".g .bar span { display: block; height: 100%; background: #3b82f6; transition: width .6s; }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/tips.css",
+ "line": 10,
+ "value": "--tips-bad #f87171",
+ "text": "--tips-bad: #f87171;"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/tips.css",
+ "line": 11,
+ "value": "--tips-good #34d399",
+ "text": "--tips-good: #34d399;"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/tips.css",
+ "line": 40,
+ "value": "rgba(59, 130, 246, .12)",
+ "text": ".notice { background: rgba(59, 130, 246, .12); border: 1px solid rgba(59, 130, 246, .4); border-radius: 8px; padding: 10px 14px; }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/tips.css",
+ "line": 40,
+ "value": "rgba(59, 130, 246, .4)",
+ "text": ".notice { background: rgba(59, 130, 246, .12); border: 1px solid rgba(59, 130, 246, .4); border-radius: 8px; padding: 10px 14px; }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/tips.css",
+ "line": 41,
+ "value": "rgba(248, 113, 113, .12)",
+ "text": ".notice.error { background: rgba(248, 113, 113, .12); border-color: rgba(248, 113, 113, .5); }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/tips.css",
+ "line": 41,
+ "value": "rgba(248, 113, 113, .5)",
+ "text": ".notice.error { background: rgba(248, 113, 113, .12); border-color: rgba(248, 113, 113, .5); }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/tips.css",
+ "line": 112,
+ "value": "#b91c1c",
+ "text": ".tp-live { display: inline-block; padding: 1px 8px; border-radius: 999px; font-size: .72rem; font-weight: 800; letter-spacing: .04em; text-transform: uppercase;"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/tips.css",
+ "line": 112,
+ "value": "#000",
+ "text": ".tp-live { display: inline-block; padding: 1px 8px; border-radius: 999px; font-size: .72rem; font-weight: 800; letter-spacing: .04em; text-transform: uppercase;"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/tips.css",
+ "line": 112,
+ "value": "#fff",
+ "text": ".tp-live { display: inline-block; padding: 1px 8px; border-radius: 999px; font-size: .72rem; font-weight: 800; letter-spacing: .04em; text-transform: uppercase;"
+ }
+ ]
+ },
+ {
+ "repo": "OpenVibe.Today",
+ "commit": "2d77c36fbc70f6aeae7c59645822d5bd48631df6",
+ "files_scanned": 3,
+ "counts": {
+ "literal_duration": 5,
+ "literal_curve": 3,
+ "spinner": 0,
+ "color_drift": 9,
+ "missing_reduced_motion": 0,
+ "total": 17
+ },
+ "score": 97.55,
+ "findings_total": 17,
+ "findings": [
+ {
+ "rule": "literal_duration",
+ "file": "public/css/today.css",
+ "line": 37,
+ "value": "1.4s",
+ "text": ".td-pill.is-live::before { content: \"\"; width: 6px; height: 6px; border-radius: 50%; background: #fff; animation: td-pulse 1.4s ease-in-out infinite; }"
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/css/today.css",
+ "line": 37,
+ "value": "ease-in-out",
+ "text": ".td-pill.is-live::before { content: \"\"; width: 6px; height: 6px; border-radius: 50%; background: #fff; animation: td-pulse 1.4s ease-in-out infinite; }"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/today.css",
+ "line": 48,
+ "value": ".15s",
+ "text": ".td-ev-link { display: flex; align-items: center; gap: 14px; padding: 10px 14px 10px 10px; border-radius: 16px; border: 1px solid var(--border, #1f2d47); backgr"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/today.css",
+ "line": 48,
+ "value": ".15s",
+ "text": ".td-ev-link { display: flex; align-items: center; gap: 14px; padding: 10px 14px 10px 10px; border-radius: 16px; border: 1px solid var(--border, #1f2d47); backgr"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/today.css",
+ "line": 73,
+ "value": ".6s",
+ "text": "html.ov-motion .td-ring-dot { animation: td-dot-in .6s cubic-bezier(.2, .8, .2, 1) both; animation-delay: calc(var(--i) * 90ms + 200ms); }"
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/css/today.css",
+ "line": 73,
+ "value": "cubic-bezier(",
+ "text": "html.ov-motion .td-ring-dot { animation: td-dot-in .6s cubic-bezier(.2, .8, .2, 1) both; animation-delay: calc(var(--i) * 90ms + 200ms); }"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/today.css",
+ "line": 95,
+ "value": "4.5s",
+ "text": "html.ov-motion .td-demo-toast { animation: td-toast 4.5s ease-in-out infinite; }"
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/css/today.css",
+ "line": 95,
+ "value": "ease-in-out",
+ "text": "html.ov-motion .td-demo-toast { animation: td-toast 4.5s ease-in-out infinite; }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/today.css",
+ "line": 36,
+ "value": "#fff",
+ "text": ".td-pill.is-live { background: var(--live-red, #e11d48); color: #fff; }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/today.css",
+ "line": 37,
+ "value": "#fff",
+ "text": ".td-pill.is-live::before { content: \"\"; width: 6px; height: 6px; border-radius: 50%; background: #fff; animation: td-pulse 1.4s ease-in-out infinite; }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/today.css",
+ "line": 92,
+ "value": "rgba(0, 0, 0, .3)",
+ "text": ".td-demo-toast { display: flex; align-items: center; gap: 12px; margin-left: 10%; padding: 12px 14px; border-radius: 16px; background: var(--bg-elevated, var(--"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/today.css",
+ "line": 206,
+ "value": "#f87171",
+ "text": ".td-remind-out.is-bad { color: #f87171; }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/today.css",
+ "line": 252,
+ "value": "--k #22c55e",
+ "text": ".td-wi.is-band { --k: color-mix(in srgb, var(--room-accent) 55%, #22c55e); }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/today.css",
+ "line": 306,
+ "value": "#22c55e",
+ "text": ".td-remind-ok { display: grid; place-items: center; width: 52px; height: 52px; margin-bottom: 10px; border-radius: 16px; background: color-mix(in srgb, #22c55e "
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/today.css",
+ "line": 306,
+ "value": "#22c55e",
+ "text": ".td-remind-ok { display: grid; place-items: center; width: 52px; height: 52px; margin-bottom: 10px; border-radius: 16px; background: color-mix(in srgb, #22c55e "
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/today.css",
+ "line": 308,
+ "value": "#ef4444",
+ "text": ".td-notice--bad { padding: 12px 14px; border-radius: 12px; border: 1px solid color-mix(in srgb, #ef4444 50%, var(--border, #1f2d47)); background: color-mix(in s"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/today.css",
+ "line": 308,
+ "value": "#ef4444",
+ "text": ".td-notice--bad { padding: 12px 14px; border-radius: 12px; border: 1px solid color-mix(in srgb, #ef4444 50%, var(--border, #1f2d47)); background: color-mix(in s"
+ }
+ ]
+ },
+ {
+ "repo": "OpenVibe.Tools",
+ "commit": "1f1ac7afc05d127533887403dd4dbfb6430c3e64",
+ "files_scanned": 69,
+ "counts": {
+ "literal_duration": 157,
+ "literal_curve": 44,
+ "spinner": 0,
+ "color_drift": 399,
+ "missing_reduced_motion": 11,
+ "total": 611
+ },
+ "score": 7.8,
+ "findings_total": 611,
+ "findings": [
+ {
+ "rule": "missing_reduced_motion",
+ "file": "apps/audio/public/css/style.css",
+ "line": 213,
+ "value": "animation",
+ "text": ".option-group { animation: fadeIn .25s ease; }"
+ },
+ {
+ "rule": "missing_reduced_motion",
+ "file": "apps/docs/public/css/style.css",
+ "line": 342,
+ "value": "animation",
+ "text": "animation: spin 1.4s ease-out infinite;"
+ },
+ {
+ "rule": "missing_reduced_motion",
+ "file": "apps/food/public/css/style.css",
+ "line": 212,
+ "value": "animation",
+ "text": ".spinner { width:24px; height:24px; border: 3px solid var(--green); --spin-c: var(--green); border-radius:50%; animation: spin 1.4s ease-out infinite; margin:0 "
+ },
+ {
+ "rule": "missing_reduced_motion",
+ "file": "apps/gateway/public/audio.html",
+ "line": 67,
+ "value": "animation",
+ "text": ".showcase-tool { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 20px; border-radius: 16px; background: var(--bg-c"
+ },
+ {
+ "rule": "missing_reduced_motion",
+ "file": "apps/gateway/public/dev.html",
+ "line": 35,
+ "value": "animation",
+ "text": ".bg-glow::before,.bg-glow::after{content:'';position:absolute;border-radius:50%;filter:blur(120px);opacity:.35;animation:gd 20s ease-in-out infinite alternate}"
+ },
+ {
+ "rule": "missing_reduced_motion",
+ "file": "apps/gateway/public/img.html",
+ "line": 67,
+ "value": "animation",
+ "text": ".showcase-tool { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 20px; border-radius: 16px; background: var(--bg-c"
+ },
+ {
+ "rule": "missing_reduced_motion",
+ "file": "apps/gateway/public/net.html",
+ "line": 74,
+ "value": "animation",
+ "text": "animation: glow-drift 20s ease-in-out infinite alternate;"
+ },
+ {
+ "rule": "missing_reduced_motion",
+ "file": "apps/img/public/css/style.css",
+ "line": 337,
+ "value": "animation",
+ "text": "animation: spin 1.4s ease-out infinite;"
+ },
+ {
+ "rule": "missing_reduced_motion",
+ "file": "apps/maps/public/css/style.css",
+ "line": 48,
+ "value": "animation",
+ "text": ".splash-content { text-align: center; animation: fadeIn 0.5s ease; }"
+ },
+ {
+ "rule": "missing_reduced_motion",
+ "file": "apps/text/public/css/shared.css",
+ "line": 305,
+ "value": "animation",
+ "text": "animation: livePulse 2s ease-in-out infinite;"
+ },
+ {
+ "rule": "missing_reduced_motion",
+ "file": "apps/yt/public/css/style.css",
+ "line": 172,
+ "value": "animation",
+ "text": "animation: spin 1.4s ease-out infinite;"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "apps/gateway/public/codes.html",
+ "line": 56,
+ "value": ".15s",
+ "text": ".cx-tabs a{display:inline-flex;align-items:center;gap:6px;flex:none;padding:7px 12px;border-radius:9px;color:var(--c-tx2);text-decoration:none;font-size:.86rem;"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "apps/text/public/badge.html",
+ "line": 38,
+ "value": ".2s",
+ "text": ".shape-pill { padding: .4rem .9rem; border-radius: 20px; border: 1px solid var(--border); background: var(--bg-tertiary); color: var(--text-secondary); font-siz"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "apps/text/public/ice.html",
+ "line": 44,
+ "value": "0.2s",
+ "text": "transition: border-color 0.2s, transform 0.2s;"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "apps/text/public/peso.html",
+ "line": 68,
+ "value": "0.3s",
+ "text": "transition: transform 0.3s, background 0.2s;"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "apps/text/public/slang.html",
+ "line": 46,
+ "value": "0.2s",
+ "text": "font-weight: 600; transition: all 0.2s; font-family: 'Inter', sans-serif;"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "apps/text/public/spanish.html",
+ "line": 34,
+ "value": "0.2s",
+ "text": "transition: border-color 0.2s;"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "apps/text/public/thumbnail.html",
+ "line": 40,
+ "value": ".2s",
+ "text": ".upload-btn { width: 100%; padding: .75rem; border-radius: 12px; border: 2px dashed var(--border); background: var(--bg-tertiary); color: var(--text-secondary);"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "apps/text/public/title.html",
+ "line": 36,
+ "value": ".2s",
+ "text": ".preset-btn { padding: .5rem .25rem; font-size: .7rem; border-radius: 10px; cursor: pointer; border: 2px solid var(--border); background: var(--bg-tertiary); co"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "apps/text/public/watermark.html",
+ "line": 29,
+ "value": ".2s",
+ "text": ".drop-zone { border: 2px dashed var(--border); border-radius: var(--radius); padding: 2rem; text-align: center; cursor: pointer; transition: all .2s; }"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "apps/text/public/wordmark.html",
+ "line": 25,
+ "value": ".2s",
+ "text": ".wm-card { background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; cursor: pointer; transition: all .2s; }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "apps/text/public/compare.html",
+ "line": 28,
+ "value": "rgba(34,197,94,0.15)",
+ "text": ".diff-add { background: rgba(34,197,94,0.15); color: #4ade80; text-decoration: none; }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "apps/text/public/json.html",
+ "line": 29,
+ "value": "#93c5fd",
+ "text": ".json-key { color: #93c5fd; }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "apps/text/public/transparent.html",
+ "line": 25,
+ "value": "#141414",
+ "text": ".canvas-wrap { background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius); padding: 2rem; text-align: center; margin: 2rem 0; bac"
+ },
+ {
+ "rule": "color_drift",
+ "file": "apps/text/public/unicode.html",
+ "line": 30,
+ "value": "rgba(255, 0, 0, 0.1)",
+ "text": ".char-invisible { background: rgba(255, 0, 0, 0.1); }"
+ }
+ ]
+ },
+ {
+ "repo": "OpenVibe.Tours",
+ "commit": "c04d2bc4e070188f55ede2a11c367c7c0003e7c1",
+ "files_scanned": 4,
+ "counts": {
+ "literal_duration": 0,
+ "literal_curve": 0,
+ "spinner": 0,
+ "color_drift": 13,
+ "missing_reduced_motion": 0,
+ "total": 13
+ },
+ "score": 99.35,
+ "findings_total": 13,
+ "findings": [
+ {
+ "rule": "color_drift",
+ "file": "public/css/tours.css",
+ "line": 32,
+ "value": "#0a0f1c",
+ "text": ".tr-pin-n { fill: #0a0f1c; font-size: 9px; font-weight: 800; }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/tours.css",
+ "line": 33,
+ "value": "rgba(0,0,0,.4)",
+ "text": ".tr-walk { filter: drop-shadow(0 2px 8px rgba(0,0,0,.4)); }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/tours.css",
+ "line": 50,
+ "value": "#b44444",
+ "text": ".tr-note-error { border-color: #b44444; color: #ffb4b4; }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/tours.css",
+ "line": 50,
+ "value": "#ffb4b4",
+ "text": ".tr-note-error { border-color: #b44444; color: #ffb4b4; }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/tours.css",
+ "line": 51,
+ "value": "#8a6a1f",
+ "text": ".tr-note-warn { border-color: #8a6a1f; color: #ffd98a; }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/tours.css",
+ "line": 51,
+ "value": "#ffd98a",
+ "text": ".tr-note-warn { border-color: #8a6a1f; color: #ffd98a; }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/tours.css",
+ "line": 81,
+ "value": "#ef4444",
+ "text": ".tr-live-card img, .tr-live-noimg { width: 100%; aspect-ratio: 16/9; object-fit: cover; border-radius: 10px; background: var(--bg-primary, #0a0f1c); display: gr"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/tours.css",
+ "line": 82,
+ "value": "#ef4444",
+ "text": ".tr-live-live { position: absolute; top: 16px; left: 16px; padding: 2px 8px; border-radius: 999px; background: #ef4444; color: #fff; font-size: .7rem; font-weig"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/tours.css",
+ "line": 82,
+ "value": "#fff",
+ "text": ".tr-live-live { position: absolute; top: 16px; left: 16px; padding: 2px 8px; border-radius: 999px; background: #ef4444; color: #fff; font-size: .7rem; font-weig"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/tours.css",
+ "line": 83,
+ "value": "#ef4444",
+ "text": ".tr-live-dot { display: inline-block; width: 9px; height: 9px; border-radius: 50%; background: #ef4444; }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/tours.css",
+ "line": 93,
+ "value": "#0a0f1c",
+ "text": ".tr-map-pin { width: 26px; height: 26px; border-radius: 50%; display: grid; place-items: center; background: var(--room-accent, #F98942); color: #0a0f1c; font-w"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/tours.css",
+ "line": 93,
+ "value": "#0a0f1c",
+ "text": ".tr-map-pin { width: 26px; height: 26px; border-radius: 50%; display: grid; place-items: center; background: var(--room-accent, #F98942); color: #0a0f1c; font-w"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/tours.css",
+ "line": 96,
+ "value": "#0a0f1c",
+ "text": ".tr-stop-num { width: 30px; height: 30px; border-radius: 50%; display: grid; place-items: center; background: var(--room-accent, #F98942); color: #0a0f1c; font-"
+ }
+ ]
+ },
+ {
+ "repo": "OpenVibe.Trade",
+ "commit": "48bcb670dd00d6206e35ae6e81a9263cd0710f64",
+ "files_scanned": 1,
+ "counts": {
+ "literal_duration": 1,
+ "literal_curve": 0,
+ "spinner": 0,
+ "color_drift": 1,
+ "missing_reduced_motion": 0,
+ "total": 2
+ },
+ "score": 99.7,
+ "findings_total": 2,
+ "findings": [
+ {
+ "rule": "literal_duration",
+ "file": "public/css/trade.css",
+ "line": 78,
+ "value": ".15s",
+ "text": ".tr-stats a { display: flex; flex-direction: column; gap: 2px; height: 100%; padding: 14px 16px; border-radius: 14px; border: 1px solid var(--border, #1f2d47); "
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/trade.css",
+ "line": 30,
+ "value": "rgba(245, 158, 11, .07)",
+ "text": "tr.is-stale td, tr.is-stale th { background: rgba(245, 158, 11, .07); }"
+ }
+ ]
+ },
+ {
+ "repo": "OpenVibe.VIP",
+ "commit": "daab64a5fbe7c4706bcd82f073b5d20cc3f48a34",
+ "files_scanned": 2,
+ "counts": {
+ "literal_duration": 1,
+ "literal_curve": 0,
+ "spinner": 0,
+ "color_drift": 8,
+ "missing_reduced_motion": 0,
+ "total": 9
+ },
+ "score": 99.35,
+ "findings_total": 9,
+ "findings": [
+ {
+ "rule": "literal_duration",
+ "file": "public/css/vip.css",
+ "line": 55,
+ "value": ".15s",
+ "text": ".creator-list a { display: flex; align-items: center; gap: 12px; height: 100%; background: var(--vip-card); border: 1px solid var(--vip-border); border-radius: "
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/vip.css",
+ "line": 13,
+ "value": "--vip-bad #f87171",
+ "text": "--vip-bad: #f87171;"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/vip.css",
+ "line": 14,
+ "value": "--vip-good #34d399",
+ "text": "--vip-good: #34d399;"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/vip.css",
+ "line": 49,
+ "value": "rgba(59, 130, 246, .12)",
+ "text": ".notice { background: rgba(59, 130, 246, .12); border: 1px solid rgba(59, 130, 246, .4); border-radius: 8px; padding: 10px 14px; }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/vip.css",
+ "line": 49,
+ "value": "rgba(59, 130, 246, .4)",
+ "text": ".notice { background: rgba(59, 130, 246, .12); border: 1px solid rgba(59, 130, 246, .4); border-radius: 8px; padding: 10px 14px; }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/vip.css",
+ "line": 50,
+ "value": "rgba(248, 113, 113, .12)",
+ "text": ".notice-error { background: rgba(248, 113, 113, .12); border-color: rgba(248, 113, 113, .5); }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/vip.css",
+ "line": 50,
+ "value": "rgba(248, 113, 113, .5)",
+ "text": ".notice-error { background: rgba(248, 113, 113, .12); border-color: rgba(248, 113, 113, .5); }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/vip.css",
+ "line": 63,
+ "value": "rgba(255, 255, 255, .06)",
+ "text": "code { background: rgba(255, 255, 255, .06); padding: 1px 5px; border-radius: 4px; }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/vip.css",
+ "line": 79,
+ "value": "#1a0505",
+ "text": ".vip-live-pill { flex: none; display: inline-block; border-radius: 999px; padding: 0 8px; font-size: .75rem; font-weight: 700; letter-spacing: .04em; text-trans"
+ }
+ ]
+ },
+ {
+ "repo": "OpenVibe.Vision",
+ "commit": "45060caa486acaad2b9bd97dc1fac2cc4786e2c1",
+ "files_scanned": 4,
+ "counts": {
+ "literal_duration": 11,
+ "literal_curve": 5,
+ "spinner": 0,
+ "color_drift": 7,
+ "missing_reduced_motion": 0,
+ "total": 23
+ },
+ "score": 95.65,
+ "findings_total": 23,
+ "findings": [
+ {
+ "rule": "literal_duration",
+ "file": "public/css/vision.css",
+ "line": 61,
+ "value": "1.8s",
+ "text": "html.ov-motion .vi-race-img path { stroke-dasharray: 420; animation: vi-draw 1.8s cubic-bezier(.2,.7,.2,1) both; }"
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/css/vision.css",
+ "line": 61,
+ "value": "cubic-bezier(",
+ "text": "html.ov-motion .vi-race-img path { stroke-dasharray: 420; animation: vi-draw 1.8s cubic-bezier(.2,.7,.2,1) both; }"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/vision.css",
+ "line": 68,
+ "value": ".6s",
+ "text": "html.ov-motion .vi-race-box { animation: vi-pop .6s cubic-bezier(.2,.9,.3,1.2) both; } html.ov-motion .vi-race-b { animation-delay: .35s; }"
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/css/vision.css",
+ "line": 68,
+ "value": "cubic-bezier(",
+ "text": "html.ov-motion .vi-race-box { animation: vi-pop .6s cubic-bezier(.2,.9,.3,1.2) both; } html.ov-motion .vi-race-b { animation-delay: .35s; }"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/vision.css",
+ "line": 108,
+ "value": ".15s",
+ "text": ".vi-task { display: grid; gap: 6px; height: 100%; padding: 16px 18px; border-radius: 18px; border: 1px solid var(--border, #1f2d47); background: var(--bg-card, "
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/vision.css",
+ "line": 108,
+ "value": ".15s",
+ "text": ".vi-task { display: grid; gap: 6px; height: 100%; padding: 16px 18px; border-radius: 18px; border: 1px solid var(--border, #1f2d47); background: var(--bg-card, "
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/vision.css",
+ "line": 122,
+ "value": ".2s",
+ "text": ".vi-demo-switch::after { content: ''; position: absolute; top: 3px; left: 3px; width: 16px; height: 16px; border-radius: 50%; background: var(--text-muted, #738"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/vision.css",
+ "line": 129,
+ "value": ".2s",
+ "text": ".vi-check > input[role=switch] { appearance: none; -webkit-appearance: none; position: relative; flex: none; width: 44px; height: 26px; margin: 0; border-radius"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/vision.css",
+ "line": 130,
+ "value": ".2s",
+ "text": ".vi-check > input[role=switch]::after { content: ''; position: absolute; top: 3px; left: 3px; width: 20px; height: 20px; border-radius: 50%; background: var(--t"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/vision.css",
+ "line": 130,
+ "value": ".2s",
+ "text": ".vi-check > input[role=switch]::after { content: ''; position: absolute; top: 3px; left: 3px; width: 20px; height: 20px; border-radius: 50%; background: var(--t"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/vision.css",
+ "line": 144,
+ "value": ".15s",
+ "text": ".vi-drop { position: relative; display: grid; justify-items: center; align-content: center; gap: 6px; min-height: 170px; padding: 24px 20px; border: 2px dashed "
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/vision.css",
+ "line": 144,
+ "value": ".15s",
+ "text": ".vi-drop { position: relative; display: grid; justify-items: center; align-content: center; gap: 6px; min-height: 170px; padding: 24px 20px; border: 2px dashed "
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/css/vision.css",
+ "line": 144,
+ "value": "ease",
+ "text": ".vi-drop { position: relative; display: grid; justify-items: center; align-content: center; gap: 6px; min-height: 170px; padding: 24px 20px; border: 2px dashed "
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/css/vision.css",
+ "line": 144,
+ "value": "ease",
+ "text": ".vi-drop { position: relative; display: grid; justify-items: center; align-content: center; gap: 6px; min-height: 170px; padding: 24px 20px; border: 2px dashed "
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/vision.css",
+ "line": 258,
+ "value": ".15s",
+ "text": ".vi-options summary::after, .vi-disclose summary::after, .vi-api summary::after { content: ''; flex: none; width: 8px; height: 8px; margin-left: auto; border-ri"
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/css/vision.css",
+ "line": 258,
+ "value": "ease",
+ "text": ".vi-options summary::after, .vi-disclose summary::after, .vi-api summary::after { content: ''; flex: none; width: 8px; height: 8px; margin-left: auto; border-ri"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/vision.css",
+ "line": 73,
+ "value": "#fbfaf6",
+ "text": ".vi-demo-receipt { display: grid; gap: 4px; width: min(100%, 300px); margin: 0 auto; padding: 18px 20px; border-radius: 6px; background: #fbfaf6; color: #222; f"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/vision.css",
+ "line": 73,
+ "value": "#222",
+ "text": ".vi-demo-receipt { display: grid; gap: 4px; width: min(100%, 300px); margin: 0 auto; padding: 18px 20px; border-radius: 6px; background: #fbfaf6; color: #222; f"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/vision.css",
+ "line": 73,
+ "value": "rgba(0, 0, 0, .25)",
+ "text": ".vi-demo-receipt { display: grid; gap: 4px; width: min(100%, 300px); margin: 0 auto; padding: 18px 20px; border-radius: 6px; background: #fbfaf6; color: #222; f"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/vision.css",
+ "line": 74,
+ "value": "#999",
+ "text": ".vi-demo-receipt strong { margin-top: 6px; padding-top: 6px; border-top: 1px dashed #999; font-size: 1.05rem; }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/vision.css",
+ "line": 75,
+ "value": "#6b6b6b",
+ "text": ".vi-demo-receipt small { color: #6b6b6b; font-size: .74rem; }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/vision.css",
+ "line": 76,
+ "value": "#fff",
+ "text": ".vi-demo-kicker { justify-self: start; padding: 2px 8px; border-radius: 6px; font: 800 .68rem/1.4 Inter, sans-serif; letter-spacing: .08em; color: #fff; backgro"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/vision.css",
+ "line": 76,
+ "value": "#6b4ab0",
+ "text": ".vi-demo-kicker { justify-self: start; padding: 2px 8px; border-radius: 6px; font: 800 .68rem/1.4 Inter, sans-serif; letter-spacing: .08em; color: #fff; backgro"
+ }
+ ]
+ },
+ {
+ "repo": "OpenVibe.Watch",
+ "commit": "eb5557e97c5075c757946d2c60a5551541b13f11",
+ "files_scanned": 2,
+ "counts": {
+ "literal_duration": 1,
+ "literal_curve": 0,
+ "spinner": 0,
+ "color_drift": 0,
+ "missing_reduced_motion": 0,
+ "total": 1
+ },
+ "score": 99.75,
+ "findings_total": 1,
+ "findings": [
+ {
+ "rule": "literal_duration",
+ "file": "public/css/home.css",
+ "line": 13,
+ "value": ".15s",
+ "text": ".hm-card { display: flex; flex-direction: column; justify-content: space-between; gap: 10px; min-width: 0; padding: 14px 16px; border-radius: 16px; border: 1px "
+ }
+ ]
+ },
+ {
+ "repo": "OpenVibe.Wiki",
+ "commit": "ee24a8beabde07168ad0f8cf36f2b6fd8ab2df78",
+ "files_scanned": 2,
+ "counts": {
+ "literal_duration": 6,
+ "literal_curve": 3,
+ "spinner": 0,
+ "color_drift": 7,
+ "missing_reduced_motion": 1,
+ "total": 17
+ },
+ "score": 95.4,
+ "findings_total": 17,
+ "findings": [
+ {
+ "rule": "missing_reduced_motion",
+ "file": "public/css/vibe.css",
+ "line": 31,
+ "value": "animation",
+ "text": ".wk-vp-steps .wk-vp-step-now { border-color: var(--wk-accent); color: var(--wk-text); background: color-mix(in srgb, var(--wk-accent) 12%, transparent); animati"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/vibe.css",
+ "line": 14,
+ "value": ".15s",
+ "text": ".wk-vibe-sizes span { display: block; height: 100%; padding: 10px 12px; border: 1px solid var(--wk-line); border-radius: 12px; background: var(--wk-panel); tran"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/vibe.css",
+ "line": 14,
+ "value": ".15s",
+ "text": ".wk-vibe-sizes span { display: block; height: 100%; padding: 10px 12px; border: 1px solid var(--wk-line); border-radius: 12px; background: var(--wk-panel); tran"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/vibe.css",
+ "line": 31,
+ "value": "1.6s",
+ "text": ".wk-vp-steps .wk-vp-step-now { border-color: var(--wk-accent); color: var(--wk-text); background: color-mix(in srgb, var(--wk-accent) 12%, transparent); animati"
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/css/vibe.css",
+ "line": 31,
+ "value": "ease-in-out",
+ "text": ".wk-vp-steps .wk-vp-step-now { border-color: var(--wk-accent); color: var(--wk-text); background: color-mix(in srgb, var(--wk-accent) 12%, transparent); animati"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/vibe.css",
+ "line": 36,
+ "value": ".2s",
+ "text": ".wk-vp-pages li { display: flex; flex-direction: column; gap: 2px; padding: 10px 12px; border: 1px solid var(--wk-line); border-radius: 12px; background: var(--"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/vibe.css",
+ "line": 42,
+ "value": "1s",
+ "text": ".wk-vp-pages .wk-vp-writing small::before { content: \"\"; display: inline-block; width: 8px; height: 8px; margin-right: 6px; border-radius: 50%; background: var("
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/css/vibe.css",
+ "line": 42,
+ "value": "ease-in-out",
+ "text": ".wk-vp-pages .wk-vp-writing small::before { content: \"\"; display: inline-block; width: 8px; height: 8px; margin-right: 6px; border-radius: 50%; background: var("
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/vibe.css",
+ "line": 45,
+ "value": "1.2s",
+ "text": ".wk-vp-dot { width: 10px; height: 10px; border-radius: 50%; background: var(--wk-accent); animation: wk-pulse 1.2s ease-in-out infinite; }"
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/css/vibe.css",
+ "line": 45,
+ "value": "ease-in-out",
+ "text": ".wk-vp-dot { width: 10px; height: 10px; border-radius: 50%; background: var(--wk-accent); animation: wk-pulse 1.2s ease-in-out infinite; }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/vibe.css",
+ "line": 18,
+ "value": "#f472b6",
+ "text": ".wk-vibe h1 span { background: linear-gradient(90deg, var(--wk-accent), color-mix(in srgb, var(--wk-accent) 40%, #f472b6)); -webkit-background-clip: text; backg"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/wiki.css",
+ "line": 10,
+ "value": "--wk-red #f87171",
+ "text": "--wk-red: #f87171;"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/wiki.css",
+ "line": 11,
+ "value": "--wk-ok #34d399",
+ "text": "--wk-ok: #34d399;"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/wiki.css",
+ "line": 12,
+ "value": "--wk-warn #fbbf24",
+ "text": "--wk-warn: #fbbf24;"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/wiki.css",
+ "line": 35,
+ "value": "#a78bfa",
+ "text": ".wk-notice.wk-ai { border-color: #a78bfa; }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/wiki.css",
+ "line": 56,
+ "value": "rgba(52, 211, 153, .22)",
+ "text": ".ov-diff ins { background: rgba(52, 211, 153, .22); text-decoration: underline; }"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/wiki.css",
+ "line": 57,
+ "value": "rgba(248, 113, 113, .22)",
+ "text": ".ov-diff del { background: rgba(248, 113, 113, .22); }"
+ }
+ ]
+ },
+ {
+ "repo": "OpenVibe.Work",
+ "commit": "44cba11abad694b8bd895aa2275675760d51f3e8",
+ "files_scanned": 3,
+ "counts": {
+ "literal_duration": 4,
+ "literal_curve": 2,
+ "spinner": 0,
+ "color_drift": 3,
+ "missing_reduced_motion": 0,
+ "total": 9
+ },
+ "score": 98.35,
+ "findings_total": 9,
+ "findings": [
+ {
+ "rule": "literal_duration",
+ "file": "public/css/app.css",
+ "line": 62,
+ "value": ".15s",
+ "text": ".job-item { display: grid; grid-template-columns: 44px minmax(0, 1fr); gap: 14px; padding: 14px 16px; border-radius: 14px; border: 1px solid var(--border, #1f2d"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/app.css",
+ "line": 62,
+ "value": ".15s",
+ "text": ".job-item { display: grid; grid-template-columns: 44px minmax(0, 1fr); gap: 14px; padding: 14px 16px; border-radius: 14px; border: 1px solid var(--border, #1f2d"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/home.css",
+ "line": 11,
+ "value": ".15s",
+ "text": ".home-cards a { display: flex; flex-direction: column; gap: 3px; height: 100%; box-sizing: border-box; padding: 12px 14px; border-radius: 12px; border: 1px soli"
+ },
+ {
+ "rule": "literal_duration",
+ "file": "public/css/home.css",
+ "line": 11,
+ "value": ".15s",
+ "text": ".home-cards a { display: flex; flex-direction: column; gap: 3px; height: 100%; box-sizing: border-box; padding: 12px 14px; border-radius: 12px; border: 1px soli"
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/css/home.css",
+ "line": 11,
+ "value": "ease",
+ "text": ".home-cards a { display: flex; flex-direction: column; gap: 3px; height: 100%; box-sizing: border-box; padding: 12px 14px; border-radius: 12px; border: 1px soli"
+ },
+ {
+ "rule": "literal_curve",
+ "file": "public/css/home.css",
+ "line": 11,
+ "value": "ease",
+ "text": ".home-cards a { display: flex; flex-direction: column; gap: 3px; height: 100%; box-sizing: border-box; padding: 12px 14px; border-radius: 12px; border: 1px soli"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/app.css",
+ "line": 64,
+ "value": "hsl(var(--h)",
+ "text": ".job-logo { width: 44px; height: 44px; border-radius: 12px; display: grid; place-items: center; font-weight: 800; font-size: 1.1rem; color: hsl(var(--h) 80% 78%"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/app.css",
+ "line": 64,
+ "value": "hsl(var(--h)",
+ "text": ".job-logo { width: 44px; height: 44px; border-radius: 12px; display: grid; place-items: center; font-weight: 800; font-size: 1.1rem; color: hsl(var(--h) 80% 78%"
+ },
+ {
+ "rule": "color_drift",
+ "file": "public/css/app.css",
+ "line": 64,
+ "value": "hsl(var(--h)",
+ "text": ".job-logo { width: 44px; height: 44px; border-radius: 12px; display: grid; place-items: center; font-weight: 800; font-size: 1.1rem; color: hsl(var(--h) 80% 78%"
+ }
+ ]
+ },
+ {
+ "repo": "OpenVibe.Zone",
+ "commit": "6ac0483c6338c55ecc8bfe355a5263d276c0e9a2",
+ "files_scanned": 1,
+ "counts": {
+ "literal_duration": 0,
+ "literal_curve": 0,
+ "spinner": 0,
+ "color_drift": 0,
+ "missing_reduced_motion": 0,
+ "total": 0
+ },
+ "score": 100,
+ "findings_total": 0,
+ "findings": []
+ }
+ ]
+}
@@ -9,6 +9,7 @@
@@ -9,6 +9,7 @@
* GET /api/v1/rundown/t/:slug
* GET /api/v1/rundown/t/:slug
* GET /api/v1/after-show/:streamId
* GET /api/v1/after-show/:streamId
* GET /api/v1/c/:username
* GET /api/v1/c/:username
+ * GET /api/v1/design-health, /api/v1/design-health/:site every site's design health (a snapshot, server/design-health/)
*
*
* A bad parameter is checked here, before the source is asked, and is a 404 problem+json, exactly like a source
* A bad parameter is checked here, before the source is asked, and is a 404 problem+json, exactly like a source
* that answered nothing. Every /api/v1 answer is CORS-open and public for two minutes; the app mounts the 120/min
* that answered nothing. Every /api/v1 answer is CORS-open and public for two minutes; the app mounts the 120/min
@@ -24,6 +25,7 @@ const { normaliseDays, isSlug, isStreamId, isUsername, streamIdOf } = require('.
@@ -24,6 +25,7 @@ const { normaliseDays, isSlug, isStreamId, isUsername, streamIdOf } = require('.
const { createServiceAuth, CAPS } = require('../auth');
const { createServiceAuth, CAPS } = require('../auth');
const { GROUPS: AI_USAGE_GROUPS, PERSON_GROUPS } = require('../analytics/ai-usage');
const { GROUPS: AI_USAGE_GROUPS, PERSON_GROUPS } = require('../analytics/ai-usage');
const GROUPS = Object.keys(AI_USAGE_GROUPS);
const GROUPS = Object.keys(AI_USAGE_GROUPS);
+const designHealth = require('../data/design-health');
const CACHE = 'public, max-age=120';
const CACHE = 'public, max-age=120';
@@ -69,6 +71,15 @@ function createApi({ network, live, analytics = null, aiUsage = null, config = {
@@ -69,6 +71,15 @@ function createApi({ network, live, analytics = null, aiUsage = null, config = {
return res.json(stats);
return res.json(stats);
});
});
+ // Every site's design health (plan L45 step 6): the snapshot scripts/design-health.js wrote from each site's code.
+ r.get('/design-health', (req, res) => res.json(designHealth.all()));
+ r.get('/design-health/:site', (req, res) => {
+ const site = designHealth.site(req.params.site);
+ if (!site) return notFound(req, res, 'report.design_site_not_found', 'No site by that name in the design health snapshot.');
+ const { generated_at, rules, weights, kept } = designHealth.all();
+ return res.json({ generated_at, rules, weights, kept, site });
+ });
+
r.get('/rundown', async (req, res) => {
r.get('/rundown', async (req, res) => {
const rundown = await live.rundown();
const rundown = await live.rundown();
if (!rundown) return notFound(req, res, 'report.rundown_not_found', 'OpenVibe.Live did not answer with The Rundown.');
if (!rundown) return notFound(req, res, 'report.rundown_not_found', 'OpenVibe.Live did not answer with The Rundown.');
@@ -4,7 +4,7 @@
@@ -4,7 +4,7 @@
* Crawl artifacts for openvibe.report, built with openvibe-shared/seo:
* Crawl artifacts for openvibe.report, built with openvibe-shared/seo:
*
*
* GET /robots.txt the public pages are crawlable; the API is not
* GET /robots.txt the public pages are crawlable; the API is not
- * GET /sitemap.xml /, /stats, /rundown and every /rundown/t/<slug> from the Rundown's topics
+ * GET /sitemap.xml /, /stats, /design, /rundown and every /rundown/t/<slug> from the Rundown's topics
* GET /llms.txt a markdown map of the site and its machine-readable answers
* GET /llms.txt a markdown map of the site and its machine-readable answers
*
*
* The sitemap reads topics through the same cached client as the pages, so a crawler is no heavier than a reader
* The sitemap reads topics through the same cached client as the pages, so a crawler is no heavier than a reader
@@ -53,6 +53,7 @@ function createDiscoveryRoutes({ config, live, bench }) {
@@ -53,6 +53,7 @@ function createDiscoveryRoutes({ config, live, bench }) {
const pages = [
const pages = [
{ loc: abs('/'), changefreq: 'daily', priority: 1.0 },
{ loc: abs('/'), changefreq: 'daily', priority: 1.0 },
{ loc: abs('/stats'), changefreq: 'daily', priority: 0.9 },
{ loc: abs('/stats'), changefreq: 'daily', priority: 0.9 },
+ { loc: abs('/design'), changefreq: 'weekly', priority: 0.6 },
{ loc: abs('/rundown'), changefreq: 'daily', priority: 0.9 },
{ loc: abs('/rundown'), changefreq: 'daily', priority: 0.9 },
{ loc: abs('/ai'), changefreq: 'daily', priority: 0.9 },
{ loc: abs('/ai'), changefreq: 'daily', priority: 0.9 },
...kinds.filter((k) => k && bench.isKindId(k.id)).map((k) => ({
...kinds.filter((k) => k && bench.isKindId(k.id)).map((k) => ({
@@ -76,11 +77,13 @@ function createDiscoveryRoutes({ config, live, bench }) {
@@ -76,11 +77,13 @@ function createDiscoveryRoutes({ config, live, bench }) {
{ title: 'Start here', links: [
{ title: 'Start here', links: [
{ title: 'OpenVibe.Report', url: abs('/'), note: 'the Report room' },
{ title: 'OpenVibe.Report', url: abs('/'), note: 'the Report room' },
{ title: 'Open numbers', url: abs('/stats'), note: 'page views, accounts and traffic for every OpenVibe site' },
{ title: 'Open numbers', url: abs('/stats'), note: 'page views, accounts and traffic for every OpenVibe site' },
+ { title: 'Design health', url: abs('/design'), note: 'how closely each site follows the shared design kit, read from its code; per site at /design/<site>' },
{ title: 'The Rundown', url: abs('/rundown'), note: 'what streamers talked about, topic by topic' },
{ title: 'The Rundown', url: abs('/rundown'), note: 'what streamers talked about, topic by topic' },
{ title: 'AI boards', url: abs('/ai'), note: 'Bench ratings, quality and cost across rooms' },
{ title: 'AI boards', url: abs('/ai'), note: 'Bench ratings, quality and cost across rooms' },
] },
] },
{ title: 'Machine-readable', links: [
{ title: 'Machine-readable', links: [
{ title: 'Open stats (JSON)', url: abs('/api/v1/stats?days=7'), note: '?days=7, 30 or 90' },
{ title: 'Open stats (JSON)', url: abs('/api/v1/stats?days=7'), note: '?days=7, 30 or 90' },
+ { title: 'Design health (JSON)', url: abs('/api/v1/design-health'), note: 'every site; /api/v1/design-health/<site> for one' },
{ title: 'The Rundown (JSON)', url: abs('/api/v1/rundown') },
{ title: 'The Rundown (JSON)', url: abs('/api/v1/rundown') },
{ title: 'Topics (JSON)', url: abs('/api/v1/rundown/topics') },
{ title: 'Topics (JSON)', url: abs('/api/v1/rundown/topics') },
{ title: 'Sitemap', url: abs('/sitemap.xml') },
{ title: 'Sitemap', url: abs('/sitemap.xml') },
@@ -22,6 +22,8 @@ const express = require('express');
@@ -22,6 +22,8 @@ const express = require('express');
const views = require('../views');
const views = require('../views');
const { normaliseDays, isSlug, isStreamId, isUsername, streamIdOf } = require('../data/source');
const { normaliseDays, isSlug, isStreamId, isUsername, streamIdOf } = require('../data/source');
const { twinPath } = require('../agent-pages');
const { twinPath } = require('../agent-pages');
+const designViews = require('../views/design');
+const designHealth = require('../data/design-health');
const CACHE = 'public, max-age=120';
const CACHE = 'public, max-age=120';
@@ -92,6 +94,14 @@ function createPageRoutes(ctx) {
@@ -92,6 +94,14 @@ function createPageRoutes(ctx) {
r.get('/developers', (req, res) => send(req, res, publicPages.developers().html));
r.get('/developers', (req, res) => send(req, res, publicPages.developers().html));
+ // Every site's design health (plan L45 step 6), from the snapshot (server/data/design-health.js).
+ r.get('/design', (req, res) => send(req, res, designViews.designHealth(designHealth.all())));
+ r.get('/design/:site', (req, res) => {
+ const site = designHealth.site(req.params.site);
+ if (!site) return notFound(req, res);
+ return send(req, res, designViews.designSite(designHealth.all(), site));
+ });
+
r.get('/stats', async (req, res) => {
r.get('/stats', async (req, res) => {
const page = await publicPages.stats(normaliseDays(req.query.days));
const page = await publicPages.stats(normaliseDays(req.query.days));
if (!page) return notFound(req, res);
if (!page) return notFound(req, res);
new file mode 100644
new file mode 100644
@@ -0,0 +1,102 @@
@@ -0,0 +1,102 @@
+'use strict';
+/**
+ * openvibe.report/design: how closely every OpenVibe site follows the shared design kit (plan L45 step 6), read from
+ * each site's code (server/design-health/scan.js, snapshot in server/design-health/snapshot.json).
+ *
+ * designHealth(snapshot) /design: the network at a glance, every site most-to-fix first, and how it is scored
+ * designSite(snapshot, site) /design/:site: one site's counts and where, each finding linked to its line on GitHub
+ */
+const kit = require('openvibe-shared/kit');
+const { layout } = require('./index');
+
+const esc = kit.esc;
+const exact = (v) => (v == null ? '—' : Number(v).toLocaleString('en-US'));
+const day = (s) => { const t = Date.parse(s || ''); return Number.isFinite(t) ? new Date(t).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric', timeZone: 'UTC' }) : ''; };
+const IN_VIEW = 15; // sites before the counted fold
+
+// The rules in the order they matter, in words a reader knows.
+const RULES = [
+ ['spinner', 'Spinners', 'A keyframe that turns a whole circle, or an animation that runs one forever. Nothing on OpenVibe spins: a loading state breathes, bobs or fills.'],
+ ['missing_reduced_motion', 'No reduced motion', 'A stylesheet that animates and never asks for prefers-reduced-motion, so a person who asked for less movement still gets it.'],
+ ['literal_duration', 'Hand-set timings', 'A transition or animation time written by hand (.25s, 300ms) where the kit says var(--ov-dur-1…4), so a look\'s motion and Calm cannot change it.'],
+ ['literal_curve', 'Hand-set curves', 'A cubic-bezier() or an ease keyword where the kit says var(--ov-ease-out), var(--ov-ease-spring) or var(--ov-ease-in-out).'],
+ ['color_drift', 'Colours outside the theme', 'A colour written by hand in a file that already uses the theme\'s variables: it stays put when a person changes their look.'],
+];
+const LABEL = Object.fromEntries(RULES.map(([k, l]) => [k, l]));
+const SHORT = { spinner: 'Spins', missing_reduced_motion: 'No reduced motion', literal_duration: 'Timing', literal_curve: 'Curve', color_drift: 'Colour' };
+// A count in words, for a phone's one-line summary of a site ("931 timings 448 curves").
+const WORDS = { spinner: 'spinning', missing_reduced_motion: 'without reduced motion', literal_duration: 'timings', literal_curve: 'curves', color_drift: 'colours' };
+
+const band = (s) => (s >= 90 ? 'is-good' : s >= 60 ? 'is-ok' : 'is-low');
+/** The score as a number on a bar: 0 to 100, coloured by band, with its words for a screen reader. */
+const scoreBar = (s) => `<span class="dh-score ${band(Number(s))}" style="--dh:${Math.max(0, Math.min(100, Number(s) || 0))}%"><b>${esc(Number(s).toFixed(Number.isInteger(Number(s)) ? 0 : 1))}</b><i aria-hidden="true"></i></span>`;
+const repoUrl = (s) => `https://github.com/OpenVibers/${encodeURIComponent(s.repo)}`;
+const median = (xs) => { const v = xs.filter((x) => Number.isFinite(x)).sort((a, b) => a - b); if (!v.length) return null; const m = v.length >> 1; return v.length % 2 ? v[m] : (v[m - 1] + v[m]) / 2; };
+
+function siteRow(s) {
+ const c = s.counts || {};
+ return `<div class="rp-tr dh-tr" role="row"><span role="cell" class="rp-site"><a href="/design/${esc(s.slug)}">${esc(s.repo)}</a><small>${exact(s.files_scanned)} ${Number(s.files_scanned) === 1 ? 'file' : 'files'}</small></span>`
+ + `<span role="cell" data-l="Score">${scoreBar(s.score)}</span>`
+ + RULES.map(([k]) => `<span role="cell" data-l="${esc(LABEL[k])}" data-s="${esc(WORDS[k])}" class="dh-n${c[k] ? '' : ' is-zero'}${k === 'spinner' && c[k] ? ' is-bad' : ''}">${exact(c[k] || 0)}</span>`).join('')
+ + (Number(c.total) ? '' : '<span role="cell" class="dh-none">Nothing to fix</span>')
+ + '</div>';
+}
+const tableHead = `<div class="rp-tr rp-th dh-tr" role="row"><span role="columnheader">Site</span><span role="columnheader">Score</span>${RULES.map(([k]) => `<span role="columnheader">${esc(LABEL[k])}</span>`).join('')}</div>`;
+
+function designHealth(snap) {
+ const sites = (snap.sites || []).slice().sort((a, b) => a.score - b.score || a.repo.localeCompare(b.repo));
+ const sum = (k) => sites.reduce((n, s) => n + (Number(s.counts && s.counts[k]) || 0), 0);
+ const spins = sum('spinner'), still = sum('missing_reduced_motion');
+ const clean = sites.filter((s) => Number(s.score) >= 100).length;
+ const head = sites.slice(0, IN_VIEW), rest = sites.slice(IN_VIEW);
+ const W = snap.weights || {};
+ const body = `<header class="rp-page-h"><p class="rp-kicker">Open numbers</p><h1>Design health of every site</h1>
+<p class="rp-lede">Read from each site's own code: motion through the kit's timings and curves, colours from the theme, a pause for people who ask for less movement, and nothing that spins. 100 is a site that follows the kit everywhere it was read.${snap.generated_at ? ` Measured ${esc(day(snap.generated_at))}, at each site's main branch.` : ''}</p></header>
+<section class="rp-tiles" aria-label="Across the network">
+<div class="rp-tile"><span class="rp-tile-l">Sites measured</span><b class="rp-tile-v">${exact(sites.length)}</b><span class="rp-tile-s">${exact(clean)} at 100</span></div>
+<div class="rp-tile"><span class="rp-tile-l">Median score</span><b class="rp-tile-v">${median(sites.map((s) => Number(s.score))) == null ? '—' : esc(String(Math.round(median(sites.map((s) => Number(s.score))) * 10) / 10))}</b><span class="rp-tile-s">of 100</span></div>
+<div class="rp-tile"><span class="rp-tile-l">Spinners</span><b class="rp-tile-v">${spins ? exact(spins) : 'None'}</b><span class="rp-tile-s">${spins ? 'still to replace' : 'nothing spins'}</span></div>
+<div class="rp-tile"><span class="rp-tile-l">Files without reduced motion</span><b class="rp-tile-v">${still ? exact(still) : 'None'}</b><span class="rp-tile-s">${still ? 'animate for everyone' : 'every animation can stop'}</span></div>
+</section>
+<section aria-labelledby="dh-sites-h"><h2 id="dh-sites-h">Every site</h2><p class="rp-fine">Most to fix first. A site's page shows where, line by line.</p>
+${sites.length ? `<div class="rp-table dh-table" role="table" aria-label="Design health, most to fix first">${tableHead}${head.map(siteRow).join('')}</div>
+${rest.length ? `<details class="dh-fold"><summary>All ${exact(sites.length)} sites <span>${exact(rest.length)} more</span></summary><div class="rp-table dh-table" role="table" aria-label="Design health, the rest">${tableHead}${rest.map(siteRow).join('')}</div></details>` : ''}`
+ : '<p class="rp-fine">No measurement yet.</p>'}
+</section>
+<section aria-labelledby="dh-how-h"><h2 id="dh-how-h">How it is measured</h2>
+<dl class="dh-rules">${RULES.map(([k, l, what]) => `<div><dt>${esc(l)} <span>−${esc(String(W[k] != null ? W[k] : ''))} each</span></dt><dd>${esc(what)}</dd></div>`).join('')}</dl>
+<p class="rp-fine">A site's score is 100 minus what its findings cost, never below 0. The rules read every stylesheet, and every <style> in a page or script, under a site's <code>public/</code> and <code>server/views/</code>; a site with none there is left out. The kit's own tokens and the fallback inside <code>var(--name, fallback)</code> never count. The rules are open: <a href="https://github.com/OpenVibers/OpenVibe.Report/blob/main/server/design-health/scan.js"><code>server/design-health/scan.js</code></a>, run by <code>scripts/design-health.js</code>. The same numbers: <a href="/api/v1/design-health"><code>/api/v1/design-health</code></a>.</p></section>`;
+ return layout({
+ title: 'Design health of every site',
+ description: 'How closely each OpenVibe site follows the shared design kit: motion timings and curves, theme colours, reduced motion and nothing that spins, read from its code.',
+ body, active: '/stats', path: '/design',
+ });
+}
+
+function designSite(snap, s) {
+ const c = s.counts || {};
+ const commit = String(s.commit || '');
+ const finds = (s.findings || []).map((f) => {
+ const where = `${f.file}${f.line ? `:${f.line}` : ''}`;
+ const href = commit ? `${repoUrl(s)}/blob/${encodeURIComponent(commit)}/${String(f.file).split('/').map(encodeURIComponent).join('/')}${f.line ? `#L${Number(f.line)}` : ''}` : '';
+ return `<li class="dh-find"><span class="dh-rule dh-rule--${esc(String(f.rule).replace(/_/g, '-'))}">${esc(SHORT[f.rule] || f.rule)}</span>`
+ + `${href ? `<a class="dh-where" href="${esc(href)}">${esc(where)}</a>` : `<span class="dh-where">${esc(where)}</span>`}`
+ + `${f.text ? `<code class="dh-text">${esc(f.text)}</code>` : ''}</li>`;
+ }).join('');
+ const total = Number(s.findings_total) || 0;
+ const body = `<header class="rp-page-h"><p class="rp-kicker"><a href="/design">Design health</a></p><h1>${esc(s.repo)}</h1>
+<p class="rp-lede dh-lede">${scoreBar(s.score)} <span>${exact(s.files_scanned)} ${Number(s.files_scanned) === 1 ? 'file' : 'files'} read${commit ? ` at <a href="${esc(`${repoUrl(s)}/tree/${encodeURIComponent(commit)}`)}"><code>${esc(commit.slice(0, 7))}</code></a>` : ''}${snap.generated_at ? `, ${esc(day(snap.generated_at))}` : ''}.</span></p></header>
+<section class="rp-tiles" aria-label="Findings by rule">${RULES.map(([k, l]) => `<div class="rp-tile"><span class="rp-tile-l">${esc(l)}</span><b class="rp-tile-v">${exact(c[k] || 0)}</b></div>`).join('')}</section>
+<section aria-labelledby="dh-where-h"><h2 id="dh-where-h">Where</h2>
+${total ? `<p class="rp-fine">${total > (s.findings || []).length ? `${exact((s.findings || []).length)} of ${exact(total)}: the ones that cost most, from as many files as possible.` : `All ${exact(total)}.`} Each opens its line on GitHub.</p><ol class="dh-finds">${finds}</ol>`
+ : '<p class="rp-fine">Nothing to fix: this site follows the kit everywhere the rules read.</p>'}
+</section>
+<p class="rp-fine"><a href="/design">Every site</a> · <a href="/design#dh-how-h">How it is measured</a> · <a href="/api/v1/design-health/${esc(s.slug)}"><code>/api/v1/design-health/${esc(s.slug)}</code></a></p>`;
+ return layout({
+ title: `${s.repo}: design health`,
+ description: `How closely ${s.repo} follows the shared design kit: score ${s.score} of 100, with every finding by rule and line.`,
+ body, active: '/stats', path: `/design/${s.slug}`,
+ });
+}
+
+module.exports = { designHealth, designSite, scoreBar };
@@ -280,7 +280,7 @@ ${sites ? `<section aria-labelledby="rp-sites-h"><h2 id="rp-sites-h">Every site<
@@ -280,7 +280,7 @@ ${sites ? `<section aria-labelledby="rp-sites-h"><h2 id="rp-sites-h">Every site<
<div class="rp-table" role="table" aria-label="Every site"><div class="rp-tr rp-th" role="row"><span role="columnheader">Site</span><span role="columnheader">Page views</span><span role="columnheader">Visitors</span><span role="columnheader">Requests, 24 h</span><span role="columnheader">95% answered in</span></div>${sites}</div></section>` : ''}
<div class="rp-table" role="table" aria-label="Every site"><div class="rp-tr rp-th" role="row"><span role="columnheader">Site</span><span role="columnheader">Page views</span><span role="columnheader">Visitors</span><span role="columnheader">Requests, 24 h</span><span role="columnheader">95% answered in</span></div>${sites}</div></section>` : ''}
${L ? `<section aria-labelledby="rp-live-h"><h2 id="rp-live-h">OpenVibe.Live</h2><div class="rp-minis">${[['Streamers', L.streamers], ['Streams', L.streams], ['Hours watched', L.hours_watched], ['Chat messages', L.messages], ['Recordings', L.vods], ['Clips', L.clips], ['AI moments', L.ai_moments]].filter(([, v]) => n(v)).map(([l, v]) => `<div><b>${fmt(v)}</b><span>${esc(l)}</span></div>`).join('')}</div></section>` : ''}
${L ? `<section aria-labelledby="rp-live-h"><h2 id="rp-live-h">OpenVibe.Live</h2><div class="rp-minis">${[['Streamers', L.streamers], ['Streams', L.streams], ['Hours watched', L.hours_watched], ['Chat messages', L.messages], ['Recordings', L.vods], ['Clips', L.clips], ['AI moments', L.ai_moments]].filter(([, v]) => n(v)).map(([l, v]) => `<div><b>${fmt(v)}</b><span>${esc(l)}</span></div>`).join('')}</div></section>` : ''}
${A && A.daily && A.daily.length > 1 ? `<section aria-labelledby="rp-acc-h"><h2 id="rp-acc-h">New accounts a day</h2>${bars(A.daily, 'New accounts a day', { h: 110 })}</section>` : ''}
${A && A.daily && A.daily.length > 1 ? `<section aria-labelledby="rp-acc-h"><h2 id="rp-acc-h">New accounts a day</h2>${bars(A.daily, 'New accounts a day', { h: 110 })}</section>` : ''}
-<section aria-labelledby="rp-how-h"><h2 id="rp-how-h">How these are counted</h2><p class="rp-fine">${esc(d.note || '')} The same numbers: <a href="/api/v1/stats?days=${esc(days)}"><code>/api/v1/stats</code></a>.</p></section>`;
+<section aria-labelledby="rp-how-h"><h2 id="rp-how-h">How these are counted</h2><p class="rp-fine">${esc(d.note || '')} The same numbers: <a href="/api/v1/stats?days=${esc(days)}"><code>/api/v1/stats</code></a>. How closely each site follows the design kit, read from its code: <a href="/design">Design health</a>.</p></section>`;
return layout({ title: `Every site's numbers, ${days} days`, description: 'Page views, people, streams and how fast each OpenVibe service answers, open to everyone.', body, active: '/stats', path: `/stats${Number(days) === 30 ? '' : `?days=${Number(days)}`}`, agent: true });
return layout({ title: `Every site's numbers, ${days} days`, description: 'Page views, people, streams and how fast each OpenVibe service answers, open to everyone.', body, active: '/stats', path: `/stats${Number(days) === 30 ? '' : `?days=${Number(days)}`}`, agent: true });
}
}
new file mode 100644
new file mode 100644
@@ -0,0 +1,91 @@
@@ -0,0 +1,91 @@
+'use strict';
+/**
+ * openvibe.report/design (plan L45 step 6): every site's design health from the snapshot, most to fix first, with a
+ * counted fold past fifteen; a site's page with its counts and each finding linked to its line at the measured commit;
+ * the same as JSON; an unknown site is a 404; a repo with nothing to read is left out; every value from the snapshot
+ * is escaped. The real snapshot (server/design-health/snapshot.json) must parse and hold sites.
+ */
+const assert = require('assert');
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+
+const FIXTURE = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'dh-')), 'design-health.json');
+const counts = (o) => ({ literal_duration: 0, literal_curve: 0, spinner: 0, color_drift: 0, missing_reduced_motion: 0, total: 0, ...o });
+const sites = [
+ { repo: 'OpenVibe.Live', commit: 'abcdef1234567890', files_scanned: 119, score: 12.5, findings_total: 40, counts: counts({ spinner: 1, literal_duration: 30, total: 31 }),
+ findings: [{ rule: 'spinner', file: 'public/css/style.css', line: 489, value: 'spin', text: '@keyframes spin{to{transform:rotate(360deg)}} <script>alert(1)</script>' }] },
+ { repo: 'OpenVibe.Zone', commit: '1111111222222', files_scanned: 1, score: 100, findings_total: 0, counts: counts({}), findings: [] },
+ { repo: 'OpenVibe.SDK', commit: '333', files_scanned: 0, score: 100, findings_total: 0, counts: counts({}), findings: [] },
+];
+for (let i = 0; i < 16; i++) sites.push({ repo: `OpenVibe.Site${i}`, commit: 'c0ffee', files_scanned: 3, score: 90 + i / 2, findings_total: 2, counts: counts({ literal_curve: 2, total: 2 }), findings: [] });
+fs.writeFileSync(FIXTURE, JSON.stringify({ generated_at: '2026-11-01T11:00:00.000Z', rules: ['literal_duration', 'literal_curve', 'spinner', 'color_drift', 'missing_reduced_motion'], weights: { spinner: 4, missing_reduced_motion: 2, literal_duration: 0.25, literal_curve: 0.25, color_drift: 0.05 }, kept: 25, sites }));
+process.env.DESIGN_HEALTH_FILE = FIXTURE;
+
+const { boot, check, done } = require('./helpers/boot');
+
+(async () => {
+ const t = await boot();
+ try {
+ await check('/design: every site with something to read, most to fix first, fifteen in view and the rest in a counted fold', async () => {
+ const r = await t.get('/design');
+ assert.strictEqual(r.status, 200);
+ assert.strictEqual(r.headers.get('cache-control'), 'public, max-age=120');
+ assert.ok(r.text.includes('<h1>Design health of every site</h1>'));
+ assert.ok(r.text.includes('<link rel="canonical" href="https://openvibe.report/design">'));
+ assert.ok(!r.text.includes('OpenVibe.SDK'), 'a repo with no stylesheet is not scored');
+ const first = r.text.indexOf('href="/design/live"'), zone = r.text.indexOf('href="/design/zone"');
+ assert.ok(first > 0 && zone > first, 'the lowest score comes first');
+ assert.match(r.text, /<summary>All 18 sites <span>3 more<\/span><\/summary>/);
+ assert.ok(r.text.includes('<b class="rp-tile-v">1</b><span class="rp-tile-s">still to replace</span>'), 'the spinners are counted');
+ assert.ok(r.text.includes('class="dh-score is-low" style="--dh:12.5%"'), 'the score is a bar in its band');
+ assert.ok(r.text.includes('server/design-health/scan.js'), 'the rules are linked');
+ });
+
+ await check('/design/:site: the counts, and each finding opens its line at the measured commit, escaped', async () => {
+ const r = await t.get('/design/live');
+ assert.strictEqual(r.status, 200);
+ assert.ok(r.text.includes('<h1>OpenVibe.Live</h1>'));
+ assert.ok(r.text.includes('href="https://github.com/OpenVibers/OpenVibe.Live/blob/abcdef1234567890/public/css/style.css#L489"'));
+ assert.ok(r.text.includes('1 of 40: the ones that cost most'));
+ assert.ok(!r.text.includes('<script>alert(1)</script>'), 'a finding\'s text is escaped');
+ assert.ok(r.text.includes('<script>alert(1)</script>'));
+ const clean = await t.get('/design/zone');
+ assert.ok(clean.text.includes('Nothing to fix'));
+ });
+
+ await check('an unknown or malformed site is a 404 page and a 404 problem', async () => {
+ assert.strictEqual((await t.get('/design/nope')).status, 404);
+ assert.strictEqual((await t.get('/design/sdk')).status, 404, 'left out of the snapshot\'s sites');
+ assert.strictEqual((await t.get('/design/%3Cx%3E')).status, 404);
+ const api = await t.get('/api/v1/design-health/nope');
+ assert.strictEqual(api.status, 404);
+ assert.match(api.headers.get('content-type') || '', /problem\+json/);
+ });
+
+ await check('the same as JSON, CORS-open', async () => {
+ const r = await t.get('/api/v1/design-health');
+ assert.strictEqual(r.status, 200);
+ assert.strictEqual(r.headers.get('access-control-allow-origin'), '*');
+ const j = r.json();
+ assert.strictEqual(j.sites.length, 18);
+ assert.strictEqual(j.weights.spinner, 4);
+ const one = (await t.get('/api/v1/design-health/live')).json();
+ assert.strictEqual(one.site.repo, 'OpenVibe.Live');
+ assert.strictEqual(one.site.findings[0].line, 489);
+ });
+
+ await check('the snapshot parses and holds sites with counts', async () => {
+ const real = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'server', 'design-health', 'snapshot.json'), 'utf8'));
+ assert.ok(Array.isArray(real.sites) && real.sites.length >= 20, `${(real.sites || []).length} sites`);
+ for (const s of real.sites) {
+ assert.ok(s.repo && s.counts && Number.isFinite(Number(s.score)), s.repo);
+ assert.ok(Number(s.files_scanned) > 0, `${s.repo}: nothing to read, so not in the snapshot`);
+ }
+ });
+ } finally {
+ await t.close();
+ fs.rmSync(path.dirname(FIXTURE), { recursive: true, force: true });
+ }
+ done();
+})().catch((err) => { console.error(err); process.exit(1); });
new file mode 100644
new file mode 100644
@@ -0,0 +1,255 @@
@@ -0,0 +1,255 @@
+'use strict';
+// design-lint/test.js — the lint's own tests: plain node and assert, over small fixtures.
+//
+// node test.js
+//
+// Each rule has to find what it should, and a clean fixture has to stay clean.
+
+const assert = require('assert');
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+
+const { scanRepo, scoreOf } = require('../server/design-health/scan.js');
+
+// ── the fixtures ─────────────────────────────────────────────────────────────────────────────────────────────
+// a repo where every style already goes through the kit
+const CLEAN_CSS = [
+ '/* a site that already uses the kit */',
+ '.card {',
+ ' background: var(--bg-primary);',
+ ' color: var(--text-primary);',
+ ' border: 1px solid var(--border);',
+ ' transition: transform var(--ov-dur-2) var(--ov-ease-out);',
+ '}',
+ '.card:hover { transform: translateY(-2px); }',
+ '.fade { animation: fade var(--ov-dur-3) var(--ov-ease-in-out) both; }',
+ '@keyframes fade { from { opacity: 0; } to { opacity: 1; } }',
+ '@media (prefers-reduced-motion: reduce) {',
+ ' .card, .fade { transition: none; animation: none; }',
+ '}',
+ ''
+].join('\n');
+
+// a repo that writes its own timing, its own curves, its own spinner and its own colours
+const DIRTY_CSS = [
+ '/* a site that hand-writes what the kit owns */',
+ '.card {',
+ ' background: var(--bg-primary);',
+ ' transition: transform .25s ease;',
+ '}',
+ '.panel { animation: fade 300ms cubic-bezier(.4, 0, .2, 1) both; }',
+ '.slow { transition-duration: 1.4s; }',
+ '.badge { color: #ff006e; background: rgb(0, 255, 65); }',
+ '@keyframes spin { to { transform: rotate(360deg); } }',
+ '.spinner { animation: spin 1s linear infinite; }',
+ ''
+].join('\n');
+
+// an inline <style> in HTML
+const DIRTY_HTML = [
+ '<!doctype html>',
+ '<html><head>',
+ '<style>',
+ ' .hero { color: #ff006e; background: var(--bg-primary); transition: opacity .2s ease-in-out; }',
+ ' .hero2 { animation: pulse 800ms linear infinite; }',
+ ' @keyframes pulse { to { transform: rotate(360deg); } }',
+ '</style>',
+ '</head><body><div class="hero">hi</div></body></html>',
+ ''
+].join('\n');
+
+const CLEAN_HTML = [
+ '<!doctype html>',
+ '<html><head>',
+ '<style>',
+ ' .hero { color: var(--text-primary); transition: opacity var(--ov-dur-1) var(--ov-ease-out); }',
+ '</style>',
+ '</head><body><div class="hero">hi</div></body></html>',
+ ''
+].join('\n');
+
+// ── the harness ──────────────────────────────────────────────────────────────────────────────────────────────
+const TMP = fs.mkdtempSync(path.join(os.tmpdir(), 'design-lint-test-'));
+
+function makeRepo(files) {
+ const dir = fs.mkdtempSync(path.join(TMP, 'repo-'));
+ for (const rel of Object.keys(files)) {
+ const full = path.join(dir, rel);
+ fs.mkdirSync(path.dirname(full), { recursive: true });
+ fs.writeFileSync(full, files[rel]);
+ }
+ return dir;
+}
+
+const results = [];
+function test(name, fn) {
+ try {
+ fn();
+ results.push({ name, ok: true });
+ } catch (err) {
+ results.push({ name, ok: false, err });
+ }
+}
+
+const rulesOf = (res, rule) => res.findings.filter((f) => f.rule === rule);
+const countOf = (res, rule) => res.counts[rule] || 0;
+
+// ── the tests ─────────────────────────────────────────────────────────────────────────────────────────────────
+const clean = scanRepo(makeRepo({ 'public/css/site.css': CLEAN_CSS, 'public/index.html': CLEAN_HTML }));
+
+test('a clean file has no findings at all', () => {
+ assert.deepStrictEqual(clean.counts, { literal_duration: 0, literal_curve: 0, spinner: 0, color_drift: 0, missing_reduced_motion: 0, total: 0 });
+ assert.deepStrictEqual(clean.findings, []);
+});
+
+test('a clean file scores 100', () => {
+ assert.strictEqual(clean.score, 100);
+});
+
+test('the kit tokens are never read as drift', () => {
+ assert.strictEqual(countOf(clean, 'literal_duration'), 0);
+ assert.strictEqual(countOf(clean, 'literal_curve'), 0);
+});
+
+const dirty = scanRepo(makeRepo({ 'public/css/site.css': DIRTY_CSS }));
+
+test('a literal duration is found, with its file and line', () => {
+ const found = rulesOf(dirty, 'literal_duration');
+ assert.ok(found.length >= 3, 'expected at least three literal durations, got ' + found.length);
+ assert.ok(found.some((f) => f.value === '.25s' && f.file === 'public/css/site.css'));
+ assert.ok(found.some((f) => f.value === '300ms'));
+ assert.ok(found.some((f) => f.value === '1.4s'));
+ assert.ok(found.every((f) => f.line > 0));
+ assert.ok(found.every((f) => f.file === 'public/css/site.css'));
+});
+
+test('a literal curve is found, whether a cubic-bezier or an ease keyword', () => {
+ const found = rulesOf(dirty, 'literal_curve');
+ assert.ok(found.some((f) => /cubic-bezier/i.test(f.value)), 'expected a cubic-bezier curve');
+ assert.ok(found.some((f) => /^ease/i.test(f.value)), 'expected an ease keyword');
+});
+
+test('a spinner is found: the keyframes that turn, and the animation that runs them forever', () => {
+ const found = rulesOf(dirty, 'spinner');
+ assert.ok(found.some((f) => f.kind === 'spinner_keyframes' && f.value === 'spin'), 'expected the spin keyframes');
+ assert.ok(found.some((f) => f.kind === 'spinner_usage' && f.value === 'spin'), 'expected the infinite animation');
+});
+
+test('a hard-coded colour is found in a file that already uses the theme', () => {
+ const found = rulesOf(dirty, 'color_drift');
+ assert.ok(found.some((f) => f.value === '#ff006e'), 'expected the hex colour');
+ assert.ok(found.some((f) => /rgb/i.test(f.value)), 'expected the rgb colour');
+});
+
+test('a file that animates without prefers-reduced-motion is found', () => {
+ const found = rulesOf(dirty, 'missing_reduced_motion');
+ assert.strictEqual(found.length, 1);
+ assert.strictEqual(found[0].file, 'public/css/site.css');
+});
+
+test('an inline <style> in HTML is linted like a CSS file', () => {
+ const html = scanRepo(makeRepo({ 'public/index.html': DIRTY_HTML }));
+ assert.ok(countOf(html, 'literal_duration') >= 1, 'expected a literal duration in the inline style');
+ assert.ok(countOf(html, 'literal_curve') >= 1, 'expected a literal curve in the inline style');
+ assert.ok(countOf(html, 'spinner') >= 1, 'expected a spinner in the inline style');
+ assert.ok(countOf(html, 'color_drift') >= 1, 'expected colour drift in the inline style');
+ assert.strictEqual(countOf(html, 'missing_reduced_motion'), 1);
+});
+
+test('an inline <style> that uses the kit is clean', () => {
+ const html = scanRepo(makeRepo({ 'public/index.html': CLEAN_HTML }));
+ assert.strictEqual(html.counts.total, 0, JSON.stringify(html.findings));
+});
+
+test('the spinner is not reported without infinite', () => {
+ const repo = makeRepo({ 'public/css/site.css': '@keyframes spin { to { transform: rotate(360deg); } }\n.a { animation: spin 1s linear; }\n' });
+ const res = scanRepo(repo);
+ assert.strictEqual(countOf(res, 'spinner'), 1, 'only the keyframes, not the finite animation');
+ assert.strictEqual(rulesOf(res, 'spinner')[0].kind, 'spinner_keyframes');
+});
+
+test('a keyframes that does not turn is not a spinner', () => {
+ const repo = makeRepo({ 'public/css/site.css': '@keyframes fade { from { opacity: 0; } to { opacity: 1; } }\n.a { animation: fade 1s linear infinite; }\n' });
+ const res = scanRepo(repo);
+ assert.strictEqual(countOf(res, 'spinner'), 0);
+});
+
+test('a var() fallback is not read as a hard-coded colour', () => {
+ const repo = makeRepo({ 'public/css/site.css': '.a { color: var(--accent, #ff006e); background: var(--bg-primary); }\n' });
+ const res = scanRepo(repo);
+ assert.strictEqual(countOf(res, 'color_drift'), 0, JSON.stringify(rulesOf(res, 'color_drift')));
+});
+
+test('a file with no theme variables is not judged for colour drift', () => {
+ const repo = makeRepo({ 'public/css/site.css': '.a { color: #ff006e; background: #000; }\n' });
+ const res = scanRepo(repo);
+ assert.strictEqual(countOf(res, 'color_drift'), 0);
+});
+
+test('the score is 100 minus the weighted counts, clamped at 0', () => {
+ assert.strictEqual(scoreOf({ spinner: 0, missing_reduced_motion: 0, literal_duration: 0, literal_curve: 0, color_drift: 0 }), 100);
+ const one = scoreOf({ spinner: 1, missing_reduced_motion: 0, literal_duration: 0, literal_curve: 0, color_drift: 0 });
+ assert.ok(one < 100 && one > 90, 'a spinner costs the most, got ' + one);
+ const many = scoreOf({ spinner: 100, missing_reduced_motion: 0, literal_duration: 0, literal_curve: 0, color_drift: 0 });
+ assert.strictEqual(many, 0, 'the score never goes below 0');
+ const over = scoreOf({ spinner: 0, missing_reduced_motion: 0, literal_duration: 0, literal_curve: 0, color_drift: 100000 });
+ assert.strictEqual(over, 0, 'a pile of colour drift cannot push the score below 0 either');
+});
+
+test('the weights are in order: spinner, then reduced motion, then duration and curve, then colour', () => {
+ const cost = (rule) => 100 - scoreOf({ spinner: 0, missing_reduced_motion: 0, literal_duration: 0, literal_curve: 0, color_drift: 0, [rule]: 1 });
+ assert.ok(cost('spinner') > cost('missing_reduced_motion'), 'a spinner weighs more than a missing reduced motion');
+ assert.ok(cost('missing_reduced_motion') > cost('literal_duration'), 'a missing reduced motion weighs more than a literal duration');
+ assert.strictEqual(cost('literal_duration'), cost('literal_curve'), 'a duration and a curve weigh the same');
+ assert.ok(cost('literal_curve') > cost('color_drift'), 'a literal curve weighs more than a hard-coded colour');
+});
+
+test('the score falls as the findings pile up', () => {
+ const full = scoreOf({ spinner: 0, missing_reduced_motion: 0, literal_duration: 0, literal_curve: 0, color_drift: 0 });
+ const some = scoreOf({ spinner: 0, missing_reduced_motion: 1, literal_duration: 2, literal_curve: 2, color_drift: 0 });
+ assert.ok(some < full);
+});
+
+test('node_modules, vendor and .min. files are skipped', () => {
+ const repo = makeRepo({
+ 'public/css/site.css': CLEAN_CSS,
+ 'public/node_modules/pkg/x.css': DIRTY_CSS,
+ 'public/vendor/x.css': DIRTY_CSS,
+ 'public/css/site.min.css': DIRTY_CSS
+ });
+ const res = scanRepo(repo);
+ assert.strictEqual(res.counts.total, 0, JSON.stringify(res.findings));
+});
+
+test('the findings are sorted by file and line', () => {
+ const repo = makeRepo({ 'public/css/b.css': DIRTY_CSS, 'public/css/a.css': DIRTY_CSS });
+ const res = scanRepo(repo);
+ for (let i = 1; i < res.findings.length; i++) {
+ const prev = res.findings[i - 1];
+ const cur = res.findings[i];
+ assert.ok(prev.file < cur.file || (prev.file === cur.file && prev.line <= cur.line), 'not sorted at ' + i);
+ }
+});
+
+test('an empty repo is clean', () => {
+ const res = scanRepo(makeRepo({ 'README.md': '# nothing here' }));
+ assert.strictEqual(res.counts.total, 0);
+ assert.strictEqual(res.score, 100);
+});
+
+// ── the report ───────────────────────────────────────────────────────────────────────────────────────────────
+let failed = 0;
+for (const r of results) {
+ if (r.ok) {
+ console.log(' ok ' + r.name);
+ } else {
+ failed++;
+ console.log(' FAIL ' + r.name);
+ console.log(' ' + (r.err && r.err.message ? String(r.err.message).split('\n').join('\n ') : r.err));
+ }
+}
+console.log('');
+console.log((results.length - failed) + '/' + results.length + ' passed');
+try { fs.rmSync(TMP, { recursive: true, force: true }); } catch (e) { /* the temp dir is the OS's problem now */ }
+process.exit(failed ? 1 : 0);