Design direction: Field Notes — DM Serif Display + DM Sans typography, deep teal (#1F6B5A) accent, full-bleed entry card photos with overlay. 9-task implementation plan covering tokens, header, feed, entry page, post form, stats/map, mobile polish, and visual QA checklist. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
46 KiB
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 atuser/pages/ - Design tokens source of truth:
user/themes/intotheeast/css/tokens.css - Accent color:
#1F6B5A(deep teal) — used everywhere#0066ccis 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 2–8
-
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 3–8.
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 1–41) 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 +
'¤t=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-fieldhidden 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 320px–1440px 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/superpowers/plans/2026-06-18-ui-redesign.md
git commit -m "docs: add UI redesign spec, plan, and visual QA checklist"