# 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 2–8

- [ ] **Step 1: Create tokens.css**

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

```css
: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:

```twig
<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**

```bash
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:

```css
*, *::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:

```css
.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**

```bash
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:

```twig
<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:

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

```bash
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:

```twig
<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:

```css
/* ── 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**

```bash
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:

```twig
<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:

```css
/* ── 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**

```bash
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:
```yaml
        -
            name: lat
            label: Latitude
            type: text
            placeholder: 'tap "Get Location" below'
```

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

Change the lng field from:
```yaml
        -
            name: lng
            label: Longitude
            type: text
            placeholder: ''
```

To:
```yaml
        -
            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:

```twig
{% 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:

```css
/* ── 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**

```bash
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:

```twig
<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:

```css
/* ── 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`:

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

Also update the polyline color:
```js
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**

```bash
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:

```css
/* ── 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:
  ```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**

```bash
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`:

```markdown
## 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**

```bash
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"
```
