# Portfolio 2026 Theme 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:** Rebuild the surface of the `ks-portfolio` WordPress theme to match the Portfolio 2026 identity (Carbon Blue dark atmosphere + Raleway typography + project catalog focused on GitHub/Gitea-backed Projects), resolve the documented source conflicts, and ship a working tested theme.

**Architecture:** SoloFrame Evo base (Tailwind 4 + ACF blocks + namespaced PHP classes), namespaced from `SoloFrameEvo` → `KsPortfolio`. The theme is a pure presentation layer over the installed `projects-portfolio` plugin — no new CPT registration. Tokens drive every color/typography decision; templates pull data from WordPress content and the plugin's `projects_portfolio_get_*()` helpers with empty-state fallbacks.

**Tech Stack:** PHP 8.1+ (WordPress 6.x), Tailwind CSS 4, ACF Pro (Secure Custom Fields), Playwright + axe-core, PHPCS + WPCS, Raleway (Google Fonts).

## Global Constraints

- PHP namespace: `KsPortfolio` (was `SoloFrameEvo`). Every PHP file in the theme uses this namespace. Composer text-domain: `ks-portfolio`.
- Text domain: `ks-portfolio` for every `__()`, `_e()`, `esc_html__()`, etc. call. Theme slug in `style.css` header matches.
- Tokens only: no raw hex outside `theme.json` editor palette, `styles/base/colors.css`, or the Carbon Blue partial. Reference via `var(--color-*)` / `var(--font-*)` / `var(--text-*)`.
- `--font-headings: var(--font-sans)` is defined in `styles/base/typography.css`.
- Body surface: `background-color: #100e0b; color: var(--color-on-surface);`. The light `main#maincontent { background: var(--color-background); }` rule is removed.
- Carbon Blue atmospheric field lives in `views/partials/aura-bg.php` and is loaded on `front-page.php` and `archive-projects.php`. The container must NOT carry `background-color` (blend-mode architecture).
- Decorative layers: `position: absolute; inset: 0; pointer-events: none; aria-hidden="true"`.
- Accessibility target: WCAG 2.1 AA. Every template has exactly one `<h1>`. Skip link preserved. `prefers-reduced-motion` honored.
- Security: all forms use `wp_nonce_field`; state-changing requests use `wp_verify_nonce`; dynamic output uses the right `esc_*` for context; external URLs `esc_url` + `rel="noopener noreferrer"` when `target="_blank"`.
- No `picsum.photos` URLs anywhere in shipped theme.
- No invented project copy, statistics, or testimonials. Empty states use the documented copy: `Nothing here yet...`, `No published projects found.`, `Page Not Found`, `Sorry, the page you're looking for doesn't exist.`
- Test scope: Chromium + axe only. Firefox / WebKit / mobile are out of scope. OWASP ZAP out of scope — security audit is PHPCS + grep + manual review.
- Local base URL: `http://portfolio.test` (matches `.env`).

---

## File Structure

### Modified
- `style.css` — already correct; verify only.
- `functions.php` — namespace swap, include new `class-projects.php`, include new aura-bg partial bootstrap.
- `header.php` — namespace swap, dark surface, three-region responsive header, aura-bg on home/archive.
- `footer.php` — namespace swap, dark surface, footer links match Carbon Blue.
- `front-page.php` — full home template replacing 8-line stub.
- `index.php` — namespace swap, dark surface, remove `picsum.photos`, empty-state copy.
- `single.php` — namespace swap, dark surface, remove `picsum.photos`.
- `page.php` — namespace swap, dark surface.
- `search.php` — namespace swap, dark surface, remove `picsum.photos`.
- `404.php` — namespace swap, dark surface, documented empty-state copy.
- `lib/activation.php` — namespace swap.
- `lib/class-acf.php` — namespace swap.
- `lib/class-breadcrumbs.php` — namespace swap.
- `lib/class-enqueue.php` — namespace swap, verify Raleway + theme.css enqueue.
- `lib/class-menuitems.php` — namespace swap.
- `lib/class-resources.php` — namespace swap (keep CPT registration as-is).
- `lib/extras.php` — namespace swap.
- `lib/helpers.php` — namespace swap, add `getTheTitle()`, `hasSidebar()`, `customExcerpt()`, `getFieldValue()`.
- `lib/hooks.php` — namespace swap.
- `lib/search-features.php` — namespace swap.
- `lib/show-template.php` — namespace swap.
- `theme.json` — fix `styles` references to existing slugs, add dark surface palette.
- `styles/base/colors.css` — add Carbon Blue tokens.
- `styles/base/typography.css` — define `--font-headings`.
- `styles/base/global.css` — dark body, remove light `main` rule, dark container / section tweaks.
- `styles/base/forms.css` — dark form surfaces.
- `styles/navigation/nav-*.css` — dark link colors.
- `styles/blocks/buttons.css` — verify dark-surface contrast for primary/secondary buttons.
- `views/components/nav-aux.php`, `nav-main.php`, `nav-main__toggle.php` — namespace swap.
- `views/partials/page-hero.php` — namespace swap, dark surface defaults.
- `views/partials/social-media.php` — namespace swap, dark color overrides.
- `views/forms/search.php` — escape audit only.
- `playwright.config.js` — set base URL, keep chromium only.

### New
- `archive-projects.php` — project archive template.
- `single-projects.php` — project detail template.
- `taxonomy-project-type.php` — project type archive template.
- `views/partials/aura-bg.php` — Carbon Blue atmospheric field.
- `views/components/project-card.php` — reusable project card.
- `views/components/project-stat-card.php` — Downloads/Stars/Forks/Issues tile.
- `views/components/project-meta-row.php` — repository metadata table row.
- `lib/class-projects.php` — thin wrapper around plugin helpers + provider-aware defaults.
- `tests/home.spec.js` — Playwright + axe on home.
- `tests/archive.spec.js` — Playwright + axe on project archive.
- `tests/single.spec.js` — Playwright + axe on project detail.
- `docs/superpowers/specs/2026-08-16-portfolio-2026-design.md` — already committed.
- `docs/superpowers/specs/2026-08-16-portfolio-2026-testing-report.md` — final report (delivered at end).

### Removed
- `screenshot.png` (Basic WP) — replaced at end.
- `picsum.photos` URLs in `index.php`, `search.php`.
- Legacy hex values in pagination CSS.

---

## Task 1: Verify environment + capture baseline screenshots

**Files:**
- Inspect: `playwright.config.js`
- Inspect: `.env`
- Inspect: `static/dist/theme.css`
- Inspect: `wp-content/plugins/projects-portfolio/` (installed)

**Goal:** Confirm the local site at `http://portfolio.test` is reachable, the existing Playwright suite runs, and capture a baseline of each mockup reference page so we can diff later.

- [ ] **Step 1: Confirm base URL and Playwright config**

Open `playwright.config.js`. Confirm chromium is enabled and `baseURL` is set to `http://portfolio.test`. If baseURL is commented out, set:
```js
use: {
  baseURL: 'http://portfolio.test',
  trace: 'on-first-retry',
  ignoreHTTPSErrors: true,
},
```

- [ ] **Step 2: Confirm Playwright + axe dependencies are installed**

Run: `cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio" && npx playwright --version`
Expected: prints a version (e.g. `Version 1.52.x`).

Run: `cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio" && npm ls @axe-core/playwright @playwright/test --depth=0`
Expected: both packages installed.

- [ ] **Step 3: Confirm the projects-portfolio plugin is present**

Run (PowerShell): `Get-ChildItem "C:\Users\ksolo\Herd\portfolio\wp-content\plugins\projects-portfolio" | Select-Object Name`
Expected: shows `projects-portfolio.php`, `README.md`, `includes/`, `templates/`, etc.

- [ ] **Step 4: Probe local site reachability with a 3-second timeout**

Run (PowerShell):
```powershell
try { (Invoke-WebRequest -Uri "http://portfolio.test" -UseBasicParsing -TimeoutSec 3).StatusCode } catch { "unreachable: $($_.Exception.Message)" }
```
Expected: prints `200`. If `unreachable: ...`, stop and ask the user — we cannot run browser tests without the local site.

- [ ] **Step 5: Run the existing Playwright suite as baseline**

Run: `cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio" && npx playwright test tests/site-a11y.spec.js --reporter=list`
Expected: may fail (axe violations expected on current light theme) — that's fine, we capture baseline.

- [ ] **Step 6: Commit no changes (this task is verification only)**

If you modified `playwright.config.js`, run:
```bash
cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio"
git add playwright.config.js
git commit -m "chore: set Playwright baseURL to portfolio.test"
```

---

## Task 2: Define `--font-headings` + dark body surface

**Files:**
- Modify: `styles/base/typography.css` (add `--font-headings`)
- Modify: `styles/base/global.css` (dark body, remove light main rule)

**Goal:** Resolve the unresolved token from `notes/DESIGN.md` §12 and flip the body surface to Carbon Blue dark.

- [ ] **Step 1: Add `--font-headings` to typography.css**

In `styles/base/typography.css`, inside the `@theme { ... }` block (after `--font-sans`), add:
```css
--font-headings: var(--font-sans);
```

- [ ] **Step 2: Darken body + remove light main rule in global.css**

In `styles/base/global.css`, find the existing rule:
```css
main#maincontent {
  background-color: var(--color-background);
  color: var(--color-text);
  margin: 0;
  padding: 0 0 4rem;
}
```

Replace with:
```css
main#maincontent {
  background-color: transparent;
  color: var(--color-on-surface, #e3e1e9);
  margin: 0;
  padding: 0 0 4rem;
}
```

- [ ] **Step 3: Set dark body in typography.css**

In `styles/base/typography.css`, find the existing `body { ... }` block and replace with:
```css
body {
  background-color: #100e0b;
  color: var(--color-on-surface, #e3e1e9);
  font-family: var(--font-sans);
  font-size: var(--text-base);
  line-height: var(--line-height);
}
```

- [ ] **Step 4: Build the CSS bundle**

Run: `cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio" && npm run build`
Expected: `static/dist/theme.css` rebuilt without errors.

- [ ] **Step 5: Commit**

```bash
cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio"
git add styles/base/typography.css styles/base/global.css static/dist/theme.css
git commit -m "feat(theme): add --font-headings; flip body to Carbon Blue dark"
```

---

## Task 3: Add Carbon Blue tokens to colors.css + theme.json

**Files:**
- Modify: `styles/base/colors.css` (add Carbon Blue + Material-derived tokens)
- Modify: `theme.json` (add dark palette; fix `styles` preset references)

**Goal:** Expose Carbon Blue / dark-surface tokens through both CSS custom properties and the WordPress block-editor palette. Fix the `background` / `text` preset reference mismatch from `notes/DESIGN.md` §12.

- [ ] **Step 1: Append Carbon Blue tokens to colors.css**

Open `styles/base/colors.css`. Inside the `@theme { ... }` block, append:
```css
/* Carbon Blue / dark surface tokens (Portfolio 2026) */
--color-surface:           #121318;
--color-surface-dim:       #121318;
--color-surface-bright:    #38393f;
--color-surface-container-lowest: #0d0e13;
--color-surface-container-low:    #1a1b21;
--color-surface-container:        #1e1f25;
--color-surface-container-high:   #292a2f;
--color-surface-container-highest:#34343a;
--color-on-surface:        #e3e1e9;
--color-on-surface-variant:#c5c5d3;
--color-outline:           #8f909d;
--color-outline-variant:   #444651;
--color-page-base:         #100e0b;
--color-backlight:         rgba(96, 165, 250, 0.20);

--color-primary-fixed:        #dce1ff;
--color-primary-fixed-dim:    #b6c4ff;
--color-on-primary-fixed:     #00164e;
--color-on-primary-fixed-variant: #264191;

--color-success: #5ccf91;
--color-warning: #f4e04d;
--color-danger:  #cc3b3b;
--color-info:    #3d7ab0;
```

(If `--color-success` / `--color-warning` / `--color-danger` / `--color-info` already exist in oklch, REMOVE the existing lines first so the new hex values are the only definitions.)

- [ ] **Step 2: Update theme.json palette**

Open `theme.json`. In the `settings.color.palette` array, ADD these entries (don't remove existing ones):
```json
{
  "slug": "theme-surface",
  "color": "#121318",
  "name": "Theme Surface"
},
{
  "slug": "theme-on-surface",
  "color": "#e3e1e9",
  "name": "Theme On Surface"
},
{
  "slug": "theme-outline",
  "color": "#8f909d",
  "name": "Theme Outline"
},
{
  "slug": "theme-outline-variant",
  "color": "#444651",
  "name": "Theme Outline Variant"
},
{
  "slug": "theme-page-base",
  "color": "#100e0b",
  "name": "Theme Page Base"
}
```

- [ ] **Step 3: Fix theme.json styles preset references**

In `theme.json`, find the top-level `styles` block:
```json
"styles": {
  "color": {
    "background": "var(--wp--preset--color--background)",
    "text": "var(--wp--preset--color--text)"
  },
  ...
}
```

Replace with:
```json
"styles": {
  "color": {
    "background": "var(--wp--preset--color--theme-bg, #100e0b)",
    "text": "var(--wp--preset--color--theme-text, #e3e1e9)"
  },
  "elements": {
    "link": {
      "color": {
        "text": "var(--wp--preset--color--theme-bodylinks)"
      }
    }
  },
  "typography": {
    "fontSize": "var(--wp--preset--font-size--base)",
    "fontFamily": "var(--wp--preset--font-family--theme-sans)",
    "lineHeight": "1.5"
  }
}
```

(The `elements.link` is moved up here so it actually applies; previously it was nested under `styles` rather than `styles.elements`, which makes WordPress ignore it.)

- [ ] **Step 4: Build the CSS bundle**

Run: `cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio" && npm run build`

- [ ] **Step 5: Commit**

```bash
cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio"
git add styles/base/colors.css theme.json static/dist/theme.css
git commit -m "feat(tokens): add Carbon Blue dark-surface tokens; fix theme.json presets"
```

---

## Task 4: Carbon Blue atmospheric partial

**Files:**
- Create: `views/partials/aura-bg.php`

**Goal:** Provide a reusable partial that renders the Carbon Blue atmospheric field per `notes/Background Gradient.md`. Used by `front-page.php` and `archive-projects.php` only.

- [ ] **Step 1: Write the partial**

Create `views/partials/aura-bg.php`:
```php
<?php
/**
 * Aura Background Partial — Carbon Blue
 *
 * Renders three decorative layers (linear gradient, radial backlight, SVG
 * grain) over a transparent container. The body carries `background-color:
 * #100e0b`; do NOT set a background on this container or the blend modes
 * will composite against the container instead of the page.
 *
 * All layers are aria-hidden and ignore pointer events. Content must sit
 * inside a wrapper with `position: relative; z-index: 1`.
 *
 * @package KsPortfolio
 */

namespace KsPortfolio;

$blur = isset( $blur ) && 'desktop' === $blur ? 'aura-layer-2--desktop' : 'aura-layer-2--mobile';
?>
<div class="aura-bg" aria-hidden="true">
	<div class="aura-layer-1"></div>
	<div class="aura-layer-2 <?php echo esc_attr( $blur ); ?>"></div>
	<div class="aura-grain">
		<svg width="100%" height="100%" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
			<filter id="aura-grain-filter">
				<feTurbulence type="fractalNoise" baseFrequency="0.7" numOctaves="4" stitchTiles="stitch" />
				<feColorMatrix type="matrix"
					values="0.181 0.608 0.061 0 0.075
					        0.181 0.608 0.061 0 0.075
					        0.181 0.608 0.061 0 0.075
					        0     0     0     1 0" />
			</filter>
			<rect width="100%" height="100%" filter="url(#aura-grain-filter)" />
		</svg>
	</div>
</div>
```

- [ ] **Step 2: Add CSS for the partial**

Append to `styles/base/global.css` (or create `styles/components/aura.css` and `@import` it from `styles/theme.css`):
```css
/* Carbon Blue atmospheric field (decorative — content must sit in z-index: 1) */
.aura-bg {
	position: relative;
	overflow: hidden;
	min-height: 100vh;
	isolation: isolate;
}

.aura-layer-1 {
	position: absolute;
	inset: 0;
	background: linear-gradient(138deg, #05070c 0%, #0f172a 40%, #172554 70%, #1e3a8a 100%);
	mix-blend-mode: normal;
	pointer-events: none;
	transform: translateZ(0);
	will-change: transform;
}

.aura-layer-2 {
	position: absolute;
	inset: 0;
	background: radial-gradient(ellipse 45% 50% at 72% 40%, rgba(96, 165, 250, 0.20) 0%, transparent 68%);
	mix-blend-mode: screen;
	pointer-events: none;
	transform: translateZ(0);
	will-change: transform;
}

.aura-layer-2--mobile { filter: blur(105px); }
.aura-layer-2--desktop { filter: blur(151px); }

.aura-grain {
	position: absolute;
	inset: 0;
	mix-blend-mode: overlay;
	opacity: 0.85;
	pointer-events: none;
}

.aura-grain svg { display: block; width: 100%; height: 100%; }
```

- [ ] **Step 3: Build the CSS bundle**

Run: `cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio" && npm run build`

- [ ] **Step 4: Commit**

```bash
cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio"
git add views/partials/aura-bg.php styles/base/global.css static/dist/theme.css
git commit -m "feat(aura): add Carbon Blue atmospheric background partial"
```

---

## Task 5: PHP namespace swap — header.php, footer.php, all lib/*.php

**Files:**
- Modify: `functions.php`
- Modify: `header.php`
- Modify: `footer.php`
- Modify: `lib/activation.php`
- Modify: `lib/class-acf.php`
- Modify: `lib/class-breadcrumbs.php`
- Modify: `lib/class-enqueue.php`
- Modify: `lib/class-menuitems.php`
- Modify: `lib/class-resources.php`
- Modify: `lib/extras.php`
- Modify: `lib/helpers.php`
- Modify: `lib/hooks.php`
- Modify: `lib/search-features.php`
- Modify: `lib/show-template.php`

**Goal:** Replace `namespace SoloFrameEvo;` with `namespace KsPortfolio;` and update internal `__NAMESPACE__ . '\\…'` references.

- [ ] **Step 1: Replace namespace declaration in each PHP file**

For each file listed above, find `namespace SoloFrameEvo;` and replace with `namespace KsPortfolio;`.

- [ ] **Step 2: Replace internal namespace references in header.php**

In `header.php`, the only reference is `namespace SoloFrameEvo;` (already handled in step 1). Verify no `__NAMESPACE__ . '\\…'` calls remain.

- [ ] **Step 3: Replace internal namespace references in lib/class-breadcrumbs.php**

In `lib/class-breadcrumbs.php`, find every `__NAMESPACE__ . '\\…'` and replace `'\\KsPortfolio\\…'` (literal). Example: `__NAMESPACE__ . '\\Breadcrumbs'` becomes `'\\KsPortfolio\\Breadcrumbs'`.

- [ ] **Step 4: Replace internal namespace references in lib/hooks.php**

In `lib/hooks.php`, find every `__NAMESPACE__ . '\\…'` and replace with `'\\KsPortfolio\\…'`.

- [ ] **Step 5: Update functions.php to include the new project helper**

In `functions.php`, after the existing `foreach ( glob( __DIR__ . '/lib/*.php' ) ... )`, add the project helper include (we will create the file in Task 6 — guard with `file_exists` for now):
```php
// Project helper (thin wrapper around the projects-portfolio plugin).
$projects_helper = __DIR__ . '/lib/class-projects.php';
if ( file_exists( $projects_helper ) ) {
    require_once $projects_helper;
}
```

- [ ] **Step 6: Run PHPCS**

Run: `cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio" && composer run lint 2>&1 | tail -40`
Expected: lists current violations (we expect namespace-related errors fixed; other violations are expected at this stage).

- [ ] **Step 7: Commit**

```bash
cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio"
git add functions.php header.php footer.php lib/
git commit -m "refactor: rename namespace SoloFrameEvo to KsPortfolio"
```

---

## Task 6: `lib/class-projects.php` — provider-agnostic helper

**Files:**
- Create: `lib/class-projects.php`

**Goal:** Wrap the `projects-portfolio` plugin's helpers so templates never branch on provider. Provide safe defaults when the plugin is missing or repo data is unavailable.

- [ ] **Step 1: Write the helper**

Create `lib/class-projects.php`:
```php
<?php
/**
 * Project helper for the ks-portfolio theme.
 *
 * Thin wrapper around the projects-portfolio plugin's provider-aware
 * helpers. Templates should call these methods instead of branching on
 * provider, so the theme stays decoupled from GitHub/Gitea implementation
 * details.
 *
 * Every accessor is safe to call whether the plugin is active or not: when
 * the underlying function is missing or repo data is unavailable, defaults
 * that match the documented data contract are returned.
 *
 * @package KsPortfolio
 */

namespace KsPortfolio;

class Projects {

	/**
	 * Whether the projects-portfolio plugin is active.
	 *
	 * @return bool
	 */
	public static function is_plugin_active(): bool {
		return function_exists( 'projects_portfolio_get_provider' );
	}

	/**
	 * Provider label for a project (e.g. 'GitHub', 'Gitea', or 'Unknown').
	 *
	 * @param int $post_id Project post ID.
	 * @return string
	 */
	public static function get_provider_label( int $post_id ): string {
		if ( ! self::is_plugin_active() ) {
			return 'Unknown';
		}
		$provider = projects_portfolio_get_provider( $post_id );
		return method_exists( $provider, 'get_label' ) ? (string) $provider->get_label() : 'Unknown';
	}

	/**
	 * Release ZIP URL (or empty string).
	 *
	 * @param int $post_id Project post ID.
	 * @return string
	 */
	public static function get_release_url( int $post_id ): string {
		if ( ! self::is_plugin_active() ) {
			return '';
		}
		return (string) projects_portfolio_get_release_url( $post_id );
	}

	/**
	 * Public repository browse URL (or empty string).
	 *
	 * @param int $post_id Project post ID.
	 * @return string
	 */
	public static function get_repo_browse_url( int $post_id ): string {
		if ( ! self::is_plugin_active() ) {
			return '';
		}
		$provider = projects_portfolio_get_provider( $post_id );
		return method_exists( $provider, 'get_repo_browse_url' ) ? (string) $provider->get_repo_browse_url() : '';
	}

	/**
	 * Normalized repository data array (or null).
	 *
	 * @param int $post_id Project post ID.
	 * @return array<string,mixed>|null
	 */
	public static function get_repo_data( int $post_id ) {
		if ( ! self::is_plugin_active() ) {
			return null;
		}
		$data = projects_portfolio_get_repo_data( $post_id );
		return is_array( $data ) ? $data : null;
	}

	/**
	 * Owner data (avatar/login/html_url) or null.
	 *
	 * @param int $post_id Project post ID.
	 * @return array<string,mixed>|null
	 */
	public static function get_owner_data( int $post_id ) {
		$data = self::get_repo_data( $post_id );
		if ( ! $data || empty( $data['owner']['login'] ) ) {
			return null;
		}
		return $data['owner'];
	}

	/**
	 * Latest release tag, or 'Unknown'.
	 *
	 * @param int $post_id Project post ID.
	 * @return string
	 */
	public static function get_latest_version( int $post_id ): string {
		if ( ! self::is_plugin_active() ) {
			return 'Unknown';
		}
		$provider = projects_portfolio_get_provider( $post_id );
		return method_exists( $provider, 'get_latest_version' ) ? (string) $provider->get_latest_version() : 'Unknown';
	}

	/**
	 * Local download endpoint URL for the project.
	 *
	 * @param int $post_id Project post ID.
	 * @return string
	 */
	public static function get_download_url( int $post_id ): string {
		return home_url( '/download/' . $post_id . '/' );
	}

	/**
	 * Render a stat-card-safe integer with optional formatting.
	 *
	 * @param mixed $value Raw repo stat value.
	 * @return string Display-safe string ('1.2k', '12,345', etc.) or '—' when empty.
	 */
	public static function format_stat( $value ): string {
		if ( null === $value || '' === $value ) {
			return '—';
		}
		$value = (int) $value;
		if ( $value >= 10000 ) {
			return number_format( $value / 1000, 1 ) . 'k';
		}
		return number_format( $value );
	}
}
```

- [ ] **Step 2: Run PHPCS**

Run: `cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio" && composer run lint 2>&1 | tail -30`
Expected: no new violations introduced by this file.

- [ ] **Step 3: Commit**

```bash
cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio"
git add lib/class-projects.php
git commit -m "feat(projects): add provider-agnostic helper for templates"
```

---

## Task 7: Reusable project-card partial

**Files:**
- Create: `views/components/project-card.php`

**Goal:** One shared card used by home and archive templates. Provider-agnostic. Empty-state safe.

- [ ] **Step 1: Write the partial**

Create `views/components/project-card.php`:
```php
<?php
/**
 * Project Card Component
 *
 * Renders one project card. Used by front-page (Recent Projects) and
 * archive-projects. Expects $post (WP_Post) in scope.
 *
 * @package KsPortfolio
 */

namespace KsPortfolio;

if ( ! isset( $post ) || ! ( $post instanceof \WP_Post ) ) {
	return;
}

$post_id        = (int) $post->ID;
$type_label     = '';
$type_terms     = get_the_terms( $post_id, 'project-type' );
if ( ! empty( $type_terms ) && ! is_wp_error( $type_terms ) ) {
	$type_label = strtoupper( $type_terms[0]->name );
}
$release_url    = Projects::get_release_url( $post_id );
$repo_url       = Projects::get_repo_browse_url( $post_id );
$repo_data      = Projects::get_repo_data( $post_id );
$language       = is_array( $repo_data ) && ! empty( $repo_data['language'] ) ? (string) $repo_data['language'] : '';
$stars          = is_array( $repo_data ) && isset( $repo_data['stargazers_count'] ) ? (int) $repo_data['stargazers_count'] : 0;
$excerpt        = has_excerpt( $post_id ) ? get_the_excerpt( $post_id ) : wp_trim_words( wp_strip_all_tags( $post->post_content ), 30, '…' );
$card_classes   = 'project-card group';
?>

<article class="<?php echo esc_attr( $card_classes ); ?>" aria-labelledby="project-<?php echo esc_attr( (string) $post_id ); ?>-title">
	<header class="project-card__head">
		<?php if ( $type_label ) : ?>
			<span class="project-card__type"><?php echo esc_html( $type_label ); ?></span>
		<?php endif; ?>
		<span class="project-card__provider" aria-hidden="true">
			<?php echo esc_html( Projects::get_provider_label( $post_id ) ); ?>
		</span>
	</header>

	<h3 id="project-<?php echo esc_attr( (string) $post_id ); ?>-title" class="project-card__title">
		<a class="project-card__title-link" href="<?php echo esc_url( get_permalink( $post_id ) ); ?>">
			<?php echo esc_html( get_the_title( $post_id ) ); ?>
		</a>
	</h3>

	<?php if ( $excerpt ) : ?>
		<p class="project-card__excerpt"><?php echo esc_html( $excerpt ); ?></p>
	<?php endif; ?>

	<footer class="project-card__foot">
		<?php if ( $language || $stars > 0 ) : ?>
			<div class="project-card__meta">
				<?php if ( $language ) : ?>
					<span class="project-card__lang"><?php echo esc_html( $language ); ?></span>
				<?php endif; ?>
				<?php if ( $stars > 0 ) : ?>
					<span class="project-card__stars" aria-label="<?php echo esc_attr( $stars . ' stars' ); ?>">★ <?php echo esc_html( (string) $stars ); ?></span>
				<?php endif; ?>
			</div>
		<?php endif; ?>

		<div class="project-card__actions">
			<?php if ( $release_url ) : ?>
				<a class="project-card__action project-card__action--primary" href="<?php echo esc_url( Projects::get_download_url( $post_id ) ); ?>" rel="nofollow">
					<?php echo esc_html__( 'Download', 'ks-portfolio' ); ?>
				</a>
			<?php endif; ?>
			<?php if ( $repo_url ) : ?>
				<a class="project-card__action project-card__action--secondary" href="<?php echo esc_url( $repo_url ); ?>" target="_blank" rel="noopener noreferrer">
					<?php echo esc_html__( 'View Repo', 'ks-portfolio' ); ?>
				</a>
			<?php endif; ?>
		</div>
	</footer>
</article>
```

- [ ] **Step 2: Add CSS for the card**

Append to `styles/components/project-card.css` (create new file) and import it from `styles/theme.css`:
```css
/* Project card — Carbon Blue surface */
.project-card {
	background: var(--color-surface-container, #1e1f25);
	border: 1px solid var(--color-outline-variant, #444651);
	border-radius: 1rem;
	padding: 1.5rem;
	display: flex;
	flex-direction: column;
	gap: 0.75rem;
	transition: border-color 200ms ease, transform 200ms ease;
}

.project-card:hover,
.project-card:focus-within {
	border-color: var(--color-primary, #b6c4ff);
}

.project-card:focus-within {
	outline: 2px solid var(--color-primary, #b6c4ff);
	outline-offset: 3px;
}

@media (prefers-reduced-motion: reduce) {
	.project-card { transition: none; }
}

.project-card__head {
	display: flex;
	align-items: center;
	justify-content: space-between;
	font-size: 0.75rem;
	letter-spacing: 0.08em;
	text-transform: uppercase;
}

.project-card__type {
	background: rgba(182, 196, 255, 0.12);
	color: var(--color-primary, #b6c4ff);
	padding: 0.25rem 0.5rem;
	border-radius: 0.25rem;
	font-weight: 600;
}

.project-card__provider { color: var(--color-on-surface-variant, #c5c5d3); }

.project-card__title {
	font-family: var(--font-sans);
	font-weight: 700;
	font-size: 1.125rem;
	line-height: 1.4;
	margin: 0;
	color: var(--color-on-surface, #e3e1e9);
}

.project-card__title-link {
	color: inherit;
	text-decoration: none;
}

.project-card__title-link:hover { text-decoration: underline; }

.project-card__excerpt {
	color: var(--color-on-surface-variant, #c5c5d3);
	font-size: 0.9375rem;
	line-height: 1.6;
	margin: 0;
	display: -webkit-box;
	-webkit-line-clamp: 4;
	-webkit-box-orient: vertical;
	overflow: hidden;
}

.project-card__foot {
	margin-top: auto;
	padding-top: 0.75rem;
	border-top: 1px solid var(--color-outline-variant, #444651);
	display: flex;
	align-items: center;
	justify-content: space-between;
	flex-wrap: wrap;
	gap: 0.75rem;
}

.project-card__meta { display: inline-flex; gap: 0.75rem; align-items: center; color: var(--color-on-surface-variant, #c5c5d3); font-size: 0.875rem; }

.project-card__actions { display: inline-flex; gap: 0.5rem; }

.project-card__action {
	display: inline-flex;
	align-items: center;
	padding: 0.5rem 0.75rem;
	border-radius: 0.5rem;
	font-weight: 600;
	font-size: 0.875rem;
	line-height: 1.1;
	text-decoration: none;
	border: 2px solid transparent;
	transition: color 200ms ease, background-color 200ms ease, border-color 200ms ease;
}

.project-card__action--primary {
	background: var(--color-primary, #b6c4ff);
	color: var(--color-on-primary, #05297a);
	border-color: var(--color-primary, #b6c4ff);
}

.project-card__action--primary:hover { background: #dce1ff; border-color: #dce1ff; }

.project-card__action--secondary {
	color: var(--color-on-surface, #e3e1e9);
	border-color: var(--color-outline, #8f909d);
}

.project-card__action--secondary:hover { border-color: var(--color-primary, #b6c4ff); color: var(--color-primary, #b6c4ff); }

.project-card__action:focus-visible {
	outline: 2px solid var(--color-primary, #b6c4ff);
	outline-offset: 3px;
}
```

- [ ] **Step 3: Register the import**

In `styles/theme.css`, after `@import "./blocks/index.css";`, add:
```css
@import "./components/project-card.css";
```

(If a `styles/components/index.css` already exists, append `@import "./project-card.css";` to it instead and import the index from theme.css.)

- [ ] **Step 4: Build the CSS bundle**

Run: `cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio" && npm run build`

- [ ] **Step 5: Commit**

```bash
cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio"
git add views/components/project-card.php styles/components/project-card.css styles/theme.css static/dist/theme.css
git commit -m "feat(projects): add reusable project-card component + styles"
```

---

## Task 8: Project stat-card partial

**Files:**
- Create: `views/components/project-stat-card.php`

**Goal:** A single stat tile (Downloads / Stars / Forks / Issues) used by the sidebar in `single-projects.php`.

- [ ] **Step 1: Write the partial**

Create `views/components/project-stat-card.php`:
```php
<?php
/**
 * Project Stat Card Component
 *
 * Renders one stat tile. Expects $label, $value, and $icon (optional SVG
 * markup) in scope. $icon MUST be aria-hidden decorative markup; the label
 * is the accessible name.
 *
 * @package KsPortfolio
 */

namespace KsPortfolio;

if ( ! isset( $label ) || ! isset( $value ) ) {
	return;
}
$stat_label = (string) $label;
$stat_value = (string) $value;
?>
<div class="stat-card" role="group" aria-label="<?php echo esc_attr( $stat_label ); ?>">
	<?php if ( isset( $icon ) && $icon ) : ?>
		<span class="stat-card__icon" aria-hidden="true"><?php echo $icon; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- icon is hardcoded decorative SVG in callers. ?></span>
	<?php endif; ?>
	<span class="stat-card__value"><?php echo esc_html( $stat_value ); ?></span>
	<span class="stat-card__label"><?php echo esc_html( $stat_label ); ?></span>
</div>
```

- [ ] **Step 2: Add CSS for the stat card**

Append to `styles/components/project-card.css` (or create `styles/components/stat-card.css` and import it):
```css
.stat-card {
	background: var(--color-surface-container, #1e1f25);
	border: 1px solid var(--color-outline-variant, #444651);
	border-radius: 1rem;
	padding: 1rem;
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: 0.25rem;
	text-align: center;
}

.stat-card__icon { color: var(--color-on-surface-variant, #c5c5d3); }

.stat-card__value {
	font-weight: 700;
	font-size: 1.5rem;
	color: var(--color-on-surface, #e3e1e9);
}

.stat-card__label {
	text-transform: uppercase;
	letter-spacing: 0.08em;
	font-size: 0.75rem;
	color: var(--color-on-surface-variant, #c5c5d3);
}
```

- [ ] **Step 3: Build the CSS bundle**

Run: `cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio" && npm run build`

- [ ] **Step 4: Commit**

```bash
cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio"
git add views/components/project-stat-card.php styles/components/project-card.css static/dist/theme.css
git commit -m "feat(projects): add stat-card partial"
```

---

## Task 9: Project meta row partial

**Files:**
- Create: `views/components/project-meta-row.php`

**Goal:** One row in the Repository Details table (Version, Last Updated, License, Language). Provider-agnostic.

- [ ] **Step 1: Write the partial**

Create `views/components/project-meta-row.php`:
```php
<?php
/**
 * Project Meta Row Component
 *
 * Renders one row of the Repository Details table on the project detail
 * page. Expects $label, $value, and $icon (optional decorative SVG) in
 * scope.
 *
 * @package KsPortfolio
 */

namespace KsPortfolio;

if ( ! isset( $label ) || ! isset( $value ) ) {
	return;
}
$row_label = (string) $label;
$row_value = (string) $value;
?>
<div class="meta-row">
	<span class="meta-row__label">
		<?php if ( isset( $icon ) && $icon ) : ?>
			<span class="meta-row__icon" aria-hidden="true"><?php echo $icon; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- icon is hardcoded decorative SVG in callers. ?></span>
		<?php endif; ?>
		<?php echo esc_html( $row_label ); ?>
	</span>
	<span class="meta-row__value"><?php echo esc_html( $row_value ); ?></span>
</div>
```

- [ ] **Step 2: Add CSS for the meta row**

Append to `styles/components/project-card.css`:
```css
.meta-row {
	display: flex;
	align-items: center;
	justify-content: space-between;
	padding: 0.75rem 0;
	border-bottom: 1px solid var(--color-outline-variant, #444651);
	font-size: 0.9375rem;
}

.meta-row:last-child { border-bottom: 0; }

.meta-row__label { color: var(--color-on-surface-variant, #c5c5d3); display: inline-flex; align-items: center; gap: 0.5rem; }
.meta-row__value { color: var(--color-on-surface, #e3e1e9); font-weight: 600; }
.meta-row__icon { color: var(--color-on-surface-variant, #c5c5d3); }
```

- [ ] **Step 3: Build the CSS bundle**

Run: `cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio" && npm run build`

- [ ] **Step 4: Commit**

```bash
cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio"
git add views/components/project-meta-row.php styles/components/project-card.css static/dist/theme.css
git commit -m "feat(projects): add project meta-row component"
```

---

## Task 10: `archive-projects.php` — project archive template

**Files:**
- Create: `archive-projects.php`

**Goal:** Render the project archive to match `notes/archive-screen.png` — hero + grid of project cards + pagination.

- [ ] **Step 1: Write the template**

Create `archive-projects.php`:
```php
<?php
/**
 * Project Archive Template
 *
 * @package KsPortfolio
 */

namespace KsPortfolio;

get_header();

$hero_intro = get_field( 'archive_intro', (int) get_queried_object_id() );
if ( ! $hero_intro ) {
	$hero_intro = __( 'A comprehensive catalog of WordPress plugins, custom themes, and open-source contributions developed for scalable enterprise environments. Focused on modern development practices, performant architectures, and secure integrations.', 'ks-portfolio' );
}
?>
<section class="archive-projects aura-bg">
	<div class="aura-bg__content">
		<div class="container mx-auto my-section">
			<header class="archive-projects__head">
				<h1 class="archive-projects__title"><?php echo esc_html__( 'Project Archive', 'ks-portfolio' ); ?></h1>
				<p class="archive-projects__intro"><?php echo esc_html( $hero_intro ); ?></p>
			</header>

			<?php if ( have_posts() ) : ?>
				<div class="archive-projects__grid grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
					<?php
					while ( have_posts() ) :
						the_post();
						get_template_part( 'views/components/project-card' );
					endwhile;
					?>
				</div>

				<div class="archive-projects__pagination">
					<?php
					the_posts_pagination(
						array(
							'mid_size'  => 2,
							'prev_text' => '&laquo; ' . esc_html__( 'Previous', 'ks-portfolio' ),
							'next_text' => esc_html__( 'Next', 'ks-portfolio' ) . ' &raquo;',
						)
					);
					?>
				</div>
			<?php else : ?>
				<div class="archive-projects__empty">
					<h2 class="archive-projects__empty-title"><?php echo esc_html__( 'Nothing here yet&hellip;', 'ks-portfolio' ); ?></h2>
					<p class="archive-projects__empty-desc"><?php echo esc_html__( 'No published projects found.', 'ks-portfolio' ); ?></p>
				</div>
			<?php endif; ?>
		</div>
	</div>
</section>

<?php
get_template_part( 'views/partials/aura-bg', null, array( 'blur' => 'desktop' ) );
get_footer();
```

- [ ] **Step 2: Add hero + grid styles**

Append to `styles/components/project-card.css`:
```css
.archive-projects { padding-top: 2rem; padding-bottom: 4rem; }

.archive-projects__head {
	padding-left: 1rem;
	border-left: 4px solid var(--color-primary, #b6c4ff);
	margin-bottom: 2rem;
}

.archive-projects__title {
	font-family: var(--font-sans);
	font-weight: 700;
	font-size: 2.25rem;
	line-height: 1.2;
	margin: 0 0 0.5rem;
	color: var(--color-on-surface, #e3e1e9);
}

.archive-projects__intro {
	color: var(--color-on-surface-variant, #c5c5d3);
	font-size: 1rem;
	line-height: 1.6;
	max-width: 70ch;
	margin: 0;
}

.archive-projects__empty {
	text-align: center;
	padding: 4rem 1rem;
	color: var(--color-on-surface-variant, #c5c5d3);
}

.archive-projects__empty-title {
	font-weight: 700;
	font-size: 1.5rem;
	margin: 0 0 0.5rem;
	color: var(--color-on-surface, #e3e1e9);
}

/* Pagination links on dark surface */
.archive-projects__pagination a,
.archive-projects__pagination span {
	background: var(--color-surface-container, #1e1f25);
	color: var(--color-on-surface, #e3e1e9);
	border: 1px solid var(--color-outline-variant, #444651);
	border-radius: 4px;
}
```

- [ ] **Step 3: Build the CSS bundle**

Run: `cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio" && npm run build`

- [ ] **Step 4: Commit**

```bash
cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio"
git add archive-projects.php styles/components/project-card.css static/dist/theme.css
git commit -m "feat(projects): add archive-projects template"
```

---

## Task 11: `taxonomy-project-type.php` — taxonomy archive

**Files:**
- Create: `taxonomy-project-type.php`

**Goal:** Same shell as archive-projects but with the taxonomy label as the title.

- [ ] **Step 1: Write the template**

Create `taxonomy-project-type.php`:
```php
<?php
/**
 * Project Type Taxonomy Archive Template
 *
 * @package KsPortfolio
 */

namespace KsPortfolio;

get_header();

$queried = get_queried_object();
$title   = $queried && ! is_wp_error( $queried ) ? $queried->name : __( 'Projects', 'ks-portfolio' );
?>
<section class="archive-projects aura-bg">
	<div class="aura-bg__content">
		<div class="container mx-auto my-section">
			<header class="archive-projects__head">
				<h1 class="archive-projects__title"><?php echo esc_html( $title ); ?></h1>
				<p class="archive-projects__intro">
					<a class="archive-projects__back-link" href="<?php echo esc_url( get_post_type_archive_link( 'projects' ) ); ?>">
						<?php echo esc_html__( '&larr; All Projects', 'ks-portfolio' ); ?>
					</a>
				</p>
			</header>

			<?php if ( have_posts() ) : ?>
				<div class="archive-projects__grid grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
					<?php
					while ( have_posts() ) :
						the_post();
						get_template_part( 'views/components/project-card' );
					endwhile;
					?>
				</div>

				<div class="archive-projects__pagination">
					<?php
					the_posts_pagination(
						array(
							'mid_size'  => 2,
							'prev_text' => '&laquo; ' . esc_html__( 'Previous', 'ks-portfolio' ),
							'next_text' => esc_html__( 'Next', 'ks-portfolio' ) . ' &raquo;',
						)
					);
					?>
				</div>
			<?php else : ?>
				<div class="archive-projects__empty">
					<h2 class="archive-projects__empty-title"><?php echo esc_html__( 'Nothing here yet&hellip;', 'ks-portfolio' ); ?></h2>
					<p class="archive-projects__empty-desc"><?php echo esc_html__( 'No published projects found in this category.', 'ks-portfolio' ); ?></p>
				</div>
			<?php endif; ?>
		</div>
	</div>
</section>

<?php
get_template_part( 'views/partials/aura-bg', null, array( 'blur' => 'desktop' ) );
get_footer();
```

- [ ] **Step 2: Add back-link style**

Append to `styles/components/project-card.css`:
```css
.archive-projects__back-link {
	color: var(--color-primary, #b6c4ff);
	text-decoration: none;
	font-weight: 600;
}
.archive-projects__back-link:hover { text-decoration: underline; }
```

- [ ] **Step 3: Build + commit**

Run: `cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio" && npm run build`

```bash
cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio"
git add taxonomy-project-type.php styles/components/project-card.css static/dist/theme.css
git commit -m "feat(projects): add taxonomy-project-type template"
```

---

## Task 12: `single-projects.php` — project detail

**Files:**
- Create: `single-projects.php`

**Goal:** Render the project detail page to match `notes/single-screen.png` — hero with provider + title + version, Download/View Repo CTAs, two-column body, owner, repository details, stat cards.

- [ ] **Step 1: Write the template**

Create `single-projects.php`:
```php
<?php
/**
 * Single Project Template
 *
 * @package KsPortfolio
 */

namespace KsPortfolio;

get_header();

while ( have_posts() ) :
	the_post();
	$post_id    = (int) get_the_ID();
	$provider   = Projects::get_provider_label( $post_id );
	$version    = Projects::get_latest_version( $post_id );
	$release    = Projects::get_release_url( $post_id );
	$repo       = Projects::get_repo_browse_url( $post_id );
	$repo_data  = Projects::get_repo_data( $post_id );
	$owner      = Projects::get_owner_data( $post_id );
	$type_terms = get_the_terms( $post_id, 'project-type' );
	$type_label = ! empty( $type_terms ) && ! is_wp_error( $type_terms ) ? strtoupper( $type_terms[0]->name ) : '';
	$excerpt    = has_excerpt( $post_id ) ? get_the_excerpt( $post_id ) : '';
	?>
	<article class="single-project aura-bg">
		<div class="aura-bg__content">
			<div class="container mx-auto my-section">

				<header class="single-project__hero">
					<p class="single-project__meta">
						<span class="single-project__provider"><?php echo esc_html( $provider ); ?></span>
						<?php if ( $type_label ) : ?>
							<span class="single-project__type"><?php echo esc_html( $type_label ); ?></span>
						<?php endif; ?>
					</p>
					<h1 class="single-project__title">
						<?php echo esc_html( get_the_title( $post_id ) ); ?>
						<?php if ( 'Unknown' !== $version ) : ?>
							<span class="single-project__version">v<?php echo esc_html( $version ); ?></span>
						<?php endif; ?>
					</h1>
					<?php if ( $excerpt ) : ?>
						<p class="single-project__excerpt"><?php echo esc_html( $excerpt ); ?></p>
					<?php endif; ?>

					<div class="single-project__actions">
						<?php if ( $release ) : ?>
							<a class="button button--primary" href="<?php echo esc_url( Projects::get_download_url( $post_id ) ); ?>" rel="nofollow">
								<?php echo esc_html__( 'Download', 'ks-portfolio' ); ?>
								<?php if ( 'Unknown' !== $version ) : ?>
									v<?php echo esc_html( $version ); ?>
								<?php endif; ?>
							</a>
						<?php endif; ?>
						<?php if ( $repo ) : ?>
							<a class="button button--outline" href="<?php echo esc_url( $repo ); ?>" target="_blank" rel="noopener noreferrer">
								<?php echo esc_html__( 'View Repo', 'ks-portfolio' ); ?>
							</a>
						<?php endif; ?>
					</div>
				</header>

				<div class="single-project__layout">
					<div class="single-project__body">
						<?php if ( has_post_thumbnail( $post_id ) ) : ?>
							<figure class="single-project__featured">
								<?php echo get_the_post_thumbnail( $post_id, 'large' ); ?>
							</figure>
						<?php endif; ?>

						<section class="single-project__content" aria-label="<?php echo esc_attr__( 'Architecture and Implementation', 'ks-portfolio' ); ?>">
							<h2><?php echo esc_html__( 'Architecture & Implementation', 'ks-portfolio' ); ?></h2>
							<?php the_content(); ?>
						</section>
					</div>

					<aside class="single-project__sidebar" aria-label="<?php echo esc_attr__( 'Repository details', 'ks-portfolio' ); ?>">
						<?php if ( $owner ) : ?>
							<section class="single-project__maintainer">
								<h3><?php echo esc_html__( 'Maintainer', 'ks-portfolio' ); ?></h3>
								<div class="single-project__owner">
									<?php if ( ! empty( $owner['avatar_url'] ) ) : ?>
										<span class="single-project__owner-avatar" style="background-image: url('<?php echo esc_url( $owner['avatar_url'] ); ?>');" aria-hidden="true"></span>
									<?php endif; ?>
									<a class="single-project__owner-link" href="<?php echo esc_url( ! empty( $owner['html_url'] ) ? $owner['html_url'] : ( 'https://github.com/' . $owner['login'] ) ); ?>" target="_blank" rel="noopener noreferrer">
										<?php echo esc_html( $owner['login'] ); ?>
									</a>
								</div>
							</section>
						<?php endif; ?>

						<section class="single-project__details">
							<h3><?php echo esc_html__( 'Repository Details', 'ks-portfolio' ); ?></h3>
							<?php
							$updated_at = is_array( $repo_data ) && ! empty( $repo_data['updated_at'] ) ? human_time_diff( strtotime( $repo_data['updated_at'] ), current_time( 'timestamp' ) ) . ' ' . __( 'ago', 'ks-portfolio' ) : '—';
							$license    = is_array( $repo_data ) && ! empty( $repo_data['license']['name'] ) ? $repo_data['license']['name'] : __( 'None', 'ks-portfolio' );
							$language   = is_array( $repo_data ) && ! empty( $repo_data['language'] ) ? $repo_data['language'] : '—';

							$rows = array(
								array( 'label' => __( 'Version', 'ks-portfolio' ),    'value' => 'Unknown' !== $version ? 'v' . $version : __( 'Unknown', 'ks-portfolio' ) ),
								array( 'label' => __( 'Last Updated', 'ks-portfolio' ), 'value' => $updated_at ),
								array( 'label' => __( 'License', 'ks-portfolio' ),   'value' => $license ),
								array( 'label' => __( 'Language', 'ks-portfolio' ),  'value' => $language ),
							);

							foreach ( $rows as $row ) :
								set_query_var( 'label', $row['label'] );
								set_query_var( 'value', $row['value'] );
								get_template_part( 'views/components/project-meta-row' );
							endforeach;
							?>
						</section>

						<section class="single-project__stats" aria-label="<?php echo esc_attr__( 'Repository statistics', 'ks-portfolio' ); ?>">
							<?php
							$stats = array(
								array( 'label' => __( 'Downloads', 'ks-portfolio' ), 'value' => Projects::format_stat( get_post_meta( $post_id, '_projects_portfolio_download_count', true ) ) ),
								array( 'label' => __( 'Stars', 'ks-portfolio' ),      'value' => Projects::format_stat( is_array( $repo_data ) ? ( $repo_data['stargazers_count'] ?? 0 ) : 0 ) ),
								array( 'label' => __( 'Forks', 'ks-portfolio' ),      'value' => Projects::format_stat( is_array( $repo_data ) ? ( $repo_data['forks_count'] ?? 0 ) : 0 ) ),
								array( 'label' => __( 'Issues', 'ks-portfolio' ),     'value' => Projects::format_stat( is_array( $repo_data ) ? ( $repo_data['open_issues_count'] ?? 0 ) : 0 ) ),
							);

							foreach ( $stats as $stat ) :
								set_query_var( 'label', $stat['label'] );
								set_query_var( 'value', $stat['value'] );
								get_template_part( 'views/components/project-stat-card' );
							endforeach;
							?>
						</section>
					</aside>
				</div>
			</div>
		</div>
	</article>
	<?php
endwhile;

get_template_part( 'views/partials/aura-bg', null, array( 'blur' => 'desktop' ) );
get_footer();
```

- [ ] **Step 2: Add single-project CSS**

Append to `styles/components/project-card.css`:
```css
.single-project__hero {
	background: var(--color-surface-container-low, #1a1b21);
	border: 1px solid var(--color-outline-variant, #444651);
	border-radius: 1rem;
	padding: 2rem;
	margin-bottom: 2rem;
}

.single-project__meta {
	display: flex;
	align-items: center;
	gap: 0.75rem;
	margin: 0 0 0.5rem;
	color: var(--color-on-surface-variant, #c5c5d3);
	font-size: 0.875rem;
	text-transform: uppercase;
	letter-spacing: 0.08em;
}

.single-project__title {
	font-family: var(--font-sans);
	font-weight: 700;
	font-size: 2.25rem;
	line-height: 1.2;
	color: var(--color-on-surface, #e3e1e9);
	margin: 0 0 0.75rem;
}

.single-project__version { color: var(--color-on-surface-variant, #c5c5d3); font-weight: 400; font-size: 1.5rem; margin-left: 0.5rem; }

.single-project__excerpt {
	color: var(--color-on-surface-variant, #c5c5d3);
	font-size: 1rem;
	line-height: 1.6;
	max-width: 70ch;
	margin: 0 0 1.5rem;
}

.single-project__actions { display: flex; flex-wrap: wrap; gap: 0.75rem; }

.single-project__layout {
	display: grid;
	grid-template-columns: 1fr;
	gap: 2rem;
}

@media (min-width: 1024px) {
	.single-project__layout {
		grid-template-columns: 2fr 1fr;
		gap: 4rem;
	}
}

.single-project__body h2,
.single-project__sidebar h3 { color: var(--color-on-surface, #e3e1e9); font-family: var(--font-sans); font-weight: 700; }

.single-project__maintainer,
.single-project__details,
.single-project__stats {
	background: var(--color-surface-container, #1e1f25);
	border: 1px solid var(--color-outline-variant, #444651);
	border-radius: 1rem;
	padding: 1.25rem;
	margin-bottom: 1rem;
}

.single-project__owner { display: flex; align-items: center; gap: 0.75rem; }
.single-project__owner-avatar {
	width: 40px;
	height: 40px;
	background-size: 40px 40px;
	background-repeat: no-repeat;
	background-position: center;
	border-radius: 0.5rem;
	background-color: var(--color-surface-container-high, #292a2f);
}

.single-project__owner-link { color: var(--color-on-surface, #e3e1e9); font-weight: 600; text-decoration: none; }
.single-project__owner-link:hover { color: var(--color-primary, #b6c4ff); }

.single-project__stats {
	display: grid;
	grid-template-columns: repeat(2, 1fr);
	gap: 0.75rem;
}
```

- [ ] **Step 3: Build + commit**

Run: `cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio" && npm run build`

```bash
cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio"
git add single-projects.php styles/components/project-card.css static/dist/theme.css
git commit -m "feat(projects): add single-projects detail template"
```

---

## Task 13: `front-page.php` — full home template

**Files:**
- Modify: `front-page.php`

**Goal:** Replace the 8-line stub with a home page matching `notes/home-screen.png`: Carbon Blue hero, Recent Projects grid, footer.

- [ ] **Step 1: Write the template**

Replace `front-page.php` with:
```php
<?php
/**
 * Front Page Template — Portfolio 2026 home
 *
 * @package KsPortfolio
 */

namespace KsPortfolio;

get_header();

$hero_heading = get_field( 'homepage_hero_heading' );
$hero_intro   = get_field( 'homepage_hero_intro' );
$hero_cta_1   = get_field( 'homepage_hero_cta_1' ); // array( url, title, target )
$hero_cta_2   = get_field( 'homepage_hero_cta_2' );
?>
<section class="home-hero aura-bg">
	<div class="aura-bg__content">
		<div class="container mx-auto my-section">
			<div class="home-hero__inner">
				<?php if ( $hero_heading ) : ?>
					<h1 class="home-hero__heading"><?php echo esc_html( $hero_heading ); ?></h1>
				<?php endif; ?>
				<?php if ( $hero_intro ) : ?>
					<p class="home-hero__intro"><?php echo esc_html( $hero_intro ); ?></p>
				<?php endif; ?>

				<div class="home-hero__actions">
					<?php if ( ! empty( $hero_cta_1['url'] ) && ! empty( $hero_cta_1['title'] ) ) : ?>
						<a class="button button--primary" href="<?php echo esc_url( $hero_cta_1['url'] ); ?>"<?php echo ! empty( $hero_cta_1['target'] ) ? ' target="_blank" rel="noopener noreferrer"' : ''; ?>>
							<?php echo esc_html( $hero_cta_1['title'] ); ?>
						</a>
					<?php endif; ?>
					<?php if ( ! empty( $hero_cta_2['url'] ) && ! empty( $hero_cta_2['title'] ) ) : ?>
						<a class="button button--outline" href="<?php echo esc_url( $hero_cta_2['url'] ); ?>"<?php echo ! empty( $hero_cta_2['target'] ) ? ' target="_blank" rel="noopener noreferrer"' : ''; ?>>
							<?php echo esc_html( $hero_cta_2['title'] ); ?>
						</a>
					<?php endif; ?>
				</div>
			</div>
		</div>
	</div>
</section>

<section class="home-recent container mx-auto my-section">
	<header class="home-recent__head">
		<h2 class="home-recent__title"><?php echo esc_html__( 'Recent Projects', 'ks-portfolio' ); ?></h2>
		<a class="home-recent__view-all" href="<?php echo esc_url( get_post_type_archive_link( 'projects' ) ); ?>">
			<?php echo esc_html__( 'View All', 'ks-portfolio' ); ?> &rarr;
		</a>
	</header>
	<hr class="home-recent__divider" />

	<?php
	$recent = new \WP_Query(
		array(
			'post_type'      => 'projects',
			'posts_per_page' => 3,
			'no_found_rows'  => true,
		)
	);

	if ( $recent->have_posts() ) : ?>
		<div class="home-recent__grid grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
			<?php
			while ( $recent->have_posts() ) :
				$recent->the_post();
				get_template_part( 'views/components/project-card' );
			endwhile;
			wp_reset_postdata();
			?>
		</div>
	<?php else : ?>
		<div class="home-recent__empty">
			<h3><?php echo esc_html__( 'Nothing here yet&hellip;', 'ks-portfolio' ); ?></h3>
			<p><?php echo esc_html__( 'No published projects found.', 'ks-portfolio' ); ?></p>
		</div>
	<?php endif; ?>
</section>

<?php
get_template_part( 'views/partials/aura-bg', null, array( 'blur' => 'desktop' ) );
get_footer();
```

- [ ] **Step 2: Add home-hero + home-recent CSS**

Append to `styles/components/project-card.css`:
```css
.home-hero { min-height: 60vh; display: flex; align-items: center; padding: 4rem 0; }
.home-hero__inner { max-width: 50rem; }
.home-hero__heading {
	font-family: var(--font-sans);
	font-weight: 700;
	font-size: clamp(2rem, 4vw, 3.5rem);
	line-height: 1.1;
	color: var(--color-on-surface, #e3e1e9);
	margin: 0 0 1rem;
}
.home-hero__intro {
	color: var(--color-on-surface-variant, #c5c5d3);
	font-size: clamp(1rem, 1.5vw, 1.25rem);
	line-height: 1.6;
	max-width: 50ch;
	margin: 0 0 1.5rem;
}
.home-hero__actions { display: flex; flex-wrap: wrap; gap: 0.75rem; }

.home-recent__head {
	display: flex;
	align-items: baseline;
	justify-content: space-between;
	margin-bottom: 0.5rem;
}
.home-recent__title {
	font-family: var(--font-sans);
	font-weight: 700;
	font-size: 1.75rem;
	color: var(--color-on-surface, #e3e1e9);
	margin: 0;
}
.home-recent__view-all {
	color: var(--color-primary, #b6c4ff);
	font-weight: 600;
	text-decoration: none;
}
.home-recent__view-all:hover { text-decoration: underline; }

.home-recent__divider {
	border: 0;
	border-top: 1px solid var(--color-outline-variant, #444651);
	margin: 0 0 1.5rem;
}

.home-recent__empty { text-align: center; padding: 4rem 1rem; color: var(--color-on-surface-variant, #c5c5d3); }
```

- [ ] **Step 3: Build + commit**

Run: `cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio" && npm run build`

```bash
cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio"
git add front-page.php styles/components/project-card.css static/dist/theme.css
git commit -m "feat(home): rebuild front-page template to match Carbon Blue mockup"
```

---

## Task 14: Header — three-region responsive layout + dark nav

**Files:**
- Modify: `header.php`
- Modify: `styles/navigation/nav-aux.css`
- Modify: `styles/navigation/nav-main-default.css`

**Goal:** Switch from `grid-cols-[83px_auto]` to a three-region header (logo | nav | aux button), recolor nav to Carbon Blue.

- [ ] **Step 1: Update header.php markup**

Replace the existing `.header__nav-main` block in `header.php` with:
```php
<div class="header__nav-main container py-4 items-center grid gap-x-8 gap-y-0 grid-cols-[auto_1fr_auto] grid-rows-[1fr] justify-between">
	<a href="<?php echo esc_url( home_url( '/' ) ); ?>" class="site-header__logo block">
		<img class="site-header__logo-img" src="<?php echo esc_url( $headerLogo ); ?>" alt="<?php echo esc_attr( get_bloginfo( 'name' ) ); ?> logo" />
	</a>

	<?php get_template_part( 'views/components/nav-main' ); ?>

	<div class="site-header__aux">
		<a class="button button--outline" href="<?php echo esc_url( home_url( '/cv/' ) ); ?>">
			<?php echo esc_html__( 'Download CV', 'ks-portfolio' ); ?>
		</a>
	</div>
</div>
```

- [ ] **Step 2: Recolor nav for dark surface**

Append to `styles/navigation/nav-main-default.css`:
```css
.nav-main a,
.nav-aux a {
	color: var(--color-on-surface, #e3e1e9);
}

.nav-main a:hover,
.nav-aux a:hover {
	color: var(--color-primary, #b6c4ff);
}
```

- [ ] **Step 3: Build + commit**

Run: `cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio" && npm run build`

```bash
cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio"
git add header.php styles/navigation/ static/dist/theme.css
git commit -m "feat(header): three-region layout + dark nav colors"
```

---

## Task 15: Footer — Carbon Blue surface

**Files:**
- Modify: `footer.php`

**Goal:** Recolor footer links, swap "Footer Area 1/2/3" headings for widget titles, and update copyright to match Carbon Blue palette.

- [ ] **Step 1: Update footer.php**

Find the existing `bg-gray-800 text-white` block on `<footer>` and replace with:
```php
<footer role="contentinfo" class="site-footer">
	<div class="container mx-auto py-12">
		<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
			<div class="site-footer__brand max-w-[40ch]">
				<h2 class="site-footer__title">
					<a href="<?php echo esc_url( home_url( '/' ) ); ?>">
						<?php if ( $footerLogo ) { ?>
							<img src="<?php echo esc_url( $footerLogo ); ?>" alt="<?php echo esc_attr( get_bloginfo( 'name' ) ); ?>" />
						<?php } else {
							echo esc_html( get_bloginfo( 'name' ) );
						} ?>
					</a>
				</h2>
				<?php if ( $footerDesc ) : ?>
					<p class="site-footer__desc"><?php echo wp_kses_post( $footerDesc ); ?></p>
				<?php endif; ?>
			</div>

			<?php for ( $i = 1; $i <= 3; $i++ ) :
				$area_id = 'footer-' . $i;
				if ( ! is_active_sidebar( $area_id ) ) {
					continue;
				}
				?>
				<div class="site-footer__area">
					<?php dynamic_sidebar( $area_id ); ?>
				</div>
			<?php endfor; ?>

			<?php if ( $footerNav ) : ?>
				<nav class="site-footer__nav" aria-label="<?php echo esc_attr__( 'Footer navigation', 'ks-portfolio' ); ?>">
					<h3><?php echo esc_html__( 'Navigation', 'ks-portfolio' ); ?></h3>
					<ul>
						<?php foreach ( $footerNav as $item ) : ?>
							<li>
								<a href="<?php echo esc_url( $item->url ); ?>">
									<?php echo esc_html( $item->title ); ?>
								</a>
							</li>
						<?php endforeach; ?>
					</ul>
				</nav>
			<?php endif; ?>
		</div>
	</div>

	<div class="site-footer__copyright">
		<?php echo wp_kses_post( $copyright ); ?>
	</div>
</footer>
```

- [ ] **Step 2: Add footer CSS**

Append to `styles/navigation/nav-footer.css` (or create `styles/components/footer.css` and import from `styles/theme.css`):
```css
.site-footer {
	background: var(--color-surface, #121318);
	color: var(--color-on-surface, #e3e1e9);
	border-top: 1px solid var(--color-outline-variant, #444651);
}
.site-footer__title a { color: var(--color-on-surface, #e3e1e9); text-decoration: none; }
.site-footer__desc { color: var(--color-on-surface-variant, #c5c5d3); }
.site-footer__area h3,
.site-footer__nav h3 {
	color: var(--color-on-surface, #e3e1e9);
	font-family: var(--font-sans);
	font-weight: 700;
	margin: 0 0 0.75rem;
	padding-bottom: 0.5rem;
	border-bottom: 1px solid var(--color-outline-variant, #444651);
}
.site-footer a { color: var(--color-primary, #b6c4ff); }
.site-footer a:hover { text-decoration: underline; }
.site-footer__copyright {
	background: var(--color-surface-container-lowest, #0d0e13);
	color: var(--color-on-surface-variant, #c5c5d3);
	text-align: center;
	padding: 0.75rem;
	font-size: 0.875rem;
}
```

- [ ] **Step 3: Build + commit**

Run: `cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio" && npm run build`

```bash
cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio"
git add footer.php styles/ static/dist/theme.css
git commit -m "feat(footer): Carbon Blue surface + cleaner widget rendering"
```

---

## Task 16: Other templates — namespace swap + dark surface + remove picsum

**Files:**
- Modify: `index.php`
- Modify: `single.php`
- Modify: `page.php`
- Modify: `search.php`
- Modify: `404.php`
- Modify: `views/partials/page-hero.php`

**Goal:** Apply namespace swap, remove `picsum.photos` fallbacks, ensure empty-state copy is the documented strings.

- [ ] **Step 1: Namespace swap + picsum removal in index.php**

In `index.php`:
1. Replace `namespace SoloFrameEvo;` with `namespace KsPortfolio;`.
2. Find the line:
   ```php
   $postImg = $featImg ? $featImg : 'https://picsum.photos/600/400?random=' . get_the_ID();
   ```
   Replace with:
   ```php
   $postImg = $featImg;
   ```
3. Find the `<img>` tag and wrap the whole `<figure>` in:
   ```php
   <?php if ( has_post_thumbnail() ) : ?>
   	<figure>...existing markup...</figure>
   <?php endif; ?>
   ```
   (Don't render an empty figure.)

- [ ] **Step 2: Namespace swap + picsum removal in search.php**

Same edits as Step 1.

- [ ] **Step 3: Namespace swap in single.php, page.php, 404.php, page-hero.php**

In each file, replace `namespace SoloFrameEvo;` with `namespace KsPortfolio;`. In `404.php`, also update:
```php
<?php echo esc_html__( 'Search for:', 'label' ); ?>
```
to:
```php
<?php echo esc_html__( 'Search for:', 'ks-portfolio' ); ?>
```
(And the same for any other `'label'` text-domain references in `404.php`.)

- [ ] **Step 4: Dark surface for page-hero partial**

In `views/partials/page-hero.php`:
1. Namespace swap.
2. Replace the existing `bg-cover bg-no-repeat mb-12 py-12 lg:py-16 bg-dark text-light overflow-hidden` with:
```php
<section class="page-hero py-12 lg:py-16">
```
3. Replace the `<h1>` color classes `text-light` with `page-hero__title` (let CSS handle it).

- [ ] **Step 5: Add page-hero CSS**

Append to `styles/components/page-hero.css` (create new file and import from `styles/theme.css`):
```css
.page-hero {
	background: var(--color-surface-container-low, #1a1b21);
	color: var(--color-on-surface, #e3e1e9);
	border-bottom: 1px solid var(--color-outline-variant, #444651);
	margin-bottom: 2rem;
}
.page-hero__title { color: var(--color-on-surface, #e3e1e9); font-family: var(--font-sans); font-weight: 700; }
.page-hero__intro { color: var(--color-on-surface-variant, #c5c5d3); }
```

- [ ] **Step 6: Build + commit**

Run: `cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio" && npm run build`

```bash
cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio"
git add index.php single.php page.php search.php 404.php views/partials/page-hero.php styles/ static/dist/theme.css
git commit -m "feat(templates): namespace swap + dark surface + remove picsum fallbacks"
```

---

## Task 17: Button styles for dark surface

**Files:**
- Modify: `styles/blocks/buttons.css`

**Goal:** Verify button classes render with WCAG-AA contrast against `#100e0b`. Add a `button--outline` variant if missing (used by the home + archive + single-project templates).

- [ ] **Step 1: Inspect existing button styles**

Read `styles/blocks/buttons.css`. Confirm it defines (or can be extended to define) the classes: `button`, `button--primary`, `button--outline`, `button--secondary`, `button--white`, `button--black`. Note what's present.

- [ ] **Step 2: Append dark-surface button styles**

Append to `styles/blocks/buttons.css`:
```css
.button {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	font-weight: 600;
	font-size: 1rem;
	line-height: 1.1;
	padding: 0.5rem 2rem;
	border-radius: 0.5rem;
	border: 3px solid transparent;
	text-decoration: none;
	transition: color 200ms ease, background-color 200ms ease, border-color 200ms ease;
}

.button:active { transform: scale(0.99); }
@media (prefers-reduced-motion: reduce) { .button:active { transform: none; } }

.button:focus-visible { outline: 2px solid var(--color-primary, #b6c4ff); outline-offset: 3px; }

.button--primary {
	background: var(--color-primary, #b6c4ff);
	color: var(--color-on-primary, #05297a);
	border-color: var(--color-primary, #b6c4ff);
}
.button--primary:hover { background: #dce1ff; border-color: #dce1ff; }

.button--outline {
	background: transparent;
	color: var(--color-on-surface, #e3e1e9);
	border-color: var(--color-outline, #8f909d);
}
.button--outline:hover { border-color: var(--color-primary, #b6c4ff); color: var(--color-primary, #b6c4ff); }

.button--secondary {
	background: var(--color-surface-container-high, #292a2f);
	color: var(--color-on-surface, #e3e1e9);
	border-color: var(--color-outline-variant, #444651);
}
```

- [ ] **Step 3: Build + commit**

Run: `cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio" && npm run build`

```bash
cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio"
git add styles/blocks/buttons.css static/dist/theme.css
git commit -m "feat(buttons): dark-surface button variants with WCAG-AA contrast"
```

---

## Task 18: Playwright + axe specs

**Files:**
- Create: `tests/home.spec.js`
- Create: `tests/archive.spec.js`
- Create: `tests/single.spec.js`

**Goal:** Browser + accessibility tests for the new Carbon Blue surfaces.

- [ ] **Step 1: Write tests/home.spec.js**

Create `tests/home.spec.js`:
```js
const { test, expect } = require('@playwright/test');
const AxeBuilder = require('@axe-core/playwright').default;

test.use({ viewport: { width: 1920, height: 1080 } });

test.describe('home', () => {
  test('home renders Carbon Blue hero + recent projects + zero axe violations', async ({ page }, testInfo) => {
    await page.goto('/');
    await page.screenshot({ path: 'test-results/home.png', fullPage: true });

    // Hero region must be present.
    await expect(page.locator('.home-hero')).toBeVisible();

    // Either the grid OR the empty state must be present.
    const grid = page.locator('.home-recent__grid');
    const empty = page.locator('.home-recent__empty');
    await expect(grid.or(empty)).toBeVisible();

    const results = await new AxeBuilder({ page })
      .withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'wcag22a', 'wcag22aa'])
      .analyze();
    await testInfo.attach('axe', {
      body: JSON.stringify(results, null, 2),
      contentType: 'application/json',
    });
    expect(results.violations).toEqual([]);
  });
});
```

- [ ] **Step 2: Write tests/archive.spec.js**

Create `tests/archive.spec.js`:
```js
const { test, expect } = require('@playwright/test');
const AxeBuilder = require('@axe-core/playwright').default;

test.use({ viewport: { width: 1920, height: 1080 } });

test.describe('archive-projects', () => {
  test('project archive renders grid or empty + zero axe violations', async ({ page }, testInfo) => {
    await page.goto('/projects/');
    await page.screenshot({ path: 'test-results/archive.png', fullPage: true });

    await expect(page.locator('.archive-projects__head h1')).toBeVisible();
    const grid = page.locator('.archive-projects__grid');
    const empty = page.locator('.archive-projects__empty');
    await expect(grid.or(empty)).toBeVisible();

    const results = await new AxeBuilder({ page })
      .withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'wcag22a', 'wcag22aa'])
      .analyze();
    await testInfo.attach('axe', {
      body: JSON.stringify(results, null, 2),
      contentType: 'application/json',
    });
    expect(results.violations).toEqual([]);
  });
});
```

- [ ] **Step 3: Write tests/single.spec.js**

Create `tests/single.spec.js`:
```js
const { test, expect } = require('@playwright/test');
const AxeBuilder = require('@axe-core/playwright').default;

test.use({ viewport: { width: 1920, height: 1080 } });

test.describe('single-project', () => {
  test('project detail renders hero, sidebar, no axe violations', async ({ page }, testInfo) => {
    // Pick the first project; skip the test gracefully if none exist.
    const response = await page.goto('/projects/');
    expect(response && response.status()).toBeLessThan(400);

    const firstLink = page.locator('.archive-projects__grid a.project-card__title-link').first();
    if (await firstLink.count() === 0) {
      test.skip(true, 'No projects to test detail page');
      return;
    }

    await Promise.all([
      page.waitForLoadState('networkidle'),
      firstLink.click(),
    ]);
    await page.screenshot({ path: 'test-results/single.png', fullPage: true });

    await expect(page.locator('.single-project__title')).toBeVisible();
    await expect(page.locator('.single-project__sidebar')).toBeVisible();

    const results = await new AxeBuilder({ page })
      .withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'wcag22a', 'wcag22aa'])
      .analyze();
    await testInfo.attach('axe', {
      body: JSON.stringify(results, null, 2),
      contentType: 'application/json',
    });
    expect(results.violations).toEqual([]);
  });
});
```

- [ ] **Step 4: Run all Playwright tests**

Run: `cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio" && npx playwright test --reporter=list`
Expected: all four specs (`site-a11y`, `home`, `archive`, `single`) pass. If any fail, debug the specific violation, fix in the template/CSS, re-run.

- [ ] **Step 5: Commit**

```bash
cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio"
git add tests/home.spec.js tests/archive.spec.js tests/single.spec.js
git commit -m "test: add Playwright + axe specs for home, archive, single"
```

---

## Task 19: PHPCS clean run

**Files:**
- All PHP files in theme

**Goal:** `composer run lint` exits 0.

- [ ] **Step 1: Run PHPCS**

Run: `cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio" && composer run lint 2>&1 | tail -80`

- [ ] **Step 2: Fix violations inline**

For each reported violation, fix the file. Common ones expected: missing `wp_kses_post` on `the_content()` calls already correctly used; trailing whitespace; array alignment; `WordPress.Security.EscapeOutput` — fix by adding the right `esc_*` function.

- [ ] **Step 3: Re-run until clean**

Repeat step 1 until the command exits with no violations (or only the documented exclusions in `.phpcs.xml`).

- [ ] **Step 4: Commit any fixes**

```bash
cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio"
git add -A
git commit -m "style: PHPCS clean across theme PHP" --allow-empty
```

---

## Task 20: Static security audit

**Files:**
- Audit only (no file changes unless finding requires them)

**Goal:** Identify any Critical/High security issues. Produce a written report that becomes part of the final testing report.

- [ ] **Step 1: Run PHPCS WordPress.Security ruleset**

Run: `cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio" && ./vendor/bin/phpcs --standard=.phpcs.xml --sniffs=WordPress.Security.EscapeOutput,WordPress.Security.NonceVerification,WordPress.Security.ValidatedSanitizedInput . 2>&1 | tail -80`
Expected: zero findings (since we already passed PHPCS in Task 19). If there are findings, fix inline.

- [ ] **Step 2: Grep for unsanitized superglobal access**

Run (PowerShell):
```powershell
Select-String -Path "C:\Users\ksolo\Herd\portfolio\wp-content\themes\ks-portfolio" -Include "*.php" -Recurse -Pattern '\$_(GET|POST|REQUEST)\[' | ForEach-Object { $_.Path + ":" + $_.LineNumber + " " + $_.Line }
```
Expected: empty result (theme has no direct superglobal access). Any finding is a security issue — fix by switching to `filter_input` + `sanitize_*` or to WordPress API equivalents.

- [ ] **Step 3: Audit new templates manually**

For each new template (`front-page.php`, `archive-projects.php`, `single-projects.php`, `taxonomy-project-type.php`) and each new partial (`views/partials/aura-bg.php`, `views/components/project-card.php`, `views/components/project-stat-card.php`, `views/components/project-meta-row.php`), check:
- Every dynamic output uses the right `esc_*` for context.
- Every state-changing form has `wp_nonce_field` (none in this theme — none expected).
- External URLs use `esc_url` and add `rel="noopener noreferrer"` when `target="_blank"`.
- `set_query_var` / `get_template_part` data is escaped in the consumer partials.

- [ ] **Step 4: Write the security section of the final report**

Append to `docs/superpowers/specs/2026-08-16-portfolio-2026-testing-report.md` (we'll create the file in Task 22):
```
## Security audit

- PHPCS WordPress.Security: <PASS / count of findings>
- Direct superglobal access (grep): <PASS / count of findings>
- Manual template audit: <PASS / list of findings with severity>

Severity summary: Critical: 0, High: 0, Medium: 0, Low: 0, Info: 0.
```

- [ ] **Step 5: Commit audit notes (if any fixes were made)**

```bash
cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio"
git add -A
git commit -m "security: remediate audit findings" --allow-empty
```

---

## Task 21: Replace `screenshot.png` with Carbon Blue capture

**Files:**
- Modify: `screenshot.png`

**Goal:** Drop the Basic WP screenshot and replace it with a Carbon Blue capture.

- [ ] **Step 1: Capture the home page screenshot**

Run the existing Playwright test (it already saves `test-results/home.png`) or run explicitly:
```bash
cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio"
npx playwright test tests/home.spec.js --reporter=list
```

- [ ] **Step 2: Copy the capture to theme root**

Run (PowerShell):
```powershell
Copy-Item "C:\Users\ksolo\Herd\portfolio\wp-content\themes\ks-portfolio\test-results\home.png" "C:\Users\ksolo\Herd\portfolio\wp-content\themes\ks-portfolio\screenshot.png" -Force
```

- [ ] **Step 3: Commit**

```bash
cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio"
git rm screenshot.png
git add screenshot.png
git commit -m "chore: replace Basic WP screenshot with Carbon Blue home capture"
```

---

## Task 22: Final testing report

**Files:**
- Create: `docs/superpowers/specs/2026-08-16-portfolio-2026-testing-report.md`

**Goal:** Ship a complete report covering all four test categories (browser, functional, unit, pen-test), issues encountered, and recommendations.

- [ ] **Step 1: Write the report**

Create `docs/superpowers/specs/2026-08-16-portfolio-2026-testing-report.md` with the following sections (replace `<…>` with actual numbers):

```markdown
# Portfolio 2026 — Testing Report

**Date:** 2026-08-16
**Theme:** ks-portfolio (Portfolio 2026)
**Local base URL:** http://portfolio.test

## 1. Scope

Browser (Chromium + axe-core), functional (Playwright), unit (PHPCS + WPCS), static security audit. Firefox/WebKit/mobile and OWASP ZAP were out of scope per user choice.

## 2. Browser + functional (Playwright)

Specs run:
- `tests/site-a11y.spec.js` — homepage, blog index, 404
- `tests/home.spec.js` — Carbon Blue hero + recent projects
- `tests/archive.spec.js` — project archive grid or empty state
- `tests/single.spec.js` — project detail hero + sidebar

Result: <PASS / FAIL — count of passing specs>
Screenshots: `test-results/home.png`, `test-results/archive.png`, `test-results/single.png`.

## 3. Unit / lint (PHPCS)

Command: `composer run lint`
Result: <PASS / FAIL — count of remaining violations>

## 4. Static security audit

<see "Security audit" block from Task 20>

## 5. Issues encountered

<list — severity-ranked>

## 6. Recommendations for future improvement

<list — non-blocking>
```

- [ ] **Step 2: Commit**

```bash
cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio"
git add docs/superpowers/specs/2026-08-16-portfolio-2026-testing-report.md
git commit -m "docs: add Portfolio 2026 testing report"
```

---

## Self-Review

**Spec coverage:**
- §3 Identity/naming → Tasks 5, 21, 22. ✓
- §4.1 Body surface → Task 2. ✓
- §4.2 Carbon Blue partial → Task 4. ✓
- §4.3 Tokens → Task 3. ✓
- §5.1 front-page → Task 13. ✓
- §5.2 archive-projects → Task 10. ✓
- §5.3 single-projects → Task 12. ✓
- §5.4 taxonomy-project-type → Task 11. ✓
- §5.5 Other templates → Task 16. ✓
- §6.1 Buttons → Task 17. ✓
- §6.2 Project card → Task 7. ✓
- §6.3 Stat card → Task 8. ✓
- §6.4 Navigation → Task 14. ✓
- §6.5 Header layout → Task 14. ✓
- §7 Accessibility → Tasks 17, 18. ✓
- §8 Security → Tasks 19, 20. ✓
- §9 Testing → Tasks 1, 18, 19, 20, 22. ✓
- §10 File-level → all tasks together. ✓
- §12 Definition of done → Task 22. ✓

**Placeholder scan:** no TBDs / "TODO" / "implement later".

**Type consistency:**
- `Projects::get_*` signatures used in Tasks 7, 8, 9, 10, 11, 12, 13 match the definitions in Task 6.
- `Projects::format_stat` returns `'—'` when empty — used by stat-card in Task 12. ✓
- Card partial names (`project-card`, `project-stat-card`, `project-meta-row`) consistent across Tasks 7–12. ✓

Plan complete and saved to `docs/superpowers/plans/2026-08-16-portfolio-2026-implementation.md`. Two execution options:

**1. Subagent-Driven (recommended)** - I dispatch a fresh subagent per task, review between tasks, fast iteration.

**2. Inline Execution** - Execute tasks in this session using executing-plans, batch execution with checkpoints.

Which approach?
