┌   ┐
54
└   ┘

summaryrefslogtreecommitdiff
path: root/.obsidian
diff options
context:
space:
mode:
authorConway <[email protected]>2026-04-06 18:40:40 -0400
committerConway <[email protected]>2026-04-06 18:40:40 -0400
commit27336783f769d5dd6114c9cfb1dc9baedb833ccb (patch)
tree38c5610b004e1975a392ad267919274c73491098 /.obsidian
parent2539c3b53c13c87c89239d0cb4ca7ff3bd8c734a (diff)
Diffstat (limited to '.obsidian')
-rw-r--r--.obsidian/plugins/abacus-sorter/main.js1077
-rw-r--r--.obsidian/plugins/abacus-sorter/manifest.json9
-rw-r--r--.obsidian/plugins/abacus-sorter/styles.css139
-rw-r--r--.obsidian/themes/Spectroscope-Gruv/manifest.json9
-rw-r--r--.obsidian/themes/Spectroscope-Gruv/theme.css473
-rw-r--r--.obsidian/themes/Spectroscope-Noir/manifest.json9
-rw-r--r--.obsidian/themes/Spectroscope-Noir/theme.css474
7 files changed, 2190 insertions, 0 deletions
diff --git a/.obsidian/plugins/abacus-sorter/main.js b/.obsidian/plugins/abacus-sorter/main.js
new file mode 100644
index 0000000..966e1a3
--- /dev/null
+++ b/.obsidian/plugins/abacus-sorter/main.js
@@ -0,0 +1,1077 @@
+/*
+Abacus Custom Sorter. It's a config-driven custom file explorer sorter for Obsidian.
+Inspired by SebastianMC's Custom File Explorer sorting.
+This plugin however does not use a bookmark dependency.
+*/
+
+var obsidian = require('obsidian');
+
+// Default Settings
+
+const DEFAULT_SETTINGS = {
+ suspended: false,
+ sortSpecFile: '',
+ statusBarEnabled: true,
+ notificationsEnabled: true,
+ contextMenuEnabled: true,
+ delayForInitialApplication: 0,
+ manualOrder: {} // { folderPath: [itemName, itemName, ...] }
+};
+
+// Sort Spec Parser
+
+const SORT_ORDERS = {
+ 'a-z': { key: 'alpha', reverse: false, caseSensitive: false },
+ 'A-Z': { key: 'alpha', reverse: false, caseSensitive: true },
+ 'z-a': { key: 'alpha', reverse: true, caseSensitive: false },
+ 'Z-A': { key: 'alpha', reverse: true, caseSensitive: true },
+ 'modified-new': { key: 'mtime', reverse: false },
+ 'modified-old': { key: 'mtime', reverse: true },
+ 'created-new': { key: 'ctime', reverse: false },
+ 'created-old': { key: 'ctime', reverse: true },
+ 'natural-a-z': { key: 'natural', reverse: false },
+ 'natural-z-a': { key: 'natural', reverse: true },
+ 'files-first': { key: 'type', filesFirst: true },
+ 'folders-first': { key: 'type', filesFirst: false },
+ 'by-metadata': { key: 'metadata' },
+ 'manual': { key: 'manual' },
+};
+
+const MATCH_TYPES = {
+ 'exact': 'exact',
+ 'prefix': 'prefix',
+ 'suffix': 'suffix',
+ 'regex': 'regex',
+ 'with-metadata': 'metadata',
+ 'files': 'files',
+ 'folders': 'folders',
+ 'all': 'all',
+};
+
+function naturalCompare(a, b) {
+ return a.localeCompare(b, undefined, { numeric: true, sensitivity: 'base' });
+}
+
+function parseSortSpec(text) {
+ if (!text || !text.trim()) return null;
+ const lines = text.split('\n');
+ const specs = {};
+ let currentFolder = null;
+ let currentGroups = [];
+ let currentGroup = null;
+
+ function finishFolder() {
+ if (currentFolder !== null) {
+ if (currentGroup) currentGroups.push(currentGroup);
+ specs[currentFolder] = { groups: currentGroups };
+ currentGroups = [];
+ currentGroup = null;
+ }
+ }
+
+ for (let rawLine of lines) {
+ const line = rawLine.trim();
+ if (!line || line.startsWith('//') || line.startsWith('#')) continue;
+
+ // Folder target
+ if (line.startsWith('target-folder:')) {
+ finishFolder();
+ currentFolder = line.slice('target-folder:'.length).trim();
+ if (currentFolder === '/') currentFolder = '/';
+ continue;
+ }
+
+ // Wildcard folder target
+ if (line.startsWith('target-folder-wildcard:')) {
+ finishFolder();
+ currentFolder = line.slice('target-folder-wildcard:'.length).trim();
+ continue;
+ }
+
+ if (currentFolder === null) continue;
+
+ // Sort order line
+ if (line.startsWith('>') || line.startsWith('< ')) {
+ const orderStr = line.replace(/^[><]\s*/, '').trim();
+ const parts = orderStr.split(/\s+/);
+ const orderName = parts[0];
+ const order = SORT_ORDERS[orderName];
+ if (order && currentGroup) {
+ currentGroup.order = { ...order };
+ if (order.key === 'metadata' && parts.length > 1) {
+ currentGroup.order.metadataField = parts[1];
+ if (parts.includes('numeric')) currentGroup.order.numeric = true;
+ }
+ } else if (order && currentGroups.length === 0 && !currentGroup) {
+ // Default order for the folder
+ if (!specs[currentFolder]) specs[currentFolder] = { groups: [] };
+ specs[currentFolder].defaultOrder = { ...order };
+ if (order.key === 'metadata' && parts.length > 1) {
+ specs[currentFolder].defaultOrder.metadataField = parts[1];
+ if (parts.includes('numeric')) specs[currentFolder].defaultOrder.numeric = true;
+ }
+ }
+ continue;
+ }
+
+ // Separator
+ if (line === '---' || line === '***') {
+ if (currentGroup) {
+ currentGroups.push(currentGroup);
+ currentGroup = null;
+ }
+ continue;
+ }
+
+ // Match rule
+ let matched = false;
+ for (const [keyword, type] of Object.entries(MATCH_TYPES)) {
+ if (line.startsWith(keyword + ':') || line === keyword) {
+ if (currentGroup) currentGroups.push(currentGroup);
+ const value = line.includes(':') ? line.slice(keyword.length + 1).trim() : '';
+ currentGroup = { matchType: type, matchValue: value, order: null };
+ if (type === 'regex' && value) {
+ try { currentGroup.matchRegex = new RegExp(value); } catch(e) { /* skip invalid regex */ }
+ }
+ matched = true;
+ break;
+ }
+ }
+ if (matched) continue;
+
+ // If nothing else matched, treat as a group label or exact match shorthand
+ if (currentGroup) currentGroups.push(currentGroup);
+ currentGroup = { matchType: 'exact', matchValue: line, order: null };
+ }
+
+ finishFolder();
+ return Object.keys(specs).length > 0 ? specs : null;
+}
+
+// Items from getSortedFolderItems may be FileExplorerItem (with .file)
+// or raw TAbstractFile objects, depending on Obsidian version. This helper normalizes.
+function getItemFile(item) {
+ return item && item.file ? item.file : item;
+}
+
+function getItemName(item) {
+ const f = getItemFile(item);
+ return f ? (f.name || '') : '';
+}
+
+function itemMatchesGroup(item, group, app) {
+ const f = getItemFile(item);
+ const name = f ? (f.name || '') : '';
+ const basename = f ? (f.basename || name.replace(/\.[^.]+$/, '')) : '';
+
+ switch (group.matchType) {
+ case 'exact':
+ return name === group.matchValue || basename === group.matchValue;
+ case 'prefix':
+ return name.startsWith(group.matchValue) || basename.startsWith(group.matchValue);
+ case 'suffix':
+ return name.endsWith(group.matchValue) || basename.endsWith(group.matchValue);
+ case 'regex':
+ return group.matchRegex ? group.matchRegex.test(name) || group.matchRegex.test(basename) : false;
+ case 'metadata': {
+ if (!f || !f.path) return false;
+ const cache = app.metadataCache.getCache(f.path);
+ if (!cache || !cache.frontmatter) return false;
+ return cache.frontmatter[group.matchValue] !== undefined;
+ }
+ case 'files':
+ return !!(f && f.children === undefined);
+ case 'folders':
+ return !!(f && f.children !== undefined);
+ case 'all':
+ return true;
+ default:
+ return false;
+ }
+}
+
+// Sorting Engine
+
+function getMetadataValue(item, field, app) {
+ const f = getItemFile(item);
+ if (!f || !f.path) return null;
+ const cache = app.metadataCache.getCache(f.path);
+ if (!cache || !cache.frontmatter) return null;
+ return cache.frontmatter[field] ?? null;
+}
+
+function makeComparator(order, app, manualOrderList) {
+ if (!order) return null;
+
+ switch (order.key) {
+ case 'alpha':
+ return (a, b) => {
+ const an = getItemName(a);
+ const bn = getItemName(b);
+ const cmp = order.caseSensitive
+ ? an.localeCompare(bn)
+ : an.toLowerCase().localeCompare(bn.toLowerCase());
+ return order.reverse ? -cmp : cmp;
+ };
+
+ case 'natural':
+ return (a, b) => {
+ const an = getItemName(a);
+ const bn = getItemName(b);
+ const cmp = naturalCompare(an, bn);
+ return order.reverse ? -cmp : cmp;
+ };
+
+ case 'mtime':
+ return (a, b) => {
+ const af = getItemFile(a);
+ const bf = getItemFile(b);
+ const at = af && af.stat ? af.stat.mtime : 0;
+ const bt = bf && bf.stat ? bf.stat.mtime : 0;
+ return order.reverse ? at - bt : bt - at;
+ };
+
+ case 'ctime':
+ return (a, b) => {
+ const af = getItemFile(a);
+ const bf = getItemFile(b);
+ const at = af && af.stat ? af.stat.ctime : 0;
+ const bt = bf && bf.stat ? bf.stat.ctime : 0;
+ return order.reverse ? at - bt : bt - at;
+ };
+
+ case 'type':
+ return (a, b) => {
+ const af = getItemFile(a);
+ const bf = getItemFile(b);
+ const aIsFolder = !!(af && af.children !== undefined);
+ const bIsFolder = !!(bf && bf.children !== undefined);
+ if (aIsFolder === bIsFolder) return 0;
+ if (order.filesFirst) return aIsFolder ? 1 : -1;
+ return aIsFolder ? -1 : 1;
+ };
+
+ case 'metadata':
+ return (a, b) => {
+ let av = getMetadataValue(a, order.metadataField, app);
+ let bv = getMetadataValue(b, order.metadataField, app);
+ if (av === null && bv === null) return 0;
+ if (av === null) return 1;
+ if (bv === null) return -1;
+ if (order.numeric) {
+ av = parseFloat(av); bv = parseFloat(bv);
+ if (isNaN(av)) return 1;
+ if (isNaN(bv)) return -1;
+ return av - bv;
+ }
+ return String(av).localeCompare(String(bv));
+ };
+
+ case 'manual':
+ return (a, b) => {
+ if (!manualOrderList) return 0;
+ const an = getItemName(a);
+ const bn = getItemName(b);
+ const ai = manualOrderList.indexOf(an);
+ const bi = manualOrderList.indexOf(bn);
+ if (ai === -1 && bi === -1) return 0;
+ if (ai === -1) return 1;
+ if (bi === -1) return -1;
+ return ai - bi;
+ };
+
+ default:
+ return null;
+ }
+}
+
+function sortItems(items, spec, app, manualOrderList) {
+ if (!spec || !spec.groups || spec.groups.length === 0) {
+ // No groups, just apply default order if present
+ if (spec && spec.defaultOrder) {
+ const cmp = makeComparator(spec.defaultOrder, app, manualOrderList);
+ if (cmp) items.sort(cmp);
+ }
+ return items;
+ }
+
+ // Assign items to groups
+ const buckets = spec.groups.map(() => []);
+ const unmatched = [];
+
+ for (const item of items) {
+ let placed = false;
+ for (let g = 0; g < spec.groups.length; g++) {
+ if (itemMatchesGroup(item, spec.groups[g], app)) {
+ buckets[g].push(item);
+ placed = true;
+ break;
+ }
+ }
+ if (!placed) unmatched.push(item);
+ }
+
+ // Sort each bucket
+ const defaultCmp = spec.defaultOrder ? makeComparator(spec.defaultOrder, app, manualOrderList) : null;
+ for (let g = 0; g < spec.groups.length; g++) {
+ const group = spec.groups[g];
+ const cmp = group.order ? makeComparator(group.order, app, manualOrderList) : defaultCmp;
+ if (cmp) buckets[g].sort(cmp);
+ }
+ if (defaultCmp) unmatched.sort(defaultCmp);
+
+ // Reassemble
+ const result = [];
+ for (const bucket of buckets) result.push(...bucket);
+ result.push(...unmatched);
+ return result;
+}
+
+// Manual Order Sorting (standalone, no spec)─
+
+function applyManualOrder(items, manualOrderList) {
+ if (!manualOrderList || manualOrderList.length === 0) return items;
+
+ const ordered = [];
+ const unordered = [];
+
+ for (const item of items) {
+ const name = getItemName(item);
+ const idx = manualOrderList.indexOf(name);
+ if (idx !== -1) {
+ ordered[idx] = item;
+ } else {
+ unordered.push(item);
+ }
+ }
+
+ // Filter out sparse gaps and append unordered
+ return ordered.filter(Boolean).concat(unordered);
+}
+
+// File Explorer Patcher─
+
+function getFileExplorerLeaf(app) {
+ const leaves = app.workspace.getLeavesOfType('file-explorer');
+ return leaves.length > 0 ? leaves[0] : null;
+}
+
+function getFileExplorer(app) {
+ const leaf = getFileExplorerLeaf(app);
+ return leaf ? leaf.view : null;
+}
+
+/**
+ * Monkey-patch a single method on an object, preserving the original.
+ * Returns an uninstall function that restores the original.
+ */
+function monkeyPatch(obj, methodName, wrapper) {
+ const original = obj[methodName];
+ const hasOwn = obj.hasOwnProperty(methodName);
+
+ // The wrapper receives the original function as its first arg
+ obj[methodName] = function(...args) {
+ return wrapper.call(this, original.bind(this), ...args);
+ };
+
+ // Return uninstaller
+ return function uninstall() {
+ if (hasOwn) {
+ obj[methodName] = original;
+ } else {
+ delete obj[methodName];
+ }
+ };
+}
+
+function patchFileExplorer(plugin) {
+ const explorer = getFileExplorer(plugin.app);
+ if (!explorer) {
+ console.warn('Abacus: File explorer view not found');
+ return false;
+ }
+
+ plugin.fileExplorer = explorer;
+
+ // Verify the view has getSortedFolderItems and requestSort
+ if (typeof explorer.getSortedFolderItems !== 'function') {
+ console.warn('Abacus: getSortedFolderItems not found on file explorer view');
+ return false;
+ }
+ if (typeof explorer.requestSort !== 'function') {
+ console.warn('Abacus: requestSort not found on file explorer view');
+ }
+
+ if (plugin.unpatchFn) return true; // already patched
+
+ // Determine where to patch: prefer prototype, fall back to instance
+ const proto = explorer.constructor.prototype;
+ const target = (typeof proto.getSortedFolderItems === 'function') ? proto : explorer;
+
+ console.log('Abacus: Patching getSortedFolderItems on', target === proto ? 'prototype' : 'instance');
+
+ // Patch getSortedFolderItems - this is called by Obsidian to get the ordered
+ // list of children (FileExplorerItem[]) for each folder in the file explorer
+ plugin.unpatchFn = monkeyPatch(
+ target,
+ 'getSortedFolderItems',
+ function(originalFn, folder) {
+ // Always call original first to get Obsidian's default sorted items
+ const defaultResult = originalFn(folder);
+
+ if (plugin.settings.suspended) return defaultResult;
+
+ const folderPath = folder ? folder.path : '';
+ if (!defaultResult || defaultResult.length === 0) return defaultResult;
+
+ // Check for sort spec
+ const spec = plugin.getSpecForFolder(folderPath);
+ const manualOrderList = plugin.settings.manualOrder[folderPath] || null;
+
+ if (spec) {
+ return sortItems([...defaultResult], spec, plugin.app, manualOrderList);
+ } else if (manualOrderList && manualOrderList.length > 0) {
+ return applyManualOrder([...defaultResult], manualOrderList);
+ }
+
+ return defaultResult;
+ }
+ );
+
+ console.log('Abacus: File explorer patched successfully');
+ return true;
+}
+
+function unpatchFileExplorer(plugin) {
+ if (plugin.unpatchFn) {
+ plugin.unpatchFn();
+ plugin.unpatchFn = null;
+ }
+}
+
+// Settings Tab─
+
+class AbacusSettingTab extends obsidian.PluginSettingTab {
+ constructor(app, plugin) {
+ super(app, plugin);
+ this.plugin = plugin;
+ }
+
+ display() {
+ const { containerEl } = this;
+ containerEl.empty();
+
+ containerEl.createEl('h2', { text: 'Abacus Custom Sorter' });
+
+ new obsidian.Setting(containerEl)
+ .setName('Suspend sorting')
+ .setDesc('Temporarily disable custom sorting and revert to Obsidian defaults.')
+ .addToggle(toggle => toggle
+ .setValue(this.plugin.settings.suspended)
+ .onChange(async (value) => {
+ this.plugin.settings.suspended = value;
+ await this.plugin.saveSettings();
+ this.plugin.refreshExplorer();
+ this.plugin.updateStatusBar();
+ }));
+
+ new obsidian.Setting(containerEl)
+ .setName('Sort specification file')
+ .setDesc('Path to a sort spec file (relative to vault root). Leave empty for none. Example: .obsidian/sort-spec.md')
+ .addText(text => text
+ .setPlaceholder('.obsidian/sort-spec.md')
+ .setValue(this.plugin.settings.sortSpecFile)
+ .onChange(async (value) => {
+ this.plugin.settings.sortSpecFile = value.trim();
+ await this.plugin.saveSettings();
+ await this.plugin.loadSortSpec();
+ this.plugin.refreshExplorer();
+ }));
+
+ new obsidian.Setting(containerEl)
+ .setName('Status bar')
+ .setDesc('Show sort status in the status bar.')
+ .addToggle(toggle => toggle
+ .setValue(this.plugin.settings.statusBarEnabled)
+ .onChange(async (value) => {
+ this.plugin.settings.statusBarEnabled = value;
+ await this.plugin.saveSettings();
+ this.plugin.updateStatusBar();
+ }));
+
+ new obsidian.Setting(containerEl)
+ .setName('Notifications')
+ .setDesc('Show notices when sorting state changes.')
+ .addToggle(toggle => toggle
+ .setValue(this.plugin.settings.notificationsEnabled)
+ .onChange(async (value) => {
+ this.plugin.settings.notificationsEnabled = value;
+ await this.plugin.saveSettings();
+ }));
+
+ new obsidian.Setting(containerEl)
+ .setName('Context menu')
+ .setDesc('Show sorting options in file explorer context menu.')
+ .addToggle(toggle => toggle
+ .setValue(this.plugin.settings.contextMenuEnabled)
+ .onChange(async (value) => {
+ this.plugin.settings.contextMenuEnabled = value;
+ await this.plugin.saveSettings();
+ }));
+
+ new obsidian.Setting(containerEl)
+ .setName('Initial delay (ms)')
+ .setDesc('Delay before applying sort on startup. Increase if sorting fails to apply.')
+ .addText(text => text
+ .setValue(String(this.plugin.settings.delayForInitialApplication))
+ .onChange(async (value) => {
+ const num = parseInt(value, 10);
+ if (!isNaN(num) && num >= 0) {
+ this.plugin.settings.delayForInitialApplication = num;
+ await this.plugin.saveSettings();
+ }
+ }));
+
+ // Manual order management
+ containerEl.createEl('h3', { text: 'Manual Order' });
+
+ const manualKeys = Object.keys(this.plugin.settings.manualOrder);
+ if (manualKeys.length === 0) {
+ containerEl.createEl('p', {
+ text: 'No manual orders defined. Right-click a folder in the file explorer to set manual ordering.',
+ cls: 'setting-item-description'
+ });
+ } else {
+ for (const folder of manualKeys.sort()) {
+ const count = this.plugin.settings.manualOrder[folder].length;
+ new obsidian.Setting(containerEl)
+ .setName(folder || '/ (vault root)')
+ .setDesc(`${count} item(s) ordered`)
+ .addButton(btn => btn
+ .setButtonText('Clear')
+ .setWarning()
+ .onClick(async () => {
+ delete this.plugin.settings.manualOrder[folder];
+ await this.plugin.saveSettings();
+ this.plugin.refreshExplorer();
+ this.display(); // refresh settings view
+ }));
+ }
+ }
+
+ // Sort spec viewer
+ if (this.plugin.sortSpecs) {
+ containerEl.createEl('h3', { text: 'Active Sort Spec' });
+ const specFolders = Object.keys(this.plugin.sortSpecs);
+ containerEl.createEl('p', {
+ text: `${specFolders.length} folder rule(s) loaded from spec file.`,
+ cls: 'setting-item-description'
+ });
+ for (const f of specFolders.sort()) {
+ const groupCount = this.plugin.sortSpecs[f].groups ? this.plugin.sortSpecs[f].groups.length : 0;
+ containerEl.createEl('p', {
+ text: ` ${f || '/'}: ${groupCount} group(s)`,
+ cls: 'setting-item-description'
+ });
+ }
+ }
+ }
+}
+
+// Manual Order Modal─
+
+class ManualOrderModal extends obsidian.Modal {
+ constructor(app, plugin, folderPath, items) {
+ super(app);
+ this.plugin = plugin;
+ this.folderPath = folderPath;
+ this.items = items.map(i => getItemName(i));
+ this.orderList = [...this.items];
+
+ // If there's an existing manual order, apply it
+ const existing = plugin.settings.manualOrder[folderPath];
+ if (existing && existing.length > 0) {
+ const ordered = [];
+ const rest = [];
+ for (const name of existing) {
+ if (this.items.includes(name)) ordered.push(name);
+ }
+ for (const name of this.items) {
+ if (!ordered.includes(name)) rest.push(name);
+ }
+ this.orderList = ordered.concat(rest);
+ }
+ }
+
+ onOpen() {
+ const { contentEl } = this;
+ contentEl.empty();
+ contentEl.addClass('abacus-manual-order-modal');
+
+ contentEl.createEl('h2', { text: 'Manual Sort Order' });
+ contentEl.createEl('p', {
+ text: `Folder: ${this.folderPath || '/ (vault root)'}`,
+ cls: 'abacus-modal-subtitle'
+ });
+ contentEl.createEl('p', {
+ text: 'Drag items to reorder, or use the arrow buttons.',
+ cls: 'abacus-modal-hint'
+ });
+
+ this.listEl = contentEl.createEl('div', { cls: 'abacus-order-list' });
+ this.renderList();
+
+ const btnRow = contentEl.createEl('div', { cls: 'abacus-modal-buttons' });
+
+ const saveBtn = btnRow.createEl('button', { text: 'Save Order', cls: 'mod-cta' });
+ saveBtn.addEventListener('click', async () => {
+ this.plugin.settings.manualOrder[this.folderPath] = [...this.orderList];
+ await this.plugin.saveSettings();
+ this.plugin.refreshExplorer();
+ if (this.plugin.settings.notificationsEnabled) {
+ new obsidian.Notice(`Abacus: Manual order saved for ${this.folderPath || '/'}`);
+ }
+ this.close();
+ });
+
+ const clearBtn = btnRow.createEl('button', { text: 'Clear Order' });
+ clearBtn.addEventListener('click', async () => {
+ delete this.plugin.settings.manualOrder[this.folderPath];
+ await this.plugin.saveSettings();
+ this.plugin.refreshExplorer();
+ if (this.plugin.settings.notificationsEnabled) {
+ new obsidian.Notice(`Abacus: Manual order cleared for ${this.folderPath || '/'}`);
+ }
+ this.close();
+ });
+
+ const cancelBtn = btnRow.createEl('button', { text: 'Cancel' });
+ cancelBtn.addEventListener('click', () => this.close());
+ }
+
+ renderList() {
+ this.listEl.empty();
+ for (let i = 0; i < this.orderList.length; i++) {
+ const row = this.listEl.createEl('div', { cls: 'abacus-order-row' });
+ row.setAttribute('draggable', 'true');
+ row.dataset.index = String(i);
+
+ // Drag handle
+ const handle = row.createEl('span', { cls: 'abacus-drag-handle', text: '⠿' });
+
+ // Index number
+ row.createEl('span', { cls: 'abacus-order-index', text: String(i + 1) });
+
+ // Item name
+ row.createEl('span', { cls: 'abacus-order-name', text: this.orderList[i] });
+
+ // Up/down buttons
+ const btnContainer = row.createEl('span', { cls: 'abacus-order-buttons' });
+
+ if (i > 0) {
+ const upBtn = btnContainer.createEl('button', { cls: 'abacus-order-btn', text: '\u25B2' });
+ upBtn.addEventListener('click', () => this.moveItem(i, i - 1));
+ }
+ if (i < this.orderList.length - 1) {
+ const downBtn = btnContainer.createEl('button', { cls: 'abacus-order-btn', text: '\u25BC' });
+ downBtn.addEventListener('click', () => this.moveItem(i, i + 1));
+ }
+
+ // Drag events
+ row.addEventListener('dragstart', (e) => {
+ e.dataTransfer.setData('text/plain', String(i));
+ row.addClass('abacus-dragging');
+ });
+ row.addEventListener('dragend', () => {
+ row.removeClass('abacus-dragging');
+ });
+ row.addEventListener('dragover', (e) => {
+ e.preventDefault();
+ row.addClass('abacus-drag-over');
+ });
+ row.addEventListener('dragleave', () => {
+ row.removeClass('abacus-drag-over');
+ });
+ row.addEventListener('drop', (e) => {
+ e.preventDefault();
+ row.removeClass('abacus-drag-over');
+ const fromIndex = parseInt(e.dataTransfer.getData('text/plain'), 10);
+ if (!isNaN(fromIndex) && fromIndex !== i) {
+ this.moveItem(fromIndex, i);
+ }
+ });
+ }
+ }
+
+ moveItem(from, to) {
+ const item = this.orderList.splice(from, 1)[0];
+ this.orderList.splice(to, 0, item);
+ this.renderList();
+ }
+
+ onClose() {
+ this.contentEl.empty();
+ }
+}
+
+// Quick Sort Modal (apply a preset sort to a folder)
+
+class QuickSortModal extends obsidian.Modal {
+ constructor(app, plugin, folderPath, items) {
+ super(app);
+ this.plugin = plugin;
+ this.folderPath = folderPath;
+ this.items = items;
+ }
+
+ onOpen() {
+ const { contentEl } = this;
+ contentEl.empty();
+ contentEl.addClass('abacus-quick-sort-modal');
+
+ contentEl.createEl('h2', { text: 'Quick Sort' });
+ contentEl.createEl('p', {
+ text: `Apply a sort order to: ${this.folderPath || '/ (vault root)'}`,
+ cls: 'abacus-modal-subtitle'
+ });
+
+ const options = [
+ ['A \u2192 Z (natural)', 'natural-a-z'],
+ ['Z \u2192 A (natural)', 'natural-z-a'],
+ ['A \u2192 Z (strict)', 'a-z'],
+ ['Z \u2192 A (strict)', 'z-a'],
+ ['Newest modified first', 'modified-new'],
+ ['Oldest modified first', 'modified-old'],
+ ['Newest created first', 'created-new'],
+ ['Oldest created first', 'created-old'],
+ ['Folders first', 'folders-first'],
+ ['Files first', 'files-first'],
+ ];
+
+ const list = contentEl.createEl('div', { cls: 'abacus-quick-sort-list' });
+
+ for (const [label, orderKey] of options) {
+ const btn = list.createEl('button', { text: label, cls: 'abacus-quick-sort-option' });
+ btn.addEventListener('click', async () => {
+ const order = SORT_ORDERS[orderKey];
+ if (order) {
+ const cmp = makeComparator(order, this.app, null);
+ if (cmp) {
+ const sorted = [...this.items].sort((a, b) => {
+ return cmp(a, b);
+ });
+ const names = sorted.map(i => getItemName(i));
+ this.plugin.settings.manualOrder[this.folderPath] = names;
+ await this.plugin.saveSettings();
+ this.plugin.refreshExplorer();
+ if (this.plugin.settings.notificationsEnabled) {
+ new obsidian.Notice(`Abacus: Sorted ${this.folderPath || '/'} by ${label}`);
+ }
+ }
+ }
+ this.close();
+ });
+ }
+
+ const cancelBtn = contentEl.createEl('button', { text: 'Cancel', cls: 'abacus-quick-sort-cancel' });
+ cancelBtn.addEventListener('click', () => this.close());
+ }
+
+ onClose() {
+ this.contentEl.empty();
+ }
+}
+
+// Main Plugin Class
+
+class AbacusSorterPlugin extends obsidian.Plugin {
+ async onload() {
+ await this.loadSettings();
+
+ this.sortSpecs = null;
+ this.originalSort = null;
+ this.fileExplorer = null;
+
+ // Add settings tab
+ this.addSettingTab(new AbacusSettingTab(this.app, this));
+
+ // Ribbon icon
+ this.ribbonIconEl = this.addRibbonIcon('arrow-up-down', 'Abacus Custom Sorter', () => {
+ this.settings.suspended = !this.settings.suspended;
+ this.saveSettings();
+ this.refreshExplorer();
+ this.updateStatusBar();
+ this.updateRibbonIcon();
+ if (this.settings.notificationsEnabled) {
+ new obsidian.Notice(`Abacus: Sorting ${this.settings.suspended ? 'suspended' : 'active'}`);
+ }
+ });
+ this.updateRibbonIcon();
+
+ // Status bar
+ this.statusBarEl = this.addStatusBarItem();
+ this.updateStatusBar();
+
+ // Context menu
+ this.registerEvent(
+ this.app.workspace.on('file-menu', (menu, file, source) => {
+ if (!this.settings.contextMenuEnabled) return;
+ if (source !== 'file-explorer-context-menu') return;
+
+ const folderPath = file.children !== undefined ? file.path : (file.parent ? file.parent.path : '');
+
+ menu.addSeparator();
+
+ menu.addItem((item) => {
+ item.setTitle('Abacus: Set manual order...')
+ .setIcon('arrow-up-down')
+ .onClick(() => {
+ this.openManualOrderModal(folderPath);
+ });
+ });
+
+ menu.addItem((item) => {
+ item.setTitle('Abacus: Quick sort...')
+ .setIcon('sort-asc')
+ .onClick(() => {
+ this.openQuickSortModal(folderPath);
+ });
+ });
+
+ if (this.settings.manualOrder[folderPath]) {
+ menu.addItem((item) => {
+ item.setTitle('Abacus: Clear manual order')
+ .setIcon('x')
+ .onClick(async () => {
+ delete this.settings.manualOrder[folderPath];
+ await this.saveSettings();
+ this.refreshExplorer();
+ if (this.settings.notificationsEnabled) {
+ new obsidian.Notice(`Abacus: Manual order cleared for ${folderPath || '/'}`);
+ }
+ });
+ });
+ }
+
+ menu.addItem((item) => {
+ item.setTitle(this.settings.suspended ? 'Abacus: Resume sorting' : 'Abacus: Suspend sorting')
+ .setIcon(this.settings.suspended ? 'play' : 'pause')
+ .onClick(async () => {
+ this.settings.suspended = !this.settings.suspended;
+ await this.saveSettings();
+ this.refreshExplorer();
+ this.updateStatusBar();
+ this.updateRibbonIcon();
+ });
+ });
+ })
+ );
+
+ // Wait for layout ready, then patch
+ this.app.workspace.onLayoutReady(() => {
+ const delay = this.settings.delayForInitialApplication || 0;
+ if (delay > 0) {
+ setTimeout(() => this.initializeSorting(), delay);
+ } else {
+ this.initializeSorting();
+ }
+ });
+
+ // Watch for file changes to refresh
+ this.registerEvent(this.app.vault.on('rename', () => this.debouncedRefresh()));
+ this.registerEvent(this.app.vault.on('create', () => this.debouncedRefresh()));
+ this.registerEvent(this.app.vault.on('delete', () => this.debouncedRefresh()));
+
+ // Commands
+ this.addCommand({
+ id: 'toggle-sorting',
+ name: 'Toggle custom sorting',
+ callback: () => {
+ this.settings.suspended = !this.settings.suspended;
+ this.saveSettings();
+ this.refreshExplorer();
+ this.updateStatusBar();
+ this.updateRibbonIcon();
+ if (this.settings.notificationsEnabled) {
+ new obsidian.Notice(`Abacus: Sorting ${this.settings.suspended ? 'suspended' : 'active'}`);
+ }
+ }
+ });
+
+ this.addCommand({
+ id: 'reload-sort-spec',
+ name: 'Reload sort specification',
+ callback: async () => {
+ await this.loadSortSpec();
+ this.refreshExplorer();
+ new obsidian.Notice('Abacus: Sort spec reloaded');
+ }
+ });
+ }
+
+ async onunload() {
+ unpatchFileExplorer(this);
+ // Trigger a re-sort so Obsidian reverts to its default order
+ const explorer = getFileExplorer(this.app);
+ if (explorer && typeof explorer.requestSort === 'function') {
+ explorer.requestSort();
+ }
+ }
+
+ async loadSettings() {
+ this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
+ // Ensure manualOrder exists
+ if (!this.settings.manualOrder) this.settings.manualOrder = {};
+ }
+
+ async saveSettings() {
+ await this.saveData(this.settings);
+ }
+
+ async initializeSorting() {
+ await this.loadSortSpec();
+ const patched = patchFileExplorer(this);
+ if (patched) {
+ this.refreshExplorer();
+ if (this.settings.notificationsEnabled) {
+ new obsidian.Notice('Abacus Custom Sorter: Active');
+ }
+ } else {
+ // Retry once after a short delay
+ setTimeout(() => {
+ const retry = patchFileExplorer(this);
+ if (retry) {
+ this.refreshExplorer();
+ if (this.settings.notificationsEnabled) {
+ new obsidian.Notice('Abacus Custom Sorter: Active');
+ }
+ }
+ }, 1000);
+ }
+ }
+
+ async loadSortSpec() {
+ this.sortSpecs = null;
+ const specPath = this.settings.sortSpecFile;
+ if (!specPath) return;
+
+ try {
+ const file = this.app.vault.getAbstractFileByPath(specPath);
+ if (file && file instanceof obsidian.TFile) {
+ const content = await this.app.vault.cachedRead(file);
+ this.sortSpecs = parseSortSpec(content);
+ }
+ } catch (e) {
+ console.error('Abacus: Failed to load sort spec:', e);
+ }
+ }
+
+ getSpecForFolder(folderPath) {
+ if (!this.sortSpecs) return null;
+
+ // Exact match
+ if (this.sortSpecs[folderPath]) return this.sortSpecs[folderPath];
+
+ // Check wildcards: try parent paths with /*
+ const parts = folderPath.split('/');
+ for (let i = parts.length - 1; i >= 0; i--) {
+ const partial = parts.slice(0, i).join('/') + '/*';
+ if (this.sortSpecs[partial]) return this.sortSpecs[partial];
+ }
+
+ // Global wildcard
+ if (this.sortSpecs['/*']) return this.sortSpecs['/*'];
+ if (this.sortSpecs['*']) return this.sortSpecs['*'];
+
+ return null;
+ }
+
+ openManualOrderModal(folderPath) {
+ // Get the TFolder and its children directly from the vault
+ const folder = this.app.vault.getAbstractFileByPath(folderPath);
+ if (!folder || !folder.children) {
+ new obsidian.Notice('Abacus: Folder not found');
+ return;
+ }
+
+ // Build item wrappers that match what getSortedFolderItems returns
+ // Each needs a .file property pointing to the TAbstractFile
+ const items = folder.children.map(child => ({ file: child }));
+
+ if (items.length === 0) {
+ new obsidian.Notice('Abacus: No items found in folder');
+ return;
+ }
+
+ // Sort by current display order (natural sort as baseline)
+ items.sort((a, b) => naturalCompare(getItemName(a), getItemName(b)));
+
+ new ManualOrderModal(this.app, this, folderPath, items).open();
+ }
+
+ openQuickSortModal(folderPath) {
+ const folder = this.app.vault.getAbstractFileByPath(folderPath);
+ if (!folder || !folder.children) {
+ new obsidian.Notice('Abacus: Folder not found');
+ return;
+ }
+
+ const items = folder.children.map(child => ({ file: child }));
+
+ if (items.length === 0) {
+ new obsidian.Notice('Abacus: No items found in folder');
+ return;
+ }
+
+ new QuickSortModal(this.app, this, folderPath, items).open();
+ }
+
+ refreshExplorer() {
+ // requestSort() tells the file explorer to re-call getSortedFolderItems
+ // for all visible folders, which triggers our patched version
+ const explorer = getFileExplorer(this.app);
+ if (explorer && typeof explorer.requestSort === 'function') {
+ explorer.requestSort();
+ }
+ }
+
+ _refreshTimeout = null;
+ debouncedRefresh() {
+ if (this._refreshTimeout) clearTimeout(this._refreshTimeout);
+ this._refreshTimeout = setTimeout(() => {
+ this.refreshExplorer();
+ }, 300);
+ }
+
+ updateStatusBar() {
+ if (!this.statusBarEl) return;
+ if (!this.settings.statusBarEnabled) {
+ this.statusBarEl.setText('');
+ return;
+ }
+ const manualCount = Object.keys(this.settings.manualOrder).length;
+ const specCount = this.sortSpecs ? Object.keys(this.sortSpecs).length : 0;
+ const state = this.settings.suspended ? 'OFF' : 'ON';
+ let text = `Abacus: ${state}`;
+ if (!this.settings.suspended && (manualCount > 0 || specCount > 0)) {
+ const parts = [];
+ if (specCount > 0) parts.push(`${specCount} spec`);
+ if (manualCount > 0) parts.push(`${manualCount} manual`);
+ text += ` (${parts.join(', ')})`;
+ }
+ this.statusBarEl.setText(text);
+ }
+
+ updateRibbonIcon() {
+ if (!this.ribbonIconEl) return;
+ if (this.settings.suspended) {
+ this.ribbonIconEl.addClass('abacus-ribbon-suspended');
+ this.ribbonIconEl.setAttribute('aria-label', 'Abacus Custom Sorter (suspended)');
+ } else {
+ this.ribbonIconEl.removeClass('abacus-ribbon-suspended');
+ this.ribbonIconEl.setAttribute('aria-label', 'Abacus Custom Sorter (active)');
+ }
+ }
+}
+
+module.exports = AbacusSorterPlugin;
diff --git a/.obsidian/plugins/abacus-sorter/manifest.json b/.obsidian/plugins/abacus-sorter/manifest.json
new file mode 100644
index 0000000..1d35c98
--- /dev/null
+++ b/.obsidian/plugins/abacus-sorter/manifest.json
@@ -0,0 +1,9 @@
+{
+ "id": "abacus-sorter",
+ "name": "Abacus Custom Sorter",
+ "version": "1.0.0",
+ "minAppVersion": "1.7.2",
+ "description": "Config-driven custom sorting of files and folders in File Explorer, with manual drag-style reordering and sort specification support.",
+ "author": "Conway",
+ "isDesktopOnly": false
+}
diff --git a/.obsidian/plugins/abacus-sorter/styles.css b/.obsidian/plugins/abacus-sorter/styles.css
new file mode 100644
index 0000000..599055a
--- /dev/null
+++ b/.obsidian/plugins/abacus-sorter/styles.css
@@ -0,0 +1,139 @@
+/* Abacus Custom Sorter */
+
+/* Ribbon icon when suspended */
+.abacus-ribbon-suspended {
+ opacity: 0.4;
+}
+
+/* Manual Order Modal */
+.abacus-manual-order-modal {
+ max-width: 500px;
+}
+
+.abacus-modal-subtitle {
+ color: var(--text-muted);
+ margin-bottom: 4px;
+}
+
+.abacus-modal-hint {
+ color: var(--text-faint);
+ font-size: var(--font-smallest);
+ margin-bottom: 12px;
+}
+
+.abacus-order-list {
+ max-height: 400px;
+ overflow-y: auto;
+ border: 1px solid var(--background-modifier-border);
+ border-radius: 6px;
+ margin-bottom: 12px;
+}
+
+.abacus-order-row {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 6px 10px;
+ border-bottom: 1px solid var(--background-modifier-border);
+ cursor: grab;
+ transition: background-color 0.1s;
+}
+
+.abacus-order-row:last-child {
+ border-bottom: none;
+}
+
+.abacus-order-row:hover {
+ background-color: var(--background-modifier-hover);
+}
+
+.abacus-order-row.abacus-dragging {
+ opacity: 0.4;
+}
+
+.abacus-order-row.abacus-drag-over {
+ background-color: var(--interactive-accent);
+ color: var(--text-on-accent);
+}
+
+.abacus-drag-handle {
+ color: var(--text-faint);
+ cursor: grab;
+ user-select: none;
+ font-size: 14px;
+ flex-shrink: 0;
+}
+
+.abacus-order-index {
+ color: var(--text-muted);
+ font-size: var(--font-smallest);
+ min-width: 20px;
+ text-align: right;
+ flex-shrink: 0;
+}
+
+.abacus-order-name {
+ flex: 1;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.abacus-order-buttons {
+ display: flex;
+ gap: 2px;
+ flex-shrink: 0;
+}
+
+.abacus-order-btn {
+ background: none;
+ border: none;
+ color: var(--text-muted);
+ cursor: pointer;
+ padding: 2px 4px;
+ font-size: 10px;
+ border-radius: 3px;
+}
+
+.abacus-order-btn:hover {
+ background-color: var(--background-modifier-hover);
+ color: var(--text-normal);
+}
+
+.abacus-modal-buttons {
+ display: flex;
+ gap: 8px;
+ justify-content: flex-end;
+}
+
+/* Quick Sort Modal */
+.abacus-quick-sort-modal {
+ max-width: 350px;
+}
+
+.abacus-quick-sort-list {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ margin-bottom: 12px;
+}
+
+.abacus-quick-sort-option {
+ text-align: left;
+ padding: 8px 12px;
+ border: 1px solid var(--background-modifier-border);
+ border-radius: 6px;
+ background: var(--background-secondary);
+ cursor: pointer;
+ transition: background-color 0.1s;
+}
+
+.abacus-quick-sort-option:hover {
+ background-color: var(--interactive-accent);
+ color: var(--text-on-accent);
+}
+
+.abacus-quick-sort-cancel {
+ margin-top: 4px;
+ align-self: flex-end;
+}
diff --git a/.obsidian/themes/Spectroscope-Gruv/manifest.json b/.obsidian/themes/Spectroscope-Gruv/manifest.json
new file mode 100644
index 0000000..4655e59
--- /dev/null
+++ b/.obsidian/themes/Spectroscope-Gruv/manifest.json
@@ -0,0 +1,9 @@
+{
+ "name": "Spectroscope-Gruv",
+ "version": "1.0.0",
+ "minAppVersion": "0.16.0",
+ "author": "Conway",
+ "authorUrl": "",
+ "id": "Spectroscope-Gruv",
+ "isDesktopOnly": false
+}
diff --git a/.obsidian/themes/Spectroscope-Gruv/theme.css b/.obsidian/themes/Spectroscope-Gruv/theme.css
new file mode 100644
index 0000000..5fe67ec
--- /dev/null
+++ b/.obsidian/themes/Spectroscope-Gruv/theme.css
@@ -0,0 +1,473 @@
+/*
+Spectroscope-Gruv is based on Gruvbox Darker, with darker backgrounds from Minimal-Dark-Coder. Zero light mode support.
+*/
+
+:root
+{
+ /* DARKENED BACKGROUND SCALE - shifted down to match Minimal-Dark-Coder depth */
+ --dark0-hard_x: 24,26,27; /* #181a1b - matches Minimal-Dark-Coder */
+ --dark0-hard: rgb(var(--dark0-hard_x));
+ --dark0_x: 32,32,32; /* #202020 - was #282828 */
+ --dark0: rgb(var(--dark0_x));
+ --dark0-soft_x: 40,38,37; /* #282625 - was #32302f */
+ --dark0-soft: rgb(var(--dark0-soft_x));
+ --dark1_x: 50,46,44; /* #322e2c - was #3c3836 */
+ --dark1: rgb(var(--dark1_x));
+ --dark2_x: 70,63,59; /* #463f3b - was #504945 */
+ --dark2: rgb(var(--dark2_x));
+ --dark3_x: 92,82,74; /* #5c524a - was #665c54 */
+ --dark3: rgb(var(--dark3_x));
+ --dark4_x: 114,101,90; /* #72655a - was #7c6f64 */
+ --dark4: rgb(var(--dark4_x));
+ --gray_x: 146,131,116; /* #928374 - unchanged */
+ --gray: rgb(var(--gray_x));
+
+ /* LIGHT SCALE - unchanged, used for text */
+ --light0-hard_x: 249,245,215; /* #f9f5d7 */
+ --light0-hard: rgb(var(--light0-hard_x));
+ --light0_x: 251,241,199; /* #fbf1c7 */
+ --light0: rgb(var(--light0_x));
+ --light0-soft_x: 242,229,188; /* #f2e5bc */
+ --light0-soft: rgb(var(--light0-soft_x));
+ --light1_x: 235,219,178; /* #ebdbb2 */
+ --light1: rgb(var(--light1_x));
+ --light2_x: 213,196,161; /* #d5c4a1 */
+ --light2: rgb(var(--light2_x));
+ --light3_x: 189,174,147; /* #bdae93 */
+ --light3: rgb(var(--light3_x));
+ --light4_x: 168,153,132; /* #a89984 */
+ --light4: rgb(var(--light4_x));
+
+ /* BRIGHT COLORS - unchanged */
+ --bright-red_x: 251,73,52; /* #fb4934 */
+ --bright-red: rgb(var(--bright-red_x));
+ --bright-green_x: 184,187,38; /* #b8bb26 */
+ --bright-green: rgb(var(--bright-green_x));
+ --bright-yellow_x: 250,189,47; /* #fabd2f */
+ --bright-yellow: rgb(var(--bright-yellow_x));
+ --bright-blue_x: 131,165,152; /* #83a598 */
+ --bright-blue: rgb(var(--bright-blue_x));
+ --bright-purple_x: 211,134,155; /* #d3869b */
+ --bright-purple: rgb(var(--bright-purple_x));
+ --bright-aqua_x: 142,192,124; /* #8ec07c */
+ --bright-aqua: rgb(var(--bright-aqua_x));
+ --bright-orange_x: 254,128,25; /* #fe8019 */
+ --bright-orange: rgb(var(--bright-orange_x));
+
+ /* NEUTRAL COLORS - unchanged */
+ --neutral-red_x: 204,36,29; /* #cc241d */
+ --neutral-red: rgb(var(--neutral-red_x));
+ --neutral-green_x: 152,151,26; /* #98971a */
+ --neutral-green: rgb(var(--neutral-green_x));
+ --neutral-yellow_x: 215,153,33; /* #d79921 */
+ --neutral-yellow: rgb(var(--neutral-yellow_x));
+ --neutral-blue_x: 69,133,136; /* #458588 */
+ --neutral-blue: rgb(var(--neutral-blue_x));
+ --neutral-purple_x: 177,98,134; /* #b16286 */
+ --neutral-purple: rgb(var(--neutral-purple_x));
+ --neutral-aqua_x: 104,157,106; /* #689d6a */
+ --neutral-aqua: rgb(var(--neutral-aqua_x));
+ --neutral-orange_x: 214,93,14; /* #d65d0e */
+ --neutral-orange: rgb(var(--neutral-orange_x));
+
+ /* FADED COLORS - unchanged */
+ --faded-red_x: 157,0,6; /* #9d0006 */
+ --faded-red: rgb(var(--faded-red_x));
+ --faded-green_x: 121,116,14; /* #79740e */
+ --faded-green: rgb(var(--faded-green_x));
+ --faded-yellow_x: 181,118,20; /* #b57614 */
+ --faded-yellow: rgb(var(--faded-yellow_x));
+ --faded-blue_x: 7,102,120; /* #076678 */
+ --faded-blue: rgb(var(--faded-blue_x));
+ --faded-purple_x: 143,63,113; /* #8f3f71 */
+ --faded-purple: rgb(var(--faded-purple_x));
+ --faded-aqua_x: 66,123,88; /* #427b58 */
+ --faded-aqua: rgb(var(--faded-aqua_x));
+ --faded-orange_x: 175,58,3; /* #af3a03 */
+ --faded-orange: rgb(var(--faded-orange_x));
+}
+
+body
+{
+ --accent-h: 12; /* --faded-red #9d0006 */
+ --accent-s: 107%;
+ --accent-l: 32%;
+
+ --link-decoration: none;
+ --link-decoration-hover: none;
+ --link-external-decoration: none;
+ --link-external-decoration-hover: none;
+
+ --tag-decoration: none;
+ --tag-decoration-hover: underline;
+ --tag-padding-x: .5em;
+ --tag-padding-y: .2em;
+ --tag-radius: .5em;
+
+ --tab-font-weight: 600;
+ --bold-weight: 600;
+
+ --checkbox-radius: 0;
+
+ --embed-border-left: 6px double var(--interactive-accent);
+}
+
+.theme-dark
+{
+ --color-red-rgb: var(--neutral-red_x);
+ --color-red: var(--neutral-red);
+ --color-purple-rgb: var(--neutral-purple_x);
+ --color-purple: var(--neutral-purple);
+ --color-green-rgb: var(--neutral-green_x);
+ --color-green: var(--neutral-green);
+ --color-cyan-rgb: var(--neutral-blue_x);
+ --color-cyan: var(--neutral-blue);
+ --color-blue-rgb: var(--faded-blue_x);
+ --color-blue: var(--faded-blue);
+ --color-yellow-rgb: var(--neutral-yellow_x);
+ --color-yellow: var(--neutral-yellow);
+ --color-orange-rgb: var(--neutral-orange_x);
+ --color-orange: var(--neutral-orange);
+ --color-pink-rgb: var(--bright-purple_x);
+ --color-pink: var(--bright-purple);
+
+ --background-primary: var(--dark0);
+ --background-primary-alt: var(--dark0);
+ --background-secondary: var(--dark0-hard);
+ --background-secondary-alt: var(--dark1);
+ --background-modifier-border: var(--dark1);
+
+ /* Adjusted for darker backgrounds - subtle highlight */
+ --cursor-line-background: rgba(var(--dark1_x), 0.4);
+
+ --text-normal: var(--light0);
+ --text-faint: var(--light1);
+ --text-muted: var(--light2);
+
+ --link-url: var(--neutral-green);
+
+ /* Header colors - rainbow progression */
+ --h1-color: var(--neutral-red);
+ --h2-color: var(--neutral-yellow);
+ --h3-color: var(--neutral-green);
+ --h4-color: var(--neutral-aqua);
+ --h5-color: var(--neutral-blue);
+ --h6-color: var(--neutral-purple);
+
+ --text-highlight-bg: var(--neutral-yellow);
+ --text-highlight-fg: var(--dark0-hard);
+
+ --text-accent: var(--neutral-orange);
+ --text-accent-hover: var(--bright-aqua);
+
+ --tag-color: var(--bright-aqua);
+ --tag-background: var(--dark2);
+ --tag-background-hover: var(--dark1);
+
+ --titlebar-text-color-focused: var(--bright-red);
+
+ --inline-title-color: var(--bright-yellow);
+
+ --bold-color: var(--neutral-yellow);
+ --italic-color: var(--light4);
+
+ --checkbox-color: var(--light4);
+ --checkbox-color-hover: var(--light4);
+ --checkbox-border-color: var(--light4);
+ --checkbox-border-color-hover: var(--light4);
+ --checklist-done-color: rgba(var(--light2_x), 0.5);
+
+ --table-header-background: rgba(var(--dark0_x), 0.2);
+ --table-header-background-hover: var(--dark2);
+ --table-row-even-background: rgba(var(--dark2_x), 0.2);
+ --table-row-odd-background: rgba(var(--dark2_x), 0.4);
+ --table-row-background-hover: var(--dark2);
+
+ --text-selection: rgba(var(--neutral-red_x), 0.6);
+ --flashing-background: rgba(var(--neutral-red_x), 0.3);
+
+ --code-normal: var(--bright-blue);
+ --code-background: var(--dark1);
+
+ --mermaid-note: var(--neutral-blue);
+ --mermaid-actor: var(--dark2);
+ --mermaid-loopline: var(--neutral-blue);
+ --mermaid-exclude: var(--dark4);
+ --mermaid-seqnum: var(--dark0);
+
+ --icon-color-hover: var(--bright-red);
+ --icon-color-focused: var(--bright-blue);
+
+ --nav-item-color-hover: var(--bright-red);
+ --nav-item-color-active: var(--bright-aqua);
+ --nav-file-tag: rgba(var(--neutral-yellow_x), 0.9);
+
+ --graph-line: var(--dark2);
+ --graph-node: var(--light3);
+ --graph-node-tag: var(--neutral-red);
+ --graph-node-attachment: var(--neutral-green);
+
+ --calendar-hover: var(--bright-red);
+ --calendar-background-hover: var(--dark1);
+ --calendar-week: var(--neutral-orange);
+ --calendar-today: var(--neutral-orange);
+
+ --dataview-key: var(--text-faint);
+ --dataview-key-background: rgba(var(--faded-red_x), 0.5);
+ --dataview-value: var(--text-faint);
+ --dataview-value-background: rgba(var(--neutral-green_x), 0.3);
+
+ --tab-text-color-focused-active: var(--neutral-yellow);
+ --tab-text-color-focused-active-current: var(--bright-red);
+}
+
+/* TABLE STYLING */
+table
+{
+ border: 1px solid var(--background-secondary) !important;
+ border-collapse: collapse;
+}
+
+thead
+{
+ border-bottom: 2px solid var(--background-modifier-border) !important;
+}
+
+th
+{
+ font-weight: 600 !important;
+ border: 1px solid var(--background-secondary) !important;
+}
+
+td
+{
+ border-left: 1px solid var(--background-secondary) !important;
+ border-right: 1px solid var(--background-secondary) !important;
+ border-bottom: 1px solid var(--background-secondary) !important;
+}
+
+.markdown-rendered tbody tr:nth-child(even)
+{
+ background-color: var(--table-row-even-background) !important;
+}
+
+.markdown-rendered tbody tr:nth-child(odd)
+{
+ background-color: var(--table-row-odd-background) !important;
+}
+
+.markdown-rendered tbody tr:nth-child(even):hover,
+.markdown-rendered tbody tr:nth-child(odd):hover
+{
+ background-color: var(--table-row-background-hover) !important;
+}
+
+/* HIGHLIGHT/MARK STYLING */
+.markdown-rendered mark
+{
+ background-color: var(--text-highlight-bg);
+ color: var(--text-highlight-fg);
+}
+
+.markdown-rendered mark a
+{
+ color: var(--red) !important;
+ font-weight: 600;
+}
+
+.search-result-file-matched-text
+{
+ color: var(--text-highlight-fg) !important;
+}
+
+/* TAG HOVER */
+.cm-hashtag-begin:hover, .cm-hashtag-end:hover
+{
+ color: var(--text-accent);
+ text-decoration: underline;
+}
+
+/* CHECKBOX STYLING */
+input[type=checkbox]
+{
+ border: 1px solid var(--checkbox-color);
+}
+
+input[type=checkbox]:checked
+{
+ background-color: var(--checkbox-color);
+ box-shadow: inset 0 0 0 2px var(--background-primary);
+}
+
+input[type=checkbox]:checked:after
+{
+ display: none;
+}
+
+/* CODE BLOCKS */
+code[class*="language-"],
+pre[class*="language-"]
+{
+ line-height: var(--line-height-tight) !important;
+}
+
+/* URL/LINK STYLING */
+.cm-url
+{
+ color: var(--link-url) !important;
+}
+
+.cm-url:hover
+{
+ color: var(--text-accent-color) !important;
+}
+
+/* EDITOR-PREVIEW CONSISTENCY */
+.cm-highlight
+{
+ color: var(--text-highlight-fg) !important;
+}
+
+.cm-inline-code
+{
+ border-radius: var(--radius-s);
+ font-size: var(--code-size);
+ padding: 0.1em 0.25em;
+}
+
+.cm-line .cm-strong
+{
+ color: var(--bold-color) !important;
+}
+
+/* MERMAID DIAGRAMS */
+.mermaid .note
+{
+ fill: var(--mermaid-note) !important;
+}
+
+.mermaid .actor
+{
+ fill: var(--mermaid-actor) !important;
+}
+
+.mermaid .loopLine
+{
+ stroke: var(--mermaid-loopline) !important;
+}
+
+.mermaid .loopText>tspan,
+.mermaid .entityLabel
+{
+ fill: var(--neutral-red) !important;
+}
+
+.mermaid .exclude-range
+{
+ fill: var(--mermaid-exclude) !important;
+}
+
+.mermaid .sequenceNumber
+{
+ fill: var(--mermaid-seqnum) !important;
+}
+
+/* CALENDAR PLUGIN */
+.calendar .week-num
+{
+ color: var(--calendar-week) !important;
+}
+
+.calendar .today
+{
+ color: var(--calendar-today) !important;
+}
+
+.calendar .week-num:hover,
+.calendar .day:hover
+{
+ color: var(--calendar-hover) !important;
+ background-color: var(--calendar-background-hover) !important;
+}
+
+/* EMBEDS */
+.markdown-embed-title
+{
+ color: var(--yellow);
+ font-weight: 600 !important;
+}
+
+/* ACTIVE LINE */
+.cm-active
+{
+ background-color: var(--cursor-line-background) !important;
+}
+
+/* FILE EXPLORER */
+.nav-file-tag
+{
+ color: var(--nav-file-tag) !important;
+}
+
+.is-flashing
+{
+ background-color: var(--flashing-background) !important;
+}
+
+/* DATAVIEW PLUGIN */
+.dataview.inline-field-key
+{
+ border-top-left-radius: var(--radius-s);
+ border-bottom-left-radius: var(--radius-s);
+ padding-left: 4px;
+ font-family: var(--font-monospace);
+ font-size: var(--font-smaller);
+ color: var(--dataview-key) !important;
+ background-color: var(--dataview-key-background) !important;
+}
+
+.dataview.inline-field-value
+{
+ border-top-right-radius: var(--radius-s);
+ border-bottom-right-radius: var(--radius-s);
+ padding-right: 4px;
+ font-family: var(--font-monospace);
+ font-size: var(--font-smaller);
+ color: var(--dataview-value) !important;
+ background-color: var(--dataview-value-background) !important;
+}
+
+/* SUGGESTION POPUP */
+.suggestion-highlight
+{
+ color: var(--bright-red);
+}
+
+/* CALLOUTS */
+body {
+ --callout-border-width: 1px;
+ --callout-border-opacity: 0.4;
+ --callout-default: var(--neutral-blue_x);
+ --callout-note: var(--neutral-blue_x);
+ --callout-summary: var(--neutral-aqua_x);
+ --callout-info: var(--neutral-blue_x);
+ --callout-todo: var(--neutral-blue_x);
+ --callout-important: var(--neutral-aqua_x);
+ --callout-tip: var(--neutral-aqua_x);
+ --callout-success: var(--neutral-green_x);
+ --callout-question: var(--neutral-yellow_x);
+ --callout-warning: var(--neutral-orange_x);
+ --callout-fail: var(--neutral-red_x);
+ --callout-error: var(--neutral-red_x);
+ --callout-bug: var(--neutral-red_x);
+ --callout-example: var(--neutral-purple_x);
+ --callout-quote: var(--gray_x);
+}
+
+/*
+
+.callout {
+ background-color: rgba(var(--callout-color), 0.2);
+}
+
+/* */
diff --git a/.obsidian/themes/Spectroscope-Noir/manifest.json b/.obsidian/themes/Spectroscope-Noir/manifest.json
new file mode 100644
index 0000000..45ffdd2
--- /dev/null
+++ b/.obsidian/themes/Spectroscope-Noir/manifest.json
@@ -0,0 +1,9 @@
+{
+ "name": "Spectroscope-Noir",
+ "version": "1.0.0",
+ "minAppVersion": "0.16.0",
+ "author": "Conway",
+ "authorUrl": "",
+ "id": "Spectroscope-Noir",
+ "isDesktopOnly": false
+}
diff --git a/.obsidian/themes/Spectroscope-Noir/theme.css b/.obsidian/themes/Spectroscope-Noir/theme.css
new file mode 100644
index 0000000..bdc423e
--- /dev/null
+++ b/.obsidian/themes/Spectroscope-Noir/theme.css
@@ -0,0 +1,474 @@
+/*
+Spectroscope-Noir is based on Spectroscope-Gruv, with pure black backgrounds inspired by the Blackbird theme.
+Still zero light mode support.
+*/
+
+:root
+{
+ /* PURE BLACK BACKGROUND SCALE - neutral, no warm tint */
+ --dark0-hard_x: 0,0,0; /* #000000 - pure black */
+ --dark0-hard: rgb(var(--dark0-hard_x));
+ --dark0_x: 10,10,10; /* #0a0a0a - near black */
+ --dark0: rgb(var(--dark0_x));
+ --dark0-soft_x: 18,18,18; /* #121212 */
+ --dark0-soft: rgb(var(--dark0-soft_x));
+ --dark1_x: 26,26,26; /* #1a1a1a */
+ --dark1: rgb(var(--dark1_x));
+ --dark2_x: 37,37,37; /* #252525 */
+ --dark2: rgb(var(--dark2_x));
+ --dark3_x: 51,51,51; /* #333333 */
+ --dark3: rgb(var(--dark3_x));
+ --dark4_x: 68,68,68; /* #444444 */
+ --dark4: rgb(var(--dark4_x));
+ --gray_x: 146,131,116; /* #928374 - unchanged */
+ --gray: rgb(var(--gray_x));
+
+ /* LIGHT SCALE - unchanged, used for text */
+ --light0-hard_x: 249,245,215; /* #f9f5d7 */
+ --light0-hard: rgb(var(--light0-hard_x));
+ --light0_x: 251,241,199; /* #fbf1c7 */
+ --light0: rgb(var(--light0_x));
+ --light0-soft_x: 242,229,188; /* #f2e5bc */
+ --light0-soft: rgb(var(--light0-soft_x));
+ --light1_x: 235,219,178; /* #ebdbb2 */
+ --light1: rgb(var(--light1_x));
+ --light2_x: 213,196,161; /* #d5c4a1 */
+ --light2: rgb(var(--light2_x));
+ --light3_x: 189,174,147; /* #bdae93 */
+ --light3: rgb(var(--light3_x));
+ --light4_x: 168,153,132; /* #a89984 */
+ --light4: rgb(var(--light4_x));
+
+ /* BRIGHT COLORS - unchanged */
+ --bright-red_x: 251,73,52; /* #fb4934 */
+ --bright-red: rgb(var(--bright-red_x));
+ --bright-green_x: 184,187,38; /* #b8bb26 */
+ --bright-green: rgb(var(--bright-green_x));
+ --bright-yellow_x: 250,189,47; /* #fabd2f */
+ --bright-yellow: rgb(var(--bright-yellow_x));
+ --bright-blue_x: 131,165,152; /* #83a598 */
+ --bright-blue: rgb(var(--bright-blue_x));
+ --bright-purple_x: 211,134,155; /* #d3869b */
+ --bright-purple: rgb(var(--bright-purple_x));
+ --bright-aqua_x: 142,192,124; /* #8ec07c */
+ --bright-aqua: rgb(var(--bright-aqua_x));
+ --bright-orange_x: 254,128,25; /* #fe8019 */
+ --bright-orange: rgb(var(--bright-orange_x));
+
+ /* NEUTRAL COLORS - unchanged */
+ --neutral-red_x: 204,36,29; /* #cc241d */
+ --neutral-red: rgb(var(--neutral-red_x));
+ --neutral-green_x: 152,151,26; /* #98971a */
+ --neutral-green: rgb(var(--neutral-green_x));
+ --neutral-yellow_x: 215,153,33; /* #d79921 */
+ --neutral-yellow: rgb(var(--neutral-yellow_x));
+ --neutral-blue_x: 69,133,136; /* #458588 */
+ --neutral-blue: rgb(var(--neutral-blue_x));
+ --neutral-purple_x: 177,98,134; /* #b16286 */
+ --neutral-purple: rgb(var(--neutral-purple_x));
+ --neutral-aqua_x: 104,157,106; /* #689d6a */
+ --neutral-aqua: rgb(var(--neutral-aqua_x));
+ --neutral-orange_x: 214,93,14; /* #d65d0e */
+ --neutral-orange: rgb(var(--neutral-orange_x));
+
+ /* FADED COLORS - unchanged */
+ --faded-red_x: 157,0,6; /* #9d0006 */
+ --faded-red: rgb(var(--faded-red_x));
+ --faded-green_x: 121,116,14; /* #79740e */
+ --faded-green: rgb(var(--faded-green_x));
+ --faded-yellow_x: 181,118,20; /* #b57614 */
+ --faded-yellow: rgb(var(--faded-yellow_x));
+ --faded-blue_x: 7,102,120; /* #076678 */
+ --faded-blue: rgb(var(--faded-blue_x));
+ --faded-purple_x: 143,63,113; /* #8f3f71 */
+ --faded-purple: rgb(var(--faded-purple_x));
+ --faded-aqua_x: 66,123,88; /* #427b58 */
+ --faded-aqua: rgb(var(--faded-aqua_x));
+ --faded-orange_x: 175,58,3; /* #af3a03 */
+ --faded-orange: rgb(var(--faded-orange_x));
+}
+
+body
+{
+ --accent-h: 12; /* --faded-red #9d0006 */
+ --accent-s: 107%;
+ --accent-l: 32%;
+
+ --link-decoration: none;
+ --link-decoration-hover: none;
+ --link-external-decoration: none;
+ --link-external-decoration-hover: none;
+
+ --tag-decoration: none;
+ --tag-decoration-hover: underline;
+ --tag-padding-x: .5em;
+ --tag-padding-y: .2em;
+ --tag-radius: .5em;
+
+ --tab-font-weight: 600;
+ --bold-weight: 600;
+
+ --checkbox-radius: 0;
+
+ --embed-border-left: 6px double var(--interactive-accent);
+}
+
+.theme-dark
+{
+ --color-red-rgb: var(--neutral-red_x);
+ --color-red: var(--neutral-red);
+ --color-purple-rgb: var(--neutral-purple_x);
+ --color-purple: var(--neutral-purple);
+ --color-green-rgb: var(--neutral-green_x);
+ --color-green: var(--neutral-green);
+ --color-cyan-rgb: var(--neutral-blue_x);
+ --color-cyan: var(--neutral-blue);
+ --color-blue-rgb: var(--faded-blue_x);
+ --color-blue: var(--faded-blue);
+ --color-yellow-rgb: var(--neutral-yellow_x);
+ --color-yellow: var(--neutral-yellow);
+ --color-orange-rgb: var(--neutral-orange_x);
+ --color-orange: var(--neutral-orange);
+ --color-pink-rgb: var(--bright-purple_x);
+ --color-pink: var(--bright-purple);
+
+ --background-primary: var(--dark0);
+ --background-primary-alt: var(--dark0);
+ --background-secondary: var(--dark0-hard);
+ --background-secondary-alt: var(--dark1);
+ --background-modifier-border: var(--dark1);
+
+ /* Adjusted for black backgrounds - subtle highlight */
+ --cursor-line-background: rgba(var(--dark1_x), 0.4);
+
+ --text-normal: var(--light0);
+ --text-faint: var(--light1);
+ --text-muted: var(--light2);
+
+ --link-url: var(--neutral-green);
+
+ /* Header colors - rainbow progression */
+ --h1-color: var(--neutral-red);
+ --h2-color: var(--neutral-yellow);
+ --h3-color: var(--neutral-green);
+ --h4-color: var(--neutral-aqua);
+ --h5-color: var(--neutral-blue);
+ --h6-color: var(--neutral-purple);
+
+ --text-highlight-bg: var(--neutral-yellow);
+ --text-highlight-fg: var(--dark0-hard);
+
+ --text-accent: var(--neutral-orange);
+ --text-accent-hover: var(--bright-aqua);
+
+ --tag-color: var(--bright-aqua);
+ --tag-background: var(--dark2);
+ --tag-background-hover: var(--dark1);
+
+ --titlebar-text-color-focused: var(--bright-red);
+
+ --inline-title-color: var(--bright-yellow);
+
+ --bold-color: var(--neutral-yellow);
+ --italic-color: var(--light4);
+
+ --checkbox-color: var(--light4);
+ --checkbox-color-hover: var(--light4);
+ --checkbox-border-color: var(--light4);
+ --checkbox-border-color-hover: var(--light4);
+ --checklist-done-color: rgba(var(--light2_x), 0.5);
+
+ --table-header-background: rgba(var(--dark0_x), 0.2);
+ --table-header-background-hover: var(--dark2);
+ --table-row-even-background: rgba(var(--dark2_x), 0.2);
+ --table-row-odd-background: rgba(var(--dark2_x), 0.4);
+ --table-row-background-hover: var(--dark2);
+
+ --text-selection: rgba(var(--neutral-red_x), 0.6);
+ --flashing-background: rgba(var(--neutral-red_x), 0.3);
+
+ --code-normal: var(--bright-blue);
+ --code-background: var(--dark1);
+
+ --mermaid-note: var(--neutral-blue);
+ --mermaid-actor: var(--dark2);
+ --mermaid-loopline: var(--neutral-blue);
+ --mermaid-exclude: var(--dark4);
+ --mermaid-seqnum: var(--dark0);
+
+ --icon-color-hover: var(--bright-red);
+ --icon-color-focused: var(--bright-blue);
+
+ --nav-item-color-hover: var(--bright-red);
+ --nav-item-color-active: var(--bright-aqua);
+ --nav-file-tag: rgba(var(--neutral-yellow_x), 0.9);
+
+ --graph-line: var(--dark2);
+ --graph-node: var(--light3);
+ --graph-node-tag: var(--neutral-red);
+ --graph-node-attachment: var(--neutral-green);
+
+ --calendar-hover: var(--bright-red);
+ --calendar-background-hover: var(--dark1);
+ --calendar-week: var(--neutral-orange);
+ --calendar-today: var(--neutral-orange);
+
+ --dataview-key: var(--text-faint);
+ --dataview-key-background: rgba(var(--faded-red_x), 0.5);
+ --dataview-value: var(--text-faint);
+ --dataview-value-background: rgba(var(--neutral-green_x), 0.3);
+
+ --tab-text-color-focused-active: var(--neutral-yellow);
+ --tab-text-color-focused-active-current: var(--bright-red);
+}
+
+/* TABLE STYLING */
+table
+{
+ border: 1px solid var(--background-secondary) !important;
+ border-collapse: collapse;
+}
+
+thead
+{
+ border-bottom: 2px solid var(--background-modifier-border) !important;
+}
+
+th
+{
+ font-weight: 600 !important;
+ border: 1px solid var(--background-secondary) !important;
+}
+
+td
+{
+ border-left: 1px solid var(--background-secondary) !important;
+ border-right: 1px solid var(--background-secondary) !important;
+ border-bottom: 1px solid var(--background-secondary) !important;
+}
+
+.markdown-rendered tbody tr:nth-child(even)
+{
+ background-color: var(--table-row-even-background) !important;
+}
+
+.markdown-rendered tbody tr:nth-child(odd)
+{
+ background-color: var(--table-row-odd-background) !important;
+}
+
+.markdown-rendered tbody tr:nth-child(even):hover,
+.markdown-rendered tbody tr:nth-child(odd):hover
+{
+ background-color: var(--table-row-background-hover) !important;
+}
+
+/* HIGHLIGHT/MARK STYLING */
+.markdown-rendered mark
+{
+ background-color: var(--text-highlight-bg);
+ color: var(--text-highlight-fg);
+}
+
+.markdown-rendered mark a
+{
+ color: var(--red) !important;
+ font-weight: 600;
+}
+
+.search-result-file-matched-text
+{
+ color: var(--text-highlight-fg) !important;
+}
+
+/* TAG HOVER */
+.cm-hashtag-begin:hover, .cm-hashtag-end:hover
+{
+ color: var(--text-accent);
+ text-decoration: underline;
+}
+
+/* CHECKBOX STYLING */
+input[type=checkbox]
+{
+ border: 1px solid var(--checkbox-color);
+}
+
+input[type=checkbox]:checked
+{
+ background-color: var(--checkbox-color);
+ box-shadow: inset 0 0 0 2px var(--background-primary);
+}
+
+input[type=checkbox]:checked:after
+{
+ display: none;
+}
+
+/* CODE BLOCKS */
+code[class*="language-"],
+pre[class*="language-"]
+{
+ line-height: var(--line-height-tight) !important;
+}
+
+/* URL/LINK STYLING */
+.cm-url
+{
+ color: var(--link-url) !important;
+}
+
+.cm-url:hover
+{
+ color: var(--text-accent-color) !important;
+}
+
+/* EDITOR-PREVIEW CONSISTENCY */
+.cm-highlight
+{
+ color: var(--text-highlight-fg) !important;
+}
+
+.cm-inline-code
+{
+ border-radius: var(--radius-s);
+ font-size: var(--code-size);
+ padding: 0.1em 0.25em;
+}
+
+.cm-line .cm-strong
+{
+ color: var(--bold-color) !important;
+}
+
+/* MERMAID DIAGRAMS */
+.mermaid .note
+{
+ fill: var(--mermaid-note) !important;
+}
+
+.mermaid .actor
+{
+ fill: var(--mermaid-actor) !important;
+}
+
+.mermaid .loopLine
+{
+ stroke: var(--mermaid-loopline) !important;
+}
+
+.mermaid .loopText>tspan,
+.mermaid .entityLabel
+{
+ fill: var(--neutral-red) !important;
+}
+
+.mermaid .exclude-range
+{
+ fill: var(--mermaid-exclude) !important;
+}
+
+.mermaid .sequenceNumber
+{
+ fill: var(--mermaid-seqnum) !important;
+}
+
+/* CALENDAR PLUGIN */
+.calendar .week-num
+{
+ color: var(--calendar-week) !important;
+}
+
+.calendar .today
+{
+ color: var(--calendar-today) !important;
+}
+
+.calendar .week-num:hover,
+.calendar .day:hover
+{
+ color: var(--calendar-hover) !important;
+ background-color: var(--calendar-background-hover) !important;
+}
+
+/* EMBEDS */
+.markdown-embed-title
+{
+ color: var(--yellow);
+ font-weight: 600 !important;
+}
+
+/* ACTIVE LINE */
+.cm-active
+{
+ background-color: var(--cursor-line-background) !important;
+}
+
+/* FILE EXPLORER */
+.nav-file-tag
+{
+ color: var(--nav-file-tag) !important;
+}
+
+.is-flashing
+{
+ background-color: var(--flashing-background) !important;
+}
+
+/* DATAVIEW PLUGIN */
+.dataview.inline-field-key
+{
+ border-top-left-radius: var(--radius-s);
+ border-bottom-left-radius: var(--radius-s);
+ padding-left: 4px;
+ font-family: var(--font-monospace);
+ font-size: var(--font-smaller);
+ color: var(--dataview-key) !important;
+ background-color: var(--dataview-key-background) !important;
+}
+
+.dataview.inline-field-value
+{
+ border-top-right-radius: var(--radius-s);
+ border-bottom-right-radius: var(--radius-s);
+ padding-right: 4px;
+ font-family: var(--font-monospace);
+ font-size: var(--font-smaller);
+ color: var(--dataview-value) !important;
+ background-color: var(--dataview-value-background) !important;
+}
+
+/* SUGGESTION POPUP */
+.suggestion-highlight
+{
+ color: var(--bright-red);
+}
+
+/* CALLOUTS */
+body {
+ --callout-border-width: 1px;
+ --callout-border-opacity: 0.4;
+ --callout-default: var(--neutral-blue_x);
+ --callout-note: var(--neutral-blue_x);
+ --callout-summary: var(--neutral-aqua_x);
+ --callout-info: var(--neutral-blue_x);
+ --callout-todo: var(--neutral-blue_x);
+ --callout-important: var(--neutral-aqua_x);
+ --callout-tip: var(--neutral-aqua_x);
+ --callout-success: var(--neutral-green_x);
+ --callout-question: var(--neutral-yellow_x);
+ --callout-warning: var(--neutral-orange_x);
+ --callout-fail: var(--neutral-red_x);
+ --callout-error: var(--neutral-red_x);
+ --callout-bug: var(--neutral-red_x);
+ --callout-example: var(--neutral-purple_x);
+ --callout-quote: var(--gray_x);
+}
+
+/*
+
+.callout {
+ background-color: rgba(var(--callout-color), 0.2);
+}
+
+/* */