Discovery
Define the problem space through stakeholder interviews and competitive analysis.
All case study pages are assembled from these reusable blocks via ACF Pro flexible content. Each block is a self-contained PHP template with scoped CSS. Editors choose from the block palette in WordPress — no code needed.
3-column asymmetric grid. Label + heading in column 1, body copy spans columns 2–3. Forces reading hierarchy — the label anchors the section, the body breathes right.
Research findings
Through twelve stakeholder interviews and two rounds of usability testing, we identified three core user needs that shaped the entire design direction and validated our initial assumptions.
labeltextSection eyebrowheadingtextSection headingbodytextareaMain copy (large, tertiary)<section class="acf-block acf-text-block">
<div class="acf-text-block__header">
<p class="acf-block__label">Research findings</p>
<h2 class="acf-block__heading">User needs shape every decision</h2>
</div>
<div class="acf-text-block__body">
<p>Through twelve stakeholder interviews and two rounds of usability testing, we identified three core user needs.</p>
</div>
<!-- Optional button -->
<div class="acf-text-block__actions">
<a href="/report.pdf" class="btn btn-secondary" target="_self">Read the report</a>
</div>
</section>/* single-portfolio.php */
.acf-text-block { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--space-2xl); padding-block: var(--space-xl); align-items: start; }
.acf-text-block__header { grid-column: 1; display: flex; flex-direction: column; gap: var(--space-m); }
.acf-text-block__body { grid-column: 2 / span 2; color: var(--text-on-dark-tertiary); font-size: var(--text-l); line-height: var(--lh-m); }
.acf-text-block__body p { margin: 0 0 var(--space-m); }
.acf-text-block__body p:last-child { margin-bottom: 0; }
@media (max-width: 768px) {
.acf-text-block { grid-template-columns: 1fr; }
.acf-text-block__body { grid-column: 1; }
}4-variant flexible text block. Layout controlled by an ACF select field. Covers bullet lists, image+text, and body-only compositions.
Variant: --bullets
Approach
A structured process that moves from broad research to focused delivery.
Variant: --no-image
Context
A fast-growing logistics startup needed a delivery tracking interface that worked equally well for dispatchers on desktop and couriers on mobile, without a separate codebase.
Variant: --image-left
Process
We mapped every touchpoint across the end-to-end delivery experience to identify friction points and prioritise redesign effort.
Variant: --image-right
Outcome
A unified responsive interface that reduced dispatcher errors by 34% and cut courier training time in half.
layoutselectbullets / no-image / image-left / image-rightlabeltextheadingtextbodywysiwygRich text, supports lists + linksimageimageRequired for image-left/rightbullet_itemsrepeaterFor bullets layout<!-- --bullets -->
<section class="acf-block acf-text-plus acf-text-plus--bullets">
<div class="acf-text-plus__header">
<p class="acf-block__label">Approach</p>
<h2 class="acf-block__heading">How we work</h2>
</div>
<div class="acf-text-plus__body-bullets">
<div class="acf-text-plus__body"><p>A structured process that moves from broad research to focused delivery.</p></div>
<ul class="acf-text-plus__bulletlist">
<li>Discovery & stakeholder interviews</li>
<li>Synthesis and affinity mapping</li>
</ul>
</div>
</section>
<!-- --no-image -->
<section class="acf-block acf-text-plus acf-text-plus--no-image">
<div class="acf-text-plus__header">…</div>
<div class="acf-text-plus__body"><p>…</p></div>
</section>
<!-- --image-left (swap the two children for --image-right) -->
<section class="acf-block acf-text-plus acf-text-plus--image-left">
<div class="acf-text-plus__image"><img src="journey-map.jpg" alt="Journey map"></div>
<div class="acf-text-plus__content">
<p class="acf-block__label">Process</p>
<h2 class="acf-block__heading">Mapping the journey</h2>
<div class="acf-text-plus__body"><p>…</p></div>
</div>
</section>/* single-portfolio.php */
.acf-text-plus { display: grid; gap: var(--space-2xl); padding-block: var(--space-xl); align-items: start; }
.acf-text-plus--no-image { grid-template-columns: 1fr 2fr; }
.acf-text-plus--bullets { display: flex; flex-direction: column; gap: var(--space-2xl); }
.acf-text-plus__body-bullets { display: grid; grid-template-columns: 1fr 2fr; gap: var(--space-2xl); align-items: start; width: 100%; min-width: 0; }
.acf-text-plus--image-right,
.acf-text-plus--image-left { grid-template-columns: 1fr 1fr; align-items: center; }
.acf-text-plus__header,
.acf-text-plus__content { display: flex; flex-direction: column; gap: var(--space-l); }
.acf-text-plus__body { color: var(--text-on-dark-tertiary); font-size: var(--text-l); line-height: var(--lh-m); }
.acf-text-plus__image { border-radius: var(--radius-xs); overflow: hidden; }
.acf-text-plus__image img { width: 100%; height: auto; display: block; object-fit: cover; }
.acf-text-plus__bulletlist { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-xs); }
.acf-text-plus__bulletlist li { display: flex; gap: var(--space-xs); align-items: flex-start; font-size: var(--text-m); color: var(--text-on-dark-tertiary); line-height: var(--lh-xs); }
.acf-text-plus__bulletlist li::before { content: ''; display: block; width: 8px; height: 8px; background: var(--surface-branding-primary); flex-shrink: 0; margin-top: 6px; }Full-bleed centred typographic statement. Used sparingly — once per case study — to mark a key finding or design principle. The text size itself communicates importance.
Good design is as little design as possible.
— Dieter Rams
quotetextThe quote textattributiontextName / source<section class="acf-pull-quote">
<div class="acf-pull-quote__inner">
<p class="acf-pull-quote__text">Good design is as little design as possible.</p>
<p class="acf-pull-quote__attr">— Dieter Rams</p>
</div>
</section>/* single-portfolio.php */
.acf-pull-quote { display: flex; flex-direction: column; align-items: flex-start; padding: var(--space-xl) 100px; }
.acf-pull-quote__inner { display: flex; flex-direction: column; gap: var(--space-l); align-items: center; max-width: 1024px; width: 100%; }
.acf-pull-quote__text {
font-size: var(--text-5xl); font-weight: var(--fw-black);
color: var(--text-on-dark-primary); line-height: 1;
text-align: center; margin: 0; letter-spacing: -.02em;
}
.acf-pull-quote__attr { font-size: var(--text-m); font-weight: var(--fw-semibold); color: var(--text-on-dark-secondary); text-transform: uppercase; letter-spacing: 4px; margin: 0; }
@media (max-width: 768px) {
.acf-pull-quote { padding-inline: 0; }
.acf-pull-quote__text { font-size: clamp(var(--text-3xl), 11vw, var(--text-5xl)); line-height: 1.05; }
}Alternating left/right task cards with JS-drawn right-angle SVG connector lines (8px radius corners). Visualises user journeys, service blueprints, or process flows.
Define the problem space through stakeholder interviews and competitive analysis.
Synthesise research into opportunity areas and a clear design brief.
Ship tested, production-ready designs with full developer handoff documentation.
labeltextheadingtextdescriptiontextHalf-width intro descriptionstepsrepeaterstep_title, step_body, step_image, side (left/right)<section class="acf-block acf-flow">
<div class="acf-flow__header">
<p class="acf-block__label">Process</p>
<h2 class="acf-block__heading">From brief to launch</h2>
</div>
<p class="acf-flow__description">Optional intro text.</p>
<div class="acf-flow__track">
<div class="acf-flow__row acf-flow__row--left">
<article class="acf-flow__card">
<div class="acf-flow__img-wrap acf-flow__img-wrap--number" aria-hidden="true">
<span class="acf-flow__number">01</span>
</div>
<div class="acf-flow__text">
<h3 class="acf-flow__title">Discovery</h3>
<p class="acf-flow__body">Define the problem space through stakeholder interviews.</p>
</div>
</article>
</div>
<!-- Connector between steps; JS sets the path from data-from / data-to -->
<div class="acf-flow__connector acf-flow__connector--left" aria-hidden="true" data-from="25" data-to="75">
<svg width="100%" height="80" xmlns="http://www.w3.org/2000/svg">
<path stroke="currentColor" stroke-width="2" fill="none"/>
</svg>
</div>
<div class="acf-flow__row acf-flow__row--right">
<article class="acf-flow__card">…</article>
</div>
</div>
</section>/* single-portfolio.php + flow.php */
.acf-flow { padding-block: var(--space-xl); }
.acf-flow__header { display: flex; flex-direction: column; gap: var(--space-m); margin-bottom: var(--space-2xl); }
.acf-flow__track { display: flex; flex-direction: column; }
.acf-flow__row { display: grid; grid-template-columns: 1fr 1fr; }
.acf-flow__row--left .acf-flow__card { grid-column: 1; }
.acf-flow__row--right .acf-flow__card { grid-column: 2; flex-direction: row-reverse; text-align: right; }
.acf-flow__card { display: flex; gap: var(--space-l); align-items: center; padding: var(--space-m); min-width: 0; }
.acf-flow__text { display: flex; flex-direction: column; gap: var(--space-s); flex: 1; min-width: 0; }
.acf-flow__title { font-size: var(--text-xl); font-weight: var(--fw-black); color: var(--text-on-dark-secondary); line-height: 1.2; margin: 0; }
.acf-flow__body { font-size: var(--text-m); color: var(--text-on-dark-tertiary); line-height: var(--lh-xs); margin: 0; }
.acf-flow__connector { width: 100%; color: var(--surface-branding-primary); }
.acf-flow__connector svg { display: block; }
@media (max-width: 550px) {
.acf-flow__row { grid-template-columns: 1fr; }
.acf-flow__row--right .acf-flow__card { grid-column: 1; flex-direction: row; text-align: left; }
.acf-flow__connector { display: none; }
}Structured data table with optional row images and bullet lists per cell. Horizontally scrollable on mobile. Used for competitive analysis, feature matrices, and research data.
| Feature | That's Adam | Competitor A | Competitor B |
|---|---|---|---|
Responsive design |
Full | Partial | None |
Dark mode |
Token-driven | None | Manual |
Design system |
|
Style guide only | None |
Note: first column header renders at text-xl in red; first column cells render in red, uppercase, semibold. Subsequent columns are grey.
labeltextheadingtextcolumnsrepeatercolumn_headingrowsrepeaterrow_label, row_image, cells (sub-repeater: cell_content, cell_bullets)<section class="acf-block acf-table-block">
<div class="acf-table-block__header">
<p class="acf-block__label">Competitive analysis</p>
<h2 class="acf-block__heading">How the options compare</h2>
</div>
<div style="overflow-x:auto;">
<table class="acf-table">
<thead>
<tr><th>Feature</th><th>That's Adam</th><th>Competitor A</th></tr>
</thead>
<tbody>
<tr>
<td>
<div class="acf-table__label-cell">
<img class="acf-table__row-img" src="row.jpg" alt="Design system"> <!-- optional -->
Design system
</div>
</td>
<td><ul class="acf-table__bullets"><li>Figma source</li><li>CSS tokens</li></ul></td>
<td>Style guide only</td>
</tr>
</tbody>
</table>
</div>
</section>/* single-portfolio.php */
.acf-table-block { display: flex; flex-direction: column; gap: var(--space-2xl); padding-block: var(--space-xl); }
.acf-table-block__header { display: flex; flex-direction: column; gap: var(--space-m); }
.acf-table { width: 100%; border-collapse: collapse; table-layout: fixed; }
/* Row and cell styles — see Table Row */
.acf-table__row-img { width: 150px; height: 150px; object-fit: cover; border-radius: var(--radius-xxs); display: block; flex-shrink: 0; }
.acf-table__bullets { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-xs); }
.acf-table__bullets li { display: flex; gap: var(--space-xs); align-items: flex-start; font-size: var(--text-m); color: var(--text-on-dark-tertiary); line-height: var(--lh-xs); }
.acf-table__bullets li::before { content: ''; display: block; width: 8px; height: 8px; background: var(--surface-branding-primary); flex-shrink: 0; margin-top: 6px; }
@media (max-width: 768px) {
.acf-table-block { overflow-x: auto; scrollbar-width: none; }
.acf-table { table-layout: auto; }
.acf-table th, .acf-table td { min-width: 300px; }
.acf-table th:first-child, .acf-table td:first-child { min-width: 160px; }
}Displays 1–4 screen images with blue drop-shadow glow. PNG exports with transparent backgrounds are required for the glow to work correctly. Supports captions and bullet annotations.
Final screens
Dashboard view
Detail view
labeltextheadingtextlayoutselectdefault / split-metaimagesrepeaterimage, caption, bullets<section class="acf-block acf-screen-mockups acf-screen-mockups--rounded">
<div class="acf-screen-mockups__header">
<p class="acf-block__label">Final screens</p>
<h2 class="acf-block__heading">Delivered solution</h2>
</div>
<div class="acf-screen-mockups__meta acf-screen-mockups__meta--split">
<div class="acf-screen-mockups__desc"><p>Optional description.</p></div>
<ul class="acf-screen-mockups__bullets"><li>Optional bullet</li></ul>
</div>
<div class="acf-screen-mockups__images acf-screen-mockups__images--scrollable">
<div class="acf-screen-mockups__item" style="width:198px;"> <!-- 418px tall × the image's ratio -->
<div class="acf-screen-mockups__mockup">
<img src="dashboard.png" alt="Dashboard view" style="aspect-ratio:432/911;" loading="lazy">
</div>
<p class="acf-screen-mockups__caption">Dashboard view</p>
</div>
</div>
</section>/* single-portfolio.php */
.acf-screen-mockups__images { display: flex; gap: 40px; align-items: center; padding-block: var(--space-m); }
.acf-screen-mockups__item { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: var(--space-m); }
/* No overflow clip on the wrapper, so the shadow can bleed outside */
.acf-screen-mockups__mockup img {
width: 100%; height: auto; min-height: 418px;
object-fit: cover; display: block;
filter: drop-shadow(0 0 24px var(--lightbox-img-shadow));
}
.acf-screen-mockups--rounded .acf-screen-mockups__mockup img { border-radius: 12px; }Editorial full-width rows, one per participant. The quote is the typographic hero — extralight italic at clamp scale. Ghost sequence numbers are oversized and purely decorative.
The handoff between design and development was always where things broke down — I needed a designer who understood both sides.
Insights
labeltextheadingtextdescriptiontextHalf-width introinterviewsrepeaterparticipant_name, participant_role, participant_photo, quote, insights (sub-repeater: insight_text)<section class="acf-block ui-block">
<header class="ui-block__header">
<p class="acf-block__label">Research</p>
<h2 class="acf-block__heading">What users told us</h2>
</header>
<p class="ui-block__description">Optional intro text.</p>
<div class="ui-block__entries">
<article class="ui-entry">
<div class="ui-entry__inner">
<span class="ui-entry__seq" aria-hidden="true">01</span>
<div class="ui-entry__main">
<div class="ui-entry__photo">
<span class="ui-entry__initials">SJ</span> <!-- or <img> -->
</div>
<div class="ui-entry__content">
<div class="ui-entry__id">
<strong class="ui-entry__name heading-m">Sarah Johnson</strong>
<span class="ui-entry__role">Senior Product Manager</span>
</div>
<blockquote class="ui-entry__quote">The handoff between design and development was always where things broke down.</blockquote>
</div>
</div>
<div class="ui-entry__insights">
<p class="ui-entry__insights-label">Insights</p>
<ul class="ui-entry__chips" aria-label="Key insights">
<li class="ui-entry__chip">Design-dev handoff pain</li>
</ul>
</div>
</div>
</article>
</div>
</section>/* template-parts/acf-blocks/user_interviews.php */
.ui-entry { position: relative; border-top: 1px solid var(--surface-branding-primary-30); overflow: hidden; }
.ui-entry:last-child { border-bottom: 1px solid var(--surface-branding-primary-30); }
.ui-entry__inner { position: relative; padding: var(--space-2xl) 0; display: flex; flex-direction: column; gap: var(--space-l); }
.ui-entry__seq {
position: absolute; top: 50%; right: -0.05em; transform: translateY(-50%);
font-size: clamp(120px, 22vw, 220px); font-weight: var(--fw-black);
}
.ui-entry__photo {
width: 240px; height: 240px; flex-shrink: 0;
border-radius: 12px; overflow: hidden;
background: var(--surface-branding-primary-10);
border: 1px solid var(--surface-branding-primary);
display: flex; align-items: center; justify-content: center;
}
.ui-entry__photo img { width: 100%; height: 100%; object-fit: cover; object-position: 50% 20%; display: block; }
.ui-entry__initials { font-size: clamp(2rem, 5vw, 4rem); font-weight: var(--fw-black); color: var(--surface-branding-primary); line-height: 1; }
.ui-entry__role { font-size: var(--text-m); font-weight: var(--fw-semibold); color: var(--text-on-dark-tertiary); text-transform: uppercase; letter-spacing: var(--ls-label-xl); }
.ui-entry__quote {
position: relative; z-index: 1; margin: 0; padding: 0;
font-size: clamp(var(--text-xl), 3.5vw, 2.4rem);
font-weight: var(--fw-extralight); font-style: italic;
line-height: var(--lh-xl); color: var(--text-on-dark-primary);
max-width: 78ch;
}
.ui-entry__insights-label { font-size: var(--text-m); font-weight: var(--fw-semibold); color: var(--text-on-dark-secondary); text-transform: uppercase; letter-spacing: var(--ls-label-xl); margin: 0; }
/* .ui-entry__chip — see Labels & Chips */Interview findings grouped into categories, one post-it note per category. Each note gets a fixed tilt and vertical nudge from a repeating pattern, so the board looks hand-pinned but renders the same every time. Hover or focus straightens the note.
labeltextheadingtextdescriptiontextareaoptional intro text, line breaks keptcategoriesrepeatercategory_title, colour (auto / yellow / light / blue / red / white), points (sub-repeater: point_text)<section class="acf-block am-block">
<header class="am-block__header">
<p class="acf-block__label">Synthesis</p>
<h2 class="acf-block__heading">Affinity map</h2>
</header>
<div class="am-board">
<!-- Tilt and nudge come from a fixed cycle in PHP, so the board renders the same every time -->
<article class="am-note am-note--yellow" style="--am-tilt:-2deg;--am-nudge:0px;">
<span class="am-note__tape" aria-hidden="true"></span>
<h3 class="am-note__title">Trust</h3>
<ul class="am-note__points" role="list">
<li class="am-note__point">Wants to see real reviews before booking</li>
</ul>
</article>
<article class="am-note am-note--blue" style="--am-tilt:1.5deg;--am-nudge:var(--space-s);">…</article>
</div>
</section>/* template-parts/acf-blocks/affinity_map.php */
.am-board {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(min(100%, calc(var(--space-5xl) * 2)), 1fr)); /* 280px */
gap: var(--space-2xl) var(--space-xl);
align-items: start;
padding: var(--space-s) 0 var(--space-m);
}
.am-note--yellow { --am-bg: var(--brand-yellow); }
.am-note--light { --am-bg: var(--brand-yellow-light); }
.am-note--blue { --am-bg: var(--brand-blue); }
.am-note--red { --am-bg: var(--brand-red); }
.am-note--white { --am-bg: var(--brand-white); }
.am-note {
position: relative; aspect-ratio: 1 / 1;
padding: var(--space-1xl) var(--space-l) var(--space-xl);
background: var(--am-bg); color: var(--brand-black);
border-radius: var(--radius-xs);
box-shadow: 0 0 var(--space-l) 0 var(--shadow-glow);
transform: translateY(var(--am-nudge, 0px)) rotate(var(--am-tilt, 0deg));
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.am-note:hover,
.am-note:focus-within { z-index: 1; transform: translateY(var(--am-nudge, 0px)) rotate(0deg) scale(1.03); }
.am-note__tape {
position: absolute; top: calc(var(--space-s) * -1); left: 50%;
width: var(--space-4xl); height: var(--space-l); border-radius: var(--radius-xxs);
transform: translateX(-50%) rotate(calc(var(--am-tilt, 0deg) * -1.5));
background: color-mix(in srgb, var(--brand-white) 55%, transparent);
}
.am-note__title {
margin: 0 0 var(--space-m); padding-bottom: var(--space-s);
font-size: var(--text-xl); font-weight: var(--fw-black); line-height: var(--lh-s);
border-bottom: 1px solid color-mix(in srgb, var(--brand-black) 25%, transparent);
}
@media (prefers-reduced-motion: reduce) { .am-note { transition: none; } }Spider/hub diagram. Desktop: 3-column grid, hub centred, stories left and right, JS-drawn bolt-animation lines connecting hub to cards. Mobile: flex column with hub top-left, vertical connector, horizontal tick lines per card.
labeltextheadingtextdescriptiontextareaoptional intro text under the headinghub_labeltextcentre hub text, defaults to "User stories"storiesrepeaterstory_title, story_body, story_image<section class="acf-block acf-user-stories">
<div class="acf-user-stories__header">
<p class="acf-block__label">Requirements</p>
<h2 class="acf-block__heading">User stories</h2>
</div>
<canvas class="acf-us-neurons" aria-hidden="true"></canvas>
<div class="acf-us-spider" id="acf-us-spider-1">
<!-- JS draws the hub-to-card lines into this SVG -->
<svg class="acf-us-spider__lines" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"></svg>
<div class="acf-us-spider__hub" data-us-hub style="grid-column:2; grid-row:1/span 1;">
<span class="acf-us-spider__hub-label">User stories</span>
</div>
<!-- Even stories go left (column 1), odd stories go right (column 3) -->
<article class="acf-us-spider__card acf-us-spider__card--left" style="grid-column:1;grid-row:1;" data-us-card>
<div class="acf-us-spider__img-wrap acf-us-spider__img-wrap--initials"><span>As</span></div>
<div class="acf-us-spider__text">
<h3 class="acf-us-spider__title">As a visitor</h3>
<p class="acf-us-spider__body">I want to quickly understand what kind of designer Adam is.</p>
</div>
</article>
<article class="acf-us-spider__card acf-us-spider__card--right" style="grid-column:3;grid-row:1;" data-us-card>…</article>
</div>
</section>/* template-parts/acf-blocks/user_stories.php */
.acf-user-stories { padding-block: var(--space-xl); position: relative; }
.acf-us-spider { position: relative; display: grid; grid-template-columns: 1fr 1fr 1fr; gap: var(--space-2xl); align-items: center; z-index: 1; padding-top: var(--space-m); }
.acf-us-spider__hub {
display: flex; align-items: center; justify-content: center;
min-width: 240px; height: 240px; padding-inline: var(--space-m);
border-radius: 999px;
border: 2px solid var(--surface-branding-primary);
box-shadow: 0 0 40px 0 var(--shadow-glow), inset 0 0 40px 0 var(--surface-branding-primary-10);
}
.acf-us-spider__card { display: flex; gap: var(--space-l); align-items: flex-start; padding: var(--space-l); border-radius: var(--radius-xs); }
.acf-us-spider__card--right { flex-direction: row-reverse; }
.acf-us-spider__img-wrap { width: 106px; height: 106px; flex-shrink: 0; border-radius: var(--radius-xs); overflow: hidden; background: var(--surface-branding-primary); }
.acf-us-spider__title { font-size: var(--text-xl); font-weight: var(--fw-black); color: var(--text-on-dark-secondary); line-height: 1.2; margin: 0; }
.acf-us-spider__body { font-size: var(--text-m); color: var(--text-on-dark-tertiary); line-height: var(--lh-xs); margin: 0; }Dossier-style cards with two rows. Row 1: full-width 1:1 photo column + bio column (name, glowing 2px divider, italic quote, thumb-icon list items, demographics). Row 2: bio rich text + placeholder squares. Multiple persona blocks alternate left/right via a global PHP counter.
“I don't have time to decode a designer's portfolio — show me the impact fast.”
Interests: Startups, SaaS, scale-up culture, impact metrics
Tech frustrations: Designers who can't communicate trade-offs
Job: Head of Product, SaaS startup
Location: Copenhagen, Denmark
Age Group: 35–44
Marcus has spent 8 years in product roles at scale-ups across the Nordics. He evaluates design partners primarily on communication quality and ability to translate user insight into shipping decisions.
.prs-card--flip via PHP global counter, reversing row directionlabeltextheadingtextpersona_cardsrepeatername, tagline, photo, quote, bio (wysiwyg), goals (repeater → goal_text), frustrations (repeater → frustration_text), job, nationality, location, gender, age_group, relationship, kids<section class="acf-block prs-block">
<header class="prs-block__header">
<p class="acf-block__label">Research</p>
<h2 class="acf-block__heading">Who we're designing for</h2>
</header>
<div class="prs-block__list">
<!-- Every second card adds .prs-card--flip -->
<div class="prs-card">
<div class="prs-card__header-row">
<div class="prs-card__photo-col">
<img class="prs-card__photo" src="marcus.jpg" alt="Marcus Klein">
</div>
<div class="prs-card__bio-col">…</div> <!-- Bio molecule -->
</div>
<div class="prs-card__extended-row">
<div class="prs-card__bio-section">
<div class="prs-li prs-li--top">
<div class="prs-li__icon"><svg class="prs-li__img--bio" …>…</svg></div>
<div class="prs-li__text prs-li__text--rich"><strong>Bio:</strong><p>Marcus has spent 8 years in product roles…</p></div>
</div>
</div>
<div class="prs-card__placeholder-col" aria-hidden="true">
<div class="prs-card__placeholder prs-card__placeholder--1"></div>
<div class="prs-card__placeholder prs-card__placeholder--2"></div>
<div class="prs-card__placeholder prs-card__placeholder--3"></div>
</div>
</div>
</div>
</div>
</section>/* template-parts/acf-blocks/personas.php */
/* Card: two-row flex column */
.prs-card { display: flex; flex-direction: column; gap: 64px; }
/* Row 1: photo col + bio col */
.prs-card__header-row { display: flex; flex-direction: row; align-items: flex-end; gap: 64px; }
.prs-card--flip .prs-card__header-row { flex-direction: row-reverse; }
/* Photo: full-width square, border-radius 16px (not a circle) */
.prs-card__photo { width: 100%; aspect-ratio: 1 / 1; object-fit: cover; border-radius: 16px; }
/* Name, divider, quote and demographics — see Bio */<?php
// A global counter keeps the left/right alternation going across several persona blocks on one page
global $thats_adam_persona_count;
if ( ! isset( $thats_adam_persona_count ) ) $thats_adam_persona_count = 0;
$thats_adam_persona_count++;
$flip = ( $thats_adam_persona_count % 2 === 0 ) ? ' prs-card--flip' : '';
?>
<div class="prs-card<?php echo $flip; ?>">…</div>Full-bleed, click-and-drag horizontal carousel. The first slide's left edge lines up with the rest of the page's content column on load, with more revealed by dragging/scrolling right — no scroll-snap, same freeform mechanic as the homepage Portfolio carousel. Each slide is one of the existing Text Block / Text + / Image layouts, nested via a second flexible-content field, rendered at a compact scale (scoped-down type/space tokens + forced single-column, bullet lists excepted) so full-page-sized blocks don't overwhelm a slide.
Step 1
Step 2
labeltextheadingtextdescriptiontextareaoptional intro text under the headingslidesflexible_contentnested — layouts: text_block, text_plus, image_full_width (same sub_fields as those top-level blocks)<section class="acf-block acf-carousel">
<div class="acf-carousel__header">
<p class="acf-block__label">Process</p>
<h2 class="acf-block__heading">Step by step</h2>
</div>
<p class="acf-carousel__description">Optional intro text.</p>
<div class="acf-carousel__track" data-carousel-track>
<!-- Each slide is a Text Block, Text + or Image Full Width block -->
<div class="acf-carousel__slide">
<section class="acf-block acf-text-block">…</section>
</div>
<div class="acf-carousel__slide">
<div class="acf-block acf-image-full">…</div>
</div>
</div>
</section>/* single-portfolio.php */
.acf-carousel__track {
width:100vw; left:50%; transform:translateX(-50%);
padding-left:calc(max(0px, (100vw - 1440px) / 2) + var(--page-padding-x));
overflow-x:auto; cursor:grab;
}
.acf-carousel__slide {
flex:0 0 auto; width:min(840px, 82vw);
--text-3xl:28px; --text-2xl:22px; --text-l:16px;
--space-2xl:var(--space-l); --space-xl:var(--space-m);
}<?php // carousel.php — each slide reuses an existing block template
while ( have_rows( 'slides' ) ) : the_row();
$slide_layout = get_row_layout();
if ( ! in_array( $slide_layout, [ 'text_block', 'text_plus', 'image_full_width' ], true ) ) continue; ?>
<div class="acf-carousel__slide">
<?php thats_adam_part( 'acf-blocks/' . $slide_layout ); ?>
</div>
<?php endwhile; ?>Full-bleed image block. Optional label and heading above the image. 8px border-radius, full container width, optional alt text override via ACF field.
Final designs
.acf-image-full__header: label + heading above image.acf-image-full__img — fills 100% container width, loading="lazy"<img> taglabeltextOptional eyebrow labelheadingtextOptional headingimageimageRequired — the image to displayalt_texttextOptional override for image alt attribute<div class="acf-block acf-image-full">
<div class="acf-image-full__header">
<p class="acf-block__label">Final designs</p>
<h2 class="acf-block__heading">Delivered solution</h2>
</div>
<img src="final-designs.jpg" alt="Final designs across devices" class="acf-image-full__img" loading="lazy">
<!-- Optional text + button -->
<div class="acf-image-full__actions">
<p class="acf-image-full__actions-text">See the full prototype.</p>
<a href="https://figma.com/…" class="btn btn-secondary" target="_blank" rel="noopener">Open prototype</a>
</div>
</div>/* single-portfolio.php */
.acf-image-full { padding-block: var(--space-xl); }
.acf-image-full__header { display: flex; flex-direction: column; gap: var(--space-m); margin-bottom: var(--space-2xl); }
.acf-image-full__img {
width: 100%; height: auto; display: block;
border-radius: var(--radius-xs); object-fit: cover;
filter: drop-shadow(0 0 24px var(--lightbox-img-shadow));
}
.acf-image-full__actions { display: flex; flex-direction: column; align-items: center; gap: var(--space-m); margin-top: var(--space-2xl); text-align: center; }
.acf-image-full__actions-text { font-size: var(--text-m); color: var(--text-on-dark-tertiary); line-height: var(--lh-xs); margin: 0; max-width: 520px; }A/B test results visualisation. Donut chart via CSS-masked conic-gradient (no canvas/SVG). Screen mockup images with glow drop-shadow. Outcome panel highlights the winner with a left-border accent.
Visual preference test
Outcome
Option A was chosen by 68% of participants. The bold headline first approach communicated credibility more quickly than leading with imagery.
labeltextheadingtexttestsrepeatertest_label, test_desc, options (sub-repeater: option_name, option_colour, option_percentage, option_mockup), outcome_text<section class="acf-block acf-pref-test">
<div class="acf-pref-test__header">
<p class="acf-block__label">Visual preference test</p>
<h2 class="acf-block__heading">Which layout do users prefer?</h2>
</div>
<div class="acf-pref-test__test">
<div class="acf-pref-test__test-meta">
<h3 class="acf-pref-test__test-label">Homepage hero layout</h3>
<p class="acf-pref-test__test-desc">Participants were shown two variants.</p>
</div>
<div class="acf-pref-test__body">
<div class="acf-pref-test__chart-col">
<div class="acf-pref-test__donut-wrap">
<!-- Gradient stops are built in PHP from the option percentages -->
<div class="acf-pref-test__donut" style="background:conic-gradient(from 0deg, var(--chart-1) 0% 68%, var(--chart-2) 68% 100%);"></div>
</div>
<ul class="acf-pref-test__legend">
<li class="acf-pref-test__legend-item">
<span class="acf-pref-test__legend-dot" style="background:var(--chart-1);"></span>
<span class="acf-pref-test__legend-name">Option A</span>
<span class="acf-pref-test__legend-pct">68%</span>
</li>
</ul>
</div>
<div class="acf-pref-test__options">
<div class="acf-pref-test__option">
<div class="acf-pref-test__option-header">
<span class="acf-pref-test__option-dot" style="background:var(--chart-1);"></span>
<span class="acf-pref-test__option-name">Option A</span>
</div>
<div class="acf-pref-test__mockup"><div class="acf-pref-test__mockup-inner"><img src="option-a.png" alt="Option A"></div></div>
</div>
</div>
</div>
<div class="acf-pref-test__outcome">
<p class="acf-pref-test__outcome-label">Outcome</p>
<p class="acf-pref-test__outcome-text">Option A was chosen by 68% of participants.</p>
</div>
</div>
</section>/* single-portfolio.php — donut chart: no JS, no SVG */
.acf-pref-test__donut {
width: 166px; height: 166px; border-radius: 50%;
-webkit-mask: radial-gradient(circle, transparent 52px, black 53px);
mask: radial-gradient(circle, transparent 52px, black 53px);
}
.acf-pref-test__legend { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-xs); width: 100%; }
.acf-pref-test__legend-item { display: flex; align-items: center; gap: var(--space-xs); }
.acf-pref-test__legend-dot { width: 8px; height: 8px; border-radius: 2px; flex-shrink: 0; }
.acf-pref-test__outcome { padding: var(--space-l); background: var(--surface-branding-primary-10); border-radius: var(--radius-xs); border-left: 3px solid var(--surface-branding-primary); }
.acf-pref-test__outcome-label { font-size: 12px; font-weight: var(--fw-semibold); color: var(--text-on-dark-secondary); text-transform: uppercase; letter-spacing: 4px; margin: 0 0 var(--space-xs); }
.acf-pref-test__outcome-text { font-size: var(--text-m); color: var(--text-on-dark-tertiary); line-height: var(--lh-xs); margin: 0; }<?php
// preference_testing.php — build the donut's conic-gradient from the percentages
$dot_colors = [ 'var(--chart-1)', 'var(--chart-2)', 'var(--chart-3)', 'var(--chart-4)' ];
$stops = [];
$cursor = 0.0;
foreach ( $options_data as $i => $opt ) {
$pct = (float) preg_replace( '/[^0-9.]/', '', $opt['pct'] ?? '' );
$end = round( $cursor + $pct, 4 );
$stops[] = ( $dot_colors[ $i ] ?? 'var(--chart-1)' ) . ' ' . round( $cursor, 4 ) . "% {$end}%";
$cursor = $end;
}
if ( $cursor < 100 ) $stops[] = "var(--surface-dominant) {$cursor}% 100%";
$conic = 'conic-gradient(from 0deg, ' . implode( ', ', $stops ) . ')';
?>
<div class="acf-pref-test__donut" style="background:<?php echo esc_attr( $conic ); ?>;"></div>