diff --git a/docs/superpowers/plans/2026-08-16-portfolio-2026-implementation.md b/docs/superpowers/plans/2026-08-16-portfolio-2026-implementation.md
new file mode 100644
index 0000000..092db9f
--- /dev/null
+++ b/docs/superpowers/plans/2026-08-16-portfolio-2026-implementation.md
@@ -0,0 +1,2372 @@
+# 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';
+?>
+
+
+
+
+
+
+
+
+
+
+
+