diff --git a/extensions/diffkit-redirect/README.md b/extensions/diffkit-redirect/README.md
new file mode 100644
index 0000000..1e40264
--- /dev/null
+++ b/extensions/diffkit-redirect/README.md
@@ -0,0 +1,81 @@
+# DiffKit Extension
+
+Standalone browser extension for redirecting only selected GitHub URLs to DiffKit.
+
+## What it does
+
+- Redirects matching GitHub URLs at page start
+- Lets you keep the extension globally enabled or disabled
+- Shows one toggle per redirect rule in the popup
+- Uses a configurable list of rules instead of redirecting every GitHub page
+- Supports both exact URL redirects and regex-based route schemas
+- Supports custom route remaps like GitHub PR changes pages to DiffKit review pages
+
+## Default rule
+
+The extension ships with these enabled rules:
+
+- `https://github.com/`
+- `https://diff-kit.com/`
+- `https://github.com/pulls/*`
+- `https://diff-kit.com/pulls`
+- `https://github.com/issues/*`
+- `https://diff-kit.com/issues`
+- `https://github.com/:owner/:repo/pull/:number`
+- `https://diff-kit.com/:owner/:repo/pull/:number`
+- `https://github.com/:owner/:repo/pull/:number/changes`
+- `https://diff-kit.com/:owner/:repo/review/:number`
+- `https://github.com/:owner/:repo/issues/:number`
+- `https://diff-kit.com/:owner/:repo/issues/:number`
+
+## Rule format
+
+```json
+[
+ {
+ "id": "github-pull-details",
+ "label": "Pull request details",
+ "description": "Redirect GitHub pull request detail pages to DiffKit.",
+ "enabled": true,
+ "match": {
+ "urlRegex": "^https://github\\.com/([^/?#]+)/([^/?#]+)/pull/(\\d+)/?$",
+ "excludeUrlRegexes": []
+ },
+ "redirect": {
+ "replacement": "https://diff-kit.com/$1/$2/pull/$3"
+ }
+ }
+]
+```
+
+For an exact URL redirect, use:
+
+```json
+{
+ "id": "one-off",
+ "label": "Specific PR page",
+ "description": "Single URL redirect",
+ "enabled": true,
+ "match": {
+ "exactUrl": "https://github.com/org/repo/pull/123"
+ },
+ "redirect": {
+ "url": "https://diff-kit.com/org/repo/pull/123"
+ }
+}
+```
+
+## Install locally
+
+1. Open `chrome://extensions`
+2. Enable Developer mode
+3. Click Load unpacked
+4. Select `extensions/diffkit-redirect`
+
+## Scope
+
+This version is intentionally limited to `github.com` in `manifest.json`. If you later want redirects from other source hosts, add those hosts to the extension matches and permissions.
+
+## Popup behavior
+
+The popup keeps all default rules enabled unless you turn specific ones off. Use the master switch to pause all redirects without losing your per-rule selections.
diff --git a/extensions/diffkit-redirect/content.js b/extensions/diffkit-redirect/content.js
new file mode 100644
index 0000000..3e37113
--- /dev/null
+++ b/extensions/diffkit-redirect/content.js
@@ -0,0 +1,22 @@
+(async function runRedirect() {
+ const shared = globalThis.DiffKitRedirect;
+ if (!shared) {
+ return;
+ }
+
+ try {
+ const config = await shared.getConfig();
+ if (!config.enabled) {
+ return;
+ }
+
+ const redirect = shared.findRedirect(window.location.href, config.rules);
+ if (!redirect) {
+ return;
+ }
+
+ window.location.replace(redirect.targetUrl);
+ } catch (error) {
+ console.error("DiffKit: failed to evaluate redirect.", error);
+ }
+})();
diff --git a/extensions/diffkit-redirect/icons/icon-128.png b/extensions/diffkit-redirect/icons/icon-128.png
new file mode 100644
index 0000000..6d072c1
Binary files /dev/null and b/extensions/diffkit-redirect/icons/icon-128.png differ
diff --git a/extensions/diffkit-redirect/icons/icon-16.png b/extensions/diffkit-redirect/icons/icon-16.png
new file mode 100644
index 0000000..ee77780
Binary files /dev/null and b/extensions/diffkit-redirect/icons/icon-16.png differ
diff --git a/extensions/diffkit-redirect/icons/icon-32.png b/extensions/diffkit-redirect/icons/icon-32.png
new file mode 100644
index 0000000..9cd8e62
Binary files /dev/null and b/extensions/diffkit-redirect/icons/icon-32.png differ
diff --git a/extensions/diffkit-redirect/icons/icon-48.png b/extensions/diffkit-redirect/icons/icon-48.png
new file mode 100644
index 0000000..65d9ada
Binary files /dev/null and b/extensions/diffkit-redirect/icons/icon-48.png differ
diff --git a/extensions/diffkit-redirect/icons/logo.svg b/extensions/diffkit-redirect/icons/logo.svg
new file mode 100644
index 0000000..3550753
--- /dev/null
+++ b/extensions/diffkit-redirect/icons/logo.svg
@@ -0,0 +1,12 @@
+
diff --git a/extensions/diffkit-redirect/manifest.json b/extensions/diffkit-redirect/manifest.json
new file mode 100644
index 0000000..feec289
--- /dev/null
+++ b/extensions/diffkit-redirect/manifest.json
@@ -0,0 +1,30 @@
+{
+ "manifest_version": 3,
+ "name": "DiffKit",
+ "version": "0.1.0",
+ "description": "Redirect selected GitHub URLs to matching DiffKit routes.",
+ "icons": {
+ "16": "icons/icon-16.png",
+ "32": "icons/icon-32.png",
+ "48": "icons/icon-48.png",
+ "128": "icons/icon-128.png"
+ },
+ "permissions": ["storage"],
+ "host_permissions": ["https://github.com/*"],
+ "action": {
+ "default_title": "DiffKit",
+ "default_popup": "popup.html",
+ "default_icon": {
+ "16": "icons/icon-16.png",
+ "32": "icons/icon-32.png"
+ }
+ },
+ "options_page": "options.html",
+ "content_scripts": [
+ {
+ "matches": ["https://github.com/*"],
+ "js": ["shared.js", "content.js"],
+ "run_at": "document_start"
+ }
+ ]
+}
diff --git a/extensions/diffkit-redirect/options.html b/extensions/diffkit-redirect/options.html
new file mode 100644
index 0000000..6fc8528
--- /dev/null
+++ b/extensions/diffkit-redirect/options.html
@@ -0,0 +1,74 @@
+
+
+
+
+
+ DiffKit Options
+
+
+
+
+
+
+
+
+ Add only the GitHub routes DiffKit currently supports. A rule can
+ match a single exact URL or a regex pattern over the full URL.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/extensions/diffkit-redirect/options.js b/extensions/diffkit-redirect/options.js
new file mode 100644
index 0000000..f310f5b
--- /dev/null
+++ b/extensions/diffkit-redirect/options.js
@@ -0,0 +1,58 @@
+(async function initOptions() {
+ const shared = globalThis.DiffKitRedirect;
+ const enabledToggle = document.getElementById("enabled-toggle");
+ const rulesEditor = document.getElementById("rules-editor");
+ const saveButton = document.getElementById("save-button");
+ const resetButton = document.getElementById("reset-button");
+ const saveStatus = document.getElementById("save-status");
+
+ function setStatus(message, tone) {
+ saveStatus.textContent = message;
+ saveStatus.dataset.tone = tone || "";
+ }
+
+ async function loadConfig() {
+ const config = await shared.getConfig();
+ enabledToggle.checked = config.enabled;
+ rulesEditor.value = JSON.stringify(config.rules, null, 2);
+
+ if (config.validationErrors.length > 0) {
+ setStatus(config.validationErrors.join(" "), "error");
+ return;
+ }
+
+ setStatus("", "");
+ }
+
+ async function saveConfig() {
+ let parsedRules;
+ try {
+ parsedRules = JSON.parse(rulesEditor.value);
+ } catch {
+ setStatus("Rules must be valid JSON.", "error");
+ return;
+ }
+
+ const result = await shared.saveConfig({
+ enabled: enabledToggle.checked,
+ rules: parsedRules,
+ });
+
+ if (!result.ok) {
+ setStatus(result.errors.join(" "), "error");
+ return;
+ }
+
+ setStatus("Rules saved.", "success");
+ await loadConfig();
+ }
+
+ saveButton.addEventListener("click", saveConfig);
+
+ resetButton.addEventListener("click", () => {
+ rulesEditor.value = JSON.stringify(shared.getDefaultRules(), null, 2);
+ setStatus("Defaults restored in the editor. Save to apply them.", "");
+ });
+
+ await loadConfig();
+})();
diff --git a/extensions/diffkit-redirect/popup.html b/extensions/diffkit-redirect/popup.html
new file mode 100644
index 0000000..60dbd33
--- /dev/null
+++ b/extensions/diffkit-redirect/popup.html
@@ -0,0 +1,41 @@
+
+
+
+
+
+ DiffKit
+
+
+
+
+
+
+
+
+
diff --git a/extensions/diffkit-redirect/popup.js b/extensions/diffkit-redirect/popup.js
new file mode 100644
index 0000000..160ff82
--- /dev/null
+++ b/extensions/diffkit-redirect/popup.js
@@ -0,0 +1,137 @@
+(async function initPopup() {
+ const shared = globalThis.DiffKitRedirect;
+ const toggle = document.getElementById("enabled-toggle");
+ const ruleList = document.getElementById("rule-list");
+ const statusCopy = document.getElementById("status-copy");
+
+ function getRuleLabel(rule) {
+ if (rule.label) {
+ return rule.label;
+ }
+
+ if (rule.description) {
+ return rule.description;
+ }
+
+ return rule.id
+ .replace(/[-_]+/g, " ")
+ .replace(/\b\w/g, (character) => character.toUpperCase());
+ }
+
+ function getRuleDescription(rule) {
+ if (rule.enabled) {
+ return "Enabled";
+ }
+
+ return "Disabled";
+ }
+
+ function renderRuleList(config) {
+ ruleList.textContent = "";
+
+ if (config.rules.length === 0) {
+ const emptyState = document.createElement("p");
+ emptyState.className = "empty-state";
+ emptyState.textContent = "No redirect rules available yet.";
+ ruleList.append(emptyState);
+ return;
+ }
+
+ for (const rule of config.rules) {
+ const button = document.createElement("button");
+ button.type = "button";
+ button.className = "rule-toggle";
+ button.dataset.ruleId = rule.id;
+ button.setAttribute("aria-pressed", String(rule.enabled));
+
+ const label = document.createElement("span");
+ label.className = "rule-copy";
+
+ const title = document.createElement("span");
+ title.className = "rule-title";
+ title.textContent = getRuleLabel(rule);
+
+ const description = document.createElement("span");
+ description.className = "rule-description";
+ description.textContent = getRuleDescription(rule);
+
+ label.append(title, description);
+ button.append(label);
+ ruleList.append(button);
+ }
+ }
+
+ function renderStatus(config) {
+ const activeRules = config.rules.filter((rule) => rule.enabled).length;
+ const totalRules = config.rules.length;
+
+ if (!config.enabled) {
+ statusCopy.textContent = `Master off. ${activeRules} of ${totalRules} rules selected.`;
+ return;
+ }
+
+ statusCopy.textContent = `${activeRules} of ${totalRules} redirects active.`;
+ }
+
+ async function render() {
+ const config = await shared.getConfig();
+ toggle.checked = config.enabled;
+ renderRuleList(config);
+ renderStatus(config);
+ }
+
+ try {
+ await render();
+ } catch {
+ statusCopy.textContent = "Failed to load extension state";
+ }
+
+ toggle.addEventListener("change", async () => {
+ const config = await shared.getConfig();
+ const nextConfig = {
+ enabled: toggle.checked,
+ rules: config.rules,
+ };
+
+ const result = await shared.saveConfig(nextConfig);
+ if (!result.ok) {
+ statusCopy.textContent = result.errors.join(" ");
+ return;
+ }
+
+ await render();
+ });
+
+ ruleList.addEventListener("click", async (event) => {
+ if (!(event.target instanceof Element)) {
+ return;
+ }
+
+ const button = event.target.closest(".rule-toggle");
+ if (!button) {
+ return;
+ }
+
+ const config = await shared.getConfig();
+ const nextRules = config.rules.map((rule) =>
+ rule.id === button.dataset.ruleId
+ ? {
+ ...rule,
+ enabled: !rule.enabled,
+ }
+ : rule
+ );
+
+ const result = await shared.saveConfig({
+ enabled: config.enabled,
+ rules: nextRules,
+ });
+
+ if (!result.ok) {
+ statusCopy.textContent = result.errors.join(" ");
+ return;
+ }
+
+ await render();
+ });
+})();
diff --git a/extensions/diffkit-redirect/shared.js b/extensions/diffkit-redirect/shared.js
new file mode 100644
index 0000000..f3f4352
--- /dev/null
+++ b/extensions/diffkit-redirect/shared.js
@@ -0,0 +1,400 @@
+(function initDiffKitRedirectShared(global) {
+ const extensionApi = global.chrome ?? global.browser;
+
+ const STORAGE_KEYS = {
+ enabled: "enabled",
+ rules: "rules",
+ };
+
+ const DEFAULT_RULES = [
+ {
+ id: "github-overview",
+ label: "Overview page",
+ description: "Redirect the GitHub overview page to DiffKit.",
+ enabled: true,
+ match: {
+ urlRegex: "^https://github\\.com/?(?:[?#].*)?$",
+ },
+ redirect: {
+ url: "https://diff-kit.com/",
+ },
+ },
+ {
+ id: "github-pulls",
+ label: "Pulls list",
+ description: "Redirect the GitHub pulls page to DiffKit pulls.",
+ enabled: true,
+ match: {
+ urlRegex: "^https://github\\.com/pulls(?:/.*)?(?:[?#].*)?$",
+ },
+ redirect: {
+ url: "https://diff-kit.com/pulls",
+ },
+ },
+ {
+ id: "github-global-issues",
+ label: "Issues list",
+ description: "Redirect GitHub global issues pages to DiffKit issues.",
+ enabled: true,
+ match: {
+ urlRegex: "^https://github\\.com/issues(?:/.*)?(?:[?#].*)?$",
+ },
+ redirect: {
+ url: "https://diff-kit.com/issues",
+ },
+ },
+ {
+ id: "github-pull-details",
+ label: "Pull request details",
+ description: "Redirect GitHub pull request detail pages to DiffKit.",
+ enabled: true,
+ match: {
+ urlRegex: "^https://github\\.com/([^/?#]+)/([^/?#]+)/pull/(\\d+)/?$",
+ },
+ redirect: {
+ replacement: "https://diff-kit.com/$1/$2/pull/$3",
+ },
+ },
+ {
+ id: "github-review-details",
+ label: "Code review page",
+ description: "Redirect GitHub PR changes pages to DiffKit review pages.",
+ enabled: true,
+ match: {
+ urlRegex:
+ "^https://github\\.com/([^/?#]+)/([^/?#]+)/pull/(\\d+)/changes/?$",
+ },
+ redirect: {
+ replacement: "https://diff-kit.com/$1/$2/review/$3",
+ },
+ },
+ {
+ id: "github-issue-details",
+ label: "Issue details",
+ description: "Redirect GitHub issue detail pages to DiffKit.",
+ enabled: true,
+ match: {
+ urlRegex: "^https://github\\.com/([^/?#]+)/([^/?#]+)/issues/(\\d+)/?$",
+ },
+ redirect: {
+ replacement: "https://diff-kit.com/$1/$2/issues/$3",
+ },
+ },
+ ];
+
+ function deepClone(value) {
+ return JSON.parse(JSON.stringify(value));
+ }
+
+ function callApi(method, ...args) {
+ return new Promise((resolve, reject) => {
+ let settled = false;
+ const callback = (result) => {
+ if (settled) {
+ return;
+ }
+
+ settled = true;
+ const runtimeError = extensionApi.runtime?.lastError;
+ if (runtimeError) {
+ reject(new Error(runtimeError.message));
+ return;
+ }
+ resolve(result);
+ };
+
+ try {
+ const maybePromise = method(...args, callback);
+ if (maybePromise && typeof maybePromise.then === "function") {
+ maybePromise.then(
+ (result) => {
+ if (settled) {
+ return;
+ }
+ settled = true;
+ resolve(result);
+ },
+ (error) => {
+ if (settled) {
+ return;
+ }
+ settled = true;
+ reject(error);
+ }
+ );
+ }
+ } catch (error) {
+ reject(error);
+ }
+ });
+ }
+
+ function storageGet(keys) {
+ return callApi(
+ extensionApi.storage.sync.get.bind(extensionApi.storage.sync),
+ keys
+ );
+ }
+
+ function storageSet(values) {
+ return callApi(
+ extensionApi.storage.sync.set.bind(extensionApi.storage.sync),
+ values
+ );
+ }
+
+ function isNonEmptyString(value) {
+ return typeof value === "string" && value.trim().length > 0;
+ }
+
+ function normalizeStringArray(value) {
+ if (!Array.isArray(value)) {
+ return [];
+ }
+
+ return value.filter(isNonEmptyString);
+ }
+
+ function normalizeRule(rawRule, index) {
+ const fallbackId = `rule-${index + 1}`;
+ const rule = {
+ id: isNonEmptyString(rawRule?.id) ? rawRule.id.trim() : fallbackId,
+ label: isNonEmptyString(rawRule?.label) ? rawRule.label.trim() : "",
+ description: isNonEmptyString(rawRule?.description)
+ ? rawRule.description.trim()
+ : "",
+ enabled: rawRule?.enabled !== false,
+ match: {
+ exactUrl: isNonEmptyString(rawRule?.match?.exactUrl)
+ ? rawRule.match.exactUrl.trim()
+ : "",
+ urlRegex: isNonEmptyString(rawRule?.match?.urlRegex)
+ ? rawRule.match.urlRegex.trim()
+ : "",
+ excludeUrlRegexes: normalizeStringArray(
+ rawRule?.match?.excludeUrlRegexes
+ ).map((entry) => entry.trim()),
+ },
+ redirect: {
+ url: isNonEmptyString(rawRule?.redirect?.url)
+ ? rawRule.redirect.url.trim()
+ : "",
+ replacement: isNonEmptyString(rawRule?.redirect?.replacement)
+ ? rawRule.redirect.replacement.trim()
+ : "",
+ },
+ };
+
+ return rule;
+ }
+
+ function isValidUrl(value) {
+ try {
+ new URL(value);
+ return true;
+ } catch {
+ return false;
+ }
+ }
+
+ function isValidRegex(value) {
+ try {
+ new RegExp(value);
+ return true;
+ } catch {
+ return false;
+ }
+ }
+
+ function validateMatch(rule) {
+ const errors = [];
+
+ if (!(rule.match.exactUrl || rule.match.urlRegex)) {
+ errors.push(
+ `Rule "${rule.id}" needs either match.exactUrl or match.urlRegex.`
+ );
+ }
+
+ if (rule.match.exactUrl && !isValidUrl(rule.match.exactUrl)) {
+ errors.push(`Rule "${rule.id}" has an invalid match.exactUrl.`);
+ }
+
+ if (rule.match.urlRegex && !isValidRegex(rule.match.urlRegex)) {
+ errors.push(`Rule "${rule.id}" has an invalid match.urlRegex.`);
+ }
+
+ for (const excludePattern of rule.match.excludeUrlRegexes) {
+ if (!isValidRegex(excludePattern)) {
+ errors.push(
+ `Rule "${rule.id}" has an invalid exclude regex "${excludePattern}".`
+ );
+ }
+ }
+
+ return errors;
+ }
+
+ function validateRedirect(rule) {
+ const errors = [];
+
+ if (!(rule.redirect.url || rule.redirect.replacement)) {
+ errors.push(
+ `Rule "${rule.id}" needs either redirect.url or redirect.replacement.`
+ );
+ }
+
+ if (rule.redirect.url && !isValidUrl(rule.redirect.url)) {
+ errors.push(`Rule "${rule.id}" has an invalid redirect.url.`);
+ }
+
+ return errors;
+ }
+
+ function validateRule(rule, seenIds) {
+ const errors = [];
+
+ if (seenIds.has(rule.id)) {
+ errors.push(`Duplicate rule id "${rule.id}".`);
+ }
+ seenIds.add(rule.id);
+
+ return errors.concat(validateMatch(rule), validateRedirect(rule));
+ }
+
+ function validateRules(candidateRules) {
+ if (!Array.isArray(candidateRules)) {
+ return {
+ ok: false,
+ errors: ["Rules must be a JSON array."],
+ rules: [],
+ };
+ }
+
+ const rules = candidateRules.map(normalizeRule);
+ const seenIds = new Set();
+ const errors = rules.flatMap((rule) => validateRule(rule, seenIds));
+
+ return {
+ ok: errors.length === 0,
+ errors,
+ rules,
+ };
+ }
+
+ function getDefaultRules() {
+ return deepClone(DEFAULT_RULES);
+ }
+
+ async function getConfig() {
+ const stored = await storageGet({
+ [STORAGE_KEYS.enabled]: true,
+ [STORAGE_KEYS.rules]: getDefaultRules(),
+ });
+
+ const validation = validateRules(stored[STORAGE_KEYS.rules]);
+ return {
+ enabled: stored[STORAGE_KEYS.enabled] !== false,
+ rules: validation.ok ? validation.rules : getDefaultRules(),
+ validationErrors: validation.ok ? [] : validation.errors,
+ };
+ }
+
+ async function saveConfig(config) {
+ const validation = validateRules(config.rules);
+ if (!validation.ok) {
+ return {
+ ok: false,
+ errors: validation.errors,
+ };
+ }
+
+ await storageSet({
+ [STORAGE_KEYS.enabled]: config.enabled !== false,
+ [STORAGE_KEYS.rules]: validation.rules,
+ });
+
+ return {
+ ok: true,
+ errors: [],
+ };
+ }
+
+ function expandReplacement(template, match) {
+ return template.replace(
+ /\$(\d+)/g,
+ (_, index) => match[Number(index)] ?? ""
+ );
+ }
+
+ function isRuleExcluded(urlString, rule) {
+ return rule.match.excludeUrlRegexes.some((excludePattern) =>
+ new RegExp(excludePattern).test(urlString)
+ );
+ }
+
+ function buildRedirectResult(rule, targetUrl, urlString) {
+ if (!targetUrl || targetUrl === urlString) {
+ return null;
+ }
+
+ return {
+ rule,
+ targetUrl,
+ };
+ }
+
+ function matchExactRedirect(urlString, rule) {
+ if (!(rule.match.exactUrl && urlString === rule.match.exactUrl)) {
+ return null;
+ }
+
+ return buildRedirectResult(rule, rule.redirect.url, urlString);
+ }
+
+ function matchRegexRedirect(urlString, rule) {
+ if (!rule.match.urlRegex) {
+ return null;
+ }
+
+ const regex = new RegExp(rule.match.urlRegex);
+ const match = urlString.match(regex);
+ if (!match) {
+ return null;
+ }
+
+ const targetUrl =
+ rule.redirect.url || expandReplacement(rule.redirect.replacement, match);
+ return buildRedirectResult(rule, targetUrl, urlString);
+ }
+
+ function findRedirectForRule(urlString, rawRule) {
+ const rule = normalizeRule(rawRule, 0);
+ if (!rule.enabled || isRuleExcluded(urlString, rule)) {
+ return null;
+ }
+
+ return (
+ matchExactRedirect(urlString, rule) || matchRegexRedirect(urlString, rule)
+ );
+ }
+
+ function findRedirect(urlString, rules) {
+ for (const rawRule of rules) {
+ const redirect = findRedirectForRule(urlString, rawRule);
+ if (redirect) {
+ return redirect;
+ }
+ }
+
+ return null;
+ }
+
+ global.DiffKitRedirect = {
+ STORAGE_KEYS,
+ getConfig,
+ getDefaultRules,
+ saveConfig,
+ validateRules,
+ findRedirect,
+ };
+})(globalThis);
diff --git a/extensions/diffkit-redirect/styles.css b/extensions/diffkit-redirect/styles.css
new file mode 100644
index 0000000..8d93010
--- /dev/null
+++ b/extensions/diffkit-redirect/styles.css
@@ -0,0 +1,425 @@
+:root {
+ --background: #ffffff;
+ --foreground: oklch(0.141 0.005 285.823);
+ --card: #ffffff;
+ --card-foreground: oklch(0.141 0.005 285.823);
+ --primary: oklch(0.21 0.006 285.885);
+ --primary-foreground: oklch(0.985 0 0);
+ --secondary: oklch(0.967 0.001 286.375);
+ --secondary-foreground: oklch(0.21 0.006 285.885);
+ --muted: oklch(0.967 0.001 286.375);
+ --muted-foreground: oklch(0.552 0.016 285.938);
+ --border: oklch(0.92 0.004 286.32);
+ --input: oklch(0.92 0.004 286.32);
+ --ring: oklch(0.871 0.006 286.286);
+ --brand: oklch(0.68 0.2 150);
+ --shadow-sm: 0 1px 2px rgba(16, 24, 40, 0.06);
+ --shadow-md: 0 12px 32px rgba(16, 24, 40, 0.08);
+ font-family:
+ "Inter Variable", "Inter", "Avenir Next", ui-sans-serif, system-ui,
+ sans-serif;
+ color: var(--foreground);
+ color-scheme: light;
+ background: var(--background);
+}
+
+* {
+ box-sizing: border-box;
+}
+
+body {
+ min-width: 360px;
+ margin: 0;
+ font-size: 13px;
+ font-weight: 450;
+ color: var(--foreground);
+ background:
+ radial-gradient(
+ circle at top right,
+ color-mix(in srgb, var(--brand) 12%, transparent),
+ transparent 30%
+ ),
+ linear-gradient(180deg, #ffffff 0%, #fafafa 100%);
+}
+
+button,
+textarea,
+input {
+ font: inherit;
+}
+
+button {
+ padding: 0.625rem 0.875rem;
+ font-size: 13px;
+ font-weight: 500;
+ color: var(--primary-foreground);
+ letter-spacing: -0.01em;
+ cursor: pointer;
+ background: var(--primary);
+ border: 1px solid transparent;
+ border-radius: 10px;
+ box-shadow: var(--shadow-sm);
+ transition:
+ background-color 120ms ease,
+ border-color 120ms ease,
+ box-shadow 120ms ease;
+}
+
+button.secondary {
+ color: var(--secondary-foreground);
+ background: var(--secondary);
+ border-color: var(--border);
+}
+
+button:hover {
+ background: color-mix(in srgb, var(--primary) 92%, white);
+}
+
+button.secondary:hover {
+ background: color-mix(in srgb, var(--secondary) 88%, white);
+}
+
+button:focus-visible,
+textarea:focus-visible,
+input:focus-visible {
+ outline: none;
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--ring) 55%, transparent);
+}
+
+code,
+pre {
+ font-family:
+ "SFMono-Regular", ui-monospace, "Cascadia Code", "Source Code Pro",
+ monospace;
+}
+
+.popup-body {
+ --background: #141518;
+ --foreground: oklch(0.965 0.003 286);
+ --card: #16171a;
+ --card-foreground: oklch(0.965 0.003 286);
+ --primary: oklch(0.965 0.003 286);
+ --primary-foreground: #121316;
+ --secondary: #202227;
+ --secondary-foreground: oklch(0.86 0.01 286);
+ --muted: #1a1c20;
+ --muted-foreground: oklch(0.7 0.012 286);
+ --border: rgba(255, 255, 255, 0.08);
+ --input: #2b2e34;
+ --ring: rgba(255, 255, 255, 0.22);
+ --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.35);
+ --shadow-md: 0 16px 40px rgba(0, 0, 0, 0.42);
+ min-width: 400px;
+ color-scheme: dark;
+ background: #141518;
+}
+
+.popup-shell {
+ padding: 0;
+}
+
+.popup-card,
+.options-panel,
+.help-panel {
+ background: color-mix(in srgb, var(--card) 96%, transparent);
+ border: 1px solid var(--border);
+ border-radius: 18px;
+ box-shadow: var(--shadow-md);
+}
+
+.popup-card {
+ padding: 12px;
+ background: transparent;
+ border: 0;
+ border-radius: 0;
+ box-shadow: none;
+}
+
+.brand-row,
+.options-header,
+.toggle-row,
+.panel-actions,
+.status-row {
+ display: flex;
+ gap: 12px;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.panel-actions {
+ margin-top: 16px;
+}
+
+.eyebrow {
+ margin: 0 0 4px;
+ font-family:
+ "Geist Mono Variable", "SF Mono", ui-monospace, "Cascadia Code", monospace;
+ font-size: 11px;
+ font-weight: 500;
+ color: var(--muted-foreground);
+ letter-spacing: 0.06em;
+}
+
+h1,
+h2,
+p {
+ margin: 0;
+}
+
+h1 {
+ font-size: 18px;
+ font-weight: 600;
+ letter-spacing: -0.02em;
+}
+
+h2 {
+ margin-bottom: 10px;
+ font-size: 15px;
+ font-weight: 600;
+ letter-spacing: -0.02em;
+}
+
+.muted,
+.lede {
+ margin-top: 12px;
+ line-height: 1.55;
+ color: var(--muted-foreground);
+}
+
+.lede {
+ max-width: 34ch;
+ text-wrap: pretty;
+}
+
+.rule-list {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 4px;
+ margin-top: 12px;
+}
+
+.empty-state {
+ grid-column: 1 / -1;
+ padding: 12px;
+ margin: 0;
+ line-height: 1.5;
+ color: var(--muted-foreground);
+ border: 1px dashed var(--border);
+ border-radius: 14px;
+}
+
+.rule-toggle {
+ display: flex;
+ gap: 6px;
+ align-items: flex-start;
+ width: 100%;
+ min-height: 44px;
+ padding: 7px 8px;
+ color: var(--muted-foreground);
+ text-align: left;
+ background: var(--secondary);
+ border: 1px solid transparent;
+ border-radius: 12px;
+ box-shadow: none;
+ transition:
+ background-color 120ms ease,
+ color 120ms ease,
+ border-color 120ms ease,
+ transform 120ms ease;
+}
+
+.rule-toggle:hover {
+ color: var(--foreground);
+ background: color-mix(in srgb, var(--secondary) 92%, white 4%);
+}
+
+.rule-toggle[aria-pressed="true"] {
+ color: #ffffff;
+ background: color-mix(in srgb, var(--foreground) 14%, #0f1012);
+}
+
+.rule-copy {
+ position: relative;
+ display: grid;
+ gap: 1px;
+ width: 100%;
+ min-width: 0;
+ padding-left: 28px;
+}
+
+.rule-copy::before {
+ position: absolute;
+ top: 0.0625rem;
+ left: 0;
+ width: 20px;
+ height: 20px;
+ content: "";
+ background: rgba(255, 255, 255, 0.08);
+ border-radius: 999px;
+ box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.06);
+}
+
+.rule-copy::after {
+ position: absolute;
+ top: 0.4375rem;
+ left: 7px;
+ width: 6px;
+ height: 6px;
+ content: "";
+ background: currentColor;
+ border-radius: 999px;
+ opacity: 0.8;
+}
+
+.rule-toggle[aria-pressed="true"] .rule-copy::before {
+ background: rgba(255, 255, 255, 0.12);
+ box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08);
+}
+
+.rule-title {
+ font-size: 12px;
+ font-weight: 600;
+ line-height: 1.2;
+ letter-spacing: -0.01em;
+}
+
+.rule-description {
+ font-family:
+ "Geist Mono Variable", "SF Mono", ui-monospace, "Cascadia Code", monospace;
+ font-size: 10px;
+ letter-spacing: 0.04em;
+ opacity: 0.72;
+}
+
+.rule-toggle[aria-pressed="true"] .rule-description {
+ color: rgba(255, 255, 255, 0.72);
+}
+
+.brand-lockup {
+ display: flex;
+ gap: 12px;
+ align-items: center;
+}
+
+.brand-logo {
+ flex: none;
+ width: 42px;
+ height: 42px;
+ border-radius: 12px;
+ box-shadow: var(--shadow-sm);
+}
+
+.popup-body button.secondary {
+ color: var(--foreground);
+ background: #1c1e23;
+ border-color: rgba(255, 255, 255, 0.08);
+}
+
+.popup-body button.secondary:hover {
+ background: #24272d;
+}
+
+.switch {
+ position: relative;
+ display: inline-flex;
+}
+
+.switch input {
+ position: absolute;
+ inset: 0;
+ opacity: 0;
+}
+
+.switch-track {
+ position: relative;
+ width: 36px;
+ height: 20px;
+ background: var(--input);
+ border-radius: 999px;
+ box-shadow: inset 0 0 0 1px var(--border);
+ transition: background 120ms ease;
+}
+
+.switch-track::after {
+ position: absolute;
+ top: 2px;
+ left: 2px;
+ width: 16px;
+ height: 16px;
+ content: "";
+ background: var(--background);
+ border-radius: 50%;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.18);
+ transition: transform 120ms ease;
+}
+
+.switch input:checked + .switch-track {
+ background: var(--primary);
+ box-shadow: none;
+}
+
+.switch input:checked + .switch-track::after {
+ transform: translateX(16px);
+}
+
+.status-row {
+ padding-top: 14px;
+ margin-top: 16px;
+ border-top: 1px solid color-mix(in srgb, var(--border) 85%, transparent);
+}
+
+.options-layout {
+ display: grid;
+ grid-template-columns: minmax(0, 2fr) minmax(280px, 1fr);
+ gap: 18px;
+ align-items: start;
+ padding: 24px;
+}
+
+.options-panel,
+.help-panel {
+ padding: 20px;
+}
+
+.rules-editor {
+ width: 100%;
+ min-height: 420px;
+ padding: 16px;
+ margin-top: 18px;
+ line-height: 1.5;
+ color: #f5f5f5;
+ resize: vertical;
+ background: #101011;
+ border: 1px solid var(--border);
+ border-radius: 16px;
+}
+
+.code-block {
+ padding: 16px;
+ margin: 0;
+ overflow: auto;
+ line-height: 1.5;
+ color: #f5f5f5;
+ background: #101011;
+ border-radius: 16px;
+}
+
+.status-copy {
+ min-height: 20px;
+ line-height: 1.45;
+ color: var(--muted-foreground);
+}
+
+.status-copy[data-tone="error"] {
+ color: oklch(0.577 0.245 27.325);
+}
+
+.status-copy[data-tone="success"] {
+ color: color-mix(in srgb, var(--brand) 60%, black);
+}
+
+@media (max-width: 900px) {
+ .options-layout {
+ grid-template-columns: 1fr;
+ }
+}