Files
intotheeast-com/docs/working/plans/2026-06-18-ui-redesign.md
T

46 KiB
Raw Blame History

UI Redesign 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: Redesign the Into the East travel blog UI using the design spec in user/docs/design/design-spec.md — introducing DM Serif Display + DM Sans fonts, a deep teal design system, full-bleed entry card photos, and polished mobile UX across all pages.

Architecture: Pure CSS + HTML/Twig changes on top of existing Grav CMS stack. New tokens.css file holds all design tokens as CSS custom properties. Existing style.css is rewritten to use those tokens. No new JS frameworks, no build pipeline.

Tech Stack: Grav CMS (PHP/Twig), Vanilla CSS (custom properties), Vanilla JS, Google Fonts CDN (DM Serif Display + DM Sans), Leaflet.js (unchanged)

Global Constraints

  • No JS framework — all interactivity stays vanilla JS
  • No build pipeline — CSS ships as plain files loaded by Grav
  • No new Grav plugins — only modify existing theme + content files
  • Grav file paths: theme is at user/themes/intotheeast/, pages at user/pages/
  • Design tokens source of truth: user/themes/intotheeast/css/tokens.css
  • Accent color: #1F6B5A (deep teal) — used everywhere #0066cc is today
  • Font stack display: 'DM Serif Display', Georgia, serif
  • Font stack UI: 'DM Sans', -apple-system, BlinkMacSystemFont, sans-serif
  • All interactive elements: min-height: 44px
  • Content max-width: 720px
  • Only edit files inside user/ — never touch the Grav core
  • Verify in browser at each task: http://100.96.115.96:8081

Task 1: Design tokens file + font loading

Files:

  • Create: user/themes/intotheeast/css/tokens.css
  • Modify: user/themes/intotheeast/templates/partials/base.html.twig (add font preconnect + tokens import)

Interfaces:

  • Produces: all CSS custom properties consumed by Tasks 28

  • Step 1: Create tokens.css

Create user/themes/intotheeast/css/tokens.css with this exact content:

:root {
    /* ── Colors ─────────────────────────────────────────────── */
    --color-ink:          #17171A;
    --color-ink-2:        #4A4850;
    --color-ink-muted:    #9896A0;
    --color-paper:        #F7F5F2;
    --color-canvas:       #FFFFFF;
    --color-border:       #E8E6E3;
    --color-border-soft:  #F0EDEA;
    --color-accent:       #1F6B5A;
    --color-accent-hover: #185647;
    --color-accent-light: #EBF5F2;
    --color-accent-on:    #FFFFFF;

    /* ── Fonts ───────────────────────────────────────────────── */
    --font-display: 'DM Serif Display', Georgia, serif;
    --font-ui:      'DM Sans', -apple-system, BlinkMacSystemFont, sans-serif;

    /* ── Type scale ──────────────────────────────────────────── */
    --text-xs:   0.75rem;
    --text-sm:   0.875rem;
    --text-base: 1rem;
    --text-md:   1.125rem;
    --text-lg:   1.375rem;
    --text-xl:   1.75rem;
    --text-2xl:  2.25rem;
    --text-3xl:  3rem;

    /* ── Leading ─────────────────────────────────────────────── */
    --leading-tight:  1.2;
    --leading-snug:   1.35;
    --leading-normal: 1.65;

    /* ── Spacing (4px grid) ──────────────────────────────────── */
    --space-1:  0.25rem;
    --space-2:  0.5rem;
    --space-3:  0.75rem;
    --space-4:  1rem;
    --space-5:  1.25rem;
    --space-6:  1.5rem;
    --space-8:  2rem;
    --space-10: 2.5rem;
    --space-12: 3rem;
    --space-16: 4rem;

    /* ── Radius ──────────────────────────────────────────────── */
    --radius-sm:   4px;
    --radius-md:   8px;
    --radius-lg:   12px;
    --radius-full: 9999px;

    /* ── Shadows ─────────────────────────────────────────────── */
    --shadow-sm: 0 1px 3px rgba(0,0,0,0.08);
    --shadow-md: 0 4px 12px rgba(0,0,0,0.10);
    --shadow-lg: 0 8px 24px rgba(0,0,0,0.14);

    /* ── Layout ──────────────────────────────────────────────── */
    --content-width:      720px;
    --site-header-height: 60px;
}
  • Step 2: Add font loading + tokens import to base.html.twig

In user/themes/intotheeast/templates/partials/base.html.twig, replace the <head> block with:

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% if page.title %}{{ page.title }} | {% endif %}{{ site.title }}</title>
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,400;0,9..40,500;0,9..40,600;1,9..40,400&family=DM+Serif+Display:ital@0;1&display=swap" rel="stylesheet">
    <link rel="stylesheet" href="{{ url('theme://css/tokens.css') }}">
    <link rel="stylesheet" href="{{ url('theme://css/style.css') }}">
</head>
  • Step 3: Verify fonts load

Open http://100.96.115.96:8081/tracker in the browser. Open DevTools → Network → filter "fonts.gstatic.com". Both DM_Sans and DM_Serif_Display should appear in the network log. If not, check the <link> href in page source.

  • Step 4: Commit
cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast/user
git add themes/intotheeast/css/tokens.css themes/intotheeast/templates/partials/base.html.twig
git commit -m "feat: add design tokens and DM font loading"

Task 2: Rewrite global styles with design tokens

Files:

  • Modify: user/themes/intotheeast/css/style.css — global reset and base styles only (header, nav, body, site-main). Per-component CSS is updated in Tasks 38.

Interfaces:

  • Consumes: all tokens from tokens.css

  • Produces: base body/typography/layout styles consumed by all templates

  • Step 1: Replace the top section of style.css

Open user/themes/intotheeast/css/style.css. Replace from line 1 through the end of the .site-main block (approximately lines 141) with:

*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

body {
    font-family: var(--font-ui);
    font-size: var(--text-base);
    line-height: var(--leading-normal);
    color: var(--color-ink);
    background: var(--color-paper);
    -webkit-font-smoothing: antialiased;
}

.site-main {
    max-width: var(--content-width);
    margin: 0 auto;
    padding: var(--space-8) var(--space-5);
}

@media (min-width: 520px) {
    .site-main { padding: var(--space-10) var(--space-6); }
}
  • Step 2: Update the login form section to use tokens

Find the /* ── Login form ── section and update input/button colors from hardcoded to tokens. Replace the .login-form block with:

.login-form { max-width: 400px; margin: var(--space-8) auto; padding: 0 var(--space-4); }
.login-form .form-field { margin-bottom: var(--space-5); }
.login-form .form-label label { display: block; font-size: var(--text-sm); font-weight: 600; margin-bottom: var(--space-2); }
.login-form input[type="text"],
.login-form input[type="password"],
.login-form input[type="email"] {
    width: 100%;
    font-family: var(--font-ui);
    font-size: var(--text-base);
    padding: 0.75rem 1rem;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    min-height: 44px;
    background: var(--color-canvas);
    color: var(--color-ink);
}
.login-form input:focus {
    outline: 2px solid var(--color-accent);
    outline-offset: 1px;
    border-color: var(--color-accent);
}
.login-form .form-actions { margin-top: var(--space-6); display: flex; flex-direction: column; gap: var(--space-3); }
.login-form .button {
    display: block; width: 100%; text-align: center;
    padding: 0.85rem 1rem; min-height: 44px;
    border-radius: var(--radius-md); font-size: var(--text-base);
    font-family: var(--font-ui); font-weight: 600;
    cursor: pointer; border: none;
}
.login-form .button.primary { background: var(--color-accent); color: var(--color-accent-on); }
.login-form .button.primary:hover { background: var(--color-accent-hover); }
.login-form .button.secondary { background: #f0f0f0; color: #333; text-decoration: none; line-height: 44px; padding: 0 1rem; }
.login-form .rememberme { display: flex; align-items: center; gap: var(--space-2); font-size: var(--text-sm); }
  • Step 3: Verify base styles apply

Open http://100.96.115.96:8081. Confirm:

  • Page background is warm paper white (not pure white)

  • Body text uses DM Sans

  • No visual regressions on login page

  • Step 4: Commit

cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast/user
git add themes/intotheeast/css/style.css
git commit -m "feat: update global styles to use design tokens"

Task 3: Site header redesign

Files:

  • Modify: user/themes/intotheeast/templates/partials/base.html.twig (header HTML)
  • Modify: user/themes/intotheeast/css/style.css (header CSS section)

Interfaces:

  • Consumes: --font-display, --color-accent, --color-border, --color-ink-2

  • Produces: site header used by all pages

  • Step 1: Update header HTML in base.html.twig

Replace the existing <header> element (the <header class="site-header"> block) with:

<header class="site-header">
    <a class="site-title" href="{{ base_url_absolute }}">into the east</a>
    <nav class="site-nav" aria-label="Main navigation">
        <a href="{{ base_url_absolute }}/tracker"{% if page.url starts with '/tracker' or page.template == 'entry' %} aria-current="page"{% endif %}>Journal</a>
        <a href="{{ base_url_absolute }}/map"{% if page.url starts with '/map' %} aria-current="page"{% endif %}>Map</a>
        <a href="{{ base_url_absolute }}/stats"{% if page.url starts with '/stats' %} aria-current="page"{% endif %}>Stats</a>
    </nav>
</header>
  • Step 2: Replace the header CSS section

Find /* ── Feed ── comment in style.css. Replace everything from the start of the file up to (but not including) the Feed comment with the new header CSS:

/* ── Header ─────────────────────────────────────────────────────────────────── */

.site-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 0 var(--space-5);
    height: var(--site-header-height);
    background: var(--color-canvas);
    border-top: 3px solid var(--color-accent);
    border-bottom: 1px solid var(--color-border);
    position: sticky;
    top: 0;
    z-index: 100;
}

.site-title {
    font-family: var(--font-display);
    font-size: var(--text-lg);
    font-weight: 400;
    letter-spacing: -0.01em;
    text-decoration: none;
    color: var(--color-ink);
    line-height: 1;
}

.site-nav {
    display: flex;
    align-items: center;
    gap: var(--space-1);
}

.site-nav a {
    font-family: var(--font-ui);
    font-size: var(--text-sm);
    font-weight: 500;
    color: var(--color-ink-2);
    text-decoration: none;
    padding: var(--space-2) var(--space-3);
    border-radius: var(--radius-sm);
    min-height: 44px;
    display: inline-flex;
    align-items: center;
    transition: color 0.15s, background 0.15s;
}

.site-nav a:hover { color: var(--color-ink); background: var(--color-paper); }
.site-nav a[aria-current="page"] { color: var(--color-accent); font-weight: 600; }
  • Step 3: Verify header

Open http://100.96.115.96:8081/tracker. Verify:

  • Thin teal bar at the very top of the header
  • "into the east" title in DM Serif Display, slightly italic quality
  • Nav links in small DM Sans
  • Active page link is teal
  • Header sticks on scroll (sticky position)

Check mobile at 375px viewport: title and nav should both fit in one row. If they don't, reduce --text-lg to --text-md for the title on mobile via media query.

  • Step 4: Commit
cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast/user
git add themes/intotheeast/templates/partials/base.html.twig themes/intotheeast/css/style.css
git commit -m "feat: redesign site header with accent bar and DM Serif title"

Task 4: Entry feed card redesign

Files:

  • Modify: user/themes/intotheeast/templates/tracker.html.twig
  • Modify: user/themes/intotheeast/css/style.css (Feed section)

Interfaces:

  • Consumes: entry frontmatter fields: header.lat, header.lng, header.location_city, header.location_country, header.hero_image, media.images

  • Produces: entry cards used in the tracker feed

  • Step 1: Rewrite the entry card HTML in tracker.html.twig

Find the {% for entry in entries %} loop in tracker.html.twig. Replace the <article class="entry-card"> block (everything from <article through </article>) with:

<article class="entry-card">
    {% set hero = null %}
    {% if entry.header.hero_image and entry.media[entry.header.hero_image] is defined %}
        {% set hero = entry.media[entry.header.hero_image] %}
    {% elseif entry.media.images|length > 0 %}
        {% set hero = entry.media.images|first %}
    {% endif %}

    <a class="entry-card-inner" href="{{ entry.url }}">
        {% if hero %}
        <div class="entry-card-photo">
            <img src="{{ hero.cropResize(720, 405).url }}" alt="{{ entry.title }}" loading="lazy">
            <div class="entry-card-photo-overlay">
                <time class="entry-date-overlay" datetime="{{ entry.date|date('Y-m-d') }}">
                    {{ entry.date|date('d M Y')|upper }}
                </time>
                {% if entry.header.location_city or entry.header.location_country %}
                <span class="entry-location-overlay">
                    📍
                    {% if entry.header.location_city %}{{ entry.header.location_city|slice(0,20) }}{% endif %}
                    {% if entry.header.location_city and entry.header.location_country %}, {% endif %}
                    {% if entry.header.location_country %}{{ entry.header.location_country }}{% endif %}
                </span>
                {% endif %}
            </div>
        </div>
        {% else %}
        <div class="entry-card-textmeta">
            <time class="entry-date-plain" datetime="{{ entry.date|date('Y-m-d') }}">
                {{ entry.date|date('d M Y')|upper }}
            </time>
            {% if entry.header.location_city or entry.header.location_country %}
            <span class="entry-location-plain">
                📍
                {% if entry.header.location_city %}{{ entry.header.location_city }}{% endif %}
                {% if entry.header.location_city and entry.header.location_country %}, {% endif %}
                {% if entry.header.location_country %}{{ entry.header.location_country }}{% endif %}
            </span>
            {% endif %}
        </div>
        {% endif %}

        <div class="entry-card-body">
            <h2 class="entry-title">{{ entry.title }}</h2>
            <p class="entry-excerpt">{{ entry.summary }}</p>
            <span class="entry-read-more">Read entry →</span>
        </div>
    </a>
</article>
  • Step 2: Replace the Feed CSS section in style.css

Find /* ── Feed ── through to the end of the /* ── Location & Weather badges ── section. Replace it entirely with:

/* ── Feed ───────────────────────────────────────────────────────────────────── */

.feed { display: flex; flex-direction: column; gap: var(--space-12); }
.feed-empty { color: var(--color-ink-muted); font-style: italic; }

.entry-card { border-bottom: 1px solid var(--color-border); padding-bottom: var(--space-12); }

.entry-card-inner {
    display: block;
    text-decoration: none;
    color: inherit;
}

/* ── Card: photo variant ──────────────────────────────────────────────────── */

.entry-card-photo {
    position: relative;
    aspect-ratio: 16 / 9;
    border-radius: var(--radius-md);
    overflow: hidden;
    background: var(--color-border);
    margin-bottom: var(--space-5);
}

.entry-card-photo img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
    transition: transform 0.45s ease;
}

.entry-card-inner:hover .entry-card-photo img { transform: scale(1.04); }

.entry-card-photo-overlay {
    position: absolute;
    inset: auto 0 0 0;
    padding: var(--space-5) var(--space-4) var(--space-3);
    background: linear-gradient(to top, rgba(0,0,0,0.58) 0%, transparent 100%);
    display: flex;
    align-items: flex-end;
    gap: var(--space-3);
    flex-wrap: wrap;
}

.entry-date-overlay {
    font-size: var(--text-xs);
    font-weight: 700;
    letter-spacing: 0.08em;
    color: rgba(255,255,255,0.92);
}

.entry-location-overlay {
    font-size: var(--text-xs);
    color: rgba(255,255,255,0.85);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 180px;
}

/* ── Card: text-only variant ──────────────────────────────────────────────── */

.entry-card-textmeta {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    margin-bottom: var(--space-3);
    flex-wrap: wrap;
}

.entry-date-plain {
    font-size: var(--text-xs);
    font-weight: 700;
    letter-spacing: 0.07em;
    color: var(--color-ink-muted);
}

.entry-location-plain {
    font-size: var(--text-xs);
    color: var(--color-ink-muted);
}

/* ── Card body ────────────────────────────────────────────────────────────── */

.entry-card-body {}

.entry-card .entry-title {
    font-family: var(--font-display);
    font-size: var(--text-xl);
    font-weight: 400;
    line-height: var(--leading-snug);
    color: var(--color-ink);
    margin-bottom: var(--space-3);
    transition: color 0.15s;
}

.entry-card-inner:hover .entry-title { color: var(--color-accent); }

.entry-excerpt {
    font-size: var(--text-base);
    line-height: var(--leading-normal);
    color: var(--color-ink-2);
    margin-bottom: var(--space-3);
}

.entry-read-more {
    font-size: var(--text-sm);
    font-weight: 500;
    color: var(--color-accent);
}

/* ── Location & weather badges (single entry page) ───────────────────────── */

.entry-location {
    font-size: var(--text-sm);
    color: var(--color-ink-2);
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
}

.entry-weather {
    font-size: var(--text-sm);
    color: var(--color-ink-2);
}
  • Step 3: Verify feed cards

Open http://100.96.115.96:8081/tracker. Verify:

  • Entry cards with photos show full-bleed 16:9 images

  • Date and location text overlay visible on the photo (white on gradient)

  • Photo zooms subtly on hover (desktop)

  • Entry title in DM Serif Display below the photo

  • Text-only cards show date+location meta row above the title

  • Mobile at 375px: images fill full width, overlay is legible

  • Step 4: Commit

cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast/user
git add themes/intotheeast/templates/tracker.html.twig themes/intotheeast/css/style.css
git commit -m "feat: redesign entry feed cards with full-bleed photo + overlay"

Task 5: Single entry page redesign

Files:

  • Modify: user/themes/intotheeast/templates/entry.html.twig
  • Modify: user/themes/intotheeast/css/style.css (Single entry + Gallery sections)

Interfaces:

  • Consumes: page.title, page.date, page.content, page.header.*, page.media.images

  • Produces: the full entry detail page

  • Step 1: Update entry.html.twig header section

Replace the <article class="entry"> through <h1 class="entry-title"> block with:

<article class="entry">
    {% set hero = null %}
    {% if page.header.hero_image and page.media[page.header.hero_image] is defined %}
        {% set hero = page.media[page.header.hero_image] %}
    {% elseif page.media.images|length > 0 %}
        {% set hero = page.media.images|first %}
    {% endif %}

    {% if hero %}
    <div class="entry-hero">
        <img src="{{ hero.cropResize(1440, 720).url }}" alt="{{ page.title }}" loading="eager">
    </div>
    {% endif %}

    <header class="entry-header">
        <div class="entry-header-meta">
            <time class="entry-date" datetime="{{ page.date|date('Y-m-d') }}">
                {{ page.date|date('l, d F Y') }}
            </time>
            {% if page.header.location_city or page.header.location_country %}
            <p class="entry-location">
                📍
                {% if page.header.location_city %}{{ page.header.location_city }}{% endif %}
                {% if page.header.location_city and page.header.location_country %}, {% endif %}
                {% if page.header.location_country %}{{ page.header.location_country }}{% endif %}
            </p>
            {% endif %}
            {% if page.header.weather_desc or page.header.weather_temp_c %}
            <p class="entry-weather">
                {% if page.header.weather_desc %}
                    {{ weather_icons[page.header.weather_desc] ?? '🌡️' }} {{ page.header.weather_desc }}
                {% endif %}
                {% if page.header.weather_temp_c %}
                    · {{ page.header.weather_temp_c|round }}°C
                {% endif %}
            </p>
            {% endif %}
        </div>

        <h1 class="entry-title">{{ page.title }}</h1>
        <div class="entry-title-rule"></div>
    </header>

(Keep the {% set weather_icons = ... %} block at the top before <article> — move it above the article element, not inside.)

  • Step 2: Replace the Single entry + Gallery CSS sections

Find /* ── Single entry ── through the end of /* ── Lightbox ── section. Replace entirely with:

/* ── Single entry ───────────────────────────────────────────────────────────── */

.entry-hero {
    width: 100%;
    max-height: 480px;
    overflow: hidden;
    margin-bottom: var(--space-8);
}

.entry-hero img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}

.entry-header { margin-bottom: var(--space-8); }

.entry-header-meta {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-3);
    margin-bottom: var(--space-4);
}

.entry-header .entry-date {
    font-size: var(--text-sm);
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--color-ink-muted);
}

.entry .entry-title {
    font-family: var(--font-display);
    font-size: var(--text-2xl);
    font-weight: 400;
    line-height: var(--leading-snug);
    color: var(--color-ink);
    margin-bottom: var(--space-4);
}

@media (min-width: 520px) {
    .entry .entry-title { font-size: var(--text-3xl); }
}

.entry-title-rule {
    height: 1px;
    background: var(--color-border);
    margin-bottom: var(--space-8);
}

.entry-body { margin-bottom: var(--space-10); }
.entry-body p { margin-bottom: 1.1em; font-size: var(--text-md); line-height: var(--leading-normal); color: var(--color-ink-2); }
.entry-body img { max-width: 100%; height: auto; border-radius: var(--radius-sm); }

.entry-footer { border-top: 1px solid var(--color-border); padding-top: var(--space-5); }
.entry-footer a { color: var(--color-accent); text-decoration: none; font-size: var(--text-sm); font-weight: 500; }
.entry-footer a:hover { color: var(--color-accent-hover); }

/* ── Photo gallery ───────────────────────────────────────────────────────────── */

.entry-gallery {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 3px;
    margin-bottom: var(--space-10);
    border-radius: var(--radius-md);
    overflow: hidden;
}

@media (min-width: 520px) {
    .entry-gallery { grid-template-columns: repeat(3, 1fr); }
}

.gallery-thumb {
    background: none;
    border: none;
    padding: 0;
    cursor: pointer;
    display: block;
    aspect-ratio: 1;
    overflow: hidden;
}

.gallery-thumb img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
    transition: opacity 0.15s;
}

.gallery-thumb:hover img,
.gallery-thumb:focus img { opacity: 0.82; }

.gallery-thumb:focus { outline: 2px solid var(--color-accent); outline-offset: 2px; }

/* ── Lightbox ────────────────────────────────────────────────────────────────── */

.lightbox {
    position: fixed;
    inset: 0;
    background: rgba(0,0,0,0.94);
    z-index: 1000;
    display: flex;
    align-items: center;
    justify-content: center;
}

.lightbox[hidden] { display: none; }

.lightbox-img {
    max-width: 92vw;
    max-height: 90vh;
    object-fit: contain;
    border-radius: var(--radius-sm);
    display: block;
}

.lightbox-close,
.lightbox-prev,
.lightbox-next {
    position: absolute;
    background: rgba(255,255,255,0.12);
    border: none;
    color: #fff;
    cursor: pointer;
    border-radius: 50%;
    width: 44px;
    height: 44px;
    font-size: 1.4rem;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: background 0.15s;
}

.lightbox-close { top: 1rem; right: 1rem; }
.lightbox-prev  { left: 0.75rem; top: 50%; transform: translateY(-50%); }
.lightbox-next  { right: 0.75rem; top: 50%; transform: translateY(-50%); }
.lightbox-close:hover,
.lightbox-prev:hover,
.lightbox-next:hover { background: rgba(255,255,255,0.26); }
  • Step 3: Move weather_icons to top of entry.html.twig

Ensure the {% set weather_icons = {...} %} block appears before <article class="entry">, not inside the header block.

  • Step 4: Verify entry page

Open any entry at http://100.96.115.96:8081/tracker/<slug>. Verify:

  • If entry has photos: full-width hero image at top, max-height 480px

  • Title in DM Serif Display, large, below the header meta

  • Thin rule under the title before body text

  • Body text at 18px, comfortable line height

  • Gallery grid below body (2-col / 3-col)

  • "← Back to journal" footer link in teal

  • Step 5: Commit

cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast/user
git add themes/intotheeast/templates/entry.html.twig themes/intotheeast/css/style.css
git commit -m "feat: redesign single entry page with hero image and display typography"

Task 6: Post form UX redesign

Files:

  • Modify: user/pages/02.post/post-form.md (hide lat/lng from UI)
  • Modify: user/themes/intotheeast/templates/post-form.html.twig
  • Modify: user/themes/intotheeast/css/style.css (Post form section)

Interfaces:

  • Consumes: Grav form plugin field rendering (inputs named data[fieldname])

  • Produces: improved mobile posting form

  • Step 1: Hide lat/lng inputs in post-form.md

In user/pages/02.post/post-form.md, add a classes key to the lat and lng field definitions so they can be hidden via CSS:

Change the lat field from:

        -
            name: lat
            label: Latitude
            type: text
            placeholder: 'tap "Get Location" below'

To:

        -
            name: lat
            label: Latitude
            type: text
            placeholder: ''
            classes: gps-hidden-field

Change the lng field from:

        -
            name: lng
            label: Longitude
            type: text
            placeholder: ''

To:

        -
            name: lng
            label: Longitude
            type: text
            placeholder: ''
            classes: gps-hidden-field

Note: Grav form plugin applies the classes value to the field wrapper div. Verify this by inspecting the rendered form HTML after the change.

  • Step 2: Update post-form.html.twig

Replace the entire content of user/themes/intotheeast/templates/post-form.html.twig with:

{% extends 'default.html.twig' %}

{% block content %}
<div class="post-form-wrap">
    <h1>New Entry</h1>
    {% include 'forms/form.html.twig' ignore missing %}

    <div class="form-action-row">
        <button type="button" id="get-location" class="btn-action">📍 Get Location</button>
        <button type="button" id="get-weather" class="btn-action">🌤 Get Weather</button>
    </div>
    <p id="location-status" class="form-status"></p>
    <p id="weather-status" class="form-status"></p>
</div>

<script>
var WMO_MAP = {
    0:'Sunny',1:'Partly cloudy',2:'Partly cloudy',3:'Cloudy',
    45:'Foggy',48:'Foggy',
    51:'Drizzle',53:'Drizzle',55:'Drizzle',56:'Drizzle',57:'Drizzle',
    61:'Rain',63:'Rain',65:'Rain',66:'Rain',67:'Rain',80:'Rain',81:'Rain',82:'Rain',
    71:'Snow',73:'Snow',75:'Snow',77:'Snow',85:'Snow',86:'Snow',
    95:'Thunderstorm',96:'Thunderstorm',99:'Thunderstorm'
};

function getField(name) {
    return document.querySelector('input[name="data[' + name + ']"]');
}

document.getElementById('get-location').addEventListener('click', function() {
    var status = document.getElementById('location-status');
    status.textContent = 'Getting location…';
    if (!navigator.geolocation) {
        status.textContent = 'Geolocation not supported.';
        return;
    }
    navigator.geolocation.getCurrentPosition(function(pos) {
        var lat = pos.coords.latitude.toFixed(6);
        var lng = pos.coords.longitude.toFixed(6);
        var latField = getField('lat');
        var lngField = getField('lng');
        if (latField) latField.value = lat;
        if (lngField) lngField.value = lng;
        status.textContent = '✓ Location captured · ' + lat + ', ' + lng;
        status.classList.add('form-status--ok');
    }, function(err) {
        status.textContent = '✗ Could not get location: ' + err.message;
        status.classList.add('form-status--err');
    });
});

document.getElementById('get-weather').addEventListener('click', function() {
    var status = document.getElementById('weather-status');
    var latField = getField('lat');
    var lngField = getField('lng');
    var lat = latField ? latField.value.trim() : '';
    var lng = lngField ? lngField.value.trim() : '';
    if (!lat || !lng) {
        status.textContent = 'Get location first, then fetch weather.';
        return;
    }
    status.textContent = 'Fetching weather…';
    var url = 'https://api.open-meteo.com/v1/forecast?latitude=' + lat +
              '&longitude=' + lng +
              '&current=temperature_2m,weather_code&temperature_unit=celsius';
    fetch(url)
        .then(function(r) { return r.json(); })
        .then(function(data) {
            var temp = Math.round(data.current.temperature_2m);
            var code = data.current.weather_code;
            var desc = WMO_MAP[code] || 'Cloudy';
            var tempField = getField('weather_temp_c');
            var descField = getField('weather_desc');
            if (tempField) tempField.value = temp;
            if (descField) descField.value = desc;
            status.textContent = '✓ Weather set · ' + desc + ' · ' + temp + '°C';
            status.classList.add('form-status--ok');
        })
        .catch(function() {
            status.textContent = '✗ Could not fetch weather — enter manually if needed.';
            status.classList.add('form-status--err');
        });
});
</script>
{% endblock %}
  • Step 3: Replace the Post form CSS section

Find /* ── Post form ── in style.css. Replace it entirely with:

/* ── Post form ──────────────────────────────────────────────────────────────── */

.post-form-wrap h1 {
    font-family: var(--font-display);
    font-size: var(--text-xl);
    font-weight: 400;
    margin-bottom: var(--space-6);
    color: var(--color-ink);
}

/* Hide GPS coordinate fields — filled by JS, not user-facing */
.gps-hidden-field { display: none !important; }

/* Grav form field wrappers */
.form-field { margin-bottom: var(--space-5); }
.form-label label {
    display: block;
    font-size: var(--text-sm);
    font-weight: 600;
    color: var(--color-ink);
    margin-bottom: var(--space-2);
}

.form-field input[type="text"],
.form-field input[type="email"],
.form-field input[type="datetime-local"],
.form-field textarea {
    width: 100%;
    font-family: var(--font-ui);
    font-size: var(--text-base);
    padding: 0.875rem 1rem;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    background: var(--color-canvas);
    color: var(--color-ink);
    min-height: 44px;
    transition: border-color 0.15s;
    -webkit-appearance: none;
}

.form-field input:focus,
.form-field textarea:focus {
    outline: 2px solid var(--color-accent);
    outline-offset: 1px;
    border-color: var(--color-accent);
}

.form-field textarea { resize: vertical; min-height: 160px; line-height: var(--leading-normal); }

/* Submit button — Grav renders it as .btn or input[type=submit] */
.form-actions input[type="submit"],
.form-actions .btn,
.form-actions button[type="submit"] {
    display: block;
    width: 100%;
    padding: 1rem;
    min-height: 52px;
    background: var(--color-accent);
    color: var(--color-accent-on);
    border: none;
    border-radius: var(--radius-md);
    font-family: var(--font-ui);
    font-size: var(--text-base);
    font-weight: 600;
    cursor: pointer;
    transition: background 0.15s;
    margin-top: var(--space-6);
}

.form-actions input[type="submit"]:hover,
.form-actions button[type="submit"]:hover { background: var(--color-accent-hover); }

/* Action buttons row (Get Location, Get Weather) */
.form-action-row {
    display: flex;
    gap: var(--space-3);
    margin-top: var(--space-5);
}

.btn-action {
    flex: 1;
    padding: 0.75rem var(--space-3);
    min-height: 44px;
    background: var(--color-canvas);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    font-family: var(--font-ui);
    font-size: var(--text-sm);
    font-weight: 500;
    cursor: pointer;
    color: var(--color-ink);
    transition: background 0.15s, border-color 0.15s;
}

.btn-action:hover { background: var(--color-paper); border-color: var(--color-accent); }

/* Status feedback lines */
.form-status {
    font-size: var(--text-sm);
    color: var(--color-ink-muted);
    margin-top: var(--space-2);
    min-height: 1.4em;
}

.form-status--ok  { color: var(--color-accent); }
.form-status--err { color: #B44A2A; }
  • Step 4: Verify post form

Open http://100.96.115.96:8081/post (logged in). Verify:

  • Lat/lng inputs not visible (.gps-hidden-field hidden via CSS)

  • Inputs have rounded corners, proper padding, focus ring in teal

  • "Get Location" and "Get Weather" buttons side by side, same width

  • "Post Entry" (or whatever the submit label is) in teal, full-width

  • Tap "Get Location" → status line shows "✓ Location captured · lat, lng" in teal

  • Mobile at 375px: all inputs and buttons are thumb-friendly

  • Step 5: Commit

cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast/user
git add pages/02.post/post-form.md themes/intotheeast/templates/post-form.html.twig themes/intotheeast/css/style.css
git commit -m "feat: redesign post form — hide GPS fields, teal CTA, better mobile UX"

Task 7: Stats + map + mini-map styling

Files:

  • Modify: user/themes/intotheeast/templates/stats.html.twig
  • Modify: user/themes/intotheeast/css/style.css (Map, Stats, Mini-map sections)

Interfaces:

  • Produces: styled stats page and map page using design tokens

  • Step 1: Update stats page heading

In user/themes/intotheeast/templates/stats.html.twig, replace the <h1 style="..."> tag with:

<div class="stats-page">
    <h1 class="stats-heading">Trip Statistics</h1>

(Remove the inline style attribute from the existing h1.)

  • Step 2: Replace Stats + Map + Mini-map CSS sections

Find /* ── Map page ── through end of /* ── Mini-map on tracker feed ──. Replace all three sections with:

/* ── Map page ───────────────────────────────────────────────────────────────── */

.map-page .site-main { max-width: none; padding: 0; }

.map-container {
    height: calc(100vh - var(--site-header-height));
    width: 100%;
}

.map-empty {
    display: flex;
    align-items: center;
    justify-content: center;
    height: 100%;
    color: var(--color-ink-muted);
    font-style: italic;
}

/* ── Stats page ─────────────────────────────────────────────────────────────── */

.stats-heading {
    font-family: var(--font-display);
    font-size: var(--text-2xl);
    font-weight: 400;
    margin-bottom: var(--space-8);
    color: var(--color-ink);
}

.stats-grid {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: var(--space-4);
    margin-bottom: var(--space-8);
}

.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 {
    display: block;
    font-family: var(--font-display);
    font-size: var(--text-3xl);
    font-weight: 400;
    color: var(--color-accent);
    line-height: 1.1;
    margin-bottom: var(--space-2);
}

.stat-label {
    display: block;
    font-size: var(--text-xs);
    font-weight: 600;
    color: var(--color-ink-muted);
    text-transform: uppercase;
    letter-spacing: 0.07em;
}

.stats-countries {
    font-size: var(--text-sm);
    color: var(--color-ink-2);
    text-align: center;
    line-height: 1.9;
}

.stats-countries-label {
    font-weight: 600;
    display: block;
    margin-bottom: var(--space-2);
    color: var(--color-ink);
    text-transform: uppercase;
    font-size: var(--text-xs);
    letter-spacing: 0.07em;
}

.stats-note {
    font-size: var(--text-xs);
    color: var(--color-ink-muted);
    text-align: center;
    margin-top: var(--space-6);
}

/* ── Mini-map on tracker feed ────────────────────────────────────────────────── */

.feed-map-wrap {
    margin-bottom: var(--space-10);
    border-radius: var(--radius-md);
    overflow: hidden;
    border: 1px solid var(--color-border);
    box-shadow: var(--shadow-sm);
}

.feed-map {
    height: 240px;
    width: 100%;
}

@media (min-width: 520px) {
    .feed-map { height: 300px; }
}

.feed-map-link {
    display: block;
    text-align: right;
    font-size: var(--text-xs);
    font-weight: 500;
    color: var(--color-accent);
    text-decoration: none;
    padding: var(--space-2) var(--space-4);
    background: var(--color-paper);
    border-top: 1px solid var(--color-border);
}

.feed-map-link:hover { color: var(--color-accent-hover); }
  • Step 3: Update Leaflet marker colors

In tracker.html.twig, find the JS that sets marker colors. Update from #0066cc to #1F6B5A and from #0044aa to #155244:

var color = isLatest ? '#155244' : '#1F6B5A';

Also update the polyline color:

L.polyline(latLngs, { color: '#1F6B5A', weight: 3, opacity: 0.7 }).addTo(map);

Do the same in map.html.twig — update any hardcoded #0066cc colors to #1F6B5A.

  • Step 4: Verify stats and map pages

Open http://100.96.115.96:8081/stats. Verify:

  • Heading in DM Serif Display
  • Numbers in DM Serif Display, teal color
  • Stat cards on warm white background with subtle shadow
  • Labels uppercase, muted gray

Open http://100.96.115.96:8081/map. Verify:

  • Map fills viewport below header

  • Markers are teal circles

  • Route line is teal

  • No horizontal scroll or layout issues

  • Step 5: Commit

cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast/user
git add themes/intotheeast/templates/stats.html.twig themes/intotheeast/templates/tracker.html.twig themes/intotheeast/templates/map.html.twig themes/intotheeast/css/style.css
git commit -m "feat: apply design tokens to stats, map, and mini-map"

Task 8: Mobile polish + reduced motion + final QA

Files:

  • Modify: user/themes/intotheeast/css/style.css (add responsive + motion CSS)

Interfaces:

  • Produces: fully responsive, accessible design at 320px1440px viewport widths

  • Step 1: Add reduced-motion support

At the bottom of style.css, append:

/* ── Accessibility ───────────────────────────────────────────────────────────── */

@media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
    }
}

/* Keyboard focus ring: visible on all interactive elements */
:focus-visible {
    outline: 2px solid var(--color-accent);
    outline-offset: 2px;
}
  • Step 2: Verify header on 320px (smallest phone)

Set browser devtools to 320px viewport. Verify:

  • Site title and nav links both visible and not overlapping

  • If title overflows, add this to style.css:

    @media (max-width: 380px) {
        .site-title { font-size: var(--text-md); }
        .site-nav a { padding: var(--space-2); font-size: 0.8rem; }
    }
    
  • Step 3: Verify post form on mobile

On a real phone (or devtools 375px), open /post and verify:

  • Inputs do not trigger zoom on focus (font-size is 1rem ≥ 16px — already set)

  • "Post Entry" button is thumb-reachable (full-width, 52px min height)

  • Get Location / Get Weather buttons are side-by-side, each at least 44px tall

  • Status feedback visible after tapping location/weather

  • Step 4: Cross-page smoke test checklist

Check each of these manually in the browser:

Page Check
/tracker Feed loads, entry cards show hero photos with overlays
/tracker Text-only cards (no photo) show date+location meta above title
/tracker Mini-map renders, teal markers and route
/map Full-height map, teal markers, route polyline
/map Tap marker → popup with date, title, "Read entry →" link
/stats 2×2 grid of stat blocks, teal numbers, correct counts
/tracker/<slug> Hero image full-width at top (if photos exist)
/tracker/<slug> Title in DM Serif Display, large
/tracker/<slug> Photo gallery (2/3-col grid), lightbox opens on tap
/post Lat/lng fields NOT visible
/post Tap Post Entry → success message → redirects to /tracker
Mobile 375px All pages usable without horizontal scroll
Mobile 375px No font size < 14px for readable text
  • Step 5: Final commit
cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast/user
git add themes/intotheeast/css/style.css
git commit -m "feat: add reduced-motion support, keyboard focus, mobile polish"

Task 9: Update documentation

Files:

  • Modify: user/docs/qa-test-plan.md (add visual QA section)

  • Step 1: Add visual design QA section to qa-test-plan.md

Append a new section to user/docs/qa-test-plan.md:

## Visual Design QA — Redesign Checklist

**Design spec:** `user/docs/design/design-spec.md`

### Typography
- [ ] DM Serif Display loads on entry titles, page headings, stats numbers, site title
- [ ] DM Sans loads on all body text, nav, labels, form fields
- [ ] No fallback font (Georgia/system-sans) visible in place of custom fonts

### Colors
- [ ] Page background is warm paper (#F7F5F2), not pure white
- [ ] All links and CTAs use teal (#1F6B5A), not blue (#0066cc)
- [ ] Active nav link is teal
- [ ] Map markers and route polyline are teal

### Entry cards
- [ ] Cards with photos show full-bleed 16:9 image
- [ ] Date + location overlay visible on photo gradient
- [ ] Entry title below photo in DM Serif Display
- [ ] Cards without photos show date/location meta row above title
- [ ] Photo zoom on hover (desktop only)

### Header
- [ ] 3px teal bar at top of header
- [ ] "into the east" title in DM Serif Display
- [ ] Sticky on scroll

### Post form
- [ ] Lat/lng inputs not visible
- [ ] "✓ Location captured" feedback in teal on success
- [ ] Submit button full-width, teal, 52px+ height

### Mobile
- [ ] All interactive elements ≥ 44px touch target
- [ ] No horizontal scroll at 375px
- [ ] iOS: no font-size zoom on input focus
  • Step 2: Commit documentation
cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast/user
git add docs/qa-test-plan.md docs/design/design-spec.md docs/working/plans/2026-06-18-ui-redesign.md
git commit -m "docs: add UI redesign spec, plan, and visual QA checklist"