# Desktop Radius & Button Consistency Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Unify radius scale and button system across both desktop frontends (`tauri/src/` online wallet and `ui/static/` offline signer), and normalize stray blue hover colors to the existing orange accent.

**Architecture:** Single comment-fenced token block at the top of both stylesheets, kept byte-identical by a unittest check. New utility classes (`.btn-chip`, `.btn-icon`, `.input-adornment`, `.form-row-inline`, `.form-actions`) replace ~10 one-off button selectors. Three button-layout patterns (adornment inside input / inline beside input / stacked below form) chosen by what the button does.

**Tech Stack:** CSS custom properties, vanilla HTML/JS, Python `unittest` for consistency assertions.

## Global Constraints

- **Radius scale:** `--radius-xs: 4px` / `--radius-sm: 8px` / `--radius-md: 12px` / `--radius-lg: 24px`. Plus literal `50%` (circles) and `0` (viewport edges). No other literal radii anywhere.
- **Button heights:** CTA ≈ 30px (`padding: 7px 16px`, `font-size: 13px`); chip ≈ 22px (`padding: 3px 9px`, `font-size: 11px`); icon-only 26×26; adornment ≈ 24px (`padding: 4px 10px`, `font-size: 12px`).
- **Hover colors:** every `#568ADA` → `var(--accent)`; every `rgba(86, 138, 218, X)` → the corresponding `var(--accent-bg-XX)` per the table in spec §4.
- **Sync invariant:** the shared block (between `/* === SHARED-TOKENS-START === */` and `/* === SHARED-TOKENS-END === */`) must be byte-identical in `tauri/src/style.css` and `ui/static/style.css`. Enforced by `tests/test_desktop_ui_consistency.py`.
- **Test runner:** project uses `unittest` (no pytest). Run with `python3 -m unittest tests.test_desktop_ui_consistency`.
- **Both frontends in scope:** every change applies to both `tauri/src/` and `ui/static/` unless the file only exists in one (e.g., `tauri/src/app.js` is online-only).
- **Out of scope:** iOS app, palette beyond blue→orange, typography, new features.

## File Structure

**New files:**
- `tests/test_desktop_ui_consistency.py` — all consistency assertions live here

**Modified files:**
- `tauri/src/style.css` (1428 lines) — add shared block at top; migrate literal radii to `var()`; replace blue with orange; delete superseded one-off button rules
- `ui/static/style.css` (649 lines) — same as above
- `tauri/src/index.html` (710 lines) — Explorer Search becomes inline; rename one-off button classes; strip inline button `style=` overrides
- `ui/static/index.html` (527 lines) — same as above
- `tauri/src/app.js` (3940 lines) — strip inline button styles in template strings; replace blue color literals in inline styles

---

## Task 1: Bootstrap consistency test + fenced shared block

**Files:**
- Create: `tests/test_desktop_ui_consistency.py`
- Modify: `tauri/src/style.css` (insert fence at line 1)
- Modify: `ui/static/style.css` (insert fence at line 1)

**Interfaces:**
- Consumes: nothing
- Produces: `tests/test_desktop_ui_consistency.py::TestSharedTokenBlock::test_block_present_and_matches` — used by later tasks to verify the sync invariant after each change.

- [ ] **Step 1: Write the failing test**

Create `tests/test_desktop_ui_consistency.py`:

```python
"""Tests for desktop UI consistency.

Covers:
  - Shared token block exists and matches byte-for-byte between
    tauri/src/style.css and ui/static/style.css.
  - No legacy blue color literals remain (added in later tasks).
  - No literal border-radius outside the scale (added in later tasks).
  - HTML/JS no longer reference legacy one-off button classes (added in later tasks).
"""

import os
import re
import unittest

REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
TAURI_CSS = os.path.join(REPO_ROOT, "tauri", "src", "style.css")
OFFLINE_CSS = os.path.join(REPO_ROOT, "ui", "static", "style.css")
TAURI_HTML = os.path.join(REPO_ROOT, "tauri", "src", "index.html")
OFFLINE_HTML = os.path.join(REPO_ROOT, "ui", "static", "index.html")
TAURI_JS = os.path.join(REPO_ROOT, "tauri", "src", "app.js")

FENCE_START = "/* === SHARED-TOKENS-START — keep in sync with the other desktop stylesheet === */"
FENCE_END = "/* === SHARED-TOKENS-END === */"


def _read(path):
    with open(path) as f:
        return f.read()


def _extract_fenced_block(path):
    text = _read(path)
    start = text.find(FENCE_START)
    end = text.find(FENCE_END)
    if start == -1:
        raise AssertionError(f"{FENCE_START!r} not found in {path}")
    if end == -1:
        raise AssertionError(f"{FENCE_END!r} not found in {path}")
    return text[start:end + len(FENCE_END)]


class TestSharedTokenBlock(unittest.TestCase):
    def test_block_present_and_matches(self):
        tauri = _extract_fenced_block(TAURI_CSS)
        offline = _extract_fenced_block(OFFLINE_CSS)
        self.assertEqual(
            tauri, offline,
            "Shared token block diverged between tauri/src/style.css and ui/static/style.css. "
            "Edit one and copy the block verbatim into the other.",
        )


if __name__ == "__main__":
    unittest.main()
```

- [ ] **Step 2: Run test to verify it fails**

Run: `python3 -m unittest tests.test_desktop_ui_consistency -v`
Expected: FAIL with `'/* === SHARED-TOKENS-START — keep in sync with the other desktop stylesheet === */' not found in …/tauri/src/style.css`

- [ ] **Step 3: Insert minimal fenced block at line 1 of `tauri/src/style.css`**

Open `tauri/src/style.css`. Currently line 1 is `:root {`. Insert the following ABOVE line 1 (so the file now starts with the fence, then the existing `:root` block follows):

```css
/* === SHARED-TOKENS-START — keep in sync with the other desktop stylesheet === */
/* Populated in Task 2. */
/* === SHARED-TOKENS-END === */

```

- [ ] **Step 4: Insert the identical minimal block at line 1 of `ui/static/style.css`**

Same insertion at the top of `ui/static/style.css`. The four lines must be byte-identical to what was inserted in Step 3 (same fence text, same comment, same blank line after).

- [ ] **Step 5: Run test to verify it passes**

Run: `python3 -m unittest tests.test_desktop_ui_consistency -v`
Expected: PASS — `Ran 1 test … OK`

- [ ] **Step 6: Commit**

```bash
git add tests/test_desktop_ui_consistency.py tauri/src/style.css ui/static/style.css
git commit -m "test: add desktop UI consistency check + fenced token block"
```

---

## Task 2: Populate shared block with the full token + button system

**Files:**
- Modify: `tauri/src/style.css` — replace the placeholder line inside the fence with the full block
- Modify: `ui/static/style.css` — same replacement, byte-identical

**Interfaces:**
- Consumes: fence markers from Task 1
- Produces: CSS classes `.btn-chip`, `.btn-icon`, `.input-adornment`, `.input-adornment.icon`, `.form-row-inline`, `.form-actions` and the input modifier `input.with-adornment` / `input.with-double-adornment`. Also CSS custom properties `--radius-xs`, `--radius-sm`, `--radius-md`, `--radius-lg`. Used by Tasks 3–13.

- [ ] **Step 1: Write the failing test — required tokens and classes are defined**

Append to `tests/test_desktop_ui_consistency.py` (after the existing `TestSharedTokenBlock` class):

```python
class TestSharedBlockContents(unittest.TestCase):
    def setUp(self):
        self.block = _extract_fenced_block(TAURI_CSS)

    def test_radius_tokens_defined(self):
        for token, value in [
            ("--radius-xs", "4px"),
            ("--radius-sm", "8px"),
            ("--radius-md", "12px"),
            ("--radius-lg", "24px"),
        ]:
            self.assertRegex(
                self.block,
                rf"{re.escape(token)}\s*:\s*{re.escape(value)}\s*;",
                f"{token}: {value} missing from shared block",
            )

    def test_button_base_selectors_grouped(self):
        # All four CTA selectors share the base rule
        self.assertRegex(
            self.block,
            r"button\.primary\s*,\s*button\.secondary\s*,\s*button\.danger\s*,\s*button\.btn-green\s*\{",
        )

    def test_new_utility_classes_defined(self):
        for selector in (".btn-chip", ".btn-icon", ".input-adornment",
                         ".form-row-inline", ".form-actions"):
            self.assertIn(
                selector + " {",
                self.block.replace("{ ", "{").replace("  ", " "),
                f"{selector} missing from shared block",
            )
```

- [ ] **Step 2: Run test to verify it fails**

Run: `python3 -m unittest tests.test_desktop_ui_consistency.TestSharedBlockContents -v`
Expected: FAIL — all four tests fail (tokens not defined yet).

- [ ] **Step 3: Replace the placeholder line in `tauri/src/style.css` with the full shared block**

Replace this in `tauri/src/style.css`:

```css
/* === SHARED-TOKENS-START — keep in sync with the other desktop stylesheet === */
/* Populated in Task 2. */
/* === SHARED-TOKENS-END === */
```

with this:

```css
/* === SHARED-TOKENS-START — keep in sync with the other desktop stylesheet === */
:root {
    --radius-xs: 4px;
    --radius-sm: 8px;
    --radius-md: 12px;
    --radius-lg: 24px;
}

/* Button family — one compact CTA size shared by all kinds */
button.primary,
button.secondary,
button.danger,
button.btn-green {
    padding: 7px 16px;
    font-size: 13px;
    border-radius: var(--radius-md);
    cursor: pointer;
    white-space: nowrap;
    line-height: 1.4;
    transition: background 0.15s, color 0.15s;
}
button.primary   { background: var(--accent); color: #000; font-weight: 600; border: none; }
button.secondary { background: var(--border); color: var(--text); border: none; }
button.danger    { background: rgba(248, 81, 73, 0.15); color: var(--red); border: 1px solid var(--red); padding: 6px 15px; }
button.btn-green { background: var(--green); color: #000; font-weight: 600; border: none; }

button.primary:hover   { background: var(--accent-dim); }
button.secondary:hover { background: var(--accent-bg-15); color: var(--accent); }
button.danger:hover    { background: rgba(248, 81, 73, 0.25); }
button.btn-green:hover { background: #2da640; }
button:disabled        { opacity: 0.5; cursor: not-allowed; }

/* Inline chips for dense rows */
.btn-chip {
    background: var(--border);
    color: var(--text-dim);
    border: none;
    border-radius: var(--radius-sm);
    padding: 3px 9px;
    font-size: 11px;
    cursor: pointer;
    transition: background 0.15s, color 0.15s;
}
.btn-chip:hover { background: var(--accent-bg-15); color: var(--accent); }

/* Icon-only utility buttons */
.btn-icon {
    background: transparent;
    color: var(--text-dim);
    border: none;
    border-radius: var(--radius-sm);
    padding: 5px;
    width: 26px;
    height: 26px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    transition: background 0.15s, color 0.15s;
}
.btn-icon:hover { background: var(--accent-bg-15); color: var(--accent); }

/* Adornment — sits inside an input on its right edge.
   Parent must be position: relative. Input must have .with-adornment. */
.input-adornment {
    position: absolute;
    right: 6px;
    top: 50%;
    transform: translateY(-50%);
    background: var(--border);
    color: var(--text-dim);
    border: none;
    border-radius: var(--radius-sm);
    padding: 4px 10px;
    font-size: 12px;
    font-weight: 500;
    line-height: 1.2;
    white-space: nowrap;
    cursor: pointer;
    z-index: 2;
    transition: background 0.15s, color 0.15s;
}
.input-adornment.icon { padding: 4px 6px; line-height: 0; display: inline-flex; align-items: center; }
.input-adornment:hover { background: var(--accent-bg-15); color: var(--accent); }
input.with-adornment        { padding-right: 56px; }
input.with-double-adornment { padding-right: 88px; }

/* Layout helpers */
.form-row-inline { display: flex; gap: 8px; align-items: center; }
.form-row-inline > input { flex: 1; }
.form-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 16px; }
/* === SHARED-TOKENS-END === */
```

- [ ] **Step 4: Copy the same block verbatim into `ui/static/style.css`**

Replace the placeholder block in `ui/static/style.css` with the exact same content as Step 3. Byte-identical. Do not change a single character.

- [ ] **Step 5: Run tests to verify all consistency tests pass**

Run: `python3 -m unittest tests.test_desktop_ui_consistency -v`
Expected: PASS — 4 tests OK (`test_block_present_and_matches`, `test_radius_tokens_defined`, `test_button_base_selectors_grouped`, `test_new_utility_classes_defined`).

- [ ] **Step 6: Visual sanity check — load Tauri dev**

Run: `cd tauri && cargo tauri dev` (in a separate terminal, leave running)
Expected: app launches; existing pages render. Buttons may look slightly off because old one-off rules below the shared block are still present and overlap. That's fine — full visual correctness is verified in Task 12. For now confirm: no broken pages, no console errors.

- [ ] **Step 7: Commit**

```bash
git add tests/test_desktop_ui_consistency.py tauri/src/style.css ui/static/style.css
git commit -m "feat(desktop-ui): add shared token block + button system"
```

---

## Task 3: Migrate `tauri/src/index.html`

**Files:**
- Modify: `tauri/src/index.html` — Explorer Search inline; rename button classes; strip inline `style=` on buttons

**Interfaces:**
- Consumes: CSS classes from Task 2 (`.btn-chip`, `.btn-icon`, `.input-adornment`, `.form-row-inline`)
- Produces: HTML that no longer references legacy class names (`copy-btn`, `tx-details-btn`, `addr-copy-btn`, `recv-addr-copy-btn`, `ab-entry-copy-btn`, `addr-row-copy-btn`, `file-chooser-btn`, `send-address-book-btn`, `send-max-btn`, `theme-toggle-btn`) for use by Task 10's class-deletion check.

- [ ] **Step 1: Write the failing test — legacy class names absent from HTML**

Append to `tests/test_desktop_ui_consistency.py`:

```python
LEGACY_BUTTON_CLASSES_IN_HTML = (
    "copy-btn",
    "tx-details-btn",
    "addr-copy-btn",
    "recv-addr-copy-btn",
    "ab-entry-copy-btn",
    "addr-row-copy-btn",
    "file-chooser-btn",
    "theme-toggle-btn",
)
LEGACY_BUTTON_IDS_IN_HTML = (
    "send-address-book-btn",
    "send-max-btn",
)


class TestTauriHtmlMigrated(unittest.TestCase):
    def setUp(self):
        self.text = _read(TAURI_HTML)

    def test_no_legacy_button_classes(self):
        for name in LEGACY_BUTTON_CLASSES_IN_HTML:
            self.assertNotRegex(
                self.text,
                rf'class="[^"]*\b{re.escape(name)}\b[^"]*"',
                f"legacy button class {name!r} still present in tauri/src/index.html",
            )

    def test_no_legacy_button_ids(self):
        for ident in LEGACY_BUTTON_IDS_IN_HTML:
            self.assertNotRegex(
                self.text,
                rf'id="{re.escape(ident)}"',
                f"legacy button id {ident!r} still present in tauri/src/index.html",
            )

    def test_explorer_search_uses_form_row_inline(self):
        # The Explorer Search input + button must live inside a .form-row-inline wrapper.
        # Verifies the layout migration happened.
        self.assertRegex(
            self.text,
            r'class="form-row-inline"[^>]*>\s*<input[^>]*id="explorer-search"',
            "Explorer Search input not wrapped in .form-row-inline",
        )

    def test_no_inline_button_padding_or_font_size(self):
        # Buttons should not carry inline style overrides for size/padding/radius.
        # Match: <button ... style="...padding:...">  or  ...font-size:...  or  ...border-radius:...
        forbidden = re.compile(
            r'<button[^>]*\bstyle="[^"]*(?:padding\s*:|font-size\s*:|border-radius\s*:)[^"]*"',
            re.IGNORECASE,
        )
        matches = forbidden.findall(self.text)
        self.assertEqual(matches, [], f"buttons with inline size/padding/radius styles: {matches[:3]}")
```

- [ ] **Step 2: Run test to verify it fails**

Run: `python3 -m unittest tests.test_desktop_ui_consistency.TestTauriHtmlMigrated -v`
Expected: FAIL — all four tests fail.

- [ ] **Step 3: Move Explorer Search to inline layout**

In `tauri/src/index.html`, find the Explorer block (around lines 280–289). Currently it looks like:

```html
<div style="display:flex; flex-direction:column; align-items:flex-start; gap:12px; margin-bottom:16px;">
    <button id="explorer-back-btn" class="secondary" onclick="explorerBack()" style="display:none; padding:6px 10px; font-size:13px;" title="Go back">←</button>
    <input type="text" id="explorer-search" placeholder="Search txid, address, or block height/hash" style="width:100%; font-family:var(--mono); font-size:13px; margin-bottom:0;">
    <button class="primary" onclick="explorerSearch()">Search</button>
</div>
```

Replace with:

```html
<div style="margin-bottom:16px;">
    <button id="explorer-back-btn" class="secondary" onclick="explorerBack()" style="display:none; margin-bottom:8px;" title="Go back">←</button>
    <div class="form-row-inline">
        <input type="text" id="explorer-search" placeholder="Search txid, address, or block height/hash" style="font-family:var(--mono); font-size:13px; margin-bottom:0;">
        <button class="primary" onclick="explorerSearch()">Search</button>
    </div>
</div>
```

(Removed inline `padding:6px 10px; font-size:13px` from the back button — class-based sizing takes over.)

- [ ] **Step 4: Rename one-off button classes to new utilities**

Apply these `class=` replacements throughout `tauri/src/index.html`:

| Old | New |
|---|---|
| `class="copy-btn"` | `class="btn-icon"` |
| `class="tx-details-btn"` | `class="btn-chip"` |
| `class="addr-copy-btn"` | `class="btn-chip"` |
| `class="recv-addr-copy-btn"` | `class="btn-chip"` |
| `class="ab-entry-copy-btn"` | `class="btn-chip"` |
| `class="addr-row-copy-btn"` | `class="btn-chip"` |
| `class="file-chooser-btn"` | `class="btn-chip"` |
| `class="theme-toggle-btn"` | `class="btn-icon"` |

For elements with multiple classes (e.g., `class="copy-btn other-class"`), preserve the other classes.

- [ ] **Step 5: Convert `#send-address-book-btn` and `#send-max-btn` to `.input-adornment`**

Find around line 342:

```html
<button onclick="showAddressBookPicker()" id="send-address-book-btn" title="Pick from address book"><i data-lucide="notebook" style="width:18px;height:18px;display:block;"></i></button>
```

Replace with:

```html
<button onclick="showAddressBookPicker()" class="input-adornment icon" title="Pick from address book"><i data-lucide="notebook" style="width:16px;height:16px;display:block;"></i></button>
```

The input on the line above (`id="send-address"`) needs `class="with-adornment"` added and its inline `padding-right:42px` removed:

```html
<input type="text" id="send-address" class="with-adornment" placeholder="bc1q... / bc1p... / sp1q..." style="margin-bottom:0;" oninput="onSendAddressInput()">
```

Find around line 367:

```html
<button id="send-max-btn" onclick="setSendMax()">Max</button>
```

Replace with:

```html
<button class="input-adornment" onclick="setSendMax()">Max</button>
```

The `send-amount` input above it needs `class="with-adornment"` and its inline `padding-right:60px` removed:

```html
<input type="text" id="send-amount" class="with-adornment" placeholder="0.001" oninput="clearSendMax(); updateSatsDisplay()" style="flex:1; margin-bottom:0;">
```

- [ ] **Step 6: Strip inline size/padding/radius overrides from any remaining `<button>` tags**

Search `tauri/src/index.html` for `<button` with `style="..."` containing `padding:`, `font-size:`, or `border-radius:`. For each match, remove those properties from the inline style (leave any non-size properties like `display:none`, `margin-*`, `color:` intact). If the style attribute becomes empty, remove it entirely.

Expected sites: `explorer-back-btn`, possibly `theme-toggle` icons, any other inline-styled button. The test in Step 1 enforces zero matches.

- [ ] **Step 7: Run tests to verify all pass**

Run: `python3 -m unittest tests.test_desktop_ui_consistency -v`
Expected: PASS — all tests including the four new `TestTauriHtmlMigrated` ones.

- [ ] **Step 8: Visual check**

Reload Tauri dev. Visit Explorer (Search appears as input + button on same row), Send (address book icon inside To input, Max inside Amount input), Transactions (copy buttons + tx-details still render — they fall back to default `<button>` until CSS cleanup in Task 10, but layout shouldn't break).

- [ ] **Step 9: Commit**

```bash
git add tests/test_desktop_ui_consistency.py tauri/src/index.html
git commit -m "feat(desktop-ui): migrate tauri index.html to new button classes + inline Search"
```

---

## Task 4: Migrate `ui/static/index.html`

**Files:**
- Modify: `ui/static/index.html` — same migration pattern as Task 3

**Interfaces:**
- Consumes: same as Task 3
- Produces: same migration check for the offline frontend

- [ ] **Step 1: Write the failing test**

Append to `tests/test_desktop_ui_consistency.py`:

```python
class TestOfflineHtmlMigrated(unittest.TestCase):
    def setUp(self):
        self.text = _read(OFFLINE_HTML)

    def test_no_legacy_button_classes(self):
        for name in LEGACY_BUTTON_CLASSES_IN_HTML:
            self.assertNotRegex(
                self.text,
                rf'class="[^"]*\b{re.escape(name)}\b[^"]*"',
                f"legacy button class {name!r} still present in ui/static/index.html",
            )

    def test_explorer_search_uses_form_row_inline(self):
        self.assertRegex(
            self.text,
            r'class="form-row-inline"[^>]*>\s*<input[^>]*id="explorer-search"',
            "Explorer Search input not wrapped in .form-row-inline",
        )

    def test_no_inline_button_padding_or_font_size(self):
        forbidden = re.compile(
            r'<button[^>]*\bstyle="[^"]*(?:padding\s*:|font-size\s*:|border-radius\s*:)[^"]*"',
            re.IGNORECASE,
        )
        matches = forbidden.findall(self.text)
        self.assertEqual(matches, [], f"buttons with inline size/padding/radius styles: {matches[:3]}")
```

- [ ] **Step 2: Run test to verify it fails**

Run: `python3 -m unittest tests.test_desktop_ui_consistency.TestOfflineHtmlMigrated -v`
Expected: FAIL — three tests fail.

- [ ] **Step 3: Apply class renames**

Same table as Task 3 Step 4. Apply throughout `ui/static/index.html`.

- [ ] **Step 4: Wrap Explorer Search in `.form-row-inline`**

Find around line 194:

```html
<input type="text" id="explorer-search" placeholder="..." …>
…
<button class="primary" onclick="explorerSearch()">Search</button>
```

(They may be siblings with an intervening div.) Refactor so the input + button live inside `<div class="form-row-inline">`, mirroring the Tauri layout from Task 3.

- [ ] **Step 5: Strip inline button size/padding overrides**

Same procedure as Task 3 Step 6.

- [ ] **Step 6: Run tests to verify all pass**

Run: `python3 -m unittest tests.test_desktop_ui_consistency -v`
Expected: PASS.

- [ ] **Step 7: Visual check**

Run the offline app: `python3 ui/server.py` (or however the offline dev mode launches — see existing scripts). Confirm Explorer Search renders inline, no obvious layout breaks.

- [ ] **Step 8: Commit**

```bash
git add tests/test_desktop_ui_consistency.py ui/static/index.html
git commit -m "feat(desktop-ui): migrate offline index.html to new button classes + inline Search"
```

---

## Task 5: Migrate `tauri/src/app.js`

**Files:**
- Modify: `tauri/src/app.js` — strip inline button styles in template strings; replace blue color literals in inline styles

**Interfaces:**
- Consumes: CSS button classes from Task 2
- Produces: app.js free of `#568ADA`, `rgba(86, 138, 218, *)`, and inline button size/padding/radius styles

- [ ] **Step 1: Write the failing test**

Append to `tests/test_desktop_ui_consistency.py`:

```python
class TestTauriJsMigrated(unittest.TestCase):
    def setUp(self):
        self.text = _read(TAURI_JS)

    def test_no_blue_hex_literal(self):
        self.assertNotIn(
            "#568ADA", self.text,
            "Legacy blue #568ADA still present in app.js",
        )
        self.assertNotIn(
            "#568ada", self.text,
            "Legacy blue #568ada (lowercase) still present in app.js",
        )

    def test_no_blue_rgba_literal(self):
        matches = re.findall(r"rgba\(\s*86\s*,\s*138\s*,\s*218\b", self.text)
        self.assertEqual(matches, [], f"legacy blue rgba still present: {matches[:3]}")

    def test_no_inline_button_padding_or_font_size_in_templates(self):
        # Same check as HTML, but for template strings inside app.js.
        forbidden = re.compile(
            r'<button[^>]*\bstyle="[^"]*(?:padding\s*:|font-size\s*:|border-radius\s*:)[^"]*"',
            re.IGNORECASE,
        )
        matches = forbidden.findall(self.text)
        self.assertEqual(matches, [], f"buttons with inline size/padding/radius styles: {matches[:3]}")
```

- [ ] **Step 2: Run test to verify it fails**

Run: `python3 -m unittest tests.test_desktop_ui_consistency.TestTauriJsMigrated -v`
Expected: FAIL — three tests fail.

- [ ] **Step 3: Strip inline button styles in template strings**

Find each `<button` in `app.js` with an inline `style="..."` containing `padding:`, `font-size:`, or `border-radius:`. Known sites include (around line 3231, 3256, 3268, 3273):

```js
<button class="secondary" onclick="…" style="font-size:12px;padding:6px 12px;">Cancel</button>
```

Remove the inline style attribute entirely (it only contained size overrides):

```js
<button class="secondary" onclick="…">Cancel</button>
```

For the Configure Node Connection button (~line 3273):

```js
<button class="secondary" onclick="…" style="font-size:12px;padding:5px 14px;">Configure Node Connection</button>
```

Becomes:

```js
<button class="secondary" onclick="…">Configure Node Connection</button>
```

Search exhaustively: `grep -n 'style="[^"]*\(padding\|font-size\|border-radius\)' tauri/src/app.js` and process each match.

- [ ] **Step 4: Replace blue color literals in inline styles**

In `app.js`, search for `#568ADA`, `#568ada`, and `rgba(86, 138, 218`. For each:
- `color:#568ADA` → `color:var(--accent)`
- `border-color:#568ADA` → `border-color:var(--accent)`
- `background:#568ADA` → `background:var(--accent)`
- `rgba(86, 138, 218, 0.15)` → `var(--accent-bg-15)`
- `rgba(86, 138, 218, 0.35)` → `rgba(247, 147, 26, 0.35)` (kept inline, single use site)

If any blue values appear inside expressions (not in CSS strings), use judgment but preserve intent.

- [ ] **Step 5: Run tests to verify all pass**

Run: `python3 -m unittest tests.test_desktop_ui_consistency -v`
Expected: PASS.

- [ ] **Step 6: Visual check**

Reload Tauri dev. Open Node settings (Configure Node Connection now renders at compact size). Open Send and try the "Add to address book" inline editor (the Cancel/Save buttons should still work; their colors will normalize fully after Task 8 cleans up the matching CSS).

- [ ] **Step 7: Commit**

```bash
git add tests/test_desktop_ui_consistency.py tauri/src/app.js
git commit -m "feat(desktop-ui): strip inline button styles + blue literals in app.js"
```

---

## Task 6: Migrate literal radii to `var(--radius-*)` in `tauri/src/style.css`

**Files:**
- Modify: `tauri/src/style.css` — every `border-radius: <literal>` outside the fenced block becomes `border-radius: var(--radius-*)`

**Interfaces:**
- Consumes: tokens from Task 2
- Produces: stylesheet free of literal radius values except `50%`, `0`, and those inside the fenced block

- [ ] **Step 1: Write the failing test**

Append to `tests/test_desktop_ui_consistency.py`:

```python
# Allowed literal radii anywhere in the stylesheets (after fence block is stripped):
# only the bare values 50% (circles) and 0 (viewport edges) may appear.
RADIUS_LITERAL_RE = re.compile(r"border-radius\s*:\s*([^;]+);")


def _strip_fenced_block(text):
    start = text.find(FENCE_START)
    end = text.find(FENCE_END)
    if start == -1 or end == -1:
        return text
    return text[:start] + text[end + len(FENCE_END):]


def _radius_literals_outside_scale(path):
    text = _strip_fenced_block(_read(path))
    bad = []
    for m in RADIUS_LITERAL_RE.finditer(text):
        value = m.group(1).strip()
        if value in ("0", "50%"):
            continue
        if value.startswith("var(--radius-"):
            continue
        bad.append(value)
    return bad


class TestTauriCssRadii(unittest.TestCase):
    def test_no_literal_radii_outside_scale(self):
        bad = _radius_literals_outside_scale(TAURI_CSS)
        self.assertEqual(
            bad, [],
            f"tauri/src/style.css has literal border-radius values outside the scale: "
            f"{sorted(set(bad))[:10]}",
        )
```

- [ ] **Step 2: Run test to verify it fails**

Run: `python3 -m unittest tests.test_desktop_ui_consistency.TestTauriCssRadii -v`
Expected: FAIL — many literal radii (3, 4, 6, 8, 9, 10, 12, 14, 16) present.

- [ ] **Step 3: Replace literal radii using the mapping**

For each `border-radius: Npx` *outside* the fenced block in `tauri/src/style.css`, apply the mapping:

| Literal | Replace with |
|---|---|
| `3px`, `4px` | `var(--radius-xs)` |
| `6px`, `8px`, `9px` | `var(--radius-sm)` |
| `10px`, `12px` | `var(--radius-md)` |
| `14px`, `16px` | `var(--radius-lg)` |
| `50%`, `0` | leave as-is |

These are exact-string substitutions. Use Edit with `replace_all: true` for each literal:

- Find `border-radius: 3px;` → replace with `border-radius: var(--radius-xs);`
- Find `border-radius: 4px;` → replace with `border-radius: var(--radius-xs);`
- Find `border-radius: 6px;` → replace with `border-radius: var(--radius-sm);`
- Find `border-radius: 8px;` → replace with `border-radius: var(--radius-sm);`
- Find `border-radius: 9px;` → replace with `border-radius: var(--radius-sm);`
- Find `border-radius: 10px;` → replace with `border-radius: var(--radius-md);`
- Find `border-radius: 12px;` → replace with `border-radius: var(--radius-md);`
- Find `border-radius: 14px;` → replace with `border-radius: var(--radius-lg);`
- Find `border-radius: 16px;` → replace with `border-radius: var(--radius-lg);`

**Caveat:** the fenced block uses `var(--radius-*)` already, so these `replace_all` substitutions won't touch it (no `Npx` literal there). But verify after: the fenced block's content must remain byte-identical to `ui/static/style.css`'s fenced block.

- [ ] **Step 4: Run tests to verify all pass**

Run: `python3 -m unittest tests.test_desktop_ui_consistency -v`
Expected: PASS — `test_no_literal_radii_outside_scale` passes; `test_block_present_and_matches` still passes (fenced block untouched).

- [ ] **Step 5: Visual check**

Reload Tauri dev. Expected differences: panels/cards visibly rounder (was 12-16px, now 24px); pills slightly different (was 6/9/10, now 8/12); some elements that were 14-16px (panels) now noticeably softer. No layout breakage.

- [ ] **Step 6: Commit**

```bash
git add tests/test_desktop_ui_consistency.py tauri/src/style.css
git commit -m "feat(desktop-ui): migrate literal radii to var(--radius-*) in tauri css"
```

---

## Task 7: Migrate literal radii in `ui/static/style.css`

**Files:**
- Modify: `ui/static/style.css` — same as Task 6 for the offline stylesheet

**Interfaces:** same as Task 6

- [ ] **Step 1: Write the failing test**

Append to `tests/test_desktop_ui_consistency.py`:

```python
class TestOfflineCssRadii(unittest.TestCase):
    def test_no_literal_radii_outside_scale(self):
        bad = _radius_literals_outside_scale(OFFLINE_CSS)
        self.assertEqual(
            bad, [],
            f"ui/static/style.css has literal border-radius values outside the scale: "
            f"{sorted(set(bad))[:10]}",
        )
```

- [ ] **Step 2: Run test to verify it fails**

Run: `python3 -m unittest tests.test_desktop_ui_consistency.TestOfflineCssRadii -v`
Expected: FAIL.

- [ ] **Step 3: Apply the same radius mapping in `ui/static/style.css`**

Same nine `replace_all` operations as Task 6 Step 3, applied to `ui/static/style.css`.

- [ ] **Step 4: Run tests to verify all pass**

Run: `python3 -m unittest tests.test_desktop_ui_consistency -v`
Expected: PASS.

- [ ] **Step 5: Commit**

```bash
git add tests/test_desktop_ui_consistency.py ui/static/style.css
git commit -m "feat(desktop-ui): migrate literal radii to var(--radius-*) in offline css"
```

---

## Task 8: Replace blue with orange in `tauri/src/style.css`

**Files:**
- Modify: `tauri/src/style.css` — every `#568ADA` / `rgba(86, 138, 218, X)` outside the fenced block becomes the orange equivalent

**Interfaces:**
- Consumes: existing `--accent-bg-*` tokens already in `:root`
- Produces: stylesheet free of blue color literals

- [ ] **Step 1: Write the failing test**

Append to `tests/test_desktop_ui_consistency.py`:

```python
BLUE_RGBA_RE = re.compile(r"rgba\(\s*86\s*,\s*138\s*,\s*218\b")


class TestTauriCssNoBlue(unittest.TestCase):
    def setUp(self):
        self.text = _strip_fenced_block(_read(TAURI_CSS))

    def test_no_blue_hex(self):
        self.assertNotIn("#568ADA", self.text)
        self.assertNotIn("#568ada", self.text)

    def test_no_blue_rgba(self):
        matches = BLUE_RGBA_RE.findall(self.text)
        self.assertEqual(matches, [], f"legacy blue rgba present: count={len(matches)}")
```

- [ ] **Step 2: Run test to verify it fails**

Run: `python3 -m unittest tests.test_desktop_ui_consistency.TestTauriCssNoBlue -v`
Expected: FAIL.

- [ ] **Step 3: Replace blue with orange**

Apply these `replace_all` substitutions in `tauri/src/style.css`:

1. `#568ADA` → `var(--accent)` (replace_all)
2. `#568ada` → `var(--accent)` (replace_all, lowercase variant)
3. `rgba(86, 138, 218, 0.06)` → `var(--accent-bg-06)` (replace_all)
4. `rgba(86, 138, 218, 0.08)` → `var(--accent-bg-08)` (replace_all)
5. `rgba(86, 138, 218, 0.10)` → `var(--accent-bg-10)` (replace_all)
6. `rgba(86, 138, 218, 0.12)` → `var(--accent-bg-10)` (replace_all — closest)
7. `rgba(86, 138, 218, 0.15)` → `var(--accent-bg-15)` (replace_all)
8. `rgba(86, 138, 218, 0.2)` → `var(--accent-bg-15)` (replace_all — closest; note `0.2` not `0.20`)
9. `rgba(86, 138, 218, 0.20)` → `var(--accent-bg-15)` (replace_all)
10. `rgba(86, 138, 218, 0.35)` → `rgba(247, 147, 26, 0.35)` (replace_all — box-shadow, kept inline)

**Note:** the secondary button hover at the top of the file (line ~336 today, but now inside the fenced block) already uses `var(--accent-bg-15)` from Task 2 — these substitutions only affect rules below the fence.

Also: there are old `#568ADA` uses in OTHER hover contexts that the new shared block doesn't cover (e.g., `#logout-btn` background, `.ab-pick-entry:hover`, `.tx-link`, `#send-save-row` styles). Step 3 catches all of these mechanically.

- [ ] **Step 4: Run tests to verify all pass**

Run: `python3 -m unittest tests.test_desktop_ui_consistency -v`
Expected: PASS.

- [ ] **Step 5: Visual check**

Reload Tauri dev. Hover the following and confirm orange (not blue):
- Any Cancel / secondary button
- A copy icon in a transaction row
- A `tx-details` chip
- An address-book entry row
- The Logout / Switch Mode buttons in the top-right
- A `tx-link` (transaction id link) — its resting color is now orange too

- [ ] **Step 6: Commit**

```bash
git add tests/test_desktop_ui_consistency.py tauri/src/style.css
git commit -m "feat(desktop-ui): warm tauri css hovers from blue to orange"
```

---

## Task 9: Replace blue with orange in `ui/static/style.css`

**Files:**
- Modify: `ui/static/style.css` — same as Task 8

**Interfaces:** same as Task 8

- [ ] **Step 1: Write the failing test**

Append to `tests/test_desktop_ui_consistency.py`:

```python
class TestOfflineCssNoBlue(unittest.TestCase):
    def setUp(self):
        self.text = _strip_fenced_block(_read(OFFLINE_CSS))

    def test_no_blue_hex(self):
        self.assertNotIn("#568ADA", self.text)
        self.assertNotIn("#568ada", self.text)

    def test_no_blue_rgba(self):
        matches = BLUE_RGBA_RE.findall(self.text)
        self.assertEqual(matches, [], f"legacy blue rgba present: count={len(matches)}")
```

- [ ] **Step 2: Run test to verify it fails**

Run: `python3 -m unittest tests.test_desktop_ui_consistency.TestOfflineCssNoBlue -v`
Expected: FAIL.

- [ ] **Step 3: Apply the same 10 replacements in `ui/static/style.css`**

Same list as Task 8 Step 3. (The offline stylesheet has fewer blue uses, but the substitutions are idempotent — running them when there's nothing to match is a no-op.)

- [ ] **Step 4: Run tests to verify all pass**

Run: `python3 -m unittest tests.test_desktop_ui_consistency -v`
Expected: PASS.

- [ ] **Step 5: Commit**

```bash
git add tests/test_desktop_ui_consistency.py ui/static/style.css
git commit -m "feat(desktop-ui): warm offline css hovers from blue to orange"
```

---

## Task 10: Delete superseded one-off button rules in `tauri/src/style.css`

**Files:**
- Modify: `tauri/src/style.css` — remove rules for `.copy-btn`, `.tx-details-btn`, `.addr-copy-btn`, etc. that are now covered by `.btn-chip` / `.btn-icon` / `.input-adornment`

**Interfaces:**
- Consumes: HTML migration completed in Task 3
- Produces: stylesheet free of legacy one-off button selectors

- [ ] **Step 1: Write the failing test**

Append to `tests/test_desktop_ui_consistency.py`:

```python
LEGACY_BUTTON_SELECTORS_IN_CSS = (
    ".copy-btn",
    ".tx-details-btn",
    ".addr-copy-btn",
    ".recv-addr-copy-btn",
    ".ab-entry-copy-btn",
    ".addr-row-copy-btn",
    ".file-chooser-btn",
    ".theme-toggle-btn",
    "#send-address-book-btn",
    "#send-max-btn",
)


class TestTauriCssLegacySelectorsRemoved(unittest.TestCase):
    def setUp(self):
        self.text = _strip_fenced_block(_read(TAURI_CSS))

    def test_no_legacy_button_selectors(self):
        # Find selector definitions — `.foo {` or `.foo:hover {` or `#foo {`
        for selector in LEGACY_BUTTON_SELECTORS_IN_CSS:
            pattern = re.compile(rf"^[^{{\n]*{re.escape(selector)}(?:[:\s,][^{{\n]*)?\{{", re.MULTILINE)
            matches = pattern.findall(self.text)
            self.assertEqual(
                matches, [],
                f"legacy selector {selector!r} still defined in tauri/src/style.css: {matches[:2]}",
            )
```

- [ ] **Step 2: Run test to verify it fails**

Run: `python3 -m unittest tests.test_desktop_ui_consistency.TestTauriCssLegacySelectorsRemoved -v`
Expected: FAIL — selectors still defined.

- [ ] **Step 3: Delete the legacy rules**

In `tauri/src/style.css`, find and delete the complete rule blocks (including `:hover` variants and any `[data-theme="light"]` overrides) for each of:

- `.copy-btn`, `.copy-btn:hover`
- `.tx-details-btn`, `.tx-details-btn:hover`
- `.addr-copy-btn`, `.addr-copy-btn:hover`
- `.recv-addr-copy-btn`, `.recv-addr-copy-btn:hover`
- `.ab-entry-copy-btn`, `.ab-entry-copy-btn:hover`
- `.addr-row-copy-btn`, `.addr-row-copy-btn:hover`, and any `tr:hover .addr-row-copy-btn` rule
- `.file-chooser-btn`, `.file-chooser-btn:hover`, plus the `[data-theme="light"] .file-chooser-btn` and `[data-theme="light"] .file-chooser-btn:hover` overrides
- `.theme-toggle-btn`, `.theme-toggle-btn:hover`, plus the `[data-theme="light"]` overrides
- `#send-address-book-btn`, `#send-address-book-btn:hover`
- `#send-max-btn`, `#send-max-btn:hover` (plus the `/* Max button — ... */` comment block above it)

**Do not delete** rules that have unrelated responsibilities — e.g., `#send-save-row`, `#send-save-link`, `#send-save-submit`, `#send-contact-clear-btn`, `#ab-picker-close-btn`, `#logout-btn`, `#switch-mode-btn`. Those have HTML migration paths not covered by Task 3 and should keep working until a future pass touches them. (The blue-color migration in Task 8 already normalized their colors.)

- [ ] **Step 4: Run tests to verify all pass**

Run: `python3 -m unittest tests.test_desktop_ui_consistency -v`
Expected: PASS.

- [ ] **Step 5: Visual check**

Reload Tauri dev. Walk through:
- Transaction list: copy icons (now `.btn-icon`), tx-details chips (now `.btn-chip`) still render correctly.
- Address Book: copy buttons render as `.btn-chip`.
- Receive: copy button on the address card renders as `.btn-chip`.
- Theme toggle (top-right): still works, renders as `.btn-icon` (light/dark theme overrides may need a one-line addition if the icon color looks wrong in light mode — if so, add `[data-theme="light"] .btn-icon { color: <appropriate-dim>; }` inside the fenced block).

- [ ] **Step 6: Commit**

```bash
git add tests/test_desktop_ui_consistency.py tauri/src/style.css
git commit -m "refactor(desktop-ui): delete superseded one-off button rules from tauri css"
```

---

## Task 11: Delete superseded one-off button rules in `ui/static/style.css`

**Files:**
- Modify: `ui/static/style.css` — same as Task 10

**Interfaces:** same as Task 10

- [ ] **Step 1: Write the failing test**

Append to `tests/test_desktop_ui_consistency.py`:

```python
class TestOfflineCssLegacySelectorsRemoved(unittest.TestCase):
    def setUp(self):
        self.text = _strip_fenced_block(_read(OFFLINE_CSS))

    def test_no_legacy_button_selectors(self):
        for selector in LEGACY_BUTTON_SELECTORS_IN_CSS:
            pattern = re.compile(rf"^[^{{\n]*{re.escape(selector)}(?:[:\s,][^{{\n]*)?\{{", re.MULTILINE)
            matches = pattern.findall(self.text)
            self.assertEqual(
                matches, [],
                f"legacy selector {selector!r} still defined in ui/static/style.css: {matches[:2]}",
            )
```

- [ ] **Step 2: Run test to verify it fails**

Run: `python3 -m unittest tests.test_desktop_ui_consistency.TestOfflineCssLegacySelectorsRemoved -v`
Expected: FAIL.

- [ ] **Step 3: Delete the legacy rules in `ui/static/style.css`**

Same selectors as Task 10 Step 3. The offline stylesheet has fewer of them — only those that exist need deleting. Confirmed present in offline css (from earlier search): `.copy-btn`, `.tx-details-btn`, `.addr-copy-btn`, `.file-chooser-btn`, `.theme-toggle-btn`. The rest can be skipped (already absent — the test will pass once the existing ones are removed).

- [ ] **Step 4: Run tests to verify all pass**

Run: `python3 -m unittest tests.test_desktop_ui_consistency -v`
Expected: PASS.

- [ ] **Step 5: Commit**

```bash
git add tests/test_desktop_ui_consistency.py ui/static/style.css
git commit -m "refactor(desktop-ui): delete superseded one-off button rules from offline css"
```

---

## Task 12: Final integration walkthrough + verify both apps

**Files:**
- No code changes (verification only). May surface follow-up fixes — handle them as small commits.

**Interfaces:**
- Consumes: all prior tasks
- Produces: hand-validated, shippable state

- [ ] **Step 1: Run the full consistency test suite one more time**

Run: `python3 -m unittest tests.test_desktop_ui_consistency -v`
Expected: PASS — all test classes (`TestSharedTokenBlock`, `TestSharedBlockContents`, `TestTauriHtmlMigrated`, `TestOfflineHtmlMigrated`, `TestTauriJsMigrated`, `TestTauriCssRadii`, `TestOfflineCssRadii`, `TestTauriCssNoBlue`, `TestOfflineCssNoBlue`, `TestTauriCssLegacySelectorsRemoved`, `TestOfflineCssLegacySelectorsRemoved`).

- [ ] **Step 2: Run the rest of the existing test suite to confirm no regressions**

Run: `python3 -m unittest discover tests -v`
Expected: PASS — all pre-existing tests still pass.

- [ ] **Step 3: Tauri dev walkthrough — dark mode**

Run `cd tauri && cargo tauri dev`. Visit each tab and check:

| Tab / surface | Check |
|---|---|
| Home / mode select | Buttons compact, panels rounder |
| Online wallet list | Panel corners 24px, no pinched-corner look |
| Send | Address book icon inside To input; Max inside Amount input; Create PSBT / Cancel buttons compact below |
| Receive | Copy button on address renders as chip; QR card rounded |
| Explorer | Search input + button on same row; hovering Search button doesn't flash blue |
| Transactions | Copy icons (btn-icon) and tx-details chips render; hovering them warms to orange |
| Broadcast | Textarea + Broadcast button stacked; button compact |
| Address Book | List items, copy chips, picker — all consistent |
| Node Connection | Configure Node Connection button is compact; Cancel buttons compact |
| Settings / About | Existing layout intact; theme toggle button works |
| Logout / Switch Mode (top-right) | Compact, orange-tinted (not blue) |

- [ ] **Step 4: Tauri dev walkthrough — light mode**

Toggle theme to light. Repeat Step 3's surfaces and confirm:
- No element has invisible / illegible text
- Hover states still warm to orange (not blue)
- Panel corners and button heights match dark mode

If anything looks wrong (e.g., `.btn-icon` in light mode needs a different color), add a one-line `[data-theme="light"]` override INSIDE the fenced block (so both stylesheets stay in sync — re-run the consistency tests after).

- [ ] **Step 5: Offline app walkthrough**

Launch the offline signer (the existing dev command — see project docs). Confirm Sign, Wallet Info, Broadcast, Explorer all render with the new system. Same visual checks as Step 3 where applicable.

- [ ] **Step 6: If any visual fixes were needed in Steps 4–5, commit them**

```bash
git add <changed-files>
git commit -m "fix(desktop-ui): <specific fix description>"
```

- [ ] **Step 7: Final consistency test run**

Run: `python3 -m unittest tests.test_desktop_ui_consistency -v`
Expected: PASS.

---

## Self-Review

**Spec coverage:**
- §1 Radius scale → Task 2 (definition) + Tasks 6, 7 (migration)
- §2 Button family → Task 2 (definition) + Tasks 10, 11 (cleanup)
- §3 Three layout patterns → Task 2 (CSS helpers) + Tasks 3, 4, 5 (HTML/JS application)
- §4 Hover-color normalization → Tasks 5 (JS), 8, 9 (CSS)
- §5 Cross-frontend sync → Task 1 (test infrastructure) + every CSS task re-runs the sync check
- Files-touched section → mapped 1:1 to Tasks 1–11
- Verification section → Task 12
- Out-of-scope items → not implemented (correctly)

**Placeholder scan:** no TBD / TODO / "implement later" / vague "add error handling" patterns. All code blocks contain complete, exact code or exact substitution rules.

**Type / name consistency:** CSS class names used in HTML (Tasks 3, 4) and JS (Task 5) all reference classes defined in Task 2. The `LEGACY_BUTTON_*` constants are defined once in Task 3 and reused in Tasks 4, 10, 11. The `_strip_fenced_block` and `_radius_literals_outside_scale` helpers are defined in Task 6 and reused in Task 7 — be sure to add Task 6's helper definitions to the test file before Task 7's test class, OR define them as module-level helpers when first added in Task 6.
