# Praxis Visual Parity Pass — Design Spec

**Status:** draft
**Date:** 2026-05-20
**Phase:** 1.5 (finish-up after Phase 1 sign-off)

## Context

Phase 1 produced a Zola-based staging site at `http://preview.praxis-am-lienhardplatz.de/` that builds, deploys, and renders all content from the live Grav site. The user signed off Phase 1 acknowledging "MENGE Unterschiede" against the live look — accepting them as follow-up work.

A visual diff against the live HTML (`https://www.praxis-am-lienhardplatz.de/`) revealed ten material gaps that prevent the theme CSS in `sass/_praxis_theme.scss` from applying. The theme CSS itself was lifted verbatim from the Grav theme, so making it work is mostly a question of restoring the markup contracts (class names, element nesting) it expects.

This spec covers that visual-parity pass: closing the markup gaps so the staging site **looks** identical to the live site, while keeping the HTML clean (no Grav-era quirks reproduced).

## Goals

- The staging site, rendered in a browser, is visually indistinguishable from the live site on the four routes that exist on both sides: `/`, `/services`, `/team`, `/kontakt` (live) ↔ `/contact` (staging), `/impressum`.
- The theme CSS in `sass/_praxis_theme.scss` is not modified — markup conforms to it.
- Recurring patterns (section-title, single-page-title, footer) are factored into Tera macros / partials so Phase-2 modernization has a single point of change.
- Footer content (hours, contact, address) lives in `config.toml` `[extra]` as a single source of truth, shared by the footer partial and the `/contact` page.

## Non-Goals (deferred to Phase 2)

- Markup modernization: no semantic HTML5 rewrite, no CSS Grid, no replacement of Bootstrap 3 grid classes.
- URL escaping in templates (`&#x2F;`): cosmetic in view-source, functional in browser. Phase 2.
- Content drift (e.g., hours displayed on home news section vs. footer): content is the user's responsibility, not markup parity.
- Accessibility audit, WCAG fixes, alt-text rewrite.
- Reproducing Grav HTML quirks (double `<p><p>...</p></p>` wrappers, `<td>...</th>` tag mismatches): we keep clean markup.

## Reference

Live HTML snapshots were captured to `/tmp/praxis-diff/live/` (transient — do not check in). The authoritative reference during implementation is what the live site currently serves; if in doubt, re-fetch.

## Architecture

### New macros

**`templates/macros/section_title.html`** — `section_title(title, anchor="")`
Renders the bordered-icon section-title pattern used on home (4×), services (6×), team (2+ ×). When `anchor` is given, wraps the heading in `<div class="anchor" id="{anchor}">`.

```html
{% macro section_title(title, anchor="") %}
<section class="section-title">
  <div class="container text-center">
    {% if anchor %}<div class="anchor" id="{{ anchor }}">{% endif %}
    <h2>{{ title }}</h2>
    {% if anchor %}</div>{% endif %}
    <span class="bordered-icon"><i class="fa fa-circle-thin"></i></span>
  </div>
</section>
{% endmacro %}
```

**`templates/macros/page_title.html`** — `page_title(title)`
Renders the `<section class="single-page-title">` banner that sits between navbar and content on every subpage.

```html
{% macro page_title(title) %}
<section class="single-page-title">
  <div class="container text-center">
    <h2>{{ title }}</h2>
  </div>
</section>
{% endmacro %}
```

### New partial

**`templates/partials/footer.html`** — three-column footer widget block + copyright bar, drawing all values from `config.extra`.

Structure (matching live):
```html
<footer class="footer">
  <div class="footer-widget-section">
    <div class="container text-center">
      <div class="row">
        <div class="col-sm-4 footer-block"> {# Sprechzeiten #} </div>
        <div class="col-sm-4 footer-block"> {# Kontakt: tel/fax/mail #} </div>
        <div class="col-sm-4 footer-block"> {# Hausanschrift + Impressum/Datenschutz #} </div>
      </div>
    </div>
  </div>
  <div class="copyright-section">
    <div class="container clearfix">
      <span class="copytext">Copyright © {current_year} | Praxis am Lienhardplatz</span>
      <ul class="list-inline pull-right">
        <li><a href="/">Home</a></li>
      </ul>
    </div>
  </div>
</footer>
```

`base.html` includes it before `</body>`, after the content block.

### Modified files

**`templates/partials/navbar.html`**
- Replace text brand with `<img src="/images/logo.png" alt="Praxis am Lienhardplatz">`.
- Add Home (`/`) as first menu item.
- Compute active state by comparing `current_path` to each link's `permalink`.

**`templates/partials/head.html`**
- Title logic: home page emits `<title>Praxis am Lienhardplatz - Dr. Oliver Riemann, Dr. Angela Krey, Christian Malyga</title>` (the brand line). Subpages emit `<title>{Page Title} | Praxis am Lienhardplatz - Dr. Oliver Riemann, Dr. Angela Krey, Christian Malyga</title>`.
- Add `<meta name="keywords" content="{{ config.extra.keywords }}">`.

**`templates/index.html`**
- Wrap Aktuelles section with `<section class="news-text ptb-text">` and add `{{ section_title::section_title(title="Aktuelles") }}` heading.
- Add `{{ section_title::section_title(title="Willkommen in unserer Praxis am Lienhardplatz") }}` to the about block.
- Leistungen section: change class from `bg-gray` to `gray-bg`. Add `x-services` to section classes. Change column class from `col-md-4 col-sm-6` to `col-md-6`. Replace flat `<a class="service-card">` with `<div class="thumbnail clearfix"><a><img/></a><div class="caption"><h3><a>title</a></h3><p>desc</p></div></div>`. Add `{{ section_title::section_title(title="Unsere Leistungen") }}`.
- Team-preview: change wrapper from `<div class="col-md-4 col-sm-6 text-center">` to `<div class="col-md-4">` containing `<figure class="thumbnail"><a><img/></a><figcaption class="caption text-center"><h3>{name} - <small>{position}</small></h3></figcaption></figure>`. Change section class to `team ptb-100`. Heading "Ihr Ärzteteam" via macro.

**`templates/section.html`**
- Insert `{{ page_title::page_title(title=section.title) }}` after the navbar (before content).
- Each child page rendered as `<section class="text ptb-text">{{ section_title::section_title(title=page.title, anchor=page.extra.anchor) }} <div class="container"><div class="row">{{ page.content | safe }}</div></div></section>`.

**`templates/page.html`**
- Same treatment as section.html for the page title banner; otherwise content goes into a `<section class="ptb-100"><div class="container">{{ page.content | safe }}</div></section>` block.

**`templates/base.html`**
- Add `{% include "partials/footer.html" %}` before `</body>`.

### Config additions

**`config.toml`** `[extra]`:
```toml
email = "info@praxis-am-lienhardplatz.de"
address_street = "Kaiserstraße 23"
address_zip_city = "42329 Wuppertal"
keywords = "Arzt, Hausarzt, Allgemeinmedizin, Innere Medizin, Internist, Kardiologie, Praxis, Gemeinschaftspraxis, Vohwinkel, Lienhardplatz, Krey, Riemann, Malyga, Corona, COVID-19, Corona-Test"

[[extra.opening_hours]]
label = "Montag-Freitag"
time = "08:00-12:00"

[[extra.opening_hours]]
label = "Montag, Dienstag, Donnerstag"
time = "15:00-18:00"
```

Phone, fax, doctors, practice_name, address are already present in `[extra]` from Phase 1.

### Content changes

**`content/contact.md`**
- Remove the hardcoded address/phone/fax/email block from markdown — the contact template will render those from `config.extra` for single-source-of-truth with the footer.
- Keep Sprechzeiten and Bereitschaftsdienst text.

For the contact page to render config values inline, the simplest approach: switch its template to a dedicated `templates/contact.html` (uses `page_title` macro + a structured layout pulling from `config.extra` + `page.content` for the Bereitschaftsdienst paragraph). The `template = "page.html"` line in frontmatter changes to `template = "contact.html"`.

**`content/_index.md`**
- Fix the table tag mismatch: `<td>...</th>` → `<td>...</td>` in the Aktuelles news block.

### Assets

**`static/images/logo.png`** ← `orig/user/themes/praxis/img/Logo_Praxis_white_H110.png` (copy verbatim — PNG with alpha, no resize).

## Per-Route Definition of Done

After implementation, each route's rendered HTML should match the live equivalent on these checks (visual, not byte-equal):

| Route | Live | Staging must show |
|---|---|---|
| `/` | logo+nav, carousel, Aktuelles w/ heading, Willkommen w/ heading, Leistungen 2-col w/ thumbnails, Ärzteteam w/ figures, footer | same |
| `/services` | single-page-title "Leistungen", 6 sub-sections each with section_title + anchor + image + text, footer | same |
| `/team` | single-page-title "Team", doctor/employee sub-sections w/ section_title + anchors, footer | same |
| `/contact` | single-page-title "Kontakt", address/hours pulled from config, Bereitschaftsdienst block, footer (Sprechzeiten in footer ↔ Sprechzeiten on page driven from same config keys) | same |
| `/impressum` | single-page-title "Impressum", Impressum+Datenschutz sub-sections w/ section_title + anchors, footer | same |

A successful pass means: visually open both URLs side by side in the browser, no obvious differences in layout, typography, spacing, or color blocks.

## Risks & Mitigations

- **Risk:** Theme CSS may have selectors that depend on Grav-specific DOM structure we don't reproduce exactly (e.g., wrapper divs we collapsed).
  **Mitigation:** Compare rendered output, not source. If a visual element is broken, grep the SCSS for the relevant selector and adapt the markup minimally.

- **Risk:** Footer values diverging between config and `/contact` page if we forget to switch contact to template-driven rendering.
  **Mitigation:** This spec explicitly converts contact.md to use `templates/contact.html`. Implementer must not leave hardcoded values in the markdown.

- **Risk:** Active-nav-link detection in Tera using `current_path` may be off by trailing slashes.
  **Mitigation:** Normalize comparison (`current_path == link.path` or both with trailing `/`); test all four nav routes after change.

## Out of Scope

Anything not listed above. Specifically:
- No new content. No deletions of content.
- No new pages or routes.
- No SSL/deploy automation changes (still rsync manual).
- No git workflow changes.

## Open Questions

None at design time. Implementation may surface CSS-selector mismatches that need a tiny markup tweak beyond the spec; record those as small commits during the implementation pass.
