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

13 KiB
Raw Blame History

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:

cover_image:
    type: filepicker
    label: Cover Image
    preview_images: true
    folder: '@self'
    accept:
        - image/*

New 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 📍)

<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

.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)

{% 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

.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)

(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

/* 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