v3 Responsive — Foundation 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: Build the shared responsive foundation that makes all 28 v3 product/section pages baseline-responsive at once, and extend the verification harness to sweep all 30 real pages.

Architecture: Every v3 page loads v3.css + product.css; the homepage additionally loads responsive.css. We make product.css the universal responsive layer: relocate the global container/section padding and the mobile-nav-drawer @media activation out of homepage-only responsive.css into product.css, and add a phone-tier (480px) block plus shared-footer responsive rules. After this, every page gets the working hamburger drawer and baseline mobile layout from product.css; responsive.css is left holding only homepage-section-specific rules. Per-page bespoke breakage is not in scope here — it is triaged in the follow-up plans.

Tech Stack: Static HTML/CSS, vanilla JS. Playwright (Chromium) for verification.


Scope

This is Plan 1 of sub-project A (spec: docs/superpowers/specs/2026-05-14-v3-responsive-all-pages-design.md). It delivers Stage 1 — the foundation. Follow-up plans, written after this ships and the all-pages harness reveals real per-page breakage:

Prerequisites


File Structure

File Status Responsibility
www/v3/dev/screenshot.mjs rewrite Verification harness — now sweeps all 30 real pages × 5 widths, supports a comma-list slug filter.
www/v3/css/product.css modify Gains a "Responsive — site-wide" region at end of file: container/section rhythm, nav-drawer @media 960px activation, template-class phone tier, shared-footer responsive.
www/v3/css/responsive.css modify Loses the global container/section block and the nav-drawer @media 960px block (both relocate to product.css). Keeps only homepage-section-specific rules.

Task 1: Extend the verification harness to all 30 pages

Files:

// Responsive verification harness for the v3 website.
// Sweeps every real v3 page at several widths, fails on horizontal overflow,
// and writes full-page screenshots to ./shots for visual review.
// Usage: node screenshot.mjs [filter]
//   filter = comma-separated substrings matched against the page slug
//   e.g. `node screenshot.mjs home,produkt-kreditoren,roi`
import { chromium } from "playwright";
import { mkdirSync } from "node:fs";
import { fileURLToPath } from "node:url";

const BASE = process.env.BASE || "http://127.0.0.1:8123";
const WIDTHS = [320, 375, 768, 1024, 1280];
const OUT = fileURLToPath(new URL("./shots/", import.meta.url));
const FILTER = (process.argv[2] || "").split(",").filter(Boolean);

// All 30 real v3 pages (drafts + archive excluded).
const PAGES = [
  "/v3/de/",
  "/v3/en/",
  "/v3/de/produkt/abschluss.html",
  "/v3/de/produkt/debitoren.html",
  "/v3/de/produkt/dokumente.html",
  "/v3/de/produkt/fpa.html",
  "/v3/de/produkt/freigaben.html",
  "/v3/de/produkt/kreditoren.html",
  "/v3/de/produkt/spesen.html",
  "/v3/de/produkt/varianz.html",
  "/v3/de/produkt/vertraege.html",
  "/v3/en/produkt/abschluss.html",
  "/v3/en/produkt/debitoren.html",
  "/v3/en/produkt/dokumente.html",
  "/v3/en/produkt/fpa.html",
  "/v3/en/produkt/freigaben.html",
  "/v3/en/produkt/kreditoren.html",
  "/v3/en/produkt/spesen.html",
  "/v3/en/produkt/varianz.html",
  "/v3/en/produkt/vertraege.html",
  "/v3/de/agenten/",
  "/v3/de/implementierung/",
  "/v3/de/roi/",
  "/v3/de/steuerberater/",
  "/v3/de/trial/",
  "/v3/en/agenten/",
  "/v3/en/implementierung/",
  "/v3/en/roi/",
  "/v3/en/steuerberater/",
  "/v3/en/trial/",
];

const slug = (p) =>
  p.replace(/^\/v3\//, "").replace(/index\.html$/, "").replace(/\.html$/, "")
   .replace(/\/$/, "").replace(/\//g, "-") || "home";

mkdirSync(OUT, { recursive: true });

const pages = FILTER.length
  ? PAGES.filter((p) => FILTER.some((f) => slug(p).includes(f)))
  : PAGES;

const browser = await chromium.launch();
const failures = [];

try {
  for (const path of pages) {
    const s = slug(path);
    for (const width of WIDTHS) {
      const page = await browser.newPage({ viewport: { width, height: 900 } });
      try {
        await page.goto(BASE + path, { waitUntil: "networkidle" });
        const overflow = await page.evaluate(() => {
          const de = document.documentElement;
          return de.scrollWidth - de.clientWidth;
        });
        await page.screenshot({ path: `${OUT}${s}-${width}.png`, fullPage: true });
        if (overflow > 1) {
          failures.push(`${s} @ ${width}px (+${overflow}px)`);
          console.log(`  FAIL  ${s} @ ${width}px (+${overflow}px)`);
        } else {
          console.log(`  ok    ${s} @ ${width}px`);
        }
      } finally {
        await page.close();
      }
    }
  }
} finally {
  await browser.close();
}

console.log(`\n${pages.length} pages x ${WIDTHS.length} widths = ${pages.length * WIDTHS.length} checks`);
if (failures.length) {
  console.log(`\n${failures.length} OVERFLOW FAILURES:`);
  for (const f of failures) console.log(`  ${f}`);
  process.exit(1);
} else {
  console.log("\nno horizontal overflow on any page at any width");
  process.exit(0);
}

Ensure the server is running (see Prerequisites), then run:

cd /Users/maximilianbrandstaetter/Orcha/.claude/worktrees/website-v3-mobile-responsive/www/v3/dev && node screenshot.mjs

Expected: it runs all 150 checks (30 pages × 5 widths, takes a few minutes), writes 150 PNGs to shots/, prints a per-check ok/FAIL line, and exits non-zero with an OVERFLOW FAILURES list. de-home (the already-done DE homepage) should show ok at all 5 widths; many other pages will FAIL at 320px/375px. This failure list is the baseline.

In the Step 2 output, confirm all 5 de-home @ ... lines say ok. If de-home fails, stop — something is wrong with the harness or server, not the pages.

git add www/v3/dev/screenshot.mjs
git commit -m "test(v3): extend responsive harness to sweep all 30 pages"

Task 2: Relocate global container/section rhythm into product.css

Moves the .wrap / section responsive padding out of homepage-only responsive.css and into product.css so it applies to every page, and adds section.p-section (the product-page section class).

Files:

In www/v3/css/responsive.css, delete this block entirely (the comment, both @media blocks, and the trailing blank line):

/* ───── Global: container + section rhythm ───── */
@media (max-width: 768px) {
  .wrap { padding: 0 24px; }
  section { padding: 80px 0; }
  section:first-of-type { padding-top: 56px; }
}
@media (max-width: 480px) {
  .wrap { padding: 0 16px; }
  section { padding: 56px 0; }
  section:first-of-type { padding-top: 40px; }
}

After this, responsive.css goes straight from its header comment block to the /* ───── Mobile nav drawer ───── comment.

Append at the very end of www/v3/css/product.css (after the final } of the existing @media (max-width: 960px) block):


/* ═══════════════════════════════════════════════════════════════
   Responsive — site-wide (sub-project A foundation, 2026-05-14)
   Breakpoints: 1024 tablet · 768 large phone · 480 phone · 960 nav.
   Applies to every v3 page (all load product.css). Homepage-section-
   specific responsive rules stay in responsive.css.
═══════════════════════════════════════════════════════════════ */

/* container & section rhythm */
@media (max-width: 768px) {
  .wrap { padding: 0 24px; }
  section { padding: 80px 0; }
  section:first-of-type { padding-top: 56px; }
  section.p-section { padding: 72px 0; }
}
@media (max-width: 480px) {
  .wrap { padding: 0 16px; }
  section { padding: 56px 0; }
  section:first-of-type { padding-top: 40px; }
  section.p-section { padding: 52px 0; }
}

Check braces balance in both files:

node -e "for(const f of ['www/v3/css/responsive.css','www/v3/css/product.css']){const c=require('fs').readFileSync(f,'utf8');const o=(c.match(/{/g)||[]).length,cl=(c.match(/}/g)||[]).length;console.log(f,o,cl,o===cl?'OK':'MISMATCH')}"

Both must report OK. Then run the harness on representative pages:

cd /Users/maximilianbrandstaetter/Orcha/.claude/worktrees/website-v3-mobile-responsive/www/v3/dev && node screenshot.mjs home,produkt-kreditoren,roi,agenten

Expected: de-home still ok at all widths (no regression — the rule simply moved files). Other pages' pass/fail set unchanged or improved vs the Task 1 baseline (this change only tightens padding, never adds width). Open shots/de-home-375.png — homepage padding still tight as before.

git add www/v3/css/responsive.css www/v3/css/product.css
git commit -m "feat(v3): move global container/section responsive padding into product.css"

Task 3: Relocate the mobile-nav-drawer activation into product.css

Moves the @media (max-width: 960px) drawer-activation block out of homepage-only responsive.css into product.css, so the hamburger drawer — whose markup chrome.de.js already renders on every page — actually works on all 30 pages.

Files:

In www/v3/css/responsive.css, delete this entire block (the comment through the closing }, and the trailing blank line):

/* ───── Mobile nav drawer ─────
   Base styles (.nav-drawer display:contents, .nav-toggle hidden + hamburger
   spans) live in product.css, which every v3 page loads — so the shared
   chrome.de.js header stays correct everywhere. This homepage-only block
   activates the drawer at <=960px. */
@media (max-width: 960px) {
  .nav-toggle { display: flex; }

  .nav-drawer {
    display: none;
    /* anchors to .nav (position:sticky); .nav-inner is not positioned */
    position: absolute;
    left: 0;
    right: 0;
    top: 100%;
    flex-direction: column;
    background: var(--bg);
    border-bottom: 1px solid var(--line);
    box-shadow: 0 12px 24px rgba(0, 0, 0, 0.08);
    padding: 12px 24px 20px;
    max-height: calc(100vh - 64px);
    overflow-y: auto;
  }
  .nav.is-open .nav-drawer { display: flex; }

  .nav-links {
    flex-direction: column;
    align-items: stretch;
    gap: 0;
  }
  /* override product.css @960 `.nav-links > a:not(.nav-links-home) { display:none }`:
     scope to .nav-drawer so the override reaches (0,2,1) specificity and wins
     on source order — a bare `.nav-links > a` (0,1,1) would lose to product.css. */
  .nav-drawer .nav-links > a,
  .nav-drawer .nav-links > .nav-dd > a {
    display: block;
    padding: 13px 0;
    border-bottom: 1px solid var(--line);
    font-size: 16px;
  }
  .nav-links > .nav-dd { width: 100%; }
  .nav-dd > a::after { display: none; }   /* hide the dropdown caret */

  /* Produkt dropdown flattens — panel sits inline, always visible */
  .nav-dd-panel {
    position: static;
    width: auto;
    opacity: 1;
    pointer-events: auto;
    transform: none;
    border: 0;
    box-shadow: none;
    padding: 4px 0 8px;
  }
  .nav-dd-panel::before,
  .nav-dd-panel::after { display: none; }
  .nav-dd-header { display: none; }
  .nav-dd-grid { grid-template-columns: 1fr; }
  .nav-dd-item { padding: 10px 8px; }

  .nav-cta {
    flex-direction: column;
    align-items: stretch;
    gap: 10px;
    margin-top: 14px;
  }
  .nav-cta .btn { justify-content: center; }
  .lang-switch { align-self: flex-start; }
}

After this, responsive.css goes straight from its header comment block to the /* ───── Homepage footer ───── */ block.

Append at the very end of www/v3/css/product.css (after the block added in Task 2):


/* mobile nav drawer activation — base styles are above (~line 52).
   Drawer drops below the sticky .nav at <=960px; hamburger toggles .is-open.
   The `.nav-drawer .nav-links > a` rule below intentionally re-displays the
   links hidden by the `.nav-links > a:not(.nav-links-home)` rule in the @960
   block higher up — equal (0,2,1) specificity, wins on source order. */
@media (max-width: 960px) {
  .nav-toggle { display: flex; }

  .nav-drawer {
    display: none;
    /* anchors to .nav (position:sticky); .nav-inner is not positioned */
    position: absolute;
    left: 0;
    right: 0;
    top: 100%;
    flex-direction: column;
    background: var(--bg);
    border-bottom: 1px solid var(--line);
    box-shadow: 0 12px 24px rgba(0, 0, 0, 0.08);
    padding: 12px 24px 20px;
    max-height: calc(100vh - 64px);
    overflow-y: auto;
  }
  .nav.is-open .nav-drawer { display: flex; }

  .nav-links {
    flex-direction: column;
    align-items: stretch;
    gap: 0;
  }
  .nav-drawer .nav-links > a,
  .nav-drawer .nav-links > .nav-dd > a {
    display: block;
    padding: 13px 0;
    border-bottom: 1px solid var(--line);
    font-size: 16px;
  }
  .nav-links > .nav-dd { width: 100%; }
  .nav-dd > a::after { display: none; }   /* hide the dropdown caret */

  /* Produkt dropdown flattens — panel sits inline, always visible */
  .nav-dd-panel {
    position: static;
    width: auto;
    opacity: 1;
    pointer-events: auto;
    transform: none;
    border: 0;
    box-shadow: none;
    padding: 4px 0 8px;
  }
  .nav-dd-panel::before,
  .nav-dd-panel::after { display: none; }
  .nav-dd-header { display: none; }
  .nav-dd-grid { grid-template-columns: 1fr; }
  .nav-dd-item { padding: 10px 8px; }

  .nav-cta {
    flex-direction: column;
    align-items: stretch;
    gap: 10px;
    margin-top: 14px;
  }
  .nav-cta .btn { justify-content: center; }
  .lang-switch { align-self: flex-start; }
}

Braces check (both files must be OK):

node -e "for(const f of ['www/v3/css/responsive.css','www/v3/css/product.css']){const c=require('fs').readFileSync(f,'utf8');const o=(c.match(/{/g)||[]).length,cl=(c.match(/}/g)||[]).length;console.log(f,o,cl,o===cl?'OK':'MISMATCH')}"

Write a temporary script www/v3/dev/_navcheck.mjs that, for each of http://127.0.0.1:8123/v3/de/produkt/kreditoren.html and http://127.0.0.1:8123/v3/de/roi/ at viewport 375×800: confirms .nav-toggle computed display is flex (visible); clicks .nav-toggle; confirms .nav gains class is-open and the .nav-drawer becomes visible (getBoundingClientRect().height > 0) with a non-zero count of visible .nav-drawer .nav-links > a links. Also for http://127.0.0.1:8123/v3/de/ at 1280×800: confirms .nav-toggle computed display is none (desktop unaffected). Print PASS/FAIL per check. Run node www/v3/dev/_navcheck.mjs. All checks must PASS. Then delete www/v3/dev/_navcheck.mjs (do not commit it).

git add www/v3/css/responsive.css www/v3/css/product.css
git commit -m "feat(v3): activate mobile nav drawer site-wide via product.css"

Task 4: Add the shared-template-class phone tier

Adds a @media (max-width: 480px) block tightening the shared product/section template classes on phones, and the min-width: 0 grid-item fix — the central shared overflow fix.

Plan correction (added during execution, 2026-05-14): the Task 1 baseline diagnostic showed every product/section page overflowing at 320/375px by a uniform amount (de pages +338/+283px, en pages +177/+122px). Diagnostics traced it to a shared cause: the shared template grids (.feature-block, .product-hero-grid, .stat-row, .cap-grid) have grid items with the default min-width: auto, so a wide child (e.g. a <table> inside .mock) forces the single-column grid wider than the viewport. The textbook fix — min-width: 0 on grid items + containing wide .mock content — is shared-foundation work and belongs here. The block below includes it. (The .footer-nav part of the uniform overflow is fixed separately by Task 5.)

Files:

Append at the very end of www/v3/css/product.css (after the block added in Task 3):


/* shared template classes — phone tier.
   Note: section:first-of-type (from the container block above) governs the
   hero's padding-top at <=768/480px; this block only tightens its bottom. */
@media (max-width: 480px) {
  .product-hero { padding-bottom: 40px; }
  .product-hero-grid,
  .feature-block,
  .feature-block.reverse { gap: 28px; }
  .product-hero-ctas {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    width: 100%;
  }
  .product-hero-ctas .btn { justify-content: center; }
  .cap-grid { margin-top: 32px; }
  .cap { padding: 28px 0 30px; }
  .callout-item { padding: 16px 18px; }
  .modules-cross-item { padding: 22px 18px 26px; }
  .stat { padding: 20px 0; }

  /* Let the single-column shared grids shrink below their content's
     intrinsic width — grid items default to min-width:auto, so a wide
     child (e.g. a table inside .mock) otherwise forces the grid, and the
     whole page, wider than the viewport. Pair with containing wide mocks. */
  .product-hero-grid > *,
  .feature-block > *,
  .stat-row > *,
  .cap-grid > * { min-width: 0; }
  .mock { max-width: 100%; overflow-x: auto; }
}

Braces check (product.css must be OK):

node -e "const c=require('fs').readFileSync('www/v3/css/product.css','utf8');const o=(c.match(/{/g)||[]).length,cl=(c.match(/}/g)||[]).length;console.log(o,cl,o===cl?'OK':'MISMATCH')"

Run the harness on representative pages:

cd /Users/maximilianbrandstaetter/Orcha/.claude/worktrees/website-v3-mobile-responsive/www/v3/dev && node screenshot.mjs home,produkt-kreditoren,produkt-abschluss,roi,agenten

Expected: de-home still ok at all widths. With the min-width: 0 fix this block should substantially reduce the failure set — lightly-bespoke pages like de-produkt-abschluss should now pass or come close. Open shots/de-produkt-abschluss-375.png — hero, capability grid, and callouts tighter and not overflowing. Note: heavily-bespoke pages (e.g. de-produkt-kreditoren) may still FAIL from per-page inline styles outside the shared grids/.mock — those are out of scope for the Foundation and handled in Plan 2. (.footer-nav overflow is still present until Task 5.)

git add www/v3/css/product.css
git commit -m "feat(v3): add phone-tier breakpoint for shared template classes"

chrome.de.js renders footer.site-footer on all 28 product/section pages; product.css styles it but has no responsive rules for it. Add them.

Files:

Append at the very end of www/v3/css/product.css (after the block added in Task 4):


/* shared site footer (footer.site-footer, rendered by chrome.de.js) */
@media (max-width: 768px) {
  footer.site-footer { padding: 56px 0 32px; }
  .footer-top { flex-direction: column; gap: 36px; }
  .footer-bottom {
    flex-direction: column;
    gap: 8px;
    align-items: flex-start;
  }
}
@media (max-width: 480px) {
  .footer-nav { grid-template-columns: 1fr 1fr; gap: 24px 20px; }
}

Braces check (product.css must be OK):

node -e "const c=require('fs').readFileSync('www/v3/css/product.css','utf8');const o=(c.match(/{/g)||[]).length,cl=(c.match(/}/g)||[]).length;console.log(o,cl,o===cl?'OK':'MISMATCH')"

Run the harness on representative pages:

cd /Users/maximilianbrandstaetter/Orcha/.claude/worktrees/website-v3-mobile-responsive/www/v3/dev && node screenshot.mjs produkt-kreditoren,roi,trial

Expected: no new failures. Open shots/de-roi-375.png and shots/de-trial-375.png and scroll to the footer: the footer's brand block + nav columns must be stacked (not a cramped side-by-side row), the nav links a clean 2-column grid, and the bottom bar stacked. Compare shots/de-roi-1280.png — the footer must be unchanged on desktop (brand left, 3-col nav right, bottom bar horizontal).

git add www/v3/css/product.css
git commit -m "feat(v3): make the shared site footer responsive"

Task 5b: Allow long headline words to break

Plan addition (added during execution, 2026-05-14): after Tasks 4-5, the en pages all passed but every de page still overflowed by a uniform +86/+31px. Diagnostics traced it to long German words in large headlines — e.g. <em>Finanzfunktion</em> renders ~379px wide at the clamped minimum display font size and cannot break, overflowing a 320px viewport. German compound words are the trigger (English equivalents are shorter, which is why en pages were already clean). The fix — overflow-wrap: break-word on the shared headline classes — is shared-foundation work; it is a no-op for text that already fits (so the homepage, which passes, is unaffected).

Files:

Append at the very end of www/v3/css/product.css (after the block added in Task 5):


/* allow long words (notably German compounds) to break rather than
   overflow — a no-op for text that already fits. */
.h-display,
.h-section,
.h-sub,
.italic-swap em { overflow-wrap: break-word; }

Braces check (product.css must be OK):

node -e "const c=require('fs').readFileSync('www/v3/css/product.css','utf8');const o=(c.match(/{/g)||[]).length,cl=(c.match(/}/g)||[]).length;console.log(o,cl,o===cl?'OK':'MISMATCH')"

Run the harness on representative pages:

cd /Users/maximilianbrandstaetter/Orcha/.claude/worktrees/website-v3-mobile-responsive/www/v3/dev && node screenshot.mjs de-home,produkt-abschluss,roi,trial,agenten

Expected: de-home still ok at all widths. de-trial (a light page whose only overflow source was the headline <em>) should now be ok at all widths. de-produkt-abschluss, de-roi, de-agenten should be ok or have a much smaller remaining overflow (any residual is per-page bespoke, for Plan 2/3). Open shots/de-trial-375.png — the hero headline wraps cleanly, no overflow.

git add www/v3/css/product.css
git commit -m "feat(v3): let long headline words break to prevent overflow"

Task 6: Foundation verification + baseline report

Confirms the foundation holds and produces the per-page failure list that feeds the follow-up plans.

Files: none (verification only — unless a regression fix is needed)

cd /Users/maximilianbrandstaetter/Orcha/.claude/worktrees/website-v3-mobile-responsive/www/v3/dev && node screenshot.mjs

Capture the full output. Confirm: de-home and en-home... note en-home will likely still FAIL (the diverged EN homepage — Plan 4). de-home MUST be ok at all 5 widths (no regression). The remaining OVERFLOW FAILURES list is expected — those pages have per-page bespoke breakage that Plans 2-4 handle.

Read these screenshots and confirm the shared structure is sound (ignore per-page bespoke issues, which are out of scope here):

Write a temporary www/v3/dev/_navcheck.mjs that confirms, at viewport 375×800, for http://127.0.0.1:8123/v3/de/produkt/kreditoren.html, http://127.0.0.1:8123/v3/de/agenten/, and http://127.0.0.1:8123/v3/de/trial/: .nav-toggle is visible, clicking it opens the drawer (.nav gets is-open, .nav-drawer visible, >0 visible nav links). Print PASS/FAIL per page. Run it, confirm all PASS, then delete www/v3/dev/_navcheck.mjs.

Write www/v3/dev/foundation-baseline.md containing: the date, the full OVERFLOW FAILURES list from Step 1 grouped by page, and a one-line note that this is the input for Plans 2-4. Commit it:

git add www/v3/dev/foundation-baseline.md
git commit -m "docs(v3): record post-foundation responsive baseline"

If Step 1 showed de-home failing or Step 2 showed desktop changed, the CSS moves regressed something. Fix it in product.css/responsive.css, re-run the relevant harness check, and commit:

git add www/v3/css/product.css www/v3/css/responsive.css
git commit -m "fix(v3): foundation regression fix"

If no regression, skip this step.


Self-Review

Checked against docs/superpowers/specs/2026-05-14-v3-responsive-all-pages-design.md:

Placeholder scan: none — every CSS task has the exact block; the harness task has the full file content.

Type/name consistency: the nav-drawer block moved in Task 3 is byte-identical to what Task 1's homepage work shipped (only the leading comment is rewritten to reflect its new home + the now-redundant inline comment about overriding product.css is condensed into the block's intro comment). slug(), PAGES, FILTER, BASE, WIDTHS are all defined and used consistently within screenshot.mjs. Class names (.nav-drawer, .nav-toggle, .product-hero, .feature-block, .cap, .callout-item, .modules-cross-item, .stat, footer.site-footer, .footer-top, .footer-nav, .footer-bottom, section.p-section) all verified present in product.css / rendered by chrome.de.js.