# 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 `
`. 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
```
- [ ] **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
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|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|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
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';
?>