20 KiB
UI/UX Alignment 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: Unify three micro-interaction patterns across the site: back navigation pills, card hover lift, and a map-to-card flash highlight.
Architecture: CSS-first — shared .back-pill class drives visual consistency; entry card markup is collapsed from a two-level article > a to a flat <a> to align hover targets across all three card types; map flash is a short CSS keyframe triggered by a JS-added class.
Tech Stack: Twig templates, vanilla CSS custom properties, vanilla JS, Playwright for tests.
Global Constraints
- Dev server:
http://localhost:8081— must be running (make start) before any Playwright run - Playwright:
npx playwright test --project=chromium tests/ui/<file>.spec.js— always run the affected spec after changes - Demo data required for story/map tests:
make demo-load - All CSS uses design tokens from
user/themes/intotheeast/css/tokens.css— never hard-code colours --color-paper: #1A1814,--color-canvas: #22201B,--color-ink: #EDE8DF— the site is dark-themed--site-header-height: 60px— fixed pills must clear the site nav- Never read
.envdirectly
File Map
| File | What changes |
|---|---|
user/themes/intotheeast/css/style.css |
Add .back-pill class; remove duplicate .story-escape block; migrate .entry-card-inner hover rules to .entry-card; add uniform card hover lift; add @keyframes card-highlight |
user/themes/intotheeast/templates/story.html.twig |
Add class="back-pill" to story-footer back link (line 61) |
user/themes/intotheeast/templates/entry.html.twig |
Add fixed top back pill before <article class="entry">; replace footer teal link with .back-pill; add .entry-back-fixed CSS |
user/themes/intotheeast/templates/trip.html.twig |
Collapse <article class="entry-card"><a class="entry-card-inner"> to <a class="entry-card"> for both card variants; update marker click handler with flash delay |
tests/ui/dailies.spec.js |
Update T2 selectors from .entry-card a[href*="..."] to .entry-card[href*="..."]; add T6 (back pills on entry page) |
tests/ui/maps.spec.js |
Add M7 (marker click adds is-highlighted class) |
Task 1: CSS foundation — .back-pill, card hover lift, flash keyframe, story-escape cleanup
Files:
- Modify:
user/themes/intotheeast/css/style.css
Interfaces:
-
Produces:
.back-pillclass (surface pill),.entry-card.is-highlightedanimation, uniform hover lift on.trip-card:hover,.entry-card:hover,.story-card:hover -
Step 1: Add
.back-pillsurface pill class
Find the /* ── Back to top pill ── section (around line 1217). Insert the following block immediately before it:
/* ── Back pill (shared navigation pill component) ───────────────────── */
.back-pill {
display: inline-flex;
align-items: center;
font-family: var(--font-ui);
font-size: var(--text-sm);
font-weight: 500;
color: var(--color-ink);
text-decoration: none;
background: var(--color-canvas);
border: 1px solid var(--color-border);
border-radius: var(--radius-full);
padding: 0.4rem 0.9rem;
transition: border-color 0.15s, color 0.15s;
cursor: pointer;
}
.back-pill:hover { border-color: var(--color-accent); color: var(--color-accent); }
- Step 2: Remove the duplicate
.story-escapeblock
Around line 958 there is a /* ── Story page escape link ── section with a .story-escape rule that is overridden later by the story-section block. Remove this entire section:
/* ── Story page escape link ──────────────────────────────────────────────────── */
.story-escape {
position: fixed;
top: var(--space-5);
left: var(--space-5);
z-index: 200;
font-size: var(--text-sm);
font-weight: 500;
color: var(--color-ink);
text-decoration: none;
background: rgba(0,0,0,0.6);
padding: var(--space-2) var(--space-4);
border-radius: var(--radius-full);
backdrop-filter: blur(4px);
}
.story-escape:hover { color: var(--color-accent); }
The authoritative .story-escape definition remains in the /* ── Story pages ── section (~line 1056).
- Step 3: Add uniform card hover lift + fix story-card transition
Find the .trip-card:hover rule (in /* ── Past trips archive ──). After the existing .trip-card:hover block, add:
.trip-card:hover,
.entry-card:hover,
.story-card:hover {
background: var(--color-surface-raised);
}
Then find .story-card in the /* ── Stories listing ── section and add background 0.15s to its existing transition so the lift animates:
/* Before: */
.story-card {
...
transition: box-shadow 0.2s;
}
/* After: */
.story-card {
...
transition: box-shadow 0.2s, background 0.15s;
}
- Step 4: Add map flash keyframe
At the end of the /* ── Feed ── section (after .entry-card and related rules, around line 210), add:
@keyframes card-highlight {
0% { background-color: color-mix(in srgb, var(--color-accent) 12%, transparent); }
100% { background-color: transparent; }
}
.entry-card.is-highlighted {
animation: card-highlight 0.7s ease-out forwards;
}
- Step 5: Verify no JS errors on the site
npx playwright test --project=chromium tests/ui/maps.spec.js -g "M1:"
Expected: PASS (map page loads without errors — confirms CSS is valid).
- Step 6: Commit
git add user/themes/intotheeast/css/style.css
git commit -m "feat: add back-pill class, card hover lift, flash keyframe; remove duplicate story-escape"
Task 2: Story template — apply .back-pill to body back link
Files:
- Modify:
user/themes/intotheeast/templates/story.html.twig
Interfaces:
-
Consumes:
.back-pillclass from Task 1 -
Step 1: Write the failing test
Add to tests/ui/stories.spec.js:
// ── S7: Story body back link is styled as a back-pill ────────────────────────
test('S7: story body back link has back-pill class', async ({ page }) => {
await page.goto('/trips/italy-2025/stories/val-dorcia-dawn');
await expect(page.locator('.story-hero__img')).toBeVisible({ timeout: 8000 });
// Scroll past the hero to reveal the story body
await page.evaluate(() => window.scrollBy(0, window.innerHeight * 1.5));
await page.waitForTimeout(300);
const bodyBack = page.locator('.story-footer .back-pill');
await expect(bodyBack).toBeAttached();
await expect(bodyBack).toHaveText(/← Back/);
});
- Step 2: Run test to verify it fails
npx playwright test --project=chromium tests/ui/stories.spec.js -g "S7:"
Expected: FAIL — "locator('.story-footer .back-pill')" found 0 elements.
- Step 3: Apply
.back-pillto the story footer back link + fix.story-footer aconflict
In story.html.twig, the story footer currently reads:
<footer class="story-footer">
<a href="{{ page.parent().url }}" onclick="if(history.length > 1){ history.back(); return false; }">← Back</a>
Change the <a> to:
<footer class="story-footer">
<a class="back-pill" href="{{ page.parent().url }}" onclick="if(history.length > 1){ history.back(); return false; }">← Back</a>
Then in style.css, find .story-footer a and add :not(.back-pill) so it no longer overrides the pill colour:
/* Before: */
.story-footer a {
font-family: var(--font-ui);
font-size: var(--text-sm);
color: var(--color-accent);
text-decoration: none;
}
/* After: */
.story-footer a:not(.back-pill) {
font-family: var(--font-ui);
font-size: var(--text-sm);
color: var(--color-accent);
text-decoration: none;
}
- Step 4: Run test to verify it passes
npx playwright test --project=chromium tests/ui/stories.spec.js -g "S7:"
Expected: PASS.
- Step 5: Run full stories suite to check no regressions
npx playwright test --project=chromium tests/ui/stories.spec.js
Expected: All S1–S7 pass.
- Step 6: Commit
git add user/themes/intotheeast/templates/story.html.twig tests/ui/stories.spec.js
git commit -m "feat: apply back-pill class to story footer back link"
Task 3: Entry page — fixed top back pill + footer back pill
Files:
- Modify:
user/themes/intotheeast/templates/entry.html.twig - Modify:
user/themes/intotheeast/css/style.css - Modify:
tests/ui/dailies.spec.js
Interfaces:
-
Consumes:
.back-pillclass from Task 1 -
Step 1: Write the failing test
Add to tests/ui/dailies.spec.js:
const KNOWN_ENTRY = '/trips/japan-korea-2026/dailies/2026-03-25-1540-wheels-down-narita.entry';
// ── T6: Entry page has a fixed top back pill and a footer back pill ───────────
test('T6: entry page has fixed back pill at top and back pill in footer', async ({ page }) => {
await page.goto(KNOWN_ENTRY);
await expect(page.locator('article.entry')).toBeVisible();
// Fixed top pill (outside the article, before it)
const topPill = page.locator('.entry-back-fixed');
await expect(topPill).toBeVisible();
await expect(topPill).toHaveText(/← Back/);
// Footer pill
const footerPill = page.locator('.entry-footer .back-pill');
await expect(footerPill).toBeVisible();
await expect(footerPill).toHaveText(/← Back/);
});
- Step 2: Run test to verify it fails
npx playwright test --project=chromium tests/ui/dailies.spec.js -g "T6:"
Expected: FAIL — .entry-back-fixed not found.
- Step 3: Add fixed top back pill to entry template
In entry.html.twig, the content block currently starts with <article class="entry">. Add the fixed pill immediately before it:
<a class="back-pill entry-back-fixed" href="{{ page.parent().url }}" onclick="if(history.length > 1){ history.back(); return false; }">← Back</a>
<article class="entry">
- Step 4: Replace footer teal text link with
.back-pill
The current entry footer (around line 124 of entry.html.twig):
<footer class="entry-footer">
<a href="{{ page.parent().url }}" onclick="if(history.length>1){event.preventDefault();history.back()}">← Back</a>
</footer>
Replace with:
<footer class="entry-footer">
<a class="back-pill" href="{{ page.parent().url }}" onclick="if(history.length > 1){ history.back(); return false; }">← Back</a>
</footer>
- Step 5: Add
.entry-back-fixedpositioning to CSS
In style.css, in the /* ── Single entry ── section, add after the existing .entry-hero rules:
.entry-back-fixed {
position: fixed;
top: calc(var(--site-header-height) + var(--space-3));
left: var(--space-4);
z-index: 100;
}
- Step 6: Run test to verify it passes
npx playwright test --project=chromium tests/ui/dailies.spec.js -g "T6:"
Expected: PASS.
- Step 7: Run full dailies suite to check no regressions
npx playwright test --project=chromium tests/ui/dailies.spec.js
Expected: T1–T6 all pass.
- Step 8: Commit
git add user/themes/intotheeast/templates/entry.html.twig user/themes/intotheeast/css/style.css tests/ui/dailies.spec.js
git commit -m "feat: add fixed top and footer back pills to entry page"
Task 4: Entry card structural refactor + CSS migration
Collapse the two-level <article class="entry-card"><a class="entry-card-inner"> to a flat <a class="entry-card">, matching the structure of trip and story cards.
Files:
- Modify:
user/themes/intotheeast/templates/trip.html.twig - Modify:
user/themes/intotheeast/css/style.css - Modify:
tests/ui/dailies.spec.js
Interfaces:
-
Produces:
.entry-cardis now an<a>element;id,data-type,data-lat,data-lngattributes remain on the card root;.entry-card-innerclass is eliminated -
Step 1: Update T2 test selectors before touching the templates
In tests/ui/dailies.spec.js, find the T2 test and replace:
// OLD — inner <a> is nested inside .entry-card
const newerCard = page.locator(`.entry-card a[href*="${NEWER_SLUG}"]`);
const olderCard = page.locator(`.entry-card a[href*="${OLDER_SLUG}"]`);
await expect(newerCard).toBeVisible();
await expect(olderCard).toBeVisible();
const newerIdx = await newerCard.evaluate(el => {
return [...document.querySelectorAll('.entry-card')].findIndex(c => c.contains(el));
});
const olderIdx = await olderCard.evaluate(el => {
return [...document.querySelectorAll('.entry-card')].findIndex(c => c.contains(el));
});
With:
// NEW — .entry-card is itself the <a>
const newerCard = page.locator(`.entry-card[href*="${NEWER_SLUG}"]`);
const olderCard = page.locator(`.entry-card[href*="${OLDER_SLUG}"]`);
await expect(newerCard).toBeVisible();
await expect(olderCard).toBeVisible();
const newerIdx = await newerCard.evaluate(el => {
return [...document.querySelectorAll('.entry-card')].findIndex(c => c === el);
});
const olderIdx = await olderCard.evaluate(el => {
return [...document.querySelectorAll('.entry-card')].findIndex(c => c === el);
});
- Step 2: Run T2 to verify it fails (not yet refactored)
npx playwright test --project=chromium tests/ui/dailies.spec.js -g "T2:"
Expected: FAIL — .entry-card[href*="..."] finds 0 elements (the href is on the inner <a>, not the article).
- Step 3: Refactor journal entry card markup in
trip.html.twig
Find the journal card block:
{% if item.type == 'journal' %}
<article class="entry-card" id="entry-{{ entry.slug }}" data-type="journal" data-lat="{{ entry.header.lat }}" data-lng="{{ entry.header.lng }}">
<a class="entry-card-inner" href="{{ entry.url }}">
Replace with:
{% if item.type == 'journal' %}
<a class="entry-card" id="entry-{{ entry.slug }}" data-type="journal" data-lat="{{ entry.header.lat }}" data-lng="{{ entry.header.lng }}" href="{{ entry.url }}">
And close the card with </a> instead of </a></article>. The closing tags currently are:
</a>
</article>
Replace with:
</a>
- Step 4: Refactor story-in-feed card markup in
trip.html.twig
Find the story-in-feed card block:
<article class="entry-card entry-card--story" id="entry-{{ entry.slug }}" data-type="story">
<a class="entry-card-inner" href="{{ entry.url }}">
Replace with:
<a class="entry-card entry-card--story" id="entry-{{ entry.slug }}" data-type="story" href="{{ entry.url }}">
And its closing tags (currently </a></article>) become:
</a>
- Step 5: Migrate
.entry-card-innerCSS rules to.entry-card
In style.css, find the /* ── Feed ── section. Currently:
.entry-card { border-bottom: 1px solid var(--color-border); padding-bottom: var(--space-12); }
.entry-card-inner {
display: block;
text-decoration: none;
color: inherit;
}
Replace with (merge inner styles onto card, .entry-card-inner is eliminated):
.entry-card {
display: block;
text-decoration: none;
color: inherit;
border-bottom: 1px solid var(--color-border);
padding-bottom: var(--space-12);
transition: background 0.15s;
}
Then find the two .entry-card-inner:hover rules and rename them to .entry-card:hover:
/* Before: */
.entry-card-inner:hover .entry-card-photo img { transform: scale(1.04); }
/* After: */
.entry-card:hover .entry-card-photo img { transform: scale(1.04); }
/* Before: */
.entry-card-inner:hover .entry-title { color: var(--color-accent); }
/* After: */
.entry-card:hover .entry-title { color: var(--color-accent); }
- Step 6: Run T2 to verify it passes
npx playwright test --project=chromium tests/ui/dailies.spec.js -g "T2:"
Expected: PASS.
- Step 7: Run full test suites to check no regressions
npx playwright test --project=chromium tests/ui/dailies.spec.js tests/ui/trip-filter.spec.js tests/ui/maps.spec.js
Expected: T1–T6, F1–F7, M1–M6 all pass.
- Step 8: Commit
git add user/themes/intotheeast/templates/trip.html.twig user/themes/intotheeast/css/style.css tests/ui/dailies.spec.js
git commit -m "refactor: collapse entry card article+a to flat <a>, unify hover targets across card types"
Task 5: Map flash — JS update + test
Files:
- Modify:
user/themes/intotheeast/templates/trip.html.twig - Modify:
tests/ui/maps.spec.js
Interfaces:
-
Consumes:
.entry-card.is-highlightedCSS animation from Task 1;id="entry-{{ slug }}"on<a class="entry-card">from Task 4 -
Step 1: Write the failing test
Add to tests/ui/maps.spec.js:
// ── M7: Clicking a trip-page map marker adds is-highlighted to the entry card ──
test('M7: clicking map marker briefly highlights the corresponding entry card', async ({ page }) => {
await page.goto('/trips/japan-korea-2026');
// Wait for map canvas and at least one marker
await expect(page.locator('#trip-map canvas.maplibregl-canvas')).toBeVisible({ timeout: 10000 });
await expect(page.locator('.maplibregl-marker').first()).toBeVisible({ timeout: 15000 });
// Click the first marker
await page.locator('.maplibregl-marker').first().click();
// Within 500ms of click + delay, one entry-card should have is-highlighted
await expect(page.locator('.entry-card.is-highlighted')).toBeVisible({ timeout: 1500 });
});
- Step 2: Run test to verify it fails
npx playwright test --project=chromium tests/ui/maps.spec.js -g "M7:"
Expected: FAIL — .entry-card.is-highlighted not found.
- Step 3: Update the marker click handler in
trip.html.twig
Find the existing marker click handler in trip.html.twig:
el.addEventListener('click', function () {
var card = document.getElementById('entry-' + entry.slug);
if (card) card.scrollIntoView({ behavior: 'smooth', block: 'center' });
});
Replace with:
el.addEventListener('click', function () {
var card = document.getElementById('entry-' + entry.slug);
if (!card) return;
card.scrollIntoView({ behavior: 'smooth', block: 'center' });
setTimeout(function () {
card.classList.add('is-highlighted');
setTimeout(function () { card.classList.remove('is-highlighted'); }, 700);
}, 350);
});
- Step 4: Run test to verify it passes
npx playwright test --project=chromium tests/ui/maps.spec.js -g "M7:"
Expected: PASS.
- Step 5: Run full maps suite
npx playwright test --project=chromium tests/ui/maps.spec.js
Expected: M1–M7 all pass.
- Step 6: Run all affected suites for final check
npx playwright test --project=chromium tests/ui/dailies.spec.js tests/ui/trip-filter.spec.js tests/ui/maps.spec.js tests/ui/stories.spec.js
Expected: All pass.
- Step 7: Commit
git add user/themes/intotheeast/templates/trip.html.twig tests/ui/maps.spec.js
git commit -m "feat: add map-to-card flash highlight on marker click"