Files
intotheeast-com/docs/working/specs/2026-06-24-frontend-polish-design.md
T

314 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Frontend Polish Design Spec
> **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:subagent-driven-development` (recommended) or `superpowers:executing-plans` to implement this plan task-by-task.
**Goal:** Elevate the visual identity and consistency of the five primary page templates — home, trip, trips listing, individual entry, and story — without touching the map page or dailies index. Improvements fall into three categories: visual identity (header, stats, pills), typographic consistency (emoji removal), and content pages (trip cards, story transitions).
**Architecture:** Mostly CSS changes in `style.css`. Two Twig templates need small additions (`trips.html.twig`, `story.html.twig`). One blueprint gets a new field (`blueprints/trip.yaml`). One partial gets emoji removed (`partials/entry-journal.html.twig`). No new JS libraries.
**Already completed as part of this session:**
- `entry.html.twig` unified with `partials/entry-journal.html.twig` — hero image removed, custom lightbox replaced with PhotoSwipe, dead CSS stripped
- See git log for the entry template rewrite commit
---
## Global Constraints
- All changes in `user/` — commit with `git -C user`
- All new CSS must use token variables — never hardcode hex values
- No new JS libraries or CDN dependencies
- Changes must degrade gracefully if optional data (cover image, location) is absent
- `prefers-reduced-motion` must be respected for any new animations
---
## A — Trip Cards: Cover Image
### Problem
The trips listing (`/trips`) renders a vertical stack of text-only cards: title, date range, entry count. For a travel blog, the archive is the viewer's first encounter with trips they haven't visited — showing no visual context is a significant missed opportunity.
### Design decision
Each `.trip-card` gets a full-width banner image above the existing text. Aspect ratio 3:1 — wide enough to suggest landscape/geography without dominating a card in a list.
```
┌─────────────────────────────────────────┐
│ [cover image — 3:1 aspect ratio] │
├─────────────────────────────────────────┤
│ Japan & South Korea │
│ Apr 2026 — Jun 2026 · 24 entries │
└─────────────────────────────────────────┘
```
### Image resolution
The card is constrained to `--content-width` (720px). Use `cropResize(720, 240)` for the 3:1 crop.
### Image source priority
1. `trip.header.cover_image` — a filename from the trip page's own media (explicit, curated)
2. First image from the first published journal entry in the trip (automatic fallback)
3. No image — card degrades to text-only (existing layout, unchanged)
### Blueprint change
Add a `cover_image` field to `user/themes/intotheeast/blueprints/trip.yaml`:
```yaml
cover_image:
type: filepicker
label: Cover Image
preview_images: true
folder: '@self'
accept:
- image/*
```
### New CSS
```css
.trip-card-cover {
aspect-ratio: 3 / 1;
overflow: hidden;
border-radius: var(--radius-md) var(--radius-md) 0 0;
background: var(--color-border);
margin: calc(-1 * var(--space-6)) calc(-1 * var(--space-6)) var(--space-5);
}
.trip-card-cover img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
transition: transform 0.45s ease;
}
.trip-card:hover .trip-card-cover img { transform: scale(1.04); }
```
The negative margin pulls the image flush to the card edges while the card keeps its existing padding for the text below.
---
## B — Replace Emoji Icons
### Problem
`partials/entry-journal.html.twig` (which now also powers `entry.html.twig`) uses `📍` for location and emoji for weather conditions (☀️, 🌧️, etc.). These are OS-rendered, variable in size, and break the typographic consistency of the warm-dark palette.
### Design decision
- **Location:** Replace `📍` with a minimal inline SVG mappin. 16×16, `currentColor`, single path.
- **Weather:** Drop the emoji prefix entirely. The text description ("Sunny", "Rain", "Partly cloudy") is the information — the emoji is decoration. Text-only is cleaner and the muted color already signals it as secondary metadata.
### SVG mappin (inline, replaces `📍`)
```html
<svg width="12" height="14" viewBox="0 0 12 14" fill="currentColor" aria-hidden="true" style="flex-shrink:0;margin-top:1px">
<path d="M6 0C3.24 0 1 2.24 1 5c0 3.75 5 9 5 9s5-5.25 5-9c0-2.76-2.24-5-5-5zm0 6.75A1.75 1.75 0 1 1 6 3.25a1.75 1.75 0 0 1 0 3.5z"/>
</svg>
```
---
## C — Header Identity
### Problem
The site header reads like a product app: text logo left, two nav links right, 60px tall, 3px teal stripe on top. The brand "into the east" at `--text-lg` with `-0.01em` tracking is timid. The content pages are atmospheric and cinematic; the header is functional and forgettable.
### Design decision
Two targeted CSS-only changes:
1. **Site title tracking:** Increase from `--text-lg` to `--text-xl`, set `letter-spacing: 0.06em`. Wider tracking on a dark background is a deliberate typographic mark — it reads as a designed wordmark rather than placeholder text.
2. **Accent stripe:** Increase from `3px` to `4px`. Apply a two-stop gradient along the 90deg axis: `linear-gradient(90deg, var(--color-accent), var(--color-accent-hover))`. This gives the stripe direction (reads left-to-right like a journey) and signals it was chosen, not defaulted.
No layout change, no template change, no height change.
---
## D — Story Opening Transition
### Problem
After the Ken Burns hero and the 40vh spacer, `story.html.twig` begins the body content immediately with prose. There is no visual breath between the cinematic full-screen image and the reading experience. The reader has no bearing — no confirmation of where they are or when.
### Design decision
Add a `.story-opener` block at the top of `.story-body`, before `{{ page.content|raw }}`. It displays the location and formatted date string centered, separated from the prose below by a thin ruled line.
```
Sorano, Italy · 1416 June 2026
────────────────────────────────
[prose begins here]
```
Data comes from `location` and `date_str`, already computed at the top of `story.html.twig`. If both are empty the opener renders nothing (zero markup visible).
The opener fades in using the existing `storyReveal` keyframe (`filter: blur → 0`, `opacity: 0 → 1`, `translateY(22px → 0)`) with a 0.8s delay so it appears after the hero title animation completes.
### New CSS
```css
.story-opener {
text-align: center;
padding-bottom: var(--space-12);
margin-bottom: var(--space-12);
border-bottom: 1px solid var(--color-border);
opacity: 0;
animation: storyReveal 0.9s cubic-bezier(.16,1,.3,1) 0.8s both;
}
.story-opener__text {
font-family: var(--font-ui);
font-size: var(--text-sm);
color: var(--color-ink-muted);
letter-spacing: 0.06em;
text-transform: uppercase;
}
@media (prefers-reduced-motion: reduce) {
.story-opener { opacity: 1; animation: none; }
}
```
### Template addition (in `story.html.twig`, inside `.story-body`, before `page.content`)
```twig
{% if location or date_str %}
<div class="story-opener">
<span class="story-opener__text">
{{- date_str -}}
{%- if location and date_str %} · {% endif -%}
{{- location -}}
</span>
</div>
{% endif %}
```
---
## E — Reading Progress Bar
### Problem
Story pages are long-form — the longest may scroll for several minutes of reading. There is no visual feedback about progress through the piece. This is a small but meaningful quality signal on an immersive reading experience.
### Design decision
A 2px teal bar fixed to the bottom edge of the site header (`top: var(--site-header-height)`), filling left-to-right as the reader scrolls through `.story-body`. Progress is calculated relative to the story body element (not the full page including the hero), so the bar reads 0% when the hero exits and 100% when the last line of content reaches the viewport bottom.
The bar is invisible before the story body enters view. It does not render at all if `prefers-reduced-motion` is set — there should be no static `width: 0` bar for reduced-motion users.
### New CSS
```css
.story-progress {
position: fixed;
top: var(--site-header-height);
left: 0;
height: 2px;
width: 0%;
background: var(--color-accent);
z-index: 200;
pointer-events: none;
will-change: width;
}
```
### JS logic (no transition — rAF-driven for smoothness)
```javascript
(function () {
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
var bar = document.getElementById('story-progress');
var body = document.querySelector('.story-body');
if (!bar || !body) return;
function update() {
var rect = body.getBoundingClientRect();
var total = body.offsetHeight - window.innerHeight;
var scrolled = -rect.top;
var pct = total > 0 ? Math.min(100, Math.max(0, (scrolled / total) * 100)) : 0;
bar.style.width = pct.toFixed(1) + '%';
}
window.addEventListener('scroll', update, { passive: true });
update();
})();
```
The element `<div class="story-progress" id="story-progress"></div>` is added to `story.html.twig` immediately after the opening `{% block content %}`.
---
## F — Pill Shape Differentiation
### Problem
All interactive pills use `border-radius: 9999px` regardless of their role. Back-navigation pills, filter buttons, panel toggles, sort toggles — they all look identical, which collapses the visual grammar. A reader cannot tell at a glance whether tapping a pill will navigate them away or toggle a filter.
### Design decision
Establish a two-shape grammar:
| Role | Shape | Classes |
|---|---|---|
| Navigation (go somewhere, leave the page) | Full pill `9999px` | `.back-pill`, `.story-escape`, `.story-totop` |
| Controls (toggle, filter, sort in place) | Rounded rect `var(--radius-sm)` = 4px | `.trip-filter-btn`, `.trip-stats-btn` |
| Panel toggles (secondary, in-place) | Full pill (keep — less prominent than controls) | `.trip-panel-toggle` |
CSS-only change. `.back-pill`, `.story-escape`, `.story-totop` are unchanged. Only `.trip-filter-btn` and `.trip-stats-btn` change from `border-radius: var(--radius-full)` to `border-radius: var(--radius-sm)`.
---
## G — Stats: Field Notes Treatment
### Problem
`.stat-block` renders as a bordered card with a `background: var(--color-canvas)` surface, box shadow, and teal accent numbers. This reads as a metrics dashboard — every SaaS product uses this pattern. For a travel journal, numbers like "1,847 km" and "3 countries" should feel earned and written, not computed and charted.
### Design decision
Two changes:
1. **Remove the box.** Drop `background`, `border`, and `box-shadow` from `.stat-block`. Replace with a `border-left: 2px solid var(--color-accent)` and `padding-left: var(--space-4)`. Text left-aligns. Numbers feel like notes in a margin, not cells in a table.
2. **Change number color.** `stat-value` moves from `var(--color-accent)` to `var(--color-ink)`. Teal numbers on dark are a SaaS color decision. Cream numbers on dark with a teal accent stripe are a traveler's notation.
The teal accent is now only the left rule — restrained, singular.
### CSS change
```css
/* Before */
.stat-block {
background: var(--color-canvas);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
padding: var(--space-6) var(--space-5);
text-align: center;
box-shadow: var(--shadow-sm);
}
.stat-value {
color: var(--color-accent);
...
}
/* After */
.stat-block {
border-left: 2px solid var(--color-accent);
padding: var(--space-2) 0 var(--space-2) var(--space-4);
text-align: left;
}
.stat-value {
color: var(--color-ink);
...
}
```
The `.trip-stats-grid` and `.stats-grid` gap/column settings are unchanged.
---
## Verification Checklist
After full implementation, check each page:
| Page | Check |
|---|---|
| `/trips` | Trip cards show cover image (or degrade to text-only gracefully); hover scales image |
| `/trips/<any-trip>` | Stats panel shows left-rule style, cream numbers; filter buttons are rounded-rect |
| `/trips/<any-trip>/dailies/<any-entry>` (standalone) | Photo strip renders via PhotoSwipe, no broken lightbox; no hero image at top |
| `/trips/<any-trip>/<any-story>` | Opener block shows location + date; progress bar fills while scrolling; no bar if reduced-motion |
| Any page | Header title has wider tracking; accent stripe is slightly thicker with gradient |
| Any page with journal entries | Location shows SVG pin; weather shows text only, no emoji |