From 3496b4a2df4a00a6a93cdac19ee24f2f1b78c4af Mon Sep 17 00:00:00 2001 From: schakal00783 Date: Sun, 2 Aug 2026 23:14:41 +0200 Subject: [PATCH] feat(gui): gruene Nadel bei angehefteter Aufnahme, Logo fuehrt zu den Aufnahmen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Der Anheften-Knopf zeigte bei angehefteter Aufnahme nur die Zahl, die Nadel blieb rot. Sie ist jetzt gruen (--ok, wie das "KI ✓"-Abzeichen). Dafuer ersetzt ein SVG mit currentColor (ICON_PIN, gleiches Muster wie ICON_TABLE) das 📌-Emoji: ein Farb-Emoji rendert immer rot, color greift daran nicht. Klick auf Wellen-Logo und Schriftzug wechselt auf den Reiter "Aufnahmen". Co-Authored-By: Claude Opus 5 (1M context) --- app/web/src/main.js | 14 +- app/web/src/style.css | 318 +++++++++++++++++++------------------- docs/08-changelog.md | 12 ++ docs/10-projektansicht.md | 8 +- 4 files changed, 192 insertions(+), 160 deletions(-) diff --git a/app/web/src/main.js b/app/web/src/main.js index 9f3b3a3..66f0c68 100644 --- a/app/web/src/main.js +++ b/app/web/src/main.js @@ -33,7 +33,7 @@ function render() { const theme = document.documentElement.dataset.theme || 'dark'; app.innerHTML = `
-
+

Aime Anruf-Assistent

@@ -53,6 +53,12 @@ function render() {
`; app.querySelectorAll('.tab').forEach(b => b.onclick = () => { state.tab = b.dataset.tab; render(); }); + // Logo + Schriftzug = Heimweg zurück auf die Aufnahmen. + app.querySelector('#brandhome').onclick = () => { + if (state.tab === 'recordings') return; + state.tab = 'recordings'; + render(); + }; app.querySelector('#themetoggle').onclick = () => applyTheme(document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark'); app.querySelector('#recbtn').onclick = toggleRecord; @@ -903,7 +909,7 @@ async function refreshRecList() { ${r.summary ? `
${escapeHtml(r.summary)}
` : ''} + title="${pinCounts[r.id] ? `In ${pinCounts[r.id]} Projekt(en) angeheftet — verwalten` : 'An ein Projekt anheften'}">${ICON_PIN}${pinCounts[r.id] ? `${pinCounts[r.id]}` : ''} `; }).join(''); @@ -1500,6 +1506,10 @@ const BUBBLE_W = 54; function tableSeats(n) { return SEAT_LAYOUTS[Math.min(6, Math.max(1, n))] || SEAT_LAYOUTS[6]; } // Toggle-button icons: three people around a table / a text sheet. +// Stecknadel als SVG statt 📌: ein Farb-Emoji bleibt immer rot, `currentColor` +// lässt sich dagegen einfärben — angeheftet = grün, sonst blass wie die +// übrigen Zeilenknöpfe. +const ICON_PIN = ``; const ICON_TABLE = ``; const ICON_SHEET = ``; diff --git a/app/web/src/style.css b/app/web/src/style.css index 362879c..2b5021b 100644 --- a/app/web/src/style.css +++ b/app/web/src/style.css @@ -395,158 +395,166 @@ body.resizing { cursor: col-resize; user-select: none; } @media (prefers-reduced-motion: reduce) { * { transition: none !important; animation: none !important; } } - -/* ========================================================================== - Suche — Google-Anmutung, aber in der Aime-Palette: viel Weißraum, eine - dominante Eingabezeile, Treffer als Textblöcke ohne Karten-Rahmen. - ========================================================================== */ -.search-page { max-width: 46rem; margin: 0 auto; padding: 1rem 1rem 4rem; } -/* Startzustand: Suchfeld optisch mittig, wie eine leere Suchseite. */ -.search-page.landing { padding-top: 12vh; } -.search-page.has-results { padding-top: 1.25rem; } - -.search-box { - display: flex; align-items: center; gap: .5rem; - border: 1px solid var(--line); border-radius: 2rem; - background: var(--surface); box-shadow: var(--shadow); - padding: .35rem .35rem .35rem 1rem; - transition: box-shadow .15s, border-color .15s; -} -.search-box:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); } -.search-ico { opacity: .55; font-size: 1rem; } -.search-input { - flex: 1; border: 0; outline: 0; background: transparent; color: var(--fg); - font: inherit; font-size: 1.05rem; padding: .55rem 0; -} -.search-clear { border: 0; background: transparent; color: var(--muted); cursor: pointer; font-size: .95rem; padding: .25rem .4rem; } -.search-clear:hover { color: var(--fg); } -.search-go { - border: 0; border-radius: 1.6rem; cursor: pointer; - background: var(--accent); color: var(--accent-fg); - font: inherit; font-weight: 600; padding: .55rem 1.1rem; -} -.search-go:hover { filter: brightness(1.07); } - -.search-hints { margin-top: 1.5rem; display: flex; flex-direction: column; gap: .5rem; align-items: flex-start; } -.search-hints .example { - border: 1px solid var(--line); border-radius: 1rem; background: var(--surface-2); - color: var(--fg); font: inherit; font-size: .88rem; padding: .35rem .8rem; cursor: pointer; text-align: left; -} -.search-hints .example:hover { border-color: var(--accent); color: var(--accent); } - -.searching { padding: 1.5rem 0; } -.search-empty { padding: 1.5rem 0; } - -/* Antwortkasten: abgesetzt wie eine KI-Übersicht, nicht wie ein Treffer. */ -.answer-box { - margin: 1.25rem 0 .5rem; padding: .9rem 1rem; - background: var(--accent-soft); border: 1px solid var(--line); border-radius: .75rem; -} -.answer-head { font-weight: 600; font-size: .82rem; letter-spacing: .04em; text-transform: uppercase; color: var(--muted); margin-bottom: .5rem; } -.answer-body { line-height: 1.55; white-space: pre-wrap; } -/* Platzhalterbalken, solange das LLM rechnet (30-90 s sind hier normal). */ -.answer-body.skeleton { display: flex; flex-direction: column; gap: .45rem; } -.answer-body.skeleton span { - height: .7rem; border-radius: .35rem; background: var(--line); opacity: .55; - animation: pulse 1.4s ease-in-out infinite; -} -.answer-body.skeleton span:nth-child(1) { width: 92%; } -.answer-body.skeleton span:nth-child(2) { width: 78%; animation-delay: .18s; } -.answer-body.skeleton span:nth-child(3) { width: 45%; animation-delay: .36s; } -@keyframes pulse { 0%, 100% { opacity: .3; } 50% { opacity: .7; } } -.answer-src { margin-top: .7rem; display: flex; flex-wrap: wrap; gap: .4rem; } -.src-chip { - border: 1px solid var(--line); background: var(--surface); color: var(--fg); - border-radius: 1rem; font: inherit; font-size: .8rem; padding: .2rem .6rem; cursor: pointer; -} -.src-chip:hover { border-color: var(--accent); color: var(--accent); } - -.result-meta { margin: .9rem 0 .3rem; } -.results { list-style: none; margin: 0; padding: 0; } -.result { padding: .85rem 0; cursor: pointer; border-bottom: 1px solid var(--line); } -.result:hover .result-title { text-decoration: underline; } -.result-src { color: var(--muted); font-size: .78rem; margin-bottom: .15rem; } -.result-title { display: block; color: var(--accent); font-size: 1.05rem; text-decoration: none; } -.result-at { color: var(--muted); font-size: .8rem; font-weight: 400; margin-left: .4rem; } -.result-snip { color: var(--fg); font-size: .9rem; line-height: 1.5; margin-top: .2rem; } -.result-snip mark { background: var(--warn-soft); color: inherit; font-weight: 600; padding: 0 .1em; border-radius: .15em; } - -/* ========================================================================== - Papierkorb + Umbenennen - ========================================================================== */ -.list-bar { display: flex; justify-content: flex-end; padding: .35rem .5rem; border-bottom: 1px solid var(--line); } -.btn.ghost { background: transparent; border-color: transparent; color: var(--muted); } -.btn.ghost:hover { color: var(--fg); border-color: var(--line); } -.btn.ghost.active { color: var(--accent); border-color: var(--accent); background: var(--accent-soft); } -.trash-hint, .trash-empty { padding: .6rem .75rem; } -.item.rec.trashed { opacity: .8; } -.item.rec.trashed b { text-decoration: line-through; text-decoration-color: var(--muted); } -.days-left { color: var(--muted); } -.days-left.urgent { color: var(--danger); font-weight: 600; } -/* Der Stift sitzt neben der H2 des Detailkopfs, ohne die Zeile umzubrechen. */ -.rename-btn { vertical-align: middle; margin-left: -.25rem; opacity: .55; } -.rename-btn:hover { opacity: 1; } -.item.rec [data-ren] { opacity: .45; } -.item.rec:hover [data-ren] { opacity: 1; } - -@media (max-width: 640px) { - .search-page.landing { padding-top: 6vh; } - .search-go { padding: .55rem .8rem; } -} -/* Abschnittstitel in der Zusammenfassung ("Neuer Projektleiter vorgestellt:"). - Etwas Luft darüber, damit die Blöcke optisch trennen — der Text steht in - white-space: pre-line, die Zeilenumbrüche kommen also aus dem Text selbst. */ -.summary .sum-head { - font-weight: 700; - color: var(--fg); - line-height: 2.2; -} -/* ---- Projektansicht --------------------------------------------------------- - Ordnergitter (Ebene 1) und darin eine Reiterleiste für die Unterreiter - (Ebene 2). Die Reiter sehen bewusst anders aus als die Kopfzeilen-Tabs, damit - klar bleibt: das ist eine Ebene INNERHALB eines Ordners. */ -.proj-head { display: flex; align-items: center; gap: 10px; margin-bottom: 4px; } -.proj-head h3 { margin: 0; flex: 1; } -.proj-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 10px; margin-top: 10px; } -.proj-card { - display: flex; align-items: center; gap: 10px; cursor: pointer; - background: var(--surface-2); border: 1px solid var(--line); - border-radius: 10px; padding: 12px 14px; transition: border-color .1s, background .1s; -} -.proj-card:hover { border-color: var(--accent); } -.proj-card .grow { flex: 1; min-width: 0; } -.proj-card b { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.proj-ico { font-size: 22px; flex: none; } - -.proj-tabs { display: flex; gap: 6px; flex-wrap: wrap; margin: 14px 0 0; border-bottom: 1px solid var(--line); padding-bottom: 8px; } -.ptab { - background: transparent; border: 1px solid transparent; color: var(--muted); - border-radius: 8px 8px 0 0; padding: 6px 12px; cursor: pointer; font: inherit; font-size: .9rem; -} -.ptab:hover { color: var(--fg); background: var(--surface-2); } -.ptab.active { color: var(--accent); background: var(--accent-soft); border-color: var(--accent); font-weight: 600; } -.ptab.add { color: var(--muted); border-style: dashed; border-color: var(--line); } -.ptab.add:hover { color: var(--accent); border-color: var(--accent); } -.ptab-n { font-size: 11px; opacity: .7; margin-left: 4px; } - -.proj-level { margin-top: 14px; } -.proj-level-head { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; flex-wrap: wrap; } -.proj-level-head .grow { flex: 1; } -.proj-level .item.rec { cursor: pointer; } - -/* 📌 in der Aufnahmenliste: blass, solange die Aufnahme nirgends hängt. */ -.pinbtn { opacity: .45; position: relative; } -.item.rec:hover .pinbtn { opacity: 1; } -.pinbtn.pinned { opacity: 1; border-color: var(--accent); color: var(--accent); } -.pin-n { font-size: 10px; font-weight: 700; margin-left: 2px; vertical-align: super; } - -/* ---- Modal ---- */ -.modal-back { - position: fixed; inset: 0; background: rgba(0, 0, 0, .5); - display: flex; align-items: center; justify-content: center; z-index: 50; padding: 20px; -} -.modal { width: min(520px, 100%); max-height: 85vh; overflow: auto; margin: 0; } -.modal h3 { margin-top: 0; } -.modal-foot { display: flex; justify-content: flex-end; gap: 8px; margin-top: 16px; } -.modal select[size] { height: auto; padding: 4px; } -.pin-current .item { padding: 6px 10px; } + +/* ========================================================================== + Suche — Google-Anmutung, aber in der Aime-Palette: viel Weißraum, eine + dominante Eingabezeile, Treffer als Textblöcke ohne Karten-Rahmen. + ========================================================================== */ +.search-page { max-width: 46rem; margin: 0 auto; padding: 1rem 1rem 4rem; } +/* Startzustand: Suchfeld optisch mittig, wie eine leere Suchseite. */ +.search-page.landing { padding-top: 12vh; } +.search-page.has-results { padding-top: 1.25rem; } + +.search-box { + display: flex; align-items: center; gap: .5rem; + border: 1px solid var(--line); border-radius: 2rem; + background: var(--surface); box-shadow: var(--shadow); + padding: .35rem .35rem .35rem 1rem; + transition: box-shadow .15s, border-color .15s; +} +.search-box:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); } +.search-ico { opacity: .55; font-size: 1rem; } +.search-input { + flex: 1; border: 0; outline: 0; background: transparent; color: var(--fg); + font: inherit; font-size: 1.05rem; padding: .55rem 0; +} +.search-clear { border: 0; background: transparent; color: var(--muted); cursor: pointer; font-size: .95rem; padding: .25rem .4rem; } +.search-clear:hover { color: var(--fg); } +.search-go { + border: 0; border-radius: 1.6rem; cursor: pointer; + background: var(--accent); color: var(--accent-fg); + font: inherit; font-weight: 600; padding: .55rem 1.1rem; +} +.search-go:hover { filter: brightness(1.07); } + +.search-hints { margin-top: 1.5rem; display: flex; flex-direction: column; gap: .5rem; align-items: flex-start; } +.search-hints .example { + border: 1px solid var(--line); border-radius: 1rem; background: var(--surface-2); + color: var(--fg); font: inherit; font-size: .88rem; padding: .35rem .8rem; cursor: pointer; text-align: left; +} +.search-hints .example:hover { border-color: var(--accent); color: var(--accent); } + +.searching { padding: 1.5rem 0; } +.search-empty { padding: 1.5rem 0; } + +/* Antwortkasten: abgesetzt wie eine KI-Übersicht, nicht wie ein Treffer. */ +.answer-box { + margin: 1.25rem 0 .5rem; padding: .9rem 1rem; + background: var(--accent-soft); border: 1px solid var(--line); border-radius: .75rem; +} +.answer-head { font-weight: 600; font-size: .82rem; letter-spacing: .04em; text-transform: uppercase; color: var(--muted); margin-bottom: .5rem; } +.answer-body { line-height: 1.55; white-space: pre-wrap; } +/* Platzhalterbalken, solange das LLM rechnet (30-90 s sind hier normal). */ +.answer-body.skeleton { display: flex; flex-direction: column; gap: .45rem; } +.answer-body.skeleton span { + height: .7rem; border-radius: .35rem; background: var(--line); opacity: .55; + animation: pulse 1.4s ease-in-out infinite; +} +.answer-body.skeleton span:nth-child(1) { width: 92%; } +.answer-body.skeleton span:nth-child(2) { width: 78%; animation-delay: .18s; } +.answer-body.skeleton span:nth-child(3) { width: 45%; animation-delay: .36s; } +@keyframes pulse { 0%, 100% { opacity: .3; } 50% { opacity: .7; } } +.answer-src { margin-top: .7rem; display: flex; flex-wrap: wrap; gap: .4rem; } +.src-chip { + border: 1px solid var(--line); background: var(--surface); color: var(--fg); + border-radius: 1rem; font: inherit; font-size: .8rem; padding: .2rem .6rem; cursor: pointer; +} +.src-chip:hover { border-color: var(--accent); color: var(--accent); } + +.result-meta { margin: .9rem 0 .3rem; } +.results { list-style: none; margin: 0; padding: 0; } +.result { padding: .85rem 0; cursor: pointer; border-bottom: 1px solid var(--line); } +.result:hover .result-title { text-decoration: underline; } +.result-src { color: var(--muted); font-size: .78rem; margin-bottom: .15rem; } +.result-title { display: block; color: var(--accent); font-size: 1.05rem; text-decoration: none; } +.result-at { color: var(--muted); font-size: .8rem; font-weight: 400; margin-left: .4rem; } +.result-snip { color: var(--fg); font-size: .9rem; line-height: 1.5; margin-top: .2rem; } +.result-snip mark { background: var(--warn-soft); color: inherit; font-weight: 600; padding: 0 .1em; border-radius: .15em; } + +/* ========================================================================== + Papierkorb + Umbenennen + ========================================================================== */ +.list-bar { display: flex; justify-content: flex-end; padding: .35rem .5rem; border-bottom: 1px solid var(--line); } +.btn.ghost { background: transparent; border-color: transparent; color: var(--muted); } +.btn.ghost:hover { color: var(--fg); border-color: var(--line); } +.btn.ghost.active { color: var(--accent); border-color: var(--accent); background: var(--accent-soft); } +.trash-hint, .trash-empty { padding: .6rem .75rem; } +.item.rec.trashed { opacity: .8; } +.item.rec.trashed b { text-decoration: line-through; text-decoration-color: var(--muted); } +.days-left { color: var(--muted); } +.days-left.urgent { color: var(--danger); font-weight: 600; } +/* Der Stift sitzt neben der H2 des Detailkopfs, ohne die Zeile umzubrechen. */ +.rename-btn { vertical-align: middle; margin-left: -.25rem; opacity: .55; } +.rename-btn:hover { opacity: 1; } +.item.rec [data-ren] { opacity: .45; } +.item.rec:hover [data-ren] { opacity: 1; } + +@media (max-width: 640px) { + .search-page.landing { padding-top: 6vh; } + .search-go { padding: .55rem .8rem; } +} +/* Abschnittstitel in der Zusammenfassung ("Neuer Projektleiter vorgestellt:"). + Etwas Luft darüber, damit die Blöcke optisch trennen — der Text steht in + white-space: pre-line, die Zeilenumbrüche kommen also aus dem Text selbst. */ +.summary .sum-head { + font-weight: 700; + color: var(--fg); + line-height: 2.2; +} +/* ---- Projektansicht --------------------------------------------------------- + Ordnergitter (Ebene 1) und darin eine Reiterleiste für die Unterreiter + (Ebene 2). Die Reiter sehen bewusst anders aus als die Kopfzeilen-Tabs, damit + klar bleibt: das ist eine Ebene INNERHALB eines Ordners. */ +.proj-head { display: flex; align-items: center; gap: 10px; margin-bottom: 4px; } +.proj-head h3 { margin: 0; flex: 1; } +.proj-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 10px; margin-top: 10px; } +.proj-card { + display: flex; align-items: center; gap: 10px; cursor: pointer; + background: var(--surface-2); border: 1px solid var(--line); + border-radius: 10px; padding: 12px 14px; transition: border-color .1s, background .1s; +} +.proj-card:hover { border-color: var(--accent); } +.proj-card .grow { flex: 1; min-width: 0; } +.proj-card b { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.proj-ico { font-size: 22px; flex: none; } + +.proj-tabs { display: flex; gap: 6px; flex-wrap: wrap; margin: 14px 0 0; border-bottom: 1px solid var(--line); padding-bottom: 8px; } +.ptab { + background: transparent; border: 1px solid transparent; color: var(--muted); + border-radius: 8px 8px 0 0; padding: 6px 12px; cursor: pointer; font: inherit; font-size: .9rem; +} +.ptab:hover { color: var(--fg); background: var(--surface-2); } +.ptab.active { color: var(--accent); background: var(--accent-soft); border-color: var(--accent); font-weight: 600; } +.ptab.add { color: var(--muted); border-style: dashed; border-color: var(--line); } +.ptab.add:hover { color: var(--accent); border-color: var(--accent); } +.ptab-n { font-size: 11px; opacity: .7; margin-left: 4px; } + +.proj-level { margin-top: 14px; } +.proj-level-head { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; flex-wrap: wrap; } +.proj-level-head .grow { flex: 1; } +.proj-level .item.rec { cursor: pointer; } + +/* Stecknadel in der Aufnahmenliste: blass, solange die Aufnahme nirgends + hängt — angeheftet wird sie gruen (dieselbe Farbe wie „KI ✓“, also + „erledigt/zugeordnet“). Das Icon ist ein SVG mit currentColor; ein 📌-Emoji + wäre in jedem Zustand rot geblieben. */ +.pinbtn { opacity: .45; position: relative; display: inline-flex; align-items: center; gap: 1px; } +.item.rec:hover .pinbtn { opacity: 1; } +.pinbtn.pinned { opacity: 1; border-color: var(--ok); color: var(--ok); background: var(--ok-soft); } +.pin-n { font-size: 10px; font-weight: 700; vertical-align: super; } + +/* ---- Modal ---- */ +.modal-back { + position: fixed; inset: 0; background: rgba(0, 0, 0, .5); + display: flex; align-items: center; justify-content: center; z-index: 50; padding: 20px; +} +.modal { width: min(520px, 100%); max-height: 85vh; overflow: auto; margin: 0; } +.modal h3 { margin-top: 0; } +.modal-foot { display: flex; justify-content: flex-end; gap: 8px; margin-top: 16px; } +.modal select[size] { height: auto; padding: 4px; } +.pin-current .item { padding: 6px 10px; } + +/* Logo + Schriftzug führen zurück auf die Aufnahmen. */ +.brand { cursor: pointer; user-select: none; } +.brand:hover .brand-wave { filter: brightness(1.15); } +.brand:hover h1 { color: var(--accent); } diff --git a/docs/08-changelog.md b/docs/08-changelog.md index c74fc55..2dc9a70 100644 --- a/docs/08-changelog.md +++ b/docs/08-changelog.md @@ -3,6 +3,18 @@ Chronik der Entwicklungsschritte. **Regel: jede zukünftige Änderung bekommt hier einen Eintrag** (Datum, Was, Warum, betroffene Dateien/Backups). +## 2026-08-02 (nachgereicht) — Grüne Nadel, Logo führt zurück zu den Aufnahmen + +**Nadel grün statt rot:** der Anheften-Knopf in der Aufnahmenliste zeigte bei einer angehefteten Aufnahme +nur die Zahl, die Nadel selbst blieb rot. Sie ist jetzt grün (`--ok`, dieselbe Farbe wie das „KI ✓"-Abzeichen). +Dafür musste das **📌-Emoji einem SVG mit `currentColor` weichen** (`ICON_PIN`, gleiches Muster wie +`ICON_TABLE`/`ICON_SHEET`): ein Farb-Emoji rendert die Schriftart immer rot, `color` greift daran nicht. + +**Logo als Heimweg:** ein Klick auf Wellen-Logo und Schriftzug „Aime" wechselt auf den Reiter „Aufnahmen" +(`#brandhome` in `render()`), mit Zeiger-Cursor und Hover-Andeutung als Hinweis darauf. + +Dateien: `app/web/src/main.js`, `app/web/src/style.css`. + ## 2026-08-02 (nachgereicht) — Ordner-Ebene umbenennbar Der erste Reiter eines Projekts hieß fest „Ordner". Er trägt jetzt einen frei wählbaren Namen, genau wie diff --git a/docs/10-projektansicht.md b/docs/10-projektansicht.md index 56d1169..6fbfeac 100644 --- a/docs/10-projektansicht.md +++ b/docs/10-projektansicht.md @@ -84,9 +84,11 @@ flowchart LR end ``` -Der 📌-Knopf in der Aufnahmenliste trägt die Zahl der Projekte, in denen die Aufnahme hängt, und ist -farbig markiert, sobald sie irgendwo angeheftet ist (`GET /api/pins/counts` — ein Request für die ganze -Liste, nicht einer pro Zeile). Die Zahl steckt zusätzlich in der Listen-Signatur `state.lastListSig`, damit +Der Nadel-Knopf in der Aufnahmenliste trägt die Zahl der Projekte, in denen die Aufnahme hängt, und wird +**grün** (`--ok`, dieselbe Farbe wie „KI ✓"), sobald sie irgendwo angeheftet ist — sonst bleibt er blass +wie die übrigen Zeilenknöpfe (`GET /api/pins/counts` — ein Request für die ganze Liste, nicht einer pro +Zeile). Die Nadel ist deshalb ein **SVG mit `currentColor` (`ICON_PIN`) und kein 📌-Emoji**: ein +Farb-Emoji ist in jedem Zustand rot und lässt sich per CSS nicht umfärben. Die Zahl steckt zusätzlich in der Listen-Signatur `state.lastListSig`, damit eine neue Anheftung die Liste neu zeichnet, ohne die 5-Sekunden-Regel („keine Änderung → DOM nicht anfassen") aufzuweichen.