# 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 ``` --- ## 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 · 14–16 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 %}
{{- date_str -}} {%- if location and date_str %} · {% endif -%} {{- location -}}
{% 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 `
` 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/` | Stats panel shows left-rule style, cream numbers; filter buttons are rounded-rect | | `/trips//dailies/` (standalone) | Photo strip renders via PhotoSwipe, no broken lightbox; no hero image at top | | `/trips//` | 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 |