diff --git a/custom_components/bahmcloud_store/app.js b/custom_components/bahmcloud_store/app.js
new file mode 100644
index 0000000..9054b81
--- /dev/null
+++ b/custom_components/bahmcloud_store/app.js
@@ -0,0 +1,87 @@
+async function apiGet() {
+ const r = await fetch("/api/bahmcloud_store", { credentials: "same-origin" });
+ return await r.json();
+}
+
+async function apiPost(payload) {
+ const r = await fetch("/api/bahmcloud_store", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ credentials: "same-origin",
+ body: JSON.stringify(payload),
+ });
+ return await r.json();
+}
+
+function el(tag, attrs = {}, children = []) {
+ const n = document.createElement(tag);
+ for (const [k, v] of Object.entries(attrs)) {
+ if (k === "class") n.className = v;
+ else if (k === "onclick") n.onclick = v;
+ else n.setAttribute(k, v);
+ }
+ for (const c of children) {
+ n.appendChild(typeof c === "string" ? document.createTextNode(c) : c);
+ }
+ return n;
+}
+
+function card(pkg) {
+ const installedBadge = el("span", { class: "badge" }, [pkg.installed ? "Installed" : "Not installed"]);
+
+ const title = el("div", {}, [
+ el("strong", {}, [pkg.name]),
+ el("div", { class: "muted" }, [pkg.repo]),
+ ]);
+
+ const ver = el("div", { class: "muted" }, [
+ `Installed: ${pkg.installed_version || "-"} | Latest: ${pkg.latest_version || "-"}`
+ ]);
+
+ const btnInstall = el("button", {
+ onclick: async () => {
+ btnInstall.disabled = true;
+ btnInstall.textContent = "Working...";
+ await apiPost({ op: "install", package_id: pkg.id });
+ await load();
+ }
+ }, [pkg.installed ? "Reinstall" : "Install"]);
+
+ const btnUpdate = el("button", {
+ onclick: async () => {
+ btnUpdate.disabled = true;
+ btnUpdate.textContent = "Working...";
+ await apiPost({ op: "update", package_id: pkg.id });
+ await load();
+ }
+ }, ["Update"]);
+
+ // Update-Button nur wenn installiert
+ btnUpdate.disabled = !pkg.installed;
+
+ const actions = el("div", { class: "actions" }, [btnInstall, btnUpdate]);
+
+ return el("div", { class: "card" }, [
+ el("div", { class: "row" }, [title, installedBadge]),
+ ver,
+ actions
+ ]);
+}
+
+async function load() {
+ const status = document.getElementById("status");
+ const list = document.getElementById("list");
+
+ status.textContent = "Loading...";
+ list.innerHTML = "";
+
+ const data = await apiGet();
+ status.textContent = `Store: ${data.store_url}`;
+
+ for (const pkg of data.packages) {
+ list.appendChild(card(pkg));
+ }
+}
+
+document.getElementById("refresh").onclick = load;
+load();
diff --git a/custom_components/bahmcloud_store/index.html b/custom_components/bahmcloud_store/index.html
new file mode 100644
index 0000000..890eec2
--- /dev/null
+++ b/custom_components/bahmcloud_store/index.html
@@ -0,0 +1,26 @@
+
+
+
+
+ Bahmcloud Store
+
+
+
+
+
Bahmcloud Store
+
+ Installation erfolgt hier im Store (Buttons).
+ Updates erscheinen danach zusätzlich unter Einstellungen → System → Updates .
+
+
+
+ Refresh
+
+
+
+
+
+
+
+
+
diff --git a/custom_components/bahmcloud_store/panel.js b/custom_components/bahmcloud_store/panel.js
new file mode 100644
index 0000000..c2100ab
--- /dev/null
+++ b/custom_components/bahmcloud_store/panel.js
@@ -0,0 +1,946 @@
+class BahmcloudStorePanel extends HTMLElement {
+ constructor() {
+ super();
+ this.attachShadow({ mode: "open" });
+
+ this._hass = null;
+
+ this._view = "store"; // store | manage | about | detail
+ this._data = null;
+ this._loading = true;
+ this._error = null;
+
+ this._customAddUrl = "";
+ this._customAddName = "";
+
+ this._search = "";
+ this._category = "all";
+ this._provider = "all"; // all|github|gitea|gitlab|other|custom
+
+ this._detailRepoId = null;
+ this._detailRepo = null;
+
+ this._readmeLoading = false;
+ this._readmeText = null;
+ this._readmeHtml = null;
+ this._readmeError = null;
+ }
+
+ set hass(hass) {
+ this._hass = hass;
+ if (!this._rendered) {
+ this._rendered = true;
+ this._render();
+ this._load();
+ }
+ }
+
+ async _load() {
+ if (!this._hass) return;
+
+ this._loading = true;
+ this._error = null;
+ this._update();
+
+ try {
+ const data = await this._hass.callApi("get", "bcs");
+ this._data = data;
+ } catch (e) {
+ this._error = e?.message ? String(e.message) : String(e);
+ } finally {
+ this._loading = false;
+ this._update();
+ }
+ }
+
+ _isDesktop() {
+ return window.matchMedia && window.matchMedia("(min-width: 1024px)").matches;
+ }
+
+ _toggleMenu() {
+ if (this._isDesktop()) return;
+ try {
+ const ev = new Event("hass-toggle-menu", { bubbles: true, composed: true });
+ this.dispatchEvent(ev);
+ } catch (_) {}
+ }
+
+ _goBack() {
+ if (this._view === "detail") {
+ this._view = "store";
+ this._detailRepoId = null;
+ this._detailRepo = null;
+ this._readmeText = null;
+ this._readmeHtml = null;
+ this._readmeError = null;
+ this._update();
+ return;
+ }
+ try {
+ history.back();
+ } catch (_) {
+ window.location.href = "/";
+ }
+ }
+
+ async _addCustomRepo() {
+ if (!this._hass) return;
+
+ const url = (this._customAddUrl || "").trim();
+ const name = (this._customAddName || "").trim() || null;
+
+ if (!url) {
+ this._error = "Please enter a repository URL.";
+ this._update();
+ return;
+ }
+
+ this._error = null;
+ this._update();
+
+ try {
+ await this._hass.callApi("post", "bcs", { op: "add_custom_repo", url, name });
+ this._customAddUrl = "";
+ this._customAddName = "";
+ await this._load();
+ this._view = "manage";
+ this._update();
+ } catch (e) {
+ this._error = e?.message ? String(e.message) : String(e);
+ this._update();
+ }
+ }
+
+ async _removeCustomRepo(id) {
+ if (!this._hass) return;
+
+ try {
+ await this._hass.callApi("delete", `bcs/custom_repo?id=${encodeURIComponent(id)}`);
+ await this._load();
+ this._view = "manage";
+ this._update();
+ } catch (e) {
+ this._error = e?.message ? String(e.message) : String(e);
+ this._update();
+ }
+ }
+
+ _openRepoDetail(repoId) {
+ const repos = Array.isArray(this._data?.repos) ? this._data.repos : [];
+ const repo = repos.find((r) => this._safeId(r?.id) === repoId);
+ if (!repo) return;
+
+ this._view = "detail";
+ this._detailRepoId = repoId;
+ this._detailRepo = repo;
+
+ this._readmeText = null;
+ this._readmeHtml = null;
+ this._readmeError = null;
+
+ this._update();
+ this._loadReadme(repoId);
+ }
+
+ async _loadReadme(repoId) {
+ if (!this._hass) return;
+ this._readmeLoading = true;
+ this._readmeError = null;
+ this._update();
+
+ try {
+ const resp = await this._hass.callApi(
+ "get",
+ `bcs/readme?repo_id=${encodeURIComponent(repoId)}`
+ );
+
+ if (resp?.ok && typeof resp.readme === "string" && resp.readme.trim()) {
+ this._readmeText = resp.readme;
+ this._readmeHtml = typeof resp.html === "string" && resp.html.trim() ? resp.html : null;
+ } else {
+ this._readmeText = null;
+ this._readmeHtml = null;
+ this._readmeError = this._safeText(resp?.message) || "README not found.";
+ }
+ } catch (e) {
+ this._readmeText = null;
+ this._readmeHtml = null;
+ this._readmeError = e?.message ? String(e.message) : String(e);
+ } finally {
+ this._readmeLoading = false;
+ this._update();
+ }
+ }
+
+ _render() {
+ const root = this.shadowRoot;
+
+ root.innerHTML = `
+
+
+
+
+
+
←
+
+
Bahmcloud Store
+
BCS — loading…
+
+
+
+ Refresh
+
+
+
+
+
+
Store
+
Manage repositories
+
Settings / About
+
+
+
+
+
+
+
+ `;
+
+ root.getElementById("refreshBtn").addEventListener("click", () => this._load());
+ root.getElementById("menuBtn").addEventListener("click", () => this._toggleMenu());
+ root.getElementById("backBtn").addEventListener("click", () => this._goBack());
+
+ for (const tab of root.querySelectorAll(".tab")) {
+ tab.addEventListener("click", () => {
+ this._view = tab.getAttribute("data-view");
+ this._update();
+ });
+ }
+
+ // prevent HA global shortcuts while typing
+ const stopIfFormField = (e) => {
+ const t = e.composedPath ? e.composedPath()[0] : e.target;
+ if (!t) return;
+ const tag = (t.tagName || "").toLowerCase();
+ const isEditable =
+ tag === "input" || tag === "textarea" || tag === "select" || t.isContentEditable;
+ if (isEditable) e.stopPropagation();
+ };
+ root.addEventListener("keydown", stopIfFormField, true);
+ root.addEventListener("keyup", stopIfFormField, true);
+ root.addEventListener("keypress", stopIfFormField, true);
+ }
+
+ _captureFocusState() {
+ const root = this.shadowRoot;
+ const ae = root.activeElement;
+ if (!ae || !ae.id) return null;
+
+ const supported = new Set(["searchInput", "categorySelect", "addUrl", "addName"]);
+ if (!supported.has(ae.id)) return null;
+
+ return {
+ id: ae.id,
+ value: ae.value,
+ selectionStart: typeof ae.selectionStart === "number" ? ae.selectionStart : null,
+ selectionEnd: typeof ae.selectionEnd === "number" ? ae.selectionEnd : null,
+ };
+ }
+
+ _restoreFocusState(state) {
+ if (!state) return;
+ const root = this.shadowRoot;
+ const el = root.getElementById(state.id);
+ if (!el) return;
+
+ try {
+ el.focus({ preventScroll: true });
+ if (
+ state.selectionStart !== null &&
+ state.selectionEnd !== null &&
+ typeof el.setSelectionRange === "function"
+ ) {
+ el.setSelectionRange(state.selectionStart, state.selectionEnd);
+ }
+ } catch (_) {}
+ }
+
+ _update() {
+ const root = this.shadowRoot;
+ const focusState = this._captureFocusState();
+
+ const content = root.getElementById("content");
+ const err = root.getElementById("error");
+ const subtitle = root.getElementById("subtitle");
+ const fabs = root.getElementById("fabs");
+
+ const v = this._safeText(this._data?.version);
+ subtitle.textContent = v ? `BCS ${v}` : "BCS — loading…";
+
+ for (const tab of root.querySelectorAll(".tab")) {
+ tab.classList.toggle("active", tab.getAttribute("data-view") === this._view);
+ }
+
+ err.textContent = this._error ? `Error: ${this._error}` : "";
+
+ fabs.innerHTML = this._view === "detail" ? this._renderFabs() : "";
+ this._wireFabs();
+
+ if (this._loading) {
+ content.innerHTML = `Loading…
`;
+ this._restoreFocusState(focusState);
+ return;
+ }
+
+ if (!this._data) {
+ content.innerHTML = `No data.
`;
+ this._restoreFocusState(focusState);
+ return;
+ }
+
+ if (this._view === "store") {
+ content.innerHTML = this._renderStore();
+ this._wireStore();
+ this._restoreFocusState(focusState);
+ return;
+ }
+
+ if (this._view === "manage") {
+ content.innerHTML = this._renderManage();
+ this._wireManage();
+ this._restoreFocusState(focusState);
+ return;
+ }
+
+ if (this._view === "about") {
+ content.innerHTML = this._renderAbout();
+ this._restoreFocusState(focusState);
+ return;
+ }
+
+ content.innerHTML = this._renderDetail();
+ this._wireDetail();
+ this._restoreFocusState(focusState);
+ }
+
+ _renderStore() {
+ const repos = Array.isArray(this._data.repos) ? this._data.repos : [];
+ const categories = this._computeCategories(repos);
+ const stats = this._computeProviderStats(repos);
+
+ const filtered = repos
+ .filter((r) => {
+ const q = (this._search || "").trim().toLowerCase();
+ if (!q) return true;
+ const hay = `${this._safeText(r?.name)} ${this._safeText(r?.description)} ${this._safeText(r?.url)} ${this._safeText(r?.owner)}`.toLowerCase();
+ return hay.includes(q);
+ })
+ .filter((r) => {
+ if (this._category === "all") return true;
+ return this._safeLower(r?.category) === this._category;
+ })
+ .filter((r) => {
+ if (this._provider === "all") return true;
+ if (this._provider === "custom") return r?.source === "custom";
+ return this._safeLower(r?.provider) === this._provider;
+ });
+
+ const options = [
+ `All categories `,
+ ...categories.map(
+ (c) =>
+ `${this._esc(c)} `
+ ),
+ ].join("");
+
+ const providerChips = this._renderProviderChips(stats);
+
+ const rows = filtered
+ .map((r) => {
+ const id = this._safeId(r?.id);
+ const name = this._safeText(r?.name) || "Unnamed repository";
+ const desc = this._safeText(r?.description) || "No description available.";
+
+ const badge =
+ r?.source === "custom"
+ ? `Custom `
+ : `Index `;
+
+ const creator = this._safeText(r?.owner) ? `Creator: ${this._safeText(r?.owner)}` : "Creator: -";
+ const latest = this._safeText(r?.latest_version) ? `Latest: ${this._safeText(r?.latest_version)}` : "Latest: unknown";
+ const cat = this._safeText(r?.category) ? `Category: ${this._safeText(r?.category)}` : null;
+ const metaSrc = this._safeText(r?.meta_source) ? `Meta: ${this._safeText(r?.meta_source)}` : null;
+
+ const lineBits = [creator, latest, cat, metaSrc].filter(Boolean);
+
+ return `
+
+
+
+
${this._esc(name)}
+
${this._esc(desc)}
+
${this._esc(lineBits.join(" · "))}
+
+ ${badge}
+
+
+ `;
+ })
+ .join("");
+
+ return `
+
+
Providers
+
${providerChips}
+
+
+
+
+ ${options}
+
+
+ ${rows || `No repositories match this filter.
`}
+ `;
+ }
+
+ _wireStore() {
+ const root = this.shadowRoot;
+ const search = root.getElementById("searchInput");
+ const cat = root.getElementById("categorySelect");
+ const chips = root.getElementById("providerChips");
+
+ if (search) {
+ search.addEventListener("input", (e) => {
+ this._search = e.target.value;
+ this._update();
+ });
+ }
+
+ if (cat) {
+ cat.addEventListener("change", (e) => {
+ this._category = String(e.target.value || "all");
+ this._update();
+ });
+ }
+
+ if (chips) {
+ for (const c of chips.querySelectorAll("[data-prov]")) {
+ c.addEventListener("click", () => {
+ const key = c.getAttribute("data-prov");
+ if (!key) return;
+ this._provider = key;
+ this._update();
+ });
+ }
+ }
+
+ for (const card of root.querySelectorAll("[data-repo]")) {
+ card.addEventListener("click", () => {
+ const id = card.getAttribute("data-repo");
+ if (id) this._openRepoDetail(id);
+ });
+ }
+ }
+
+ _renderProviderChips(stats) {
+ const order = ["all", "github", "gitea", "gitlab", "other", "custom"];
+ const labels = {
+ all: "All",
+ github: "GitHub",
+ gitea: "Gitea",
+ gitlab: "GitLab",
+ other: "Other",
+ custom: "Custom",
+ };
+ const values = {
+ all: stats.total,
+ github: stats.github,
+ gitea: stats.gitea,
+ gitlab: stats.gitlab,
+ other: stats.other,
+ custom: stats.custom,
+ };
+
+ return order
+ .map((key) => {
+ const count = values[key] ?? 0;
+ const active = this._provider === key ? " active" : "";
+ return `${labels[key]} ${count}
`;
+ })
+ .join("");
+ }
+
+ _computeProviderStats(repos) {
+ const s = { total: 0, github: 0, gitea: 0, gitlab: 0, other: 0, custom: 0 };
+ if (!Array.isArray(repos)) return s;
+
+ for (const r of repos) {
+ s.total += 1;
+
+ if (r?.source === "custom") s.custom += 1;
+
+ const p = this._safeLower(r?.provider) || "other";
+ if (p === "github") s.github += 1;
+ else if (p === "gitea") s.gitea += 1;
+ else if (p === "gitlab") s.gitlab += 1;
+ else s.other += 1;
+ }
+ return s;
+ }
+
+ _renderDetail() {
+ const r = this._detailRepo;
+ if (!r) return `Repository not found.
`;
+
+ const name = this._safeText(r?.name) || "Unnamed repository";
+ const desc = this._safeText(r?.description) || "No description available.";
+ const url = this._safeText(r?.url) || "#";
+
+ const badge =
+ r?.source === "custom"
+ ? `Custom `
+ : `Index `;
+
+ const latest = this._safeText(r?.latest_version) ? `Latest: ${this._safeText(r?.latest_version)}` : "Latest: unknown";
+
+ const infoBits = [
+ this._safeText(r?.owner) ? `Creator: ${this._safeText(r?.owner)}` : "Creator: -",
+ latest,
+ this._safeText(r?.provider) ? `Provider: ${this._safeText(r?.provider)}` : null,
+ this._safeText(r?.category) ? `Category: ${this._safeText(r?.category)}` : null,
+ this._safeText(r?.meta_author) ? `Author: ${this._safeText(r?.meta_author)}` : null,
+ this._safeText(r?.meta_maintainer) ? `Maintainer: ${this._safeText(r?.meta_maintainer)}` : null,
+ this._safeText(r?.meta_source) ? `Meta: ${this._safeText(r?.meta_source)}` : null,
+ ].filter(Boolean);
+
+ const readmeBlock = this._readmeLoading
+ ? `Loading README…
`
+ : this._readmeText
+ ? `
+
+
+
README
+
Rendered Markdown
+
+
+
+
+
+ Show raw Markdown
+
+
${this._esc(this._readmeText)}
+
+
+
+ `
+ : `
+
+
README
+
${this._esc(this._readmeError || "README not found.")}
+
+ `;
+
+ return `
+
+
+
+
+
+
${this._esc(name)}
+
${this._esc(desc)}
+
${this._esc(infoBits.join(" · "))}
+
+
+ ${badge}
+
+
+
+ ${readmeBlock}
+
+
+
+
+
Installation & Updates
+
+ Installation and updates are performed manually via Settings → System → Updates .
+ The Store UI is used to browse repositories and trigger installation/update actions.
+
+
+ Updates remain manual (like HACS).
+
+
+
+
+ `;
+ }
+
+ _wireDetail() {
+ const root = this.shadowRoot;
+ const mount = root.getElementById("readmePretty");
+ if (!mount) return;
+
+ if (this._readmeText) {
+ // Client renderer may be unavailable; prefer server-provided HTML
+ if (this._readmeHtml) {
+ mount.innerHTML = this._readmeHtml;
+ this._postprocessRenderedMarkdown(mount);
+ return;
+ }
+ mount.innerHTML = `Rendered HTML not available. Use “Show raw Markdown”.
`;
+ return;
+ }
+
+ mount.innerHTML = "";
+ }
+
+ _postprocessRenderedMarkdown(container) {
+ if (!container) return;
+ try {
+ const links = container.querySelectorAll("a[href]");
+ links.forEach((a) => {
+ a.setAttribute("target", "_blank");
+ a.setAttribute("rel", "noreferrer noopener");
+ });
+ } catch (_) {}
+ }
+
+ _renderFabs() {
+ const r = this._detailRepo;
+ if (!r) return "";
+
+ return `
+
+ `;
+ }
+
+ _wireFabs() {
+ const root = this.shadowRoot;
+ const r = this._detailRepo;
+ if (!r) return;
+
+ const url = this._safeText(r?.url);
+
+ const open = root.getElementById("fabOpen");
+ const reload = root.getElementById("fabReload");
+ const info = root.getElementById("fabInfo");
+
+ if (open) open.addEventListener("click", () => url && window.open(url, "_blank", "noreferrer"));
+ if (reload) {
+ reload.addEventListener("click", () => {
+ if (this._detailRepoId) this._loadReadme(this._detailRepoId);
+ });
+ }
+ if (info) {
+ info.addEventListener("click", () => {
+ this._view = "about";
+ this._update();
+ });
+ }
+ }
+
+ _renderManage() {
+ const repos = Array.isArray(this._data.repos) ? this._data.repos : [];
+ const custom = repos.filter((r) => r?.source === "custom");
+
+ const list = custom
+ .map((r) => {
+ const id = this._safeId(r?.id);
+ const name = this._safeText(r?.name) || "Unnamed repository";
+ const url = this._safeText(r?.url) || "";
+ return `
+
+
+
+
${this._esc(name)}
+
${this._esc(url)}
+
+
+ Remove
+
+
+
+ `;
+ })
+ .join("");
+
+ return `
+
+
Manage repositories
+
Add public repositories from any git provider.
+
+
+
+
+
+
Display name (optional)
+
+
+
+
+ Add repository
+
+
+
+
+ ${list || `No custom repositories added yet.
`}
+ `;
+ }
+
+ _wireManage() {
+ const root = this.shadowRoot;
+
+ const addUrl = root.getElementById("addUrl");
+ const addName = root.getElementById("addName");
+ const addBtn = root.getElementById("addBtn");
+
+ if (addUrl) addUrl.addEventListener("input", (e) => (this._customAddUrl = e.target.value));
+ if (addName) addName.addEventListener("input", (e) => (this._customAddName = e.target.value));
+ if (addBtn) addBtn.addEventListener("click", () => this._addCustomRepo());
+
+ for (const btn of root.querySelectorAll("[data-remove]")) {
+ btn.addEventListener("click", () => {
+ const id = btn.getAttribute("data-remove");
+ if (id) this._removeCustomRepo(id);
+ });
+ }
+ }
+
+ _renderAbout() {
+ const v = this._safeText(this._data?.version) || "-";
+ return `
+
+
Settings / About
+
Language: English (v1). i18n will be added later.
+
Theme: follows Home Assistant light/dark automatically.
+
Accent: Bahmcloud Blue.
+
BCS version: ${this._esc(v)}
+
+ `;
+ }
+
+ _computeCategories(repos) {
+ const set = new Set();
+ for (const r of repos) {
+ const c = this._safeLower(r?.category);
+ if (c) set.add(c);
+ }
+ return Array.from(set).sort();
+ }
+
+ // --- HARDENING HELPERS (fixes [object Object]) ---
+ _safeText(v) {
+ if (v === null || v === undefined) return "";
+ const t = typeof v;
+ if (t === "string") return v;
+ if (t === "number" || t === "boolean") return String(v);
+ return ""; // objects/arrays/functions => empty (prevents [object Object])
+ }
+
+ _safeLower(v) {
+ const s = this._safeText(v);
+ return s ? s.trim().toLowerCase() : "";
+ }
+
+ _safeId(v) {
+ const s = this._safeText(v);
+ return s || "";
+ }
+
+ _esc(s) {
+ return String(s ?? "")
+ .replaceAll("&", "&")
+ .replaceAll("<", "<")
+ .replaceAll(">", ">")
+ .replaceAll('"', """)
+ .replaceAll("'", "'");
+ }
+}
+
+customElements.define("bahmcloud-store-panel", BahmcloudStorePanel);
diff --git a/custom_components/bahmcloud_store/styles.css b/custom_components/bahmcloud_store/styles.css
new file mode 100644
index 0000000..46c2faf
--- /dev/null
+++ b/custom_components/bahmcloud_store/styles.css
@@ -0,0 +1,10 @@
+body { font-family: system-ui, sans-serif; margin:0; }
+.wrap { padding: 16px; max-width: 1000px; margin: 0 auto; }
+.card { border: 1px solid #ddd; border-radius: 10px; padding: 12px; margin: 10px 0; }
+.row { display:flex; justify-content:space-between; gap: 12px; align-items: flex-start; }
+.badge { border: 1px solid #bbb; border-radius: 999px; padding: 2px 8px; font-size: 12px; height: fit-content; }
+.muted { color: #666; font-size: 13px; margin-top: 4px; }
+.actions { display:flex; gap: 8px; margin-top: 10px; }
+button { padding: 8px 12px; cursor:pointer; }
+button[disabled] { opacity: 0.6; cursor: not-allowed; }
+a { color: inherit; }