` widget titles (moved to sidebar wrapper) |
-| `css/tailwind.css` | Add `.widget-header` and `.widget-collapsible` styles |
-
-## Files NOT Modified
-
-- `tailwind.config.js` — no config changes needed (using arbitrary values)
-- `_data/*.js` — no data changes
-- `eleventy.config.js` — no config changes
-- `indiekit-endpoint-homepage/` — no plugin changes
-- `indiekit-endpoint-cv/` — no plugin changes
-
-## Testing
-
-1. Verify homepage renders correctly with all three changes
-2. Test accordion open/close on projects section
-3. Test sidebar collapse/expand and localStorage persistence (close browser, reopen, verify state)
-4. Test dark mode for all color-coded borders
-5. Test mobile responsiveness (sidebar stacks to full-width, widgets should still be collapsible)
-6. Verify h-card microformat markup is preserved in the author-card widget
-7. Verify the /cv/ page is unaffected (cv-projects on /cv/ uses a different template or the same template — if same, accordion applies there too, which is acceptable)
-8. Visual check with playwright-cli on the live site after deployment
-
-## Risks
-
-- **Widget title extraction:** Moving titles from individual widget files to the wrapper requires updating 10+ files. Risk of missing one or breaking a title.
-- **localStorage key collisions:** Using `widget-{type}` as keys. If the same widget type appears twice in the sidebar config, they'd share state. Mitigate by using `widget-{index}` or `widget-{type}-{index}`.
-- **Alpine.js load order:** Widgets wrapped in `` may not have Alpine.js available when the wrapper tries to initialize. Solution: the wrapper's `x-data` is outside ``, so Alpine handles the toggle, and `` handles lazy-loading the widget content inside.
diff --git a/docs/plans/2026-02-24-homepage-ui-ux-plan.md b/docs/plans/2026-02-24-homepage-ui-ux-plan.md
deleted file mode 100644
index 61cff4b..0000000
--- a/docs/plans/2026-02-24-homepage-ui-ux-plan.md
+++ /dev/null
@@ -1,592 +0,0 @@
-# Homepage UI/UX Improvements — Implementation Plan
-
-> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
-
-**Goal:** Improve homepage scannability by adding post card color-coded borders, collapsible project accordion, and collapsible sidebar widgets.
-
-**Architecture:** Three independent rendering changes in the Eleventy theme's Nunjucks templates + Tailwind CSS. No data model changes. Alpine.js handles all interactivity (already loaded). localStorage persists sidebar widget collapse state.
-
-**Tech Stack:** Nunjucks templates, Tailwind CSS (arbitrary values), Alpine.js, localStorage
-
-**Design doc:** `docs/plans/2026-02-24-homepage-ui-ux-design.md`
-
----
-
-## Task 1: Post Card Color-Coded Left Borders
-
-**Files:**
-- Modify: `_includes/components/sections/recent-posts.njk`
-
-This is the simplest change — add a `border-l-[3px]` class with a type-specific color to each `` element. The template already branches by post type (like, bookmark, repost, reply, photo, article, note), so each branch gets its own color.
-
-**Color mapping (from design doc):**
-| Post Type | Classes |
-|-----------|---------|
-| Like | `border-l-[3px] border-l-red-400 dark:border-l-red-500` |
-| Bookmark | `border-l-[3px] border-l-amber-400 dark:border-l-amber-500` |
-| Repost | `border-l-[3px] border-l-green-400 dark:border-l-green-500` |
-| Reply | `border-l-[3px] border-l-primary-400 dark:border-l-primary-500` |
-| Photo | `border-l-[3px] border-l-purple-400 dark:border-l-purple-500` |
-| Article | `border-l-[3px] border-l-surface-300 dark:border-l-surface-600` |
-| Note | `border-l-[3px] border-l-surface-300 dark:border-l-surface-600` |
-
-### Step 1: Implement the color-coded borders
-
-The `` tag on **line 19** is shared by ALL post types. The type detection happens INSIDE the article (lines 22-27 set variables, lines 28-226 branch by type). Since we need different border colors per type, we must move the `` tag inside each branch, OR use a Nunjucks variable to set the border class before the article opens.
-
-**Approach:** Set a border class variable before the `` tag using Nunjucks `{% set %}` blocks. This keeps the single `` tag and avoids duplicating it 7 times.
-
-In `_includes/components/sections/recent-posts.njk`, replace the block from line 18 through line 28 (the `{% for %}`, type detection variables, and `` opening) with this version that sets a border class variable:
-
-**Current (lines 18-19):**
-```nunjucks
- {% for post in collections.posts | head(maxItems) %}
-
-```
-
-**After:** Insert the type detection BEFORE the `` tag, set a `borderClass` variable, and add it to the article's class list:
-
-```nunjucks
- {% for post in collections.posts | head(maxItems) %}
-
- {# Detect post type for color-coded left border #}
- {% set likedUrl = post.data.likeOf or post.data.like_of %}
- {% set bookmarkedUrl = post.data.bookmarkOf or post.data.bookmark_of %}
- {% set repostedUrl = post.data.repostOf or post.data.repost_of %}
- {% set replyToUrl = post.data.inReplyTo or post.data.in_reply_to %}
- {% set hasPhotos = post.data.photo and post.data.photo.length %}
-
- {% if likedUrl %}
- {% set borderClass = "border-l-[3px] border-l-red-400 dark:border-l-red-500" %}
- {% elif bookmarkedUrl %}
- {% set borderClass = "border-l-[3px] border-l-amber-400 dark:border-l-amber-500" %}
- {% elif repostedUrl %}
- {% set borderClass = "border-l-[3px] border-l-green-400 dark:border-l-green-500" %}
- {% elif replyToUrl %}
- {% set borderClass = "border-l-[3px] border-l-primary-400 dark:border-l-primary-500" %}
- {% elif hasPhotos %}
- {% set borderClass = "border-l-[3px] border-l-purple-400 dark:border-l-purple-500" %}
- {% else %}
- {% set borderClass = "border-l-[3px] border-l-surface-300 dark:border-l-surface-600" %}
- {% endif %}
-
-
-```
-
-Then **remove** the duplicate type detection variables that currently exist inside the article (lines 22-26), since they've been moved above. The rest of the template still uses these same variable names in the `{% if likedUrl %}` / `{% elif %}` branches, so those continue to work — the variables are already set.
-
-**Important:** The type detection variables (`likedUrl`, `bookmarkedUrl`, `repostedUrl`, `replyToUrl`, `hasPhotos`) are currently declared on lines 22-26 inside the ``. After this change, they're declared before the ``. Since they're still within the same `{% for %}` loop scope, all subsequent `{% if %}` checks on lines 28+ continue to reference them correctly. Remove lines 22-26 to avoid redeclaring the same variables.
-
-### Step 2: Build and verify
-
-Run:
-```bash
-cd /home/rick/code/indiekit-dev/indiekit-eleventy-theme
-npm run build
-```
-
-Expected: Build completes with exit 0, no template errors.
-
-### Step 3: Visual verification with playwright-cli
-
-```bash
-playwright-cli open https://rmendes.net
-playwright-cli snapshot
-```
-
-Verify: Post cards in "Recent Posts" section have colored left borders (red for likes, green for reposts, etc.). Take a screenshot for evidence.
-
-### Step 4: Commit
-
-```bash
-cd /home/rick/code/indiekit-dev/indiekit-eleventy-theme
-git add _includes/components/sections/recent-posts.njk
-git commit -m "feat: add color-coded left borders to post cards by type"
-```
-
----
-
-## Task 2: Projects Accordion
-
-**Files:**
-- Modify: `_includes/components/sections/cv-projects.njk`
-
-Convert the always-expanded 2-column project cards grid into an Alpine.js accordion. Each card shows a collapsed summary row (name + status badge + date range + chevron) and expands on click to reveal description + technology tags.
-
-### Step 1: Implement the accordion
-
-Replace the entire content of `_includes/components/sections/cv-projects.njk` with the accordion version.
-
-**Current behavior:** Lines 16-58 render a `grid grid-cols-1 sm:grid-cols-2 gap-4` with each card showing name, status, dates, description, and tech tags all at once.
-
-**New behavior:** Same grid layout, but each card has:
-- A clickable summary row (always visible): project name (linked if URL), status badge, date range, chevron icon
-- A collapsible detail section (hidden by default): description + tech tags, revealed with `x-show` + `x-transition`
-
-**Full replacement for `cv-projects.njk`:**
-
-```nunjucks
-{#
- CV Projects Section - collapsible project cards (accordion)
- Data fetched from /cv/data.json via homepage plugin
-#}
-
-{% set sectionConfig = section.config or {} %}
-{% set maxItems = sectionConfig.maxItems or 10 %}
-{% set showTechnologies = sectionConfig.showTechnologies if sectionConfig.showTechnologies is defined else true %}
-
-{% if cv and cv.projects and cv.projects.length %}
-
-
- {{ sectionConfig.title or "Projects" }}
-
-
-
- {% for item in cv.projects | head(maxItems) %}
-
- {# Summary row — always visible, clickable #}
-
-
- {# Detail section — collapsible #}
-
- {% if item.startDate %}
-
- {{ item.startDate }}{% if item.endDate %} – {{ item.endDate }}{% else %} – Present{% endif %}
-
- {% endif %}
-
- {% if item.description %}
- {{ item.description }}
- {% endif %}
-
- {% if showTechnologies and item.technologies and item.technologies.length %}
-
- {% for tech in item.technologies %}
-
- {{ tech }}
-
- {% endfor %}
-
- {% endif %}
-
-
- {% endfor %}
-
-
-{% endif %}
-```
-
-**Key details:**
-- `x-data="{ expanded: {} }"` on the `` — object-based tracking, independent toggles
-- `@click.stop` on the project name `` link — prevents the button click handler from firing when clicking the link
-- Date range shown in summary row on `sm:` screens, and duplicated inside the collapsible detail for mobile (`sm:hidden`)
-- `x-cloak` hides detail sections during Alpine.js initialization
-- `x-transition` with opacity + translate-y for smooth reveal
-- Chevron rotates 180deg via `:class="expanded[index] && 'rotate-180'"`
-- `aria-expanded` attribute for accessibility
-
-### Step 2: Build and verify
-
-```bash
-cd /home/rick/code/indiekit-dev/indiekit-eleventy-theme
-npm run build
-```
-
-Expected: Exit 0.
-
-### Step 3: Visual verification with playwright-cli
-
-```bash
-playwright-cli open https://rmendes.net
-playwright-cli snapshot
-```
-
-Verify: Projects section shows collapsed cards with name + status + date + chevron. Click a project card to expand — description and tech tags appear with smooth animation.
-
-### Step 4: Commit
-
-```bash
-cd /home/rick/code/indiekit-dev/indiekit-eleventy-theme
-git add _includes/components/sections/cv-projects.njk
-git commit -m "feat: convert projects section to collapsible accordion"
-```
-
----
-
-## Task 3: Sidebar Widget Collapsibility
-
-**Files:**
-- Modify: `_includes/components/homepage-sidebar.njk` — add collapsible wrapper
-- Modify: `css/tailwind.css` — add `.widget-header` and `.widget-collapsible` styles
-- Modify: 10 widget files — remove `` titles (moved to sidebar wrapper)
-
-This is the most complex change. The sidebar dispatcher wraps each widget in a collapsible Alpine.js container. The wrapper provides the `` title + chevron toggle, and a CSS rule hides the inner widget title to avoid duplication.
-
-### Step 1: Add CSS classes for widget collapsibility
-
-In `css/tailwind.css`, add these classes inside the existing `@layer components` block (after the `.widget-title` rule, around line 293):
-
-```css
- /* Collapsible widget wrapper */
- .widget-header {
- @apply flex items-center justify-between cursor-pointer;
- }
-
- .widget-header .widget-title {
- @apply mb-0;
- }
-
- .widget-chevron {
- @apply w-4 h-4 text-surface-400 transition-transform duration-200 shrink-0;
- }
-
- /* Hide inner widget titles when the collapsible wrapper provides one */
- .widget-collapsible .widget .widget-title {
- @apply hidden;
- }
-
- /* Hide FeedLand's custom title in collapsible wrapper */
- .widget-collapsible .widget .fl-title {
- @apply hidden;
- }
-```
-
-### Step 2: Rewrite homepage-sidebar.njk with collapsible wrapper
-
-Replace the entire content of `_includes/components/homepage-sidebar.njk`.
-
-**Widget title map** (from design doc):
-
-| widget.type | Title |
-|-------------|-------|
-| search | Search |
-| social-activity | Social Activity |
-| github-repos | GitHub |
-| funkwhale | Listening |
-| recent-posts | Recent Posts |
-| blogroll | Blogroll |
-| feedland | FeedLand |
-| categories | Categories |
-| webmentions | Webmentions |
-| recent-comments | Recent Comments |
-| fediverse-follow | Fediverse |
-| author-card | Author |
-| custom-html | (from widget.config.title or "Custom") |
-
-**New `homepage-sidebar.njk`:**
-
-```nunjucks
-{# Homepage Builder Sidebar — renders widgets from homepageConfig.sidebar #}
-{# Each widget is wrapped in a collapsible container with localStorage persistence #}
-{% if homepageConfig.sidebar and homepageConfig.sidebar.length %}
- {% for widget in homepageConfig.sidebar %}
-
- {# Resolve widget title #}
- {% if widget.type == "search" %}{% set widgetTitle = "Search" %}
- {% elif widget.type == "social-activity" %}{% set widgetTitle = "Social Activity" %}
- {% elif widget.type == "github-repos" %}{% set widgetTitle = "GitHub" %}
- {% elif widget.type == "funkwhale" %}{% set widgetTitle = "Listening" %}
- {% elif widget.type == "recent-posts" %}{% set widgetTitle = "Recent Posts" %}
- {% elif widget.type == "blogroll" %}{% set widgetTitle = "Blogroll" %}
- {% elif widget.type == "feedland" %}{% set widgetTitle = "FeedLand" %}
- {% elif widget.type == "categories" %}{% set widgetTitle = "Categories" %}
- {% elif widget.type == "webmentions" %}{% set widgetTitle = "Webmentions" %}
- {% elif widget.type == "recent-comments" %}{% set widgetTitle = "Recent Comments" %}
- {% elif widget.type == "fediverse-follow" %}{% set widgetTitle = "Fediverse" %}
- {% elif widget.type == "author-card" %}{% set widgetTitle = "Author" %}
- {% elif widget.type == "custom-html" %}{% set widgetTitle = (widget.config.title if widget.config and widget.config.title) or "Custom" %}
- {% else %}{% set widgetTitle = widget.type %}
- {% endif %}
-
- {% set widgetKey = "widget-" + widget.type + "-" + loop.index0 %}
- {% set defaultOpen = "true" if loop.index0 < 3 else "false" %}
-
- {# Collapsible wrapper — Alpine.js handles toggle, localStorage persists state #}
-
-
- {% endfor %}
-{% endif %}
-```
-
-**Key architecture decisions:**
-- The wrapper provides the outer card styling (`bg-white`, `rounded-lg`, `border`, `shadow-sm`) and the title + chevron
-- The inner widget files keep their `.widget` class, but the inner title is hidden via CSS `.widget-collapsible .widget .widget-title { display: none; }`
-- The `search` widget was previously inline in the sidebar — it's now included directly (no separate file), with the inner `` removed since the wrapper provides it
-- The `custom-html` widget's inner `` is removed — the wrapper uses `widget.config.title` or "Custom"
-- The `` wrappers remain inside the individual widget files — the collapsible wrapper is OUTSIDE ``, so the toggle works immediately even before the lazy-loaded content initializes
-- `widgetKey` uses `widget.type + "-" + loop.index0` to avoid localStorage key collisions if the same widget type appears twice
-- First 3 widgets open by default (`loop.index0 < 3`), rest collapsed
-
-**Note on widget `.widget` class and double borders:** The wrapper div already has `bg-white rounded-lg border shadow-sm`, and the inner `.widget` class also has those styles. To avoid double borders/shadows, we need to neutralize the inner `.widget` styling when inside `.widget-collapsible`. Add this to the CSS in Step 1:
-
-```css
- /* Neutralize inner widget card styling when inside collapsible wrapper */
- .widget-collapsible .widget {
- @apply border-0 shadow-none rounded-none mb-0 bg-transparent;
- }
-```
-
-### Step 3: Build and verify
-
-```bash
-cd /home/rick/code/indiekit-dev/indiekit-eleventy-theme
-npm run build
-```
-
-Expected: Exit 0.
-
-### Step 4: Visual verification with playwright-cli
-
-```bash
-playwright-cli open https://rmendes.net
-playwright-cli snapshot
-```
-
-Verify:
-- All sidebar widgets have a title + chevron header
-- First 3 widgets are expanded, remaining are collapsed
-- Click a collapsed widget title → it expands smoothly
-- Click an expanded widget title → it collapses
-- No duplicate titles visible (inner titles hidden)
-- No double borders or shadows on widget cards
-
-### Step 5: Test localStorage persistence
-
-```bash
-playwright-cli click # Toggle a widget
-playwright-cli eval "localStorage.getItem('widget-social-activity-1')"
-playwright-cli close
-playwright-cli open https://rmendes.net
-playwright-cli snapshot
-```
-
-Verify: The widget you toggled retains its state after page reload.
-
-### Step 6: Verify dark mode
-
-```bash
-playwright-cli eval "document.documentElement.classList.add('dark')"
-playwright-cli screenshot --filename=dark-mode-sidebar
-```
-
-Verify: Widget headers, chevrons, and collapsed/expanded states look correct in dark mode.
-
-### Step 7: Verify mobile responsiveness
-
-```bash
-playwright-cli resize 375 812
-playwright-cli snapshot
-```
-
-Verify: Sidebar stacks below main content, widgets are still collapsible.
-
-### Step 8: Commit
-
-```bash
-cd /home/rick/code/indiekit-dev/indiekit-eleventy-theme
-git add _includes/components/homepage-sidebar.njk css/tailwind.css
-git commit -m "feat: add collapsible sidebar widgets with localStorage persistence"
-```
-
----
-
-## Task 4: Deploy and Final Verification
-
-**Files:** None (deployment commands only)
-
-### Step 1: Push theme repo
-
-```bash
-cd /home/rick/code/indiekit-dev/indiekit-eleventy-theme
-git push origin main
-```
-
-### Step 2: Update submodule in indiekit-cloudron
-
-```bash
-cd /home/rick/code/indiekit-dev/indiekit-cloudron
-git submodule update --remote eleventy-site
-git add eleventy-site
-git commit -m "chore: update eleventy-site submodule (homepage UI/UX improvements)"
-git push origin main
-```
-
-### Step 3: Build and deploy
-
-```bash
-cd /home/rick/code/indiekit-dev/indiekit-cloudron
-make prepare
-cloudron build --no-cache && cloudron update --app rmendes.net --no-backup
-```
-
-### Step 4: Final visual verification on live site
-
-```bash
-playwright-cli open https://rmendes.net
-playwright-cli screenshot --filename=homepage-final
-playwright-cli snapshot
-```
-
-Verify all three changes are live:
-1. Post cards have color-coded left borders
-2. Projects section is collapsible (all collapsed by default)
-3. Sidebar widgets are collapsible (first 3 open, rest collapsed)
-4. Dark mode works for all changes
-5. h-card (Author widget) is present and contains proper microformat markup
-
----
-
-## Risk Mitigation Notes
-
-1. **`` + Alpine.js interaction:** The collapsible wrapper's `x-data` is OUTSIDE ``. Alpine.js initializes the toggle immediately. The `` inside widget files handles lazy-loading the widget content. This means the toggle button works before the widget content loads — expanding an unloaded widget triggers `` visibility, which then loads the content.
-
-2. **FeedLand widget:** Uses custom `fl-title` instead of `widget-title`. The CSS rule `.widget-collapsible .widget .fl-title { display: none; }` handles this case.
-
-3. **Author card widget:** Has no inner `` — it just includes `h-card.njk`. The CSS hiding rule won't find anything to hide, which is fine. The wrapper provides "Author" as the title.
-
-4. **Search widget:** Was previously inline in the sidebar with its own `` + ``. Now it's inline inside the collapsible wrapper with the `` removed. The `` wrapper is preserved inside for lazy-loading Pagefind.
-
- **Wait — re-reading the new sidebar template:** The search widget was changed to NOT use `` in the inline version. Let me note this: the search widget should keep its `` wrapper inside the collapsible content div. Update the search case to:
- ```nunjucks
- {% elif widget.type == "search" %}
-
-
-
- ```
-
-5. **Custom HTML widget:** Similarly should keep `` wrapper:
- ```nunjucks
- {% elif widget.type == "custom-html" %}
- {% set wConfig = widget.config or {} %}
-
-
-
- ```
diff --git a/docs/plans/2026-02-25-identity-editor-design.md b/docs/plans/2026-02-25-identity-editor-design.md
deleted file mode 100644
index 2ae1148..0000000
--- a/docs/plans/2026-02-25-identity-editor-design.md
+++ /dev/null
@@ -1,177 +0,0 @@
-# Editable Identity via Homepage Plugin — Design
-
-## Goal
-
-Make all author identity fields (hero content, h-card data, social links) editable from the Indiekit admin UI, stored permanently in MongoDB via the homepage plugin's existing `identity` field.
-
-## Architecture
-
-Extend `indiekit-endpoint-homepage` with a three-tab admin interface and an `identity` data section. The theme templates check `homepageConfig.identity.*` first, falling back to `site.author.*` environment variables. No new plugin, no new data file — identity lives in the existing `homepage.json`.
-
-## Tab Structure
-
-### Tab 1: Homepage Builder (`/homepage`)
-
-Existing functionality, reorganized. Contains:
-- Layout selection (presets + radio options)
-- Hero config (enabled, show social toggles)
-- Content Sections (drag-drop)
-- Homepage Sidebar (drag-drop)
-- Footer (drag-drop)
-
-### Tab 2: Blog Sidebar (`/homepage/blog-sidebar`)
-
-Extracted from the current single-page dashboard:
-- Blog Listing Sidebar (drag-drop widget list)
-- Blog Post Sidebar (drag-drop widget list)
-
-### Tab 3: Identity (`/homepage/identity`)
-
-New form page with sections:
-
-**Profile:**
-- `name` — text input
-- `avatar` — text input (URL)
-- `title` — text input (job title / subtitle)
-- `pronoun` — text input
-- `bio` — textarea
-- `description` — textarea (site description shown in hero)
-
-**Location:**
-- `locality` — text input (city)
-- `country` — text input
-- `org` — text input (organization)
-
-**Contact:**
-- `url` — text input (author URL)
-- `email` — text input
-- `keyUrl` — text input (PGP key URL)
-
-**Skills:**
-- `categories` — tag-input component (comma-separated skills/interests)
-
-**Social Links:**
-- Full CRUD list using add-another pattern
-- Each entry: `name` (text), `url` (text), `rel` (text, default "me"), `icon` (select from: github, linkedin, bluesky, mastodon, activitypub)
-- Add, remove, reorder
-
-## Data Model
-
-### MongoDB Document (`homepageConfig` collection)
-
-The existing singleton document gains an `identity` field:
-
-```javascript
-{
- _id: "homepage",
- layout: "two-column",
- hero: { enabled: true, showSocial: true },
- sections: [...],
- sidebar: [...],
- blogListingSidebar: [...],
- blogPostSidebar: [...],
- footer: [...],
- identity: {
- name: "Ricardo Mendes",
- avatar: "https://...",
- title: "Middleware Engineer & DevOps Specialist",
- pronoun: "he/him",
- bio: "Building infrastructure, automating workflows...",
- description: "DevOps engineer, IndieWeb enthusiast...",
- locality: "Brussels",
- country: "Belgium",
- org: "",
- url: "https://rmendes.net",
- email: "rick@example.com",
- keyUrl: "https://...",
- categories: ["IndieWeb", "OSINT", "DevOps"],
- social: [
- { name: "GitHub", url: "https://github.com/rmdes", rel: "me", icon: "github" },
- { name: "Bluesky", url: "https://bsky.app/profile/rmendes.net", rel: "me atproto", icon: "bluesky" },
- ...
- ]
- },
- updatedAt: "ISO 8601 string"
-}
-```
-
-### JSON File
-
-Written to `content/.indiekit/homepage.json` on save (same as existing behavior). Eleventy file watcher triggers rebuild.
-
-## Data Precedence
-
-```
-homepageConfig.identity.name → if truthy, use it
- → else fall back to site.author.name (env var)
-```
-
-Applied in the theme templates (hero.njk, h-card.njk) and anywhere else that reads `site.author.*` or `site.social`.
-
-The simplest approach: create a computed `author` object in `_data/homepageConfig.js` that merges identity over site.author, so templates can use a single variable.
-
-## Components Used (from @rmdes/indiekit-frontend)
-
-| Field | Component |
-|-------|-----------|
-| Name, title, pronoun, locality, country, org, url, email, keyUrl | `input()` macro |
-| Bio, description | `textarea()` macro |
-| Categories/skills | `tag-input()` macro |
-| Social links | `add-another()` macro wrapping input fields per entry |
-| Icon selection | `select()` macro with predefined icon options |
-| Form sections | `fieldset()` macro with legends |
-| Save/cancel | `button()` macro (primary/secondary) |
-| Errors | `errorSummary()` + field-level `errorMessage` |
-
-## Tab Navigation
-
-URL-based tabs using server-rendered pages (not client-side switching):
-
-- `GET /homepage` — Homepage Builder tab
-- `GET /homepage/blog-sidebar` — Blog Sidebar tab
-- `GET /homepage/identity` — Identity tab
-
-Each tab is a separate form with its own POST endpoint:
-- `POST /homepage/save` — existing, handles layout/hero/sections/sidebar/footer
-- `POST /homepage/save-blog-sidebar` — handles blogListingSidebar + blogPostSidebar
-- `POST /homepage/save-identity` — handles identity fields
-
-A shared tab navigation bar appears at the top of all three pages.
-
-## Files Changed
-
-### indiekit-endpoint-homepage (plugin)
-
-| File | Change |
-|------|--------|
-| `index.js` | Add identity routes, identity configSchema |
-| `lib/controllers/dashboard.js` | Split blog sidebar into separate GET handler, add identity GET/POST |
-| `lib/controllers/api.js` | Add identity to public config endpoint |
-| `lib/storage/config.js` | Handle identity save/merge |
-| `views/homepage-dashboard.njk` | Remove blog sidebar sections, add tab nav |
-| `views/homepage-blog-sidebar.njk` | New — extracted blog sidebar UI |
-| `views/homepage-identity.njk` | New — identity editor form |
-| `views/partials/tab-nav.njk` | New — shared tab navigation partial |
-| `locales/en.json` | Add identity i18n strings |
-
-### indiekit-eleventy-theme (theme)
-
-| File | Change |
-|------|--------|
-| `_data/homepageConfig.js` | Merge identity over site.author, expose computed `author` object |
-| `_includes/components/sections/hero.njk` | Use merged author data instead of raw site.author |
-| `_includes/components/h-card.njk` | Use merged author data instead of raw site.author |
-
-## Not Changed
-
-- `_data/site.js` — env vars remain as fallback source, untouched
-- Main feed templates — don't reference author data
-- Post layout — uses site.author for meta tags (will get override via merged data)
-- Other plugins — no changes needed
-
-## Constraints
-
-- Identity editor uses standard Indiekit frontend components (no custom JS beyond add-another)
-- Social link icons limited to the set already defined in hero.njk/h-card.njk SVGs (github, linkedin, bluesky, mastodon, activitypub) — extensible later
-- Avatar is a URL field, not file upload (avatar image hosting is separate)
-- All dates stored as ISO 8601 strings
diff --git a/docs/plans/2026-02-25-identity-editor-plan.md b/docs/plans/2026-02-25-identity-editor-plan.md
deleted file mode 100644
index 2d7b9a2..0000000
--- a/docs/plans/2026-02-25-identity-editor-plan.md
+++ /dev/null
@@ -1,468 +0,0 @@
-# Editable Identity via Homepage Plugin — Implementation Plan
-
-> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
-
-**Goal:** Make all author identity fields editable from the Indiekit admin UI via a three-tab homepage dashboard with Identity CRUD.
-
-**Architecture:** Extend `indiekit-endpoint-homepage` with URL-based tabs (Homepage Builder, Blog Sidebar, Identity). Identity data stored in `homepageConfig.identity` in MongoDB + `homepage.json`. Theme templates check identity data first, falling back to `site.author.*` env vars.
-
-**Tech Stack:** Express.js, Nunjucks, @rmdes/indiekit-frontend components, MongoDB, Eleventy
-
----
-
-### Task 1: Add tab navigation partial and new routes in index.js
-
-**Files:**
-- Create: `indiekit-endpoint-homepage/views/partials/tab-nav.njk`
-- Modify: `indiekit-endpoint-homepage/index.js`
-
-**Step 1: Create the tab navigation partial**
-
-Create `views/partials/tab-nav.njk`:
-
-```nunjucks
-{# Tab navigation for homepage admin - server-rendered URL tabs #}
-
-
-```
-
-**Step 2: Add new routes in index.js**
-
-In the `get routes()` getter, after the existing routes (after `protectedRouter.get("/api/config", apiController.getConfig);`), add:
-
-```javascript
- // Blog sidebar tab
- protectedRouter.get("/blog-sidebar", dashboardController.getBlogSidebar);
- protectedRouter.post("/save-blog-sidebar", dashboardController.saveBlogSidebar);
-
- // Identity tab
- protectedRouter.get("/identity", dashboardController.getIdentity);
- protectedRouter.post("/save-identity", dashboardController.saveIdentity);
-```
-
-**Step 3: Verify**
-
-Run: `node -e "import('./index.js')"` from the plugin directory to check for syntax errors.
-
-**Step 4: Commit**
-
-```bash
-git add views/partials/tab-nav.njk index.js
-git commit -m "feat(homepage): add tab navigation partial and identity/blog-sidebar routes"
-```
-
----
-
-### Task 2: Add i18n strings for tabs and identity editor
-
-**Files:**
-- Modify: `indiekit-endpoint-homepage/locales/en.json`
-
-**Step 1: Add the new i18n keys**
-
-Add `"tabs"` block as a new top-level key inside `"homepageBuilder"`, and add `"identity"` block. Keep all existing keys unchanged.
-
-Add under `"homepageBuilder"`:
-
-```json
-"tabs": {
- "builder": "Homepage",
- "blogSidebar": "Blog Sidebar",
- "identity": "Identity"
-},
-```
-
-Add the `"identity"` block:
-
-```json
-"identity": {
- "title": "Identity",
- "description": "Configure your author profile, contact details, and social links. These override environment variable defaults.",
- "saved": "Identity saved successfully. Refresh your site to see changes.",
- "profile": {
- "legend": "Profile",
- "name": { "label": "Name", "hint": "Your display name" },
- "avatar": { "label": "Avatar URL", "hint": "URL to your avatar image" },
- "title": { "label": "Title", "hint": "Job title or subtitle" },
- "pronoun": { "label": "Pronoun", "hint": "e.g. he/him, she/her, they/them" },
- "bio": { "label": "Bio", "hint": "Short biography" },
- "description": { "label": "Site Description", "hint": "Description shown in the hero section" }
- },
- "location": {
- "legend": "Location",
- "locality": { "label": "City", "hint": "City or locality" },
- "country": { "label": "Country" },
- "org": { "label": "Organization", "hint": "Company or organization" }
- },
- "contact": {
- "legend": "Contact",
- "url": { "label": "URL", "hint": "Your personal website URL" },
- "email": { "label": "Email" },
- "keyUrl": { "label": "PGP Key URL", "hint": "URL to your public PGP key" }
- },
- "skills": {
- "legend": "Skills & Interests",
- "categories": { "label": "Categories", "hint": "Comma-separated skills, interests, or tags" }
- },
- "social": {
- "legend": "Social Links",
- "description": "Add links to your social profiles. These appear in the hero section and h-card.",
- "name": { "label": "Name" },
- "url": { "label": "URL" },
- "rel": { "label": "Rel" },
- "icon": { "label": "Icon" }
- }
-}
-```
-
-**Step 2: Verify no JSON syntax errors**
-
-Run: `node -e "JSON.parse(require('fs').readFileSync('locales/en.json','utf8')); console.log('OK')"`
-
-**Step 3: Commit**
-
-```bash
-git add locales/en.json
-git commit -m "feat(homepage): add i18n strings for tabs and identity editor"
-```
-
----
-
-### Task 3: Add dashboard controller methods for blog sidebar and identity
-
-**Files:**
-- Modify: `indiekit-endpoint-homepage/lib/controllers/dashboard.js`
-
-**Step 1: Add `parseSocialLinks` helper function**
-
-Add at the top of the file, after the existing imports:
-
-```javascript
-/**
- * Parse social links from form body.
- * Express parses social[0][name], social[0][url] etc. into nested objects.
- */
-function parseSocialLinks(body) {
- const social = [];
- if (!body.social) return social;
- const entries = Array.isArray(body.social) ? body.social : Object.values(body.social);
- for (const entry of entries) {
- if (!entry || (!entry.name && !entry.url)) continue;
- social.push({
- name: entry.name || "",
- url: entry.url || "",
- rel: entry.rel || "me",
- icon: entry.icon || "",
- });
- }
- return social;
-}
-```
-
-**Step 2: Update the existing `get` method**
-
-Add `activeTab: "builder"` to the `response.render()` call.
-
-**Step 3: Update the existing `save` method**
-
-The save method must preserve blog sidebar and identity data that are no longer part of the homepage builder form. Read the current config first and merge:
-
-```javascript
-// Get current config to preserve fields not in this form
-let currentConfig = await getConfig(application);
-
-const config = {
- layout: layout || "single-column",
- hero: typeof hero === "string" ? JSON.parse(hero) : hero,
- sections: typeof sections === "string" ? JSON.parse(sections) : sections,
- sidebar: typeof sidebar === "string" ? JSON.parse(sidebar) : sidebar,
- blogListingSidebar: currentConfig?.blogListingSidebar || [],
- blogPostSidebar: currentConfig?.blogPostSidebar || [],
- footer: typeof footer === "string" ? JSON.parse(footer) : footer,
- identity: currentConfig?.identity || null,
-};
-```
-
-**Step 4: Add `getBlogSidebar` controller method**
-
-Renders `homepage-blog-sidebar` view with `activeTab: "blog-sidebar"`, current config, widgets, and blogPostWidgets.
-
-**Step 5: Add `saveBlogSidebar` controller method**
-
-Reads `blogListingSidebar` and `blogPostSidebar` from request body, preserves all other config fields, saves, redirects to `/homepage/blog-sidebar?saved=1`.
-
-**Step 6: Add `getIdentity` controller method**
-
-Reads `config.identity || {}`, renders `homepage-identity` view with `activeTab: "identity"`.
-
-**Step 7: Add `saveIdentity` controller method**
-
-Parses form fields (`identity-name`, `identity-bio`, etc.) and social links using `parseSocialLinks(body)`. Builds identity object, preserves all other config fields, saves, redirects to `/homepage/identity?saved=1`.
-
-**Step 8: Verify**
-
-Run: `node -e "import('./lib/controllers/dashboard.js')"` to check for syntax errors.
-
-**Step 9: Commit**
-
-```bash
-git add lib/controllers/dashboard.js
-git commit -m "feat(homepage): add blog sidebar and identity controller methods"
-```
-
----
-
-### Task 4: Refactor homepage-dashboard.njk — remove blog sidebar, add tab nav
-
-**Files:**
-- Modify: `indiekit-endpoint-homepage/views/homepage-dashboard.njk`
-
-**Step 1: Add tab nav include**
-
-After the page header ``, before the success message `{% if request.query.saved %}`, add:
-
-```nunjucks
-{% include "partials/tab-nav.njk" %}
-```
-
-**Step 2: Remove blog sidebar sections from HTML**
-
-Remove the two `` blocks:
-- Blog Listing Sidebar (with `id="blog-listing-sidebar-list"` and `id="blog-listing-sidebar-json"`)
-- Blog Post Sidebar (with `id="blog-post-sidebar-list"` and `id="blog-post-sidebar-json"`)
-
-**Step 3: Remove blog sidebar JavaScript**
-
-From the `
- {{ sectionConfig.title or "Projects" }} -
- -- {{ item.startDate }}{% if item.endDate %} – {{ item.endDate }}{% else %} – Present{% endif %} -
- {% endif %} - - {% if item.description %} -{{ item.description }}
- {% endif %} - - {% if showTechnologies and item.technologies and item.technologies.length %} -