You are an expert frontend developer and UI designer building a single-page personal portfolio.
Write the result to a file called `index.html`. Do not paste the whole page into the chat as one code block. Build it in passes: structure first, then styles, then the JavaScript, then a final accessibility and responsiveness check. Tell me what you've done after each pass so I can look at it before you continue.
<audience>
The reader is a recruiter or hiring manager who has roughly eight seconds before deciding whether to keep scrolling. They are not necessarily technical. Every section must answer "so what can she actually do" rather than "what tools does she like". Optimise for credibility and scannability over cleverness.
</audience>
<technical_constraints>
- One self-contained `index.html`. No build step, no npm, no external CSS or JS files.
- No CSS framework. Write hand-authored CSS in a single `<style>` block in the head, organised with CSS custom properties for all colours, spacing and type sizes. Do not use Tailwind or any CDN-loaded framework.
- Vanilla JavaScript only, in a single `<script>` block before `</body>`. No libraries.
- The only external request is the Google Fonts stylesheet for Poppins.
- Semantic HTML5: `<header>`, `<main>`, `<section>`, `<article>`, `<footer>`. Every section gets an `aria-labelledby` pointing at its heading.
- Accessible: visible focus states on every interactive element, correct heading order with exactly one `<h1>`, `alt` text on every image, all toggles and disclosures as real `<button>` elements with `aria-expanded` and `aria-controls`.
- Respect `prefers-reduced-motion: reduce` by disabling all transitions and scroll animations.
- Responsive at 360px, 768px and 1440px with no horizontal scroll at any width.
- Use `rem` for type and spacing, not `px`.
</technical_constraints>
<brand_and_design>
This is a personal portfolio, not a company site. It carries no company branding, no logos and no borrowed identity. The design intent is a **gallery**: a quiet, warm, almost entirely neutral page with one disciplined accent, so that the project screenshots become the only real colour on screen. Those screenshots contain green-branded product work, which is exactly why the page itself must not compete with them.
Define these as CSS custom properties on `:root` and use them everywhere. Every contrast ratio below has been calculated, not estimated. Do not substitute approximate shades.
--bg: #FAF8F5 /* Warm stone. Page background. */
--surface: #FFFFFF /* Cards and raised panels. */
--ink: #33383B /* Warm charcoal. All headings and body text. 11.19:1 on --bg. */
--ink-muted: #6E7377 /* Warm grey. Secondary and caption text only. 4.52:1 on --bg. */
--accent: #2F5D5A /* Deep dusty teal. The single accent. 6.99:1 on --bg. */
--accent-wash: #E8EFED /* Very pale teal tint. Panel backgrounds and tag fills only. */
--rule: #E3DFD9 /* Warm light stone. Decorative borders and dividers only. */
Colour rules, which matter and are not negotiable:
- Never use pure black, red or yellow anywhere. `--ink` replaces black.
- **There is exactly one accent colour.** Do not introduce a second, do not add a "secondary accent", do not invent tints beyond `--accent-wash`. The restraint is the design.
- `--accent` passes AA for body text, so unlike most accent colours it may be used for inline links in running prose, as well as buttons, icons, active states and large headings.
- White text on an `--accent` fill is 7.41:1, so solid teal buttons with white labels are safe.
- `--ink-muted` is 4.24:1 on `--accent-wash`, which falls short of AA. Inside any washed panel or tag, use `--ink` for all text, never `--ink-muted`.
- `--rule` is deliberately low-contrast and is for decorative dividers and card edges only. It must never be used as a focus indicator or as the sole means of conveying state.
- **Focus rings use `--accent`**, 2px solid with a 2px offset, on every interactive element. This is the one place the accent must be loud.
- Small pops of accent, used sparingly, are the goal. If more than roughly a tenth of the visible page is teal at any scroll position, there is too much of it.
Typography: Poppins throughout, loaded from Google Fonts with weights 300, 400, 500 and 600. Set a clear scale (roughly 3rem hero, 1.5rem section headings, 1rem body, 0.875rem captions) and cap body line length at about 65 characters. Use generous vertical rhythm; whitespace is doing most of the design work here.
Visual style: soft rounded corners (12–16px), subtle low-opacity shadows rather than hard borders, thin `--rule` dividers between sections. Simple, elegant, professional, warm. Nothing that looks like a template.
Motion: one restrained idea, executed well. Use a single `IntersectionObserver` to fade and lift sections in as they enter the viewport, once each, 300–400ms, and nothing else. No parallax, no bouncing, no typewriter effects.
</brand_and_design>
<structure>
**1. Head**
The head contains: a `<title>` in the form "Bridget [[ SURNAME ]] — AI-empowered generalist"; a meta description written for search results; Open Graph and Twitter card tags; an inline SVG favicon in `--accent` (no external file); the Poppins font link; the `<style>` block; and this commented block for me to fill in later:
<!-- ANALYTICS: paste GA4 gtag snippet here -->
<!-- ANALYTICS: paste Vercel Analytics script here (separate from GA4) -->
*Asset paths.* All images live in an `assets/` folder beside `index.html`. Use predictable, lowercase, hyphenated filenames so I know exactly what to save and where: `assets/bridget.jpg` for the hero, and `assets/project-scattergories.png`, `assets/project-te-reo-towns.png`, `assets/project-crm.png`, `assets/project-chatbot.png`, `assets/project-search.png`, `assets/project-portfolio.png` for the cards. List these paths back to me at the end.
**2. Hero**
Split layout on desktop, stacked on mobile with the image first. Left: a placeholder `<img>` in a soft-rounded arch or circular crop, with a real `alt` attribute and explicit `width`/`height` to prevent layout shift. Right: the copy.
Write the greeting as "Hi, I'm Bridget." then produce **three alternative subtitle variants**. Render variant A live in the page, and leave B and C directly above it as HTML comments clearly labelled `<!-- HERO VARIANT B -->` and `<!-- HERO VARIANT C -->` so I can swap them by moving one line.
- Variant A: my original, kept as written. "Not a developer. Not a designer. An AI-empowered generalist who builds solutions."
- Variant B: the same personality but leading with what I am rather than what I am not, so a skimming reader hits something positive first.
- Variant C: an outcome-led version that names the value to an employer rather than the identity.
Supporting paragraph: "I'm in my happy place in a small but mighty team, wearing many hats, building things, solving problems, and making things better."
CTAs: a primary "Get in touch" button (solid `--accent`, white text, `mailto:` link) and a secondary LinkedIn link styled as a quiet text button with an external-link icon. Use `[EMAIL PLACEHOLDER]` and `[LINKEDIN URL PLACEHOLDER]` so they are impossible to miss.
**3. Projects**
This is the centrepiece of the page. Read this section carefully.
*Data structure.* Do not hand-write the cards into the HTML. Define a single `const projects = [...]` array of objects in the script block and render the grid from it. This is deliberate: it is the one source of truth, it makes adding a seventh project trivial, and the structure itself is part of what the page is demonstrating. Each object has the shape:
{
id, title, blurb,
status: "live" | "internal" | "prototype" | "self",
url, // null unless status is "live"
pendingLabel, // badge text if status is "live" but url is unfilled
media: { type: "image" | "gif" | "video", src, poster, alt },
tech: [], // short tag strings
builtWith, approach, learnings // the three reveal blocks
}
*Card front.* Media area at the top, then title, then a one-line blurb, then tech tags as small pill-shaped chips filled `--accent-wash` with `--ink` text in a monospace font. Then the footer row of the card.
*Card states.* Only one project is publicly clickable, so the labelling must describe **what each thing is**, not whether it shipped. Keep exactly two visual treatments so the grid stays calm: a filled `--accent` button for anything live, and a quiet non-clickable badge in `--ink-muted` with a thin `--rule` border for everything else. Only the badge text changes.
- `status: "live"` renders a "View live" button linking out, with `target="_blank"` and `rel="noopener noreferrer"`.
- `status: "internal"` renders the badge "Internal tool".
- `status: "prototype"` renders the badge "Working prototype".
- `status: "self"` renders a "See the prompt" button that opens the modal, and no external link.
**Dead-link guard, required.** Never render a link that goes nowhere. In the render function, treat a project as live only if `status === "live"` *and* `url` is a non-empty string starting with `https://`. If a project is marked live but its `url` is still an unreplaced placeholder, fall back to a non-clickable badge showing that project's `pendingLabel`. This is a safety net, not a TODO: a card upgrades itself the moment a real URL is pasted in, and it is structurally impossible to ship a broken link.
Two different situations trip this guard, which is why the label is per-project rather than hardcoded:
- **Te Reo Towns** has not launched yet, so `pendingLabel: "Launching soon"` is a legitimate state that can ship.
- **Te Reo Scattergories** is already live and simply has not had its URL pasted in, so `pendingLabel: "⚠ URL MISSING"`. This must never ship, and the label is deliberately ugly so it cannot be missed.
Also log a `console.warn` naming any project that trips the guard, so it shows up even if the visual badge is overlooked.
*Media handling.* **Version one ships with `type: "image"` on all six cards.** Still build the slot to accept all three types, because motion will be added to some cards later and that must be a one-field change, not a rebuild. The slot is a fixed 16:10 aspect ratio with `object-fit: cover`, so mixing types never breaks grid alignment:
- `type: "image"` renders an `<img>` with `loading="lazy"` and explicit dimensions.
- `type: "gif"` renders an `<img>`. Note in a code comment that GIFs cannot be paused, so under `prefers-reduced-motion: reduce` the `poster` still image is swapped in via JavaScript instead.
- `type: "video"` renders `<video muted loop playsinline autoplay preload="metadata">` with the `poster` attribute set. Pause it under `prefers-reduced-motion: reduce`. Prefer this over GIF wherever a file is available, since it is far smaller and can actually be paused.
*The reveal.* Each card has its own disclosure button in the card footer labelled "How I built it", with a small chevron that rotates 180 degrees when open. Clicking it expands that one card in place. Requirements:
- Animate using `grid-template-rows: 0fr` to `1fr` on a wrapper, or `max-height`, so the expansion is smooth and does not cause a hard jump.
- Cards expand independently. Opening one must not close or alter the others.
- Wire up `aria-expanded` on the button and `aria-controls` pointing at the panel `id`, and set `hidden` correctly so screen readers and keyboard users get the same experience.
- The revealed panel contains three labelled blocks, in this order, each with a small `--accent` icon or label:
- **Built with** — the tools and models used
- **How I approached it** — the working process for that specific build
- **What I learned** — the honest reflection, including what did not work
Style the panel as a distinct inset area: `--accent-wash` background with a 3px `--accent` left border, so it reads as "looking under the bonnet" without resorting to a fake terminal window. All text inside the panel is `--ink`, never `--ink-muted`, because the muted grey does not meet contrast on the wash. Only the "Built with" content uses a monospace font; the other two blocks are normal prose in Poppins.
*Content.* I am writing all project content myself. Populate every `blurb`, `builtWith`, `approach` and `learnings` field with a clearly marked placeholder in this exact format so nothing fabricated can ever ship by accident:
"[[ BLURB: one line on the problem and the result ]]"
Do not invent plausible-sounding descriptions, prompts, processes or learnings. Placeholders only.
*The six projects.* Use this exact order. It opens with the one thing a reader can click, lands the real-workplace signal in the middle, and closes by pointing at the page they are standing in.
1. **Te Reo Scattergories** — status `live`, `url: "[[ SCATTERGORIES URL ]]"`, `pendingLabel: "⚠ URL MISSING"`, media `image`
2. **Te Reo Towns** — status `live`, `url: "[[ TE REO TOWNS URL ]]"`, `pendingLabel: "Launching soon"`, media `image`
3. **CRM tool** — status `internal`, `url: null`, media `image`
4. **Te Reo Chatbot** — status `prototype`, `url: null`, built in Lovable, media `image` for now
5. **Search** — status `prototype`, `url: null`, media `image` for now
6. **This page** — status `self`, `url: null`, media `image`. A card like any other, with a screenshot placeholder of the page itself. Its "How I built it" reveal is where the build story for this portfolio goes, and its card footer carries the "See the prompt" button, which opens the modal described in the footer section below. This card exists because the reader is standing inside the artefact, which makes it the strongest single piece of evidence on the page.
Grid: two columns from 768px, one column below. Cards must be equal height within a row when closed.
**4. My process**
A four-step horizontal timeline on desktop, collapsing to a vertical stacked list on mobile. Each step is a numbered marker in `--accent`, a short heading, and one sentence. Draw the connecting line as a CSS pseudo-element in `--rule`, not an image. Use inline SVG icons only, no icon font, no icon library.
1. **Solid prompts, validated** — build in one model, validate the output in another.
2. **Step by step** — decisions documented, fresh chats for fresh problems, never one sprawling thread.
3. **Strict guardrails** — standing instructions demanding honesty, cited sources, and options rather than a single confident answer.
4. **Human oversight** — security and final content always get human review and sign-off.
**5. Tools**
A simple responsive badge grid, text only, no logo images. Each badge is a bordered pill in `--rule` with `--ink` text, subtly lifting on hover. Name: Gemini, Gems, Lovable, Claude, Claude Cowork, Claude Skills, Claude in Chrome. Add a single short line above the grid framing these as the current stack rather than a fixed list.
**6. Footer**
Minimal. Copyright with the year rendered by JavaScript so it never goes stale. A repeat of the email and LinkedIn links. Below that, the quiet text link "Built by Claude (see my prompt)".
Both that footer link and the "See the prompt" button on the sixth project card open the same modal. Modal requirements:
- Centred, max-width around 720px, scrollable body, backdrop with a soft blur.
- Opens with heading "Here's the exact prompt I used to generate this page" and a `<pre><code>` block containing `[[ PASTE THE FULL PROMPT HERE ]]`.
- Include a "Copy prompt" button using the Clipboard API, with a brief "Copied" confirmation state.
- Closes on backdrop click, on the close button, and on the Escape key.
- Trap focus inside the modal while it is open, return focus to the triggering element on close, and set `aria-hidden` on the background content. Use the native `<dialog>` element with `showModal()` if you are confident in its behaviour, since it handles most of this for free.
</structure>
<final_check>
Before you tell me it's finished, verify and report on each of these:
- No horizontal scroll at 360px.
- Every interactive element is reachable and operable by keyboard alone, with a visible focus ring.
- Every reveal panel and the modal have correct `aria-expanded` / `aria-controls` / focus handling.
- No fabricated project content anywhere. Every content placeholder is still in `[[ ]]` form.
- No colour outside the seven custom properties defined above. In particular, no second accent colour has crept in.
- No black, red or yellow.
- Focus rings are `--accent`, visible on every interactive element including the reveal buttons and the modal controls.
- No company logo, wordmark or borrowed brand identity appears anywhere on the page.
- The sixth card renders no external link, and no card anywhere renders a link that goes nowhere. Prove the dead-link guard works: with both URLs unfilled, Te Reo Towns must show "Launching soon" and Scattergories must show "⚠ URL MISSING", and both must appear in `console.warn`. Then paste a dummy `https://` URL into one and confirm it becomes a real link.
- All six cards currently use `type: "image"`, and switching any one of them to `"video"` requires changing only that card's `media` object. Confirm this by temporarily switching one card and checking nothing else breaks.
- Under `prefers-reduced-motion: reduce`, videos pause and GIFs are replaced by their poster frames.
- Because the project grid is rendered from the `projects` array, it will be empty if JavaScript is disabled. That is an accepted trade-off for the data-driven structure. Add a `<noscript>` message inside the projects section pointing the reader at my email, and do not attempt a server-rendered fallback.
Then give me a short numbered list of everything I need to fill in myself, in the order I should do it.
</final_check>